拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32F103C8T6扫地机器人实战:引脚分配、PWM与编码器测速全解析

简介基于STM32F103C8T6的扫地机器人设计源码是一份评审得分98分的高分毕业设计项目面向嵌入式、计算机相关专业正在准备期末大作业、课程设计或毕业设计的学生也适合希望进行项目实战练习的学习者。资源包共230个文件压缩包约5.22MB以C源文件、H头文件、Keil工程文件及编译生成的hex、axf等类型为主工程中已集成STM32标准外设库的TIM、Flash、RCC、ADC、I2C、USART、CAN等模块驱动可直接打开编译和烧录验证。目前已有372人学习下载具备一定的参考价值。通过这份代码读者能快速掌握扫地机器人的整体工程结构与底层外设配置思路了解电机控制、传感器采集、串口通信等典型环节的实现方法既可作为答辩讲解素材也能在此基础上进行功能扩展与二次开发。1. 为什么这个项目要把 STM32F103C8T6 用到极致做扫地机器人设计主控选型往往比算法更先卡住人。STM32F103C8T6 这颗芯片在“高分项目”里出现频率极高原因很实际它只有 20KB RAM、64KB Flash、48 脚 LQFP 封装却同时需要管住电机调速、编码器测速、多路避障传感器、电池电量监测和清扫机构。真正做完一轮的人会告诉你这个项目的难点不在“跑起来”而在 48 个引脚怎么分配、定时器资源怎么复用、中断优先级怎么抢以及代码怎么组织才不会在调试时改一处崩三处。如果你准备把这个项目作为课程设计或竞赛作品第一件事要扭转的认知是不要急着买现成的智能小车底盘加一个成品扫地机模块那样分拿不高。常见做法是用一块 STM32F103C8T6 最小系统板搭配自己设计的电机驱动与传感器接口电路外挂 2~4 路红外避障、1 路防跌落、1 路编码器电机。这篇博文把硬件结构、PWM 驱动、串口调试、代码框架和常见坑按我自己的思路捋一遍新手按步骤能复现老手看参数和边界也有可对照的东西。2. 先把 STM32F103C8T6 最小系统板的引脚和资源盘清楚2.1 C8T6 的引脚紧张点不在 GPIO而在定时器通道STM32F103C8T6 是 48 脚封装去除电源、地、复位、BOOT、晶振之后可用 GPIO 在 37 个左右。看起来不少但你同时要接两个电机每个电机需要 2 路 PWM 或 1 路 PWM 加 1 个方向位、两路编码器每路至少 2 个输入捕获、3~5 路红外避障、1 路超声波或防跌落、一个蜂鸣器、一个 OLED 或者串口引脚立刻进入“挤一挤”的状态。/* 我常用的引脚分配HAL 库PA/PB 优先PC 留作备用 */ // 电机 PWMTIM2 的 CH1/CH2 输出到 PA0/PA1左侧驱动CH3/CH4 输出到 PA2/PA3右侧驱动 // 编码器输入TIM4 的 CH1/CH2 接 PB6/PB7左轮TIM3 的 CH1/CH2 接 PA6/PA7右轮 // 红外避障PB0~PB2 三路前方避障PB3 左侧、PB4 右侧 // 防跌落传感器PB5左前、PB8右前 // 串口调试PA9(TX)/PA10(RX) // 电池电压检测PA4ADC1_IN4 // 清扫电机PB1仅开关用 MOSFET 控制这里最容易踩的坑是一个定时器的多个通道如果同时做 PWM 和编码器输入会互相干扰。所以 PWM 一定要和编码器拆到不同的定时器上。典型做法是 PWM 用 TIM2编码器用 TIM3 和 TIM4避免 APB1 和 APB2 总线上的时钟配置混在一起后出现“电机转了一半突然停了”的问题。STM32F103C8T6 引脚功能表中PA0~PA7 同时属于 TIM2/TIM3 的输入输出通道不少项目组图省事把四个电机 PWM 放在 TIM2、编码器放在 TIM3结果两个 TIM 的时钟树没有各自配好调试时查了三天最后发现是 PWM 频率被编码器初始化改了。2.2 最小系统板与自制驱动板之间的信号电平问题蓝板即 STM32F103C8T6 最小系统板的 GPIO 是 3.3V 逻辑而市面上许多电机驱动模块L298N、TB6612 等的控制引脚虽然兼容 3.3V 输入但 L298N 的使能脚如果接了 5V 逻辑的 Arduino 习惯用法直接连到 C8T6 的 PB 口会有风险。保险的做法是如果驱动板是 5V 逻辑中间加一个 4 个 MOS 管的电平转换模块如果是 TB6612 这类标准 3.3V 驱动的模块直接接线没问题。还有一个很多人忽略的细节STM32F103C8T6 最小系统板上通常没有给 ADC 做基准电压电路VREF 和 VDDA 直接连到了 3.3V。如果你要用 PA4 采集电池电压需要用电阻分压把电池电压降到 3.3V 以下具体计算见后面的代码。不要直接把 7.4V 锂电池正极接 PA4那样片上 ADC 输入引脚会烧毁只能重新换芯片。2.3 我一般怎么验证最小系统板是否正常上电第一件事不是烧程序而是测三件事3.3V 和 GND 之间电压是否稳定在 3.28~3.32VNRST 引脚在按下复位键时能否拉到 0BOOT0 跳线是否处于 GND 位置。确认这三项没问题再连 ST-Link 烧录。一个高效的验证代码是让 PA9 串口以 115200 波特率周期性输出“SYSTEM OK”同时在 PB12 接一个 LED 翻转。如果串口能收到数据且 LED 以特定频率闪烁说明晶振起振、时钟树和调试串口都已就位。这个步骤能排除掉接近一半的“程序跑飞”问题——很多时候不是程序的问题而是芯片压根没跑起来。3. 扫地机器人的电机驱动与编码器测速必须放在同一个章节里调3.1 电机驱动的 PWM 频率选择与死区问题在 STM32F103C8T6 上配置 PWM 输出时有几个参数要一起决定预分频系数 PSC、自动重载值 ARR、脉冲宽度 CCR。PWM 频率由公式PWM频率 定时器时钟 / ((PSC1) * (ARR1))决定。对于普通直流减速电机如常见的 N20 电机PWM 频率一般选 10kHz 到 20kHz 之间。低于 5kHz 会听到明显的电流噪声高于 30kHz 时很多 MOS 驱动管的开关损耗增大电机反而没劲。// 初始化左电机 PWMTIM2_CH1/PA0PWM 频率 10kHz void Motor_PWM_Init(void) { TIM_TimeBaseInitTypeDef TIM_BaseStruct; TIM_OCInitTypeDef TIM_OCStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); TIM_BaseStruct.TIM_Period 7199; // ARR TIM_BaseStruct.TIM_Prescaler 0; // PSC72MHz/(01)/7200 10kHz TIM_BaseStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_BaseStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_BaseStruct); TIM_OCStruct.TIM_OCMode TIM_OCMode_PWM1; TIM_OCStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OCStruct.TIM_Pulse 0; TIM_OCStruct.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCStruct); TIM_OC2Init(TIM2, TIM_OCStruct); TIM_Cmd(TIM2, ENABLE); }上面这个配置里 PSC0、ARR7199对应 72MHz 定时器时钟下的 10kHz PWM 输出。如果你的驱动模块是 TB6612还需要把 STBY 引脚拉高否则电机永远不转。有的国产驱动模块把 STBY 和 GND 做了内部下拉这时你要通过 GPIO 输出高电平而不是直接接 3.3V否则引脚驱动能力不够会导致电机启动瞬间电压跌落。在调试 PWM 时手摸电机轴感受力度和听声音比看示波器更直观。如果电机在占空比 20% 以下完全不转大概率不是 PWM 配置问题而是电机驱动芯片的输入阈值或电机本身启动电压过高。扫地机器人这种启停频繁、经常遇到地毯和杂物的小功率应用建议把最小有效占空比设到 15%~20% 而非从 0 线性加速否则机器人到地毯边缘时会原地卡死。3.2 编码器测速STM32F103C8T6 的定时器编码器模式怎么配置测速有两条路一是外部中断读 A/B 相信号用 GPIO 模拟编码器计数二是直接使用 STM32 定时器的编码器接口模式。前者代码直观但 CPU 开销大后者是硬件自动处理、适合主频不高的 C8T6。我一般推荐直接用定时器的编码器模式这也是芯片数据手册中明确列出的功能。// TIM4 编码器接口模式初始化左轮编码器 A 相 PB6 / B 相 PB7 void Encoder_TIM4_Init(void) { TIM_TimeBaseInitTypeDef TIM_BaseStruct; TIM_ICInitTypeDef TIM_ICStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStruct); TIM_BaseStruct.TIM_Period 0xFFFF; TIM_BaseStruct.TIM_Prescaler 0; TIM_BaseStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_BaseStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, TIM_BaseStruct); TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_ICStruct.TIM_ICFilter 10; // 滤波器10 对应约 1.8us 脉宽滤波防抖动 TIM_ICInit(TIM4, TIM_ICStruct); TIM_SetCounter(TIM4, 0); TIM_Cmd(TIM4, ENABLE); }定时器编码器模式下计数器会根据 A/B 相的顺序自动加减TIM_EncoderMode_TI12 表示双沿计数即 A 相和 B 相的上升沿和下降沿都计数。分辨率是电机自带码盘线数的 4 倍例如电机码盘 13 线/转那么每转产生 52 个计数。读取时用short speed (short)TIM_GetCounter(TIM4);然后立即清 0否则 16 位计数器会溢出回绕。注意使用 TIM4 编码器后这个定时器就不能再做别的定时任务。如果要周期读取速度可以用定时器中断TIM6 或 TIM7 这些基本定时器避免和 TIM4 冲突。3.3 周期读取速度并转换轮速转速换算公式轮速(mm/s) 计数差值 / 4 / 线数 * 周长 * 读取频率。举例10ms 读一次短整数计数值为 52对应一圈13 线电机轮径 65mm那么速度 52 / 52 * 3.1415 * 65 / 0.01 6518 mm/s——这个数值一看就不对问题出在把每转的 4 倍频和线数搞混了。实际计数值是13*452对应一圈所以 10ms 内 52 个计数表示每秒 100 圈也就是 10000rpm。正常小车的轮速在 200~500mm/s 之间对应到 10ms 周期里大约只有几个到几十个计数。// 10ms 定时器中断中读取轮速 volatile int16_t left_encoder_cnt; volatile int16_t right_encoder_cnt; float left_speed_mm_s, right_speed_mm_s; void TIM6_IRQHandler(void) { if (TIM_GetITStatus(TIM6, TIM_IT_Update)) { left_encoder_cnt (int16_t)TIM_GetCounter(TIM4); right_encoder_cnt (int16_t)TIM_GetCounter(TIM3); TIM_SetCounter(TIM4, 0); TIM_SetCounter(TIM3, 0); // 13 线电机4 倍频轮径 65mm10ms 周期 // 每计数 1/(13*4) 圈 65mm * PI / 52 3.927 mm left_speed_mm_s left_encoder_cnt * 3.927f * 100.0f; right_speed_mm_s right_encoder_cnt * 3.927f * 100.0f; TIM_ClearITPendingBit(TIM6, TIM_IT_Update); } }定时器中断里做了浮点运算这在 72MHz 的 C8T6 上开销不小。优化方式是提前把系数算成定点数left_speed_mm_s left_encoder_cnt * 3927 / 1000或者直接保留计数差值、在 PID 计算时再转换。对于 5 年以上的开发者这里真正的考察点是如果你的电机在低占空比下抖动、轮速反馈不稳问题大概率是编码器误计数而非 PID 参数。加 IO 滤波电容10nF 到 100nF 对地比调软件滤波更有效。4. 避障、防跌落与清扫策略用 STM32F103C8T6 的 GPIO 中断和 ADC 组合实现4.1 红外避障的判定逻辑与阈值标定扫地机器人避障传感器的常见选型有三种红外对管便宜、环境光影响大、超声波成本高、扫地机上较少、ToF 激光测距效果好但接口复杂。STM32F103C8T6 引脚有限我一般用三路红外避障模块左边、中间、右边模块输出数字高低电平直接用 GPIO 读取。中间红外检测到障碍且两边无障碍整体后退 20cm 后随机转 90°左侧检测到障碍右转 45°右侧检测到障碍左转 45°三路都无障碍直行判定逻辑本身不复杂但要注意红外模块的盲区问题。常见 HC-SR501 之类的人体红外不适合做近距离避障而光电对管模块的检测距离一般在 2cm~30cm 可调。安装时要让三个传感器的探测区域有重叠但不互相干扰。如果三个传感器同时安装在一个平面上两两之间的串扰会导致中间传感器误报这在高分项目答辩时会被老师追问。每个模块上都有一个可调电位器用来设置检测距离阈值。标定方法简单把模块放在地面前方障碍物分别放在 5cm、10cm、15cm、20cm 处用万用表量模块输出记录翻转的临界点。注意THRESHOLD 旋钮调节的是灵敏度不是距离如果你发现检测距离不稳定先看供电电压。红外模块的工作电压范围通常在 3.3V~5V电压低于 3.0V 时发射管亮度不足检测距离会直接从 20cm 掉到 8cm。// 避障状态读取与转向决策 typedef struct { uint8_t front_left; uint8_t front_middle; uint8_t front_right; uint8_t drop_left; uint8_t drop_right; } SensorGroup; void Robot_Navigation_Task(void) { SensorGroup sg; sg.front_left GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_3); sg.front_middle GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0); sg.front_right GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4); sg.drop_left GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5); sg.drop_right GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8); if (!sg.drop_left || !sg.drop_right) { // 有跌落风险立即后退并掉头优先于避障 Motor_Control(0, 0); // 先停止避免惯性冲出去 Delay_ms(100); Motor_Back(200); // 后退 Delay_ms(300); Motor_Rotate_Right(250); // 原地右转 Delay_ms(250); } else if (!sg.front_middle) { // 中间有障碍 Motor_Back(180); // 后退 Delay_ms(250); Motor_Rotate_Left(220); // 左转随机方向 Delay_ms(350); } else if (!sg.front_left !sg.front_right) { // 只有中间障碍转固定角度 Motor_Rotate_Right(220); Delay_ms(300); } else if (!sg.front_left) { Motor_Rotate_Right(200); // 左侧障碍则右转 Delay_ms(200); } else if (!sg.front_right) { Motor_Rotate_Left(200); Delay_ms(200); } else { Motor_Forward(220); // 无障碍直行 } }这套逻辑的问题在于固定的Delay_ms在代码里会阻塞主循环扫地机一旦检测到障碍从后退到转向完成期间无法响应新的避障事件。实际项目里推荐用状态机替换掉阻塞延时。不过对“高分项目”来说能同时在文档里写出“阻塞延时方案的局限”和“改为状态机后带来实时性收益”比单纯贴代码更能拉开分数差距。4.2 防跌落传感器必须接入紧急停止通道防跌落传感器通常用红外对管斜向下安装检测与地面的距离。判断跌落的条件是“该传感器返回值与正常地面平坦值不一致”。常见误判来源是深色地毯、黑色瓷砖和阳光直射深色表面反射率低会被误判为空或洼地。我踩过的一个实际坑是由于 STM32F103C8T6 的 GPIO 内部上拉不够红外模块输出端悬空时读到的电平不稳定导致会随机触发“跌落”动作。解决方式是把防跌落传感器的输出接成外部中断EXTI的下降沿触发上升沿清标志。这样即使 GPIO 读取有毛刺也不会让主循环误判。// 外部中断初始化防跌落PA8 左前跌落 / PA11 右前跌落 / PA12 辅助 void EXTI_DropSensor_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; EXTI_InitTypeDef EXTI_InitStruct; NVIC_InitTypeDef NVIC_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStruct.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_11 | GPIO_Pin_12; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; // 内部上拉输入 GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource8); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource11); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource12); EXTI_InitStruct.EXTI_Line EXTI_Line8 | EXTI_Line11 | EXTI_Line12; EXTI_InitStruct.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStruct.EXTI_Trigger EXTI_Trigger_Falling; EXTI_InitStruct.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStruct); NVIC_InitStruct.NVIC_IRQChannel EXTI9_5_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStruct.NVIC_IRQChannelSubPriority 0; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); }防跌落优先级应该高于避障和正常运动。我在程序里设置了一个全局drop_flag主循环每次进入运动控制前先检查它。只要防跌落标志被置位立即停止电机。这一步很容易被忽略但答辩时老师往往会问“如果扫地机走到楼梯口你怎么保证它不掉下去”——光靠避障传感器朝前看是发现不了地面缺失的。4.3 用 ADC 实时检测电量与堵转保护STM32F103C8T6 内部自带 12 位 ADC最多支持 16 个外部通道。扫地机器人项目里我一般用一路通道检测电池电压一路检测电机电流如果有采样电阻的话。电池电压分压计算7.4V 锂电池2 节 18650 串联ADC 最大输入 3.3V分压电阻取 10K10K中点在满电时约 3.7V超过 3.3V 会削波所以取 10K6.8K7.4V 时中点电压 7.4 * 6.8/(106.8) ≈ 3.0V// 读取电池电压单位 mV uint16_t Battery_GetLevel(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); uint16_t adc_val ADC_GetConversionValue(ADC1); // 分压比6.8/(106.8) 0.4048 // ADC 满量程对应 3.3V因此实际电压 adc_val / 4095 * 3.3 / 0.4048 uint32_t voltage_mv (uint32_t)adc_val * 3300UL / 4095UL * 100UL / 40UL; return (uint16_t)voltage_mv; }电量低于阈值比如单节 3.3V即总压 6.6V时扫地机应该自动停止清扫并回到充电底座——如果没有充电底座就亮 LED 并蜂鸣报警。做这个功能的意义是电机在低电压下工作电流会飙升轻则卡死不转重则烧驱动管。堵转保护通常有两种实现一是检测编码器——如果 PWM 占空比大于 60% 而编码器反馈速度低于某个阈值判定堵转并停转 2 秒二是检测电流——通过采样电阻放大后接入 ADC软件设电流上限。前者不需要额外硬件更适合初版原型。5. 附加的清扫电机与底盘结构在 AB 面防跌落的基础上加一块边刷控制5.1 边刷电机控制与堵转保护扫地机器人除了两个行走轮还至少需要一个边刷电机有的带滚刷。边刷电机常见做法是用一个 MOSFET 控制开关不需要 PWM 调速因为边刷转速通常固定。C8T6 的 PB1 输出高低电平控制 MOS 管栅极MBR0520 之类的续流二极管加上就够用。边刷电机堵转非常频繁——头发丝、线缆、小石子都会卡住它。如果不加保护堵转电流会持续发热严重时烧毁 MOSFET。低成本方案是在 MOS 管源极串联一个 0.1Ω 采样电阻用运放差分放大器LM358放大后接入 STM32F103C8T6 的 ADC1_IN5PA5再设定堵转电流阈值。#define BRUSH_STALL_CURRENT_MA 800 // 堵转电流阈值 800mA #define BRUSH_STALL_CHECK_MS 300 // 持续 300ms 判定堵转 // 在主循环中周期性检查边刷电流 void Brush_Check_Stall(void) { static uint16_t stall_count 0; uint16_t current_ma ADC_GetBrushCurrent(); // 读取 PA5 转换结果并换算为 mA if (current_ma BRUSH_STALL_CURRENT_MA) { if (stall_count BRUSH_STALL_CHECK_MS / 10) { stall_count 10; // 每 10ms 检查一次 } else { GPIO_ResetBits(GPIOB, GPIO_Pin_1); // 关闭边刷 stall_count 0; // 尝试反向启动一次低电平 500ms 后重新开启 } } else { stall_count 0; } }反向启动的细节是DC 电机堵转后直接恢复同向 PWM大概率还是卡住。高级一点的策略是给一短反向脉冲让刷子反冲一下再恢复正常方向。反向脉冲宽度控制在 50ms~200ms强度不要超过正常工作电流的 120%否则容易把电机内部碳刷打坏。5.2 防跌落传感器安装位置与滤光罩防跌落传感器的安装位置通常在底盘前部边缘且要保证传感器红外发射管与接收管不被底盘遮挡。如果底盘是 3D 打印的建议把传感器开槽处做成向内凹的斜面防止灰尘堆积。一个关键参数传感器检测距离要大于底盘边缘到地面的垂直距离。如果底盘前缘离地 12mm那么防跌落传感器必须能稳定检测 20mm 以上距离保险起见在 30~40mm 处。在浅色地板和深色地毯之间的切换瞬间传感器输出可能连续翻转几次所以软件上一定要做 2~3 次连续读取一致才算有效否则扫地机每次从瓷砖到地毯会误判一次“台阶”。// 软件滤波连续 3 次采样一致才更新状态 uint8_t Drop_Filtered_Read(GPIO_TypeDef* port, uint16_t pin) { uint8_t sample1 GPIO_ReadInputDataBit(port, pin); Delay_us(100); uint8_t sample2 GPIO_ReadInputDataBit(port, pin); Delay_us(100); uint8_t sample3 GPIO_ReadInputDataBit(port, pin); if (sample1 sample2 sample2 sample3) return sample1; else return DROP_NORMAL; // 采样不一致时按正常处理防止抖动误触发 }6. 从“能转”到“能答辩”把参数标定和异常日志做成项目亮点6.1 串口调试命令与状态监控协议一个能拿高分的扫地机项目最后拼的不是能不能转而是能不能说出每个参数为什么是这个值。串口监控在这里扮演两个角色运行时实时查看各传感器状态和电机 PWM 值通过命令修改参数而不用重新编译烧录。我常用的串口协议很简单PC 发送一行带参数的命令MCU 解析后修改全局变量。协议格式为CMD:PARAM例如PWM_LEFT:180表示把左轮基础 PWM 设置为 180。// 串口接收解析在中断中收字符主循环里处理命令 void UART_Process_Command(char *cmd) { if (strncmp(cmd, PWM_LEFT:, 9) 0) { g_left_pwm_target atoi(cmd 9); printf(Set left PWM %d\r\n, g_left_pwm_target); } else if (strncmp(cmd, STALL_CUR:, 10) 0) { g_brush_stall_ma atoi(cmd 10); printf(Set brush stall current %d mA\r\n, g_brush_stall_ma); } else if (strncmp(cmd, STATUS, 6) 0) { // 打印全部变量状态机状态、传感器电平、PWM、编码器计数、电量 printf(State%d,LM%d,MM%d,RM%d,DR_L%d,DR_R%d\r\n, g_state, gpio_left, gpio_mid, gpio_right, drop_l, drop_r); } else { printf(Unknown command: %s\r\n, cmd); } }每次软硬件联调前先发一条STATUS确认所有传感器都正常比拿着万用表一根根量快得多。答辩演示时也建议放一台笔记本电脑连着串口屏幕实时滚动传感器数据评审老师会认为你有完整的测试验证环节。6.2 轮速闭环调试里的三个观察点码盘方向反了编码器计数值为负说明 A/B 相接反把 TIM 编码器配置里的TIM_ICPolarity_Rising改成TIM_ICPolarity_Falling或者直接在电机驱动端交换 A/B 线。PID 输出震荡如果P项增益超过 2 就出现左右电机交替加速的声音检查TIM_SetCompare的值是否被限幅。我一般把 PWM 限制在 0~900共 7200 的 12.5% 到 100%低于 120 直接输出 0避免低占空比下的非线性。编码器计数值跳变电机在强电流时电机电源地线会和信号地共用导致 STM32F103C8T6 的 GND 参考电平被抬高编码器信号产生毛刺。应对电机电源单独走线在编码器模块电源引脚加 10uF0.1uF 电容并且在代码里开 TIM_ICFilter10。6.3 一个提高效率的调试技巧OLED 滚动显示状态页如果不想每次调试都接电脑在 I2C 接口的 0.96 寸 OLED 上做两页状态显示第一页电量百分比、左右编码器计数、左右轮速、当前状态机编号第二页PWM 设定值、边刷电流、最近一次避障触发方向用 I2C1PB6SCLPB7SDA会默认占用 PB6/PB7——如果你把两路编码器放在同一端口直接冲突。所以 OLED 的 I2C 我放在 PB8/PB9 使用软件模拟 I2C而不是硬件 I2C1这一点在引脚分配时就要提前规划。PB6/PB7 留给左轮编码器 TIM4_CH1/CH2PB8/PB9 用 GPIO 模拟 100kHz I2C 信号实测刷新一帧 128x64 的 OLED 大约 15ms完全不影响 10ms 的控制周期。在最后一个调试阶段把传感器的原始电平、电机的理论速度、PID 输出值同时打印到串口用串口绘图软件画成曲线观察左右轮速度在直线行驶时是否一致。如果左轮总比右轮慢 10%大概率是两个电机负载差异造成的可以通过软件设置g_left_speed_offset 1.08修正而不是调 PID——调 PID 只会让两个轮都震荡。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门