Как стать автором
Обновить

Настраиваем сильносвязанные шины контроллера на плате Teensy 4.1

Время на прочтение10 мин
Количество просмотров1.5K
В прошлой статье мы увидели, как может возрасти скорость работы с портом, если обращаться к нему не через обычную, а через сильносвязанную шину. Причины кроются в латентности шины. В целом, у меня есть целая статья про DMA, где я рассматриваю влияние латентности на скорость работы. Там показано, что латентность – зло! Ну, а сильносвязанные шины работают без неё.



Контроллер MIMXRT1062, установленный на плате Teensy 4.1, имеет целых две сильносвязанные шины: для выборки инструкций и для работы с данными. Это позволяет работать с памятью, подключённой через них на максимальной скорости без использования кэша. Какие это даёт преимущества, я уже описывал ранее . Кроме того, мы уже ощутили эффект от работы с такими шинами в синтезируемом процессоре в этой статье.

В конце последней из указанных статей я сокрушался, что для решения проблем с латентностью нам приходится использовать синтезированную систему. А разработчики от NXP предоставили нам возможность поработать в таком же режиме и на обычном микроконтроллере. Давайте посмотрим, что нам это даст. Возни предстоит много, поэтому сегодня мы только всё настроим. Итак, начинаем…

Предыдущие статьи цикла:

  1. Запускаем программу созданную в NXP MCUXpresso на плате Teensy 4.1
  2. Teensy 4.1 через MCUXpresso. Часть 2. Осваиваем GPIO и UART

1. Немного теории


По умолчанию, карта памяти у проектов, которые мы импортируем из NXP-шных примеров, выглядит так:



128 килобайт ОЗУ DTC (подключённого через сильносвязанную шину данных), 128 килобайт ОЗУ через ITC (через сильносвязанную шину инструкций) и 768 килобайт ОЗУ, подключённого через обычную шину, с высокой латентностью.

А если мы возьмём штатный проект от Teensy, то там скрипт компоновщика задаёт всё чуть иначе. Есть вариант скрипта для аппаратуры без внешних SPI чипов:



То же самое текстом.
MEMORY
{
	ITCM (rwx):  ORIGIN = 0x00000000, LENGTH = 512K
	DTCM (rwx):  ORIGIN = 0x20000000, LENGTH = 512K
	RAM (rwx):   ORIGIN = 0x20200000, LENGTH = 512K
	FLASH (rwx): ORIGIN = 0x60000000, LENGTH = 1984K
}


Есть — для аппаратуры, где они припаяны:



То же самое текстом.
MEMORY
{
	ITCM (rwx):  ORIGIN = 0x00000000, LENGTH = 512K
	DTCM (rwx):  ORIGIN = 0x20000000, LENGTH = 512K
	RAM (rwx):   ORIGIN = 0x20200000, LENGTH = 512K
	FLASH (rwx): ORIGIN = 0x60000000, LENGTH = 7936K
	ERAM (rwx):  ORIGIN = 0x70000000, LENGTH = 16384K
}


Как видим, в обоих случаях имеется 512К сильносвязанной памяти инструкций, 512К сильносвязанной памяти данных и 512К памяти, подключённой через обычную шину. Почему так? Давайте разбираться. Сразу скажу, что на самом деле, это не совсем корректная информация. Но обо всём по порядку.

У контроллеров семейства MIMXRT1060, к которому относится и наш MIMXRT1062, имеется механизм FlexRAM. Внутри контроллера имеется мегабайт ОЗУ. Половина этого объёма намертво завязана на обычную шину. А вторую половину можно подключать к одной из трёх шин: сильносвязанной шине инструкций (будем дальше обозначать её красным цветом), сильносвязанной шине данных (обозначим зелёным), либо к обычной шине (пусть она будет обозначена синим цветом). По умолчанию, эта половина распределена именно так, как задаёт среда разработки MCUXPresso: 128 килобайт на ITCM, 128 килобайт на DTCM и 256 килобайт на обычную шину. Плюс 512 безусловных килобайт (которые не могут быть куда-то переключены), итого 256+512=768 килобайт на обычную шину.

Но мы можем включить режим перераспределения. И тогда первая половина окажется нарезана на 16 кусочков по 32 килобайта, каждый из которых мы сможем проецировать куда угодно. Причём, будучи системными программистами, распределение по умолчанию мы можем задать хоть так:


Хоть так:



Хоть, как-то подобно. Положение не важно. Важно количество. Остальное скоммутируется автоматически. Очень грубо процесс коммутации можно показать так (точный процесс есть в документации, но я выкинул всё, что отвлекает).



Стрелки покрашены только для того, чтобы в них не запутаться. Я специально взял цвета, отличные от чисто красного, чисто зелёного и чисто синего. Аппаратура выстроит адреса так, чтобы блоки располагались непрерывно в адресном пространстве каждой из шин. То есть, можно сказать, что сами блоки не имеют адреса. Он назначается при коммутации. Именно поэтому последовательность при назначении не важна, важно количество блоков, спроецированных на ту или иную шину.

По умолчанию 4 страницы спроецированы на ITCM (4*32=128), 4 страницы – на DTCM и 8 страниц – на OC (то есть, OnChip).



Правда, если быть совсем точным, то по умолчанию включён режим «коммутируем, как прописано в битах Fuse», но это выходит за рамки статьи. На Teensy 4.1 будет прописано именно так. А рассуждения, показанные выше, позволяют активировать коммутацию, сохранив соотношение, которое было сразу при включении Teensy.

2. Так откуда в скриптах Teensy не один, а полтора мегабайта ОЗУ


Если порыться по форумам, то разработчик платы Teensy рассуждает так: «Нам спущено огромное количество быстродействующей памяти, а мы вместо этого используем только малую толику, работая с основной памятью через медленную шину!» И я согласен с его возмущением. Но 32К*16 = 512К. А в его скриптах компоновщика на ITCM и DTCM в сумме уходит мегабайт! И вообще, у него в сумме память ITCM + DTCM + OC даёт полтора мегабайта, а в чипе есть только один! Ох и полазал я по документам и по форумам, пока у меня не сложилась картинка в голове.

Автор рассматривает шины ITCM и DTCM в комплексе. У него даже есть такая забавная конструкция в скрипте компоновщика:

_itcm_block_count = (SIZEOF(.text.itcm) + SIZEOF(.ARM.exidx) + 0x7FFF) >> 15;
_flexram_bank_config = 0xAAAAAAAA | ((1 << (_itcm_block_count * 2)) - 1);
_estack = ORIGIN(DTCM) + ((16 - _itcm_block_count) << 15);

Он вычисляет реально занятый участок кода, округляет его до границы 32К вверх, после чего вычисляет маску, которая раскрашивает те квадратики, показанные выше так, чтобы для кода было выделено минимально необходимое количество памяти. Остальное будет отдано под данные.

С одной стороны, он молодец. Автоматизировал всё. С другой – надо следить, чтобы суммарный объём не вылетел за 512 килобайт. Я об этом узнал, когда стал разбираться в теме. А для этого мне понадобилось заняться работой с другой средой разработки. Пока не полез – пребывал в счастливом неведении. Зачем разбираться, когда пользуешься работающей системой?

В общем, в моём решении всё будет задаваться вручную, хотя бы потому, что у нас возможности доработки скрипта компоновщика сильно ограничены (его мы будем править в следующей статье). Опять же, пусть все пользователи знают матчасть! Но в целом – я же не библиотеку пишу, а статью! Кто будет повторять мои действия, тот уже знает про оба варианта. Он может выбрать любой из путей!

3. Итак, что надо понять перед началом работы


Итого. Я должен прикинуть, какое соотношение программы и данных будет у меня. Сегодня я буду делать вариант с соотношением 50 / 50. 256 килобайт для кода и 256 для данных. Поняв принцип, по аналогии можно будет настраивать соотношение под любой частный случай.

Первое – идём и правим карту памяти в настройках. Но перед тем, как мы займёмся этим, я должен рассказать об одной особенности.

В карте по умолчанию, шина ITCM проецируется на адрес 0. А для ловли нулевых указателей невозможно обращение к зоне от 0 до 1F. В Teensy это решается через скрипт компоновщика. У нас возможности его правки при штатной работе (напомню, мы будем править его в следующей статье) ограничены. Поэтому я просто поставлю начальный адрес региона не 0, как там написано, а 0x40 (для надёжности). Тогда компоновщик точно ничего не разместит в запретной области.

Надо бы и длину сделать не 0x40000 (это как раз 256К), а вычесть 0x40. То есть, 0x3FFC0… Но это уже каждый решает в меру своего перфекционизма. Всё-таки такую константу запомнить сложно. Можно каждый раз вычислять, можно просто писать 0x40000 и надеяться, что вы никогда не напишете код под завязку.

Хотя, слышал я от нашего сотрудника, занимающегося поддержкой компилятора, историю, когда код в одни дни недели собирался, а в другие – нет. Потому что автор набил его именно под завязку. А туда, согласно применённым макросам, дата сборки автоматически вбивалась. Длина строк Sunday, Monday, Tuesday, Wednesday и т.п. – разная. Некоторые влезали, некоторые – нет. Так что случаи разные бывают. Но в примере я нахально оставлю 0x40000. А к чему это может привести – рассказал художественно.

4. Шаг 1: правим карту памяти


Первое, что надо сделать – поправить карту памяти проекта в настройках. Чтобы не переделывать её каждый раз, можно выполнить экспорт и затем – импортировать. Но на самом деле, изменений не так и много.

Ещё раз отмечу, что суммарный объём памяти в секциях ITCM и DTCM должен быть не более 512 килобайт. И менять соотношение можно с гранулярностью 32 килобайта.

Секция ITCM не должна начинаться с нулевого адреса, так как первые 0x20 байт используются для защиты от нулевых указателей. Я для надёжности в примере написал 0x40.





5. Шаг 2: правим настройки модуля MPU (Memory Protection Unit)


В функции main есть такой вызов:



То же самое текстом.
int main(void)
#else
void main(void)
#endif
{
	int i;
    BOARD_ConfigMPU();

    BOARD_InitPins();
    BOARD_BootClockRUN();
    BOARD_InitDebugConsole();


Переходим к телу этой функции и видим, что на ITCM и DTCM задаются регионы, размером 128К. Чтобы не мудрить, предлагаю заменить размер на 512К, каким бы ни был фактический объём. Голубым я выделил ориентир, по которому ищутся нужные нам строки. Хотя лучше, конечно, генерить точные константы силами скрипта компоновщика, а тут – заносить их. Но это уже тема для отдельной статьи.



То же самое текстом.
    /* Region 5 setting: Memory with Normal type, not shareable, outer/inner write back */
    MPU->RBAR = ARM_MPU_RBAR(5, 0x00000000U);
    MPU->RASR = ARM_MPU_RASR(0, ARM_MPU_AP_FULL, 0, 0, 1, 1, 0, ARM_MPU_REGION_SIZE_512KB);

    /* Region 6 setting: Memory with Normal type, not shareable, outer/inner write back */
    MPU->RBAR = ARM_MPU_RBAR(6, 0x20000000U);
    MPU->RASR = ARM_MPU_RASR(0, ARM_MPU_AP_FULL, 0, 0, 1, 1, 0, ARM_MPU_REGION_SIZE_512KB);


6. Шаг 3: правим Startup код


6.1 Зачем


Если сейчас собрать и загрузить проект – ничего не заработает. И почему – не совсем ясно. Потому что как можно разобраться, если у нас нет аппаратного отладчика? UART и GPIO тоже не настроены. И что делать? К счастью, я уже знаю причину. У меня даже была написана статья на эту тему (Поддельная «голубая пилюля»). В комментариях под ней мне был задан вопрос, зачем я потратил столько времени, если можно было просто выкинуть проблемную «пилюлю» в мусорку? А вот ответ пришёл как раз сейчас. Там у меня были средства, чтобы получить опыт. А сейчас – никаких средств… Но из опыта, по сходному поведению, сразу стало ясно, в чём дело.

GCC по умолчанию настраивает SP на конец памяти. Мы увеличили объём сегмента данных – SP прыгнул в его конец. А при старте аппаратная карта памяти всё ещё 128+128+768. Пока мы не поменяем её, SP указывает на отсутствующую память. Любая попытка сохранить что-то в стеке приведёт к аппаратному сбою!

Проверить, на самом деле, всё очень просто. Вот настройка:



Меняем End на Post Data:



Собираем, прошиваем – работает. Значит, дело в этом. Но когда данных станет много – стек снова уедет в запретную область, поэтому возвращаем End и начинаем разбираться.

6.2 Первая правка – необходимая, но не достаточная


Идёт в Startup код:



Добавляем в начало функции ResetISR() несколько строк, которые я подглядел в Teensy. Правда, потом они творчески доработаны на основе чтения других форумов… Короче, должно быть так (вставляемые строки выделены жёлтым):



То же самое текстом.
void ResetISR(void) {

    // Disable interrupts
    __asm volatile ("cpsid i");

    IOMUXC_GPR->GPR17 = 0xffffaaaa;
    IOMUXC_GPR->GPR16 |= 4;
    IOMUXC_GPR->GPR14 = 0x00AA0000;

    __asm volatile ("MSR MSP, %0" : : "r" (&_vStackTop) : );

	PMU->MISC0_SET = 1<<3; //Use bandgap-based bias currents for best performance (Page 1175)


#if defined (__USE_CMSIS)
// If __USE_CMSIS defined, then call CMSIS SystemInit code
    SystemInit();
#else
…


Здесь важно надо понять принцип формирования волшебной константы 0xffffaaaa. В Teensy она формируется автоматически скриптом компоновщика, как я показывал выше. Но на самом деле, всё просто. Вот таблица из документа:



0xFFFF – это двоичное 11 11 11 11 11 11 11 11. Восемь страниц подключаются к ITCM.

0xAAAA – соответственно, двоичное 10 10 10 10 10 10 10 10. Восемь страниц подключаются к DTCM.

У каждой страницы размер 32 килобайта.

Если в вашем коде соотношение другое – меняйте константу соответственно. Прочие константы менять не надо.

Собираем, запускаем, не работает! Вот тут мне пришлось просидеть ночь. Я знал, почему не работает, но у меня не было средств, чтобы понять, на каком шаге всё падает!

6.3 Вторая правка


После массы экспериментов выяснилось, что падает всё несколько раньше. Да-да! Вот такой ассемблерный код у функции получается после компиляции:

6000233c <ResetISR>:
6000233c:	b580      	push	{r7, lr}
6000233e:	b672      	cpsid	i
60002340:	4ba3      	ldr	r3, [pc, #652]	; (600025d0 <ResetISR+0x294>)
60002342:	f44f 002a 	mov.w	r0, #11141120	; 0xaa0000
60002346:	4aa3      	ldr	r2, [pc, #652]	; (600025d4 <ResetISR+0x298>)
60002348:	49a3      	ldr	r1, [pc, #652]	; (600025d8 <ResetISR+0x29c>)
6000234a:	645a      	str	r2, [r3, #68]	; 0x44
6000234c:	6c1a      	ldr	r2, [r3, #64]	; 0x40
6000234e:	f042 0204 	orr.w	r2, r2, #4
60002352:	641a      	str	r2, [r3, #64]	; 0x40
60002354:	6398      	str	r0, [r3, #56]	; 0x38
60002356:	f381 8808 	msr	MSP, r1

Я показал участок до установки SP… Дальше сбоев быть не должно, ведь память спроецирована на шины, указатель SP настроен… Сбой где-то на этом участке! Понимание проблемы пришло после того, как я стал сравнивать HEX файлы, получившиеся при разных сборках, при этом занося в SP не адаптивное значение, как сейчас, а константу. Тогда HEX файлы стали разливаться одним единственным байтом:



То же самое текстом.
:101FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11
:101FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01
:101FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1
:10200000000004203D23006039230060B5270060F4
:1020100035230060312300602D23006000000000A4
:102020000000000000000000000000002923006004


Значение соответствует адресу, заносимому в SP, а круглый адрес наводит на мысль, что перед нами заголовок. Собственно, если сравнить выделенное голубым с ассемблерным кодом, то видно, что перед нами адрес запуска. В общем, это обычный Cortex M заголовок!

Загрузчик берёт это значение, кладёт его в SP. Запускает нашу функцию… А какая там первая строка? Правильно:

6000233c:	b580      	push	{r7, lr}

Идёт попытка положить регистры в стек, а физической памяти там нет. Выходит, что всё виснет ещё до того, как дошло до настройки карты памяти.

Для устранения проблемы, надо добавить к функции один дополнительный атрибут, выделенный жёлтым:



То же самое текстом.
__attribute__ ((section(".after_vectors.reset")))
__attribute__ ((naked))
void ResetISR(void) {

    // Disable interrupts
    __asm volatile ("cpsid i");

    IOMUXC_GPR->GPR17 = 0xffffaaaa;
    IOMUXC_GPR->GPR16 |= 4;
    IOMUXC_GPR->GPR14 = 0x00AA0000;

    __asm volatile ("MSR MSP, %0" : : "r" (&_vStackTop) : );

	PMU->MISC0_SET = 1<<3; //Use bandgap-based bias currents for best performance (Page 1175)


#if defined (__USE_CMSIS)
// If __USE_CMSIS defined, then call CMSIS SystemInit code
    SystemInit();
#else


Собираем, запускаем – работает!

Исключительно для проформы проверяем ассемблерный код:

6000233c <ResetISR>:
6000233c:	b672      	cpsid	i
6000233e:	4ba3      	ldr	r3, [pc, #652]	; (600025cc <ResetISR+0x290>)
60002340:	f44f 002a 	mov.w	r0, #11141120	; 0xaa0000
60002344:	4aa2      	ldr	r2, [pc, #648]	; (600025d0 <ResetISR+0x294>)
60002346:	49a3      	ldr	r1, [pc, #652]	; (600025d4 <ResetISR+0x298>)
60002348:	645a      	str	r2, [r3, #68]	; 0x44
6000234a:	6c1a      	ldr	r2, [r3, #64]	; 0x40
6000234c:	f042 0204 	orr.w	r2, r2, #4
60002350:	641a      	str	r2, [r3, #64]	; 0x40
60002352:	6398      	str	r0, [r3, #56]	; 0x38
60002354:	f381 8808 	msr	MSP, r1

Ну да, никакого сохранения в стеке. Потому и заработало!

Всё! Задача решена! В принципе, данные уже попали в сильносвязанную память, так как соответствующая секция прописана первой в списке. А как разместить в сильносвязанной программе код, мы рассмотрим в следующий раз.
Теги:
Хабы:
+7
Комментарии0

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн