简介:2021年12届蓝桥杯第一场停车计费赛题,要求设计一套停车场自动收费系统,覆盖车辆进出管理、时长记录与动态计费;核心考察数据结构选型、时间戳差值算法、异常输入处理,并融合STM32下的嵌入式开发。蓝桥杯作为全国性软件与信息技术竞赛,本题对备赛者、单片机学习者和职场技能提升者均有较强参考价值。资源包共341个文件、13.81MB,主体为C语言源码(.c/.h),内含HAL库驱动和应用层逻辑;另有.o/.axf/.hex等编译连接产物、.uvprojx工程配置与.sct链接脚本,可直接导入Keil MDK在STM32G4上编译运行,也可用于分析编译过程与内存布局。目前已有472人学习下载,目录结构清晰,便于按模块查阅源码,尤其适合赛后复盘BSP板级驱动与业务功能实现。除停车计费主逻辑外,资源内还有定时器、串口、ADC等外设驱动示例,能帮助读者掌握中断服务、状态机与低耦合代码组织方式,完整理解从硬件底层到上层业务输出的工程化流程。
1. 停车计费题,为什么成了历届选手的“分水岭”
2021年第12届蓝桥杯单片机省赛第一场,“停车计费”这四个字一亮出来,现场很多人是松了口气的——没有复杂的温度曲线、没有PWM占空比调光、没有一串串看不懂的报文协议,说白了就是一辆车进来、一辆车出去、算个钱。但我当时考完就有一个直觉:这道题拿高分的人不会太多。因为它不考你“会不会某个外设”,它考的是你“能不能在一个看似简单的业务场景里,把状态、时间、存储这些东西掰扯清楚”。
先说清楚,这道题面向的是单片机设计开发组,用的是国信长天那套CT107D开发板,主控是IAP15F2K61S2。题目模拟的是一个停车场管理系统:有车辆进场、有车辆出场、要显示车位剩余数量、要记录停车时长、要按规则计算停车费。核心外设覆盖了独立按键、LED指示灯、数码管动态扫描、DS1302实时时钟、AT24C02掉电存储。这几样东西单拆开看,练习册上哪道题都有,但是组合在一起塞进一个“停车计费”的业务壳里,考察的就不再是外设驱动,而是业务逻辑的条理性。
为什么我喊它“分水岭”?因为这道题对两类人特别不友好。第一类是那种外设驱动背得滚瓜烂熟、但一遇到多状态交互就大脑空白的选手。DS1302他会读,AT24C02他会写,数码管他会扫,但“进场按键按下去之后,系统要做什么、显示什么、时间存到哪儿、再次上电之后怎么恢复”这一连串问题,他一下子串不起来。第二类是那种平时练习只跑官方示例工程、从来没自己从头搭建过逻辑框架的选手。示例工程里只有“按下按键—现象出现”的直线逻辑,而停车计费要求的是“事件驱动+状态流转+掉电恢复”的完整闭环,这已经无限接近真实产品开发了。
反过来,对平时习惯画画流程图、写着写着就把main函数分成“初始化-按键处理-显示刷新-业务逻辑”几个模块的人来说,这道题就是个送分题。同样4个小时的考试时间,有人光调DS1302的时间写入就耗掉一个多小时,有人四十分钟就把框架搭完了,剩下的时间全在扣边界条件。差距不是手速,是思维习惯。
所以我在给备赛的人做辅导时,从来不会只让他们对着题目敲代码。我会先问一句:如果这个停车场是你家楼下那个,你觉得它应该怎么工作?想明白这个问题,代码怎么写都是顺理成章的事。下一节我就把这道题的功能需求逐条掰开,看看题目背后到底想让你实现什么。
2. 功能需求逐条拆解:每一行字背后都有坑
2.1 停车业务里的三个核心对象:车辆、车位、账单
停车计费的业务模型,说穿了就是三类数据在转:车辆信息、车位状态、计费账单。蓝桥杯这道题不会让你真的去识别车牌,它用按键来模拟车辆的进出,用数码管来展示当前的业务数据,用LED来指示系统状态。你要做的事,就是把这套“模拟系统”做得跟真的一样。
先说车辆进场。按一下“进场”按键,系统要判断当前有没有空余车位,如果有,车位剩余数量减一,记录当前时间作为这辆车的进场时间,同时亮起对应的指示灯表示“有车进来了”。如果车位已经满了,你得拒绝进场,最好能有个提示。再说车辆出场。按“出场”按键,系统读取当前时间,减去那辆车的进场时间,算出停车时长,再套用题目给的计费规则算出费用。费用怎么显示、要不要累加到总营收里、累计数据掉电之后还在不在,这些都是考点。
这里就出现第一个容易让新手翻车的点:停车场不止一个车位,但蓝桥杯题目的硬件资源有限,它不会让你真的管理几十个车位。通常的做法是用数码管显示“剩余车位数”,用LED的点亮数量模拟“当前占用状态”。比如8个LED对应8个车位,亮表示占用。这样一来,你既要维护一个“剩余数量”的整型变量,又要维护一个“车位占用状态”的数组或者位变量。两者必须同步更新,这就是第一个逻辑链。
第二个坑藏在“进出记录”里。真实停车场要记录每辆车的进出时间,但单片机资源有限,题目不会让你写一个链表去存几十辆车的记录。它通常只模拟“当前有一辆车在场内”这种情况,或者最多用一个结构体保存最近一条进场记录。换句话说,你不需要做车队管理,但必须保证“进场的时刻”被可靠地保存下来,并且出场的时候能准确算出差值。这个“保存”动作,就是DS1302和AT24C02登场的地方:DS1302负责提供实时时间,AT24C02负责掉电之后不让数据丢失。
2.2 计费规则:看似是小学数学,实则是状态机考题
停车费怎么算,是整道题的灵魂。蓝桥杯的计费题通常不会让你做一个静态价格表,它会让费用跟“当前时刻”挂钩。比如工作日白天一个价、夜间一个价、周末又是一个价;或者更常见的套路是:按小时计费,前若干小时一个单价,超过之后另一个单价,甚至封顶价格。这样一来,“当前时间属于哪个计费段”这件事,就变成了一个必须实时判断的逻辑。
我当时看到这题,第一反应是“这得用分段函数”,第二反应是“分段函数的边界条件才是真正的坑”。比如跨时段停车怎么算?晚上10点进场、第二天早上7点出场,按哪个费率?题目要是规定了“按进场时刻费率计费”那还好说,取进场时间判断一次就行;要是要求“按实际占用时段分段累加”,那逻辑复杂度直接上一个台阶。考场上我见过有人在这里反复改条件判断,改到最后自己都晕了,这就是典型的“没在动手前把规则画成图”的后果。
我的建议是,考场上拿到题目,先别碰单片机,拿张草稿纸把计费规则写成分段表达式,把所有边界时间点标出来。比如“前2小时5元/小时,之后3元/小时,24小时封顶20元”——你就得问自己:第2小时整到底按哪个算?23小时59分进场跟00点01分进场有什么区别?这些边界想清楚了再写代码,就是一次过;不想清楚,就是无尽的if嵌套地狱。
2.3 外设清单背后的真实分配方案
再捋一遍这道题用到的东西,逐一对应它的职责:
- 数码管:显示剩余车位、停车时长、费用金额,或者滚动显示进出记录。
- LED:显示车位占用状态、系统运行状态、满位报警。
- 独立按键:模拟进场、出场、参数设置(比如切换显示模式、调整费率)。
- DS1302:提供实时时钟,记录进场和出场的时刻。
- AT24C02:保存累计营收、剩余车位、当前费率等数据,掉电不丢。
- 蜂鸣器/继电器:出场或者满位的时候给出声光提示。
你会发现,每个外设的音量都不大,但合在一起,它们必须被一套清晰的主逻辑串起来。这个主逻辑,就是下一节要讲的“状态机”思路。还有个细节值得提一下:CT107D开发板上数码管和LED经常共用IO口,平时练习的时候你没觉得有什么,真到写代码的时候,动态扫描和LED刷新一旦共用延时,就会出现显示闪烁、亮度不均的问题。这个问题我在第四部分会专门讲,赛场上无数人栽在它手里。
3. 程序框架设计:从“能跑”到“不乱”的状态机思路
3.1 为什么我劝你别用一堆if硬怼业务逻辑
我见过太多备赛的人写代码,main函数里先初始化,然后一个while(1)套三层if——if(按键1按下)、if(按键2按下)、if(系统处于某种状态)。功能单的时候没问题,一旦像停车计费这样既要计时又要算钱又要存数据,就乱了:按键扫描和数码管刷新挤在一起,延时一长,DS1302的时序就错位;业务变量到处可见,一个地方的赋值改了,另一个地方不知道,bug查半天。
这套东西在工程上有个正式的名字叫“状态机”。听起来高大上,其实本质很简单:把系统的运行过程拆成若干个互斥的状态,每个状态只做自己该做的事,事件来了才切换状态。拿停车计费来说,至少有这么几个状态:空闲待机、进场处理、出场处理、计费显示、满位报警。每个状态下,按键按下去的含义不一样:空闲时按“出场”键可能无效,处理中再按“进场”键可能不应该响应。用状态机管理这些约束,逻辑一下子就干净了。
具体到代码,我不主张上多复杂的框架,考场上也没时间写什么操作系统。我通常的做法是定义几个枚举常量表示状态,在while循环里用一个switch把状态分支打开:
typedef enum { STANDBY, VEHICLE_IN, VEHICLE_OUT, BILLING } SysState; SysState currentState = STANDBY; while (1) { KeyScan(); DisplayRefresh(); switch (currentState) { case STANDBY: standbyHandler(); break; case VEHICLE_IN: vehicleInHandler(); break; case VEHICLE_OUT: vehicleOutHandler(); break; case BILLING: billingHandler(); break; default: break; } }这个框架看着简单,但它的价值在于把“按键处理”和“业务逻辑”解耦了。按键只管记录“有没有事件发生”,状态机决定“事件在当前状态下要不要响应、怎么响应”。这样每条代码路径都清晰可查,调试的时候也容易复现问题。
3.2 显示刷新与按键扫描的分时设计
把按键扫描和数码管刷新放进while主循环,是大多数人的第一直觉。但这里有个隐患:单片机是单线程的,数码管动态扫描需要周期性地刷新才能稳定显示,按键需要消抖延时,DS1302读写又有自己的时序。如果它们串在一起,互相等待,整个系统就会表现出“按键按下没反应”“数码管闪烁”这类令人抓狂的现场。
我的做法是给每个模块分配明确的执行节奏。最土但最实用的方案是“标志位+主循环轮询”:用一个定时器产生1ms的中断,在中断里对按键进行扫描、对数码管进行刷新,同时维护一个系统时间戳;主循环只处理业务逻辑,判断时间戳是否到了该干什么的时间点。这样按键消抖、显示刷新这些对时序敏感的工作,被牢牢锁在中断里,业务逻辑再复杂也不会拖垮它们。
这里插一句避坑心得:考试和平时练习不一样,赛场上我见过有人在中断里写DS1302读写,结果因为时序太长,导致主循环里的数据显示卡住。教训就是:中断里只做最轻量的操作——清标志、计数器累加、IO翻转——把DS1302这种带时序的外设读写全部放到主循环里去做。你可以用“时间片轮询”的思路:每隔100ms去读一次DS1302,每隔200ms去把新的数据写入AT24C02,完全够用,而且稳定得多。
3.3 掉电存储与上电恢复的完整闭环
停车计费这种题,AT24C02的存在意义就在于“断电了,账不能乱”。你想,一个停车场要是停个电,累计营收清零了,车位数也乱了,那还怎么营业?所以考试必考一个动作:把关键数据写进EEPROM,然后在系统上电的时候读回来。
写什么数据?通常包括:当前剩余车位数、累计营收金额、费率参数,如果系统要支持“断电解锁”,还得记录一个状态标记位。这里有个关键点:不是任何时候都该写EEPROM。你想想,每次按键都要写一次,AT24C02的擦写寿命虽然一般标称10万次,但考试时反复按、反复写,万一执行到一半断电,数据就毁了。更合理的方案是:业务变化的时候先在内存里改,等某个特定时机(比如出场结算完成、满位状态改变)再落盘。
我在做这道题的时候,习惯把AT24C02的写入封装成一个函数,在“数据发生变化且系统空闲”的时候调用,而不是在按键中断里直接写。上电恢复的代码放到初始化阶段,一开机先把关键参数读出来,再根据这些参数把LED状态、数码管显示、当前状态机所处状态全部恢复。有些同学忘了恢复“当前状态”,导致断电前停车场是满位的,重启之后指示灯全部熄灭,车位数也变成初始值,这不是功能缺失,这是逻辑没闭环。
4. 实测踩坑记录:我在这道题上交过的“学费”
4.1 数码管亮度不均,真相是刷新节奏没对齐
这道题最让我印象深刻的坑,是数码管和LED抢IO。CT107D开发板为了省引脚,把数码管的段选和LED的位选放在同一个IO口上。平时做一个LED流水灯、做一个数码管计数器,大家都觉得理所当然。但停车计费要同时显示很多内容:剩余车位、计费金额、时间,你还得让8个LED保持“某一辆车占用”的状态。搞不好就是:数码管正常显示,LED那边亮度不均匀,甚至几个LED偷偷灭掉。
排查下来,问题出在刷新时序上。数码管动态扫描要求每一位点亮一小段时间再切到下一位,整个周期人眼看起来才是连续显示的。LED如果在你扫描数码管的时候被顺带改写了位选,它就会闪。解决思路很简单:把LED的锁存操作和数码管扫描放在同一个时间片里,或者干脆先把数码管的某一位置显示完,再统一刷新LED状态,不让两者交错。说完这个,后续大家在训练的时候见到“显示闪烁”可以先从锁存器操作入手,别一上来就怀疑硬件坏了。
4.2 按键“按一下变成两下”,消抖和防重触发要一起做
停车计费里按键是个高频操作,进场、出场、切换显示、费率设置全得靠它。但按键在高频使用下有个老毛病:机械抖动。按下一次,电平在几个毫秒内会反复跳变,如果你只读了一次电平就当成一次有效按下,那就会出现“按一次算两次”的现象。你明明想让一辆车进场,结果系统进了两辆,直接满位。
基础的消抖大家都会,延时10ms再读一次。但考场上还有更隐蔽的坑:“重触发”。就算电平稳定了,你的手指还存在一个按住不放的过程。如果代码里没做“释放检测”,那按键一直被按住,主循环每次跑到按键判断都会触发一次业务逻辑,结果就是车辆一辆接一辆进场。我的处理方法是加状态标记:只有检测到“从按下到释放”的完整过程,才记一次有效按键。相当于把边沿触发当成需要同时确认低电平变高电平变化的过程。
4.3 DS1302读写失败,十有八九是时序缝隙没留够
题库里DS1302不是难题,很多人调通一次就再也没管过。但停车计费要求你频繁读取时间做计费运算,这就把DS1302的读写时序问题暴露出来了。尤其是把DS1302的操作放进中断之后,随时可能被打断,读回来的时间就会出现一秒跳两秒、甚至读到负数的情况。
解决思路有两个方向:一是把DS1302的读写全部放到主循环,用时间片保证读操作完整执行;二是读取的时候连读两次,两次结果一致才采用。这个方案虽然土,但在比赛场景下非常实用,稳定性提升巨大。另外,DS1302在掉电之后是靠电池维持走时的,如果你在调试过程中发现时间总是不对,先检查开发板上有没有装电池、跳动开关有没有拨对位置。别笑,考场上有太多人因为电池没装,花了一个小时在调代码。
4.4 AT24C02写入时机不对,累计营收悄悄归零
AT24C02写数据本身不难,难的是“什么时候写、什么时候不写”。我见过一个同学的代码,每次出场结算完直接写EEPROM,测了三次都正常。结果一到测试“断电再上电”的环节,数据就乱套了。查到最后,发现他是每次启动都把默认值写进EEPROM,把之前存好的数据覆盖了。这还不如不写。
这就是典型的“上电恢复逻辑”和“默认参数初始化”顺序搞反了。你应该先读EEPROM,读到有效标志位就用EEPROM里的数据;读不到有效标志位,才说明是第一次使用,再把默认值写进去。我在代码注释里特意写了三行:先读、判断标志、再决定是否初始化。顺序反了,就会像我那个同学一样,每次上电都“重置系统”,数据永远存不住。
5. 从停车计费到整场备赛,最值钱的其实是思维习惯
5.1 一道省赛题的考点清单,比想象中全
把停车计费这道题吃透,你会发现它几乎覆盖了单片机开发的全部基础考点:GPIO控制、动态数码管扫描、按键消抖与状态检测、定时器中断、DS1302实时时钟、AT24C02 I2C存储、状态机业务逻辑、掉电恢复机制。这些名词单独拿出来,每一个都是省赛的常客。所以你要是能把这道题从头到尾独立写出来,省赛拿个二等奖以上的概率就很高了。
我建议备赛的同学不要只盯着“我要把题做完”,而是做完之后做三件事:重新画一遍状态图,把边界条件标出来;把硬件资源分配表写出来,看看有没有更优方案;把代码里所有“魔法数字”改成宏定义或者枚举,提升可读性。这三件事做完,这道题才算真正属于你。
5.2 主观上觉得“差不多”,比赛时直接露馅
备赛的时候有个现象很典型:看例题代码,觉得自己都懂;照着敲了一遍,能编译能下载,就觉得拿下了。等你合上课本自己写,或者我让你换个计费规则、改一下停车位数量,你试试能不能一次跑通。停车计费这道题,网上能找到的源码版本不下十种,但看懂别人的代码和自己在考场上从零写出来,是两码事。
我比较推荐的做法是“限时复现”:找一道往年真题,给自己定4小时,全程不翻参考代码,只允许看芯片手册和开发板原理图,模拟考试状态。第一次复现大概率翻车,没关系,记录下卡住的位置;隔三天再复现一次,这个过程比做十套练习题都管用。考场上拼的就是这种“肌肉记忆”。
5.3 考场上稳住的三条心法
最后分享三条考场经验,这三条是我带了很多届备赛的人之后总结出来的。
第一条:拿到题先做需求拆解和状态图,别急着写代码。磨刀不误砍柴工,有草稿纸上的流程图,你写代码的速度会快一倍,而且不会走到一半迷路。
第二条:每个功能模块写完立刻验证,不要等全部写完再下载试。单片机这玩意儿是“硬件+软件”联合体,堆了太多代码再调试,出了bug都不知道是逻辑错了还是外设时序错了。分段验证,能让你永远知道自己在哪一步出了问题。
第三条:留出至少一小时做“断电测试”。蓝桥杯的评分是功能测试,测试员不会只在你正常操作的情况下打分,他们一定会断电重启、乱按按键、甚至在你没启动的时候直接看显示。把你上电恢复、按键防抖、边界条件全部用“刁难模式”跑一遍,比赛分数就没那么大悬念了。
说到底,停车计费这道题能教给我们的,不只是怎么调一个DS1302或者怎么刷一个数码管,而是一套“从需求到代码再到可靠性”的完整思考方式。这玩意儿,才是比分数本身值钱得多的东西。
本文还有配套的精品资源,点击获取