打印

基于GD32F103VK实现modbus-RTU主站

[复制链接]
5824|20
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
#申请原创#
一、modbus协议
      Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气 Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。
Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。
      对于串行连接,存在两个变种,它们在数值数据表示不同和协议细节上略有不同。Modbus RTU是一种紧凑的,采用二进制表示数据的方式,Modbus ASCII是一种人类可读的,冗长的表示方式。这两个变种都使用串行通信(serial communication)方式。RTU格式后续的命令/数据带有循环冗余校验的校验和,而ASCII格式采用纵向冗余校验的校验和。被配置为RTU变种的节点不会和设置为ASCII变种的节点通信,反之亦然。




使用特权

评论回复
沙发
zeshoufx|  楼主 | 2021-7-13 16:48 | 只看该作者
二、常用的开源modbus协议

Modbus作为一种常见的工业通信协议,几乎被所有的设备所支持,如果能在软件或者设备中增强Modbus通信功能,
无疑对于市场应用来说是个很吸引人的卖点。而对 于Modbus开发来说,网络上存在相当多的开源库,
其中libmodbus (http://www. libmodbus. org)和freemo-dbus ( htp://www.freemodbus.org)可以说是其中的翘楚,
值得开发者认真分析和学习。

使用特权

评论回复
板凳
zeshoufx|  楼主 | 2021-7-13 16:51 | 只看该作者
zeshoufx 发表于 2021-7-13 16:48
二、常用的开源modbus协议

Modbus作为一种常见的工业通信协议,几乎被所有的设备所支持,如果能在软件或者 ...

三、硬件驱动函数
static void InitHardUsart(void)
{
        /* 第1步:打开GPIO和USART部件的时钟 */
        rcu_periph_clock_enable(RCU_GPIOA);
        rcu_periph_clock_enable(RCU_USART1);

        /* 第2步:将USART Tx的GPIO配置为推挽复用模式 */
        gpio_init(GPIOA,GPIO_MODE_AF_PP,GPIO_OSPEED_50MHZ,GPIO_PIN_2);
       
        /* 第3步:将USART Rx的GPIO配置为浮空输入模式
                由于CPU复位后,GPIO缺省都是浮空输入模式,因此下面这个步骤不是必须的
                但是,我还是建议加上便于阅读,并且防止其它地方修改了这个口线的设置参数
        */
        gpio_init(GPIOA,GPIO_MODE_IN_FLOATING,GPIO_OSPEED_50MHZ,GPIO_PIN_3);
       
       
        /* 第4步: 配置串口硬件参数 */
        usart_baudrate_set(USART1,USART1_BAUD);
        usart_parity_config(USART1,USART_PM_NONE);
        usart_stop_bit_set(USART1,USART_STB_1BIT);
        usart_word_length_set(USART1,USART_WL_8BIT);
        usart_transmit_config(USART1,USART_TRANSMIT_ENABLE);
        usart_receive_config(USART1,USART_RECEIVE_ENABLE);
        usart_hardware_flow_cts_config(USART1,USART_CTS_DISABLE);
        usart_hardware_flow_rts_config(USART1,USART_RTS_DISABLE);

        usart_interrupt_enable(USART1,USART_INT_RBNE);        /* 使能接收中断 */
        /*
                USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
                注意: 不要在此处打开发送中断
                发送中断使能在SendUart()函数打开
        */
        usart_enable(USART1);                /* 使能串口 */

        /* CPU的小缺陷:串口配置好,如果直接Send,则第1个字节发送不出去
                如下语句解决第1个字节无法正确发送出去的问题 */
        usart_interrupt_flag_clear(USART1,USART_INT_FLAG_TC);     /* 清发送完成标志,Transmission Complete flag */
}

/**
  * 函数功能: 配置NVIC,设定USART接收中断优先级.
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明:无
  */
static void NVIC_Configuration_USART(void)
{
       
        /* 使能串口1中断 */
        nvic_irq_enable(USART1_IRQn,2,2);

}

/*
*********************************************************************************************************
*        函 数 名: RS485_InitTXE
*        功能说明: 配置RS485发送使能口线 TXE
*        形    参: 无
*        返 回 值: 无
*********************************************************************************************************
*/
static void RS485_InitTXEN(void)
{
        rcu_periph_clock_enable(RCU_GPIOA);
        gpio_init(GPIOA,GPIO_MODE_OUT_PP,GPIO_OSPEED_50MHZ,GPIO_PIN_1);
}

/*
*********************************************************************************************************
*        函 数 名: RS485_SendBefor
*        功能说明: 发送数据前的准备工作。对于RS485通信,请设置RS485芯片为发送状态,
*                          并修改 UartVarInit()中的函数指针等于本函数名,比如 g_tUart2.SendBefor = RS485_SendBefor
*        形    参: 无
*        返 回 值: 无
*********************************************************************************************************
*/
void RS485_SendBefor(void)
{
        RS485_TX_EN();        /* 切换RS485收发芯片为发送模式 */
}

/*
*********************************************************************************************************
*        函 数 名: RS485_SendOver
*        功能说明: 发送一串数据结束后的善后处理。对于RS485通信,请设置RS485芯片为接收状态,
*                          并修改 UartVarInit()中的函数指针等于本函数名,比如 g_tUart2.SendOver = RS485_SendOver
*        形    参: 无
*        返 回 值: 无
*********************************************************************************************************
*/
void RS485_SendOver(void)
{
        RS485_RX_EN();        /* 切换RS485收发芯片为接收模式 */
}


/*
*********************************************************************************************************
*        函 数 名: RS485_SendBuf
*        功能说明: 通过RS485芯片发送一串数据。注意,本函数不等待发送完毕。
*        形    参: 无
*        返 回 值: 无
*********************************************************************************************************
*/
void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen)
{
        switch(USART_SELECT_NUM)
        {
                case 1:
                {
                        comSendBuf(COM1, _ucaBuf, _usLen);
                }break;
                case 2:
                {       
                        comSendBuf(COM2, _ucaBuf, _usLen);
                }break;
                case 3:
                {
                        comSendBuf(COM3, _ucaBuf, _usLen);
                }break;
                default:
                {
                       
                }break;
        }
}


使用特权

评论回复
地板
zeshoufx|  楼主 | 2021-7-13 16:52 | 只看该作者
zeshoufx 发表于 2021-7-13 16:51
三、硬件驱动函数

四、主站函数
/*
*********************************************************************************************************
*        函 数 名: MODH_Send01H
*        功能说明: 发送01H指令,查询1个或多个保持寄存器
*        形    参: _addr : 从站地址
*                          _reg : 寄存器编号
*                          _num : 寄存器个数
*        返 回 值: 无
*********************************************************************************************************
*/
void MODH_Send01H(uint8_t _addr, uint16_t _reg, uint16_t _num)
{
        g_tModH.TxCount = 0;
        g_tModH.TxBuf[g_tModH.TxCount++] = _addr;                /* 从站地址 */
        g_tModH.TxBuf[g_tModH.TxCount++] = 0x01;                /* 功能码 */       
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg >> 8;        /* 寄存器编号 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg;                /* 寄存器编号 低字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _num >> 8;        /* 寄存器个数 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _num;                /* 寄存器个数 低字节 */
       
        MODH_SendAckWithCRC();                /* 发送数据,自动加CRC */
        g_tModH.fAck01H = 0;                /* 清接收标志 */
        g_tModH.RegNum = _num;                /* 寄存器个数 */
        g_tModH.Reg01H = _reg;                /* 保存03H指令中的寄存器地址,方便对应答数据进行分类 */       
}

/*
*********************************************************************************************************
*        函 数 名: MODH_Send02H
*        功能说明: 发送02H指令,读离散输入寄存器
*        形    参: _addr : 从站地址
*                          _reg : 寄存器编号
*                          _num : 寄存器个数
*        返 回 值: 无
*********************************************************************************************************
*/
void MODH_Send02H(uint8_t _addr, uint16_t _reg, uint16_t _num)
{
        g_tModH.TxCount = 0;
        g_tModH.TxBuf[g_tModH.TxCount++] = _addr;                /* 从站地址 */
        g_tModH.TxBuf[g_tModH.TxCount++] = 0x02;                /* 功能码 */       
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg >> 8;        /* 寄存器编号 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg;                /* 寄存器编号 低字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _num >> 8;        /* 寄存器个数 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _num;                /* 寄存器个数 低字节 */
       
        MODH_SendAckWithCRC();                /* 发送数据,自动加CRC */
        g_tModH.fAck02H = 0;                /* 清接收标志 */
        g_tModH.RegNum = _num;                /* 寄存器个数 */
        g_tModH.Reg02H = _reg;                /* 保存03H指令中的寄存器地址,方便对应答数据进行分类 */       
}

/*
*********************************************************************************************************
*        函 数 名: MODH_Send03H
*        功能说明: 发送03H指令,查询1个或多个保持寄存器
*        形    参: _addr : 从站地址
*                          _reg : 寄存器编号
*                          _num : 寄存器个数
*        返 回 值: 无
*********************************************************************************************************
*/
void MODH_Send03H(uint8_t _addr, uint16_t _reg, uint16_t _num)
{
        g_tModH.TxCount = 0;
        g_tModH.TxBuf[g_tModH.TxCount++] = _addr;                /* 从站地址 */
        g_tModH.TxBuf[g_tModH.TxCount++] = 0x03;                /* 功能码 */       
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg >> 8;        /* 寄存器编号 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg;                /* 寄存器编号 低字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _num >> 8;        /* 寄存器个数 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _num;                /* 寄存器个数 低字节 */
       
        MODH_SendAckWithCRC();                /* 发送数据,自动加CRC */
        g_tModH.fAck03H = 0;                /* 清接收标志 */
        g_tModH.RegNum = _num;                /* 寄存器个数 */
        g_tModH.Reg03H = _reg;                /* 保存03H指令中的寄存器地址,方便对应答数据进行分类 */       
}

/*
*********************************************************************************************************
*        函 数 名: MODH_Send04H
*        功能说明: 发送04H指令,读输入寄存器
*        形    参: _addr : 从站地址
*                          _reg : 寄存器编号
*                          _num : 寄存器个数
*        返 回 值: 无
*********************************************************************************************************
*/
void MODH_Send04H(uint8_t _addr, uint16_t _reg, uint16_t _num)
{
        g_tModH.TxCount = 0;
        g_tModH.TxBuf[g_tModH.TxCount++] = _addr;                /* 从站地址 */
        g_tModH.TxBuf[g_tModH.TxCount++] = 0x04;                /* 功能码 */       
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg >> 8;        /* 寄存器编号 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg;                /* 寄存器编号 低字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _num >> 8;        /* 寄存器个数 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _num;                /* 寄存器个数 低字节 */
       
        MODH_SendAckWithCRC();                /* 发送数据,自动加CRC */
        g_tModH.fAck04H = 0;                /* 清接收标志 */
        g_tModH.RegNum = _num;                /* 寄存器个数 */
        g_tModH.Reg04H = _reg;                /* 保存03H指令中的寄存器地址,方便对应答数据进行分类 */       
}

/*
*********************************************************************************************************
*        函 数 名: MODH_Send05H
*        功能说明: 发送05H指令,写强置单线圈
*        形    参: _addr : 从站地址
*                          _reg : 寄存器编号
*                          _value : 寄存器值,2字节
*        返 回 值: 无
*********************************************************************************************************
*/
void MODH_Send05H(uint8_t _addr, uint16_t _reg, uint16_t _value)
{
        g_tModH.TxCount = 0;
        g_tModH.TxBuf[g_tModH.TxCount++] = _addr;                        /* 从站地址 */
        g_tModH.TxBuf[g_tModH.TxCount++] = 0x05;                        /* 功能码 */       
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg >> 8;                /* 寄存器编号 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg;                        /* 寄存器编号 低字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _value >> 8;                /* 寄存器值 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _value;                        /* 寄存器值 低字节 */
       
        MODH_SendAckWithCRC();                /* 发送数据,自动加CRC */

        g_tModH.fAck05H = 0;                /* 如果收到从机的应答,则这个标志会设为1 */
}

/*
*********************************************************************************************************
*        函 数 名: MODH_Send06H
*        功能说明: 发送06H指令,写1个保持寄存器
*        形    参: _addr : 从站地址
*                          _reg : 寄存器编号
*                          _value : 寄存器值,2字节
*        返 回 值: 无
*********************************************************************************************************
*/
void MODH_Send06H(uint8_t _addr, uint16_t _reg, uint16_t _value)
{
        g_tModH.TxCount = 0;
        g_tModH.TxBuf[g_tModH.TxCount++] = _addr;                        /* 从站地址 */
        g_tModH.TxBuf[g_tModH.TxCount++] = 0x06;                        /* 功能码 */       
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg >> 8;                /* 寄存器编号 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg;                        /* 寄存器编号 低字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _value >> 8;                /* 寄存器值 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _value;                        /* 寄存器值 低字节 */
       
        MODH_SendAckWithCRC();                /* 发送数据,自动加CRC */
       
        g_tModH.fAck06H = 0;                /* 如果收到从机的应答,则这个标志会设为1 */
}

/*
*********************************************************************************************************
*        函 数 名: MODH_Send10H
*        功能说明: 发送10H指令,连续写多个保持寄存器. 最多一次支持23个寄存器。
*        形    参: _addr : 从站地址
*                          _reg : 寄存器编号
*                          _num : 寄存器个数n (每个寄存器2个字节) 值域
*                          _buf : n个寄存器的数据。长度 = 2 * n
*        返 回 值: 无
*********************************************************************************************************
*/
void MODH_Send10H(uint8_t _addr, uint16_t _reg, uint8_t _num, uint8_t *_buf)
{
        uint16_t i;
       
        g_tModH.TxCount = 0;
        g_tModH.TxBuf[g_tModH.TxCount++] = _addr;                /* 从站地址 */
        g_tModH.TxBuf[g_tModH.TxCount++] = 0x10;                /* 从站地址 */       
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg >> 8;        /* 寄存器编号 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _reg;                /* 寄存器编号 低字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _num >> 8;        /* 寄存器个数 高字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = _num;                /* 寄存器个数 低字节 */
        g_tModH.TxBuf[g_tModH.TxCount++] = 2 * _num;        /* 数据字节数 */
       
        for (i = 0; i < 2 * _num; i++)
        {
                if (g_tModH.TxCount > H_RX_BUF_SIZE - 3)
                {
                        return;                /* 数据超过缓冲区超度,直接丢弃不发送 */
                }
                g_tModH.TxBuf[g_tModH.TxCount++] = _buf;                /* 后面的数据长度 */
        }
       
        MODH_SendAckWithCRC();        /* 发送数据,自动加CRC */
}


/*
*********************************************************************************************************
*        函 数 名: MODH_ReciveNew
*        功能说明: 串口接收中断服务程序会调用本函数。当收到一个字节时,执行一次本函数。
*        形    参:
*        返 回 值: 1 表示有数据
*********************************************************************************************************
*/
void MODH_ReciveNew(uint8_t _data)
{
        /*
                3.5个字符的时间间隔,只是用在RTU模式下面,因为RTU模式没有开始符和结束符,
                两个数据包之间只能靠时间间隔来区分,Modbus定义在不同的波特率下,间隔时间是不一样的,
                所以就是3.5个字符的时间,波特率高,这个时间间隔就小,波特率低,这个时间间隔相应就大

                4800  = 7.297ms
                9600  = 3.646ms
                19200  = 1.771ms
                38400  = 0.885ms
        */
        uint32_t timeout;

        g_modh_timeout = 0;
       
        timeout = 35000000 / HBAUD485;                /* 计算超时时间,单位us 35000000*/
       
        /* 硬件定时中断,定时精度us 硬件定时器2用于MODBUS从机, 定时器3用于MODBUS从机主机*/
        bsp_StartHardTimer(3, timeout, (void *)MODH_RxTimeOut);

        if (g_tModH.RxCount < H_RX_BUF_SIZE)
        {
                g_tModH.RxBuf[g_tModH.RxCount++] = _data;
        }
}

使用特权

评论回复
5
zeshoufx|  楼主 | 2021-7-13 16:55 | 只看该作者
zeshoufx 发表于 2021-7-13 16:52
四、主站函数
/*
********************************************************************************** ...

五、运行效果
主站发送请求至modbus slave截图如图所示


使用特权

评论回复
6
zeshoufx|  楼主 | 2021-7-13 16:58 | 只看该作者
zeshoufx 发表于 2021-7-13 16:55
五、运行效果
主站发送请求至modbus slave截图如图所示

六、从站响应主站

使用特权

评论回复
7
zeshoufx|  楼主 | 2021-7-13 17:01 | 只看该作者
zeshoufx 发表于 2021-7-13 16:58
六、从站响应主站

七、运行过程

使用特权

评论回复
8
zeshoufx|  楼主 | 2021-7-13 17:02 | 只看该作者

八、主函数

#include "bitband.h"
#include "systick.h"
#include "led.h"
#include "key.h"
#include "usart.h"

#include "bsp_SysTick.h"
#include "bsp_usartx_fifo.h"
#include "modbus_host.h"

void TestModbusHost(uint8_t value)
{
        switch(value)
        {
                case 0:
                        //printf("即将发送命令:【读线圈(继电器)状态】MODH_ReadParam_01H\n");
                        MODH_ReadParam_01H(REG_D01, 4);
                        break;
                case 1:
                        //printf("即将发送命令:【读多个寄存器(内部寄存器)】MODH_ReadParam_03H\n");
                        MODH_ReadParam_03H(REG_P01, 1);
                        break;
                case 2:
                        //printf("即将发送命令:【读输入离散量(控制按钮)】MODH_ReadParam_02H\n");
                        MODH_ReadParam_02H(REG_T01, 2);
                        break;
                case 3:
                        //printf("即将发送命令:【读输入寄存器(模拟量信号)】MODH_ReadParam_04H\n");
                        MODH_ReadParam_04H(REG_A01, 1);
                        break;
                case 4:
                        //printf("即将发送命令:【写单个寄存器(内部寄存器)】MODH_WriteParam_06H\n");
                        MODH_WriteParam_06H(REG_P01, 1);
                        break;
                case 5:
                        //printf("即将发送命令:【写单个寄存器(内部寄存器)】MODH_WriteParam_06H\n");
                        MODH_WriteParam_06H(REG_P01, 0);
                        break;        
                case 6:
                        //printf("即将发送命令:【写单个线圈(继电器)】MODH_WriteParam_05H\n");
                        MODH_WriteParam_05H(REG_D04, 1);
                        break;
                case 7:
                        //printf("即将发送命令:【写单个线圈(继电器)】MODH_WriteParam_05H\n");
                        MODH_WriteParam_05H(REG_D04, 0);
                        break;         
                case 8:      
                        //printf("即将发送命令:【写多个保存寄存器(内部寄存器)】MODH_WriteParam_10H\n");
                        {
                                uint8_t buf[4];
                               
                                buf[0] = 0x01;
                                buf[1] = 0x02;
                                buf[2] = 0x03;
                                buf[3] = 0x04;
                                MODH_WriteParam_10H(REG_P01, 2, buf);
                        }
                        break;
                default:
                {
                }break;
        }               
}


int main(void)
{
//        u32 uid[3]={0};
//        enhenced_run(ENABLE);
//        nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2);
//        systick_set(96);
//        led_set();
//        key_set();
//        usart_set(9600);
//       
//        get_uid(uid);
//        printf("sram=%dK bytes,flash=%dK bytes\r\n",get_memsize(mem_type_sram),get_memsize(mem_type_flash));
//        printf("uid=[%x%x%x]\r\n",uid[0],uid[1],uid[2]);
//    while (1)
//        {
//        led1=!led1;
//                delay_ms(200);
//                led2=!led2;
//                delay_ms(200);
//                led3=!led3;
//                delay_ms(200);
//                led4=!led4;
//                delay_ms(200);
//    }

        Usart_FIFO_Init();
        SysTick_Init();
        while ( 1 )
        {                        
                MODH_Poll();
                TestModbusHost(1);//用于测试向从机发送数据,正式使用有用户编写
        }
}





使用特权

评论回复
9
zeshoufx|  楼主 | 2021-7-13 17:08 | 只看该作者

上传工程文件

Video_20210713_043729_159.gif (4 MB )

运行

运行

10-modbus_host.zip

6.21 MB

工程文件

使用特权

评论回复
10
zhitao2072| | 2021-7-13 17:40 | 只看该作者
谢谢分享,你很牛掰啊

使用特权

评论回复
11
dwdsp| | 2021-7-14 08:38 | 只看该作者
顶个!很不错了!

使用特权

评论回复
12
90houyidai| | 2021-7-14 11:11 | 只看该作者
不错的模板工程

使用特权

评论回复
13
勇敢的大白菜| | 2021-7-14 16:05 | 只看该作者
学习了,好好下载之后,学习一下。

使用特权

评论回复
14
zeshoufx|  楼主 | 2021-7-14 16:25 | 只看该作者
zhitao2072 发表于 2021-7-13 17:40
谢谢分享,你很牛掰啊

一起学习交流,,

使用特权

评论回复
15
zeshoufx|  楼主 | 2021-7-14 16:26 | 只看该作者
dwdsp 发表于 2021-7-14 08:38
顶个!很不错了!

谢谢你,,,一起学习交流,,

使用特权

评论回复
16
一路向北lm| | 2021-7-14 16:45 | 只看该作者
主站用的 libmodbus?

使用特权

评论回复
17
zeshoufx|  楼主 | 2021-7-14 17:14 | 只看该作者

不是,,,

使用特权

评论回复
18
zeshoufx|  楼主 | 2021-7-14 22:02 | 只看该作者

一起学习交流,,,,

使用特权

评论回复
19
zeshoufx|  楼主 | 2021-7-15 08:38 | 只看该作者
勇敢的大白菜 发表于 2021-7-14 16:05
学习了,好好下载之后,学习一下。

可以一起学习交流,,,,,,,,,,

使用特权

评论回复
20
编号10086| | 2021-7-19 17:15 | 只看该作者
可以,正好要用安富莱的例程,直接抄一下楼主的,就可以不用自己一直了。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

67

主题

1962

帖子

14

粉丝