总结: 20 kHz 的 TIM6 控制中断计算量过大并且优先级最高长期占用 CPU导致 SysTick、主循环和 USB 通信无法及时执行。表面上是 USB 和上位机卡住实际上是高频控制中断执行时间过长导致的 CPU 调度饥饿问题。STM32 电机控制中 USB 虚拟串口卡死问题的排查一、问题现象项目使用 STM32G431实现以下功能TIM6 以 20 kHz 频率执行 VF/FOC 控制TIM1 输出三相互补 PWM主循环通过 USB CDC 向 VOFA 发送 JustFloat 数据VOFA 用于观察角度、三相电压和 SVPWM 波形。程序运行后发现TIM6 计数器一直变化电机控制变量和 PWM 比较值也在更新但 VOFA 只能收到少量数据随后波形停止刷新USB 有时表现为一直处于BUSY状态。一开始看起来像是 USB CDC 出了问题但最终发现真正的根因在 20 kHz 控制中断中。二、先划分软件的执行层次这个程序可以分成四层TIM6 控制中断 ↓ 系统时基 SysTick ↓ 主循环中的 Vofa 发送任务 ↓ USB CDC 发送与完成中断上位机卡住时不能直接从 USB 驱动开始查而应该从前往后确认CPU 是否还有时间执行到 USB 发送代码。三、第一步确认主循环是否还能运行首先在主循环中临时增加一个计数器volatile uint32_t MainLoopCount 0; while (1) { MainLoopCount; Vofa_Send_Task(); }让程序连续运行一段时间然后暂停 CPU观察MainLoopCount。判断方法持续快速增加主循环正常只增加少量数值后基本不动CPU 大部分时间被中断占用完全不增加程序可能停在初始化、异常处理或某个死循环中。本次调试中MainLoopCount在数秒内只增加了约 255 次。这说明程序并未真正卡死而是主循环几乎得不到执行时间。四、第二步确认 SysTick 是否正常执行VOFA 发送任务使用HAL_GetTick()控制发送周期if ((HAL_GetTick() - last_send_tick) 10U) { return; }因此只要uwTick不增加发送任务就会一直提前返回。先检查 SysTick 寄存器SysTick-CTRL SysTick-LOAD SysTick-VAL正常情况下SysTick-CTRL应满足ENABLE 1 TICKINT 1 CLKSOURCE 1本项目中读取到SysTick-CTRL 0x00010007 SysTick-LOAD 0x0002980F这说明 SysTick 硬件已经正确启动并按 170 MHz 系统时钟配置为约 1 ms 周期。但是硬件计数器工作并不代表中断函数一定得到执行。因此在SysTick_Handler()中临时加入计数volatile uint32_t SysTickIrqCount 0; void SysTick_Handler(void) { SysTickIrqCount; HAL_IncTick(); }运行约 20 秒后却发现SysTickIrqCount ≈ 6 uwTick ≈ 6由此可以判断SysTick 外设本身配置正确但 SysTick 中断几乎没有获得 CPU 执行时间。五、第三步检查高频控制中断是否占满 CPU接着在 TIM6 回调中增加计数器volatile uint32_t Tim6IrqCount 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { Tim6IrqCount; Motor_StateMachine_Run(MotorSystem); } }测试结果表现为Tim6IrqCount 持续快速增加 SysTickIrqCount 几乎不增加 MainLoopCount 增长极慢这说明 TIM6 中断一直在运行但它占用了绝大部分 CPU 时间。此时再检查中断优先级HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 0, 0);而 HAL 默认的 SysTick 优先级为#define TICK_INT_PRIORITY 15UL数值越小抢占优先级越高。因此 TIM6 为最高优先级而 SysTick 为最低优先级。当 TIM6 中断执行时间接近或超过 50 us 时SysTick 和主循环就会被严重挤压。六、找到 TIM6 中断负载过高的原因TIM6 的频率为170 MHz / 8500 20 kHz所以每次中断之间只有1 / 20000 50 us控制代码中原本使用了cos(state-Theta); sin(state-Theta); fmod(state-Theta_Integrator, _2PI);虽然输入和输出变量是float但cos()、sin()和fmod()默认执行双精度运算。它们在 20 kHz 中断中被频繁调用造成了明显的计算负担。将其修改为单精度版本cosf(state-Theta); sinf(state-Theta); fmodf(state-Theta_Integrator, _2PI);同时确保浮点常量带有f后缀-0.5f 0.86f修改后主循环计数从数秒内几百次提升到数百万次说明 TIM6 中断的执行时间显著下降CPU 重新有时间处理后台任务。七、调整中断优先级为了防止控制中断再次阻塞系统时基和 USB将优先级调整为USB_LP_IRQn 优先级 0 SysTick_IRQn 优先级 1 TIM6_DAC_IRQn 优先级 2对应修改#define TICK_INT_PRIORITY 1UL以及HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 2, 0);调整后观察到uwTick 与实际运行时间基本一致 SysTickIrqCount 按约 1 kHz 增长 Tim6IrqCount 按约 20 kHz 增长 MainLoopCount 持续快速增加至此系统调度恢复正常。八、最后检查 USB 发送状态在确认主循环能够正常执行之后再检查 USB。USB 正常枚举时应满足hUsbDeviceFS.dev_state 0x03 hUsbDeviceFS.pClassData ! NULL其中0x01默认状态 0x02已分配地址 0x03配置完成 0x04挂起状态本项目每帧发送 9 个浮点数和 4 字节帧尾9 × 4 4 40 字节原程序存在一个数据拷贝错误memcpy(Byte_Data_Array, Send_Data_Array, data_len);Send_Data_Array实际只有 36 字节但这里读取了 40 字节而且两个数组位于同一结构体中可能产生越界读取和重叠访问。正确写法为memcpy(Byte_Data_Array, Send_Data_Array, data_num * sizeof(float));然后单独填充 4 字节 JustFloat 帧尾。同时不能在主循环中无间隔发送应限制发送频率例如 10 ms 一帧if ((HAL_GetTick() - last_send_tick) 10U) { return; }这样 VOFA 的接收频率约为 100 Hz已经足够观察控制波形。九、最终故障链路这次问题的完整因果关系是TIM6 以 20 kHz 运行 ↓ 中断中执行双精度 sin、cos 和 fmod ↓ TIM6 中断执行时间过长 ↓ 低优先级 SysTick 和主循环得不到执行 ↓ uwTick 几乎不增加 ↓ VOFA 发送任务一直被 10 ms 判断提前返回 ↓ USB 发送完成处理也受到影响 ↓ VOFA 表现为波形停止或卡住最终解决措施包括将sin/cos/fmod改为sinf/cosf/fmodf调整 USB、SysTick 和 TIM6 的中断优先级修正 VOFA 数据拷贝长度将 VOFA 发送频率限制为约 100 Hz发送前检查 USB 是否处于配置完成状态。十、针对这个问题的最短排查路径以后再次遇到类似现象只需要按下面顺序判断VOFA 卡住 ↓ 主循环是否运行 ├─ 否检查高频中断是否占满 CPU └─ 是 ↓ uwTick 是否增加 ├─ 否检查 SysTick 配置、优先级和中断占用 └─ 是 ↓ 是否进入 CDC_Transmit_FS ├─ 否检查发送周期判断 └─ 是 ↓ USB 是否为 CONFIGURED 状态 ├─ 否检查枚举和 USB 连接 └─ 是 ↓ TxState 是否长期为 1 ├─ 是检查 USB 发送完成中断 └─ 否检查数据格式和 VOFA 配置这次调试最重要的经验是外设仍在计数不代表 CPU 有时间执行主循环USB 显示BUSY也不一定是 USB 驱动故障。面对“上位机卡住”应先确认实时控制中断是否侵占了系统的全部执行时间。