实时操作系统与任务调度
AC63 蓝牙音频 SDK 基于 FreeRTOS V9.0.0 内核与杰理(JieLi)自研的 os_api 抽象层构建多任务实时系统。本文档介绍该 SDK 的 RTOS 内核选型、统一任务/信号量/消息队列 API、任务优先级与 taskq 调度机制,以及典型任务的创建与生命周期管理方式。
Purpose and Scope
本页面覆盖"系统运行时"目录下的 RTOS 内核与任务调度机制,包括:
- 内核选型与双配置体系(FreeRTOS 内核 +
os_api统一接口层) os_task_create等任务生命周期 API 与 taskq 消息机制- 优先级、时基(tick)、调度器配置与典型任务的创建参数
- 信号量、互斥锁等同步原语在 SDK 中的使用方式
与任务调度并列的系统运行时话题(如中断处理、系统定时器、电源管理、看门狗)属于目录下的其他页面,本文不展开;蓝牙协议栈、音频编解码等上层功能虽然依赖任务调度,但其内部实现归各自页面(如 Mesh、音频编解码)负责。
Overview
AC63 系列芯片的固件是一个典型的嵌入式实时多任务系统:音频播放、蓝牙协议栈、UI 交互、外设驱动等子系统各自运行在独立任务中,由 RTOS 内核统一调度。SDK 将内核能力封装为以 os_ 前缀开头的统一 API(声明于 os_api.h),使上层应用与底层内核(FreeRTOS 或厂商自研内核)解耦。
从仓库结构可以看到两套内核头文件并存:
include_lib/system/os/FreeRTOS/:FreeRTOS V9.0.0 标准内核头文件(FreeRTOS.h、task.h、queue.h、semphr.h)及其工程配置FreeRTOSConfig.h;include_lib/system/os/os_api.h/os_cfg.h/os_type.h/os_error.h:杰理统一 OS 抽象层,采用 µC/OS-II 风格 API(如os_task_create、os_taskq_post、OS_SCHED_LOCK_EN等),并扩展了 taskq(任务消息队列)机制。
上层代码(apps/、cpu/ 下的驱动与测试代码)几乎全部通过 os_api.h 的 os_* 接口创建任务与同步对象,而不是直接调用 FreeRTOS API。这一抽象让同一份应用代码可以跨内核移植,同时 os_cfg.h 中保留了 µC/OS-II 风格的可裁剪配置(任务数上限 31、tick 100Hz、时间片轮转等)。
Architecture
flowchart TD
subgraph sg_App["应用层 apps/"]
AppTask["音频/蓝牙/UI 任务<br/>audio_pwm_task / iic_slave_task"]
end
subgraph sg_Driver["驱动与测试 cpu/"]
DriverTask["外设驱动任务<br/>uart_u_task / ledc"]
end
subgraph sg_OsApi["OS 抽象层 include_lib/system/os/"]
OsApi["os_api.h<br/>os_task_create / os_taskq_post / os_sem_create"]
OsCfg["os_cfg.h<br/>µC/OS-II 风格配置"]
OsType["os_type.h / os_error.h"]
end
subgraph sg_Kernel["RTOS 内核"]
FreeRTOS["FreeRTOS V9.0.0<br/>task.h / queue.h / semphr.h"]
FrConfig["FreeRTOSConfig.h<br/>8 级优先级 / 100Hz tick"]
end
AppTask -->|"os_* 调用"| OsApi
DriverTask -->|"os_* 调用"| OsApi
OsApi -->|"封装调度原语"| FreeRTOS
OsApi --> OsCfg
OsApi --> OsType
FreeRTOS --> FrConfig
架构设计意图:内核层只负责最底层的任务切换与同步原语;os_api.h 提供稳定的、面向业务的编程接口,并把"任务 + 私有消息队列(taskq)"这一常用模型合并到 os_task_create 的参数中(qsize 参数),从而在创建任务时就为它分配好消息队列——这是本 SDK 任务模型区别于裸 FreeRTOS 的关键设计,详见下文 taskq 机制。
内核与配置体系
SDK 中存在两套并行配置:
FreeRTOS 内核配置(FreeRTOSConfig.h):configUSE_PREEMPTION = 1(抢占式调度)、configTICK_RATE_HZ = 100(100Hz 时基)、configMAX_PRIORITIES = 8、configTOTAL_HEAP_SIZE = 30KB、configMAX_TASK_NAME_LEN = 12(任务名最长 12 字节,因此 os_task_create 的 name 参数同样受此限制)、启用互斥锁/递归互斥锁/计数信号量/静态分配,关闭软件定时器与任务通知。
OS 抽象层配置(os_cfg.h):OS_MAX_TASKS = 31(最多 31 个任务)、OS_TICKS_PER_SEC = 100、OS_LOWEST_PRIO = 0、OS_IDLE_PRIO = OS_LOWEST_PRIO(空闲任务优先级为 0,数值越大优先级越高)、OS_TIME_SLICE_EN = 1(时间片轮转)、OS_PRIORITY_INVERSION = 1(处理优先级翻转)、OS_SCHED_LOCK_EN = 1(支持 OSSchedLock/Unlock)、OS_PARENT_TCB = 1(记录父任务 TCB)。可裁剪模块包括 taskq(OS_TASKQ_EN)、互斥锁(OS_MUTEX_EN)、信号量(OS_SEM_EN)等,全部使能。
任务生命周期 API
os_api.h 是任务调度的核心接口文件,先以 typedef void *TaskHandle_t 定义任务句柄,再提供全套任务管理函数。所有任务相关 API 均返回 int 错误码。
任务创建:os_task_create
int os_task_create(void (*task)(void *p_arg),
void *p_arg,
u8 prio,
u32 stksize,
int qsize,
const char *name);
Source: os_api.h
参数语义(来自头文件注释):
| 参数 | 含义 |
|---|---|
task | 任务回调函数,签名 void (*)(void *) |
p_arg | 传给任务回调的参数指针 |
prio | 任务优先级(u8),数值越大优先级越高;空闲任务为 0 |
stksize | 任务栈大小,单位是 u32(4 字节字),与常见以字节为单位的 API 不同 |
qsize | 任务私有消息队列(taskq)大小,单位字节;传 0 表示不创建队列 |
name | 任务名,长度不能超过 configMAX_TASK_NAME_LEN(12 字节) |
设计意图:把"栈 + 优先级 + 消息队列"打包成一个创建调用,使应用只需一行代码即可得到一个可接收 taskq 消息的完整任务。这是 SDK 各驱动任务(如 audio_pwm_task、iic_slave)的标准创建方式。
多核亲和创建:os_task_create_affinity_core
int os_task_create_affinity_core(void (*task)(void *p_arg),
void *p_arg,
u8 prio,
u32 stksize,
int qsize,
const char *name,
u8 core);
Source: os_api.h
在支持多核的平台上(os_cpu.h 定义 OS_CPU_CORE),通过末尾的 core 参数把任务绑定到指定核,用于把实时性要求高的任务(如音频、射频)与普通任务隔离。
任务删除协议
int os_task_del_req(const char *name); /* 请求删除任务 */
int os_task_del_res(const char *name); /* 任务响应删除,标记资源已释放 */
int os_task_del(const char *name); /* 直接删除任务 */
Source: os_api.h
删除采用"请求-应答"两阶段协议(与 OS_TASK_DEL_REQ / OS_TASK_DEL_RES / OS_TASK_DEL_OK 状态常量对应):请求方调用 os_task_del_req,被删任务在合适时机调用 os_task_del_res 确认自身资源已释放(可以用 OS_TASK_SELF 指代自己),避免任务正在使用资源时被强行删除。任务句柄别名 OS_TASK_SELF(0x1)与 OS_TASK_FATHER(0x2)用于在回调中引用当前任务或父任务。
延时与当前任务查询
void os_time_dly(int time_tick); /* 任务延时,单位 tick;中断或关总中断下禁止调用 */
const char *os_current_task(); /* 返回当前任务名,可用于日志/调试 */
Source: os_api.h
os_time_dly 以 tick 为单位让出 CPU,是任务循环中最常用的"节流"手段;头文件明确要求中断上下文或系统总中断关闭期间不得调用。
taskq 任务消息队列机制
os_api.h 定义了四类队列消息类型:
#define Q_MSG 0x100000
#define Q_EVENT 0x200000
#define Q_CALLBACK 0x300000
#define Q_USER 0x400000
Source: os_api.h
消息类型的高 8 位作为分类标签,供 os_taskq_accept 等接收接口区分消息来源:Q_MSG 普通消息、Q_EVENT 事件通知、Q_CALLBACK 回调消息、Q_USER 用户自定义消息。
发送:os_taskq_post
int os_taskq_post(const char *name, int argc, ...);
Source: os_api.h
向名为 name 的任务队列发送 Q_USER 类型消息。argc 为后续可变参数个数,最多 8 个 int;可变参数会打包进该任务创建时分配的 taskq。这是 SDK 中任务间通信最主要的途径:生产者不需要持有消费者任务的对象引用,只需知道任务名,天然解耦。
接收:os_taskq_accept(非阻塞)
/* 非阻塞方式查询 taskq
* @param argc 最大可获取的 queue 长度,单位(int)
* @param argv 存放 queue 的 buf
*/
Source: os_api.h
接收侧通常在主循环中调用非阻塞的 os_taskq_accept 轮询自己的队列,取出消息后按消息类型分发。这一"创建时带队列 + 主循环轮询队列"的模型构成了 SDK 绝大多数驱动任务的主循环骨架(见"核心流程")。
同步原语
除 taskq 外,os_api.h 还提供信号量、互斥锁、消息队列、定时器等同步接口。仓库中的典型用法(来自实际驱动代码):
os_sem_create(&pwm_need_resume_sem, 0); /* 创建初值 0 的信号量 */
os_task_create(audio_pwm_task, NULL, 2, 1024, 128, "audio_pwm_task");
Sources: audio_pwm.c
os_sem_create(&ledc0_sem, 1); /* 创建初值 1 的信号量(互斥用法) */
os_sem_create(&ledc1_sem, 1);
Source: ledc.c
信号量初值为 1 时充当互斥锁保护共享外设寄存器(如 LEDC 通道),初值为 0 时用作事件通知(如 PWM 唤醒)。os_cfg.h 中 OS_SEM_EN、OS_MUTEX_EN、OS_PRIORITY_INVERSION 等开关表明内核原生支持信号量/互斥锁及优先级翻转处理,锁等待不会无限阻塞高优先级任务。
调度器行为与核心流程
抢占式调度与时间片
FreeRTOSConfig.h 中 configUSE_PREEMPTION = 1 启用抢占式调度:就绪态中优先级最高的任务立即获得 CPU,低优先级任务无需主动让出。configTICK_RATE_HZ = 100 提供 100Hz 时基,configUSE_16_BIT_TICKS = 0 使用 32 位 tick 计数(约 497 天溢出,远大于 16 位 tick 的 655 秒)。configUSE_PORT_OPTIMISED_TASK_SELECTION = 1 使用硬件指令优化任务选择。
同一优先级内,os_cfg.h 的 OS_TIME_SLICE_EN = 1 开启时间片轮转,配合 configIDLE_SHOULD_YIELD = 1,保证同级任务公平共享 CPU,空闲任务不独占总线。OS_IDLE_PRIO = 0 表明空闲任务处于最低优先级,任何业务任务都优先于它运行。
典型任务生命周期
一个典型驱动任务(如 iic_slave_task)的生命周期如下:
sequenceDiagram
participant Main as 主流程/其他任务
participant Api as os_api 抽象层
participant Kern as RTOS 内核
participant Task as 任务回调 (如 iic_slave_task)
Main->>Api: os_task_create(task, p_arg, prio, stksize, qsize, name)
Api->>Kern: 分配 TCB/栈/队列,加入就绪表
Kern-->>Api: 返回错误码
Api-->>Main: 返回错误码
Note over Kern: 调度器按优先级运行最高就绪任务
Kern->>Task: 切换上下文,进入任务函数
loop 任务主循环
Task->>Api: os_taskq_accept(&argc, argv) 非阻塞查询
Api-->>Task: 消息或空
Task->>Task: 分发处理 (Q_USER/Q_EVENT...)
Task->>Api: os_time_dly(ticks) 延时让出 CPU
end
Task->>Api: os_task_del_res(OS_TASK_SELF) 响应删除请求
任务创建后即进入就绪队列,由内核按优先级调度;任务内部是一个"查询 taskq → 分发处理 → 延时"的无限循环。qsize > 0 时队列由内核在创建阶段一并分配,因此 os_taskq_post 无需再做分配即可投递消息。
优先级设计参考
从仓库中的实际任务创建参数可以归纳本 SDK 的优先级使用习惯(数值越大优先级越高,0 为空闲任务):
| 任务 | 优先级 | 栈大小(u32) | qsize(字节) | 出处 |
|---|---|---|---|---|
| 空闲任务 | 0 | — | — | os_cfg.h |
audio_pwm_task(音频 PWM) | 2 | 1024 | 128 | cpu/bd19/audio_pwm.c |
iic_slave(IIC 从机) | 30 | 1024 | 64 | cpu/br25/iic_slave_test.c |
uart_u_task(UART 测试) | 31 | 512 | 0 | cpu/bd29/uart_test.c |
uart_flow_ctrl_task | 31 | 128 | 0 | cpu/br23/uart_test.c |
可见:实时性要求不高的后台任务(音频 PWM)用低优先级;IIC、UART 等与外部交互的驱动任务用 30~31 高优先级;不接收 taskq 消息的任务(UART 测试)qsize 传 0,节省内存。应用开发时应在 1~31 之间为任务选取合适优先级,避免多个高优先级任务互相饿死。
使用示例
示例一:创建带 taskq 的驱动任务(音频 PWM)
cpu/bd19/audio_pwm.c 展示了"信号量通知 + 低优先级任务"的典型组合:PWM 中断或状态变化通过信号量唤醒任务,任务处理完毕后自行延时。
clk_set("sys", 96 * 1000000L);
os_sem_create(&pwm_need_resume_sem, 0);
os_task_create(audio_pwm_task, NULL, 2, 1024, 128, "audio_pwm_task");
Source: audio_pwm.c
创建参数解读:优先级 2(低优先级后台任务)、栈 1024×4 字节、队列 128 字节、任务名 audio_pwm_task(符合 12 字节限制)。
示例二:创建高优先级外设任务(IIC 从机)
printf("%s() %d\n", __func__, __LINE__);
os_task_create(iic_slave_task, NULL, 30, 1024, 64, "iic_slave");
Source: iic_slave_test.c
IIC 从机要求低延迟响应主机请求,因此优先级给到 30,并分配 64 字节 taskq 用于接收上层控制消息。
示例三:音频编码任务与信号量(Mesh 应用)
audio_encoder_task_create(encode_task, "audio_enc");
...
os_sem_create(&demo_enc->pcm_frame_sem, 0);
Sources: audio_codec_demo.c
上层还封装了更高层的任务工厂(如 audio_encoder_task_create),其内部同样基于 os_task_create;pcm_frame_sem 初值为 0,用于 PCM 帧到达时唤醒编码任务——信号量在此作为"帧级事件通知"使用。
配置选项
FreeRTOS 内核配置(include_lib/system/os/FreeRTOS/FreeRTOSConfig.h)
| 配置宏 | 值 | 说明 |
|---|---|---|
configUSE_PREEMPTION | 1 | 抢占式调度 |
configTICK_RATE_HZ | 100 | 系统时基 100Hz(10ms/tick) |
configMAX_PRIORITIES | 8 | 内核级最大优先级数(抽象层为 31 级) |
configTOTAL_HEAP_SIZE | 30×1024 | 内核堆大小 30KB |
configMINIMAL_STACK_SIZE | 256 | 最小栈(u32 单位语境下为 256 字) |
configMAX_TASK_NAME_LEN | 12 | 任务名最大长度(字节) |
configUSE_MUTEXES / configUSE_RECURSIVE_MUTEXES | 1 / 1 | 互斥锁与递归互斥锁 |
configUSE_COUNTING_SEMAPHORES | 1 | 计数信号量 |
configSUPPORT_STATIC_ALLOCATION | 1 | 支持静态分配(配合 configUSE_MALLOC_FAILED_HOOK=1) |
configUSE_16_BIT_TICKS | 0 | 32 位 tick 计数 |
configCHECK_FOR_STACK_OVERFLOW | 0 | 未开启栈溢出运行时检查 |
configUSE_TIMERS | 0 | 关闭软件定时器任务 |
configUSE_TASK_NOTIFICATIONS | 0 | 关闭任务通知(消息传递依赖 taskq) |
OS 抽象层配置(include_lib/system/os/os_cfg.h)
| 配置宏 | 值 | 说明 |
|---|---|---|
OS_MAX_TASKS | 31 | 应用最大任务数 |
OS_TICKS_PER_SEC | 100 | 每秒 tick 数,与内核时基一致 |
OS_LOWEST_PRIO / OS_IDLE_PRIO | 0 | 最低/空闲任务优先级 |
OS_TIME_SLICE_EN | 1 | 同优先级时间片轮转 |
OS_PRIORITY_INVERSION | 1 | 处理优先级翻转 |
OS_SCHED_LOCK_EN | 1 | 调度锁(OSSchedLock/Unlock) |
OS_PARENT_TCB | 1 | 记录父任务 TCB |
OS_TASKQ_EN 及子开关 | 1 | taskq 全套功能(post/pend/accept/flush/query) |
OS_MUTEX_EN | 1 | 互斥锁模块 |
OS_SEM_EN | 1 | 信号量模块 |
API 参考(os_api.h 核心接口)
int os_task_create(void (*task)(void *p_arg), void *p_arg, u8 prio, u32 stksize, int qsize, const char *name)
创建任务并使其进入就绪态。stksize 单位为 u32(字),qsize 为 taskq 字节数,name 不得超过 12 字节。返回错误码,0 表示成功。
int os_task_create_affinity_core(...同上..., u8 core)
多核平台下将任务绑定到指定 core 运行。
int os_task_del_req(const char *name) / int os_task_del_res(const char *name) / int os_task_del(const char *name)
任务删除三接口:请求删除、任务自答(资源已释放)、直接删除。任务自身可用 OS_TASK_SELF 代替任务名;OS_TASK_DEL_REQ/RES/OK 三个常量标记删除协议状态。
int os_taskq_post(const char *name, int argc, ...)
向指定任务发送 Q_USER 消息,argc 个可变参数(最多 8 个 int)打包入目标任务的 taskq。Throws/返回:错误码;目标任务不存在或队列满时返回非 0。
int os_taskq_accept(int argc, int *argv)(非阻塞)
任务主循环中查询自身 taskq,argc 表示可容纳的最大消息数(int 单位),argv 为接收缓冲区。无消息时立即返回;消息分类标签为 Q_MSG/Q_EVENT/Q_CALLBACK/Q_USER。
void os_time_dly(int time_tick)
任务延时 time_tick 个 tick 并让出 CPU。约束:不得在中断或系统总中断关闭状态下调用。
const char *os_current_task(void)
返回当前运行任务的任务名,用于日志与调试。
void os_init(void) / void os_start(void) / void os_init_tick(int)
内核初始化/启动/时基初始化,保留接口,由系统启动代码在 main 早期调用。
失败模式、边界情况与并发
- 任务名长度限制:
configMAX_TASK_NAME_LEN = 12,超长任务名会被截断或导致创建失败;同时os_taskq_post/os_task_del_*依赖任务名定位任务,命名需唯一且稳定。 - taskq 溢出:
qsize有限(常见 64~128 字节),高频率生产者可能把队列写满,os_taskq_post返回非 0 错误码。生产者的调用方代码必须检查返回值,必要时改用os_taskq_post_front(优先级插入)或加大qsize。 - 消息参数上限:单次
os_taskq_post最多 8 个 int;传指针时需保证指针指向的对象在消费端处理完前保持有效(典型做法是传静态/全局对象地址)。 - 优先级反转:
OS_PRIORITY_INVERSION = 1表明内核按 µC/OS-II 惯例用优先级继承/置顶处理互斥锁导致的优先级反转;信号量(非互斥锁)场景下无此保护,应避免用二值信号量保护共享资源。 - 栈溢出:
configCHECK_FOR_STACK_OVERFLOW = 0,运行时不做栈溢出检查,因此stksize必须按最深层调用链加中断现场余量估算;局部大数组/递归是栈溢出的主要来源。 - 中断上下文约束:
os_time_dly及阻塞式 pend 接口禁止在中断中调用;中断与任务之间应通过"中断置信号量/发 taskq → 任务处理"的模式交互。 - 删除安全性:两阶段删除协议(
del_req → del_res)确保任务不会在持有资源时被销毁;被删任务应在主循环合适位置调用os_task_del_res(OS_TASK_SELF)。
性能与运维
- 时基 100Hz 意味着
os_time_dly(1)的定时精度为 10ms;需要更高精度时使用硬件定时器(见系统定时器页面),不要依赖任务延时。 - 高优先级任务(30~31)应保持"短小精悍":长时间运行会阻塞所有低优先级任务;长耗时操作应拆分为多步 + 状态机,或临时降低自身优先级。
configUSE_MALLOC_FAILED_HOOK = 1提供分配失败钩子,便于捕获堆耗尽;configUSE_IDLE_HOOK = 1提供空闲钩子,可在此实现低功耗/看门狗喂狗。- 任务栈与 qsize 的分配来自内核堆(30KB),任务数量(
OS_MAX_TASKS = 31)与堆大小共同构成内存预算上限,新增任务前需评估剩余堆空间。
扩展点
- 空闲钩子:
configUSE_IDLE_HOOK = 1,实现vApplicationIdleHook可接管空闲期行为(低功耗、统计)。 - 分配失败钩子:
configUSE_MALLOC_FAILED_HOOK = 1,实现vApplicationMallocFailedHook处理堆耗尽。 - 任务名寻址:
os_taskq_post/os_task_del_req均以任务名为寻址键,可通过封装"注册表 + 名称常量"统一管理任务名,避免字符串散落。 - 高层任务工厂:如
apps/mesh/audio_codec_demo.c中的audio_encoder_task_create,可在os_task_create之上再封装领域任务(自带参数结构、队列、信号量),保持底层 API 稳定。
Related Links
- os_api.h(OS 抽象层 API 声明)
- os_cfg.h(OS 抽象层配置)
- FreeRTOSConfig.h(FreeRTOS 内核配置)
- task.h / queue.h / semphr.h(FreeRTOS 内核头文件)
- 相关实现示例:audio_pwm.c、iic_slave_test.c、audio_codec_demo.c
- 相邻页面:系统启动流程(
os_init/os_start调用链)、中断与事件机制、系统定时器