跳转至

通用通信(Comm)驱动

写在前面的,本文档仅对驱动类中重要部分进行说明,要详细了解各个类的功能请直接看头文件中类的接口。

驱动源码仓库地址:https://github.com/ZJU-HelloWorld/HW-Components

使用通信类别

该驱动用于统一常见的通信方式,同时通过封装使使用者能更加简便使用相应的通信,减少使用者踩坑的情况。目前已实现的通信协议包括:CAN、FDCAN(Classic)、UART。

使用前准备

使用前序要做一下准备:

  • 确保 config.cmake 文件中设置 use_hwcomponents_bsp_comm 选项为 ON,若为 OFF 则根驱动配置需求在 CMakeLists.txt 文件中重配置为 ON

1. 设计说明

为统一通信方式,通用通信驱动被分为通信对象(发送端 Transmitter 和接收端 Receiver)与通信管理器(发送管理器 TxMgr 和接收管理器 RxMgr)。

发送管理器负责管理发送端,当发送管理器对应硬件处于可发送状态且程序中开启了发送,发送管理器将依次获取发送端需要发出的报文并发出,同时考虑到有多个发送端共用一条发送报文的情况(如 4 个电机共用一条控制报文),因此发送管理器还负责将 ID 相同的发送端编码到同一条报文后一起发出的任务。

接收管理器负责管理接收端,当接收管理器对应的硬件接收到了数据,则会根据接收到数据对应的 ID,将数据传递给对应的接收端

Transmitter

用于统一发送端,需要通过通用通信驱动发送的类需继承该接口类,其中 txId 方法用于获取发送 ID,便于发送管理器进行管理。encode 方法用于编码待发送的数据,由发送管理器在需要发送报文时调用,要求仅对需要编码的部分进行处理(建议在其中统计编码成功与编码失败的次数)。txSuccessCb 方法作为发送成功回调,由发送管理器在报文成功发出时调用(建议用于统计发送成功的次数,通过与编码成功次数对比可用于判断编码成功的消息是否都成功发出)。

Receiver

用于统一接收端,需要通过通用通信驱动接收的类需要继承该接口类,其中 rxId 方法用于获取接收 ID,编译接收管理器进行管理。decode 方法用于解码收到的数据,由接收管理器再接收到对应 ID 的报文时调用(建议在其中统计解码成功与解码失败的次数,根据二者可以判断是否有收到对应 ID 的数据以及收到的正确数据是否有更新)。同时还有接口方法 isUpdate(用于获取接收器是否更新),clearUpdateFlag(用于清除清新标志,对应 isUpdate)以及 registerUpdateCallback(提供更新回调的注册接口),这些接口用于在使用者需要时调用。

TxMgr

用于实现某外设的发送功能,具体的外设如 CAN、UART 等均继承于该虚基类。

发送管理器运行的基本流程为:

  1. 在初始化时传入相关的硬件配置,然后通过 addTransmitter 添加所需的发送端,发送管理器会根据发送端的 ID 进行分类管理
  2. 为适配各个发送器可能有不同的发送频率或是交替发送的特殊需求,当某个发送端需要发送时,需调用setTransmitterNeedToTransmit 方法将发送端对应的报文设置为待发送状态(调用一次,发送一次,编码失败是不会发送)
  3. 所有需要发送的发送端调用完 setTransmitterNeedToTransmit 方法后调用startTranmit 方法用以开启本轮控制周期中的报文发送(一轮仅需在最后调用一次)。
  4. 开启发送后,发送管理器会遍历添加到内部的报文 ID,对于需要发送的报文,会调用其对应的所有发送端进行报文编码,当全部通过后发送该报文。
  5. 在硬件对应的可发送回调用继续发送报文,直至本轮所有报文发送完毕。

在最初的设计中本计划当某 ID 对应报文发送完成后便将对应的发送端从发送管理器内部删除,但在实际的测试中,当下次循环开始时若仍有未发完的报文时,容易产生一个中断在添加发送器而另一个中断在删除发送器的情况,因此及其容易导致硬件错误,为此最终方案改为额外增加一个用于记录某条 ID 报文是否需要发送,而不对发送完成的发送端进行删除,由此避免反复增加删除的问题。

RxMgr

用于实现某外设的接收功能,具体的外设如 CAN、UART 等均继承于该虚基类。

  1. 在初始化时传入相关的硬件配置,然后通过 addReceiver 添加所需的接收端,发送管理器会根据接收端的 ID 进行分类管理。
  2. 添加完所有所需的接收端后,调用 startReceive 方法开启接收中断(仅需开启一次)。
  3. 在接受回调中,接收管理器会根据接收到数据对应的 ID,将报文传递给对应 ID 的所有接收端进行数据解包

2. 硬件配置与使用示例

注意:由于内部使用了硬件句柄,因此如果计划将实例作为全局变量时(全局变量初始化时对应的硬件句柄可能会还未初始化完毕),建议采取一下方法:

  1. 声明指针,后续通过 new 的方式进行初始化。
  2. 声明指针,后续通过返回函数(CreateXXXIns)中的静态变量(因为该变量只有在第一次调用该函数时才会运行初始化程序)进行初始化。
  3. 使用无参构造函数,后续调用 init 方法进行初始化。
  4. 使用无参构造函数,后续使用拷贝赋值函数或是移动赋值函数进行初始化。

2.1 CAN

外设驱动配置

CAN 配置

在使用 CAN 进行通信时,使用 CubeMx 配置时先将 CAN 的引脚配置完成,然后调整 PerscalerTime Quanta in Bit Segment 1Time Quanta in Bit Segment 2 使得 Baud Rate 为所需波特率(常见为 1Mb/s,注意配置中 Seg1/(Seg1+Seg2) 应大于 0.2,否者在实际的使用中及其容易导致通信出错,一般建议 0.7~0.8,同时 Seg1 与 Seg2 建议数值上大于 3)。

CAN 中断配置

配置完成后开启发送中断与对应的接受中断(当使用多个 CAN 接收时建议平均分配到两个接收中断上,如 CAN1 使用 RX0 interrupts,CAN2 使用 RX1 interrupt)。

使用示例

以电机组件为例,电机类同时继承了 TransmitterReceiver

C++
#include "can_tx_mgr.hpp"
#include "can_rx_mgr.hpp"
/* 进一步包含其他头文件 */

namespace hw_comm = hello_world::comm;
namespace hw_motor = hello_world::motor;

hw_comm::CanRxMgr* can_rx_mgr_ptr = nullptr;
hw_comm::CanTxMgr* can_tx_mgr_ptr = nullptr;

hw_motor::Motor* motor_ptr_ls[8] = {nullptr};

void Init(void)
{
  /* FIFO 与 CubeMx 中 RX 中断对应 */
  can_rx_mgr_ptr = new hw_comm::CanRxMgr(&hcan1, hw_comm::CanRxMgr::RxType::kFifo0);
  can_tx_mgr_ptr = new hw_comm::CanTxMgr(&hcan1);

  for (uint8_t i = 0; i < 8; i++) {
    /* 创建电机 */
    motor_ptr_ls[i] = hw_motor::A1(i + 1);

    /* 将电机添加到对应 CAN 的发送、接受管理器中 */
    can_tx_mgr_ptr->addTransmitter(motor_ptr_ls[i]);
    can_rx_mgr_ptr->addReceiver(motor_ptr_ls[i]);
  }

  /* 配置过滤器、开启接收同时开启 CAN 外设 */
  can_rx_mgr_ptr->filterInit();
  can_rx_mgr_ptr->startReceive();
  HAL_CAN_Start(&hcan1);
}

/* 以 1ms 为周期调用的函数 */
void Loop1ms(void)
{
  static uint32_t cnt = 0;
  cnt++;

  /*
      设置电机命令
  */

  /* 根据需求进行分频 */
  if (cnt % 3 == 0) {
    /* 将电机设置为发送状态 */
    for (uint8_t i = 0; i < 8; i += 2) {
      can_tx_mgr_ptr->setTransmitterNeedToTransmit(motor_ptr_ls[i]);
    }
  } else if (cnt % 3 == 1) {
    /* 将电机设置为发送状态 */
    for (uint8_t i = 1; i < 8; i += 2) {
      can_tx_mgr_ptr->setTransmitterNeedToTransmit(motor_ptr_ls[i]);
    }
  }

  /* 开启本轮发送 */
  can_tx_mgr_ptr->startTransmit();
}

/* 接收回调函数 */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef* hcan)
{
  can_rx_mgr_ptr->rxFifoMsgPendingCallback(hcan);
}

/* 发送回调函数 */
void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef* hcan)
{
  can_tx_mgr_ptr->txMailboxCompleteCallback(hcan);
}

void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef* hcan)
{
  can_tx_mgr_ptr->txMailboxCompleteCallback(hcan);
}

/* 错误回调函数 */
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef* hcan)
{
  can_rx_mgr_ptr->errorCallback(hcan);
  can_tx_mgr_ptr->errorCallback(hcan);
}

2.2 FDCAN

外设驱动配置

FDCAN 配置

在使用 FDCAN 进行通信时,使用 CubeMx 配置时先将 FDCAN 的引脚配置完成,Frame Format 配置为 Classic mode,然后调整 Nominal PerscalerNominal Time Seg1Nominal Time Seg2 使得 Nominal Baud Rate 为所需波特率(常见为 1Mb/s,注意配置中 Seg1/(Seg1+Seg2) 应大于0.2,否者在实际的使用中及其容易导致通信出错,一般建议 0.7~0.8,同时 Seg1 与 Seg2 建议数值上大于 3)。完成后将配置的参数分别填写到 Data PrescalerData Time Seg1Data Time Seg2

FDCAN RAM配置

进一步填写 Basic Parameters 中自 Message Ram Offset 起的部分。由于所有的 FDCAN 共用大小为 2560 个字的 RAM,因此需要根据实际需要进行配置,同时保证各个 FDCAN 的 RAM 部分不发生交叠(由 Message Ram Offset决定)。

建议的配置方法为: 1. 调整 Message Ram Offset 使每个 FDCAN 基本分到一样的空间,以使用两个 FDCAN 为例,FDCAN1 的 Message Ram Offset 配置为 0,FDCAN2 的 Message Ram Offset 配置为 1280。 2. 若使该驱动提供的 filterInit 进行滤波器配置,则将 Std Filters Nbr 配置为 1,Rx Fifo0 Elmts NbrRx Fifo1 Elmts Nbr (根据实际需求配置,但建议不同的 FDCAN 使用不同的 FIFO)配置为 8 左右,Tx Fifo Queue Elmts Nbr 配置为 8 左右。

FDCAN 中断配置

配置完成后开启中断(当使用多个 FDCAN 时建议平均分配到两个中断上,如 FDCAN1 使用 Interrupt 0,FDCAN2 使用 Interrupt 1)。

使用示例

以电机组件为例,电机类同时继承了 TransmitterReceiver

C++
#include "fdcan_tx_mgr.hpp"
#include "fdcan_rx_mgr.hpp"
/* 进一步包含其他头文件 */

namespace hw_comm = hello_world::comm;
namespace hw_motor = hello_world::motor;

hw_comm::FdCanRxMgr* fdcan_rx_mgr_ptr = nullptr;
hw_comm::FdCanTxMgr* fdcan_tx_mgr_ptr = nullptr;

hw_motor::Motor* motor_ptr_ls[8] = {nullptr};

void Init(void)
{
  fdcan_rx_mang_ptr = new hw_comm::FdCanRxMgr(&hfdcan1, hw_comm::FdCanRxMgr::RxType::kFifo0);
  fdcan_tx_mang_ptr = new hw_comm::FdCanTxMgr(&hfdcan1);

  /* Interrupt Line 需与 CubeMx 中勾选的中断相对应 */
  fdcan_rx_mang_ptr->configInterruptLines(hw_comm::FdCanRxMgr::ItLine::k0);
  fdcan_tx_mang_ptr->configInterruptLines(hw_comm::FdCanTxMgr::ItLine::k0);

  for (uint8_t i = 0; i < 8; i++) {
    /* 创建电机 */
    motor_ptr_ls[i] = hw_motor::A1(i + 1);

    /* 将电机添加到对应 FDCAN 的发送、接受管理器中 */
    fdcan_tx_mgr_ptr->addTransmitter(motor_ptr_ls[i]);
    fdcan_rx_mgr_ptr->addReceiver(motor_ptr_ls[i]);
  }

  /* 配置过滤器、开启接收同时开启 FDCAN 外设 */
  fdcan_rx_mgr_ptr->filterInit();
  fdcan_rx_mgr_ptr->startReceive();
  HAL_FDCAN_Start(&fdhcan1);
}

/* 以 1ms 为周期调用的函数 */
void Loop1ms(void)
{
  static uint32_t cnt = 0;
  cnt++;

  /*
      设置电机命令
  */

  /* 根据需求进行分频 */
  if (cnt % 3 == 0) {
    /* 将电机设置为发送状态 */
    for (uint8_t i = 0; i < 8; i += 2) {
      fdcan_tx_mgr_ptr->setTransmitterNeedToTransmit(motor_ptr_ls[i]);
    }
  } else if (cnt % 3 == 1) {
    /* 将电机设置为发送状态 */
    for (uint8_t i = 1; i < 8; i += 2) {
      fdcan_tx_mgr_ptr->setTransmitterNeedToTransmit(motor_ptr_ls[i]);
    }
  }

  /* 开启本轮发送 */
  fdcan_tx_mgr_ptr->startTransmit();
}

/* 接收回调函数 */
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef* hfdcan, uint32_t RxFifo0ITs)
{
  fdcan_rx_mang_ptr->rxFifoCallback(hfdcan, RxFifo0ITs);
}

/* 发送回调函数 */
void HAL_FDCAN_TxFifoEmptyCallback(FDCAN_HandleTypeDef* hfdcan)
{
  fdcan_tx_mang_ptr->txFifoEmptyCallback(hfdcan);
}

/* 错误回调函数 */
void HAL_FDCAN_ErrorStatusCallback(FDCAN_HandleTypeDef* hfdcan, uint32_t ErrorStatusITs)
{
  fdcan_tx_mang_ptr->errorStatusCallback(hfdcan, ErrorStatusITs);
  fdcan_rx_mang_ptr->errorStatusCallback(hfdcan, ErrorStatusITs);
}

2.3 UART

外设驱动配置

UART DMA 配置

在使用 UART 进行通信时,使用 CubeMx 配置时先将 UART 的引脚配置完成,Basic Parameters 中的参数根据实际需要配置。然后进行 DMA 配置,根据需要配置发送与接收的 DMA,发送的 DMA 的 Mode 配置为 Normal接收的 DMA 的 Mode 配置为 Circular,二者的 Priority 建议配置为 HighVery High

UART 中断配置

进一步配置 UART 的中断,开启 UART 的全局中断,同时尽可能保证 DMA 接收中断的 Preemption PrioritySub Priority低于 global interrupt(即优先级更高

要求 DMA 接收中断优先级更高的原因在于当在中断中接收处理耗时较长时,容易导致 DMA 的半/全接收与 UART 的空闲中断同时被挂起的情况,在当前中断结束后,若二者的优先级相等,则空闲中断会优先触发,进而导致接收处理出现问题(绝大多数情况下半/全接收会在空闲中断前产生)。

使用示例

以遥控器 DT7 组件及自定义通信类为例(包含数据更新测试),遥控器类继承了 Receiver,自定义通信类同时继承了 TransmitterReceiver

假定自定义通信帧为帧头+数据段+帧尾,其中帧头(四字节)为 0x3F 0x4F + 接收 ID(uint_8) + 数据段长度(uint_8),帧尾(1字节)为 0xFF

C++
#include "uart_tx_mgr.hpp"
#include "uart_rx_mgr.hpp"
/* 进一步包含其他头文件 */

namespace hw_comm = hello_world::comm;
namespace hw_rc = hello_world::remote_control;

class TxRx : public hw_comm::Transmitter, public hw_comm::Receiver
{
 public:
  TxRx(uint32_t tx_id, uint32_t rx_id)
      : Transmitter(), Receiver(), tx_id_(tx_id), rx_id_(rx_id) {}
  virtual ~TxRx(void) = default;

  virtual uint32_t txId(void) const override
  {
    return tx_id_;
  }

  virtual uint32_t rxId(void) const override
  {
    return rx_id_;
  }

  virtual bool encode(uint8_t* data, size_t* len) override
  {
    if (*len >= 8) {
      /* 帧头 */
      data[0] = 0x3F;
      data[1] = 0x4F;
      data[2] = tx_id_;  // 接收方ID
      data[3] = 0x03;    // 数据长度

      /* 数据段 */
      data[4] = 0xAA;
      data[5] = 0xBB;
      data[6] = 0xCC;

      /* 帧尾 */
      data[7] = 0xFF;

      *len = 8;

      encode_success_cnt_++;
      return true;
    } else {
      encode_fail_cnt_++;
      return false;
    }
  }

  virtual void txSuccessCb(void) override
  {
    transmit_success_cnt_++;
  }

  virtual bool decode(const uint8_t* data, size_t len) override
  {
    /* 帧头帧尾校验 */
    if (data[0] == 0x3F && data[1] == 0x4F && data[2] == rx_id_ &&
        data[3] == len - 5 && data[len - 1] == 0xFF && data[3] <= 13) {
      memset(data_, 0, 13);
      memcpy(data_, data + 4, data[3]);
      decode_success_cnt_++;
      is_updated_ = true;

      if (update_cb_) {
        update_cb_();
      }
      return true;
    } else {
      decode_fail_cnt_++;
      return false;
    }
  }

  virtual bool isUpdate(void) const override { return is_updated_; }

  virtual void clearUpdateFlag(void) override { is_updated_ = false; }

  virtual void registerUpdateCallback(pUpdateCallback cb) override
  {
    update_cb_ = cb;
  }

 private:
  uint32_t tx_id_ = 0;
  uint32_t rx_id_ = 0;

  uint8_t data_[13] = {0};

  uint8_t encode_success_cnt_ = 0;
  uint8_t encode_fail_cnt_ = 0;
  uint8_t transmit_success_cnt_ = 0;

  uint8_t decode_success_cnt_ = 0;
  uint8_t decode_fail_cnt_ = 0;

  bool is_updated_ = false;
  pUpdateCallback update_cb_ = nullptr;
};

class DataProcesser : public hello_world::MemMgr
{
 public:
  static bool ProcessData(const uint8_t* rx_data, size_t rx_data_len,
                          size_t* rx_data_processed_len, uint32_t* id,
                          uint8_t* processed_data, size_t* processed_data_len)
  {
    for (size_t i = 0; i < rx_data_len; i++) {
      if (decodeByte(rx_data[i], processed_data, processed_data_len)) {
        *rx_data_processed_len = i + 1;
        *id = processed_data[2];
        *processed_data_len = processed_data[3] + 5;
        return true;
      }
    }

    return false;
  }

  private:
  enum class Fsm {
    kWaiForHeader,
    kWaitForData,
    kWaitForCheck,
  };

  DataProcesser(void) = default;
  virtual ~DataProcesser(void) = default;

  static bool decodeByte(uint8_t byte, uint8_t* processed_data,
                         size_t* processed_data_len)
  {
    /* 数据溢出则直接重新接收 */
    if (*processed_data_len == tmp_data_idx_) {
      tmp_data_idx_ = 0;
      fsm_ = Fsm::kWaiForHeader;
    }

    /* 数据处理 */
    switch (fsm_) {
      case Fsm::kWaiForHeader:
        if (byte == 0x3F && tmp_data_idx_ == 0) {
          processed_data[tmp_data_idx_++] = byte;
        } else if (byte == 0x4F && tmp_data_idx_ == 1) {
          processed_data[tmp_data_idx_++] = byte;
        } else if (tmp_data_idx_ == 2 || tmp_data_idx_ == 3) {
          processed_data[tmp_data_idx_++] = byte;
          if (tmp_data_idx_ == 4) {
            fsm_ = Fsm::kWaitForData;
          }
        } else {
          tmp_data_idx_ = 0;
        }
        break;
      case Fsm::kWaitForData:
        processed_data[tmp_data_idx_++] = byte;
        if (tmp_data_idx_ == processed_data[3] + 4) {
          fsm_ = Fsm::kWaitForCheck;
        }
        break;
      case Fsm::kWaitForCheck:
        processed_data[tmp_data_idx_++] = byte;
        if (byte == 0xFF) {
          tmp_data_idx_ = 0;
          fsm_ = Fsm::kWaiForHeader;
          return true;
        } else {
          fsm_ = Fsm::kWaiForHeader;
          return false;
        }
        break;
    }

    return false;
  }

  static Fsm fsm_;
  static size_t tmp_data_idx_;
};

DataProcesser::Fsm DataProcesser::fsm_ = DataProcesser::Fsm::kWaiForHeader;
size_t DataProcesser::tmp_data_idx_ = 0;

hw_comm::UartRxMgr* uart_rx_mgr_ptr = nullptr;
hw_comm::UartTxMgr* uart_tx_mgr_ptr = nullptr;
hw_comm::UartRxMgr* rc_rx_mgr_ptr = nullptr;

hw_rc::DT7* rc_ptr = nullptr;
TxRx* tx_rx1_ptr = nullptr;
TxRx* tx_rx2_ptr = nullptr;

/* 看门狗刷新函数(封装) */
static void RefreshIwdg(void)
{
  HAL_IWDG_Refresh(&hiwdg1);
}

void Init(void)
{
  tx_rx1_ptr = new TxRx(0x01, 0x02);
  tx_rx2_ptr = new TxRx(0x02, 0x01);

  uart_rx_mgr_ptr = new hw_comm::UartRxMgr(
      &huart6, hw_comm::UartRxMgr::EofType::kManual, 14, 14);

  /* 配置自定义处理函数 */
  uart_rx_mgr_ptr->registerProcessDataFunc(DataProcesser::ProcessData);
  uart_rx_mgr_ptr->addReceiver(tx_rx1_ptr);
  uart_rx_mgr_ptr->addReceiver(tx_rx2_ptr);

  uart_tx_mgr_ptr = new hw_comm::UartTxMgr(&huart6, 13);
  uart_tx_mgr_ptr->addTransmitter(tx_rx1_ptr);
  uart_tx_mgr_ptr->addTransmitter(tx_rx2_ptr);

  rc_ptr = new hw_rc::DT7();
  /* 注册更新回调函数,用于在接收到遥控器信号时刷新看门狗 */
  rc_ptr->registerUpdateCallback(RefreshIwdg);

  rc_rx_mgr_ptr = new hw_comm::UartRxMgr(
      &huart3, hw_comm::UartRxMgr::EofType::kIdle,
      hw_rc::DT7::kRcRxDataLen + 1, hw_rc::DT7::kRcRxDataLen);
  rc_rx_mgr_ptr->addReceiver(rc_ptr);

  /* 开启接收 */
  uart_rx_mgr_ptr->startReceive();
  rc_rx_mgr_ptr->startReceive();
}

/* 以 1ms 为周期调用的函数 */
void Loop1ms(void)
{
  static uint32_t cnt = 0;
  cnt++;

  /* 定时发送 */
  if (cnt % 1000 == 0) {
    uart_tx_mgr_ptr->setTransmitterNeedToTransmit(tx_rx1_ptr);
  } else if (cnt % 1000 == 500) {
    uart_tx_mgr_ptr->setTransmitterNeedToTransmit(tx_rx2_ptr);
  }

  /* 开启本轮发送 */
  uart_tx_mgr_ptr->startTransmit();

  /* 当遥控器更新时处理数据 */
  if (rc_ptr->isUpdate()) {
    rc_ptr->clearUpdateFlag();
    ...
  }
}

/* 接收回调函数 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef* huart, uint16_t Size)
{
  uart_rx_mgr_ptr->rxEventCallback(huart, Size);
  rc_rx_mgr_ptr->rxEventCallback(huart, Size);
}

/* 错误回调 */
void HAL_UART_ErrorCallback(UART_HandleTypeDef* huart)
{
  uart_rx_mgr_ptr->errorCallback(huart);
  rc_rx_mgr_ptr->errorCallback(huart);
}

/* 发送回调函数 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef* huart)
{
  uart_tx_mgr_ptr->txCpltCallback(huart);
}

HAL 库 UART 部分注意事项

  1. 串口接收常用 HAL_UARTEx_ReceiveToIdle_DMA 进行接收,其对应的回调函数为 HAL_UARTEx_RxEventCallback(触发空闲中断时会被调用),而对于 DMA 接收而言,其常见的回调函数为 HAL_UART_RxCpltCallbackHAL_UART_RxHalfCpltCallback,但当使用 HAL_UARTEx_ReceiveToIdle_DMA 进行 DMA 接收时,DMA 的半接收与全接受会转而调用 HAL_UARTEx_RxEventCallback,而触发的来源可以通过 HAL_UARTEx_GetRxEventType 函数进行确认。
  2. 串口的 DMA 接收范围 NormalCircular 两种模式。前者每次都从头开始接收,而后者则会紧跟上一次数据继续接收(比如上一次接收的数据最后一位位于索引为 10 的位置,则下一次接收将从索引为 11 的位置开始)。同时前者开启一次接收一次,后者则只需开启一次便会持续接收,因此前者常用的使用方法为在接收回调中在处理完数据后再次调用 HAL_UARTEx_ReceiveToIdle_DMA 开启接收。
  3. 开启 HAL_UARTEx_ReceiveToIdle_DMA 时不一定总是开启成功,因为某些原因可能导致错误位被置位,因而导致 HAL_UARTEx_ReceiveToIdle_DMA 开启失败。实际使用时可以一直调用直到返回 HAL_OK,或是在 HAL_UART_ErrorCallback 中再次调用以开启接收。
  4. HAL_UARTEx_RxEventCallback 函数的 Size 参数代表的是当前 DMA 接收到的数据的最后一位相对缓冲区开头的长度(为索引 +1),而非本次接收到的数据长度(Normal 等效于接收到数据的长度,而 Circular 不是),同时当接收的数据达到缓冲区半长与全长时都会触发,要注意的是,当接收到的数据恰好为缓冲区长度一半的时候,正常情况下会先因为半接收触发一次 HAL_UARTEx_RxEventCallback,然后再因为空闲中断再次触发一次 HAL_UARTEx_RxEventCallback。同时当接收的数据长度恰好为缓冲区长度时只会因全接收触发一次 HAL_UARTEx_RxEventCallback,而空闲中断不会触发。

H7 变量位置配置

对 H7 而言 DMA 可工作的区域必须大于 0x24000000(具体查看数据手册),因此需修改 CubeMx 生成的 STM32xxx_FLASH.ld 配置文件,具体方法为:

  1. MEMORY 中可看到 RAM_D1 区域满足位于 DMA 可工作范围内,将如下代码拷贝到 SECTIONS 之中(建议放 ._user_heap_stack 后)
Text Only
/* Define output sections */
SECTIONS
{
...

/* ADD RAM_D1 section */
.RAM_D1 :
{
. = ALIGN(4);
*(.RAM_D1)
*(.RAM_D1.*)
. = ALIGN(4);
} >RAM_D1

...
}
  1. 在需要 DMA 工作的变量的声明后补上 __section("RAM_D1.<VAR>")<VAR>为变量名,为方便在 *.map 文件中查找),比如:
C++
// uint8_t buf[32];
uint8_t buf[32] __section("RAM_D1.buf");

如此变量便可以编译到指定的位置上。

  1. 变量及其编译位置可在 build 生成的 *.map 文件中直接搜索变量查看。
  2. 注意:每次使用 STM32CubeMX 生成代码时,STM32xxx_FLASH.ld 配置文件会被修改为原样,因此需重新修改
  1. 将该指定到 DMA 可工作区域的变量提供给串口接收和发送管理器作为 DMA 缓冲区。

3. 调试技巧

  1. 所有的接收管理器与发送管理器内部均有 status_ 数据成员,用于标识错误(通过或运算结合),当管理器未正常工作时可在 Ozone 中进行查看,看是否由错误标志被置位。
  2. 所有接收管理器中具有 decode_success_cnt_ 数据成员,当有报文被接收端成功解包时,该变量会随时间变大,当该值不发生变化时则说明未接受到数据或是报文没有被任何接收器解包
  3. 所有发送管理器中具有 encode_success_cnt_ 数据成员,当有报文被成功编码时,该变量会随时间变大,当该值不发生变化时则说明没有任何报文通过编码