第十四届蓝桥杯单片机省赛(本科组)题解与代码精讲 一、 整体架构与模块化设计head.h全局头文件包含宏定义、结构体/联合体定义以及各模块的函数声明。smg.c数码管动态扫描与显示缓存管理。arrkeys.c矩阵键盘扫描与按键状态机支持短按与长按检测。led.c/ds18b20.c/pcf8591.c/ne555.c各外设的底层驱动。main.c核心业务逻辑、定时器中断调度与界面状态机。typedef struct { unsigned char b0 :1; unsigned char b1 :1; unsigned char b2 :1; unsigned char b3 :1; unsigned char b4 :1; unsigned char b5 :1; unsigned char b6 :1; unsigned char b7 :1; }bits; typedef union { bits b; unsigned char hex; }hextobin; hextobin led,buzz;在传统的 51 单片机开发中控制 LED 灯通常需要繁琐的位运算。例如要单独点亮 L1P0.0我们可能会写P0 P0 0xFE;或者P0 ~0x01;。这种方式不仅代码可读性差而且在控制多个 LED 组合状态时极易因为位运算写错而导致显示异常。为了解决这个痛点在head.h中定义了一个名为hextobin的联合体union。联合体的妙处在于它允许同一块内存以不同的数据类型来访问。在这里我将一个unsigned char8位字节和一个包含 8 个位域b0到b7的结构体映射到同一地址。这样一来控制 LED 就变得极其直观想点亮 L1直接写led.b.b0 0;蓝桥杯板子 LED 为低电平点亮想熄灭 L2直接写led.b.b1 1;。最后只需将led.byte整体赋值给 P0 端口即可。这种写法不仅大大降低了出错率还让代码的意图一目了然是比赛中非常实用的提效技巧。二、 底层驱动模块解析1. 数码管显示模块 (smg.c)数码管采用动态扫描的方式。为了避免主循环阻塞导致数码管闪烁将smg_loop()放入了定时器中断中。显示缓存使用了一个全局数组smg_word[8]主程序只需要调用smg_set(pos, word)修改缓存中断函数会自动刷新。#include STC15F2K60S2.H code unsigned char Seg_Table[] { 0xc0, //0 0xf9, //1 0xa4, //2 0xb0, //3 0x99, //4 0x92, //5 0x82, //6 0xf8, //7 0x80, //8 0x90, //9 0x88, //A 10 0x83, //b 11 0xc6, //C 12 0xa1, //d 13 0x86, //E 14 0x8e, //F 15 0xBF, //- 16 0xFF, //none 17 0x8C, //P 18 0x89,//H 19 1000,1001 0x40,0x79,0x24,0x30,0x19,0x12,0x02,0x78,0x00,0x10,0x08,0x03,0x46,0x21,0x06,0x0e }; unsigned char smg_word[8]{17,17,17,17,17,17,17}; void P2andP00(unsigned char p2,p0) { P0p0; P2P20x1F; P2p2|P2; P2P20x1F; } void smg_set(unsigned char pos,word) { smg_word[pos]word; } void smg_loop() { static unsigned char i0; P2andP00(0xc0,0x01i); P2andP00(0xe0,Seg_Table[smg_word[i]]); smg_word[i]17; i; if(i8)i0; }蓝桥杯单片机的数码管显示依赖于两个 74HC573 锁存器分别控制段选和位选而这两个锁存器的使能端又由 74HC13838译码器来控制。理解 P2 口高 3 位与 38 译码器的映射关系是写好数码管驱动的前提。在代码的P2andP00函数中我们通过操作 P2 端口的高 3 位P2.5, P2.6, P2.7来选择通道当 P2 高 3 位为110即0xC0的高 3 位时38 译码器的 Y4 通道输出低电平打开位选锁存器。当 P2 高 3 位为111即0xE0的高 3 位时Y5 通道输出低电平打开段选锁存器。代码中P2 (P2 0x1F) | channel;这行非常关键P2 0x1F用于清除 P2 口的高 3 位保留低 5 位状态然后通过| channel将目标通道值如0xC0或0xE0写入高 3 位。这样既完成了通道切换又不会影响 P2 口低 5 位可能连接的其他外设如单总线或 I2C保证了系统的稳定性。2. 矩阵键盘与长按检测 (arrkeys.c)题目要求 S9 长按 2 秒清除数据且按键响应时间 ≤0.2s。传统的delay延时消抖会严重阻塞系统因此采用状态机轮询的方式。#include STC15F2K60S2.H unsigned key_read()//¶ÀÁ¢°´¼ü { unsigned char key0; P440;P421;P351;P341; if(P300)key7; if(P310)key6; if(P320)key5; if(P330)key4; P340;P351;P421;P441; if(P330)key16; if(P320)key17; if(P310)key18; if(P300)key19; P341;P350;P421;P441; if(P330)key12; if(P320)key13; if(P310)key14; if(P300)key15; P341;P351;P420;P441; if(P330)key8; if(P320)key9; if(P310)key10; if(P300)key11; return key; } unsigned char key_loop() { static unsigned char nowstate0,laststate0; unsigned char keynum0; laststatenowstate; nowstatekey_read(); if(laststate0nowstate7)keynum7; if(laststate0nowstate6)keynum6; if(laststate0nowstate5)keynum5; if(laststate0nowstate4)keynum4; if(laststate0nowstate11)keynum11; if(laststate0nowstate10)keynum10; if(laststate0nowstate9)keynum9; if(laststate0nowstate8)keynum8; if(laststate0nowstate15)keynum15; if(laststate0nowstate14)keynum14; if(laststate0nowstate13)keynum13; if(laststate0nowstate12)keynum12; if(laststate0nowstate19)keynum19; if(laststate0nowstate18)keynum18; if(laststate0nowstate17)keynum17; if(laststate0nowstate16)keynum16; if(laststate9nowstate9)keynum90; return keynum; } unsigned char key_get() { unsigned char temp0; tempkey_loop(); return temp; }在key_loop函数中定义了laststate上一次状态和nowstate当前状态。短按检测当laststate 0无按键且nowstate 某键值时说明按键刚刚被按下。此时我们记录键值并返回实现“按下瞬间立即触发”保证了 ≤0.2s 的响应要求。长按检测当laststate 9且nowstate 9时说明 S9 键正处于持续按下的状态。此时函数返回一个特殊值90。主循环接收到90后就可以启动一个计数器进行长按计时。这种非阻塞的按键检测机制使得主循环可以以极高的频率运转从而完美兼顾了按键响应、数码管刷新和传感器采集。3. DS18B20 温度采集 (ds18b20.c)温度采集分为“启动转换”和“读取数据”两步。为了节省时间在代码中将其拆分。void Write_DS18B20(unsigned char dat); unsigned char Read_DS18B20(void); bit init_ds18b20(void); #define SKIP_ROM 0XCC #define CONVERT_T 0x44 #define READ_SCRATCHPAD 0xBE void ds18b20_convert() { unsigned char i; iinit_ds18b20; Write_DS18B20(SKIP_ROM); Write_DS18B20(CONVERT_T); } float ds18b20_read() { unsigned char tl0,th0,i0; float t; int temp; iinit_ds18b20(); Write_DS18B20(SKIP_ROM); Write_DS18B20(READ_SCRATCHPAD); tlRead_DS18B20(); thRead_DS18B20(); temptl|(th8); ttemp/16.0; return t; }DS18B20 返回的是 16 位的补码数据。在ds18b20_read函数中我们将读取到的高 8 位和低 8 位拼接成一个完整的 16 位整数temp。由于 DS18B20 默认配置为 12 位精度其最低 4 位代表小数部分。因此将temp乘以0.0625即除以 16.0即可得到带小数的实际温度值。实战细节题目要求温度保留 1 位小数。51单片机没有硬件浮点运算在代码中通常会将温度值放大 10 倍转为整数处理例如23.2℃存为232在数码管显示时再通过除法和取模运算分别提取十位、个位和小数位。4. PCF8591 光敏检测 (pcf8591.c)光敏电阻接在 PCF8591 的 AIN1 通道。通过 I2C 读取 ADC 值用来判断环境的“亮”与“暗”。void I2CStart(void); void I2CStop(void); void I2CSendByte(unsigned char byt); unsigned char I2CReceiveByte(void); unsigned char I2CWaitAck(void); void I2CSendAck(unsigned char ackbit); unsigned char ack0; void pcf8591_adc() { I2CStart(); I2CSendByte(0x90); ackI2CWaitAck(); I2CSendByte(0x01); ackI2CWaitAck(); I2CStop(); } unsigned char pcf8591_read() { unsigned char dat0; I2CStart(); I2CSendByte(0x91); ackI2CWaitAck(); datI2CReceiveByte(); I2CSendAck(1); I2CStop(); return dat; }5. NE555 测频与湿度换算 (ne555.c/main.c)NE555 输出的是方波脉冲题目要求通过频率计算湿度200Hz~2000Hz 有效。思路是利用定时器 1 作为计数器对外部脉冲计数利用定时器 0 计时 1 秒。1 秒时间到读取定时器 1 的计数值即为频率。oid Timer0_Isr(void) interrupt 3 { static int T00,T10,T20,T30,T40,T50; T0;T1;T2,T4,T5; if(T01) { T00; smg_loop(); } if(T120) { T10; key_loop(); keyflag1; } if(T2400) { T20; adcflag; if(adcflag1)adcflag0; } if(getflag1)//²É¼¯±ê־λ { T3; mode03; if(T33000) { T30; mode0lastmode; getflag0; overflag0; } } if(T41000)//ne555ƵÂÊ { T40; countflag1; } if(T5100)//0.1sÉÁ˸led4 { T50; ledflag; ledflagledflag%2; } } void Timer0_Init(void) //1ºÁÃë12.000MHz { AUXR | 0x80; //¶¨Ê±Æ÷ʱÖÓ1Tģʽ TMOD 0x04; //ÉèÖö¨Ê±Æ÷ģʽ TL0 0xFF; //ÉèÖö¨Ê±³õʼֵ TH0 0xFF; //ÉèÖö¨Ê±³õʼֵ TR0 1; //¶¨Ê±Æ÷0¿ªÊ¼¼ÆÊ± ET0 1; //ʹÄܶ¨Ê±Æ÷0ÖÐ¶Ï EA1; } void Timer1_Isr(void) interrupt 1 { hz; } void Timer1_Init(void) //1ºÁÃë12.000MHz { AUXR | 0x40; //¶¨Ê±Æ÷ʱÖÓ1Tģʽ TMOD 0x0F; //ÉèÖö¨Ê±Æ÷ģʽ TL1 0x20; //ÉèÖö¨Ê±³õʼֵ TH1 0xD1; //ÉèÖö¨Ê±³õʼֵ TF1 0; //Çå³ýTF1±êÖ¾ TR1 1; //¶¨Ê±Æ÷1¿ªÊ¼¼ÆÊ± ET1 1; //ʹÄܶ¨Ê±Æ÷1ÖÐ¶Ï }三、 核心业务逻辑解析 (main.c)1. 定时器中断的时间片调度整个系统的“心脏”是定时器 0 的 1ms 中断。我在中断里划分了多个时间片实现了多任务调度2ms刷新数码管20ms扫描键盘400msADC 采集标志位翻转1000ms结算 NE555 频率100msLED 闪烁控制用于温度超限报警Timer0_Isr中断服务函数在上面列了2. 光敏触发采集机制核心难点题目要求光敏从“暗”变“亮”时触发一次数据采集数码管跳转到温湿度显示界面3 秒后自动返回原界面。同时触发期间需要屏蔽按键操作。在主循环中通过对比上一次 ADC 值 (lastadc) 和当前 ADC 值 (nowadc) 来判断边沿当lastadc处于暗区间且nowadc进入亮区间时置位getflag 1。在定时器中断中如果getflag 1则强制mode0 3触发界面并启动 3 秒倒计时。倒计时结束后恢复mode0并清零getflag。while(1) { if(getflag0)//´¥·¢ºóÆÁ±Î°´¼ü { valuekey_get(); } if(keyflag1) { keyvalue; keyflag0; if(key90) { k; if(k100) { k0; trigger0; maxh0; maxt0; gettime20; gettime10; } } else k0; } if(value4)//ÏÔʾ½çÃæÇл» { mode0; if(mode02)mode00; if(mode00) { led.b.b00;led.b.b11;led.b.b21; P2andP0(0x80,led.hex); } if(mode01) { led.b.b01;led.b.b10;led.b.b21; P2andP0(0x80,led.hex); } if(mode02) { led.b.b01;led.b.b11;led.b.b20; P2andP0(0x80,led.hex); } } if(value5)//»ØÏÔ½çÃæÇл» { if(mode0!0) { mode1; mode1mode1%3; } } if(value8)// { if(mode02) { tc; if(tc99)tc0; } } if(value9)//- { tc--; if(tc0)tc99; } view(); time_read(); if(adcflag1getflag0)//²É¼¯´¥·¢ { pcf8591_adc(); lastadcnowadc; nowadcpcf8591_read(); tempurture(); if(lastadc76nowadc76)//Âú×ã²É¼¯Ìõ¼þ { getflag1; frequencyf; humidity(); lastmodemode0; echo(); gettime1time3[1]; gettime2time3[2]; if(t1tc) { overflag1; } else overflag0; } } if((overflag1)(getflag1))//ζȱȽϿØÖÆled2 { if(ledflag0) { led.b.b30; P2andP0(0x80,led.hex); } else { led.b.b31; P2andP0(0x80,led.hex); } } else { led.b.b31; P2andP0(0x80,led.hex); } if(countflag1)//ne555 { fhz; hz0; countflag0; } } }3. 数据回显与最值/平均值计算每次触发采集后需要将当前的温度、湿度存入数组并计算历史最大值和平均值。定义了两个数组oldt[]和oldh[]来存储历史数据。在echo()函数中更新最大值每次新数据与maxt/maxh比较。计算平均值遍历数组求和除以触发次数trigger。判断温湿度是否“均升高”当前值同时大于上一次的值时点亮 L6。void echo()//»ØÏÔÊý¾Ý´¦Àí { unsigned char i0; oldt[trigger]t1; oldh[trigger]dampness; if(trigger2) { if((t1oldt[trigger-1])(dampnessoldh[trigger-1])) { led.b.b50; P2andP0(0x80,led.hex); } else { led.b.b51; P2andP0(0x80,led.hex); } } if(trigger0) { maxtt1; maxhdampness; } else { if(t1maxt)maxtt1; if(dampnessmaxh)maxhdampness; } for(i0;itrigger;i) { avertavertoldt[i]; averhaverholdh[i]; } if(trigger!0) { avertavert/(trigger1); averhaverh/(trigger1); } trigger; if(trigger99)trigger0; }4. 界面状态机与数码管格式化显示系统有 4 个主界面时间、回显、参数、触发显示回显界面下又有 3 个子界面温度、湿度、时间。在view()函数中通过switch(mode0)和switch(mode1)进行嵌套切换。显示格式化技巧利用三目运算符trigger0 ? 17 : maxt/10当没有触发记录时显示17即段码表中的0xFF熄灭有记录时正常显示数字。小数点的处理通过给段码值20或加上小数点对应的位值来点亮小数点。void view() { switch(mode0) { case 0://ʱ¼äÏÔʾ { smg_set(0,time0[2]); smg_set(1,time1[2]); smg_set(2,16); smg_set(3,time0[1]); smg_set(4,time1[1]); smg_set(5,16); smg_set(6,time0[0]); smg_set(7,time1[0]); break; } case 1://»ØÏÔ { switch(mode1) { case 0://ÎÂ¶È { smg_set(0,12); smg_set(1,17); smg_set(2,trigger0?17:maxt/10); smg_set(3,trigger0?17:maxt%10); smg_set(4,trigger0?17:16); smg_set(5,trigger0?17:(int)(avert*10)/100); smg_set(6,trigger0?17:(int)(avert*10)%100/1020); smg_set(7,trigger0?17:(int)(avert*10)%10); break; } case 1://ʪ¶È { smg_set(0,19); smg_set(1,17); smg_set(2,trigger0?17:maxh/10); smg_set(3,trigger0?17:maxh%10); smg_set(4,trigger0?17:16); smg_set(5,trigger0?17:(int)(averh*10)/100); smg_set(6,trigger0?17:(int)(averh*10)%100/1020); smg_set(7,trigger0?17:(int)(averh*10)%10); break; } case 2://ʱ¼ä { smg_set(0,15); smg_set(1,trigger/10); smg_set(2,trigger%10); smg_set(3,trigger0?17:gettime2/10); smg_set(4,trigger0?17:gettime2%10); smg_set(5,trigger0?17:16); smg_set(6,trigger0?17:gettime1/10); smg_set(7,trigger0?17:gettime1%10); break; } } break; } case 2://²ÎÊý { smg_set(0,18); smg_set(1,17); smg_set(2,17); smg_set(3,17); smg_set(4,17); smg_set(5,17); smg_set(6,tc/10); smg_set(7,tc%10); break; } case 3://´¥·¢ÏÔʾ { smg_set(0,14); smg_set(1,17); smg_set(2,17); smg_set(3,t1/10); smg_set(4,t1%10); smg_set(5,16); smg_set(6,dampness/10); smg_set(7,dampness%10); break; } } } void tempurture()//ζȻñÈ¡ { float t0.0; ds18b20_convert(); tds18b20_read(); t1(unsigned int)t; } void P2andP0(unsigned char p2,p0) { P0p0; P2P20x1F; P2p2|P2; P2P20x1F; } void humidity()//ʪ¶Èת»» { if(frequency200) { dampness10; led.b.b40; P2andP0(0x80,led.hex); } else if(frequency2000) { dampness90; led.b.b40; P2andP0(0x80,led.hex); } else { dampness(frequency50)/45;//¹«Ê½ÓÐÎÊÌâ led.b.b41; P2andP0(0x80,led.hex); } }5. 按键处理与长按清除在主循环中通过keyflag获取按键值。S4 (界面切换)mode0在 0、1、2 之间循环并同步控制 L1、L2、L3 的状态。S8 / S9 (参数加减)在参数界面 (mode02) 下调整温度阈值tc。S9 长按清除当key_get()返回90代表 S9 处于持续按下状态时启动计数器k。当k 100结合 20ms 的扫描周期约 2 秒时清零所有历史数据。四、 易错点与踩坑总结NE555 频率范围题目明确规定 200Hz~2000Hz 为有效数据。在humidity()函数中必须加入if(frequency 200 || frequency 2000)的判断并将湿度设为无效值如 90同时点亮 L5 指示灯。数码管消影在smg_loop中送完段码后记得将smg_word[i]重置为 17熄灭码防止下一位扫描时出现“鬼影”。

相关新闻

最新新闻

SWIDERM身体护理怎么样

SWIDERM身体护理怎么样

洗完澡换上贴身衣物时,身体护理有没有质感,会比开瓶那一下香不香更明显。SWIDERM身体护理的特点,是把浴室里的柔润清洁、露肤前的触感整理、睡前或穿衣前的油相收尾连起来。它不是把香气做得很满,而是让身体肌肤在不同生活节点里显…

2026/7/9 6:41:37
网络安全必备计算机网络全解|面试/护网/渗透全覆盖(一文吃透)

网络安全必备计算机网络全解|面试/护网/渗透全覆盖(一文吃透)

导语 很多网安新手、渗透测试从业者、护网人员最大的短板:只会用工具,不懂网络底层。漏洞抓包看不懂、TCP三次握手面试答不全、路由转发原理模糊、端口协议混淆,导致渗透踩坑、面试挂科、漏洞复现失败。 本文基于网安实战视角,重构…

2026/7/9 6:41:37
基于大语言模型的网页信息提取Agent:从原理到工程实践

基于大语言模型的网页信息提取Agent:从原理到工程实践

🚀 30款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度 大家好,我是专注于技术分享的博主。今天我们来深入探讨一个近期在开发者社区引发热议的话题: AI Agent&#…

2026/7/9 6:41:37
为什么头部科技公司已悄悄切换主力模型?GPT-5与Claude Fable 5的token经济模型、上下文坍缩阈值、思维链稳定性深度拆解

为什么头部科技公司已悄悄切换主力模型?GPT-5与Claude Fable 5的token经济模型、上下文坍缩阈值、思维链稳定性深度拆解

更多请点击: https://codechina.net 第一章:头部科技公司主力模型切换的底层动因与战略信号 近年来,Google、Meta、Microsoft 和 Anthropic 等头部科技公司陆续将其公开产品线的主力大模型从通用基础架构转向专用化、轻量化与推理优化的新一…

2026/7/9 6:41:37
172.1.RFID设备智能管理-WiFi-基于STM32单片机物联网设计【硬件+APP+云平台】

172.1.RFID设备智能管理-WiFi-基于STM32单片机物联网设计【硬件+APP+云平台】

(1)硬件端 设备端: 1. ESP8266-01s:使用wifi模块进行联网使用; 2. 0.96寸OLED:用于显示的设备的状态,实时显示数据; 3. STM32F103C8T6:用于所有程序的中控和模块数据通信…

2026/7/9 6:41:37
小米平板5 Windows驱动完整解决方案:让Android平板变身生产力工具

小米平板5 Windows驱动完整解决方案:让Android平板变身生产力工具

小米平板5 Windows驱动完整解决方案:让Android平板变身生产力工具 【免费下载链接】MiPad5-Drivers https://github.com/Project-Aloha/windows_oem_xiaomi_nabu 项目地址: https://gitcode.com/gh_mirrors/mi/MiPad5-Drivers 想要将您的小米平板5从Android设…

2026/7/9 6:36:36

月新闻