3.27_433:实现并验证RF433模块接收相应指令:新增UART路由核心模块,使程序能响应RF433/RS485指令,并向UART2输出LOG(RS485由于硬件原因未验证)

This commit is contained in:
2026-03-27 16:21:00 +08:00
parent 71027ebc46
commit c809273bd9
78 changed files with 7188 additions and 2811 deletions

View File

@ -3,16 +3,17 @@
* @file io_monitor.c
* @brief IO状态监控模块实现
* @author Application Layer
* @version 1.1
* @version 1.2
******************************************************************************
* @attention
* 本模块实现四路数字输入的状态监控
* 关键特性:
* 1. 10ms定时扫描平衡响应速度和CPU占用
* 2. 软件去抖连续3次相同状态才确认变化
* 3. 状态变化时自动上报ASCII格式消息
*
* 3. 状态变化时通过回调函数上报,支持多端口路由
*
* 修订历史:
* v1.2 - 增加事件回调机制,支持多端口路由
* v1.1 - 修复审查报告中危-5去抖计数器初始化优化
******************************************************************************
*/
@ -22,23 +23,64 @@
#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;
uint16_t pin;
uint8_t current_state;
uint8_t debounce_counter;
uint8_t last_raw_state;
uint32_t change_count;
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},
@ -46,9 +88,75 @@ static io_channel_t di_channels[IO_CHANNEL_COUNT] = {
{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;
@ -58,63 +166,156 @@ static uint8_t calc_checksum(const char *data, uint8_t len)
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);
UART2_Print_String(msg);
/* 根据是否设置回调函数选择发送方式 */
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++;
} else if (ch->current_state != raw_state) {
}
/* 分支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);
}
@ -123,18 +324,46 @@ void IO_Monitor_Task(void)
}
}
/**
* @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);
@ -143,16 +372,57 @@ uint8_t IO_Monitor_GetAllStates(void)
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");
}