3、STM32基础:GPIO控制、时钟树配置、中断系统入门

好,咱们开始第三讲。这一章是STM32开发的基石,说白了就是三板斧:怎么让引脚听话(GPIO)、怎么让芯片跑得又快又稳(时钟树)、以及怎么让芯片学会“主动汇报”而不是傻等(中断)。

我刚开始用STM32时,觉得GPIO不就是高低电平嘛,有啥好学的?结果第一次做项目,LED死活不亮,查了半天发现是时钟没开。嗯,这种坑,我猜你们迟早也会踩一次。

3.1 GPIO控制——让引脚听你指挥

GPIO,全称General Purpose Input Output,通用输入输出口。说白了,就是芯片伸出来的那些金属脚,你可以控制它输出高电平(3.3V)或低电平(0V),也可以读取外部信号是高还是低。

STM32的GPIO比51单片机复杂不少。每个GPIO端口(比如PA、PB、PC)有16个引脚,每个引脚可以配置成多种模式。我列个表,你们感受一下:

模式 说明 典型用途
推挽输出 能输出高低电平,驱动能力强 LED、蜂鸣器
开漏输出 只能输出低电平,高电平靠外部上拉 I2C总线、多设备共用线
浮空输入 引脚电平不确定,完全由外部决定 按键检测(需外部上拉)
上拉输入 内部接上拉电阻,默认高电平 按键检测(省外部电阻)
下拉输入 内部接下拉电阻,默认低电平 某些传感器信号
模拟模式 引脚直接连到ADC/DAC内部 采集模拟电压

写代码时,我习惯用HAL库。初始化一个GPIO,其实就是填一个结构体:

GPIO_InitTypeDef GPIO_InitStruct = {0};

// 开启GPIOA时钟(这一步最容易忘!)
__HAL_RCC_GPIOA_CLK_ENABLE();

// 配置PA5为推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 让PA5输出高电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
我曾经踩过的坑: 配置GPIO前忘记开启对应时钟。芯片上电后,所有外设时钟默认是关闭的。你不开时钟,寄存器写进去根本没用。LED不亮?先查时钟开了没。

3.2 时钟树配置——芯片的心脏跳动

时钟,就是芯片的“心跳”。没有时钟,CPU不干活,外设也不干活。STM32的时钟树,说白了就是一堆时钟源、分频器、倍频器,最终生成各个模块需要的时钟频率。

STM32常用的时钟源有四种:

  • HSI(高速内部时钟): 8MHz,芯片内部自带,精度一般,但上电就能用。
  • HSE(高速外部时钟): 通常接8MHz晶振,精度高,适合要求高的场合。
  • LSI(低速内部时钟): 40kHz左右,给独立看门狗和RTC用。
  • LSE(低速外部时钟): 32.768kHz,专门给RTC用,走时准。

我个人习惯,做显微镜项目时,主频尽量跑到最高。比如STM32F103,最高72MHz。怎么来的?用HSE 8MHz晶振,经过PLL锁相环9倍频,得到72MHz。代码配置如下:

RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

// 配置HSE为时钟源,开启PLL
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;  // 8MHz * 9 = 72MHz
HAL_RCC_OscConfig(&RCC_OscInitStruct);

// 配置系统时钟、AHB、APB1、APB2分频
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                              | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;   // AHB = 72MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;    // APB1 = 36MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;    // APB2 = 72MHz
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
小技巧: APB1总线最高只能跑36MHz,APB2可以跑72MHz。所以像I2C、SPI这些外设,如果挂在APB1上,时钟别配超了。我见过有人把APB1配成72MHz,芯片直接罢工。

3.3 中断系统入门——让CPU不再傻等

中断是什么?打个比方:你在看书(主程序),突然手机响了(中断事件),你放下书接电话(执行中断服务函数),接完继续看书(返回主程序)。

没有中断时,CPU只能轮询——不停地问“按键按了吗?数据到了吗?”效率极低。有了中断,CPU可以安心干正事,有事了外设会主动喊它。

STM32的中断系统,核心是NVIC(嵌套向量中断控制器)。它负责管理所有中断的优先级和使能。配置一个外部中断的步骤,我总结为三步:

  1. 配置GPIO为中断模式(选择触发边沿)
  2. 配置NVIC(使能中断、设置优先级)
  3. 编写中断服务函数(处理事件)

举个例子,用PA0引脚检测按键下降沿中断:

// 第一步:配置GPIO中断模式
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;  // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP;           // 内部上拉,默认高电平
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 第二步:配置NVIC
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);  // 抢占优先级0,子优先级0
HAL_NVIC_EnableIRQ(EXTI0_IRQn);          // 使能中断

// 第三步:中断服务函数(放在stm32f1xx_it.c中)
void EXTI0_IRQHandler(void)
{
    if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
    {
        // 处理按键事件
        // 比如:切换LED状态
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
        
        // 清除中断标志位(必须做!)
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
    }
}
注意: 中断服务函数里不要做耗时操作,比如延时、打印。中断讲究“快进快出”。我见过有人在中断里放了个HAL_Delay(1000),结果整个系统卡死。中断里只做标志位设置,具体处理放到主循环里。

3.4 实战经验:显微镜项目中的中断应用

在显微镜项目中,中断用得最多的地方有两个:

  • 编码器信号: 电机转动时,编码器输出脉冲。用外部中断捕获脉冲,计算位置和速度。
  • 按键/限位开关: 平台移动到极限位置时,限位开关触发中断,立即停止电机,防止撞坏。

我记得有一次调试自动对焦,电机跑到极限位置没停,直接把丝杆顶死了。后来加了限位开关中断,优先级设成最高,一触发就紧急停止。从那以后,我所有运动控制项目,限位中断都是最高优先级。

核心要点回顾:
  • GPIO配置前,先开时钟。
  • 时钟树配置,注意APB1和APB2的限速。
  • 中断服务函数要短,只做标志位操作。
  • 中断标志位一定要清除,否则会一直进中断。

好了,这一章就到这里。下一章咱们开始玩定时器——你会发现,有了定时器,很多事就变得简单了。