4、软件逻辑与初始化顺序故障排查

软件层面的PWM故障,说实话,比硬件问题更隐蔽。硬件出问题,示波器一抓波形就露馅了。软件逻辑错了,代码编译通过,芯片跑得欢,就是输出不对。我见过太多工程师抱着示波器在板子上戳半天,最后发现是初始化顺序搞反了。

这一章,咱们就聊聊软件逻辑里的几个大坑。PWM初始化函数调用顺序、中断优先级冲突、DMA配置错误、主循环与PWM更新打架。每个坑我都踩过,有的还不止一次。

核心观点:软件逻辑故障,本质上是时序和资源冲突。芯片不会说谎,它只是严格按照你写的代码执行。你写错了,它就执行错了。

4.1 PWM初始化函数调用顺序错误

初始化顺序这事儿,看着简单,但最容易出问题。我个人的习惯是:先配置时钟,再配置GPIO,然后配置定时器,最后配置PWM输出通道。这个顺序不能乱。

为什么?你想想看,如果你先配置了定时器的PWM模式,但时钟还没开,定时器寄存器根本写不进去。或者GPIO还没配成复用功能,PWM信号就输出到了普通GPIO上,那能对吗?

我遇到过的一个典型案例:某次调试一个四轴飞行器的电调,PWM输出死活没波形。查了两天,最后发现是GPIO初始化放在了定时器初始化之后。定时器已经把PWM通道配置好了,但GPIO还是默认的浮空输入模式。信号根本没送到引脚上去。

正确的初始化顺序应该是这样的:

// 正确的PWM初始化顺序
void PWM_Init(void)
{
    // 第一步:使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    
    // 第二步:配置GPIO为复用功能
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;  // TIM3_CH1
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    // 第三步:配置定时器时基
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_TimeBaseStructure.TIM_Prescaler = 83;      // 84MHz / 84 = 1MHz
    TIM_TimeBaseStructure.TIM_Period = 999;         // 1kHz PWM
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
    
    // 第四步:配置PWM通道
    TIM_OCInitTypeDef TIM_OCInitStructure;
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 500;            // 50%占空比
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM3, &TIM_OCInitStructure);
    
    // 第五步:使能定时器
    TIM_Cmd(TIM3, ENABLE);
    
    // 第六步:如果是高级定时器,还需要使能主输出
    // TIM_CtrlPWMOutputs(TIM1, ENABLE);
}

注意:高级定时器(TIM1、TIM8等)有一个额外的刹车功能和主输出使能位。如果你用的是高级定时器,别忘了调用 TIM_CtrlPWMOutputs() 使能主输出。我见过有人在这上面卡了整整一个下午。

4.2 中断配置与优先级冲突

PWM相关的中断,最常见的是更新中断(溢出中断)和捕获比较中断。中断配置错了,轻则PWM波形抖动,重则系统死机。

优先级冲突是个大问题。我记得有一次做伺服驱动器,PWM中断里要做电流环计算,计算量比较大。结果PWM中断优先级设得太低,被一个串口中断抢了。每次串口发数据,PWM波形就出现一个毛刺。电机跟着抖一下,那叫一个酸爽。

我的建议是:

  • PWM中断优先级要设高——尤其是做实时控制时,PWM周期是系统的时基,不能被打乱。
  • 中断服务函数要短——PWM中断里只做最必要的事,比如更新比较值、读取ADC结果。复杂的运算放到主循环或后台任务里。
  • 注意中断嵌套——如果多个中断共享优先级组,要确保不会出现优先级反转。

这里给一个中断优先级配置的参考:

// 中断优先级配置示例
// 使用NVIC_PriorityGroup_2(2位抢占优先级,2位子优先级)

void NVIC_Configuration(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
    // TIM3更新中断 - 高优先级
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  // 最高抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    // 串口中断 - 低优先级
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;  // 较低抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

小技巧:调试中断优先级问题时,可以在中断服务函数里翻转一个GPIO,用示波器看这个GPIO的波形。如果波形出现异常抖动或丢失,说明中断响应有问题。这个方法我用了十年,屡试不爽。

4.3 DMA传输配置错误

DMA配合PWM,通常用于自动更新占空比。比如你要输出一个正弦波PWM,用DMA把正弦表数据自动搬运到定时器的比较寄存器里。这个方案效率很高,但配置起来坑也不少。

常见的DMA配置错误有:

  • 传输方向搞反了——DMA从内存到外设,还是从外设到内存?PWM更新占空比,应该是内存到外设。
  • 地址增量设置错误——内存地址要增量,外设地址不增量。这个搞反了,数据就全乱了。
  • 传输完成中断没开——DMA传完一轮数据后,需要重新加载新的数据表。如果没开传输完成中断,DMA就停在那里了。
  • 循环模式没使能——如果你要连续输出波形,DMA必须工作在循环模式。

我曾经犯过一个低级错误:DMA的传输数据宽度设置错了。定时器的比较寄存器是16位的,我设成了32位传输。结果每次DMA写入,都把比较寄存器和旁边的寄存器一起覆盖了。PWM波形完全乱套。

正确的DMA配置示例:

// DMA配置 - 用于自动更新PWM占空比
void DMA_Config(void)
{
    DMA_InitTypeDef DMA_InitStructure;
    
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
    
    DMA_DeInit(DMA1_Stream5);  // TIM3_UP对应DMA1 Stream5
    
    DMA_InitStructure.DMA_Channel = DMA_Channel_5;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&TIM3->CCR1;  // 外设地址:比较寄存器
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)&SineTable[0];   // 内存地址:正弦表
    DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;            // 方向:内存到外设
    DMA_InitStructure.DMA_BufferSize = 256;                            // 传输长度
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;   // 外设地址不增量
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;            // 内存地址增量
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;  // 16位
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;          // 16位
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;                    // 循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    
    DMA_Init(DMA1_Stream5, &DMA_InitStructure);
    
    // 使能DMA传输完成中断(可选,用于重新加载数据)
    DMA_ITConfig(DMA1_Stream5, DMA_IT_TC, ENABLE);
    
    // 使能DMA
    DMA_Cmd(DMA1_Stream5, ENABLE);
}

4.4 主循环与PWM更新逻辑冲突

这个坑比较隐蔽。主循环里如果直接修改PWM的占空比,而PWM中断也在修改同一个寄存器,就会产生竞争条件。

举个例子:主循环里根据按键输入调整PWM占空比,同时PWM中断里根据ADC采样结果做闭环控制。两个地方都在写TIMx->CCR1,最后谁写进去的值,完全取决于时序。结果就是PWM输出忽大忽小,系统不稳定。

解决方案其实不复杂:

  • 使用影子寄存器——很多定时器都有影子寄存器,写入CCR的值会在下一个更新事件才生效。这能避免在PWM周期中间被修改。
  • 加互斥保护——如果必须在主循环里修改,可以关中断再修改,改完再开中断。但关中断时间要短,不能影响PWM的实时性。
  • 用双缓冲——准备两个缓冲区,一个给PWM中断用,一个给主循环写。主循环写完后,通过一个标志通知中断切换缓冲区。

我个人比较推荐双缓冲方案。虽然代码量多一点,但安全可靠,不会出现数据不一致的问题。

// 双缓冲方案示例
volatile uint16_t pwm_buffer[2];  // 两个缓冲区
volatile uint8_t active_buffer = 0;  // 当前使用的缓冲区索引

// 主循环中更新占空比
void Update_PWM_Duty(uint16_t new_duty)
{
    uint8_t inactive_buffer = 1 - active_buffer;  // 找到空闲缓冲区
    pwm_buffer[inactive_buffer] = new_duty;       // 写入空闲缓冲区
    __disable_irq();
    active_buffer = inactive_buffer;              // 切换缓冲区
    TIM3->CCR1 = pwm_buffer[active_buffer];       // 更新比较值
    __enable_irq();
}

// PWM中断服务函数
void TIM3_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
    {
        // 使用当前缓冲区中的值
        uint16_t duty = pwm_buffer[active_buffer];
        // ... 其他处理
        
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}

警告:在多任务系统中,如果PWM更新逻辑和主循环共享了全局变量,一定要加 volatile 关键字。否则编译器优化后,可能从寄存器里读旧值,而不是从内存里读新值。这个bug非常难查,我吃过一次亏。

知识体系总览

下面这张图把软件逻辑与初始化顺序的故障排查思路串起来了。你可以把它当作排查时的路线图。

软件逻辑与初始化顺序故障排查知识体系 PWM软件逻辑故障排查 初始化顺序错误 中断优先级冲突 DMA传输配置错误 主循环与PWM更新冲突 时钟 → GPIO → 定时器 → 通道 高级定时器需使能主输出 PWM中断优先级设最高 中断服务函数尽量短 方向:内存→外设 数据宽度:16位对齐 循环模式 + 传输完成中断 使用影子寄存器 关中断保护 + 双缓冲 排查口诀:先时钟,再引脚,中断优先要最高 DMA方向别搞反,主循环里加保护 排查时按这四个维度逐一检查,90%的软件逻辑问题都能定位

软件逻辑的故障排查,说白了就是跟代码的执行顺序和资源竞争做斗争。每次遇到PWM输出异常,先别急着动硬件,把初始化流程、中断配置、DMA参数、主循环逻辑过一遍。很多时候,问题就出在这些看似不起眼的地方。