尧图精选

STM32自动售货机项目实战:从硬件设计到状态机代码完整解析

🕒 发布时间:2026/9/1 23:29:36 📁 来源:尧图网络
简介本资源是一套基于STM32F10x系列微控制器的自动售货机嵌入式系统完整源码工程面向嵌入式初学者、高校电子类课程设计学生及STM32项目实践者解决无人值守商品销售系统的软硬件协同开发问题涵盖货道控制、传感器检测、支付交互与人机界面等核心功能。压缩包共229个文件含40余个C源文件实现外设驱动与业务逻辑、42个头文件h与目标文件o以及Keil工程配置uvprojx/uvoptx、启动代码s、映射文件map和固件输出hex等关键构建产物整体8.09MB结构完整可直接编译下载运行。已有66人学习下载源码严格遵循模块化设计包含stm32f10x_usart、adc、i2c、tim等标准外设驱动以及系统初始化、事件调度、货品管理与状态监控等应用层模块便于理解中断机制、状态机设计与多任务协调逻辑是深入掌握STM32实战开发的优质参考范例。 做嵌入式这么多年我始终觉得自动售货机是个特别值得反复打磨的练手项目。它不像点个灯、转个风扇那样单一而是真正把STM32开发里最常见的环节全串了起来按键输入、OLED显示、电机驱动、状态机管理、ADC采集、串口通信、数据掉电保存甚至联网上报一整套下来基本把单片机主流外设都过了一遍。这篇文章我就把基于STM32F103C8T6从零做一台自动售货机的完整过程整理出来包括系统怎么拆、硬件怎么接、代码怎么写、坑怎么躲给想系统练一遍STM32开发的朋友一条能直接参考的路。关于这个项目我多说一句它最大的价值不是我做出了一台机器而是你能在同一个项目里体会到嵌入式开发的完整闭环——从需求分析、硬件选型、电路设计到驱动编写、状态机调度再到最后整机调试和问题排查每一步都有它独特的坑。这篇文章适合两类人一类是刚学完STM32基础外设、想做个综合项目练手的入门开发者另一类是已经能跑通单个外设、但没做过完整系统的同学这篇文章会告诉你系统级整合和单模块实验到底差在哪里。1. 项目整体设计先把功能拆干净再动手1.1 一台自动售货机的功能模块拆解自动售货机从用户视角看特别简单投钱、选货、拿走东西。但落到嵌入式实现上屏幕背后的逻辑链条相当长。我建议在写第一行代码之前先把功能需求拆成几个明确的模块这比任何软件架构都重要。一台功能完整的售货机核心模块大致如下人机交互模块用什么向用户展示商品信息用户怎么选货。大多数入门方案用OLED屏或LCD屏加矩阵按键进阶可以换成触摸屏。支付模块真实售货机需要硬币识别器或扫码支付。学习项目通常用按键模拟投币也就是按下投币键增加余额这样可以把重点放在业务逻辑上。出货控制模块根据用户选择驱动对应货道的电机把商品推出来。这个模块是整个系统的执行核心也是最容易出问题的部分。货物检测模块用红外对射传感器或其他方式确认商品真的掉落了避免扣了钱不出货这种尴尬情况。数据管理模块记录库存、价格、销售状态并且在掉电后不丢失。这部分用STM32内部Flash就能搞定不需要外挂EEPROM。这几个模块组合起来就构成了一个标准的业务闭环。一台机器上可能有6个甚至12个货道每个货道一个电机、一个红外检测位主控芯片要管理这些并行资源这已经不是一个顺序执行的裸机程序能轻松搞定的了状态机就是为这种情况准备的。1.2 主控选型为什么STM32F103C8T6是性价比之选主控我选了STM32F103C8T6也就是大家常说的蓝色药丸核心板。这颗芯片在STM32家族里属于入门级但性能对这个项目来说绰绰有余72MHz主频、64KB Flash、20KB SRAM、3个USART、2个SPI、2个I2C、一个10通道的12位ADC温度范围也够宽。选它的理由其实有三层。第一资料极度丰富。从野火、正点原子到江科大各种教程和例程早就把这个芯片的每个外设翻了个底朝天遇到问题几乎都能搜到解决方案对学习项目来说这是最大的隐形优势。第二成本低。一片C8T6散片几块钱到十几块钱核心开发板也不贵做坏了不心疼。第三性能在这个项目里是够用但不算浪费——自动售货机没有大量浮点运算也没有复杂图像处理一颗Cortex-M3内核搞定全部逻辑绰绰有余。当然如果你手头有STM32F407、F429这些更高端的板子也可以用只是有点大材小用。反过来如果只是做一些更简单的单外设实验51单片机也能凑合但要做到多任务调度、状态机管理、掉电存储和联网上报还是STM32这个级别的芯片更顺手。1.3 状态机是整台机器的灵魂我见过不少同学做这类项目第一反应是写个while(1)循环里面扫码、按key、开电机、延时、再扫。前几次运行看着还行但一旦按钮按快了、电机还没出完货、用户又选了另一个商品程序就乱套了。自动售货机的业务流程天然适合用状态机建模。这玩意说白了就是给程序定义几个状态每个状态下只处理该处理的事由事件触发状态切换。我的主状态划分是这样状态行为跳转条件待机态显示欢迎界面、商品信息检测到投币事件投币态显示当前余额、提示选货余额足够并选择商品出货态驱动电机、等待出货检测出货完成或超时退币态计算找零、提示取货完成找零或余额归零严格按状态机的思路写代码最大的好处是逻辑清晰。每一时刻程序在做什么、能响应什么、不能响应什么都明明白白不会出现用户乱按导致程序跑飞的问题。后面第3章我会给出具体的代码实现这里先强调概念状态机不是花架子而是这种事件驱动多阶段业务流程场景的标准解。2. 硬件电路与底层驱动每个外设都要盯死2.1 电源系统电机一启动MCU就复位多半是电源的锅很多入门项目死在第一步不是代码问题而是电源没设计好。自动售货机有电机、有屏幕、有主控它们对电源的需求差异很大绝对不能一个5V全带起来。我的方案是这样外部用12V直流适配器供电12V先经过一个DC-DC降压模块转成5V给电机驱动和传感器供电5V再经过AMS1117-3.3给MCU供电。三路电源各走各的互不干扰。关键点在于电机启动的瞬间电流可能到几百毫安甚至更高如果和MCU共用一条电源线母线电压会被瞬间拉低MCU立刻复位。解决思路是两件事一是电源路径分开电感或磁珠把模拟电源和数字电源隔离二是在电机电源两端加大容量电解电容470uF以上同时并联一个0.1uF的陶瓷电容滤高频。我实测下来加了电容和隔离后电机再怎么频繁启停MCU都稳如老狗。另外所有芯片的电源引脚都要就近放去耦电容。这是个老生常谈的问题但很多人就是不做。STM32F103C8T6的每个VDD引脚附近放一个0.1uF陶瓷电容电解电容放一个10uF的就够了。别小看这几个电容高速数字电路的电流尖峰全指着它们。2.2 N20减速电机驱动与光耦隔离电路出货机构我用的是N20减速电机这个电机在航模和智能小车上非常常见体积小、扭矩大工作电压3-6V带减速箱后转速能降到每分钟几十到几百转非常适合推出货道里的商品。驱动N20电机有几种方案我给你们对比一下驱动方案特点适用场景ULN2003达林顿管阵列便宜、可驱动多个电机、但压降大货道多、成本敏感DRV8833等电机驱动芯片双H桥、支持PWM调速、带保护需要调速和正反转光耦MOS管分立电路电气隔离好、灵活性强对干扰敏感的场合我做的时候是ULN2003接电机 光耦隔离控制信号的组合。有人会觉得ULN2003内部已经有续流二极管了直接接MCU不就行了吗确实行但问题是电机启停产生的电磁干扰会通过共地回路窜进MCU轻则OLED花屏重则程序跑飞。我在控制信号通路上加了PC817光耦把MCU和电机驱动彻底隔离开效果立竿见影。光耦电路其实很简单MCU的GPIO接光耦输入端的LED串一个300欧限流电阻光耦输出端接ULN2003的输入端。注意一点光耦两侧必须单独供电、单独地如果两边共地隔离效果就归零了。实际在调出货的时候我还发现一个细节电机不能只是简单的通电转一会就停因为不同货道的商品大小、重量不一样弹簧转一圈和转三圈推出的距离完全不同。更可靠的做法是让电机转到某个位置后用限位开关或霍尔传感器停止或者至少用红外对射传感器检测商品掉落后再停。我在下面软件部分会细说这个逻辑。2.3 OLED显示、矩阵键盘和掉货检测人机交互部分我用了一块0.96寸的I2C接口OLED屏驱动芯片是SSD1306分辨率128x64。接线非常简单VCC接3.3V、GND接地、SCL和SDA分别接I2C1的PB6和PB7STM32F103默认复用两个信号线上各加一个4.7k上拉电阻到3.3V。I2C地址一般是0x3C也有极少数是0x3D程序里可以做成自动探测。OLED显示内容怎么设计很关键直接影响到用户的体验。我调试的时候一开始只显示一行Welcome后来发现用户根本不知道有哪些商品、多少钱。迭代了几个版本之后我最终用了四行布局第一行显示商品代号和价格第二行显示当前余额第三行高亮显示当前选中的货道第四行显示系统提示请投币、出货中、请取货等。这种信息密度基本是这个小屏的极限了再多就得换大屏。按键部分我用的是4x4矩阵键盘总共16个键对应12个货道加一个投币键加两个加钱键1元、5元其余几个按键留作扩展。矩阵键盘的原理就是行列扫描先把所有行输出高电平然后逐行拉低读取每列的输入状态就能确定哪个键被按下。扫描函数放在一个定时器中断里每2ms执行一次加上一个简单的消抖逻辑连续读到两次相同的键值才确认基本不会误触发。掉货检测我用的是漫反射红外传感器E18-D80NK装在出货口的正下方。它的原理很简单发射管发射红外线商品落下经过时反射回来被接收管收到输出一个低电平信号。注意这个传感器工作电压是5V输出信号到MCU引脚前最好加个分压电阻不然3.3V的STM32引脚可能扛不住5V电平。2.4 通信模块扩展串口调试、联网上报与设备唯一ID自动售货机如果不联网感觉少了点灵魂。我用串口2接了ESP8266模块通过AT指令上报库存和销售数据。ESP8266的玩法已经被社区玩烂了但作为售货机项目我建议你至少实现两件事一是定期把当前库存和销售额上报到云平台二是接收远程下发的货道价格调整指令。接线就是STM32的USART2_TX接ESP8266的RXDUSART2_RX接ESP8266的TXD波特率115200。注意ESP8266的供电能力有限直接用STM32板的3.3V可能会因为电流不足导致模块反复重启最好单独用一块5V转3.3V的稳压模块给ESP8266供电。这里有个特别容易忽略的点STM32的每颗芯片内部都有唯一的96位ID地址在0x1FFFF7E8。这个ID是出厂时烧死的全世界不重复。我在做联网上报时直接把设备序列号取出来作为售货机的唯一标识这样后端服务器能区分不同机器。你以后做设备联网方案时也可以借鉴这个思路省得自己烧录序列号了。3. 软件实现从裸机逻辑到可用程序3.1 标准库还是HAL库这个问题几乎是STM32社区每个项目开坑前必吵的。我的建议很直接如果你还处于能跑通例程但不太理解底层细节的阶段先用标准库如果你已经有项目经验、想提高开发效率或者打算用CubeMX快速搭外设配置直接学HAL库。自动售货机这个项目我做的时候用的是标准库理由有两点。第一这个项目用到的外设GPIO、定时器、串口、I2C、ADC都比较基础标准库的代码量并不大反而更直观方便你去理解寄存器的实际操作。第二网上搜到的例程一大半都是标准库写的参考起来零门槛。如果你的目标是商业项目、后期维护和团队协作那HAL库是更现代的选择用CubeMX生成初始化代码确实省事。只是HAL库封装得比较深遇到问题排查起来对数电基础的要求更高。工程结构我建议按模块分文件千万别把几百行代码全塞进main.c。我自己的目录结构是这样的BSP文件夹放各外设驱动bsp_oled.c、bsp_key.c、bsp_motor.c、bsp_uart.cAPP文件夹放业务逻辑app_vending_machine.c、app_state_machine.cMain.c只负责硬件初始化和主循环调度。这样写的好处是以后想加个传感器、换种显示方式只需要动对应的驱动文件主逻辑完全不用碰。3.2 三个基础工具printf重定向、非阻塞延时、ADC多通道DMA写正式项目前我建议先把三个基础设施搭好它们会贯穿整个开发过程省下大量时间。3.2.1 printf重定向到串口调试嵌入式程序最常用的工具就是串口打印。但默认的printf是输出到标准输出设备的在单片机里需要重定向一下让它把字符送到串口发送。标准库做法是重写fputc函数#include stdio.h int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }注意在Keil里要勾选MicroLIB微库选项否则printf的后台会占用大量资源还可能链接报错。以后你写任何STM32程序第一步就可以把这套串口调试框架搭好后面全靠它救命。3.2.2 用DWT实现非阻塞延时刚学51单片机的时候大家都用过delay函数for循环空转等时间。但在自动售货机这种有多个并行任务的系统里一个delay(1000)就会卡死所有其他任务——显示不更新了、按键没反应了、串口也卡住了。所以正确做法是用定时器或者DWT实现非阻塞延时。DWT是Cortex-M3内核自带的调试硬件里面有个CYCCNT寄存器每个时钟周期加1拿它做微秒级延时非常方便而且不占用额外的定时器外设void DWT_Delay_us(uint32_t us) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while (DWT-CYCCNT - start ticks); }使用的时候把延时拆成检查时间是否到了的状态查询放在主循环里轮询这样出货的过程中屏幕还能继续刷新按键还能响应。3.2.3 ADC多通道扫描DMA采集自动售货机里用ADC的场景很多比如读电位器校准系统参数、读传感器电压判断货物状态。如果只有一个通道直接在循环里ADC_GetConversionValue就行但多通道同时采集就必须用扫描DMA模式否则CPU会被频繁中断拖死。我的做法是用ADC1的扫描模式启用DMA1通道1连续采集3个通道比如一个电位器校准电压、两个传感器信号DMA传输完成后在主循环里读取数组就行。初始化的时候注意设置DMA的外设地址和数据存储器地址的递增模式外设地址不递增因为始终是同一个ADC数据寄存器内存地址递增因为要依次写到数组里。这个配置没弄对的话数据会全挤在数组的第一个位置这是很多人踩过的坑。3.3 自动售货机主逻辑状态机的代码落地主逻辑我写成了一套状态机核心代码不长但非常清晰。首先定义状态枚举typedef enum { STATE_IDLE 0, STATE_COIN, STATE_SELECT, STATE_DISPENSE, STATE_REFUND } VmState; VmState currentState STATE_IDLE; uint16_t balance 0; // 当前余额单位元 uint8_t inventory[12]; // 12个货道的库存 uint16_t price[12]; // 12个货道的价格主循环就是反复执行一个状态处理函数while (1) { uint8_t key Key_Scan(); stateMachine(key); }状态机核心逻辑void stateMachine(uint8_t key) { switch (currentState) { case STATE_IDLE: OLED_ShowIdleScreen(); if (key KEY_INSERT_COIN) { currentState STATE_COIN; } break; case STATE_COIN: // 投币状态按键模拟投币 if (key KEY_COIN_1) { balance 1; } if (key KEY_COIN_5) { balance 5; } // 余额大于0允许进入选货状态 if (balance 0) { currentState STATE_SELECT; } break; case STATE_SELECT: OLED_ShowSelectScreen(balance); if (key KEY_ITEM_0 key KEY_ITEM_11) { uint8_t track key - KEY_ITEM_0; if (inventory[track] 0 balance price[track]) { selectedTrack track; currentState STATE_DISPENSE; } else { OLED_ShowError(库存不足或余额不够); } } break; case STATE_DISPENSE: // 出货状态 if (DispenseProduct(selectedTrack)) { balance - price[selectedTrack]; currentState STATE_REFUND; } else { currentState STATE_SELECT; // 出货失败返回选货 } break; case STATE_REFUND: // 找零状态如果还有余额就提示用户 OLED_ShowRefund(balance); if (key KEY_CONFIRM) { balance 0; currentState STATE_IDLE; } break; } }出货函数DispenseProduct是关键它不只是开电机转一会就关这么简单。我加入了一个红外传感器确认逻辑uint8_t DispenseProduct(uint8_t track) { Motor_On(track); // 非阻塞等待最多4秒期间不断检测红外传感器 uint32_t timeout getTick() 4000; while (getTick() timeout) { if (DROP_SENSOR_READ() 1) { // 检测到商品掉落 Motor_Off(track); inventory[track]--; saveDataToFlash(); return 1; } Key_Scan(); // 出货期间保持按键扫描防止卡死 } Motor_Off(track); return 0; // 超时未出货返回失败 }这套逻辑的好处是如果商品因为卡住没掉下来电机会自动停止不会一直空转烧毁同时用户还能按按键系统不会假死。但要注意红外检测和状态机的结合有一个细节出货过程中要持续检测不能只检测一次。因为商品可能中途卡了一下又掉下来检测需要在窗口期内持续轮询。3.4 状态机编码的三个防坑细节写状态机代码时有几个细节特别容易出错我一个个说。第一状态机的出口要清理干净。每次从出货态回到选货态之前一定要关掉对应的电机。否则电机一直通电轻则耗电重则把商品一直推出来造成连续出货。我调试时遇到过两次这个Bug都是因为状态切换时漏了执行Motor_Off。第二按键消抖不能只靠延时。在状态机里用delay消抖会卡住整个系统。我的做法是把消抖逻辑放到每2ms一次定时器中断里扫描主循环只负责取已经被确认过的键值。这样就算出货过程中用户疯狂按键也不会干扰到状态机的主流程。第三注意状态机的非法输入。比如用户没投币直接按选货键或者在出货中又投了一次币这些情况都要有明确的响应。我的设计是每个状态只处理它关心的按键其他按键直接忽略。这样可以最大限度避免状态错乱。4. 调试工具、高频问题与避坑实录4.1 推荐工具链ST-Link、Keil、串口助手、逻辑分析仪硬件调试工具就四样ST-Link下载调试器、Keil MDK开发环境、串口助手软件、逻辑分析仪。前两样做开发必备串口助手用来监视程序运行状态逻辑分析仪用来查时序问题。ST-Link能做的事很多除了下载固件还可以在线调试、单步运行、查看变量值。我强烈建议你一开始就学会用ST-Link在线调试功能而不是每次改完代码重新烧录、看现象猜问题。Keil里打断点、Watch窗口看变量值、Peripherals窗口看寄存器状态这些都是效率神器。ST-Link Utility这个工具也值得留一份它可以免Keil直接烧录hex文件还能读Flash内容。逻辑分析仪是排查I2C、UART等通信问题的大杀器。有一次我调ESP8266串口一直收到乱码用逻辑分析仪抓波形才发现是我把TX和RX接反了这种问题光靠看串口助手根本发现不了。4.2 从热搜词看初学者高频问题速查我在整理这篇文章的时候顺手翻了翻STM32社区里大家搜得最多的关键词发现很多问题在自动售货机这个项目里同样会出现直接整理成一份速查表高频问题典型原因解决办法ST-Link连接不上芯片驱动没装好、杜邦线太长、BOOT0被拉高重装驱动、换短连接线、确认BOOT0接地delay函数一调用就卡死SysTick定时器没初始化、中断优先级配置错误检查SystemInit、NVIC优先级分组程序加入电机后OLED花屏电机干扰电源、地线不干净光耦隔离、电机电源加电容、单独供电串口打印全是乱码波特率不匹配、外部晶振频率不对确认两边波特率、检查晶振是否8MHz串口接收不定长数据收不全没用空闲中断、帧间隔判断太短用USART空闲中断加DMA接收缓冲区ADC多通道DMA数据全在数组第一位DMA内存地址没有配置递增模式配置DMA_InitStructure.DMA_MemoryInc复用的引脚不工作引脚被JTAG/SWD功能占用禁用JTAG保留SWD或者换引脚程序掉电后库存数据丢失数据只存在内存里没写Flash在Flash开辟存储页保存参数每台设备无法区分没有读取芯片唯一ID读取0x1FFFF7E8处的96位ID这里单拎两个和自动售货机项目特别相关的展开说一下。一个是stm32禁用jtag。这款芯片的PA15、PB3、PB4默认是JTAG引脚如果你把它们当作普通IO用比如接按键需要调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)禁用JTAG功能。但注意禁用JTAG后ST-Link的SWD下载调试功能仍然保留所以不影响开发。如果你做的是别的项目把SWD也关了那固件下载就彻底没门了只能先用串口擦除Flash再恢复非常麻烦。另一个是stm32串口接收不定长数据。自动售货机连ESP8266后会收到TCP连接断开、IPD等不定长的网络数据不能按固定帧长接收。正确方案是用串口空闲中断一帧数据发完、总线空闲时触发IDLE中断此时判断接收缓冲区里有多少数据就代表这一帧有多长。配合DMA接收可以实现CPU零参与的数据接收。4.3 三个真实排错案例最后分享三个我在调试这个项目时真实遇到、并且花了很久才定位的问题给大家做个参考。案例一一启动电机MCU就复位OLED闪一下黑屏最开始我以为是程序Bug反复检查状态机逻辑完全没问题。后来用示波器测5V电源轨才发现电机启动瞬间电压从5V掉到3.2VMCU的3.3V LDO输出也跟着掉到2V左右复位电压都没保住。解决方法是把电机电源和逻辑电源彻底分开电机用12V转5V的DC-DC单独供电MCU用另一个LDO中间地线单点汇合。另外在电机两端并了一个470uF电解电容和一个0.1uF陶瓷电容启动瞬间的压降从1.8V降到了0.3V以内。案例二串口第一次能收到数据第二次就完全卡死这个问题特别隐蔽。我的串口接收用的是中断加环形缓冲第一次调试正常第二次就收不到中断了。查了半天发现是接收中断服务函数里没有清空USART_ITConfig(RXNE)的中断标志位导致中断反复进入、缓冲区溢出后死锁。这个坑在几乎所有STM32串口开发里都会遇到记住一条铁律进中断后第一步清标志位第二步读数据第三步再清溢出标志位ORE顺序不能乱。案例三按键一次按下余额跳了好几块看起来像是消抖没做好但我已经加了延时消抖了。后来用逻辑分析仪抓按键波形才明白是矩阵键盘扫描的时候行和列的几个IO没有设置为正确的输入/输出模式。行方向有几个引脚复用成了JTAG功能电平初始化不对导致读到的键值不稳定。换成禁用JTAG之后再配一遍GPIO问题就消失了。从那以后我每次开发板上电后的第一件事就是先确认所有用到的引脚是否被其他复用功能占用。写在最后的一点个人体会这台自动售货机我前后调了三个周末最大的感悟不是代码有多难写而是系统级调试和单模块实验完全是两回事。单模块实验的时候LED不亮就是IO配错了但整机跑起来以后一个现象背后可能藏着三个原因需要一层一层排查。遇到问题先别急着改代码用万用表测电压、用示波器看波形、用逻辑分析仪抓时序把现象定位到电还是信号的层面再动手去改。这个项目做完后如果还想继续扩展我建议往三个方向走一是把FreeRTOS引进来把状态机任务、按键扫描任务、网络上报任务拆成真正的多任务感受一下嵌入式操作系统的调度方式二是把OLED换成带触摸的显示屏把LVGL图形库移植上去做一个带商品图片、动画效果的交互界面三是把出货电机的控制升级成带电流检测的方案通过电机电流的变化判断是否有商品卡住这样比红外传感器更省器件也更接近商业售货机的做法。每一版改进本质上都是对STM32这片开发板上最核心的嵌入式知识的又一次加深。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →