当前位置: 首页 > news >正文

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

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/lfbb<stdint.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无锁设计惊艳到了~

http://www.cnnetsun.cn/news/3947440.html

相关文章:

  • 罗源县建设局网站如何助力民生与建设:全方位解读罗源县建设局网站
  • UI Auto Monkey自动化测试:原理、架构与Python实战
  • GetQzonehistory终极指南:完整备份QQ空间说说的免费解决方案
  • 5分钟掌握Windows平台消息防撤回技术:RevokeMsgPatcher完整解决方案
  • 本地DNA分析实战:使用SuperDNA命令行工具处理基因原始数据
  • LangChain框架解析:从核心概念到RAG应用实战
  • AI论文写作工具全攻略:从文献检索到自动引用
  • Raw Accel 终极指南:Windows鼠标加速驱动完整教程
  • 用Wand-Enhancer方案彻底突破WeMod专业版限制:3步实现永久免费解锁
  • FlicFlac音频格式转换工具:5分钟快速上手的完整指南
  • 虚拟机网站建设全攻略:从零基础入门到高效运维的真心话
  • React嵌套属性编辑器实现与优化指南
  • 免费下载B站大会员4K视频:Python开源工具完整指南
  • AI Agent 面试题 465:Agent的目标达成度评估和进度追踪机制
  • AMD锐龙处理器调试工具:SMUDebugTool完全使用指南
  • 终极Windows和Office激活解决方案:KMS_VL_ALL_AIO智能激活工具完整指南
  • 算法面试——二分查找:旋转数组、二维矩阵、边界
  • 如何快速获取网易云和QQ音乐的LRC歌词?这个开源工具让你告别手动搜索烦恼
  • 如何去除Gemini复制的多余符号?AI导出鸭实测!手慢无!
  • 一个没加 volatile 的单例,让 1% 的请求拿到了半初始化的对象
  • 广西住房和城乡建设厅网站_官方办事入口与最新政策查询全指南
  • OpenClaw Windows部署 保姆级操作教程,全程可视化无需命令行配置
  • Steam创意工坊免费下载终极指南:跨平台开源工具WorkshopDL深度解析
  • AI - Java之Spring AI Alibaba
  • Self-Correcting Large Language Models: Generation vs. Multiple Choice
  • 如何高效清理重复照片:AntiDupl智能图片去重工具完整指南
  • 从部署到工程化:MiniMax H3视频生成模型本地应用深度解析
  • 网站规划与建设进度如何把控:从0到1的实战复盘与避坑指南
  • 5个简单步骤:FanControl终极风扇控制配置指南
  • 中兴光猫工厂模式解锁工具:5分钟获取高级管理权限终极指南