PAYATHON 2026

STM32F446RE 接收字节为何触发 DMA Transfer error 中断?

排障赵师傅

使能顺序基本可行,先修正代码中的两处错误:

  • DMA_Config(USART2->DR, ...) 传入的是数据寄存器的当前值,DMA 的 PAR 需要的是寄存器地址。
  • pRXStream->CR |= (1 << 6) 将传输方向设成了 Memory-to-peripheral。UART 接收应使用 Peripheral-to-memory,即 DIR[1:0] = 00。

这两处配置会在首个接收请求到来时导致错误的 DMA 总线访问,可以解释“初始化正常,一收到字节就发生 Transfer error”的现象。要确定具体是哪次访问失败,还需检查中断发生时的 PAR、M0AR、CR 和 HISR。

为什么初始化正常,收到字节才报错?

使能 DMA Stream 不会立即触发 UART 数据传输。USART 收到数据并发出 DMA 请求后,DMA 才会按配置的地址和方向访问总线。

原代码:

DMA_Config(USART2->DR, (uint32_t)pDest, RXSIZE);

会先读取 USART2->DR,再把读出的值写入 DMA1_Stream5->PAR。例如,读出值为 0 时,PAR 就会被设为 0x00000000,而不是 USART 数据寄存器的地址。

应改为:

DMA_Config((uint32_t)&USART2->DR, (uint32_t)pDest, RXSIZE);

DMA_SxCR.DIR[1:0] 的编码如下:

DIR[1:0]传输方向
00Peripheral-to-memory
01Memory-to-peripheral
10Memory-to-memory

下面这行代码设置的方向与原注释相反:

pRXStream->CR |= (1 << 6); // 实际设置为 Memory-to-peripheral

应改为:

pRXStream->CR &= ~DMA_SxCR_DIR;

UART 接收时,PAR 始终指向 USART2->DR,M0AR 指向接收缓冲区。方向设置错误时,应修正 DIR,不要交换这两个地址。

使能顺序应该怎样安排?

原来的顺序:

RE → DMAR → DMA Stream EN → UE

在首次初始化、USART 此前处于关闭状态且没有遗留请求的情况下,可以工作。由于最后才设置 UE,配置期间 USART 尚未开始正常接收。

为方便检查,可以按以下顺序初始化:

  1. 开启 GPIO、USART2 和 DMA1 时钟,配置 PA3 的 AF7。
  2. 保持 USART 关闭,关闭 DMA Stream,并等待 EN 清零。
  3. 配置 DMA 通道、方向、地址、长度和 circular mode。
  4. 清除 DMA 遗留标志,配置中断。
  5. 配置 USART 波特率和帧格式,清除遗留接收状态。
  6. 设置 RE 和 DMAR,使能 DMA Stream,最后设置 UE。

原代码选择的 DMA1_Stream5、Channel 4 对应 USART2_RX,1 << 27 也确实将 CHSEL[2:0] 设置为 100。需要重点检查的是地址和 DIR。

最小修改示例

使用实际分配的接收缓冲区

建议先由链接器分配缓冲区,避免固定地址与其他变量或栈重叠:

#define RXSIZE 20U

static volatile uint8_t rx_buf[RXSIZE];

int main(void)
{
    UART2_init();
    DMA_init();

    DMA_Config((uint32_t)&USART2->DR,
               (uint32_t)rx_buf,
               RXSIZE);

    UART2_config();

    while (1)
    {
    }
}

volatile 便于 CPU 观察 DMA 更新的内存,但不能解决缓冲区覆盖或读写同步问题。

修正 DMA 配置及清标志方式

可将原来的 DMA_init() 替换为:

#define RX_DMA_CLEAR_FLAGS (DMA_HIFCR_CFEIF5  | \
                            DMA_HIFCR_CDMEIF5 | \
                            DMA_HIFCR_CTEIF5  | \
                            DMA_HIFCR_CHTIF5  | \
                            DMA_HIFCR_CTCIF5)

void DMA_init(void)
{
    DMA_Stream_TypeDef *stream = DMA1_Stream5;

    RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
    (void)RCC->AHB1ENR;

    stream->CR &= ~DMA_SxCR_EN;
    while ((stream->CR & DMA_SxCR_EN) != 0U)
    {
    }

    /*
     * Channel 4
     * DIR = 00: Peripheral-to-memory
     * PINC = 0, MINC = 1
     * PSIZE = MSIZE = 00: byte
     * CIRC = 1
     */
    stream->CR = DMA_SxCR_CHSEL_2 |
                 DMA_SxCR_CIRC    |
                 DMA_SxCR_MINC    |
                 DMA_SxCR_DMEIE   |
                 DMA_SxCR_TEIE    |
                 DMA_SxCR_HTIE    |
                 DMA_SxCR_TCIE;

    stream->FCR = 0U; /* Direct mode */

    DMA1->HIFCR = RX_DMA_CLEAR_FLAGS;

    NVIC_ClearPendingIRQ(DMA1_Stream5_IRQn);
    NVIC_EnableIRQ(DMA1_Stream5_IRQn);
}

原代码中的:

DMA1->HIFCR = 0x00;

不会清除任何标志。HIFCR 采用写 1 清除机制,需要清除哪个标志,就向对应的清除位写 1。中断中也应直接赋值,避免用 |= 对清除寄存器做读改写。

DMA_Config() 的实现可以保留,但必须在 Stream 关闭时调用:

void DMA_Config(uint32_t peripheral_addr,
                uint32_t memory_addr,
                uint16_t count)
{
    DMA1_Stream5->PAR  = peripheral_addr;
    DMA1_Stream5->M0AR = memory_addr;
    DMA1_Stream5->NDTR = count;
}

中断处理还有哪些问题?

进入 DMA IRQ 不等于发生 Transfer error

DMA1_Stream5_IRQHandler() 同时处理半传输、传输完成和错误事件。只有检测到 DMA_HISR_TEIF5,才能确认发生了 Transfer error。

调试时应先保存 HISR,再清除标志。优先处理错误分支;正常事件分别使用独立的 if,避免多个标志同时置位时,else if 跳过其中的事件:

volatile uint32_t dma_rx_error;
volatile uint32_t dma_rx_half_count;
volatile uint32_t dma_rx_full_count;

void DMA1_Stream5_IRQHandler(void)
{
    uint32_t status = DMA1->HISR;

    uint32_t errors = status & (DMA_HISR_TEIF5 |
                                DMA_HISR_DMEIF5 |
                                DMA_HISR_FEIF5);

    if (errors != 0U)
    {
        dma_rx_error = status;

        USART2->CR3 &= ~USART_CR3_DMAR;
        DMA1_Stream5->CR &= ~DMA_SxCR_EN;

        DMA1->HIFCR = RX_DMA_CLEAR_FLAGS;
        return;
    }

    if ((status & DMA_HISR_HTIF5) != 0U)
    {
        DMA1->HIFCR = DMA_HIFCR_CHTIF5;
        ++dma_rx_half_count;
    }

    if ((status & DMA_HISR_TCIF5) != 0U)
    {
        DMA1->HIFCR = DMA_HIFCR_CTCIF5;
        ++dma_rx_full_count;
    }
}

该示例在发生错误后停止接收请求并保留状态,不会自动恢复。重新配置前,仍需等待 Stream 的 EN 清零。

原来的 HT、TC 回调都包含 while (1)。修复 DMA 后,接收 10 字节会触发半传输中断,接收 20 字节会触发传输完成中断,程序仍会停在回调中。实际使用时,处理函数应在记录事件或处理数据后及时返回。

IDLE 标志不能通过写 SR 清除

下面的操作无法按要求清除 STM32F446 USART 的 IDLE 标志:

USART2->SR &= ~(1 << 4);

需要依次读取 SR 和 DR:

void USART2_IRQHandler(void)
{
    uint32_t status = USART2->SR;

    if ((status & USART_SR_IDLE) != 0U)
    {
        (void)USART2->DR;

        /* 在这里记录事件或读取 DMA 剩余计数 */
    }
}

读取 DR 会影响接收状态。与 DMA 配合使用时,需要考虑读取时机,尤其是在连续数据流中。建议先关闭 IDLEIE,确认 DMA 基础接收正常后,再加入空闲检测。

波特率与缓冲区注意事项

USART2->BRR = 0x8B 对应约 115200 baud 的前提是:OVER8 = 0,且 USART2 的 PCLK1 为 16 MHz。如果时钟初始化修改了 APB1 分频,就必须重新计算,不能只根据 CPU 主频判断。

在 16 倍过采样下,可按以下关系计算:

BRR ≈ round(PCLK1 / baud_rate)

使用 pDest = SRAM2_BASE + 0x800 时,需要确认整个接收区域都有效,并已在链接布局中预留。即使地址位于 SRAM 中,直接指定地址也不会让链接器自动避开这块空间。

修正后,可在发送一个字节前后检查:

  • PAR 是否等于 &USART2->DR。
  • CR.DIR 是否为 00。
  • M0AR 是否指向实际缓冲区。
  • 在没有额外接收字节的情况下,NDTR 是否从 20 减至 19。
  • 缓冲区首字节是否更新,TEIF5 是否保持为 0。

Circular mode 会在计数归零后重新装载长度,从缓冲区起始位置继续写入。应用需要及时消费数据,避免尚未处理的内容被覆盖。

备注:内容仅供参考。