在嵌入式开发中,ADC(模数转换器)是最基础也最关键的模块之一。无论是电源监控、传感器数据采集还是信号处理,都离不开ADC的精准测量。而GD32F303作为国产32位MCU的优秀代表,其内置的12位ADC模块配合DMA(直接内存访问)功能,能够实现高效、稳定的多通道数据采集。本文将带你从零开始,通过一个完整的"三通道电压表"项目,深入理解ADC+DMA的实战应用。
这个项目不仅能同时监测三个不同电压点(比如电源电压、传感器输出和分压信号),还会通过串口实时显示测量结果。更重要的是,我们会探讨如何将ADC配置、DMA传输、定时器触发以及数据处理逻辑有机整合,形成一个完整的解决方案。文章最后会提供完整的Keil工程文件,你可以直接移植到自己的项目中。
在开始写代码之前,我们需要先理清整个系统的架构。一个好的架构设计能让后续开发事半功倍,也便于维护和扩展。
我们的三通道电压表需要测量三个独立的电压信号。以GD32F303CCT6为例,我们可以选择以下引脚配置:
注意:实际应用中,输入电压不能超过MCU的工作电压(通常3.3V)。如果测量更高电压,必须使用分压电路。
整个系统的软件流程可以分为以下几个关键部分:
初始化阶段:
运行阶段:
数据处理:
这种架构的优势在于:
ADC和DMA的协同工作是本项目的核心。正确配置这两者,才能确保数据采集的稳定性和准确性。
GD32F303的ADC支持多达16个外部通道,在我们的项目中,我们需要配置ADC0的三个通道。以下是关键配置步骤:
c复制// 1. 使能ADC时钟
rcu_periph_clock_enable(RCU_ADC0);
rcu_adc_clock_config(RCU_CKADC_CKAHB_DIV5); // AHB=120MHz/5=24MHz
// 2. 配置ADC工作模式
adc_mode_config(ADC_MODE_FREE); // 独立模式
// 3. 设置ADC分辨率和数据对齐
adc_resolution_config(ADC0, ADC_RESOLUTION_12B); // 12位精度
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); // 右对齐
// 4. 配置扫描模式和触发方式
adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); // 启用扫描模式
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, DISABLE); // 禁用连续转换
// 5. 设置通道数量和采样时间
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 3); // 3个规则通道
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_6, ADC_SAMPLETIME_13POINT5);
adc_regular_channel_config(ADC0, 1, ADC_CHANNEL_7, ADC_SAMPLETIME_13POINT5);
adc_regular_channel_config(ADC0, 2, ADC_CHANNEL_8, ADC_SAMPLETIME_13POINT5);
// 6. 配置外部触发
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE);
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_T1_CH0);
// 7. 校准ADC
delay_ms(1);
adc_calibration_enable(ADC0);
// 8. 使能ADC
adc_enable(ADC0);
几个关键点需要注意:
DMA的配置需要与ADC完美配合,确保每个通道的数据都能被正确搬运到内存中。以下是DMA的配置代码:
c复制// 定义用于存储ADC结果的数组
uint16_t adc_values[3] = {0};
// 1. 使能DMA时钟
rcu_periph_clock_enable(RCU_DMA0);
// 2. 配置DMA通道
dma_parameter_struct dma_init_struct;
dma_struct_para_init(&dma_init_struct);
dma_init_struct.periph_addr = (uint32_t)&ADC_RDATA(ADC0); // 外设地址
dma_init_struct.memory_addr = (uint32_t)adc_values; // 内存地址
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; // 传输方向
dma_init_struct.number = 3; // 传输数量
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 外设地址不递增
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 内存地址递增
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT;
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT;
dma_init_struct.priority = DMA_PRIORITY_HIGH;
dma_init(DMA0, DMA_CH0, &dma_init_struct);
// 3. 使能DMA通道
dma_channel_enable(DMA0, DMA_CH0);
// 4. ADC中使能DMA
adc_dma_mode_enable(ADC0);
DMA配置中的几个关键参数:
为了实现固定频率的采样,我们使用定时器来触发ADC转换。这种方法比软件触发更精确,也能减轻CPU负担。
我们使用定时器1的通道0作为ADC的触发源。假设我们需要100Hz的采样率(每10ms采样一次),配置如下:
c复制// 1. 使能定时器时钟
rcu_periph_clock_enable(RCU_TIMER1);
// 2. 初始化定时器
timer_parameter_struct timer_init_struct;
timer_struct_para_init(&timer_init_struct);
timer_init_struct.prescaler = 120 - 1; // 分频系数
timer_init_struct.alignedmode = TIMER_COUNTER_EDGE;
timer_init_struct.counterdirection = TIMER_COUNTER_UP;
timer_init_struct.period = 10000 - 1; // 自动重装载值
timer_init_struct.clockdivision = TIMER_CKDIV_DIV1;
timer_init(TIMER1, &timer_init_struct);
// 3. 配置通道0为PWM模式
timer_oc_parameter_struct timer_ocinit_struct;
timer_channel_output_struct_para_init(&timer_ocinit_struct);
timer_ocinit_struct.outputstate = TIMER_CCX_ENABLE;
timer_ocinit_struct.ocpolarity = TIMER_OC_POLARITY_HIGH;
timer_ocinit_struct.ocidlestate = TIMER_OC_IDLE_STATE_LOW;
timer_channel_output_config(TIMER1, TIMER_CH_0, &timer_ocinit_struct);
// 4. 设置PWM占空比(50%)
timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, 5000);
timer_channel_output_mode_config(TIMER1, TIMER_CH_0, TIMER_OC_MODE_PWM0);
timer_channel_output_shadow_config(TIMER1, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE);
// 5. 使能定时器
timer_enable(TIMER1);
计算说明:
采样率的选择需要考虑多方面因素:
| 应用场景 | 推荐采样率 | 考虑因素 |
|---|---|---|
| 电源监控 | 10-100Hz | 电源波动通常较慢 |
| 温度传感器 | 1-10Hz | 温度变化缓慢 |
| 音频信号 | 8kHz以上 | 满足奈奎斯特采样定理 |
| 振动检测 | 1kHz以上 | 捕捉高频振动 |
在我们的三通道电压表中,100Hz的采样率对于大多数应用已经足够。如果需要更高精度,可以调整定时器的分频和重装载值。
获取到ADC原始值后,我们需要将其转换为实际电压值,并进行适当的滤波处理以提高测量精度。
ADC转换公式如下:
code复制电压 = (ADC原始值 × 参考电压) / (2^分辨率 - 1)
对于GD32F303:
实现代码:
c复制float adc_to_voltage(uint16_t adc_value)
{
const float VREF = 3.3f; // 参考电压
return (adc_value * VREF) / 4095.0f;
}
如果使用分压电路测量更高电压,还需要考虑分压比:
c复制float adc_to_voltage_with_divider(uint16_t adc_value, float r1, float r2)
{
float v_adc = adc_to_voltage(adc_value);
return v_adc * (r1 + r2) / r2; // 分压公式
}
ADC测量中难免会有噪声,我们可以通过软件滤波来提高稳定性。以下是几种常用滤波方法的对比:
| 滤波方法 | 实现复杂度 | 内存需求 | 延迟 | 效果 |
|---|---|---|---|---|
| 均值滤波 | 低 | 中 | 高 | 一般 |
| 滑动平均 | 中 | 中 | 中 | 较好 |
| 中值滤波 | 高 | 高 | 高 | 抗脉冲干扰 |
| 一阶滞后 | 低 | 低 | 低 | 一般 |
推荐使用滑动平均滤波,实现代码如下:
c复制#define FILTER_WINDOW_SIZE 8
typedef struct {
float buffer[FILTER_WINDOW_SIZE];
uint8_t index;
float sum;
} sliding_filter_t;
float sliding_filter(sliding_filter_t* filter, float new_value)
{
// 减去最旧的值
filter->sum -= filter->buffer[filter->index];
// 添加新值
filter->buffer[filter->index] = new_value;
filter->sum += new_value;
// 更新索引
filter->index = (filter->index + 1) % FILTER_WINDOW_SIZE;
// 返回平均值
return filter->sum / FILTER_WINDOW_SIZE;
}
初始化滤波器:
c复制sliding_filter_t channel_filters[3];
void init_filters(void)
{
for(int i = 0; i < 3; i++) {
memset(&channel_filters[i], 0, sizeof(sliding_filter_t));
for(int j = 0; j < FILTER_WINDOW_SIZE; j++) {
channel_filters[i].buffer[j] = 0.0f;
}
}
}
使用滤波器:
c复制void process_adc_values(void)
{
float voltages[3];
for(int i = 0; i < 3; i++) {
voltages[i] = adc_to_voltage(adc_values[i]);
voltages[i] = sliding_filter(&channel_filters[i], voltages[i]);
}
// 后续处理...
}
现在我们已经完成了各个模块的开发,接下来需要将它们整合成一个完整的系统,并考虑一些工程优化。
主程序的逻辑应该尽量简洁,主要工作由中断和DMA完成:
c复制int main(void)
{
// 硬件初始化
gpio_init();
uart_init(115200); // 串口初始化
adc_dma_init();
timer_init();
// 滤波器初始化
init_filters();
// 主循环
while(1) {
// 每100ms发送一次数据
delay_ms(100);
send_voltage_data();
// 其他任务...
}
}
通过串口发送电压数据,可以使用JSON格式便于上位机解析:
c复制void send_voltage_data(void)
{
float voltages[3];
get_filtered_voltages(voltages); // 获取滤波后的电压值
printf("{\"ch0\":%.2f,\"ch1\":%.2f,\"ch2\":%.2f}\r\n",
voltages[0], voltages[1], voltages[2]);
}
如果需要OLED显示,可以添加如下代码:
c复制void update_oled_display(float voltages[3])
{
oled_clear();
oled_printf(0, 0, "CH0: %.2fV", voltages[0]);
oled_printf(0, 2, "CH1: %.2fV", voltages[1]);
oled_printf(0, 4, "CH2: %.2fV", voltages[2]);
// 绘制简单的电压条
for(int i = 0; i < 3; i++) {
uint8_t length = (uint8_t)(voltages[i] / 3.3f * 128);
oled_draw_hline(0, 16 + i*10, length);
}
oled_refresh();
}
一个好的工程结构能大大提高代码的可维护性。推荐按如下方式组织文件:
code复制/Project
|-- /CMSIS // 内核支持文件
|-- /Firmware
| |-- /Drivers // 外设驱动
| | |-- adc_dma.c // ADC和DMA配置
| | |-- timer.c // 定时器配置
| | |-- uart.c // 串口通信
| |-- /Filters // 滤波算法
| | |-- sliding_avg.c
| |-- /Utils // 工具函数
|-- /Inc // 头文件
|-- /Middlewares // 中间件
|-- /User
| |-- main.c // 主程序
关键头文件示例(adc_dma.h):
c复制#ifndef __ADC_DMA_H__
#define __ADC_DMA_H__
#include "gd32f30x.h"
#define ADC_CHANNEL_COUNT 3
extern uint16_t adc_values[ADC_CHANNEL_COUNT];
void adc_dma_init(void);
float adc_to_voltage(uint16_t adc_value);
#endif /* __ADC_DMA_H__ */
在实际开发中,ADC应用可能会遇到各种问题。下面分享一些常见问题及解决方法。
可能原因及解决方案:
电源噪声:
信号源阻抗过高:
接地问题:
PCB布局问题:
调试步骤:
排查方法:
完成基本功能后,我们可以考虑进一步优化和扩展系统功能。
对于电池供电的应用,可以采取以下措施降低功耗:
示例代码:
c复制void enter_low_power_mode(void)
{
// 配置唤醒源(如定时器中断)
exti_interrupt_flag_clear(EXTI_0);
exti_init(EXTI_0, EXTI_INTERRUPT, EXTI_TRIG_RISING);
// 进入停止模式
pmu_to_stopmode(WFI_CMD);
// 唤醒后恢复时钟配置
system_clock_config();
}
GD32F303有多个ADC模块,可以配置它们协同工作:
配置示例(ADC同步模式):
c复制// 配置ADC同步模式
adc_mode_config(ADC_DAUL_REGULAL_PARALLEL); // 规则组并行模式
// 配置主从ADC
adc_master_slave_mode_config(ADC0, ADC_MASTER);
adc_master_slave_mode_config(ADC1, ADC_SLAVE);
可以开发简单的上位机程序,实现:
Python示例(使用PySerial和Matplotlib):
python复制import serial
import matplotlib.pyplot as plt
import json
ser = serial.Serial('COM3', 115200, timeout=1)
plt.ion()
fig, ax = plt.subplots()
x, y1, y2, y3 = [], [], [], []
while True:
try:
line = ser.readline().decode().strip()
data = json.loads(line)
x.append(len(x))
y1.append(data['ch0'])
y2.append(data['ch1'])
y3.append(data['ch2'])
ax.clear()
ax.plot(x, y1, label='CH0')
ax.plot(x, y2, label='CH1')
ax.plot(x, y3, label='CH2')
ax.legend()
plt.pause(0.01)
except (json.JSONDecodeError, KeyError):
continue