Yet another contribution. В общем :)
Зашёл очередной фикс. Поломашка в master, и скорее всего никого пока не задела. Кроме отмороженных, кто сидит на ночных сборках или собирает сам:
Больше фиксов хороших и разных!
UPD: и практически следом:
Наконец-то починили. Джва года ждал:
В Konsole есть такая возможность: при возникновении сигнала в терминале, который генерируется, например, так:
или так:
сделать какие-то действия.
Настраивается: Меню (если выключено: Ctrl+Shift+M) → Настройка → Настроить уведомления…
Нас интересуют:
- Сигнал в активном сеансе
- Сигнал в неактивном сеансе
Что бы выделить терминал в панели задач, нужно включить пункт “Выделить программу в панели задач” и нажать Применить или Ок.
Собственно, что это даёт: когда сигнал возникнет, то Konsole выделит, подсветит кнопку приложения в панели задач, примерно так:

При этом сделает это на текущем экране, вне зависимости от того, на каком рабочем столе находится приложение. А если кликнуть по ней, то автоматически активируется окно с переключением на соответствующий рабочий стол.
Плюсом, что сигнал может быть и на удалённом сервере, при подключении через SSH: терминал обрабатывает событие локально.
А удобство в том, что можно запустить длительный процесс сборки и переключиться на чтение документации или кода. Или котиков в интернете смотреть. Не важно, главное, что окно покажет, что процесс завершился и стоит глянуть результат.
Запуск примерно так:
Не стоит ставить &&
, или ||
: мы же хотим среагировать при любом коде возврата make
? Но если нужно именно для успешного или наоборот неуспешного завершения сигнализировать, то карты вам в руки.
На днях поборол жабу, и позволил себе купить на замену стокового системного HDD HGST Travelstart Z7K500 / 500Гб, новый SSD Samsung SSD 870 EVO.
Первое впечатление - систему стероидами накачали. А дальше - увлекательная процедура миграции нужного и не очень в новое окружение.
После неудачного удаления всех точек и треков с навигатора, решил, что автоматический бекап это наше всё.
Сделал средствами Systemd.
Для начала определяемся с местом для бекапа, пусть это будет ~/Documents/GPX
и создаём там скрипт ~/Documents/GPX/sync.sh
. А внутре… у ней неонка:
#!/bin/sh
#
# Ref:
# https://askubuntu.com/questions/25071/how-to-run-a-script-when-a-specific-flash-drive-is-mounted
#
# `systemctl list-units -t mount`
#
# "~/.local/share/systemd/user/garmin-backup.service"
#
#
# ```
# [Unit]
# Description=Backup GARMIN GPSMAP 64s GPX data
# # Use output from the: `systemctl list-units -t mount`
# Requires=run-media-hatred-GARMIN.mount
# After=run-media-hatred-GARMIN.mount
#
# [Service]
# ExecStart=/home/hatred/Documents/GPX/sync.sh
#
# [Install]
# WantedBy=run-media-hatred-GARMIN.mount
# ```
#
# `systemctl --user start garmin-backup`
# `systemctl --user enable garmin-backup`
#
# Monitor:
#
# ```sh
# journalctl --user --unit garmin-backup -f
# ```
#
prefix="/run/media/hatred/GARMIN/Garmin/GPX"
dir=$(dirname $0)
cd "$dir"
echo "Work dir: " $(pwd)
sleep 2
stat "$prefix" 2> /dev/null && \
rsync -rubv --suffix=.old "$prefix" . && \
touch sync_last.txt && \
notify-send -u normal -a "Garmin Sync" "Garmin GPSMAP 64s" "Backup done"
Да, я не разбирался с переменными для имени пользователя, поэтому указывал полные пути с учётом своего рабочего аккаунта: hatred
. Т.е. нужно будет скорректировать под себя.
Следующий шаг: сам юнит. ~/.local/share/systemd/user/garmin-backup.service
:
# Ref:
# https://askubuntu.com/questions/25071/how-to-run-a-script-when-a-specific-flash-drive-is-mounted
#
# systemctl list-units -t mount
# systemctl --user start garmin-backup.service
# systemctl --user enable garmin-backup.service
#
# journalctl --user --unit garmin-backup -f
#
[Unit]
Description=Backup GARMIN GPSMAP 64s GPX data
Requires=run-media-hatred-GARMIN.mount
After=run-media-hatred-GARMIN.mount
[Service]
ExecStart=/home/hatred/Documents/GPX/sync.sh
[Install]
WantedBy=run-media-hatred-GARMIN.mount
Тут главное, это запуститься, когда увидим, что нужно устройство примонтировалось. У меня, когда монтируется GARMIN, он использует метку GARMIN для своего внутреннего хранилища (куда и пишутся GPX файлы). Путь для монтирования выходит /run/media/$USER/GARMIN
.
Systemd создаёт временный сервис, на который можно сослаться. Если просто, то возьмите путь монтирования, у меня это /run/media/hatred/GARMIN
, замените все /
на -
(кроме первого) и добавьте в конце .mount
: run-media-hatred-GARMIN.mount
. Ну или смотрим вывод:
systemctl list-units -t mount
после монтирования раздела GARMIN.
А за подробностями по ссылке:
После этого включаем сервис:
systemctl --user enable garmin-backup.service
systemctl --user start garmin-backup.service
Последить за работой:
journalctl --user --unit garmin-backup -f
“Никогда такого не было и вот опять” (с). В этот раз зависание имеет место быть, если воткнуто какое-то USB устройство.
Я уже толком не вспомню ход разбирательства, но меня смущал тот факт, что на 5.10 этой проблемы изначально не было и нет на более ранних LTS ядрах, которые имеют место быть в Manjaro (вот тот самый случай, когда несколько ядер это лютый вин).
Первая мысль: помирает железо. Тем более, что в какой-то момент стало невозможно работать с мышкой Logitech MX Master, подключенный по BT. BT на ThinkPad T530 подключен по USB. Невозможность заключалась в том, что она постоянно отваливалась и подключалась опять. Лаги очень раздражали.
Потом что-то меня навело на мысль, посмотреть на параметры модулей-драйверов USB:
xhci_hcd
- USB 3.0 (или по модному: USB 3.2 Gen1)
ehci-pci
- USB 2.0
Больше интересовало второе, так как все проблемные подключенцы были на USB 2.0. Выхлоп примерно такой:
$ modinfo ehci-pci
name: ehci_pci
filename: (builtin)
license: GPL
file: drivers/usb/host/ehci-pci
author: Alan Stern
author: David Brownell
description: EHCI PCI platform driver
И тут меня заинтересовала эта строчка:
Это означает, что модуль встроен в ядро. К слову, даже если он встроен, его параметры и интерфейс доступны через /sys/module/
.
Решил глянуть, как обстоят дела на 5.4, а там модуль собран отдельно:
$ find /lib/modules/5.4.176-1-MANJARO/ -name '*ehci*.ko*'
/lib/modules/5.4.176-1-MANJARO/kernel/drivers/usb/host/ehci-pci.ko.xz
/lib/modules/5.4.176-1-MANJARO/kernel/drivers/usb/host/ehci-hcd.ko.xz
/lib/modules/5.4.176-1-MANJARO/kernel/drivers/usb/host/ehci-fsl.ko.xz
/lib/modules/5.4.176-1-MANJARO/kernel/drivers/usb/host/ehci-platform.ko.xz
Что это означает? Всё, что встроено в ядро может инициализировать на самых ранних стадиях старта системы, ещё на этапе работы с RAM-диском (initrd) и даже вообще до подключения корневой файловой системы. А модули уже подгружаются или с initrd образа или уже с основной файловой системы, тут как настроено.
Я уже сталкивался с ситуацией по работе, когда подобное различие влияло на работу системы. Получалось, что модуль, ответственный за работу одной железки стартовал достаточно рано, когда блок, генерирующий тактовый сигнал (“клок”) не неё ещё не был проинициализирован и завершал свою инициализацию с ошибкой. Типичное состояние гонки. Да, на этот случай предусмотрен специальный код возврата EPROBE_DEFER
и он не был использован в этот раз чисто по ошибке. Но дело в том, что когда драйвер был собран модулем, то он успешно грузился на более поздних стадиях загрузки системы и клок был проинициализирован и устройство запускалось нормально.
Собственно, на этом месте, у меня, похоже, и зародилась мысль: слишком ранняя инициализация, возможно что-то не стабилизировалось и нужно попытаться как-то переинициализировать железку после старта.
Самый просто способ: вынести драйвера в виде модулей. Но пересобирать ядро не хотелось. Поэтому включил логику: почти все драйвера предлагают функционал bind/unbind. Устройства на PCI шине (по крайней мере на PCIe) могут быть пересканированы (т.е. чисто теоретически PCIe - Hot Plug, но лучше не проверяйте :)). А что станет, если после загрузки, отключить железку от драйвера и потом подключить опять? Почти всё железо содержит функционал Reset настроек в исходное состояяние, что будет если сбросить и сконфигурировать всё, когда система уже готова?
Сказано - сделано. И вы не поверите: после загрузки отключил USB контроллеры, подключил обратно и попытался сделать suspend. И сработало! Ядро 5.15 - resume заработал.
К слову, на 5.16 сломано более сильно. И WA ниже не помогает. Сижу на 5.15.
Более того, вылечился BT и мышка перестала отваливаться при работе (при работе через Logitech Unify Receiver она и так работала хорошо, но теперь и по BT стала - плюс один порт свободен).
Код под катом.
Что текущий основной парсер, что грядущий (или нагрянувший, как посмотреть) парсер на clangd работают поверх libclang и сотоварищи и умеют разбирать то, что умеет собирать clang.
Внезапно, он умеет не всё. И это не всё, к примеру, отлично умеет GCC. Вот то, с чем я столкнулся сам:
Под катом будет хак, который я использовал, что бы запуститься и работать с этим добром. UPD: хак больше не работает, так как этот код вообще удалён из репозитория QtC ветки master на момент 23.05.2022.
Но сейчас не об этом. Просматривая изменения в апстриме, наткнулся на этот коммит:
И визуально это выглядит так:

Похоже, жизнь станет проще. К сожалению, мне сейчас не на чем проверить. Как только дойдут руки, то обязательно это сделаю. Как минимум, есть ещё нюанс с неподдерживаемыми параметрами командной строки, но оно сейчас обходится так:
# Hack QtC Clang:
# https://bugreports.qt.io/browse/QTCREATORBUG-22329
export QTC_CLANG_CMD_OPTIONS_BLACKLIST="-mxl-soft-mul;-mlongcalls;-fstrict-volatile-bitfields"
Данная функциональность, судя по этому списку изменений будет доступна в Qt Creator 7.0, т.е. совсем скоро:
Ну а ниже, как обещал - костыль. Требует пересборки Qt Creator из исходников.
Тут обновлял свой CMakeProjectManager2 и краем глаза заметил добавление файла dist/changes-7.0.0.md. Решил перечитать (с некоторыми изменениями я сталкиваюсь регулярно, так как живу на master-ветке).
Там фигурирует:
### C++
* Switched to Clangd by default (QTCREATORBUG-22917)
И таки да, они переключились на кодовую модель от Clangd. Оно появилось ещё раньше, где-то к версии 6.0, но была выключена. Можно ознакомиться с анонсом из первоисточника: Qt Creator and clangd: An Introduction. Или почитать комментарии к соответствующей новости на Phoronix.
В частности, они там объясняют причины. Основная (как мне кажется): libclang больше активно не развивается и для полноценной навигации, рефакторинга и прочего приходится держать две модели.
Там же они рекомендуют использовать самую последнюю версию clangd. Так что ссылки ниже ваши други:
Ну а теперь мои личные впечатления… Точнее табличка, которую я для себя составил. Плюсы и минусы.
В 2016 году (а в 2020 дошли по Большой Пионерской) уже была попытка добраться до Острой по хребту, пробираясь по “милым Приморским рощам” (они же: “Южный Лес”). В этом году решил закрыть этот гештальт, плюс размять ногу после микротравмы на пробежке.
Встречайте: GeoCrop v2.0
Нового функционала не завезли, но обновил для сборки с современными Proj. Сейчас актуальная версия API - 6. Проверял с Proj 8.0.1.
Итак:
- CMake: почищен файл, приведён чуточку к виду Modern CMake.
- Для сборки нужен Proj ≥ 6.0, так как используется его API теперь. Proj4 не поддерживается, если нужно, то берите код по метке v1.0
- Исправлен досадный баг с полигоном обрезки: последним элементом нужно указать начальную точку, что бы полигон закрылся. Старые версии Gdal ругались (точно не помню), но игнорировали и обрезали, моя текущая версия (3.3.1 на момент публикации) уже вываливается с ошибкой.
Задача: сделать USB стик с Ubuntu, но так, что бы состояние сохранялось. Между перезагрузкам.
Забегая вперёд, это делается, но с одним ограничением: ядро не меняется. Но под мои задачи этого достаточно.
И смешно и грустно :)
Итак, начнём.
Мне нужно было скачать одну статью с информацией по ковидной тест-системе (пытался понять значение КП, которое сам получил на тесте). И располагалась она на сайте: Электронный журнал “Справочник заведующего КДЛ”, который является частью сервисов Action Media: https://action-media.ru и использует для входа единый аккаунт. На статью перешёл с сайта производителя тест системы Векто-Бест.
Доступ к журналу платный, но предоставляется пробный бесплатный доступ на 3 или 4 дня.
При регистрации требую телефон и почту. Но так как для меня этот сайт является мусорным, то оставлять свой основной мейл и телефон, ровно как и мои реальные ФИО мне нет никакого резона, то я воспользовался сервисом https://smska.us/. Взял второй в списке и ввёл.
Скачал статью и успокоился. Но они начали закидывать меня тоннами спама. Решил снести аккаунт. Но, как это полагается у подобных сайтов, кнопки удаления нет. Полез в саппорт и вот такой вот диалог состоялся:
Я: День добрый! Как удалить свой аккаунт?
Бот помощник: Здравствуйте! Если удалить учетную запись, вы больше не сможете зарегистрироваться на сайтах «Актион» и получать приглашения на закрытые мероприятия. Отписаться от рассылок можно в разделе «Рассылки».
Не переживайте, если после отписки придет несколько писем. Нам нужно время, чтобы остановить рассылки.
Вы получили ответ на свой вопрос?
Я: Нет, нужен менеджер
Елена Табачукова: Здравствуйте, Алекс , рады видеть Вас на нашем сайте
Я: День добрый! Нужно удалить аккаунт
Кристина Вырлан: Здравствуйте, Алекс , рады видеть Вас на нашем сайте
Минуту, пожалуйста
Алекс, к сожалению технически удалить аккаунт невозможно, так как ранее проходили обучение и хранятся сертификаты на данном аккаунте
Я: Я не проходил у вас обучения и сертификатов нет. Я уже 5 минут кликаю на отписку от чёртовой горы подписок и ещё не дошёл до конца. С таким отношением в печь такие сервисы. Спасибо. Запросите технический персонал (разработчиков), что бы удалили записи в базе данных по e-mail ZXXXXXY@inbox.ru. Надеюсь на понимание.
Кристина Вырлан: Алекс, ранее данный аккаунт принадлежал ХмХХХву Роману, Вы переделали аккаунт на себя и свою почту, просьба вернуть все данные обратно, в таком случае все данные и Ваш аккаунт удалиться и Вы вернете аккаунт Роману
Я: Вы по телефону смотрите? :) Это телефон сервиса Онлайн-СИМ, то что нужно для регистрации на мусорных сайтах
Кристина Вырлан: Screenshot_5.png 0.03Mb
Вы стерли чужой аккаунт , сменив все данные на себя
На скрине - логи системы с изменением настроек профиля :) скрины переписки под катом.
Короче, Роман сфейлился, указав левый номер, потом работал с аккаунтом. Там пачка платёжных документов, пройденные курсы, сертификаты.
Будьте внимательны и осторожны :)
Цитата:
Many functions in the kernel sleep (ie. call schedule()) directly or indirectly: you can never call them while holding a spinlock, or with preemption disabled. This also means you need to be in user context: calling them from an interrupt is illegal.
Неделю достаточно трудоёмкой (в плане воспроизведения) отладки пришлось потрать по причинам:
- Незнания информации из цитаты выше (компетентность? не, не слышал).
- Наличия неявной блокировки в недрах ALSA при вызове колбека
snd_pcm_ops::trigger()
.
Про второй пункт попадалась информация:
Run under stream lock
Этот самый “stream lock” обеспечивается семейством функций:
snd_pcm_stream_lock()
/snd_pcm_stream_unlock()
snd_pcm_stream_lock_irq()
/snd_pcm_stream_unlock_irq()
snd_pcm_stream_lock_irqsave()
/snd_pcm_stream_unlock_irqsave()
И вводит понятия: “atomic context” and “non-atomic context”.
Контекст определяется значением флага substream->pcm->nonatomic
(bool snd_pcm::nonatomic
).
Если не сказано иного, то там значение 0, или “atomic context”.
И этот самый контекст вляет на то, какой тип блокировки будет использоваться: mutex или spin lock.
По неудачному стечению обстоятельств в колбеке snd_pcm_ops::trigger()
появился вызов cancel_delayed_work_sync()
. А контекст был объявлен как “atomic”. И работать всё стало очень весело, в зависимости от того, куда какое нейтрино попадёт :)
Проявлялось по разному:
- случайные зависания при закрытии аудио устройства
- случайные зависания при перезагрузке (если устройство было открыто)
В терминал при этом вылетало такое:
Requesting system reboot
[ 56.034544] watchdog: BUG: soft lockup - CPU#3 stuck for 23s! [init:7475]
А дальше вариции:
-
Стектрейсы, например:
[ 52.084474] Modules linked in: sch_htb xlnx_sdi_audio regmap_mmio v_comp(O) v_scaler(O) v_process(O) snd_soc_xlnx_audio_formatter snd_soc_xlnx_i2s mali(O) xilinx_dp_card xilinx_dp_pcm xilinx_dp_codec snd_soc_tlv320aic3x pwm_cadence(O) max6966_keys(O) max6966_backlight(O) max6966_leds(O) max6966_mfd(O) fbili9341(O) it666x(O) xilinx_hdmi_rx(O) xilinx_vphy(O) xilinx_sdirxss u_dma_buf(O) uio_pdrv_genirq dmaproxy(O) al5d(O) xlnx_vcu_clk al5e(O) allegro(O) xlnx_vcu xlnx_vcu_core xlnx_pl_snd_card snd_soc_core snd_pcm_dmaengine snd_pcm snd_timer snd soundcore xilinx_vpss_scaler xilinx_video videobuf2_v4l2 v4l2_fwnode videobuf2_common videobuf2_dma_contig videobuf2_memops videodev media
[ 52.144543] CPU: 1 PID: 6938 Comm: init Tainted: G O 4.19.0-xilinx-v2019.2 #83
[ 52.153054] Hardware name: Epiphan X3 (DT)
[ 52.157136] pstate: 20000005 (nzCv daif -PAN -UAO)
[ 52.161915] pc : smp_call_function_single+0x140/0x170
[ 52.166952] lr : smp_call_function_single+0x110/0x170
[ 52.171985] sp : ffffff802e0f3bb0
[ 52.175283] x29: ffffff802e0f3bb0 x28: ffffffc03d3eac00
[ 52.180586] x27: 0000000000000002 x26: ffffff8008b887c8
[ 52.185881] x25: ffffff8008b9a9a0 x24: ffffff8008125800
[ 52.191185] x23: 0000000000000002 x22: ffffff8008b9a9c0
[ 52.196480] x21: ffffffbebffc7b58 x20: ffffff8008b88548
[ 52.201774] x19: ffffff802e0f3be0 x18: 0000000000000000
[ 52.207069] x17: 0000000000000000 x16: 0000000000000000
[ 52.212364] x15: 0000000000000000 x14: 0000000000000000
[ 52.217659] x13: 0000000000000000 x12: 0000000000000000
[ 52.222953] x11: 0000000000000000 x10: 0000000000000000
[ 52.228248] x9 : 0000000000000000 x8 : 0000000000000000
[ 52.233543] x7 : 0000000000000000 x6 : ffffff802e0f3be0
[ 52.238838] x5 : ffffff802e0f3be0 x4 : 0000000000000004
[ 52.244133] x3 : ffffff802e0f3bf8 x2 : 0000000000000000
[ 52.249428] x1 : 0000000000000003 x0 : 0000000000000000
[ 52.254723] Call trace:
[ 52.257155] smp_call_function_single+0x140/0x170
[ 52.261844] perf_event_exit_cpu_context+0x84/0xe0
[ 52.266624] perf_reboot+0x2c/0x60
[ 52.270011] notifier_call_chain+0x54/0x90
[ 52.274099] blocking_notifier_call_chain+0x54/0x80
[ 52.278960] kernel_restart_prepare+0x1c/0x40
[ 52.283299] kernel_restart+0x14/0x60
[ 52.286945] __se_sys_reboot+0xd4/0x1f0
[ 52.290764] __arm64_sys_reboot+0x18/0x20
[ 52.294758] el0_svc_handler+0xc8/0x120
[ 52.298584] el0_svc+0x8/0xc
-
Периодические репорты:
[ 62.477485] BUG: workqueue lockup - pool cpus=2 node=0 flags=0x0 nice=0 stuck for 36s!
В общем, рекомендую к внимательному чтению:
Только не смотрите на год :)
И перед тем, как ставить msleep()
/mutex_lock()
/etc, изучите, кто вызывает этот код, есть ли там блокировка и какая. А если код ваш, посмотрите на шпаргалку по выбору типа блокировки.
PS а если полезно под рукой иметь глоссарий. В частности он объяснил мне, что это за функции с суффиксом _bh
и что такое tasklet.
Тот самый случай, когда WA ломается.
Причина проста: наличие параметра ядра scsi_mod.scan=sync
.
По умолчанию его нет, поэтому и проблема не слишком актуальна. А появился он… как WA проблемы с тем же самым Resume after Speep. Но тогда рушился XOrg:
Таким образом, наличие этой опции на ядре 4.15+ решает проблему с выходом из сна и падением XOrg и никак не мешает работе вплоть до ядер 5.9.x, но начиная с ядра 5.10 эта опция должна быть удалена, иначе уже она является причиной зависания при выходе из сна.
Скорее всего такое поведение тоже является ошибочным
Возникла задача: запускать тест, только если явно указано, что его нужно запускать. Иными словами пропускать его при обычным прогоне, без параметров.
Тестовый фреймворк GTest.
GTest, как и многие другие имеет возможность фильтровать тесты, иными словами, выбирать только те, которые нужно запустить в данный момент времени:
./tests --gtest_filter=Categoty.some_test
Если фильтр не указывается, то запускаются все тесты. При этом, у Google Test есть макрос, который в рантайме позволяет исключить тест из прогона:
Остаётся придумать, как передать условие, по которому этот макрос вызовется.
Дополнительный параметр командной строки заводить не хотелось, тем более, что уже есть фильтр… Оказалось, что достучаться к тому, что передано для --gtest_filter=
в качестве параметра проще простого:
std::string str = ::testing::GTEST_FLAG(filter);
Сама строка фильтра: набор масок, разделённых двоеточием. Собственно, нам достаточно проверить, что фильтр не содержит имени нашего теста и в этом случае скипнуть его:
TEST(Category, some_test) {
std::string str = ::testing::GTEST_FLAG(filter);
auto enable_pos = str.find("Category.some_test");
if (enable_pos == std::string::npos) {
GTEST_SKIP();
}
}
Теперь, тест запустится, только если явно указать имя теста при запуске:
./tests --gtest_filter=Category.some_test
или все тесты, включая наш:
./tests --gtest_filter=Category.some_test:*
Под катом небольшой бонус :)
Сгенерировать нули.
В этом месяце (точнее в прошлом) я не прошёл бы тест на продуктивность (недавно узнал про такие: productivity test): 5 тасок и не все в завершённом состоянии.
С одной из них провозился 1.5 недели. Хотя сложного, на первый, взгляд ничего и нет.
А суть её заключается вот в чём: генерировать нули в звуковом устройстве, то есть - тишину.
Поехали!
Точнее генерировать ничего и не нужно: буфера в ALSA кольцевые, нужно его заполнить нулями один раз и просто двигать указатель по нему.
Двигать указатель.
Казалось бы, а тут-то какие сложности?
В предыдущей заметке я конспективно указал основные термины и понятия связанные с PCM. Нам нужен период, его размер и sample rate, он же - частота дискретизации.
Берём таймер. Пусть будет High Resolution Timer. Когда срабатывает ALSA колбек struct snd_pcm_ops::prepare
у нас уже есть все нужные параметры для стрима, типа количества каналов, битности семплов, частоты. Есть и информация о размере выделенного кольцевого буфера: substream->runtime->period_size
. Только нужно быть внимательным, смотрим на тип: snd_pcm_uframes_t
и понимаем, что это размер периода не в герцах, не в единицах времени, а во фреймах.
Так вот, в этом колбеке, мы настраиваем таймер, что бы он тикал один раз в Period Size. Максимально точно в целых числах получается вот так:
period = runtime->period_size; // во фреймах, тут как договоритесь в hw_params, может быть нифига не кратен rate
rate = runtime->rate; // в HZ, например 48000, это частата **фреймов**
sec = period / rate; // вычленим секунды, в более-менее low-latency здесь всегда будет 0
// ну и посчитаем максимально точно ту часть, что в наносекундах
period %= rate;
nsecs = div_u64((u64)period * 1000000000UL + rate - 1, rate);
// чисто хелпер, по сути sec * 10^9 + nsecs
period_time = ktime_set(sec, nsecs);
После чего, в обработчике struct snd_pcm_ops::trigger
, по START активируем таймер и он будет тикать раз в period_time
наносекунд. А дальше, в обработчике прерывания таймера, мы вызываем специально предназначенную для этого функцию:
void snd_pcm_period_elapsed(struct snd_pcm_substream *substream)
Как сказано в документации, вольный перевод:
обновляет статус PCM для следующего периода
Вызывается из обработчика прерывания, когда источник семплов запроцессил (сиречь - положил в память) period_size фреймов. Заобновится указатель на данные, пробудятся те, кто данные ждёт и т.п.
Если вдруг между вызовами случилось несколько period_size - нужно сделать один вызов.
По сути, эта функция, вызывает другой наш колбек: struct snd_pcm_ops::pointer
, который и говорит текущую позицию в кольцевом буфере.
То есть, на основании показаний таймера мы можем сказать виртуальную позицию в буфере. И тут никаких проблем: нам нужно спросить сколько таймер накрутил времени относительно какой-то точки отсчёта (логично сохранить некий аналог now()
при старте таймера). И пересчитать эту дельту на основании rate
и buffer_size
в позицию. В коде это выглядит так:
now = hrtimer_cb_get_time(timer); // текущее время таймера, обновляется пока он работает
delta = ktime_us_delta(now, base_time); // дельта между текущим временем и базовым
delta = div_u64(delta * runtime->rate + 999999, 1000000); // время множим на частоту, получаем дельту во фреймах
// относительно старта таймера
// применяем трюки для минимизации ошибки округления
// делим на 10^6, потому как выше дельту в мкс (us) получили
div_u64_rem(delta, runtime->buffer_size, &pos); // Ну и берём остаток от деления на размер буфера, получаем искомую
// позцию
Действительно, текста много, а сложностей - ноль. Оно работает, данные шлются. Вот только процесс в пользовательском пространстве жрёт 100% CPU. А при работе с реальным железом - нет.
Начинаем разбираться.
Для начала открываем для себя (ещё кто-то будет спрашивать, зачем я ВСЁ ядро в IDE гружу? :), что struct snd_pcm_ops::pointer
вызывается далеко не из одного места:

Красными кружками обведены наиболее активные потребители, выявленные при помощи логов.
__snd_pcm_lib_xfer()
используется для любого трансфера, осуществляющегося через read()
/write()
на устройстве. И частота его следований, в целом, соответствовало таковому для железа. А вот snd_pcm_delay()
, который является обработчиком для IOCTL: SNDRV_IOCTL_DELAY
зовётся очень активно. При этом, было замечено, что при работе с реальным железом, задержка или 0, что означает - данных ещё нет, или 8 и больше, что означает - приложению есть что прочитать. А вот у меня с тишиной (помимо 0) и 1, и 2, и, редко, больше.
А всё почему? А потому, что между вызовами таймер тикает и время движется. И в любой момент времени (ну практически) выходит так, что данные как бы есть и софт старательно их вычитывает.
Вылечилось, как обычно, при помощи sleep()
… Шутка. Почти.
Для железного модуля позиция высчитывалась на основании переданных байт в регистре счётчика. Согласно документации, данные эти обновляются каждый period_size
, но на самом деле - каждые 8 фреймов. Точнее так: каждые 32 байта: модуль, скорее всего, завязан именно на байты, просто в моём режиме: PCM_S16LE/Stereo получается 8 фреймов.
Выходит, что у железа есть задержка в 8 фреймов. На частоте 48000Hz это примерно 166мкс. И этого хватает, что бы не вводить процессор в busy loop.
В общем, я добавил такую “виртуальную” задержку и в программный генератор: считается позиция, если она сдвинулась на 8 или более фреймов, то сохраняем и возвращаем новое значение. В противном случае возвращаем старое значение.
Вот теперь - работает!
Для чего это нужно или чуть больше конкретики
Есть IP Core от Xilinx: Audio PCM Formatter. Он нужен для того, что бы переложить данные из других PL (FPGA) модулей, подключенных в цепочку по AXI шине в память, к которой можно достучаться из CPU в прочитать устройством. Это режим Capture. Есть и обратный режим: переложить данные из памяти и передать по AXI шине.
Как и всё на шине этот модуль тактируется, когда данные есть. Если источником данных является ADC, то данные есть всегда, пока работает ADC, а он работает, обычно, всегда когда устройство открыто и в работе. ADC без разницы что оцифровывать: реальный звук или наводки на проводах.
Другое дело, когда источником сигнала является SDI или HDMI вход. У этих чудиков может быть и физически кабель воткнут, а аудио не быть. Или просто кабель не воткнут. С одной стороны это удобно: семплы не идут, значит сигнала нет. Пробуем потом (ага, тут или поллинг или платформоспецифичные проверки со стороны user-space).
Но теряется унификация: для аналогового аудио у нас данные всегда идут: или реальные или условные нули (наводки).
В общем возникла идея: а что если в драйвере PCM Formatter’а детектировать отсутствие данных и переключаться на программный генератор тишины. И переключаться обратно, когда данные появились. Если это сделать быстрее, чем period size, то мало кто и заметит. Точнее так, мы не озаботились о плавном переходе между позицией указателя между генератором и железом, поэтому в момент переключения могут быть некие переходные процессы, но, в целом, всё работает нормально.
Поэтому 1.5 недели включали в изучение вопроса - как это вообще скрестить между собой.