嵌入式arm32笔记

liuxiaobai
2025-04-19 / 0 评论 / 3 阅读 / 正在检测是否收录...

STM32 GPIO 模式说明(附 HAL 库代码)


一、GPIO 模式的区别

1. 输入模式(像"听声音的耳朵")

  • 浮空输入:耳朵完全悬空,容易受干扰(比如没接线的麦克风)。
  • 上拉输入:耳朵默认听到"高电平",按下按钮时变低(适合按钮接 GND)。
  • 下拉输入:耳朵默认听到"低电平",按下按钮时变高(适合按钮接 VCC)。

2. 输出模式(像"发声的嘴巴")

  • 推挽输出:嘴巴既能大喊(输出高电平),又能沉默(输出低电平),驱动能力强(适合 LED、电机)。
  • 开漏输出:嘴巴只能沉默(输出低电平),需要外部帮忙大喊(靠上拉电阻拉高),避免信号冲突(适合 I2C 总线)。

3. 复用功能模式(像"变身技能")

  • 引脚变身成 UART、SPI、PWM 等专用功能(比如让引脚变成串口通信线)。

4. 模拟模式(像"测量温度的手")

  • 直接读取真实世界的连续信号(比如用 ADC 测量温度传感器电压)。

二、使用场景对比表

模式典型场景HAL库代码关键字标准库(STD)代码关键字LL库代码关键字
浮空输入外部自带上下拉的按钮、UART_RX接收引脚GPIO_MODE_INPUTGPIO_Mode_IN_FLOATINGLL_GPIO_MODE_FLOATING
上拉输入按钮一端接GND(按下变低电平)GPIO_PULLUPGPIO_Mode_IPULL_GPIO_PULL_UP
下拉输入按钮一端接VCC(按下变高电平)GPIO_PULLDOWNGPIO_Mode_IPDLL_GPIO_PULL_DOWN
推挽输出驱动LED、蜂鸣器、继电器GPIO_MODE_OUTPUT_PPGPIO_Mode_Out_PPLL_GPIO_MODE_OUTPUT
开漏输出I2C通信、电平转换GPIO_MODE_OUTPUT_ODGPIO_Mode_Out_ODLL_GPIO_MODE_OUTPUT_OD
复用推挽SPI时钟线、UART发送引脚GPIO_MODE_AF_PPGPIO_Mode_AF_PPLL_GPIO_MODE_ALTERNATE
复用开漏I2C数据线GPIO_MODE_AF_ODGPIO_Mode_AF_ODLL_GPIO_MODE_ALTERNATE_OD
模拟模式ADC读取温度、光敏传感器GPIO_MODE_ANALOGGPIO_Mode_AINLL_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 外设(此处省略)
}

四、关键注意事项

  1. 电压匹配:STM32 大部分引脚是 3.3V,接 5V 设备需电平转换。
  2. 开漏模式:使用时要外接上拉电阻(通常 4.7kΩ)。
  3. 初始化顺序:先配置时钟 (__HAL_RCC_GPIOx_CLK_ENABLE()),再配置 GPIO。
  4. 复用功能:除了 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]
               ↓长按超时
               → 触发长按事件

七、扩展建议

  1. 多按键支持:使用按键结构体数组管理多个按键
  2. 连发功能:在PRESSED状态添加连发计数器
  3. 组合按键:通过状态机组合检测多个按键状态
  4. 事件回调:使用函数指针实现事件回调机制

八、 关键参数说明

参数推荐值说明
扫描周期5-20ms平衡响应速度和CPU占用
消抖时间10-30ms根据按键特性调整
长按判定时间800-1500ms符合人体工学设计

这种实现方式具有以下优势:

  1. 低资源占用:定时器中断扫描不阻塞主程序
  2. 精准计时:可精确检测短按/长按时间
  3. 强扩展性:方便增加双击检测等高级功能
  4. 硬件无关:可移植到不同平台使用

基于状态机的增强型按键扫描(支持长按/短按/连按)


一、状态机升级设计

// 按键状态枚举
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
    }
};

六、 调试技巧

  1. 状态跟踪
const char* state_names[] = {
    "IDLE", "PRESS_DEB", "PRESSED", 
    "RELEASE_DEB", "LONG_PRESS", "REPEAT"
};

printf("Current State: %s\r\n", state_names[key->state]);
  1. 时序分析
// 在定时器中断中记录时间戳
static uint32_t timestamp = 0;
timestamp = HAL_GetTick();
  1. 逻辑分析仪

    • 监控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 == tail0
满队列(tail+1)%size == headsize-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 数据覆盖问题

  • 现象:队列满后继续压入数据导致旧数据丢失
  • 解决方案

    1. 检查返回值,丢弃新数据
  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;
}

八、关键注意事项

  1. 初始化检查:确保size≥2
  2. 线程安全:多任务/中断环境需加锁
  3. 数据时效:及时处理队列数据避免溢出
  4. 内存对齐:缓冲区地址建议4字节对齐(提升访问效率)

环形队列是嵌入式系统的"数据传送带"——合理设计才能保证系统流畅运行!


扫描二维码,在手机上阅读!
0

评论 (0)

取消