本帖最后由 suyong_yq 于 2015-5-5 22:55 编辑
中断串口驱动设计笔记
suyong_yq@126.com 2015-05-05
【从RTOS到串口驱动设计】 【使用中断+缓冲区解决速度同步问题】 【UART_Write】 【UART_Read】 【浅谈共享同步】
PS:在这个文档里,我以粗糙但直接的方式记录了设计中断串口驱动程序的思路,文字组织得随心所欲,一气呵成,各位看官权当成小说来读吧。
【从RTOS到串口驱动设计】之前在单片机上写样例程序的时候,经常会用到UART串口这个模块与上位机进行交互。大多数情况下,单元模块的演示程序都没有用到RTOS,程序的内容绝大多数都是顺序执行,没有太复杂的运行环境,所以在设计UART串口驱动程序的时候,一般只要实现的能够在轮询方式下发送和接收就足够用了。
比如说,我在FSL KL25平台上(FRDM-KL25Z开发板)设计了一些实用的串口驱动应用程序接口(API) typedef struct { uint32_t BusClkHz; uint32_t Baudrate; } UART_Config_T;
bool UART_ConfigTransfer(uint32_t idx, const UART_Config_T *configPtr);; void UART_PutTxDataBlocking(uint32_t idx, uint8_t txData); uint8_t UART_GetRxDataBlocking(uint32_t idx); void UART_TxBufferBlocking(uint32_t idx, uint8_t *txBufPtr, uint32_t txBufLen); void UART_RxBufferBlocking(uint32_t idx, uint8_t *rxBufPtr, uint32_t rxBufLen); |
其实现内容如下: bool UART_ConfigTransfer(uint32_t idx, const UART_Config_T *configPtr) { uint16_t sbr_val; UART_Type *uartPtr = gUartBasePtr[idx]; /* Disable the Rx and Tx. */ uartPtr ->C2 &= ~(UART_C2_TE_MASK | UART_C2_RE_MASK); /* configure uart1 for 8-bit mode , no parity */ uartPtr ->C1 = 0U; /* calculate the sbr value. */ sbr_val = (configPtr->BusClkHz >> 4)/configPtr->Baudrate; uartPtr ->BDH = (uint8_t)(((0x1F00 & sbr_val) >> 8)&UART_BDH_SBR_MASK); uartPtr ->BDL = (uint8_t)(sbr_val & UART_BDL_SBR_MASK); uartPtr ->C3 = 0U; uartPtr ->S1 = 0x1FU; uartPtr ->S2 = 0U; /* enable the tx and rx */ uartPtr ->C2 |= (UART_C2_TE_MASK | UART_C2_RE_MASK); return true; } void UART_PutTxData(uint32_t idx, uint8_t txData) { UART_Type *uartPtr = gUartBasePtr[idx]; uartPtr ->D = txData; } bool UART_IsTxBufferEmpty(uint32_t idx) { UART_Type *uartPtr = gUartBasePtr[idx]; return ( 0U != (uartPtr ->S1 & UART_S1_TDRE_MASK) ); } void UART_PutTxDataBlocking(uint32_t idx, uint8_t txData) { while (!UART_IsTxBufferEmpty(idx) ) {} UART_PutTxData(idx, txData); } uint8_t UART_GetRxData(uint32_t idx) { UART_Type *uartPtr = gUartBasePtr[idx]; return (uint8_t)(uartPtr ->D); } bool UART_IsRxBufferFull(uint32_t idx) { UART_Type *uartPtr = gUartBasePtr[idx]; return (0U != (uartPtr ->S1 & UART_S1_RDRF_MASK) ); } uint8_t UART_GetRxDataBlocking(uint32_t idx) { while (!UART_IsRxBufferFull(idx) ) {} return UART_GetRxData(idx); } void UART_TxBufferBlocking(uint32_t idx, uint8_t *txBufPtr, uint32_t txBufLen) { uint32_t i; for (i = 0U; i < txBufLen; i++) { UART_PutTxDataBlocking(idx, *(txBufPtr+i)); } } void UART_RxBufferBlocking(uint32_t idx, uint8_t *rxBufPtr, uint32_t rxBufLen) { uint32_t i; for (i = 0U; i < rxBufLen; i++) { *(RxBufPtr+i) = UART_GetRxDataBlocking(idx); } }
|
最近我打算在RTOS(FreeRTOS)环境下写一些实用的小程序。移植好系统之后,首先写了闪烁小灯的任务,直接调用之前在裸板环境下使用的GPIO驱动程序,控制小灯引脚的高低电平,啊哈,轻松搞定。然后想着要不再写一个mini的shell吧,用UART串口同上位机交互的,duang!突然傻眼了,之前的UART串口驱动程序貌似不好再用了。各位看官说了,直接调用原来的轮询串口驱动程序不是也能出字符串么?好吧,虽然现象是这样,但实际已经破坏的RTOS的多任务应用框架了,会带来很大的隐患。好吧,让我们暂时先放下程序,研究一下裸板驱动程序向RTOS环境下迁移会遇到的几个问题。
在多任务环境中运行的程序,访问任何共享资源都需要做好同步,这是我们都知道的。对于在单片机上运行的RTOS,一般不会像Linux那样设计内存保护功能,否则RTOS的设计就太复杂了,而多数RTOS本身只是一个调度器内核,尽量少地占用系统的存储与计算资源。在RTOS中运行的任务及中断程序可以在任何时候访问底层所有的外设模块寄存器,这个时候,所有的外设模块就成为了RTOS多任务环境下的共享资源,必然面临着访问共享资源的同步问题。有同学又举手发言了,那在RTOS中封装这些外设驱动程序时,在裸板驱动程序的基础上,加个锁不就解决了共享资源的同步问题了么?没错,在RTOS环境中,对共享资源加锁是有必要的,但仍有其它问题要解决。
在将裸板驱动程序迁移到RTOS环境中的另一个重要的原则就是要避免死等硬件。RTOS的用武之地在于管理CPU,控制程序的执行流程,通过调度器精确地控制每个任务执行的时间点。多任务执行的每个时间刻度都是严格设计好的,且不能拖延时间影响到后面执行的任务(如果提前完成任务,还可以运行idle任务)。这个时候,如果在某个任务中调用了轮询串口驱动程序发送一个字符串,这个任务在发送字符串完成之前就会一直卡在发送函数中,确切地说,是卡在等待底层的UART收发器发送数据到总线的过程中。UART收发器的工作速率相对于CPU来讲是非常慢的(115200的波特率同Core Clock的48MHz/120MHz差了好几个数量级),让任务一直这样等着底层硬件的工作实在是浪费系统时间。如果是等待接收数据,那就更惨了,很有可能等到地老天荒海枯石烂了,谁知道UART接收器啥时候才能收到一个数回来,整个应用程序都会被一个小小的串口卡住,这对与RTOS应用是不能接受的。按照RTOS的行事风格,如果任务要等某个事件,调度器会暂时让这个任务睡下去,把CPU分给其它就绪任务运行,等事件到来之后,才有可能唤醒任务继续执行。
这个时候我们想到了使用中断,中断机制生来就是为了解决外设和CPU工作速度不匹配的问题的。但一般情况下,中断响应的都是单个的事件,如何把中断应用到连续发送一串字符,并且封装成驱动程序呢?这可是个精细活,且听我细细道来。
目前为止,我们总结裸板UART驱动程序(轮询方式)向RTOS环境下迁移的两个问题: · 共享同步。在RTOS中使用共享资源需要增加同步量控制 · 速度同步。在CPU与UART外设模块间需要解决工作协调的问题
同步!都是同步!要在RTOS中设计并使用驱动程序,就必须解决好这两个同步问题。
从实用角度上看,速度同步的问题更普遍,即使在裸板应用中也会遇到,我们已经知道可以用中断方式解决,但要封装成驱动,还需要在设计上多斟酌一下。从最开始的分析中可知,只要在实现功能的基础上加锁,共享同步的问题也就迎刃而解,但前提是要先搞定速度同步这个涉及到功能实现的问题。OK,我们就先解决速度同步的问题,然后各个击破!
【使用中断解决速度同步的问题】为了解决串口驱动程序在等待串口收发引擎在物理层上处理信号传递时占住CPU不放的问题(轮询标志位),我知道,中断机制能帮上大忙。可是如何使用串口中断呢?
这里我描述了一个不当使用串口中断的反面例子。在目前流传的大部分开发板例子程序中,关于串口中断的演示,基本上都使用了相同的应用模型,即:配置好串口接收中断后,当触发接收中断时,调用接收的API(UART_GetRxDataBlocking())接收一个字节,再调用轮询发送的API(UART_TxBufferBlocking())把收到的字节回发出去。
void UART0_IRQHandler(void) { uint8_t dat; if ( UART0_IsRxBufferFull() ) { dat = UART0_GetRxData(); UART0_PutTxDataBlocking(dat); } } |
仔细想一下,这个demo程序的意义不大,除了能够说明如何使用中断之外,跟串口中断接收的功能应用没有什么关联,如果要使用串口中断接收一串字符之后再处理,并且要封装成实用的API,切实地把CPU的资源分配给执行运算逻辑,需要做的工作还有很多。而若是仅仅演示中断的用法,使用Systick或是其它某个可以产生中断的定时器是更合适的选择,程序更简单,更容易说明问题。
现在我们面临的是设计中断串口驱动的问题,既然涉及到设计,那么首先搞清楚需求总是必要的。我希望使用操作系统中常用的驱动接口驱动串口中断功能,包括Open, Close, Read, Write, Ioctl等功能:Open和Close对应的是使用驱动功能的准备和收尾工作;Ioctl为用户提供可选的配置模块本身的接口,可选实现;Read和Write功能是实现数据流传输的主要API,是实现驱动设计的重点内容。那么,参考典型的应用需求,整理出API如下:
void UART_Open(void); void UART_Close(void); int UART_Write(uint8_t *buf, int len); int UART_Read(uint8_t *buf, int len); void UART_Flush(void); |
【UART_Write】对于Write的功能,既然已经抽象到API的层次,那么我希望在应用程序调用这个API时,可以不用考虑底层的UART收发器低速的发送过程。既然是写入,就是立刻、马上、分分钟搞定的事情,写入之后紧接着就可以继续做后面的事情。当然,底层的UART设备在执行具体的发送过程时,由于波特率的限制,传输速度还是比较慢的,这就需要用中断+缓冲区的方式解决速度同步问题,具体的设计思路如下: 1. 在驱动程序中准备一个发送缓冲区(mUartTxIntBuffer)用于速度同步。通过后面编程过程证明,把这个缓冲区组织成Ring Buffer(环形缓冲区)(mUartTxIntFifoHandlerStruct)是不错的选择。 2. 当调用UART_Write写数时,实际上是将数据写入到发送缓冲区中。对于应用程序而言,一旦把数写入到发送缓冲区,就算是操作完成,可以执行后面的运算逻辑了。向缓冲区中写数的过程是从内存到内存,速度很快。 3. 运行在后台的驱动中断服务程序(UARTx_ISRHandler),专门负责将发送缓冲区中的数逐个取出并填充到UART发送寄存器中,让物理设备把数据发送到总线上。发送数据到总线上的周期相对较长,但由于是在后台通过中断完成的,应用程序几乎不会受到影响。 4. 发送缓冲区溢出的情况。若是调用UART_Write向缓冲区中写数时,发送缓冲区的空间很充裕,那么直接写入数据就可以进行后续逻辑了。但若是发送缓冲区空间不足了(此时缓冲区中的数还没有发完),这个时候我定义的执行内容是等待,在没有使用RTOS的情况下,UART_Write就死等环形缓冲区的空位,一旦空出来就向里填充,直到填充完所有要写入的数据为止。设定较长发送缓冲区的长度会改善溢出的情况,但会占用更多的静态内存,可根据实际情况进行调整。 5. 触发发送过程。KL25的UART提供了一个事件可以用于触发发送过程——发送寄存器空标志位(UART_S1[TDRE])及相应的中断源,一旦发送寄存器中的数据被发送出去之后,就触发中断,除非再用一个数据填充到发送寄存器中,填饱它的肚子。 · 在最开始的时候,缓冲区是空的,但是发送中断也是要关闭的,因此此时没有任何数据可以喂给发送寄存器。 · 在发送缓冲区为空时填充数据后,需要人工打开发送中断,触发发送过程。 · 发送中断服务程序在取出发送缓冲区的最后一个数后(此时发送缓冲区为空),关闭发送中断。 6. 清空发送缓冲区。在某些情况下,一定要等到发送缓冲区中的数据全部发送完成才能执行后面的操作,OK。为此需要专门设计一个API——UART_Flush(),在这个函数里不需要做什么实质性的操作,但一定等到发送缓冲区的数据被发送中断服务程序全部发完才离开。
如此以来,发送过程的设计框图如下:
实现代码如下:
#define UART_INT_TX_BUFFER_LEN (32U)
uint8_t mUartTxIntBuffer[UART_INT_TX_BUFFER_LEN]; volatile RBUF_StructHandler_T mUartTxIntFifoHandlerStruct; volatile bool mUartTxFifoIsInProcess;
/* return the count of sending data successfully. */ int UART_Write(uint8_t *buf, int len) { int txIdx = 0U; if (mUartTxFifoIsInProcess) { while (txIdx < len) { /* Wait the buffer to be available. */ while (RBUF_IsFull((RBUF_StructHandler_T *)&mUartTxIntFifoHandlerStruct)); RBUF_PutDataIn((RBUF_StructHandler_T *)&mUartTxIntFifoHandlerStruct, *(buf+txIdx)); txIdx++; } } else /* The Tx fifo is empty and no tx ISR would happen, need to trigger the tx ISR. */ { while ( (txIdx < len) && (!RBUF_IsFull((RBUF_StructHandler_T *)&mUartTxIntFifoHandlerStruct)) ) { /* Wait the buffer to be available. */ //while (RBUF_IsFull((RBUF_StructHandler_T *)&mUartTxIntFifoHandlerStruct)); RBUF_PutDataIn((RBUF_StructHandler_T *)&mUartTxIntFifoHandlerStruct, *(buf+txIdx)); txIdx++; } /* Get one data to trigger the ISR. */ mUartTxFifoIsInProcess = true; /* Mark the transfer. */ UART_EnableTxBufferEmptyInt(DEMO_UART_IDX, true);
/* Fill the rest data or just return. */ while (txIdx < len) { /* Wait the buffer to be available. */ while (RBUF_IsFull((RBUF_StructHandler_T *)&mUartTxIntFifoHandlerStruct)); RBUF_PutDataIn((RBUF_StructHandler_T *)&mUartTxIntFifoHandlerStruct, *(buf+txIdx)); txIdx++; } } return txIdx; }
void UART_Flush(void) { /* Wait until all the data in tx buffer is send by ISR. */ while (mUartTxFifoIsInProcess) {}; }
void UART2_IRQHandler(void) { /* Tx process. */ if (mUartTxFifoIsInProcess && UART_IsTxBufferEmpty(2U)) { if (RBUF_IsEmpty((RBUF_StructHandler_T *)&mUartTxIntFifoHandlerStruct)) { mUartTxFifoIsInProcess = false; UART_EnableTxBufferEmptyInt(2U, false); } else { UART_PutTxData(2U, RBUF_GetDataOut((RBUF_StructHandler_T *)&mUartTxIntFifoHandlerStruct) ); } } //... }
|
【UART_Read】对于Read的功能同发送过程的实现类似。应用程序在调用UART_Read读数的时候,也应该是不考虑底层UART收发器的低速的接收过程,立刻、马上、分分钟就要拿到数据,只有当数据还没有准备好的时候(有可能压根就没有数据传过来),才会等待数据。为了同步在应用程序中高速地读取过程和UART收发器相对低速地接收数据的问题,这也需要用中断+缓冲区的方式解决,具体的设计思路如下: 1. 在驱动程序中准备一个接收缓冲区(mUartRxIntBuffer)用于速度同步。同发送缓冲区类似,把这个缓冲区组织成Ring Buffer(mUartRxIntFifoHandlerStruct)。 2. 当调用UART_Read读数时,实际上是将数据从接收缓冲区中读出来。从缓冲区中写数的过程是从内存到内存,速度很快。 3. 运行在后台的驱动中断服务程序(UARTx_ISRHandler),专门负责向接收缓冲区中逐个填入从UART接收寄存器(UART_D)的数,让物理设备把数据从总线上取下来。从总线上取数的周期相对较长,但由于是在后台通过中断完成的,应用程序几乎不会受到影响。 4. 接收缓冲区溢出的情况。接收中断程序在搬运数据的过程中,若是接收缓冲区中还有空位,则顺利填入数据,若是已经满了,我实在没有别的好办法处理多数来的数据,为了保证已经接收数据的完整性,只能舍弃掉溢出的数据。如此,接收缓冲区长度的安排就很重要的,同发送缓冲期类似,较长的缓冲区可以改善溢出的情况,但会占用更多的静态内存。另外,接收的数据是跟应用程序相关的,在应用程序中也尽块消耗掉缓冲区中的数据。 5. 触发接收过程。呵呵,这里同发送过程就不一样了,但同所有的从机编程模型类似,数据的接收方永远不知道发送方什么时候会发数据,因此接收中断随时处于接收待命的状态,监听信道。当然,这里不需要把CPU浪费在轮询过程中,中断事件可以帮忙搞定。
接收过程的设计框图如下:
实现代码如下: /* Rx Ring Buffer */ #define UART_INT_RX_BUFFER_LEN (32U) uint8_t mUartRxIntBuffer[UART_INT_RX_BUFFER_LEN]; volatile RBUF_StructHandler_T mUartRxIntFifoHandlerStruct;
/* return the count of receiving data successfully. * blocking mode. */ int UART_Read(uint8_t *buf, int len) { int rxIdx = 0; while (rxIdx < len) { while (RBUF_IsEmpty((RBUF_StructHandler_T *)&mUartRxIntFifoHandlerStruct)); *(buf+rxIdx) = RBUF_GetDataOut((RBUF_StructHandler_T *)&mUartRxIntFifoHandlerStruct); rxIdx++; } return rxIdx; }
void UART2_IRQHandler(void) { //... /* Rx process. */ if (UART_IsRxBufferFull(2U)) { /* Read the data from rx register and fill it into rx fifo. */ if (RBUF_IsFull((RBUF_StructHandler_T *)&mUartRxIntFifoHandlerStruct)) { /* ignore the data if the rx fifo is full. */ UART_GetRxData(2U); } else { RBUF_PutDataIn((RBUF_StructHandler_T *)&mUartRxIntFifoHandlerStruct, UART_GetRxData(2U)); } } }
|
【浅谈共享同步】在前面通过分别使用发送缓冲区和接收缓冲区,解决了速度同步的问题,这已经能够满足绝大多数应用场景的需求了。即使是在RTOS应用中,如果木有打算像高级操作系统那样将设备的使用权绑定到任务上,中断方式的串口驱动程序也足够胜任通信的任务了。不是么,RTOS中也是允许使用中断的,并且避免了CPU死等底层传输标志位的情况。实际上,做到这一步,我就已经打算在FreeRTOS使用了。
然而,这里仍然有个隐患,即使现在我感冒头痛流鼻涕都快睁不开眼睛了,我也要坚持把这一点隐患暴露在光天化日之下。我们知道,在RTOS的多任务环境下,除非是任务本身的超循环,否则在正常执行逻辑中不能出现用while()等某个标志的情况。一旦要等,那么就必然要使用RTOS自带的事件同步组件,例如事件、信号量、锁等,这样可以在等待的时候把CPU让出来执行其它的任务。再来检查一下已经列写出来的代码清单,使用while()等待的地方还有很多,若是要跟RTOS密切地结合起来,这些while()等待的程序都要相应地替换成使用RTOS中的同步组件进行等待,这就是关于RTOS中共享同步的问题。当然,我已经把问题描述出来了,就是这个while()等待,要在RTOS中替换起来却是很麻烦的事情(但并不是思路不清晰)。若是要很好地解决驱动程序共享同步的问题,就需要使用“任务队列”这个高级玩意。在RTOS中,一个串口设备被多个任务共享使用是一个合理的需求(例如映射到printf上的串口通道),那么当某个任务通过同步组件等在这个驱动上时,很有可能前面有已经有多个任务已经在等了,那么就必须使用任务队列把所有等待的任务组织起来。另外,在唤醒任务队列中的任务时也有很多讲究,搞不好会出现“互锁”的情况。。。
一些功能强大的RTOS提供了完善的同步事件及“任务队列”等组件,但这些RTOS对系统硬件资源的需求也比较大,多数也是收费的,不够“接地气”。而轻量级的RTOS(FreeRTOS、uCOS-X)在广大的吊丝电子工程师中更受欢迎,那么仅使用解决了速度同步的中断+缓冲区的串口驱动就已经能够满足需求了,如果担心在写缓冲区的过程中被高优先级的任务打断,那么只要用临界区把操作缓冲区的过程保护起来,就完全没有问题。另外,若是确实需要多任务同步共享驱动的情况,也可以简化问题,稍微修改一下串口驱动为非阻塞式的,然后把对同步组件的调用放到应用任务中,也可以解决问题。
即使思路明确,我仍然不想继续这样折腾这个串口驱动,因为一个同时能在裸板和RTOS环境下工作正常的驱动程序可以让我节约很多开发时间,我希望把更多精力和时间用来创造更多有意思的东西。
好了,现在我可以考虑在FreeRTOS上用这个NB的串口驱动搞点什么好玩的设计了。
- End
我发现直接贴到帖子里的格式有点乱,提供PDF格式的文档便于大家阅读,另外还有可在FRDM-KL25板上运行的代码包供下载: |