• STM32实战总结:HAL之modbus


    什么是modbus

    Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气 Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。

    要注意的是::::::MODBUS协议是一种软件协议,是一种人为约定的协议,他和SPI,IIC,CAN总线协议还是有些不同的,SPI,IIC,CAN总线这些协议必须是设备在硬件上支持的,可以说SPI,IIC,CAN总线是一种软硬件的结合体,也就是常分为两层即物理层和协议层,MODBUS本身就是类似于协议层的东西。Modbus通信标准协议可以通过各种传输方式传播,如 RS232C、RS485、光纤、无线电等。

    Modbus比其他通信协议使用的更广泛的主要原因有:

    1. 公开发表并且无版权要求

    2. 易于部署和维护

    3. 对供应商来说,修改移动本地的比特或字节没有很多限制

    Modbus允许多个 (大约240个) 设备连接在同一个网络上进行通信,举个例子,一个测量温度和湿度的装置,并且将结果发送给计算机。在数据采集与监视控制系统(SCADA)中,Modbus通常用来连接监控计算机和远程终端控制系统(RTU)。

    Modbus协议大致分为以下两种串行传输模式:

    • Modbus-RTU

    • Modbus-ASCII

    一个设备只会使用一种协议,一般来说大部分的设备都是Modbus-RTU协议。

    设备必须要有RTU协议!这是Modbus协议上规定的,且默认模式必须是RTU,ASCII作为可选项。一般学习Modbus协议,只需要了解RTU协议,ASCll了解即可。

    Modbus通讯过程

    Modbus是一种单主站的主/从通信模式。,主机发送,从机应答,主机不发送,总线上就没有数据通信。 

    举例: 一个总线上有一个主机,多个从机,主机查询其中一个从机,首先得给这些从机分配地址(每个地址必须唯一),分配好地址后,主机先查询,然后发数据,从机得到主机发送的数据,然后对应地址的从机回复,主机得到从机数据。

    注意:

    Modbus不能判断从机是否忙,也没有对应的仲裁机制,我们只能通过软件对数据进行适当的处理!

    帧结构

    帧结构 = 地址 + 功能码+ 数据 + 校验

    ●地址: 占用一个字节,范围0-255,其中有效范围是1-247,其他有特殊用途。Modbus网络上只能有一个主站存在,主站在 Modbus网络上没有地址,从站的地址范围为 0 - 247,其中 0 为广播地址,从站的实际地址范围为 1 - 247。

    ●功能码:占用一个字节,功能码的意义就是,知道这个指令是干啥的,比如你可以查询从机的数据,也可以修改数据,所以不同功能码对应不同功能。

    部分功能码:

    其中,功能码03和06是比较常用的。

    ●数据:占用一个或多个字节,根据功能码不同,有不同结构。

    ●校验:为了保证数据不错误,增加这个,然后再把前面的数据进行计算看数据是否一致,如果一致,就说明这帧数据是正确的,我再回复;如果不一样,说明你这个数据在传输的时候出了问题,数据不对的,所以就抛弃了。

    举例说明

    我们大部分时候都是用modbus来和传感器通信。如果要查询传感器上的信息,用03查询功能码,如果需要修改传感器寄存器的值就用06修改功能码,其他的不需要过多关注,用到的时候再去了解。

    查询功能码

    比如我们现在要使用STM32查询某传感器的数据,该传感器的地址为01。

    主机发送: 01 03 00 00 00 01 84 0A
    从机回复: 01 03 02 19 98 B2 7E

    什么意思?解析如下:

    发送数据解析

    01-地址,也就是你传感器的地址
    03-功功能码,03代表查询功能,查询传感器的数据
    00 00-代表查询的起始寄存器地址.说明从0x0000开始查询。这里需要说明以下,Modbus把数据存放在寄存器中,通过查询寄存器来得到不同变量的值,一个寄存器地址对应2字节数据
    00 01-代表查询了一个寄存器.结合前面的00 00,意思就是查询从0开始的1个寄存器值
    84 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到84前面为止。

    回复数据解析

    01-地址,也就是你传感器的地址
    03-功功能码,03代表查询功能,查询传感器的数据。这里要注意的是注意发给从机的功能码是啥,从机就要回复同样的功能码,如果不一样说明这一帧数据有错误
    02-代表后面数据的字节数,因为上面说到,一个寄存器有2个字节,所以后面的字节数肯定是2*查询的寄存器个数;
    19 98-寄存器的值是19 98,结合发送的数据看出,01这个寄存器的值为19 98
    B2 7E-循环冗余校验

    总结就是:

    发送:从机的地址+我要干嘛的功能码+我要查的寄存器的地址+我要查的寄存器地址的个数+校验码

    回复:从机的地址+主机发我的功能码+要发送给主机数据的字节数+数据+校验码

    修改功能码

    主机发送: 01 06 00 00 00 01 48 0A
    从机回复: 01 06 00 00 00 01 48 0A

    看上去怎么一样的啊?是不是错了?答案是这是正确的。

    发送数据解析

    01-主机要查询的从机地址
    06-功能码,06代表修改单个寄存器功能,修改有些不同,有修改一个寄存器和修改多个寄存器;
    00 00-代表修改的起始寄存器地址.说明从0x0000开始.
    00 01-代表修改的值为00 01.结合前面的00 00,意思就是修改0号寄存器值为00 01;
    48 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到48前面为止

    回复数据解析

    01-从机返回给主机自己的地址,说明这就是主机查的从机
    06-功能码,代表修改单个寄存器功能,主机发啥功能码,从机就必须回什么功能码;
    00 00-代表修改的起始寄存器地址.说明是0x0000.
    00 01-代表修改的值为00 01.结合前面的00 00,意思就是修改0号寄存器值为00 01;
    48 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到48前面为止;

    如果回复的一样,说明这个数据是修改成功的;如果功能码不是06,而是别的,说明从机回复的数据有误,主机可以做相应的处理。

    如果我要修改多个寄存器,难道用06发好几次,这样不会太傻了吗?所以Modbus RTU协议包含了修改连续多个寄存器的方法,就是功能码为0x10;这个大家自己去查询,基本和上面的数据格式差不多。

    注意,ModBus只是一个软件协议,传输时可以通过串口来进行通信。 

    为了提升传输效率,可以结合DMA使用。

    MX配置

    配置串口

    配置DMA

    发送和接收都采用DMA

    开启空闲中断

    关键代码

    1. /* Includes ------------------------------------------------------------------*/
    2. #include "MyApplication.h"
    3. /* Private define-------------------------------------------------------------*/
    4. #define FunctionCode_Read_Register (uint8_t)0x03
    5. #define FunctionCode_Write_Register (uint8_t)0x06
    6. #define Modbus_Order_LENGTH (uint8_t)8
    7. /* Private variables----------------------------------------------------------*/
    8. /* Private function prototypes------------------------------------------------*/
    9. static void Protocol_Analysis(UART_t*); //协议分析
    10. static void Modbus_Read_Register(UART_t*); //读寄存器
    11. static void Modbus_Wrtie_Register(UART_t*); //写寄存器
    12. /* Public variables-----------------------------------------------------------*/
    13. Modbus_t Modbus =
    14. {
    15. 1,
    16. Protocol_Analysis
    17. };
    18. /*
    19. * @name Protocol_Analysis
    20. * @brief 协议分析
    21. * @param UART -> 串口指针
    22. * @retval None
    23. */
    24. static void Protocol_Analysis(UART_t* UART)
    25. {
    26. UART_t* const COM = UART;
    27. uint8_t i = 0,Index = 0;
    28. //串口3停止DMA接收
    29. HAL_UART_DMAStop(&huart3);
    30. //过滤干扰数据,首字节为modbus地址,共8字节
    31. for(i=0;i<UART3_Rec_LENGTH;i++)
    32. {
    33. //检测键值起始数据Modbus.Addr
    34. if(Index == 0)
    35. {
    36. if(*(COM->pucRec_Buffer+i) != Modbus.Addr)
    37. continue;
    38. }
    39. *(COM->pucRec_Buffer+Index) = *(COM->pucRec_Buffer+i);
    40. //已读取7个字节
    41. if(Index == Modbus_Order_LENGTH)
    42. break;
    43. Index++;
    44. }
    45. //计算CRC-16
    46. CRC_16.CRC_Value = CRC_16.CRC_Check(COM->pucRec_Buffer,6); //计算CRC值
    47. CRC_16.CRC_H = (uint8_t)(CRC_16.CRC_Value >> 8);
    48. CRC_16.CRC_L = (uint8_t)CRC_16.CRC_Value;
    49. //校验CRC-16
    50. if(((*(COM->pucRec_Buffer+6) == CRC_16.CRC_L) && (*(COM->pucRec_Buffer+7) == CRC_16.CRC_H))
    51. ||
    52. ((*(COM->pucRec_Buffer+6) == CRC_16.CRC_H) && (*(COM->pucRec_Buffer+7) == CRC_16.CRC_L)))
    53. {
    54. //校验地址
    55. if((*(COM->pucRec_Buffer+0)) == Modbus.Addr)
    56. {
    57. //处理数据
    58. if((*(COM->pucRec_Buffer+1)) == FunctionCode_Read_Register)
    59. {
    60. Modbus_Read_Register(COM);
    61. }
    62. else if((*(COM->pucRec_Buffer+1)) == FunctionCode_Write_Register)
    63. {
    64. Modbus_Wrtie_Register(COM);
    65. }
    66. }
    67. }
    68. //清缓存
    69. for(i=0;i<UART3_Rec_LENGTH;i++)
    70. {
    71. *(COM->pucRec_Buffer+i) = 0x00;
    72. }
    73. }
    74. /*
    75. * @name Modbus_Read_Register
    76. * @brief 读寄存器
    77. * @param UART -> 串口指针
    78. * @retval None
    79. */
    80. static void Modbus_Read_Register(UART_t* UART)
    81. {
    82. UART_t* const COM = UART;
    83. //校验地址
    84. if((*(COM->pucRec_Buffer+2) == 0x9C) && (*(COM->pucRec_Buffer+3) == 0x41))
    85. {
    86. 回应数据
    87. //地址码
    88. *(COM->pucSend_Buffer+0) = Modbus.Addr;
    89. //功能码
    90. *(COM->pucSend_Buffer+1) = FunctionCode_Read_Register;
    91. //数据长度
    92. *(COM->pucSend_Buffer+2) = 8;
    93. //SHT30温度
    94. *(COM->pucSend_Buffer+3) = ((uint16_t)((SHT30.fTemperature+40)*10))/256;
    95. *(COM->pucSend_Buffer+4) = ((uint16_t)((SHT30.fTemperature+40)*10))%256;
    96. //SHT30湿度
    97. *(COM->pucSend_Buffer+5) = 0;
    98. *(COM->pucSend_Buffer+6) = SHT30.ucHumidity;
    99. //继电器状态
    100. *(COM->pucSend_Buffer+7) = 0;
    101. *(COM->pucSend_Buffer+8) = Relay.Status;
    102. //蜂鸣器状态
    103. *(COM->pucSend_Buffer+9) = 0;
    104. *(COM->pucSend_Buffer+10) = Buzzer.Status;
    105. //插入CRC
    106. CRC_16.CRC_Value = CRC_16.CRC_Check(COM->pucSend_Buffer,11); //计算CRC值
    107. CRC_16.CRC_H = (uint8_t)(CRC_16.CRC_Value >> 8);
    108. CRC_16.CRC_L = (uint8_t)CRC_16.CRC_Value;
    109. *(COM->pucSend_Buffer+11) = CRC_16.CRC_L;
    110. *(COM->pucSend_Buffer+12) = CRC_16.CRC_H;
    111. //发送数据
    112. UART3.SendArray(COM->pucSend_Buffer,13);
    113. }
    114. }
    115. /*
    116. * @name Modbus_Read_Register
    117. * @brief 写寄存器
    118. * @param UART -> 串口指针
    119. * @retval None
    120. */
    121. static void Modbus_Wrtie_Register(UART_t* UART)
    122. {
    123. UART_t* const COM = UART;
    124. uint8_t i;
    125. 回应数据
    126. //准备数据
    127. for(i=0;i<8;i++)
    128. {
    129. *(COM->pucSend_Buffer+i) = *(COM->pucRec_Buffer+i);
    130. }
    131. //发送数据
    132. UART3.SendArray(COM->pucSend_Buffer,8);
    133. //提取数据
    134. //校验地址 -> 继电器
    135. if((*(COM->pucRec_Buffer+2) == 0x9C) && (*(COM->pucRec_Buffer+3) == 0x43))
    136. {
    137. //控制继电器
    138. if(*(COM->pucRec_Buffer+5) == 0x01)
    139. {
    140. Relay.Relay_ON();
    141. }
    142. else
    143. {
    144. Relay.Relay_OFF();
    145. }
    146. }
    147. //校验地址 -> 蜂鸣器
    148. if((*(COM->pucRec_Buffer+2) == 0x9C) && (*(COM->pucRec_Buffer+3) == 0x44))
    149. {
    150. //控制蜂鸣器
    151. if(*(COM->pucRec_Buffer+5) == 0x01)
    152. {
    153. Buzzer.ON();
    154. }
    155. else
    156. {
    157. Buzzer.OFF();
    158. }
    159. }
    160. }
    161. /********************************************************
    162. End Of File
    163. ********************************************************/

    CRC校验代码

    1. /* Includes ------------------------------------------------------------------*/
    2. #include "MyApplication.h"
    3. /* Private define-------------------------------------------------------------*/
    4. /* Private variables----------------------------------------------------------*/
    5. /* Private function prototypes------------------------------------------------*/
    6. static uint16_t CRC_Check(uint8_t*,uint8_t); //CRC校验
    7. /* Public variables-----------------------------------------------------------*/
    8. CRC_16_t CRC_16 = {0,0,0,CRC_Check};
    9. /*******************************************************
    10. 说明:CRC添加到消息中时,低字节先加入,然后高字
    11. CRC计算方法:
    12. 1.预置116位的寄存器为十六进制FFFF(即全为1);称此寄存器为CRC寄存器;
    13. 2.把第一个8位二进制数据(既通讯信息帧的第一个字节)与16位的CRC寄存器的低
    14. 8位相异或,把结果放于CRC寄存器;
    15. 3.把CRC寄存器的内容右移一位(朝低位)用0填补最高位,并检查右移后的移出位;
    16. 4.如果移出位为0:重复第3步(再次右移一位);
    17. 如果移出位为1:CRC寄存器与多项式A001(1010 0000 0000 0001)进行异或;
    18. 5.重复步骤34,直到右移8次,这样整个8位数据全部进行了处理;
    19. 6.重复步骤2到步骤5,进行通讯信息帧下一个字节的处理;
    20. 7.将该通讯信息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低
    21. 字节进行交换;
    22. ********************************************************/
    23. /*
    24. * @name CRC_Check
    25. * @brief CRC校验
    26. * @param CRC_Ptr->数组指针,LEN->长度
    27. * @retval CRC校验值
    28. */
    29. uint16_t CRC_Check(uint8_t *CRC_Ptr,uint8_t LEN)
    30. {
    31. uint16_t CRC_Value = 0;
    32. uint8_t i = 0;
    33. uint8_t j = 0;
    34. CRC_Value = 0xffff;
    35. for(i=0;i<LEN;i++)
    36. {
    37. CRC_Value ^= *(CRC_Ptr+i);
    38. for(j=0;j<8;j++)
    39. {
    40. if(CRC_Value & 0x00001)
    41. CRC_Value = (CRC_Value >> 1) ^ 0xA001;
    42. else
    43. CRC_Value = (CRC_Value >> 1);
    44. }
    45. }
    46. CRC_Value = ((CRC_Value >> 8) + (CRC_Value << 8)); //交换高低字节
    47. return CRC_Value;
    48. }
    49. /********************************************************
    50. End Of File
    51. ********************************************************/

     

     

  • 相关阅读:
    吃鸡缺少msvcp140.dll解决方法,msvcp140.dll丢失的3个修复方法
    串口字符串转换
    Apache DolphinScheduler 在奇富科技的首个调度异地部署实践
    OSED 考试总结
    华为OD机试 - BOSS的收入 - 回溯(Java 2023 B卷 100分)
    Shiziku 开启adb权限 之 三星S10+ 主板机
    设备树和uboot启动,kernel启动
    【Mysql】EXPLAIN
    什么是RPA自动化办公?
    赛轮集团受邀出席2024国际新能源智能网联汽车创新生态大会
  • 原文地址:https://blog.csdn.net/qq_28576837/article/details/127981068