简介:本资源是一套基于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和PB7(STM32F103默认复用),两个信号线上各加一个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的RXD,USART2_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.c),APP文件夹放业务逻辑(app_vending_machine.c、app_state_machine.c),Main.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.2V,MCU的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这片开发板上最核心的嵌入式知识的又一次加深。
本文还有配套的精品资源,点击获取