STM32 GPIO 模式说明(附 HAL 库代码)
一、GPIO 模式的区别
1. 输入模式(像"听声音的耳朵")
- 浮空输入:耳朵完全悬空,容易受干扰(比如没接线的麦克风)。
- 上拉输入:耳朵默认听到"高电平",按下按钮时变低(适合按钮接 GND)。
- 下拉输入:耳朵默认听到"低电平",按下按钮时变高(适合按钮接 VCC)。
2. 输出模式(像"发声的嘴巴")
- 推挽输出:嘴巴既能大喊(输出高电平),又能沉默(输出低电平),驱动能力强(适合 LED、电机)。
- 开漏输出:嘴巴只能沉默(输出低电平),需要外部帮忙大喊(靠上拉电阻拉高),避免信号冲突(适合 I2C 总线)。
3. 复用功能模式(像"变身技能")
- 引脚变身成 UART、SPI、PWM 等专用功能(比如让引脚变成串口通信线)。
4. 模拟模式(像"测量温度的手")
- 直接读取真实世界的连续信号(比如用 ADC 测量温度传感器电压)。
二、使用场景对比表
模式 | 典型场景 | HAL库代码关键字 | 标准库(STD)代码关键字 | LL库代码关键字 |
---|---|---|---|---|
浮空输入 | 外部自带上下拉的按钮、UART_RX接收引脚 | GPIO_MODE_INPUT | GPIO_Mode_IN_FLOATING | LL_GPIO_MODE_FLOATING |
上拉输入 | 按钮一端接GND(按下变低电平) | GPIO_PULLUP | GPIO_Mode_IPU | LL_GPIO_PULL_UP |
下拉输入 | 按钮一端接VCC(按下变高电平) | GPIO_PULLDOWN | GPIO_Mode_IPD | LL_GPIO_PULL_DOWN |
推挽输出 | 驱动LED、蜂鸣器、继电器 | GPIO_MODE_OUTPUT_PP | GPIO_Mode_Out_PP | LL_GPIO_MODE_OUTPUT |
开漏输出 | I2C通信、电平转换 | GPIO_MODE_OUTPUT_OD | GPIO_Mode_Out_OD | LL_GPIO_MODE_OUTPUT_OD |
复用推挽 | SPI时钟线、UART发送引脚 | GPIO_MODE_AF_PP | GPIO_Mode_AF_PP | LL_GPIO_MODE_ALTERNATE |
复用开漏 | I2C数据线 | GPIO_MODE_AF_OD | GPIO_Mode_AF_OD | LL_GPIO_MODE_ALTERNATE_OD |
模拟模式 | ADC读取温度、光敏传感器 | GPIO_MODE_ANALOG | GPIO_Mode_AIN | LL_GPIO_MODE_ANALOG |
三、HAL 库示例代码
场景1:按钮控制 LED(上拉输入 + 推挽输出)
#include "stm32f1xx_hal.h"
#define BUTTON_PIN GPIO_PIN_0
#define BUTTON_PORT GPIOB
#define LED_PIN GPIO_PIN_13
#define LED_PORT GPIOC
int main(void) {
HAL_Init();
// 配置 LED 为推挽输出
GPIO_InitTypeDef gpio;
gpio.Pin = LED_PIN;
gpio.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(LED_PORT, &gpio);
// 配置按钮为上拉输入(按钮另一端接 GND)
gpio.Pin = BUTTON_PIN;
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_PULLUP; // 内部上拉
HAL_GPIO_Init(BUTTON_PORT, &gpio);
while (1) {
// 按钮按下时(检测低电平)
if (HAL_GPIO_ReadPin(BUTTON_PORT, BUTTON_PIN) == GPIO_PIN_RESET) {
HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET); // LED 亮
} else {
HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); // LED 灭
}
HAL_Delay(10); // 简单防抖
}
}
场景2:I2C 通信(开漏输出 + 复用模式)
// I2C 引脚配置(以 SDA 为例)
void I2C_GPIO_Init(void) {
GPIO_InitTypeDef gpio;
gpio.Pin = GPIO_PIN_7; // 假设 SDA 在 PB7
gpio.Mode = GPIO_MODE_AF_OD; // 复用开漏模式
gpio.Pull = GPIO_PULLUP; // 外部需要上拉电阻
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &gpio);
// 还需配置 I2C 外设(此处省略)
}
场景3:ADC 读取模拟信号(模拟模式)
// 配置 PA1 为模拟输入(接温度传感器)
void ADC_GPIO_Init(void) {
GPIO_InitTypeDef gpio;
gpio.Pin = GPIO_PIN_1;
gpio.Mode = GPIO_MODE_ANALOG; // 模拟模式
gpio.Pull = GPIO_NOPULL; // 不需要上下拉
HAL_GPIO_Init(GPIOA, &gpio);
// 还需配置 ADC 外设(此处省略)
}
四、关键注意事项
- 电压匹配:STM32 大部分引脚是 3.3V,接 5V 设备需电平转换。
- 开漏模式:使用时要外接上拉电阻(通常 4.7kΩ)。
- 初始化顺序:先配置时钟 (
__HAL_RCC_GPIOx_CLK_ENABLE()
),再配置 GPIO。 - 复用功能:除了 GPIO 配置,还需激活对应的外设功能(如 SPI、I2C)。
总结:STM32 的 GPIO 就像"万能插座",通过不同的模式变身适应各种任务。配置时记住:输入模式听信号,输出模式发信号,复用模式变身份,模拟模式读真实!
基于状态机和定时器实现按键扫描(STM32 HAL库版)
一、 设计思路
- 状态机:将按键行为分解为多个状态,消除机械抖动影响
- 定时器中断:周期性扫描按键状态(推荐5-10ms扫描周期)
优势:
- 占用CPU资源少
- 可检测短按/长按/连发
- 天然防抖
二、按键状态机设计
typedef enum {
KEY_STATE_IDLE, // 空闲状态
KEY_STATE_PRESS_DETECT, // 按下检测
KEY_STATE_PRESS_DEBOUNCE,// 按下消抖
KEY_STATE_PRESSED, // 确认按下
KEY_STATE_RELEASE_DEBOUNCE // 释放消抖
} KeyState;
typedef struct {
GPIO_TypeDef* GPIOx; // 按键所属GPIO组
uint16_t GPIO_Pin; // 按键引脚
KeyState state; // 当前状态
uint8_t press_count; // 按下计数器
uint8_t is_pressed; // 按下标志
uint8_t is_long_pressed; // 长按标志
} KeyHandle;
三、 定时器配置(以TIM2为例)
// 定时器初始化(10ms周期)
void TIMER_Init(void) {
TIM_HandleTypeDef htim;
htim.Instance = TIM2;
htim.Init.Prescaler = 8000 - 1; // 8MHz / 8000 = 1KHz
htim.Init.Period = 10 - 1; // 1KHz / 10 = 100Hz (10ms)
htim.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_Base_Init(&htim);
HAL_TIM_Base_Start_IT(&htim); // 启用定时器中断
HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
// 定时器中断服务函数
void TIM2_IRQHandler(void) {
HAL_TIM_IRQHandler(&htim2);
Key_Scan_Handler(); // 调用按键扫描
}
四、 按键扫描核心代码
#define DEBOUNCE_TIME 2 // 消抖时间 10ms*2=20ms
#define LONG_PRESS_TIME 100 // 长按时间 10ms*100=1000ms
KeyHandle key1 = {
.GPIOx = GPIOB,
.GPIO_Pin = GPIO_PIN_0,
.state = KEY_STATE_IDLE
};
void Key_Scan_Handler(void) {
uint8_t key_level = HAL_GPIO_ReadPin(key1.GPIOx, key1.GPIO_Pin);
switch(key1.state) {
case KEY_STATE_IDLE:
if(key_level == GPIO_PIN_RESET) { // 检测到按下
key1.state = KEY_STATE_PRESS_DETECT;
key1.press_count = 0;
}
break;
case KEY_STATE_PRESS_DETECT:
key1.press_count++;
if(key1.press_count >= DEBOUNCE_TIME) {
if(key_level == GPIO_PIN_RESET) {
key1.state = KEY_STATE_PRESSED;
key1.is_pressed = 1;
// 触发短按事件
printf("Key Pressed!\r\n");
} else {
key1.state = KEY_STATE_IDLE;
}
key1.press_count = 0;
}
break;
case KEY_STATE_PRESSED:
key1.press_count++;
if(key_level == GPIO_PIN_SET) { // 检测释放
key1.state = KEY_STATE_RELEASE_DEBOUNCE;
key1.press_count = 0;
} else if(key1.press_count >= LONG_PRESS_TIME) {
key1.is_long_pressed = 1;
// 触发长按事件
printf("Long Press!\r\n");
key1.press_count = 0;
}
break;
case KEY_STATE_RELEASE_DEBOUNCE:
key1.press_count++;
if(key1.press_count >= DEBOUNCE_TIME) {
key1.state = KEY_STATE_IDLE;
key1.is_pressed = 0;
key1.is_long_pressed = 0;
// 触发释放事件
printf("Key Released!\r\n");
}
break;
}
}
五、 主程序调用
int main(void) {
HAL_Init();
SystemClock_Config();
TIMER_Init();
// 按键GPIO初始化
GPIO_InitTypeDef gpio;
gpio.Pin = GPIO_PIN_0;
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &gpio);
while(1) {
// 主循环处理其他任务
if(key1.is_pressed) {
// 处理按键按下事件
key1.is_pressed = 0;
}
if(key1.is_long_pressed) {
// 处理长按事件
key1.is_long_pressed = 0;
}
}
}
六、状态机流程图
[IDLE] → 检测到按下 → [PRESS_DETECT]
↓消抖成功
→ [PRESSED] → 检测释放 → [RELEASE_DEBOUNCE] → [IDLE]
↓长按超时
→ 触发长按事件
七、扩展建议
- 多按键支持:使用按键结构体数组管理多个按键
- 连发功能:在PRESSED状态添加连发计数器
- 组合按键:通过状态机组合检测多个按键状态
- 事件回调:使用函数指针实现事件回调机制
八、 关键参数说明
参数 | 推荐值 | 说明 |
---|---|---|
扫描周期 | 5-20ms | 平衡响应速度和CPU占用 |
消抖时间 | 10-30ms | 根据按键特性调整 |
长按判定时间 | 800-1500ms | 符合人体工学设计 |
这种实现方式具有以下优势:
- 低资源占用:定时器中断扫描不阻塞主程序
- 精准计时:可精确检测短按/长按时间
- 强扩展性:方便增加双击检测等高级功能
- 硬件无关:可移植到不同平台使用
基于状态机的增强型按键扫描(支持长按/短按/连按)
一、状态机升级设计
// 按键状态枚举
typedef enum {
KEY_STATE_IDLE, // 空闲状态
KEY_STATE_PRESS_DEB, // 按下消抖
KEY_STATE_PRESSED, // 确认按下
KEY_STATE_RELEASE_DEB, // 释放消抖
KEY_STATE_LONG_PRESS, // 长按状态
KEY_STATE_REPEAT // 连按状态
} KeyState;
// 按键配置结构体
typedef struct {
GPIO_TypeDef* GPIOx; // GPIO端口
uint16_t GPIO_Pin; // 引脚
KeyState state; // 当前状态
uint32_t press_tick; // 按下计时
uint8_t is_short_press; // 短按标志
uint8_t is_long_press; // 长按标志
uint8_t repeat_count; // 连按次数
uint8_t long_press_triggered;// 防止长按重复触发
} KeyHandle;
// 时间参数配置(单位:ms)
#define DEBOUNCE_TIME 20 // 消抖时间
#define LONG_PRESS_TIME 1000 // 长按判定时间
#define REPEAT_INTERVAL 200 // 连按触发间隔
二、 增强型状态转移图
[IDLE]
↓检测到按下 → [PRESS_DEB]
↓消抖成功 → [PRESSED]
↓持续按下超过LONG_PRESS → [LONG_PRESS]
↓释放 → [RELEASE_DEB] → [IDLE](触发短按)
↑消抖失败 ←
[LONG_PRESS]
↓保持按下 → 持续触发连按(REPEAT_INTERVAL周期)
↓释放 → [RELEASE_DEB] → [IDLE]
三、核心扫描逻辑(定时器中断中调用)
void Key_Scan_Handler(KeyHandle* key) {
uint8_t key_level = HAL_GPIO_ReadPin(key->GPIOx, key->GPIO_Pin);
static uint32_t tick = 0; // 需在定时器中断中维护tick
switch(key->state) {
// 空闲状态 -------------------------------------------------
case KEY_STATE_IDLE:
if(key_level == GPIO_PIN_RESET) { // 检测到按下
key->state = KEY_STATE_PRESS_DEB;
key->press_tick = tick;
}
break;
// 按下消抖 -------------------------------------------------
case KEY_STATE_PRESS_DEB:
if((tick - key->press_tick) >= DEBOUNCE_TIME) {
if(key_level == GPIO_PIN_RESET) { // 确认按下
key->state = KEY_STATE_PRESSED;
key->press_tick = tick;
} else { // 抖动干扰
key->state = KEY_STATE_IDLE;
}
}
break;
// 已确认按下 -----------------------------------------------
case KEY_STATE_PRESSED:
if(key_level == GPIO_PIN_SET) { // 提前释放(短按)
key->state = KEY_STATE_RELEASE_DEB;
key->is_short_press = 1;
}
else if((tick - key->press_tick) >= LONG_PRESS_TIME) {
key->state = KEY_STATE_LONG_PRESS;
key->is_long_press = 1;
key->long_press_triggered = 0;
}
break;
// 长按状态 -------------------------------------------------
case KEY_STATE_LONG_PRESS:
if(key_level == GPIO_PIN_SET) { // 释放
key->state = KEY_STATE_RELEASE_DEB;
}
else { // 持续长按触发连按
if((tick - key->press_tick) >= REPEAT_INTERVAL) {
key->repeat_count++;
key->press_tick = tick; // 重置计时
// 触发连按回调(需自定义实现)
// Key_Repeat_Callback();
}
}
break;
// 释放消抖 -------------------------------------------------
case KEY_STATE_RELEASE_DEB:
if((tick - key->press_tick) >= DEBOUNCE_TIME) {
key->state = KEY_STATE_IDLE;
key->press_tick = 0;
key->long_press_triggered = 0;
}
break;
}
tick++; // 每次扫描tick自增
}
四、 主程序处理逻辑
KeyHandle user_key = {
.GPIOx = GPIOB,
.GPIO_Pin = GPIO_PIN_0,
.state = KEY_STATE_IDLE
};
int main(void) {
// 初始化代码...
while(1) {
// 短按处理
if(user_key.is_short_press) {
user_key.is_short_press = 0;
printf("Short Press Detected!\r\n");
// 执行短按动作...
}
// 长按处理(只触发一次)
if(user_key.is_long_press) {
user_key.is_long_press = 0;
user_key.long_press_triggered = 1;
printf("Long Press Detected!\r\n");
// 执行长按动作...
}
// 连按处理(持续触发)
if(user_key.repeat_count > 0) {
printf("Repeat Count: %d\r\n", user_key.repeat_count);
// 执行连按次数对应操作...
user_key.repeat_count = 0;
}
}
}
五、高级功能扩展建议
1. 回调函数机制
// 定义回调函数指针
typedef void (*KeyCallback)(void);
typedef struct {
// ...原有结构体成员
KeyCallback short_press_cb;
KeyCallback long_press_cb;
KeyCallback repeat_cb;
} KeyHandle;
// 在主处理中调用回调
if(user_key.is_short_press && user_key.short_press_cb) {
user_key.short_press_cb();
}
2. 双击检测实现
// 添加状态
KEY_STATE_DOUBLE_CHECK
// 添加计时器
uint32_t double_click_timeout;
// 在释放消抖后启动双击检测
case KEY_STATE_RELEASE_DEB:
// ...原有逻辑
double_click_timeout = tick + 300; // 300ms内检测第二次点击
3. 按键参数可配置化
typedef struct {
// ...其他成员
uint16_t debounce_time;
uint16_t long_press_time;
uint16_t repeat_interval;
} KeyConfig;
KeyHandle user_key = {
.config = {
.debounce_time = 20,
.long_press_time = 1000,
.repeat_interval = 200
}
};
六、 调试技巧
- 状态跟踪:
const char* state_names[] = {
"IDLE", "PRESS_DEB", "PRESSED",
"RELEASE_DEB", "LONG_PRESS", "REPEAT"
};
printf("Current State: %s\r\n", state_names[key->state]);
- 时序分析:
// 在定时器中断中记录时间戳
static uint32_t timestamp = 0;
timestamp = HAL_GetTick();
逻辑分析仪:
- 监控GPIO电平变化
- 观察状态切换时机
- 验证消抖时间准确性
七、参数优化建议
应用场景 | 推荐参数 |
---|---|
机械按键 | DEBOUNCE_TIME = 20-50ms |
触摸按键 | DEBOUNCE_TIME = 50-100ms |
快速操作界面 | REPEAT_INTERVAL = 100-150ms |
安全关键操作 | LONG_PRESS_TIME = 2000ms |
实现效果说明:
- 短按:快速点击立即响应
- 长按:按住超过1秒触发一次
- 连按:长按后每200ms触发一次
- 精准消抖:消除触点抖动影响
- 低CPU占用:所有处理在定时器中断完成
这种设计可轻松移植到任何嵌入式平台,只需调整GPIO操作相关代码即可。
环形队列
一、环形队列基本概念
1.1 核心特点
- 循环利用内存:首尾相连的存储结构
- FIFO原则:先进先出(First In First Out)
- 免内存分配:提前分配固定大小的缓冲区
1.2 应用场景
- 串口接收数据缓冲
- 任务间通信(生产者-消费者模型)
- 中断与主程序数据传递
二、队列结构定义
2.1 数据结构
typedef struct {
uint8_t *buffer; // 数据存储区指针
uint32_t size; // 缓冲区总容量(实际可用size-1)
uint32_t head; // 队首索引(弹出位置)
uint32_t tail; // 队尾索引(压入位置)
} QueueType_t;
typedef enum {
QUEUE_OK, // 操作成功
QUEUE_OVERLOAD, // 队列已满
QUEUE_EMPTY // 队列为空
} QueueStatus_t;
三、核心函数解析
3.1 队列初始化
void QueueInit(QueueType_t *queue, uint8_t *buffer, uint32_t size) {
queue->buffer = buffer; // 绑定存储区
queue->size = size; // 设置容量
queue->head = 0; // 初始化队首
queue->tail = 0; // 初始化队尾
}
3.2 数据压入(单字节)
QueueStatus_t QueuePush(QueueType_t *queue, uint8_t data) {
// 计算下一个空位
uint32_t next_tail = (queue->tail + 1) % queue->size;
// 检查队列是否已满
if (next_tail == queue->head) {
return QUEUE_OVERLOAD;
}
// 存储数据并更新队尾
queue->buffer[queue->tail] = data;
queue->tail = next_tail;
return QUEUE_OK;
}
3.3 数据弹出(单字节)
QueueStatus_t QueuePop(QueueType_t *queue, uint8_t *pdata) {
// 检查队列是否为空
if (queue->head == queue->tail) {
return QUEUE_EMPTY;
}
// 取出数据并更新队首
*pdata = queue->buffer[queue->head];
queue->head = (queue->head + 1) % queue->size;
return QUEUE_OK;
}
四、关键逻辑图解
4.1 队列状态判断
状态 | 判断条件 | 可用容量 |
---|---|---|
空队列 | head == tail | 0 |
满队列 | (tail+1)%size == head | size-1(最大) |
4.2 内存布局示意图
[示例] size=5 的缓冲区:
索引: 0 1 2 3 4
数据: A B C D (空)
↑head ↑tail
可用空间:4(实际存储4个元素)
五、使用示例(串口数据接收)
5.1 硬件连接
- STM32 USART1
- 波特率 115200
- 启用接收中断
5.2 代码实现
// 定义队列(缓冲区大小256)
#define UART_BUF_SIZE 256
uint8_t uart_buffer[UART_BUF_SIZE];
QueueType_t uart_queue;
// 初始化队列
QueueInit(&uart_queue, uart_buffer, UART_BUF_SIZE);
// 串口中断服务函数
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
uint8_t data = USART1->DR;
QueuePush(&uart_queue, data); // 压入接收数据
}
}
// 主程序处理数据
void ProcessUartData(void) {
uint8_t data;
while (QueuePop(&uart_queue, &data) == QUEUE_OK) {
// 处理数据(示例:回传)
USART_SendData(USART1, data);
}
}
六、常见问题解决
6.1 队列容量为何是size-1?
- 设计需求:需要区分队列满和空的状态
- 数学证明:当head==tail时为空,tail+1==head时为满
6.2 数据覆盖问题
- 现象:队列满后继续压入数据导致旧数据丢失
解决方案:
- 检查返回值,丢弃新数据
if (QueuePush(&queue, data) == QUEUE_OVERLOAD) {
// 处理队列满的情况
}
1. 增大队列容量
6.3 多线程环境问题
- 风险:中断和主程序同时操作队列导致数据错乱
- 解决方案:
// 在操作队列前关闭中断
__disable_irq();
QueuePush(&queue, data);
__enable_irq();
七、性能优化技巧
7.1 批量操作优化
// 批量压入(减少模运算次数)
uint32_t QueuePushArray(QueueType_t *queue, uint8_t *data, uint32_t len) {
uint32_t free_space = QueueCountFree(queue);
uint32_t actual_len = MIN(len, free_space);
// 分两段复制(绕过缓冲区末尾)
uint32_t first_chunk = queue->size - queue->tail;
if (first_chunk >= actual_len) {
memcpy(&queue->buffer[queue->tail], data, actual_len);
} else {
memcpy(&queue->buffer[queue->tail], data, first_chunk);
memcpy(queue->buffer, data + first_chunk, actual_len - first_chunk);
}
queue->tail = (queue->tail + actual_len) % queue->size;
return actual_len;
}
八、关键注意事项
- 初始化检查:确保size≥2
- 线程安全:多任务/中断环境需加锁
- 数据时效:及时处理队列数据避免溢出
- 内存对齐:缓冲区地址建议4字节对齐(提升访问效率)
环形队列是嵌入式系统的"数据传送带"——合理设计才能保证系统流畅运行!
评论 (0)