第四章:GPIO与基本外设
各位同学,欢迎来到实战课的第一站。今天我们要聊的,是嵌入式开发里最基础、也最绕不开的几个模块——GPIO、定时器、中断系统,还有那个默默无闻的SysTick。
你可能会想,GPIO不就是点个灯、读个按键吗?有什么好讲的?嗯,我当年也是这么想的。直到有一次,我在一个电机驱动项目里,因为GPIO输出模式没配对,导致MOS管驱动波形异常,烧了三个驱动板……从那以后,我再也不敢小看任何一个引脚配置。
核心观点:GPIO是MCU的“手脚”,定时器是“心跳”,中断是“神经反射”。这三样东西玩不转,运动控制就无从谈起。
4.1 GPIO配置与操作
GPIO,全称General Purpose Input Output,通用输入输出口。说白了,就是MCU用来跟外界打交道的引脚。
我个人习惯,拿到一个新板子,第一件事就是先把所有用到的GPIO初始化代码写好。别急着写业务逻辑,先把“手脚”活动开。
4.1.1 GPIO的工作模式
现在的MCU,GPIO模式多得让人眼花。但常用的就这几种:
- 推挽输出:最常用。能输出高电平,也能输出低电平。驱动LED、继电器都没问题。
- 开漏输出:只能输出低电平,高电平要靠外部上拉。常用于I2C总线、电平转换场景。
- 浮空输入:引脚电平完全由外部决定。适合读取按键状态。
- 上拉/下拉输入:内部有电阻拉高或拉低。防止引脚悬空时电平不确定。
- 复用功能:引脚交给内部外设(如UART、SPI)控制。这时候GPIO寄存器就不直接管用了。
经验之谈:我在项目中遇到过一个问题——按键按下时电平抖动,导致一次按下被误判成多次。后来加了内部上拉,再配合软件消抖,问题就解决了。记住:硬件能做的事,别让软件硬扛。
4.1.2 GPIO配置实战
以STM32为例,配置一个GPIO输出,代码大概长这样:
// 初始化PA5为推挽输出
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE(); // 开时钟,这一步容易忘
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);
// 操作引脚
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 输出高
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 翻转电平
你看,代码本身不复杂。但有几个坑:
- 时钟没开,配置了也白配。我见过有人查了两天bug,最后发现是时钟没使能。
- 速度别乱设。高速模式会增加功耗和噪声。驱动LED用低速就够了。
- 输出高电平时,注意负载电流。MCU引脚一般只能输出几毫安到二十毫安,驱动大负载要加三极管或MOS管。
4.2 定时器与PWM生成
定时器,是MCU里最强大的外设之一。它不仅能计时,还能生成PWM、捕获输入信号、编码器接口……在运动控制里,PWM是控制电机速度的核心手段。
4.2.1 定时器的工作原理
说白了,定时器就是一个不断递增(或递减)的计数器。它内部有一个时钟源,每来一个时钟脉冲,计数器就加1。当计数值达到某个设定值时,就会触发事件——比如更新中断、PWM翻转等。
我习惯把定时器想象成一个“秒表”:
- 预分频器:相当于把时钟频率降下来。比如72MHz的时钟,预分频设为71,就变成1MHz。
- 自动重装载值:计数器从0数到这个值,然后归零重新开始。这个值决定了定时周期。
- 比较值:当计数器等于这个值时,输出电平翻转。这就是PWM的占空比控制。
4.2.2 PWM生成实战
生成一个50Hz、占空比50%的PWM,用于控制舵机:
// 定时器2,通道1输出PWM
TIM_HandleTypeDef htim2;
TIM_OC_InitTypeDef sConfigOC = {0};
__HAL_RCC_TIM2_CLK_ENABLE();
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71; // 72MHz / 72 = 1MHz
htim2.Init.Period = 19999; // 1MHz / 20000 = 50Hz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 9999; // 占空比 50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
注意:PWM频率和占空比的计算,一定要确认好时钟树。我曾经在一个项目里,因为时钟配置搞混了,PWM频率算出来差了10倍,电机转得忽快忽慢。后来用示波器一看,才发现是预分频算错了。
4.3 中断系统
中断,是MCU处理异步事件的机制。没有中断,你就得轮询——CPU啥也别干了,就盯着那个引脚看。有了中断,CPU可以该干嘛干嘛,事件来了再处理。
4.3.1 外部中断
外部中断,就是引脚电平变化时触发的中断。常用于按键检测、编码器信号、传感器触发等场景。
配置外部中断的步骤:
- 配置GPIO为输入模式
- 将GPIO连接到EXTI(外部中断控制器)
- 配置中断触发方式(上升沿、下降沿、双边沿)
- 使能中断线
- 编写中断服务函数
// 配置PA0为外部中断,下降沿触发
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_SYSCFG_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);
// 使能中断线
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
// 中断服务函数
void EXTI0_IRQHandler(void)
{
if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
{
// 处理中断事件
// 注意:中断里别做耗时操作
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
}
}
避坑指南:我曾经在中断服务函数里调了一个延时函数,结果系统直接卡死。为什么?因为中断里不能阻塞!中断服务函数要短小精悍,最好只设置标志位,具体处理放到主循环里做。
4.3.2 定时器中断
定时器中断,就是定时器计数到某个值时触发的中断。常用于周期性任务,比如每1ms采集一次传感器数据。
// 配置定时器2,每1ms触发一次中断
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71; // 1MHz
htim2.Init.Period = 999; // 1ms
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_Base_Init(&htim2);
HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器并使能中断
void TIM2_IRQHandler(void)
{
if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET)
{
// 每1ms执行一次
// 比如:更新PID控制量
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
}
}
4.4 SysTick定时器
SysTick,是Cortex-M内核自带的一个24位递减定时器。它不依赖MCU厂商的外设库,是内核级别的。
SysTick最常见的用途,就是提供操作系统的心跳,或者作为HAL库的延时基准。说白了,它就是那个“嘀嗒嘀嗒”的节拍器。
配置SysTick非常简单:
// 配置SysTick为1ms中断一次
// SystemCoreClock 是系统时钟频率
SysTick_Config(SystemCoreClock / 1000);
// SysTick中断服务函数
void SysTick_Handler(void)
{
// 每1ms执行一次
// 比如:更新系统时间戳
uwTick++;
}
我个人习惯,用SysTick来做软件定时器。比如:
- 定义一个全局变量,每1ms加1
- 需要延时的地方,读取当前值,加上延时时间,然后等待变量达到这个值
- 这样就不会阻塞CPU,还能实现多个定时器并行
总结一下:GPIO是基础,定时器是工具,中断是效率,SysTick是节拍。这四个东西组合起来,你已经能写出大部分嵌入式应用了。下一节,我们会把这些知识串起来,做一个完整的运动控制示例。