目录
前言
首先回顾一下上一节介绍的Modbus通信协议基本理论,首先介绍了Modbus通信协议的主从通信模式特点,分析了Modbus通信的传输特点;其次介绍了两种Modbus通信协议基本的数据格式:Modbus-RTU协议和Modbus-ASCLL协议。Modbus通信协议是在RS-485串口实验的基础上实现的,简单说就是首先要实现RS-485的串口通信,对所收发的数据串按照Modbus的规则编写(比作数据的加密处理)因此在程序编写上主要分为3个步骤:1.实现1ms中断计时的定时器;2.实现发送和接收数据的串口;3.Modbus程序编写。本节将本着从理论落实到实践的角度对Modbus通信协议进行代码实现。
1. 硬件介绍
1.2 硬件电路介绍
微处理器选用:STM32F103;
RS485_IN和RS485_OUT为收发引脚:选用MCU的PB10和PB11引脚作为RS-485的接收引脚和发送引脚;
RS485_DE为收发状态控制引脚:当RS485_DE为高电平时,芯片处于发送状态;当RS485_DE为低电平时,芯片处于接收状态;
阻抗匹配电路:R44、R45、R46为阻抗匹配电阻
1.2 硬件通信平台
Modbus通信实验平台搭建如下:将USB串口准换成TTL电平,再将TTL电平转换为RS-485差分信号之后连接于STM32从机1。
Modbus通信实验调试软件如下:
下载链接链接:https://pan.baidu.com/s/1ccJkBmZJQhuChypKy-qoug
提取码:ppe1
2. 软件介绍
要想实现Modbus的程序,首先应当完成三件事:
(1)实现1ms中断计时的定时器;
(2)实现发送和接收数据的串口;
(3)Modbus程序编写。
2.1 定时器程序设计
利用TIM2实现1ms的定时中断功能。
2.1.1 配置时钟函数
/******************************************************************
功能: 配置时钟函数
******************************************************************/
//通用定时器 2 中断初始化
void Timer2_Init() //1ms产生1次更新事件
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;//结构体类型初始化:包含自动重装载值,分频系数,计数方式
//①TIM2时钟使能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_DeInit(TIM2);
//定时器TIM2初始化
TIM_TimeBaseStructure.TIM_Period=1000-1; // 自动重装载周期 1ms
TIM_TimeBaseStructure.TIM_Prescaler=72-1; // 分频系数72M/72=1MHZ-->1us
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; //设置时钟分频因子为不分频
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //计数方式为向上计数
//②初始化定时器参数
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
//③设置TIM2允许更新中断
TIM_ITConfig(TIM2, TIM_IT_Update,ENABLE);//使能TIM2更新中断
//④使能TIM2
TIM_Cmd(TIM2,ENABLE);
}
2.1.2 定时器中断服务子程序
/******************************************************************
功能: 定时器中断服务子程序
******************************************************************/
void TIM2_IRQHandler() //定时器2的中断服务子函数 1ms一次中断
{
u8 st;
st= TIM_GetFlagStatus(TIM2, TIM_FLAG_Update); //检测TIM2中断更新标志位
if(st==SET) //如果TIM2满足中断标志
{
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除TIM2中断更新标志位
//每一毫秒所要执行的任务
if(modbus.timrun!=0)
{
modbus.timout++;
if(modbus.timout>=4) //间隔时间达到了4毫秒时间
{
modbus.timrun=0;//关闭定时器--停止定时
modbus.reflag=1; //收到一帧数据
}
}
}
}
2.2 串口程序设计
从库函数操作层面结合寄存器的描述来设置串口,以达到我们最基本的通信功能。串口设置的一般步骤可以总结为如下几个步骤:
(1)串口时钟使能, GPIO 时钟使能;
(2)串口复位;
(3)GPIO 端口模式设置;
(4)串口参数初始化;
(5)开启中断并且初始化 NVIC;
(6)使能串口;
(7)编写中断处理函数。
2.2.1 配置串口函数:
void RS485_Init()
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
//①串口时钟使能, GPIO 时钟使能,复用时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO, ENABLE); //开启USART2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); //开启USART2时钟
//②串口复位
USART_DeInit(USART2); //串口2复位
//RS485DE引脚初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_ModeGPIO_Mode_Out_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.5
RS485_RT_0; //使MAX485芯片处于接收状态(收发控制引脚)
//③GPIO端口模式设置
//初始化485串口引脚以及串口配置
//USART1_TX PB.10
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.2
//USART1_RX PB.11
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.3
//④串口参数初始化、结构体指针成员变量
USART_InitStructure.USART_BaudRate = 9600; //设置波特率为9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为 8 位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1; //一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No; //无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART2, &USART_InitStructure); //串口参数初始化
//⑤开启中断
USART_ITConfig(USART2,USART_IT_RXNE,ENABLE); //开启串口响应中断,USART_IT_RXNE接收到数据中断
//⑥使能串口
USART_Cmd(USART2, ENABLE);//串口使能
USART_ClearFlag(USART2,USART_FLAG_TC ); //清除串口TC发送完成中断标志
2.2.2 初始化中断服务子程序:
/******************************************************************
功能: 初始化NVIC
******************************************************************/
//⑤初始化NVIC(定时器中断+串口中断)
void NVIC_Init()
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 中断优先级
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //定时器产生更新事件中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //子优先级2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure);
}
2.2.3 串口中断响应事件:
/******************************************************************
功能: Modbus3字节接收中断处理
******************************************************************/
void USART2_IRQHandler() //MODBUS字节接收中断
{
u8 st,sbuf;
st=USART_GetITStatus(USART2, USART_IT_RXNE); //判断读寄存器是否非空(RXNE)
if(st==SET) //返回值是 SET,说明是串口接收到数据完成中断发生
{
sbuf=USART2->DR;
if( modbus.reflag==1) //有数据包正在处理
{
return ;
}
modbus.rcbuf[modbus.recount++]=sbuf;//利用数组存放接收的数据
modbus.timout=0;
if(modbus.recount==1) //收到主机发来的一帧数据的第一字节
{
modbus.timrun=1; //启动定时
}
}
}
2.3 modbus程序编写
2.2.1 crc16较验程序
根据crc16的高位字节值表和低位字节值表编写校验程序。
/******************************************************************
功能: CRC16校验
******************************************************************/
uint crc16( uchar *puchMsg, uint usDataLen )
{
uchar uchCRCHi = 0xFF ; // 高CRC字节初始化
uchar uchCRCLo = 0xFF ; // 低CRC 字节初始化
unsigned long uIndex ; // CRC循环中的索引
while ( usDataLen-- ) // 传输消息缓冲区
{
uIndex = uchCRCHi ^ *puchMsg++ ; // 计算CRC
uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex] ;
uchCRCLo = auchCRCLo[uIndex] ;
}
return ( uchCRCHi << 8 | uchCRCLo ) ;
}
2.3.2 Modbus宏定义
#define RS485_RT_1 GPIO_SetBits(GPIOA, GPIO_Pin_5) //485发送状态
#define RS485_RT_0 GPIO_ResetBits(GPIOA, GPIO_Pin_5) //485置接收状态
typedef struct
{
u8 myadd; //本设备的地址
u8 rcbuf[64]; //Modbus接收缓冲区64个字节
u16 timout; //Modbus的数据断续时间
u8 recount; //Modbus端口已经收到的数据个数
u8 timrun; //Modbus定时器是否计时的标志
u8 reflag; //收到一帧数据的标志
u8 Sendbuf[64]; //Modbus发送缓冲区
}
MODBUS;
2.3.3 Modbus函数初始化
/******************************************************************
功能: Modbus函数初始化
******************************************************************/
void Modbus_Init()
{
modbus.myadd=4; //本从设备的地址
modbus.timrun=0; //Modbus定时器停止计时
RS485_Init();
}
2.3.4 Modbus事件函数
/******************************************************************
功能: Modbus事件函数
******************************************************************/
void Mosbus_Event()
{
u16 crc;
u16 rccrc;
if(modbus.reflag==0) //没有收到Modbus的数据
{
return ;
}
crc= crc16(&modbus.rcbuf[0], modbus.recount-2); //计算校验码,-2去除两位校验码
rccrc=modbus.rcbuf[modbus.recount-2]*256 + modbus.rcbuf[modbus.recount-1]; //收到的校验码
if(crc == rccrc) //数据包符号CRC校验规则
{
if(modbus.rcbuf[0] == modbus.myadd) //确认数据包是否是发给本设备的 确认接收的地址是本机地址
{
switch(modbus.rcbuf[1]) //分析功能码
{
case 0: break;
case 1: break;
case 2: break;
case 3: Modbud_fun3(); break; //3号功能码处理
case 4: break;
case 5: break;
case 6: Modbud_fun6(); break; //6号功能码处理
case 7: break;
//....
}
}
else if(modbus.rcbuf[0] == 0) //如果是广播地址则不处理
{
}
}
modbus.recount=0;
modbus.reflag=0;
}
2.3.5 Modbus读功能码处理
/******************************************************************
功能: Modbus3号功能码处理
******************************************************************/
void Modbud_fun3() //3号功能码处理---主机要读取本从机的寄存器
{
u16 Regadd; //寄存器起始地址
u16 Reglen; //寄存器个数
u16 byte;
u16 i,j;
u16 crc;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //得到要读取的寄存器的首地址
Reglen=modbus.rcbuf[4]*256+modbus.rcbuf[5]; //得到要读取的寄存器的数量
i=0;
modbus.Sendbuf[i++]=modbus.myadd; //本设备地址
modbus.Sendbuf[i++]=0x03; //功能码
byte=Reglen*2; //要返回的数据字节数
modbus.Sendbuf[i++]=byte%256;
for(j=0;j<Reglen;j++)
{
modbus.Sendbuf[i++]=Reg[Regadd+j]/256;
modbus.Sendbuf[i++]=Reg[Regadd+j]%256;
}
crc=crc16(modbus.Sendbuf,i);
modbus.Sendbuf[i++]=crc/256; //
modbus.Sendbuf[i++]=crc%256;
RS485_RT_1;
for(j=0;j<i;j++)
{
RS485_byte(modbus.Sendbuf[j]);
}
RS485_RT_0;
}
2.3.6 Modbus写单个寄存器功能码处理
/******************************************************************
功能: Modbus6写单个寄存器功能码处理
******************************************************************/
void Modbud_fun6() //6号功能码处理
{
u16 Regadd;
u16 val;
u16 i,crc,j;
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;
crc=crc16(modbus.Sendbuf,i);
modbus.Sendbuf[i++]=crc/256; //
modbus.Sendbuf[i++]=crc%256;
RS485_RT_1;
for(j=0;j<i;j++)
{
RS485_byte(modbus.Sendbuf[j]);
}
RS485_RT_0;
}
2.3.7 主函数
/******************************************************************
功能: 主函数
******************************************************************/
u16 Reg[]={0x0000, //本设备寄存器中的值
0x0001,
0x0002,
0x0003,
0x0004,
0x0005,
0x0006,
0x0007,
0x0008,
0x0009,
0x000A,
};
int main()
{
Timer2_Init(); //初始化定时器Timer2
Mosbus_Init(); //初始化定时器中断
Isr_Init();
while(1)
{
Mosbus_Event(); //处理MODbus数据
}
}
往期博客:
来源:oschina
链接:https://my.oschina.net/u/4265074/blog/4262241