Modbus程序
Modbus的功能实现应该是:
1、从机
从机应该时刻处于接收状态,以便于主机能够随时访问从机数据。具体的实现思路为:从机将485的使能位使能为接收状态,然后当接收到数据的时候将数据存储到一个接收缓存中,当接收完成标志位为1 的时候,对存储的数据进行解析。然后返回相应的数据
2、主机
上位机模仿无线模块,通过USART1向主机发送查询请求,主机接收到之后。通过485模块直接向从机发送查询/修改等命令然后再将收到的数据发送回上位机。
从机代码
一、主函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168); //延时初始化
uart_init(4800); //串口初始化波特率为115200
RS485_Init(4800);
LED_Init(); //初始化与LED连接的硬件接口
TIM3_Int_Init(100-1,8400-1);//10ms
while(1)
{
if(RecvData_Flag ==1){
Baowenjiexi();
RecvData_Flag=0;
}
}
}
在主函数中通过 uart_init(4800) 和 RS485_Init(4800);
分别设置uart1 和uart2 的波特率
二、定时器函数(定时器中断)
timer.h
#ifndef _TIMER_H
#define _TIMER_H
#include "sys.h"
extern u16 RecvData_Flag;//数据是否接收完成:0为未接收;1为接收完成
extern u16 Time_last; // 距离上次接收数据过去多少时间
extern u16 RecvData_Length;//接收到的数据长度
void TIM3_Int_Init(u16 arr,u16 psc);
#endif
timer.c
#include "timer.h"
#include "led.h"
//通用定时器3中断初始化
//arr:自动重装值。
//psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器3!
void TIM3_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); ///使能TIM3时钟
TIM_TimeBaseInitStructure.TIM_Period = arr; //自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //定时器分频
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);//初始化TIM3
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //允许定时器3更新中断
TIM_Cmd(TIM3,ENABLE); //使能定时器3
NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定时器3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x03; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
u16 RecvData_Flag=0;//数据是否接收完成:0为未接收;1为接收完成
u16 RecvData_Length=0;//接收到的数据长度
u16 Time_last=0; // 距离上次接收数据过去多少时间
//定时器3中断服务函数
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
{
if(Time_last<60000){
Time_last++;
}
if((Time_last>10)&&(RecvData_Length!=0)){
RecvData_Flag=1;
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
}
通过使用定时器中断当延时到达十毫秒并且接收数据的长度不等于零的时候我们就认定信息接收完毕,就将接收完毕状态位RecvData_Flag
置为1
三、Modbus相关函数
modbus.h
#ifndef _MODBUS_H
#define _MODBUS_H
#include "sys.h"
extern u8 SendBuf[100];
void Baowenjiexi(void);
void Data_Send(u8 *sendbuf,u16 send_len);
void Data_Send1(u8 *sendbuf,u16 send_len);
u16 get_CRC(u8 *buf,u16 len);
#endif
modbus.c
#include "modbus.h"
#include "usart.h"
#include "timer.h"
#include "crc16.h"
#include "rs485.h"
u8 Modbus_SendBuf[100];
u8 Modbus_RecvBuf[100];
u16 modbus_io[100];
u16 modbus_id=1; //id
u16 modbus_code;//modbus功能码
u16 modbus_CRC;//校验位
u16 modbus_Package_sum=0;
u16 i,t;
void Data_Send(u8 *sendbuf,u16 send_len)
{
RS485_TX_EN=1;
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
for(t=0;t<send_len;t++)
{
USART_SendData(USART2, sendbuf[t]); //向串口2发送数据
while(USART_GetFlagStatus(USART2,USART_FLAG_TC)!=SET);//等待发送结束
}
RS485_TX_EN=0;
}
void Data_Send1(u8 *sendbuf,u16 send_len)
{
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
for(t=0;t<send_len;t++)
{
USART_SendData(USART1, sendbuf[t]); //向串口1发送数据
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
}
}
void Baowenjiexi(){
//printf("%02X",RecvData_Length);
Data_Send1(Modbus_RecvBuf,RecvData_Length);
//RecvData_Length=0;
//需增加校验位计算
modbus_CRC=crc16tablefast(Modbus_RecvBuf,RecvData_Length-2);
//printf("0x%02X\r\n",modbus_CRC);
//printf("0x%02X\r\n",Modbus_RecvBuf[RecvData_Length-2]<<8|Modbus_RecvBuf[RecvData_Length-1]);
//printf("\r\n报文校验...\r\n");
if(modbus_CRC==(Modbus_RecvBuf[RecvData_Length-2]<<8|Modbus_RecvBuf[RecvData_Length-1]))//校验是否通过
{
//printf("\r\n报文校验通过:\r\n");
if(modbus_id==Modbus_RecvBuf[0])
{
//printf("\r\nID通过\r\n");
modbus_code=Modbus_RecvBuf[1];
switch(modbus_code){
case 3:
//主机要求从机反馈一些内容
Modbus_SendBuf[0]=modbus_id;//ID
Modbus_SendBuf[1]=3; //功能码
Modbus_SendBuf[2]=(Modbus_RecvBuf[4]<<8|Modbus_RecvBuf[5])*2;//字节码长度=寄存器长度x2
for(modbus_Package_sum=0;modbus_Package_sum<Modbus_SendBuf[2];modbus_Package_sum+=2)
{
Modbus_SendBuf[3+modbus_Package_sum]=modbus_io[Modbus_RecvBuf[4]<<8|Modbus_RecvBuf[5]+modbus_Package_sum/2>>8];
Modbus_SendBuf[4+modbus_Package_sum]=modbus_io[Modbus_RecvBuf[4]<<8|Modbus_RecvBuf[5]+modbus_Package_sum/2];
}
Modbus_SendBuf[3+modbus_Package_sum]=crc16tablefast(Modbus_SendBuf,3+modbus_Package_sum)>>8;
Modbus_SendBuf[4+modbus_Package_sum]=crc16tablefast(Modbus_SendBuf,3+modbus_Package_sum);
Data_Send(Modbus_SendBuf,5+modbus_Package_sum);
RecvData_Length=0;
break;
case 6:
for(i=0;i<RecvData_Length;i++)
{
Modbus_SendBuf[i] = Modbus_RecvBuf[i];
}
Data_Send(Modbus_SendBuf,RecvData_Length);
RecvData_Length=0;
break;
case 16:
for(i=0;i<RecvData_Length;i++)
{
Modbus_SendBuf[i] = Modbus_RecvBuf[i];
}
Data_Send(Modbus_SendBuf,RecvData_Length);
RecvData_Length=0;
break;
}
}
}else{
//printf("\r\n报文校验未通过:\r\n");
RecvData_Length=0;
}
}
Data_Send(u8 *sendbuf,u16 send_len)
以及Data_Send1(u8 *sendbuf,u16 send_len)
函数分别表示向串口2以及串口1发送数据,数据的内容为sendbuf的内容,而send_len则代表发送数据的长度。
Baowenjiexi()
是执行报文解析的函数,其是根据报文的格式进行报文拆分,由于接收到的报文是存储在Modbus_RecvBuf
(例如:0x01 0x03 0x00 0x00 0x00 0x0A 0xC5 0xCD)中的,在Modbus格式中Modbus_RecvBuf[0]
(0x01)存储的是要进行通讯的地址,第二个字节Modbus_RecvBuf[1]
(0x03)是操作的类型,再往后的两个字节( 0x00 0x00)是代表主机要跟哪个id的从机通讯,最后两个字节(0xC5 0xCD)是校验码。在报文解析中,首先要校验的就是校验码,这决定了从机收到的信息是否正确。如果校验码正确,则对比当前收到的报文的id信息和本机的id信息是否一致,一致的话就进行相应操作码的反馈。
四、CRC校验码
CRC16.h
#ifndef __CRC16_H
#define __CRC16_H
#include "stm32f4xx.h"
//////////////////////////////////////////////////////////////////
//变量声明
extern uint8_t crc16_data[];
//////////////////////////////////////////////////////////////////
//函数声明
uint16_t crc16tablefast(uint8_t *ptr, uint16_t len);
#endif
CRC16.c
#include "crc16.h"
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef int int32_t;
const uint16_t polynom = 0xA001
uint16_t crc16bitbybit(uint8_t *ptr, uint16_t len)
{
uint8_t i;
uint16_t crc = 0xffff;
if (len == 0) {
len = 1;
}
while (len