拿到D223的开源上位机源码后如何理解和修改?本文从C#代码层面,解析上位机的帧接收解析、CRC校验、波形绘制和校准界面实现。
一、上位机架构总览
D223的C#上位机基于WinForms开发,使用Visual Studio 2022社区版word_3word_10。核心代码结构word_3word_7:
zlTool/
├── msgStruct.cs # 消息协议:帧头定义、CRC校验、命令枚举、校准算法
├── Setting.cs # 设置页面:量程切换、24位数据转换、校准参数
├── WaveForm.cs # 波形页面:数据接收、解析、绘制
├── rxTim定时器 # 100ms接收定时器:帧查找、校验、分发
└── Program.cs # 主程序入口
二、命令ID分类体系
D223上位机与采集卡之间的命令按功能分区word_3:
| 范围 | 类别 | 说明 |
|---|---|---|
| 0x01-0x07 | ADC基础控制 | 通道使能、设备参数、波形数据、触发设置、采样率 |
| 0x08-0x1A | 数据记录 | Flash记录、SRAM记录、在线记录、连续数据读取 |
| 0x20-0x28 | DDS/DAC控制 | 参数获取/设置、工作模式、校准 |
| 0x30-0x34 | IO控制 | IO状态读取、输出设置、PWM控制 |
| 0x40-0x42 | ADC高级设置 | 量程、信号类型、自动触发 |
| 0x50-0x5F | 系统设置 | 校准参数、IP/端口、设备地址/波特率、传感器单位 |
| 0xF0 | 固件版本 | 软件版本号获取 |
这套命令分区体系覆盖了D223的全部功能,共30+条命令word_3。
三、帧接收与解析
3.1 100ms接收定时器
上位机使用100ms定时器rxTim检查接收缓冲区word_6:
每100ms执行一次:
1. 遍历查找帧头 0x55 0xAA
2. 找到帧头后,读取帧长度
3. 判断缓冲区剩余数据 ≥ 一帧完整长度?
- 不够 → 等下一次100ms
- 够 → 继续
4. CRC16校验
- 错误 → 删除错误帧头(0x55AA),重新查找
- 正确 → 交给业务层处理
5. 删除已处理的一帧数据
6. 如果遍历完都没找到0x55AA → 清空缓冲区
3.2 帧结构定义
所有消息统一格式word_6note_63:
csharp
// 帧结构
[帧头: 0x55AA] [命令ID: 1B] [帧长度: 2B] [数据段: NB] [CRC16: 2B]
C#结构体定义word_3:
csharp
public struct msgHead {
public ushort msgHead; // 0x55AA(大端)
public byte cmdId; // 命令ID
public ushort frameLen; // 帧总长度(大端)
}
3.3 大小端处理
STM32是小端,通信协议用大端word_3:
csharp
// 大小端转换宏
#define SWAP16(x) ((((x) & 0xFF) << 8) | (((x) >> 8) & 0xFF))
#define SWAP32(x) ((((x) & 0xFF) << 24) | (((x) & 0xFF00) << 8) | \
(((x) >> 8) & 0xFF00) | (((x) >> 24) & 0xFF))
帧头在内存中存储为0xAA55,但发送到USB线上是0x55 0xAAnote_63。
3.4 CRC16校验
上位机使用CRC16-Modbus算法word_6note_63:
csharp
public static ushort getCrc16WithTail(byte[] data, int len) {
byte crcL = 0xFF, crcH = 0xFF;
len -= 2; // 减去CRC本身2字节
while(len-- > 0) {
int index = crcH ^ data[dataOffset++];
crcH = (byte)(crcL ^ tableH[index]);
crcL = tableL[index];
}
return (ushort)((crcH << 8) | crcL);
}
CRC16-Modbus参数:
- 初始值:0xFFFF
- 多项式:0xA001
- 查表法加速,预计算256个值的tableH和tableLpdf_127
四、波形数据接收与显示
4.1 波形数据包结构
采集卡上传的波形数据包结构word_5:
csharp
struct adcWaveBuf {
ushort msgHead; // 0x55AA
byte cmdId; // 波形数据命令ID
ushort frameLen; // 帧长度
byte adcStartCh; // 起始通道号
byte adc_ch_Enable; // 通道使能位掩码
byte trigCh; // 触发通道
ushort trigValue; // 触发值
ushort samplingRate; // 采样率
byte adcRange; // 量程
byte adcSignalType; // 信号类型
short[] data; // ADC波形数据
ushort crc; // CRC16
}
4.2 数据接收解析
上位机接收波形数据的处理流程word_5:
csharp
void OnWaveDataReceived(byte[] data) {
// 解析数据包
ushort msgHead = (data[0] << 8) | data[1]; // 0x55AA
byte cmdId = data[2];
ushort frameLen = (data[3] << 8) | data[4];
// 提取波形参数
byte adcStartCh = data[5];
byte adcChEnable = data[6];
byte trigCh = data[7];
ushort trigValue = (data[8] << 8) | data[9];
ushort samplingRate = (data[10] << 8) | data[11];
// 提取ADC数据
int dataOffset = 12;
int dataLen = frameLen - dataOffset - 2; // 减去CRC
short[] waveData = new short[dataLen / 2];
for(int i = 0; i < dataLen / 2; i++) {
waveData[i] = (short)((data[dataOffset + i*2] << 8) | data[dataOffset + i*2 + 1]);
}
// 显示波形
DrawWaveform(waveData, samplingRate, trigValue);
}
4.3 波形绘制
波形绘制的核心代码word_5:
csharp
void DrawWaveform(short[] data, int samplingRate, int trigValue) {
// 计算时间轴
float timeStep = 1.0f / samplingRate; // 每点的时间间隔
// 绘制网格
DrawGrid();
// 绘制波形
for(int i = 0; i < data.Length - 1; i++) {
float x1 = i * timeStep;
float y1 = AdcToVoltage(data[i]); // ADC值转电压
float x2 = (i + 1) * timeStep;
float y2 = AdcToVoltage(data[i + 1]);
DrawLine(x1, y1, x2, y2);
}
// 标记触发点
MarkTriggerPoint(trigValue);
// 显示测量参数
DisplayMeasurements(data);
}
4.4 ADC值转电压
csharp
float AdcToVoltage(short adcValue) {
// 根据量程转换
if(adcRange == 1) // ±5V
return (adcValue - 32768) / 32768.0f * 5.0f;
else // ±10V
return (adcValue - 32768) / 32768.0f * 10.0f;
}
五、校准界面实现
5.1 校准参数数据结构
C#上位机中,每通道的校准参数word_7word_6:
csharp
public static class adcCalibParams {
public static Int32[] calib_adcCalib_digitalA = new Int32[8]; // A点数字量
public static Int32[] calib_adcCalib_analogA = new Int32[8]; // A点模拟量(×100000)
public static byte[] calib_adcCalib_enableA = new byte[8]; // A点使能
public static Int32[] calib_adcCalib_digitalB = new Int32[8]; // B点数字量
public static Int32[] calib_adcCalib_analogB = new Int32[8]; // B点模拟量
public static byte[] calib_adcCalib_enableB = new byte[8]; // B点使能
public static Int32[] correctFactor = new Int32[8]; // 校正系数(×100000)
public static Int32[] calibZero = new Int32[8]; // 零点偏移(×100000)
public static bool calibMode; // 校准模式标志
public static byte[] sensorUnit = new byte[8]; // 传感器单位
}
5.2 两点校准算法
C#上位机中的两点线性校准公式word_3:
csharp
// msgStruct.cs
public static double calc_adcCalibData(double data, double d1, double a1,
double d2, double a2) {
return (((data - d1) * (a2 - a1) / (d2 - d1)) + a1);
}
5.3 24位ADC数据转换
Setting.cs中的24位数据转换word_3:
csharp
// Setting.cs
if (_prxData.adcRange == 1) // ±5V量程
value = (SWAPS32(adcValue) - 8388608) / 83886.08 / 10;
else // ±10V量程
value = (SWAPS32(adcValue) - 8388608) / 167772.16 / 10;
// 8388608 = 2^23, 24位ADC中位值
六、固件端的数据上传配合
6.1 采集端数据打包
固件中,波形数据满后的打包上传流程word_5:
c
if(_adcFunctionParam.adc_collectFullFlag == 1) {
// 填充帧头
_adcBuf.msgHead = SWAP16(0x55aa);
_adcBuf.cmdId = _cmdId_getAdc_waveData;
_adcBuf.frameLen = SWAP16(sizeof(_adcBuf));
// 填充参数
_adcBuf.adc_ch_Enable = _framDatas._adcParam.adc_ch_Enable;
_adcBuf.trigCh = _framDatas._adcParam.trig_Ch;
_adcBuf.samplingRate = SWAP16(_framDatas._adcParam.samplingRate);
_adcBuf.trigValue = SWAP16(_framDatas._adcParam.trigValue);
_adcBuf.adcRange = _framDatas._adcParam.adcRange;
_adcBuf.adcSignalType = _framDatas._adcParam.adcSignalType;
// CRC校验
_adcBuf.crc = SWAP16(getCrc16WithTail((u8*)&_adcBuf, SWAP16(_adcBuf.frameLen)));
// USB发送
usb_sendMsg((u8*)&_adcBuf, SWAP16(_adcBuf.frameLen));
_adcFunctionParam.adc_collectFullFlag = 0;
_adcFunctionParam.deviceWorkMode = _deviceWorkMode_Stop;
}
6.2 数据采集流程
固件中的波形数据采集流程word_5:
c
if(_uadc.trig_flag == 1) { // 已触发
for(u8 i = 0; i < 8; i++) {
if(_framDatas._adcParam.adc_ch_Enable & (1 << i)) {
// 通道使能,存储数据
if(_uadc.adc_collectIndex < (ADC_WAVE_ONCE_LEN - 1)) {
// SWAP16大小端转换
_adcBuf.data[_uadc.adc_collectIndex++] = SWAP16(_uadc.getAdc[i]);
} else {
// 缓冲区满,设置上传标志
_adcFunctionParam.adc_collectFullFlag = 1;
_adcFunctionParam.deviceWorkMode = _deviceWorkMode_sendBusy;
break;
}
}
}
}
七、传感器单位支持
7.1 预置单位列表
上位机预置17种传感器单位word_3:
csharp
public static string[] sensorUnitList = {
"V", "mV", // 电压
"A", "mA", // 电流
"℃", "m℃", // 温度
"N", "kg", "g", // 力/重量
"G", "K", "mK", // 转速
"MPa", "kPa", "hPa", "Pa", // 压力
"℉" // 华氏温度
};
7.2 自定义单位扩展
注意:D223支持最大256个自定义单位,可以任意修改note_259。用户可以根据实际传感器定义任意工程单位,远超17种预置单位。
八、实际测量数据示例
D223在实际使用中的测量精度示例word_5:
8.1 输入1.000V时的8通道读数
| 通道 | 读数(V) | 偏差(mV) |
|---|---|---|
| ADC_1 | 1.00276 | +2.76 |
| ADC_2 | 1.00251 | +2.51 |
| ADC_3 | 1.00234 | +2.34 |
| ADC_4 | 1.00182 | +1.82 |
| ADC_5 | 1.00206 | +2.06 |
| ADC_6 | 1.00176 | +1.76 |
| ADC_7 | 0.99975 | -0.25 |
| ADC_8 | 1.00011 | +0.11 |
8.2 输入2.000V时的8通道读数
| 通道 | 读数(V) | 偏差(mV) |
|---|---|---|
| ADC_1 | 1.99693 | -3.07 |
| ADC_2 | 1.99687 | -3.13 |
| ADC_3 | 1.99688 | -3.12 |
| ADC_4 | 1.99616 | -3.84 |
| ADC_5 | 1.99648 | -3.52 |
| ADC_6 | 1.99611 | -3.89 |
| ADC_7 | 1.99681 | -3.19 |
| ADC_8 | 1.99715 | -2.85 |
数据显示未标定状态下偏差在±4mV以内,通过两点标定可将精度提升到0.01% FSRword_5note_244。
九、二次开发建议
9.1 添加自定义命令
- 在msgStruct.cs中定义新的命令ID
- 在rxTim的命令分发switch中添加case
- 实现业务逻辑
9.2 修改波形显示
- 在WaveForm.cs中修改DrawWaveform函数
- 可以添加FFT分析、峰值检测、频率测量等功能
9.3 借助AI工具
ZLinear官方建议:可以借助AI工具轻松实现二次开发,自由定制上位机功能note_259。完整源代码 + AI辅助 = 快速定制。
十、总结
D223上位机C#开发的核心要点:
| 模块 | 关键文件 | 核心功能 |
|---|---|---|
| 帧解析 | rxTim定时器 | 100ms检查、帧头查找、CRC校验 |
| 命令分发 | msgStruct.cs | 30+命令分类管理 |
| 波形显示 | WaveForm.cs | 数据接收、时间轴、波形绘制 |
| 校准管理 | Setting.cs | 两点校准、校正系数、24位转换 |
| 单位转换 | msgStruct.cs | 17种预置 + 256种自定义 |
| 大小端 | SWAP16/SWAP32 | 协议大端 ↔ MCU小端 |
下篇预告:《D223真实应用场景案例:振动监测、电机测试与电力分析》