硬件与软件交互全解析:协议、控制与数据采集实践

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)
java 复制代码
// 十六进制表示
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 创建协议帧

协议帧 = 固定格式的 "数据容器",专门解决 "双方怎么准确传数据" 的问题。

java 复制代码
// 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);  // 长度+标志
    
    // 设备ID
    orders[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)

java 复制代码
// 第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)

java 复制代码
// 第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;  // 0xXX
    
    if (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;      // 地址块1
        order.part2[0] = (byte) (nSample);     // 1-5号阀
    } else if (nSample >= 6 && nSample < 12) {
        order.part1[0] = ADDR_TAP2_APP1;      // 地址块2
        order.part2[0] = (byte) (nSample - 6); // 6-11号阀
    } else if (nSample >= 12) {
        order.part1[0] = ADDR_TAP3_APP1;      // 地址块3
        order.part2[0] = (byte) (nSample - 12); // 12+号阀
    }
    
    base.sendOrder(new String(createOrder(order), "XXXXXX"));
}

物理示例:开 1 号阀→抽 1 号瓶气体,开 2 号阀→抽 2 号瓶气体。

3.3 采样控制 sampleValue(int port)

java 复制代码
// 第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)

java 复制代码
// 第374-378行
public void zeroValue(boolean bOpen) {
    logger.info("send order 5...");
    ctrlValves(12, bOpen);  // 控制12号电磁阀
}
  • 零点阀作用:切换环境空气,建立测量基准;关闭时接通样气。

3.5 循环模式 circlMode(boolean bOpen)

java 复制代码
// 第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)

java 复制代码
// 第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. 数据采集:硬件→软件

java 复制代码
// 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 初始化串口

java 复制代码
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 发送数据

java 复制代码
// 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 接收数据

java 复制代码
// 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 协议帧解析

java 复制代码
// SerialComm.java 第311-396行
private boolean receiveData(int nData) {
    byte uReChar = (byte) nData;
    
    // 步骤1:检测帧头 0x5A
    if ((0x5A == uReChar) && (0 == m_RecNum)) {
        m_RecBuf[0] = uReChar;
        m_RecNum = 1;
        return false;
    }
    
    // 步骤2:检测第二字节 0xA5
    if ((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 电压→光强

java 复制代码
// RealTimeData.java 第187-189行
private static double caculateVotage(double dLight) {
    return dLight * 4.096 / 65535;  // 16位ADC转换
}

7.2 光强→浓度

java 复制代码
// ThicknessData.java
public static double caculateC12ThickChange(double light) {
    // 光强 → C12浓度
    return ...; // 复杂算法
}

public static double caculateC13ThickChange(double light) {
    // 光强 → C13浓度
    return ...; // 复杂算法
}

7.3 Delta 计算

java 复制代码
// 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 最终计算

java 复制代码
// 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 串口连接问题

java 复制代码
// 现象:无法打开串口
// 可能原因: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 数据接收异常

java 复制代码
// 现象:接收不到数据
// 检查点:
// 1. 串口是否打开
if (!bIsConnected) {
    logger.error("串口未连接");
    return;
}

// 2. 数据校验是否正确
if (checksum != calculatedChecksum) {
    logger.info("数据校验失败");
    return;
}

// 3. 监听器是否注册
serialPort.notifyOnDataAvailable(true);

7.3 硬件响应超时

java 复制代码
// 现象:发送指令后硬件无响应
// 解决方案:增加重试机制

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;
}
相关推荐
Larry_Yanan14 小时前
QML学习笔记(四十八)QML与C++交互:QML中可实例化C++对象
c++·笔记·qt·学习·ui·交互
2301_8035545214 小时前
c++调用客户端库与kafka交互
c++·kafka·交互
猫林老师1 天前
HarmonyOS语音交互与媒体会话开发实战
交互·harmonyos·媒体
2501_938780281 天前
《轨道交通检测系统中 Qt 与数据库交互的优化方案》
数据库·qt·交互
Hare_bai1 天前
WPF的MVVM模式核心架构与实现细节
ui·架构·c#·wpf·交互·xaml·mvvm
程序员小远1 天前
selenium元素定位---(元素点击交互异常)解决方法
自动化测试·软件测试·python·selenium·测试工具·测试用例·交互
Vizio<2 天前
《模仿人类皮肤层与环层小体的社交交互机器人皮肤》2024 IEEE/ASME TMECH 论文解读
人工智能·笔记·学习·机器人·交互·触觉传感器
PineappleCode2 天前
Canvas 复杂交互步骤:从事件监听 to 重新绘制全流程
交互
Jason_zhao_MR2 天前
RK3576机器人核心:三屏异显+八路摄像头,重塑机器人交互与感知
linux·人工智能·嵌入式硬件·计算机视觉·机器人·嵌入式·交互