看这篇文章之前要对Modbus协议要有一个简单的了解,本篇文章以STM32单片机为例写一个简易版的从机Modbus.

Modbus通信机制需要单片机两个外设资源:串口和定时器。

设一个向上计数的定时器,计数周期为3.5个字符的时间。3.5个字符时间如何计算请参考这个https://zhidao.baidu.com/question/2266066387336737428.html

其实这个时间设长一点也没关系,比如设个10ms,100ms甚至是1s,如果设为1s,主机的Modbus发送两帧数据的间隔就不能低于1s,看完从机的具体实现就会明白为什么了。

用Stm32CubeIDE配置一个50ms中断的定时器

 再配置串口

串口中断回调函数如下:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);
  /* NOTE: This function Should not be modified, when the callback is needed,
           the HAL_UART_TxCpltCallback could be implemented in the user file
   */
    if(huart->Instance == huart1.Instance)
    {
        Rx_Buf[RxCount++] = aRx1Buffer;//把接收到的数据存入接收缓存数组中
        __HAL_TIM_SET_COUNTER(&htim7,0);//只要有数据进来就离开清空定时器计数器,如果没有新的数据进来即从此刻开始计时50ms,进入定时器中断回调函数,此时意味着接收完一帧数据。
        HAL_TIM_Base_Start_IT(&htim7);//启动定时器
        HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRx1Buffer, 1);   //再开启接收串口中断
    }
}

 在定时器中断回调函数如下:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == htim7.Instance)
    {
        SLAVE_RS485_SEND_MODE;//485切换成发送模式,不再接受新的串口中断
        HAL_TIM_Base_Stop_IT(&htim7);//停止定时器
        MB_Parse_Data();// 提取数据帧,进行解析数据帧
        MB_Analyze_Execute();//对接收到的数据进行分析并执行
        RxCount = 0;//清空接收计数
        SLAVE_RS485_RECEIVE_MODE;//数据处理完毕后,再重新接收串口中断
    }
}
串口引脚接了485芯片,SLAVE_RS485_SEND_MODE和SLAVE_RS485_RECEIVE_MODE就是普通的IO口,用于控制485芯片发送与接收模式
#define SLAVE_RS485_SEND_MODE                     HAL_GPIO_WritePin(USART1_EN_GPIO_Port, USART1_EN_Pin, GPIO_PIN_SET)
#define SLAVE_RS485_RECEIVE_MODE              HAL_GPIO_WritePin(USART1_EN_GPIO_Port, USART1_EN_Pin, GPIO_PIN_RESET)

接下来看提取数据帧函数

/* 提取数据帧,进行解析数据帧 */
void MB_Parse_Data()
{
  PduData.Code = Rx_Buf[1];                   // 功能码
  PduData.Addr = ((Rx_Buf[2]<<8) | Rx_Buf[3]);// 寄存器起始地址
  PduData.Num  = ((Rx_Buf[4]<<8) | Rx_Buf[5]);// 数量(Coil,Input,Holding Reg,Input Reg)
  PduData._CRC = MB_CRC16((uint8_t*)&Rx_Buf,RxCount-2);             // CRC校验码
  PduData.byteNums = Rx_Buf[6];               // 获得字节数
}
PduData是一个结构体如下:
/* 类型定义 ------------------------------------------------------------------*/
typedef struct {
  __IO uint8_t  Code ;              // 功能码
  __IO uint8_t byteNums;             // 字节数
  __IO uint16_t Addr ;            // 操作内存的起始地址
  __IO uint16_t Num;                 // 寄存器或者线圈的数量
  __IO uint16_t _CRC;                 // CRC校验码
  __IO uint8_t *ValueReg;           // 10H功能码的数据
}PDUData_TypeDef;
MB_CRC16校验函数适用于校验一帧数据是否正确,这个函数需要数据表很占篇幅,稍后把它放到文章的最后,读者直接调用即可。接下来看最重要的对接收到的数据进行分析并执行
我们以简单的写线圈寄存器为例
/**
  * 函数功能: 对接收到的数据进行分析并执行
  * 输入参数: 无
  * 返 回 值: 异常码或0x00
  * 说    明: 判断功能码,验证地址是否正确.数值内容是否溢出,数据没错误就发送响应信号
  */
uint8_t MB_Analyze_Execute(void )
{
  uint16_t ExCode = EX_CODE_NONE;
    uint16_t tem_crc;
    if(PduData._CRC !=((Rx_Buf[RxCount-1])<<8 | Rx_Buf[RxCount-2]))
    {
        /* Modbus异常响应 */
      ExCode = EX_CODE_02H;            // 异常码02H
       return ExCode;
    }

  /* 根据功能码分别做判断 */
  switch(PduData.Code)
  {
   /* ---- 01H  02H 读取离散量输入(Coil Input)---------------------- */
    case FUN_CODE_01H:
        break;
        case FUN_CODE_02H:
        break;
        case FUN_CODE_03H:
        break;
        case FUN_CODE_04H:
        break;
        case FUN_CODE_05H:
            /* 写入一个线圈值 */
      if(PduData.Num == 0xFF00)
      {
         usCoilBuf[PduData.Addr] = 1;//把这个PduData.Addr地址的线圈寄存器写1
      }
      else
      {
         usCoilBuf[PduData.Addr] = 0;//把这个PduData.Addr地址的线圈寄存器写0
      }
        MB_SendRX();//把接收到的数据原封不动的发送出去(即应答)
        break;
  }
  /* 数据帧没有异常 */
  return ExCode; //   EX_CODE_NONE
}


12-23 19:18
查看更多