简介:面向蓝桥杯单片机设计与开发赛项的备赛者,这份资料汇总了2023年赛方下发的官方资料包,并整理有多届真题及部分参考代码,适合备赛初期熟悉赛题考点,也适合后期对照工程文件排查代码问题。压缩包内共537个文件,既有.c、.h源码与.uvproj等Keil工程文件,也有.hex、.m51等编译输出,还包含.pdf真题文档与.png截图,可清晰还原各届赛题的程序框架与调试过程。整体容量144.54MB,文件按模块区分,便于按需检索。目前已有1658人学习下载,对希望快速上手蓝桥杯单片机赛项、获得完整赛题样例的同学有较高参考价值。 2023年第十四届蓝桥杯单片机比赛的核心资料和备赛思路,我整理了一份比较完整的东西。这届比赛用的主控芯片是STC15F2K60S2(IAP15F2K61S2),开发板是国信长天的CT107D,软件环境是Keil C51加STC-ISP下载。整个备赛和参赛过程踩了一些坑、也总结了一些通用性比较强的方法,这篇文章把硬件外设、代码工程结构、模块写法、常见错误和国赛客观题方向都讲清楚,希望能帮到准备打这场比赛的人。
1. 2023蓝桥杯比赛的基本盘:硬件和备赛环境
1.1 主控芯片与开发板的等价关系
先说硬件。蓝桥杯单片机赛项多年来一直围绕STC15系列做文章,2023年省赛和国赛的开发平台是CT107D,板载芯片是IAP15F2K61S2(也就是STC15F2K60S2的IAP版本,程序存储和运行机制更灵活)。这颗芯片属于STC15系列1T增强型8051内核,指令周期比传统12T的STC89C52快很多,内部集成高精度R/C时钟,默认运行频率通常是11.0592MHz,比赛题目也基本默认在这个频率下出题。
开发板上的外设资源比较固定:8个LED(低电平点亮)、8位数码管(两片74HC573锁存器分别控制段选和位选)、独立按键和矩阵按键、NE555方波发生器(用于测频)、AT24C02(EEPROM)、DS18B20(温度传感器)、PCF8591(ADC/DAC,模拟电压采集)、DS1302(实时时钟)、超声波接口、串口和蜂鸣器/继电器(通过ULN2003驱动)。
这些外设几乎每年都会考,只是组合方式不同,所以备赛核心不是“学完所有外设”,而是把所有外设的驱动代码写成可复用的模板,考试时直接调。
1.2 软件工具链的搭配细节
常用工具是Keil C51(版本建议V9.60以上,Win10/Win11下兼容性更好)和STC-ISP烧录软件。有几个细节值得注意:
- 创建工程时芯片选型:在Keil中选“STC15F2K60S2”或“IAP15F2K61S2”均可,如果没有该型号,可以选“STC15系列”通用型号,或者用STC-ISP软件中自带的“Keil仿真设置”功能添加芯片型号到Keil。
- 烧录频率与串口选择:默认使用11.0592MHz,STC-ISP中“输入用户程序运行时的IRC频率”一定要选11.0592MHz,否则波特率和延时函数全偏。
- 头文件:代码中常用
<STC15F2K60S2.H>或<STC15.H>,寄存器定义基本能满足比赛需求,不需要自己写复杂的SFR映射。
提示:驱动NE555测频时,需要将跳线帽接好,使定时器0的计数输入与P34引脚相连。这属于硬件层配置,程序写对了但跳线没接,频率永远是0。
2. 外设驱动的写法与硬件抽象思路
2.1 锁存器控制:LED与数码管的核心逻辑
CT107D开发板上,74HC573锁存器的控制逻辑是必须背下来的:
- Y4C(P2^7置1)锁存段码
- Y5C(P2^6置1)锁存位码
- Y6C(P2^5置1)锁存键盘/蜂鸣器控制
实操中必须先打开锁存、送数据、再关闭锁存,顺序不能错。比如点亮第一个LED:
sbit LATCH_SEG = P2^7; // 段选锁存 sbit LATCH_BIT = P2^6; // 位选锁存 void SelectHC573(unsigned char channel) { P2 = (P2 & 0x1F) | (channel << 5); } void DisplayLED() { SelectHC573(4); // Y4C选中,段选锁存通道 P0 = 0xFE; // 低电平点亮LED1 SelectHC573(0); // 关闭锁存 }这里的P2 = (P2 & 0x1F) | (channel << 5)是把P2^7、P2^6、P2^5三位用于控制Y4/Y5/Y6,其余位必须清零,否则会误触发其它锁存器,这是新手最容易出错的地方。
2.2 数码管动态扫描的刷新时机
数码管必须动态扫描,利用视觉暂留效应逐位点亮,刷新频率建议1ms~2ms切换一位,8位数码管完整一轮刷新约8~16ms,肉眼看起来就是全部常亮的。
数码管驱动有两个细节容易翻车:
- 段码表必须和板子丝印对应:共阳数码管段码表一般是
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90,对应0~9。 - 消隐处理:切换位选前先将段码置为0xFF(全部灭),再切换位选,最后再送段码,这样不会出现拖影。
一个稳定的数码管扫描函数大概长这样:
void Display_Scan() { static unsigned char index = 0; P0 = 0xFF; // 消隐 SelectHC573(4); // 打开段选锁存 P0 = 0xFF; // 送全灭 SelectHC573(0); // 关闭 SelectHC573(5); // 打开位选锁存 P0 = (1 << index); // 选中第index位 SelectHC573(0); // 关闭 SelectHC573(4); // 打开段选 P0 = code_tab[disp_buf[index]]; // 显示数据 SelectHC573(0); // 关闭 index = (index + 1) % 8; }建议在定时器中断中调用这个扫描函数,主循环只改显存数组,避免主循环被数码管刷新占用大量CPU时间。
2.3 按键处理的防抖与短按长按逻辑
独立按键和矩阵按键的区别在于IO电平的读取方式。矩阵键盘4x4扫描思路很简单:行线输出低电平,读列线电平判断按键位置。但有两个常见坑:
- 是否需要上拉电阻:STC15系列单片机P0口内部没有上拉,而P2、P3、P4口上电后为准双向口(内部有弱上拉)。比赛板上的独立按键J5跳线默认接“按键”模式,P3^2/P3^3是外部中断0/1输入,也可以用普通IO读取。矩阵键盘的P4^4~P4^7做行线或列线,一般无需外加上拉。
- 去抖时机:不要只用
delay(10)去抖,更可靠的是基于定时器扫描的去抖方式——每隔5~10ms读一次按键状态,连续两次电平一致才确认按下。
实战中我会维护一个按键状态表:
#define KEY_DEBOUNCE_MS 8 void Key_Scan() { if (timer_cnt % KEY_DEBOUNCE_MS == 0) { unsigned char current = Read_Key_Matrix(); static unsigned char last = 0xFF; unsigned char changed = current ^ last; last = current; for (int i = 0; i < 16; i++) { if ((changed & (1 << i)) && ((current & (1 << i)) == 0)) { key_event[i] = 1; // 有效按下事件 } } } }这种事件的触发机制,比在主循环里反复判断if(KEY==0)更快、更可靠,尤其是比赛要求同时响应多个按键或短按长按区分时,事件标志位方案几乎是最省心的。
2.4 定时器与中断:1ms时基怎么给
STC15F2K60S2的定时器0/1/2配置和传统8051有些差异,但比赛常用定时器0做1ms时基中断,定时器1做波特率发生器,定时器2做PWM或额外定时。
配置定时器0为16位重装载模式,11.0592MHz主频下初值计算方式是:
- 定时器0每个机器周期=1T,也就是
1/11.0592MHz ≈ 90.42ns - 若要1ms中断一次,需要计数
1ms / 90.42ns ≈ 11059次 - 16位定时器最大65536,所以初值 = 65536 - 11059 = 54477,即0xD4CD
代码如下:
void Timer0_Init(void) { AUXR |= 0x80; // 定时器0工作在1T模式 TMOD &= 0xF0; TMOD |= 0x01; // 16位不自动重装(实际比赛常用自动重装,用TL0/TH0在主程序中重新赋值或使能定时器的8位自动重装) TL0 = 0xCD; TH0 = 0xD4; ET0 = 1; TR0 = 1; EA = 1; }注意:STC15系列定时器0的1T模式由AUXR寄存器控制,很多传统8051教程不会提这个寄存器,容易导致初值算对但实际延时不正常。还有一点,如果使用12T模式,要AUXR &= ~0x80,初值相应要除以12。
2.5 串口:需要背的波特率初始化
比赛考串口通信的概率很高,而且经常和PC通信、字符串处理、指令解析结合。11.0592MHz下用定时器1做波特率发生器,9600波特率的配置是经典值:
void UART_Init(void) { SCON = 0x50; // 8位可变波特率,允许接收 AUXR |= 0x01; // 定时器1在1T模式 TMOD &= 0x0F; TMOD |= 0x20; // 定时器1工作在模式2(8位自动重装) TH1 = 0xFD; // 9600波特率 @11.0592MHz,且定时器1为1T模式时这个初值通常为0xFD TL1 = 0xFD; TR1 = 1; ES = 1; EA = 1; }如果串口收到的数据乱码,优先检查两点:一是STC-ISP中IRC频率是不是11.0592MHz;二是定时器1是否工作在1T模式。很多人在串口配置上翻车都是因为AUXR没置位,实际波特率变成了理论值的一半。
串口接收建议用中断方式,将接收到的字节存到缓冲区,主循环解析。解析协议时尽量用状态机,不要在主循环里用阻塞等待while(!RI),因为比赛题目通常要求串口收发和数码管显示同时进行。
3. 工程结构:从“能亮灯”到“不慌不忙”
3.1 均匀分层的模块化工程
很多同学比赛时代码全堆在一个main.c里,功能少还好,题目做完一半就变成意大利面。我建议从备赛第一天就建好分层结构,把硬件驱动和业务逻辑分离。
我习惯的工程目录大概是:
Project/ ├── main.c ├── config.h ├── delay.c / delay.h ├── timer.c / timer.h ├── uart.c / uart.h ├── key.c / key.h ├── display.c / display.h ├── ds18b20.c / ds18b20.h ├── at24c02.c / at24c02.h ├── pcf8591.c / pcf8591.h ├── ds1302.c / ds1302.h └── ne555.c / ne555.h每个模块只暴露必要的接口,比如:
// display.h void Display_Init(void); void Display_SetNum(unsigned char pos, unsigned char num); void Display_Scan(void);main.c里只写业务逻辑:
void main() { System_Init(); while(1) { Key_Process(); Data_Process(); UART_Process(); } }这样做的好处很直接:比赛题目无论怎么组合,main.c只需要改业务逻辑,底层驱动几乎不动。
3.2 调度框架:前后台与时间片轮询
比赛时用实时的前后台结构就够了,不用上RTOS。前台是定时器0中断(负责数码管刷新、按键扫描、计时更新等),后台是主循环(负责业务逻辑、串口解析、数据显示更新)。
我会在timer.c里维护一个全局时间基准:
volatile unsigned int timer_cnt = 0; // 毫秒计数器 void Timer0_Isr() interrupt 1 { timer_cnt++; Display_Scan(); // 1ms刷新一位数码管 Key_Scan(); // 8ms扫描一次按键 }主循环里用判断时间差实现非阻塞延时:
static unsigned int last_time = 0; if (timer_cnt - last_time >= 500) { last_time = timer_cnt; // 每500ms做一次温度采集 }这种写法比起delay(500)更稳,因为主循环在等待期间不会错过按键、串口和显示刷新。比赛时多任务并行是常态,非阻塞延时几乎是必备技能。
3.3 从省赛到国赛:驱动模板的价值
省赛题目通常只要“实现LED流水灯、数码管显示、按键调参、温度采集”这类基础组合。国赛才会加入更多模块联动、数据存储、高低优先级切换等设计。但驱动模板完全通用,例如DS18B20和AT24C02在省赛、国赛出现概率都很高,差别仅在逻辑层如何处理数据。
真正拉开差距的是你对模块数据流的组织。比如温度显示带小数,要处理整数部分和小数部分;按键切换参数时要保存EEPROM掉电不丢失;串口指令改变参数时要更新显存数组。这些操作和硬件本身关系不大,但代码质量直接影响调试效率。
3.4 一个可以直接抄的模板结构
我整理过一个比较成熟的模板,核心思想是把初始化、中断、主循环三件事完全分离:
// main.c #include "config.h" void System_Init() { P0M0 = 0x00; P0M1 = 0x00; P2M0 = 0x00; P2M1 = 0x00; P3M0 = 0x00; P3M1 = 0x00; P4M0 = 0x00; P4M1 = 0x00; Delay_Init(); Timer0_Init(); UART_Init(); Display_Init(); } void main() { System_Init(); while(1) { Key_Process(); Display_Process(); UART_Process(); System_Process(); } }这个模板的好处在于,考试时你先写好System_Init和几个空壳Process函数,后面往壳子里填代码即可,不慌。
4. 比赛重难点:温度、EEPROM、频率测量与客观题
4.1 DS18B20的时序和代码坑
DS18B20是单总线器件,时序要求严格,代码写不好直接读不到温度。开发和比赛板上一般用P1^0或P1^1接DS18B20(CT107D上是P1^1),需要通过跳线帽短接才能使用。
关键坑有三个:
- 初始化时序:主机拉低480us以上,释放后等待15~60us,DS18B20会拉低60~240us作为存在脉冲。如果读不到存在脉冲,原因大概率是上拉电阻问题或者引脚模式不对。STC15系列读单总线前要把引脚设为高阻输入(
P1M1 |= 0x02; P1M0 &= ~0x02;),否则内部弱上拉可能影响总线电平。 - 读时序:主机拉低1us后释放,然后在15us内读取电平。读取时要关闭中断(
EA=0; ... EA=1;),否则中断抖动会让位读取错乱。 - ROM匹配:比赛一般只有一个DS18B20,可以写跳过ROM(0xCC)再发转换命令(0x44),读取温度发0xCC、0xBE。如果板上挂过多器件才需要考虑ROM匹配。
温度转换需要时间,12位精度下约750ms,所以主循环读取温度时轮询DQ == 1表示转换完成,或者干脆固定延迟800ms再读。
4.2 AT24C02的IIC读写与掉电存储
AT24C02是IIC接口EEPROM,2Kbit(256字节),比赛经常用来存设定参数,比如温度上限、密码等。IIC时序相对简单,但要注意STC15的P2^0(SCL)和P2^1(SDA)在CT107D板上接的是PCF8591和AT24C02的共用IIC总线,通过J9跳线选择。程序写法要兼容同一总线上两个IIC器件。
一个稳定的IIC通信要注意:
- 启动/停止条件:SCL高电平时SDA下降沿为启动,SCL高电平时SDA上升沿为停止。
- ACK响应:主机发送完一个字节后,第9个时钟周期读SDA,低电平表示从机应答。若收到NACK,说明器件地址错误或总线冲突。
- 写一个字节后要等待:AT24C02页写周期约5ms,写完需要给内部擦写时间,否则下一个操作会失败。
比赛常用需求就是开机读EEPROM,显示上次设定的参数;按键调整参数后写入EEPROM。写代码时注意别把参数地址和器件地址搞混,AT24C02器件地址一般是0xA0(写)/0xA1(读),PCF8591的器件地址是0x90(写)/0x91(读)。
4.3 NE555测频率的两种方案
NE555在比赛板上的接法是通过P34引脚输出方波,频率由电位器调节。测频有两条路线:
- 方案一:定时器0计数,定时器1定时。定时器0配置为计数器模式,下降沿计数;定时器1产生1秒或100ms的中断,在中断中读取定时器0计数值并清零,即可得到频率。1秒定时下计数值就是频率值(Hz)。
- 方案二:定时器0测周法。对高频信号测周、对低频信号测频,比赛一般用方案一。
STC15系列定时器0在计数器模式下,计数输入引脚是P34(T0),和NE555输出对应。需要设置TMOD的C/T bit,还要注意P34引脚需要设为高阻输入且不能有上拉,否则计数可能异常。
void Timer0_Counter_Init() { TMOD &= 0xF0; TMOD |= 0x05; // 定时器0工作在16位计数器模式 TH0 = 0x00; TL0 = 0x00; TR0 = 1; // 启动计数 // 定时器1负责1秒定时的初始化略 }在定时器1的1秒中断里读取TH0<<8 | TL0,然后清零重新计数即可。如果读数偏低,检查NE555跳线帽和输出引脚是否正确。
4.4 国赛客观题考察点
国赛和部分省赛有客观题环节,考察范围集中在数电、模电、C语言、单片机基础、电路分析。常见的考点包括:
- 数制转换与补码/原码、BCD码
- 逻辑门电路(与非门、或非门、异或门真值表)
- 三极管三种工作状态(截止、放大、饱和)以及基本放大电路计算
- 运放电路(同相、反相、差分、比较器、虚短虚断)
- C语言指针、结构体、位运算、函数指针
- 8051中断系统(优先级、中断入口地址、外部中断触发方式)
- 定时器/计数器工作方式
- 串口通信基础知识(波特率、帧格式、奇偶校验)
准备客观题不用刷太多题,把蓝桥杯历年的客观题过一遍,再把《单片机原理及应用》教材中关于中断、定时器、串口的课后题做一遍,基本就够。真正容易失分的反而是进制转换速度和逻辑代数化简熟练度,这些在30秒内就要出答案,平时要多练。
5. 备赛节奏与临场心态
5.1 备赛时间安排
如果从零开始备赛,我建议的时间线是:
- 第1~2周:熟悉开发板和Keil环境,点亮LED、驱动数码管、按键扫描,掌握锁存器控制。
- 第3~4周:掌握定时器、中断、串口通信,理解1ms时基和非阻塞延时思想。
- 第5~6周:逐个搞定DS18B20、AT24C02、PCF8591、DS1302、NE555这几个模块,每个模块写一个最小demo并跑通。
- 第7周:刷近五年省赛真题,每道题从读题到实现控制在3小时内。
- 第8~9周:刷国赛真题,重点练综合题和客观题。
- 第10周:模拟考试环境,完整做一套题,练习读题、排错和代码组织。
比赛时间是省赛5小时、国赛5小时(具体以官方通知为准),很多人不是不会写,而是把大量时间浪费在调试BUG上。平时练习就按“写代码—烧录—调BUG”的闭环来训练,提高排错速度。
5.2 考场上的代码组织技巧
读题阶段就把需求拆解成功能点,每完成一个功能就烧录验证一次,不要等全部写完再烧录,否则BUG叠加会让你崩溃。
调试时优先确认模块是否工作,再调逻辑。比如温度不对,先在数码管上显示原始温度原始数据,判断是否“0X8501”这类负温度特征,再决定是时序问题还是转换命令问题。显示数据不对就先单独测试数码管扫描,排除显示层错误。
编码规范也很重要。变量命名建议用功能名+类型前缀,比如temp_int、disp_buf[]、key_event[]。考试中途如果现场改需求(估题和真题有时会有微调),清晰的命名能让你快速定位并修改。
5.3 几个考场常见的“翻车”点
汇总一下我见过的、也是我自己踩过的几个高频问题:
- P2口锁存器误操作:选择锁存器时直接用
P2 = 0x80,把P2^7之外的位全改了,导致ADC采样或其它外设异常。建议用P2 = (P2 & 0x1F) | (channel << 5)这种清位再置位的写法。 - 数码管刷新卡主循环:没有把扫描放进定时器中断,主循环收到串口数据时数码管闪断严重。
- 串口和定时器争抢中断优先级:串口接收和数码管扫描都在中断里处理,串口优先级设置不对会导致丢字节,合理调整
IP寄存器。 - DS18B20读温度时关中断关太久:DS18B20位时序要求15us内读完,但如果关闭中断时间过长,定时器时基会偏差,累计后数码管闪烁。正确做法是只在读位那短暂关闭中断,其余时间保持中断开启。
- 矩阵按键无上拉造成误触发:STC15的P4口是准双向口,按矩阵键盘时读到的电平可能不稳,排查时可设置
P4M1/P4M0为准双向或加强上拉。
6. 一个相对完整的最小工程示例
下面给一个精简版的模板代码,包含定时器0(1ms)、数码管扫描、独立按键读取三个核心模块,可以作为比赛框架的起点。
// config.h #ifndef __CONFIG_H__ #define __CONFIG_H__ #include <STC15F2K60S2.H> #include <intrins.h> #define FOSC 11059200UL void System_Init(void); void Timer0_Init(void); void UART_Init(void); #endif // timer.c #include "config.h" volatile unsigned int timer_cnt = 0; void Timer0_Init(void) { AUXR |= 0x80; // 1T模式 TMOD &= 0xF0; TMOD |= 0x01; // 16位 TL0 = 0xCD; // 1ms @11.0592MHz TH0 = 0xD4; ET0 = 1; TR0 = 1; EA = 1; } void Timer0_Isr(void) interrupt 1 { TL0 = 0xCD; TH0 = 0xD4; timer_cnt++; Display_Scan(); } // display.c #include "config.h" unsigned char code_tab[] = {0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90}; unsigned char disp_buf[8] = {0,1,2,3,4,5,6,7}; void SelectHC573(unsigned char channel) { P2 = (P2 & 0x1F) | (channel << 5); } void Display_Scan() { static unsigned char index = 0; P0 = 0xFF; SelectHC573(4); P0 = 0xFF; SelectHC573(0); SelectHC573(5); P0 = (1 << index); SelectHC573(0); SelectHC573(4); P0 = code_tab[disp_buf[index]]; SelectHC573(0); index = (index + 1) % 8; } // main.c #include "config.h" void main() { System_Init(); while(1) { } }这段代码的核心是“显示扫描靠中断,主循环放逻辑”。你可以在主循环里处理按键、计算数据、更新disp_buf,数码管刷新完全不用你操心。
最后再说说资料的利用方式。网上能搜到很多往届真题、模块代码和经验帖,但别做“收藏党”,每一份资料拿回来后要自己动手敲一遍。备赛资料的意义不是看,而是变成你手上的肌肉记忆。2023年的赛题在开发板和编译器环境上和前几年差异不大,打好基本功,比追任何押题都靠谱。祝大家比赛顺利。
本文还有配套的精品资源,点击获取