ADC模拟数字转换
所谓的ADC(Analog-to-Digital Convert, 模拟数字转换)就是把模拟信号转换称为数字信号,供MCU进一步进行计算或者传送给其它设备处理。 ADC通常用于传感器数据的采集,一般传感器会把观测的物理量通过转换为电压值,也就是所谓的模拟信号。 由于计算机的世界里只有0和1,所以需要ADC转换器把电压值转换为计算机认识的数字量。
对于传感器而言,有两个参数比较关键:灵敏度和量程。量程很好理解,就是ADC最大的采样值所对应的物理量。 灵敏度是指传感器在稳态工作情况下输出量变化对输入变化量的比值。在这里使用ADC,那么输出一般都是电压值, 灵敏度描述的是敏感物理量与电压值之间的函数关系。一般情况下,两者之间是一个线性的关系,或者说是在一定区间内近似线性的, 因而灵敏度一般就是一个常值。
把传感器的电压信号经过放大后,加载到ADC转换器上,就可以得到数字量。数字量的位数决定了ADC转换的精度,也决定了传感器的精度。 位数越高转换精度就越高。ADC转换电路有很多种,STM32F407使用的是一种逐次比较的方法,具体实现我们不在这里讨论。
在STM32F407中的ADC都是12位的,一共有19个通道,其中16个可以用来连接传感器等外部信号,另外有两个用于内部的信号源, 一个用于\(V_{BAT}\)。它们还支持单次、连续、扫描、不连续等等各种采集方式。本文主要介绍STM32的ADC模块。
1. ADC通道
在STM32上,几乎所有外设的配置都会涉及到芯片引脚的配置。比如说, 我们在串口通信中配置引脚PA9和PA10工作在USART1下分别用于Tx和Rx。 这些管脚的复用功能可以在数据手册第61页的Table 9中查找。 但从它的表头中(如下图1所示)我们可以看到这些复用功能不关ADC的事儿。
图1 引脚复用功能表头 |
在读参考手册时, 我发现STM32有常规通道(regular channel)和注入通道(injected channel)两种,它们两个之间的关系就好像是任务函数与中断函数一样。 正常在对常规通道进行ADC转换时,触发了一个注入通道转换请求,就会暂停对常规通道的转换转而处理注入通道的请求。 当注入通道转换完毕后,就会恢复对常规通道的转换。
这只是触发注入(Trigged Injection)时的关系,还有一种自动注入(Auto Injection)的模式。这种模式是当常规通道的转换完毕后, 就自动执行注入通道的转换操作,就好像是对常规通道的一个扩充。这种自动注入与触发注入模式之间是冲突的,同一时间只能工作在一种模式下。 或者不使用注入通道。
STM32提供常规通道和注入通道的目的还是在于提高ADC的转换效率。我们在一个控制周期中,可能同时要采集多路的ADC数据, 如果通过软件依次触发采集各路数据,过程就会很繁琐,而且效率低下。但我们可以把需要采集的ADC通道按照一定顺序编排如常规通道或者注入通道中, 然后通过某种触发方式,由硬件依次采集每个通道的数据,并写到内存中。
常规通道的转换序列最多支持16个通道,注入通道则是4个。对于常规通道,我们需要在ADC_SQR1的L字段写入待转换的通道数量。 并依次填写转换通道到ADC_SQRx寄存器中。序列中各个通道的顺序完全由我们任意指定,可以重复, 像[ADC_IN3, ADC_IN8, ADC_IN2, ADC_IN3]这样的序列是合法的, 只是对重复的通道多转换了几次而已。类似的,对于注入通道其序列和数量是由寄存器ADC_JSQR决定的。
2. 触发方式
触发进行ADC转换主要有两种方式:软件触发和外部触发。
软件触发方式相对简单,我们可以通过向ADC_CR2寄存器的SWSTART位写1触发转换常规通道里的转换序列;也可以置位ADC_CR2寄存器的JSWSTART, 触发注入通道中的转换序列。
外部触发方式则相对复杂一点,它需要监听计时器或者外部中断线EXTI的信号。控制寄存器ADC_CR2的EXTEN和JEXTEN位描述了外部触发转换的条件, 为00b时关闭外部触发,为01b时在外部信号的上升沿触发转换,为10b时在下降沿转换,为11b时在上升沿和下降沿都做转换。 常规通道和注入通道都有16个可能的外部触发事件,通过EXTSEL和JEXTSEL字段进行配置。 具体的触发事件参见参考手册的第401页的表68和表69。
根据处理的通道形式是单个通道还是多个通道、是否连续转换,ADC有4种转换的形式,由控制寄存器ADC_CR1中的SCAN字段和ADC_CR2中的CONT字段决定, 总结如下:
表1 ADC转换方式
CONT = 0 | CONT = 1 | |
---|---|---|
SCAN = 0 | 单个通道单次转换 | 单个通道连续转换 |
SCAN = 0 | 多个通道单次扫描 | 多个通道连续扫描 |
对单个通道的转换结果都存储在数据寄存器中ADC_DR,对于多通道的转换结果,也会存储在ADC_DR。 但一般都是通过DMA机制保存扫描方式的数据, 把多通道转换的结果直接写到内存中,这样可以提高系统的效率。
此外,还有一种不连续采样的方式,这种方式实际上是对扫描方式的一个变种。它ADC_CR1的DISCNUM字段来定义每个触发事件扫描的通道数量, 扫描通道仍然是由ADC_SQR定义的。一次触发事件,只扫描DISCNUM个通道,下次触发时,将继续扫描DISCNUM个通道。 比如说在SQR中定义了8个通道:0,1,2,3,4,5,6,7。DISCNUM = 3,那么第一次触发时扫描通道0,1,2,第二次触发时扫描通道3,4,5,第三次触发时则扫描剩下的序列。 再次触发时将重0开始重新扫描。需要注意的是,不能同时对常规通道和注入通道使用这种不连续采样的方式。
3. 转换时钟
在ADC转换器中一共涉及到模拟和数字两个时钟。
模拟时钟ADCCLK是用于驱动AD转换的。这个时钟是从APB2总线时钟分出来的,对所有的ADC外设都适用。 我们可以通过修改ADC公共寄存器ADC_CCR的ADCPRE字段,对APB2时钟进行2/4/6/8分频。因为STM32采用的是逐次比较的ADC转换方法, 而且转换器的位长是12位,所以一次ADC转换至少需要12个ADCCLK周期。
数字时钟与APB2的总线时钟是一致的。它主要用来进行寄存器的读写访问的。
4. 例程
在本文的例程中,我们将连续的进行两个通道的ADC转换,为了提高系统的效率,我们采用DMA的形式获取ADC转换结果。 这样在系统完成初始化操作后,ADC和DMA就会在后台协作一直监视这目标信号,降低了软件的复杂度。
通过查询数据手册,我们可以看到PA0和PA1分别可以用作三个ADC的通道0和通道1。 结合原理图知道这两个引脚已经用排针引出, 我们可以直接用杜邦线将之接到外部电源上测试,只是注意电压值不要超过3.3V否则可能烧坏引脚。
我们通过函数adc_gpio_init()配置PA0和PA1工作在模拟输入的模式下,同时关闭上下拉电阻。
void adc_gpio_init() {
RCC->AHB1ENR.bits.gpioa = 1;
GPIOA->MODER.bits.pin0 = GPIO_Mode_Analog;
GPIOA->PUPDR.bits.pin0 = GPIO_Pull_No;
GPIOA->MODER.bits.pin1 = GPIO_Mode_Analog;
GPIOA->PUPDR.bits.pin1 = GPIO_Pull_No;
}
在函数adc_dma_init()中,我们通过参数buf指定一段缓存来存放ADC转换后的数据。 具体的配置内容在代码的注释中已经写的十分详细了这里不再赘述。
void adc_dma_init(uint16 *buf) {
RCC->AHB1ENR.bits.dma2 = 1;
// DMA2_Stream0_Channel0 -> ADC1
dma_reset_stream(DMA2_Stream0);
DMA2_Stream0->CR.bits.CHSEL = 0; // 通道选择
DMA2_Stream0->CR.bits.DIR = DMA_DIR_P2M; // 传输方向
DMA2_Stream0->CR.bits.CIRC = 1; // 打开循环模式
DMA2_Stream0->CR.bits.PL = DMA_Priority_Low; // 低优先级
DMA2_Stream0->CR.bits.PINC = 0; // 外设地址不增长
DMA2_Stream0->CR.bits.PSIZE = DMA_PSIZE_16Bits; // 外设数据宽度
DMA2_Stream0->CR.bits.MINC = 1; // 内存增长
DMA2_Stream0->CR.bits.MSIZE = DMA_PSIZE_16Bits; // 内存数据宽度
DMA2_Stream0->CR.bits.MBURST = DMA_Burst_0; // Single Transfer
DMA2_Stream0->CR.bits.PBURST = DMA_Burst_0; // Single Transfer
DMA2_Stream0->FCR.bits.DMDIS = 0; // 保持Direct Mode
DMA2_Stream0->FCR.bits.FTH = DMA_FIFO_2;
DMA2_Stream0->PAR = (uint32)(&(ADC1->DR)); // 指定外设地址
DMA2_Stream0->M0AR = (uint32)buf; // 指定内存地址
DMA2_Stream0->NDTR.all = 2; //
DMA2_Stream0->CR.bits.EN = 1;
}
我们在函数adc_init()中完成ADC转换器的所有初始化配置,首先分别调用adc_gpio_init()和adc_dma_init()完成引脚和DMA的初始化, 并开启ADC1的驱动时钟。
void adc_init(uint16 *buf) {
adc_gpio_init();
adc_dma_init(buf);
RCC->APB2ENR.bits.adc1 = 1;
接着,我们配置ADC的通用寄存器,做出一些基本的配置,主要是指定各个转换器独立工作,开启DMA。
ADC_COM->CCR.bits.MULTI = ADC_Mode_Independent; // ADC独立工作
ADC_COM->CCR.bits.ADCPRE = ADC_Prescaler_Div2; // 分频系数
ADC_COM->CCR.bits.DMA = ADC_DMA_Mode1; // 开启DMA
ADC_COM->CCR.bits.DELAY = ADC_SamplingDelay_5Cycles; // 两个采样工作之间的时间间隔
然后,配置ADC1让它工作在连续扫描的状态,并告知有两个ADC任务需要通过常规通道处理。
ADC1->CR1.bits.RES = ADC_RES_12Bits; // 分辨率
ADC1->CR1.bits.SCAN = 1; // 开启SCAN模式
ADC1->CR2.bits.CONT = 1; // 开启连续转换模式
ADC1->CR2.bits.EXTEN = ADC_ExtTrigger_Dis; // 禁止触发检测
ADC1->CR2.bits.ALIGN = ADC_Align_Right; // 右对齐
ADC1->SQR1.bits.L = 2 - ADC_SQR_LOffset; // 2个转换在规则序列中
而后,我们需要在常规通道中安排转换任务,设定它们的采样周期。
ADC1->SMPR2.bits.SMP0 = ADC_Sample_Time_3Cycles; // 采样频率
ADC1->SMPR2.bits.SMP1 = ADC_Sample_Time_3Cycles;
ADC1->SQR3.bits.SQ1 = 0; // 依次转换0,1两个通道
ADC1->SQR3.bits.SQ2 = 1;
最后,配置ADC1在每次完成常规通道中的任务后重新触发新的转换。同时开启DMA和ADC转换器,通过软件的形式触发开始转换。
ADC1->CR2.bits.DDS = 1; // 连续DMA请求
ADC1->CR2.bits.DMA = 1; // 开启DMA
ADC1->CR2.bits.ADON = 1; // 开启ADC
ADC1->CR2.bits.SWSTART = 1; // 开始转换
}
如此,我们定义一个全局的数组adcbuf,并在main()函数中调用函数adc_init(),指定adcbuf为转换结果缓存。 那么在完成初始化操作后,就可以随时通过adcbuf来获取转换结果,甚是方便, 详细请参考源代码。
uint16 adcbuf[2];
int main(void) {
// 此处省略了与ADC无关的各种语句
adc_init(adcbuf);
uart_send_bytes(USART1, (uint8*)&adcbuf[0], 2);
uart_send_bytes(USART1, (uint8*)&adcbuf[1], 2);
}
5. 总结
模拟/数字转换器(ADC)是计算机观察物理世界的一个重要工具,是很多传感器得以应用的基础。 STM32的ADC转换器提供了常规通道和注入通道,使得我们可以一次触发进行多个通道的转换,借助DMA机制可以有效的提高系统效率。