ARTICLE DETAIL

资讯详情

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

STM32 US100串口超声波测距实战:从协议解析到状态机实现

STM32 US100串口超声波测距实战:从协议解析到状态机实现 简介面向STM32嵌入式开发者与超声波测距学习者的完整工程包围绕US100模块串口触发模式实现非接触测距可应用于物联网终端、自动化设备与机器人避障等嵌入式场景。资源共353个文件、14.43MB内容覆盖C源码与头文件、Keil工程配置文件uvprojx/uvoptx、编译中间文件o/d/crf/map/lst、可直接烧录的HEX/AXF固件另含HTM说明页、PDF用户手册和Word文档目录层级明确便于渐进式学习。目前已有181人学习适合正在调测US100或希望从PWM触发改为串口触发的开发者。包内完整展示了STM32的UART初始化、串口中断接收、回波计时与距离换算的完整代码并包含模块参数调整、抗干扰处理、不同波特率配置说明及原理图资料有助于透彻理解超声波传播速度计算与异步测量机制并可快速迁移到其他MCU平台。1. US100 串口触发测距把测距读成一帧串口字节用 STM32 做超声波测距大多数人的第一反应是照搬 HC-SR04 的套路GPIO 拉低触发引脚再用定时器输入捕获去量回波高电平的宽度最后除以 58 得到厘米数。这套方法在裸机 while 循环里没问题一旦工程里同时跑着编码器中断、蓝牙透传和 OLED 刷新中断响应抖动几十微秒测距结果就开始跳。换成 US100 并切到串口触发模式之后测距过程彻底变简单STM32 串口发一个字节 0x55模块内部自己完成超声波发射、回波检测和时间换算最后从串口返回两个字节的距离值单位是毫米连除法都不用写。本文要拆的正是这个压缩包里的 STM32F103 标准外设库工程重点看串口模式如何选、定时器在这个方案里真正承担什么职责、串口收帧状态机怎么写以及实测中常见的数据毛刺从哪儿来。2. US100 串口帧协议与 STM32 UART 参数匹配2.1 先确认硬件处于 UART 模式而不是 GPIO 模式US100 模块的引脚上有一个 MODE 控制位它直接决定模块对外暴露的是电平接口还是串口接口。MODE 接高电平时模块进入 UART 串口模式原来用于 GPIO 触发的 TRIG 和 ECHO 引脚退化为串口的 TX/RXMODE 悬空或接低电平时模块工作在与 HC-SR04 类似的 GPIO 电平触发模式需要单片机精确控制触发脉宽并用输入捕获测量回波高电平时间。压缩包里描述的需求明确是 us-100 串口触发所以硬件连接时 MODE 引脚必须接 VCC否则即便串口初始化完全正确发出去的 0x55 也不会被模块当作串口指令解释。这个 PIN 模式差异是排错时最先该确认的环节。很多人在调试时发现串口发 0x55 没有返回第一反应是检查波特率和接线但问题往往出在模块并没有进入串口模式。检查方法很简单拿万用表量 MODE 引脚对地电压如果是高电平说明工作模式正确如果是 0V 则先解决硬件通路。确认模式之后再考虑另一件事US100 的串口指令不是任意字节它只有两条固定指令返回帧结构也完全不同后面这一节把指令和返回帧放在一起看。2.2 0x55 触发测距与 0x50 读取温度返回帧长不一样US100 在 UART 模式下只有两个常用命令返回帧结构如下表发送指令功能返回帧返回数据含义0x55触发一次测距两字节高字节在前低字节在后单位毫米0x50读取模块内部温度一字节温度整数值单位摄氏度0x55 指令发出后模块经历发射、等待回波、内部计时这一串过程最后从串口返回两个字节。需要注意这两个字节是模块已经算好的距离值不是原始时间差所以不需要再除以 2也不用乘声速直接拼成一个 uint16_t 就是毫米数。0x50 指令则返回一个字节温度这个温度值可以用于第 4 章的声速补偿也可以用于粗略判断环境温度。两条指令的返回帧长度不同意味着接收端不能简单用一个收满 N 个字节的固定计数器必须根据上一次发送的指令来决定解析边界这正是后面状态机章节要处理的内容。从测量周期上看US100 完成一次测距的时间与量程有关量程 4.5m 时最长响应约 30ms 左右。因此工程里轮询间隔建议不小于 60ms我用 80ms 做默认周期。如果发完 0x55 后 100ms 还没收到任何返回字节基本可以判定模块无响应或通信链路异常这时要主动把距离置为无效值而不是让主程序一直等中断。2.3 波特率自动识别机制与 UART 初始化顺序US100 在 UART 模式上电后支持 9600 和 115200 两种常见波特率模块通过检测上电后接收到的第一个字节自动完成速率匹配。这个自动识别机制带来一个容易被忽视的初始化顺序问题如果把模块和 STM32 同时上电STM32 紧接着发送 0x55 作为第一次测距触发那么这一帧实际上会被模块当作波特率识别字节消费掉不会产生任何距离返回值。因此稳妥的启动流程是模块上电后至少等待 1s待模块内部完成初始化再发送第一帧 0x55。波特率选 9600 还是 115200取决于系统对实时性的要求。单次测距的通信数据量极小返回只有两字节9600 波特率下完整接收两字节约需 2.1ms115200 下约 0.17ms对整个 60ms 量级的测量周期来说都不是瓶颈。但 115200 对信号质量更敏感模块与单片机之间用超过 20cm 的杜邦线时旁边一旦有电机 PWM 或舵机线偶发的帧错误会明显增加。下表给出了取舍参考波特率两字节返回耗时适用场景干扰下的表现9600约 2.1ms线束较长、电机干扰大帧错误率低115200约 0.17ms近距离排线、测量频率高易受边沿抖动影响就这个工程而言我一般把 USART2 的波特率配成 9600这样既能保证 8Hz 以上的测距频率又能留出抗干扰余量。STM32 侧的代码用标准外设库初始化 USART2引脚选 PA2/PA3完整配置如下void US100_UART_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 使能 GPIOA 与 USART2 外设时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; // PA2 - USART2_TX GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_3; // PA3 - USART2_RX GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 9600; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_TX | USART_Mode_RX; USART_Init(USART2, USART_InitStructure); USART_Cmd(USART2, ENABLE); USART_ClearFlag(USART2, USART_FLAG_RXNE); }代码里 PA2 配置为复用推挽输出PA3 配置为浮空输入这是标准外设库下 UART 引脚最常见的组合。USART_InitStructure 里的数据位 8、停止位 1、无校验与 US100 模块默认的 8N1 格式完全对齐。初始化接近末尾处的 USART_ClearFlag 是很多人会漏掉的细节模块上电瞬间如果在 RX 线上产生毛刺电平USART 硬件可能会误置 RXNE 标志清一次标志可以避免第一个中断帧读到脏数据。如果使用的是 Keil MDK 工程记得先在芯片包安装管理里确认已经装好 STM32F1 系列支持包否则编译这个标准外设库工程时会出现器件型号不匹配的报错。3. 基于 STM32 定时器与串口中断的测距实现3.1 stm32f10x_tim.c 在工程里的真正职责接收超时看门狗打开压缩包会看到 stm32f10x_tim.c这是 ST 标准外设库的定时器驱动文件很多人会误以为它在工程里用于测量超声波回波时间。实际上在 US100 的串口模式下模块已经把所有测时逻辑内化STM32 的定时器根本不需要做输入捕获。这个文件被链接进来是因为工程需要用定时器产生一个周期中断作为串口接收超时的看门狗。看门狗的业务逻辑是如果发完 0x55 之后 100ms 内没有收到完整返回帧就认为测量失败。原因很现实——当障碍物超出量程或者模块没有正确识别触发指令时USART 接收中断永远不会触发程序如果没有超时兜底会一直停在那次测量状态里后续所有轮询都被卡死。下面的代码展示了一个 20ms 周期的 TIM3 中断配合一个计数器变量实现超时判定static volatile uint16_t us100_timeout_cnt 0; static volatile uint16_t us100_distance 0xFFFF; // 默认无效值 #define US100_TIMEOUT_MS 100 void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); us100_timeout_cnt; if (us100_timeout_cnt US100_TIMEOUT_MS / 20) { us100_distance 0xFFFF; // 超时清除距离值 } } }定时器配置为 20ms 更新中断us100_timeout_cnt 每进一次中断就加 1。当计数超过 5 次即累计 100ms 没有收到有效帧时把距离改成 0xFFFF 表示无数据。这里把变量声明为 volatile是因为它们会在中断上下文和主循环同时被访问不加 volatile 可能被编译器优化成寄存器副本导致主循环读到过期值。真正触发一次有效接收后应该在串口中断里把 us100_timeout_cnt 清零让超时计时从最新一帧开始重新计数这一点对应 3.2 节代码中的注释位置。3.2 用状态机代替固定计数正确完成两字节组帧US100 的返回帧只有两字节但帧边界并不总是干净的。假如上一次发送的是 0x50 温度指令模块只返回一字节温度如果接收端用收满两字节就是距离的简单计数器温度字节就会被错当成距离高字节后续所有解析全部错位。所以接收逻辑用状态机实现而不是单纯计数。#define FRAME_IDLE 0 #define FRAME_HIGH 1 #define FRAME_LOW 2 static uint8_t us100_rx_state FRAME_IDLE; static uint8_t us100_rx_high 0; void USART2_IRQHandler(void) { uint8_t data; if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { data USART_ReceiveData(USART2); us100_timeout_cnt 0; // 收到字节即刷新超时计数 switch (us100_rx_state) { case FRAME_IDLE: // 无论收到什么字节先作为距离高字节缓存 us100_rx_high data; us100_rx_state FRAME_HIGH; break; case FRAME_HIGH: // 收到第二个字节与高字节拼成距离值 us100_distance ((uint16_t)us100_rx_high 8) | data; us100_rx_state FRAME_LOW; break; case FRAME_LOW: // 如果前两字节是温度数据这里会被跳过 us100_rx_state FRAME_IDLE; break; default: us100_rx_state FRAME_IDLE; break; } } }这个状态机的思想是把任何一帧的第一个字节都当作高字节缓存第二个字节到达后立即拼帧。即使模块返回的是单字节温度数据状态机也只会把它当作高字节缓存起来而不会误拼出一个错误的距离值。因为 0x50 温度指令和 0x55 测距指令不会在同一个轮询周期内同时发出所以单字节温度帧残留下来不会污染下一次测距结果。实际工程中我习惯把 USART2 的 NVIC 优先级设为高于主循环里的其他软中断确保收帧不被长时间打断但这个工程里没有复杂的中断嵌套需求默认优先级就够用。说到工程文件压缩包里出现的 Template.axf 是 Keil 编译链接后生成的 ARM 可执行文件说明这个工程已经在本机编译通过多个 uvguix 文件是不同电脑打开工程时保存的 GUI 布局记录并没有实际用途。重新移植工程时可以先运行一遍压缩包里的 keilkilll.bat它会自动清理 axf、o、d 这类编译产物避免带着旧目标文件直接烧录导致改了代码却不生效的假象。3.3 业务层距离判断量程、盲区与无效值组帧完成后距离值已经是一个 uint16_t但对业务代码来说还不能直接用。US100 的标称量程是 2cm 到 450cm超出这个区间的读数要么是近场盲区噪声要么是超量程后的残留值要么是超时后的 0xFFFF。业务层需要把这些情况过滤成统一语义下面的函数把毫米值转换成厘米并对不在有效区间的数据返回 0调用方用 0 表示本次测量不可信。uint16_t US100_GetDistanceCm(void) { if ((us100_distance 20) (us100_distance 4500)) { return us100_distance / 10; // mm - cm } return 0; }为什么要单独做这个判断而不是让上层直接读 us100_distance因为不同的无效值有不同的含义调试时它们的可读性也不一样。0xFFFF 表示通信超时2cm 以下的数值表示障碍物已经进入探头盲区450cm 以上的数值可能是模块内部测量上限附近的毛刺。下表把各区间与处理建议对应起来方便代码 review 时一眼看明白距离值范围含义建议处理0xFFFF通信超时或无回波本轮不更新障碍物距离小于 20mm近场盲区按最小安全距离处理20mm 4500mm正常量程直接参与业务逻辑大于 4500mm超出稳定量程视为无效忽略在业务循环里调用也很简单典型写法是触发后延时 60ms再读一次距离值while (1) { US100_TriggerMeasure(); DelayMs(60); uint16_t dist US100_GetDistanceCm(); if (dist ! 0) { printf(distance: %d cm\r\n, dist); } DelayMs(20); }这里有个节奏问题需要强调printf 输出本身会占用一部分串口时间如果用的是 USART1 调试输出而 US100 挂在 USART2 上两个串口互不干扰但如果图省事把调试输出和 US100 接在同一个串口上printf 发出的字符会被模块当成指令解析产生不可预料的返回帧。工程上尽量把调试串口和传感器串口分开。4. US100 实测校准温度补偿与回波干扰处理4.1 环境温度对声速的影响到底有多大US100 模块内部默认按 331.4m/s 的声速计算距离这个默认值对应 0°C 干燥空气。实际空气中声速与温度近似线性关系公式是 v 331.45 0.607 × TT 为摄氏温度。以 25°C 室温为例实际声速约 346.6m/s比默认值高约 4.6%测量 100cm 距离时会产生约 4.6cm 的系统性偏差。换句话说不补偿温度的话模块读出的距离会比真实值偏大因为模块用更慢的声速推算出的时间差对应了更长的距离。如果项目在室内空调环境使用温差范围小这种偏差可以忽略但室外小车或农业自动化场景昼夜温差可能超过 20°C此时就需要补偿。US100 的 0x50 指令可以直接读模块内部温度补偿逻辑非常简单float us100_speed_by_temp(uint8_t temp_celsius) { return 331.45f 0.607f * temp_celsius; }读出温度后把测距结果乘上 (默认声速 / 实际声速) 的修正系数即可。补偿后的精度在 ±0.5cm 以内前提是传感器周围没有热源直吹——模块背部长时间贴着电机驱动板时内部温度会偏高测出来的声速修正量反而引入额外误差。4.2 障碍物边沿的多回波毛刺如何过滤实际项目中常见的另一类误差不是温漂而是超声波波束打到物体边沿时产生的跳变。比如小车正对一根立柱超声波波束中心可能扫过柱子的侧面棱线一部分反射波提前返回一部分正常返回模块内部可能取到异常短的路径。这类毛刺的特点是单次跳变幅度大但不会连续出现滑动窗口滤波比简单平均更有效。我常用的做法是连续采 5 次去掉最大值和最小值再取中间 3 次的平均值这样一个孤立的跳变点会被直接剔除而连续移动的障碍物距离仍然能跟上。4.3 用标尺对比验证整条链路验证 US100 测距是否准确最直接的办法是把模块固定正前方放一块至少 A4 纸面积的硬纸板作为反射面用卷尺量出真实距离再与串口输出的厘米值对比。建议从 20cm 开始每 20cm 记录一组数据做到 200cm 共 10 组。正常情况下误差应控制在 ±1cm 内。如果 60cm 以内误差明显检查模块高度是否让超声波波束指向了地面如果测试距离越远误差越大优先确认环境温度和默认声速的关系。最后把两组数据做成表格贴进项目文档后续改代码时直接对照这个基线比反复看串口日志直观得多。本文还有配套的精品资源点击获取
返回列表