一、简介
本文主要阐述STM32串口的几种工作中使用的工作模式和编程思路。串口通常情况下使用的是:1个起始位,8个数据位,无奇偶校验,1位停止位
,每传输1个字节的数据相当于传输10bit,在波特率:9600的情况下,1字节需要1.04ms,如果传输波特率:115200传输1字节数据需要0.087ms。
1.串口发送模式
串口发送方式主要分为3种:轮询、中断、DMA发送。下面简述一下各个模式优缺点:
模式 | 优点 | 缺点 |
---|---|---|
轮询 | 代码最简单 | 会导致代码阻塞,会被任何中断,在带os的系统中还容易被高优先级任务打断 |
中断 | 将发送优先级提高到该串口中断级别,不会被任务打断,只会被更高优先级中断打断 | 占用cpu和中断 |
DMA | 不需要cpu参与数据发送,发送也不会被打断 | 代码复杂 |
(1)轮询发送
轮询发送是将需要发送的数据写到串口的数据(DR)寄存器中,然后通过循环等待发送完成TC标致位置位判断数据是否发送结束。一般串口在115200波特率下发送1字节数据时间大概0.087ms,在没有os的情况下,程序需要阻塞89ms,只会被中断打断。在有os的情况下还需要考虑到任务切换和高优先级任务的使用,串口发送的一包数据就会是断断续续的,时间更长。
(2)中断发送
中断发送是对轮询发送的一种改进,其目的是连续发送一包数据,不被低优先级中断和任务打断。其思路是开启串口发送中断,在应用层调用驱动层发送函数时,将中断发送标志位置1/或直接发送第一字节,触发串口中断,在中断中发送剩余数据。期间每次传输完成时进一次中断,其余时间cpu可以运行别的任务,而且减少了查询发送完成标志位的时间。对于没有os的系统来说做到了时间片处理非阻塞发送。
(3)DMA发送
该模式需要根据手册提前配置DMA通道模式等信息,通过HAL库提供的发送函数,DMA会自动将内存中的参数搬运到串口外设中,CPU执行完发送命令就可以执行别的任务了。在dma发送结束时会产生DMA中断告诉用户数据是否发送结束。
2.串口接收模式
串口接收也有轮询模式,主要是中断和DMA的方式。下面简述一下各个模式优缺点:
模式 | 优点 | 缺点 |
---|---|---|
中断 | 代码简单 | 需要多次进入中断 |
DMA | 不需要cpu参与每字节接收,仅产生少量的中断 | 需要配合空闲中断处理 |
(1)中断接收
中断接收是一种常见的接收方式,适合波特率较低(9600)或任务不复杂的情况下使用。9600传输数据时,使用串口中断接收数据大约1.04ms进一次中断,在要求不高的情况下使用中断接收也能满足设计要求,尤其对裸机基于时间片的代码框架来说影响不大,代码也简洁。但是随着波特率的提高此方法则不太适用,当波特率很高(115200以上),若使用串口中断接收意味着每87us会进入一次中断,那么对主程序的时间片影响将会很大,故推荐使用DMA。
(2)DMA接收+空闲中断接收
需要提前配置DMA通道,DMA模式,使用DMA搬运数据,串口空闲中断判断驱动层的帧结束。当空闲中断产生时将DMA映射的数据拷贝到应用层的接收缓存区。与中断接收区别是不用每个字节进入一次中断,拷贝数据也是将一帧数据拷贝到缓存区。发送1K数据则会在89ms内进1000次中断,若再提高波特率,对裸机时间片的系统来说影响就很大了。
二、串口配置
UART(Universal Asynchronous Receiver/Transmitter)通用异步收发传输器,UART 作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输,是在应用程序开发过程中使用频率最高的数据总线。
RT-Thread串口配置步骤:
(1)通过串口名字找到串口句柄
#define SAMPLE_UART_NAME "uart2"
static rt_device_t serial;
serial = rt_device_find(SAMPLE_UART_NAME);
(2)配置串口参数
使用rt_device_control()函数修改串口配置参数
/* 初始化配置参数 */
struct serial_configure uart2_config = RT_SERIAL_CONFIG_DEFAULT;
/* step2:修改串口配置参数 */
uart2_config.baud_rate = BAUD_RATE_115200; //修改波特率为 9600
uart2_config.data_bits = DATA_BITS_8; //数据位 8
uart2_config.stop_bits = STOP_BITS_1; //停止位 1
uart2_config.bufsz = 128; //修改缓冲区 buff size 为 128
uart2_config.parity = PARITY_NONE; //无奇偶校验位
rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &uart2_config);
(3)设置串口接收回调函数
当发生中断(接收中断或者空闲中断)时,会调用注册的回调函数。用户可以在回调函数中写一些无阻塞的功能。虽然回调函数是在中断中执行,所以不能有阻塞和延时。
/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
/* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
rt_sem_release(&rx_sem);
cnt++;
return RT_EOK;
}
/* 设置接收回调函数 */
rt_device_set_rx_indicate(serial, uart_input);
(4)打开串口设备
打开设备时可以对串口的模式进行配置,rt-thread提供三种模式:中断模式、轮询模式、DMA 模式。发送和接收只能从中选一个,发送和接收可以配置的不一致。默认使用中断接收和轮询发送。
/* 以中断接收及轮询发送模式打开串口设备 */
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflag)
oflag支持模式参数为下列几个:
#define RT_DEVICE_FLAG_INT_RX 0x100 /**< INT mode on Rx 中断接收 */
#define RT_DEVICE_FLAG_DMA_RX 0x200 /**< DMA mode on Rx DMA接收 */
#define RT_DEVICE_FLAG_INT_TX 0x400 /**< INT mode on Tx 中断发送 */
#define RT_DEVICE_FLAG_DMA_TX 0x800 /**< DMA mode on Tx DMA接收 */
rt_device_open函数中初始化DMA模式会进行参数检查,需要确认使用串口DMA宏被定义。如:BSP_UART2_RX_USING_DMA
,否则会rt_serial_open()
返回错误。当对应串口DMA宏在rtconfig.h
中被定义后,通过宏定义设置到对应串口对象的uart_dma_flag参数中,在串口初始化阶段通过rt_hw_usart_init()中的rt_hw_serial_register()函数会把对应uart_dma_flag设置到串口dev->flag中,后续串口open时只是检查一下参数。
完整的串口配置例程代码如下:
/*
* 程序清单:这是一个 串口 设备使用例程
* 例程导出了 uart_sample 命令到控制终端
* 命令调用格式:uart_sample uart2
* 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
* 程序功能:通过串口输出字符串"hello RT-Thread!",然后错位输出输入的字符
*/
#include <rtthread.h>
#include "module_uart.h"
#include "drv_usart.h"
#define SAMPLE_UART_NAME "uart2"
/* 初始化配置参数 */
struct serial_configure uart2_config = RT_SERIAL_CONFIG_DEFAULT;
/* 用于接收消息的信号量 */
static struct rt_semaphore rx_sem;
static rt_device_t serial;
uint16_t cnt = 0;
/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
/* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
rt_sem_release(&rx_sem);
cnt++;
return RT_EOK;
}
static void serial_thread_entry(void *parameter)
{
char ch;
while (1)
{
/* 从串口读取一个字节的数据,没有读取到则等待接收信号量 */
while (rt_device_read(serial, -1, &ch, 1) != 1)
{
/* 阻塞等待接收信号量,等到信号量后再次读取数据 */
rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
}
/* 读取到的数据通过串口输出 */
rt_device_write(serial, 0, &ch, 1);
}
}
int uart2_init(void)
{
rt_err_t ret;
/* 查找系统中的串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);
if (!serial)
{
rt_kprintf("find %s failed!\n", SAMPLE_UART_NAME);
return RT_ERROR;
}
/* step2:修改串口配置参数 */
uart2_config.baud_rate = BAUD_RATE_115200; //修改波特率为 9600
uart2_config.data_bits = DATA_BITS_8; //数据位 8
uart2_config.stop_bits = STOP_BITS_1; //停止位 1
uart2_config.bufsz = 128; //修改缓冲区 buff size 为 128
uart2_config.parity = PARITY_NONE; //无奇偶校验位
rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &uart2_config);
/* 初始化信号量 */
rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);
/* 以中断接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
/* 设置接收回调函数 */
rt_device_set_rx_indicate(serial, uart_input);
/* 创建 serial 线程 */
rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
/* 创建成功则启动线程 */
if (thread != RT_NULL)
{
rt_thread_startup(thread);
}
else
{
ret = RT_ERROR;
}
return ret;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(uart_sample, uart device sample);
三、串口发送
串口的三种发送:轮询、中断、DMA发送。下面是发送程序对应用层的接口:
rt_size_t rt_device_write(rt_device_t dev,
rt_off_t pos,
const void *buffer,
rt_size_t size)
里面本质上会调用rt_serial_write()函数往串口设备写数据,里面又细分了三个函数:_serial_int_tx()(中断发送) _serial_dma_tx()(dma发送)_serial_poll_tx()轮询发送。
1.轮询发送
轮询发送是最简单的发送模式,rt-thread提供的是阻塞的发送函数。程序会将需要发送的数据一字节一字节的写到串口的DR寄存器,然后查询发送完成标志,直到数据完全发送结束。优点:程序简单易懂,缺点:浪费cpu资源,容易被高优先级任务打断,一帧数据发送断断续续的。
_serial_poll_tx()的具体实现:每次发送1字节数据循环length长度
rt_inline int _serial_poll_tx(struct rt_serial_device *serial, const rt_uint8_t *data, int length)
{
int size;
RT_ASSERT(serial != RT_NULL);
size = length;
while (length)
{
serial->ops->putc(serial, *data);
++ data;
-- length;
}
return size - length;
}
putc函数的实现:根据单片机型号将数据写入DR或者TDR寄存器中,然后等待发送完成标志。按照这种方式,代码需要在这阻塞到一帧数据完全发送结束才会执行后面的代码。再此期间代码会被高优先级任务和中断打断。
static int stm32_putc(struct rt_serial_device *serial, char c)
{
struct stm32_uart *uart;
RT_ASSERT(serial != RT_NULL);
uart = rt_container_of(serial, struct stm32_uart, serial);
UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_TC);
#if defined(SOC_SERIES_STM32L4) || defined(SOC_SERIES_STM32WL) || defined(SOC_SERIES_STM32F7) || defined(SOC_SERIES_STM32F0) \
|| defined(SOC_SERIES_STM32L0) || defined(SOC_SERIES_STM32G0) || defined(SOC_SERIES_STM32H7) || defined(SOC_SERIES_STM32L5)\
|| defined(SOC_SERIES_STM32G4) || defined(SOC_SERIES_STM32MP1) || defined(SOC_SERIES_STM32WB) ||defined(SOC_SERIES_STM32F3)\
|| defined(SOC_SERIES_STM32U5)
uart->handle.Instance->TDR = c;
#else
if(uart->config->is485){
HAL_GPIO_WritePin(uart->config->de_port,uart->config->de_pin, GPIO_PIN_RESET);
}
uart->handle.Instance->DR = c;
#endif
while (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) == RESET);
if(uart->config->is485){
HAL_GPIO_WritePin(uart->config->de_port,uart->config->de_pin, GPIO_PIN_SET);
}
return 1;
}
2.中断发送
与轮询发送区别是多了一个等待完成信号量的内容。bug是里面字符发送函数与轮询发送函数一样,永远返回1,所以等待完成函数不会被触发。
rt_inline int _serial_int_tx(struct rt_serial_device *serial, const rt_uint8_t *data, int length)
{
int size;
struct rt_serial_tx_fifo *tx;
RT_ASSERT(serial != RT_NULL);
size = length;
tx = (struct rt_serial_tx_fifo*) serial->serial_tx;
RT_ASSERT(tx != RT_NULL);
while (length)
{
if (serial->ops->putc(serial, *(char*)data) == -1)
{
rt_completion_wait(&(tx->completion), RT_WAITING_FOREVER);
continue;
}
data ++; length --;
}
return size - length;
}
3.DMA发送
rt_data_queue_push()
:将数据放入dma数据队列中,这里需要特别注意一下直接是应用层写的数据,没有发送缓存区,若是局部变量,在dma发送是可能已经释放掉,无法发送正确数据。其次在发送过程中修改应用层数据,dma会直接发送修改后的数据,这点也需要特别注意。
serial->ops->dma_transmit(serial, (rt_uint8_t *)data, length, RT_SERIAL_DMA_TX)
:在函数指针背后调用的是对hal库分装一层的dma传输函数。
HAL_UART_Transmit_DMA()
:是个无阻塞函数,也就是执行完这一行时,数据开始发送,但是不会等到数据完全发送结束。
rt_inline int _serial_dma_tx(struct rt_serial_device *serial, const rt_uint8_t *data, int length)
{
rt_base_t level;
rt_err_t result;
struct rt_serial_tx_dma *tx_dma;
tx_dma = (struct rt_serial_tx_dma*)(serial->serial_tx);
result = rt_data_queue_push(&(tx_dma->data_queue), data, length, RT_WAITING_FOREVER);
if (result == RT_EOK)
{
level = rt_hw_interrupt_disable();
if (tx_dma->activated != RT_TRUE)
{
tx_dma->activated = RT_TRUE;
rt_hw_interrupt_enable(level);
/* make a DMA transfer */
serial->ops->dma_transmit(serial, (rt_uint8_t *)data, length, RT_SERIAL_DMA_TX);
}
else
{
rt_hw_interrupt_enable(level);
}
return length;
}
else
{
rt_set_errno(result);
return 0;
}
}
static rt_size_t stm32_dma_transmit(struct rt_serial_device *serial, rt_uint8_t *buf, rt_size_t size, int direction)
{
if (RT_SERIAL_DMA_TX == direction)
{
if (HAL_UART_Transmit_DMA(&uart->handle, buf, size) == HAL_OK)
{
return size;
}
else
{
return 0;
}
}
return 0;
}
void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
{
/* Transfer Complete Interrupt management ***********************************/
else if (((flag_it & (DMA_FLAG_TC1 << hdma->ChannelIndex)) != RESET) && ((source_it & DMA_IT_TC) != RESET))
{
if((hdma->Instance->CCR & DMA_CCR_CIRC) == 0U)
{
/* Disable the transfer complete and error interrupt */
__HAL_DMA_DISABLE_IT(hdma, DMA_IT_TE | DMA_IT_TC);
/* Change the DMA state */
hdma->State = HAL_DMA_STATE_READY;
}
/* Clear the transfer complete flag */
__HAL_DMA_CLEAR_FLAG(hdma, __HAL_DMA_GET_TC_FLAG_INDEX(hdma));
/* Process Unlocked */
__HAL_UNLOCK(hdma);
if(hdma->XferCpltCallback != NULL)
{
/* Transfer complete callback */
hdma->XferCpltCallback(hdma);
}
}
}
四、串口接收
串口接收常见的分中断接收和DMA接收。在RTT串口配置中设置了一个ringbuf,其作用是用来接收数据的。无论使用中断接收还是DMA接收数据都会存入ringbuf中。
1.中断接收
在RTT中,中断接收的流程如下:
中断接收无论如何都会触发单片机中断向量表中的中断函数,只需要去查找中断函数中具体实现即可。如果是接收中断则进入rt_hw_serial_isr函数否则则去清除一些错误标志位。
void USART2_IRQHandler(void)
{
/* enter interrupt */
rt_interrupt_enter();
uart_isr(&(uart_obj[UART2_INDEX].serial));
/* leave interrupt */
rt_interrupt_leave();
}
static void uart_isr(struct rt_serial_device *serial)
{
...
/* UART in mode Receiver -------------------------------------------------*/
if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET) &&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_RXNE) != RESET))
{
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_IND);
}
...
else
{
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_ORE) != RESET)
{
__HAL_UART_CLEAR_OREFLAG(&uart->handle);
}
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_NE) != RESET)
{
__HAL_UART_CLEAR_NEFLAG(&uart->handle);
}
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_FE) != RESET)
{
__HAL_UART_CLEAR_FEFLAG(&uart->handle);
}
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_PE) != RESET)
{
__HAL_UART_CLEAR_PEFLAG(&uart->handle);
}
}
}
rt_hw_serial_isr()可以分成两部分读取数据和回调函数调用部分。读取数据部分是负责把串口接收数据读取到ringbuf中。
while (1)
{
ch = serial->ops->getc(serial);
if (ch == -1) break;
/* disable interrupt */
level = rt_hw_interrupt_disable();
rx_fifo->buffer[rx_fifo->put_index] = ch;
rx_fifo->put_index += 1;
if (rx_fifo->put_index >= serial->config.bufsz) rx_fifo->put_index = 0;
/* if the next position is read index, discard this 'read char' */
if (rx_fifo->put_index == rx_fifo->get_index)
{
rx_fifo->get_index += 1;
rx_fifo->is_full = RT_TRUE;
if (rx_fifo->get_index >= serial->config.bufsz) rx_fifo->get_index = 0;
_serial_check_buffer_size();
}
/* enable interrupt */
rt_hw_interrupt_enable(level);
}
上述使用serial->ops->getc(serial)函数,看这个名字就能猜到是获取串口接收的一个字节数据。对于STM32来说就是将串口的DR寄存器通过这个函数返回。将DR寄存器中的值存入ringbuf,还对ringbuf满的情况做了一下异常处理,ringbuf is_full标志位会置一,数据会覆盖最早的数据导致数据丢失,所以需要保证应用层及时从ringbuf中取出数据。
static int stm32_getc(struct rt_serial_device *serial)
{
int ch;
struct stm32_uart *uart;
RT_ASSERT(serial != RT_NULL);
uart = rt_container_of(serial, struct stm32_uart, serial);
ch = -1;
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET)
{
...
ch = uart->handle.Instance->DR & stm32_uart_get_mask(uart->handle.Init.WordLength, uart->handle.Init.Parity);
...
}
return ch;
}
再看看回调函数部分,若回调函数不为空,且这次就收到数据则调用一次接收回调函数。通常回调函数中释放一个信号量通知应用层程序从ringbuf中读取数据。其实这也看出了中断方式的一个缺陷,每接收一字节放一次ringbuf,调用一次回调函数,应用层也只能一字节一字节接收。为了减CPU的使用率还提供了DMA的方式。
/* 调用回调函数部分 */
/* invoke callback */
if (serial->parent.rx_indicate != RT_NULL)
{
rt_size_t rx_length;
/* get rx length */
level = rt_hw_interrupt_disable();
rx_length = (rx_fifo->put_index >= rx_fifo->get_index)? (rx_fifo->put_index - rx_fifo->get_index):
(serial->config.bufsz - (rx_fifo->get_index - rx_fifo->put_index));
rt_hw_interrupt_enable(level);
if (rx_length)
{
serial->parent.rx_indicate(&serial->parent, rx_length);
}
}
break;
2.DMA接收
这里触发串口中断的判断变了,变成了DMA半满、满中断和空闲中断触发串口中断,三者是或的关系。接收中断中如果开启DMA则会执行以下代码:
else if ((uart->uart_dma_flag) && (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_IDLE) != RESET)
&& (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_IDLE) != RESET))
{
level = rt_hw_interrupt_disable();
recv_total_index = serial->config.bufsz - __HAL_DMA_GET_COUNTER(&(uart->dma_rx.handle));
recv_len = recv_total_index - uart->dma_rx.last_index;
uart->dma_rx.last_index = recv_total_index;
rt_hw_interrupt_enable(level);
if (recv_len)
{
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_DMADONE | (recv_len << 8));
}
__HAL_UART_CLEAR_IDLEFLAG(&uart->handle);
}
else if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) &&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TC) != RESET))
{
if ((serial->parent.open_flag & RT_DEVICE_FLAG_DMA_TX) != 0)
{
HAL_UART_IRQHandler(&(uart->handle));
}
UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_TC);
}
DMA在处理的过程中也是调用rt_hw_serial_isr与中断一致,接收长度需要根据DMA接收长度增加个小计算得出。后续处理与中断接收也基本一致,将数据更新进ringbuf再调用回调函数。最大的区别是DMA一次会接收一帧或者半帧数据才进一次中断。回调函数处理也会有所不一样,官方例程也不在释放信号量了,通过消息队列传出串口接收数据大小,交给应用层处理。