一、项目概述

本项目基于 EEPW 论坛「Let's Do」2026 年第 2 期活动,完成了简易超声波测距仪训练营的全部基础任务与部分扩展任务。
项目基于 Zephyr RTOS,运行在 FRDM-MCXA153 开发板上,采用三线程并行架构,将超声波采样、LCD 显示、OLED 渲染完全解耦,互不阻塞:
项目基于 Zephyr RTOS,运行在 FRDM-MCXA153 开发板上,采用三线程并行架构,将超声波采样、LCD 显示、OLED 渲染完全解耦,互不阻塞:
HC-SR04 超声波测距(Zephyr sensor 子系统驱动,GPIO 触发+回波计时)
UART 串口实时上传(printk 输出原始值与滤波后数据,供上位机监视)
LCD1602 数字显示(PCF8574T I2C 背板,软件 4-bit 时序驱动,支持小数位显示)
双级数据滤波(中值滤波去毛刺 + 自适应 EMA 平滑,兼顾抗干扰与响应速度)
Class2 Unit OLED HUD(U8g2 图形库移植,大号数字 + 雷达扫描动画 + 实时曲线面积图)
与常规"轮询延时"写法不同,本项目完全基于 Zephyr 多线程 + 消息队列实现模块解耦:传感器线程只管采样与滤波,通过两个独立 k_msgq 分别广播给 LCD 线程和 OLED 线程,彼此互不干扰、互不阻塞。
二、硬件连接
2.1 接线总览
本项目硬件构成较简单,核心是 LPI2C0 总线复用——LCD1602(经 PCF8574T 背板)与 Class2 Unit OLED(SSD1306 驱动)挂载在同一条 I2C 总线上,通过不同的从机地址区分;HC-SR04 则是纯 GPIO 器件,不占用 I2C 资源。

关键细节:HC-SR04 的 ECHO 引脚输出的是 5V 电平的高电平脉冲宽度信号,而 FRDM-MCXA153 的 GPIO 是 3.3V 逻辑,实际接线中务必确认分压或电平转换,避免损坏 MCU 引脚。
2.2 引脚对应表
功能 | FRDM-MCXA153 引脚 | 外设 | 备注 |
TRIG | ARD_D2 (gpio2 pin4) | HC-SR04 | 输出,触发 10μs 高电平脉冲 |
ECHO | ARD_D3 (gpio3 pin0) | HC-SR04 | 输入,脉宽正比于往返时间 |
I2C | LPI2C0 (100kHz) | PCF8574T @0x27 | LCD1602 背板,软件驱动 |
I2C | LPI2C0 (同总线) | SSD1306 @0x3C | Class2 Unit OLED,U8g2 驱动 |
UART | Console UART | PC 上位机 | printk 实时打印测距数据 |
本项目未使用 Zephyr display/LVGL 子系统,OLED 完全通过自行移植的 U8g2 库 + 裸机 I2C API 驱动,最大化压缩 Flash 占用。
三、软件架构
3.1 整体架构图

3.2 关键模块说明
模块 | 函数/变量 | 功能描述 |
传感器采样 | sensor_thread_fn() | 200ms 周期采样,-EIO 判定超量程,连续确认去抖 |
数据滤波 | dist_filter_apply() | 5点中值滤波 + EMA 平滑,定点数学实现 |
中位数求解 | median_of_n() | 插入排序求中位数,窗口极小无需额外内存 |
LCD 驱动 | lcd_i2c_*() | 移植自 marcoschwartz LiquidCrystal_I2C,4-bit 时序 |
OLED 移植层 | u8x8_byte_zephyr_hw_i2c() | U8g2 → Zephyr I2C API 回调转译 |
OLED 渲染 | oled_thread_fn() | 50ms 刷新,帧间插值 + 曲线填充绘制 |
四、核心功能实现
4.1 UART 串口通信
Zephyr 的 printk 默认经由 Console UART 输出,无需额外配置驱动,prj.conf 中启用即可:
CONFIG_SERIAL=y CONFIG_CONSOLE=y CONFIG_UART_CONSOLE=y
传感器线程每次采样后,无论原始值还是滤波后数据都会实时打印,方便在上位机侧同步观察滤波效果:
printk("[#%04u] raw=%d.%d cm -> filt=%d.%d cm\n",
seq, (int)raw_cm_int, (int)raw_cm_frac1,
(int)msg.cm_int, (int)msg.cm_frac1);超量程时同样会明确打印状态,避免上位机端产生歧义:
if (oor_counter >= OOR_CONFIRM_COUNT || !last_valid) {
msg.out_of_range = true;
printk("[#%04u] OUT OF RANGE (confirmed)\n", seq);
}4.2 超声波测距原理与数据换算
HC-SR04 的测距原理基于声波往返飞行时间(TOF):MCU 通过 TRIG 引脚发出 10μs 高电平脉冲触发测量,模块随即发射 8 个 40kHz 超声波脉冲并等待回波,ECHO 引脚随即输出与"发射到接收"时间成正比的高电平脉宽。
本项目直接复用 Zephyr sensor 子系统的标准 API 完成采样,MCU 侧只需处理换算好的距离通道值:
int ret = sensor_sample_fetch(hcsr04_dev); struct sensor_value dist; sensor_channel_get(hcsr04_dev, SENSOR_CHAN_DISTANCE, &dist); /* Zephyr sensor_value 为定点结构:val1(整数部分) + val2(微单位小数部分) */ int32_t cm_int = dist.val1 * 100 + dist.val2 / 10000; int32_t cm_frac1 = (dist.val2 % 10000) / 1000;
驱动返回 -EIO 时表示本次未收到有效回波(可能超出量程或发生斜面镜面反射漏检),项目对此做了连续确认去抖处理,避免偶发单次丢波导致显示画面剧烈闪烁:
if (oor_counter >= OOR_CONFIRM_COUNT || !last_valid) {
msg.out_of_range = true; /* 连续多次无回波,才确认为真正超量程 */
} else {
msg.cm_int = last_cm_int; /* 偶发丢波:沿用上一次有效值 */
}4.3 双级滤波算法:中值滤波 + 自适应 EMA
超声波测距的"毛刺"通常表现为偶发的单点跳变(多次反射、边缘散射导致某次读数远偏离真实值),而非持续性高频噪声。这类脉冲型异常值有个特性:只要窗口内多数样本仍正常,离群值排序后必然落在两端,不会被选为中位数——这正是中值滤波天然克制毛刺的原理。
static int32_t median_of_n(const int32_t *vals, uint8_t n)
{
int32_t tmp[MEDIAN_WINDOW];
memcpy(tmp, vals, sizeof(int32_t) * n);
for (uint8_t i = 1; i < n; i++) {
int32_t key = tmp[i];
int8_t j = (int8_t)i - 1;
while (j >= 0 && tmp[j] > key) {
tmp[j + 1] = tmp[j];
j--;
}
tmp[j + 1] = key;
}
return tmp[n / 2];
}中值滤波之后,再叠加一级自适应 EMA(指数滑动平均)做趋势平滑,兼顾"去噪声"和"跟得上真实变化"这对矛盾需求:小幅波动用慢速系数压制残留抖动,大幅跳变(真实距离快速变化)自动切换快速系数紧跟:
int32_t diff = median_val - f->ema_x10;
int32_t abs_diff = (diff < 0) ? -diff : diff;
if (abs_diff > ADAPTIVE_THRESHOLD_X10) {
/* 判定为真实变化:快速系数,几乎立即跟上 */
f->ema_x10 += (EMA_ALPHA_FAST_NUM * diff) / EMA_ALPHA_FAST_DEN;
} else {
/* 判定为噪声抖动:慢速系数,持续平滑 */
f->ema_x10 += (EMA_ALPHA_SLOW_NUM * diff) / EMA_ALPHA_SLOW_DEN;
}全程定点数学:滤波器内部统一使用"十分之一厘米"为最小单位的 int32_t 定点数运算,未引入任何浮点/libm 依赖,在 Cortex-M(无 FPU)平台上运行开销极低。
4.4 LCD1602 显示驱动(含小数位)
LCD 驱动完整移植自经典的 marcoschwartz/LiquidCrystal_I2C 时序逻辑,通过 PCF8574T 的 8 个 GPIO 扩展位模拟 HD44780 的 4-bit 并行接口:
static void write4bits(lcd_i2c_t *lcd, uint8_t nibble)
{
uint8_t data = (nibble & 0x0F) << 4;
data |= lcd->backlight_val;
pcf8574_write(lcd, data);
en_pulse(lcd, data); /* EN 引脚拉高保持>450ns,再拉低 */
}显示内容含小数位的格式化,直接利用滤波器输出的"整数+一位小数"定点结构拼接字符串:
static void format_distance(char *buf, bool out_of_range,
int32_t cm_int, int32_t cm_frac1)
{
if (out_of_range) {
snprintf(tmp, sizeof(tmp), "Dist: OUT RANGE");
} else {
snprintf(tmp, sizeof(tmp), "Dist:%6d.%1d cm",
(int)cm_int, (int)cm_frac1);
}
}4.5 OLED HUD:U8g2 移植与UI渲染
Class2 Unit OLED(SSD1306 驱动)通过完整移植 U8g2 图形库实现,仅需实现两个平台回调即可接入 U8g2 生态:
uint8_t u8x8_byte_zephyr_hw_i2c(u8x8_t *u8x8, uint8_t msg,
uint8_t arg_int, void *arg_ptr)
{
switch (msg) {
case U8X8_MSG_BYTE_SEND:
memcpy(&s_buf[s_buf_idx], arg_ptr, arg_int);
s_buf_idx += arg_int;
break;
case U8X8_MSG_BYTE_END_TRANSFER:
i2c_write(s_i2c_dev, s_buf, s_buf_idx, s_i2c_addr);
break;
/* ... */
}
}在此基础上设计了"数字仪表盘+雷达扫描+曲线填充"HUD 界面:右上角同心圆配合 8 方向查表旋转扫描线,完全避开浮点三角函数:
static const int8_t radar_dx[8] = { 10, 7, 0, -7, -10, -7, 0, 7 };
static const int8_t radar_dy[8] = { 0, 7, 10, 7, 0, -7, -10, -7 };曲线部分采用帧间线性插值(Tween)技术:传感器每 200ms 更新一次,OLED 每 50ms 刷新一帧,中间用插值平滑过渡,配合相邻采样点顶部连线,呈现出"曲线下方填充"而非生硬阶梯的视觉效果:
disp_x10 = prev_x10 + (int32_t)(((int64_t)(target_x10 - prev_x10) * tween_step) / TWEEN_STEPS);
五、基础任务验证
5.1 串口数据上传
通过 west build && west flash 烧录后打开串口终端,可实时看到每 200ms 一条的测距日志,格式清晰、原始值与滤波值同屏对照:
[#0102] raw=23.4 cm -> filt=23.1 cm [#0103] raw=41.7 cm -> filt=23.6 cm ← 毛刺已被滤除,未直接体现在输出 [#0104] raw=22.9 cm -> filt=23.2 cm
5.2 超声波数据读取
Zephyr sensor 子系统封装了 HC-SR04 的 GPIO 触发时序与回波计时逻辑,应用层调用标准 sensor_sample_fetch() + sensor_channel_get() 即可获得距离通道值,验证了 Zephyr 传感器抽象层在实际硬件上的可用性。
5.3 LCD 数字显示
LCD1602 第一行固定显示标题,第二行实时刷新距离数值,超量程时明确提示 OUT RANGE,避免误读为 0 或异常小数:
状态 | 第一行 | 第二行 |
正常测距 | Dist. Meter | Dist: 23.4 cm |
超出量程 | Dist. Meter | Dist: OUT RANGE |
六、扩展任务实现
6.1 小数位显示
滤波器全程以"十分之一厘米"为最小单位运算(cm_int + cm_frac1 定点组合),LCD 与 OLED 两块屏幕均直接显示一位小数,精度达到 0.1cm,满足训练营扩展任务要求。
6.2 数据滤波(重点扩展任务)
这是本项目投入精力最多的扩展环节,采用**"中值滤波去毛刺 + 自适应 EMA 平滑趋势"**的组合方案:
滤波级 | 作用 | 关键参数 |
中值滤波 | 剔除单点脉冲型毛刺 | MEDIAN_WINDOW=5 |
自适应 EMA | 平滑抖动,同时不牺牲对真实变化的响应 | 慢速α=0.3 / 快速α=0.8,阈值3cm |
超量程去抖 | 避免偶发丢波导致显示闪烁 | 连续3次确认 |
相比单一滑动平均"一刀切"的处理方式,自适应机制能在同一套参数下同时兼顾"稳"与"快":小幅噪声被慢速系数持续压制,而手部靠近/远离这类真实位移变化能被快速系数几乎实时捕捉,不会出现"数值追不上手速"的迟钝感。
七、OLED HUD 设计(附加创意实现)

在完成基本训练营任务之外,本项目额外investment在 OLED 视觉效果上,力求做出"仪表盘/雷达 HUD"质感:
┌────────────────────────────────────┐ │ 23.4cm ╭─╮ │ ← logisoso18_tn大号数字 + 雷达动画 │ ╰─╯ │ │────────────────────────────────── │ │ ····························· │ ← 点阵虚线参考网格 │ ╱‾╲___╱‾‾╲___╱‾╲___ │ ← 曲线填充实时历史图 └────────────────────────────────────┘
核心技巧:
纯定点数学:雷达旋转、曲线插值全程无浮点运算,Cortex-M0+/M33 无 FPU 平台也能流畅运行
环形缓冲区:历史数据用固定长度数组循环写入,避免动态内存分配
断点处理:超量程时曲线主动断开而非画到0,一眼区分"距离为0"与"未测到"
八、效果对比总结
环节 | 优化前 | 优化后 | 关键手段 |
原始数据 | 偶发毛刺跳变(±10cm量级) | 平滑连续变化 | 中值滤波剔除离群值 |
响应速度 | 纯慢速EMA时跟踪迟钝 | 真实变化几乎立即响应 | 自适应EMA双系数切换 |
超量程提示 | 单次丢波即闪烁 | 仅连续多次确认才提示 | 计数去抖 |
OLED曲线 | 阶梯状矩形柱 | 平滑曲线+下方填充 | 帧间插值+顶点连线 |
九、遇到的问题与解决
问题1:CMake 找不到 u8g2 源文件
现象:Cannot find source file: .../external/zephyr/modules/lib/u8g2/csrc/...。
原因:误将 modules/ 当作 ZEPHYR_BASE 的子目录拼接路径,而 zephyr-ide 实际把外部模块与 zephyr 仓库放在 workspace 下的平级目录。
解决:改用 west list u8g2 -f "{abspath}" 动态查询模块真实绝对路径,不再手工拼接猜测。
问题2:链接阶段报错缺失 kernel32/user32 等 Windows 库
现象:ARM 交叉编译链接时报错找不到一堆 Win32 系统库。
原因:build/ 目录残留了此前误配置为 native_sim(主机模拟器)时的 CMake 缓存,CMakeCache.txt 里仍保留 Windows 主机链接规则。
解决:彻底删除 build/ 目录后重新执行干净的 west build,问题即消失。
问题3:曲线抖动明显,毛刺影响观感
现象:单纯显示原始距离值时,OLED 曲线频繁出现突刺尖峰。
原因:超声波回波偶发多次反射导致的单点异常值未经处理直接进入渲染管线。
解决:引入中值滤波+自适应EMA双级滤波器,从数据源头解决问题,而非依赖显示层平滑。
十、总结与展望
本次活动完成了全部基础任务和2个扩展任务:
✅ MCU 通过 Console UART 实时上传原始与滤波后测距数据
✅ 基于 Zephyr sensor 子系统读取 HC-SR04 超声波数据
✅ LCD1602(PCF8574T)完整移植 4-bit I2C 时序驱动并显示数字
✅ 支持一位小数精度显示
✅ 实现中值滤波+自适应EMA双级滤波算法,兼顾抗毛刺与响应速度
✅ 额外完成 Class2 Unit OLED 的 U8g2 移植与 HUD 可视化设计
后续可以扩展的方向:
接入物理按键中断,动态切换测距量程与曲线纵轴分度
引入卡尔曼滤波替代当前的中值+EMA组合,进一步提升动态跟踪精度
增加蜂鸣器/LED,实现距离阈值报警功能
OLED 界面增加历史峰值标记与统计信息(最大/最小/平均值)
感谢 EEPW 论坛提供的这次实践机会!从 GPIO 时序驱动的 HC-SR04,到 I2C 软件模拟的 LCD1602,再到完整移植的 U8g2 图形库,这次训练营让我对 Zephyr RTOS 的多线程协作模型和嵌入式图形界面开发有了更系统的理解,期待下一期活动继续深入探索!
我要赚赏金
