3. OSAL层解析:操作系统抽象层

各位同学,咱们今天聊聊SOEM里一个容易被忽视、但实际非常重要的模块——OSAL层。

OSAL,全称Operating System Abstraction Layer,操作系统抽象层。说白了,就是给SOEM穿上一件“通用外衣”。

为什么要搞这么一层?

你想啊,SOEM是个开源主站,它得跑在各种平台上:Linux、Windows、RTOS、甚至裸机。每个平台的线程怎么创建?定时器怎么用?互斥锁怎么加?都不一样。如果没有OSAL,你每移植到一个新平台,就得把整个SOEM的代码翻个底朝天,把所有跟系统相关的调用全改一遍。那画面太美我不敢看。

OSAL的作用,就是把所有跟操作系统相关的接口,统一封装成一组标准函数。你移植的时候,只需要实现这组函数就行了。SOEM的核心代码,完全不用动。

我个人习惯,拿到一个新平台,第一件事就是看OSAL层能不能跑通。这步搞定了,后面就顺了。

3.1 osal.h 头文件解析

咱们先打开osal.h,看看它定义了哪些东西。

// osal.h 核心内容
#ifndef __OSAL_H__
#define __OSAL_H__

#include <stdint.h>
#include <stdbool.h>

// 定时器结构体
typedef struct {
    uint32_t    tv_sec;     // 秒
    uint32_t    tv_usec;    // 微秒
} osal_timerticks_t;

// 线程函数指针类型
typedef void *(*osal_threadfunc_t)(void *);

// 线程句柄
typedef struct {
    void *handle;           // 平台相关句柄
    bool   active;          // 是否活跃
} osal_thread_t;

// 互斥锁
typedef struct {
    void *handle;           // 平台相关句柄
} osal_mutex_t;

// 信号量
typedef struct {
    void *handle;           // 平台相关句柄
} osal_sem_t;

// 定时器
typedef struct {
    void *handle;           // 平台相关句柄
    uint32_t interval_us;   // 定时周期(微秒)
    bool     active;        // 是否激活
} osal_timer_t;

// 函数声明
int  osal_timer_create(osal_timer_t *timer, uint32_t interval_us);
int  osal_timer_start(osal_timer_t *timer);
int  osal_timer_stop(osal_timer_t *timer);
bool osal_timer_expired(osal_timer_t *timer);
int  osal_timer_delete(osal_timer_t *timer);

int  osal_thread_create(osal_thread_t *thread, osal_threadfunc_t func, void *arg, uint32_t stack_size);
int  osal_thread_join(osal_thread_t *thread);
int  osal_thread_delete(osal_thread_t *thread);

int  osal_mutex_create(osal_mutex_t *mutex);
int  osal_mutex_lock(osal_mutex_t *mutex);
int  osal_mutex_unlock(osal_mutex_t *mutex);
int  osal_mutex_delete(osal_mutex_t *mutex);

int  osal_sem_create(osal_sem_t *sem, int init_count);
int  osal_sem_wait(osal_sem_t *sem, uint32_t timeout_ms);
int  osal_sem_post(osal_sem_t *sem);
int  osal_sem_delete(osal_sem_t *sem);

uint64_t osal_get_system_time_us(void);
void     osal_sleep_ms(uint32_t ms);

#endif // __OSAL_H__

嗯,这里要注意,osal_timerticks_t这个结构体,它存的是秒和微秒。为什么不用毫秒?因为EtherCAT的同步精度要求很高,微秒级是必须的。我在做运动控制项目时,曾经因为定时器精度不够,导致伺服驱动器抖动。后来换成微秒级定时,问题就解决了。

核心设计思想:OSAL层只定义接口,不实现具体功能。每个平台需要提供自己的实现文件,比如osal_linux.cosal_win32.cosal_freertos.c等。

3.2 osal.c 源码分析

接下来看osal.c。这个文件是OSAL层的“骨架”,它不包含具体实现,而是提供一些辅助函数和默认行为。

// osal.c 核心内容
#include "osal.h"
#include <stdio.h>
#include <string.h>

// 默认的定时器创建函数
// 如果平台没有提供,这里会返回错误
__attribute__((weak)) int osal_timer_create(osal_timer_t *timer, uint32_t interval_us)
{
    // 弱符号定义,允许平台覆盖
    (void)timer;
    (void)interval_us;
    return -1; // 未实现
}

// 系统时间获取
// 这个函数通常由平台提供
__attribute__((weak)) uint64_t osal_get_system_time_us(void)
{
    // 返回0表示未实现
    return 0;
}

// 简单的毫秒延时
// 如果平台没有提供精确延时,可以用这个
void osal_sleep_ms(uint32_t ms)
{
    // 忙等待实现
    uint64_t start = osal_get_system_time_us();
    uint64_t end = start + (uint64_t)ms * 1000;
    while (osal_get_system_time_us() < end) {
        // 空转
    }
}

这里有个技巧,__attribute__((weak))是GCC的弱符号属性。意思是:如果平台没有提供这个函数的实现,就使用这里的默认实现(通常返回错误)。如果平台提供了,就覆盖它。

我曾经在移植到某个RTOS时,发现定时器创建总是失败。查了半天,原来是RTOS的定时器API参数跟SOEM默认的不一样。我重写了osal_timer_create,问题就解决了。嗯,这就是弱符号的好处——你不需要改SOEM的代码,只需要提供自己的实现。

3.3 定时器接口详解

定时器是EtherCAT主站的核心。DC(分布式时钟)同步、周期性数据收发,都依赖精确的定时。

咱们看看定时器的生命周期:

  1. 创建osal_timer_create,指定周期(微秒)
  2. 启动osal_timer_start,开始计时
  3. 检查osal_timer_expired,判断是否超时
  4. 停止osal_timer_stop,暂停计时
  5. 删除osal_timer_delete,释放资源

避坑指南:我曾经在Linux上使用timerfd实现定时器,发现osal_timer_expired返回后,定时器不会自动重置。需要手动调用osal_timer_start重新启动。这个行为跟硬件定时器不同,要注意。

定时器的典型用法:

// 定时器使用示例
osal_timer_t my_timer;

// 创建一个1ms的定时器
osal_timer_create(&my_timer, 1000); // 1000微秒 = 1毫秒

// 启动
osal_timer_start(&my_timer);

while (1) {
    if (osal_timer_expired(&my_timer)) {
        // 定时时间到,执行周期性任务
        // 比如发送EtherCAT帧
        
        // 重新启动定时器
        osal_timer_start(&my_timer);
    }
    
    // 其他任务
}

3.4 线程接口详解

SOEM的主站通常运行在单独的线程中。这个线程负责周期性地发送和接收EtherCAT帧。

线程接口包括:

  • 创建osal_thread_create,指定线程函数、参数、栈大小
  • 等待osal_thread_join,等待线程结束
  • 删除osal_thread_delete,强制终止线程

线程函数的签名是固定的:

void *my_ecat_thread(void *arg)
{
    // 参数通常是SOEM的上下文
    ecat_context_t *ctx = (ecat_context_t *)arg;
    
    while (ctx->running) {
        // 发送帧
        ecat_send_frame(ctx);
        
        // 接收帧
        ecat_receive_frame(ctx);
        
        // 等待下一个周期
        osal_sleep_ms(1); // 1ms周期
    }
    
    return NULL;
}

// 创建线程
osal_thread_t thread;
osal_thread_create(&thread, my_ecat_thread, &ctx, 4096);

注意:线程栈大小要合理设置。太小会导致栈溢出,太大浪费内存。我一般给4KB~8KB,如果用到printf等库函数,可能需要更大。曾经在STM32上因为栈给小了,程序跑着跑着就HardFault,查了两天才发现是栈溢出。

3.5 互斥锁与信号量

多线程环境下,共享资源的保护是必须的。OSAL提供了互斥锁和信号量。

接口 功能 典型场景
osal_mutex_create/lock/unlock/delete 互斥锁,保护共享资源 保护EtherCAT状态机、数据缓冲区
osal_sem_create/wait/post/delete 信号量,用于同步 通知数据到达、等待事件

互斥锁的使用:

osal_mutex_t data_mutex;
osal_mutex_create(&data_mutex);

// 写数据
osal_mutex_lock(&data_mutex);
shared_data = new_value;
osal_mutex_unlock(&data_mutex);

// 读数据
osal_mutex_lock(&data_mutex);
value = shared_data;
osal_mutex_unlock(&data_mutex);

信号量的使用:

osal_sem_t data_ready;
osal_sem_create(&data_ready, 0); // 初始计数为0

// 生产者线程
void *producer(void *arg)
{
    // 产生数据
    osal_sem_post(&data_ready); // 通知消费者
    return NULL;
}

// 消费者线程
void *consumer(void *arg)
{
    osal_sem_wait(&data_ready, 1000); // 等待1秒
    // 处理数据
    return NULL;
}

3.6 系统时间与延时

osal_get_system_time_us返回系统当前时间,单位微秒。这个函数用于计算时间差、超时判断等。

osal_sleep_ms是毫秒级延时。注意,这个延时是“至少”延时指定毫秒数,实际延时可能更长(取决于系统调度)。

为什么需要这两个函数?

EtherCAT的DC同步要求主站和从站的时间同步。主站需要知道精确的时间戳,才能计算传输延迟。如果系统时间不准确,DC同步就会失败。

我记得有一次,在某个嵌入式Linux上,gettimeofday的精度只有10ms,导致DC同步一直报错。后来改用clock_gettime的CLOCK_MONOTONIC,精度提升到微秒级,问题就解决了。

3.7 移植要点总结

移植OSAL层时,需要实现以下函数:

  • 定时器:create, start, stop, expired, delete
  • 线程:create, join, delete
  • 互斥锁:create, lock, unlock, delete
  • 信号量:create, wait, post, delete
  • 系统时间:get_system_time_us
  • 延时:sleep_ms

每个平台只需要实现这十几个函数,SOEM就能跑起来。是不是很简单?

核心原则:OSAL层是SOEM和操作系统之间的“翻译官”。它把SOEM的通用请求,翻译成平台特有的系统调用。移植工作,就是写好这个翻译官。

好了,OSAL层就讲到这里。下一节咱们会深入SOEM的初始化流程,看看主站是怎么一步步建立起来的。


公众号:蓝海资料掘金营,微信deep3321