这个阶段用 Zephyr 的话来说就是在做 BSP(板级支持包)。传统单片机开发里 BSP 是一堆 bsp_xxx.c/h,而 Zephyr 里它变成了"设备树 overlay + Kconfig + 一堆小验证工程"。
一、先花五分钟搞懂:Zephyr 是怎么"认识"你的硬件的
如果你和我一样是从 STM32/HAL 过来的,第一件事要转变观念:Zephyr 把"哪个引脚干什么"从 C 代码里拿走了。
一个 Zephyr 应用 = 三份东西:
| prj.conf(Kconfig) | 功能开关:要不要 SPI、要不要显示驱动、堆多大 | CubeMX 里的勾选 |
| boards/*.overlay(设备树) | 硬件描述:引脚分配、外设参数、别名 | CubeMX 的引脚图 |
| src/*.c | 业务逻辑 | 用户代码 |
C 代码里拿外设的"三板斧"宏(全工程就这三种写法,记住就够):
/* ① 从别名/节点取"描述结构体"——编译期生成,零运行时开销 */
static const struct gpio_dt_spec trig = GPIO_DT_SPEC_GET(DT_ALIAS(sonar_trig), gpios);
static const struct pwm_dt_spec servo = PWM_DT_SPEC_GET(DT_ALIAS(servo_pwm));
const struct device *display = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
/* ② 用时先查 ready */
if (!gpio_is_ready_dt(&trig)) { ... }
/* ③ 正常调用 */
gpio_pin_set_dt(&trig, 1);二、环境验证:点灯(水一遍)
开箱帖点过灯了,但写帖时发现忘附图,再水一次:)

板载 RGB 灯,三个 GPIO 低电平点亮。设备树里长这样(板卡自带的,不用我们写):
externalzephyrboardsnxpfrdm_mcxa153frdm_mcxa153.dts

C 代码里 DT_ALIAS(led0) 一取就用:
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
while (1) {
gpio_pin_toggle_dt(&led);
k_msleep(1000);
}注意一个细节:GPIO_DT_SPEC_GET 一次把"哪个口、哪个脚、什么电平有效"三件事打包带走,之后所有操作都是 _dt 后缀的函数自动识别——全程不需要出现 gpio3、12 这种裸数字。这就是 BSP 的"业务代码与引脚解耦"。
编译烧录:



三、HC-SR04 超声波:第一个"自定义外设"
HC-SR04 都熟:Trig 给个 ≥10µs 高脉冲,模块发 40kHz 声波,Echo 回一个和距离成正比的高电平,d = t×343/2。

接线
Trig 接 GPIO 输出,Echo 接 GPIO 输入配双边沿中断。这里有个必须较真的点:Echo 是 5V 电平,MCXA153 的 GPIO 不耐 5V——要么电阻分压,要么直接买 3.3V 兼容的 HC-SR04P。我一开始没注意,后面发现mcu link连不上A153,但是好在mcu没完全挂,千万别学我 T_T。
BSP 第一步:给引脚起别名
板卡设备树里没有"超声波"这种东西,要在应用 overlay 里自己声明。Zephyr 声明 GPIO 引脚最省事的写法是借 gpio-leds 这个现成绑定(兼容串随便借,只要属性对得上):
/ {
aliases {
sonar-trig = &sonar_trig_pin;
sonar-echo = &sonar_echo_pin;
};
app_gpio_pins {
compatible = "gpio-leds";
sonar_trig_pin: sonar_trig {
gpios = <&gpio3 28 GPIO_ACTIVE_HIGH>;
};
sonar_echo_pin: sonar_echo {
gpios = <&gpio3 27 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>;
};
};
};aliases 是这套 BSP 的灵魂:业务代码只认 sonar-trig 这个小名,哪天引脚要挪,只改 overlay 一行,C 代码不动。
BSP 第二步:驱动代码
测距的核心是脉宽计时,用 GPIO 双边沿中断 + 信号量最合适——上升沿记时标、下降沿收尾并唤醒等待线程:
static void echo_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
if (gpio_pin_get_dt(&echo_spec) > 0) {
t_rise = k_cycle_get_32(); /* 上升沿:声波出发 */
} else {
t_fall = k_cycle_get_32(); /* 下降沿:收到回波 */
k_sem_give(&sem_echo_done);
}
}
static int32_t sonar_measure_distance_mm(void)
{
gpio_pin_set_dt(&trig_spec, 1);
k_busy_wait(12); /* ≥10µs 触发脉冲 */
gpio_pin_set_dt(&trig_spec, 0);
if (k_sem_take(&sem_echo_done, K_MSEC(35)) != 0) {
return -1; /* 超时 = 4m 内没东西 */
}
uint32_t cycles = (t_fall >= t_rise) ? (t_fall - t_rise)
: (UINT32_MAX - t_rise + t_fall + 1);
uint32_t us = (uint32_t)k_cyc_to_us_floor64(cycles);
return (int32_t)((us * 343ULL) / 2000ULL);
}几个当时琢磨过的点:
为什么用 k_cycle_get_32() 不用 k_uptime_get_32():后者单位是 ms,几十厘米的变化只有零点几 ms,精度不够;cycle 是 CPU tick,96MHz 下分辨率 10ns 级。
为什么中断里给信号量而不是直接算:中断要短,耗时活(换算、打印)都放线程。
回波超时会污染下一次测量:后来的版本在触发前先排空信号量、再加个 echo_armed 保险栓,非测量窗口的边沿一律忽略。
连续采样加了 5 点中值滤波,偶发跳变基本绝迹。
实际测试发现还挺准的
四、MG90S 舵机:第一个要动 pinctrl 的外设

舵机信号是 50Hz PWM,0.5ms~2.5ms 脉宽对应 0°~180°。接线一句话:信号线接 PWM 输出,独立 5V 供电,和板子共地。
BSP 时刻:默认引脚不够用,自己写 pinmux
查 MCXA153 的引脚表后我选了 P3_9(FlexPWM0 的 B1 通道)。但板卡自带的 pinctrl 里没有这组配置——BSP 工作来了:在 overlay 里自己加一组引脚配置:
#include <nxp/mcx/MCXA153VLH-pinctrl.h>
&pinctrl {
pinmux_flexpwm0_pwm1: pinmux_flexpwm0_pwm1 {
group0 {
pinmux = <PWM0_B1_P3_9>; /* P3_9 = FlexPWM0 SM1 B1 */
slew-rate = "fast";
};
};
};
&flexpwm0_pwm1 {
status = "okay";
pinctrl-0 = <&pinmux_flexpwm0_pwm1>;
pinctrl-names = "default";
};这就是 Zephyr 的引脚复用哲学:引脚功能不由 GPIO 寄存器顺手配,而是集中交给 pinctrl 子系统,设备启用时自动应用。#include 那个头文件提供 PWM0_B1_P3_9 这类宏——每个引脚每个功能一个宏,查表即得。
PWM 设备描述借 pwm-leds 绑定:
servo_dev {
compatible = "pwm-leds";
servo_node: servo_0 {
pwms = <&flexpwm0_pwm1 1 20000000 0>; /* 通道1,20ms 周期=50Hz */
};
};C 代码就三行核心:
static const struct pwm_dt_spec servo = PWM_DT_SPEC_GET(DT_ALIAS(servo_pwm)); uint32_t pulse_us = SERVO_MIN_PULSE_US + ((uint32_t)angle * (SERVO_MAX_PULSE_US - SERVO_MIN_PULSE_US)) / 180; pwm_set_pulse_dt(&servo, PWM_USEC(pulse_us));
五、ST7735 屏:节点最多的一个外设

160×128 小彩屏,4 线 SPI。这是三个外设里设备树节点最多、最"BSP"的一个。
三层节点,层层套娃
/ {
chosen {
zephyr,display = &st7735; /* 钦定系统主屏 */
};
mipi_dbi {
compatible = "zephyr,mipi-dbi-spi"; /* Zephyr 显示协议层 */
spi-dev = <&lpspi0>; /* 底层用哪个 SPI 控制器 */
dc-gpios = <&gpio3 30 GPIO_ACTIVE_HIGH>;
st7735: st7735r@0 {
compatible = "sitronix,st7735r";
reg = <0>;
mipi-max-frequency = <8000000>;
width = <160>; height = <128>;
madctl = <0x60>; colmod = <0x55>; /* 方向 + RGB565 */
pwctr1 = [a2 02 84]; /* ...一长串电源/伽马参数 */
};
};
};
&lpspi0 {
status = "okay";
cs-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
};结构上值得看懂:应用代码面对的是 display 通用 API(display_write/display_blanking_off),中间是 Zephyr 的 mipi-dbi 协议层,底下才落到 SPI 控制器。换一块 ILI9341 屏?改 st7735r 节点里的兼容串和参数,C 代码零改动。
pwctr1/gamctr 是屏厂给的初始化序列,Zephyr 驱动照单全收。这块不用懂原理,照抄即可,后面 radar 篇详聊。
注意事项
字节序:MCU 小端、SPI 按大端发 RGB565,写之前要 __builtin_bswap16();
显存存不起:160×128×2 = 40KB,SRAM 总共 24KB。所以刷屏用一行 320 字节的行缓冲逐行写,纯色填充足够快。
static uint16_t line_buffer[160];
uint16_t swapped = __builtin_bswap16(color);
for (int i = 0; i < 160; i++) line_buffer[i] = swapped;
desc.buf_size = sizeof(line_buffer);
desc.width = 160; desc.height = 1; desc.pitch = 160;
for (int y = 0; y < 128; y++) {
display_write(display_dev, 0, y, &desc, line_buffer);
}六、攒 BSP 的一周,总结四句
每个外设一个最小验证工程:hello→blinky→PWM→超声波→屏→舵机,各自独立可编译。好处是集成出问题时随时能二分定位——后来的花屏排查就靠 5_1 这个最小屏工程救的命。
overlay 就是 BSP 的本体:引脚、外设参数、别名全在里面,C 代码只认别名。把它当宝贝维护,别在代码里写裸引脚号。
驱动三步走背下来:DT_ 宏取描述 → is_ready 检查 → 调用,Zephyr 所有驱动都是这个节奏,会一个就会全部。
Zephyr 中文资料太少了。

我要赚赏金
