文章目录
-
- 前言
- [一、Zephyr 工程的基本结构](#一、Zephyr 工程的基本结构)
- [二、Kernel 基础 API:延时与系统时间](#二、Kernel 基础 API:延时与系统时间)
-
- [2.1 常用 API](#2.1 常用 API)
- [2.2 示例代码](#2.2 示例代码)
- [三、Device Model:设备管理 API](#三、Device Model:设备管理 API)
-
- [3.1 常用 API](#3.1 常用 API)
- [3.2 示例](#3.2 示例)
- 四、Devicetree:设备树常用宏
-
- [4.1 常用宏](#4.1 常用宏)
- [4.2 使用示例](#4.2 使用示例)
- [五、GPIO API:输入、输出和中断](#五、GPIO API:输入、输出和中断)
-
- [5.1 常用 API](#5.1 常用 API)
- [5.2 完整示例:LED 闪烁](#5.2 完整示例:LED 闪烁)
- [六、Thread API:线程管理](#六、Thread API:线程管理)
-
- [6.1 常用 API](#6.1 常用 API)
- [6.2 示例:创建两个线程](#6.2 示例:创建两个线程)
- [6.3 线程优先级](#6.3 线程优先级)
- [七、Semaphore API:信号量](#七、Semaphore API:信号量)
-
- [7.1 常用 API](#7.1 常用 API)
- [7.2 示例代码](#7.2 示例代码)
- [八、Mutex API:互斥锁](#八、Mutex API:互斥锁)
-
- [8.1 常用 API](#8.1 常用 API)
- [8.2 示例代码](#8.2 示例代码)
- [九、Message Queue API:消息队列](#九、Message Queue API:消息队列)
-
- [9.1 常用 API](#9.1 常用 API)
- [9.2 完整示例:两个线程通过队列通信](#9.2 完整示例:两个线程通过队列通信)
- [十、Timer API:软件定时器](#十、Timer API:软件定时器)
-
- [10.1 常用 API](#10.1 常用 API)
- [10.2 示例代码](#10.2 示例代码)
- [十一、Work Queue API:工作队列](#十一、Work Queue API:工作队列)
-
- [11.1 常用 API](#11.1 常用 API)
- [11.2 示例](#11.2 示例)
- [十二、UART API:串口通信](#十二、UART API:串口通信)
-
- [12.1 常用 API](#12.1 常用 API)
- [12.2 Polling 发送示例](#12.2 Polling 发送示例)
- [12.3 配置文件](#12.3 配置文件)
- [十三、SPI API](#十三、SPI API)
- [十四、I2C API](#十四、I2C API)
- [十五、PWM 和 ADC API](#十五、PWM 和 ADC API)
-
- [15.1 PWM](#15.1 PWM)
- [15.2 ADC](#15.2 ADC)
- [十六、CAN / CAN FD API](#十六、CAN / CAN FD API)
-
- [16.1 常用 API](#16.1 常用 API)
- [16.2 常见工作模式](#16.2 常见工作模式)
- [16.3 CAN FD 帧标志位](#16.3 CAN FD 帧标志位)
- [16.4 CAN FD 发送示例](#16.4 CAN FD 发送示例)
- [十七、Logging API:日志调试](#十七、Logging API:日志调试)
- [十八、Shell API:命令行调试](#十八、Shell API:命令行调试)
- [十九、STM32 HAL / FreeRTOS 与 Zephyr API 对照表](#十九、STM32 HAL / FreeRTOS 与 Zephyr API 对照表)
- [二十、Zephyr 开发中常见的几个坑](#二十、Zephyr 开发中常见的几个坑)
-
- [20.1 Devicetree 和 Kconfig 混淆](#20.1 Devicetree 和 Kconfig 混淆)
- [20.2 在中断中使用阻塞 API](#20.2 在中断中使用阻塞 API)
- [20.3 不检查返回值](#20.3 不检查返回值)
- [20.4 混淆逻辑电平和物理电平](#20.4 混淆逻辑电平和物理电平)
- [20.5 将 RTOS 软件定时器当成精密硬件定时器](#20.5 将 RTOS 软件定时器当成精密硬件定时器)
- [20.6 不同 Zephyr 版本混用 API](#20.6 不同 Zephyr 版本混用 API)
- [二十一、推荐的 API 学习顺序](#二十一、推荐的 API 学习顺序)
- 二十二、官方学习资源
- 总结
前言
最近开始学习 Zephyr RTOS,发现 Zephyr 与传统的 STM32 HAL 开发方式存在较大的差异。
对于熟悉 STM32 或 FreeRTOS 的开发者来说,Zephyr 最大的学习难点并不完全在于 RTOS 内核,而是需要同时理解:
- Kernel API:线程、调度、信号量、消息队列等。
- Device Model:统一的硬件设备管理模型。
- Devicetree:通过设备树描述硬件资源。
- Kconfig:配置系统需要启用的功能。
- Driver API:GPIO、UART、SPI、I2C、CAN 等外设驱动。
- Logging / Shell:日志系统与命令行调试工具。
实际上,在日常嵌入式开发中,我们并不需要掌握 Zephyr 的全部 API,只需要熟悉常用的接口,就能够完成绝大部分嵌入式应用开发。
本文按照实际开发场景,对 Zephyr 常用 API 进行分类整理,并结合 STM32 HAL 和 FreeRTOS 进行对比,帮助初学者快速建立 Zephyr 开发思维。
官方文档: https://docs.zephyrproject.org/latest/
API 参考: https://docs.zephyrproject.org/latest/doxygen/html/
本文以 Zephyr 4.x 的常用 API 为基础。具体函数、配置项和驱动支持情况应以项目实际使用的 Zephyr 版本为准。
一、Zephyr 工程的基本结构
在学习 API 之前,首先需要认识 Zephyr 的应用工程结构。
一个最简单的 Zephyr 应用通常包含:
text
my_zephyr_app/
├── CMakeLists.txt
├── prj.conf
├── app.overlay
└── src/
└── main.c
各个文件的作用如下:
| 文件 | 作用 |
|---|---|
main.c |
编写应用程序 |
CMakeLists.txt |
配置源文件与编译构建 |
prj.conf |
配置 Zephyr 功能 |
app.overlay |
修改或补充开发板设备树配置 |
其中需要特别理解两个概念。
1. Devicetree 管理硬件描述
例如启用某个 UART 控制器:
dts
&usart2 {
status = "okay";
current-speed = <115200>;
};
这里的 usart2 仅作为示例,实际节点名称取决于开发板的设备树。
2. Kconfig 管理软件功能
例如在 prj.conf 中:
ini
CONFIG_GPIO=y
CONFIG_SERIAL=y
CONFIG_LOG=y
表示启用 GPIO、串口和日志相关功能。
简单来说:
Devicetree 决定有哪些硬件、硬件如何配置;Kconfig 决定编译哪些软件功能。
二、Kernel 基础 API:延时与系统时间
头文件:
c
#include <zephyr/kernel.h>
2.1 常用 API
| API | 功能 | 说明 |
|---|---|---|
k_msleep(ms) |
毫秒级休眠 | 当前线程休眠 |
k_usleep(us) |
微秒级休眠 | 精度受系统影响 |
k_sleep(timeout) |
通用休眠 | 支持多种超时参数 |
k_busy_wait(us) |
忙等待 | 等待时不主动让出 CPU |
k_yield() |
主动让出 CPU | 允许其他就绪线程运行 |
k_uptime_get() |
获取运行时间 | 返回 64 位毫秒计数 |
k_uptime_get_32() |
获取运行时间 | 返回低 32 位毫秒计数 |
k_cycle_get_32() |
获取周期计数 | 可用于短时间测量 |
常用超时宏:
| 宏 | 作用 |
|---|---|
K_MSEC(x) |
毫秒超时 |
K_USEC(x) |
微秒超时 |
K_SECONDS(x) |
秒级超时 |
K_NO_WAIT |
不等待 |
K_FOREVER |
无限等待 |
2.2 示例代码
c
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
int main(void)
{
while (1) {
printk("Hello Zephyr!\n");
k_msleep(1000);
}
}
这里的:
c
k_msleep(1000);
表示当前线程休眠约 1000 ms。
需要注意的是,k_msleep() 并不是精确的硬件延时函数。对于电机控制等严格实时场景,应根据实际需求使用硬件定时器、PWM 或其他外设机制。
三、Device Model:设备管理 API
Zephyr 使用统一的设备模型管理硬件外设。
头文件:
c
#include <zephyr/device.h>
3.1 常用 API
| API | 功能 |
|---|---|
DEVICE_DT_GET() |
根据设备树节点获取设备对象 |
DEVICE_DT_GET_ANY() |
获取匹配兼容类型的某个设备 |
DEVICE_DT_GET_ONE() |
获取指定兼容类型的设备 |
device_is_ready() |
检查设备是否初始化就绪 |
device_get_binding() |
根据名称查找设备,旧代码中较常见 |
3.2 示例
c
const struct device *uart_dev =
DEVICE_DT_GET(DT_NODELABEL(usart2));
if (!device_is_ready(uart_dev)) {
printk("UART device not ready!\n");
}
其中:
c
const struct device *uart_dev;
可以理解为 Zephyr 中的设备对象指针。
如果熟悉 STM32 HAL,可以粗略类比:
c
UART_HandleTypeDef huart2;
但两者并不完全等价。Zephyr 的 struct device 是统一驱动模型的一部分,而 HAL 的 Handle 通常与具体外设的底层实现紧密关联。
注意:
DEVICE_DT_GET()依赖编译期生成的设备信息。如果设备树未正确启用设备,或缺少相应驱动,可能产生编译或链接错误。
四、Devicetree:设备树常用宏
头文件:
c
#include <zephyr/devicetree.h>
4.1 常用宏
| 宏 | 功能 |
|---|---|
DT_ALIAS(name) |
根据别名获取设备树节点 |
DT_NODELABEL(name) |
根据节点标签获取节点 |
DT_PATH(...) |
根据路径获取节点 |
DT_CHOSEN(prop) |
获取 chosen 节点 |
DT_PROP(node, prop) |
读取节点属性 |
DT_NODE_EXISTS(node) |
判断节点是否存在 |
DT_NODE_HAS_STATUS() |
检查节点状态 |
4.2 使用示例
c
#define LED_NODE DT_ALIAS(led0)
这段代码表示:
通过设备树中名为 led0 的别名找到对应节点。
再例如:
c
#define UART_NODE DT_NODELABEL(usart2)
表示获取节点标签为 usart2 的设备树节点。
需要特别强调:
Zephyr 的 DT_* 通常是编译期宏,而不是运行时查找函数。
设备树经过构建系统处理后,会生成相应的 C 头文件,供程序通过这些宏获取硬件配置信息。
五、GPIO API:输入、输出和中断
GPIO 是学习 Zephyr 外设驱动最合适的起点。
头文件:
c
#include <zephyr/drivers/gpio.h>
5.1 常用 API
| API | 功能 |
|---|---|
GPIO_DT_SPEC_GET() |
获取设备树中的 GPIO 描述信息 |
gpio_is_ready_dt() |
检查 GPIO 控制器是否就绪 |
gpio_pin_configure_dt() |
配置 GPIO 模式 |
gpio_pin_set_dt() |
设置输出逻辑状态 |
gpio_pin_get_dt() |
读取输入逻辑状态 |
gpio_pin_toggle_dt() |
翻转 GPIO |
gpio_pin_interrupt_configure_dt() |
配置 GPIO 中断 |
gpio_init_callback() |
初始化中断回调 |
gpio_add_callback() |
注册中断回调 |
常用配置宏:
c
GPIO_INPUT
GPIO_OUTPUT
GPIO_OUTPUT_ACTIVE
GPIO_OUTPUT_INACTIVE
GPIO_PULL_UP
GPIO_PULL_DOWN
GPIO_INT_EDGE_RISING
GPIO_INT_EDGE_FALLING
GPIO_INT_EDGE_BOTH
5.2 完整示例:LED 闪烁
c
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#define LED_NODE DT_ALIAS(led0)
#if !DT_NODE_HAS_STATUS(LED_NODE, okay)
#error "Board does not have an enabled led0 alias"
#endif
static const struct gpio_dt_spec led =
GPIO_DT_SPEC_GET(LED_NODE, gpios);
int main(void)
{
int ret;
/* 检查设备是否就绪 */
if (!gpio_is_ready_dt(&led)) {
printk("GPIO not ready!\n");
return -1;
}
/* 配置为输出,初始逻辑状态为非有效 */
ret = gpio_pin_configure_dt(
&led, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
printk("GPIO configure failed\n");
return ret;
}
while (1) {
/* 翻转输出逻辑状态 */
ret = gpio_pin_toggle_dt(&led);
if (ret < 0) {
printk("GPIO toggle failed\n");
break;
}
k_msleep(500);
}
return ret;
}
prj.conf:
ini
CONFIG_GPIO=y
这里最值得注意的是:
c
GPIO_DT_SPEC_GET(LED_NODE, gpios)
该宏从设备树中提取 GPIO 信息,包括:
- GPIO 控制器
- 引脚编号
- 有效电平等标志信息
因此不需要像 STM32 HAL 那样在应用中手动编写大量引脚配置代码。
注意有效电平:
c
gpio_pin_set_dt(&led, 1);
这里的 1 表示逻辑有效状态,并不一定是物理高电平。如果设备树配置了 GPIO_ACTIVE_LOW,逻辑 1 可能对应物理低电平。
六、Thread API:线程管理
Zephyr 使用 Thread 作为基本调度单位,类似 FreeRTOS 中的 Task。
头文件:
c
#include <zephyr/kernel.h>
6.1 常用 API
| API | 功能 | FreeRTOS 对照 |
|---|---|---|
K_THREAD_DEFINE() |
静态定义线程 | 静态任务创建机制 |
k_thread_create() |
创建线程 | xTaskCreate() |
k_thread_start() |
启动延迟线程 | 无完全等价接口 |
k_thread_abort() |
终止线程 | vTaskDelete() |
k_thread_suspend() |
挂起线程 | vTaskSuspend() |
k_thread_resume() |
恢复线程 | vTaskResume() |
k_thread_priority_set() |
设置优先级 | vTaskPrioritySet() |
k_current_get() |
获取当前线程 | xTaskGetCurrentTaskHandle() |
6.2 示例:创建两个线程
c
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
void thread1(void *a, void *b, void *c)
{
while (1) {
printk("Thread 1 running\n");
k_msleep(500);
}
}
void thread2(void *a, void *b, void *c)
{
while (1) {
printk("Thread 2 running\n");
k_msleep(1000);
}
}
/* 定义线程1 */
K_THREAD_DEFINE(
thread1_id,
1024,
thread1,
NULL, NULL, NULL,
5,
0,
0
);
/* 定义线程2 */
K_THREAD_DEFINE(
thread2_id,
1024,
thread2,
NULL, NULL, NULL,
6,
0,
0
);
K_THREAD_DEFINE() 的参数:
c
K_THREAD_DEFINE(
name,
stack_size,
entry,
p1, p2, p3,
priority,
options,
delay
);
| 参数 | 含义 |
|---|---|
name |
线程标识 |
stack_size |
线程栈大小 |
entry |
线程入口函数 |
p1~p3 |
线程参数 |
priority |
优先级 |
options |
线程选项 |
delay |
初始启动延迟,单位毫秒 |
6.3 线程优先级
Zephyr 的一个重要特点:
优先级数值越小,优先级越高。
例如:
text
优先级 0 > 优先级 5 > 优先级 10
其中,在默认的线程优先级模型中:
- 负数优先级:协作式线程(Cooperative)
- 非负数优先级:抢占式线程(Preemptive)
具体可用优先级范围由 Kconfig 配置决定。
这和 FreeRTOS 常见的优先级规则不同,迁移代码时尤其需要注意。
七、Semaphore API:信号量
信号量主要用于线程之间的同步与事件通知。
头文件:
c
#include <zephyr/kernel.h>
7.1 常用 API
| API | 功能 |
|---|---|
K_SEM_DEFINE() |
静态定义信号量 |
k_sem_init() |
初始化信号量 |
k_sem_take() |
获取信号量 |
k_sem_give() |
释放信号量 |
k_sem_reset() |
重置信号量 |
k_sem_count_get() |
获取计数值 |
7.2 示例代码
c
/* 定义二值信号量 */
K_SEM_DEFINE(my_sem, 0, 1);
/* 线程A:等待信号量 */
void thread_a(void)
{
k_sem_take(&my_sem, K_FOREVER);
/* 收到通知后执行任务 */
}
/* 线程B或者ISR:发送通知 */
void notify_thread_a(void)
{
k_sem_give(&my_sem);
}
执行逻辑:
text
线程A
|
v
k_sem_take()
|
v
等待信号量
|
| 线程B / ISR 调用 k_sem_give()
v
线程A恢复执行
常见应用:
- UART 接收完成通知
- CAN 接收事件通知
- ADC 转换完成通知
- 线程之间的事件同步
中断使用注意:
k_sem_give() 可以在中断中使用;ISR 中调用 k_sem_take() 时只能采用 K_NO_WAIT,不能等待阻塞。
八、Mutex API:互斥锁
Mutex 用于多个线程安全地访问共享资源。
头文件:
c
#include <zephyr/kernel.h>
8.1 常用 API
| API | 功能 |
|---|---|
K_MUTEX_DEFINE() |
静态定义互斥锁 |
k_mutex_init() |
初始化互斥锁 |
k_mutex_lock() |
获取锁 |
k_mutex_unlock() |
释放锁 |
8.2 示例代码
c
K_MUTEX_DEFINE(spi_mutex);
void access_spi_device(void)
{
if (k_mutex_lock(&spi_mutex, K_FOREVER) == 0) {
/* 访问共享SPI设备 */
k_mutex_unlock(&spi_mutex);
}
}
当两个线程需要访问同一个 SPI 总线或共享数据结构时,可以通过 Mutex 实现互斥访问。
需要注意:
- 互斥锁应由拥有它的线程释放。
- 不能在 ISR 中使用 Mutex。
- 不应长时间持有互斥锁,避免阻塞其他任务。
九、Message Queue API:消息队列
消息队列是嵌入式多任务程序的重要通信方式。
例如机器人控制器中:
text
CAN接收
|
v
消息队列
|
v
电机数据处理线程
|
v
更新电机状态
头文件:
c
#include <zephyr/kernel.h>
9.1 常用 API
| API | 功能 |
|---|---|
K_MSGQ_DEFINE() |
静态定义队列 |
k_msgq_init() |
初始化队列 |
k_msgq_put() |
发送消息 |
k_msgq_get() |
接收消息 |
k_msgq_peek() |
查看消息,不移除 |
k_msgq_purge() |
清空队列 |
k_msgq_num_used_get() |
获取已用消息数量 |
k_msgq_num_free_get() |
获取空闲消息数量 |
9.2 完整示例:两个线程通过队列通信
c
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <stdint.h>
/* 定义消息结构体 */
struct motor_msg {
uint32_t motor_id;
int32_t speed;
};
/* 定义消息队列,容量8 */
K_MSGQ_DEFINE(
motor_queue,
sizeof(struct motor_msg),
8,
4
);
/* 生产者线程 */
void producer(void *a, void *b, void *c)
{
struct motor_msg msg = {
.motor_id = 1,
.speed = 0
};
while (1) {
msg.speed += 100;
int ret = k_msgq_put(
&motor_queue,
&msg,
K_NO_WAIT
);
if (ret != 0) {
printk("Message queue full!\n");
}
k_msleep(500);
}
}
/* 消费者线程 */
void consumer(void *a, void *b, void *c)
{
struct motor_msg msg;
while (1) {
if (k_msgq_get(
&motor_queue,
&msg,
K_FOREVER) == 0) {
printk(
"Motor ID: %u, Speed: %d\n",
msg.motor_id,
msg.speed
);
}
}
}
/* 创建两个线程 */
K_THREAD_DEFINE(
producer_id, 1024,
producer, NULL, NULL, NULL,
5, 0, 0
);
K_THREAD_DEFINE(
consumer_id, 1024,
consumer, NULL, NULL, NULL,
5, 0, 0
);
int main(void)
{
printk("Message queue demo started\n");
return 0;
}
这个例子模拟:
- 生产者线程每隔 500 ms 生成一条电机速度消息。
- 生产者调用
k_msgq_put()。 - 消费者通过
k_msgq_get()阻塞等待。 - 收到数据后打印电机 ID 和速度。
消息队列会复制结构体内容,因此生产者修改局部消息对象不会直接改变已经入队的数据。
如果消息队列满了,使用 K_NO_WAIT 的发送操作会立即返回错误,需要根据业务设计丢弃、重试或其他处理策略。
十、Timer API:软件定时器
头文件:
c
#include <zephyr/kernel.h>
10.1 常用 API
| API | 功能 |
|---|---|
K_TIMER_DEFINE() |
静态定义定时器 |
k_timer_init() |
初始化定时器 |
k_timer_start() |
启动定时器 |
k_timer_stop() |
停止定时器 |
k_timer_status_get() |
获取到期次数 |
k_timer_status_sync() |
等待定时器到期 |
k_timer_remaining_get() |
获取剩余时间 |
10.2 示例代码
c
void timer_callback(struct k_timer *timer)
{
/* 定时器到期处理 */
}
K_TIMER_DEFINE(my_timer, timer_callback, NULL);
void start_my_timer(void)
{
k_timer_start(
&my_timer,
K_SECONDS(1),
K_MSEC(500)
);
}
含义:
- 启动后 1 秒第一次到期。
- 此后每 500 ms 周期到期。
特别注意:Zephyr 软件定时器的到期回调在系统时钟中断上下文中执行。
因此不要直接在回调中执行阻塞操作或大量耗时运算。
如果需要处理复杂业务,可以让回调通知一个线程,或者提交一个 Work Queue 工作项。
十一、Work Queue API:工作队列
工作队列用于将非紧急处理任务交给指定的线程执行。
头文件:
c
#include <zephyr/kernel.h>
11.1 常用 API
| API | 功能 |
|---|---|
K_WORK_DEFINE() |
定义工作项 |
k_work_init() |
初始化工作项 |
k_work_submit() |
提交到系统工作队列 |
k_work_submit_to_queue() |
提交到自定义队列 |
K_WORK_DELAYABLE_DEFINE() |
定义延迟工作项 |
k_work_schedule() |
延迟提交工作 |
k_work_reschedule() |
重新安排执行时间 |
11.2 示例
c
void work_handler(struct k_work *work)
{
printk("Work handler running\n");
}
/* 静态定义工作项 */
K_WORK_DEFINE(my_work, work_handler);
/* 提交工作 */
void submit_my_work(void)
{
k_work_submit(&my_work);
}
例如在 UART 或 CAN 接收中断发生时,可以提交工作项:
text
外设产生中断
|
v
中断服务程序
|
v
k_work_submit()
|
v
Work Queue线程
|
v
执行后续处理
需要注意,系统工作队列中的任务可能相互影响,因此对时延有严格要求的工作不能仅依靠系统工作队列保证实时性。
十二、UART API:串口通信
头文件:
c
#include <zephyr/drivers/uart.h>
Zephyr UART 驱动主要提供三种 API 方式。
| 模式 | 特点 | 适用场景 |
|---|---|---|
| Polling | 简单,轮询收发 | 调试、低速通信 |
| Interrupt-driven | 中断驱动 | 常规串口通信 |
| Async | 异步事件驱动,通常结合 DMA | 高吞吐或复杂串口应用 |
12.1 常用 API
| API | 功能 |
|---|---|
uart_poll_out() |
轮询发送一个字符 |
uart_poll_in() |
轮询查询并读取一个字符 |
uart_irq_rx_enable() |
使能接收中断 |
uart_irq_rx_disable() |
关闭接收中断 |
uart_irq_callback_user_data_set() |
注册中断回调 |
uart_fifo_read() |
读取 FIFO 数据 |
uart_fifo_fill() |
写入 FIFO 数据 |
uart_callback_set() |
注册异步回调 |
uart_tx() |
异步发送 |
uart_rx_enable() |
启动异步接收 |
uart_rx_disable() |
停止异步接收 |
12.2 Polling 发送示例
假设 uart_dev 已指向一个初始化完成的 UART 设备:
c
const char msg[] = "Hello Zephyr UART!\r\n";
for (int i = 0; msg[i] != '\0'; i++) {
uart_poll_out(uart_dev, msg[i]);
}
其中:
c
uart_poll_out(uart_dev, msg[i]);
每次发送一个字符。
12.3 配置文件
轮询通信通常只需要启用串口功能:
ini
CONFIG_SERIAL=y
中断模式:
ini
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
异步模式:
ini
CONFIG_SERIAL=y
CONFIG_UART_ASYNC_API=y
需要注意,具体 MCU 驱动不一定同时支持所有模式。
同一个 UART 外设不能同时使用中断驱动 API 和 Async API 的回调机制,否则可能发生冲突。
官方文档:
https://docs.zephyrproject.org/latest/hardware/peripherals/uart.html
十三、SPI API
头文件:
c
#include <zephyr/drivers/spi.h>
| API | 功能 |
|---|---|
SPI_DT_SPEC_GET() |
获取 SPI 设备配置信息 |
spi_is_ready_dt() |
检查 SPI 设备是否就绪 |
spi_transceive() |
同步全双工收发 |
spi_transceive_dt() |
使用设备树配置收发 |
spi_write() |
同步写入 |
spi_write_dt() |
使用设备树配置写入 |
spi_read() |
同步读取 |
spi_read_dt() |
使用设备树配置读取 |
SPI 相关操作通常使用:
c
struct spi_config
struct spi_buf
struct spi_buf_set
来配置总线模式、频率以及收发缓冲区。
例如:
c
spi_transceive_dt(
&spi_dev,
&tx_buf_set,
&rx_buf_set
);
这里假设 spi_dev 为已经正确初始化的 struct spi_dt_spec。
对于机器人应用,SPI 常用于:
- 绝对值编码器
- IMU
- 外部 ADC
- 高速数据交换
十四、I2C API
头文件:
c
#include <zephyr/drivers/i2c.h>
| API | 功能 |
|---|---|
I2C_DT_SPEC_GET() |
获取 I2C 设备描述 |
i2c_is_ready_dt() |
检查总线设备是否就绪 |
i2c_write_dt() |
写入数据 |
i2c_read_dt() |
读取数据 |
i2c_write_read_dt() |
先写后读 |
i2c_burst_read_dt() |
连续读取寄存器数据 |
例如:
c
uint8_t reg = 0x00;
uint8_t data[2];
i2c_write_read_dt(
&i2c_dev,
®,
sizeof(reg),
data,
sizeof(data)
);
这里假设 i2c_dev 已正确初始化。
该函数可以实现典型的寄存器读取流程:
text
发送设备寄存器地址
|
v
重新开始读操作
|
v
读取指定字节数
实际使用时需要注意寄存器地址宽度、设备协议、I2C 地址等配置。
十五、PWM 和 ADC API
15.1 PWM
头文件:
c
#include <zephyr/drivers/pwm.h>
| API | 功能 |
|---|---|
PWM_DT_SPEC_GET() |
获取 PWM 配置 |
pwm_is_ready_dt() |
检查 PWM 控制器 |
pwm_set() |
设置 PWM 周期与脉宽 |
pwm_set_dt() |
使用设备树描述设置 PWM |
pwm_set_pulse_dt() |
按设备树周期设置脉宽 |
pwm_set_cycles() |
以硬件时钟周期为单位配置 PWM |
例如:
c
pwm_set(
pwm_dev,
channel,
PWM_MSEC(1),
PWM_USEC(500),
PWM_POLARITY_NORMAL
);
假设 pwm_dev 和 channel 已正确配置,上述调用设置:
- PWM 周期为 1 ms。
- 高有效脉宽为 500 μs。
- 占空比为 50%。
15.2 ADC
头文件:
c
#include <zephyr/drivers/adc.h>
| API | 功能 |
|---|---|
ADC_DT_SPEC_GET() |
获取 ADC 通道信息 |
adc_is_ready_dt() |
检查 ADC |
adc_channel_setup_dt() |
配置 ADC 通道 |
adc_read_dt() |
采样 ADC |
adc_raw_to_millivolts_dt() |
将采样值转换为毫伏 |
常见应用包括电压、电流和模拟量传感器采样。
对于 FOC 控制这种需要互补 PWM、死区控制和 ADC 精确同步触发的场景,需要进一步评估具体 STM32 驱动是否提供相应功能。Zephyr 通用 PWM/ADC API 并不等同于 STM32 高级定时器的全部能力。
十六、CAN / CAN FD API
CAN 是机器人控制器、电机驱动器和工业设备常用的通信方式。
Zephyr 提供统一的 CAN Controller API。
头文件:
c
#include <zephyr/drivers/can.h>
16.1 常用 API
| API | 功能 |
|---|---|
can_get_capabilities() |
查询控制器支持的功能 |
can_set_mode() |
设置 CAN 工作模式 |
can_set_bitrate() |
设置仲裁阶段波特率 |
can_set_bitrate_data() |
设置 CAN FD 数据阶段波特率 |
can_start() |
启动 CAN 控制器 |
can_stop() |
停止 CAN 控制器 |
can_send() |
发送 CAN 数据帧 |
can_add_rx_filter() |
添加接收过滤器和回调 |
can_add_rx_filter_msgq() |
将收到的帧发送到消息队列 |
can_remove_rx_filter() |
删除接收过滤器 |
can_get_state() |
获取 CAN 总线状态 |
can_recover() |
从 Bus-Off 状态尝试恢复 |
can_bytes_to_dlc() |
字节数转换为 DLC |
can_dlc_to_bytes() |
DLC 转换为字节数 |
16.2 常见工作模式
c
CAN_MODE_NORMAL
CAN_MODE_LOOPBACK
CAN_MODE_LISTENONLY
CAN_MODE_FD
16.3 CAN FD 帧标志位
| 标志位 | 含义 |
|---|---|
CAN_FRAME_IDE |
29 位扩展帧 ID |
CAN_FRAME_RTR |
远程帧 |
CAN_FRAME_FDF |
CAN FD 帧 |
CAN_FRAME_BRS |
CAN FD 波特率切换 |
16.4 CAN FD 发送示例
假设 can_dev 已获取,且控制器已经完成 FD 模式配置、速率配置和 can_start():
c
struct can_frame frame = {
.id = 0x123,
.dlc = can_bytes_to_dlc(12),
.flags = CAN_FRAME_FDF | CAN_FRAME_BRS,
.data = {
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C
}
};
int ret = can_send(
can_dev,
&frame,
K_MSEC(100),
NULL,
NULL
);
if (ret < 0) {
printk("CAN FD send failed: %d\n", ret);
}
需要注意:
- CAN FD 模式需要控制器及驱动支持。
- 通常需要在
prj.conf中配置CONFIG_CAN=y和CONFIG_CAN_FD_MODE=y。 - CAN FD 的仲裁阶段和数据阶段波特率需要正确匹配总线。
can_send()的设备必须提前完成初始化并启动。- 如果使用 BRS,通信双方都需要支持相应配置。
CAN FD 的 DLC 与数据字节数并不完全相同:
| DLC | 数据长度 |
|---|---|
| 0~8 | 0~8 字节 |
| 9 | 12 字节 |
| 10 | 16 字节 |
| 11 | 20 字节 |
| 12 | 24 字节 |
| 13 | 32 字节 |
| 14 | 48 字节 |
| 15 | 64 字节 |
因此不能简单将 64 字节直接写成 .dlc = 64。
在实际机器人控制项目中,推荐使用:
text
CAN FD RX
|
v
接收过滤器
|
v
Message Queue
|
v
通信处理线程
|
v
电机状态解析
|
v
控制逻辑
Zephyr 的 can_add_rx_filter_msgq() 可以直接将匹配的 CAN 帧放入消息队列,从而减少手动编写中断回调转发代码的工作量。
官方文档:
https://docs.zephyrproject.org/latest/hardware/peripherals/can/controller.html
十七、Logging API:日志调试
头文件:
c
#include <zephyr/logging/log.h>
常用 API:
| API | 功能 |
|---|---|
LOG_MODULE_REGISTER() |
注册日志模块 |
LOG_ERR() |
错误日志 |
LOG_WRN() |
警告日志 |
LOG_INF() |
普通信息 |
LOG_DBG() |
调试日志 |
LOG_HEXDUMP_INF() |
输出十六进制数据 |
示例:
c
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(motor, LOG_LEVEL_DBG);
void motor_init(void)
{
LOG_INF("Motor initialized");
LOG_DBG("Motor debug message");
}
prj.conf:
ini
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=4
在正式项目中,更推荐使用 Logging 进行模块化调试,而不是全部使用 printk()。
Logging 支持模块级日志控制、过滤和不同日志输出后端。
十八、Shell API:命令行调试
头文件:
c
#include <zephyr/shell/shell.h>
常用 API:
| API | 功能 |
|---|---|
SHELL_CMD_REGISTER() |
注册命令 |
SHELL_CMD_ARG_REGISTER() |
注册带参数检查的命令 |
SHELL_STATIC_SUBCMD_SET_CREATE() |
创建子命令集合 |
shell_print() |
输出消息 |
shell_warn() |
输出警告 |
shell_error() |
输出错误 |
示例:
c
#include <zephyr/shell/shell.h>
static int cmd_hello(
const struct shell *sh,
size_t argc,
char **argv)
{
shell_print(sh, "Hello Zephyr!");
return 0;
}
SHELL_CMD_REGISTER(
hello,
NULL,
"Print hello message",
cmd_hello
);
配置并启用 Shell 后,可在终端执行:
text
uart:~$ hello
Hello Zephyr!
对于机器人控制器,可以通过 Shell 实现:
text
motor status
motor enable
can status
system info
这些命令需要开发者进一步编写相应处理函数,并做好权限及安全限制。
十九、STM32 HAL / FreeRTOS 与 Zephyr API 对照表
如果原来熟悉 STM32 HAL 或 FreeRTOS,可以通过以下表格快速找到功能相近的接口。
| STM32 HAL / FreeRTOS | Zephyr API |
|---|---|
HAL_Delay() |
k_msleep() |
HAL_GetTick() |
k_uptime_get() |
HAL_GPIO_WritePin() |
gpio_pin_set_dt() |
HAL_GPIO_ReadPin() |
gpio_pin_get_dt() |
HAL_GPIO_TogglePin() |
gpio_pin_toggle_dt() |
HAL_UART_Transmit() |
uart_poll_out() / uart_tx() |
HAL_UART_Receive_IT() |
UART Interrupt API |
HAL_UART_Receive_DMA() |
UART Async API(驱动支持时) |
HAL_SPI_TransmitReceive() |
spi_transceive() |
HAL_I2C_Mem_Read() |
i2c_write_read_dt() 等组合接口 |
HAL_FDCAN_AddMessageToTxFifoQ() |
can_send() |
xTaskCreate() |
k_thread_create() |
vTaskDelay() |
k_msleep() |
xSemaphoreTake() |
k_sem_take() |
xSemaphoreGive() |
k_sem_give() |
xQueueSend() |
k_msgq_put() |
xQueueReceive() |
k_msgq_get() |
xTimerStart() |
k_timer_start() |
需要注意,以上接口只是功能上的近似对应关系,并不能不加修改地直接互换。
二十、Zephyr 开发中常见的几个坑
20.1 Devicetree 和 Kconfig 混淆
例如:
ini
CONFIG_CAN=y
只是启用 CAN 软件功能,并不意味着某个 CAN 控制器节点一定已经启用。
还需要确认相关设备树、引脚复用及驱动配置。
20.2 在中断中使用阻塞 API
例如:
c
k_msleep(100);
不应该在 ISR 中调用。
中断中应优先完成必要且简短的工作,再通过信号量、消息队列或者 Work Queue 触发后续处理。
20.3 不检查返回值
例如:
c
int ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT);
if (ret < 0) {
printk("GPIO configuration failed: %d\n", ret);
}
在实际项目中,应当检查设备初始化、通信发送、资源申请等操作的返回值。
20.4 混淆逻辑电平和物理电平
Zephyr GPIO API 带有逻辑有效电平的概念。
使用 GPIO_ACTIVE_LOW 时,逻辑 1 不代表物理引脚一定为高电平。
20.5 将 RTOS 软件定时器当成精密硬件定时器
软件定时器适合周期任务、事件触发等场景。
如果需要高精度 PWM、ADC 同步采样或者严格周期控制,应采用合适的 MCU 硬件外设和实时设计方案。
20.6 不同 Zephyr 版本混用 API
Zephyr 持续更新,部分 API、驱动能力及配置项可能发生变化。
建议:
- 项目固定使用一个明确版本。
- 阅读与项目版本对应的文档。
- 编译时以本地
include/zephyr/下的头文件为准。 - 尽量参考对应版本的官方 Samples。
二十一、推荐的 API 学习顺序
对于有一定 STM32 或 FreeRTOS 基础的开发者,推荐按下面的顺序学习。
| 阶段 | 学习模块 | 学习目标 |
|---|---|---|
| 第一阶段 | Kernel、GPIO | LED、延时、GPIO 中断 |
| 第二阶段 | Device、Devicetree | 看懂硬件配置 |
| 第三阶段 | UART | 串口收发 |
| 第四阶段 | Thread | 创建和管理多线程 |
| 第五阶段 | Semaphore、Mutex | 线程同步和资源互斥 |
| 第六阶段 | Message Queue | 线程间传递消息 |
| 第七阶段 | Timer、Work Queue | 定时任务和异步处理 |
| 第八阶段 | SPI、I2C、PWM、ADC | 外设驱动开发 |
| 第九阶段 | CAN / CAN FD | 通信节点开发 |
| 第十阶段 | Logging、Shell | 工程调试与管理 |
建议学习方法:
阅读 API → 运行官方 Sample → 修改参数 → 自己编写一个小程序 → 再应用到项目。
不要一开始就深入研究所有 Zephyr 内核实现,否则很容易陷入大量宏定义、构建脚本和驱动源码中。
二十二、官方学习资源
以下是建议收藏的官方文档:
总结
Zephyr RTOS 的 API 虽然非常丰富,但对于一般嵌入式应用,真正需要优先掌握的主要集中在:
- Kernel API:线程、时间管理和调度。
- GPIO / UART API:基础硬件控制与通信。
- Semaphore / Mutex / Message Queue:线程同步与数据传递。
- Devicetree / Device Model:统一管理硬件资源。
- SPI / I2C / CAN FD:外部设备和总线通信。
- Logging / Shell:程序调试与维护。
掌握这些内容后,就可以尝试构建更加完整的嵌入式系统,例如:
text
Zephyr RTOS
|
+------------+------------+
| | |
UART Thread CAN Thread Control Thread
| | |
+------ Message Queue ----+
|
v
Application
|
+----------+----------+
| | |
SPI PWM GPIO
对于机器人控制器、电机驱动器以及工业嵌入式设备来说,这些 API 都具有很高的实际使用价值。
后续还可以结合 STM32G474、CAN FD 和 RS485,继续研究基于 Zephyr 的实时通信与多线程控制程序。
Zephyr 的学习重点不是记住所有 API,而是能够理解什么时候使用什么 API,以及如何将这些 API 组合成一个可维护、可扩展的嵌入式应用。