赞
踩
因项目中需要用到485电路以及多设备通讯,采用Modbus协议通讯方式,本文写的目的就是记录笔记也提供给初学者一点参考。里面的内容可能会有错误,仅供参考。
上图是项目中的电路,也算一个最基本的485电路,没什么好讲的,这个博主讲的不错,可以参考他的。
学习以前参考以下博文链接:
Modbus协议是一种应用层报文传输协议,协议本身并没有定义物理层,所以支持多种电气接口,直接可以理解成他是软件层面的,各种的电气接口比如RS232、RS485、TCP/IP等,他们是硬件层面。他们之间互不影响。
Modbus是一主多从的通信协议
Modbus通信中只有一个设备可以发送请求。其他从设备接收主机发送的数据来进行响应,从机是任何外围设备,如I/O传感器,阀门,网络驱动器,或其他测量类型的设备。从站处理信息和使用Modbus将其数据发送给主站。
也就是说,不能Modbus同步进行通信,主机在同一时间内只能向一个从机发送请求,总线上每次只有一个数据进行传输,即主机发送,从机应答,主机不发送,总线上就没有数据通信。
从机不会自己发送消息给主站,只能回复从主机发送的消息请求。
Modbus协议同时规定了二十几种功能码,但是常用的只有3种,用于对存储区的读写,如下表所示:
我们主要就是用到03h读取从机寄存器的数据,06h对从机指定寄存器写入指定数据,10h对从机多个寄存器写入数据。
我们主要就是学习它的协议格式,主要用到3种功能码,也就对应与3种发送数据的格式。
比如 :主机发送 01 03 00 01 00 01 D5 CA
主机一共发送8个字节。
0x01:表示主机要与从机地址是0x01的设备进行通讯
0x03:功能码,代表我们发送这个指令的作用是什么,03表示我们要读取从机的数据
0x00:要读取从机寄存器地址的高位
0x01:要读取从机寄存器地址的低位
0x00:要读取从机寄存器数量的高位
0x01:要读取从机寄存器数量的低位
D5:前6位数据效验的低位
CA:前6位数据效验的高位
总得来说这段代码的含义是:查询从机地址为0x01的0x0001寄存器地址的0x0001个数据。
从机收到这段协议后,应回复如下格式 01 03 02 00 03 F8 48
从机一共发送7个字节
0x01:表示主机要与从机地址是0x01的设备进行通讯
0x03:功能码,代表我们发送这个指令的作用是什么,03表示我们要读取从机的数据
0x02:返回的数据个数(要读取的寄存器个数*2)——>返回数据的字节都是寄存器的2倍
0x00:从机返回数据的高位
0x03:从机返回数据的低位
F8 48:前面几位数据的效验码
比如 :主机发送 01 06 00 01 00 17 98 04
主机一共发送8个字节。
0x01:表示主机要与从机地址是0x01的设备进行通讯
0x03:功能码,代表我们发送这个指令的作用是什么,06表示我们要向从机写入数据
0x00:要写入从机寄存器地址的高位
0x01:要写入从机寄存器地址的低位
0x00:要写入从机数据的高位
0x17:要写入从机数据的低位
98 04:前6位数据效验码
总得来说这段代码的含义是:向从机地址为0x01的0x0001地址写入数据0x0017
从机回复格式:01 06 00 01 00 17
从机回复的内容和主机发送的一样。
比如 :主机发送 : 01 10 00 05 00 02 04 01 02 03 04 92 9F
主机一共发送11个字节。
0x01:表示主机要与从机地址是0x01的设备进行通讯
0x10:功能码,10表示我们要向从机多个寄存器写入数据
0x00:要写入从机寄存器地址的起始地址的高位
0x05:要写入从机寄存器地址的起始地址的低位
0x00:要写入寄存器个数的高位
0x02:要写入寄存器个数的低位
0x04:要写入的字节数
01 02:写入第一个寄存器的数据
03 04:写入第二个寄存器的数据
92 9F:前面数据效验码
从机回复:01 10 00 05 00 02 51 C9
从机返回的数据可以理解为就是主机发送的前6个字节加上自己数据的效验码。表面从机01从地址0x0005开始写入2个寄存器数据。
到这样发送数据协议格式介绍完毕。
Modbus只是个协议,485电路时硬件层面,通讯还是串口通讯。
连线就是485的A B连接485转USB的A B脚,485芯片的连接如上面电路所示,连接STM32串口1。
- void Serial_Init(void)
- {
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
-
- GPIO_InitTypeDef GPIO_InitStructure;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init(GPIOA, &GPIO_InitStructure);
-
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init(GPIOA, &GPIO_InitStructure);
-
- USART_InitTypeDef USART_InitStructure;
- USART_InitStructure.USART_BaudRate = 9600;
- USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
- USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
- USART_InitStructure.USART_Parity = USART_Parity_No;
- USART_InitStructure.USART_StopBits = USART_StopBits_1;
- USART_InitStructure.USART_WordLength = USART_WordLength_8b;
- USART_Init(USART1, &USART_InitStructure);
-
- USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
-
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
-
- NVIC_InitTypeDef NVIC_InitStructure;
- NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
- NVIC_Init(&NVIC_InitStructure);
-
- USART_Cmd(USART1, ENABLE);
- }
我们发送数据的格式主要就是以数组的方式。
- void Serial_SendByte(uint8_t Byte)
- {
- USART_SendData(USART1, Byte);
- while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
- }
-
- void Serial_SendArray(uint8_t *Array, uint16_t Length)
- {
- uint16_t i;
- for (i = 0; i < Length; i ++)
- {
- Serial_SendByte(Array[i]);
- }
- }
主要是将接收到的数据依次存放到对应的数组中,当modbus.reflag==1表示还有数据正在处理中,反之则进行数据存储,当开始存储第二个数据时开启定时器计时,主要目的判断接收数据是否完毕,如果超过一段时间没有数据,则表明这一次数据接收完毕
- void USART1_IRQHandler(void)
- {
- u8 st,Res;
- st = USART_GetITStatus(USART1, USART_IT_RXNE);
- if(st == SET)//接收中断
- {
- Res =USART_ReceiveData(USART1); //读取接收到的数据
-
-
- if( modbus.reflag==1) //有数据包正在处理
- {
- return ;
- }
- modbus.rcbuf[modbus.recount++] = Res;
- //USART_SendData(USART2, Res);//接受到数据之后返回给串口1
- modbus.timout = 0;
- if(modbus.recount == 1) //已经收到了第二个字符数据
- {
- modbus.timrun = 1; //开启modbus定时器计时
- }
- }
- }
配置定时器1ms进入一次中断。这里主要说明下1ms怎么配置的。
定时时间 = [(PSC +1) *(ARR+1)]/72M
这里的PSC指定就是分频倍数,ARR是溢出数,72M是时钟频率。
- void Timer_Init(void)
- {
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
-
- TIM_InternalClockConfig(TIM2);
-
- TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
- TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
- TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
- TIM_TimeBaseInitStructure.TIM_Period = 1000 - 1;
- TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //定时1ms
- TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
- TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
-
- TIM_ClearFlag(TIM2, TIM_FLAG_Update);
- TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
-
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
-
- NVIC_InitTypeDef NVIC_InitStructure;
- NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
- NVIC_Init(&NVIC_InitStructure);
-
- TIM_Cmd(TIM2, ENABLE);
- }
定时器设置1ms进入中断1次,运行时间不为0的情况下开始计时,超过8ms则表明这一次接收数据完毕,将数据接收结束标志位置1处理(modbus.reflag = 1),当数据接收完毕,则STM32可以对接收到的数据进行数据分析和处理执行相应的操作了
- void TIM2_IRQHandler(void)
- {
- if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
- {
- if(modbus.timrun != 0)//运行时间!=0表明
- {
- modbus.timout++;
- if(modbus.timout >=8)
- {
- modbus.timrun = 0;
- modbus.reflag = 1;//接收数据完毕
- }
-
- }
- TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
-
- }
- }
STM32作为从机时需要用到的数据。
- typedef struct
- {
- //作为从机时使用
- u8 myadd; //本设备从机地址
- u8 rcbuf[100]; //modbus接受缓冲区
- u8 timout; //modbus数据持续时间
- u8 recount; //modbus端口接收到的数据个数
- u8 timrun; //modbus定时器是否计时标志
- u8 reflag; //modbus一帧数据接受完成标志位
- u8 sendbuf[100]; //modbus接发送缓冲区
-
- }MODBUS;
- void Modbus_Func3(void)
- {
- uint16_t Regadd,Reglen;
- uint8_t i,j;
- //得到要读取寄存器的首地址
- Regadd = modbus.rcbuf[2]*256+modbus.rcbuf[3];//读取的首地址
- //得到要读取寄存器的数据长度
- Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];//读取的寄存器个数
- //发送回应数据包
- i = 0;
- modbus.sendbuf[i++] = modbus.myadd; //ID号:发送本机设备地址
- modbus.sendbuf[i++] = 0x03; //发送功能码
- modbus.sendbuf[i++] = ((Reglen*2)%256); //返回字节个数
- for(j=0;j<Reglen;j++) //返回数据
- {
- //reg是提前定义好的16位数组(模仿寄存器)
- modbus.sendbuf[i++] = Reg[Regadd+j]/256;//高位数据
- modbus.sendbuf[i++] = Reg[Regadd+j]%256;//低位数据
- }
- Modbus_CRC16(modbus.sendbuf,i); //计算要返回数据的CRC
- modbus.sendbuf[i++] = Modbus_RCR[0];
- modbus.sendbuf[i++] = Modbus_RCR[1];
- //数据包打包完成
- // 开始返回Modbus数据
-
- Serial_SendArray(modbus.sendbuf,i);//发送从机数据
- }
串口助手发送: 01 03 00 03 00 01 串口助手会自动计算前面的效验值,一起发送给STM32从机。查询寄存器地址为0x0003的1个数据 。
从机返回的是 :2个字节,指定地址的数据为0x0004。数据返回没问题
现在查询2个数据,发送 01 03 00 03 00 02
从机返回的是 :4个字节,指定地址的数据为0x0004和0x0005,数据返回没问题
- void Modbus_Func6(void)
- {
- u16 Regadd;//地址16位
- u16 val;//值
- u16 i;
- i=0;
- Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //得到要修改的地址
- val=modbus.rcbuf[4]*256+modbus.rcbuf[5]; //修改后的值(要写入的数据)
- Reg[Regadd]=val; //修改本设备相应的寄存器
-
- //以下为回应主机
- modbus.sendbuf[i++]=modbus.myadd;//本设备地址
- modbus.sendbuf[i++]=0x06; //功能码
- modbus.sendbuf[i++]=Regadd/256;//写入的地址
- modbus.sendbuf[i++]=Regadd%256;
- modbus.sendbuf[i++]=val/256;//写入的数值
- modbus.sendbuf[i++]=val%256;
- Modbus_CRC16(modbus.sendbuf,i);//获取crc校验位
- //crc校验位加入包中
- modbus.sendbuf[i++] = Modbus_RCR[0];
- modbus.sendbuf[i++] = Modbus_RCR[1];
-
- //数据发送包打包完毕
-
- Serial_SendArray(modbus.sendbuf,i);//发送从机数据
- }
发送前我们先查询0x0001的本来的数据为多少,再进行写入,写入后再查询是否写入成功。
串口助手首先发送:01 03 00 01 00 01
串口助手再发送:01 06 00 01 00 06
串口助手最后发送:01 03 00 01 00 01
首先回复是0x0001寄存器的数据为 0x0002
其次修改0x0001寄存器数据为0x0006
最后查询0x0001寄存器的数据就是为0x0006,表明数据写入成功。
- //这是往多个寄存器器中写入数据
- //功能码0x10指令即十进制16
- void Modbus_Func16(void)
- {
- uint16_t Regadd;//地址16位
- uint16_t Reglen;
- uint16_t i;
- Regadd= modbus.rcbuf[2]*256+modbus.rcbuf[3]; //要修改内容的起始地址
- Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];//读取的寄存器个数
-
- for(i=0;i<Reglen;i++)//向指定地址写数据
- {
- //接收数据的第7位开始是数据接收位
- Reg[Regadd+i] = modbus.rcbuf[7+i*2]*256 + modbus.rcbuf[8+i*2];
- }
- //数据写入完成,下面进行打包回复数据
-
- //回应主机的内容
- modbus.sendbuf[0]=modbus.rcbuf[0];//本设备地址
- modbus.sendbuf[1]=modbus.rcbuf[1]; //功能码
- modbus.sendbuf[2]=modbus.rcbuf[2];//写入的高位地址
- modbus.sendbuf[3]=modbus.rcbuf[3];//写入的低位地址
- modbus.sendbuf[4]=modbus.rcbuf[4]; //寄存器个数高位
- modbus.sendbuf[5]=modbus.rcbuf[5];//寄存器个数低位
- Modbus_CRC16(modbus.sendbuf,6);//获取crc校验位
- modbus.sendbuf[6]=Modbus_RCR[0]; //crc校验位加入包中
- modbus.sendbuf[7]=Modbus_RCR[1]; //crc校验位加入包中
-
- //数据发送包打包完毕
-
- //发送数据包
- Serial_SendArray(modbus.sendbuf,8);
- }
发送前我们先查询0x0001寄存器地址的2个数据为多少,再进行写入,写入后再查询是否写入成功。
串口助手首先发送:01 03 00 01 00 02
串口助手再发送:01 10 00 01 00 02 04 00 08 00 90
串口助手最后发送:01 03 00 01 00 02
首先回复是0x0001起始寄存器的数据为 0x0002, 0x0003
其次修改0x0001起始寄存器数据为0x0008,0x0009
最后查询0x0001起始寄存器的数据就是为0x0008,0x0009,表明数据写入成功。
随便百度的一个。
- unsigned int Modbus_CRC16(unsigned char *data, unsigned int len)
- {
- unsigned int i, j, tmp, CRC16;
-
- CRC16 = 0xFFFF; //CRC寄存器初始值
- for (i = 0; i < len; i++)
- {
- CRC16 ^= data[i];
- for (j = 0; j < 8; j++)
- {
- tmp = (unsigned int)(CRC16 & 0x0001);
- CRC16 >>= 1;
- if (tmp == 1)
- {
- CRC16 ^= 0xA001; //异或多项式
- }
- }
- }
- //低位在前
- Modbus_RCR[0] = (unsigned char) (CRC16 & 0x00FF);
- Modbus_RCR[1] = (unsigned char) ((CRC16 & 0xFF00)>>8);
- return CRC16;
- }
只有到数据接收完成才能进行数据处理
- void Modbus_Event(void)
- {
- //uint16_t crc,rccrc;//crc和接收到的crc
- //没有收到数据包
- if(modbus.reflag == 0) //如果接收未完成则返回空
- {
-
- return;
- }
- //收到数据包(接收完成)
- //通过读到的数据帧计算CRC
- //参数1是数组首地址,参数2是要计算的长度(除了CRC校验位其余全算)
- Modbus_CRC16(modbus.rcbuf,modbus.recount-2); //获取CRC校验位
- // 读取数据帧的CRC
- //printf("%x","%x",(crc & 0x00FF),((crc & 0xFF00)>>8));
-
- //rccrc = modbus.rcbuf[modbus.recount-2]*256+modbus.rcbuf[modbus.recount-1];//计算读取的CRC校验位
-
- //等价于下面这条语句
- //rccrc=modbus.rcbuf[modbus.recount-1]|(((u16)modbus.rcbuf[modbus.recount-2])<<8);//获取接收到的CRC
- if(Modbus_RCR[0] == modbus.rcbuf[modbus.recount-2] && Modbus_RCR[1] == modbus.rcbuf[modbus.recount-1]) //CRC检验成功 开始分析包
- {
- if(modbus.rcbuf[0] == modbus.myadd) // 检查地址是否时自己的地址
- {
- switch(modbus.rcbuf[1]) //分析modbus功能码
- {
- case 0: break;
- case 1: break;
- case 2: break;
- case 3: Modbus_Func3(); break;//这是读取寄存器的数据
- case 4: break;
- case 5: break;
- case 6: Modbus_Func6(); break;//这是写入单个寄存器数据
- case 7: break;
- case 8: break;
- case 9: break;
- case 16: Modbus_Func16(); break;//写入多个寄存器数据
- }
- }
- else if(modbus.rcbuf[0] == 0) //广播地址不予回应
- {
-
- }
- }
- modbus.recount = 0;//接收计数清零
- modbus.reflag = 0; //接收标志清零
- }
定义一个Reg数组,来模拟STM32作为从机寄存器的数据。
将STM32作为从机的地址为0x01
- uint16_t Reg[] =
- {
- 0x0001,
- 0x0002,
- 0x0003,
- 0x0004,
- 0x0005,
- };
- void Modbus_Init(void)
- {
- modbus.myadd = 0x01; //从机设备地址为
- modbus.timrun = 0; //modbus定时器停止计算
- }
- int main(void)
- {
-
- Serial_Init();
- Timer_Init();
- Modbus_Init();
-
- while (1)
- {
- Modbus_Event();
- }
- }
到此,STM32作为从机的代码写完了,也验证成功。下一部分将记录STM32作为主机给另外的一个STM32作为从机进行通讯。以上内容仅供参考。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。