STM32F103: странная проблема с DMA (исправлено, но не решено)

Пытаюсь заюзать DMA для пересылки данных из фиксированного массива в регистр, так же находящийся в памяти по фиксированному адресу (на него отображается шина SSD1963).

Ежели адрес источника данных не инкрементируется (т.е. чтение идёт всё время из одного и того же адреса), то всё ок - на экране видим однотонную заливку (на картинке ниже синий фон именно так и создан).

Если же адрес инкрементируется (в соответствии с флагом в соответствующем регистре контроллера DMA), то каждые первые n посылок (в терминологии референс-мануала -- "токенов") портятся, но каким-то не очень случайным образом:

Ширина испорченной зоны подозрительно похожа на 32. Посылки все 16-битные (16-битных массив пересылается в 16-битный регистр). Эффект не зависит ни от стартового адреса, ни от количества данных в одной транзакции. Скорость работы шины вряд ли виновата (т.к. равномерная заливка всё-таки работает и тактовая частота микроконтроллера более чем в два раза ниже, чем тактовая на видеочипе).

Код:

    SSD1963_SetArea (sx, ex, y, y);
    RCC->AHBENR |= RCC_AHBENR_DMA1EN; 

    DMA1_Channel1->CMAR = (uint32_t)(&buffer[0]); 
    DMA1_Channel1->CPAR = (uint32_t)(&SSD1963.Data); 
    DMA1_Channel1->CNDTR = Count;

    DMA1_Channel1->CCR = DMA_CCR1_MEM2MEM | /* memory-to-memory */
            (0*DMA_CCR1_PL_1) | (0*DMA_CCR1_PL_0) | /* 11: very high priority mode, 00: low priority */
            DMA_CCR1_MSIZE_0 | /* memory size: 16bit */
            DMA_CCR1_PSIZE_0 | /* periph size: 16bit */
            DMA_CCR1_MINC | /* memory increment enabled */
            DMA_CCR1_DIR ; /* direction: read from memory */

    SSD1963.Cmd = (SSD1963_WRITE_MEMORY_START);

    DMA1_Channel1->CCR |= DMA_CCR1_EN; 
    while (!(DMA1->ISR & DMA_ISR_TCIF1)) {}; 
    DMA1_Channel1->CCR &= ~DMA_CCR1_EN; 
    DMA1->IFCR = DMA_IFCR_CTCIF1; 

    RCC->AHBENR &= ~RCC_AHBENR_DMA1EN;



Если я пересылаю ровно точно тот же массив в точно тот же регистр "вручную", но всё пишется нормально, просто медленнее. Если я запускаю программу под отладчиком (опции компиляции все точно те же) и смотрю на регистры, то там записанно именно то, что надо - и глюка пересылки данных НЕТ.

Вопрос - чё за хренотень и как с ней бороться?

Upd: нашёл. Виновата оптимизация -O3. Если её не так туго затягивать, то на -O1 всё работает как и должно работать. Я понял это, когда в отладчике стал смотреть инициализацию DMA по шагам. В сишном коде все операции записаны в том порядке, в котором они ДОЛЖНЫ быть - сначала устанавливаем регистры адресов, затем настраиваем транзакцию, затем включаем... а оптимизатор решил, что всё это неправильно и в итоге получилось так: сначала блок DMA включается на передачу, успевает что-то передать (тот самый мусор!), а затем уже исполняются инструкции, которые его настраивают и дальше высылаются корректные данные.
Поменять канал DMA не пробовали? А то у меня на STM32 периодически бывала радость, что одно и тожэ действие (запись в регистры DAC и ADC) работает на одних каналах и не работает на других. Притом для ADC1/ADC2 и ADC3 (которые практически одинаковые АЦП) это были разные каналы!
А с другими каналами вообще не работает - функция исполняется без видимых ошибок, но на экране ничего нет совсем! фантастика

Запускаю под отладчиком - всё нормально рисуется. Без отладчика - ничего не рисуется.

Edited at 2017-01-04 07:26 pm (UTC)
АХАХАХАХ

НАШЁЛ

АХАХАХАХ

виноват -O3 !! если поставить -O1, то всё работает как должно
s -O3 тоже должно работать, только нужно пометить регистры пересылаемые через дма (которые могут изменитьтся сами собой) kak volatile
дык они так и помечены
оптимизатору пофиг, он их всё равно перемешал

проблему решила обёртка функций атрибутом
void __attribute__((optimize("O0"))) SSD1963_WritePixelsLine (uint16_t sx, uint16_t ex, uint16_t y, const uint16_t *buffer)

теперь внутри обёрнутых функций код "как есть", в том же порядке
Не, ну это хрень.
Если -O3 ломает код - 99% у вас таки undefined behavior или типа того (не факт, что именно в вашем коде - возможно, в используемых хидерах). Компилятор, конечно, не безбажен, но лучше б найти, в чём дело.

Классический случай (не ваш, но удобен в качестве примера) - использование volatile bool в качестве флага для блокирования доступа к переменным (не volatile) из других потоков. Компилятор с чистой совестью меняет местами обращения к volatile и non volatile переменным, и в итоге что-то оказывается без защиты.
А делать volatile всё - отстой и тормоза. Лечится использованием compiler fence/memory fence (для gcc можно asm volatile("" ::: "memory"); - это значит, что все операции с переменными, которые есть в коде до этого места - отработают до него, а все, что после - действительно после).
Думаю, один или два таких барьера исправят вам ситуацию не хуже отключения оптимизации - и без пачки volatile (которые опять же её ломают).
с т.з. компилятора это был доступ к регистрам, побочных эффектов он не обнаружил и решил, что раз так, то давай-ка я их переставлю (для чего - х.з, факт в том, что таки переставил). Откуда ему знать, что побочный эффект на самом деле есть? Это в самом деле Undefined behavior, ибо информация о том, что происходит ВНЕ процессора и его памяти компилятору в принципе недоступна
Вроде memory barrier как раз для таких целей, отучить все от оптимизации - компилятор, сильно умный проц, итп.
privedite uzhe kod...
esli pomechaete peremennije kak volatile, togda kompiljatov voobshe ne dolzhen ih trogatj. esli dve peremennije volatile, to obe ne budet trogatj, i perestavitj mestami on ih nikak ne mozhet.
kod priveden v tekste zapisi

kompilator ne trogal peremennie. On izemnil poyadok obrasheniya k nim. Perestavit' mozhet, ochevidno - perestavil zhe.
странно это, может изменилось чего, сейчас и компиляторы изменились, давно я под гцц не писал, кроме драйверов ядра...
насколько я знаю, volatile обязывает компилер обращать внимание на переменную, даже если обращение не имеет явных побочных эффектов - т.е. он не может ВЫКИНУТЬ его из кода. Про переставить местами.. видимо, это уже не так обязательно
в листинге нету ничего про волатайл
вот это помечено как волатайл?
DMA1_Channel1 ?

просто лучше наверное данные демаркировать, покрайней мере раньше так было,
про asm volatile я вообще первый раз слышу.
да, ВСЕ переменные, отображаемые на хардварные регистры, помечены как волатайл и жёстко пришпилены к фиксированным адресам
Собственно, в применении к dma - в вашем случае (dma на вывод) строго обязателен memory fence перед запуском dma. Для dma на ввод - перед чтением полученных данных.
В либах это должно бы быть из коробки...
я решил, что пользоваться готовыми либами буду лишь тогда, когда пойму, что мне экстремально лень делать всё то же самое. Например, я сначала попробовал написать свою реализацию FAT, а затем взял FatFs. И с USB так же - поковырявшись с неделю, плюнул и допилил полуготовый код.

Ну, тогда сделайте себе макрос, дёргающий asm volatile (на x86/x64 он же процессору должен пинка давать) и пользуйтесь, чтобы сообщить компилятору о том, что происходит обращение к памяти из волшебного мира.
Потом расскажите, помогло ли... Должно бы - это стандартный путь.
я параноик. Обставил теперь все критичные куски кода всякими там прагмами (там, где я не хочу, чтобы компилер менял порядок операций, а то снова решит, что не нужно перед данными команду слать, можно и потом), атрибутами функций И этими самыми "заборами".

Век живи, век учись - ни разу не нужно было, а вот теперь без них не обойтись
Кстати, на десктопах (где мультитрединг и out of order execution - норма жизни) тоже никто о барьерах не думает - просто они "из коробки" встроены в механизмы синхронизации. Т.е. воспользовался critical section - и даже не узнал, что компилятор с процессором могли сломать твой код.
Синхронизация и барьеры — это РАЗНЫЕ вещи.

Барьер нужен потому, что команда записи в память/регистр/etc. в железе выполняется не мгновенно — особенно если это регистр на какой-нибудь периферийной шине, копошащейся на своей мелкой частоте. В результате можно одной командой записать в регистр X, потом следующей же командой его прочитать — и вместо X получить хуйню какую-нибудь, потому что X туда физически записаться ещё не успел. Хотя команды выполнились в правильном порядке и без всякого мультитрединга.

Или, например, реальное, из errata на STM32F2 — включение AHB занимает 2 такта, поэтому если мы одной командой включим AHB, а вот прямо следующей что-то попробуем записать в регистр на ней, то ничего не запишется, потому что AHB на самом деле ещё не включилась.
Тем не менее, на PC/Mac барьеры - неотъемлемая часть межпоточной синхронизации (ну, если не лезть под капот, а просто писать многопоточный код).
В user-mode мне ни разу не приходилось писать memory barrier руками - я, собственно, об этом.
Необходимая, но недостаточная часть — потому что решают ту же проблему «а переварило ли железо».

Если поток A пишет что-то в память, а поток Б из неё читает, то Б должен удостовериться, что:

1) А действительно записал
2) оно действительно физически туда записалось

DMB/DSB решают только вторую проблему, к мультитредингу и out of order это не имеет всё равно никакого отношения (на STM32F103 тоже вообще-то есть многозадачные ОС, в которых тоже надо решать задачу синхронизации потоков), ну и DMA вполне можно рассматривать как частный случай аппаратной многопоточности, во многом не хуже физического мультитрединга.
Ну да. Просто используется "высокоуровневый" примитив, включающий в себя все нужные низкоуровневые.
И это правильный подход - т.к. завтра набор низкоуровневых примитивов может оказаться совсем другим.

А с многозадачными ос понятно, они даже на avr есть (кстати, нравится мне тамошняя "critical section" из atomic.h - ничего общего с привычными с винды)
Просто используется "высокоуровневый" примитив, включающий в себя все нужные низкоуровневые

Это общий принцип нормального HAL.

На микроконтроллерах люди просто чаще работают напрямую с железом и реже — в чисто высокоуровневых средах.
после выхода golang говорить о critical section как-то стыдно, простите...
Да ладно, стыдно говорить о вещах, лежащих в том числе и в основе golang?
То, что от явной синхронизации стремятся уйти (и правильно) - ещё не делает её ненужной, а просто прячет под капот.

А вообще довольно забавно наблюдать адептов какой-то новой технологии, "отрекающихся от старого мира". Особенно - когда главные составляющие технологии не такие уж и новые (как, к примеру, в своё время отряхнули пыль с функционального программирования)
понятно что синхронизация каналов в го на барьерах и примитивах основанных на барьерах.
но вот если в качестве примера сразу приводится критикал секшен... это как-то стыдно...
Совершенно не стыдно. Из виндозных примитивов синхронизации critical section - самая базовая вещь (ну не рассматривать же в качестве основы spinlock или атомики? Спинлок ограничен в применении, а атомики вообще не про то...)
А вот почему я в качестве примера привёл винду, при том что сам уже больше 5 лет сижу на маке - самому интересно. Видать, крепко Рихтер в башке засел.
вам правильно про барьер пишут, ладно компилятор, компилятор может только порядок инструкций поменять, а может еще порядок измениться и в рантайме, если вдруг код будет выполняться на процессоре с пайплайнами...
этот код никогда не будет исполняться на процесоре с реордерингом и пайплайнами хотя бы потому, что этот код - драйвер DMA-модуля, который нигде кроме 10x серии, насколько я знаю, не используется


Edited at 2017-01-05 08:02 pm (UTC)
не лучше ли было саму отсылку в дма написать на ассемблерной вставке, чтобы было атомарно.
т.е. инлайн функция, где пишутся данные и команда в определенном порядке.
эхм.. ну вот сейчас она практически на ассемблере - чистый Си-код, обставленный всеми этими страшилками и заглушками. Компилируется без изменений порядка в аналогичную последовательность записей в регистры... так зачем париться именно с ассемблером, если можно писать на Си с тем же результатом?

бестолково как то со стороны компилятора, костыль же. В этих архитектурах добрая часть всяко разно инициализации и что каждый раз фортели ожидать?

Никому не верь.
Всегда будь начеку.
Они уже здесь. Они среди нас.
buffer выровнен по границе 32 байта? В DMA невыровненный буфер иногда приводит к чудесам. upd пишут, что адрес должен быть выровнен по transfer size, но это в общем не очень понятно. Как быть с произвольным количеством данных?
Надо бы воткнуть инструкции ISB и DSB перед запуском DMA. Окружив это дело блоком asm volatile.


Edited at 2017-01-04 10:19 pm (UTC)
выравнивание данных никак не повлияло. Это именно что "фортель компилятора", произвольно переставившего инструкции, которы с его точки зрения не имеют побочных эффектов
у Cortex-M3 вроде нет, но это не значит, что компилятор не может переставить инструкции (он как раз может и часто это делает, если "всё равно побочных эффектов нет")
Я спрашивал из-за команд ISB/DSB, самому компилятору хватит заклинания asm volatile memory.
Кстати, нагуглил - вроде культурно (так, чтобы код соответствовал вашей архитектуре) пишется __sync_synchronize - http://gcc.gnu.org/onlinedocs/gcc-4.6.2/gcc/Atomic-Builtins.html
Но не проверял.
ISB/DSB/DMB (тут необходима и достаточна последняя) к неупорядоченному выполнению отношения не имеют.

Это просто гарантия, что у процессора отработает вся внутренняя механика.
Ага, спасибо. __sync_synchronize её вызывает, я полагаю? (выглядит более правильным методом, чем руками нужный барьер расписывать)
ХЗ, я ставлю __DMB() и __DSB(), которые в CMSIS определены:

__attribute__((always_inline)) __STATIC_INLINE void __DSB(void)
{
__ASM volatile ("dsb 0xF":::"memory");
}

__attribute__((always_inline)) __STATIC_INLINE void __DMB(void)
{
__ASM volatile ("dmb 0xF":::"memory");
}
Нет. Но instruction pipeline есть, и его можно (иногда нужно) сбрасывать. Даже в какой-то давней эррате упоминалась необходимость ISB для Cortex M3 (кажется, связано было с переходом в засыпание).
Ну а DSB для профилактики тоже воткнуть не помешает, потом поможет избежать проблем, когда код будет копипаститься для Cortex M4 или M7.
Это не в «давней эррате», это в официальном руководстве ARM по работе с Cortex-M ;)

Перед уходом в спячку делается DSB, перед DMA — DMB.

В эррате знаю только про STM32F2, у которого после включения тактирования шины надо подождать, пока она реально включится — а это от 2 тактов и больше.