ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 US100超声波模块串口测距实战:从CubeMX配置到非阻塞状态机

STM32 US100超声波模块串口测距实战:从CubeMX配置到非阻塞状态机 简介基于STM32与US100超声波模块的串口触发测距完整工程资源面向物联网、自动控制、机器人等领域的嵌入式开发者与电子爱好者。资源包含64个C源码与64个头文件、62个编译中间文件o/crf/d、2个hex和axf烧录文件、2个uvprojx工程文件另有PDF用户手册和DOC说明文档共353个文件压缩包整体约14.43MB结构清晰适合直接对照学习。工程采用US100串口触发模式涵盖UART初始化、触发命令发送、回波中断读取、STM32定时器计时以及距离计算声速×时间/2等完整流程并考虑了发射功率、接收灵敏度等参数调整与抗干扰措施。已有181人学习适合具备一定STM32基础、正在开展测距、避障或自主导航项目的开发者参考可直接在Keil环境中打开编译验证。1. 从 US100 串口触发说起STM32 测距为什么先看这根线调试超声波模块最怕的不是距离读得不准而是模块上电后根本没按你以为的方式工作。US100 这类带 MCU 的一体化模块除了常见的 GPIO 脉宽输出还藏着一条串口触发路径把 Trig/Echo 当作 UART 引脚上电后发一个字节 0x55 就能读回毫米级距离免去测量脉宽、算时间戳的整套逻辑。对 STM32 测距项目来说这条路径门槛低、排查直观很适合作为超声波模块落地的第一选择。下面按协议、接线、HAL 实现和调试套路展开照着做就能把 US100 跑通并且知道异常数据到底出在哪。2. US100 串口测距原理模式选择、数据格式与电平匹配在写代码之前先搞清楚模块的行为。US100 的不同工作模式不是由寄存器切换的而是由上电瞬间引脚状态决定这一点直接影响了接线和初始化顺序值得单独说清楚。2.1 从一上电就开始US100 靠引脚状态选择工作模式US100 的 Trig 和 Echo 不是固定输入或输出。模块上电时如果 Trig 和 Echo 同时保持高电平模块进入 UART 串口模式否则进入 GPIO 触发模式。串口模式下Trig 就是模块的串口接收脚 RXEcho 就是模块的串口发送脚 TX。模块内存的固件会在这两种模式下做完全不同的数据处理。在实际开发板上STM32F103C8T6 的串口 TX 要接到模块的 Trig串口 RX 要接到模块的 Echo和字面含义刚好相反。经常有人按 HC-SR04 的习惯把 Trig 接到 MCU 的普通 GPIO、Echo 接到普通输入结果发 0x55 毫无反应就是这个原因。下面是比较表工作模式上电时 Trig/Echo 状态数据交互方式适用场景GPIO 模式至少有一个为低电平触发脉冲 回波脉宽兼容 HC-SR04 逻辑占用定时器UART 串口模式两脚均为高电平MCU 发命令模块返回字节省定时器数据直接是毫米值2.2 串口触发协议0x55 测距、0x50 测温串口模式下模块与外部 MCU 之间是主从问答关系。模块默认波特率 96008 数据位无校验1 停止位这个参数不能通过软件更改STM32CubeMX 里面要按这个配置。MCU 发送一个字节0x55模块测量一次距离并返回 2 个字节高字节在前低字节在后单位是毫米。例如返回0x02 0x17距离就是0x02 * 256 0x17 535mm。MCU 发送0x50模块返回一个字节的板载温度值单位摄氏度带符号。命令返回长度返回示例换算结果0x552 字节0x02 0x17535 mm0x501 字节0x0F15 ℃温度数据有个特殊编码如果返回字节的最高位为 1表示负温度实际值用256 - raw_value再取负。比如收到0xFF代表-(256 - 255) -1℃。这里还有个容易忽略的细节0x55 和 0x50 分别对应 ASCII 字符U和P用串口调试助手调试时发送区必须选择 HEX 发送否则你发的是字符串里面的字节模块会无法识别。2.3 电平匹配3.3V 供电优先于加电平转换US100 供电范围是 2.4V 到 5.5V但 UART 引脚输出的高电平会跟着 VCC 走。给模块供 5V 时模块 TXD 返回的高电平接近 5V直接接到 STM32F103 的 RX 引脚前要先确认该引脚是否容忍 5V不同引脚、不同批次的数据手册要求并不一致。工程上我一般建议直接用 3.3V 给 US100 供电。模块在 3.3V 下发射功率略低但室内测距、避障这种场景仍然够用实测距离范围也能到 4 米级别。这样模块 TXD 的高电平就是 3.3V和 STM32 串口电平天然匹配不需要加电平转换芯片。如果项目必须用 5V 供电常见做法是在模块 TXD 与 MCU RX 之间串一个 1kΩ 电阻限流再配合稳压管或电阻分压不推荐直接连。提示无论用哪种供电方式GND 必须和 STM32 共地。串口通信一旦乱码或收不到数据第一件事就是用万用表量两边 GND 是否有压差。3. 用 STM32CubeMX 配置 US100 串口触发的最小工程理解了协议接下来把它落到实际工程。这里以 STM32F103C8T6 为例用 STM32CubeMX 生成 HAL 工程只保留串口触发测距的最小代码方便直接抄走。3.1 接线STM32F103C8T6 用 USART1 接 US100我习惯把 USART1 留给 US100USART2 留作调试日志打印这样测距模块和上位机串口互不干扰。US100 串口模式下两个信号线的接法如下STM32F103C8T6US100说明PA9 / USART1_TXTrigSTM32 发命令给模块PA10 / USART1_RXEcho模块返回数据给 STM323.3VVCC供电GNDGND共地如果模块丝印只写了 Trig 和 Echo不要被名字带偏串口模式下它们就是 RX 和 TX。模块的 Trig 脚接 MCU 的 TX是因为数据方向是 MCU → 模块。3.2 CubeMX 参数表波特率 9600 不能改打开 STM32CubeMX选择 STM32F103C8T6 后在Connectivity USART1里选择异步模式。参数按下面这张表填配置项值理由ModeAsynchronous标准 UART 收发Baud Rate9600US100 固件固定波特率Word Length88 位数据ParityNone无校验Stop Bits11 位停止位USART1 global interruptEnable为后续非阻塞模式预留这里最容易出错的点就是波特率。有人习惯性设成 115200导致模块返回的全是乱码。US100 的 9600 是写死在模块内部的不是外部可配置参数所以 CubeMX 里必须跟着改。系统时钟继续用默认 72MHz不需要额外处理。3.3 核心代码读一次距离的最小实现生成工程后写一个Us100_ReadDistanceMm函数封装发送命令和接收距离的逻辑。代码里用 HAL_UART_Transmit 发 0x55用 HAL_UART_Receive 等 2 字节返回HAL_StatusTypeDef Us100_ReadDistanceMm(uint16_t *dist_mm) { uint8_t cmd 0x55; uint8_t rx_buf[2]; HAL_StatusTypeDef status; // 发送测距命令 status HAL_UART_Transmit(huart1, cmd, 1, 100); if (status ! HAL_OK) { return status; } // 等待模块返回 2 字节距离超时 100ms status HAL_UART_Receive(huart1, rx_buf, 2, 100); if (status HAL_OK) { *dist_mm ((uint16_t)rx_buf[0] 8) | (uint16_t)rx_buf[1]; } return status; }这段代码的逻辑是发完命令后阻塞等待接收完成100ms 超时。模块正常返回时把高字节左移 8 位再与低字节做或运算得到毫米单位距离。如果返回值不是 HAL_OK调用方可以认为本次测距无效。注意这个函数不去判断 0xFFFF因为 0xFFFF 是模块正常通信后的“超量程”结果和通信超时是两回事应该交给上层去区分。在主循环里每 200ms 读一次距离并打印uint16_t distance 0; while (1) { if (HAL_UART_Transmit(huart2, (uint8_t*)start\r\n, 7, 100) ! HAL_OK) { // 串口2作为调试口发送失败可以忽略 } if (Us100_ReadDistanceMm(distance) HAL_OK) { // 这里先不做 0xFFFF 判断演示最小流程 } HAL_Delay(200); }这版代码对应的是阻塞方式适合先把链路跑通。它的问题也很明显HAL_UART_Receive 会占住 CPU直到收到 2 字节或超时为止。在只有测距任务的演示工程里没问题后续要接 OLED、电机或按键就需要换成第 5 章的状态机写法。3.4 强制进入串口模式的两种做法如果模块当前处于 GPIO 模式光发 0x55 是无效的。最简单可靠的验证方式是模块上电瞬间用跳线把 Trig 和 Echo 同时接到 3.3V保持 1 秒后再断开然后正常接回 MCU。这种方式适合实验室确认模块没问题。产品化一点的常见做法是用 MCU 的一个 GPIO 控制模块 VCC通过三极管或 MOS 管实现上电时序先把 USART1 的 TX/RX 两个脚配置为普通推挽输出并拉高再给模块上电模块检测到两个脚都为高就会进入串口模式。等延时结束后再把两个脚重新配置为 UART 复用模式。核心代码如下void US100_ForceUartMode(void) { GPIO_InitTypeDef gpio {0}; // 1. 先把 PA9/PA10 配置为普通输出并拉高 gpio.Pin GPIO_PIN_9 | GPIO_PIN_10; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9 | GPIO_PIN_10, GPIO_PIN_SET); // 2. PA1 输出高电平三极管导通US100 上电 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); HAL_Delay(100); // 3. 恢复为 UART 模式 gpio.Pin GPIO_PIN_9; gpio.Mode GPIO_MODE_AF_PP; HAL_GPIO_Init(GPIOA, gpio); gpio.Pin GPIO_PIN_10; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, gpio); }这段代码做了一个“先拉高、再上电、最后切回串口”的时序。第 3 步重新配置引脚后USART1 的收发功能才恢复正常。如果你的模块 VCC 一直由电源供电而不是由 MCU 控制那么程序里再怎么做都无法让已上电的模块重新选模式这是硬时序问题不是代码问题。4. 上手调通 US100 串口测距串口助手、数据校验与坑点代码写完后真正费时间的是调通环节。很多问题从单片机这边看是“模块没返回”实际是模块工作模式错了或者上位机串口助手设置不对。这一章把验证顺序和常用排查手段串一遍。4.1 先用 PC 串口调试助手验证模块真实行为不要一上来就把 US100 接到 STM32先用 USB 转 TTL 模块在电脑上验证能省掉一半的怀疑。常见 USB 转 TTL 芯片是 CH340 或 FTDI系统里装好驱动后会出现一个 COM 口。接线如下USB 转 TTLUS100TXDTrigRXDEcho3.3VVCCGNDGND然后打开串口调试助手选择波特率 9600数据位 8停止位 1无校验。发送区输入55勾选“HEX 发送”接收区也切到“HEX 显示”。如果模块正常发送一次 0x55 后接收区会显示两个字节。比如把模块对准 20cm 处的靶面返回00 C8换算后就是 200mm。这一步能同时验证三件事模块是否处于串口模式、9600 波特率是否匹配、USB 转 TTL 的 TX/RX 有没有接反。如果收不到先把 TXD 和 RXD 对调再试一次再不行就用万用表量模块 VCC 引脚电压确认不是供电问题。4.2 通过重映射 printf 把测距结果打印到第二个串口在 STM32 工程里USART1 已被 US100 占用调试信息就放到 USART2。CubeMX 里再开一个 USART2波特率设为 115200用于连接电脑的 USB 转 TTL。然后重定向 printf 到 USART2#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart2, (uint8_t *)ch, 1, HAL_MAX_DELAY); return ch; }MDK 工程里记得在 Options 的 Target 页面勾选 “Use MicroLIB”否则 printf 重定向会因为半主机模式而卡死。这样在主循环里直接写uint16_t dist 0; if (Us100_ReadDistanceMm(dist) HAL_OK) { printf(dist:%u mm\r\n, dist); } else { printf(timeout\r\n); }烧录后电脑上用另一个串口调试助手打开 USART2 对应的 COM 口就能看到连续的距离值。调 US100、写日志、串口下载各用各的口排查时不会互相干扰。4.3 返回值的三层判断超时、超量程、正常US100 的数据返回有几种情况不能只用“收到数据”来判断成功。建议在应用层做三层判断情况表现处理建议通信超时HAL_UART_Receive 返回 HAL_TIMEOUT检查接线、供电、模块状态超量程返回 0xFFFF目标太远、处于盲区或反射面太弱正常返回值小于 0xFFFF单位毫米对应代码if (Us100_ReadDistanceMm(dist) ! HAL_OK) { printf(uart timeout\r\n); } else if (dist 0xFFFF) { printf(out of range\r\n); } else { printf(distance:%u mm\r\n, dist); }0xFFFF 是模块固件约定在无法得到有效回波时回传的占位值它代表“测量失败”不是真实距离。如果目标离模块小于约 2cm模块进入盲区也可能返回 0xFFFF 或跳动值安装时要把传感器和目标面留出余量。4.4 高频出现的四个坑坑一模块根本没进串口模式。表现是发 0x55 后所有命令石沉大海。解决方法是回到 3.4 节强制用跳线或受控电源让模块识别串口模式。坑二CH340 或 FTDI 驱动装好但串口调试助手还是打不开 COM 口。先检查设备管理器里端口号确认没被其他程序占用如果提示“设备被占用”把串口助手关掉重开USB 线重插一次。坑三数据跳变很大。先看模块供电是否稳定很多 3.3V 是从开发板 LDO 出来后直接用的负载变化会引起纹波。在模块 VCC 和 GND 之间并联一个 100uF 电解电容和一个 0.1uF 陶瓷电容通常能改善。坑四串口烧写失败。如果你把 US100 接在 USART1 上又用 USART1 通过串口下载程序模块会干扰 ISP 下载时序典型现象是“连接芯片失败”。下载时拔掉模块的 RX/TX 两根线或者改用 SWD 下载最省事。这也是很多 STM32 开发者在调试“串口烧写失败”时忽略的外设占用问题。4.5 快速验收统计 50 次测量的抖动功能跑通后做一个简单验收把目标固定在 30cm 处连续测 50 次统计最小、最大、平均和抖动范围。代码可以直接放在主循环里一次性执行#define SAMPLE_CNT 50 uint32_t total 0; uint16_t min_val 0xFFFF; uint16_t max_val 0; uint16_t val 0; for (uint8_t i 0; i SAMPLE_CNT; i) { if (Us100_ReadDistanceMm(val) HAL_OK val ! 0xFFFF) { total val; if (val min_val) min_val val; if (val max_val) max_val val; } HAL_Delay(30); } printf(min%u max%u avg%lu jitter%u\r\n, min_val, max_val, total / SAMPLE_CNT, max_val - min_val);jitter 就是max_val - min_val。固定目标下US100 的抖动一般能控制在几个毫米到十几毫米具体取决于供电、目标表面和安装振动。如果 jitter 连续跑出几十毫米优先查电源和反射面而不是继续调软件。5. 非阻塞实现用状态机把 US100 串口触发测距收紧第 3 章的阻塞版适合先跑通但真正放到项目里主循环被 HAL_UART_Receive 卡住后OLED 刷新、按键扫描、电机控制都会出现可见卡顿。换一种方式用串口中断和状态机把测距变成后台任务。5.1 用 UART 中断把距离读取变成后台任务先定义几个状态再通过 HAL 的发送完成回调和接收完成回调推进状态typedef enum { US100_STATE_IDLE, US100_STATE_TX, US100_STATE_RX, US100_STATE_DONE } US100_State; static US100_State us100_state US100_STATE_IDLE; static uint8_t us100_cmd 0x55; static uint8_t us100_rx_buf[2]; static volatile uint8_t us100_measure_ready 0; static uint16_t us100_distance_mm 0; void US100_StartMeasurement(void) { us100_measure_ready 0; us100_state US100_STATE_TX; HAL_UART_Transmit_IT(huart1, us100_cmd, 1); } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { us100_state US100_STATE_RX; HAL_UART_Receive_IT(huart1, us100_rx_buf, 2); } } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { us100_state US100_STATE_DONE; us100_measure_ready 1; } }回调里不处理数据只改状态。真正取数据在主循环的轮询函数里做void US100_Poll(void) { switch (us100_state) { case US100_STATE_DONE: if (us100_measure_ready) { us100_distance_mm ((uint16_t)us100_rx_buf[0] 8) | (uint16_t)us100_rx_buf[1]; } us100_measure_ready 0; us100_state US100_STATE_IDLE; break; default: break; } }主循环里每 100ms 发起一次测量间隔由 tick 控制static uint32_t last_measure_ms 0; if (HAL_GetTick() - last_measure_ms 100) { last_measure_ms HAL_GetTick(); US100_StartMeasurement(); } US100_Poll();主循环可以在等待测量结果的 100ms 里继续跑显示刷新、按键扫描等任务不会再被串口接收阻塞。5.2 加一个超时看门狗防止模块卡死中断方式有个隐患如果模块在测量过程中突然掉线RX 等待永远不会完成状态一直停在 US100_STATE_RX下一次发送命令可能因为串口忙而失败整个测距流程就卡住了。解决方法是给 RX 状态增加一个超时判断void US100_Poll(void) { switch (us100_state) { case US100_STATE_RX: if (HAL_GetTick() - last_measure_ms 300) { HAL_UART_AbortReceive(huart1); HAL_UART_AbortTransmit(huart1); us100_state US100_STATE_IDLE; us100_measure_ready 0; } break; case US100_STATE_DONE: if (us100_measure_ready) { us100_distance_mm ((uint16_t)us100_rx_buf[0] 8) | (uint16_t)us100_rx_buf[1]; } us100_measure_ready 0; us100_state US100_STATE_IDLE; break; default: break; } }300ms 没收到完整数据就调用 HAL_UART_AbortReceive 和 HAL_UART_AbortTransmit 清掉当前串口事务回到 IDLE等待下一次定时启动。这样即使模块被拔掉或线松动系统也能自动恢复测距不需要人工复位。把超时处理放进状态机后US100 就不再需要阻塞等待整个系统的测距节奏由 tick 驱动后续接 OLED 或电机控制也不会因为超声波测量打乱时序。本文还有配套的精品资源点击获取
返回列表