Полгода назад я думал, что поднять Wi‑Fi на планшете будет примерно как на макетке: есть чип, есть шина, драйвер шлёт команды, чип отвечает. На макетке с ESP32 это заняло 950 строк Rust и несколько дней. На Samsung Galaxy Tab S6 (Snapdragon 855) то же самое заняло 32 000 строк, около десяти перезагрузок всего чипа без единого сообщения об ошибке и одну микросхему питания, которую пришлось превратить в свидетеля.
Ниже вся хронология, с логами.
TL;DR: на Snapdragon 855 «Wi‑Fi‑чип» не существует как отдельное устройство. Вся логика 802.11 исполняется в прошивке сотового модема, и чтобы эта прошивка согласилась работать, хост должен поднять для неё четыре сервера, отдать ей память через TrustZone, проголосовать за семь рельс питания и два такта и не ошибиться ни в одном байте. На любую ошибку хоста SoC отвечает тихим сбросом из secure world, без паники и без кода возврата.
Кто я и зачем это всё. Мы небольшая команда, пишем свою ОС на Rust с нуля: не Linux и не форк, своё ядро без std, планировщик, хранилище, графика. Планшет понадобился как инструмент, чтобы увидеть, чего именно требуют чипы от хоста, когда между ними нет вендорских блобов. Я отвечал за Wi‑Fi. Этот текст я писал по журналу bring‑up‑а и по истории коммитов, поэтому порядок событий здесь настоящий, включая тупики.
Содержание: что было на макетке; что оказалось внутри планшета; как я научился видеть причину тихого сброса; девять этапов по порядку и десять сбросов, которые их сопровождали; что осталось не сделано.
Что было на макетке
950 строк, один UART и TLS, который считал модуль.
Началось всё с платы F4VE: STM32F407, дисплей ILI9341 по SPI, резистивный тач и рядом модуль ESP32 со стоковой прошивкой ESP‑AT, подключённый к микроконтроллеру одним UART‑ом. Wi‑Fi выглядел так: пишешь в порт AT+CWJAP="ssid","pass", читаешь WIFI GOT IP. Пишешь AT+CIPSTART="SSL","host",443, и микроконтроллер без единого байта криптографии разговаривает с сервером по HTTPS, потому что TLS считает модуль.
Весь драйвер был парсером строк и десятком команд. Скан сетей, вход, captive portal, обновление по воздуху заработали за несколько дней. Потом ESP32 стал самостоятельной платой той же ОС, и те же 950 строк просто линковались с Wi‑Fi‑стеком Espressif.
Дальше захотелось настоящего устройства с экраном, батареей и корпусом. Galaxy Tab S6 подошёл: Snapdragon 855, разблокируемый загрузчик, консоль по USB. План был буквально «сделать то же самое». Дисплей и тач сопротивлялись в меру. Wi‑Fi сопротивлялся иначе.
Что оказалось внутри планшета
Отдельного Wi‑Fi‑чипа нет. Есть protection domain в модеме.
Первое, что я сделал, это открыл стоковое дерево устройств. Ожидал увидеть Wi‑Fi‑контроллер на PCIe. Увидел qcom,icnss@18800000, а рядом qcom,mss@4080000 с PAS id 4. WLAN на этом чипе это protection domain внутри модемной подсистемы MPSS: один Hexagon DSP, одна прошивка modem.mbn, внутри неё WLAN.HL.3.2.0.c3-00910. WCN3990 оказался радиофронтендом на системном NoC, то есть на той же шине, что и память.
Прошивка подписана Qualcomm, загружается и проверяется TrustZone. Прочитать её я не могу, изменить не могу, посмотреть на её регистры тоже: чтение QDSP6SS из обычного мира даёт bus error. Зато у неё есть список требований к хосту, и вот он.
| Что требует прошивка | Что это | Что будет, если не дать |
|---|---|---|
| DMA в нашу память | 12 copy engines, каналы прямого доступа к RAM | Wi‑Fi не работает вообще |
Файловый сервер tqftp | TFTP поверх внутреннего IPC | Пишет server_check.txt; без успешной записи WLAN не стартует |
Дисковый сервер rmtfs | Сектора разделов modemst1/2, fsg, fsc | Без него модем умирает на 40‑й секунде с efs sync failed |
Карта доменов pd-mapper | Список «кто где живёт» | Без ответа на kernel/elf_loader WLAN‑домен не поднимается |
Передача памяти memshare | Перевод регионов RAM в собственность модема через secure world | Без MSA‑региона прошивка отказывается инициализироваться |
| IPC: SMEM, SMP2P, GLINK, QRTR, QMI | Общая память и очереди сообщений | Всё управление идёт через это |
В стоковом Android всё это делают rmtfs, tqftpserv, pd-mapper и драйвер icnss, и вы про них никогда не слышали, потому что они стартуют из init до того, как загрузится рабочий стол. У меня их не было. Значит, писать.
Как я научился видеть причину тихого сброса
72 байта в микросхеме питания и второе ядро в роли watchdog'а.
Первый же серьёзный сбой выглядел так. Консоль по USB молчит. Через секунду планшет показывает логотип загрузчика и стартует заново. В логе последняя строка про предыдущий шаг, и всё. Обработчик паники не вызывался. Вектор исключений не срабатывал. Ни одного прерывания. PMIC говорит ровно то, что говорит после любой штатной перезагрузки.
Так SoC отвечает на ошибку хоста, если её заметил TrustZone или гипервизор: перезагрузкой всего чипа вместо исключения в EL1. Улик не остаётся. JTAG на серийном планшете недоступен. Мне нужен был свидетель, который переживает сброс.
Единственная память, которая переживает сброс, это энергонезависимые ячейки SDAM в микросхеме питания PMIC, доступные по SPMI. Я выделил там 72 байта под «последние слова» и 16 байт под «испытания». Механика простая: перед каждым шагом, который может повесить ядро или вызвать сброс (вызов smc в secure world, первое чтение SMEM, обращение к регистру блока с возможно выключенным тактом), код ставит метку с именем шага: scm pas, smem toc, glink attach, wlfw cap. После шага метка снимается.
Если чип перезагрузился, следующая загрузка первым делом читает SDAM и видит, какая метка осталась стоять.
Второе ядро Cortex‑A76 работает watchdog'ом: если первое ядро восемь секунд не отвечает, второе пишет в те же 72 байта вердикт с адресом, где первое застряло. Так первая команда после каждой перезагрузки стала sys.boot, и её вывод выглядит примерно так (сокращено):
> sys.boot
last_reset: reset
previous:
reason: reset "the last run was up when it ended, and left no words"
died_at: +9.4 s
step: wlfw fw_ready wait
note: a step marked as one that can stall the core; the mark was never cleared
crashes: 8 total (panic 1, watchdog 1, reset 6)Метка доказывает одно: шаг с таким именем начался и до сброса не закончился. Кто сбросил и почему, она не говорит. Но разрешение в один шаг оказалось достаточным, чтобы разобрать все десять случаев.
Девять этапов и десять сбросов
Порядок настоящий. Тупики оставлены.
Этап 1. Питание и такты
На Snapdragon рельсы и такты не включаются, а голосуются. Каждая рельса это имя из базы cmd-db, каждый голос идёт через RPMh в контроллер AOP, отдельное Cortex‑M ядро внутри SoC, и AOP агрегирует голоса всех клиентов. Хост один из голосующих. Для модема нужны семь рельс, два опорных такта и команда load_state modem on.
Сброс № 3 пришёл именно отсюда, хотя я его долго не понимал. Оба ядра молчат, меток нет, PMIC чист, укусил аппаратный watchdog. Оказалось, что на передаче управления модему я снимал свой proxy‑голос за cx.lvl до нуля. Рельса vdd_cx питает всю цифровую логику SoC, а не только модем.
Пока модем был занят и голосовал сам, агрегат держался на его голосе. Как только модем уходил в idle, CX проваливалась, и останавливался весь интерконнект. Корреляция «модем занят: живёт, модем в простое: сброс» и была доказательством. Фикс: пол уровней, cx.lvl не ниже NOM.
Этап 2. Загрузка подписанного образа
Модем грузится через PAS (Peripheral Authentication Service). Хост даёт secure world метаданные образа вызовом init_image, раскладывает сегменты в carve‑out на 160 МиБ, вызывает auth_and_reset. TrustZone проверяет подпись и запускает Hexagon. Здесь я собрал два сброса.
Сброс № 1: через десять секунд после первого обращения к модему, ядро в простое. Сброс приходил от времени, не от действия, значит кто‑то трогал память после нас. Единственное разделяемое, что я трогал, была SMEM, и я аккуратно обслуживал по ней кэш инструкциями dc civac.
Две контрольные прошивки решили вопрос: без мьютекса и без обращений к модему сброс есть, без инструкций обслуживания кэша сброса нет. Разделяемая с модемом память должна быть некэшируемым окном, и никаких dc по ней.
Сброс № 2 был хитрее: 6–10 секунд после старта модема, все метки сняты, то есть все шаги PAS прошли. Что TrustZone продолжает читать после auth_and_reset? Метаданные образа. А они лежали у меня в Vec на куче. Куча освободила буфер и переиспользовала, запись попала в страницу, которую TrustZone к тому моменту считал своей.
В референсном ядре Linux есть след того же: qcom_scm_pas_metadata_release вызывается только после auth, начиная с 5.18. Метаданные переехали в статическое некэшируемое окно LENT_PAS на 2 МиБ вне кучи.
Этап 3. IPC снизу вверх
Дальше стек связи с модемом: SMEM (2 МиБ общей памяти с таблицей элементов), SMP2P (биты состояния, тот самый slave-kernel), GLINK (очереди FIFO поверх SMEM), QRTR (маршрутизатор сервисов) и QMI (протокол запросов). Каждый уровень я писал сам, сверяясь с mainline Linux. Правило, к которому пришёл быстро: всё, что модем написал в общую память, считается недоверенным. Смещение, длина, адрес в QMI проверяются по окну до разыменования.
Сброс № 4 не был сбросом SoC, но выглядел похоже: модем читал пустые заголовки EFS, «восстанавливал» файловую систему из золотой копии и падал на Graceful Restart через 20 секунд. Я долго искал баг в своём дисковом сервере.
Тень на диске была валидна. Значит, модем читал не то, что я писал. Окно LENT для буфера начиналось с 0x8530_0000, не на границе 2 МиБ, и первый мегабайт попадал в кэшируемый блок identity‑карты. Сдвинул на 0x8520_0000, добавил const‑проверки выравнивания всех окон.
Этап 4. Четыре сервера
Теперь модем мог говорить, и первое, что он сделал, стал искать на QRTR сервисы: rmtfs (id 14), tqftp (4096), pd-mapper, memshare. И пользоваться ими как клиент.
Дисковый сервер rmtfs отдаёт модему сектора разделов modemst1, modemst2, fsg. Это EFS, персистентная память модема, которая живёт на диске хоста и переживает factory reset. Я решил не отдавать настоящий диск: при OPEN раздел читается в RAM один раз, все RW_IOVEC идут в копию, пути записи на диск в коде нет. Модем видит, что записи «легли», диск не меняется.
Здесь случился сброс № 5, самый показательный. На этом планшете раздела fsg в GPT нет вообще. Я отвечал на OPEN /boot/modem_fsg отказом no such partition, как отвечает rmtfs из утилит qrtr для Linux. Модем умирал ровно через 23 миллисекунды, каждый раз. Таймлайн из моего же сервера:
> ipc.hosts
mss (qrtr node 0)
rmtfs OPEN /boot/modem_fs1 ok read hdr 512 B @ +0
rmtfs OPEN /boot/modem_fs2 ok read hdr 512 B @ +0
rmtfs OPEN /boot/modem_fsg REFUSED no such partition
smp2p slave-kernel 6 -> 7 +23 ms
rproc running -> crashed (SSR reason: <empty>)Причины в SMEM нет, потому что модем умирает раньше, чем стартует его собственный сервис ошибок. Фикс: fsg открывается пустым, как тень из нулей размером с modemst1. Для EFS нули это состояние «стёртая золотая копия», прошивка его знает и идёт дальше. Почему отказ фатален за 23 мс, а отсутствие сервера убивает только через 40 секунд, я не знаю, код закрыт.
Сброс № 6: сервисы 66 и 43 на QRTR есть, а WLFW (0x45) не появляется, и модем падает на 40‑й секунде. WLAN‑домен внутри модема ждал двух вещей: ответа pd-mapper на запрос kernel/elf_loader и успешной записи server_check.txt через tqftp. Отдельно без rmtfs на 40‑й секунде валился efs sync. Пока все четыре сервера не подняты до старта модема, Wi‑Fi не появляется.
Сброс № 7: assign_mem для MSA‑региона отвергнут secure world. MSA это регион самоаутентификации, который хост через memshare передаёт в собственность модема. Адрес 0x8bc0_0000 я взял из dtsi другого релиза. В живом дереве планшета по этому адресу лежала память гипервизора; настоящий pil_wlan_fw_region был на 0x8c20_0000. Урок: адреса только из живого device tree, никаких офлайн‑копий.
Этап 5. Рукопожатие WLFW
Когда сервис 0x45 наконец появился, началось рукопожатие с WLAN‑прошивкой по QMI: IND_REGISTER, CAP (вернул версию прошивки), MSA_INFO, BDF_DOWNLOAD (пять сегментов board data), CAL_REPORT, и остаётся дождаться FW_READY_IND.
Вместо неё пришёл сброс № 8, тот самый, ради которого я и написал метки в SDAM. Фаза стояла на wlfw fw_ready wait. Это шаг после последнего запроса, значит все мои команды прошивка приняла, и сброс случился, пока я ждал её ответа. Подозрение сдвинулось с «я неверно послал» на «прошивка что‑то сделала по моему запросу и наткнулась на препятствие».
Сначала я подозревал MSA: сверил адрес живьём, поменял размер с 1,5 на 1 МиБ по qcom,wlan-msa-memory. Сброс остался. Значит, не память.
Что ещё референс включает перед этим шагом? В ath10k_snoc_clk_enable два такта: cxo_ref_clk_pin (в дереве rfclka2) и qdss через AOP. После CAL_REPORT прошивка инициализирует радио и трогает блок, чей такт хост не включил; обращение к незатактированному блоку это bus error и сброс. Проголосовал за оба такта. FW_READY_IND пришла через восемь секунд после старта модема.
Этап 6. Copy engines, HTT, WMI
Дальше уже знакомая по ath10k территория: 12 copy engines с кольцами дескрипторов, HTT для данных, WMI для управления. Прошивка пишет и читает эти кольца по DMA. Я отдал ей ровно 4 МиБ и надеялся, что забор поставит кто‑то другой, потому что поток 0x640 в apps_smmu в стоковом дереве помечен qcom,smmu-s1-bypass: адреса в дескрипторах физические. Попытка включить трансляцию stage‑1 по дороге ломала инициализацию, и я отложил это, пометив «не сделано».
Этап 7. WPA
Супликант я написал на safe Rust без unsafe: 4‑way handshake, вывод PTK и GTK. А потом ключи уезжают в прошивку командой VDEV_INSTALL_KEY, потому что шифрует трафик hwcrypto внутри модема. Даже когда handshake считается на хосте, ключ трафика лежит у модема. Это не наша особенность, так на любой ОС на этом чипе.
Этапы 8 и 9. Мелочи, которые тоже перезагружают чип
Сброс № 9: я попробовал прочитать лог TrustZone из IMEM. Узел tz-log в дереве заявляет 0x146bf720 + 0x3000, а родительский IMEM 0x146bf000 + 0x1000. Окно узла выходит за конец родителя, чтение за концом и есть сброс. Лог у Samsung к тому же зашифрован.
Сброс № 10 я не наблюдал и закрыл превентивно: Normal‑маппинг, даже некэшируемый, допускает спекулятивное чтение вперёд, а Cortex‑A76 читает вперёд охотно. Над hyp_mem и carve‑out‑ами других ядер это увидит XPU. Резерв стал Device‑памятью, начиная на мегабайт раньше hyp_mem.
Все десять сбросов в одной таблице
Для тех, кто пришёл за таблицей.
| № | Симптом | Причина | Фикс |
|---|---|---|---|
| 1 | Сброс через ~10 с после первого обращения к модему, ядро в простое | Обслуживали кэш (dc civac) по общей с модемом памяти SMEM | SMEM как некэшируемое окно, ни одной инструкции обслуживания кэша по разделяемому |
| 2 | Сброс через 6–10 с после старта модема, все метки сняты | Метаданные подписанного образа лежали в Vec; куча переиспользовала буфер, запись попала в страницу под защитой TrustZone | Отдельное статическое окно LENT_PAS на 2 МиБ вне кучи |
| 3 | Оба ядра молчат, PMIC чист, укусил аппаратный watchdog | Сняли свой голос за рельсу cx.lvl до нуля; она питает всю цифровую логику SoC | Пол уровней: cx.lvl не ниже NOM |
| 4 | Модем читает пустые заголовки EFS и падает через ~20 с | Окно для буфера rmtfs начиналось не на границе 2 МиБ; первый мегабайт попал в кэшируемый блок | Выравнивание всех окон, const‑проверки |
| 5 | Модем fatal ровно через 23 мс после OPEN /boot/modem_fsg | Раздела fsg на планшете нет, мы отвечали отказом, как rmtfs в Linux | Открывать fsg пустым: нули для EFS означают «стёртая золотая копия» |
| 6 | Сервис WLFW (0x45) не появляется; модем fatal на ~40 с | WLAN‑домен ждал pd-mapper и tqftp; отдельно без rmtfs валился EFS sync | Поднять все четыре сервера до старта модема |
| 7 | assign_mem для MSA отвергнут secure world | Адрес региона взял из dtsi другого релиза; в живом дереве там память гипервизора | Читать адрес только из живого device tree |
| 8 | Сброс после CAL_REPORT, фаза wlfw fw_ready wait | Прошивка после калибровки трогает блок, чей такт хост не включил (rfclka2, qdss) | Голосовать за оба такта до рукопожатия |
| 9 | Сброс при чтении лога TrustZone | Узел tz-log заявляет окно шире родительского IMEM на 4 байта | Не читать за reg родителя; лог у Samsung всё равно зашифрован |
| 10 | Не наблюдён | Normal‑маппинг допускает спекулятивное чтение над памятью гипервизора | Резерв как Device‑память, на мегабайт раньше hyp_mem |
Что получилось и что нет
32 000 строк, восемь секунд до FW_READY, один незакрытый пункт.
Wi‑Fi работает: скан, вход в сеть по WPA2, DHCP, TLS поверх, обновление ОС по воздуху. От старта модема до FW_READY_IND восемь секунд. Код: около 32 000 строк против 950 на ESP32, из них примерно треть это четыре сервера для модема и валидация всего, что он присылает.
Что не сделано. Stage‑1 трансляция SMMU для потока Wi‑Fi: mainline Linux делает её на том же чипе, у меня она в плане. Пока прошивка модема пишет в мою память по физическим адресам, и единственные заборы стоят у гипервизора Samsung и XPU Qualcomm, а не у меня. Персистентный EFS: тень вместо диска означает, что часть функций сотового модема без сохранения состояния работать не будет; для планшета без SIM это осознанная цена.
Чему меня это научило, если коротко. На современном SoC радио это сопроцессор с DMA, файловой системой и своим IPC. Все девять этапов были обслуживанием его требований, а не настройкой чипа в привычном смысле. Если вы работаете с Snapdragon под Linux, всё это делает за вас downstream‑стек, и он делает это правильно. Просто полезно знать, что именно он делает.
Если интересен конкретный этап с адресами регистров, спрашивайте в комментариях, разверну. Если найдёте ошибку, скажите, поправлю.
Паразит в планшетеНа макетке F4VE Wi‑Fi был 950 строками за UART‑ом. На планшете Snapdragon 855 стал 32 000: девять стадий, дюжина тихих сбросов SoC, четыре сервера, которые мы поднимаем для чипа. «Wi‑Fi‑чип» — половина модема с DMA, файловой системой и собственным радио: жилец с ключами арендодателя. Что он может, кого касается (каждый Android на Snapdragon, ноутбуки с Windows, iPhone с модемом Qualcomm) и как изолировать его прозрачно.
Сброс без свидетелейSoC отвечает на ошибку хоста не ошибкой, а перезагрузкой — и не оставляет улик: DRAM стёрта, PMIC как после штатного ресета, лог TrustZone зашифрован. Как мы построили свидетеля в 72 байтах памяти PMIC и разобрали десять тихих сбросов по одному.
Мегабайт, который убил модемОдин кэшируемый мегабайт в окне, одолженном модему, заставил его прочитать нули там, где мы записали заголовки, «восстановить» файловую систему и упасть. Почему кэшируемость и выравнивание на 2 МиБ — условия безопасности на SoC с другими мастерами, и как их теперь проверяет компилятор.
23 миллисекунды, чтобы солгатьМодем попросил раздел, которого на планшете нет. Честный отказ «no such partition» убил его за 23 мс; пустой раздел из нулей — нет. Почему прошивка проверяет хоста на каждом шаге и почему изоляция должна быть прозрачной: правильной по форме, пустой по сути.