AVR单片机教程——ADC

发布者:omicron25最新更新时间:2020-09-07 来源: eefocus关键字:AVR  单片机  ADC 手机看文章 扫描二维码
随时随地手机看文章

ADC

计算机的世界是0和1的。单片机可以通过读取0和1来确定按键状态,也可以输出0和1来控制LED。即使是看起来不太0和1的PWM,好像可以输出0到5V之间的电压一样,达到0和1之间的效果,但本质上还是高低电平。


但是,世界上终究还是有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位数据。


分辨率与精度是不同的概念。在这篇入门级教程中,我们只需要知道,A/D转换是会有误差的(数据手册23.7.4一节介绍了可能的误差来源)。即使是相同的电压,两次测量的结果也可能是不同的。


要进行A/D转换,需要提供参考电压和待测电压,转换的结果为待测电压参考电压×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转换触发源。


开发板提供了3.3V电源,可用于给只支持3.3V的设备供电。我们用ADC来测量这个电压,然后在串口上输出。


#include

#include


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("Vn");

    

    while (1)

        ;

}

数据手册28.8节指明,当ADC时钟为200kHz时,ADC绝对精度可以达到1.9LSB(1LSB就是1024中的1)。经计算得,为了使ADC时钟不超过这个速率,分频系数应该取128。


所测电压为voltage=ADC1024×5V,但直接这样计算会涉及到浮点运算,而AVR硬件不支持浮点,所有浮点运算都是软件实现的,速度相当慢,两个float相乘需要1000多个指令周期,除法需要更多,都是应该竭力避免的。尽管最后的电压是一个小数,但可以通过移动小数点把它变成整数。5V参考电压下,精度1.9LSB约为9.28mV,因此右移两位,以10mV为单位计算。先算乘法以避免浮点除法,算式变为voltage=ADC×5001024。


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的大小关系,这种噪声会导致严重后果,如红色波形所示:

在阈值128附近,噪声使转换结果上下浮动,导致判断出的状态迅速跳变。用户只是慢慢地把旋钮转过中间的位置,这显然不是我们想要的结果。


这时候就需要滞回比较器出场了。滞回比较器的核心特性是,使输出在0和1之间改变的输入阈值在两个方向上是不同的:当信号从低到高越过高阈值时,输出变为1;当信号从高到低越过低阈值时,输出变为0;如绿色波形所示(图中是反相的)。于是,当输入达到高阈值时,输出变为1,此时只要噪音没有大到使输入回到低阈值,输出将一直保持为1,滤除了噪声。


我们写一个程序,用LED来指示电位器旋钮位置在中点的哪一侧,并在串口上输出每一次状态改变,方便我们观察。


#include

#include

#include

#include


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 ? "onn" : "offn");

    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

#include


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

void ldr_event_cycle();


#endif

event.c:


#include "event.h"

#include


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

#include

#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函数就可以完全还给客户了。


关键字:AVR  单片机  ADC 引用地址:AVR单片机教程——ADC

上一篇:BMP180测量海拔高度传感器单片机程序
下一篇:ATmega328P定时器详解

推荐阅读最新更新时间:2024-11-03 14:07

凭借十余年开发经验,复旦微强势进入车载MCU市场
汽车芯片是指用于车体汽车电子控制装置和车载汽车电子控制装置的半导体产品。汽车芯片大致可以分为:主控芯片、MCU功能芯片、功率芯片、存储芯片、通信芯片及其他芯片(传感芯片为主)六大类。 根据Strategy Analytics数据,在传统燃油车中,MCU价值占比最高,达到23%;其次为功率半导体,达到21%;传感器排名第三,占比为13%。而在纯电动车型中,功率半导体使用量大幅提升,占比最高,达到55%,其次为MCU,达到11%;传感器占比为7%。 同时,在纯电动车中,半导体芯片的总含量也在显著增加,根据 Strategy Analytics 数据,2019 年纯电动车单车平均半导体价值达到了775美元,为燃油车的两倍有余,而
[汽车电子]
51单片机中断系统实验
一、实验内容 INT0端接单次脉冲发生器。按一次脉冲产生一次中断,CPU使P1.0状态发生一次反转,P1.0接LED灯,以查看信号反转。 根据实验内容编写一个程序,并在实验系统上调试和验证。 二、仿真图 三、代码 C语言实现: sbit LED=P1^0; void INT0_IN(); void main() { LED=0; INT0_IN(); while(1) ; } void INT0_IN() { EA=1;//总中断打开 EX0=1;//外部中断0 打开 IT0=0;//负边沿触发 } void exter0() interrupt 0 { IE0
[单片机]
51<font color='red'>单片机</font>中断系统实验
PIC单片机与51单片机的区别
(1)总线结构:MCS-51单片机的总线结构是冯-诺依曼型,计算机在同一个存储空间取指令和数据,两者不能同时进行;而PIC单片机的总线结构是哈佛结构,指令和数据空间是完全分开的,一个用于指令,一个用于数据,由于可以对程序和数据同时进行访问,所以提高了数据吞吐率。正因为在PIC单片机中采用了哈佛双总线结构,所以与常见的微控制器不同的一点是:程序和数据总线可以采用不同的宽度。数据总线都是8位的,但指令总线位数分别位12、14、16位。 (2)流水线结构:MCS-51单片机的取指和执行采用单指令流水线结构,即取一条指令,执行完后再取下一条指令;而PIC的取指和执行采用双指令流水线结构,当一条指令被执行时,允许下一条指令同时被取出,这样
[单片机]
单片机结构和原理
89C51单片机结构框图 1、一个8位 的微处理器CPU。 2、片内数据存储器(RAM128B/256B):用以存放可以读/写的数据,如运算的中间结果、最终结果以及欲显示的数据等。 3、片内4kB程序存储器Flash ROM(4KB):用以存放程序、一些原始数据和表格。 4、四个8位并行I/O(输入/输出)接口 P0~P3:每个口可以用作输入,也可以用作输出。 5、两个或三个定时/计数器: 每个定时/计数器都可以设置成计数方式,用以 对 外部事件进行计数,也可以设置成定时方式,并可以根据计数或定时的结果 实现计算机控制 6、一个全双工UART的串行I/O口:可实现单片机与单片机或其它微机之间串行通信。 7、片内振荡器和时钟产生
[单片机]
<font color='red'>单片机</font>结构和原理
AVR熔丝位操作时的要点和需要注意的相关事项
对AVR熔丝位的配置是比较细致的工作,用户往往忽视其重要性,或感到不易掌握。 下面给出对AVR熔丝位的配置操作时的一些要点和需要注意的相关事项。 在AVR的器件手册中,对熔丝位使用已编程(Programmed)和未编程(Unprogrammed)定义熔丝位的状态,“Unprogrammed”表示熔丝状态为“1”(禁止)“Programmed”表示熔丝状态为“0”(允许)。因此,配置熔丝位的过程实际上是“配置熔丝位成为未编程状态“1”或成为已编程状态“0””。 在使用通过选择打钩“ ”方式确定熔丝位状态值的编程工具软件时,请首先仔细阅读软件的使用说明,弄清楚“ ”表示设置熔丝位状态为“0”还是为“1”。 使用CVAVR中的编
[单片机]
<font color='red'>AVR</font>熔丝位操作时的要点和需要注意的相关事项
基于单片机控制的家用采暖洗浴器设计
引言 本文所介绍的家用电热水循环采暖洗浴器的一项关键技术是纳米材料远红外薄膜电加热管。用氯化锡、碳酸银、氯化铁、氧化铝、氧化锌、氧化钛、二氧化硅、柠檬酸、乙醇、聚乙二醇、二甲苯、氨以及超细纯硅粉、锡粉等二十余种材料采用化学法配置以二氧化锡为主要含量的纳米凝胶,选用高强度石英玻璃管为底衬,在高温下进行高温喷涂和提拉烘干,在管外壁瞬间形成厚约 6μm的薄膜层,制成纳米二氧化锡电激发远红外薄膜液体加热管。管的直经 20mm、长 15cm、壁厚 2mm,耐高温 860℃,承受水压 0.5MPa,远红外波长 150-250 μm,功率 800W(220V);将多只加热管进行平行式串联组合便制成总功率 0.8kW~12kW的加热
[单片机]
基于C8051F单片机的镍氢电池组管理系统
文章描述了镍氢电池充放电原理和特性的分析,并根据镍氢电池充放电管理需求,提出了一种基于C8051F单片机对多节镍氢电池串联电池组进行综合监测和管理的方案,通过设计:实现了新型电池管理电路,包括完整的硬件和软件解决方案。 随着中国煤炭工业的发展和矿山装备技术的进步,我国对煤矿甲烷安全监控系统,运输监控系统,应急救援系统等使用的后备电源的设备要求越来越高,尤其 是其安全特性。作为煤矿用后备电源的重要的组成之一,镍氢电池无论在安全性上,还是可靠性,成本等方面,都具有较大优势。镍氢电池组是一个串联的组成系 统,其中任何单节电池损坏必将影响整个电池组,如何在保证镍氢电池安全性能的同时,发挥电池自身最大的能量效率,这是矿用镍氢电池管理系统研究和
[单片机]
基于C8051F<font color='red'>单片机</font>的镍氢电池组管理系统
STM32单片机SPI的使用原理解析
1、SPI使用原理 以数据交换实现数据传输,第一个跳变沿实现数据输出,第二个跳变沿实现采样。如下图 2、GPIO的配置 GPIO_InitTypeDef GPIO_InitStructure; //配置SPI2管脚 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_ Pi n = GPIO_Pin_13 |GPIO_Pin_14| GPIO_Pin_15; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStru
[单片机]
STM32<font color='red'>单片机</font>SPI的使用原理解析
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
随便看看

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved