当前位置:   article > 正文

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

stm32modbus库

本人软件工程专业,关于硬件只有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,具体描述见下图:

 2.MODBUS RTU中的数据帧结构

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

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

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

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


二.代码上的实现

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

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

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

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

        步骤如下:

1.初始化定时器和USART

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

  1. void Serial_Init(void){
  2. // 1.开启时钟(USART与GPIO)
  3. RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);// USART
  4. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);// GPIO
  5. // 2.GPIO初始化(TX——复用输出,RX——输入)
  6. GPIO_InitTypeDef GPIO_InitStructure;
  7. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  8. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  9. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  10. GPIO_Init(GPIOA,&GPIO_InitStructure);
  11. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
  12. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  13. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  14. GPIO_Init(GPIOA,&GPIO_InitStructure);
  15. // 3.配置USART
  16. USART_InitTypeDef USART_InitStructure = {
  17. .USART_BaudRate = 9600,// 波特率
  18. .USART_HardwareFlowControl = USART_HardwareFlowControl_None, // 硬件流控制
  19. .USART_Mode = USART_Mode_Tx | USART_Mode_Rx,// 指定发送功能 如果又要发送也要接收 可以采用 A | B 的格式
  20. .USART_Parity = USART_Parity_No,// 校验位
  21. .USART_StopBits = USART_StopBits_1,// 停止位
  22. .USART_WordLength = USART_WordLength_8b
  23. };
  24. USART_Init(USART1,&USART_InitStructure);
  25. // 开启中断
  26. USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
  27. // 配置NVIC
  28. NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
  29. // 初始化NVIC的USART1通道
  30. NVIC_InitTypeDef NVIC_InitStructure = {
  31. .NVIC_IRQChannel = USART1_IRQn,
  32. .NVIC_IRQChannelCmd = ENABLE,
  33. .NVIC_IRQChannelPreemptionPriority = 1,
  34. .NVIC_IRQChannelSubPriority = 1
  35. };
  36. NVIC_Init(&NVIC_InitStructure);
  37. // 4.开启USART(或配置中断)
  38. USART_Cmd(USART1,ENABLE);
  39. }

        以及定时器的相关配置:

  1. void Timer_Init(uint16_t arr,uint16_t psc){
  2. //RCC内部时钟 ON
  3. RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
  4. //时钟源选择
  5. TIM_InternalClockConfig(TIM3);
  6. //配置时机单元
  7. TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
  8. TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 不分频
  9. TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
  10. TIM_TimeBaseInitStructure.TIM_Period = arr ; // 因为预分频器和计数器都有1个数的偏差,所以这里要再减去一个1
  11. TIM_TimeBaseInitStructure.TIM_Prescaler = psc ; // Tout = ((arr+1)*(psc+1))/Tclk ;
  12. TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
  13. TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
  14. TIM_ClearFlag(TIM3,TIM_IT_Update);
  15. //配置输出中断控制
  16. TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);
  17. //配置NVIC
  18. NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 优先级分组
  19. NVIC_InitTypeDef NVIC_InitStructure;
  20. NVIC_InitStructure.NVIC_IRQChannel = TIM_Channel_3; // 中断通道
  21. NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 制特定中断通道的使能状态
  22. NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级
  23. NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 响应优先级
  24. NVIC_Init(&NVIC_InitStructure);
  25. //启动定时器
  26. TIM_Cmd(TIM3,ENABLE);
  27. }

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

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

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

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

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

  1. // 定时器中的变量定义:
  2. volatile uint32_t last_capture_time = 0;
  3. const uint32_t max_frame_time = 4000; // 设定的最大帧传输时间,单位为定时器计数值
  4. // 串口中的变量定义:
  5. uint8_t Serial_RxPacket[100] = {0};
  6. uint16_t Serial_RxLength = 0;
  7. uint8_t Serial_RxFlag;
  8. uint8_t clearBufferFlag = 0;

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

  1. void TIM3_IRQHandler(void){
  2. if(TIM_GetITStatus(TIM3,TIM_IT_CC3) != RESET){ // 输入捕获中断触发,计算两个捕获之间的时间间隔
  3. uint32_t current_capture_time = TIM_GetCapture1(TIM3);
  4. uint32_t time_interval = current_capture_time - last_capture_time;
  5. if (time_interval > max_frame_time) {
  6. // 超过设定的最大帧传输时间,认为一帧数据传输结束
  7. // 处理完整的数据帧
  8. Serial_RxFlag = 1;
  9. }
  10. // 重置定时器捕获时间
  11. last_capture_time = current_capture_time;
  12. TIM_ClearITPendingBit(TIM3,TIM_IT_CC3);
  13. }
  14. }

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

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

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

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

  1. #include "stm32f10x.h"
  2. #include "Timer.h"
  3. #include "Serial.h"
  4. uint8_t Serial_TxPacket[100] = {0}; // 发送内容
  5. extern uint8_t Serial_RxPacket[100]; // 接收内容
  6. extern uint16_t Serial_RxLength;
  7. extern uint16_t modbus_io[100]; // modbus寄存器内数据
  8. // uint16_t modbus_id = 0X01; // id号
  9. uint16_t modbus_function; // 功能码
  10. uint16_t modbus_check; // 校验位
  11. uint16_t modbus_packege_times = 0; // 总包计数
  12. uint16_t CRC_check_result; // CRC校验的结果
  13. uint16_t calculate_crc16(const uint8_t *data, size_t len) {
  14. // printf("%d\n",len);
  15. // 初始化crc为0xFFFF
  16. uint16_t crc = 0xFFFF;
  17. // 循环处理每个数据字节
  18. for (size_t i = 0; i < len; i++) {
  19. // 将每个数据字节与crc进行异或操作
  20. crc ^= data[i];
  21. // 对crc的每一位进行处理:如果最低位为1,则右移一位并执行异或0xA001操作(即0x8005按位颠倒后的结果)
  22. for (int j = 0; j < 8; j++) {
  23. if (crc & 0x0001) {
  24. crc = (crc >> 1) ^ 0xA001;
  25. }
  26. // 如果最低位为0,则仅将crc右移一位
  27. else {
  28. crc = crc >> 1;
  29. }
  30. }
  31. }
  32. return crc;
  33. }
  34. void Data_Funcion_3(void){
  35. Serial_TxPacket[0] = Serial_RxPacket[0]; // ID
  36. Serial_TxPacket[1] = Serial_RxPacket[1]; // 功能码
  37. // 字节长度,根据接收的内容4,5位来判断
  38. Serial_TxPacket[2] = (Serial_RxPacket[4] << 8 | Serial_RxPacket[5]) * 2;
  39. for(modbus_packege_times = 0;modbus_packege_times<Serial_TxPacket[2];modbus_packege_times+=2)
  40. {
  41. Serial_TxPacket[3+modbus_packege_times] = modbus_io[modbus_packege_times / 2] >> 8;
  42. Serial_TxPacket[4+modbus_packege_times] = modbus_io[modbus_packege_times / 2];
  43. }
  44. // 校验码
  45. CRC_check_result = calculate_crc16(Serial_TxPacket,Serial_TxPacket[2] + 3);
  46. Serial_TxPacket[3+modbus_packege_times] = (CRC_check_result) & 0xFF;
  47. Serial_TxPacket[4+modbus_packege_times] = (CRC_check_result>>8) & 0xFF;
  48. Serial_SendArray(Serial_TxPacket,5+modbus_packege_times);
  49. return ;
  50. }
  51. void Data_Funcion_6(void){
  52. // Serial_TxPacket[0] = Serial_RxPacket[0]; // ID
  53. // Serial_TxPacket[1] = Serial_RxPacket[1]; // 功能码
  54. modbus_io[Serial_RxPacket[3] - 1] = Serial_RxPacket[4];
  55. modbus_io[Serial_RxPacket[3]] = Serial_RxPacket[5];
  56. Serial_SendArray(Serial_RxPacket,Serial_RxLength);
  57. return ;
  58. }
  59. void Data_Resolve(void){
  60. // 需增加校验位计算
  61. modbus_check = calculate_crc16(Serial_RxPacket,Serial_RxLength-2);
  62. if(modbus_check != 0) // 校验是否通过
  63. {
  64. Serial_TxPacket[0] = 0x01; // 预设id
  65. if(Serial_RxPacket[0] == Serial_TxPacket[0]){ // 确认id号是否一致
  66. modbus_function = Serial_RxPacket[1];
  67. switch(modbus_function)
  68. {
  69. case 3 : // 根据03功能码,主机要求从机反馈内容
  70. Data_Funcion_3();
  71. break;
  72. case 6 :
  73. Data_Funcion_6();
  74. break;
  75. // case 16 :
  76. // Serial_SendArray(Serial_TxPacket,Serial_RxLength);
  77. // break;
  78. default :
  79. break;
  80. }
  81. }
  82. }
  83. Serial_RxFlag = 0;
  84. Serial_RxLength = 0;
  85. }

三.使用软件

        使用Keil给板子上程序后,我这边使用了Modbus Pull和Modbus Slave做实验,网上可以查到并且下载。

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

         工程代码:

github上的项目工程

        目前我只做了这两个功能码,如果这边有什么错误的地方还请大佬们给出指点(。・∀・)~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/107879
推荐阅读
相关标签
  

闽ICP备14008679号