单片机裸机和 rtos 开发过程中,如何保证全局变量在中断和主循环中读写的正确性?

单片机裸机和 rtos 开发过程中,如何保证全局变量在中断和主循环中读写的正确性?裸机开发和 rtos 开发实战分别都是如何处理的?

回答·45
最热
最新
  • 首先要明白这几个知识点:关键字 volatile 的使用,原子操作,临界区的使用。 明白的直接跳到文中的4.全局变量的使用及保护处查看。 1.关键字 volatile 关键字 volatile 用于告诉编译器,说明被修身的变量可能会被意想不到地改变,防止编译器对代码进行优化。 比如如下程序: 1 ucNms=0x65; 2 ucNms=0x66; 3 ucNms=0x67; 4 ucNms=0x68; 上述 4 条语句,如果变量在声明的时候(unsigned char ucNms;)没有使用 volatile,那么编译器有可能对其优化,只编译最后一条语句 ucNms=0x68;(即忽略前三条语句,只产生一条机器汇编代码);如果变量在声明的时候(volatile unsigned char ucNms;)使用了 volatile,则编译器会逐一地进行编译并产生四条相应的机器代码(产生四条代码)。 精确地说就是,编译器在编译这个变量语句时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。所以下面几个情况在声明的时候需要用 volatile 关键字对其修饰: 1)并行设备的硬件寄存器(如:状态寄存器) 2)一个中断服务子程序中会访问到的非自动变量(Non-automatic variables) 3)多线程应用中被几个任务共享的变量 2.原子操作 原子操作可以理解为不被打断的操作,可以是一个步骤的操作,也可以是多个步骤的操作,总之确保操作不被打断。 3.临界区 指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。 4.全局变量的使用及保护 单片机裸机编程,使用全局变量时,一般是一个或多个*.c 文件(或模块)中会使用到某个全局变量(假设为 A),还有中断中也会用到这个全局变量。这样在使用时就要考虑变量的安全性。单片机裸机编程是前后台系统,如下图: 首先要明白大循环(后台)对这个变量的访问是依次的,不管全局变量 A 是在哪一个模块或者*.c 文件中,每一个时刻只有一个地方对变量 A 访问。然后中断和中断嵌套程序中也会有对全局变量 A 的访问。 于是就存在这样的问题,大循环(后台)在访问全局变量 A 时(比如说访问到一半时),被中断(前台)程序打断并修改了全局变量 A,这样大循环(后台)程序再次对全局变量 A 访问,就会导致访问到的 A 存在不确定性。从而会影响程序的不正常运行。 这样就可以很明确的知道,只要在大循环(后台)访问 A 时,不让中断(前台)打断其访问即可。确保对 A 的访问是原子操作。于是就有这样的解决方法: 关中断-->>全局变量 A-->>开中断 有的时候,如果访问变量 A 的过程比较长,可以对全局变量 A 做一个副本拷贝 a,用拷贝的 a 作为模块处理的数据。于是就有了这样: 关中断-->>访问全局变量 A-->>副本拷贝 a-->>开中断->>操作副本拷贝 a 这种复杂的情况也可以做一个锁这样做: 大循环(后台): 关中断-->>上锁-->>开中断-->>访问变量 A-->>关中断-->>解锁-->>开中断 中断(前台): 如果是解锁的,操作全局变量 A,如果是上锁的就不操作 当然,如果访问全局变量 A 本身就是一个原子操作(比如一条指令就可以访问完成),这样也就不需要做开关中断的处理了。 示例 1:禁止中断方法保护全局变量 大循环(后台) ET0=0;//禁止定时中断 访问全局变量 A; 其他代码部分; ET0=1;//开启允许定时中断 定时器中断(前台) 操作全局变量 A; 示例 2:加锁的方法保护全局变量 大循环(后台) ET0=0;//禁止定时中断 Lock=1; ET0=1;//开启允许定时中断 访问全局变量 A; 其他代码部分; ET0=0;//禁止定时中断 Lock=0; ET0=1;//开启允许定时中断 定时器中断(前台) If(lock==0)操作全局变量 A;  else{;} 示例 3:加锁的方法保护全局变量 大循环(后台) Lock=1;//若此条语句对应汇编指令是原子操作可以不用开关中断保护此锁 访问全局变量 A; 其他代码部分; Lock=0;//若此条语句对应汇编指令是原子操作可以不用开关中断保护此锁 定时器中断(前台) If(lock==0)操作全局变量 A;  else{;} 总结下:中断全局变量尽量要用 volatile 修饰,中断全局变量要原子操作访问,要时刻明白中断全局变量是临界区资源,共享访问时需要保护。 另外还有一种无原子锁读操作的方法,由于性能明显低于开关中断的方法,并不推荐对于中断比较敏感的系统也可采用。下面贴出参考代码如下: AType getA(void){     AType tmp;     do {         tmp = A;     } while(tmp != A);     return tmp } 就是两次读取中断中的全局变量 A,判断两次读取值是否相等,相等的话就认为线程中对全局变量 A 的访问没有被打断,返回的访问值是安全可靠的。
  • 首先应该区分单写单读、单写多读、多写多读 其次区分操作数长度(很多体系结构、操作系统、甚至编译器,对短数据类型都是有原子操作的) 最后才考虑关中断、互斥锁之类的常规保护 10 年经验的不应该还来问这样的问题
  • 裸机开发如果中断处理函数需要访问,加个 flag。rtos 需要加锁
  • 按照问题描述,在一个单核体系中,如果目的是为了保证变量读写的正确性,那仅需要 volatile 即可,除非该变量是某个结构体,因为结构体的成员操作并不是天然原子的。 对于一个基本类型的变量,bus 对该地址对操作总是原子的,除非该变量处于一个非总线位宽对齐的地址,那即使是指令级别的操作仍然会有可能被中断打断造成基础变量的数据异常。大部分情况编译器会保证基础变量的 layout 约束总是总线位宽对齐的,但当强制指定 padding 时需要小心。 原子操作只能保证上下文的互斥性,而对于读写的顺序性没法保证,即如果先写再读但是调试发现读到的是旧数据,通过 cpu 级别的原子操作是无法解决这个问题的。之所以大部分情况下通过关中断或者锁可以解决,往往是因为条件判断下的代码块与中断的操作具有相关性,和该条件变量本身无关。 spinlock 的底层实现都需要 smp 架构体系对于总线锁的支持,例如 ldrex 或者 strex。 读写的一致性仅仅只是读写的顺序和可见性是一致的,上下文是否需要互斥取决于该上下文是否存在多个修改者以及是否允许被撕裂(尽管大多数情况不被允许)
  • 有没有 rtos,对于主循环和中断来说都是一样的,如果是原子操作,都不需要任何操作,如果是非原子操作,那么就需要用关中断,或者原子锁来创建临界区
  • 在中断外将全局变量的写操作加上临界区保护,最安全的方式就是关闭中断并保存当前中断状态寄存器,完成写操作后将中断状态回复即可; 对于中断中全局变量的写操作,最好不要用锁机制,因为这可能在未来是一个潜在的 bug ,可以考虑用底半步机制,通过消息的方式让线程去写; 当然,所有的写操作前后都是需要加入临界区资源保护的。
  • 申请一个互斥量,访问共用资源时进行上锁
  • 尽量不要使用全局变量 如果非要使用 那么就要保证全局变量的单一属性 不要把一个变量搞得那么复杂 同时严格定义变量可能的值 赋值前进行严格的参数检查 同时最好用指针代替变量 好处很多的
  • 看了下评论,关 volatile 什么事?
  • 祼机要靠程序员自己控制的。 os 下写要用互斥锁,保证不会同时去写。读就无所谓了。