В процессе переноса алгоритма распознавания маркеров на OMAP платформу я обнаружил, что начисто забыл каким образом высчитывается память, которая распределяется между ARM и DSP. Восстанавливая крупицы ценной информации, которые щедро разбросаны по разным мануалам и форумам Texas Instruments, я решил зафиксировать с таким трудом добытые и упакованные в некоторое подобие осмысленной системы данные.
Мы будем пользоваться услугами Codec Engine от TI, про эту систему обеспечения передачи данных между ARM и DSP я немного рассказал в другой статье. Там же я упомянул, что CE берет на себя заботу о выделении необходимой нам памяти, поэтому самое время рассказать о том как это делается.
Как вы понимаете, в архитектуре OMAP подсистемы ARM и DSP работают с общей памятью. Поэтому придется учитывать их требования и интересы чтобы избежать конфликтов; более того, они совместно используют разделяемую память для обмена. Ситуацию усугубляет то, что ARM Linux работает с виртуальной памятью, а DSP — с физической — читает и пишет как есть, по реальным адресам.
На самом деле, нам нужно организовать три типа памяти:
- которая нужна только ARM;
- которая нужна только DSP;
- разделяемая память, через которую ARM и DSP обмениваются данными (с любезной помощью Codec Engine).
Поехали.
ARM only
С ARM все обстоит проще всего. Здесь крутится Linux со своей системой виртуальной памяти, который самостоятельно назначает адреса из виртуального пространства. Как мы помним, в концепции виртуальной памяти ее может быть даже больше, чем количество физической. Нам нужно сделать только одну вещь — ограничить аппетиты Linux, а точнее обмануть его насчет физической памяти, доступной в системе. На моей плате BeagleBoard расположено ОЗУ емкостью 512МБ. Не мудрствуя лукаво, отдадим Linux половину.
Делается это в конфигурации загрузчика, который во время загрузки ядра передает последнему командную строку следующего вида:
1 2 3 4 5 |
Kernel command line: console=ttyO2,115200n8 mem=256M omapfb.vram=0:3M omap_vout.vid1_static_vrf b_alloc=y ethaddr=00:02:03:aa:bb:01 mpurate=auto buddy=none camera=none vram=16M omapfb.mode=dvi:640x48-24@60 omapdss.def_disp=dvi root=/dev/mmcblk0p2 rw rootfstype=ext3 rootwait |
Нас интересует параметр mem=256M, который указывает Linux на какой объем памяти он может рассчитывать. Сразу заметим, что из этой памяти 256М параметр vram=16M заберет 16 мегабайт: это размер видеопамяти для фреймбуфера. Кстати, вот еще один провал в памяти: эти строки, совместно с omapfb.vram=0:3M, omapfb.mode=dvi:640×48-24@60, omapdss.def_disp=dvi я настраивал для отображения видео через dvi и точно помню, что рассчитывал размер картинки отдельно для каждого из цветов RGB. Для того, чтобы сказать почему фреймбуферу нужно отдать именно 16 мегабайт, мне нужно будет погрузиться в аналогичные воспоминания )
После загрузки с такой командной строкой ядро отрапортует:
1 |
Memory: 240MB = 240MB total |
Все правильно, это доступная память 256М за вычетом видеопамяти 16М. Проверяем:
1 2 3 4 |
# free -h total used free shared buff/cache available Mem: 224M 54M 122M 356K 48M 158M Swap: 0B 0B 0B |
Опс… почему 224М? А потому что ядру тоже нужно место, и если мы посчитаем сколько оно занимает, в сумме получится 16М:
1 2 3 4 5 6 7 8 9 10 11 |
[ 0.000000] Virtual kernel memory layout: [ 0.000000] vector : 0xffff0000 - 0xffff1000 ( 4 kB) [ 0.000000] fixmap : 0xfff00000 - 0xfffe0000 ( 896 kB) [ 0.000000] DMA : 0xffc00000 - 0xffe00000 ( 2 MB) [ 0.000000] vmalloc : 0xd0800000 - 0xf8000000 ( 632 MB) [ 0.000000] lowmem : 0xc0000000 - 0xd0000000 ( 256 MB) [ 0.000000] modules : 0xbf000000 - 0xc0000000 ( 16 MB) [ 0.000000] .init : 0xc0008000 - 0xc0039000 ( 196 kB) [ 0.000000] .text : 0xc0039000 - 0xc0671334 (6369 kB) [ 0.000000] .data : 0xc0672000 - 0xc083ca40 (1835 kB) [ 0.000000] .bss : 0xc083ca64 - 0xc0dd2888 (5720 kB) |
Итак, из доступной на BeagleBoard памяти 512М мы отдали Linux 256М и 256М осталось для DSP и Codec Engine. В самом Linux эти 256М разошлись так: на видеопамять 16М, на ядро тоже 16М, в результате приложениям осталось 224М. Теперь смотрим дальше, что будет происходить с оставшимся объемом 256М.
DSP only
Как я уже упомянул, DSP оперирует физическими адресами в памяти. Поэтому нам нужно вооружиться шестнадцатеричным калькулятором и составить таблицу распределения памяти. Мы начнем с адреса 0x80000000, потому что начиная с него начинает стартовать шина памяти. Запомним полезное число: 0x10000000, которое соответствует размеру памяти 256М, которую мы отдали ARM. Это означает, что доступная для дальнейших экспериментов (то, что осталось после того как Linux забрал свое) область будет начинаться с 0x80000000 + 256М = 0x90000000.
Как вы уже догадались, с диапазоном адресов от 0x80000000 до 0x90000000 будет работать Linux.
Теперь нам нужно принять важное решение: какую часть из оставшейся памяти отдать DSP? На несвоевременный вопрос «почему не всю оставшуюся»? отвечаю, что нам еще нужно распределить область обмена между ARM и DSP. Об этом будет в третьем параграфе, а сейчас мы договоримся, что отдаем DSP область 0x90C00000 — 0xA0000000.
С концом памяти все понятно: это 0x80000000 + 512М, то есть конец физической памяти. Откуда взялось значение 0x90C00000? Отвечу на это, что как и все важные решения, оно было принято «отфонарным» способом. Ладно, шучу, шучу ) Почему именно такая граница, я покажу опять таки в следующем параграфе. А сейчас мы посмотрим, каким образом приложение на DSP стороне узнает о наших важных решениях.
Делается это с помощью конфигрурационного файла config.bld, интересуемый кусок которого в нашем случае будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
var evm3530_ExtMemMap = { DDRALGHEAP: { name: "DDRALGHEAP", base: 0x90C00000, len: 0x03000000, space: "data" }, DDR2: { name: "DDR2", base: 0x93C00000, len: 0x03000000, space: "code/data" }, SR1: { name: "SR1", base: 0x96C00000, len: 0x00200000, space: "data" }, SR0: { name: "SR0", base: 0x96E00000, len: 0x00200000, space: "data" } }; |
Как следует из записи, DSP использует еще более тонкую нарезку памяти для различных нужд, например DDRALGHEAP это память предназначенная для самого Codec Engine. Мы не будем сильно погружаться в эту структуру, для нас существенным является то, что для DSP будет выделена память начиная с адреса 0x90C00000 и завершит эту область сегмент SR0 (Shared Region). Поскольку сегменты идут непрерывно друг за другом (сумма базы сегмента и его длины равна базе следующего сегмента), то последний занятый адрес памяти будет 0x96E00000 + 0x00200000 = 0x97A00000.
Вот мы и определили область памяти, с которой будет работать DSP. Она начинается с физического адреса 0x90C00000 и заканчивается физическим адресом 0x97A00000. В самом коде нет необходимости помнить о физических границах. В частности, механизм Codec Engine самостоятельно выделяет память для массивов данных в этих границах, для этого он вызывает функцию следующего вида, в которой только нужно заполнить структуру memTab и указать, сколько памяти и с какими характеристиками нам нужно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
Int LW_AF_alloc(const IALG_Params *algParams, IALG_Fxns **pf, IALG_MemRec memTab[]) { /* Request memory for my object */ memTab[0].size = sizeof(LW_AF_Obj); memTab[0].alignment = 0; memTab[0].space = IALG_EXTERNAL; memTab[0].attrs = IALG_PERSIST; /* Request memory for twiddle buf */ memTab[1].size = BUFSIZE_1D; memTab[1].alignment = 8; memTab[1].space = IALG_EXTERNAL; memTab[1].attrs = IALG_PERSIST; * * * /* Request memory for img tmp buf */ memTab[6].size = BUFSIZE_2D; memTab[6].alignment = 8; memTab[6].space = IALG_EXTERNAL; memTab[6].attrs = IALG_PERSIST; return(NUMBUFS); } |
После выделения памяти, кодек отдаст нам указатели на выделенные области памяти в memtab[0].base … memtab[6].base.
ARM + DSP
Здесь начинается самое интересное. Нам нужен разделяемый массив памяти, который одновременно будет использовать как ARM, так и DSP. Через этот массив Codec Engine будет гнать данные в обеих направлениях.
Управляет распределением модуль ядра CMEM, который запускается со стороны ARM Linux. Модуль принимает параметры, через которые задаются размеры и местонахождения пулов памяти. Выглядит это примерно так:
1 2 3 4 5 6 7 8 |
MODPATH=/lib/modules/3.0.50/kernel/drivers/dsp/ # start syslink and cmem insmod $MODPATH/syslink.ko TRACE=1 TRACEFAILURE=1 # BeagleBoard mem=256M version insmod $MODPATH/cmemk.ko phys_start=0x90000000 phys_end=0x90C00000 pools=20x4096,10x131072,4x2621440 |
Перед запуском модуля cmemk.ko запускается собственно модуль syslink.ko, принадлежащий Codec Engine.
Из конфигурации CMEM видно, что используется область памяти от 0x90000000 до 0x90С00000, и это логично, потому что после 0x90С00000 идет уже область памяти эксклюзивно принадлежащая DSP. И все таки, откуда взялось это значение — 0x90С00000? Чтобы ответить на этот вопрос, обратим внимание на распределение пулов памяти, заданное параметром pools. Для моего проекта самым важным является передача сигнального и опорного 2D изображения с ARM на DSP, а также передача результата — 2D изображения в обратном направлении. Одна матрица остается в запасе, поэтому я распределил 4 пула размером 2621400 байт. В этот размер поместится байтовая черно-белая картинка размером 1280х1024 комплексных чисел.
Замечу, что поскольку CMEM — менеджер непрерывной памяти, то использовать пул кусками не получится. То есть нельзя будет в области размером 2621400 байт взять например два массива половинной длины. Один массив — один пул, такое правило. Я взял 4 пула чтобы иметь запас для матриц изображения, а для данных поменьше распределил 10 пулов по 131024 байт и 20 пулов по 4096 байт. Считаем границы:
0x96E00000 + 20 х 4096 + 10 х 131024 + 4 х 22621400 = 0x90С00000. Вот откуда взялось это значение.
Со стороны Linux указатели на эти области памяти возвращаются функциями вида
inBufSigImgArm = Memory_alloc(BUFSIZE_2D, &allocParams);
После этого указатели прописываются в специальной структуре, которая волшебным образом, с помощью Codec Engine также будет доступна со стороны DSP:
ARM:
1 2 3 4 5 6 7 8 9 |
universalInBufDesc.descs[0].bufSize = BUFSIZE_1D; universalInBufDesc.descs[1].bufSize = BUFSIZE_1D; universalOutBufDesc.descs[0].bufSize = BUFSIZE_1D; // указатели попадают в структуру universalInBufDesc.descs[0].buf = inBufSigImgArm; universalInBufDesc.descs[1].buf = inBufRefImgArm; universalOutBufDesc.descs[0].buf = outBufImgArm; |
DSP:
1 2 3 |
inBufSigImgDsp = inBufs->descs[0].buf; inBufRefImgDsp = inBufs->descs[1].buf; outBufImgDsp = outBufs->descs[0].buf; |
Вот и все. Если структура имеет тип In, то данные заполненные со стороны ARM, после вызова кодека со стороны ARM попадут в функцию, которая будет вызвана со стороны DSP. И наоборот, результат работы DSP, который в структуре имеет тип Out, будет возвращен в ARM.
В моем проекте DSP выполняет тяжелонагруженные процедуры вычисления свертки изображений, которые требуют двумерного преобразования Фурье. Конечно, целочисленная арифметика DSP в архитектуре OMAP это не совсем то, что нужно для такого проекта. Но попробовать стоило, хотя бы для того чтобы сравнить быстродействие с другой реализацией — двумерное преобразование Фурье на GPU Raspberry Pi. Об этом — в следующих материалах
Ответить