尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Android平台Modbus主站实现:串口通信与协议解析实战
简介这份资源面向需要在 Android 平台上实现 Modbus 通信的开发者尤其适合从事工业自动化设备对接、希望用手机或平板控制 PLC、温控器、变频器等硬件的工程师。项目以 Java 为主将开源 Modbus 库 jamod 适配到 Android 环境覆盖串口 RTU 与 TCP/IP 两种通信方式涉及协议帧构建与解析、串口交互、后台线程管理及异常处理等关键环节属于需要一定 Android 与 Java 基础的中阶实践素材。压缩包共 173 个文件以 105 个 java 源码为核心辅以 33 个 png 图示、14 个 apt 文档及 html、sh、txt 等说明与脚本文件整体约 869KB体积轻量、结构清晰。目前已有 279 人学习下载。读者可从中获得可运行的 Modbus 主站实现参考、协议文档与示例说明便于对照理解请求响应流程并在此基础上按具体设备地址与数据结构做定制适配。1. 工业现场的数据搬运工为什么要在 Android 上跑 Modbus 主站去年帮一个做产线巡检的团队看设备他们手里是一批加固平板跑着定制的 Android 系统工人拿着它沿着机柜走一圈要把十几台温控器、电表、变频器的寄存器值读回来再同步到后台。问题在于这些设备清一色是 RS485 接口走 Modbus RTU 协议而平板本身没有串口。他们最初的方案是每台设备配一个网关成本高、故障点多现场一断网就抓瞎。后来换了个思路用 USB 转 485 的转接头插在平板上让 Android 直接当 Modbus 主站去轮询。这个项目就是干这件事的——一个跑在 Android 上的 Modbus Master 实现核心围绕串口通信和 Modbus 协议帧的收发解析展开。它适合两类人一类是做工控上位机、手持终端、巡检设备的开发者需要在 Android 侧直接对接 PLC、仪表、传感器另一类是刚接触 Modbus、想找一个能跑起来的完整例子把协议帧、CRC 校验、串口参数这些概念落到代码里的人。下面我按「这东西怎么组织、怎么跑起来、怎么改」的顺序拆一遍中间会贴关键代码也会讲几个我踩过的坑。2. 项目结构与技术底座串口、协议帧、线程模型三件套2.1 目录组织与模块职责这个项目的结构不复杂典型的 Android 工程加一个串口通信层。核心目录大致是这样分的app/ src/main/java/com/example/modbusmaster/ serial/ # 串口打开、参数配置、读写封装 SerialPort.java SerialPortFinder.java SerialInputOutputManager.java modbus/ # 协议层帧构造、CRC、功能码处理 ModbusMaster.java ModbusRequest.java ModbusResponse.java CRC16.java ui/ # 界面设备配置、轮询展示 MainActivity.java DeviceConfigActivity.java util/ # 字节工具、日志 src/main/jniLibs/ # 串口用的 .so 库按 ABI 分目录serial包负责和底层打交道打开/dev/tty*设备节点、设置波特率、数据位、停止位、校验位然后把字节流读出来。modbus包是纯协议逻辑不依赖 Android输入输出都是字节数组方便单独测试。ui包把两者串起来配置设备地址、功能码、寄存器起始地址和数量定时轮询。这种分层的好处是协议层可以脱离设备跑单元测试串口层出问题也不会污染协议逻辑。串口这块绕不开 JNI。Android 没有公开的串口 API常见做法是用一个 C 写的serial_port.c编译成.soJava 侧通过native方法调用open、close、read、write。项目里jniLibs下按armeabi-v7a、arm64-v8a放了对应的库打包时会自动带上。这里要注意不同平板芯片架构不一样库没放全就会出现UnsatisfiedLinkError后面避坑章节会细说。2.2 串口参数配置与打开流程串口能不能通八成看参数对不对。Modbus RTU 常用的组合是 9600 波特率、8 数据位、1 停止位、无校验8N1也有用 19200 或 38400 的校验位偶校验8E1也常见。项目里把参数封装成一个配置对象打开串口时一次性传进去// SerialPort.java 关键片段 public class SerialPort { private FileDescriptor mFd; private FileInputStream mFileInputStream; private FileOutputStream mFileOutputStream; public SerialPort(File device, int baudrate, int dataBits, int stopBits, int parity) throws SecurityException, IOException { // 权限检查没有读写权限直接抛异常避免后续 read 返回 -1 难排查 if (!device.canRead() || !device.canWrite()) { try { Process su Runtime.getRuntime().exec(/system/bin/su); // 实际项目里这里通常是引导用户手动授权或使用系统签名权限 } catch (IOException e) { throw new SecurityException(串口设备无读写权限: device.getPath()); } } // 调用 native 层打开设备返回文件描述符 mFd open(device.getAbsolutePath(), baudrate, dataBits, stopBits, parity); if (mFd null) { throw new IOException(串口打开失败: device.getPath()); } mFileInputStream new FileInputStream(mFd); mFileOutputStream new FileOutputStream(mFd); } private native static FileDescriptor open(String path, int baudrate, int dataBits, int stopBits, int parity); public native void close(); }这段代码的逻辑是先检查设备节点权限再交给 native 层去open拿到FileDescriptor后包成输入输出流。参数含义很直白——baudrate是波特率必须和从站设备一致差一个档位就是一堆乱码dataBits一般是 8stopBits是 1 或 2parity用 0/1/2 表示无/奇/偶校验。我一般会把这几项做成界面上可选的配置而不是写死因为现场设备五花八门写死等于给自己挖坑。打开之后读数据不能放在主线程否则界面直接卡死。项目里用了一个独立的读线程把InputStream的字节源源不断读进缓冲区再交给协议层去拼帧// SerialInputOutputManager.java 读线程核心 public class SerialInputOutputManager implements Runnable { private final InputStream mInputStream; private final ByteBuffer mBuffer ByteBuffer.allocate(4096); private volatile boolean mRunning true; Override public void run() { byte[] chunk new byte[256]; while (mRunning) { try { int len mInputStream.read(chunk); // 阻塞读直到有数据或异常 if (len 0) { synchronized (mBuffer) { mBuffer.put(chunk, 0, len); // 累积到缓冲区等待协议层取走 } } } catch (IOException e) { mRunning false; // 读异常通常意味着串口被拔掉或关闭 } } } }这里用ByteBuffer做累积缓冲是因为串口数据是流式的一次read可能只读到半个帧也可能一次读到多个帧的粘连数据。协议层需要根据功能码和字节数去判断帧边界不能假设一次读就是一个完整响应。这个细节很多新手会忽略直接拿一次read的结果去解析结果时好时坏非常玄学。2.3 Modbus 帧构造与 CRC 校验Modbus RTU 的帧格式是从站地址1 字节 功能码1 字节 数据N 字节 CRC162 字节低字节在前。读保持寄存器用功能码 0x03请求帧里数据部分是起始寄存器地址2 字节 寄存器数量2 字节都是大端序。构造请求帧的代码如下// ModbusRequest.java 构造读保持寄存器请求 public static byte[] buildReadHoldingRegisters(int slaveId, int startAddr, int quantity) { byte[] frame new byte[8]; frame[0] (byte) slaveId; // 从站地址1~247 frame[1] 0x03; // 功能码读保持寄存器 frame[2] (byte) (startAddr 8); // 起始地址高字节 frame[3] (byte) (startAddr 0xFF); // 起始地址低字节 frame[4] (byte) (quantity 8); // 寄存器数量高字节 frame[5] (byte) (quantity 0xFF); // 寄存器数量低字节 int crc CRC16.calculate(frame, 6); // 只对前 6 字节算 CRC frame[6] (byte) (crc 0xFF); // CRC 低字节在前 frame[7] (byte) (crc 8); return frame; }参数说明slaveId是从站地址总线上每个设备唯一范围 1 到 2470 是广播地址startAddr是寄存器起始地址注意有些设备手册给的是 40001 这种编号实际协议里要减 40001 得到偏移quantity是一次读多少个寄存器上限 125。CRC 只算前 6 字节结果低字节在前这个字节序搞反了从站会直接不响应而且没有任何错误提示只能靠抓包看。CRC16 的实现是标准的 Modbus 多项式 0xA001逐位异或移位// CRC16.java public static int calculate(byte[] data, int length) { int crc 0xFFFF; for (int i 0; i length; i) { crc ^ (data[i] 0xFF); for (int j 0; j 8; j) { if ((crc 0x0001) ! 0) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc 0xFFFF; }这段是纯位运算没有查表速度对轮询场景完全够用。如果设备多、轮询频率高可以换成预计算 256 项查表能快几倍但一般没必要。校验响应帧时把收到的字节去掉最后两字节算一遍 CRC再和帧尾的两字节比对不一致就丢弃重发。2.4 响应解析与超时重试响应帧的结构是从站地址 功能码 字节数 数据 CRC。功能码 0x03 的响应里字节数等于寄存器数量乘 2数据部分每个寄存器 2 字节大端。解析时要先校验地址和功能码是否匹配再校验 CRC最后按字节数取数据// ModbusResponse.java 解析读寄存器响应 public static int[] parseReadResponse(byte[] resp, int expectedSlave, int quantity) { if (resp.length 5) return null; // 最短响应地址功能码字节数CRC(2) if ((resp[0] 0xFF) ! expectedSlave) return null; // 地址不匹配可能是别的从站 if ((resp[1] 0xFF) ! 0x03) return null; // 功能码不匹配或异常响应 int byteCount resp[2] 0xFF; if (byteCount ! quantity * 2) return null; // 数据长度对不上 // 校验 CRC对除最后两字节外的所有字节计算 int crcCalc CRC16.calculate(resp, resp.length - 2); int crcRecv (resp[resp.length - 2] 0xFF) | ((resp[resp.length - 1] 0xFF) 8); if (crcCalc ! crcRecv) return null; // CRC 错误丢弃 int[] values new int[quantity]; for (int i 0; i quantity; i) { values[i] ((resp[3 i * 2] 0xFF) 8) | (resp[4 i * 2] 0xFF); } return values; }这里每个判断都对应一种现场故障地址不匹配说明总线上有多个设备同时响应或者地址配错功能码不对可能是从站不支持该功能返回了异常码功能码最高位置 1字节数不对通常是寄存器数量配错CRC 错则是线路干扰或波特率不匹配。把这些分支都返回null而不是抛异常是为了让上层能统一做重试不至于一个坏帧把整个轮询线程搞崩。超时重试是主站必须做的。Modbus RTU 没有握手发出去之后只能等等不到就重发。项目里给每次请求设一个超时窗口常见是 300 到 1000 毫秒超时后重试 2 到 3 次还不行就标记该设备离线跳过继续轮询下一台。这个逻辑放在ModbusMaster里用一个状态机管理「发送—等待—解析—重试」的循环避免阻塞 UI。3. 跑起来从零到读回第一个寄存器值3.1 环境准备与工程导入先把工程拉下来用 Android Studio 打开。build.gradle里minSdkVersion一般设到 21 以上targetSdkVersion跟着当前版本走。需要确认两件事一是jniLibs目录下有没有对应你平板架构的.so文件二是AndroidManifest.xml里有没有 USB 相关的权限声明。如果用的是 USB 转 485 转接头还需要声明android.hardware.usb.host特性uses-feature android:nameandroid.hardware.usb.host android:requiredtrue / uses-permission android:nameandroid.permission.USB_PERMISSION /编译前检查local.properties里的 SDK 路径对不对然后直接Build Make Project。如果报UnsatisfiedLinkError先别急着改代码去看jniLibs下是不是只有armeabi-v7a而你的设备是arm64-v8a补上对应目录的库再试。3.2 硬件连接与串口设备识别硬件这边USB 转 485 模块插到平板上A 接 A、B 接 B接到从站设备的 485 口。有些模块 A/B 标反了接上不通就调换一下这是最常见的接线问题。接好后Android 系统会枚举出一个/dev/ttyUSB0或者/dev/ttyACM0之类的设备节点具体名字取决于转接芯片。项目里的SerialPortFinder会去/dev下扫描tty开头的节点列出来给用户选// SerialPortFinder.java 扫描可用串口 public VectorString getAllDevices() { VectorString devices new Vector(); File dev new File(/dev); File[] files dev.listFiles(); if (files null) return devices; for (File f : files) { String name f.getName(); // 过滤出 ttyUSB / ttyACM / ttyS 这类串口节点 if (name.startsWith(ttyUSB) || name.startsWith(ttyACM) || name.startsWith(ttyS)) { devices.add(f.getAbsolutePath()); } } return devices; }扫描出来之后界面上让用户选一个配上波特率等参数点连接。连上之后先别急着轮询手动发一帧读请求看有没有响应。这一步能快速区分是串口没通还是协议配错。3.3 一次完整轮询的调用链从界面点「开始轮询」到数据回显调用链是这样的MainActivity启动一个定时任务每隔固定间隔调用ModbusMaster.readHoldingRegisters(slaveId, addr, qty)ModbusMaster构造请求帧通过SerialPort的OutputStream写出去读线程把响应字节收进缓冲区ModbusMaster在超时窗口内从缓冲区取数据拼成完整帧后交给ModbusResponse.parseReadResponse解析解析成功就把int[]回调给界面刷新失败就重试或标记离线。// ModbusMaster.java 单次读请求的简化流程 public int[] readHoldingRegisters(int slaveId, int startAddr, int quantity, int timeoutMs) { byte[] request ModbusRequest.buildReadHoldingRegisters(slaveId, startAddr, quantity); for (int retry 0; retry MAX_RETRY; retry) { serialPort.write(request); // 发送请求帧 byte[] resp readFrame(timeoutMs); // 在超时窗口内收一帧 if (resp ! null) { int[] values ModbusResponse.parseReadResponse(resp, slaveId, quantity); if (values ! null) return values; // 解析成功直接返回 } // 超时或解析失败短暂延时后重试避免总线冲突 sleepQuietly(50); } return null; // 多次重试仍失败上层标记设备离线 }timeoutMs建议设成「帧传输时间 从站处理时间」的余量9600 波特率下一个 8 字节请求大约 8 毫秒加上从站响应300 毫秒足够。MAX_RETRY设 3 次比较稳妥再多会拖慢整个轮询周期。readFrame内部要从缓冲区里按帧边界取数据不能简单读一次就返回否则遇到粘包就解析失败。3.4 用日志和抓包验证通信第一次跑通之前建议在发送和接收处都打日志把字节数组按十六进制打出来// 十六进制打印排查帧内容最直接 public static String toHex(byte[] data) { StringBuilder sb new StringBuilder(); for (byte b : data) { sb.append(String.format(%02X , b)); } return sb.toString().trim(); }发出去的请求帧和收到的响应帧都打出来对照设备手册看地址、功能码、寄存器地址对不对CRC 对不对。如果请求帧正确但从站不响应重点查接线、波特率、从站地址如果响应帧有但 CRC 错查校验位配置和线路干扰如果响应帧功能码是0x830x03 最高位置 1说明从站返回了异常去看异常码含义通常是寄存器地址越界或不支持该功能。有条件的用 USB 转 485 带指示灯的那种模块发送时灯闪、接收时灯闪能直观判断数据有没有出去、有没有回来。4. 避坑与排查那些让轮询时好时坏的细节4.1 串口权限与设备节点找不到现象SerialPortFinder扫不到任何设备或者打开时抛SecurityException。原因有两个一是 USB 转 485 模块没被系统识别/dev下根本没有对应节点二是节点有但当前应用没有读写权限普通应用访问/dev/ttyUSB0会被拒。解决先确认模块插好、系统设置里能看到 USB 设备权限问题在定制系统上可以让系统签名或预置权限通用做法是引导用户用 root 或使用系统级应用普通应用想直接开串口确实受限这一点要提前和硬件方案确认。4.2 波特率或校验位不匹配导致全是乱码现象能收到数据但字节内容完全对不上CRC 永远校验失败。原因波特率、数据位、停止位、校验位四项里有一项和从站不一致。9600 配成 19200或者从站是偶校验而主站配了无校验都会导致采样错位。解决逐项对照设备手册核对尤其是校验位很多仪表默认 8E1 而不是 8N1。改完参数重新打开串口不要指望热切换生效。4.3 粘包与半包导致解析随机失败现象轮询跑一会儿就失败几次重试又能成功日志里响应帧长度忽长忽短。原因串口是字节流一次read可能读到半个帧也可能读到上一帧的尾巴加下一帧的头。解决不要用单次read的结果直接解析要用累积缓冲区按「地址功能码字节数」推算帧长凑够一帧再解析多余的字节留给下一次。项目里SerialInputOutputManager的ByteBuffer就是干这个的解析时从缓冲区取够长度再消费。4.4 主线程做串口读写导致 ANR现象点开始轮询后界面卡死几秒后系统弹「应用无响应」。原因串口read是阻塞的放在主线程会一直占着 UI 线程。解决读线程独立轮询逻辑也放到工作线程或定时任务里解析完通过Handler或LiveData回主线程刷新界面。所有write和read都不碰主线程这是硬规矩。4.5 多设备轮询时的总线冲突现象总线上挂多台设备时偶尔某台读不到单独读又正常。原因485 是半双工总线同一时刻只能有一个设备发送。如果主站发完请求还没等从站响应完就发下一帧或者两台从站地址配重了就会冲突。解决严格串行轮询一请求一响应完成后再发下一台帧间留至少 3.5 个字符时间的静默间隔检查从站地址是否唯一。轮询周期别设太短给每台设备留够响应时间。5. 二次开发把轮询逻辑改成可配置的任务引擎原项目的轮询逻辑比较直接设备参数基本写死在代码或简单配置里。真要上产线设备数量、寄存器映射、数据类型16 位整数、32 位浮点、高低字节序都不一样得把它改成一个可配置的任务引擎。我的做法是定义一个设备描述结构把「读什么、怎么解析、多久读一次」都数据化// DeviceTask.java 可配置的轮询任务 public class DeviceTask { public int slaveId; // 从站地址 public int functionCode; // 功能码0x03 或 0x04 public int startAddr; // 起始寄存器 public int quantity; // 寄存器数量 public int intervalMs; // 轮询间隔 public String dataType; // int16 / uint16 / float32 / int32 public boolean wordSwap; // 32 位数据是否高低字交换 public String tag; // 业务标签如 温度1 }解析层根据dataType把int[]转成实际值。32 位浮点要两个寄存器拼成 4 字节再按 IEEE 754 转float这里字节序是最容易翻车的地方——有的设备高字在前有的低字在前wordSwap就是给这个用的// 32 位浮点解析处理字交换 public static float toFloat32(int high, int low, boolean wordSwap) { int h wordSwap ? low : high; int l wordSwap ? high : low; int bits ((h 0xFFFF) 16) | (l 0xFFFF); return Float.intBitsToFloat(bits); }验证方法很实在拿设备手册给的一个已知值比如 25.5 度看解析出来对不对不对就切wordSwap再试两种都不对就检查是不是用了 32 位整数而不是浮点。改完之后把任务列表做成 JSON 配置从文件或后台下发现场调参不用重新打包这一点在产线部署时省了大量来回。还有一个技巧是把轮询调度和协议解析彻底解耦调度器只管按intervalMs触发任务、收集结果解析器只管把字节转成业务值中间用队列传递。这样加新设备类型只是加一个解析器不动调度逻辑排查问题时也能单独把某一帧喂给解析器复现不用连硬件。从那以后我每次接新的 Modbus 设备都强制先用十六进制日志把请求帧和响应帧对一遍手册确认地址、功能码、字节序全对再写解析代码省掉了很多「数据看着像但又不对」的来回折腾。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Docker容器从概念到配置落地:镜像分层、资源限制与故障排查实战

Docker容器从概念到配置落地:镜像分层、资源限制与故障排查实战

刚接手一台别人搭好的机器,docker ps一屏幕容器名全是随机生成的形容词加科学家名字,端口映射靠翻聊天记录,日志文件把磁盘吃满,某个容器隔三差五被系统杀掉还查不出原因——这种场面我见过太多次。Docker 这玩意儿入门门槛低得离…

📅 2026/10/26 4:34:19
微服务架构中的业务完整性验证设计:分层校验与对账兜底

微服务架构中的业务完整性验证设计:分层校验与对账兜底

单库单表的时候,一个BEGIN...COMMIT就能兜住的事,拆成六个服务之后,你得写两套校验、一张对账表、一个补偿任务,半夜还可能被叫起来查为什么订单是"已支付"但库存没扣。我见过太多团队在微服务拆分的第一版设计里&#…

📅 2026/10/26 4:34:19
A2UI:基于大语言模型的智能UI生成技术解析

A2UI:基于大语言模型的智能UI生成技术解析

1. 项目概述:A2UI与传统UI生成模式的本质差异第一次接触A2UI这个概念是在去年的一次开发者峰会上,当时看到演示中一个完整的后台管理系统界面在30秒内从自然语言描述生成出来,着实让我这个做了十年前端的老兵震惊。A2UI(AI-Agent-…

📅 2026/10/26 4:34:19
MORE NEWS

更多资讯

📰

AI音乐创作:多模态生成式AI技术实践与优化

1. 项目概述:当AI遇上音乐创作去年夏天,我第一次用AI生成了一段30秒的电子舞曲,当时就被这种创作方式震撼了。如今看到小影科技与Google Cloud的合作案例,不禁感叹AI音乐创作已经进化到如此成熟的地步。这个项目本质上是通过多模态…

📰

微信排队小程序技术架构与高并发优化实践

1. 排队小程序的核心价值与市场需求在当今快节奏的商业环境中,排队管理已成为服务行业的核心痛点之一。从餐饮门店到政务大厅,从医院挂号到景区售票,排队现象无处不在却又饱受诟病。传统排队方式存在三大致命缺陷:顾客需长时间滞留…

📰

云端AI音乐生成技术解析与优化实践

1. 项目概述:当AI音乐创作遇上云端算力去年夏天第一次体验小影科技的AI音乐生成工具时,我被其创作效率震惊了——输入几句简单的歌词描述,30秒内就能生成带有完整编曲的demo。但真正让我好奇的是,这种需要处理多模态数据&#xff…

📰

基于Hadoop+Spark的地铁客流预测系统实战解析

1. 项目背景与核心价值地铁客流预测系统是智慧城市建设的核心组成部分。作为一名长期从事交通大数据分析的工程师,我见证了传统预测方法(如时间序列分析)在应对突发客流时的乏力。2021年某城市早高峰突发暴雨,传统模型预测误差高达…

📰

PyTracking训练HOT任务:数据管线、损失设计与显存优化实战

1. 从一次“跑不动”的深夜说起:HOT 训练到底卡在哪做 PyTracking 这套框架的人,大概率都经历过同一个夜晚:数据集下载完了,配置文件改好了,python train.py一敲,终端开始刷日志,loss 从 20 多慢…

📰

港口智能调度系统:三角测量与边缘计算实践

1. 港口码头智能调度系统的核心挑战港口码头作为全球物流体系的关键节点,每天面临着数以千计的集装箱卡车、吊装设备、船舶和作业人员的复杂交互。传统调度方式主要依赖人工经验和二维平面监控,存在三个致命缺陷:空间感知局限:二维…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬