目录


  • 基本参数
    接口
    串口参数
    演示上位机串口通信连接步骤
    电源地址码修改方法
    通讯协议
    MODBUS_RTU帧结构
    地址域
    功能域
    数据域
    CRC校验

    MODBUS RTU 通讯协议
    通讯信息传输过程
    地址码
    功能码
    数据区
    静止时间要求
    MODBUS功能码简介
    功能码“03”:读多路寄存器输入
    功能码“06”:写单个寄存器
    错误校验码(CRC校验)
    使用python控制
    依赖
    python脚本
    运行效果

客服提供的资料合集:eTM-3020C程控电源通信协议(P系列_DP系列_C系列)

基本参数

接口

  (摘自客服提供的资料《联机使用步骤》)

  本机可选配RS232或者RS485或者USB传输标准与计算机通信, 仅支持Modbus通讯协议(目前只支持功能码03、06)。

串口参数

  波特率:9600;
  起始位:1;
  数据位:8;
  校验位:无;
  停止位:1;
  地址码:0x01(可修改);

演示上位机串口通信连接步骤

  • 连接通信线、电源线,打开电源;

    • 注:

    1. 如选配的接口是RS232接口时串口线需要使用出厂机器配送的2-3母母交叉线;
    2. 如选配的接口是USB接口时,需要确认插上USB线、打开电源后,在电脑“设备管理器”中能识别到USB对应的串口号,如果没有识别到设备口号则需要安装相应的USB驱动;
  • 打开上位机演示软件;

  • 在软件上添加“电源设备”:打开上位机软件后,在电脑桌面右下角的任务栏找到软件图标,右键选择“采样端口设置”,在“设备名称”窗口设置一个设备名称,在“端口号”窗口选择电源联机后对应的串口号(可在电脑的“设备管理器”中查找对应的COM号),其他的选项按需要选择或默认参数即可,设置完参数点“添加设备”, 此时软件窗口这边就会生成对应的电源设备图标;

  • 双击已添加的电源设备图标,打开串口进行控制;

电源地址码修改方法

  (摘自客服提供的资料《电源地址码修改方法》)

  本机出厂默认地址码为0x01,如需要修改本机地址码,可参照下面方法修改:

  • 方法一:连接串口后通过发送指令修改地址码,详细可参照通信资料;
  • 方法二:进入工程模式修改:
    1. 型号P系列电源(功率300W(含)以下的小程控电源)修改方法:
      1. 在电源关机的状态下,按下电源“ON/OFF”小开关不松手再按下橙色电源开关给电源上电,直到电源电压窗口显示“Addr”内容后松开电源“ON/OFF”小开关,此时已进入工程模式;
      2. 电压窗口显示“Addr”的同时,电流窗口显示的就是本机地址码值,此时, 可直接通过按“M3”、“M4”按键增加或者减小地址码值;(地址码可设置范围:0-250);设置完重新开机即开始生效;
    2. 型号C系列电源(大功率双旋钮数控电源)修改方法:
      1. 在电源关机的状态下,按下电源“ON/OFF”小开关不松手再按下橙色电源开关给电源上电,直到电源电压窗口显示“O.U.T.”内容松开电源“ON/OFF”小开关,此时已进入工程模式;
      2. 在电压窗口显示为“O.U.T.” 时,按一次“电压调节旋钮”切换电压窗口显示为“Addr”,此时对应功率窗口显示的就是本机地址码值,然后通过旋转“电压调节旋钮”可增加或者减小地址码值;(地址码可设置范围:0-250);设置完重新开机即开始生效;

通讯协议

  (摘自客服提供的资料《(D)P系列_C系列通讯协议》)

  P系列/C系列仪表采用RS232或者RS485传输标准与计算机通讯(本机只支持功能码03,06 —— 20180611版)

  波特率:9600
  起始位:1
  数据位:8
  校验位:无
  停止位:1

MODBUS_RTU帧结构

  消息发送至少要以3.5个字符时间的停顿间隔开始;整个消息帧必须作为一连续的数据传输流,如果在帧完成之前有超过3.5个字符时间的停顿时间,接收设备将刷新不完整的消息并假定下一字节是一个新消息的地址域。同样地,如果一个新消息在小于3.5个字符时间内接着前个消息开始,接收的设备将认为它是前一消息的延续。


  一帧信息的标准结构如下所示:

开始 地址域 功能域 数据域 CRC校验 结束
T1-T2-T3-T4 8Bit 8Bit n个8Bit 16Bit T1-T2-T3-T4

地址域

  主机通过将要联络的从机的地址放入消息中的地址域来选通从设备,单个从机的地址范围是1…15(十进制)。
  地址0是用作广播地址,以使所有的从机都能认识。

功能域

  有效的编码范围是1…255(十进制);当消息从主机发往从机时,功能代码将告之从机需要去干什么。例如:读/写一组寄存器的数据内容等。

数据域

  主机发给从机的数据域中包含了从机完成功能域的动作时所必要的附加信息;如:寄存器地址等。

CRC校验

  CRC生成之后,低字节在前,高字节在后。

  本仪表通讯时帧与帧之间的响应间隔,通讯速率大于等于9600bps时不大于5ms

序号 名称 说明 范围 小数点位数 读写 参数通讯地址
0 ON/OFF 电源输出/停止设定 1,0 0 r/w 0001H
1 OP.S 保护状态(注1) 0-0xFFFF 0 r 0002H
2 规格型号 规格型号 0-65535 0 r 0003H
3 尾缀分类 尾缀分类 0-0xFFFF 0 r 0004H
4 小数点位数 V_A_W位数(注2) 0-0xFFFF 0 r 0005H
5 U 电源的电压显示值 0-65535 2 r 0010H
6 I 电源的电流显示值 0-65535 3 r 0011H
7 P 电源的功率显示值 32位数据 3 r 0012H(高16位),0013H(低16位)
9 SetU 设置电压 0-65535 2 r/w 0030H
10 SetI 设置电流 0-65535 3 r/w 0031H
12 OVP 过压保护设定值 0-65535 2 r/w 0020H
13 OCP 过流保护设定值 0-65535 2 r/w 0021H
14 OPP 过功率保护设定值 32位数据 2 r/w 0022H(高16位),0023H(低16位)
15 RS-Adder 通讯地址设定 1~250 0 r/w 9999H

  红注:红色序号部分是公用的。
  蓝色序号部分是可编程专用的。
  黑色序号部分是可选的。

    • 注1:保护状态位定义如下

union _ST
{
	struct
	{
		uint8_t isOVP:1;	//过压保护
		uint8_t isOCP:1;	//过流保护
		uint8_t isOPP:1;	//过功率保护
		uint8_t isOTP:1;	//过温保护
		uint8_t isSCP:1;	//短路保护
	}OP;
	uint8_t Dat;
};

  • 注2:小数点位数信息如下图
case 0x0005://电压电流功率小数点位数
	Dat = ShowPN;// ((2<<8) | (3<<4) | (3<<0)) ;//0.00V 0.000A 0.000W
	break;

  比如读到:0x0233,就是 电压2个小数,电流3个小数,功率3个小数

MODBUS RTU 通讯协议

  • 通讯数据的类型及格式:
      信息传输为异步方式,并以字节为单位。在主站和从站之间传递的通讯信息是10位的字格式:

字格式(串行数据) 10位二进制
起始位 1位
数据位 8位
奇偶校验位
停止位 1位

  • 通讯数据(信息帧)格式
数据格式 地址码 功能码 数据区 CRC校验
数据长度 1字节 1字节 N字节 16位CRC码(冗余循环码)

通讯信息传输过程

  当通讯命令由发送设备(主机)发送至接收设备(从机)时,符合相应地址码的从机接收通讯命令,并根据功能码及相关要求读取信息,如果CRC校验无误,则执行相应的任务,然后把执行结果(数据)返送给主机。返回的信息中包括地址码、功能码、执行后的数据以及CRC校验码。如果CRC校验出错就不返回任何信息。

地址码

  地址码是每次通讯信息帧的第一字节(8位),从1到250。这个字节表明由用户设置地址的从机将接收由主机发送来的信息。每个从机都必须有唯一的地址码,并且只有符合地址码的从机才能响应回送信息。当从机回送信息时,回送数据均以各自的地址码开始。主机发送的地址码表明将发送到的从机地址,而从机返回的地址码表明回送的从机地址。相应的地址码表明该信息来自于何处。

功能码

  是每次通讯信息帧传送的第二个字节。ModBus通讯规约可定义的功能码为1到127。作为主机请求发送,通过功能码告诉从机应执行什么动作。作为从机响应,从机返回的功能码与从主机发送来的功能码一样,并表明从机已响应主机并且已进行相关的操作。


MODBUS部分功能码

功能码 定 义 操 作(二进制)
02 读开关量输入DI 读取一路或多路开关量状态输入数 (遥信)
01 读状态量输出OUT 读取一路或多路开关量输出状态数据
03 读寄存器数据 读取一个或多个寄存器的数据
05 写开关量输出OUT 控制一路继电器“合/分”输出,遥控
06 写单路寄存器 把一组二进制数据写入单个寄存器
10 写多路寄存器 把多组二进制数据写入多个寄存器

数据区

  数据区包括需要由从机返送何种信息或执行什么动作。这些信息可以是数据(如:开关量输入/输出、模拟量输入/输出、寄存器等等)、参考地址等。例如,主机通过功能码03告诉从机返回寄存器的值(包含要读取寄存器的起始地址及读取寄存器的长度),则返回的数据包括寄存器的数据长度及数据内容。对于不同的从机,地址和数据信息都不相同(应给出通讯信息表)。
  电源采用Modbus通讯规约,主机(PLC、RTU、PC机、DCS等)利用通讯命令(功能码03),可以任意读取其数据寄存器(其数据信息表详见附录)。
  响应的命令格式是从机地址、功能码、数据区及CRC码。数据区的数据都是两个字节,并且高位在前。

静止时间要求

  发送数据前要求数据总线静止时间即无数据发送时间大于(5ms 波特率为9600时).

MODBUS功能码简介

功能码“03”:读多路寄存器输入

  例如:
  主机要读取地址为01,起始地址为0001的3个从机寄存器数据。



从机数据寄存器的地址和数据

寄存器地址 寄存器数据(16进制) 对应参数
0010 0BB8 (30.00V) U
0011 01F4 (5.00A) I
0012 3A98 (150.00W) P

主机发送的报文格式

主机发送 字节数 发送的信息 备 注
从机地址 1 01 发送至地址为01的从机
功能码 1 03 读寄存器
起始地址 2 0010 起始地址为0001
读数据长度 2 0003 读取3个寄存器(共6个字节)
CRC码 2 040E 由主机计算得到CRC码



从机响应返回的报文格式

从机响应 字节数 返回的信息 备 注
从机地址 1 01 来自从机01
功能码 1 03 读寄存器
数据长度(字节数) 1 06 共6个字节
寄存器1的数据 2 0BB8 地址为0001寄存器的内容
寄存器2的数据 2 01F4 地址为0002寄存器的内容
寄存器3的数据 2 3A98 地址为0003寄存器的内容
CRC码 2 D311 由从机计算得到CRC码

功能码“06”:写单个寄存器

  主机利用这个功能码把数据保存到电源内部数据存储器中去。Modbus通讯规约中的寄存器指的是16位(即2字节),并且高位在前。这样存储器都是二个字节。


  例如:
  主机要把0E10保存到地址为0004的从机寄存器中去(从机地址码为01)。



主机发送的报文格式

主机发送 字节数 发送的信息 备 注
从机地址 1 01 发送至地址为01的从机
功能码 1 06 写单个寄存器
起始地址 2 0004 要写入的寄存器的地址
保存数据 2 0E10 待写入0004地址的数据
CRC码 2 CDA7 由主机计算得到CRC码


从机响应返回的报文格式

从机响应 字节数 返回的信息 备 注
从机地址 1 01 发送至地址为01的从机
功能码 1 06 写单个寄存器
起始地址 2 0004 要写入的寄存器的地址
保存数据 2 0E10 0004地址的数据
CRC码 2 CDA7 由从机计算得到CRC码

错误校验码(CRC校验)

  主机或从机可用校验码进行判别接收信息是否正确。由于电子噪声或一些其它干扰,信息在传输过程中有时会发生错误,错误校验码(CRC)可以检验主机或从机在通讯数据传送过程中的信息是否有误,错误的数据可以放弃(无论是发送还是接收),这样增加了系统的安全和效率。
  MODBUS通讯协议的CRC(冗余循环码)包含2个字节,即16位二进制数。CRC码由发送设备(主机)计算,放置于发送信息帧的尾部。接收信息的设备(从机)再重新计算接收到信息的CRC,比较计算得到的CRC是否与接收到的相符,如果两者不相符,则表明出错。

  • CRC码的计算方法
  1. 预置1个16位的寄存器为十六进制FFFF(即全为1);称此寄存器为CRC寄存器;
  2. 把第一个8位二进制数据(既通讯信息帧的第一个字节)与16位的CRC寄存器的低8位相异或,把结果放于CRC寄存器;
  3. 把CRC寄存器的内容右移一位(朝低位)用0填补最高位,并检查右移后的移出位;
  4. 如果移出位为0:重复第3步(再次右移一位); 如果移出位为1:CRC寄存器与多项式A001(1010 0000 0000 0001)进行异或;
  5. 重复步骤3和4,直到右移8次,这样整个8位数据全部进行了处理;
  6. 重复步骤2到步骤5,进行通讯信息帧下一个字节的处理;
  7. 将该通讯信息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低字节进行交换;
  8. 最后得到的CRC寄存器内容即为:CRC码。

使用python控制

依赖

modbus_tk
pyserial

python脚本

import time
import serial
import serial.tools.list_ports
import modbus_tk.defines as cst
from modbus_tk import modbus_rtu


def connect_serial(keyword: str = "", baud_rate: int = None, timeout: int = 1):
    """
    连接串口
    :param keyword: 串口名关键词
    :param baud_rate: 波特率
    :param timeout: 超时时间
    :return: 串口类
    """
    serial_list = list(serial.tools.list_ports.comports())
    serial_list_len = len(serial_list)
    if serial_list_len <= 0:
        raise ValueError("Can't find a serial port")
    else:
        if not keyword:
            print("找到如下串口:")
            for serial_port in serial_list:
                print("\t", str(serial_port))
            print("请输入要连接的串口关键词:")
            keyword = input()
        if not baud_rate:
            print("请输入使用的波特率:")
            baud_rate = input()
            try:
                baud_rate = int(baud_rate)
            except:
                baud_rate = 9600
        for _ in range(serial_list_len):
            if keyword.lower() in str(serial_list[_]).lower():
                serial_port = serial.Serial(serial_list[_].name, baud_rate, timeout=timeout)
                print("与", serial_list[_], "建立连接!")
                return serial_port
        raise ValueError("Can't find the serial port")


class power_supply:
    """
    电源类
    """
    def __init__(self, serial_obj: serial.serialwin32.Serial, addr: int):
        """
        构造函数
        :param serial_obj: 串口类
        :param addr: 从机地址
        """
        self.modbus_rtu_obj = modbus_rtu.RtuMaster(serial_obj)
        self.modbus_rtu_obj.set_timeout(1.0)
        self.addr = addr
        self.name = self.read(0x0003)
        self.class_name = self.read(0x0004)

        dot_msg = self.read(0x0005)
        self.W_dot = 10 ** (dot_msg & 0x0F)
        dot_msg >>= 4
        self.A_dot = 10 ** (dot_msg & 0x0F)
        dot_msg >>= 4
        self.V_dot = 10 ** (dot_msg & 0x0F)

        protection_state_int = self.read(0x0002)
        self.isOVP = protection_state_int & 0x01
        self.isOCP = (protection_state_int & 0x02) >> 1
        self.isOPP = (protection_state_int & 0x04) >> 2
        self.isOTP = (protection_state_int & 0x08) >> 3
        self.isSCP = (protection_state_int & 0x10) >> 4

        self.V(0)

    def read(self, reg_addr: int, reg_len: int = 1):
        """
        读取寄存器
        :param reg_addr: 寄存器地址
        :param reg_len: 寄存器个数,1~2
        :return: 数据
        """
        if reg_len <= 1:
            return self.modbus_rtu_obj.execute(self.addr, cst.READ_HOLDING_REGISTERS, reg_addr, reg_len)[0]
        elif reg_len >= 2:
            raw_tuple = self.modbus_rtu_obj.execute(self.addr, cst.READ_HOLDING_REGISTERS, reg_addr, reg_len)
            return raw_tuple[0] << 16 | raw_tuple[1]

    def write(self, reg_addr: int, data: int, data_len: int = 1):
        """
        写入数据
        :param reg_addr: 寄存器地址
        :param data: 待写入的数据
        :param data_len: 数据长度
        :return: 写入状态
        """
        if data_len <= 1:
            self.modbus_rtu_obj.execute(self.addr, cst.WRITE_SINGLE_REGISTER, reg_addr, output_value=data)
            if self.read(reg_addr) == data:
                return True
            else:
                return False
        elif data_len >= 2:
            self.modbus_rtu_obj.execute(self.addr, cst.WRITE_SINGLE_REGISTER, reg_addr, output_value=data >> 16)
            self.modbus_rtu_obj.execute(self.addr, cst.WRITE_SINGLE_REGISTER, reg_addr + 1, output_value=data & 0xFFFF)
            if self.read(reg_addr) == (data >> 16) and self.read(reg_addr + 1) == (data & 0xFFFF):
                return True
            else:
                return False

    def read_protection_state(self):
        """
        读取保护状态
        :return: 保护状态寄存器原始值
        """
        protection_state_int = self.read(0x0002)
        self.isOVP = protection_state_int & 0x01
        self.isOCP = (protection_state_int & 0x02) >> 1
        self.isOPP = (protection_state_int & 0x04) >> 2
        self.isOTP = (protection_state_int & 0x08) >> 3
        self.isSCP = (protection_state_int & 0x10) >> 4
        return protection_state_int

    def V(self, V_input: float = None):
        """
        读取表显电压或写入目标电压
        :param V_input: 电压值,单位:伏特
        :return: 表显电压或目标电压
        """
        if V_input is None:
            return self.read(0x0010) / self.V_dot
        else:
            self.write(0x0030, int(V_input * self.V_dot + 0.5))
            return self.read(0x0030) / self.V_dot

    def A(self, A_input: float = None):
        """
        读取表显电流或写入限制电流
        :param A_input: 电流值,单位:安
        :return: 表显电流或限制电流
        """
        if A_input is None:
            return self.read(0x0011) / self.A_dot
        else:
            self.write(0x0031, int(A_input * self.A_dot + 0.5))
            return self.read(0x0031) / self.A_dot

    def W(self):
        """
        读取表显功率
        :return: 表显功率,单位:瓦
        """
        return self.read(0x0012, 2) / self.W_dot

    def OVP(self, OVP_input: float = None):
        """
        读取或写入过压保护设定值
        :param OVP_input:过压保护设定值
        :return:过压保护设定值
        """
        if OVP_input is None:
            return self.read(0x0020) / self.V_dot
        else:
            self.write(0x0020, int(OVP_input * self.V_dot + 0.5))
            return self.read(0x0020) / self.V_dot

    def OCP(self, OAP_input: float = None):
        """
        读取或写入过流保护设定值
        :param OAP_input:过流保护设定值
        :return:过流保护设定值
        """
        if OAP_input is None:
            return self.read(0x0021) / self.A_dot
        else:
            self.write(0x0021, int(OAP_input * self.A_dot + 0.5))
            return self.read(0x0021) / self.A_dot

    def OPP(self, OPP_input: float = None):
        """
        读取或写入过功率保护设定值
        :param OPP_input:过功率保护设定值
        :return:过功率保护设定值
        """
        if OPP_input is None:
            return self.read(0x0022, 2) / self.W_dot
        else:
            self.write(0x0022, int(OPP_input * self.W_dot + 0.5), 2)
            return self.read(0x0022, 2) / self.W_dot

    def Addr(self, addr_input: int = None):
        """
        读取或改变从机地址
        :param addr_input: 要设成的从机地址, 1~250
        :return: 从机地址
        """
        if addr_input is None:
            self.addr = self.read(0x9999)
            return self.addr
        else:
            self.write(0x9999, addr_input)
            self.addr = addr_input
            return self.read(0x9999)

    def set_volt(self, V_input, error_range: int = 0.05, timeout: int = 60):
        """
        设置目标电压,等待转换完成并测量响应时间
        :param V_input: 目标电压,单位:伏特
        :param error_range: 容许的误差大小
        :param timeout: 超时时间
        :return:
        """
        old_volt = self.V()
        self.V(V_input)
        start_time = time.time()
        while abs(self.V() - V_input) > error_range:
            if (time.time() - start_time) > timeout:
                raise ValueError("Set volt timeout")
        print("从", old_volt, "V跳至", self.V(), "V, 用时", time.time() - start_time, "秒")

    def operative_mode(self, mode_input: int = None):
        """
        读取或写入工作状态
        :param mode_input: 工作状态,1: 开启输出; 0: 关闭输出
        :return: 当前工作状态
        """
        if mode_input is None:
            return self.read(0x0001)
        else:
            if mode_input:
                self.write(0x0001, 1)
            else:
                self.write(0x0001, 0)
            return self.read(0x0001)


if __name__ == '__main__':
    eTM_3020C = power_supply(connect_serial("CH340", 9600), 1)
    eTM_3020C.operative_mode(1)
    eTM_3020C.set_volt(0)
    eTM_3020C.set_volt(12)
    eTM_3020C.set_volt(0)
    eTM_3020C.set_volt(16)
    eTM_3020C.set_volt(0)
    eTM_3020C.set_volt(32)
    print("当前表显电压:", eTM_3020C.V())
    print("当前表显电流:", eTM_3020C.A())
    print("当前表显功率:", eTM_3020C.W())
    eTM_3020C.set_volt(0)
    eTM_3020C.operative_mode(0)

运行效果

  可见,该电源升压较快,降压较慢:

D:\Anaconda3\envs\eTM-3020C\python.exe D:/Work/Python/eTM-3020C/main.py

与 COM3 - USB-SERIAL CH340 (COM3) 建立连接!
从 0.0 V跳至 0.0 V, 用时 0.0859994888305664 秒
从 0.0 V跳至 11.97 V, 用时 1.754119634628296 秒
从 11.97 V跳至 0.05 V, 用时 6.720765113830566 秒
从 0.05 V跳至 15.96 V, 用时 1.924328327178955 秒
从 15.96 V跳至 0.05 V, 用时 7.704945802688599 秒
从 0.05 V跳至 31.96 V, 用时 1.5403895378112793 秒
当前表显电压: 31.96
当前表显电流: 0.0
当前表显功率: 0.0
从 31.96 V跳至 0.05 V, 用时 10.615259647369385 秒

进程已结束,退出代码0