在樹莓派Zero上使用C#+Mono驅動TM1637四位數碼管


最近閑着無聊,買了個樹莓派Zero,准備在上面跑.Net Core,來驅動各種傳感器

   就是上面這貨。之前手上已經有一個樹莓派3B+,但是介於3B+已經被我掛在路由器旁邊當做服務器用,不是很方便拿來研究接口,於是就挑了一個便宜的Zero玩玩,事實證明,我想太天真了,我以為只要是Linux系統,就能安裝.net Core,實際上呢,我整了一個晚上才不得不認識到一個事實:即便是.net Core也是認CPU架構的,Pi Zero用的ARMv6就是不支持,哎早知道在買之前多做做功課了,買一個樹莓派4也是個不錯的選擇啊。

幸好蒼天不負有心人,我找到了 另外一個能在Linux上面運行.net的途徑,那就是在Linux上面安裝一個Mono,然后.net通過Mono當做虛擬機運行,其實在原理上和.net core是差不多的,可是Mono在性能上比原生的.net core差了很多便是,不過我們只是用來跑外部模塊,也不是很需要多高性能便是了。

好了,嘮嗑正式結束,讓我們開始正題吧

首先,我們需要在Linux上面配置Mono的程序,講人話就是安裝Mono,不過在安裝之前,我們還需要更改源,畢竟樹莓派自帶的源別指望在國內有好的下載體驗

sudo sed -i 's#://raspbian.raspberrypi.org#s://mirrors.tuna.tsinghua.edu.cn/raspbian#g' /etc/apt/sources.list
sudo sed -i 's#://archive.raspberrypi.org/debian#s://mirrors.tuna.tsinghua.edu.cn/raspberrypi#g' /etc/apt/sources.list.d/raspi.list

運行上述兩條指令,把樹莓派自帶的源替換成清華源,這樣安裝Mono會快很多

sudo apt-get install mono-devel mono-complete mono-dbg

運行上面指令后,在樹莓派Zero上就會自動安裝配置完畢Mono環境了。

對了,為了方便調試,我們還需要配置SSH的遠程root連接

sudo nano /etc/ssh/sshd_config

運行上述指令后

 

 

 找到這一條,然后改成上圖這樣子后(其實也就去掉#,后面的參數改成yes罷了)

完事以后,按Ctrl+X,退出編輯並覆蓋保存就行。

sudo service ssh --full-restart

最后我們運行上述指令重啟SSH服務以后就能夠以root權限登錄樹莓派了。

以上是樹莓派的系統的配置過程。

接下來我們需要配置Visual Studio

首先我們新建一個項目,由於最新的Mono支持.net core,所以我們直接建立.net core 3框架的項目就行,而且甚至不需要拖家帶口帶上.net core那么多運行庫就能直接在Mono虛擬機下跑,簡直了...

然后,我們需要有一個擴展能夠直接在PC上遠程調試樹莓派上的程序,因此

搜索Mono的調試插件,有很多個,功能都差不多,挑一個順手的就行

安裝好Mono調試插件以后

 

 

需要配置下Mono調試插件的設置

 其實主要的無非就是這么幾個,新建一個配置,輸入IP、端口、用戶名和密碼,避免麻煩最好直接上root權限,反正自己用

然后每次調試的時候,點擊通過SSH生成和調試

 

 就能獲得和本地調試一樣的體驗,不得不說,這個體驗實在是太好了。

接下來是項目的

其實也就一點,在Nuget上面找一個第三方的庫來調用GPIO接口就行,沒別的了

Nuget下搜索Raspberry,下面的庫基本上都是關於調用樹莓派gpio的,隨便挑一個便是

我這邊選擇了文檔最為齊全的Unosquare.Raspberry.IO

 

 

 下面兩個是依賴項,尤其是WiringPi,是直接管理接口的主要庫

好,以上是准備工作,下面的是具體實現

 

 

 上面這張圖,對應的就是樹莓派Zero上,一共40個針腳的定義,其中,兩個5V的接口可以直接當做電源輸入或者輸出用,GND是接地這個沒啥好說的,我們主要看GPIO,這里有很多很多GPIO接口,這些接口才是負責信號輸入以及輸出使用,我們控制的主要也是這些接口。

 

 

 然后我們這次的主角也上場了

 

 注意看接線的顏色,其中CLK和DIO代表時鍾信號和數據信號,雖說是時鍾信號,其實是類似於發送命令的接口,因此都接GPIO,VCC是電源,這個沒啥好說的,就是輸入電源(注意看傳感器的電壓,如果電壓過高會燒毀傳感器,所以樹莓派預留了兩個3.3V的電壓接口),GND是接地,隨便找個接地的接口插上去就行。

 根據照片所示,我使用了4,14,16,18號接口,其中16口接了時鍾信號,18口接了數據信號

好了,線也接好了,環境也配置好了,我們正式開始編程階段

Pi.Init<BootstrapWiringPi>();//初始化通信接口,分配內存空間等
var clkPin = Pi.Gpio[BcmPin.Gpio23];//引用16接口
var dataPin = Pi.Gpio[BcmPin.Gpio24];//引用18接口
clkPin.PinMode = GpioPinDriveMode.Output;//設置16接口模式為輸出
dataPin.PinMode = GpioPinDriveMode.Output;//設置18接口模式為輸出

上面代碼是初始化階段,反正剛開頭照這個姿勢填就行了,值得注意的是接口引用部分

你看,我明明CLK接口插的是樹莓派16號物理接口,為啥這里引用的卻是GPIO23呢,其實這個是編碼方式的不同導致的,主要有以下兩種

  • BCM

編號側重CPU寄存器,根據BCM2835的GPIO寄存器編號。

  • wiringPi

編號側重實現邏輯,把擴展GPIO端口從0開始編號,這種編號方便編程。

 具體使用哪一種,需要看調用庫用的哪一套,因為我用的這個庫使用的是Bcm(引用的時候已經寫明了BcmPin)所以查表得知,16接口對應的GPIO23,18接口對應的GPIO24

接口配置完畢以后我們就可以正式開始驅動四位數碼管了

驅動數碼管實際上是操控TM1637芯片,我們的操作規程需要滿足TM1637芯片的特性,

其中最主要的特性是

 

//數據輸入開始
void startDisp()
{
clkPin.Write(GpioPinValue.High);//CLK拉為高電平 dataPin.Write(GpioPinValue.High);//DIO拉為高電平 dataPin.Write(GpioPinValue.Low);//和上面那句指令一起就是DIO由高變低 clkPin.Write(GpioPinValue.Low);//然后CLK上的時鍾信號拉低,DIO接口的數據允許改變,代表開始寫入數據
}

上面這四句執行完后,就表示告訴TM1637芯片,我要開始寫數據了,后面DIO接口的任何電位變化,都是我要寫的數據,下面就是寫數據的過程

//開始寫入數據
void writeByte(byte input)
{ for (int i = 0; i < 8; i++)//每次寫入一個byte,一共8bit {   clkPin.Write(GpioPinValue.Low);//確保無誤輸入前再拉低一次時鍾 ,代表開始寫入數據   if ((input & 0x01) == 1)//判斷每一位的高低電平   {     dataPin.Write(GpioPinValue.High);   }   else   {     dataPin.Write(GpioPinValue.Low);   }   input >>= 1;//每寫入完畢一次,移動一位   clkPin.Write(GpioPinValue.High);//每次輸入完一位,就拉高一次時鍾 } //應答信號ACK,這里本來是用來判斷DIO腳是否被自動拉低,代表上面寫入的數據TM1637已經接受到了
//但是我這里閑麻煩,直接將CLK信號低高低的拉,讓芯片直接執行下一步操作
clkPin.Write(GpioPinValue.Low);//先拉低 clkPin.Write(GpioPinValue.High);//需要判斷D是否為低電平此期間C一直拉高 clkPin.Write(GpioPinValue.Low);//應答完畢以后拉低C
}

 上面執行完以后,我們就已經向芯片發送了一個字節,也就是8位的數據,寫完以后,我們還需要告知芯片,數據傳輸完畢了

//結束條件是CLK為高時,DIO由低電平變為高電平
void stopDisp()
{
clkPin.Write(GpioPinValue.Low);//先拉低CLK,代表允許DIO改變數據 dataPin.Write(GpioPinValue.Low);//拉低DIO clkPin.Write(GpioPinValue.High);//CLK拉高,滿足結束條件前半部分 dataPin.Write(GpioPinValue.High);//DIO由低變高,代表數據輸入結束
}

好了,上面三段代碼一起執行完畢,就表示一個完整的,信號從准備輸入,開始輸入,結束輸入的過程,每次要輸入1字節,都需要經過以上三個過程,因此我們將上面三個過程分別寫成各自的方法,畢竟是需要經常調用的東西,而且基本上都不會變。以上的代碼實現,都是基於TM1637規格書,就是上面的接口說明實現的。

下面開始介紹該怎么在數碼管上顯示出東西來

在開始之前,我們先仔細看看數碼管是怎么一個樣子的

  //      A
  //     ---
  //  F |   | B
  //     -G-
  //  E |   | C
  //     ---
  //      D
   XGFEDCBA
  00111111,    // 0 //0x3f
  00000110,    // 1 //0x06
  01011011,    // 2 //0x5b
  01001111,    // 3 //0x4f
  01100110,    // 4 //0x66
  01101101,    // 5 //0x6d
  01111101,    // 6 //0x7d
  00000111,    // 7 //0x07
  01111111,    // 8 //0x7f
  01101111,    // 9 //0x6f
  01110111,    // A //0x77
  01111100,    // b //0x7C
  00111001,    // C //0X39
  01011110,    // d //0X5E
  01111001,    // E //0X79
  01110001     // F //0X71

上圖展示了數碼管,一個字樣的顯示方式,跟我們寫漢字一樣,一共准備了7個筆畫,我們想讓哪個筆畫亮起來,就讓那個筆畫的電平拉高就行,總的來說還是挺直觀的,因為我們只有7個筆畫,但是一個比特有8位,所有還有一位空置為低電平,如果有其他用處的話,可以補上()。於是我們把這些二進制,通過計算器換算成16進制的話就變成了0x3F樣式的字節碼

static byte[] Characters = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};//0~9,A,b,C,d,E,F            

當然,也可以自己任意根據上面的描述,編寫想要的走線圖案,不一定非要按照0到9的數字或者字母定式來寫。 

//設置基本參數 
startDisp();//開始寫入指令
writeByte(0x40);//指定功能參數
stopDisp();//結束寫入指令

//設置顯示地址以及顯示內容
startDisp();
writeByte(0xC0);//設置首地址,指向第一個字符
var Date = DateTime.Now.ToString("hhmm").ToCharArray();//獲得當前日期,並表示為小時分鍾
byte[] Characters = { 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71 };//0~9,A,b,C,d,E,F            
for (int i = 0; i <4; i++)//循環更改四個字符的顯示,想更改數碼管的顯示,只要更改循環體內的操作就行
{
  if (i != 1) writeByte(Characters[Date[i] - 48]);//從Characters數組根據索引獲得字符顯示的編碼
  else writeByte((byte)(Characters[Date[1] - 48] + 0x80));//第二個字符帶有冒號,因此將第一位空置拉高
}
stopDisp();

//開始寫入亮度
startDisp();
writeByte(0x8f);
stopDisp(); 

 以上代碼便是驅動數碼管顯示的完整代碼,循環運行上述代碼就能不斷驅動數碼管顯示當前的時間,同時更改循環體內的writeByte()方法參數,就能實現不同字符的顯示。startDisp();stopDisp(); writeByte();方法體,都在上面有完全的展示。

除去上述三個方法,

writeByte(0x40);//指定功能參數
writeByte(0xC0);//設置首地址,指向第一個字符
writeByte((byte)(Characters[Date[1] - 48] + 0x80));//顯示 :符號,這三個參數需要單獨講一下。
writeByte(0x8f);//指定亮度

首先,上述代碼的先后順序不能變,一定是先指定功能參數,后指定顯示位置,然后指定顯示內容,最后指定顯示亮度

而功能參數0x40寫入進去有啥用呢

我們查閱TM1637的規格書可知

 

 

 0x40翻譯成二進制便是

 0 | 1  | 0 | 0 |  0 | 0 | 0 |  0
B7|B6|B5|B4|B3|B2|B1|B0

根據上述表格我們可以知道01000000(0X40)所代表的的意思就是

1:數據寫到顯示寄存器,也就是功能是顯示

2:地址的增加模式是自+1

3:測試模式為普通模式

在此介紹一下前兩種的區別

第一條的意思就是,這個芯片是支持按鍵響應和屏幕輸出的,也就是說,如果B1置1則芯片功能是讀取按鈕 (雖然數位管上並沒有任何按鍵),B1置0就是顯示輸出模式

第二條的意思就是,

for (int i = 0; i <4; i++)//循環更改四個字符的顯示,想更改數碼管的顯示,只要更改循環體內的操作就行
{
  writeByte(Characters[Date[i] - 48]);//從Characters數組根據索引獲得字符顯示的編碼
}

如果B2置0,功能為自動地址增加模式,那么循環體內每次循環寫入一個字符以后,下一次循環光標位置就會移到下一個字符的位置,就和我們打字類似

那么如果B2置1,功能為固定地址模式的話,顧名思義,就是哪個位置顯示什么字符串由我們決定。

那么顯示代碼就變成了

  startDisp();
  writeByte(0xC0);//第一個字符
  writeByte(Characters[Date[0] - 48]);
  stopDisp();

  startDisp();
  writeByte(0xC1);//第二個字符
  writeByte(Characters[Date[1] - 48]);
  stopDisp();

  startDisp();
  writeByte(0xC2);//第三個字符
  writeByte(Characters[Date[2] - 48]);
  stopDisp();

  startDisp();
  writeByte(0xC3);//第四個字符
  writeByte(Characters[Date[3] - 48]);
  stopDisp();

那么這個0xC0,0xC1,0xC2,0xC3哪里來的呢,同樣查閱規格書可知

 

 就是11000000,11000001,11000010,11000011,上述幾個二進制轉換成16進制便是C0,C1,C2,C3,當然,該芯片最多可支持顯示6個字符

  對了,中間這個  : 的符號,並不占用一個字符顯示,這個符號歸類到0xC1地址內,被當成了一個標點使用

   XGFEDCBA
  00111111,    // 0 //0x3f

還記得上面那張圖吧,A~G,分別表示7個筆畫,但是多了一位閑置的在這里就派上用場了,只要把0xC1,也就是第二個字符的位置最高位置1,變成10111111,那么這個符號變會顯示出來

writeByte((byte)(Characters[Date[1] - 48] + 0x80));

代碼上就是在原先的基礎上加上0x80就可以了。

上面的步奏全部完成以后,其實只是把需要顯示的數據存到芯片里面而已,芯片還沒有輸出任何數據給數碼管,因為我們還什么都看不到

所以我們還需要再輸入一次命令

writeByte(0x8f);//指定亮度並顯示

那這個 0x8f又是哪里來的呢

 

 

這里同樣有一張表格,B3表示開關,B0~B2表示脈沖寬帶pwm,脈沖寬度越長,代表輸出給數碼管的時間越長,也就越亮,參照之前的方法,把對應的8位二進制轉換成16進制填入進去就行,我想都看到這里了,應該沒啥疑問的。

好,上述就是完整的教程,下面貼完整代碼

        private static void Main(string[] args)
        {
            Pi.Init<BootstrapWiringPi>();//初始化通信接口,分配內存空間等
            var clkPin = Pi.Gpio[BcmPin.Gpio23];//引用16接口
            var dataPin = Pi.Gpio[BcmPin.Gpio24];//引用18接口
            clkPin.PinMode = GpioPinDriveMode.Output;//設置16接口模式為輸出
            dataPin.PinMode = GpioPinDriveMode.Output;//設置18接口模式為輸出
            clkPin.Write(GpioPinValue.Low);//初始化電平為低,可不加
            dataPin.Write(GpioPinValue.Low);//初始化電平為低,可不加

            void startDisp()
            {
                //數據輸入開始
                clkPin.Write(GpioPinValue.High);//CLK拉為高電平
                dataPin.Write(GpioPinValue.High);//DIO拉為高電平
                dataPin.Write(GpioPinValue.Low);//和上面那句指令一起就是DIO由高變低
                clkPin.Write(GpioPinValue.Low);//然后CLK上的時鍾信號拉低,DIO接口的數據允許改變,代表開始寫入數據
            }
            void stopDisp()
            {
                //結束條件是CLK為高時,DIO由低電平變為高電平
                clkPin.Write(GpioPinValue.Low);//先拉低CLK,代表允許DIO改變數據
                dataPin.Write(GpioPinValue.Low);//拉低DIO
                clkPin.Write(GpioPinValue.High);//CLK拉高,滿足結束條件前半部分
                dataPin.Write(GpioPinValue.High);//DIO由低變高,代表數據輸入結束
            }
            void writeByte(byte input)
            {
                //開始寫入數據
                for (int i = 0; i < 8; i++)//每次寫入一個byte,一共8bit
                {
                    clkPin.Write(GpioPinValue.Low);//確保無誤輸入前再拉低一次時鍾 ,代表開始寫入數據
                    if ((input & 0x01) == 1)//判斷每一位的高低電平
                    {
                        dataPin.Write(GpioPinValue.High);
                    }
                    else
                    {
                        dataPin.Write(GpioPinValue.Low);
                    }
                    input >>= 1;//每寫入完畢一次,移動一位
                    clkPin.Write(GpioPinValue.High);//每次輸入完一位,就拉高一次時鍾
                }
                //應答信號ACK,這里本來是用來判斷DIO腳是否被自動拉低,代表上面寫入的數據TM1637已經接受到了,
                //但是我們這里閑麻煩,直接將CLK信號低高低的拉,讓芯片直接執行下一步操作
                clkPin.Write(GpioPinValue.Low);//先拉低
                clkPin.Write(GpioPinValue.High);//需要判斷D是否為低電平此期間C一直拉高
                clkPin.Write(GpioPinValue.Low);//應答完畢以后拉低C
            }
            void Show()
            {
                //設置基本參數
                startDisp();//開始寫入指令
                writeByte(0x40);//指定功能參數為自動增加
                stopDisp();//結束寫入指令

                //設置顯示地址以及顯示內容
                startDisp();
                writeByte(0xC0);//設置首地址,指向第一個字符
                var Date = DateTime.Now.ToString("hhmm").ToCharArray();//獲得當前日期,並表示為小時分鍾
                byte[] Characters = { 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71 };//0~9,A,b,C,d,E,F
                for (int i = 0; i < Date.Length; i++)
                {
                    if (i != 1) writeByte(Characters[Date[i] - 48]);//從Characters數組根據索引獲得字符顯示的編碼
                    else writeByte((byte)(Characters[Date[1] - 48] + 0x80));//第二個字符帶有冒號,因此將第一位空置拉高
                }

                //開始寫入亮度
                startDisp();
                writeByte(0x8f);
                stopDisp();
            }
            while (true)
            {
                Show();
            } 
        }

以上是字符地址自增加的代碼

        private static void Main(string[] args)
        {
            Pi.Init<BootstrapWiringPi>();//初始化通信接口,分配內存空間等
            var clkPin = Pi.Gpio[BcmPin.Gpio23];//引用16接口
            var dataPin = Pi.Gpio[BcmPin.Gpio24];//引用18接口
            clkPin.PinMode = GpioPinDriveMode.Output;//設置16接口模式為輸出
            dataPin.PinMode = GpioPinDriveMode.Output;//設置18接口模式為輸出
            clkPin.Write(GpioPinValue.Low);//初始化電平為低,可不加
            dataPin.Write(GpioPinValue.Low);//初始化電平為低,可不加

            void startDisp()
            {
                //數據輸入開始
                clkPin.Write(GpioPinValue.High);//CLK拉為高電平
                dataPin.Write(GpioPinValue.High);//DIO拉為高電平
                dataPin.Write(GpioPinValue.Low);//和上面那句指令一起就是DIO由高變低
                clkPin.Write(GpioPinValue.Low);//然后CLK上的時鍾信號拉低,DIO接口的數據允許改變,代表開始寫入數據
            }
            void stopDisp()
            {
                //結束條件是CLK為高時,DIO由低電平變為高電平
                clkPin.Write(GpioPinValue.Low);//先拉低CLK,代表允許DIO改變數據
                dataPin.Write(GpioPinValue.Low);//拉低DIO
                clkPin.Write(GpioPinValue.High);//CLK拉高,滿足結束條件前半部分
                dataPin.Write(GpioPinValue.High);//DIO由低變高,代表數據輸入結束
            }
            void writeByte(byte input)
            {
                //開始寫入數據
                for (int i = 0; i < 8; i++)//每次寫入一個byte,一共8bit
                {
                    clkPin.Write(GpioPinValue.Low);//確保無誤輸入前再拉低一次時鍾 ,代表開始寫入數據
                    if ((input & 0x01) == 1)//判斷每一位的高低電平
                    {
                        dataPin.Write(GpioPinValue.High);
                    }
                    else
                    {
                        dataPin.Write(GpioPinValue.Low);
                    }
                    input >>= 1;//每寫入完畢一次,移動一位
                    clkPin.Write(GpioPinValue.High);//每次輸入完一位,就拉高一次時鍾
                }
                //應答信號ACK,這里本來是用來判斷DIO腳是否被自動拉低,代表上面寫入的數據TM1637已經接受到了,
                //但是我們這里閑麻煩,直接將CLK信號低高低的拉,讓芯片直接執行下一步操作
                clkPin.Write(GpioPinValue.Low);//先拉低
                clkPin.Write(GpioPinValue.High);//需要判斷D是否為低電平此期間C一直拉高
                clkPin.Write(GpioPinValue.Low);//應答完畢以后拉低C
            }
            void Show()
            {
                //設置基本參數
                startDisp();//開始寫入指令
                writeByte(0x44);//指定功能參數為固定地址顯示
                stopDisp();//結束寫入指令

                var Date = DateTime.Now.ToString("hhmm").ToCharArray();//獲得當前日期,並表示為小時分鍾
                byte[] Characters = { 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71 };//0~9,A,b,C,d,E,F
                //設置顯示地址以及顯示內容
                startDisp();
                writeByte(0xC0);//第一個字符
                writeByte(Characters[Date[0] - 48]);
                stopDisp();

                startDisp();
                writeByte(0xC1);//第二個字符
                writeByte((byte)(Characters[Date[1] - 48] + 0x80));//第二個字符帶有冒號,因此將第一位空置拉高
                stopDisp();

                startDisp();
                writeByte(0xC2);//第三個字符
                writeByte(Characters[Date[2] - 48]);
                stopDisp();

                startDisp();
                writeByte(0xC3);//第四個字符
                writeByte(Characters[Date[3] - 48]);
                stopDisp();

                //開始寫入亮度
                startDisp();
                writeByte(0x8f);
                stopDisp();
            }
            while (true)
            {
                Show();
            } 
        }

以上是固定字符顯示代碼

對於.net core的項目來說,如果想使用Mono運行,那么命令是

 

 執行mono xxxx.dll的方式,如果是普通的.net4.0框架的程序才是mono xxxx.exe的方式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM