简介面向TI TMS320F280049C DSP开发者的PWM相移控制示例工程基于C28x内核与driverlib驱动库重点演示PWM模块的相位偏移生成、计数器与比较寄存器配置、死区时间设置、中断事件响应等关键操作可直接导入CCS进行编译调试适用于电机驱动、数字电源、工业自动化等实时控制场景。压缩包共含33个文件以C源码、汇编启动代码、cmd链接脚本、driverlib.lib库、CCS工程配置文件及编译调试产物为主整体318KB结构完整覆盖从复位引导、外设初始化到程序链接分配的开发链路。目前已有1030人学习下载对于希望掌握F280049C PWM应用的开发者具有不错的参考价值。通过学习这份工程能够理清PWM模块从寄存器配置到实际输出波形的完整流程理解C28x DSP的存储器组织与链接命令写法同时熟悉CCS环境下的编译、烧写和调试方法。项目内还包含readme说明与调用driverlib.lib的典型用法便于对照实践并为后续PID控制等复杂算法扩展提供基础。1. 拿到一个 PWM 工程文件先别急着烧板子做电机控制或者数字电源的工程师第一次拿到 TMS320F280049C 的 PWM 例程时多半会被 EPWM 模块里那一堆寄存器名字吓一跳。TBPRD、CMPA、DBRED、TBPHS、AQCTLA……这些缩写看起来像一个黑匣子但 PWM 恰恰是 F280049C 上最核心、也最不能写错的外设占空比不对是小事死区不够或者故障保护没配置板上的 MOS 管会直接冒烟。这篇文章顺着一个常见 PWM 工程文件的组织方式把时钟链路、初始化顺序、死区和相位配置讲清楚再列出几种示波器上最常见的故障现象和排查思路适合刚从普通单片机转过来、第一次接触 C2000 实时控制芯片的开发者。2. 时基时钟从哪来先看懂 PWM 频率的分频链路做电机控制的同事第一次拿 F280049C 的 PWM 例程时问得最多的一个问题就是为什么代码里写的是 20kHz示波器上看到的却是 10kHz 甚至是 5kHz绝大多数情况下都不是周期寄存器写错而是时基时钟没有按预期工作。EPWM 模块的时基计数器每跳一步需要多长时间完全由两级分频和计数模式共同决定链条上任何一环理解错了输出频率都会对不上账。2.1 从 100MHz 主频到 TBCLK两级分频不能省TMS320F280049C 的内核最高跑到 100MHz这个频率是系统时钟 SYSCLK。EPWM 模块的输入时钟 EPWMCLK 默认从 SYSCLK 而来但实时控制里通常不会让时基计数器直接以 100MHz 步进因为这样算出来的周期值会太小而且死区时间、相位差这些时间量会很难换算。EPWM 内部先做第一级分频 EPWMCLKDIV再做第二级分频 CLKDIV两级分频相乘后取倒数才是时基时钟 TBCLK 的周期。用公式表示就是f_TBCLK f_EPWMCLK / (EPWMCLKDIV × CLKDIV)分频参数常见取值作用所在寄存器EPWMCLKDIV1 / 2 / 4 / 8 / 16 / 32 / 64 / 128EPWM 模块级时钟分频TBCTL 高两位字段CLKDIV1 / 2 / 4 / 8 / 16 / 32 / 64 / 128时基计数器预分频TBCTL 中字段TBPRD1 ~ 65535时基周期匹配值TBPRD 寄存器16 位TBCTR0 ~ 65535当前计数值TBCTR 寄存器16 位这两个分频器的选值直接决定了 TBCLK 和后面所有时间量的换算关系。举个例子如果 TBCLK 100ns那么死区延迟计数写 10 就是 1us相位差计数写 33 就是 3.3us如果把分频理解错了一档同样的数字在示波器上看会差一倍甚至更多。所以我把时钟链路放在第一个小节来讲后面所有寄存器的数值计算都要回到这里来对账。2.2 三种计数模式三种频率公式时基计数器支持向上计数、向下计数、向上-向下计数三种模式。频率公式很容易在手册里查到但很多人会忽略计数从 0 开始这个细节导致周期值差 1。向上计数计数器从 0 递增到 TBPRD然后回到 0。一个完整周期经过 TBPRD 1 个 TBCLK所以 f TBCLK / (TBPRD 1)。向下计数计数器从 TBPRD 递减到 0然后回到 TBPRD。同样经历 TBPRD 1 个 TBCLK公式与向上一致。向上-向下计数计数器从 0 递增到 TBPRD再递减回 0一个周期经历 2 × TBPRD 个 TBCLK所以 f TBCLK / (2 × TBPRD)。向上和向下模式虽然公式相同但波形在时间轴上的位置不一样多数电机控制会选用向上-向下计数因为波形中心对齐、谐波小数字电源则更常用向上计数实现简单、相位容易对齐。2.3 一个算错频率的典型例子假设主频 100MHz、EPWMCLKDIV 1、CLKDIV 10那么 TBCLK 10MHz。想在向上计数模式下输出 20kHzTBPRD 1 10MHz / 20kHz 500所以 TBPRD 499。如果手滑把 TBPRD 写成了 500实际频率就变成 10MHz / 501 ≈ 19.96kHz。示波器上肉眼可能看不出来但如果你用频率计去量、或者多个 PWM 通道做同步这个误差就会被放大。另一个容易犯的错是把向上计数和向上-向下计数的公式混用。同样的 20kHz向上-向下计数时 TBPRD 要写 250不是 499。注意TBPRD 是 16 位寄存器最大值 65535。如果 TBCLK 很高、目标频率又很低TBPRD 可能溢出。频率低于 1kHz、时基又没分频时尤其要小心。3. 跑通一个最小 PWM 工程初始化代码逐行拆解很多刚接触 F280049C 的开发者会直接去找例程里的 PWM 配置复制一段过来就下板结果引脚没输出、频率不对、占空比不动然后开始怀疑芯片坏了。实际上一个最小的 EPWM 输出需要四步走齐使能外设时钟、配置 GPIO 复用、设置时基、设置动作限定器。缺任何一步示波器上都是零。3.1 打开外设时钟把 GPIO 切到 EPWM 复用功能第一步不是配置 PWM 寄存器而是让 EPWM 模块先通电。很多初学者在初始化里漏掉了外设时钟使能这一步导致后面所有寄存器写完没有任何反应。另外F280049C 的 GPIO0 和 GPIO1 默认是普通输入输出口必须通过引脚复用配置把这两个引脚切到 EPWM1A 和 EPWM1B 功能上PWM 信号才能从对应引脚送出来。下面这段是典型的初始化开头#include board.h #include epwm.h #define PWM_TBCLK_MHZ 10 // 时基时钟 10MHz #define PWM_FREQ_KHZ 20 // 目标 PWM 频率 20kHz #define PWM_TBPRD (PWM_TBCLK_MHZ * 1000 / PWM_FREQ_KHZ - 1) // 499 #define PWM_CMPA_HALF (PWM_TBPRD / 2) // 249 void EPWM1_Init(void) { // 1) 打开 EPWM1 外设时钟这一步漏了就全白写 SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_EPWM1); // 2) 引脚复用GPIO0 - EPWM1AGPIO1 - EPWM1B GPIO_setPinConfig(GPIO_0_EPWM1_A); GPIO_setPinConfig(GPIO_1_EPWM1_B); GPIO_setPadConfig(GPIO_0, GPIO_PIN_TYPE_STD); GPIO_setPadConfig(GPIO_1, GPIO_PIN_TYPE_STD); }这段代码里最关键的是引脚复用配置。普通单片机里输出 PWM 可能只要把 GPIO 设成输出模式就行但 C2000 系列的外设信号必须通过 MUX 复用表选通GPIO_setPinConfig 干的就是这件事。GPIO_setPadConfig 设置的是引脚电气特性标准模式下推挽输出、不上拉不下拉适合接数字输入或驱动小功率负载。注意不同封装、不同引脚上 EPWM 的复用编号不一样。比如同一个 EPWM1A 信号在有的封装上映射到 GPIO0在另一封装上可能映射到 GPIO15必须对照参考手册的引脚复用表确认。3.2 配置时基周期、初值、计数模式时基部分的配置顺序很重要我的习惯是先配分频、再配周期、最后配计数器初值。如果先写 TBPRD 再改分频TBCLK 频率会变算好的周期值等于白写。下面这段是时基配置// 3) 时基时钟 100MHz / 1 / 10 10MHz EPWM_setClockPrescaler(EPWM1_BASE, EPWM_CLOCK_DIVIDER_1, // EPWMCLKDIV 1 EPWM_PRESCALE_DIVIDER_10); // CLKDIV 10 // 4) 周期值向上计数10MHz / (499 1) 20kHz EPWM_setTimeBasePeriod(EPWM1_BASE, PWM_TBPRD); // 5) 计数器清零等待启动 EPWM_setTimeBaseCounter(EPWM1_BASE, 0); // 6) 向上计数模式 EPWM_setTimeBaseCounterMode(EPWM1_BASE, EPWM_COUNTER_MODE_UP);EPWM_setClockPrescaler 的第一个参数 EPWM_CLOCK_DIVIDER_1 是 EPWMCLKDIV第二个参数 EPWM_PRESCALE_DIVIDER_10 是 CLKDIV。两个参数容易传反传反的结果是分频倍数不变但概念上完全错位后面一旦要调整其中一个就会发现自己改错了对象。3.3 动作限定器CMPA 在哪个边沿触发哪个动作时基只是把计数器跑起来真正决定输出波形的是一组动作限定器Action Qualifier。动作限定器的作用是在计数器等于某个特定值时把输出置高、置低或翻转。EPWM 的每个输出通道 A/B 都有四个触发点CTR 0、CTR PRD、计数器递增时 CMPA/CMPB 匹配、计数器递减时 CMPA/CMPB 匹配。// 7) 比较值CMPA 249向上计数时约 50% 占空比 EPWM_setCounterCompareValue(EPWM1_BASE, EPWM_COUNTER_COMPARE_A, PWM_CMPA_HALF); // 8) 影子装载计数器归零时一次性装载比较值 EPWM_setCounterCompareShadowLoadMode( EPWM1_BASE, EPWM_COUNTER_COMPARE_A, EPWM_COMP_LOAD_ON_CNTR_ZERO); // 9) 动作限定器 // CTRPRD 时输出低电平 // 递增计数匹配 CMPA 时输出高电平 // CTR0 和递减匹配 CMPA 时不动 EPWM_setActionQualifierAction( EPWM1_BASE, EPWM_AQ_OUTPUT_A, EPWM_AQ_OUTPUT_NO_ACTION, // CTR 0 EPWM_AQ_OUTPUT_LOW, // CTR PRD EPWM_AQ_OUTPUT_HIGH, // 递增时 CMPA 匹配 EPWM_AQ_OUTPUT_NO_ACTION); // 递减时 CMPA 匹配这段配置产生的波形是从 0 计数到 CMPA 之前输出低电平到 CMPA 时拉高继续计数到 TBPRD 时拉低然后回到 0 开始下一周期。只要 CMPA 的值在 0 到 TBPRD1 之间占空比就是近似线性的。CMPA 写 249、TBPRD 写 499高电平区间是 250 个计数周期占空比约 50.1%和理论值几乎一致。3.4 影子装载周期中断里更新占空比的前提很多做电源的工程师第一次遇到“占空比改了没反应”的问题时都是从影子装载开始的。EPWM 的比较值寄存器默认是带影子的你写入 CMPA真正生效的寄存器不会立刻更新而是要等某个装载事件触发。这个设计是为了避免占空比在一个周期中间突然跳变导致输出波形出现窄脉冲或毛刺。装载时机触发事件适用场景立即装载写入后立刻生效静态配置、调试计数器归零时TBCTR 0周期中断内更新最常见计数器到周期时TBCTR TBPRD对称 PWM 的同步点在我上面的配置里影子装载设为“计数器归零时”也就是每个 PWM 周期开始的位置才把新占空比送入硬件。配合后文会讲到的周期中断可以实现每个周期都更新占空比这也是 SVPWM 和数字电源控制的基本前提。3.5 一个完整的 main 函数骨架把上面的初始化串起来main 函数的结构大概是这样的void main(void) { // 初始化系统时钟、看门狗等基础配置 Device_init(); // 初始化 EPWM1 输出 EPWM1_Init(); // GPIO 和 EPWM 配置完成后开启全局中断如果需要 // 中断初始化放在 EPWM 配置之后 while (1) { // 主循环不参与实时占空比修改 // 实时控制放在 EPWM 周期中断里做 } }这个结构在现网项目里很常见主循环只负责显示、通信、参数处理占空比的实时更新全部放到中断里。如果你试图在主循环里用延时翻转 GPIO 来模拟 PWMCPU 根本无法在微秒级稳定定时这也是单片机做不了高质量电源/电机控制的原因——EPWM 是硬件自动跑的数不是软件翻转出来的。4. 从单路方波到互补驱动死区、相位与故障保护单路 PWM 只是第一步真正让 F280049C 脱颖而出的是多通道协调能力。电机驱动需要互补输出而且要确保上下桥臂不会同时导通多路交错电源需要相位精确错开大功率装置需要硬件级故障保护。这几件事全部由 EPWM 模块内部的死区单元、相位同步单元和故障区TZ完成不需要 CPU 介入。4.1 死区参数怎么算DBRED 和 DBFED 的单位是 TBCLK互补输出就是把同一路 PWM 同时送给上桥和下桥驱动但两个信号不能同时为高否则母线会直通短路。死区模块的作用是在上升沿或下降沿插入一段延迟让上桥关断之后下桥才开始导通。F280049C 的死区延迟寄存器 DBRED 和 DBFED 以 TBCLK 周期为单位所以算死区时间先要回到第 2 章的时钟链路。举个例子TBCLK 10MHz想要 1us 死区那么 DBRED 10MHz × 1us 10。我见过很多人在这一步把死区设得特别大比如 100 个计数在 10MHz 时基下就是 10us对 20kHz 的 PWM 来说占掉了整整 20% 的周期波形直接畸变。死区时间不是越大越安全它要大于功率管的关断延迟又要尽量小否则输出波形失真、效率下降。死区参数计算方式典型值DBRED上升沿延迟 死区时间 × TBCLK101us 10MHzDBFED下降沿延迟 死区时间 × TBCLK101us 10MHz死区模式仅上升沿 / 仅下降沿 / 双向双向最常用配置死区模块的典型代码如下// 死区上升沿延迟 10 个 TBCLK 1us EPWM_setRisingEdgeDelayCount(EPWM1_BASE, 10); EPWM_setFallingEdgeDelayCount(EPWM1_BASE, 10); EPWM_setDeadBandDelayMode(EPWM1_BASE, EPWM_DB_RED, true); EPWM_setDeadBandDelayMode(EPWM1_BASE, EPWM_DB_FED, true); EPWM_setDeadBandMode(EPWM1_BASE, EPWM_DB_RED_FED_ENABLE); // EPWM1A 高有效EPWM1B 低有效互补输出 EPWM_setDeadBandPolarity(EPWM1_BASE, EPWM_DB_POLARITY_RED_ACTIVE_HIGH, EPWM_DB_POLARITY_FED_ACTIVE_LOW);注意死区配置必须要放在时基和动作限定器都配置完之后。死区模块是从 A 路输入信号取原始的 PWM 波形再分别生成延迟后的 A 和互补的 B中间任何一步没配置好看波形时都是乱的。4.2 互补输出只有一路使能还不够A/B 极性配合很多人以为把死区配好了EPWM1A 和 EPWM1B 就自动互补了。不是的你还要决定 EPWM1B 的极性。上面代码里把 B 路设成“下降沿有效”实际输出就是 A 的反相信号再叠加上死区延迟才是真正安全的互补波。如果你忘了配极性B 路可能会和 A 路同相上下桥同时导通一上电就炸。调试互补输出有个笨但有效的办法先把死区设成 0看 A/B 波形是不是完全反相确认反相后再逐步加大死区直到你看到两个信号之间出现清晰的“窗口”空隙。这个顺序能帮你区分是死区问题还是极性配置问题。4.3 相位同步TBPHS 与 SYNC 信号多路 PWM 场景里比如三相逆变器或者多相交错电源第二路 PWM 要相对第一路错开固定相位。F280049C 的 EPWM 模块之间通过 SYNC 信号级联主模块在计数器归零时发出同步脉冲从模块收到脉冲后把 TBPHS 寄存器里的相位值装载到计数器里实现相位对齐。相位值换算和相位角度有关还是以 TBPRD 499、TBCLK 10MHz 的向上计数模式为例360 度对应 500 个计数周期那么 120 度就是 500 × 120 / 360 ≈ 166。配置成从模块的方法如下// EPWM1 为主模块计数器归零时发同步脉冲 EPWM_setSyncOutPulseMode(EPWM1_BASE, EPWM_SYNC_OUT_PULSE_ON_COUNTER_ZERO); // EPWM2 为从模块相位滞后 120 度 EPWM_setPhaseShift(EPWM2_BASE, 166); EPWM_enablePhaseShiftLoad(EPWM2_BASE, true);相位同步配置里最容易犯的错是把 TBPHS 直接写成角度值比如写 120。这里存的是计数值不是角度必须先按照时基和周期换算。另外一个坑是主模块和从模块必须使用同一个时基时钟频率如果两个通道的分频不一样相位会随时间漂移无法保持稳定。4.4 TZ 故障保护硬件过流时直接强制输出TZ 模块的全称是 Trip Zone相当于 PWM 硬件输出的“急停开关”。它监控一组故障信号源一旦触发可以立刻把 EPWM 输出强制为高、低或高阻态整个过程不需要 CPU 参与。这个特性在大功率电源里至关重要过流信号从比较器出来到 PWM 关闭必须是硬件级延迟而不是等中断里软件去改寄存器。// TZ1 故障输入触发时EPWM1A 和 EPWM1B 强制输出低电平 EPWM_setTripZoneAction(EPWM1_BASE, EPWM_TZ_ACTION_LOW, // EPWM1A EPWM_TZ_ACTION_LOW); // EPWM1B EPWM_enableTripZoneSignals(EPWM1_BASE, EPWM_TZ_SIGNAL_OSHT1);TZ 配置有两个容易踩的坑。一是故障信号极性不对有的板子过流信号是低有效有的高有效配置反了就会出现一上电就关断、或者过流了纹丝不动。二是故障发生后没有软件清标志PWM 一直处于锁定状态必须手动清除 TZ 标志才能重新输出。我在后面第 5 章的排查里会再展开这两个问题。5. PWM 输出异常排查示波器上的五个翻车现场这章写一些我调 PWM 时亲眼见过、也帮别人查过的故障现象。每一条都按“现象 → 原因 → 解决”的顺序整理示波器上看到类似波形时可以对照着查。5.1 现象PWM 引脚一直是低电平最常见的问题是引脚复用没配好。F280049C 的每一位 GPIO 出厂时默认是普通 IO即使你把 EPWM 的所有寄存器都配对了引脚没有切到复用功能输出就是低电平。排查时先看 GPIO 控制寄存器里的 MUX 字段确认是不是选到了对应的 EPWM 功能。另一个可能原因是外设时钟没开EPWM 模块整个没跑起来寄存器写什么都不生效。解决方式先确认 SysCtl_enablePeripheral 已调用再用示波器量引脚电平如果确实低电平接着查 GPIO 复用配置。用一个最简单的例程——只配 EPWM1A 输出固定占空比——排除其他因素。5.2 现象频率和计算值差一倍示波器上看到 10kHz代码里算出来是 20kHz这种“减半”现象几乎都是计数模式搞错了。向上-向下计数模式一个完整周期要走 2 × TBPRD 个 TBCLK如果你按向上计数公式算周期值又用了向上-向下模式频率就会减半。解决方式很简单把计数模式改成向上计数或者把 TBPRD 减半。另一种情况是分频参数配置错位EPWMCLKDIV 和 CLKDIV 传反。两个参数都是 1 到 128 的分频值传反后表面看总分频倍数没变但以后调试时会非常迷惑。建议在代码注释里把这两个参数对应的 TBCLK 频率写清楚。5.3 现象占空比更新了波形不变占空比改了但示波器上纹丝不动最典型的原因是影子装载没触发。如果你用的是“计数器归零装载”模式而 PWM 模块还没开始工作、计数器停在某个值上你写进去的 CMPA 会一直待在影子寄存器里永远不会进入工作寄存器。解决方式是先确认时基已经在跑再查看状态寄存器里的装载标志是否被清除。另外有些代码会同时修改 TBPRD 和 CMPA比如做软起动、调频调压。此时必须让两个装载点同步否则 TBPRD 装了新值、CMPA 还在用旧值波形会出现一个周期突然变窄或变宽。5.4 现象互补输出上电瞬间直通这是最危险的一种现象上电瞬间 EPWM1A 和 EPWM1B 同时出现高电平功率管直接炸掉。原因通常是两个一是死区模块配置太晚上电时死区还没生效二是 GPIO 在上电复位期间的默认状态和 PWM 逻辑不匹配输出管被短暂导通。解决方式有两个组合用最稳。第一把 TZ 模块配置成上电后默认强制低电平等所有 PWM 寄存器配好、死区生效后再由软件清除 TZ 标志释放输出。第二在硬件上把上下桥驱动的使能引脚用电阻下拉确保芯片复位期间驱动芯片不工作。5.5 现象输出波形边沿毛刺多、上升沿有回勾波形边沿出现毛刺很多人的第一反应是继续调代码寄存器其实八成的边沿毛刺是硬件布局问题。PWM 引脚走线过长、门极驱动电阻过大或过小、地回路不好都会在高低电平切换瞬间产生振铃。代码上能做的一是检查 GPIO 的驱动强度配置输出能力不够就换更强的驱动二是在门极驱动信号上串联一个小电阻减缓边沿斜率。如果波形只是偶尔出现一个窄毛刺还要留意是不是电流探头或示波器探头地线太长。用最短的地弹簧接地排除测量引入的假毛刺不要上来就怀疑芯片。6. 从固定占空比到 SVPWM把代码包接进电机控制链路固定占空比的 PWM 只能让电机转起来真正做 FOC磁场定向控制时需要的是空间矢量调制SVPWM。SVPWM 的输入是两相静止坐标系下的电压矢量输出是三相逆变器六个开关管的占空比组合。EPWM 部分要做的事其实很纯粹在正确的时刻把计算出来的新占空比送到 CMPA/CMPB并且保证装载点在一个周期的固定位置。这个工作通常放在 EPWM 周期中断里完成中断触发点选计数器归零正好对应 PWM 周期的起点。__interrupt void EPWM1_ISR(void) { // 从 FOC 计算得到新的占空比 uint16_t dutyA svpwm_calc_dutyA(); uint16_t dutyB svpwm_calc_dutyB(); // 更新比较值影子装载在计数器归零时生效 EPWM_setCounterCompareValue(EPWM1_BASE, EPWM_COUNTER_COMPARE_A, dutyA); EPWM_setCounterCompareValue(EPWM1_BASE, EPWM_COUNTER_COMPARE_B, dutyB); // 清除中断标志重新使能 EPWM_clearEventTriggerInterruptStatus( EPWM1_BASE, EPWM_EVENT_TRIGGER_INTERRUPT_STATUS_ETFLG); Interrupt_clearACKGroup(INTERRUPT_ACK_GROUP3); }从 SVPWM 映射到 CMPA/CMPB 的公式本身不复杂先把目标电压矢量转换成三相占空比再按扇区做一次平移。EPWM 侧的难点在于时序预算——20kHz 的 PWM 周期是 50us你需要在这段时间里完成电流采样、坐标变换、PI 调节和占空比输出。如果全部堆在中断里做就很容易超时。我的习惯是把 FOC 算法拆成两半一半在 EPWM 中断里做另一半放主循环里做利用双缓冲让数据不打架。我自己第一次调 F280049C 的 PWM 时死区设得太短一个瞬间直通就烧掉了一路驱动后来学到的教训是上电首次输出前先让 TZ 把输出锁住确认示波器波形全部正常再释放。这个习惯留到现在也替我挡过不少次故障。希望这篇笔记里的公式、代码和排查思路能帮到你祝你的板子一次点亮。本文还有配套的精品资源点击获取