最近在写一个serial 的应用想起以前写过的一些单片机上的uart 程序,有着许许多多的圈圈点点的,也就来扒一扒串口机制的事情了。 学习单片机都会接触到串口这个东西,多数的教程都是讲讲如何把寄存器配置好,然后可以发出数据、接收数据,而对如何应用基本完全不谈。而其实不管是哪一类CPU,串口的模式基本相同,毕竟这东西从单片机诞生至今也算是个白发老头的年纪了。uart 寄存器配置不说,每个CPU有自己的一套,我主要说数据的收发。 一种是Polling机制,如下图,发送/接收约定数量个数的数据,不断的检测发送/接收标志位,然后把新数据填入buff 中直到达到约定的数量后结束跳出。很明显,这里有个很大的问题,如果发送/接收没有达到约定数量就会出现在等待发送/接收成功这个检测点不断的循环,没有数据的情况下那就完全跳不出这个环节了,程序也就死机了
以STM32为例的Polling机制程序
1 2 3 4 5 6 7 8 9 10 11 | for ( i=0;TxBuf1[i]!= '\0' ;i++) { USART_SendData(USART1,TxBuf1[i]); // 发送Data while (USART_GetFlagStatus(USART1, USART_FLAG_TC)==RESET); //等待发送成功 } for ( i=0;RxBuf1[i]!= '\0' ;i++) { RxBuf[i] = USART_ReceiveData(USART1); // 接收Data while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE)==RESET); //等待发送成功 } |
另一种常见的机制就是Interrupt。中断机制的好处在于程序一直处于大循环中,只有数据来到了标志位产生并中断了才进入中断函数接收一个byte,发送接收后返回Loop 的运行代码继续运行。这里就很好的避免了Polling 等待中却没有数据来临而导致的死循环情况。但这里又产生一个问题数据的发送/接收数量要到什么时候才能停止?当然我们可以定义一个约定长度,当填满这个buff的size时认为数据发送/接收结束。还是以STM32 为例,中断uart 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | void USART1_IRQHandler() { if (USART_GetITStatus(USART1,USART_IT_RXNE) != RESET) //读中断产生 { USART_ClearITPendingBit(USART1,USART_IT_RXNE); //清除中断标志 RxBuffer1[Rx_Num] = USART_ReceiveData(USART1); Rx_Num++; if (Rx_Num == MAX_BUF) //接收数据数量完成 { USART_ITConfig(USART1, USART_IT_RXE, DISABLE); //关闭接收中断 } } if (USART_GetITStatus(USART1, USART_IT_TXE) != RESET) //发送中断产生 { USART_SendData(USART1, TxBuffer1[Tx_Num--]); if (Tx_Num == 0) //发送数据数量完成 { USART_ITConfig(USART1, USART_IT_TXE, DISABLE); //关闭发送中断 } } } |
到此,上面两种最最基本最常见的uart 机制。然而这种方式并不能很好的处理我们日常使用串口的数据,例如不定长度的,数据本身断流的问题,已经高级cpu 中内置buff 的自动发送,大数据块发送等等。
因此在基础的uart 数据收发上,引入了分层的数据收发管理 serial。serial 顾名思义就是串行数据,用于管理uart 等串行设备传送上来的数据并加以分析机制来管理数据状态。
通常 uart 的数据发送/接收分三种模式:轮询、中断、缓存buff(DMA 或 BUF), 前面两种模式上面都介绍过,而第三种通常在比较高级点的cpu 上才会支持,如SMT32 的USART DMA,数据一旦设置给cpu DMA 控制器,控制器自动把DMA 中的数据自动发送,发送/结束过程CPU 完全不参与,结束后DMA控制器会告诉CPU 任务已经完成了,适合数据量大的情况下使用。分清三种模式后,便可针对这三种模式来做数据的收发管理,对于应用来说我们希望只用 Uart_send(*buf); 和 len=Uart_Read(*buf); 两个简单的函数就实现数据的发送和接收,并且不会对主程序造成高阻塞情况。使用Uart API 接口函数 不需要知道uart 如何操作数据,在发送时候,只要串口打开,并且可运行下,把数据发送给Uart API 即可,在接收时候只要有数据接收到并且可用的情况下就能读取到数据。设计中为高效利用稀有的ram 使用ringbuf (环形缓存),当然如果选择Polling 模式就会绕开ringbuf了。 Send 部分设计相对简单,整个逻辑基本如下:
数据进入后判断当前配置的状态,选择响应的发送方式。polling 会打断整个process 直至发送完毕后才把CPU还给主进程,Interrupt 不断在中断函数与主process 之间来回切换,DMA 则配置完后完全不需要管理 process 继续自己跑。 产考代码:
需要用到的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | struct serial_ringbuffer { uint8_t buffer[RB_BUFSZ]; uint16_t put_index, get_index; uint16_t lst_time; }; struct serial_configure { uint16_t baud_rate; uint16_t data_bits; uint16_t stop_bits; uint16_t parity; uint16_t bit_order; uint16_t invert; uint16_t reserved; }; typedef struct _serial_device { /* UART1 or UART2 ...*/ uart_device dev; struct serial_configure config; /* rx structure */ struct serial_ringbuffer *int_rx; /* tx structure */ struct serial_ringbuffer *int_tx; /* tx dataqueue */ struct queue tx_dq; /* dma transfer flag */ unsigned char dma_flag; }serial_device; |
发送部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | static size_t serial_write( serial_device *dev, const void *buffer, size_t size) { uint8_t *ptr; size_t write_nbytes = 0; struct serial_device *serial; if (size == 0) return 0; serial = ( struct serial_device *)dev; ptr = (uint8_t*)buffer; if (dev->flag & FLAG_INT_TX) /* int mode*/ { /* cp buff to ringbuff tx */ while (size) { serial_ringbuffer_putchar(serial->int_tx, *ptr); ptr++; size--; } } else if (dev->flag & FLAG_DMA_TX) /* dma mode*/ { dma_transmit(serial, buffer, size); size = 0; } else { /* polling mode */ while (size) { putc (serial, *ptr); ++ ptr; -- size; } } return size; } |
Interrupt 模式和 DMA 模式还需要在中断中做判断处理,后面再帖中断处理的事件处理方式.
读取数据和写入数据比较接近,但也有很多区别,发送数据只要把发送buff 指定后便可以了,而接受数据需要判断数据长度是否到达 或者 是否达到预定的时间内都已经没有数据输入了,之后才能把接收到的数据送给 应用层。即使uart 设备已经接收到数据,但没有到达特定条件不能把数据送往应用层,应用层也获取不到数据,len=Uart_Read(*buf)获得的len 为 0 。数据的读取基本是应用程序中的一个动作,只要数据符合要求,就返回长度表示有效,否则返回0个数据表示当前没有数据,直到下一次应用程序再次询问的时候再发送下一次的状态给应用程序。可以看到Interrupt 上接收多了一个timer ,这是用于当数据接收没有存满buffer 而又已经没有数据来临的时候让timer计时,超出规定时间后把数据传送给应用层处理。 接收部分强化的主要是中端后的判断处理,接收部分代码和发送基本相似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | static size_t serial_read( struct device *dev, void *buffer, size_t size) { uint8_t *ptr; uint8_t *dat; static uint8_t dma_st = 0; uint32_t read_nbytes = 0; struct serial_device *serial; if (size == 0) return 0; serial = ( struct serial_device *)dev; ptr = (uint8_t *)buffer; if (dev->flag & FLAG_INT_RX) { /* interrupt mode Rx */ if (dev->flag & FLAG_INT_OVERTIME) { read_nbytes = serial_ringbuffer_getc(serial->int_rx); dat = serial->int_rx; size = read_nbytes; while (size--) { *prt = dat & 0xFF; prt++; dat++; } } } else if (dev->flag & FLAG_DMA_RX) /* dma mode*/ { if (dma_st == 0) { dma_receive(serial, buffer, size); dma_st = 1; } else if (dev->flag & FLAG_INT_DMARN) { read_nbytes = serial_ringbuffer_getc(serial->int_rx); dma_st = 0; } } else { /* polling mode */ while ((uint32_t)ptr - (uint32_t)buffer < size) { *ptr = serial-> getc (serial); ptr ++; read_nbytes++; } } return read_nbytes; } |
两个函数serial_write()和serial_read() 便可简单的封装给上层应用。而重点的地方还在于Interrupt 函数中对数据的判断处理,上面两个函数主要还是把数据分发给配置好适合的收发模式,而中断决定读取数据的情况。
STM32为例 中断函数:
1 2 3 4 5 6 7 8 9 10 11 | void USART1_IRQHandler( void ) { if (UART1->SR & USART_FLAG_TXE) { UART_Tx_ISR(); } if (UART1->SR & USART_FLAG_RXNE) { UART_Rx_ISR(); } } |
分别给接收和发送创造一个中断处理函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | void UART_Tx_ISR( void ) { unsigned short datalen; unsigned short wp, rp; if (id >= MAX_UART_NUM) return ; //snapshot wp = device->int_tx.put_index; rp = device->int_tx.get_index; //get data len datalen = UART_Get_Data_Len(wp, rp, UART_BUF_LEN); if (datalen == 0) { USART_ITConfig(USART1, USART_IT_TXE, DISABLE); return ; } USART1->DR = g_uart_tx_buf[id].Buf[rp]; device->int_tx.get_index ++; device->int_tx.get_index %= UART_BUF_LEN; } #define OVERTIME 50; void UART_Rx_ISR( void ) { unsigned short dat; if (id >= MAX_UART_NUM) return ; dat = USART1->DR; g_uart_rx_buf[id].int_rx.buffer[put_index] = dat & 0xFF; g_uart_rx_buf[id].int_rx.put_index ++; g_uart_rx_buf[id].int_rx.put_index %= UART_BUF_LEN; g_uart_rx_buf[id].int_rx.lst_time = OVERTIME; } |
其中 lst_time 需要添加一个定时器来减,当时间到达0 即可关闭uart rx 中断,并置位标志位,让应用读取数据后再打开。 PS : 一位前辈和我说过这么一句话,其实破平台也能做出优质的产品,而如果代码机制是破的,再高频率的CPU 再好的平台做出来的产品都是很劣质的产品。好的代码机制能做出好的产品设计。