STM32硬件库(非HAL库)实现MODBUS RTU协议的03,06功能码(读以及与单个发送)

这篇具有很好参考价值的文章主要介绍了STM32硬件库(非HAL库)实现MODBUS RTU协议的03,06功能码(读以及与单个发送)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本人软件工程专业,关于硬件只有408的基础,后续学习发现一些博主所讲以及b站上所给的教程并不是很清晰,故编写此文档供大家查看。

如果我说的地方哪里有问题,希望大家可以给出意见!(●ˇ∀ˇ●)


参考文档:

Modbus通讯协议常用功能码解释_modbus功能码_Lee139499的博客-CSDN博客


目录

一.什么是MODBUS RTU

1.关于MODBUS中的功能码

 2.MODBUS RTU中的数据帧结构

​编辑

二.代码上的实现

1.初始化定时器和USART

2.设置定时器为输入捕获模式

3.在USART接收中断中记录定时器值

4.自定义文件,针对于MODBUS协议对数据进行处理

三.使用软件


一.什么是MODBUS RTU

         MODBUS是一种单主站的主/从通讯模式。Modbus网络上只有一个主站,主站在Modbus网络上没有地址,从站的地址范围为0-247,其中0为广播地址,从站的实际地址范围为1-247。

        通信由主机发起,一问一答式,从机无法主动向主机发送数据。

        传输过程中,两个字节之间的相邻时间不得大于3.5个字符的时间,否则视为一帧数据传输结束。

1.关于MODBUS中的功能码

常用的就是01、02、03、04、05、06、15、16,具体描述见下图:

  • 串口modbus协议06功能码,stm32,嵌入式硬件,学习

 2.MODBUS RTU中的数据帧结构

  • 串口modbus协议06功能码,stm32,嵌入式硬件,学习

        地址:设备的 MODBUS 地址,用于标识通信中的从设备。

        功能码:表示对从设备执行的操作,例如读取保持寄存器、写单个寄存器等。

        数据(2字节):传输的数据,由两个字节组成。具体数据内容可能根据功能码不同而有所变化。        

        CRC校验(2字节):用于验证数据的完整性,由两个字节组成。该校验值是在数据帧中的所有字段(包括地址、功能码和数据)被计算后得到的。

串口modbus协议06功能码,stm32,嵌入式硬件,学习


二.代码上的实现

        此项目中,我使用的是STM32F103C8T6开发板,串口使用USART。

        因为我只需要实现了03,06功能码,所以代码部分只有针对这两个功能码的实现。

        那么,根据该协议,我们需要使用定时器来实现判断两个字节之间的相邻时间,确保数据传输的时间间隔不得大于设定好的时间。

        在串口USART中判断两个字节之间的相邻时间,以确保数据帧传输不超过设定的时间阈值。我们使用一个定时器来记录两个字节之间的时间,并在定时器中断中进行判断。

        步骤如下:

1.初始化定时器和USART

      首先,你需要初始化定时器和USART,确保它们已经配置正确。

void Serial_Init(void){

    // 1.开启时钟(USART与GPIO)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);// USART
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);// GPIO
    // 2.GPIO初始化(TX——复用输出,RX——输入)
    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);
    // 3.配置USART
    USART_InitTypeDef USART_InitStructure = {
        .USART_BaudRate = 9600,// 波特率
        .USART_HardwareFlowControl = USART_HardwareFlowControl_None, // 硬件流控制
        .USART_Mode = USART_Mode_Tx | USART_Mode_Rx,// 指定发送功能 如果又要发送也要接收 可以采用 A | B 的格式
        .USART_Parity = USART_Parity_No,// 校验位
        .USART_StopBits = USART_StopBits_1,// 停止位
        .USART_WordLength = USART_WordLength_8b
    };
    USART_Init(USART1,&USART_InitStructure);

    // 开启中断
    USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
    // 配置NVIC
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    // 初始化NVIC的USART1通道
    NVIC_InitTypeDef NVIC_InitStructure = {
        .NVIC_IRQChannel = USART1_IRQn,
        .NVIC_IRQChannelCmd = ENABLE,
        .NVIC_IRQChannelPreemptionPriority = 1,
        .NVIC_IRQChannelSubPriority = 1
    };
    NVIC_Init(&NVIC_InitStructure);

    // 4.开启USART(或配置中断)
    USART_Cmd(USART1,ENABLE);
}

        以及定时器的相关配置:

void Timer_Init(uint16_t arr,uint16_t psc){

	//RCC内部时钟 ON
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
	
	//时钟源选择
	TIM_InternalClockConfig(TIM3);
	
	//配置时机单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;						// 不分频
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;					// 向上计数
	TIM_TimeBaseInitStructure.TIM_Period = arr ;									// 因为预分频器和计数器都有1个数的偏差,所以这里要再减去一个1
	TIM_TimeBaseInitStructure.TIM_Prescaler = psc ;									// Tout = ((arr+1)*(psc+1))/Tclk ;
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;	
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
	
	
	
	TIM_ClearFlag(TIM3,TIM_IT_Update);
	//配置输出中断控制
	TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);
	
	
	//配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);									// 优先级分组
	
	NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM_Channel_3;									// 中断通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;									// 制特定中断通道的使能状态
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;						// 抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;								// 响应优先级
	NVIC_Init(&NVIC_InitStructure); 
	


	//启动定时器
	TIM_Cmd(TIM3,ENABLE);
}

2.设置定时器为输入捕获模式

        将定时器设置为输入捕获模式,以便在USART接收到一个字节时记录定时器的值。

// 配置定时器通道为输入捕获模式
void configure_input_capture() {
    // 配置输入捕获通道 CHx 为输入捕获模式
    TIM3->CCMR1 |= TIM_CCMR1_CC1S_0; // 将CC1S位设置为01,选择输入捕获通道1为TI1
    // 配置输入捕获通道 CHx 的触发边沿或状态变化条件
    TIM3->CCER |= TIM_CCER_CC1P; // 设置捕获边沿为下降沿触发,如果需要上升沿触发,可以选择设置为TIM_CCER_CC1NP
    // 使能捕获通道 CHx
    TIM3->CCER |= TIM_CCER_CC1E;
}

3.在USART接收中断中记录定时器值

        在USART接收中断中,记录定时器的当前值,并在接收到字节时启动或重置定时器。

// 定时器中的变量定义:
volatile uint32_t last_capture_time = 0;
const uint32_t max_frame_time = 4000; // 设定的最大帧传输时间,单位为定时器计数值

// 串口中的变量定义:
uint8_t Serial_RxPacket[100] = {0};
uint16_t Serial_RxLength = 0;
uint8_t Serial_RxFlag;
uint8_t clearBufferFlag = 0;

        以下为定时器的中断配置:

void TIM3_IRQHandler(void){

	if(TIM_GetITStatus(TIM3,TIM_IT_CC3) != RESET){	 // 输入捕获中断触发,计算两个捕获之间的时间间隔
		uint32_t current_capture_time = TIM_GetCapture1(TIM3);
        uint32_t time_interval = current_capture_time - last_capture_time;

		if (time_interval > max_frame_time) {
            // 超过设定的最大帧传输时间,认为一帧数据传输结束
            // 处理完整的数据帧
			Serial_RxFlag = 1;
        }

        // 重置定时器捕获时间
        last_capture_time = current_capture_time;

		TIM_ClearITPendingBit(TIM3,TIM_IT_CC3);
		
	}	

}

        以下为串口USART的中断配置:

void USART1_IRQHandler(void){

	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
        // 接收到一个字节数据,记录定时器的当前值
        last_capture_time = TIM_GetCapture3(TIM3);
        Serial_RxPacket[Serial_RxLength++] = USART_ReceiveData(USART1);
        Serial_RxFlag = 1;
        USART_ClearITPendingBit(USART1,USART_IT_RXNE);// 清除标志位
        
	}

}

        在定时器输入捕获中断中判断两个字节之间的时间: 当定时器捕获到第二个字节时,计算两个捕获之间的时间间隔。如果这个时间间隔超过设定的阈值,则视为一帧数据传输结束。

4.自定义文件,针对于MODBUS协议对数据进行处理

#include "stm32f10x.h"
#include "Timer.h"
#include "Serial.h"
uint8_t Serial_TxPacket[100] = {0};                        // 发送内容
extern uint8_t Serial_RxPacket[100];                       // 接收内容
extern uint16_t Serial_RxLength;

extern uint16_t modbus_io[100];                            // modbus寄存器内数据
// uint16_t modbus_id = 0X01;                              // id号
uint16_t modbus_function;                                  // 功能码
uint16_t modbus_check;                                     // 校验位
uint16_t modbus_packege_times = 0;                         // 总包计数
uint16_t CRC_check_result;                                 // CRC校验的结果



uint16_t calculate_crc16(const uint8_t *data, size_t len) {
    // printf("%d\n",len);
    
    // 初始化crc为0xFFFF
    uint16_t crc = 0xFFFF;

    // 循环处理每个数据字节
    for (size_t i = 0; i < len; i++) {
        // 将每个数据字节与crc进行异或操作
        crc ^= data[i];

        // 对crc的每一位进行处理:如果最低位为1,则右移一位并执行异或0xA001操作(即0x8005按位颠倒后的结果)
        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } 
            // 如果最低位为0,则仅将crc右移一位
            else {
                crc = crc >> 1;
            }
        }
    }
    return crc;
}


void Data_Funcion_3(void){
    Serial_TxPacket[0] = Serial_RxPacket[0];                // ID
    Serial_TxPacket[1] = Serial_RxPacket[1];                // 功能码
    // 字节长度,根据接收的内容4,5位来判断
    Serial_TxPacket[2] = (Serial_RxPacket[4] << 8 | Serial_RxPacket[5]) * 2;


    for(modbus_packege_times = 0;modbus_packege_times<Serial_TxPacket[2];modbus_packege_times+=2)
    {
        Serial_TxPacket[3+modbus_packege_times] = modbus_io[modbus_packege_times / 2] >> 8;
        Serial_TxPacket[4+modbus_packege_times] = modbus_io[modbus_packege_times / 2];
    }         
    // 校验码
    CRC_check_result = calculate_crc16(Serial_TxPacket,Serial_TxPacket[2] + 3);
    Serial_TxPacket[3+modbus_packege_times] = (CRC_check_result) & 0xFF;
    Serial_TxPacket[4+modbus_packege_times] = (CRC_check_result>>8) & 0xFF;


      

    Serial_SendArray(Serial_TxPacket,5+modbus_packege_times);
    return ;
}


void Data_Funcion_6(void){
    // Serial_TxPacket[0] = Serial_RxPacket[0];             // ID
    // Serial_TxPacket[1] = Serial_RxPacket[1];             // 功能码
    modbus_io[Serial_RxPacket[3] - 1] = Serial_RxPacket[4];         
    modbus_io[Serial_RxPacket[3]] = Serial_RxPacket[5];
    
    Serial_SendArray(Serial_RxPacket,Serial_RxLength);
    return ;
}


void Data_Resolve(void){

    // 需增加校验位计算
    modbus_check = calculate_crc16(Serial_RxPacket,Serial_RxLength-2);
    
    if(modbus_check != 0)                                  // 校验是否通过
    {   
        Serial_TxPacket[0] = 0x01;                         // 预设id
        if(Serial_RxPacket[0] == Serial_TxPacket[0]){      // 确认id号是否一致

            modbus_function = Serial_RxPacket[1];
            switch(modbus_function)
            { 
                case 3 :                                   // 根据03功能码,主机要求从机反馈内容 
                    Data_Funcion_3();
                    break;
                case 6 :
                    Data_Funcion_6();
                    break;
                // case 16 :
                //     Serial_SendArray(Serial_TxPacket,Serial_RxLength);
                //     break;
                default :
                    break;
            }

        }

    }
    Serial_RxFlag = 0;
    Serial_RxLength = 0;
}




三.使用软件

        使用Keil给板子上程序后,我这边使用了Modbus Pull和Modbus Slave做实验,网上可以查到并且下载。串口modbus协议06功能码,stm32,嵌入式硬件,学习

        我们如果使用板子当作从机的话,那么只需要使用到Pull就可以了,不需要使用到Slave。

         工程代码:

github上的项目工程

        目前我只做了这两个功能码,如果这边有什么错误的地方还请大佬们给出指点(。・∀・)~文章来源地址https://www.toymoban.com/news/detail-779626.html

到了这里,关于STM32硬件库(非HAL库)实现MODBUS RTU协议的03,06功能码(读以及与单个发送)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Modbus-RTU协议C#实现

    1、安装依赖包 System.IO.Ports 2、读协议 3、写协议 4、CRC16校验

    2024年02月15日
    浏览(42)
  • 【正点原子STM32】RS485串行通信标准(串口基础协议 和 MODBUS协议、总线连接、通信电路、通信波形图、RS485相关HAL库驱动、RS485配置步骤、)

    一、RS485介绍 二、RS485相关HAL库驱动介绍 三、RS485配置步骤 四、编程实战 五、总结 串口、UART、TTL、RS232、RS422和RS485之间的关系可以如此理解: 串口 :是一个广义术语,通常指的是采用串行通信协议的接口,它可以包括多种具体的物理接口标准和逻辑电平标准。 UART (通用

    2024年04月13日
    浏览(47)
  • STM32实现基于RS485的简单的Modbus协议

    我这里用STM32实现,其实可以搬移到其他MCU,之前有项目使用STM32实现Modbus协议 这个场景比较正常,很多时候都能碰到 这里主要是Modbus和变频器通信 最常见的是使用Modbus实现传感器数据的采集,我记得之前用过一些传感器都是Modbus协议 这就需要MCU实现Modbus协议,不过实际使

    2024年02月08日
    浏览(46)
  • DGIOT-Modbus-RTU控制指令05、06的配置与下发

    [小 迪 导 读]:伴随工业物联网在实际应用中普及,Modbus-RTU作为行业内的标准化通讯协议。在为物联网起到采集作用的同时,设备的控制也是一个密不可分的环节。 场景解析:在使用Modbus对设备进行采集后,可以通过自动控制和手动控制来实现动环或者设备的运行状态调节。

    2024年02月09日
    浏览(24)
  • Profibus-DP转modbus RTU网关modbus rtu协议

    捷米JM-DPM-RTU网关在Profibus总线侧实现主站功能,在Modbus串口侧实现从站功能。可将ProfibusDP协议的设备(如:E+H流量计、倍福编码器等)接入到Modbus网络中;通过增加DP/PA耦合器,也可将Profibus PA从站接入Modbus网络。在Modbus串口侧提供RS485和RS232两种电平接口。 捷米JM-DPM-RTU网关

    2024年02月10日
    浏览(32)
  • C# ModBus协议(RTU )详细指南

    ModBus协议:官方的解释是Modbus协议是一种通信协议,用于在自动化设备之间进行数据传输。它最初是由Modicon公司于1979年开发的,现在已成为工业界的一种通用协议。Modbus协议有多种变体,包括 Modbus-RTU、Modbus-TCP和Modbus-ASCII 等,其中Modbus-RTU是最常用的变体之一。Modbus协议基于

    2024年02月04日
    浏览(47)
  • modbus-tcp-rtu协议图表

    MODBUS TCP 读寄存器 请求 序号 意义 所占字节 字节存放格式 1 事务处理标识 2个字节 高字节在前 2 协议标识 2个字节 高字节在前 3 长度 2个字节 高字节在前 4 单元标识 1个字节 0x00-0xff 5 功能码 1个字节 0x03 6 起始寄存器地址 2个字节 高字节在前 7 寄存器个数 2个字节 高字节在前

    2024年01月23日
    浏览(26)
  • Modbus-RTU功能码

    以下图片中的帧解析都不含站号和校验码 在一个远程设备中,使用该功能码读取线圈的 1 至 2000 连续状态。请求 PDU (功能码-地址-数据)详细说明了起始地址,即指定的第一个线圈地址和线圈编号。从零开始寻址线圈。因此寻址线圈 1-16 为 0-15(PLC地址一般也是这样,寄存器

    2024年02月04日
    浏览(35)
  • MODBUS RTU 通信协议 CRC16校验算法

    CRC校验码是一个2个字节(16位二进制)的数。 发送端:发送的数据计算CRC校验码----发送:数据+CRC校验码 接收端:收到数据后重新计算CRC校验码,然后和接收到数据中的CRC校验码进行比较,判断是否相等。 如果不相等:数据传输过程中出错,给出错误应答。 CRC16 校验源码

    2024年02月16日
    浏览(39)
  • 嵌入式 STM32 通讯协议--MODBUS

    目录 一、自定义通信协议 1、协议介绍 2、网络协议 3、自定义的通信协议  二、MODBUS通信协议 1、概述 2、MODBUS帧结构  协议描述 3、MODBUS数据模型   4、MODBUS事务处理的定义 5、MODBUS功能码  6、功能码定义   7、MODBUS数据链路层 8、MODBUS地址规则  9、MODBUS帧描述 10、MODBUS两种

    2024年02月11日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包