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

共1条 1/1 1 跳转至

【let'sdo|2026年第2期】简易超声波测距仪实践-过程贴

菜鸟
2026-08-26 22:46:26     打赏


将在zephyr下进行开发,zephyr中对nxp主流开发板均有适配,如下

(.venv) l@l:~/zephyrproject$ west boards | grep "frdm"
frdm_mcxa153
frdm_ke17z
frdm_kl25z
frdm_mcxa266
frdm_mcxa346
frdm_mcxa366
frdm_mcxw72
frdm_mcxw23
frdm_mcxw70
frdm_rw612
frdm_imx93
frdm_k22f
frdm_imxrt1186
frdm_mcxw71
frdm_mcxl255
frdm_mcxa577
frdm_ke16z
frdm_mcxn947
frdm_mcxe247
frdm_mcxc242
frdm_mcxc444
frdm_kw41z
frdm_mcxa344
frdm_mcxe31b
frdm_k32l2b3
frdm_imx91
frdm_ke17z512
frdm_mcxa156
frdm_k82f
frdm_k64f
frdm_mcxn236
frdm_ke15z

其中frdm_macx153就是本次使用的开发板,所以不需要再进行board适配了

本次使用的模块有,US-100超声波模块和c4001毫米波传感器

但是这两种模块zephyr里都没有驱动,所以需要自己写驱动

首先介绍一下US-100

image.png

US-100 是一款超声波测距传感器,外形和常见的 HC-SR04 几乎一样,但多了几个实用特性。

核心特点:

宽电压供电:支持 2.4V~5.5V,3V 和 5V 系统都能直接用,不需要电平转换。

双工作模式,靠背面跳线切换:

  • 拔掉跳线 → HC-SR04 兼容模式,用 Trigger/Echo 引脚,通过回波脉冲宽度算距离。

  • 装上跳线 → 串口 UART 模式,9600 波特率通信。发 0x55 读回两字节(16 位)得到毫米距离;发 0x50 读回温度(摄氏度)。UART 模式适合接电脑 USB 转串口,或者那些做不了 HC-SR04 精确时序的设备。

主要参数:

  • 工作电流:2mA

  • 测距范围:2cm~450cm(实际 10cm~250cm 效果最好)

  • 精度:0.3cm + 1%

  • 测量角度:小于 15°

  • 工作温度:-20~+70℃

  • 尺寸:45 × 20mm(不含排针),重 9g

UART 模式还能读温度,可以用来做声速温度补偿,让测距更准——这是相比 HC-SR04 的一个明显优势。


因为常用的HC-SR04用的是Trigger/Echo 引脚,通过回波脉冲宽度算距离,所以驱动也选择这个模式

驱动如下

/*
 * HC-SR04 ultrasonic distance sensor.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT vnd_hcsr04

#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/atomic.h>

#include <my_module/hcsr04.h>

LOG_MODULE_REGISTER(hcsr04, CONFIG_HCSR04_LOG_LEVEL);

/* Datasheet asks for a trigger pulse of at least 10us. */
#define HCSR04_TRIG_PULSE_US 10U

/*
 * Speed of sound in dry air at 20 degC is 343 m/s, i.e. 0.343 mm/us. The pulse
 * covers the distance twice, so mm = us * 343 / 2000. Worst case us is bounded
 * by HCSR04_ECHO_MAX_US below, so the multiplication stays in 32 bits.
 */
#define HCSR04_SOUND_MM_PER_MS 343U

/*
 * Nothing in range gives a ~38ms pulse. Anything at or above that is a
 * "no object" report rather than a distance, and 4m (the rated maximum) is
 * only 23ms, so treat the rest as out of range.
 */
#define HCSR04_ECHO_MAX_US 36000U

struct hcsr04_config {
    struct gpio_dt_spec trig;
    struct gpio_dt_spec echo;
    uint16_t timeout_ms;
    uint16_t interval_ms;
};

struct hcsr04_data {
    const struct device *dev;
    struct gpio_callback echo_cb;
    /* Serializes fetches, and guards distance_mm against a concurrent get. */
    struct k_mutex lock;
    /* Given by the echo ISR once a full pulse has been captured. */
    struct k_sem done;
    /* ISR state: timestamp of the rising edge, and whether we saw one. */
    uint32_t rise_cycles;
    bool rise_seen;
    atomic_t pulse_cycles;
    int64_t next_trigger_uptime;
    uint32_t distance_mm;
};

static void hcsr04_echo_handler(const struct device *port, struct gpio_callback *cb, uint32_t pins)
{
    struct hcsr04_data *data = CONTAINER_OF(cb, struct hcsr04_data, echo_cb);
    const struct hcsr04_config *config = data->dev->config;
    uint32_t now = k_cycle_get_32();
    int level;

    ARG_UNUSED(port);
    ARG_UNUSED(pins);

    level = gpio_pin_get_dt(&config->echo);
    if (level < 0) {
        return;
    }

    if (level == 1) {
        data->rise_cycles = now;
        data->rise_seen = true;
        return;
    }

    /* A falling edge without a rising one is noise from a previous run. */
    if (!data->rise_seen) {
        return;
    }

    data->rise_seen = false;
    atomic_set(&data->pulse_cycles, now - data->rise_cycles);
    (void)gpio_pin_interrupt_configure_dt(&config->echo, GPIO_INT_DISABLE);
    k_sem_give(&data->done);
}

/* Emits the trigger pulse and waits for the echo. Returns the pulse width. */
static int hcsr04_measure(const struct device *dev, uint32_t *echo_us)
{
    const struct hcsr04_config *config = dev->config;
    struct hcsr04_data *data = dev->data;
    int64_t wait_ms;
    int ret;

    /* The sensor needs a quiet period between bursts, see measure-interval-ms. */
    wait_ms = data->next_trigger_uptime - k_uptime_get();
    if (wait_ms > 0) {
        k_msleep((int32_t)wait_ms);
    }

    data->rise_seen = false;
    k_sem_reset(&data->done);

    ret = gpio_pin_interrupt_configure_dt(&config->echo, GPIO_INT_EDGE_BOTH);
    if (ret < 0) {
        LOG_ERR("failed to enable echo interrupt: %d", ret);
        return ret;
    }

    ret = gpio_pin_set_dt(&config->trig, 1);
    if (ret < 0) {
        goto disable;
    }

    k_busy_wait(HCSR04_TRIG_PULSE_US);

    ret = gpio_pin_set_dt(&config->trig, 0);
    if (ret < 0) {
        goto disable;
    }

    data->next_trigger_uptime = k_uptime_get() + config->interval_ms;

    if (k_sem_take(&data->done, K_MSEC(config->timeout_ms)) != 0) {
        LOG_WRN("no echo within %u ms", config->timeout_ms);
        ret = -EIO;
        goto disable;
    }

    *echo_us = k_cyc_to_us_floor32((uint32_t)atomic_get(&data->pulse_cycles));

    return 0;

disable:
    (void)gpio_pin_interrupt_configure_dt(&config->echo, GPIO_INT_DISABLE);

    return ret;
}

static int hcsr04_sample_fetch(const struct device *dev, enum sensor_channel chan)
{
    struct hcsr04_data *data = dev->data;
    uint32_t echo_us;
    int ret;

    if (chan != SENSOR_CHAN_ALL && chan != SENSOR_CHAN_DISTANCE) {
        return -ENOTSUP;
    }

    k_mutex_lock(&data->lock, K_FOREVER);

    ret = hcsr04_measure(dev, &echo_us);
    if (ret == 0) {
        if (echo_us >= HCSR04_ECHO_MAX_US) {
            LOG_DBG("echo %u us: nothing in range", echo_us);
            ret = -ERANGE;
        } else {
            data->distance_mm = echo_us * HCSR04_SOUND_MM_PER_MS / 2000U;
        }
    }

    k_mutex_unlock(&data->lock);

    return ret;
}

static int hcsr04_channel_get(const struct device *dev, enum sensor_channel chan,
struct sensor_value *val)
{
    struct hcsr04_data *data = dev->data;

    if (chan != SENSOR_CHAN_DISTANCE) {
        return -ENOTSUP;
    }

    k_mutex_lock(&data->lock, K_FOREVER);
    /* SENSOR_CHAN_DISTANCE is in metres, the measurement is in millimetres. */
    sensor_value_from_milli(val, (int64_t)data->distance_mm);
    k_mutex_unlock(&data->lock);

    return 0;
}

static int hcsr04_init(const struct device *dev)
{
    const struct hcsr04_config *config = dev->config;
    struct hcsr04_data *data = dev->data;
    int ret;

    if (!gpio_is_ready_dt(&config->trig)) {
        LOG_ERR("trig port %s not ready", config->trig.port->name);
        return -ENODEV;
    }

    if (!gpio_is_ready_dt(&config->echo)) {
        LOG_ERR("echo port %s not ready", config->echo.port->name);
        return -ENODEV;
    }

    ret = gpio_pin_configure_dt(&config->trig, GPIO_OUTPUT_INACTIVE);
    if (ret < 0) {
        LOG_ERR("failed to configure trig pin: %d", ret);
        return ret;
    }

    ret = gpio_pin_configure_dt(&config->echo, GPIO_INPUT);
    if (ret < 0) {
        LOG_ERR("failed to configure echo pin: %d", ret);
        return ret;
    }

    /* Keep it off until a fetch asks for it, so stray edges are ignored. */
    ret = gpio_pin_interrupt_configure_dt(&config->echo, GPIO_INT_DISABLE);
    if (ret < 0) {
        LOG_ERR("echo pin does not support interrupts: %d", ret);
        return ret;
    }

    gpio_init_callback(&data->echo_cb, hcsr04_echo_handler, BIT(config->echo.pin));

    ret = gpio_add_callback_dt(&config->echo, &data->echo_cb);
    if (ret < 0) {
        LOG_ERR("failed to add echo callback: %d", ret);
        return ret;
    }

    k_mutex_init(&data->lock);
    k_sem_init(&data->done, 0, 1);

    return 0;
}

static DEVICE_API(sensor, hcsr04_driver_api) = {
    .sample_fetch = hcsr04_sample_fetch,
    .channel_get = hcsr04_channel_get,
};

#define HCSR04_DEFINE(i)                                                                           
    static struct hcsr04_data hcsr04_data_##i = {                                              
        .dev = DEVICE_DT_INST_GET(i),                                                      
    };                                                                                         
    static const struct hcsr04_config hcsr04_config_##i = {                                    
        .trig = GPIO_DT_SPEC_INST_GET(i, trig_gpios),                                      
        .echo = GPIO_DT_SPEC_INST_GET(i, echo_gpios),                                      
        .timeout_ms = DT_INST_PROP(i, timeout_ms),                                         
        .interval_ms = DT_INST_PROP(i, measure_interval_ms),                               
    };                                                                                         
    SENSOR_DEVICE_DT_INST_DEFINE(i, hcsr04_init, NULL, &hcsr04_data_##i, &hcsr04_config_##i,   
                     POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY,                     
                     &hcsr04_driver_api);

DT_INST_FOREACH_STATUS_OKAY(HCSR04_DEFINE)

整体结构:

配置与数据分离,这是 Zephyr 驱动的惯例。hcsr04_config 存放编译期不变的东西(trig/echo 引脚、超时、测量间隔),从设备树读取;hcsr04_data 存放运行期状态(测量结果、同步原语、ISR 中间状态)。

工作原理:

测距靠 hcsr04_measure。流程是:先保证距上次触发有足够静默期,然后使能 echo 引脚的双边沿中断,在 trig 引脚上打一个至少 10μs 的脉冲,再等信号量。

回波捕获在中断里做,hcsr04_echo_handler:

  • 上升沿 → 记录时间戳 rise_cycles

  • 下降沿 → 计算脉冲宽度存入原子变量 pulse_cycles,关中断,给信号量放行

距离换算用声速。20℃ 干燥空气声速 343 m/s,声波往返两次

即代码里的 echo_us * 343 / 2000。、

测试一下

截图 2026-08-24 16-10-07.png

图形基本没问题,精度也我预想的要高一些,基本能够完成近距离场景下的测距


C4001

image.png

C4001 24GHz 毫米波人体存在检测传感器,最大检测范围 25 米、100°*40° 波束角和 24GHz 工作频率。该传感器可在 -40℃ 至 85℃ 的温度范围内工作,波特率为 9600,尺寸紧凑,仅为 26mm*30mm。该传感器采用 FMCW 调制,专为高级人体存在检测应用而设计,可在各种环境条件下提供可靠的性能。

板子上提供了 VIN、GND、RX、TX 和 OUT 引脚,官方只提供了arduino库,所以zephyr驱动也需要自己写。

该模块同样支持两种模式,常规的uart模式和iic模式

UART 模式下,传感器和主控是一问一答加主动上报的关系:主控通过 TX/RX 两根线跟模块通信,配置好检测模式后,模块会按固定波特率(9600)持续往外吐数据帧。写 Zephyr 驱动时,底层要绑定一个 UART 控制器,通过 uart_poll_in(轮询)或中断回调把字节收进缓冲区,再按 C4001 的帧格式解析出"有人/没人""距离""速度"这些信息。因为是标准串口,几乎不挑板子,任何带 UART 的 MCU 都能跑,是最通用、最适合上手的方式。缺点是波特率固定、收发要自己管缓冲和帧同步,解析逻辑得照着厂商协议一点点对。

I2C 模式下,传感器作为从机挂在 I2C 总线上,默认地址 0x2A(可选 0x2B),主控通过读写寄存器的方式跟它交互——想拿存在状态、距离、速度,就去读对应的寄存器地址。写 Zephyr 驱动时,底层绑定一个 I2C 控制器,用 i2c_reg_read/i2c_write_read 这类标准 API 按寄存器地址取值即可,不用像 UART 那样自己做帧同步和缓冲管理,解析负担轻很多。而且 I2C 是总线结构,同一条线上可以再挂别的器件,省引脚。代价是需要额外的 SCL/SDA 上拉电阻,对布线和总线时序更敏感,长距离或干扰环境下不如 UART 。

官方主推的是uart模式,从板子上的丝印也能看出来。所以驱动也按uart来写

C4001 的串口协议是纯 ASCII 字符串,像个小型命令行终端,例如:

  • 配置命令:ASCII 字符串,以回车换行(rn)结尾,参数用空格分隔。例如 setRange 0.6 6rn

  • 读取命令的响应:以 Response 开头,rn 结尾

  • 命令执行结果:成功回 Done,失败回 Error

  • 模块甚至会打印一个命令提示符 DFRobot:/>

这对写解析器来说其实更简单——不用算校验和,按行读、按字符串匹配就行。

驱动代码太长,就不再贴了,看一下效果

image.png

这颗毫米波雷达的实测精度达不到我最初设想的桌面级水平。从波形上看,蓝色曲线(距离)和紫色曲线(速度)在匀速往复一个周期的测试中形态基本正常,数据是可用的。

但它的定位决定了能力边界:这是一颗面向室内存在性检测的模块,距离量程为1.2m-35m,近距离精度和分辨率都不足以支撑精细的桌面级测量。

因此需要对项目重新规划,采用双模块分工的方案:毫米波模块负责远距离测距与唤醒,超声波模块负责近距离测距,两者互补覆盖完整量程。






共1条 1/1 1 跳转至

回复

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