蓝桥杯单片机第十二届省赛代码解析
蓝桥杯单片机赛道的代码学习
蓝桥杯第十二届省赛代码解析(51单片机)
一、项目整体结构
第十二届省赛/ ├── Driver/ # 底层驱动层 │ ├── Init.c/h # 系统初始化 │ ├── Led.c/h # LED 与蜂鸣器、继电器驱动 │ ├── Key.c/h # 矩阵键盘驱动 │ ├── Seg.c/h # 数码管驱动 │ ├── iic.c/h # I2C 总线 + DAC 驱动 │ └── onewire.c/h # 单总线 + DS18B20 温度驱动 └── User/ └── main.c # 主程序逻辑
硬件平台: STC15F2K60S2 单片机(蓝桥杯 CT107D 竞赛板)
功能概述:
-
实时采集 DS18B20 温度
-
三种显示界面(温度 / 参数设置 / DAC电压)
-
两种 DAC 输出模式
-
LED 指示当前状态
二、底层驱动模块详解
2.1 系统初始化 Init.c
void System_Init()
{
P0 = 0xff;
P2 = P2 & 0x1f | 0x80; // 打开 LED 锁存器
P2 &= 0x1f; // 关闭锁存器
P0 = 0x00;
P2 = P2 & 0x1f | 0xa0; // 打开蜂鸣器/继电器锁存器
P2 &= 0x1f;
}
原理: CT107D 板使用 74HC573 锁存器控制外设,P2 的高 3 位(P2.7~P2.5)作为锁存器片选。
| P2[7:5] | 控制的外设 |
|---|---|
| 100 (0x80) | LED 锁存器 |
| 101 (0xA0) | 蜂鸣器/继电器锁存器 |
| 110 (0xC0) | 数码管位选锁存器 |
| 111 (0xE0) | 数码管段选锁存器 |
初始化目的:
-
先将 P0 = 0xFF,打开 LED 锁存器 → 所有 LED 熄灭(低电平亮,高电平灭)
-
再将 P0 = 0x00,打开蜂鸣器锁存器 → 蜂鸣器和继电器关闭
2.2 LED 驱动 Led.c
void Led_Disp(unsigned char addr, enable)
{
static unsigned char temp = 0x00;
static unsigned char temp_old = 0xff;
if(enable)
temp |= 0x01 << addr; // 置位对应 bit
else
temp &= ~(0x01 << addr); // 清零对应 bit
if(temp != temp_old) // 只有数据变化时才刷新硬件
{
P0 = ~temp; // LED 低电平点亮,取反
P2 = P2 & 0x1f | 0x80;
P2 &= 0x1f;
temp_old = temp;
}
}
要点:
-
addr:LED 编号(0~7 对应 L1~L8) -
enable:1=点亮,0=熄灭 -
使用
static变量保存当前 LED 状态,避免每次调用都写硬件(减少总线操作) -
temp != temp_old判断:只有状态改变时才操作锁存器,这是竞赛代码常用的优化技巧
同文件还有:
-
Beep(flag):控制蜂鸣器(bit6 = 0x40) -
Relay(flag):控制继电器(bit4 = 0x10)
2.3 矩阵键盘 Key.c
CT107D 使用 4×4 矩阵键盘,行列扫描法:
列(输出):P44, P42, P35, P34 行(输入):P33, P32, P31, P30
扫描逻辑: 每次只拉低一列,检测四行是否有低电平。
P44=0; P42=1; P35=1; P34=1; // 拉低第1列 if(P33==0) temp = 4; // S4 if(P32==0) temp = 5; // S5 ...
键码映射表:
| 按键 | 键码 | 本题作用 |
|---|---|---|
| S4 | 4 | 界面切换 |
| S5 | 5 | 输出模式切换 |
| S8 | 8 | 参数自减 |
| S9 | 9 | 参数自加 |
2.4 数码管驱动 Seg.c
unsigned char seg_dula[] = {
0xc0, // 0
0xf9, // 1
0xa4, // 2
0xb0, // 3
0x99, // 4
0x92, // 5
0x82, // 6
0xf8, // 7
0x80, // 8
0x90, // 9
0xff, // 10 → 熄灭
0xc6, // 11 → C
0x8c, // 12 → P
0x88 // 13 → A
};
显示流程(动态扫描):
1. 先关段选(P0=0xff,送段选锁存器)→ 消影 2. 送位选数据(选中哪一位数码管) 3. 送段选数据(显示什么字符) 4. 若 point=1,则 P0 &= 0x7f(点亮小数点,最高位为小数点,低电平有效)
2.5 I2C 总线 + DAC 驱动 iic.c
硬件: PCF8591(8位 DAC/ADC 芯片),通过 I2C 连接。
-
SDA → P2.1
-
SCL → P2.0
I2C 时序关键函数
| 函数 | 作用 |
|---|---|
IIC_Start() |
产生起始信号(SCL高时SDA下降沿) |
IIC_Stop() |
产生停止信号(SCL高时SDA上升沿) |
IIC_SendByte() |
发送1字节(MSB先发) |
IIC_WaitAck() |
等待从机应答 |
DAC 写入函数
void Da_Write(unsigned char dat)
{
IIC_Start();
IIC_SendByte(0x90); // PCF8591 写地址(地址0x48,写操作)
IIC_WaitAck();
IIC_SendByte(0x41); // 控制字节:启用DAC输出(bit6=1),选通道1
IIC_WaitAck();
IIC_SendByte(dat); // DAC 数值(0~255 对应 0~5V)
IIC_WaitAck();
IIC_Stop();
}
电压换算: dat = Voltage * 51
-
5V → 255,0V → 0,1V → 51,4V → 204
2.6 单总线 DS18B20 温度驱动 onewire.c
数据线: P1.4 (DQ)
读温度完整流程
float rd_temperature()
{
// 第一步:发送转换命令
init_ds18b20(); // 复位
Write_DS18B20(0xcc); // 跳过 ROM(Skip ROM)
Write_DS18B20(0x44); // 开始温度转换
// 第二步:读取温度数据
init_ds18b20(); // 再次复位
Write_DS18B20(0xcc); // 跳过 ROM
Write_DS18B20(0xbe); // 读暂存器(Read Scratchpad)
low = Read_DS18B20(); // 读低字节
high = Read_DS18B20(); // 读高字节
return ((high<<8) | low) / 16.0; // 分辨率0.0625°C,除以16换算
}
温度换算原理:
-
DS18B20 返回 12 位原始数据,单位 1/16 °C
-
例:原始值 0x01A0 = 416,416 ÷ 16 = 26.0°C
为什么上电要先读一次再延时750ms?
-
DS18B20 上电默认温度寄存器为 85°C(出厂复位值)
-
上电后立即读会得到 85°C 的错误值
-
先触发一次转换并等待 750ms(12位分辨率最大转换时间),再读才准确
三、主程序逻辑 main.c
3.1 全局变量
unsigned char Key_Val, Key_Down, Key_Old, Key_Up; // 按键状态 unsigned char Seg_Buf[8]; // 数码管显示缓存(8位) unsigned char Seg_Point[8]; // 小数点控制 unsigned char Seg_Disp_Mode; // 显示界面:0=温度 1=参数设置 2=DAC unsigned char Temperature_Params_Ctrol = 25; // 温度阈值(默认25°C) float Voltage_Output; // DAC 输出电压 float Temperature; // 实时温度 bit Ouput_Mode; // DAC模式:0=温控 1=线性
3.2 按键处理 Key_Proc()
边沿检测算法(竞赛必背!)
Key_Val = Key_Read(); // 当前键值 Key_Down = Key_Val & (Key_Old ^ Key_Val); // 下降沿(按下瞬间) Key_Up = ~Key_Val & (Key_Old ^ Key_Val); // 上升沿(松开瞬间) Key_Old = Key_Val; // 保存本次键值
原理图解:
Key_Old ^ Key_Val → 找出"发生变化的位" Key_Val & 变化位 → 变化且当前为1 → 下降沿(从0变1,注意低电平有效则为按下) ~Key_Val & 变化位 → 变化且当前为0 → 上升沿(从1变0)
注意:
Key_Read()中按键按下返回非零键码,未按下返回 0。 所以Key_Down != 0代表刚按下某键。
按键功能逻辑
| 按键 | 键码 | 功能 |
|---|---|---|
| S4 | 4 | 界面循环切换(0→1→2→0),进入界面1时同步控制值到显示值,离开界面1时保存设置 |
| S8 | 8 | 界面1中:温度参数-1(最小0) |
| S9 | 9 | 界面1中:温度参数+1(最大99) |
| S5 | 5 | 切换 DAC 输出模式(异或翻转) |
界面切换时的数据同步逻辑:
进入界面1:Temperature_Params = Temperature_Params_Ctrol(用实际值初始化编辑值) 离开界面1(进入界面2时):Temperature_Params_Ctrol = Temperature_Params(保存编辑值)
3.3 数据显示 Seg_Proc()
每 500ms 执行一次(由 Seg_Slow_Down 控制)。
界面 0:温度显示
显示格式:C - - XX.XX Seg_Buf[0] = 11 → 显示 'C' Seg_Buf[4..7] → 温度值(整数2位 + 小数2位) Seg_Point[5] = 1 → 第6位有小数点(即 XX.XX)
数字拆分方法:
// 设 Temperature = 26.75 (unsigned char)Temperature = 26 // 截断整数部分 (unsigned char)Temperature / 10 % 10 = 2 // 十位 (unsigned char)Temperature % 10 = 6 // 个位 (unsigned int)(Temperature * 100) = 2675 2675 / 10 % 10 = 7 // 第一位小数 2675 % 10 = 5 // 第二位小数
界面 1:参数设置
显示格式:P - - - - XX Seg_Buf[0] = 12 → 显示 'P' Seg_Buf[4~5] = 10 → 熄灭(不显示) Seg_Buf[6~7] → 温度参数(0~99)
界面 2:DAC 电压显示
显示格式:A - - - X.XX Seg_Buf[0] = 13 → 显示 'A' Seg_Buf[5~7] → 电压值(整数1位 + 小数2位)
3.4 LED 与 DAC 控制 Led_Proc()
DAC 输出逻辑
模式 0(温控模式,Ouput_Mode = 0):
实时温度 > 阈值 → 输出 5V 实时温度 ≤ 阈值 → 输出 0V
模式 1(线性模式,Ouput_Mode = 1):
温度 < 20°C → 输出 1V 温度 > 40°C → 输出 4V 20°C ~ 40°C → 输出 = 0.15 × (温度 - 20) + 1(线性插值)
线性关系:斜率 = (4-1)/(40-20) = 0.15 V/°C
DAC 数值换算:
Da_Write(Voltage_Output * 51); // 5V = 255 = 5×51,所以乘51即可
LED 指示
ucLed[0] = !Ouput_Mode; // L1: 模式0时亮(温控模式指示) ucLed[1] = (0 == Seg_Disp_Mode); // L2: 温度界面时亮 ucLed[2] = (1 == Seg_Disp_Mode); // L3: 参数界面时亮 ucLed[3] = (2 == Seg_Disp_Mode); // L4: DAC界面时亮
3.5 定时器中断 Timer0Init() & Timer0Server()
定时器配置: 1ms 中断 @ 12MHz,12T 模式
TL0 = 0x18; TH0 = 0xFC; // 计数初值 0xFC18 = 64536,65536-64536 = 1000 个机器周期 = 1ms
中断服务函数(每 1ms 执行一次):
void Timer0Server() interrupt 1
{
if(++Key_Slow_Down == 10) Key_Slow_Down = 0; // 10ms 执行一次按键扫描
if(++Seg_Slow_Down == 500) Seg_Slow_Down = 0; // 500ms 执行一次数据采集/显示
if(++Seg_Pos == 8) Seg_Pos = 0; // 数码管位置轮询(0~7)
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], Seg_Point[Seg_Pos]); // 刷新当前位数码管
Led_Disp(Seg_Pos, ucLed[Seg_Pos]); // 刷新当前位 LED
}
动态扫描原理:
-
每 1ms 刷新一位数码管,8位轮一圈需 8ms
-
刷新频率 = 125Hz,人眼看起来是同时亮的(视觉暂留)
-
同时复用
Seg_Pos控制 LED,因为 LED 和数码管位选共用 P2 控制结构
减速计数器(Rate Limiting):
变量 计数上限 实际频率 Key_Slow_Down 10 → 10ms 执行一次 Key_Proc() Seg_Slow_Down 500 → 500ms 执行一次 Seg_Proc()
四、程序执行流程
上电 │ ├─ rd_temperature() ← 触发一次温度转换(丢弃结果) ├─ Delay750ms() ← 等待转换完成,避免读到85°C ├─ System_Init() ← 初始化 LED 和外设为关闭状态 ├─ Timer0Init() ← 启动定时器中断 │ └─ while(1) 主循环 ├─ Key_Proc() ← 每 10ms:读键值,处理按键事件 ├─ Seg_Proc() ← 每 500ms:采集温度,更新数码管缓存 └─ Led_Proc() ← 每次循环:计算 DAC 值并输出,更新 LED 状态 (后台:Timer0 每 1ms 中断一次) ├─ 驱动数码管动态扫描 ├─ 驱动 LED 刷新 └─ 维护减速计数器
五、关键知识点总结(竞赛备考)
5.1 锁存器操作模板
// 向某锁存器输出数据的固定写法: P0 = data; P2 = P2 & 0x1f | 0xX0; // X0 = 对应锁存器地址 P2 &= 0x1f; // 关闭锁存器(锁存数据)
5.2 按键边沿检测模板
Key_Val = Key_Read(); Key_Down = Key_Val & (Key_Old ^ Key_Val); // 按下事件 Key_Up = ~Key_Val & (Key_Old ^ Key_Val); // 松开事件 Key_Old = Key_Val;
5.3 减速计数器模板
// 在中断中:
if(++counter == N) counter = 0;
// 在主循环处理函数中:
void Xxx_Proc() {
if(counter) return; // counter 不为0时跳过(即 N-1 次都跳过)
// 实际处理...
}
效果:每 N ms 执行一次该函数。
5.4 DS18B20 温度读取要点
-
每次读温度需要两次初始化
-
第一次:发转换命令
0xCC + 0x44 -
第二次:发读取命令
0xCC + 0xBE,然后读两字节 -
原始值 ÷ 16.0 = 摄氏温度
-
上电后必须等 750ms 再读,否则返回 85°C
5.5 DAC 输出电压换算
PCF8591:5V 参考电压,8位分辨率 DAC 数值 = 目标电压 × 255 / 5 = 目标电压 × 51
更多推荐



所有评论(0)