0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

串口设备框架serial_v2源码分析-阻塞模式

冬至子 来源:ZOCO_9333 作者:ZOCO_9333 2023-09-14 11:34 次阅读

硬件工作模式选择分析
在serial_v2中,串口设备以应用层视角,即阻塞模式或非阻塞模式来作为该串口设备的开启标志.

// 使用阻塞接收阻塞发送模式开启uart_dev设备
rt_device_open(uart_dev,RT_DEVICE_FLAG_TX_BLOCKING|RT_DEVICE_FLAG_TX_BLOCKING);
以下为所有的设备开启模式选择:

#define RT_DEVICE_FLAG_RX_BLOCKING
#define RT_DEVICE_FLAG_RX_NON_BLOCKING
#define RT_DEVICE_FLAG_TX_BLOCKING
#define RT_DEVICE_FLAG_TX_NON_BLOCKING
对于其在底层传输中到底是采用轮询,中断,还是DMA,则取决于在rtconfig中对发送与接收缓冲区以及DMA相关的宏进行配置.

//开启DMA
#define RT_SERIAL_USING_DMA
//将串口1的发送缓冲区与接收缓冲区的大小设置为128
#define BSP_UART1_RX_BUFSIZE 128
#define BSP_UART1_TX_BUFSIZE 128
serial_v2串口设备框架对底层硬件工作模式的选择原则大致可总结为:
(该原则对于TX与RX通用 )

缓冲区大小被设置为0,则使用轮询模式.
若开启DMA,缓冲区大小不为0,则优先使用DMA模式。
若未开启DMA,且缓存又并非为0,则使用中断模式。
接下来,笔者将详细分析,串口设备框架与驱动是如何选择硬件工作模式的.
RT-Thread中,如果我们想使用串口,那我们的第一步将会是:

//假设以阻塞接收和阻塞发送模式打开串口
rt_device_open(uart_dev,RT_DEVICE_RX_BLOCKING|RT_DEVICE_TX_BLOCKING)
阻塞模式作为一种应用层的视角,其底层可以由轮询,中断,或DMA中任意一种硬件工作模式实现.
在serial_v2中,rt_device_open()函数将会调用框架层的rt_serial_init()与rt_serial_open()函数,从而根据已有的配置,从三种硬件工作模式中选择其中一种。

//串口初始化函数
//先将发送与接收缓冲区初始化为NULL, 然后对波特率等参数进行设置
static rt_err_t rt_serial_init(struct rt_device * dev)
{
...
//先发送缓冲区与接收缓缓冲区赋值为空,方便日后初始化
serial->serial_rx = RT_NULL;
serial->serial_tx = RT_NULL;
...
if (serial->ops->configure)
//此次跳转到驱动层的函数,对波特率等参数进行配置
result = serial->ops->configure(serial, &serial->config);
}
//串口开启函数, 主要调用rt_serial_rx_enable与rt_serial_tx_enable函数会对硬件工作模式做出选择
static rt_err_t rt_serial_open(struct rt_device *dev, rt_uint16_t oflag)
{
//根据设备开启模式,对RX进行初始化
if (serial->serial_rx == RT_NULL)
rt_serial_rx_enable(dev, dev->open_flag &(RT_SERIAL_RX_BLOCKINGRT_SERIAL_RX_NON_BLOCKING));
//根据设备开启模式,对TX进行初始化
if (serial->serial_tx == RT_NULL)
rt_serial_tx_enable(dev, dev->open_flag & (RT_SERIAL_TX_BLOCKING|RT_SERIAL_TX_NON_BLOCKING));
}
接下来我们先具体分析一下在rt_serial_open()中调用的rt_serial_rx_enable()如何选择RX硬件工作模式

static rt_err_t rt_serial_rx_enable(struct rt_device * dev, rt_uint16_t rx_oflag) {
// 第一种选择:如果接收缓冲区的大小为0,则首先使用轮询接收
if (serial->config.rx_bufsz == 0) {
...
//将设备的接收函数指针指向轮询接收
dev->read = _serial_poll_rx;
dev->open_flag |= RT_SERIAL_RX_BLOCKING;
...
}
//为接收缓冲区分配内存,其缓冲区大小为在rtconfig中进行设置
rx_fifo = (struct rt_serial_rx_fifo * ) rt_malloc
(sizeof(struct rt_serial_rx_fifo) + serial->config.rx_bufsz);
//初始化完成量,用于阻塞等待
rt_completion_init(&(rx_fifo->rx_cpt));
//调用 control(),根据已有的配置,决定使用DMA或中断
serial->ops->control(serial,RT_DEVICE_CTRL_CONFIG, (void *) RT_SERIAL_RX_BLOCKING )
}
serial->ops->control会调用驱动层的stm32_control(),对DMA或中断进行选择,笔者仅将关键部分列出 :

static rt_err_t stm32_control(struct rt_serial_device *serial,
int cmd, void arg)
{
//RX模式下的工作模式选择
if(ctrl_arg&( RT_DEVICE_FLAG_RX_BLOCKING | RT_DEVICE_FLAG_RX_NON_BLOCKING))
{
...
//先判断DMA相关的宏是否被定义,如果是则将ctrl_arg赋值为DMA相关标志,为下文初始化做准备
if (uart->uart_dma_flag & RT_DEVICE_FLAG_DMA_RX)
ctrl_arg = RT_DEVICE_FLAG_DMA_RX;
else
ctrl_arg = RT_DEVICE_FLAG_INT_RX;
...
}
...
//如果ctrl_arg被赋予了DMA标志相关的值,则选择DMA模式,对DMA进行初始化
if (ctrl_arg & (RT_DEVICE_FLAG_DMA_RX | RT_DEVICE_FLAG_DMA_TX)) {
#ifdef RT_SERIAL_USING_DMA
//具体分析见下文
stm32_dma_config(serial, ctrl_arg);
}
else
{
//如果未开启DMA.则选择中断模式,则对中断相关的函数进行初始化
//具体分析见下文
stm32_control(serial, RT_DEVICE_CTRL_SET_INT, (void
)ctrl_arg);
break;
}
}
stm32_control()对中断接收进行初始化:

static rt_err_t stm32_control(struct rt_serial_device *serial, int cmd, void *arg)
{
...
case RT_DEVICE_CTRL_SET_INT:
//设置单个中断抢占优先级和响应优先级
HAL_NVIC_SetPriority(uart->config->irq_type, 1, 0);
//设置使能中断通道
HAL_NVIC_EnableIRQ(uart->config->irq_type);
//使能RXNE相关中断
if (ctrl_arg == RT_DEVICE_FLAG_INT_RX)
__HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_RXNE);
break;
}
stm32_dma_config()对DMA接收进行初始化

static void stm32_dma_config(struct rt_serial_device *serial, rt_ubase_t flag)
{
...
if (RT_DEVICE_FLAG_DMA_RX == flag)
{
//设置DMA接收数据所用的RX缓冲区
DMA_Handle = &uart->dma_rx.handle;
dma_config = uart->config->dma_rx;
...
if (RT_DEVICE_FLAG_DMA_RX == flag)
{
__HAL_LINKDMA(&(uart->handle), hdmarx, uart->dma_rx.handle);
}
}
...
//使能DMA相关中断
if (flag == RT_DEVICE_FLAG_DMA_RX)
{
rx_fifo = (struct rt_serial_rx_fifo )serial->serial_rx;
RT_ASSERT(rx_fifo != RT_NULL);
/
Start DMA transfer /
if (HAL_UART_Receive_DMA(&(uart->handle), rx_fifo->buffer, serial->config.rx_bufsz) != HAL_OK)
{
/
Transfer error in reception process */
RT_ASSERT(0);
}
CLEAR_BIT(uart->handle.Instance->CR3, USART_CR3_EIE);
__HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_IDLE);
}
...
//此处省略其他的初始化函数
}
以上就是RX的硬件工作模式选择,与对应硬件初始化的部分代码
接下来分析一下rt_serial_tx_enable()如何选择硬件工作模式与初始化硬件:

static rt_err_t rt_serial_tx_enable(struct rt_device*dev, rt_uint16_t rx_oflag){
// 第一种情况:如果发送缓冲区的大小为0,则使用轮询发送
if (serial->config.tx_bufsz == 0)
{
...
dev->read = _serial_poll_tx;
dev->open_flag |= RT_SERIAL_TX_BLOCKING;
...
}
//如果是阻塞模式,接下来就将调用底层control函数,选择DMA或中断
if (tx_oflag == RT_SERIAL_TX_BLOCKING) {
optmode = serial->ops->control(serial,RT_DEVICE_CHECK_OPTMODE,
(void *)RT_DEVICE_FLAG_TX_BLOCKING);


*补充 : serial->ops->control()函数的源码
*即如何在DMA与中断中做出选择
*RT_DEVICE_CHECK_OPTMODE:

  • {
  • //如果开启DMA,那么优先使用DMA
    
  • if (ctrl_arg & RT_DEVICE_FLAG_DMA_TX)
    
  • return RT_SERIAL_TX_BLOCKING_NO_BUFFER;
    
  • else
    
  • //未开启则使用中断
    
  • return RT_SERIAL_TX_BLOCKING_BUFFER;
    
  • }

//optmode的值,即上述调用函数的返回值有两种情况:
//RT_SERIAL_TX_BLOCKING_BUFFER,使用框架层缓冲区,即中断发送,
//RT_SERIAL_TX_BLOCKING_NO_BUFFER,不使用框架层缓冲区,即使用DMA发送
//中断与DMA发送在软件层面的一大区别是中断发送需要使用框架层的发送缓冲区而DMA发送直接使用应用层缓冲区,不需要框架层分配缓冲区
if (optmode == RT_SERIAL_TX_BLOCKING_BUFFER){
//使用中断发送
//则首先为tx缓冲区分配有tx_bufsz大小的空间
tx_fifo = (struct rt_serial_tx_fifo ) rt_malloc
(sizeof(struct rt_serial_tx_fifo)+serial->config.tx_bufsz);
//将设备的写函数指针指向中断发送函数
dev->write = _serial_fifo_tx_blocking_buf;
...
}
else
{
//使用DMA模式发送,此处缓冲区没有分配tx_bufsz大小的空间
//DMA使用的是应用层的buffer
tx_fifo = (struct rt_serial_tx_fifo
) rt_malloc
(sizeof(struct rt_serial_tx_fifo));
//初始化DMA
serial->ops->control(serial,RT_DEVICE_CTRL_CONFIG,
(void *)RT_SERIAL_TX_BLOCKING);
//因为使用不使用框架层接收缓冲区,因此将其设置为NULL
rt_memset(&tx_fifo->rb, RT_NULL, sizeof(tx_fifo->rb));
...
}
//将激活标识设置为false,激活标识用于标记发送缓存是否被使用
tx_fifo->activated = RT_FALSE;
//阻塞发送需要初始化完成量
rt_completion_init(&(tx_fifo->tx_cpt));
...
}
}
stm32_control对中断发送进行初始化:

static rt_err_t stm32_control(struct rt_serial_device *serial, int cmd, void *arg)
{
...
case RT_DEVICE_CTRL_SET_INT:
//设置单个中断抢占优先级和响应优先级
HAL_NVIC_SetPriority(uart->config->irq_type, 1, 0);
//设置使能中断通道
HAL_NVIC_EnableIRQ(uart->config->irq_type);
//使能TXE相关中断
if (ctrl_arg == RT_DEVICE_FLAG_INT_TX)
__HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_TXE);
break;
}
stm32_dma_config对DMA发送进行初始化:

static void stm32_dma_config(struct rt_serial_device *serial, rt_ubase_t flag)
{
...
else //RT_DEVICE_FLAG_DMA_TX == flag
{
//对相关句柄进行赋值
DMA_Handle = &uart->dma_tx.handle;
dma_config = uart->config->dma_tx;
}
...
if (RT_DEVICE_FLAG_DMA_TX == flag)
{
//设置好DMA的发送缓冲区
__HAL_LINKDMA(&(uart->handle), hdmatx, uart->dma_tx.handle);
}
...
//此处省略其他的初始化函数
}
目前通过分析,我们理清楚了串口设备框架是如何与驱动合作,对底层的硬件工作方式,根据已有的配置做出选择,并对选择的硬件工作方式进行相关的初始化工作,接下来,我们将分析serial_v2在阻塞模式下的具体工作流程.

阻塞模式
阻塞模式下的数据接收
数据接收函数分析
static rt_ssize_t rt_serial_read(struct rt_device *dev,
rt_off_t pos,
void *buffer,
rt_size_t size)
{
...
//如果设置了接收缓冲区,则调用_serial_fifo_rx(),即使用中断或DMA模式接收
if (serial->config.rx_bufsz)
{
return _serial_fifo_rx(dev, pos, buffer, size);
}
//否则缓冲区大小为0,使用轮询模式,此处结论与上文一致
return _serial_poll_rx(dev, pos, buffer, size);
}
轮询
轮询,就是CPU通过不断地查询某个外部设备的状态,如果外部设备准备好,就可以向其发送数据或者读取数据。然而,这种方式由于CPU不断查询总线,导致指令执行受到影响,效率较低。 以下是serial_v2中轮询_serial_poll_rx()函数的部分源码.

//驱动框架层函数
rt_ssize_t _serial_poll_rx(struct rt_device *dev,
rt_off_t pos,
void *buffer,
rt_size_t size) {
...
while(size)
{
//调用驱动层函数去读一个数据
getc_element = serial->ops->getc(serial);
if (getc_element == -1) break;
*getc_buffer = getc_element;
++ getc_buffer;
-- size;
}
return getc_size - size;
}
其中serial->ops->getc(serial)在底层驱动中调用stm32_getc()
以下为stm32_getc()部分源码

//驱动层函数
static int stm32_getc(struct rt_serial_device *serial){
...
ch = -1;
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET)
ch = UART_GET_RDR(&uart->handle,stm32_uart_get_mask(...));
return ch;
}
通过以上代码,我们可以发现,轮询的底层实现就是就是不断检查RXNE(接收数据寄存器非空标志位),然后从RDR(接收数据寄存器)中读取数据,如果某一次RXNE寄存器被复位,那么_serial_poll_rx()函数会直接返回已经读到的数据的数量,结束轮询接收。

中断
中断方式克服了CPU轮询外部设备的缺点,正常情况下,CPU执行指令,不会主动去检查外部设备的状态。外部设备准备好之后,向CPU发送中断信号,CPU收到中断信号后停止当前的工作,会根据中断信号指定的设备号处理相应的设备。这种处理方式既不影响CPU的工作,也能保证外部设备的数据得到及时处理,工作效率很高.

由上文rt_serial_read()函数分析可知,中断接收是由_serial_fifo_rx(dev, pos, buffer, size)实现的。

//驱动框架层中断接收部分代码
static rt_ssize_t _serial_fifo_rx(struct rt_device *dev,
rt_off_t pos,
void *buffer,
rt_size_t size)
{
if (dev->open_flag & RT_SERIAL_RX_BLOCKING)
{
...
//此处可以注意到,阻塞接收函数第一步是检查应用层想读的数据量是否大于缓存,
//如果大于,函数将不工作,直接返回,并提示需要改变缓冲区大小设置
if (size > serial->config.rx_bufsz)
{
LOG_W("(%s) serial device received data:[%d] larger than " "rx_bufsz:[%d], please increase the BSP_UARTx_RX_BUFSIZE option",dev->parent.name, size, serial->config.rx_bufsz);
return 0;
}
//计算中断接收到缓冲区的字节数量
recv_len = rt_ringbuffer_data_len(&(rx_fifo->rb));
//如果收到的数据小于应用层要求的数据,那么调用rt_completion_wait陷入等待
//直至有足够的数据才退出
if (recv_len < size)
{
rx_fifo->rx_cpt_index = size;
//与下文中断服务函数中完成量的唤醒遥相呼应,从而在应用层视角看是以阻塞模式进行数据接收
rt_completion_wait(&(rx_fifo->rx_cpt),RT_WAITING_FOREVER);
}
//关中断,确保多线程下不会出现竞态条件
level = rt_hw_interrupt_disable();
//将缓冲区的数据转移到应用层缓冲区
recv_len = rt_ringbuffer_get(&(rx_fifo->rb), buffer, size);
rt_hw_interrupt_enable(level);
}
}
看到这里可能有人会问,怎么数据突然就跑到接收缓冲区了,数据是在什么时候被放进去的,详细的流程是什么?接下来,笔者就通过一个场景以及相关的代码大致来说明中断接收的整个流程.

说明:
RDR: RX Data Register 接收数据寄存器
RXNE: RX Data Register Not Empty 接收数据寄存器为空
uart_isr() : 驱动层中断服务函数
rt_hw_serial_isr() : 驱动框架层中断服务函数
rx_fifo: 接收缓冲区

假设接收缓冲区大小为128,即在etconfig中设置#define BSP_UART1_RX_BUFSIZE 128
调用rt_device_open(uart_dev, RT_SERIAL_RX_BLOCKING),与rt_device_read(uart_dev,buffer,128),即应用层一次读取128个数据
用户输入一段字符
设备收到字符触发RXNE中断
驱动层中断服务函数uart_isr()接收RXNE中断,将字符传入缓冲区,并调用rt_hw_serial_isr()
框架层中断服务函数rt_hw_serial_isr()根据rx_fifo接收的数据量,来判断阻塞接收是否结束,若接收足够的数据则唤醒完成量,并设置回调函数的参数
若rx_fifo中未获取足够的数据,重复4 - 6步,直至接收完成,此时rt_device_read()函数结束,返回接收字节数量.
//驱动层中断服务函数
static void uart_isr(struct rt_serial_device *serial)
{
...
//如果RXNE未被复位,且接收到RXNE中断,则从RDR寄存器中取数据
if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET) &&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_RXNE) != RESET))
{
//调用环形缓冲区ringbuff的putchar(),将读取的一个字节放入ringbuffer
rt_ringbuffer_putchar(&(rx_fifo->rb), UART_GET_RDR(&
uart->handle, stm32_uart_get_mask(...)));
//调用设备框架层中断服务函数,做后续的处理,决定读取是否完成,见下一个代码块
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_IND);
}
...
}
//框架层中断服务函数
void rt_hw_serial_isr(struct rt_serial_device *serial, int event)
{
...
case RT_SERIAL_EVENT_RX_IND:
//计算RX缓冲区的数据量
rx_length = rt_ringbuffer_data_len(&rx_fifo->rb);
if (serial->parent.open_flag & RT_SERIAL_RX_BLOCKING)
{
//如果ringbuffer缓冲区中有足够的数据,那么可以调用完成量的唤醒函数
//唤醒上文中rt_serial_read()中等待的完成量
if (rx_fifo->rx_cpt_index && rx_length >= rx_fifo->rx_cpt_index)
{
rx_fifo->rx_cpt_index = 0;
//代表此次数据接收完成
//此处的唤醒与上文完成量的等待遥相呼应
rt_completion_done(&(rx_fifo->rx_cpt));
}
if (serial->parent.rx_indicate != RT_NULL)
//为回调函数函数设置参数
serial->parent.rx_indicate(&(serial->parent), rx_length);
}
}
总结,驱动层保证数据从RDR寄存器到rx_fifo,即接收缓冲区,框架层则统计tx_fifo中的数据是否符合应用层要求,,如果符合要求,则将数据复制到应用层缓冲区,二者有机结合,形成阻塞读取.

DMA
中断方式效率虽然很高,但是对于大量数据的传输就显得力不从心。大量的中断会导致CPU忙于处理中断而减少对指令的处理,效率会变的很低。对于大量的数据传输可以不通过CPU而直接传送到内存,这种方式叫做DMA(DIrect Memory Access)。使用DMA方式,外部设备在数据准备好之后只需向DMA控制器发送一个命令,把数据的地址和大小传送过去,由DMA控制器负责把数据从外部设备直接存放到内存因此,DMA方式适合处理大量的数据。

由上文rt_serial_read()函数分析可知,DMA接收是由_serial_fifo_rx(dev, pos, buffer, size)实现的.虽然DMA模式接收的串口设备框架代码与中断接收一致,但是在中断服务函数部分还是有很多不同.

//驱动框架层函数
static rt_ssize_t _serial_fifo_rx(struct rt_device *dev,
rt_off_t pos,
void *buffer,
rt_size_t size)
{
//中断接收部分代码
if (dev->open_flag & RT_SERIAL_RX_BLOCKING)
{
...
//此处可以注意到,阻塞接收的函数第一步是检查应用层想读的数据量是否大于缓存,
//如果大于,函数将直接返回,并提示需要改变缓冲区大小设置
if (size > serial->config.rx_bufsz)
{
LOG_W("(%s) serial device received data:[%d] larger than " "rx_bufsz:[%d], please increase the BSP_UARTx_RX_BUFSIZE option",dev->parent.name, size, serial->config.rx_bufsz);
return 0;
}
//计算中断接收到缓冲区的字节数量
recv_len = rt_ringbuffer_data_len(&(rx_fifo->rb));
//如果收到的数据小于应用层要求的数据,那么调用rt_completion_wait陷入等待
//直至有足够的数据才退出
if (recv_len < size)
{
rx_fifo->rx_cpt_index = size;
//与下文中断服务函数中完成量的唤醒遥相呼应,从而在应用层视角看是以阻塞模式进行数据接收
rt_completion_wait(&(rx_fifo->rx_cpt), RT_WAITING_FOREVER);
}
//关中断,确保多线程下不会出现竞态条件
level = rt_hw_interrupt_disable();
//将缓冲区的数据转移到应用层缓冲区
recv_len = rt_ringbuffer_get(&(rx_fifo->rb), buffer, size);
rt_hw_interrupt_enable(level);
}
}
同样,我们再次通过一个场景以及相关的代码大致来说明DMA接收的整个流程

说明:
IDLE: 检测到总线空闲中断
uart_isr() : 驱动层中断服务函数
dma_recv_isr() : 驱动层DMA接收中断处理函数
rt_hw_serial_isr() : 驱动框架层中断服务函数
rx_fifo: 接收缓冲区

调rt_device_open(),rt_device_read()
DMA外设读取用户输入的一批字符
DMA将字符放入缓冲区后触发IDLE中断
中断服务函数uart_isr()接收IDLE中断,调用dma_recv_isr()计算接收字符数量
dma_recv_isr()最后调用rt_hw_serial_isr(),根据缓冲区数据数量来决定是否唤醒完成量,即如果数据不够则不唤醒,继续接收数据,最后设置回调函数的参数
重复2 - 5步, 直至接收完成,此时rt_device_read()中的完成量被唤醒,读取函数结束,返回接收字节数量
static void uart_isr(struct rt_serial_device *serial)
{
#ifdef RT_SERIAL_USING_DMA
//查看IDLE中断是否发生
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))
{
//调用设备框架层中断处理函数
dma_recv_isr(serial, UART_RX_DMA_IT_IDLE_FLAG);
__HAL_UART_CLEAR_IDLE_FLAG(&uart->handle);
}
#endif
}
static void dma_recv_isr(struct rt_serial_device *serial,
rt_uint8_t isr_flag) {
...
//省略计算接收数据量的过程
if (recv_len) {
//将接收的数据量一同发往串口设备框架层
uart->dma_rx.remaining_cnt = counter;
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_DMADONE
| (recv_len << 8));
}
...
}
//串口设备框架层中断服务函数
void rt_hw_serial_isr(struct rt_serial_device * serial, int event) {
//获取DMA接收的数据量
rx_length = (event & (~0xff)) >> 8;
if (rx_length)
{
//关中断,避免并发bug
level = rt_hw_interrupt_disable();
//更新环形缓冲区的索引
rt_serial_update_write_index(&(rx_fifo->rb),rx_length);
//更新完成后再开中断
rt_hw_interrupt_enable(level);
}
if (serial->parent.open_flag & RT_SERIAL_RX_BLOCKING)
{
//如果缓冲区数据足够则唤醒完成量
if (rx_fifo->rx_cpt_index && rx_length >= rx_fifo->rx_cpt_index )
{
rx_fifo->rx_cpt_index = 0;
//唤醒完成量
rt_completion_done(&(rx_fifo->rx_cpt));
}
}
}
总结,DMA接收与中断接收很多代码是复用的,其思路与中断接收及其相似,但在接收数据方面,DMA独立自行接收数据,不需要像中断接收一样,每次读取数据都占用CPU,从RDR寄存器拿到数据,再放入缓冲区中.在DMA模式下,中断处理程序接收到TC中断时,数据已然在接收缓冲区中.因此DMA接收的效率比中断更高.

阻塞模式下的数据发送
数据发送函数分析
数据发送也遵循最开始提出的硬件工作模式选择,即缓冲区为0则使用轮询,开启DMA则优先使用DMA,否则使用中断.

static rt_ssize_t rt_serial_write(struct rt_device *dev,
rt_off_t pos,
const void *buffer,
rt_size_t size)
{
...
if (serial->config.tx_bufsz == 0)
{
//发送缓冲区为0则,使用轮询
return _serial_poll_tx(dev, pos, buffer, size);
}
if (dev->open_flag & RT_SERIAL_TX_BLOCKING)
{
//如果tx缓冲区为空指针,则使用DMA发送
//因为DMA发送只需要应用层缓冲区,不需要框架层缓冲区
if ((tx_fifo->rb.buffer_ptr) == RT_NULL)
{
return _serial_fifo_tx_blocking_nbuf(dev, pos, buffer, size);
}
//需要用到框架层缓冲区,则使用中断发送
return _serial_fifo_tx_blocking_buf(dev, pos, buffer, size);
}
...
}
轮询
//框架层轮询发送函数
rt_ssize_t _serial_poll_tx(struct rt_device *dev,
rt_off_t pos,
const void *buffer,
rt_size_t size)
{
...
//核心代码
while (size)
{
//调用驱动层函数,一次将一个字节发送出去,详解见下文
serial->ops->putc(serial, *putc_buffer);
++ putc_buffer;
-- size;
}
return putc_size - size;
}
//驱动层函数
//serial->ops->putc实际为串口驱动中的stm32_putc函数:
static int stm32_putc(struct rt_serial_device *serial, char c)
{
//重置传输完成中断标志(ISR寄存器中)
UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_TC);
//将一个字节的数据放入TDR寄存器中
UART_SET_TDR(&uart->handle, c);
//陷入等待,直到获取到传输完成标志位(TC),此处体现了阻塞发送,即未发送完成则一直等待
while (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) == RESET);
return 1;
}
中断
//框架层中断发送函数
static rt_ssize_t _serial_fifo_tx_blocking_buf(
struct rt_device *dev,
rt_off_t pos,
const void *buffer,
rt_size_t size)
{
...
//当激活标识为TRUE,代表tx_fifo即发送缓冲区正在被使用,因此直接返回0,不发送数据
if (tx_fifo->activated == RT_TRUE) return 0;
//之前tx_fifo未被使用,则现在将其设置为TRUE,表示被当前发送线程中的中断发送占用
tx_fifo->activated = RT_TRUE;
//阻塞发送的核心代码
while (size)
{
//将应用层中的数据先复制一份到发送缓冲区中
tx_fifo->put_size = rt_ringbuffer_put(&(tx_fifo->rb),
(rt_uint8_t *)buffer + offset,size);
//调用底层驱动的传输函数,发送数据
serial->ops->transmit(serial,
(rt_uint8_t *)buffer + offset,
tx_fifo->put_size,
RT_SERIAL_TX_BLOCKING);
//更新已经发送的数据量
offset += tx_fifo->put_size;
size -= tx_fifo->put_size;
//等待传输完成
rt_completion_wait(&(tx_fifo->tx_cpt), RT_WAITING_FOREVER);
}
return length;
}
关于上文的函数调用:

serial->ops->transmit()函数会调用驱动层的stm32_transmit()
stm_32transmit()又直接会调用驱动层的stm32_control()对中断进行初始化.,从而进行发送
我们直接看stm32_control(serial, RT_DEVICE_CTRL_SET_INT, (void * )tx_flag);

//驱动层中断发送函数
static rt_err_t stm32_control(struct rt_serial_device *serial,
int cmd, void *arg)
{
...
case RT_DEVICE_CTRL_SET_INT:
//设置单个中断抢占优先级和响应优先级
HAL_NVIC_SetPriority(uart->config->irq_type, 1, 0);
//设置使能中断通道
HAL_NVIC_EnableIRQ(uart->config->irq_type);
if (ctrl_arg == RT_DEVICE_FLAG_INT_TX)
//开启TXE中断
__HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_TXE);
break;
...
}
此处,我们结合中断,来梳理中断发送的大致流程:

说明:
TDR: TX Data Register 发送数据寄存器
TXE: TX Data Register Empty 发送数据寄存器为空
TC: Transmission Complete 发送完成
uart_isr() : 驱动层中断服务函数
rt_hw_serial_isr() : 驱动框架层中断服务函数
tx_fifo: 发送缓冲区

假设有1024字节的数据通过串口发送,发送缓冲区设置为128;
用户调用rt_device_open(uart_dev,RT_SERIAL_TX_BLOCKING)与rt_device_write(uart_dev,buffer,1024);
1024字节中先有128个字节先被复制到缓冲区,调用驱动层stm32_transmit(),初始化中断相关函数,启动发送
中断服务函数uart_isr()检测到TXE中断,将一个字节放入TDR中
(在TDR中的数据后续会被转移至移位寄存器,TDR因此又会空出来)
中断服务函数uart_isr()会接收后续的TXE中断,从而将tx_fifo中的数据都被依次送往TDR寄存器
当缓冲区中没有可发送的数据时,关闭TXE中断,开启TC中断
如果检测到TC中断,则代表移位寄存器中所有的数据都被发送完,此时关闭TC中断,驱动层中断服务函数uart_isr()调用应用层中断服务函数rt_hw_serial_isr()
在rt_hw_serial_isr()中,检测tx_fifo是否为空,为空唤醒完成量,rt_device_write()进行新一轮的组阻塞发送
当所有1024个数据被分8次发送完成,rt_device_write()返回,否则重复3-8步
static void uart_isr(struct rt_serial_device *serial)
{
//TXE标志位被设立,且检测到TXE中断
if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TXE) != RESET) &&(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TXE)) != RESET)
{
...
//如果能够从tx_fifo中读到一个字节的数据,则将其发送到TDR中,
if (rt_ringbuffer_getchar(&(tx_fifo->rb), &put_char))
{
UART_SET_TDR(&uart->handle, put_char);
}
else
{
//如果没有数据可以发送,则关TXE中断,开TC中断
__HAL_UART_DISABLE_IT(&(uart->handle), UART_IT_TXE);
__HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_TC);
}
}
...
//TC标志位被设立,且检测到TC中断
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) &&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TC) != RESET))
{
//传输完成,则关闭TC中断,调用驱动框架层中断服务函数rt_hw_serial_isr()
__HAL_UART_DISABLE_IT(&(uart->handle), UART_IT_TC);
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_TX_DONE);
}
}
void rt_hw_serial_isr(struct rt_serial_device *serial, int event)
{
case RT_SERIAL_EVENT_TX_DONE:
{
//获取tx_fifo中的数据长度
tx_length = rt_ringbuffer_data_len(&tx_fifo->rb);
if (tx_length == 0)
{
tx_fifo->activated = RT_FALSE;
//tx_fifo中的数据全部发送完,则唤醒完成量,进行下一次发送
if (serial->parent.tx_complete != RT_NULL)
serial->parent.tx_complete(&serial->parent, RT_NULL);
//设置回调函数
if (serial->parent.open_flag & RT_SERIAL_TX_BLOCKING)
rt_completion_done(&(tx_fifo->tx_cpt));
break;
}
}
}
一个可能存在的问题:
因为中断发送应用层数据量可能会大于发送缓冲区大小,因此可能需要多次发送,例如上文发送1024个数据需要将数据分8次拷贝到缓存中,然而,在上述代码中我们可以发现,在一次发送完,tx_fifo的激活标识就被设置为flase了,这意味者,还在工作的tx_fifo被标注为不工作,那么在多线程环境下,这很可能会导致恶性的bug,针对此问题,笔者已经提了相关的pr(#7997).

//有问题的代码
if (tx_length == 0)
{
tx_fifo->activated = RT_FALSE;
...
}
总结,中断发送跟轮询发送明显的区别在于,中断发送是直接将数据放入RDR(RX Data Register),然后返回,而轮询发送,有用while死等TC中断这一过程.因此中断发送相较于轮询发送,减少了对于CPU的占用率.

//中断发送
UART_SET_TDR(&uart->handle, put_char);
//轮询发送
UART_SET_TDR(&uart->handle, c);
while (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) == RESET);
DMA
//DMA发送 框架层函数
static rt_ssize_t _serial_fifo_tx_blocking_nbuf(
struct rt_device *dev, rt_off_t pos,
const void *buffer, rt_size_t size)
{
if (tx_fifo->activated == RT_TRUE) return 0;
//将激活标识设置为TRUE
tx_fifo->activated = RT_TRUE;
//开启DMA发送
rst = serial->ops->transmit(serial,
(rt_uint8_t *)buffer,
size,
RT_SERIAL_TX_BLOCKING);
//等待完成量的唤醒
rt_completion_wait(&(tx_fifo->tx_cpt), RT_WAITING_FOREVER);
return rst;
}
//DMA发送 驱动层函数
static rt_ssize_t stm32_transmit(struct rt_serial_device *serial,
rt_uint8_t *buf,
rt_size_t size,
rt_uint32_t tx_flag)
{
if (uart->uart_dma_flag & RT_DEVICE_FLAG_DMA_TX)
{
//调用DMA传输
HAL_UART_Transmit_DMA(&uart->handle, buf, size);
return size;
}
}
DMA的大致传输流程:
说明:
TC: Transmission Complete

假设有256字节的数据通过串口发送,发送缓冲区设置为256
用户调用rt_device_open(uart_dev,RT_SERIAL_TX_BLOCKING)与rt_device_write(uart_dev,buffer,256);
驱动层调用HAL_UART_Transmit_DMA(),开启DMA数据发送
数据发送完成后,驱动层中断服务函数uart_isr()会接收TC中断,继而调用HAL_UART_TxCpltCallback函数中的rt_hw_serial_isr().在驱动框架层的中断服务函数中,会唤醒完成量,结束一次DMA发送
//DMA发送 驱动层函数
static void uart_isr(struct rt_serial_device *serial)
{
...
//接收到TC中断
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) &&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TC) != RESET))
{
if (uart->uart_dma_flag & RT_DEVICE_FLAG_DMA_TX)
{
//HAL_UART_TxCpltCallback()将被调用
HAL_UART_IRQHandler(&(uart->handle));
}
}
}
//DMA发送 驱动层函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
//计算发送数量
level = rt_hw_interrupt_disable();
trans_total_index = __HAL_DMA_GET_COUNTER(&(uart->dma_tx.handle));
rt_hw_interrupt_enable(level);
if (trans_total_index) return;
//调用驱动层中断服务函数
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_TX_DMADONE);
}
//DMA发送驱动框架层函数
void rt_hw_serial_isr(struct rt_serial_device *serial, int event)
{
case RT_SERIAL_EVENT_TX_DMADONE:
{
//将激活标志设置为FALSE,其他线程可以使用串口发送数据
tx_fifo->activated = RT_FALSE;
//设置回调函数的实参
if (serial->parent.tx_complete != RT_NULL)
serial->parent.tx_complete(&serial->parent, RT_NULL);
if (serial->parent.open_flag & RT_SERIAL_TX_BLOCKING)
{
//唤醒完成量
rt_completion_done(&(tx_fifo->tx_cpt));
break;
}
}
}
DMA发送只需要调用HAL_UART_Transmit_DMA(),开启DMA发送,然后等待TC中断,发送的具体过程都由DMA自行处理,不需要像中断一样,还需要进行持续接收TXE中断,将数据放入TDR寄存器,以及关闭TXE中断开启TC中断等一系列操作,因此进一步降低了CPU的占用率,节省系统资源.

写在最后
以上就是对于串口设备框架serial_v2的源码分析,鉴于篇幅,只分析了阻塞模式,非阻塞模式的整体框架与设置模式是相同的。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 驱动器
    +关注

    关注

    52

    文章

    8154

    浏览量

    145986
  • 缓冲器
    +关注

    关注

    6

    文章

    1917

    浏览量

    45449
  • STM32
    +关注

    关注

    2266

    文章

    10871

    浏览量

    354776
  • 串口中断
    +关注

    关注

    0

    文章

    64

    浏览量

    13858
  • RT-Thread
    +关注

    关注

    31

    文章

    1272

    浏览量

    39916
收藏 人收藏

    评论

    相关推荐

    linux串口通信阻塞与非阻塞问题。

    一般情况下串口通信中read()函数都是阻塞的。当然也可以通过设置文件等方法把串口设为非阻塞的。这里就有一个问题,当串口设置为
    发表于 07-14 15:57

    RT-Thread串口的硬件工作模式有哪几种呢?怎样去使用

    了 rt_hw_serial_register() 函数注册串口设备到操作系统中,也是与串口硬件直接打交道的媒介,这一层将会看到串口硬件的配
    发表于 03-11 15:03

    rt-thread驱动篇之serial v1 v2 和 X测试

    时序。回环测试数据丢失率使用阻塞模式打开串口设备。这次通过串口调试助手以 20ms 的定时间隔,发送 384 字节数据。即便考虑到
    发表于 04-14 14:36

    如何基于rt-thread的serial框架来实现msh组件的使用

    设备驱动框架serial,通过serial我们能找到当前操作的是哪个串口,另一个就是配置cfg,这个cfg里的参数就是我们上面注册
    发表于 05-13 16:13

    串口框架V1和V2版本对比差异及其串口V2的一些设计实现思想

    _RX_MINBUFSZ 64#define RT_SERIAL_TX_MINBUFSZ 64模式选择上的差别V2版本 基本完全兼容V1版本的串口
    发表于 08-02 15:52

    帮助你看懂并理解rtt官方的IO设备模型框架

    多态。从设备驱动框架层到IO设备管理层也是这样的。比如串口设备类rt_serial_device
    发表于 10-09 15:30

    如何使用RT-Thread的串口设备

    STM32F4 探索者开发板上验证。接着分析了示例代码的实现,最后深入地描述了 RT-Thread 设备管理框架串口的联系。问题阐述RT-Thread 提供了一套简单的 I/O
    发表于 10-25 11:05

    有没有哪位铁子通过studio添加serial_v2并修改成功过的

    4.1.0版本内核stm32f407利用studio添加串口Serial_v2,有没有哪位铁子通过studio添加serial_v2并修改成功过的,给分享下呗
    发表于 11-09 14:13

    GD32F425 + rtthread5.0使用UART V2版本无法完成初始化如何解决?

    环境: 系统版本:rtthread 5.0 MCU型号:GD32F425 设置MCU的UART1 为串口打印功能。 使用的serial_V2版本串口组件 问题: 在程序初始化之处,在初始化串口
    发表于 09-03 11:55

    Serial Monitor (串口监视、检测、分析工具)v

    Serial Monitor:Serial Monitor是一款功能强大的串口监视、检测、分析工具,软件使用更加简单,尤其适合开发人员使用。 
    发表于 05-26 09:03 86次下载

    Proteus串口资料COMPIM Serial Port

    Proteus串口资料COMPIM Serial Port Model The COMPIM model is a Physical Interface Model of a serial port. Incoming
    发表于 04-17 16:13 0次下载

    串口猎人(serial hunter) v31中文版_串口猎人免费下载

    电子发烧友网站提供《串口猎人(serial hunter) v31中文版_串口猎人免费下载.rar》资料免费下载
    发表于 01-17 08:41 0次下载

    Serial Studio:串口设备的仪表盘软件

    电子发烧友网站提供《Serial Studio:串口设备的仪表盘软件.zip》资料免费下载
    发表于 06-13 16:09 2次下载
    <b class='flag-5'>Serial</b> Studio:<b class='flag-5'>串口</b><b class='flag-5'>设备</b>的仪表盘软件

    nrf_serial库的使用技巧

    nrf_serial 库是nordic封装的比较上层的串口抽象层。可以支持多个串口,可以使用阻塞、中断、DMA三种模式。接口API比较简单,
    的头像 发表于 10-24 14:33 654次阅读

    串口驱动分析serial driver

    前两节我们介绍串口驱动的框架和tty core部分。这节我们介绍和硬件紧密相关的串口驱动部分。
    的头像 发表于 09-04 14:23 341次阅读
    <b class='flag-5'>串口</b>驱动<b class='flag-5'>分析</b>之<b class='flag-5'>serial</b> driver