Новая жизнь старого синтезатора. Часть 2

Продолжение истории про старый сгоревший синтезатор, в который я пытаюсь вдохнуть новую жизнь путем полной замены железа, отвечающего за генерацию звука, на программный синтезатор, построенный на базе мини-компьютера EmbedSky E8 с Linux на борту. Как это часто бывает, между публикацией первой и второй части статьи прошло гораздо больше времени, чем планировалось, но, тем не менее, продолжим.7eaafec883eda0dca0adce08642d0110.jpg

В предыдущей части был изложен процесс выбора аппаратной платформы для нового «мозга» синтезатора с описанием технических характеристик решения, кратко освещен процесс сборки необходимых библиотек и проблем, с которыми пришлось столкнуться в процессе. Теперь же что касается железа, то мы посмотрим как устроена клавиатурная матрица синтезатора, а дальше будет больше деталей посвященных софтовой части.

Клавиатурная матрицаКлавиатурная матрица синтезатора очень похожа на обычную клавиатурную матрицу, которые многие любители микроконтроллеров уже наверняка подключали к своим Arduino. Для каждой клавиши синтезатора на ней предусмотрено от одного (в наиболее дешевых моделях) до двух (в основной массе моделей) переключателей. С помощью двух расположенных рядом переключателей, один из которых при нажатии клавиши замыкается немного раньше другого, микроконтроллер может определить условную силу, а точнее скорость, с которой клавиша была нажата, чтобы впоследствии был воспроизведен звук соответствующей громкости. Выглядит это так: 78c812c5202cf0eb8587d1a679fd4c98.jpgНа обратной стороне платы размещены диоды, которые предотвращают «ложное» считывание нажатых клавиш при одновременном нажатии нескольких клавиш. Вот фрагмент принципиальной схемы клавиатурной матрицы, на которой видны эти два переключателя и подсоединенные к ним диоды: ca5eef5d191c1c5d4e3207de541cde66.png

Чтобы просканировать матрицу, микроконтроллер последовательно подтягивает столбцы (выводы, помеченные как N) к питанию, и проверяет уровень на строках (выводы, помеченные как B). Если уровень какой-либо строки окажется высоким, значит соответствующая активному в данный момент сочетанию «столбец-строка» клавиша нажата. На схеме показана лишь часть клавиатуры — всего на ней 76 клавиш (13 строк и 6×2 колонок, что дает в сумме 156 возможных вариантов при сканировании матрицы и 25 используемых выводов микроконтроллера). Сканирование всей клавиатуры осуществляется несколько десятков раз в секунду и незаметно для исполнителя.

В моем синтезаторе микроконтроллером, ответственным за сканирование клавиатуры, изначально был 8-битный однократно программируемый микроконтроллер Hitachi HD63B05V0, работающий на частоте 8 МГц и имеющий 4 КБ ROM и 192 байта RAM памяти. К сожалению, данный контроллер оказался нерабочим после инцидента с питанием, описанного в начале первой статьи. Зато, к счастью, он оказался почти совместим по выводам с имеющимся у меня контроллером ATmega162, на который я его и заменил, перерезав и перепаяв всего лишь 2 дорожки на плате, одна из которых — это вывод RESET, оказавшийся совсем не в том месте, как у HD63B05V0.

Поскольку такое включение контроллера не позволяло мне воспользоваться встроенным UART (так как он тоже был на других выводах), то для вывода информации о нажатых клавишах я воспользовался этой односторонней (только запись) реализацией последовательного порта. Также в микроконтроллер был залит загрузчик TinySafeBoot, также использующий программную реализацию последовательного порта, для возможности будущего обновления прошивки. Поскольку в качестве языка для быстрой разработки всего высокоуровневого ПО синтезатора я выбрал Python + Qt5, то для TinySafeBoot я также написал модуль на Python, который позволяет считывать и записывать прошивку в микроконтроллер AVR. Сам микроконтроллер AVR подключен к последовательному порту UART1 на плате EmbedSky E8 и питается от напряжения 3.3V, чтобы избежать необходимости в преобразовании уровней.

Исходный код прошивки для AVR #include #include #include #include #include «dbg_putchar.h»

#define MIDI_BASE 18 #define ZERO_BASE 28 #define KEYS_COUNT 76

#define hiz (port, dir) do { \ (dir) = 0; \ (port) = 0; \ } while (0)

#define alow (port, dir) do { \ (dir) = 0xff; \ (port) = 0; \ } while (0)

uint8_t keys[KEYS_COUNT];

/* Get state of a row by its index * starting from 1 to 13 */ uint8_t getRow (uint8_t idx) { if (idx <= 8) { return (PINC & (1 << (8 - idx))); } else if (idx >= 9 && idx <= 11) { return (PINE & (1 << (11 - idx))); } else if (idx == 12) { return (PINA & (1 << PIN6)); } else if (idx == 13) { return (PINA & (1 << PIN4)); }

return 0; }

inline void activateColumn1(uint8_t idx) { PORTD = 0×00 | (1 << (8 - idx)); PORTB = 0x00; }

void activateColumn2(uint8_t idx) { if (idx <= 3) { PORTB = 0x00 | (1 << (idx + 4)); PORTD = 0x00; } else if (idx == 4) { PORTB = 0x00 | (1 << PIN4); PORTD = 0x00; } else if (idx == 5 || idx == 6) { PORTD = 0x00 | (1 << (idx - 5)); PORTB = 0x00; } }

inline void deactivateColumns (void) { PORTD = 0×00; PORTB = 0×00; }

inline void initPorts (void) { hiz (PORTA, DDRA); hiz (PORTC, DDRC); hiz (PORTE, DDRE); PORTB = 0×00; DDRB = 0xfe; DDRD = 0xff; }

void resetRows (void) { /* output low */ alow (PORTC, DDRC); alow (PORTE, DDRE);

/* don’t touch PA7 & PA5 */ DDRA |= 0×5f; PORTA &= ~0×5f;

_delay_us (10);

/* back to floating input */ hiz (PORTC, DDRC); hiz (PORTE, DDRE); DDRA &= ~0×5f; }

/* base MIDI note number is 25: C#0 */

int main (void) { uint8_t row, col, layer; uint8_t note, offset;

initPorts (); memset (keys, 0, sizeof (keys)); dbg_tx_init (); dbg_putchar ('O'); dbg_putchar ('K');

while (1) { for (layer = 0; layer < 2; layer++) { for (col = 1; col <= 6; col++) { if (!layer) activateColumn1(col); else activateColumn2(col);

for (row = 1; row <= 13; row++) { note = 6 * row + col + MIDI_BASE; offset = note - ZERO_BASE;

if (getRow (row)) { if (! layer) { /* increase velocity counter */ if (keys[offset] < 254 && !(keys[offset] & 0x80)) keys[offset]++; } else { if (!(keys[offset] & 0x80)) { /* generate note-on event */ dbg_putchar(0x90); dbg_putchar(note); /*dbg_putchar(keys[offset]);*/ dbg_putchar(0x7f); /* stop counting */ keys[offset] |= 0x80; } } } else { if (layer) continue;

if (keys[offset] & 0×80) { /* generate note off event */ dbg_putchar (0×90); dbg_putchar (note); dbg_putchar (0×00); /* reset key state */ keys[offset] = 0×00; } } }

deactivateColumns (); resetRows (); } } }

return 0; } Модуль на Python для TinySafeBoot import serial import binascii import struct import intelhex import sys

class TSB (object): CONFIRM = '!' REQUEST = '?'

def __init__(self, port): self.port = serial.Serial (port, baudrate=9600, timeout=1) self.flashsz = 0

def check (self): if not self.flashsz: raise Exception («Not activated»)

def activate (self): self.port.write (»@@@») (self.tsb, self.version, self.status, self.sign, self.pagesz, self.flashsz, self.eepsz) = \ struct.unpack (»<3sHB3sBHH", self.port.read(14)) self.port.read(2) self.pagesz *= 2 self.flashsz *= 2 self.eepsz += 1 assert(self.port.read() == self.CONFIRM)

def rflash (self, progress=None, size=0): self.check () self.port.write («f») self.addr = 0 self.flash = » size = self.flashsz if not size else size while self.addr < size: if progress is not None: progress("read", self.addr, size) self.port.write(self.CONFIRM) page = self.port.read(self.pagesz) if len(page) != self.pagesz: raise Exception("Received page too short: %d" % len(page)) self.addr += len(page) self.flash += page return self.flash.rstrip('\xff')

def wflash (self, data, progress=None): if len (data) % self.pagesz!= 0: data = data + »\xff» * (self.pagesz — (len (data) % self.pagesz)) assert (len (data) % self.pagesz == 0) self.check () self.port.write («F») self.addr = 0 assert (self.port.read () == self.REQUEST) while self.addr < len(data): if progress is not None: progress("write", self.addr, len(data)) self.port.write(self.CONFIRM) self.port.write(data[self.addr:self.addr + self.pagesz]) self.addr += self.pagesz assert(self.port.read() == self.REQUEST) self.port.write(self.REQUEST) return self.port.read() == self.CONFIRM

def vflash (self, data, progress=None): fw = self.rflash (progress, len (data)) return fw == data

def info (self): print «Tiny Safe Bootloader: %s» % self.tsb print «Page size: %d» % self.pagesz print «Flash size: %d» % self.flashsz print «EEPROM size: %d» % self.eepsz

if __name__ == »__main__»: import argparse

def progress (op, addr, total): sys.stdout.write (»\r%s address: $%0.4x/$%0.4x» % (op, addr, total)) sys.stdout.flush ()

parser = argparse.ArgumentParser () parser.add_argument («filename», help=«firmware file in Intel HEX format») parser.add_argument (»--device», help=«Serial port to use for programming», default=»/dev/ttyUSB0») args = parser.parse_args ()

tsb = TSB (args.device) tsb.activate () tsb.info () fw = intelhex.IntelHex (args.filename) assert (tsb.wflash (fw.tobinstr (), progress)) assert (tsb.vflash (fw.tobinstr (), progress)) print »\nOK\n» В качестве программатора для AVR я сначала использовал программатор на базе Launchpad MSP430, коих у меня имеется в наличии несколько штук, а затем это самодельное чудо (неплохо работающее, кстати), уступило место прибывшему из Китая программатору TL866CS MiniPro. Ощущения от нового программатора крайне положительные.Очень подробно про устройство клавиатуры синтезатора и способы ее сканирования, включая один очень оригинальный способ сканирования через интерфейс микроконтроллера AVR для подключения внешней микросхемы ОЗУ рассказывается на сайте OpenMusicLabs

Приготовление ядра с поддержкой Realtime Preemption Отчасти для получения большего контроля над планировщиком и снижения задержки (latency) при проигрывании звука, а отчасти из спортивного интереса, я решил использовать ядро с патчем PREEPMT RT, одной из основных особенностей которого является то, что прерывания также становятся «процессами», которые могут быть вытеснены планировщиком с учетом приоритета. Оригинальное ядро, поставляемое Samsung для процессора S5PV210, на базе которого построена система, базируется на ядре версии 3.0.8, судя по всему от Android. Ни один из патчей RT_PREEMPT, имеющихся на сайте проекта, предназначенных для данной версии ядра (3.0.8), не хотел накладываться на исходники без конфликтов, но в конце концов, разрешив все конфликты вручную, удалось наложить патч версии 3.0.8-rt23.Из-за того, что в модифицированном таким образом ядре модифицированными также оказались такие базовые структуры, как spinlock и mutex, с ним перестали линковаться поставляемые в виде скомпилированных объектных файлов проприетарные драйверы некоторых периферийных устройств: видеокамер, контроллера ёмкостного тачскрина, и, что самое ужасное, аудиокодека. Вернемся к ним позже, а сейчас отключим их и попытаемся первый раз запустить плату со свежесобранным ядром реального времени и… получим моментальный kernel panic. Происходил он еще до запуска отладчика kgdb (который, как выяснилось позже, все равно не работал бы, даже если бы запустился), так что для отладки пришлось вставлять printf-ы в файл init/main.c, функцию start_kernel, чтобы определить место, в котором все рушится. Таким образом выяснилось, что последнее, что успевало сделать ядро, это вызвать функцию hrtimers_init (), инициализирующую таймеры высокого разрешения и их прерывания. Этот код зависит от конкретной платформы, и в нашем случае находится в arch/arm/plat-s5p/hr-time-rtc.c. Как я уже говорил, одной из основных особенностей ядра с патчем PREEMPT RT является то, что прерывания становятся потоками. Это возможно и в обычном ядре, но ядро с PREEMPT RT по-умолчанию пытается сделать таковыми почти все прерывания. Дальнейший анализ кода показал, что для работы этих потоков используется задача kthreadd_task, которая инициализируется в самом конце функции start_kernel — гораздо позже, чем происходит инициализация таймеров. Падение же происходило из-за того, что прерывание таймера ядро пыталось сделать потоковым, в то время как kthreadd_task еще NULL. Решается это установкой для отдельных прерываний, которые не стоит делать потоковыми ни при каких обстоятельствах, флага IRQF_NO_THREAD который и был добавлен к флагам прерывания таймера в hr-time-rtc.c. Ура! Ядро загрузилось, но это еще только начало…

Как я уже упоминал выше, одним из побочных эффектов стало то, что модуль, отвечающий за аудио ввод/вывод, перестал линковаться с новым ядром. Отчасти это было тем, что ядро с PREEMPT RT поддерживает (в версии 3.0.8) только механизм управления памятью SLAB, а изначально модуль был скомпилирован с включенным механизмом SLUB, который не поддерживается новым ядром. Однако, мне посчастливилось работать в Лаборатории Касперского, и я уговорил коллегу декомпилировать для меня файлы драйвера и кодека с помощью декомпилятора Hex-Rays для ARM, после чего удалось практически полностью воссоздать их исходный код. Практически — потому что в результате с «новым» драйвером аудиоинтерфейс стал определяться, однако из-за каких-то различий в низкоуровневой процедуре инициализации регистров микросхемы WM8960 звук проигрывался с артефактами. Какое-то время я пытался подправить свой драйвер, но потом выбрал более легкий путь — я отправил в техподдержку китайской компании EmbedSky Tech, где покупал мини-компьютер, свой патч с PREEMPT_RT, и попросил их скомпилировать для меня и выслать файлы аудиодрайвера. Ребята быстро откликнулись и прислали мне файлы, с которым звук, наконец, заработал как положено.

Кстати, пока я возился со своим декомпилированным драйвером, я обнаружил, что отладчик kgdb не работает ни с моим, ни с оригинальным ядром. Как выяснилось, для его работы требуется поддержка синхронного (polling) опроса последовательного порта, которая отсутствовала в драйвере последовательного порта Samsung (drivers/tty/serial/samsung.c). Я добавил в драйвер требуемую поддержку, основанную на этом патче, после чего отладчик заработал.

Копаем дальше. Вторым побочным эффектом нового ядра оказалась крайне низкая, с большими «лагами», скорость работы всех четырех многострадальных последовательных портов системы на кристалле S5PV210, в результате чего была невозможна нормальная работа в терминале через последовательный порт, а также не работала как положено перепрошивка контроллера AVR, опрашивающего клавиатуру синтезатора. Я долго пытался понять в чем причина, но заметил лишь то, что ввод каждого символа в терминале приводил к генерации нескольких миллионов прерываний последовательного порта — ядро, похоже, не спешило их обрабатывать. В итоге я решил эту проблему тем, что с помощью вышеупомянутого флага IRQF_NO_THREAD сделал все прерывания последовательных портов непотоковыми. Это решение вышло не очень красивым, потому что помимо драйвера Samsung пришлось внести изменения в файлы serial_core.c и serial_core.h, затрагивающие вообще все последовательные порты. Потому что в ядре с PREEMPT RT нельзя использовать spin_lock_t в драйверах, которые NO_THREAD, а нужно использовать raw_spinlock_t.

В оригинальном ядре, которое, как я говорил выше, поддерживает различные периферийные устройства, такие как видеокамеры, аппаратные кодеки, HDMI и т.д., из 512 МБ оперативной памяти было доступно лишь около 390 МБ, а остальное было зарезервировано для работы вышеуказанных устройств, причем всегда (даже если в процессе конфигурирования ядра они были отключены). Очень расточительно, особенно учитывая, что лишние 120 МБ оперативной памяти синтезатору очень даже не помешают для хранения сэмплов. Память резервировалась в файле arch/arm/mach-s5pv210/mach-tq210.c, который является главной точкой сбора всей информации о конфигурации и устройствах конкретной машины (в нашем случае — платы). Комментируем выделение памяти — вызов функции s5p_reserve_bootmem, и получаем 120 МБ дополнительной памяти для работы синтезатора.

Последнее изменение, которое было внесено в ядро, касалось минимального размера буфера для аудиоданных, который в оригинале был равен одной странице памяти, что при частоте дискретизации 44100 Гц, 2 канала по 16 бит давало примерно 20 мс — многовато. Это значение было изменено в файле sound/soc/samsung/dma.c на 128 байт, после чего минимальный размер буфера уменьшился до нескольких миллисекунд без ущерба стабильности и работоспособности.

Исходный код ядра с PREEMPT RT и всеми модификациями на GitHub

Как происходит общение микроконтроллера AVR с LinuxSampler AVR подключен к последовательному порту платы мини-компьютера, и выплевывает в свой софтверный UART готовые MIDI-сообщения. Дабы избавить себя от необходимости писать драйверы, было принято решение использовать в качестве транспорта для всех аудио и MIDI-данных сервер JACK. Небольшое приложеньице на C подключается к последовательному порту, регистрирует себя в JACK как MIDI-OUT и начинает перенаправлять туда все полученные MIDI-сообщения, а JACK уже доставляет их в LinuxSampler. Дешево и сердито.Исходный код приложения-моста между последовательным портом и JACK #include #include #include #include #include #include #include #include #include #include #include #include #include #include

#define UART_SPEED B9600

jack_port_t *output_port; jack_client_t *jack_client = NULL; int input_fd;

void init_serial (int fd) { struct termios termios; int res;

res = tcgetattr (fd, &termios); if (res < 0) { fprintf (stderr, "Termios get error: %s\n", strerror(errno)); exit (EXIT_FAILURE); }

cfsetispeed (&termios, UART_SPEED); cfsetospeed (&termios, UART_SPEED);

termios.c_iflag &= ~(IGNPAR | IXON | IXOFF); termios.c_iflag |= IGNPAR;

termios.c_cflag &= ~(CSIZE | PARENB | CSTOPB | CREAD | CLOCAL); termios.c_cflag |= CS8; termios.c_cflag |= CREAD; termios.c_cflag |= CLOCAL;

termios.c_lflag &= ~(ICANON | ECHO); termios.c_cc[VMIN] = 3; termios.c_cc[VTIME] = 0;

res = tcsetattr (fd, TCSANOW, &termios); if (res < 0) { fprintf (stderr, "Termios set error: %s\n", strerror(errno)); exit (EXIT_FAILURE); } }

double get_time (void) { double seconds; int ret; struct timeval tv;

ret = gettimeofday (&tv, NULL);

if (ret) { perror («gettimeofday»); exit (EX_OSERR); }

seconds = tv.tv_sec + tv.tv_usec / 1000000.0;

return seconds; }

double get_delta_time (void) { static double previously = -1.0; double now; double delta;

now = get_time ();

if (previously == -1.0) { previously = now;

return 0; }

delta = now — previously; previously = now;

assert (delta >= 0.0);

return delta; }

static double nframes_to_ms (jack_nframes_t nframes) { jack_nframes_t sr;

sr = jack_get_sample_rate (jack_client);

assert (sr > 0);

return (nframes * 1000.0) / (double)sr; }

static double nframes_to_seconds (jack_nframes_t nframes) { return nframes_to_ms (nframes) / 1000.0; }

static jack_nframes_t ms_to_nframes (double ms) { jack_nframes_t sr;

sr = jack_get_sample_rate (jack_client);

assert (sr > 0);

return ((double)sr * ms) / 1000.0; }

static jack_nframes_t seconds_to_nframes (double seconds) { return ms_to_nframes (seconds * 1000.0); }

static void process_midi_output (jack_nframes_t nframes) { int t, res; void *port_buffer; char midi_buffer[3]; jack_nframes_t last_frame_time;

port_buffer = jack_port_get_buffer (output_port, nframes); if (port_buffer == NULL) { printf («jack_port_get_buffer failed, cannot send anything.\n»); return; }

jack_midi_clear_buffer (port_buffer);

last_frame_time = jack_last_frame_time (jack_client); t = seconds_to_nframes (get_delta_time ());

res = read (input_fd, midi_buffer, sizeof (midi_buffer)); if (res < 0 && errno == EAGAIN) return; res = jack_midi_event_write(port_buffer, t, midi_buffer, 3);

if (res!= 0) { printf («jack_midi_event_write failed, NOTE LOST.»); } }

static int process_callback (jack_nframes_t nframes, void *notused) { if (nframes <= 0) { printf("Process callback called with nframes = 0; bug in JACK?"); return 0; }

process_midi_output (nframes); return 0; }

int connect_to_input_port (const char *port) { int ret;

ret = jack_port_disconnect (jack_client, output_port);

if (ret) { printf («Cannot disconnect MIDI port.»);

return -3; }

ret = jack_connect (jack_client, jack_port_name (output_port), port);

if (ret) { printf («Cannot connect to %s.», port);

return -4; }

printf («Connected to %s.», port);

return 0; }

static void init_jack (void) { int i, err;

jack_client = jack_client_open («midibridge», JackNullOption, NULL);

if (jack_client == NULL) { printf («Could not connect to the JACK server; run jackd first?»); exit (EXIT_FAILURE); }

err = jack_set_process_callback (jack_client, process_callback, 0); if (err) { printf («Could not register JACK process callback.»); exit (EXIT_FAILURE); }

char port_name[32]; snprintf (port_name, sizeof (port_name), «midi_out»); output_port = jack_port_register (jack_client, port_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0);

if (output_port == NULL) { printf («Could not register JACK output port '%s'.», port_name); exit (EXIT_FAILURE); }

if (jack_activate (jack_client)) { printf («Cannot activate JACK client.»); exit (EXIT_FAILURE); } }

static void usage (void) { fprintf (stderr, «usage: midibridge -a \n»); exit (EXIT_FAILURE); }

int main (int argc, char *argv[]) { int ch; char *autoconnect_port_name = NULL;

while ((ch = getopt (argc, argv, «a:»)) != -1) { switch (ch) { case 'a': autoconnect_port_name = strdup (optarg); break; default: usage (); } }

input_fd = open (»/dev/ttySAC1», O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK); if (input_fd < 0) { fprintf(stderr, "Cannot open serial port %s\n", strerror(errno)); return EXIT_FAILURE; }

init_serial (input_fd);

init_jack ();

if (autoconnect_port_name) { if (connect_to_input_port (autoconnect_port_name)) { printf («Couldn’t connect to '%s', exiting.», autoconnect_port_name); exit (EXIT_FAILURE); } }

getc (stdin);

return 0; } Такое решение также позволяет проигрывать MIDI-файлы через JACK с помощью jack-smf-player, который я скомпилировал для ARM и WAV/MP3 через mplayer с поддержкой вывода звука в JACK.

Бонус Благодаря комментарию nefelim4ag к предыдущему посту, я узнал про существование libhybris — библиотеки, которая позволяет использовать Android-драйвера в обычной Linux-системе. После некоторых танцев с бубнами, всех подробностей которых я, к сожалению, уже не помню, мне удалось завести libhybris в своей системе и пересобрать Qt 5 и PyQt5 с поддержкой OpenGL ES 2.0, EGLFS и Qt Quick 2.0. Теперь мой пользовательский интерфейс использует Qt Quick и выглядит в соответствии с последними модными тенденциями косит под Android 4.0.6626e8dd950794cef42ff300f752b068.png

Напоследок Небольшое демо — пока только аудио, так как синтезатор сейчас находится в наполовину разобранном состоянии. Видео же будет в следующем посте, который родится скорее всего в августе, после того как приедет заказанная в Китае плата, соединяющая воедино все части синтезатора. Кроме того, следующий пост будет, скорее всего, посвящен уже не таким низкоуровневым манипуляциям с ядром, а процессу доведения до ума пользовательской части софта на PyQt5 и QtQuick и, конечно, демонстрации получившегосяЕсли кому-то интересно:

Список всего ПО, которое было кросс-компилировано для ARM alsa-lib-1.0.27.2 alsa-utils-1.0.27.2 libaudiofile-0.3.6 dbus-1.8.0 dropbear-2014.63 fftw-3.3.3 fluidsynth-1.1.6 fontconfig-2.11.0 freetype-2.5.3 glib-2.34.3 libicu-52.1 jack-audio-connection-kit-0.121.3 jack-smf-utils-1.0 libffi-3.0.13 libgig-3.3.0 libgig-svn libhybris libsamplerate-0.1.8 libsndfile-1.0.25 linuxsampler-1.0.0 linuxsampler-svn mplayer SVN-r36900–4.4.6 openssl-1.0.0l psutil-1.2.1 pyjack-0.5.2 PyQt-gpl-5.2 pyserial-2.7 Python-2.7.6 strace-4.8 tslib-1.4.1 Если вам потребуется собрать что-то из этого списка и возникнут проблемы, я с удовольствием поделюсь опытом. Кроме того, многое из сказанного здесь справедливо для другой популярной платформы под названием FriendlyARM Tiny210, которая построена на базе того же самого процессора S5PV210 и, возможно, кому-то понадобится использовать с ней ядро реального времени.

© Habrahabr.ru