一、通信架构总览
D223 的通信链路可以概括为:
┌──────────────────────────────────────────────────┐
│ 上位机 (PC) │
│ USB CDC 虚拟串口 │
└──────────────────────┬───────────────────────────┘
│ 0x55AA 协议帧 + CRC16
┌──────────────────────┴───────────────────────────┐
│ STM32H743 (主控) │
│ ┌──────────┐ ┌───────────┐ ┌────────────────┐ │
│ │ RT-Thread │ │ USB消息处理 │ │ ADC功能线程 │ │
│ │ 调度器 │ │ (优先级1) │ │ (优先级0,最高) │ │
│ └──────────┘ └───────────┘ └────────────────┘ │
│ │
│ TIM12(USB超时) TIM13(ADC采集) TIM14(DAC输出) │
└──────────────────────┬───────────────────────────┘
│ SPI/QSPI
┌──────────────────────┴───────────────────────────┐
│ FPGA (GW1NR-LV4QN88C6) │
│ 信号路由 / ADC数据中转 / DI状态缓存 │
└──────────────────────────────────────────────────┘
D223 采用 STM32H743 + FPGA 双芯片架构,与 7606 的单 STM32 架构有本质区别 note_63。所有外部信号(ADC/DI/DO)都经过 FPGA 中转,再通过 SPI/QSPI 传递给 STM32,这决定了 D223 在通信和数据处理上的独特方式。
二、USB CDC 虚拟串口通信
2.1 物理层:USB 2.0 HS + CH340 驱动
D223 通过 USB 2.0 HS 接口 与 PC 通信,使用 USB CDC 虚拟串口 协议 word_8。在 PC 端需要安装 CH340 驱动,安装后系统会枚举出一个虚拟 COM 口(如 COM3、COM16 等),上位机软件通过标准的串口 API 即可收发数据 word_8。
💡 提示 :CH340 驱动安装包可在百度网盘的开源资料目录中找到,路径为
000.开发软件安装包/5.CH340驱动(USB串口驱动)word_8。
2.2 串口参数
D223 使用 USB CDC 虚拟串口,串口参数对实际传输无影响(USB 总线不受波特率限制),但上位机软件通常配置为:
| 参数 | 值 |
|---|---|
| 波特率 | 115200(仅形式参数) |
| 数据位 | 8 |
| 停止位 | 1 |
| 校验位 | None |
从手册提供的 XCOM 调试截图可以看到,实际通信测试使用的就是上述配置 word_8。
三、自定义通信协议详解
3.1 帧格式
D223 使用 ZLinear 统一的自定义协议,帧格式如下 word_7word_8:
┌──────┬───────┬──────────┬─────────────┬──────┐
│ Head │ CmdId │ FrameLen │ Data │ CRC │
│ 2B │ 1B │ 2B │ N Bytes │ 2B │
│0x55AA│ │ │ │ │
└──────┴───────┴──────────┴─────────────┴──────┘
| 字段 | 长度 | 说明 |
|---|---|---|
| Head | 2 字节 | 帧头,固定为 0x55AA |
| CmdId | 1 字节 | 命令 ID,标识指令类型 |
| FrameLen | 2 字节 | 帧总长度(含 CRC),大端序 |
| Data | N 字节 | 数据负载,长度可变 |
| CRC | 2 字节 | CRC16 校验,覆盖从帧头到 CRC 前的所有数据 |
⚠️ 注意大小端 :
FrameLen和CRC采用 大端序(Big-Endian) ,代码中使用SWAP16()函数进行大小端转换 word_8。而 7606 代码分析文档中也提到"所有多字节字段采用小端序 (Little-Endian)" word_4,这里需要特别注意------不同型号可能存在差异,开发时务必以目标型号的实际代码为准。
3.2 msgHead 结构体
协议头部在 C# 上位机和 C 固件中都通过结构体映射,保证跨平台一致 word_7:
csharp
// C# 上位机 (msgStruct.cs)
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct msgHead
{
public UInt16 head; // 帧头: 0x55AA
public byte cmdId; // 命令ID
public UInt16 frameLen; // 帧长度(含CRC)
}
c
// C 固件 (msgStruct.h)
typedef struct {
u16 msgHead; // 包头 0x55AA
u8 cmdId; // 命令ID
u16 frameLen; // 包长度(含CRC)
} msgHead;
3.3 命令 ID 分类
根据 7606 上位机代码分析,命令 ID 按功能范围划分如下 word_3word_7:
| 范围 | 命令类别 | 典型命令 |
|---|---|---|
| 0x01-0x07 | ADC 基础控制 | 通道使能、设备参数、波形数据 |
| 0x08-0x1A | 数据记录 | Flash记录、SRAM记录、在线记录、连续读取 |
| 0x20-0x28 | DDS/DAC 控制 | 参数获取/设置、工作模式、校准 |
| 0x30-0x34 | IO 控制 | IO状态读取、输出设置、PWM控制 |
| 0x40-0x42 | ADC 高级设置 | 量程、信号类型、自动触发 |
| 0x50-0x5F | 系统设置 | 校准参数、IP/端口、设备地址 |
| 0xF0/0x80 | 固件版本 | 软件版本号获取 |
D223 的命令 ID 与 7606 基本一致,均遵循同一协议体系 word_3word_7。
3.4 常用消息结构体
协议定义了一套层次化的消息结构体体系 word_3:
| 结构体名 | 用途 | 字段 |
|---|---|---|
msgHead |
公共帧头 | head + cmdId + frameLen |
msgNoParam |
无参数命令 | head + cmdId + frameLen + ch + crc |
msgUbyte |
单字节参数 | + data(1B) |
msgUWord |
双字节参数 | + data(2B) |
msgGetAdcDatas |
ADC 波形数据 | + dataLen + chEnable + signalType + range + startCh + trigCh + samplingRate + trigValue + adcData4000 |
msgGetDioAll |
全部IO状态 | + din8 + dout4 + pwmEnable4 + pwmFreq4 + pwmDuty4 |
msgDdsDacParam |
DDS参数 | + ch + samplingRate + freq + volt + duty + bias + gain |
四、实战:单点 ADC 读取协议解析
4.1 发送命令
以单点记录仪(同时读取 8 通道 ADC 和 8 通道 DI 状态)为例,发送命令为 word_8:
55 AA 08 00 08 00 11 A6
| 字节 | 值 | 含义 |
|---|---|---|
| 55 AA | 0x55AA | 帧头 |
| 08 | 0x08 | cmdId = 单点读取 |
| 00 08 | 0x0008 | 帧长度 = 8 字节(大端序) |
| 00 | 0x00 | 通道号(未使用,默认0) |
| 11 A6 | 0x11A6 | CRC16 校验值 |
4.2 接收返回数据
采集卡返回 43 字节数据 word_8:
55 AA 08 00 2B 03 00 00 00 00 81 64 C0 00 81 64 BE
00 81 5F 41 00 81 61 A4 00 81 58 45 00 81 59 11
00 81 63 AC 00 81 64 14 0D E6
| 偏移 | 值 | 含义 |
|---|---|---|
| 0-1 | 55 AA | 帧头 |
| 2 | 08 | cmdId |
| 3-4 | 00 2B | 帧长度 = 43 字节 |
| 5 | 03 | ADC 通道使能状态(bit0~bit7 对应 CH1~CH8) |
| 6 | 00 | 信号类型(0=电压,1=电流) |
| 7 | 00 | ADC 量程(0=±5V,1=±10V) |
| 8 | 00 | 8通道 DI 状态(bit0~bit7 对应 DIN1~DIN8) |
| 9-12 | 00 81 64 C0 | 通道1 ADC 值(24位,大端序) |
| 13-16 | ... | 通道2~8 ADC 值 |
| 41-42 | 0D E6 | CRC16 校验值 |
4.3 ADC 值换算
ADC 原始值为 24 位有符号数,换算公式为 word_8:
电压值 = (rawValue - 0x800000) / 0x800000 × 量程
其中:
0x800000 = 8388608 (24位中位值,对应0V)
0x7FFFFF = 8388607 (最大正量程)
0x000000 = 0 (最大负量程)
以通道1为例:0x008164C0 = 8484800
电压 = (8484800 - 8388608) / 8388608 × 5 = 0.010V × (5V量程)
💡 在线记录模式 下,ADC 数据连续上传,发送
55 AA 16 00 0C FF 00 00 4E 20 44 77后采集卡会持续返回数据直到手动停止 word_8。其中0x4E20= 20000,即 20K 采样率。
五、上位机接收状态机解析
5.1 核心流程
上位机 C# 代码中,串口数据接收采用 100ms 定时器 + 缓冲区扫描 的状态机模式 word_7word_8:
SerialPort.DataReceived 事件
│
▼
┌───────────────────────┐
│ Com_DataReceived() │ 串口中断,读取原始字节流
│ 追加到 usbRxMsgBuf │ List<byte> 动态缓冲区
└──────────┬────────────┘
│
▼ (100ms 定时器 rxTim_Tick 触发)
┌───────────────────────┐
│ ① 搜索帧头 0x55AA │ 遍历缓冲区查找帧头
│ ② 判断帧长度是否完整 │ 剩余数据 ≥ frameLen?
│ ③ CRC16 校验 │ checkCrc16WithTail()
│ ④ 分发到业务层 │ 触发 _deviceRxMsg 事件
│ ⑤ 移除已处理帧 │ RemoveRange(0, frameLen)
└───────────────────────┘
5.2 关键代码逻辑
csharp
private void rxTim_Tick(object sender, EventArgs e)
{
// 1. 缓冲区数据不足5字节,直接返回
while (usbRxMsgBuf.Count > 5)
{
byte[] usbRxBuf = usbRxMsgBuf.ToArray();
bool findNewHead = false;
// 2. 搜索帧头 0x55 0xAA
for (UInt16 i = 0; i < usbRxBuf.Length; i++)
{
if (usbRxBuf[i] == 0x55 && i != (usbRxBuf.Length - 1))
{
if (usbRxBuf[i + 1] == 0xAA)
{
// 清除帧头前的脏数据
usbRxMsgBuf.RemoveRange(0, i);
findNewHead = true;
break;
}
}
}
// 3. 转换为协议头结构体,读取帧长度
_pRxHead = (msgHead)msgStruct.BytesToDataStruct(usbRxBuf, typeof(msgHead));
// 4. 判断数据是否足够一帧
if ((usbRxBuf.Length) >= SWAP16(_pRxHead.frameLen))
{
// 5. CRC16 校验
string crcResult = crcCheck.checkCrc16WithTail(usbRxBuf, SWAP16(_pRxHead.frameLen));
if (crcResult != "OK")
{
// 校验失败:删除错误的帧头,重新搜索
usbRxMsgBuf.RemoveRange(0, 2);
break;
}
// 6. 校验通过:抛给业务层处理
_deviceRxMsg(usbRxMsgBuf.ToArray());
// 7. 删除已处理的一帧
usbRxMsgBuf.RemoveRange(0, SWAP16(_pRxHead.frameLen));
break;
}
else
{
// 数据不够一帧,等待下次接收
return;
}
}
// 遍历完都没找到帧头,清空缓冲区
if (!findNewHead)
{
usbRxMsgBuf.Clear();
}
}
5.3 观察者模式消息分发
上位机采用 观察者模式 进行消息分发。主窗体定义事件委托,各功能子页面通过订阅接收数据 word_7:
csharp
// Form1.cs - 主窗体定义事件
public delegate void deviceRxMsg(byte[] data);
public event deviceRxMsg _deviceRxMsg;
// 各子页面订阅消息事件
this._mainForm._deviceRxMsg += new deviceRxMsg(rxMsgProcess);
| 页面 | 切换时自动请求 |
|---|---|
| 波形显示 | _cmdId_getDeviceParam |
| 数据记录 | _cmdId_getDeviceParam |
| DDS 控制 | _cmdId_ddsDacGetParamTotal |
| DIO 控制 | _cmdId_getIoAll |
| 系统设置 | _cmdId_getSettingParam |
六、CRC16 校验实现
6.1 固件端(C 语言)
D223 固件使用 CRC16-Modbus 算法,采用 查表法 实现 note_63:
c
u16 getCrc16WithTail(unsigned char *puchMsg, unsigned short usDataLen)
{
u8 crcL = 0xFF, crcH = 0xFF; // CRC初始值 0xFFFF
usDataLen -= 2; // 减去尾部2字节CRC本身
while(usDataLen--)
{
uIndex = crcH ^ *puchMsg++;
crcH = crcL ^ tableH[uIndex]; // 查表法高字节
crcL = tableL[uIndex]; // 查表法低字节
}
return (crcH << 8 | crcL);
}
6.2 CRC16-Modbus 算法参数
| 参数 | 值 |
|---|---|
| 多项式 | 0xA001(0x8005 反转) |
| 初始值 | 0xFFFF |
| 输入反转 | 是 |
| 输出反转 | 是 |
| 异或输出 | 0x0000 |
该算法与 Modbus RTU 标准完全一致 pdf_127。
6.3 固件端消息处理
固件端收到数据后,先验证帧头和 CRC,再按 cmdId 分发 note_63:
c
void fun_msgProcess_task(u8 port, u8* rxData)
{
msgHead *_pRx = (msgHead*)rxData;
// 1. 帧头验证
if(_pRx->msgHead != SWAP16(0x55AA))
return;
// 2. CRC16 校验
if(!checkCrc16WithTail((u8*)rxData, SWAP16(_pRx->frameLen)))
return;
// 3. 记录通信端口
global_hostCommunicationPort = port;
// 4. 命令分发
switch(_pRx->cmdId)
{
case _cmdId_getIoAll: fun_msgGetIoAll(); break;
case _cmdId_getAdcValue: fun_msgGetAdcValue(); break;
case _cmdId_setDdsDacParam: fun_msgSetDdsDacParam(rxData); break;
// ... 更多命令
}
}
七、RT-Thread 多线程架构
7.1 线程模型
D223 固件基于 RT-Thread RTOS,核心线程如下 note_63:
| 线程 | 创建函数 | 优先级 | 职责 |
|---|---|---|---|
| ADC 功能线程 | rt_adcFunc_init() |
0(最高) | ADC 采集、双缓冲管理、数据上传 |
| USB 消息处理线程 | rt_msgProcess_init() |
1 | 接收上位机指令、命令分发、回复数据 |
| 主循环 | while(1) |
--- | 动态更新采样率、读取 FPGA 寄存器 |
7.2 定时器分工
| 定时器 | 时钟源 | 功能 |
|---|---|---|
| TIM12 | 240MHz | USB 接收超时检测(1ms 周期) |
| TIM13 | 240MHz | ADC 数据采集触发 |
| TIM14 | 1MHz | DAC 波形输出触发 |
7.3 系统时钟树
D223 使用 STM32H743,时钟配置如下 note_63:
HSE 25MHz → PLLM=5 → 5MHz → PLLN=192 → 960MHz (VCO)
├── PLLP=2 → 480MHz (SYSCLK)
└── PLLQ=20 → 48MHz (USB)
SYSCLK 480MHz → HCLK 240MHz (DIV2)
→ APB1 120MHz (DIV2)
→ APB2 120MHz (DIV2)
TIM 时钟 = 240MHz (APB1 定时器时钟)
TIM13 使用 240MHz 时钟计算采样率:
ARR = 240000000 / samplingRate - 1note_63
八、ADC 双缓冲机制
D223 在在线记录模式下采用 AB 双缓冲 机制,避免数据丢失 note_63:
┌─────────────────┐
FPGA FIFO ──────→ │ 缓冲区 A │ ──→ USB 上传
│ 缓冲区 B │ ──→ USB 上传
└─────────────────┘
工作流程:
1. 数据写入缓冲区A,A满后切换到B
2. 同时上传缓冲区A的数据
3. B满后切换回A,同时上传B的数据
4. 循环往复...
关键代码逻辑:
c
// 双缓冲标志: 0=A缓冲, 1=B缓冲
_uadc.recordAB_flag
// A缓冲满 → 标记flag=2, 切换到B
// B缓冲满 → 标记flag=3, 切换回A
// 数据写入(大端转换)
_adcBuf.data[_uadc.adc_collectIndex++] = SWAP16(ADC数据);
FPGA FIFO 读取采用 QSPI DMA 方式,每次读取 66 字节(包含索引和 ADC 数据),通过判断 FIFO 写索引差值来计算新数据量 note_63。
九、DDS 波形输出详解
9.1 工作模式
D223 的 4 通道 DAC 支持 6 种工作模式 note_63:
| 模式 | 枚举值 | 说明 |
|---|---|---|
| 停止输出 | 0x00 | DAC 输出 0V |
| 直流输出 | 0x01 | 输出固定电压 |
| 正弦波 | 0x02 | 500 点查找表 + 相位累加 |
| 三角波 | 0x03 | 线性上升 → 线性下降 |
| 方波 | 0x04 | 占空比可调 |
| 自定义波形 | 0x05 | 从数组循环查表 |
9.2 DDS 计算核心
正弦波使用 500 点查找表 + 相位累加器 实现 note_63:
c
// 相位累加
_ddsDac.dacIndex[i] += freq * 500000 / interruptFreq;
_ddsDac.dacIndex[i] %= 500000; // 模500000(500点 × 1000精度)
// 查表索引
dacIndexInt = _ddsDac.dacIndex[i] / 1000;
// 查表 + 幅度 + 偏置
_ddsDac.dacCCR[i] = sinTable[dacIndexInt] * amp / 65536 + bias;
多通道相位同步 :同频通道共享通道 0 的相位累加器,仅叠加相位偏移 phase * 500000 / 360,实现多通道相位同步输出 note_63。
9.3 DAC 硬件输出
DAC 通过 SPI2 逐通道写入 DAC 芯片,由 TIM14 中断驱动 note_63:
c
void ddsDac_tim_task()
{
if(任意通道使能)
{
ddsDacCalc(); // 计算所有通道的CCR值
// 通道1: 寄存器地址 0x08
DA_CS(0);
dac_txBuf[0] = 0x08;
dac_txBuf[1] = _ddsDac.dacCCR[0] >> 8; // 高8位
dac_txBuf[2] = _ddsDac.dacCCR[0] & 0xFF; // 低8位
HAL_SPI_Transmit(&hspi2, dac_txBuf, 3, 1);
DA_CS(1);
// 通道2: 0x0a, 通道3: 0x0c, 通道4: 0x0e 同理
}
}
9.4 DAC 输出率动态调整
TIM14 基于 1MHz 时钟,DAC 输出率可通过 FRAM 参数动态调整 note_63:
c
u32 dacFreq = _framDatas.dacOutRate; // 目标输出率
htim14.Instance->ARR = 1000000 / dacFreq - 1; // 设置定时器重载值
十、与 7606 通信能力对比
D223 和 7606 虽然使用相同的协议体系,但通信通道有显著差异 word_3:
| 特性 | D223 | 7606 |
|---|---|---|
| 主通信 | USB 2.0 HS(CDC 虚拟串口) | USB Type-C(CDC 虚拟串口) |
| RS485 | USART2(PD4/PD5/PD6,经 FPGA) | USART1(PA9/PA10,SP3485 收发器) |
| 以太网 | ❌ 无 | ✅ W5100S 硬件 TCP/IP |
| Modbus RTU | ❌ 无独立串口 | ✅ 独立 RS485 串口 |
| Modbus TCP | ❌ 无 | ✅ 以太网 TCP |
| FPGA 中转 | ✅ 有 | ❌ 无 |
| 帧协议 | 0x55AA + CRC16 | 0x55AA + CRC16(相同) |
7606 支持 三套独立通信通道,三通道对比 word_3:
| 特性 | USB 自定义协议 | Modbus RTU | Modbus TCP |
|---|---|---|---|
| 物理层 | USB 虚拟串口 | RS485 串口 | 以太网 TCP |
| 帧头 | 0x55AA | 设备地址 | MBAP 头 |
| 校验 | CRC16(帧尾) | CRC16(帧尾) | 无(TCP 保证) |
| 数据量 | 大(波形数据) | 小(寄存器) | 小(寄存器) |
| 用途 | 波形 + 控制 | 工业控制 | 工业控制 |
| 独立串口 | 主串口 | 独立串口 | Socket |
D223 仅保留 USB 通道,定位为 中速采集 + DDS 信号发生,面向不需要工业总线但对采样率有要求的场景。
十一、开发资源与工具链
11.1 开源资料清单
D223 的全部开源资料存放在百度网盘 word_8:
| 资料包 | 内容 |
|---|---|
DABM-D223上位机直接运行版.7z |
zlTool 免安装上位机,无需安装任何软件即可使用 |
C#_visualStudio_DABM_D223.7z |
C# 上位机源码(Visual Studio 2022) |
STM32 DABM D223.7z |
STM32 固件源码(Keil MDK 5 + RT-Thread) |
FPGA DABM_D223.7z |
FPGA 工程文件(高云半导体) |
DABM-D223.pdf |
原理图(PDF 格式) |
11.2 开发工具链
| 工具 | 用途 | 版本 |
|---|---|---|
| Keil MDK 5 | STM32 固件开发 | 最新版 |
| STM32CubeMX | STM32 配置工具 | 最新版 |
| RT-Thread | 实时操作系统 | 支持包 |
| Visual Studio 2022 | C# 上位机开发 | 社区版 |
| QT 5.14.2 | QT 上位机开发 | --- |
| LabVIEW 2024 | LabVIEW 开发 | Q1 (32位) |
| 高云 FPGA IDE | FPGA 开发 | --- |
| Source Insight | 代码阅读 | --- |
| CH340 驱动 | USB 虚拟串口驱动 | --- |
⚠️ LabVIEW 说明 :手册中列出 LabVIEW 为支持的开发环境,网盘也提供了 LabVIEW 2024 安装包,但 未提供 LabVIEW VI 例程文件。如需在 LabVIEW 中使用 D223,需自行通过 VISA 串口控件实现 0x55AA 协议通信 word_8。
11.3 快速上手建议
- 安装 CH340 驱动 → 连接 D223 → 确认设备管理器中出现 COM 口
- 运行 zlTool → 选择对应 COM 口 → 点击连接 → 验证通信正常
- 阅读 C# 源码 → 重点看
msgStruct.cs(协议定义)和Form1.cs(接收状态机) - 二次开发 → 参考
msgStruct结构体体系,实现自己的协议解析逻辑
十二、总结
核心要点
| 要点 | 说明 |
|---|---|
| 通信方式 | USB 2.0 HS + CDC 虚拟串口 + CH340 驱动 |
| 协议格式 | 0x55AA + cmdId + frameLen + data + CRC16,大端序 |
| CRC 算法 | CRC16-Modbus,多项式 0xA001,初始值 0xFFFF |
| 上位机架构 | 观察者模式 + 100ms 定时器状态机 + 事件驱动 |
| 固件架构 | RT-Thread RTOS + AB 双缓冲 + FPGA 中转 |
| DDS 输出 | 500 点查表 + 相位累加器,支持正弦/三角/方波/自定义 |
| 开发资源 | C# 源码 + STM32 固件 + FPGA 工程 + 免安装上位机 |
开发避坑指南
- 大小端问题 :协议中 FrameLen 和 CRC 是大端序,代码中频繁使用
SWAP16()转换,开发时务必注意 - CRC 校验范围:CRC 覆盖从帧头到 CRC 前的所有字节,计算时需排除尾部 2 字节 CRC 本身
- 100ms 定时器:上位机每 100ms 检查一次缓冲区,高频数据流可能需要调整定时器周期
- FPGA 中转延迟:D223 的 DI/DO 经过 FPGA,与 7606 直连 GPIO 相比存在额外延迟
- DAC 输出范围:0-5V,需要更高电压时需外接运放
- LabVIEW 无例程:需自行通过 VISA 串口实现协议通信
参考资料
- DABM-D223 用户手册(通信协议、命令示例、上位机代码解析)word_8
- DABM-D223 代码解析(GPIO 定义、RT-Thread 线程、DDS/DAC、CRC16、双缓冲、时钟树)note_63
- 7606 上位机代码分析1(帧格式、msgHead 结构体、命令 ID、接收状态机)word_7
- 7606 上位机代码分析2(命令 ID 枚举、三套通信协议、数据流处理)word_3
- 7606 代码分析(固件架构、USB 消息分发、命令 ID 列表)word_4
- MODBUS 软件开发实战指南(CRC16-Modbus 算法原理与实现)pdf_127
本文基于 ZLinear 官方文档和开源代码进行分析。如有出入,请以厂家最新文档和源码为准。
如果觉得有用,欢迎点赞收藏!有问题欢迎评论区交流~