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);
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);
3.3 中断系统入门——让CPU不再傻等
中断是什么?打个比方:你在看书(主程序),突然手机响了(中断事件),你放下书接电话(执行中断服务函数),接完继续看书(返回主程序)。
没有中断时,CPU只能轮询——不停地问“按键按了吗?数据到了吗?”效率极低。有了中断,CPU可以安心干正事,有事了外设会主动喊它。
STM32的中断系统,核心是NVIC(嵌套向量中断控制器)。它负责管理所有中断的优先级和使能。配置一个外部中断的步骤,我总结为三步:
- 配置GPIO为中断模式(选择触发边沿)
- 配置NVIC(使能中断、设置优先级)
- 编写中断服务函数(处理事件)
举个例子,用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);
}
}
3.4 实战经验:显微镜项目中的中断应用
在显微镜项目中,中断用得最多的地方有两个:
- 编码器信号: 电机转动时,编码器输出脉冲。用外部中断捕获脉冲,计算位置和速度。
- 按键/限位开关: 平台移动到极限位置时,限位开关触发中断,立即停止电机,防止撞坏。
我记得有一次调试自动对焦,电机跑到极限位置没停,直接把丝杆顶死了。后来加了限位开关中断,优先级设成最高,一触发就紧急停止。从那以后,我所有运动控制项目,限位中断都是最高优先级。
- GPIO配置前,先开时钟。
- 时钟树配置,注意APB1和APB2的限速。
- 中断服务函数要短,只做标志位操作。
- 中断标志位一定要清除,否则会一直进中断。
好了,这一章就到这里。下一章咱们开始玩定时器——你会发现,有了定时器,很多事就变得简单了。