十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

蓝桥杯自动售水机赛题拆解:状态机与单片机外设实战

蓝桥杯自动售水机赛题拆解:状态机与单片机外设实战 简介蓝桥杯单片机第三届初赛第四届模拟题自动售水机是一个面向竞赛备赛的完整单片机项目资源适合参加蓝桥杯单片机设计与开发赛项的选手也适合希望通过真实案例强化嵌入式编程能力的初学者。整个压缩包共24个文件大小约760KB除了主程序main.c和多个.h头文件外还包含iic.c、onewire.c、ds1302.c等驱动源码分别实现I²C通信、一线总线OneWire和DS1302实时时钟的底层控制同时提供Keil工程文件、Hex烧录文件、编译链接产物以及原版赛题PDF便于导入开发环境直接编译、烧录和对照需求分析。目前已有704人学习下载。资源完整呈现了自动售水机的软硬件实现思路从传感器数据采集、实时时钟读取到出水控制逻辑和状态显示交互都有清晰代码可参考对于备赛者既可以按赛题要求逐步验证功能也能从中学到如何划分模块、编写驱动和进行工程配置是一份适合赛前系统训练和赛后复盘的学习素材。 每年蓝桥杯单片机组的备赛群里都会有人问同一句话自动售水机这道题到底在考什么我第一次翻到这份第三届初赛的赛题时第一反应是——售水机不是应该用嵌入式Linux加个流量计才像话么看完整张卷子才发现它把单片机课上几乎所有基本功都装进去了矩阵键盘、动态数码管、继电器驱动、EEPROM存储、定时器调度。做完之后我特意看了一眼当年这道题的得分分布两极分化相当严重会写状态机的人四十多分钟就交卷不会的人写了两百行顺序代码还一直被按键打断。这篇文章不打算给你一份可以原样背走的答案而是把这道题从读题、硬件分配、程序框架到现场调试的全过程拆开讲。不管你是第一次备赛蓝桥杯还是已经入了门但总在功能一多程序就乱的边缘试探这份思路应该都能帮你省下不少盲调的时间。1. 赛题拆解自动售水机到底在考什么1.1 还原功能需求赛题原文本里的功能描述通常不长但每个字都是考点。以自动售水机这道题为参考结合当年我参加比赛时的记忆核心功能一般可以还原成这几条系统上电后数码管显示欢迎界面或初始金额。通过矩阵键盘模拟投币比如按下对应数字键代表投入1元、5元、10元面额。选择出水模式常见的是1元杯、2元杯、3元杯或大杯/小杯。金额足够时继电器吸合模拟出水阀打开数码管实时显示剩余金额或出水倒计时。金额不足、重复选择等异常操作需要蜂鸣器或LED给出明确提示。支持查询累计营业额以及通过特定按键组合修改单价修改结果掉电不丢失。这其实就是一台简化版自动售水机的全部业务闭环。你不需要真的做出水流传感器也不需要联网支付所有物理量都用单片机的外设模拟按键代表投币继电器代表出水阀数码管代表显示屏EEPROM代表后台存储。题目把真实产品抽象成了嵌入式开发里最常见的输入、处理、输出、存储四个环节。1.2 真正的考点不是外设是业务逻辑很多备赛的人会把精力放在矩阵键盘怎么写数码管怎么不闪烁这些外设驱动上这当然没错但自动售水机这道题真正拉开差距的地方在于多个外设组合起来之后的流程编排。用大白话说售水机不是一个按一次键执行一次动作的简单装置它是一个连续的业务闭环投币、选择、出水、结束这四个阶段互相衔接而且中间任何一步都可能被用户乱按。比如出水过程中用户又按了其他按钮系统应该忽略投币之后没有选择过一段时间应该自动回到待机状态金额刚好等于单价出水结束时余额归零状态也得能正确切回空闲。如果你用从头顺序执行到尾的思路去写程序会陷入大量 if 嵌套每一个异常情况都要单独打补丁。最后代码是能跑但稍微改动一个按键定义整个逻辑就崩了。而状态机天然就是为这种多阶段、可打断、可恢复的业务场景准备的。1.3 试题背后的知识点清单我当年刷完这道题之后习惯性地把它踩中的考点列了一遍大致是这些考点对应功能环节难度矩阵键盘扫描与消抖投币、选择出水模式★★☆动态数码管显示金额、剩余水量、提示信息★★★定时器中断显示刷新、出水倒计时★★★继电器与蜂鸣器出水阀、错误报警★★☆I2C总线与EEPROM单价、营业额的掉电保存★★★★状态机设计整个售水流程★★★★看到这个清单你就明白了这道题不是靠某一个高深技巧取胜的它考的是你把一堆基础外设拧成一套完整业务系统的能力。2. 硬件资源分配赛板上每一份资源都怎么用2.1 先盘一遍板上外设蓝桥杯官方赛板的外设相当齐全矩阵键盘、独立按键、8位数码管、8个LED、蜂鸣器、继电器、AT24C02、DS18B20、DS1302、AD/DA电位器全都在。自动售水机这道题真正需要的其实只有矩阵键盘、数码管、继电器、蜂鸣器和EEPROM。DS18B20、DS1302那些模块哪怕板上焊着也不要去碰。这里有一个很实际的建议备赛时不要老想着板子上的东西我全都要驱动一遍按题目需求做最小资源分配能让你省下大量排查时间。自动售水机的功能里没有任何一处需要实时温度或日历时钟你去初始化DS18B20反而多了一堆可能出错的单总线时序。2.2 引脚分配参考以官方赛板常见的引脚定义为例我给出一套典型的资源分配方案。请注意不同批次板子的引脚可能有差异动手前务必对着原理图确认一遍。外设/功能引脚或端口说明数码管段码P0经锁存器输出段码数码管位选P2部分引脚经138译码器选择当前位数LED指示P2部分引脚经138译码器/锁存器控制矩阵键盘行/列P3或P4按下时读回键值继电器P2某个引脚驱动模拟出水阀蜂鸣器P2某个引脚提示音AT24C02P2口对应SCL/SDAI2C总线写具体程序之前我强烈建议先画一张自己看得懂的引脚-功能对应表贴在调试笔记上。这样写代码时不用每次翻原理图而且后面查错时能一眼定位这个引脚是不是和那个外设打架了。2.3 驱动极性陷阱继电器和蜂鸣器千万别想当然这道题里面最容易翻车的地方不是键盘也不是数码管而是继电器和蜂鸣器的驱动极性。很多赛板上蜂鸣器用的是PNP三极管驱动单片机引脚给低电平时蜂鸣器响而继电器用的是NPN三极管或ULN2003达林顿管通常给高电平时继电器才吸合。两者的有效电平正好相反。如果你只凭经验以为蜂鸣器低有效继电器也低有效一上电继电器就乱吸合出水阀不受控制地动作整个售水流程全乱套。正确做法是写代码前先看原理图确认每个驱动管的接法然后在代码里把宏定义写清楚#define BEEP_ON P2_0 0 // 蜂鸣器低电平触发 #define RELAY_ON P2_1 1 // 继电器高电平触发这种宏定义不仅是给自己看的也是给评分老师看的。代码里出现 magic number 和一眼能读懂的宏阅卷时印象分完全不一样。3. 程序框架状态机驱动的售水流程3.1 为什么说状态机是这道题的胜负手自动售水机的业务天然就是一个状态模型我用很少的几个状态就能把整个流程描述清楚空闲(IDLE) - 投币中(COIN) - 选择中(SELECT) - 出水中(OUT) - 空闲(IDLE)每一个状态里程序只处理这个状态下该处理的事。比如在空闲状态按数字键是投币但在出水状态按下数字键应该被忽略否则用户中途乱按就会把金额算错。状态机的每一层逻辑都清晰独立不用担心一个按键事件渗透到不该出现的阶段。还有一个容易被忽略的好处状态机让代码的调试变得特别方便。现场如果发现出水时间少了1秒你可以直接定位到 OUT 状态里的倒计时逻辑而不用在几百行顺序代码里捞针。3.2 核心数据结构主程序里我习惯定义这样一组变量typedef enum { STATE_IDLE, STATE_COIN, STATE_SELECT, STATE_OUT_WATER, STATE_FAULT } sys_state_t; sys_state_t current_state STATE_IDLE; u16 remain_money; // 当前可用金额单位分 u16 unit_price; // 当前单价单位分 u16 total_sales; // 累计营业额掉电保存 u16 out_countdown; // 出水倒计时单位10ms这里一个关键技巧是金额全部用分为单位用整数运算避免浮点数。51单片机做浮点运算慢、还容易产生精度误差而元转分只是把一个数乘100的事显示的时候再拆成元、角、分三个数字就行了。3.3 计价逻辑与边界处理计价逻辑是整个程序里最容易写漏的部分。我当时的实现思路是这样的投币阶段每收到一次有效按键就在 remain_money 上加对应面额上限设为2000分20元超过部分直接丢弃并提示。选择出水模式后先判断 remain_money unit_price 是否成立。不足时蜂鸣器响两声状态不切换。进入出水状态后用定时器每隔10ms扣减金额每次扣掉单价对应的1/100出水总时长等于单价的100倍 / 100 * 10ms这样逻辑上和金额扣减完全同步。出水过程中剩余金额一旦归零立即关断继电器回到空闲状态。边界情况一定要单独列出来测投入金额刚好等于单价出水结束余额必须是0投入金额小于单价按选择键应该被拒绝出水过程中再次投币投币键应无效。这些反人类操作是评测系统最爱出的测试点。3.4 显示刷新与按键扫描的时序配合程序框架上我强烈推荐用定时器中断做时基主循环做逻辑。最常见的分配是定时器0每2ms中断一次用于数码管动态扫描保证每秒刷新频率足够肉眼不闪烁。定时器1每10ms中断一次用于按键扫描和出水倒计时更新。主循环只负责状态机切换和业务逻辑。这样做的核心原因是显示不能卡。如果显示刷新放在主循环里一旦主循环里遇到长延时比如EEPROM写入的5ms等待数码管就会闪一下。而把刷新放在中断里主循环哪怕慢一点显示依然稳定。4. 代码实现几个关键模块的写法4.1 矩阵键盘扫描与消抖矩阵键盘的扫描原理不复杂关键是消抖和防止连按。我常用的写法是逐行扫描法u8 keyscan(void) { u8 row, col, key 0; for (row 0; row 4; row) { set_row_low(row); // 当前行输出低电平 col read_col(); // 读回列电平 if (col ! 0x0F) { // 有按键按下 delay_ms(10); // 消抖 col read_col(); if (col ! 0x0F) { // 消抖后仍按下确认有效 key row * 4 col_index(col); wait_key_release(); // 等待释放防止连按 } } } return key; }这里wait_key_release()很容易被忽略。没有它的话一次按键可能被读成多次投一块钱变成了投三块钱现场看到余额猛涨的画面真的很崩溃。所以我对这个函数的建议是宁可多等一次按键释放也不要省这几毫秒的响应时间。4.2 数码管动态显示动态显示的核心是分时点亮。用定时器配合一个显示缓冲数组每2ms切换到下一位u8 code seg_code[] {...}; // 0~9段码表 u8 disp_buf[8]; // 待显示的数字或符号 void display_isr(void) { static u8 pos 0; P0 0x00; // 消隐防止拖影 set_seg(seg_code[disp_buf[pos]]); set_pos(pos); // 选中当前位 pos (pos 1) 0x07; }关键点有两个一是先消隐再送位选不然会看到淡淡的拖影二是显示缓冲数组是全局的主循环里往disp_buf写入内容中断里读它二者互不干扰。硬件上还要注意138译码器的位选锁存如果用的是锁存器必须先送段码、再开锁存、最后关锁存顺序错了显示全乱。4.3 AT24C02 读写封装EEPROM 在自动售水机里存两样东西单价和累计营业额。写 AT24C02 本质就是I2C协议我把它封装成几个小函数void I2C_Start(void); void I2C_Stop(void); u8 I2C_WriteByte(u8 dat); u8 I2C_ReadByte(void); void EEPROM_Write(u16 addr, u8 dat); u8 EEPROM_Read(u16 addr);存整数时我习惯把16位整数拆成高字节和低字节分两个地址写入void EEPROM_WriteInt(u16 addr, u16 val) { EEPROM_Write(addr, val 8); EEPROM_Write(addr 1, val 0xFF); }这里要特别提醒AT24C02 每一个字节写入之后都需要大约5ms的内部写周期。如果你连续写两个地址必须在两次写入之间加延时或者通过ACK来判断忙状态否则第二个字节很可能写不进去。这个坑我在调试营业额保存时踩过一度以为芯片坏了后来查了数据手册才发现是两次写入间隔太短。4.4 主逻辑状态机主循环的代码是我整个程序里最简短的部分因为逻辑都被拆到状态里了void main(void) { sys_init(); while (1) { key keyscan(); switch (current_state) { case STATE_IDLE: if (is_coin_key(key)) { add_money(key); current_state STATE_COIN; } break; case STATE_COIN: if (is_select_key(key)) { if (remain_money unit_price) { current_state STATE_SELECT; } else { beep_error(); } } break; case STATE_OUT_WATER: // 出水处理由定时器中断完成 break; default: break; } } }看到没有主循环本身不长真正的工作都在每个状态对应的处理函数里。这种代码结构哪怕换个人来接手也能在五分钟内看懂整个业务流程。5. 调试复盘我在现场踩过的坑5.1 按下一次投币键金额却加了两次这是我调试时遇到的第一个诡异现象。原因就是上面提到的按键连击松手的过程中按键的电平抖动被扫描函数又判了一次有效。解决办法是扫描到按键后等待完全释放再返回键值或者用检测到按下沿作为有效触发而不是检测到持续按下。建议在测试时专门做一个压力测试连续快速按同一个键20次看金额是否严格累加20次。很多时灵时不灵的Bug其实是连按导致的不是逻辑问题。5.2 数码管在出水时闪得厉害动态扫描在待机状态很稳定一到出水状态就开始闪原因是出水逻辑里有延时函数。我当时在出水状态用了delay_ms(1000)来模拟出水1秒看似省事实际上把整个显示刷新全部卡死了。解决方法是把延时全部改成定时器计时的非阻塞方式。我定义了一个出水倒计时变量在定时器1的中断里每10ms减一次主循环里只用判断这个变量是否减到0。所有地方都别用阻塞延时这是51单片机多任务调度的最朴素的雏形。5.3 掉电后单价总是回到默认值出厂默认价1元我改成2元断电再上电又变回1元。排查后发现是EEPROM读写时机错了修改单价时直接写进 AT24C02但没有考虑到AT24C02写周期延时导致数据没写进去。还有一种更隐蔽的情况程序每次循环都不停地把RAM里的单价写进EEPROM结果写得太频繁把EEPROM寿命消耗掉了数据反而写不进。正确的做法是只在设置确认、退出设置模式时写一次运行过程中不写存储。5.4 现场评测的隐藏测试点根据我参赛和后来辅导学弟学妹的经验评测老师对自动售水机这类题目除了看基本功能还会专门盯几个隐藏点出水过程中乱按其他按键系统不能乱。投入刚好等于单价的金额出水结束后余额必须为0且状态要回到空闲。投入金额不足时选择出水模式要有明确提示不能静默无反应。修改单价后正常关机再开机单价要保持修改后的值。这些测试点全部对应着状态机的边界处理和EEPROM读写时机是区分能跑和测不挂的分水岭。最后分享一个我个人的备赛习惯拿到一道赛题不要急着敲代码先在纸上把状态图、引脚分配表、关键变量列出来再动手。自动售水机这道题我做完后最大的收获不是学会了某个外设而是第一次感觉到程序不是写出来的是设计出来的。如果你也在准备蓝桥杯单片机组建议把这套状态机非阻塞调度的思路吃透后面遇到智能小车、电子钟、环境监测这些题你会发现骨架都是同一个。本文还有配套的精品资源点击获取
返回列表