如何在Linux中编程创建虚拟串口设备实现Modbus从站模拟器?
实现直接创建PTY设备的Modbus RTU从站模拟器
完全可以跳过socat直接实现,核心是创建一对伪终端(PTY)设备:一端绑定你的Modbus RTU从站服务端处理消息,另一端作为可被客户端连接的/dev/pts/x设备,和socat的PTY隧道逻辑完全一致。以下是C和Go语言的实现思路:
C语言实现方案
C语言可直接调用POSIX标准API操作PTY,步骤清晰:
- 创建PTY设备对:用
openpty()简化创建主从设备对,或用更底层的posix_openpt()打开主设备,再通过grantpt()、unlockpt()解锁从设备,最后用ptsname()获取从设备路径(如/dev/pts/10)。 - 配置终端属性:通过
tcsetattr()为主设备配置串口参数(波特率、数据位、停止位、奇偶校验),模拟真实串口特性,必须和客户端参数匹配。 - 绑定Modbus服务端:将主设备的文件描述符作为Modbus RTU从站的IO端口,替换原socat提供的串口设备,在主设备上监听读写事件,解析并响应Modbus RTU帧。
- 客户端连接:让Modbus客户端直接连接
ptsname()返回的从设备路径即可通信。
关键代码片段示例:
#include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <pty.h> #include <termios.h> int main() { int master_fd, slave_fd; char slave_path[256]; struct termios term_attr; // 创建PTY设备对 if (openpty(&master_fd, &slave_fd, slave_path, NULL, NULL) != 0) { perror("openpty failed"); exit(1); } printf("Slave PTY path: %s\n", slave_path); // 配置串口属性(9600波特率、8N1) tcgetattr(master_fd, &term_attr); cfsetispeed(&term_attr, B9600); cfsetospeed(&term_attr, B9600); term_attr.c_cflag &= ~PARENB; // 无奇偶校验 term_attr.c_cflag &= ~CSTOPB; // 1停止位 term_attr.c_cflag &= ~CSIZE; term_attr.c_cflag |= CS8; // 8数据位 tcsetattr(master_fd, TCSANOW, &term_attr); // 将master_fd传入Modbus RTU从站处理逻辑 run_modbus_rtu_server(master_fd); close(master_fd); close(slave_fd); return 0; }
Go语言实现方案
Go语言可通过系统调用操作PTY,核心逻辑和C一致:
- 创建PTY设备对:调用
syscall.Openpt()打开主设备,用syscall.Grantpt()、syscall.Unlockpt()解锁从设备,再通过syscall.Ptsname()获取从设备路径,将主设备文件描述符封装为os.File对象。 - 配置终端属性:通过
syscall.Tcsetattr()配置串口参数,确保和客户端的波特率、奇偶校验等匹配。 - 绑定Modbus服务端:将主设备的
os.File作为Modbus RTU从站的IO接口,替换原串口设备,通过Read()/Write()方法处理Modbus帧收发。
关键代码片段示例:
package main import ( "fmt" "os" "syscall" "unsafe" ) func main() { // 打开PTY主设备 masterFd, err := syscall.Openpt(syscall.O_RDWR) if err != nil { panic(err) } defer syscall.Close(masterFd) // 解锁从设备 if err := syscall.Grantpt(masterFd); err != nil { panic(err) } if err := syscall.Unlockpt(masterFd); err != nil { panic(err) } // 获取从设备路径 slavePath, err := syscall.Ptsname(masterFd) if err != nil { panic(err) } fmt.Printf("Slave PTY path: %s\n", slavePath) // 转换为os.File对象 masterFile := os.NewFile(uintptr(masterFd), "pty-master") defer masterFile.Close() // 配置串口属性(9600波特率、8N1) var termios syscall.Termios _, _, errno := syscall.Syscall6(syscall.SYS_TCGETATTR, uintptr(masterFd), uintptr(unsafe.Pointer(&termios)), 0, 0, 0, 0) if errno != 0 { panic(errno) } termios.Cflag &^= syscall.PARENB // 无奇偶校验 termios.Cflag &^= syscall.CSTOPB // 1停止位 termios.Cflag &^= syscall.CSIZE termios.Cflag |= syscall.CS8 // 8数据位 termios.Ispeed = 9600 termios.Ospeed = 9600 _, _, errno = syscall.Syscall6(syscall.SYS_TCSETATTR, uintptr(masterFd), uintptr(syscall.TCSANOW), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) if errno != 0 { panic(errno) } // 将masterFile传入Modbus RTU从站处理逻辑 runModbusRTUServer(masterFile) }
注意事项
- 必须保证PTY主设备的终端参数(波特率、奇偶校验等)和客户端完全一致,否则会出现通信乱码。
- PTY设备会在进程退出后自动销毁,无需手动清理。
- 普通用户默认拥有创建PTY的权限,无需root(依赖
/dev/ptmx的访问权限)。
内容的提问来源于stack exchange,提问作者microo8
相关产品推荐
相关产品推荐

