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] | 传输方向 |
|---|---|
00 | Peripheral-to-memory |
01 | Memory-to-peripheral |
10 | Memory-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 尚未开始正常接收。
为方便检查,可以按以下顺序初始化:
- 开启 GPIO、USART2 和 DMA1 时钟,配置 PA3 的 AF7。
- 保持 USART 关闭,关闭 DMA Stream,并等待
EN清零。 - 配置 DMA 通道、方向、地址、长度和 circular mode。
- 清除 DMA 遗留标志,配置中断。
- 配置 USART 波特率和帧格式,清除遗留接收状态。
- 设置
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 会在计数归零后重新装载长度,从缓冲区起始位置继续写入。应用需要及时消费数据,避免尚未处理的内容被覆盖。
备注:内容仅供参考。
