资讯动态

基于STM32最小系统板的扫地机器人开发实战

发布时间:2026/9/16 16:42:31 来源:尧图企业网站定制
简介面向STM32学习者和毕业设计、期末大作业学生的扫地机器人完整项目资源。基于STM32最小系统板开发实现了自动清扫、超声波避障、自动回充、自适应巡航与覆盖清扫等核心功能代码注释丰富新手也能看明白适合作为嵌入式课程设计和毕设的高分参考方案。资源包共153个文件压缩后仅5.1MB以C源码、头文件及工程配置文件为主同时包含汇编启动文件、链接脚本和编译辅助脚本目录结构清晰便于直接导入工程查看和部署。已有562人学习下载。这是作者手打的98分项目导师认可度高除了完整源码外还提供工程备份与清理脚本能帮助理解STM32外设驱动、PWM调速、超声波测距、自动回充逻辑等关键实现有效节省开发调试时间可直接在此工程基础上二次扩展。1. 为什么用 STM32 最小系统板做扫地机器人扫地机器人这种项目在实验室和毕业设计里出现频率很高但大多数方案卡在同一个地方用了完整开发板体积大、引脚浪费真正布局到小车底盘上时才发现连电机驱动和传感器排线都挤不开。STM32 最小系统板恰恰是解决这个问题的关键零件——它只保留 MCU 启动必需的晶振、复位电路、电源稳压和 BOOT 配置把其他引脚全部留给外设一块 F103C8T6 的板子甚至比硬币大不了太多却能同时驱动两个直流电机、一路超声波、一路红外接收和一组电量检测 ADC。这个方案的价值不只在省空间。STM32 的定时器资源非常充裕F103C8T6 有 4 个通用定时器可以同时输出四路 PWM 驱动两路电机还剩余两个定时器做输入捕获测速或者超声波回波计时不占用 CPU 轮询时间。项目中要求的自动清扫、超声波避障、自动回充、自适应巡航这些功能本质上都是对 GPIO、定时器、ADC 和中断资源的调度属于 STM32 最擅长的任务类型。适合的人群是已经能点灯、会串口打印、想往运动控制方向走一两步的开发者从零开始搞定一台能跑、能躲、能自己回去充电的小车大概需要两到三个周末。前提是先把最小系统板的选型意识和引脚规划做对否则后边每加一个模块就重排一次线工程直接失控。2. 最小系统板选型与引脚规划先把资源算清楚2.1 F103C8T6 还是 F407VGT6不是越贵越好热词里同时出现 stm32f103c8t6 最小系统板 和 stm32f407vgt6 最小系统板说明初学者经常在这两个型号之间犹豫。从扫地机器人这个负载来看F103C8T6 的 72MHz 主频、64KB Flash、20KB SRAM 完全够用——电机控制用定时器 PWM避障用超声波回充用红外和 ADC没有一路涉及浮点运算或复杂的图像处理。F407VGT6 的优势在于 168MHz 主频和更大的存储但代价是功耗更高、LQFP100 封装对手工焊接更不友好。只有在需要接摄像头做视觉导航、或者用 DSP 库跑比较复杂的滤波算法时F407 才值得考虑。做超声波避障和覆盖清扫F103C8T6 是性价比最优解一块几块钱的板子就能跑完整个项目。从实际使用的角度F103C8T6 还有两个隐形的优势。其一是资料密度极高无论是寄存器版还是标准库版几乎每个引脚的功能都能找到现成的参考代码其二是它可以直接用 USB 转串口下载程序不需要额外的仿真器虽然用 ST-Link 体验更好但起码不会因为缺调试器而卡住进度。// main.c 中判断当前使用的芯片型号防止选错启动文件 #if defined(STM32F10X_MD) // 中等密度器件适用于 F103C8T6 #define LED_GPIO_PORT GPIOB #define LED_GPIO_PIN GPIO_Pin_12 #elif defined(STM32F40XX) // 适用于 F407VGT6引脚不同 #define LED_GPIO_PORT GPIOF #define LED_GPIO_PIN GPIO_Pin_9 #endif这段预处理的作用是在一份工程里兼容两种芯片避免在 CubeMX 里反复切换型号。实际项目里不需要这样的写法但通过宏区分芯片型号可以帮你在选型阶段明确一个观念F103 和 F407 的库函数接口差别很大不能指望一份代码无缝迁移。2.2 引脚分配电机、超声波、红外、ADC 互不冲突规划引脚是硬件设计的第一步建议在画 PCB 或接线之前先画一张引脚分配表。电机驱动用定时器 PWM 输出超声波用输入捕获或外部中断红外接收用外部中断电池电压检测用 ADC回充对接检测用普通 GPIO。功能推荐引脚定时器/外设说明左电机 PWMPA8 / PA9TIM1_CH1 / CH2高级定时器带互补输出右电机 PWMPA6 / PA7TIM3_CH1 / CH2通用定时器够用左电机方向PB12 / PB13GPIO 输出控制 H 桥正反转右电机方向PB14 / PB15GPIO 输出同上超声波 TrigPA0GPIO 输出10us 高电平触发超声波 EchoPA1TIM2_CH2 输入捕获测量回波脉宽电量检测PA4ADC1_IN4分压电阻采样红外回充信号PB3外部中断 EXTI3引导小车对准充电座注意例子中避免把 PWM 引脚和超声波 Echo 引脚混用否则当电机转动时PWM 的电气噪声会串入 Echo 通道导致测距跳变。PA0 和 PA1 在 F103 上同时属于 TIM2 的通道 1 和通道 2如果把超声波接在这里可以直接用定时器输入捕获精度高于外部中断加micros()的软件计时方案。2.3 最小系统板上必须留的调试接口不管选哪块板子SWD 调试口一定要引出来。接法是 SWDIO 接 PA13、SWCLK 接 PA14、GND 共地三根线就能用 ST-Link 实现下载和在线调试。可能遇到的典型问题在热词里也有体现——error: no stm32 target found这个错误 90% 的情况是接线松动或调试口被禁用。# 使用 st-flash 命令行工具烧录适合没有图形界面的环境 st-flash --connect-under-reset write build/robot.bin 0x08000000--connect-under-reset参数的意思是让 ST-Link 在复位状态下强行连接目标芯片专门用于解决调试口被复用成 GPIO 之后无法连接的问题。如果程序跑飞把 SWD 引脚拉低正常连接会失败加上这个参数可以复位后立刻接管芯片保住调试能力。如果没有这个选项唯一救砖的办法就是拉 BOOT0 到高电平进入系统存储器模式用串口 ISP 擦除 Flash。3. 电机驱动与运动控制自动清扫的基础是可靠的差速控制3.1 TB6612 驱动板与 PWM 频率的选择清扫动作要求小车在直行、转弯、原地旋转之间频繁切换驱动芯片推荐用 TB6612FNG 而不是 L298N。TB6612 的导通压降只有 0.5V 左右L298N 在同样电流下压降可能到 2V 以上这意味着电池电压 7.4V 时TB6612 供给电机的电压比 L298N 高出近两伏同样的 PWM 占空比下转速差异非常明显。另外 TB6612 支持 PWM 频率最高 100kHz给超声波避障留出了干净的电气环境。左电机AIN1 - PB12, AIN2 - PB13, PWMA - PA8 右电机BIN1 - PB14, BIN2 - PB15, PWMB - PA6控制逻辑是AIN11、AIN20 时电机正转AIN10、AIN21 时反转AIN1 和 AIN2 相同则刹车。PWM 引脚负责调速方向引脚负责换向两者独立。// 定时器3输出PWM驱动右电机频率20kHz void Motor_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // PA6 复用为 TIM3_CH1 gpio.GPIO_Pin GPIO_Pin_6; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); // 方向引脚PB14、PB15普通推挽输出 gpio.GPIO_Pin GPIO_Pin_14 | GPIO_Pin_15; gpio.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, gpio); // PWM频率 72MHz / (7200 - 1) / (100 - 1) 20kHz tim.TIM_Prescaler 7200 - 1; tim.TIM_Period 100 - 1; tim.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, tim); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_Pulse 0; oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, oc); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }PWM 频率 20kHz 是常见的选择超出人耳听觉范围不会在清扫时产生刺耳的啸叫。预分频 7200 意味着计数器时钟变成 10kHz周期 100 对应 100 个计数点占空比调节精度 1%对吸尘电机和驱动电机都够用。如果使用 7.4V 电池占空比 100% 时电机端电压大约 6.9V 到 7.2V去掉驱动压降转速接近空载最大值建议软件限幅在 90% 以免启动电流过大。3.2 增量式 PID让两轮转速差稳定下来超声波避障要求小车直行时不跑偏。开环控制下即使两个电机型号完全相同绕组电阻和机械摩擦的细微差异也会导致小车在一两米内明显偏航。解决办法是加编码器测速构成闭环。这里从简化角度出发推荐用带霍尔编码器的 N20 电机和增量式 PID。霍尔编码器输出两路相位差 90 度的方波通过定时器编码器接口模式直接获得转速和方向不需要额外的外部中断。// 编码器计数值换算为实际速度 // 减速比 30:1编码器线数 7电机转一圈输出 7*30 210 个脉冲 int16_t speed TIM_GetCounter(TIM4); // 读取脉冲数 TIM_SetCounter(TIM4, 0); // 清零重新计数 // 每10ms读取一次速度单位脉冲数 / 100ms后续换算为 cm/s增量式 PID 的核心在于输出的是控制量的增量而不是绝对大小。第 k 次的控制输出等于第 k-1 次的输出加上一个增量这个增量由比例、积分、微分三项组成int16_t Speed_PID_Calc(int16_t target, int16_t current) { static int16_t error_last 0; static int32_t integral 0; int16_t error target - current; int16_t output; integral error; // 抗积分饱和限制积分量范围 if (integral 300) integral 300; if (integral -300) integral -300; output KP * error KI * integral KD * (error - error_last); error_last error; return output; }调用这个函数之后把输出叠加到当前 PWM 占空比上。参数标定建议按先 P 后 I 再 D 的顺序来只加 P 让小车直行增大到出现轻微振荡然后取当前值的 60% 作为最终 P加入 I 消除稳态误差D 用于抑制超调扫地机器人这种惯量小的系统 D 通常很小甚至为 0。落地的核心思路是直行时用同一个目标速度PID 输出差异控制两轮转速一致转弯时直接从算法层把左右轮目标速度设成相反数。3.3 自适应巡航的速度档位划分自适应巡航在扫地机器人上的含义是根据当前工作状态实时调整速度。遇到障碍物或者电量低时降速宽阔区域时提速。这不涉及复杂的交通目标预测但需要一套明确的策略这里把巡航速度横向比较列出:工作状态目标速度PWM 限幅说明正常清扫25 cm/s70%覆盖率优先沿墙清扫15 cm/s50%跟随更稳定探测盲区小电量偏低15 cm/s50%优先保证回充成功回充对准8 cm/s30%低速便于红外对准速度切换的平滑处理值得多写几行代码防止从 25cm/s 突然掉到 8cm/s 造成过冲或打滑。每次巡航速度变更前让速度值朝着目标值递增或递减步长设为 1cm/s每 20ms 执行一次1 秒内完成变速。这样电机电流不会突变对电池电压的冲击也会小很多。4. 超声波避障与覆盖清扫从躲开墙到扫满全屋4.1 HC-SR04 测距的正确测量姿势HC-SR04 的测距原理是Trig 引脚给 10us 高电平触发模块模块自动发出 8 个 40kHz 超声波脉冲然后 Echo 引脚输出一个高电平脉宽脉宽时长对应声波往返时间。距离 高电平时间 × 340m/s / 2。常见错误是在主循环里用delay等待 Echo 变低这样测距期间 CPU 完全卡死电机 PID 失喂小车反而会跑偏。正确做法是用输入捕获中断或外部中断 定时器计时。// 定时器2输入捕获模式测距 // PA1 - TIM2_CH2上升沿开始计时下降沿读取时间差 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC2) ! RESET) { if (TIM_GetCapture2(TIM2) ! 0) { // 第二次触发为下降沿 uint32_t width TIM_GetCapture2(TIM2) - last_capture; distance_mm width * 0.017; // 72MHz计数1us计数72次 TIM_SetCounter(TIM2, 0); last_capture 0; } else { // 第一次触发为上升沿 last_capture TIM_GetCapture2(TIM2); } TIM_ClearITPendingBit(TIM2, TIM_IT_CC2); } }宽度值除以 72 得到微秒数乘以 0.34mm/us 再除以 2往返就是距离毫米值。代码里有一个容易出错的细节第一次进入中断时TIM_GetCapture2返回的是上升沿的计数值此时计数值不为 0 但last_capture为 0所以判断条件要区分是第几次触发。也可以用标志位来区分但更稳妥的是在上升沿清零计数器、下降沿直接读取计数值避免减法的溢出问题。超声波测距有一个天然的物理限制探测波束角大约 15 度对细小物体比如桌腿和斜面比如地毯边缘的反射不可靠。扫地机器人通常在小车正前方装一个超声波左右两侧各装一个红外或超声波形成三方向探测。如果只装一个转弯时扫不到侧面障碍碰撞是必然的。4.2 覆盖清扫策略弓字形、螺旋形与随机碰撞热词里明确提到了“覆盖清扫”这是清扫效率和路径规划的综合体现。完整意义上的覆盖清扫需要 SLAM 和栅格地图但基于 STM32F103C8T6 的资源实践中的可行路线是“分区 弓字形 沿墙回补”的组合。弓字形是最常用的覆盖策略小车先直行碰到障碍物或边界后旋转 180 度偏移一个机身宽度后反向直行如此反复。实现时维护两个变量当前行进的 x 坐标和已覆盖的行数当超声波检测到前方 30cm 内有障碍物时原地旋转 90 度前进一个机身宽度约 25cm再反向旋转 90 度继续直行。// 弓字形覆盖逻辑核心 void Coverage_Tick(void) { if (obstacle_distance 30) { // 前方障碍 Motor_Spin(0, 90); // 原地右转90度 Motor_Forward(5); // 低速前进一段固定距离 Motor_Spin(1, 90); // 原地左转90度 forward_direction !forward_direction; // 行进方向取反 } else { Motor_Forward(cruise_speed); // 正常巡航 } }螺旋形清扫更适合单个房间的中央区域先沿着房间边缘走一圈然后逐步向内收缩每圈向内收缩一个机身宽度像蚊香那样覆盖。螺旋形的优势是转弯少、路径重复率低但要求能感知边界通常用沿墙传感器实现。实际产品大多混合使用先螺旋形清扫当前房间然后切换弓字形沿房间长边覆盖最后沿墙走一圈补扫边缘盲区。覆盖清扫的本质问题是重复率和漏扫率之间的权衡。提高漏扫率的反面是重复率高追求少重复就会漏扫。对 F103 这个级别的 MCU实用的折中方案是记录每个 25cm x 25cm 栅格的访问状态当小车经过某个栅格时在 RAM 位置数组里标记一个字节清扫结束后检查存在多少未访问的连续区域若有遗漏则补扫。这种轻量级标记法不需要完整地图RAM 占用只要几十个字节是性价比很高的方案。4.3 避障决策与速度联动避障不能做成“检测到障碍就停”否则清扫效率极低。实践中将避障响应分为三个层次前方 30cm 以外视为安全维持巡航速度前方 15cm 到 30cm减速到巡航速度的 60%同时小角度偏转方向试探前方 15cm 以内停车原地旋转 90 度重新探测增加侧向探测后决策变得更完善如果正前方有障碍但左侧安全优先左转而不是原地旋转这样能减少对覆盖路径的破坏。这里有一个控制细节转向幅度应该与距离成比例而不是固定角度——距离 25cm 时转 15 度距离 10cm 时转 45 度距离越近转角越大。5. 自动回充与电量管理让小车在工作结束后自己回家5.1 电量检测电路与低电量状态机自动回充的前提是可靠的电池电量检测。用两个电阻对电池电压分压送到 STM32 的 ADC 引脚通过 ADC 采样计算出当前电压值。// 7.4V锂电池分压比 10K/(10K20K) 1/3ADC参考电压3.3V // ADC值范围 0-4095对应电压 0-3.3V uint16_t adc ADC_GetConversionValue(ADC1); float battery_voltage adc * 3.3f / 4095.0f * 3.0f;分压电阻的选择要平衡功耗和采样精度。10K 20K 的分压网络在 7.4V 下消耗电流约 0.25mA对电池容量来说可以忽略同时 10K 的源阻抗对 STM32 的 ADC 来说略高推荐在 ADC 引脚和地之间加一个 100nF 电容滤波并启用 ADC 采样保持时间到 55.5 周期能有效降低采样误差。低电量判断不能只做一个瞬时电压阈值。锂电池在电机启动瞬间电压跌落非常厉害如果只判断瞬时值小车一启动就会误判为低电量触发错误的回充动作。更稳的判定是连续采样 20 次间隔 100ms其中 15 次低于阈值才确认低电量。从正常工作状态进入回充状态状态机设计为正常清扫 - 低电量确认 - 中断清扫 - 寻找充电座 - 红外对准 - 对接充电。5.2 红外引导对接从粗定位到精确对准自动回充的难点不是找到充电座的大致方向而是最后几厘米的对准。充电座上有两个红外发射管一个发射宽角度引导信号一个发射窄角度对准信号。小车尾部装一个红外接收管接收充电座信号后通过比较左右接收强度来调整方向。红外对接需要区分引导模式和对接模式。引导模式下小车以较宽的角度寻找信号源接近到 50cm 以内切换为对接模式此时只使用前方的红外接收管根据信号强度调整左右轮速度差// 红外引导值范围0-100值越大说明越靠近充电座中心 void Dock_Align(int16_t left_signal, int16_t right_signal) { if (left_signal right_signal 5) { // 偏左需要右转校正 Motor_Drive(6, 10); // 左轮6cm/s右轮10cm/s } else if (right_signal left_signal 5) { // 偏右需要左转校正 Motor_Drive(10, 6); } else { Motor_Forward(8); // 对准低速前进 } }对接成功不能靠判定“到达”而应检测充电电流。当机械触点接合后充电回路上的采样电阻会产生压降MCU 通过 ADC 读取到充电电流大于设定阈值比如 500mA就认为对接完成此时立即刹停电机并进入充电模式。如果不做电流检测小车的惯性可能让它在接触瞬间继续前进顶开充电座导致接触不良。5.3 充电完成与重新出发的切换逻辑扫地机器人的回充不止发生在电量低时。自动清扫完成后如果检测到工作区域已经覆盖完毕也应该主动回充这就需要在“低电量回充”之外增加“完成回充”这一触发条件。两种回充在流程上没有本质区别但状态流转不同低电量回充结束后如果用户不干预小车再次启动时应当从上次未完成的区域继续而完成回充结束后下次启动应当从头开始新一轮清扫。这个区分在实际工程中很重要。如果完成清扫后不清除地图标记小车会认为整个房间已经扫过了下一次启动时可能只随便跑两步就结束。在回充触发函数中两个入口要打上不同的状态标记volatile uint8_t charge_reason 0; // 0无, 1低电量, 2清扫完成 // 低电量触发 if (battery_low_confirm()) { charge_reason 1; // 保存未完成地图 Enter_Charge_Mode(); } // 完成触发 if (coverage_complete()) { charge_reason 2; // 清除地图标记 Enter_Charge_Mode(); }回充失败是扫地机器人最难受的故障如果能量耗尽前还没进充电座机器人会死在半路前功尽弃。因此超时保护必须做低电量确认后最多尝试 3 次对接若仍失败原地停车并发出低电量告警音而不是死循环消耗剩余电量。3 次失败后残余电量可能只够维持几分钟的待机保留这部分电量比继续乱撞好得多。6. 调试排错最小系统板项目的三板斧6.1 Keil 环境与标准外设库的常见故障排查工程问题集中在四个方面这里做一个对照现象大概率原因排查手段下载报 error: no stm32 target foundSWD 接线松 / 调试口被复用st-flash --connect-under-reset 强连电机抖动不转PWM 频率太低或方向引脚悬空示波器查 PWM 波形万用表量方向电平超声波测距恒为 0Trig 未发 10us 脉冲或 Echo 接错引脚GPIO 翻转测试确认引脚功能ADC 采样值跳动采样时间太短或电机噪声干扰加 100nF 电容延长采样周期error: no stm32 target found出现时先用示波器确认 SWDIO 上有没有波形。如果有波形但连接失败多半是调试口被代码禁用此时把 BOOT0 拉高断电重上电用串口擦除 Flash再恢复正常 BOOT 模式。最小系统板设计时把 BOOT0 引脚引出来并加上拉/下拉电阻能省很多折腾时间。6.2 串口波形输出用数据调 PID调 PID 和测距时串口是非常重要的“示波器”。建议把实时速度、目标速度、PWM 占空比、超声波距离通过串口以 CSV 格式输出在 PC 端用串口助手保存成文件再用 Excel 或 Python 做图。import serial import matplotlib.pyplot as plt ser serial.Serial(COM5, 115200, timeout1) speed_data [] target_data [] for i in range(200): line ser.readline().decode().strip() if line: parts line.split(,) if len(parts) 4: speed_data.append(float(parts[0])) target_data.append(float(parts[1])) plt.plot(speed_data, labelactual) plt.plot(target_data, labeltarget) plt.legend() plt.show()串口输出频率要固定建议 100Hz 或 50Hz这样做的目的是让采样周期和 PID 计算周期一致。如果输出间隔不固定画出来的速度曲线会失真就无法判断当前 P 值到底是偏大还是偏小。用 115200 波特率输出 4 个浮点数加逗号和换行符每帧大约 20 字节100Hz 只有 160kbps剩余带宽充足。6.3 一个少有人提的技巧用空占空比校准 PWM 死区很多最小系统板的 PWM 输出在占空比很小的时候并没有实测验证。某块板子的 FET 驱动电路存在死区效应当占空比低于 3% 时电机完全不动高于这个值才突然转动形成“起步跳变”。这不影响正常工作但会让 PID 在小速度调节时失灵——因为输出 2% 和 4% 的 PWM 对应的实际速度可能相同。校准方法让电机空转从 0 开始逐步增加占空比每个值稳定 200ms记录速度。找到速度从 0 跳变到非零的临界占空比例如 3%。在 PID 输出时做如下处理int16_t pwm_out pid_output; if (pwm_out 0 pwm_out 30) pwm_out 30; // 启动下限 if (pwm_out 0 pwm_out -30) pwm_out -30;这样避开非线性区让命令速度和实际速度保持线性关系。另一个实用技巧是给每个功能模块设置独立的日志开关用串口接收数字命令来启停特定模块的日志输出省去反复重新编译下载的时间。例如收字符l打印传感器数据收p打印 PID 参数收c清除地图标记。用 DMA 中断串口收发日志不影响实时控制彻底把调参数变成“改串口命令”而不是“改代码重烧”。这套组合下来整个项目的调试节奏会明显加快回归测试也更有底。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价