STM32从零到量产开发:四路继电器工业控制模块开发— 数字输入(bsp_di)设计说明

1. 模块概述

本模块实现了 STM32 四路继电器工业控制板上的 4通道光耦隔离数字输入(DI) 驱动。该驱动封装了底层 GPIO 读取逻辑,提供统一的逻辑电平接口,并内置软件消抖机制,确保在工业现场环境下获取稳定可靠的输入状态。

核心特性

  • 硬件抽象:统一封装 GPIO 端口与引脚,上层业务无需关心具体引脚映射。
  • 逻辑统一:自动处理光耦“低电平有效”特性,对外统一暴露 1=有输入0=无输入
  • 软件消抖:基于系统时基 HAL_GetTick() 实现非阻塞式定时消抖,有效滤除工业现场干扰。
  • 多模式读取:支持单通道读取、全通道位图读取及原始电平调试读取。

2. 硬件配置与映射

2.1 引脚定义

驱动依赖 CubeMX 生成的 main.h 中的宏定义,具体映射关系如下:

通道编号 GPIO 端口 引脚号 备注
DI1 DI1_GPIO_Port DI1_Pin (PB0) 光耦隔离输入
DI2 DI2_GPIO_Port DI2_Pin (PB1) 光耦隔离输入
DI3 DI3_GPIO_Port DI3_Pin (PB10) 光耦隔离输入
DI4 DI4_GPIO_Port DI4_Pin (PB11) 光耦隔离输入

2.2 电气特性与极性

  • 输入模式:GPIO 配置为 上拉输入
  • 有效电平低电平有效(Active Low)。
    • 外部信号接入 $
      ightarrow$ 光耦导通 $
      ightarrow$ 引脚被拉低 (GPIO_PIN_RESET)。
    • 外部信号断开 $
      ightarrow$ 光耦截止 $
      ightarrow$ 引脚被上拉至高电平 (GPIO_PIN_SET)。
  • 逻辑转换:驱动内部自动将物理低电平转换为逻辑 1

3. 核心数据结构

3.1 硬件引脚映射表

用于解耦具体引脚配置,便于移植:

typedef struct {
    GPIO_TypeDef *port;  // GPIO 端口指针
    uint16_t      pin;   // GPIO 引脚掩码
} di_pin_t;

static const di_pin_t s_di_tbl[DI_CH_NUM] = {
    { DI1_GPIO_Port, DI1_Pin },
    { DI2_GPIO_Port, DI2_Pin },
    { DI3_GPIO_Port, DI3_Pin },
    { DI4_GPIO_Port, DI4_Pin },
};

3.2 消抖状态管理结构

每个通道独立维护消抖状态,避免通道间相互影响:

typedef struct {
    uint8_t  raw;           // 实时原始引脚电平(仅底层扫描使用)
    uint8_t  stable;        // 消抖完成后的稳定状态(业务/Modbus读取)
    uint32_t last_change;   // 电平跳变时间戳 (ms),消抖计时基准
} di_debounce_t;

static di_debounce_t s_di[DI_CH_NUM]; // 4路独立状态缓存

4. 软件消抖机制

4.1 设计原理

采用 状态机 + 时间戳 的非阻塞消抖算法,不占用 CPU 资源,适合在主循环中周期调用。

4.2 工作流程

  1. 采样:读取当前物理电平 cur
  2. 跳变检测
    • cur != raw:更新 raw,重置 last_change = HAL_GetTick()
    • cur == raw:保持当前状态,继续计时。
  3. 稳定确认
    • 条件:cur != stable(HAL_GetTick() - last_change) >= DI_DEBOUNCE_MS
    • 动作:更新 stable = cur,通知上层状态变更。

4.3 参数配置

#define DI_DEBOUNCE_MS 10u  // 消抖确认时长 10ms
  • 建议:工业场景下光耦输入建议 5~10ms;干接点/传感器输入建议 3~5ms。
  • 扫描周期:推荐主循环调用 BSP_DI_Scan() 周期为 1~5ms

5. API 接口详解

5.1 初始化

void BSP_DI_Init(void);
  • 功能:初始化消抖状态缓存,读取当前电平作为初始稳定值。
  • 注意:GPIO 硬件模式需先由 MX_GPIO_Init() 配置完成。

5.2 周期扫描

void BSP_DI_Scan(void);
  • 功能:执行 4 路通道的消抖状态机更新。
  • 调用要求:必须在主循环中 周期性调用,依赖 HAL_GetTick() 时基。

5.3 获取单路状态

uint8_t BSP_DI_Get(uint8_t ch);
  • 参数ch 通道编号 (1~4)。
  • 返回1=有输入,0=无输入。非法通道返回 0
  • 用途:业务逻辑判断、Modbus 寄存器映射。

5.4 获取全通道状态

uint8_t BSP_DI_GetAll(void);
  • 返回:4位有效掩码。
    • bit0 $
      ightarrow$ DI1
    • bit1 $
      ightarrow$ DI2
    • bit2 $
      ightarrow$ DI3
    • bit3 $
      ightarrow$ DI4
  • 用途:批量状态上报、快速位运算判断。

5.5 获取原始电平(调试用)

uint8_t BSP_DI_GetRaw(uint8_t ch);
  • 参数ch 通道编号 (1~4)。
  • 返回1=低电平(有输入),0=高电平(无输入)。
  • 用途:硬件排查、消抖参数调试、光耦故障诊断。

6. 使用示例

// 1. 系统初始化阶段
BSP_DI_Init();

// 2. 主循环 (Main Loop)
while (1) {
    BSP_DI_Scan();  // 必须周期调用,建议 1-5ms
    
    // 业务逻辑
    if (BSP_DI_Get(1)) {
        // DI1 触发,执行相应动作
        Relay_Control(RELAY_1, ON);
    }
    
    // 批量检查
    uint8_t all_di = BSP_DI_GetAll();
    if (all_di == 0x0F) {
        // 4路全通
    }
    
    HAL_Delay(1); // 简易延时,实际项目建议使用定时器中断或 RTOS 任务
}

7. 注意事项

  1. 时基依赖BSP_DI_Scan 强依赖 HAL_GetTick(),确保 SysTick 中断正常运行且未被长时间阻塞。
  2. 扫描频率:扫描周期应小于消抖时间(Scan_Period < DI_DEBOUNCE_MS),否则可能导致消抖失效或响应延迟。
  3. 线程安全:当前实现为裸机单线程模型。若移植至 RTOS,需对 s_di 数组加互斥锁保护。
  4. 硬件一致性:修改引脚映射时,务必同步更新 s_di_tblmain.h 中的定义。

bsp_di.h代码实现

/**
  ******************************************************************************
  * @file    bsp_di.h
  * @brief   4通道光耦隔离数字输入驱动 - 对应产品规格书 3.3 数字输入端子
  *
  * 硬件映射 (来自 CubeMX main.h):
  *   DI1 -> PB0, DI2 -> PB1, DI3 -> PB10, DI4 -> PB11
  *
  * 极性说明:
  *   GPIO 配置为上拉输入, 光耦导通时引脚被拉低
  *   规格书 3.3: "输入为低有效" -> 读到低电平 = 外部有输入信号
  *   本驱动对上层统一暴露逻辑值: 1=有输入, 0=无输入
  *
  * 消抖机制说明:
  *   软件定时消抖,多次采样电平保持 DEBOUNCE_CONFIRM_MS 毫秒一致,才更新稳定状态
  *   扫描依赖系统毫秒时基 HAL_GetTick()
  ******************************************************************************
  */
#ifndef __BSP_DI_H
#define __BSP_DI_H

#ifdef __cplusplus
extern "C" {
#endif

#include "main.h"

/* 数字输入总通道数量 */
#define DI_CH_NUM            4u

/* 软件消抖确认时长配置说明 */
/* 如果按键输入硬件已经有消抖电容,工业场景软件消抖取5~10ms */
/* 如果是无光耦隔离/外部干接点/传感器输入,软件消抖不能省,建议设 3~5ms */
#define DI_DEBOUNCE_MS       10u

/**
  * @brief  初始化4路数字输入GPIO,初始化消抖缓存状态
  * @note   GPIO硬件模式由MX_GPIO_Init完成配置,本函数仅初始化内部消抖变量
  */
void BSP_DI_Init(void);

/**
  * @brief  DI通道消抖定时扫描函数
  * @note   必须在主循环固定周期调用,推荐扫描周期 1~5ms;依赖 HAL_GetTick() 获取系统毫秒时基
  */
void BSP_DI_Scan(void);

/**
  * @brief  获取单路DI经过软件消抖后的稳定逻辑状态(业务正常读取接口)
  * @param  ch: 通道编号 1~4
  * @retval 1=外部有输入信号,0=无输入信号
  */
uint8_t BSP_DI_Get(uint8_t ch);

/**
  * @brief  批量获取全部4路DI消抖后稳定状态,返回位图掩码
  * @retval 4位有效掩码
  *         bit0对应DI1、bit1对应DI2、bit2对应DI3、bit3对应DI4;bit=1代表该通道有输入
  */
uint8_t BSP_DI_GetAll(void);

/**
  * @brief  获取单路DI当前实时原始电平(未经过软件消抖,仅调试排查硬件时使用)
  * @param  ch: 通道编号 1~4
  * @retval 1=硬件检测到输入,0=无输入
  */
uint8_t BSP_DI_GetRaw(uint8_t ch);

#ifdef __cplusplus
}
#endif
#endif /* __BSP_DI_H */

bsp_di.c代码实现

/**
  ******************************************************************************
  * @file    bsp_di.c
  * @brief   4通道数字输入驱动实现 (含软件消抖)
  ******************************************************************************
  */
#include "bsp_di.h"
#include "main.h"

/* ===================== 硬件引脚定义 ===================== */
/* DI 引脚映射结构体 (通道号 1~4 对应数组索引 0~3) */
typedef struct
{
    GPIO_TypeDef *port;
    uint16_t      pin;
} di_pin_t;

/* 硬件引脚映射表 */
static const di_pin_t s_di_tbl[DI_CH_NUM] =
{
    { DI1_GPIO_Port, DI1_Pin },
    { DI2_GPIO_Port, DI2_Pin },
    { DI3_GPIO_Port, DI3_Pin },
    { DI4_GPIO_Port, DI4_Pin },
};

/* ===================== 消抖状态结构定义 ===================== */
/* 单通道消抖状态结构体 */
typedef struct
{
    uint8_t  raw;           /* 实时原始引脚电平,仅底层扫描使用,上层禁止读取 */
    uint8_t  stable;        /* 消抖完成稳定状态,业务/Modbus读取 */
    uint32_t last_change;   /* 电平跳变时间戳(ms),消抖计时基准 */
} di_debounce_t;

/* 四路独立消抖状态缓存 */
static di_debounce_t s_di[DI_CH_NUM];

/* ===================== 内部底层私有函数 ===================== */
/**
  * @brief  底层读取单通道原始引脚电平
  * @param  idx 内部数组下标 0~3
  * @retval 1 = 低电平有输入,0 = 无输入
  * @note  硬件电平适配层,仅此处修改高低有效逻辑,上层全部无感
  */
static uint8_t DI_ReadRawLevel(uint8_t idx)
{
    return (HAL_GPIO_ReadPin(s_di_tbl[idx].port, s_di_tbl[idx].pin) == GPIO_PIN_RESET) ? 1u : 0u;
}

/* ===================== 对外公共接口 ===================== */
/**
  * @brief  DI模块初始化,初始化所有通道消抖状态
  */
void BSP_DI_Init(void)
{
    uint8_t i;
    uint32_t now = HAL_GetTick();
    for (i = 0u; i < DI_CH_NUM; i++)
    {
        s_di[i].raw         = DI_ReadRawLevel(i);
        s_di[i].stable      = s_di[i].raw;
        s_di[i].last_change = now;
    }
}

/**
  * @brief  周期消抖扫描函数,主循环定时调用
  * @note  检测电平跳变重置计时,稳定达到消抖时长后更新可靠状态
  */
void BSP_DI_Scan(void)
{
    uint8_t  i;
    uint8_t  cur;
    uint32_t now = HAL_GetTick();

    for (i = 0u; i < DI_CH_NUM; i++)
    {
        cur = DI_ReadRawLevel(i);
        if (cur != s_di[i].raw)
        {
            s_di[i].raw         = cur;
            s_di[i].last_change = now;
        }
        else
        {
            if ((cur != s_di[i].stable) && ((now - s_di[i].last_change) >= DI_DEBOUNCE_MS))
            {
                s_di[i].stable = cur;
            }
        }
    }
}

/**
  * @brief  获取单路消抖完成后的DI状态
  * @param  ch 通道号 1~4
  * @retval 0无输入 / 1有输入,非法通道返回0
  */
uint8_t BSP_DI_Get(uint8_t ch)
{
    if (ch < 1u || ch > DI_CH_NUM)
    {
        return 0u;
    }
    return s_di[ch - 1u].stable;
}

/**
  * @brief  获取四路DI整体状态掩码
  * @retval bit0~bit3 对应 DI1~DI4,bit置1代表有输入
  */
uint8_t BSP_DI_GetAll(void)
{
    uint8_t i;
    uint8_t mask = 0u;
    for (i = 0u; i < DI_CH_NUM; i++)
    {
        if (s_di[i].stable)
        {
            mask |= (uint8_t)(1u << i);
        }
    }
    return mask;
}

/**
  * @brief  获取单路未消抖原始电平,仅调试诊断使用
  * @param  ch 通道号 1~4
  * @retval 1低电平有输入 / 0无输入,非法通道返回0
  */
uint8_t BSP_DI_GetRaw(uint8_t ch)
{
    if (ch < 1u || ch > DI_CH_NUM)
    {
        return 0u;
    }
    return DI_ReadRawLevel(ch - 1u);
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐