蓝桥杯单片机学习笔记(十三):V2026 大模板构建(下)
温馨提示 :本文内容源自米醋电子工作室培训课,由本人学习、整理并补充说明。
本篇是在"V2026 大模板构建(上)"的基础上继续扩展,重点整理 DS1302 时钟、PCF8591、EEPROM、超声波和串口等模块。为了保持你的原始学习痕迹,本文不改变原代码的主体逻辑,只对讲解文字进行润色,并在代码中适当添加注释;个别容易导致编译错误或理解偏差的地方,会在正文中单独用"注意"说明。
如果说上一篇是在搭建模板的"骨架"------LED、数码管、按键、NE555、PWM、温度这些基础模块,那么本篇更像是在给模板安装"器官"和"神经系统":
- DS1302 像一块电子手表,负责记住时间和日期;
- PCF8591 像一个"模拟量翻译官",把光敏、电位器这些模拟信号翻译成单片机能处理的数字量;
- EEPROM 像一个小本子,断电以后也能记住数据;
- 超声波像一把电子尺,用声音的往返时间测距离;
- 串口像一条对外通信的电话线,让单片机能和电脑或其他设备交流。
这些模块在国赛题目中经常会和显示、按键、定时器混合出现。真正难的地方不只是"会写某一个函数",而是能在模板中把它们安排得不打架、不阻塞、不互相干扰。
目录
- 一、前言:本篇为什么重要
- [二、DS1302 时钟底层](#二、DS1302 时钟底层)
- [1. 时间写入函数](#1. 时间写入函数)
- [2. 时间读取函数:两次读取防跳变](#2. 时间读取函数:两次读取防跳变)
- [3. BCD 码与十进制的转换](#3. BCD 码与十进制的转换)
- [4. 12/24 小时制切换](#4. 12/24 小时制切换)
- [5. 读取函数与 12/24 小时制的配合](#5. 读取函数与 12/24 小时制的配合)
- [6. 年、月、日、周的写入与读取](#6. 年、月、日、周的写入与读取)
- [三、PCF8591 芯片底层](#三、PCF8591 芯片底层)
- [1. PCF8591 的作用](#1. PCF8591 的作用)
- [2. AD 读取函数](#2. AD 读取函数)
- [3. DA 输出函数](#3. DA 输出函数)
- [4. 使用时的易错点](#4. 使用时的易错点)
- [四、EEPROM 底层](#四、EEPROM 底层)
- [1. 为什么需要 EEPROM](#1. 为什么需要 EEPROM)
- [2. 5ms 延时函数](#2. 5ms 延时函数)
- [3. EEPROM 写入函数](#3. EEPROM 写入函数)
- [4. EEPROM 读取函数](#4. EEPROM 读取函数)
- [5. 为什么读写 EEPROM 时建议关闭中断](#5. 为什么读写 EEPROM 时建议关闭中断)
- 五、超声波底层
- [1. 超声波测距的基本原理](#1. 超声波测距的基本原理)
- [2. 10us 触发脉冲](#2. 10us 触发脉冲)
- [3. 距离读取函数](#3. 距离读取函数)
- [4. 超声波模块的易错点](#4. 超声波模块的易错点)
- 六、串口通信
- [1. 串口为什么对频率要求严格](#1. 串口为什么对频率要求严格)
- [2. 串口初始化函数](#2. 串口初始化函数)
- [3. printf 重定向](#3. printf 重定向)
- [4. main.c 中的串口变量](#4. main.c 中的串口变量)
- [5. 串口接收中断函数](#5. 串口接收中断函数)
- [6. 定时器中增加 uart_tick](#6. 定时器中增加 uart_tick)
- [7. 串口处理函数](#7. 串口处理函数)
- [8. 串口模块的常见错误](#8. 串口模块的常见错误)
- [七、本篇小结:V2026 下半部分模板的核心思想](#七、本篇小结:V2026 下半部分模板的核心思想)
- 八、学习建议:如何把这些模块真正用熟
一、前言:本篇为什么重要
上一篇模板更偏向"基础显示与输入":数码管怎么亮、LED 怎么控制、按键怎么读取、NE555 怎么测频、DS18B20 怎么读温度。它们像是一台机器的外壳、按钮和仪表盘。
本篇继续向外设深处走,主要解决三个问题:
- 时间类数据怎么保存和读取:比如时、分、秒,年、月、日、周;
- 模拟量和非易失性数据怎么处理:比如光敏电阻、滑动变阻器、DAC 输出、EEPROM 读写;
- 复杂外设怎么与主循环配合:比如超声波测距和串口接收。
在蓝桥杯单片机国赛中,题目往往不会单独考"请你写一个 DS1302 读取函数"这么简单,而是会把多个模块组合起来。例如:
- 按键设置时间,DS1302 保存时间,数码管显示时间;
- 光敏电阻采集电压,经过阈值判断后控制继电器和 LED;
- EEPROM 保存参数,重新上电后还能恢复上一次设置;
- 串口发送指令,单片机接收后修改显示模式;
- 超声波检测距离,距离过近时蜂鸣器报警。
所以,本篇的学习重点不是机械背代码,而是理解每个模块在模板中的"位置"。可以把整个大模板想象成一个城市:
- 定时器是城市里的节拍钟,负责让所有部门按时工作;
- 主循环是市政府,每一轮都检查各个任务是否需要处理;
- 中断像紧急电话,发生特殊情况时立刻打断当前流程;
- 外设底层函数像各个办事窗口,专门负责某一类硬件;
- 显示函数像公告栏,把结果展示给用户。
只有知道每个部门负责什么,写比赛程序时才不会把所有逻辑都堆成一团。
二、DS1302 时钟底层
DS1302 是蓝桥杯开发板中常见的实时时钟芯片。它可以记录时、分、秒,也可以记录年、月、日、星期。简单理解,它就像开发板上的一块"小手表"。
单片机本身虽然可以用定时器计时,但如果单片机复位、断电或者程序重启,普通变量里的时间就会丢失。而 DS1302 这种 RTC 芯片可以在外部电池或备用电源支持下继续走时,因此更适合做"真实时间"的保存。
在模板中,DS1302 主要有三类函数:
| 函数类别 | 作用 | 可以理解成 |
|---|---|---|
| 时间写入 | 把时、分、秒写入 DS1302 | 给电子表校时 |
| 时间读取 | 从 DS1302 读出时、分、秒 | 看电子表现在几点 |
| 日期读写 | 写入或读取年、月、日、周 | 看日历 |
1. 时间写入函数
时间写入函数整体和之前版本相比变化不大。它的核心思路是:先关闭写保护,再把时、分、秒写到对应寄存器里,最后重新打开写保护。
DS1302 有写保护机制。可以把它想象成一本带锁的记账本:
- 写数据前,要先把锁打开;
- 写完以后,要把锁重新锁上;
- 这样可以避免程序误操作,把时间数据乱改。
c
//时间设置函数
void Rtc_Set(unsigned char *ucRtc)
{
// 关闭写保护,允许写入 DS1302 寄存器
Write_Ds1302_Byte(0x8e,0x00);
// 写入小时寄存器
// ucRtc[0] 为十进制小时,需要转换成 BCD 格式后写入
Write_Ds1302_Byte(0x84,ucRtc[0]/10*16 + ucRtc[0]%10);
// 写入分钟寄存器
Write_Ds1302_Byte(0x82,ucRtc[1]/10*16 + ucRtc[1]%10);
// 写入秒寄存器
Write_Ds1302_Byte(0x80,ucRtc[2]/10*16 + ucRtc[2]%10);
// 打开写保护,防止后续误写
Write_Ds1302_Byte(0x8e,0x80);
}
这里 ucRtc 数组的含义一般是:
| 数组位置 | 含义 | 示例 |
|---|---|---|
ucRtc[0] |
小时 | 13 |
ucRtc[1] |
分钟 | 25 |
ucRtc[2] |
秒 | 25 |
如果写入:
c
unsigned char ucRtc[3] = {13,25,25};
就相当于把 DS1302 的时间设置为 13:25:25。
2. 时间读取函数:两次读取防跳变
在时间读取函数中,V2026 模板采用了和 DS18B20 温度读取类似的思想:连续读取两次,只有两次结果一致时才认为数据稳定。
为什么要这样做?
因为时间是一直在走的。假设你正在读取 13:25:59,刚好在读取过程中秒数跳到了 13:26:00,那么就可能出现小时、分钟、秒不是同一时刻的数据。比如你可能读到:
text
13 : 25 : 00
这个时间看起来合法,但其实是"上一秒的分钟"和"下一秒的秒"拼出来的。就像你拍集体照时,有人刚好动了一下,照片就容易虚影。
两次读取一致的做法,就像连续拍两张照片,如果两张照片完全一样,就说明这一刻比较稳定,可以放心使用。
c
//时间读取函数
void Rtc_Read(unsigned char *ucRtc)
{
unsigned char temp1[3],temp2[3];
do
{
// 第一次读取:时、分、秒
temp1[0] = Read_Ds1302_Byte(0x85);
temp1[1] = Read_Ds1302_Byte(0x83);
temp1[2] = Read_Ds1302_Byte(0x81);
// 第二次读取:时、分、秒
temp2[0] = Read_Ds1302_Byte(0x85);
temp2[1] = Read_Ds1302_Byte(0x83);
temp2[2] = Read_Ds1302_Byte(0x81);
}while(temp1[0] != temp2[0] || temp1[1] != temp2[1] || temp1[2] != temp2[2]);
// BCD 转十进制
ucRtc[0] = temp1[0]/16*10 + temp1[0]%16;
ucRtc[1] = temp1[1]/16*10 + temp1[1]%16;
ucRtc[2] = temp1[2]/16*10 + temp1[2]%16;
}
3. BCD 码与十进制的转换
DS1302 内部保存时间时,通常使用 BCD 码。小白最容易卡在这里。
BCD 可以理解为"用十六进制的形式装十进制的数字"。例如十进制的 25,在 BCD 中不是普通二进制的 25,而是写成:
text
0x25
其中:
- 高四位
2表示十位; - 低四位
5表示个位。
所以,十进制转 BCD 的公式是:
c
十进制数 / 10 * 16 + 十进制数 % 10
例如:
c
25 / 10 * 16 + 25 % 10 = 2 * 16 + 5 = 37 = 0x25
反过来,BCD 转十进制的公式是:
c
BCD数 / 16 * 10 + BCD数 % 16
例如:
c
0x25 / 16 * 10 + 0x25 % 16 = 2 * 10 + 5 = 25
可以把 BCD 想象成一个两层抽屉:左边抽屉放十位,右边抽屉放个位。我们平时用的十进制数字要先拆开放进去,读出来时再重新拼回普通数字。
4. 12/24 小时制切换
在过往模板中,我们通常只使用 24 小时制。例如:
text
00:00 表示凌晨 0 点
13:00 表示下午 1 点
23:59 表示晚上 11 点 59 分
但如果国赛题目要求 12 小时制,比如显示 AM/PM 或要求在 12 小时模式下运行,就需要增加小时制切换函数。
12 小时制可以理解为把一天拆成两个 12 小时:
| 24 小时制 | 12 小时制 |
|---|---|
| 00:00 | 12:00 AM |
| 01:00 | 1:00 AM |
| 12:00 | 12:00 PM |
| 13:00 | 1:00 PM |
| 23:00 | 11:00 PM |
这部分最容易错的地方是 12 点 和 0 点:
00:00不是0 AM,而是12 AM;12:00不是0 PM,而是12 PM;- 下午 1 点到晚上 11 点,需要从 24 小时制中减去 12。
c
//时间进制转换函数
//注意:根据下面代码逻辑,mode == 1 时切换成 12 小时制,mode == 0 时切换成 24 小时制
void Hour_Format(bit mode)
{
unsigned char hour_reg,hour_dec;
unsigned char is_pm = 0;//判断是不是下午
unsigned char is_12 = 0;//判断是否开启12小时制
// 读取小时寄存器内容
hour_reg = Read_Ds1302_Byte(0x85);
// 判断当前 DS1302 是否处于 12 小时制
// bit7 为 1 表示 12 小时制,bit7 为 0 表示 24 小时制
is_12 = (hour_reg & 0x80)?1:0;
if(is_12)//12小时制
{
// 12 小时制下,bit5 用于表示 AM/PM
// bit5 为 1 表示 PM,bit5 为 0 表示 AM
is_pm = (hour_reg & 0x20);
// 取出小时数据。12 小时制下小时十位一般看 bit4
hour_dec = (hour_reg & 0x10)/16*10 + hour_reg%16;
}
else//24小时制
{
// 24 小时制下,小时十位由 bit4、bit5 参与表示
hour_dec = (hour_reg & 0x30)/16*10 + hour_reg%16;
}
// 关闭写保护,准备修改小时寄存器
Write_Ds1302_Byte(0x8e,0x00);
if(mode == 1)//切换成12小时进制
{
if(!is_12)//如果当前不是12进制,就先从 24 小时制换算成 12 小时制
{
if(hour_dec == 0) {hour_dec = 12;is_pm = 0;}//24小时进制的0点,对应 12:xx AM
else if(hour_dec < 12) is_pm = 0;//上午
else if(hour_dec == 12) is_pm = 1;//中午 12 点,对应 12:xx PM
else {hour_dec -= 12;is_pm = 1;}//下午,小时数减去 12
}
// 写入 12 小时制标志、AM/PM 标志和小时 BCD 值
Write_Ds1302_Byte(0x84,0x80 | (is_pm?0x20:0x00) | hour_dec/10*16+hour_dec%10);
//写入(pm/am),真实时间
}
else
{
if(is_12)//12-24
{
if(is_pm)//下午
{
// PM 状态下,除 12 PM 外,其余时间要加 12
if(hour_dec != 12)
hour_dec += 12;
}
else//上午
{
// 12 AM 对应 24 小时制中的 00 点
if(hour_dec == 12)
hour_dec = 0;//12am=00:00
}
}
// 写回 24 小时制格式的小时值
Write_Ds1302_Byte(0x84,hour_dec/10*16 + hour_dec%10);
}
}
易错提醒 :
这段函数里
mode == 1的分支实际是在切换成 12 小时制 ,mode == 0的分支实际是在切换成 24 小时制 。因此,注释最好和代码保持一致,否则后面调用Hour_Format(Rtc_Format)时很容易把模式想反。
5. 读取函数与 12/24 小时制的配合
只会切换小时制还不够,读取函数也要能识别当前 DS1302 是 12 小时制还是 24 小时制。
这是因为两种模式下,小时寄存器中有效位的含义不完全一样:
| 模式 | 关键标志 | 小时数据取法 |
|---|---|---|
| 12 小时制 | bit7 = 1 | 主要取 bit4 和低四位 |
| 24 小时制 | bit7 = 0 | 取 bit5、bit4 和低四位 |
如果读取时不区分模式,就像用同一把钥匙开两把不同的锁,读出来的小时值可能不对。
c
void Rtc_Read(unsigned char *ucRtc)
{
unsigned char temp1[3],temp2[3];
do
{
// 第一次读取时、分、秒
temp1[0] = Read_Ds1302_Byte(0x85);
temp1[1] = Read_Ds1302_Byte(0x83);
temp1[2] = Read_Ds1302_Byte(0x81);
// 第二次读取时、分、秒
temp2[0] = Read_Ds1302_Byte(0x85);
temp2[1] = Read_Ds1302_Byte(0x83);
temp2[2] = Read_Ds1302_Byte(0x81);
}while(temp1[0] != temp2[0] || temp1[1] != temp2[1] || temp1[2] != temp2[2]);
if(temp1[0] & 0x80) //12小时进制
ucRtc[0] = (temp1[0] & 0x10)/16*10 + temp1[0]%16;
else //24小时进制
ucRtc[0] = (temp1[0] & 0x30)/16*10 + temp1[0]%16;
// 分钟和秒钟都是普通 BCD 读取方式
ucRtc[1] = temp1[1]/16*10 + temp1[1]%16;
ucRtc[2] = temp1[2]/16*10 + temp1[2]%16;
}
这段代码的关键是:
c
if(temp1[0] & 0x80)
它是在判断小时寄存器的 bit7。bit7 就像 DS1302 小时寄存器里的"模式标签":
- 标签为 1:当前是 12 小时制;
- 标签为 0:当前是 24 小时制。
补充说明 :
如果题目只要求显示 12 小时制下的小时数字,这样读取小时基本够用。
如果题目还要求显示 AM/PM,则还需要额外读取
temp1[0] & 0x20,用它判断当前是上午还是下午。
6. 年、月、日、周的写入与读取
DS1302 不仅能保存时、分、秒,还能保存年、月、日、周。以前模板中可能很少用这部分,但国赛题目可能会要求显示日期,或者要求在不同日期下执行不同逻辑,因此也需要提前准备。
可以把时、分、秒理解成"钟表",把年、月、日、周理解成"日历"。如果题目要求显示完整时间,例如:
text
26-05-20 13:25:25
那么只写时、分、秒就不够了,还要把日期部分也读出来。
c
//年月日周的写入
void Date_Set(unsigned char *ucRtc)
{
// 关闭写保护
Write_Ds1302_Byte(0x8e,0x00);
// 写入年份。DS1302 通常只存年份后两位,例如 2026 年写入 26
Write_Ds1302_Byte(0x8c,ucRtc[0]/10*16 + ucRtc[0]%10);//年
// 写入月份
Write_Ds1302_Byte(0x88,ucRtc[1]/10*16 + ucRtc[1]%10);//月
// 写入日期
Write_Ds1302_Byte(0x86,ucRtc[2]/10*16 + ucRtc[2]%10);//日
// 写入星期
Write_Ds1302_Byte(0x8a,ucRtc[3]/10*16 + ucRtc[3]%10);//周
// 打开写保护
Write_Ds1302_Byte(0x8e,0x80);
}
//年月日周的读取
void Date_Read(unsigned char *ucRtc)
{
unsigned char temp[4];
// 读取年、月、日、周寄存器
temp[0] = Read_Ds1302_Byte(0x8d);
temp[1] = Read_Ds1302_Byte(0x89);
temp[2] = Read_Ds1302_Byte(0x87);
temp[3] = Read_Ds1302_Byte(0x8b);
// BCD 转十进制
ucRtc[0] = temp[0]/16*10 + temp[0]%16;
ucRtc[1] = temp[1]/16*10 + temp[1]%16;
ucRtc[2] = temp[2]/16*10 + temp[2]%16;
ucRtc[3] = temp[3]/16*10 + temp[3]%16;
}
对于年月日周的读取,和时分秒一样,仍然可以用一个数组来保存:
| 数组位置 | 含义 | 示例 |
|---|---|---|
ucRtc[0] |
年 | 26 |
ucRtc[1] |
月 | 5 |
ucRtc[2] |
日 | 20 |
ucRtc[3] |
周 | 3 |
需要特别注意的是:年份一般只存后两位。
例如:
- 2026 年写入
26; - 2031 年写入
31; - 2099 年写入
99。
它不像电脑系统里的完整年份,不会自动知道这是 2026 还是 1926。因此在显示时,如果想显示完整年份,可以在前面人为补上 20。
补充建议 :
日期读取函数也可以借鉴时间读取函数的思路,连续读取两次,避免刚好跨日、跨月或数据跳变时读到不一致的数据。不过日期变化频率远低于秒数,实际比赛中通常直接读取也够用。
三、PCF8591 芯片底层
PCF8591 是蓝桥杯开发板中常用的 AD/DA 芯片。它的作用可以理解为"翻译官":
- AD:把模拟电压翻译成数字量;
- DA:把数字量翻译成模拟电压。
单片机本身处理的是 0 和 1,但光敏电阻、滑动变阻器输出的是连续变化的电压。PCF8591 就像一个测量员,帮单片机把这些连续变化的电压量化成 0~255 之间的数字。
1. PCF8591 的作用
在蓝桥杯开发板中,PCF8591 常与以下外设相关:
| 控制字 | 常见对应对象 | 说明 |
|---|---|---|
0x41 |
光敏电阻 | 用于读取光照变化 |
0x43 |
滑动变阻器 | 用于读取电位器位置 |
记忆方法 :
可以把
0x41先记成"光",把0x43记成"滑"。实际写题时,一定要结合板子原理图和题目要求确认通道,不要只靠死记。
PCF8591 通过 I2C 总线通信,因此函数中会反复出现这些步骤:
I2CStart():开始通信;I2CSendByte():发送设备地址或控制字;I2CWaitAck():等待应答;I2CReceiveByte():接收数据;I2CSendAck():发送应答或非应答;I2CStop():结束通信。
可以把 I2C 通信想象成打电话:
- Start 是拨号;
- SendByte 是说话;
- Ack 是对方回答"听到了";
- ReceiveByte 是听对方说话;
- Stop 是挂电话。
2. AD 读取函数
PCF8591 的 AD 读取函数用于读取光敏电阻或滑动变阻器的值。你的模板中加入了 do...while 机制:连续读取两次,只有两次值相同才返回。
这样做的目的和前面 DS1302、DS18B20 类似,都是为了减少跳变影响。因为模拟量本身可能有抖动,例如光照变化、电位器接触不稳、电源干扰等,都可能让读数在相邻两次之间轻微变化。
c
//AD读取函数
unsigned char Ad_Read(unsigned char addr)
{
unsigned char temp1,temp2;
do
{
// 第一次读取
I2CStart();
I2CSendByte(0x90); // 发送 PCF8591 写地址
I2CWaitAck();
I2CSendByte(addr); // 发送通道控制字,例如 0x41 或 0x43
I2CWaitAck();
I2CStart();
I2CSendByte(0x91); // 发送 PCF8591 读地址
I2CWaitAck();
temp1 = I2CReceiveByte();// 接收第一次 AD 转换结果
I2CSendAck(1); // 发送非应答,表示本次读取结束
I2CStop();
// 第二次读取
I2CStart();
I2CSendByte(0x90);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStart();
I2CSendByte(0x91);
I2CWaitAck();
temp2 = I2CReceiveByte();// 接收第二次 AD 转换结果
I2CSendAck(1);
I2CStop();
}while(temp1 != temp2);
return temp1;
}
这段代码的结构可以理解为:
text
读一次 → 再读一次 → 比较两次是否相同 → 相同才返回
就像你用电子秤称东西,如果第一次显示 100g,第二次也显示 100g,你就比较放心;如果第一次 100g,第二次 101g,你可能会再称一次。
补充提醒 :
模拟量天然可能轻微波动。如果外部电压一直在变化,
while(temp1 != temp2)可能会让函数等待较久。比赛中如果发现程序卡顿,可以考虑改成"允许误差范围"或者"读取多次取平均值"的方法。本文不修改原代码,只说明这个潜在问题。
3. DA 输出函数
DA 输出函数的作用是把一个 0~255 的数字写给 PCF8591,让它输出对应的模拟电压。
如果把 AD 看成"电压变数字",那么 DA 就是"数字变电压"。它像一个音量旋钮:数字越大,输出电压越高;数字越小,输出电压越低。
c
//DA写入函数
void Da_Write(unsigned char dat)
{
I2CStart();
I2CSendByte(0x90); // 发送 PCF8591 写地址
I2CWaitAck();
I2CSendByte(0x41); // 发送控制字,常用于打开 DA 输出并选择通道
I2CWaitAck();
I2CSendByte(dat); // 写入 DA 输出数据,范围通常为 0~255
I2CWaitAck();
I2CStop();
}
在主程序中,常见换算方式是:
c
Da_Write(Da_Out_100x / 100.0f * 51);
如果 Da_Out_100x 表示放大 100 倍后的电压值,例如:
text
250 表示 2.50V
那么:
text
2.50V × 51 ≈ 127.5
因为 PCF8591 的 8 位 DA 输出范围通常是 0~255,对应 0~5V,所以每 1V 大约对应 51 个数字量。
4. 使用时的易错点
PCF8591 模块看起来代码不长,但很容易出现"读数不对"的问题。常见原因如下:
1. 通道控制字写错
0x41 和 0x43 对应的输入对象不同。如果题目要求读取滑动变阻器,你却读了光敏电阻,就会发现显示值怎么调都不对。
2. 没有理解 AD 值和电压的关系
AD 读出来的是 0~255 的数字,不是直接的电压。如果要转换成电压,一般可以这样理解:
text
电压 ≈ AD值 / 255 × 5V
在模板中常用简化写法:
c
Ad1_100x = Ad_Read(0x41) / 51.0f * 100;
这里 51.0f 的来源就是:
text
255 / 5 = 51
因此,Ad_Read(...) / 51.0f 得到大约多少伏,再乘 100 变成整数形式,方便数码管显示两位小数。
3. I2C 时序被打断
如果 I2C 正在通信时被中断打断,某些情况下可能会出现数据异常。尤其是 EEPROM 写入时更明显,PCF8591 一般影响较小,但仍然要注意程序结构不要过度阻塞。
四、EEPROM 底层
EEPROM 是一种掉电不丢失的数据存储器。可以把它想象成单片机旁边的一本"小笔记本":
- 普通变量像草稿纸,断电就没了;
- EEPROM 像笔记本,写进去以后断电也能保留。
在蓝桥杯题目中,EEPROM 常用于保存参数,例如:
- 保存阈值;
- 保存用户设置;
- 保存计数值;
- 保存上一次运行状态;
- 上电后恢复默认或历史数据。
1. 为什么需要 EEPROM
假设题目要求你通过按键设置一个报警阈值,设置完成后即使单片机断电,下次上电也要保留这个阈值。此时如果只用普通变量,例如:
c
unsigned char threshold = 30;
那么断电以后这个变量就会回到初始值,无法记住用户修改。
这时就要使用 EEPROM。它就像把设置写进了本子里,下次开机再从本子里读出来。
2. 5ms 延时函数
以前模板中,为了保证 EEPROM 写入时序,可能会使用多个 I2C_Delay(255)。新版模板中,你使用 STC-ISP 生成了一个 5ms 延时函数。这样写更清晰,也更容易看懂。
c
//延时函数
void Delay5ms(void) //@12.000MHz
{
unsigned char data i, j;
i = 59;
j = 90;
do
{
while (--j);
} while (--i);
}
这里的延时函数不是随便写的,而是根据晶振频率生成的。注释中的 @12.000MHz 表示这个延时是在 12MHz 晶振条件下计算出来的。
注意 :
如果你的工程使用的系统频率不是 12MHz,这个延时时间就可能不再准确。就像同样数 100 下,有人说话快,有人说话慢,实际经过的时间不一样。
3. EEPROM 写入函数
EEPROM 写入函数有三个参数:
| 参数 | 含义 | 举例 |
|---|---|---|
str |
要写入的数据数组 | EEPROM_write1 |
addr |
写入起始地址 | 0 |
num |
写入数据数量 | 8 |
c
//EEPROM写入函数
//str:写入数据的数组,addr:写入数组的首地址,num:写入数据的数量
void EEPROM_Write(unsigned char *str,unsigned char addr,unsigned char num)
{
I2CStart();
I2CSendByte(0xa0); // 发送 EEPROM 写地址
I2CWaitAck();
I2CSendByte(addr); // 发送要写入的起始地址
I2CWaitAck();
while(num--)
{
I2CSendByte(*str++); // 依次发送数组中的数据
I2CWaitAck();
I2C_Delay(200); // 字节之间做适当延时,保证时序稳定
}
I2CStop();
Delay5ms(); // 写入完成后等待 EEPROM 内部写周期结束
}
这段代码可以理解为:
text
打开通信 → 告诉 EEPROM 我要写 → 告诉它从哪里开始写 → 一个字节一个字节送过去 → 结束通信 → 等它写完
就像你把一串数字抄到本子上:
- 先翻开本子;
- 找到第几页第几行;
- 一个数字一个数字写进去;
- 合上本子;
- 等墨水干一下。
Delay5ms() 就相当于"等墨水干"。如果刚写完马上读,有可能 EEPROM 内部还没真正写完,读出来的数据就可能不稳定。
补充提醒 :
EEPROM 通常有页写入限制。蓝桥杯常见用法中,一次从地址 0 写入 8 个字节问题不大;如果以后写入更多数据,要注意不要跨页写入导致地址回卷。简单说,不要以为
num可以无限大地一直写下去。
4. EEPROM 读取函数
EEPROM 读取函数整体与之前模板相比变化不大。读取时也有三个参数:
| 参数 | 含义 |
|---|---|
str |
读取出来后存放到哪里 |
addr |
从 EEPROM 哪个地址开始读 |
num |
读取多少个字节 |
c
//EEPROM读取函数
void EEPROM_Read(unsigned char *str,unsigned char addr,unsigned char num)
{
I2CStart();
I2CSendByte(0xa0); // 先发送写地址,用于指定内部存储地址
I2CWaitAck();
I2CSendByte(addr); // 告诉 EEPROM 从哪个地址开始读
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1); // 再发送读地址,开始读取数据
I2CWaitAck();
while(num--)
{
*str++ = I2CReceiveByte(); // 接收一个字节,并存入数组
if(num)
I2CSendAck(0); // 还要继续读,发送应答
else
I2CSendAck(1); // 最后一个字节,发送非应答
}
I2CStop();
}
这段代码有一个细节:读取前先发送了 0xa0 和 addr,然后又重新开始并发送 0xa1。这是因为 EEPROM 需要先知道"你要从哪里读",然后才开始把数据传给单片机。
可以类比成你去图书馆借书:
- 先告诉管理员你要第几号书架;
- 管理员定位到对应位置;
- 然后你再开始把书一本本拿出来。
5. 为什么读写 EEPROM 时建议关闭中断
在执行 EEPROM 的读写操作时,建议关一下总中断:
c
EA = 0;//关中断
EEPROM_Read(EEPROM_read,0,8);//执行操作
EA = 1;//开中断
原因是 EEPROM 通信依赖 I2C 时序,而 I2C 时序本质上是通过程序一步一步"模拟"出来的。如果正在发送一位数据时,定时器中断突然进来,可能会打乱通信节奏。
这就像两个人正在按节奏敲摩斯电码,突然有人插进来讲话,接收方就可能听错。
因此,在 EEPROM 读写这类比较敏感的 I2C 操作中,临时关闭中断可以提高稳定性。
但也要注意,关闭中断的时间不宜过长。因为关闭中断期间:
- 数码管动态扫描会暂停;
- PWM 输出会暂停;
- 定时器计数类变量不会更新;
- 串口接收可能错过数据。
所以,EEPROM 读写应尽量短小,不要在关中断期间做大量无关操作。
五、超声波底层
超声波模块是新模板中变化比较大的部分。它的功能是测量距离,常见思路是:发出一个超声波脉冲,然后等待回波返回,通过时间差计算距离。
可以把它理解成"对着墙喊一声,然后听回声"。
- 离墙越近,回声回来得越快;
- 离墙越远,回声回来得越慢;
- 如果很久都没听到回声,就认为超出测量范围。
1. 超声波测距的基本原理
超声波测距的核心公式是:
text
距离 = 声速 × 时间 / 2
为什么要除以 2?
因为超声波走的是一个来回:
text
模块 → 障碍物 → 模块
计时得到的是往返时间,而我们需要的是单程距离,所以要除以 2。
在空气中,声速大约是:
text
340 m/s = 34000 cm/s = 0.034 cm/us
因此:
text
距离 = 时间 × 0.034 / 2 = 时间 × 0.017
这也是代码中使用 time * 0.017 的原因。
2. 10us 触发脉冲
超声波模块一般需要一个至少 10us 的触发脉冲。你的模板中使用 STC-ISP 生成了一个 10us 延时函数,然后通过 us_tx 引脚发出触发信号。
触发过程可以理解为按一下门铃:
us_tx = 1:按下门铃;Delay10us():保持按住 10us;us_tx = 0:松开门铃;- 模块开始发射超声波。
3. 距离读取函数
c
unsigned int time;
unsigned int ultrasound_read()
{
CMOD = 0x00; // 设置 PCA 相关模式,使用前先清理配置
CH=CL=0; // 清零计数高低字节
us_tx = 1; // 给超声波模块触发端一个高电平
Delay10us(); // 保持 10us
us_tx = 0; // 拉低触发端,完成触发脉冲
CR=1; // 启动 PCA 计数
while((us_rx==1)&&(CF==0)); // 等待回波结束,或者等待计数溢出
CR=0; // 停止计数
if(CF==0)
{
time=CH<<8 |CL; // 合成计数值
//340m/s=34000cm/s=0.034cm/us
//s=t*v/2=0.034*t/2=0.017*t
return time*0.017;
}
else
{
CF = 0; // 清除溢出标志
return 999; // 超时或超量程时返回 999
}
}
这段代码大致可以拆成五步:
第一步:清空计数器
c
CH=CL=0;
这一步像把秒表归零。每次测距前,都要从 0 开始计时。
第二步:发送触发脉冲
c
us_tx = 1;
Delay10us();
us_tx = 0;
这一步像对着前方"喊一声"。
第三步:启动计时
c
CR=1;
这一步像按下秒表开始按钮。
第四步:等待回波结束或计数溢出
c
while((us_rx==1)&&(CF==0));
这一步像一直听回声。如果 us_rx 仍然处于回波状态,并且计数器还没溢出,就继续等待。
第五步:计算距离
c
time=CH<<8 |CL;
return time*0.017;
把计数结果换算成距离。
补充提醒 :
time * 0.017中使用了小数,最终函数返回类型是unsigned int,因此小数部分会被舍去。如果题目要求更高精度,可以考虑把距离放大 10 倍或 100 倍保存。不过本文不改变原代码,只说明这个精度问题。
4. 超声波模块的易错点
1. 引脚方向和接线错误
超声波模块一般有触发端和回波端。如果 us_tx 和 us_rx 接反,程序逻辑再正确也测不出来。
2. 没有清零计数器
如果每次测距前不清零 CH 和 CL,本次测量会叠加上一次残留值,就像秒表没有归零,结果一定不准。
3. 没有处理超时
如果没有障碍物或者距离太远,回波可能一直等不到。代码中通过 CF 溢出标志处理超时,并返回 999,这是一种比较实用的保护方式。
4. 测距函数可能阻塞主循环
while((us_rx==1)&&(CF==0)); 是等待式写法。如果回波时间较长,主循环会暂时卡在这里。比赛中如果测距频率不高,一般可以接受;但如果同时还要处理串口、按键、显示,就要注意不要过于频繁调用超声波读取函数。
六、串口通信
串口是本篇最后一个模块,也是很容易出错的模块。它的作用是让单片机和电脑或其他设备进行数据交换。
可以把串口想象成两个人用固定语速打电话:
- 波特率就是说话速度;
- 数据位就是每句话的内容长度;
- 起始位和停止位像一句话的开头和结尾;
- 如果双方说话速度不一致,就会听错。
1. 串口为什么对频率要求严格
串口通信最怕"节拍不准"。比如电脑按 9600bps 的速度发送数据,而单片机的波特率实际偏差太大,就会导致接收乱码。
这就像两个人约定每秒说 10 个字,但一个人说得太快,另一个人听不清;一个人说得太慢,另一个人又会把停顿误认为结束。
所以,串口初始化函数最好用 STC-ISP 按照实际晶振频率生成,并且要确认:
- 晶振频率是否填对;
- 波特率是否和上位机一致;
- 是否勾选串口中断;
- 定时器资源是否与其他模块冲突。
2. 串口初始化函数
下面是 9600bps、12MHz 条件下的串口初始化函数。这里选择定时器 2 作为波特率发生器。
c
#include "Uart.h"
#include <stdio.h>
void Uart1_Init(void) //9600bps@12.000MHz
{
SCON = 0x50; //8位数据,可变波特率,允许接收
AUXR |= 0x01; //串口1选择定时器2为波特率发生器
AUXR &= 0xFB; //定时器时钟12T模式
T2L = 0xE6; //设置定时初始值低字节
T2H = 0xFF; //设置定时初始值高字节
AUXR |= 0x10; //定时器2开始计时
ES = 1; //使能串口1中断
EA = 1; //打开总中断
}
这段代码中的几个关键寄存器可以这样理解:
| 寄存器/位 | 作用 | 类比 |
|---|---|---|
SCON |
设置串口工作方式 | 规定电话怎么说话 |
AUXR |
选择波特率发生器和时钟模式 | 选择节拍器 |
T2L/T2H |
定时器 2 初值 | 设定节拍快慢 |
ES |
串口中断开关 | 允许电话响铃提醒 |
EA |
总中断开关 | 总电源开关 |
3. printf 重定向
为了能使用 printf() 通过串口输出,需要重定向 putchar()。否则 C 标准库不知道应该把字符输出到哪里。
c
extern char putchar(char ch)
{
SBUF = ch; // 把要发送的字符写入串口缓冲寄存器
while(TI == 0); // 等待发送完成
TI = 0; // 清除发送完成标志
return ch;
}
这段代码的含义是:
- 把一个字符放进
SBUF; - 等待硬件把它发出去;
- 发送完成后,
TI会置 1; - 程序清除
TI,准备发送下一个字符。
可以把 SBUF 想象成邮筒,TI 是邮递员贴在邮筒上的"已寄出"标签。只有看到"已寄出",我们才能继续投下一封信。
格式提醒 :
这段代码中
while(TI == 0);后面紧跟的TI = 0;缩进容易让人误解。由于while后面有分号,循环体其实是空语句,TI = 0;是在等待结束后执行的。为了可读性,实际写工程时可以写得更清楚一些,但本文不改变你的原代码主体。
4. main.c 中的串口变量
在 main.c 中,需要先定义串口接收相关变量:
c
pdata unsigned char uart_data[30] = {0};
pdata unsigned char uart_index = 0;
pdata unsigned char uart_tick = 0;
这三个变量的作用如下:
| 变量 | 作用 | 类比 |
|---|---|---|
uart_data[30] |
存放接收到的数据 | 临时收件箱 |
uart_index |
当前写到数组的第几个位置 | 收件箱里的插入位置 |
uart_tick |
记录多久没有收到新数据 | 安静计时器 |
串口接收通常是一个字符一个字符来的。比如电脑发送:
text
HELLO
单片机不是一次收到整个单词,而是可能依次收到:
text
H → E → L → L → O
所以需要一个数组把这些字符先存起来,等一段时间没有新字符了,再认为这一帧数据接收完成。
5. 串口接收中断函数
串口接收中断的作用是:每当收到一个字节,立刻把它存入缓冲区。
c
//串口中断处理函数
void Uart1_Isr(void) interrupt 4
{
if (RI) //检测串口1接收中断
{
uart_tick = 0;//连续10秒没接收到信息,就说明接收完了
uart_data[uart_index++] = SBUF;
RI = 0; //清除串口1接收中断请求位
}
}
这段代码可以理解成:
text
电话响了 → 拿起电话听一个字 → 放进本子里 → 清除响铃标志
其中:
RI是接收完成标志;SBUF中存放刚收到的字节;uart_data[uart_index++]表示先存入当前下标,再把下标加 1;uart_tick = 0表示只要收到新数据,就重新开始计时。
文字提醒 :
注释里写"连续10秒没接收到信息"容易引起误解。如果定时器 1 是 1ms 中断一次,
uart_tick < 10实际通常表示 10ms 左右,不是 10 秒。更准确的说法是:"连续一小段时间没有接收到新字符,就认为一帧数据接收完成。"
安全提醒 :这段代码没有判断
uart_index是否超过uart_data[30]的范围。如果电脑连续发送很多字符,可能导致数组越界。正式工程中可以加边界判断。本文为了遵守"不修改代码主体"的要求,只在这里提醒。
6. 定时器中增加 uart_tick
为了判断一帧串口数据是否接收完成,需要在定时器中断中不断增加 uart_tick。
c
void Timer1_Isr(void) interrupt 3
{
Seg_Slow_Down++;
Key_Slow_Down++;
uart_tick++;
}
这就像有一个秒表一直在走。每收到一个新字符,秒表归零;如果秒表走到一定值还没有新字符,就说明这一帧数据大概率接收完了。
这种做法叫"空闲超时判断"。它不依赖固定结束符,而是通过接收间隔判断数据结束。
适用场景:
- 上位机发送不定长字符串;
- 没有明确结束符;
- 简单模板中想快速判断一帧数据。
不适合场景:
- 串口数据中间可能有较长停顿;
- 数据量很大;
- 对协议完整性要求很高;
- 必须严格按帧头、帧尾、校验位解析。
7. 串口处理函数
串口处理函数放在主循环中执行。它不会每次都处理数据,而是先判断:
- 有没有接收到数据;
- 是否已经一段时间没有新数据;
- 如果满足条件,就输出接收到的字符串,并清空缓冲区。
c
//串口处理函数
void uart_proc()
{
if(uart_index == 0) return;//没接收到东西直接返回
if(uart_tick < 10) return;
printf("%s",uart_data);
memset(uart_data,0,uart_index);
uart_index = 0;
}
这段代码可以理解成快递站处理包裹:
uart_index == 0:一个包裹都没有,直接休息;uart_tick < 10:可能还有包裹正在路上,先等等;printf("%s", uart_data):把收到的数据打印出去;memset(...):清空收件箱;uart_index = 0:下次从头开始放。
重要提醒 :
printf("%s", uart_data);要求uart_data是以\0结尾的字符串。由于数组初始化为 0,且处理后会清零,在短数据场景下通常能正常工作。但如果接收数据刚好填满数组,或者没有及时清零,就可能出现字符串结束位置不明确的问题。正式工程中可以在输出前手动补一个\0。
8. 串口模块的常见错误
串口模块很常见,但也是最容易因为小细节报错的地方。
1. %s 写成 &s
字符串输出应该写:
c
printf("%s",uart_data);
不能写成:
c
printf("&s",uart_data);
%s 是格式控制符,表示输出字符串;&s 只是普通字符,不会把 uart_data 当字符串输出。
2. 变量名拼写错误
比如定义的是:
c
uart_index
但使用时写成:
c
uart_idnex
编译器就会报:
text
undefined identifier
这类错误不难,但很浪费时间。建议所有变量名都用统一规则,比如:
uart_data:串口数据;uart_index:串口下标;uart_tick:串口超时计数。
3. 中文标点混入代码
例如:
c
uart_proc();
这里的分号是中文全角 ;,不是英文半角 ;。Keil C51 无法识别,会报非法字符或语法错误。
正确写法是:
c
uart_proc();
在单片机 C 语言里,标点符号一定要用英文半角。中文分号、中文括号、中文逗号都可能导致编译失败。
4. 缓冲区越界
如果 uart_data 只有 30 个字节,但外部发送了超过 30 个字符,而代码没有限制 uart_index,就可能写出数组范围。
这就像一个只有 30 格的收纳盒,你却硬塞第 31 个东西进去,后面的内存数据就可能被覆盖。
5. 中断函数里做太多事
串口接收中断里最好只做"存数据、清标志"这类简单操作,不建议在中断里直接 printf() 或做复杂解析。中断函数越短,系统越稳定。
七、本篇小结:V2026 下半部分模板的核心思想
本篇主要补齐了 V2026 大模板中偏"外设扩展"的部分。整体来看,这一部分的核心思想可以概括为一句话:
让单片机不仅能显示和响应按键,还能记时间、读模拟量、存数据、测距离、做通信。
1. DS1302 模块
DS1302 像开发板上的电子手表。学习重点是:
- 写时间前要关闭写保护;
- 写完时间后要打开写保护;
- 时分秒使用 BCD 格式;
- 读取时可以连续读两次防止跳变;
- 12/24 小时制切换时要特别注意 0 点和 12 点;
- 年份一般只保存后两位。
2. PCF8591 模块
PCF8591 像模拟量和数字量之间的翻译官。学习重点是:
0x41和0x43不要弄混;- AD 读取结果是 0~255,不是直接电压;
- 需要换算成电压时,可用
AD / 51.0近似; - DA 输出也是 0~255 对应大约 0~5V;
- 模拟量可能跳变,读取时要考虑稳定性。
3. EEPROM 模块
EEPROM 像掉电不丢失的小笔记本。学习重点是:
- 写入前要指定地址;
- 写入后要等待内部写周期;
- 读写过程中建议关闭中断;
- 一次写入不要随意跨页;
- 保存参数时要规划好每个地址存什么。
4. 超声波模块
超声波像一把用声音测距离的尺子。学习重点是:
- 触发信号一般需要 10us 高电平;
- 计时前要清零计数器;
- 距离计算要除以 2,因为声音走了往返;
- 超时要返回特殊值,避免程序死等;
- 测距函数可能阻塞主循环,不宜调用太频繁。
5. 串口模块
串口像单片机和电脑之间的电话线。学习重点是:
- 波特率必须和上位机一致;
- 初始化函数要匹配实际晶振;
- 接收中断里只做简单存储;
- 主循环中再处理完整数据;
%s、变量名、英文分号这些细节不能错;- 接收缓冲区要注意越界问题。
八、学习建议:如何把这些模块真正用熟
学习蓝桥杯单片机模板,最忌讳的是只背函数,不理解模块之间的关系。模板不是一堆孤立的代码,而是一套可以拼装的工具箱。
建议按照下面顺序练习:
1. 先单独跑通每个底层
不要一开始就把 DS1302、PCF8591、EEPROM、超声波、串口全部放进一个工程。这样一旦出错,很难判断是哪一块的问题。
建议先分别测试:
- DS1302:能否正确显示时分秒;
- 日期读取:能否显示年、月、日、周;
- PCF8591:转动电位器,数码管是否变化;
- EEPROM:写入数据后断电重启,能否读回;
- 超声波:改变距离,显示值是否跟着变化;
- 串口:电脑发送字符,单片机能否接收并回显。
2. 再练习"按键 + 外设"组合
单独跑通之后,再加入按键控制。例如:
- 按 S4 切换显示时间和日期;
- 按 S5 保存当前阈值到 EEPROM;
- 按 S6 从 EEPROM 读取阈值;
- 按 S7 切换 12/24 小时制;
- 按 S8 让串口输出当前传感器数据。
这样可以训练你把底层函数接入主逻辑的能力。
3. 最后练习"定时器调度"
复杂题目中,很多任务不能随便放在 while(1) 里一直执行。比如:
- 数码管要高频刷新;
- 按键要低频扫描;
- 温度读取不能太频繁;
- 超声波测距不能一直阻塞;
- EEPROM 写入不能反复执行;
- 串口接收需要中断配合。
因此要养成"分频调度"的思维:
| 任务 | 建议处理方式 |
|---|---|
| 数码管动态扫描 | 定时器中断中快速刷新 |
| PWM | 定时器中断中稳定执行 |
| 按键扫描 | 主循环中低频执行 |
| 传感器读取 | 主循环中定时执行 |
| EEPROM 写入 | 按键触发或条件触发,避免频繁写 |
| 串口接收 | 中断接收,主循环处理 |
4. 遇到错误时按模块排查
如果程序运行异常,不要一上来就怀疑所有代码。可以按下面思路排查:
- 编译错误:先看变量名、分号、括号、中文标点;
- 显示异常:先看数码管缓冲区和动态扫描;
- 按键异常 :先看
Key_Read()、Key_Down、变量类型; - 时间异常:先看 BCD 转换和 DS1302 写保护;
- AD 异常:先看通道控制字和电压换算;
- EEPROM 异常:先看是否关闭中断、是否等待写周期;
- 超声波异常:先看引脚、触发脉冲、计数器清零;
- 串口异常 :先看波特率、晶振、
printf重定向和中断开关。
九、常见易错点汇总表
| 模块 | 易错点 | 后果 | 建议 |
|---|---|---|---|
| DS1302 | do...while 后少分号 |
编译报错 | 结尾补英文 ; |
| DS1302 | BCD 当普通十进制处理 | 时间显示错误 | 写入和读取都要转换 |
| DS1302 | 12/24 小时制模式注释写反 | 调用逻辑混乱 | 以代码分支为准重新核对 |
| 日期 | 年份只存两位 | 显示完整年份时缺少前缀 | 显示时人为补 20 |
| PCF8591 | 0x41、0x43 弄混 |
读错传感器 | 看题目和原理图确认 |
| PCF8591 | 模拟量严格要求两次完全相同 | 可能等待较久 | 必要时改成取平均或容差判断 |
| EEPROM | 写完马上读 | 数据可能没写完 | 写后延时 5ms 左右 |
| EEPROM | 读写时被中断打断 | 数据异常 | 读写时短暂关闭中断 |
| 超声波 | 未清零计数器 | 距离错误 | 每次测距前清 CH、CL |
| 超声波 | 等待回波阻塞 | 主循环卡顿 | 降低调用频率或加超时 |
| 串口 | 波特率不匹配 | 接收乱码 | 用 STC-ISP 按实际晶振生成 |
| 串口 | %s 写成 &s |
无法正确输出字符串 | 使用 printf("%s", data) |
| 串口 | 中文分号混入 | Keil 报非法字符 | 全部使用英文半角符号 |
| 串口 | 缓冲区不限制长度 | 数组越界 | 正式工程中加边界判断 |
十、结语
到这里,V2026 大模板的下半部分就基本整理完成了。相比上篇,本篇涉及的外设更多,通信方式也更复杂:
- DS1302 使用专用时序读写寄存器;
- PCF8591 和 EEPROM 都依赖 I2C;
- 超声波依赖触发、回波和计时;
- 串口依赖波特率、中断和缓冲区。
这些内容看起来分散,但背后的思想其实很统一:
先把底层封装成稳定函数,再在主循环中按任务调用,最后用定时器中断提供统一节拍。
如果把比赛程序比作一场舞台演出,那么:
- 定时器是节拍器;
- 主循环是导演;
- 按键是观众输入;
- 数码管是舞台屏幕;
- DS1302 是后台时钟;
- EEPROM 是记事本;
- PCF8591 是传感器翻译;
- 超声波是测距道具;
- 串口是场外通信通道。
每个模块都不应该抢戏,但每个模块都要在需要时准确登场。真正熟练的模板,不是把所有代码背下来,而是知道:题目一出现某个需求,应该调用哪个模块,改哪个变量,放在哪个调度位置,以及哪些硬件冲突需要提前避开。
后续做综合题时,可以把本文当成一张"外设地图":遇到时间问题看 DS1302,遇到模拟量看 PCF8591,遇到掉电保存看 EEPROM,遇到距离看超声波,遇到上位机通信看串口。这样模板就不是一堆代码,而是一套可以快速组合的比赛工具箱。
最后,我也会将新模板的资源上传到本站,同时出一期新模板使用说明,请放心学习。