Uart 不定长收发的 DMA 方案
01 应用功能介绍
串口的通讯在不同应用上经常需要处理不同长度的收发报文需求,对于应用而言,由于发送和接收的长度未知,发送的不定长可通过配置 FIFO 空阈值和 DMA Burst 的方式实现,接收的不定长功能可通过配置 FIFO 空阈值、DMA Burst 和字符超时功能的方式实现。
综上所述,本文将分析实现不定长发送和不定长接收这两个功能。
02 发送不定长
发送不定长功能由三个对象主体构成,Uart TX 模块、DMA 触发器、用户 Memory 操作接口,结构如图 2.1 所示。

图2.1 UART DMA 不定长发送功能框图
1•串口配置
串口基本初始化如代码清单 2.1 所示,初始化内容包括 Uart 功能模式、奇偶校验功能、停止位配置、数据位配置、波特率配置、FIFO 阈值配置。其中注意发送 FIFO 空阈值的设置,与 DMA Burst 设置有密切关系,下文的 DMA 配置会讲。
代码清单 2.1 Uart 初始化
void User_UART1_Init(void)
{
LL_UART_MspInit();
UART1->CR0_b.UE = 0;
UART1->CR1 =
(0x03 << UART1_CR1_LEN_Pos) | //8bit 长度
(0x00 << UART1_CR1_STP_Pos) | //1 个 stop 位
(0x00 << UART1_CR1_PEN_Pos); //关闭奇偶校验
UART1->FIFOCTRL = (0xc << UART1_FIFOCTRL_TXFT_Pos) | //发送 FIFO 空阈值
(0x7 << UART1_FIFOCTRL_RXFT_Pos); //接收 FIFO 满阈值
UART1->BAUD = SystemCoreClock/USER_UART_BAUDRATE; //波特率
UART1->CR0 =
(1 << UART1_CR0_RFR_Pos) | //RX FIFO 复位
(1 << UART1_CR0_TFR_Pos) | //TX FIFO 复位
(0 << UART1_CR0_NFE_Pos) | //使能 FIFO
(1 << UART1_CR0_DTE_Pos) | //使能 TX DMA
(1 << UART1_CR0_UE_Pos) | //使能 UART
(1 << UART1_CR0_RE_Pos) | //使能 RX
(1 << UART1_CR0_TE_Pos) ; //使能 TX
Uart_TxDMA_Config((uint32_t)&TXBuffer1, (uint32_t)&UART1->TDR, sizeof(TXBuffer1)); //配置RX DMA
}
2 •Tx DMA 配置
DMA 配置如代码清单 2.2 所示,需要注意 DMA 传输模式和 DMA burst 的设置(标红部分),本案例传输模式为单次传输,使用连续传输会导致数据发送异常(因为是不定长发送,需每次数据填充完成后,将数据长度填入 DBL 寄存器)。
DMA Burst 配置:数据进入 UART Tx FIFO 后将由硬件自动搬出到 UART 的发送线上,所以一次填入 Tx FIFO 后,数据会不断减少,当剩余数据量到达设定的 UART TxFIFO 空阈值时,就可以产生一次 DMA 请求,此时将自动从内存里的 buf 搬运一次数据,即图 2.1中的用户层 buf(紫色)到 Tx Data 寄存器(蓝色)。

图 2.2 UART TxFIFO 的 DMA 搬运结构
如图 2.2 所示,此时 UART Tx FIFO 内分为两部分,还未发送的 FIFO 出口段与空闲出的 FIFO 入口段。DMA 通道搬运到 FIFO 入口处的空闲段,所以每次 DMA 搬运的块大小要小于空闲段大小才能保证 Tx FIFO 不溢出。
例子:若 DMA burst 设置为每次搬运 4 个长度的数据,此时 UART FIFO 空阈值需设置为≤12,这样每次产生 DMA 请求,都能保证 FIFO 有足够的余量存 DMA 搬运过来的数据。
代码清单 2.2 DMA 配置
void Uart_TxDMA_Config(uint32_t src_addr, uint32_t dst_addr, uint16_t size)
{
DMA->CH[0].REG.CER = 0; //配置 DMA 先禁能对应通道
DMA->CH[0].REG.SAR = src_addr; //配置 DMA 源地址
DMA->CH[0].REG.DAR = dst_addr; //配置 DMA 目标地址
DMA->CH[0].REG.DBL = size;
DMA->CH[0].REG.CTR = (0x06 << DMA_CTR_PRI_Pos) | //通道优先级为高优先级
(0x00 << DMA_CTR_TM_Pos) | //DMA 传输模式 单次传输
(0x01 << DMA_CTR_TC_Pos) | //DMA 传输类型 M2P
(0x00 << DMA_CTR_SAI_Pos) | //DMA 源地址递增
(0x01 << DMA_CTR_DBL_Pos) | //目标端 burst
(0x01 << DMA_CTR_SBL_Pos) | //源端 burst
(0x01 << DMA_CTR_DAI_Pos) | //DMA 目的地址不变
(0x00 << DMA_CTR_SDS_Pos) | //源端传输位宽 = byte
(0x00 << DMA_CTR_DDS_Pos) | //目的端传输位宽 = byte
(0x06 << DMA_CTR_DHF_Pos) | //目标端握手设置为 UART1_tX
(0x00 << DMA_CTR_SMS_Pos) |
(0x00 << DMA_CTR_DMS_Pos);
DMA->CH[0].REG.STR = 0x07; //清除标志位
}
代码清单 2.3 是 UART 中断相关的配置,本例程里不开启中断,后续根据应用开发可选择适当开启。
代码清单 2.3 中断配置
//UART ISR
UART0->IER.bit.TXDONEIE = 0; //UART TX Done Interrupt Enable
UART0->IER.bit.RXDATTOIE = 0; //UART RX Data Timeout Interrupt Enable
UART0->IER.bit.BREAKIE = 0; //UART Break Interrupt Enable
UART0->IER.bit.FEIE = 0; //UART Frame Error Interrupt Enable
UART0->IER.bit.PEIE = 0; //UART Parity Error Interrupt Enable
UART0->IER.bit.RXOVEIE = 0; //RXFIFO Overflow Interrupt Enable
UART0->IER.bit.TXEMPTYIE = 0; //TXFIFO Empty Interrupt Enable
UART0->IER.bit.RXFULLIE = 0; //RXFIFO Full Interrupt Enable
NVIC_EnableIRQ(UART0_IRQn);
NVIC_SetPriority(UART0_IRQn, 1);
3 不定长发送代码处理
不定长发送实现代码如代码清单 2.4 所示,应用流程图如图 2.2 所示。

图 2.3 不定长发送应用框图
向 TXbuffer 填充数据,填充完成后,配置填充数据的长度,使能 DMA 传输(在 DMA配置内不要使能 DMA 传输),等待发送完成标志位置位,再进行下一次填入。通过此方案即可实现不定长发送功能。
代码清单 2.4 发送不定长函数
void test(void)
{
for(uint8_t i = 0;i<20;i++) //数据填充
{
TXBuffer1[i] = i + 1;
}
DMA->CH[0].REG.DBL = 20; //每次填充数据的长度
DMA->CH[0].REG.CER = 1; //使能 DMA 传输
while(UART1->INT_b.TDIF == 0) //等待发送完成标志
;
UART1->INT_b.TDIF = 1;
memset(TXBuffer1, 0, sizeof(TXBuffer1));
}
03 接收不定长
发送不定长功能由三个对象主体构成,Uart RX 模块、DMA 触发器、用户 Memory 操作接口,结构如图 3.1 所示。

图 3.1 UART DMA 不定长接收功能框图
不定长接收实现如代码清单 3.3 所示,处理框图如图 3.2 所示。DMA 搬运完成后,若此时 FIFO 剩余数据不满足 DMA 搬运条件,则字符超时,通过 CPU 读取 FIFO 内剩余数据。
1•串口配置
串口基本初始化如代码清单 3.1 所示,初始化内容包括 Uart 功能模式、奇偶校验功能、停止位配置、数据位配置、波特率配置、FIFO 阈值配置。其中注意发送 FIFO 空阈值的设置,与 DMA Burst 设置有密切关系,下文的 DMA 配置会讲。
代码清单 3.1 Uart 初始化
void User_UART1_Init(void)
{
LL_UART_MspInit();
UART1->CR0_b.UE = 0;
UART1->CR1 =
(0x03 << UART1_CR1_LEN_Pos) | //8bit 长度
(0x00 << UART1_CR1_STP_Pos) | //1 个 stop 位
(0x00 << UART1_CR1_PEN_Pos); //关闭奇偶校验
UART1->FIFOCTRL = (0xc << UART1_FIFOCTRL_TXFT_Pos) | //发送 FIFO 空阈值
(0x7 << UART1_FIFOCTRL_RXFT_Pos); //接收 FIFO 满阈值
UART1->BAUD = SystemCoreClock/USER_UART_BAUDRATE; //波特率
UART1->CR0 =
(1 << UART1_CR0_RFR_Pos) | //RX FIFO 复位
(1 << UART1_CR0_TFR_Pos) | //TX FIFO 复位
(0 << UART1_CR0_NFE_Pos) | //使能 FIFO
(1 << UART1_CR0_DRE_Pos) | //使能 RX DMA
(1 << UART1_CR0_UE_Pos) | //使能 UART
(1 << UART1_CR0_RE_Pos) | //使能 RX
(1 << UART1_CR0_TE_Pos) ; //使能 TX
Uart_RxDMA_Config((uint32_t)&UART1->RDR,(uint32_t)&RXBuffer1, sizeof(RXBuffer1)); //配置 RX DMA
}
2• Rx DMA 配置
DMA 配置如代码清单 3.2 所示。
DMA Burst 和 FIFO 满阈值配置:数据进入 UART Rx FIFO 后将由 DMA 搬进内存,所以一次填入 Rx FIFO 后,数据会不断增多,当剩余数据量到达设定的 UART RxFIFO 满阈值时,就可以产生一次 DMA 请求,此时将自动从 FIFO 向 buf 搬运一次数据,即图 3.1中的 Rx Data 寄存器(蓝色)到用户层 buf(紫色)。
UART FIFO 满阈值和 DMA burst 需要设置为合适的值(FIFO 满阈值>DMA Burst,保证每次接收数据够进入超时处理),避免数据接收异常(FIFO 满阈值=DMA Burst 时,无法触发接收超时,无法进入超时处理复位 DMA)。目前版本芯片,由于 DMA 启动传输后无法停止(无法修改 DMA 传输总长度),因此每次接收完一帧数据后,需重新复位 DMA。
例子:若 DMA burst 设置为每次搬运 4 个长度的数据,此时 UART FIFO 满阈值需设置为 8,这样每次有 8 个数据的时候,都能保证 DMA 能搬运数据,且保证每次都能置位字符超时标志位,实现每一帧数据接收完成后复位 DMA。
代码清单 3.2 DMA 配置
void Uart_TxDMA_Config(uint32_t src_addr, uint32_t dst_addr, uint16_t size)
{
DMA->CH[0].REG.CER = 0; //配置 DMA 先禁能对应通道
DMA->CH[0].REG.SAR = src_addr; //配置 DMA 源地址
DMA->CH[0].REG.DAR = dst_addr; //配置 DMA 目标地址
DMA->CH[0].REG.DBL = size;
DMA->CH[0].REG.CTR = (0x06 << DMA_CTR_PRI_Pos) | //通道优先级为高优先级
(0x01 << DMA_CTR_TM_Pos) | //DMA 传输模式 连续传输
(0x01 << DMA_CTR_TC_Pos) | //DMA 传输类型 M2P
(0x01 << DMA_CTR_SAI_Pos) | //DMA 源地址不变
(0x00 << DMA_CTR_DBL_Pos) | //目标端 burst
(0x00 << DMA_CTR_SBL_Pos) | //源端 burst
(0x01 << DMA_CTR_DAI_Pos) | //DMA 目的地址递增
(0x00 << DMA_CTR_SDS_Pos) | //源端传输位宽 = byte
(0x00 << DMA_CTR_DDS_Pos) | //目的端传输位宽 = byte
(0x07 << DMA_CTR_DHF_Pos) | //目标端握手设置为 UART1_RX
(0x00 << DMA_CTR_SMS_Pos) |
(0x00 << DMA_CTR_DMS_Pos);
UART1->CR0 |= UART1_CR0_RTOE_Msk; //使能 RX 超时功能
UART1->RTO = 39; //配置 RX 超时时间
DMA->CH[0].REG.STR = 0x07; //清除标志位
DMA->CH[0].REG.CER = 1; //使能 DMA 传输
}
3•接收不定长代码处理
不定长发送实现代码如代码清单 3.3 所示,应用流程图如图 3.2 所示。

图 3.2 不定长接收框图
接收数据后,判断是否字符超时,超时则进入超时处理,清除超时标志位,获取当前DMA 传输长度,通过 CPU 读取 FIFO 内剩余数量,复位并重新配置 DMA。通过此方案即可实现不定长发送功能。
代码清单 3.3 接收不定长函数
Void Uart1_Read() {
uint16_t recvLen,temp,DBL_Len;
DBL_Len = sizeof(RXBuffer1); //DMA 搬运总长度
if(UART1->INT & UART1_INT_RTOI_Msk){ //利用字符超时标志接收不定长数据
UART1->INT_b.RTOI = 1; //清除超时标志位
recvLen =(DBL_Len - DMA->CH[0].REG.DBL); //获取当前 DMA 搬运的数据长度
temp = UART1->STATUS_b.RFL; //获取 DMA 搬运完 FIFO 剩余数据长度
for(uint8_t i = 0; i < temp; i++) //CPU 读取 FIFO 剩余数据到 buf
{
RXBuffer1[recvLen + i] = UART1->RDR;
}
RCU->KEYR = 0x3FAC87E4U;
RCU->AHB0RSTR_b.DMARST = 0; //复位 DMA
__NOP();__NOP();__NOP();__NOP();__NOP();
RCU->AHB0RSTR_b.DMARST = 1;
RCU->KEYR = 1;
Uart_RxDMA_Config((uint32_t)&UART1->RDR,(uint32_t)&RXBuffer1, sizeof(RXBuffer1)); //重新配置 DMA
}
}
04FAQ
问 TX FIFO 的剩余数据小于 DMA 的一次搬运数据量的时候,是怎么处理?
答 该功能的 DMA 传输数据长度(DBL)与填充数据长度大小一致,DMA 每次搬运后,会递减相应的长度。当发送区数据小于 DMA 设置的 Burst,这时 DMA 传输数据长度(DBL)也小于 DMA Burst 长度,此时 DMA 内部会采用 Single 形式搬运,即一次搬一个数据。
问:DMA 发送请求如何生成?
答:当前 TX FIFO 数据小于等于设置的 TX FIFO 空阈值时,会生成 DMA 请求。
问:DMA 接收请求如何生成?
答:当前 RX FIFO 数据大于等于设置的 RX FIFO 满阈值时,会生成 DMA 请求。
问:超时检测在接收数据的意义?
答:由于不定长接收的数据长度未知,若此时 RX FIFO 内数据小于满阈值,未触发DMA 搬运,需要使用超时检测将剩余数据通过 CPU 读出。
问:为什么接收不会出现发送区的剩余数据小于 DMA 的一次搬运数据量,导致 DMA采用 Singal 的方式搬运?
答:发送可知长度,在填充数据后可直接将数据长度写入 DMA DBL,但接收无法知道数据长度,用户层一般把接收 DMA 的 DBL 设置为一个较大的值,且每次接收完成后会复位 DMA,不会出现 RX FIFO 的剩余数据小于 DMA 的一次搬运数据量。
