/** ****************************************************************************** * @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 "multi_uart_router.h" #include "main.h" #include /*============================================================================== * 调试宏定义 *============================================================================*/ /* 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输出 * * 回调函数原型: * void callback(uint8_t channel, uint8_t state, const char *event_msg) * * 使用场景: * - 需要将事件发送到其他端口时设置回调 * - 传入NULL恢复默认UART2输出方式 */ 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格式的状态变化消息 * 消息格式: $DI_EVENT,,*\r\n * 如果设置了回调函数,则通过回调发送;否则发送到UART2 * * @param channel: 通道编号,从0开始计数(输入) * @param state: 通道状态,0=低电平,1=高电平(输入) * @return 无返回值 * * 事件路由说明: * 1. 始终通过MultiUART_SendString发送到UART1(RF433)端口 * 2. 如果设置了回调函数(g_event_callback),也通过回调发送 * 3. UART2仅用于调试日志输出 */ 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,,*\r\n * 如果设置了回调函数,则通过回调发送;否则发送到UART2 * * @param channel: 通道编号,从0开始计数(输入) * @param state: 通道状态,0=低电平,1=高电平(输入) * @return 无返回值 * * 事件路由说明: * 1. 始终通过MultiUART_SendString发送到UART1(RF433)端口 * 2. 如果设置了回调函数(g_event_callback),也通过回调发送 * 3. UART2仅用于调试日志输出 */ 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); /* 输出调试日志到UART2,记录状态变化 */ DEBUG_LOG("CH%d -> %s", channel + 1, state ? "HIGH" : "LOW"); /*---------------------------------------------------------- * 向UART1(RF433模块)发送状态变化事件 * 这是IO事件的主要路由通道,用于无线上报到上位机 *----------------------------------------------------------*/ MultiUART_SendString(PORT_UART1, msg); /* 输出完整消息到UART2,方便调试查看 */ DEBUG_LOG("RF433 TX: \"%s\"", msg); /*---------------------------------------------------------- * 如果设置了回调函数,也通过回调发送 * 用于支持额外的自定义处理逻辑 *----------------------------------------------------------*/ if (g_event_callback != NULL) { g_event_callback(channel, state, msg); } } /*============================================================================== * 公共函数实现 *============================================================================*/ /** * @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"); }