【.NET 與樹莓派】數模轉換

在開始之前,需要說明一對很耳熟的概念——數字訊號 & 模擬訊號。

這些概念的理論有些複雜,你如果相當有興趣,可以找來有關的文獻細細研究;若你不關心那是啥只想知道咋用,那就通俗但不庸俗地理解一下。

數字訊號:訊號值離散,不連續。顯著特點它有兩個值:0 和 1。CPU 最喜歡這兩個值。對應到 GPIO 口上就是前面咱們說過的低電平和高電平;

模擬訊號:訊號內容是連續的,量化的。根據有效範圍和參考對象確定的數值。這些值在某一段時間內會不斷變化的。用電腦接一個大炮播放音樂,輸出的就是模擬訊號。咱們使用的這些電子模組,通過參考電壓,將客觀的物理量轉化為電壓。這個電壓值最後可以量化為一個數值。

比如,我在門上裝一個感測器,這個感測器可以感知振動的大小產生不同的值,並規定最小的振幅輸出 0V 電壓,振幅 XXX (最大值)輸出 5V 電壓。這樣一來,你就知道每天從你家門前經過的人裡面有多少人會踢你的門,踢的力度有多大。

像我們常見的旋扭,音響上面調音量的那個,就可以你旋轉不同的角度,輸出不同的電壓值,然後你可以規定一個範圍:0 – 1023,即10位精度(二進位位),0表示音量最小,1023表示音量最大。如果旋扭旋到中間的某個地方,可以根據模擬量到計算音量的百分比。我要是旋到 511 左右,那就把音量控制為 50%。

現在,你可以猜猜,樹莓派的 GPIO 引腳通訊時用的是上述的哪一種訊號?複習一下,高考必考的喲,咱們在操作引腳時,是不是經常控制它輸出高電平、低電平;或者讀入高電平、低電平?嗯,這就是了,它就認識兩個值——0、1。看來,樹莓派的通訊引腳處理的是數字訊號。

很遺憾的是,樹莓派沒有硬體層集成的模擬訊號引腳。像 Arduino 板子上的 A0 – A5,就是模擬引腳,可以讀出輸入的模擬訊號。咱們的大草莓並沒有這類引腳。當然你會想到,我能不能用 Arduino 來讀模擬訊號,再傳給樹莓派就行了。思想很正確,肯定可以這樣做的。傳輸方式你可以選用最簡單的串口通訊;也可以用 I2C、SPI 等協議來傳輸;或者用帶Wi-fi的 Arduino 板子(或加裝無線模組)用無線網來傳,協議可以自由選——TCP、UDP,就是Socket編程唄……總之,只要你會玩,咋玩都行。

不過,話又說回來,如果你手頭上沒有 Arduino 板子,就為了讀個模擬量去買塊開發板,這也不划算。所以呢,最「秀」的解決方案是買個數模轉換模組。

樹莓派官方系統有原生驅動,支援一些數模轉換模組(這種情況下也可以買個帶數模轉換的擴展板,價格比較嚇人),比如 ADS1115,這模組不錯,老周也想買一塊,但在某寶轉了一圈,既找到好用的現成模組,單賣晶片的倒是很多,但不方便使用。如果咱們只是學習或實驗,或者做的東東非工業級的,或者對精度要求不高的話,找個 8 位的數模轉換模組就夠了。

某寶上既便宜又容易找到的就是這種。

 

 不同廠家生產的模組,外形有點不同,晶片是 PCF8591。還是老規矩,哪個便宜買哪個。

這個模組是 8 位的,也就是說,你讀到的數值範圍在 0 到 255,正好是一個位元組。說實,老周覺得這精度算是可以的,想想咱們平時調整音量也就是 0% – 100%,只有 100 個值,而用這個模組能得到 256 個值,非特殊需求夠用了。

這個模組有兩邊引腳,看個正面的圖。

 

 焊接的引腳是彎的,會擋住PCB板上印的絲印文字,但沒啥關係。

先看左邊,第一個是 AOUT 這個是輸出的,根據模擬量輸出不同的電壓。我們重點關注後面四個,這四個都是模擬輸入。也就是說,PCF8591模組有四個通道可以用,可以讀到來自四個設備的訊號(前提是這四路是相互獨立的,而不是差分值)。

右邊一排就是標準的 I2C 引腳,SCL接時鐘線,SDA是數據線,GND接負,VCC接 5V。

有意思的是,這個模組它集成了三個感測器:熱敏電阻、光敏電阻和電位器。熱敏電阻感知外部溫度輸出模擬量,光敏電阻感知外部光照輸出模擬量,而電位器則是通過螺絲刀旋轉來得到不同的電阻值,並轉化為模擬量。

這三個感測器默認是與AIN0 – AIN4相連的(只用到三個通道,有一個是空的,這個你得問賣家,到底哪個是空的)。也就是說,你在不連接其他器件時,PCF8591也能讀到數值,數值來源於上述幾個集成的感測器。

但在本文中,老周是想讓大草莓讀取壓電陶瓷輸出的模擬訊號,並且壓電陶瓷模組是接在 AIN0 引腳上。那麼,這就要解決一個問題:如何讓PCF8591模組不讀取集成的那三個感測器的訊號而是讀我們自己連接的器件訊號?請注意看,這個模組上有三個短路帽,下圖中綠色標註的地方。

 

怎麼做呢,很簡單,直接拔掉,不用工具的,徒手就能拔,就像拔牙一樣拔。 這三個傢伙就是和集成的三個感測器連接的,拔掉短路帽,就不通了。這樣就能讀你自己連接的器件了。

====================================================

下面說說要編程時怎麼用 PCF8591 模組。

1、從機地址:0x48。

2、協議很好辦,就發送一個位元組就行了,稱為控制位元組(Control Byte)。這個位元組的每個二進位位可以進行參數設置。

b7  b6  b5  b4  b3  b2  b1  b0

b7 位固定為 0,不用管,就給它 0 就行。

b6 指定 AOUT 引腳是否啟用,即是否啟用模擬輸出。0 不啟用;1 啟用。我們這裡只是用來讀數據,不啟用,給它0即可。

b5 和 b4 用來選擇四個通道的編製方式。

  00:每個通道獨立。其實本文的示例就用這種方式,一通道接一設備就行。

  01:三路差值,兩個引腳的值相減得到的結果存入一個通道。這種情況下可能產生負值,所以 8 位的範圍是 -128 到 127,這種情況要用sbyte 類型來表示。

    channel 0 = AIN0 – AIN1

              channel 1 = AIN1 – AIN2

               channel 2 = AIN2 – AIN3

       故差分方式僅存儲三個通道的數值。

       10:混合方式。AIN0 引腳 讀到的值存入通道0,AIN1 引腳處的數據存到通道1;最後 AIN2 – AIN3 的差分結果存到通道2。只用到三個通道。

        11:兩路差分。

                channel 0 = AIN0 – AIN1

                channel 1 = AIN2 – AIN3

b3 位為固定值——0。

b2 位配置通道地址是否自增。即讀取完 channel0,指針會自動跳到 channel 1。為了讀取更靈活,咱們不啟用,設置為 0。

b1 和 b0 位,指定要訪問的通道。

          00     -> channel 0

          01     -> channel 1

         10      -> channel 2

          11      -> channel 3

所以,根據本文示例的情形,b6 為 0,b5、b4 為 0,b2 為 0,最後得到各個通道的地址為:

        const byte CN1 = 0x00;
        const byte CN2 = 0x01;
        const byte CN3 = 0x02;
        const byte CN4 = 0x03;

示例用到壓電陶瓷模組。

 

 注意,陶瓷片和PCB板買回來一般是沒接線的,咱們自己接也很好辦,用螺絲刀把接線柱的螺絲擰松,然後把線捅進去,再擰緊螺絲即可。注意這玩意兒是分正負的,紅色的線接「INPUT」,黑色的線接「GND」。接好之後就是這樣。

 

 

壓電陶瓷模組有三個引腳,- 接GND,+ 接5V,S 輸出訊號,接 PCF8591 的 AIN0 引腳。

接線的時候,最好把樹莓派的 5V 和 GND 引出到麵包板。麵包板一般有兩排供電專用的孔。

 

 畢竟大草莓上只有兩個 5V 引腳,如果你還要接個散熱風扇,就不怎麼夠用了,所以引出來可以接很多器件;另一方面,把 5V 和 GND 引出來,可以方便讓 PCF8591 模組和壓電陶瓷模組共地,這樣它們對於相對 0V 有個參考值,也能使模擬訊號更穩定。

 

 

=========================================================

最後一步,就是寫程式了。

先聲明一些必備常量。

        // 從機地址
        const int ADDR = 0x48;

        // 以下是讀各個轉換通道的控制位元組
        /*
        MSB                  LSB
         0  X  X  X  0  X  X  X
            |  |  |     |  |__|
            |  |  |     |   這兩位用來選擇通道
            |  |  |     |       00 - AIN0       01 - AIN1
            |  |  |     |       10 - AIN2       11 - AIN3
            |  |  |     |--該位指定通道地址是否自動增長,1啟用,0不啟用
            |  |__|
            |  這兩位設置通道對模擬訊號的編製方式
            |       00 - 每個通道獨立,一一對應著,即單端訊號
            |           AIN0 -> channel 0
            |           AIN1 -> channel 1
            |           AIN2 -> channel 2
            |           AIN3 -> channel 3
            |       01 - 三路差分訊號
            |           AIN0 - AIN1 -> channel 0
            |           AIN1 - AIN2 -> channel 1
            |           AIN2 - AIN3 -> channel 2
            |           四引腳共地
            |       10 - 單端訊號 + 差分訊號
            |           AIN0 -> channel 0
            |           AIN1 -> channel 1
            |           AIN2 - AIN3 -> channel 2
            |           前兩個是單端訊號
            |       11 - 兩路差分訊號
            |           AIN0 - AIN1 -> channel 0
            |           AIN2 - AIN3 -> channel 1
            |           AIN1,AIN3 不共地
            |--是否讓模組開啟模擬訊號輸出,即數字訊號轉模擬訊號(AOUT引腳)
        */
        const byte CN1 = 0x00;
        const byte CN2 = 0x01;
        const byte CN3 = 0x02;
        const byte CN4 = 0x03;

接著,連接 I2C 設備。

            I2cConnectionSettings cst = new(1, ADDR);
            I2cDevice device = I2cDevice.Create(cst);

這裡壓電陶瓷模組只用了 PCF8591的 AIN0 引腳,所以只用到通道0。

    device.WriteByte(CN1);

從始至終我們只用到一個通道,所以控制位元組我們發送一次就可以了,除非你要重新選擇其他通道,或修改參數。

然後 PCF8591 模組會不斷把讀到的值存到通道的暫存器中,咱們只需要不斷的 read 就行了。

            while (looping)
            {
                readval = device.ReadByte();
                // 計算要輸出的字元數
                int outputs = readval * 100 / 255 * 80 / 100;
                // 0 不輸出
                if (outputs == 0)
                {
                    continue;
                }
                for (int x = 0; x < outputs; x++)
                {
                    Write("");
                }
                ……
            }

轉換精度是 8 位,正好讀一個位元組就夠。

上面程式碼的意思:根據讀到的模擬量,算出百分比,把值乘以 100 再除以 255,防止整數運算後舍入為0。比如,要是計算結果為 0.12,就會變成0了。80表示若值是 255 時(100%)在控制台總共輸出 80 塊黑色磚頭(▮),如果是 50% 就輸出 40 塊磚頭。由於前面乘以 100 放大了數值,所以後面要除以100來「中和」一下。當然,這樣算有點亂,你覺得這樣不好,可以轉成浮點數來算,再轉回整數就行了。

這個示例的原理,就是壓電陶瓷在受到不同的外力時電壓會改變,因此輸出不同的模擬訊號,當咱們用東西敲打陶瓷片(圓圓的像塊餅乾的那個),力度不同就會看到螢幕上輸出不數量的磚頭。

一場浩大的打擊樂器表演即將開始。

是不是很有魔性?

最後,把完整程式碼貼一下,老周就不上傳 .zip 了,擔心以後部落格空間不夠用。

using System;
using static System.Console;
using static System.Threading.Thread;
using System.Device.I2c;

namespace dacapp
{
    class Program
    {
        // 從機地址
        const int ADDR = 0x48;

        // 以下是讀各個轉換通道的控制位元組
        /*
        MSB                  LSB
         0  X  X  X  0  X  X  X
            |  |  |     |  |__|
            |  |  |     |   這兩位用來選擇通道
            |  |  |     |       00 - AIN0       01 - AIN1
            |  |  |     |       10 - AIN2       11 - AIN3
            |  |  |     |--該位指定通道地址是否自動增長,1啟用,0不啟用
            |  |__|
            |  這兩位設置通道對模擬訊號的編製方式
            |       00 - 每個通道獨立,一一對應著,即單端訊號
            |           AIN0 -> channel 0
            |           AIN1 -> channel 1
            |           AIN2 -> channel 2
            |           AIN3 -> channel 3
            |       01 - 三路差分訊號
            |           AIN0 - AIN1 -> channel 0
            |           AIN1 - AIN2 -> channel 1
            |           AIN2 - AIN3 -> channel 2
            |           四引腳共地
            |       10 - 單端訊號 + 差分訊號
            |           AIN0 -> channel 0
            |           AIN1 -> channel 1
            |           AIN2 - AIN3 -> channel 2
            |           前兩個是單端訊號
            |       11 - 兩路差分訊號
            |           AIN0 - AIN1 -> channel 0
            |           AIN2 - AIN3 -> channel 1
            |           AIN1,AIN3 不共地
            |--是否讓模組開啟模擬訊號輸出,即數字訊號轉模擬訊號(AOUT引腳)
        */
        const byte CN1 = 0x00;
        const byte CN2 = 0x01;
        const byte CN3 = 0x02;
        const byte CN4 = 0x03;

        static void Main(string[] args)
        {
            // 初始化i2c連接
            I2cConnectionSettings cst = new(1, ADDR);
            I2cDevice device = I2cDevice.Create(cst);
            // 變數標誌程式是否應繼續循環
            bool looping = true;
            // 當收到 Ctrl + C 時釋放資源
            CancelKeyPress += (_, _) => looping = false;

            // 選擇讀通道1的數據
            device.WriteByte(CN1);
            byte readval = default;   //讀到的數據
            while (looping)
            {
                readval = device.ReadByte();
                // 計算要輸出的字元數
                int outputs = readval * 100 / 255 * 80 / 100;
                // 0 不輸出
                if (outputs == 0)
                {
                    continue;
                }
                for (int x = 0; x < outputs; x++)
                {
                    Write("");
                }
                Write('\n');
                Sleep(500);
            }
        }
    }
}

====================================================================

【題外話】

有同學可能會有疑問:樹莓派我買 4B 還是 400 ?這個嘛,你得知道,400 是把主板裝在一個鍵盤裡面的,40 pin 針腳沒少,但少了個 USB 介面。體積肯定較大,如果你還是喜歡手巴掌以內的尺寸,還是買 4B 吧,弄個外殼攜帶也方便,儘管CPU不能和英特爾干,但出差時候作為一體機耍耍還不錯,它有 GPIO 針腳,必要時還可以幫客戶調一調硬體。

  • 要是你不帶出去,把它做成監控主機(買個攝影機模組就完事,至於多少錢的就看你的銀行卡容量了)也挺爽;
  • 刷個 KODI 當電視盒子;
  • 加個手把開《極品破車》(這遊戲不知道有沒Linux版);
  • 發揮藍牙作用 + SSD可以做個床頭小唱機、隨身聽啥的;+ 光碟機可以弄成影碟機;
  • 做個無線投影儀也可以;
  • 買些可編程的燈帶,把你家變成K廳也行;
  • 弄個刷卡門鎖也行(停電了就麻煩);
  • 可以用來做直播(直播赤腳踩刀鋒);
  • 不嫌主板太大的話,搞個紅外人體識別模組做成感應樓梯燈(住小區的話就別弄了,鄰居一伸手就把你的草莓吃掉);
  • 老周買了個人體稱重模組,自己弄了個體重測量儀,沒事可以看看現在自己有多胖(國葯的秤只能給小學生用);
  • 買個 PH 玻璃探頭,可以檢測自來水酸鹼性(就是用到本文的數模轉換,TDS檢測頭原理一樣),這個挺貴的;
  • 把你吃灰的硬碟全拿出來,建個家庭網盤。嗯,常說的NAS。不過,無線路由的WIFI訊號好像不太給力。
  • 超聲波洗魚缸。聲波發生器不好買,老周是借了一個 9V 的模組(人家工廠裡面用的)玩了兩下,沒做成,根本洗不幹凈。電壓 9 V,要獨立供電。也不知道他們廠用這些……估計是驅蚊用的。
  • 紫外線強度實時監測。模組某寶上有,但是把大草莓掛在外面曬,有點怕。

……

反正,只要你敢想,並付諸實踐,什麼 DIY 方式都能實現。