为什么要用环形缓冲区
假设有这样的场景:串口中断正在快速读取数据,主循环中较慢地解析数据。如果保存串口当前发送的数据后立即做处理,可能会有丢帧的风险。如果我们使用先进先出的数据结构——环形缓冲区,把串口存取的数据存进去,主循环可随时读取,既可以规避掉丢帧的风险,也确保了数据次序正确。
环形缓冲区的实现
需要一个缓存数组,它是固定大小的,来作为环形缓冲区。为了正确的读取数据,我们还需要写指针和读指针。指针的移动,通过_next()函数中指针+1取模实现,确保数组不会越界。
// 1. 缓存数组:固定大小的存储空间(比如256字节)
static volatile uint8_t s_rxbuf[IMU_UART_RX_BUF_SIZE];
// 2. 写指针:下一个要写入的位置
static volatile uint16_t s_wr = 0;
// 3. 读指针:下一个要读取的位置
static volatile uint16_t s_rd = 0;
// 4. 计算下一个位置(核心:取模实现“环形”)
static inline uint16_t _next(uint16_t idx)
{
return (uint16_t)((idx + 1u) % IMU_UART_RX_BUF_SIZE);
}
环形缓冲区的使用
回到我们一开始的场景,也就是我们决定使用环形缓冲区的起点,我们遭遇的是串口中断和主循环之间的矛盾。
我们应该在中断中使用写指针写数据。如下_push()函数:计算下一个写入位置,把字节写入写指针指向的环形缓冲区位置。当下一个写位置与读位置相等时,说明缓冲区满了,将读位置后移一位,即丢弃最旧的一个字节。最后把当前写指针后移。
- 写入数据 _push() :中断里调用
static inline void _push(uint8_t b)
{
// 计算下一个写入位置
uint16_t next = _next(s_wr);
// 如果【下一个写位置 == 读位置】= 缓冲区满了
if (next == s_rd) {
s_rd = _next(s_rd); // 丢弃最旧的一个字节
}
s_rxbuf[s_wr] = b; // 把字节写入缓存
s_wr = next; // 写指针后移
}
同样地,在主循环解析数据时需要用到读指针读取数据。如下_pop()函数:当写指针和读指针指向同一个位置时,说明没有待读数据,返回-1;如果有待读数据,则读出数据,读指针后移。
2. 读取数据 _pop() : 主循环解析调用
static inline int _pop(uint8_t *out)
{
if (s_wr == s_rd) return -1; // 空的,没数据可读
*out = s_rxbuf[s_rd]; // 读出数据
s_rd = _next(s_rd); // 读指针后移
return 0;
}
- 状态判断总结
缓冲区空(没数据)-》s_wr == s_rd
缓冲区满(存不下)-》 _next(s_wr) == s_rd
浙公网安备 33010602011771号