1. STM32 ADC寄存器操作深度解析
在嵌入式系统开发中,ADC(模数转换器)是最常用的外设之一。STM32系列MCU内置了高性能的ADC模块,通过直接操作寄存器可以实现灵活高效的模拟信号采集。相比使用库函数,寄存器级操作能带来更精确的控制和更高的执行效率。
2. ADC寄存器详解
2.1 ADC状态寄存器(ADC_SR)
ADC_SR寄存器位于偏移地址0x00处,复位值为0x00000000。这个寄存器包含了ADC模块的各种状态标志:
- EOC位(End of Conversion):转换完成标志
- JEOC位(Injected End of Conversion):注入通道转换完成标志
- AWD位(Analog Watchdog):模拟看门狗触发标志
注意:读取DR寄存器会自动清除EOC标志,但JEOC和AWD标志需要手动清除。
2.2 ADC控制寄存器1(ADC_CR1)
偏移地址0x04,复位值0x00000000。这个寄存器主要控制ADC的工作模式和中断:
- SCAN位:开启扫描模式(多通道采集)
- JEOCIE位:注入通道转换完成中断使能
- AWDIE位:模拟看门狗中断使能
- DISCNUM[2:0]:不连续模式下的通道数
- JDISCEN位:注入通道不连续模式使能
- JAUTO位:自动注入模式
2.3 ADC控制寄存器2(ADC_CR2)
偏移地址0x08,复位值0x00000000。这是ADC最重要的控制寄存器:
- ADON位:ADC开启/关闭控制
- CONT位:连续转换模式
- CAL位:启动校准
- RSTCAL位:复位校准
- ALIGN位:数据对齐方式(0=右对齐,1=左对齐)
- JEXTTRIG/EXTTRIG位:外部触发选择
- JSWSTART/SWSTART位:软件启动转换
提示:每次上电后必须先执行校准(设置CAL位),校准完成才能开始正常转换。
2.4 ADC采样时间寄存器
ADC_SMPR1(偏移0x0C)和ADC_SMPR2(偏移0x10)控制各通道的采样时间:
- SMPx[2:0]:通道x的采样时间选择
- 可选值:1.5/7.5/13.5/28.5/41.5/55.5/71.5/239.5周期
采样时间计算公式:
采样时间(μs) = (SMPx + 1) × (1/ADC时钟频率)
例如,当ADC时钟为12MHz时:
- SMPx=0(1.5周期):0.125μs
- SMPx=6(239.5周期):20μs
2.5 序列寄存器
ADC_SQR1-3(偏移0x2C-0x34)控制规则通道转换序列:
- L[3:0]:转换序列长度(0=1个通道,1=2个通道...)
- SQx[4:0]:序列中第x个转换的通道号
ADC_JSQR(偏移0x38)控制注入通道转换序列:
- JL[1:0]:注入序列长度(0=1个通道,1=2个通道...)
- JSQx[4:0]:注入序列中第x个转换的通道号
3. ADC编程实例
3.1 单次采样模式(查询方式)
c复制void ADC1_Single_Init(void) {
// 1. 开启时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN;
RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6; // 12MHz ADC时钟
// 2. 配置GPIO
GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0); // PA0模拟输入
// 3. 配置ADC
ADC1->CR1 = 0; // 单通道、无扫描
ADC1->CR2 = 0; // 单次转换、右对齐
ADC1->SQR1 = 0; // 1个转换
ADC1->SQR3 = 0; // 通道0
ADC1->SMPR2 = ADC_SMPR2_SMP0_2; // 239.5周期采样
// 4. 校准
ADC1->CR2 |= ADC_CR2_ADON;
delay_ms(1);
ADC1->CR2 |= ADC_CR2_CAL;
while(ADC1->CR2 & ADC_CR2_CAL);
}
uint16_t ADC1_Read(void) {
ADC1->CR2 |= ADC_CR2_SWSTART;
while(!(ADC1->SR & ADC_SR_EOC));
return ADC1->DR;
}
3.2 连续采样模式(中断方式)
c复制volatile uint16_t ADC_Value = 0;
void ADC1_Cont_Init(void) {
// 1. 时钟和GPIO配置(同上)
// 2. 配置ADC
ADC1->CR1 = ADC_CR1_EOCIE; // 使能中断
ADC1->CR2 = ADC_CR2_CONT | ADC_CR2_ADON; // 连续模式
// 3. 配置NVIC
NVIC_EnableIRQ(ADC1_2_IRQn);
NVIC_SetPriority(ADC1_2_IRQn, 1);
// 4. 校准和启动
ADC1->CR2 |= ADC_CR2_CAL;
while(ADC1->CR2 & ADC_CR2_CAL);
ADC1->CR2 |= ADC_CR2_SWSTART;
}
void ADC1_2_IRQHandler(void) {
if(ADC1->SR & ADC_SR_EOC) {
ADC_Value = ADC1->DR;
ADC1->SR &= ~ADC_SR_EOC;
}
}
3.3 多通道扫描模式(DMA方式)
c复制uint16_t ADC_Values[3]; // 存储3个通道的数据
void ADC1_DMA_Init(void) {
// 1. 开启时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN;
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 2. 配置GPIO(PA0-PA2)
GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0 |
GPIO_CRL_MODE1 | GPIO_CRL_CNF1 |
GPIO_CRL_MODE2 | GPIO_CRL_CNF2);
// 3. 配置DMA
DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR;
DMA1_Channel1->CMAR = (uint32_t)ADC_Values;
DMA1_Channel1->CNDTR = 3;
DMA1_Channel1->CCR = DMA_CCR_MINC | DMA_CCR_CIRC |
DMA_CCR_PSIZE_0 | DMA_CCR_MSIZE_0;
// 4. 配置ADC
ADC1->CR1 = ADC_CR1_SCAN;
ADC1->CR2 = ADC_CR2_CONT | ADC_CR2_DMA | ADC_CR2_ADON;
ADC1->SQR1 = (2 << 20); // 3个转换
ADC1->SQR3 = (0 << 0) | (1 << 5) | (2 << 10); // 通道0,1,2
ADC1->SMPR2 = ADC_SMPR2_SMP0_2 | ADC_SMPR2_SMP1_2 | ADC_SMPR2_SMP2_2;
// 5. 启动
DMA1_Channel1->CCR |= DMA_CCR_EN;
ADC1->CR2 |= ADC_CR2_CAL;
while(ADC1->CR2 & ADC_CR2_CAL);
ADC1->CR2 |= ADC_CR2_SWSTART;
}
4. 关键注意事项
4.1 时钟配置
STM32F1系列ADC时钟最大14MHz,在72MHz系统时钟下必须6分频:
c复制RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6; // 12MHz
4.2 采样时间选择
采样时间需要根据信号源阻抗选择:
- 低阻抗信号:13.5周期(~1.1μs @12MHz)
- 高阻抗信号:239.5周期(~20μs @12MHz)
4.3 校准流程
每次开启ADC后必须执行校准:
c复制ADC1->CR2 |= ADC_CR2_ADON;
delay_ms(1); // 等待ADC稳定
ADC1->CR2 |= ADC_CR2_CAL;
while(ADC1->CR2 & ADC_CR2_CAL); // 等待校准完成
4.4 数据对齐
STM32 ADC支持两种数据对齐方式:
- 右对齐(ALIGN=0):数据在DR寄存器的低12位
- 左对齐(ALIGN=1):数据在DR寄存器的高12位
提示:右对齐更直观,直接读取就是0-4095的原始值。
4.5 多通道采集顺序
在多通道扫描模式下,转换顺序由SQR寄存器决定:
c复制// 设置3个通道的转换顺序:CH2→CH5→CH8
ADC1->SQR1 = (2 << 20); // 3个转换
ADC1->SQR3 = (2 << 0) | (5 << 5) | (8 << 10);
5. 性能优化技巧
5.1 减少转换时间
转换时间 = 采样时间 + 12.5个ADC周期
例如,当ADC时钟为12MHz时:
- 采样时间13.5周期:总时间约2.17μs
- 采样时间239.5周期:总时间约21μs
5.2 使用DMA提高效率
对于多通道高速采集,DMA可以显著降低CPU开销:
c复制// 配置DMA循环模式
DMA1_Channel1->CCR |= DMA_CCR_CIRC;
5.3 注入通道的使用
注入通道可以中断规则通道序列,适合高优先级信号:
c复制// 配置注入通道
ADC1->JSQR = (1 << 20) | (3 << 15); // 1个注入通道,CH3
ADC1->CR1 |= ADC_CR1_JAUTO; // 自动注入
5.4 模拟看门狗
可以设置阈值监控特定通道:
c复制// 监控通道1,阈值1000-3000
ADC1->HTR = 3000;
ADC1->LTR = 1000;
ADC1->CR1 |= ADC_CR1_AWDEN | ADC_CR1_AWDIE;
6. 常见问题排查
6.1 采样值不准确
可能原因及解决方法:
- 未执行校准:确保每次上电后执行校准流程
- ADC时钟超限:检查是否超过14MHz限制
- 采样时间不足:增加SMPx设置
- 电源噪声:添加滤波电容(0.1μF+1μF)
6.2 DMA不工作
检查要点:
- DMA通道是否使能(CCR.EN=1)
- 内存地址是否自增(CCR.MINC=1)
- 数据宽度是否匹配(CCR.PSIZE/MSIZE)
- ADC的DMA是否开启(CR2.DMA=1)
6.3 中断不触发
排查步骤:
- NVIC是否配置正确
- 中断标志是否清除
- 中断是否使能(CR1.EOCIE/JEOCIE/AWDIE)
- 优先级设置是否冲突
6.4 多通道顺序错误
确保:
- SQR1.L设置正确(N-1)
- SQR3中的SQx顺序正确
- DMA缓冲区大小匹配通道数
7. 实际应用案例
7.1 电池电压监测
c复制// 配置内部参考电压通道(CH17)
ADC1->SQR3 = 17 << 0;
ADC1->SMPR1 = ADC_SMPR1_SMP17_2; // 239.5周期
// 读取并计算VREFINT(典型值1.2V)
uint16_t vrefint = ADC1_Read();
float vdda = 1.2f * 4095 / vrefint;
// 读取电池电压(分压电阻)
uint16_t vbat = ADC1_Read();
float battery_voltage = vbat * vdda / 4095 * 2; // 假设分压比1:1
7.2 温度传感器读取
STM32内部温度传感器连接在通道16:
c复制// 启用温度传感器
ADC1->CR2 |= ADC_CR2_TSVREFE;
// 配置通道16
ADC1->SQR3 = 16 << 0;
ADC1->SMPR1 = ADC_SMPR1_SMP16_2; // 239.5周期
// 读取并计算温度
uint16_t temp = ADC1_Read();
float temperature = ((1.43 - temp*vdda/4095) / 0.0043) + 25;
7.3 多路模拟开关控制
通过GPIO控制模拟开关,实现更多通道扩展:
c复制// 控制CD4051模拟开关
void Select_Channel(uint8_t ch) {
GPIOB->ODR &= ~0x07; // 清低3位
GPIOB->ODR |= (ch & 0x07); // 设置通道
}
// 读取扩展通道
uint16_t Read_Ext_ADC(uint8_t ch) {
Select_Channel(ch);
delay_us(10); // 稳定时间
return ADC1_Read();
}
8. 高级应用技巧
8.1 过采样提高分辨率
通过软件过采样可以将12位ADC提升到14位甚至16位分辨率:
c复制#define OVERSAMPLE 16 // 16倍过采样
uint16_t Oversample_Read(void) {
uint32_t sum = 0;
for(int i=0; i<OVERSAMPLE; i++) {
sum += ADC1_Read();
}
return (sum + OVERSAMPLE/2) / OVERSAMPLE; // 四舍五入
}
8.2 硬件均值滤波
利用ADC的多次转换硬件求平均功能:
c复制// 启用硬件均值(16次平均)
ADC1->CR1 |= ADC_CR1_OVRIE;
ADC1->CR2 |= ADC_CR2_DMA;
ADC1->SQR1 = (15 << 20); // 16次转换
8.3 定时器触发采样
使用定时器自动触发ADC采样,实现精确间隔采集:
c复制// 配置TIM2触发ADC
TIM2->CR2 |= TIM_CR2_MMS_1; // TRGO输出=更新事件
TIM2->PSC = 71; // 1MHz时钟
TIM2->ARR = 999; // 1kHz触发频率
// ADC配置外部触发
ADC1->CR2 |= ADC_CR2_EXTTRIG | ADC_CR2_EXTSEL_2;
8.4 低功耗间歇采样
在低功耗应用中,可以间歇开启ADC:
c复制void LowPower_ADC_Read(void) {
ADC1->CR2 |= ADC_CR2_ADON;
delay_us(10); // 唤醒时间
ADC1->CR2 |= ADC_CR2_SWSTART;
while(!(ADC1->SR & ADC_SR_EOC));
uint16_t val = ADC1->DR;
ADC1->CR2 &= ~ADC_CR2_ADON; // 关闭ADC
}
9. 寄存器操作与库函数对比
9.1 性能对比
- 寄存器操作:代码量小,执行速度快,适合对性能要求高的场景
- 库函数:可读性好,易于维护,适合快速开发
9.2 代码量比较
以ADC初始化为例:
- 寄存器版本:约20行代码
- 标准库版本:约50行代码
- HAL库版本:约80行代码
9.3 适用场景建议
建议使用寄存器操作的场景:
- 对性能要求严格的实时应用
- 资源受限的MCU(Flash/RAM不足)
- 需要精确控制时序的操作
- 学习底层工作原理时
10. 开发调试技巧
10.1 使用逻辑分析仪调试
通过GPIO输出调试信号:
c复制// 在关键位置添加调试引脚操作
GPIOB->BSRR = GPIO_BSRR_BS8; // 置高
ADC1->CR2 |= ADC_CR2_SWSTART;
while(!(ADC1->SR & ADC_SR_EOC));
GPIOB->BSRR = GPIO_BSRR_BR8; // 置低
10.2 利用断点查看寄存器
在调试器中设置数据断点,监控ADC_DR寄存器变化。
10.3 串口打印调试信息
输出ADC配置和采样值:
c复制printf("ADC_CR1: 0x%08X\n", ADC1->CR1);
printf("ADC_CR2: 0x%08X\n", ADC1->CR2);
printf("Value: %d (%.2fV)\n", adc_val, adc_val*3.3f/4095);
10.4 使用示波器验证
测量实际信号与ADC结果的对应关系,检查线性度和噪声。
