STM8的ADC模数转换器
A/D转换器概述
模拟信号和数字信号的定义
-
简单描述什么是模拟信号和数字信号:
- 模拟量在时域上是连续的,比如一个人的身高;今年是150cm,过一年是155cm,在这一年的时间中他的身高数值是连续变化的,可能一月是151.22cm,二月是152.10cm……在一张身高为纵轴时间为横轴的折线图上可以连成一条不断开的线
- 数字量在时域上是离散的,比如公交车里的人数,这个时间节点是10人,过了一会儿到下个时间节点有人下车变成8人,而这两个节点之间就是断开的,其间公交车的人数不会出现10.5人,9.5人的变化
-
因为模拟量包含的数据是无尽的,比如刚刚身高的例子,若是没有测量精度限制,小数点后可以有无穷位,计算机显然不能保存无穷的信息(在浮点数的精度一节中可以更加详细地了解这一点),因此要把现实当中的模拟信号转换为数字信号才能被计算机所处理,计算机得出的数字信号同样需要进行转换变成模拟信号输出
ADC的定义
- ADC,全称模拟数字转换资源,其作用是将外界的模拟电参量转换为数字信号,让单片机得以接受与处理外界信号,因此在中高端单片机芯片中很常见
- STM8S系列单片机芯片带有一路10位的基于逐次逼近型的ADC,最多支持16通道(通道数多少和芯片封装引脚数目有关)
ADC的分类
-
ADC1
STM8S芯片的001/003/103/105型号内置的ADC属于ADC1,最多支持10通道,拥有一些扩展功能:
- 转换结果上/下限报警功能(也即硬件A/D看门狗):当转换结果超出设定值时报警
- 拥有单次/连续转换方式,并且还支持带缓存的连续方式/单次扫描/连续扫描三种工作方式
-
ADC2
STM8S芯片的207/208芯片型号内置的ADC属于ADC2,最多支持16通道,功能相对ADC1较为简单,拥有单次/连续转换方式
本章主要介绍比较简单的ADC2功能,对ADC1也是通用的
ADC的模拟通道
-
与TIM1的捕获/比较通道类似,ADC也需要借助通道将外部的模拟信号引入,这个通道即模拟通道,它也是与一般的GPIO引脚功能复用的
-
模拟通道的对应引脚
通过查询数据手册的芯片引脚图,可以知道模拟通道功能与哪些GPIO引脚复用,它们零散地分布于各组GPIO端口中
以48脚的STM8S105C6(有10个模拟通道)为例:
- AIN0-7复用PB0-7
- AIN8复用PE7,AIN9复用PE6
对于有更多引脚(共有16个模拟通道)的型号:
- AIN10复用PF0
- AIN11-15对应PF3-7
-
模拟通道的引脚配置
如果需要启用模拟通道,需要把引脚模式配置为关外部中断的悬浮输入模式
这是因为配置为ADC复用功能时,引脚的的I/O功能会被自动禁用,配置成悬浮输入可以避免弱上拉输入对模拟通道的干扰
代码如下:
PB_DDR = 0; PB_CR1 = 0; PB_CR2 = 0;//PB7-0配置 PE_DDR &= 0x3F; PE_CR1 &= 0x3F;//001111111 PE_CR2 &= 0x3F;//PE7和6 PF_DDR &= 0x06; PF_CR1 &= 0x06;//00000110 PF_CR2 &= 0x06;//PF7-3和PF0
ADC原理与相关概念
逐次逼近型ADC
-
逐次逼近的ADC工作原理转换速度快,精度高,是应用最普遍的转换方法,也是STM8S上ADC所用的方式
逐次逼近的工作原理是让单片机产生不同的输出电压,将输入与不同的输出电压进行比较,最接近的那个输出值也即可以代表输入模拟量的数字量,由此将模拟信号转换为了数字信号
所谓逐次逼近指的是这个比较过程,其原理是对分搜索法,具体流程如下:
-
首先,将输入信号加到比较器的一端
-
单片机会有一个用于ADC过程的最大参考电压VREF,也就是单片机ADC最大能转换的电压
以VREF为参考,有多个位控制加到比较器另一端的输出电压的值,第x位控制着VREF的1/2x,具体控制方法如下:
-
首先使最高位1,相当于取VREF的1/2(也就是1/21)与输入电压比较,如果值在此范围内,就把最高位置0,否则置1
-
然后处理下一位,这一位相当于取VREF的1/4,如果上一位是1,这一位也是1,那么相当于输出了3/4的VREF,如果输入的值比这个值高,就再去处理下一位;比这个值低,就把这一位置0再处理下一位
-
重复如上步骤,直到处理完所有的位,从高位到低位逐位增加转换位数如此逐位加下去,不断逼近输入电压,便是逐次逼近的含义
-
分辨率
-
分辨率
ADC能分辨量化信号的能力,用二进制位数表示,如10位,8位,从上文介绍的原理不难看出,这个位数n决定了ADC能区分2n个不同的电压等级
ADC能输出(判断)的最小电压,就是1/2n×VREF,n越大,这个值也就越小,能分辨的电压越小,也就是分辨率高
-
量化分辨率
ADC能输出(判断)的最小电压1/2n×VREF,称为量化分辨率LSB,也称数字量最小有效位
比如一个10位的ADC,参考电平为芯片常见的3.3V,它的LSB是1/210×3.3=3.22mV,这个数值能够量化体现ADC的分辨率高低
- 根据公式,可见想要提升量化分辨率,可以通过降低VREF实现
精度
-
注意分辨率和精度是两个概念:分辨率是固定的;分辨率很高,但受温漂、线性度的影响,精度可能不高
-
绝对精度
输出的数字值与理想值之间的偏差,以量化分辨率的分数值来衡量,如±1LSB,意味着实际输出的数字值可能与理想值相差不超过1个最低有效位
-
相对精度
整个输入范围内,每个输出码对应的模拟输入值与理想值之间的偏差,关注的是线性度:输出码与模拟输入值之间的关系应该是线性的,即每个码对应的模拟输入值与理想值之间的偏差应该是一致的
-
举一个例子分辨决定和相对精度:
例如满量程为10V,10位A/D芯片,其绝对精度为±1/2LSB,求其绝对精度值与相对精度
10位芯片,故210=1024,得LSB=10V/1024=9.77mV,绝对精度1/2LSB=4.88mV,相对精度为4.88mV/10V=0.048%
转换时间
- 完成一次A/D转换需要的时间
- 其倒数就是转换速率
量程
- 能转换的模拟输入电压范围
- 分单极性(范围从0开始)和双极型(范围分正负)
单片机ADC的性能指标
- 上文提到ADC的性能由分辨率、精度、转换速度等指标来衡量,现在探讨单片机上的ADC资源(以基础的ADC2为例介绍)的性能指标由什么决定
转换速度
-
fADC时钟
ADC核心单元的转换速度与fADC时钟有很大关系
fADC是fMASTER经过ADC_CR1的SPSEL[2:0]配置的预分频器再次分频后得到的时钟
而fADC的最高频率取决于VDDA的电压,3.3V下为4MHz,5V则为6MHz
-
转换用时
ADC单元转换一次需要需要14个时钟周期:3个采样ADC时钟和11个转换过程时钟,不同的fADC下所需转换时间不同,例如在4MHz下完成一次转换最短用时为14×(1/4M)=3.5us
此外上电唤醒ADC时需要一定时间等待其稳定(类似启动HSE)
分辨率与精度
-
如上文所述,数据手册上注明的ADC器件参数8位、10位、16位等便是单片机ADC的分辨率,至于量化分辨率和精度的计算不再赘述
-
在上文提到通过改变参考电压,可以提示量化分辨率,以提升精度
- 低于48引脚的单片机没有正负参考电压引脚,VREF+默认为模拟电压正VDDA,VREF-默认为模拟电源地VSSA
- 64/80引脚的单片机有正负参考电压引脚,VREF=VREF+-VREF-,即降低VREF+或提高VREF-
ADC的模式及启用
- 较为简单的ADC2支持单次转换和连续转换两种模式
- 功能更多的ADC1额外支持三种转换模式:带缓存的连续方式/单次扫描/连续扫描
单次转换
-
单次转换概述
-
只转换一次,一旦转换完成,就将结果保存在ADC数据寄存器ADC_DR中
-
完成时ADC控制/状态寄存器ADC_CSR的转换结束标志位EOC被置1,结束后用户可以对EOC清零
另外如果EOCIE位为1则在结束时产生中断
-
-
启用单次转换模式
ADC_CR1的CONT位(位1)置0,然后将ACD_CR1_ADON位置1
连续转换
-
连续转换概述
-
上一次转换完成(通过标志位判断)后即刻启动下一次转换,之间没有停顿
直到ADON位被置0(关闭ADC)或者ADC_CR1的CONT位(位1)置0(改为单次转换模式)
-
必须在当前转换结束前读取上一次的转换结果,并清零EOC,否则连续的转换会导致数据覆盖
-
这种模式适用于对同一通道连续多次A/D转换的操作
-
-
启用连续转换模式
ADC_CR1的CONT位(位1)置1,与单次转换相反
带缓冲的连续方式
-
与普通的连续转换对比
- 本质上仍是连续转换方式,区别在于每一次转换后将结果保存在缓存中,不会马上被下一次转换覆盖
- 转换结果保存到其中寄存器ADC_DBxR中,这些寄存器即缓存,共有8或10(取决于通道数)个16位,每个16位被分为高低位两个8位寄存器ADC_DBxRH和ADC_DBxRL
- 当缓存被写满时,EOC才为1,这时用户需要读出这些数据;ADC_CR3_OVR标志位提示出现出现数据覆盖现象
-
启用带缓存的连续模式
ADC_CR1的CONT位(位1)置1
ADC_CR2的SCAN位(位1)置0
ADC_CR3的DBUF位(位7)置1
- ADC2没有SCAN位和DBUF位这两个控制位,由此只有CONT控制的单次和连续两种模式可选
ADC1特有的单次扫描转换
-
ADC1特有的单次转换概述
- ADC1特有的单次和普通的单次转换区别在于其触发后会从0通道开始转换,完成后自动切换到下一通道,将结果保存道ADC_DBxRH和ADC_DBxRL中
- 直到所有通道都转换完成才将标志位EOC置1,这时应该把ADON置0,要进行新一轮转换时再置1
- 适合对所有通道进行单次转换,不必和普通单次转换一样要用软件配置切换转换通道
-
启用ADC1特有单次转换
ADC_CR1的CONT位(位1)置0
ADC_CR2的SCAN位(位1)置1
ADC1特有的连续扫描转换
-
ADC1特有的连续转换概述
和ADC1特有的单次转换类似,只是扫描转换完成后不停止,继续继续下一轮,直到ADON被置0断电或CONT为0(在下一轮的最后一个通道转换结束时停止)
当EOC为1时表示完成一轮转换,这时用户需要读出这些数据防止数据覆盖;ADC_CR3_OVR标志位提示出现出现数据覆盖现象
-
启用ADC1特有连续转换
ADC_CR1的CONT位(位1)置1
ADC_CR2的SCAN位(位1)置1
ADC配置流程
ADC配置简述
-
选择转换通道
设置ADC_CSR的CH[3:0]位
-
配置时钟分频系数
设置ADG_CR1的SPSEL[2:0]位,对fMASTER时钟再次分频得到fADC时钟
-
外部触发与数据排列
设置ADG_CR2,配置外部触发方式和触发使能,配置转换数据对齐方式
-
初始化模拟信号输入引脚
-
引脚采用不带中断的悬空输入方式
-
还需配置施密特触发器禁止寄存器ADC_TDR寄存器
-
-
启用ADC
将ACD_CR1_ADON位置1,给ADC转换器加电
- 在上电后需要等待一段时间让其稳定
- 不用时置零,避免额外耗电
ADC相关寄存器
-
ADC控制/状态寄存器ADC_CSR
用于选择模拟通道,并且可以配置中断,另外还有控制和反映转换状态的位
关于中断的配置,取决于fADC和fCPU两者的值:如果CPU频率更高,采用中断方式可以减少ADC用时对程序运行的阻碍,而ADC时钟更高,则用查询方式更合适;
ADC完成时产生的中断向量号为22,使用
#pragma vector=24
-
AIN0-7对应PB0-7;AIN8对应PE7;AIN9对应PE6
AIN10对应PF0;AIN11-15对应PF3-7
可以直接使用通道序号来配置,如使用PE6时就
ADC_CSR_CH=9;
-
-
配置寄存器1 ADC_CR1
配置分频系数和设定转换模式,开启ADC的ADON位也在这个寄存器
分频系数由SPSEL[2:0]设定,分频值为设定值+2
-
配置寄存器2 ADC_CR2
配置启用外部事件触发ADC转换:ADC2可以受TIM1产生的TRGO事件或是ADC_ETR引脚上的外部中断触发进行一次AD转换
另外还能配置产生数据对齐的对齐方式
-
数据寄存器ADC_DR
转换结果保存在这个寄存器中(对于单次和连续转换模式)
分为ADC_DRH和ADC_DRL高低两个寄存器,因为10位的ADC资源得到的结果一个8位的寄存器放不下,使用要分成两个寄存器,ADC_CR2配置的对齐方式由此而来:
- 采用左对齐,高8位写入H寄存器(D9-D2),剩下写入L(D1-D0),读取时要先高后低
- 采用右对齐,低8位写入L(D9-D8),剩下写到H(D7-D0),读取时要先低后高
- 必须按顺序连续两条命令读取,因为芯片有读取一个寄存器时自动锁存另一个寄存器的功能来保证数据一致性,不按顺序读容易出错
ADC_DBxRH、ADC_DBxRL类似,只是有多个寄存器器以储存各个通道的结果,不再赘述
-
施密特触发器禁止寄存器ADC_TDR
由于有16个模拟通道,分为ADC_TDRH和ADC_TDRL两个寄存器,从高位到低位控制各个通道施密特触发器的工作
- 置0时使能施密特触发功能,可以滤除高频干扰信号提升精度
- 置1时禁止,禁止是为了降低I/O引脚的功耗
代码实现
-
初始化ADC
对ADC进行初始化配置的示例,按需更改各个控制位以启用不同功能
void ADC_Init() { //首先配置引脚,此处使用PE6引脚,对应AIN9通道,将其配置为悬浮输入 PE_DDR_DDR6 = 0; PE_CR1_C16 = 0; PE_CR2_C26 = 0; //接下来配置ADC //由于每个寄存器有很多功能,再使用两位16进制数来配置不方便,直接配置各个位 ADC_CSR_EOCIE = 1;//转换结束后中断 ADC_CSR_CH = 9;//PE6引脚,对应AIN9通道 ADC_CR1_SPSEL = 0;//对f_master进行2分频得到f_adc ADC_CR1_CONT = 0;//单次转换模式 ADC_CR2_ALIGN = 1;//数据右对齐,读数时先读低位,再读高位 ADC_TDRH = 0xFF; ADC_TDRL = 0xFF;//关施密特触发器以降低功耗 ADC_CR1 |= 0x01;//首次上电,唤醒ADC }
-
实验代码
接下来以使用PE6作输入引脚,进行模数转换的实验,实验结果是将PE6测得的电压显示到数码管中,因为数码管的动态刷新占用了主程序,所以使用中断的方式来进行ADC转换,并且使用定时器来启动ADC
float v_ref=4.45;//参考电压,实际的参考电压未必恰是5V,需要通过测量得到 u16 adc_val=0;//存放获取的ADC转换值 float vol;//存放ADC的最终结果 int main() { Digit_init();//使用数码管来显示结果 ADC_init();//见上文,设置PE6为输入 TIM4_init();//恰当设定定时器来启动ADC asm("rim"); while(1) { display(2,(u16)vol);//这是电压的个位 display(3,((u8)(vol*10))%10);//这是电压的小数点后一位 } } #pragma vector=24 //ADC完成时的中断函数 __interrupt void ADC_IRQHandler(void) { ADC_CSR_EOC = 0;//清除ADC中断标志EOC adc_val = ADC_DRL;//先取低位 adc_val |= ADC_DRH<<8;//再取高位 //现在得到的还不是实际的电压值,只是ADC_DR中的值,需要进行计算 vol = (adc_val/1024.0)*v_ref; //根据ADC逐次逼近法的原理,10位ADC的LSB=v_ref/1024 //为了简化计算,也可以将这个值提前算好,如5V参考电压下为4.882mv } #pragma vector = 25//TIM4中断 __interrupt void TIM4_Overflow_IRQHandler() { TIM4_SR &= 0xFE; ADC_CR1_ADON = 1;//使用定时器来启动ADC } //如果你的单片机上有支持静态显示的相关硬件,不必使用动态刷新来显示多位数的话 //那么就不必用定时器去启动ADC,这个写法只是为了兼容没有相应硬件的单片机
滤波算法
-
为何要滤波
上文的代码已经可以实现ADC转换,但是其精度还能进一步提升
ADC的分辨率并非决定ADC精度的唯一要素,提升AD转换系统可靠性也很重要:除了对供电和转换参考电压进行优化(前文已介绍为何以及如何调整参考电压),还需要对输入信号进行处理,滤除高频干扰信号以防测量结果不稳定
而滤波处理有两种方式(这和按键一节中的两种去抖方式类似)
-
硬件滤波
在信号输入前接RC低通滤波器,截止频率fC=1/(2πRC)
滤除截止频率以上的信号,后再送入引脚的施密特触发器进行整型
一如按键去抖的介绍,硬件滤波效果好,但是成本高,电路设计复杂
-
软件滤波
又称数字滤波,显然对于复杂的模拟信号不能再使用按键那样简单的延时消抖,而要使用滤波算法,滤波算法有多种,下面介绍较为简单的一种方法
算术平均滤波算法:类似评委打分时的机制,去掉最高分和最低分(避免少数极端数据对整体的影响),再将剩下的分数求平均值,实现代码如下:
//在使用这个函数前,要定义一个数组,将多次采样数据存入其中 u16 adc_temp[10]={0};//10次采样数据 u16 adc_val=0;//单次采样数据结果 void AVG_adctemp(void) { u8 i,j;//循环用的变量 u16 temp;//暂存用的变量 for(i=10;i>=1;i--)//将AD_Vtemp中的数据由小到大排序 { for(j=0;j<(i-1);j++) { if(adc_temp[j]>adc_temp[j+1]) { temp=adc_temp[j]; adc_temp[j]=adc_temp[j+1]; adc_temp[j+1]=temp; } } } for(i=2;i<=7;i++)//去掉两个最低和最高值,求平均 { adc_val += adc_temp[i]; } adc_val = (u16)(adc_val/6); }
-
使用软件滤波的实验
对上文的实验代码进行一些改进,加上软件滤波
float v_ref=4.45;//参考电压 float vol;//最终结果 u8 i = 0;//用于10次采样的数组遍历 extern u16 adc_val;//滤波后得到的采样数据结果 extern u16 adc_temp[10]; //软件滤波的函数写在另一个文件中,使用extern在main中调用这些变量 void ADC_init(void); void AVG_adctemp(void); int main() { Digit_init(); ADC_init(); TIM4_init(); asm("rim");//开总中断 while(1) { Display(2,(u16)vol); Display(3,((u8)(vol*10))%10); } } #pragma vector=24 __interrupt void ADC_IRQHandler(void) { ADC_CSR_EOC = 0;//清除ADC中断标志EOC if(i<10) { adc_temp[i] = ADC_DRL; adc_temp[i] |= ADC_DRH<<8;//取10次ADC测量值 i++; } if(i>=10) { i=0; AVG_adctemp();//进行软件滤波,其函数写在另一个文件中 vol = (adc_val/1024.0)*v_ref; } } #pragma vector = 25//TIM4中断号为23,偏移2得25 __interrupt void TIM4_Overflow_IRQHandler() { TIM4_SR &= 0xFE; ADC_CR1_ADON = 1; } //由于中断中处理的东西变多了,因此数码管会有闪烁,使用静态显示效果会更完美 //如果不使用中断来进行ADC测量,将这些代码封装进函数中,按需调用即可 //另外,不使用中断时要适时插入等待转换完成的语句while(ADC_CSR_EOC==0); //以防处理数据的过程和ADC的过程发生冲突 //不使用中断时也要记得清除转换完成标志位ADC_CSR_EOC=0;
本文来自博客园,作者:无术师,转载请注明原文链接:https://www.cnblogs.com/untit1ed/p/18660274
本文使用知识共享4.0协议许可 CC BY-NC-SA 4.0
请注意: 特别说明版权归属的文章以及不归属于本人的转载内容(如引用的文章与图片)除外
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程