资讯详情

资讯详情

AVR单片机AD转换实验:寄存器配置、代码编写与传感器扩展

在单片机项目里AD转换模数转换是我用得非常多的一项功能。不管是做电池电压监测、可调旋钮输入还是接热敏电阻测温度、光敏电阻判断环境亮度第一步几乎都是先把模拟电压读进单片机变成数字量。很多教材把这个实验安排得很靠前但它背后的东西其实一点不少——参考电压怎么选、转换时钟怎么分频、结果为什么分两个字节存放每个细节都影响最终读数稳不稳定。这篇博文是我整理的一次完整AD转换应用实验从面包板上的电路搭建讲起到寄存器配置、查询和中断两种代码写法再到串口和屏幕上的结果呈现最后补上精度处理和传感器扩展适合刚学完GPIO和定时器、准备啃ADC的读者也适合做项目时总被读数不准困扰的同学照着查漏补缺。下文以最常见的AVR系列8位单片机为例寄存器和流程在同系列芯片上基本通用。1. 实验电路的搭建电位器、参考电压与去耦电容1.1 为什么用电位器作输入源AD转换实验的输入源我建议先用电位器而不是直接上传感器。原因很简单电位器是纯阻性分压输出电压完全确定拧到哪里就是哪里没有传感器那种滞回、非线性、温度漂移的额外变量这能让你把注意力集中在ADC模块本身上。接线也很简单10k电位器第一脚接VCC第三脚接GND中间抽头接单片机的ADC0引脚。我用的是常见的B10K直插电位器面包板就能搭。单片机工作电压用5V用3.3V也行参考电压跟着变就行开发板供电通过USB转串口模块给5V注意模块的5V输出能力不要太差否则后面ADC读数会随负载一起波动。中间抽头那根线尽量短不要和电源线绞在一起。接好之后先拿数字万用表量抽头对地电压应该能在0V到5V之间连续变化。这一步非常关键——很多时候程序写对了读数却不对回头一查居然是电位器没接好或者单片机根本没供电。我习惯先拿万用表把输入电压确认了再写程序这样后续才好逐一对照排查。1.2 参考电压选哪种AVCC、内部基准还是外部基准参考电压直接决定ADC量程输入模拟电压达到参考电压时ADC输出最大值。AVR系列单片机的ADC参考源有三种配置方式在寄存器里但首先得明白它们的区别。AVCC用AVCC引脚上的电压做参考。开发板VCC是5V就选它实测最直观满量程对应5V一个码值大约4.88mV。内部基准部分型号是1.1V部分型号是2.56V不随VCC波动适合电池供电的系统。比如用锂电池供电电压从满电一路下降拿AVCC做参考的话读数自己就会飘这时候内部基准更稳。代价是内部基准精度一般量程也小。外部AREF电压在AREF引脚外接高精度基准源比如常见的431基准芯片稳定性最好。实验里用不上但做带计量要求的项目时几乎是必需品。我在这个实验里选AVCC因为5V供电挺干净输入信号又只是电位器分压不需要高精度。各种参考源在ADMUX寄存器里的配置我列成了表格参考源REFS1:REFS0适用场景AREF引脚外部电压00外接基准源追求高精度AVCC01电源稳定时的常规测量内部基准11电池供电、VCC波动大的场合具体内部基准电压是多少伏不同芯片略有差异动手前先查数据手册确认一下。1.3 去耦电容和模拟地处理模拟电路最容易被忽视的就是噪声但AD转换恰恰最怕噪声。读的是电压一旦电源有纹波参考电压和输入源都在抖读出来的数自然就不稳。我的建议是单片机VCC和GND之间并一个10uF电解电容再加一个100nF陶瓷电容离芯片电源脚越近越好电位器的VCC端和GND端之间也可以并一个100nFADC0引脚如果离电位器比较远可以在引脚附近对地加一个100nF到1uF的小电容滤掉高频噪声。注意别加太大否则输入源驱动能力不够时读数响应会变迟钝。模拟地和数字地的问题在面包板上一般做到所有地线汇到同一个公共点就够了。实在讲究的话可以把模拟部分电位器、参考电容的地和数字部分LED、数码管、按键的地分两条走线在单片机的GND引脚处单点汇合防止数字噪声通过地线串进模拟回路。这个原则现在用不上但做PCB时你一定会感谢自己提前知道。2. 寄存器视角看AVR的ADCADMUX和ADCSRA逐个拆2.1 ADMUX决定参考、对齐和通道AVR的ADC配置核心就两个寄存器ADMUX和ADCSRA。先把ADMUX讲透它控制三件事参考电压、对齐方式、输入通道。参考电压上面已经提过由REFS1:REFS0两位决定按照表里配置即可。对齐方式看ADLAR位它决定10位结果在ADCL和ADCH两个寄存器里怎么放。ADLAR0是右对齐低8位在ADCL高2位在ADCH的低2位ADLAR1是左对齐高8位在ADCH低2位在ADCL的高2位。左对齐最大的好处是你可以直接读ADCH拿到高8位做8位精度测量时非常方便。通道选择由MUX4:0位决定ADC0对应00000ADC1对应00001以此类推。每个ADC引脚在配置为模拟输入之前要先把对应的DDR位清零设为输入状态这一点和普通GPIO输入是完全一致的。2.2 ADCSRA使能、时钟分频、转换控制ADCSRA这个寄存器每一位都有戏。ADENADC使能位为0时整个ADC模块关闭省电。ADSC启动转换位写1启动一次转换单次转换结束后硬件自动清零所以可以用查询这个位来判断转换是否完成。ADATE自动触发使能配合ADCSRB选择触发源可以实现自由运行、定时器溢出、比较匹配等多种触发方式。ADIF转换完成中断标志位。ADIE中断使能位。ADPS2:0预分频选择决定ADC模块的工作时钟。ADC模块有自己的内部时钟手册推荐范围一般是50kHz到200kHz太高会牺牲精度。分频值要从系统时钟算出来假设开发板是16MHz晶振选择128分频得到125kHz正好落在区间正中。如果系统时钟是8MHz选64分频也得到125kHz。常用搭配我整理成了表系统时钟推荐分频ADC时钟16MHz128125kHz8MHz64125kHz4MHz32125kHz1MHz8125kHz规律就是分频后尽量落在125kHz附近。低于50kHz只是转换略慢不影响正确性超过200kHz时结果可能不稳定不建议。2.3 结果的字节序陷阱先读ADCL再读ADCH这是AVR的ADC一个容易踩的坑。10位转换结果同时存在两个寄存器里如果主程序边转换边读两个字节可能来自不同的转换时刻数据就错了。为此芯片设计了一个锁存机制读ADCL时ADCH的值会被锁存到内部缓冲里再读ADCH拿到的就是和ADCL配对的那一个结果。反过来如果先读ADCH两个值就不保证是同一拍的。标准姿势永远是这样uint16_t res ADCL; res | (uint16_t)ADCH 8;某些编译环境把ADC直接映射成16位寄存器一条赋值语句就帮你处理了锁存顺序但手工拆寄存器时这个规矩逃不掉。别小看这个顺序我见过有人读出的数偶尔跳变一大块最终定位下来就是字节序没按规矩来改了一行代码就稳了。3. 两种转换流程的完整代码查询方式与中断方式3.1 查询方式最简单可靠的读数套路如果只是做电压监测查询方式最直接void adc_init(void) { ADMUX (1 REFS0); // AVCC参考右对齐默认通道ADC0 ADCSRA (1 ADEN) | (1 ADPS2) | (1 ADPS1) | (1 ADPS0); // 16MHz / 128 125kHz } uint16_t read_adc(uint8_t ch) { ch 0x07; // 只取低3位通道号 ADMUX (ADMUX 0xF8) | ch; // 清低3位再写入新通道 ADCSRA | (1 ADSC); // 启动转换 while (!(ADCSRA (1 ADIF))); // 等待转换完成 ADCSRA | (1 ADIF); // 写1清除标志 uint16_t result ADCL; result | (uint16_t)ADCH 8; return result; }主循环里延时100ms左右读一次ADC0把结果拆成高低字节发送到串口。这个方式优点是简单缺点是转换期间CPU一直在忙等如果嵌在实时控制循环里会卡节奏。不过慢速测量场景完全够用逻辑也容易调。这里必须提醒一句关于ADIF的清除读了这个标志位之后要写1才能清零写0是无效的。我第一次写这段代码时用了ADCSRA ~(1ADIF)结果标志永远清不掉下一次启动转换后while判断立刻误认为转换完成读到的全是旧数据。这个点数据手册里写得很清楚但实际踩的人特别多。3.2 中断方式适合后台轮询的写法如果以后要做多通道数据采集每次都查询等待就太奢侈了用中断才是正道volatile uint16_t adc_value; volatile uint8_t adc_done; void adc_init_isr(void) { ADMUX (1 REFS0); ADCSRA (1 ADEN) | (1 ADIE) | (1 ADPS2) | (1 ADPS1) | (1 ADPS0); ADCSRA | (1 ADSC); // 启动第一次转换 } ISR(ADC_vect) { adc_value ADC; // 编译器组合读取ADCL和ADCH adc_done 1; }中断方式转换完自动跳进ISR主循环完全不用等每来一次中断数据就刷新一次。切换通道时在ISR里改ADMUX的MUX位然后重新置ADSC就可以了。需要注意中断向量名在不同编译器里写法可能略有差异编译报错时去工具链头文件里查一下正确的向量名以编译器提供的为准。3.3 连续转换模式与触发源想更省事还可以开自动触发连续转换ADCSRA | (1 ADATE)然后置一次ADSC之后ADC每隔一段时间自动再来一次转换每次都触发中断。触发源由ADCSRB寄存器决定默认的自由运行模式就是转换一完成立刻开始下一次。用这种方式adc_value基本是实时刷新的特别适合配合PWM调速这类需要连续采样的场合。要注意的是连续模式下转换占用的时间片更多而且如果不小心改了MUX通道下一次转换立即在新通道上跑程序里要做好通道切换的逻辑同步。我一般只在需要高频采样时才开连续模式慢速测量还是单次转换更可控。4. 数据呈现与电压换算串口、数码管与OLED三种实测4.1 电压换算公式为什么除以1024而不是1023ADC读出来的是0到1023的码值要换算成电压公式是Vin code x Vref / 1024注意除的是1024而不是1023。原因在于ADC内部量化是把0到Vref分成1024个区间每个区间的宽度是Vref/10240V对应码值0Vref附近严格说略低于Vref对应1023。虽然1023在部分教材里也出现过但按数据手册的量化模型用1024更规范理论误差也就0.1%的量级不过严谨一点没坏处。5V参考下每个LSB大约是4.88mV。推荐用整数运算避免浮点数开销uint32_t mv (uint32_t)code * 5000UL / 1024UL;然后分别发送整数和小数部分。浮点打印在8位机上很费资源avr的标准printf库默认还不支持%f需要额外链接浮点支持库内存占用明显变大。所以我更推荐上面的整数拆分方式够用还省内存。4.2 串口观察最直观的调试手段串口是确认ADC数据正确性的第一手段。写一个最简单的发送函数查询发送寄存器空标志然后把码值和电压值一起发到串口助手。旋转电位器码值从0平滑涨到1023左右电压从0走到5V基本就能确认电路和代码都没问题。实测中码值末位跳几个LSB是非常正常的硬件不可能完全静止电源本身就有微小波动。只要不出现大范围乱跳就是健康状态。想看得更直观可以用串口绘图工具把电压画成波形曲线慢慢拧电位器观察曲线的跟随效果排查间歇性接触不良时特别好用。我调试ADC电路的第一道工序永远是接串口看数据而不是直接接屏幕。4.3 数码管与OLED显示做实验总得有个可视化出口。三种方案我都在实验里试过整理一下区别数码管动态扫描显示占用的IO比较多但优点是便宜、阳光下可见适合设备状态显示。OLED屏幕I2C接口接线少显示内容极为丰富电压值、曲线、百分比都能放上去。实验中可以使用常见的点阵驱动方案帧率不用强求刷新几赫兹就够。LCD16025V逻辑电平兼容性好但并行接线多还要调对比度电位器不如前两者省心。我的建议是先用串口把数据确认对了再去折腾屏幕。很多人一上来就接OLED显示乱码半天回头才发现串口里的数据本来就有问题。屏幕只是好听数据源才是关键。5. 精度陷阱与杂散干扰十位ADC实测中的那些意外5.1 十位ADC实际能用的有效位数数据手册标注这是10位ADC但实际有效位数往往只有8到9位。原因有几个参考电压本身有噪声电源纹波会通过AVCC直接耦合进参考源输入引脚的源阻抗影响采样保持电容的充电精度外加内部比较器噪声以及面包板上的串扰。5V参考下1个LSB是4.88mV如果电源纹波有几十毫伏最低位基本就是在抖。所以一个非常常见的省事做法是只读ADCH高8位。分辨率变成5V/256约19.5mV。很多要求不高的项目比如电池电量显示、旋钮刻度读取8位已经足够而且天然稳定。左对齐模式就是为这个准备的。做项目之前先想清楚自己到底要多少位精度再决定读法别一味追求10位全读出来结果最后一位天天跳反而把自己搞晕。5.2 源阻抗和采样时间AVR的ADC内部有一个采样保持电容转换开始时要通过模拟输入引脚给它充电充电是否充分取决于源阻抗。源阻抗太大比如分压电阻用了100k以上或者传感器输出是高阻电容还没充够就切换了读数就会偏低。手册给出的经验值是模拟源阻抗尽量控制在10k以内超过就要注意。解法有两个降低分压回路的总阻值比如用10k电位器替代100k电位器。在ADC引脚对地并联一个0.1uF到1uF的电容。电容相当于预先存了大量电荷输入电压变化时用来给采样保持电容充电读数会稳很多代价是信号响应变慢。输入电压快速波动时要留给它充电的时间。5.3 软件滤波方案采样多次再取平均是成本最低的优化方案。几种常见做法各有特点方案原理适用场景简单平均连读N次取平均值静态信号最推荐上手去极值平均去掉最大值最小值再平均有偶发毛刺的场合滑动平均新数据进来旧数据出去动态信号响应不滞后实测下来测电位器电压我最常用的是连读8次去极值平均一组读数耗时几十毫秒肉眼完全感觉不到延迟。最小二乘、卡尔曼滤波这类方法在这个量级上先别折腾收益还抵不上时间成本。5.4 悬空引脚的坑一次典型排查过程有一次调试我只接了ADC0的电位器程序里顺手把ADC1也读了一下结果发现ADC1的值在1023附近上下乱跳。一开始我以为是电源不行换了独立电源也一样。后来把测量对象换回ADC0一切正常才意识到问题出在ADC1引脚悬空上。仔细想原理很简单模拟输入引脚是高阻状态悬空时引脚电压由外部电场、引脚自身漏电流和内部电路决定哪来的稳定电压表现在数据上就是满量程或者0附近来回跳。处理方式也简单不用的ADC通道就不要去读如果硬件上已经把引脚引出来但暂时没接设备可以接到固定的电平上直接接地或者通过电阻接地都行。这个不算故障纯属使用习惯问题。排查过程也不复杂就是把怀疑对象的每个环节依次替换验证。我把这类经验总结成一句话先怀疑硬件再怀疑代码最后才怀疑芯片坏了。6. 从电压输入到传感器采集分压电路与温度、光照测量扩展6.1 传感器接入AVR ADC的三条原则实验做到这一步ADC的基本功就扎实了。接下来自然会想能不能测点真实的东西接传感器比接电位器复杂一点点但三条原则是通用的传感器输出电压必须落在0到Vref之间超过Vref不仅读数截顶还可能导致引脚损坏。一般要求输入电压不能超过VCC加0.3V。输出阻抗别太高。高阻抗就加缓冲电容或者用运放跟随器做阻抗变换。分压电阻的选取要让信号尽量铺满ADC量程。量程利用率越高分辨率利用率也越高。这三条在下面的例子里立刻就能用到。6.2 NTC热敏电阻测温分压、查表与公式换算NTC热敏电阻是负温度系数温度升高阻值下降。通常把固定电阻和NTC串联成一分压器把温度转成电压。上拉电阻选多少合适原则是在目标测温范围的中点分压输出电压接近Vref的一半这样线性度最好。比如测0到100度常温25度时NTC约10k就选10k固定电阻串联分压点电压约2.5V正好占半量程。换算温度有两种常见路线用B值公式近似计算。B值是厂商给的常数公式为R R25 x exp(B x (1/T - 1/298.15))T是开尔文温度倒推即可。优点是省存储缺点是精度一般。查表加线性插值。事先测出NTC在几个温度点的阻值写成查找表程序里做分段插值精度高不少。浮点运算在8位机上有点费时间好在温度变化慢每秒算一次完全够。我做过一个这样的温度计误差大概能控制在正负1度左右室内温度监测完全够用。6.3 光敏电阻与环境光判断光敏电阻的电路更简单固定电阻串联光敏电阻光线亮时光敏电阻阻值降低分压点电压随之变化。判断逻辑就是连续读几次取平均值再和预先标定的阈值比较。有一个容易被忽略的细节光敏电阻对光线的响应是缓慢的而且和光源类型、遮挡方向都有关所以阈值要做滞回。比如白天亮度略低于阈值不立刻开灯要低到阈值的90%才动作反过来亮度要升到阈值的110%才关闭。这种回差控制在工业上很常见做环境光控灯、教室照明补光都非常实用。6.4 一个把ADC串起来的简单工程如果把前面的知识点串成一个完整小工程我推荐做这个ADC0接NTC分压屏幕上实时显示温度一个LED做高温报警温度超过阈值就点亮。整体结构是这样定时中断每200ms触发一次ADC转换转换完成后在中断里读取结果并存入缓冲区主循环负责做去极值平均、换算成温度、刷新显示、判断报警。这个流程把查询和中断两种模式、软件滤波、电压换算、传感器分压全部用上了。虽然是老掉牙的项目但能把每个环节都稳稳吃透比堆十个花哨的演示工程有用得多。扩展方向也很明确加上继电器就能控加热垫改成光敏电阻就能做智能灯换成分压式电流检测就能做电池监测本质都是同一套基本功。最后分享一点我自己的体会。AD转换这个实验初看不大但它是从数字世界跨向模拟世界的第一个门槛。我见过太多人卡在读数乱跳上最后发现不是芯片坏了而是电位器没接好、参考电压选错、或者ADIF清除方式不对。排查这类问题时我的习惯是先拿万用表从输入电压量起一步一步往后追输入源是干净的程序逻辑是正确的显示出来的码值没道理不稳定。做实验不要怕慢一个环节一个环节验证反而最快。把电路搭起来、两种代码各跑一遍、再用传感器扩展一次之后ADC这块基本就没什么死角了以后做电池供电、信号采集甚至闭环控制这些基本功迟早会派上大用场。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →