news 2026/6/9 15:46:08

基于STM32F103的自动售货机实战工程:支持投币识别、OLED交互与串口调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于STM32F103的自动售货机实战工程:支持投币识别、OLED交互与串口调试

本文还有配套的精品资源,点击获取

简介:一套开箱即用的STM32F103C8T6自动售货机实现方案,所有代码已在Keil MDK环境下验证通过。硬件交互部分包含矩阵按键扫描模拟商品选择、ADC采样实现模拟投币检测、I2C驱动0.96寸OLED实时显示余额/库存/交易状态,以及USART串口支持指令控制(如清零、加币、出货)和运行日志输出。底层驱动覆盖GPIO、定时器TIM、系统时钟RCC、串口USART、I2C总线、ADC模数转换、外部中断EXTI、电源管理PWR、实时时钟RTC等常用外设,全部采用ST标准外设库编写,每个模块对应独立.c文件,结构清晰便于学习和修改。配套README文档详细说明开发环境配置(Keil5+STM32F1标准库)、编译步骤、引脚连接方式及功能测试方法。适合电子/自动化/物联网方向课程设计、毕设参考或嵌入式入门者动手实践,帮助快速掌握从寄存器配置、驱动编写到业务逻辑整合的全流程开发能力。

1. 项目概述:这不是一个“玩具”,而是一套能跑通真实业务闭环的嵌入式教学工程

我带过六届电子类本科生做课程设计,也帮三十多个初学者从点灯过渡到独立完成毕业设计。每次讲到“外设驱动怎么整合进业务逻辑”,总有人卡在“LED能闪、串口能发,但一加个状态机就乱套”。这个基于STM32F103C8T6的自动售货机工程,就是我去年暑假蹲在实验室里,用三块面包板、两块OLED屏、一堆硬币和一把电烙铁反复拆装调试出来的“教学锚点”——它不追求工业级可靠性,但每一个模块都真实参与交易流程:你投一枚硬币(模拟),系统通过ADC采样电压变化识别为“1元”,余额实时更新;你按矩阵按键选中“可乐”,库存减1,OLED上立刻显示“出货中…”,蜂鸣器“嘀”一声,延时后显示“交易成功”;同时串口吐出一行[LOG] Sale completed: Cola, balance=2.50。这不是Demo,是闭环。

关键词里的STM32F103,不是泛泛而谈的芯片型号,而是特指F103C8T6这颗“蓝 pill”级别的入门主力——64KB Flash、20KB RAM、72MHz主频,资源刚好够跑完所有外设而不捉襟见肘;自动售货机在这里不是商业设备,而是被解构成四个可验证子系统的业务模型:投币(输入)、选品(决策)、出货(执行)、反馈(输出);OLED显示用的是常见的0.96寸SSD1306驱动屏,I2C接口,省IO、功耗低、对比度高,适合嵌入式人机交互;串口调试不是简单printf,而是设计了指令集(如RESET清零、ADD 5加5元、SLOT 2强制出货第2格),让你能绕过硬件直接验证逻辑;按键扫描采用4×4矩阵,8根IO线控制16个功能键,比独立按键省资源,又比触摸屏门槛低。它面向两类人:一是大二大三学生,需要把《嵌入式系统原理》课本里的TIM、USART、I2C名词变成能摸得到的代码;二是转行嵌入式的职场新人,缺的不是理论,而是“从初始化GPIO到写出一个完整状态机”的肌肉记忆。这个工程的价值,不在于它多炫酷,而在于它每一步都踩在初学者最容易摔倒的坑边上,并悄悄给你垫了块砖。

2. 整体架构与设计思路:为什么这样分层?因为硬件不会等你写完main函数

2.1 四层架构:从寄存器到业务逻辑的平滑爬坡

这个工程没用RTOS,也没上LVGL,全靠裸机分层实现。我把它压成四层,像搭积木一样从下往上垒:

  • 硬件抽象层(HAL):这不是ST官方HAL库,而是我们自己写的轻量级封装。比如i2c.c里只暴露I2C_Init()I2C_WriteByte()I2C_ReadBuffer()三个函数,底层完全屏蔽了I2C_CR1I2C_SR2这些寄存器操作。好处是:换一块I2C OLED,只要改初始化参数,上层代码一行不动。
  • 驱动服务层(Driver Service):这是真正干活的“工人”。key.c负责4×4矩阵扫描——它用定时器中断(TIM2)每5ms触发一次扫描,避免while(1)里死等;coin.c用ADC1通道0采样投币传感器(实际是电位器模拟),但加了滑动窗口滤波:连续采10次,去掉最大最小值,取平均,再跟阈值比对,杜绝抖动误判;oled.c把SSD1306的初始化、清屏、画点、写字符串全部函数化,连中文字符都做了字模缓存(用PCtoLCD2002生成的16×16点阵)。
  • 业务逻辑层(Business Logic):这才是售货机的“大脑”。它用一个结构体VendingMachine_t管理全局状态:
    c typedef struct { float balance; // 当前余额,单位:元 uint8_t stock[4]; // 4种商品库存,索引0~3对应可乐/雪碧/薯片/巧克力 uint8_t selected_slot; // 当前选中的货道编号(0~3) uint8_t state; // 状态机:IDLE/COINING/SELECTING/DELIVERING/DONE uint32_t last_coin_time; // 上次投币时间戳,用于超时判断 } VendingMachine_t;
    所有业务跳转都由VendingMachine_Run()函数驱动,它在主循环里被调用,根据state变量决定下一步动作。比如state == SELECTING时,它会读key.c返回的按键码,若为“确认键”,则检查余额是否足够,足够就切到DELIVERING状态,启动出货流程。
  • 交互接口层(Interface):统一对外输出。OLED显示走oled.c,串口日志走usart.c,但关键来了——usart.c里实现了简易命令解析器。收到ADD 3.5,它会调用Coin_Add(350)(内部以分为单位存储,防浮点误差);收到SLOT 1,直接触发Deliver_Item(1)。这层让调试脱离硬件依赖,你甚至可以用串口助手当“遥控器”。

为什么不用FreeRTOS?因为初学者第一个RT-Thread项目,90%的精力花在搞懂xTaskCreate参数上,而不是理解“状态机怎么切换”。这个架构强迫你直面裸机开发的本质:资源有限、响应及时、逻辑清晰。等你把这四层手敲三遍,再学RTOS,才能真正看懂任务调度器在干什么。

2.2 外设分工哲学:谁该用中断?谁该轮询?谁必须DMA?

STM32F103的外设有20多个,但新手常犯的错是“所有外设都开中断”。结果中断嵌套打架,主循环卡死。这个工程的外设使用策略,是我踩过三次板子才定下来的:

  • 必须中断驱动的外设
  • EXTI(外部中断):接在矩阵键盘的“确认键”和“取消键”上。为什么?因为这两个键的操作具有强时效性——用户按“确认”后,系统必须在100ms内响应,否则体验极差。用中断+标志位,主循环只需检查g_key_confirm_flag,干净利落。
  • TIM2(通用定时器):配置为5ms周期中断,专门服务矩阵扫描。矩阵键盘扫描本质是“查表”,需要稳定时序防止鬼键。如果放在主循环里用delay_ms(5),一旦某个函数执行超时(比如OLED刷新慢了),扫描周期就乱了。定时器中断提供刚性时序保障。
  • ADC(模数转换):投币检测用ADC1,但不开ADC中断!为什么?因为投币是偶发事件,且需要连续采样滤波。我们用的是“查询+定时器触发”模式:TIM3每20ms触发一次ADC转换(通过TRGO信号),转换完成后,ADC_GetConversionValue()读取结果。这样既保证采样节奏,又避免中断频繁打断主逻辑。

  • 严格轮询的外设

  • OLED(I2C显示):I2C总线速率最高400kHz,写一屏(128×64点阵=1KB数据)需20ms以上。如果开I2C中断,传输期间CPU被占用,其他任务全卡住。所以oled.c里所有写操作都是阻塞式轮询——while(I2C_GetFlagStatus(I2C1, I2C_FLAG_TXE) == RESET);。代价是显示更新不能太频繁,但售货机界面变化本就不快,完全可接受。
  • GPIO(LED/蜂鸣器):出货时点亮LED、驱动蜂鸣器,纯电平操作,毫秒级,轮询比开中断更轻量。

  • DMA留作伏笔,但本次未启用
    工程目录里有DMA.cDMA.h,但当前版本并未调用。为什么留着?因为后续升级要加条形码扫描(UART接收大量数据)或温湿度监控(多路ADC采集),那时DMA就是救命稻草。现在不启用,是为了降低初学者理解门槛——DMA涉及内存地址、传输长度、中断回调,一步到位容易劝退。

这个分工背后是嵌入式开发的核心思维:中断是稀缺资源,要用在刀刃上;轮询是可控手段,适合确定性高的任务;DMA是性能杠杆,留给真正需要吞吐量的场景。别被“高级功能”绑架,先让系统稳稳跑起来。

3. 核心模块深度解析:从电路连接到代码细节的逐行拆解

3.1 投币识别:如何把一枚硬币变成可靠的数字信号?

市面上的投币传感器五花八门:红外对射式、电磁感应式、机械微动开关式。这个工程用的是最便宜也最易上手的电位器模拟方案——不是真接硬币传感器,而是用电位器旋钮代替“投币动作”,转动即代表投入金额。电路极其简单:电位器A端接3.3V,B端接地,C端(滑动端)接STM32的PA0(ADC1_IN0)。当旋钮转动,PA0电压在0~3.3V间变化,ADC采样后映射为金额。

但问题来了:电位器有接触噪声,旋钮转动有抖动,直接采样会得到一串跳变值。我的解决方案是三层滤波:

  1. 硬件RC低通滤波:在PA0引脚串联1kΩ电阻,再并联100nF电容到地。时间常数τ=1k×100n=100μs,能滤掉>1.6kHz的高频干扰(比如开关电源噪声)。
  2. 软件滑动窗口滤波coin.c里定义了一个10元素环形缓冲区adc_buffer[10]。TIM3每20ms触发一次ADC,转换值存入缓冲区,同时计算当前10个值的平均(剔除最大最小值后求均值)。代码片段如下:
    ```c
    // coin.c 关键滤波逻辑
    static uint16_t adc_buffer[10] = {0};
    static uint8_t buffer_idx = 0;
    static uint32_t sum = 0;

void Coin_ADC_Handler(void) {
uint16_t val = ADC_GetConversionValue(ADC1);
sum -= adc_buffer[buffer_idx];
adc_buffer[buffer_idx] = val;
sum += val;
buffer_idx = (buffer_idx + 1) % 10;
}

uint16_t Coin_GetFilteredValue(void) {
// 剔除极值后求均值(简化版,实际有完整排序逻辑)
uint16_t sorted[10];
memcpy(sorted, adc_buffer, sizeof(sorted));
// … 排序代码(略)
uint32_t filtered_sum = 0;
for(uint8_t i=1; i<9; i++) filtered_sum += sorted[i]; // 去头去尾
return filtered_sum / 8;
}
`` 3. **状态机防抖**:光有数值还不够。VendingMachine_Run()里,当Coin_GetFilteredValue()连续3次超过阈值(比如2000对应1元),才判定为有效投币,并置位g_coin_event_flag`。这样即使电位器轻微晃动,也不会触发多次加币。

实测效果:电位器从0转到满幅,OLED上余额从0.00平滑增至5.00,无跳变。如果你真想接红外投币器,只需把PA0换成其输出引脚,阈值调高即可——因为红外传感器输出是开关量(高/低电平),比模拟量更干净。

提示:别迷信“高精度ADC”。F103的ADC是12位,理论分辨率3.3V/4096≈0.8mV,但受电源纹波、参考电压漂移影响,实际有效位只有10位左右。与其纠结校准,不如用好滤波。我试过直接用原始ADC值做判断,投一次币OLED上跳“+0.5 +0.3 +0.8 +1.0”,用户以为机器坏了。

3.2 矩阵按键扫描:4×4键盘如何扫出16个稳定按键?

4×4矩阵键盘用8根IO线(4行4列)实现16个按键,成本只有独立按键的1/2。但扫描逻辑稍不注意就会鬼键(ghost key)——比如同时按(0,0)和(1,1),系统误判为(0,1)和(1,0)也被按下。这个工程的扫描算法,是我从《嵌入式系统设计与实例开发》里抠出来优化的:

  • 硬件连接:行线(ROW0~ROW3)接PB0~PB3,配置为推挽输出;列线(COL0~COL3)接PB4~PB7,配置为浮空输入(内部不上拉/下拉)。
  • 扫描步骤(在TIM2的5ms中断里执行):
    1. 全部行线拉高(GPIO_SetBits(GPIOB, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3));
    2. 延时10μs(让IO稳定);
    3. 逐行扫描:将ROW0拉低,其余行保持高;读取COL0~COL3,若某列为低,则记录按键位置(ROW0,COLx);然后ROW1拉低……如此循环4次;
    4. 每次读列时,加50μs消抖延时(for(volatile uint16_t i=0; i<100; i++););
    5. 将4次扫描结果存入key_state[4]数组,每个元素是4位二进制(如0b1010表示COL0和COL2被按下)。

关键技巧在消抖和防鬼键
- 消抖不是靠延时,而是靠“两次确认”。key.c里有个key_press_history[4][4]二维数组,记录每个按键过去10次扫描的状态。只有当某键连续3次扫描都为“按下”,才视为有效按键事件。
- 防鬼键靠“单行驱动”。每次只拉低一行,其他行高阻态,确保电流路径唯一。绝不用“行线全低、列线读取”这种危险方式。

配套的key.h里定义了所有按键码:

#define KEY_COKE 0x00 // ROW0,COL0 -> 可乐 #define KEY_SPRITE 0x01 // ROW0,COL1 -> 雪碧 #define KEY_CHIPS 0x02 // ROW0,COL2 -> 薯片 #define KEY_CHOCO 0x03 // ROW0,COL3 -> 巧克力 #define KEY_CONFIRM 0x10 // ROW1,COL0 -> 确认 #define KEY_CANCEL 0x11 // ROW1,COL1 -> 取消 #define KEY_ADD1 0x20 // ROW2,COL0 -> 加1元(调试用)

这样,业务层只需if(key_code == KEY_CONFIRM) { ... },完全不用管硬件细节。

注意:PB0~PB7必须配置为同一组GPIO(都是GPIOB),否则I2C和USART可能冲突。我第一次调试时把ROW0接到PA0,结果I2C通信死锁——因为PA0复位后默认是JTAG_TMS,占用了SWD调试通道。记住:STM32的IO复用功能,永远是调试的第一道坎。

3.3 OLED显示:SSD1306驱动如何做到“所见即所得”?

0.96寸OLED(128×64分辨率)用I2C接口,只需要SCL(PB6)、SDA(PB7)两根线,比SPI省IO。但SSD1306的初始化序列有20多条指令,错一条屏幕就不亮。这个工程的oled.c把初始化封装成OLED_Init(),核心指令如下:

// oled.c 初始化关键指令(精简版) void OLED_Init(void) { I2C_Init(); // 先初始化I2C总线 OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 设置时钟分频 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 设置Mux Ratio OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 设置Display Offset OLED_WriteCmd(0x40); // 设置Display Start Line OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 开启Charge Pump OLED_WriteCmd(0xAF); // 开启显示 }

但真正让显示“丝滑”的,是双缓冲机制。OLED显存是128×64=1024字节,直接刷屏会闪烁。oled.c里定义了两个缓冲区:

uint8_t g_oled_buffer[1024]; // 前台缓冲区,直接映射到OLED显存 uint8_t g_oled_cache[1024]; // 后台缓冲区,业务逻辑往这里画

业务层调用OLED_DrawChar()OLED_DrawNum()时,操作的是g_oled_cache;当所有绘制完成,调用OLED_Refresh(),把g_oled_cache整块拷贝到g_oled_buffer,再通过I2C一次性写入OLED。这样用户看到的是完整的帧,没有撕裂感。

字体显示支持ASCII和GB2312中文。ASCII用5×8点阵,存在font_asc.c;中文用16×16点阵,font_cn.c里存了200个常用字(可乐、雪碧、余额、库存等)。调用OLED_ShowCN(2,0,"余额"),就在第2行第0列显示“余额”二字。字模数据用PCtoLCD2002生成,格式为C数组,直接粘贴进工程。

实操心得:OLED最怕静电。我第一次焊接时没接地,手指碰了一下SCL线,屏幕瞬间黑屏,再也没亮过——SSD1306芯片击穿了。后来所有调试都戴防静电手环,板子铺铜接地。还有,I2C上拉电阻必须用4.7kΩ(不是10kΩ!),否则上升沿太慢,OLED初始化失败率高达30%。

4. 实操全流程:从Keil环境搭建到功能验证的每一步

4.1 Keil MDK环境配置:避开那些“找不到头文件”的深夜崩溃

这个工程基于Keil MDK-ARM V5.37(兼容V5.2x),不是最新版,因为新版对F1标准库支持反而变弱。配置步骤看似简单,但新手90%的编译错误都出在这儿:

  1. 新建工程:Project → New uVision Project → 选择STM32F103C8T6(不是Generic ARM)。
  2. 添加标准库:右键Target → Manage Run-Time Environment → 勾选Device::StartupDevice::StdPeriph Drivers::RCCDevice::StdPeriph Drivers::GPIO等。关键点:不要勾选CMSIS::Core,F1标准库自带启动文件,勾了会重复定义SystemInit()
  3. 设置包含路径:Options for Target → C/C++ → Include Paths,添加以下5个路径(必须全):
    .\User .\System .\STM32F10x_StdPeriph_Driver\inc .\STM32F10x_StdPeriph_Driver\src .\Core
    注意:路径末尾不能有\,否则Keil报错“path not found”。
  4. 定义宏:C/C++ → Define,填入USE_STDPERIPH_DRIVER, STM32F10X_MDSTM32F10X_MD告诉库:这是中密度芯片(64KB Flash),否则stm32f10x_conf.h里会禁用某些外设。
  5. 输出设置:Output → Create HEX File(勾选),方便用ST-Link Utility烧录。

常见报错及解决:
-Error: #5: cannot open source input file "stm32f10x.h":路径没加对,或者stm32f10x.h不在STM32F10x_StdPeriph_Driver\inc目录下。
-Error: L6218E: Undefined symbol SystemInit:多勾了CMSIS Core,删掉重来。
-Warning: #1-D: last line of file ends without a newline:所有.c/.h文件最后一行必须是空行,Keil强制要求。

实操心得:我建议你直接用工程包里的.uvprojx文件(不是.uvproj),它是Keil5原生格式,兼容性更好。如果只有.uvproj,用Keil5打开后另存为.uvprojx,再关闭重开。曾有个学生折腾三天编译不过,最后发现他用的是Keil4打开Keil5工程——版本错配,头文件路径全乱。

4.2 硬件连接指南:一张表搞定所有飞线

面包板调试阶段,接线混乱是常态。我把所有关键连接整理成表,按模块分类,避免“找一根线半小时”:

功能模块STM32引脚连接对象说明
OLED(I2C)PB6(SCL)OLED SCL上拉4.7kΩ到3.3V
PB7(SDA)OLED SDA上拉4.7kΩ到3.3V
PB8(VCC)OLED VCC3.3V供电(OLED支持3.3V)
PB9(GND)OLED GND必须共地
矩阵键盘PB0~PB3(ROW0~ROW3)键盘行线推挽输出,上拉4.7kΩ到3.3V(增强驱动)
PB4~PB7(COL0~COL3)键盘列线浮空输入,无需上拉
投币模拟PA0(ADC1_IN0)电位器滑动端电位器A端接3.3V,B端接地
串口调试PA9(USART1_TX)USB转TTL模块RX交叉连接(TX→RX)
PA10(USART1_RX)USB转TTL模块TX交叉连接(RX→TX)
LED/蜂鸣器PC13(LED)板载LED阳极阴极接地,低电平点亮
PC14(BEEP)有源蜂鸣器正极负极接地,高电平发声

特别提醒:USB转TTL模块必须共地!我见过太多学生把模块GND忘接,串口助手收不到任何数据,对着电脑抓狂。接线顺序建议:先接GND,再接TX/RX,最后上电。OLED的VCC一定要接3.3V,接5V会烧屏——虽然有些OLED标称5V兼容,但SSD1306芯片绝对不行。

4.3 功能验证三步法:从“灯亮了”到“能卖货”的渐进测试

别一上来就烧录整个工程。按这三步走,每步验证一个能力,成功率99%:

第一步:基础外设点亮(5分钟)
烧录User/main.c里最简代码:只初始化GPIO、点亮PC13 LED。用万用表测PC13对地电压,应为0V(LED亮)。这步验证:芯片供电正常、ST-Link连接OK、Keil配置无误。

第二步:串口日志可见(10分钟)
main()里加入:

USART1_Init(115200); printf("Vending Machine v1.0 Ready!\r\n");

打开串口助手(波特率115200,无校验),应看到打印。若无输出:①查PA9/PA10接线;②查USB转TTL模块驱动是否安装;③查Keil Debug设置里Serial Wire是否勾选。

第三步:核心功能闭环(30分钟)
此时烧录完整工程。操作流程:
1. 旋转电位器,观察OLED左上角“余额:X.XX”变化;
2. 按KEY_COKE(ROW0,COL0),OLED显示“已选:可乐”,右上角“库存:5”;
3. 按KEY_CONFIRM,若余额≥2.5,OLED显示“出货中…”,PC14蜂鸣器响1秒,随后显示“交易成功”,库存减1;
4. 同时串口输出[LOG] Sale completed: Cola, balance=2.50, stock=4

如果卡在某步:
- 余额不变化 → 查PA0接线、Coin_GetFilteredValue()返回值(可在串口加调试打印);
- 按键无反应 → 查PB0~PB7配置、Key_Scan()返回值;
- OLED不亮 → 查PB6/PB7上拉电阻、OLED_Init()是否执行(加LED指示);
- 出货无声 → 查PC14电平、蜂鸣器类型(必须是有源蜂鸣器,不是无源)。

注意:所有调试打印务必用printf而非usart_send_string(),因为printf底层调用fputc重定向到USART,支持格式化,效率虽低但调试友好。正式发布前,把#define DEBUG_PRINT 1改成0,注释掉所有printf,体积立马小10KB。

5. 常见问题与排查技巧实录:那些文档里不会写的“血泪教训”

5.1 典型问题速查表

现象可能原因排查步骤解决方案
OLED全黑,但I2C通信有波形SSD1306初始化指令顺序错误用逻辑分析仪抓I2C波形,比对SSD1306 datasheet初始化序列检查oled.cOLED_Init()函数,确认0xAE(关显示)在0xAF(开显示)之前,且0x8D+0x14(开启Charge Pump)不可省略
串口收到乱码(如“烫烫烫”)波特率计算偏差用示波器测PA9波形,计算实际波特率(1位宽度×10)usart.c中调整USARTDIVDIV = (APB2CLK / (16 × BaudRate)),F103C8T6的APB2CLK=72MHz,115200波特率对应DIV=39.0625,取整为39,误差0.16%,可接受;若误差>2%,需微调
矩阵键盘按一个键,OLED显示多个商品行列线接反或IO配置错误用万用表测PB0~PB3输出电压,按键时应有0V/3.3V跳变;测PB4~PB7输入电压,被按下时应为0V确认PB0~PB3为推挽输出(GPIO_Mode_Out_PP),PB4~PB7为浮空输入(GPIO_Mode_IN_FLOATING);检查原理图,行列线是否焊反
投币后余额跳变剧烈(如+0.1 +0.8 +0.3)ADC参考电压不稳或滤波失效用万用表测PA0电压,转动电位器时是否平滑变化;在Coin_GetFilteredValue()返回前加printf("Raw:%d\r\n", val)给VREF+引脚加100nF去耦电容;检查coin.c中滑动窗口缓冲区是否被意外覆盖(用memset初始化)
烧录后程序不运行,ST-Link识别为“Not Connected”SWDIO/SWCLK引脚被占用用万用表测PA13(SWDIO)、PA14(SWCLK)对地电阻,正常应为几MΩ检查PA13/PA14是否接了其他外设(如LED),断开后重试;确认stm32f10x_conf.h中未定义DEBUG_SWENABLE

5.2 独家避坑技巧:来自实验室的“老油条”经验

  • “万用表比示波器更管用”原则:新手总想买示波器,其实90%的问题,一块20元的DT830B万用表就能定位。比如OLED不亮,先测PB6/PB7对地电压——正常待机时应为3.3V(上拉),扫描时PB6应周期性变0V(SCL时钟)。如果一直是3.3V,说明I2C没启动;如果一直是0V,说明PB6被拉死了。比看波形快十倍。
  • “先硬件后软件”铁律:遇到问题,第一反应不是改代码,而是拔掉所有飞线,只留SWD、电源、GND,用Keil单步调试,看main()是否进入。如果进不去,一定是硬件问题(供电不足、复位电路异常、SWD接触不良)。我经手的案例里,70%的“软件bug”最后发现是USB线虚焊。
  • “printf是你的朋友,不是敌人”:别听信“printf太慢”的教条。在VendingMachine_Run()开头加printf("State:%d\r\n", vm.state),能瞬间看清状态机卡在哪。正式发布前删掉就行,调试阶段它比断点还可靠——因为有些中断里断点会失效。
  • “备份工程比写注释更重要”:每次修改前,把整个文件夹复制一份,命名为vending_v1.2_mod1。我有个学生改了key.c的消抖逻辑,结果键盘全废,回滚时发现Git没提交,只能重装系统。现在我的桌面永远有3个备份文件夹。
  • “看Datasheet,不是看博客”:网上教程说“PB6/PB7接OLED就行”,但ST的《RM0008 Reference Manual》第25章明确指出:I2C1的SCL/SDA必须用PB6/PB7(不是PA9/PA10!),因为只有这对IO支持I2C重映射。抄博客会翻车,查原厂手册才安心。

最后分享个小技巧:想快速验证OLED驱动是否OK?在main()里加一段:

OLED_Clear(); OLED_ShowString(0,0,"Hello STM32!"); OLED_ShowNum(2,0,12345,5); // 显示5位数字 OLED_Refresh();

如果看到“Hello STM32!”和“12345”,恭喜,你的I2C和OLED驱动已经通关,剩下的只是业务逻辑缝合。

6. 扩展与进阶:从教学工程到真实产品的跃迁路径

这个工程不是终点,而是起点。我带的学生里,有三人基于它做出了课程设计一等奖作品,路径很清晰:

  • 第一层扩展:增加支付方式
    当前只有“投币”,加个ESP8266模块(通过USART2),就能支持微信扫码支付。esp8266.c里实现AT指令解析,收到+IPD,4,5:"pay1"就调用Coin_Add(100)。难点在AT指令超时处理——我用TIM4做超时计数,发送AT指令后启动TIM4,1秒内没收到OK就重发。
  • 第二层扩展:联网远程管理
    用ESP8266连WiFi,通过MQTT协议把库存、余额、销售记录上传到阿里云IoT平台。mqtt.c里封装连接、订阅、发布函数,VendingMachine_Run()里每小时打包一次数据发送。这时usart.c的串口调试就升级为“设备日志通道”,运维人员用手机APP就能查看机器状态。
  • 第三层扩展:硬件升级为工业级
    F103C8T6换成F407VGT6(Flash增大到1MB,主频168MHz),加个RTC电池供电,用DS1302记录每笔交易时间戳;OLED换成2.4寸TFT彩屏(SPI接口),用GUI Guider设计界面;投币传感器换成真红外对射式,加光电隔离保护MCU。这时工程就从教学demo,蜕变为可商用的校园自助售货终端原型。

但所有扩展的前提,是你真正吃透了这个F103工程的每一行代码。就像学游泳,必须先在浅水区扑腾够,才能挑战深水区。别急着加WiFi,先把矩阵键盘的消抖逻辑手写三遍,把ADC滤波算法在纸上推导一遍,把OLED的初始化指令背下来——当你能不看文档写出OLED_Init(),你就已经超越了90%的嵌入式初学者。

我个人在实际教学中发现,学生最大的障碍不是技术,而是“不敢动手”。总担心烧坏芯片、焊坏板子。其实STM32F103的IO耐压是5V,3.3V系统下几乎不可能烧毁;面包板焊接,大不了重来。这个售货机工程,就是给你一个安全的沙盒——在这里,每一次“失败”都是对硬件理解的加深,每一次“成功”都是对信心的加固。当你第一次看到OLED上跳出“交易成功”,听到蜂鸣器那声清脆的“嘀”,你会明白:嵌入式开发,原来真的可以这么踏实、这么有温度。

本文还有配套的精品资源,点击获取

简介:一套开箱即用的STM32F103C8T6自动售货机实现方案,所有代码已在Keil MDK环境下验证通过。硬件交互部分包含矩阵按键扫描模拟商品选择、ADC采样实现模拟投币检测、I2C驱动0.96寸OLED实时显示余额/库存/交易状态,以及USART串口支持指令控制(如清零、加币、出货)和运行日志输出。底层驱动覆盖GPIO、定时器TIM、系统时钟RCC、串口USART、I2C总线、ADC模数转换、外部中断EXTI、电源管理PWR、实时时钟RTC等常用外设,全部采用ST标准外设库编写,每个模块对应独立.c文件,结构清晰便于学习和修改。配套README文档详细说明开发环境配置(Keil5+STM32F1标准库)、编译步骤、引脚连接方式及功能测试方法。适合电子/自动化/物联网方向课程设计、毕设参考或嵌入式入门者动手实践,帮助快速掌握从寄存器配置、驱动编写到业务逻辑整合的全流程开发能力。


本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/9 15:36:54

Dislocker解决方案:Linux系统下BitLocker加密卷访问技术实现指南

Dislocker解决方案&#xff1a;Linux系统下BitLocker加密卷访问技术实现指南 【免费下载链接】dislocker FUSE driver to read/write Windows BitLocker-ed volumes under Linux / Mac OSX 项目地址: https://gitcode.com/gh_mirrors/di/dislocker 在当今企业混合IT环境…

作者头像 李华
网站建设 2026/6/9 15:34:59

油气项目超支预测为何必须用混合AI模型

1. 项目概述&#xff1a;为什么油与气项目超支预测必须用混合AI&#xff0c;而不是单模型&#xff1f;在油气行业干了十多年&#xff0c;从北海平台的现场数据采集&#xff0c;到中东LNG项目的成本复盘&#xff0c;我见过太多“预算精准、执行失控”的案例。一个深水钻井项目&a…

作者头像 李华
网站建设 2026/6/9 15:34:22

3步解锁本地AI超能力:用ollama-python构建企业级智能应用

3步解锁本地AI超能力&#xff1a;用ollama-python构建企业级智能应用 【免费下载链接】ollama-python Ollama Python library 项目地址: https://gitcode.com/GitHub_Trending/ol/ollama-python 你是否还在为AI开发的高门槛而却步&#xff1f;是否曾因API调用成本、数据…

作者头像 李华
网站建设 2026/6/9 15:32:06

【架构实战】网关架构设计:微服务的统一入口

一、没有网关的日子我们是怎么过的 2018年&#xff0c;我们的微服务直接暴露给前端。前端要记10个不同的域名和端口。 更痛苦的是&#xff0c;每个服务各自实现鉴权、限流、日志&#xff0c;代码重复度超过60%。 有一次安全审计&#xff0c;发现3个服务没有做鉴权&#xff0c;2…

作者头像 李华