modbus 协议是应用于电子控制器上的一种通用协议,它已经成为通用工业标准。只要遵循此协议,不同厂商生产的控制设备可以连成工业网络,进行集中控制。modbus协议能实现控制器互相之间、控制器经网络和设备之间进行通信。modbus协议是请求响应模式(应答),即控制器向设备发起访问请求,然后设备进行响应。 modbus协议也是主从通信,所以请求只能由主机发起,从设备不能主动发起通信请求,从设备和从设备之间不能进行通信。
modbus协议的基本单元是信息帧,RTC在modbus中的帧结构如下:
从设备在接收到主设备发送过来的请求后,若发现由通讯、无法处理、地址不对等问题,则返回一个错误性质的消息。消息的异常类型如下图,而出现由于通信问题引起的无法接受数据,主设备只能依赖超时处理。超时后主设备会再次发送一次查询动作。异常和正常返回的主要区别有两个:正常返回时,功能码使用从设备接受到的,异常则最高位置1;正常返回时数据域返回响应的数据,异常则只填写一个异常码。
以下列举常见功能的数据帧格式,使用modbusRTU协议通讯,主要是为了获取数据。这里说下获取寄存器的数值以及设置寄存器的数值。注意使用的寄存器是16bit
Modbus Poll :Modbus主机仿真器,用于测试和调试Modbus从设备。该软件支持ModbusRTU、ASCII、TCP/IP。用来帮助开发人员测试Modbus从设备,或者其它Modbus协议的测试和仿真。它支持多文档接口,即,可以同时监视多个从设备/数据域。每个窗口简单地设定从设备ID,功能,地址,大小和轮询间隔。你可以从任意一个窗口读写寄存器和线圈。如果你想改变一个单独的寄存器,简单地双击这个值即可。或者你可以改变多个寄存器/线圈值。提供数据的多种格式方式,比如浮点、双精度、长整型(可以字节序列交换)。这个软件能在百度上搜索直接下载。
在stm32中使用串口usart1来现实modbus传输协议。由于串口传输信息是把一连串的数据都发送出去,所以数据什么时候结束,咱们并不知道。这里使用modbus协议中的帧和帧之前至少要有3.5字节的时间间隙来做帧的分隔。
设计的流程是:设备开启后初始化串口、系统时钟、以及基础定时器6和7。串口的配置主要有开启串口的接受中断使能、校验中断使能。在串口中断服务函数中把接受到的数据保存到数组中,同时写系统定时器的val寄存器(这里从新开始计时)。系统定时器配置为3.5个字节的传输时间为定时时间,定时器中断触发,说明最后接受到的数据到现在已经超过了3.5个字节的时间间隙,可以认为上一帧数据传输结束。在定时器中断服务函数中处理接受到的帧,并响应主机。数据接受保存在120个字节的数组中,并用len标记其长度。
void ConfigUsart(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
GPIO_CLK_FUNC(GPIO_CLK_ENABLE,ENABLE);
U_CLK_FUNC(U_CLK_ENABLE,ENABLE);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStruct.GPIO_Pin = RX_PIN;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(RX_PORT, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = TX_PIN;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(TX_PORT, &GPIO_InitStruct);
USART_InitStruct.USART_BaudRate = U_BAUDRATE;
USART_InitStruct.USART_WordLength = U_WORDLENGTH;
USART_InitStruct.USART_Parity = U_PARITY;
USART_InitStruct.USART_StopBits =U_STOPBITS;
USART_InitStruct.USART_Mode = U_MODE;
USART_InitStruct.USART_HardwareFlowControl =U_HDFC;
USART_Init(USARTx, &USART_InitStruct);
}
void InitUsart(void)
{
ConfigUsart();
USART_ITConfig(USARTx,U_IT,ENABLE);
USART_ClearFlag(USARTx,U_IT);
nvic_usart_config();
USART_Cmd(USARTx, ENABLE);
}
串口中断和基本定时器的配置
void nvic_usart_config(void)
{
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 3;
NVIC_Init(&NVIC_InitStruct);
}
void nvic_time6_config(void)
{
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
NVIC_InitStruct.NVIC_IRQChannel = TIM6_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 4;
NVIC_Init(&NVIC_InitStruct);
}
系统定时器的配置
SysTick_Config(time_val);
/*time_val是根据波特率计算出来的3.5个字节传输的定时时间*/
串口中断服务函数,这里主要是接收数据,存放在数组recvz_data中,并用len标记一帧数据的长度
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET){
/*这里写系统定时器的VAL,val值会清零从新计时。只要接收到数据就重置为3.5个字节的定时时间*/
SysTick->VAL = 0xff;
/*recv_occur用来标记串口接收到数据了、len标记数组中有效数据的长度,这里暂时没有把校验中断的处理补上,后面再补上*/
recv_occur = 1;
recv_data[len++] = USART1->DR&0xff;
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
系统定时器的中断服务函数,系统定时器中断触发说明接收最后一个字节到现在已经过了3.5个字节的时间间隙,可以认为上一个帧数据传输已经结束。
void SysTick_Handler(void)
{
if(recv_occur == 1){
/*recv_occur 用来标记是否接收了数据,因为系统定时器在开机的时候就配置了。 */
respend_resquest();
recv_occur = 0;
/*处理响应后,把数组中的数据清除,接收到的数据从数组的头部开始存放*/
len = 0;
}
}
基本的架构流程就是上述的。然后就是响应主设备的请求函数。CRC计算方法在文末,要是想检验是否正确,可以使用CRC在线计算。
uint16_t GenerateCrc(uint8_t *c,int num)
{
uint16_t crc = 0xffff,code = 0xa001;
char i,j;
for(i = 0;i< num;i++){
crc ^= c[i];
for(j = 0;j < 8;j++){
if(crc&1){
crc>>=1;
crc^=code;
}else{
crc>>=1;
}
}
}
return crc;
}
void respend_resquest(void)
{
uint8_t i=0,j = 0;
uint16_t crc,data,tempcrc;
function = recv_data[1];
if((recv_data[0] != local_addr) && (recv_data[0] != 0)){
/*校验是否发给自己,即从设备的地址是否合格*/
send_info[i++] = local_addr;
send_info[i++] = function|0x80;
send_info[i++] = 2;
crc = GenerateCrc(send_info,i);
send_info[i++] = crc&0xff;
send_info[i++] = crc>>8&0xff;
SendNo((char*)send_info,i);
return;
}
crc = GenerateCrc(recv_data,len-2);
tempcrc = (recv_data[len-2] & 0xff)|((recv_data[len-1]&0xff) <<8);
/*校验传输中是否收到干扰*/
if(tempcrc != crc){
send_info[i++] = local_addr;
send_info[i++] = function|0x80;
send_info[i++] = 8;
crc = GenerateCrc(send_info,i);
send_info[i++] = crc&0xff;
send_info[i++] = crc>>8&0xff;
SendNo((char*)send_info,i);
return;
}
/*由于时间有限,这里只实现了部分功能码,要是想实现别的,直接添加case xx:就可以*/
switch(function){
case 03:
send_info[i++] = local_addr;
send_info[i++] = function;
reg_no = recv_data[i]<<8|recv_data[i+1];
reg_count = recv_data[i+2]<<8 | recv_data[i+3];
if(reg_no < 0 || reg_no >=10 || reg_no+reg_count > 10){
send_info[i++] = 3;
}else{
respond = reg_count*2;
//send_info[i++] = respond>>8&0xff;
send_info[i++] = respond & 0xff;
for(j = 0;j < reg_count;j++){
send_info[i++] = test_reg[reg_no+j]>>8&0xff;
send_info[i++] = test_reg[reg_no+j]&0xff;
}
}
break;
case 04:
break;
case 06:
send_info[i++] = local_addr;
send_info[i++] = function;
reg_no = recv_data[i]<<8|recv_data[i+1];
data = recv_data[i+2]<<8|recv_data[i+3];
if(reg_no < 0 || reg_no >= 10 /*|| reg_no + reg_count > 6*/){
send_info[i++] = 3;
}else{
test_reg[reg_no] = data;
send_info[i++] = reg_no>>8&0xff;
send_info[i++] = reg_no&0xff;
send_info[i++] = test_reg[reg_no]>>8&0xff;
send_info[i++] = test_reg[reg_no]&0xff;
}
break;
case 16:
break;
default:
send_info[i++] = local_addr;
send_info[i++] = function|0x80;
send_info[i++] = 1;
}
crc = GenerateCrc(send_info,i);
send_info[i++] = crc&0xff;
send_info[i++] = crc>>8&0xff;
SendNo((char*)send_info,i);
}
void SendNo(char* c,char num)
{ /*这个函数用于把生成好的信息帧返回给主设备*/
char i = 0;
for(i = 0; i < num;i++){
SendChar(c[i]);
}
while(USART_GetFlagStatus(USARTx,USART_FLAG_TC) == RESET);
}
上面就是M3使用串口实现的modbus通讯步骤和流程。当然实现的方法有很多,希望有好的想法的大神不吝赐教。编译通过后,使用上述讲解中的modbus poll来检测,结果如下。
到这里基本的通讯部分功能已经实现。后续把整个项目的流程都会写出来的。