前言
Linux 开发时,经常会遇到串口通信来完成两个设备之间的交互。
串口通信依赖于一种叫做串行通信协议的规则,它在数据传输过程中控制数据的流动,包括数据位的设置、波特率的调整、校验位的确定以及停止位的选择等。
然而,在串口通信中,我们通常不知道对方会发送多少数据,也不清楚数据何时发送完毕。简而言之,问题在于:我们如何确保接收到一帧完整的数据?判断一个完整帧就需要知道,帧什么时候开始,又在什么时候结束。
判断完整帧方式
串口传输的数据长度不一,如果接收不完整,将直接影响到后续的业务处理。为了应对接收不定长数据的挑战,我们有几种常见的处理方法:
- 固定格式法: 双方约定,数据帧以特定的字符序列表示数据帧开始和数据帧结束。例如以字符序列"FF FE"表示帧开头,以"FF EF"表示帧结尾。接收方在接收到开始序列时开始收集数据,直到接收到结束序列为止。这种方法虽然简单明了,但需要接收方对每个字符进行判断,这会消耗 CPU 资源,增加能耗。
- 接收中断与超时判断: 当串口接收到数据时,会触发接收中断。并且一般情况下两帧数据之间会有一段间隔,由此我们可以设计一个定时器,如果在设定的时间内没有接收到新的字符,则超时就认为当前的数据帧已经接收完毕。
- 空闲中断法: 当串口在一段时间内没有接收到新数据时,会触发空闲中断。 空闲中断实际上与接收中断的超时判断原理相似,但空闲中断是硬件自带的功能,而接收中断的超时判断则需要我们自己实现。一旦接收到空闲中断,我们就可以认为已经接收到了一帧完整的数据。
接收中断
我们先理解下接收中断,当接收方接收到数据时,就会触发接收中断。
举例:某中断寄存器的如下图所示。当接收方收到数据时,图中的串口状态寄存器第 5 位(RXNE)会被置 1。当我们将 USART_DR 寄存器的值都出来后,该位置又会被清除,即置 0。
接收中断与超时判断实现
硬件
- 联盛德W 801 开发板
- typec 数据线:用于调试或下载程序
- USB 转 TTL:用于uart 1 串口通信
W 801 的UART 中断状态寄存器表说明如下表。
SDK代码中对uart 中断状态的定义如下:
代码语言:javascript复制/*
* uart interrupt source register bits definition
*/
#define UIS_TX_FIFO_EMPTY (1UL<<0)
#define UIS_TX_FIFO (1UL<<1)
#define UIS_RX_FIFO (1UL<<2)
#define UIS_RX_FIFO_TIMEOUT (1UL<<3)
#define UIS_CTS_CHNG (1UL<<4)
#define UIS_BREAK (1UL<<5)
#define UIS_FRM_ERR (1UL<<6)
#define UIS_PARITY_ERR (1UL<<7)
#define UIS_OVERRUN (1UL<<8)
UIS_TX_FIFO_EMPTY
:发送FIFO空中断。当发送FIFO(First In, First Out,先进先出队列)为空时,该位被设置,位值为(1UL<<0)
,即二进制的00000001
。UIS_TX_FIFO
:发送FIFO中断。当发送FIFO中的数据量达到某个阈值时触发,位值为(1UL<<1)
,即二进制的00000010
。UIS_RX_FIFO
:接收FIFO中断。当接收FIFO中的数据量达到某个阈值时触发,位值为(1UL<<2)
,即二进制的00000100
。UIS_RX_FIFO_TIMEOUT
:接收FIFO超时中断。当接收FIFO中有数据,但在一定时间内没有新的数据到来时触发。位值为(1UL<<3)
,即二进制的00001000
。UIS_CTS_CHNG
:CTS(Clear To Send,清除发送)信号变化中断。当CTS信号的状态发生变化时触发,位值为(1UL<<4)
,即二进制的00010000
。UIS_BREAK
:"break"条件检测中断。当检测到"break"条件时触发,如前所述,"break"是一个长时间的低电平信号。位值为(1UL<<5)
,即二进制的00100000
。UIS_FRM_ERR
:帧错误中断。当检测到帧错误时触发,帧错误发生在数据帧的起始位或停止位不正确时。位值为(1UL<<6)
,即二进制的01000000
。UIS_PARITY_ERR
:奇偶校验错误中断。当使用奇偶校验并且接收到的数据与预期的奇偶校验位不匹配时触发。位值为(1UL<<7)
,即二进制的10000000
。UIS_OVERRUN
:溢出错误中断。当接收FIFO已满,但新的数据仍然到来时触发,导致最旧的数据被覆盖。位值为(1UL<<8)
,即二进制的100000000
。
判断接收中断需要使用 UIS_RX_FIFO
,即uart 中断状态表中的第 2 位。
代码实现
串口初始化
定义接收数据变量和信号量,并定义接收的回调函数。
代码语言:javascript复制// 存储接收到的数据
char rx_fifo_buf[1024] = {0};
// 存储收到数据的长度
uint16_t rx_length = 0;
// 定义一个信号量
tls_os_sem_t *sem_rx = NULL;
// 接收回调函数
s16 uart1_rx_callback(u16 len, void *user_data)
{
// 将接收到字符累加
rx_length = len;
tls_os_sem_release(sem_rx);
return WM_SUCCESS;
}
定义串口的波特率、数据位、停止位等,并调用 tls_uart_port_init
进行初始化。最后为uart串口 1绑定接收回调函数。
// 串口初始化
void uart1_init()
{
// 初始化uart1引脚,函数内部会自动开启串口时钟,
wm_uart1_rx_config(WM_IO_PB_07);
wm_uart1_tx_config(WM_IO_PB_06);
// 配置uart参数
tls_uart_options_t opt;
opt.baudrate = UART_BAUDRATE_B115200; // 波特率
opt.paritytype = TLS_UART_PMODE_DISABLED; // 无奇偶校验
opt.stopbits = TLS_UART_ONE_STOPBITS; // 一个停止位
opt.charlength = TLS_UART_CHSIZE_8BIT; // 数据长度
opt.flow_ctrl = TLS_UART_FLOW_CTRL_NONE; // 没有流控制
// 初始化串口,这个函数内部会开启串口中断
// 当第二个参数为NULL的时候,串口会按默认参数初始化这个串口,具体请看SDK函数tls_uart_port_init的实现
if (WM_SUCCESS != tls_uart_port_init(TLS_UART_1, &opt, 0))
{
printf("uart1 init errorn");
}
// 另一种方式初始化串口:使用默认方式初始化,在定制参数。默认初始化可能不符合要求,那么就用sdk来修改uart参数
// if (WM_SUCCESS != tls_uart_port_init(TLS_UART_1, NULL, 0))
// {
// printf("uart1 init errorn");
// }
// tls_uart_set_baud_rate(TLS_UART_1, UART_BAUDRATE_B9600)//波特率
// tls_uart_set_parity(TLS_UART_1, TLS_UART_PMODE_DISABLED);//无奇偶校验
// tls_uart_set_stop_bits(TLS_UART_1, TLS_UART_ONE_STOPBITS);//一个停止位
// 为串口绑定接收回调函数
tls_uart_rx_callback_register((u16)TLS_UART_1, (s16(*)(u16, void *))uart1_rx_callback, NULL);
}
接收中断与超时判断
接收中断判断,在串口 1 接收中断里,我们可以使用 port->regs->UR_INTS
获取 UART 中断状态寄存器,如果 rxfifo trigger level interrupt
有值,且UART 中断屏蔽寄存器中的 UIS_RX_FIFO
位没有被屏蔽(表示接收中断是使能的)则进入接收中断处理,调用 port->regs->UR_RXW
接收字符,并保存于临时变量 recv->buf
中。
#define UART_RX_INT_FLAG (UIS_RX_FIFO | UIS_RX_FIFO_TIMEOUT | UIS_BREAK |
UIS_OVERRUN | UIS_FRM_ERR | UIS_PARITY_ERR)
#define UART_RX_ERR_INT_FLAG (UIS_BREAK | UIS_FRM_ERR |
UIS_PARITY_ERR)
#define UART_TX_INT_FLAG (UIS_TX_FIFO | UIS_TX_FIFO_EMPTY)
ATTRIBUTE_ISR void UART1_IRQHandler(void)
{
struct tls_uart_port *port = &uart_port[1];
struct tls_uart_circ_buf *recv = &port->recv;
u8 rx_byte_cb_flag = uart_rx_byte_cb_flag[1];
u32 intr_src;
u32 rx_fifocnt;
u32 fifos;
u8 ch = 0;
u8 escapefifocnt = 0;
u32 rxlen = 0;
csi_kernel_intrpt_enter();
intr_src = port->regs->UR_INTS;
if ((intr_src & UIS_RX_FIFO) && (0 == (port->regs->UR_INTM & UIS_RX_FIFO)))
{
// 处理接收中断
rx_fifocnt = (port->regs->UR_FIFOS >> 6) & 0x3F; // 从UART FIFO状态寄存器读取数据个数
escapefifocnt = rx_fifocnt;
port->plus_char_cnt = 0;
rxlen = rx_fifocnt;
// 检查环形缓冲区`recv`的可用空间是否小于或等于`RX_CACHE_LIMIT`。如果是,则更新缓冲区尾部指针以限制接收的数据量。
if (CIRC_SPACE(recv->head, recv->tail, TLS_UART_RX_BUF_SIZE) <= RX_CACHE_LIMIT)
{
recv->tail = (recv->tail RX_CACHE_LIMIT) & (TLS_UART_RX_BUF_SIZE - 1);
}
while (rx_fifocnt-- > 0)
{
// UR_RXW是UART RX 起始地址寄存器,读取接收数据存入缓存区
ch = (u8) port->regs->UR_RXW;
recv->buf[recv->head] = ch; // 将接收到的字符`ch`存储到环形缓存区`recv->buf`的头部位置。
recv->head = (recv->head 1) & (TLS_UART_RX_BUF_SIZE - 1); // 更新环形缓存区的头部指针,准备下一次数据存储。
}
// 调用接收回调函数
if (port->rx_callback!=NULL && !rx_byte_cb_flag)
{
port->rx_callback(rxlen, port->priv_data); // 传递接收到的数据长度,和私有数据(这里并没有使用)
}
}
csi_kernel_intrpt_exit();
}
通过 tls_os_sem_create
创建一个信号量,与回调函数和当前线程之间通信。通过 tls_os_sem_acquire
等待信号,这里设置了 20 个时钟周期,若在 20 个周期内未收到 sem_rx
信号,这表示这个串口在 20 个时钟周期内没有收到数据,接收超时,已收到一个完整帧。
之后再通过 tls_uart_read()
函数读出 recv->buf
值,并将 rx_length
清零,调用 tls_uart_write()
函数将数据通过uart 1 串口发送回去。
// uart线程
void uart1_task(void *sdata)
{
uint16_t rx_len = 0;
// 用于判断信号量是否成功获取
tls_os_status_t os_status = TLS_OS_ERROR;
// 创建信号量,用于回调函数和线程之间通信
tls_os_sem_create(&sem_rx, 0);
// 初始化串口
uart1_init();
for (;;)
{
os_status = tls_os_sem_acquire(sem_rx, 20); // 等待信号量到来,等待20个系统时钟周期
// 若发生错误(不为0),即在20个周期内未收到sem_rx信号,这表示这个串口在20个时钟周期内没有收到数据,接收超时,已收到一个完整帧
if (os_status)
{
// 如果串口有数据,表示一个包已经接收完了。
if (rx_length > 0)
{
// 读出这个包的数据,注意这个包不能超过1024个字节
rx_len = tls_uart_read(TLS_UART_1, rx_fifo_buf, rx_length);
rx_length = 0;
tls_uart_write(TLS_UART_1, rx_fifo_buf, rx_len); // 发送
}
}
}
}
小结
STM 32 串口通信功能在linux 开发和嵌入式开发中是最频繁使用的功能之一,从接收者角度来看,是不知道发送方的数据量是有多大的。如何判断一次发送的数据量,对接收者来说就是一个挑战。
前面提到有三种方案可以解决这个问题,本文介绍了接收中断和超时检测的方法来处理,并且在W 801 开发板上提供了详尽的指导教程。