STM32从零到量产开发:四路继电器工业控制模块开发-RS232 Shell 模块(rs232_shell)设计说明
·
STM32从零到量产开发:四路继电器工业控制模块开发-RS232 Shell 模块(rs232_shell)设计说明
1. 模块概述
1.1 文件信息
- 头文件:
rs232_shell.h - 源文件:
rs232_shell.c - 功能描述: 基于 UART4 (PC10/PC11, 115200 8N1) 的 RS232 上位机调试命令行 (Shell) 模块。
1.2 核心功能
通过 RS232 串口与电脑上位机/串口助手交互,支持以下命令:
HELP: 显示命令列表STATUS: 显示继电器/数字输入/运行时间汇总DI: 读取 4 路数字输入RELAY n ON|OFF: 控制单个继电器 (n=1~4)RELAY ALL <val>: 批量设置继电器 (val=0~15 或 0x0~0xF)PARAM: 显示 Modbus 参数 (从机地址/波特率/通信使能)ADDR <1-247>: 修改 Modbus 从机地址 (掉电保存)BAUD <0-4>: 修改 RS485 波特率索引 (掉电保存并立即生效)FACTORY: 恢复出厂默认参数INFO: 显示固件版本/统计信息STATS: 诊断统计: 帧/CRC/异常/继电器寿命/运行时间FS: 查看通信故障安全配置与状态FS EN 0|1: 启用/禁用故障安全FS SEC <1-255>: 设置故障安全超时秒数PING: 心跳测试 (回复 PONG)LED: 4 路 LED 自检 (全部点亮 1.5s, 确认接线)
1.3 协议规范
- 格式: 行文本协议,以
\r或\n结尾 - 大小写: 命令大小写不敏感
- 分隔符: 空格分隔参数
2. 架构与数据流
2.1 数据流链路
- 硬件读取:
BSP_RS232_ReadByte从环形缓存读取字节 - 行缓存:
s_line拼接完整行 (\r/\n结束) - 分词处理:
Tokenize生成tokens参数数组及argc计数 - 命令分发:
Shell_Execute匹配关键字并调用对应业务函数 - 业务执行: 各
Cmd函数操作硬件或 Flash - 格式化输出:
Shell_Send系列函数将结果格式化输出至串口
2.2 解耦设计说明
- 波特率管理: 移除本地波特名字符串,统一调用
bsp_rs485提供的RS485_GetBaudStr接口,消除跨模块耦合。 - 宏定义驱动: 通道数量、掩码上限等全部引用各 BSP 模块宏 (
RELAY_CH_NUM,DI_CH_NUM,RELAY_MASK),帮助文本与校验边界自动随驱动扩展。 - 输出原语封装: 所有串口输出统一走
Shell_SendStr/SendChar等私有原语,不直接调用 HAL,便于后续替换输出通道。 - 无标准库依赖: 不使用
printf系列标准库格式化函数,数字输出自行实现,避免引入体积庞大且不可重入的stdio。
3. 对外接口 API
本模块仅暴露以下 2 个对外接口:
3.1 RS232Shell_Init
void RS232Shell_Init(void);
- 功能: 初始化 RS232 Shell,打印欢迎横幅和命令提示符。
- 注意: 需在
BSP_RS232_Init()之后调用。
3.2 RS232Shell_Process
void RS232Shell_Process(void);
- 功能: RS232 Shell 主任务处理。
- 特性: 非阻塞,每收到完整一行才解析执行一次。
- 建议: 在主循环高频调用 (5~10ms)。
4. 内部实现详解
4.1 宏与常量配置
#define SHELL_PROMPT "P02> " // 命令行提示符
#define SHELL_LINE_MAX 64u // 单条命令最大字符缓存长度
#define SHELL_TOKEN_MAX 4u // 单条命令最大支持分词个数
4.2 核心处理逻辑
4.2.1 字符接收与回显 (RS232Shell_Process)
- 循环读取
BSP_RS232_ReadByte()。 - 遇到
\r或\n时,若缓存有有效字符,则补\0,打印回显[RX:...],触发BSP_RS232Pulse()指示灯,调用Shell_Execute,最后重置索引并打印提示符。 - 普通字符进行本地回显并存入
s_line,超长部分丢弃以防溢出。
4.2.2 命令分发 (Shell_Execute)
- 调用
Tokenize将命令行按空格/Tab分割为指针数组。 - 使用
StrEqualCI进行大小写不敏感匹配。 - 匹配成功后调用对应的
Cmd_XXX处理函数。 - 未匹配命令返回
ERR: unknown command. Type HELP.。
4.3 工具函数
| 函数名 | 功能描述 |
|---|---|
Shell_SendUInt |
无标准库依赖的 32位十进制数字输出 |
Shell_SendHexByte |
单字节两位大写十六进制输出 |
StrEqualCI |
大小写不敏感字符串全等比较 |
ParseUInt |
解析十进制/十六进制(0x)数字字符串 |
Tokenize |
按空格/Tab分割字符串,就地修改原字符串为 \0 分隔 |
4.4 业务命令处理
- RELAY: 支持单路
RELAY n ON|OFF和批量RELAY ALL <val>。批量模式支持十进制及十六进制掩码,校验RELAY_MASK边界。 - ADDR/BAUD: 修改参数后调用
FlashParam_Save持久化,BAUD 修改后立即调用BSP_RS485_SetBaudrate生效。 - FS (FailSafe): 支持查询当前状态、启用/禁用 (
EN 0|1)、设置超时 (SEC <1-254>)。0 和 255 为非法值(255为内部哨兵值)。 - LED: 调用
BSP_LED_SelfTest()触发硬件自检,后台计时 1.5s。
5. 依赖模块
本模块依赖以下底层及中间层模块:
bsp_rs232.h: 串口读写、脉冲指示bsp_relay.h: 继电器控制、动作计数、寿命掩码bsp_di.h: 数字输入读取bsp_led.h: LED 自检bsp_rs485.h: 波特率字符串获取、波特率切换flash_param.h: 参数读取、保存、恢复出厂system_app.h: 运行时间、复位计数modbus_rtu.h: Modbus 帧统计、CRC错误统计、故障安全状态
6. 注意事项
- 线程安全: 本模块设计为单线程主循环轮询,
s_line及s_idx为静态全局变量,不可在中断或其他线程中直接访问。 - 缓冲区限制: 单行命令最大 64 字节,超过部分将被截断丢弃。
- 参数限制: 最多支持 4 个 Token(1个命令 + 3个参数)。
- Flash 写入:
ADDR、BAUD、FS等修改参数的命令会触发 Flash 写入,频繁调用可能影响 Flash 寿命。 - LED 自检:
LED命令触发后为阻塞式或后台计时式(取决于 BSP 实现),期间可能影响其他 LED 状态指示。
/**
******************************************************************************
* @file rs232_shell.h
* @brief RS232 上位机调试命令行 (Shell) - 基于 UART4 (PC10/PC11, 115200 8N1)
*
* 功能: 通过 RS232 串口与电脑上位机/串口助手交互, 支持以下命令:
* HELP 显示命令列表
* STATUS 显示继电器/数字输入/运行时间汇总
* DI 读取 4 路数字输入
* RELAY n ON|OFF 控制单个继电器 (n=1~4)
* RELAY ALL <val> 批量设置继电器 (val=0~15 或 0x0~0xF)
* PARAM 显示 Modbus 参数 (从机地址/波特率/通信使能)
* ADDR <1-247> 修改 Modbus 从机地址 (掉电保存)
* BAUD <0-4> 修改 RS485 波特率索引 (掉电保存并立即生效)
* FACTORY 恢复出厂默认参数
* INFO 显示固件版本/统计信息
* STATS 诊断统计: 帧/CRC/异常/继电器寿命/运行时间 (#3/#4)
* FS 查看通信故障安全配置与状态 (#1)
* FS EN 0|1 启用/禁用故障安全 (#1)
* FS SEC <1-255> 设置故障安全超时秒数 (#1)
* PING 心跳测试 (回复 PONG)
* LED 4 路 LED 自检 (全部点亮 1.5s, 确认接线) (#LED redesign)
*
* 协议: 行文本协议, 以 \\r 或 \\n 结尾, 命令大小写不敏感, 空格分隔参数
******************************************************************************
*/
#ifndef __RS232_SHELL_H
#define __RS232_SHELL_H
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief 初始化 RS232 Shell (打印欢迎横幅和命令提示符)
* @note 需在 BSP_RS232_Init() 之后调用
*/
void RS232Shell_Init(void);
/**
* @brief RS232 Shell 主任务处理
* @note 非阻塞, 每收到完整一行才解析执行一次。建议在主循环高频调用
*/
void RS232Shell_Process(void);
#ifdef __cplusplus
}
#endif
#endif /* __RS232_SHELL_H */
/**
******************************************************************************
* @file rs232_shell.c
* @brief RS232 UART4 上位机命令行解析模块
*
* 数据流完整链路:
* 硬件串口字节读取(BSP_RS232_ReadByte) -> s_line 行缓存拼完整行(\r/\n 结束)
* -> Tokenize 分词生成 tokens 参数数组、argc 参数计数
* -> Shell_Execute 分发命令
* -> 各 Cmd 业务函数操作硬件 / Flash
* -> Shell_Send 系列格式化输出串口
*
* 解耦说明:
* 1. 原本地波特名字符串已移除,统一调用 bsp_rs485 提供的 RS485_GetBaudStr 接口,
* 消除跨模块业务常量耦合,波特配置仅维护一份映射表。
* 2. 通道数量、掩码上限等全部引用各 BSP 模块的宏 (RELAY_CH_NUM / DI_CH_NUM /
* RELAY_MASK),帮助文本与校验边界自动随驱动扩展,杜绝硬编码。
* 3. 所有串口输出统一走 Shell_SendStr / SendChar 等私有原语,不直接调用 HAL,
* 便于后续整体替换输出通道。
* 4. 本模块不使用任何标准库格式化函数 (printf 系列),数字输出自行实现,
* 避免引入体积庞大且不可重入的 stdio。
******************************************************************************
*/
#include "rs232_shell.h"
#include "bsp_rs232.h"
#include "bsp_relay.h"
#include "bsp_di.h"
#include "bsp_led.h"
#include "bsp_rs485.h"
#include "flash_param.h"
#include "system_app.h"
#include "modbus_rtu.h"
#include <string.h>
/* -------------------------- API 快速导航 --------------------------
* void RS232Shell_Init(void); Shell 初始化, 打印欢迎横幅与提示符
* void RS232Shell_Process(void); 命令行周期轮询处理, 主循环 5~10ms 调用
*
* 注: 本模块仅上述 2 个对外接口, 其余命令处理与字符串工具函数
* 全部为 static 私有实现, 集中放在文件末尾的区块 5。
* ------------------------------------------------------------------ */
/* -------------------------- 区块 1:宏与只读常量 -------------------------- */
/* 命令行提示符 */
#define SHELL_PROMPT "P02> "
/* 单条命令最大字符缓存长度,预留'\0'结束符位置 */
#define SHELL_LINE_MAX 64u
/* 单条命令最大支持分词个数 (主命令 + 最多 3 个参数) */
#define SHELL_TOKEN_MAX 4u
/* -------------------------- 区块 2:模块内部静态全局变量 -------------------------- */
/**
* @brief 存储单条完整命令行字符,生命周期RS232Shell_Process全局复用
*/
static char s_line[SHELL_LINE_MAX];
/**
* @brief 当前行有效字符计数,接收字节持续自增
*/
static uint16_t s_idx = 0u;
/* -------------------------- 内部私有函数前置声明 -------------------------- */
/* 输出底层原语 */
static void Shell_SendStr(const char *s);
static void Shell_SendChar(char c);
static void Shell_SendCrLf(void);
static void Shell_SendUInt(uint32_t v);
static void Shell_SendHexByte(uint8_t v);
/* 字符串工具函数 */
static uint8_t StrEqualCI(const char *a, const char *b);
static uint8_t ParseUInt(const char *s, uint32_t *out);
static uint8_t Tokenize(char *buf, char *tokens[], uint8_t max_tokens);
/* 各命令处理回调函数 */
static void Cmd_Help(void);
static void Cmd_Status(void);
static void Cmd_DI(void);
static void Cmd_Relay(uint8_t argc, char *argv[]);
static void Cmd_Param(void);
static void Cmd_Addr(uint8_t argc, char *argv[]);
static void Cmd_Baud(uint8_t argc, char *argv[]);
static void Cmd_Factory(void);
static void Cmd_Info(void);
static void Cmd_Ping(void);
static void Cmd_Stats(void);
static void Cmd_FailSafe(uint8_t argc, char *argv[]);
static void Cmd_LED(uint8_t argc, char *argv[]);
/* 命令分发入口 */
static void Shell_Execute(char *line);
/* -------------------------- 区块 4:对外公共业务 API -------------------------- */
/**
* @brief RS232 Shell初始化,上电打印欢迎横幅与命令提示符
* @retval 无
* @note 必须在BSP_RS232_Init串口初始化后调用
*/
void RS232Shell_Init(void)
{
Shell_SendCrLf();
Shell_SendStr("=====================================\r\n");
Shell_SendStr(" P02 4-CH Relay Controller\r\n");
Shell_SendStr(" RS232 Debug Shell v1.0\r\n");
Shell_SendStr(" Baud 115200 8N1 (UART4 PC10/PC11)\r\n");
Shell_SendStr(" Type HELP for command list\r\n");
Shell_SendStr("=====================================\r\n");
Shell_SendStr(SHELL_PROMPT);
}
/**
* @brief RS232命令行周期轮询处理函数,主循环5~10ms调用
* @retval 无
* @数据流:循环读取所有串口字节,拼接完整行后执行Shell_Execute解析指令
* @note 自动过滤纯回车空帧,每收到一条指令触发RS232指示灯50ms脉冲
* @note 运行在主循环上下文,非阻塞:缓存读空即返回
* 【关联指引】字节来源为 bsp_rs232 的环形缓存 (由 UART4 接收中断填充),
* 本函数在主循环中消费并拼装成整行命令。
*/
void RS232Shell_Process(void)
{
int16_t ch;
while ((ch = BSP_RS232_ReadByte()) >= 0)
{
if (ch == '\r' || ch == '\n')
{
/* 收到换行,存在有效字符才解析执行 */
if (s_idx > 0u)
{
s_line[s_idx] = '\0';
Shell_SendStr("\r\n[RX:");
Shell_SendStr(s_line);
Shell_SendStr("]\r\n");
BSP_RS232Pulse();
Shell_Execute(s_line);
Shell_SendStr(SHELL_PROMPT);
s_idx = 0u;
}
}
else
{
/* 普通字符本地回显并存入行缓存 */
Shell_SendChar((char)ch);
/* 留 1 字节给结束符 '\0',超长部分直接丢弃,防止缓存溢出 */
if (s_idx < (SHELL_LINE_MAX - 1u))
{
s_line[s_idx++] = (char)ch;
}
}
}
}
/* -------------------------- 区块 5:内部私有底层处理函数 -------------------------- */
/* ---- 5.1 输出底层原语 ---- */
/**
* @brief 串口发送字符串封装
* @param[in] s 以'\0'结尾字符串,允许NULL空指针,内部保护
* @retval 无
*/
static void Shell_SendStr(const char *s)
{
if (s != NULL)
{
BSP_RS232_SendString(s);
}
}
/**
* @brief 串口输出单个ASCII字符
* @param[in] c 待输出字符
* @retval 无
*/
static void Shell_SendChar(char c)
{
BSP_RS232_Send((const uint8_t *)&c, 1u);
}
/**
* @brief 输出标准Windows CRLF换行 \r\n
* @retval 无
*/
static void Shell_SendCrLf(void)
{
Shell_SendStr("\r\n");
}
/**
* @brief 无标准库依赖,格式化输出32位十进制数字
* @param[in] v 待打印无符号32位整数
* @retval 无
* @note 逻辑:数值倒序存入局部缓存,再逆序输出字符
*/
static void Shell_SendUInt(uint32_t v)
{
char buf[12]; /* 缓存数组:uint32最大值4294967295 一共10位,开12字节足够预留空间 */
uint8_t i = 0u; /* 缓存下标非负,量产规范统一用无符号类型 */
/* 特殊处理数字0:直接打印字符'0',避免下面循环不执行、无输出 */
if (v == 0u)
{
Shell_SendChar('0');
return;
}
/* 循环拆分每一位,倒序存入buf,比如567->'7''6''5'
i < 11u 为缓存边界保护,避免写满 buf[12] 越界 */
while (v > 0u && i < 11u)
{
buf[i++] = (char)('0' + (v % 10u));
v /= 10u;
}
while (i > 0u)
{
Shell_SendChar(buf[--i]); /* ->'5''6''7' */
}
}
/**
* @brief 输出单字节两位大写十六进制
* @param[in] v 0~255单字节数值
* @retval 无
*/
static void Shell_SendHexByte(uint8_t v)
{
static const char hex[] = "0123456789ABCDEF";
Shell_SendChar(hex[(v >> 4u) & 0x0Fu]); /* 取出高4位,输出第一位 16 进制字符 */
Shell_SendChar(hex[v & 0x0Fu]); /* 取出低4位,输出第二位 16 进制字符 */
}
/* ---- 5.2 字符串工具函数 ---- */
/**
* @brief 大小写不敏感字符串全等比较,适配串口助手随意大小写输入场景
* @param[in] a 字符串1(命令token)
* @param[in] b 字符串2(内置命令关键字)
* @retval uint8_t 1完全相等,0不匹配
*/
static uint8_t StrEqualCI(const char *a, const char *b)
{
/* 循环条件:两个字符串当前字符都不是结束符'\0',才继续对比
关系运算符(!= > < ==)运算结果是 int, char会整型提升 int比较,注意类型转换 */
while (*a != '\0' && *b != '\0')
{
char ca = *a; /* 取出a当前字符 */
char cb = *b; /* 取出b当前字符 */
/* 统一转成大写再比较,实现大小写不敏感 */
if (ca >= 'a' && ca <= 'z')
{
ca -= (char)32;
}
if (cb >= 'a' && cb <= 'z')
{
cb -= (char)32;
}
/* 转换后字符不一致 → 字符串不相等,直接返回0(不匹配) */
if (ca != cb)
{
return 0u;
}
/* 两个指针同时后移,对比下一个字符 */
a++;
b++;
}
/* 循环退出两种情况:
1. 其中一个字符串提前走完(出现'\0')
2. 两个同时走到'\0'(完全等长、所有字符匹配)
*a == *b 用来判断两者是否同时结束:
- 都等于'\0' → 返回1,匹配成功
- 一个结束、另一个还有字符 → 返回0,长度不同不匹配
*/
return (uint8_t)(*a == *b); /* a,b同时走到结束符 \0,说明a,b等长 */
}
/**
* @brief 解析十进制 / 0x十六进制数字字符串
* @param[in] s 待解析数字token
* @param[out] out 解析结果输出uint32_t变量地址
* @retval uint8_t 1解析成功,0非法字符/空串/数值越界
*/
static uint8_t ParseUInt(const char *s, uint32_t *out)
{
/* out:输出容器,是调用者主动提供的存储位置,属于调用方责任,这里不做out合法性判断 */
if (s == NULL || *s == '\0')
{
return 0u;
}
uint32_t v = 0u;
uint8_t base = 10u; /* 进制基数,默认十进制10,识别0x前缀后切换16 */
const char *p = s; /* s = 数据源起点,p = 遍历用的临时游标,这样保留原始入参 s,后续代码随时能拿到字符串起始地址 */
if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X'))
{
base = 16u;
p += 2; /* 游标向后跳 2 字节,跳过前缀0x,从真正数字位开始解析 */
}
/* 专门拦截「只有 0x 前缀、没有数字」的非法情况 */
if (*p == '\0')
{
return 0u;
}
while (*p != '\0')
{
char c = *p;
uint8_t d;
/* 区分数字、小写十六进制、大写十六进制 */
if (c >= '0' && c <= '9')
{
d = (uint8_t)(c - '0');
}
else if (c >= 'a' && c <= 'f')
{
d = (uint8_t)(c - 'a' + 10);
}
else if (c >= 'A' && c <= 'F')
{
d = (uint8_t)(c - 'A' + 10);
}
else
{
return 0u; /* 出现空格、符号、G/Z等非法字符,直接失败 */
}
/* 校验当前字符不能大于等于进制基数,比如12A前面没有0x证明是10进制(0-9),A是10,非法 */
if (d >= base)
{
return 0u;
}
/* 进位计算:旧值 × 进制 + 当前位数值 */
v = v * base + (uint32_t)d;
p++; /* 游标下移,处理下一个字符 */
}
*out = v;
return 1u;
}
/**
* @brief 将完整命令字符串(如RELAY 0x02 ON)按空格/Tab分割为参数指针数组
* @param[in] buf 原始完整命令行s_line缓存
* @param[out] tokens 参数指针输出数组
* @param[in] max_tokens 最大支持参数个数上限
* @retval uint8_t 实际分割参数数量argc
* @数据流:遍历行,跳过空白,截取一段覆盖'\0'分割,存入tokens数组
* @note 会就地修改 buf 内容(把分隔符替换为 '\0'),tokens 指向 buf 内部
*/
static uint8_t Tokenize(char *buf, char *tokens[], uint8_t max_tokens)
{
uint8_t n = 0u;
char *p = buf;
/* 控制整体遍历,同时限制最大参数数量 */
while (*p != '\0' && n < max_tokens)
{
/* 跳过连续空格、Tab */
while (*p == ' ' || *p == '\t')
{
p++;
}
/* 如果跳过空白直接到字符串结尾,没有剩余参数,直接跳出外层循环,结束分割 */
if (*p == '\0')
{
break;
}
tokens[n++] = p;
/* 持续向后移动游标,直到遇到空格、Tab、`\0` 三者之一停下 */
while (*p != '\0' && *p != ' ' && *p != '\t')
{
p++;
}
/* 截断字符串,实现分段隔离 */
if (*p != '\0')
{
*p = '\0';
p++;
}
}
return n;
}
/* ---- 5.3 各命令处理回调函数 ---- */
/**
* @brief HELP指令:打印全部支持命令清单
* @retval 无
* @note 通道数与掩码上限由宏动态打印,驱动扩展通道时帮助文本自动同步
*/
static void Cmd_Help(void)
{
Shell_SendStr("Commands:\r\n");
Shell_SendStr(" HELP - show this help\r\n");
Shell_SendStr(" STATUS - show relay/DI/uptime\r\n");
Shell_SendStr(" DI - read ");
Shell_SendUInt(DI_CH_NUM);
Shell_SendStr(" digital inputs\r\n");
Shell_SendStr(" RELAY n ON|OFF - set relay n (1-");
Shell_SendUInt(RELAY_CH_NUM);
Shell_SendStr(")\r\n");
Shell_SendStr(" RELAY ALL <val> - set all (0-");
Shell_SendUInt(RELAY_MASK);
Shell_SendStr(" or 0x0-0x");
Shell_SendHexByte(RELAY_MASK);
Shell_SendStr(")\r\n");
Shell_SendStr(" PARAM - show Modbus params\r\n");
Shell_SendStr(" ADDR <1-247> - set Modbus slave address\r\n");
Shell_SendStr(" BAUD <0-4> - set RS485 baudrate index\r\n");
Shell_SendStr(" FACTORY - restore factory defaults\r\n");
Shell_SendStr(" INFO - firmware & statistics\r\n");
Shell_SendStr(" STATS - diagnostics: frames/crc/excep/relay-life/runtime\r\n");
Shell_SendStr(" FS - show comm fail-safe config & state\r\n");
Shell_SendStr(" FS EN 0|1 - enable/disable fail-safe (#1)\r\n");
Shell_SendStr(" FS SEC <1-254> - set fail-safe timeout (seconds)\r\n");
Shell_SendStr(" PING - echo test (reply PONG)\r\n");
Shell_SendStr(" LED - self-test 4 LEDs (RUN/COMM/RS232/FAULT)\r\n");
}
/**
* @brief STATUS指令:读取并打印继电器、DI、系统运行时长、复位次数
* @retval 无
*/
static void Cmd_Status(void)
{
uint8_t relays = BSP_Relay_GetAll();
uint8_t dis = BSP_DI_GetAll();
uint8_t i; /* 通道下标非负,量产规范统一用无符号类型 */
char line[32];
Shell_SendStr("RELAY: ");
for (i = 0u; i < RELAY_CH_NUM; i++)
{
line[0] = (char)('1' + i);
line[1] = '=';
if (((relays >> i) & 1u) != 0u)
{
line[2] = 'O';
line[3] = 'N';
line[4] = ' ';
line[5] = '\0';
}
else
{
line[2] = 'O';
line[3] = 'F';
line[4] = 'F';
line[5] = ' ';
line[6] = '\0';
}
Shell_SendStr(line);
}
Shell_SendCrLf();
Shell_SendStr("DI: ");
for (i = 0u; i < DI_CH_NUM; i++)
{
line[0] = (char)('1' + i);
line[1] = '=';
line[2] = (char)('0' + ((dis >> i) & 1u));
line[3] = ' ';
line[4] = '\0';
Shell_SendStr(line);
}
Shell_SendCrLf();
Shell_SendStr("UPTIME: ");
Shell_SendUInt(SystemApp_GetUptime());
Shell_SendStr("s RESETS: ");
Shell_SendUInt(SystemApp_GetResetCount());
Shell_SendCrLf();
}
/**
* @brief DI指令:读取四路数字输入消抖电平并打印
* @retval 无
*/
static void Cmd_DI(void)
{
uint8_t dis = BSP_DI_GetAll();
uint8_t i; /* 通道下标非负,量产规范统一用无符号类型 */
char line[12];
Shell_SendStr("DI: ");
for (i = 0u; i < DI_CH_NUM; i++)
{
line[0] = (char)('1' + i);
line[1] = '=';
line[2] = (char)('0' + ((dis >> i) & 1u));
line[3] = ' ';
line[4] = '\0';
Shell_SendStr(line);
}
Shell_SendCrLf();
}
/**
* @brief RELAY指令:单路/批量控制继电器输出
* @param[in] argc Tokenize返回参数总个数,如RELAY 1 ON → 拆分 3 段,argc=3
* @param[in] argv 分词后参数字符串数组
* argv[0]= "RELAY"(主命令)
* argv[1] = 通道号 / "ALL"
* argv[2] = ON/OFF/ 批量掩码数值
* @retval 无
*/
static void Cmd_Relay(uint8_t argc, char *argv[])
{
/* 非法指令:RELAY 至少需要 3 段 (命令 + 通道 + 状态) */
if (argc < 3u)
{
Shell_SendStr("Usage: RELAY n ON|OFF | RELAY ALL <val>\r\n");
return;
}
if (StrEqualCI(argv[1], "ALL"))
{
uint32_t v;
/* 解析失败 或 数值超过合法掩码上限 */
if (!ParseUInt(argv[2], &v) || v > RELAY_MASK) /* #define RELAY_MASK ((uint8_t)((1u << RELAY_CH_NUM) - 1u)) */
{
Shell_SendStr("ERR: value 0-");
Shell_SendUInt(RELAY_MASK);
Shell_SendStr(" (or 0x0-0x");
Shell_SendHexByte(RELAY_MASK);
Shell_SendStr(")\r\n");
return;
}
BSP_Relay_SetAll((uint8_t)v);
Shell_SendStr("OK\r\n");
return;
}
uint32_t ch;
/* 单路通道解析分支 */
if (!ParseUInt(argv[1], &ch) || ch < 1u || ch > RELAY_CH_NUM)
{
Shell_SendStr("ERR: channel 1-");
Shell_SendUInt(RELAY_CH_NUM);
Shell_SendCrLf();
return;
}
/* 满足单路设置指令 */
uint8_t on;
/* 状态字符串兼容判断:同时接受 ON/OFF 与 1/0 两种写法 */
if (StrEqualCI(argv[2], "ON") || StrEqualCI(argv[2], "1"))
{
on = RELAY_ON;
}
else if (StrEqualCI(argv[2], "OFF") || StrEqualCI(argv[2], "0"))
{
on = RELAY_OFF;
}
else
{
Shell_SendStr("ERR: ON/OFF expected\r\n");
return;
}
BSP_Relay_Set((uint8_t)ch, on);
Shell_SendStr("OK\r\n");
}
/**
* @brief PARAM指令:读取Flash存储Modbus从站地址、波特索引、链路使能
* @retval 无
* @note 波特文本通过RS485_GetBaudStr统一接口获取,无本地波特表
*/
static void Cmd_Param(void)
{
const param_t *p = FlashParam_Get();
const char *baud = RS485_GetBaudStr(p->baud_index);
Shell_SendStr("SlaveAddr=");
Shell_SendUInt(p->slave_addr);
Shell_SendStr(" BaudIndex=");
Shell_SendUInt(p->baud_index);
Shell_SendStr("(");
Shell_SendStr(baud);
Shell_SendStr(") LinkEnable=");
Shell_SendUInt(p->link_enable);
Shell_SendCrLf();
}
/**
* @brief ADDR指令:修改并保存Modbus从站地址到Flash
* @param[in] argc 参数个数
* @param[in] argv argv[1]为地址数字字符串
* @retval 无
*/
static void Cmd_Addr(uint8_t argc, char *argv[])
{
if (argc < 2u)
{
Shell_SendStr("Usage: ADDR <1-247>\r\n");
return;
}
uint32_t v;
if (!ParseUInt(argv[1], &v) || v < SLAVE_ADDR_MIN || v > SLAVE_ADDR_MAX)
{
Shell_SendStr("ERR: addr 1-247\r\n");
return;
}
int8_t r = FlashParam_Save((uint8_t)v, 0xFFu, 0xFFu, 0xFFu, 0xFFu);
if (r == 0)
{
Shell_SendStr("OK (new addr takes effect immediately)\r\n");
}
else
{
Shell_SendStr("ERR: save failed\r\n");
}
}
/**
* @brief BAUD指令:修改RS485波特索引,保存并即时切换串口
* @param[in] argc 参数个数
* @param[in] argv argv[1]波特索引数字串
* @retval 无
*/
static void Cmd_Baud(uint8_t argc, char *argv[])
{
if (argc < 2u)
{
Shell_SendStr("Usage: BAUD <0-4>\r\n");
return;
}
uint32_t v;
if (!ParseUInt(argv[1], &v) || v > BAUD_INDEX_MAX)
{
Shell_SendStr("ERR: index 0-4\r\n");
return;
}
/* 约定: 第1参 slave_addr 用 0 表示"不修改"(0 非合法地址, 可作哨兵),
* 其余参数用 0xFF 表示"不修改"。误传 0xFF 到 slave_addr 位会被判非法。 */
int8_t r = FlashParam_Save(0u, (uint8_t)v, 0xFFu, 0xFFu, 0xFFu);
if (r != 0)
{
Shell_SendStr("ERR: save failed\r\n");
return;
}
r = BSP_RS485_SetBaudrate((uint8_t)v);
if (r == 0)
{
Shell_SendStr("OK (RS485 baudrate changed to ");
Shell_SendStr(RS485_GetBaudStr((uint8_t)v));
Shell_SendStr(")\r\n");
}
else
{
Shell_SendStr("ERR: apply failed\r\n");
}
}
/**
* @brief FACTORY指令:恢复Flash全部出厂参数
* @retval 无
*/
static void Cmd_Factory(void)
{
FlashParam_ResetDefault();
Shell_SendStr("OK: factory defaults restored (recommend reboot)\r\n");
}
/**
* @brief INFO指令:打印固件版本、运行时长、Modbus帧统计、Flash校验信息
* @retval 无
*/
static void Cmd_Info(void)
{
Shell_SendStr("P02 4-CH Relay Controller\r\n");
Shell_SendStr("FW: v1.0 MCU: STM32F103RCT6\r\n");
Shell_SendStr("Uptime=");
Shell_SendUInt(SystemApp_GetUptime());
Shell_SendStr("s Resets=");
Shell_SendUInt(SystemApp_GetResetCount());
Shell_SendCrLf();
Shell_SendStr("Modbus Frames=");
Shell_SendUInt(Modbus_GetFrameCount());
Shell_SendStr(" CRC_Err=");
Shell_SendUInt(Modbus_GetCRCErrorCount());
Shell_SendCrLf();
const param_t *p = FlashParam_Get();
Shell_SendStr("Flash: magic=0x");
/* 逐字节高位在前输出,避免依赖 printf 系列格式化函数 */
Shell_SendHexByte((uint8_t)(p->magic >> 8u));
Shell_SendHexByte((uint8_t)(p->magic & 0xFFu));
Shell_SendStr(" crc=0x");
Shell_SendHexByte((uint8_t)(p->crc >> 24u));
Shell_SendHexByte((uint8_t)(p->crc >> 16u));
Shell_SendHexByte((uint8_t)(p->crc >> 8u));
Shell_SendHexByte((uint8_t)(p->crc & 0xFFu));
Shell_SendCrLf();
}
/**
* @brief PING指令:串口回显测试,回复PONG
* @retval 无
*/
static void Cmd_Ping(void)
{
Shell_SendStr("PONG\r\n");
}
/**
* @brief STATS指令:打印Modbus诊断、继电器动作计数、整机运行时长
* @retval 无
*/
static void Cmd_Stats(void)
{
uint8_t i;
uint8_t life;
Shell_SendStr("== Diagnostics (#3) ==\r\n");
Shell_SendStr("Frames=");
Shell_SendUInt(Modbus_GetFrameCount());
Shell_SendStr(" CRC_Err=");
Shell_SendUInt(Modbus_GetCRCErrorCount());
Shell_SendStr(" Excep=");
Shell_SendUInt(Modbus_GetExceptionCount());
Shell_SendCrLf();
Shell_SendStr("Relay Act: ");
for (i = 0u; i < RELAY_CH_NUM; i++)
{
Shell_SendStr("R");
Shell_SendChar((char)('1' + i));
Shell_SendStr("=");
Shell_SendUInt(BSP_Relay_GetActionCount((uint8_t)(i + 1u)));
Shell_SendChar(' ');
}
Shell_SendCrLf();
life = BSP_Relay_GetLifeAlarmMask();
Shell_SendStr("LifeAlarmMask=0x");
Shell_SendHexByte(life);
Shell_SendStr(" (bit0~3 = channel reached life threshold)\r\n");
Shell_SendStr("== RunTime (#4) ==\r\n");
Shell_SendStr("RunSec=");
Shell_SendUInt(SystemApp_GetRunSeconds());
Shell_SendStr(" RunHours=");
Shell_SendUInt(SystemApp_GetRunHours());
Shell_SendCrLf();
}
/**
* @brief FS指令:查看/配置通信故障安全使能、超时时间
* @param[in] argc 参数数量
* @param[in] argv 操作参数数组
* @retval 无
*/
static void Cmd_FailSafe(uint8_t argc, char *argv[])
{
const param_t *p = FlashParam_Get();
/* 无子命令:仅查询当前配置与生效状态 */
if (argc < 2u)
{
Shell_SendStr("FailSafe: en=");
Shell_SendUInt(p->fail_safe_en);
Shell_SendStr(" timeout=");
Shell_SendUInt(p->fail_safe_sec);
Shell_SendStr("s active=");
Shell_SendUInt(Modbus_IsFailSafeActive());
Shell_SendStr(" (active=1 means comm-timeout -> safety state)\r\n");
return;
}
if (StrEqualCI(argv[1], "EN"))
{
uint32_t v;
if (argc < 3u || !ParseUInt(argv[2], &v) || v > FAILSAFE_EN_MAX)
{
Shell_SendStr("Usage: FS EN 0|1\r\n");
return;
}
int8_t r = FlashParam_Save(0u, 0xFFu, 0xFFu, (uint8_t)v, 0xFFu);
Shell_SendStr(r == 0 ? "OK (fail-safe enable changed)\r\n" : "ERR: save failed\r\n");
}
else if (StrEqualCI(argv[1], "SEC"))
{
uint32_t v;
/* 0 与 >254 均非法:0 表示禁用应改用 FS EN 0,255 是"不修改"哨兵 */
if (argc < 3u || !ParseUInt(argv[2], &v) || v == 0u || v > 254u)
{
Shell_SendStr("Usage: FS SEC <1-254>\r\n");
return;
}
int8_t r = FlashParam_Save(0u, 0xFFu, 0xFFu, 0xFFu, (uint8_t)v);
Shell_SendStr(r == 0 ? "OK (timeout changed)\r\n" : "ERR: save failed\r\n");
}
else
{
Shell_SendStr("Usage: FS | FS EN 0|1 | FS SEC <1-255>\r\n");
}
}
/**
* @brief LED自检命令:触发四路LED全亮1.5s硬件自检
* @param[in] argc 参数计数(本指令无参数,占位兼容接口)
* @param[in] argv 参数数组(未使用)
* @retval 无
* @note 调用BSP_LED_SelfTest写入自检截止时间戳,由BSP_LED_Refresh后台计时
*/
static void Cmd_LED(uint8_t argc, char *argv[])
{
(void)argc;
(void)argv;
Shell_SendStr("LED self-test: all 4 LEDs ON 1.5s (RUN/COMM/FAULT/RS232)\r\n");
BSP_LED_SelfTest();
Shell_SendStr("OK\r\n");
}
/* ---- 5.4 命令分发入口 ---- */
/**
* @brief 完整命令行分发匹配入口
* @param[in] line 已拼接完成、以'\0'结尾的完整命令字符串(s_line缓存)
* @retval 无
* @数据流:line → Tokenize拆分tokens/argc → 匹配关键字调用对应Cmd函数
*/
static void Shell_Execute(char *line)
{
char *tokens[SHELL_TOKEN_MAX];
uint8_t argc = Tokenize(line, tokens, SHELL_TOKEN_MAX);
/* 全空白行:无任何 token,直接忽略 */
if (argc == 0u)
{
return;
}
if (StrEqualCI(tokens[0], "HELP") || StrEqualCI(tokens[0], "?"))
{
Cmd_Help();
}
else if (StrEqualCI(tokens[0], "STATUS"))
{
Cmd_Status();
}
else if (StrEqualCI(tokens[0], "DI"))
{
Cmd_DI();
}
else if (StrEqualCI(tokens[0], "RELAY"))
{
Cmd_Relay(argc, tokens);
}
else if (StrEqualCI(tokens[0], "PARAM"))
{
Cmd_Param();
}
else if (StrEqualCI(tokens[0], "ADDR"))
{
Cmd_Addr(argc, tokens);
}
else if (StrEqualCI(tokens[0], "BAUD"))
{
Cmd_Baud(argc, tokens);
}
else if (StrEqualCI(tokens[0], "FACTORY"))
{
Cmd_Factory();
}
else if (StrEqualCI(tokens[0], "INFO"))
{
Cmd_Info();
}
else if (StrEqualCI(tokens[0], "STATS"))
{
Cmd_Stats();
}
else if (StrEqualCI(tokens[0], "FS"))
{
Cmd_FailSafe(argc, tokens);
}
else if (StrEqualCI(tokens[0], "PING"))
{
Cmd_Ping();
}
else if (StrEqualCI(tokens[0], "LED"))
{
Cmd_LED(argc, tokens);
}
else
{
Shell_SendStr("ERR: unknown command. Type HELP.\r\n");
}
}
更多推荐


所有评论(0)