計算機的世界是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位寄存器ADCH
與ADCL
,但程序能夠直接使用ADC
,編譯器會處理好一些注意事項);當左對齊時,分辨率取8,轉換結果在ADCH
中。能夠直接把ADC
當作16位寄存器,編譯器會處理好一些注意事項。工具
ADC有4種參考電壓可供選擇,分別是AREF
、AVCC
(5V)、1.1V
和2.56V
,由REFS1:0
選擇。8個單端端口(開發板上引出了4個,端口0
到3
),以及一些差分端口(1x
、10x
、200x
增益)和兩個參考電壓,共32個通道,能夠經過多路複用器鏈接到ADC上進行轉換,由MUX4:0
選擇。注意,ADC只有一個,在同一時刻只能轉換一個通道的電壓。優化
ADCSRA
和ADCSRB
用於控制A/D轉換。ADCSRA
中ADEN
啓用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°。電氣屬性至關於物理實驗中的滑動變阻器,若是把兩個定片接在VCC
和GND
上,動片電壓就能夠指示旋鈕旋轉的角度,而且一般與角度是成正比的。
以前提到過,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); }
normal
和hysteresis
函數二選一,其中後者使用了滯回比較的算法。
在normal
模式下,把電位器調整到中點附近的一個位置,你會發現黃色的TX指示燈發了瘋同樣地閃,串口軟件顯示一長串的「on」和「off」(仔細調,必定會有)——你根本不須要製造任何干擾,僅憑ADC的偏差就可讓程序運行地很是糟糕。若是用滿10位的分辨率,這樣的現象會更加明顯。
而在hysteresis
模式下,這樣的情況不會出現。
光敏電阻是一種特殊的電阻器,在光強的時候電阻小,在光弱的時候電阻大。將一個光敏電阻與一個普通電阻串聯,接在VCC
和GND
之間,測量中間點的電壓,就能知道光的強弱。
固然,已知開發板上與光敏電阻串聯的電阻是10kΩ,根據某一時刻的ADC轉換結果,也能夠計算出此時光敏電阻的阻值。不過不要誤會,是經過電壓而不是阻值來得到光強。
與電位器同樣,若是要檢測光的強與弱兩種狀態,也要用到滯回比較。取兩個閾值爲100
和150
,二者相差較大,這是由於咱們要在光較弱時開燈,這又會加強亮度(有點負反饋的意味),若是相差不夠大,就會陷入循環當中。
這兩個閾值是隨便取的,實際應用應根據具體環境取值。因而容易想到要把這個功能從應用程序中抽離出來成爲一個庫。可是,不一樣於以前經常使用的、返回外設狀態讓客戶來決定操做的函數(儘管仍是能夠這麼寫),這個庫是事件驅動的:客戶註冊事件發生時要執行的動做,把程序流程交給框架來控制。
程序分爲三個文件:event.h
、event.c
和main.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
根據參數執行相應動做:當e
爲true
時,光由弱變強,關燈;反之開燈。在調用ldr_event_init
時,把這個函數的指針做爲參數傳入。隨後,每隔1秒調用一次ldr_event_cycle
。
請先花一點時間,把庫的每一行理解清楚。而後,咱們站在客戶的角度來看,使用這個庫是相對方便的——只需考慮事件,即光的變化,而無需考慮過程,即如何檢測這一變化——事實上客戶根本沒有去檢測,更別說如何了。不過,main
函數必須每隔一段時間調用一次ldr_event_cycle
。在學了定時器中斷之後,main
函數就能夠徹底還給客戶了。
查閱相關資料,瞭解ADC有哪些類型。
改進上一講中的RGBW燈程序,使LED亮度適應環境光強。
結合代碼消化吸取事件驅動的概念。推薦閱讀:Event-driven Programming - TechnologyUK