3. STM32基础外设驱动:GPIO控制LED与按键、定时器中断、USART串口通信原理与编程
好,咱们进入第三章节。前面两章我们把开发环境搭好了,也跑通了第一个点灯程序。但说真的,那个点灯只是让你感受一下“我能控制单片机了”。这一章,我们要把嵌入式开发最核心的三个基本功练扎实:GPIO、定时器、串口。
这三个东西有多重要?我打个比方。GPIO就是单片机的“手脚”,用来跟外界交互;定时器是它的“心跳”,用来精确控制节奏;串口是它的“嘴巴”,用来跟电脑或者其他设备说话。你想想看,任何一个嵌入式系统,是不是都离不开这三样?
3.1 GPIO:从点灯到按键检测
GPIO,全称 General Purpose Input Output,通用输入输出。说白了,就是单片机上的那些引脚,你可以让它们输出高电平或低电平,也可以读取外部信号是高还是低。
STM32的GPIO有8种工作模式。但实际项目中,常用的就那么几种。我列个表给你看:
| 模式 | 用途 | 典型场景 |
|---|---|---|
| 推挽输出 | 输出高低电平 | 驱动LED、蜂鸣器 |
| 开漏输出 | 需要外部上拉 | I2C总线、多设备共用 |
| 浮空输入 | 电平不确定 | 外部信号检测 |
| 上拉输入 | 默认高电平 | 按键检测(按下为低) |
| 下拉输入 | 默认低电平 | 按键检测(按下为高) |
| 模拟输入 | 读取连续电压 | ADC采样 |
3.1.1 点亮LED——推挽输出实战
咱们还是用最经典的LED闪烁来练手。不过这次,我要你理解每一行代码在干什么。
// 初始化PB0为推挽输出
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE(); // 使能GPIOB时钟
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速即可
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
// 主循环中闪烁
while (1)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 亮
HAL_Delay(500); // 延时500ms
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 灭
HAL_Delay(500);
}
这里有个细节要注意:时钟使能。STM32的外设默认都是关闭时钟的,你要用哪个外设,就得先打开它的时钟。我刚开始学的时候,经常忘了这一步,结果怎么配置引脚都没反应。嗯,后来就长记性了。
3.1.2 按键检测——上拉输入实战
按键检测看起来简单,但坑不少。最典型的问题就是抖动。机械按键在按下和释放的瞬间,电平会来回跳变几次,如果不做处理,一次按下可能被误判成好几次。
消抖的方法有两种:硬件消抖(加RC滤波)和软件消抖(延时采样)。这里我们讲软件消抖,因为不增加成本。
// 按键初始化 - 使用上拉输入
void Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0; // PA0接按键
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉,默认高电平
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
// 按键扫描函数 - 带消抖
uint8_t Key_Scan(void)
{
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) // 检测到按下
{
HAL_Delay(10); // 延时10ms,避开抖动区间
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) // 再次确认
{
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); // 等待释放
return 1; // 有效按键
}
}
return 0; // 无按键
}
你看,逻辑其实很简单:第一次检测到按下,先等10ms,再读一次。如果还是按下状态,说明是有效按键,不是抖动。然后等待按键释放再返回,这样一次按下只触发一次动作。
3.2 定时器中断:让单片机学会“准时”
前面我们用 HAL_Delay() 做延时,但这种方法有个大问题:延时期间CPU啥也干不了,就在那傻等。这叫阻塞式延时。在实际项目中,这种写法基本不能用。
那怎么办?用定时器中断。让定时器自己计数,计到指定值就触发中断,CPU在中断里处理任务。这样CPU平时可以干别的事,不用干等。
3.2.1 定时器的工作原理
STM32的定时器本质上就是一个递增计数器。它内部有一个时钟源,每来一个时钟脉冲,计数器就加1。当计数器的值等于你设置的自动重装载值(ARR)时,就会触发更新事件,计数器归零重新开始。
定时时间的计算公式:
定时时间 = (预分频值 + 1) × (自动重装载值 + 1) / 定时器时钟频率
举个例子,如果定时器时钟是72MHz,你想产生1ms的定时中断:
- 预分频值设为 71(72分频),得到1MHz的计数频率
- 自动重装载值设为 999,这样计数1000次就是1ms
void TIM_Init(void)
{
TIM_HandleTypeDef htim2 = {0};
__HAL_RCC_TIM2_CLK_ENABLE(); // 使能TIM2时钟
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71; // 72MHz / 72 = 1MHz
htim2.Init.Period = 999; // 计数1000次,1ms
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_Base_Init(&htim2);
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0); // 设置中断优先级
HAL_NVIC_EnableIRQ(TIM2_IRQn); // 使能中断
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器中断
}
// 中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
// 每1ms执行一次的任务
// 比如:按键扫描、LED闪烁控制等
}
}
3.3 USART串口通信:单片机的“嘴巴”
串口通信,是嵌入式开发中最常用的调试手段。没有之一。你想想看,单片机跑起来之后,你怎么知道它内部发生了什么?靠LED闪烁?太原始了。靠显示屏?成本高。最方便的就是通过串口把数据发到电脑上,用串口助手一看,什么都清楚了。
3.3.1 串口通信协议
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)是一种异步串行通信协议。说人话就是:一根线发数据,一根线收数据,不需要时钟线,双方约定好速度就行。
串口通信的几个关键参数:
| 参数 | 说明 | 常用值 |
|---|---|---|
| 波特率 | 每秒传输的比特数 | 9600、115200 |
| 数据位 | 一帧数据中的有效位数 | 8位(最常用) |
| 停止位 | 一帧结束的标志位 | 1位 |
| 校验位 | 用于检错 | 无校验(NONE) |
3.3.2 串口初始化与收发
STM32的串口配置,核心就是设置波特率和中断。我一般用115200波特率,速度够快,稳定性也好。
UART_HandleTypeDef huart1;
void UART_Init(void)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置TX(PA9)和RX(PA10)为复用功能
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF1_USART1; // 复用功能映射
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// USART1配置
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX; // 同时使能收发
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
// 使能接收中断
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
// 发送字符串
void UART_SendString(char *str)
{
HAL_UART_Transmit(&huart1, (uint8_t *)str, strlen(str), 1000);
}
// 接收中断回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 处理接收到的数据
// 比如:回显、解析命令等
HAL_UART_Transmit(&huart1, &rx_data, 1, 100); // 回显
HAL_UART_Receive_IT(&huart1, &rx_data, 1); // 继续接收
}
}
3.4 综合实战:按键控制LED,串口打印状态
好了,我们把前面学的三个知识点串起来,做一个完整的例子:按一下按键,LED状态翻转,同时通过串口打印当前LED的状态。
uint8_t led_status = 0; // 0:灭, 1:亮
int main(void)
{
HAL_Init();
SystemClock_Config(); // 配置系统时钟为72MHz
LED_Init(); // 初始化LED
Key_Init(); // 初始化按键
UART_Init(); // 初始化串口
UART_SendString("System Start!\r\n");
while (1)
{
if (Key_Scan()) // 检测到按键按下
{
led_status = !led_status; // 翻转状态
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0,
led_status ? GPIO_PIN_SET : GPIO_PIN_RESET);
// 通过串口打印状态
if (led_status)
UART_SendString("LED ON\r\n");
else
UART_SendString("LED OFF\r\n");
}
}
}
这个例子虽然简单,但它包含了嵌入式开发的完整闭环:输入(按键)→ 处理(状态翻转)→ 输出(LED显示+串口打印)。你以后做的任何项目,本质上都是这个模式的扩展。
- GPIO的推挽输出用于控制LED等外设,上拉输入用于按键检测
- 按键必须做消抖处理,软件消抖10ms延时是常用方案
- 定时器中断比HAL_Delay更高效,适合周期性任务
- 串口是调试利器,建议每个模块初始化后都打印状态信息
说实话,这三个外设我用了十几年,每次新项目都离不开它们。你把这章的内容吃透了,后面学LoRa通信、GPS定位、数据采集这些高级功能,就会轻松很多。因为不管多复杂的系统,底层都是这些基础外设在支撑。