从零打造STM32迷你售货机:硬件拆解与状态机实战
去年夏天在电子市场闲逛时,偶然发现角落里积灰的OLED屏和矩阵按键,突然萌生了做个桌面级售货机的念头。这个看似简单的项目,实际上融合了嵌入式开发中最经典的三大难题:外设驱动整合、状态机设计以及用户交互逻辑。下面就将这六周从零搭建的过程,包括那些深夜调试的血泪教训,完整呈现给各位硬件爱好者。
1. 硬件架构设计与核心器件选型
1.1 主控与显示模块的黄金组合
STM32F103C8T6这颗蓝色小芯片堪称嵌入式界的"瑞士军刀",Cortex-M3内核搭配72MHz主频,对于需要驱动多个外设的售货机项目再合适不过。我特别看重它丰富的GPIO资源(37个I/O口)和3个USART接口,这为后续扩展留下了充足空间。
显示模块选用0.96寸OLED(SSD1306驱动),相比LCD有三大优势:
- 功耗表现:全亮状态下仅20mA,待机时低于1mA
- 可视角度:170度无死角显示
- 响应速度:刷新率可达100Hz
实际接线时发现个有趣现象:I²C接口的OLED只需要4根线(VCC、GND、SCL、SDA),比SPI接口节省3根线。这对于GPIO紧张的迷你项目至关重要。
1.2 输入输出设备配置方案
矩阵按键采用4x4布局,通过74HC165移位寄存器扩展输入。这种设计将16个按键压缩到3个GPIO口,接线示意图如下:
列扫描线 → PC0-PC3 行读取线 → 74HC165 → SPI1继电器模块选用HK19F-DC5V,关键参数:
- 触点容量:10A/250VAC
- 动作时间:<10ms
- 线圈功耗:0.36W
特别提醒:继电器的反电动势可能干扰MCU,务必在线圈两端并联1N4148续流二极管。
2. 状态机设计与系统逻辑实现
2.1 五状态工作模型
售货机的核心是状态流转,我将业务流程抽象为五个状态:
typedef enum { STATE_IDLE, // 待机状态 STATE_SELECT, // 商品选择 STATE_QUANTITY, // 数量设定 STATE_PAYMENT, // 支付处理 STATE_DELIVERY // 出货状态 } VendingState;状态转换触发条件如下表所示:
| 当前状态 | 触发事件 | 下一状态 | 执行动作 |
|---|---|---|---|
| IDLE | 按键1/5 | SELECT | 显示商品列表 |
| SELECT | 按键9/13 | QUANTITY | 更新数量显示 |
| QUANTITY | 按键12 | PAYMENT | 计算总价 |
| PAYMENT | 投币完成 | DELIVERY | 驱动继电器 |
2.2 按键消抖的硬件方案
传统软件消抖需要20-50ms延时,这在状态机中会阻塞流程。我的解决方案是:
- 在74HC165输入端并联0.1μF电容
- 配置STM32的硬件消抖滤波器(如下寄存器配置)
GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF0_EVENTOUT; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); // 启用输入滤波器 GPIO->PUPDR |= GPIO_PUPDR_PUPD0_1; // 下拉 GPIO->PUPDR |= GPIO_PUPDR_PUPD1_1;实测可将按键抖动从毫秒级降低到微秒级,状态转换更加可靠。
3. OLED界面优化技巧
3.1 分层渲染策略
为避免频繁刷新导致的闪烁,采用三级显示缓存:
- 背景层:静态元素(如边框、标题)
- 数据层:动态数值(价格、数量)
- 交互层:光标、按钮高亮
刷新时仅更新必要区域,通过以下指令局部刷新:
void OLED_PartialRefresh(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) { SSD1306_SetColumnAddress(x0, x1); SSD1306_SetPageAddress(y0/8, y1/8); HAL_I2C_Mem_Write(&hi2c1, SSD1306_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT, &buffer[y0/8][x0], x1-x0+1, 100); }3.2 字体压缩技术
标准16pt字体占16x16像素,通过自定义字模可压缩到12x16。以数字"8"为例:
const uint8_t Font12x16_8[] = { 0x1E, 0x3F, 0x33, 0x33, 0x3F, 0x1E, 0x1E, 0x3F, 0x33, 0x33, 0x3F, 0x1E };相比标准字库节省25%显示空间,这在小型OLED上尤为宝贵。
4. 出货机制与异常处理
4.1 继电器驱动电路优化
最初直接使用GPIO驱动继电器,发现两个问题:
- 线圈吸合时导致电源电压跌落
- MCU复位时可能误动作
改进方案:
- 增加MOSFET驱动电路(IRLZ44N)
- 配置硬件看门狗(IWDG)
电路原理图:
STM32 GPIO → 1kΩ电阻 → IRLZ44N栅极 │ └─ 10kΩ下拉电阻 继电器线圈接在MOSFET漏极与12V电源之间4.2 故障检测机制
通过ADC监测关键点电压,建立三级保护:
电源监测:检测3.3V和5V轨电压
hadc1.Instance = ADC1; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DMAContinuousRequests = ENABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; HAL_ADC_Start(&hadc1);温度监测:DS18B20检测继电器温度
出货反馈:光电传感器验证商品掉落
当检测到异常时,系统会自动:
- 切断继电器电源
- OLED显示错误代码
- 蜂鸣器发出特定报警音
5. 功耗优化实战记录
5.1 运行模式划分
通过实测发现不同模块的功耗差异惊人:
| 模块 | 工作电流 | 休眠电流 |
|---|---|---|
| STM32全速 | 36mA | 2.1mA |
| OLED显示 | 20mA | 0.05mA |
| 继电器保持 | 72mA | 0mA |
据此设计三种工作模式:
- 活跃模式:所有外设供电(<150mA)
- 待机模式:关闭继电器和OLED背光(<25mA)
- 休眠模式:仅维持RTC(<3mA)
5.2 动态时钟调整
根据负载动态调整系统时钟,关键代码:
void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; // 外部8MHz晶振 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 根据不同模式调整PLL倍频 if(power_mode == POWER_HIGH) { RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 72MHz } else { RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL4; // 32MHz } HAL_RCC_OscConfig(&RCC_OscInitStruct); }实测可降低30%以上的动态功耗,这对电池供电版本尤为重要。
6. 项目进阶与扩展思路
6.1 无线功能集成
通过ESP-01S模块添加WiFi连接,实现两个实用功能:
- 远程库存管理:上传销售数据到云平台
- 固件OTA更新:无需拆机即可升级程序
接线示意图:
ESP8266 STM32 TX → PA3(RX) RX → PA2(TX) EN → 3.3V GND → GND6.2 机械结构改良建议
经过三个原型迭代,总结出机械设计的黄金法则:
- 出货滑道倾斜角度≥30度
- 商品隔间宽度比商品大2-3mm
- 光电传感器安装位置距离出货口5-8cm
推荐使用3D打印的模块化结构,便于调整参数。PLA材料厚度建议≥2mm,关键受力部位可增加到3mm。