Как правильно использовать объекты C ++ (и изменчивые) внутри подпрограмм прерывания?

В настоящее время я работаю с микроконтроллерами 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 и 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) Класс Queue можно создать по шаблону для класса политики, который может дополнительно добавлять volatile ко всем своим переменным-членам только при необходимости. Опять же, разработчик класса должен знать это заранее, и решение сложнее для понимания, ну да ладно.

Мне любопытно узнать, не хватает ли мне более простого решения этой проблемы. В качестве примечания, я компилирую без поддержки C ++ 11/14 (пока).


person Vincent Zalzal    schedule 04.03.2015    source источник
comment
Есть ли на вашей платформе cli и sei барьеры памяти?   -  person David Schwartz    schedule 04.03.2015
comment
@DavidSchwartz Нет (я думаю, не на 100%). Однако у меня есть доступ к инструкции _MemoryBarrier ().   -  person Vincent Zalzal    schedule 04.03.2015
comment
@DavidSchwartz Я присмотрелся к документу, и это действительно препятствия для памяти.   -  person Vincent Zalzal    schedule 04.03.2015


Ответы (1)


Да, встроенная функция определенно необходима.
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;
person sithereal    schedule 04.03.2015