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 数据流链路

  1. 硬件读取: BSP_RS232_ReadByte 从环形缓存读取字节
  2. 行缓存: s_line 拼接完整行 (\r/\n 结束)
  3. 分词处理: Tokenize 生成 tokens 参数数组及 argc 计数
  4. 命令分发: Shell_Execute 匹配关键字并调用对应业务函数
  5. 业务执行: 各 Cmd 函数操作硬件或 Flash
  6. 格式化输出: Shell_Send 系列函数将结果格式化输出至串口

2.2 解耦设计说明

  1. 波特率管理: 移除本地波特名字符串,统一调用 bsp_rs485 提供的 RS485_GetBaudStr 接口,消除跨模块耦合。
  2. 宏定义驱动: 通道数量、掩码上限等全部引用各 BSP 模块宏 (RELAY_CH_NUM, DI_CH_NUM, RELAY_MASK),帮助文本与校验边界自动随驱动扩展。
  3. 输出原语封装: 所有串口输出统一走 Shell_SendStr / SendChar 等私有原语,不直接调用 HAL,便于后续替换输出通道。
  4. 无标准库依赖: 不使用 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. 注意事项

  1. 线程安全: 本模块设计为单线程主循环轮询,s_lines_idx 为静态全局变量,不可在中断或其他线程中直接访问。
  2. 缓冲区限制: 单行命令最大 64 字节,超过部分将被截断丢弃。
  3. 参数限制: 最多支持 4 个 Token(1个命令 + 3个参数)。
  4. Flash 写入: ADDRBAUDFS 等修改参数的命令会触发 Flash 写入,频繁调用可能影响 Flash 寿命。
  5. 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");
    }
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐