基于STM32f103的modbus协议485通信(从机)

目录

  • 简介
    • 1. 上层协议
    • 2. 自己定义的协议
      • 1. 保存
      • 2. 发送
      • 3. 接收
      • 4. 解析
  • Modbus
    • 1. modbus基于232 485 以太网的上层协议
    • 2. 通信方式 --请求 -- 响应
    • 3.数据长度
    • 4. 数据模型:
    • 5. 事务处理过程
    • 6.异常码
    • 7.功能码
  • Modbus: 数据格式
    • 1. Modbus通信模式
    • 2. 数据格式:RTU ASCII
    • 3. STM32如何判断RTU报文帧数据传输完成 -- 3.5ms
  • CRC
  • 程序

简介

MODBUS 是 OSI 模型第 7 层上的应用层报文传输协议,它在连接至不同类型总线或网络的设备之间提供客户机/服务器通信。[^1]

1. 上层协议

串口——位协议——位
基于STM32f103的modbus协议485通信(从机)_第1张图片

2. 自己定义的协议

帧头、地址信息、数据长度、数据块、检验码、帧尾
1A1B——1————len——……——ab cd——A1 B1
在这里我把地址信息设为每个MCU的ID,检验码为计算得到的CRC校验码

1. 保存

数组、结构体
Typedef struct{
U8 head[2];
U8 id;//保存设备地址
U16 lenth;
U8 data[64];//保存接收到的数据
U8 jiaoyan[2];
U8 end[2];
}XXX;

2. 发送

——循环
我这里还没上系统,所以开的是时间片

3. 接收

串口中断+DMA 、空闲中断 、 定时器的溢出中断

4. 解析

——状态机
由自己定义的数据解析方式

Modbus

1. modbus基于232 485 以太网的上层协议

基于STM32f103的modbus协议485通信(从机)_第2张图片
数据帧格式:
基于STM32f103的modbus协议485通信(从机)_第3张图片

2. 通信方式 --请求 – 响应

请求:功能码+数据
正常响应:操作码+数据
基于STM32f103的modbus协议485通信(从机)_第4张图片
异常响应:差错码+异常码
基于STM32f103的modbus协议485通信(从机)_第5张图片

3.数据长度

对串行链路通信来说,MODBUS PDU=256-服务器地址(1字节)-CRC(2字节)=253字节
从而:
RS232/RS485 ADU=253字节+服务器地址(1byte)+CRC(2字节)=256字节。
TCP MODBUS ADU=249字节+MBAP(7字节)=256字节。

4. 数据模型:

基于STM32f103的modbus协议485通信(从机)_第6张图片
离散量输入:按键、热释电
线圈:LED、BEEP、继电器
输入寄存器:光照、噪声、空气质量
保持寄存器:DHT11、OLED、W25Q

5. 事务处理过程

基于STM32f103的modbus协议485通信(从机)_第7张图片

6.异常码

基于STM32f103的modbus协议485通信(从机)_第8张图片

7.功能码

公共功能码、用户自定义功能码、保留功能码
基于STM32f103的modbus协议485通信(从机)_第9张图片
03 功能码:
请求 —— 响应
在这里插入图片描述
基于STM32f103的modbus协议485通信(从机)_第10张图片
在这里插入图片描述
请求:03 00 00 00 03
响应:03 06 00 37 00 50 ….
基于STM32f103的modbus协议485通信(从机)_第11张图片

Modbus: 数据格式

在这里插入图片描述

1. Modbus通信模式

单播、广播
基于STM32f103的modbus协议485通信(从机)_第12张图片
在这里插入图片描述

2. 数据格式:RTU ASCII

基于STM32f103的modbus协议485通信(从机)_第13张图片
在这里插入图片描述

3. STM32如何判断RTU报文帧数据传输完成 – 3.5ms

基于STM32f103的modbus协议485通信(从机)_第14张图片

CRC

生成 CRC 的过程为:

  1. 将一个 16 位寄存器装入十六进制 FFFF (全 1). 将之称作 CRC 寄存器.
  2. 将报文的第一个 8 位字节与 16 位 CRC 寄存器的低字节异或,结果置于 CRC 寄存器.
  3. 将 CRC 寄存器右移 1 位 (向 LSB 方向), MSB 充零. 提取并检测 LSB.
  4. (如果 LSB 为 0): 重复步骤 3 (另一次移位).
    (如果 LSB 为 1): 对 CRC 寄存器异或多项式值 0xA001 (1010 0000 0000 0001).
  5. 重复步骤 3 和 4,直到完成 8 次移位。当做完此操作后,将完成对 8 位字节的完整操作。
  6. 对报文中的下一个字节重复步骤 2 到 5,继续此操作直至所有报文被处理完毕。
  7. CRC 寄存器中的最终内容为 CRC 值.
  8. 当放置 CRC 值于报文时,高低字节必须交换。

程序

#include "modbus.h"
#include "rs485.h"
#include "stdio.h"
#include "crc_16_tab.h"
#include "crc_8_tab.h"

MODBUS modbus_slave={.id=0x01,.rxcount=0,.rxover=0,.txcount=0};

uint8_t Get_ID(void)
{
	u8 id=0;
	u8 *p = (u8 *)0x1FFFF7E8;
	id = CRC_8_Tab(p,12);
	return id;
}

void Modbus_Config(uint32_t brr)
{
	u8 ID;
	RS485_Config(brr);
	TIM3_Config(72,4000);  //>3.6  --4ms
	ID = Get_ID();
	modbus_slave.id = ID;
	printf("ID=%d\r\n",modbus_slave.id);
}

void TIM3_Config(u16 psc,u16 arr)
{
	NVIC_InitTypeDef NVIC_InitStruct;
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //TIM3时钟使能
	
	TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频因子 -- 用于输入捕获
	TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
	TIM_TimeBaseInitStruct.TIM_Period = arr-1;  //重装载值
	TIM_TimeBaseInitStruct.TIM_Prescaler = psc -1; //分频值
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct); //初始化TIM3时基单元
	TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);//更新中断
	
	NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn;//TIM3中断
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //中断通道使能
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;//次级
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;//占先
	NVIC_Init(&NVIC_InitStruct);//初始化 NVIC 寄存器
	
	TIM_Cmd(TIM3,DISABLE);//失能TIM3
}

void USART3_IRQHandler(void)
{
	if(USART_GetITStatus(USART3,USART_IT_RXNE))
	{	
		USART_ClearFlag(USART3,USART_FLAG_RXNE);
		modbus_slave.rxbuff[modbus_slave.rxcount++] = USART_ReceiveData(USART3);
		if(modbus_slave.rxcount ==1)
		{
			TIM_Cmd(TIM3,ENABLE);//启动TIM3
		}
		TIM_SetCounter(TIM3,0);//计数器清0
	}
}

void TIM3_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM3,TIM_IT_Update))
	{
		TIM_ClearFlag(TIM3,TIM_FLAG_Update);
		TIM_Cmd(TIM3,DISABLE);//失能TIM3
		modbus_slave.rxover = 1;
	}
}

void Modbus_EvenPoll(void)
{
	u8 i=0;
	u16 rx_crc =0; //存放接收到的CRC的值
	u16 current_crc = 0;//存放计算的CRC的值
	if(modbus_slave.rxover==0)return;//未接收完成
	if(modbus_slave.rxcount<4)goto MODBUS_REEOR;//解决TIM bug问题
		//测试
		for(i=0;i<modbus_slave.rxcount;i++)
		{
			printf("%x\t",modbus_slave.rxbuff[i]);
		}
		printf("\r\n");
		
		//判断CRC
		rx_crc = (modbus_slave.rxbuff[modbus_slave.rxcount-1]<<8) | modbus_slave.rxbuff[modbus_slave.rxcount-2];
		//计算的CRC的值
		current_crc = CRC_16_Tab(modbus_slave.rxbuff,modbus_slave.rxcount-2);
		if(current_crc != rx_crc)goto MODBUS_REEOR;
		//判断ID -- 是否是广播地址 和 本身地址
		if((modbus_slave.rxbuff[0] != modbus_slave.id)&&(modbus_slave.rxbuff[0] != 0x00))goto MODBUS_REEOR;
		//功能码
		switch(modbus_slave.rxbuff[1])
		{
			case 0x00:break;
			case 0x01:break;//读线圈
			case 0x02:break;//读输入离散量
			case 0x03:Modbus_Funtion03();break;
			default:Modbus_FunctionError(modbus_slave.rxbuff[1],0x01);break;//错误
		}
		//发送数据
		RS485_SendData(modbus_slave.txbuff,modbus_slave.txcount);

MODBUS_REEOR:
	modbus_slave.txcount = 0;
	modbus_slave.rxcount = 0;
	modbus_slave.rxover = 0;

}

void Modbus_Function00(void)
{
	u16 tx_crc = 0;
  //设备ID
	modbus_slave.txbuff[modbus_slave.txcount++] = modbus_slave.id;
	//功能码
	modbus_slave.txbuff[modbus_slave.txcount++] = 0x00;
	//数据码
	modbus_slave.txbuff[modbus_slave.txcount++] = 0x00;
	//crc校验
	tx_crc =CRC_16_Tab(modbus_slave.txbuff,modbus_slave.txcount);
	//将crc值存放发送缓冲区
	modbus_slave.txbuff[modbus_slave.txcount++] = (tx_crc & 0xff);
	modbus_slave.txbuff[modbus_slave.txcount++] = (tx_crc & 0xff00)>>8;
}

void Modbus_FunctionError(u16 error_code,u16 abnormal)
{
	u16 tx_crc =0;
	//设备id
	modbus_slave.txbuff[modbus_slave.txcount++] = modbus_slave.rxbuff[0];
	//差错码
	modbus_slave.txbuff[modbus_slave.txcount++] = error_code | 0x80;
	//异常码
	modbus_slave.txbuff[modbus_slave.txcount++] = abnormal;
	//CRC校验
	//计算CRC
	tx_crc = CRC_16_Tab(modbus_slave.txbuff,modbus_slave.txcount);
	//将CRC校验值写入发送缓存区
	modbus_slave.txbuff[modbus_slave.txcount++] = tx_crc & 0xff;//低位
	modbus_slave.txbuff[modbus_slave.txcount++] = (tx_crc & 0xff00)>>8;//高位
}

u16 hold_reg[6]={0x10,0x20,0x30,0x40,0x50,0x60};
void Modbus_Funtion03(void)
{
	u16 tx_crc = 0;
	u16 register_count  = 0;//保存寄存器的数量
	u16 register_addr = 0;//寄存器的起始地址
	//寄存器数量
	register_count = (modbus_slave.rxbuff[4]<<8) | modbus_slave.rxbuff[5];
	//寄存器的起始地址
	register_addr = (modbus_slave.rxbuff[2]<<8) | modbus_slave.rxbuff[3];
	//判断寄存器的数量是否在0x01 ~ 0x7D之间
	if(register_count <0x01 || register_count>0x7D)
	{
		Modbus_FunctionError(0x03,0x03);
	}
	//寄存器的起始地址  起始地址+寄存器的数量
	if(register_addr>6 || (register_addr +register_count)>6)
	{
		Modbus_FunctionError(0x03,0x02);
	}
	//响应
	//设备ID
	modbus_slave.txbuff[modbus_slave.txcount++] = modbus_slave.rxbuff[0];
	//功能码
	modbus_slave.txbuff[modbus_slave.txcount++] = 0x03;
	//字节数
	modbus_slave.txbuff[modbus_slave.txcount++] = 2*register_count;
	//寄存器的数值
	for(u8 i=0;i<register_count;i++)
	{
		modbus_slave.txbuff[modbus_slave.txcount++] = (hold_reg[register_addr+i]&0xff00)>>8;//高位
		modbus_slave.txbuff[modbus_slave.txcount++] = hold_reg[register_addr+i]&0xff;//低位
	}
	//计算CRC
	tx_crc = CRC_16_Tab(modbus_slave.txbuff,modbus_slave.txcount);
	//将CRC值存放发送缓存区
	modbus_slave.txbuff[modbus_slave.txcount++] = (tx_crc & 0xff);
	modbus_slave.txbuff[modbus_slave.txcount++] = (tx_crc & 0xff00)>>8;
}

void RS485_SendData(u8 *tx_buff,u8 lenth)
{
	//设置为发送模式
	RS485_RE = RS485_ModeTx;
	for(u8 i=0;i<lenth;i++)
	{
		//发送数据
		USART3->DR = tx_buff[i];
		//判断数据是否发送完成
		while(USART_GetFlagStatus(USART3,USART_FLAG_TC)!=SET);//等待发送结束
	}
	//配置为接收模式
	RS485_RE = RS485_ModeRx;
}
extern float temp,hum;
//更新数据
void Updata_Data(void)
{
	hold_reg[0] = temp;
	hold_reg[1] = hum;
}

本篇接上一篇RS485上实现的,加油

上一篇 基于STM32f103——RS458通信
上上篇 基于STM32f103c8t6的红外接收发送

[参考文档]:modbus协议

你可能感兴趣的:(stm32)