AVR單片機教程——ADC

ADC

計算機的世界是0和1的。單片機能夠經過讀取0和1來肯定按鍵狀態,也能夠輸出0和1來控制LED。即便是看起來不太0和1的PWM,好像能夠輸出0到5V之間的電壓同樣,達到0和1之間的效果,但本質上仍是高低電平。html

可是,世界上終究仍是有0和1沒法表示的。若是引腳上被施加0到5V之間的電壓,寄存器PINx沒法告訴咱們具體狀況,只能指示這個電壓是1.5V如下仍是3V以上(參考數據手冊「Electrical characteristics」)。這種能夠連續變化的信號稱爲模擬信號,與離散的、只能取0或1(0或5V)的數字信號對立。算法

這並不表明數字世界沒法處理模擬信號,相反,一種至關經常使用的處理模擬信號的方法,就是把模擬信號轉換成數字信號,用處理器來運算,而後再轉換成模擬信號。這個過程當中涉及到模擬-數字轉換和數字-模擬轉換,分別須要ADC和DAC來實現。大多數單片機,做爲現實世界中的工具,須要接觸模擬信號,尤爲是模擬信號的輸入,會集成ADC。編程

ADC的一個參數是分辨率,指它的位數,反映了能夠產生的不一樣輸出的數量(8位ADC能夠產生0~255的值)與量化最小物理量(一般是電壓)的能力(好比當參考電壓爲2.56V時,理想狀況下,8位ADC能夠分辨兩個相差0.01V的電壓的不一樣)。AVR單片機帶有的ADC是10位的。框架

另外一個參數是轉換速率,每秒進行A/D轉換的次數。AVR單片機的ADC爲了達到10位分辨率的精度,最大轉換速率爲15kSPS(千次採樣每秒)。若是能夠接受較低的精度,也能夠以200kSPS採樣,得到8位數據。ide

分辨率與精度是不一樣的概念。在這篇入門級教程中,咱們只須要知道,A/D轉換是會有偏差的(數據手冊23.7.4一節介紹了可能的偏差來源)。即便是相同的電壓,兩次測量的結果也多是不一樣的。函數

要進行A/D轉換,須要提供參考電壓和待測電壓,轉換的結果爲\(\frac {待測電壓} {參考電壓} \times 2^{分辨率}\)。寄存器ADMUX中的ADLAR位控制轉換結果的對齊方式。當右對齊時,公式中分辨率取10,轉換結果在16位寄存器ADC中(其實是兩個8位寄存器ADCHADCL,但程序能夠直接使用ADC,編譯器會處理好一些注意事項);當左對齊時,分辨率取8,轉換結果在ADCH中。能夠直接把ADC當作16位寄存器,編譯器會處理好一些注意事項。工具

ADC有4種參考電壓可供選擇,分別是AREFAVCC(5V)、1.1V2.56V,由REFS1:0選擇。8個單端端口(開發板上引出了4個,端口03),以及一些差分端口(1x10x200x增益)和兩個參考電壓,共32個通道,能夠經過多路複用器鏈接到ADC上進行轉換,由MUX4:0選擇。注意,ADC只有一個,在同一時刻只能轉換一個通道的電壓。優化

ADCSRAADCSRB用於控制A/D轉換。ADCSRAADEN啓用ADC組件,ADSC位啓動一次轉換,到ADIF位爲1時轉換結束,須要寫1才能清零。ADPS2:0選擇ADC時鐘分頻係數,這關係到轉換速率:首次採樣(啓用ADC後第一次或同時)須要25個ADC時鐘週期,隨後每次採樣須要13個。ADCSRB能夠選擇A/D轉換觸發源。ui

開發板提供了3.3V電源,可用於給只支持3.3V的設備供電。咱們用ADC來測量這個電壓,而後在串口上輸出。spa

#include <avr/io.h>
#include <ee1/uart.h>

int main()
{
    uart_init(UART_TX);
    ADMUX  =    0b01 << REFS0  // AVCC as reference
           |     0b0 << ADLAR  // right adjust
           | 0b00000 << MUX0;  // ADC0 single ended
    ADCSRA =       1 << ADEN   // enable ADC
           |       1 << ADSC   // start conversion
           |       1 << ADIF   // clear flag
           |   0b111 << ADPS0; // divide by 128
           
    while (!(ADCSRA & 1 << ADIF)) // wait until flag is set
        ;
    
    uint16_t voltage = (uint32_t)ADC * 500 >> 10; // ADC / 1024 * 500 (* 10mV)
    uint8_t integer = 0;                          // integer part of voltage
    while (integer * 100 <= voltage)              // calculate integer part
        ++integer;
    --integer;
    uint8_t decimal = voltage - integer * 100;    // calculate decimal part
    
    uart_print_int(integer);                      // print the voltage
    uart_print_char('.');
    uart_set_align(UART_ALIGN_RIGHT, 2, '0');
    uart_print_int(decimal);
    uart_print_string("V\n");
    
    while (1)
        ;
}

數據手冊28.8節指明,當ADC時鐘爲200kHz時,ADC絕對精度能夠達到1.9LSB(1LSB就是1024中的1)。經計算得,爲了使ADC時鐘不超過這個速率,分頻係數應該取128。

所測電壓爲\(voltage = \frac {ADC} {1024} \times 5V\),但直接這樣計算會涉及到浮點運算,而AVR硬件不支持浮點,全部浮點運算都是軟件實現的,速度至關慢,兩個float相乘須要1000多個指令週期,除法須要更多,都是應該竭力避免的。儘管最後的電壓是一個小數,但能夠經過移動小數點把它變成整數。5V參考電壓下,精度1.9LSB約爲9.28mV,所以右移兩位,以10mV爲單位計算。先算乘法以免浮點除法,算式變爲\(voltage = \frac {ADC \times 500} {1024}\)

ADC的值直接與500相乘會溢出,所以須要先提高爲uint32_t。固然,你能夠把算式約分一下,但不改變會溢出的事實。儘管32位整數不太好處理,但相比浮點數仍是容易得多。而後是一個除法。16位整數除法須要173個CPU指令週期(參考:Multiply and Divide Routines),是比較耗時的。儘管這個程序中只計算一次,但仍是應該儘可能想辦法避免耗時的操做。注意到除數1024是一個特殊的數,是2的10次方,能夠經過移位運算來作除法,而移位運算相比除法快得多(也許編譯器會把/ 1024優化成>> 10)。

而後咱們須要把這個數的百位部分拿出來做電壓的整數部分,十位和個位做小數部分,能夠經過除以100和模100來實現。因爲這裏的100是一個編譯期常數,編譯期極可能把這個除法和取模優化掉,不調用100多週期的過程。這裏咱們感覺一下手動優化。因爲變量voltage必定小於500,能夠用乘法和比較的循環來試出這個商,其中乘法的執行次數不超過6次——AVR單片機有雙週期乘法指令。而後,用乘法與減法求出餘數。

ADC是單片機編程中相對容易用到浮點與乘除法的場合,設計算法時應儘可能注意避免耗時的運算,或手動編寫優化的算法來代替。

電位器

電位器,開發板右側兩個旋鈕中左邊一個,能夠連續轉動300°。電氣屬性至關於物理實驗中的滑動變阻器,若是把兩個定片接在VCCGND上,動片電壓就能夠指示旋鈕旋轉的角度,而且一般與角度是成正比的。

以前提到過,A/D轉換是有偏差的,即便輸入電壓保持不變,轉換結果也可能上下浮動。若是再加上一些電磁干擾,好比附近有電機,這種噪音會更加明顯。若是一個程序須要檢測電位器旋轉的位置在中點的哪邊,並僅僅是簡單地比較轉換結果與128的大小關係,這種噪聲會致使嚴重後果,如紅色波形所示:

[picture]

在閾值128附近,噪聲使轉換結果上下浮動,致使判斷出的狀態迅速跳變。用戶只是慢慢地把旋鈕轉過中間的位置,這顯然不是咱們想要的結果。

這時候就須要滯回比較器出場了。滯回比較器的核心特性是,使輸出在0和1之間改變的輸入閾值在兩個方向上是不一樣的:當信號從低到高越太高閾值時,輸出變爲1;當信號從高到低越太低閾值時,輸出變爲0;如綠色波形所示(圖中是反相的)。因而,當輸入達到高閾值時,輸出變爲1,此時只要噪音沒有大到使輸入回到低閾值,輸出將一直保持爲1,濾除了噪聲。

咱們寫一個程序,用LED來指示電位器旋鈕位置在中點的哪一側,並在串口上輸出每一次狀態改變,方便咱們觀察。

#include <ee1/pot.h>
#include <ee1/led.h>
#include <ee1/uart.h>
#include <ee1/delay.h>

void init();
void normal();
void hysteresis();

int main()
{
    init();
    while (1)
    {
        normal();
//         hysteresis();
        delay(1);
    }
}

static bool status;

void change(bool _value)
{
    status = _value;
    uart_print_string(_value ? "on\n" : "off\n");
    led_set(LED_BLUE, _value);
}

void init()
{
    pot_init(ADC_0);
    led_init();
    uart_init(UART_TX);
    status = pot_read() >= 128;
}

void normal()
{
    bool now = pot_read() >= 128;
    if (status != now)
        change(now);
}

void hysteresis()
{
    uint8_t pot = pot_read();
    if (status && pot < 124)
        change(0);
    else if (!status && pot >= 132)
        change(1);
}

normalhysteresis函數二選一,其中後者使用了滯回比較的算法。

normal模式下,把電位器調整到中點附近的一個位置,你會發現黃色的TX指示燈發了瘋同樣地閃,串口軟件顯示一長串的「on」和「off」(仔細調,必定會有)——你根本不須要製造任何干擾,僅憑ADC的偏差就可讓程序運行地很是糟糕。若是用滿10位的分辨率,這樣的現象會更加明顯。

而在hysteresis模式下,這樣的情況不會出現。

光敏電阻

光敏電阻是一種特殊的電阻器,在光強的時候電阻小,在光弱的時候電阻大。將一個光敏電阻與一個普通電阻串聯,接在VCCGND之間,測量中間點的電壓,就能知道光的強弱。

固然,已知開發板上與光敏電阻串聯的電阻是10kΩ,根據某一時刻的ADC轉換結果,也能夠計算出此時光敏電阻的阻值。不過不要誤會,是經過電壓而不是阻值來得到光強。

與電位器同樣,若是要檢測光的強與弱兩種狀態,也要用到滯回比較。取兩個閾值爲100150,二者相差較大,這是由於咱們要在光較弱時開燈,這又會加強亮度(有點負反饋的意味),若是相差不夠大,就會陷入循環當中。

這兩個閾值是隨便取的,實際應用應根據具體環境取值。因而容易想到要把這個功能從應用程序中抽離出來成爲一個庫。可是,不一樣於以前經常使用的、返回外設狀態讓客戶來決定操做的函數(儘管仍是能夠這麼寫),這個庫是事件驅動的:客戶註冊事件發生時要執行的動做,把程序流程交給框架來控制。

程序分爲三個文件:event.hevent.cmain.c,前兩個能夠獨立成庫,供之後使用,爲了方便,和可執行程序放在一塊兒了。

event.h

#ifndef EVENT_H
#define EVENT_H

#include <stdint.h>
#include <stdbool.h>

void ldr_event_init(uint8_t _thl, uint8_t _thh, void (*_func)(bool));
void ldr_event_cycle();

#endif

event.c

#include "event.h"
#include <ee1/ldr.h>

static void (*handler)(bool);
static uint8_t low, high;
static bool status;

void ldr_event_init(uint8_t _thl, uint8_t _thh, void (*_func)(bool))
{
    ldr_init(ADC_1);
    low = _thl;
    high = _thh;
    handler = _func;
    uint8_t ldr = ldr_read();
    if (ldr <= low)
        handler(status = 0);
    else
        handler(status = 1);
}

void ldr_event_cycle()
{
    uint8_t ldr = ldr_read();
    if (status && ldr <= low)
        handler(status = 0);
    else if (!status && ldr >= high)
        handler(status = 1);
}

main.c

#include <ee1/led.h>
#include <ee1/delay.h>
#include "event.h"

void handler(bool e)
{
    if (e)
        led_off();
    else
        led_on();
}

int main()
{
    led_init();
    ldr_event_init(100, 150, handler);
    while (1)
    {
        ldr_event_cycle();
        delay(1000);
    }
}

客戶先編寫事件處理函數handler,參數爲一個bool,返回void,這是ldr_event_init所規定的。handler根據參數執行相應動做:當etrue時,光由弱變強,關燈;反之開燈。在調用ldr_event_init時,把這個函數的指針做爲參數傳入。隨後,每隔1秒調用一次ldr_event_cycle

請先花一點時間,把庫的每一行理解清楚。而後,咱們站在客戶的角度來看,使用這個庫是相對方便的——只需考慮事件,即光的變化,而無需考慮過程,即如何檢測這一變化——事實上客戶根本沒有去檢測,更別說如何了。不過,main函數必須每隔一段時間調用一次ldr_event_cycle。在學了定時器中斷之後,main函數就能夠徹底還給客戶了。

做業

  1. 查閱相關資料,瞭解ADC有哪些類型。

  2. 改進上一講中的RGBW燈程序,使LED亮度適應環境光強。

  3. 結合代碼消化吸取事件驅動的概念。推薦閱讀:Event-driven Programming - TechnologyUK

相關文章
相關標籤/搜索