这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【过程帖】简易超声波测距仪实践——基础外设调试:从零攒一套ZephyrBSP

共3条 1/1 1 跳转至

【过程帖】简易超声波测距仪实践——基础外设调试:从零攒一套ZephyrBSP

菜鸟
2026-09-20 21:56:14     打赏
上一篇开箱点了灯,从这篇开始正式做项目,第一阶段的目标很朴素:把雷达系统要用的外设一个一个全部调通——超声波、LCD 屏、舵机。

这个阶段用 Zephyr 的话来说就是在做 BSP(板级支持包)。传统单片机开发里 BSP 是一堆 bsp_xxx.c/h,而 Zephyr 里它变成了"设备树 overlay + Kconfig + 一堆小验证工程"。

一、先花五分钟搞懂:Zephyr 是怎么"认识"你的硬件的

如果你和我一样是从 STM32/HAL 过来的,第一件事要转变观念:Zephyr 把"哪个引脚干什么"从 C 代码里拿走了

一个 Zephyr 应用 = 三份东西:

文件功能类比
prj.conf(Kconfig)功能开关:要不要 SPI、要不要显示驱动、堆多大CubeMX 里的勾选
boards/*.overlay(设备树)硬件描述:引脚分配、外设参数、别名CubeMX 的引脚图
src/*.c业务逻辑用户代码

C 代码里拿外设的"三板斧"宏(全工程就这三种写法,记住就够):

/* ① 从别名/节点取"描述结构体"——编译期生成,零运行时开销 */
static const struct gpio_dt_spec trig = GPIO_DT_SPEC_GET(DT_ALIAS(sonar_trig), gpios);
static const struct pwm_dt_spec servo  = PWM_DT_SPEC_GET(DT_ALIAS(servo_pwm));
const struct device *display = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));

/* ② 用时先查 ready */
if (!gpio_is_ready_dt(&trig)) { ... }

/* ③ 正常调用 */
gpio_pin_set_dt(&trig, 1);

二、环境验证:点灯(水一遍)

开箱帖点过灯了,但写帖时发现忘附图,再水一次:)

image-20260915224853065.png

板载 RGB 灯,三个 GPIO 低电平点亮。设备树里长这样(板卡自带的,不用我们写):

externalzephyrboardsnxpfrdm_mcxa153frdm_mcxa153.dts

C 代码里 DT_ALIAS(led0) 一取就用:

#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);

gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
while (1) {
    gpio_pin_toggle_dt(&led);
    k_msleep(1000);
}

注意一个细节:GPIO_DT_SPEC_GET 一次把"哪个口、哪个脚、什么电平有效"三件事打包带走,之后所有操作都是 _dt 后缀的函数自动识别——全程不需要出现 gpio3、12 这种裸数字。这就是 BSP 的"业务代码与引脚解耦"。

编译烧录:

image-20260915225432315.pngimage-20260915225437827.png3.png

三、HC-SR04 超声波:第一个"自定义外设"

HC-SR04 都熟:Trig 给个 ≥10µs 高脉冲,模块发 40kHz 声波,Echo 回一个和距离成正比的高电平,d = t×343/2。

image-20260920213037778.png

接线

Trig 接 GPIO 输出,Echo 接 GPIO 输入配双边沿中断。这里有个必须较真的点:Echo 是 5V 电平,MCXA153 的 GPIO 不耐 5V——要么电阻分压,要么直接买 3.3V 兼容的 HC-SR04P。我一开始没注意,后面发现mcu link连不上A153,但是好在mcu没完全挂,千万别学我 T_T。

BSP 第一步:给引脚起别名

板卡设备树里没有"超声波"这种东西,要在应用 overlay 里自己声明。Zephyr 声明 GPIO 引脚最省事的写法是借 gpio-leds 这个现成绑定(兼容串随便借,只要属性对得上):

/ {
    aliases {
        sonar-trig = &sonar_trig_pin;
        sonar-echo = &sonar_echo_pin;
    };
    app_gpio_pins {
        compatible = "gpio-leds";
        sonar_trig_pin: sonar_trig {
            gpios = <&gpio3 28 GPIO_ACTIVE_HIGH>;
        };
        sonar_echo_pin: sonar_echo {
            gpios = <&gpio3 27 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>;
        };
    };
};

aliases 是这套 BSP 的灵魂:业务代码只认 sonar-trig 这个小名,哪天引脚要挪,只改 overlay 一行,C 代码不动。

BSP 第二步:驱动代码

测距的核心是脉宽计时,用 GPIO 双边沿中断 + 信号量最合适——上升沿记时标、下降沿收尾并唤醒等待线程:

static void echo_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
    if (gpio_pin_get_dt(&echo_spec) > 0) {
        t_rise = k_cycle_get_32();          /* 上升沿:声波出发 */
    } else {
        t_fall = k_cycle_get_32();          /* 下降沿:收到回波 */
        k_sem_give(&sem_echo_done);
    }
}

static int32_t sonar_measure_distance_mm(void)
{
    gpio_pin_set_dt(&trig_spec, 1);
    k_busy_wait(12);                        /* ≥10µs 触发脉冲 */
    gpio_pin_set_dt(&trig_spec, 0);

    if (k_sem_take(&sem_echo_done, K_MSEC(35)) != 0) {
        return -1;                          /* 超时 = 4m 内没东西 */
    }
    uint32_t cycles = (t_fall >= t_rise) ? (t_fall - t_rise)
                         : (UINT32_MAX - t_rise + t_fall + 1);
    uint32_t us = (uint32_t)k_cyc_to_us_floor64(cycles);
    return (int32_t)((us * 343ULL) / 2000ULL);
}

几个当时琢磨过的点:

  • 为什么用 k_cycle_get_32() 不用 k_uptime_get_32():后者单位是 ms,几十厘米的变化只有零点几 ms,精度不够;cycle 是 CPU  tick,96MHz 下分辨率 10ns 级。

  • 为什么中断里给信号量而不是直接算:中断要短,耗时活(换算、打印)都放线程。

  • 回波超时会污染下一次测量:后来的版本在触发前先排空信号量、再加个 echo_armed 保险栓,非测量窗口的边沿一律忽略。

  • 连续采样加了 5 点中值滤波,偶发跳变基本绝迹。

实际测试发现还挺准的

四、MG90S 舵机:第一个要动 pinctrl 的外设

image.png

舵机信号是 50Hz PWM,0.5ms~2.5ms 脉宽对应 0°~180°。接线一句话:信号线接 PWM 输出,独立 5V 供电,和板子共地

BSP 时刻:默认引脚不够用,自己写 pinmux

查 MCXA153 的引脚表后我选了 P3_9(FlexPWM0 的 B1 通道)。但板卡自带的 pinctrl 里没有这组配置——BSP 工作来了:在 overlay 里自己加一组引脚配置:

#include <nxp/mcx/MCXA153VLH-pinctrl.h>

&pinctrl {
    pinmux_flexpwm0_pwm1: pinmux_flexpwm0_pwm1 {
        group0 {
            pinmux = <PWM0_B1_P3_9>;   /* P3_9 = FlexPWM0 SM1 B1 */
            slew-rate = "fast";
        };
    };
};

&flexpwm0_pwm1 {
    status = "okay";
    pinctrl-0 = <&pinmux_flexpwm0_pwm1>;
    pinctrl-names = "default";
};

这就是 Zephyr 的引脚复用哲学:引脚功能不由 GPIO 寄存器顺手配,而是集中交给 pinctrl 子系统,设备启用时自动应用。#include 那个头文件提供 PWM0_B1_P3_9 这类宏——每个引脚每个功能一个宏,查表即得。

PWM 设备描述借 pwm-leds 绑定:

servo_dev {
    compatible = "pwm-leds";
    servo_node: servo_0 {
        pwms = <&flexpwm0_pwm1 1 20000000 0>;  /* 通道1,20ms 周期=50Hz */
    };
};

C 代码就三行核心:

static const struct pwm_dt_spec servo = PWM_DT_SPEC_GET(DT_ALIAS(servo_pwm));

uint32_t pulse_us = SERVO_MIN_PULSE_US +
    ((uint32_t)angle * (SERVO_MAX_PULSE_US - SERVO_MIN_PULSE_US)) / 180;
pwm_set_pulse_dt(&servo, PWM_USEC(pulse_us));

五、ST7735 屏:节点最多的一个外设

image-20260920214601706.png

160×128 小彩屏,4 线 SPI。这是三个外设里设备树节点最多、最"BSP"的一个。

三层节点,层层套娃

/ {
    chosen {
        zephyr,display = &st7735;      /* 钦定系统主屏 */
    };
    mipi_dbi {
        compatible = "zephyr,mipi-dbi-spi";   /* Zephyr 显示协议层 */
        spi-dev = <&lpspi0>;                  /* 底层用哪个 SPI 控制器 */
        dc-gpios = <&gpio3 30 GPIO_ACTIVE_HIGH>;

        st7735: st7735r@0 {
            compatible = "sitronix,st7735r";
            reg = <0>;
            mipi-max-frequency = <8000000>;
            width = <160>;  height = <128>;
            madctl = <0x60>;  colmod = <0x55>;   /* 方向 + RGB565 */
            pwctr1 = [a2 02 84];  /* ...一长串电源/伽马参数 */
        };
    };
};

&lpspi0 {
    status = "okay";
    cs-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
};

结构上值得看懂:应用代码面对的是 display 通用 API(display_write/display_blanking_off),中间是 Zephyr 的 mipi-dbi 协议层,底下才落到 SPI 控制器。换一块 ILI9341 屏?改 st7735r 节点里的兼容串和参数,C 代码零改动。

pwctr1/gamctr 是屏厂给的初始化序列,Zephyr 驱动照单全收。这块不用懂原理,照抄即可,后面 radar 篇详聊。

注意事项

  1. 字节序:MCU 小端、SPI 按大端发 RGB565,写之前要 __builtin_bswap16();

  2. 显存存不起:160×128×2 = 40KB,SRAM 总共 24KB。所以刷屏用一行 320 字节的行缓冲逐行写,纯色填充足够快。

static uint16_t line_buffer[160];
uint16_t swapped = __builtin_bswap16(color);
for (int i = 0; i < 160; i++) line_buffer[i] = swapped;

desc.buf_size = sizeof(line_buffer);
desc.width = 160;  desc.height = 1;  desc.pitch = 160;
for (int y = 0; y < 128; y++) {
    display_write(display_dev, 0, y, &desc, line_buffer);
}

六、攒 BSP 的一周,总结四句

  1. 每个外设一个最小验证工程:hello→blinky→PWM→超声波→屏→舵机,各自独立可编译。好处是集成出问题时随时能二分定位——后来的花屏排查就靠 5_1 这个最小屏工程救的命。

  2. overlay 就是 BSP 的本体:引脚、外设参数、别名全在里面,C 代码只认别名。把它当宝贝维护,别在代码里写裸引脚号。

  3. 驱动三步走背下来:DT_ 宏取描述 → is_ready 检查 → 调用,Zephyr 所有驱动都是这个节奏,会一个就会全部。

  4. Zephyr 中文资料太少了





关键词: let's do     zephyr     BSP     调试    

菜鸟
2026-09-20 22:08:11     打赏
2楼

论坛不能直接贴视频,很难受。最终项目我已经做好了,大概就是这样的效果,这几天应该会发成果帖子。项目我整理好会开源出来,欢迎大伙点点star :)点赞关注不迷路bushi

image.png


专家
2026-09-21 07:22:19     打赏
3楼

谢谢楼主分享


共3条 1/1 1 跳转至

回复

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