4、STM32基础外设编程(一):GPIO输入输出、外部中断、定时器中断与PWM输出

各位同学,欢迎来到实战指南的第四讲。从这一章开始,我们正式进入STM32的底层外设编程。说实话,这部分内容才是嵌入式开发的硬核所在。你想想看,单片机再厉害,最终不还是得靠这些基础外设去感知世界、控制设备吗?

今天我们要啃的四个知识点——GPIO、外部中断、定时器中断和PWM输出,几乎是每个电力数据采集项目都绕不开的。我当年做第一个项目时,就在GPIO的配置上栽过跟头,后来才明白,越是基础的东西,越要搞透彻。

4.1 GPIO输入输出:最基础也最容易被忽视

GPIO,全称是General Purpose Input Output,通用输入输出口。说白了,它就是单片机跟外界打交道的「手脚」。

STM32的GPIO有好几种模式,我习惯把它们分成两大类:输入模式和输出模式。输入模式里又分浮空输入、上拉输入、下拉输入;输出模式则分推挽输出和开漏输出。

重点提醒:在电力数据采集场景中,GPIO最常用的两个用途是:读取按键状态(输入)和控制继电器/指示灯(输出)。

配置GPIO其实就三步:使能时钟、设置模式、读写数据。我给大家看个最简单的例子——点亮一个LED灯。

// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

// 配置PB0为推挽输出,速度50MHz
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);

// 点亮LED(低电平有效)
GPIO_ResetBits(GPIOB, GPIO_Pin_0);

嗯,这里要注意:很多开发板的LED是低电平点亮,也有高电平点亮的。我建议你拿到板子后先看原理图,别像我当年一样,焊好电路发现灯死活不亮,最后查了半天才发现是电平反了。

4.2 外部中断:让单片机学会「主动响应」

轮询方式读取按键,说白了就是CPU一直在那「盯着」引脚看,效率很低。外部中断就不一样了——引脚电平变化时,CPU会暂停当前任务,先去处理中断。

配置外部中断的步骤稍微多一点,但逻辑很清晰:

  1. 配置GPIO为输入模式
  2. 使能AFIO时钟
  3. 配置EXTI线
  4. 配置NVIC(嵌套向量中断控制器)
  5. 编写中断服务函数

我的经验:在电力数据采集中,外部中断常用于检测过零信号或脉冲计数。我曾经用外部中断配合定时器,实现了电网频率的实时测量,精度能达到0.01Hz。

看个按键中断的例子:

// 配置PA0为外部中断线0,下降沿触发
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);

// 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

避坑指南:我曾经在中断服务函数里做了太多事情,导致主程序卡死。记住:中断服务函数要「短小精悍」,只做标志位设置或数据读取,耗时操作放到主循环里处理。

4.3 定时器中断:精确的时间控制

定时器是STM32里最强大的外设之一。它本质上就是一个计数器,时钟源驱动它不断累加,当计数值达到预设值时,就会触发中断。

STM32的定时器分三类:基本定时器(TIM6/TIM7)、通用定时器(TIM2~TIM5)和高级定时器(TIM1/TIM8)。做电力数据采集,通用定时器用得最多。

配置定时器中断的公式很简单:

定时时间 = (预分频值 + 1) × (自动重装载值 + 1) / 定时器时钟频率

举个例子,我想让定时器每1ms中断一次:

// 假设定时器时钟为72MHz
// 预分频值设为7199,自动重装载值设为9
// 定时时间 = (7199+1) × (9+1) / 72000000 = 0.001s = 1ms

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

TIM_TimeBaseStructure.TIM_Prescaler = 7199;      // 预分频
TIM_TimeBaseStructure.TIM_Period = 9;             // 自动重装载值
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);        // 使能更新中断
TIM_Cmd(TIM2, ENABLE);                            // 启动定时器

实用技巧:在电力数据采集中,我常用定时器中断来生成ADC采样时钟。比如每100μs触发一次中断,在中断里启动一次ADC转换,这样就能实现固定频率的采样。

4.4 PWM输出:模拟信号的数字实现

PWM,脉冲宽度调制。说白了就是通过调节高低电平的占空比,来模拟不同的电压值。在电力电子领域,PWM简直是万能工具——调压、调速、调光,样样都行。

STM32的定时器可以直接输出PWM波,不需要软件模拟。配置步骤:

  1. 配置定时器的时基单元
  2. 配置输出比较通道
  3. 设置占空比
  4. 使能定时器和PWM输出

看个例子,用TIM2的通道1输出50Hz、50%占空比的PWM:

// 配置GPIO为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;          // TIM2_CH1对应PA0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

// 配置TIM2时基
TIM_TimeBaseStructure.TIM_Prescaler = 7199;        // 72MHz / 7200 = 10kHz
TIM_TimeBaseStructure.TIM_Period = 199;            // 10kHz / 200 = 50Hz
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

// 配置PWM模式1
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 99;                // 50%占空比
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);

TIM_Cmd(TIM2, ENABLE);                             // 启动定时器

我的建议:做PWM输出时,先算好频率和分辨率。比如你要控制一个电机,PWM频率太低会听到啸叫声,太高又可能增加开关损耗。我一般取1kHz~20kHz之间,具体看负载特性。

4.5 实战组合:按键控制LED亮度

把今天学的知识串起来,做个实用的小项目:用按键控制PWM占空比,实现LED亮度调节。

思路是这样的:

  • 外部中断检测按键按下
  • 每次按下,占空比增加10%
  • 达到100%后归零
  • PWM输出驱动LED

代码框架:

volatile uint8_t duty_cycle = 0;  // 当前占空比

void EXTI0_IRQHandler(void) {
    if(EXTI_GetITStatus(EXTI_Line0) != RESET) {
        duty_cycle += 10;
        if(duty_cycle > 100) duty_cycle = 0;
        
        // 更新占空比
        TIM_SetCompare1(TIM2, (uint16_t)(duty_cycle * 1.99));
        
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

int main(void) {
    // 初始化GPIO、外部中断、定时器PWM...
    // 代码略,参考前面章节
    
    while(1) {
        // 主循环可以做其他事情
    }
}

避坑指南:我曾经在中断里直接调用TIM_SetCompare1函数,结果因为函数执行时间太长,导致中断响应变慢。后来我改成在中断里只设置标志位,在主循环里更新PWM占空比,问题就解决了。

好了,这一章的内容就到这里。GPIO、外部中断、定时器中断和PWM输出,这四个基础外设是STM32开发的「四件套」。你想想看,电力数据采集项目里,哪个环节能离开它们?

下一章我们会继续深入,学习ADC采样和DMA传输。到时候你会发现,有了今天的基础,那些高级外设用起来会顺手很多。

记住:基础不牢,地动山摇。把这些外设玩透了,后面的路就好走了。