前言

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914375.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914375.shtml
英文地址,请注明出处:http://en.pswp.cn/news/914375.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL技术笔记-备份与恢复完全指南

目录 前言 一、备份概述 &#xff08;一&#xff09;备份方式 &#xff08;二&#xff09;备份策略 二、物理备份及恢复 &#xff08;一&#xff09;备份操作 &#xff08;二&#xff09;恢复操作 三、逻辑备份及恢复 &#xff08;一&#xff09;逻辑备份 &#xff0…

SpringBoot或OpenFeign中 Jackson 配置参数名蛇形、小驼峰、大驼峰、自定义命名

SpringBoot或OpenFeign中 Jackson 配置参数名蛇形、小驼峰、大驼峰、自定义命名 前言 在调用外部接口时&#xff0c;对方给出的接口文档中&#xff0c;入参参数名一会大写加下划线&#xff0c;一会又是驼峰命名。 示例如下&#xff1a; {"MOF_DIV_CODE": "xx…

uni-app 途径站点组件开发与实现分享

在移动应用开发中&#xff0c;涉及到出行、物流等场景时&#xff0c;途径站点的展示是一个常见的需求。本文将为大家分享一个基于 uni-app 开发的途径站点组件&#xff0c;该组件能够清晰展示路线中的各个站点信息&#xff0c;包括站点名称、到达时间、是否已到达等状态&#x…

kotlin中集合的用法

从一个实际应用看起以下kotlin中代码语法正确吗 var testBeanAIP0200()var testList:List<AIP0200> ArrayList()testList.add(testBean)这段Kotlin代码存在语法错误&#xff0c;主要问题在于&#xff1a;List<AIP0200> 是Kotlin中的不可变集合接口&#xff0c;不能…

深入理解 Java Map 与 Set

文章目录前言1. 搜索树1.1 什么是搜索树1.2 查找1.3 插入1.4 删除情况一&#xff1a;cur 没有子节点&#xff08;即为叶子节点&#xff09;情况二&#xff1a;cur 只有一个子节点&#xff08;只有左子树或右子树&#xff09;情况三&#xff1a;cur 有两个子节点&#xff08;左右…

excel如何只保留前几行

方法一&#xff1a;手动删除多余行 选中你想保留的最后一行的下一行&#xff08;比如你只保留前10行&#xff0c;那选第11行&#xff09;。按住 Shift Ctrl ↓&#xff08;Windows&#xff09;或 Shift Command ↓&#xff08;Mac&#xff09;&#xff0c;选中从第11行到最…

实时连接,精准监控:风丘科技数据远程显示方案提升试验车队管理效率

风丘科技推出的数据远程实时显示方案更好地满足了客户对于试验车队远程实时监控的需求&#xff0c;并真正实现了试验车队的远程管理。随着新的数据记录仪软件IPEmotion RT和相应的跨平台显示解决方案的引入&#xff0c;让我们的客户端不仅可在线访问记录器系统状态&#xff0c;…

灰盒级SOA测试工具Parasoft SOAtest重新定义端到端测试

还在为脆弱的测试环境、强外部依赖和低效的测试复用拖慢交付而头疼&#xff1f;尤其在银行、医疗、制造等关键领域&#xff0c;传统的端到端测试常因环境不稳、接口难模拟、用例难共享而举步维艰。 灰盒级SOA测试工具Parasoft SOAtest以可视化编排简化复杂测试流程&#xff0c…

OKHttp 核心知识点详解

OKHttp 核心知识点详解 一、基本概念与架构 1. OKHttp 简介 类型&#xff1a;高效的HTTP客户端特点&#xff1a; 支持HTTP/2和SPDY&#xff08;多路复用&#xff09;连接池减少请求延迟透明的GZIP压缩响应缓存自动恢复网络故障2. 核心组件组件功能OkHttpClient客户端入口&#…

从“被动巡检”到“主动预警”:塔能物联运维平台重构路灯管理模式

从以往的‘被动巡检’转变至如今的‘主动预警’&#xff0c;塔能物联运维平台对路灯管理模式展开了重新构建。城市路灯属于极为重要的市政基础设施范畴&#xff0c;它的实际运行状态和市民出行安全以及城市形象有着直接且紧密的关联。不过呢&#xff0c;传统的路灯管理模式当下…

10. 常见的 http 状态码有哪些

总结 1xx: 正在处理2xx: 成功3xx: 重定向&#xff0c;302 重定向&#xff0c;304 协商缓存4xx: 客户端错误&#xff0c;401 未登录&#xff0c;403 没权限&#xff0c;404 资源不存在5xx: 服务器错误常见的 HTTP 状态码详解 HTTP 状态码&#xff08;HTTP Status Code&#xff0…

springBoot对接第三方系统

yml文件 yun:ip: port: username: password: controller package com.ruoyi.web.controller.materials;import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.materials.service.IYunService; import o…

【PTA数据结构 | C语言版】车厢重排

本专栏持续输出数据结构题目集&#xff0c;欢迎订阅。 文章目录题目代码题目 一列挂有 n 节车厢&#xff08;编号从 1 到 n&#xff09;的货运列车途径 n 个车站&#xff0c;计划在行车途中将各节车厢停放在不同的车站。假设 n 个车站的编号从 1 到 n&#xff0c;货运列车按照…

量子计算能为我们做什么?

科技公司正斥资数十亿美元投入量子计算领域&#xff0c;尽管这项技术距离实际应用还有数年时间。那么&#xff0c;未来的量子计算机将用于哪些方面&#xff1f;为何众多专家坚信它们会带来颠覆性变革&#xff1f; 自 20 世纪 80 年代起&#xff0c;打造一台利用量子力学独特性质…

BKD 树(Block KD-Tree)Lucene

BKD 树&#xff08;Block KD-Tree&#xff09;是 Lucene 用来存储和快速查询 **多维数值型数据** 的一种磁盘友好型数据结构&#xff0c;可以把它想成&#xff1a;> **“把 KD-Tree 分块压缩后落到磁盘上&#xff0c;既能做磁盘顺序读&#xff0c;又能像内存 KD-Tree 一样做…

【Mysql作业】

第一次作业要求1.首先打开Windows PowerShell2.连接到MYSQL服务器3.执行以下SQL语句&#xff1a;-- 创建数据库 CREATE DATABASE mydb6_product;-- 使用数据库 USE mydb6_product;-- 创建employees表 CREATE TABLE employees (id INT PRIMARY KEY,name VARCHAR(50) NOT NULL,ag…

(C++)STL:list认识与使用全解析

本篇基于https://cplusplus.com/reference/list/list/讲解 认识 list是一个带头结点的双向循环链表翻译总结&#xff1a; 序列容器&#xff1a;list是一种序列容器&#xff0c;允许在序列的任何位置进行常数时间的插入和删除操作。双向迭代&#xff1a;list支持双向迭代&#x…

Bash函数详解

目录**1. 基础函数****2. 参数处理函数****3. 文件操作函数****4. 日志与错误处理****5. 实用工具函数****6. 高级函数技巧****7. 常用函数库示例****总结&#xff1a;Bash 函数核心要点**1. 基础函数 1.1 定义与调用 可以自定义函数名称&#xff0c;例如将greet改为yana。❌…

Python爬虫实战:研究rows库相关技术

1. 引言 在当今数字化时代,互联网上存在着大量有价值的表格数据,这些数据以 HTML 表格、CSV、Excel 等多种格式存在。然而,由于数据源的多样性和不规范性,表格结构往往存在复杂表头、合并单元格、不规则数据行等问题,给数据的自动化处理带来了巨大挑战。 传统的数据处理工…

通过同态加密实现可编程隐私和链上合规

1. 引言 2023年9月28日&#xff0c;a16z 的加密团队发布了 Nakamoto Challenge&#xff0c;列出了区块链中需要解决的最重要问题。尤其是其中的第四个问题格外引人注意&#xff1a;“合规的可编程隐私”&#xff0c;因为Zama团队已经在这方面积极思考了一段时间。本文提出了使…