ARTICLE DETAIL

资讯详情

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

基于STM32F103ZET6的步进小车四合一控制源码解析

基于STM32F103ZET6的步进小车四合一控制源码解析 简介STM32F103ZET6步进电机智能小车完整程序源码基于Keil开发环境编写支持红外遥控、避障、跟随、循迹四种工作模式适合电子竞赛、课程设计与智能小车自制等场景。系统以ULN2003驱动28BYJ-48步进电机通过VS1838B接收红外遥控信号并借助1602液晶显示当前状态代码按遥控、避障、跟随、循迹功能模块划分结构清晰便于移植和学习。源码包共204个文件包含40个C源文件、41个头文件以及Keil工程配置文件、hex烧录文件、链接脚本和批处理工具等压缩后大小仅4.12MB可快速下载使用。全部程序已在作者自制的同型号小车上实测可用烧录后可立即体验红外遥控、自动避障、跟随与循迹功能且关键模块均有注释适合入门参考。目前已有397人学习下载对于需要掌握步进电机控制、红外通信与多传感器协作的STM32开发者来说是一份实用的代码范本。1. 一辆车上装四种控制逻辑STM32F103ZET6步进小车的源码拆解路径同一个底盘上集成红外遥控、避障、跟随、循迹四种功能大多数人的第一反应是先砍掉两三样只保核心。但这套STM32F103ZET6源码全部做完了且执行机构用的是28BYJ-48步进电机加ULN2003驱动板不是常见的直流减速电机。步进电机在开环状态下就能把车轮转角控制到固定角度转弯半径和直线距离都可以按脉冲数推算这是它适合教学实验的原因代价是四相八拍时序、加减速调度和轮组差速都得自己写。下面按驱动接线、NEC红外协议解码、三模式仲裁、加减速与失步自查四个层次拆解这套源码尤其适合正在做课程设计、竞赛练习或准备从直流电机迁移到步进电机的读者。2. ULN2003驱动28BYJ-48的接线与八拍步序先解决“走起来”的问题2.1 四相八拍步序表和脉冲当量换算28BYJ-48是永磁减速步进电机内部转子齿数对应的电气步距角是5.625°减速比标称1/64实际为1/63.6835。电机绕组每换相一次输出轴只转动5.625°/64≈0.0879°轮子转一整圈需要64×8512个半步这里的“步”指半步序列中的一次换相。实际项目里常把输出轴的脉冲当量直接写成360°/512≈0.703°/步所有转向角度、直线距离都基于这个值换算。12V版本的绕组电阻比5V版本高工作电流小但四拍模式下转矩波动大低速启动时容易卡死在齿槽转矩最低点所以源码采用八拍半步模式。八拍切换时绕组重叠导通始终有两个或多个线圈保持励磁不存在完全没有转矩的过渡点。下表是源码中实际使用的八拍序列表拍序A(IN1)B(IN2)C(IN3)D(IN4)励磁相11000A21100AB30100B40110BC50010C60011CD70001D81001DA在“前进两轮同向正转”的安装方式下左转和右转实际靠差速实现。差速转弯的步数差等于轮子走过的弧长差除以脉冲当量换算关系。若轮径65mm、轴距120mm转过90°时两轮弧长差约为π×120/2≈188mm对应半步数188/(π×65/512)≈471步。把这471步作为转向基础步数在程序里乘上0.9~1.1的修正系数可以补偿两个轮子打滑差异和装配误差。2.2 引脚分配与GPIO初始化代码源码的引脚分配不是随意定的思路是电机控制引脚集中在GPIOB低8位便于一次BSRR指令同时更新两个轮子红外信号输入集中在GPIOA低4位LCD1602放在GPIOC避免和调试用的USART1冲突。以我手里的实车接线为准引脚分配如下功能STM32引脚ULN2003/模块端子左轮步进A~DPB0~PB3ULN2003(1) IN1~IN4右轮步进A~DPB4~PB7ULN2003(2) IN1~IN4两块驱动板分开VS1838B红外输出PA0接收头OUT接PA0GND共地红外避障模块OUTPA1数字输出检测到障碍输出低电平红外循迹左传感器PA2白线高/黑线低红外循迹右传感器PA3白线高/黑线低LCD1602PC0~PC6RS/RW/EN占前三个D4~D7占后四个两块ULN2003驱动板必须与STM32F103ZET6共地否则电机绕组电流回路会经信号线回流轻则GPIO波形发毛重则烧掉PA0/PB0端口。ULN2003板上的指示灯在无脉冲时偶尔闪烁这是达林顿管漏电流导致的不代表程序出错。GPIO初始化部分这样写void GPIO_Init_MotorAndSensor(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // VS1838B输出端上拉 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); }关键点有两个。第一电机引脚用推挽输出STM32F103ZET6在50MHz翻转速率下GPIO电平切换只要几十纳秒完全满足28BYJ-48每拍最短1ms的换相周期这里不需要PWMULN2003是达林顿开关电路只有导通和关断两种状态。第二VS1838B用上拉输入而不是浮空输入因为接收头本身是集电极开路结构无载波时输出高电平依赖外部上拉启用内部上拉能省掉一颗4.7kΩ电阻同时也让下降沿更陡外部中断不容易漏触发。2.3 步进电机驱动函数与转向执行逻辑轮子动作的核心是Motor_Step函数它同时更新左右两组绕组的励磁相位const uint8_t step_table[8] {0x08, 0x0C, 0x04, 0x06, 0x02, 0x03, 0x01, 0x09}; void Motor_Step(int8_t dir_L, int8_t dir_R) { static uint8_t idx_L 0, idx_R 0; if (dir_L 0) idx_L (idx_L 1) 0x07; else if (dir_L 0) idx_L (idx_L 7) 0x07; if (dir_R 0) idx_R (idx_R 1) 0x07; else if (dir_R 0) idx_R (idx_R 7) 0x07; GPIOB-BSRR (0xFF 16) | (step_table[idx_L] 0) | (step_table[idx_R] 4); }step_table里的值是引脚位掩码0x08对应PB3值按A→AB→B→BC→C→CD→D→DA顺序排列。dir_L和dir_R取值为1、-1、0分别控制正向、反向、停止。idx_R通过7实现取模减法等效于反向走一拍。GPIOB-BSRR低16位写1置位引脚、高16位写1清零引脚这一句把左轮PB0~PB3和右轮PB4~PB7一次性更新完毕避免分两次写GPIO时中间状态导致轮子抖动。上层控制封装成四条指令void Car_Forward(uint16_t steps) { for (uint16_t i0; isteps; i) Motor_Step(1, 1); } void Car_Backward(uint16_t steps) { for (uint16_t i0; isteps; i) Motor_Step(-1, -1); } void Car_TurnLeft(uint16_t steps) { for (uint16_t i0; isteps; i) Motor_Step(0, 1); } void Car_TurnRight(uint16_t steps){ for (uint16_t i0; isteps; i) Motor_Step(1, 0); }转向采用“单轮停转”而不是“双轮反转”是有讲究的。28BYJ-48经过64倍减速后输出扭矩大约300g·cm双轮反转转弯时两个轮子同时克服地面摩擦车身会产生明显横摆抖动单轮转弯时另一轮保持锁死相当于绕固定点转动轨迹更接近圆弧。如果要做原地掉头才把左右轮步序反向但循迹场景下单轮转弯更利于传感器留在线内。3. VS1838B红外遥控解码NEC协议按位拆分与按键映射3.1 NEC协议的符号宽度和测量窗口VS1838B输出的信号是反相的无红外载波时输出高电平检波到38kHz载波时输出低电平。所以NEC协议的引导码在接收端表现为9ms低电平加4.5ms高电平典型逻辑0是562.5µs低加562.5µs高逻辑1是562.5µs低加1687.5µs高。帧结构固定为32位8位用户码、8位用户反码、8位键码、8位键反码最后跟一个560µs的结束脉冲。不需要上升沿下降沿同时采样只测两个下降沿之间的间隔就能区分三个符号。原因是NEC协议两次下降沿的间隔完整携带信息逻辑0的间隔是562.5562.51125µs逻辑1是562.51687.52250µs引导码是9000450013500µs。三者数值跨度很大阈值可以给得很宽抗干扰能力强。STM32F103ZET6主频72MHz把TIM5预分频设为71后计数频率变成1MHzTIM5-CNT每1µs加一两次CNT的差值就是µs单位的下降沿间隔。3.2 外部中断加定时器测宽解码源码VS1838B的OUT接PA0配置为下降沿触发外部中断TIM5负责计数。main函数初始化时需要把EXTI_Line0下降沿触发、NVIC中断优先级分组都配好然后在中断里完成解码void TIM5_Config(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); TIM_TimeBaseInitTypeDef TIM_Init; TIM_Init.TIM_Prescaler 71; // 72MHz/721MHz每格1µs TIM_Init.TIM_Period 0xFFFF; // 16位自动重装 TIM_Init.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM5, TIM_Init); TIM_Cmd(TIM5, ENABLE); } void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) SET) { EXTI_ClearITPendingBit(EXTI_Line0); static uint16_t last 0; static uint8_t bit_cnt 0; static uint32_t tmp 0; uint16_t now TIM5-CNT; uint16_t diff now - last; last now; if (diff 10000) { bit_cnt 0; tmp 0; // 引导码重新开始收集 } else if (diff 1500) { tmp | (0x80000000 bit_cnt); // 逻辑1 bit_cnt; } else if (diff 800) { // 逻辑0 bit_cnt; } else { bit_cnt 0; // 小于0.8ms视为干扰 } if (bit_cnt 32) { g_ir_code tmp; g_ir_flag 1; bit_cnt 0; } } }关键参数取值逻辑diff大于10ms才认定是引导码因为一次按键过程中不会出现第二次这么长的低电平窗口。逻辑0和逻辑1的分界阈值取1500µs落在1125µs和2250µs中间偏安全侧即使电机转动导致电源纹波让脉宽偏移10%也不会误判。小于800µs的间隔直接丢弃这是为了过滤电机换相瞬间GPIO串扰产生的毛刺。主循环用查询方式取结果解码不卡电机if (g_ir_flag) { g_ir_flag 0; uint8_t addr g_ir_code 0xFF; uint8_t addr_inv (g_ir_code 8) 0xFF; uint8_t cmd (g_ir_code 16) 0xFF; uint8_t cmd_inv (g_ir_code 24) 0xFF; if (cmd (uint8_t)~cmd_inv) { // 反码校验通过再执行按键动作 } }反码校验值得保留。NEC发送端会同时发送反码校验通过才执行能避免遥控器电池电压下降时产生的误码导致小车突然转向。3.3 键码映射表与四模式下遥控接管按键值取自帧的第16到第23位也就是cmd字节。下面是这套源码实际使用的映射表键码(hex)遥控器按键小车动作0x45CH-前进100步0x46CH后退100步0x47CH左转80步0x48PREV右转80步0x44NEXT停止并释放绕组0x43EQ切换到循迹模式0x40PLAY切换到避障模式0x41VOL-切换到跟随模式0x42VOL切换到手动模式模式切换用main函数里的变量g_mode控制每个模式对应一个处理函数。遥控器在自动模式下按前进、后退键会强制切回手动模式这样设计的好处是循迹过程中车跑偏时按一下遥控方向键就能立即接管不需要先按VOL再操作。4. 循迹、避障、跟随三模式的状态机切换与实时仲裁4.1 红外循迹的阈值判断与转向修正循迹部分使用两个TCRT5000红外反射模块分别接PA2、PA3。TCRT5000的模拟输出经比较器在模块上调好阈值后输出数字电平源码里不重复做ADC采样直接用GPIO读取。不同厂家模块的白/黑电平定义不同有的是白线输出高、黑线输出低有的正好相反接线后先用万用表量一次再确定判断方向。循迹状态机的判定逻辑如下表这里把“1”定义为“检测到黑线”左传感器右传感器状态判定执行动作00出线或十字路口两轮停转200ms再原速前进01偏向右侧右轮停转左轮正转80步修正10偏向左侧左轮停转右轮正转80步修正11正常直线两轮继续前进“出线”状态下立刻原地停止200ms很重要它让车体在十字路口停下来判断而不会直接冲出赛道。步进电机在这个场景的优势很明显突然停止不会像直流电机那样惯性滑行80步修正量在程序里就是固定的角度增量不需要PID误差积分逻辑简单且可复现。如果要把两路传感器升级成五路循迹模块中间一路专门处理路口判断左右两路继续做偏差修正这套代码的判定逻辑基本不用动。4.2 避障模式的扫描式决策单路红外避障模块只能判断正前方有没有障碍判断不了障碍偏向哪边。源码采用时序扫描策略发现障碍后先右转90°此时若障碍离开探测范围说明障碍在左侧直接直行若右转后仍探测到障碍说明障碍范围较大再左转180°从右侧绕开。写成状态机如下typedef enum { AVOID_GO, AVOID_TURN_R, AVOID_TURN_L } avoid_state_t; avoid_state_t avoid_state AVOID_GO; void Avoid_Handle(void) { uint8_t obs GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); switch (avoid_state) { case AVOID_GO: if (!obs) { // 1为无障0为有障 Car_Stop(); avoid_state AVOID_TURN_R; } else { Car_Forward(1); } break; case AVOID_TURN_R: Car_TurnRight(90); if (!obs) { avoid_state AVOID_TURN_L; // 转过90°仍有障碍 } else { avoid_state AVOID_GO; } break; case AVOID_TURN_L: Car_TurnLeft(180); avoid_state AVOID_GO; break; } }注意TURN_R分支执行完Car_TurnRight(90)后立刻又读了一次obs因为Car_TurnRight是阻塞式函数执行完90步后障碍位置已经变化必须以新状态重新判断。实际调试时把避障模块的VCC单独从5V引脚引线不要和电机共用接线端子否则电机绕组电流拉低模块供电obs会误变。4.3 跟随模式的差分速度控制跟随模式与避障共用红外模块区别在于避障只看有/无跟随看的是连续反射强度。如果红外模块输出的只是数字量就采用时间积分方式检测到目标存在的时间占比越高判定目标越近。更常规的做法是双路模块把左右两个传感器放在车头两侧45°方向用距离差生成差速err left_dist - right_dist; speed_L base_speed Kp * err; speed_R base_speed - Kp * err;Kp取值0.5~2输出限幅到步进电机每拍间隔不小于1.2ms。若是单路模块退化为PID距离保持目标在设定距离内则前进太近则后退中间地带停车。这里的“距离”是红外模块导通占比估算出来的相对值不是毫米数跟随精度在5cm以内。要注意28BYJ-48带减速箱响应速度天然比直流电机慢因此跟随模式下Kp不宜超过2否则会来回振荡。4.4 用函数指针数组统一三模式调度模式切换最容易写成一大段if-else嵌套模块多了之后新增模式要翻动主函数。源码用函数指针数组做分发typedef enum { MODE_MANUAL, MODE_TRACK, MODE_AVOID, MODE_FOLLOW, MODE_MAX } mode_t; mode_t g_mode MODE_MANUAL; typedef void (*mode_fn)(void); const mode_fn g_mode_table[MODE_MAX] { [MODE_MANUAL] Manual_Handle, [MODE_TRACK] Track_Handle, [MODE_AVOID] Avoid_Handle, [MODE_FOLLOW] Follow_Handle, }; void Mode_Dispatch(void) { g_mode_table[g_mode](); }主循环只调用Mode_Dispatch()遥控按键设置g_mode后下一次循环立即生效。函数指针数组优于switch的地方在于数组内容是const存放在Flash里不占RAM新增模式只需要加一个枚举成员和数组元素后续维护成本低。实测Mode_Dispatch执行时间在10µs以内远小于步进电机一拍1.2ms不会造成电机丢步。5. 步进加减速打断和失步自查落地调试时的两个硬指标5.1 定时器中断改频实现梯形加减速阻塞式for循环驱动只适合低速演示连续高速运行丢步率会直线上升。更稳的做法是把每拍触发挪进TIM3中断速度由ARR实时调整const uint16_t interval_table[9] {5000, 3000, 2200, 1800, 1500, 1300, 1200, 1100, 1000}; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); Motor_Step(g_dir_L, g_dir_R); static uint8_t level 0; if (g_accel 1 level 8) { level; TIM3-ARR interval_table[level]; } else if (g_accel -1 level 0) { level--; TIM3-ARR interval_table[level]; } } }interval_table单位是微秒代表拍间隔从5000µs出发逐级降到1000µs对应速度从200步/s提高到1000步/s。每级停留200ms是下限再快容易进共振区。28BYJ-48在200~400Hz有明显噪声把每级停留时间放到300ms以上能缓解。停止时不要直接抬ARR要逐级放回5000µs再停避免惯性冲击齿轮打齿。5.2 失步表现与绕组复位自检失步是步进开环控制最隐蔽的问题程序计数走完了车轮实际位置没到不报错、不停机后续距离计算全偏。现场查失步先用机械法断电后手拨车轮28BYJ-48带减速箱手感应均匀且有阻尼某个角度明显卡顿说明绕组短路或某一路驱动断路。电学法留两组变量调试时打印对比extern volatile int32_t g_steps_done; static int32_t g_steps_target; // TIM3中断里每走一拍 g_steps_done // 停车后判断是否等于目标值 if (g_steps_done ! g_steps_target) g_fault 1;有偏差先看中断嵌套是否把Motor_Step夹在中间再看ULN2003的IN1~IN4和GPIOB是否错位。另外每次切进循迹前做一次绕组复位按八拍序退到第一拍最后两拍各保持10ms让转子停稳在初始角循迹的转向基准才一致。最后一个参数经验把interval_table最小值改到700µs空跑20秒摸ULN2003温度。超过60℃说明绕组长时间导通导致温升偏高把最小值放宽到1200µs温度会立刻回落。12V的28BYJ-48参考这条经验线就能把寿命拉长换成5V版本还需要再保守一档否则电机内部温升会损坏减速箱润滑脂。本文还有配套的精品资源点击获取
返回列表