2015年10月31日 星期六

{ 樹莓派 + Arduino } 紫外線強度偵測 ( 使用大型數字顯示 )

網頁最後修改時間:2015/10/31

在這篇網頁中,將說明賣場紫外線( UltraViolet, UV ) 強度偵測模組使用的方法,以及將所取得的類比電壓轉換為數位數值輸出,並經由所求得的多項式方程式算出相對應的類比電壓以及紫外線指數值。最後將相關資料使用大型數字的方式顯示在整合型 LCD 上。

如何在整合型 LCD 顯示大型數字,請參考 "{ 單晶片 + Arduino + 樹莓派 } 整合型 LCD ( @ I2C 模式 ) 的漂亮數字顯示 ( 自訂字型或圖案 ) " 網頁中的說明與範例展示影片。

/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
有購買商品的使用者,網頁中所需相關資料已放置於雲端硬碟,請自行下載使用!
其餘的使用者,請自行依照提供之連結下載相關資料,程式碼複製貼上使用!
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
*********************************************************************************
網頁中使用的零件可至露天賣場訂購:
*********************************************************************************

紫外線簡介:

紫外線位於波長 100 ~ 400 nm ( nanometer ) 的範圍,在此範圍又分為 UV-C、UV-B 和 UV-A 三個區段
Electromagnetic spectrim, source: www.ccohs.ca
其中,
  • UVA ( 波長 315 - 400 nm )
    在紫外線中佔 95 %,可以穿透透明的玻璃和塑膠,可以直達肌膚的真皮層,導致皮膚曬傷、曬黑、老化與產生皺紋。
  • UVB ( 波長 280 - 315 nm )
    到達地球表面的 UVB,在紫外線中只佔 2%,只有在夏天或是午後特別強。部分的 UVB 會被透明玻璃吸收,過量照射會使得皮膚曬黑、曬傷脫皮勝制有致癌的可能。但適當的 UVB 能觸進體內礦物質代謝與形成維生素 D。
  • UVC ( 波長 100 - 280 nm )
    此波長範圍無法穿透透明玻璃與塑膠。短時間曝曬會灼傷皮膚,長時間曝曬會導致皮膚癌,但可作為殺菌燈使用。

數位讀值與類比電壓、紫外線指數間的關係方程式:

網頁所使用的紫外線強度偵測模組,,可實際偵測的紫外線波長範圍位於 240 ~ 370 nm,包含全部 UVB 與部分 UVA 和 UVC 範圍。

當在電源輸入 <VCC> 處輸入電壓 DC +3.3V ~ 5V,在不同的紫外線強度照射下,就能得到相對應的類比電壓  DC +0 ~ 1V+ 的輸出 ( 但取決於紫外線強度,這裡只是限定一個範圍曲對照值,所以當紫外線指數夠強的話,就會大於 1V,而且越強值越大 ),對照下表就能得到相對應的紫外線指數。
紫外線強度偵測模組輸出數據對照表
類比電壓的輸出,可以使用電表量測 <OUT> <GND> 之間的電壓,但這只建議用在驗證撰寫的程式輸出結果用,而不是實際應用上。

實際應用的時候,必須使用類比轉數位晶片 (ADC),將要檢測的類比電壓接到 ADC 輸入接腳上,根據 ADC 的參考電壓 (Vref) 以及解析度 (Resolutio) 做換算 ( 除了 Arduino 有內建 ADC 功能之外,網頁中的 ADC 選用 MCP3008 )。

* 曲線擬合

打開 EXCEL 將上面的數據輸入到表格中,分別框選 "UV Index" 與 "Analog Value" 和 "Vout" 與 "Analog Value",選擇繪圖類型 "XY 散佈圖" 畫出圖形。

在散佈圖中的曲線點擊滑鼠右鍵,在出現的選單選擇 "加上趨勢線"。

在 "趨勢線格式" 的設定畫面中,點選 "多項式",在 "順序" 右邊選擇多項式的階數,不同的階數所擬合出來的曲線直接就會出現在圖形上。雖然兩條曲線越接近越好,但是越高階代表浮點數運算越多,所需要的編譯空間越大,不是每一種微處理器都有那麼大的空間,這是使用上必須要考量的。
趨勢線格式設定畫面
完成趨勢線的設置之後,記得點選 "圖表上顯示公式",就會將這條擬合曲線的方程式顯示在圖形上 (下圖只有顯示擬合曲線沒有顯示方程式,請自己擬合)。利用這兩個方程式,只要得到類比轉數位的讀值之後,就能代入方程式取得紫外線指數和類比電壓值。

紫外線強度偵測模組數據曲線擬合圖

參考電路圖:

下面兩個電路圖分別用在有無 ADC 功能的系統上。由於樹莓派沒有類比轉數位的功能,所以必須外掛 ADC 晶片與電壓準位轉換模組作為通訊用,這樣才能讓兩個系統程式輸出結果是一致的。

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* Arduino:
紫外線強度偵測模組 - Arduino 測試接線圖
/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* Raspberry Pi ( A/B/B+/B2 都適用 ):
紫外線強度偵測模組 - 樹莓派測試接線圖

程式碼與測試結果:

下面的程式碼使用 "{ 單晶片 + Arduino + 樹莓派 } 整合型 LCD ( @ I2C 模式 ) 的漂亮數字顯示 ( 自訂字型或圖案 ) " 網頁中的程式碼進行修改,所以下面只列出增加的部分。

由於程式碼主要著重於 ADC 取值之後的簡單數學運算,其他的都是使用既有的函式庫呼叫使用,因此不對程式碼多做說明。重點應該落在硬體線路的佈線上面,若有相關硬體函式庫的問題,請自行尋找賣場部落格關於該零件的範例網頁,裡面就有詳細介紹。

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* Arduino

樣本程式碼:Numerics.ino

程式開頭的部分新加入一個類比轉數位接腳的定義和一個可以在任意位置顯示字串的函式
#define uvPin   A0
void displayCharOnLCD( int line, int column, const char *dp, unsigned char len );

displayCharOnLCD() 函式程式碼如下,複製貼上即可
void displayCharOnLCD( int line, int column, const char *dp, unsigned char len )
{
    unsigned char i;

    Wire.beginTransmission(IIC_ADDR_LCD1);

    Wire.write( 0x80 );
    Wire.write( 0x80 + ( line - 1 ) * 0x40 + ( column - 1 ) );
    Wire.write( 0x40 );

    for( i = 0; i < len; i++)
    {
        Wire.write( *dp++ );
    }

    Wire.endTransmission();
}

加入下面的程式碼在 setup() 函式最下面
void setup()
{
    //.. 前方程式碼省略, 新增在 setup() 最下面

    pinMode( uvPin, INPUT );
    digitalWrite( led, LOW );

    Serial.print(F( "UV Index Demo\n" ));

    displayCharOnLCD( 1, 1, " UV Index Demo", 15 );
    delay(1000);
    displayCharOnLCD( 2, 1, "   Starting", 11 );
    delay(1000);
    displayCharOnLCD( 2, 12, ".", 1 );
    delay(1000);
    displayCharOnLCD( 2, 13, ".", 1 );
    delay(1000);
    displayCharOnLCD( 2, 14, ".", 1 );

    delay(1000);
}

將原本在 loop() 函式上面的外面的 char nums[8]=""; 移除,複製下面程式碼取代 loop() 函式。
取線擬合的方程式請依照網頁上面的說明自行求出,然後取代下面橘色處的程式碼。EXCEL 擬合出來的多項式方程式,y 代表 uvIndex,x 代表 uvAnalog
void loop()
{
    char nums[8] = "";
    uint16_t uvAnalog = 0;
    float uvIndex = 0.0, uvVout = 0.0;
    uvAnalog = readadc( uvPin );   //connect UV sensors to Analog 0
    if( uvAnalog <= 10 )
    {
        uvAnalog = 0;
    }
    else if( uvAnalog >= 240 )
    {
        uvIndex = 11;
        uvVout = 1170;  // mA
    }
    else
    {
        uvIndex = /*{ A*uvAnalog^2+B*uvAnalog+C }*/;   // 填入曲線擬合的方程式
                                                // UVI vs. Analog Value
        uvVout  = /*{ a*uvAnalog^2+b*uvAnalog+c }*/;   // 填入曲線擬合的方程式
                                                // Vout vs. Analog Value
    }
    Serial.print( "uvAnalog=" );   
    Serial.print( uvAnalog );
    Serial.print( ", uvVout=" );   
    Serial.print( uvVout );
    Serial.print( "mA, uvIndex=" );   
    Serial.println( uvIndex );

    clearLCD();
    // 分別顯示 uvAnalog、uvVout 和 uvIndex 的值在 LCD 上
    // uvAnalog
    snprintf( nums, 8, "%4d", uvAnalog );
    displayNumeric( nums, 4 );
    // 最多顯示最後面三個位元,故LCD 螢幕最前面的上面兩行前面5個字元都可以使用
    //...
    displayCharOnLCD( 1, 1, " UV",  3 );
    displayCharOnLCD( 2, 1, " adc", 4 );
    delay(2000); // 延遲兩秒顯示下一個
    // uvVout, 輸出最多四個位元 (mV)
    // 輸出為 1.2f
    clearLCD();
    snprintf( nums, 8, " %1d.%1d%1d",\ 
            int( uvVout/1000 ), \
            int( ( int(uvVout) % 1000) / 100), \
            int( ( int(uvVout) %  100) / 10) ); // x.xx V            
    displayNumeric( nums, 5 );
    displayCharOnLCD( 1, 1, " UV",  3 );
    displayCharOnLCD( 2, 1, "volt", 4 ); 
    delay(2000);
    // uvIndex
    clearLCD();
    // Arduino IDE 的 snprintf 函數組合浮點數會有問題,
    // 所以使用整數的方式做顯示
    // uvIndex 取到小數第一位        
    snprintf( nums, 8, " %2d.%1d", int(uvIndex), int (uvIndex * 10) % 10 );
    displayNumeric( nums, 5 );
    displayCharOnLCD( 1, 1, " UV",   3 );
    displayCharOnLCD( 2, 1, "index", 5 );
    delay(2000);
}

編譯上傳程式碼之後,在室內開窗戶實測 (2015/10/29, 台南上午 11 點, 陰天不見太陽) 可得到下面類似的結果
Arduino - 紫外線強度偵測模組實測 - 類比轉數位值
Arduino - 紫外線強度偵測模組實測 - 類比電壓值 (計算)
Arduino - 紫外線強度偵測模組實測 - 紫外線指數 (計算)

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* Raspberry Pi ( A/B/B+/B2 都適用 )

樣本程式碼:IIC_Numeric.c

為了使用 Raspberry Pi B2 做測試,使用新燒錄的作業系統 2015-09-24-easpbian-jessie,並做了以下重點設置與套件安裝
  • raspi-config
    • I2C Enable
    • Serial Disable
    • CPU Frew 1000MHz
    • TimeZone HongKong
    • Expand FS
  • WiringPi ( V2.29 )
*********************************************************************************
MCP3008 雖然接腳安裝在 SPI 通訊功能接腳上,但程式使用 Bit-Banging 的方式實作 SPI 通訊,沒有使用硬體 SPI,所以除非要重新改寫程式使用硬體 SPI 做通訊,否則千萬不要打開樹莓派硬體 SPI 的功能。
*********************************************************************************
請增加下面幾個標頭檔到程式開頭的地方
#include <time.h>
#include <wiringPi.h>
#include <math.h>

在標頭頭下方,新增加下面的程式碼
#define CLK     11
#define MISO    9
#define MOSI    10
#define CS      8

#define UVSENSOR_ADC    0
 
int spi_mcp3008_init(int mosipin, int misopin, int cloclpin, int cspin);
int readadc(int adcnumber);

其中,兩個新增函式的程式碼如下;分別是 MCP3008 通訊用的接腳初始化,以及取回類比轉數位指定通道值的函式。
int spi_mcp3008_init(int mosipin, int misopin, int clockpin, int cspin)
{
    // 初始化 wiringPi,並使用 BCM_GPIO 的接腳號碼    
    if (wiringPiSetupGpio() == -1)
        return -1 ;
    
    pinMode(mosipin , OUTPUT);
    pinMode(misopin ,  INPUT);
    pinMode(clockpin, OUTPUT);
    pinMode(cspin   , OUTPUT);
    
    return 0;
}

int readadc(int adcnum)
{
    int i, commandout, adcout;
    
    if((adcnum > 7) || (adcnum < 0))
        return -1;
    
    digitalWrite(CS, HIGH);
    
    digitalWrite(CLK, LOW);     // start clock low
    digitalWrite(CS , LOW);     // bring CS low
    
    commandout = adcnum;
    commandout |= 0x18;         // start bit + single-ended bit
    commandout <<= 3;           // we only need to send 5 bits here
    for(i = 0; i < 5; i++)
    {
        if(commandout & 0x80)
            digitalWrite(MOSI, HIGH);
        else
            digitalWrite(MOSI, LOW);
        commandout <<= 1;
        digitalWrite(CLK, HIGH);
        digitalWrite(CLK,  LOW);
    }
    
    adcout = 0;
    // read in one empty bit, one null bit and 10 ADC bits
    for(i = 0; i < 12; i++)
    {
        digitalWrite(CLK, HIGH);
        digitalWrite(CLK,  LOW);
        adcout <<= 1;
        if(digitalRead(MISO))
            adcout |= 1;
    }
    
    digitalWrite(CS, HIGH);
    
    adcout >>= 1;           // first bit is 'null' so drop it
    return adcout;
}

main() 函式上方新增加入整合型 LCD 的支援函式 displayStringAt() 的宣告
void displayStringAt(int line, int column, const char dp[], int len);

複製再貼上下面 displayStringAt() 函式程式碼
void displayStringAt(int line, int column, const char dp[], int len)
{
    int i;
    for( i = 0; i < len; i++ )
    {
        lcd1.displayCharAt( line, column + i, dp[i]);
    }
}

複製下面程式碼替代原本的 main() 裡面的程式碼
int main(void)
{
    int i;
    const int pointsOfSample = 10;
    unsigned int uvAnalog;
    float uvIndex, uvVout;    
    
    if (spi_mcp3008_init(MOSI, MISO, CLK, CS) == -1 )
        return -1;    
    
    printf( "UV Sensor Demo\n" );
    
    char nums[8]="";
    lcd1.clear();
    delay(100);
    lcd1.writeCGRAM( &CGRAM_block[0], 4, 1 ); 
    lcd1.writeCGRAM( &CGRAM_block[32], 2, 5 );
    displayStringAt( 1, 1, " UV Sensor Demo", 15 );
    delay(1000);
    displayStringAt( 2, 1, "   Starting", 11 );
    delay(1000);
    lcd1.displayCharAt( 2, 12, '.');
    delay(1000);
    lcd1.displayCharAt( 2, 13, '.');
    delay(1000);
    lcd1.displayCharAt( 2, 14, '.');
    
    delay(1000);
    
    while(1)
    {
        uvAnalog = 0;
        uvIndex = 0.0;
        uvVout = 0.0;
        for( i = 1; i <= pointsOfSample; i++ )
        {
            uvAnalog    += readadc( UVSENSOR_ADC );
            delay(2);
        }
        uvAnalog = (( uvAnalog * 0.66) / pointsOfSample ));
        
        if( uvAnalog <= 10 )
        {
            uvIndex = 0;
        }
        else if( uvAnalog >= 240 )
        {
            uvIndex = 11;
            uvVout = 1170;
        }
        else
        {
            uvIndex = /*{ A*uvAnalog^2+B*uvAnalog+C }*/;   // 填入曲線擬合的方程式
                                                           // UVI vs. Analog Value
            uvVout  = /*{ a*uvAnalog^2+b*uvAnalog+c }*/;   // 填入曲線擬合的方程式
                                                           // Vout vs. Analog Value
        }
        
        printf("uvAnalog=%d, uvIndex=%.1f, uvVout=%.2fmV\n", uvAnalog, uvIndex, uvVout);
        
        lcd1.clear();
        // 分別顯示 uvAnalog、uvVout 和 uvIndex 的值在 LCD 上
        // uvAnalog
        snprintf( nums, 8, "%4d", uvAnalog );
        displayNumeric( nums, 4 );
        // 最多顯示最後面三個位元,故LCD 螢幕最前面的上面兩行前面5個字元都可以使用
        //...
        displayStringAt( 1, 1, " UV",  3 );
        displayStringAt( 2, 1, " adc", 4 );
        delay(2000); // 延遲兩秒顯示下一個
        // uvVout, 輸出最多四個位元 (mV)
        // 輸出為 1.2f
        lcd1.clear();
        snprintf( nums, 8, " %1d.%1d%1d", \
                int( uvVout/1000 ), \
                int( ( int(uvVout) % 1000) / 100), \
                int( ( int(uvVout) %  100) / 10) );// x.xx V            
        //snprintf( nums, 8, " %1d.%1d%1d", 1, 2, 3 );
        displayNumeric( nums, 5 );
        displayStringAt( 1, 1, " UV",  3 );
        displayStringAt( 2, 1, "volt", 4 ); 
        delay(2000);
        // uvIndex
        lcd1.clear();
        snprintf( nums, 8, " %2.1f", uvIndex );
        //snprintf( nums, 8, " %2.1f", 10.3 );
        displayNumeric( nums, 5 );
        displayStringAt( 1, 1, " UV",   3 );
        displayStringAt( 2, 1, "index", 5 );
        delay(2000);      
    }    
    return 0;    
}

在樹莓派下輸入以下指令編譯為執行檔

sudo g++ JLX1602A4IIC.o UVIndex.c -lwiringPi -lm -o uvindex

執行指令

sudo ./uvindex

提供適當的紫外線光源,就會得到與下面類似的輸出
樹莓派 - 紫外線強度偵測模組 - 數據輸出
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
紫外線強度偵測模組在測試時,必須要有紫外線的照射才會有讀值。因此在沒有太陽光照射的室內環境下,是不會有 ADC 讀值出現的;此時,若是移動模組到有陽光的透明窗戶邊,才會開始有讀值 ( 看陽光強度,ADC 5 ~ 10 或是以上都有可能 ( 甚至更高,要看季節、陽光強度 ) );要有更高的讀值,最好到室外且有陽光的環境下做測試才容易出現高的讀值( ADC 100 - 200+ ),千萬不要搞錯用其他的光源做測試,因為是得不到讀值的!
有雲的天氣,以及量測時的模組高度... 等因素,都會影響紫外線指數的最終讀值結果,這延伸閱讀的部分可以參考美國國家環境保護局 (www2.eap.gov ) 裡的一篇網頁 "How is the UV Index calculated?" ( www2 epa gov/sunwise/how-uv-index-calculated ) 中的說明 ( 不然零件隨附資料中也有 )。
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*

要確認程式輸出與方程式的正確性,只要實際量測輸出的類比電壓值,然後由整合型 LCD 上或是程式輸出訊息驗證兩者的數值是否一致就可以得知。如下面照片,使用驗鈔筆 ( 我用膠帶將紫外線模組的感測頭直接與紫外線 LED 綁在一起) 做測試,不然數值都會是零。
樹莓派 - 紫外線強度偵測模組 - 紫外線強度數據驗證

結論:

類比轉數位的晶片使用上,參考電壓 (Vref) 的穩定非常重要!

以 10-bit 解析度的 ADC 來說,若參考電壓是 3.3V,則每一個變化代表 3.223 mV (3.3/1024) 的變化。若此時參考電壓壓降為 3.25V,則每一個變化代表 3.174 mV (3.3/1024) 的變化。當 ADC 取值為 100 時,兩者之間的差距就會有 4.883 mV;這是在電壓變化 0.05V 的輸出差距。

解決的方法可以使用穩定的參考電壓晶片;或是額外將參考電壓引入到另一個類比轉數位通道中,採取一定數量之後取平均值,得到平均的參考電壓在與取值之後的數據做運算得到特定通道更準確的類比轉數位值,範例可以參考這一篇網頁中的介紹。

原本使用 MCP3008 在樹莓派是希望使用 DC 3.3V 電源並使用 DC 3.3V 做為參考電壓,但由於取值問題導致計算發生錯誤的情況,又不希望耽擱太多時間下,於是改變使用電源為 DC 5V ( 參考電壓維持使用 DC 3.3V )並增加一個四通道電壓轉換模組,負責將四個與樹莓派的通訊接腳轉換為 3.3V,取值與計算後的數據才正確。

如果能夠提供穩定的 3.3V 或是更低的穩定參考電壓,就可以在不增加電壓準位轉換模組與改變輸入電壓的情況下,維持使用 3.3V 的電源輸入,降低線路佈線的惱人程度!

我沒有在完成這個網頁之後再次做確認上面電壓輸入的問題,但我想有可能是那個時候程式碼有問題導致,但至少現在已解決問題!若有人有任何想法,請告訴我!

表格中對於小於 10 以及大於 1170 的 analog value 都設定為一個固定的值,限定 analog value 在一定的範圍內,如果需要這些範圍外的數值,就取消掉那些判斷的條件式即可;因為範圍就屬於外插了,不建議考慮!

對於手邊有整合型 LCD 來說,這裡的程式碼是混合了大型數字與基本字元顯示的一個例子。喜歡它,就去搞懂它!搞懂了,就是你(妳)的了!


沒有留言:

張貼留言