keer_zu 发表于 2025-8-27 01:14

使用DMA方式获取多通道ADC数据的完整指南

<h1>使用DMA方式获取多通道ADC数据的完整指南</h1>
<p>当使用DMA方式采样多个ADC通道时,理解如何组织和访问数据至关重要。以下是详细的解释和代码示例。</p>
<h2>数据存储方式</h2>
<p>当使用DMA进行多通道ADC采样时,数据在内存中的存储方式取决于ADC的配置:</p>
<ol>
<li><strong>连续模式</strong>:ADC按顺序转换所有通道,DMA按顺序存储结果</li>
<li><strong>扫描模式</strong>:ADC按预设顺序转换多个通道,DMA按相同顺序存储结果</li>
</ol>
<p>对于N个通道的采样,DMA缓冲区中的数据排列通常为:</p>
<pre><code>[通道0值, 通道1值, 通道2值, ..., 通道N-1值, 通道0值, 通道1值, ...]
</code></pre>
<h2>配置步骤</h2>
<h3>1. 初始化ADC和DMA</h3>
<p>首先,需要在CubeMX或代码中正确配置ADC和DMA:</p>
<pre><code class="language-c">// 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;
</code></pre>
<h3>2. 配置ADC通道和规则序列</h3>
<pre><code class="language-c">// 配置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(&amp;hadc1, &amp;sConfig);

// 通道1
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = ADC_REGULAR_RANK_2;
HAL_ADC_ConfigChannel(&amp;hadc1, &amp;sConfig);

// 重复上述过程,配置所有7个通道...
// 通道6
sConfig.Channel = ADC_CHANNEL_6;
sConfig.Rank = ADC_REGULAR_RANK_7;
HAL_ADC_ConfigChannel(&amp;hadc1, &amp;sConfig);
</code></pre>
<h3>3. 定义数据缓冲区并启动DMA传输</h3>
<pre><code class="language-c">#define ADC_CHANNELS 7
#define ADC_SAMPLES 100 // 每个通道采样100次

// 缓冲区大小 = 通道数 × 采样次数
uint16_t adc_buffer;

// 启动ADC DMA传输
HAL_ADC_Start_DMA(&amp;hadc1, (uint32_t*)adc_buffer, ADC_CHANNELS * ADC_SAMPLES);
</code></pre>
<h2>访问特定通道的数据</h2>
<p>DMA缓冲区中的数据按照转换顺序存储。要访问特定通道的数据,需要知道它在序列中的位置。</p>
<h3>方法1:直接计算索引</h3>
<pre><code class="language-c">// 获取第channel个通道的第sample次采样值
uint16_t get_channel_data(uint16_t* buffer, int channel, int sample) {
    // 计算索引:sample * 总通道数 + 通道序号
    return buffer;
}

// 示例:获取通道3的第50次采样值
uint16_t value = get_channel_data(adc_buffer, 3, 50);
</code></pre>
<h3>方法2:使用二维数组视角</h3>
<p>虽然物理上是一维数组,但可以逻辑上视为二维数组:</p>
<pre><code class="language-c">// 将一维数组视为二维数组的实用函数
uint16_t get_adc_value(int channel, int sample) {
    return adc_buffer;
}

// 打印所有通道的最新采样值
void print_latest_samples(void) {
    int latest_sample = ...; // 需要跟踪最新样本索引

    for (int ch = 0; ch &lt; ADC_CHANNELS; ch++) {
      uint16_t value = get_adc_value(ch, latest_sample);
      float voltage = value * 3.3f / 4095.0f;
      printf(&quot;Channel %d: %d (%.2f V)\n&quot;, ch, value, voltage);
    }
}
</code></pre>
<h3>方法3:使用结构体数组(更高级的技巧)</h3>
<p>如果您希望数据组织更加直观,可以使用结构体:</p>
<pre><code class="language-c">// 定义包含所有通道数据的结构体
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.ch3; // 第50次采样中的通道3值
</code></pre>
<h2>处理DMA传输完成中断</h2>
<p>当使用循环DMA模式时,数据会不断被覆盖。您需要跟踪当前可用的数据位置:</p>
<pre><code class="language-c">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-&gt;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 &gt;= ADC_SAMPLES * ADC_CHANNELS) {
      dma_last_index = 0;
    }
}

// 处理新数据的函数
void process_new_data(uint32_t start, uint32_t end) {
    for (uint32_t i = start; i &lt; end; i += ADC_CHANNELS) {
      // 确保不越界
      if (i + ADC_CHANNELS &gt; ADC_SAMPLES * ADC_CHANNELS) {
            break;
      }
      
      // 处理这一组采样(包含所有通道)
      for (int ch = 0; ch &lt; ADC_CHANNELS; ch++) {
            uint16_t value = adc_buffer;
            // 处理每个通道的值...
      }
    }
}
</code></pre>
<h2>实用技巧和注意事项</h2>
<ol>
<li><strong>缓冲区大小</strong>:确保缓冲区足够大,能容纳所有采样数据,避免数据被覆盖前未来得及处理。</li>
<li><strong>数据对齐</strong>:如果使用结构体方法,确保结构体大小与DMA传输的数据对齐。</li>
<li>** volatile关键字**:在中断和主程序之间共享的变量(如索引)应使用volatile关键字。</li>
<li><strong>超时处理</strong>:添加超时机制,防止因硬件故障导致程序卡死。</li>
<li><strong>错误处理</strong>:检查ADC和DMA的错误标志,并实现相应的错误处理回调函数。</li>
</ol>
<pre><code class="language-c">// ADC错误回调
void HAL_ADC_ErrorCallback(ADC_HandleTypeDef* hadc) {
    // 处理ADC错误
    printf(&quot;ADC error: 0x%08lX\n&quot;, hadc-&gt;ErrorCode);
}

// DMA错误回调
void HAL_DMA_ErrorCallback(DMA_HandleTypeDef* hdma) {
    // 处理DMA错误
    printf(&quot;DMA error\n&quot;);
}
</code></pre>
<p>通过以上方法,您可以有效地使用DMA方式采集多通道ADC数据,并轻松访问每个通道的采样值。这种方法大大提高了数据采集效率,减少了CPU开销。</p>
页: [1]
查看完整版本: 使用DMA方式获取多通道ADC数据的完整指南