nRF Connect SDK 基础课程第七课:多线程应用

Nordic Semiconductor

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):等待资源、休眠、挂起

系统线程

系统启动时自动创建:

  1. Main thread:执行 RTOS 初始化,调用用户 main()
  2. 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()
  • 时间片轮转条件
  • 工作队列用途与使用场景

本课学习价值

本课是嵌入式架构升级的关键:

  • 从 “超级循环” 升级为多任务并发架构
  • 学会优先级设计,保证关键任务实时性
  • 掌握线程让出 / 休眠,避免饥饿、降低功耗
  • 学会时间片简化同优先级调度
  • 掌握工作队列,实现高实时性与低优先级任务分离

掌握本章内容,你就可以设计并实现稳定、高效、低功耗的多线程嵌入式系统。

 

短距离

适用于短距离物联网的蓝牙低功耗及多协议系统级芯片(支持Thread、Matter、Zigbee协议)

长距离

适用于LTE-M/NB-IoT、GNSS、DECT NR+和NTN的蜂窝物联网系统级封装

Wi-Fi

低功耗Wi-Fi 6协同ICs,支持2.4 GHz/5 GHz频段选项及WPA3加密协议

电源管理ICs

适用于电池供电设备的电源管理IC(PMIC),nPM系列提供充电与稳压功能选项。

AI及软件工具

工具与NPU加速边缘人工智能开发和部署

订阅Nordic新闻简报

了解最新信息!订阅后即可获取最新Nordic及物联网资讯

立即订阅