SA3|遗留 DGUS 串口屏驱动:从裸协议到推送引擎的归档解读

Babel36acl 方法与工具 无~ 5 次阅读 预计阅读时间: 16 分钟 发布于 1 天前 最后更新于 1 小时前 3597 字


在 HMIS 会话协议替换之前,这套基于 DWIN DGUS 内核的串口屏驱动支撑了整整两代产品的运行。本文从源文件出发,完整拆解其协议层、流解码器、实例上下文与推送引擎的设计,作为遗留系统的技术归档。

flowchart TB
    subgraph DGUS[DGUS 串口屏协议]
        D1[裸帧发送]
        D2[流解码器]
        D3[实例上下文]
    end
    subgraph PUSH[推送引擎]
        P1[定时刷新]
        P2[变量同步]
    end
    D1 --> D2 --> PUSH
    D3 --> PUSH
    style DGUS fill:transparent,stroke:#8dc7ff,color:#eaf4ff
    style PUSH fill:transparent,stroke:#8dc7ff,color:#eaf4ff



目录

  1. 背景:为什么归档
  2. DGUS 协议帧结构
  3. 实例上下文:packer_screen_dgus_t
  4. 帧构建:写变量与读变量
  5. 流解码器:状态机与 CRC 校验
  6. 变量寄存器映射
  7. 推送引擎:packer_screen_engine
  8. 参数同步机制
  9. 文本输出 API
  10. 配置变更与解码器重置
  11. 总结:归档的价值

1. 背景:为什么归档

GENERIC_DEVICE_CONTROLLER 项目早期的人机交互界面依赖 DWIN 品牌的 DGUS 串口屏,协议层基于 DGUS 内核的 0x82/0x83 指令实现变量读写。这套驱动经历了两个大版本的迭代——从裸的串口收发演进到带状态机的流解码器,再到上层封装了参数自动同步与文本日志输出引擎。

后来系统整体切换至 HMIS 会话协议(基于带 session ID 的请求-应答模型),原 DGUS 驱动不再用于新设计,但其协议适配层、状态机设计、参数打包策略仍有参考价值,因此以本文归档。

⚠ 注意 以下代码均已脱敏,所有产品级名称替换为 GENERIC_DEVICE_CONTROLLER,具体硬件地址与参数枚举已泛化。

2. DGUS 协议帧结构

DWIN DGUS 协议运行在 USART1(物理层为 RS232),帧格式统一为:

帧头: 0x5A 0xA5
长度: LEN(1 字节,后续字节数)
命令: CMD(1 字节)
数据: [data ... n 字节]
CRC : [CRC_H CRC_L](可选,从 CMD 到 data 末尾)

关键特性:

  • 大端序:多字节数据(VP 地址、数值)统一采用 Big-Endian。
  • 字地址:DGUS 变量存储以 16-bit word 为单元,地址连续。
  • CRC 范围:从 CMD 字节开始到数据末尾(包含 0x5A 0xA5 LEN)。
  • 典型波特率:115200 bps。

3. 实例上下文:packer_screen_dgus_t

驱动核心数据结构 packer_screen_dgus_t 封装了实例级别的协议上下文:

typedef struct {
    uint8_t  tx_buf[PACKER_SCREEN_TX_BUF_SIZE]; /* 发送缓冲区 */
    uint8_t  rx_buf[PACKER_SCREEN_RX_BUF_SIZE]; /* 接收缓冲区 */

    /* 配置 */
    uint32_t baudrate;
    uint8_t  has_crc;       /* 是否启用 CRC */
    uint16_t vp_addr_base;  /* DGUS VP 基地址 */

    /* 流解码器状态 */
    dgus_decoder_state_t decoder_state;
    uint8_t  frame_buf[PACKER_SCREEN_FRAME_MAX];
    uint16_t frame_len;
    uint16_t frame_cursor;

    /* 回调 */
    void (*on_frame)(packer_screen_dgus_t *ctx, uint8_t *frame, uint16_t len);
    void (*on_error)(packer_screen_dgus_t *ctx, int err_code);
} packer_screen_dgus_t;

这种设计将协议状态(解码器、缓冲区)与配置(波特率、CRC 使能、VP 基址)绑定在同一个实例中,支持多个 DGUS 屏并存——虽然实际产品中只会用一个。

4. 帧构建:写变量与读变量

DGUS 变量操作的核心两条指令:

指令 CMD 功能
write_var 0x82 从指定 VP 地址开始写入 N 个字的数据
read_var 0x83 从指定 VP 地址开始读取 N 个字的数据

帧构建函数示例:

/**
 * 构建写变量帧(0x82)
 * @param vp_addr  DGUS 变量地址(字地址)
 * @param data     待写入数据(大端序)
 * @param word_cnt 字数
 * @param[out] frame  输出帧(含 5A A5 帧头)
 * @return 帧长度
 */
uint16_t dgus_build_write_var(packer_screen_dgus_t *ctx,
                               uint16_t vp_addr,
                               const uint16_t *data,
                               uint8_t word_cnt,
                               uint8_t *frame)
{
    uint16_t idx = 0;
    uint8_t payload_len = 2 + word_cnt * 2; /* VP_addr(2) + data(2*word_cnt) */
    uint8_t len_byte = 1 + 1 + payload_len; /* CMD(1) + LEN(1) -> 实际协议中的 LEN 是剩余长度 */

    /* 帧头 */
    frame[idx++] = 0x5A;
    frame[idx++] = 0xA5;
    frame[idx++] = payload_len + 1; /* LEN = CMD(1) + payload */
    frame[idx++] = 0x82;            /* CMD */
    frame[idx++] = (vp_addr >> 8) & 0xFF;
    frame[idx++] = vp_addr & 0xFF;

    for (uint8_t i = 0; i < word_cnt; i++) {
        frame[idx++] = (data[i] >> 8) & 0xFF;
        frame[idx++] = data[i] & 0xFF;
    }

    if (ctx->has_crc) {
        uint16_t crc = dgus_calc_crc(&frame[3], idx - 3); /* 从 CMD 算起 */
        frame[idx++] = (crc >> 8) & 0xFF;
        frame[idx++] = crc & 0xFF;
    }

    return idx;
}

读变量帧(0x83)结构类似,数据部分仅需 VP 地址 + 字数,无需写入值。DGUS 屏收到后以 0x83 响应帧返回数据。

5. 流解码器:状态机与 CRC 校验

串口收数据是字节流,需要从无头流中正确切分帧。驱动使用四级状态机:

typedef enum {
    DECODER_IDLE,      /* 等待 0x5A */
    DECODER_SYNC1,    /* 已收到 0x5A,等待 0xA5 */
    DECODER_LEN,      /* 已收到 0xA5,等待 LEN */
    DECODER_DATA      /* 已收到 LEN,累积后续数据 */
} dgus_decoder_state_t;

解码流程(每收到 1 字节调用一次):

void dgus_decoder_feed(packer_screen_dgus_t *ctx, uint8_t byte)
{
    switch (ctx->decoder_state) {
    case DECODER_IDLE:
        if (byte == 0x5A) {
            ctx->decoder_state = DECODER_SYNC1;
            ctx->frame_cursor = 0;
            ctx->frame_buf[ctx->frame_cursor++] = byte;
        }
        break;

    case DECODER_SYNC1:
        if (byte == 0xA5) {
            ctx->decoder_state = DECODER_LEN;
            ctx->frame_buf[ctx->frame_cursor++] = byte;
        } else {
            ctx->decoder_state = DECODER_IDLE; /* 失步 */
        }
        break;

    case DECODER_LEN:
        ctx->frame_buf[ctx->frame_cursor++] = byte;
        ctx->frame_len = byte; /* LEN = 剩余字节数 */
        ctx->decoder_state = DECODER_DATA;
        break;

    case DECODER_DATA:
        ctx->frame_buf[ctx->frame_cursor++] = byte;
        /* 已收齐 CMD + 数据 + [CRC] */
        if (ctx->frame_cursor >= (3 + ctx->frame_len)) {
            dgus_decoder_frame_ready(ctx);
            ctx->decoder_state = DECODER_IDLE;
        }
        break;
    }
}

帧就绪时的校验逻辑:

  1. 检查 frame_cursor == 3 + LEN(3 = 5A + A5 + LEN 已占用)。
  2. ctx->has_crc,取末尾两字节为 CRC,从 CMD 到数据末尾重新计算并比较。
  3. 校验通过则调用 on_frame 回调;失败调用 on_error 并丢弃该帧。
🔑 设计要点 CRC 校验范围 包含帧头 5A A5 和 LEN 字段。这是 DWIN 官方协议的定义,解码器实现时必须注意区分。

6. 变量寄存器映射

DGUS 屏内部维护一个 16-bit 宽的变量寄存器空间(VP),地址范围为 0x0000–0xFFFF。产品中用到的关键系统信息区:

VP 地址 名称 说明
0x1000 VAR_SYS_STATE 系统状态字
0x1001 VAR_SYS_RUNNING 运行计数 / 心跳
0x1002 VAR_SYS_SELF_CHECK 自检结果位图
0x1003 VAR_SYS_ALARM 报警标志

其他变量地址(如参数区 0x2000–0x20FF)由 DGUS 工程配置决定,驱动通过 vp_addr_base 偏移访问。

7. 推送引擎:packer_screen_engine

在底层协议驱动之上,packer_screen_engine 封装了一个定时推送引擎:

typedef struct {
    packer_screen_dgus_t *dgus;
    uint32_t tick_ms;

    /* 周期任务 */
    uint32_t sysinfo_interval_ms;   /* 系统信息推送间隔 */
    uint32_t paramsync_interval_ms; /* 参数同步间隔 */
    uint32_t log_flush_interval_ms; /* 日志刷新间隔 */

    /* 内部定时器 */
    uint32_t last_sysinfo_tick;
    uint32_t last_paramsync_tick;
    uint32_t last_log_tick;
} packer_screen_engine_t;

引擎主循环(通常在 1ms10ms 定时器中调用):

void packer_screen_engine_poll(packer_screen_engine_t *eng, uint32_t now_ms)
{
    if (now_ms - eng->last_sysinfo_tick >= eng->sysinfo_interval_ms) {
        update_system_info(eng->dgus);
        eng->last_sysinfo_tick = now_ms;
    }

    if (now_ms - eng->last_paramsync_tick >= eng->paramsync_interval_ms) {
        sync_params(eng->dgus);
        eng->last_paramsync_tick = now_ms;
    }

    if (now_ms - eng->last_log_tick >= eng->log_flush_interval_ms) {
        flush_log(eng->dgus);
        eng->last_log_tick = now_ms;
    }
}

8. 参数同步机制

参数同步是引擎中最核心也是最复杂的部分。它将运行时参数表(GENERIC_DEVICE_CONTROLLER 的全局 runtime_params[])批量写入 DGUS VP 地址。

关键逻辑:

static void sync_params(packer_screen_dgus_t *dgus)
{
    for (int i = 0; i < runtime_param_count; i++) {
        runtime_param_t *rp = &runtime_params[i];
        if (!rp->dirty)
            continue;

        uint32_t val = rp->value;
        uint16_t words[2];

        /* uint32 拆成两个 DGUS word(大端序) */
        words[0] = (uint16_t)(val >> 16);  /* 高 16 位 */
        words[1] = (uint16_t)(val & 0xFFFF); /* 低 16 位 */

        uint16_t vp_addr = dgus->vp_addr_base + rp->vp_offset;
        dgus_build_write_var(dgus, vp_addr, words, 2, dgus->tx_buf);
        bsp_uart_port_transmit(USART1, dgus->tx_buf, ...);

        rp->dirty = false;
    }
}

处理细节:

  • dirty 标志由外部业务代码在修改参数时置位,避免无效写入。
  • 单次 write_var 最多写入 64 个字(受 DGUS 协议限制)。
  • 32-bit 参数拆成两个连续的 16-bit word,DGUS 屏在 UI 端组合显示。
  • 写入间隔由 paramsync_interval_ms 控制(通常 50–200 ms)。

9. 文本输出 API

调试与状态信息输出是串口屏的常见需求。驱动封装了三级文本 API:

函数 行为
write_text(col, row, str) 在指定行列写入字符串
write_line(line, str) 写入整行文本(自动换行)
log_printf(fmt, ...) 格式化日志追加到滚动缓冲区

三者底层最终都映射到:

bsp_uart_port_transmit(USART1, data, len);

但差异在于数据组织方式:

  • write_text/write_line 直接操纵 DGUS 的文本显示 VP 区(如 0x3000+ 映射到文本控件)。
  • log_printf 维护一个 FIFO 日志环,通过 flush_log 定时将积压日志写入 DGUS 的日志显示区。

注意:文本写入是单向推送(write-only),DGUS 侧不做应答确认——串口屏接收后直接在 UI 刷新。

10. 配置变更与解码器重置

DGUS 驱动支持运行时切换 has_crc 标志,但这是一个破坏性操作:CRC 启/禁用会改变帧结构(新增/删除尾部两字节),已部分接收的帧将全部无效。因此配置变更时必须重置解码器:

void packer_screen_dgus_reconfig(packer_screen_dgus_t *ctx,
                                  uint32_t baudrate,
                                  uint8_t has_crc)
{
    /* 1. 停止屏幕通信 */
    bsp_uart_port_stop(USART1);

    /* 2. 重置解码器状态机 */
    dgus_decoder_reset(ctx);

    /* 3. 清空 RX FIFO 中的残留字节 */
    bsp_uart_port_flush_rx(USART1);

    /* 4. 更新配置 */
    ctx->baudrate = baudrate;
    ctx->has_crc  = has_crc;

    /* 5. 重新初始化 UART(波特率可能改变) */
    bsp_uart_port_init(USART1, ctx->baudrate);

    /* 6. 发送空 NOP 帧等待屏端就绪 */
    dgus_send_nop(ctx);
}

dgus_decoder_reset 只需将状态机置回 DECODER_IDLE 并清空帧计数器:

static inline void dgus_decoder_reset(packer_screen_dgus_t *ctx) {
    ctx->decoder_state = DECODER_IDLE;
    ctx->frame_cursor = 0;
    ctx->frame_len = 0;
}

11. 总结:归档的价值

虽然 DGUS 驱动在项目中已被 HMIS 协议取代,但它的设计模式在今天仍有参考意义:

  1. 状态机流解码器:串口无帧边界,状态机同步 + CRC 校验是通用范式。
  2. 实例上下文隔离packer_screen_dgus_t 将协议栈所有状态封装在结构体内,天然支持多实例。
  3. 分层引擎:协议驱动(字节级)→ 推送引擎(消息级)→ 业务调用(参数/日志),每一层职责清晰。
  4. 批量参数同步:dirty 标志 + 定时刷新的模式,避免了 N 个独立定时器分别写入的碎片化。
  5. 配置变更安全:重置解码器 + 清空 FIFO 的做法是处理"流协议参数切换"的标准动作。

这套代码从最初的原型验证到量产服役历时两年半,经过四次重构。归档不代表废弃——对于理解"如何为一个没有标准协议栈的串口设备编写可靠驱动",它仍是最好的教材。


GENERIC_DEVICE_CONTROLLER · 遗留系统归档 · SA3

此作者没有提供个人介绍。
最后更新于 2026-05-30