蓝桥杯第16届单片机
codecode是一个特定的扩展关键字。它的意思是将这些数据存储在程序存储器ROM / Flash中而不是数据存储器RAM中。为什么要用code节省宝贵的RAM51单片机的RAM空间非常小而ROM空间相对较大。你代码里的seg_wei数码管位选和seg_duan数码管段选都是查表用的固定数据在程序运行过程中绝对不会被修改。EEPROM的底层//122 ////while5 //void ee_write(unsigned char *str,unsigned char addr,unsigned char num) //{ // EA0; // I2CStart(); // I2CSendByte(0xa0); // I2CWaitAck(); // // I2CSendByte(addr); // I2CWaitAck(); // // while(num--) // { // I2CSendByte(*str); // I2CWaitAck(); // I2C_Delay(200); // } // I2CStop(); // // EA1; // I2C_Delay(255); // I2C_Delay(255); // I2C_Delay(255); // I2C_Delay(255); //}最好还是保持第一种写法把EA1放在I2CStop()后面延时函数的前面。下面是具体的原理解释1. 为什么会有这4个I2C_Delay(255)当你调用I2CStop()后单片机确实已经把数据发给 EEPROM (AT24C02) 了。但是EEPROM 把接收到的数据搬运并烧写到它内部的存储介质中是需要物理时间的这个时间叫“页写周期”通常是 5 毫秒左右。这4个I2C_Delay(255)就是为了让单片机“傻等”这 5 毫秒保证 EEPROM 写完以免紧接着进行下一次读写时失败。2. 为什么EA1要放在延时前面保护通信时序EA0关闭中断是为了保护前面I2CStart到I2CStop的通信过程。因为软件模拟 I2C 的时序很严格不能被定时器中断打断否则会导致通信失败。释放 CPU 响应能力 通信一旦结束I2CStop执行完时序就不再需要保护了此时立刻EA1打开总中断那么在单片机“傻等”这 5 毫秒的过程中定时器中断依然可以正常工作。如果你把EA1放到了最后面意味着在这长达 5 毫秒的延时等待中单片机的总中断EA依然是关闭状态。后果 所有的定时器中断、串口中断全都会被挂起卡住。如果是做蓝桥杯比赛或者带有数码管的开发板你会发现每次往 EEPROM 写数据时数码管都会明显闪烁或抖动一下因为数码管有 5 毫秒没被点亮或没被切换同时这期间如果串口有数据发过来也可能会漏接。//void ee_read(unsigned char *str,unsigned char addr,unsigned char num) //{ // EA0; // // I2CStart(); // I2CSendByte(0xa0); // I2CWaitAck(); // // I2CSendByte(addr); // I2CWaitAck(); // // I2CStart(); // I2CSendByte(0xa1); // I2CWaitAck(); // // while(num--) // { // *strI2CReceiveByte(); // if(num)I2CSendAck(0); // else I2CSendAck(1); // } // I2CStop(); // // EA1; //}这完全取决于你在代码里是把 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当num1时循环体刚好只执行了 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 度绝对应该用num1把它当数字存。这样不仅省空间读出来以后可以直接用来做加减乘除计算。如果拆成字符存读出来还得自己写代码把3和5拼成数字 35纯属脱裤子放屁。sys_init()不需要加时序保护EA0和EA1关键在于你调用它的位置。你把sys_init()放在了main()函数的开头并且在Timer1_Init()和Timer0_Init()之前。这个时候你的定时器根本还没有开始工作单片机的总中断EA通常也还是默认关闭状态。既然没有任何中断会打断它自然就不存在“保护”的需求它安安静静地按顺序执行完就行了。超声波底层//产生8个40kHZ的脉冲 void wave_init() {//50%的占空比25us就是40khz unsigned char i; EA0;//关闭总中断防止中断打断 for(i0;i8;i) { Tx1; Delay12us(); Tx0; Delay12us(); } EA1; }这段代码是专门用来驱动超声波发送探头Tx发射超声波的。超声波测距的原理是发射头发出声波遇到障碍物反弹接收头收到声波。为了让这个过程顺利进行发射的声波必须满足特定的物理和硬件要求。1. 为什么是 40kHz为什么延时 12us你板子上的超声波发射探头它的硬件物理共振频率是 40kHz。也就是说只有你给它提供 40kHz 的方波电信号它才能把电能最大化地转换成超声波发出去偏高或偏低都会导致发不出声音或距离极短。计算周期频率 f40,000 Hz那么一个完整的周期时间 T1/400000.000025 秒25 微秒(us)。50%占空比一个周期内高电平和低电平的时间各占一半即 25 us/212.5 us。代码实现代码里写了Tx1; Delay12us(); Tx0; Delay12us();虽然延时函数是 12us但是单片机执行Tx1、Tx0以及for循环本身也需要耗费零点几微秒的时间。加起来刚好约等于 12.5us 的高电平和 12.5us 的低电平凑成完美的 25us40kHz。2. 为什么是循环 8 次for(i0;i8;i)这是超声波测距的行业标准做法2. 为什么计算公式是time * 0.017重点这是一个非常经典的物理公式简化距离 速度 × 时间 ÷ 2因为声音是一去一回算单程距离要除以 2。3. CF标志位是干什么的硬件超时保护CF是这个定时器PCA的溢出标志位。如果发的脉冲太少比如只发1、2个声波能量太弱跑不远接收头可能听不到回声。如果发的脉冲太多比如发20个发射时间太长导致前面的声波已经撞到近处障碍物弹回来了你探头还在发这叫“余震盲区变大”会导致测不到近距离的物体。unsigned int distance(void) { unsigned int time;//16位用int CH CL0;//定时器从0开始计时 CMOD0X00;//配置成16位不自动重装模式 wave_init();//发送一个超声波脉冲 //超声波模块发完波后RX会由低电平变为高电平 //如果硬件响应较慢RX还没来得及拉高 CR1;//开始计时 while((Rx1)(CF0));//等待超声波接收信号 //RX1没收到回波 //CF0没超时 CR0;//关闭定时器停止计时 if(CF0)//CF0没超时成功收到回波 { time (CH8)|CL; return (time*0.017); } else//CF1超时了没收到回波 { CF 0;//把超时标志清零 return 0;//返回0代表测距失败 } }1. 它是如何“掐秒表”的清零秒表CH CL 0;和CMOD 0x00;就是把单片机内部的一个 16 位硬件定时器在这里用的是 PCA 模块清零并配置好工作模式准备开始计“滴答”声。发令枪响wave_init()发出超声波相当于开枪。按下秒表CR 1;让定时器开始疯狂计数每过 1 微秒也就是 1us定时器里的数字就加 1。声音的速度 在常温空气中声速大约是 340m/s。把它换算成厘米和微秒因为单片机计时的单位是微秒340m/s34000cm/s0.034cm/us。计算推导总距离 time(微秒) × 0.034 (厘米/微秒)单程距离 (time× 0.034) ÷ 2简化后 time * 0.017结论 只要你把计时器计出来的数字乘以0.017得出的结果直接就是厘米(cm)。这个定时器是 16 位的它能计数的最大范围是 0 到 65535。如果超声波探头前面完全没有障碍物或者对着天空发声波一去不复返。Rx就会一直持高电平1。如果没有CF0这个限制单片机就会永远卡在while(Rx1)里面死机常见问题大赏开机瞬间大电流拉低电压导致 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_pos8。你的一秒钟计数器是计 1000 次t1000ms1000。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 开始真正的转换。