Всем привет.
Некоторое время назад мы писали о том как нам удалось запустить SIP телефон на STM32F4-Discovery c 1 Мб ROM и 192 Кб RAM) на базе Embox. Тут надо сказать, что та версия была минимальной и соединяла два телефона напрямую без сервера. Поэтому мы решили запустить более полноценный телефон со звонком через сервер, передачей голоса в обе стороны, но при этом уложиться в как можно меньший размер памяти.
Для телефона было решено выбрать приложение simple_pjsua в составе библиотеки PJSIP. Это минимальное приложение, которое умеет регистрироваться на сервере, принимать и отвечать на звонки. Ниже я сразу приведу описание того как это запустить на STM32F7-Discovery.
Как запускать
- Конфигурируем Embox
make confload-platform/pjsip/stm32f7cube
- В файле conf/mods.config задаем нужный SIP аккаунт.
include platform.pjsip.cmd.simple_pjsua_imported( sip_domain="server", sip_user="username", sip_passwd="password")
где server — это SIP server (например, sip.linphone.org), username и password — имя пользователя и пароль от аккаунта.
- Собираем Embox командой make. Про прошивку платы у нас есть на вики и в статье.
- Запускаем в консоли Embox команду “simple_pjsua_imported”
00:00:12.870 pjsua_acc.c ....SIP outbound status for acc 0 is not active 00:00:12.884 pjsua_acc.c ....sip:alexk2222@sip.linphone.org: registration success, status=200 (Registration succes 00:00:12.911 pjsua_acc.c ....Keep-alive timer started for acc 0, destination:91.121.209.194:5060, interval:15s
- Наконец, осталось вставить колонки или наушники в аудио выход, а говорить в два маленьких MEMS микрофона рядом с дисплеем. Звоним с линукса через приложение simple_pjsua, pjsua. Ну или можно любое другое типа linphone.
Все это описано на нашем вики.
Как мы к этому пришли
Итак, изначально встал вопрос о выборе аппаратной платформы. Так как было ясно, что STM32F4-Discovery не подойдет по памяти, была выбрана STM32F7-Discovery. У нее 1 Мб флэшка и 256 Кб ОЗУ (+ 64 специальной быстрой памяти, которую мы тоже будем использовать). Тоже не густо для звонков через сервер, но решили попробовать влезть.
Условно для себя задачу разделили на несколько этапов:
- Запуск PJSIP на QEMU. Это было удобно для отладки, плюс у нас там уже была поддержка кодека AC97.
- Запись голоса и воспроизведение на QEMU и на STM32.
- Портирование приложения simple_pjsua из состава PJSIP. Оно позволяет регистрироваться на SIP сервере и звонить.
- Развернуть свой собственный сервер на базе Asterisk и тестировать на нем, после чего попробовать внешние, такие как sip.linphone.org
Звук в Embox работает через Portaudio, который используется и в PISIP. На QEMU проявились первые проблемы — хорошо проигрывались WAV на 44100 Hz, а вот на 8000 что-то явно шло не так. Оказалось, что дело было в настройке частоты — по умолчанию в аппаратуре она была 44100, и у нас это программно не изменялось.
Здесь, наверное, стоит немного пояснить как вообще происходит проигрывание звука. Звуковой карте можно установить некоторый указатель на кусок памяти, из которого нужно проигрывать или записывать на заранее установленной частоте. После того как буфер заканчивается, вырабатывается прерывание, и выполнение продолжается со следующего буфера. Дело в том, что эти буферы нужно успевать заполнять заранее, пока проигрывается предыдущий. С этой проблемой мы еще столкнемся дальше на STM32F7.
Далее, мы арендовали сервер и развернули на нем Asterisk. Так как отлаживаться нужно было много, а говорить в микрофон много не хотелось, то нужно было сделать автоматическое проигрывание и запись. Для этого мы пропатчили simple_pjsua так, чтобы можно было подсунуть файлы вместо аудиоустройств. В PJSIP это делается довольно просто, так как у них есть понятие порт, которым может являться как устройство, так и файл. И эти порты можно гибко подключать к другим портам. Посмотреть код можно в нашем pjsip репозитории. В итоге, схема была следующая. На сервере Asterisk я завел два аккаунта — для Linux и для Embox. Далее на Embox выполняется команда simple_pjsua_imported, Embox регистрируется на сервере, после чего с Линукса звоним на Embox. В момент соединения проверяем на сервере Asterisk, что все соединение установлено, и спустя некоторое время должны услышать в Embox звук с Линукса, а в Линуксе сохраняем тот файл, который проигрывается из Embox.
После того как это заработало на QEMU, мы перешли к портированию на STM32F7-Discovery. Первая проблема — не влазили в 1 Мб ROM без включенной оптимизации компилятора “-Os” по размеру образа. Поэтому включили “-Os”. Далее, патчем отключили поддержку C++, так она нужна только для pjsua, а мы используем simple_pjsua.
После того как уместили simple_pjsua, решили, что шансы это запустить теперь есть. Но сначала нужно было разобраться с записью и воспроизведением голоса. Вопрос — куда записывать? Выбрали внешнюю память — SDRAM (128 Мб). Вы можете попробовать это сами:
Создаст стерео WAV с частотой 16000 Hz и продолжительностью 10 секунд:
record -r 16000 -c 2 -d 10000 -m C0000000
Проигрываем:
play -m C0000000
Тут возникло две проблемы. Первая с кодеком — используется WM8994, и в нем есть такое понятие как слот, и этих слотов 4. Так вот, по умолчанию, если это не настраивать, то при воспроизведении аудио, проигрывание происходит во всех четырех слотах. Поэтому на частоте 16000 Hz мы получали 8000 Hz, ну а для 8000 Hz воспроизведение просто не работало. Когда выбрали только слоты 0 и 2, то заработало как надо. Еще одной проблемой был аудио интерфейс в STM32Cube, в котором аудио выход работает через SAI (Serial Audio Interface) синхронно с аудио входом (не разбирался в деталях, но получается что они делят общий clock и при инициализации аудио выхода к нему как-то привязывается аудио вход). То есть запустить их по отдельности нельзя, поэтому сделали следующее — всегда работают (в том числе генерируются прерывания) аудио вход и аудио выход. Но когда в системе ничего не проигрывается, то мы просто подсовываем аудио выходу пустой буфер, а когда запускается проигрывание, то по-честному начинаем его заполнять.
Далее столкнулись с тем, что звук при записи голоса был очень тихим. Это происходит из-за того, что MEMS микрофоны на STM32F7-Discovery как-то плохо работают на частотах ниже 16000 Hz. Поэтому выставляем 16000 Hz, если даже приходит 8000 Hz. Для этого правда понадобилось добавить программное преобразование одной частоты в другую.
Далее пришлось увеличить размер кучи, которая располагается в RAM. По нашим подсчетам, pjsip требовал около 190 Кб, а у нас осталось всего около 100 Кб. Тут пришлось задействовать немного внешней памяти — SDRAM (около 128 Кб).
После всех этих правок я увидел первые пакеты между Линуксом и Embox, и услышал звук! Но звук был ужасный, совсем не такой как на QEMU, ничего нельзя было разобрать. Тогда мы задумались в чем может быть дело. Отладка показала, что Embox просто не успевает заполнять/выгружать аудио буферы. Пока pjsip обрабатывает один фрейм, успевало произойти 2 прерывания о завершении обработки буферов, что слишком много. Первой мыслью для ускорения была оптимизация компилятора, но она была уже включена в PJSIP. Второе — аппаратная плавающая точка, про нее мы рассказывали в статье. Но как показала практика, FPU не дало существенного прироста в скорости. Следующим шагом было выставление приоритетов потоков. В Embox есть разные стратегии планирования, и я включил ту, которая поддерживает приоритеты, и выставил аудио потокам самый максимальный приоритет. Это тоже не помогло.
Следующая была идея, что мы работаем с внешней памятью и хорошо бы туда переместить структуры, к которым обращение происходит крайне часто. Я провел предварительный анализ того когда и под что simple_pjsua выделяет память. Оказалось, что из 190 Кб первые 90 Kб выделяются под внутренние нужды PJSIP и к ним обращение происходит не очень часто. Далее во время входящего звонка вызывается функция pjsua_call_answer, в которой затем и выделяются буферы для работы с входящими и исходящими фреймами. Это было еще около 100 Кб. И тут мы поступили следующим образом. До момента звонка данные располагаем во внешнюю память. Как только звонок — сразу подменяем кучу на другую — в RAM. Таким образом, все “горячие” данные были перенесены в более быструю и предсказуемую память.
В итоге все это вместе позволило запустить simple_pjsua и позвонить через свой сервер. А затем уже и через другие сервера такие как sip.linphone.org.
Выводы
В итоге получилось запустить simple_pjsua с передачей голоса в обе стороны через сервер. Проблему с дополнительно потраченными 128 Кб SDRAM можно решить путем использования чуть более мощного Cortex-M7 (например, STM32F769NI с 512 Кб RAM), но при этом мы еще не оставили надежды влезть и в 256 Кб :) Будем рады, если кто-то заинтересуется, а еще лучше — попробует. Все исходники, как обычно, есть в нашем репозитории.
Автор: alexkalmuk