ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

【单片机】利用双缓冲机制保护全局变量不受中断影响以及环形队列

【单片机】利用双缓冲机制保护全局变量不受中断影响以及环形队列 volatile uint8_t buffer_index 0; uint32_t buffer[2] {0}; void main() { while (1) { uint8_t current_index buffer_index; process_data(buffer[current_index 0x01]); // 读非活跃缓冲区 } } void ISR() { buffer_index ^ 1; // 切换缓冲区 buffer[buffer_index 0x01] read_sensor(); // 写新缓冲区 }以上方法只用于简单的主循环读、中断写的情况且读的过程中只能有1次中断写如果复杂点可以采用循环队列或者读写锁中断不能阻塞之类如果主循环和中断同时读或者同时写是不能用循环队列的会冲突。volatile uint8_t buffer_index 0; uint32_t buffer[2] {0}; void main() { while (1) { uint8_t current_index buffer_index ^ 1; buffer[current_index 0x01] read_sensor(); // 写新缓冲区 buffer_index ^ 1; // 切换缓冲区 } } void ISR() { process_data(buffer[buffer_index 0x01]); // 读非活跃缓冲区 }以上方法只用于简单的主循环写、中断读的情况如果复杂点可以采用循环队列或者临界区之类如果主循环和中断同时读或者同时写是不能用循环队列的会冲突。想要无锁特性有一个大前提——SPSC单生产者 - 单消费者。在ARM处理器中绝大多数指令都可以被中断打断某些特殊指令如异常返回指令不会被中断打断中断会延迟处理可以临时禁用中断确保关键代码段不会被中断打断使用#include stdatomic.h补充在全局变量小于等于数据总线大小时对变量的读/写是原子操作。两个线程的一读一写或同时写没问题不会有高优先级的操作被覆盖的情况。但类似“a”属于“读-改-写”是非原子操作会被中断打断。两个线程的一读一写没问题但同时写会有高优先级写结果被覆盖丢失的情况。全局变量大于数据总线大小时属于非原子操作两个线程的一读一写可以用双缓冲保护同时写只能用锁处理。https://www.kdocs.cn/l/cjss6awiCP49补充环形队列是通过牺牲一个存储单元来区分队满和队空。一定是先改内容再改指针和冻结数据先写flash再改EE一样。如果先改指针就会出现写的内容还没完成就被读走了。最小代码示例/******************************************************************************* ** 函数名称 ** 功 能顺序队操作函数 ** 修改日志 *******************************************************************************/ void initUartQueue(TYPE_UART_QUEUE *qu) { qu-front qu-rear 0; } //对空回1队不空回0 int isUartQueueEmpty(TYPE_UART_QUEUE *qu) { if (qu-front qu-rear) { return 1; } else return 0; } //对满回1队不满回0 int isUartQueueFull(TYPE_UART_QUEUE *qu) { if ((qu-rear 1) % sample_maxsize qu-front) { return 1; } else return 0; } //往队列里写数据成功回1失败回0 int enUartQueue(TYPE_UART_QUEUE *qu, unsigned char *x) { if ((qu-rear 1) % sample_maxsize qu-front) return 0; qu-data[qu-rear] *x; //先改存储单元 qu-rear (qu-rear 1) % sample_maxsize; //后改读写指针 return 1; } //从队列里读数据成功回1失败回0 int deUartQueue(TYPE_UART_QUEUE *qu, unsigned char *x) { if (qu-front qu-rear) return 0; *x qu-data[qu-front]; //先改存储单元 qu-front (qu-front 1) % sample_maxsize; //后改读写指针 return 1; }取模算法提速补充// 当 SIZE 是 2 的幂次时 x % SIZE x (SIZE - 1)第二种提速方式就是比大小https://www.kdocs.cn/l/cvs9putAw8Cs节约存储补充设定一个回绕点回绕点是元素数量的倍数2倍、3倍。。队头队尾一直加直到回绕点清零这样通过头尾差值判断队满还是队空差值最大不超过元素总数。最小代码里面回绕点等于元素数量所以需要牺牲一个存储单元。元素数量属于小模定位存储位置。回绕点属于大模判断队满队空队头队尾的计算。如果SIZE是2的幂次时队头队尾变量的天然的回绕点是SIZE的整倍数无需特殊处理只需要即可这种一定是队头队尾变量可以达到2^32如果是24位定时器算超时时间是绝对不能省模的。优化后的代码kfifo示例#includehttps://github.com/DNedic/lfbbstdint.h // 环形FIFO队列结构体缓冲区解耦32位指针自然回绕适配UART数据缓存 typedef struct { unsigned char *data; // 数据缓冲区指针外部独立分配需为2的幂次大小 uint32_t front; // 队头指针指向待读取数据的位置 uint32_t rear; // 队尾指针指向待写入数据的位置 uint32_t size; // 缓冲区有效大小必须为2的幂次如16/32/64/128 } TYPE_UART_QUEUE; /** * brief 初始化UART队列 * param qu: 队列结构体指针 * param buf: 外部分配的缓冲区指针 * param size: 缓冲区大小必须0且为2的幂次 * retval 1: 初始化成功0: 初始化失败指针空/大小不合法 */ int initUartQueue(TYPE_UART_QUEUE *qu, unsigned char *buf, uint32_t size) { // 校验1队列/缓冲区指针非空 if (qu NULL || buf NULL) { return 0; } // 校验2缓冲区大小为2的幂次且大于0位运算寻址的必要条件 if (!(size 0 (size (size - 1)) 0)) { return 0; } qu-data buf; qu-size size; qu-front 0; qu-rear 0; return 1; } /** * brief 判断队列是否为空 * param qu: 队列结构体指针 * retval 1: 队空/指针空0: 队列非空 */ int isUartQueueEmpty(TYPE_UART_QUEUE *qu) { if (qu NULL) { return 1; // 空指针直接视为队空避免非法解引用 } return (qu-front qu-rear) ? 1 : 0; } /** * brief 判断队列是否为满100%利用缓冲区无牺牲单元 * param qu: 队列结构体指针 * retval 1: 队满/指针空0: 队列未满 */ int isUartQueueFull(TYPE_UART_QUEUE *qu) { if (qu NULL) { return 1; // 空指针直接视为队满避免非法解引用 } // 无符号差值判断避免32位指针溢出导致的逻辑错误 return ((qu-rear - qu-front) qu-size) ? 1 : 0; } /** * brief 入队操作写入1字节数据 * param qu: 队列结构体指针 * param x: 待写入数据的指针 * retval 1: 入队成功0: 入队失败队满/指针空 */ int enUartQueue(TYPE_UART_QUEUE *qu, unsigned char *x) { // 双指针非空防护避免空指针解引用 if (qu NULL || x NULL) { return 0; } if (isUartQueueFull(qu)) { return 0; } // 位运算替代取模x%size x(size-1)提升单片机执行效率 qu-data[qu-rear (qu-size - 1)] *x; qu-rear; // 32位无符号指针自然溢出回绕无需手动取模 return 1; } /** * brief 出队操作读取1字节数据 * param qu: 队列结构体指针 * param x: 存储读取数据的指针 * retval 1: 出队成功0: 出队失败队空/指针空 */ int deUartQueue(TYPE_UART_QUEUE *qu, unsigned char *x) { // 双指针非空防护避免空指针解引用 if (qu NULL || x NULL) { return 0; } if (isUartQueueEmpty(qu)) { return 0; } // 位运算快速映射物理缓冲区地址中断中执行无压力 *x qu-data[qu-front (qu-size - 1)]; qu-front; // 32位无符号指针自然溢出回绕无需手动取模 return 1; }节约存储第二种方法补充冻结记录算法常用不能用在跨执行流的情况比如中断环形队列中没有队头只有队尾增加一个记录数通过记录数的大小判断队满队空。/******************************************************************************* ** 函数名称 ** 功 能顺序队操作函数 ** 修改日志 *******************************************************************************/ void initUartQueue(TYPE_UART_QUEUE *qu) { qu-rear 0; // 队尾指针初始化为0 qu-count 0; // 元素计数初始化为0队列为空 } //对空回1队不空回0 int isUartQueueEmpty(TYPE_UART_QUEUE *qu) { if (qu-count 0) { return 1; } else return 0; } //对满回1队不满回0 int isUartQueueFull(TYPE_UART_QUEUE *qu) { if (qu-count sample_maxsize) { return 1; } else return 0; } //往队列里写数据成功回1失败回0 int enUartQueue(TYPE_UART_QUEUE *qu, unsigned char *x) { if (qu-count sample_maxsize) return 0; qu-data[qu-rear] *x; //先改存储单元将数据写入队尾空位置 qu-rear (qu-rear 1) % sample_maxsize; //后改队尾指针循环后移 qu-count; // 元素计数1同步实际元素数量 return 1; } //从队列里读数据成功回1失败回0 int deUartQueue(TYPE_UART_QUEUE *qu, unsigned char *x) { if (qu-count 0) return 0; // 先改存储单元通过rear和count推导队头读取队头数据 int front (qu-rear - qu-count sample_maxsize) % sample_maxsize; *x qu-data[front]; qu-count--; // 元素计数-1同步实际元素数量无需修改rear指针 return 1; }实际使用中冻结记录存在norFlash往往需要多分配一个扇区用于擦除。“sample_maxsize”等于所有扇区可以存的总条数“qu-count”最大值等于需要记录的总条数。“qu-count”在每次写数据时会加一但是不能超过需要记录的总条数读数据的时候不会减一。补充结合双缓冲和环形队列可以有一个LFBB无锁双区缓冲区Lock-Free Bipartite Buffer。普通环形缓冲区只能对外提供1字节1字节的数据由于回绕无法提供一段连续内存。双区缓冲区将1段内存分成A区和B区只是逻辑上区分且不等长比“乒乓”高级如果A区剩余空间不够则用B区B区剩余空间不够则用A区总能提供一段连续内存给DMA之类的用。双缓冲的设计更加适合DMA发送场景。代码示例https://github.com/DNedic/lfbb补充DMA中的“乒乓”是第一次在A缓存下一次在B缓存。不等于“半完成中断”你可以收数据收到一半触发DMA半完成中断。补充被Linux内核用C写的kfifo无锁设计惊艳到了~
返回列表