我见过很多解释 volatile 关键字用法的例子,但所有这些都引用单个变量,例如在 ISR 中递增的刻度计数器,然后在主循环中读取。但是主循环和 ISR 之间共享的缓冲区怎么样,其中主循环是生产者和 ISR 消费者(或其他方式)。
假设我有一个简单的环形缓冲区结构:
struct RingBuffer
{
uint8_t *data;
uint32_t write_index;
uint32_t read_index;
};
struct RingBuffer buffer;
data必须是易失性的吗?或者整个buffer(这也意味着函数参数也必须是易失性的)?我查看了这种数据结构的一些实现,但我看不到任何易失性,这让我很烦恼。 我在 stm32 上编写了一个简单的程序,用于驱动带缓冲的液晶显示器。代码如下所示:
struct OutputLine
{
GPIO_TypeDef *gpio;
uint16_t pin;
};
typedef struct OutputLine OutputLine_t;
struct HD44780
{
OutputLine_t enable;
OutputLine_t rs;
OutputLine_t rw;
OutputLine_t data[4]; // data[0..3] == D4..7
Fifo_t fifo;
};
在主循环中,我检查液晶屏是否忙,如果不忙,则队列中是否有东西。到 lcd 的命令是在定时器 irq 中发送的
while (1)
{
/* USER CODE END WHILE */
HD44780_Update(&lcd);
/* USER CODE BEGIN 3 */
}
void HD44780_Update(HD44780_t *lcd)
{
if (Fifo_Empty(&lcd->fifo)) {
return;
}
if (HD44780_Busy(lcd)) {
return;
}
HD44780_FifoItem_t item;
Fifo_Read(&lcd->fifo, &item);
HD44780_WriteByte(lcd, item.byte, item.rs);
}
void TIM6_IRQHandler(void)
{
if (LL_TIM_IsActiveFlag_UPDATE(TIM6)) {
LL_TIM_ClearFlag_UPDATE(TIM6);
i = (i + 1) & 1;
HD44780_ClearDisplay(&lcd);
HD44780_Puts(&lcd, strings[i], strlen(strings[i]));
}
}
无论 Fifo_t 是否不稳定,结果都是正确的。
什么是
volatile
?它可能是 C 语言中最容易被误解的关键字(也许除了 restrict
)。
volatile
通知编译器该对象(变量)容易产生副作用。这意味着它可以被正常程序执行路径中的编译器不可见的内容更改。例如,通过硬件(DMA、硬件寄存器)或信号(异常)处理程序,这些处理程序不被程序调用。示例:
uint32_t counter;
void TIM6_IRQHandler(void)
{
/* .... */
counter++;
}
void foo(void)
{
while(counter < 1000);
printf("x");
}
以及生成的代码:https://godbolt.org/z/h3qz7Y5W6
foo:
ldr r3, .L8
ldr r3, [r3]
.L6:
cmp r3, #1000
bcc .L6
如您所见,如果没有
volatile
,counter
只会加载到寄存器一次,并且即使 foo
达到 counter
,函数 1000
也会以死循环结束。对于 volatile
counter
,每次使用之前都会从内存中读取,因为编译器知道它容易产生副作用:https://godbolt.org/z/KThnG3sq4
生成的代码:
foo:
ldr r2, .L8
.L6:
ldr r3, [r2]
cmp r3, #1000
bcc .L6
现在,每次使用时都会将
counter
加载到寄存器中。
volatile
不保证原子性缓存一致性。