在嵌入式开发中,ADC(模数转换器)采集模拟信号是常见需求。当我们需要同时采集多路传感器数据时,如何高效读取ADC值就成了关键问题。我在实际项目中尝试过两种主流方案:传统的轮询模式和使用DMA的直接存储器访问模式。
轮询模式就像是你亲自去超市买东西,每次都要问售货员"有货了吗?",直到拿到商品为止。代码实现上就是通过HAL_ADC_Start()启动转换后,不断调用HAL_ADC_PollForConversion()检查转换是否完成。这种方式简单直接,但CPU需要一直等待转换完成,效率较低。
DMA模式则像是雇了个跑腿小哥,你只需要告诉他"去超市帮我买这些东西",然后就可以去做其他事情了。DMA控制器会自动把ADC转换结果搬运到指定内存位置,完全不需要CPU参与。实测下来,这种方式的效率提升非常明显。
我使用的是STM32F407开发板,需要采集4路模拟信号。在CubeMX中配置ADC时,需要注意以下几点:
关键配置代码如下:
c复制static void MX_ADC1_Init(void)
{
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 4;
hadc1.Init.DMAContinuousRequests = ENABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
}
在FreeRTOS环境下,我们需要创建一个专门的数据采集任务。这里我使用了CMSIS_V2接口:
c复制osThreadAttr_t adcTask_attributes = {
.name = "ADCTask",
.stack_size = 256 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
void StartADCTask(void *argument)
{
MX_ADC1_Init();
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer, ADC_BUFFER_SIZE);
for(;;)
{
// 处理采集到的数据
ProcessADCData();
osDelay(10);
}
}
轮询模式的实现相对简单,但需要注意时序控制。我在项目中是这样实现的:
c复制void PollingADCRead(void)
{
uint16_t adcValues[4];
while(1)
{
for(int i=0; i<4; i++)
{
HAL_ADC_Start(&hadc1);
if(HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
{
adcValues[i] = HAL_ADC_GetValue(&hadc1);
}
}
// 数据处理
ProcessData(adcValues);
osDelay(1);
}
}
我使用逻辑分析仪测量了两种模式下的性能差异:
| 指标 | 轮询模式 | DMA模式 |
|---|---|---|
| 单次采集耗时(us) | 45 | 12 |
| CPU占用率(%) | 38 | 5 |
| 任务切换延迟(us) | 15 | 3 |
从数据可以看出,轮询模式虽然实现简单,但CPU占用率明显偏高。特别是在需要高频采集的场景下,这种模式会导致系统响应变慢。
DMA模式的配置稍微复杂一些,但带来的性能提升非常值得。这是我的DMA配置代码:
c复制static void MX_DMA_Init(void)
{
__HAL_RCC_DMA2_CLK_ENABLE();
hdma_adc1.Instance = DMA2_Stream0;
hdma_adc1.Init.Channel = DMA_CHANNEL_0;
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;
hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_adc1);
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
}
DMA模式下,数据是自动更新的,我们需要合理设计缓冲区。我采用了双缓冲技术:
c复制#define ADC_BUF_SIZE 16
uint16_t adcBuffer[ADC_BUF_SIZE];
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
// 转换完成回调
ProcessADCData(adcBuffer);
}
void StartADCTask(void *argument)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer, ADC_BUF_SIZE/4);
for(;;)
{
osDelay(100);
}
}
经过多次项目实践,我总结出一些选择ADC采集模式的经验:
在FreeRTOS环境下,还需要注意:
我在一个工业传感器项目中,将采集方式从轮询改为DMA后,系统稳定性明显提升,CPU负载从70%降到了20%左右。这个改进让系统有余力处理更多的通信和显示任务。