缓冲区设定

使用RAM_D2作为读写缓冲区配置

STM32H7 _ _ _ _ _ x_FLASH.ld配置

.dma_buffer : /* 冒号前有一个空格 */
{
  *(.dma_buffer)
} >RAM_D2

然后在需要进行读写缓冲区的文件中添加以下定义:

/// 这里通过宏定义来定义DMA_BUFFER
/// __ICCARM__:表示 IAR EWARM 编译器
/// 所以如果是GCC的话就使用else块中的定义了

#if defined( __ICCARM__ )
  #define DMA_BUFFER \
      _Pragma("location=\".dma_buffer\"")
#else
  #define DMA_BUFFER \
      __attribute__((section(".dma_buffer")))
#endif

#define BUFFER_SIZE 256

DMA_BUFFER uint8_t tx_buffer[BUFFER_SIZE];
static uint8_t tx_idx = 0;
DMA_BUFFER uint8_t rx_buffer[BUFFER_SIZE];

串口异常处理函数

如果不对串口异常进行处理,清理相关错误标记,可能无法继续执行中断函数

/// 错误码是定义如下
#define  HAL_UART_ERROR_NONE             (0x00000000U)    /*!< No error                */
#define  HAL_UART_ERROR_PE               (0x00000001U)    /*!< Parity error            */
#define  HAL_UART_ERROR_NE               (0x00000002U)    /*!< Noise error             */
#define  HAL_UART_ERROR_FE               (0x00000004U)    /*!< Frame error             */
#define  HAL_UART_ERROR_ORE              (0x00000008U)    /*!< Overrun error           */
#define  HAL_UART_ERROR_DMA              (0x00000010U)    /*!< DMA transfer error      */
#define  HAL_UART_ERROR_RTO              (0x00000020U)    /*!< Receiver Timeout error  */

/// 下面错误处理函数中把奇偶校验错误以及数据溢出错误标志清除
/// 可以直接在调试时打断点查看huart->ErrorCode的值
void HAL_UART_ErrorCallback(UART_HandleTypeDef* huart) {
  if (huart->Instance == USART6) {
    __HAL_UART_CLEAR_OREFLAG(huart);
    __HAL_UART_CLEAR_PEFLAG(huart);
    huart->RxState = HAL_UART_STATE_READY;
    HAL_UARTEx_ReceiveToIdle_DMA(huart, rx_buffer, BUFFER_SIZE);
     __HAL_DMA_DISABLE_IT(&hdma_usart6_rx, DMA_IT_HT);
  }
}

关闭半中断

在调用HAL_UARTEx_ReceiveToIdle_DMA函数时默认打开“半中断”,数据在接收到一半(左右)时会触发HAL_UARTEx_RxEventCallback回掉函数, 如果仅需要处理完整数据的可以在调用HAL_UARTEx_ReceiveToIdle_DMA函数后直接执行__HAL_DMA_DISABLE_IT(&hdma_usart6_rx, DMA_IT_HT);,即可关闭半中断

HAL_UARTEx_ReceiveToIdle_DMA(huart, rx_buffer, BUFFER_SIZE);
 __HAL_DMA_DISABLE_IT(&hdma_usart6_rx, DMA_IT_HT);

内存保护单元配置(MPU)

NameValue
MPU RegionEnabled
MPU Region Base Address0x24000000
MPU Region Size512K
MPU SubRegion Disable0x0
MPU TEX field levellevel 0
MPU Access PermissionALL ACCESS PERMITTED
MPU Instruction AccessENABLE
MPU Shareability PermissionENABLE
MPU Cacheable PermissionENABLE
MPU Bufferable PermissionDISABLE

接收 & 发送(非常重要)

H7系列的芯片不论是接收还是发送,在传输数据之前一定要清理并失效缓冲区的数据,否则收发数据将无法正常进行(不会报错但数据是不会被更新的)

以下代码能够正常收发数据:

// DMA缓冲区
DMA_BUFFER uint8_t tx_buffer[TX_BUFFER_SIZE];
DMA_BUFFER uint8_t rx_buffer[RX_BUFFER_SIZE];

// 将数据写入UART发送缓存,并通过DMA非阻塞方式发送。
// 参数: buf - 待发送数据指针;size - 数据字节数。
// 返回: 如果数据超出缓存大小返回-1,否则启动传输并返回0。
int write(const uint8_t* buf, uint16_t size) {
  if (size > TX_BUFFER_SIZE) {
    return -1;  // 错误:数据长度超过缓冲区限制
  }

  memcpy(tx_buffer, buf, size);
  // 清理缓存以确保DMA读取到最新数据
  SCB_CleanInvalidateDCache_by_Addr((uint32_t*)(tx_buffer),
                                    TX_BUFFER_SIZE / 4);
  // 通过DMA发送数据(非阻塞方式)
  int ret = HAL_UART_Transmit_DMA(huart, tx_buffer, size);
  return ret;
}

// 启动DMA接收指定的字节数。
void read(size) {
  SCB_CleanInvalidateDCache_by_Addr((uint32_t*)(rx_buffer),
                                    RX_BUFFER_SIZE / 4);
  HAL_UART_Receive_DMA(huart, rx_buffer, size);
}

数据的接收方式

HAL_UART_Receive_DMA:

接收size个字节,收满后触发中断,回调函数是void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart)

HAL_UARTEx_ReceiveToIdle_DMA

空闲中断,默认情况下开启半满中断,当缓冲区处于半满或全满状态以及空闲状态时会触发中断,回调函数是void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef* huart, uint16_t size),可以通过__HAL_DMA_DISABLE_IT(&hdma_usartx_rx, DMA_IT_HT)关闭半满中断

循环模式(Circular)

DMA接收和读取都可以开启Circular模式,在模型情况下,需要在读中断处理函数中需要重启开启读中断,否则中断函数只会被触发一次。而在循环模式中,只需要在初始化是调用下读取函数,DMA会自动读取数据,当数据到达缓冲区末尾时会回到头部继续写入。

可以通过以下方式获取当前缓冲区中已写入的数据(可读数据)

uint16_t pos = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usartx_rx);

为了能够将DMA的数据完整的接收,不要在中断中执行任何耗时操作,而应该尽快将缓冲区的数据存储到FIFO队列中,然后在RTOS的任务或其他任务中消费

// UART接收事件回调:将新接收数据写入环形缓冲区后释放信号量。
// 参数: huart - UART硬件句柄;size - 接收数据字节数。
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef* huart, uint16_t size) {
  if (huart == huartx) {
    static uint16_t old_pos = 0;  // 显式初始化为0
    uint16_t pos;

    pos = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usartx_rx);
    if (pos != old_pos) {
      if (pos > old_pos) {
        rb_write(&rx_rb, &(rx_buffer[old_pos]), pos - old_pos);
      } else {
        // 数据发生环绕,分两段写入缓冲区
        rb_write(&rx_rb, &(rx_buffer[old_pos]), RX_BUFFER_SIZE - old_pos);
        if (pos > 0) {
          rb_write(&rx_rb, rx_buffer, pos);
        }
      }
      old_pos = pos;
      // 激活信号量,让RTOS消费数据
      osSemaphoreRelease(taskBinarySemHandle);
    }

  }
}

🦉 如何选择

如果封包是整包发送的(一次发送完整的含有包头和包体+校验),推荐使用空闲中断模式,一次接收整个包然后解析 如果封包不是整包发送的,可以使用常规模式先接收包头,然后从包头拿到包体长度后再接收包体 如果有大量数据需要接收(文件,音频流)可使用循环模式+FIFO,先快速将数据挪到FIFO中,然后再对FIFO中的数据进行处理 调试 由于DMA是独立于MCU运行, 所以打断点是无法使其停止的。如果尝试通过打断点去观测DMA接收缓冲区的数据是不可控的。推荐的调试方案还是通过其他方式的输出(串口或网口)把DMA接收到的数据实时输出进行观察。