STM32智能小车毕设指南:从供电驱动到PID速度闭环

STM32智能小车毕设指南:从供电驱动到PID速度闭环 简介基于STM32F103的智能小车毕业设计说明文档面向电子、嵌入式方向学生及开发者系统阐述智能小车控制系统整体设计思路重点解决循迹与避障的软硬件协同问题。文档为1个DOCX格式文件压缩包大小约3.51MB涵盖摘要、绪论、硬件电路、软件算法、实验测试与结论等完整章节目录清晰便于查阅。目前已有177人学习/下载。详细介绍了STM32F103最小系统、电机驱动电路、红外探测及超声波避障电路的设计方法并结合Keil环境给出PWM调速、舵机转向、循迹与避障算法的程序流程与实现思路同时包含实验测试环节对静态验证和动态运行性能进行分析为毕业设计撰写或智能车项目开发提供了从硬件到软件、从理论到实测的系统参考。1. 毕业设计选STM32智能小车最该先解决的是供电和驱动打开《毕业设计基于stm32的智能小车设计说明.docx》这类文档里面往往已经有完整的系统框图和软件流程。可真把车按文档装起来最常见的结局是通电后一边轮子转、另一边不动或者小车一上电猛地冲出去撞到墙。问题几乎都出在同一处供电和驱动没打通后面的代码全都白调。STM32智能小车的难度不在单片机本身而在用GPIO、定时器和中断把电机与传感器正确地组合起来。下文按硬件驱动、循迹避障、测速闭环、答辩前排错的顺序展开对刚点完LED灯的新手和想补上速度闭环的老手都适用照着这个顺序能少走一个月的弯路。2. 把STM32最小系统板接上电机驱动GPIO和PWM先跑通2.1 选型与接线C8T6够用驱动芯片优先TB6612先定硬件。STM32F103C8T6最小系统板俗称蓝板的GPIO数量、定时器资源和72MHz主频对小尺寸智能小车完全够用。方向引脚4个、PWM输出两路、编码器输入两路、循迹传感器两到四路、超声波一路全部接完也只占二十个左右引脚。预算宽裕选STM32F103ZET6或更高配的开发板也可以但板子做大后走线变长接触不良的排查难度跟着上来本科毕设阶段不建议为了“看起来高级”换大板。电机驱动芯片的选型比单片机更关键小车的动态表现一大半由驱动决定。市面常用方案是L298N和TB6612两者差别明显项目L298NTB6612FNG逻辑电压5V2.7~5.5V3.3V可直接驱动电机电压5~12V2.7~13.5VVM脚饱和压降约2V约0.5V持续电流2A需加散热片1.2A模块本身不用散热上电表现空载发热明显温升很小TT马达正常行驶时电流几百毫安堵转瞬间能到1.5A左右TB6612余量足够而且压降小同样电池电压下电机两端得到的电压更高。L298N便宜十块钱左右但发热、噪声和PWM波形失真都要额外处理。常见做法是两个电机配一个TB6612模块AIN1/AIN2/BIN1/BIN2四个方向输入接STM32的GPIOPWMA/PWMB接PWM通道STBY接高电平使能。我一般把方向引脚放在PA4/PA5/PB0/PB1PWM放在TIM3的CH1/CH2PA6/PA7这样后续编码器接在PB6/PB7时四个端口组互不挤占。2.2 操作STM32的GPIO方向引脚配推挽输出方向引脚的GPIO配置本身没有技巧但有一个原则同一时刻只允许一个方向电平组合出现。以左电机为例IN11、IN20正转IN10、IN21反转IN11、IN21是急停制动。有些同学把两个方向脚分两行代码初始化结果顺序一调上电瞬间电机先猛转半圈。用标准库把方向引脚集中初始化同时把上电默认状态设为“两个电机都反转”void motor_gpio_init(void) { GPIO_InitTypeDef io; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); io.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5; // 左电机 IN1/IN2 io.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 io.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, io); io.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; // 右电机 IN3/IN4 GPIO_Init(GPIOB, io); GPIO_SetBits(GPIOA, GPIO_Pin_5); // 上电默认IN10,IN21反转 GPIO_SetBits(GPIOB, GPIO_Pin_1); // 右电机同样默认反转 }这组配置单独放一个函数的意义在于让初始化顺序可控先确定两个电机在“反转”状态再开放PWM输出永远不会出现上电就向前冲的情况。方向电平的切换建议封装成 motor_set() 函数内部根据目标方向是FWD、REV还是STOP 分别配置两个引脚不要在主循环里到处直接写 GPIO_SetBits 和 GPIO_ResetBits。后面接上编码器发现方向反了也只需要在这个函数里改一处。2.3 用定时器输出PWM频率、占空比和初始值调速用PWM。TIM2/TIM3/TIM4都可以做选择依据是引脚位置和是否与编码器端口冲突。上面方向放在PA4/PA5/PB0/PB1PWM放TIM3的CH1/CH2对应PA6/PA7完全错开。PWM输出要配三样东西GPIO复用推挽、定时器时基、输出比较结构体void pwm_init(void) { GPIO_InitTypeDef io; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); io.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; // TIM3_CH1 / CH2 io.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽才输出PWM io.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, io); TIM_TimeBaseStructInit(tb); tb.TIM_Prescaler 71; // 72MHz 分频后计数频率1MHz tb.TIM_Period 63; // 1MHz / 64 15.6kHz TIM_TimeBaseInit(TIM3, tb); TIM_OCStructInit(oc); oc.TIM_OCMode TIM_OCMode_PWM1; // CNT CCR 时输出有效 oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; // 初始占空比0防止上电抖动 oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, oc); TIM_OC2Init(TIM3, oc); TIM_Cmd(TIM3, ENABLE); }TIM_Prescaler为71得到1MHz计数频率TIM_Period为63意味着PWM周期是64个计数约15.6kHz。这个频率在电机驱动上听不到明显啸叫也不会因为电流纹波大让驱动芯片过热。64级占空比调节对小车足够每级约1.6%步进配合后面的PID不会因为分辨率不够造成可见抖动。有两个参数后续调车时常要改如果电机在低速时发抖把PWM频率提到20kHz以上但要查驱动芯片的最高PWM频率TIM_Pulse初始值必须给0极性保持High。若把极性改成Low占空比0对应的是恒高电平上电瞬间电机会全速转答辩演示翻车多数是这种细节。2.4 供电与地线电机和单片机必须分开供供电是整个项目最容易被低估的环节。带编码器的减速电机启动瞬间电流能瞬时超过1A两个电机同时起步电池电压可能跌到保护板断电阈值MCU瞬间复位现象就是“车一动就重启”。常见做法是电池7.4V或11.1V先进TB6612的VM脚用驱动模块板载稳压输出5V给最小系统板的5V脚STM32板载AMS1117再转3.3V给芯片和传感器。这样电机大电流不经过最小系统板电压跌落被稳压器吸收一部分。还需要在电池接入端并一个大电解电容和一个小瓷片电容470uF以上电解电容吸收换向电流尖峰104陶瓷电容滤高频。电机反转的瞬间会产生反向电动势地线电位被拉偏如果控制板和驱动板的地线分开接跳变电压可能超过GPIO容忍范围。所有地必须连成同一个网络电池负极、TB6612的GND、最小系统板GND、编码器和传感器GND全部短接。上电后用万用表量一遍各路电压再插下载器VM应在电池电压附近5V输出不低于4.8V3.3V在3.28V左右。哪一路偏差过大先查稳压芯片方向和电容极性。3. 循迹与避障TCRT5000和HC-SR04怎么接怎么读3.1 循迹模块选型与阈值校准循迹最常用的是TCRT5000红外反射模块板上集成红外发射管、接收管和LM393比较器输出数字电平。检测距离在1~25mm之间可调安装高度10~15mm效果最好太高容易误判太低会蹭到纸面赛道。接线三个脚VCC接3.3V或5VGND接地信号脚接STM32的GPIO。完整的一套四路模块占四个GPIO毕业设计演示两路通常已经够用四路的优势是十字路口判断更可靠。校准是循迹能跑的前提。把车放在黑线正上方调节模块上的蓝色电位器让模块指示灯恰好发生亮灭翻转。注意翻转方向因模块型号而异不能想当然认为“遇黑线输出低电平”。有条件就用万用表量一下信号脚白区读数在0V附近黑线读数在3.3V附近记下这两个值再去写判断逻辑。太多同学的循迹失败不是代码逻辑错而是阈值没校准传感器在线上和线外输出同为一种电平程序无从判断。接线时左传感器接PA0右传感器接PA1GPIO配成上拉输入。PA0/PA1同时还是TIM2的CH1/CH2输入脚但只做数字输入时不冲突后面超声波的定时器用TIM2时注意别复用同一端口。3.2 两路循迹转向决策两路传感器对称分布在黑线两侧正常直行时两个都在白色区域输出都为0。车往右偏时左轮压到黑线左传感器输出1需要左转回到线上。完整决策如下左传感器右传感器车身状态控制动作00居中直行10向右偏出左轮减速、右轮保持01向左偏出右轮减速、左轮保持11十字或交叉线直行决策代码核心就几行void track_ctrl(void) { uint8_t l GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); uint8_t r GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); switch ((l 1) | r) { case 0b00: set_speed(50, 50); break; case 0b10: set_speed(30, 52); break; // 右偏减速左轮 case 0b01: set_speed(52, 30); break; // 左偏减速右轮 default: set_speed(50, 50); break; // 十字路口保守直行 } }转向用“一侧减速、另一侧保持”比“单边停止”平滑在半径较小的弯道上不容易脱线。占空比差值按偏差程度分两档更好小幅偏差用45/55大幅偏差用30/55。这样PWM从50起步能跑到70遇到急弯不减速也不冲出赛道。等第四章的速度闭环做完这里的占空比参数会替换成左右轮的目标速度差。3.3 用STM32定时器测HC-SR04回波宽度避障常用HC-SR04超声波模块VCC接5VTrig和Echo各接一个GPIO。Trig给一个10us以上的高电平脉冲模块发射8个40kHz超声波回波到达后Echo脚输出高电平高电平持续时间就是声波往返时间。距离公式是 distance_cm time_us / 58这个系数是声速340m/s推出来的宿舍环境不用温度补偿。Echo高电平最长约23ms对应4米量程比一个系统滴答还长主循环里干等会卡死整辆车。常见做法是Echo接外部中断引脚上升沿进中断记下当前计数值下降沿进中断算差值。先配一个1MHz计数的定时器让CNT单位直接是微秒void echo_timer_init(void) { TIM_TimeBaseInitTypeDef tb; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructInit(tb); tb.TIM_Prescaler 71; // 72MHz分频到1MHzCNT每1即1us tb.TIM_Period 0xFFFF; // 自由计数不自动重载 TIM_TimeBaseInit(TIM2, tb); TIM_Cmd(TIM2, ENABLE); }Echo接PA4配成外部中断EXTI4上升沿和下降沿都使能。中断里做两次时间戳记录volatile uint32_t echo_start, echo_duration; void EXTI4_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line4) ! RESET) { if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4) Bit_SET) { echo_start TIM2-CNT; // 上升沿记录起点 } else { uint32_t now TIM2-CNT; echo_duration (now echo_start) ? (now - echo_start) : (0xFFFF - echo_start) now; } EXTI_ClearITPendingBit(EXTI_Line4); } }回波宽度换算距离dist_cm echo_duration / 58。HC-SR04有2cm左右盲区小于3cm按3cm处理。1MHz计数下定时器65535us约65.5ms大于最大回波23ms正常量程内不会溢出但代码里保留回绕补偿逻辑是稳妥做法。读取时若echo_done标志没置位直接沿用上一次的距离值避免主循环拿到一个无意义的0。3.4 循迹与避障的优先级切换两个传感器同时工作时小车不能一边循线一边绕障。常见做法是在主循环入口判断距离小于25cm进入避障模式执行“右转90度、前进一段、回正”的动作距离恢复安全后切回循迹。避障期间不要再调track_ctrl否则传感器信号会让模式反复切换车在原地扭秧歌。用一个超时锁避免这个问题uint8_t mode MODE_LINE; uint32_t avoid_start_ms 0; void loop_decide(void) { if (ultrasonic_cm() 25 mode MODE_LINE) { mode MODE_AVOID; avoid_start_ms systick_ms(); } if (mode MODE_AVOID systick_ms() - avoid_start_ms 600) { mode MODE_LINE; } if (mode MODE_LINE) { track_ctrl(); } else { avoid_ctrl(); } }avoid_start_ms 由SysTick维护的毫秒计数提供进入避障后至少锁定600ms。转向时间先按固定值调占空比20转1秒量实际转角再调整到接近90度。避障结束回到循迹时车头不一定正对线track_ctrl会按偏差表自动修正一般两三个决策周期就能回到黑线上。超声波Trig引脚可以用PA3和PA4的Echo放同一个端口初始化代码也更集中。4. 编码器测速与串口调PID直行的闭环怎么上4.1 带编码器的电机和TIM编码器模式入门套装配的TT马达不带编码器只能开环跑。要上闭环电机必须带霍尔编码器常见MG513这类AB相输出减速电机。编码器输出两路方波相位差90度正转A相超前B相反转相反。STM32通用定时器有编码器接口模式硬件直接在AB两路的全部边沿计数并自动判别方向相当于四倍频不需要软件判断方向也不需要外部中断防丢步。编码器信号线接PB6、PB7对应TIM4的CH1和CH2。配置核心如下void encoder_tim4_init(void) { GPIO_InitTypeDef io; TIM_TimeBaseInitTypeDef tb; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); io.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; io.GPIO_Mode GPIO_Mode_IPU; // 上拉最稳防止悬空误计数 GPIO_Init(GPIOB, io); TIM_TimeBaseStructInit(tb); tb.TIM_Prescaler 0; tb.TIM_Period 0xFFFF; // 编码器计数自由运行 TIM_TimeBaseInit(TIM4, tb); TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI1, // TI1和TI2双沿计数4倍频 TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM4-CNT 0; TIM_Cmd(TIM4, ENABLE); }三个细节容易被忽略。一是GPIO用IPU上拉输入编码器模块输出端若是开漏结构不上拉读不到电平二是TIM_EncoderMode_TI1表示两个输入都参与计数分辨率提到四倍频低速测速更稳三是方向由硬件自动判别TIM4-CNT会正向或反向计数读取时用有符号方式读否则反转时差值会变成65535附近的乱值。4.2 M法测速与每圈脉冲标定速度测量最常见的是M法固定时间窗内统计计数增量再换算成转速。每10ms读一次TIM4-CNT差值就是10ms内的脉冲数。每圈总脉冲数 编码器线数 × 4 × 减速比。以常见电机参数为例编码器线数13减速比30每圈脉冲 13×4×30 1560。但不同批次参数有偏差最可靠是实测标定让车轮悬空程序累计转过的总脉冲数同时数轮子转十圈用卷尺量这十圈的直线距离反推出“每米多少脉冲”。测速函数写法#define PULSE_PER_REV 1560 #define SPEED_SAMPLE_MS 10 int16_t encoder_get_speed_rpm(void) { static int16_t last_cnt 0; int16_t cur (int16_t)TIM4-CNT; // 有符号读取方向自含 int16_t delta cur - last_cnt; last_cnt cur; return (int16_t)((float)delta * 60000.0f / (PULSE_PER_REV * SPEED_SAMPLE_MS)); }强制转int16_t读取TIM4-CNT计数器从65535回绕到0时差值依然正确。10ms采样周期内脉冲数一般几百个离32767很远不会出现溢出。转速单位是rpm即每分钟多少转。如果电机速度变化很剧烈把采样周期拉长到50ms能平滑读数代价是PID响应变慢适合慢速巡线场景高速比赛还是保持10ms。4.3 增量式PID和串口调试速度闭环用增量式PID。增量式输出的是“每个周期在现有PWM基础上加多少”不累积历史误差存储积分饱和问题天然轻切入和调试都容易。结构体和更新函数如下typedef struct { float kp, ki, kd; float target, last_err, prev_err; int16_t output; } speed_pid_t; int16_t pid_update(speed_pid_t *p, float feedback) { float err p-target - feedback; float delta p-kp * (err - p-last_err) p-ki * err p-kd * (err - 2.0f * p-last_err p-prev_err); p-prev_err p-last_err; p-last_err err; p-output (int16_t)delta; if (p-output PID_MAX) p-output PID_MAX; if (p-output -PID_MAX) p-output -PID_MAX; return p-output; }调参从全零开始只给kp从0.5往上加观察轮子是否来回震荡。震荡后加kd抑制超调有稳态误差再加kiki从0.01量级起步。调参前最关键的一步是确认PWM方向和编码器方向一致如果电机正转但编码器计数为负PID会正反馈发散轮子越转越快直到撞限幅。发现方向反了先改motor_set或编码器取反再整定参数。用串口看实际值是调PID的基本功printf(L:%d rpm R:%d rpm outL:%d outR:%d\r\n, speed_l, speed_r, pid_l.output, pid_r.output);串口配置115200、8N1在10ms的PID周期结束后打印一行。打印频率过高会拖长控制周期阻塞式printf在波特率115200下每打印一字符约87us一行三十多个字符就要3ms左右明显占用周期所以printf放在主循环由标志位触发控制中断里只置标志不打印。4.4 让PID服务于循迹目标速度差循迹和速度闭环的结合点是把转向表达为“左右轮目标转速差”。直行时左右目标都是60rpm右转弯时右轮62、左轮40差值22。运动控制和转向决策彻底解耦track_ctrl里只负责把传感器状态翻译成目标速度传感器状态左目标rpm右目标rpm006060104062016240116060对比开环占空比的方案速度闭环在电池电压下降、地面摩擦变化时能自动补偿转弯时的速度差也稳定。毕业设计答辩时“用增量式PID保证车速稳定”是一个能展开讲两分钟的亮点前提是能说清楚比例项负责跟随目标、微分项抑制震荡、积分项消除静差三个作用以及为什么增量式更适合单片机做限幅。5. 答辩前最后一公里Keil、中断优先级与HardFault定位5.1 换电脑装Keil时常见的三个问题芯片包、驱动、USB识别答辩换电脑几乎是必然。装好MDK主程序后如果新建工程找不到STM32F103C8是因为没装器件包打开Pack Installer搜索“STM32F1xx_DFP”安装即可。下载器方面ST-Link需要装ST官方驱动DAP-Link走CMSIS-DAP协议免驱。插入电脑后设备管理器里看不到设备或显示感叹号先换USB线很多线只支持充电不支持数据再换一个USB2.0后置口避开延长线。还不行就把ST-Link固件升级一版升级完重新枚举一次。5.2 中断优先级与delay卡死中断里调用delay_ms是随机卡死的头号原因。SysTick的中断优先级如果比当前抢占它的中断低SysTick一直得不到执行delay就永远等不到标志。解决办法是全局只执行一次NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)所有外设的抢占优先级按两级配置SysTick的抢占优先级设为最低的15。另一个常见错误是不同外设用了不同的分组方式比如串口中断按Group_2配置、定时器按Group_3配置中断嵌套行为直接乱掉排查起来非常隐蔽。5.3 HardFault和栈检查程序跑飞进入HardFault_Handler时让调试器停在现场在HardFault_Handler打断点看Call Stack窗口和寄存器LR寄存器的值决定异常发生在主流程还是中断里PC附近的汇编能直接看到访问了哪个非法地址。常见原因不外乎数组越界、未初始化指针、串口缓冲区溢出。栈溢出检查有个土办法把栈区初始化为0xAA跑完一轮功能后查看从栈底向上还剩多少0xAA没被覆盖剩余不足总栈一半就把Stack_Size调大。5.4 加一个遥控手柄兜底如果传感器现场状态不好保留一个手动遥控通道能救场。用NRF24L01或蓝牙串口模块接自制遥控手柄接收端把UART配置成空闲中断加DMA收到完整帧再解析。帧格式自定义即可0xAA 0x55 油门 转向 校验。主循环里超过200ms没收到新帧切换回循迹或避障自动模式。若改用ESP8266做WiFi遥控接口同样是串口注意电平一致和波特率匹配。帧解析函数直接放在串口中断回调里处理不要在main函数里逐字节轮询否则主循环的PID周期会被拖长。本文还有配套的精品资源点击获取