二進位時鐘是一個有吸引力的項目,它可以讓您展示您的數位電子、微控制器程式設計知識以及物理組裝和中等複雜電路的能力。關於這個主題已經寫了很多文章。然而,很少有人深入研究設計決策和必要的妥協。這是一個遺憾,因為該主題提供了一個極好的學習機會,可以探索微控制器的物理限制、歐姆定律的應用、多工多個 LED 的收益遞減以及乾淨且易於故障排除的系統的元件佈局。
本文重點在於二進位時鐘形式的六列乘四行 LED 矩陣,如圖 1 所示。為了獲得最大的可訪問性,我們將討論限制在 Arduino Nano Every 上的 I/O 引腳和電源的物理功能。請注意,本文中提出的想法直接適用於七段 LED 顯示器,或更普遍地適用於所有多工 LED 陣列。
現在是回顧二進位時鐘操作的好時機,因為我們將重點放在低階硬體和多工方面。這篇 DigiKey Arduino 文章是一個很好的起點,它描述如何讀取時鐘。它還提供了對比設計以及 Arduino 程式碼,可以幫助您更好地理解多工操作。
圖 1:麵包板上 Arduino Nano Every 的圖片,背景為 LED 陣列和 Digilent Analog Discovery。
什麼是多工?
多工是一種有限資源的時間共享形式。在我們的範例中,微控制器的 I/O 引腳數量有限。透過時分多工(Time Division Multiplexing,TDM),我們使用循環序列來連續啟動一個 LED 或一組 LED。透過快速排序 LED 激活,所有 LED 將顯示為同等點亮。這種品質取決於閃爍融合,即人眼無法偵測多工陣列中 LED 的快速「閃爍」。
LED 通常使用共陽極或共陰極配置成組排列。圖 2 所示的範例採用具有共陽極配置的 6 列 x 4 行 LED 矩陣。
技術提示:術語時分多工(TDM) 通常與乙太網路等通訊系統相關。在乙太網路中,許多電腦共用一個通訊通道。由於通道是有限的資源,因此每台電腦都在已建立的通訊協定內輪流進行。雖然我們對 LED 多工術語 TDM 的應用過於簡單化,但它是理解複雜通訊系統的一個很好的起點。
圖 2:採用 Arduino Nano Every 的 6 列 x 4 行多路 LED 矩陣示意圖。
術語「共陽極」和「共陰極」可能會引起混淆。例如,對圖 2 中的 LED 矩陣的檢查顯示,許多二極體按列共享陽極連接,並按行共享陰極連接。只有當我們檢查激活序列時,才能揭示共陽極配置。
圖 3 顯示了多工器時序的邏輯分析儀視圖。關於常見的二極體連接,我們注意到在任何給定時間只有一個列被啟動。因此,公共連接是陽極。在此範例中,關聯的列電晶體將所有關聯 LED 的陽極拉至 5.0VDC 電源軌。然後,各個行驅動器接地以啟動所需的 LED。
關於圖 3,我們觀察到:
-
列驅動訊號呈現為對應 Arduino D2 至 D7 引腳的前 6 個訊號。為了方便起見,訊號名稱已新增為紅色突出顯示文字。我們看到,在任何時候,只有一個訊號處於活動狀態。請注意,PNP 電晶體用作列驅動器。另外,請記住,該電晶體將透過將其基極拉向地而被激活。
-
最下面的 4 條線是對應 Arduino 輸出腳位 D19 至 D21 的行驅動器。如圖 2 所示,它們也是低電平有效,其中透過將二極體拉向地來打開二極體。
-
Digilent Analog Discovery 已配置為顯示與 4 行磁碟機相關的二進位數。這是某些邏輯分析儀的一項功能,允許操作員將多行「連接」成一個方便的 ASCII、十六進位或二進位顯示。為了方便起見,這已以藍色突出顯示。
技術提示:配置 LED 多工的一般方法有兩種,包括共陽極和共陰極。本文介紹具有共陽極 LED 的 PNP 列驅動器。這裡,PNP 電晶體將公共陽極連接到正極電源軌,然後將接地應用於各個陰極。前面提到的 DigiKey 二進位時鐘文章介紹了一個帶有 NPN 列驅動器和共陰極 LED 的矩陣。在那裡,NPN 電晶體將公共陰極接地,並將正電壓施加到所需的行。當將通用驅動器應用於行而不是列時,將組件旋轉 90 度時,需要仔細檢查。
圖 3:邏輯分析儀螢幕截圖,顯示 LED 時序,並添加了測試和亮點以供澄清。
技術提示:有關多路復用和共陽極/陰極的討論適用於七段 LED 顯示器和 LED 陣列。
微控制器如何限制多工顯示的電流?
本文受到 Arduino Nano Every 功能的限制。這包括可用電源以及 Nano Every 的板載電源、單一引腳和引腳組。
具有特色 ATMega4809 微控制器的相關 Arduino Nano Every 規格包括:
ABX00028-datasheet.pdf (arduino.cc)
-
5.0VDC 電源電流限制 = 950mA
-
單一引腳(源或匯)= 40mA
-
軌引腳(高溫下的拉電流或灌電流)= 100mA
至於所選的 SSL-LX21573GD LED:
-
室溫穩定電流 = 25mA
-
10us 脈衝的峰值電流 = 150mA
這些數字大部分是絕對設計最大額定值。為了延長使用壽命,我們必須保持較寬的安全餘裕。首先,我們將每個當前值減半。正如我們將看到的,這項決定對多工選項具有嚴重影響。它對圖 2 原理圖產生影響,對電晶體列驅動器和多工 LED 的數量產生影響。
技術提示:微控制器的內部晶片使用小型接合線連接到 I/O 接腳。這些電線和矽走線的載流能力有限。這反映在設備的設計最大規格。在前面的範例中,我們看到單一 I/O 引腳的設計最大值為 40mA。然而,以最大電流運行的三個這樣的引腳將淹沒晶片到電源或晶片到接地的接合線或矽走線。這種累積電流是一個重要但經常被忽略的設計考量。這個錯誤會毀掉微控制器,也許不會立即毀壞,但會導致產品不可靠。
亮度與佔空比的函數關係
在全面解釋電晶體列驅動器和電阻器選擇之前,我們必須先了解閃爍和 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);
}
當我們考慮對單一 LED 的影響時,此 PWM 操作與 TDM 操作直接相關。在這兩種情況下,LED 都會在給定的開啟和關閉時間內閃爍。在這兩種情況下,正是這個佔空比決定了 LED 亮度。
結果如圖 4 所示,其中 LED 工作在 100%、50%、25%、12.5% 和 6.25% 佔空比下,分別對應於 1、2、4、8 和 16 時分 TDM 時段的多工。對於低佔空比 LED 來說結果並不好。事實上,很難看到佔空比為 1/16 的 LED。
圖 4:顯示以不同佔空比驅動時 LED 的相對亮度的圖片。
多工方法和LED亮度的影響
我們的多工目標是在保持當前硬體限制的同時獲得最大 LED 亮度。現在我們對硬體的物理限制有了更好的了解,我們可以探索多工方法:
-
一次一個 LED:我們可以消除列驅動器並允許微控制器直接控制列和行。雖然這對於電路成本和簡單性來說是非常理想的,但我們很快就會遇到電流限制。如果沒有列驅動器,相關的微控制器接腳必須提供必要的電流。這限制了我們一次只能使用一個或兩個 LED,因為累積電流將超過微控制器的設計最大值。對於 6 x 4 陣列,這需要 4% 或 8% 的佔空比。LED 燈將非常暗淡。
-
一次一列,列驅動器和行由微控制器控制:透過 PNP 電晶體向列提供電流,我們可以自由地打開所有行。對於 6 x 4 陣列,這會導致每個 LED 的佔空比為 17%。對於光線昏暗的房間來說,本文所提供的結果是最低限度可以接受的。
-
一次一列,帶有列和行驅動電晶體。這種方法允許使用高於微控制器容量的電流來驅動 LED。由於 LED 的佔空比較低,因此可以透過增加電流來提高亮度。LED 資料表中存在一定的模糊性,但電流肯定可以增加到設計最大值,甚至可能增加 2 倍,而不會損壞 LED。回想一下,我們選擇的 LED 指定為 25mA 連續電流和 150mA 10us 脈衝。這種方法需要仔細考慮 LED 溫度,並可能導致 LED 壽命顯著縮短。使用風險自負。
-
並行控制:對於整篇文章,我們假設微控制器引腳是限制資源。有多種連接埠擴充選項可以直接控制 LED。8 位元 TLC6C598 移位暫存器就是一個例子。它具有 50mA 開漏驅動器,最大 VDS 為 40VDC。74HC595 是另一個適合麵包板原型設計的常見選項。
多工 LED 的電阻器選擇
需要選擇適當的電阻器來確定 LED 的工作電流。為列驅動電晶體的基極選擇合適的電阻器也很重要。當啟動的 LED 數量隨顯示數量變化時,此基極電阻對於保持一致的 LED 亮度非常重要。
LED 的計算相對簡單。第一步是確定當前的限制。透過所選的多工方案,在任何給定時間最多有 4 個 LED 處於活動狀態。我們遇到的第一個限制是微控制器的累積電流。對於我們保守的設計,該電流為 50mA。因此,每個 LED 的電流被限制在 13mA 左右。電阻計算如下:
R_{LED} = \dfrac{V_{rail} – V_{LED}}{I} = \dfrac {5.0-2.0}{0.013}= 220\, \Omega
我們假設 PNP 列驅動器 V_{CE} 非常接近零。
對於電晶體基極電阻的計算,我們將實施強制 beta 條件。此電晶體工作點可確保電晶體深度飽和,從而確保所有 LED 具有相同的亮度。為了設定此條件,我們選擇電阻器,使基極電流為集電極電流的 1/10。假設有 4 個活動 LED,集電極電流約 50mA。透過強制 beta 操作,我們將強制基極電流為 5mA。
R_{Base} = \dfrac{V_{rail} – V_{diode}}{I} = \dfrac {5.0-0.7}{0.005} \approx 820\, \Omega
改善空間
這篇文章的重點是多工LED 的各個方面。很少甚至沒有考慮將該設備作為可靠的計時器來操作。本說明所附的程式碼使用了 Arduino millis() 函數,該函數並不被認為是可靠的即時計時器。所有此類設備都依賴微控制器的高速振盪器,該振盪器的精確度遠不如正確實現的 32.768kHz 時脈晶體。
為了提高效能,您可能需要整合即時時鐘模組。對於額外的挑戰,請嘗試使用具有天線的基於 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);
}
}