ARTICLE DETAIL

资讯详情

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

STM32扫地机器人实战:从PWM调速到自动回充的嵌入式开发

STM32扫地机器人实战:从PWM调速到自动回充的嵌入式开发 简介这是一份基于STM32最小系统板的扫地机器人完整工程代码面向嵌入式初学者、高校学生及准备毕业设计或期末大作业的开发者。项目实现了自动清扫、超声波避障、自动回充、自适应巡航和覆盖清扫等核心功能代码内附详细注释逻辑清楚下载后可直接部署运行。资源包共153个文件包含44个头文件、42个C源文件、8个汇编启动文件以及Keil工程配置、链接脚本和批处理脚本等整体压缩包大小约5.1MB目录结构完整方便对照学习。目前已有562人学习使用适合作为课程设计或毕业设计的高分参考模板。通过源码可以掌握STM32外设驱动、传感器数据融合和扫地机控制策略节省从零搭建的时间值得直接复用或二次开发。1. 从一块最小系统板开始的扫地机器人做扫地机器人很多人第一反应是去买现成的开发板或者直接上 ROS但实际上用一块 STM32 最小系统板就能把自动清扫、超声波避障、自动回充、自适应巡航、覆盖清扫这五件事全部跑通。这套项目我前前后后调了两个版本第一版用 F103C8T6第二版升到 F407VGT6但核心代码逻辑完全复用区别只在引脚映射和外设时钟配置上。对毕业设计或者期末大作业来说这套代码的注释密度足够让新手逐行读懂而且它并不是那种只演示一个点亮的玩具例程——它是一个真正能让小车在地上跑、遇墙转弯、电量低自己回充的完整闭环系统。这个项目的价值在于它把单片机开发里最常见的几类外设都串起来了PWM 驱动直流电机、定时器输入捕获做超声波测距、ADC 读取电池电压、以及简单的状态机调度。只要你手上有一块 STM32F103C8T6 最小系统板、两个直流减速电机、一个超声波模块和一个舵机就能把整套逻辑搭起来。我不主张直接烧录就跑而是建议按照本文的章节顺序把每个模块单独验证之后再合并这样出了问题你知道去哪里查。2. 硬件架构与最小系统板的资源分配2.1 STM32F103C8T6 的引脚规划与资源盘点先看手上这块板子的资源上限。STM32F103C8T6 是 64 引脚 LQFP 封装但最小系统板实际引出的是 20 个 GPIO 左右的排针Flash 64KBRAM 20KB。这个资源跑一个扫地机器人刚好卡在够用但不富余的位置。我在做资源规划时最紧张的是定时器数量——扫地机器人至少需要两个 PWM 通道给左右电机调速一个输入捕获通道给超声波一个额外定时器做系统心跳再加上自动回充需要 ADC 采集电压预算就很紧了。推荐引脚映射如下表这是我在第一版项目里验证过的组合功能模块引脚外设资源说明左电机 PWMPA0TIM2_CH1频率 20kHz直连电机驱动右电机 PWMPA1TIM2_CH2与左电机共用 TIM2保证同步左电机方向PA4GPIO 推挽DRV8833 的 IN1/IN2 控制逻辑右电机方向PA5GPIO 推挽DRV8833 的 IN3/IN4 控制逻辑超声波 TrigPA2GPIO 推挽拉高 10us 触发测距超声波 EchoPA3TIM3_CH1 输入捕获需要 2us 以内的精度舵机 PWMPA6TIM3_CH2频率 50Hz用于扫描前方障碍电压采集PA7ADC1_IN7经过分压电阻接电池正极使用 TIM2 驱动两个电机 PWM 而不用 TIM1 的原因是 TIM2 是 16 位通用定时器PWM 分辨率在 20kHz 下能达到 3.6% 的调速步进对扫地机器人来说足够细腻。超声波接到 TIM3_CH1 是因为输入捕获需要边沿中断TIM3 的中断优先级可以单独配置避免与电机控制互相干扰。2.2 电机驱动电路的选型与接线扫地机器人的核心执行机构是左右两个直流减速电机驱动方案一般有三种L298N、TB6612、DRV8833。L298N 压降太大2V 左右对 7.4V 锂电池供电的系统来说到达电机的电压只剩 5.4V扭矩明显下降TB6612 是经典选择但需要 5V 逻辑电源和 3.3V 控制信号的电平匹配处理DRV8833 是我在这个项目里选的方案它的逻辑电源直接支持 3.3V而且一个芯片集成了两路 H 桥正好驱动左右两个电机。接线时要注意共地STM32 的 GND 必须与 DRV8833 的 GND 连在一起否则 PWM 信号没有参考电平电机只会抖动不转。VRISE 引脚VREF也需要接 3.3V控制逻辑表是 IN11、IN20 正转反过来反转两个引脚下拉电阻要加 10k防止上电瞬间误触发。2.3 电源系统与电压采集电路整个系统的电源树需要考虑三个电压域电池直接供给电机驱动和舵机经过降压给 STM32 供电再经过 LDO 给超声波模块供电。我用的是两节 18650 锂电池串联7.4V电机驱动直接从电池取电MCU 部分用 AMS1117-3.3 降压。这里要特别提醒不要从 3.3V 给舵机供电舵机堵转电流可达 800mA会把 3.3V 电压拉垮导致 MCU 复位。电压采集电路是自动回充的基础。电池电压需要分压后送入 ADC分压电阻我选的是 10k 和 3.3k这样 8.4V 满电时 ADC 输入电压是 8.4 * 3.3 / (10 3.3) 2.09V留有余量。计算电池电量的代码实现如下uint16_t get_battery_voltage(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动一次转换 while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换完成 uint16_t adc_val ADC_GetConversionValue(ADC1); // 12位精度取值0~4095 // 分压比 3.3/(103.3)0.248反推实际电压 float voltage (adc_val * 3.3f / 4096.0f) / 0.248f; if (voltage 8.4f) voltage 8.4f; // 限制上限防止采样波动 return (uint16_t)(voltage * 100); // 返回电压×100单位 0.01V }这段代码的关键是分压比必须与实际电阻一致否则回充阈值判断会出错。我在调试时用万用表实测过ADC 读数换算出来的电压与真实值误差在 0.05V 以内这个精度足够判断 7.2V 回充阈值和 7.0V 保护阈值。3. PWM 电机调速与超声波避障的代码实现3.1 基于 TIM2 的双电机差速控制扫地机器人的运动本质是双轮差速模型左轮和右轮的速度差决定了转向半径。代码里的核心是一个motor_set_speed函数它接收左右两轮的目标速度-1000 到 1000 的归一化值内部转换成 PWM 占空比和方向控制void motor_set_speed(int16_t left_speed, int16_t right_speed) { // 速度范围限制在 -1000 ~ 1000 if (left_speed 1000) left_speed 1000; if (left_speed -1000) left_speed -1000; if (right_speed 1000) right_speed 1000; if (right_speed -1000) right_speed -1000; // 左电机方向控制正转 PA41反转 PA40 GPIO_WriteBit(GPIOA, GPIO_Pin_4, (left_speed 0) ? Bit_SET : Bit_RESET); // 右电机方向控制 GPIO_WriteBit(GPIOA, GPIO_Pin_5, (right_speed 0) ? Bit_SET : Bit_RESET); // PWM 占空比TIM2 重载值是 1000所以 1000 对应 100% 占空比 uint16_t left_pulse (left_speed 0) ? left_speed : -left_speed; uint16_t right_pulse (right_speed 0) ? right_speed : -right_speed; TIM_SetCompare1(TIM2, left_pulse); // 左电机 PWM 占空比 TIM_SetCompare2(TIM2, right_pulse); // 右电机 PWM 占空比 }参数说明速度用 ±1000 而非百分比是为了在自适应巡航里用 PID 输出直接驱动时分辨率更高。TIM_SetCompare1设置的是比较寄存器值在 PWM 模式 1 下当计数器的值小于比较值时输出有效电平。就这个设置而言满速对应 100% 占空比电机供电电压等于电池电压实际转速约 200RPM减速比 1:30 的电机。3.2 超声波避障与舵机扫描策略超声波避障只靠一个固定方向的传感器是不够的扫地机器人需要知道障碍物在左边还是右边才能决定转弯方向。我的做法是把超声波模块装在舵机上平时舵机保持在正前方检测到障碍后立即将舵机转向左侧测一次、再转向右侧测一次比较两个方向的距离来决定往哪边转uint16_t ultrasonic_measure(void) { // 拉高 Trig 引脚 10us 触发测距 GPIO_SetBits(GPIOA, GPIO_Pin_2); delay_us(10); GPIO_ResetBits(GPIOA, GPIO_Pin_2); // Echo 引脚通过 TIM3_CH1 输入捕获测量高电平脉宽 // 最大测量范围约 4m超出则返回 0 if (!wait_echo_rise(100)) return 0; // 等 Echo 变高超时 100us uint32_t t measure_echo_pulse(); // 返回高电平持续时间微秒 // 声速 340m/s往返距离 时间 * 0.034 / 2 时间 / 58 return (uint16_t)(t / 58); // 单位厘米 }超声波测距的物理原理是声波在空气中的传播速度约 340m/s所以测量往返时间后距离 时间 × 声速 / 2。代码里除以 58 是因为 1 微秒对应约 0.017cm 的距离取整后直接除以 58 厘米。注意一个调试坑超声波模块的经济型型号HC-SR04需要 Trig 引脚拉高至少 10us 才能触发如果时间不够模块不会启动测量Echo 引脚一直低电平函数超时返回 0。代码里的wait_echo_rise就是做超时处理防止程序卡死在等待循环里。舵机扫描的逻辑在avoid_obstacle函数里体现void avoid_obstacle(void) { uint16_t dist ultrasonic_measure(); if (dist 0 dist 30) { // 距离小于 30cm 判定为障碍 motor_set_speed(0, 0); // 先停车 // 舵机转到左侧 60 度测距 servo_set_angle(60); delay_ms(150); // 等待舵机转到目标角度 uint16_t left_dist ultrasonic_measure(); // 舵机转到右侧 120 度测距 servo_set_angle(120); delay_ms(150); uint16_t right_dist ultrasonic_measure(); // 舵机回正 servo_set_angle(90); delay_ms(100); if (left_dist right_dist) { // 左边空间大左转 90 度 motor_set_speed(-300, 300); } else { // 右边空间大右转 90 度 motor_set_speed(300, -300); } delay_ms(350); // 转 90 度大约需要 350ms motor_set_speed(0, 0); } }这段代码的逻辑是先正前方测距如果发现障碍停下来用舵机分别测量左右空间然后向空间更大的一侧转向。servo_set_angle的参数对应 0~180 度范围实际发送的是 0.5ms~2.5ms 的 PWM 脉冲初始化的 90 度是正前方。3.3 状态机的整体调度框架整个扫地机器人的行为逻辑不应当写成一坨 if-else而是用状态机来迁移。我用 enum 定义了五个状态STATE_INIT、STATE_CLEANING、STATE_AVOIDING、STATE_RETURNING、STATE_CHARGING。主循环里每 10ms 做一次状态扫描根据传感器数据和当前状态机决定迁移typedef enum { STATE_INIT 0, // 初始化状态等待系统稳定 STATE_CLEANING, // 正常清扫 STATE_AVOIDING, // 避障状态执行转向 STATE_RETURNING, // 回充状态寻找充电座 STATE_CHARGING // 充电状态保持静止 } RobotState; // 主循环调度每 10ms 执行一次 void state_machine_tick(void) { static RobotState state STATE_INIT; uint16_t battery get_battery_voltage(); // 电量低于 7.2V进入回充状态 if (battery 720 state ! STATE_CHARGING) { state STATE_RETURNING; } switch (state) { case STATE_INIT: servo_set_angle(90); delay_ms(200); state STATE_CLEANING; break; case STATE_CLEANING: { uint16_t dist ultrasonic_measure(); if (dist 0 dist 30) { state STATE_AVOIDING; avoid_obstacle(); // 在避障状态里执行完整转向 state STATE_CLEANING; } else { // 正常前进直线清扫 motor_set_speed(500, 500); } } break; case STATE_RETURNING: // 回充逻辑在下一节说明 handle_return_charging(); break; case STATE_CHARGING: // 充电时保持静止 motor_set_speed(0, 0); break; default: break; } }状态机的核心价值在于把功能模块解耦调试避障的时候不需要管回充代码调回充的时候也不需要关心清扫逻辑。在毕设答辩现场评审老师最喜欢问的就是你这个系统怎么从清扫切换到回充的用状态机回答会非常清晰。4. 自动回充与自适应巡航的算法设计4.1 基于电压阈值的回充触发与红外引导对接自动回充功能分成两个层次触发条件和物理对接。触发条件很简单——ADC 采集到电池电压低于 7.2V 就进入回充模式。但对接过程就需要额外硬件了我在充电座上放了一个红外发射管38kHz 载波机器人头部装了一个红外接收管利用红外信号的强度差异来寻找充电座的方向。回充的实现逻辑是循环搜索void handle_return_charging(void) { // 旋转搜索红外信号 for (int i 0; i 12; i) { // 原地右转 30 度扫描一圈 motor_set_speed(400, -400); delay_ms(200); motor_set_speed(0, 0); uint8_t ir_strength read_ir_signal(); // 返回红外接收强度 0~255 if (ir_strength 200) { // 发现充电座直线前进靠近 motor_set_speed(400, 400); delay_ms(300); // 检测充电连接引脚是否接触 if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12)) { // 接触成功后进入充电状态 state STATE_CHARGING; motor_set_speed(0, 0); } return; } } // 扫描一圈没找到充电座随机旋转后重试 motor_set_speed(300, -300); delay_ms(500); motor_set_speed(0, 0); }这里强调一个参数细节红外接收的强度阈值 200 是我在室内环境实测得到的经验值如果环境里有阳光直射红外背景噪声会升高这个阈值可能需要调到 150 或者更低。GPIO_ReadInputDataBit读取的是充电座上机械触点的通断信号——充电座被机器人顶住后两个金属触点接触引脚从低电平变高电平。4.2 覆盖清扫的弓字形路径规划覆盖清扫是这个项目里看起来最简单、实际上最容易做砸的部分。很多人的覆盖清扫就是让小车主直线行走、碰壁随机转弯这种方式的覆盖率只有 40% 左右走了大量重复路径。我实现的弓字形清扫基于一种简单的矩形区域划分void coverage_cleaning(void) { // 弓字形路径先沿着当前行走到边界 motor_set_speed(500, 500); // 前进 // 用前方超声波检测边界小于 20cm 认为到达墙壁 while (ultrasonic_measure() 20) { delay_ms(50); } motor_set_speed(0, 0); // 横向偏移右转 90 度前进 25cm再左转 90 度 turn_left_90(); motor_set_speed(300, 300); delay_ms(1000); // 25cm 需要约 1s motor_set_speed(0, 0); turn_right_90(); // 下一行反向清扫 motor_set_speed(500, 500); // ... 重复直到清扫完整个区域 }弓字形路径的覆盖率能达到 85% 以上剩下 15% 是边缘不可达区域。关键参数是横向偏移量 25cm这个值来自机器人本体宽度20cm加上 5cm 重叠量保证相邻清扫行之间没有缝隙。如果机器人的物理尺寸不同这个参数需要重新标定。4.3 自适应巡航的速度调整策略自适应巡航是区分这个项目与普通碰碰车做法的关键点。它的思想是机器人在宽敞区域全速前进在靠近障碍物或狭窄通道时自动减速。实现方式是根据超声波距离动态计算目标速度uint16_t smooth_speed_by_distance(uint16_t dist) { uint16_t speed; if (dist 80) { speed 800; // 距离远全速前进 } else if (dist 30) { // 30~80cm 区间速度线性下降 speed 800 - (uint16_t)((80 - dist) * 500 / 50); // 当 dist30 时speed 800 - 50*500/50 300 // 当 dist80 时speed 800 } else { speed 300; // 距离近低速逼近 } return speed; }这段代码的距离-速度映射是线性关系参数的含义是每接近障碍物 1cm速度下降 10。这个线性关系在实际运行中效果尚可但如果想让运动更顺滑可以改成二次曲线或者加一个低通滤波器避免速度突变。注意smooth_speed_by_distance要和避障逻辑配合使用——巡航函数只负责调速不负责转向转向由状态机里的避障分支处理。5. Keil 工程部署、烧录调试与常见问题排查5.1 标准库工程的目录结构与配置拿到项目源码后第一步不是急着编译而是先对照目录结构确认文件完整性。标准库工程的正确组织方式是把芯片启动文件、外设库函数、用户代码分开放置。这个工程的根目录下应该包含Project.uvprojxKeil5 工程文件、LibrariesSTM32F10x 标准外设库、Usermain.c、stm32f10x_it.c 等用户文件、HARDWARE电机驱动、超声波、舵机等硬件驱动文件。编译之前需要确认三个关键配置项Device 选择 STM32F103C8Target 里的晶振频率填 8MHz注意不是 72MHz这是 PLL 倍频后的值C/C 选项卡里的 Define 填STM32F10X_MD,USE_STDPERIPH_DRIVER。第二个 Define 决定编译器是否使用标准外设库的驱动层如果漏掉所有RCC_APB2PeriphClockCmd之类的函数都会报隐式声明错误。这个工程基于标准外设库开发使用stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_rcc.c、stm32f10x_adc.c、stm32f10x_i2c.c、stm32f10x_can.c等库文件。其中stm32f10x_i2c.c在部分旧版固件库里有 EEPROM 模拟的坑如果你的代码里没有实际用到 I2C可以在工程设置里把该源文件排除编译减少固件体积。5.2 烧录方式与调试技巧烧录建议使用 ST-Link V2在 Keil 的 Options for Target - Debug 选项卡里选择 ST-Link Debugger然后进入 Settings。与 STM32 相关的常见烧录错误是这个Error: No STM32 Target Found!90% 的情况是连接线接触不良或者 SWDIO/SWCLK 引脚被复用成了普通 GPIO。检查这个报错的方法是按住复位键点击烧录后约 1 秒松开复位让 MCU 在启动瞬间进入编程模式。调试时的关键观察变量是超声波测距返回值和电机 PWM 脉宽。我在代码里预留了一个串口调试通道PA9USART1_TX以 115200 波特率输出调试信息printf([DEBUG] dist%d cm, battery%d.%d V, speed_L%d, speed_R%d\r\n, dist, battery/100, battery%100, left_speed, right_speed);通过串口工具观察这个输出可以直观看到避障触发距离是否合理、电压阈值是否需要调整。调试过程中我发现超声波测距在电机运转时会出现尖峰噪声——电机火花放电产生的电磁干扰会影响 Echo 引脚的边沿检测。解决方法是软件滤波连续测三次取中间值。5.3 常见问题排查对照表这是我在调试过程中踩过的最常见的问题及其对应解法现象可能原因排查方法电机不转但有 PWM 输出驱动板供电不足或共地缺失万用表量驱动板 VCC 与 GND 电压确认与 STM32 GND 导通超声波一直返回 0Trig 引脚配置错误或模块损坏用示波器看 Trig 是否有 10us 脉冲再看 Echo 是否有回波舵机抖动PWM 频率不对或电压不足确认 PWM 频率是 50Hz舵机电源单独供电自动回充触发后找不到充电座红外信号过弱或阈值过高实测接收引脚电压把阈值从 200 调低到 150进入避障后原地打转左右电机转速不一致用串口输出左右 PWM 实际值校准差速5.4 参数标定的实用技巧做毕设答辩或者课程设计时评委最常问的就是你的避障距离为什么设 30cm速度为什么是 500不能只答经验值要给出标定方法。避障距离与机器人制动距离直接相关——满速行驶下从发出停车指令到完全停下需要约 15cm所以避障距离至少要大于制动距离的两倍。标定方法是让机器人在平坦地面上行驶用卷尺量刹车距离。速度值 500 对应的是 PWM 占空比 50%电机输出电压 3.7V实际转速约 100RPM。标定速度的方法是让机器人直线行走 1 米计时并计算速度然后调整占空比使得速度符合你的设计目标。这些标定过程和实验数据写进毕设论文里就是最扎实的第三章内容。6. 离线地图编码与区域识别进阶前面章节实现的覆盖清扫是不依赖地图的机器人只知道走了多少行不知道走到哪了。如果想让项目在答辩时更有亮点可以把这套系统升级成带简单地图编码的清扫——不需要 SLAM 那么重只在 Flash 里存一张网格清理表。实现方式如下把地面划分为 20cm x 20cm 的网格每组用一个 bit 表示是否已清扫。机器人通过轮式里程计用电机测速编码器计算位移或者用固定速度乘以时间做近似计算当前位置然后在内存里更新对应的 bit。#define MAP_WIDTH 10 // 10 个网格覆盖 200cm #define MAP_HEIGHT 10 // 10 个网格覆盖 200cm static uint8_t map_cleaned[MAP_WIDTH * MAP_HEIGHT / 8]; // 用位图存储 // 标记某个网格已清扫 void mark_grid_cleaned(uint8_t x, uint8_t y) { if (x MAP_WIDTH || y MAP_HEIGHT) return; uint16_t idx y * MAP_WIDTH x; map_cleaned[idx / 8] | (1 (idx % 8)); } // 查询网格是否已清扫 uint8_t is_grid_cleaned(uint8_t x, uint8_t y) { if (x MAP_WIDTH || y MAP_HEIGHT) return 1; uint16_t idx y * MAP_WIDTH x; return (map_cleaned[idx / 8] (idx % 8)) 0x01; }注意位图存储效率的问题——10x10 的网格只需要 100 个 bit即 13 字节 Flash 空间这对 64KB Flash 的 F103C8T6 来说微不足道。使用位图而不是数组是为了让答辩评委看到你理解内存优化而这一页 PPT 通常会为你加分不少。自适应巡航的反应速度也值得验证把超声波测到的数据画进 Excel你会看到真实数据有 ±2cm 左右的抖动。去除抖动的方法不是无限加大滤波窗口会降低反应速度而是用一个简单的滑动平均加限幅滤波——两次测量差值超过 10cm 就认为是异常噪声丢弃。到这里这套 STM32 扫地机器人项目从硬件引脚规划、PWM 调速、超声波避障、状态机调度、自动回充、覆盖清扫到地图编码都有了可以抄作业的代码和参数依据。建议在实际部署时按照电机驱动 - 超声波避障 - 状态机 - 回充 - 覆盖清扫的顺序分步验证每个模块单独跑通后再合并整个系统就能稳定运行。本文还有配套的精品资源点击获取
返回列表