硬件与软件交互全解析:协议、控制与数据采集实践
1. 系统概述
1.2 硬件架构
┌─────────────────────────────────────────────────────┐
│ 医疗检测设备 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 泵 │ │ 电磁阀 │ │ 传感器 │ │ LED │ │
│ │(抽气) │ │(开关) │ │(检测) │ │(指示) │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ ↓ │
│ ┌──────────┐ │
│ │ 主控板 │ │
│ │ (Arduino) │ │
│ └─────┬─────┘ │
│ │ RS232 │
│ ↓ │
└─────────────┼─────────────────────────────────────┘↓┌──────────┐│ 电脑 ││ (Java) │└──────────┘
1.3 通信方式
- 接口:RS232 串口(COM1/COM2/COM3...)
- 速率:9600/115200bps
- 协议:自定义二进制
2. 通信协议详解
2.1 协议帧结构
┌─────┬─────┬────────┬────────┬────────┬─────┐
│ 0x5A│ 0xA5│ 长度+5 │设备ID高│设备ID低│命令 │
└─────┴─────┴────────┴────────┴────────┴─────┘固定头 固定头+─────┬─────┬─────┬──────┐
│参数1│参数2│参数3│校验和│
└─────┴─────┴─────┴──────┘
示例解析(控制泵速度 = 6)
// 十六进制表示
5A A5 08 02 05 B8 06 00 XX// 字节分解
0x5A → 固定头1
0xA5 → 固定头2
0x08 → 数据长度(8字节) + 5
0x02 → 主控板ID高字节
0x05 → 应用模块ID(5=泵控制模块)
0xB8 → 命令字(控制泵)
0x06 → 泵速度低字节(6)
0x00 → 泵速度高字节(0)
0xXX → 校验和
2.2 设备 ID 定义
| ID | 模块 | 功能 |
|---|---|---|
| 0x02 | 主控板 | 协调各模块 |
| 0x01 | APP1 | 电磁阀控制 |
| 0x03 | APP3 | 电压采集 |
| 0x05 | APP5 | 泵控制 |
| 0x06 | APP6 | 打印控制 |
2.3 创建协议帧
协议帧 = 固定格式的 “数据容器”,专门解决 “双方怎么准确传数据” 的问题。
// NewProtocol.java 第123-164行
public byte[] createOrder(final Order order) {byte nCount = (byte) (order.commad_str[1] & 0x0f);byte[] orders = new byte[nCount + 8];// 固定头orders[0] = 0x5a;orders[1] = (byte) 0xa5;orders[2] = (byte) ((1 << 7) + nCount + 5); // 长度+标志// 设备IDorders[3] = (byte) (order.commid_str[0] & 0x1f);orders[4] = (byte) (order.commid_str[1] & 0x1f);// 命令orders[5] = order.commad_str[0];orders[6] = (byte) ((order.commad_str[1] & 0xf0) >> 4);// 参数(1-8个)switch (nCount) {case 8: orders[14] = order.part4[1];case 7: orders[13] = order.part4[0];case 6: orders[12] = order.part3[1];case 5: orders[11] = order.part3[0];case 4: orders[10] = order.part2[1];case 3: orders[9] = order.part2[0];case 2: orders[8] = order.part1[1];case 1: orders[7] = order.part1[0];}// 计算校验和int num = 0;for (int i = 3; i < (nCount + 7); i++) {num += orders[i];}orders[nCount + 7] = (byte) num;return orders;
}
3. 硬件控制接口详解
3.1 泵控制 ctrlPump(int nRate)
// 第248-266行
public void ctrlPump(int nRate) {Order order = new Order();order.commid_str[0] = MASTER_ID; // 0x02 主控板order.commid_str[1] = APP5_ID; // 0x05 泵模块order.commad_str[0] = COMMAND_HGKGP_PUMP; // 0xB8 命令// 速度值(16位)order.part1[0] = (byte) (nRate & 0xff); // 低字节order.part1[1] = (byte) ((nRate >> 8) & 0xff); // 高字节base.sendOrder(new String(createOrder(order), "XXXXXX"));
}
- 速度参数含义:
0= 停止,6= 低速采集,8= 中速清空,10= 快速清空
3.2 电磁阀控制 ctrlValves(int nIndex, boolean bOpen)
// 第269-299行
public void ctrlValves(int nSample, boolean bOpen) {Order order = new Order();order.commid_str[0] = MASTER_ID; // 0x02 主控板order.commid_str[1] = APP1_ID; // 0x01 电磁阀模块order.commad_str[0] = COMMAND_HGKGP_SETTAP; // 0xXXif (bOpen) {order.commad_str[1] = (byte) ((COMMAND_SETTAP_OPEN << 4) + 3);} else {order.commad_str[1] = (byte) ((COMMAND_SETTAP_CLOSE << 4) + 3);}// 根据电磁阀编号选择地址if (nSample < 6) {order.part1[0] = ADDR_TAP1_APP1; // 地址块1order.part2[0] = (byte) (nSample); // 1-5号阀} else if (nSample >= 6 && nSample < 12) {order.part1[0] = ADDR_TAP2_APP1; // 地址块2order.part2[0] = (byte) (nSample - 6); // 6-11号阀} else if (nSample >= 12) {order.part1[0] = ADDR_TAP3_APP1; // 地址块3order.part2[0] = (byte) (nSample - 12); // 12+号阀}base.sendOrder(new String(createOrder(order), "XXXXXX"));
}
物理示例:开 1 号阀→抽 1 号瓶气体,开 2 号阀→抽 2 号瓶气体。
3.3 采样控制 sampleValue(int port)
// 第381-391行
public void sampleValue(int nSample) {if (nSample <= 10) {ctrlValves(1 + nSample, true); // 打开对应电磁阀} else if ((nSample > 10) && (nSample < 16)) {ctrlValves(2 + nSample, true);} else if (nSample == 16) {ctrlValves(1, true);}
}// 示例:sampleValue(1)
// → ctrlValves(2, true)
// → 打开2号电磁阀
// → 从1号采样瓶抽气
调用示例:sampleValue(1) 打开 2 号电磁阀,抽取 1 号采样瓶气体;sampleValue(2) 打开 3 号电磁阀,抽取 2 号采样瓶气体。
3.4 零点控制 zeroValue(boolean bOpen)
// 第374-378行
public void zeroValue(boolean bOpen) {logger.info("send order 5...");ctrlValves(12, bOpen); // 控制12号电磁阀
}
- 零点阀作用:切换环境空气,建立测量基准;关闭时接通样气。
3.5 循环模式 circlMode(boolean bOpen)
// 第343-371行
public void circlMode(boolean bOpen) {Order order = new Order();order.commid_str[0] = MASTER_ID;order.commid_str[1] = APP1_ID;order.commad_str[0] = COMMAND_HGKGP_SETTAP;if (bOpen) {order.commad_str[1] = (byte) ((COMMAND_SETTAP_OPEN << 4) + 4);} else {order.commad_str[1] = (byte) ((COMMAND_SETTAP_CLOSE << 4) + 4);}// 控制0号电磁阀ctrlValves(0, bOpen);
}
- 开循环:气体在腔体循环复用,稳定测量;闭循环:气体单向流动,完成采集后清空。
3.6 LED 控制 ctrlLeds(int nIndex, boolean bOpen)
// 第302-334行
public void ctrlLeds(int nIndex, boolean bOpen) {Order order = new Order();order.commid_str[0] = MASTER_ID;order.commid_str[1] = APP1_ID;order.commad_str[0] = COMMAND_HGKGP_SETLED;order.commad_str[1] = (byte) ((COMMAND_SETLED_FLASH << 4) + 3);// 根据索引选择LED地址int nPort;if (nIndex <= 10) {nPort = 4 - (nIndex - 1) / 2;order.part1[0] = ADDR_LED1_APP1; // LED地址块1} else {nPort = (nIndex - 1) / 2 - 4;order.part1[0] = ADDR_LED2_APP1; // LED地址块2}if (bOpen) {order.part1[1] = (byte) ((0x01 << nPort)); // 点亮} else {order.part1[1] = (byte) (0x00); // 熄灭}base.sendOrder(new String(createOrder(order), "xxxxxxx"));
}
用途:指示当前端口与设备工作状态。
4. 数据采集:硬件→软件
// MainFrame.java 第3109-3158行
case NewProtocol.COMMAND_HGKGP_REALV1V:// 解包数据int nData = (order.commad_str[1] >> 4) & 0x0f; // 采样次数// C12电压(32位)int nV1 = ((order.part2[1]& 0xff)<<24) | ((order.part2[0]&0xff)<<16) | ((order.part1[1]&0xff)<<8) | (order.part1[0]&0xff);// C13电压(32位)int nV2 = ((order.part4[1]& 0xff)<<24) | ((order.part4[0]&0xff)<<16) | ((order.part3[1]&0xff)<<8) | (order.part3[0]&0xff);// 计算平均电压double C12Sum = nV1 * 1.0 / nData;double C13Sum = nV2 * 1.0 / nData;// 添加到实时数据数组RealTimeData.addRealTimeData(C12Sum, C13Sum);// 界面更新SwingUtilities.invokeLater(new Runnable() {public void run() {votageChart.addVotageSeriesData(C12Sum, C13Sum);lC12Votage.setText(String.format("12C:%.2f V", C12Sum));lC13Votage.setText(String.format("13C:%.2f V", C13Sum));// 计算并显示厚度gasChart.addThickSeriesData(ThicknessData.caculateC12ThickChange(C12Sum),ThicknessData.caculateC13ThickChange(C13Sum));}});
5. 串口通信实现
5.1 初始化串口
private boolean openCom() {// 1. 查找串口portId = CommPortIdentifier.getPortIdentifier("COM1");// 2. 打开串口(超时2秒)serialPort = (SerialPort) portId.open("Serial Communication", 2000);// 3. 设置参数serialPort.setSerialPortParams(9600, // 波特率SerialPort.DATABITS_8, // 数据位SerialPort.STOPBITS_1, // 停止位SerialPort.PARITY_NONE // 校验位);// 4. 获取输入输出流outputStream = serialPort.getOutputStream(); // 发送inputStream = serialPort.getInputStream(); // 接收// 5. 添加监听器(接收数据)serialPort.addEventListener(new serialPortListener());serialPort.notifyOnDataAvailable(true);bIsConnected = true;return true;
}
5.2 发送数据
// SerialComm.java 第252-287行
public boolean sendOrder(String strOrder) {if (strOrder != null) {try {// 检查串口状态if (b_com_status && outputStream != null && bIsConnected) {// 转换为字节并发送outputStream.write(strOrder.getBytes("xxxxxxxx"));return true;}} catch (Exception e) {logger.error(e);// 出错时关闭连接close();return false;}}return false;
}
5.3 接收数据
// SerialComm.java 第65-124行
public class serialPortListener implements SerialPortEventListener {public void serialEvent(SerialPortEvent event) {switch (event.getEventType()) {case SerialPortEvent.DATA_AVAILABLE: // 有数据到达while (newData != -1) {newData = inputStream.read(); // 读一个字节// 解析协议帧if (receiveData(newData)) {// 完整帧,通知监听者for (CommunicationListener listener : listeners) {listener.dataAvailable(new String(m_RecBuf, 0, m_RecNum, "xxxxx"));}// 清空缓冲区m_RecNum = 0;}}break;case SerialPortEvent.OE: // 溢出错误logger.info("溢位错误");break;// ... 其他错误}}
}
5.4 协议帧解析
// SerialComm.java 第311-396行
private boolean receiveData(int nData) {byte uReChar = (byte) nData;// 步骤1:检测帧头 0x5Aif ((0x5A == uReChar) && (0 == m_RecNum)) {m_RecBuf[0] = uReChar;m_RecNum = 1;return false;}// 步骤2:检测第二字节 0xA5if ((0x5A == m_RecBuf[0]) && (0xA5 == uReChar)) {m_RecBuf[1] = uReChar;m_RecNum = 2;return false;}// 步骤3:读取数据长度if (m_RecNum == 2) {m_RecBuf[2] = uReChar;int nRec = uReChar & 0x7f;if (nRec < 4 || nRec > 21) {// 长度异常,清空m_RecNum = 0;return false;}M_RECNUMB = nRec; // 保存数据长度m_RecNum = 3;return false;}// 步骤4:读取数据部分if (m_RecNum < M_RECNUMB + 2) {m_RecBuf[m_RecNum] = uReChar;m_RecNum++;return false;}// 步骤5:读取校验和并验证if (m_RecNum == M_RECNUMB + 2) {m_RecBuf[m_RecNum] = uReChar; // 校验和// 计算校验和int nResult1 = 0;for (int i = 3; i < M_RECNUMB - 1; i++) {nResult1 += m_RecBuf[i];}nResult1 = nResult1 & 0xff;// 验证if (nResult1 == nData) {return true; // 校验通过} else {return false; // 校验失败}}return false;
}
6. 数据计算:从电压到 DOB 值
7.1 电压→光强
// RealTimeData.java 第187-189行
private static double caculateVotage(double dLight) {return dLight * 4.096 / 65535; // 16位ADC转换
}
7.2 光强→浓度
// ThicknessData.java
public static double caculateC12ThickChange(double light) {// 光强 → C12浓度return ...; // 复杂算法
}public static double caculateC13ThickChange(double light) {// 光强 → C13浓度return ...; // 复杂算法
}
7.3 Delta 计算
// ThicknessData.java 第111-113行
public static double caculateDelta(double c12, double c13) {// Delta = ((C13/C12)/10000 - 标准值) * 1000 / 标准值return ((c13/c12)/10000 - 0.01123686) * 1000 / 0.01123686;
}
Delta 含义:相对标准偏差(千分率)
7.4 DOB 最终计算
// MainFrame.java 第3515-3528行
// Delta30分钟
double del30 = -25 + (delta_3 - delNihe_3);// Delta0分钟(基准)
double del00 = -25 + (delta_0 - delNihe_0);// DOB = Delta30 - Delta0
double temp = del30 - curDel00;// DLL修正
temp = DllUtil.INSTANCE.caculate(curThick, c12, temp);// 保存最终结果
status.setDob(temp);// 判断结果
if (temp >= 4.0) {阳性(+) // 检测到幽门螺杆菌
} else {阴性(-) // 未检测到
}
7. 调试与常见问题
7.1 串口连接问题
// 现象:无法打开串口
// 可能原因:COM口被占用、波特率不匹配、驱动问题// 解决方案
try {portId = CommPortIdentifier.getPortIdentifier("COM1");serialPort = (SerialPort) portId.open("Serial Communication", 2000);
} catch (NoSuchPortException e) {MessageBoxUtil.showMessageBox(null, "串口不存在");
} catch (PortInUseException e) {MessageBoxUtil.showMessageBox(null, "串口被占用");
}
7.2 数据接收异常
// 现象:接收不到数据
// 检查点:
// 1. 串口是否打开
if (!bIsConnected) {logger.error("串口未连接");return;
}// 2. 数据校验是否正确
if (checksum != calculatedChecksum) {logger.info("数据校验失败");return;
}// 3. 监听器是否注册
serialPort.notifyOnDataAvailable(true);
7.3 硬件响应超时
// 现象:发送指令后硬件无响应
// 解决方案:增加重试机制public boolean sendOrderWithRetry(String order, int maxRetries) {for (int i = 0; i < maxRetries; i++) {if (sendOrder(order)) {// 等待响应Thread.sleep(100);if (checkResponse()) {return true;}}}return false;
}
