4. 微控制器(MCU)数据采集:STM32/GD32 GPIO与ADC初始化、DMA传输配置、定时器触发采样、数据缓存策略
好,咱们进入正题。这一章讲的是MCU怎么把传感器的模拟信号变成数字量,再存起来。说白了,就是让芯片听话,把数据老老实实搬进内存。
我个人习惯把这一块分成四个步骤:GPIO和ADC初始化、DMA传输配置、定时器触发采样、数据缓存策略。每一步都有坑,咱们一个一个说。
4.1 GPIO与ADC初始化——别让引脚闲着
先看GPIO。ADC的输入引脚必须配置成模拟模式。我见过有人把引脚配成推挽输出,结果ADC读数一直飘——嗯,那相当于把输出缓冲器接在了采样电容上,能准才怪。
以STM32F103为例,PA0作为ADC1的通道0:
// 使能GPIOA和ADC1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
// 配置PA0为模拟输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式
GPIO_Init(GPIOA, &GPIO_InitStructure);
// ADC1初始化
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道,不扫描
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 不连续转换(由定时器触发)
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2; // 定时器2的CC2触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; // 通道数
ADC_Init(ADC1, &ADC_InitStructure);
// 配置ADC通道,采样时间尽量长一点
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
// 使能ADC
ADC_Cmd(ADC1, ENABLE);
// 校准
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
4.2 DMA传输配置——让CPU歇着
ADC转换完的数据,如果让CPU一个个去读,那CPU就啥也别干了。DMA就是干这个的——数据从ADC的数据寄存器直接搬到内存,CPU该干嘛干嘛。
配置DMA时要注意:传输方向是外设到内存,外设地址固定(ADC1->DR),内存地址递增(数组)。
#define ADC_BUFFER_SIZE 100
uint16_t adc_buffer[ADC_BUFFER_SIZE];
void DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA1_Channel1);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); // ADC数据寄存器地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buffer; // 内存缓冲区
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设到内存
DMA_InitStructure.DMA_BufferSize = ADC_BUFFER_SIZE; // 传输次数
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
// 使能DMA
DMA_Cmd(DMA1_Channel1, ENABLE);
}
4.3 定时器触发采样——精准的时间控制
压传感器测量,采样间隔必须稳定。用软件延时?不靠谱。用定时器触发ADC,才是正经做法。
定时器产生PWM或更新事件,通过内部触发信号直接启动ADC转换。这样采样间隔由硬件保证,精度到微秒级。
// 定时器2配置,产生100kHz的触发信号(10us一次)
void TIM2_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseStructure.TIM_Period = 719; // 72MHz / (720) = 100kHz
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 选择输出比较通道2作为触发源
TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
TIM_Cmd(TIM2, ENABLE);
}
然后在ADC初始化里,把触发源设为ADC_ExternalTrigConv_T2_CC2。这样定时器每更新一次,ADC就自动转换一次,DMA自动搬走数据。
4.4 数据缓存策略——别让数据打架
DMA一直在往缓冲区写数据,主循环或中断在读取数据。如果读写指针撞上了,数据就乱了。
我常用的策略是双缓冲(也叫乒乓缓冲)。两个缓冲区,DMA写一个,CPU读另一个,满了就交换。
#define HALF_BUFFER_SIZE 50
uint16_t adc_buffer[ADC_BUFFER_SIZE]; // 总大小100,分成两个半区
// 在DMA半传输中断和传输完成中断中切换标志
volatile uint8_t buffer_ready = 0;
void DMA1_Channel1_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC1)) // 传输完成(后半区写满)
{
buffer_ready = 2; // 后半区数据就绪
DMA_ClearITPendingBit(DMA1_IT_TC1);
}
if(DMA_GetITStatus(DMA1_IT_HT1)) // 半传输完成(前半区写满)
{
buffer_ready = 1; // 前半区数据就绪
DMA_ClearITPendingBit(DMA1_IT_HT1);
}
}
// 主循环中处理
void ProcessADCData(void)
{
uint16_t *pData;
uint16_t len;
if(buffer_ready == 1)
{
pData = adc_buffer; // 前半区
len = HALF_BUFFER_SIZE;
buffer_ready = 0;
// 处理数据...
}
else if(buffer_ready == 2)
{
pData = adc_buffer + HALF_BUFFER_SIZE; // 后半区
len = HALF_BUFFER_SIZE;
buffer_ready = 0;
// 处理数据...
}
}
4.5 整体流程与知识体系
下面这张图,把整个数据采集链路串起来了。你对照着看,思路会清晰很多。
4.6 常见问题与排查思路
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| ADC读数全为0 | GPIO未配成模拟模式;ADC未使能 | 检查GPIO_Mode;检查ADC_Cmd |
| 数据跳动大 | 采样时间太短;电源噪声 | 增大采样时间;加去耦电容 |
| DMA只传一次就停 | DMA模式未设Circular | 设置DMA_Mode_Circular |
| 数据错位 | 缓冲区读写冲突 | 改用双缓冲策略 |
嗯,这一章的内容就这些。GPIO和ADC初始化是基础,DMA让传输自动化,定时器保证采样节奏,双缓冲解决数据冲突。把这四步走通了,数据采集的骨架就搭起来了。
实际项目中,你可能还会遇到采样率不够、DMA中断响应慢等问题。到时候再根据具体场景调参数就好。核心思路不变——硬件能干的,别让软件干;中断里能少干的,别多干。
公众号:蓝海资料掘金营,微信deep3321