4、OSAL移植实战:实现osal_timer、osal_thread、osal_mutex等接口、对接FreeRTOS
好,咱们接着往下走。上一章我们把OSAL的抽象层框架搭起来了,说白了就是定义了一堆接口函数,但里面都是空的。这一章,咱们就要往这些空壳子里填真正的代码——对接FreeRTOS。
我个人习惯,做移植的时候喜欢从最基础的开始。就像盖房子,先打地基。OSAL里最基础的是什么?我觉得是互斥锁(mutex)和定时器(timer)。线程(thread)虽然也基础,但它的实现往往依赖于前面两个。所以我的顺序是:先搞定mutex,再搞定timer,最后搞thread。
核心思路:OSAL是SOEM定义的一套抽象接口,FreeRTOS是具体的RTOS。我们的任务就是让OSAL的接口调用FreeRTOS的API。说白了,就是写一个“翻译层”。
4.1 移植前的准备工作
动手之前,先确认几件事。我在项目中遇到过好几次,移植到一半发现FreeRTOS的配置不对,又回头改,很浪费时间。
- FreeRTOSConfig.h 要检查:确保
configUSE_MUTEXES和configUSE_TIMERS都设置为1。这两个宏默认可能是0,不打开的话,你调用xSemaphoreCreateMutex()和xTimerCreate()会直接返回NULL。 - 堆大小要够:SOEM运行起来需要分配一些内存,尤其是创建socket和EtherCAT数据帧的时候。我建议把
configTOTAL_HEAP_SIZE至少设到 30KB 以上。如果你用的芯片RAM比较小,比如STM32F103的20KB,那就要精打细算了。 - 时钟节拍要匹配:OSAL的定时器是基于系统tick的。FreeRTOS的
configTICK_RATE_HZ默认是1000,也就是1ms一个tick。这个值我建议不要改,因为SOEM内部很多超时计算都是以毫秒为单位的。
小技巧:如果你不确定FreeRTOS的配置是否正确,可以先写一个简单的测试函数,调用 xSemaphoreCreateMutex(),看看返回值是不是NULL。如果是NULL,那肯定是配置问题。
4.2 实现osal_mutex
互斥锁,说白了就是一把锁。谁拿到锁,谁就能进临界区。FreeRTOS里用 SemaphoreHandle_t 来实现互斥锁。嗯,这里要注意,FreeRTOS的信号量和互斥量虽然都是 SemaphoreHandle_t 类型,但创建函数不同。互斥量要用 xSemaphoreCreateMutex(),不能用 xSemaphoreCreateBinary()。为什么?因为互斥量有优先级继承机制,能防止优先级反转。我在一个项目中就吃过这个亏,用了二进制信号量当互斥锁,结果高优先级任务被低优先级任务堵死了,查了两天才找到原因。
来看代码实现:
// osal_mutex.c
#include "osal.h"
#include "FreeRTOS.h"
#include "semphr.h"
int osal_mutex_init(osal_mutex_t *mutex)
{
// 创建一个FreeRTOS互斥量
SemaphoreHandle_t handle = xSemaphoreCreateMutex();
if (handle == NULL) {
return -1; // 创建失败
}
*mutex = (osal_mutex_t)handle;
return 0;
}
int osal_mutex_lock(osal_mutex_t *mutex)
{
// 获取互斥锁,等待时间设为portMAX_DELAY,表示一直等
if (xSemaphoreTake((SemaphoreHandle_t)*mutex, portMAX_DELAY) == pdTRUE) {
return 0;
}
return -1;
}
int osal_mutex_unlock(osal_mutex_t *mutex)
{
// 释放互斥锁
if (xSemaphoreGive((SemaphoreHandle_t)*mutex) == pdTRUE) {
return 0;
}
return -1;
}
void osal_mutex_free(osal_mutex_t *mutex)
{
// 删除互斥量
vSemaphoreDelete((SemaphoreHandle_t)*mutex);
*mutex = NULL;
}
这段代码逻辑很清晰。每个函数都对应一个FreeRTOS的API。你可能会问,为什么 osal_mutex_lock 要用 portMAX_DELAY?因为SOEM的API设计是阻塞式的,如果拿不到锁,就一直等下去。如果你想让调用者能设置超时时间,那就要改OSAL的接口定义了。但既然SOEM已经定好了接口,我们就按它的规矩来。
注意:在中断服务函数里,绝对不能调用 osal_mutex_lock!因为 xSemaphoreTake 带阻塞时间,而中断里不允许阻塞。如果你非要在中断里用,那得用 xSemaphoreTakeFromISR,但OSAL的接口不支持这种用法。所以,设计上要避免在中断里操作互斥锁。
4.3 实现osal_timer
定时器这块,SOEM用的是“一次性定时器”。什么意思?就是设定一个超时时间,时间到了就触发一次,然后定时器就停了。不是那种周期性的。FreeRTOS的软件定时器默认是周期性的,但我们可以通过设置 pdFALSE 参数让它变成一次性。
我记得第一次移植的时候,我忘了设置这个参数,结果定时器一直周期触发,导致SOEM的状态机不停地超时重试,整个系统都乱了。后来查了FreeRTOS的手册才发现问题。
// osal_timer.c
#include "osal.h"
#include "FreeRTOS.h"
#include "timers.h"
void osal_timer_start(osal_timer_t *timer, uint32_t time_ms)
{
// 启动一个一次性定时器,time_ms是超时时间
// pdFALSE 表示一次性定时器
xTimerChangePeriod((TimerHandle_t)*timer,
pdMS_TO_TICKS(time_ms),
0);
xTimerStart((TimerHandle_t)*timer, 0);
}
int osal_timer_expired(osal_timer_t *timer)
{
// 检查定时器是否超时
// FreeRTOS没有直接查询定时器是否超时的API
// 我们可以通过检查定时器的状态来判断
if (xTimerIsTimerActive((TimerHandle_t)*timer) == pdFALSE) {
return 1; // 定时器已停止,说明超时了
}
return 0;
}
int osal_timer_create(osal_timer_t *timer)
{
// 创建一个定时器,回调函数设为NULL
// 因为我们不需要回调,只需要查询状态
TimerHandle_t handle = xTimerCreate(
"osal_timer", // 名字,调试用
1000, // 初始周期,随便设
pdFALSE, // 一次性定时器
NULL, // 定时器ID
NULL // 回调函数,设为NULL
);
if (handle == NULL) {
return -1;
}
*timer = (osal_timer_t)handle;
return 0;
}
这里有个坑,我必须要说一下。FreeRTOS的软件定时器依赖于一个叫“定时器服务任务”的东西。这个任务的优先级和堆栈大小是在 FreeRTOSConfig.h 里配置的。如果这个任务的堆栈太小,或者优先级太低,定时器就可能不准,甚至不触发。我曾经在一个项目中,把定时器服务任务的堆栈设成了128字节,结果定时器经常“失踪”。后来改成256字节,问题就解决了。
建议:把 configTIMER_TASK_STACK_DEPTH 设为至少256,configTIMER_TASK_PRIORITY 设为比普通任务高一点,比如3或4。
4.4 实现osal_thread
线程,在FreeRTOS里叫“任务”。SOEM的线程接口很简单:创建、启动、停止。但FreeRTOS的任务一旦创建并启动,就会一直运行,除非你主动删除它。所以 osal_thread_create 实际上对应的是 xTaskCreate,而 osal_thread_stop 对应的是 vTaskDelete。
来看代码:
// osal_thread.c
#include "osal.h"
#include "FreeRTOS.h"
#include "task.h"
int osal_thread_create(void *thr, int stack_size, void *func, void *param)
{
// thr 是线程句柄,stack_size 是堆栈大小(单位:字节)
// func 是线程函数,param 是参数
BaseType_t ret;
TaskHandle_t handle = NULL;
// FreeRTOS的堆栈大小单位是字(word),不是字节
// 所以需要转换:stack_size / sizeof(portSTACK_TYPE)
// 对于32位MCU,sizeof(portSTACK_TYPE) = 4
ret = xTaskCreate(
(TaskFunction_t)func, // 线程函数
"osal_thread", // 任务名字
stack_size / sizeof(portSTACK_TYPE), // 堆栈大小(字)
param, // 参数
tskIDLE_PRIORITY + 1, // 优先级,比空闲任务高一级
&handle // 任务句柄
);
if (ret == pdPASS) {
*(TaskHandle_t*)thr = handle;
return 0;
}
return -1;
}
void osal_thread_stop(void *thr)
{
// 删除任务
if (thr != NULL && *(TaskHandle_t*)thr != NULL) {
vTaskDelete(*(TaskHandle_t*)thr);
*(TaskHandle_t*)thr = NULL;
}
}
这里有个细节要注意:stack_size 的单位是字节,但FreeRTOS的 xTaskCreate 要求堆栈大小以“字”为单位。对于32位MCU,一个字是4字节。所以要做除法。我见过有人忘了这个转换,直接把字节数传进去,结果任务堆栈被分配了4倍于预期的空间,浪费了RAM。反过来,如果传的值太小,任务可能栈溢出。
警告:SOEM的EtherCAT主站任务,堆栈大小建议至少设到1024字节(256字)。如果开启了DC同步模式,或者要处理大量从站,建议设到2048字节以上。否则,任务在运行过程中可能会栈溢出,导致系统崩溃。
4.5 整体对接与测试
好了,三个核心接口都实现了。现在把它们放到一起,编译一下。嗯,这里要注意,你的工程里要包含 osal.h、osal_mutex.c、osal_timer.c、osal_thread.c,以及FreeRTOS的源码。链接的时候,确保FreeRTOS的库路径正确。
我个人习惯,写一个简单的测试函数来验证移植是否正确:
// test_osal.c
#include "osal.h"
void test_thread(void *param)
{
osal_timer_t timer;
osal_mutex_t mutex;
// 测试互斥锁
osal_mutex_init(&mutex);
osal_mutex_lock(&mutex);
// 临界区操作...
osal_mutex_unlock(&mutex);
osal_mutex_free(&mutex);
// 测试定时器
osal_timer_create(&timer);
osal_timer_start(&timer, 100); // 100ms超时
while (!osal_timer_expired(&timer)) {
// 等待超时
vTaskDelay(10);
}
// 定时器已超时
// 测试完成,删除自己
vTaskDelete(NULL);
}
void main(void)
{
// 初始化硬件和FreeRTOS
// ...
// 创建测试线程
osal_thread_t thr;
osal_thread_create(&thr, 1024, test_thread, NULL);
// 启动调度器
vTaskStartScheduler();
}
如果这个测试能顺利跑完,说明你的OSAL移植成功了。接下来,就可以把SOEM的主站代码加进来了。
4.6 常见问题与避坑指南
- 定时器不触发:检查
configUSE_TIMERS是否开启,以及定时器服务任务的堆栈和优先级是否合理。 - 互斥锁死锁:检查是否有任务在持有锁的情况下被挂起或删除。我曾经在一个项目中,一个任务拿了锁之后调用了
vTaskDelay,结果另一个高优先级任务也在等这把锁,导致系统卡死。解决办法是:拿锁之后尽快释放,不要在临界区里做耗时操作。 - 线程堆栈溢出:FreeRTOS提供了
uxTaskGetStackHighWaterMark()函数,可以查看任务的堆栈使用情况。我建议在调试阶段,每个任务都调用一下这个函数,看看堆栈还剩多少。如果剩余空间小于总堆栈的20%,就加大堆栈。 - 编译错误:未定义的引用:检查是否把
osal_mutex.c、osal_timer.c、osal_thread.c都添加到了工程里。有时候漏了一个文件,链接就会报错。
总结一下:OSAL移植的本质,就是把SOEM的抽象接口映射到FreeRTOS的具体API上。互斥锁用 xSemaphoreCreateMutex,定时器用 xTimerCreate,线程用 xTaskCreate。只要把这三个核心接口搞定了,剩下的就是一些细节调整。嗯,这一章的内容就到这里。下一章,我们会把SOEM的网络接口(NIC驱动)移植到具体的硬件平台上。