第四章 操作系统抽象层(OSAL)设计
好,咱们进入第四章。这一章我打算聊聊OSAL——操作系统抽象层。说实话,这个模块在点钞机里太关键了。你想想看,点钞机跑的是实时任务,钞票一张张飞过去,你必须在几毫秒内完成识别、计数、判断真假。这时候,底层操作系统是FreeRTOS还是uC/OS,其实不重要。重要的是,你的上层应用代码不能跟某个OS绑死。
我早年吃过这个亏。当时一个项目用了某款RTOS,后来芯片缺货被迫换平台,结果调度器、队列、信号量全要重写,差点没把我逼疯。从那以后,我坚持在项目里加一层OSAL。说白了,就是给所有OS相关操作做个封装,上层只调用我这套接口,底层换OS不影响业务逻辑。
4.1 任务调度抽象
任务调度是OSAL的核心。点钞机里任务分好几类:高速传感器采集、电机控制、UI响应、通信处理。它们的优先级和周期都不一样。
我习惯这样设计任务调度接口:
typedef void (*TaskFunc_t)(void *param);
typedef enum {
TASK_PRIO_HIGHEST = 0,
TASK_PRIO_HIGH,
TASK_PRIO_NORMAL,
TASK_PRIO_LOW,
TASK_PRIO_IDLE
} TaskPriority_t;
typedef struct {
TaskFunc_t func;
void *param;
TaskPriority_t priority;
uint32_t stackSize;
const char *name;
} TaskConfig_t;
// OSAL 任务接口
int OSAL_TaskCreate(TaskConfig_t *cfg, TaskHandle_t *handle);
void OSAL_TaskDelete(TaskHandle_t handle);
void OSAL_TaskDelay(uint32_t ms);
void OSAL_TaskSuspend(TaskHandle_t handle);
void OSAL_TaskResume(TaskHandle_t handle);
这里有个细节——TaskHandle_t。我把它定义成void*,这样底层可以是FreeRTOS的TaskHandle_t,也可以是uC/OS的OS_TCB*。上层根本不用关心。
4.2 消息队列抽象
任务之间怎么通信?消息队列是最常用的方式。点钞机里,传感器采集完数据,通过队列发给识别模块;识别完的结果,再通过队列发给计数模块。一环扣一环。
我设计的队列接口长这样:
typedef void* QueueHandle_t;
QueueHandle_t OSAL_QueueCreate(uint32_t itemSize, uint32_t queueLen);
int OSAL_QueueSend(QueueHandle_t q, const void *data, uint32_t timeoutMs);
int OSAL_QueueReceive(QueueHandle_t q, void *buf, uint32_t timeoutMs);
int OSAL_QueuePeek(QueueHandle_t q, void *buf, uint32_t timeoutMs);
void OSAL_QueueDelete(QueueHandle_t q);
uint32_t OSAL_QueueMessagesWaiting(QueueHandle_t q);
注意timeoutMs这个参数。我统一用毫秒,底层实现时再转成OS的tick。为什么?因为上层业务逻辑只关心时间,不关心tick频率。你想想看,如果底层OS的tick是1ms,另一个是10ms,上层代码写死tick数,移植时全要改。用毫秒就省心了。
4.3 信号量与互斥锁
信号量和互斥锁,很多人搞混。我简单说下区别:信号量用于同步,互斥锁用于保护共享资源。点钞机里,信号量典型场景是——传感器采集完一帧数据,发信号量通知识别任务开始处理。互斥锁典型场景是——多个任务都要修改计数值,加锁保护。
接口设计如下:
// 信号量
typedef void* SemaphoreHandle_t;
SemaphoreHandle_t OSAL_SemCreate(uint32_t initCount);
int OSAL_SemTake(SemaphoreHandle_t sem, uint32_t timeoutMs);
int OSAL_SemGive(SemaphoreHandle_t sem);
void OSAL_SemDelete(SemaphoreHandle_t sem);
// 互斥锁
typedef void* MutexHandle_t;
MutexHandle_t OSAL_MutexCreate(void);
int OSAL_MutexLock(MutexHandle_t mutex, uint32_t timeoutMs);
int OSAL_MutexUnlock(MutexHandle_t mutex);
void OSAL_MutexDelete(MutexHandle_t mutex);
嗯,这里要注意。互斥锁和信号量虽然接口长得像,但底层实现不同。互斥锁有优先级继承机制,能防止优先级反转。信号量没有。所以保护共享资源时,一定要用互斥锁,别图省事用信号量代替。
4.4 内存管理抽象
点钞机的内存很金贵。一般SRAM就几十KB到几百KB,不能随便malloc/free。我建议OSAL里提供两种内存分配方式:静态分配和动态分配。
静态分配用于固定大小的数据结构,比如任务栈、队列缓冲区。动态分配用于不确定大小的临时数据,但要加超时机制,防止内存泄漏。
// 静态内存池
typedef struct {
void *poolStart;
uint32_t blockSize;
uint32_t blockCount;
void *freeList;
} MemPool_t;
void OSAL_MemPoolInit(MemPool_t *pool, void *mem, uint32_t blockSize, uint32_t blockCount);
void *OSAL_MemPoolAlloc(MemPool_t *pool);
void OSAL_MemPoolFree(MemPool_t *pool, void *ptr);
// 动态内存
void *OSAL_Malloc(uint32_t size);
void OSAL_Free(void *ptr);
我个人习惯,点钞机里90%的内存用静态池分配。为什么?因为静态池分配时间是确定的,O(1)复杂度。动态分配可能产生碎片,时间也不确定。你想想看,点钞机高速运行时,内存分配慢1ms,可能就导致丢钞。
4.5 时间管理抽象
时间管理是OSAL里容易被忽视的部分。点钞机需要精确计时,比如电机加速时间、传感器采样间隔、通信超时。我提供的接口如下:
uint64_t OSAL_GetTickMs(void); // 系统启动以来的毫秒数
uint64_t OSAL_GetTickUs(void); // 微秒级时间戳(硬件支持时)
void OSAL_DelayMs(uint32_t ms); // 阻塞延时
void OSAL_DelayUs(uint32_t us); // 微秒级延时(忙等)
int OSAL_TimerCreate(uint32_t periodMs, TimerCallback_t cb, void *arg);
void OSAL_TimerStart(TimerHandle_t timer);
void OSAL_TimerStop(TimerHandle_t timer);
注意OSAL_GetTickUs这个接口。它依赖硬件定时器,不是所有平台都支持。我一般会在实现时加个条件编译,硬件不支持就返回0,上层调用方自己判断。
还有一个细节——时间戳的溢出处理。32位毫秒计数器大约49天溢出一次。点钞机虽然不会连续跑49天,但万一呢?我建议用64位计数器,或者实现溢出检测逻辑。
OSAL_GetTickMs()做超时判断时,直接比较当前时间和目标时间。结果系统运行一段时间后,时间戳溢出,超时判断永远不成立。后来改成差值比较法:if ((now - start) > timeout),这样即使溢出也没问题。
4.6 OSAL的整体架构
最后,我画一下OSAL的整体结构。它分三层:
| 层次 | 内容 | 说明 |
|---|---|---|
| 应用层 | 业务逻辑、算法模块 | 只调用OSAL接口,不直接调用OS |
| OSAL层 | 任务、队列、信号量、内存、时间 | 统一接口,平台无关 |
| OS适配层 | FreeRTOS、uC/OS、RT-Thread等 | 实现OSAL接口,可替换 |
适配层的代码通常放在一个单独的文件里,比如osal_port.c。换平台时,只需要重写这个文件。我见过有些团队把适配层代码散落在各个模块里,换平台时改得想哭。千万别学他们。
好了,这一章就到这里。OSAL设计好了,后续开发会非常顺畅。下一章我们聊聊点钞机的核心——传感器数据采集与处理。到时候我会讲讲怎么用OSAL的队列和信号量,搭一个高效的数据流水线。