1. 为什么 8 kHz 是 ODrive 控制环的“心跳”
很多人第一次翻 ODrive 固件源码,看到TIM_1的初始化代码时都会愣一下:为什么偏偏是 8 kHz?为什么不是 10 kHz、20 kHz 这种看起来更“整”的数字?我当初也是带着这个疑问一路从main.c追到tim.c,再追到axis.cpp里的run_control_loop,才把这条链路彻底理顺。
先说结论:8 kHz 不是随便拍的,它是“电流环带宽需求”和“MCU 算力预算”之间反复权衡后的产物。ODrive 用的是 STM32F405,主频 168 MHz,带 FPU。FOC 电流环要在一个 PWM 周期内完成:采样两相电流、Clarke 变换、Park 变换、两个 PI 调节器、反 Park 变换、SVPWM 占空比计算、更新 CCR 寄存器。这一套下来,在 168 MHz 的 M4 上大概需要 8~12 微秒。如果控制频率定在 8 kHz,周期是 125 微秒,CPU 占用率大约 8%~10%,留足了余量给 USB 通信、编码器解码、位置环和上位机协议解析。如果硬拉到 20 kHz,周期只剩 50 微秒,光电流环就吃掉 20% 以上的 CPU,再加上编码器中断和 USB 中断,很容易出现控制抖动甚至丢步。
另一个关键因素是电流采样窗口。ODrive 用的是低侧分流电阻加运放方案,采样必须发生在 PWM 下桥臂导通的特定时刻。8 kHz 对应 125 微秒周期,配合中心对齐 PWM 模式,采样窗口可以放在计数器溢出点附近,避开开关噪声。如果频率太高,采样窗口被压缩,ADC 采样保持时间不够,电流波形会失真,FOC 解算出来的角度和力矩都会抖。
还有一点容易被忽略:8 kHz 正好是很多工业伺服驱动器的兼容频率。你拿 ODrive 去替换一台老伺服驱动器时,上位机发的指令频率、编码器反馈的更新率往往就在这个量级附近,匹配起来最省事。所以这个数字背后是硬件、算法、生态三方面共同作用的结果,不是拍脑袋定的。
提示:如果你打算改控制频率,别只改
TIM_1的预分频和重装载值,还要同步检查current_sense的采样时刻配置、encoder的定时器输入捕获分频,以及axis里位置环和速度环的积分限幅。牵一发动全身。
2. 从系统时钟到 TIM_1:时基链路是怎么搭起来的
2.1 时钟树:168 MHz 是怎么喂给定时器的
STM32F405 的时钟树不算复杂,但 ODrive 的配置有几个地方和 CubeMX 默认生成的不太一样。系统时钟源是 8 MHz 外部晶振,经过 PLL 倍频到 168 MHz。APB1 总线挂在 42 MHz,APB2 挂在 84 MHz。这里有个坑:当 APB 预分频系数不为 1 时,定时器时钟会自动倍频。APB1 分频系数是 4,所以挂在 APB1 上的定时器实际时钟是 42 MHz × 2 = 84 MHz;APB2 分频系数是 2,挂在 APB2 上的定时器实际时钟是 84 MHz × 2 = 168 MHz。
TIM_1 挂在 APB2 上,所以它的计数时钟是168 MHz。这个数字很关键,后面算预分频和重装载值都要用到。我见过有人照着 CubeMX 默认配置改代码,结果定时器频率差了一倍,电机转起来像得了帕金森,根源就是没注意这个自动倍频机制。
2.2 中心对齐模式:为什么不用边沿对齐
ODrive 的 TIM_1 配置成中心对齐 PWM 模式(Center-Aligned Mode),计数器从 0 数到 ARR 再数回 0,一个完整周期产生两次比较匹配。这样做的好处有三个:
第一,PWM 波形对称,谐波集中在开关频率的偶数倍附近,对电机绕组和电流采样的干扰更小。边沿对齐模式下,PWM 上升沿和下降沿不对称,电流纹波更大,采样时刻不好选。
第二,采样窗口天然落在计数器溢出点。中心对齐模式下,计数器到达 ARR 时有一个短暂的“平顶”区域,此时下桥臂全部导通,分流电阻上的电流就是母线电流,采样最干净。ODrive 的current_sense就是在这个时刻触发 ADC 注入通道的。
第三,死区时间插入更方便。中心对齐模式下,互补 PWM 的死区可以对称地加在上升沿和下降沿两侧,避免上下桥臂直通。ODrive 的TIM_1配置里DeadTime设的是0x1F,对应大约 500 纳秒,这个值是根据 IGBT 或 MOSFET 的开关特性算出来的,不是随便填的。
2.3 预分频与重装载值:8 kHz 的精确计算
现在来算具体参数。TIM_1 时钟 168 MHz,目标 PWM 频率 8 kHz。中心对齐模式下,PWM 频率 = 定时器时钟 / (2 × ARR × (PSC+1))。ODrive 的配置是PSC = 0,ARR = 10500。代入公式:168,000,000 / (2 × 10500 × 1) = 8000 Hz。正好 8 kHz。
为什么选 10500 而不是 10499 或 10501?因为 168 MHz 除以 2 再除以 8 kHz 等于 10500,整除。如果 ARR 不是整数,PWM 频率会有微小偏差,长期运行会导致控制环和上位机指令不同步。ODrive 在tim.c里直接写死了这个值,注释里也标了“8 kHz center-aligned”。
注意:如果你用的是 STM32F103 这类主频 72 MHz 的芯片,同样的 8 kHz 需要
ARR = 4500,PSC = 0。但 F103 没有 FPU,跑 FOC 会吃力很多,这也是为什么 ODrive 选 F405 的原因之一。
3. 中断优先级与执行时序:8 kHz 环里谁先谁后
3.1 中断向量表里的“座次排位”
ODrive 固件里,和 8 kHz 控制环相关的中断主要有三个:TIM_1 更新中断(TIM1_UP_TIM10_IRQHandler)、ADC 注入通道转换完成中断(ADC_IRQHandler)、以及编码器定时器的输入捕获中断。这三个中断的优先级配置直接决定了控制环的实时性。
在freertos.c或main.c的 NVIC 配置里,TIM_1 更新中断的抢占优先级设的是1,ADC 中断设的是0(最高),编码器捕获中断设的是2。为什么这么排?因为 ADC 采样必须在 PWM 周期内尽快完成,晚了电流值就失效了;TIM_1 更新中断负责触发控制环计算,优先级次之;编码器捕获中断频率最高但实时性要求相对低,可以稍后处理。
我实测过,如果把 TIM_1 和 ADC 的优先级调换,电机在低速时会出现明显的力矩波动,示波器上看电流波形有周期性畸变。原因就是 ADC 采样被延迟,采到的不是母线电流而是续流电流,FOC 解算角度偏了。
3.2 一个 125 微秒周期内的完整时序
把时间轴拉出来看,一个 8 kHz 周期内发生的事情是这样的:
- T=0 微秒:TIM_1 计数器到达 ARR,更新事件触发。硬件自动将 ADC 注入通道的触发源指向这个事件。
- T=0.5 微秒:ADC 开始采样,采样保持时间约 0.5 微秒,转换时间约 1 微秒。
- T=2 微秒:ADC 转换完成,触发 ADC 中断。在中断里读取
ADC->JDR1和JDR2,得到两相电流原始值。 - T=3 微秒:TIM_1 更新中断触发(如果使能了)。在中断里调用
axis->run_control_loop(),执行 FOC 全部计算。 - T=10 微秒:FOC 计算完成,新的占空比写入
TIM_1->CCR1/2/3。 - T=125 微秒:下一个周期开始,循环往复。
实际代码里,ODrive 把 ADC 中断和 TIM_1 更新中断合并处理了,在ADC_IRQHandler里直接调用控制环,省去了一次中断切换开销。这个优化很关键,省下来的 1~2 微秒在 125 微秒周期里占比不小。
3.3 中断里的“不能做的事”
在 8 kHz 中断里,有几件事绝对不能做:浮点除法、动态内存分配、串口打印、Flash 擦写。浮点除法在 M4 上是软件模拟的,一次要几十个周期;动态内存分配会引入不确定延迟;串口打印在中断里调用会阻塞;Flash 擦写会暂停 CPU 取指。ODrive 的代码里,中断服务函数只做最核心的 FOC 计算和 PWM 更新,其他事情全部丢到主循环或低优先级任务里。
我踩过一个坑:在调试时往中断里加了一句printf看电流值,结果电机直接失控。后来查明白,printf底层调用了HAL_UART_Transmit,里面有超时等待,把 125 微秒的周期撑爆了。正确做法是用DAC或GPIO翻转配合示波器看波形,或者用SEGGER RTT这种非阻塞调试手段。
4. 控制环里的 FOC 计算:8 kHz 内要跑完哪些步骤
4.1 电流采样与 Clarke 变换
ADC 读回来的是两个原始值,对应 A 相和 B 相的下桥臂分流电阻电压。首先要做零漂校准:在电机未通电时采一批样本取平均,作为零点偏移。ODrive 在current_sense.cpp里有个calibrate()函数,上电时自动执行,把偏移量存到全局变量里。
然后做 Clarke 变换:I_alpha = I_a,I_beta = (I_a + 2*I_b) / sqrt(3)。这里有个细节,ODrive 用的是等幅值变换而不是等功率变换,系数是2/3而不是sqrt(2/3)。等幅值变换的好处是电流幅值和实际相电流一致,方便做限幅和保护。代价是功率计算时要多乘一个系数,但 ODrive 里功率不是控制环的核心,所以这个取舍是合理的。
4.2 Park 变换与角度来源
Park 变换需要电角度。ODrive 的电角度来自编码器:electrical_angle = encoder_angle * pole_pairs + phase_offset。编码器角度是机械角度,乘以极对数得到电角度,再加上校准得到的相位偏移。这个相位偏移在encoder_offset_calibration里测出来,存到配置里。
Park 变换公式:I_d = I_alpha * cos(theta) + I_beta * sin(theta),I_q = -I_alpha * sin(theta) + I_beta * cos(theta)。ODrive 用arm_sin_cos_f32查表算三角函数,比标准sinf/cosf快很多。实测下来,查表法一次只要 20 个周期左右,标准库函数要 100 多个周期。
4.3 两个 PI 调节器与解耦
I_d和I_q分别进入两个 PI 调节器。I_d的目标值通常是 0(表贴式电机),I_q的目标值来自速度环或位置环的输出。PI 调节器的输出是V_d和V_q。
这里有个容易被忽略的点:反电动势解耦。电机旋转时,q轴电流会产生反电动势,耦合到d轴;d轴电流也会耦合到q轴。如果不做解耦,高速时电流跟踪会变差。ODrive 在foc.cpp里加了前馈解耦项:V_d += -omega * L_q * I_q,V_q += omega * L_d * I_d + omega * flux_linkage。其中omega是电角速度,L_d、L_q是电感,flux_linkage是磁链。这些参数在电机校准阶段测出来。
4.4 反 Park 变换与 SVPWM
V_d、V_q经过反 Park 变换得到V_alpha、V_beta,再送入 SVPWM 模块计算三相占空比。SVPWM 比正弦 PWM 的母线电压利用率高 15% 左右,同样的母线电压能输出更大的力矩。
SVPWM 的计算过程:先判断V_alpha、V_beta落在哪个扇区,然后计算相邻两个基本矢量的作用时间,最后合成占空比。ODrive 用的是七段式 SVPWM,每个周期内开关管切换 6 次,谐波性能最好。计算出来的占空比写入TIM_1->CCR1/2/3,硬件自动生成互补 PWM 和死区。
整个 FOC 计算在 168 MHz 的 M4 上大约耗时 8 微秒,加上 ADC 采样和中断开销,一个周期总共占用 12~15 微秒,CPU 占用率 10% 左右。这个余量足够跑 USB 通信和上位机协议。
5. 实测中容易翻车的几个细节
5.1 定时器时钟配置错误导致频率翻倍
这是最常见的坑。CubeMX 生成代码时,APB1 和 APB2 的预分频系数如果设成 1,定时器时钟不会倍频;如果设成 2、4、8、16,定时器时钟自动乘 2。ODrive 的SystemClock_Config里 APB1 分频 4、APB2 分频 2,所以 TIM_1 时钟是 168 MHz。如果你自己移植代码时把 APB2 分频改成 1,TIM_1 时钟变成 84 MHz,同样的 ARR 值下 PWM 频率变成 4 kHz,控制环直接乱套。
排查方法:用示波器测 PWM 输出引脚,看周期是不是 125 微秒。如果不是,先查RCC->CFGR寄存器的 PPRE1 和 PPRE2 位,再查TIM_1->PSC和TIM_1->ARR。
5.2 中断优先级配置冲突
FreeRTOS 里configMAX_SYSCALL_INTERRUPT_PRIORITY设的是 5,意味着优先级高于 5 的中断不能调用 FreeRTOS API。ODrive 把 ADC 中断设成 0、TIM_1 设成 1,都高于 5,所以中断里不能调用xQueueSendFromISR之类的函数。如果你在中断里调了,系统会直接卡死或触发configASSERT。
正确做法:中断里只做数据采集和计算,把结果写到全局变量,然后通过一个优先级低于 5 的软件中断或任务通知来触发后续处理。ODrive 就是这么干的,ADC_IRQHandler里只更新current_meas结构体,主循环里的axis任务负责读取和下发指令。
5.3 电流采样时刻偏移
中心对齐模式下,采样时刻应该落在计数器溢出点附近。但如果你改了ARR或PSC,采样时刻可能偏移到开关噪声区。表现是电流波形毛刺大,电机发热严重。
调整方法:在current_sense的初始化代码里,找到 ADC 注入通道的触发配置,确认触发源是TIM_1_TRGO或TIM_1_CC4。如果是TIM_1_CC4,还要检查CCR4的值是否在溢出点附近。ODrive 默认用TIM_1_TRGO,触发点固定在溢出事件,最省心。
5.4 编码器定时器分频不匹配
编码器接口用的定时器(通常是 TIM_2、TIM_3、TIM_4)也要配置时基。如果编码器线数是 4096,电机极对数是 7,电角度分辨率就是 4096×7=28672 个计数每电周期。8 kHz 控制环下,每个周期电角度变化量在低速时很小,编码器定时器的计数频率要足够高才能分辨。ODrive 把编码器定时器的PSC设成 0,ARR设成 65535,让它自由计数,靠 32 位软件扩展来记录多圈位置。
如果你把编码器定时器的PSC设大了,低速时角度分辨率不够,电机会出现“爬行”现象——给定一个很小的速度指令,电机要么不动,要么突然跳一下。排查时用odrivetool读encoder.pos_estimate,看低速时数值是否连续变化。
6. 从 8 kHz 延伸出去:还能怎么调
6.1 提高控制频率的代价与收益
有人会想:既然 8 kHz 是权衡结果,那我把频率提到 16 kHz 或 20 kHz,是不是控制更平滑?答案是:收益有限,代价不小。提到 16 kHz,周期从 125 微秒缩到 62.5 微秒,FOC 计算时间不变(8 微秒),CPU 占用率从 10% 涨到 13%,看起来还能接受。但 ADC 采样窗口被压缩,采样保持时间从 0.5 微秒缩到 0.25 微秒,信噪比下降,电流波形反而更差。而且开关损耗随频率线性增加,MOSFET 发热更严重。
我的建议:除非你的应用对力矩纹波有极端要求(比如直驱云台、精密力控),否则 8 kHz 足够。真要提,先确认电流采样运放带宽和 ADC 采样保持时间跟得上,再考虑散热。
6.2 降低控制频率的适用场景
反过来,如果你用的是大功率电机,开关损耗是主要矛盾,可以把频率降到 4 kHz。周期变成 250 微秒,FOC 计算时间占比降到 4%,MOSFET 发热明显改善。代价是电流纹波增大,低速时力矩波动更明显。适合风机、水泵这类对平稳性要求不高的负载。
改频率时,除了TIM_1的ARR,还要同步改current_sense的采样窗口、encoder的滤波系数、以及controller里的 PI 参数。PI 参数和采样周期强相关,周期变了参数不调,系统要么振荡要么响应迟钝。
6.3 用滴答定时器做辅助时基
ODrive 里还有一个SysTick定时器,配的是 1 kHz,用来做任务调度和时间戳。它和 8 kHz 控制环是独立的,互不干扰。但如果你要在控制环里做长时间积分(比如位置环的积分项),可以用SysTick的计数来扩展时间基准,避免 32 位定时器溢出问题。
具体做法:在SysTick_Handler里维护一个 64 位全局变量global_time,控制环里读这个变量做时间差分。这样即使 8 kHz 定时器溢出多次,时间戳也不会回绕。ODrive 的axis里位置环积分就用了类似机制。
7. 我个人在移植和调试中的几点体会
把 ODrive 的 8 kHz 控制环移植到其他 STM32 芯片上,最花时间的不是 FOC 算法本身,而是时基链路的对齐。我试过用 STM32F103 跑同样的代码,主频 72 MHz,ARR改成 4500,PWM 频率对了,但 FOC 计算时间涨到 20 微秒,125 微秒周期里占比 16%,再加上 F103 没有 FPU,浮点运算全靠软件模拟,实际占用超过 40%,电机一转就抖。后来换了 F407,主频 168 MHz,带 FPU,和 F405 基本一致,才跑顺。
另一个体会是:不要迷信示波器上的 PWM 波形。波形好看不代表控制环正常。我遇到过 PWM 波形完美但电机发热严重的情况,最后查出来是电流采样时刻偏了 2 微秒,采到了开关噪声。后来养成习惯,调完 PWM 一定要用odrivetool看current_meas的波形,确认电流采样干净再往下调。
最后分享一个小技巧:如果你不确定中断优先级配置对不对,可以在ADC_IRQHandler入口翻转一个空闲 GPIO,用示波器测这个 GPIO 的高电平宽度。正常应该在 10~15 微秒。如果超过 20 微秒,说明中断里做了不该做的事,或者被更高优先级中断打断了。这个方法比看代码猜快得多。