从零打造迷你自动售货机:STM32F103C8T6全流程实战
去年夏天,我在清理工作室时翻出一堆闲置的电子元件——几块吃灰的STM32开发板、0.96寸OLED屏、矩阵键盘模块,还有上次项目剩下的继电器。这些看似不相关的零件突然让我萌生一个想法:何不做一个迷你自动售货机模型?既能消化库存元件,又能复刻我们每天都能见到的商业设备工作原理。经过三周的反复调试,这个巴掌大的装置不仅能完整模拟真实售货机的商品选择、投币找零流程,还加入了LED指示灯和音效反馈。下面就将整个制作过程拆解成可复用的经验,特别适合有一定嵌入式基础的创客练手。
1. 硬件架构设计与关键元件选型
1.1 核心控制器:STM32F103C8T6的性价比之选
这块被称为"蓝色药丸"的最小系统板虽然只有64KB Flash和20KB RAM,但72MHz的主频足够处理售货机的控制逻辑。相比Arduino,STM32的优势在于:
- GPIO资源丰富:37个可用IO口轻松应对矩阵键盘+OLED+继电器的需求
- 硬件SPI接口:驱动OLED时刷新速率可达8MHz
- 内置定时器:精确控制按键消抖和继电器动作时序
实际使用中需要注意:
// 时钟配置示例(使用内部8MHz RC振荡器) RCC_DeInit(); RCC_HSEConfig(RCC_HSE_OFF); RCC_HSICmd(ENABLE); while(RCC_GetFlagStatus(RCC_FLAG_HSIRDY) == RESET);1.2 显示模块:SSD1306 OLED的省电方案
0.96寸128x64分辨率的OLED屏在功耗和显示效果上完胜LCD:
- 零待机功耗:黑色像素完全关闭
- 高对比度:即使在阳光下也清晰可见
- 硬件I2C接口:仅需4根连线
接线时常见的坑:
- 如果屏幕闪烁,检查I2C上拉电阻(通常需要4.7kΩ)
- 地址冲突时尝试将屏幕的SA0引脚接高电平(0x3D地址)
1.3 输入输出设备组合
矩阵键盘:4x4布局实现16个按键功能,扫描原理如下表:
| 扫描方式 | 优点 | 缺点 |
|---|---|---|
| 行扫描 | 代码简单 | 需要较多IO口 |
| 中断触发 | 省电 | 电路复杂 |
| ADC分压 | 仅需1个IO | 精度要求高 |
最终选择行扫描法,配合10ms定时器消抖:
uint8_t KEY_Scan(void) { static uint8_t key_up = 1; if(key_up && (KEY_ROW1==0 || KEY_ROW2==0 || KEY_ROW3==0 || KEY_ROW4==0)) { delay_ms(10); key_up = 0; // 扫描具体行列... } else if(KEY_ROW1==1 && KEY_ROW2==1 && KEY_ROW3==1 && KEY_ROW4==1) { key_up = 1; } return key_val; }2. 电路设计与实战接线图
2.1 电源管理设计
整个系统采用USB 5V供电,通过AMS1117-3.3稳压芯片为STM32供电。继电器模块需要单独5V电源防止电流倒灌,实际接线时特别注意:
警告:继电器线圈在断开时会产生反向电动势,必须并联续流二极管
完整的元件清单:
| 元件 | 型号 | 数量 | 备注 |
|---|---|---|---|
| 主控 | STM32F103C8T6 | 1 | 需安装Bootloader |
| 显示屏 | SSD1306 OLED | 1 | I2C接口 |
| 键盘 | 4x4矩阵 | 1 | 薄膜式最佳 |
| 继电器 | SRD-05VDC | 1 | 常开触点 |
2.2 信号线连接方案
为了避免飞线混乱,建议按功能分区连接:
显示模块:
- SCL -> PB6
- SDA -> PB7
- VCC -> 3.3V
- GND -> GND
矩阵键盘:
- 行线 -> PA0~PA3
- 列线 -> PA4~PA7
继电器控制:
- IN -> PC13
- VCC -> 5V(独立电源)
- GND -> 共地
3. 软件架构与核心算法实现
3.1 状态机设计:售货流程的完美建模
将用户操作抽象为五个状态,用枚举变量实现:
typedef enum { STATE_IDLE, // 待机状态 STATE_SELECT_ITEM, // 选择商品 STATE_SELECT_QTY, // 选择数量 STATE_PAYMENT, // 投币支付 STATE_DELIVERY // 出货状态 } VendingState;状态转换触发条件:
| 当前状态 | 触发事件 | 下一状态 | 动作 |
|---|---|---|---|
| IDLE | 按下S1/S5 | SELECT_ITEM | 显示商品菜单 |
| SELECT_ITEM | 按下S9/S13 | SELECT_QTY | 显示数量选择 |
| SELECT_QTY | 按下S12 | PAYMENT | 计算总价 |
| PAYMENT | 投币足够 | DELIVERY | 驱动继电器 |
3.2 价格计算与投币逻辑
采用结构体存储交易数据,避免全局变量混乱:
typedef struct { uint8_t current_item; uint8_t quantity; uint16_t unit_price[8]; // 8种商品单价 uint16_t total; uint16_t coin_inserted; uint16_t change; } TransactionData;投币验证算法示例:
void process_coin(uint16_t amount) { transaction.coin_inserted += amount; if(transaction.coin_inserted >= transaction.total) { transaction.change = transaction.coin_inserted - transaction.total; state = STATE_DELIVERY; OLED_Clear(); OLED_ShowString(0, 16, "Change:", FONT_16); OLED_ShowNumber(56, 16, transaction.change, FONT_16); } else { uint16_t remaining = transaction.total - transaction.coin_inserted; OLED_ShowString(0, 32, "Need more:", FONT_16); OLED_ShowNumber(80, 32, remaining, FONT_16); } }4. 调试技巧与性能优化
4.1 OLED显示刷新优化
直接刷新全屏会导致肉眼可见的闪烁,采用差异刷新策略:
- 定义显示缓存结构
typedef struct { char line1[21]; // 128像素/6像素每字符 ≈ 21字符 char line2[21]; char line3[21]; char line4[21]; } DisplayBuffer;- 比较新旧内容,仅更新变化部分
void smart_refresh(DisplayBuffer *new, DisplayBuffer *old) { if(strcmp(new->line1, old->line1)) { OLED_ShowString(0, 0, new->line1, FONT_16); strcpy(old->line1, new->line1); } // 其他行同理... }4.2 继电器驱动时序问题
测试中发现继电器有时会误动作,通过示波器捕获到的问题:
- 线圈驱动信号上升沿太缓(>2ms)
- MCU复位时GPIO处于浮空状态
改进方案:
// 硬件改进:GPIO增加10k下拉电阻 // 软件改进: void relay_control(uint8_t on) { if(on) { GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET); delay_ms(50); // 确保完全吸合 } else { GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET); delay_ms(20); // 确保完全释放 } }4.3 低功耗设计技巧
虽然售货机通常常电运行,但加入休眠模式可延长电池续航:
- 配置停机模式:
void enter_stop_mode(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后需要重新配置系统时钟 SystemInit(); }- 通过按键中断唤醒:
void EXTI0_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line0) != RESET) { EXTI_ClearITPendingBit(EXTI_Line0); // 唤醒处理逻辑 } }这个项目最让我惊喜的是,用成本不到50元的元件就实现了一个真实可用的自动售货机核心功能。特别是在调试继电器时序时,用逻辑分析仪捕获到的信号异常最终让我理解了硬件消抖的重要性。如果想让项目更具商业价值,可以考虑增加RFID支付模块或者网络远程库存管理功能——这正好是我下一步的升级计划。