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_MUTEXESconfigUSE_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.hosal_mutex.cosal_timer.cosal_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.cosal_timer.cosal_thread.c 都添加到了工程里。有时候漏了一个文件,链接就会报错。

总结一下:OSAL移植的本质,就是把SOEM的抽象接口映射到FreeRTOS的具体API上。互斥锁用 xSemaphoreCreateMutex,定时器用 xTimerCreate,线程用 xTaskCreate。只要把这三个核心接口搞定了,剩下的就是一些细节调整。嗯,这一章的内容就到这里。下一章,我们会把SOEM的网络接口(NIC驱动)移植到具体的硬件平台上。

OSAL移植架构图(对接FreeRTOS) SOEM 应用层 OSAL 抽象层(接口定义) osal_mutex osal_timer osal_thread xSemaphoreCreateMutex xTimerCreate / xTimerStart xTaskCreate FreeRTOS 内核 图:OSAL移植架构,从SOEM应用层到FreeRTOS内核的映射关系

专注资料整理