拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ODrive固件源码解析:从时钟树到8kHz定时器中断的时基设计

ODrive固件源码解析:从时钟树到8kHz定时器中断的时基设计

1. 为什么一个电机控制固件要先聊定时器

很多人第一次翻 ODrive 的源码,注意力都会被 FOC 算法、电流环、编码器校准这些"看起来更高级"的东西吸走,结果在axis.cpp、motor.cpp里绕了半天,最后卡在一个最朴素的问题上:这些控制逻辑到底是谁在什么时候调用的?答案就藏在定时器里。ODrive 整个控制体系的"心跳"来自一个 8 kHz 的定时器中断,也就是每 125 微秒触发一次。这个数字不是随便拍的,它直接决定了电流环的带宽、PWM 的更新频率、采样和计算的时序关系。你把这一层搞明白了,后面看电流环、速度环、位置环的代码就会顺很多,因为你会清楚地知道"这段代码是在中断里跑的,那段是在主循环里跑的",而这两者的时序约束完全不同。

这篇是 ODrive 固件源码解析系列的第二篇,专门啃定时器时基到 8 kHz 控制环这条链路。我会从时钟树怎么配、定时器怎么初始化、中断里到底干了什么、主循环和中断怎么分工这几个角度,把这条链路完整拆开。适合已经看过第一篇、对 ODrive 整体架构有初步印象的读者,也适合任何在做电机控制、想搞清楚"控制频率到底怎么落地"的嵌入式开发者。哪怕你用的不是 ODrive,而是自己拿 STM32 搭的板子,这套时基设计的思路照样能直接抄。

需要先说明一点:ODrive 的固件版本迭代比较多,不同版本在定时器配置、中断优先级、控制环拆分上会有差异。我下面讲的是基于常见固件结构(以 STM32F4 系列主控、TIM 定时器产生控制中断这一典型实现)的解析,具体到你手上的版本,寄存器名、函数名可能略有出入,但设计思想和时序逻辑是一致的。遇到对不上的地方,按思路去对照你本地的源码即可。

2. 时钟树与时基:8 kHz 到底是怎么算出来的

2.1 从系统时钟到定时器时钟的推导

要理解 8 kHz,得先搞清楚定时器的时钟源从哪来。以 STM32F405 这类主控为例,外部晶振一般是 8 MHz,经过 PLL 倍频后系统时钟跑到 168 MHz。定时器的时钟并不是直接等于系统时钟,它挂在 APB1 或 APB2 总线上,而总线时钟和定时器时钟之间还有一个倍频规则:当 APB 预分频系数不为 1 时,定时器时钟等于对应 APB 总线时钟的 2 倍。

假设 APB1 预分频设为 4,那么 APB1 总线时钟是 168/4 = 42 MHz,而挂在 APB1 上的定时器实际时钟是 42 × 2 = 84 MHz。这个 84 MHz 就是我们要拿来分频的基准。很多人算时基的时候直接把系统时钟代进去,结果频率差了一倍,中断频率怎么调都不对,这是最常见的坑之一。

有了定时器时钟,接下来就是两个参数:预分频器(PSC)和自动重装载值(ARR)。定时器溢出频率的公式是:

中断频率 = 定时器时钟 / ((PSC + 1) × (ARR + 1))

我们要 8 kHz,定时器时钟 84 MHz,那么 (PSC+1)×(ARR+1) = 84,000,000 / 8,000 = 10500。这个 10500 可以有很多种拆法,比如 PSC+1 = 1、ARR+1 = 10500,或者 PSC+1 = 10、ARR+1 = 1050,等等。选哪一组,取决于你对计数精度和寄存器范围的要求。

2.2 参数选择背后的取舍

这里有个容易被忽略的细节:ARR 的值决定了计数器的量程,也间接影响你后续如果要改频率时的灵活性。如果 PSC 设得很小、ARR 设得很大,那么调整 ARR 就能比较精细地改频率;反过来如果 ARR 很小,频率调整的步进就会很粗。ODrive 这类固件通常会把 PSC 设成一个固定值,把 ARR 作为可调项,方便在不同控制频率之间切换。

另外还要注意 ARR 是 16 位还是 32 位。STM32 的 TIM2、TIM5 是 32 位定时器,ARR 可以到 2^32;而 TIM1、TIM8 等高级定时器是 16 位,ARR 最大 65535。如果你算出来的 ARR+1 超过 65535,就必须换定时器或者调整 PSC。10500 这个值远小于 65535,所以用 16 位定时器也完全够用。

我实际调试时习惯把参数列成一张表,方便对照和快速切换:

目标频率定时器时钟PSC+1ARR+1实际频率误差
8 kHz84 MHz1105008000.0 Hz0
8 kHz84 MHz1010508000.0 Hz0
8 kHz84 MHz1001058000.0 Hz0
10 kHz84 MHz1840010000.0 Hz0
4 kHz84 MHz1210004000.0 Hz0

从表里能看出来,只要整除关系成立,误差就是零。但现实中时钟不一定这么整,比如某些配置下定时器时钟是 90 MHz,那 90000000/8000 = 11250,依然能整除。真正麻烦的是除不尽的情况,这时候就要在 PSC 和 ARR 之间找一个乘积最接近目标值的组合,然后接受一点点频率误差。对电流环来说,几百赫兹的频率偏差通常可以接受,但如果你做的是高精度位置控制,最好还是把时钟配成能整除的。

2.3 中断优先级与实时性考量

时基配好了,中断能不能准时触发、会不会被别的中断打断,是另一个关键问题。ODrive 的控制中断优先级通常设得比较高,因为它直接关系到电流环的稳定性。如果控制中断被 USB、UART 这类通信中断长时间抢占,电流环就会出现抖动,严重时甚至炸机。

STM32 的 NVIC 支持抢占优先级和子优先级。控制中断一般给一个较高的抢占优先级(数值较小),通信类中断给较低的抢占优先级。这样即使通信中断正在处理,控制中断也能把它打断,保证 125 微秒的节拍不丢。但也不能把控制中断设成最高优先级就完事,因为如果它里面执行时间过长,会反过来阻塞其他中断,导致系统响应变差。所以中断里的代码必须尽量短,这也是为什么 ODrive 把很多计算放到主循环、只在中断里做最关键的采样和 PWM 更新。

提示:中断优先级的数值越小,优先级越高。配置时先确定哪些中断绝对不能延迟(控制、故障保护),再给它们分配高优先级,其余按重要性依次排开。

3. 中断服务函数里到底跑了什么

3.1 中断触发的完整链路

定时器溢出后,硬件会自动把中断标志位置位,然后跳转到对应的中断服务函数(ISR)。在 ODrive 的固件里,这个 ISR 通常叫TIMx_IRQHandler或者经过 HAL 封装后的回调函数。它做的事情可以概括成一条流水线:读取编码器、读取电流采样、执行 FOC 计算、更新 PWM 占空比、处理故障检测。

这条流水线的每一步都有严格的时序要求。比如电流采样必须在 PWM 的特定时刻进行,太早采到的可能是开关噪声,太晚采到的电流已经变了。编码器读取虽然相对宽松,但也要保证每次控制周期都能拿到最新值。FOC 计算是纯数学运算,耗时相对固定。PWM 更新则必须在下一个周期开始前完成,否则这一拍的控制量就丢了。

我见过不少自己写 FOC 的朋友,把这一整条链路全塞进中断,结果中断执行时间超过了 125 微秒,系统直接卡死。正确的做法是评估每一步的耗时,把能挪出去的挪到主循环,中断里只保留"必须在这个时刻做"的部分。

3.2 采样与 PWM 的时序配合

这里展开讲一下采样和 PWM 的配合,因为这是 8 kHz 控制环里最讲究的地方。常见的做法是让定时器工作在中心对齐模式(也叫对称 PWM),计数器从 0 数到 ARR 再数回 0,形成一个三角波。PWM 的占空比在计数器等于 ARR(波峰)或 0(波谷)时更新,而电流采样安排在波峰或波谷附近,因为这时候开关管的状态最稳定,采样噪声最小。

中心对齐模式相比边沿对齐模式,好处是 PWM 谐波分布更优、电机噪声更小,代价是同样的定时器时钟下,PWM 频率是边沿对齐的一半。所以如果你要 8 kHz 的控制频率,用中心对齐的话,定时器的溢出频率要设成 16 kHz,然后在每次溢出时判断是波峰还是波谷,只在其中一个时刻执行控制计算。这个细节如果没搞清楚,很容易出现"控制频率只有 4 kHz"的问题。

注意:中心对齐模式下,一个完整的 PWM 周期包含两次溢出中断。如果你在每次中断里都跑一遍 FOC,实际控制频率就翻倍了,电流环参数会全部失配。

3.3 中断执行时间的实测与优化

中断执行时间怎么测?最土但最有效的办法是在 ISR 入口拉高一个 GPIO,出口拉低,用示波器看高电平持续时间。我实测过一套典型的 FOC 中断,在 168 MHz 主频下,采样加计算加 PWM 更新大概在 20 到 40 微秒之间,占 125 微秒周期的 16% 到 32%,留有余量。如果你的实现超过 80 微秒,就要警惕了,一旦有突发情况(比如故障检测触发)就可能超时。

优化的方向有几个:把三角函数查表化、把浮点运算改成定点、把不紧急的故障检测挪到主循环。ODrive 在早期版本里大量使用浮点,后来为了性能也做了不少优化。你自己写的时候,先保证功能正确,再逐步优化耗时,不要一上来就追求极致。

4. 主循环与中断的分工设计

4.1 为什么不能所有事都在中断里做

新手最容易犯的错误,就是觉得"中断里做最实时,那就把所有逻辑都放中断"。这个想法在简单场景下能跑,但在电机控制里会出大问题。原因有三:第一,中断执行时间越长,越容易错过下一个中断,导致控制节拍丢失;第二,中断里不能做阻塞操作,比如等待某个标志位、延时、打印调试信息,这些都会让系统卡死;第三,中断里访问共享数据要考虑原子性,稍不注意就会出现数据竞争。

ODrive 的设计思路很清晰:中断负责"硬实时"的部分,也就是必须在精确时刻完成的采样和 PWM 更新;主循环负责"软实时"的部分,比如状态机切换、通信处理、参数更新、故障恢复。这两者之间通过一组共享变量和标志位来传递数据。

4.2 数据传递与原子性保护

中断和主循环之间传递数据,最典型的就是电流环的设定值。主循环根据速度环或位置环算出一个目标电流,写到一个变量里,中断读取这个变量作为电流环的输入。问题来了:如果主循环正在写这个变量(比如 32 位浮点,需要多条指令),中断突然插进来读,就可能读到一个"半新半旧"的值。

解决办法有几种。最简单的是用单字节或单字长的变量,保证读写是一条指令完成的,天然原子。如果数据超过一个字长,就要用临界区保护,在写的时候关中断,写完再开。但关中断会影响实时性,所以临界区要尽可能短。另一种做法是双缓冲:主循环写缓冲区 A,中断读缓冲区 B,写完后交换指针。这样中断永远读的是完整的数据,不需要关中断。

ODrive 里对这类共享数据的处理比较讲究,具体用哪种方式取决于数据的重要性和更新频率。你在移植或修改时,一定要先搞清楚每个共享变量的读写方是谁,再决定要不要保护。

4.3 状态机在主循环中的调度

主循环里跑的是一个状态机,管理电机的各种状态:空闲、校准、闭环控制、故障。状态切换不是随便切的,比如从空闲切到闭环,要先完成编码器校准和电流偏置校准,这些校准过程需要一定时间,不能放在中断里做。主循环按顺序推进这些步骤,每一步完成后更新状态,中断则根据当前状态决定要不要执行控制计算。

这种设计的好处是,状态切换的逻辑可以写得很清晰,不用担心被中断打断。坏处是状态切换有一定的延迟,因为要等主循环走到那一步。对大多数应用来说,这个延迟可以接受。如果你需要极快的状态响应,可以把关键的状态判断也放进中断,但要非常小心。

5. 常见问题与排查实录

5.1 中断频率不对的排查思路

中断频率不对是最常见的问题,表现可能是电机抖动、电流环震荡、或者干脆不转。排查顺序我一般是这样:先用示波器或逻辑分析仪测中断对应的 GPIO 翻转频率,确认硬件层面中断有没有按预期触发。如果频率不对,回去查时钟树配置,重点看 APB 预分频和定时器倍频规则。如果频率对但电机还是不对,再查中断里的计算逻辑。

有个隐蔽的坑是:有些固件在初始化时会先配一个默认频率,等系统稳定后再切到目标频率。如果你只看了初始化那一段,可能会以为频率是错的。所以排查时要顺着调用链把整个初始化流程走一遍,别只看一个函数。

5.2 中断丢失与优先级反转

中断丢失的表现是控制节拍偶尔少一拍,电机在高速时会有轻微异响。原因通常是中断执行时间太长,或者被更高优先级的中断长时间占用。排查方法是统计中断进入次数,和理论值对比。如果实际次数偏少,就要找是谁占用了时间。

优先级反转是另一个坑:低优先级中断持有某个资源,高优先级中断等这个资源,结果高优先级被低优先级阻塞。解决办法是尽量不在中断里用互斥锁,如果必须用,要支持优先级继承。在电机控制里,更好的做法是根本不让中断去抢资源,所有资源在初始化阶段就分配好。

5.3 常见问题速查表

现象可能原因排查方法解决方向
中断频率只有预期一半中心对齐模式两次溢出都执行了控制测 GPIO 翻转频率只在波峰或波谷执行
电机高速时异响中断偶尔丢失统计中断次数缩短 ISR 执行时间
电流环震荡控制频率与参数不匹配核对实际频率重算 PID 参数
采样噪声大采样时刻不在波峰波谷示波器看采样触发点调整采样时机
系统偶发卡死ISR 里有阻塞操作检查 ISR 代码移除延时和等待

5.4 几个我踩过的坑

第一个坑是忘了使能定时器的更新中断。定时器配好了、也开始计数了,但中断就是不进,查了半天发现TIM_ITConfig没调用。这种低级错误在赶进度的时候特别容易犯,建议初始化函数写完后逐行核对一遍。

第二个坑是中断标志位没清。STM32 的定时器中断进 ISR 后要手动清标志位,如果忘了清,ISR 会反复进入,系统直接卡死。HAL 库一般会在回调前帮你清,但如果你用的是寄存器操作,就得自己清。

第三个坑是共享变量没保护。我早期写的一个版本,主循环更新目标电流时被中断打断,导致电流环拿到一个错误值,电机猛地抖一下。后来改成单字长变量加双缓冲,问题就没了。这个坑不踩一次很难有深刻体会,但希望你看完这篇能避开。

6. 从 8 kHz 延伸出去的设计思考

6.1 控制频率是不是越高越好

很多人会想,既然 8 kHz 能跑,那提到 16 kHz、20 kHz 是不是控制效果更好?理论上频率越高,电流环带宽可以做得越宽,动态响应越快。但实际上有几个限制:第一,主控算力有限,频率翻倍意味着每拍的计算时间减半,可能来不及算完;第二,PWM 频率提高会增加开关损耗,发热上升;第三,电流采样对时序要求更苛刻,采样窗口变窄,噪声更容易进来。

所以控制频率的选择是算力、损耗、精度三者之间的平衡。ODrive 选 8 kHz 是一个比较稳妥的折中,既能保证不错的动态性能,又给计算留了余量。如果你做的是低电感高速电机,可能需要更高的频率;如果是大功率低速电机,8 kHz 甚至更低都够用。

6.2 时基设计对其他模块的影响

8 kHz 这个时基不只服务于电流环,它还是整个系统的时间基准。比如速度环和位置环通常跑在更低的频率上,通过对 8 kHz 中断计数来分频实现。故障检测的响应时间、通信的超时判断,也都可以基于这个时基来算。所以时基一旦定下来,整个系统的时间相关逻辑都要跟着它走。

这也意味着,如果你要改控制频率,不能只改定时器参数,还要检查所有依赖这个时基的模块。速度环的分频系数、故障检测的计数阈值、通信超时的时间换算,都得同步更新。漏改一个,就可能出现"电流环正常但速度环抽风"的怪现象。

6.3 移植到其他主控时的注意事项

如果你想把 ODrive 的这套时基设计移植到别的主控,比如国产的 GD32 或者别的 Cortex-M 芯片,核心思路是一样的,但细节要重新核对。时钟树结构不同,定时器时钟的计算方式可能不一样;中断向量表不同,ISR 的名字和注册方式要改;HAL 库的 API 也可能有差异。

我的建议是先把时钟树和定时器配置单独调通,用一个 GPIO 翻转来验证频率,确认无误后再往上叠控制逻辑。不要一上来就把整套代码搬过去,出了问题很难定位是时基的问题还是控制逻辑的问题。分步验证,是嵌入式调试最省时间的办法。

最后分享一个我个人的习惯:每次调定时器,我都会在纸上把时钟树画一遍,从晶振到系统时钟到总线时钟到定时器时钟,每一步的倍频分频都标清楚,然后算出 PSC 和 ARR。这张纸我会留着,后面改频率或者换芯片的时候直接对照,比翻代码快得多。这个笨办法帮我省了无数次排查时间,你也可以试试。

返回列表