这四个外设是蓝桥杯每年必考的基础内容,而且它们有一个共同点:全部通过P0口+74HC573锁存器控制。搞懂了锁存器,这四个外设就都通了。


74HC573锁存器:CT107D硬件设计的灵魂

先把原理搞清楚。CT107D板子上LED灯、数码管段选、数码管位选、蜂鸣器、继电器一共需要20+个IO来控制,但P0口只有8个引脚。解决办法是用74HC573锁存器做"分时复用"——P0口同一时刻只驱动一个模块,数据锁存后就保持输出不变,P0口再切给下一个模块用。

P2口高3位选择打开哪个锁存器:

P2.7  P2.6  P2.5  |  选通器件
  1     0     0    |  U8 → LED灯 (0x80)
  1     0     1    |  U9 → 蜂鸣器+继电器 (0xA0)
  1     1     0    |  U7 → 数码管位选 (0xC0)
  1     1     1    |  U6 → 数码管段选 (0xE0)

标准的"开门关门"操作模板(所有锁存器操作都长这样):

unsigned char Temp;
P0 = 需要输出的数据;           // 第1步:放数据到P0
Temp = P2 & 0x1f;              // 第2步:保护P2低5位
Temp |= 0x80;                  // 第3步:设置高3位(改这个值切换锁存器)
P2 = Temp;                     // 第4步:开门,数据被锁存
Temp = P2 & 0x1f;              // 第5步:关门
P2 = Temp;

这里有一个非常容易犯的错:忘记 P2 & 0x1f。P2.0和P2.1分别连接着I2C的SCL和SDA线,如果你不保护低5位,操作锁存器的时候会改变I2C引脚的电平,导致I2C通信出错。这个问题非常隐蔽,我一开始调I2C的时候纠结了半天才发现是这个原因。


LED驱动:低电平点亮 + 脏检测优化

CT107D的8个LED(L1~L8)是共阳接法,低电平点亮。这意味着你要让某个LED亮,需要输出0而不是1。

项目里的完整LED驱动代码(来自 Driver/Led.c):

#include <Led.h>

idata unsigned char Temp_0_Val, Temp_0_Old;

void Led_Disp(unsigned char* ucLed) {
    unsigned char Temp;
    Temp_0_Val = 0x00;

    // 把数组中的8个LED状态合并为1个字节
    Temp_0_Val = ucLed[0] | (ucLed[1]<<1) | (ucLed[2]<<2) | (ucLed[3]<<3)
               | (ucLed[4]<<4) | (ucLed[5]<<5) | (ucLed[6]<<6) | (ucLed[7]<<7);

    // 脏检测:数据没变就不操作锁存器
    if(Temp_0_Val != Temp_0_Old) {
        P0 = ~Temp_0_Val;           // 取反!低电平点亮
        Temp = P2 & 0x1f;
        Temp |= 0x80;               // 选通LED锁存器
        P2 = Temp;
        Temp = P2 & 0x1f;
        P2 = Temp;
        Temp_0_Old = Temp_0_Val;
    }
}

void Led_Off() {
    unsigned char Temp;
    P0 = 0xff;                     // 全灭(低电平点亮,全1=全灭)
    Temp = P2 & 0x1f;
    Temp |= 0x80;
    P2 = Temp;
    Temp = P2 & 0x1f;
    P2 = Temp;
    Temp_0_Old = 0x00;
}

几个值得注意的设计细节:

1) 数组化接口:业务层用 ucLed[8] 数组控制每个LED,ucLed[0]=1 就是点亮L1。Led_Disp函数内部把数组转成一个字节再操作锁存器。这样上层代码不需要关心位操作。

2) 脏检测if(Temp_0_Val != Temp_0_Old) 这一行是锁存器操作的重要优化。Led_Proc在调度器里每1ms执行一次,但LED状态可能几百毫秒才变一次。如果不做脏检测,每1ms都操作一次锁存器完全是浪费。

3) ~Temp_0_Val 取反:因为LED是低电平点亮。这个取反很容易忘,忘的话LED的亮灭逻辑就全反了。


蜂鸣器和继电器:高电平有效,共用一个锁存器

蜂鸣器和继电器跟LED不同,它们是高电平有效的。而且它们共用U9锁存器(0xA0):

  • P0.4 → 继电器
  • P0.6 → 蜂鸣器

项目代码里的实现:

idata unsigned char Temp_1_Val = 0x00, Temp_1_Old = 0xff;  // 一定要赋初值!

void Relay(bit Enable) {
    unsigned char Temp;
    if(Enable)
        Temp_1_Val |= 0x10;        // P0.4 置1
    else
        Temp_1_Val &= ~(0x10);     // P0.4 清0

    if(Temp_1_Val != Temp_1_Old) {
        P0 = Temp_1_Val;            // 注意:不取反!高电平有效
        Temp = P2 & 0x1f;
        Temp |= 0xa0;               // 蜂鸣器/继电器锁存器
        P2 = Temp;
        Temp = P2 & 0x1f;
        P2 = Temp;
        Temp_1_Old = Temp_1_Val;
    }
}

void Beep(bit Enable) {
    unsigned char Temp;
    if(Enable)
        Temp_1_Val |= 0x40;        // P0.6 置1
    else
        Temp_1_Val &= ~(0x40);     // P0.6 清0

    if(Temp_1_Val != Temp_1_Old) {
        P0 = Temp_1_Val;
        Temp = P2 & 0x1f;
        Temp |= 0xa0;
        P2 = Temp;
        Temp = P2 & 0x1f;
        P2 = Temp;
        Temp_1_Old = Temp_1_Val;
    }
}

这里有一个非常巧妙的设计:继电器和蜂鸣器共用 Temp_1_Val 这个状态变量,通过位操作(|= 和 &=~)独立控制各自对应的位,互不干扰。比如你同时开继电器和蜂鸣器,Temp_1_Val = 0x50(P0.4和P0.6都为1),操作一次锁存器就能同时控制两个外设。

还有一个关键细节:Temp_1_Old = 0xff 的初始值Temp_1_Val 初始为 0x00(全部关闭),Temp_1_Old 初始为 0xff。这样第一次调用 Relay 或 Beep 时,0x00 != 0xff 成立,脏检测通过,锁存器会被正确写入。如果你把 Temp_1_Old 也初始化为 0x00,那第一次调用时脏检测不通过,锁存器不会被写入——蜂鸣器和继电器的初始状态就不确定了。这个bug非常隐蔽,比赛时踩到基本就得跪。

LED/蜂鸣器/继电器的高/低电平对比:

外设 有效电平 P0数据 初始化值
LED 低电平 ~Temp_0_Val 0xFF(全灭)
继电器 高电平 Temp_1_Val 0x00(全关)
蜂鸣器 高电平 Temp_1_Val 0x00(全关)

数码管驱动:消影是关键

数码管是共阳极的,和LED一样低电平点亮。但跟LED不同的是,数码管需要动态扫描——每次只亮一位,8位轮流显示,利用人眼的视觉暂留效应看起来就像同时亮着。

消影问题是数码管驱动中最容易出bug的地方。先看看不做消影会怎样:

时刻1: 位选=第0位, 段选="3" → 第0位显示"3"
时刻2: 位选=第1位, 段选="5" → 但是在切换位选的瞬间,
       第0位还没来得及灭掉,段选已经变成了"5"
       → 第0位短暂地闪烁了"5"(重影!)

解决办法就是消影——先关所有段选,再切换位选,最后开新的段选:

code unsigned char Seg_Dula[] = {
    0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90,  // 0~9
    0xff,   // 10: 全灭
    0xC7,   // 11: L
    0xBF,   // 12: -
    0x86,   // 13: E
    0x8C    // 14: P
};

void Seg_Disp(Wela, Dula, Point) {
    unsigned char Temp;

    /* 第一步:消影 —— 关闭所有段选 */
    P0 = 0xff;
    Temp = P2 & 0x1f;
    Temp |= 0xe0;       // 段选锁存器
    P2 = Temp;
    Temp &= 0x1f;
    P2 = Temp;

    /* 第二步:位选 —— 选哪一位 */
    P0 = 0x01 << Wela;
    Temp = P2 & 0x1f;
    Temp |= 0xc0;       // 位选锁存器
    P2 = Temp;
    Temp &= 0x1f;
    P2 = Temp;

    /* 第三步:段选 —— 显示什么 */
    P0 = Seg_Dula[Dula];
    if(Point) P0 &= 0x7f;   // 显示小数点(清除最高位)
    Temp = P2 & 0x1f;
    Temp |= 0xe0;       // 段选锁存器
    P2 = Temp;
    Temp &= 0x1f;
    P2 = Temp;
}

消影三步曲:关段选 → 切位选 → 开段选。顺序不能乱。

小数点编码的巧妙设计

数码管缓冲区 Seg_Buf[8] 里的每个元素本应该存0~14的数字索引。但显示小数点怎么办?代码用了一个很巧妙的办法:

// 显示小数点:在数字上加上 ',' 的ASCII码(44)
Seg_Buf[6] = Speed_Now_10x / 10 % 10 + ',';

// 在中断里解码
if(Seg_Buf[Seg_Pos] >= ',')                           // >= 44 说明要显示小数点
    Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);      // 减去44得到数字,Point=1
else
    Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);             // 不带小数点

这样就不用额外维护一个"小数点标志位数组"了,一个数组同时编码了数字值和小数点标志。

动态扫描在中断里完成

void Time1_Server() interrupt 3 {
    // ... 其他处理 ...

    Seg_Pos = (++Seg_Pos) % 8;          // 每次+1,0~7循环
    if(Seg_Buf[Seg_Pos] >= ',')
        Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);
    else
        Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);
}

Timer1每1ms中断一次,每次中断刷新一位,8次中断完成一轮扫描,刷新率 = 1000/8 = 125Hz,远超人眼50Hz的临界闪烁频率,所以看起来完全不会闪烁。


系统初始化

上电时必须正确初始化所有外设。Init.c 里的代码:

void System_Init() {
    unsigned char Temp = 0x00;

    /* LED初始化 —— 全部熄灭(低电平点亮,0xFF=全灭)*/
    P0 = 0xff;
    Temp = P2 & 0x1f;
    Temp |= 0x80;    // LED锁存器
    P2 = Temp;
    Temp = P2 & 0x1f;
    P2 = Temp;

    /* 蜂鸣器/继电器初始化 —— 全部关闭(高电平有效,0x00=全关)*/
    P0 = 0x00;
    Temp = P2 & 0x1f;
    Temp |= 0xa0;    // 蜂鸣器/继电器锁存器
    P2 = Temp;
    Temp = P2 & 0x1f;
    P2 = Temp;
}

注意LED和蜂鸣器/继电器的初始化P0值正好相反——LED是 0xFF(全灭),蜂鸣器/继电器是 0x00(全关),因为它们的极性不同。


7474HC573、LED驱动、HC573、LED驱动、踩坑总结

  1. LED取反忘写:LED低电平点亮,P0要取反。这个错犯了至少三次。
  2. P2低5位没保护P2 & 0x1f 不能省,否则I2C通信会出问题。
  3. 数码管没消影:必须是"关段选→切位选→开段选",直接切位选会重影。
  4. Temp_1_Old 初始值:必须是 0xff,否则第一次操作锁存器不会生效。
  5. 蜂鸣器不取反:蜂鸣器/继电器是高电平有效,P0直接赋值,不要取反!跟LED相反。

锁存器操作口诀:P0放数据,P2开门,P2关门。每个细节都不能错。

 

Logo

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

更多推荐