1. 赛题难点解析与整体思路

蓝桥杯单片机第十五届模拟题1确实是个硬骨头,我刚开始做的时候也踩了不少坑。这道题的核心难点主要集中在数据输入左移处理、EEPROM存储转换、记录时间判定这几个关键环节。很多同学做到一半就卡住了,要么是数据左移效果出不来,要么是EEPROM存储的数据不对,还有的记录时间采集错误。

我花了整整两天时间调试这个题目,最后发现问题的关键在于对数据流向的清晰把握。这道题需要我们在数码管显示、按键输入、时间记录、数据存储等多个模块之间建立正确的逻辑关系。比如说,数据左移不是在按键上做文章,而是通过数码管显示来控制;EEPROM存储也不是简单地把数据写进去,需要经过进制转换处理。

从整体架构来看,这个题目可以分为三个主要界面:时间显示界面、数据输入界面和记录查看界面。每个界面都有其特定的功能和显示要求,而且界面之间的切换要保证数据的一致性。我在实际调试中发现,最容易出问题的地方是界面切换时的数据初始化,如果没有处理好,就会导致数据显示错乱或者功能异常。

2. 数据输入与左移实现详解

2.1 数码管显示控制原理

数据左移是这个题目最大的难点之一,很多同学理解错了方向。其实左移效果不是通过移动按键输入来实现的,而是通过控制数码管的显示顺序。让我用最直白的话来解释这个机制。

想象一下数码管的8个显示位就像8个停车位,我们从右往左停车。每按下一个数字键,这个数字就停在最右边的车位(第7位),之前停着的车依次向左移动一个车位。这样看起来就像是数字在不断左移,实际上我们只是在调整每个车位显示什么数字而已。

在代码中,这个逻辑是通过一个显示缓冲数组SMG_Buff来实现的。初始化时,所有位置都设置为10(表示不显示)。当有按键输入时,我们将输入的数字存入密码数组pasword中,然后通过一个循环来更新显示缓冲:

for(i=0;i<pasword_Index;i++)
    SMG_Buff[7-i] = pasword[pasword_Index-i-1];

这个循环很有意思,它从右往左填充显示缓冲。pasword_Index表示当前已经输入的数字个数,通过7-i这个索引,我们实现了从最右端开始显示,每次新输入都挤着之前的数字往左移动的效果。

2.2 按键输入限制处理

题目要求输入4位数字后就不能继续输入了,这个需要在按键处理函数中加上限制条件。我在Key_Prco()函数中做了这样的处理:

if(Key_Down >= 1 && Key_Down <= 10 && pasword_Index < 4)
{
    pasword[pasword_Index] = Key_Down - 1;
    pasword_Index++;
}

这里的pasword_Index < 4就是关键限制条件。当已经输入4位数字后,这个条件就不满足了,即使再按数字键也不会被处理。这种设计既简单又有效,避免了复杂的状态判断。

在实际调试中,我发现还需要处理没有输入时的显示情况。当没有任何输入时,相关的数码管位应该熄灭,否则会显示乱码。这就是为什么代码中还有这样的判断:

if(pasword_Index == 0)
{
    for(n=3;n<8;n++)
        SMG_Buff[n] = 10;
}

这个循环将第3到第7位数码管都设置为10(熄灭),确保显示界面的整洁性。

3. 记录时间采集与处理技巧

3.1 起始时间准确记录

记录时间的要求很特别——不是记录输入完成的时间,而是记录第一次按下按键的时间。这个要求很容易被忽略,我第一遍做的时候就栽在这里了。

问题的难点在于如何准确捕捉"第一次按下"这个时刻。我采用了一个标志位pasword_Flag来解决这个问题。初始值为0,表示还没有记录起始时间。当检测到有按键输入且标志位为0时,就记录当前时间并将标志位置1:

if(pasword[0] != 10 && pasword_Flag == 0)
{
    for(j=0;j<3;j++)
        ucRtc_MAX[j] = ucRtc[j];
    pasword_Flag = 1;
}

这里pasword[0] != 10表示至少已经有一个数字输入(因为初始值都是10),pasword_Flag == 0确保只记录第一次按键的时间。一旦记录完成,标志位就被置1,后续的按键就不会重复记录时间了。

这种设计保证了即使输入过程中有删除重新输入的情况,起始时间仍然是最初第一次按键的时间,完全符合题目要求。

3.2 时间数据格式转换

从DS1302读取的时间数据是BCD码格式,需要转换成十进制才能进行后续处理。这里涉及到进制转换的知识点,很多同学容易混淆。

BCD码的本质是用4位二进制表示一个十进制数字。比如小时数23,在BCD码中就是0010 0011,相当于十进制的35。所以我们需要进行转换:

EEPROM_Data[0] = ((ucRtc_MAX[0]/16*10) + (ucRtc_MAX[0]%16));

这个转换公式看起来复杂,其实原理很简单。ucRtc_MAX[0]/16得到十位数,ucRtc_MAX[0]%16得到个位数,然后十位数×10+个位数就得到了真正的十进制数值。

我在调试过程中发现,如果底层时钟库直接返回十进制数,就不需要这个转换步骤。但大多数DS1302驱动返回的都是BCD码,所以这个转换是必须的。

4. EEPROM存储机制深入解析

4.1 数据转换与存储格式

EEPROM存储是这个题目最技术性的部分,涉及到数据转换和存储格式的处理。题目要求将4位输入数字和起始时间存储在EEPROM中,但存储的数据格式有特定要求。

首先需要将4个单独的数字转换成一个整数:

pasword_input = pasword[0]*1000 + pasword[1]*100 + pasword[2]*10 + pasword[3];

这样就把像[1,2,3,4]这样的数组转换成了1234这个整数。但EEPROM每个地址只能存储一个字节(0-255),所以我们需要把这个整数拆分成两个字节存储。

这里用到了位操作技巧:

EEPROM_Data[2] = pasword_input >> 8;      // 取高8位
EEPROM_Data[3] = pasword_input & 0x00ff;  // 取低8位

8相当于除以256,得到的是高8位的值;& 0x00ff是位掩码操作,只保留低8位,高8位清零。这样就把一个16位的整数拆分成了两个8位的字节。

4.2 EEPROM读写操作

EEPROM的读写需要遵循严格的时序,我使用的是I2C协议的EEPROM(如AT24C02)。读写函数需要按照芯片的时序要求来编写:

写入函数需要先发送起始信号,然后发送设备地址(0xA0表示写操作),接着发送要写入的存储地址,最后发送要写入的数据:

void EEPROM_Write(unsigned char* EEPROM_String, unsigned char addr, unsigned char num)
{
    IIC_Start();
    IIC_SendByte(0xA0);
    IIC_WaitAck();
    IIC_SendByte(addr);
    IIC_WaitAck();
    
    while(num--)
    {
        IIC_SendByte(*EEPROM_String++);
        IIC_WaitAck();
        IIC_Delay(200);
    }
    
    IIC_Stop();
}

读取操作稍微复杂一些,需要先发送写操作地址和要读取的存储地址,然后重新发送起始信号和读操作地址(0xA1),最后读取数据:

void EEPROM_Read(unsigned char* EEPROM_String, unsigned char addr, unsigned char num)
{
    IIC_Start();
    IIC_SendByte(0xA0);
    IIC_WaitAck();
    IIC_SendByte(addr);
    IIC_WaitAck();
    
    IIC_Start();
    IIC_SendByte(0xA1);
    IIC_WaitAck();
    
    while(num--)
    {
        *EEPROM_String++ = IIC_RecByte();
        if(num)
            IIC_SendAck(0);
        else
            IIC_SendAck(1);
    }
    
    IIC_Stop();
}

在实际使用中,需要注意EEPROM的写入周期限制。频繁写入会缩短芯片寿命,所以要避免不必要的写操作。在这个题目中,我们只在确认输入完成后才进行一次写入操作。

5. 记录指示灯与界面切换逻辑

5.1 指示灯状态判断

记录指示灯的要求是:如果当前输入的数据大于上次保存的数据,指示灯亮起。这个功能看似简单,但实现起来有几个细节需要注意。

首先需要保存上一次输入的数据,我使用了一个全局变量pasword_input_old来存储。初始值设置为10000,确保第一次输入的任何4位数都比它小(因为4位数最大是9999),这样指示灯就不会亮。

判断逻辑很简单:

LED_Flag = (pasword_input > pasword_input_old);

这是一个布尔表达式,如果当前输入大于旧值,LED_Flag为1(真),否则为0(假)。然后将这个标志赋值给LED显示数组:

ucLED[3] = LED_Flag;

这样第四个LED就会根据比较结果亮起或熄灭。

这里有个细节:每次保存完成后需要更新旧值,否则下次比较时还是和最早的数据比较:

pasword_input_old = pasword_input;

5.2 界面切换与数据管理

界面切换是这个题目最容易出bug的地方。三个界面(时间显示、数据输入、记录查看)之间的切换需要管理好各种状态变量。

当切换到记录界面时,需要保存数据到EEPROM并重置输入状态:

if(SMG_Disp_Mode == 2) // 进入记录页面
{
    // ... EEPROM存储操作
    
    memset(pasword, 10, 4);  // 清除输入数据
    pasword_Index = 0;        // 重置输入指针
    pasword_Flag = 0;         // 重置标志位
}

当切换回输入界面时,需要能够重新输入数据。这里题目有个隐藏要求:返回输入界面后可以继续输入数据并记录新的时间。这就是为什么我们需要在切换到记录界面时清除输入状态,这样返回输入界面时就可以重新开始了。

界面切换是通过按键11(通常是S11)实现的:

case 11:
    if(++SMG_Disp_Mode == 3)
        SMG_Disp_Mode = 0;
    // ... 其他处理
    break;

SMG_Disp_Mode在0、1、2之间循环,分别对应三个界面。这种设计简单明了,避免了复杂的条件判断。

在实际调试中,我发现界面切换时最容易出现的问题是显示残留。比如从输入界面切换到其他界面再切换回来,之前显示的内容可能没有完全清除。这就需要我们在每个界面的显示处理函数中确保完全刷新显示缓冲,不要依赖之前的状态。

6. 完整代码分析与调试技巧

6.1 主循环与模块化设计

整个程序采用模块化设计,各个功能封装成独立的函数,通过全局变量进行数据交换。这种设计使得代码结构清晰,便于调试和维护。

主循环非常简单:

void main()
{
    Timer0Init();
    Set_Rtc(ucRtc);
    
    while(1)
    {
        Key_Prco();
        SMG_Prco();
        LED_Prco();
    }
}

三个处理函数分别负责按键处理、数码管显示处理和LED显示处理。定时器中断负责扫描显示和按键去抖。

这种设计的好处是每个模块相对独立,出问题时可以单独调试。比如数码管显示不正常,可以重点查看SMG_Prco()函数;按键无响应,就检查Key_Prco()函数。

6.2 调试技巧与常见问题

在调试这个题目时,我总结了一些实用的技巧:

首先,要学会使用数码管显示调试信息。当程序出现异常时,可以在数码管上显示关键变量的值,帮助定位问题。比如可以临时修改SMG_Prco()函数,在数码管上显示pasword_Index或pasword_Flag的值。

其次,要注意全局变量的初始化。很多奇怪的问题都是由于变量没有正确初始化导致的。特别是状态标志位和数组,一定要在程序开始时赋予明确的初始值。

再次,注意函数执行时间。中断服务函数中的代码要尽量简短,避免影响定时精度。如果发现数码管闪烁或按键响应迟钝,可能是某些函数执行时间太长导致的。

最后,做好代码版本管理。调试过程中每次只修改一个地方,如果问题没有解决就及时回退。不要同时修改多个地方,否则很难定位问题根源。

常见的问题包括:数码管显示乱码(通常是显示缓冲没有正确更新)、按键无响应(可能是去抖参数设置不合理)、EEPROM读写失败(时序问题或地址错误)、时间记录不正确(标志位管理错误)等。

通过模块化的设计和系统的调试方法,这些问题都可以逐步解决。最重要的是保持耐心,仔细分析每个环节的数据流向和处理逻辑。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐