使用DMA方式获取多通道ADC数据的完整指南
当使用DMA方式采样多个ADC通道时,理解如何组织和访问数据至关重要。以下是详细的解释和代码示例。
数据存储方式
当使用DMA进行多通道ADC采样时,数据在内存中的存储方式取决于ADC的配置:
- 连续模式:ADC按顺序转换所有通道,DMA按顺序存储结果
- 扫描模式:ADC按预设顺序转换多个通道,DMA按相同顺序存储结果
对于N个通道的采样,DMA缓冲区中的数据排列通常为:
[通道0值, 通道1值, 通道2值, ..., 通道N-1值, 通道0值, 通道1值, ...]
配置步骤
1. 初始化ADC和DMA
首先,需要在CubeMX或代码中正确配置ADC和DMA:
// ADC初始化配置
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; // 启用扫描模式
hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换模式
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.NbrOfDiscConversion = 0;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 7; // 7个通道
hadc1.Init.DMAContinuousRequests = ENABLE; // 启用DMA连续请求
// DMA初始化配置
hdma_adc1.Instance = DMA1_Channel1;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
2. 配置ADC通道和规则序列
// 配置ADC通道和它们在规则序列中的顺序
ADC_ChannelConfTypeDef sConfig = {0};
// 通道0
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
// 通道1
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = ADC_REGULAR_RANK_2;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
// 重复上述过程,配置所有7个通道...
// 通道6
sConfig.Channel = ADC_CHANNEL_6;
sConfig.Rank = ADC_REGULAR_RANK_7;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
3. 定义数据缓冲区并启动DMA传输
#define ADC_CHANNELS 7
#define ADC_SAMPLES 100 // 每个通道采样100次
// 缓冲区大小 = 通道数 × 采样次数
uint16_t adc_buffer[ADC_CHANNELS * ADC_SAMPLES];
// 启动ADC DMA传输
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_CHANNELS * ADC_SAMPLES);
访问特定通道的数据
DMA缓冲区中的数据按照转换顺序存储。要访问特定通道的数据,需要知道它在序列中的位置。
方法1:直接计算索引
// 获取第channel个通道的第sample次采样值
uint16_t get_channel_data(uint16_t* buffer, int channel, int sample) {
// 计算索引:sample * 总通道数 + 通道序号
return buffer[sample * ADC_CHANNELS + channel];
}
// 示例:获取通道3的第50次采样值
uint16_t value = get_channel_data(adc_buffer, 3, 50);
方法2:使用二维数组视角
虽然物理上是一维数组,但可以逻辑上视为二维数组:
// 将一维数组视为二维数组的实用函数
uint16_t get_adc_value(int channel, int sample) {
return adc_buffer[sample * ADC_CHANNELS + channel];
}
// 打印所有通道的最新采样值
void print_latest_samples(void) {
int latest_sample = ...; // 需要跟踪最新样本索引
for (int ch = 0; ch < ADC_CHANNELS; ch++) {
uint16_t value = get_adc_value(ch, latest_sample);
float voltage = value * 3.3f / 4095.0f;
printf("Channel %d: %d (%.2f V)\n", ch, value, voltage);
}
}
方法3:使用结构体数组(更高级的技巧)
如果您希望数据组织更加直观,可以使用结构体:
// 定义包含所有通道数据的结构体
typedef struct {
uint16_t ch0;
uint16_t ch1;
uint16_t ch2;
uint16_t ch3;
uint16_t ch4;
uint16_t ch5;
uint16_t ch6;
} adc_sample_t;
// 将DMA缓冲区重新解释为结构体数组
adc_sample_t* adc_samples = (adc_sample_t*)adc_buffer;
// 现在可以直接访问数据
uint16_t value = adc_samples[50].ch3; // 第50次采样中的通道3值
处理DMA传输完成中断
当使用循环DMA模式时,数据会不断被覆盖。您需要跟踪当前可用的数据位置:
volatile uint32_t dma_current_index = 0;
volatile uint32_t dma_last_index = 0;
// DMA传输完成回调函数
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
// 获取当前DMA位置
dma_current_index = ADC_SAMPLES * ADC_CHANNELS - __HAL_DMA_GET_COUNTER(hadc->DMA_Handle);
// 处理从dma_last_index到dma_current_index之间的数据
process_new_data(dma_last_index, dma_current_index);
// 更新最后索引
dma_last_index = dma_current_index;
// 如果到达缓冲区末尾,回绕到开头
if (dma_last_index >= ADC_SAMPLES * ADC_CHANNELS) {
dma_last_index = 0;
}
}
// 处理新数据的函数
void process_new_data(uint32_t start, uint32_t end) {
for (uint32_t i = start; i < end; i += ADC_CHANNELS) {
// 确保不越界
if (i + ADC_CHANNELS > ADC_SAMPLES * ADC_CHANNELS) {
break;
}
// 处理这一组采样(包含所有通道)
for (int ch = 0; ch < ADC_CHANNELS; ch++) {
uint16_t value = adc_buffer[i + ch];
// 处理每个通道的值...
}
}
}
实用技巧和注意事项
- 缓冲区大小:确保缓冲区足够大,能容纳所有采样数据,避免数据被覆盖前未来得及处理。
- 数据对齐:如果使用结构体方法,确保结构体大小与DMA传输的数据对齐。
- ** volatile关键字**:在中断和主程序之间共享的变量(如索引)应使用volatile关键字。
- 超时处理:添加超时机制,防止因硬件故障导致程序卡死。
- 错误处理:检查ADC和DMA的错误标志,并实现相应的错误处理回调函数。
// ADC错误回调
void HAL_ADC_ErrorCallback(ADC_HandleTypeDef* hadc) {
// 处理ADC错误
printf("ADC error: 0x%08lX\n", hadc->ErrorCode);
}
// DMA错误回调
void HAL_DMA_ErrorCallback(DMA_HandleTypeDef* hdma) {
// 处理DMA错误
printf("DMA error\n");
}
通过以上方法,您可以有效地使用DMA方式采集多通道ADC数据,并轻松访问每个通道的采样值。这种方法大大提高了数据采集效率,减少了CPU开销。