Атомное приращение не работает должным образом в прерывании

У меня есть 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 к плате, и нам нужно подождать, прежде чем вносить какие-либо изменения.

Спасибо, что нашли время взглянуть.

Обычно вы используете ldrex/strex для атомарности по отношению к другим ядрам, имеющим общий когерентный кеш L1, и он работает через протокол MESI. Документирует ли ваша система, что она дает вам атомарность по отношению к другому процессору? Полагаю, для этого потребуется воспользоваться автобусным шлюзом или чем-то в этом роде. Если вы проводите тест, в котором оба процессора одновременно выполняют атомарное приращение одной и той же переменной, получите ли вы правильные результаты?

Nate Eldredge 02.03.2024 04:58

Кроме того, является ли dmb ish правильным барьером? Внутренний разделяемый домен, опять же, обычно представляет собой ядра, совместно использующие кэш L1, и, в частности, все они обычно работают под управлением одной и той же ОС. Интересно, действительно ли ваше другое ядро ​​находится в домене внутреннего общего доступа? Если нет, то dmb ish может не сбросить кэши обратно достаточно далеко, чтобы обеспечить глобальную видимость.

Nate Eldredge 02.03.2024 05:00

@NateEldredge хорошие моменты: общая память находится в OCRAM. Индексы чтения/записи обновляются правильно, однако они обновляются только с одной стороны. Когда у меня будет время, я добавлю общий счетчик и посмотрю, правильно ли он увеличился. Барьер dmb ish вставляется компилятором. Знаете ли вы, как я могу дать указание компилятору генерировать более агрессивные инструкции dmb?

Lucan de Groot 04.03.2024 09:54

Сомневаюсь, что можно, кроме взлома компилятора. Я не думаю, что gcc действительно поддерживает использование атомики для чего-либо, кроме многопоточности, которая представляет собой несколько однородных ядер во внутренней разделяемой области. Возможно, вам придется вернуться к использованию volatile для ваших грузов и магазинов и вставлять барьеры вручную, например. asm("dmb osh" : : : "memory");

Nate Eldredge 05.03.2024 04:23

@NateEldredge У меня только что было время проверить это. Я добавил общий атомный счетчик и позволил обоим ядрам увеличить его в 1000000 раз. Полученное число не равно 2000000. Спасибо за помощь, задам свой вопрос.

Lucan de Groot 11.03.2024 08:40
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
153
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

С помощью @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 не дает сбоя, когда другое ядро ​​изменяет эту строку кэша; предположительно, он также работает только для внутреннего совместного использования.

Peter Cordes 11.03.2024 20:44

Отключение прерываний обеспечивает атомарность только в одноядерной системе. Если бы другое ядро ​​могло модифицировать переменную, x.store(x.load() + 1) не будет атомарным, и именно так volatile компилируется. И отключение прерываний даже на обоих ядрах не поможет. Если только единственный код на другом ядре, который изменяет общую переменную, не находится внутри обработчика прерываний, и вы не оставляете прерывания отключенными на достаточно долгое время, так что, если он уже был в этом обработчике, он завершит приращение до того, как вы выполните приращение.

Peter Cordes 11.03.2024 20:47

Другие вопросы по теме

На каком CORE выполняется планирование потоков ядра в многопроцессной среде IA-32 (также известной как многоядерная)?
Как запустить множество потоков/процессов с глобальным тайм-аутом
Пользовательский ThreadPool и очередь запросов
Все потоки получают одинаковые результаты от функции
Безопасно ли использование TerminateThread, если я все равно закрою процесс сразу после этого?
Многопоточность с указанным условием в Python
Усилия по распараллеливанию сценариев PowerShell. Задание не ждет, пока оно получит ответ от REST API, и завершает задание преждевременно
Необходимо ли явно передавать общую переменную в функцию потоковой передачи в Python, используя args в Threads(), или напрямую обращаться к ней?
Почему доступ к файлам в WebBroker-ISAPI-Module работает только в отдельном потоке?
Использует ли асинхронное ожидание сообщений Windows для возврата управления потоку пользовательского интерфейса?