我想请一些帮助,以便更好地理解以下段落的一部分:
" volatile关键字限定符表示可以在程序之外更改变量。例如,外部设备可以将数据写入端口。编译器有时会暂时使用缓存或寄存器将值保存在内存位置以进行优化。如果外部写入修改了内存位置,则此更改将不会反映在缓存或寄存器值中。"
(它来自书:理解和使用c指针,第178-179页)
我的歧义是在这些短语之间:"将值保存在 a 内存位置"和"如果外部写入修改了内存位置"。
我的问题是:我得到的印象是,如果外部设备将数据写入端口,该数据将存储到某个位置(???),那么它们将被存储到寄存器/缓存(??)和然后在c语言源代码的变量里面。有些东西被我误解了。据我所知,正常的工作流程应该是:外部设备 - >小型临时缓冲区 - > RAM内存中的变量,(当数据从小工具传输到MCU的RAM时)
#define PORT 0xB0000000
unsigned int volatile * const port = (unsigned int*) PORT;
*port = 0x0BF4; // write to port
value = *port; // read from port
答案 0 :(得分:5)
Memory mapped I/O devices不要通过CPU内核的寄存器(或通常是缓存)。这就是为什么他们外部,他们只是挂在内存总线上的某个地方,假装是内存。
因此来自这种设备的值将直接出现在(对CPU)看起来像内存的位置。
在你给出的例子中,这个:
*port = 0x0BF4; // write to port
可能会导致A / D转换器开始转换,这个
value = *port; // read from port
可以读取结果值。这是不是一个非常典型的设计(A / D转换器往往比这更复杂,等等),但它是可能的。
如果编译器认为“嘿,只有从写入此值的位置读取”,它可能会用
替换这两个语句value = 0x0BF4; // "optimized", but broken since no more I/O occurs
如果您尝试从该A / D转换器读取值,这将毁掉您的一天。
声明位置volatile
告诉编译器不要对访问该位置的副作用做出任何假设。
如果你看一下类似STM32F4基于ARM的微控制器,它有大量的内存映射I / O(串行端口,USB控制器,以太网,定时器,A / D和D / A转换器......) “一切都在那里”加上一堆内部(对核心,但仍然是内存映射)的东西。
答案 1 :(得分:4)
正如其他人所说,这些是CPU内核本身外部的项目,它可能是内存映射外设(例如uart状态寄存器或定时器寄存器等)。
#define SOME_STATUS_REGA (*((volatile unsigned int *)0x10008000))
void fun ( void )
{
while(SOME_STATUS_REGA==0) continue;
}
#define SOME_STATUS_REGB (*((unsigned int *)0x10008000))
void more_fun ( void )
{
while(SOME_STATUS_REGB==0) continue;
}
使用一个目标和工具链生成
00000000 <fun>:
0: e59f200c ldr r2, [pc, #12] ; 14 <fun+0x14>
4: e5923000 ldr r3, [r2]
8: e3530000 cmp r3, #0
c: 0afffffc beq 4 <fun+0x4>
10: e12fff1e bx lr
14: 10008000 andne r8, r0, r0
00000018 <more_fun>:
18: e59f300c ldr r3, [pc, #12] ; 2c <more_fun+0x14>
1c: e5933000 ldr r3, [r3]
20: e3530000 cmp r3, #0
24: 112fff1e bxne lr
28: eafffffe b 28 <more_fun+0x10>
2c: 10008000 andne r8, r0, r0
你可以看到more_fun,不易变的情况下,它读取位置一次比较一次,但进入无限循环。编译器完成了我们告诉它要做的事情,因为变量无法改变,没有理由燃烧时钟周期重新读取不会改变的东西,所以如果它不是第一个并且只读它永远不会为零所以这会陷入无限循环。
如果你让它变得不稳定,那么你就会问这个问题&#34;编译器在每次访问代码时读取或写入它。你可以在有趣的情况下看到它,它每次都通过循环返回读取该地址,看它是否已经改变。 volatile关键字是这两种行为之间的区别。
它不一定是改变这些值的硬件,如果使用全局变量在isr和前台代码之间进行通信,则内存中的变量可以由isr和/或前台代码更改,因此两者都需要把它当作不稳定的。
您还可以使用多核/多线程处理器,其中每个核心/线程可以独立访问共享资源。在这种情况下,您不仅需要使用volatile,而且如果内核不共享相同的缓存,则可能需要缓存该缓存,并且如果需要原子操作,可能必须具有硬件和/或软件锁定(ldrex / ARM世界中的strex是第一步。)
修改
另一个演示,问题不仅在于读取,还在于写入。假设您有一个外设,您需要编写配置寄存器来设置某个模式,然后再次编写它以使用该模式启用它。或者你有一个硬件接口,每个写入增加一些逻辑指针,你做一系列的写操作。
#define SOMETHING1 (*((volatile unsigned char *)0x10002000))
void fun ( void )
{
SOMETHING1=5;
SOMETHING1=5;
SOMETHING1=6;
}
#define SOMETHING2 (*((unsigned char *)0x10002000))
void more_fun ( void )
{
SOMETHING2=5;
SOMETHING2=5;
SOMETHING2=6;
}
没有易失性,外围设备无法正常运行。对同一指针/地址的多次写入被视为死代码并进行了优化。
00000000 <fun>:
0: e3a02005 mov r2, #5
4: e3a01006 mov r1, #6
8: e59f300c ldr r3, [pc, #12] ; 1c <fun+0x1c>
c: e5c32000 strb r2, [r3]
10: e5c32000 strb r2, [r3]
14: e5c31000 strb r1, [r3]
18: e12fff1e bx lr
1c: 10002000 andne r2, r0, r0
00000020 <more_fun>:
20: e3a02006 mov r2, #6
24: e59f3004 ldr r3, [pc, #4] ; 30 <more_fun+0x10>
28: e5c32000 strb r2, [r3]
2c: e12fff1e bx lr
30: 10002000 andne r2, r0, r0
EDIT2
Clang / llvm也证明了这个问题
#define A (*((volatile unsigned char *)0x10002000))
void afun ( void )
{
A = 4;
A = 5;
A = 6;
A |= 1;
while(A==0) continue;
}
#define B (*((unsigned char *)0x10002000))
void bfun ( void )
{
B = 4;
B = 5;
B = 6;
B |= 1;
while(B==0) continue;
}
产
00000000 <afun>:
0: e3a00a02 mov r0, #8192 ; 0x2000
4: e3a01004 mov r1, #4
8: e3800201 orr r0, r0, #268435456 ; 0x10000000
c: e5c01000 strb r1, [r0]
10: e3a01005 mov r1, #5
14: e5c01000 strb r1, [r0]
18: e3a01006 mov r1, #6
1c: e5c01000 strb r1, [r0]
20: e5d01000 ldrb r1, [r0]
24: e3811001 orr r1, r1, #1
28: e5c01000 strb r1, [r0]
2c: e5d01000 ldrb r1, [r0]
30: e3510000 cmp r1, #0
34: 0afffffc beq 2c <afun+0x2c>
38: e12fff1e bx lr
0000003c <bfun>:
3c: e3a00a02 mov r0, #8192 ; 0x2000
40: e3a01007 mov r1, #7
44: e3800201 orr r0, r0, #268435456 ; 0x10000000
48: e5c01000 strb r1, [r0]
4c: e12fff1e bx lr
如果你正在做一些可以优化它们的领域中的两件事,那么添加volatile不会伤害你。 (在某个序列中对每个寄存器进行单次写入,对寄存器进行单次读取,单次也表示不进行循环)。如果你进行多次写入(通常在配置外设时经常发生)进行读取修改写入(x | =某些东西,y&amp; = something,z ^ = something等),那肯定会对你造成伤害。
如果你使用的工具链没有优化器,或者你选择不优化你就不会有这个问题,但是如果你关闭挥发物就不能携带代码,如果你不习惯性交易,你最终会遇到麻烦使用跨越编译或其他类似域的变量/代码(硬件是与软件分开的编译域)。
答案 2 :(得分:1)
在C之前添加&#34; volatile&#34;键盘,每次访问没有register
限定符的对象都会导致对象的地址加载或存储到对象的地址。鉴于声明int i,j;
,代码:
i+=j;
j+=i;
i+=j;
会从内存加载i
和j
,添加它们,并将结果存储到i
。然后,它会再次从内存中加载i
和j
,添加它们,并将结果存储到j
。最后,它会第三次从内存中加载i
和j
,添加它们,并将结果存储到i
。因此,三个语句将导致六个加载,三个加法和三个存储。
如果没有什么&#34;特别&#34;关于i
和j
,以下内容会更有效:
register int t1,t2;
t1=i; t2=j;
t1+=t2; t2+=t1; t1+=t2;
i=t1; j=t2;
虽然这看起来更像代码,但t1
和t2
上的操作不需要加载和存储。因此,编译器只需要生成两个加载,三个加法和两个存储 - 节省了四个加载和一个存储与原始加载相比的成本。
让编译器自动将前一种代码转换为后者将有助于解决一个问题:有时看起来像变量的东西可能会以编译器不知道的方式改变。这可能是因为除了存储器之外的电路连接到存储器总线上(许多系统具有I / O设备,当代码尝试读取或写入某些地址时,它们被连接以响应),或者因为机器可能通过调度响应外部刺激控制到一个称为中断处理程序的特殊代码段,然后在中断处理程序返回时恢复正在执行的操作。中断处理程序通常读取和写入变量,这些变量也可以通过主线代码访问(实际上,这是它们存在的原因之一)但是如果代码执行类似的操作:
while(!data_received)
;
并依赖于数据变为可用时设置data_received的中断处理程序,如果编译器将其替换为以下代码,则此类代码可能会失败:
t1 = data_received;
while(t1)
;
会执行循环&#34;更快&#34;但是在数据到达时无法退出循环。
volatile
的目的是告诉编译器某些对象需要&#34;特殊&#34;治疗。一些编译器(明智的,恕我直言)会将volatile
解释为访问如此标记的对象可能以编译器不知道的方式任意影响系统中的所有内容,从而允许构造如下: / p>
extern volatile char * volatile dma_mem;
extern volatile unsigned dma_count, dma_command, dma_busy;
void put_data(char *data, unsigned size)
{
dma_mem = data;
dma_count = size;
// Following will trigger hardware to automatically copy "dma_count"
// bytes from memory starting at "dma_mem"; dma_busy will read as
// zero once operation is complete.
dma_command = OUTPUT_MEMORY; // Exact value depends on ardware
while(dma_busy)
;
}
在编译器上避免在volatile
访问中保留寄存器中的任何内容时,可以使用上述函数从&#34;普通&#34;输出数据。内存提供所有外部访问在函数返回之前完成。但是,如果编译器甚至在优化名称中的volatile
次访问中将事物保存在寄存器中,则此类代码可能会失败,除非放入数据的缓冲区也是合格的volatile
。
PS - 虽然volatile
可以并且经常用于I / O访问,但对于那些受中断影响的事物,通常不需要(*)。在许多情况下,I / O地址将使用
#define PORTA (*(unsigned char*)0xD000)
#define PORTB (*(unsigned char*)0xD002)
虽然标准并不要求编译器将这些地址视为易失性,但许多编译器无论如何都会这样做,因为程序员和#39;使用这些地址意味着他们知道编译器不知道的事情。相比之下,由中断处理程序设置的标志看起来像普通的RAM一样,只有volatile
标志表示它们有任何特殊之处。
(*)我看过许多供应商提供的头文件,它们不会将volatile
用于I / O地址。如果编译器使用或不使用该关键字生成相同的代码,则为编译器添加更多措辞以便在每次构建时进行咀嚼都会减慢编译速度。 标准的作者故意避免要求所有编译器都适合嵌入式或系统编程,因此不会努力禁止使编译器不适合此类目的的行为。特定目的的代码应仅适用于适用于此类目的的编译器;如果这样的代码在故意不适合该目的的编译器上失败,那并不意味着代码被破坏了 - 而是意味着编译器不再适合用于此类代码。
PS - 对于编译器根据不是volatile
的常量地址进行任何有用的优化,它必须要么知道&#34;知道&#34;没有其他对象被观察为具有相同的地址,或者允许即使两个整数x和y相等,*(uint8_t*)x
和*(uint8_t)y
也可能不会被识别为影响对方。由于标准表示将一个指向整数并返回的指针往返产生一些&#34;比较等于&#34;对于原始指针,但并不是说它实际上可以用于任何目的,这将是符合但意外的。
例如,考虑以下程序包含两个单独的翻译单元[假设包含所需的标题]
// UNIT ONE
extern unsigned char foo;
extern uintptr_t volatile tfoo;
int foo_addr(void)
{
tfoo = (uintptr_t)&foo;
return tfoo == 0x12345678;
}
// UNIT TWO
void foo_addr(void);
unsigned char foo;
uintptr_t volatile tfoo;
int main(void)
{
int ok = foo_addr();
foo = 2;
if (ok)
(unsigned char*)0x12345678 = 4;
return ok + foo;
}
如果foo
没有给出地址0x12345678,则不会对地址0x12345678进行写操作,代码将返回0.如果foo
的地址为0x12345678,则(unsigned char*)0x12345678
1}}将成为foo
的合法指针,并且应该要求编译器识别访问权限,除非它确定它不会将往返指针到整数转换视为可以使用指针。
(unsigned char*)0x12345678 as aliasing everything it would need to alias would be to treat it as
volatile and refrain from caching in registers anything whose address has been exposed. Useful optimizations from treating such a variable as not being
volatile`视为罕见。