系列文章目录
rt-thread 之 fal移植
rt-thread 之 生成工程模板
STM32------串口理论篇
rt-thread------串口V1版本(一)配置
rt-thread------串口V1版本(二)发送篇
文章目录
- 系列文章目录
- 一、串口的接收
- 中断接收
- DMA接收
一、串口的接收
串口接收常见的分中断接收和DMA接收,虽然RTT框架提供了轮训接收,但是其使用场景不多也不展开讨论。在RTT串口配置中设置了一个ringbuf,其作用是用来接收数据的。无论使用中断接收还是DMA接收数据都会存入ringbuf中。
配置时还注册了一个接收回调函数其作用是在接收完成(或者DMA半满、满中断时)时通知应用层代码作相应处理。下面详细讲讲这两种接收方式的代码实现。
中断接收
在RTT论坛刷到一个大佬的帖子给出了中断接收的流程。
中断接收无论如何都会触发单片机中断向量表中的中断函数,只需要去查找中断函数中具体实现即可。以usart2为例:
void USART2_IRQHandler(void)
{
/* enter interrupt */
rt_interrupt_enter();
uart_isr(&(uart_obj[UART2_INDEX].serial));
/* leave interrupt */
rt_interrupt_leave();
}
其中uart_isr
函数展开后与中断接收相关的如下:
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
函数否则则去清除一些错误标志位。显然重点在if成立的条件语句中。其源码如下所示,进入函数是个状态机,显然是想让这部分代码复用。只需要查看RT_SERIAL_EVENT_RX_IND
相关代码。
void rt_hw_serial_isr(struct rt_serial_device *serial, int event)
{
switch (event & 0xff)
{
case RT_SERIAL_EVENT_RX_IND:
{
int ch = -1;
rt_base_t level;
struct rt_serial_rx_fifo* rx_fifo;
/* interrupt mode receive */
rx_fifo = (struct rt_serial_rx_fifo*)serial->serial_rx;
RT_ASSERT(rx_fifo != RT_NULL);
/* 读取串口数据部分 */
/* 回调函数部分 */
}
上述代码可以分成两部分读取数据和回调函数调用部分。
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);
}
读取数据部分是负责把串口接收数据读取到ringbuf中,使用serial->ops->getc(serial)
函数,看这个名字就能猜到是获取串口接收的一个字节数据。对于STM32来说就是将串口的DR寄存器通过这个函数返回。看一下具体代码的实现,果然如此:
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;
}
将DR寄存器中的值存入ringbuf,还对ringbuf满的情况做了一下异常处理,ringbuf is_full
标志位会置一,数据会覆盖最早的数据导致数据丢失,所以需要保证应用层及时从ringbuf中取出数据。
再看看回调函数部分,若回调函数不为空,且这次就收到数据则调用一次接收回调函数。通常回调函数中释放一个信号量通知应用层程序从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;
}
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一次会接收一帧或者半帧数据才进一次中断。回调函数处理也会有所不一样,官方例程也不在释放信号量了,通过消息队列传出串口接收数据大小,交给应用层处理。
下面好好讲一下或
驱动框架会正对多数项目设计或者按照较为全面的方式去设计,这难免会和自己项目存在一些冲突。若使用RTT提供的V1版本DMA接收配合modbus协议则会出现一些问题,其问题是modbus会把帧长度不对的数据丢掉,而DMA半满和满中断也会触发回调函数发送串口接收的字节数,这就导致会有一帧数据被截成两段触发两次串口中断,最后数据被应用层丢弃。那就回归modbus协议断帧的本质是3.5个字符没接收到数据,所以改成DMA空闲中断触发串口中断即可,修改如下所示:
那么为什么RTT一开始DMA接收版本代码需要这两个回调函数也触发串口中断呢?存在即有意义,如果有些串口协议需要通过半满中断读取这一帧的长度呢?在满中断和空闲中断增加一个和帧的操作也能得到一个完整的帧。
个人观点是RTT可以将这些做成用户可选择的模式,用户可以根据自己实际项目选择是三个中断或的形式触发还是一个空闲中断触发。
参考:
RTT串口V1版本的使用分析及问题排查指南(一)