蓝桥杯单片机学习笔记(十三) V2026大模板构筑(下)

蓝桥杯单片机学习笔记(十三):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 怎么读温度。它们像是一台机器的外壳、按钮和仪表盘。

本篇继续向外设深处走,主要解决三个问题:

  1. 时间类数据怎么保存和读取:比如时、分、秒,年、月、日、周;
  2. 模拟量和非易失性数据怎么处理:比如光敏电阻、滑动变阻器、DAC 输出、EEPROM 读写;
  3. 复杂外设怎么与主循环配合:比如超声波测距和串口接收。

在蓝桥杯单片机国赛中,题目往往不会单独考"请你写一个 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 总线通信,因此函数中会反复出现这些步骤:

  1. I2CStart():开始通信;
  2. I2CSendByte():发送设备地址或控制字;
  3. I2CWaitAck():等待应答;
  4. I2CReceiveByte():接收数据;
  5. I2CSendAck():发送应答或非应答;
  6. 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. 通道控制字写错

0x410x43 对应的输入对象不同。如果题目要求读取滑动变阻器,你却读了光敏电阻,就会发现显示值怎么调都不对。

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 我要写 → 告诉它从哪里开始写 → 一个字节一个字节送过去 → 结束通信 → 等它写完

就像你把一串数字抄到本子上:

  1. 先翻开本子;
  2. 找到第几页第几行;
  3. 一个数字一个数字写进去;
  4. 合上本子;
  5. 等墨水干一下。

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();
}

这段代码有一个细节:读取前先发送了 0xa0addr,然后又重新开始并发送 0xa1。这是因为 EEPROM 需要先知道"你要从哪里读",然后才开始把数据传给单片机。

可以类比成你去图书馆借书:

  1. 先告诉管理员你要第几号书架;
  2. 管理员定位到对应位置;
  3. 然后你再开始把书一本本拿出来。

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 引脚发出触发信号。

触发过程可以理解为按一下门铃:

  1. us_tx = 1:按下门铃;
  2. Delay10us():保持按住 10us;
  3. us_tx = 0:松开门铃;
  4. 模块开始发射超声波。

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_txus_rx 接反,程序逻辑再正确也测不出来。

2. 没有清零计数器

如果每次测距前不清零 CHCL,本次测量会叠加上一次残留值,就像秒表没有归零,结果一定不准。

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;
}

这段代码的含义是:

  1. 把一个字符放进 SBUF
  2. 等待硬件把它发出去;
  3. 发送完成后,TI 会置 1;
  4. 程序清除 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. 串口处理函数

串口处理函数放在主循环中执行。它不会每次都处理数据,而是先判断:

  1. 有没有接收到数据;
  2. 是否已经一段时间没有新数据;
  3. 如果满足条件,就输出接收到的字符串,并清空缓冲区。
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 像模拟量和数字量之间的翻译官。学习重点是:

  • 0x410x43 不要弄混;
  • 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. 遇到错误时按模块排查

如果程序运行异常,不要一上来就怀疑所有代码。可以按下面思路排查:

  1. 编译错误:先看变量名、分号、括号、中文标点;
  2. 显示异常:先看数码管缓冲区和动态扫描;
  3. 按键异常 :先看 Key_Read()Key_Down、变量类型;
  4. 时间异常:先看 BCD 转换和 DS1302 写保护;
  5. AD 异常:先看通道控制字和电压换算;
  6. EEPROM 异常:先看是否关闭中断、是否等待写周期;
  7. 超声波异常:先看引脚、触发脉冲、计数器清零;
  8. 串口异常 :先看波特率、晶振、printf 重定向和中断开关。

九、常见易错点汇总表

模块 易错点 后果 建议
DS1302 do...while 后少分号 编译报错 结尾补英文 ;
DS1302 BCD 当普通十进制处理 时间显示错误 写入和读取都要转换
DS1302 12/24 小时制模式注释写反 调用逻辑混乱 以代码分支为准重新核对
日期 年份只存两位 显示完整年份时缺少前缀 显示时人为补 20
PCF8591 0x410x43 弄混 读错传感器 看题目和原理图确认
PCF8591 模拟量严格要求两次完全相同 可能等待较久 必要时改成取平均或容差判断
EEPROM 写完马上读 数据可能没写完 写后延时 5ms 左右
EEPROM 读写时被中断打断 数据异常 读写时短暂关闭中断
超声波 未清零计数器 距离错误 每次测距前清 CHCL
超声波 等待回波阻塞 主循环卡顿 降低调用频率或加超时
串口 波特率不匹配 接收乱码 用 STC-ISP 按实际晶振生成
串口 %s 写成 &s 无法正确输出字符串 使用 printf("%s", data)
串口 中文分号混入 Keil 报非法字符 全部使用英文半角符号
串口 缓冲区不限制长度 数组越界 正式工程中加边界判断

十、结语

到这里,V2026 大模板的下半部分就基本整理完成了。相比上篇,本篇涉及的外设更多,通信方式也更复杂:

  • DS1302 使用专用时序读写寄存器;
  • PCF8591 和 EEPROM 都依赖 I2C;
  • 超声波依赖触发、回波和计时;
  • 串口依赖波特率、中断和缓冲区。

这些内容看起来分散,但背后的思想其实很统一:

先把底层封装成稳定函数,再在主循环中按任务调用,最后用定时器中断提供统一节拍。

如果把比赛程序比作一场舞台演出,那么:

  • 定时器是节拍器;
  • 主循环是导演;
  • 按键是观众输入;
  • 数码管是舞台屏幕;
  • DS1302 是后台时钟;
  • EEPROM 是记事本;
  • PCF8591 是传感器翻译;
  • 超声波是测距道具;
  • 串口是场外通信通道。

每个模块都不应该抢戏,但每个模块都要在需要时准确登场。真正熟练的模板,不是把所有代码背下来,而是知道:题目一出现某个需求,应该调用哪个模块,改哪个变量,放在哪个调度位置,以及哪些硬件冲突需要提前避开。

后续做综合题时,可以把本文当成一张"外设地图":遇到时间问题看 DS1302,遇到模拟量看 PCF8591,遇到掉电保存看 EEPROM,遇到距离看超声波,遇到上位机通信看串口。这样模板就不是一堆代码,而是一套可以快速组合的比赛工具箱。

最后,我也会将新模板的资源上传到本站,同时出一期新模板使用说明,请放心学习。

相关推荐
今天背单词了吗98011 小时前
缓存与数据库双写不一致问题及终极解决方案(高频面试题)
java·数据库·学习·缓存
xuhaoyu_cpp_java11 小时前
Git学习(六)
git·学习
冉卓电子11 小时前
MPC5604B/C MC_RGM 复位模块全解
c语言·开发语言·单片机·嵌入式硬件
浅痕~12 小时前
智能知识学习平台
学习
Cat_Rocky12 小时前
k8s 监控平台 Prometheus简单学习
学习·kubernetes·prometheus
qq_5710993512 小时前
学习周报四十六
学习
爱上好庆祝12 小时前
学习JS的第十一天(wed APIs的结束)
学习
minglie112 小时前
j2b描述ethercat
学习
blevoice12 小时前
在杰理AC6966B开发板上TWS开发指南(上):使能与配对配置
单片机·嵌入式硬件·ac6966b蓝牙音响方案·杰理智能音箱开发·杰理ac6965e蓝牙音频开发