3、ADC驱动开发:初始化流程、通道配置、采样时序设置、DMA传输配置
ADC驱动开发,说白了就是让芯片的“耳朵”能听准外界的模拟信号。我刚开始做这个的时候,总觉得不就是配几个寄存器嘛,结果采样出来的数据全是乱的。后来才明白,初始化流程、通道选择、采样时间、DMA搬运,每一步都有讲究。
这一节,咱们就把ADC驱动开发的四个核心环节拆开揉碎。你跟着走一遍,基本就能搞定大部分MCU的ADC外设了。
3.1 ADC初始化流程——先让外设“活”起来
初始化ADC,不是上来就配采样通道。你得先给它供电、选时钟、定分辨率。我习惯按这个顺序来:
- 使能ADC时钟——大部分MCU的ADC外设默认是关着的,得先打开时钟门控。
- 配置ADC时钟分频——ADC有最高工作频率限制,比如STM32的ADC最高14MHz,超了采样值就飘。
- 设置分辨率与数据对齐——12位、10位、8位?左对齐还是右对齐?这决定了你读到的寄存器值怎么换算。
- 选择转换模式——单次转换、连续转换、扫描模式?看应用场景选。
- 使能ADC——最后一步,调用校准函数,然后正式使能。
关键点:ADC使能后,通常需要等待一个稳定时间(比如几十微秒)再开始转换。我见过有人刚使能就立刻采样,前几个数据全是错的。
// 以STM32F4为例,ADC初始化核心代码
void ADC_Init(void)
{
// 1. 使能ADC1时钟
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// 2. 配置时钟分频,ADC时钟 = APB2时钟 / 4
RCC->CFGR |= RCC_CFGR_ADCPRE_DIV4;
// 3. 设置12位分辨率,右对齐
ADC1->CR1 |= ADC_CR1_RES_12BIT;
ADC1->CR2 |= ADC_CR2_ALIGN_RIGHT;
// 4. 单次转换模式,不使能扫描
ADC1->CR1 &= ~ADC_CR1_SCAN;
ADC1->CR2 &= ~ADC_CR2_CONT;
// 5. 使能ADC,等待稳定
ADC1->CR2 |= ADC_CR2_ADON;
delay_us(100); // 等待稳定
// 6. 校准
ADC1->CR2 |= ADC_CR2_CAL;
while(ADC1->CR2 & ADC_CR2_CAL);
}
3.2 通道配置——选对“耳朵”听哪个信号
ADC初始化完了,接下来就是告诉它:你要听哪个引脚上的信号。每个ADC模块通常有多个输入通道,比如ADC1的通道0对应PA0,通道1对应PA1。
配置通道时,我一般关注两个东西:
- 采样通道选择——在规则序列寄存器里写入你要用的通道号。
- 采样时间设置——每个通道可以单独配置采样周期数,这个直接影响采样精度。
我的习惯:如果多个通道的输入阻抗不同,我会给高阻抗信号源配更长的采样时间。比如传感器输出阻抗10kΩ,采样周期至少设到15个ADC时钟周期以上。
// 配置ADC1通道0(PA0),采样时间144周期
ADC1->SQR3 |= 0 << 0; // 第1个转换:通道0
ADC1->SMPR2 |= 7 << 0; // 采样时间:144周期
3.3 采样时序设置——时间不对,数据全废
采样时序是ADC驱动里最容易踩坑的地方。为什么?因为采样时间太短,电容没充满,数据偏小;采样时间太长,转换速度跟不上。
采样时间由两部分组成:
| 参数 | 说明 | 典型值 |
|---|---|---|
| 采样周期数 | ADC采样开关闭合的时间,以ADC时钟周期为单位 | 3~480周期 |
| 转换周期数 | 逐次逼近所需的时间,固定为12周期(12位) | 12周期 |
| 总转换时间 | 采样周期 + 转换周期 | 15~492周期 |
举个例子:ADC时钟12MHz,采样周期设144,那么采样时间就是144/12MHz = 12μs。加上转换的1μs,一次转换总共13μs。采样率大约77kHz。
我曾经踩过的坑:用ADC采集音频信号,采样周期设了3个时钟周期,结果高频成分全衰减了。后来查手册才发现,信号源内阻大,采样时间不够。换成144周期后,波形才正常。所以,采样时间不是越小越好,得看信号源阻抗。
3.4 DMA传输配置——让数据自己“跑”起来
如果每次转换完都靠CPU去读数据,那CPU就别干别的了。DMA就是干这个的——转换完成自动把数据搬到内存里。
配置DMA传输ADC数据,我总结了三步:
- 使能DMA时钟,配置DMA通道。
- 设置源地址为ADC数据寄存器,目标地址为用户定义的数组。
- 设置传输长度(要转换的次数),使能循环模式(如果需要连续采样)。
// DMA配置示例:ADC1转换结果自动搬运到buffer
void ADC_DMA_Config(void)
{
// 1. 使能DMA2时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
// 2. 配置DMA2_Stream0,通道0对应ADC1
DMA2_Stream0->PAR = (uint32_t)&(ADC1->DR); // 源:ADC数据寄存器
DMA2_Stream0->M0AR = (uint32_t)adc_buffer; // 目标:内存数组
DMA2_Stream0->NDTR = 100; // 传输100次
// 3. 配置传输方向、大小、循环模式
DMA2_Stream0->CR |= DMA_SxCR_DIR_PS; // 外设到内存
DMA2_Stream0->CR |= DMA_SxCR_MSIZE_16BIT; // 内存16位
DMA2_Stream0->CR |= DMA_SxCR_PSIZE_16BIT; // 外设16位
DMA2_Stream0->CR |= DMA_SxCR_CIRC; // 循环模式
// 4. 使能DMA流
DMA2_Stream0->CR |= DMA_SxCR_EN;
// 5. 使能ADC的DMA请求
ADC1->CR2 |= ADC_CR2_DMA;
}
小技巧:DMA传输完成后,通常会触发中断。你可以在中断里设置一个标志位,主循环检测到标志位后处理数据。这样CPU既不忙等,又能及时拿到最新数据。
3.5 整体流程框架
下面这张图,把ADC驱动开发的四个环节串起来了。你照着这个流程走,基本不会漏步骤。
3.6 避坑清单
最后,我把这些年调试ADC驱动遇到的典型问题列出来。你调试的时候,可以对照着查:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 采样值始终为0 | ADC未使能、通道未配置、GPIO未设为模拟模式 | 检查时钟、GPIO模式、ADC使能位 |
| 采样值偏小或波动大 | 采样时间太短、信号源阻抗高 | 增大采样周期数,降低ADC时钟频率 |
| DMA传输数据错位 | DMA地址未对齐、传输宽度不匹配 | 确保源/目标地址对齐,外设和内存宽度一致 |
| 转换速度达不到预期 | ADC时钟分频太大、采样周期过长 | 优化时钟分频,在精度允许下减小采样周期 |
| 多通道数据混乱 | 扫描序列未正确配置、DMA缓冲区未对应 | 检查SQR寄存器,确保DMA缓冲区大小匹配通道数 |
最后提醒一句:ADC驱动调试,最忌讳“凭感觉配参数”。每次改完寄存器,都去量一下实际引脚上的波形,看看采样时间够不够。我见过太多人代码写得飞起,结果示波器一挂,发现采样窗口根本没对准信号。