最近閑着無聊,買了個樹莓派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的方式
