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