一、UART(Universal Asynchronous Receiver Transmitter,通用异步收发器)
1、UART发送 1Byte数据
软件或DMA控制器 把1Byte数据写入UART的发送数据寄存器TDR后,UART清零TXE标志和TC标志,并自动将数据从发送数据寄存器TDR转移到发送移位寄存器TSR,转移完成后置位TXE标志并触发UART中断请求(TXE中断使能时)、DMA请求(UART的DMA发送模式使能时),同时UART发送器根据UART协议,将发送移位寄存器TSR的数据发送到TX引脚,发送完成后,若TXE标志为1,则置位TC标志并触发UART中断请求(TC中断使能时)。
2、UART接收 1Byte数据
UART接收器 根据UART协议从RX引脚上收到1Byte数据后,将数据保存到接收数据寄存器RDR,然后置位RXNE标志并触发UART中断请求(RXNE中断使能时)、DMA请求(UART的DMA接收模式使能时),后面若在RX引脚上检测到一个空闲帧,则置位IDLE标志并触发UART中断请求(IDLE中断使能时)。
二、程序开发流程
1、发送多字节数据
① 轮询发送
先把1Byte数据写入发送数据寄存器TDR,然后通过TC位判断数据是否已发完,若没发完则持续等待它发完, 等发完了再写下一个字节到发送数据寄存器TDR, 如此循环,直到所有字节全部发完。
/* 函数功能:用UART1发送多字节数据. 此接口供外部模块调用
返回值:实际发送的字节数
*/
int uart1_write_string(uint8_t *buff, int len)
{
ASSERT(buff != NULL && len > 0);
for(int i = 0; i < len; i++)
{
int block_timeout = 2000; //阻塞超时时间
UART1_RESET_TC_BIT(); //软件对TC bit写0,清零TC bit
uart1->TDR = buff[i]; //写1Byte数据到UART1的寄存器TDR, UART1将自动发送
while(uart1的TC位为0 && block_timeout--); //等待数据发完(数据从移位寄存器TSR发送到引脚TX), 并带超时判断
if(block_timeout == 0)
return i; // 数据在超时时间内没发完, 则发送失败, 返回已发送的字节数
}
return len; //返回已发送的字节数
}
void uart1_init(void)
{
初始化uart1(uart1引脚, uart1波特率、数据位个数、奇偶校验、停止位个数, ...);
}
② TXE中断发送
先把要发送的多字节数据缓存到一个队列中,然后使能UART的TXE中断,当发送数据寄存器TDR为空时会置位TXE标志并触发UART中断请求,在中断函数中,每次取出缓存队列中的一字节数据并发出去(发出去就是把数据写入发送数据寄存器TDR,注意此操作会导致TXE标志清零),当发出最后1个字节后(注意是发出,此时发送控制器正在把发送移位寄存器TSR的数据发送到TX引脚),禁能TXE中断。此方式的缺点是,不能确定最后一个字节何时发完。
queue_t g_tx_queue; //发送队列,用于缓存待发送的数据
/* 函数功能:用UART1发送多字节数据. 此接口供外部模块调用
返回值:实际发送的字节数
*/
int uart1_write_string(uint8_t *buff, int len)
{
ASSERT(buff != NULL && len > 0);
interrupt_disable();
int ok_len = queue_write(&g_tx_queue, buff, len); //把要发送的多字节数据,缓存到发送队列g_tx_queue
interrupt_enable();
UART1_INT_TXE_ENABLE(); //使能UART1的TXE中断
return ok_len; //返回成功写入队列的字节数
}
void uart1_irq_handler(void)
{
uint8_t data;
if(TXE bit置1, 且TXE中断已使能)
{
if(queue_read(&g_tx_queue, &data) == FALSE) //发送队列没有数据, 说明最后一字节已发出
{
UART1_INT_TXE_DISABLE(); //禁能UART1的TXE中断
return;
}
uart1->TDR = data; //将取出的1Byte数据, 写到UART1的寄存器TDR, UART1将自动发送
}
}
void uart1_init(void)
{
初始化uart1(uart1引脚, uart1波特率、数据位个数、奇偶校验、停止位个数, ...);
在NVIC中,设置UART1中断优先级,并使能UART1中断;
}
③ TC中断发送
在初始化时先使能UART的TC中断。先发出第一个字节,然后把剩余字节缓存到一个队列中,UART每发完一字节数据后会置位TC标志并触发UART中断请求,在中断函数中,每次取出缓存队列中的一字节数据并发出去(发出去就是把数据写入发送数据寄存器TDR,注意此操作会导致TC标志清零),当最后1个字节已发完,则向TC bit写0以清零TC标志。此方式的缺点是,每次都要等发送数据寄存器TDR和发送移位寄存器TSR都为空的时候,才能发送下一字节,发送效率低。
queue_t g_tx_queue; //发送队列,用于缓存待发送的数据
/* 函数功能:用UART1发送多字节数据. 此接口供外部模块调用
返回值:实际发送的字节数
*/
int uart1_write_string(uint8_t *buff, int len)
{
int ok_len = 0;
ASSERT(buff != NULL && len > 0);
interrupt_disable();
if(TXE bit为0) //寄存器TDR非空, 说明uart1正在发数据
{
ok_len = queue_write(&g_tx_queue, &buff[0], len); //把要发送的多字节数据,缓存到发送队列g_tx_queue
}
else //寄存器TDR为空, 则发出第1字节, 以便触发TC中断, 剩余字节缓存到队列中, 由TC中断负责发送
{
uart1->TDR = buff[0]; //将第1字节数据写到寄存器TDR, UART1将自动发送
ok_len = queue_write(&g_tx_queue, &buff[1], len - 1); //剩余字节缓存到发送队列g_tx_queue
ok_len++;
}
interrupt_enable();
return ok_len; //返回实际发送的字节数
}
void uart1_irq_handler(void)
{
uint8_t data;
if(TC bit置1, 且TC中断已使能)
{
if(queue_read(&g_tx_queue, &data) == FALSE) //发送队列没有数据, 说明最后一字节已发完
{
UART1_RESET_TC_BIT(); //软件对TC bit写0,清零TC bit
if(uart1->tx_done_callback)
{
uart1->tx_done_callback(); //通知任务数据发完了
}
return;
}
uart1->TDR = data; //将取出的1Byte数据, 写到UART1的寄存器TDR, UART1将自动发送
}
}
void uart1_init(void)
{
初始化uart1(uart1引脚, uart1波特率、数据位个数、奇偶校验、停止位个数, ...);
在NVIC中,设置UART1中断优先级,并使能UART1中断;
使能UART1的TC中断;
}
④ TXE中断发送、TC中断确定结束
先把要发送的多字节数据缓存到一个队列中,然后使能UART的TXE中断,当发送数据寄存器TDR为空时会置位TXE标志并触发UART中断请求,在TXE标志触发的UART中断函数中,每次取出缓存队列中的一字节数据并发出去(发出去就是把数据写入发送数据寄存器TDR,注意此操作会导致TXE标志清零),当发出最后1个字节后(注意是发出,此时发送控制器正在把发送移位寄存器TSR的数据发送到TX引脚),禁能TXE中断,然后使能TC中断,当UART把最后1个字节发完后(注意是发完,此时发送控制器已将发送移位寄存器TSR的数据全部发送到了TX引脚),置位TC标志并触发UART中断请求,在TC标志触发的UART中断函数中,调用回调函数以通知任务数据发完了,并向TC bit写0以清零TC标志,最后禁能TC中断。这种方式的发送效率高,同时能确定最后一个字节何时发完,是比较推荐的方式。
queue_t g_tx_queue; //发送队列,用于缓存待发送的数据
/* 函数功能:用UART1发送多字节数据, 此接口供外部模块调用
返回值:实际发送的字节数
*/
int uart1_write_string(uint8_t *buff, int len)
{
ASSERT(buff != NULL && len > 0);
interrupt_disable();
int ok_len = queue_write(&g_tx_queue, buff, len); //把要发送的多字节数据,缓存到发送队列g_tx_queue
interrupt_enable();
UART1_INT_TXE_ENABLE(); //使能UART1的TXE中断
return ok_len; //返回成功写入队列的字节数
}
void uart1_irq_handler(void)
{
uint8_t data;
if(TXE bit置1, 且TXE中断已使能)
{
if(queue_read(&g_tx_queue, &data) == FALSE) //发送队列没有数据, 说明最后一字节已发出
{
UART1_INT_TXE_DISABLE(); //禁能UART1的TXE中断
UART1_INT_TC_ENABLE(); //使能UART1的TC中断
return;
}
uart1->TDR = data; //将取出的1Byte数据, 写到UART1的寄存器TDR, UART1将自动发送
}
else if(TC bit置1, 且TC中断已使能)
{
UART1_RESET_TC_BIT(); //软件对TC bit写0,清零TC bit
UART1_INT_TC_DISABLE(); //禁能UART1的TC中断
if(uart1->tx_done_callback)
{
uart1->tx_done_callback(); //通知任务数据发完了
}
}
}
void uart1_init(void)
{
初始化uart1(uart1引脚, uart1波特率、数据位个数、奇偶校验、停止位个数, ...);
在NVIC中,设置UART1中断优先级,并使能UART1中断;
}
⑤ DMA发送、TC中断确定结束
把任务A待发送的数据,拷贝到发送环形缓存(超出缓存长度的数据将不拷贝),若UART正在发送,则直接把拷贝的字节数返回给任务A,否则先根据环形缓存状态配置好DMA(配好后DMA会自动发送数据),然后把拷贝的字节数返回任务A。
. 根据环形缓存的当前状态,配置DMA:
DMA为单次模式, 传输方向为memory --> peripheral, 搬运次数为N;
peripheral:起始地址为寄存器TDR地址,地址不增加,单数据长度为1字节;
memory: 起始地址为 ADDR_X,地址增加,单数据长度为1字节;
使能DMA channel的传输完成中断(在传输完成中断函数中,使能UART的TC中断);
使能DMA channel;
最后使能UART的DMA发送模式(当寄存器TDR为空时,UART自动向DMA控制器发送DMA请求,DMA收到请求后会搬运数据)。
PS:因为DMA只能搬运线性地址空间的数据,而发送环形缓存中的数据在内存中的地址可能是非线性的,因此得把环形缓存中的数据分成线性的两部分,这样就得让DMA分两次发,第一次在任务A的上下文环境中发,第二次当DMA发完后在TC触发的UART中断函数中发。
举例:假设把任务A待发送的数据拷贝到发送环形缓存后,缓存中有5字节数据(如下图的图A),此时UART没在发数据,则需配置DMA以便启动第一次发送,此时配置DMA时设置搬运次数为2(addr_05 ~ addr_06之间的2个数据),起始地址为 addr_05。当配置好后,发送寄存器TDR为空时UART会向DMA控制器发送DMA请求,DMA开始把数据从环形缓存搬到UART寄存器TDR中,触发UART开始发送。当DMA搬运完最后一个数据(即addr_06对应的数据)后,触发DMA传输完成中断,在其中断函数中,使能UART的TC中断,后面当UART把最后一个数据(即addr_06对应的数据)发完后,触发TC中断,在其中断函数中,禁能TC中断,并更新发送环形缓存的状态(如下图的图B,此时缓存中还有3字节数据),然后重新配置DMA启动第二次发送,此时设置DMA搬运次数为3(addr_00 ~ addr_02之间的3个数据),起始地址为 addr_00。当DMA第二次搬运完最后一个字节后(即addr_02对应的数据),同样的,最终又会进入TC标志触发的UART中断,在其中断函数中,再次更新发送环形缓存的状态(如下图的图C,此时缓存为空),此时发送环形缓存为空,即发送结束。
queue_t g_tx_queue; //发送队列,用于缓存待发送的数据
/* 函数功能:用UART1发送多字节数据. 此接口供外部模块调用
返回值:实际发送的字节数
*/
int uart1_write_string(uint8_t *buff, int len)
{
int ok_len = 0;
ASSERT(buff != NULL && len > 0);
interrupt_disable();
ok_len = queue_write(&g_tx_queue, &buff[0], len); //把要发送的多字节数据,缓存到发送队列g_tx_queue
interrupt_enable();
if(TXE bit为0) //寄存器TDR非空, 说明uart1正在发数据
return ok_len;
根据环形缓存状态配置DMA; //配好后DMA会自动发送数据
return ok_len; //返回实际发送的字节数
}
void dma_uart1_tx_channel_irq_handler(void)
{
if(TCIFx标志置位) //transfer complete flag
{
清零TCIFx标志;
使能UART1的TC中断;
}
}
void uart1_irq_handler(void)
{
if(TC bit置1, 且TC中断已使能)
{
清零TC标志;
禁能UART1的TC中断;
更新发送环形缓存的状态; //read_idx, 缓存剩余数据个数
if(环形缓存还有数据)
{
根据环形缓存状态配置DMA; //配好后DMA会自动发送数据
}
}
}
void uart1_init(void)
{
初始化UART1(uart1引脚, uart1波特率、数据位个数、奇偶校验、停止位个数, ...);
使能UART1的DMA发送模式;
在NVIC中,设置UART1中断优先级,并使能UART1中断; //TC标志置位时触发UART中断
在NVIC中,设置UART1 TX对应的DMA channel中断的优先级并使能; //DMA传输完成时触发中断
}
2、接收不定长数据
① RXNE中断接收、定时器超时确定帧结束
在UART初始化时使能UART的RXNE中断,创建一个定时器并设置好超时时间,然后创建一个接收队列用于缓存接收的数据。当UART从RX引脚上接收到1Byte数据后,置位RXNE标志并触发中断,在中断函数中,将寄存器RDR的值读出并缓存到接收队列中(读寄存器RDR会导致RXNE标志清零),然后复位定时器(若定时器未启动则启动定时器)。当定时器超时后,调用回调函数通知任务已接收到一帧数据。
queue_t g_rx_queue; //接收队列,用于缓存接收的数据
bool g_rx_ok_flag = FALSE; //接收完成标志
timer_t g_timer; //软件定时器, 用于一帧接收超时判断
/* 函数功能:读UART1接收的一帧数据. 此接口供外部模块调用
返回值:实际接收的字节数
*/
int uart1_read_string(uint8_t *buff, int len)
{
int rcv_len = 0;
ASSERT(buff != NULL && len > 0);
interrupt_disable();
if(g_rx_ok_flag == TRUE) //已接收到一帧数据
{
rcv_len = queue_read(&g_rx_queue, buff, len); //将接收队列的缓存数据, 拷贝到任务指定buff
g_rx_ok_flag = FALSE; //任务已读取接收的数据, 可重新接收新数据
}
interrupt_enable();
return rcv_len; //返回实际接收的字节数
}
void uart1_rx_timer_timeout_callback(void)
{
interrupt_disable();
g_rx_ok_flag = TRUE; //设置接收完成标志
if(uart1->rx_done_callback)
{
int rcv_len = queue_len(&g_rx_queue);
interrupt_enable();
uart1->rx_done_callback(rcv_len); //通知任务已接收到一帧数据
}
interrupt_enable();
}
void uart1_irq_handler(void)
{
if(RXNE bit置1, 且RXNE中断已使能)
{
uint8_t data = uart1->RDR; //从寄存器RDR中读出接收的数据(读RDR导致RXNE bit清零)
if(g_rx_ok_flag == TRUE) //上一帧数据未被任务读取, 则不再接收新数据
return;
queue_write(&g_rx_queue, data); //将接收的数据缓存到接收队列中
timer_reset(&g_timer); //复位定时器
}
else
{
清零各种能触发中断的error标志;
}
}
void uart1_init(void)
{
初始化uart1(uart1引脚, uart1波特率、数据位个数、奇偶校验、停止位个数, ...);
在NVIC中,设置UART1中断优先级,并使能UART1中断; //在NVIC中, UART1中断是 IRQ#37
使能UART1的RXNE中断;
timer_set_timeout(&g_timer, TIMEOUT_5_MS); //设置定时器超时时间
timer_set_call_back(&g_timer, uart1_rx_timer_timeout_callback); //设置定时器回调函数
}
② RXNE中断接收、IDLE中断确定帧结束
在UART初始化时使能UART的RXNE中断、IDLE中断,并创建一个接收队列用于缓存接收的数据。当UART从RX引脚上接收到1Byte数据后,置位RXNE标志并触发中断,在RXNE标志触发的中断函数中,将寄存器RDR的值读出并缓存到接收队列中(读寄存器RDR会导致RXNE标志清零)。当UART在RX引脚检测到一个idle frame时,置位IDLE标志并触发中断,在IDLE标志触发的中断函数中,调用回调函数通知任务已接收到一帧数据,并清零IDLE标志。
queue_t g_rx_queue; //接收队列,用于缓存接收的数据
bool g_rx_ok_flag = FALSE; //接收完成标志
/* 函数功能:读UART1接收的一帧数据. 此接口供外部模块调用
返回值:实际接收的字节数
*/
int uart1_read_string(uint8_t *buff, int len)
{
int rcv_len = 0;
ASSERT(buff != NULL && len > 0);
interrupt_disable();
if(g_rx_ok_flag == TRUE) //已接收到一帧数据
{
rcv_len = queue_read(&g_rx_queue, buff, len); //将接收队列的缓存数据, 拷贝到任务指定buff
g_rx_ok_flag = FALSE; //任务已读取接收的数据, 可重新接收新数据
}
interrupt_enable();
return rcv_len; //返回实际接收的字节数
}
void uart1_irq_handler(void)
{
if(RXNE bit置1, 且RXNE中断已使能)
{
uint8_t data = uart1->RDR; //从寄存器RDR中读出接收的数据(读RDR导致RXNE bit清零)
if(g_rx_ok_flag == TRUE) //上一帧数据未被任务读取, 则不再接收新数据
return;
queue_write(&g_rx_queue, data); //将接收的数据缓存到接收队列中
}
else if(IDLE bit置1, 且IDLE中断已使能)
{
uint8_t data = uart1->RDR; //读寄存器RDR可清零IDLE标志
if(g_rx_ok_flag == TRUE) //上一帧数据未被任务读取, 则不再接收新数据
return;
g_rx_ok_flag = TRUE; //设置接收完成标志
if(uart1->rx_done_callback)
{
int rcv_len = queue_len(&g_rx_queue);
uart1->rx_done_callback(rcv_len); //通知任务已接收到一帧数据
}
}
else
{
清零各种能触发中断的error标志;
}
}
void uart1_init(void)
{
初始化uart1(uart1引脚, uart1波特率、数据位个数、奇偶校验、停止位个数, ...);
在NVIC中,设置UART1中断优先级,并使能UART1中断; //在NVIC中, UART1中断是 IRQ#37
使能UART1的RXNE中断;
使能UART1的IDLE中断;
}
③ DMA接收、IDLE中断确定帧结束
Ⅰ、配置DMA:
循环模式,传输方向为 peripheral–> memory,搬运次数为 接收环形缓存总字节长度;
peripheral:起始地址为 寄存器RDR地址,地址不增加,单数据长度为1字节;
memory:起始地址为 接收环形缓存起始地址,地址增加,单数据长度为1字节;
使能DMA channel 的传输过半中断、传输完成中断;
使能DMA channel;
最后使能UART的DMA接收模式。
Ⅱ、使能UART的IDLE中断。
Ⅲ、在DMA传输过半中断、DMA传输完成中断、IDLE中断的中断函数中,计算DMA写入接收环形缓存的数据字节数并更新环形缓存的状态。在IDLE中断的中断函数中,调用接收回调函数把收到的数据长度告诉任务A。
queue_t g_rx_queue; //接收队列,用于缓存接收的数据
uint16_t remain_cnt; //上一次保存的寄存器DMA_CNDTRx的值(DMA剩余搬运次数), 用于计算DMA写入接收缓存的数据个数
/* 函数功能:读UART1接收的一帧数据. 此接口供外部模块调用
返回值:实际接收的字节数
*/
int uart1_read_string(uint8_t *buff, int len)
{
int rcv_len = 0;
ASSERT(buff != NULL && len > 0);
interrupt_disable();
rcv_len = queue_read(&g_rx_queue, buff, len); //将接收队列的缓存数据, 拷贝到任务指定buff
interrupt_enable();
return rcv_len; //返回实际接收的字节数
}
void dma_uart1_rx_channel_irq_handler(void)
{
if(HTIFx标志置位) //half transfer flag
{
清零TCIFx标志;
}
else if(TCIFx标志置位) //transfer complete flag
{
清零TCIFx标志;
}
else
{
清零各种能触发中断的error标志;
return;
}
/* 计算DMA写入环形缓存的数据个数,并更新环形缓存的write_idx */
uint16_t rcv_len = 0;
uint16_t counter = __HAL_DMA_GET_COUNTER(&(uart1->dma_rx.handle));
if (counter <= remain_cnt)
{
recv_len = remain_cnt - counter;
}
else
{
recv_len = g_rx_queue.config.rx_bufsz + remain_cnt - counter;
}
remain_cnt = counter;
更新g_rx_queue的write_idx(DMA写入的字节长度为recv_len);
}
void uart1_irq_handler(void)
{
if(IDLE bit置1, 且IDLE中断已使能)
{
清零IDLE标志;
/* 计算DMA写入环形缓存的数据个数,并更新环形缓存的write_idx */
uint16_t rcv_len = 0;
uint16_t counter = __HAL_DMA_GET_COUNTER(&(uart1->dma_rx.handle));
if (counter <= remain_cnt)
{
recv_len = remain_cnt - counter;
}
else
{
recv_len = g_rx_queue.config.rx_bufsz + remain_cnt - counter;
}
remain_cnt = counter;
更新g_rx_queue的write_idx(DMA写入的字节长度为recv_len);
/* 通知任务已接收到一帧数据 */
if(uart1->rx_done_callback)
{
uart1->rx_done_callback(queue_len(&g_rx_queue));
}
return;
}
清零各种能触发中断的error标志;
}
void uart1_init(void)
{
初始化UART1(uart1引脚, uart1波特率、数据位个数、奇偶校验、停止位个数, ...);
使能UART1的DMA接收模式;
配置DMA:循环模式, 传输方向为 peripheral--> memory, 搬运次数为 g_rx_queue.config.rx_bufsz;
peripheral:起始地址为寄存器RDR地址,地址不增加,单数据长度为1字节;
memory: 起始地址为 &g_rx_queue.buff[0],地址增加,单数据长度为1字节;
使能DMA channel的传输过半中断、传输完成中断;//用于准确计算DMA写入接收缓存的数据个数
使能DMA channel; //UART1 RX对应的DMA channel
最后使能UART的DMA接收模式。 //UART收到1Byte数据后自动向DMA控制器发送DMA请求
remain_cnt = g_rx_queue.config.rx_bufsz; //remain_cnt初值和寄存器DMA_CNDTRx保持一致
使能UART1的IDLE中断;
在NVIC中,设置UART1中断优先级,并使能UART1中断;
在NVIC中,设置UART1 RX对应的DMA channel中断优先级并使能; //UART中断优先级 < DMA_channel中断优先级
}
三、注意事项
1、UART接收时,使能RXNE中断后,会同时使能ORE中断。如果在接收数据时,软件没有在1Byte时间内读完数据寄存器DR,ORE标志会置位并触发UART中断,如果此时出现ORE =1、RXNE = 0 的情况,而在中断函数中软件因为RXNE为0没有去读数据寄存器DR,导致ORE标志没能清零,则ORE标志会一直触发中断。
PS:出现ORE =1、RXNE = 0的场景:软件正在读寄存器SR和DR期间,此时移位寄存器RSR接收完了1Byte数据,则ORE置1(此时因为RXNE标志为1,寄存器RSR的数据不会转移到寄存器DR,此数据最终命运是被后面接收的数据所覆盖),当软件读完寄存器DR后,RXNE = 0。
参考资料
[1] STM32F103xx datasheet.
[2] STM32F10xxx reference manual.