在中断例程中使用 C++ 对象(和易失性对象)的正确方法是什么?

2023-12-31

我目前正在使用 Atmel AVR 微控制器 (gcc),但希望答案适用于一般微控制器世界,即通常是单线程但带有中断。

我知道如何使用volatile在 C 代码中访问可在 ISR 中修改的变量时。例如:

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 and 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)还需要吗?是否调用该方法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) The 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;
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在中断例程中使用 C++ 对象(和易失性对象)的正确方法是什么? 的相关文章