Administrator
Published on 2025-11-21 / 2 Visits
0
0

串口

概念

串口 (Serial Port)一种用于设备间串行通信的接口。常见的串口标准包括 RS-232、RS-422 和 RS-485。

RS232

概念

常用DB9接口,通常只使用三根线:

2号脚:RXD(接收数据)

3号脚:TXD(发送数据)

5号脚:GND(信号地)

属于单端信号传输,全双工,发送和接收使用独立线路,可以同时进行,点对点通信,只能连接两台设备。

大传输距离约15米,速率一般不超过20kbps。抗干扰能力弱,不适合长距离和复杂环境。

接线

设备A的TXD → 设备B的RXD

设备A的RXD ← 设备B的TXD

设备A的GND ↔ 设备B的GND

RS485

概念

常采用两线制(也有四线制但较少),常用A/B线:

A(或+):数据正

B(或-):数据负

GND(信号地)

属于差分信号传输,抗干扰能力更强,半双工,发送和接收共用两根线,同一时间只能单向传输。点对多点通信,一条总线上可挂载多达128个设备,适合组建工业网络。

最大传输距离可达1200米(9600bps时),速率最高可达10Mbps。

采用差分信号,抗干扰能力强,适合工业环境。

差分信号(Differential Signal)是指在两根导线上分别传输一个正向信号和一个反向信号(例如 +V 和 -V),接收端通过比较两根线之间的电压差(Vdiff = V+ - V-)来识别逻辑状态(0或1)。这种传输方式不同于单端信号(一根信号线 + 地线),而是用一对线同时传输信号。

RS-485之所以抗干扰强,根本原因就是它使用差分信号传输,而不是像RS-232那样使用相对于地的单端信号。

接收端不关心A或B线的绝对电平,只关心两根线之间的电压差(VA - VB)。

只要干扰是共模的(即同时影响两根线),它就不会影响最终的电压差判断。这就是所谓的共模抑制比,RS-485收发器通常有非常高的CMRR,能有效抑制高达数十伏的共模干扰。

RS-485通过将信号承载于两根线的“差值”上,使得那些同时影响两根线的“共模干扰”在数学上被相互抵消,从而极大地提高了信号在恶劣工业环境中的完整性和可靠性。

接线

设备A的A → 设备B的A

设备A的B → 设备B的B

(所有设备A/A相连,B/B相连,GND可单点接地)

接收和发送

在RS-485编程中,必须非常明确地区分接收端和发送端,尤其是在软件逻辑层面。这主要是由RS-485的半双工特性决定的。

在RS-485总线上,每个节点(无论是主机还是从机)都连接在同一对差分线(A、B)上。

任何一个节点,在物理上都可以发送数据,也可以接收数据。

关键点:它不能同时发送和接收。在某一时刻,整个总线上只能有一个设备处于发送状态,其他所有设备都必须处于接收状态。

编程时,收发的对象决定,由用户代码控制,你的代码必须清楚地知道当前设备在做什么:

作为发送端(主机)时:

将收发器控制引脚(DE_RE_PIN)设置为高电平(发送模式)。

通过串口外设发送数据。

等待数据完全从硬件缓冲区发送出去。

立即、必须将DE_RE_PIN设置为低电平(接收模式),以释放总线。

作为接收端(从机)时:

确保DE_RE_PIN处于低电平(接收模式)。

通过串口外设接收数据。

根据接收到的数据(如地址)决定是否需要响应。

传输数据协议

各个485设备,遵循的是工业标准协议(自定其实也可以),即Modbus RTU。

在RTU模式下,帧与帧之间必须有至少3.5个字符时间的静默间隔,用于标识一帧的结束和下一帧的开始。

Modbus是严格的“一问一答”模式,Modbus协议的设计哲学是主从式、查询/响应,同一时刻,只能单个发送,单个应答,无法同一时刻所有485同时应答数据,这个需要程序在设计时考虑好。

Modbus RTU

Modbus RTU数据帧由地址域、功能码、数据域、CRC校验四部分组成:

[地址域: 1字节] [功能码: 1字节] [数据域: N字节] [CRC校验: 2字节]

地址域:1字节,标识目标从机地址,有效范围1~247。

功能码:1字节,指示操作类型(如读/写寄存器)。

数据域:长度可变,包含寄存器地址、数量、值等。

CRC校验:2字节,对整帧数据进行循环冗余校验,低字节在前,高字节在后。

规范定义的功能码:

0x01 读线圈状态 读取输出位状态

0x02 读离散输入 读取输入位状态

0x03 读保持寄存器 读取输出寄存器值

0x04 读输入寄存器 读取输入寄存器值

0x05 写单个线圈 写入单个输出位

0x06 写单个寄存器 写入单个寄存器值

0x0F 写多个线圈 写入多个输出位

0x10 写多个寄存器 写入多个寄存器值

每个功能码对应的数据域也不一样,如0x03定义为:

起始地址(2字节) + 寄存器数量(2字节)

1个 Modbus 寄存器存储 2 个字节(16位)

示例:

发送:

01 03 00 00 00 02 CRC

01: 设备地址

03: 功能码(读保持寄存器)

00 00: 起始地址(从地址0开始)

00 02: 寄存器数量(读取2个寄存器)

这个请求的完整意思是:“喂,地址为01的设备,请把你从地址0开始的2个寄存器(也就是地址0和地址1)的数据,通过03功能码返回给我。”

响应:

假设设备上:

地址0的寄存器里存的值是 1234 (十六进制为 0x04D2)

地址1的寄存器里存的值是 5678 (十六进制为 0x162E)

那么设备会返回:

01 03 04 04 D2 16 2E CRC

我们来解析这个响应:

01: 设备地址

03: 功能码

04: 字节数。这里最关键!

你要了 2个寄存器。

每个寄存器是 2字节。

所以总共返回 2 × 2 = 4 字节的数据。这个 04 就是告诉你后面跟着4个字节。

04 D2: 第一个寄存器的值(1234)

16 2E: 第二个寄存器的值(5678)

Java示例

//核心通信类

import com.fazecast.jSerialComm.SerialPort;

public class RS485Comm {

    private SerialPort serialPort;
    // 假设我们用一个虚拟的GPIO引脚来控制DE/RE
    // 在真实嵌入式Java(如Raspberry Pi)中,这会是一个真实的GPIO操作
    private boolean isSendingMode = false; 

    public RS485Comm(String portName) {
        this.serialPort = SerialPort.getCommPort(portName);
        // 常用配置: 9600, 8, N, 1
        serialPort.setBaudRate(9600);
        serialPort.setNumDataBits(8);
        serialPort.setParity(SerialPort.NO_PARITY);
        serialPort.setNumStopBits(SerialPort.ONE_STOP_BIT);
        
        if (serialPort.openPort()) {
            System.out.println("Port opened successfully.");
            // 默认处于接收模式
            setReceiveMode();
        } else {
            System.err.println("Failed to open port.");
        }
    }

    // 切换到发送模式 (拉高DE/RE)
    private void setTransmitMode() {
        if (!isSendingMode) {
            System.out.println("[CTRL] Switching to TRANSMIT mode.");
            // 在真实硬件上,这里会是 GPIO.output(DE_RE_PIN, GPIO.HIGH);
            isSendingMode = true;
        }
    }

    // 切换到接收模式 (拉低DE/RE)
    private void setReceiveMode() {
        if (isSendingMode) {
            System.out.println("[CTRL] Switching to RECEIVE mode.");
            // 在真实硬件上,这里会是 GPIO.output(DE_RE_PIN, GPIO.LOW);
            isSendingMode = false;
        }
    }

    // 发送数据包
    public void sendPacket(byte[] data) {
        setTransmitMode();
        serialPort.writeBytes(data, data.length);
        // 等待数据发送完成
        try {
            Thread.sleep(50); // 简单延时,确保硬件缓冲区已清空
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        setReceiveMode(); // 发送完毕,立即切换回接收模式
    }

    // 接收数据包 (带超时)
    public byte[] receivePacket(int timeoutMs) {
        byte[] buffer = new byte[256];
        long startTime = System.currentTimeMillis();
        
        while (System.currentTimeMillis() - startTime < timeoutMs) {
            if (serialPort.bytesAvailable() > 0) {
                int numRead = serialPort.readBytes(buffer, buffer.length);
                byte[] receivedData = new byte[numRead];
                System.arraycopy(buffer, 0, receivedData, 0, numRead);
                return receivedData;
            }
            try {
                Thread.sleep(10); // 短暂休眠,避免CPU占用过高
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return null;
            }
        }
        return null; // 超时
    }

    public void close() {
        serialPort.closePort();
        System.out.println("Port closed.");
    }
}


// 主机示例
public class Master {

    public static void main(String[] args) {
        // 假设RS-485适配器连接在COM3 (Windows) 或 /dev/ttyUSB0 (Linux)
        RS485Comm comm = new RS485Comm("COM3"); 

        byte slaveAddress = 1;
        byte[] query = {slaveAddress, 0x01, 0x55}; // [地址, 功能码, 数据]

        System.out.println("[MASTER] Sending query to slave " + slaveAddress);
        comm.sendPacket(query);

        System.out.println("[MASTER] Waiting for response...");
        byte[] response = comm.receivePacket(1000); // 等待1秒

        if (response != null && response.length > 0) {
            System.out.printf("[MASTER] Received from %02X: %s\n", response[0], bytesToHex(response));
        } else {
            System.out.println("[MASTER] No response.");
        }

        comm.close();
    }

    // 辅助函数:字节数组转十六进制字符串
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString();
    }
}


//从机示例
public class Slave {

    private static final byte MY_ADDRESS = 1;
    private static RS485Comm comm;

    public static void main(String[] args) {
        comm = new RS485Comm("COM3"); // 从机也连接在同一个串口

        System.out.printf("[SLAVE %02X] Listening for commands...\n", MY_ADDRESS);

        while (true) {
            byte[] receivedData = comm.receivePacket(0); // 无限等待

            if (receivedData != null && receivedData.length > 0) {
                System.out.printf("[SLAVE %02X] Received: %s\n", MY_ADDRESS, bytesToHex(receivedData));

                // 检查地址是否匹配
                if (receivedData[0] == MY_ADDRESS) {
                    System.out.printf("[SLAVE %02X] Address matched. Sending response.\n", MY_ADDRESS);
                    byte[] response = {MY_ADDRESS, 0x02, (byte) 0xAA}; // [我的地址, 功能码, 数据]
                    comm.sendPacket(response);
                }
            }
        }
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString();
    }
}


Comment