简介:这是一份基于STM32最小系统板的扫地机器人完整项目方案,面向电子、嵌入式方向的学生与开发者,尤其适合毕业设计、期末大作业和课程设计场景。项目实现了自动清扫、超声波避障、自动回充、自适应巡航及覆盖清扫等核心功能,代码包含详细注释,新手也能快速读懂并部署运行。
资源包共153个文件,压缩后约5.1MB,以h头文件、c源代码文件为主,辅以uvprojx/uvoptx等Keil工程配置、s汇编启动文件以及crf编译中间文件,目录结构清晰,便于查阅和二次开发。核心代码覆盖STM32外设驱动、定时器PWM控制、ADC采样等知识模块,可直接导入Keil编译烧录。
目前已有562人浏览学习,项目经作者本人手打并获得98分的高分认定,导师认可度较高。下载后简单配置硬件即可使用,也可作为嵌入式综合开发的参考模板,帮助理解机器人导航、避障和电源管理的完整实现思路。
1. 一块最小系统板能把扫地机器人做到什么程度
一块几十元的蓝板 STM32F103C8T6 最小系统板,不画底板、不重做 PCB,直接飞线就能搭出一台带自动清扫、超声波避障、自动回充、自适应巡航和覆盖清扫的扫地机器人。这个结论听起来反直觉,但实际做下来会发现:这些功能没有一个是靠算力堆出来的,全部依赖状态机的合理划分和传感器数据的正确融合。STM32 在这个项目里更像一个调度核心,而不是计算核心,主要工作是把电机转速、超声波距离、充电电流这些信号按时序处理干净。
做这类项目的难点通常不在硬件本身,而在于五个功能互相穿插时的任务切换——正在弓字形巡扫时突然遇到障碍要避让、低电量时要从任意位置回到充电座、回充对接失败后还要重新巡航。把这些状态切换理顺,比把某一项功能做完美更关键。适合的人群主要是做毕设或课设的学生,以及想从点灯进阶到完整机电项目的嵌入式开发。读完你会清楚每个模块该怎么选型、参数怎么设、联调时优先看哪个信号。
2. 最小系统板选型与硬件搭建:先定 IO 和电源再写代码
2.1 STM32F103C8T6 够用的边界在哪里
在 STM32 系列里做扫地机器人,最常见的选择是 STM32F103C8T6 最小系统板。72MHz 主频、64KB Flash、20KB SRAM,不带地图和视觉可以跑得很轻松。热词里出现的 STM32F407VGT6 最小系统板是另一个量级的选择,168MHz 主频配 512KB Flash,适合跑 FreeRTOS 加灰度传感器栅格地图融合的复杂方案。但纯超声波避障加红外回充的扫地机,F103C8T6 的定时器资源和 IO 数量完全覆盖,多花成本买 F407 反而要处理更复杂的电源和引脚映射。
选型时还要注意最小系统板的实际差异。刷机用 ST-LINK,SWD 接口占用 PA13/PA14,写代码时这两个引脚不要分配外设。晶振和 BOOT 配置基本不用动,但 3.3V 和 GND 引脚要确认清楚——便宜的蓝色板子丝印偶尔会标反,上电前用万用表量一遍比什么都有用。
2.2 电机驱动、编码器与超声波传感器的引脚分配
扫地机器人本体一般用两轮差速底盘,两个直流减速电机配霍尔编码器,前侧或斜前方装 2 到 3 个 HC-SR04 超声波传感器。电机驱动选 TB6612 而不用 L298N,原因在于 TB6612 的 MOSFET 导通压降小,4 节镍氢电池或 2 节 18650 串联供电时能留更多电压给电机。每路电机需要 3 个控制信号:两个方向脚加一个 PWM 速度脚。
| 功能模块 | 信号 | STM32 引脚 | 定时器/外设 | 说明 |
|---|---|---|---|---|
| 左电机 TB6612 | AIN1/AIN2 | PB12/PB13 | GPIO 输出 | 控制正反转 |
| 左电机 TB6612 | PWMA | PA0 | TIM2_CH1 | PWM 调速,默认 20kHz |
| 右电机 TB6612 | BIN1/BIN2 | PB14/PB15 | GPIO 输出 | 控制正反转 |
| 右电机 TB6612 | PWMB | PA1 | TIM2_CH2 | PWM 调速 |
| 左电机编码器 | A/B 相 | PB6/PB7 | TIM4_CH1/CH2 | 编码器模式直读转速 |
| 右电机编码器 | A/B 相 | PB8/PB9 | TIM3_CH1/CH2 | 编码器模式 |
| 超声波 1(前) | Trig/Echo | PB0/PB1 | GPIO 输出/输入捕获 | 测距 2cm-400cm |
| 超声波 2(左前) | Trig/Echo | PB10/PB11 | GPIO 输出/输入捕获 | 避障扫描用 |
| 电压检测 | ADC | PA4 | ADC1_IN4 | 分压后采样电池电压 |
引脚分配的一条实用原则是:PWM 和编码器必须绑在硬件定时器上,GPIO 模拟 PWM 会占死 CPU,影响超声波测距的时序。上面的表里 TIM2 做电机 PWM,TIM4/TIM3 做编码器接口,三者互不冲突。超声波 Echo 引脚如果不用输入捕获,改成外部中断加定时器计时也可以,但输入捕获更省心,不用在中断里反复翻转引脚。
2.3 电源方案与电压检测的分压计算
电源是整个项目里最容易埋雷的部分。电机堵转瞬间电流可能冲到 1A 以上,如果逻辑供电和电机供电共用一路,单片机会在堵转瞬间复位。常见做法是两节 18650 串联(标称 7.4V)直接给 TB6612 的 VM 供电,再通过一颗 AMS1117-3.3 稳压给 STM32 和传感器单独供电,电机和逻辑地在电池负极处单点汇合。
电压检测用于低电量判断,不能直接接到 PA4,要用电阻分压。取 R1=10K、R2=4.7K,电池满电 8.4V 时分压输出 8.4×4.7/(10+4.7)≈2.87V,低于 3.3V 参考电压。ADC 采到的原始值换算成实际电压的公式:
// adc_value 是 12 位 ADC 的原始值,VREF=3.3V float Vbat; Vbat = (float)adc_value / 4095.0f * 3.3f; // 分压点电压 Vbat = Vbat * (10.0f + 4.7f) / 4.7f; // 还原电池端真实电压 // 电压低于 6.6V 时切换回充状态机 if (Vbat < 6.6f) { robot_state = STATE_RETURN_DOCK; }这里的关键点在于分压电阻的精度直接决定回充触发电压的准确性,建议用 1% 精度的电阻。另外采样时刻要避开电机启动瞬间,软件里对连续 5 次采样取均值,间隔 10ms 一次,滤掉 PWM 开关造成的毛刺。镍氢电池方案则要重新计算分压比,因为满电电压只有 5.2V 左右。
3. 两轮差速运动控制与超声波避障:让车先走直再谈避障
3.1 差速模型的速度解算与转向策略
扫地机器人底盘大多是两轮差速结构,控制核心是左右轮速度的分配。线速度 v 和角速度 ω 与左右轮轮速 vL、vR 的关系是:v=(vL+vR)/2,ω=(vR−vL)/L,其中 L 是轮距。实际写代码时不需要用这个公式反推每个动作的精确速度,更实用的做法是直接定义动作原语:直行是 vL=vR=设定速度,原地左转是 vL=−vR,前进左转是 vL=0.3v、vR=v。
// 动作原语定义,speed 是单位 mm/s,转向半径由左右轮速差决定 void motion_go(float speed) { set_motor(LEFT, speed); set_motor(RIGHT, speed); } void motion_rotate(float angle) { // angle>0 逆时针,<0 顺时针;原地旋转时两轮速度相反 set_motor(LEFT, -ROTATE_SPEED); set_motor(RIGHT, ROTATE_SPEED); // 旋转角度由编码器累计换算,轮距 L=160mm,转一圈周长=π*L // 目标累计编码器脉冲 = angle/360 * π * L / (轮周长/减速比/编码器线数) } void motion_stop(void) { set_motor(LEFT, 0); set_motor(RIGHT, 0); }motion_rotate 的注释里提到编码器脉冲换算,这个参数每个底盘都不样。霍尔编码器常见配置是电机减速比 30:1、编码器线数 13,那么电机轴转一圈输出 13×2(AB 相)×30=780 个脉冲。轮径 65mm 的轮子走一毫米大约对应 780/(π×65)≈3.8 个脉冲。先把实测的脉冲数标定出来,再写转向逻辑,否则角度会越偏越多。
3.2 编码器闭环:速度 PID 的调参顺序
开环 PWM 调速的问题很明显——电池电压从 8.4V 掉到 7.2V 时,同样占空比下电机转速会掉百分之十几,直线就走歪。解决方式是加编码器 PID 闭环,控制周期取 20ms。增量式 PID 的计算量小,适合 F103C8T6 这种资源紧张的场景:
typedef struct { float kp, ki, kd; float err_last, err_prev; float out; } pid_t; float pid_update(pid_t *pid, float target, float actual) { float err = target - actual; float p_out = pid->kp * err; float i_out = pid->ki * (err + pid->err_last + pid->err_prev); // 实际实现里积分项单独累加,这里用简化的增量形式 float d_out = pid->kd * (err - pid->err_last); float inc = p_out + i_out + d_out; // 输出限幅,防止 PWM 饱和后恢复慢 if (pid->out + inc > 999) pid->out = 999; else if (pid->out + inc < 0) pid->out = 0; else pid->out += inc; pid->err_prev = pid->err_last; pid->err_last = err; return pid->out; }PID 参数不要一套抄完就完事,按 Kp、Ki、Kd 的顺序逐个试。先把 Ki 和 Kd 设 0,Kp 从 0.5 开始加,观察电机是否能快速响应目标转速且不振荡。响应慢就加 Kp,出现持续振荡就减 Kp。然后再加 Ki 消除稳态误差,最后用很小的 Kd 抑制超调。常见问题是积分项累积过大导致 PWM 饱和,堵转解除后电机猛冲,限幅必须加。串口调试 PID 参数时,把目标转速和实际转速都打印出来,跑一段直线看两条曲线贴合程度。
3.3 超声波避障状态机:多方向扫测代替单点测距
HC-SR04 单次测距的逻辑是拉高 Trig 10us 以上,然后测量 Echo 高电平持续时间,距离等于时间除以 58(单位厘米)。但扫地机器人用的超声波不能只测正前方,否则侧向的障碍物会直接撞上。常见方案是前向和左前两个超声波交替采样,避障时先减速,然后原地旋转探测两侧空间的可用距离,选择空旷的一侧转向。
// 一次测距,返回厘米;超时返回 -1 int16_t us_distance(sonar_t *s) { HAL_GPIO_WritePin(s->trig_port, s->trig_pin, GPIO_PIN_SET); delay_us(12); HAL_GPIO_WritePin(s->trig_port, s->trig_pin, GPIO_PIN_RESET); // 用输入捕获测量 Echo 脉宽,超时 30ms 认为无回波 if (s->echo_timeout) return -1; return s->echo_width_us / 58.0f; } // 避障动作选择:前方障碍 <25cm 时,比较左前和右前可用距离 void avoid_obstacle(void) { motion_stop(); int16_t left_dist = sonar_measure(LEFT_FRONT); int16_t right_dist = sonar_measure(RIGHT_FRONT); if (left_dist > right_dist) { motion_rotate(+90); // 空地大的一侧优先 } else { motion_rotate(-90); } motion_go(CRUISE_SPEED_LOW); }注意一个细节:HC-SR04 的 Echo 引脚不能接在普通 GPIO 上干等,干等会卡死主循环。输入捕获配合定时器是一劳永逸的方案,Echo 上升沿和下降沿都触发捕获,差值直接读出脉宽。实测中超声波在软性障碍物上经常测不到回波,返回 -1 要按无障碍处理,否则机器人会对窗帘这类障碍物产生迷之转向。还有一种情况是超声波串扰,两个探头同时测距会互相干扰,软件上错开 30ms 触发即可。
4. 覆盖清扫与自适应巡航算法:小内存也能做栅格覆盖
4.1 弓字形覆盖:从行为模式到栅格标记
扫地机器人能不能扫干净,核心指标是覆盖率。随机碰撞式清扫实现最简单,但重复路径多,看起来在工作实际效率不高。弓字形覆盖是一种更实用的路线策略:机器人沿直线清扫,碰到边界后原地旋转 180 度,侧移一个清扫宽度继续反向清扫,如此往复覆盖整个区域。
STM32F103C8T6 没有 IMU,航向角靠编码器积分得出,长时间运行会积累误差。更可靠的做法是不依赖绝对坐标,只在地图上记录相对位置。把地图划分成 30×20 的栅格,每个格子约 10cm×10cm,用一个 uint32_t 数组就能存下 960 个栅格的状态(30*20=600 位,一个 32 位数组就够了):
#define GRID_W 30 #define GRID_H 20 uint32_t grid_map[(GRID_W * GRID_H + 31) / 32]; void grid_mark(int x, int y) { if (x < 0 || x >= GRID_W || y < 0 || y >= GRID_H) return; int idx = y * GRID_W + x; grid_map[idx / 32] |= (1u << (idx % 32)); } int grid_is_marked(int x, int y) { if (x < 0 || x >= GRID_W || y < 0 || y >= GRID_H) return 1; // 边界视为已覆盖 int idx = y * GRID_W + x; return (grid_map[idx / 32] >> (idx % 32)) & 1u; }坐标更新用编码器里程计累积:每个控制周期根据左右轮脉冲数计算位移和转角,更新当前栅格坐标并标记。弓字形巡扫时,每条直线的目标长度由障碍物位置决定:当超声波在前方 X 距离处探测到边界时,本地图当前行结束,机器人转向并进入下一行,侧移量取清扫宽度的一半到三分之二,保证相邻两行之间有重叠覆盖。
4.2 自适应巡航速度调度:按障碍密度调整功率
自适应巡航可以理解为根据当前环境的障碍物密度动态调节清扫速度:开阔区域高速巡航,拥挤区域低速慢扫。速度的反馈信号直接取最近一段时间内的避障次数。维护一个环形计数器,记录过去 60 秒内的避障触发次数,超过阈值就降挡,长时间没有避障则升挡。
typedef enum { SPEED_LOW = 10, SPEED_MED = 18, SPEED_HIGH = 26 } cruise_speed_t; cruise_speed_t g_speed = SPEED_MED; void cruise_tune_speed(uint8_t avoid_count_1min) { if (avoid_count_1min > 6) { g_speed = SPEED_LOW; // 狭窄区域,低速提高避障成功率 } else if (avoid_count_1min < 2) { g_speed = SPEED_HIGH; // 开阔区域,高速提升单位时间覆盖面积 } else { g_speed = SPEED_MED; // 默认中等速度 } // 速度切换时建议做斜坡处理,每 100ms 调整一次,避免电机电流突变 motion_set_target_speed(g_speed); }速度切换的斜坡很重要。如果直接从 26mm/s 跳到 10mm/s,电机的 PWM 突变会让机身顿挫,编码器计数瞬间丢步,里程积分也跟着出错。常见做法是目标速度改变时,每次控制周期只允许调整 PWM 比较值不超过 30,让加减速平滑。低速挡还应该配合提高超声波避障的触发距离,因为速度越低传感器采样间隔越短,反应距离可以缩短到 15cm 而不会撞上。
4.3 清扫电量预算与结束条件
覆盖清扫不是无限跑下去的,要预留足够的电量给回充过程。电压采样值降到一个阈值(比如 6.8V)时,先进入返航状态;如果继续降到 6.4V,则强制停止清扫直接断电保护电池。除此之外,清扫时间上限用 30 分钟比较合适——按正常清扫速度,30 分钟覆盖一个十平方米的房间已经足够。时间上限的作用是兜底,防止机器在复杂地形里来回绕圈浪费电量。
// 主循环中的清扫结束判断,优先级从高到低 if (Vbat < 6.4f) { // 严重低电,立刻停车 motion_stop(); enter_state(STATE_POWER_OFF); } else if (Vbat < 6.8f) { // 低电,进入回充 enter_state(STATE_RETURN_DOCK); } else if (sweep_time >= SWEEP_MAX_30MIN) { enter_state(STATE_RETURN_DOCK); }覆盖清扫的完成判断不依赖时间,而是看栅格地图:连续 20 秒没有出现新的未覆盖栅格,且当前行的障碍物已经触发过两次转向,就认为这一区域清扫完毕。要注意判断条件里必须有一个时间下限,否则机器人刚启动时地图全是空的,任何一瞬间都满足“无新增栅格”的条件。
5. 自动回充实现与整机调试技巧:用充电电流锁定对接成功
5.1 红外引导对接与状态机
自动回充的常见方案是充电座发射两束红外光,机器人左右两侧各装一个红外接收管,根据两路信号的强弱差调整航向,差额缩小说明正对充电座。这个过程的本质是一个比例引导:差值大时转大角度,差值小时微调直行。
typedef enum { DOCK_SEARCH, // 原地旋转找红外信号 DOCK_APPROACH, // 对准基座缓慢前进 DOCK_CONTACT, // 极片接触,等待充电电流 DOCK_CHARGING // 充电中,电压回升后继续 } dock_state_t; void dock_control(void) { switch (g_dock_state) { case DOCK_SEARCH: motion_rotate(10); // 持续慢转,直到任一接收管有信号 if (ir_left_detect() || ir_right_detect()) g_dock_state = DOCK_APPROACH; break; case DOCK_APPROACH: int diff = ir_left_value() - ir_right_value(); // diff 越大表示偏左,向右修正 set_motor(LEFT, DOCK_SPEED + diff * 0.8f); set_motor(RIGHT, DOCK_SPEED - diff * 0.8f); if (charge_current_detect()) g_dock_state = DOCK_CONTACT; break; case DOCK_CONTACT: motion_stop(); if (charge_current_detect()) g_dock_state = DOCK_CHARGING; // 电流连续存在 1 秒才算对接成功 else g_dock_state = DOCK_SEARCH; // 虚接抖动,重新搜索 break; } }充电电流检测不能只靠电压变化判断。接触瞬间电池电压会被充电座拉高,但没接触好的话电压几秒后就回落。更可靠的判断是串在充电回路里的采样电阻两端电压,用运放放大后送到 STM32 的 ADC 引脚。连续 1 秒检测到充电电流才切换为充电状态,能有效过滤极片弹跳引起的误判。对接失败时不要原地重试,后退一段距离再重新搜索红外信号,同时更新地图上充电座的方位信息,下次回充优先朝那个方向走。
5.2 整机验证的关键串口日志
五个功能全部接好后,验证时最容易出问题的是状态机跳变太快,肉眼跟不上。建议用串口把状态切换和设备状态按固定格式打出来,波特率 115200,10Hz 频率输出一行。串口打印用的是编程中的标准方法,通过 UART 发送格式化字符串,PC 端用串口调试助手或 Vofa 观看波形。
// 主循环末尾打印状态,格式为 CSV 方便画图 printf("STATE:%d,V:%d.%dV,L:%d,R:%d,US:%d,OC:%d\r\n", g_robot_state, Vbat_int, Vbat_frac, encoder_left_speed, encoder_right_speed, sonar_front_cm, grid_covered_count);对照日志检查三个指标:直线行驶时左右编码器速度差是否小于 5%;超声波触发避障到电机停转的响应时间是否小于 100ms;每次回充对接时 DOCK_SEARCH 到 DOCK_CONTACT 的停留时间是否稳定。延时函数卡死的问题也经常出现在这个阶段,最常见原因是超声波测距里用了阻塞式 delay_us 等待 Echo 信号,导致定时器中断无法响应。排查时优先怀疑所有阻塞延时,全部改成非阻塞的超时检查。最后一类隐藏问题出现在 ST-LINK 烧录后的首次运行时,各模块共用一个 3.3V 电源,电流超出 LDO 能力会导致芯片反复复位,用示波器看 3.3V 引脚波形能立刻确认。
本文还有配套的精品资源,点击获取