
简介一份面向STM32嵌入式开发学习者的本科毕业设计论文主题为驿站取货小车系统适合自动化、电子信息、物联网等专业学生作为课程设计或毕业设计的参考资料。论文以STM32F103C8T6单片机为核心完整覆盖系统方案制定、硬件电路设计、软件程序设计以及仿真调试四大部分。硬件方面详细说明主控模块选择、电机驱动、OV7670图像采集、电源供电及其他外围电路软件方面讲解主程序、数字识别、按键处理、电机控制及LCD1602显示等子模块其中数字识别部分细致梳理了灰度处理、二值化、图像分割与识别流程。仿真调试部分给出实物展示与实验效果便于理解循迹和显示模块的实际工作状态。资源包仅含一个文档大小约为十点七五兆文件为可直接阅读的论文全文目录结构清晰。目前已有两百二十九人学习下载适合需要参考具体设计思路、硬件连接和软件流程的初学者及开发者。1. 基于STM32的驿站取货小车到底在解决什么问题驿站取货小车本质上是一个微型AGV它要在驿站货架间的狭窄通道里把包裹从某个格口取回并送到出口或者把新到的包裹从入库口运到对应格口。如果整个系统只靠遥控或上位机远程指令会受到网络延迟和操作精度的双重限制所以在车端放一颗STM32让它自己完成循迹、避障、取货机构动作这三件离散但必须按顺序发生的事。适合谁看呢正在做基于STM32的毕业设计、想把仓库或驿站里的转运环节自动化的一线工程师以及第一次拿STM32做移动平台的嵌入式开发者。这篇文章不写如何搭一个App去遥控小车而是把车端该有的硬件选型、外设配置、运动控制算法和调试手段讲清楚。你会看到STM32在整个系统中不是“能亮灯就行”的存在它要同时承担定时器PWM输出、编码器采集、PID运算、状态机调度等多重任务。下面从系统拆分讲起。2. 系统总体架构与STM32主控选型2.1 驿站取货小车的功能模块拆分驿站里的小车和工厂里的AGV相比路径更短但动作序列更琐碎要会让车、要在指定格口精准停下、要把包裹从格口轻轻推出来再走。这些动作落到工程上可以拆成四个层次感知层六路红外循迹传感器、两路超声波避障、左右轮霍尔编码器有的方案还会加一个称重传感器判断包裹有没有放到位。决策层STM32主控承担传感器数据读取、运动解算、状态机切换、串口协议解析。执行层两个带编码器的N20减速电机、一个推杆舵机、蜂鸣器和状态指示灯。通信层和驿站调度系统的串口或RS485接口也用来输出调试信息。这样分层的好处是每层可以独立更换。比如今天用红外循迹明天驿站地面铺了反光地砖想换成电磁循迹只需要改感知层代码和传感器硬件不影响PID和状态机的上层逻辑。我见过不少同学把感知、决策、执行搅在一起一个状态里既能读传感器又能控电机后面想加个格口编号识别都无从下手。2.2 为什么用STM32而不是树莓派或Arduino很多第一次接触嵌入式的人会问为什么不用树莓派树莓派跑Linux可以做复杂的视觉识别和全局路径规划但它开机慢、功耗高驿站地面灰尘大SD卡在震动环境里容易坏。Arduino则反过来上手快、库函数全但面对多路编码器高速计数、定时器中断嵌套和PID运算时性能和资源都比较吃紧而且当你想用免驱USB转串口同时挂几个外设时引脚和中断优先级很快就排不开了。STM32恰好站在二者之间。以STM32F103C8T6为例72MHz主频、20KB SRAM、64KB Flash内部有3个16位普通定时器和1个高级定时器还有5个USART、2个I2C、2个SPI。对驿站取货小车这种“两路电机PWM、两路编码器正交计数、六路GPIO循迹、一路舵机PWM、一路串口”的外设规模不用外扩芯片就能全部接住。更重要的是STM32有硬件PWM和正交编码器接口编码器计数不需要CPU介入MCU内核可以腾出来跑控制算法和协议栈这一点Arduino很难做到。2.3 STM32最小系统与外设资源规划先列清楚引脚分配再画PCB能省掉后面排线的很多折腾。我一般会在开发初期做一张资源占用表贴在工位旁边外设数量建议引脚定时器/外设说明电机PWM左右轮2路PA8、PA11TIM1_CH1、TIM1_CH420kHz PWM编码器左轮A/B2路PA0、PA1TIM2_CH1/CH2正交编码模式编码器右轮A/B2路PA6、PA7TIM3_CH1/CH2正交编码模式循迹传感器6路PB0-PB5GPIO上拉输入数字量0/1超声波避障2路PB6-PB9GPIO输入输出Trig和Echo舵机推杆1路PA9TIM1_CH250Hz PWM调试串口2路PA2/PA3、PA10/PA9USART2、USART1115200bps蜂鸣器/LED2个PB10、PB11GPIO推挽输出指示状态注意STM32F103的TIM1和TIM2在引脚上可能有remap选项选型时先查数据手册确认不会有物理引脚冲突。如果出现冲突优先换编码器接口因为PWM在不同定时器之间迁移更容易编码器的正交解码逻辑对定时器有硬性要求。STM32最小系统本身只有六个元件8MHz晶振、两个20pF负载电容、复位按键和10k上拉电阻、BOOT0下拉电阻、3.3V电源加去耦电容。这部分可以直接买市面上的最小系统核心板然后画一块底板承接电机驱动、传感器和电源转换项目周期能省一半。底板上我习惯把电机驱动和单片机地线做单点连接避免大电流回路干扰编码器信号。3. 硬件电路设计电机驱动、传感器与电源3.1 直流电机驱动电路与PWM频率选择动力部分推荐N20微型减速电机额定电压6V带霍尔编码器减速比1:30空载转速约300rpm。这种电机在2公斤负载下跑驿站平地绰绰有余且编码器输出AB相信号可以直接接入STM32定时器。驱动芯片用TB6612FNG它比老的L298N压降小得多逻辑部分3.3V可直连STM32不必再加电平转换芯片。TB6612每个通道需要两个方向引脚AIN1/AIN2和一个PWM使能脚我的接法是方向引脚用普通GPIOPWM使能脚接定时器通道。这样想换向就电平翻转想调速就改占空比。用STM32CubeMX将TIM1的通道配置为PWM Generation预分频PSC设3自动重载值ARR设999主频72MHz下PWM频率为72MHz/(4×1000)18kHz这个频率高于人耳敏感区电机啸叫很小又不会因为频率太高导致MOS开关损耗过大。// main.c 中启动PWM并设置初始占空比为0 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, 0);代码先启动了PWM输出通道再把比较寄存器清零防止上电瞬间电机猛转。第三个参数0就是CCR值范围0到999。实际占空比等于CCR/(ARR1)也就是说想输出60%占空比CCR写600而不是0.6。新手经常把浮点占空比直接当CCR用导致电机停转或全速这是我在调试现场看到最多的低级错误。3.2 循迹、避障与编码器接口驿站地面通常是浅色地砖用TCRT5000这类红外反射式传感器很可靠模块自带比较器输出数字信号白底输出高电平黑色引导线输出低电平。每路占用一个GPIO配置成上拉输入。代码里周期性读取即可但要注意传感器间距要小于引导线宽度的三分之一六路并排布局大约35mm跨度可以覆盖最常见的2cm宽引导线。编码器接口则需要小心处理。N20电机的霍尔编码器是开漏输出板子上必须加上拉电阻到3.3V否则信号边沿不够陡高速时定时器会漏计数。接线方式是编码器A相接定时器CH1、B相接CH2在CubeMX中把定时器模式选为Encoder Mode硬件自动完成正交解码和倍频。// 读取编码器计数值TIM2为例返回int16_t有符号数 int16_t encoder_left (int16_t)__HAL_TIM_GET_COUNTER(htim2); __HAL_TIM_SET_COUNTER(htim2, 0); // 读取后清零周期内计数这段代码先取计数值再清零。之所以不连续两次读取做差是因为控制周期是10ms定时器中断驱动用清零点方式可以避免中断延迟导致的时间基准漂移。缺点是在读取和清零之间可能有几个脉冲漏掉但在10ms周期内误差不到千分之一可以忽略。编码器一圈约300个脉冲1:30减速后车轮转一圈输出9000个脉冲精度完全够用。3.3 取货机构的舵机控制取货执行机构我做过两种方案推杆式和夹爪式。驿站格口里包裹尺寸变化大推杆式可靠性更高用一个MG995大扭力舵机带动L型推板把包裹从格口推到小车托盘上。MG995堵转电流可达1.2A必须单独供电不能吃单片机3.3V。舵机控制用的是50Hz PWM脉宽1ms对应0度2ms对应180度。因为和电机PWM频率不同需要单独用一个定时器配置为PSC71、ARR19999这样计数频率1MHz整体PWM频率50HzCCR值从1000到2000正好对应1ms到2ms。// 舵机角度控制函数angle_ccr取值1000到2000 void servo_set_angle(uint16_t angle_ccr) { __HAL_TIM_SET_COMPARE(htim4, TIM_CHANNEL_1, angle_ccr); } // 推杆完全伸出servo_set_angle(2000); // 推杆完全收回servo_set_angle(1000);角度和脉宽不是严格线性实际装车后要用尺子量推杆行程标定出“格口边缘刚好对齐”的CCR值再写进代码。别直接用0度到180度的理论映射每台舵机的中位都有差异。3.4 电源方案与滤波电源是驿站小车最容易出诡异问题的地方。我用两节18650锂电池串联标称7.4V分成三路电机驱动VM端直接取自电池中间放一个470uF电解电容吸收堵转电流尖峰。STM32和传感器用AMS1117-3.3降压3.3V输出端并联10uF钽电容和100nF陶瓷电容。舵机用LM2596降压到5V注意LM2596是开关电源输出纹波较大舵机地线和单片机地线之间用0欧电阻单点连接避免噪声串入模拟地。注意AMS1117最大输出电流只有1A如果超声波模块、循迹传感器和STM32同时工作总电流约300mA余量充足。但千万别把舵机接在AMS1117后面舵机启动瞬间的1A电流会直接把3.3V拉到2V以下造成单片机复位。很多人喜欢把陶瓷电容全换成钽电容追求低ESR但在高性价比设计里陶瓷电容在5V直流偏压下实际容量会衰减到标称值的一半左右所以模拟电源部分保留一个钽电容数字部分用X5R陶瓷电容就够。4. 运动控制与取货逻辑的STM32实现4.1 PID速度环的参数设计与代码差速小车的运动控制分两层外层确定左右轮的目标速度来自循迹或指令内层是每个轮子的速度闭环。速度环最常用增量式PID输出的是PWM占空比调整量不容易产生大幅超调也天然带一点记忆效应。typedef struct { float kp, ki, kd; float target; float last_err; float integ; float out; } Pid_t; int16_t pid_calc(Pid_t *pid, float feedback) { float err pid-target - feedback; pid-integ err; // 抗积分饱和积分限幅 if (pid-integ 200) pid-integ 200; if (pid-integ -200) pid-integ -200; float out pid-kp * err pid-ki * pid-integ pid-kd * (err - pid-last_err); pid-last_err err; // 输出限幅避免占空比进入死区 if (out 950) out 950; if (out 50) out 50; return (int16_t)out; }这个函数在10ms定时器中断里调用feedback是当前周期编码器脉冲数target由上层循迹算法给定。积分限幅很重要如果小车被卡住几秒钟积分不会无限累积解除卡阻后不会猛地窜出去。输出下限50是因为占空比太小时电机根本转不动留在死区里反而控制不住。参数初始值经验数据是kp20、ki5、kd2然后根据响应曲线微调启动时低频振荡甚至抖动kp偏大降到12-15。匀速阶段实际速度总比目标值低一截加大ki但别超过10。高速急停时过冲明显加kd抑制但kd太大会让编码器噪声被放大出现高频甩动。4.2 循迹与路口判断的六路传感器策略驿站路径大多是直线加直角弯循迹策略用位置偏差比用模糊规则更简单。六路传感器从右到左编号1到6中间两路压线时认为车头正对引导线偏右时左轮减速右轮加速偏左时反过来。void line_follow(uint8_t sensor_state) { int16_t left_speed, right_speed; switch (sensor_state) { case 0x18: // 中间两路压线直行 left_speed BASE_SPEED; right_speed BASE_SPEED; break; case 0x10: // 车头偏右左轮加速 left_speed BASE_SPEED 30; right_speed BASE_SPEED - 30; break; case 0x08: // 车头偏左右轮加速 left_speed BASE_SPEED - 30; right_speed BASE_SPEED 30; break; case 0x3C: // 六路全压线视为路口 left_speed BASE_SPEED; right_speed BASE_SPEED; break; default: left_speed BASE_SPEED; right_speed BASE_SPEED; break; } pid_left.target left_speed; pid_right.target right_speed; }这里用状态字节的位来表示哪几路传感器压线0x18是中间两路0x10是偏右那一路。BASE_SPEED一般取编码器计数周期内40个脉冲约合0.2m/s转弯时差速30个脉冲能让车头在一秒内完成纠偏。在这个场景里六路全压线代表十字路口此时要停车确认格口号或按预定方向转弯。如果只用两路传感器小车到路口悬空会误判为出线然后乱打方向盘。4.3 取货流程状态机取货听起来是连续动作但实际要拆成离散状态否则任何一个环节出错都会让小车“精神错乱”。我用一个枚举型状态机状态之间通过条件变量跳转typedef enum { IDLE, MOVE_TO_GRID, ALIGN, PUSH, RETRACT, MOVE_TO_EXIT, DONE } SysState; void state_machine_run(void) { static SysState state IDLE; static uint32_t state_timer 0; switch (state) { case IDLE: if (receive_pickup_cmd()) state MOVE_TO_GRID; break; case MOVE_TO_GRID: line_follow(read_line_sensors()); if (check_arrive_at_grid()) { state ALIGN; state_timer HAL_GetTick(); } break; case ALIGN: // 超声波测距距离格口边缘小于5cm认为到位 if (ultrasonic_distance() 5.0f) state PUSH; break; case PUSH: servo_set_angle(2000); // 推杆伸出 HAL_Delay(500); state RETRACT; break; case RETRACT: servo_set_angle(1000); HAL_Delay(500); state MOVE_TO_EXIT; break; case MOVE_TO_EXIT: line_follow(read_line_sensors()); if (check_exit_position()) state DONE; break; case DONE: send_uart(pickup_complete\n); state IDLE; break; } }状态机里每一拍只能执行一个case所以循迹和避障不会同时乱调舵机。push和retract用了HAL_Delay阻塞延时这是有意的因为推杆动作期间小车本身就应该停着不动。但循迹路径上严禁用HAL_Delay否则编码器采样会断PID瞬间失控。这里500ms延时足够推杆伸出到位如果换了型号更慢的舵机要实测后改大。4.4 串口通信协议与解析小车和驿站管理系统的通信我用的是最简单的行协议ASCII字符指令换行符结尾。上位机下发“G:3:5”表示去第3排第5格小车收到回“S:0”表示开始执行到达后回“P:OK”。串口中断接收积累到换行符后置标志主循环里解析。void USART1_IRQHandler(void) { uint8_t byte; if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_RXNE)) { HAL_UART_Receive(huart1, byte, 1, 0); rx_buffer[rx_index] byte; rx_index (rx_index 1) % BUFFER_SIZE; if (byte \n) rx_complete 1; } } void handle_command(void) { if (!rx_complete) return; rx_complete 0; if (strncmp((char*)rx_buffer, G:, 2) 0) { int grid atoi((char*)rx_buffer[2]); target_grid grid; HAL_UART_Transmit(huart1, (uint8_t*)S:0\n, 4, 100); } }这段代码里接收缓冲区用了环形结构防止一帧内数据被异步任务覆盖。HAL_UART_Receive最后一个参数0表示非阻塞读因为RXNE标志已经置位数据一定可取。协议里必须约定帧最大长度我的实现里BUFFER_SIZE是64字节超过就从头覆盖避免一帧脏数据让解析器跑飞。5. 系统调试与关键参数整定技巧5.1 用串口日志可视化PID响应小车跑起来的第一个夜班通常耗费在调PID上。最直接的办法是把目标速度和实际速度都通过串口发出来PC端随便用哪个串口助手画个时间序列图就能看出超调和振荡。我用USART2以115200波特率输出一行文本每50ms发一次int16_t fb_l read_encoder_speed_left(); int16_t fb_r read_encoder_speed_right(); char buf[64]; int len snprintf(buf, sizeof(buf), T:%d,%d F:%d,%d\r\n, pid_left.target, pid_right.target, fb_l, fb_r); HAL_UART_Transmit(huart2, (uint8_t*)buf, len, 100);调参路径是先清掉ki和kd只加kp让小车速度出现小幅振荡后把kp往回退20%作为基础值再逐步加ki消除静态偏差最后加kd压过冲kd大约是ki的三分之一。每轮改完参数要跑同一段直道对比别在转弯处调速度环弯道对差速的干扰会让你判断错方向。5.2 编码器、PWM和定时器的三个常见坑第一个坑是编码器方向反了电机正转时计数器递减。解决方法是CubeMX里把Encoder Mode的Polarity设为相反或者交换A、B相在代码里的解析顺序不用改硬件。第二个坑是PWM死区带动不了电机TB6612在电源电压被大电流拉低时逻辑阈值会抖动给VM和GND之间并一个100uF电解电容能解决大部分问题。第三个坑是16位计数器溢出如果码盘线数高编码器在20ms内就能数满65535回绕导致速度计算出现负值毛刺。排查方法是在串口日志里看速度曲线有没有周期性掉到负数的尖峰有就换成32位计数模式F103的TIM2和TIM5可以配置为32位。5.3 用看门狗兜底再验证三个验收指标最后一道防护是STM32内部独立看门狗IWDG主循环里每200ms喂一次狗。代码跑飞或陷入死循环时看门狗会在1秒左右强制复位让小车回到IDLE状态等待新指令而不是在货架中间原地空转把电池放干。注意IWDG一旦开启就无法在调试时关闭用ST-Link单步调试前要先把喂狗代码注释掉否则每走一步就会触发复位。验收我只看三个数字第一重复停靠精度连续10次执行同一取货指令车头基准线前后偏差不超过20mm第二取货成功率100次动作里包裹被顺利推出不掉的次数要高于95%第三连续往返不宕机时长正常密度下跑4小时以上不出现死机。这三个数字比任何花哨的功能描述都有说服力跑完这组测试再顺手看一眼复位源寄存器RCC_CSR确认没有随机复位这个系统就可以交付给驿站长期跑了。本文还有配套的精品资源点击获取