LED 數碼管,你可以將它看做是 N 個發光二級管的組合,一個燈負責顯示一個段,七個段組合一位數字,再加一個小數點,這么一來,一位數碼管就有八段。一般,按照順時針的方向給每個段編號。

上圖中的 h 就是顯示小數點的段,許多電路圖上都標為 dp。
這么看來,要顯示一位數字,你就需要九根連接線。由於連接的方向不同,又產生了“共陽”和“共陰”兩個概念。
共陽:即共享陽極,也就是電源正極。導線V接到電源正極上(需要串聯電阻,網上很多說要 1k 歐,其實400-500歐就可以了),然后從V並聯出八條走線,分別連接八段數碼管,而每段數碼管的負極都單獨連接。這九根線就成了一正八負。
共陰:就是使用共同的負極。用八條線(設為V1到V8),分別單獨連接電源正極,然后串聯電阻,依次接到八段數碼管上,最后每段數碼管的負相同,即八正一負。
你要是覺得別人的圖太復雜看不懂,老周替你找了一張簡單的。

至於說怎么分辨出共陽和共陰,根據上面對二者的特點描述,方法也不難。首先,一條線連到電源正極,一條聯到負極(當然不要忘了串電阻),然后在數碼管上隨便找兩個引腳接入電路,並且要保證連接后其中某一段LED會亮的。這時候,你保持電源負極不變,用其他引腳輪流去接觸電源正極,如果有多個LED發光,說明你手上的玩意兒是共陰的。同樣,保持電源正極連接不變,依次嘗試把其他引腳接到負極,如果有多段LED發光,說明是共陽的。
那么,開發板如何控制哪段LED發光,哪段不發光?這里頭的原理,還是那個不變的規律——電流從高電勢流向低電勢,即電壓高的會流向電壓低的。
1、共陽數碼管:共用電源正極,可以認為它輸出的是高電平,然后八個段接到 GPIO 口,要想哪段LED發光就讓對應的接口輸出低電平,不發光就輸出高電平。
2、共陰數碼管:共用電源負極,可以認為它輸出的是低電平,要讓某段LED發光,就讓對應的 GPIO 口輸出高電平。
一位數碼管就占用了九個 GPIO 接口了,要是兩位數呢,再加九個,那就成了十八個了,要是有四位數呢,那估計你要買幾塊開發板了。就算你拼接了幾塊開發板,如何統一控制就很頭痛了。為了節約 GPIO 引腳資源,於是又有新名詞問世了——段掃描。
這里咱們就別管它是靜動掃描還是動態掃描,因為我們今天的主題是借助專門的驅動芯片的,所以有關掃描的事兒,簡單了解就行。為了減少接線數量,可以把每位數的段合為一個並聯電路,再單獨一根線來控制數字位。例如

這么一折騰,四位數碼管只需要 4 + 8 = 12 根線就能連接。不過,細心的你,此時肯定發現問題了,要是這樣連接,豈不是在同一時刻只能允許一段LED發光?那我需要多段LED發光咋辦?那就得掃描了,實際上就是不斷地執行循環,輪番地切換控制,只要切換的速度夠快,人眼是覺察不到閃爍的,於是就可以瞞天過海,騙過你的眼睛了。至於說能不能騙過貓的眼睛就不知道了,這有待生物學家們去驗證了。
比如,我要讓這四位數碼管顯示1213,好的,“1”是 b、c 段發光,其他段不發光

“2”是 a、b、d、e、g 五段發光。

“3”是a、b、c、d、g 發光。

第一步,顯示第一位“1”,把 1+ 接通,2+ 到 4+ 不通,再把 b c 段接通;
第二步,顯示第二位“2”,把 2+ 接通,1+、3+、4+ 不通,再接通 a b d e g 。
第三步,顯示第三位“1”,和第一位的段相同,但數位上是接通 3+,1+、2+、4+不通。
第四步,顯示第四位“3”,把 4+ 接通,其他位不通,再接通a b c d g。
最后讓上面四個步驟不斷地循環。
只要你的單片機夠快,你幾乎看不到閃爍。但樹莓派是帶操作系統的,不管怎樣,通過系統層再到硬件的調用肯定會慢一拍,會出現閃爍或者部分LED段亮度不夠的情況。這個循環可能用純粹的微控制器開發板會快一點。
然而,哪怕用上了掃描方案,還是不能解決問題。第一,占用開板的接口仍然很多,要是有八位數碼管,那得16個以上的接口了;第二,開發板把“精力”都花在循環掃描上了,就沒空去處理其他事情了,這樣未免太浪費。於是,就出現了專門驅動LED數碼管的芯片。常見的如 74HC595、TM1637、TM1638、TM1650 等。
本文老周介紹的是 TM1638,這個“TM”不是“他媽”的意思,而是指“天微電子”。所以,你不能讀作“他媽 1638”。1637 在微軟開源的 Iot.Bindings 庫里面已經封裝了。現在某寶上能買到的 TM1637 模塊基本上是封裝為時鍾模塊,即沒有小數點,而是中間加個“:”,顯示時鍾用的。
而 TM1638 一般封裝為一個復合模塊,老周買的是這個,有八位數碼管,下面有八個按鈕(有的是十六個按鈕),頂部有八個發光二極管。

這個模塊有除了供電的兩個引腳,用三根線來控制,怎么說也比用十幾根線來得簡便。
STB:可以理解為命令控制線,在發送命令之前,STB要拉到低電平,發完命令或讀取完按鈕信息后,需要把STB拉回高電平。
CLK:時鍾線,其實用來控制硬件的數據處理節奏。
DIO:數據線,高電平表示1,低電平表示0。
注意:不管是發送還是接收數據,都是從字節的低位開始的。
這個模塊,其實如果玩熟練了,並不復雜,只是它用的不是標准的 SPI、IIC 協議,所以我們只能自行封裝。依據數據手冊,每個二進制位的讀寫操作都在時鍾線的上升沿完成。上升沿就是 CLK 線從低電平轉到高電平的瞬間,這個時間極短,就算偵聽 PinEventTypes.Rising 事件(類似單片機中的中斷),有可能也來不及,因為模塊一旦收到此信號就會馬上處理。所以,我們在寫代碼時,可以換個思路——在每個時鍾上升沿到來之前把數據線DIO 的電平固定好,這樣就不怕由於時間來不及而導致讀寫錯位了。
不妨看看數據手冊中的時序圖。

從時序圖中可以看到。在CLK線發生上升沿時,DIO必須准備好數據(不管是拉高還是拉低),因為 TM1638 模塊是以上升沿作為數據發送的信號的。也就是說,只要是在CLK的上升沿到來之前,都可以修改DIO的電平。
故,下面的 WriteByte 方法,兩個版本都是可以的。
// 版本一 void WriteByte(byte val) { // 從低位傳起 int i; for (i = 0; i < 8; i++) { // 拉低clk線 _gpio.Write(CLKPin, 0); // 修改dio線 if ((val & 0x01) == 0x01) { _gpio.Write(DIOPin, 1); } else { _gpio.Write(DIOPin, 0); } // 右移一位 val >>= 1; // 拉高clk線,向模塊發出一位 _gpio.Write(CLKPin, 1); } } // 版本二 void WriteByte(byte val) { // 從低位傳起 int i; for (i = 0; i < 8; i++) { // 修改dio線 if ((val & 0x01) == 0x01) { _gpio.Write(DIOPin, 1); } else { _gpio.Write(DIOPin, 0); } // 右移一位 val >>= 1; // 拉低clk線 _gpio.Write(CLKPin, 0); // 拉高clk線,向模塊發出一位 _gpio.Write(CLKPin, 1); } }
兩個版本的區別在於:第一個版本中,每次發送二進制位時,先拉低CLK,再改變DIO,再拉高CLK;第二個版本則是先改變DIO的電平,再拉低CLK,然后又拉高CLK。
其核心就是——每個二進制位都要制造一個CLK的上升沿,所以CLK在什么時候拉低不重要,重要的是只有拉低再拉高才能產生電平上升的跳變過程。
而STB線的使用並不是看每個字節,而是看命令,發送命令前,STB要拉低電平,發送完命令后,STB線要拉高。命令可能是一個字節,也可能是兩個、三個字節。總之,發送一條命令前要拉低STB,發完后要拉高STB。
下面看看有哪些命令可用。

這個表把命令分為三類:設置命令、顯示控制、要操作的寄存器的地址。模塊通過一個字節的最高兩位(B6、B7就是第7、8位)來區分。比如,你要調整數碼管的顯示亮度,屬於顯示控制命令,因此,你寫入的命令字節的最高兩位必須是 0b 10xx xxxx。
1、設置命令
格式:0b_01xx_xxxx

通過上表,會發現一件事——當把無關項全填上0后,原來有兩條命令是一樣的。配置模塊為寫顯示寄存器模式時的命令是 0100 0000,並且將寄存器尋址方式設為自動增加模式時,命令也是 0100 0000。
后面兩條測試命令我們可以不管它,先看第一條,把數據寫到顯示寄存器,也就是說你要八位數碼管顯示會么,就把要顯示的LED段數據寫入對應的寄存器中。不知道大伙伴們還記不得前文中說的,數碼管每個位有七段,加上小數點是八段,每段對應一個二進制位,喲西,正好是一個字節。排列順序是從低位到高位。
dp g f e d c b a
0 0 0 0 0 0 0 0
如果要顯示0,即a b c d e f 要點亮,那就是 0011 1111;
要顯示1,即 b c 段要點亮,也就是 0000 0110;
要顯示3,即 a b c d g 段要點亮,就是 0100 1111。
最高位是小數點,若要讓3后面的小數點點亮,就是 1100 1111。
要點亮的位放 1,不點亮的位放 0。
這款TM1638模塊有八位數碼管,因此,需要有八個寄存器來存放,每個寄存器對應一位。

可數據手冊中我們看到了十六個寄存器,地址從 0x00 到 0x0F。原來每個數碼位有兩個字節,占了兩個寄存器。第一個字節 SEG1 到 SEG8,就是一位數碼管中的八段,那么第二個字節中還有兩位(SEG9、SEG10)是啥?回過頭再看看這模塊,每一位數碼管上面都對應有一盞小燈,所以這第二個字節的第一位(SEG9)就是用來控制這個小燈亮不亮的,因為模塊只為單個數碼管配了一個燈,所以只有 SEG9 位有效,SEG10 用不上。
舉個例子,假如我要在第二位數碼管上顯示“1”,從表中看到,GRID2 的 SEG1-SEG8,對應寄存器地址為 0x02,前面我們分析過,顯示“1”,就是讓 b c 段發光,字節是 0000 0110,所以,往 0x02 寫入 0x06(0110)即可,如果還想點亮第二位數碼管上面的燈,就向 0x03 寫入 0x01(0000 0001)即可。
咱們進一步總結發現,點亮數碼管的寄存器地址都是偶數,即 2 * n,假設要控制第一位,地址就是 2 * 0 = 0,要控制第三位,則地址就是 2 * 2 = 4。排序從0開始,即第0位到第7位。
點亮數碼管上面的小燈,其寄存器地址是奇數,即 2 * n + 1,例如,要點亮第五位的小燈,寄存器地址為 2 * 4 + 1 = 9,寫入 0x80。
2、尋址與寫數據
下面說說兩種寄存器尋址方式,即設置命令中的
![]()
如果是自動增加地址,要發送兩條命令:
1、(STB拉低)一個字節,0100 0000,表示自增地址(STB拉高);
2、(STB拉低)N 個字節,其中第一個字節是首地址,之后是數據。模塊會將第一個數據字節寫入首地址,然后地址自動 +1,再寫第二個,……
例如,0x02 0x81 0x77 0x25,標定首地址是 0x02,把 0x81 寫入 0x02;然后地址 +1 變成 0x03,再把 0x77 寫入0x03;地址再++,變成0x04,把0x25寫入0x04(STB拉高)。
如果是固定地址呢
1、(STB拉低)發送命令 0100 0100,即 0x44(STB拉高);
2、(STB拉低)寫入兩個字節,第一個是地址 0x02,第二個是數據0x80(STB拉高);
3、(STB拉低)寫入兩個字節,第一個是地址 0x03,第二個是數據 0x77(STB拉高);
4、(STB拉低)寫入兩個字節,第一個是地址 0x04,第二個是數據 0x25(STB拉高)。
時序如下


3、顯示控制命令

顯示控制命令都是 10xx xxxx 格式,高四位字節都是 1000,參數設置用到的只有低四位。其中,低三位用來設置亮度,表中的“消光數量”說白了就是亮度調整,范圍是 0 - 7,因為只有三個二進制位,所以最大值只能是 7。第四位用來設置是否開啟數碼管的顯示,如果為 0 表示關閉數碼管顯示,就算你把亮度調到7也不會顯示;如果為 1 表示開啟數碼管顯示。說簡單一點就是,第四位,1 時開顯示器,0 是關顯示器。
=====================================================================================
好了,前面所講的都是理論介紹,這個模塊還有一個掃描按鍵的功能,這個老周下一篇爛文再扯,本文的重點是說說怎么寫顯存(顯示寄存器),即讓數碼管顯示指定內容。
前文中已經寫好了 WriteByte 方法,下面咱們再加一層封裝,寫個 WriteCommand 方法,用於向 TM1638 發送命令。
void WriteCommand(byte cmd, params byte[] data) { // 拉低stb _gpio.Write(STBPin, 0); WriteByte(cmd); if (data.Length > 0) { // 寫附加數據 foreach (byte b in data) { WriteByte(b); } } // 拉高stb _gpio.Write(STBPin, 1); }
如果命令只有一個字節,那么傳參數時只考慮 cmd 參數,data 參數忽略;如果命令帶附加數據,則傳給 data 參數。比如上面說的自動增加地址,cmd 傳寄存器地址,data 傳要寫入各個寄存器的數據。
隨后,我們再往上封裝一層,實現 SetChar 方法,直接設置要顯示的數據,以及顯示在第幾位數碼管上。
public void SetChar(byte c, byte pos) { // 寄存器地址 byte reg = (byte)(pos * 2); byte com = (byte)((byte)TM1638Command.SetDisplayAddress | reg); WriteCommand(com, c); }
參數 c 表示要寫入的數據,也就是一位數碼管中各個段的二進制位的值;pos 參數指的顯示在第幾位,老周買的這個模塊有八位數碼管,所以,pos 參數的取值范圍是 0 到 7。寄存器的地址就是 pos * 2。
為了在初始化時,或者需要時清空所有數碼管的顯示(所有二進制位置0),還要寫一個 CleanChars 方法。
public void CleanChars() { int i = 0; while(i < 8) { SetChar(0x00, (byte)i); i++; } }
接下來是控制每位數碼管對應的小燈。
public void SetLED(byte n, bool on) { byte addr = (byte)(n * 2 + 1); //寄存器地址 // 1100_xxxx byte cmd = (byte)((byte)TM1638Command.SetDisplayAddress| addr ); byte data = (byte)(on? 1 : 0); WriteCommand(cmd,data); } public void CleanLEDs() { int i=0; while(i<8) { SetLED((byte)i, false); i++; } }
n 選擇控制第幾個燈,和數碼管一樣,從 0 到 7,on 表示是否點亮,true 點亮否則熄滅。
上面代碼用的命令,可以用枚舉類型聲明,使用時直接訪問。
internal enum TM1638Command : byte { // 讀按鈕掃描 ReadKeyScanData = 0b_0100_0010, // 自動增加地址 AutoIncreaseAddress = 0b_0100_0000, // 固定地址 FixAddress = 0b_0100_0100, // 選擇要讀寫的寄存器地址 SetDisplayAddress = 0b_1100_0000, // 顯示控制設置 DisplayControl = 0b_1000_0000 }
為了方便操作,也可以將常用的數字(0-9)的數據用常量聲明,使用時直接引用。
public class Numbers { public const byte Num0 = 0b_0011_1111; //0 public const byte Num1 = 0b_0000_0110; //1 public const byte Num2 = 0b_0101_1011; //2 public const byte Num3 = 0b_0100_1111; //3 public const byte Num4 = 0b_0110_0110; //4 public const byte Num5 = 0b_0110_1101; //5 public const byte Num6 = 0b_0111_1101; //6 public const byte Num7 = 0b_0000_0111; //7 public const byte Num8 = 0b_0111_1111; //8 public const byte Num9 = 0b_0110_1111; //9 public const byte DP = 0b_1000_0000; //小數點
}
下面是 TM1638 類的完整代碼,這里老周選用的是固定地址的寄存器讀寫方式。
public class TM1638 : IDisposable { GpioController _gpio; // 構造函數 public TM1638(int stbPin, int clkPin, int dioPin) { STBPin = stbPin; // STB 線連接的GPIO號 CLKPin = clkPin; // CLK 線連接的GPIO號 DIOPin = dioPin; // DIO 線連接的GPIO號 _gpio = new(); // 將各GPIO引腳初始化為輸出模式 InitPins(); // 設置為固定地址模式 InitDisplay(true); } // 打開接口,設定為輸出 private void InitPins() { _gpio.OpenPin(STBPin, PinMode.Output); _gpio.OpenPin(CLKPin, PinMode.Output); _gpio.OpenPin(DIOPin, PinMode.Output); } private void InitDisplay(bool isFix = true) { if (isFix) { WriteCommand((byte)TM1638Command.FixAddress); } else { WriteCommand((byte)TM1638Command.AutoIncreaseAddress); } // 清空顯示 CleanChars(); CleanLEDs(); WriteCommand(0b1000_1111); //亮度最高 + 開啟顯示 } #region 公共屬性 // 控制引腳號 public int STBPin { get; set; } public int CLKPin { get; set; } public int DIOPin { get; set; } #endregion public void Dispose() { _gpio?.Dispose(); } #region 輔助方法 void WriteByte(byte val) { // 從低位傳起 int i; for (i = 0; i < 8; i++) { // 拉低clk線 _gpio.Write(CLKPin, 0); // 修改dio線 if ((val & 0x01) == 0x01) { _gpio.Write(DIOPin, 1); } else { _gpio.Write(DIOPin, 0); } // 右移一位 val >>= 1; //_gpio.Write(CLKPin, 0); // 拉高clk線,向模塊發出一位 _gpio.Write(CLKPin, 1); } } void WriteCommand(byte cmd, params byte[] data) { // 拉低stb _gpio.Write(STBPin, 0); WriteByte(cmd); if (data.Length > 0) { // 寫附加數據 foreach (byte b in data) { WriteByte(b); } } // 拉高stb _gpio.Write(STBPin, 1); } #endregion public void SetChar(byte c, byte pos) { // 寄存器地址 byte reg = (byte)(pos * 2); byte com = (byte)((byte)TM1638Command.SetDisplayAddress | reg); WriteCommand(com, c); } public void SetLED(byte n, bool on) { byte addr = (byte)(n * 2 + 1); //寄存器地址 // 1100_xxxx byte cmd = (byte)((byte)TM1638Command.SetDisplayAddress| addr ); byte data = (byte)(on? 1 : 0); WriteCommand(cmd,data); } public void CleanChars() { int i = 0; while(i < 8) { SetChar(0x00, (byte)i); i++; } } public void CleanLEDs() { int i=0; while(i<8) { SetLED((byte)i, false); i++; } } }
下面簡單試一下,在第一位數碼管上顯示4,第四位數碼管上顯示2,第七位數碼管上顯示5。並點亮第二、第八盞小燈。
static void Main(string[] args) { using TM1638 dev = new(13, 19, 26); dev.SetChar(Numbers.Num4, 0); dev.SetChar(Numbers.Num2, 3); dev.SetChar(Numbers.Num5, 6); dev.SetLED(1, true); dev.SetLED(7, true); }
上傳到樹莓派上面,運行效果如下圖所示。

再給一個例子,咱們讀取一下樹莓派當前的 CPU 溫度,並用數碼管顯示。
static void Main(string[] args) { using TM1638 dev = new(13, 19, 26); while (true) { string result = File.ReadAllText("/sys/class/thermal/thermal_zone0/temp"); // 還要除以1000 result = (float.Parse(result) / 1000f).ToString("#.00"); Console.WriteLine("計算結果:\"{0}\"", result); // 拆分字符串,顯示各個數字 int len = result.Length; List<byte> datas = new List<byte>(); for (byte i = 0; i < len; i++) { // 小數點不單獨占一個位,要忽略 if (result[i] == '.') { continue; } char ch = result[i]; // 獲取顯示數據 byte b = Numbers.GetData(ch); // 如果該位不是最后一位 // 且下一個字符是小數點,則應該點亮 DP if (i < (len - 1) && result[i + 1] == '.') { b |= Numbers.DP; } datas.Add(b); } for (byte x = 0; x < datas.Count; x++) { dev.SetChar(datas[x], x); } Thread.Sleep(2000); } }
執行 dotnet 命令發布代碼。
dotnet publish
執行 scp 命令上傳到樹莓派。
scp -r bin\Debug\net5.0\publish\* pi@<樹莓派地址>:/home/pi/<你自己挑個目錄>
然后運行示例程序:dotnet xxx.dll
就能看到CPU的溫度了。

