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

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

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

Чтобы просканировать матрицу, микроконтроллер последовательно подтягивает столбцы (выводы, помеченные как 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 я сначала использовал программатор на базе Launchpad MSP430, коих у меня имеется в наличии несколько штук, а затем это самодельное чудо (неплохо работающее, кстати), уступило место прибывшему из Китая программатору TL866CS MiniPro. Ощущения от нового программатора крайне положительные.
Очень подробно про устройство клавиатуры синтезатора и способы ее сканирования, включая один очень оригинальный способ сканирования че��ез интерфейс микроконтроллера AVR для подключения внешней микросхемы ОЗУ рассказывается на сайте OpenMusicLabs
Отчасти для получения большего контроля над планировщиком и снижения задержки (latency) при проигрывании звука, а отчасти из спортивного интереса, я решил использовать ядро с патчем PREEPMT RT, одной из основных особенностей которого является то, что прерывания также становятся «процессами», которые могут быть вытеснены планировщиком с учетом приоритета. Оригинальное ядро, поставляемое Samsung для процессора S5PV210, на базе которого построена система, базируется на ядре версии 3.0.8, судя по всему от Android. Ни один из патчей RT_PREEMPT, имеющихся на сайте проекта, предназначенных для данной версии ядра (3.0.8), не хотел накладываться на исходники без конфликтов, но в конце концов, разрешив все конфликты вручную, удалось наложить патч версии 3.0.8-rt23.
Из-за того, что в модифицированном таким образом ядре модифицированными также оказались такие базовые структуры, как spinlock и mutex, с ним перестали линковаться поставляемые в виде скомпилированных объектных файлов проприетарные драйверы некоторых периферийных устройств: видеокамер, контроллера ёмкостного тачскрина, и, что самое ужасное, аудиокодека. Вернемся к ним позже, а сейчас отключим их и попытаемся первый раз запустить плату со свежесобранным ядром реального времени и… получим моментальный kernel panic. Происходил он еще до запуска отладчика kgdb (который, как выяснилось позже, в��е равно не работал бы, даже если бы запустился), так что для отладки пришлось вставлять printf-ы в файл
Как я уже упоминал выше, одним из побочных эффектов стало то, что модуль, отвечающий за аудио ввод/вывод, перестал линковаться с новым ядром. Отчасти это было тем, что ядро с PREEMPT RT поддерживает (в версии 3.0.8) только механизм управления памятью SLAB, а изначально модуль был скомпилирован с включенным механизмом SLUB, который не поддерживается новым ядром. Однако, мне посчастливилось работать в Лаборатории Касперского, и я уговорил коллегу декомпилировать для меня файлы драйвера и кодека с помощью декомпилятора Hex-Rays для ARM, после чего удалось практически полностью воссоздать их исходный код. Практически — потому что в результате с «новым» драйвером аудиоинтерфейс стал определяться, однако из-за каких-то различий в низкоуровневой процедуре инициализации регистров микросхемы WM8960 звук проигрывался с артефактами. Какое-то время я пытался подправить свой драйвер, но потом выбрал более легкий путь — я отправил в техподдержку китайской компании EmbedSky Tech, где покупал мини-компьютер, свой патч с PREEMPT_RT, и попросил их скомпилировать для меня и выслать файлы аудиодрайвера. Ребята быстро откликнулись и прислали мне файлы, с которым звук, наконец, заработал как положено.
Кстати, пока я возился со своим декомпилированным драйвером, я обнаружил, что отладчик kgdb не работает ни с моим, ни с оригинальным ядром. Как выяснилось, для его работы требуется поддержка синхронного (polling) опроса последовательного порта, которая отсутствовала в драйвере последовательного порта Samsung (
Копаем дальше. Вторым побочным эффектом нового ядра оказалась крайне низкая, с большими «лагами», скорость работы всех четырех многострадальных последовательных портов системы на кристалле S5PV210, в результате чего была невозможна нормальная работа в терминале через последовательный порт, а также не работала как положено перепрошивка контроллера AVR, опрашивающего клавиатуру синтезатора. Я долго пытался понять в чем причина, но заметил лишь то, что ввод каждого символа в терминале приводил к генерации нескольких миллионов прерываний последовательного порта — ядро, похоже, не спешило их обрабатывать. В итоге я решил эту проблему тем, что с помощью вышеупомянутого флага IRQF_NO_THREAD сделал все прерывания последовательных портов непотоковыми. Это решение вышло не очень красивым, потому что помимо драйвера Samsung пришлось внести изменения в файлы
В оригинальном ядре, которое, как я говорил выше, поддерживает различные периферийные устройства, такие как видеокамеры, аппаратные кодеки, HDMI и т.д., из 512 МБ оперативной памяти было доступно лишь около 390 МБ, а остальное было зарезервировано для работы вышеуказанных устройств, причем всегда (даже если в процессе конфигурирования ядра они были отключены). Очень расточительно, особенно учитывая, что лишние 120 МБ оперативной памяти синтезатору очень даже не помешают для хранения сэмплов. Память резервировалась в файле
Последнее изменение, которое было внесено в ядро, касалось минимального размера буфера для аудиоданных, который в оригинале был равен одной странице памяти, что при частоте дискретизации 44100 Гц, 2 канала по 16 бит давало примерно 20 мс — многовато. Это значение было изменено в файле
Исходный код ядра с PREEMPT RT и всеми модификациями на GitHub
AVR подключен к последовательному порту платы мини-компьютера, и выплевывает в свой софтверный UART готовые MIDI-сообщения. Дабы избавить себя от необходимости писать драйверы, было принято решение использовать в качестве транспорта для всех аудио и MIDI-данных сервер JACK. Небольшое приложеньице на C подключается к последовательному порту, регистрирует себя в JACK как MIDI-OUT и начинает перенаправлять туда все полученные MIDI-сообщения, а JACK уже доставляет их в LinuxSampler. Дешево и сердито.
Такое решение также позволяет проигрывать MIDI-файлы через JACK с помощ��ю
Благодаря комментарию nefelim4ag к предыдущему посту, я узнал про существование libhybris — библиотеки, которая позволяет использовать Android-драйвера в обычной Linux-системе. После некоторых танцев с бубнами, всех подробностей которых я, к сожалению, уже не помню, мне удалось завести libhybris в своей системе и пересобрать Qt 5 и PyQt5 с поддержкой OpenGL ES 2.0, EGLFS и Qt Quick 2.0. Теперь мой пользовательский интерфейс использует Qt Quick ивыглядит в соответствии с последними модными тенденциями косит под Android 4.0.

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

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

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

Чтобы просканировать матрицу, микроконтроллер последовательно подтягивает столбцы (выводы, помеченные как 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 <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #include <string.h> #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 = 0x00 | (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 = 0x00; PORTB = 0x00; } inline void initPorts(void) { hiz(PORTA, DDRA); hiz(PORTC, DDRC); hiz(PORTE, DDRE); PORTB = 0x00; DDRB = 0xfe; DDRD = 0xff; } void resetRows(void) { /* output low */ alow(PORTC, DDRC); alow(PORTE, DDRE); /* don't touch PA7 & PA5 */ DDRA |= 0x5f; PORTA &= ~0x5f; _delay_us(10); /* back to floating input */ hiz(PORTC, DDRC); hiz(PORTE, DDRE); DDRA &= ~0x5f; } /* 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] & 0x80) { /* generate note off event */ dbg_putchar(0x90); dbg_putchar(note); dbg_putchar(0x00); /* reset key state */ keys[offset] = 0x00; } } } 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 <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/time.h> #include <unistd.h> #include <assert.h> #include <string.h> #include <sysexits.h> #include <errno.h> #include <signal.h> #include <fcntl.h> #include <termios.h> #include <jack/jack.h> #include <jack/midiport.h> #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 <input port>\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 и

Напоследок
Небольшое демо — пока только аудио, так как синтезатор сейчас находится в наполовину разобранном состоянии. Видео же будет в следующем посте, который родится скорее всего в августе, после того как приедет заказанная в Китае плата, соединяющая воедино все части синтезатора. Кроме того, следующий пост будет, скорее всего, посвящен уже не таким низкоуровневым манипуляциям с ядром, а процессу доведения до ума пользовательской части софта на 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 и, возможно, кому-то понадобится использовать с ней ядро реального времени.