在中断例程中使用C++对象(和volatile)的正确方法是什么
What is the correct way of using C++ objects (and volatile) inside interrupt routines?
我目前正在使用Atmel AVR微控制器(gcc),但希望答案适用于一般的微控制器世界,即通常是单线程的,但有中断。
我知道在访问可以在ISR中修改的变量时如何在C代码中使用volatile
。例如:
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);
}
}
由于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());
}
}
上面的代码可以说可读性更强。但是,在这种情况下,应该如何处理volatile
?
1) 还需要吗?即使函数被声明为inline
,调用方法Queue::isEmpty()
是否以某种方式确保了对g_queue.m_popIndex
的非优化访问?我对此表示怀疑。我知道编译器使用启发式来确定是否不应该优化访问,但我不喜欢将这种启发式作为通用解决方案。
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) 不需要,不需要将整个类实例标记为volatile,因为这可能会阻止其他潜在的优化
4) 您可以使用继承。一个接口,声明队列必须具有哪些函数,以及两个继承类,一个用于与ISR一起使用(具有队列索引volatile),另一个用于不使用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;
- 为不同配置设置MSVC_RUNTIME_LIBRARY的正确方法是什么
- C++避免重复声明的语法是什么
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 实现无开销push_back的最佳方法是什么
- C++从另一个类访问公共静态向量的正确方法是什么
- "throw expression code" 1e7 >返回 d 是什么?投掷标准::overflow_error( "too big" ) : d;意味 着?
- C++中名称篡改的目的是什么
- 在 c++ 中拥有一组结构的正确方法是什么?
- 这个指针和内存代码打印是什么?我不知道是打印垃圾还是如何打印我需要的值
- 是什么阻止DOMTimerCoordinator::NextID进入无休止的循环
- 派生类销毁的最佳实践是什么
- 这个语法std::class<>{}(arg1, arg2) 在C++中是什么意思?
- 通过JNI传递数据数组的最快方法是什么
- "using namespace std;"在C++的作用是什么?
- 在两台机器之间进行时间戳的最佳c++chrono函数是什么
- 文件系统:复制功能的速度秘诀是什么
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 使用QQuickFramebufferObject时同步数据的最佳方式是什么
- 在中断例程中使用C++对象(和volatile)的正确方法是什么
- 在 c++ 中,void* volatile* 的含义是什么?