Files
433_STM32/Core/Src/io_monitor.c

429 lines
16 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
******************************************************************************
* @file io_monitor.c
* @brief IO状态监控模块实现
* @author Application Layer
* @version 1.2
******************************************************************************
* @attention
* 本模块实现四路数字输入的状态监控
* 关键特性:
* 1. 10ms定时扫描平衡响应速度和CPU占用
* 2. 软件去抖连续3次相同状态才确认变化
* 3. 状态变化时通过回调函数上报,支持多端口路由
*
* 修订历史:
* v1.2 - 增加事件回调机制,支持多端口路由
* v1.1 - 修复审查报告中危-5去抖计数器初始化优化
******************************************************************************
*/
#include "io_monitor.h"
#include "uart2_print.h"
#include "main.h"
#include <string.h>
/*==============================================================================
* 调试宏定义
*============================================================================*/
/* DEBUG_IO_MONITOR: 调试日志开关置1时启用调试输出置0时禁用 */
#define DEBUG_IO_MONITOR 1
#if DEBUG_IO_MONITOR
/* 调试日志宏,带模块前缀"[IO]"方便在串口调试终端中过滤日志 */
#define DEBUG_LOG(fmt, ...) UART2_Print_Printf("[IO] " fmt "\r\n", ##__VA_ARGS__)
#else
/* 禁用调试日志时,将宏定义为空,避免生成无用代码 */
#define DEBUG_LOG(fmt, ...)
#endif
/*==============================================================================
* 数据结构定义
*============================================================================*/
/**
* @brief IO通道数据结构
* @note 用于描述一个数字输入通道的完整状态信息
*
* 设计目的:
* 该结构体封装了监控单个数字输入(DI)通道所需的全部状态信息,包括
* 硬件连接参数(GPIO端口和引脚)、当前稳定状态、去抖计数器、原始采样状态
* 以及状态变化统计。这使得多通道扫描逻辑能够以统一的结构处理每个通道。
*
* 字段说明:
* - port: GPIO端口指针指向硬件寄存器(如GPIOB)
* - pin: GPIO引脚编号(如GPIO_PIN_4)用于HAL库读取函数
* - current_state: 经过去抖处理后的稳定状态0=低电平1=高电平
* - debounce_counter: 去抖计数器,累加相同采样值的次数,达到阈值才确认状态变化
* - last_raw_state: 上一次采样的原始电平状态,用于检测电平变化
* - change_count: 状态变化总次数统计,用于诊断和监控
*/
typedef struct {
GPIO_TypeDef *port; /**< GPIO端口指针指向外设寄存器基地址 */
uint16_t pin; /**< GPIO引脚编号对应HAL库的引脚定义 */
uint8_t current_state; /**< 经去抖处理后的稳定状态0=低电平1=高电平 */
uint8_t debounce_counter; /**< 去抖计数器,连续采样到相同值的次数 */
uint8_t last_raw_state; /**< 上一次采样的原始电平,用于变化检测 */
uint32_t change_count; /**< 状态变化累计次数,用于性能监控 */
} io_channel_t;
/*==============================================================================
* 全局变量定义
*============================================================================*/
/**
* @brief 四路数字输入通道配置表
* @note 静态初始化表定义四个监控通道的GPIO硬件连接
*
* 各通道映射关系:
* - 通道0: GPIOB, GPIO_PIN_4
* - 通道1: GPIOB, GPIO_PIN_5
* - 通道2: GPIOB, GPIO_PIN_6
* - 通道3: GPIOB, GPIO_PIN_7
*
* 初始化值说明所有状态和计数器均初始化为0表示上电初始状态未知
*/
static io_channel_t di_channels[IO_CHANNEL_COUNT] = {
{GPIOB, GPIO_PIN_4, 0, 0, 0, 0},
{GPIOB, GPIO_PIN_5, 0, 0, 0, 0},
{GPIOB, GPIO_PIN_6, 0, 0, 0, 0},
{GPIOB, GPIO_PIN_7, 0, 0, 0, 0}
};
/**
* @brief 上一次扫描时刻的时间戳
* @note 用于实现10ms固定间隔扫描避免每次都执行扫描操作
* HAL_GetTick()返回系统运行毫秒数
*/
static uint32_t last_scan_tick = 0;
/**
* @brief 事件上报使能标志
* @note 置true时允许状态变化自动上报置false时禁止上报
* 可用于批量操作时临时抑制不必要的事件通知
*/
static bool report_enabled = true;
/**
* @brief IO事件回调函数指针
* @note 设置后IO状态变化将通过回调函数上报
* 为NULL时使用默认的UART2_Print_String输出
*/
static io_event_callback_t g_event_callback = NULL;
/*==============================================================================
* 内部静态函数声明
*============================================================================*/
/**
* @brief 计算异或校验和
* @note 对输入数据的每个字节执行异或运算,生成校验码
* 用于DI_EVENT消息的校验和计算
*
* @param data: 待计算校验和的数据缓冲区(输入)
* @param len: 数据长度,以字节为单位(输入)
* @return 校验和: uint8_t类型的异或结果
*
* 算法原理:遍历数据缓冲区,将每个字节与累加器进行异或操作
* 初始值为0最终结果为所有字节的异或和
*/
static uint8_t calc_checksum(const char *data, uint8_t len);
/**
* @brief 发送DI状态变化事件
* @note 构造并发送ASCII格式的状态变化消息至UART2
* 消息格式: $DI_EVENT,<channel>,<state>*<checksum>\r\n
*
* @param channel: 通道编号从0开始计数(输入)
* @param state: 通道状态0=低电平1=高电平(输入)
* @return 无返回值
*
* 调用说明:
* - 此函数在状态变化被确认后调用,用于通知上位机或日志系统
* - 内部会调用calc_checksum计算校验和
* - 通过UART2_Print_String发送原始字符串
*/
static void send_di_event(uint8_t channel, uint8_t state);
/*==============================================================================
* 内部静态函数实现
*============================================================================*/
/**
* @brief 计算异或校验和
* @note 对输入数据的每个字节执行异或运算,生成校验码
* 用于DI_EVENT消息的校验和计算
*
* @param data: 待计算校验和的数据缓冲区(输入)
* @param len: 数据长度,以字节为单位(输入)
* @return 校验和: uint8_t类型的异或结果
*
* 算法原理:遍历数据缓冲区,将每个字节与累加器进行异或操作
* 初始值为0最终结果为所有字节的异或和
*/
static uint8_t calc_checksum(const char *data, uint8_t len)
{
uint8_t cs = 0;
for (uint8_t i = 0; i < len; i++) {
cs ^= (uint8_t)data[i];
}
return cs;
}
/**
* @brief 发送DI状态变化事件
* @note 构造并发送ASCII格式的状态变化消息
* 消息格式: $DI_EVENT,<channel>,<state>*<checksum>\r\n
* 如果设置了回调函数则通过回调发送否则发送到UART2
*
* @param channel: 通道编号从0开始计数(输入)
* @param state: 通道状态0=低电平1=高电平(输入)
* @return 无返回值
*
* 调用说明:
* - 此函数在状态变化被确认后调用,用于通知上位机或日志系统
* - 内部会调用calc_checksum计算校验和
* - 如果设置了回调函数则通过回调发送否则通过UART2_Print_String发送
*/
static void send_di_event(uint8_t channel, uint8_t state)
{
char msg[32];
uint8_t cs;
/* 构造消息主体channel+1将0-base转换为1-base的用户可见编号 */
int len = snprintf(msg, sizeof(msg), "$DI_EVENT,%d,%d*", channel + 1, state);
/* 计算异或校验和,跳过'$'符号只对正文部分计算 */
cs = calc_checksum(msg + 1, len - 1);
/* 将校验和追加到消息末尾,格式为两位十六进制数 */
snprintf(msg + len, sizeof(msg) - len, "%02X\r\n", cs);
/* 根据是否设置回调函数选择发送方式 */
if (g_event_callback != NULL) {
/* 通过回调函数发送,支持多端口路由 */
g_event_callback(channel, state, msg);
} else {
/* 默认发送到UART2 */
UART2_Print_String(msg);
}
/* 输出调试日志,记录状态变化 */
DEBUG_LOG("CH%d -> %s", channel + 1, state ? "HIGH" : "LOW");
}
/*==============================================================================
* 公共函数实现
*============================================================================*/
/**
* @brief IO监控模块初始化
* @note 在系统启动时调用,初始化所有数字输入通道的默认状态
*
* @param 无
* @return 无
*
* 功能说明:
* 1. 读取各通道GPIO引脚的当前电平状态
* 2. 初始化去抖计数器为1(已稳定采样一次)
* 3. 重置变化计数器和扫描时间戳
* 4. 使能事件上报功能
*
* 初始化策略:
* - 去抖计数器初始化为1而非0这样首次采样时只需再连续采样2次
* 即可确认状态相比初始化为0可加快首次状态确认速度
*/
void IO_Monitor_Init(void)
{
/* 遍历所有IO通道进行初始化 */
for (int i = 0; i < IO_CHANNEL_COUNT; i++) {
io_channel_t *ch = &di_channels[i];
/* 读取GPIO当前电平HAL_GPIO_ReadPin返回GPIO_PinState类型 */
ch->current_state = HAL_GPIO_ReadPin(ch->port, ch->pin) ? 1 : 0;
/* 记录原始状态作为上次采样值 */
ch->last_raw_state = ch->current_state;
/* 去抖计数器初始化为1已完成首次稳定采样 */
ch->debounce_counter = 1;
/* 变化计数清零 */
ch->change_count = 0;
}
/* 初始化扫描时间戳为0确保首次扫描立即执行 */
last_scan_tick = 0;
/* 使能自动上报功能 */
report_enabled = true;
/* 输出初始化完成日志,显示初始各通道状态掩码 */
DEBUG_LOG("Init OK, initial states: 0x%02X", IO_Monitor_GetAllStates());
}
/**
* @brief IO监控定时任务
* @note 应在主循环或定时器中断中周期性调用,执行扫描和去抖处理
*
* @param 无
* @return 无
*
* 算法说明 - 软件去抖三段式状态机:
* 阶段1: 采样值与上次相同 -> 计数器累加
* 阶段2: 采样值与上次不同 -> 重置计数器并更新上次值
* 阶段3: 计数器达到阈值(IO_DEBOUNCE_COUNT)且与当前稳定状态不同
* -> 确认状态变化更新current_state触发事件上报
*
* 去抖阈值说明:
* - IO_DEBOUNCE_COUNT通常定义为3表示连续3次采样相同才确认
* - 结合IO_SCAN_PERIOD_MS(10ms)去抖确认时间为20-30ms
* - 可有效滤除机械开关的抖动噪声
*/
void IO_Monitor_Task(void)
{
/* 获取当前系统时间戳(毫秒) */
uint32_t current_tick = HAL_GetTick();
/*----------------------------------------------------------
* 扫描周期控制
* 仅当距上次扫描超过IO_SCAN_PERIOD_MS时才执行新扫描
* 这种节流机制可避免高频调用时CPU资源浪费
*----------------------------------------------------------*/
if (current_tick - last_scan_tick < IO_SCAN_PERIOD_MS) {
return;
}
/* 更新扫描时间戳 */
last_scan_tick = current_tick;
/*----------------------------------------------------------
* 遍历所有通道执行去抖扫描
*----------------------------------------------------------*/
for (int i = 0; i < IO_CHANNEL_COUNT; i++) {
io_channel_t *ch = &di_channels[i];
/* 读取GPIO引脚当前电平转换为0/1表示 */
uint8_t raw_state = HAL_GPIO_ReadPin(ch->port, ch->pin) ? 1 : 0;
/*----------------------------------------------------------
* 去抖逻辑分支
*----------------------------------------------------------*/
if (raw_state != ch->last_raw_state) {
/* 分支1: 采样值发生变化 -> 重置去抖计数器 */
ch->debounce_counter = 0;
ch->last_raw_state = raw_state;
} else {
/* 分支2: 采样值与上次相同 -> 累加去抖计数器 */
if (ch->debounce_counter < IO_DEBOUNCE_COUNT) {
ch->debounce_counter++;
}
/* 分支3: 计数器达到阈值且与稳定状态不一致 -> 确认状态变化 */
else if (ch->current_state != raw_state) {
/* 更新为新确认的稳定状态 */
ch->current_state = raw_state;
/* 增加变化统计计数 */
ch->change_count++;
/* 事件上报(已使能情况下) */
if (report_enabled) {
send_di_event(i, raw_state);
}
}
}
}
}
/**
* @brief 获取指定通道的当前状态
* @note 返回经过去抖处理后的稳定状态值
*
* @param channel: 通道编号从0开始计数(输入)
* @return 通道状态: 0=低电平1=高电平通道无效时返回0
*
* 使用说明:
* - 通道编号超出有效范围(0-3)时静默返回0不报错
* - 返回的是已去抖的稳定状态,非原始采样值
*/
uint8_t IO_Monitor_GetState(uint8_t channel)
{
/* 参数边界检查超出范围的通道返回0 */
if (channel >= IO_CHANNEL_COUNT) {
return 0;
}
return di_channels[channel].current_state;
}
/**
* @brief 获取所有通道状态的组合掩码
* @note 将四路通道状态打包为一个字节返回,方便批量读取和显示
*
* @param 无
* @return 通道状态掩码: uint8_t类型每位对应一个通道状态
*
* 位域定义:
* - bit0: 通道0状态
* - bit1: 通道1状态
* - bit2: 通道2状态
* - bit3: 通道3状态
*
* 示例返回0x05(0101b)表示通道0=高、通道1=低、通道2=高、通道3=低
*/
uint8_t IO_Monitor_GetAllStates(void)
{
uint8_t states = 0;
/* 遍历所有通道,将各通道状态按位组合成掩码 */
for (int i = 0; i < IO_CHANNEL_COUNT; i++) {
if (di_channels[i].current_state) {
states |= (1 << i);
}
}
return states;
}
/**
* @brief 设置事件上报使能状态
* @note 可用于在批量操作或初始化过程中临时抑制事件上报
*
* @param enable: true=使能上报false=禁用上报(输入)
* @return 无
*
* 使用场景:
* - 系统初始化期间不希望产生大量事件,可先禁用再恢复
* - 批量设置多个通道状态时,可先禁用避免中间状态触发事件
*/
void IO_Monitor_EnableReport(bool enable)
{
report_enabled = enable;
DEBUG_LOG("Report %s", enable ? "enabled" : "disabled");
}
/**
* @brief 获取指定通道的状态变化次数
* @note 用于诊断和性能监控,统计各通道状态变化频率
*
* @param channel: 通道编号从0开始计数(输入)
* @return 变化次数: uint32_t类型通道无效时返回0
*
* 使用说明:
* - 变化次数从模块初始化后开始累计
* - 可用于检测某通道是否频繁触发或存在故障(如开关抖动)
*/
uint32_t IO_Monitor_GetChangeCount(uint8_t channel)
{
/* 参数边界检查超出范围返回0 */
if (channel >= IO_CHANNEL_COUNT) {
return 0;
}
return di_channels[channel].change_count;
}
/**
* @brief 设置IO事件回调函数
* @note 设置后IO状态变化将通过回调函数上报
*
* @param callback: 回调函数指针NULL则使用默认UART2输出(输入)
* @return 无
*
* 使用说明:
* - 设置回调后send_di_event()将通过回调发送事件
* - 传入NULL恢复默认UART2输出方式
* - 回调函数原型: void callback(uint8_t channel, uint8_t state, const char *event_msg)
*/
void IO_Monitor_SetEventCallback(io_event_callback_t callback)
{
g_event_callback = callback;
DEBUG_LOG("Event callback %s", callback ? "set" : "cleared");
}