资讯动态

ODrive 移植 FreeRTOS 实战:多轴 FOC 控制确定性优化

发布时间:2026/10/1 7:27:20 来源:尧图企业网站定制
1. 为什么要在 ODrive 上跑 RTOSODrive 这块板子在 DIY 圈子里火了好几年双路 FOC 驱动、编码器闭环、USB/CAN 接口一应俱全拿来驱动云台电机、机器人关节、CNC 主轴都相当顺手。但只要你真正把它用在多轴联动或者对时序有硬要求的场景里就会发现原厂固件那套裸机 中断的架构开始捉襟见肘。我最早是在一个三轴机械臂项目里踩到这个坑的三个 ODrive 通过 CAN 总线协同主机下发轨迹点的周期是 1ms结果 ODrive 这边偶尔会因为 USB 通信或者编码器 SPI 读取阻塞导致电流环控制周期抖动到 1.5ms 以上电机立刻发出可闻的啸叫力矩输出也变得不平滑。这个问题的根源在于 ODrive 原厂固件本质上是前后台架构主循环里轮询处理 USB、UART、CAN 的命令解析和状态上报而 FOC 的电流环、速度环、位置环全部挂在定时器中断里。中断优先级一旦被高优先级任务抢占或者主循环里有阻塞操作整个控制链路的确定性就没了。对于单轴、低动态响应的应用这套架构够用但一旦涉及多轴同步、高频轨迹跟踪、或者需要同时跑通信协议栈和上层逻辑裸机架构的短板就暴露无遗。把FreeRTOS这类实时操作系统移植到 ODrive 上核心目的不是为了用 RTOS 而用 RTOS而是要把控制任务、通信任务、状态管理任务解耦让每个任务有明确的优先级和确定的调度周期。电流环依然可以放在最高优先级的中断或任务里保证 10kHz~20kHz 的稳定执行通信协议栈放到中低优先级任务即使偶尔阻塞也不会影响控制上层轨迹规划、状态机、日志记录各占一个任务互不干扰。这就是ODrive 跑 RTOS这件事的真正价值——用调度器换来确定性。适合读这篇内容的人我大致分三类一是已经玩过 ODrive 原厂固件、想进一步做多轴协同或者定制控制逻辑的进阶玩家二是做机器人关节、云台、精密运动控制需要把 ODrive 当核心驱动板来用的工程师三是正在学 FreeRTOS、想找一个真实且有一定复杂度的项目来练手的嵌入式学习者。不管你是哪一类下面这些从选型到落地的细节应该都能帮你少走不少弯路。2. 整体方案设计与任务划分思路2.1 硬件资源盘点与可行性判断ODrive 的硬件版本有好几代常见的是 v3.5、v3.6 和基于 STM32F405 的版本。以 v3.5 为例主控是STM32F405RGT6168MHz 主频192KB SRAM1MB Flash外设包括两路 TIM1/TIM8 高级定时器用于 PWM 输出、多路 ADC 用于电流采样、SPI 接口接编码器、USB FS、CAN、UART。这个资源跑 FreeRTOS 是绰绰有余的——FreeRTOS 内核本身在 Cortex-M4 上占用大概 6~10KB Flash、1~2KB RAM剩下的资源完全够用。但这里有个关键点ODrive 原厂固件已经占用了大量外设和中断。你要做的不是从零开始写而是在理解原厂代码结构的基础上把裸机主循环改造成 RTOS 任务。所以第一步是梳理清楚哪些中断是必须保留的、哪些可以改成任务、哪些可以合并。我当时的做法是画了一张中断和任务的映射表功能模块原厂实现方式RTOS 改造方案优先级电流环 FOCTIM1/TIM8 中断保留中断优先级最高5 (最高)编码器采样SPI 轮询/中断独立任务周期 100us4速度/位置环主循环轮询独立任务周期 1ms3CAN 通信中断 主循环独立任务 中断通知2USB 通信主循环轮询独立任务1状态上报/日志主循环低优先级任务0 (最低)这张表是整个改造的骨架。核心原则是越靠近硬件的、时序要求越严的越要靠近中断越靠近应用层的、越可以容忍延迟的越要放到低优先级任务。2.2 为什么选 FreeRTOS 而不是其他 RTOS市面上 RTOS 不少FreeRTOS、RT-Thread、Zephyr、ThreadX 各有拥趸。我选 FreeRTOS 的理由很实际第一STM32CubeMX 原生支持。你可以直接在 CubeMX 里勾选 FreeRTOS生成带 CMSIS-RTOS v2 封装的工程框架任务创建、队列、信号量的 API 都是现成的省去大量移植工作。ODrive 本身就是 STM32F405CubeMX 生成的底层初始化代码可以直接参考。第二社区资料最多。正点原子、野火、硬汉嵌入式都有完整的 FreeRTOS 教程和笔记遇到问题搜索一下基本都能找到答案。相比之下 Zephyr 的学习曲线陡得多ThreadX 虽然性能好但中文资料偏少。第三内核精简可裁剪。FreeRTOS 的 config 文件里可以把不需要的功能全部关掉比如软件定时器、事件组、流缓冲区最终内核占用可以压到很小。对于 ODrive 这种资源不算特别宽裕的板子这一点很重要。第四与 ODrive 原厂代码风格兼容。ODrive 原厂固件是用 C 写的FreeRTOS 也是 C 接口混编没有障碍。你完全可以把原厂的Axis、Motor、Encoder类保留只是在它们外面包一层任务调度。2.3 任务划分的粒度与优先级设计任务划分太粗等于没解耦划分太细上下文切换开销又上来了。我的经验是按控制周期分层次10kHz~20kHz 层电流环 FOC必须放在定时器中断里不能做成任务。因为 FreeRTOS 的任务切换最快也在微秒级而且受临界区影响做不到 50us 的稳定周期。这一层保留原厂的中断服务程序只做最核心的 Clarke/Park 变换、PID 计算、SVPWM 输出。1kHz 层速度环、位置环、编码器角度更新。可以做成一个高优先级任务用vTaskDelayUntil保证 1ms 周期。100Hz~1kHz 层CAN 命令解析、轨迹插值。做成中优先级任务用队列接收中断发来的数据。10Hz~100Hz 层USB 通信、状态上报、参数读写。低优先级任务允许被抢占。事件驱动层故障处理、模式切换。用信号量或任务通知触发优先级根据紧急程度定。这里有个坑要提前说FreeRTOS 的vTaskDelayUntil在周期任务里如果遇到任务执行时间超过周期会出现追周期现象导致后续任务连续执行。解决办法是加一个判断如果发现已经超时就跳过本次或者重置基准时间。我在速度环任务里就遇到过这个问题后来加了一行if (xTaskGetTickCount() - lastWakeTime period * 2) lastWakeTime xTaskGetTickCount();才稳定下来。3. 核心细节解析与实操要点3.1 中断优先级与 FreeRTOS 的兼容配置这是移植过程中最容易翻车的地方。Cortex-M4 的 NVIC 优先级分组、FreeRTOS 的configMAX_SYSCALL_INTERRUPT_PRIORITY、以及 STM32 HAL 库的优先级设置三者必须对齐否则会出现在中断里调用 FreeRTOS API 导致硬件错误的经典问题。具体来说STM32F405 的 NVIC 优先级是 4 位分组建议设为NVIC_PRIORITYGROUP_4即 4 位全部用于抢占优先级没有子优先级。FreeRTOS 的configMAX_SYSCALL_INTERRUPT_PRIORITY通常设为 5意思是优先级数值大于等于 5 的中断数值越大优先级越低才能调用FromISR结尾的 API。而电流环的定时器中断优先级要设为 0~4 之间这样它永远不会被 FreeRTOS 的临界区屏蔽保证控制环的实时性。我当时的配置是这样的// FreeRTOSConfig.h #define configPRIO_BITS 4 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 #define configKERNEL_INTERRUPT_PRIORITY \ (configLIBRARY_LOWEST_INTERRUPT_PRIORITY (8 - configPRIO_BITS)) #define configMAX_SYSCALL_INTERRUPT_PRIORITY \ (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY (8 - configPRIO_BITS))然后在main.c里设置HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); // 电流环定时器中断优先级 2高于 configMAX_SYSCALL HAL_NVIC_SetPriority(TIM1_UP_TIM10_IRQn, 2, 0); // CAN 接收中断优先级 6可以调用 FromISR API HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 6, 0);注意如果你在优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断里调用了任何 FreeRTOS API哪怕只是xQueueSendFromISR系统会在第一次运行时直接进入configASSERT死循环。这个坑我踩过两次一次是 CAN 中断优先级设成了 4一次是编码器 SPI 中断忘了改。3.2 电流环中断与任务的边界划分电流环是整个系统的心脏它的执行时间必须绝对确定。我的做法是中断里只做最核心的计算所有非必要的操作全部踢到任务里。具体来说TIM1 的更新中断频率 20kHz里只做这几件事读取 ADC 注入组的电流采样值Ia、Ib执行 Clarke 变换和 Park 变换执行 d 轴和 q 轴的 PID 计算执行反 Park 变换和 SVPWM 占空比计算写入定时器比较寄存器而下面这些操作全部放到任务里编码器角度读取和校准1kHz 任务速度计算和速度环 PID1kHz 任务电流环 PID 参数更新通过队列从通信任务传入故障检测和状态上报100Hz 任务这样做的原因是电流环中断的执行时间必须控制在几微秒以内。如果在中途去读 SPI 编码器SPI 传输本身就要几微秒再加上可能的等待中断时间直接翻倍20kHz 的周期就保不住了。我实测过把编码器读取放进电流环中断后中断执行时间从 3.2us 涨到了 8.7usPWM 波形开始出现明显的抖动。3.3 编码器数据的一致性与共享编码器角度是电流环和速度环都要用的数据这就涉及任务间共享数据的原子性问题。如果电流环中断正在读取角度值而速度环任务同时更新了这个值就可能读到半新半旧的数据导致 Park 变换用错角度电机抖动。我的解决方案是双缓冲 原子指针切换typedef struct { float angle; float velocity; uint32_t timestamp; } EncoderData_t; static EncoderData_t encBuf[2]; static volatile uint8_t encActiveBuf 0; // 编码器任务里更新 void EncoderTask(void *arg) { EncoderData_t *buf encBuf[encActiveBuf ^ 1]; buf-angle readEncoderAngle(); buf-velocity calculateVelocity(); buf-timestamp xTaskGetTickCount(); encActiveBuf ^ 1; // 原子切换 } // 电流环中断里读取 void CurrentLoopISR(void) { EncoderData_t *buf encBuf[encActiveBuf]; float theta buf-angle; // ... FOC 计算 }这个方案的好处是读和写永远不会冲突因为读的是当前活动缓冲区写的是另一个缓冲区切换只是一个字节的赋值在 Cortex-M4 上是原子的。代价是多占了一倍的内存但对于只有几个浮点数的结构体来说完全可以接受。实操心得encActiveBuf一定要声明为volatile否则编译器优化后可能把它缓存到寄存器里导致中断里读到的永远是旧值。这个坑我在调试时遇到过电机转起来角度完全不对查了半天才发现是编译器优化的问题。4. 实操过程与核心环节实现4.1 工程框架搭建与 FreeRTOS 移植第一步是在 STM32CubeMX 里创建工程。选好 STM32F405RGT6配置时钟树HSE 8MHz 晶振PLL 到 168MHz然后开启需要的外设TIM1 和 TIM8 的互补 PWM 输出、ADC1 和 ADC2 的注入组、SPI1 和 SPI2、CAN1、USB OTG FS、UART2。这些配置可以参考 ODrive 原厂的硬件原理图引脚分配基本一致。然后在 Middleware 里勾选 FreeRTOS接口选 CMSIS_V2配置里把TOTAL_HEAP_SIZE设为 32KBODrive 的 192KB SRAM 够用MINIMAL_STACK_SIZE设为 128 字MAX_PRIORITIES设为 8USE_PREEMPTION开启USE_MUTEXES开启。生成代码后你会得到一个带MX_FREERTOS_Init()的工程。这时候不要急着写业务逻辑先做一件事把 ODrive 原厂的 FOC 相关代码移植过来。原厂代码里Motor、Encoder、Axis这几个类是核心但它们的实现依赖大量 HAL 库调用和全局变量直接复制会有一堆编译错误。我的做法是分层移植底层硬件抽象层把原厂的HAL_GPIO_WritePin、HAL_ADC_GetValue等调用保留但把初始化部分改成 CubeMX 生成的MX_xxx_Init()。FOC 算法层把 Clarke、Park、SVPWM、PID 这些纯计算函数直接复制它们不依赖硬件改一下头文件就能用。状态管理层原厂的Axis::run_control_loop()拆成多个任务函数每个任务负责一个环节。4.2 电流环中断的完整实现电流环是整个系统里最需要精细调校的部分。下面是我实际使用的 TIM1 更新中断服务程序的核心结构void TIM1_UP_TIM10_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim1, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim1, TIM_FLAG_UPDATE); // 1. 读取电流采样ADC 注入组已由硬件触发完成 float ia adcToCurrent(ADC1-JDR1); float ib adcToCurrent(ADC2-JDR1); float ic -ia - ib; // 2. 读取编码器角度双缓冲无锁 EncoderData_t *enc encBuf[encActiveBuf]; float theta enc-angle; float theta_elec theta * POLE_PAIRS; // 3. Clarke 变换 float i_alpha ia; float i_beta (ia 2.0f * ib) * ONE_BY_SQRT3; // 4. Park 变换 float sin_t, cos_t; fast_sincos(theta_elec, sin_t, cos_t); float i_d i_alpha * cos_t i_beta * sin_t; float i_q -i_alpha * sin_t i_beta * cos_t; // 5. 电流环 PID float v_d pid_update(pid_d, current_setpoint_d - i_d); float v_q pid_update(pid_q, current_setpoint_q - i_q); // 6. 反 Park 变换 float v_alpha v_d * cos_t - v_q * sin_t; float v_beta v_d * sin_t v_q * cos_t; // 7. SVPWM svpwm_calc(v_alpha, v_beta, duty_a, duty_b, duty_c); // 8. 写入比较寄存器 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, duty_a); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, duty_b); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_3, duty_c); } }这段代码在 168MHz 的 STM32F405 上执行时间大约是 3.5us20kHz 周期是 50us占用率 7%留足了余量。其中fast_sincos是我用查表加线性插值实现的比标准sinf/cosf快大约 5 倍精度在 0.01 弧度以内对 FOC 来说完全够用。注意current_setpoint_d和current_setpoint_q是速度环任务写入的必须用volatile修饰或者用原子操作保护。我一开始忘了加volatile结果电流环一直用旧值电机响应迟钝查了两天才发现。4.3 速度环与位置环任务的实现速度环任务周期 1ms优先级设为 3低于电流环中断高于通信任务。它的核心逻辑是void VelocityTask(void *arg) { TickType_t lastWake xTaskGetTickCount(); const TickType_t period pdMS_TO_TICKS(1); for (;;) { // 读取编码器数据 EncoderData_t *enc encBuf[encActiveBuf]; float vel enc-velocity; // 速度环 PID float iq_setpoint pid_update(pid_vel, velocity_setpoint - vel); // 写入电流环设定值原子操作 taskENTER_CRITICAL(); current_setpoint_q iq_setpoint; current_setpoint_d 0.0f; // 通常 d 轴给定为 0 taskEXIT_CRITICAL(); // 位置环如果启用 if (control_mode POSITION_MODE) { float pos enc-angle; float vel_cmd pid_update(pid_pos, position_setpoint - pos); velocity_setpoint vel_cmd; } vTaskDelayUntil(lastWake, period); } }这里用taskENTER_CRITICAL()保护电流设定值的写入是因为电流环中断可能在任意时刻读取这两个变量。虽然 Cortex-M4 上 32 位浮点数的读写本身是原子的但 d 轴和 q 轴两个值需要同时更新否则可能出现 d 轴新值、q 轴旧值的中间状态。临界区只有几个指令周期对系统实时性影响可以忽略。位置环我放在同一个任务里通过control_mode变量切换。这样做的好处是位置环和速度环共享编码器数据不需要额外的同步机制。缺点是位置环的带宽受限于 1ms 周期对于高动态的位置跟踪可能不够。如果需要更高的位置环带宽可以把位置环也放进电流环中断但那样中断执行时间会增加需要权衡。4.4 通信任务与命令队列CAN 和 USB 通信都做成独立任务通过队列接收中断发来的数据。以 CAN 为例QueueHandle_t canRxQueue; void CAN_RX0_IRQHandler(void) { CAN_RxHeaderTypeDef header; uint8_t data[8]; HAL_CAN_GetRxMessage(hcan1, CAN_RX_FIFO0, header, data); CanMessage_t msg { .id header.StdId, .len header.DLC }; memcpy(msg.data, data, header.DLC); BaseType_t woken pdFALSE; xQueueSendFromISR(canRxQueue, msg, woken); portYIELD_FROM_ISR(woken); } void CanTask(void *arg) { CanMessage_t msg; for (;;) { if (xQueueReceive(canRxQueue, msg, portMAX_DELAY) pdTRUE) { processCanCommand(msg); } } }portYIELD_FROM_ISR是关键它让中断退出后立即切换到等待队列的任务减少延迟。如果没有这一句任务要等到下一个系统节拍才会被调度延迟可能达到 1ms。USB 通信任务类似但 USB 的中断处理更复杂我建议直接用 STM32 的 USB CDC 库在CDC_Receive_FS回调里往队列发数据任务里解析。注意 USB 任务的优先级要设低一些因为 USB 传输本身有重传机制偶尔的延迟不会丢数据。5. 常见问题与排查技巧实录5.1 FreeRTOS 堆栈溢出与检测FreeRTOS 任务堆栈溢出是新手最容易遇到的问题症状通常是任务运行一段时间后突然死机或者某个变量莫名其妙被改写。我在调试时遇到过一次速度环任务运行几分钟后velocity_setpoint突然变成 0电机停转。查了半天才发现是 CAN 任务的堆栈太小溢出后踩到了相邻任务的内存。FreeRTOS 提供了两种堆栈溢出检测机制在FreeRTOSConfig.h里配置#define configCHECK_FOR_STACK_OVERFLOW 2模式 1 只在任务切换时检查堆栈指针是否越界速度快但可能漏检模式 2 会在任务创建时填充一个魔术字切换时检查最后几个字节是否被改写检测更可靠但稍慢。我建议用模式 2然后在vApplicationStackOverflowHook里打印出错的任务名void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { printf(Stack overflow in task: %s\n, pcTaskName); for (;;); }各任务的堆栈大小我实测下来的推荐值任务推荐堆栈字说明速度环任务256有浮点运算和 PID位置环任务256同上CAN 任务128主要是解析和队列操作USB 任务256USB 库调用较深状态上报任务128主要是格式化输出编码器任务128SPI 读取和计算实操心得configCHECK_FOR_STACK_OVERFLOW只在调试阶段开量产固件里关掉因为它会增加每次任务切换的开销。另外vApplicationStackOverflowHook里不要调用任何 FreeRTOS API因为此时系统状态已经不可信了。5.2 中断延迟与任务抖动RTOS 环境下中断延迟主要来自两个方面一是 FreeRTOS 的临界区taskENTER_CRITICAL会屏蔽中断二是高优先级任务抢占导致的调度延迟。对于电流环这种硬实时中断必须保证它的优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY这样它永远不会被 FreeRTOS 的临界区屏蔽。但即使这样如果系统里有大量taskENTER_CRITICAL调用中断延迟还是会增加。我的做法是尽量用原子操作代替临界区。比如更新一个 32 位变量直接用__atomic_store_n或者简单的赋值就行不需要进临界区。只有涉及多个变量的一致性更新时才用临界区而且临界区里的代码要尽可能短。任务抖动方面vTaskDelayUntil的精度受系统节拍频率影响。默认的configTICK_RATE_HZ是 1000即 1ms 一个节拍那么 1ms 周期的任务抖动最大就是 1ms。如果对抖动敏感可以把节拍频率提高到 10000但那样系统开销会增加。我的经验是1kHz 的控制任务用 1kHz 节拍就够了抖动在可接受范围内如果需要更精确的周期直接用定时器中断触发任务通知。5.3 FOC 启动与转子初始位置检测这是 FOC 控制里绕不开的问题。无感 FOC 需要知道转子初始位置才能正确换相而 ODrive 原厂固件用的是高频注入 凸极追踪的方法。移植到 RTOS 后这个启动过程要放在一个独立任务里因为它需要几百毫秒到几秒的时间不能阻塞其他任务。我的启动流程是这样的预定位给 d 轴一个固定电流让转子转到已知位置持续 500ms。高频注入在 d 轴叠加一个高频小信号通过检测 q 轴电流响应来判断凸极位置。滑模观测器启动当转速超过一定阈值后切换到滑模观测器估算角度。闭环运行观测器收敛后进入正常的 FOC 闭环。这个过程放在StartupTask里优先级设为 4高于速度环启动完成后自动删除自己或者挂起。注意启动过程中电流环中断必须正常运行否则高频注入的响应检测无法进行。注意高频注入的幅值和频率需要根据电机参数调整。幅值太大会导致电机发热和噪声太小则检测不到响应。我一般从额定电流的 10% 开始试频率设在 1kHz 左右然后根据实际响应调整。5.4 常见问题速查表现象可能原因排查方法解决方案电机抖动、啸叫电流环周期不稳定用示波器测 PWM 波形周期检查中断优先级确保高于 configMAX_SYSCALL任务运行一段时间后死机堆栈溢出开启 configCHECK_FOR_STACK_OVERFLOW增大对应任务堆栈中断里调用 API 后死机优先级配置错误检查 configASSERT 是否触发调整中断优先级到 configMAX_SYSCALL 以下编码器角度跳变数据竞争检查双缓冲切换是否原子用 volatile 修饰活动缓冲区索引CAN 通信丢包队列溢出检查队列长度和任务处理速度增大队列长度或提高任务优先级电机启动失败初始位置检测不准观察启动时电流波形调整高频注入幅值和频率速度环响应迟钝电流设定值未更新检查 volatile 修饰给共享变量加 volatile 或原子操作USB 连接不稳定USB 任务优先级过高检查任务优先级分配降低 USB 任务优先级避免抢占控制任务6. 性能实测与优化经验6.1 改造前后的对比数据我在同一块 ODrive v3.5 上做了对比测试电机是 5065 云台电机编码器是 AS5047P负载是 1kg 的机械臂关节。测试项目包括电流环抖动、速度环带宽、多轴同步误差。指标原厂裸机固件FreeRTOS 改造后改善幅度电流环周期抖动±15%±2%显著改善速度环带宽80Hz150Hz提升 87%位置环跟踪误差1Hz 正弦0.8°0.3°降低 62%多轴同步误差CAN 1ms 周期200us50us降低 75%CPU 占用率45%38%略降电流环抖动改善最明显因为原厂固件里 USB 和 CAN 的处理会偶尔阻塞主循环间接影响中断响应。改成 RTOS 后通信任务被隔离电流环中断的响应时间稳定在 1us 以内。速度环带宽提升是因为原厂固件里速度环和通信共用一个主循环实际执行周期不稳定。改成 1ms 周期任务后速度环的采样和控制周期严格一致PID 参数可以调得更激进。6.2 优化技巧与注意事项第一把频繁调用的函数放到 RAM 里执行。STM32F405 的 Flash 访问有等待周期168MHz 下需要 5 个等待周期。把电流环中断服务程序和 FOC 计算函数用__attribute__((section(.RamFunc)))放到 RAM 里执行速度能提升 20% 左右。具体做法是在链接脚本里定义一个.RamFunc段然后在启动时把这段代码从 Flash 复制到 RAM。第二用 DMA 减轻 CPU 负担。编码器 SPI 读取可以用 DMAADC 采样也可以用 DMA。这样 CPU 只需要在 DMA 完成中断里处理数据不需要轮询等待。ODrive 原厂固件里 ADC 已经是 DMA 触发的但 SPI 编码器还是轮询改成 DMA 后编码器任务的 CPU 占用从 8% 降到了 2%。第三合理设置系统节拍。configTICK_RATE_HZ设为 1000 时1ms 周期的任务抖动最大 1ms。如果对抖动敏感可以设为 2000 或 5000但要注意configTICK_RATE_HZ越高系统节拍中断的开销越大。我的经验是 1000 到 2000 之间比较平衡。第四避免在中断里做浮点运算。Cortex-M4 有硬件 FPU浮点运算本身很快但如果在中断里做大量浮点运算会延长中断执行时间。我的做法是把浮点运算尽量放在任务里中断里只做必要的变换和 PID。如果必须在中断里做浮点确保 FPU 上下文保存已开启configENABLE_FPU设为 1。第五用任务通知代替信号量。FreeRTOS 的任务通知xTaskNotifyFromISR比信号量更快占用内存更少。在只需要一对一同步的场景比如中断通知任务处理数据用任务通知比用信号量效率高 30% 以上。6.3 后续扩展方向这套 RTOS 框架搭好之后扩展就方便多了。比如要加一个轨迹规划任务只需要创建一个新任务从队列接收轨迹点用五次多项式或者 S 曲线插值然后更新位置设定值。要加数据记录功能创建一个低优先级任务定期把电流、速度、位置写入 SD 卡或者通过 USB 上传。要加多轴协同可以通过 CAN 或者以太网在多个 ODrive 之间同步时钟每个 ODrive 的轨迹规划任务根据同步时钟对齐。我目前正在做的一个扩展是基于 CANopen 的分布式控制把 ODrive 作为 CANopen 从站支持 CSP循环同步位置模式。这样可以直接接入工业机器人控制器用标准的 CANopen 协议栈来调度多轴运动。FreeRTOS 的任务架构让这个扩展变得很自然CANopen 协议栈跑一个任务PDO 映射和同步信号处理跑另一个任务控制环完全不受影响。最后分享一个小技巧如果你在调试时发现某个任务偶尔执行时间过长可以用uxTaskGetSystemState获取所有任务的运行时间统计找出 CPU 占用最高的任务。FreeRTOS 还提供了vTaskGetRunTimeStats函数配合一个高精度的定时器可以精确到微秒级地统计每个任务的执行时间。这个工具在优化阶段非常有用我靠它定位了好几个性能瓶颈。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑