搜索
bottom↓
回复: 44

开源PLC学习笔记15(MODBUS 与组态软件的通讯)——2013_12_03

  [复制链接]
(234718559)

出0入0汤圆

发表于 2013-12-3 19:24:47 | 显示全部楼层 |阅读模式
上节理解了一个modbus通讯的过程,主要目的只是入门,有个基本了解,因为CRC校验和如何串口收发还会在后面重点理解。

沿着上节的思路,本节用同样的方法分析一个和mcgs组态软件通讯的例子,选择它的原因是它是找到的现成中比较好的,有PROTEUS仿真,非常方便,比和触摸屏通讯方便。
http://www.wlcpu.com/archives/1570#respond


动画有点乱,但基本上能看出,通过组态软件可以开关单片机上的LED,本程序也可以下载到开发板上演示。

这节将会理解上节所没有的功能命令,同时理解mcgs的modbus通讯协议,举一反三就可以移植到和其它组态软件的通讯。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
(234713648)

出0入0汤圆

发表于 2013-12-3 20:46:38 | 显示全部楼层
谢谢,正在学习!
(234712384)

出0入0汤圆

发表于 2013-12-3 21:07:42 | 显示全部楼层
您好,请问,您这个动画图,用什么软件录制的
(234706458)

出0入0汤圆

发表于 2013-12-3 22:46:28 来自手机 | 显示全部楼层
楼主加油,一直关注你的帖子
(234700026)

出0入0汤圆

发表于 2013-12-4 00:33:40 | 显示全部楼层
(234659250)

出0入0汤圆

 楼主| 发表于 2013-12-4 11:53:16 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-4 12:05 编辑

感觉这个程序组织得比上一个例子好,另外串口接收和中断做得也好(和开源PLC的做法有得一比)。为了简洁,去除了和温度相关的功能,基本上不用什么功夫,可以得到如下的文件组织。


主函数非常简洁,容易调试。

首先
    SYSTEM_DISABLE_INTERRUPT();
     TimerInit();
    UartInit();
    SYSTEM_ENABLE_INTERRUPT();


第一次看到这种写法,
定时器和串口初始化时禁止中断,

找到定义,
#define SYSTEM_DISABLE_INTERRUPT()     EA=0
#define SYSTEM_ENABLE_INTERRUPT()      EA=1

禁止中断和开启中断函数都是用宏定义的,对我这样的初学者来说,感觉这样写法高级些。论坛上就有一个帖子专门讲宏定义的,http://www.amobbs.com/thread-4931200-1-1.html。浏览一下,不怎么适合入门。

再回到程序当中,
    while(1) {      
        timerProc();
                 
        checkComm0Modbus();//检测modbus帧
    }


timerProc是定时器0的处理,这个定时器的用法和开源PLC中定时器0的用法一致,都是用来判断串口接收是否结束,从这里也可以看出,程序是接收完一组数据后才开始处理的。

然后就是检测modbus帧,类似与开源PLC中的FX1NProcessing函数,通过对检验报文格式,作出不同的回应。

*************************************************************

要理解主函数会收到什么,作出什么反映之前,先把目光仿真uart和timer0上,看看他俩是如何一起完成报文的接收的。

先看定时器中断函数
//定时器0 1ms 中断
void timer0IntProc() interrupt 1
{
    TL0 = TIMER_LOW;
    TH0 = TIMER_HIGHT;

    bt1ms = 1;
}


定时器0中断的主要目的是设置中断标志位 bt1ms=1,可以想到在某个函数里用到bt1ms来计时,
//定时处理
void timerProc(void)
{
    if(bt1ms)
    {
        bt1ms = 0;

        if(receTimeOut>0)
        {
            receTimeOut--;
            if(receTimeOut==0 && receCount>0)   //判断通讯接收是否超时
            {
                receCount = 0;      //将接收地址偏移寄存器清零
            }
        }
    }
}

因为该函数相当简单(已去掉了不相干的内容)

该函数的核心是判断报文是否已接收完毕,并把接收数组坐标清零。(和开源PLC不同的是,这里没有设置 UDRFlag = 1 这样的标志位)

*******************************************
这里的串口中断函数让我印象深刻,
void commIntProc(void) interrupt 4
{
    if (!RI) return;

    RI = 0;
    receTimeOut = 20;
    receBuf[receCount] = SBUF;
    receCount ++;
    receCount &= 0x0f;        // 最多只接收16个字节数据
}


只处理串口接收中断,
设置接收间断,用来判断是否接收完成;
把接收的字符存入数组,
当receCount=15时,再++时变为0。


不过,感觉uart和timer0写得这么简单,那么重压是不是到了 checkComm0Modbus();//检测modbus帧 身上?
下一步就是理解本节的核心 检测modbus帧。




本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
(234656125)

出0入0汤圆

 楼主| 发表于 2013-12-4 12:45:21 | 显示全部楼层
oldbeginner 发表于 2013-12-4 11:53
感觉这个程序组织得比上一个例子好,另外串口接收和中断做得也好(和开源PLC的做法有得一比)。为了简洁, ...

理解了uart和timer0的作用后,就可以集中精力开始理解modbus协议是如何实现的

//检查uart0数据
void checkComm0Modbus(void)
{
        UINT16 crcData;
        UINT16 tempData;

        if (receCount > 4) {
                switch (receBuf[1]) {
                        case 1://读取线圈状态(读取点 16位以内)
                        case 3://读取保持寄存器(一个或多个)
                        case 5://强制单个线圈
                        case 6://设置单个寄存器
                                if (receCount >= 8) {
                                        处理函数1()
                                }
                                break;
                        case 15://设置多个线圈
                                处理函数2();
                                break;
                        case 16://设置多个寄存器
                               处理函数3();
                                break;
                        default:
                                break;
                }
        }               
}




其实,在处理函数2和3中还会对报文长度再次判断。
*****************************************

处理函数1
                    UART_DISABLE_INTERRUPT();

                    if (receBuf[0] == localAddr) {
                        ModbusDelay (10);

                        crcData = crc16(receBuf, 6);
                        if (crcData == receBuf[7] + (receBuf[6] << 8)) {
                            //校验正确
                            if (receBuf[1] == 1) {
                                //读取线圈状态(读取点 16位以内)
                               readCoil();
                            }
                           
                            else if (receBuf[1] == 3) {
                                //读取保持寄存器(一个或多个)
                               readRegisters();
                            }
                            else if (receBuf[1] == 5) {
                                //强制单个线圈
                                forceSingleCoil();
                            }
                            else if (receBuf[1] == 6) {
                                //写单个寄存器
                                presetSingleRegister();
                            }
                        }
                    }

                    RI = 0;
                    receCount = 0;

                    UART_ENABLE_INTERRUPT();

在处理函数1中,首先停止串口中断,然后

判断地址和CRC校验是否正确,然后

判断功能码,并分别调用相对应的函数。

最后,复位RI和receCount并打开串口中断。

逻辑结构很清晰。

************************************


处理函数2  //设置多个线圈
                tempData = receBuf[6];
                tempData += 9;  //数据个数
                if (receCount >= tempData) {
                    if (receBuf[0] == localAddr) {
                        crcData = crc16(receBuf, tempData - 2);
                        if (crcData == (receBuf[tempData-2] << 8) + receBuf[tempData-1]) {
                            //forceMultipleCoils();
                        }
                    }
                    receCount = 0;
                }
设置多个线圈,原程序中没有对应的函数,这里当作
设置单个线圈的扩展即可,精力会放在后面的设置单个线圈上。

***************************************


处理函数3 //设置多个寄存器
                tempData = (receBuf[4] << 8) + receBuf[5];
                tempData = tempData * 2;    //数据个数
                tempData += 9;

                if (receCount >= tempData) {

                    UART_DISABLE_INTERRUPT();

                    if (receBuf[0] == localAddr) {
                        crcData = crc16(receBuf, tempData - 2);
                        if (crcData == (receBuf[tempData-2] << 8) + receBuf[tempData-1]) {
                            presetMultipleRegisters();
                        }
                    }
                    RI = 0;
                    receCount = 0;

                    UART_ENABLE_INTERRUPT();
                }
理解成设置单个寄存器的扩展,先把注意力放在单个寄存器设置上。

*********************************************



本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
(234652612)

出0入0汤圆

 楼主| 发表于 2013-12-4 13:43:54 | 显示全部楼层
oldbeginner 发表于 2013-12-4 12:45
理解了uart和timer0的作用后,就可以集中精力开始理解modbus协议是如何实现的

//检查uart0数据

在理解读寄存器或写寄存器之前,还需要先理解一下串口发送函数

//开始发送
void beginSend(void)
{
        UartSendBytes (sendBuf, sendCount);
}


调用了一个函数,
void UartSendBytes (UCHAR *buf,UINT nLen)
{
        UINT i;

        for (i = 0;i < nLen;i ++)
                UartSendByte (buf);
}

再调,
void UartSendByte (UCHAR ucByte)
{
    SBUF = ucByte;
    while (!TI);
    TI = 0;
}


考虑到串口中断函数里的第一句,if (!RI) return;
串口发送基本与中断无关。

***************************************

下一步只理解和读线圈相关的函数和寄存器地址设置。




(234645803)

出0入0汤圆

 楼主| 发表于 2013-12-4 15:37:23 | 显示全部楼层
oldbeginner 发表于 2013-12-4 13:43
在理解读寄存器或写寄存器之前,还需要先理解一下串口发送函数

//开始发送

在理解寄存器地址上遇到了障碍,很多都是0x 1x 2x等等一笔带过。没有找到合适的资料入门,边研究代码(只RTU格式)边理解了。
先分析读线圈,


意义如下:

<1>设备地址:在一个485总线上可以挂接多个设备,此处的设备地址表示想和哪一个设备通讯。例子中为想和1号(十进制的17是十六进制的01)通讯。

<2>命令号01:读取数字量的命令号固定为01。

<3>起始地址高8位、低8位:表示想读取的开关量的起始地址(起始地址为0)。比如例子中的起始地址为0。

<4>寄存器数高8位、低8位:表示从起始地址开始读多少个开关量。例子中为3个开关量。

<5>CRC校验:是从开头一直校验到此之前。在此协议的最后再作介绍。此处需要注意,CRC校验在命令中的高低字节的顺序和其他的相反。

然后,设备响应(通过checkComm0Modbus函数)


意义如下:

<1>设备地址和命令号和上面的相同。

<2>返回的字节个数:表示数据的字节个数,也就是数据1,2...n中的n的值。

<3>数据1...n:由于每一个数据是一个8位的数,所以每一个数据表示8个开关量的值,每一位为0表示对应的开关断开,为1表示闭合。比如例子中,表示0号(索引号为0)LED闭合,1号闭合,2闭合;如果询问的开关量不是8的整倍数,那么最后一个字节的高位部分无意义,置为0。返回值为7,即0000 0111,表示3个LED都闭合。

<4>CRC校验同上。

******************************************************


//读线圈状态
void readCoil(void)
{
                读取地址判断();
               
                读取位数判断();

                把位数转为字节数();

                读取线圈状态();

                生成报文并发送();
}

把这个函数分成5个功能块,如何开始理解,比较难的部分是 读取线圈状态(),这里有2个循环,并且又调用了一个函数。

读取地址判断();
       addr = receBuf[3];
        tempAddr = addr;

这里的地址只取了低8位,高8位被忽略。这是因为本程序没有高8位的需求而简化,
并且有注释:
//字地址 0 - 255 (只取低8位)
//位地址 0 - 255 (只取低8位)

读取位数判断();
    //读取的位个数
    bitCount = receBuf[5];

同上。

把位数转为字节数();
    byteCount = bitCount / 8;                   //字节个数
    if (bitCount % 8 != 0)
        byteCount++;
这里用一个例子好理解,比如bitCount=9,则byteCount=9/8=1,然后
因为 9%8 !=0 成立,byteCount++,变成了2。
就是如果要读9个开关量,则用2个字节。

读取线圈状态();
    for (k = 0; k < byteCount; k++) {
        //字节位置
        position = k + 3;
        sendBuf[position] = 0;

        for (i = 0; i < 8; i++) {
            getCoilVal(tempAddr, &tempData);

            sendBuf[position] |= (tempData << i);
            tempAddr++;
            if (tempAddr >= addr + bitCount) {
                //读完
                exit = 1;
                break;
            }
        }
      
if (exit == 1)
            break;}

这里有两个循环,首先
for(字节循环)
    for(位循环)
        getCoilVal(tempAddr, &tempData);
可以看出调用的getCoilVal是一位一位的读取状态,然后移位完成一个字节。

position=k+3;是因为响应数据是从第4位开始的(索引是3)。

if (tempAddr >= addr + bitCount)
是用来判断是否要读取的位数都读好了,读好后设定标志位。

*********************************************
再来看getCoilVal,做了更改,用LED表示线圈。
//取线圈状态 返回0表示成功
UINT16 getCoilVal(UINT16 addr, UINT16 *tempData)
{
    UINT16 result = 0;
    UINT16 tempAddr;

    tempAddr = addr & 0xfff;
    //只取低8位地址
    switch( tempAddr )        //只取低8位地址 & 0xff
    {
        case 0:
                if (LED0)
                    *tempData = 1;
                else
                    *tempData = 0;
                break;
        case 1:
                if (LED1)
                    *tempData = 1;
                else
                    *tempData = 0;
                break;

        case 2:
                if (LED2)
                    *tempData = 1;
                else
                    *tempData = 0;
                break;

        case 3:
                if (LED3)
                    *tempData = 1;
                else
                    *tempData = 0;
                break;

        case 4:
                if (LED4)
                    *tempData = 1;
                else
                    *tempData = 0;
                break;

        case 5:
                if (LED5)
                    *tempData = 1;
                else
                    *tempData = 0;
                break;

        case 6:
                if (LED6)
                    *tempData = 1;
                else
                    *tempData = 0;
                break;

        case 7:
                if (LED7)
                    *tempData = 1;
                else
                    *tempData = 0;
                break;
              default:
                break;
    }
    return result;
}

其中
sbit LED0 = P1^0; // 对应线圈0
sbit LED1 = P1^1; // 对应线圈1
sbit LED2 = P1^2; // 对应线圈2
sbit LED3 = P1^3; // 对应线圈3
sbit LED4 = P1^4; // 对应线圈4
sbit LED5 = P1^5; // 对应线圈5
sbit LED6 = P1^6; // 对应线圈6
sbit LED7 = P1^7; // 对应线圈7

这样就可以返回线圈状态了。

*************************************

再返回readCoil函数,看他的最后一个功能
生成报文并发送();
    sendBuf[0] = localAddr;
    sendBuf[1] = 0x01;
    sendBuf[2] = byteCount;
    byteCount += 3;
    crcData = crc16(sendBuf, byteCount);
    sendBuf[byteCount] = crcData >> 8;
    byteCount++;
    sendBuf[byteCount] = crcData & 0xff;
    sendCount = byteCount + 1;

    beginSend();
赋地址,赋命令值,赋返回字节数,
CRC校验,
发送。

***********************************

感觉还是有难度的,虽然只理解了读线圈,但应该可以同样方法理解其它几个功能命令。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
(234627254)

出0入0汤圆

 楼主| 发表于 2013-12-4 20:46:32 | 显示全部楼层
oldbeginner 发表于 2013-12-4 15:37
在理解寄存器地址上遇到了障碍,很多都是0x 1x 2x等等一笔带过。没有找到合适的资料入门,边研究代码(只 ...

在继续其它函数之前,先来实践一下,这次使用组态王,只作两个LED等的控制。

可以看到设置线圈是采用的05命令,另外组态王读线圈命令是01 FF FF 00 01 FD EE(自动发送),网上找了一下,不知道原因。

不知道这方面的资料为何这么少?
不管怎样,下一步先看一下05功能,怎样写线圈?最后再把组态王的modbus命令和寄存器整理一下。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
(234626824)

出0入0汤圆

 楼主| 发表于 2013-12-4 20:53:42 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-5 09:46 编辑

05功能,首先看一下modbus对05命令的定义,

注意05命令一条只能下置一个开关量的状态。
设备响应:如果成功把计算机发送的命令原样返回,否则不响应。


再看具体程序
//强制单个线圈
void forceSingleCoil(void)
{
                读取地址判断();
               
                判断闭合还是断开();

                写线圈状态();

                生成报文并发送();
}

逻辑结构也是很清晰的,

读取地址判断();
        //addr = (receBuf[2]<<8) + receBuf[3];
        //tempAddr = addr & 0xfff;
        addr = receBuf[3];
        tempAddr = addr;

这里的地址只取了低8位,高8位被忽略。这是因为本程序没有高8位的需求而简化,改为注释的前两句既是高低地址都有效时的写法。
并且有注释:
//字地址 0 - 255 (只取低8位)
//位地址 0 - 255 (只取低8位)

判断闭合还是断开();
        onOff = receBuf[4];

        if (onOff == 0xff) {
                //设为ON
                tempData = 1;
        }
        else if (onOff == 0x00) {
                //设为OFF
                tempData = 0;
        }
看上图定义,[4][5]只能是[FF][00]表示闭合[00][00]表示断开,其他数值非法。

写线圈状态();
        setCoilVal(tempAddr, tempData);
调用了一个写线圈状态函数。

生成报文并发送()
        for (i = 0; i < receCount; i++) {
                sendBuf = receBuf;
        }
        sendCount = receCount;

        beginSend();
很简单,因为是原文发送。

*********************************************

//设定线圈状态 返回0表示成功
UINT16 setCoilVal(UINT16 addr, UINT16 tempData)
{
        UINT16 result = 0;
        UINT16 tempAddr;

        tempAddr = addr & 0xfff;

        switch(tempAddr)                // & 0xff
        {
                case 0:
                                if (tempData)
                                        LED0 = 1;
                                else
                                        LED0 = 0;
                                break;
                case 1:
                                if (tempData)
                                        LED1 = 1;
                                else
                                        LED1 = 0;
                                break;
                case 2:
                                if (tempData)
                                        LED2 = 1;
                                else
                                        LED2 = 0;
                                break;
                case 3:
                                if (tempData)
                                        LED3 = 1;
                                else
                                        LED3 = 0;
                                break;
                case 4:
                                if (tempData)
                                        LED4 = 1;
                                else
                                        LED4 = 0;
                                break;
                case 5:
                                if (tempData)
                                        LED5 = 1;
                                else
                                        LED5 = 0;
                                break;
                case 6:
                                if (tempData)
                                        LED6 = 1;
                                else
                                        LED6 = 0;
                                break;
                case 7:
                                if (tempData)
                                        LED7 = 1;
                                else
                                        LED7 = 0;
                                break;
                default:
                                break;
        }
        return result;
}
函数简单易懂,和读线圈类似。

这样,01和05命令基本上就可以理解了。

线圈操作就是位操作,所以会涉及到位运算的一些概念。

初学者的一个优势是,什么东西都是新鲜的(一个副作用就是掉坑里的次数会比较多),包括位运算,这里学习一下。
http://hi.baidu.com/tomspirit/item/590743fd045bcf7e3c198b57
位运算应用口诀
清零取反要用与,某位置一可用或
若要取反和交换,轻轻松松用异或


功能                                                 | 示例                                 | 位运算
----------------------                        +---------------------------        +--------------------
去 掉最后一位                                 | (101101->10110)                 | x >> 1
在最后加一个0                                 | (101101->1011010)         | x << 1
在最后加一个1                                 | (101101->1011011)         | x << 1+1
把最后一位变成1                         | (101100->101101)                 | x | 1
把最后一位变成0                         | (101101->101100)                 | x | 1-1
最后一位取反                                 | (101101->101100)                 | x ^ 1
把右数第k位变成1                         | (101001->101101,k=3)         | x | (1 << (k-1))
把右数第k位变成0                         | (101101->101001,k=3)         | x & ~ (1 << (k-1))
右数第k位取反                                | (101001->101101,k=3)         | x ^ (1 << (k-1))
取末三位                                         | (1101101->101)                 | x & 7
取末k位                                         | (1101101->1101,k=5)         | x & ((1 << k)-1)

取 右数第k位                                 | (1101101->1,k=4)                 | x >> (k-1) & 1

把 末k位变成1                                 | (101001->101111,k=4)         | x | (1 << k-1)
末 k位取反                                 | (101001->100110,k=4)         | x ^ (1 << k-1)
把 右边连续的1变成0                         | (100101111->100100000) | x & (x+1)
把右起第一个0变成 1                         | (100101111->100111111) | x | (x+1)
把右边连续的0变成1                         | (11011000->11011111)         | x | (x-1)
取右边连续的1                                 | (100101111->1111)         | (x ^ (x+1)) >> 1
去掉右起第一个1的左边                 | (100101000->1000)         | x & (x ^ (x-1))

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
(234624182)

出0入0汤圆

发表于 2013-12-4 21:37:44 | 显示全部楼层
楼主的笔记也一直在看,给力,加油!
(234572699)

出0入0汤圆

 楼主| 发表于 2013-12-5 11:55:47 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-5 12:04 编辑
oldbeginner 发表于 2013-12-4 20:53
05功能,首先看一下modbus对05命令的定义,

注意05命令一条只能下置一个开关量的状态。


继续模拟量寄存器的读写功能,
首先,读模拟量寄存器03,定义如下


<3>起始地址高8位、低8位:表示想读取的模拟量的起始地址(起始地址为0)。比如例子中的起始地址为0。 <4>寄存器数高8位、低8位:表示从起始地址开始读多少个模拟量。例子中为3个模拟量。注意,在返回的信息中一个模拟量需要返回两个字节。

相应从机收到后,作出响应


//读寄存器
void readRegisters(void)
{
                读取地址判断();
               
                读取个数判断();

                把个数转为字节数();

                读取寄存器状态();

                生成报文并发送();
}
和读线圈是一个模子里出来的,只是改成了读寄存器。

读取地址判断();
        //addr = (receBuf[2]<<8) + receBuf[3];
        //tempAddr = addr & 0xfff;
        addr = receBuf[3];
        tempAddr = addr;
这里的地址只取了低8位,高8位被忽略。这是因为本程序没有高8位的需求而简化,
并且有注释:
//字地址 0 - 255 (只取低8位)
//位地址 0 - 255 (只取低8位)

读取个数判断();
        readCount = receBuf[5];

把个数转为字节数();
        byteCount = readCount * 2;
每个寄存器的数据占两个字节。

读取寄存器状态();
    for (i = 0; i < byteCount; i += 2, tempAddr++) {
        getRegisterVal(tempAddr, &tempData);
        sendBuf[i+3] = tempData >> 8;
        sendBuf[i+4] = tempData & 0xff;
    }
调用了getRegisterVal函数来读取。

生成报文并发送();
    sendBuf[0] = localAddr;
    sendBuf[1] = 3;
    sendBuf[2] = byteCount;
    byteCount += 3;
    crcData = crc16(sendBuf, byteCount);
    sendBuf[byteCount] = crcData >> 8;
    byteCount++;
    sendBuf[byteCount] = crcData & 0xff;

    sendCount = byteCount + 1;

    beginSend();
已经很熟悉了。

*************************************

//取寄存器值 返回0表示成功
UINT16 getRegisterVal(UINT16 addr, UINT16 *tempData)
{
    UINT16 result = 0;
    UINT16 tempAddr;

    tempAddr = addr & 0xfff;

    switch(tempAddr)     //& 0xff
    {
        case 0:
            *tempData = testRegister0;
            break;
        case 1:
            *tempData = testRegister1;
            break;
        case 2:
            *tempData = testRegister2;
            break;
        case 3:     
            *tempData = testRegister3;   
            break;
        case 4:
            *tempData = testRegister4;
            break;   
        case 5:
            *tempData = testRegister5;
            break;
        case 6:
            *tempData = testRegister6;
            break;
        case 7:
            *tempData = testRegister7;
            break;
        case 8:
            *tempData = testRegister8;
            break;
        case 9:
            *tempData = testRegister9;
            break;
   
        default:
            break;
    }
    return result;
}

其中寄存器定义如下
UINT16 testRegister0,    // 测试寄存器
            testRegister1,
            testRegister2,
            testRegister3,
            testRegister4,//存放当前温度
            testRegister5,//读写寄存器  控制P1口输出
            testRegister6,
            testRegister7,
            testRegister8,
            testRegister9;

再回到MCGS仿真的例子里,
看看都有哪些寄存器

并没有线圈寄存器,事实上,线圈寄存器是利用test5的位来实现的。



我的学习目的是寄存器,那么要看温度,

温度寄存器名字 wendu,地址(只看后面)00 04
然后调慢更新周期,
在PROTEUS上得到,

有些不太理解,01 06 00 04xxxx 把温度写到PROTEUS上?先继续,下一步学习功能06。

01 03 00 03 00 02可以理解,读出地址00 03和00 04上寄存器数据,这样wendu这个变量得到了赋值。



本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
(234569790)

出0入0汤圆

 楼主| 发表于 2013-12-5 12:44:16 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-5 12:59 编辑
oldbeginner 发表于 2013-12-5 11:55
继续模拟量寄存器的读写功能,
首先,读模拟量寄存器03,定义如下

注意此命令一条只能下置一个模拟量的状态。
设备响应:如果成功把计算机发送的命令原样返回,否则不响应



void presetSingleRegister(void)                //设置单个寄存器
{
                读取地址判断();
               
                得到数据();

                写寄存器();

                生成报文并发送();
}


读取地址判断();
        //addr = (receBuf[2]<<8) + receBuf[3];
        //tempAddr = addr & 0xfff;
        addr = receBuf[3];
        tempAddr = addr;                 //& 0xff
同理,只针对低8位,

得到数据();
        tempData = ( receBuf[4]<<8 ) + receBuf[5];

写寄存器();

        setRegisterVal(tempAddr,tempData);
调用了一个函数。

生成报文并发送();
        sendBuf[0] = localAddr;
        sendBuf[1] = 6;
        sendBuf[2] = addr >> 8;
        sendBuf[3] = addr & 0xff;
        sendBuf[4] = receBuf[4];
        sendBuf[5] = receBuf[5] ;

        setCount = 6;                //共6个字节
        crcData = crc16(sendBuf,6);
        sendBuf[6] = crcData >> 8;
        sendBuf[7] = crcData & 0xff;

        sendCount = 8;
        beginSend();

相当于复习了好几遍这种格式,重复是初学者好朋友。
当然,重复不是对所有人都有效,例如

升仙了!

又升仙了!

干嘛说个“又”呢?

娘子,和牛魔王出来看上帝……


*****************************************


//设置寄存器值 返回0表示成功
UINT16 setRegisterVal(UINT16 addr, UINT16 tempData)
{
        UINT16 result = 0;
        UINT16 tempAddr;

        tempAddr = addr & 0xfff;

        switch(tempAddr)         //& 0xff
        {
                case 0:
                        testRegister0 = tempData;
                        break;
                case 1:
                        testRegister1 = tempData;
                        break;
                case 2:
                        testRegister2 = tempData;
                        break;
                case 3:
                        testRegister3 = tempData;
                        break;
                case 4:
                        testRegister4 = tempData;
                            P1=tempData ; //&& 0X00FF
                        break;
                case 5:
                        testRegister5 = tempData;
                        break;
                case 6:
                        testRegister6 = tempData;
                        break;
                case 7:
                        testRegister7 = tempData;
                        break;
                case 8:
                        testRegister8 = tempData;
                        break;
                case 9:
                        testRegister9 = tempData;
                        break;
                default:
                        break;
        }
        return result;
}
又升仙了!

******************************************

因为我们的目的是和组态软件通讯,学到的命令不在多,关键在用,所以功能命令暂时理解到此,下一步就放在组态软件的MODBUS通讯上。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
(234568231)

出0入0汤圆

发表于 2013-12-5 13:10:15 | 显示全部楼层
听课。

(234562886)

出0入0汤圆

 楼主| 发表于 2013-12-5 14:39:20 | 显示全部楼层
oldbeginner 发表于 2013-12-5 12:44
注意此命令一条只能下置一个模拟量的状态。
设备响应:如果成功把计算机发送的命令原样返回,否则不响应
...

可以入门的,组态软件modbus通讯的例子还是有些少。

看了功能命令函数后,基本清楚对组态软件来说,寄存器命名是1xxxx 或 4xxxx,但是单片机都只认xxxx,如何区别1或4,单片机利用了功能命令,通过switch case来区别。
因为对4xxxx寄存器来说,是不能用01或05命令的,同样对1xxxx寄存器来说,也不能用03 06命令的。

组态王MODBUS设置步骤,http://www.chinabaike.com/z/gyzd/877961.html
寄存器                读写属性
数字量读
1XXXX                只读

数字量写
0XXXX                只写

模拟量读
3XXXX                只读

模拟量写
4XXXX                只写

实例不好找,等找到后再更新。

MCGS MODBUS设置步骤,http://www.chinabaike.com/z/gyzd/877993.html
在“通道类型”下拉窗口中有4种类型可以选择,分别对应不同模块类型:
1输入继电器:对应模块的数字量输入。
0输出继电器:对应模块的数字量输出
3输入寄存器:对应模块的模拟量输入。
4输出寄存器:对应模块的模拟量输出
(234559514)

出0入0汤圆

发表于 2013-12-5 15:35:32 | 显示全部楼层
支持楼主.....关注楼主......
(234326948)

出0入0汤圆

发表于 2013-12-8 08:11:38 | 显示全部楼层
您好,请问,您这个动画图,用什么软件录制的
(234316633)

出0入0汤圆

 楼主| 发表于 2013-12-8 11:03:33 | 显示全部楼层
PCH 发表于 2013-12-8 08:11
您好,请问,您这个动画图,用什么软件录制的

camsatia ...
(234126532)

出0入0汤圆

 楼主| 发表于 2013-12-10 15:51:54 | 显示全部楼层
oldbeginner 发表于 2013-12-5 14:39
可以入门的,组态软件modbus通讯的例子还是有些少。

看了功能命令函数后,基本清楚对组态软件来说,寄存 ...

理解freemodbus后,虽然只是刚学习单片机几个月,但感觉freemodbus滥用状态机(比如最关键状态是完整的帧收到了,然后再开始处理数据并反馈主机,顺序执行好好的,非要假设主机在从机发送的时候又送来命令;这种处理应该放在主机上,而不是从机的职责),把整体结构搞得复杂难懂,明明不是操作系统,非得往上靠。

从机就应该把重点放在从机的职责上(接收命令执行,然后反馈),而不是老想着主机发命令超过我的处理能力怎么办,这种想一劳永逸的从机不仅让自己的结构复杂了好多倍,导致犯错的机会也增加好多倍,本来想节约时间的通用程序反而成了麻烦制造机。

笔记15的代码是实战中非常好的实例,可以根据需要进行修改,结构清晰易懂。

这里复习一遍。
#include "Project.h"

void main(void)
{
        SYSTEM_DISABLE_INTERRUPT();
        TimerInit();
        UartInit();


        SYSTEM_ENABLE_INTERRUPT();
  
        while(1) {      
                timerProc();
                        
                checkComm0Modbus();//检测modbus帧

        }
}

首先,是初始化,定时器初始化和串口初始化,

***********************************
1、定时器初始化
void TimerInit(void)
{
        TMOD = 0x51;
        TH0 = TIMER_HIGHT;
        TL0 = TIMER_LOW;
        TR0 = 1;
            ET0 = 1;
        TH1 = 0;                        //9600
            TL1 = 0;
        TR1 = 0;                        //定时器1用于计数定时器2用于波特
        ET1 = 1;
        //PT1 = 1;

        IT0 = 1;
            IT1 = 1;
        EX0 = 0;
        PX0 = 1;
            EX1 = 0;
}
这里有两个定时器,定时器1用来确定串口波特率,定时器0用来判断字符串是否接收完毕。


*************************************
2、串口初始化
void UartInit ()
{
        IE=0x90;
        TMOD = (TMOD & 0X0F) | 0X20;        //串口工作在方式1
        TH1 = -22118400L/12/32/9600;    //求当波特率是9600时定时器的初值
        TL1 = -22118400L/12/32/9600;
        TR1 = 1;
        SCON = 0X50;                    //01010000;
        PCON |= 0X80;                   //波特率加倍
}
波特率加倍了,所以22118400= 11059200*2

*******************************************
3、定时处理,用来判断字符串是否接收完毕。
//定时处理
void timerProc(void)
{
        if(bt1ms)
        {
                bt1ms = 0;

        if(receTimeOut>0)
        {
            receTimeOut--;
            if(receTimeOut==0 && receCount>0)   //判断通讯接收是否超时
            {
                receCount = 0;      //将接收地址偏移寄存器清零
            }
        }
        }
}
timerProc是在两个中断下的判断
receCount是接收数组的下标,当receTimeOut减到0时,表示相邻字符接收间隔过长(20毫秒,在9600波特率下,modbus只需4ms即可),可以认为上一个字符串已经接收完毕。

//定时器0 1ms 中断
void timer0IntProc() interrupt 1
{
        TL0 = TIMER_LOW;
            TH0 = TIMER_HIGHT;

        bt1ms = 1;
}
定时器每1ms中断一次,并让receCount减一。

void commIntProc(void) interrupt 4
{
        if (!RI) return;

        RI = 0;

        receTimeOut = 20;
        receBuf[receCount] = SBUF;
        receCount ++;
        receCount &= 0x0f;        // 最多只接收16个字节数据
}
每次接收一个字符,将receTimeOut重新设置为20。

**********************************************
4、根据收到的指令,执行相应功能
//检查uart0数据
void checkComm0Modbus(void)
{
        UINT16 crcData;
        UINT16 tempData;

        if (receCount > 4) {
                switch (receBuf[1]) {
                        case 1://读取线圈状态(读取点 16位以内)
                        case 3://读取保持寄存器(一个或多个)
                        case 5://强制单个线圈
                        case 6://设置单个寄存器
                                if (receCount >= 8) {
                                        处理函数1();
                                }
                                break;
                        case 15://设置多个线圈
                                处理函数2();
                                break;
                        case 16://设置多个寄存器
                               处理函数3();
                                break;
                        default:
                                break;
                }
        }               
}

没有利用函数散转,简洁利索。
处理函数1
                    UART_DISABLE_INTERRUPT();

                    if (receBuf[0] == localAddr) {
                        ModbusDelay (10);

                        crcData = crc16(receBuf, 6);
                        if (crcData == receBuf[7] + (receBuf[6] << 8)) {
                            //校验正确
                            if (receBuf[1] == 1) {
                                //读取线圈状态(读取点 16位以内)
                               readCoil();
                            }
                           
                            else if (receBuf[1] == 3) {
                                //读取保持寄存器(一个或多个)
                               readRegisters();
                            }
                            else if (receBuf[1] == 5) {
                                //强制单个线圈
                                forceSingleCoil();
                            }
                            else if (receBuf[1] == 6) {
                                //写单个寄存器
                                presetSingleRegister();
                            }
                        }
                    }

                    RI = 0;
                    receCount = 0;

                    UART_ENABLE_INTERRUPT();

在处理函数1中,首先停止串口中断,然后

判断地址和CRC校验是否正确,然后

判断功能码,并分别调用相对应的函数。

最后,复位RI和receCount并打开串口中断。

逻辑结构很清晰。



处理函数2  //设置多个线圈
                tempData = receBuf[6];
                tempData += 9;  //数据个数
                if (receCount >= tempData) {
                    if (receBuf[0] == localAddr) {
                        crcData = crc16(receBuf, tempData - 2);
                        if (crcData == (receBuf[tempData-2] << 8) + receBuf[tempData-1]) {
                            //forceMultipleCoils();
                        }
                    }
                    receCount = 0;
                }
设置多个线圈,原程序中没有对应的函数,这里当作
设置单个线圈的扩展即可,精力会放在后面的设置单个线圈上。



处理函数3 //设置多个寄存器
                tempData = (receBuf[4] << 8) + receBuf[5];
                tempData = tempData * 2;    //数据个数
                tempData += 9;

                if (receCount >= tempData) {

                    UART_DISABLE_INTERRUPT();

                    if (receBuf[0] == localAddr) {
                        crcData = crc16(receBuf, tempData - 2);
                        if (crcData == (receBuf[tempData-2] << 8) + receBuf[tempData-1]) {
                            presetMultipleRegisters();
                        }
                    }
                    RI = 0;
                    receCount = 0;

                    UART_ENABLE_INTERRUPT();
                }


*********************************************
5、
//读线圈状态
void readCoil(void)
{
                读取地址判断();
               
                读取位数判断();

                把位数转为字节数();

                读取线圈状态();

                生成报文并发送();
}


//强制单个线圈
void forceSingleCoil(void)
{
                读取地址判断();
               
                判断闭合还是断开();

                写线圈状态();

                生成报文并发送();
}

//读寄存器
void readRegisters(void)
{
                读取地址判断();
               
                读取个数判断();

                把个数转为字节数();

                读取寄存器状态();

                生成报文并发送();
}

void presetSingleRegister(void)                //设置单个寄存器
{
                读取地址判断();
               
                得到数据();

                写寄存器();

                生成报文并发送();
}

*******************************************
6、发送报文
//开始发送
void beginSend(void)
{
        UartSendBytes (sendBuf, sendCount);
}

调用了一个函数,
void UartSendBytes (UCHAR *buf,UINT nLen)
{
        UINT i;

        for (i = 0;i < nLen;i ++)
                UartSendByte (buf);
}

再调,
void UartSendByte (UCHAR ucByte)
{
    SBUF = ucByte;
    while (!TI);
    TI = 0;
}

考虑到串口中断函数里的第一句,if (!RI) return;
串口发送基本与中断无关。

****************************************

笔记15中代码结构明晰,逻辑简单,即使在使用中有不足之处也能很容易发现。从入门角度和简单应用上,比freemodbus好太多了。


(232544682)

出0入0汤圆

发表于 2013-12-28 23:16:04 | 显示全部楼层
楼主是个细心人。谢谢楼主的分享。
本来觉得很头大的问题,看了楼主的分析后,豁然开朗,受益匪浅!
(230932298)

出0入0汤圆

发表于 2014-1-16 15:09:08 | 显示全部楼层
楼主的帖子很受用,向楼主学习
(230778763)

出0入0汤圆

发表于 2014-1-18 09:48:03 | 显示全部楼层
正在学习正,已掌握03命令代码的编写
(230646130)

出0入0汤圆

发表于 2014-1-19 22:38:36 | 显示全部楼层
源码在你的博客上得到了,非常的感谢。花了两天的时间,从头到尾仔细看了两遍这个笔记,写得非常详细,对我帮助非常的大。现在仍然有些疑问,估计看了源码后这个疑问会消失80%。
(228787149)

出0入0汤圆

发表于 2014-2-10 11:01:37 | 显示全部楼层
楼主真是一个好人啊!根据你的例程理解 MCGS和51的通信!期待你的更新!楼主能给个QQ或者联系方式吗!
(226197450)

出0入0汤圆

发表于 2014-3-12 10:23:16 | 显示全部楼层
little4_su 发表于 2014-1-19 22:38
源码在你的博客上得到了,非常的感谢。花了两天的时间,从头到尾仔细看了两遍这个笔记,写得非常详细,对我 ...

我也需要他的源代码,能给我吗?在什么博客上面?
(226174553)

出0入0汤圆

发表于 2014-3-12 16:44:53 | 显示全部楼层
oldbeginner 发表于 2013-12-10 15:51
理解freemodbus后,虽然只是刚学习单片机几个月,但感觉freemodbus滥用状态机(比如最关键状态是完整的帧 ...

没看懂3.5个字符时间在哪体现?是那个20ms超时吗,为什么要是20ms 假如波特率是1200 超时时间又是多少呢
(226161465)

出0入0汤圆

发表于 2014-3-12 20:23:01 | 显示全部楼层
oldbeginner 发表于 2013-12-4 12:45
理解了uart和timer0的作用后,就可以集中精力开始理解modbus协议是如何实现的

//检查uart0数据

处理函数1中 ModbusDelay (10);延时程序干嘛用的呢?
(225682972)

出0入0汤圆

发表于 2014-3-18 09:17:54 | 显示全部楼层
linbo411 发表于 2014-3-12 10:23
我也需要他的源代码,能给我吗?在什么博客上面?

他的源码是51的,上位机用的是组态王,被我看了个透。理解后我移植到了M16上,在WinAVR2010编译软件下全部编译通过,用虚拟串口软件ConfigureVirtualSerialDrive、ISIS7professional模拟硬件、ModBus调试工具Commix联调正常读写。在硬件测试通过,硬件测试部分的测试所用的上位机分别是Modbus调试精灵、力控组态软件、威纶通触屏TK6070ik。
(225682638)

出0入0汤圆

发表于 2014-3-18 09:23:28 | 显示全部楼层
little4_su 发表于 2014-3-18 09:17
他的源码是51的,上位机用的是组态王,被我看了个透。理解后我移植到了M16上,在WinAVR2010编译软件下全 ...

我也找到了,但是感觉他的程序有问题
(225674950)

出0入0汤圆

发表于 2014-3-18 11:31:36 | 显示全部楼层
linbo411 发表于 2014-3-18 09:23
我也找到了,但是感觉他的程序有问题

他的程序没问题啊!
(225667085)

出0入0汤圆

发表于 2014-3-18 13:42:41 | 显示全部楼层
little4_su 发表于 2014-3-18 11:31
他的程序没问题啊!

从机收到8个字节就解析了,他的,因该超时以后解析
(225368309)

出0入0汤圆

发表于 2014-3-22 00:42:17 | 显示全部楼层
linbo411 发表于 2014-3-18 13:42
从机收到8个字节就解析了,他的,因该超时以后解析

他这个不是收到8个字节就解析,而是如果收1-byte数据超过1ms就算是通讯超时。为什么选1ms?而不是2或4或10ms,这个你在前文有过这样类似疑问。我帮你解答一下,9600pbs的波特率下,传送一位需时1/9600ms,那么传送1-byte的数据就是8/9600s=0.8ms,这就是为什么他是1ms的原因。3.5个字符的时间就是3.5x0.8=3ms左右!
其实他这样处理是有弊端,会造成通讯不顺畅。原因1:定时器一直在计时,使低概率事件变为高概率,原因2:ModBus通讯是一串数据的收发,他把他分割看待(就是收1-byte超时处理方式),此为不妥。
他应该做这样的处理,在void commIntproc(void interrupt 4)放入启动定时器的代码,在void timerproc(void)函数里放入停止定时器的代码,更为合理些。
但是,我觉得做以下这样的处理更好,在串口接收中断函数中装入定时器的初值(也就是超时所需的时间,4ms吧)且启动之。在定时器中断函数中有一接收完成标志位,4ms后计时达到,进入定时中断,这时停止计时器,并且置位接收完成中断标志位。主程序里再判断标志位状态,在判断之前先关全局中断,若接收完成标志位=1,则清接收完成标志位、清零接收数据个数计数器,并且调用ModBus解析函数。整个过程简述为,串口有数据进入,就计时,默认4ms接收完成,完成后就应该处理接收到的那串数据。我也是这么做的!

另外,威纶触屏TK6070i(软件EB8000)和显控触屏SK102AE(软件SKWorkshop)对01功能码有些奇葩的处理。假如下位机位的起始是0x34,也就是说0x34前面不再有别的地址,而你只读一个线圈,是读不到的,它发下去的数据是01 01 00 30 00 10 xx xx,并非我们理解的01 01 00 34 00 01 xx xx。若你读0x35线圈,发下去的数据也是01 01 00 30 00 10 xx xx,也不是我们理解的01 01 00 35 00 01 xx xx。当然这是题外话,有机会你可以感受一下!
(225327326)

出0入0汤圆

发表于 2014-3-22 12:05:20 | 显示全部楼层
to 楼上,HMI的超时提醒,很烦躁的,时间太短,问了下 威纶的FAE ,好像也不可调。
(225113709)

出0入0汤圆

发表于 2014-3-24 23:25:37 | 显示全部楼层
因该超时以后解析
(224966984)

出0入0汤圆

发表于 2014-3-26 16:11:02 | 显示全部楼层
oldbeginner 发表于 2013-12-5 11:55
继续模拟量寄存器的读写功能,
首先,读模拟量寄存器03,定义如下

01 06 00 04   这个04应该指的是test5的值哦。
(215303230)

出0入0汤圆

发表于 2014-7-16 12:33:36 | 显示全部楼层
本帖最后由 luckyzpy 于 2014-7-16 12:36 编辑

不错,能解释这么清楚花时间了!敬佩,程序中是有些问题,但从机不多,模拟应该是没有问题的!
(209496626)

出0入0汤圆

发表于 2014-9-21 17:30:20 | 显示全部楼层
oldbeginner 发表于 2013-12-4 11:53
感觉这个程序组织得比上一个例子好,另外串口接收和中断做得也好(和开源PLC的做法有得一比)。为了简洁, ...

写的真好,好好学习一下,先收藏
(205181390)

出0入0汤圆

发表于 2014-11-10 16:10:56 | 显示全部楼层
你好,请问这个测试程序可以发我一份吗?
(180479778)

出0入0汤圆

发表于 2015-8-23 13:44:28 | 显示全部楼层
好东西先收了
(102982156)

出0入0汤圆

发表于 2018-2-5 12:51:30 | 显示全部楼层
谢谢分享,先收藏
(80663532)

出0入0汤圆

发表于 2018-10-21 20:28:34 | 显示全部楼层
hao现在正在做这个,有用,谢谢!
(27475005)

出0入0汤圆

发表于 2020-6-28 11:04:01 | 显示全部楼层
楼主做的  是我在论坛中看到做的最好的 关于modbus通信
(1717787)

出0入0汤圆

发表于 2021-4-22 13:50:59 | 显示全部楼层
确实做的很细致
(1712309)

出0入0汤圆

发表于 2021-4-22 15:22:17 | 显示全部楼层

好东西先收了
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子论坛 ( 公安交互式论坛备案:44190002001997 粤ICP备09047143号 )

GMT+8, 2021-5-12 11:00

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表