简介:面向DSP开发初学者及电机驱动、电源变换工程师,这套基于TI TMS320F28069数字信号处理器的工程包实现了液晶屏显示与可调频率脉宽调制(PWM)输出,通过按键或外部输入调整PWM频率,并在液晶屏上实时刷新数值,有助于快速理解C2000系列外设配置与联调方法。压缩包共含126个文件,整体仅320KB,其中38个头文件定义寄存器与数据结构,25个C源文件实现系统控制、LCD驱动及PWM逻辑,另有链接脚本、汇编启动文件与编译辅助文件,构成从上电初始化、时钟配置到外设中断响应的完整工程骨架。已有321人参与学习,适合作为轻量级参考样例。资源内提供R61509控制器液晶屏驱动、PWM频率动态调节代码、默认中断服务程序以及必要的系统初始化框架,还附带OUT可执行文件与GEL调试脚本,方便对照寄存器配置和实际输出波形进行排错,可有效缩短基于F28069的显示与调频项目开发周期。
1. 一块F28069就能做的可调频率PWM信号源
做电机驱动、开关电源或者逆变器调试时,手上最缺的往往不是示波器,而是一个频率、占空比都能随手改的PWM信号源。用单片机定时器翻转GPIO虽然能出波形,但频率一高CPU就被占死,相位和占空比也没法精细控制。TMS320F28069液晶显示可调频率PWM输出这套方案,本质是把C2000系列DSP里硬件化的ePWM模块用起来:频率由时基寄存器决定,占空比由比较寄存器决定,CPU只负责响应按键和刷新液晶屏,输出波形完全交给硬件。适合实验室做信号源、电源开发前期验证、或者给学生做电机控制实验的起点。整套系统一块板加一个1602液晶就能跑通,核心代码只有几百行。
2. 先看ePWM:频率从哪来,时基寄存器怎么设
2.1 为什么要用ePWM而不是定时器翻转GPIO
很多从8位单片机转过来的开发者第一反应是用定时器中断翻转IO口:中断里对计数值做判断,到了就翻转电平。这个办法在F28069上也能跑,但有一个绕不过去的代价——中断响应延迟。CPU在90MHz主频下确实很快,但一旦同时要处理按键扫描、液晶刷新、通信协议,中断翻转的抖动就会直接体现在PWM的周期上,示波器上看到的是频率在飘,而不是稳定在设定值。
F28069的ePWM模块是完全硬件化的:时基计数器自己跑,计数到周期值后自动回零,比较值匹配时由硬件动作限定器(Action Qualifier)直接改引脚电平,整个过程CPU零参与。开发者要做的只是把目标频率换算成时基计数器的周期值TBPRD,把占空比换算成比较值CMPA,然后一次性写入寄存器。之后CPU可以去刷LCD、读按键,波形依然稳稳地输出。这也是C2000系列做电机控制、数字电源时的标准用法。
2.2 时基寄存器配置:先算出TBCLK和TBPRD
ePWM模块的时钟来自系统时钟EPWMCLK,F28069在90MHz主频下EPWMCLK默认也是90MHz。这个时钟要先经过HSPCLKDIV和CLKDIV两级分频,才得到真正驱动时基计数器的TBCLK:
TBCLK = EPWMCLK / (HSPCLKDIV × CLKDIV)TBCTL寄存器里有两个分频位段:HSPCLKDIV是高位分频,可选1/2/4/8等;CLKDIV是低位分频,可选1/2/4直到128。分频系数越大,TBCLK越低,同样16位的TBPRD能表达的周期就越长,也就是最低可输出频率越低,但代价是频率调节分辨率变粗。
我习惯先定TBCLK再选分频:把HSPCLKDIV设为2,TBCLK降为45MHz,这样大部分常用频段(300Hz到几百kHz)都能落在一个合理的TBPRD范围里。计数器工作在增减计数模式时,一个完整PWM周期等于计数器从零加到TBPRD再减回零,所以输出频率为:
fPWM = TBCLK / (2 × TBPRD)反过来,给定目标频率就算周期寄存器值:
TBPRD = TBCLK / (2 × fPWM)这里有个细节很多新手会忽略:TBPRD是整数,除完会有余数,实际输出频率会略低于目标值。比如TBCLK=45MHz,想要2500Hz,算出来TBPRD=9000,实际频率恰好2500Hz;想要3333Hz,算出来TBPRD=6750.67,取整为6750后实际频率是3333.33Hz,误差不到万分之一,大多数场景可接受。但如果目标频率取得很偏,取整误差会被放大,后面第四章会专门说怎么让液晶显示值跟真实输出值对得上。
2.3 ePWM初始化:最小可用代码
下面这段代码配置EPWM1A引脚输出PWM,工作模式为增减计数,初始频率1kHz、占空比50%。注意GPIO复用配置的顺序:必须先使能ePWM模块时钟,再配GPIO的MUX位,最后写时基和控制寄存器,这个顺序错了会出现寄存器写了但引脚没输出的问题。
#include "F2806x_Device.h" #define EPWM_CLK_HZ 90000000UL // 系统EPWMCLK,90MHz void InitEPwm1(void) { // 1. 使能ePWM1模块时钟 SysCtrlRegs.PCLKCR0.bit.EPWM1ENCLK = 1; // 2. 配置GPIO0和GPIO1为EPWM1A和EPWM1B功能 // GPIO0/GPIO1的MUX位在GPAMUX1寄存器低4位 GpioCtrlRegs.GPAMUX1.bit.GPIO0 = 0x01; // GPIO0 -> EPWM1A GpioCtrlRegs.GPAMUX1.bit.GPIO1 = 0x01; // GPIO1 -> EPWM1B // 3. 设置时基分频:HSPCLKDIV=2,CLKDIV=1,TBCLK=45MHz EPwm1Regs.TBCTL.bit.CLKDIV = 0x00; EPwm1Regs.TBCTL.bit.HSPCLKDIV = 0x01; // 4. 增减计数模式,影子寄存器在计数器为0时装载 EPwm1Regs.TBCTL.bit.CTRMODE = 0x02; // 增减计数 EPwm1Regs.TBCTL.bit.PRDLD = 0x00; // TBPRD影子装载 // 5. 初始频率1kHz,占空比50% // TBPRD = 45MHz / (2 * 1000) = 22500 EPwm1Regs.TBPRD = 22500; EPwm1Regs.CMPA.CMPA = 11250; // 50%占空比 // 6. 动作限定:增计数到CMPA时输出低,周期顶点时输出高 // 这样在增减计数模式下,一个周期内高电平占CMPA/TBPRD比例 EPwm1Regs.AQCTLA.bit.CAU = 0x01; // CAU: 增计数CMPA匹配 -> 输出低 EPwm1Regs.AQCTLA.bit.PRD = 0x02; // PRD: 周期匹配 -> 输出高 }这段代码有几个参数值得细说。HSPCLKDIV=0x01表示2分频,CLKDIV=0x00表示1分频,合成TBCLK=45MHz。CTRMODE=0x02是增减计数,这种模式下输出频率分辨率和占空比分辨率都比增计数模式高,因为FPGA周期用了两段斜坡。CMPA写入的是影子寄存器,真正的比较寄存器会在特定装载点同步更新,避免了运行中改占空比导致输出突然跳变。AQCTLA里的CAU和PRD定义了输出动作:增计数到CMPA拉低,到周期顶点拉高,下一个周期开始重复,这样输出的是正极性PWM,占空比是CMPA/TBPRD。
如果只需要单路PWM,这套配置就够了。但做可调频率信号源时,初始化只是起点,真正的工作是把频率从按键输入换算成TBPRD,并且保证跳频瞬间输出不乱——那是第四节的内容。
3. LCD显示与按键调频:把人机交互接进DSP
3.1 1602的4线接法与初始化时序
F28069的GPIO是3.3V电平,很多1602液晶模块标注5V供电,但数据线上的高电平阈值只有2.2V左右,所以大多数模块可以直接用3.3V单片机驱动,不需要额外电平转换。前提是模块的背光和对比度供电分开接:背光接5V,逻辑VDD也接5V,数据线由DSP直接推3.3V高电平,实测稳定工作。真正容易出问题的是VO对比度引脚,悬空的时候屏幕要么全黑要么全白,后面第五章会专门讲。
为了省引脚,我用4线模式:D4到D7、RS、EN,一共6根线。4线模式下每条指令要分高4位和低4位两次写入,代码看着啰嗦,但省下的两个GPIO在日后加编码器或者串口时很值。下面是驱动代码,用固定延时代替忙标志检测,省去把D7配置为输入读取BF的麻烦:
#include "F2806x_Device.h" #include <string.h> #define LCD_RS_PIN GPIO_Number_31 // 随意选两个空闲GPIO #define LCD_EN_PIN GPIO_Number_32 void GPIOWritePin(uint16_t pin, uint16_t val); // 底层GPIO写函数,省略实现 static void LcdDelayMs(unsigned int ms) { unsigned int volatile i, j; for (i = 0; i < ms; i++) for (j = 0; j < 9000; j++) ; // 90MHz下约1ms延时 } static void LcdWriteNibble(uint16_t data) { // 把data的低4位写到D4~D7,然后拉一个EN脉冲 GPIOWritePin(GPIO_Number_33, (data >> 0) & 1); GPIOWritePin(GPIO_Number_34, (data >> 1) & 1); GPIOWritePin(GPIO_Number_35, (data >> 2) & 1); GPIOWritePin(GPIO_Number_36, (data >> 3) & 1); GPIOWritePin(LCD_EN_PIN, 1); LcdDelayMs(1); GPIOWritePin(LCD_EN_PIN, 0); LcdDelayMs(1); } static void LcdWriteByte(uint16_t isCmd, uint16_t data) { // RS=0写命令,RS=1写数据 GPIOWritePin(LCD_RS_PIN, isCmd ? 0 : 1); LcdWriteNibble(data >> 4); // 先送高4位 LcdWriteNibble(data & 0x0F); // 再送低4位 } void LcdInit(void) { LcdDelayMs(50); // 等待LCD内部上电复位 // 4线模式初始化时序:必须按0x33 -> 0x32 -> 0x28顺序来 LcdWriteNibble(0x03); // 第一次请求8位模式 LcdDelayMs(5); LcdWriteNibble(0x03); // 第二次请求8位模式 LcdDelayMs(1); LcdWriteNibble(0x03); // 第三次请求8位模式 LcdDelayMs(1); LcdWriteNibble(0x02); // 切换为4位模式 LcdWriteByte(1, 0x28); // 4线模式,2行,5x7字体 LcdWriteByte(1, 0x0C); // 开显示,不显示光标 LcdWriteByte(1, 0x06); // 写入后光标右移 LcdWriteByte(1, 0x01); // 清屏 LcdDelayMs(2); } void LcdShowFreq(unsigned long freqHz) { char buf[17]; // 手写整数转字符串,避免printf家族拖累Flash和速度 // buf内容形如 "F= 2500Hz " // 具体转换代码见第4.2节 }初始化时序里的0x33、0x32、0x28是整个LCD驱动最容易翻车的地方。很多开发者的做法是上电后直接写0x28,但4线模式下液晶默认工作在8位模式,必须先用0x33和0x32把总线宽度降到4位,再写功能设定指令0x28。三个0x03之间的延时也不能省,尤其第一个要等够5毫秒以上,否则液晶内部状态机还没准备好,后面指令全部丢失。这段初始化代码我在不同批次模块上验证过,延时保守一点,成功率百分之百。
3.2 按键调频的交互设计:步进与消抖
按键调频看起来简单,但交互逻辑设计不好会让整个工具很难用。最直观的方案是加减两个键,每按一次频率加或减一个步进值。但固定步进有个两难:步进设1Hz,从1kHz调到50kHz要按49000下;步进设1kHz,想精确调到1005Hz就做不到。
我的做法是粗调细调两档:一个模式键切换当前是粗调还是细调,细调步进1Hz,粗调步进取当前频率的1%再取整。这样低频段粗调精细,高频段粗调快速。按键扫描放在定时器中断里,每5毫秒采样一次,连续三次采样值一致才认为按键有效,这就是最简单的消抖。检测到有效按键后置一个标志位,不在中断里直接改频率,而是让主循环去处理,避免中断函数过长影响其他实时任务。
// 按键扫描结构体和状态机 #define KEY_SAMPLE_MS 5 // 每5ms采样一次 #define KEY_DEBOUNCE 3 // 连续3次一致才有效 typedef struct { uint16_t stableCount; // 连续一致的次数 uint16_t lastLevel; // 上一次采样电平 uint16_t pressed; // 有效按键事件,主循环消费后清零 } KeyState_t; volatile KeyState_t keyUp = {0, 1, 0}; // 频率+ volatile KeyState_t keyDown = {0, 1, 0}; // 频率- volatile KeyState_t keyMode = {0, 1, 0}; // 粗/细调切换 void ScanKey(KeyState_t *key, uint16_t levelNow) { if (levelNow == key->lastLevel) { key->stableCount++; if (key->stableCount >= KEY_DEBOUNCE) { if (levelNow == 0) { // 稳定低电平=按键按下 key->pressed = 1; } } } else { key->stableCount = 0; // 电平不一致,重新计数 key->lastLevel = levelNow; } }这里的消抖逻辑很简单但有效:上一次电平和本次一致才累加计数器,只要出现一次抖动导致的电平变化,计数器清零重来。连续三次采样值稳定为低电平,才认为按键真正按下。注意按键释放的检测没有加在这里——按下一次只触发一次事件,即使按住不放也不会连续跳频率,要连调就一下一下按,这个交互习惯更符合调节精密频率的场景。想支持长按连发,可以在stableCount超过某个阈值后每10次采样再产生一次事件,不影响当前逻辑。
3.3 CPU定时器中断里扫描,主循环里干活
按键扫描如果放在主循环里用延时消抖,会跟LCD刷新的延时纠缠在一起,导致手感时快时慢。更干净的架构是用CPU定时器0产生5毫秒周期中断,中断服务函数里只做两件事:读GPIO电平、更新按键状态机。主循环里检查按键标志,有事件才更新PWM参数和LCD显示。
__interrupt void cpuTimer0ISR(void) { // 读取三个按键的电平,更新状态机 ScanKey((KeyState_t *)&keyUp, GPIOReadPin(KEY_UP_PIN)); ScanKey((KeyState_t *)&keyDown, GPIOReadPin(KEY_DOWN_PIN)); ScanKey((KeyState_t *)&keyMode, GPIOReadPin(KEY_MODE_PIN)); // 清除定时器中断标志 PieCtrlRegs.PIEACK.all = PIEACK_GROUP1; }这个中断服务函数只花几十个CPU周期,5毫秒的扫描周期占空比极低,完全不影响ePWM的硬件输出。真正改频率和刷LCD的代码放在主循环里,即使LCD写入要等毫秒级延时,也只会让主循环变慢,PWM波形依然稳定。
4. 把PWM和LCD拼成一个系统:主循环状态机与完整代码
4.1 频率到寄存器值的换算:别让显示值和真实值打架
新手最容易犯的错是:按键调的是目标频率,液晶显示也是目标频率,但实际输出频率因为TBPRD取整而略偏向另一边,用频率计一测发现显示值和真值差了零点几个Hz,心里就开始怀疑代码有问题。其实不是代码的问题,是整数算术的必然结果。
解决办法是让液晶显示“实际算出来的频率”,而不是“用户想要的目标频率”。每次按键改变目标频率freqTarget后,先用公式算出TBPRD,再用TBPRD反推实际频率freqActual:
TBPRD = TBCLK / (2 * freqTarget) // 整数除法 freqActual = TBCLK / (2 * TBPRD) // 用整数算回实际频率这样液晶上显示的数字永远是硬件真正输出的频率,和示波器、频率计的测量值严格一致,不会再出现“显示3000Hz实测2999.7Hz”的困惑。低频段误差大时这个细节更关键:比如TBCLK=45MHz,目标频率20Hz时TBPRD=112500,超过了16位寄存器上限65535,这时候必须加大CLKDIV降低TBCLK,否则根本无法输出。所以完整的频率换算函数要带一个分频自适应逻辑:
#define MAX_TBPRD 65535UL void ApplyFreq(unsigned long freqTarget) { unsigned long tbcclk = EPWM_CLK_HZ / 2; // HSPCLKDIV=2,先得45MHz unsigned long div = 1; // 额外分频系数 unsigned long tbprd, cmpVal; // 如果目标频率太低,TBPRD会超上限,就加大CLKDIV while ((tbcclk / (2 * freqTarget)) > MAX_TBPRD) { div *= 2; // 分频翻倍,TBCLK减半 tbcclk /= 2; } tbprd = tbcclk / (2 * freqTarget); // 周期寄存器值 cmpVal = tbprd * dutyPermille / 1000; // 占空比千分数 // 如果分频档位变了,才需要改CLKDIV,否则只改TBPRD和CMPA if (div != currentClkDiv) { currentClkDiv = div; EPwm1Regs.TBCTL.bit.CLKDIV = Log2(div); // 把分频系数写成寄存器值 } EPwm1Regs.TBPRD = (uint16_t)tbprd; EPwm1Regs.CMPA.CMPA = (uint16_t)cmpVal; // 用实际TBPRD反推真实频率,给LCD显示用 actualFreqHz = tbcclk / (2 * tbprd); }这段代码里dutyPermille是占空比的千分数,比如500代表50%。用千分数而不是浮点数,是为了避免浮点运算在DSP上拖慢速度,也避免编译器链接进浮点库导致Flash暴涨。applyFreq函数可以在主循环里被按键事件调用,也可以在初始化时调用一次,让系统上电就输出默认频率和占空比。改动CLKDIV时要注意,它会影响之后所有频率计算,所以用一个currentClkDiv变量记录当前分频档位,只有档位变化时才重写TBCTL,避免每次按键都做无意义的分频切换。
4.2 主循环状态机与完整代码
主循环的逻辑是一个典型的事件驱动状态机:检查按键标志,有事件就更新频率或切换粗细调模式,然后刷新LCD。下面把LcdShowFreq的整数转字符串代码也补全,形成一份可以直接抄走的完整示例:
char lcdLine1[17]; unsigned long actualFreqHz; // 实际输出频率 unsigned int dutyPermille = 500; // 默认50%占空比 unsigned int fineStep; // 当前步进:1Hz或1% void LcdShowFreq(unsigned long freqHz) { char *p = &lcdLine1[3]; char tmp[8]; int i = 0; lcdLine1[0] = 'F'; lcdLine1[1] = '='; lcdLine1[2] = ' '; // 整数转字符串,反向填充 do { tmp[i++] = '0' + (freqHz % 10); freqHz /= 10; } while (freqHz > 0); while (i > 0) { *p++ = tmp[--i]; } *p++ = 'H'; *p++ = 'z'; *p = '\0'; // 写入LCD第一行,这里省略逐字节发送的代码 } void main(void) { InitSysCtrl(); // 初始化系统时钟和看门狗 InitGpio(); // 配置按键GPIO为输入,LCD引脚为输出 InitPieCtrl(); // 初始化PIE中断控制器 InitCpuTimers(); // 初始化CPU定时器0 ConfigCpuTimer(&CpuTimer0, 200, 5000); // 5ms周期 InitEPwm1(); // 初始化PWM输出 LcdInit(); ApplyFreq(1000); // 上电默认输出1kHz // 主循环:只处理按键事件和刷新显示 while (1) { if (keyUp.pressed) { keyUp.pressed = 0; if (fineStep == 1) { ApplyFreq(actualFreqHz + 1); // 细调+1Hz } else { ApplyFreq(actualFreqHz + actualFreqHz / 100); } } if (keyDown.pressed) { keyDown.pressed = 0; if (fineStep == 1) { ApplyFreq(actualFreqHz - 1); } else { ApplyFreq(actualFreqHz - actualFreqHz / 100); } } if (keyMode.pressed) { keyMode.pressed = 0; fineStep = (fineStep == 1) ? 100 : 1; // 粗调/细调切换 } LcdShowFreq(actualFreqHz); LcdWriteByte(1, 0x80); // 光标定位第一行 // 发送lcdLine1内容...(略) } }这段主循环的关键是:所有频率计算都基于actualFreqHz,而不是某个独立的target变量。细调时按一下加1Hz,但实际写入的TBPRD经过取整,显示值跟着取整结果走,永远不会出现显示值和真值的偏差。粗调步进取actualFreqHz的1%,也就是1%步进,从50kHz降到10Hz大约要按160次,完全可以接受。调用ApplyFreq时内部的低频分频自适应会保证输出不中断,这就是最简但完整的可调频率PWM系统。
5. F28069可调频PWM的避坑记录:5个必踩的坑
5.1 跳频瞬间输出一个超宽脉冲,示波器上像打嗝
现象:从10kHz跳到20kHz的瞬间,输出波形里突然插了一个宽度明显异常的脉冲,有时候这个脉冲宽到像直流电平,会直接触发后端功率器件的过流保护。
原因:同时改了TBPRD和CMPA,但两个影子寄存器的装载时间点不一定完全同步。尤其是在增减计数模式下,TBPRD在计数器为0时装载,CMPA如果配置为周期装载,那么CMPA的更新比TBPRD晚一个完整时基周期。在这个错位的窗口里,硬件会拿着新周期配旧比较值,或者反过来,导致占空比瞬间失真。
解决:更新频率时,先按比例算好新的CMPA,再写CMPA,紧接着写TBPRD,两条语句之间不要插入其他操作。并且把TBCTL的PRDLD位设为0(零装载),让TBPRD在计数器过零时更新;CMPA也保持影子模式,装载点同样设为零装载。这样两个寄存器的更新点都对齐在时基计数器过零的时刻,跳频瞬间的异常脉冲就消失了。如果还不行,可以按照6.1节的方法,在改频率前发一个软件同步信号,强制时基计数器归零。
5.2 液晶背光亮但屏幕上一片空白
现象:上电后背光亮了,但显示屏永远是白的,写什么指令都没反应。查GPIO配置、查数据线连接都没问题。
原因:1602液晶的VO对比度引脚悬空。不同批次的模块,VO悬空时内部偏压可能太高或太低,表现为要么全黑要么全白。很多教程只画了VDD、VSS和背光三根线的接法,把VO漏了,但VO才是决定字能不能显示出来的关键。
解决:VO引脚接一个10K电位器,两端分别接VDD和VSS,中间抽头接VO。上电后调电位器,直到屏幕出现两行淡淡的黑影,这个位置就是最佳对比度。3.3V驱动5V模块时,对比度电压大约在1V附近,调节范围很窄,电位器要买多圈的。另一个常见原因是初始化时序里的延时不够,把LcdInit里的50ms上电等待加到100ms基本能解决。
5.3 按一下按键频率跳好几个值
现象:按键手感正常,但每按一次频率不是加1Hz,而是加了3到5Hz,偶尔还反向跳一次。
原因:机械按键的触点在闭合和断开的瞬间会产生几十毫秒的抖动,如果按键扫描没有消抖,一次按下会被识别成多次触发。用外部中断沿触发的方式扫按键最容易出现这个问题,因为抖动会产生一连串上升沿和下降沿。
解决:用定时器轮询方式,每5毫秒采样一次,连续三次采样值一致才确认按键状态变化,就是我3.2节展示的状态机。注意每次按下只产生一个pressed事件,事件被主循环消费后立刻清零,按住不放也不会重复触发。这个方案能根治抖动问题,唯一的代价是5毫秒的响应延迟,手感上几乎感觉不到。
5.4 用了sprintf格式化频率,Flash直接爆了
现象:在LcdShowFreq里用sprintf(buf, "F=%dHz", freq)格式化字符串,编译能过,但一链接就报Flash空间不足,或者下载运行后LCD刷新变得极慢。
原因:printf家族的格式化函数在C2000的编译环境里会链接进一整套浮点解析和输出代码,占用的Flash空间动辄十几KB。F28069的Flash虽然有128KB以上,但加上控制算法和液晶驱动后,这种开销太奢侈了,而且运行时逐字符解析整数比手写转换慢一个数量级,刷新一屏要浪费几毫秒。
解决:手写整数转字符串,几十行就搞定,而且只占几十字节Flash。4.2节我已经给了实现。如果一定要用格式化函数,至少用snprintf并限制在整数格式,别碰浮点;但我的建议是这个场景根本不需要,手写转换还能精确控制字符串在LCD上的位置。
5.5 GPIO配置了但PWM引脚始终是固定电平
现象:代码看起来完全正确,初始化里也写了GPAMUX配置,但输出引脚一直是高电平或者低电平,用示波器量不到波形。查遍寄存器发现EPwm1Regs里的值都是对的。
原因:GPIO的MUX位配置被覆盖了,或者ePWM模块的时钟没有在PCLKCR0寄存器里使能。F28069的GPIO配置分两段:GPAMUX1寄存器决定引脚功能,GPADIR决定方向。如果只配了GPAMUX但GPADIR仍被设成输出方向,引脚依然输出数字电平而不是ePWM波形。另外SysCtrlRegs.PCLKCR0里的EPWM1ENCLK位如果不置1,整个ePWM模块都不工作,写寄存器全部无效。
解决:初始化顺序固定为“使能EPWM时钟 -> 配GPAMUX选复用功能 -> 配置GPADIR为输入或输出”(见2.3节)。用GPADIR的时候有个陷阱:ePWM引脚在复用模式下方向由模块控制,GPADIR的值不影响波形输出,但新手容易按普通GPIO的习惯去设置它,导致理解混乱。可以在调试时用表达式窗口看GpioCtrlRegs.GPAMUX1的值,确认GPIO0对应的两位确实是0x01,而不是0x00(普通GPIO)或0x02(其他外设)。
6. 进阶:让输出频率跳变更干净,顺便校准一下
6.1 用软件同步信号让跳频瞬间相位连续
前文解决了跳频毛刺的问题,但还有一个细节:只改TBPRD和CMPA,时基计数器本身是自由运行的,它在任何位置都可能接受新周期值。如果计数器正在从零往上数,新TBPRD比当前计数值小,理论上会出现计数超过新周期的“溢出”,虽然硬件会立即回零,但这个异常周期会产生一个非常窄的毛刺脉冲。
F28069的ePWM模块提供了一个更优雅的解决方案:软件同步信号SWFSYNC。在写入新周期值之前,先置位TBCTL的SWFSYNC位,时基计数器会立即清零并重新开始计数,所有影子寄存器同步装载。这样改频率的动作发生在时基周期边界上,相位从头开始,波形干净利落。
void ApplyFreqSynced(unsigned long freqTarget) { unsigned long tbprd, cmpVal; // 先计算新周期和比较值 tbprd = CalcTbprd(freqTarget); // 内部包含分频自适应 cmpVal = tbprd * dutyPermille / 1000; // 发软件同步,时基归零后装载影子寄存器 EPwm1Regs.TBCTL.bit.SWFSYNC = 1; // 影子装载会在计数器归零时统一生效 EPwm1Regs.TBPRD = (uint16_t)tbprd; EPwm1Regs.CMPA.CMPA = (uint16_t)cmpVal; }注意SWFSYNC位是写1后硬件自动清零的,不需要软件去复位。实际测试时,如果输出接的是感性负载,建议在跳频瞬间观察电流波形而不是电压波形,电压上的毛刺可能被感性负载的续流效应掩盖,但电流会诚实地反应出来。
6.2 用示波器和频率计做两手验证
跳频干净不代表频率准。F28069内部有锁相环,但锁相环的输出精度依赖外部晶振,如果板上用的是内部振荡器,频率误差可能到百分之几。用示波器测量PWM周期数一下格数当然能粗略确认,但更准确的做法是:把PWM引脚接到频率计,或者在示波器上用测量功能读频率,然后对比液晶上显示的实际频率值。两者应该完全一致,因为液晶显示的本来就是用整数运算反推出的实际值。
如果发现整体频率偏高或者偏低同一个比例,先怀疑时钟源。外接无源晶振时,负载电容不匹配会产生几十ppm级别的频偏,示波器测不出来但频率计能看出来。需要微调时,最简单的方法是给freqCalc函数里加一个软件修正系数:用实测频率除以理论频率得到比值,乘到TBCLK的计算里。修正系数是浮点数,但只在计算时用一下,不影响运行时速度。
6.3 最后的习惯
这套方案我前后做过三轮,第一轮用定时器中断翻转GPIO,被跳频毛刺折腾了整整两天;第二轮老老实实切到ePWM,半天就调通了;第三轮才把软件同步和分频自适应补上。现在我的习惯是:无论做电源还是电机控制,凡是要输出PWM的地方,一律先把ePWM的时基分频和影子装载模式想清楚,再写应用逻辑。硬件外设已经把90%的活干完了,剩下那10%就是几个寄存器值,别自己绕远路。希望帮到你。
本文还有配套的精品资源,点击获取