这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【let'sdo|2026年第2期】(成果贴)简易超声波测距仪

共2条 1/1 1 跳转至

【let'sdo|2026年第2期】(成果贴)简易超声波测距仪

菜鸟
2026-08-31 00:44:10     打赏

本次实现:小数显示、五点滑动平均滤波,以及使用 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 会保留该逻辑属性。

image.png

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 模式,上位机保存数据时也能知道当时使用的显示单位。

image.png

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,串口则保留具体错误码用于调试。

image.png

功能实现完整代码:

后缀修改为.c :main.doc


院士
2026-08-31 17:06:19     打赏
2楼

学习了,谢谢分享。


共2条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]