3.27_433:实现并验证RF433模块接收相应指令:新增UART路由核心模块,使程序能响应RF433/RS485指令,并向UART2输出LOG(RS485由于硬件原因未验证)
This commit is contained in:
@ -12,7 +12,7 @@
|
||||
* 2. 完善的安全防护(缓冲区边界检查、超时重置、字符过滤)
|
||||
* 3. 异或校验,FF特权后门
|
||||
* 4. 支持RL、DI、ECHO指令
|
||||
*
|
||||
*
|
||||
* 修订历史:
|
||||
* v1.1 - 修复审查报告高危-6、中危-7/8
|
||||
******************************************************************************
|
||||
@ -27,37 +27,148 @@
|
||||
#include <stdio.h> // snprintf
|
||||
#include <stdlib.h> // atoi
|
||||
|
||||
/*==============================================================================
|
||||
* 调试宏定义
|
||||
*============================================================================*/
|
||||
/* DEBUG_CMD_PARSER: 调试日志开关,置1时启用解析过程日志输出 */
|
||||
#define DEBUG_CMD_PARSER 1
|
||||
|
||||
#if DEBUG_CMD_PARSER
|
||||
/* 调试日志宏,带模块前缀"[CMD]"方便过滤 */
|
||||
#define DEBUG_LOG(fmt, ...) UART2_Print_Printf("[CMD] " fmt "\r\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define DEBUG_LOG(fmt, ...)
|
||||
#endif
|
||||
|
||||
/*==============================================================================
|
||||
* 解析器状态机定义
|
||||
*============================================================================*/
|
||||
/**
|
||||
* @brief 指令解析器状态枚举
|
||||
* @note 状态机各状态定义,描述指令解析的完整生命周期
|
||||
*
|
||||
* 状态转换图:
|
||||
*
|
||||
* [空闲] ---'$'---> [解析命令]
|
||||
* [解析命令] ---','---> [解析参数1]
|
||||
* [解析命令] ---'*'---> [解析校验和]
|
||||
* [解析参数1] ---','---> [解析参数2]
|
||||
* [解析参数1] ---'*'---> [解析校验和]
|
||||
* [解析参数2] ---'*'---> [解析校验和]
|
||||
* [解析校验和] ---'\n'---> [完成]
|
||||
* [完成] ---'$'---> [解析命令]
|
||||
* 任何状态 ---超时---> [空闲]
|
||||
*
|
||||
* 各状态说明:
|
||||
* - PARSE_IDLE: 等待帧起始符'$',处于空闲状态
|
||||
* - PARSE_CMD: 正在解析命令字段(直到遇到','或'*')
|
||||
* - PARSE_PARAM1: 正在解析第一个参数(直到遇到','或'*')
|
||||
* - PARSE_PARAM2: 正在解析第二个参数(直到遇到'*')
|
||||
* - PARSE_CHECKSUM: 正在解析校验和(两个十六进制字符直到'\n')
|
||||
* - PARSE_COMPLETE: 完整帧已解析完成,等待处理
|
||||
*/
|
||||
typedef enum {
|
||||
PARSE_IDLE,
|
||||
PARSE_CMD,
|
||||
PARSE_PARAM1,
|
||||
PARSE_PARAM2,
|
||||
PARSE_CHECKSUM,
|
||||
PARSE_COMPLETE
|
||||
PARSE_IDLE = 0, /**< 空闲状态,等待帧起始 */
|
||||
PARSE_CMD, /**< 正在解析命令字段 */
|
||||
PARSE_PARAM1, /**< 正在解析第一个参数 */
|
||||
PARSE_PARAM2, /**< 正在解析第二个参数 */
|
||||
PARSE_CHECKSUM, /**< 正在解析校验和 */
|
||||
PARSE_COMPLETE /**< 解析完成,可处理 */
|
||||
} parse_state_t;
|
||||
|
||||
/*==============================================================================
|
||||
* 数据结构定义
|
||||
*============================================================================*/
|
||||
/**
|
||||
* @brief 解析器上下文数据结构
|
||||
* @note 保存指令解析的完整状态信息,包括当前状态、已解析字段和统计计数
|
||||
*
|
||||
* 设计目的:
|
||||
* 解析器需要记忆多个中间状态:当前解析状态、已接收的各字段内容、
|
||||
* 校验和累加值、上下文字符索引等。这些信息集中保存在此结构体中,
|
||||
* 实现无状态的逐字节处理(Stateless Byte-by-Byte Processing)模式。
|
||||
*
|
||||
* 字段说明:
|
||||
* - state: 当前解析状态机的状态
|
||||
* - frame: 已解析完成的指令帧结构(cmd_frame_t类型)
|
||||
* - field_index: 当前字段的字符写入位置索引
|
||||
* - checksum_acc: 校验和累加器,逐字节异或得到
|
||||
* - cs_buffer: 接收的校验和字符缓存(两个HEX字符)
|
||||
* - cs_index: 校验和字符缓存的当前写入位置
|
||||
* - last_rx_tick: 上次接收到数据的时间戳(用于超时检测)
|
||||
* - error_count: 解析错误累计次数
|
||||
* - valid_count: 有效指令帧累计次数
|
||||
*/
|
||||
typedef struct {
|
||||
parse_state_t state;
|
||||
cmd_frame_t frame;
|
||||
uint8_t field_index;
|
||||
uint8_t checksum_acc;
|
||||
uint8_t cs_buffer[2];
|
||||
uint8_t cs_index;
|
||||
uint32_t last_rx_tick;
|
||||
uint32_t error_count;
|
||||
uint32_t valid_count;
|
||||
parse_state_t state; /**< 解析状态机当前状态 */
|
||||
cmd_frame_t frame; /**< 已解析指令帧数据 */
|
||||
uint8_t field_index; /**< 当前字段的字符写入位置 */
|
||||
uint8_t checksum_acc; /**< 校验和累加器(异或运算) */
|
||||
uint8_t cs_buffer[2]; /**< 接收的校验和字符缓存 */
|
||||
uint8_t cs_index; /**< 校验和字符缓存写入位置 */
|
||||
uint32_t last_rx_tick; /**< 上次接收时间戳(毫秒) */
|
||||
uint32_t error_count; /**< 解析错误累计次数 */
|
||||
uint32_t valid_count; /**< 有效帧累计次数 */
|
||||
} parser_context_t;
|
||||
|
||||
/*==============================================================================
|
||||
* 全局变量定义
|
||||
*============================================================================*/
|
||||
/**
|
||||
* @brief 解析器上下文实例
|
||||
* @note static修饰确保仅本文件内可访问,保存解析过程全部状态
|
||||
*/
|
||||
static parser_context_t ctx;
|
||||
|
||||
/**
|
||||
* @brief 响应回调函数指针
|
||||
* @note 用于将响应发送到正确的源端口
|
||||
*/
|
||||
static cmd_response_callback_t g_response_callback = NULL;
|
||||
|
||||
/**
|
||||
* @brief 当前源端口ID
|
||||
* @note 记录当前正在解析的指令来自哪个端口
|
||||
*/
|
||||
static uint8_t g_current_source_port = 0;
|
||||
|
||||
/*==============================================================================
|
||||
* 内部静态函数声明
|
||||
*============================================================================*/
|
||||
static void reset_parser(void);
|
||||
static bool is_valid_cmd_char(char c);
|
||||
static bool is_valid_param_char(char c);
|
||||
static uint8_t hex_char_to_val(char c);
|
||||
static uint8_t hex_to_byte(char high, char low);
|
||||
static uint8_t calc_checksum(const char *data, uint8_t len);
|
||||
static void send_response_ok(const char *content);
|
||||
static void send_response_err(const char *err_code);
|
||||
static bool is_str_empty(const char *str);
|
||||
static bool is_str_numeric(const char *str);
|
||||
static void process_cmd_frame(const cmd_frame_t *frame);
|
||||
|
||||
/*==============================================================================
|
||||
* 内部辅助函数实现
|
||||
*============================================================================*/
|
||||
/**
|
||||
* @brief 重置解析器到初始状态
|
||||
* @note 清除所有上下文信息,准备接收新指令帧
|
||||
*
|
||||
* @param 无
|
||||
* @return 无
|
||||
*
|
||||
* 重置内容:
|
||||
* - 状态恢复为PARSE_IDLE
|
||||
* - 字段索引清零
|
||||
* - 校验和累加器清零
|
||||
* - 校验和缓存索引清零
|
||||
* - cmd_frame结构体全部清零
|
||||
*
|
||||
* 调用时机:
|
||||
* - 解析出错时
|
||||
* - 解析完成处理后
|
||||
* - 接收到完整帧后的空闲期
|
||||
*/
|
||||
static void reset_parser(void)
|
||||
{
|
||||
ctx.state = PARSE_IDLE;
|
||||
@ -67,16 +178,59 @@ static void reset_parser(void)
|
||||
memset(&ctx.frame, 0, sizeof(ctx.frame));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 验证命令字符合法性
|
||||
* @note 命令字段只能包含大写字母和数字
|
||||
*
|
||||
* @param c: 待验证的字符(输入)
|
||||
* @return bool: true=合法,false=非法
|
||||
*
|
||||
* 过滤规则:
|
||||
* - 允许: 'A'-'Z', '0'-'9'
|
||||
* - 禁止: 小写字母、标点符号、控制字符等
|
||||
*
|
||||
* 安全意义:
|
||||
* - 限制命令字符集可防止注入攻击
|
||||
* - 简化解析逻辑,避免处理边界情况
|
||||
*/
|
||||
static bool is_valid_cmd_char(char c)
|
||||
{
|
||||
return isupper((unsigned char)c) || isdigit((unsigned char)c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 验证参数字符合法性
|
||||
* @note 参数字段可包含大部分可打印字符,但排除特殊分隔符
|
||||
*
|
||||
* @param c: 待验证的字符(输入)
|
||||
* @return bool: true=合法,false=非法
|
||||
*
|
||||
* 过滤规则:
|
||||
* - 允许: isprint()返回true的字符,但不包括'*'
|
||||
* - 禁止: '*'、'\r'、'\n'及其他不可打印字符
|
||||
*
|
||||
* 特殊说明:
|
||||
* - '*'是校验和起始分隔符,不能出现在参数中
|
||||
* - '\r'和'\n'是帧结束符
|
||||
*/
|
||||
static bool is_valid_param_char(char c)
|
||||
{
|
||||
return isprint((unsigned char)c) && c != '*' && c != '\r' && c != '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 十六进制字符转换为数值
|
||||
* @note 将单个十六进制字符('0'-'9','A'-'F')转换为对应的4位数值
|
||||
*
|
||||
* @param c: 十六进制字符(输入)
|
||||
* @return uint8_t: 转换后的数值(0-15),非法字符返回0
|
||||
*
|
||||
* 转换规则:
|
||||
* '0'-'9' -> 0-9
|
||||
* 'A'-'F' -> 10-15
|
||||
* 'a'-'f' -> 10-15 (小写也支持)
|
||||
* 其他字符 -> 0 (默认处理)
|
||||
*/
|
||||
static uint8_t hex_char_to_val(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
@ -85,11 +239,37 @@ static uint8_t hex_char_to_val(char c)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将两个十六进制字符转换为一个字节
|
||||
* @note 高位字符在前,地位字符在后,组合成完整的字节值
|
||||
*
|
||||
* @param high: 高位十六进制字符(输入)
|
||||
* @param low: 低位十六进制字符(输入)
|
||||
* @return uint8_t: 组合后的字节值(0-255)
|
||||
*
|
||||
* 计算公式:result = (high_nibble << 4) | low_nibble
|
||||
*
|
||||
* 示例:
|
||||
* hex_to_byte('A', 'F') 返回 0xAF (十进制175)
|
||||
*/
|
||||
static uint8_t hex_to_byte(char high, char low)
|
||||
{
|
||||
return (hex_char_to_val(high) << 4) | hex_char_to_val(low);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 计算异或校验和
|
||||
* @note 对输入数据的每个字节执行异或运算,生成校验码
|
||||
*
|
||||
* @param data: 待计算校验和的数据缓冲区(输入)
|
||||
* @param len: 数据长度,以字节为单位(输入)
|
||||
* @return uint8_t: 校验和(异或结果)
|
||||
*
|
||||
* 算法原理:
|
||||
* 遍历数据缓冲区,将每个字节与累加器进行异或操作。
|
||||
* 初始累加器为0,最终结果为所有字节的异或和。
|
||||
* XOR的特性:a^a=0, a^0=a, 具有可逆性,适合简单校验
|
||||
*/
|
||||
static uint8_t calc_checksum(const char *data, uint8_t len)
|
||||
{
|
||||
uint8_t cs = 0;
|
||||
@ -99,40 +279,99 @@ static uint8_t calc_checksum(const char *data, uint8_t len)
|
||||
return cs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送成功响应消息
|
||||
* @note 构造并发送指令执行成功的响应帧
|
||||
*
|
||||
* @param content: 响应内容字符串指针(输入)
|
||||
* @return 无
|
||||
*
|
||||
* 响应帧格式:
|
||||
* $OK,<content>*<checksum>\r\n
|
||||
*
|
||||
* 校验和计算范围:
|
||||
* 从'$'之后到'*'之前的全部内容(不含$和*)
|
||||
*
|
||||
* 使用示例:
|
||||
* send_response_ok("RL,1") -> $OK,RL,1*XX\r\n
|
||||
*/
|
||||
static void send_response_ok(const char *content)
|
||||
{
|
||||
char msg[64];
|
||||
uint8_t cs;
|
||||
|
||||
|
||||
int len = snprintf(msg, sizeof(msg), "$OK,%s*", content);
|
||||
cs = calc_checksum(msg + 1, len - 1);
|
||||
snprintf(msg + len, sizeof(msg) - len, "%02X\r\n", cs);
|
||||
|
||||
UART2_Print_String(msg);
|
||||
|
||||
if (g_response_callback != NULL) {
|
||||
g_response_callback(g_current_source_port, msg);
|
||||
} else {
|
||||
UART2_Print_String(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送错误响应消息
|
||||
* @note 构造并发送指令执行失败的响应帧
|
||||
*
|
||||
* @param err_code: 错误代码字符串(输入),如"PARAM"、"CS"、"CMD"
|
||||
* @return 无
|
||||
*
|
||||
* 响应帧格式:
|
||||
* $ERR,<err_code>*<checksum>\r\n
|
||||
*
|
||||
* 错误代码含义:
|
||||
* - "PARAM": 参数格式或值非法
|
||||
* - "CS": 校验和不匹配
|
||||
* - "CMD": 命令无法识别
|
||||
*/
|
||||
static void send_response_err(const char *err_code)
|
||||
{
|
||||
char msg[32];
|
||||
uint8_t cs;
|
||||
|
||||
|
||||
int len = snprintf(msg, sizeof(msg), "$ERR,%s*", err_code);
|
||||
cs = calc_checksum(msg + 1, len - 1);
|
||||
snprintf(msg + len, sizeof(msg) - len, "%02X\r\n", cs);
|
||||
|
||||
UART2_Print_String(msg);
|
||||
|
||||
if (g_response_callback != NULL) {
|
||||
g_response_callback(g_current_source_port, msg);
|
||||
} else {
|
||||
UART2_Print_String(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 判断字符串是否为空
|
||||
* @note 检查指针是否为NULL或首字符是否为'\0'
|
||||
*
|
||||
* @param str: 待检查的字符串指针(输入)
|
||||
* @return bool: true=空字符串或NULL,false=非空
|
||||
*/
|
||||
static bool is_str_empty(const char *str)
|
||||
{
|
||||
return (str == NULL || str[0] == '\0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 判断字符串是否全为数字
|
||||
* @note 验证字符串中的所有字符是否都是十进制数字
|
||||
*
|
||||
* @param str: 待检查的字符串指针(输入)
|
||||
* @return bool: true=全为数字,false=包含非数字字符或为空
|
||||
*
|
||||
* 实现逻辑:
|
||||
* 从字符串开头遍历到'\0',检查每个字符是否满足isdigit()。
|
||||
* 一旦遇到非数字字符立即返回false。
|
||||
*/
|
||||
static bool is_str_numeric(const char *str)
|
||||
{
|
||||
/* 空字符串检查 */
|
||||
if (is_str_empty(str)) {
|
||||
return false;
|
||||
}
|
||||
/* 逐字符检查是否为数字 */
|
||||
while (*str) {
|
||||
if (!isdigit((unsigned char)*str)) {
|
||||
return false;
|
||||
@ -142,160 +381,322 @@ static bool is_str_numeric(const char *str)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 指令帧处理函数
|
||||
* @note 根据解析出的命令类型分发到相应的处理函数
|
||||
*
|
||||
* @param frame: 已解析完成的指令帧结构指针(输入)
|
||||
* @return 无
|
||||
*
|
||||
* 支持的指令列表:
|
||||
* - RL <relay_id> <state>: 控制继电器开关
|
||||
* - DI [channel]: 查询数字输入状态
|
||||
* - ECHO: 回显测试指令
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 调试日志输出指令详情
|
||||
* 2. 根据cmd字段分发到对应处理分支
|
||||
* 3. 参数合法性校验
|
||||
* 4. 调用业务层函数执行操作
|
||||
* 5. 发送成功/失败响应
|
||||
*/
|
||||
static void process_cmd_frame(const cmd_frame_t *frame)
|
||||
{
|
||||
DEBUG_LOG("CMD=%s P1=%s P2=%s CS=%02X/%02X %s",
|
||||
/*----------------------------------------------------------
|
||||
* 调试日志:打印完整指令信息
|
||||
*----------------------------------------------------------*/
|
||||
DEBUG_LOG("CMD=%s P1=%s P2=%s CS=%02X/%02X %s",
|
||||
frame->cmd, frame->param1, frame->param2,
|
||||
frame->received_cs, frame->calculated_cs,
|
||||
frame->skip_checksum ? "(skip)" : "");
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 继电器控制指令: RL <state>
|
||||
* 格式: $RL,<state>*<checksum>
|
||||
* 说明: 单路继电器,state为0(关)或1(开)
|
||||
*----------------------------------------------------------*/
|
||||
if (strcmp(frame->cmd, "RL") == 0) {
|
||||
if (!is_str_numeric(frame->param1) || !is_str_numeric(frame->param2)) {
|
||||
/* 参数合法性检查:param1必须为数字 */
|
||||
if (!is_str_numeric(frame->param1)) {
|
||||
send_response_err("PARAM");
|
||||
DEBUG_LOG("Invalid RL params: not numeric");
|
||||
DEBUG_LOG("Invalid RL param: not numeric");
|
||||
return;
|
||||
}
|
||||
|
||||
int relay_id = atoi(frame->param1);
|
||||
int state = atoi(frame->param2);
|
||||
|
||||
if (relay_id >= 1 && relay_id <= 4 && (state == 0 || state == 1)) {
|
||||
Relay_SetState(relay_id, state ? true : false);
|
||||
|
||||
|
||||
/* 将参数字符串转换为整数 */
|
||||
int state = atoi(frame->param1);
|
||||
|
||||
/* 状态值范围检查: 0或1 */
|
||||
if (state == 0 || state == 1) {
|
||||
/* 调用业务层函数设置继电器状态 */
|
||||
Relay_SetState(state ? true : false);
|
||||
|
||||
/* 构造并发送成功响应 */
|
||||
char resp[32];
|
||||
snprintf(resp, sizeof(resp), "RL,%d,%d", relay_id, state);
|
||||
snprintf(resp, sizeof(resp), "RL,%d", state);
|
||||
send_response_ok(resp);
|
||||
|
||||
DEBUG_LOG("Relay %d -> %s", relay_id, state ? "ON" : "OFF");
|
||||
|
||||
DEBUG_LOG("Relay -> %s", state ? "ON" : "OFF");
|
||||
} else {
|
||||
/* 参数值超出有效范围 */
|
||||
send_response_err("PARAM");
|
||||
DEBUG_LOG("Invalid RL params: id=%d state=%d", relay_id, state);
|
||||
DEBUG_LOG("Invalid RL param: state=%d", state);
|
||||
}
|
||||
}
|
||||
/*----------------------------------------------------------
|
||||
* 数字输入查询指令: DI [channel]
|
||||
* 格式1(查询全部): $DI*<checksum> 或 $DI,0*<checksum>
|
||||
* 格式2(查询单通道): $DI,<channel>*<checksum>
|
||||
*----------------------------------------------------------*/
|
||||
else if (strcmp(frame->cmd, "DI") == 0) {
|
||||
/*----------------------------------------------------------
|
||||
* 分支1: 查询全部通道状态
|
||||
* param1为空或为"0"时触发
|
||||
*----------------------------------------------------------*/
|
||||
if (is_str_empty(frame->param1) || strcmp(frame->param1, "0") == 0) {
|
||||
/* 获取所有通道状态的组合掩码 */
|
||||
uint8_t states = IO_Monitor_GetAllStates();
|
||||
/* 构造响应消息,将四路状态按位展开为ASCII字符 */
|
||||
char resp[32];
|
||||
snprintf(resp, sizeof(resp), "DI,%d%d%d%d",
|
||||
snprintf(resp, sizeof(resp), "DI,%d%d%d%d",
|
||||
(states >> 0) & 1, (states >> 1) & 1,
|
||||
(states >> 2) & 1, (states >> 3) & 1);
|
||||
send_response_ok(resp);
|
||||
DEBUG_LOG("DI all states: 0x%02X", states);
|
||||
}
|
||||
/*----------------------------------------------------------
|
||||
* 分支2: 查询指定单通道状态
|
||||
* param1为数字时触发
|
||||
*----------------------------------------------------------*/
|
||||
else if (is_str_numeric(frame->param1)) {
|
||||
int channel = atoi(frame->param1);
|
||||
/* 通道编号范围检查: 1-4 */
|
||||
if (channel >= 1 && channel <= 4) {
|
||||
/* 获取该通道的当前状态(内部使用0-base索引) */
|
||||
uint8_t state = IO_Monitor_GetState(channel - 1);
|
||||
char resp[32];
|
||||
snprintf(resp, sizeof(resp), "DI,%d,%d", channel, state);
|
||||
send_response_ok(resp);
|
||||
DEBUG_LOG("DI%d = %d", channel, state);
|
||||
} else {
|
||||
/* 通道编号超出范围 */
|
||||
send_response_err("PARAM");
|
||||
DEBUG_LOG("Invalid DI channel: %d", channel);
|
||||
}
|
||||
}
|
||||
/*----------------------------------------------------------
|
||||
* 分支3: 参数格式非法
|
||||
*----------------------------------------------------------*/
|
||||
else {
|
||||
send_response_err("PARAM");
|
||||
DEBUG_LOG("Invalid DI param: not numeric");
|
||||
}
|
||||
}
|
||||
/*----------------------------------------------------------
|
||||
* 回显测试指令: ECHO
|
||||
* 格式: $ECHO*<checksum>
|
||||
* 用途: 测试通信链路是否正常
|
||||
*----------------------------------------------------------*/
|
||||
else if (strcmp(frame->cmd, "ECHO") == 0) {
|
||||
send_response_ok("ECHO");
|
||||
DEBUG_LOG("ECHO response sent");
|
||||
}
|
||||
/*----------------------------------------------------------
|
||||
* 未知命令处理
|
||||
*----------------------------------------------------------*/
|
||||
else {
|
||||
send_response_err("CMD");
|
||||
DEBUG_LOG("Unknown command: %s", frame->cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/*==============================================================================
|
||||
* 公共函数实现
|
||||
*============================================================================*/
|
||||
/**
|
||||
* @brief 指令解析器初始化
|
||||
* @note 在系统启动时调用,初始化解析器上下文
|
||||
*
|
||||
* @param 无
|
||||
* @return 无
|
||||
*
|
||||
* 初始化内容:
|
||||
* - 使用memset将整个上下文结构清零
|
||||
* - 将状态显式设置为PARSE_IDLE
|
||||
*
|
||||
* 注意:
|
||||
* 全局变量ctx本身是static且初始化为{0},
|
||||
* 此函数调用memset是确保明确初始化,
|
||||
* 防止未来新增字段时出现未初始化问题
|
||||
*/
|
||||
void CmdParser_Init(void)
|
||||
{
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
ctx.state = PARSE_IDLE;
|
||||
|
||||
|
||||
DEBUG_LOG("Init OK");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 向解析器输入一个字节
|
||||
* @note 核心解析函数,实现状态机逻辑,应对每个接收字节调用一次
|
||||
*
|
||||
* @param byte: 待解析的接收字节(输入)
|
||||
* @param current_tick: 当前系统时间戳,毫秒单位(输入)
|
||||
* @return 无
|
||||
*
|
||||
* 算法说明 - 状态机核心逻辑:
|
||||
* 本函数是整个解析器的核心,实现了有限状态机(FSM)。
|
||||
* 每次调用处理一个字节,根据当前状态(ctx.state)和输入字节
|
||||
* 决定状态转换和动作。
|
||||
*
|
||||
* 状态转换详细说明:
|
||||
*
|
||||
* [PARSE_IDLE]:
|
||||
* - byte='$' -> reset_parser(), state=PARSE_CMD
|
||||
* - 其他字节 -> 忽略,保持IDLE
|
||||
*
|
||||
* [PARSE_CMD]:
|
||||
* - byte=',' -> 字段结束,state=PARSE_PARAM1,field_index=0
|
||||
* - byte='*' -> 字段结束,state=PARSE_CHECKSUM,cs_index=0
|
||||
* - is_valid_cmd_char(byte) -> 写入cmd缓冲区,checksum_acc^=byte
|
||||
* - 其他情况 -> error_count++, reset_parser()
|
||||
*
|
||||
* [PARSE_PARAM1]:
|
||||
* - byte=',' -> 字段结束,state=PARSE_PARAM2,field_index=0
|
||||
* - byte='*' -> 字段结束,state=PARSE_CHECKSUM,cs_index=0
|
||||
* - is_valid_param_char(byte) -> 写入param1缓冲区,checksum_acc^=byte
|
||||
* - 其他情况 -> error_count++, reset_parser()
|
||||
*
|
||||
* [PARSE_PARAM2]:
|
||||
* - byte='*' -> 字段结束,state=PARSE_CHECKSUM,cs_index=0
|
||||
* - is_valid_param_char(byte) -> 写入param2缓冲区,checksum_acc^=byte
|
||||
* - 其他情况 -> error_count++, reset_parser()
|
||||
*
|
||||
* [PARSE_CHECKSUM]:
|
||||
* - byte='\n' -> 校验和接收完成,验证并设置state=PARSE_COMPLETE
|
||||
* - byte='\r' -> 忽略(回车符)
|
||||
* - 其他情况 -> 写入cs_buffer[cs_index++],最多2个字符
|
||||
*
|
||||
* [PARSE_COMPLETE]:
|
||||
* - reset_parser()
|
||||
* - byte='$' -> state=PARSE_CMD (允许连续帧)
|
||||
*
|
||||
* 超时处理:
|
||||
* 如果当前状态不是IDLE且距离上次接收超过PARSE_TIMEOUT_MS,
|
||||
* 则判定为接收超时,重置解析器到IDLE状态
|
||||
*/
|
||||
void CmdParser_FeedByte(uint8_t byte, uint32_t current_tick)
|
||||
{
|
||||
/*----------------------------------------------------------
|
||||
* 超时检测
|
||||
* 如果接收到新字节且距离上次接收已超时,则重置解析器
|
||||
*----------------------------------------------------------*/
|
||||
if (ctx.state != PARSE_IDLE && ctx.state != PARSE_COMPLETE) {
|
||||
if (current_tick - ctx.last_rx_tick >= PARSE_TIMEOUT_MS) {
|
||||
ctx.error_count++;
|
||||
DEBUG_LOG("Timeout, reset parser");
|
||||
reset_parser();
|
||||
/* 超时后如果收到'$',立即开始解析新帧 */
|
||||
if (byte == '$') {
|
||||
ctx.state = PARSE_CMD;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* 更新最后接收时间戳 */
|
||||
ctx.last_rx_tick = current_tick;
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 状态机主分支:根据当前状态处理输入字节
|
||||
*----------------------------------------------------------*/
|
||||
switch (ctx.state) {
|
||||
/*----------------------------------------------------------
|
||||
* PARSE_IDLE: 等待帧起始符'$'
|
||||
*----------------------------------------------------------*/
|
||||
case PARSE_IDLE:
|
||||
if (byte == '$') {
|
||||
reset_parser();
|
||||
ctx.state = PARSE_CMD;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* PARSE_CMD: 解析命令字段
|
||||
*----------------------------------------------------------*/
|
||||
case PARSE_CMD:
|
||||
if (byte == ',') {
|
||||
/* 命令字段结束符,遇到逗号转入参数1解析 */
|
||||
ctx.frame.cmd[ctx.field_index] = '\0';
|
||||
ctx.state = PARSE_PARAM1;
|
||||
ctx.field_index = 0;
|
||||
} else if (byte == '*') {
|
||||
/* 无参数命令,遇到'*'直接转入校验和解析 */
|
||||
ctx.frame.cmd[ctx.field_index] = '\0';
|
||||
ctx.state = PARSE_CHECKSUM;
|
||||
ctx.field_index = 0;
|
||||
ctx.cs_index = 0;
|
||||
} else if (is_valid_cmd_char(byte)) {
|
||||
/* 有效的命令字符,写入缓冲区 */
|
||||
if (ctx.field_index < CMD_MAX_LEN - 1) {
|
||||
ctx.frame.cmd[ctx.field_index++] = byte;
|
||||
ctx.checksum_acc ^= byte;
|
||||
ctx.checksum_acc ^= byte; /* 累加到校验和 */
|
||||
} else {
|
||||
/* 缓冲区溢出,命令过长 */
|
||||
ctx.error_count++;
|
||||
reset_parser();
|
||||
}
|
||||
} else {
|
||||
/* 无效字符,命令只能包含大写字母和数字 */
|
||||
ctx.error_count++;
|
||||
reset_parser();
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* PARSE_PARAM1: 解析第一个参数
|
||||
*----------------------------------------------------------*/
|
||||
case PARSE_PARAM1:
|
||||
if (byte == ',') {
|
||||
/* 参数1结束符,遇到逗号转入参数2解析 */
|
||||
ctx.frame.param1[ctx.field_index] = '\0';
|
||||
ctx.state = PARSE_PARAM2;
|
||||
ctx.field_index = 0;
|
||||
} else if (byte == '*') {
|
||||
/* 参数1结束符,遇到'*'直接转入校验和解析 */
|
||||
ctx.frame.param1[ctx.field_index] = '\0';
|
||||
ctx.state = PARSE_CHECKSUM;
|
||||
ctx.field_index = 0;
|
||||
ctx.cs_index = 0;
|
||||
} else if (is_valid_param_char(byte)) {
|
||||
/* 有效参数字符,写入缓冲区 */
|
||||
if (ctx.field_index < PARAM_MAX_LEN - 1) {
|
||||
ctx.frame.param1[ctx.field_index++] = byte;
|
||||
ctx.checksum_acc ^= byte;
|
||||
} else {
|
||||
/* 缓冲区溢出,参数过长 */
|
||||
ctx.error_count++;
|
||||
reset_parser();
|
||||
}
|
||||
} else {
|
||||
/* 无效字符 */
|
||||
ctx.error_count++;
|
||||
reset_parser();
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* PARSE_PARAM2: 解析第二个参数
|
||||
*----------------------------------------------------------*/
|
||||
case PARSE_PARAM2:
|
||||
if (byte == '*') {
|
||||
/* 参数2结束符,遇到'*'转入校验和解析 */
|
||||
ctx.frame.param2[ctx.field_index] = '\0';
|
||||
ctx.state = PARSE_CHECKSUM;
|
||||
ctx.field_index = 0;
|
||||
ctx.cs_index = 0;
|
||||
} else if (is_valid_param_char(byte)) {
|
||||
/* 有效参数字符,写入缓冲区 */
|
||||
if (ctx.field_index < PARAM_MAX_LEN - 1) {
|
||||
ctx.frame.param2[ctx.field_index++] = byte;
|
||||
ctx.checksum_acc ^= byte;
|
||||
@ -308,34 +709,51 @@ void CmdParser_FeedByte(uint8_t byte, uint32_t current_tick)
|
||||
reset_parser();
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* PARSE_CHECKSUM: 解析校验和
|
||||
*----------------------------------------------------------*/
|
||||
case PARSE_CHECKSUM:
|
||||
if (byte == '\n') {
|
||||
/* 帧结束符'\n'到达,校验和解析完成 */
|
||||
/* 将两个十六进制字符转换为字节 */
|
||||
ctx.frame.received_cs = hex_to_byte(ctx.cs_buffer[0], ctx.cs_buffer[1]);
|
||||
ctx.frame.calculated_cs = ctx.checksum_acc;
|
||||
/* 0xFF作为特殊值,跳过校验和验证(后门) */
|
||||
ctx.frame.skip_checksum = (ctx.frame.received_cs == 0xFF);
|
||||
|
||||
if (ctx.frame.skip_checksum ||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 校验和验证
|
||||
*----------------------------------------------------------*/
|
||||
if (ctx.frame.skip_checksum ||
|
||||
ctx.frame.received_cs == ctx.frame.calculated_cs) {
|
||||
/* 校验通过,设置帧有效标志 */
|
||||
ctx.frame.valid = true;
|
||||
ctx.state = PARSE_COMPLETE;
|
||||
ctx.valid_count++;
|
||||
} else {
|
||||
/* 校验失败,发送错误响应 */
|
||||
ctx.error_count++;
|
||||
DEBUG_LOG("Checksum error: recv=%02X calc=%02X",
|
||||
DEBUG_LOG("Checksum error: recv=%02X calc=%02X",
|
||||
ctx.frame.received_cs, ctx.frame.calculated_cs);
|
||||
send_response_err("CS");
|
||||
reset_parser();
|
||||
}
|
||||
} else if (byte != '\r') {
|
||||
/* 忽略回车符'\r',接收校验和字符(最多2个HEX) */
|
||||
if (ctx.cs_index < 2) {
|
||||
ctx.cs_buffer[ctx.cs_index++] = byte;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* PARSE_COMPLETE: 解析完成状态
|
||||
*----------------------------------------------------------*/
|
||||
case PARSE_COMPLETE:
|
||||
/* 重置解析器,准备接收下一帧 */
|
||||
reset_parser();
|
||||
/* 如果立即收到'$',允许连续帧解析 */
|
||||
if (byte == '$') {
|
||||
ctx.state = PARSE_CMD;
|
||||
}
|
||||
@ -343,6 +761,27 @@ void CmdParser_FeedByte(uint8_t byte, uint32_t current_tick)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 指令解析器任务函数
|
||||
* @note 在主循环中周期性调用,处理已解析完成的指令帧
|
||||
*
|
||||
* @param 无
|
||||
* @return 无
|
||||
*
|
||||
* 工作模式:
|
||||
* CmdParser_FeedByte()负责接收字节并解析,
|
||||
* 此函数负责在帧解析完成后执行相应的业务处理。
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 检查是否处于PARSE_COMPLETE状态
|
||||
* 2. 检查帧是否valid
|
||||
* 3. 调用process_cmd_frame()执行指令
|
||||
* 4. 重置解析器到IDLE状态
|
||||
*
|
||||
* 注意:
|
||||
* 此设计将"接收解析"与"业务处理"分离,
|
||||
* 避免了中断处理函数中执行复杂业务逻辑
|
||||
*/
|
||||
void CmdParser_Task(void)
|
||||
{
|
||||
if (ctx.state == PARSE_COMPLETE && ctx.frame.valid) {
|
||||
@ -351,6 +790,21 @@ void CmdParser_Task(void)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询是否存在已完成的指令帧
|
||||
* @note 非阻塞查询接口,供外部模块检查是否有待处理帧
|
||||
*
|
||||
* @param frame: 帧数据输出指针,如果不为NULL则复制帧数据(输出)
|
||||
* @return bool: true=存在有效帧,false=无有效帧
|
||||
*
|
||||
* 使用场景:
|
||||
* 外部模块(如主循环)可轮询此函数,
|
||||
* 当返回true时获取帧数据进行处理
|
||||
*
|
||||
* 注意:
|
||||
* 此函数只是查询,不自动清除帧数据。
|
||||
* 帧数据的清除需要调用CmdParser_Acknowledge()
|
||||
*/
|
||||
bool CmdParser_HasCompleteFrame(cmd_frame_t *frame)
|
||||
{
|
||||
if (ctx.state == PARSE_COMPLETE && ctx.frame.valid) {
|
||||
@ -362,17 +816,81 @@ bool CmdParser_HasCompleteFrame(cmd_frame_t *frame)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 确认并清除已完成的指令帧
|
||||
* @note 在处理完一帧后调用,重置解析器到初始状态
|
||||
*
|
||||
* @param 无
|
||||
* @return 无
|
||||
*
|
||||
* 使用说明:
|
||||
* 当外部模块通过CmdParser_HasCompleteFrame获取帧数据后,
|
||||
* 处理完毕应调用此函数清除状态,防止重复处理
|
||||
*/
|
||||
void CmdParser_Acknowledge(void)
|
||||
{
|
||||
reset_parser();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取解析错误累计次数
|
||||
* @note 用于诊断通信质量或协议错误统计
|
||||
*
|
||||
* @param 无
|
||||
* @return uint32_t: 错误帧累计次数
|
||||
*
|
||||
* 错误类型包括:
|
||||
* - 字符验证失败
|
||||
* - 缓冲区溢出
|
||||
* - 校验和不匹配
|
||||
* - 接收超时
|
||||
*/
|
||||
uint32_t CmdParser_GetErrorCount(void)
|
||||
{
|
||||
return ctx.error_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取有效帧累计次数
|
||||
* @note 用于诊断通信成功率统计
|
||||
*
|
||||
* @param 无
|
||||
* @return uint32_t: 有效帧累计次数
|
||||
*
|
||||
* 计算公式:成功率 = valid_count / (valid_count + error_count)
|
||||
*/
|
||||
uint32_t CmdParser_GetValidCount(void)
|
||||
{
|
||||
return ctx.valid_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置响应回调函数
|
||||
* @note 用于将响应路由到正确的源端口
|
||||
*
|
||||
* @param callback: 回调函数指针
|
||||
* @return 无
|
||||
*
|
||||
* 回调函数原型:
|
||||
* void callback(uint8_t source_port, const char *response)
|
||||
*
|
||||
* 参数说明:
|
||||
* - source_port: 指令来源端口ID
|
||||
* - response: 待发送的响应字符串
|
||||
*/
|
||||
void CmdParser_SetResponseCallback(cmd_response_callback_t callback)
|
||||
{
|
||||
g_response_callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置当前源端口
|
||||
* @note 在开始解析新指令前调用,记录指令来源
|
||||
*
|
||||
* @param port_id: 端口ID
|
||||
* @return 无
|
||||
*/
|
||||
void CmdParser_SetSourcePort(uint8_t port_id)
|
||||
{
|
||||
g_current_source_port = port_id;
|
||||
}
|
||||
|
||||
222
Core/Src/cmd_router.c
Normal file
222
Core/Src/cmd_router.c
Normal file
@ -0,0 +1,222 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file cmd_router.c
|
||||
* @brief 指令路由与响应分发模块实现
|
||||
* @author Application Layer
|
||||
* @version 1.0
|
||||
******************************************************************************
|
||||
* @attention
|
||||
* 本模块实现指令路由与响应分发功能
|
||||
* 设计依据:多通信接口统一指令处理系统开发计划 第3.2节
|
||||
*
|
||||
* 核心职责:
|
||||
* - 从各UART端口读取数据并喂入解析器
|
||||
* - 根据指令来源端口路由响应
|
||||
* - 管理响应路由表
|
||||
*
|
||||
* 工作流程:
|
||||
* 1. 中断中将接收字节写入对应端口的环形缓冲区
|
||||
* 2. 主循环中CmdRouter_Task轮询各端口缓冲区
|
||||
* 3. 读取字节喂入CmdParser解析器
|
||||
* 4. 解析完成后通过回调发送响应到源端口
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "cmd_router.h"
|
||||
#include "multi_uart_router.h"
|
||||
#include "cmd_parser.h"
|
||||
#include "uart2_print.h"
|
||||
#include "debug_log.h"
|
||||
#include <string.h>
|
||||
|
||||
#define DEBUG_CMD_ROUTER 1
|
||||
|
||||
#if DEBUG_CMD_ROUTER
|
||||
#define ROUTER_LOG(fmt, ...) UART2_Print_Printf("[ROUTER] " fmt "\r\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define ROUTER_LOG(fmt, ...)
|
||||
#endif
|
||||
|
||||
#define RX_LOG_BUFFER_SIZE 128
|
||||
|
||||
typedef struct {
|
||||
uint8_t source_port;
|
||||
uint32_t last_feed_tick;
|
||||
uint32_t rx_count;
|
||||
bool active;
|
||||
} port_parser_state_t;
|
||||
|
||||
static port_parser_state_t g_port_states[PORT_COUNT];
|
||||
|
||||
static cmd_response_handler_t g_response_handler = NULL;
|
||||
|
||||
static uint32_t g_processed_count = 0;
|
||||
static uint32_t g_routed_count = 0;
|
||||
|
||||
static uint8_t g_current_parsing_port = PORT_UART2;
|
||||
|
||||
static uint8_t g_rx_log_buffer[PORT_COUNT][RX_LOG_BUFFER_SIZE];
|
||||
static uint16_t g_rx_log_len[PORT_COUNT];
|
||||
static uint32_t g_rx_log_last_tick[PORT_COUNT];
|
||||
#define RX_LOG_TIMEOUT_MS 50
|
||||
|
||||
static void flush_rx_log(port_id_t port_id)
|
||||
{
|
||||
if (g_rx_log_len[port_id] == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
UART2_Print_Printf("[ROUTER] %s RX: \"", MultiUART_GetPortName(port_id));
|
||||
|
||||
for (uint16_t i = 0; i < g_rx_log_len[port_id]; i++) {
|
||||
uint8_t c = g_rx_log_buffer[port_id][i];
|
||||
if (c >= 0x20 && c < 0x7F) {
|
||||
UART2_Print_Send(&c, 1);
|
||||
} else if (c == '\r') {
|
||||
UART2_Print_Send((const uint8_t *)"\\r", 2);
|
||||
} else if (c == '\n') {
|
||||
UART2_Print_Send((const uint8_t *)"\\n", 2);
|
||||
} else {
|
||||
char hex[4];
|
||||
snprintf(hex, sizeof(hex), "\\x%02X", c);
|
||||
UART2_Print_Send((const uint8_t *)hex, 4);
|
||||
}
|
||||
}
|
||||
|
||||
UART2_Print_String("\"\r\n");
|
||||
|
||||
g_rx_log_len[port_id] = 0;
|
||||
}
|
||||
|
||||
static void append_rx_log(port_id_t port_id, uint8_t byte)
|
||||
{
|
||||
if (g_rx_log_len[port_id] < RX_LOG_BUFFER_SIZE) {
|
||||
g_rx_log_buffer[port_id][g_rx_log_len[port_id]++] = byte;
|
||||
}
|
||||
g_rx_log_last_tick[port_id] = HAL_GetTick();
|
||||
}
|
||||
|
||||
static void cmd_parser_response_callback(uint8_t source_port, const char *response)
|
||||
{
|
||||
if (g_response_handler != NULL) {
|
||||
g_response_handler((port_id_t)source_port, response, strlen(response));
|
||||
g_routed_count++;
|
||||
} else {
|
||||
MultiUART_SendString((port_id_t)source_port, response);
|
||||
g_routed_count++;
|
||||
}
|
||||
|
||||
LOG_INFO("ROUTER", "TX[%s]: %s",
|
||||
MultiUART_GetPortName((port_id_t)source_port), response);
|
||||
}
|
||||
|
||||
void CmdRouter_Init(void)
|
||||
{
|
||||
for (port_id_t i = 0; i < PORT_COUNT; i++) {
|
||||
g_port_states[i].source_port = i;
|
||||
g_port_states[i].last_feed_tick = 0;
|
||||
g_port_states[i].rx_count = 0;
|
||||
g_port_states[i].active = false;
|
||||
|
||||
g_rx_log_len[i] = 0;
|
||||
g_rx_log_last_tick[i] = 0;
|
||||
}
|
||||
|
||||
g_response_handler = NULL;
|
||||
g_processed_count = 0;
|
||||
g_routed_count = 0;
|
||||
g_current_parsing_port = PORT_UART2;
|
||||
|
||||
CmdParser_SetResponseCallback(cmd_parser_response_callback);
|
||||
|
||||
ROUTER_LOG("Init OK, %d ports registered", PORT_COUNT);
|
||||
}
|
||||
|
||||
void CmdRouter_Task(void)
|
||||
{
|
||||
uint32_t current_tick = HAL_GetTick();
|
||||
|
||||
for (port_id_t port_id = 0; port_id < PORT_COUNT; port_id++) {
|
||||
if (port_id == PORT_UART2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (g_rx_log_len[port_id] > 0 &&
|
||||
(current_tick - g_rx_log_last_tick[port_id]) >= RX_LOG_TIMEOUT_MS) {
|
||||
flush_rx_log(port_id);
|
||||
}
|
||||
}
|
||||
|
||||
for (port_id_t port_id = 0; port_id < PORT_COUNT; port_id++) {
|
||||
if (port_id == PORT_UART2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint16_t rx_count = MultiUART_GetRxCount(port_id);
|
||||
|
||||
if (rx_count == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
g_current_parsing_port = port_id;
|
||||
CmdParser_SetSourcePort(port_id);
|
||||
|
||||
uint8_t byte;
|
||||
while (MultiUART_ReadByte(port_id, &byte) > 0) {
|
||||
append_rx_log(port_id, byte);
|
||||
CmdParser_FeedByte(byte, HAL_GetTick());
|
||||
g_port_states[port_id].rx_count++;
|
||||
}
|
||||
|
||||
if (CmdParser_HasCompleteFrame(NULL)) {
|
||||
flush_rx_log(port_id);
|
||||
g_processed_count++;
|
||||
g_port_states[port_id].active = true;
|
||||
}
|
||||
}
|
||||
|
||||
CmdParser_Task();
|
||||
}
|
||||
|
||||
void CmdRouter_SetResponseHandler(cmd_response_handler_t handler)
|
||||
{
|
||||
g_response_handler = handler;
|
||||
ROUTER_LOG("Response handler %s", handler ? "set" : "cleared");
|
||||
}
|
||||
|
||||
void CmdRouter_SendResponse(port_id_t port, const char *response, uint16_t len)
|
||||
{
|
||||
if (port >= PORT_COUNT || response == NULL || len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
MultiUART_Send(port, (const uint8_t *)response, len);
|
||||
g_routed_count++;
|
||||
|
||||
LOG_INFO("ROUTER", "SendResponse[%s]: %.*s",
|
||||
MultiUART_GetPortName(port), len, response);
|
||||
}
|
||||
|
||||
void CmdRouter_BroadcastResponse(const char *response, uint16_t len)
|
||||
{
|
||||
if (response == NULL || len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (port_id_t port = 0; port < PORT_COUNT; port++) {
|
||||
MultiUART_Send(port, (const uint8_t *)response, len);
|
||||
g_routed_count++;
|
||||
}
|
||||
|
||||
LOG_INFO("ROUTER", "Broadcast: %.*s", len, response);
|
||||
}
|
||||
|
||||
uint32_t CmdRouter_GetProcessedCount(void)
|
||||
{
|
||||
return g_processed_count;
|
||||
}
|
||||
|
||||
uint32_t CmdRouter_GetRoutedCount(void)
|
||||
{
|
||||
return g_routed_count;
|
||||
}
|
||||
148
Core/Src/debug_log.c
Normal file
148
Core/Src/debug_log.c
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file debug_log.c
|
||||
* @brief 增强型调试日志系统实现
|
||||
* @author Application Layer
|
||||
* @version 1.0
|
||||
******************************************************************************
|
||||
* @attention
|
||||
* 本模块实现增强型调试日志系统
|
||||
* 设计依据:多通信接口统一指令处理系统开发计划 第3.4节
|
||||
*
|
||||
* 日志格式:[LEVEL][MODULE] message
|
||||
* 示例:[INFO][CMD] Command received: RL
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "debug_log.h"
|
||||
#include "uart2_print.h"
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define MAX_MODULES 16
|
||||
#define MODULE_NAME_LEN 16
|
||||
|
||||
typedef struct {
|
||||
char name[MODULE_NAME_LEN];
|
||||
bool enabled;
|
||||
} module_config_t;
|
||||
|
||||
static log_level_t g_current_level = LOG_LEVEL_DEBUG;
|
||||
static module_config_t g_modules[MAX_MODULES];
|
||||
static bool g_modules_initialized = false;
|
||||
|
||||
static const char *const g_level_str[] = {
|
||||
[LOG_LEVEL_DEBUG] = "DEBUG",
|
||||
[LOG_LEVEL_INFO] = "INFO ",
|
||||
[LOG_LEVEL_WARN] = "WARN ",
|
||||
[LOG_LEVEL_ERROR] = "ERROR",
|
||||
};
|
||||
|
||||
static void init_modules(void)
|
||||
{
|
||||
if (g_modules_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < MAX_MODULES; i++) {
|
||||
g_modules[i].name[0] = '\0';
|
||||
g_modules[i].enabled = false;
|
||||
}
|
||||
|
||||
g_modules_initialized = true;
|
||||
}
|
||||
|
||||
static bool is_module_enabled(const char *module)
|
||||
{
|
||||
if (module == NULL || module[0] == '\0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
init_modules();
|
||||
|
||||
for (int i = 0; i < MAX_MODULES; i++) {
|
||||
if (g_modules[i].name[0] != '\0' &&
|
||||
strncmp(g_modules[i].name, module, MODULE_NAME_LEN - 1) == 0) {
|
||||
return g_modules[i].enabled;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DebugLog_Init(void)
|
||||
{
|
||||
g_current_level = LOG_LEVEL_DEBUG;
|
||||
init_modules();
|
||||
|
||||
UART2_Print_String("[LOG] Debug log system initialized\r\n");
|
||||
}
|
||||
|
||||
void DebugLog_SetLevel(log_level_t level)
|
||||
{
|
||||
g_current_level = level;
|
||||
}
|
||||
|
||||
log_level_t DebugLog_GetLevel(void)
|
||||
{
|
||||
return g_current_level;
|
||||
}
|
||||
|
||||
void DebugLog_EnableModule(const char *module, bool enable)
|
||||
{
|
||||
if (module == NULL || module[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
init_modules();
|
||||
|
||||
for (int i = 0; i < MAX_MODULES; i++) {
|
||||
if (g_modules[i].name[0] == '\0') {
|
||||
strncpy(g_modules[i].name, module, MODULE_NAME_LEN - 1);
|
||||
g_modules[i].name[MODULE_NAME_LEN - 1] = '\0';
|
||||
g_modules[i].enabled = enable;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strncmp(g_modules[i].name, module, MODULE_NAME_LEN - 1) == 0) {
|
||||
g_modules[i].enabled = enable;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DebugLog_Output(log_level_t level, const char *module, const char *fmt, ...)
|
||||
{
|
||||
if (level < g_current_level) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_module_enabled(module)) {
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[256];
|
||||
int len = 0;
|
||||
|
||||
len += snprintf(buffer + len, sizeof(buffer) - len, "[%s][%s] ",
|
||||
g_level_str[level], module ? module : "MAIN");
|
||||
|
||||
if (len < (int)sizeof(buffer) - 1) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
len += vsnprintf(buffer + len, sizeof(buffer) - len, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
if (len > 0 && len < (int)sizeof(buffer)) {
|
||||
buffer[len++] = '\r';
|
||||
if (len < (int)sizeof(buffer)) {
|
||||
buffer[len++] = '\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (len > 0) {
|
||||
UART2_Print_Send((const uint8_t *)buffer, len);
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -34,6 +34,11 @@
|
||||
#include "cmd_parser.h"
|
||||
#include "relay_control.h"
|
||||
|
||||
/* 多通信接口统一指令处理系统模块 */
|
||||
#include "multi_uart_router.h"
|
||||
#include "cmd_router.h"
|
||||
#include "debug_log.h"
|
||||
|
||||
#if (RF433_MODE == RF433_MODE_TX) || (RF433_MODE == RF433_MODE_BOTH)
|
||||
#include "rf433_tx_app.h"
|
||||
#endif
|
||||
@ -62,6 +67,7 @@
|
||||
|
||||
/* USER CODE BEGIN PV */
|
||||
static uint8_t uart2_rx_byte = 0;
|
||||
static uint8_t uart3_rx_byte = 0;
|
||||
/* USER CODE END PV */
|
||||
|
||||
/* Private function prototypes -----------------------------------------------*/
|
||||
@ -112,9 +118,17 @@ int main(void)
|
||||
CmdParser_Init();
|
||||
Relay_Init();
|
||||
|
||||
/* 初始化多通信接口统一指令处理系统 */
|
||||
MultiUART_Init();
|
||||
CmdRouter_Init();
|
||||
DebugLog_Init();
|
||||
|
||||
/* 启动UART2接收中断 */
|
||||
HAL_UART_Receive_IT(&huart2, &uart2_rx_byte, 1);
|
||||
|
||||
/* 启动UART3接收中断 - RS485接口 */
|
||||
HAL_UART_Receive_IT(&huart3, &uart3_rx_byte, 1);
|
||||
|
||||
/* 初始化RF433模块 - 使用默认配置 */
|
||||
rf433_init(NULL);
|
||||
|
||||
@ -155,6 +169,10 @@ int main(void)
|
||||
IO_Monitor_Task();
|
||||
CmdParser_Task();
|
||||
|
||||
/* 多通信接口统一指令处理系统任务 */
|
||||
MultiUART_Task();
|
||||
CmdRouter_Task();
|
||||
|
||||
#if (RF433_MODE == RF433_MODE_TX) || (RF433_MODE == RF433_MODE_BOTH)
|
||||
/* TX任务 */
|
||||
rf433_tx_app_task();
|
||||
@ -212,7 +230,7 @@ void SystemClock_Config(void)
|
||||
|
||||
/**
|
||||
* @brief UART接收完成中断回调函数
|
||||
* @note 处理UART1(RF433)和UART2(调试口)的接收数据
|
||||
* @note 处理UART1(RF433)、UART2(调试口)和UART3(RS485)的接收数据
|
||||
* @param huart: UART句柄指针
|
||||
* @retval 无
|
||||
*/
|
||||
@ -220,31 +238,56 @@ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
|
||||
{
|
||||
if (huart->Instance == USART1)
|
||||
{
|
||||
/* 调用RF433模块的UART接收回调 */
|
||||
/* 先保存接收到的字节,避免被rf433_hal回调覆盖 */
|
||||
uint8_t rx_byte = rf433_uart_rx_tmp;
|
||||
|
||||
/* 调用RF433模块的UART接收回调(内部会重启接收) */
|
||||
rf433_hal_uart_rxcplt_callback();
|
||||
|
||||
/* 喂入多UART路由器,支持RF433指令接收 */
|
||||
MultiUART_FeedByte(PORT_UART1, rx_byte);
|
||||
}
|
||||
else if (huart->Instance == USART2)
|
||||
{
|
||||
/* 喂入指令解析器 */
|
||||
/* 喂入指令解析器 - UART2保持原有处理方式 */
|
||||
CmdParser_FeedByte(uart2_rx_byte, HAL_GetTick());
|
||||
|
||||
/* 重新启动接收 */
|
||||
HAL_UART_Receive_IT(&huart2, &uart2_rx_byte, 1);
|
||||
}
|
||||
else if (huart->Instance == USART3)
|
||||
{
|
||||
/* 喂入多UART路由器 - RS485接口 */
|
||||
MultiUART_FeedByte(PORT_UART3, uart3_rx_byte);
|
||||
|
||||
/* 重新启动接收 */
|
||||
HAL_UART_Receive_IT(&huart3, &uart3_rx_byte, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief UART发送完成中断回调函数
|
||||
* @note 处理UART2发送完成,触发下一次发送
|
||||
* @note 处理UART1/UART2/UART3发送完成,触发下一次发送
|
||||
* @param huart: UART句柄指针
|
||||
* @retval 无
|
||||
*/
|
||||
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
|
||||
{
|
||||
if (huart->Instance == USART2)
|
||||
if (huart->Instance == USART1)
|
||||
{
|
||||
/* 调用多UART路由器的UART1发送完成回调 */
|
||||
MultiUART_TxCpltCallback(PORT_UART1);
|
||||
}
|
||||
else if (huart->Instance == USART2)
|
||||
{
|
||||
/* 调用UART2打印模块的发送完成回调 */
|
||||
UART2_Print_TxCpltCallback();
|
||||
}
|
||||
else if (huart->Instance == USART3)
|
||||
{
|
||||
/* 调用多UART路由器的UART3发送完成回调 */
|
||||
MultiUART_TxCpltCallback(PORT_UART3);
|
||||
}
|
||||
}
|
||||
|
||||
/* USER CODE END 4 */
|
||||
|
||||
351
Core/Src/multi_uart_router.c
Normal file
351
Core/Src/multi_uart_router.c
Normal file
@ -0,0 +1,351 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file multi_uart_router.c
|
||||
* @brief 多UART统一路由核心模块实现
|
||||
* @author Application Layer
|
||||
* @version 1.0
|
||||
******************************************************************************
|
||||
* @attention
|
||||
* 本模块实现多UART端口的统一管理
|
||||
* 设计依据:多通信接口统一指令处理系统开发计划 第3.1、3.3、3.5节及附录A
|
||||
*
|
||||
* 关键特性:
|
||||
* 1. 中断+环形缓冲区接收机制
|
||||
* 2. 非阻塞发送机制
|
||||
* 3. 端口上下文表管理
|
||||
* 4. 响应路由表
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "multi_uart_router.h"
|
||||
#include "uart2_print.h"
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define DEBUG_MULTI_UART 1
|
||||
|
||||
#if DEBUG_MULTI_UART
|
||||
#define DEBUG_LOG(fmt, ...) UART2_Print_Printf("[MUART] " fmt "\r\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define DEBUG_LOG(fmt, ...)
|
||||
#endif
|
||||
|
||||
static uart_port_context_t g_port_ctx[PORT_COUNT];
|
||||
|
||||
static UART_HandleTypeDef *const g_port_uart_map[PORT_COUNT] = {
|
||||
[PORT_UART1] = &huart1,
|
||||
[PORT_UART2] = &huart2,
|
||||
[PORT_UART3] = &huart3,
|
||||
};
|
||||
|
||||
static const char *const g_port_name_map[PORT_COUNT] = {
|
||||
[PORT_UART1] = "UART1",
|
||||
[PORT_UART2] = "UART2",
|
||||
[PORT_UART3] = "UART3",
|
||||
};
|
||||
|
||||
static void rx_ring_init(uart_rx_ring_t *ring)
|
||||
{
|
||||
ring->head = 0;
|
||||
ring->tail = 0;
|
||||
ring->count = 0;
|
||||
ring->overflow_count = 0;
|
||||
}
|
||||
|
||||
static void tx_ring_init(uart_tx_ring_t *ring)
|
||||
{
|
||||
ring->head = 0;
|
||||
ring->tail = 0;
|
||||
ring->count = 0;
|
||||
ring->is_sending = false;
|
||||
ring->overflow_count = 0;
|
||||
}
|
||||
|
||||
static bool rx_ring_push(uart_rx_ring_t *ring, uint8_t byte)
|
||||
{
|
||||
bool success = true;
|
||||
|
||||
__disable_irq();
|
||||
if (ring->count >= UART_RX_BUFFER_SIZE) {
|
||||
ring->overflow_count++;
|
||||
success = false;
|
||||
} else {
|
||||
ring->buffer[ring->head] = byte;
|
||||
ring->head = (ring->head + 1) % UART_RX_BUFFER_SIZE;
|
||||
ring->count++;
|
||||
}
|
||||
__enable_irq();
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
static uint16_t rx_ring_pop(uart_rx_ring_t *ring, uint8_t *byte)
|
||||
{
|
||||
uint16_t result = 0;
|
||||
|
||||
__disable_irq();
|
||||
if (ring->count > 0) {
|
||||
*byte = ring->buffer[ring->tail];
|
||||
ring->tail = (ring->tail + 1) % UART_RX_BUFFER_SIZE;
|
||||
ring->count--;
|
||||
result = 1;
|
||||
}
|
||||
__enable_irq();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static uint16_t tx_ring_push(uart_tx_ring_t *ring, const uint8_t *data, uint16_t len)
|
||||
{
|
||||
uint16_t written = 0;
|
||||
|
||||
__disable_irq();
|
||||
for (uint16_t i = 0; i < len; i++) {
|
||||
if (ring->count >= UART_TX_BUFFER_SIZE) {
|
||||
ring->overflow_count++;
|
||||
break;
|
||||
}
|
||||
ring->buffer[ring->head] = data[i];
|
||||
ring->head = (ring->head + 1) % UART_TX_BUFFER_SIZE;
|
||||
ring->count++;
|
||||
written++;
|
||||
}
|
||||
__enable_irq();
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
static void tx_kickoff(port_id_t port_id)
|
||||
{
|
||||
uart_port_context_t *ctx = &g_port_ctx[port_id];
|
||||
uart_tx_ring_t *ring = &ctx->tx_ring;
|
||||
uint8_t byte;
|
||||
bool has_data = false;
|
||||
|
||||
__disable_irq();
|
||||
if (!ring->is_sending && ring->count > 0) {
|
||||
byte = ring->buffer[ring->tail];
|
||||
ring->tail = (ring->tail + 1) % UART_TX_BUFFER_SIZE;
|
||||
ring->count--;
|
||||
ring->is_sending = true;
|
||||
has_data = true;
|
||||
}
|
||||
__enable_irq();
|
||||
|
||||
if (has_data) {
|
||||
HAL_UART_Transmit_IT(ctx->huart, &byte, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiUART_Init(void)
|
||||
{
|
||||
for (port_id_t i = 0; i < PORT_COUNT; i++) {
|
||||
uart_port_context_t *ctx = &g_port_ctx[i];
|
||||
|
||||
ctx->huart = g_port_uart_map[i];
|
||||
ctx->name = g_port_name_map[i];
|
||||
|
||||
rx_ring_init(&ctx->rx_ring);
|
||||
tx_ring_init(&ctx->tx_ring);
|
||||
|
||||
ctx->rx_tmp = 0;
|
||||
ctx->rx_count = 0;
|
||||
ctx->tx_count = 0;
|
||||
ctx->error_count = 0;
|
||||
ctx->initialized = true;
|
||||
}
|
||||
|
||||
DEBUG_LOG("Init OK, %d ports configured", PORT_COUNT);
|
||||
}
|
||||
|
||||
void MultiUART_FeedByte(port_id_t port_id, uint8_t byte)
|
||||
{
|
||||
// DEBUG_LOG("FeedByte: %02X", byte);
|
||||
if (port_id >= PORT_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
uart_port_context_t *ctx = &g_port_ctx[port_id];
|
||||
|
||||
if (!ctx->initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rx_ring_push(&ctx->rx_ring, byte)) {
|
||||
ctx->error_count++;
|
||||
DEBUG_LOG("%s RX overflow", ctx->name);
|
||||
}
|
||||
|
||||
ctx->rx_count++;
|
||||
}
|
||||
|
||||
void MultiUART_Task(void)
|
||||
{
|
||||
for (port_id_t i = 0; i < PORT_COUNT; i++) {
|
||||
uart_port_context_t *ctx = &g_port_ctx[i];
|
||||
|
||||
if (!ctx->initialized) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i == PORT_UART2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tx_kickoff(i);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiUART_Send(port_id_t port_id, const uint8_t *data, uint16_t len)
|
||||
{
|
||||
if (port_id >= PORT_COUNT || data == NULL || len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (port_id == PORT_UART2) {
|
||||
UART2_Print_Send(data, len);
|
||||
return;
|
||||
}
|
||||
|
||||
uart_port_context_t *ctx = &g_port_ctx[port_id];
|
||||
|
||||
if (!ctx->initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t written = tx_ring_push(&ctx->tx_ring, data, len);
|
||||
|
||||
if (written > 0) {
|
||||
ctx->tx_count += written;
|
||||
tx_kickoff(port_id);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiUART_SendString(port_id_t port_id, const char *str)
|
||||
{
|
||||
if (str == NULL) {
|
||||
return;
|
||||
}
|
||||
MultiUART_Send(port_id, (const uint8_t *)str, strlen(str));
|
||||
}
|
||||
|
||||
void MultiUART_SendFmt(port_id_t port_id, const char *fmt, ...)
|
||||
{
|
||||
if (fmt == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[128];
|
||||
va_list args;
|
||||
|
||||
va_start(args, fmt);
|
||||
int len = vsnprintf(buffer, sizeof(buffer), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (len > 0) {
|
||||
if (len >= (int)sizeof(buffer)) {
|
||||
len = sizeof(buffer) - 1;
|
||||
}
|
||||
MultiUART_Send(port_id, (const uint8_t *)buffer, len);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiUART_TxCpltCallback(port_id_t port_id)
|
||||
{
|
||||
if (port_id >= PORT_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (port_id == PORT_UART2) {
|
||||
UART2_Print_TxCpltCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
uart_port_context_t *ctx = &g_port_ctx[port_id];
|
||||
uart_tx_ring_t *ring = &ctx->tx_ring;
|
||||
uint8_t byte;
|
||||
bool has_more = false;
|
||||
|
||||
__disable_irq();
|
||||
ring->is_sending = false;
|
||||
|
||||
if (ring->count > 0) {
|
||||
byte = ring->buffer[ring->tail];
|
||||
ring->tail = (ring->tail + 1) % UART_TX_BUFFER_SIZE;
|
||||
ring->count--;
|
||||
ring->is_sending = true;
|
||||
has_more = true;
|
||||
}
|
||||
__enable_irq();
|
||||
|
||||
if (has_more) {
|
||||
HAL_UART_Transmit_IT(ctx->huart, &byte, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const char *MultiUART_GetPortName(port_id_t port_id)
|
||||
{
|
||||
if (port_id >= PORT_COUNT) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
return g_port_name_map[port_id];
|
||||
}
|
||||
|
||||
uint16_t MultiUART_GetRxCount(port_id_t port_id)
|
||||
{
|
||||
if (port_id >= PORT_COUNT) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t count;
|
||||
__disable_irq();
|
||||
count = g_port_ctx[port_id].rx_ring.count;
|
||||
__enable_irq();
|
||||
return count;
|
||||
}
|
||||
|
||||
uint16_t MultiUART_ReadByte(port_id_t port_id, uint8_t *byte)
|
||||
{
|
||||
if (port_id >= PORT_COUNT || byte == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uart_rx_ring_t *ring = &g_port_ctx[port_id].rx_ring;
|
||||
|
||||
__disable_irq();
|
||||
if (ring->count == 0) {
|
||||
__enable_irq();
|
||||
return 0;
|
||||
}
|
||||
|
||||
*byte = ring->buffer[ring->tail];
|
||||
ring->tail = (ring->tail + 1) % UART_RX_BUFFER_SIZE;
|
||||
ring->count--;
|
||||
__enable_irq();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint16_t MultiUART_GetTxAvailable(port_id_t port_id)
|
||||
{
|
||||
if (port_id >= PORT_COUNT) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t available;
|
||||
__disable_irq();
|
||||
available = UART_TX_BUFFER_SIZE - g_port_ctx[port_id].tx_ring.count;
|
||||
__enable_irq();
|
||||
return available;
|
||||
}
|
||||
|
||||
uint32_t MultiUART_GetOverflowCount(port_id_t port_id)
|
||||
{
|
||||
if (port_id >= PORT_COUNT) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return g_port_ctx[port_id].rx_ring.overflow_count +
|
||||
g_port_ctx[port_id].tx_ring.overflow_count;
|
||||
}
|
||||
@ -3,17 +3,17 @@
|
||||
* @file relay_control.c
|
||||
* @brief 继电器控制模块实现
|
||||
* @author Application Layer
|
||||
* @version 1.1
|
||||
* @version 2.0
|
||||
******************************************************************************
|
||||
* @attention
|
||||
* 本模块实现继电器的安全控制
|
||||
* 本模块实现单路继电器的安全控制
|
||||
* 关键特性:
|
||||
* 1. 最小切换间隔保护,防止频繁切换损坏继电器
|
||||
* 2. 状态记录,支持诊断
|
||||
* 2. 状态记录
|
||||
* 3. 调试日志输出
|
||||
*
|
||||
* 修订历史:
|
||||
* v1.1 - 修复审查报告中危-9/10:对齐RELAY_COUNT与参数校验逻辑
|
||||
* v2.0 - 精简为单路继电器控制,移除冗余功能
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
@ -29,74 +29,42 @@
|
||||
#define DEBUG_LOG(fmt, ...)
|
||||
#endif
|
||||
|
||||
#define MAX_RELAY_ID 4
|
||||
|
||||
static bool current_states[MAX_RELAY_ID] = {false, false, false, false};
|
||||
static bool current_state = false;
|
||||
static uint32_t last_toggle_tick = 0;
|
||||
static uint32_t toggle_count = 0;
|
||||
|
||||
void Relay_Init(void)
|
||||
{
|
||||
HAL_GPIO_WritePin(RL_Control_GPIO_Port, RL_Control_Pin, GPIO_PIN_RESET);
|
||||
|
||||
for (int i = 0; i < MAX_RELAY_ID; i++) {
|
||||
current_states[i] = false;
|
||||
}
|
||||
current_state = false;
|
||||
last_toggle_tick = 0;
|
||||
toggle_count = 0;
|
||||
|
||||
DEBUG_LOG("Init OK, state=OFF");
|
||||
}
|
||||
|
||||
void Relay_SetState(uint8_t relay_id, bool state)
|
||||
void Relay_SetState(bool state)
|
||||
{
|
||||
if (relay_id < 1 || relay_id > MAX_RELAY_ID) {
|
||||
DEBUG_LOG("Invalid relay ID: %d", relay_id);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t current_tick = HAL_GetTick();
|
||||
|
||||
if (current_tick - last_toggle_tick < RELAY_MIN_INTERVAL) {
|
||||
DEBUG_LOG("Toggle too fast, ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t idx = relay_id - 1;
|
||||
|
||||
if (current_states[idx] == state) {
|
||||
if (current_state == state) {
|
||||
DEBUG_LOG("State unchanged: %s", state ? "ON" : "OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
if (relay_id == 1) {
|
||||
HAL_GPIO_WritePin(RL_Control_GPIO_Port, RL_Control_Pin,
|
||||
state ? GPIO_PIN_SET : GPIO_PIN_RESET);
|
||||
}
|
||||
HAL_GPIO_WritePin(RL_Control_GPIO_Port, RL_Control_Pin,
|
||||
state ? GPIO_PIN_SET : GPIO_PIN_RESET);
|
||||
|
||||
current_states[idx] = state;
|
||||
current_state = state;
|
||||
last_toggle_tick = current_tick;
|
||||
toggle_count++;
|
||||
|
||||
DEBUG_LOG("Relay %d -> %s (count=%lu)", relay_id, state ? "ON" : "OFF", toggle_count);
|
||||
DEBUG_LOG("Relay -> %s", state ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
bool Relay_GetState(uint8_t relay_id)
|
||||
bool Relay_GetState(void)
|
||||
{
|
||||
if (relay_id < 1 || relay_id > MAX_RELAY_ID) {
|
||||
return false;
|
||||
}
|
||||
return current_states[relay_id - 1];
|
||||
}
|
||||
|
||||
void Relay_Toggle(uint8_t relay_id)
|
||||
{
|
||||
if (relay_id < 1 || relay_id > MAX_RELAY_ID) {
|
||||
return;
|
||||
}
|
||||
Relay_SetState(relay_id, !current_states[relay_id - 1]);
|
||||
}
|
||||
|
||||
uint32_t Relay_GetToggleCount(void)
|
||||
{
|
||||
return toggle_count;
|
||||
return current_state;
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
* 1. 环形缓冲区避免数据丢失
|
||||
* 2. 中断安全,支持ISR中调用
|
||||
* 3. 非阻塞发送,不影响实时性
|
||||
*
|
||||
*
|
||||
* 修订历史:
|
||||
* v1.1 - 修复审查报告高危-1/2/3,中危-4
|
||||
******************************************************************************
|
||||
@ -22,95 +22,243 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/*==============================================================================
|
||||
* 调试宏定义
|
||||
*============================================================================*/
|
||||
/* DEBUG_PRINT_ENABLED: 调试日志开关,置1时启用模块自测日志输出 */
|
||||
#define DEBUG_PRINT_ENABLED 1
|
||||
|
||||
#if DEBUG_PRINT_ENABLED
|
||||
/* 调试日志宏,带模块前缀"[UART2]"方便过滤日志 */
|
||||
#define DEBUG_LOG(fmt, ...) UART2_Print_Printf("[UART2] " fmt "\r\n", ##__VA_ARGS__)
|
||||
#else
|
||||
/* 禁用时为空宏,避免无用代码生成 */
|
||||
#define DEBUG_LOG(fmt, ...)
|
||||
#endif
|
||||
|
||||
/*==============================================================================
|
||||
* 数据结构定义
|
||||
*============================================================================*/
|
||||
/**
|
||||
* @brief 环形发送缓冲区数据结构
|
||||
* @note 采用Ring Buffer(环形缓冲区)设计,实现FIFO队列管理
|
||||
*
|
||||
* 设计目的:
|
||||
* 解决UART发送与CPU执行速度不匹配的问题。当UART正在发送数据时,
|
||||
* 后续数据先存入缓冲区,待发送完成后再取出发送,实现异步非阻塞打印。
|
||||
* 环形缓冲区相比线性缓冲区的优势是无需数据搬移,head和tail指针
|
||||
* 循环递增,到达末尾后自动回绕到开头。
|
||||
*
|
||||
* 字段说明:
|
||||
* - buffer: 存放数据的字节数组,大小由UART2_TX_BUFFER_SIZE定义
|
||||
* - head: 写入位置指针,指向下一个待写入位置(生产者指针)
|
||||
* - tail: 读取位置指针,指向下一个待读取位置(消费者指针)
|
||||
* - count: 当前缓冲区中有效数据字节数
|
||||
* - is_sending: 发送忙标志,表示UART硬件是否正在发送数据
|
||||
* - overflow_count: 溢出错误计数,缓冲区满时丢弃数据的次数
|
||||
*
|
||||
* 索引计算规则:
|
||||
* head和tail均采用模运算实现回绕: new_index = (old_index + 1) % buffer_size
|
||||
* 这确保了指针在达到缓冲区末尾时自动回到开头,形成"环形"效果
|
||||
*
|
||||
* 线程安全说明:
|
||||
* 所有修改共享资源的代码段均使用__disable_irq/__enable_irq暂时禁用中断,
|
||||
* 防止在临界区内被ISR打断导致数据竞争(race condition)
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t buffer[UART2_TX_BUFFER_SIZE];
|
||||
volatile uint16_t head;
|
||||
volatile uint16_t tail;
|
||||
volatile uint16_t count;
|
||||
volatile bool is_sending;
|
||||
volatile uint16_t overflow_count;
|
||||
uint8_t buffer[UART2_TX_BUFFER_SIZE]; /**< 发送数据缓冲区,静态分配避免动态内存 */
|
||||
volatile uint16_t head; /**< 写指针,下一个数据写入位置 */
|
||||
volatile uint16_t tail; /**< 读指针,下一个数据读取位置 */
|
||||
volatile uint16_t count; /**< 缓冲区中有效数据字节计数 */
|
||||
volatile bool is_sending; /**< UART发送忙标志,防止重复启动发送 */
|
||||
volatile uint16_t overflow_count; /**< 溢出错误计数,统计因缓冲区满丢弃的数据 */
|
||||
} ring_buffer_t;
|
||||
|
||||
/*==============================================================================
|
||||
* 全局变量定义
|
||||
*============================================================================*/
|
||||
/**
|
||||
* @brief 环形发送缓冲区实例
|
||||
* @note static修饰确保仅本文件内可访问,初始化为全零
|
||||
*/
|
||||
static ring_buffer_t tx_ring = {0};
|
||||
|
||||
/*==============================================================================
|
||||
* 公共函数实现
|
||||
*============================================================================*/
|
||||
/**
|
||||
* @brief UART2打印模块初始化
|
||||
* @note 在系统启动或UART外设初始化后调用,重置环形缓冲区状态
|
||||
*
|
||||
* @param 无
|
||||
* @return 无
|
||||
*
|
||||
* 功能说明:
|
||||
* 1. 重置所有指针(head/tail/count)为初始状态
|
||||
* 2. 清除发送忙标志
|
||||
* 3. 清零溢出错误计数器
|
||||
*
|
||||
* 初始化安全:
|
||||
* 此函数应在UART硬件初始化完成之后调用,确保huart2已正确配置
|
||||
*/
|
||||
void UART2_Print_Init(void)
|
||||
{
|
||||
/* 重置环形缓冲区各状态变量 */
|
||||
tx_ring.head = 0;
|
||||
tx_ring.tail = 0;
|
||||
tx_ring.count = 0;
|
||||
tx_ring.is_sending = false;
|
||||
tx_ring.overflow_count = 0;
|
||||
|
||||
|
||||
/* 输出模块初始化完成日志 */
|
||||
DEBUG_LOG("Init OK, buffer size: %d", UART2_TX_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送数据到UART2(核心写入函数)
|
||||
* @note 将数据写入环形缓冲区,若UART空闲则自动启动首次发送
|
||||
* 此函数是线程安全的,可从ISR或主线程调用
|
||||
*
|
||||
* @param data: 待发送数据缓冲区的指针(输入)
|
||||
* @param len: 待发送数据字节数(输入)
|
||||
* @return 无返回值
|
||||
*
|
||||
* 算法说明 - 生产者逻辑:
|
||||
* 1. 遍历待发送数据的每个字节
|
||||
* 2. 检查缓冲区是否有空间(count < buffer_size)
|
||||
* 3. 如有空间,将数据写入buffer[head],head递增并回绕
|
||||
* 4. 如缓冲区满,放弃剩余数据并增加overflow_count
|
||||
*
|
||||
* 发送触发机制:
|
||||
* - 如果缓冲区有数据且UART当前空闲(is_sending=false),
|
||||
* 设置is_sending=true并立即触发首次发送(Kickoff)
|
||||
* - 后续发送由TxCpltCallback中断回调驱动,形成连续发送直到缓冲区清空
|
||||
*
|
||||
* 中断安全:
|
||||
* - 使用__disable_irq/__enable_irq保护临界区
|
||||
* - 防止在检查count和写入buffer之间被ISR打断
|
||||
*/
|
||||
void UART2_Print_Send(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
/* 参数合法性检查,防止空指针或零长度 */
|
||||
if (len == 0 || data == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
uint16_t written = 0;
|
||||
bool needs_kickoff = false;
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 临界区:禁用中断,保护共享缓冲区的写入操作
|
||||
*----------------------------------------------------------*/
|
||||
__disable_irq();
|
||||
for (uint16_t i = 0; i < len; i++) {
|
||||
/* 检查缓冲区是否有空间 */
|
||||
if (tx_ring.count >= UART2_TX_BUFFER_SIZE) {
|
||||
/* 缓冲区已满,丢弃数据并计数 */
|
||||
tx_ring.overflow_count++;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
/* 将数据写入环形缓冲区,更新写指针(模缓冲区大小回绕) */
|
||||
tx_ring.buffer[tx_ring.head] = data[i];
|
||||
tx_ring.head = (tx_ring.head + 1) % UART2_TX_BUFFER_SIZE;
|
||||
tx_ring.count++;
|
||||
written++;
|
||||
}
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 判断是否需要触发首次发送
|
||||
* 条件:成功写入数据 AND UART当前处于空闲状态
|
||||
*----------------------------------------------------------*/
|
||||
if (written > 0 && !tx_ring.is_sending) {
|
||||
tx_ring.is_sending = true;
|
||||
needs_kickoff = true;
|
||||
}
|
||||
__enable_irq();
|
||||
|
||||
__enable_irq(); /* 退出临界区 */
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 启动首次发送(Kickoff)
|
||||
* 从缓冲区取出第一个字节通过DMA/中断方式发送
|
||||
*----------------------------------------------------------*/
|
||||
if (needs_kickoff) {
|
||||
uint8_t byte;
|
||||
/* 再次禁用中断以读取尾指针(消费者操作) */
|
||||
__disable_irq();
|
||||
byte = tx_ring.buffer[tx_ring.tail];
|
||||
__enable_irq();
|
||||
/* 启动UART中断发送,发送完成后会触发TxCpltCallback */
|
||||
HAL_UART_Transmit_IT(&huart2, &byte, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送字符串到UART2
|
||||
* @note 封装UART2_Print_Send,自动计算字符串长度
|
||||
*
|
||||
* @param str: 待发送的以'\0'结尾的字符串指针(输入)
|
||||
* @return 无返回值
|
||||
*
|
||||
* 使用说明:
|
||||
* - str必须为有效指针且以'\0'结尾
|
||||
* - strlen计算长度时不包括终止符,所以实际发送的也不包括
|
||||
*/
|
||||
void UART2_Print_String(const char *str)
|
||||
{
|
||||
/* 空指针保护 */
|
||||
if (str == NULL) {
|
||||
return;
|
||||
}
|
||||
/* 使用strlen获取字符串长度并发送 */
|
||||
UART2_Print_Send((const uint8_t *)str, strlen(str));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 格式化打印到UART2
|
||||
* @note 仿printf风格,支持可变参数格式化输出
|
||||
*
|
||||
* @param fmt: 格式化字符串,与printf语法兼容(输入)
|
||||
* @param ...: 可变参数列表,对应格式化字符串中的占位符(输入)
|
||||
* @return 无返回值
|
||||
*
|
||||
* 实现说明:
|
||||
* 1. 使用va_list/va_start/va_end处理可变参数
|
||||
* 2. vsnprintf将格式化参数写入临时缓冲区(128字节)
|
||||
* 3. 将格式化后的字符串通过UART2_Print_Send发送
|
||||
*
|
||||
* 缓冲区限制:
|
||||
* - 最大格式化输出为127字节(128-1留作字符串终止符)
|
||||
* - 超出部分会被截断,不报错
|
||||
*
|
||||
* 线程安全:
|
||||
* 此函数本身是中断安全的,但内部调用UART2_Print_Send,
|
||||
* 多线程并发调用时输出可能交叉
|
||||
*/
|
||||
void UART2_Print_Printf(const char *fmt, ...)
|
||||
{
|
||||
/* 格式化字符串合法性检查 */
|
||||
if (fmt == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[128];
|
||||
va_list args;
|
||||
|
||||
|
||||
char buffer[128]; /* 临时格式化缓冲区,大小固定 */
|
||||
va_list args; /* 可变参数列表变量 */
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 可变参数处理:初始化va_list并执行格式化
|
||||
*----------------------------------------------------------*/
|
||||
va_start(args, fmt);
|
||||
int len = vsnprintf(buffer, sizeof(buffer), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 发送格式化后的字符串
|
||||
* vsnprintf返回值len:
|
||||
* - >=0: 成功格式化,需要发送的字符数(不含终止符)
|
||||
* - <0: 格式化失败,不发送任何数据
|
||||
*----------------------------------------------------------*/
|
||||
if (len >= 0) {
|
||||
/* 边界检查:若len超过缓冲区实际容量则截断 */
|
||||
if (len >= (int)sizeof(buffer)) {
|
||||
len = sizeof(buffer) - 1;
|
||||
}
|
||||
@ -118,51 +266,127 @@ void UART2_Print_Printf(const char *fmt, ...)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief UART2打印任务(轮询模式驱动)
|
||||
* @note 在主循环或定时器中调用,驱动环形缓冲区数据发送
|
||||
* 与TxCpltCallback配合形成轮询+中断混合发送模式
|
||||
*
|
||||
* @param 无
|
||||
* @return 无
|
||||
*
|
||||
* 工作原理:
|
||||
* 正常发送流程依赖TxCpltCallback中断回调驱动。但若某些平台
|
||||
* 或情况下中断被屏蔽,此函数作为备用发送机制,从缓冲区取出
|
||||
* 数据启动新的发送。
|
||||
*
|
||||
* 调用时机:
|
||||
* 建议在主循环中周期性调用(如每次循环或10ms定时),
|
||||
* 配合UART空闲中断或DMA完成中断实现高效发送
|
||||
*
|
||||
* 中断安全:
|
||||
* - 使用__disable_irq/__enable_irq保护临界区
|
||||
*/
|
||||
void UART2_Print_Task(void)
|
||||
{
|
||||
uint8_t byte;
|
||||
uint16_t current_tail;
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 临界区:检查是否可以发送
|
||||
*----------------------------------------------------------*/
|
||||
__disable_irq();
|
||||
/* 条件:不正在发送 且 缓冲区有数据 */
|
||||
if (tx_ring.is_sending || tx_ring.count == 0) {
|
||||
__enable_irq();
|
||||
return;
|
||||
return; /* 条件不满足,不执行发送 */
|
||||
}
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 从缓冲区取出数据,更新读指针
|
||||
*----------------------------------------------------------*/
|
||||
current_tail = tx_ring.tail;
|
||||
tx_ring.tail = (tx_ring.tail + 1) % UART2_TX_BUFFER_SIZE;
|
||||
tx_ring.tail = (tx_ring.tail + 1) % UART2_TX_BUFFER_SIZE; /* 环形回绕 */
|
||||
tx_ring.count--;
|
||||
tx_ring.is_sending = true;
|
||||
tx_ring.is_sending = true; /* 标记为发送中,防止重复启动 */
|
||||
byte = tx_ring.buffer[current_tail];
|
||||
__enable_irq();
|
||||
|
||||
__enable_irq(); /* 退出临界区 */
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 启动UART中断发送
|
||||
*----------------------------------------------------------*/
|
||||
HAL_UART_Transmit_IT(&huart2, &byte, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief UART发送完成回调函数
|
||||
* @note 应在UART TX完成中断中调用,负责驱动环形缓冲区连续发送
|
||||
* 此函数是发送流程的核心驱动引擎
|
||||
*
|
||||
* @param 无
|
||||
* @return 无
|
||||
*
|
||||
* 工作原理 - 消费者逻辑:
|
||||
* 1. 清除is_sending标志,表示UART硬件已空闲
|
||||
* 2. 检查环形缓冲区是否还有待发送数据
|
||||
* 3. 如有,取出下一字节并启动新的发送
|
||||
* 4. 重复上述过程直到缓冲区清空
|
||||
*
|
||||
* 中断安全:
|
||||
* - 使用__disable_irq/__enable_irq保护共享数据访问
|
||||
* - 在中断上下文调用,必须确保操作原子性
|
||||
*
|
||||
* 调用约定:
|
||||
* 此函数需要与HAL库中断处理正确关联,通常在
|
||||
* HAL_UART_TxCpltCallback中断回调中调用
|
||||
*/
|
||||
void UART2_Print_TxCpltCallback(void)
|
||||
{
|
||||
uint8_t byte;
|
||||
uint16_t current_tail;
|
||||
bool has_more = false;
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 第一步:标记UART为空闲状态
|
||||
*----------------------------------------------------------*/
|
||||
__disable_irq();
|
||||
tx_ring.is_sending = false;
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 第二步:检查缓冲区是否有更多数据待发送
|
||||
*----------------------------------------------------------*/
|
||||
if (tx_ring.count > 0) {
|
||||
/* 取出下一字节,更新读指针(环形回绕) */
|
||||
current_tail = tx_ring.tail;
|
||||
tx_ring.tail = (tx_ring.tail + 1) % UART2_TX_BUFFER_SIZE;
|
||||
tx_ring.count--;
|
||||
tx_ring.is_sending = true;
|
||||
tx_ring.is_sending = true; /* 立即标记为发送中 */
|
||||
byte = tx_ring.buffer[current_tail];
|
||||
has_more = true;
|
||||
has_more = true; /* 标记有待发送数据 */
|
||||
}
|
||||
__enable_irq();
|
||||
|
||||
|
||||
/*----------------------------------------------------------
|
||||
* 第三步:如有待发送数据,立即启动下一次发送
|
||||
*----------------------------------------------------------*/
|
||||
if (has_more) {
|
||||
HAL_UART_Transmit_IT(&huart2, &byte, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询发送模块忙状态
|
||||
* @note 用于判断是否有数据正在发送或缓冲区中是否有待发数据
|
||||
*
|
||||
* @param 无
|
||||
* @return bool: true=忙(正在发送或缓冲区有数据),false=空闲
|
||||
*
|
||||
* 使用场景:
|
||||
* - 在进入低功耗模式前检查是否有数据待发送
|
||||
* - 在系统关机前确认所有调试日志已发送完毕
|
||||
*
|
||||
* 中断安全:
|
||||
* - 使用临界区保护,确保检查到返回之间数据不被修改
|
||||
*/
|
||||
bool UART2_Print_IsBusy(void)
|
||||
{
|
||||
bool busy;
|
||||
@ -172,6 +396,22 @@ bool UART2_Print_IsBusy(void)
|
||||
return busy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询发送缓冲区可用空间
|
||||
* @note 返回环形缓冲区当前剩余可写入的空间大小
|
||||
*
|
||||
* @param 无
|
||||
* @return uint16_t: 可用字节数
|
||||
*
|
||||
* 计算公式:available = buffer_size - count
|
||||
*
|
||||
* 使用场景:
|
||||
* - 在发送大数据块前检查缓冲区是否足够容纳
|
||||
* - 实现流量控制逻辑
|
||||
*
|
||||
* 中断安全:
|
||||
* - 使用临界区保护
|
||||
*/
|
||||
uint16_t UART2_Print_Available(void)
|
||||
{
|
||||
uint16_t available;
|
||||
@ -181,32 +421,64 @@ uint16_t UART2_Print_Available(void)
|
||||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取溢出错误计数
|
||||
* @note 统计因缓冲区满导致数据丢弃的次数,用于诊断
|
||||
*
|
||||
* @param 无
|
||||
* @return uint16_t: 溢出错误累计次数
|
||||
*
|
||||
* 使用说明:
|
||||
* - 此计数器仅在缓冲区满时递增
|
||||
* - 若计数持续增长,说明UART发送速度跟不上数据产生速度
|
||||
* - 可通过增大UART2_TX_BUFFER_SIZE或降低打印频率解决
|
||||
*/
|
||||
uint16_t UART2_Print_GetOverflowCount(void)
|
||||
{
|
||||
return tx_ring.overflow_count;
|
||||
}
|
||||
|
||||
/*==============================================================================
|
||||
* 标准库printf重定向实现
|
||||
*============================================================================*/
|
||||
/**
|
||||
* @brief printf重定向函数 (Keil MDK)
|
||||
* @note 重定向标准库printf到UART2
|
||||
* @param ch: 待发送字符
|
||||
* @param f: 文件指针(未使用)
|
||||
* @retval 发送的字符
|
||||
* @brief printf重定向函数 (Keil MDK编译器)
|
||||
* @note 将标准库printf输出重定向到UART2
|
||||
*
|
||||
* @param ch: 待发送字符(输入)
|
||||
* @param f: 文件指针(未使用,Keil MDK参数要求)
|
||||
* @return int: 发送的字符
|
||||
*
|
||||
* 编译器条件编译:
|
||||
* 此函数仅在__CC_ARM(或__ARMCC_VERSION)定义时编译,
|
||||
* 即Keil MDK-ARM/ARMCC编译器环境下生效
|
||||
*
|
||||
* 实现说明:
|
||||
* 标准库printf最终会调用fputc输出每个字符,
|
||||
* 此处重定向到UART2_Print_Send实现串口打印
|
||||
*/
|
||||
#if defined(__CC_ARM) || defined(__ARMCC_VERSION)
|
||||
int fputc(int ch, FILE *f)
|
||||
{
|
||||
(void)f;
|
||||
(void)f; /* 未使用参数,避免编译器警告 */
|
||||
UART2_Print_Send((uint8_t *)&ch, 1);
|
||||
return ch;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief printf重定向函数 (GCC)
|
||||
* @note 重定向标准库printf到UART2
|
||||
* @param ch: 待发送字符
|
||||
* @retval 发送的字符
|
||||
* @brief printf重定向函数 (GCC编译器 - 单字符版本)
|
||||
* @note 将标准库printf输出重定向到UART2
|
||||
*
|
||||
* @param ch: 待发送字符(输入)
|
||||
* @return int: 发送的字符
|
||||
*
|
||||
* 编译器条件编译:
|
||||
* 此函数仅在__GNUC__定义时编译,即GCC/Clang编译器环境下生效
|
||||
*
|
||||
* 实现说明:
|
||||
* 新一代ARM GCC使用__io_putchar作为printf输出目标,
|
||||
* 此处将其重定向到UART2_Print_Send
|
||||
*/
|
||||
#if defined(__GNUC__)
|
||||
int __io_putchar(int ch)
|
||||
@ -215,9 +487,22 @@ int __io_putchar(int ch)
|
||||
return ch;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief write系统调用重定向 (GCC编译器)
|
||||
* @note 将文件系统write调用重定向到UART2
|
||||
*
|
||||
* @param file: 文件描述符(未使用,仅为兼容标准接口)
|
||||
* @param ptr: 数据缓冲区指针(输入)
|
||||
* @param len: 数据长度(输入)
|
||||
* @return int: 已发送的字节数
|
||||
*
|
||||
* 实现说明:
|
||||
* 有些GCC配置下printf会调用_write而非__io_putchar,
|
||||
* 此函数提供完整的write接口兼容
|
||||
*/
|
||||
int _write(int file, char *ptr, int len)
|
||||
{
|
||||
(void)file;
|
||||
(void)file; /* 忽略文件描述符,只处理标准输出 */
|
||||
UART2_Print_Send((uint8_t *)ptr, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user