3、固件开发环境配置:STM32CubeMX工程生成、FreeRTOS任务规划、HAL库与LL库混合编程
做机器人运动学标定,说白了就是让电机按照你算好的轨迹去跑。但电机不会自己动,它得靠固件来驱动。这一章,我们就来搭这个固件开发环境。
我个人习惯,先把STM32CubeMX生成的工程骨架搞定,再往里填FreeRTOS的任务逻辑,最后用HAL库和LL库混合着把底层寄存器调明白。这三步走顺了,后面的标定算法才有地方跑。
3.1 用STM32CubeMX生成工程骨架
我刚开始用CubeMX时,总觉得它生成的代码太啰嗦。后来做多了才发现,它帮你省掉的其实是那些最容易出错的时钟配置和引脚映射。你想想看,一个机器人项目少说十几个GPIO,外加定时器、编码器接口、CAN总线,手写初始化代码?那得疯。
这里我列一下标定项目常用的外设配置清单:
| 外设模块 | 用途 | 推荐配置 |
|---|---|---|
| RCC (时钟) | 系统主频、外设时钟源 | HSE + PLL,主频168MHz (STM32F407) |
| TIM2/3/4/5 | 编码器接口,读取电机位置 | Encoder Mode,Counter Period 65535 |
| TIM8 | PWM输出,控制电机速度 | PWM Generation CH1~CH4,频率20kHz |
| USART1/2 | 与上位机通信,下发标定指令 | 115200 baud,8N1,DMA发送 |
| CAN1 | 与驱动器通信(部分方案) | 1Mbps,FIFO中断接收 |
| GPIO | 限位开关、急停、状态指示灯 | 上拉输入/推挽输出 |
在CubeMX里配好这些,点生成代码。嗯,这里要注意:生成前务必检查一下Project Settings里的Toolchain,选MDK-ARM或者STM32CubeIDE,别选错了。
3.2 FreeRTOS任务规划:让标定流程跑起来
机器人标定不是单线程能搞定的。你得同时读编码器、算运动学、发控制指令、还要跟PC通信。用裸机while循环?我试过,最后代码乱成一锅粥。
FreeRTOS的好处,就是把不同功能拆成独立任务。每个任务只管自己的事,通过队列和信号量来同步。我一般这样规划任务:
- 控制任务 (Control Task):最高优先级。负责读取编码器位置,计算逆运动学,输出PWM占空比。周期1ms。
- 通信任务 (Comm Task):中等优先级。处理上位机发来的标定指令,把结果打包发回去。用队列接收指令。
- 监控任务 (Monitor Task):低优先级。检查限位开关、电机温度、电压。出问题就发紧急停止信号。
- 标定计算任务 (Calib Task):中等优先级。跑标定算法,比如最小二乘法拟合参数。这个任务只在标定模式下才激活。
任务栈大小怎么定?我踩过坑。一开始给控制任务只配了256字节,结果跑着跑着就HardFault。后来用uxTaskGetStackHighWaterMark()一查,发现实际用了400多字节。建议控制任务至少512字节,通信任务256字节,标定任务1024字节(因为要跑矩阵运算)。
3.3 HAL库与LL库混合编程:取长补短
HAL库封装得好,但慢。LL库快,但代码量大。我的做法是:初始化用HAL,性能关键路径用LL。
举个例子,编码器读取:
// 初始化阶段:用HAL库配置定时器编码器模式
// 这部分CubeMX已经帮你生成了,不用手写
// 运行阶段:用LL库直接读寄存器,省去HAL的层层封装
// 在控制任务中,1ms周期调用
int32_t read_encoder(TIM_TypeDef *tim) {
// LL库直接操作寄存器,比HAL_TIM_GetCounter()快3倍以上
return (int32_t)LL_TIM_GetCounter(tim);
}
// 如果需要清零,直接写寄存器
void reset_encoder(TIM_TypeDef *tim) {
LL_TIM_SetCounter(tim, 0);
}
为什么这么干?HAL_TIM_GetCounter()里面有一堆断言、中断状态检查、超时处理。在1ms的控制周期里,每多一个函数调用都是浪费。LL库直接读CNT寄存器,一条指令搞定。
但LL库也有短板——配置复杂。比如初始化一个定时器,用LL库你得手动设置PSC、ARR、CCR、极性、预装载……十几行代码。而HAL库一句HAL_TIM_PWM_Init()就搞定。所以我的原则是:初始化走HAL,运行时走LL。
混合编程的核心思路:
- CubeMX生成的HAL初始化代码,不动它。
- 在用户代码区(/* USER CODE BEGIN */)里,用LL库写高性能函数。
- 中断服务函数里,尽量用LL库,减少中断延迟。
- 非实时任务(如通信、日志),继续用HAL库,开发效率高。
3.4 整体架构:一张图说清楚
下面这张SVG图,把整个固件架构串起来了。从CubeMX生成,到FreeRTOS任务调度,再到HAL/LL混合编程,一目了然。
3.5 避坑指南:我踩过的几个坑
最后,分享几个实际项目中遇到的坑,希望能帮你省点时间。
- CubeMX生成的代码里,HAL_Delay()默认用了SysTick中断。如果你在FreeRTOS里也用了SysTick做时间片调度,两者会冲突。解决办法:在FreeRTOSConfig.h里把configUSE_TICKLESS_IDLE设为0,或者把HAL_Delay()重定向到vTaskDelay()。
- LL库的GPIO翻转速度比HAL快,但要注意位带操作。我试过用LL_GPIO_TogglePin()在中断里翻转IO,用示波器看波形,比HAL_GPIO_TogglePin()快了将近1us。但如果你需要同时翻转多个引脚,建议用位带操作,一条指令搞定。
- FreeRTOS的任务栈大小,别猜,用工具测。在任务里加一行
UBaseType_t stackHighWaterMark = uxTaskGetStackHighWaterMark(NULL);,然后通过串口打印出来。我见过有人给通信任务配了1024字节,实际只用128字节,白白浪费内存。 - 中断优先级分组要统一。HAL库默认用NVIC_PRIORITYGROUP_4,FreeRTOS也要求用这个分组。如果你改了,FreeRTOS的临界区保护会失效。别问我怎么知道的……
好了,环境搭好了,任务分好了,库也混着用了。下一件事,就是让电机转起来,开始采集标定数据。不过那是后面章节的事,先把这一章的工程跑通再说。