ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于STM32的自动售货机设计:主控、电机驱动与状态机实现

2026/9/17 2:19:12 拓冰建站 浏览量
基于STM32的自动售货机设计:主控、电机驱动与状态机实现 简介基于STM32的自动售货机完整工程项目包含源代码与文档说明适合电子信息、自动化、计算机等专业学生用于课程设计、毕业设计或项目初期演示。项目基于STM32标准库开发覆盖定时器、Flash、RCC、ADC、I2C、CAN、USART等常用外设的驱动配置代码按功能模块拆分结构清晰、可读性好便于二次扩展。压缩包内共有229个文件以C源文件、头文件、编译生成的o/hex目标文件、Keil工程文件以及Markdown说明文档为主整体约8.07MB下载后可直接使用Keil打开工程完成编译、烧录和验证省去环境搭建与驱动移植的重复工作。目前已有293人学习下载代码均经过实际运行测试既适合嵌入式初学者对照源码理解外设初始化和售货机控制逻辑也可作为课程设计、毕业设计的基础框架进一步扩展支付模块、联网功能或更多货道联动控制提升项目的完整度与创新性。1. 基于STM32的自动售货机先立好主控、电机与状态机的三角关系很多人以为自动售货机就是把货道电机转一圈货掉下来收钱走人。真正动手之后才发现电机转起来只占整个工程的一小部分最难的是回答三个问题电机有没有把货真正推出去、支付结果怎么可靠地接进来、整个售货流程在异常情况下怎么回到安全状态。基于STM32的自动售货机项目本质上是用一块F103级别的芯片把PWM驱动、ADC电流采样、串口通信和有限状态机串成一个完整产品。对于刚做完流水灯和温湿度传感器的开发者这是一个能摸到产品边界的进阶项目对课程设计和毕业设计这套东西的源码量、文档量和可展示性也都非常合适。下面这套方案从硬件选型开始讲逐步落到代码结构和调试方法最后给出参数整定和文档组织的具体建议。2. STM32自动售货机的硬件选型与出货电路设计2.1 从最小系统到货道驱动STM32自动售货机需要哪些功能模块先明确这台机器的功能边界。常见的弹簨货道自动售货机有十几个货道每个货道一个直流减速电机电机旋转带动螺旋弹簧把货品推前掉落到取货口。主控需要做四件事接收按键选货信号、与支付模块完成金额确认、控制对应货道电机转动、判断货是否掉出。主控芯片一般选STM32F103C8T6。理由不复杂72MHz主频跑状态机和通信绰绰有余LQFP48封装手工焊接可行网上参考资料最多做毕业设计还有现成的开发板和仿真器生态。源码组织上遵循“驱动层、业务层、应用层”三层结构驱动层只操作寄存器或HAL库业务层跑状态机和超时逻辑应用层管菜单和日志。货道驱动是硬件的关键点。单路电机工作电流约0.3A到1A直接用IO口驱动不可能需要H桥或电机驱动芯片。常见做法是用TB6612FNG单颗芯片内置两路H桥峰值输出1.2A逻辑电压和电机电压分离比L298N的总线压降小、发热低也不必在原理图上画一堆分立MOS管。一个四路货道的小机器用两颗TB6612就够了。2.2 直流电机不会自己报告状态出货检测要靠采样电阻电机驱动本身不复杂难的是“出货成功”怎么判断。低成本方案中扛大梁的是电流检测直流有刷电机运转时电流大致能反映负载大小。弹簧空转、推着货走、货卡住三种状态电流特征分得开。具体做法是在电机低端串一支采样电阻。电机电流流过分流电阻产生压降经过运放放大后送入STM32的ADC。采样电阻用0.1欧姆/1W的贴片电阻放大倍数10倍那么ADC输入电压与电流的关系是1A对应1V。STM32的ADC参考电压为3.3V折算下来可测最大电流约3.3A留足裕量。需要特别注意的是电源分区。电机瞬态启动电流会拉低母线电压如果主控和电机共用一组电源ADC采样和Flash写入会出现偶发异常。正确做法是外部适配器12V输入后分两路一路经过DC-DC降压到5V给逻辑电路另一路直接给电机驱动。两路地在电源入口处单点相连避免地环流。2.3 IO分配表出货前先把引脚表定死引脚规划决定了后面布线和代码的改动量建议在画原理图之前就把引脚功能表定死。以下是一份四货道加扫码支付的最小分配方案功能模块引脚配置说明电机1方向/使能PA0、PA1推挽输出控制TB6612的AIN1/AIN2电机1 PWMPA8定时器1通道1输出10kHz PWM电机2方向/使能PA2、PA3推挽输出电机2 PWMPA9定时器1通道2电流采样ADCPA4、PA5ADC1通道4/通道5采样两路电机电流4x4矩阵按键PB0~PB7输入带上拉行列扫描OLED显示屏PB8、PB9I2C1地址0x3C支付模块串口PA9、PA10USART1115200 8N1调试日志串口PA2、PA3USART2115200EEPROM存储PB10、PB11I2C2AT24C02地址0xA0注意USART1的PA9与定时器1通道2复用冲突所以实际项目里PWM和串口要错开引脚上面表格只是一种示意。定引脚时第一件事是打开CubeMX的引脚冲突检查第二件事是把所有复用功能列成一张表贴在工位前面。2.4 初始化代码让定时器、ADC和GPIO先跑起来引脚和时钟配置用CubeMX生成骨架但关键参数要自己心里有数。PWM频率选择10kHz理由是两个一是避开人耳敏感频段电机轴和弹簧不会发出刺耳啸叫二是H桥开关损耗在10kHz级别还可以接受。占空比初始给80%电机在12V下转速约等于额定值的八成既能保证推力又不会让弹簧高速空转导致货品飞出。void Motor_Init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_TIM1_CLK_ENABLE(); // 电机方向引脚 PA0/PA1 配置为推挽输出 gpio.Pin GPIO_PIN_0 | GPIO_PIN_1; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, gpio); // PWM 引脚 PA8 配置为复用功能 gpio.Pin GPIO_PIN_8; gpio.Mode GPIO_MODE_AF_PP; gpio.Alternate GPIO_AF1_TIM1; HAL_GPIO_Init(GPIOA, gpio); // TIM1 输出 10kHz PWM默认占空比 80% // 时钟 72MHzPSC7ARR899得到频率 72M/(8*900) 10kHz htim1.Init.Prescaler 7; htim1.Init.Period 899; htim1.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(htim1); TIM_OC_InitTypeDef ocp {0}; ocp.OCMode TIM_OCMODE_PWM1; ocp.Pulse 720; // 900*80% ocp.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim1, ocp, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); }代码里设置的预分频和自动重装值决定了PWM频率修改时按公式Fpwm 72MHz / ((PSC1) * (ARR1))自行换算。Pulse字段就是比较寄存器值改变它可以动态调占空比。经验是占空比不要给到100%堵转时满占空比会让电流直接顶到电源上限给控制逻辑留不出区分空间。3. 用电流特征判定出货结果电机驱动与掉货检测的实现3.1 弹簧货道的负载特征与电机选型弹簧货道属于典型的LIFO出货结构最前排的货品被螺旋弹簧向前推进货品落下后弹簧继续空转。一个出货循环里电机负载经历了“启动浪涌、推货带载、空转”三个阶段。启动浪涌电流可达工作电流的3到5倍持续几十毫秒推货过程电流比空转高出20%到50%如果货品卡住电流会持续保持在高位不回落。电机选型方面货道电机是带减速箱的直流永磁电机减速比约1:50到1:100输出轴转速30到60转/分钟这样弹簧才能缓慢推动货品。工作电压是12V还是24V取决于机器规格这里以12V电机为例24V方案只需改电源电压和PWM占空比标定判定逻辑完全一致。3.2 电流采样与中值滤波别让启动浪涌骗了你ADC采样的难点不在硬件在数据处理。电机刚启动时的浪涌电流和堵转电流在数值上很像如果以“电流超过阈值”作为卡货判据点一下启动就会误判。处理办法是加时间窗电机启动后一段时间内不判定等浪涌过去再开始采样。#define MOTOR_START_DELAY_MS 90 // 跳过启动浪涌 #define MOTOR_OVER_CURRENT_MS 300 // 持续超流判定时间 #define MOTOR_MAX_RUN_MS 8000 // 最大运行时间 #define CURRENT_THRESHOLD_MA 450 // 堵转阈值实测后标定 void Motor_CurrentTask(void) { static uint32_t overCurrentCount 0; // 电机启动前90ms不采样避免浪涌误判 if (HAL_GetTick() - motorStartTick MOTOR_START_DELAY_MS) { return; } uint16_t adcValue ADC_GetAverage(ADC_CH_CURRENT); // 已做中值滤波 uint16_t currentMa (uint16_t)(adcValue * 1000 / adc1maFactor); if (currentMa CURRENT_THRESHOLD_MA) { overCurrentCount; } else { overCurrentCount 0; } // 连续300ms超流判定为卡货 if (overCurrentCount * 5 MOTOR_OVER_CURRENT_MS) { Motor_Stop(); Machine_GoToState(STATE_FAULT); } else if (HAL_GetTick() - motorStartTick MOTOR_MAX_RUN_MS) { // 超时强制停止按出货失败处理 Motor_Stop(); Machine_SetError(ERR_VEND_TIMEOUT); } }采样采用滑动中值滤波连续采17个点排序取中间值能滤掉换向器火花和电源纹波带来的尖峰同时不引入相位延迟。上电后先做一次ADC零点标定把零电流对应的ADC值存到EEPROM防止运放偏置电压造成温漂误判。3.3 出货判定参数表空转、带载、堵转的区别做完硬件后第一步不是写业务逻辑而是把每个货道空转和带载的电流数据测出来。下面是一张12V减速电机的典型记录表实际数值以你的减速比和弹簧间距为准状态电流范围持续时间判定策略空转120~180mA整个循环正常出货可接受推货带载220~350mA约1~3秒正常出货不需要额外处理启动浪涌600~900mA30~80ms前90ms不采样堵转400~600mA持续判定卡货停止并退币这里的核心思想是阈值取“带载最大值”和“堵转最小值”的中间值两层都留出余量。如果两个区间重叠优先调低减速比或更换电机型号而不是靠软件硬切。弹簧间距过大、货品过重都会造成电流区间漂移这是机械和电气相互妥协的过程。3.4 堵转后的反向抖落提高出货成功率的一个小技巧直流电机堵转并不总是真卡死有时候只是货品卡在弹簧和挡板之间的临界点。碰到这种情况一个有效手段是让电机反方向转300毫秒再正转。反向转动会让弹簧稍微松退给货品重新调整位置的空间。void Motor_Unblock(void) { // 反向后正转每次300ms Motor_SetDirection(MOTOR_REVERSE); Motor_SetDuty(60); HAL_Delay(300); Motor_SetDirection(MOTOR_FORWARD); Motor_SetDuty(80); motorStartTick HAL_GetTick(); // 重置启动计时重新走电流判定 }注意反向时间不能太长否则弹簧会把后面的货往回退打乱货道排列。这个功能做成可配置项通过参数表决定启用还是关闭有些货道弹簧卡得紧反向反而会把出货逻辑搞乱。4. 状态机与支付协议让整个售货流程可维护4.1 状态划分与转移表先把流程画清楚再动代码售货流程看起来是一个线性过程选货、付钱、出货、结束。但异常情况会把线性流程变成一团乱麻比如用户选货后取消、支付超时、出货失败后要退款。不用状态机直接写if嵌套三四个异常分支之后代码就没法改动了。这里把系统划分为五个状态IDLE空闲、SELECT已选货、PAYING支付中、VEND出货中、FAULT故障。状态转移的关键在于事件驱动按键事件、支付事件、定时器事件都通过一个事件队列进入状态机状态机内部根据当前状态和事件决定动作。当前状态触发事件下一状态执行动作IDLE按键选货SELECT点亮对应货道指示灯记录所选货道SELECT支付成功VEND启动电机开始出货流程SELECT支付超时IDLE清零所选货道指示灯熄灭VEND电流判定正常IDLE扣减库存发放找零VEND卡货/超时FAULT停止电机触发退款流程FAULT维护人员复位IDLE清理错误码恢复可用4.2 状态机代码骨架注意防重入和事件清空状态机写在主循环里而不是用RTOS省掉任务切换开销逻辑也更直观。关键点是一个状态对应一个入口函数状态切换只发生在状态机的Run函数内部外部代码不能直接调状态入口否则会出现一个状态被重复进入的竞态问题。void Machine_Run(void) { Event_t evt Queue_Dequeue(); switch (machineState) { case STATE_IDLE: if (evt EVENT_KEY_SELECT) { machineState STATE_SELECT; Machine_SelectEnter(); } break; case STATE_SELECT: if (evt EVENT_PAY_OK) { machineState STATE_VEND; Machine_VendEnter(); // 启动电机、记录时间戳 } else if (evt EVENT_PAY_TIMEOUT) { machineState STATE_IDLE; Machine_IdleEnter(); } break; case STATE_VEND: if (evt EVENT_VEND_DONE) { Storage_DecrementStock(curSlot); machineState STATE_IDLE; Machine_IdleEnter(); } else if (evt EVENT_VEND_FAULT) { machineState STATE_FAULT; Refund_Start(); // 进入退款流程 } break; default: break; } }这里有两个容易踩的坑。第一个是事件队列出队后如果没有匹配到当前状态事件直接丢弃不允许缓存到下一轮否则出货过程中的残留按键会把状态机打乱。第二个是进入VEND状态时立刻把当前出货时间戳存下来超时判断统一以这个时间戳为基准不要在定时器回调里改状态机状态。4.3 与支付模块的串口协议帧头加长度加校验支付模块通过串口和主控通信常见的有投币器和扫码模块。串口通信最大的坑是粘包和半包必须定义帧协议。下面是一个极简但够用的帧格式适合跟大部分扫码模块对接。// 帧格式AA 55 LEN CMD DATA... CS // 其中LEN表示CMDDATA的总字节数CS为前面所有字节的异或校验 // 支付成功示例AA 55 06 0A 01 00 00 00 01 0B // 第1字节CMD0x0A表示支付通知后4字节为订单金额单位分 #define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define FRAME_CMD_PAY 0x0A uint8_t Uart_GetFrame(uint8_t *cmd, uint8_t *data) { static uint8_t buf[64]; static uint8_t state 0; static uint8_t len 0; static uint8_t idx 0; while (HAL_UART_Receive(huart1, rxByte, 1, 0) HAL_OK) { buf[idx] rxByte; switch (state) { case 0: if (rxByte FRAME_HEAD1) state 1; else idx 0; break; case 1: state (rxByte FRAME_HEAD2) ? 2 : 0; break; case 2: len rxByte; if (len 0 || len 16) state 0; // 异常帧头丢弃 else state 3; break; case 3: if (idx len 4) { // 长度域后实际字节数 if (Uart_CheckXor(buf, idx)) { *cmd buf[3]; memcpy(data, buf 4, len - 1); state 0; idx 0; return 1; } state 0; idx 0; } break; } } return 0; }上面的接收函数用状态机逐字节解析不依赖串口空闲中断任何波特率下都能稳定工作。异或校验虽然不是强校验但对售货机这种短帧、噪声可控的场景足够。实际上更稳的做法是支付成功后主控和支付模块再做一次金额确认查询双重保险避免计费争议。4.4 EEPROM库存落盘掉电不丢账库存和销售记录必须掉电保存不能依赖SRAM。使用板载AT24C02按货道顺序存放库存值。最简单可靠的方案是双备份写入同一个地址写两份数据读取时比对不一致则恢复为较小值。AT24C02的理论擦写寿命是100万次按每天售出200次计算寿命超过十年磨损均衡不是刚需。但如果做毕设或者长期测试频繁改写同一地址仍可能在调试期就写坏一片芯片。稳妥起见把库存区切分为A/B两个扇区写库时将数据写入非当前扇区同时维护usage字段区分当前活跃扇区。这是一个典型的“备区写入、读主校验”模式代码量不大但可靠性提升明显。5. 源码调试、参数整定与文档组织从能跑到不怕改5.1 串口日志分级把状态跳转打出来看拿到源码后第一件事不是改逻辑而是把日志系统打开。日志分四级ERR、WARN、INFO、DBG通过一个宏统一控制开关。DBG级别打印ADC原始值、PWM占空比和电流计算结果INF级别打印状态机跳转每次跳转都带时间戳和触发事件。状态跳转日志的格式建议这样设计[tick][状态1]-[状态2], eventxx。整理日志时用脚本把跳转序列拉出来一条售货流程应该是IDLE-SELECT-VEND-IDLE的闭环。如果看到VEND-FAULT直接去查电流采样曲线。5.2 电流阈值的标定流程空转、带载、堵转各测一组阈值不能拍脑袋定。标定流程是空货道连续运行20次记录电流最大值放标准货品运行30次记录电流最大值人为卡住货品运行10次记录电流最小值。最终阈值取卡货最小值和带载最大值的中间点。下表是一份记录模板建议打印出来贴在调试台上测试工况测试次数最大电流(mA)最小电流(mA)备注空转20180120弹簧无货带载30350220标准可乐罐堵转10600400卡死状态5.3 常见故障现象与排查方向出货完成后商品没掉出来、扫码付款后不出货、电机转了但反馈失败这几类问题在调试期反复出现。对症下药的方向如下现象优先排查项电机不转方向引脚电平、PWM是否启动、电源是否被拉低电机转了但报卡货电流阈值偏低、采样电阻接触不良、弹簧和货道摩擦大出货成功但系统认为失败电流判定时间窗太短、带载电流和空转电流区间重叠串口收不到支付结果帧长度计算错误、波特率不匹配、没有共地按键偶尔失灵矩阵扫描没做去抖、引脚内部上拉未开启5.4 文档说明配一份决策记录而不是复制手册“源代码文档说明”这个标题里的文档最有价值的部分不是原理图和寄存器表而是决策记录。每个硬件选型和软件方案旁边的注释回答“为什么这么选而不那样做”。例如电流检测为什么选采样电阻而不是红外对射传感器PWM频率为什么定10kHz而不是100Hz文档里写清楚这些拿到你代码的人才能快速理解和接手。配套文档建议包含六份原理图PDF、PCB源文件、引脚分配表、BOM清单、接线表、调试记录。其中接线表必须和源码里的引脚宏定义一一对应调试记录保留每次阈值标定的原始数据。文档和源码同版本号管理改代码当天必须同步更新文档否则一个月后再看自己都认不出当初为什么这样写。本文还有配套的精品资源点击获取