蓝桥杯第16届单片机
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() 函数的内部逻辑。它的执行顺序是:
-
先读取上一次的温度(发送
0xBE读取暂存器)。 -
再命令它开始下一次转换(发送
0x44开始转换)。
DS18B20 芯片刚上电时,内部默认的初始温度就是 85℃。 看看你目前 main 函数里的执行顺序:
Delay750ms(); // 此时你根本没有给 DS18B20 发送转换命令!它在原地发呆了 750ms。
rd_temperature(); // 延时结束后,你调用函数。它第一步先读取,读到了默认的 85℃(这个值被你丢弃了),第二步它才发出 0x44 开始真正的转换。
为什么最大只有 006、007?(原因剖析)
-
板子上的 CX20106A 接收芯片非常敏感。发完波之后,探头外壳会自己震动(这叫余震)。
-
在余震期间,
RX会被它自己的声音拉低。 -
在这个大工程里,
t1service每毫秒打断一次,导致你的程序去检查RX的时候,刚好撞在余震的枪口上,瞬间跳出循环!计算出来的时间只有一两百微秒(对应就是 0 厘米,加上你的+3,就变成了006左右跳动)。
更多推荐



所有评论(0)