У меня есть Google Coral Dev Micro с SoC RT1176 (800 МГц Cortex-m7 и 400 МГц Cortex-m4), m7 работает под управлением FreeRTOS, а m4 работает без ОС, компилируя с использованием GCC none eabi 9.3.1 со следующими флагами:
-Wall -Wno-psabi -mthumb -fno-common -ffunction-sections -fdata-sections -ffreestanding -fno-builtin -mapcs-frame --specs=nano.specs --specs=nosys.specs -u _printf_float -std=gnu99 -g -Os -save-temps -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -DNDEBUG
Я хочу передать данные, сгенерированные в прерывании таймера, между двумя ядрами, используя общую память, и я создал свой собственный буфер FIFO без блокировки в общей памяти:
фифо.х:
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
template <typename T, size_t num_elements>
class LockLessFifo {
public:
LockLessFifo() : read_idx(0), write_idx(0), buffer({0}) {}
size_t size() {
if (read_idx <= write_idx) {
return write_idx - read_idx;
} else {
return num_elements - (read_idx - write_idx);
}
}
constexpr std::size_t capacity() { return buffer.max_size(); }
void put(const T& entry) {
// Wait for space
while ((num_elements - size()) < 2) {
;
}
size_t new_write_idx = (write_idx + 1) % num_elements;
buffer[new_write_idx] = entry;
// Update the write index AFTER writing the data
write_idx = new_write_idx;
}
T get(const bool block = true) {
if (!block && size() == 0)
return -1;
while (size() == 0) {
;
}
// Consume
size_t new_read_idx = (read_idx + 1) % num_elements;
T retrieved_value = buffer[new_read_idx];
// Update the read index AFTER reading (to signal availability to the producer)
read_idx = new_read_idx;
return retrieved_value;
}
private:
std::atomic<std::size_t> read_idx;
std::atomic<std::size_t> write_idx;
std::array<std::atomic<T>, num_elements> buffer;
};
Shared_fifo.h
#include "fifo.h"
using shared_element_t = uint32_t;
constexpr std::size_t SHARED_MEMORY_SIZE = 0x1700; // Manually calculated free space using map file
constexpr std::size_t SHARED_MEMORY_ELEMENTS = SHARED_MEMORY_SIZE / sizeof(shared_element_t);
LockLessFifo<shared_element_t, SHARED_MEMORY_ELEMENTS> shared_fifo __attribute__((section(".noinit.$rpmsg_sh_mem")));
Я не ожидаю, что код FIFO будет идеальным, но он функционален. Я поделился этим только в завершение и для того, чтобы дать весь контекст.
FIFO считывается из m7 следующим образом: main_m7.cpp
#include <cstdio>
#include "libs/base/ipc_m7.h"
#include "shared_fifo.h"
extern "C" [[noreturn]] void app_main(void* param) {
(void)param;
coralmicro::IpcM7::GetSingleton()->StartM4();
uint32_t counter1, counter2;
while (true) {
counter1 = shared_fifo.get(); // Works
counter2 = shared_fifo.get(); // Always 0
const std::size_t fifo_size = shared_fifo.size();
constexpr std::size_t fifo_capacity = shared_fifo.capacity();
printf("[M7] counter: %lu/%lu size: %u/%u\r\n", counter1, counter2, fifo_size, fifo_capacity);
}
}
FIFO заполняется на m4 следующим образом:
main_m4.cpp
#include <atomic>
#include <cmath>
#include <cstdio>
#include "fsl_pit.h"
#include "shared_fifo.h"
static std::atomic<std::size_t> counter1 = 0;
static std::atomic<std::size_t> counter2 = 0;
void my_pit_irq() {
// Clear IRQ flag
PIT_ClearStatusFlags(PIT1, kPIT_Chnl_0, kPIT_TimerFlag);
counter1.store(counter1.load() + 1); // Works
counter2.fetch_add(1); // Always 0
SDK_ISR_EXIT_BARRIER;
}
extern "C" [[noreturn]] void app_main(void* param) {
(void)param;
configure_and_start_timer(); // removed for clarity
while (true) {
shared_fifo.put(counter1.load()); // Works
shared_fifo.put(counter2.load()); // Always 0
}
}
Вывод этого кода:
(...)
[M7] counter: 71666076/0 size: 1470/1472
[M7] counter: 71666076/0 size: 1470/1472
[M7] counter: 71666077/0 size: 1470/1472
[M7] counter: 71666077/0 size: 1470/1472
[M7] counter: 71666077/0 size: 1470/1472
(...)
Почему неправильное приращение (счетчик1) работает, а правильное приращение (счетчик2) — нет? Я посмотрел разборку:
Прерывание PIT:
_Z10my_pit_irqv:
// Clear IRQ flag
ldr r3, .L3
movs r2, #1
str r2, [r3, #268]
ldr r2, .L3+4 // Load .LANCHOR0
dmb ish
ldr r3, [r2] // Load value stored at address of .LANCHOR0 (.load())
dmb ish
adds r3, r3, #1 // Increment by 1
dmb ish
str r3, [r2] // Store incremented value (.store())
ldr r3, .L3+8 // Load .LANCHOR1
dmb ish
dmb ish // Dubble barrier?
.L2:
ldrex r2, [r3] // load
adds r2, r2, #1 // increment
strex r1, r2, [r3] // store
cmp r1, #0 // check
bne .L2 // retry
dmb ish
dsb 0xF
bx lr
.L4:
.align 2
.L3:
.word 1074626560
.word .LANCHOR0 // counter1
.word .LANCHOR1 // counter2
И главное:
app_main:
push {r0, r1, r2, lr}
ldr r6, .L14 // Counter1
ldr r4, .L14+4
ldr r5, .L14+8 // Counter2
.L13:
// This is the same as for counter2
add r1, sp, #4
ldr r3, [r6] // Load the value of counter1
dmb ish
mov r0, r4
str r3, [sp, #4]
bl _ZN5LockLessFifoImLj1472EE3putERKm // Put it in the FIFO
dmb ish
// This is the same as for counter1
add r1, sp, #4
ldr r3, [r5] // Load the value of counter2
dmb ish
mov r0, r4
str r3, [sp, #4]
bl _ZN5LockLessFifoImLj1472EE3putERKm // Put it in the FIFO
b .L13
.L15:
.align 2
.L14:
.word .LANCHOR0
.word _ZN511shared_fifoE
.word .LANCHOR1
Я не понимаю, где что-то идет не так. Я знаю, что FIFO работает, поскольку приращение работает, и мы видим, как число, напечатанное в терминале, увеличивается. Сгенерированная сборка очень похожа и выглядит правильно. К сожалению, я не могу подключить отладчик (пока), так как мне нужно будет припаять разъем JTAG к плате, и нам нужно подождать, прежде чем вносить какие-либо изменения.
Спасибо, что нашли время взглянуть.
Кроме того, является ли dmb ish правильным барьером? Внутренний разделяемый домен, опять же, обычно представляет собой ядра, совместно использующие кэш L1, и, в частности, все они обычно работают под управлением одной и той же ОС. Интересно, действительно ли ваше другое ядро находится в домене внутреннего общего доступа? Если нет, то dmb ish может не сбросить кэши обратно достаточно далеко, чтобы обеспечить глобальную видимость.
@NateEldredge хорошие моменты: общая память находится в OCRAM. Индексы чтения/записи обновляются правильно, однако они обновляются только с одной стороны. Когда у меня будет время, я добавлю общий счетчик и посмотрю, правильно ли он увеличился. Барьер dmb ish вставляется компилятором. Знаете ли вы, как я могу дать указание компилятору генерировать более агрессивные инструкции dmb?
Сомневаюсь, что можно, кроме взлома компилятора. Я не думаю, что gcc действительно поддерживает использование атомики для чего-либо, кроме многопоточности, которая представляет собой несколько однородных ядер во внутренней разделяемой области. Возможно, вам придется вернуться к использованию volatile для ваших грузов и магазинов и вставлять барьеры вручную, например. asm("dmb osh" : : : "memory");
@NateEldredge У меня только что было время проверить это. Я добавил общий атомный счетчик и позволил обоим ядрам увеличить его в 1000000 раз. Полученное число не равно 2000000. Спасибо за помощь, задам свой вопрос.





С помощью @NateEldredge в комментариях я нашел ответ: атомы C++ неправильно компилируются на этой аппаратной платформе. Компилятор генерирует инструкции dbm ish, которые синхронизируют только внутренний разделяемый домен.
Я проверил это, увеличив общий счетчик 1000000 раз на каждом ядре, и результат не равен 2000000.
Это можно было бы исправить, вручную заменив инструкции dbm ish на dbm osh, но быстрого и грязного исправления оказалось недостаточно:
// the attempt that didn't work
for (size_t i = 0; i < 1000000; i++) {
asm("dmb osh" : : : "memory");
unica::shared_fifo.shared_counter++; // not actually atomic wrt. other core
asm("dmb osh" : : : "memory");
}
К сожалению, я просто откажусь от использования std::atomic и вернусь к использованию volatile и маскировке IRQ там, где это необходимо.
Обновление после обсуждения на Reddit: https://www.reddit.com/r/embedded/comments/1bv1hqw/comment/kxz7yz8/?context=3
Похоже, что область памяти была настроена как недоступная для совместного использования. Если у меня будет время, я проверю, проблема ли в этом.
Имеет смысл; барьеры не создают атомарность, они могут только помочь с упорядочиванием (и, возможно, с видимостью между несогласованными кэшами). Таким образом, кажется, что LDREX/STREX не дает сбоя, когда другое ядро изменяет эту строку кэша; предположительно, он также работает только для внутреннего совместного использования.
Отключение прерываний обеспечивает атомарность только в одноядерной системе. Если бы другое ядро могло модифицировать переменную, x.store(x.load() + 1) не будет атомарным, и именно так volatile компилируется. И отключение прерываний даже на обоих ядрах не поможет. Если только единственный код на другом ядре, который изменяет общую переменную, не находится внутри обработчика прерываний, и вы не оставляете прерывания отключенными на достаточно долгое время, так что, если он уже был в этом обработчике, он завершит приращение до того, как вы выполните приращение.
Обычно вы используете
ldrex/strexдля атомарности по отношению к другим ядрам, имеющим общий когерентный кеш L1, и он работает через протокол MESI. Документирует ли ваша система, что она дает вам атомарность по отношению к другому процессору? Полагаю, для этого потребуется воспользоваться автобусным шлюзом или чем-то в этом роде. Если вы проводите тест, в котором оба процессора одновременно выполняют атомарное приращение одной и той же переменной, получите ли вы правильные результаты?