计算机的世界是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