code

code 是一个特定的扩展关键字。

它的意思是:将这些数据存储在程序存储器(ROM / Flash)中,而不是数据存储器(RAM)中。

为什么要用 code?

节省宝贵的RAM:51单片机的RAM空间非常小,而ROM空间相对较大。

你代码里的 seg_wei(数码管位选)和 seg_duan(数码管段选)都是查表用的固定数据,在程序运行过程中绝对不会被修改。

EEPROM的底层

//1+2+2
////while+5
//void ee_write(unsigned char *str,unsigned char addr,unsigned char num)
//{
//	EA=0;
//	I2CStart();
//	I2CSendByte(0xa0);
//	I2CWaitAck();
//	
//	I2CSendByte(addr);
//	I2CWaitAck();
//	
//	while(num--)
//	{
//		I2CSendByte(*str++);
//		I2CWaitAck();
//		I2C_Delay(200);
//	}
//	I2CStop();
//	
//	EA=1;
//	I2C_Delay(255);
//	I2C_Delay(255);
//	I2C_Delay(255);
//	I2C_Delay(255);
//}

最好还是保持第一种写法(把 EA=1 放在 I2CStop() 后面,延时函数的前面)。

下面是具体的原理解释:

1. 为什么会有这4个 I2C_Delay(255)?

当你调用 I2CStop() 后,单片机确实已经把数据发给 EEPROM (AT24C02) 了。但是,EEPROM 把接收到的数据搬运并烧写到它内部的存储介质中,是需要物理时间的(这个时间叫“页写周期”,通常是 5 毫秒左右)。

这4个 I2C_Delay(255) 就是为了让单片机“傻等”这 5 毫秒,保证 EEPROM 写完,以免紧接着进行下一次读写时失败。

2. 为什么 EA=1 要放在延时前面?

  • 保护通信时序: EA=0 关闭中断是为了保护前面 I2CStart 到 I2CStop 的通信过程。因为软件模拟 I2C 的时序很严格,不能被定时器中断打断,否则会导致通信失败。

  • 释放 CPU 响应能力: 通信一旦结束(I2CStop执行完),时序就不再需要保护了!此时立刻 EA=1 打开总中断,那么在单片机“傻等”这 5 毫秒的过程中,定时器中断依然可以正常工作。

如果你把 EA=1 放到了最后面:

  • 意味着在这长达 5 毫秒的延时等待中,单片机的总中断 EA 依然是关闭状态。

  • 后果: 所有的定时器中断、串口中断全都会被挂起卡住。如果是做蓝桥杯比赛或者带有数码管的开发板,你会发现每次往 EEPROM 写数据时,数码管都会明显闪烁或抖动一下(因为数码管有 5 毫秒没被点亮或没被切换),同时这期间如果串口有数据发过来,也可能会漏接。

//void ee_read(unsigned char *str,unsigned char addr,unsigned char num)
//{
//	EA=0;
//	
//	I2CStart();
//	I2CSendByte(0xa0);
//	I2CWaitAck();
//	
//	I2CSendByte(addr);
//	I2CWaitAck();
//	
//	I2CStart();
//	I2CSendByte(0xa1);
//	I2CWaitAck();
//		
//	while(num--)
//	{
//		*str++=I2CReceiveByte();
//		if(num)I2CSendAck(0);
//		else I2CSendAck(1);
//	}
//		I2CStop();
//	
//	EA=1;
//}

这完全取决于你在代码里是把 35 当作“数字”存,还是当作“字符(文本)”存。

但在单片机编程(尤其是蓝桥杯等比赛)中,99% 的情况下 num 应该等于 1。

1. 为什么 num = 1 是对的?(当作数字存)

在C语言中,一个 unsigned char(无符号字符型/单字节)可以存储的大小范围是 0 到 255。 你的数字是 35,它完全可以被塞进一个字节里。

unsigned char val = 35;
ee_write(&val, 0x00, 1); // 存的时候,num写1,只占用EEPROM的一个地址
ee_read(&val, 0x00, 1);  // 读的时候,num写1,直接读出35

当 num=1 时,循环体刚好只执行了 1 次,完美读出 1 个字节。


2. 网课博主为什么说 num = 2?(当作字符存)

博主的情况是把 35 当成了字符串 "35",也就是把它拆成了字符 '3' 和字符 '5'。 在计算机里,字符 '3' 的ASCII码是十进制的 51,字符 '5' 的ASCII码是 53。

unsigned char str[2] = {'3', '5'}; // 这是一个数组,占2个字���
ee_write(str, 0x00, 2); // num等于2,占用EEPROM的两个地址

这种写法每次存入一个字符,循环体内执行 2 次。???

总结建议

如果你只是想记录一个传感器数据、设定阈值(比如温度上限 35 度),绝对应该用 num=1,把它当数字存。这样不仅省空间,读出来以后可以直接用来做加减乘除计算。如果拆成字符存,读出来还得自己写代码把 '3' 和 '5' 拼成数字 35,纯属脱裤子放屁。

sys_init() 不需要加时序保护(EA=0 和 EA=1)

关键在于你调用它的位置。

你把 sys_init() 放在了 main() 函数的开头,并且在 Timer1_Init() 和 Timer0_Init() 之前。

这个时候,你的定时器根本还没有开始工作,单片机的总中断(EA)通常也还是默认关闭状态。既然没有任何中断会打断它,自然就不存在“保护”的需求,它安安静静地按顺序执行完就行了。

超声波底层

//产生8个40kHZ的脉冲
void wave_init()
{//50%的占空比,25us就是40khz
	unsigned char i;
	EA=0;//关闭总中断,防止中断打断
	
	for(i=0;i<8;i++)
	{
		Tx=1;
		Delay12us();
		Tx=0;
		Delay12us();
	}
      EA=1;
}

这段代码是专门用来驱动超声波发送探头(Tx)发射超声波的。

超声波测距的原理是:发射头发出声波,遇到障碍物反弹,接收头收到声波。为了让这个过程顺利进行,发射的声波必须满足特定的物理和硬件要求。

1. 为什么是 40kHz?为什么延时 12us?

你板子上的超声波发射探头,它的硬件物理共振频率是 40kHz。也就是说,只有你给它提供 40kHz 的方波电信号,它才能把电能最大化地转换成超声波发出去(偏高或偏低都会导致发不出声音或距离极短)。

  • 计算周期:频率 f=40,000 Hz,那么一个完整的周期时间 T=1/40000=0.000025 秒=25 微秒(us)。

  • 50%占空比:一个周期内,高电平和低电平的时间各占一半,即 25 us/2=12.5 us。

  • 代码实现:代码里写了 Tx=1; Delay12us(); Tx=0; Delay12us();

  • 虽然延时函数是 12us,但是单片机执行 Tx=1、Tx=0 以及 for 循环本身也需要耗费零点几微秒的时间。加起来刚好约等于 12.5us 的高电平和 12.5us 的低电平,凑成完美的 25us(40kHz)。

2. 为什么是循环 8 次?(for(i=0;i<8;i++))

这是超声波测距的行业标准做法:

2. 为什么计算公式是 time * 0.017?(重点!)

这是一个非常经典的物理公式简化:距离 = 速度 × 时间 ÷ 2(因为声音是一去一回,算单程距离要除以 2)。

3. CF标志位是干什么的?(硬件超时保护)

CF 是这个定时器(PCA)的溢出标志位。

  • 如果发的脉冲太少(比如只发1、2个):声波能量太弱,跑不远,接收头可能听不到回声。

  • 如果发的脉冲太多(比如发20个):发射时间太长,导致前面的声波已经撞到近处障碍物弹回来了,你探头还在发,这叫“余震盲区变大”,会导致测不到近距离的物体。

  • unsigned int distance(void)
    {
    	unsigned  int time;//16位用int
    	CH = CL=0;//定时器从0开始计时
    	CMOD=0X00;//配置成16位不自动重装模式
    	
    
    	wave_init();//发送一个超声波脉冲
    	//超声波模块发完波后,RX会由低电平变为高电平
    	//如果硬件响应较慢,RX还没来得及拉高
    	
    	CR=1;//开始计时
    	while((Rx==1)&&(CF==0));//等待超声波接收信号
    	//RX==1没收到回波
    	//CF==0没超时
    	CR=0;//关闭定时器,停止计时
    	
    	if(CF==0)//CF==0没超时,成功收到回波
    	{
    		time = (CH<<8)|CL;
    		return (time*0.017);
    	}
    	else//CF==1超时了,没收到回波
    	{
    		CF = 0;//把超时标志清零
    		return 0;//返回0,代表测距失败
    	
    	}
    
    }

    1. 它是如何“掐秒表”的?

  • 清零秒表: CH = CL = 0; 和 CMOD = 0x00; 就是把单片机内部的一个 16 位硬件定时器(在这里用的是 PCA 模块)清零,并配置好工作模式,准备开始计“滴答”声。

  • 发令枪响: wave_init() 发出超声波,相当于开枪。

  • 按下秒表: CR = 1; 让定时器开始疯狂计数(每过 1 微秒,也就是 1us,定时器里的数字就加 1)。

  • 声音的速度: 在常温空气中,声速大约是 340m/s。把它换算成厘米和微秒(因为单片机计时的单位是微秒):340m/s=34000cm/s=0.034cm/us。

  • 计算推导:

    • 总距离 = time (微秒) × 0.034 (厘米/微秒)

    • 单程距离 = (time × 0.034) ÷ 2

    • 简化后 = time * 0.017

  • 结论: 只要你把计时器计出来的数字,乘以 0.017,得出的结果直接就是厘米(cm)。

  • 这个定时器是 16 位的,它能计数的最大范围是 0 到 65535。

  • 如果超声波探头前面完全没有障碍物,或者对着天空发,声波一去不复返。Rx 就会一直持高电平(1)。

  • 如果没有 CF==0 这个限制,单片机就会永远卡在 while(Rx==1) 里面死机

常见问题大赏

 开机瞬间大电流拉低电压导致 DS18B20 死机(最核心原因)


看你的 `main()` 函数,你把 `sys_init();` 写在了最后面!
单片机刚上电时,所有的引脚默认都是高电平(`0xFF`),这意味着你的**蜂鸣器和继电器在上电瞬间是全部狂响、全开的状态**!
在这期间,你不仅死等了 `Delay750ms()`,还去读取温度、又强行加上了 `ee_write`(写EEPROM需要非常稳定的电压)。

蜂鸣器和继电器同时工作会拉低板子的电压并产生巨大电磁干扰,极容易导致 DS18B20 芯片死机或初始化失败,从而读出全 0 数据(即 `00.0`)。

✅ 修改方法:
必须把 `sys_init();` 放到 `main` 函数的第一行!上电第一时间先关闭外设。

DS18B20 读取频率太快
在 `seg_proc()` 里(大约每隔 20ms 到 80ms 就会执行一次)频繁调用 `tem = rd_temperature();`。
DS18B20 物理上需要 **750ms** 才能完成一次温度转换。如果你每隔几十毫秒就去踢它一脚(发送 `0x44` 命令),它原本正在转换的温度就会被迫中断并重新开始,永远也转换不完,极大概率导致读出 `00.0` 或卡死在 `85.0`。

✅ 修改方法:
给温度读取单独加一个“减速器”,让它**每隔 800ms 读取一次**,这就绝对不会和 EEPROM 的 I2C 时序发生挤占冲突了!

为什么只有第一个数码管(显示3)会闪?

原因有以下两点,它们“完美”地撞在了一起:

1. 你的 ee_write 运行时间太长了,而且你把它放在了中断里! 你看你的 ee_write 函数最后面,有4个 I2C_Delay(255);。这4个死延时加起来大概要占用 10~20毫秒 的时间。 你把它放在了 t1service() 定时器中断里。

这意味着:每隔一秒钟,单片机就会在这个中断里卡住 20 毫秒。在这 20 毫秒内,数码管的动态扫描被迫停止了!停在了哪一位,哪一位就会被异常点亮 20 毫秒(平时每位只亮 1 毫秒),导致这一位突然变得极亮,肉眼看着就是“闪烁”。

2. 为什么偏偏是第 0 位(第一个数码管)? 因为一个绝妙的数学巧合:

你的数码管是 8 位循环扫描的(++seg_pos==8)。

你的一秒钟计数器是计 1000 次(++t1000ms==1000)。

1000 刚好能被 8 整除! 这意味着:每次 t1000ms 刚好数到 1000 的时候,seg_pos 绝对绝对等于 0! 所以,每次卡住的那 20 毫秒,单片机总是刚好在点亮第 0 位(第一个数码管),于是第一个数码管就成了唯一的“受害者”,每秒钟闪一下。

核心黄金法则:中断里绝对不能放长延时

我们要把 ee_write 移出定时器中断,放到 main 函数的 while(1) 主循环里去执行。

解决温度上电85

我们来剖析一下你写的 rd_temperature() 函数的内部逻辑。它的执行顺序是:

  1. 先读取上一次的温度(发送 0xBE 读取暂存器)。

  2. 再命令它开始下一次转换(发送 0x44 开始转换)。

DS18B20 芯片刚上电时,内部默认的初始温度就是 85℃。 看看你目前 main 函数里的执行顺序:

Delay750ms();      // 此时你根本没有给 DS18B20 发送转换命令!它在原地发呆了 750ms。
rd_temperature();  // 延时结束后,你调用函数。它第一步先读取,读到了默认的 85℃(这个值被你丢弃了),第二步它才发出 0x44 开始真正的转换。

为什么最大只有 006、007?(原因剖析)

  1. 板子上的 CX20106A 接收芯片非常敏感。发完波之后,探头外壳会自己震动(这叫余震)。

  2. 在余震期间,RX 会被它自己的声音拉低。

  3. 在这个大工程里,t1service 每毫秒打断一次,导致你的程序去检查 RX 的时候,刚好撞在余震的枪口上,瞬间跳出循环!计算出来的时间只有一两百微秒(对应就是 0 厘米,加上你的 +3,就变成了 006 左右跳动)。

更多推荐