文章推荐
MODBUS_RTU通讯规约
Modbus通信协议详解
modbus slave和modbus poll使用说明(含软件下载地址)
RS485简介
RS485:串行、半双工。
物理层
串口是一种接口标准,它规定了接口的电气标准,简单说只是物理层的一个标准。
在串口接口标准上使用各种协议进行通讯及设备控制。
典型的串行通讯标准是RS232和RS485,它们定义了电压,阻抗等,没有定义协议。
差分信号
两条线传输一个信号,使用一对双绞线,其中一根线定义为A,另一个定义为B。两个485接口连接,A连接A,B连接B。
长距离布线会有信号衰减,而且引入噪声和干扰的可能性更大,在线缆A和B上的表现就是电压幅度的变化,但是,采用差分线的好处就是,差值相减就会忽略掉干扰依旧能输出正常的信号,把这种差分接收器忽略两条信号线上相同电压的能力称为共模抑制。
拓扑结构
RS485有两线制和四线制两种接线,四线制只能实现点对点的通信方式,现很少采用,多采用的是两线制接线方式,这种接线方式为总线拓扑结构,在同一总线上最多可以挂接32个节点。
RS-485总线同I2C,也是主从模式,支持点对点单从机模式,也支持多从机模式,不支持多主机模式。
理想情况下RS485需要2个匹配电阻,其阻值要求等于传输电缆的特性阻抗(一般为120Ω)
引脚连接
RE引脚在低电平时表示接收数据,高电平时表示发送数据。
ModBus通讯协议
简介
Modbus是一种串行通信协议。
Modbus是一种一主一从的一对一通信方式(主机发一帧,从机回一帧的形式),当然也一主多从,但实际也是一对一通信,同一时刻只能有一个从机进行响应。如果需要和多个从机同时通信,这里也支持使用广播,即主机发送指令,所有从机接收指令并执行,但不进行应答。
当进行一主多从通信时,主机通过从机ID号来区分要通信的从机设备。从机ID范围为1~ 247,0为广播地址,248~255为用户自定义地址。
目前总共有4种通信形式,RTU、ASCII、TCP、Plus。
RTU
其报文格式是十六进制的,由Slave ID+数据+CRC校验三部分组成。
要注意的是,数据部分高位数据在前,低位数据在后,而CRC校验则是低位在前,高位在后。即数据存储为小端模式,CRC校验码存储为大端模式
报文解析功能码03
代码
.c文件
#include <stdio.h>
#include <string.h>
#include "stm32f10x.h"
#include "modbus.h"
#include "lcd.h"
#include "delay.h"
u16 UART4_RX_STA;
u16 UART4_TX_STA;
u8 UART4_RX_BUF[UART4_REC_LEN];
u8 UART4_TX_BUF[UART4_REC_LEN];
int sendsta=1;
modbus_receive poll; //poll软件发送(单片机做从机)
modbus_receive slave; //slave软件反馈(单片机做主机)
modbus_send slav; //单片机做从机发送数据
u16 ModeBus_Data[5]={0x01,0x02,0x03,0x04,0x05};
void RS485_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体类型变量
USART_InitTypeDef USARTInitDef;
NVIC_InitTypeDef NVICInitDef;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); //使能始终,使能挂载在APB2,的GPIOA
RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART4, ENABLE); //开串口的时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //设置引脚PC10 TX
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //设置成通用推挽复用输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //设置传输速率为50MHZ
GPIO_Init(GPIOC, &GPIO_InitStructure);//调用GPIO初始化函数,进行初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; //设置引脚PC11 RX
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //设置成浮空输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //设置传输速率为50MHZ
GPIO_Init(GPIOC, &GPIO_InitStructure);//调用GPIO初始化函数,进行初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //设置引脚PC11 RE
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置成浮空输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //设置传输速率为50MHZ
GPIO_Init(GPIOC, &GPIO_InitStructure);//调用GPIO初始化函数,进行初始化
//--------------------------------------------------------------------------------
USARTInitDef.USART_BaudRate= 9600;//波特率
USARTInitDef.USART_HardwareFlowControl= USART_HardwareFlowControl_None; //是否选择硬件流
USARTInitDef.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
USARTInitDef.USART_Parity= USART_Parity_No; //是否奇偶校验
USARTInitDef.USART_StopBits=USART_StopBits_1;
USARTInitDef.USART_WordLength=USART_WordLength_8b;
USART_Init(UART4, &USARTInitDef);
//--------------------------------------------------------------------------------
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //分组
NVICInitDef.NVIC_IRQChannel=UART4_IRQn;
NVICInitDef.NVIC_IRQChannelCmd=ENABLE;
NVICInitDef.NVIC_IRQChannelPreemptionPriority=1;
NVICInitDef.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVICInitDef);
//--------------------------------------------------------------------------------
USART_ITConfig(UART4, USART_IT_RXNE, ENABLE); //配置中断
USART_ITConfig(UART4, USART_IT_IDLE, ENABLE); //配置中断,中断源一般分开写
USART_Cmd(UART4, ENABLE);//使能串口
RS485_TX_RX=0;
}
//中断接收:方法二:利用中断标志位(RXNE.IDLE)注意不要在串口加\r\n
void UART4_IRQHandler()
{
u8 res;
if(USART_GetITStatus(UART4,USART_IT_RXNE)) //SR寄存器的 RXNE位,读寄存器非空,判断是否有数据
{
res = USART_ReceiveData(UART4); //读取数据相当于清除中断
UART4_RX_BUF[UART4_RX_STA&0X3FFF]=res; //将接收到数据存入数组
UART4_RX_STA++; //数组长度变量++
if(UART4_RX_STA>(UART4_REC_LEN-1))UART4_RX_STA=0;//接受的数据长度超过数组最大值,重新开始
}
if(USART_GetITStatus(UART4,USART_IT_IDLE)) //SR寄存器的 IDLE位,判断总线是否空闲,也就是判断是否接受完数据
{
res = UART4->SR;
res = UART4->DR; //清空闲中断标志位,看中文数据操作手册:先读SR,再读DR,后清除中断
UART4_RX_STA|=0x8000;//第十五位置一
}
}
void ModeBus_Send_Data(u8* Buffer,u8 len) //发送数据
{
u8 i;
RS485_TX_RX=1;
delay_ms(10);
for(i=0;i<len;i++)
{
while(USART_GetFlagStatus(UART4,USART_FLAG_TC)==RESET); //判断是否发送完成
USART_SendData(UART4,Buffer[i]);
}
while(USART_GetFlagStatus(UART4,USART_FLAG_TC)==RESET);
delay_ms(10);
RS485_TX_RX=0;
}
//使用查表法计算CRC16 高效
//CRC高位字节值表
const u8 auchCRCHi[] = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40
} ;
/* CRC低位字节值表*/
const u8 auchCRCLo[] = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,
0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,
0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,
0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,
0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,
0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,
0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,
0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,
0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,
0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,
0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,
0x43, 0x83, 0x41, 0x81, 0x80, 0x40
} ;
//CRC16计算
u16 crc16(u8 *puchMsg, u16 usDataLen)
{
u8 uchCRCHi = 0xFF ;
u8 uchCRCLo = 0xFF ;
u32 uIndex ;
while (usDataLen--)
{
uIndex = uchCRCHi ^ *puchMsg++ ;
uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex] ;
uchCRCLo = auchCRCLo[uIndex] ;
}
return (uchCRCHi << 8 | uchCRCLo) ;
}
//------------------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------单片机做从机--------------------------------------------------------------
/*
电脑poll软件做主机
配置好后,poll软件自动发送
*/
void ModBus485_Poll(void)
{
if(UART4_RX_STA & 0x8000)
{
poll.receive_length=UART4_RX_STA & 0X3FFF; //获取STA的大小,有几次RXNE的中断,就有多少数据传输
poll.calculate_crc=crc16(UART4_RX_BUF,poll.receive_length-2); // 计算,求出poll软件发来的校验码(根据前面的数据,计算出主机发送报文格式的校验码)
poll.receive_crc=((UART4_RX_BUF[poll.receive_length-2]<<8)|(UART4_RX_BUF[poll.receive_length-1])); //将发来的检验码存入到(主机报文发送的校验码)
if(UART4_RX_BUF[0]==addr) //地址相同
{
if(poll.calculate_crc==poll.receive_crc) //检验码相同
{
switch(UART4_RX_BUF[1]) //判断是那个功能码
{
case 1:
case 2:break;
case 3:poll_mod3();break;
}
ModeBus_Send_Data(UART4_TX_BUF,UART4_TX_STA);
}
memset(UART4_RX_BUF,0,UART4_REC_LEN); //清零
memset(UART4_TX_BUF,0,UART4_REC_LEN);
UART4_RX_STA=0;
UART4_TX_STA=0;
}
sendsta=0;
}else return;
}
/*
单片机做从机模式3
向poll软件返回数据
*/
void poll_mod3(void)
{
u8 i;
slav.start_addr=((UART4_RX_BUF[2]<<8)|(UART4_RX_BUF[3]));
slav.num=((UART4_RX_BUF[4]<<8)|(UART4_RX_BUF[5]));
UART4_TX_BUF[UART4_TX_STA++]=addr; //从机地址
UART4_TX_BUF[UART4_TX_STA++]=0x03; //功能吗
UART4_TX_BUF[UART4_TX_STA++]=(slav.num*2); //一个寄存器占两个字节
for(i=0;i<slav.num;i++) //传输的寄存器数值
{
UART4_TX_BUF[UART4_TX_STA++]=ModeBus_Data[slav.start_addr+i]/256;
UART4_TX_BUF[UART4_TX_STA++]=ModeBus_Data[slav.start_addr+i];
}
slav.send_crc=crc16(UART4_TX_BUF,UART4_TX_STA); //检验码
UART4_TX_BUF[UART4_TX_STA++]=slav.send_crc/256;
UART4_TX_BUF[UART4_TX_STA++]=slav.send_crc;
}
//------------------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------单片机做主机--------------------------------------------------------------
/*
单片机做主机发送(模式3)
参数:
slave:从机的地址
start_addr:起始地址
num:寄存器个数
*/
void master_mode_3(u8 slave,u16 start_addr,u8 num)
{
u16 master_crc;
UART4_TX_BUF[0]=slave; //从机地址
UART4_TX_BUF[1]=0x03; //功能吗
UART4_TX_BUF[2]=start_addr/256; //起始地址高位
UART4_TX_BUF[3]=start_addr%256; //起始地址低位
UART4_TX_BUF[4]=num/256; //寄存器个数高位
UART4_TX_BUF[5]=num%256; //寄存器个数低位
master_crc=crc16(UART4_TX_BUF,6); //获取CRC校验位
UART4_TX_BUF[6]=master_crc/256;
UART4_TX_BUF[7]=master_crc;
ModeBus_Send_Data(UART4_TX_BUF,8);
delay_ms(100);
memset(UART4_TX_BUF,0,UART4_REC_LEN);
}
/*
单片机做主机 处理从机(slave软件)响应数据(模式3)
*/
void ModBus485_Slave(void)
{
if(UART4_RX_STA & 0x8000)
{
slave.receive_length=UART4_RX_STA & 0X3FFF; //获取STA的大小,有几次RXNE的中断,就有多少数据传输
slave.calculate_crc=crc16(UART4_RX_BUF,slave.receive_length-2); // 计算,求出poll软件发来的校验码(根据前面的数据,计算出主机发送报文格式的校验码)
slave.receive_crc=((UART4_RX_BUF[slave.receive_length-2]<<8)|(UART4_RX_BUF[slave.receive_length-1])); //将发来的检验码存入到(主机报文发送的校验码)
if(UART4_RX_BUF[0]==addr) //地址相同
{
if(slave.calculate_crc==slave.receive_crc) //检验码相同
{
switch(UART4_RX_BUF[1]) //判断是那个功能码
{
case 1:
case 2:break;
case 3:slave_mod3();break;
}
}
memset(UART4_RX_BUF,0,UART4_REC_LEN); //清零
UART4_RX_STA=0;
}
}else return;
}
/*
单片机做主机
打印出poll软件(从机)返回的数据
*/
void slave_mod3(void)
{
int count=(int)UART4_RX_BUF[2];//这是数据个数(不是寄存器个数)
printf("从机返回 %d 个寄存器数据\n",count/2);
printf("第一个寄存器的值= %d\n",(UART4_RX_BUF[3]<<8)|UART4_RX_BUF[4]);
printf("第二个寄存器的值= %d\n",(UART4_RX_BUF[5]<<8)|UART4_RX_BUF[6]);
printf("第三个寄存器的值= %d\n",(UART4_RX_BUF[7]<<8)|UART4_RX_BUF[8]);
printf("\n");
delay_ms(1000);
}
.h文件
#ifndef MODBUS_H
#define MODBUS_H
#include <stdio.h>
#include "stm32f10x.h"
#include "stdint.h"
#include "sys.h"
#define UART4_REC_LEN 200
#define RS485_TX_RX PCout(8) //RE引脚
#define addr 1
extern int sendsta;
/*
返回的数据
单片机做从机时,接收poll软件自动发送的数据
单片机做主机时,接收slave软件返回的数据
*/
typedef struct receive
{
u16 calculate_crc; //计算出主机发送的校验位
u16 receive_crc; //主机接收到的校验位
u8 receive_length; //接受的数据的长度
}modbus_receive; //接收poll软件的发送
/*
发送的数据
*/
typedef struct send
{
u16 send_crc; //从机的发送的校验码
u16 start_addr; //起始地址
u16 num; //发送寄存器的数目
}modbus_send; //单片机做主机 的模式3发送
void RS485_Init(void);
void ModeBus_Send_Data(u8* Buffer,u8 len);
void ModBus485_Poll(void);
void poll_mod3(void);
void master_mode_3(u8 slave,u16 start_addr,u8 num);
void ModBus485_Slave(void);
void slave_mod3(void);
#endif