零知派——STM32智能小车系列教程(二):HC-SR04超声波测距 + SG90舵机云台原理与调试
引言智能小车要实现的功能很多——避障、跟随、贴边、灭火、直线行驶每一个模块单独调试起来都有自己的难点。如果一开始就把所有模块塞进一个工程里联调出了问题很难分清到底是传感器硬件的问题、电平极性的问题还是上层控制算法的问题所以这一系列教程的思路是先把每个功能模块单独拎出来配一个不依赖其他硬件的最小测试程序把模块本身调通调透再回头看它在完整项目里是怎么被使用的目录一、引言二、超声波测距模块硬件原理2.1 超声波发送接收2.2 超时保护机制三、舵机云台控制原理3.1 PWM信号与角度3.2 舵机云台使用方式四、硬件接线说明4.1 引脚定义4.2 接线方案表4.3 连接示意图五、独立测试程序5.1 调试流程5.2 视频演示六、项目超声波-舵机云台功能实现6.1 SonarServo 底层封装6.2 AvoidBehavior 超声波避障6.3 NearWallBehavior 贴边行驶6.4 MagicBehavior 测距跟随6.5 系统流程图七、常见问题解答(FAQ)Q1贴边模式PID参数应该怎么从零开始整定Q2避障模式中为什么 IR 的优先级比超声波高Q3超声波模块测距时为什么建议测量周期 ≥ 60ms一、引言这是系列的第二篇专门讲一套很有意思的硬件组合HC-SR04 超声波测距传感器 SG90 舵机云台进行三个完全不同的行为模式——避障模式遇到障碍物自动绕开贴边行驶模式沿着墙壁保持固定距离前进魔术手模式小车跟着手掌前后移动而前进后退类似“魔术悬浮”效果这三个模式共用同一套物理硬件区别只在于上层控制算法怎么去解读测距数据、怎么去调度舵机转角。这是嵌入式项目里很常见也很值得学习的一种设计思路硬件复用逻辑分层。把这套硬件单独拎出来调通再看三种算法分别是怎么在它之上搭建的会比孤立地看某一个模式的代码更容易理解整体设计意图避障模式加入了舵机左右扫描和 IR 红外传感器的紧急避障形成了三传感器融合方案。贴边模式用到了完整的 PID 闭环控制魔术手模式利用舵机固定角度下的连续测距实现距离跟随。这三个模式共用底层硬件驱动但上层逻辑完全不同非常适合用来演示分层设计的工程思路这个系列教程拆成以下几篇篇次内容第一篇已完成五路循迹模块TCRT5000 LM393/LM339第二篇本文HC-SR04 超声波测距 SG90 舵机云台三模式应用第三篇红外双目跟随模块物体跟随功能第四篇霍尔编码器测速与直线行驶 PID含 PID 自整定第五篇DHT11 温湿度、火焰传感器、风扇灭火模块第六篇蓝牙/串口双通道通信协议设计终篇整车项目架构总览把前面六篇串起来讲整体设计思路二、超声波测距模块硬件原理HC-SR04 是一种基于声波反射时间差测距的传感器模块。左侧标有T的探头负责发出超声波脉冲右侧标有R的探头负责接收反射回来的超声波2.1 超声波发送接收模块背面集成了发射驱动电路提升信号幅度驱动压电探头、接收放大电路将微弱回波信号放大、比较器电路与阈值比较输出数字电平以及定时逻辑控制Trig触发和Echo脉冲宽度输出①HC-SR04 完整时序图HC-SR04 的通信时序是本模块使用中最关键的部分理解时序才能写出可靠的驱动代码时序说明主控给 Trig 一个 ≥10μs 的高电平脉冲模块自动发出 8 个 40kHz 的超声波脉冲模块等待回波Echo 引脚拉高收到回波后 Echo 引脚拉低高电平持续时间 超声波往返时间两次测量之间间隔 ≥60ms避免发射信号与回波信号相互干扰②距离公式推导常温20°C下声速Echo 高电平时间为 TT T 微秒超声波走过的总路程去回单程距离障碍物到传感器的实际距离这就是为什么代码里直接用dur / 58来计算距离是对公式的工程化近似误差小于0.5%完全在传感器本身±3mm的精度范围内③内部电路原理图发射电路主控芯片产生 40kHz 方波信号、通过三极管或 MOS 管驱动放大、驱动超声波发射探头T发出 40kHz 声波接收电路接收探头R将声波信号转换为微弱的电信号、经过多级放大电路放大、经过比较器整形为数字信号、送入主控芯片判断是否有回波2.2 超时保护机制当测距目标超出量程 400cm或者探头朝向完全吸音的软质材料时超声波不会反射回来Echo引脚会一直保持低电平程序如果没有超时保护会无限等待阻塞整个loop()主循环int SonarServo::getDistance() { digitalWrite(UltraTrigPin, LOW); delayMicroseconds(2); digitalWrite(UltraTrigPin, HIGH); delayMicroseconds(10); digitalWrite(UltraTrigPin, LOW); const unsigned long TIMEOUT_US 35000UL; // 35ms超时 ≈ 6m量程 unsigned long t0 micros(); // 等待Echo变高超时则视为无效 while (!digitalRead(UltraEchoPin) (micros() - t0) TIMEOUT_US); if (digitalRead(UltraEchoPin) LOW) return -1; // 超时返回无效值 // 测量高电平持续时间 t0 micros(); while (digitalRead(UltraEchoPin) (micros() - t0) TIMEOUT_US); unsigned long dur micros() - t0; int cm (int)(dur / 58); return (cm 400) ? -1 : cm; // 超出量程也返回-1 }TIMEOUT_US 35000 对应约 35ms换算距离约 6m超出HC-SR04实际量程任何有效测量都会在超时之前完成确保函数调用时间有上界①测距范围与盲区HC-SR04 标称的有效测距范围大约在2cm 到 400cm之间近距离盲区约 2cm 以内发射和接收用的是同一类型的超声波换能器发射脉冲结束后接收电路需要一定的恢复时间太近的距离反射波会和发射的余振混在一起分辨不清楚远距离限制超过 400cm受限于超声波在空气中传播时的衰减太远反射回来的信号强度不足以被稳定识别②检测角度与盲点HC-SR04 的超声波探头有一个发射锥角通常在15 度左右它检测的不是一个点而是一个扇形区域内的最近反射物体。如果赛道两侧或者前方有形状不规则、表面凹凸或者倾斜角度较大的障碍物超声波反射方向会发生偏折导致测距结果不准甚至完全测不到。这也是为什么单纯依赖超声波做避障时常常需要配合舵机左右摆动多次测量取最近值三、舵机云台控制原理SG90 内部集成了直流电机、减速齿轮组和位置反馈电位器。与普通直流电机给电就转、断电就停不同舵机的核心能力是角度伺服控制——给一个指令角度舵机会自动转动到该角度并通过内部闭环保持在那里3.1 PWM信号与角度舵机依靠PWM信号的脉宽而非占空比、也非频率来确定目标角度。标准控制信号参数①PWM信号规范SG90 舵机接收的 PWM 信号规范如下参数值PWM 频率50HzPWM 周期20ms脉宽范围0.5ms ~ 2.5ms②脉宽与角度的对应关系脉冲宽度对应角度说明0.5 ms0°最小角度1.5 ms90°中位居中2.5 ms180°最大角度③舵机内部闭环控制原理SG90 舵机内部包含一个完整的闭环控制系统给定一个 PWM 脉宽舵机就会转到对应的角度并保持住直到收到新的脉宽指令。外力强行转动舵机输出轴时舵机会主动抵抗并回到设定角度3.2 舵机云台使用方式项目里舵机不是用来驱动机械臂或者轮子转向而是顶着超声波传感器做一个简单的云台舵机转到某个角度超声波就朝那个方向测距这种“测距前先转舵机、转到位再读数”的用法本质上是用时间分割的方式实现了一个简易的扇形扫描雷void SonarServo::sweepTo(int to, int stepMs) { static int _current SERVO_DEFAULT_POS; int step (to _current) ? 1 : -1; while (_current ! to) { _current step; _servo.write(_current); delay(stepMs); } }注意事项舵机每转到一个新角度都需要等待一定时间约 100~200ms让机械结构稳定下来再触发超声波测距否则转动过程中的震动会让测距结果出现明显抖动项目 sweepTo() 函数中内置了步进延时四、硬件接线说明4.1 引脚定义①HC-SR04 对外提供四个引脚引脚名称方向功能本项目接法1VCC输入电源正极5V接 5V2Trig输入触发信号输入接 D313Echo输出回波信号输出接 D304GND—电源地接 GND注意Trig 和 Echo 方向相反——Trig 是主控发给模块的触发指令Echo 是模块回传给主控的测距结果不要接反②SG90 引出三根线线色名称功能本项目接法棕色/黑色GND电源地接 GND红色VCC电源正极5V接 5V橙色/黄色SIGPWM 控制信号接 D15ServoPWM14.2 接线方案表模块引脚零知派迷你板引脚代码常量说明HC-SR04VCC5V—超声波模块需要 5V 供电HC-SR04GNDGND—共地HC-SR04TrigD31UltraTrigPin触发信号输出脉冲HC-SR04EchoD30UltraEchoPin回波信号读取高电平脉宽SG90 舵机红线 (VCC)5V—舵机供电建议独立供电SG90 舵机棕/黑线 (GND)GND—共地SG90 舵机橙/黄线 (SIG)D15ServoPWM1PWM 控制信号请注意超声波模块和舵机模块直插小车扩展板超声波引脚和舵机接口P1超声波模块的VCC和GND方向请勿接反否则导致模块烧毁4.3 连接示意图五、独立测试程序下面这个程序只依赖超声波模块和舵机两路硬件不需要电机、循迹等其他模块。引脚定义与主项目pinsdefine.h完全一致可以直接在零知派迷你板上单独烧录测试/************************************************************************************** * 文件: Sonar_Servo_Standalone_Test.ino * 作者零知实验室深圳市在芯间科技有限公司 * 功能HC-SR04超声波测距 SG90舵机云台 独立测试程序 * 引脚定义与主项目 pinsdefine.h 完全一致不修改任何引脚 * 单独烧录通过串口监视器实时观察角度-距离对应关系 **************************************************************************************/ #include Servo.h // ── 引脚与主项目 pinsdefine.h 保持一致── #define UltraTrigPin 31 #define UltraEchoPin 30 #define ServoPWM1 15 #define SERVO_CENTER 80 // 实车中位角 Servo g_servo; int getDistance() { // 1. 拉低至少2μs确保触发信号初始状态干净 digitalWrite(UltraTrigPin, LOW); delayMicroseconds(2); // 2. 拉高至少10μs触发一次测量 digitalWrite(UltraTrigPin, HIGH); delayMicroseconds(10); digitalWrite(UltraTrigPin, LOW); // 3. 等待 Echo 变高含超时保护35ms const unsigned long TIMEOUT_US 35000UL; unsigned long t0 micros(); while (!digitalRead(UltraEchoPin) (micros() - t0) TIMEOUT_US); if (!digitalRead(UltraEchoPin)) { Serial.println(F([SONAR] 超时无回波障碍物超出量程或未接线)); return -1; } // 4. 测量 Echo 高电平持续时间 t0 micros(); while (digitalRead(UltraEchoPin) (micros() - t0) TIMEOUT_US); unsigned long dur micros() - t0; // 5. 换算为厘米距离(cm) dur(μs) / 58 int cm (int)(dur / 58); if (cm 400) { Serial.println(F([SONAR] 超量程400cm)); return -1; } return cm; } void setup() { Serial.begin(115200); delay(200); pinMode(UltraTrigPin, OUTPUT); pinMode(UltraEchoPin, INPUT); g_servo.attach(ServoPWM1); g_servo.write(SERVO_CENTER); delay(500); // 等舵机转到中位稳定 Serial.println(F()); Serial.println(F( HC-SR04 SG90 云台独立测试程序 )); Serial.println(F( 串口波特率115200 )); Serial.println(F( 舵机将在 30°~150° 之间自动来回扫描 )); Serial.println(F( 用手在不同角度遮挡观察距离数值变化 )); Serial.println(F()); } void loop() { // 舵机从30°到150°逐步扫描每5°停留测一次距离 static int angle 30; static int stepDir 1; // 1 向右-1 向左 static bool sweeping true; g_servo.write(angle); delay(200); // 等舵机到位并消除振动 int dis getDistance(); // 格式化输出方便截图或做折线图 Serial.print(F([CLOUD] Angle)); Serial.print(angle); Serial.print(F(°\tDist)); if (dis 0) Serial.println(F(无效)); else { Serial.print(dis); Serial.println(F(cm)); } // 推进角度 angle stepDir * 5; if (angle 150) { angle 150; stepDir -1; } if (angle 30) { angle 30; stepDir 1; } delay(60); // 每次测量间隔≥60ms避免串扰 }5.1 调试流程①验证超声波基本功能先不装舵机只接HC-SR04固定对准一面墙观察串口输出的距离是否稳定同一位置多次读数偏差应在±2cm内②验证舵机基本功能先不接HC-SR04只接舵机烧录后观察舵机是否从30°开始做30°~150°的来回扫描确认转动方向和幅度是否符合预期③联合测试两者都接好用手在不同角度位置遮挡对比距离读数随角度的变化验证云台测距的完整功能观察串口输出内容5.2 视频演示零知派迷你板——HC-SR04 超声波云台扫描与测距测试本视频演示零知智能小车项目中 HC-SR04 超声波测距模块与 SG90 舵机云台的联合独立测试。舵机在 30°~150° 范围内往复扫描超声波模块在每个角度下测量前方障碍物距离串口监视器同步打印角度与对应距离数值六、项目超声波-舵机云台功能实现在 sc_perception.h/.cpp 中SonarServo 类把超声波测距和舵机转动这两个操作封装在一起封装的核心价值在于三个行为模式避障、贴边、魔术手都可以直接调用 g_sonar.getDistance() 和 g_sonar.sweepTo()不需要各自重复写驱动代码也不会出现两个行为模式同时操作同一个舵机对象的冲突因为同一时刻只有一个行为模式在运行6.1 SonarServo 底层封装SonarServo类封装了超声波测距和舵机控制两个底层操作对外提供三个公共方法class SonarServo { private: Servo _servo; public: SonarServo(); // 初始化舵机默认居中 int getDistance(); // 单次测距返回厘米数-1 表示超时 void sweepTo(int to, int stepMs 8); // 平滑转动到目标角度 void setAngle(int angle); // 直接设置角度 };getDistance() 实现核心是 35ms 超时保护和 -1 无效值返回内部位置记录设计void SonarServo::sweepTo(int to, int stepMs) { static int _current SERVO_DEFAULT_POS; // 静态变量记录当前角度 int step (to _current) ? 1 : -1; while (_current ! to) { _current step; _servo.write(_current); delay(stepMs); } }_current是一个static局部变量函数调用结束后不会销毁每次调用时都从上次停留的角度开始转动而不是从0°或某个固定角度重新开始sweepTo()逐度步进的方式比直接write(to)更平滑能有效减少舵机转动时的抖动6.2 AvoidBehavior 超声波避障避障模式的核心思路是一个状态机 三点扫描 IR 融合的决策系统①三点扫描策略const int AvoidBehavior::SCAN_ANGLES[3] {80, 130, 30}; // 前 左 右注意舵机 0° 朝右180° 朝左80° ≈ 正前方。if (millis() - _scanTs SCAN_STEP_MS) { int d g_sonar.getDistance(); if (d 0) _dist[_scanIdx] d; // 无效读数保持上次值 _scanIdx (_scanIdx 1) % 3; g_sonar.setAngle(SCAN_ANGLES[_scanIdx]); _scanTs millis(); }三个角度轮换扫描每SCAN_STEP_MS120ms切换一个角度②决策优先级优先级条件动作①IR 双侧都有触发立即后退②IR 左触发 左方超声波近大幅右转③IR 右触发 右方超声波近大幅左转④前方超声波近≤28cm停车→左右决策→转向⑤仅 IR 左触发右前进⑥仅 IR 右触发左前进⑦全无障碍自适应速度前进单点测距只能知道“前方有没有障碍”但不知道“障碍在左边还是右边”。三点扫描可以同时获得前方、左侧、右侧三个方向的距离信息决策更智能③前方障碍决策逻辑if (dF 0 dF BLOCK_CM) { bool irBias false; if (irL !irR) { irBias true; } // 左有IR→偏向右转 else if (irR !irL) { irBias false; } // 右有IR→偏向左转 else { irBias (dR dL); // 右边空间更大→向右转 } if (dL BLOCK_CM dR BLOCK_CM) { g_motor.goWithDirec(carBack); // 两侧都堵→后退 return; } irBias ? g_motor.goWithDirec(carRight) : g_motor.goWithDirec(carLeft); }IR 与超声波的互补关系传感器优势劣势超声波测距精准2~400cm响应慢有盲区锥角内检测IR 红外响应极快近距离可靠只能检测有无不能测距④自适应速度无障碍时根据前方距离调整速度if (dF 0 dF SLOW_CM) { int spd map(constrain(dF, BLOCK_CM, SLOW_CM), BLOCK_CM, SLOW_CM, 60, 140); g_motor.motorRun(spd, spd); } else { g_motor.goWithDirec(carForwardSlow); }前方越近在 28~50cm 区间速度越慢避免急刹车式的不平滑控制6.3 NearWallBehavior 贴边行驶贴边模式要解决的问题是让小车始终和一侧的墙壁保持固定距离前进——这是一个典型的闭环控制问题用PID来实现①左右扫描锁定墙侧void NearWallBehavior::onEnter() { g_sonar.sweepTo(30); delay(200); int dR 0; for (int i 0; i 3; i) { int d g_sonar.getDistance(); if(d0) dR d; delay(30); } dR / 3; g_sonar.sweepTo(150); delay(200); int dL 0; for (int i 0; i 3; i) { int d g_sonar.getDistance(); if(d0) dL d; delay(30); } dL / 3; _wallLeft (dL dR); // 选择距离更近的一侧贴墙 g_sonar.setAngle(_wallLeft ? 150 : 30); // 舵机锁定在该方向 }确定贴边方向后舵机就固定在那个角度不再转动循环测量到墙壁的距离②PID 闭环控制void NearWallBehavior::update() { int dis g_sonar.getDistance(); if (dis 0) dis (int)setpoint; dis constrain(dis, 0, (int)(setpoint * 2)); _input dis; _pid.Compute(); int lPwm _wallLeft ? (int)(basePwm - _output) : (int)(basePwm _output); int rPwm _wallLeft ? (int)(basePwm _output) : (int)(basePwm - _output); g_motor.motorRun(constrain(lPwm, -255, 255), constrain(rPwm, -255, 255)); }PID参数说明参数默认值作用Kp4.0比例项偏差越大修正越猛Ki0.02积分项消除稳态偏差Kd0.2微分项抑制振荡setpoint17cm目标距离basePwm70基础速度为什么贴边用PID三项而循迹只用PD两项对比项循迹PD贴边PID偏差类型瞬时传感器读值没有固定偏置持续测距有稳定状态的误差积分项作用积分容易在连续弯道过冲积分消除长时间行驶中的稳态偏差是否需要I不需要需要调试建议先用纯PKiKd0观察是否能大致维持设定距离逐步加I消除长时间运行后的稳态偏差最后加D抑制振荡6.4 MagicBehavior 测距跟随手掌在小车正前方移动小车跟着手掌的远近自动前进后退——手靠近就后退、手远离就前进void MagicBehavior::update() { int dis g_sonar.getDistance(); if (dis 0) { g_motor.stop(); return; } // 无效读数停车 int MAX_D FAR NEAR; // 18 13 31cm dis constrain(dis, 0, MAX_D); if (dis FAR) { // 手太远 → 前进距离越远速度越快 g_motor.motorRun(map(dis, FAR, MAX_D, 60, 120), map(dis, FAR, MAX_D, 60, 120)); } else if (dis NEAR) { // 手太近 → 后退距离越近速度越快 g_motor.motorRun(-map(dis, 0, NEAR, 120, 60), -map(dis, 0, NEAR, 120, 60)); } else { // 死区 NEAR(13cm) ~ FAR(18cm) → 停车 g_motor.stop(); } }①死区设计阈值值含义NEAR13cm太近 → 后退FAR18cm太远 → 前进死区13~18cm保持静止5cm 宽死区的作用是避免手稍微抖动一下小车就来回小幅度抽动给操作者一定的容错空间。②距离→速度映射场景映射效果距离 18cmmap(dis, 18, 31, 60, 120)越远走得越快最大 120距离 13cmmap(dis, 0, 13, 120, 60)越近退得越快最大 12013~18cm 之间停车容错死区这种渐进式响应手感比较自然不会出现突然加速或急停的突兀感6.5 系统流程图七、常见问题解答(FAQ)Q1贴边模式PID参数应该怎么从零开始整定A推荐顺序先置 Ki0、Kd0只用P控制从小值比如1.0开始逐步增大Kp观察小车能不能大致维持在目标距离附近来回摆动。找到能维持但有明显振荡的Kp后加入Kd从0.1开始抑制振荡最后加入小量Ki0.01~0.05消除长距离行驶中的稳态偏差。全程可以通过蓝牙热发WKP/WKI/WKD在不重烧的情况下实时调整Q2避障模式中为什么 IR 的优先级比超声波高AIR 传感器的响应速度比超声波快得多适合处理紧急情况如超声波盲区内的障碍、侧方突然出现的障碍。而超声波虽然响应慢但能提供精确的距离数据适合做常规避障决策。这种IR 超声波融合的设计让系统兼顾了快速响应和精确测距Q3超声波模块测距时为什么建议测量周期 ≥ 60msA为了避免发射信号与回波信号相互干扰。超声波从发射到接收最远需要约 400cm × 2 / 340m/s ≈ 23.5ms加上处理时间和余量60ms 是比较安全的间隔系统核心设计思路底层硬件操作应该被封装成简单、无状态、可复用的接口测距、转动到指定角度上层不同的业务逻辑各自维护自己的状态和参数通过组合这些底层接口实现差异化的行为