Vin*_*zal 5 c++ interrupt volatile isr
我目前正在使用 Atmel AVR 微控制器 (gcc),但希望答案一般适用于微控制器世界,即通常是单线程但带有中断。
我知道volatile在访问可以在 ISR 中修改的变量时如何在 C 代码中使用。例如:
uint8_t g_pushIndex = 0;
volatile uint8_t g_popIndex = 0;
uint8_t g_values[QUEUE_SIZE];
void waitForEmptyQueue()
{
bool isQueueEmpty = false;
while (!isQueueEmpty)
{
// Disable interrupts to ensure atomic access.
cli();
isQueueEmpty = (g_pushIndex == g_popIndex);
sei();
}
}
ISR(USART_UDRE_vect) // some interrupt routine
{
// Interrupts are disabled here.
if (g_pushIndex == g_popIndex)
{
usart::stopTransfer();
}
else
{
uint8_t value = g_values[g_popIndex++];
g_popIndex &= MASK;
usart::transmit(value);
}
}
Run Code Online (Sandbox Code Playgroud)
因为 g_popIndex 在 ISR 内部修改并在 ISR 外部访问,所以必须声明它volatile以指示编译器不要优化对该变量的内存访问。请注意,除非我弄错了,g_pushIndex并且g_values不需要声明volatile,因为它们没有被 ISR 修改。
我想把与队列相关的代码封装在一个类里面,这样可以复用:
class Queue
{
public:
Queue()
: m_pushIndex(0)
, m_popIndex(0)
{
}
inline bool isEmpty() const
{
return (m_pushIndex == m_popIndex);
}
inline uint8_t pop()
{
uint8_t value = m_values[m_popIndex++];
m_popIndex &= MASK;
return value;
}
// other useful functions here...
private:
uint8_t m_pushIndex;
uint8_t m_popIndex;
uint8_t m_values[QUEUE_SIZE];
};
Queue g_queue;
void waitForEmptyQueue()
{
bool isQueueEmpty = false;
while (!isQueueEmpty)
{
// Disable interrupts to ensure atomic access.
cli();
isQueueEmpty = g_queue.isEmpty();
sei();
}
}
ISR(USART_UDRE_vect) // some interrupt routine
{
// Interrupts are disabled here.
if (g_queue.isEmpty())
{
usart::stopTransfer();
}
else
{
usart::transmit(g_queue.pop());
}
}
Run Code Online (Sandbox Code Playgroud)
上面的代码可以说更具可读性。但是,volatile在这种情况下应该怎么做呢?
1)还需要吗?调用该方法是否Queue::isEmpty()以某种方式确保对 的非优化访问g_queue.m_popIndex,即使函数已声明inline?我不信。我知道编译器使用启发式方法来确定是否不应优化访问,但我不喜欢依赖这种启发式方法作为通用解决方案。
2)我认为一个有效(有效)的解决方案是Queue::m_popIndex volatile在类定义中声明成员。但是,我不喜欢这个解决方案,因为类的设计者Queue需要确切地知道它将如何用于知道哪个成员变量必须是volatile。它不会随着未来的代码更改而很好地扩展。此外,所有Queue实例现在都将拥有一个volatile成员,即使某些实例未在 ISR 内使用。
3) 如果将Queue类视为内置类,我认为自然的解决方案是将全局实例g_queue本身声明为volatile,因为它在 ISR 中被修改并在 ISR 之外访问。然而,这并不奏效,因为只能volatile在volatile对象上调用函数。突然间,Queue必须声明 的所有成员函数volatile(不仅仅是const那些或在 ISR 中使用的那些)。再次,设计师怎么能Queue提前知道呢?此外,这会惩罚所有Queue用户。仍然有可能复制所有成员函数并在类中同时具有volatile和非volatile重载,因此非volatile用户不会受到惩罚。不漂亮。
4)Queue可以在策略类上对该类进行模板化,该类可以volatile仅在需要时选择性地添加到其所有成员变量中。同样,类设计者需要提前知道这一点,解决方案更难理解,但是很好。
我很想知道我是否缺少一些更简单的解决方案。作为旁注,我在编译时不支持 C++11/14(还)。
是的,内联是绝对需要的。
1) 编译器通常会在每次调用内联函数的地方放置一个新的内联函数副本。这种优化似乎不会影响易失性变量。所以这没关系。
2)我赞同这是正确的解决方案(带有扩展名)。因为唯一需要保持不变的变量实际上是队列索引。
3)不,不需要将整个类实例标记为易失性,因为它可能会阻止其他潜在的优化。
4)可以使用继承。一个接口,声明队列必须具有哪些函数,以及两个继承类,一个用于 ISR(具有易失性队列索引),另一个用于不使用 ISR。此外,您始终可以定义模板化的类:
template<typename T>
class IQueue
{
public:
virtual bool isEmpty() const = 0;
virtual T pop() = 0;
protected:
uint8_t pushIndex;
T values[QUEUE_SIZE];
};
template<typename T>
class ISRQueue : public IQueue<T>
{
volatile uint8_t popIndex;
public:
inline bool isEmpty()const
{
return (pushIndex == popIndex);
}
inline T pop()
{
T value = values[popIndex++];
popIndex &= MASK;
return value;
}
};
template<typename T>
class Queue : public IQueue<T>
{
uint8_t popIndex;
public:
inline bool isEmpty()const
{
return (pushIndex == popIndex);
}
inline T pop()
{
T value = values[popIndex++];
popIndex &= MASK;
return value;
}
};
typedef ISRQueue<uint8_t> ISRQueueUInt;
typedef ISRQueue<uint8_t> QueueUInt;
Run Code Online (Sandbox Code Playgroud)