Наконец-то пришла пора продолжить изучение возможностей платы Zynq QMTech и SoC XC7Z020. Следующая интересная задача, которую я для себя придумал в качестве обучающей - оснастить плату Wi-Fi модулем Realtek RTL8822CS и, если Wi-Fi модуль будет не нужен, а нужна будет ещё одна флешка - вторым портом для SD-карточки. Если интересны подробности того, как я это всё реализовал - добро пожаловать под кат. 

Важно! Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи - рассказать о своем опыте, с чего можно начать, при изучении отладочных плат на базе Zynq. Я не являюсь профессиональным разработчиком под ПЛИС и SoC Zynq, не являюсь системным программистом под Linux и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется. Что ж, поехали…

Сформулируем цель и задачи

Итак, у нас есть плата, описанная в этой статье и есть желание прикрутить к ней Wi-Fi модуль Realtek RTL8822CS. Модуль, разумеется, должен корректно инициализироваться в ОС Linux и обеспечить пропускную способность до 150 Мбит\с (с чем связано такое ограничение  и именно этой цифрой - ниже). В качестве альтернативного варианта для использования выведенного SDIO - необходимо организовать второй порт для подключения SD-карт.

Исходя из этого можно сформировать несколько задач, решив которые мы можем достигнуть цели:

  1. Проверить возможность выноса линий SDIO через EMIO (т.е. через пины PL-части Zynq) и подготовить bitstream-файл;

  2. Проверить работоспособность SDIO через EMIO в приложении baremetal с использованием microSD-карточки;

  3. Изготовить мезонинные платы для Wi-Fi модуля и смонтировать на них компоненты;

  4. Подготовить First Stage Bootloader, Device Tree Source, U-Boot, Linux Kernel в нужной конфигурации для запуска Wi-Fi модуля;

  5. Подготовить модуль ядра для работы Wi-Fi модуля, а именно модуль cfg80211, который является линуксовым API для 802.11 устройств;

  6. Подготовить модуль ядра 88x2cs, который является драйвером для модуля Wi-Fi RTL8822CS который работает поверх SDIO-шины;

  7. Проверить работоспособность сети, сделать сканы эфира, посмотреть рабочие уровни, сделать замеры скорости downstream/upstream-трафиком через iPerf;

Итак. Перейдем к реализации намеченного, в целом, не выглядит сильно сложно =)

Настройка проекта в Vivado и конфигурация PL

В этой главе я кратко (без скриншотов) опишу процедуру создания нового проекта с акцентами на важных моментах. Если необходима инструкция с картинками - можно обратиться к моим предыдущим статьям. 

Открываем Vivado и создаем проект через кнопку Create Project. Откроется мастер настройки нового проекта, быстро пройдемся по шагам. 

  1. Даём имя проекту, например SDIO_EMIO и указываем папку сохранения;

  2. Указываем, что это RTL Project, оставляем включенной галочку Do not specify sources at this time;

  3. Находим модель SoC использованного на плате. В моем случае это xc7z020clg400-1;

  4. Нажимаем Finish.

В левом меню IP Integrator нажимаем Create Block Design и создаем новый дизайн. Назовём его для простоты block_design.

Добавляем примитив процессорной системы Zynq на диаграмму через нажатие правой кнопкой Add IP - ZYNQ7 Processing System. Сразу же выполним предложенные действия от автоматизатора, через команду на зеленом поле Run Block Automation и откроем настройки процессорной системы. 

Я заранее сохранил шаблон настроек PS, чтобы в каждом проекте не прописывать всё заново. Взять его можно тут: ссылка на Github. Через меню Presets - Apply Configuration применяем его и сразу переходим в меню Peripherial I/O Pins.

Ставим галочку возле меню SD 1 и проверяем, что выбран вариант подключения через EMIO:

Остальные настройки я оставил без изменений, в т.ч. частоты тактирования SDIO, PL. Получаем следующую картину (если развернуть группу сигнальных линий SDIO_1):

В данном случае нас интересуют линии CLK, CMD и 4 линии DATA, что в общем-то является стандартным набором для SDIO-шины. Но вот незадача, каждая из линий организована тремя отдельными сигналами. Для решения этой задачи нам необходимо руками создать буфер IOBUF, который будет управляться через активный пин T с тремя состояниями и разрулит Input, Output сигналы как нам нужно для использования его с двунаправленным сигналом I/O. Выглядит он следующим образом:

Т.к. в стандартной библиотеке графических примитивов он отсутствует - нужно будет создать свой IP-модуль и добавить его на схему. Для каждого из сигнала он будет свой.  

Через меню Sources - Add Sources (Alt + A) добавляем Design Source - Create File, выбираем тип файла Verilog и назовём его iobuf_cmd.

Запишем следующее содержимое:

module iobuf_cmd	
(
    input iobuf_i,     // Вход буфера
    input iobuf_t,     // Входной сигнал с тремя состояниями, high=input, low=output
    output iobuf_o,    // Выход буфера
    inout  iobuf_io     	// Двунаправленный порт (подключается напрямую к top-level порту)
);

IOBUF #(
    .DRIVE(12),                 // Указывается Drive Strength для сигналов
    .IBUF_LOW_PWR("FALSE"),     // Low Power - "TRUE", High Perforrmance = "FALSE"
    .IOSTANDARD("LVCMOS33"),    // Указывается стандарт сигнала
    .SLEW("FAST")               // Указывается slew rate
) IOBUF_inst (
    .O(iobuf_o),      // Выход буфера
    .IO(iobuf_io),    // Двунаправленный порт
    .I(iobuf_i),      // Вход буфера
    .T(iobuf_t)       // Входной сигнал с тремя состояниями
);
endmodule 

То же самое сделаем для сигнала CLK, только файл назовём iobuf_clk. Запишем в него следующее содержание:

module iobuf_clk	
(
    input iobuf_i,     // Вход буфера
    input iobuf_t,     // Входной сигнал с тремя состояниями, high=input, low=output
    output iobuf_o,    // Выход буфера
    inout  iobuf_io    // Двунаправленный порт (подключается напрямую к top-level порту)
);

IOBUF #(
    .DRIVE(12),                 // Указывается Drive Strength для сигналов
    .IBUF_LOW_PWR("FALSE"),     // Low Power - "TRUE", High Perforrmance = "FALSE"
    .IOSTANDARD("LVCMOS33"),    // Указывается стандарт сигнала
    .SLEW("FAST")               // Указывается slew rate
) IOBUF_inst (
    .O(iobuf_o),      // Выход буфера
    .IO(iobuf_io),    // Двунаправленный порт
    .I(iobuf_i),      // Вход буфера
    .T(iobuf_t)       // Входной сигнал с тремя состояниями
);
endmodule 

С SDIO DATA немного сложнее т.к. она состоит из нескольких линий. Так же создадим файл iobuf_data и запишем в него следующее содержимое:

module iobuf_data
(
    input [3:0] iobuf_i,    // Вход буфера
    input iobuf_t,          // Входной сигнал с тремя состояниями, high=input, low=output
    output [3:0]iobuf_o,    // Выход буфера
    inout [3:0] iobuf_io    // Двунаправленный порт (подключается напрямую к top-level порту)
);

    generate  genvar i ; for (i=0; i<4; i=i+1) 
    begin 

    IOBUF #(
    .DRIVE(12),                 // Указывается Drive Strength для сигналов
    .IBUF_LOW_PWR("FALSE"),     // Low Power - "TRUE", High Perforrmance = "FALSE"
    .IOSTANDARD("LVCMOS33"),    // Указывается стандарт сигнала
    .SLEW("FAST")               // Указывается slew rate
)
     IOBUF_inst (
                .O  (iobuf_o[i]),  // Выход буфера
                .IO (iobuf_io[i]), // Двунаправленный порт
                .I  (iobuf_i[i]),  // Вход буфера
                .T  (iobuf_t)      // Входной сигнал с тремя состояниями
             );
    end
    endgenerate
endmodule

Для каждой из линии создается свой экземпляр примитива IOBUF.

Поочередно размещаем добавленные модули на диаграмму через клик правой мышкой по Diagram - Add Module. Соединяем сигналы с нужными портами и получится следующее:

Хотелось бы обратить внимание на две важные вещи:

  1. На порт iobuf_t модуля для сигнала CLK я добавил Constant с шириной в 1 бит и со значением 0, т.к. управляющего T-пина для CLK не предусмотрено;

  2. Сделал порты iobuf_io как External и дал им соответствующие названия;

  3. Соединил порты Input с портами Output;

На текущем этапе можно запустить синтез и назначить выходы портов на реальные физические порты. Для этого нужно:

  1. Нажимаем на меню block_design правой кнопкой и выбираем команду Create HDL Wrapper

  2. В меню IP Integrator выбираем Generate Block Design

  3. Выбираем Synthesis Option - Global и нажимаем Apply, затем Generate

  4. Дожидаемся окончания генерации;

  5. Выбираем Run Synthesis для старта синтеза нашего дизайна.

  6. Дожидаемся окончания синтеза;

После окончания синтеза переходим в меню Open Synthesized Design и откроется меню Package с нижним меню I/O Ports. Далее необходимо:

  1. Отметить подходящие физические I/O пины

  2. Убедиться, что включена подтяжка PULLUP на всех пинах кроме CLK;

В моем случае получается следующее:

Сохраняем настройки, будет предложено создать файл с constraints. Называем его как удобно и нажимаем Ok. После нажимаем Generate bitstream, соглашаемся с тем, что синтез устарел и ждем окончания всех необходимых процедур до получения сообщения:

После этого можем экспортировать BSP в SDK и запустить проверку работоспособности SDIO в baremetal. Делается это через меню File - Export - Export Hardware, ставим Include bitstream. Нажимаем Ok и запускаем SDK через команду File - Launch SDK.

Проверка SDIO через microSD-карту в Baremetal

Прежде чем приступить к проверке необходимо подключить microSD-карту к пинам, которые мы размечали в следующем разделе. Смотрим в первую очередь на распиновку microSD-карты:

С пинами VDD (3.3V) и VSS (GND) всё в целом понятно. С другими пинами есть нюанс, для пина CMD и шины DATA требуется подтяжка к высокому уровню через 10 кОм резисторы. В моем случае, чтобы не городить навесным монтажом резисторы, я взял одну из плат, заготовленных для Wi-Fi модуля и запаял на него все что нужно:

Если посмотреть на принципиальную схему этой платы - все выглядит вот таким образом:

Питание подключено напрямую в соответствии с тем, как оно выведено на гребенку. Для удобства отладки логическим анализатором - я подпаивал МГТФ-проводки. 

После того, как все необходимые линии подключены - можно вернуться в SDK к созданию проекта. После запуска SDK обзываем проект SDIO_EMIO и нажимаем Next, выбираем шаблон проекта Hello World.

Открываем файл helloworld.c из древа проекта и пишем в него следующий код, который подключит нужные библиотеки и поможет нам быстро сделать проверку:

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "xsdps.h"
#include "xparameters.h"
#include "xil_types.h"

int main()
{
		int Status;

		static XSdPs SdInstance;
		XSdPs_Config *SdConfig;

    init_platform();

    xil_printf("\r\nSD Raw Read/ Write Test\r\n");

		SdConfig = XSdPs_LookupConfig(XPAR_XSDPS_1_DEVICE_ID); 	
    if (NULL == SdConfig)
    {
      xil_printf("XSdPs_LookupConfig failed\r\n");
      return XST_FAILURE;
    }
    else
    {
      xil_printf("XSdPs_LookupConfig ok \r\n");
    }

    Status = XSdPs_CfgInitialize(&SdInstance, SdConfig, SdConfig->BaseAddress);
    if (Status != XST_SUCCESS)
    {
      xil_printf("XSdPs_CfgInitialize failed \r\n");
      return XST_FAILURE;
    }
    else
    {
      xil_printf("XSdPs_CfgInitialize ok \r\n");
    }

    Status = XSdPs_CardInitialize(&SdInstance);
    xil_printf("XSdPs_CardInitialize... \r\n");
    if (Status != XST_SUCCESS)
    {
      xil_printf("XSdPs_CardInitialize failed \r\n");
    }
    else
    {
      xil_printf("XSdPs_CardInitialize ok \r\n");
    }
}

Кликаем правой кнопкой по проекту SDIO_EMIO и выбираем Build Project. Смотрим, что проект собирается без проблем. Важный момент - выбрать правильный SDIO-контроллер в строке SdConfig = XSdPs_LookupConfig(XPAR_XSDPS_1_DEVICE_ID);

Подключаем JTAG-отладчик и после сборки проекта заливаем bitstream-файл выполнив команду Xilinx - Program FPGA и дожидаемся сигнала FPGA DONE на плате. 

Открываем Serial Port, который принадлежит нашей плате. Например: minicom -D /dev/ttyUSB0. Узнать какой порт принадлежит плате можно запустив команду udevadm monitor | grep ttyUSB и выткнуть-воткнуть miniUSB кабель из платы. Там можно увидеть сообщения по которым можно понять какой у нас порт. Например:

megalloid@megalloid-lenovo:~$ udevadm monitor | grep ttyUSB
KERNEL[191576.781021] remove   /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0/tty/ttyUSB0 (tty)
KERNEL[191576.781124] unbind   /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0 (usb-serial)
KERNEL[191576.781170] remove   /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0 (usb-serial)
UDEV  [191576.796568] remove   /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0/tty/ttyUSB0 (tty)
UDEV  [191576.804931] unbind   /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0 (usb-serial)
UDEV  [191576.811644] remove   /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0 (usb-serial)
KERNEL[191579.176521] add      /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0 (usb-serial)
KERNEL[191579.177593] add      /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0/tty/ttyUSB0 (tty)
KERNEL[191579.177706] bind     /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0 (usb-serial)
UDEV  [191579.217863] add      /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0 (usb-serial)
UDEV  [191579.225195] add      /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0/tty/ttyUSB0 (tty)
UDEV  [191579.229537] bind     /devices/pci0000:00/0000:00:1d.0/0000:05:00.0/0000:06:01.0/0000:08:00.0/0000:09:02.0/0000:0a:00.0/usb3/3-2/3-2.1/3-2.1.3/3-2.1.3.2/3-2.1.3.2.4/3-2.1.3.2.4:1.0/ttyUSB0 (usb-serial)

После запускаем проект также кликнув по корневому значку проекта выбрав меню Run As - Launch on hardware (System Debugger). В выводе в консоль мы должны увидеть следующее:

Что означают эти сообщения? Это значит, что драйвер успешно “достучался” до карточки и может спокойно начать с ней обмен. Значит наш SDIO-контроллер работает. Идём дальше.

Подробнее о standalone-драйвере можно почитать тут.

Трассировка и изготовление мезонинных плат для Wi-Fi модуля

Следующим шагом нужно подготовить платы, на которые я запаяю радиомодуль, со всей необходимой обвязкой. В качестве первого радиомодуля был взят модуль от FN-Link с Realtek RTL8822CS чипом, модель модуля 6222B-SRC. Подробнее можно посмотреть тут, ссылка на Datasheet.

Кратко скажу, что это простой MIMO 2x2 Dual Band модуль Wi-Fi с поддержкой 802.11ac и Bluetooth 4.2 или 5.0 в зависимости от модели. 

Так как модуль мы будем подключать к гребенке PLS на плате с шагом в 2.54мм. - было решено сделать такой же разъем и на мезонинной плате. 

Распиновка со стороны платы: 

Распиновка со стороны модуля:

Решено было вывести в т.ч. и Bluetooth для дальнейших экспериментов. В итоге общая обвязка модуля выглядит следующим образом:

Всё в целом достаточно примитивно и в соответствии с референсной схемой и небольшими изменениями, такими как пин включения\выключения питания модуля и прочих мелких доработок. Антенный тракт было решено вывести на u.FL-разъем, для удобства и возможности подключения любых подходящих для 2.4/5ГГц антенн. 

Получившиеся платы достаточно просты в трассировке и не представляют собой никакого технического ноу-хау, но линии данных нужно выровнять т.к. SDIO-шина тут достаточно скоростная, чтобы пренебречь таким важным аспектом трассировки. 

Результат трассировки в 2D:

Результат трассировки в 3D:

Вид сверху:

Вид снизу:

Вид платы после монтажа компонентов:

Т.к. плата изначально разрабатывалась не только для этой версии Wi-Fi модуля - остались не установленными некоторые компоненты. 

В целом можно переходить к подготовке всего необходимого для работы с радиомодулем с точки зрения основных управляющих сигналов и подготовки необходимых файлов для работы радиомодуля в Linux. Об этом в следующих главах.

Небольшое дополнение проекта в Vivado

Для того, чтобы была возможность запустить радиомодуль необходимо соблюсти два условия:

  1. Подать низкое напряжение на затвор P-канального ключа VT1, т.е. на линию WL_BT_ON, чтобы подать питание на радиомодуль;

  2. Подать высокое напряжение на ножку WL_REG_ON радиомодуля, чтобы запустить его;

Поэтому на этом этапе нам будет нужно слегка модернизировать проект и добавить 2 сигнала с AXI GPIO-контроллера и завести их на соответствующие ножки: 

  1. Для транзисторного ключа это ножка 11 на общей гребенке, что соответствует физическому пину W20;

  2. Для сигнала WL_REG_ON это 25-я ножка на той же гребенке, что соответствует в моем случае пину V17;

Как работать с AXI GPIO Я рассказывал в этой статье и на деталях повторяться не буду. В нашем случае нужно выполнить лишь несколько шагов:

  1. Добавляем в дизайн IP-модуль AXI GPIO;

  2. Выполняем предложенные шаги автоматизации предложенные Vivado, отметив все галочки;

  3. Появится блок AXI GPIO, необходимо зайти в его опции и поставить галочку All Outputs, поставить ширину шины GPIO Width в значение 2 (нужно 2 сигнала) и в Default Output вписать 0x00000002, чтобы по умолчанию после загрузки bitstream-файла оба пина приняли необходимое значение;

  4. Называем External Interface от AXI GPIO как MGMT_GPIO и сохраняемся. Должно получиться следующее:

  1. Синтезируем дизайн, проверяем, что всё прошло успешно;

  2. Переходим в меню Open Synthesized Design и назначаем MGMT_GPIO[0] пин W20, а MGMT_GPIO[1] пин V17;

  3. Изменяем у этих двух пинов I/O Std на LVCMOS3.3 и сохраняем;

  4. Запускаем генерацию bitstream-файла через Generate bitstream;

  5. Экспортируем сгенерированный bitstream и всё что необходимо для дальнейших шагов через команду File - Export - Export Hardware

Все изменения, необходимые на этом этапе мы внесли. Переходим к подготовке всего необходимого для работы с радиомодулем в Linux;

Подготовка FSBL, Device Tree Sources, U-Boot

На этом этапе можно переходить к созданию загрузчиков, файлу описания железа и компиляции ядра Linux. Многие моменты и детали Я целенаправленно опущу т.к. уже рассказывал в предыдущей статье а отличающимся моментам посвящу основную часть повествования. 

Первым этапом надо подготовить FSBL. Для этого нужно:

  1. В меню Xilinx SDK переходим в меню File - New - Application Project;

  2. Именуем его как FSBL;

  3. Нажимаем клавишу Next;

  4. Из заготовок выбираем Zynq FSBL;

  5. Дожидаемся окончания компиляции и проверяем, что появился файл FSBL.elf

Вторым этапом необходимо подготовить Device Tree Source/Blob файл который будет использован для сборки U-Boot. Для этого сделаем следующее:

  1. Добавляем BSP Repository из репозитория device-tree-xlnx. Для этого нужно выбрать меню Xilinx - Repositories и указать папку с device-tree-xlnx в секцию Global Repositories

  2. Добавляем BSP Project через меню File - New - Board Support Package, даём ему имя и выбираем внизу меню device_tree и в следующем меню нажимаем Ok, если не требуется внесения каких-либо изменений. 

  3. Выполним редактирование файла zynq-7000.dtsi и находим секцию описывающую SDIO1 в строке sdhci1: mmc@e0101000;

Дополняем секцию следующим содержанием (файл можно взять тут) :

Переходим в папку с сгенерированными dts-файлами в системной консоли:

megalloid@megalloid-lenovo:~$ cd Zynq/Projects/SDIO_EMIO/SDIO_EMIO.sdk/device_tree/

Компонуем исходники:

gcc -I my_dts -E -nostdinc -undef -D__DTS__ -x assembler-with-cpp -o system.dts system-top.dts

Компилируем в blob:

dtc -I dts -O dtb -o system.dtb system.dts

Всё. Device Tree готов.

Следующим этапом необходимо подготовить U-Boot для нашей платы. Нужно выполнить несколько простых шагов:

  1. Перейти в папку с U-Boot: cd u-boot-xlnx

  2. Задать переменные для кросс-компиляции: export CROSS_COMPILE=arm-linux-gnueabihf-; export ARCH=arm

  3. Очистить сырцы от предыдущих результатов компиляции и применить конфиг для Zynq по умолчанию: make distclean; make xilinx_zynq_virt_defconfig

  4. Положить в папку с U-Boot полученный в предыдущем шаге system.dtb в папку arch/arm/dts/ c именем zynq-qmtech.dtb

  5. Указываем какой Device Tree файл использовать: export DEVICE_TREE="zynq-qmtech"

  6. Далее необходимо зайти в меню конфигуратора и изменить ряд опций: make menuconfig 

  7. Опция для того, чтобы активировать поддержку AXI GPIO, которая используется для включения питания модуля и подачи сигнала WL_REG_ON на радиомодуле: Device Drivers - GPIO Support - Xilinx GPIO Driver 

  8. Сохраняем конфиг (можно взять тут) в файл .config

  9. Запускаем сборку: make -j$(nproc) и дожидаемся окончания компиляции;

После этого можно скомпоновать FSBL, Bitstream-файл и U-Boot в один загрузочный файл BOOT.BIN. Подробное описание компоновки можно найти в моей прошлой статье. В целом надо выполнить несколько простых шагов:

  1. Найти файл в каталоге с проектом First stage bootloader. По умолчанию он находится в SDIO_EMIO/SDIO_EMIO.sdk/FSBL/Debug/FSBL.elf

  2. Найти Bitstream-файл для программируемой логики. Найти его можно в каталоге SDIO_EMIO/SDIO_EMIO.sdk/block_design_wrapper_hw_platform_0/block_design_wrapper.bit

  3. Найти бинарный файл U-Boot. Данный файл после компиляции лежит в папке с U-Boot: u-boot-xlnx/u-boot.elf

  4. Открываем в Xilinx SDK пункт меню Xilinx - Create Boot Image;

  5. Выбираем куда сохранить новый bif-файл, я его сохраняю в корневом каталоге проекта;

  6. Добавляем поочередно файлы FSBL.elf, block_design_wrapper.bit, u-boot.elf в секцию Boot Image Partitions;

  7. Нажимаем Create Image и видим как в каталоге с bif-файлом появляется BOOT.BIN;

Теперь необходимо залить этот файл на загрузочную SD-карту и можно попробовать загрузиться. Если мы видим приглашение от U-Boot и что светится светодиод FPGA DONE - значит мы всё выполнили правильно. 

Можно перейти к проверке того, проинициализирован ли AXI GPIO в U-Boot и проверим их. Для этого сначала выведем список устройств которые проинициализированы. Это можно сделать через команду dm tree.

Отлично. Видно, что у нас на борту есть два SDIO-контроллера, и два GPIO-контроллера. Попробуем изменить состояние ножки транзистора. Для этого нужно:

  1. Посмотреть какие адреса в GPIO доступны и какие отвечают за те, или иные ножки:  gpio status -a

  2. В конце вывода видно, что есть Bank GPIO с двумя ножками, которые соответствуют значению по умолчанию:

    1. Проверки ради можно изменить состояние ножки gpio@412000000 через команду: gpio toogle gpio@412000000 или gpio set gpio@412000000 / gpio clear gpio@412000000 для установки логической 1 или 0 соответственно

Так. Загрузчик подготовлен. Теперь можно переходить к подготовке ядра Linux и RootFS со всем необходимым для нас содержимым

Компиляция ядра Linux и подготовка RootFS с использованием Buildroot

Так же как и прошлых главах, для исключения необходимости подробного объяснения каждого шага, сошлюсь на предыдущую статью как скомпилировать ядро Linux. Перейдем к выполнению шагов, необходимых для сборки ядра с необходимыми модулями и драйверами. Для этого нужно:

  1. Перейти в папку с клонированным репозиторием linux-xlnx;

  2. Обновить его (при необходимости): cd linux-xlnx; git fetch -p; git checkout master; git pull; 

  3. Экспортируем также переменные для кросс-компиляции: export CROSS_COMPILE=arm-linux-gnueabihf-; export ARCH=arm

  4. Делаем очистку от результатов предыдущих сборок: make distclean

  5. Загружаем дефолтный конфиг: make ARCH=arm xilinx_zynq_defconfig;

  6. Изменим дефолтный конфиг загрузив меню конфигурации: make ARCH=arm menuconfig

  7. Изменим опции конфигурации (можно взять тут) , устанавливаем опцию (проследим, что там вместо [M] в поле выбора стоит символ звездочки [*] : Networking Support - Wireless - cfg80211 

  8. Сохраняем конфиг в файл .config и выходим из меню конфигурации;

  9. Запускаем процесс компиляции: make ARCH=arm UIMAGE_LOADADDR=0x8000 uImage -j8

  10. Компилируем модули ядра: make ARCH=arm -j8 modules

  11. Копируем uImage из каталога linux-xlnx/arch/arm/boot/ на загрузочную SD-карту;

Далее необходимо подготовить образ RootFS с всеми необходимым userspace-утилитами, типа wpa_supplicant, wpa_cli, etc. Собирать RootFS будем с помощью системы сборки buildroot, статью о которой я писал до этого. Итак, перейдем к сборке, для этого нужно:

  1. Заходим в папку с buildroot: cd buildroot

  2. Обновляем его до последней версии: git fetch -p; git checkout master; git pull;

  3. Делаем очистку от результатов предыдущих сборок: make distclean

  4. Запускаем меню конфигурации: make -C /home/megalloid/buildroot O=$PWD ARCH=arm nconfig

    Изменяем опции конфигурации по списку ниже;

  5. Опция 1: Target options - Target Architecture - ARM (little endian)

  6. Опция 2: Target options - Target Architecture Variant - cortex-A9

  7. Опция 3: Target options - Enable NEON SIMD extension support - [*]

  8. Опция 4: Target options - Enable VFP extension support - [*]

  9. Опция 5: Target options - Target ABI - EABIhf

  10. Опция 6: Target options - Floating point strategy - NEON 

  11. Опция 7: Build options - Number of jobs to run simultaneously - 8 (по количеству ядер CPU)

  12. Опция 8: Toolchain - C library - glibc

  13. Опция 9: Toolchain - Kernel headers - Custom Git Repository 

  14. Опция 10: Toolchain - URL of custom repository -  </home/megalloid/linux-xlnx>

  15. Опция 11: Toolchain - Custom repository version - <хэш-сумма от git show-ref | grep master из папки linux-xlnx>

  16. Опция 12: Toolchain - Custom kernel headers series - 5.15.x

  17. Опция 13: Toolchain - Install glibc utilities - [*]

  18. Опция 14: Toolchain - GCC compiler Version - gcc 9.x

  19. Опция 15: Toolchain - Enable C++ support - [*]

  20. Опция 16: Toolchain - Enable compiler link-time-optimization support - [*]

  21. Опция 17: Toolchain - Enable compiler OpenMP support - [*]

  22. Опция 18: Target packages - Miscellaneous - haveged

  23. Опция 19: Target packages - Networking applications - dhcpd

  24. Опция 20: Target packages - Networking applications - dropbear

  25. Опция 21: Target packages - Networking applications - ifupdown scripts

  26. Опция 22: Target packages - Networking applications - hostapd

  27. Опция 23: Target packages - Networking applications - iperf

  28. Опция 24: Target packages - Networking applications - iperf3

  29. Опция 25: Target packages - Networking applications - fping

  30. Опция 26: Target packages - Networking applications - iw

  31. Опция 27: Target packages - Networking applications - iwd

  32. Опция 28: Target packages - Networking applications - wpa_supplicant

  33. Опция 28: Target packages - Networking applications - wireless tools

  34. Опция 29: Filesystem images - cpio the root filesystem - [*]

  35. Опция 30:  Filesystem images - Compression method - lzma

  36. Опция 31: Host Utilites - host dosfstools

  37. Опция 32: Host Utilites - host genimage

  38. Опция 33: Host Utilites - host mtools

  39. После этой длительной конфигурации выбираем Save и сохраняем конфиг (можно взять тут и подложить к себе);

  40. Нажимаем F9 и запускаем процесс сборки: make -C /home/megalloid/buildroot O=" class="formula inline">PWD ARCH=arm BR2_JLEVEL="(((nproc) - 1))"

  41. Ждём когда закончится компиляция, самое время заварить и выпить кофе =)

Теперь когда образ RootFS собрался надо его подписать и привести к виду, в котором его сможет корректно переварить U-Boot:

  1. Переходим в папку с готовыми образами: cd buildroot/images

  2. Производим подпись: mkimage -A arm -T ramdisk -C gzip -d rootfs.cpio uramdisk.image.gz

  3. Складываем uramdisk.image.gz на загрузочную SD-карту;

Настраиваем загрузку образов в U-Boot:

  1. Надо загрузиться в консоль U-Boot и записать туда: setenv mmc_boot “fatload mmc 0 0x2000000 uramdisk.image.gz; fatload mmc 0 0x4000000 uImage; fatload mmc 0 0x4500000 system.dtb; bootm 0x4000000 0x2000000 0x4500000;”

  2. Сохраняем эту переменную через saveenv и перезагружаемся;

  3. Дожидаемся загрузки ОС;

Теперь можно переходить к компиляции драйвера и непосредственно настройке Wi-Fi модуля.

Компиляция модуля ядра 88х2cs для Wi-Fi модуля

Переходим к последнему элементу, необходимому для работы Wi-Fi модуля - драйверу нашего Wi-Fi модуля. На просторах GITHUB я нашел вполне себе собираемый и рабочий вариант драйвера. Чтобы собрать драйвер out-of-tree нужно сделать следующее:

  1. Склонировать исходные коды себе: git clone https://github.com/megalloid/rtl88x2cs

  2. Переключим ветку на версию драйвера совместимую с 5.15: git checkout tune_for_jethub_5_14

  3. Запускаем компиляцию: make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KSRC="/home/megalloid/linux-xlnx" (где KSRC - путь до папки с ядром LInux из предыдущего шага);

  4. На выходе мы получим в этой папке файл 88x2cs.ko, его нужно положить туда же на загрузочную SD-карту;

Проверяем, загрузится ли модуль ядра и появится ли wlan-интерфейс:

  1. Монтируем SD-карту в файловую систему: mount /dev/mmcblk0p1 /mnt

  2. Переходим в каталог SD-карты: cd /mnt/

  3. Выполняем insmod 88x2cs.ko для загрузки модуля ядра;

Запускаем программу ifconfig - и видим, что появился wlan0 интерфейс:

Это говорит о том, что модуль ядра успешно загрузился и готов к работе. В следующей главе перейдем к настройке wlan0 интерфейса и проверке работоспособности Wi-Fi, прогону скорости через iPerf;

Про модули ядра можно почитать тут.

Настройка Wi-Fi интерфейса и измерение скорости с использованием iPerf

Для настройки Wi-Fi необходимо первым делом создать конфиг, удобнее всего в этом случае его создать на SD-карте в виде файла wpa_supplicant.conf

Выполним в консоли Zynq следующие команды:

  1. Создадим файл конфигурации: touch /mnt/wpa_supplicant.conf

  2. Открываем его на редактирование: vi /mnt/wpa_supplicant.conf

  3. Записываем в него следующее содержимое и сохраняем: 

network={
	ssid="ASUS"
	scan_ssid=1
	key_mgmt=WPA-PSK
	psk="megapassword"
}
  1. Запускаем wpa_supplicant: wpa_supplicant -B -Dnl80211 -iwlan0 -c/mnt/wpa_supplicant.conf -P/var/run/wpa_supplicant.pid -dd -f/mnt/wpa_supplicant.log

  2. Через некоторое время появится IP-адрес на интерфейсе wlan0 и будет установлена связь с роутером;

Теперь можно прогнать трафик с компьютера (который подключен к роутеру кабелем) на Zynq и обратно. Для этого нужно:

  1. Запустить на компьютере: iperf3 -s

  2. Запустить на Zynq: iperf3 -c <IP компьютера> -N -i1 -t60 -u 

  3. Чтобы пустить трафик в обратном направлении на Zynq нужно выполнить: iperf3 -c <IP компьютера> -N -i1 -t60 -R -u

Наблюдаем результат:

# iperf3 -c 192.168.2.3 -N -i1 -u -b300M
Connecting to host 192.168.2.3, port 5201
[  5] local 192.168.2.233 port 55279 connected to 192.168.2.3 port 5201
[ ID] Interval           Transfer     Bitrate         Total Datagrams
[  5]   0.00-1.00   sec  15.9 MBytes   133 Mbits/sec  11481  
[  5]   1.00-2.00   sec  16.8 MBytes   141 Mbits/sec  12154  
[  5]   2.00-3.00   sec  17.0 MBytes   143 Mbits/sec  12336  
[  5]   3.00-4.00   sec  17.0 MBytes   142 Mbits/sec  12275  
[  5]   4.00-5.00   sec  16.3 MBytes   137 Mbits/sec  11812  
[  5]   5.00-6.00   sec  16.3 MBytes   137 Mbits/sec  11831  
[  5]   6.00-7.00   sec  17.0 MBytes   143 Mbits/sec  12332  
[  5]   7.00-8.00   sec  10.4 MBytes  87.4 Mbits/sec  7545  
[  5]   8.00-9.00   sec  14.8 MBytes   124 Mbits/sec  10736  
[  5]   9.00-10.00  sec  15.3 MBytes   128 Mbits/sec  11050  

- - - - - - - - - - - - - - - - - - - - - - - - -

[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.00  sec   157 MBytes   132 Mbits/sec  0.000 ms  0/113552 (0%)  sender
[  5]   0.00-10.03  sec   157 MBytes   131 Mbits/sec  0.066 ms  0/113551 (0%)  receiver
iperf Done.

# iperf3 -c 192.168.2.3 -N -i1 -u -b300M -R
Connecting to host 192.168.2.3, port 5201
Reverse mode, remote host 192.168.2.3 is sending
[  5] local 192.168.2.233 port 50266 connected to 192.168.2.3 port 5201
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-1.00   sec  12.0 MBytes   101 Mbits/sec  0.043 ms  0/8693 (0%)  
[  5]   1.00-2.00   sec  12.2 MBytes   102 Mbits/sec  0.072 ms  0/8842 (0%)  
[  5]   2.00-3.00   sec  12.6 MBytes   105 Mbits/sec  0.161 ms  16279/25380 (64%)  
[  5]   3.00-4.00   sec  12.1 MBytes   101 Mbits/sec  0.032 ms  16435/25185 (65%)  
[  5]   4.00-5.00   sec  12.5 MBytes   105 Mbits/sec  0.076 ms  17924/27002 (66%)  
[  5]   5.00-6.00   sec  12.4 MBytes   104 Mbits/sec  0.320 ms  16380/25350 (65%)  
[  5]   6.00-7.00   sec  12.6 MBytes   105 Mbits/sec  0.055 ms  16807/25910 (65%)  
[  5]   7.00-8.00   sec  12.5 MBytes   105 Mbits/sec  0.196 ms  17230/26287 (66%)  
[  5]   8.00-9.00   sec  12.3 MBytes   103 Mbits/sec  0.050 ms  16305/25184 (65%)  
[  5]   9.00-10.00  sec  12.6 MBytes   106 Mbits/sec  0.100 ms  11046/20165 (55%)  

- - - - - - - - - - - - - - - - - - - - - - - - -

[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.03  sec   359 MBytes   300 Mbits/sec  0.000 ms  0/259695 (0%)  sender
[  5]   0.00-10.00  sec   124 MBytes   104 Mbits/sec  0.100 ms  128406/217998 (59%)  receiver

iperf Done.

Скорость в целом соответствует заявленным Xilinx цифрам: около 20 Мбайт\сек (около 150 Мбит\с). В итоге если будем иметь подходящие условия в эфире - эта скорость будет максимальной и в 2.4 ГГц и 5 ГГц. Результат соответствует всем ожиданиям.

Подведем итог

После длительного кропотливого исследования без каких-либо how-to удалось обуздать эту железку. На общую проработку этого всего ушло около недели, что в целом доставило немало удовольствия по мере продвижения к решению задачи. Буду надеяться, что мой опыт кому-нибудь пригодится и будет высоко оценен. Спасибо!

Комментарии (2)


  1. Albert2009ru
    01.04.2022 08:57
    +2

    Огромное человеческое спасибо Вам за статью. Вы пишете, что не являетесь профессионалом в Embedded. Может, конечно, в прямом смысле Вы деньги на встраиваемых системах не зарабатываете, но талант у Вас есть однозначно. Будет здорово, если итогом цикла статей станет полноценная книга - я бы с удовольствием приобрёл.


    1. megalloid Автор
      01.04.2022 11:08
      +2

      Очень рад читать такие отзывы к плодам своего труда. Буду стараться и дальше радовать своих читателей качественным и полезным материалом. Такие отзывы мотивируют работать дальше! Спасибо!