在嵌入式开发中,模拟信号采集是个永恒的话题。想象一下,你正在设计一个智能温室系统,需要同时监测土壤湿度、环境温度和光照强度。这些传感器输出的都是连续变化的电压信号,而GD32F103的ADC(模数转换器)就是把这些模拟信号转换成数字值的桥梁。
但问题来了——如果让CPU亲自处理每个通道的ADC数据,就像让厨师亲自去菜市场采购食材一样低效。这时候DMA(直接存储器访问)就派上用场了。它就像个勤快的助手,能自动把ADC转换结果搬运到指定内存区域,完全不需要CPU插手。我在去年做的工业传感器项目中,正是靠这个组合实现了8路信号的同时采集,CPU占用率始终低于5%。
ADC和DMA的配合还有个隐藏优势:抗干扰性。传统轮询方式采集多通道时,通道切换会导致采样间隔不均匀。而DMA的连续搬运就像流水线作业,能保持严格的等间隔采样,这对需要做FFT分析的场合特别重要。记得有次调试电机振动传感器,改用DMA后频谱图上的杂波立刻减少了30%。
GD32的时钟配置比STM32更"敏感",这是我的血泪教训。有次ADC读数总是不准,折腾半天发现是APB2时钟超频了。官方手册明确写着ADC模块最大时钟不能超过14MHz,但容易被忽略的是,这个限制还包括了ADC采样保持时间的等效时钟。
推荐这样配置(以72MHz系统时钟为例):
c复制rcu_pll_config(RCU_PLLSRC_HXTAL, RCU_PLL_MUL9); // 8MHz晶振×9=72MHz
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV6); // 72MHz/6=12MHz
特别注意:GD32F103的ADC时钟源只能来自APB2,这点和STM32F103不同。如果发现ADC根本不起作用,先检查rcu_periph_clock_enable(RCU_ADC0)是否调用。
配置ADC通道引脚时,很多新手会掉进这个坑:
c复制gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_4);
看起来没问题对吧?但实际上GD32的GPIO速度配置会影响ADC输入阻抗。在信号源阻抗较高时(如>10kΩ),建议改用低速模式:
c复制gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_10MHZ, GPIO_PIN_4);
实测发现,对于MQ-135这类气体传感器,低速模式能使读数稳定性提升20%以上。
缓冲区大小不是随便定的,要考虑两个黄金法则:
我的常用配置模板:
c复制#define CHANNELS 3 // 3路信号
#define DEPTH 16 // 每通道16次采样
uint32_t adc_buffer[CHANNELS][DEPTH];
曾有个智能电表项目,最初用DEPTH=8导致谐波分析不准,改成16后立即得到完美波形。这就是深度学习的价值——字面意义上的"深度"。
配置DMA时有个致命细节:GD32的DMA外设地址必须用&ADC_RDATA(ADCx),而不是STM32常用的&ADCx->DR。这是我调试到凌晨3点才发现的差异。
完整配置示例:
c复制dma_parameter_struct dma_cfg = {
.direction = DMA_PERIPHERAL_TO_MEMORY,
.memory_addr = (uint32_t)adc_buffer,
.memory_inc = DMA_MEMORY_INCREASE_ENABLE,
.memory_width = DMA_MEMORY_WIDTH_32BIT,
.number = CHANNELS * DEPTH,
.periph_addr = (uint32_t)&ADC_RDATA(ADC0), // 关键点!
.periph_inc = DMA_PERIPH_INCREASE_DISABLE,
.periph_width = DMA_PERIPHERAL_WIDTH_32BIT,
.priority = DMA_PRIORITY_HIGH // 实时性要求高时用HIGH
};
dma_circulation_enable(DMA0, DMA_CH0); // 循环模式必须开启
GD32的ADC采样时间配置比STM32更精细,有7个可选档位。对于高阻抗信号源(如PT100热电阻),需要更长的采样时间:
c复制adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_4, ADC_SAMPLETIME_239POINT5);
但要注意,采样时间越长,最大采样率就越低。有个很实用的公式:
code复制最大采样率 = 1 / (采样时间 + 转换时间)
其中GD32F103的转换时间固定为12.5个ADC时钟周期。假设采样时间配置为71.5周期,ADC时钟12MHz,则:
code复制最大采样率 = 12MHz / (71.5 + 12.5) ≈ 142.8kHz
但在多通道模式下,这个速率还要除以通道数。
采集到数据后,如何在主循环中高效处理?分享我的三板斧:
c复制uint32_t adc_buf[2][CHANNELS][DEPTH];
volatile uint8_t active_buf = 0;
// DMA中断中
void DMA0_Channel0_IRQHandler(void) {
if(dma_interrupt_flag_get(DMA0, DMA_CH0, DMA_INT_FLAG_FTF)) {
active_buf ^= 1; // 切换缓冲区
dma_interrupt_flag_clear(DMA0, DMA_CH0, DMA_INT_FLAG_FTF);
}
}
c复制uint16_t smooth_adc(uint32_t *buf, uint8_t depth) {
uint32_t sum = 0;
for(uint8_t i=0; i<depth; i++) {
sum += buf[i] >> 4; // 12位ADC右移4位
}
return sum / depth;
}
c复制#define ADC_REF 3300 // 3.3V参考电压,单位mV
int check_sensor(uint16_t adc_val) {
int voltage = (adc_val * ADC_REF) / 4095;
if(voltage < 100 || voltage > 3200) { // 有效范围0.1V-3.2V
return -1; // 传感器异常
}
return voltage;
}
有次发现通道数据全部错位,原来是DMA配置漏了这一行:
c复制dma_init_ADC0.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
没有开启内存地址自增,DMA就会把所有通道数据都塞到第一个内存位置。这种bug最可怕的是编译不会报错,运行时数据看起来也"合理"。
GD32F103的VDDA和VREF+必须接3.3V,这点和STM32不同。有次为了省事直接悬空VREF+,结果ADC读数随供电电压波动。后来用TL431做了个精准3.3V基准,读数稳定性立即提升一个数量级。
DMA和ADC中断优先级配置不当会导致数据丢失。我的经验法则是:
c复制nvic_irq_enable(DMA0_Channel0_IRQn, 1, 0); // 优先级1
nvic_irq_enable(ADC0_1_IRQn, 2, 0); // 优先级2
工业现场遇到的奇葩问题:电机启动时ADC读数跳变。最终解决方案:
c复制uint16_t median_filter(uint32_t *buf, uint8_t depth) {
uint16_t temp[DEPTH];
for(uint8_t i=0; i<depth; i++) {
temp[i] = buf[i] >> 4;
}
bubble_sort(temp, depth); // 简单的冒泡排序
return temp[depth/2];
}
记得有次去客户现场调试,发现他们的变频器会导致ADC采集到30kHz的干扰信号。后来用示波器抓包发现是电源耦合干扰,在开发板电源入口处加了个π型滤波器(100μF+10Ω+100nF)才解决问题。这告诉我们:硬件设计不过关,软件再优秀也白搭。