Свежий OpenWrt на Xiaomi BE7000 без UART, или как три бага в драйвере Ethernet притворялись мёртвой платой
У меня есть Xiaomi BE7000, и железо там вполне приличное (IPQ9554, коммутатор QCA8084 на четыре порта 2.5G, QCN9274 на 5 ГГц, гигабайт памяти и 128 МБ NAND), а вот стоковая прошивка мне быстро стала тесной. Хотелось поставить нормальный OpenWrt из main с ядром 6.18, причём не через kexec поверх стока, а чтобы он грузился прямо с флеша.
Порт под этот роутер уже делает kravasuper в своей ветке xiaomi_be7000, так что я собрал его как есть и прошил, после чего получил быстро мигающий белый диод и полное отсутствие сети, Wi-Fi и SSH, а через четыре включения питания загрузчик сам вернул сток. В обсуждении порта на такое жалуются и другие владельцы, и обычно всё списывают на ревизию платы.
UART на плате есть, но он на 1.8 В, адаптера под такой уровень у меня не было, а обычный на 3.3 В цеплять не хотелось, так что искать причину пришлось вслепую, и как я это делал, как раз и есть большая часть текста.
Два слота прошивки
Роутер спасает то, что на флеше у него два слота прошивки, rootfs в mtd23 и rootfs_1 в mtd24, а какой из них грузить, загрузчик Xiaomi решает по переменным окружения вроде flag_boot_rootfs и flag_last_success. Сток у меня жил в rootfs_1, поэтому OpenWrt можно было писать в rootfs, вообще не трогая рабочую систему.
Из стока это делается через ubiformat на свободный раздел и nvram set с nvram commit для флагов, а если OpenWrt не загрузился, достаточно четыре раза подряд выключить и включить питание (давая каждый раз около минуты), и загрузчик сам уйдёт на другой слот. Единственное, что трогать нельзя ни в коем случае, это разделы с самим загрузчиком, 0:APPSBL и его копию, всё остальное здесь восстанавливается.
Дальше я просто гонял одно и то же, прошивал OpenWrt в свободный слот, загружался, возвращался на сток и смотрел, что произошло, только смотреть было не на что, потому что у системы не было ни сети, ни консоли.
Логи во флеш
Я добавил в образ маленькую службу, которая стартует как можно раньше и раз в секунду сохраняет dmesg и список процессов во флеш, в rootfs_data этого же слота. Потом я загружался в сток, подключал том через ubiattach, вытаскивал его целиком через cat /dev/ubiN_M и уже на компьютере разбирал утилитой ubireader, потому что стоковое ядро не умеет монтировать UBIFS со сжатием zstd.
Первая версия службы писала всё в один файл, и на этом я потерял кучу времени. В логе стабильно был обрыв на 10-18 секунде, и я долго искал, где система зависает, пока не понял, что она не зависает вовсе, а это я сам выключаю питание, чтобы вернуться на сток, и следующая загрузка затирает данные предыдущей. Когда каждая загрузка стала писать в свою папку, картина оказалась совсем другой, и дальше уже было что разбирать.
Проверка линка в QCA8084
В логах нашлось вот что.
qca8084 ... BaseR link failed!
qcom_ppe ... PPE port 1 failed to connect phylink
Функция qca8084_xpcs_set_mode() после настройки PCS и XPCS коммутатора ждёт линк BaseR 100 мс и, если не дождалась, возвращает ошибку. Проблема в том, что этот линк зависит от сигнала SerDes со стороны процессора (UNIPHY0), а процессор начинает его отдавать только при открытии порта, где-то в phylink_start и дальше в pcs_config, так что проверка идёт в момент, когда линка просто не может быть.
У автора порта, судя по всему, загрузчик оставляет UNIPHY включённым, и проверка проходит, а на моей плате он этого не делал. Я превратил ошибку в предупреждение (патч 0901), и линк поднимается позже, когда порт открывают. Правильнее, конечно, было бы настраивать SerDes процессора до подключения PHY, но для начала хватило и этого.
Зависание в napi_disable
Само по себе неподключившееся PHY не должно было бы валить всю систему, ну не создался порт и не создался. Но у меня не стартовало вообще ничего, а в списке процессов вечно висел kmodloader.
Оказалось, что после ошибки драйвер qcom-ppe начинает себя разбирать и в edma_cfg_rx_napi_delete() и edma_cfg_tx_napi_delete() безусловно вызывает napi_disable(), в том числе для NAPI, который уже выключен или ни разу не включался. Такой вызов не возвращается никогда, процесс застревает в napi_disable_locked с захваченным RTNL, а поскольку модули грузит S10boot, вместе с ним встаёт и весь дальнейший запуск. Патч 0900 просто не зовёт napi_disable() для уже выключенного NAPI, и это уже настоящий баг драйвера, который от платы не зависит.
Oops при откате порта
Как только зависание исчезло, система пошла дальше и упала с oops на мусорном указателе. В ppe_port_mac_init() при сбое edma_port_setup() запускается откат, в котором индекс i доходит до минус единицы, а следующий цикл написан как while (i), и для минус единицы он, естественно, продолжается и лезет в port[-2], port[-3] и дальше по памяти перед массивом. Патч 0902 переписывает эту очистку так, чтобы индекс в минус не уходил.
По отдельности каждая из этих ошибок находится легко, просто вместе они дают ровно ту картину, которую все принимали за неудачную ревизию платы. Ранняя проверка линка ломает подключение PHY, откат после неё зависает, а если убрать зависание, откат падает.
Что в итоге
С этими тремя патчами система грузится до LuCI и SSH примерно за 25 секунд, гигабитный порт выдаёт около 940 Мбит/с в обе стороны по iperf3 без ошибок CRC и потерь, Wi-Fi работает на обоих диапазонах, калибровка подхватывается из раздела ART.
Если будете собирать сами, проверьте, что в профиле устройства есть kmod-ath11k-ahb. У меня его не было, встроенное радио на 2.4 ГГц сидело без драйвера, и заметил я это далеко не сразу, так что теперь перед каждой прошивкой грепаю манифест образа по нужным пакетам. Ещё для нормального приёма на 5 ГГц в DTS нужны два gpio-hog (TLMM6 в ноль и TLMM7 в единицу), без них радио работает, но слышит заметно хуже.
Wi-Fi 7, моя ошибка
Сначала EHT80 на 5 ГГц в режиме точки доступа не поднимался, hostapd писал Failed to set beacon parameters, и я, посмотрев на обрезанный вывод iw, решил, что драйвер объявляет EHT только для режима клиента. Написал об этом в README, в комментарии к PR и на форуме, а потом полез глубже и надолго ушёл в историю с WIPHY_FLAG_SUPPORTS_MLO и ATH12K_FW_FEATURE_MLO, которая к делу отношения не имела вообще.
А причина была совсем глупая. У интерфейса точки доступа в /etc/config/wireless есть своя опция disabled, отдельная от опции самого радио, и в тестовых скриптах я переключал только радио, а опция интерфейса в паре прогонов так и осталась включённой, поэтому hostapd просто нечего было поднимать. На чистой холодной загрузке без единой правки в системных скриптах EHT80 поднялся и с фиксированным каналом, и с автовыбором, MacBook подключился как 802.11be с EHT-MCS 11, а iperf3 показал 945 Мбит/с. Все опубликованные выводы потом пришлось исправлять, так что теперь любое ограничение драйвера я сначала повторяю на чистой загрузке с нетронутыми файлами.
Пока с этим возился, ещё два раза долго искал ошибку там, где её не было. Если заливать файл на роутер через ssh ... 'cat > file' < file без ключа -q, а SSH при этом каждый раз заново добавляет ключ хоста, то строка Warning: Permanently added ... попадает первой строкой прямо в файл, и скрипт на ucode начинает падать с синтаксической ошибкой, как будто файл побился. А функцию log() в модулях ucode нужно импортировать в каждом файле отдельно, иначе вызов без импорта даёт не ошибку выполнения, а всё ту же синтаксическую ошибку при компиляции.
Wi-Fi 7 и страна RU
Когда роутер встал на место старого и получил его настройки, EHT80 снова перестал подниматься с той же самой ошибкой, но на этот раз iw reg get показывал у QCN9274 флаг NO-EHT сразу на всех диапазонах.
В ath12k этот флаг ставит ath12k_map_fw_phy_flags() в reg.c, которая смотрит на битовую маску phybitmap, присланную прошивкой радиомодуля, и если там стоит ATH12K_REG_PHY_BITMAP_NO11BE, запрещает EHT во всех регуляторных правилах разом (маска одна на всё событие, а не на отдельный канал). При стране RU прошивка присылает этот запрет, а когда я для проверки временно выставил US, флаг тут же пропал везде. Первый раз Wi-Fi 7 у меня заработал как раз потому, что в тестовой конфигурации страна ещё не была выставлена.
То есть это решение регуляторной таблицы внутри прошивки WLAN.WBE.1.6-01243-QCAHKSWPL_SILICONZ-1, а не драйвера и не OpenWrt, и патчем ядра оно не лечится.
Про 6 ГГц
В PR меня спросили, нет ли у BE7000 скрытого 6 ГГц, который сток просто не включает, и вместо того чтобы гадать, я решил проверить.
Драйвер ath12k регистрирует диапазон 6 ГГц только тогда, когда прошивка в регуляторных возможностях (событие WMI ext_reg_cap) сообщает верхнюю границу high_5ghz_chan не ниже 5925 МГц. Нигде это значение не печатается, поэтому я добавил printk в wmi.c, пересобрал один пакет mac80211 и подменил модуль прямо на работающем роутере. С ath12k так можно, он висит на PCIe и нормально выгружается и загружается обратно, а вот с ath11k для 2.4 ГГц лучше не пробовать, он завязан на сопроцессор Q6, и после такой подмены помогает только перезагрузка.
Прошивка сообщила 5835 МГц, то есть обычную верхнюю границу 5 ГГц, до порога 6 ГГц не хватает 90 МГц. Прошивка на этой плате 6 ГГц просто не заявляет, и связано это с антенным трактом или с тем, как собиралась прошивка под плату, снаружи не понять, но драйвер, DTS и OpenWrt тут точно ни при чём.
Обновления и слоты
sysupgrade в порте для BE7000 всегда пишет образ в rootfs_1 и переключает загрузку на него. Если вы, как и я, стоите в rootfs, обновление ляжет на место стока, а текущий рабочий OpenWrt останется во втором слоте запасным, что удобно, но сток после первого же обновления пропадёт.
На живом роутере я это проверил, образ встал во второй слот, флаги переключились и настройки из /etc/config переехали. Не переезжает только состояние автозапуска служб, потому что симлинки в /etc/rc.d в бэкап не входят, и службы, включённые руками, после обновления придётся включить ещё раз.
Тема для LuCI
Стандартная тема LuCI работает, но выглядит так, будто ей лет двадцать, и раз уж роутер теперь основной, я сделал свою. В ней меню слева по разделам, быстрый переход к любой странице по Cmd K или /, светлая и тёмная схема и нормальный вид на телефоне, где таблицы превращаются в карточки. Цвета совместимы с Bootstrap, поэтому сторонние приложения LuCI в ней не разваливаются, а к BE7000 она не привязана и встаёт на любой OpenWrt с LuCI 23.05 и новее.

Ссылки
Образы, патчи, скрипты установки со стока и отката, а также тема лежат в репозитории timofey-maykov/be7000-openwrt. Обсуждение порта и мои комментарии с подробностями есть в PR #20604, а сам порт живёт у kravasuper, и без него ничего этого не было бы.
Патчи 0900 и 0902 исправляют настоящие баги qcom-ppe и от платы не зависят, так что их я хочу отправить в ядро. Если у вас тоже BE7000 и тот же мигающий белый диод на образе из порта, попробуйте сборку с патчами и расскажите, что получилось, особенно если ревизия платы у вас другая.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.