搜索
bottom↓
回复: 9

开源PLC学习笔记18(MODBUS的其它实现VC ARDUINO等)——2013_12_12

[复制链接]

出0入0汤圆

发表于 2013-12-12 16:08:26 | 显示全部楼层 |阅读模式
本帖最后由 oldbeginner 于 2013-12-12 21:18 编辑

在modbus这里确实费了很多时间和精力,值不值得要到后面才知道。因为PLC学习还有很多内容和难点在后面,这节笔记可以作为一个节点,以后这方面的补充可以加在本节后面。

首先,是了解一下modbus上位机,VC如何发送命令给单片机?
搜一下,找到资源,
http://www.codefans.net/soft/23457.shtml


界面很简单,符合入门要求。



来往的数据信息截图


这里有个理解,读线圈和写线圈不是针对同一个线圈,读线圈是针对LED的,写线圈是针对开关的。这一点是在下位机编程时更改对象造成的。
//取线圈状态 返回0表示成功
INT16U getCoilVal(INT16U addr, INT16U *tempData)

//设定线圈状态 返回0表示成功
INT16U setCoilVal(INT16U addr, INT16U tempData)。

上位机是用VC写的,VC也是强忍着耐心看了几集孙鑫的讲座,不是不好,是一集的时间太长了。
因为学习PLC,不可避免要涉及到上位机,虽然借用三菱的上位机在商业上是个更好的点子。在学习上,创建一个能实现简单功能的上位机对理解PLC帮助很大。

***********************************************************
之所以涉及Arduino,也是因为开源的程序,
http://code.google.com/p/simple-modbus/
其中有一句话很吸引人,
The libraries are very robust and non blocking, all communication takes place in the background allowing your code to run without any delay in execution (well almost).

而且主从都有,结构很清晰,至少能借鉴一下。
**********************************************************

先从VC开始理解。

本帖子中包含更多资源

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

x

出0入0汤圆

 楼主| 发表于 2013-12-12 17:03:38 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-12 17:07 编辑

这是一个基于对话框的程序,
在对话框初始化函数,BOOL CAAADlg::OnInitDialog(),主要调用了
MODBUS_S_Init(3,CBR_9600,8,NOPARITY,ONESTOPBIT);
用来初始化MODBUS的
                  端口,9600,n,8,1(次序不一样)

/*************MODBUS_SERVER.cpp*****************************/
char MODBUS_S_Init(unsigned long xPort, unsigned long xBabd, unsigned char xDataSize,
                                unsigned char xParity, unsigned char xStopBit)
{
        if(OpenPort(xPort,xBabd,xDataSize,xParity, xStopBit,4096,4096,1000))
        {
                return(1);
        }
        else
        {
                return(0);
        }
}

又调用了OpenPort函数,
///打开串口////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool OpenPort(unsigned long xPort, unsigned long xBabd, unsigned char xDataSize,
                                unsigned char xParity, unsigned char xStopBit, unsigned long InputBuffSize,
                                unsigned long OutputBuffSize, unsigned long dwTimerOut)
{  
        设置事件();

        取得串口字符();

        打开通讯端口();

        清理缓存器();

        通讯成功();
}

感觉,比VB复杂,VB只要使用MSCOMM非常方便。

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


理解一下读命令的流程,

void CAAADlg::OnBRead()
{
                发送读指令并接受返回();
               
                判断是否为数字();

                生成并发送命令();

                文本显示();
}

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

发送读指令并接受返回()

        short int ilist[200];
        bool          blist[200];
        CString rtu,regadd,regcount,strtmp;
        
        memset(ilist,0x00,200);
        memset(blist,0x00,200);

        m_rtu.GetWindowText(rtu);
        m_regadd.GetWindowText(regadd);
        m_regcount.GetWindowText(regcount);

搜一下,memset用法
http://blog.csdn.net/yangsen2016/article/details/1638503
memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法

然后,
        if(rtu.IsEmpty())
        {
                MessageBox("PLC地址不能为空","MODBUS测试",MB_OK);
                return;
        }
        else if(regadd.IsEmpty())
        {
                MessageBox("变量地址不能为空","MODBUS测试",MB_OK);
                return;
        }
        else if(regcount.IsEmpty())
        {
                MessageBox("变量数量不能为空","MODBUS测试",MB_OK);
                return;
        }
常规判断

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

判断是否为数字()

        unsigned short int i_regadd, i_regcount;
        unsigned char i_rtu;

        for(int i=0; i<rtu.GetLength(); i++)
        {
                if(isdigit(rtu[0])==0)
                {
                        MessageBox("PLC地址不能为字符","MODBUS测试",MB_OK);
                        return;
                }
        }
        i_rtu= atoi(rtu);

        for(i=0; i<regadd.GetLength(); i++)
        {
                if(isdigit(regadd[0])==0)
                {
                        MessageBox("变量地址不能为字符","MODBUS测试",MB_OK);
                        return;
                }
        }
        i_regadd= atoi(regadd);
        if(i_regadd > 50000 || i_regadd <= 0)
        {
                        MessageBox("变量地址范围不正确","MODBUS测试",MB_OK);
                        return;
        }

        for(i=0; i<regcount.GetLength(); i++)
        {
                if(isdigit(regcount[0])==0)
                {
                        MessageBox("变量数量不能为字符","MODBUS测试",MB_OK);
                        return;
                }
        }
        i_regcount= atoi(regcount);
        if(i_regcount > 200)
        {
                        MessageBox("变量数量不能大于200","MODBUS测试",MB_OK);
                        return;
        }
常规用法,确保数字有效

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

生成并发送命令()

首先,生成显示的文字
        char state= 0;
        int nlength=m_r_text.GetTextLength();
        m_r_text.SetSel(nlength,  nlength);  
        m_r_text.ReplaceSel("发送数据\r\n");

然后,
        if(i_regadd >= 40001 && i_regadd <= 49999)
        {               
                state= MODBUS_S_ReadMultiRegD(i_rtu,i_regadd,i_regcount,ilist);
        }
        else if(i_regadd >= 10001 && i_regadd <= 19999)
        {               
                state= MODBUS_S_ReadMultiRegM_1x(i_rtu,i_regadd,i_regcount,blist);
        }        
        else if(i_regadd >= 1 && i_regadd <= 9999)
        {
                state= MODBUS_S_ReadMultiRegM(i_rtu,i_regadd,i_regcount,blist);
        }
根据地址选择命令,因为使用了线圈,所以调用MODBUS_S_ReadMultiRegM函数,这个函数放在后面理解。

然后,
        if(state==-1)
        {
                nlength=m_r_text.GetTextLength();
                m_r_text.SetSel(nlength,  nlength);  
                m_r_text.ReplaceSel("接收数据CRC检验失败\r\n");               
        }
        else if(state==-2)
        {
                nlength=m_r_text.GetTextLength();
                m_r_text.SetSel(nlength,  nlength);  
                m_r_text.ReplaceSel("接收数据失败,无应答\r\n");               
        }
        else
        {
                nlength=m_r_text.GetTextLength();
                m_r_text.SetSel(nlength,  nlength);  
                m_r_text.ReplaceSel("接收数据成功.\r\n");               
        }
根据返回值判断命令执行状况,需要配合调用的函数来理解。

最后,
        CString str;
        if(i_regadd >= 40001 && i_regadd <= 49999)
        {
                for(i=0; i<i_regcount; i++)
                {                        
                        strtmp.Format("%d--%d\r\n", i_regadd+i,ilist);                                                        
                     
  str+= strtmp;
                }
        }
        else
        {
                for(i=0; i<i_regcount; i++)
                {                        
                        strtmp.Format("%d--%2x\r\n", i_regadd+i,blist);                                                        
                        str+= strtmp;
                }
        }        
要读的数值放入str中

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

文本显示()
        nlength=m_r_text.GetTextLength();
        m_r_text.SetSel(nlength,  nlength);  
        m_r_text.ReplaceSel(str);
把str显示出

本帖子中包含更多资源

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

x

出0入0汤圆

 楼主| 发表于 2013-12-12 17:27:54 | 显示全部楼层
oldbeginner 发表于 2013-12-12 17:03
这是一个基于对话框的程序,
在对话框初始化函数,BOOL CAAADlg::OnInitDialog(),主要调用了
MODBUS_S_Ini ...

/*读一个或多个开关量  参数:站号,开始地址, 读的数量, 返回数据变量指针  返回:-1表示CRC校验失败,-2表示无应答, 大于零表示接收数据成功*/
/*00000*/
char MODBUS_S_ReadMultiRegM(unsigned char rtu, unsigned short int RegAdd, unsigned short int RegCount, bool *list)
{
        生成报文();

        发送数据();
                判断返回数据();
}

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

生成报文()

        unsigned short int crc16;
        unsigned short int crctmp;
        
        memset(buff, 0x00, 255);
        buff[0]= rtu;
        buff[1]= 0x01;
        buff[2]= (unsigned char)(RegAdd >> 8);
        buff[3]= (unsigned char)RegAdd;
        buff[4]= (unsigned char)(RegCount >> 8);
        buff[5]= (unsigned char)RegCount;
        crc16= CalcCrcFast(buff, 6);
        buff[6]= (unsigned char)(crc16 >> 8);
        buff[7]= (unsigned char)crc16;
复习一下报文格式,


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

发送数据()
    unsigned long strlen;
    if(IsOpen())
    {
        //发送数据
        strlen= WriteChar(8, (char *)buff);
        if(strlen==8)
        {
            //读数据
            memset(buff, 0x00, 255);
            Sleep(50);
            strlen= ReadChar(255, (char *)buff, 1000);
调用了两个函数WriteChar和ReadChar,这两个函数好像天书,暂且不理。
就是利用串口发送并接收了字符。

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

判断返回数据()
            if(strlen==0)
            {
                //无返回
                return(-2);               
            }
            else
            {
                //返回长度有效,解析接收缓冲区
                if(strlen== (3+((RegCount+7)/8)+ 2) && buff[0]== rtu && buff[1]== 0x01)
                {
                    crc16= CalcCrcFast(buff, 3+((RegCount +7)/8));
                    crctmp= buff[strlen-2];
                    crctmp= crctmp << 8 | buff[strlen-1];
                    if(crc16== crctmp )
                    {
                        unsigned char row=0, col=0;                        
                        for(int i=0; i<RegCount; i++)
                        {                        
                            row= i / 8;                           
                            col= i % 8;                           
                            list= buff[3 + row] >> col & 0x01;
                        }                                
                    }
                    else
                    {
                        return(-1);
                    }
                }
设备响应:[设备地址] [命令号01] [返回的字节个数][数据1][数据2]...[数据n][CRC校验的低8位] [CRC校验的高8位]




本帖子中包含更多资源

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

x

出0入0汤圆

 楼主| 发表于 2013-12-12 19:59:36 | 显示全部楼层
oldbeginner 发表于 2013-12-12 17:27
/*读一个或多个开关量  参数:站号,开始地址, 读的数量, 返回数据变量指针  返回:-1表示CRC校验失败 ...


MODBUS_SERVER.H

        /*1.对单个PLC操作*/

    /*读一个或多个模拟量  参数:站号,开始地址, 读的数量, 返回数据变量指针  返回:-1表示CRC校验失败,-2表示无应答, 大于零表示接收数据成功*/
        char MODBUS_S_ReadMultiRegD(unsigned char rtu, unsigned short int RegAdd, unsigned short int RegCount, short int *list);
       
        /*读一个或多个开关量  参数:站号,开始地址, 读的数量, 返回数据变量指针  返回:-1表示CRC校验失败,-2表示无应答, 大于零表示接收数据成功*/
        char MODBUS_S_ReadMultiRegM(unsigned char rtu, unsigned short int RegAdd, unsigned short int RegCount, bool *list);

        /*读一个或多个开关量  参数:站号,开始地址, 读的数量, 返回数据变量指针  返回:-1表示CRC校验失败,-2表示无应答, 大于零表示接收数据成功*/
        char MODBUS_S_ReadMultiRegM_1x(unsigned char rtu, unsigned short int RegAdd, unsigned short int RegCount, bool *list);

        /*写一个模拟量 参数:站号,写地址, 返回数据变量指针  返回:小于零失败,大于零表示成功*/
        char MODBUS_S_WriteSingRegD(unsigned char rtu, unsigned short int RegAdd, short int var);

        /*写一个模拟量 参数:站号,写地址, 返回数据变量指针  返回:小于零失败,大于零表示成功*/
        char MODBUS_S_WriteSingRegM(unsigned char rtu, unsigned short int RegAdd, bool var);

        /*2.对全部PLC操作*/

        char MODBUS_S_A_WriteSingRegD(unsigned short int RegAdd, bool var);
        char MODBUS_S_A_WriteSingRegM(unsigned short int RegAdd, short int var);
       
        /*3.端口操作*/
        char MODBUS_S_Init(unsigned long xPort, unsigned long xBabd, unsigned char xDataSize,
                                unsigned char xParity, unsigned char xStopBit);
        char MODBUS_S_UnInit();
*******************************************************************************************

PORT.H

        inline bool IsOpen();

        bool OpenPort(unsigned long xPort, unsigned long xBabd, unsigned char xDataSize,
                                unsigned char xParity, unsigned char xStopBit, unsigned long InputBuffSize,
                                unsigned long OutputBuffSize, unsigned long dwTimerOut);

        void ClosePort();

        void ClearBuffer();

        unsigned long ReadChar(unsigned long dwBufferLength, char *buff, unsigned long dwWaitTime);

        unsigned long WriteChar(unsigned long dwBufferLength, char *buff);
   
************************************************************************************************

结构比较清晰,虽然串口处理函数有些难度,但如果只作为接口来用还是不错的。

本帖子中包含更多资源

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

x

出0入0汤圆

 楼主| 发表于 2013-12-12 20:58:44 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-12 21:25 编辑
oldbeginner 发表于 2013-12-12 19:59
MODBUS_SERVER.H

        /*1.对单个PLC操作*/


******************************************
Arduino以简单为美,看看是不是?

modbus从机样例

#include <SimpleModbusSlave.h>

#define  LED 9  

enum
{     
  ADC_VAL,     
  PWM_VAL,        
  HOLDING_REGS_SIZE
};

unsigned int holdingRegs[HOLDING_REGS_SIZE];

void setup()
{
  modbus_configure(&Serial, 9600, SERIAL_8N2, 2, 2, HOLDING_REGS_SIZE, holdingRegs);   
  
  pinMode(LED, OUTPUT);
}

void loop()
{
modbus_update();
  
  holdingRegs[ADC_VAL] = analogRead(A0);
  
  analogWrite(LED, holdingRegs[PWM_VAL]>>2);
}

**********************************************
看起来结构很简单。

实现的功能是什么?
This example code will receive the adc ch0 value from the arduino master.
   It will then use this value to adjust the brightness of the led on pin 9.
   The value received from the master will be stored in address 1 in its own
   address space namely holdingRegs[].

翻译如下,从主机接收adc模拟量ch0的值。根据ch0值的大小调节LED灯的亮度,LED灯接在数字口9上。
主机的值会被保存在地址1,名字是holdingRegs[]。

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

那么SimpleModbusSlave.h到底有什么?

#include "Arduino.h"

// function definitions
unsigned int modbus_update();
void modbus_configure(HardwareSerial *SerialPort,
                                                                                        long baud,
                                                                                        unsigned char byteFormat,
                                                                                        unsigned char _slaveID,
                      unsigned char _TxEnablePin,
                      unsigned int _holdingRegsSize,
                      unsigned int* _regs);

这个MODBUS库只有两个对外的函数,分别是update 和 configure。从名字上就可以判断出他们的作用,结合上面的例子可以更加确认。


******************************************************************************
看一下如何实现的,
SimpleModbusSlave.cpp

首先定义变量
#define BUFFER_SIZE 128

// frame[] is used to recieve and transmit packages.
// The maximum serial ring buffer size is 128

unsigned char frame[BUFFER_SIZE];
unsigned int holdingRegsSize; // size of the register array
unsigned int* regs; // user array address
unsigned char broadcastFlag;
unsigned char slaveID;
unsigned char function;
unsigned char TxEnablePin;
unsigned int errorCount;
unsigned int T1_5; // inter character time out
unsigned int T3_5; // frame delay
HardwareSerial* ModbusPort;

变量和以前的没有多大区别,除了2点:
1、多了一个T1_5,1.5个字符的时间,第一次看到

2、HardwareSerial变量,这个变量要查看arduino的资料
http://wiki.geek-workshop.com/do ... :hardwareserial.cpp

class HardwareSerial : public Stream
{
  private:
    ring_buffer *_rx_buffer;
    ring_buffer *_tx_buffer;
    volatile uint8_t *_ubrrh;
    volatile uint8_t *_ubrrl;
    volatile uint8_t *_ucsra;
    volatile uint8_t *_ucsrb;
    volatile uint8_t *_ucsrc;
    volatile uint8_t *_udr;
    uint8_t _rxen;
    uint8_t _txen;
    uint8_t _rxcie;
    uint8_t _udrie;
    uint8_t _u2x;
    bool transmitting;
  public:
    HardwareSerial(ring_buffer *rx_buffer, ring_buffer *tx_buffer,
      volatile uint8_t *ubrrh, volatile uint8_t *ubrrl,
      volatile uint8_t *ucsra, volatile uint8_t *ucsrb,
      volatile uint8_t *ucsrc, volatile uint8_t *udr,
      uint8_t rxen, uint8_t txen, uint8_t rxcie, uint8_t udrie, uint8_t u2x);
    void begin(unsigned long);
    void begin(unsigned long, uint8_t);
    void end();
    virtual int available(void);
    virtual int peek(void);
    virtual int read(void);
    virtual void flush(void);
    virtual size_t write(uint8_t);
    inline size_t write(unsigned long n) { return write((uint8_t)n); }
    inline size_t write(long n) { return write((uint8_t)n); }
    inline size_t write(unsigned int n) { return write((uint8_t)n); }
    inline size_t write(int n) { return write((uint8_t)n); }
    using Print::write; // pull in write(str) and write(buf, size) from Print
    operator bool();
};

HardwareSerial是一个类,这个类含有一个环形缓冲区,用来串口通讯。
因为比较复杂,没有必要全部研究,只关注

available() - To check if something is in the receive buffer 是否接收缓存

read() - To extract the bytes from the buffer 从缓存读取字节

write() - To transmit raw bytes out 发送字节

begin() - To initialize the serial routine 初始化串口任务

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

看看设置函数
void modbus_configure(HardwareSerial *SerialPort,          long baud,
                        unsigned char byteFormat,                        unsigned char _slaveID,
                      unsigned char _TxEnablePin,                 unsigned int _holdingRegsSize,
                      unsigned int* _regs)
{
  ModbusPort = SerialPort;
        (*ModbusPort).begin(baud, byteFormat);
        slaveID = _slaveID;
  holdingRegsSize = _holdingRegsSize;
  regs = _regs;
  TxEnablePin = _TxEnablePin;
  pinMode(TxEnablePin, OUTPUT);
  digitalWrite(TxEnablePin, LOW);
  errorCount = 0;

先是赋值,没有难度
然后,
        if (baud > 19200)
        {
                T1_5 = 750;
                T3_5 = 1750;
        }
        else
        {
                T1_5 = 15000000/baud; // 1T * 1.5 = T1.5
                T3_5 = 35000000/baud; // 1T * 3.5 = T3.5
        }
}   

原文是这样解释的,
        // Modbus states that a baud rate higher than 19200 must use a fixed 750 us
  // for inter character time out and 1.75 ms for a frame delay for baud rates
  // below 19200 the timing is more critical and has to be calculated.
  // E.g. 9600 baud in a 10 bit packet is 960 characters per second
  // In milliseconds this will be 960characters per 1000ms. So for 1 character
  // 1000ms/960characters is 1.04167ms per character and finally modbus states
  // an inter-character must be 1.5T or 1.5 times longer than a character. Thus
  // 1.5T = 1.04167ms * 1.5 = 1.5625ms. A frame delay is 3.5T.
理解一下,
当波特率大于19200时,MODBUS规定必须用750微秒作为内部字符间隔,1.75毫秒作为帧间隔。

波特率小于19200时,计时非常关键,必须计算。

例如9600波特率,一秒传960个字符。

也就是每个字符需要1.04167毫秒。

内部字符间隔必须是1.5T或小于1.5T,因此
1.5T = 1.5 * 1.04167 = 1.5625毫秒。

帧间隔是3.5T。

(有1.5T这一要求吗? 第一次听说,又加了一个麻烦,需要确认一下。)
搜一下,
http://zhidao.baidu.com/link?url ... 0CRiZ22jYrLa9TVVpWa
发送端:发送一帧后延时7*T(其中3.5T是停止时间,3.5T是起始时间)再发送第二帧,保证一帧数据里头各字节不能间的延时不能超过1.5T。

真得有,之前怎么会漏掉呢?

不过有一点,之前的例子虽然没有特意留意1.5T,但是在这方面并没有遇到问题,也许某些特殊的场合会遇到1.5T的问题。

这样MODBUS的配置就可以理解了,然后再理解那个比较复杂的update函数。

本帖子中包含更多资源

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

x

出0入0汤圆

 楼主| 发表于 2013-12-12 22:13:16 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-12 22:14 编辑
oldbeginner 发表于 2013-12-12 20:58
******************************************
Arduino以简单为美,看看是不是?

unsigned int modbus_update()
{
  if ((*ModbusPort).available())
  {
          unsigned char buffer = 0;
          unsigned char overflow = 0;
        
          while ((*ModbusPort).available())
          {
                  if (overflow)
                          (*ModbusPort).read();
                  else
                  {
                          if (buffer == BUFFER_SIZE)
                                  overflow = 1;
                          frame[buffer] = (*ModbusPort).read();
                          buffer++;
                  }
                  delayMicroseconds(T1_5); // inter character time out
          }

先判断串口缓存是否有数据,如果有,则处理,并且分成了overflow情况和非overflow情况,根据字面意思也可以理解
原文的解释,
                  // The maximum number of bytes is limited to the serial buffer size of 128 bytes
                  // If more bytes is received than the BUFFER_SIZE the overflow flag will be set and the
                  // serial buffer will be red untill all the data is cleared from the receive buffer.

翻译:缓冲区只有128个字节,超过这个数就会溢出,从而设定overflow标志。

另外,这里调用了T1.5,每个字符接收之间停顿T1.5时间。

然后继续,
          if (overflow)
                  return errorCount++;
原文解释,

          // If an overflow occurred increment the errorCount
          // variable and return to the main sketch without
          // responding to the request i.e. force a timeout
翻译:错误记录次数加1,然后返回。

继续,
          // The minimum request packet is 8 bytes for function 3 & 16
    if (buffer > 7)
          {
                  unsigned char id = frame[0];
               
                  broadcastFlag = 0;
               
                  if (id == 0)
                          broadcastFlag = 1;

frame是前面接收的缓存数组,根据经验,这里要开始分析报文了。
因为这里只实现功能3和16,所以报文最短长度为8,即要求buffer>7。

继续,
      if (id == slaveID || broadcastFlag) // if the recieved ID matches the slaveID or broadcasting id (0), continue
      {
        unsigned int crc = ((frame[buffer - 2] << 8) | frame[buffer - 1]); // combine the crc Low & High bytes
        if (calculateCRC(buffer - 2) == crc) // if the calculated crc matches the recieved crc continue
        {
容易理解,如果地址和校验码符合,

继续,
                                  function = frame[1];
                                  unsigned int startingAddress = ((frame[2] << 8) | frame[3]); // combine the starting address bytes
                                  unsigned int no_of_registers = ((frame[4] << 8) | frame[5]); // combine the number of register bytes        
                                  unsigned int maxData = startingAddress + no_of_registers;
                                  unsigned char index;
                                  unsigned char address;
                                  unsigned int crc16;
读取报文信息,比如功能码,起始地址等。

继续,
                                  // broadcasting is not supported for function 3
                                  if (!broadcastFlag && (function == 3))
                                  {
                                          if (startingAddress < holdingRegsSize) // check exception 2 ILLEGAL DATA ADDRESS
                                          {
                                                  if (maxData <= holdingRegsSize) // check exception 3 ILLEGAL DATA VALUE
                                                  {
当功能码是3时,并且地址和数据都有效,则。。。

继续,
                                                          unsigned char noOfBytes = no_of_registers * 2;
                // ID, function, noOfBytes, (dataLo + dataHi)*number of registers,
                                        //  crcLo, crcHi

                                                          unsigned char responseFrameSize = 5 + noOfBytes;
                                                          frame[0] = slaveID;
                                                          frame[1] = function;
                                                          frame[2] = noOfBytes;
                                                          address = 3; // PDU starts at the 4th byte
                                                          unsigned int temp;
                                                        
                                                          for (index = startingAddress; index < maxData; index++)
                                                          {
                                                                  temp = regs[index];
                                                                  frame[address] = temp >> 8; // split the register into 2 bytes
                                                                  address++;
                                                                  frame[address] = temp & 0xFF;
                                                                  address++;
                                                          }        
                                                        
                                                          crc16 = calculateCRC(responseFrameSize - 2);
                                                          frame[responseFrameSize - 2] = crc16 >> 8; // split crc into 2 bytes
                                                          frame[responseFrameSize - 1] = crc16 & 0xFF;
                                                          sendPacket(responseFrameSize);
                                                  }
生成报文并发送,类似笔记15中的结构。发送函数是sendPacket,放在后面理解。
复习一下,功能码3的回复格式,


继续,
                                                  }
                                                  else        
                                                          exceptionResponse(3); // exception 3 ILLEGAL DATA VALUE
                                          }
                                          else
                                                  exceptionResponse(2); // exception 2 ILLEGAL DATA ADDRESS
                                  }
                                  else if (function == 16)
                                  {
如果是功能码16,则。。。。

继续,
                                          // Check if the recieved number of bytes matches the calculated bytes
            // minus the request bytes.
                                          // id + function + (2 * address bytes) + (2 * no of register bytes) +
            // byte count + (2 * CRC bytes) = 9 bytes
                                          if (frame[6] == (buffer - 9))
                                          {
                                                  if (startingAddress < holdingRegsSize) // check exception 2 ILLEGAL DATA ADDRESS
                                                  {
                                                          if (maxData <= holdingRegsSize) // check exception 3 ILLEGAL DATA VALUE
                                                          {
功能码16是写多个寄存器,判断条件是否满足

继续,
                                                                  address = 7; // start at the 8th byte in the frame
                                                               
                                                                  for (index = startingAddress; index < maxData; index++)
                                                                  {
                                                                          regs[index] = ((frame[address] << 8) | frame[address + 1]);
                                                                          address += 2;
                                                                  }        
                                                               
                                                                  // only the first 6 bytes are used for CRC calculation
                                                                  crc16 = calculateCRC(6);
                                                                  frame[6] = crc16 >> 8; // split crc into 2 bytes
                                                                  frame[7] = crc16 & 0xFF;
                                                               
                                                                  // a function 16 response is an echo of the first 6 bytes from
                                                                  // the request + 2 crc bytes

                                                                  if (!broadcastFlag) // don't respond if it's a broadcast message
                                                                          sendPacket(8);
                                                          }

for结构是用来写寄存器的值,然后就是生成报文并发送。类似结构已经看过至少10遍了。

最后,
                                                          else        
                                                                  exceptionResponse(3); // exception 3 ILLEGAL DATA VALUE
                                                  }
                                                  else
                                                          exceptionResponse(2); // exception 2 ILLEGAL DATA ADDRESS
                                          }
                                          else
                                                  errorCount++; // corrupted packet
          }                                       
                                  else
                                          exceptionResponse(1); // exception 1 ILLEGAL FUNCTION
        }
                          else // checksum failed
                                  errorCount++;
      } // incorrect id
    }
          else if (buffer > 0 && buffer < 8)
                  errorCount++; // corrupted packet
  }
        return errorCount;
}        

一群跑龙套的,主要是场记,记录下错误类型。

******************************************************************************************
因为只有功能码3和16,相对还是比较简单好理解的。即使这样,感觉执行功能码的结构没有笔记15的好。                                               

最后,看一下发送函数
void sendPacket(unsigned char bufferSize)
{
  digitalWrite(TxEnablePin, HIGH);
               
  for (unsigned char i = 0; i < bufferSize; i++)
    (*ModbusPort).write(frame);
               
        (*ModbusPort).flush();
        
        // allow a frame delay to indicate end of transmission
        delayMicroseconds(T3_5);
        
  digitalWrite(TxEnablePin, LOW);
}

真不讲究,用flush。

和ModbusPort相关的函数都要在HardwareSerial中找,因为有注释,直接复制
http://wiki.geek-workshop.com/do ... :hardwareserial.cpp

void HardwareSerial::flush()
{
  // 当缓冲区不为空时,UDR保持完整,所以当EMPTY && SENT时,TXC触发。
  while (transmitting && ! (*_ucsra & _BV(TXC0)));
  transmitting = false;
}

size_t HardwareSerial::write(uint8_t c)
{
  int i = (_tx_buffer->head + 1) % SERIAL_BUFFER_SIZE;

  // 如果输出缓冲区满了,它没有什么可做,
  // 除了等待中断处理程序清空它。
  // ???: 这里以return 0代替?
  while (i == _tx_buffer->tail)
    ;

  _tx_buffer->buffer[_tx_buffer->head] = c;
  _tx_buffer->head = i;

  sbi(*_ucsrb, _udrie);
  // 清空TXC位 -- "不能通过在位址上写入1来清空"
  transmitting = true;
  sbi(*_ucsra, TXC0);

  return 1;
}

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

对Arduino怎样从AVR发展比较感兴趣,只是AVR日薄西山,投入学习Arduino的态度比较犹豫,感觉像有些势利。


本帖子中包含更多资源

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

x

出0入0汤圆

 楼主| 发表于 2013-12-13 12:01:30 | 显示全部楼层
oldbeginner 发表于 2013-12-12 22:13
unsigned int modbus_update()
{
  if ((*ModbusPort).available())

Arduino 主机modbus

#include <SimpleModbusMaster.h>

#define baud 9600
#define timeout 1000
#define polling 200 // the scan rate

#define retry_count 10

// used to toggle the receive/transmit pin on the driver
#define TxEnablePin 2

#define LED 9

enum
{
  PACKET1,
  PACKET2,
  TOTAL_NO_OF_PACKETS // leave this last entry
};

Packet packets[TOTAL_NO_OF_PACKETS];

packetPointer packet1 = &packets[PACKET1];
packetPointer packet2 = &packets[PACKET2];

unsigned int readRegs[1];

unsigned int writeRegs[1];

void setup()
{
// read 1 register starting at address 0  
  modbus_construct(packet1, 2, READ_HOLDING_REGISTERS, 0, 1, readRegs);

  // write 1 register starting at address 1  
  modbus_construct(packet2, 2, PRESET_MULTIPLE_REGISTERS, 1, 1, writeRegs);

  modbus_configure(&Serial, baud, SERIAL_8N2, timeout, polling, retry_count, TxEnablePin, packets, TOTAL_NO_OF_PACKETS);
  
  pinMode(LED, OUTPUT);
}

void loop()
{
  modbus_update();
  
  writeRegs[0] = analogRead(A0); // update data to be written to arduino slave
  
  analogWrite(LED, readRegs[0]>>2);
}

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

主机要实现的功能,
The example will use packet1 to read a register from address 0 (the adc ch0 value)
   from the arduino slave. It will then use this value to adjust the brightness
   of an led on pin 9 using PWM.
   It will then use packet2 to write a register (its own adc ch0 value) to address 1
   on the arduino slave adjusting the brightness of an led on pin 9 using PWM.

主机利用报文1,读取从机地址0的模拟值,然后利用PWM调节LED亮度(连接数字输出9)

主机利用报文2写从机寄存器地址1(模拟值),然后从机根据该值大小调节亮度。

其中Packet是定义在SimpleModbusMaster.h中一个类

****************************************************************
SimpleModbusMaster.h

typedef struct
{
// specific packet info
  unsigned char id;
  unsigned char function;
  unsigned int address;
        // For functions 1 & 2 data is the number of points
  // For functions 3, 4 & 16 data is the number of registers
  // For function 15 data is the number of coils

  unsigned int data;
  unsigned int* register_array;
  
  // modbus information counters
  unsigned int requests;
  unsigned int successful_requests;
        unsigned int failed_requests;
        unsigned int exception_errors;
  unsigned int retries;
         
  // connection status of packet
  unsigned char connection;
  
}Packet;

另外还有三个函数声明,
void modbus_update();

void modbus_construct(Packet *_packet,
                                                                                        unsigned char id,
                                                                                        unsigned char function,
                                                                                        unsigned int address,
                                                                                        unsigned int data,
                                                                                        unsigned int* register_array);
                                                                                       
void modbus_configure(HardwareSerial* SerialPort,
                                                                                        long baud,
                                                                                        unsigned char byteFormat,
                                                                                        unsigned int _timeout,
                                                                                        unsigned int _polling,
                                                                                        unsigned char _retry_count,
                                                                                        unsigned char _TxEnablePin,
                                                                                        Packet* _packets,
                                                                                        unsigned int _total_no_of_packets);

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




出0入0汤圆

 楼主| 发表于 2013-12-13 12:27:16 | 显示全部楼层
oldbeginner 发表于 2013-12-13 12:01
Arduino 主机modbus

#include

在SimpleModbusMaster.cpp中,

首先,定义变量

// state machine states
#define IDLE 1
#define WAITING_FOR_REPLY 2
#define WAITING_FOR_TURNAROUND 3

主机存在三种状态,空闲,等待回复,等待工作完成。(题外,freemodbus就有10种状态,三种类型,可以组合4*2*4=32种组合,但出现的就几种)

#define BUFFER_SIZE 128

unsigned char state;
unsigned char retry_count;
unsigned char TxEnablePin;

定义状态,重试次数,发送使能

unsigned char frame[BUFFER_SIZE];

unsigned char buffer;

unsigned int timeout; // timeout interval

unsigned int polling; // turnaround delay interval

unsigned int T1_5; // inter character time out in microseconds

unsigned long delayStart; // init variable for turnaround and timeout delay

unsigned int total_no_of_packets;

Packet* packetArray; // packet starting address

Packet* packet; // current packet

HardwareSerial* ModbusPort;

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

变量定义后,开始函数理解
void modbus_update()
{
        switch (state)
        {
                case IDLE:
                idle();
                break;
                case WAITING_FOR_REPLY:
                waiting_for_reply();
                break;
                case WAITING_FOR_TURNAROUND:
                waiting_for_turnaround();
                break;
        }
}
根据不同状态,调用相应函数。

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

void idle()
{
  static unsigned int packet_index;       
       
        unsigned int failed_connections = 0;
       
        unsigned char current_connection;

定义变量,报文数组下标等

        do
        {               
                if (packet_index == total_no_of_packets) // wrap around to the beginning
                        packet_index = 0;
如果数组下标等于报文长度,则再从0开始

                // proceed to the next packet
                packet = &packetArray[packet_index];
               
                // get the current connection status
                current_connection = packet->connection;
相关赋值。

                if (!current_connection)
                {                       
                        // If all the connection attributes are false return
                        // immediately to the main sketch

                        if (++failed_connections == total_no_of_packets)
                                return;
                }
继续,

                packet_index++;     
   
        // if a packet has no connection get the next one               
        }while (!current_connection);

        constructPacket();
}
最后生成报文。

出0入0汤圆

 楼主| 发表于 2013-12-13 15:17:12 | 显示全部楼层
oldbeginner 发表于 2013-12-13 12:27
在SimpleModbusMaster.cpp中,

首先,定义变量

Packet结构还没有理解过,不过从下面的函数中也能得到理解

void constructPacket()
{         
  packet->requests++;

  frame[0] = packet->id;

  frame[1] = packet->function;

  frame[2] = packet->address >> 8; // address Hi

  frame[3] = packet->address & 0xFF; // address Lo

        // For functions 1 & 2 data is the number of points

  // For functions 3, 4 & 16 data is the number of registers

  // For function 15 data is the number of coils


  frame[4] = packet->data >> 8; // MSB

  frame[5] = packet->data & 0xFF; // LSB

从机地址,功能码,寄存器地址,数据
继续,

        unsigned char frameSize;   
       
// construct the frame according to the modbus function  
  if (packet->function == PRESET_MULTIPLE_REGISTERS)
                frameSize = construct_F16();

        else if (packet->function == FORCE_MULTIPLE_COILS)
                frameSize = construct_F15();

        else // else functions 1,2,3 & 4 is assumed. They all share the exact same request format.
    frameSize = 8; // the request is always 8 bytes in size for the above mentioned functions.

根据功能码,调用不同的函数,返回值是报文长度。
继续,
        unsigned int crc16 = calculateCRC(frameSize - 2);
       
  frame[frameSize - 2] = crc16 >> 8; // split crc into 2 bytes

  frame[frameSize - 1] = crc16 & 0xFF;

  sendPacket(frameSize);

CRC校验,并发送报文。
继续,

        state = WAITING_FOR_REPLY; // state change
       
        // if broadcast is requested (id == 0) for function 15 or 16 then override
  // the previous state and force a success since the slave wont respond

        if (packet->id == 0)
                        processSuccess();
}
发送后,状态设置位等待回复。
功能码15和16,特殊处理。

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

void processSuccess()
{
        packet->successful_requests++; // transaction sent successfully

        packet->retries = 0; // if a request was successful reset the retry counter

        state = WAITING_FOR_TURNAROUND;

        delayStart = millis(); // start the turnaround delay
}
设置状态等待工作完成,millis是等待一段时间的函数。

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

void waiting_for_turnaround()
{
  if ((millis() - delayStart) > polling)
                state = IDLE;
}
设置状态为空闲。


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

主机要比从机复杂,因为要处理不同状态。

目前用不到主机,暂时先理解到这里。

// get the serial data from the buffer
void waiting_for_reply()
{
        if ((*ModbusPort).available()) // is there something to check?
        {
                unsigned char overflowFlag = 0;
                buffer = 0;
                while ((*ModbusPort).available())
                {
                        // The maximum number of bytes is limited to the serial buffer size
      // of BUFFER_SIZE. If more bytes is received than the BUFFER_SIZE the
      // overflow flag will be set and the serial buffer will be read until
      // all the data is cleared from the receive buffer, while the slave is
      // still responding.
                        if (overflowFlag)
                                (*ModbusPort).read();
                        else
                        {
                                if (buffer == BUFFER_SIZE)
                                        overflowFlag = 1;
                       
                                frame[buffer] = (*ModbusPort).read();
                                buffer++;
                        }
                        // This is not 100% correct but it will suffice.
                        // worst case scenario is if more than one character time expires
                        // while reading from the buffer then the buffer is most likely empty
                        // If there are more bytes after such a delay it is not supposed to
                        // be received and thus will force a frame_error.
                        delayMicroseconds(T1_5); // inter character time out
                }
                       
                // The minimum buffer size from a slave can be an exception response of
    // 5 bytes. If the buffer was partially filled set a frame_error.
                // The maximum number of bytes in a modbus packet is 256 bytes.
                // The serial buffer limits this to 128 bytes.
       
                if ((buffer < 5) || overflowFlag)
                        processError();      
      
                // Modbus over serial line datasheet states that if an unexpected slave
    // responded the master must do nothing and continue with the time out.
                // This seems silly cause if an incorrect slave responded you would want to
    // have a quick turnaround and poll the right one again. If an unexpected
    // slave responded it will most likely be a frame error in any event
                else if (frame[0] != packet->id) // check id returned
                        processError();
                else
                        processReply();
        }
        else if ((millis() - delayStart) > timeout) // check timeout
        {
                processError();
                state = IDLE; //state change, override processError() state
        }
}

void processReply()
{
        // combine the crc Low & High bytes
  unsigned int received_crc = ((frame[buffer - 2] << 8) | frame[buffer - 1]);
  unsigned int calculated_crc = calculateCRC(buffer - 2);
       
        if (calculated_crc == received_crc) // verify checksum
        {
                // To indicate an exception response a slave will 'OR'
                // the requested function with 0x80
                if ((frame[1] & 0x80) == 0x80) // extract 0x80
                {
                        packet->exception_errors++;
                        processError();
                }
                else
                {
                        switch (frame[1]) // check function returned
      {
        case READ_COIL_STATUS:
        case READ_INPUT_STATUS:
        process_F1_F2();
        break;
        case READ_INPUT_REGISTERS:
        case READ_HOLDING_REGISTERS:
        process_F3_F4();
        break;
        case FORCE_MULTIPLE_COILS:
        case PRESET_MULTIPLE_REGISTERS:
        process_F15_F16();
        break;
        default: // illegal function returned
        processError();
        break;
      }
                }
        }
        else // checksum failed
        {
                processError();
        }
}

出0入0汤圆

发表于 2013-12-13 15:51:18 | 显示全部楼层
感觉好复杂啊
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-5-3 02:26

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

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