本次实现:小数显示、五点滑动平均滤波,以及使用 SW2 切换 cm/m 显示模式。
1. 扩展功能的数据流
三个扩展功能都在主循环的测量结果上继续处理,不需要增加线程:
sensor_value | +--转换为毫米 raw_mm | +--五点滑动平均 filtered_mm | +--串口输出 raw_mm 和 filtered_mm | +--根据 g_long_range 格式化为 cm 或 m | +--刷新 SSD1306 OLED
程序始终使用毫米作为内部计算单位,只有准备显示时才转换成厘米或米。这样可以避免在滤波过程中反复进行浮点运算,也可以保留传感器驱动提供的毫米分辨率。
2. 距离数据转换
Zephyr 的传感器 API 使用 struct sensor_value 返回测量值:
struct sensor_value distance; sensor_channel_get(g_sensor, SENSOR_CHAN_DISTANCE, &distance);
其中 val1 是整数部分,val2 是百万分之一单位的小数部分。距离通道的单位是米,所以程序按下面的方法转换成毫米:
int32_t raw_mm = (int32_t)distance.val1 * 1000 + distance.val2 / 1000;
例如驱动返回 val1 = 0、val2 = 234000,计算结果为:
raw_mm = 0 x 1000 + 234000 / 1000 = 234 mm
使用 int32_t 保存毫米值,既足以覆盖 HC-SR04 的测量范围,又比直接使用浮点数更适合 MCU。
3. 不使用浮点数显示小数
3.1 厘米模式
1 cm 等于 10 mm,因此整数除法的商是厘米整数部分,余数是十分之一厘米:
snprintk(distance_line, sizeof(distance_line), "%ld.%ld cm", (long)(distance_mm / 10), (long)(distance_mm % 10));
当 distance_mm = 234 时:
234 / 10 = 23 234 % 10 = 4 显示结果 = 23.4 cm
这里不需要启用浮点格式化,也不需要使用 %f。snprintk() 同时传入目标数组大小,避免格式化结果越过 distance_line 的边界。
3.2 米模式
1 m 等于 1000 mm。米模式保留三位小数,正好可以继续表达毫米:
snprintk(distance_line, sizeof(distance_line), "%ld.%03ld m", (long)(distance_mm / 1000), (long)(distance_mm % 1000));
%03ld 表示余数不足三位时在前面补零,因此 234 mm 会显示为 0.234 m,1234 mm 会显示为 1.234 m。
OLED 上出现小数只代表显示分辨率,不表示 HC-SR04 在所有环境下都具有 1 mm 的实际精度。温度、目标表面、目标角度和环境反射都会影响结果。
4. 五点滑动平均滤波
4.1 状态变量
滤波器使用一个长度为 5 的静态数组:
#define FILTER_WINDOW 5 static int32_t g_filter_values[FILTER_WINDOW]; static uint8_t g_filter_count; static uint8_t g_filter_position;
三个变量分别表示:
g_filter_values:保存最近的距离样本;
g_filter_count:当前已经写入的有效样本数,最大为 5;
g_filter_position:下一次写入数组的位置。
这些变量声明为 static,其生命周期覆盖程序整个运行过程,并且启动时自动初始化为 0。
4.2 写入新样本和索引回绕
g_filter_values[g_filter_position] = value; g_filter_position = (g_filter_position + 1U) % FILTER_WINDOW;
每次调用先覆盖当前位置,再将索引加一。取模运算使索引按照以下顺序循环:
0 -> 1 -> 2 -> 3 -> 4 -> 0 -> 1 ...
数组填满后,新样本会覆盖最旧的样本,因此不需要移动整个数组。
4.3 处理启动阶段
if (g_filter_count < FILTER_WINDOW)
{
g_filter_count++;
}程序刚启动时还没有 5 个有效样本。g_filter_count 会从 1 增加到 5,然后保持不变。这样第一次测量只除以 1,第二次测量除以 2,不会把尚未使用的数组元素当成有效的零值。
4.4 求平均值
int64_t sum = 0;
for (uint8_t index = 0; index < g_filter_count; index++)
{
sum += g_filter_values[index];
}
return (int32_t)(sum / g_filter_count);求和变量使用 int64_t,可以避免窗口增大后累加发生 32 位溢出。当前窗口只有 5,遍历求和的开销很小,代码也比维护动态累加和更直观。
假设连续测得 230、235、250、240、245 mm,滤波结果为:
(230 + 235 + 250 + 240 + 245) / 5 = 240 mm
窗口越大,读数越平稳,但显示响应也越慢。修改 FILTER_WINDOW 可以对比不同窗口大小的效果。
5. SW2 按键切换 cm/m
5.1 从设备树取得按键
FRDM-MCXA153 的 sw0 别名指向 SW2,实际引脚为 P3_29:
#define BUTTON_NODE DT_ALIAS(sw0) static const struct gpio_dt_spec g_button = GPIO_DT_SPEC_GET(BUTTON_NODE, gpios);
gpio_dt_spec 中包含 GPIO 控制器、引脚编号和有效电平属性。SW2 在板卡设备树中是低电平有效,使用 gpio_pin_configure_dt() 时 Zephyr 会保留该逻辑属性。

5.2 配置 GPIO 中断
gpio_pin_configure_dt(&g_button, GPIO_INPUT); gpio_pin_interrupt_configure_dt(&g_button, GPIO_INT_EDGE_TO_ACTIVE); gpio_init_callback(&g_button_callback, button_pressed, BIT(g_button.pin)); gpio_add_callback(g_button.port, &g_button_callback);
GPIO_INT_EDGE_TO_ACTIVE 表示按键从非活动状态进入活动状态时触发。由于 SW2 是低电平有效,按下按键时产生该逻辑边沿。BIT(g_button.pin) 指定回调只处理 SW2 对应的 GPIO 位。
5.3 中断回调和软件消抖
机械按键按下一次时可能产生多个快速边沿。回调函数使用系统运行时间实现 250 ms 消抖:
uint32_t now = k_uptime_get_32();
if ((uint32_t)(now - g_last_button_ms) >= 250U)
{
g_long_range = !g_long_range;
g_last_button_ms = now;
}在 250 ms 内重复到达的中断不会改变模式。通过无符号减法计算时间差,即使 32 位运行时间计数器发生回绕,短时间间隔判断仍然有效。
回调没有执行 OLED 刷新、字符串格式化或串口打印,只修改状态变量,因此中断处理保持简短。g_long_range 和 g_last_button_ms 使用 volatile,防止编译器假设它们不会被异步回调修改。
6. 根据状态选择显示内容
程序使用以下常量定义两种显示范围:
#define SHORT_RANGE_MM 1000 #define LONG_RANGE_MM 4000 int32_t max_mm = g_long_range ? LONG_RANGE_MM : SHORT_RANGE_MM;
display_distance() 的分支顺序是:
if (distance_mm > max_mm)
{
snprintk(distance_line, sizeof(distance_line), "OUT OF RANGE");
}
else if (g_long_range)
{
/* 格式化为 m */
}
else
{
/* 格式化为 cm */
}因此两种状态对应:
g_long_range == false:显示一位小数的厘米,范围提示为 0-100 cm;
g_long_range == true:显示三位小数的米,范围提示为 0-4 m。
切换的是显示单位和超量程阈值,不是 HC-SR04 的硬件工作范围。传感器仍然按相同方式发送和接收超声波。
7. 串口对比原始值和滤波值
主循环同时打印滤波前后的结果:
printk("distance_raw=%ld mm distance_filtered=%ld mm range=%s\n",
(long)raw_mm,
(long)filtered_mm,
g_long_range ? "m" : "cm");示例输出:
distance_raw=238 mm distance_filtered=235 mm range=cm
distance_raw 用于观察传感器的即时波动,distance_filtered 用于观察滤波后的稳定程度。range 字段记录当前 OLED 模式,上位机保存数据时也能知道当时使用的显示单位。

8. 测量错误处理
只有 sensor_sample_fetch() 和 sensor_channel_get() 都成功时,程序才把结果送入滤波器:
if (result < 0)
{
printk("HC-SR04 measurement failed: %d\n", result);
cfb_framebuffer_clear(g_display, false);
cfb_print(g_display, "Sensor error", 0, 16);
cfb_framebuffer_finalize(g_display);
}
else
{
int32_t raw_mm = (int32_t)distance.val1 * 1000 +
distance.val2 / 1000;
int32_t filtered_mm = moving_average_mm(raw_mm);
display_distance(filtered_mm);
}超时或 GPIO 读取失败不会向滑动平均数组写入错误值,从而避免一次失败把后续几次显示一起拉低。OLED 显示 Sensor error,串口则保留具体错误码用于调试。

功能实现完整代码:
后缀修改为.c :main.doc
我要赚赏金
