恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Android Modbus主站实现:串口收发、CRC校验与多从站轮询
首页
资讯中心
/
Android Modbus主站实现:串口收发、CRC校验与多从站轮询
Android Modbus主站实现:串口收发、CRC校验与多从站轮询
发布时间:2026/10/15 17:11:28
简介面向Android开发者的Modbus通信实现项目基于Java版jamod库移植支持串口、TCP、UDP主从通信适合需要对接PLC、温控器、变频器等工业设备的移动应用开发者。压缩包共173个文件以Java源码为主体辅以png界面素材、apt与html格式的使用文档、配置文件、脚本及独立jar包整体大小仅约869KB结构清晰便于按需取用。目前已有278人参与学习下载。资料围绕Modbus协议核心展开除基础协议解析与功能码、寄存器类型说明外还针对Android虚拟机环境与串口访问进行了适配并给出串口/网络主从设备的具体通信示例与进程映像处理思路覆盖连接创建、请求收发、异常处理及线程模型等关键环节。对照这些示例与文档开发者可快速掌握在Android平台上构建Modbus主站或从站的完整流程再结合具体硬件文档即可完成定制化工业通信应用。1. Android 上做 Modbus 主站串口收发这件事比想象中麻烦产线调试时最常遇到的需求之一是一台安卓平板通过串口接上 Modbus 从站设备又要当主站去读写寄存器。很多人以为就是把数据发出去、收回来真正动手才发现串口打开方式、CRC 校验、超时重发、线程调度全是坑。这套 Android 上的 Modbus-master 源码包解决的就是这件事它自带串口初始化、RTU 帧封装、CRC16 校验、事务状态管理和日志输出适合做 HMI 看板、设备参数配置工具、产线数据采集终端的人直接拿去改。新手照着把串口参数和从站地址一填就能跑通闭环熟手也能在既有协议栈上替换成自己的功能码。2. 串口通信前置波特率、设备节点与收发线程2.1 串口参数为什么默认是 8N1Modbus RTU 跑在串口上用的最普遍的一组参数是波特率 9600 或 115200、8 个数据位、1 个停止位、无校验也就是常说的 8N1。不是每个设备都支持校验位很多老仪表只认 8N1一旦配成 8E1 或者 7E1从站直接不响应。所以我一般先看从站设备铭牌上的波特率和校验设置再在代码里把串口配置做成可配置项而不是写死。public SerialPort openSerialPort(File device, int baudRate) throws IOException { SerialPort serialPort new SerialPort(device, baudRate, 8, 1, N, 0); return serialPort; }这段代码用的是常见的 Android 串口 JNI 方案构造参数依次是设备文件、波特率、数据位、停止位、校验位和流控标志。校验位传N是 Modbus RTU 最稳的选择。流控标志一般传 0不启用硬件流控因为多数 RS485 设备不接 RTS/CTS 线反而容易卡在读等待。2.2 打开串口的代码骨架与权限处理串口设备在 Android 上表现为/dev/ttyS0、/dev/ttyMT1这类节点普通 App 没有直接访问权限需要 root 或者通过chmod 666把设备节点权限放开。实际项目中我一般把权限处理放到 open 方法里避免每次启动都要手动改权限。private File getSerialDevice() { File dev new File(/dev/ttyS1); if (!dev.exists()) { dev new File(/dev/ttyMT1); } return dev; } private void prepareDevicePermission(File device) { Process su Runtime.getRuntime().exec(su); su.getOutputStream().write((chmod 666 device.getAbsolutePath() \n).getBytes()); su.getOutputStream().flush(); su.waitFor(); }这里prepareDevicePermission是典型的权限放开操作设备节点路径是经常变的东西换一台板子就要重新确认。我建议把所有候选设备节点放在一个数组里遍历查找存在哪个用哪个这样换硬件时只改配置数组。2.3 读线程必须单独开不能被 UI 拖死一个容易翻车的地方是把串口读取直接放在主线程里。串口的 read 是阻塞的主线程一旦被阻塞整个界面就卡死超时之后才能恢复。正确做法是单独起一个读线程循环读取数据并丢给解析器处理。private void startReadThread(SerialPort serialPort) { new Thread(() - { byte[] buffer new byte[256]; InputStream inputStream serialPort.getInputStream(); while (!Thread.currentThread().isInterrupted()) { int len inputStream.read(buffer); if (len 0) { byte[] data new byte[len]; System.arraycopy(buffer, 0, data, 0, len); handleReceivedData(data); } } }).start(); }核心点是read返回的长度不稳定一次可能只读到半个帧所以handleReceivedData里还需要做粘包拆包处理按超时时间把不是完整 Modbus 帧的残包缓存起来等后续字节到达再拼凑。读线程退出时记得interrupt()并关闭流不然线程会永久卡在阻塞读上。3. Modbus RTU 主站帧构造与 CRC 校验命令从哪里来3.1 RTU 帧结构拆解与应用场景Modbus RTU 主站发的每一帧都有固定格式从站地址 1 字节、功能码 1 字节、数据区 N 字节、CRC 校验 2 字节。以读保持寄存器为例数据区包含起始寄存器地址 2 字节和寄存器数量 2 字节全部是大端序。这是 Modbus 最基础也最重要的一点地址和数量都是高字节在前Java 里用ByteBuffer默认大端序刚好匹配但用DataOutputStream时要小心默认是小端写入。字段名长度字节说明从站地址11~247主站发命令时填目标从站地址功能码103 读保持寄存器、06 写单个寄存器、16 写多个寄存器等数据区N随功能码变化读写地址、数量、数据值CRC 低位1CRC16 计算结果的低字节在前CRC 高位1CRC16 计算结果的高字节在后需要注意功能码 03 和 04 的差别03 读保持寄存器04 读输入寄存器同一台设备上两种寄存器的地址空间可能重叠读错功能码会拿到完全不同的数据。封装命令时一定要把功能码做成参数方便上位机侧自由切换。3.2 CRC16-Modbus 校验代码与字节序CRC 算法是 RTU 帧正确性的关键。标准 CRC16-Modbus 的初始化值是0xFFFF多项式是0xA001计算完还要按低字节在前输出很多人在这一步翻车。public static int crc16Modbus(byte[] data) { int crc 0xFFFF; for (byte b : data) { crc ^ (b 0xFF); for (int i 0; i 8; i) { if ((crc 1) ! 0) { crc (crc 1) ^ 0xA001; } else { crc crc 1; } } } return crc; }crc运算全程用无符号模式b 0xFF把负数变成 0~255 的正数。返回值的低字节要拼到帧末尾高字节跟随其后。有个自测技巧把完整帧含 CRC再丢进同一个函数如果结果是0x0000说明帧内的 CRC 是对的商品级工具验证也都用这个办法。3.3 常用功能码报文设计与封装实际工程里最常用到的是读保持寄存器、写单个寄存器、写多个寄存器。以写单个寄存器为例功能码是 06数据区包含寄存器地址 2 字节、值 2 字节。封装这些命令时我习惯用一个独立类。public static byte[] buildWriteSingleReg(byte addr, int regIndex, int value) { ByteBuffer buffer ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN); buffer.put(addr); buffer.put((byte) 0x06); buffer.putShort((short) regIndex); buffer.putShort((short) value); byte[] frameWithoutCrc buffer.array(); int crc crc16Modbus(frameWithoutCrc); ByteBuffer fullBuffer ByteBuffer.allocate(9).order(ByteOrder.BIG_ENDIAN); fullBuffer.put(frameWithoutCrc); fullBuffer.put((byte) (crc 0xFF)); fullBuffer.put((byte) ((crc 8) 0xFF)); return fullBuffer.array(); }缓冲区分配大小是按帧长预计算的8 字节数据加 1 字节 CRC 就是 9 字节。regIndex和value用short接收天然符合大端格式。如果寄存器数量超过 125 个Modbus 协议规定分多条命令发送这个在封装处就要提前拦截避免底层传参溢出。4. 事务封装与多从站轮询超时重发与数据解析4.1 主站事务状态机从发送到确认的完整闭环一个合格的主站不是简单把命令丢到串口就完事它要跟踪每个命令的状态已发送、等待响应、超时、重发、完成。我常用一个轻量事务管理对象每个事务持有命令、从站地址、期望功能码、超时次数和回调。public class ModbusTransaction { private byte[] sendFrame; private byte deviceAddr; private int functionCode; private int retryTimes; private int maxRetry 3; private long timeoutMs 500; private long sentTime; public boolean isExpired() { return System.currentTimeMillis() - sentTime timeoutMs; } }sentTime在发送成功那一刻记录每次主循环检查isExpired。超过timeoutMs且重发次数小于maxRetry把同一帧重新发出去。这里有个经验值串口轮询间隔宁可慢一点也不要设 200ms 的激进超时现场设备响应慢是常态500ms 起步比较稳。4.2 多从站轮询调度排队与间隔控制实际项目里一条总线上通常挂着 5~10 台从站轮询时得顺序发命令不能并发往串口里塞数据。我用一个事务队列保存所有待发任务主线程按固定时间片取出队首事务发送然后进入等待状态。public void pollLoop() { while (running) { ModbusTransaction tx pollNext(); if (tx ! null) { sendFrame(tx.sendFrame); tx.sentTime System.currentTimeMillis(); waitForResponse(tx); } sleepInterruptibly(50); } }轮询间隔由两部分组成一个从站的响应等待时间加上固定的 50ms 兜底延时。如果从站响应很慢把timeoutMs调大即可。注意轮询循环不能因为一个从站永远无响应就卡死整条总线遇到超时三次的从站要跳过它继续轮询下一个这在waitForResponse内部就要做判断。4.3 响应解析与异常码识别区分正常帧、异常帧和噪声读回来的字节要先判断是不是完整帧再从帧里拆出功能码和异常码。Modbus 有个规约细节如果从站返回的功能码最高位置 1比如请求 03 返回 0x83说明从站拒绝了请求真正的错误码在数据区的第一个字节。public static int parseResponse(byte[] response, byte addr, int expectFunc) { if (response.length 3) return RESP_INCOMPLETE; if (response[0] ! addr) return RESP_WRONG_ADDR; int func response[1] 0xFF; if ((func 0x80) ! 0) { return response[2] 0xFF; // 异常码 } if (func ! expectFunc) return RESP_WRONG_FUNC; return RESP_OK; }正常帧和异常帧的标志很好认但最常见的却是收到完全不期待的数据。比如总线上有噪声读到的字节长度不对、CRC 校验不过这时不能把垃圾数据当响应。所以解析前先做 CRC 再匹配地址和功能码三个条件缺一个都丢弃。现场往往是噪声和正常响应混合出现宁丢勿错丢弃后等超时重发才可靠。5. 避坑指南从站无响应、CRC 错乱与串口抢占5.1 从站无响应先查波特率、从站地址和 RS485 方向现象是主站把命令发出去了日志里也能看到发送的十六进制帧但从站就是没有任何回复。第一次遇到这种问题我怀疑过是代码问题后来查下来是波特率配错了设备实际工作在 9600主站配了 115200两边鸡同鸭讲。最快的排查办法是用串口助手先直接发一帧标准的读命令如果收发两端都正常说明问题在主站逻辑上如果串口助手也没响应就要检查 RS485 的方向控制。很多 USB 转 RS485 线会自动切换收发方向但某些板载 RS485 芯片需要 GPIO 拉高才能发送发出后还要延时释放总线否则从站的响应发不回来被自己吞掉。解决进入调试模式时把波特率、设备地址、功能码、数据全部打印出来先用 PC 工具验证再检查 RS485 方向引脚的延时是否在发送完成后保留了 5~10ms。5.2 CRC 校验总失败整帧校验还是去校验和的问题现象是响应帧长度正确、地址也对、功能码也对但 CRC 校验一直失败。最经典的错误是把包含 CRC 的完整响应帧反复拿去跑 CRC 算法然后和帧内 CRC 对比得出失败结论。正确做法要么只用数据部分计算后和帧内 CRC 对比要么把整帧代入算法判断结果是否为0x0000。另外很多从站响应帧前面会带一个前导零或换行符如果没做粘包处理就把多余字节带进计算CRC 必然失败。解决解析响应时先按帧结构剥离地址、功能码和数据区再对这整段不带 CRC 的部分计算 CRC帧头帧尾的噪声通过超时拆包机制隔离。5.3 UI 卡顿和串口被其他进程抢占现象是界面操作时偶尔卡住几秒或者某一次发送命令后数据一直刷新不出来过一会儿又恢复正常。常见原因有两个某个地方把读线程或者在主线程上执行了串口写操作另一个是同一串口节点同时被系统后台服务或其他 App 抢占比如某应用在自动检测设备时占用了/dev/ttyS1导致当前进程打开失败。解决把所有串口写操作统一放到发送线程读操作只允许读线程执行打开设备前用异常捕获失败后重试指定次数同时检查是否有其他进程占用节点必要时在打开前强制 close。5.4 读回来的浮点数量级不对寄存器字序 AB/CD 还是 CD/AB现象是读到的寄存器值本身没错但用两个寄存器拼成 float 后数值明显不对比如实际温度是 25.6读出来却变成 6.8e-35。原因在于 Modbus 寄存器按 16 位存储32 位 float 需要拼两个寄存器不同厂家对高低字顺序定义不同。有设备高字在前、低字在后也有反过来的代码不做兼容就出现这种玄学错误。解决加一个字节序配置项支持ABCD和CDAB两种拼接顺序。解析时先读两个 int16再根据配置决定用哪个做高 16 位最后合成 float。这个配置要放到和波特率同级的参数文件里换设备时一目了然。6. 验证技巧用模拟从站和日志把主站跑稳把主站功能写完不能直接上真机就完事。我习惯先用 PC 端串口模拟工具做一个简单的从站回显这样能快速验证主站发送的帧格式是否正确。模拟从站设置好波特率 9600、地址 1把主站发来的命令原样打印出来再手工组织一个正常响应帧发回去。这个过程能暴露 90% 的封帧问题比如地址错位、CRC 字节顺序反了、寄存器数量超范围。日志是另一个必需品。在主站的发送和接收两侧都加上十六进制日志带毫秒时间戳方便分析超时和重发的节奏。[ send ] 2025-01-13 10:00:00.123 01 03 00 00 00 02 C4 0B [ recv ] 2025-01-13 10:00:00.612 01 03 04 00 01 00 02 85 B6看到上面这种日志格式第一眼就能判断出来回时间差 489ms说明超时阈值不能低于 500ms响应帧长度 7 字节数据区 4 字节表示读到了两个寄存器。如果响应帧长度不符也能直接看出是拆包逻辑漏了。从那以后我每次联调新设备都强制走一遍同样的流程先模拟从站验证封帧再上真实设备跑半小时日志统计重发率重发率超过 1% 就去查波特率和线缆质量。这套习惯救过不少次现场希望帮到你。本文还有配套的精品资源点击获取