ARTICLE · 1310861
《从源码搭建嵌入式软件框架》06|换了系统,为什么上层代码可以少改一点
如果上层到处直接调用操作系统接口,换系统时就得四处修改。
平台适配层像一位翻译:上层表达“我要做什么”,下层负责翻译成当前系统的调用。🧩
这次沿 FreeRTOS 实现,看看它具体翻译了什么。下文名称已通用化,保留实际接口形状和关键分支;代码片段用于解释,不是完整独立程序。
示例假设已包含公共接口头文件,以及 、 等标准头文件。
一、创建线程:上层传需求,下层安排任务
线程可以先理解为一条独立推进的执行流程。上层需要告诉框架:执行哪个函数、带什么参数、需要什么属性。

公共接口形如:
intos_thread_create(os_thread_handle_t *out,void *(*entry)(void *),void *arg,const os_thread_attr_t *attr);
例如,创建一个持续输出心跳的线程:
staticvoid *heartbeat(void *arg){(void)arg;for (;;) {puts(”线程还在工作”);os_thread_sleep(1000);}return NULL;}os_thread_handle_t thread = NULL;os_thread_attr_t attr = {.type = OS_THREAD_DYNAMIC,.core = OS_CORE_ANY, /* 不固定到某个核心 */.priority = 3,.stack_size = 4096,.name = ”heartbeat”};int rc = os_thread_create(&thread, heartbeat, NULL, &attr);if (rc != 0) {/* 创建或管理登记失败,不能继续使用这个输出句柄 */}
上层没有出现 TaskHandle_t,也没有直接调用 xTaskCreate()。
实际路径还多了一步:先由平台包装创建任务,再由任务管理层登记线程;登记失败,会尝试删除刚创建的任务。
创建出的任务可能在接口返回前就开始运行,所以参数与共享资源必须提前准备好。
平台包装内部再决定:动态还是自动管理缓冲区的静态任务、固定核心还是不固定核心。下面只展示动态任务的创建分支:
/* 适配层关键分支,省略资源分配和失败清理。 */if (core_is_pinned(core)) {result = xTaskCreatePinnedToCore(entry_bridge, name, stack_depth, context,priority, &native_task, core);} else {result = xTaskCreate(entry_bridge, name, stack_depth, context,priority, &native_task);}
entry_bridge 是入口桥接函数:取出先前保存的用户函数和参数,再调用它。
上面的心跳线程长期运行,退出与回收不在这个示例中展开。
二、毫秒怎么变成系统能理解的时间?
上层说“等 100 毫秒”,FreeRTOS 使用的通常是 tick,也就是系统计时节拍。
统一时间单位,才能避免上层自己到处换算。

线程休眠中的关键逻辑是:
/* 线程休眠实现节选,名称通用化,省略日志。 */if (milliseconds == 0) {taskYIELD(); /* 让出执行机会,不进入定时休眠 */return 0;}TickType_t ticks = pdMS_TO_TICKS(milliseconds);if (ticks == 0) {ticks = 1; /* 非零休眠,换算后至少取 1 tick */}vTaskDelay(ticks);return 0;
但锁和信号量的等待不是照抄这段代码:它们直接换算普通的毫秒值,没有上述“至少 1 tick”分支。
因此,同样写 1 毫秒,在较粗的系统节拍下,不同接口的实际行为可能不同。
心跳示例也不是精确的每秒调度:输出耗时和任务调度都会影响间隔。
三、互斥锁:保护一份共享数据
互斥锁解决的是:同一时刻,只有一个线程进入这段受保护的操作。🔒
比如多个线程都要增加计数。下面假设先调用初始化函数,再允许其他线程访问;使用期间不删除锁。

static os_mutex_handle_t counter_lock = NULL;static uint32_t counter;intcounter_init(void){return os_mutex_create(&counter_lock);}intcounter_add_one(void){int rc = os_mutex_wait(counter_lock, 100);if (rc != 0) {return rc; /* 没拿到锁,不修改计数 */}++counter;return os_mutex_release(counter_lock);}
所有读写这份计数的线程,都要遵守同一把锁的规则。只给写入加锁、读取却随意访问,并没有完整保护共享数据。
适配层负责把锁操作翻译成 FreeRTOS 接口:
/* 创建时调用:xSemaphoreCreateMutex()。 */if (xSemaphoreTake(native_mutex, timeout_ticks) != pdTRUE) {return ERR_TIMEOUT;}return STATUS_OK;/* 释放则通过 xSemaphoreGive(native_mutex) 完成。 */
等待参数还有三种约定:
0 | |
pdMS_TO_TICKS() 换算 | |
UINT32_MAX | portMAX_DELAY,按内核配置解释长时间等待 |
注意示例中:如果加一成功、但释放锁失败,返回的仍是失败状态。此时不能盲目重试整个加一操作,否则可能重复计数。
四、信号量:不是抢位置,而是等通知
互斥锁强调“谁正在使用资源”;这里的计数信号量强调“还有几份可领取的通知”。
初始计数为 0,发送一次增加一份,成功等待一次取走一份。

static os_sem_handle_t ready = NULL;intnotification_init(void){return os_sem_create(&ready, 0);}/* 由普通线程调用:发送一份通知。 */intnotify_once(void){return os_sem_post(ready);}/* 由普通线程调用:等待并领取一份通知。 */intreceive_once(void){int rc = os_sem_wait(ready, 1000);if (rc == 0) {puts(”收到一份通知”);}return rc;}
在通知初始化成功、对象保持有效的前提下调用。这里的 post 是“增加计数”,不是给每个等待者都广播一次。
内部对应:
/* 创建;当前计数上限为 65535。 */native_sem = xSemaphoreCreateCounting(65535, initial_count);/* 发送与领取,分别调用: */xSemaphoreGive(native_sem);xSemaphoreTake(native_sem, timeout_ticks);
发送太多、计数达到上限时,继续发送会失败。
信号量本身也不携带消息内容;要传任务编号或数据包,还需要另外的数据结构。
这些包装使用普通的 Take/Give,不能因为名字通用,就直接拿到中断里调用。
五、定时器:到点以后,回调到底在哪执行?
软件定时器负责“到时间后发起处理”,不是给每个定时器创建一条独立线程。
这里可以传入一个任务执行环境,作为回调的投递目标:

沿用前面的信号量,做一次“200 毫秒后发送通知”:
staticvoid *timer_notify(void *arg){(void)arg;if (os_sem_post(ready) != 0) {/* 通知未送达,应记录失败,不能当作发送成功。 */}return NULL;}os_timer_handle_t timer = NULL;os_timer_attr_t attr = {.type = OS_TIMER_ONCE,.name = ”notify_later”,.param = NULL,.cb = timer_notify};int rc = os_timer_create(&timer, worker, &attr);if (rc == 0) {rc = os_timer_start(timer, 200);}
假设信号量已初始化,worker 是已准备好的执行环境,两者在定时器及回调使用期间保持有效。
单次定时器触发后也不会自动删除对象,后续仍需安排回收。
具体适配有三步:
用 xTimerCreate()创建 FreeRTOS 软件定时器。启动时,把毫秒换成 tick,非零间隔至少取 1 tick。 到点后,尝试把用户回调作为任务提交给指定执行环境。
如果没有指定执行环境,或者任务投递失败,包装会直接在 FreeRTOS 定时器服务任务中调用用户回调。
因此,不能假定“回调永远在 worker 线程里”。示例只发送通知,实际回调也应保持短小,避免长时间等待。
启动返回成功,不代表回调已经执行;停止或删除定时器,也不能直接推断之前投递出去的回调任务已被撤回。
六、接口统一了,不代表差异消失了
上层可以少改,是因为系统调用被集中到了适配层;不是换系统时,只换一个函数名就万事大吉。
还有几件事必须继续核对:
sizeof(StackType_t) 后传给底层;必须核对实际 SDK 的参数单位,不能只凭变量名判断 | |
公共线程头文件中也仍然有与 FreeRTOS 栈和任务状态相关的配置。抽象层已经建立,但平台细节并没有全部藏干净。
小结:把变化留在下面,把规则讲给上面
线程、锁、信号量和定时器,统一的不只是名称,还包括参数单位、返回状态和调用方式。
真正值得检查的是:时间怎么换算,回调在哪执行,失败怎样返回,资源何时释放。
这些规则对齐了,上层才有机会少改一点。🙂
下一篇:内存不只是一句malloc。内部 RAM、外部 RAM 都能分配,为什么还要统一入口,又该由谁负责释放?















































































