设计二进制时钟: 理解多路复用约束

设计一个二进制时钟是一个有吸引力的项目,它能让您展示数字电子学、微控制器编程以及物理组装和中等复杂电路的能力。关于这个主题,已经有许多文章进行了探讨。然而,深入探讨设计决策和必要的妥协的文章却不多。这是一个遗憾,因为这个主题提供了一个极好的机会来探索微控制器的物理限制、欧姆定律的应用、多路复用的递减收益以及易于故障排除的组件布局。

本文主要讨论以二进制时钟形式呈现的六列四行 LED 矩阵,如图1所示。为了最大的可访问性,我们将把对话限制在 Arduino Nano Every 上的I/O引脚和电源的物理功能上。请注意,本文中提出的想法直接适用于七段 LED 显示器,或者更一般地说,适用于所有多路LED阵列。

现在是回顾二进制时钟操作的好时机,因为我们将重点关注底层硬件和多路复用方面。

1 : Arduino Nano Every 在面包板上的图片,背景是 LED 阵列和 Digilent Analog Discovery。

多路复用定义

多路复用是有限资源的一种时间共享形式。在我们的例子中,微控制器只有有限的I/O引脚。通过使用时分多路复用(TDM),我们能够用循环序列连续激活LED或LED组。通过快速切换 LED 的激活状态,所有LED看起来都是一样亮的。这种效果依赖于“闪烁融合”(flicker fusion),即人眼无法检测到多路复用阵列中LED的快速切换。

LED通常以公共阳极或公共阴极的形式排列成组。图2展示了一个具有6列4行 LED 矩阵和公共阳极配置的示例。

技术提示: 术语“时分多路复用”(TDM)通常与以太网等通信系统相关联。在以太网中,许多电脑共享一个通信通道。由于信道是有限的资源,每台计算机根据已建立的通信协议轮流使用。虽然我们将TDM这一术语应用于LED多路复用是一种简化,但它确实为我们理解复杂的通信系统提供了一个很好的起点。

2 :采用Arduino Nano Every的6列4行复用LED矩阵示意图。

公共阳极和公共阴极这两个术语会引起混淆。例如,对图2中的LED矩阵的检查显示,许多二极管按列共享阳极连接,按行共享阴极连接。只有当我们检查激活序列时,才会发现共同的阳极配置。

图3给出了多路复用器时序的逻辑分析仪视图。关于公共二极管连接,我们注意到在任何给定时间有一个且只有一个列被激活。因此,共同连接是阳极。在本例中,关联列晶体管将所有关联led的阳极拉到5.0 VDC导轨上。单独的行驱动器然后应用一个地来激活所需的LED(s)。

关于图3,我们观察到:

列驱动信号呈现为Arduino D2到D7引脚对应的前6个信号。为方便起见,已将信号名称添加为红色高亮文本。我们看到一个,而且只有一个,信号在任何时候都是活跃的。请注意,PNP晶体管被用作列驱动器。另外,回想一下,这个晶体管将通过将其底座拉向地面来激活。

最低的4行是Arduino输出引脚D19到D21对应的行驱动器。这些也是如图2所示的低电平激活,其中二极管通过将其拉向地面打开。

Digilent Analog Discovery已配置为显示与4行驱动器相关的二进制数。这是一些逻辑分析仪的一个功能,允许操作员将几行“加入”成一个方便的ASCII, HEX或二进制显示。为了方便起见,我们用蓝色突出显示了这一点。

技术提示 :配置LED多路复用有两种常用方法,包括公共阳极和公共阴极。本文的特点是PNP列驱动器与共同阳极LED。在这里,PNP晶体管将公共阳极连接到正轨,然后将接地应用到各个阴极。当组件旋转90度,将公共驱动器应用于行而不是列时,需要仔细检查。

3 :逻辑分析仪屏幕截图显示了带有测试和高光的LED定时。

技术提示 :关于多路复用和共阳极/阴极的讨论适用于七段LED显示器和LED阵列。

当前的限制

本文受到Arduino Nano Every功能的限制。这既包括可用的电源,也包括Nano Every的板载电源、单个引脚和引脚组。

具有ATMega4809微控制器规格的相关Arduino Nano Every是:

ABX00028-datasheet.pdf (arduino.cc)

  • 5.0直流电源电流限制= 950毫安

  • 单个引脚(源或汇)= 40毫安

  • 电源轨引脚(在高温下)= 100毫安

对于选用的SSL-LX2573GDLED灯:

  • 室温稳定电流= 25毫安

  • 10 us脉冲的峰值电流= 150毫安

这些数字中的大多数是绝对设计最大额定值。为了长寿命,我们必须保持较宽的安全裕度。作为起点,我们把每一个电流值减半。我们将看到,这个决定对复用选项有严重的影响。它在图2原理图上投下阴影,对晶体管列驱动器和多路复用led的数量产生影响。

技术提示 :微控制器的内部芯片使用小键合线连接到I/O引脚。这些线与硅走线一起具有有限的载流能力。这反映在器件的设计最大规格上。在前面的例子中,我们看到单个I/O引脚具有40 mA的设计最大值。然而,三个这样的引脚在最大电流下工作将压倒模对电源或模对地的键合线或硅走线。这种累积电流是一个重要但经常被忽视的设计考虑因素。这是一个会破坏微控制器的错误,也许不会立即破坏,但会导致产品不可靠。

占空比vs亮度

在解释晶体管列驱动器和电阻选择之前,我们需要理解闪烁与LED亮度之间的关系。请记住,LED矩阵是采用时分多路复用(TDM)的。每个LED都会“闪烁”,因为它会依次打开和关闭,如图3中的时序图所示。您可能已经进行过相关的实验,探索了LED的闪烁现象。

在您学习 Arduino 的早期阶段,您可能已经使用 PWM 命令来调整 LED 的亮度,例如:

void loop() {
    static uint8_t val;             // Maintain contents across loop iterations
    analogWrite(LED_PIN, val++);    // About 5 seconds to reach 100% duty cycle
    delay(20);
}

这个PWM操作与TDM操作直接相关,当我们考虑对单个LED的影响时。在两种情况下,LED都会以一定的时间开启和关闭,并且这种开启和关闭的时间比例决定了LED的亮度。

结果如图4所示,LED分别以100%、50%、25%、12.5%和6.25%的占空比工作,这分别对应于TDM槽的1、2、4、8和16个时间段。对于低占空比的LED,结果并不理想。事实上,很难看到占空比为1/16的LED。

4 :不同占空比下led的相对亮度。

多路复用的方法

我们的多路复用目标是获得最大的LED亮度,同时保持在当前的硬件限制内。现在我们对硬件的物理限制有了更好的了解,我们可以探索复用方法:

  • 一次一个LED:我们可以消除列驱动器,并允许微控制器直接控制列和行。虽然这对于电路成本和简单性来说是非常理想的,但我们很快就遇到了当前的限制。如果没有列驱动器,相关的微控制器引脚必须提供必要的电流。这限制了我们一次只能使用一个或两个LED,因为累积电流将超过微控制器设计的最大值。考虑到6 x 4阵列,这需要4%或8%的占空比。LED会很暗。

  • 每次一列,由微控制器控制列驱动器和行:使用PNP晶体管向列提供电流,我们可以自由地打开所有行。给定6 × 4阵列,这导致每个LED的占空比为17%。本文中给出的结果对于光线昏暗的房间来说是最低限度可接受的。

  • 用列、行驱动晶体管一次一列。这种方法允许LED以高于微控制器容量的电流驱动。由于LED具有低占空比,因此可以通过增加电流来增加亮度。在LED数据表中有一定的模糊性,然而,电流肯定可以增加到设计最大值,也许是2倍以上,而不会损坏LED。回想一下,我们选择的LED指定为连续25 mA和150 mA, 10 us脉冲。这种方法需要仔细考虑LED温度,并可能导致LED寿命显著缩短。使用风险自负。

  • 并行控制:对于这篇文章,我们假设微控制器引脚是有限的资源。有任何数量的端口扩展选项,将允许直接控制LED。一个例子是8位TLC6C598移位寄存器。它具有50mA开漏驱动器,最大VDS为40 VDC。74HC595是另一种适用于面包板原型设计的常见选择。

电阻的选择

需要选择合适的电阻来确定 LED 的工作电流。为柱驱动晶体管的基极选择合适的电阻也很重要。当激活LED 的数量随着显示的数量变化时,这个基极电阻对于保持一致的 LED 亮度很重要。

LED 的计算相对简单。第一步是确定当前的局限性。使用所选的多路复用方案,在任何给定时间最多可激活4个LED。我们遇到的第一个限制是微控制器的累积电流。对于保守的设计,这是50mA。因此,每个LED被限制在约13 mA。电阻器计算为:

image

这里我们假设PNP列驱动器VCE非常接近于零。
对于晶体管基极电阻的计算,我们将实现一个强制的beta条件。这个晶体管工作点确保晶体管深度饱和,这将确保所有LED具有相同的亮度。为了设置这个条件,我们选择电阻器,使基极电流为集电极电流的1/10。给定4个有源LED,集电极电流约为50 mA。通过强制beta操作,我们将强制基极电流为5mA。

image

改进空间

这篇文章的重点是多路LED的各个方面。很少甚至没有考虑将设备作为可靠的计时器来操作。本说明所附的代码使用了 Arduino millis() 函数,该函数不被称为可靠的实时计时器。所有这些设备都依赖于微控制器的高速振荡器,它不像正确实现的32.768 kHz时钟晶体那样精确。

为了提高性能,你可能想要集成一个实时时钟模块。对于一个额外的挑战,可以尝试一个基于 GPS 或 WWVB (原子钟接收器)的带天线的计时器。

#define C0_PIN 2
#define C1_PIN 3
#define C2_PIN 4
#define C3_PIN 5
#define C4_PIN 6
#define C5_PIN 7

#define R0_PIN 21
#define R1_PIN 20
#define R2_PIN 19
#define R3_PIN 18

void set_column(uint8_t c) {

  digitalWrite(C0_PIN, HIGH);
  digitalWrite(C1_PIN, HIGH);
  digitalWrite(C2_PIN, HIGH);
  digitalWrite(C3_PIN, HIGH);
  digitalWrite(C4_PIN, HIGH);
  digitalWrite(C5_PIN, HIGH);

  switch (c) {
    case 0: digitalWrite(C0_PIN, LOW); break;
    case 1: digitalWrite(C1_PIN, LOW); break;
    case 2: digitalWrite(C2_PIN, LOW); break;
    case 3: digitalWrite(C3_PIN, LOW); break;
    case 4: digitalWrite(C4_PIN, LOW); break;
    case 5: digitalWrite(C5_PIN, LOW); break;
    default: break;
  }

}


void set_row(uint8_t n) {

  bool D0 = ((n & 0x01) == 0);
  bool D1 = ((n & 0x02) == 0);
  bool D2 = ((n & 0x04) == 0);
  bool D3 = ((n & 0x08) == 0);

  digitalWrite(R0_PIN, D0);
  digitalWrite(R1_PIN, D1);
  digitalWrite(R2_PIN, D2);
  digitalWrite(R3_PIN, D3);

}


void set_LED(uint8_t c, uint8_t n) {

  set_column(c);
  set_row(n);

}


void setup() {
  pinMode(C5_PIN, OUTPUT);
  pinMode(C4_PIN, OUTPUT);
  pinMode(C3_PIN, OUTPUT);
  pinMode(C2_PIN, OUTPUT);
  pinMode(C1_PIN, OUTPUT);
  pinMode(C0_PIN, OUTPUT);

  pinMode(R3_PIN, OUTPUT);
  pinMode(R2_PIN, OUTPUT);
  pinMode(R1_PIN, OUTPUT);
  pinMode(R0_PIN, OUTPUT);
}


void loop() {

  uint8_t i;

  uint32_t now = millis() /1000;

  uint8_t seconds = now % 60;
  uint8_t minutes = (now / 60) % 60;
  uint8_t hours = (now / 3600) % 24;

  uint8_t hoursHigh = hours / 10;
  uint8_t hoursLow = hours % 10;

  uint8_t minutesHigh = minutes / 10;
  uint8_t minutesLow = minutes % 10;

  uint8_t secondsHigh = seconds / 10;
  uint8_t secondsLow = seconds % 10;

  for (i = 0; i < 6; i++) {

    switch (i) {

      case 0: set_LED(0, secondsLow); break;
      case 1: set_LED(1, secondsHigh); break;
      case 2: set_LED(2, minutesLow); break;
      case 3: set_LED(3, minutesHigh);break;
      case 4: set_LED(4, hoursLow); break;
      case 5: set_LED(5, hoursHigh); break;
      default: break;
    }
    delay(2);
  }
}