欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

步科触摸屏HMI通过MODBUS RTU与ESP32通讯

程序员文章站 2022-07-14 10:01:16
...

步科触摸屏HMI通过MODBUS RTU与ESP32通讯

经历了多天的折腾,在万念俱灰之时灵光一现,搞定了步科HMI与ESP32的通讯。两者连接通讯,主要是为了工业设备物联。话说踩了好多坑,不清楚MODBUS RTU是啥东东,学习了一堆的资料,不懂TX的还是要看看的,了解了后发现其实还是挺简单的,就是串口通讯,只是约定格式相互发送串口讯息,具体内容自行百度。

话不多说,进入正文。

步科HMI的配置

先到步科网站下载组态软件,连接 https://www.kinco.cn/download/software/all,下载 Kinco DTools组态软件并安装。

打开软件后,菜单 文件->新建工程
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
工程名称和路径,自行定义,HMI型号,我手头上正好有一个GL070E,就选了这个,其它同学就选相应的型号吧。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
在下一步后,串口0要给PLC,那我就用串口2,具体如何物理接线可以查看 菜单->帮助->通讯连接说明。在这里我用的是直接在线模拟,可以不接线,等在模拟通过了没问题再连线也不迟。

通信参数按我上图红框所示设置。然后点击完成。

接下来关键的来了…
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
在元件库窗口打开PLC,可以右击选择小图标,方便找。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
找到Modbus RTU Extend并拖到主窗口。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
再把通讯连接,把串口 拖到主窗口,并连接com2和modbus,连接成功的标志就是拖动HMI或PLC图标,串口线会跟着动。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
在工程结构窗口,打开HMI->HMI0->窗口->0:Frame0,编辑窗口内容。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
拖放一个 位状态设定 到主窗口。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
下面的很关键,地址类型选0X,对应modbus里的线圈coil,也就是是/否两个值了,地址填1,PLC是从1开始的,对应的是modbus里的0。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
在位状态设定里,类型选择 切换形状。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
在标签里,勾选 使用标签,把0,1和标签内容都改成 开关
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
再拉个 位状态指示灯,地址类型0x,地址1。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
拉个 数值元件,地址类型选 4X,很重要,这个类型才能传数值型的数据。地址填1,启用输入功能。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
再拉一个数值元件,启用输入功能的勾去掉,类型 4X,地址 2。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
设置完后界面如下:
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
然后菜单->工具->全部编译。

ttgo t-display esp32的显示配置

手头上正好有ttgo t-display esp32的板子,如图
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
某宝上多的很也便宜,还带tft屏。不过所有的esp32都是可以连通的,不一定要这个板子。

这个板子的显示屏刚开始不懂也不是很好搞,只有github的代码,几乎没教程。我理一下踩过的坑。

源码在 https://github.com/Xinyuan-LilyGO/TTGO-T-Display
TFT_eSPI的驱动在 https://github.com/Bodmer/TFT_eSPI

先安装驱动,有2种法,

第一种,直接在github下载后,放到 C:\Users\zheng\Documents\Arduino\libraries 里。

第二种,arduino ide里,菜单->工具->管理库,搜索 TFT_eSPI,直接安装即可,我是用了这个,感觉方便点。
坑来了
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
ttgo的github上的这段话,我一直没看明白什么意思,在技术群里弱弱的问了一下,只收到一堆的风凉话,没一个能解决问题的。还是自力更生吧。

下载后打开文件夹,可以看到有TTGO-T-Display.h这个文件。
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
这个库能支持好多驱动类型的屏,但不同的屏的针脚配置不一样,那这个文件就是针对该屏的配置文件。

把这个文件复制到TFT_espi库的安装目录下的User_Setup里,

如图
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
然后回到上级目录,找到User_Setup_Select.h这个头文件,简单的不能再简单的教程上只写了

Add #include <User_Setups/TTGO_T_Display.h> to TFT_eSPI/User_Setup_Select.h

然后我就把这头文件包含进去,然后打开测试文件 TTGO-T-Display.ino,怎么显示都不正常,颜色是反的,显示的位置也不对,搜遍整个网络都没有解决方法。

经过几天的琢磨研究找到了解决方法。大大坑啊,这教程也太简单了,填坑如下:

坑1:User_Setup_Select.h里,需要把#include <User_Setup.h>这句给注释了,否则屏幕的分辨率会不正确。TTGO_T_Display.h这个文件就是根据自己的屏从User_Setup.h修改而来的,正确的分辨率是135*240,两个文件同时放着会被干扰而导致显示位置不正确。

坑2:颜色反了,搞得我以为是屏坏了,都想退货了,但想到刚开始到手的时候显示是对了,想想应该是哪里配置的问题。然后就一顿狂搜啊,才搞明白,TFT原来就是有反色这个配置,TNND,在哪配置?打开了TTGO_T_Display.h这个文件,才发现,TFT_INVERSION_ON 这个宏定义,果断去掉注释,颜色就显示正常了。

esp32的modbus rtu slave从机通讯

modbus rtu通讯协议是不复杂,但要自己写个程序去实现还是很耗时的,那就找找现成的库,arduino有现成的arduinomodbus库,试了试,在arduino nano里可以编译通过,在esp32里过不了,大概的问题出在串口定义上,懒得去解决,费脑。继续找,好东西找到了,是针对esp32和esp8266的modbus库,连接奉上
https://github.com/emelianov/modbus-esp8266
下载后放到库文件夹里如:C:\Users\xxx\Documents\Arduino\libraries。
接口函数在API.md里,例子自带一个在C:\Users\xxx\Documents\Arduino\libraries\modbus-esp8266\examples\RTU-slave里,源码如下:

/*
  ModbusRTU ESP8266/ESP32
  Simple slave example
  
  (c)2019 Alexander Emelianov (aaa@qq.com)
  https://github.com/emelianov/modbus-esp8266
*/

#include <ModbusRTU.h>

#define REGN 10
#define SLAVE_ID 1

ModbusRTU mb;

void setup() {
  Serial.begin(9600, SERIAL_8N1)
  mb.begin(&Serial);
  mb.slave(SLAVE_ID);
  mb.addHreg(REGN);
  mb.Hreg(REGN, 100);
}

void loop() {
  mb.task();
  yield();
}

太精悍了,连个说明都没有。又得继续发扬艰苦奋头的研究精神。研究结果如下:

modbus里的地址是从0开始的,上面有讲plc是从1开始的,也就是说modbus里的地址0对应的plc里的地址1。

modbus rtu slave要用的的接口我具体一下
增加寄存器接口,用之前先增加,相当于初始化一个地址。

bool addHreg(uint16_t offset, uint16_t value = 0, uint16_t numregs = 1);//加保持寄存器(holding register) 功能号 03 地址类型 4x,用于和步科的HMI通讯
bool addCoil(uint16_t offset, bool value = false, uint16_t numregs = 1);//加线圈状态(Coil Status) 功能号 01地址类型 0x,用于和步科的HMI通讯
bool addIsts(uint16_t offset, bool value = false, uint16_t numregs = 1);//加输入寄存器(Input Register)功能号 04 地址类型 3x
bool addIreg(uint16_t offset, uint16_t value = 0, uint16_t nemregs = 1);//加输入寄存器(Input Register)功能号 04 地址类型 3x

写寄存器,对应着4个类型的写

bool Hreg(uint16_t offset, uint16_t value);
bool Coil(uint16_t offset, bool value);
bool Ists(uint16_t offset, bool value);
bool Ireg(uint16_t offset, uint16_t value);

读寄存器,对应着4个类型

uint16_t Hreg(uint16_t offset);
bool Coil(uint16_t offset);
bool Ists(uint16_t offset);
uint16_t Ireg(uint16_t offset);

删除寄存器

bool removeHreg(uint16_t offset, uint16_t numregs = 1);
bool removeCoil(uint16_t offset, uint16_t numregs = 1);
bool removeIsts(uint16_t offset, uint16_t numregs = 1);
bool removeIreg(uint16_t offset, uint16_t numregs = 1);
void task();//在loop()里调用
bool begin(HardwareSerial* port, int16_t txPin=-1);//初始化串口
void master();//声明主机
void slave(uint8_t slaveId);//声明从机

主机和从机不能同时声明,这里只用到从机。

回调接口

//当收到相应类型的写事件
bool onSetCoil(uint16_t address, cbModbus cb = nullptr, uint16_t numregs = 1);
bool onSetHreg(uint16_t address, cbModbus cb = nullptr, uint16_t numregs = 1);
bool onSetIsts(uint16_t address, cbModbus cb = nullptr, uint16_t numregs = 1);
bool onSetIreg(uint16_t address, cbModbus cb = nullptr, uint16_t numregs = 1);

//当收相应类型的读事件
bool onGetCoil(uint16_t address, cbModbus cb = nullptr, uint16_t numregs = 1);
bool onGetHreg(uint16_t address, cbModbus cb = nullptr, uint16_t numregs = 1);
bool onGetIsts(uint16_t address, cbModbus cb = nullptr, uint16_t numregs = 1);
bool onGetIreg(uint16_t address, cbModbus cb = nullptr, uint16_t numregs = 1);

写了那么多接口,源码如下:

#include <TFT_eSPI.h>
#include <SPI.h>
#include <ModbusRTU.h>

TFT_eSPI tft = TFT_eSPI(); // 调用自定义库

#define HREGN 0x00 //holding register 0号地址
#define HREGN1 0x01//holding register 1号地址
#define CREGN 0x00//coil status 0号地址
#define IREGN 0x00//input register 0号地址
#define SREGN 0x00//input status 0号地址
#define SLAVE_ID 1 //1号从站

ModbusRTU mb;

void tft_init(){
  tft.init();
  //tft.setRotation(0);//屏幕方向
  tft.fillScreen(TFT_BLACK);//清除屏幕成指定颜色
  tft.setTextDatum(TL_DATUM);//文本位置参考基准 top left
}

int curLine = 0;
int maxLine = 24;
int lineHeight = 10;
int debugMode = 0;

//用于屏幕一行行输出信息
void debug(String msg){
  if(!debugMode){
    debugMode = 1;
    tft.fillScreen(TFT_BLACK);
  }
  tft.setTextColor(TFT_WHITE);
  tft.setTextSize(1);//文本大小  

  tft.drawString(msg, 0, curLine*lineHeight);
  curLine++;
  if(curLine>=maxLine){
    curLine = 0;
    tft.fillScreen(TFT_BLACK);
  }
}

//holding register 0号地址的写数据的事件回调函数
uint16_t lastSVal;
uint16_t HVal1;
uint16_t cbHregSet(TRegister* reg, uint16_t val) {
  if(lastSVal != val){
    lastSVal = val;
    debug(String("HregSet val:")+String(val));
  }
  //HVal1 = val;
  mb.Hreg(HREGN1, val);//往holding register 1号地址写数据
  return val;
}

//holding register 0号地址读数据的事件回调函数
uint16_t lastGVal;
uint16_t cbHregGet(TRegister* reg, uint16_t val) {
  if(lastGVal!=val){
    lastGVal = val;
    debug(String("HregGet val:")+String(val));
  }  
  return val;
}
//holding register 1 号地址的读数据事件回调函数
uint16_t cbHregGet1(TRegister* reg, uint16_t val) {
  //把0号地址赋于的值返回。
  return val;
}

//coil 0号写事件
uint16_t cbCoilSet(TRegister* reg, uint16_t val) {
  debug(String("CoilSet val:")+String(val));
  return val;
}

//coil0号读事件
uint16_t cbCoilGet(TRegister* reg, uint16_t val) {
  debug(String("CoilGet val:")+String(val));
  return val;
}

//input register 写事件
uint16_t cbIregSet(TRegister* reg, uint16_t val) {
  debug(String("IregSet val:")+String(val));
  return val;
}

//input register 读事件
uint16_t cbIregGet(TRegister* reg, uint16_t val) {
  debug(String("IregGet val:")+String(val));
  return val;
}

//input status 写事件
uint16_t cbIstsSet(TRegister* reg, uint16_t val) {
  debug(String("IstsSet val:")+String(val));
  return val;
}

//input status 读事件
uint16_t cbIstsGet(TRegister* reg, uint16_t val) {
  debug(String("IstsGet val:")+String(val));
  return val;
}

void setup() {
  tft_init();

  debug("Modbus RTU Slave");
  Serial.begin(9600, SERIAL_8N1);//定义串口通讯
  
  mb.begin(&Serial);
  mb.slave(SLAVE_ID);
  mb.addHreg(HREGN);//注册保持寄存器
  mb.onSetHreg(HREGN, cbHregSet);
  mb.onGetHreg(HREGN, cbHregGet);
  mb.addHreg(HREGN1);
  mb.onGetHreg(HREGN1, cbHregGet1);
  
  mb.addCoil(CREGN);//注册线圈寄存器
  mb.onSetCoil(CREGN, cbCoilSet);
  //mb.onGetCoil(CREGN, cbCoilGet);

  //mb.addIreg(IREGN);//注册输入寄存器
  //mb.onSetCoil(IREGN, cbIregSet);
  //mb.onGetCoil(IREGN, cbIregGet);

  mb.addIsts(SREGN);//注册输入状态寄存器
  mb.onSetIsts(SREGN, cbIstsSet);
  //mb.onGetIsts(SREGN, cbIstsGet);
}

void loop() {
  // put your main code here, to run repeatedly:
  mb.task();
  delay(10);
}

愉快的下载到esp32里吧,开发板选择 ESP32 Dev Module,端口选择一下,我正好是COM4。

HMI与ESP32通讯测试

在步科组态软件Kinco DTools打开菜单->工具->直接在线模拟
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
HMI端的COM2对应的PC端口,选择ESP32与电脑相连的端口,我这里是COM4。然后点击模拟。

下面看看实测效果:

开关类型的步科触摸屏HMI通过MODBUS RTU与ESP32通讯
不断点击开关,单片机就会收到Coil的值,关是0,开是65280,说明开关或是状态类型的传输已经通了。

数值类型的
步科触摸屏HMI通过MODBUS RTU与ESP32通讯
在点击1所示的输入框,调出数字数窗口,输入888后点击enter,esp32的hreg 0即收到数据888,然后把888赋给hreg 1,HMI在询问hreg 1的值时,即把888返回,在HMI右边的框就看到了888。说明holding register的通讯也通了。

完工。