nRF Connect SDK 基于 Zephyr RTOS 构建,支持完整的多线程应用开发。第七课多线程应用系统讲解裸机与 RTOS 差异、线程生命周期、调度器、优先级、时间片轮转与工作队列,并用三个实操实验完整演示线程创建、让出、休眠、抢占与任务卸载,是从裸机思维转向 RTOS 工程化开发的关键一课。
课程定位
本课是从 “单任务顺序执行” 升级到 “多任务并发架构” 的核心章节。你将理解 RTOS 如何管理 CPU 资源,学会拆分任务、设计优先级、避免线程饥饿,并使用工作队列优化高优先级线程的实时性,为复杂传感器、无线、外设并发系统打下基础。
核心学习目标
- 理解裸机与 RTOS 编程的核心区别与适用场景
- 掌握 Zephyr 线程状态、类型、优先级与调度机制
- 学会静态创建线程、使用 k_yield()、k_msleep() 实现线程协作
- 理解时间片轮转机制并在工程中启用
- 掌握工作队列(workqueue)用法,将非紧急任务从高优先级线程卸载
- 通过三个实验理解调度器行为、线程饥饿、抢占与低功耗设计
一、裸机 vs RTOS 编程
裸机应用(Bare-metal)
- 结构:初始化后在 main() 中一个超级循环 while(1)
- 执行:顺序执行,仅被 ISR 打断
- 优点:简单、无调度开销、内存占用小
- 缺点:复杂项目难以维护、扩展性差、难以并行处理多任务
RTOS 应用
- 结构:将功能拆分为多个独立 线程(thread)
- 内核(kernel)统一调度、同步、通信
- 优势:模块化、可扩展、易移植、内置大量库与协议栈
- ISR 同样存在,但回调逻辑应尽量短,耗时工作交给线程
在 Zephyr 中,main() 函数是可选的。RTOS 自动启动主线程完成初始化,若没有用户 main(),主线程退出后系统依然正常运行其他线程。
二、Zephyr RTOS 基础:线程、调度器、ISR
线程(Thread)
线程是 RTOS 调度的最小执行单元。
线程状态
- Running:正在 CPU 上执行
- Runnable(Ready):等待 CPU
- Non-runnable(Unready):等待资源、休眠、挂起
系统线程
系统启动时自动创建:
- Main thread:执行 RTOS 初始化,调用用户 main()
- Idle thread:无任务可运行时执行,负责低功耗管理
用户线程
由开发者创建,用于处理传感器、通信、控制等任务。
工作队列线程(Workqueue thread)
- 专门处理工作项(work item) 的线程
- 按 FIFO 执行
- 用途:从 ISR 或高优先级线程卸载非紧急耗时操作
- 共享栈,比独立线程更轻量
线程优先级
- 数值越小优先级越高
- 负数:协作线程(cooperative),本课不涉及
- 非负数:可抢占线程(preemptible)
- 默认优先级数:15(0–15)
- 主线程优先级:0(最高)
- 空闲线程优先级:15(最低)
调度器(Scheduler)
- 决定哪个线程获得 CPU
- Zephyr 默认是 tickless 事件驱动调度
- 调度触发点:线程状态变化、信号量、互斥锁、k_yield()、时间片耗尽等
中断服务程序(ISR)
- 优先级高于所有线程
- 必须短小,不能阻塞
- 耗时工作应通过工作队列交给线程处理
三、核心 API 概览
#include <zephyr/kernel.h>
K_THREAD_DEFINE(线程ID, 栈大小, 入口函数, p1,p2,p3, 优先级, 选项, 延迟);
k_yield();
k_msleep(5);
k_busy_wait(1000000);
k_work_init(&work, 处理函数);
k_work_submit_to_queue(&queue, &work);
实操练习 1:线程创建、优先级、让出与休眠
目标:学会创建线程,理解优先级、k_yield()、k_msleep() 对调度的影响。
步骤 1:基础工程
打开 l7/l7_e1
步骤 2:定义栈与优先级
#define STACKSIZE 1024
#define THREAD0_PRIORITY 7
#define THREAD1_PRIORITY 7
步骤 3:线程入口函数
void thread0(void)
{
while (1) {
printk("Hello, I am thread0\n");
}
}
void thread1(void)
{
while (1) {
printk("Hello, I am thread1\n");
}
}
步骤 4:静态定义线程
K_THREAD_DEFINE(thread0_id, STACKSIZE, thread0, NULL,NULL,NULL,
THREAD0_PRIORITY, 0, 0);
K_THREAD_DEFINE(thread1_id, STACKSIZE, thread1, NULL,NULL,NULL,
THREAD1_PRIORITY, 0, 0);
步骤 5:现象:线程饥饿
编译烧录后只看到 thread0 输出,thread1 完全得不到运行。
原因:同优先级下,第一个运行的线程永不释放 CPU。
步骤 6:使用 k_yield() 让出
void thread0(void)
{
while (1) {
printk("Hello, I am thread0\n");
k_yield();
}
}
结果:thread0 打印一次后让出,thread1 永久霸占 CPU。
步骤 7:两个线程都 k_yield()
void thread0(void)
{
while (1) {
printk("Hello, I am thread0\n");
k_yield();
}
}
void thread1(void)
{
while (1) {
printk("Hello, I am thread1\n");
k_yield();
}
}
结果:交替打印。
缺点:频繁调度耗电。
步骤 8:使用 k_msleep(5) 休眠(推荐)
void thread0(void)
{
while (1) {
printk("Hello, I am thread0\n");
k_msleep(5);
}
}
void thread1(void)
{
while (1) {
printk("Hello, I am thread1\n");
k_msleep(5);
}
}
结果:同样交替打印,但线程进入 Non-runnable,系统可运行空闲线程进入低功耗。
结论:
- k_yield():状态变为 Runnable,仍参与调度
- k_msleep():状态变为 Non-runnable,超时后才恢复
- 延时优先用休眠,让出用于同优先级快速切换
实操练习 2:时间片轮转(Time slicing)
目标:让同优先级线程自动公平轮转,无需手动 k_yield()。
步骤 1:工程 l7/l7_e2
线程内无让出、无休眠,只忙等:
void thread0(void)
{
while (1) {
printk("Hello, I am thread0\n");
k_busy_wait(1000000);
}
}
步骤 2:启用时间片(prj.conf)
CONFIG_TIMESLICING=y
CONFIG_TIMESLICE_SIZE=10
CONFIG_TIMESLICE_PRIORITY=0
含义:
- 时间片长度:10ms
- 作用于 ≥0 的优先级
- 只对同优先级有效
步骤 3:现象
两线程自动交替运行。
步骤 4:修改优先级打破轮转
#define THREAD0_PRIORITY 6
#define THREAD1_PRIORITY 7
结果:thread0 永久运行,thread1 饥饿。
说明:时间片只作用于同优先级。
实操练习 3:工作队列(Workqueue)卸载任务
目标:将高优先级线程中的非紧急耗时操作,卸载到低优先级工作队列,提升系统实时性。
步骤 1:工程 l7/l7_e3
步骤 2:定义优先级
#define THREAD0_PRIORITY 2
#define THREAD1_PRIORITY 3
#define WORKQ_PRIORITY 4
步骤 3:模拟耗时工作
static inline void emulate_work(void)
{
for (volatile int count_out = 0; count_out < 300000; count_out++);
}
步骤 4:高 / 低优先级线程
void thread0(void)
{
uint64_t ts, dt;
while (1) {
ts = k_uptime_get();
emulate_work();
dt = k_uptime_delta(&ts);
printk("thread0 %lld ms\n", dt);
k_msleep(20);
}
}
void thread1(void)
{
uint64_t ts, dt;
while (1) {
ts = k_uptime_get();
emulate_work();
dt = k_uptime_delta(&ts);
printk("thread1 %lld ms\n", dt);
k_msleep(20);
}
}
步骤 5:现象
高优先级 thread0 频繁阻塞低优先级 thread1。
步骤 6:定义工作项与处理函数
struct work_info {
struct k_work work;
char name[25];
} my_work;
void offload_function(struct k_work *work)
{
emulate_work();
}
步骤 7:启动工作队列并初始化工作项
k_work_queue_start(&offload_work_q, my_stack_area,
K_THREAD_STACK_SIZEOF(my_stack_area), WORKQ_PRIORITY, NULL);
k_work_init(&my_work.work, offload_function);
步骤 8:提交工作项(不再原地执行)
void thread0(void)
{
uint64_t ts, dt;
while (1) {
ts = k_uptime_get();
// 提交到工作队列,不阻塞
k_work_submit_to_queue(&offload_work_q, &my_work.work);
dt = k_uptime_delta(&ts);
printk("thread0 %lld ms\n", dt);
k_msleep(20);
}
}
步骤 9:结果
- thread0 瞬间完成(~0ms)
- thread1 执行时间大幅缩短
- 非紧急工作在低优先级工作队列异步完成
- 系统实时性、响应速度显著提升
课后测评
范围包括:
- 裸机与 RTOS 区别
- 线程状态与类型
- 优先级规则
- 调度器与重新调度点
- k_yield() vs k_msleep()
- 时间片轮转条件
- 工作队列用途与使用场景
本课学习价值
本课是嵌入式架构升级的关键:
- 从 “超级循环” 升级为多任务并发架构
- 学会优先级设计,保证关键任务实时性
- 掌握线程让出 / 休眠,避免饥饿、降低功耗
- 学会时间片简化同优先级调度
- 掌握工作队列,实现高实时性与低优先级任务分离
掌握本章内容,你就可以设计并实现稳定、高效、低功耗的多线程嵌入式系统。