Java实现Modbus读写数据

news2024/10/11 14:17:14

背景

由于当时项目周期赶,引入了一个PLC4X组件,上手快。接下来就是使用这个组件遇到的一些问题:

  • 关闭连接NioEventLoop没有释放导致oom
  • 设计思想是一个设备一个连接,而不是一个网关一个连接
  • 连接断开后客户端无从感知
    前两个问题解决方案参考上篇文章,最后一个问题虽然可以通过isConnect()方法获取到状态,但是连接断开后这个状态并没有更新,只能代码实现失败重连。
    所以为了解决以上问题,我打算重新封装一个Modbus组件。

步骤

代码如下所示,目前只分享modbus-core相关的代码。

  • modbus-core:实现设备读写指令的下发以及应答。
  • modbus-app:实现通用的可灵活配置的modbus设备接入层,通过更新配置信息即可快速引入新设备,无需手写代码重启应用。

在这里插入图片描述
为了快速实现modbus组件封装,这里引入了Vertx框架(基于事件+异步)官网链接,而不是原生的Netty框架。

引入架包

<!-- 目前我这里引入最新的版本(4.4.4) -->
<dependency>
     <groupId>io.vertx</groupId>
     <artifactId>vertx-core</artifactId>
     <version>${vertx.version}</version>
 </dependency>

工具类

ByteUtil

package com.bho.modbus.utils;

import java.nio.ByteBuffer;

public class ByteUtil {

    /**
     * 字节数组转字符串
     * @param bytes
     * @return
     */
    public static  String bytesToHexString(byte[] bytes) {
        StringBuffer sb = new StringBuffer(bytes.length);
        String sTemp;
        for (int i = 0; i < bytes.length; i++) {
            sTemp = Integer.toHexString(0xFF & bytes[i]);
            if (sTemp.length() < 2) {
                sb.append(0);
            }
            sb.append(sTemp.toUpperCase());
        }
        return sb.toString();
    }

    /**
     * int整型转字节数组
     * @param data
     * @param offset
     * @param len
     * @return
     */
    public static byte[] intToBytes(int data, int offset, int len) {
        ByteBuffer buffer = ByteBuffer.allocate(4);
        buffer.putInt(data);
        byte[] bytes = buffer.array();
        if (len - offset == 4) {
            return bytes;
        }
        byte[] dest = new byte[len];
        System.arraycopy(bytes, offset, dest, 0, len);
        return dest;
    }

    /**
     * 字节数组转int整型
     * @param bytes
     * @param offset
     * @param len
     * @return
     */
    public static int bytesToInt(byte[] bytes, int offset, int len) {
        ByteBuffer buffer = ByteBuffer.allocate(4);
        for (int i = len; i < 4; i ++) {
            buffer.put((byte) 0x00);
        }
        for (int i = offset; i < offset + len; i++) {
            buffer.put(bytes[i]);
        }
        buffer.flip();
        return buffer.getInt();
    }



}

Crc16

package com.bho.modbus.utils;


public class Crc16 {

    /**
     * 获取CRC16校验码
     * @param arr_buff
     * @return
     */
    public static byte[] getCrc16(byte[] arr_buff) {
        int len = arr_buff.length;

        // 预置 1 个 16 位的寄存器为十六进制FFFF, 称此寄存器为 CRC寄存器。
        int crc = 0xFFFF;
        int i, j;
        for (i = 0; i < len; i++) {
            // 把第一个 8 位二进制数据 与 16 位的 CRC寄存器的低 8 位相异或, 把结果放于 CRC寄存器
            crc = ((crc & 0xFF00) | (crc & 0x00FF) ^ (arr_buff[i] & 0xFF));
            for (j = 0; j < 8; j++) {
                // 把 CRC 寄存器的内容右移一位( 朝低位)用 0 填补最高位, 并检查右移后的移出位
                if ((crc & 0x0001) > 0) {
                    // 如果移出位为 1, CRC寄存器与多项式A001进行异或
                    crc = crc >> 1;
                    crc = crc ^ 0xA001;
                } else
                    // 如果移出位为 0,再次右移一位
                    crc = crc >> 1;
            }
        }
        return intToBytes(crc);
    }


    private static byte[] intToBytes(int value) {
        byte[] src = new byte[2];
        src[1] = (byte) ((value >> 8) & 0xFF);
        src[0] = (byte) (value & 0xFF);
        return src;
    }



}

实体类

ModbusMode

目前只实现了以下两种通信方式,可根据自己需求加入其它通信方式。

package com.bho.modbus.model;

import com.bho.modbus.utils.ByteUtil;
import com.bho.modbus.utils.Crc16;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import lombok.extern.log4j.Log4j2;

import java.nio.ByteOrder;

@Log4j2
public enum ModbusMode {


    /**
     * 【事务ID(2) + 协议标识(2) + 数据长度(2)】 + 从机地址(1) + 功能码(1) + 数据区(N)
     */
    TCP,
    /**
     * 从机地址(1) + 功能码(1) + 数据区(N) + 【校验码(2)】
     *
     */
    RTU,
    ;
    public ByteToMessageDecoder getDecoder() {
        if (this == ModbusMode.TCP) {
            return new LengthFieldBasedFrameDecoder(ByteOrder.BIG_ENDIAN, 65536, 4,
                    2, 0, 6, true);
        }
        if (this == ModbusMode.RTU){
            return new LengthFieldBasedFrameDecoder(ByteOrder.BIG_ENDIAN, 65536, 2,
                    1, 2, 0, true);
        }
        return null;
    }

    public byte[] readData(byte[] bytes) {
        int len = bytes.length;
        if (this == ModbusMode.RTU) {
            byte[] tempArr = new byte[len - 2];
            System.arraycopy(bytes, 0, tempArr, 0, tempArr.length);
            byte[] crc16 = Crc16.getCrc16(tempArr);
            if (crc16[0] != bytes[len -2] || crc16[1] != bytes[len - 1]) {
                log.error("Modbus receive illegal data:{}", ByteUtil.bytesToHexString(bytes));
                return null;
            }
            if (log.isDebugEnabled()) {
                log.debug("read data:{}", ByteUtil.bytesToHexString(tempArr));
            }
            return tempArr;
        }
        if (this == ModbusMode.TCP) {
            if (log.isDebugEnabled()) {
                log.debug("read data:{}", ByteUtil.bytesToHexString(bytes));
            }
            return bytes;
        }
        return null;
    }

    public byte[] writeData(byte[] bytes) {
        if (log.isDebugEnabled()) {
            log.debug("write data:{}",ByteUtil.bytesToHexString(bytes));
        }
        int len = bytes.length;
        if (this == ModbusMode.RTU) {
            byte[] crc16 = Crc16.getCrc16(bytes);
            byte[] tempArr = new byte[len + 2];
            System.arraycopy(bytes, 0, tempArr, 0, len);
            tempArr[len] = crc16[0];
            tempArr[len + 1] = crc16[1];
            return tempArr;
        }
        if (this == ModbusMode.TCP) {
            byte[] tempArr = new byte[len + 6];
            tempArr[1] = 0x01;
            byte[] lenBytes = ByteUtil.intToBytes(len, 2, 2);
            tempArr[4] = lenBytes[0];
            tempArr[5] = lenBytes[1];
            System.arraycopy(bytes, 0, tempArr, 6, len);
            return tempArr;
        }
        return null;
    }


}

ModbusFunc

功能码

package com.bho.modbus.model;


/**
 * Modbus常见功能码
 */
public enum ModbusFunc {

    /**
     * 错误代码
     * 01:非法的功能码
     * 02:非法的寄存器地址
     * 03:非法的数据值
     * 04:从机故障
     */


    /**
     * 请求:
     *      功能代码:1字节 0x01
     *      起始地址:2字节 0x0000-0xffff
     *      线圈数量:2字节 0x0001-0x07d0(2000)
     *
     * 正确响应:
     *      功能代码:1字节 0x01
     *        字节数:1字节 N(读线圈个数/8,余数不为0则加1)
     *      线圈状态:N字节
     *
     * 错误响应:
     *      功能代码:1字节 0x81
     *      错误代码:1字节 0x01-0x04
     */
    READ_COILS((byte)0x01),//读连续线圈状态
    READ_DISCRETE_COILS((byte)0x02),//读离散线圈状态 同上


    /**
     * 请求:
     *       功能代码:1字节 0x03
     *       起始地址:2字节 0x0000-0xffff
     *     寄存器数量:2字节 0x0001-0x007d(125)
     *
     * 正确响应:
     *       功能代码:1字节 0x03
     *         字节数:1字节 2N(N为寄存器数量)
     *      寄存器数量:2N字节
     *
     * 错误响应:
     *      功能代码:1字节 0x83
     *      错误代码:1字节 0x01-0x04
     */
    READ_HOLDING_REGISTERS((byte)0x03),//读保持寄存器值
    READ_INPUT_REGISTERS((byte)0x04),//读输入寄存器值 同上

    /**
     * 请求:
     *      功能代码:1字节 0x05
     *      起始地址:2字节 0x0000-0xffff
     *      线圈状态:2字节 0x0000/0xff00
     *
     * 正确响应:
     *      功能代码:1字节 0x05
     *      起始地址:2字节 0x0000-0xffff
     *      线圈状态:2字节 0x0000/0xff00
     *
     * 错误响应:
     *      功能代码:1字节 0x85
     *      错误代码:1字节 0x01-0x04
     */
    WRITE_SINGLE_COILS((byte)0x05),//写单个线圈
    /**
     * 请求:
     *      功能代码:1字节 0x06
     *      起始地址:2字节 0x0000-0xffff
     *      寄存器值:2字节 0x0000-0xffff
     *
     * 正确响应:
     *      功能代码:1字节 0x06
     *      起始地址:2字节 0x0000-0xffff
     *      寄存器值:2字节 0x0000-0xffff
     *
     * 错误响应:
     *      功能代码:1字节 0x86
     *      错误代码:1字节 0x01-0x04
     */
    WRITE_SINGLE_HOLDING_REGISTERS((byte)0x06),//写单个保持寄存器
    /**
     * 请求:
     *      功能代码:1字节 0x10
     *      起始地址:2字节 0x0000-0xffff
     * 写入寄存器个数:2字节 0x0001-0x007b(123)
     *    写入字节数:1字节 2N(N为寄存器个数)
     *     寄存器值:2N字节 0x0000-0xffff
     *
     * 正确响应:
     *      功能代码:1字节 0x10
     *      起始地址:2字节 0x0000-0xffff
     * 写入寄存器个数:2字节 0x0001-0x007b(123)
     *
     * 错误响应:
     *      功能代码:1字节 0x90
     *      错误代码:1字节 0x01-0x04
     */
    WRITE_MULTI_HOLDING_REGISTERS((byte)0x10),//写多个保持寄存器
    /**
     * 请求:
     *      功能代码:1字节 0x0F
     *      起始地址:2字节 0x0000-0xffff
     *   写入线圈个数:2字节 0x0001-0x07b0(1968)
     *    写入字节数:1字节 N(N为线圈个数/8,余数不为0则加1)
     *     线圈状态:N字节
     *
     * 正确响应:
     *      功能代码:1字节 0x0F
     *      起始地址:2字节 0x0000-0xffff
     *   写入线圈个数:2字节 0x0001-0x07b0(1968)
     *
     * 错误响应:
     *      功能代码:1字节 0x8F
     *      错误代码:1字节 0x01-0x04
     */
    WRITE_MULTI_COILS((byte)0x0F),//写多个线圈

    ;

    private byte func;

    ModbusFunc(byte func) {
        this.func = func;
    }



    public byte getFunc() {
        return func;
    }
}

ModbusParamConfig

下发指令参数配置信息

package com.bho.modbus.model;


import lombok.Data;


@Data
public class ModbusParamConfig {

    private RegisterType registerType;//寄存器类型
    private int registerAddress;//寄存器地址
    private String name;//指标名称
    private DataType dataType;//指标数据类型
    private int numberSplit;//(除)倍数

    public enum RegisterType {
        COIL,
        HOLDING_REGISTER,
        INPUT_REGISTER;
    }

    public enum DataType {
        BOOL,
        FLOAT,
        INT;
    }

}

SendCmdTask

下发指令任务

package com.bho.modbus.model;

import com.alibaba.fastjson.JSONObject;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import lombok.Data;

import java.util.List;


@Data
public class SendCmdTask {


    private List<ModbusParamConfig> paramConfigs;//参数列表
    private JSONObject reqParam;//请求参数 写数据必填
    private Boolean isWrite;//是否是写数据
    private Integer slaveId;//从机ID
    private Integer reqTimeout;//请求超时时间(秒)
    private Promise<JSONObject> promise;
    private Long timerId;


    public SendCmdTask(Vertx vertx, List<ModbusParamConfig> paramConfigs, JSONObject reqParam, Boolean isWrite, Integer slaveId, Integer reqTimeout) {
        this.paramConfigs = paramConfigs;
        this.reqParam = reqParam;
        this.isWrite = isWrite;
        this.slaveId = slaveId;
        this.reqTimeout = Math.max(reqTimeout, 5);
        Promise<JSONObject> promise = Promise.promise();
        this.promise = promise;
        this.timerId = vertx.setTimer(reqTimeout * 1000, hh -> promise.tryFail("Request timeout"));
    }
}

核心类

package com.bho.modbus.core;

import com.alibaba.fastjson.JSONObject;
import com.bho.modbus.model.SendCmdTask;
import com.bho.modbus.model.ModbusFunc;
import com.bho.modbus.model.ModbusMode;
import com.bho.modbus.model.ModbusParamConfig;

import com.bho.modbus.utils.ByteUtil;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.buffer.impl.BufferImpl;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.impl.NetSocketImpl;
import lombok.extern.log4j.Log4j2;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

@Log4j2
public class ModbusConnection {
    private String ip;//从机IP
    private Integer port;//从机端口
    private AtomicBoolean isAlive;//从机是否在线
    private ModbusMode mode;//通讯模式
    private NetSocket netSocket;//客户端连接
    private boolean isInitiativeClose;//是否是主动关闭连接
    private Long failRetryTimerId;//失败重试定时器ID
    private Integer failRetryIntervalSecond;//连接断开后重连间隔时间
    private Integer reqTimeoutSecond = 1;//请求超时时间
    private Long queueTimerId;//队列定时器
    private ConcurrentLinkedQueue<SendCmdTask> writeQueue;//写队列 优先写
    private ConcurrentLinkedQueue<SendCmdTask> readQueue;//读队列
    private Map<String, Promise<byte[]>> promiseMap;

    private Vertx vertx;

    public ModbusConnection(Vertx vertx, String ip, Integer port, Integer failRetryIntervalSecond, ModbusMode mode) {
        this.vertx = vertx;
        this.ip = ip;
        this.port = port;
        this.failRetryIntervalSecond = failRetryIntervalSecond;
        this.mode = mode;
        this.isAlive = new AtomicBoolean(false);
        this.writeQueue = new ConcurrentLinkedQueue<>();
        this.readQueue = new ConcurrentLinkedQueue<>();
        this.promiseMap = new ConcurrentHashMap<>();
        consumerTaskQueue(true);
    }

    /**
     * 建立连接
     * @return
     */
    public Future<Boolean> connect(){
        NetClient netClient = vertx.createNetClient();
        return vertx.executeBlocking(b -> {
            netClient.connect(port, ip)
                    .onSuccess(socket -> {
                        log.info("Modbus connect success, ip:{}, port:{}", ip, port);
                        netSocket  = socket;
                        isAlive.set(true);
                        b.tryComplete(true);
                        NetSocketImpl netSocketImpl = (NetSocketImpl) socket;
                        netSocketImpl.channelHandlerContext().pipeline().addFirst(mode.getDecoder());
                        socket.handler(buf -> {
                            byte[] bytes = mode.readData(buf.getBytes());
                            if (bytes == null) {
                                return;
                            }
                            int slaveId = ByteUtil.bytesToInt(bytes, 0, 1);
                            int funcNo = ByteUtil.bytesToInt(bytes, 1, 1);
                            int errFuncNo = funcNo - 128;
                            String key = String.format("%s_%s", slaveId, funcNo);
                            String errKey = String.format("%s_%s", slaveId, errFuncNo);
                            if (promiseMap.containsKey(key)) {
                                Promise<byte[]> promise = promiseMap.get(key);
                                byte[] content = new byte[bytes.length - 2];
                                System.arraycopy(bytes, 2, content, 0, content.length);
                                promise.tryComplete(content);
                            } else if (promiseMap.containsKey(errKey)) {
                                Promise<byte[]> promise = promiseMap.get(errKey);
                                int data = ByteUtil.bytesToInt(bytes, 2, 1);
                                switch (data) {
                                    case 1:
                                        promise.tryFail("Illegal function code");
                                        break;
                                    case 2:
                                        promise.tryFail("Illegal register address");
                                        break;
                                    case 3:
                                        promise.tryFail("Illegal data value");
                                        break;
                                    case 4:
                                        promise.tryFail("Slave fault");
                                        break;
                                }
                            }
                        });
                        socket.closeHandler(h -> {
                            if (!isInitiativeClose) {
                                log.error("Modbus connect close, ip:{}, port:{}", ip, port);
                                failRetryTimerId = vertx.setTimer(failRetryIntervalSecond * 1000, hh -> connect());
                            } else {
                                log.info("Modbus connect close, ip:{}, port:{}", ip, port);
                            }
                        });
                    }).onFailure(err -> {
                        log.error("Modbus connect fail, ip:{}, port:{}, msg:{}", ip, port, err.getMessage());
                        isAlive.set(false);
                        b.fail(err.getMessage());
                        failRetryTimerId = vertx.setTimer(failRetryIntervalSecond * 1000, h -> connect());
                    });
        });
    }

    /**
     * 是否在线
     * @return
     */
    public boolean isActive() {
        return isAlive.get();
    }

    /**
     * 断开连接
     */
    public void close() {
        isInitiativeClose = true;
        if (failRetryTimerId != null) {
            vertx.cancelTimer(failRetryTimerId);
        }
        if (queueTimerId != null) {
            vertx.cancelTimer(queueTimerId);
        }
        if (netSocket != null) {
            netSocket.close();
        }
    }

    /**
     * 下发读写任务(串行 优先写任务)
     * 若并行可直接调用executeTask执行任务,无需排队等候一个个消费任务
     * @param task 读写任务
     * @return
     */
    public Promise<JSONObject> offerTask(SendCmdTask task) {
        if (task.getIsWrite()) {
            writeQueue.offer(task);
        } else {
            readQueue.offer(task);
        }
        return task.getPromise();
    }

    /**
     * 消费任务队列 500毫秒轮询一次 优先消费写任务
     * @param delayFlag
     */
    private void consumerTaskQueue(boolean delayFlag){
        if(delayFlag){
            queueTimerId = vertx.setTimer(500,id->{
                consumerTaskQueue(false);
            });
            return;
        }
        if(writeQueue.isEmpty() && readQueue.isEmpty()){
            consumerTaskQueue(true);
            return;
        }
        if(!writeQueue.isEmpty()){
            SendCmdTask sendCmdTask = writeQueue.poll();
            sendCmdTask.getPromise().future().onComplete(h->{
                consumerTaskQueue(false);
            });
            executeTask(sendCmdTask);
            return;
        }
        if(!readQueue.isEmpty()){
            SendCmdTask sendCmdTask = readQueue.poll();
            sendCmdTask.getPromise().future().onComplete(h->{
                consumerTaskQueue(false);
            });
            executeTask(sendCmdTask);
        }
    }

    private Future<Void> executeTask(SendCmdTask sendCmdTask){
        vertx.cancelTimer(sendCmdTask.getTimerId());
        Future<JSONObject> future;
        if (sendCmdTask.getIsWrite()) {
            future = executeWrite(sendCmdTask.getReqParam(), sendCmdTask.getParamConfigs(), sendCmdTask.getSlaveId());
        } else {
            future = executeQuery(sendCmdTask.getParamConfigs(), sendCmdTask.getSlaveId());
        }
        return future.onSuccess(res -> sendCmdTask.getPromise().tryComplete(res))
                .onFailure(err -> sendCmdTask.getPromise().tryFail(err)).map(o -> null);
    }

    /**
     * 写数据
     * @param reqParam 下发参数
     * @param paramConfigs 参数配置列表
     * @param slaveId 从机ID
     * @return
     */
    private Future<JSONObject> executeWrite(JSONObject reqParam, List<ModbusParamConfig> paramConfigs, Integer slaveId) {
        if (!isActive()) {
            return Future.failedFuture("Gateway offline");
        }
        boolean isMerge = isMergeSendCmd(paramConfigs);
        if (isMerge) {
            int registerAddress = paramConfigs.get(0).getRegisterAddress();
            ModbusParamConfig.RegisterType registerType = paramConfigs.get(0).getRegisterType();
            Promise<byte[]> promise = Promise.promise();
            List<String> keyList = paramConfigs.stream().map(ModbusParamConfig::getName).collect(Collectors.toList());
            return vertx.executeBlocking(h -> {
                Buffer buffer = getWriteCmd(registerAddress, slaveId, reqParam, keyList, registerType, promise);
                netSocket.write(buffer);
                promise.future().onSuccess(buf -> {
                    h.complete(reqParam);
                }).onFailure(err -> {
                    log.error("Modbus executeWrite fail, ip:{}, port:{}, slaveId:{}, msg:{}", ip, port, slaveId, err.getMessage());
                    h.tryFail(err.getMessage());
                });
            });
        }

        List<Future<Object>> futures = new ArrayList<>();
        Future blockingFuture = Future.succeededFuture();
        for (int i = 0; i < paramConfigs.size(); i++) {
            ModbusParamConfig paramConfig = paramConfigs.get(i);
            ModbusParamConfig.RegisterType registerType = paramConfig.getRegisterType();
            Promise<byte[]> promise = Promise.promise();
            blockingFuture = blockingFuture.compose(suc -> singleExecuteWrite(slaveId, reqParam, promise, registerType, paramConfig),
                    err -> singleExecuteWrite(slaveId, reqParam, promise, registerType, paramConfig));
            futures.add(blockingFuture);
        }
        return commonReplyResult(futures, paramConfigs);

    }

    private Future<Object> singleExecuteWrite(int slaveId, JSONObject reqParam, Promise<byte[]> promise, ModbusParamConfig.RegisterType registerType, ModbusParamConfig paramConfig) {
        return vertx.executeBlocking(h -> {
            Buffer buffer = getWriteCmd(paramConfig.getRegisterAddress(), slaveId, reqParam, Arrays.asList(paramConfig.getName()), registerType, promise);
            netSocket.write(buffer);
            promise.future().onSuccess(buf -> {
                h.tryComplete(reqParam.get(paramConfig.getName()));
            }).onFailure(err -> {
                log.error("Modbus executeWrite fail, ip:{}, port:{}, slaveId:{}, key:{}, msg:{}",
                        ip, port, slaveId, paramConfig.getName(), err.getMessage());
                h.tryFail(err.getMessage());
            });
        });
    }

    /**
     * 读数据
     * @param paramConfigs 参数配置列表
     * @param slaveId 从机ID
     * @return
     */
    private Future<JSONObject> executeQuery(List<ModbusParamConfig> paramConfigs, Integer slaveId) {
        if (!isActive()) {
            return Future.failedFuture("Gateway offline");
        }
        boolean isMerge = isMergeSendCmd(paramConfigs);
        if (isMerge) {
            int registerAddress = paramConfigs.get(0).getRegisterAddress();
            ModbusParamConfig.RegisterType registerType = paramConfigs.get(0).getRegisterType();
            int num = paramConfigs.size();
            Promise<byte[]> promise = Promise.promise();
            Buffer buffer = getQueryCmd(registerAddress, num, slaveId, registerType, promise);
            return vertx.executeBlocking(h -> {
                netSocket.write(buffer);
                promise.future().onSuccess(buf -> {
                    JSONObject jsonObject = new JSONObject();
                    for (int i = 0; i < paramConfigs.size(); i++) {
                        ModbusParamConfig paramConfig = paramConfigs.get(i);
                        switch (registerType) {
                            case COIL:
                                Integer pow = Double.valueOf(Math.pow(2, i % 8)).intValue();
                                jsonObject.put(paramConfig.getName(), (pow & buf[i / 8 + 1]) == pow);
                                break;
                            case INPUT_REGISTER:
                            case HOLDING_REGISTER:
                                jsonObject.put(paramConfig.getName(), getValue(ByteUtil.bytesToInt(buf, i * 2 + 1, 2), paramConfig.getNumberSplit(), paramConfig.getDataType()));
                                break;
                        }
                    }
                    h.complete(jsonObject);
                }).onFailure(err -> {
                    log.error("Modbus executeQuery fail, ip:{}, port:{}, slaveId:{}, msg:{}", ip, port, slaveId, err.getMessage());
                    h.tryFail(err.getMessage());
                });
            });
        }
        List<Future<Object>> futures = new ArrayList<>();
        Future blockingFuture = Future.succeededFuture();
        for (int i = 0; i < paramConfigs.size(); i++) {
            ModbusParamConfig paramConfig = paramConfigs.get(i);
            ModbusParamConfig.RegisterType registerType = paramConfig.getRegisterType();
            Promise<byte[]> promise = Promise.promise();
            blockingFuture = blockingFuture.compose(suc -> singleExecuteQuery(slaveId, promise, registerType, paramConfig),
                    err -> singleExecuteQuery(slaveId, promise, registerType, paramConfig));
            futures.add(blockingFuture);
        }
        return commonReplyResult(futures, paramConfigs);
    }

    private Future<Object> singleExecuteQuery(int slaveId, Promise<byte[]> promise, ModbusParamConfig.RegisterType registerType, ModbusParamConfig paramConfig) {
        return vertx.executeBlocking(h -> {
            Buffer buffer = getQueryCmd(paramConfig.getRegisterAddress(), 1, slaveId, paramConfig.getRegisterType(), promise);
            netSocket.write(buffer);
            promise.future().onSuccess(buf -> {
                switch (registerType) {
                    case COIL:
                        h.complete(Integer.valueOf(buf[1]) == 1);
                        break;
                    case INPUT_REGISTER:
                    case HOLDING_REGISTER:
                        h.complete(getValue(ByteUtil.bytesToInt(buf, 1, 2), paramConfig.getNumberSplit(), paramConfig.getDataType()));
                        break;
                }
            }).onFailure(err -> {
                log.error("Modbus executeQuery fail, ip:{}, port:{}, slaveId:{}, key:{}, msg:{}",
                        ip, port, slaveId, paramConfig.getName(), err.getMessage());
                h.tryFail(err.getMessage());
            });
        });
    }

    /**
     * 如果所有参数寄存器类型一致并且地址连续 则合并成一条命令下发
     * @param paramConfigs
     * @return 是否可以合并下发命令
     */
    private boolean isMergeSendCmd(List<ModbusParamConfig> paramConfigs) {
        if (paramConfigs.size() == 1) {
            return false;
        }
        int lastPos = paramConfigs.get(0).getRegisterAddress();
        ModbusParamConfig.RegisterType registerType = paramConfigs.get(0).getRegisterType();
        for (int i = 1; i < paramConfigs.size(); i++) {
            int curPos = paramConfigs.get(i).getRegisterAddress();
            if (curPos - lastPos != 1) {
                return false;
            }
            ModbusParamConfig.RegisterType curRegisterType = paramConfigs.get(i).getRegisterType();
            if (registerType != curRegisterType) {
                return false;
            }
            lastPos = curPos;
        }
        return true;
    }

    /**
     * 获取查询数据命令
     * @param startPos 查询地址
     * @param num 查询数量
     * @param slaveId 从机ID
     * @param registerType 寄存器类型
     * @param promise
     * @return
     */
    private Buffer getQueryCmd(int startPos, int num, int slaveId, ModbusParamConfig.RegisterType registerType, Promise<byte[]> promise) {
        byte[] bytes = new byte[6];
        bytes[0] = ByteUtil.intToBytes(slaveId, 3, 1)[0];
        switch (registerType) {
            case COIL:
                bytes[1] = ModbusFunc.READ_COILS.getFunc();
                break;
            case HOLDING_REGISTER:
                bytes[1] = ModbusFunc.READ_HOLDING_REGISTERS.getFunc();
                break;
            case INPUT_REGISTER:
                bytes[1] = ModbusFunc.READ_INPUT_REGISTERS.getFunc();
                break;
        }
        Integer func = ByteUtil.bytesToInt(bytes, 1, 1);
        String key = String.format("%s_%s", slaveId, func);
        byte[] startPosBytes = ByteUtil.intToBytes(startPos, 0, 4);
        bytes[2] = startPosBytes[2];
        bytes[3] = startPosBytes[3];
        byte[] numBytes = ByteUtil.intToBytes(num, 0, 4);
        bytes[4] = numBytes[2];
        bytes[5] = numBytes[3];
        Buffer buffer = new BufferImpl();
        buffer.appendBytes(mode.writeData(bytes));
        promiseMap.put(key, promise);
        long timeId = vertx.setTimer(reqTimeoutSecond * 1000, h -> promise.tryFail("Request timeout"));
        promise.future().onComplete(res -> {
            promiseMap.remove(key);
            vertx.cancelTimer(timeId);
        });
        return buffer;
    }

    /**
     * 获取写数据命令
     * @param startPos 查询地址
     * @param slaveId 从机ID
     * @param reqParam 写参数
     * @param keys  参数列表
     * @param registerType 寄存器类型
     * @param promise
     * @return
     */
    private Buffer getWriteCmd(int startPos, int slaveId, JSONObject reqParam,
                               List<String> keys, ModbusParamConfig.RegisterType registerType, Promise<byte[]> promise) {
        int len = keys.size() == 1 ? 6 : (registerType == ModbusParamConfig.RegisterType.HOLDING_REGISTER ?
                7 + keys.size() * 2 : 7 + Double.valueOf(Math.ceil(keys.size() / 8.0)).intValue());
        byte[] bytes = new byte[len];
        bytes[0] = ByteUtil.intToBytes(slaveId, 3, 1)[0];
        byte[] startPosBytes = ByteUtil.intToBytes(startPos, 0, 4);
        bytes[2] = startPosBytes[2];
        bytes[3] = startPosBytes[3];
        if (keys.size() == 1) {
            switch (registerType) {
                case COIL:
                    bytes[1] = ModbusFunc.WRITE_SINGLE_COILS.getFunc();
                    boolean value = reqParam.getBoolean(keys.get(0));
                    if (value) {
                        bytes[4] = (byte) 0xFF;
                    } else {
                        bytes[4] = 0x00;
                    }
                    bytes[5] = 0x00;
                    break;
                case HOLDING_REGISTER:
                    bytes[1] = ModbusFunc.WRITE_SINGLE_HOLDING_REGISTERS.getFunc();
                    byte[] dataArr = ByteUtil.intToBytes(reqParam.getInteger(keys.get(0)), 2, 2);
                    bytes[4] = dataArr[0];
                    bytes[5] = dataArr[1];
                    break;
            }
        } else {
            byte[] dataNum = ByteUtil.intToBytes(keys.size(), 2, 2);
            bytes[4] = dataNum[0];
            bytes[5] = dataNum[1];
            switch (registerType) {
                case COIL:
                    bytes[1] = ModbusFunc.WRITE_MULTI_COILS.getFunc();
                    int dataSize = Double.valueOf(Math.ceil(keys.size() / 8.0)).intValue();
                    bytes[6] = ByteUtil.intToBytes(dataSize, 3, 1)[0];
                    for (int i = 0; i < dataSize; i += 2) {
                        int sum = 0;
                        int startIndex = i * 8;
                        int endIndex = (i + 2) * 8;
                        endIndex = endIndex > keys.size() ? keys.size() : endIndex;
                        for (int j = startIndex; j < endIndex; j++) {
                            sum += Double.valueOf(Math.pow(2, j)).intValue() * (reqParam.getBoolean(keys.get(j)) ? 1 : 0);
                        }
                        byte[] sumArr = ByteUtil.intToBytes(sum, 2, 2);
                        if (i + 8 < keys.size()) {
                            bytes[i + 7] = sumArr[0];
                            bytes[i + 8] = sumArr[1];
                        } else {
                            bytes[i + 7] = sumArr[1];
                        }
                    }
                    break;
                case HOLDING_REGISTER:
                    bytes[1] = ModbusFunc.WRITE_MULTI_HOLDING_REGISTERS.getFunc();
                    bytes[6] = ByteUtil.intToBytes(keys.size() * 2, 3, 1)[0];
                    for (int i = 0; i < keys.size(); i++) {
                        String paramKey = keys.get(i);
                        Integer value = reqParam.getInteger(paramKey);
                        byte[] dataArr = ByteUtil.intToBytes(value, 2, 2);
                        bytes[i * 2 + 7] = dataArr[0];
                        bytes[i * 2 + 8] = dataArr[1];
                    }
                    break;
            }
        }
        Integer func = ByteUtil.bytesToInt(bytes, 1, 1);
        String key = String.format("%s_%s", slaveId, func);
        Buffer buffer = new BufferImpl();
        buffer.appendBytes(mode.writeData(bytes));
        promiseMap.put(key, promise);
        long timeId = vertx.setTimer(reqTimeoutSecond * 1000, h -> promise.tryFail("Request timeout"));
        promise.future().onComplete(res -> {
            promiseMap.remove(key);
            vertx.cancelTimer(timeId);
        });
        return buffer;
    }

    private Future<JSONObject> commonReplyResult(List<Future<Object >> futures, List<ModbusParamConfig> paramConfigs) {
        return vertx.executeBlocking(b -> {
            Future.join(futures).onComplete(h -> {
                JSONObject okJson = new JSONObject();
                JSONObject errJson = new JSONObject();
                for (int i = 0; i < paramConfigs.size(); i++) {
                    ModbusParamConfig paramConfig = paramConfigs.get(i);
                    Future<Object> objectFuture = futures.get(i);
                    if (objectFuture.succeeded()) {
                        okJson.put(paramConfig.getName(), objectFuture.result());
                    } else {
                        errJson.put(paramConfig.getName(), objectFuture.cause().getMessage());
                    }
                }
                if (okJson.size() > 0) {
                    b.tryComplete(okJson);
                } else {
                    b.tryFail(errJson.getString(paramConfigs.get(0).getName()));
                }
            });
        });
    }

    private Object getValue(int value, int numberSplit, ModbusParamConfig.DataType dataType) {
        if (numberSplit == 1) {
            return value;
        }
        Float temp = value * 1f / numberSplit;
        switch (dataType) {
            case INT :
                return Math.round(temp);
            case FLOAT:
                return temp;
        }
        return temp;
    }

}

测试

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.bho.modbus.model.ModbusMode;
import com.bho.modbus.core.ModbusConnection;
import com.bho.modbus.model.ModbusParamConfig;
import com.bho.modbus.model.SendCmdTask;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import lombok.extern.log4j.Log4j2;

import java.util.List;

@Log4j2
public class TestModbus {
    public static final String READ_DATA = "[" +
            "        {" +
            "          \"name\": \"a\"," +
            "          \"registerType\": \"HOLDING_REGISTER\"," +
            "          \"registerAddress\": 504," +
            "          \"dataType\": \"FLOAT\"," +
            "          \"numberSplit\": 10" +
            "        }," +
            "        {" +
            "          \"name\": \"b\"," +
            "          \"registerType\": \"HOLDING_REGISTER\"," +
            "          \"registerAddress\": 505," +
            "          \"dataType\": \"FLOAT\"," +
            "          \"numberSplit\": 10" +
            "        }," +
            "        {" +
            "          \"name\": \"c\"," +
            "          \"registerType\": \"HOLDING_REGISTER\"," +
            "          \"registerAddress\": 506," +
            "          \"dataType\": \"FLOAT\"," +
            "          \"numberSplit\": 10" +
            "        }," +
            "        {" +
            "          \"name\": \"d\"," +
            "          \"registerType\": \"HOLDING_REGISTER\"," +
            "          \"registerAddress\": 507," +
            "          \"dataType\": \"INT\"," +
            "          \"numberSplit\": 1" +
            "        }," +
            "        {" +
            "          \"name\": \"e\"," +
            "          \"registerType\": \"HOLDING_REGISTER\"," +
            "          \"registerAddress\": 508," +
            "          \"dataType\": \"INT\"," +
            "          \"numberSplit\": 1" +
            "        }]";

    private static final String WRITE_DATA = "[" +
            "        {" +
            "          \"name\": \"do0\"," +
            "          \"registerType\": \"COIL\"," +
            "          \"registerAddress\": 20," +
            "          \"dataType\": \"BOOL\"," +
            "          \"numberSplit\": 1" +
            "        }" +
            "        ,{" +
            "          \"name\": \"do1\"," +
            "          \"registerType\": \"COIL\"," +
            "          \"registerAddress\": 21," +
            "          \"dataType\": \"BOOL\"," +
            "          \"numberSplit\": 1" +
            "        }" +

            "]";
    public static void main(String[] args) {
        testReadData();
//        testWriteData();;


    }

    private static void testWriteData() {
        Vertx vertx = Vertx.vertx();
        ModbusConnection connection = new ModbusConnection(vertx,"127.0.0.1", 502, 30, ModbusMode.TCP);
        Future<Boolean> connectFuture = connection.connect();
        JSONObject reqParam = new JSONObject();
        reqParam.put("do0", false);
        reqParam.put("do1", false);
        List<ModbusParamConfig> modbusParamConfigs = JSONArray.parseArray(WRITE_DATA, ModbusParamConfig.class);
        connectFuture.onComplete(con -> {
            if (connectFuture.succeeded()) {
                SendCmdTask task = new SendCmdTask(vertx, modbusParamConfigs, null, false, 21, 10);
                Promise<JSONObject> promise = connection.offerTask(task);
                promise.future().onSuccess(suc -> {
                    log.info("read:"+suc);
                }).onFailure(err -> System.err.println(err.getMessage()));

                SendCmdTask task2 = new SendCmdTask(vertx, modbusParamConfigs, reqParam, true, 21, 10);
                Promise<JSONObject> promise2 = connection.offerTask(task2);
                promise2.future().onSuccess(suc -> {
                    log.info("write:"+suc);
                }).onFailure(err -> System.err.println(err.getMessage()));
            } else {
                System.err.println("gateway offline");
            }
        });

    }

    private static void testReadData() {
        Vertx vertx = Vertx.vertx();
        ModbusConnection connection = new ModbusConnection(vertx,"127.0.0.1", 502, 30, ModbusMode.TCP);
        Future<Boolean> connectFuture = connection.connect();
        List<ModbusParamConfig> modbusParamConfigs = JSONArray.parseArray(READ_DATA, ModbusParamConfig.class);
        connectFuture.onComplete(con -> {
            if (connection.isActive()) {
                SendCmdTask task = new SendCmdTask(vertx, modbusParamConfigs, null, false, 2, 10);
                Promise<JSONObject> promise = connection.offerTask(task);
                promise.future().onSuccess(suc -> {
                    log.info(suc);
                }).onFailure(err -> System.err.println(err.getMessage()));
            } else {
                System.err.println("gateway offline");
            }
        });

    }
}

运行结果如下:
其实这两个读写示例如果是一个网关可以共用一个Modbus连接。
在这里插入图片描述
在这里插入图片描述

modbus-app配置参数在这里插入图片描述

格式如下:

{
  "readable": {
    "devType01": {
      "ReportData": [
        {
          "name" : "xxx",
          "registerType" : "COIL",
          "registerAddress" : 1,
          "dataType" : "BOOL",
          "numberSplit" : 1
        }
      ]
    },
    "devType02": {
      "ReportData": [
        {
          "name" : "a",
          "registerType" : "HOLDING_REGISTER",
          "registerAddress" : 1,
          "dataType" : "INT",
          "numberSplit" : 1
        },
        {
          "name" : "b",
          "registerType" : "HOLDING_REGISTER",
          "registerAddress" : 2,
          "dataType" : "INT",
          "numberSplit" : 10
        },
        {
          "name": "c",
          "registerType": "",
          "dataType": "FLOAT",
          "mbScript": "(a*10000+b)/10"
        }
      ]
    }
  },
  "writable": {
    "devType01": {
      "Control": [
        {
          "name": "operation",
          "registerType": "COIL",
          "registerAddress": 21,
          "dataType": "BOOL",
          "numberSplit": 1
        }
      ]
    }
  },
  "readDataPeriods": [
    {
      "period" : 60,
      "deviceTypes": ["devType01"]
    },
    {
      "period" : 600,
      "deviceTypes": ["devType02","devType03"]
    }
  ]
}

具体怎么实现这边就不过多讲解了…

结束

不保证代码正确,我这边只是大概实现了一下,仅供参考。若有问题,请批评指出,我会虚心接受并积极修复问题。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

什么牌子的电容笔比较好?开学值得买触控笔推荐

大部分学生都没有固定的收入&#xff0c;所以他们选择的商品都是偏向性价比高的。随着iPad的不断升级&#xff0c;它的各种功能也会越来越多&#xff0c;将会慢慢地走进我们的生活和工作中。随着电子设备的不断更新和软件的完善&#xff0c;电容笔的性能也在不断提高&#xff0…

783. 二叉搜索树节点最小距离

783. 二叉搜索树节点最小距离 C代码&#xff1a;二叉树 int min; int pre;int dfs(struct TreeNode* root) {if (root NULL) {return;}dfs(root->left);if (pre ! -1) {min fmin(min, root->val - pre);}pre root->val; // 中序遍历dfs(root->right); }int mi…

【深度学习】 Python 和 NumPy 系列教程(三):Python容器:1、列表List详解(初始化、索引、切片、更新、删除、常用函数、拆包、遍历)

目录 一、前言 二、实验环境 三、Python容器&#xff08;Containers&#xff09; 0、容器介绍 1、列表&#xff08;List&#xff09; 1. 初始化 a. 创建空列表 b. 使用现有元素初始化列表 c. 使用列表生成式 d. 复制列表 2. 索引和切片 a. 索引 b. 负数索引 c. 切…

MySQL触发器详解保证入土

文章目录 简介一、MySQL触发器基础触发器分类基础常用关键字1. 定义触发器2. 创建和删除触发器3. 执行时机和条件 二、MySQL触发器的使用场景1. 数据完整性约束插入触发器更新触发器删除触发器 2. 数据变更日志的记录与追踪3. 触发器与存储过程的对比与选择 三、触发器的性能和…

强大的JTAG边界扫描(5):FPGA边界扫描应用

文章目录 1. 获取芯片的BSDL文件2. 硬件连接3. 边界扫描测试4. 总结 上一篇文章&#xff0c;介绍了基于STM32F103的JTAG边界扫描应用&#xff0c;演示了TopJTAG Probe软件的应用&#xff0c;以及边界扫描的基本功能。本文介绍基于Xilinx FPGA的边界扫描应用&#xff0c;两者几乎…

巨人互动|Facebook海外户Facebook风控规则有什么

Facebook是全球最大的社交媒体平台之一&#xff0c;每天有数十亿的用户在其上发布、分享和交流各种内容。为了维护平台的安全性和用户体验&#xff0c;Facebook制定了严格的风控规则来监测和处理违规行为。下面小编讲讲Facebook风控规则。 巨人互动|Google海外户&Google Ad…

CocosCreator3.8研究笔记(十一)CocosCreator Prefab(预制件)理解

相信很多朋友都不知道 Prefab 是什么&#xff1f;为什么要使用Prefab &#xff1f; 怎么使用Prefab&#xff1f; 接下来&#xff0c;我们就一步一步来揭晓答案。 一、Prefab 是什么 &#xff1f; Prefab&#xff1a;大家习惯性地称为“预制件” 或“预制体” &#xff0c;简单说…

Java事件机制简介 内含面试题

面试题分享 云数据解决事务回滚问题 点我直达 2023最新面试合集链接 2023大厂面试题PDF 面试题PDF版本 java、python面试题 项目实战:AI文本 OCR识别最佳实践 AI Gamma一键生成PPT工具直达链接 玩转cloud Studio 在线编码神器 玩转 GPU AI绘画、AI讲话、翻译,GPU点亮…

昇腾Ascend TIK自定义算子开发教程(概念版)

一、参考资料 【2023 CANN训练营第一季】Ascend C算子开发入门&#xff08;中&#xff09; 二、重要说明 TIK2编程范式把算子核内的处理程序&#xff0c;分成多个流水任务&#xff0c;任务之间通过队列&#xff08;Queue&#xff09;进行通信和同步&#xff0c;并通过统一的…

MySQL——备份和还原

备份 热备 即MySQL服务在运行的时候进行的备份 mysqldump命令 mysqldump --databases db1 db2 db3 > dump.sql mysqldump -uroot -pSanchuang1234# --all-databases >all_db.sql mysqldump -uroot -pSanchuang123# --databases TENNIS >/backup/tennis.sql mysq…

分享一个python基于数据可视化的智慧社区服务平台源码

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人七年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、微信小程序、爬虫、大数据等&#xff0c;大家有这一块的问题可以一起交流&#xff01; &#x1…

【rust/egui】(九)使用painter绘制一些图形—基本使用

说在前面 rust新手&#xff0c;egui没啥找到啥教程&#xff0c;这里自己记录下学习过程环境&#xff1a;windows11 22H2rust版本&#xff1a;rustc 1.71.1egui版本&#xff1a;0.22.0eframe版本&#xff1a;0.22.0上一篇&#xff1a;这里 painter 定义pub struct Painter {///…

领域驱动设计:DDD分层架构

文章目录 DDD 分层架构DDD 分层架构最重要的原则DDD 分层架构推动架构演进三层架构如何演进到 DDD 分层架构 微服务架构模型有好多种&#xff0c;例如整洁架构、CQRS 和六边形架构等等。每种架构模式虽然提出的时代和背景不同&#xff0c;但其核心理念都是为了设计出“高内聚低…

gpt测试

已知a地一石头售价80&#xff0c;b地售价112&#xff0c;小明初始资金8000&#xff0c;在a地全仓购入后&#xff0c;去b地出售&#xff0c;然后小明又回到a地&#xff0c;再次全仓购入然后去b地出售&#xff0c;这样继续出售10次后&#xff0c;小明有多少钱&#xff1f;石头是不…

Java中wait和notify详解

线程的调度是无序的&#xff0c;随机的&#xff0c;但是也是有一定的需求场景&#xff0c;希望能够有序执行&#xff0c;join算是一种控制顺序的方式&#xff08;功能有限&#xff09;——》一个线程执行完&#xff0c;才能执行另一个线程&#xff01; 本文主要讲解的&#xf…

C++---类和对象

这里写目录标题 封装简介语法二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录 封装 简介语法 类 &#xff1a;抽象的 共性的 对象&#xff1a;实例化的 具体的 个性的 封装 就是把属性和行为放在一起 加一些访问权…

CCRC-PIPA个人信息保护评估师

个人信息保护评估师 (Personal InformationProtec-tion Assessor&#xff0c;简称 “PIPA”) 是由中国网络安全审查技术与认证中心(简称CCRC) 推出的面向个人信息保护领域的培训认证。CCRC-PIPA课程以《个人信息保护法》、法规、部门规章、相关国家标准和行业最佳实践为基础&am…

代理IP在海外SEO优化中有哪些关键作用?

代理IP在海外SEO优化业务中的应用越来越受到企业的重视。它为企业提供了大量不同地区的IP地址&#xff0c;帮助企业模拟不同地区、不同设备的用户行为&#xff0c;有助于更准确地了解当地的搜索引擎规则和优化策略&#xff0c;更好地评估网站的排名和流量。 一、代理IP的优势 …

【Stable Diffusion XL】huggingface diffusers 官方教程解读

文章目录 01 TutorialDeconstruct a basic pipelineDeconstruct the Stable Diffusion pipelineAutopipelineTrain a diffusion model 相关链接&#xff1a; GitHub&#xff1a; https://github.com/huggingface/diffusers 官方教程&#xff1a;https://huggingface.co/docs/di…

第2章_瑞萨MCU零基础入门系列教程之面向过程与面向对象

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…