首先本代码是基于小蜜蜂老师的模板进行编写的希望对大家有所帮助。4T满分截图部分题目展示下面直接进行代码解析// 1. 头文件引用 #include STC15F2K60S2.H // STC15系列单片机的核心寄存器定义文件 #include onewire.H // 单总线协议驱动 (通常用于 DS18B20 温度传感器) #include intrins.H // 内部函数库包含 _nop_() 等空操作指令用于精确延时 #include iic.H // I2C 总线协议驱动 (通常用于 PCF8591 DAC 或 AT24C02) // 2. 数码管段码表 (共阳极) // 0-9, A, b, C, d, E, F (共16个字符) // 0xC0 对应 0, 0xF9 对应 1 ... unsigned char code SMG_NODOT[] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E }; // 带小数点的段码表 (0-9) // 原理将 SMG_NODOT 的值与 0x80 (小数点位) 进行“或”运算 // 例如0x40 0xC0 | 0x80 (这里数据看起来像是单独定义的或者是特定硬件接法) // 注意通常共阳极小数点是 0x80这里 0x40 可能是硬件接在 dp 位不同或者是代码特有逻辑 unsigned char code SMG_DOT[] { 0x40, 0x79, 0x24, 0x30, 0x19, 0x12, 0x02, 0x78, 0x00, 0x10 }; // 3. 全局变量定义 (系统状态核心) // --- 模式控制变量 --- unsigned char mode 1; // 主显示模式1温度界面, 2DAC界面, 3误差界面 unsigned char mode_temp 0; // 温度子模式0自动模式, 1手动模式 // --- 数据处理变量 --- float volt; // 存储计算出的电压值 (用于 DAC 输出) float temp; // 存储 DS18B20 读取的原始温度值 // --- LED 闪烁与计时控制 (用于定时器中断) --- char js 2; // 通用计时/闪烁控制变量 (控制闪烁频率) char jo 0; // 奇偶计数器 (用于交替闪烁逻辑) // --- 温度相关状态机变量 --- char temp_smg; // 最终显示在数码管上的温度值 (可能包含修正) char temp_last; // 上一次采样的温度值 (用于计算温差) char temp_err_led; // 温度变化差值 (用于控制 LED 报警闪烁) char temp_count; // 闪烁持续时间计数器 char temp_flag 0; // 温度状态标志 (0无变化, 1升温, 2降温) char temp_flag_lx 0; // 温度连续变化标志 (用于秒计数逻辑) // --- 误差与修正 --- char temp_err 0; // 用户设定的误差修正值 (通过按键调整) char temp_err_real; // 实际应用的误差值 // --- DAC 与 继电器控制 --- unsigned char dac_dat 100; // DAC 输出数值 (0-255)默认初始值 100 unsigned char jdq_mode 0; // 继电器锁状态0解锁(可操作), 1上锁(不可操作) // --- 硬件状态输出缓存 --- unsigned char led_state 0xff; // LED 状态缓存 (0xff 表示全灭共阳极) unsigned char jdq_state 0x00; // 继电器控制缓存 (0x00 可能表示吸合或断开视电路而定) // --- 时间控制 --- char second_r 0; // 秒计数器 (右/临时) char second_l 0; // 秒计数器 (左/超时控制) // --- 按键处理 --- unsigned char key_flag; // 按键按下标志位 (用于中断计数) unsigned char key_js; // 按键长按计时器 (用于区分短按/长按)一.底层驱动1.延时函数// 1ms 延时函数 (基于12MHz晶振) void Delay1ms(void) { unsigned char data i, j; i 12; j 169; do { while (--j); } while (--i); } // 200us 延时函数 void Delay200us(void) { unsigned char data i, j; i 3; j 82; do { while (--j); } while (--i); } // 50us 延时函数 (用于按键消抖) void Delay50us(void) { unsigned char data i; _nop_(); _nop_(); // 空操作指令微调时间 i 147; while (--i); }注释解释蓝桥杯开发板通常使用12MHz晶振。这些函数通过嵌套循环消耗CPU时间来实现精确延时。Delay1ms用于数码管动态扫描消隐Delay50us专门用于按键按下时的硬件消抖。2. 74HC573 锁存器控制// 选择锁存器通道 void HC573(unsigned char channel) { switch(channel) { case 4: P2(P20X1F)|0X80; break; // L1-L8 LED 继电器 case 5: P2(P20X1F)|0Xa0; break; // BUZZ RELAY (蜂鸣器/继电器使能) case 6: P2(P20X1F)|0Xc0; break; // SMG_BIT (数码管位选) case 7: P2(P20X1F)|0Xe0; break; // SMG_IO (数码管段选) } }注释解释这是蓝桥杯开发板最核心的逻辑。P2口的高3位P2.7-P2.5用来选择控制哪个外设。关键技巧P2 0x1F(00011111) 是为了保留 P2 低5位的值不变只修改高3位。如果不这样做可能会意外改变正在运行的外设状态。3.数码管显示操作// 初始化系统状态 void init_system() { HC573(4); P00Xff; // 关闭所有LED HC573(5); P00x00; // 关闭蜂鸣器和继电器 } // 设置数码管某一位显示什么 void set_smg(unsigned char pos, unsigned char value) { HC573(6); P00X01pos; // 选择位选 (pos位) HC573(7); P0value; // 输出段选码 (显示内容) Delay1ms(); // 保持1ms (动态扫描) HC573(6); P00X01pos; // 消隐再次选中该位 HC573(7); P00xff; // 段选全灭 (防止拖影) } // 全部数码管熄灭 void smg_xy() { HC573(6); P00xff; // 位选全0 (不选中任何位) HC573(7); P00xff; // 段选全1 (共阳极1为灭) }注释解释set_smg函数实现了动态扫描的核心。先选中某一位位选输出数据段选延时保持最后必须进行“消隐”操作先关闭段选再关闭位选或像代码中那样在关闭位选前将段选全灭否则会出现串位显示。二.外设1.DS18B20 温度读取void ds18b20() { unsigned char MSB, LSB; // 定义高8位和低8位变量 unsigned int dat; // 定义合并后的16位数据变量 // --- 步骤 1: 启动温度转换 --- init_ds18b20(); // 1. 复位 DS18B20 (发送复位脉冲) SMG_SHOW(); // 【关键技巧】: 在等待总线稳定或转换开始时刷新数码管防止熄灭 Write_DS18B20(0xcc); // 2. 跳过 ROM 匹配指令 (因为总线上只有一个传感器) Write_DS18B20(0x44); // 3. 发送温度转换指令 (开始进行 A/D 转换) // --- 步骤 2: 模拟延时等待 --- // 官方驱动通常需要延时 750ms 等待转换完成。 // 这里没有使用死板的 Delay(750ms)而是通过多次调用 SMG_SHOW() 来消耗时间。 // 这样既满足了时序要求又维持了人机交互界面的刷新。 SMG_SHOW(); // 消耗时间片 1 init_ds18b20(); // 4. 再次复位 DS18B20准备读取 SMG_SHOW(); // 消耗时间片 2 SMG_SHOW(); // 消耗时间片 3 (此时 DS18B20 内部转换已完成) // --- 步骤 3: 读取温度数据 --- Write_DS18B20(0xcc); // 5. 再次跳过 ROM Write_DS18B20(0xbe); // 6. 发送读取暂存器指令 (Read Scratchpad) LSB Read_DS18B20(); // 7. 读取低 8 位 (小数部分在低4位) MSB Read_DS18B20(); // 8. 读取高 8 位 (符号位在高5位) // --- 步骤 4: 数据合并与处理 --- dat MSB; dat (MSB 8) | LSB; // 将高8位左移8位与低8位进行“或”运算合并成16位数据 // 判断温度正负 (检查高5位是否全为0) // 0xf800 1111 1000 0000 0000 // 如果高5位是0说明是正温度 if ((dat (0xf800)) 0x0000) { temp dat * 0.0625; // 正温度直接乘以分辨率 0.0625 } // 注意代码中省略了负温度的处理逻辑可能默认只处理正温或通过其他方式处理 // --- 步骤 5: 更新全局状态 --- temp_last temp_smg; // 保存上一次的显示温度 (用于计算温差) temp_smg temp temp_err_real; // 计算最终显示温度 (原始温度 用户修正值) temp_err_led temp_smg - temp_last; // 计算本次温度变化量 (用于驱动 LED 闪烁逻辑) }注释解释代码中穿插了SMG_SHOW()这是为了在等待DS18B20转换时转换需要几百毫秒刷新数码管防止数码管熄灭。temp_err_led记录了温度的变化量这是为了在中断里判断温度是升高了还是降低了从而控制LED闪烁。2.DAC 输出控制 (PCF8591)// 底层驱动I2C 通信协议 // 功能向 PCF8591 发送一个字节的数据控制 DAC 输出电压 void dac_output(unsigned char dat) { I2CStart(); // 1. 发送 I2C 起始信号 I2CSendByte(0x90); // 2. 发送设备地址 (PCF8591 写地址) // 0x90 1001 0000 (A2-A0接地LSB0表示写) I2CWaitAck(); // 3. 等待从机应答 I2CSendByte(0x43); // 4. 发送控制字节 // 0x43 0100 0011 // Bit61 (使能DAC), Bit0-111 (选择通道3) I2CWaitAck(); // 5. 等待从机应答 I2CSendByte(dat); // 6. 发送数据 (0-255) // 这个值决定了输出电压的大小 I2CWaitAck(); // 7. 等待从机应答 I2CStop(); // 8. 发送 I2C 停止信号释放总线 } // 业务逻辑DAC 策略控制 // 功能根据模式决定输出什么电压 void dac_chuli() { unsigned char dat; // 定义 DAC 的数字量 (0-255) // 模式 0自动模式 (温度控制电压) if(mode_temp 0) { // 分段函数逻辑 if(temp_smg 10) { volt 2; // 温度 10°C固定输出 2V } else if(temp_smg 10 temp_smg 40) { // 10°C 温度 40°C线性增长 // 斜率 k (5V - 2V) / (40 - 10) 3 / 30 0.1 V/°C volt 2 0.1 * (temp_smg - 10); } else if(temp_smg 40) { volt 5; // 温度 40°C固定输出 5V (饱和) } // 将电压转换为 DAC 数字量 // 公式D (V_out / V_ref) * 255 // 这里 V_ref 5V所以 dat volt * 51 dat volt * (255 / 5.0); dac_output(dat); // 调用底层驱动输出 } // 模式 1手动模式 (用户控制电压) else if(mode_temp 1) { dac_output(dac_dat); // 直接输出用户设定的值 (dac_dat 由按键控制) } }注释解释dac_chuli是一个典型的“策略模式”函数。它根据mode_temp的值决定是走自动算法温度查表还是直接输出用户设定的手动值。3.数码管显示逻辑void SMG_SHOW() { // 根据全局变量 mode 的值决定显示哪个界面 switch(mode) { // 模式 1温度显示界面 case 1: // 1. 显示单位/标识符 // 在第0位数码管显示 C (假设 SMG_NODOT[12] 是 C 的段码) set_smg(0, SMG_NODOT[12]); // 2. 温度数值显示 (动态位数处理) // 情况A: 温度 10 (个位数) if(temp_smg 9) { // 只显示个位 (第7位)高位自动熄灭 (消隐) set_smg(7, SMG_NODOT[temp_smg % 10]); } // 情况B: 10 温度 999 (十位或百位) else if(temp_smg 999) { // 显示十位 (第6位) 和 个位 (第7位) // 注意这里代码似乎省略了百位的显示逻辑或者题目要求只显示后两位 set_smg(6, SMG_NODOT[temp_smg / 10]); // 取十位 set_smg(7, SMG_NODOT[temp_smg % 10]); // 取个位 } break; // 模式 2DAC 数值显示界面 case 2: // 1. 显示单位/标识符 // 在第0位数码管显示 H (假设 SMG_NODOT[10] 是 H 的段码代表Hex或High) set_smg(0, SMG_NODOT[10]); // 2. DAC数值显示 (0-255) // 情况A: 个位数 (0-9) if(dac_dat 9) { set_smg(7, SMG_NODOT[dac_dat % 10]); } // 情况B: 十位数 (10-99) else if(dac_dat 99) { set_smg(6, SMG_NODOT[dac_dat / 10]); // 十位 set_smg(7, SMG_NODOT[dac_dat % 10]); // 个位 } // 情况C: 百位数 (100-255) else if(dac_dat 999) { set_smg(5, SMG_NODOT[dac_dat / 100]); // 百位 set_smg(6, SMG_NODOT[(dac_dat / 10) % 10]); // 十位 set_smg(7, SMG_NODOT[dac_dat % 10]); // 个位 } break; // 模式 3误差/偏差显示界面 case 3: // 1. 显示单位/标识符 // 在第0位显示自定义符号 (0x8c)可能是 - 或 E (Error) set_smg(0, 0x8c); // 2. 误差数值显示 (带符号显示) // 情况A: 正误差或零 ( 0) if(temp_err 0) { // 直接显示数值 set_smg(7, SMG_NODOT[temp_err]); } // 情况B: 负误差 ( 0) else if(temp_err 0) { // 先显示负号 - (0xbf 是共阳极 - 的段码) set_smg(6, 0xbf); // 再显示数值的绝对值 (-temp_err) set_smg(7, SMG_NODOT[-temp_err]); } break; } // 3. 消隐处理 // 关闭所有位选防止上一轮显示的残影鬼影 smg_xy(); }注释解释这个函数非常繁琐但重要。它根据全局变量mode模式的不同调用set_smg在数码管的不同位置显示不同的数组内容。注意处理了多位数的拆分/10,%10。4.按键扫描void key_scan() { // 检测 S12 (模式切换键) // 技巧行反转法扫描矩阵按键 // 0x08 0000 1000, ~0x08 1111 0111 // 将 P3.3 拉低如果按键按下P3.5 也会被拉低 P3 ~0X08; if(P35 0) // 检测 P3.5 是否为低电平 { Delay50us(); // 软件消抖 if(P35 0) { // 功能切换主显示模式 (1-2-3-1) mode; if(mode 4) { mode 1; // 当切回模式1时将用户设定的误差值写入实际误差变量 temp_err_real temp_err; } // 等待松手 (死循环)期间保持系统刷新防止死机 while(P35 0) { dac_chuli(); // 保持 DAC 输出稳定 SMG_SHOW(); // 保持数码管显示 } } } // 检测 S16 (加/开关键) // 注意这里直接检测 P34说明 S16 和 S17 可能是独立按键或者 P3 口已配置好 if(P34 0) { Delay50us(); if(P34 0) { // 功能复用根据 mode 决定按键功能 if(mode 1) // 模式1控制继电器 { if(jdq_mode 0) // 只有在解锁状态下才能操作 jdq_state | 0x10; // 继电器吸合 (P0.4置0) } else if(mode 2) // 模式2DAC 数值增加 { dac_dat 5; // 步进为 5 if(dac_dat 255) dac_dat 255; // 限幅 } else if(mode 3) // 模式3误差值增加 { temp_err; if(temp_err 9) temp_err 9; // 限幅 } while(P34 0) { dac_chuli(); SMG_SHOW(); } } } // 检测 S13 (功能/长按键) P3 ~0X04; // 0000 0100 - 1111 1011 (将 P3.2 拉低) if(P35 0) // 检测 P3.5 { Delay50us(); if(P35 0) { // 核心逻辑长按检测 // 在按住按键期间不断置位 key_flag并刷新显示 while(P35 0) { dac_chuli(); SMG_SHOW(); key_flag 1; // 通知定时器中断按键被按住了 } // 松手后进行判断 // key_js 是在定时器中断中累加的 (每50ms加1) // 30 * 50ms 1500ms 1.5秒 if(key_js 30) // 短按 (小于1.5秒) { // 切换 自动/手动 模式 if(mode_temp 0) mode_temp 1; else if(mode_temp 1) mode_temp 0; key_js 0; // 清零计数器 key_flag 0; } else // 长按 (大于1.5秒) { // 切换 继电器锁 状态 (解锁 - 上锁) if(jdq_mode 0) jdq_mode 1; else if(jdq_mode 1) jdq_mode 0; key_flag 0; key_js 0; } } } // 检测 S17 (减/关关键) if(P34 0) { Delay50us(); if(P34 0) { // 功能复用与 S16 逻辑相反 if(mode 1) // 模式1控制继电器 { if(jdq_mode 0) jdq_state ~(0x10); // 继电器断开 (P0.4置1) } else if(mode 2) // 模式2DAC 数值减小 { dac_dat - 5; if(dac_dat 0) dac_dat 0; // 限幅 } else if(mode 3) // 模式3误差值减小 { temp_err--; if(temp_err -9) temp_err -9; // 限幅 } while(P34 0) { dac_chuli(); SMG_SHOW(); } } } }注释解释按键逻辑是代码中最复杂的部分。短按/长按检测S13按键通过key_flag和key_js在中断中计数来判断用户是短按切换自动/手动还是长按切换上锁/解锁。这是通过在while(按键按下)循环中不重置计数器实现的。功能复用同一个按键S16/S17在不同mode下有不同的功能控制继电器、调节DAC、调节误差。三.主循环与中断1.定时器0 中断void init_timer0() { // 1. 设置工作模式 // TMOD 0x01 - 0000 0001 // 低4位控制 T0: GATE0, C/T0 (定时), M10, M01 (模式1: 16位定时器) TMOD 0X01; // 2. 装载初值 (50ms 12MHz) // 65536 - 50000 15536 TH0 (65536 - 50000) / 256; // 高8位 TL0 (65536 - 50000) % 256; // 低8位 // 3. 开启中断 ET0 1; // 允许定时器0中断 EA 1; // 允许总中断 // 4. 启动定时器 TR0 1; // 开始计数 } void timer0() interrupt 1 { // 1. 定时器重载 (产生50ms基准时钟) // 晶振12MHz机器周期1us。65536-50000 15536即50ms溢出一次 TH0 (65536 - 50000) / 256; TL0 (65536 - 50000) % 256; // 2. 按键长按计数器 (配合S13功能) // 如果按键扫描中检测到按键按下(key_flag1)这里每50ms加1 // 约计数30次(1.5秒)判定为长按用于区分S13的短按(切模式)和长按(上锁) if (key_flag 1) { key_js; } // 3. 自动/手动模式指示 (LED1) // mode_temp: 0自动, 1手动 // 逻辑自动时LED1灭(0)手动时LED1亮(1) if (mode_temp 0) { led_state (~0x01); // LED1 灭 } else { led_state | 0x01; // LED1 亮 } // 4. 温度变化报警闪烁逻辑 (LED2, LED3) // js 是一个全局计数器用于控制闪烁频率 if (js 0) js--; else if (js 0) { // LED2 闪烁逻辑 (温度升高/维持) // temp_err_led 0 表示温度在升高 if ((temp_err_led 1 || temp_flag 1) temp_flag ! 2) { temp_flag 1; // 标记状态为“升高” temp_count; // 计数器累加 led_state (~0x02); // LED2 亮 (共阳极0为亮) // 闪烁40次 (约2秒) 后熄灭并重置 if (temp_count 40) { led_state | 0x02; // LED2 灭 temp_count 0; temp_flag 0; // 重置状态 } } // LED3 闪烁逻辑 (温度降低) // temp_err_led 0 表示温度在降低 else if (temp_err_led -1 || temp_flag 2) { temp_flag 2; // 标记状态为“降低” temp_count; led_state (~0x04); // LED3 亮 if (temp_count 40) { led_state | 0x04; // LED3 灭 temp_count 0; temp_flag 0; } } // 5. 秒计数与特殊闪烁 (LED4) // 只要温度在变化(temp_err_led非0)就启动秒计数 if ((temp_err_led 1 || temp_err_led -1) || temp_flag_lx 1) { temp_flag_lx 1; second_r; // 计数器r second_l; // 计数器l // 这里实现了一个“慢速闪烁”逻辑 // 每4次中断(约200ms)触发一次状态翻转 if ((second_r - 1) % 4 0) { jo; // 翻转计数器 if (jo % 2 ! 0) led_state (~0x08); // 奇数次亮 if (jo % 2 0) led_state | 0x08; // 偶数次灭 } // 60秒超时保护 if (second_l 60) { led_state | 0x08; // 强制熄灭LED4 temp_flag_lx 0; second_l 0; second_r 0; jo 0; } } } // 6. 锁状态指示 (LED8) // jdq_mode: 0解锁, 1上锁 // 逻辑解锁时灭上锁时亮 if (jdq_mode 0) { led_state (~0x80); // LED8 灭 } else if (jdq_mode 1) { led_state | 0x80; // LED8 亮 } }注释解释中断服务程序ISR不应该做太耗时的操作。这里的逻辑主要是更新变量如key_js计数second_r计时具体的硬件操作P0赋值是在main循环中完成的。2.main主函数void main() { init_system(); // 初始化硬件 init_timer0(); // 开启定时器 while(1) { dac_chuli(); // 1. 处理DAC输出 (实时性要求高) ds18b20(); // 2. 读取温度 (包含延时放在循环中) key_scan(); // 3. 扫描按键 SMG_SHOW(); // 4. 刷新数码管 // 5. 输出 LED 和 继电器 状态 HC573(4); P0led_state; // LED状态由中断计算得出 HC573(5); P0jdq_state; // 继电器状态由按键设置 } }注释解释主循环采用了前后台系统Super Loop架构。顺序很重要先处理数据DAC、温度再处理交互按键、显示最后输出硬件状态。分工明确中断负责“感知”时间流逝和按键长短主循环负责“执行”具体的业务逻辑。四.总结我这篇代码虽然有些地方写法比较“野”但它完美展示了蓝桥杯考察的核心拿到比赛上是完全没问题的硬件基础锁存器、I2C、单总线时序。算法逻辑浮点数转整型显示、电压映射、温度区间判断。状态机通过mode和mode_temp控制程序流程。抗干扰按键消抖、数码管消隐。