失误一

sprintf(pucseg_buf, "P1 %3.1f",(float)freq_parm); 初始状态下莫名其妙在数码管上显示9.05

freq_parm 确实是 9.0。导致显示 9.05 的罪魁祸首是:你的格式化字符串少敲了一个空格,导致 8 位的数码管只刷新了 7 位,最后 1 位保留了上一个界面的“残影”。

下面我为你详细拆解一下这个现象的成因以及修改方法。

🔍 为什么会显示 9.05?(残影现象拆解)

在 8051 的数码管底层驱动(seg_tran)中,通常是将格式化好的字符串逐个映射到 8 个物理数码管上。需要特别注意的是:小数点 . 会和它前面的数字合并,共用同一个数码管。

我们来仔细数一下你代码中的字符数和实际占用的数码管数量:

  • P2 的代码sprintf(pucseg_buf, "P2 %02d", 40);

    • 字符串:"P2" + 4个空格 + "40"。

    • 占用数码管:2 + 4 + 2 = 8 个。刚好填满,完美刷新。

  • P3 的代码sprintf(pucseg_buf, "P3 %2.1f", 0.6);

    • 字符串:"P3" + 4个空格 + "0.6"(包含小数点的3个字符)。

    • 占用数码管:2 + 4 + 2('0.'算1个,'6'算1个)= 8 个。刚好填满。

  • P1 的出bug代码sprintf(pucseg_buf, "P1 %3.1f", 9.0);

    • 字符串:"P1" + 3个空格 + "9.0"。

    • 占用数码管:2 + 3 + 2('9.'算1个,'0'算1个)= 7 个!

因为 "P1 %3.1f" 只使用了 7 个物理数码管,所以第 8 个数码管根本没有被这段代码覆盖到。它上面显示的 5,实际上是你切入这个界面之前,上一个界面(比如 Mode 2 里的超声波距离,或者 Mode 1 里的湿度值)留在第 8 个数码管上的数字残影。

解救方案

多占一格

以后显示小数点都可以这么做

失误二:继电器“开关次数”变“轰炸计数”(逻辑缺陷)

问题描述:
题目 4.10 要求:“统计继电器开关次数(即吸合次数)”。
在你的 ultrasonic_proc 函数中:
if(ultras_val > dist_parm_2) {
set_peripheral(1, 1);
relay_const++; // 致命错误:这里每 100ms 就会疯狂加 1!
}

因为你的 ultrasonic_proc 每 100ms 运行一次,只要距离大于参数,继电器保持吸合状态期间,你的 relay_const 就会每秒钟增加 10 次!并且你每 100ms 都在疯狂擦写 EEPROM,这不仅数据错误,几分钟就能把板子上的 EEPROM 写废。

 

修复方案:
引入边缘检测(只在继电器从断开变为吸合的那一瞬间加 1 并保存)

void ultrasonic_proc(void)
{
if(uiultras_dly < 100) return;
uiultras_dly = 0;

ultras_val = wave_recv();

if(ultras_val > dist_parm_2)
{
if(relay == 0) // 判断上升沿:原本是断开的,现在要吸合了
{
relay = 1;
relay_const++;
// 只有变化时才写入 EEPROM,保护芯片
EEPROM_buf[0] = relay_const;
EEPROM_write(EEPROM_buf, 0x00, 1);
}
set_peripheral(1, motor); // 注意这里用你定义的变量
}
else
{
relay = 0;
set_peripheral(0, motor);
}
}

失误三:上电没有读取 EEPROM(不符初始状态要求)

问题描述:
题目要求将次数保存在 EEPROM 的地址 0 中。目的是为了掉电不丢失数据。
在你的 main 函数里,根本没有读取 EEPROM 的操作。每次单片机重启,relay_const 都会被初始化为 0。评委在测试时,断电重启发现次数归零,这部分分数会直接扣没。

 

修复方案:
main() 函数的死循环前,把 EEPROM 的数据读出来赋给变量:


void main(void)
{
cls_peripheral();
Timer2_Init();
Timer1_Init();
Timer0_Init();

// --- 增加这一段开机读取操作 ---
EEPROM_read(EEPROM_buf, 0x00, 1); // 假设你的 iic.c 里有 read 函数
relay_const = EEPROM_buf[0];
// ----------------------------

EA = 1;
while(1) { ... }
}


重要经验

我第一次写的代码
无论是在定时器2还是PCA中只要使用

PWM = (PWM + 1) % 10; 
if(ultras_val > dist_parm_2) 
{relay = 1; 
 relay_const++; 
 } 
 else 
 relay = 0; 
 if(freq > freq_parm_2) 
 { 
 if(PWM < 8) 
motor = 1; 
 else 
 motor = 0; 
 } 
 else 
 { 
if(PWM < 2) 
 motor = 1; 
else 
 motor = 0; 
 } 
 set_peripheral(relay, motor); 
EEPROM_buf[0] = relay_const; 
 EEPROM_write(EEPROM_buf, 0x00, 1);

这个逻辑整个单片机的时序就会陷入混乱,数码管闪烁个不停且按键响应缓慢,LED亮灭混乱,闪烁明显频率太低。原本我打算是以PCA作为主中断1ms进入一次,定时器2作为100微秒进入一次完成PWM输出,但是就会出现比上面所说更严重的混乱,数码管闪烁严重且有严重延迟,LED亮灭完全混乱
 

这是一次非常典型的**“中断过载”加上“锁存器冲突”**引发的系统级灾难!

你遇到的数码管严重闪烁、LED 群魔乱舞、按键像死机一样迟钝,根本原因就在你注释掉的这段代码里。这是一个所有初学 51 单片机和备战蓝桥杯的同学都会踩的超级大坑。

为你揭开这个“系统崩溃”背后的两大罪魁祸首,以及完美的解决架构:

💣 致命元凶一:在极高频中断里写 EEPROM(系统直接卡死)

这是导致你按键迟钝、数码管严重卡顿的根本原因。

  • 时间账本:IIC 通信是靠软件延时(_nop_())模拟的,写入一次 EEPROM(AT24C02)至少需要 5 毫秒(5000 微秒) 左右的芯片内部处理时间。

  • 你的设定:你打算用 100 微秒(us)进一次定时器 2 来做 PWM。

  • 灾难现场:定时器每 100us 触发一次中断,然后冲进去执行 EEPROM_write,结果写一次要花 5000us!这意味着中断执行的时间,比中断产生的周期足足慢了 50 倍!
    CPU 彻底溺水了,它永远都在中断里面处理 IIC 的延时,主循环里的数码管扫描(seg_proc)和按键扫描(key_proc)根本抢不到 CPU 时间,当然会死机、闪烁、没反应!

💥 致命元凶二:在中断里直接操作 set_peripheral(锁存器冲突导致群魔乱舞)

这是导致你的 LED 亮灭完全混乱的根本原因。

CT107D 开发板上,LED、数码管、继电器、电机,全都是共用 P0 端口,并通过 P2 端口控制 74HC138 译码器来切换 74HC573 锁存器的。

  • 主循环的 led_disp()seg_disp() 正在操作 P0 和 P2 准备点亮某个数码管。

  • 突然!100us 的定时器中断来了,打断了主循环,跳进中断执行 set_peripheral()

  • 中断把 P0 和 P2 强行改成了继电器和电机的状态。

  • 等中断执行完回到主循环时,P0 和 P2 的数据已经被篡改了!主循环接着往下走,就把本该给电机的数据,错误地锁存给了 LED 或数码管,导致群魔乱舞。

第一次修改
 

🛠️ “抢救”指南:如何搭建稳定且丝滑的 PWM 架构

要想实现 100us 精度的高频 PWM 输出,并且系统不卡顿,你必须将“逻辑计算”和“硬件输出”严格分离,并加入**“现场保护”**。

请按照以下三个步骤重构你的代码:

第一步:把所有的“逻辑判断”和“EEPROM 读写”踢出中断

把继电器条件、电机 PWM 占空比计算,全部放到主循环的一个函数里(比如可以和超声波合并,或者单独写一个 logic_proc),100ms 执行一次即可


// 新增一个全局变量,用来告诉中断该输出多少占空比
unsigned char target_pwm = 0;

void logic_proc(void) // 放在 while(1) 里,并加 100ms 延时限制
{
if(uilogic_dly < 100) return;
uilogic_dly = 0;

// 1. 继电器与 EEPROM 逻辑 (沿用我们之前改好的边沿触发)
if(ultras_val > dist_parm_2) {
if(relay == 0) {
relay = 1;
relay_const++;
EEPROM_buf[0] = relay_const;
EEPROM_write(EEPROM_buf, 0x00, 1);
}
} else {
relay = 0;
}

// 2. 提前计算好电机的 PWM 占空比目标值 (不需要在中断里每次都算)
if(freq > freq_parm_2) {
target_pwm = 8; // 80% 占空比
} else {
target_pwm = 2; // 20% 占空比
}
}

第二步:打造一个“纯净且安全”的 100us 中断

中断里只做最简单的计数,并且在操作外设时,必须保存和恢复 P0、P2 的状态(现场保护)


// 这是一个 100us 的中断 (比如 Timer 1 或者 定时器 2)
void Timerx_Isr(void) interrupt x
{
// 1. 定义局部变量,用于保存现场
unsigned char p0_bak, p2_bak;

// (如果在这里有其他计时器需求,代码放这里)

// 2. 极简的 PWM 计数
PWM = (PWM + 1) % 10;
if(PWM < target_pwm) {
motor = 1;
} else {
motor = 0;
}

// 3. !!!核心:保护现场!!!
p0_bak = P0; // 把打断时的 P0 状态存起来
p2_bak = P2; // 把打断时的 P2 状态存起来

// 4. 安全地操作继电器和电机
set_peripheral(relay, motor);

// 5. !!!核心:恢复现场!!!
P2 = p2_bak; // 把 P2 原封不动地还给主循环
P0 = p0_bak; // 把 P0 原封不动地还给主循环
}

第三步:时间切片分离

你原本是用 Timer 2 既做 1ms 计时(数码管/按键延时),又想兼顾 PWM。
正确的做法是:

  • 定时器 0:专门用作频率测量(外部计数)。

  • 定时器 1:配置为 100us 的自动重装载,专门且仅用于处理上面的 PWM 和 set_peripheral

  • 定时器 2:配置为 1ms 中断,用来给你的 ulms、数码管、按键等提供时间基准(注意:这个 1ms 中断里绝对不要出现 set_peripheral)。

修改后依然有问题还是存在问题我的wet_val长时间是0了,怎么调都没用

在 CT107D 开发板上,PCF8591(ADC/DAC)和 AT24C02(EEPROM)共用同一条 IIC 总线,而这条总线的引脚正好接在 P2.0 (SDA)P2.1 (SCL) 上。

我们来看看你的 100us 定时器中断里发生了什么:

  1. 主循环正在愉快地和 PCF8591 通信(ADC 采样)。

  2. 在通信的某一瞬间,ADC 芯片将 SDA(P2.0)拉低为 0,准备发送数据。

  3. 就在这微秒之间,你的 100us 定时器中断触发了!

  4. 中断执行了 p2_bak = P2;。在 C 语言底层,这句话读取的是 P2 引脚的物理状态,于是 p2_bak 的最低位记录下了 0

  5. 中断里的 PWM 算完后,执行了 P2 = p2_bak;

  6. 致命一击:这句话把刚刚记录的 0 硬写进了单片机的 P2 内部锁存器里!从此刻起,单片机开始强行把 SDA 引脚拉低,彻底锁死了 IIC 总线。

总线被单片机自己锁死后,PCF8591_ADC() 读取到的数据永远都是 0,这就是 wet_val 毫无反应的真相!



第二次修改及一个终级方案
 

🚀 比赛最强套路:“原子操作”护体大法(仅需加几行代码)

既然我们不想改底层的寄存器,那办法非常简单粗暴:在主循环执行硬件操作时,暂时把中断关掉,不准它来捣乱;执行完马上打开!

这就是单片机开发中最经典的**“关总中断(EA = 0;)”**技巧

请你把你的中断改回你最熟悉、最简单的样子(或者放进你的 PCA 里):


// 你的原版极简中断,什么都不用改!
void PCA_Isr(void) interrupt 7
{
// ... 前面的计时和数码管扫描代码 ...

PWM = (PWM + 1) % 10;

// ... 你原本的 target_pwm 判断逻辑 ...

set_peripheral(relay, motor); // 直接调用,舒坦!

// ... 清除标志位等代码 ...
}

然后,只需要在主循环的几个关键函数里,加上 EA = 0EA = 1 护体即可!

1. 拯救 ADC/DAC (解决 wet_val 为 0)

找到你的 adc_dac_proc 函数,把读写 IIC 的那两行包起来:


void adc_dac_proc(void)
{
if(uiadc_dly < 100) return;
uiadc_dly = 0;

EA = 0; // 关中断!ADC 读取时不准打扰
wet_val = (float)(PCF8591_ADC())/255.0 * 100.0;
EA = 1; // 读完立刻开中断!

if(wet_val == 100) wet_val = 99;

// ... 你的数学计算部分不变 ...
dac_val = dac_veg * 51;

EA = 0; // 关中断!DAC 输出时不准打扰
PCF8591_DAC(dac_val);
EA = 1; // 立刻开中断!
}

2. 拯救 LED (解决乱闪)

找到你的 led_proc 函数,保护最后一句输出:


void led_proc(void)
{
// ... 前面的逻辑计算都不变 ...

EA = 0; // 关中断!锁存 LED 时不准打扰
led_disp(ucled);
EA = 1; // 立刻开中断!
}

3. 拯救 EEPROM (解决系统卡顿)

在上一版中,我们已经把 EEPROM 放入了边沿触发(只在继电器状态改变的那一瞬间写一次)。写入时同样加保护:


void ultrasonic_proc(void)
{
// ... 前面的超声波测距不变 ...

if(ultras_val > dist_parm_2) {
if(relay == 0) {
relay = 1;
relay_const++;
EEPROM_buf[0] = relay_const;

EA = 0; // 关中断!保护 EEPROM 写入时序
EEPROM_write(EEPROM_buf, 0x00, 1);
EA = 1; // 开中断!
}
} else {
relay = 0;
}
}

总结下来就是我们可以朴素的理解为一旦有涉及这种PWM输出,使用多个定时器的场景,我们就应该使用EA=0/1这种技巧,一般应用于IIC、ds1302、ds18b20这类较复杂的模块的函数和LED这种极易受到影响的模块的函数被调用的时候就使用这个技巧
 


PCA的优化

这种复杂的情形必将导致PCA计算有严重延时,导致NE555频率输出最后错误,所以我们这里对原本的PCA进行优化

主要记忆

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐