摘要:串口是通信中最常用的通信方式,可能写串口的驱动,能写几十种方法, 查询方式,中断方式,DMA方式,定时器方式。可能也其中几种方式的组合形式,经典的用法是:发送用查询方式,接收用中断方式,或者DMA+空闲中断。本篇不讲串口是啥,现在还在讲串口是啥,估计会被喷。今天来聊一聊串口常用的几种方式,最简单的方法就不说了。
队列这个词在数据局结构中出现的比较多,与之对应的就是堆栈,但是两者的读取方式又完全不同。
FIFO 是First-In First-Out的缩写,它是一个具有先入先出特点的缓冲区。串口设计FIFO的目的是为了提高串口的通讯性能。如果没有FIFO或者说缓冲区的长度只有1字节,那么使用接收中断,就意味着每次收到一个字节的数据就要进一次中断,这样频繁进中断会占用CPU资源。另外如果没有及时读走数据,那么下一个字节数据就会覆盖之前的数据,导致数据丢失,这在通讯速率高的场合很有可能出现。
使用FIFO,可以在连续接收若干个数据后才产生一次中断,然后一起进行处理。这样可以提高接收效率,避免频繁进中断,适用于大数据传输。你可能会想到如果FIFO中的数据没有达到指定长度而无法产生中断怎么办,通常MCU会有接收超时中断,即在一定的时间内没有接收到数据会进入中断,可以利用这个中断把不足FIFO长度的数据最后都读取完。
FIFO类似售票排队窗口,先到的人看到能先买到票,然后先走,后来的人只能后买到票。
在计算机中,每个信息都是存储在存储单元中的,当有大量数据的时候,我们不能存储所有的数据,那么计算机处理数据的时候,只能先处理先来的,那么处理完后呢,就会把数据释放掉,再处理下一个。那么,已经处理的数据的内存就会被浪费掉。因为后来的数据只能往后排队,如过要将剩余的数据都往前移动一次,那么效率就会低下了,肯定不现实,所以,环形队列就出现了。
点击下方视频动态演示出队入队
在计算机中,是没有环形的内存的,只不过是我们将顺序的内存处理过,让某一段内存形成环形,使他们首尾相连,简单来说,这其实就是一个数组,只不过有两个指针,一个指向列队头,一个指向列队尾。指向列队头的指针是缓冲区可读的数据,指向列队尾的指针是缓冲区可写的数据,通过移动这两个指针即可对缓冲区的数据进行读写操作了,直到缓冲区已满(头尾相接),将数据处理完,可以释放掉数据,又可以进行存储新的数据了。
实现的原理:
视频来自正在一名考研的UP主:秃头少女王某人。计算机专业考研这个是必考点,视频讲的很棒,祝她一战成硕,金榜题名!
串口环形缓冲区收发:在初学单片机的时候我们知道的串口收发都是:接收一个数据,触发中断,然后把数据发回来。这种处理方式是没有缓冲的,当数量太大的时候,亦或者当数据接收太快的时候,我们来不及处理已经收到的数据,那么,当再次收到数据的时候,就会将之前还未处理的数据覆盖掉。那么就会出现丢包的现象了,对我们的程序是一个致命的创伤。
那么如何避免这种情况的发生呢,很显然,上面说的一些队列的特性很容易帮我们实现我们需要的情况。将接受的数据缓存一下,让处理的速度有些许缓冲,使得处理的速度赶得上接收的速度,上面又已经分析了普通队列与环形队列的优劣了,那么我们肯定是用环形队列来进行实现了。下面就是代码的实现:
typedef struct{ uint16_t usWrite; uint16_t usRead; uint16_t usLenght; /* FIFO 结构 */ uint8_t ucRing_Buff[RINGBUFF_LEN];}RingBuff_T;extern RingBuff_T g_ringBuff;
初始化结构体相关信息:使得我们的环形缓冲区是头尾相连的,并且里面没有数据,也就是空的队列,所有元素清0。
void RingBuff_Init(void){ g_ringBuff.usWrite = 0; g_ringBuff.usRead = 0; g_ringBuff.usLenght = 0;}
/*** @brief Write_RingBuff* @param uint8_t _ucWriteData* @return 0:环形缓冲区已满,写入失败;1:写入成功* @note 往环形缓冲区写入uint8_t类型的数据*/uint8_t Write_RingBuff(uint8_t _ucWriteData){ if(g_ringBuff.usLenght >= RINGBUFF_LEN) /*判断缓冲区是否已满*/ { return 0; } g_ringBuff.ucRing_Buff[g_ringBuff.usRead] = _ucWriteData; g_ringBuff.usRead = (g_ringBuff.usRead + 1) % RINGBUFF_LEN; /*防止越界非法访问*/ g_ringBuff.usLenght++; return 1;}
/*** @brief Read_RingBuff* @param u8 *rData,用于保存读取的数据* @return 0:环形缓冲区没有数据,读取失败; 1:读取成功* @note 从环形缓冲区读取一个uint8_t类型的数据*/uint8_t Read_RingBuff(uint8_t *_usReadData){ if(g_ringBuff.usLenght == 0)/*判断非空*/ { return 0; } *_usReadData = g_ringBuff.ucRing_Buff[g_ringBuff.usWrite];/*先进先出FIFO,从缓冲区头出*/ g_ringBuff.usWrite = (g_ringBuff.usWrite + 1) % RINGBUFF_LEN; /*防止越界非法访问*/ g_ringBuff.usLenght--; return 1;}
对于读写操作需要注意的地方有两个:
“”
环形缓冲器(ringr buffer),也称作圆形队列(circular queue),循环缓冲区(cyclic buffer),圆形缓冲区(circula buffer),是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流。
圆形缓冲区的一个有用特性是:当一个数据元素被用掉后,其余数据元素不需要移动其存储位置。相反,一个非圆形缓冲区(例如一个普通的队列)在用掉一个数据元素后,其余数据元素需要向前搬移。换句话说,圆形缓冲区适合实现先进先出缓冲区,而非圆形缓冲区适合后进先出缓冲区。
那么如何将环形缓冲器ringr buffer应用到串口上面呢?这里我们使用RT-Thread
的源码。
/* ring buffer */struct rt_ringbuffer{ uint8_t *buffer_ptr; uint16_t read_mirror : 1; uint16_t read_index : 15; uint16_t write_mirror : 1; uint16_t write_index : 15; uint16_t buffer_size;};
void rt_ringbuffer_init(struct rt_ringbuffer *rb, uint8_t *pool, uint16_t size){ RT_ASSERT(rb != NULL); RT_ASSERT(size > 0); /* initialize read and write index */ rb->read_mirror = rb->read_index = 0; rb->write_mirror = rb->write_index = 0; /* set buffer pool and size */ rb->buffer_ptr = pool; rb->buffer_size = RT_ALIGN_DOWN(size, RT_ALIGN_SIZE);}
unsigned long rt_ringbuffer_put(struct rt_ringbuffer *rb, const uint8_t *ptr, uint16_t length){ uint16_t size; RT_ASSERT(rb != NULL); /* whether has enough space */ size = rt_ringbuffer_space_len(rb); /* no space */ if (size == 0) return 0; /* drop some data */ if (size < length) length = size; if (rb->buffer_size - rb->write_index > length) { /* read_index - write_index = empty space */ memcpy(&rb->buffer_ptr[rb->write_index], ptr, length); /* this should not cause overflow because there is enough space for * length of data in current mirror */ rb->write_index += length; return length; } memcpy(&rb->buffer_ptr[rb->write_index], &ptr[0], rb->buffer_size - rb->write_index); memcpy(&rb->buffer_ptr[0], &ptr[rb->buffer_size - rb->write_index], length - (rb->buffer_size - rb->write_index)); /* we are going into the other side of the mirror */ rb->write_mirror = ~rb->write_mirror; rb->write_index = length - (rb->buffer_size - rb->write_index); return length;}
unsigned long rt_ringbuffer_get(struct rt_ringbuffer *rb, uint8_t *ptr, uint16_t length){ unsigned long size; RT_ASSERT(rb != NULL); /* whether has enough data */ size = rt_ringbuffer_data_len(rb); /* no data */ if (size == 0) return 0; /* less data */ if (size < length) length = size; if (rb->buffer_size - rb->read_index > length) { /* copy all of data */ memcpy(ptr, &rb->buffer_ptr[rb->read_index], length); /* this should not cause overflow because there is enough space for * length of data in current mirror */ rb->read_index += length; return length; } memcpy(&ptr[0], &rb->buffer_ptr[rb->read_index], rb->buffer_size - rb->read_index); memcpy(&ptr[rb->buffer_size - rb->read_index], &rb->buffer_ptr[0], length - (rb->buffer_size - rb->read_index)); /* we are going into the other side of the mirror */ rb->read_mirror = ~rb->read_mirror; rb->read_index = length - (rb->buffer_size - rb->read_index); return length;}
1.首先将RT-Thread的ringbuffer.c和ringbuffer.h文件加入到我们的MDK工程中去。
2.编写串口相关的底层硬件bsp代码,也就是初始化GPIO和串口相关的配置,这个就很简单,大家应该都会。在串口初始化代码中记得要手动将串口的非空中断和空闲中断打开。
UART_HandleTypeDef huart1;//这个要定义为全局的变量void uart_init(u32 bound){ huart1.Instance=USART1; //USART1 huart1.Init.BaudRate=bound; //波特率 huart1.Init.WordLength=UART_WORDLENGTH_8B; //字长为8位数据格式 huart1.Init.StopBits=UART_STOPBITS_1; //一个停止位 huart1.Init.Parity=UART_PARITY_NONE; //无奇偶校验位 huart1.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控 huart1.Init.Mode=UART_MODE_TX_RX; //收发模式 HAL_UART_Init(&huart1);//HAL_UART_Init()会使能UART1 __HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);/*使能串口接收非空中断 */ __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE); /*使能串口接收空闲中断 */ }void HAL_UART_MspInit(UART_HandleTypeDef *huart){ //GPIO端口设置 GPIO_InitTypeDef GPIO_InitStruct; if(huart->Instance==USART1)//如果是串口1,进行串口1 MSP初始化 { __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); //使用中断肯定要配置NVIC HAL_NVIC_SetPriority(USART1_IRQn, 1, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); }}
3.定义一个结构头rt_ringbuffer 类型的变量ring_buf,变量名随便取,阿猫阿狗都可以,只要你自己认得就行。
struct rt_ringbuffer ring_buf;
4.定义一个串口接收缓冲区数组,数组名随便取,阿猫阿狗都可以,只要你自己认得就行。
static uint8_t s_USART1_RxBuf[256];
5.初始化ringbuffer
rt_ringbuffer_init(&ring_buf,s_USART1_RxBuf,256);
位置放在哪里都可以,我这里就放在串口串口初始化之前了。
6.编写中断服务函数
void USART1_IRQHandler(void) { uint8_t receive_char; if((__HAL_USART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)) //接收非空中断 { HAL_UART_Receive(&huart1, &receive_char, 1, 1000);//将收到的数据放入receive_char rt_ringbuffer_put(&ring_buf,&receive_char,1);//将receive_char压入到ring_buf中 USART1_Len++; __HAL_USART_CLEAR_FLAG(&huart1,UART_FLAG_RXNE);//清除接收非空中断 } if((__HAL_USART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)) //接收空闲中断 { rt_ringbuffer_get(&ring_buf,s_USART1_RxBuf, USART1_Len);//将rring_buf的数据读出到到s_USART1_RxBuf中 s_USART1_RxBuf[USART1_Len] = '\0'; printf("%s\r\n",s_USART1_RxBuf); USART1_Len = 0 ;//数据帧长度复位 __HAL_USART_CLEAR_IDLEFLAG(&huart1); }}
这里面的代码我写的应该很简单了,首先我们在初始化中是能了接收中断和空闲中断,那么如果有数据过来,就会触发中断。进入中断服务函数,进来之后首先判断接收中断标志位是否置位为1,如果是1说明收数据来了,通过HAL_UART_Receive
函数将数据存入receive_char
中,再通过rt_ringbuffer_put
将数据压入队列之中。如果数据接收完了就会触发空闲中断,这时通过rt_ringbuffer_get
函数将数据读出到我们定义的数组中打印出来。
7.主函数
int main(void){ HAL_Init(); Stm32_Clock_Init(); delay_init(72); uart_init(115200); while(1) { }}
主函数啥都不要写,完了。
队列 (Queue):是一种先进先出(First In First Out ,简称 FIFO)的线性表,只允许在一端插入,在另一端进行删除。
FIFO一般用于不同时钟域之间的数据传输,比如FIFO的一端是AD数据采集,另一端是计算机的PCI总线,假设其AD采集的速率为16位 100K SPS,那么每秒的数据量为100K×16bit=1.6Mbps,而PCI总线的速度为33MHz,总线宽度32bit,其最大传输速率为1056Mbps,在两个不同的时钟域间就可以采用FIFO来作为数据缓冲。另外对于不同宽度的数据接口也可以用FIFO,例如单片机位8位数据输出,而DSP可能是16位数据输入,在单片机与DSP连接时就可以使用FIFO来达到数据匹配的目的。
/* 串口设备结构体 */typedef struct{ USART_TypeDef *uart; /* STM32内部串口设备指针 */ uint8_t *pTxBuf; /* 发送缓冲区 */ uint8_t *pRxBuf; /* 接收缓冲区 */ uint16_t usTxBufSize; /* 发送缓冲区大小 */ uint16_t usRxBufSize; /* 接收缓冲区大小 */ __IO uint16_t usTxWrite; /* 发送缓冲区写指针 */ __IO uint16_t usTxRead; /* 发送缓冲区读指针 */ __IO uint16_t usTxCount; /* 等待发送的数据个数 */ __IO uint16_t usRxWrite; /* 接收缓冲区写指针 */ __IO uint16_t usRxRead; /* 接收缓冲区读指针 */ __IO uint16_t usRxCount; /* 还未读取的新数据个数 */}UART_T;
static void Uart_FIFO_Init(void){ g_tUart1.uart = USART1; /* STM32 串口设备 */ g_tUart1.pTxBuf = g_TxBuf1; /* 发送缓冲区指针 */ g_tUart1.pRxBuf = g_RxBuf1; /* 接收缓冲区指针 */ g_tUart1.usTxBufSize = 1024; /* 发送缓冲区大小 */ g_tUart1.usRxBufSize = 1024; /* 接收缓冲区大小 */ g_tUart1.usTxWrite = 0; /* 发送FIFO写索引 */ g_tUart1.usTxRead = 0; /* 发送FIFO读索引 */ g_tUart1.usRxWrite = 0; /* 接收FIFO写索引 */ g_tUart1.usRxRead = 0; /* 接收FIFO读索引 */ g_tUart1.usRxCount = 0; /* 接收到的新数据个数*/ g_tUart1.usTxCount = 0; /* 待发送的数据个数 */}
UART_HandleTypeDef UartHandle; static void Uart_GPIO_Config(void){ GPIO_InitTypeDef GPIO_InitStruct; /* 使能 GPIO TX/RX 时钟 */ USART1_TX_GPIO_CLK_ENABLE(); USART1_RX_GPIO_CLK_ENABLE(); /* 使能 USARTx 时钟 */ USART1_CLK_ENABLE(); /* 配置TX引脚 */ GPIO_InitStruct.Pin = USART1_TX_PIN; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(USART1_TX_GPIO_PORT, &GPIO_InitStruct); /* 配置RX引脚 */ GPIO_InitStruct.Pin = USART1_RX_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(USART1_RX_GPIO_PORT, &GPIO_InitStruct); UartHandle.Instance = USART1; UartHandle.Init.BaudRate = 115200; UartHandle.Init.WordLength = UART_WORDLENGTH_8B; UartHandle.Init.StopBits = UART_STOPBITS_1; UartHandle.Init.Parity = UART_PARITY_NONE; UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE; UartHandle.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&UartHandle); __HAL_USART_CLEAR_FLAG(&UartHandle, USART_FLAG_TC); /* 清除TC发送完成标志 */ __HAL_USART_CLEAR_FLAG(&UartHandle, USART_FLAG_RXNE);/* 清除RXNE接收标志 */ __HAL_USART_ENABLE_IT(&UartHandle, USART_IT_RXNE); /* 使能接收数据寄存器非空中断 */ /* 配置NVIC the NVIC for UART */ HAL_NVIC_SetPriority(USART1_IRQn, 0, 1); HAL_NVIC_EnableIRQ(USART1_IRQn); }
什么时候要将数据压入FIFO?当然是在向串口发送数据的时候将数据压入FIFO去啦,这里面的逻辑也很简单就是将要发送的数据压入FIFO缓冲区,之后打开发送中断就可以了。
void UartSendBuf(uint8_t *_ucaBuf, uint16_t _usLen){ uint16_t i; for (i = 0; i < _usLen; i++) { /* 如果发送缓冲区已经满了,则等待缓冲区空 */ while (1) { __IO uint16_t usCount; usCount = g_tUart1.usTxCount; if (usCount < g_tUart1.usTxBufSize) { break;/*如果发送缓冲区没有满就跳出去,让发送缓冲区填满*/ } else if(usCount == g_tUart1.usTxBufSize)/* 数据已填满缓冲区 */ { if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_TXE)== RESET) { __HAL_UART_ENABLE_IT(&UartHandle,UART_IT_TXE); /* 使能发送中断(缓冲区空) */ } } } /* 将新数据填入发送缓冲区 */ g_tUart1.pTxBuf[g_tUart1.usTxWrite] = _ucaBuf[i]; DISABLE_INT(); if (++g_tUart1.usTxWrite >= g_tUart1.usTxBufSize) { g_tUart1.usTxWrite = 0; } g_tUart1.usTxCount++; ENABLE_INT(); } __HAL_UART_ENABLE_IT(&UartHandle,UART_IT_TXE); /* 使能发送中断(缓冲区空) */}
如果要发送的数据没有超过发送缓冲区大小,实现起来还比较容易,直接把数据填到 FIFO 里面,并使能发送空中断即可。如果超过了 FIFO 大小,就需要等待有空间可用,针对这种情况有个重要的知识点,就是当缓冲刚刚填满的时候要判断发送空中断是否开启了,如果填满了还没有开启,就会卡死在 while 循环中,所以多了一个刚填满时的判断,填满了还没有开启发送空中断,要开启下。
什么时候要从FIFO中读出?当然是在从串口获取数据的时候将从FIFO中读数据啦。
uint8_t UartGetChar(uint8_t *_pByte){ uint16_t usCount; /* usRxWrite 变量在中断函数中被改写,主程序读取该变量时,必须进行临界区保护 */ DISABLE_INT(); usCount = g_tUart1.usRxCount; ENABLE_INT(); /* 如果读和写索引相同,则返回0 */ if (usCount == 0) /* 已经没有数据 */ { return 0; } else { *_pByte = g_tUart1.pRxBuf[g_tUart1.usRxRead];/* 从串口接收FIFO取1个数据 */ /* 改写FIFO读索引 */ DISABLE_INT(); if (++g_tUart1.usRxRead >=g_tUart1.usRxBufSize) { g_tUart1.usRxRead = 0; } g_tUart1.usRxCount--; ENABLE_INT(); return 1; }}
void USART1_IRQHandler(void){ /* 处理接收缓冲区中断 */ if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_RXNE)!= RESET) { /* 从串口接收数据寄存器读取数据存放到接收FIFO */ uint8_t ch; ch = READ_REG(g_tUart1.uart->DR); g_tUart1.pRxBuf[g_tUart1.usRxWrite] = ch; if (++g_tUart1.usRxWrite >= g_tUart1.usRxBufSize) { g_tUart1.usRxWrite = 0; } if (g_tUart1.usRxCount < g_tUart1.usRxBufSize) { g_tUart1.usRxCount++; } } /* 处理发送缓冲区空中断 */ if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_TXE)!= RESET) { if (g_tUart1.usTxCount == 0) { /* 发送缓冲区的数据已取完时, 禁止发送缓冲区空中断 (注意:此时最后1个数据还未真正发送完毕)*/ CLEAR_BIT(g_tUart1.uart->CR1, USART_CR1_TXEIE); /* 使能数据发送完毕中断 */ __HAL_UART_ENABLE_IT(&UartHandle,UART_IT_TC);/* 使能数据发送完毕中断 */ } else { /* 从发送FIFO取1个字节写入串口发送数据寄存器 */ g_tUart1.uart->DR = g_tUart1.pTxBuf[g_tUart1.usTxRead]; if (++g_tUart1.usTxRead >= g_tUart1.usTxBufSize) { g_tUart1.usTxRead = 0; } g_tUart1.usTxCount--; } } /* 数据bit位全部发送完毕的中断 */ if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_TC)!= RESET) { if (g_tUart1.usTxCount == 0) { /* 如果发送FIFO的数据全部发送完毕,禁止数据发送完毕中断 */ __HAL_USART_CLEAR_FLAG(&UartHandle,USART_FLAG_TC);/*发送完毕中断*/ }}
const char buf1[] = "接收到串口命令1\r\n";const char buf2[] = "接收到串口命令2\r\n";const char buf3[] = "接收到串口命令3\r\n";const char buf4[] = "接收到串口命令4\r\n";int main(void){ uint8_t read[100] = {0} ; bsp_Init(); while(1) { /* 接收到的串口命令处理 */ if (UartGetChar(read)!=0) { UartSendBuf((uint8_t *)read, sizeof(read)/sizeof(uint8_t)); switch (read[0]) { case '1': UartSendBuf((uint8_t *)buf1, strlen(buf1)); break; case '2': UartSendBuf((uint8_t *)buf2, strlen(buf2)); break; case '3': UartSendBuf((uint8_t *)buf3, strlen(buf3)); break; case '4': UartSendBuf((uint8_t *)buf4, strlen(buf4)); break; default: break; } } }}
串口扫盲就是几个状态标志位不好理解,只要理解好这张图就好办了,其他的请参考相关的教程。
2.USART_FLAG_TC当发送移位寄存器里的每个字节通过TX脚一位一位发送出去之后,该标志位值就会被置1。通过这个标志位的值可以判断发送移位寄存器里的数据有没有被全部发送出去;
3.USART_FLAG_TXE和USART_FLAG_TC之间的联系 结合上面流程图来进行说明,实际上发送移位寄存器通过TX脚发送数据这个过程是比较耗时的,所以在此过程进行时,可通过判断当USART_FLAG_TXE = 1,即发送数据寄存器里的数据已被全部转入发送移位寄存器时,就让CPU往发送数据寄存器转入新的数据。当发送移位寄存器把数据帧全部发送出去之后,可通过判断USART_FLAG_TC = 1,证明数据帧的最后一个字节都已经通过TX脚发送完了。
免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删