Ответ 1
Проблема без летучих
Предположим, что volatile
не указан в массиве данных. Затем компилятор C
и ЦП не знает, что его элементы изменяются вне потока программы. Некоторые
вещи, которые могут произойти тогда:
-
Весь массив может быть загружен в кеш при вызове
myTask()
первый раз. Массив может оставаться в кеше навсегда и никогда снова обновляется с "основной" памяти. Эта проблема более насущна для многоядерных CPU, еслиmyTask()
привязан к одному ядру, например. -
Если
myTask()
встроен в родительскую функцию, компилятор может решить для поднятия нагрузки вне цикла даже до точки, где передача DMA не завершено. -
Компилятор может даже определить, что запись не происходит
memoryBuffer
и предположим, что элементы массива остаются в 0 все время (что снова вызовет множество оптимизаций). Это может произойти, если программа была довольно небольшой, и весь код виден компилятору (или используется LTO). Помните: после того, как компилятор ничего не знает о DMA и что он пишет "неожиданно и дико в память", (с точки зрения компилятора).
Если компилятор тупой/консервативный, а CPU не очень сложный (одноядерный, без выполнения вне порядка), код может работать даже без объявления volatile
. Но это также может не...
Проблема с энергозависимой
Making
весь массив volatile
часто является пессимизацией. По причинам скорости вы
вероятно, хотите развернуть цикл. Поэтому вместо загрузки из
массив и приращение индекса чередующимся образом, например
load memoryBuffer[m]
m += 1;
load memoryBuffer[m]
m += 1;
load memoryBuffer[m]
m += 1;
load memoryBuffer[m]
m += 1;
он может быстрее загружать сразу несколько элементов и увеличивать индекс в больших шагах, таких как
load memoryBuffer[m]
load memoryBuffer[m + 1]
load memoryBuffer[m + 2]
load memoryBuffer[m + 3]
m += 4;
Это особенно верно, если нагрузки могут быть сплавлены вместе (например, для выполнения одна 32-разрядная загрузка вместо двух 16-разрядных нагрузок). Далее вы хотите, чтобы компилятор для использования команды SIMD для обработки нескольких элементов массива с помощью одна инструкция.
Эти оптимизации часто предотвращаются, если загрузка происходит из энергозависимой памяти, поскольку компиляторы обычно очень консервативны с загрузить/сохранить переупорядочивание по обращениям с энергозависимой памятью. Снова поведение отличается от поставщиков компиляторов (например, MSVC и GCC).
Возможное решение 1: ограждения
Итак, вы хотите сделать массив нестабильным, но добавить подсказку для компилятора/процессора, говорящего "когда вы видите эту строку (выполнить этот оператор), очистите кеш и перезагрузите массив из памяти". В C11 вы можете вставить atomic_thread_fence в начале myTask()
. Такие ограждения препятствуют переупорядочению нагрузок/хранилищ на них.
Поскольку у нас нет компилятора C11, мы используем intrinsics для этой задачи. Компилятор ARMCC имеет __dmb()
встроенный (барьер памяти данных). Для GCC вы можете посмотреть __sync_synchronize()
(doc).
Возможное решение 2: атомная переменная, содержащая состояние буфера
Мы используем следующий шаблон в нашей кодовой базе (например, при чтении данных из
SPI через DMA и вызов функции для ее анализа): буфер объявляется как
простой массив (no volatile
) и в каждый буфер добавляется атомный флаг, который
устанавливается, когда передача DMA завершена. Код выглядит чем-то
например:
typedef struct Buffer
{
uint16_t data[10][20];
// Flag indicating if the buffer has been filled. Only use atomic instructions on it!
int filled;
// C11: atomic_int filled;
// C++: std::atomic_bool filled{false};
} Buffer_t;
Buffer_t buffers[2];
Buffer_t* volatile currentDmaBuffer; // using volatile here because I'm lazy
void setupDMA(void)
{
for (int i = 0; i < 2; ++i)
{
int bufferFilled;
// Atomically load the flag.
bufferFilled = __sync_fetch_and_or(&buffers[i].filled, 0);
// C11: bufferFilled = atomic_load(&buffers[i].filled);
// C++: bufferFilled = buffers[i].filled;
if (!bufferFilled)
{
currentDmaBuffer = &buffers[i];
... configure DMA to write to buffers[i].data and start it
}
}
// If you end up here, there is no free buffer available because the
// data processing takes too long.
}
void DMA_done_IRQHandler(void)
{
// ... stop DMA if needed
// Atomically set the flag indicating that the buffer has been filled.
__sync_fetch_and_or(¤tDmaBuffer->filled, 1);
// C11: atomic_store(¤tDmaBuffer->filled, 1);
// C++: currentDmaBuffer->filled = true;
currentDmaBuffer = 0;
// ... possibly start another DMA transfer ...
}
void myTask(Buffer_t* buffer)
{
for (uint8_t n=0; n<10; n++)
for (uint8_t m=0; m<20; m++)
foo(buffer->data[n][m]);
// Reset the flag atomically.
__sync_fetch_and_and(&buffer->filled, 0);
// C11: atomic_store(&buffer->filled, 0);
// C++: buffer->filled = false;
}
void waitForData(void)
{
// ... see setupDma(void) ...
}
Преимущество спаривания буферов с атомом заключается в том, что вы можете обнаружить, когда обработка слишком медленная, что означает, что вам нужно буферизировать больше, сделать входные данные медленнее или код обработки быстрее или что угодно достаточный в вашем случае.
Возможное решение 3: поддержка ОС
Если у вас есть (встроенная) ОС, вы можете использовать другие шаблоны вместо использования изменчивых массивов. В ОС мы используем пулы памяти и очереди. Последний может быть заполнен из потока или прерывания, и поток может блокироваться очередь, пока она не будет пустой. Шаблон выглядит примерно так:
MemoryPool pool; // A pool to acquire DMA buffers.
Queue bufferQueue; // A queue for pointers to buffers filled by the DMA.
void* volatile currentBuffer; // The buffer currently filled by the DMA.
void setupDMA(void)
{
currentBuffer = MemoryPool_Allocate(&pool, 20 * 10 * sizeof(uint16_t));
// ... make the DMA write to currentBuffer
}
void DMA_done_IRQHandler(void)
{
// ... stop DMA if needed
Queue_Post(&bufferQueue, currentBuffer);
currentBuffer = 0;
}
void myTask(void)
{
void* buffer = Queue_Wait(&bufferQueue);
[... work with buffer ...]
MemoryPool_Deallocate(&pool, buffer);
}
Это, вероятно, самый простой подход к реализации, но только если у вас есть ОС и если переносимость не является проблемой.