4、STM32微控制器基础:选型、GPIO、定时器中断与DMA

做嵌入式这么多年,我接触过不少MCU平台。但要说最顺手、生态最完善的,STM32绝对排在前列。这一章咱们就聊聊STM32的几个核心基础——选型、GPIO、定时器中断和DMA。这些是压传感器数据采集系统的地基,地基打不牢,后面再漂亮的算法也跑不稳。

4.1 STM32系列选型:别盲目追高,够用就好

很多新手问我:「选STM32是不是越贵越好?」其实不是。我见过有人用F407做LED闪烁,纯粹浪费。选型要看三点:性能、外设、功耗。

咱们压传感器项目,核心需求是:

  • 至少1路ADC(12位以上)
  • 1个定时器产生采样触发
  • 支持DMA传输
  • I2C或SPI接口(传感器通信)
  • 成本敏感

我个人习惯,这种场景首选STM32F103C8T6(蓝色 pill板)。72MHz主频,20KB RAM,64KB Flash,够用。如果你需要更低功耗,可以考虑STM32L0系列。但记住——选型时留20%的余量,别把资源用满。

我的经验:有一次项目选了STM32F030,结果后期要加个FFT运算,RAM不够,只能换芯片。重新画板、重新调试,折腾了两周。所以,前期多花10分钟评估,后期省10天。

4.2 GPIO配置:别小看这最简单的外设

GPIO看似简单,但坑不少。我刚开始做项目时,就因为在GPIO配置上翻过车——输出模式选错了,导致LED死活不亮。

STM32的GPIO有8种模式,咱们常用的就几种:

模式 用途 说明
推挽输出 驱动LED、蜂鸣器 能输出高低电平,驱动能力强
开漏输出 I2C总线 需要外部上拉电阻
浮空输入 按键检测 电平不确定,需外部上拉/下拉
上拉/下拉输入 按键检测(省外部电阻) 内部有弱上拉/下拉
模拟输入 ADC采样 必须配置为模拟模式

配置代码其实很简单,但要注意顺序:先使能时钟,再配置模式。我见过有人把顺序搞反,结果GPIO死活不工作。

// 以PA0为例,配置为推挽输出
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  // 先开时钟

GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;   // 推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;  // 速度选50MHz
GPIO_Init(GPIOA, &GPIO_InitStruct);

小技巧:GPIO速度别选太高。50MHz够用就别选100MHz,否则EMI干扰会变大。我在做传感器采集时,就因为这个吃了亏——ADC数据跳得厉害,最后发现是GPIO速度太高导致的串扰。

4.3 定时器中断:精准采样的心脏

压传感器数据采集,最关键的就是定时采样。你不能用delay(),那会卡死CPU。正确做法是用定时器中断——每隔固定时间触发一次中断,在中断里启动ADC采样。

STM32的定时器很强大。咱们用最基本的通用定时器TIM3举例:

// 配置TIM3,产生1ms中断
void TIM3_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    
    TIM_TimeBaseInitTypeDef TIM_InitStruct;
    TIM_InitStruct.TIM_Prescaler = 72 - 1;      // 72MHz / 72 = 1MHz
    TIM_InitStruct.TIM_Period = 1000 - 1;       // 1MHz / 1000 = 1kHz (1ms)
    TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_InitStruct);
    
    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);  // 使能更新中断
    TIM_Cmd(TIM3, ENABLE);                      // 启动定时器
    
    // 配置NVIC
    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
}

// 中断服务函数
void TIM3_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
        
        // 在这里启动ADC采样
        ADC_SoftwareStartConvCmd(ADC1, ENABLE);
    }
}

注意:中断服务函数里别做复杂运算!我曾经在中断里做滤波计算,结果中断时间太长,导致下一次中断被错过。正确做法是:中断里只设置标志位,主循环里处理数据。

4.4 DMA传输:解放CPU的利器

你想想看,如果每次ADC转换完都要CPU去读数据,那CPU就一直在等。尤其是多通道采样时,CPU基本被占满。这时候就需要DMA出场了。

DMA(直接存储器访问)说白了就是——数据搬运工。它可以在外设和内存之间自动搬数据,搬完了再通知CPU。这样CPU就可以去干别的事。

咱们压传感器项目,典型配置是:ADC连续采样,DMA自动把结果搬到内存数组里。CPU只需要在数组满了之后一次性处理。

// 配置ADC1 + DMA1
#define ADC_BUFFER_SIZE  100
uint16_t adc_buffer[ADC_BUFFER_SIZE];

void ADC_DMA_Init(void)
{
    // 1. 配置ADC(略,重点是DMA)
    
    // 2. 配置DMA
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    
    DMA_InitTypeDef DMA_InitStruct;
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;  // 外设地址
    DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)adc_buffer;     // 内存地址
    DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;               // 外设→内存
    DMA_InitStruct.DMA_BufferSize = ADC_BUFFER_SIZE;              // 传输数量
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不变
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;          // 内存地址递增
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 16位
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;                  // 循环模式
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;
    DMA_Init(DMA1_Channel1, &DMA_InitStruct);
    
    // 3. 使能DMA传输完成中断(可选)
    DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
    
    // 4. 使能DMA通道
    DMA_Cmd(DMA1_Channel1, ENABLE);
    
    // 5. 使能ADC的DMA请求
    ADC_DMACmd(ADC1, ENABLE);
}

关键点:DMA的循环模式(Circular)特别适合数据采集。它会自动从头开始覆盖旧数据,你只需要在中断里判断「数据满了没」。我习惯用双缓冲——DMA填满一半时触发中断,CPU处理这一半,DMA继续填另一半。这样数据不会丢。

4.5 知识体系总览

说了这么多,咱们用一张图把整个逻辑串起来。你看了就明白——选型、GPIO、定时器、DMA,它们是怎么配合的。

STM32压传感器数据采集系统 - 核心逻辑 压传感器 模拟电压信号 (0-3.3V) ADC1 12位逐次逼近 GPIO模拟模式 DMA1 自动搬运数据 循环模式 内存数组 adc_buffer[] 100个采样点 TIM3定时器 1ms中断触发 启动ADC采样 CPU 处理数据 滤波/校准/输出 工作流程: TIM3定时中断 → 启动ADC采样 → DMA自动搬运数据到内存 → CPU空闲时处理 核心:CPU只负责处理,不参与数据搬运,效率最大化

这张图把整个流程说清楚了。定时器是「指挥官」,告诉ADC什么时候干活;DMA是「搬运工」,把数据从ADC搬到内存;CPU是「老板」,只负责最后的数据处理。各司其职,效率才高。

避坑指南:我曾经在配置DMA时,忘了使能ADC的DMA请求(ADC_DMACmd),结果DMA一直不工作。查了两天,最后发现是少了一行代码。所以,配置完一定要检查——外设的DMA请求使能了没有?

好了,这一章的内容就这些。选型、GPIO、定时器、DMA,这四个基础打牢了,后面的传感器驱动和数据处理就水到渠成。记住:基础不牢,地动山摇。别急着跑,先把走路练稳。


公众号:蓝海资料掘金营,微信deep3321