【Android】在平板上实现Rs485的数据通讯
前言
在工业控制领域,Android 设备通过 RS485 接口与 PLC(可编程逻辑控制器)通信是一种常见的技术方案。最近在实现一个项目需要和plc使用485进行通讯,记录下实现的方式。
我这边使用的从平的Android平板,从平里面已经对这个串口进行了优化和提供了开发工具包,这样哦我们就不需要自己实现这方面的东西了,【Uart 】就是提供的工具类。
正文
先贴代码
public class Rs485Util {private static final String TAG = "Rs485Util";private static final String UART_PATH = "/dev/ttyWK1";private static final int BAUD_RATE = 19200;private static final String QUERY_COMMAND = "01030000001AC401";private static final int QUERY_INTERVAL = 199; // 定时查询间隔(毫秒)private static final int SEND_RETRY_COUNT = 3; // 命令重试次数private static final int SEND_DELAY = 51; // 重试间隔(毫秒)private static final int COMMAND_INTERVAL = 101; // 不同命令间间隔(毫秒)private static volatile Rs485Util instance;private Uart uart485;private final ScheduledExecutorService queryExecutor; // 定时查询线程池private final ScheduledExecutorService commandExecutor; // 命令处理线程池private final BlockingQueue<String> commandQueue;private final AtomicBoolean isRunning = new AtomicBoolean(false);private final AtomicBoolean isQuerying = new AtomicBoolean(false);private final AtomicBoolean isQueryPaused = new AtomicBoolean(false); // 查询暂停标记private String lastCommand = null; // 上一条命令记录// 重要指令private static final String START_COMMAND_1 = "010600060001A80B";private static final String START_COMMAND_0 = "01060006000069CB";private static final String STOP_COMMAND_1 = "010600070001F9CB";private static final String STOP_COMMAND_0 = "010600070000380B";private Rs485Util() {// 初始化命令线程池(单线程,确保命令顺序执行)commandExecutor = Executors.newSingleThreadScheduledExecutor(r -> {Thread thread = new Thread(r, "RS485-Command-Thread");thread.setDaemon(true);return thread;});// 初始化查询线程池(单线程,定时发送查询指令)queryExecutor = Executors.newSingleThreadScheduledExecutor(r -> {Thread thread = new Thread(r, "RS485-Query-Thread");thread.setDaemon(true);return thread;});// 命令队列(存储待发送的命令指令)commandQueue = new LinkedBlockingQueue<>();}// 单例模式获取实例public static Rs485Util getInstance() {if (instance == null) {synchronized (Rs485Util.class) {if (instance == null) {instance = new Rs485Util();}}}return instance;}/*** 打开485串口并初始化通信*/public synchronized void open485Uart() {if (isRunning.get()) {Log.e(TAG, "串口已处于打开状态");return;}commandExecutor.execute(() -> {try {// 初始化串口uart485 = new Uart(UART_PATH, BAUD_RATE, true);uart485.setReceiveListener(bytes -> {String receiveData = DigitalTransUtil.byte2hex(bytes);PLCUtil.analyzePLCData(receiveData);});uart485.start();Log.e(TAG, "串口启动成功");isRunning.set(true);// 启动定时查询任务startQueryTask();// 启动命令队列处理processCommandQueue();} catch (Exception e) {e.printStackTrace();Log.e(TAG, "启动485串口失败:" + e.getMessage());closeResources(); // 异常时释放资源}});}/*** 启动定时查询任务(带暂停判断)*/private void startQueryTask() {if (isQuerying.get()) return;// 定时发送查询指令,每次发送前检查是否被暂停queryExecutor.scheduleAtFixedRate(() -> {// 仅在运行中且未被暂停时发送查询指令if (uart485 != null && isRunning.get() && !isQueryPaused.get()) {try {byte[] sendByte = DigitalTransUtil.hex2byte(QUERY_COMMAND);uart485.send(sendByte);Log.d(TAG, "发送查询指令: " + QUERY_COMMAND);} catch (Exception e) {Log.e(TAG, "查询命令发送失败: " + e.getMessage());}}}, 0, QUERY_INTERVAL, TimeUnit.MILLISECONDS);isQuerying.set(true);}/*** 处理命令队列(发送命令时暂停查询)*/private void processCommandQueue() {commandExecutor.execute(() -> {while (isRunning.get()) {try {// 从队列获取命令(阻塞等待新命令)String command = commandQueue.take();// 1. 暂停定时查询(确保命令发送时无查询干扰)
// pauseQuery();// 2. 不同命令间等待间隔if (lastCommand != null && !lastCommand.equals(command)) {Thread.sleep(COMMAND_INTERVAL);}// 3. 发送命令(带重试)sendCommandInternal(command);// 4. 更新最后一条命令记录lastCommand = command;// 5. 恢复定时查询(命令发送完成)
// resumeQuery();} catch (InterruptedException e) {Log.e(TAG, "命令处理线程被中断", e);Thread.currentThread().interrupt();break;} catch (Exception e) {Log.e(TAG, "命令处理异常: " + e.getMessage());// 异常时也需恢复查询resumeQuery();}}});}/*** 内部发送命令(带重试机制)*/private void sendCommandInternal(String strCommand) throws InterruptedException {if (uart485 == null || !isRunning.get()) {Log.e(TAG, "串口未初始化或已关闭,无法发送命令");return;}for (int i = 0; i < SEND_RETRY_COUNT; i++) {try {byte[] sendByte = DigitalTransUtil.hex2byte(strCommand);uart485.send(sendByte);Log.e(TAG, "命令发送成功(第" + (i + 1) + "次): " + strCommand);// 非最后一次重试时等待间隔if (i < SEND_RETRY_COUNT - 1) {Thread.sleep(SEND_DELAY);}} catch (Exception e) {Log.e(TAG, "命令发送失败(第" + (i + 1) + "次): " + e.getMessage());// 最后一次重试失败时,仍继续后续流程(避免阻塞)if (i == SEND_RETRY_COUNT - 1) {Log.e(TAG, "命令达到最大重试次数: " + strCommand);}}}}/*** 暂停定时查询*/private synchronized void pauseQuery() {if (!isQueryPaused.get()) {isQueryPaused.set(true);Log.e(TAG, "暂停定时查询");}}/*** 恢复定时查询*/private synchronized void resumeQuery() {if (isQueryPaused.get()) {isQueryPaused.set(false);Log.e(TAG, "恢复定时查询");}}/*** 发送命令接口(线程安全)*/public void sendString(String strCommand) {if (!isRunning.get()) {Log.e(TAG, "串口未打开,无法发送命令");return;}try {boolean isSpecial = START_COMMAND_1.equals(strCommand) || STOP_COMMAND_1.equals(strCommand);if (isSpecial) {commandQueue.clear();}commandQueue.put(strCommand);Log.e(TAG, "命令已加入队列: " + strCommand);} catch (InterruptedException e) {Log.e(TAG, "添加命令到队列被中断", e);Thread.currentThread().interrupt();}}/*** 停止485串口通信*/public synchronized void stop485Uart() {if (!isRunning.get()) {Log.e(TAG, "串口已处于关闭状态");return;}Log.e(TAG, "正在停止485串口通信...");isRunning.set(false);isQuerying.set(false);isQueryPaused.set(false); // 重置暂停状态closeResources();}/*** 关闭所有资源*/private void closeResources() {// 关闭查询线程池if (queryExecutor != null) {queryExecutor.shutdownNow();try {if (!queryExecutor.awaitTermination(500, TimeUnit.MILLISECONDS)) {Log.e(TAG, "查询任务未能及时关闭");}} catch (InterruptedException e) {queryExecutor.shutdownNow();Thread.currentThread().interrupt();}}// 关闭命令线程池if (commandExecutor != null) {commandExecutor.shutdownNow();try {if (!commandExecutor.awaitTermination(500, TimeUnit.MILLISECONDS)) {Log.e(TAG, "命令任务未能及时关闭");}} catch (InterruptedException e) {commandExecutor.shutdownNow();Thread.currentThread().interrupt();}}// 清空命令队列commandQueue.clear();// 关闭串口if (uart485 != null) {try {uart485.stop();} catch (Exception e) {Log.e(TAG, "关闭串口异常: " + e.getMessage());}uart485 = null;}Log.e(TAG, "485串口资源已完全释放");}/*** 检查串口是否已打开*/public boolean isUartOpen() {return isRunning.get();}
}
代码解析
使用两个单线程调度线程池实现任务分离:
queryExecutor
:负责定时发送查询指令,采用scheduleAtFixedRate实现固定间隔执行
commandExecutor
:处理命令队列,确保命令按顺序执行
线程池配置为守护线程(thread.setDaemon(true)),避免应用退出时线程残留。这种分离设计保证了查询任务和命令任务的独立性,防止相互干扰。
通过BlockingQueue
实现命令的缓冲与有序处理:
所有命令先进入队列等待,由专门的线程按顺序取出并发送
特殊命令(启动 / 停止)具有清空队列的优先权:
boolean isSpecial = START_COMMAND_1.equals(strCommand) || STOP_COMMAND_1.equals(strCommand);
if (isSpecial) { commandQueue.clear(); }
这种设计解决了多线程发送命令的冲突问题,保证了命令执行的顺序性,同时确保关键操作(如启动 / 停止)能够立即执行。
关键方法解析
1. 串口初始化:open485Uart()
该方法是启动通信的入口,主要完成:
检查当前状态,避免重复打开
初始化 UART 设备,配置端口路径(/dev/ttyWK1)和波特率(19200)
设置接收数据的监听器,实现数据的异步处理:
uart485.setReceiveListener(bytes -> {//数据的解析处理String receiveData = DigitalTransUtil.byte2hex(bytes);PLCUtil.analyzePLCData(receiveData);
});
2. 定时查询:startQueryTask()
实现对 PLC 的定时查询功能:
采用固定间隔(199ms)发送查询指令QUERY_COMMAND
发送前检查运行状态和暂停标记,确保仅在合适状态下发送
通过scheduleAtFixedRate实现周期性执行
3. 命令处理:processCommandQueue()
命令处理的核心流程:
从队列阻塞获取命令(commandQueue.take())
不同命令间保持固定间隔(101ms),避免命令发送过于密集
调用sendCommandInternal()实际发送命令(包含重试逻辑)
更新最后一条命令记录,用于间隔判断
4. 资源释放:closeResources()
该方法负责在通信结束或异常时释放所有资源:
关闭线程池(shutdownNow() + awaitTermination)
清空命令队列,避免残留命令干扰
关闭串口设备,释放硬件资源
重置所有状态标记,确保下次启动正常
在和PLC对接的时候,哥们建议我在进行定时循环或者类似的操作的时候,最好不要把时间卡在5、10 等 5 的倍数,核心逻辑还是与 PLC 扫描周期的 “同步冲突” 有关
避免同步重叠
PLC 的扫描周期通常是动态变化的(如因程序复杂度波动在 8~12ms)。若通讯间隔固定为 10ms(5 的倍数),可能与 PLC 的扫描周期 “同步”—— 例如 PLC 在第 10ms、20ms 时正处于数据刷新阶段,此时外部设备(如 SCADA、HMI)发送通讯请求,可能导致:
数据读取不完整:PLC 尚未完成输出刷新,读取到的是 “旧数据”。
通讯响应延迟:PLC 优先处理内部程序,暂时搁置通讯请求,导致外部设备超时。
实际建议
通讯间隔应避开 PLC 的典型扫描周期范围,或采用非固定间隔(如随机增加 1~2ms 偏移量)。例如:
若 PLC 扫描周期约为 10ms,通讯间隔可设为 12ms 或 8ms,减少同步概率。
对于需要高频通讯的场景(如毫秒级控制),建议采用 PLC 支持的高速通讯协议(如 Profinet IO、EtherCAT),而非依赖定时轮询。
还有PLC的撞包概念
“撞包” 是工业通讯中的通俗说法,指数据帧冲突
,即多个设备在同一时间向 PLC 的通讯总线发送数据,导致信号干扰、数据丢失。
常见场景
采用半双工通讯协议(如 RS485 总线的 Modbus RTU)时,总线上的多个从设备(如传感器、变频器)若同时向 PLC(主设备)发送响应,会导致数据帧重叠。
总线负载过高:当多个设备的通讯频率过高(如间隔过短),总线上数据帧密集,容易发生碰撞。
解决方式
采用全双工协议(如 Profinet、EtherNet/IP):通过交换机实现点对点通讯,避免总线冲突。
严格主从机制:如 Modbus RTU 中,由 PLC(主设备)轮流查询从设备,从设备仅在被询问时响应,禁止主动发送数据。
降低总线负载:控制总线上的设备数量,或延长通讯间隔,确保数据帧发送时间不重叠。