STM32:实现DMA模式下的UART通信

9

我正在尝试实现DMA模式下的UART,以便每次按下按钮时都能传输一个简单字符串。

因此,我使用了CubeMX生成代码,并配置了UART2 TX DMA在正常(非循环)模式下,也没有FIFO和突发。

每当我以调试模式运行代码时,我会看到第一次尝试发送字符串时,它可以正常工作并发送字符串,但在DMA IRQ处理程序中,它调用TxHalfCpltCallback而不是TxCpltCallback ,并且UART gState将保持BUSY模式,因此我无法再使用它来传输字符串。

我的问题是为什么会调用TxHalfCpltCallback而不是TxCpltCallback? 我应该如何处理它(因为HAL参考手册表示它等待发送缓冲区的后半部分!什么?)

还有,发送下一半的数据是否会释放UART的gState?

我想请某人给我们提供一个配置项目中UART的示例。

7个回答

25

如果您正在使用DMA,则将有两个中断:一个是当缓冲区的一半被传输时触发,另一个是当第二半部分(全部)被传输时触发。

如果一切正常,这两个中断都应该被触发。其原因在于,当发送大量数据时,您可以在TxHalfCpltCallback中向缓冲区的第一半部分加载新数据,同时DMA正在传输缓冲区的第二半部分。而且,在TxCpltCallback中,当第一半部分正在传输时,您可以再次将新数据加载到缓冲区的第二半部分。

优点在于,您不必等待整个传输完成,然后再将下一块数据复制到缓冲区中,而是可以在传输仍在进行时就开始加载它。

以下是一个例子:

在此示例中,使用DMA传输2000字节,通过传输一半完成传输完成中断来实现最佳性能。

CPU在传输一半完成中断回调中向传输缓冲区的第一半部分加载新数据,而DMA在后台传输缓冲区的第二半部分。

然后,在传输完成中,CPU通过新数据将传输缓冲区的第二半部分加载,同时DMA在后台传输先前更新的第一半部分。

#include "stm32f4xx.h"

uint8_t dma_buffer[2000];
volatile uint8_t toggle = 0;

UART_HandleTypeDef huart2;
DMA_HandleTypeDef hdma_usart2_tx;

void uart_gpio_init()
{
  GPIO_InitTypeDef GPIO_InitStruct;

  __GPIOA_CLK_ENABLE();

  /**USART2 GPIO Configuration
  PA2     ------> USART2_TX
  PA3     ------> USART2_RX
  */
  GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_3;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

void uart_dma_init()
{
  /* DMA controller clock enable */
  __DMA1_CLK_ENABLE();

  /* Peripheral DMA init*/
  hdma_usart2_tx.Instance = DMA1_Stream6;
  hdma_usart2_tx.Init.Channel = DMA_CHANNEL_4;
  hdma_usart2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
  hdma_usart2_tx.Init.PeriphInc = DMA_PINC_DISABLE;
  hdma_usart2_tx.Init.MemInc = DMA_MINC_ENABLE;
  hdma_usart2_tx.Init.PeriphDataAlignment = DMA_MDATAALIGN_BYTE;
  hdma_usart2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
  hdma_usart2_tx.Init.Mode = DMA_NORMAL;
  hdma_usart2_tx.Init.Priority = DMA_PRIORITY_LOW;
  hdma_usart2_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
  HAL_DMA_Init(&hdma_usart2_tx);

  __HAL_LINKDMA(&huart2,hdmatx,hdma_usart2_tx);

  /* DMA interrupt init */
  HAL_NVIC_SetPriority(DMA1_Stream6_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Stream6_IRQn);
}

void uart_init()
{
  __USART2_CLK_ENABLE();

  huart2.Instance = USART2;
  huart2.Init.BaudRate = 115200;
  huart2.Init.WordLength = UART_WORDLENGTH_8B;
  huart2.Init.StopBits = UART_STOPBITS_1;
  huart2.Init.Parity = UART_PARITY_NONE;
  huart2.Init.Mode = UART_MODE_TX_RX;
  huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  HAL_UART_Init(&huart2);

  /* Peripheral interrupt init*/
  HAL_NVIC_SetPriority(USART2_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(USART2_IRQn);
}

/* This function handles DMA1 stream6 global interrupt. */
void DMA1_Stream6_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_usart2_tx);
}

void USART2_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart2);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
  uint16_t i;
  toggle = !toggle;

  for(i = 1000; i < 1998; i++)
  {
    if(toggle)
      dma_buffer[i] = '&';
    else
      dma_buffer[i] = 'z';
  }

  dma_buffer[1998] = '\r';
  dma_buffer[1999] = '\n';
}

void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart)
{
  uint16_t i;

  for(i = 0; i < 1000; i++)
  {
    if(toggle)
      dma_buffer[i] = 'y';
    else
      dma_buffer[i] = '|';
  }
}

int main(void)
{
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  uart_gpio_init();
  uart_dma_init();
  uart_init();

  uint16_t i;

  for(i = 0; i < 1998; i++)
  {
    dma_buffer[i] = 'x';
  }

  dma_buffer[1998] = '\r';
  dma_buffer[1999] = '\n';

  while(1)
  {
    HAL_UART_Transmit_DMA(&huart2, dma_buffer, 2000);
  }
}

这个例子是为STM32F4 Discovery板(STM32F407VG)编写的。相应的DMA实例、UART-DMA通道、GPIO和替代功能设置应根据使用的STM32微控制器进行更改。


优点在于您无需等待整个传输完成,就可以将下一块数据复制到缓冲区中,而且您可以在传输仍在进行时开始加载它。这就是为什么HAL_UART_TxHalfCpltCallback和HAL_UART_TxCpltCallback实际上会在传输完成一半/全部之前触发的原因吗? - Alex Shenfield
@AlexShenfield,“HAL_UART_TxHalfCpltCallback”实际上是在发送了缓冲区的一半时触发,而“HAL_UART_TxCpltCallback”则是在整个缓冲区都发送完成时触发。 - Bence Kaulics
我假设这并不一定是当缓冲区的数据实际上被放到“线路”上时发生的? 我已经将我的板连接到逻辑分析仪,并获得了类似于https://visualgdb.com/w/wp-content/uploads/2017/09/21-dma.png的跟踪。在传输一半/结束之前,tx一半完成和tx完成中断就会触发。 - Alex Shenfield
抱歉,问题有点菜鸟 :-) - Alex Shenfield
@Alex 嗯,是的,GPIO 切换会添加一些测量误差,而且发送的数据也不是很多。如果有 1-2 千字节的话,我认为它会更加准确。但是,确实不是那么精确。 :) - Bence Kaulics
1
我知道这个问题已经有5年了,但是如果有人想知道为什么半中断和完成中断不是在中间触发的话,那是因为据我所知,它们是在字节通过DMA移入输出移位寄存器时触发的,而不是在字节完全传输完成时触发。 - undefined

11

你的问题看起来类似于DMA UART with HAL remain busy bug。 你应该启用HAL_UART_IRQHandler()

也就是说,在"main.c"(或者任何你初始化硬件的地方)中添加:

HAL_NVIC_SetPriority(USART2_IRQn, 0, 0);                                        
HAL_NVIC_EnableIRQ(USART2_IRQn);

在 "stm32f4xx_it.c" 文件中:
void USART2_IRQHandler(void)
{ 
  HAL_UART_IRQHandler(&huart2);
}

1
尽管这并没有回答实际的OP问题,但这是非常重要的信息。我花了一个小时才弄清楚为什么我的代码只传输一次,然后永远处于HAL_BUSY状态。 - Maple
这应该是繁忙状态的解决方案。 - Simon Maghiar
如果你喜欢使用CubeMX,你也可以在UART设置中的"NVIC Settings"选项卡中启用UART中断。 - chrisemb

5

使用裸寄存器方法编写DMA传输(当然也包括接收)要比使用笨重的HAL库简单得多。

以STM32F446为例(假设寄存器的复位值):

DMA1_Stream6 -> NDTR = nTransfers;
DMA1_Stream6 -> PAR = (uint32_t)&(USART2 -> DR);
DMA1_Stream6 -> M0AR = (uint32_t)&dataBuff;
DMA1_Stream6 -> CR = DMA_SxCR_CHSEL_2 | DMA_SxCR_MINC | DMA_SxCR_DIR_0 | DMA_SxCR_TCIE; // you can enable half transfer enable as well

USART2 -> BRR = FCLK / LOWSPEED;
USART2 -> CR3 |= USART_CR3_DMAT;
USART2 -> CR1 = (USART_CR1_TE | USART_CR1_RE | USART_CR1_UE);
DMA1_Stream6 -> CR |= DMA_SxCR_EN;

相当容易 - 不是吗?

void DMA1_Stream6_IRQHandler(void) {  // now it does nothing only clears the flag
    if(DMA1 -> HISR & (DMA_HISR_TCIF6)) {
        DMA1 -> HIFCR |= DMA_HISR_TCIF6;
        while(!(USART2 -> SR & USART_SR_TC));
    }
}

2

对于那些使用STM32CubeIDE和FreeRTOS的人来说,问题可能在于中断优先级。 FreeRTOS使用configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY来设置可以调用中断安全的FreeRTOS API函数的最高中断优先级。默认情况下,此值设置为5,如果DMA和UART中断具有相同的优先级,则它们将无法触发!

通常,DMA和UART中断函数不会调用FreeRTOS API函数,因此可以更高。对于STM32微控制器,优先级范围是4到0。

要在SM32CubeIDE中实现这一点,您需要在NVIC配置中取消选项“使用FreeRTOS函数”的勾选,并相应地设置DMA和UART中断的优先级:

NVIC示例配置


1
如果您使用该函数

HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) 

CubeMX库中,它将启用所有的DMA中断。您可以通过清除DMA_SxCR寄存器中的HTIE位来禁用一半传输中断。

0
对我来说,当使用DMA时,我遇到了传输错误。通过启用TXE中断,问题得以解决:
void sas_write(char* buf, uint16_t size)
{
    HAL_UART_Transmit_DMA(&uart_handle, buf, size) ;
    __HAL_UART_ENABLE_IT(&uart_handle, UART_IT_TXE) ;
}

0
如果您使用STM32CubeMX生成了项目代码,您还可以在USARTx模式和配置面板的NVIC设置中启用USARTx全局中断,以解决gState保持繁忙时出现的问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接