全部代码开源,GitHub搜索Tomimi-HCH/zephyr-ultrasonic-radar-scanner

一、功能一览
| 扇形扫描 | 舵机带动探头 0°~180° 往返扫描,1° 步进,18ms/步 |
| 实时测距 | 每个角度测一次,有效量程 20mm ~ 4000mm |
| 雷达画面 | TFT 屏实时绘制:网格、扫描线、红色回波遮挡线 |
| 量程切换 | 30 / 60 / 100 / 200 cm 四档,板载 SW2 循环切换 |
| 暂停/继续 | 板载 SW3 一键暂停扫描 |
| 温度上报 | 板载 P3T1755 温度传感器,I3C 读取,串口每 5 秒报一次 |
舵机连接 HC-SR04 探头模拟雷达转台,旁边放一把尺子当距离标尺,正前方立一块挡板当被测目标——挡板进入超声波波束,屏幕上对应角度就该出现回波。这张全景图里还能看到屏幕上的扇形网格和左侧已经扫出来的一小段回波(挡板来自立创开源硬件平台博主 __Aknice 的开源设计):

| 网格 | 深绿 | 半径 78px 半圆 + 3 圈等距环(26/52/78px)+ 7 根辐条(每 30° 一根) |
| 扫描线 | 亮绿 | 当前舵机指向的角度,跟着探头实时转动 |
| 遮挡线 | 红 | 该角度扫到物体:从物体距离一直画到量程边缘,有"余辉"效果 |
目标检测的结果(角度范围、宽度、距离)当前通过串口输出,屏幕上也预留了黄色目标框的绘图接口。
串口 115200,接上终端就能看到系统的心跳。每秒打印一次当前角度和距离,每 5 秒报一次温度,每帧结束打印检测到的目标:
[00:00:12.345] <inf> final_radar_system: angle= 45 deg dist= 512 mm [00:00:13.350] <inf> final_radar_system: angle= 51 deg dist= -- (no echo) [00:00:15.000] <inf> final_radar_system: Ambient temperature: 26.3 C [00:00:16.207] <inf> final_radar_system: object #0: 40-52 deg L=18.4cm D=51cm [00:00:19.500] <inf> final_radar_system: frame 5: no object
目标检测每帧结束打印一次:一帧最多检测 3 个目标,串口只报最优的一个;整帧没扫到目标就打 frame x: no object。
按一下 SW2,切一档量程;按一下 SW3,扫描暂停,再按继续——都有对应的日志:
[00:00:21.000] <inf> final_radar_system: SW2: range switched to 600 mm (60 cm) [00:00:23.000] <inf> final_radar_system: SW3: scanning paused
主要参数

| 扫描范围 / 步进 | 0°~180°,1° 步进,18ms/步,端点停留 100ms |
| 测距范围 | 20mm – 4000mm(有效回波判定),超时 35ms 判无回波 |
| 距离换算 | d(mm) = 脉宽(µs) × 343 / 2000(343m/s,往返除 2) |
| 量程档位 | 30 / 60 / 100 / 200 cm,SW2 循环切换,默认 1m |
| 舵机驱动 | 50Hz PWM,500–2500µs ↔ 0–180° 线性映射 |
| 屏幕 | ST7735,160×128,RGB565,SPI 8MHz(inversion-on) |
| 雷达画面 | 半径 78px 半圆 + 3 档环(26/52/78px)+ 7 根辐条,黑底 |
| 遮挡显示 | 物体→量程边缘红色径向线,按角度持久保留,重扫更新 |
| Flash / RAM 占用 | 69.6 KB / 128 KB(54%)· 16.3 KB / 24 KB(68%) |
| 串口 | 115200 8-N-1;1s 打角度/距离,5s 打温度 |
| OS | Zephyr v4.4.0,3 个应用线程(main 优先级 0,servo/sonar 优先级 5)+ 2 组 GPIO 中断(Echo 双边沿 / SW2+SW3) |
RAM 怎么压下来的值得说一句:160×128×2 = 40KB 的显存副本存不起(SRAM 总共 24KB 可用),所以雷达底图不存位图,每次擦除时用解析几何现场判断"这个像素是不是网格"(圆环看平方距离、辐条看叉积+点积),用查表法(Q15 定点三角表)代替 sinf()。
二、原理与系统框图

原理和蝙蝠回声定位一模一样,三步就能讲清楚:
第一步,测一个点的距离。 HC-SR04 的用法是:主控给 Trig 引脚一个 10µs 以上的高脉冲,模块就发出 8 个 40kHz 超声波脉冲,然后把 Echo 引脚拉高;声波撞到物体弹回来被接收到,Echo 再拉低。Echo 高电平的持续时间,就是声波往返的时间。声速约 343 m/s,往返要除以 2,换成微秒和毫米就是:
距离(mm) = 脉宽(µs) × 343 / 2000
举个例:测到 Echo 脉宽 1000µs,距离就是 171.5mm,约 17cm。
第二步,把一个点变成一条扫描线。 单个探头只能测正前方一条线上的距离。把它架在舵机上,每转 1° 测一次,181 步扫完半个平面,就得到 181 个「角度 + 距离」的数据对——这就是最原始的极坐标扫描雷达。
第三步,把数据画成画面。 屏幕是直角坐标,极坐标换算像素只差一个三角函数:x = 圆心x − r·cosθ,y = 圆心y − r·sinθ。哪个角度有回波,就在对应方位、对应半径上点亮像素。加上网格和转动的扫描线,就是雷达扇面图了。
真正的工程量在于:怎么让"转、测、画"三件事在一颗只有 32KB SRAM 的 MCU 上互不打架地跑起来。
整个系统就是"一块开发板 + 三个外设",软件侧只有 3 条应用线程 + 2 组 GPIO 中断(Echo 双边沿一组;SW2/SW3 按键一组,50ms 防抖跑在 Zephyr 系统工作队列上,不占应用线程):

几条线值得单独说:
Trig 是 GPIO 输出,Echo 是双边沿中断输入:HC-SR04 的 Echo 高电平脉宽就是声波往返时间,用 k_cycle_get_32() 硬件周期计数在上升沿/下降沿各记一次时标,换算距离。回波脉宽最长 23ms(4m 量程),所以等回波的超时设了 35ms。
"转一步、测一次"用信号量串起来:舵机线程每转 1° 发一个信号量,超声波线程被唤醒才触发测距——保证每个样本都对应一个确定的机械角度,不会"边转边测"引入误差。
样本走消息队列进 main:采样永远不画面,画面永远不等采样,两边互不拖累。
硬件平台一句话回顾:FRDM-MCXA153 搭载 NXP MCXA153(Arm® Cortex®-M33,96 MHz),128 KB Flash / 32 KB SRAM,板载 MCU-Link 调试器和 P3T1755DP 温度传感器。

| MCU 型号 | MCXA153VLH (Arm® Cortex®-M33) |
| 主频 | 96 MHz |
| Flash / SRAM | 128 KB Flash, 32 KB SRAM (含 8KB ECC) |
| 调试器 | 板载 MCU-Link (CMSIS-DAP / J-Link 固件) |
| 板载传感器 | P3T1755DPJ I3C/I2C 温度传感器 |
板子只有 32 KB SRAM(含 8KB ECC,实际可用 24 KB),对 Zephyr 来说相当紧凑——但这个项目证明了它够用:显存副本不存、三角函数查表,最终 16.3 KB 搞定,怎么压的见第五章软件设计。
三、硬件清单与接线
1. 物料清单
| FRDM-MCXA153 | NXP 官方开发板 | 1 | 本次活动开发板 |
| 舵机 | MG90S,180° | 1 | 独立 5V 供电 |
| 超声波模块 | HC-SR04 | 1 | 建议换 3.3V 兼容的 HC-SR04P |
| TFT 屏 | 1.8" ST7735,160×128,SPI | 1 | 8MHz SPI |
| 5V 电源 | 舵机独立供电 | 1 | 必须与板子共地 |
| 杜邦线、舵机支架 | — | 若干 | 支架把探头架离桌面 |
2. 接线总表
| 舵机 PWM | P3_9 | MG90S 信号线(橙) | FlexPWM0,50Hz,500–2500µs ↔ 0–180° |
| 舵机电源 | — | MG90S 红/棕 | 独立 5V 供电,必须与板子共地 |
| Trig | P3_28 | HC-SR04 Trig | GPIO 输出,12µs 高脉冲触发 |
| Echo | P3_27 | HC-SR04 Echo | GPIO 输入+下拉,5V 电平必须分压 |
| HC-SR04 电源 | — | VCC/GND | 5V;建议用 3.3V 兼容的 HC-SR04P |
| SPI SCK | P1_1 | 屏 SCK | LPSPI0 板卡默认引脚 |
| SPI MOSI | P1_0 | 屏 SDA | |
| SPI MISO | P1_2 | 字库 SO(暂未使用) | |
| 共享 CS | P1_3 | 屏 CS | 低=屏 |
| 屏 DC | P3_30 | 屏 DC | 数据/命令选择 |
| 屏背光 | P3_1 | 屏 BL | 高电平点亮 |
| 屏电源 | — | VCC/GND | 3.3V |
| SW3 | P1_7(板载) | — | 暂停/继续扫描 |
| SW2 | P3_29(板载) | — | 循环切换量程 |
| 温度 | 板载 P3T1755 | — | I3C,地址 0x48,免接线 |
实际接好的样子(彩虹排针是屏,紫灰两根是超声波,舵机单独一组线):

3. 供电的两个血泪提醒
舵机必须独立 5V:MG90S 启动/堵转电流能把 MCU 直接拉复位。我第一版把舵机插板子的 5V 排针上,程序跑到一半就"重启",查了半天软件,最后发现是电源——共地!独立供电!
HC-SR04 的 Echo 是 5V:MCXA153 的 GPIO 不耐 5V。要么用电阻分压(10k+10k),要么直接买 3.3V 兼容的 HC-SR04P。别学我侥幸直连(我的板子 SWD 调试器后来疑似就是这类原因挂的)。
四、Zephyr 设备树:和 FreeRTOS 不一样的开发方式
这个项目是用 Zephyr RTOS 开发的。如果你是从 STM32 + FreeRTOS(或者裸机)过来的,第一个要适应的差异就是:Zephyr 里"硬件长什么样"不归 C 代码管,归设备树(Devicetree)管。
1. 思路差异:硬件描述与代码分离
FreeRTOS 项目里,硬件配置通常散落在代码中:引脚号是宏定义、GPIO 初始化是 GPIO_Init()、SPI 参数在某个 bsp_xxx.c 里。换一块板子,得翻遍工程改一堆 .c/.h。
Zephyr 把这件事拆成了三层:
| 设备树 | boards/frdm_mcxa153.overlay | 硬件长什么样:哪个引脚接什么、什么电平有效、SPI 跑多快 |
| Kconfig | prj.conf | 要哪些功能:编不编 GPIO/PWM/SPI/显示驱动、日志怎么打 |
| C 代码 | src/*.c | 只写逻辑,通过 DT_ALIAS() 等宏"按名字"取硬件描述 |
好处是改硬件不改代码:换引脚、换屏幕、甚至换一块 Zephyr 支持的板子,理论上只需要重写 overlay。下面把本项目的 overlay 拆开讲一遍。
2. 别名
应用代码要用的引脚,全部在 overlay 里起好名字:
/ {
aliases {
sonar-trig = &sonar_trig_pin;
sonar-echo = &sonar_echo_pin;
servo-pwm = &servo_node;
lcd-backlight = &lcd_blk_pin;
font-cs = &font_cs_pin;
/* Product control is the board's SW3, named user_button_3 in the BSP. */
sw0 = &user_button_3;
/* SW2 doubles as the ISP entry button; safe to sample as GPIO at runtime. */
range-btn = &user_button_2;
};
app_gpio_pins {
compatible = "gpio-leds";
sonar_trig_pin: sonar_trig {
gpios = <&gpio3 28 GPIO_ACTIVE_HIGH>;
label = "HC-SR04 Trig Pin";
};
sonar_echo_pin: sonar_echo {
gpios = <&gpio3 27 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>;
label = "HC-SR04 Echo Pin";
};
...
};
servo_dev {
compatible = "pwm-leds";
servo_node: servo_0 {
pwms = <&flexpwm0_pwm1 1 20000000 0>; /* 50Hz, 20ms, PWM0_B1_P3_9 */
};
};
};gpio-leds / pwm-leds :Trig、Echo 这些引脚既不是 LED 也不是按键,但 Zephyr 设备树里最简单的"声明一个普通 GPIO"的办法就是挂在 gpio-leds 兼容节点下——反正应用代码只通过别名取 gpios 属性,不会真的去跑 LED 驱动。舵机同理,借 pwm-leds 声明一路 50Hz 的 PWM。
别名就是 C 代码的"入口"。C 里这样拿硬件:
/* main.c:全部通过别名取设备树描述,代码里没有一个硬编码引脚号 */ static const struct gpio_dt_spec trig_spec = GPIO_DT_SPEC_GET(DT_ALIAS(sonar_trig), gpios); static const struct gpio_dt_spec echo_spec = GPIO_DT_SPEC_GET(DT_ALIAS(sonar_echo), gpios); static const struct pwm_dt_spec servo_spec = PWM_DT_SPEC_GET(DT_ALIAS(servo_pwm)); static const struct gpio_dt_spec btn0_spec = GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios);
如果哪天 Trig 从 P3_28 换到别的脚,只改 overlay 里的 &gpio3 28,C 代码一行不动。
3. pinctrl:引脚复用
MCXA153 的每个引脚都有好几种功能,舵机 PWM 用的 P3_9 要显式复用成 FlexPWM0 的 B1 通道:
&pinctrl {
pinmux_flexpwm0_pwm1: pinmux_flexpwm0_pwm1 {
group0 {
pinmux = <PWM0_B1_P3_9>;
slew-rate = "fast";
drive-strength = "low";
};
};
};
&flexpwm0_pwm1 {
status = "okay";
pinctrl-0 = <&pinmux_flexpwm0_pwm1>;
pinctrl-names = "default";
};PWM0_B1_P3_9 这类枚举值来自 NXP HAL 提供的 MCXA153VLH-pinctrl.h, overlay 开头 #include 进来就能用——不用去查手册算寄存器值。
4. 显示屏节点:参数全在设备树里
ST7735 这块屏,Zephyr 自带驱动(sitronix,st7735r),所以不用写一行驱动代码,只要在 overlay 里把屏的"身份证"填全:分辨率、偏移、像素格式、伽马表、初始化寄存器……驱动启动时照着配置初始化:
mipi_dbi {
compatible = "zephyr,mipi-dbi-spi";
spi-dev = <&lpspi0>;
dc-gpios = <&gpio3 30 GPIO_ACTIVE_HIGH>;
st7735: st7735r@0 {
compatible = "sitronix,st7735r";
mipi-max-frequency = <8000000>; /* 杜邦线 15MHz 边沿振铃会花屏,8MHz 足够 */
width = <160>;
height = <128>;
madctl = <0x60>;
colmod = <0x55>;
inversion-on; /* 这块 1.8" 屏需要反显,缺省 INV_OFF 会整屏花/发白 */
/* 电源/伽马寄存器表略,照屏厂初始化序列填 */
};
};&lpspi0 {
status = "okay";
cs-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
};设备树只负责把事实描述清楚,时序配合在驱动代码里。
最后,chosen { zephyr, display = &st7735; }; 把这个节点指定为系统默认显示屏,应用代码用 DEVICE_DT_GET(DT_CHOSEN(zephyr_display)) 拿到设备句柄,调 Zephyr 统一的 display API 写像素——至于是 ST7735 还是别的什么屏,应用根本不关心。
5. prj.conf:要哪些功能,一句话一行
设备树描述硬件,Kconfig 决定哪些子系统编进固件。本项目的 prj.conf 一共 16 行,每行一句话:
| CONFIG_GPIO=y / PWM / SPI | 三类外设驱动 |
| CONFIG_DISPLAY=y + CONFIG_ST7735R=y | 显示子系统 + ST7735 驱动 |
| CONFIG_SENSOR=y + CONFIG_P3T1755=y + CONFIG_I3C=y | 传感器子系统 + 板载温度传感器(I3C 总线) |
| CONFIG_LOG=y + CONFIG_LOG_MODE_IMMEDIATE=y | 日志;立即模式(血泪教训,见踩坑章坑 3) |
| CONFIG_CBPRINTF_FP_SUPPORT=y | 日志里能打印 %f 浮点 |
| CONFIG_MAIN_STACK_SIZE=1536 | 主线程栈,要跑绘图和算法,默认 1024 不够 |
| CONFIG_FPU=y | 使能硬件 FPU(目标宽度要用 cosf/sqrtf) |
| CONFIG_HEAP_MEM_POOL_SIZE=2048 | 内核堆压到 2KB——32KB SRAM 的紧箍咒 |
| CONFIG_NEWLIB_LIBC=n | 不用 newlib,省 Flash/RAM |
6. 小结
FreeRTOS 的思路是"代码适配板子",Zephyr 的思路是"板子被描述,代码读描述"。这个项目全部硬件相关的东西——4 个 GPIO、1 路 PWM、1 块 SPI 屏、2 个按键——都浓缩在一个 100 行的 overlay 里,C 代码里没有一个引脚号。想把这个雷达移植到另一块 Zephyr 支持的板子?重写 overlay 就行了。
五、软件设计
成果是一帧帧雷达画面,但软件的核心其实是把"转、测、画"三条时间线解耦——舵机转它的,超声波测它的,屏幕画它的,谁也别等谁。这一章用流程图和代码把 3 条应用线程 + 2 组 GPIO 中断捋一遍。
1. 三条线程怎么配合:信号量 + 消息队列
这一节回答"三个线程谁等谁":舵机线程是节拍器,超声波线程是生产者,main 线程是消费者。

ps:其实我还让claude写了聚类算法,用来判断遮挡物的长度,但是这段时间比较忙,没有调试,所以不做过多说明
舵机线程每转一步只干两件事:更新角度、k_sem_give(&sem_servo_ready);超声波线程被信号量唤醒后才触发测距——每个样本都对应一个确定的机械角度,不会"边转边测"引入误差:
/* sonar_thread:被信号量驱动,测一个样本塞一个样本 */
while (true) {
(void)k_sem_take(&sem_servo_ready, K_FOREVER);
struct radar_sample_t sample = {
.angle = SERVO_ANGLE_REVERSED ? (uint8_t)(180 - g_current_angle) :
(uint8_t)g_current_angle,
.dist_mm = (int16_t)sonar_sample_distance_mm(),
.frame_id = g_current_frame_id,
.is_frame_end = (g_current_angle == 180),
};
put_latest_sample(&sample);
}消息队列满的时候,采样线程的做法是丢掉最旧的、塞进最新的——宁可画面丢点,也不让采样被刷屏卡住(屏幕 SPI 一笔一画很慢):
/* put_latest_sample:队列满时丢旧保新,生产者永不阻塞 */
static void put_latest_sample(const struct radar_sample_t *sample)
{
if (k_msgq_put(&radar_msgq, sample, K_NO_WAIT) == 0) {
return;
}
struct radar_sample_t discarded;
(void)k_msgq_get(&radar_msgq, &discarded, K_NO_WAIT);
if (k_msgq_put(&radar_msgq, sample, K_NO_WAIT) != 0) {
LOG_WRN("Radar queue full; sample at %u deg dropped", sample->angle);
}
}2. 测距状态机:GPIO 双边沿中断捕获回波
这一节回答"一个距离值是怎么测出来的":一次触发、两个边沿、一个超时。
HC-SR04 的 Echo 高电平脉宽就是声波往返时间。Echo 配成双边沿中断,上升沿/下降沿各记一次时标:
/* Echo 双边沿回调:echo_armed 屏蔽测量窗口之外的杂散边沿 */
static void echo_gpio_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
ARG_UNUSED(dev); ARG_UNUSED(cb); ARG_UNUSED(pins);
if (!echo_armed) {
return;
}
int val = gpio_pin_get_dt(&echo_spec);
if (val < 0) {
return;
}
if (val > 0) {
t_rise = k_cycle_get_32();
} else if (echo_flag == false) {
t_fall = k_cycle_get_32();
echo_flag = true;
echo_armed = false;
k_sem_give(&sem_echo_done);
}
}一次完整测量的流程:

换算核心只有两行,但藏着两个细节——32 位计数器回绕处理,以及"343 m/s、往返除 2"为什么恰好是 ÷2000:
/* 时标差(处理 32 位回绕)→ 微秒 → 毫米 */ uint32_t cycles = (t_fall >= t_rise) ? (t_fall - t_rise) : ((UINT32_MAX - t_rise) + t_fall + 1U); uint32_t us = (uint32_t)k_cyc_to_us_floor64(cycles); int32_t dist_mm = (int32_t)((us * 343ULL) / 2000ULL); return (dist_mm >= 20 && dist_mm <= 4000) ? dist_mm : -1;
3. 无显存的雷达画面:Q15 查表 + 解析几何
160×128 RGB565 的显存副本要 40KB,而 SRAM 总共 32KB——存不起位图。两个省法:
画不用算:0°~180° 的 sin/cos 做成 Q15 定点查表(sin_lut_q15[181] / cos_lut_q15[181]),极坐标转像素全程整数运算,没有一次 sinf();
擦不用存:擦扫描线时要还原底下的网格,但不存底图——用解析几何现场判断"这个像素是不是网格线",圆环看平方距离、辐条看叉积+点积:
/* 极坐标 → 像素:纯整数 + Q15 查表 */
static void spoke_segment(int16_t r_from, int16_t r_to, uint8_t angle, uint16_t color)
{
for (int16_t r = r_from; r <= r_to; r++) {
int16_t x = RADAR_CENTER_X - (int16_t)(((int32_t)r * cos_lut_q15[angle]) >> 15);
int16_t y = RADAR_CENTER_Y - (int16_t)(((int32_t)r * sin_lut_q15[angle]) >> 15);
gui_radar_draw_pixel(x, y, color);
}
}/* static_pixel_is_set:判断像素是否属于静态网格(圆环/辐条),用于擦除恢复 */ int32_t r2 = dx * dx + dy * dy; /* 圆环:到圆心的平方距离 */ ... int32_t cross = dy * cos_lut_q15[a] - dx * sin_lut_q15[a]; /* 辐条:垂直距离 */ int32_t dot = -(dx * cos_lut_q15[a] + dy * sin_lut_q15[a]); /* + 投影范围 */
"红色遮挡线"的余辉效果则是 shadow_dist_mm[181] 数组的功劳:每个角度记最近一次遮挡距离,扫到就擦旧画新,没回波就擦掉——十几行代码,效果见演示视频。
六、软件调试:三个让我睡不好的坑
坑 1:SWD 调试器离奇死亡,串口 ISP 救命
现象:开发到一半,west flash 突然怎么都连不上目标核:pyOCD 报 libusb claim 失败,装了 LinkServer 报 Could not connect to core。定位:换一台电脑、换 USB 口、connect-under-reset 全试过,最后同型号的另一块板子一插就通——实锤是调试器/目标侧的硬件问题,软件无解。根因:至今怀疑是 5V Echo 直灌 MCU 引脚的后遗症。解法:绝境之下发现 FRDM-MCXA153 支持串口 ISP:按住 SW2 复位就进 ROM 里的 ISP bootloader,通过 USB 虚拟串口直接烧 bin 文件。虽然不能在线调试了,但烧录运行完全不受影响,项目得以继续。这也解释了为什么后面所有验证都是"串口日志 + 眼睛看屏"的土办法。
教训:5V 信号不要侥幸直连 3.3V 的 MCU 引脚;烧不进去先怀疑硬件,换一块板子是最快的二分法。
坑 2:屏幕花屏,三轮才抓完凶手
现象:5_1 刷屏正常了,最终工程却还是花;花屏治好后颜色又反了(黑底变白底)。定位:用 8_radar_gui_engine 做二分(有逐像素绘图、无字库、无舵机),加上"每 5 秒一次的 ROM 读事务是唯一变量"的推理,把字库整个停掉——不花了。根因:其实是三个凶手叠加——① Zephyr 的 st7735r 驱动不写 inversion-on 属性时默认发 INV_OFF(关反显),而这块屏恰恰必须开反显,不开就是整屏噪点;② 字库 ROM 读期间 P1_3 手动翻转 + SPI 控制器在两种配置间来回重配,时序上是最脏的干扰源;③ inversion-on 是硬件像素取反,不是"治花屏的开关",开了它颜色就得在软件里找补。解法:overlay 加一行 inversion-on;;字库停用(font_chip.c 保留在源码树里但不编入构建,屏上只画雷达图);软件写屏前统一取反补偿,颜色宏保持直觉值,一处代码全搞定。
教训:遇到玄学问题就搭最小复现工程做二分;每次只改一个变量,变量就是嫌疑人。
坑 3:日志"静默消失"
现象:整合后发现串口只有零星温度日志,每秒的角度/距离全丢了——没有报错,没有告警。定位:Zephyr 日志默认是"缓冲 + 后台线程打印",主线程被逐像素 SPI 刷屏占满后,后台线程抢不到 CPU,日志缓冲一满新消息就被静默丢弃。根因:不是没打日志,是日志没机会被打印出来。解法:改 CONFIG_LOG_MODE_IMMEDIATE=y(调用处直接打印)后一条不丢。这个教训写进了工程文档的"常见坑"第一条。
教训:日志丢了不一定是没打,可能是没机会打;调试期先把 LOG_MODE_IMMEDIATE 打开。
顺带的发现:SW2 还能这么用
调试坑 1 的时候发现 SW2 是 ISP 按钮,一度以为它废了。后来读板级设备树发现它就是个普通 GPIO(P3_29,低有效),ISP 功能只在复位采样那一下——运行中当按键随便用。于是量程切换就有了着落。按住 SW2 复位还是会进 ISP,那属于硬件行为,程序管不着。
七、结语与展望
先说说还可以做什么:
遮挡线随时间衰减变暗(真雷达的余辉感更强);
逐像素写屏改成按行批量 display_write,帧率能快好几倍;
距离中值滤波,进一步压跳变;
量程档位存入 Flash,掉电记忆。
从开箱到雷达转起来,前前后后差不多两三周,实际上我只写了外设驱动,业务代码是用Claude Code写的,甚至这篇帖子大部分也是AI写的,我只做了部分勘误和调整。vibe coding越用越爽,越爽越用,一入AI深似海,从此Token是路人。但开发依旧没有一帆风顺,真正写功能代码的时间可能不到一半——另一半都花在"屏幕为什么是花的""日志为什么没了""调试器为什么连不上"上,尤其是调试器连不上的问题,我一开始还以为是我环境出问题了,查了半天都没查出来,实在没办法了,用串口烧录程序了。嵌入式就是这样,天坑不填完,功能不露面。好在每填一个坑都记了档,这篇帖子和工程文档里的"常见坑速查表"就是这次的全部学费,吃一堑长一智。
回头看这个系列的三篇,刚好是一条完整的学习路径:点灯建立信心,外设逐个击破攒零件,最后把零件攒成一个能跑的系统。如果你也是 Zephyr 新手,希望这条路径和这些坑能帮你少走点弯路。
如果有机会,我打算写写 Zephyr 的入门笔记,其实Github上也有个Zephyr rtos的中文教程(X-Gen-Lab/zephyr-learning-system),教程很详细。但是这个里面版本比较久并且有些小错误,还是要多多参考官方文档。
特别鸣谢:
感谢立创开源硬件平台博主 __Aknice——本项目测试中使用的雷达挡板来自他的开源设计的PCB,欢迎大家关注他的主页和项目;
感谢 DigiKey 的活动和 NXP 的这块板子(差点被我干碎了T_T);
感谢 Zephyr 社区,设备树和驱动模型省掉的驱动开发量。
我要赚赏金
