
简介面向蓝桥杯单片机组省赛参赛者的代码方案包以第十六届省赛第一套题为载体完整呈现环境状态、运动检测、参数设置与统计数据四个功能模块的实现逻辑尤其针对运动状态转换、采集间隔处理和LED/继电器控制的难点给出了可直接借鉴的写法。资源共3个文件以HTML页面、inscode工程入口和gitignore配置为主压缩包仅6KB轻量便于直接对照题目运行与修改。目前已有195人学习下载可作为赛前两周冲刺或赛后复盘的重要参考。通过阅读代码结构、关键函数注释与页面交互流程可快速理解比赛环境的整体搭建思路减少自行摸索的时间成本。1. 认识蓝桥杯省赛的“游戏规则”1.1 省赛到底考什么蓝桥杯单片机省赛全称是“蓝桥杯全国软件和信息技术专业人才大赛”的电子类单片机组省赛。备赛前先搞清楚一件事这个比赛不考炫技考的是“在规定时间内用一块固定开发板完成一套功能组合”。省赛时长通常是4小时题面分客观题和主观题两大部分客观题以选择、填空为主考察51单片机原理、数电模电基础大概占30分主观题就是程序设计加硬件逻辑实现占70分左右。真正拉开差距的是主观题里那一堆功能模块的代码组织能力。比赛指定平台是国信长天开发的竞赛板核心芯片是STC15F2K60S2或IAP15F2K61S2属于增强型51内核。这块板子上集成的外设非常固定LED灯、独立按键、矩阵按键、数码管、DS18B20温度传感器、DS1302时钟芯片、PCF8591ADC/DAC芯片、AT24C02 EEPROM、NE555方波发生器、超声波测距接口、PWM输出等。说白了省赛就是围着这些模块出题今天让你做个温度警报器明天让你做个电子钟后天让你做个电磁炉控制面板功能千变万化但底层外设就这些。很多人备赛有个误区觉得“我学过51单片机会流水灯应该够了”。实际上省赛题目往往要求三四个外设同时工作比如“用DS18B20采集温度数码管显示按键调节上下限超限触发LED和蜂鸣器”。如果你只会点灯现场写代码肯定写不完更别说还要保证稳定性。所以备赛的核心任务就是把每个模块的驱动代码提前写好、调通、注释清楚比赛时直接复用。1.2 板载资源与考点对应关系我备赛时把板载资源整理成了一张对照表按这个表去逐个模块攻克效率很高板载模块涉及芯片/接口省赛常考方式LED灯P0口经74HC573锁存状态指示、流水灯、跑马灯效果数码管P0口段选、P2口位选数字显示、动态刷新、参数跳变显示独立按键P3.0~P3.3模式切换、参数加减、功能启停矩阵按键P1口行列扫描数字输入、复杂菜单选择温度采集DS18B20单总线实时温度采集、上下限判断、温度报警时钟DS1302 SPI电子钟、定时闹钟、时间设置ADC采集PCF8591的AIN通道电压测量、光敏电阻、电位器读取EEPROMAT24C02 I2C接口参数掉电保存、开机恢复上次设置方波测频NE555 定时器计数频率测量、周期计算PWM输出定时器或PCA模块电机调速、LED呼吸灯、加热功率控制这张表透露出的信息是省赛题目从来不孤立的考某一个芯片而是喜欢做“功能组合”。比如做电磁炉控制就得同时用按键输入火力档位、数码管显示功率、定时器产生PWM控制加热、DS18B20做锅底温度采集、EEPROM保存上次火力设置几乎把板载资源考了一大半。所以你的代码工程必须从一开始就按“模块化、可复用”来组织否则后面根本来不及。2. 代码架构比“写对函数”更重要2.1 一个能拿奖的工程结构长什么样很多同学写单片机代码习惯所有东西全堆在main.c里一个几百行的while循环外设驱动函数一个接一个往下排。这种写法在平时做课设勉强能跑但到了蓝桥杯省赛现场基本必翻车。因为题目是临时要你加功能、改参数代码全堆在一起改一处崩三处调试起来极其痛苦。我建议的工程结构是这样的Project/ ├── main.c // 主函数负责初始化和主循环调度 ├── config.h // 全局配置位定义、数据类型、常用宏 ├── delay.c/.h // 软件延时 ├── timer.c/.h // 定时器初始化与中断处理 ├── key.c/.h // 按键扫描与消抖 ├── smg.c/.h // 数码管显示驱动 ├── ds18b20.c/.h // 温度传感器驱动 ├── pcf8591.c/.h // ADC/DAC驱动 ├── at24c02.c/.h // EEPROM驱动 ├── ds1302.c/.h // 时钟芯片驱动 └── pwm.c/.h // PWM输出驱动每个模块的.c文件放函数实现.h文件放函数声明和宏定义main.c只负责流程控制。这样做最大的好处是比赛时你拿到题目先判断用哪几个外设把对应的.c文件加进工程调用函数接口就行不需要重新读一遍几十行的寄存器配置代码。等于你提前把“砖头”烧好了现场只需要“砌墙”。另外有一个细节容易被忽略config.h里把引脚定义统一集中管理。比如数码管位选接在P2口如果某年题目换了个位选方式你只需要改config.h里的宏不用满工程搜索修改。这个习惯在现场调试时能省下大量时间。2.2 按键扫描与数码管刷新的核心写法省赛题目里按键和数码管几乎必考。这两个模块看似简单但很多人写出来的代码存在两个致命问题一是按键消抖写得生硬用delay消抖阻塞了主循环二是数码管刷新放在主循环里用延时切换位选导致刷新不匀亮度闪烁。先说按键。我用的扫描框架是“定时器节拍 状态判断”不用delay消抖避免阻塞。// key.h #ifndef __KEY_H__ #define __KEY_H__ #define KEY_SUM 4 extern unsigned char key_scan(void); #endif// key.c #include key.h #include config.h sbit KEY1 P3^0; sbit KEY2 P3^1; sbit KEY3 P3^2; sbit KEY4 P3^3; static unsigned char key_state[KEY_SUM] {0}; // 状态机存储 static unsigned int key_cnt[KEY_SUM] {0}; // 按下计时 // 在定时器中断里1ms调用一次 // 返回当前有动作的按键号0表示无动作 unsigned char key_scan(void) { unsigned char key_now 0; bit level 0; unsigned char i 0; // 读取全部按键电平 for(i 0; i KEY_SUM; i) { switch(i) { case 0: level KEY1; break; case 1: level KEY2; break; case 2: level KEY3; break; case 3: level KEY4; break; default: break; } switch(key_state[i]) { case 0: // 检测按下 if(level 0) { key_state[i] 1; key_cnt[i] 0; } break; case 1: // 消抖确认 if(level 0) { if(key_cnt[i] 10) // 10ms消抖 { key_state[i] 2; key_now i 1; // 返回按键号 } } else { key_state[i] 0; // 抖动取消 } break; case 2: // 检测释放 if(level 1) { key_state[i] 0; key_cnt[i] 0; } break; } } return key_now; }这套状态机的好处是消抖、松手检测全部非阻塞整个过程不占用主循环的CPU时间。它还有一个隐性优势判断“单击”和“长按”非常方便。在state2阶段可以增加长按计时超时后执行连击逻辑这在菜单调节参数时非常实用——比如持续按住“加”键数值会连续递增而不是按一下加一下。再说数码管。动态显示的核心是“逐位点亮循环刷新”利用人眼视觉暂留效果让所有位看起来同时亮。关键在于刷新要尽量均匀而且不能中断太长时间否则亮度不均。我通常把数码管刷新也放到定时器中断里每2ms刷新一位8位数码管一轮刷新只要16ms刷新率约60Hz肉眼完全看不出闪烁。// smg.c - 数码管显示驱动核心 #include smg.h #include config.h // 共阴数码管段码表: 0~9 A~F unsigned char code smg_duan[] { 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71 }; unsigned char code smg_wei[] {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; static unsigned char dis_buf[8]; // 显示缓冲区 // 设置某一位显示某个字符 void smg_display(unsigned char pos, unsigned char num, bit dot) { if(pos 8) { dis_buf[pos] smg_duan[num % 16]; if(dot) dis_buf[pos] | 0x80; // 点亮小数点 } } // 定时器中断里2ms调用一次刷新一位 void smg_refresh(void) { static unsigned char cur 0; P0 0x00; // 消隐防止位选切换时残影 P2 P2 0x1f; // 关闭位选锁存 P2 P2 | (smg_wei[cur]); // 开通当前位 P0 dis_buf[cur]; // 送段码 P2 P2 0x1f; // 关闭位选锁存 if(cur 8) cur 0; }这里有个细节P0 0x00先消隐再切位选。如果不消隐位选切换瞬间会出现短时间的“拖影”也就是上一个数字的残影残留在当前位上。蓝桥杯裁判评分看的就是细节显示清晰度会直接影响主观分。另外P2口操作采用“先清后置”的方式是因为这款板子P2口高位用于锁存控制低三位才是位选直接赋值容易误触锁存信号。2.3 定时器中断如何安排任务蓝桥杯省赛的程序我个人习惯主循环只干三件事根据标志位调用各模块的任务函数其余时间全部“空转”。真正的时间敏感操作全部交给定时器中断。这样做的好处是任务函数之间没有相互阻塞每个函数都执行得非常短系统响应非常及时。典型的中断调度框架是这样的// timer.c unsigned int sys_1ms 0; // 1ms时基计数器 bit flag_1ms 0; // 1ms标志位 bit flag_10ms 0; // 10ms标志位 bit flag_100ms 0; // 100ms标志位 // 定时器0初始化1ms中断一次 void timer0_init(void) { TMOD 0xF0; TMOD | 0x01; // 定时器0工作在方式116位定时 TH0 (65536 - 12000) / 256; // 12MHz晶振1ms约1000个机器周期 TL0 (65536 - 12000) % 256; ET0 1; // 使能定时器0中断 EA 1; // 开总中断 TR0 1; // 启动定时器0 } void timer0_isr() interrupt 1 { TH0 (65536 - 12000) / 256; TL0 (65536 - 12000) % 256; sys_1ms; flag_1ms 1; if(sys_1ms % 10 0) flag_10ms 1; if(sys_1ms % 100 0) flag_100ms 1; smg_refresh(); // 数码管每2ms刷新这里简单1ms刷新也行 }主循环里这样写while(1) { if(flag_10ms) { flag_10ms 0; key_scan(); // 按键扫描 } if(flag_100ms) { flag_100ms 0; ds18b20_update_temp(); // 温度刷新 } }这里有个计算细节STC15系列单片机在1T模式下指令周期是系统时钟/112MHz晶振下1ms需要12000个时钟周期所以重载值是65536 - 12000。如果你用的是12T模式的老80511ms只需要1000个机器周期重载值就是65536 - 1000。这个参数很多人现场算错导致时间基准全乱我建议备赛时就确认好板子处于哪个模式并写注释标清楚。把按键扫描也放进中断节拍后你会发现主循环非常清爽。多任务“同时运行”的效果就是靠这种时间片轮转实现的——它不是真的并行而是在1ms的时间切片里快速切换任务宏观上看所有任务都在跑。3. 真题场景拆解51单片机实现电磁炉控制3.1 题目需求怎么转换成功能表省赛题目最喜欢拿生活电器做文章电磁炉就是典型。这边用一个电磁炉控制面板的题目来做完整拆解。题目要求大致是通过按键调节火力档位1~9档数码管显示当前档位和功率百分比火力档位通过PWM控制加热丝功率同时用DS18B20检测锅底温度超温自动停止加热并保存档位到EEPROM下次开机自动恢复。拿到这种题目第一步不是写代码而是拆功能。我习惯先在草稿纸上画一张功能表功能模块输入处理输出火力调节按键1、按键-1档位1~9限幅数码管显示档位、百分比加热控制当前档位将档位映射到PWM占空比PWM引脚输出温度检测DS18B20温度转换、超温判断数码管温度显示、蜂鸣器报警掉电保存当前档位写入AT24C02EEPROM存储开机恢复读取EEPROM档位初始化恢复上次火力这样一拆整个程序的结构就清晰了。每个功能对应一个模块写好各自的接口函数然后到main.c里做状态流转。这就是“项目代码”的意义——它不仅仅是代码更是一套拆解问题的方法论。3.2 PWM加热控制与档位映射实现电磁炉加热丝控制的核心是PWM。51单片机没有硬件PWM除了增强型自带PCA模块这里我直接用定时器模拟PWM这也是省赛最通用的做法。思路是这样用定时器0固定产生周期周期内分“高电平时间”和“低电平时间”。比如PWM周期100Hz10ms1档占空比10%高电平1ms5档占空比50%高电平5ms9档占空比90%高电平9ms。// pwm.c sbit HEAT P1^0; // 加热控制引脚 unsigned char pwm_duty 50; // 占空比 0~100 unsigned int pwm_cnt 0; // 当前计时计数 // 在定时器中断中每0.1ms调用一次 void pwm_tick(void) { if(pwm_cnt 100) // 100 * 0.1ms 10ms即100Hz周期 { pwm_cnt 0; } if(pwm_cnt pwm_duty) { HEAT 1; // 高电平输出 } else { HEAT 0; // 低电平输出 } } // 设置占空比 void pwm_set_duty(unsigned char duty) { if(duty 100) duty 100; pwm_duty duty; }然后档位和占空比的映射关系可以这样设计#define FIRE_MAX 9 unsigned char fire_level 5; // 当前档位1~9 // 档位映射到占空比 void fire_update_pwm(void) { unsigned char duty 0; // 1档占空比10%9档占空比90%线性映射 duty 10 (fire_level - 1) * 10; pwm_set_duty(duty); }这样2档就是20%占空比6档是60%逻辑很直观。这种映射方式在现场改参数非常方便评委要求“3档时占空比30%”你只需要改公式里的基准值。3.3 状态机架构串起整个程序电磁炉控制不能只靠顺序执行因为多个按键操作、温度报警、开关切换可能同时发生。这时候就需要一个简单的状态机来管理“开机/待机/加热中/超温保护”这些状态。// main.c 状态机核心 typedef enum { SYS_OFF, // 关机状态 SYS_STANDBY, // 待机状态 SYS_HEATING, // 加热状态 SYS_OVERTEMP, // 超温保护状态 } sys_state_t; sys_state_t sys_state SYS_OFF; // 主循环任务调度 void sys_task(void) { unsigned char key key_scan(); switch(sys_state) { case SYS_OFF: HEAT 0; if(key 4) // 按“开/关”键进入待机 { sys_state SYS_STANDBY; // 从EEPROM读回上次档位 fire_level at24c02_read_byte(0x00); if(fire_level 1 || fire_level 9) fire_level 5; fire_update_pwm(); } break; case SYS_STANDBY: if(key 1 fire_level FIRE_MAX) { fire_level; fire_update_pwm(); at24c02_write_byte(0x00, fire_level); // 掉电保存 } if(key 2 fire_level 1) { fire_level--; fire_update_pwm(); at24c02_write_byte(0x00, fire_level); } if(key 4) { sys_state SYS_OFF; } if(key 3) { sys_state SYS_HEATING; } break; case SYS_HEATING: HEAT 1; // PWM由pwm_tick驱动 // 超温检测 if(temp_current 300) // 300代表100.0℃按实际定标 { sys_state SYS_OVERTEMP; HEAT 0; buzzer_on(); } if(key 4) { sys_state SYS_STANDBY; HEAT 0; } break; case SYS_OVERTEMP: // 温度降到安全值以下才恢复 if(temp_current 250) { sys_state SYS_HEATING; buzzer_off(); } break; } }这个状态机的价值在于每个状态只响应特定按键和条件状态切换路径清晰逻辑不容易乱。即便是省赛最后半小时加需求比如“待机状态下长按3键进入定时模式”你也只需要在SYS_STANDBY里加一个分支不会影响其他状态。还有一个细节EEPROM写入AT24C02需要时间约5ms如果每次按键都立刻写会阻塞主循环而且频繁写EEPROM会缩短寿命。实际工程中我通常只做到“关机时保存一次”或者“档位变化后启动1秒定时到时才写”。但这属于优化项如果现场没时间至少在档位变化时写一次保证掉电不丢档位即可。4. 客观题与调试避坑经验4.1 客观题怎么准备更高效省赛客观题考察的面比较杂但从多年真题来看反复出现的知识点很集中51单片机存储器结构片内RAM分区、特殊功能寄存器SFR分布、中断系统中断源、优先级、中断入口地址、定时器/计数器工作方式、初值计算、重装值、串口通信波特率计算、SBUF操作、I/O口结构准双向口、上拉电阻、读引脚与读锁存器的区别、AD/DA转换基本原理、复位电路、看门狗等。这块我个人的备考方式是不背零散知识点而是结合代码来记。比如问到“定时器方式1的最大定时时间是多少”如果你写过定时器代码就知道16位定时器最大计数65536个机器周期在12MHz、12T模式下最大定时约65.536ms。这个结论不是背出来的是根据配置代码算出来的记得扎实得多。还有一个高频考点是“上电复位后各寄存器的初始值”。备考时我建议把常用寄存器P0~P3、TMOD、TCON、SCON、IE、IP等的复位值整理成一张表考前过一遍。比如P0~P3复位后都是0xFF即全部引脚输出高电平TCON复位后是0x00这样在程序初始化时你就知道哪些位需要手动清零哪些本来就是默认值。4.2 Keil调试与现场常见故障排查现场调试最大的痛点就是“现象不对但不知道从哪里查”。代码写完了下载进板子结果数码管不亮、按键没反应、温度显示乱跳。我把自己踩过的坑和排查思路整理成了表格对应的都是高频问题故障现象可能原因排查思路数码管全灭位选锁存没打开检查P2口位选锁存操作用万用表量位选引脚电平数码管显示乱码段码表错误或共阴共阳选错确认段码表是否匹配板子数码管类型按键按了没反应引脚配置成准双向口问题检查是否误设为高阻输入模式加上拉电阻按键触发两次没有消抖或消抖时间太短增大消抖计时到20ms左右温度显示85DS18B20未初始化成功检查单总线时序延时是否足够EEPROM数据读回全是0xFFI2C时序问题检查起始/停止信号、应答位判断PWM加热不工作定时器中断里没调用pwm_tick确认tick函数挂到了中断服务程序里Keil调试时我强烈建议学会用Peripherals窗口观察硬件状态。在调试模式下菜单栏Peripherals里可以打开Port0~Port3、Timer0/1、Interrupt、Serial等窗口实时观察引脚电平、定时器计数值、中断标志位。比如你怀疑某个引脚没有输出直接在Port窗口看对应位的勾选状态比拿示波器点板子快得多。仿真时如果发现引脚完全不变大概率是程序根本没跑到这一行配合断点检查执行流程。另外还有一个大家经常忽略的工具Keil的逻辑分析仪窗口。虽然它不能直接测外部信号但可以观察程序中任意变量的波形变化。比如你用软件延时产生PWM逻辑分析仪窗口能看到PWM波形的占空比和周期是否符合预期不需要接实际硬件。这个功能对验证PWM输出、按键时长这类时间相关的逻辑非常实用。4.3 现场编程的“保分策略”省赛是限时编程遇到没把握的功能一定先保底再做优化。我的策略是先按“能用、稳定”的标准把基本功能全做出来再回头优化显示效果和交互逻辑。比如数码管小数点亮不亮、闪烁效果这些是加分项但前提是基本功能不会因为改一个显示函数而崩掉。在修改代码前先把原始版本另存一份备份或者用Keil的版本管理功能保留历史版本。现场最怕的不是写不出代码而是改来改去改出一个更烂的版本最后时间不够没法回退。所以每次改动前我会在main函数入口加一个version注释——大改动就复制整个工程文件夹小改动就加注释标记防止现场改上头。5. 最后的备赛建议手头有往届真题的话不要急着从头到尾做先把每套题的功能模块拆出来标出重复出现的高频模块。我统计下来数码管按键定时器几乎是每套必考DS18B20和EEPROM出现频率也很高这两块的驱动程序务必背到能默写的程度。真正开始刷真题时控制在3.5小时内完成整套代码留出半小时检查和比赛节奏保持一致。再分享一个小技巧准备一个“代码速查本”把每个模块的初始化和核心函数用法写成简短摘要。比如DS18B20读取温度需要几个步骤、AT24C02写一个字节的时序流程考前翻一翻进考场心里就踏实很多。这些摘要要用自己的话写考试时看到类似的配置能快速回忆起来比临时翻资料快得多。从我个人参赛的经验来看蓝桥杯省赛拿奖的关键不是天赋而是对板载资源的熟练度和工程组织能力。把每个模块的驱动代码提前整理好、调通、注释清楚现场再把它们像积木一样拼起来这个流程走顺了哪怕遇到没见过的题目组合也能稳住心态按部就班地完成。本文还有配套的精品资源点击获取