前言
在工业控制领域,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(主设备)轮流查询从设备,从设备仅在被询问时响应,禁止主动发送数据。
降低总线负载:控制总线上的设备数量,或延长通讯间隔,确保数据帧发送时间不重叠。