在嵌入式开发领域,实时操作系统(RTOS)正变得越来越重要。Zephyr作为一款专为物联网设备设计的开源RTOS,凭借其轻量级、模块化和跨平台特性,正在获得越来越多开发者的青睐。本文将带你从零开始,在STM32开发板上完成第一个Zephyr应用的开发,让你快速掌握这个强大工具的基本使用方法。
在开始之前,我们需要准备好开发环境。Zephyr使用一套名为West的工具链来管理项目构建流程,这与传统的嵌入式开发工具链有所不同。
首先,确保你的开发机满足以下基本要求:
安装步骤:
bash复制# 安装依赖工具
sudo apt update && sudo apt install -y git cmake ninja-build gperf \
ccache dfu-util device-tree-compiler wget \
python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \
make gcc gcc-multilib g++-multilib libsdl2-dev
# 安装West工具
pip3 install --user west
echo 'export PATH=~/.local/bin:"$PATH"' >> ~/.bashrc
source ~/.bashrc
提示:Windows用户可以使用WSL2来获得与Linux相似的环境体验,或者直接使用Windows版的Python和工具链。
验证West安装是否成功:
bash复制west --version
接下来,我们需要获取Zephyr源代码并设置开发环境:
bash复制# 初始化工作区
west init ~/zephyrproject
cd ~/zephyrproject
# 获取Zephyr源代码和所有依赖项
west update
# 导出Zephyr CMake包
west zephyr-export
# 安装Python依赖
pip3 install --user -r ~/zephyrproject/zephyr/scripts/requirements.txt
最后,安装工具链。Zephyr支持多种工具链,这里我们使用Zephyr SDK:
bash复制# 下载Zephyr SDK
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.1/zephyr-sdk-0.16.1_linux-x86_64.tar.xz
# 解压并安装
tar xvf zephyr-sdk-0.16.1_linux-x86_64.tar.xz
cd zephyr-sdk-0.16.1
./setup.sh
现在,我们可以开始创建第一个Zephyr项目了。我们将基于STM32 Nucleo-F401RE开发板进行演示,这是STMicroelectronics推出的一款性价比极高的开发板。
项目创建步骤:
bash复制mkdir -p ~/zephyr_workspace/hello_world
cd ~/zephyr_workspace/hello_world
bash复制mkdir src
touch src/main.c CMakeLists.txt prj.conf
CMakeLists.txt文件,添加以下内容:cmake复制cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(hello_world)
target_sources(app PRIVATE src/main.c)
prj.conf配置文件:config复制CONFIG_PRINTK=y
CONFIG_STDOUT_CONSOLE=y
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
src/main.c源文件:c复制#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
void main(void)
{
printk("Hello World from Zephyr on STM32!\n");
while (1) {
k_msleep(1000);
printk("System running for %d seconds\n", k_uptime_get()/1000);
}
}
Zephyr使用CMake作为构建系统,West作为前端工具。构建过程会自动处理所有依赖关系,包括板级支持包(BSP)和设备树配置。
构建命令:
bash复制# 在项目目录下执行
west build -b nucleo_f401re .
这个命令会:
-b参数指定的开发板名称自动选择正确的BSP构建完成后,你可以在build/zephyr目录下找到生成的固件文件,主要是:
zephyr.elf:ELF格式的可执行文件zephyr.bin:二进制格式的固件zephyr.hex:Intel HEX格式的固件将STM32 Nucleo开发板通过USB连接到电脑,它会被识别为一个虚拟串口设备和调试探头。我们可以使用West直接烧录和调试。
烧录命令:
bash复制west flash
这个命令会自动:
查看输出:
烧录完成后,我们可以通过串口终端查看程序输出。Nucleo开发板会创建一个虚拟串口设备(在Linux上通常是/dev/ttyACM0,Windows上是COM端口)。
使用你喜欢的串口终端工具(如minicom、picocom或Putty)连接这个串口,配置为:
你应该能看到类似这样的输出:
code复制Hello World from Zephyr on STM32!
System running for 1 seconds
System running for 2 seconds
...
现在我们已经成功运行了第一个Zephyr程序,让我们更深入地看看项目中的各个部分是如何工作的。
Zephyr使用设备树来描述硬件配置。对于Nucleo-F401RE开发板,其设备树定义位于zephyr/boards/arm/nucleo_f401re目录中。
我们可以查看开发板的设备树配置:
bash复制cat ~/zephyrproject/zephyr/boards/arm/nucleo_f401re/nucleo_f401re.dts
设备树定义了开发板上的各种硬件资源,如:
Zephyr使用Kconfig系统来管理软件功能的配置。我们在prj.conf中设置的选项就是Kconfig配置。
一些常用的配置选项:
| 配置选项 | 描述 |
|---|---|
| CONFIG_PRINTK | 启用printk输出功能 |
| CONFIG_STDOUT_CONSOLE | 启用标准输出控制台 |
| CONFIG_SERIAL | 启用串口驱动支持 |
| CONFIG_UART_CONSOLE | 设置串口作为控制台输出 |
我们可以使用交互式配置工具来查看和修改配置:
bash复制west build -t menuconfig
Zephyr是一个多线程RTOS,让我们修改Hello World程序,创建一个简单的线程:
c复制#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
void worker_thread(void *arg1, void *arg2, void *arg3)
{
while (1) {
printk("Worker thread running\n");
k_msleep(500);
}
}
K_THREAD_DEFINE(worker_tid, 1024, worker_thread, NULL, NULL, NULL, 7, 0, 0);
void main(void)
{
printk("Hello World from Zephyr on STM32!\n");
while (1) {
k_msleep(1000);
printk("Main thread running for %d seconds\n", k_uptime_get()/1000);
}
}
这段代码创建了一个优先级为7的工作线程,它会每500ms打印一次消息。主线程继续每秒打印一次运行时间。
重新构建并烧录程序,你应该能看到两个线程交替输出信息。
现在我们已经掌握了Zephyr的基本使用方法,让我们为项目添加一些更实用的功能。
Nucleo-F401RE开发板上有一个用户LED(LD2),连接到PA5引脚。我们可以通过Zephyr的GPIO API来控制它。
修改prj.conf,添加GPIO支持:
config复制CONFIG_GPIO=y
更新src/main.c:
c复制#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/gpio.h>
/* 获取LED设备树节点 */
#define LED_NODE DT_ALIAS(led0)
/* 获取LED设备规范 */
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED_NODE, gpios);
void main(void)
{
int ret;
if (!device_is_ready(led.port)) {
printk("LED device is not ready\n");
return;
}
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
printk("Error configuring LED pin\n");
return;
}
printk("Hello World from Zephyr on STM32!\n");
while (1) {
gpio_pin_toggle_dt(&led);
k_msleep(1000);
printk("System running for %d seconds\n", k_uptime_get()/1000);
}
}
Nucleo开发板上还有一个用户按钮(B1),连接到PC13引脚。我们可以添加按钮检测功能。
更新prj.conf:
config复制CONFIG_GPIO=y
更新src/main.c:
c复制#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/gpio.h>
#define LED_NODE DT_ALIAS(led0)
#define BUTTON_NODE DT_ALIAS(sw0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED_NODE, gpios);
static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(BUTTON_NODE, gpios);
static struct gpio_callback button_cb_data;
void button_pressed(const struct device *dev, struct gpio_callback *cb,
uint32_t pins)
{
printk("Button pressed at %d ms\n", k_uptime_get());
}
void main(void)
{
int ret;
if (!device_is_ready(led.port) || !device_is_ready(button.port)) {
printk("Error: devices not ready\n");
return;
}
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
printk("Error configuring LED pin\n");
return;
}
ret = gpio_pin_configure_dt(&button, GPIO_INPUT);
if (ret < 0) {
printk("Error configuring button pin\n");
return;
}
ret = gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE);
if (ret < 0) {
printk("Error configuring button interrupt\n");
return;
}
gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin));
gpio_add_callback(button.port, &button_cb_data);
printk("Press the user button...\n");
while (1) {
gpio_pin_toggle_dt(&led);
k_msleep(1000);
}
}
这段代码实现了:
虽然命令行工具很强大,但使用IDE可以提高开发效率。Zephyr官方支持VS Code作为开发环境。
设置步骤:
安装VS Code和C/C++扩展
在项目目录下创建.vscode文件夹和设置文件:
settings.json:json复制{
"cmake.configureArgs": [
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
"-DBOARD=nucleo_f401re"
],
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools"
}
tasks.json:json复制{
"version": "2.0.0",
"tasks": [
{
"label": "Build with West",
"type": "shell",
"command": "west",
"args": ["build"],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
},
{
"label": "Flash with West",
"type": "shell",
"command": "west",
"args": ["flash"],
"problemMatcher": []
}
]
}
打开项目文件夹,VS Code会自动检测CMake项目并配置智能提示
在Zephyr开发过程中,你可能会遇到一些问题。以下是一些常见问题及其解决方法:
症状:执行west flash时提示找不到设备或烧录失败。
解决方案:
bash复制sudo cp ~/zephyrproject/zephyr/scripts/udev/rules.d/99-openocd.rules /etc/udev/rules.d/
sudo udevadm control --reload
症状:程序烧录成功,但串口终端没有输出。
解决方案:
prj.conf中是否启用了串口和控制台:config复制CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
症状:构建时出现内存不足错误。
解决方案:
CONFIG_MINIMAL_LIBC代替完整C库Zephyr支持多种调试方法:
printk输出调试信息bash复制west build -t debug
CONFIG_LOG=y)现在你已经掌握了Zephyr的基本使用方法,可以进一步探索其更多功能:
Zephyr提供了许多常见传感器的驱动,如温度传感器、加速度计等。例如,添加BME280温湿度传感器:
更新prj.conf:
config复制CONFIG_I2C=y
CONFIG_BME280=y
在代码中使用传感器:
c复制#include <zephyr/drivers/sensor.h>
const struct device *sensor = DEVICE_DT_GET_ONE(bosch_bme280);
if (sensor == NULL || !device_is_ready(sensor)) {
printk("Could not get BME280 device\n");
return;
}
struct sensor_value temp, press, humidity;
sensor_sample_fetch(sensor);
sensor_channel_get(sensor, SENSOR_CHAN_AMBIENT_TEMP, &temp);
sensor_channel_get(sensor, SENSOR_CHAN_PRESS, &press);
sensor_channel_get(sensor, SENSOR_CHAN_HUMIDITY, &humidity);
printk("Temp: %d.%06d C, Pressure: %d.%06d kPa, Humidity: %d.%06d %%\n",
temp.val1, temp.val2, press.val1, press.val2,
humidity.val1, humidity.val2);
Zephyr支持多种网络协议,包括蓝牙、Wi-Fi、TCP/IP等。例如,启用蓝牙功能:
更新prj.conf:
config复制CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="Zephyr Hello"
在代码中初始化蓝牙:
c复制#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};
void bt_ready(int err)
{
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}
printk("Bluetooth initialized\n");
err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);
if (err) {
printk("Advertising failed to start (err %d)\n", err);
return;
}
}
void main(void)
{
int err;
err = bt_enable(bt_ready);
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
}
while (1) {
k_msleep(1000);
}
}
Zephyr提供了强大的电源管理功能,可以显著降低设备功耗:
c复制#include <zephyr/pm/pm.h>
#include <zephyr/pm/policy.h>
void main(void)
{
/* 配置电源管理策略 */
pm_policy_state_lock_get(PM_STATE_SUSPEND_TO_IDLE, PM_ALL_SUBSTATES);
while (1) {
/* 进入低功耗模式 */
k_sleep(K_SECONDS(10));
}
}
随着项目复杂度增加,良好的代码组织变得尤为重要。Zephyr项目推荐的结构如下:
code复制hello_world/
├── CMakeLists.txt # 主CMake配置文件
├── prj.conf # 主Kconfig配置文件
├── boards/ # 自定义板级配置
│ └── my_custom_board.defconfig
├── src/ # 应用程序源代码
│ ├── main.c
│ └── drivers/ # 自定义驱动
├── include/ # 头文件
│ └── app_config.h
├── dts/ # 自定义设备树覆盖
│ └── my_overlay.overlay
└── modules/ # 外部模块
└── my_module/
├── CMakeLists.txt
└── src/
对于团队开发,建议使用Git进行版本控制。典型的.gitignore文件内容:
code复制# Build directory
build/
# Generated files
zephyr/.config
zephyr/include/generated/
zephyr/zephyr.*
当项目变得复杂时,可能需要优化性能和资源使用:
调整线程栈大小:在prj.conf中设置:
config复制CONFIG_MAIN_STACK_SIZE=1024
CONFIG_IDLE_STACK_SIZE=256
使用内存池:对于固定大小的内存分配,使用内存池比动态分配更高效:
c复制K_MEM_POOL_DEFINE(my_pool, 64, 256, 4, 4);
void *block = k_mem_pool_alloc(&my_pool, 64, K_NO_WAIT);
使用ISR:对于时间敏感的操作用中断服务例程:
c复制void gpio_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
/* 中断处理代码 */
}
优化打印:减少printk调用,或使用异步日志:
config复制CONFIG_LOG=y
CONFIG_LOG_MODE_OVERFLOW=y
使用编译器优化:在prj.conf中设置:
config复制CONFIG_SIZE_OPTIMIZATIONS=y
# 或
CONFIG_SPEED_OPTIMIZATIONS=y
确保代码质量的一个重要环节是测试。Zephyr提供了测试框架支持:
单元测试:创建测试用例:
c复制#include <zephyr/ztest.h>
static void test_led(void)
{
zassert_true(device_is_ready(led.port), "LED device not ready");
}
void test_main(void)
{
ztest_test_suite(hello_world_tests,
ztest_unit_test(test_led)
);
ztest_run_test_suite(hello_world_tests);
}
运行测试:
bash复制west build -t run
硬件在环测试:使用Twister测试框架:
bash复制twister -p nucleo_f401re -T tests/
对于团队项目,设置CI/CD流水线可以自动化构建和测试过程。典型的GitHub Actions配置:
yaml复制name: Zephyr CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends git cmake ninja-build gperf \
ccache dfu-util device-tree-compiler wget \
python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \
make gcc gcc-multilib g++-multilib libsdl2-dev
- name: Install West
run: pip3 install west
- name: Initialize Zephyr
run: |
west init zephyrproject
cd zephyrproject
west update
west zephyr-export
pip3 install -r zephyr/scripts/requirements.txt
- name: Build Hello World
run: |
cd zephyrproject
west build -p always -b nucleo_f401re zephyr/samples/hello_world
Zephyr拥有活跃的开源社区,提供了丰富的学习资源:
zephyr/samples/目录遇到问题时,可以先搜索是否已有解决方案,或者在社区提问。提问时请提供:
对于熟悉FreeRTOS的开发者,这里有一些关键概念对比:
| 功能 | FreeRTOS | Zephyr |
|---|---|---|
| 任务 | xTaskCreate | k_thread_create |
| 队列 | xQueueCreate | k_msgq_init |
| 信号量 | xSemaphoreCreateBinary | k_sem_init |
| 互斥锁 | xSemaphoreCreateMutex | k_mutex_init |
| 定时器 | xTimerCreate | k_timer_init |
| 内存管理 | pvPortMalloc | k_malloc |
主要差异:
让我们把这些知识综合起来,构建一个简单的IoT设备,它能够:
完整代码示例:
prj.conf:
config复制CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="EnvSensor"
CONFIG_I2C=y
CONFIG_BME280=y
CONFIG_SENSOR=y
CONFIG_LOG=y
CONFIG_PM=y
src/main.c:
c复制#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/pm/pm.h>
#define INTERVAL_SECONDS 30
static const struct device *sensor = DEVICE_DT_GET_ONE(bosch_bme280);
static struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_NAME_COMPLETE, "EnvSensor", 9),
};
static void bt_ready(int err)
{
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}
err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);
if (err) {
printk("Advertising failed to start (err %d)\n", err);
return;
}
printk("Bluetooth initialized\n");
}
static int read_sensor_data(void)
{
struct sensor_value temp, humidity;
if (sensor_sample_fetch(sensor) < 0) {
printk("Sensor sample fetch failed\n");
return -1;
}
sensor_channel_get(sensor, SENSOR_CHAN_AMBIENT_TEMP, &temp);
sensor_channel_get(sensor, SENSOR_CHAN_HUMIDITY, &humidity);
printk("Temperature: %d.%06d C, Humidity: %d.%06d %%\n",
temp.val1, temp.val2, humidity.val1, humidity.val2);
return 0;
}
void main(void)
{
int err;
if (!device_is_ready(sensor)) {
printk("Sensor device not ready\n");
return;
}
err = bt_enable(bt_ready);
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}
/* 配置低功耗模式 */
pm_policy_state_lock_get(PM_STATE_SUSPEND_TO_IDLE, PM_ALL_SUBSTATES);
while (1) {
read_sensor_data();
k_sleep(K_SECONDS(INTERVAL_SECONDS));
}
}
这个示例展示了如何将Zephyr的各种功能组合起来创建一个实用的IoT设备。你可以进一步扩展它,比如添加云端连接、数据记录或更复杂的电源管理策略。