基于TMS320F28027的智能小车开发(一):电机PWM驱动模块详解与避坑指南(附b站视频教程)
前言 / 导语b站详细视频介绍b站视频链接如无法打开复制BV码到b站搜索BV1QFoYBDE3G在微控制器MCU的开发中电机控制是小车的“双腿”也是理解底层定时器与PWM脉宽调制外设的最佳切入点。本系列文章将以德州仪器TI的TMS320F28027为核心手把手带你完成一台多功能智能小车的底层驱动开发。 本篇为第一期直流电机PWM驱动模块。我们将一针见血地剖析硬件逻辑、发波机制并揭秘TI官方底层库的“句柄”设计机制一、PWM调速基本原理PWMPulse Width Modulation脉冲宽度调制是控制直流电机速度最有效、最常用的方法。其核心思想是通过快速开关电源调节在一个固定周期内通电时间与总时间的比例即占空比占空比越高平均电压越接近电源电压电机转速越快反之则越慢。从而改变电机两端的平均电压实现平滑调速。二、 硬件控制逻辑 (让肌肉听大脑指挥)要让小车动起来首先要理清大脑MCU是如何指挥肌肉电机的。小车搭载了UMW L9110S集成电路来控制步进电机。控制逻辑非常简单一句话概括一脚给低电平另一脚给PWM即可实现单向调速。输入端 IA输入端 IB电机状态低电平 (L)低电平 (L)刹车高电平 (H)高电平 (H)刹车低电平 (L)PWM高频切换 (H/L)正转/反转 (占空比决定转速)PWM高频切换 (H/L)低电平 (L)反转/正转 (占空比决定转速)根据硬件连接我们的PWM通道分配如下右电机IA接GPIO4 (EPWM3A)IB接GPIO5 (EPWM3B)。左电机IA接GPIO6 (EPWM4A)IB接GPIO7 (EPWM4B)。三、 核心代码逐块精讲从引脚到波形的诞生很多教程只给代码不讲原理今天我们把底层扒开看看一行行代码究竟在控制单片机内部的什么硬件。我们的电机PWM驱动配置分为四个主要步骤初始化、引脚复用、功能配置、事件触发。用户模块层user_component/motor不对PWM模块信号做预分频配置其为增计数和影子寄存器模式并在计数器过零时装载。配置动作模块使得计数器在CMP值前输出低电平在CMP值后输出高电平。初始化CMPA的值为0保证电机在初始化后保持不动。具体请查看以下代码1. 打通物理通道引脚复用配置 (Motor_pinConfigure)微控制器的引脚默认是普通GPIO我们要把它们的使用权移交给ePWM外设。void Motor_pinConfigure(void) { // 1. 设置复用模式 (MUX) // 严格尊重硬件内部连线GPIO4/5硬件直连EPWM3GPIO6/7直连EPWM4 GPIO_setMode(myGpio, GPIO_Number_4, GPIO_4_Mode_EPWM3A); // 右电机IN1 GPIO_setMode(myGpio, GPIO_Number_5, GPIO_5_Mode_EPWM3B); // 右电机IN2 GPIO_setMode(myGpio, GPIO_Number_6, GPIO_6_Mode_EPWM4A); // 左电机IN1 GPIO_setMode(myGpio, GPIO_Number_7, GPIO_7_Mode_EPWM4B); // 左电机IN2 // 2. 禁用上拉电阻 GPIO_setPullUp(myGpio, GPIO_Number_4, GPIO_PullUp_Disable); GPIO_setPullUp(myGpio, GPIO_Number_5, GPIO_PullUp_Disable); // ... 6, 7同理 // 3. 设置为输出方向 GPIO_setDirection(myGpio, GPIO_Number_4, GPIO_Direction_Output); GPIO_setDirection(myGpio, GPIO_Number_5, GPIO_Direction_Output); // ... 6, 7同理 }【讲解点/避坑回顾】配置PWM输出引脚时通常要禁用内部上拉电阻 (PullUp_Disable)以避免改变波形的高低电平驱动能力。2. 发波引擎核心功能配置 (Motor_functionConfigure)这段代码是全场的重头戏它决定了波形的频率和占空比逻辑。void Motor_functionConfigure(void) { // 1. 全局时钟同步锁没收发令枪防止多个PWM波形相位错乱 CLK_disableTbClockSync(myClk); // 2. TBCLK时钟分频配置不分频让PWM模块吃满系统最高主频 PWM_setHighSpeedClkDiv(myPwm3, PWM_HspClkDiv_by_1); PWM_setClkDiv(myPwm3, PWM_ClkDiv_by_1); // ... myPwm4 同理 // 3. TBCTR 计数器模式向上计数模式 (Up-Count) PWM_setCounterMode(myPwm3, PWM_CounterMode_Up); // ... myPwm4 同理 // 4. TBPRD 周期寄存器决定PWM的频率 PWM_setPeriod(myPwm3, 60000); PWM_setPeriodLoad(myPwm3, PWM_PeriodLoad_Shadow); // 开启影子寄存器防止波形撕裂 // ... myPwm4 同理 // 5. CMPA/CMPB 比较寄存器决定PWM的占空比 PWM_setCmpA(myPwm3, 0); // 初始给0 PWM_setShadowMode_CmpA(myPwm3, PWM_ShadowMode_Shadow); // 写入影子寄存器 PWM_setLoadMode_CmpA(myPwm3, PWM_LoadMode_Zero); // 在计数器归零时更新真实值 // ... myPwm3的CMPB以及myPwm4的CMPA/B配置同理 // 6. Action Qualifier 动作限定器发波逻辑的灵魂 // 逻辑当计数器向上计数达到CMPA时输出高电平(Set)达到周期值PRD时输出低电平(Clear) PWM_setActionQual_CntUp_CmpA_PwmA(myPwm3, PWM_ActionQual_Set); PWM_setActionQual_Period_PwmA(myPwm3, PWM_ActionQual_Clear); // ... 其余通道同理 // 7. 重新开启时钟同步鸣枪起跑所有PWM瞬间同步开始发波 CLK_enableTbClockSync(myClk); }【讲解点 - 为什么要“先关后开”时钟同步】在底层驱动设计中我们要时刻警惕 CPU“代码顺序执行”带来的微观时间差。 因为单片机执行代码是一行行跑的。如果不关时钟先配置完的 PWM3 就会立刻开始发波偷跑等 CPU 花了几十上百个机器周期去把 PWM4 配置好时两者在时间轴上已经产生了不可挽回的相位错位。 所以我们必须先关掉全局时钟这就像是没收了田径场的起跑发令枪强行把所有 PWM 通道死死按在 0 的起跑线上等 CPU 慢条斯理地把所有通道都配置妥当后再瞬间鸣枪开启同步让所有电机的 PWM 波形在同一物理瞬间完美“齐步走”【讲解点 - 影子寄存器的妙用】这里必须提一下PWM_ShadowMode_Shadow影子模式。这体现了工业级MCU的严谨 假如小车正在高速行驶程序突然要求改变速度比如修改CMPA的值。如果我们直接修改正在工作的真实寄存器刚好赶上计数器数到一半可能会导致当前这个周期的波形严重畸变。开启影子模式后我们修改的其实是“备用寄存器”。只有等到当前周期结束、计数器归零PWM_LoadMode_Zero的那一瞬间备用值才会瞬间加载到真实寄存器中。这样就能保证每一个PWM周期都是完美无缺的3. 跨界联动事件配置 (Motor_eventConfigure)如果说前面的代码只是为了让电机转起来那这段代码就体现了系统架构的高级感让PWM模块成为其他外设的“指挥官”。void Motor_eventConfigure(void) { // 1. 设置PWM中断当计数器归零时触发一次中断 (视业务需求处理) PWM_setIntMode(myPwm3, PWM_IntMode_CounterEqualZero); PWM_setIntPeriod(myPwm3, PWM_IntPeriod_FirstEvent); // 2. 级联触发ADC采样的“发令枪” (SOC - Start of Conversion) // 配置逻辑当PWM计数器等于周期值Period时产生一个 SOCA 脉冲 PWM_setSocAPulseSrc(myPwm3, PWM_SocPulseSrc_CounterEqualPeriod); PWM_setSocAPeriod(myPwm3, PWM_SocPeriod_FirstEvent); // 每发生一次匹配就触发一次 PWM_enableSocAPulse(myPwm3); // 开启触发开关 }【讲解点 - 为什么要做SOC联动】在我们的智能小车项目中红外循迹需要ADC模数转换器不断读取传感器的电压。 新手通常会用 CPU 跑一个死循环while(1)或者定时器中断去疯狂拉取ADC数据这会极大地浪费 CPU 算力。 而我们利用PWM自带的 SOC (Start of Conversion) 硬件触发机制让 PWM3 在每次发波周期结束时顺手在硬件底层给 ADC 发送一个脉冲命令 ADC“我这轮波形发完了你赶紧去采个样”全程无需 CPU 干预实现了硬件级的自动化这就是 C2000 芯片真正的强大之处四、 运动控制算法实现 (差速转向)应用层在app.c中配置智能小车的五个动作。有了底层的PWM上层的运动控制就是对CMPA和CMPB的数值游戏了。通过控制左右轮的转速差即可实现任意方向的运动。// 直行左轮PWM3A给30000右轮PWM4B给30000 void Go_Straight(void) { PWM_setCmpA(myPwm3, 30000); // 左轮PWM PWM_setCmpB(myPwm3, 0); // 左轮低电平 PWM_setCmpA(myPwm4, 30000); // 右轮PWM (若硬件镜像安装需反转配合配置cmpB) PWM_setCmpB(myPwm4, 0); } // 紧急停止 void Stop(void) { PWM_setCmpA(myPwm3, 0); PWM_setCmpB(myPwm3, 0); // 双双清零实现刹车 PWM_setCmpA(myPwm4, 0); PWM_setCmpB(myPwm4, 0); } // 右转 void Turn_Right(void) { PWM_setCmpA(myPwm3, 0); //右轮反转 PWM_setCmpB(myPwm3, 40000); PWM_setCmpA(myPwm4, 30000); //左轮正转 PWM_setCmpB(myPwm4, 0; }五、 避坑指南代码全对电机死活不转在开发期间你极有可能会遇到这个史诗级大坑所有的代码都跟教程一模一样示波器一量GPIO就是没有PWM波形输出回忆一下我们在第三节讲的“句柄指针”。如果你只声明了变量PWM_Handle myPwm3;此时它只是一个空指针野指针里面并没有装载 F28027 ePWM3 的真实物理基地址你往一个空地址发送配置包裹单片机根本收不到电机自然纹丝不动。【终极解法】在官方提供的F2802x_Device.c文件中必须补全对句柄的初始化绑定// 将物理基地址(BASE_ADDR)正式赋予句柄 myPwm3 PWM_init((void *)PWM_ePWM3_BASE_ADDR, sizeof(PWM_Obj)); myPwm4 PWM_init((void *)PWM_ePWM4_BASE_ADDR, sizeof(PWM_Obj));加上这两句让指针指向真实的寄存器你的电机瞬间就会重新焕发活力