Продолжаю повествование о том, как проходит мое изучение возможностей отладочной платы с SoC Zynq 7000 на базе отладочной платы QMTech. В этой статье я опишу то, как я решал задачу примитивного обмена данными между PS и PL с использованием baremetal application и при использовании Linux. Всем интересующимся добро пожаловать под кат!

Дисклеймер
Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи — рассказать о своем опыте, с чего можно начать, при изучении отладочных плат на базе Zynq. Я не являюсь профессиональным разработчиком под ПЛИС и SoC Zynq, не являюсь системным программистом под Linux и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется.
Перед тем как приступить к выполнению каких-либо действий из этого урока — настоятельно рекомендую прочитать предыдущие уроки из серии уроков по Zynq. Инженер, который хочет повторить изложенное в этом уроке, уже должен уметь или хотя бы иметь представление о том, как:
-
Работать с Xilinx Vivado и иметь понимание того, как и в какой последовательность организуется разработка с использованием данной среды;
-
Что такое Xilinx SDK и как она взаимосвязана с Xilinx Vivado;
-
Собрать baremetal-приложение для Zynq, как его откомпилировать и запустить на отладке;
-
Собрать загрузочный образ Linux с FSBL, bitstream-файлом, U-Boot, Device Tree, Root FS и ядром;
Постановка задачи
Чтобы развивать дальнейшее повествование и изучать что-то новое нужна была реальная интересная задача. И я ее придумал! Ребята из моей команды рассказывали в этой статье историю того, как шла разработка Яндекс.Станции-Max и упомянули устройства для организации тестовых испытаний плат, так называемые “джиги” (англ. jigs).
Это устройство организовано таким образом, что есть специальная платформа, которая встаёт иглами на тестовые точки и делает через иглы определенные манипуляции для проведения тестирования. Не вдаваясь в подробности, в конечном итоге данные измерений преобразуются в специально сформированный набор импульсов и отправляются в процессор на обработку. Основная задача промежуточных вычислений до наступления момента интерпретации результатов тестирования — сосчитать количество импульсов с того или иного канала. Но с наращиванием количества каналов счёта таких импульсов есть ограничение в возможностях расширения. Плюсом к этому обработка импульсов идет через процессор с ядром Cortex-M4 и ограниченным набором по периферии, т.е. о реальной параллельности счёта большого числа каналов не может идти и речи. И тут я подумал о том, что можно же считать параллельно при помощи ПЛИС и забирать данные в Linux и отсылать уже данные по сети на сервер при помощи Zynq!
И тут в голове оформилась интересная задача:
-
Нужно организовать внутри ПЛИС набор счетчиков, которые будут считать импульсы с частотой до 2МГц;
-
Счетчики должны быть включаемыми\выключаемыми;
-
Счетчики должны быть сбрасываемыми;
-
Для включения, выключения и сброса счетчиков должны быть отдельные регистры управления;
-
Счётчики должны работать независимо друг от друга и синхронно;
-
Управление и считывание данных из счётчиков должно быть доступно из PS или Linux;
-
Счётчики должны считать точное количество импульсов (допустимое отклонение 1-2 импульса).
Исходя из постановки задачи был сформирован четкий план действий:
-
Создаем проект, настраиваем Processing System и Processor System Reset.
-
Настраиваем соответствующие пины ПЛИС, которые будут входами для импульсов;
-
В ПЛИС делаем блок ограничения импульсов частотой более 2 МГц и проверяем генератором сигналов или другой ПЛИС;
-
Добавляем счетчик импульсов в ПЛИС и проверяем корректность счёта сгенерировав конкретное количество импульсов другой ПЛИС;
-
Организуем некую память в которую можно записывать и из которой можно читать данные как из ПЛИС, так и из PS-части. В этой же памяти внутри ПЛИС организуем две ячейки памяти которые будут выступать в качестве двух регистров управления (сброс, включение и выключение) и одну в качестве регистра хранения команд;
-
Организуем блок управления счетчиками внутри ПЛИС, который сможет опрашивать память на предмет наличия команды из PS, и по команде “забирать” данные из счётчиков и сохранять их в память, сбрасывать значение и включаться\выключаться.
-
Протестировать полученный результат с использованием baremetal-приложения и из Linux.
Что ж, общее понимание, того что нужно сделать есть, можно приступить к реализации намеченного.
Подготовительный этап
Создаем новый проект к нашей плате по шаблону. Как это сделать — описано в предыдущих статьях. Подробно описывать этот шаг я не буду и сразу перейдем к формированию Block Design.
Нажимаем кнопку Create Block Design и в первую очередь добавляем IP-ядро с именем Zynq Processing System. После этого его можно быстро сконфигурировать с помощью tcl-скрипта. Я заготовил Preset для быстрой настройки для платы QMTech и поэтому в меню нажимаем Presets — Apply Configuration и выбираем файл который вы можете взять отсюда.

После этого нужно перейти в меню Clock Configuration — PL Fabric Clocks и включаем FCLK_CLK1 на той же частоте что и FCLK_CLK0. Этот тактовый сигнал нам понадобится для подключения логического анализатора (ILA — Integrated Logic Analyzer) и отладки полученного автомата.

После этого добавим в наш дизайн блок Processor System Reset. Выполняем предложенные автоматизации и должно получиться следующее:

Далее можно переходить к настройкам пинов через добавление Physical Constraints. Добавляем файл physical_constr и переходим в него из древа проектов для редактирования.

В файл physical_constr записываем следующее:
set_property -dict { PACKAGE_PIN P20 IOSTANDARD LVCMOS33 } [get_ports { pulse_1 } ]; set_property -dict { PACKAGE_PIN N20 IOSTANDARD LVCMOS33 } [get_ports { pulse_2 } ]; set_property -dict { PACKAGE_PIN T19 IOSTANDARD LVCMOS33 } [get_ports { pulse_3 } ];
Поясню немного по содержанию. В этом файле мы указываем имена портов которые будут использованы в дизайне и сопоставляем их реальным физическим выводам Zynq. В данном случае мы добавляем три порта для счётчиков.
Обработаем входные сигналы и сосчитаем их
Теперь можно начать с обработки входных импульсов. В первую очередь, нужно сделать модуль антизвона и ограничить частоты входных сигналов. Это сделать довольно легко, необходимо лишь перенести в проект модуль debouncer, который фигурировал в предыдущих статьях. Создаем новый source-файл, именуем его как debouncer и открываем на редактирование. Подробное объяснение по работе данного автомата я делал в этой статье и код обильно снабжен поясняющими комментариями. На разборе этого кода я останавливаться не буду.

Исходный код модуля debouncer. Ссылка.
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: - // Engineer: megaloid // // Create Date: 08/14/2021 02:39:31 PM // Design Name: Zynq Multichannel Counter // Module Name: debouncer // Project Name: // Target Devices: // Tool Versions: // Description: Debouncer for input signals from pins // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module debouncer // Параметры #( parameter CNT_WIDTH = 4 // Разрядность счётчика, выбрана подходящая для того, // чтобы можно было пропустить высокую частоту счетчика но не больше 2МГц ) // Порты ( input clk_i, // Clock input input rst_i, // Reset input input sw_i, // Switch input output reg sw_state_o, // Состояние нажатия клавиши output reg sw_down_o, // Импульс "кнопка нажата" output reg sw_up_o // Импульс "кнопка отпущена" ); reg [1:0] sw_r; // Триггер для исключения метастабильных состояний always @ (negedge rst_i or posedge clk_i) if (~rst_i) sw_r <= 2'b00; else sw_r <= {sw_r[0], ~sw_i}; reg [CNT_WIDTH-1:0] sw_count; // Счетчик для фиксации состояния wire sw_change_f = (sw_state_o != sw_r[1]); wire sw_cnt_max = &sw_count; always @(negedge rst_i or posedge clk_i) // Каждый положительный фронт сигнала clk_i проверяем, состояние на входе sw_i if (~rst_i) begin sw_count <= 0; sw_state_o <= 0; end else if(sw_change_f) // И если оно по прежнему отличается от предыдущего begin // стабильного, то счетчик инкрементируется. sw_count <= sw_count + 'd1; if(sw_cnt_max) // Счетчик достиг максимального значения. sw_state_o <= ~sw_state_o; // Фиксируем смену состояний. end else // А вот если, состояние опять равно зафиксированному стабильному, sw_count <= 0; // то обнуляем счет. Было ложное срабатывание always @(posedge clk_i) begin sw_down_o <= sw_change_f & sw_cnt_max & ~sw_state_o; // Формируем импульс при нажатии кнопки sw_up_o <= sw_change_f & sw_cnt_max & sw_state_o; // Формируем импульс при отпускании кнопки end endmodule
Следующим шагом нам необходимо сосчитать все импульсы которые прилетают из debouncer-модуля. Создаем еще один файл с исходным кодом и называем его counter. Записываем в него код достаточно простого автомата. Ссылка.
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: - // Engineer: megalloid // // Create Date: 08/14/2021 02:51:56 PM // Design Name: Zynq Multichannel Counter // Module Name: counter // Project Name: - // Target Devices: - // Tool Versions: - // Description: Pulses counter // // Dependencies: - // // Revision: - // Revision 0.01 - File Created // Additional Comments: - // ////////////////////////////////////////////////////////////////////////////////// module counter( input pulse_i, // Входной порт для сгенерированных импульсов input rst_i, // Вход для сигнала сброса значения счетчика input ena_i, // Вход для сигнала на разрешение считать импульсы output [31:0] cnt_o // Выходной сигнал значения счётчика ); reg[31:0] cnt_r = 0; // Регистр для хранения значения счётчика assign cnt_o = cnt_r; // Присваиваем регистр к выходному порту always @ (posedge pulse_i or negedge rst_i) begin // Каждый раз когда будет получен сигнал сброса или импульса if (~rst_i) begin cnt_r <= 32'b0; // Сбрасываем счетчик end else if(pulse_i && ena_i) begin cnt_r <= cnt_r + 1'b1; // Или считаем если было разрешение на счёт end end endmodule
После этого мы можем добавить в дизайн интересующее нас количество debouncer-ов и счётчиков. Я остановился на трёх каналах. Добавим эти блоки в наш дизайн, соединим порты проводниками, сделаем входные порты для debouncer-ов, переименуем их и получим следующее:

Теперь переходим к организации памяти для хранения данных счетчиков.
AXI-интерфейс, BRAM и вот это всё
Для того, чтобы осуществить обмен информации между PS и PL используется универсальный интерфейс AXI. Данный интерфейс предлагает широкий спектр функций, для организации информационного обмена внутри Zynq и по сути является универсальной шиной для самых разнообразных применений, с высокой пропускной способностью и минимальными задержками.
Глубокое рассмотрение принципов работы интерфейса AXI не планировалось в этой статье, и в Интернете очень много информации на этот счёт, и я решил не углубляться в описание возможностей AXI. Но за то расскажу о том, как применение этого интерфейса помогло мне решить поставленную задачу.
Для организации нужного нам обмена мы задействуем уже готовый, предложенный компанией Xilinx, блок AXI SmartConnect, который является преемником AXI Interconnect. В нашем случае будет достаточно использовать самый простой вариант — AXI4-Lite, который достаточно прост, и подходит для передачи небольших порций информации и позволяет только читать и записывать 32-битные слова за раз. Используется обычно для доступа к низкоскоростным периферийным устройствам. Самое то для получения и отправки команд счетчиками и выгрузки значений трёх счётчиков.
Не будем останавливаться на теории и перейдем к выполнению задачи. Добавим в наш дизайн IP-блок AXI SmartConnect и сразу зайдем в его настройки и выберем количество Slave-интерфейсов равным 1. Подключаем сигнал сброса и тактирования следующим образом:

Для хранения информации внутри ПЛИС мы можем задействовать AXI BRAM Controller и Block Memory Generator. Добавим их в дизайн и перейдем к первоначальной настройке.
Двойным кликом заходим в настройки AXI BRAM Controller-а и настраиваем настройки следующим образом:

Выбираем AXI Protocol AXI4LITE. И делаем один BRAM Interface. Остальные настройки оставляем по умолчанию. Нажимаем ОК.
Соединяем интерфейсы Master AXI с Slave AXI и подключаем тактовый сигнал и сигнал сброса к AXI BRAM Controller:

Переходим к настройке Block Memory Generator. Выбираем Memory Type как True Dual Port RAM и остальные настройки оставляем по умолчанию.

Подключаем соединения и получаем следующий вид:

Следующим шагом нужно определить какое количество памяти будет задействовано для хранения данных в BRAM. Для этого нужно зайти в верхнее меню Window — Address Editor и автоматически назначить адреса:

После этого произойдет автоматическая разметка по адресу смещения 0x4000_0000 в размере 8К. Нам этого будет достаточно для записи. Оставим значение по умолчанию.
Блок управления счетчиками
Для того, чтобы осуществить доступ к BRAM из нашей кастомной RTL-ки необходимо определить какие сигналы должны быть подключены. Итак, нам нужен BRAM_PORTB и именно по этому порту мы будем взаимодействовать с памятью. Развернем его и после этого можно увидеть набор сигналов:

Собственно все эти сигналы мы должны сформировать для того, чтобы работать с памятью. В первую очередь необходимо добавить два сигнала для Enable B Port (enb) и Reset B Port (rstb).
Для enb сделаем константу как сигнал разрешающий постоянную работу. Добавим IP-блок Contsant и откроем настройки. Выставляем ширину шины в 1 и сигнал по умолчанию — 1.

Для сигнала rstb — делаем логический элемент NOT через IP-блок Utility Vector Logic. Заходим в настройки выставляем тип NOT и ширину так же в 1.

Подключаем следующим образом:

Теперь можно перейти к созданию RTL-ки которая будет являться ключевым элементом всего проекта. Назовём его counter_mgmt. Итого нам нужно сделать целый список портов:
-
Вход для сигнала тактирования;
-
Вход для сброса;
-
Входные 32-битные шины с текущим значением счётчиков для последующей записи в память;
-
Выходные сигналы включения\выключения счётчиков;
-
Выходные сигналы для сброса значения счётчиков;
-
Выходной сигнал Write Enable для Block Memory Generator, сигнализирующий ему о том, что будет производиться запись в память;
-
Выходная 32-битная шина Address будет сообщать в какую ячейку памяти нужно будет проводить запись или из которой нужно производить чтение;
-
32-битная шина входных и выходных данных, которые будут использоваться при записи и чтении информации из BRAM;
Оформляем всё это и получается следующий набор входных портов:
////////////////////////////////////////////////////////////////////////////////// // Company: - // Engineer: Andrey Zaostrovnykh // // Create Date: 08/14/2021 02:39:31 PM // Design Name: Zynq Multichannel Counter // Module Name: counter_mgmt // Project Name: // Target Devices: // Tool Versions: // Description: Management module for counters // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module counter_mgmt( input clk_i, // Вход для сигнала тактирования input rst_i, // Вход для сброса автомата input [31:0] cnt_0_data, // Значение 1-го счетчика input [31:0] cnt_1_data, // Значение 2-го счетчика input [31:0] cnt_2_data, // Значение 3-го счетчика output cnt_0_en, // Включение 1-го счетчика output cnt_0_rst, // Сброс 1-го счетчика output cnt_1_en, // Включение 1-го счетчика output cnt_1_rst, // Сброс 1-го счетчика output cnt_2_en, // Включение 1-го счетчика output cnt_2_rst, // Сброс 1-го счетчика output we, // Сигнал Write Enable output [31:0] addr, // Адрес чтения-записи output [31:0] dout, // Выходные данные для записи в память input [31:0] din // Входные данные при чтении из памяти ); endmodule
Сохраняем код и добавляем его на общую диаграмму дизайна. Соединяем его с Block Memory Generator следующим образом:

После определения набора входных\выходных сигналов можно подумать об общей логике работы все автомата в целом.
Задумка получается следующая. Работа всего автомата в целом будет управляться через регистр, который будет являться ячейкой памяти в BRAM. То есть, наш автомат будет в постоянном режиме опрашивать эту ячейку, и если будут обнаружены какие-то команды, которые запишет PS в эту ячейку — мы будем выполнять ту или иную команду. Вторая и третья ячейка памяти будет отвечать за включение\выключение счетчиков, и сброс значения счетчика соответственно. Четвертая, пятая, шестая ячейка будут отвечать за хранение значения счётчиков для передачи из в PS.
Карта памяти получается следующая:

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

Таким образом максимальное количество счётчиков которое можно сделать при таком планировании расходования памяти — 32 штуки.
Для хранения и промежуточной обработки данных объявим два регистра для хранения 32-битных данных о состоянии сброса и состояния счетчиков. Присвоим их соответствующим выходам нашего модуля. Запишем это в файл counter_mgmt.v:
reg [31:0] cnt_rst; reg [31:0] cnt_en; assign cnt_0_rst = cnt_rst[0]; assign cnt_1_rst = cnt_rst[1]; assign cnt_2_rst = cnt_rst[2]; assign cnt_0_en = cnt_en[0]; assign cnt_1_en = cnt_en[1]; assign cnt_2_en = cnt_en[2];
Следующим шагом нужно определиться с набором команд, которые мы будем посылать на исполнение. Тут всё достаточно очевидно:

После этого можно в целом ввести соответствующие режимы в которых будет находиться данный модуль:
localparam IDLE = 4'd1; localparam EN = 4'd2; localparam EN_W = 4'd3; localparam EN_R = 4'd4; localparam RST = 4'd5; localparam RST_W = 4'd6; localparam RST_R = 4'd7; localparam WRT = 4'd8; localparam WRT_W = 4'd9; localparam WRT_R = 4'd10;
Так же добавим регистр для хранения текущего значения состояния автомата:
reg [3:0] state = IDLE;
Определим оставшиеся сигналы и регистры которые нам пригодятся при работе с памятью BRAM.
reg we_r = 1'd0;// Регистр для сигнализирования о том что будет производиться запись assign we = we_r;// Присвоение значения регистра выходному сигналу reg [31:0] addr_r = 32'd0;// Регистр для передачи адреса для записи\чтения assign addr = addr_r;// Присвоение значения регистра выходному сигналу reg [31:0] dout_r = 32'd0; // Регистр для хранения данных передаваемых в BRAM assign dout = dout_r;// Присвоение значения регистра выходному сигналу
Добавим еще регистр, который будет использован для последовательной записи значений из каждого счетчика:
reg [3:0] reg_choose = 4'h0;
Опишем, что должно происходить c Write Enable и Address при попадании автомата в тот или иной state:
always @(posedge clk_i) begin case(state) IDLE: begin // Пока состояние IDLE читаем адрес 0x0 we_r <= 1'b0; addr_r <= 32'd0; end EN: begin // Когда получена команда EN читаем адрес 0x4 we_r <= 1'b0; addr_r <= 32'd4; end EN_W: begin // Получаем значения вкл\выкл счетчиков we_r <= 1'b0; // и прочитав переходим к записи в управляющие выходы addr_r <= 32'd4; end EN_R: begin // Сбрасываем регистр команды we_r <= 1'b1; addr_r <= 32'd0; end RST: begin // Когда получена команда RST читаем адрес 0x8 we_r <= 1'b0; addr_r <= 32'd8; end RST_W: begin // Получаем значения какие счетчики надо сбросить we_r <= 1'b0; // и прочитав переходим к записи в управляющие выходы addr_r <= 32'd8; end RST_R: begin // Сбрасываем регистр команды we_r <= 1'b1; addr_r <= 32'd0; end WRT: begin // Когда получена команда WRT включаем режим записи we_r <= 1'b1; addr_r <= 32'h0; end WRT_W: begin // Делаем пробег по всем адресам и записываем значения we_r <= 1'b1; case (reg_choose) 4'd0: begin addr_r <= 32'hC; reg_choose <= 4'h1; end 4'd1: begin addr_r <= 32'h10; reg_choose <= 4'h2; end 4'd2: begin addr_r <= 32'h14; reg_choose <= 4'h3; end default: begin addr_r <= 32'h0; end endcase end WRT_R: begin// Сбрасываем значения команды we_r <= 1'b1; addr_r <= 32'd0; reg_choose <= 4'h0; end default: begin we_r <= 1'b0; addr_r <= 32'd0; end endcase end
Теперь нам нужно сделать механизм который будет в цикле опрашивать BRAM на предмет наличия той или иной команды на входе и определять текущий state автомата:
always @(posedge clk_i) begin if (rst_i == 1'b1) begin state <= IDLE; end else begin case(state) IDLE: begin// Если мы находимся в режиме IDLE case(din) // Смотрим на сигнал din подключенный к BRAM 32'd1: begin// Если получена команда 0x1 state <= EN;// Переходим в EN end 32'd2: begin// Если получена команда 0x2 state <= RST;// Переходим в RST end 32'd3: begin// Если получена команда 0x3 state <= WRT;// Переходим в WRT end default: begin// Если ни одно из значений не подошло state <= state;// То остаёмся в том же состоянии end endcase end EN: begin state <= EN_W; end EN_W: begin state <= EN_R; end EN_R: begin state <= IDLE; end RST: begin state <= RST_W; end RST_W: begin state <= RST_R; end RST_R: begin state <= IDLE; end WRT: begin state <= WRT_W; end WRT_W: begin case (reg_choose) 4'd2: begin state <= WRT_R; end default: begin state <= IDLE; end endcase end WRT_R: begin state <= IDLE; end default: begin state <= IDLE; end endcase end end
И для передачи данных сделаем еще один behavioral-блок для того, чтобы передавать данные:
always @(posedge clk_i) begin case(state) EN_R: begin cnt_en[0] <= din[0]; cnt_en[1] <= din[1]; cnt_en[2] <= din[2]; dout_r <= 32'b0; end RST_R: begin cnt_rst[0] <= din[0]; cnt_rst[1] <= din[1]; cnt_rst[2] <= din[2]; dout_r <= 32'b0; end WRT_W: begin case (reg_choose) 4'd0: begin dout_r <= cnt_0_data; end 4'd1: begin dout_r <= cnt_1_data; end 4'd2: begin dout_r <= cnt_2_data; end default: begin dout_r <= 32'h0; end endcase end WRT_R: begin dout_r <= 32'b0; end endcase end
Полный исходный код модуля доступен тут.
После этого можно подключить к ключевым выходам обмена логический анализатор. Им мы сможем воспользоваться когда будет активна PS-система. Добавить логический анализатор можно путем добавление IP-блока Integrated Logic Analyzer.
Подключаем тактирование ILA к FCLK_CLK1. И открываем настройки лог. анализатора. Выбираем Monitor Type как Native. Записываем Number of Probes в значение 13 штук.

Настраиваем пробники на ширину данных и расставляем их на схеме. Полная схема того, что вышло после подключения лог. анализатора:

К слову. Наличие логического анализатора достаточно сильно увеличивает время синтеза всей схемы в целом. Поэтому если вы уверены в работоспособности своего автомата — логический анализатор можно не добавлять. Ну а поскольку я начинающий и не верю что всё заработает с первого раза — я соединю все линии связанные с управлением счётчиком с лог. анализаторам и пойду заваривать кофе, пока идет синтез.
На будущее отмечу общую последовательность работы с лог.анализатором:
-
Заливаем из Hardware manager в Vivado bitstream-файл в ПЛИС;
-
Открываем SDK и делаем Clean Project;
-
Запускаем приложение (в случае если мы отлаживаемся в baremetal-приложении);
-
Нажимаем кнопку Refresh device и откроется логический анализатор;
-
Расставляем нужные триггеры на нужных линиях и наблюдаем за результатом;
Попробуйте подебажить самостоятельно. Там ничего сложного нет.
Проверим, что получилось
Теперь можно подготовить проект к синтезу и имплементации с генерацией bitstream-файла.
-
На основном поле дизайна нажимаем Validate Design и проверяем, всё ли хорошо.
-
В левом меню Hierarchy правой кнопкой кликаем на файле zynq.bd и делаем команду Create HDL Wrapper, выбираем Let Vivado manage wrapper and auto-update и нажимаем ОК.
-
После нажимаем команду в левой части окна Generate Block Design, выбираем Synthesis Options — Global и нажимаем Generate. Дожидаемся конца генерации и запускаем синтез через команду Generate bitstream.
Процедура синтеза будет достаточно длительной (около 10 минут).
После окончания синтеза можно экспортировать результат в SDK и протестировать результат. Сначала мы попробуем проверить проект из baremetal-приложения и загрузим всё через JTAG. Отключаем питание платы, и вытащим microSD-карту.
Нажимаем меню File — Export — Export Hardware. Ставим галочку Include bitstream. И после окончания импорта запускаем SDK через меню File — Launch SDK.
После того как откроется Xilinx SDK создаем новый проект File — New — Application Project.

Пишем имя нового проекта, нажимаем Next и берем за основу шаблонный проект Hello World. После того как создан новый проект, переходим в его иерархию и находим файл helloworld.c:

И редактируем его следующим образом:
#include <stdio.h> #include "platform.h" #include "xil_printf.h" #include "xil_io.h" #include "xparameters.h" #include "sleep.h" int main() { int num; int rev; init_platform(); // Enable all counters Xil_Out32(XPAR_BRAM_0_BASEADDR + 4, 0x7);// Регистр ENA и значение 0b111 Xil_Out32(XPAR_BRAM_0_BASEADDR + 0, 0x1);// Команда ENA xil_printf("Enable all counters \n\r"); Xil_Out32(XPAR_BRAM_0_BASEADDR + 8, 0x7);// Регистр RST и значение 0b111 Xil_Out32(XPAR_BRAM_0_BASEADDR + 0, 0x2);// Команда RST xil_printf("Counting...\n\r"); while(1) { Xil_Out32(XPAR_BRAM_0_BASEADDR + 0, 0x3);// Записываем в память текущие значения for(num = 0; num < 6 ; num++) { rev = Xil_In32(XPAR_BRAM_0_BASEADDR + num * 4); xil_printf("The data at 0x%x is 0x%x \n\r", XPAR_BRAM_0_BASEADDR + num * 4, rev); } xil_printf("\n\r"); usleep(500000); } cleanup_platform(); return 0; }
Исходный код можно взять тут.
Если описать то, что делает этот код то получается следующий алгоритм:
-
Инициализируем платформу;
-
Записываем новые значения в регистр ENA, чтобы включились первые три счётчика;
-
Отправляем команду ENA в регистр команд;
-
Записываем новые значения в регистр RST, чтобы был отключен сигнал сброса на трех счётчиках (0 — сброс активен, 1 — не активен);
-
Отправляем команду RST в регистр команд;
-
Теперь наши счетчики могут считать входящие импульсы и мы можем проводить чтение. Делаем это чтение с помощью команды WRT в цикле;
-
Выводим значения ячеек памяти в цикле;
После загружаем bitstream в FPGA через меню Xilinx — Program FPGA и нажимаем Program.

После можно стартовать проект. Кликаем правой кнопкой по проекту, выбираем пункт Run as — Launch on hardware (System debugger).
Если подать импульсы на ножки, которые мы указали в constraints-файле — то счетчики будут нарастать. Проверить это можно если открыть консоль minicom и понаблюдать за выводом:
minicom -D /dev/ttyUSBx
Бесконечно будут сыпаться значения наших регистров:
The data at 0x40000000 is 0x0 - текущее значение регистра команд The data at 0x40000004 is 0x7 - регистр вкл\выкл счётчиков The data at 0x40000008 is 0x7 - регистр сброса счётчиков The data at 0x4000000C is 0x14 - значение первого счётчика The data at 0x40000010 is 0x1E - значение второго счётчика The data at 0x40000014 is 0x18 - значение третьего счётчика
Отлично, в baremetal работает. Можно попробовать подергать импульсы из Linux.
Проверка из Linux
На этом этапе можем считать, что наша основная часть работы выполнена и можно проверить работает ли такое же взаимодействие как в baremetal из Linux. Тут всё достаточно просто, читать данные и управлять счетчиками мы будем с помощью простой userspace-программы на Си.
Для этого нам нужно:
-
Пересобрать образ BOOT.BIN собранный в прошлых занятиях с новым bitstream-файлом собранным в этом проекте и залить его на SD-карту заменив предыдущий. Прочитать об этом можно в этой статье, а готовый BOOT.BIN и все остальные файлы можно тут;
-
Установить кросс-компилятор и откомпилировать исходный файл нашей программы для Linux;
-
Загрузить программу на плату и запустить её;
-
Включив счетчики, отключив сигнал сброса, послать определенное количество импульсов и убедиться, что всё работает как задумано изначально;
Перейдем к реализации намеченного. Устанавливаем кросс-компилятор:
sudo apt install gcc-arm-linux-gnueabihf
Далее нам необходимо создать файл который мы будем компилировать и потом запускать на Zynq. В папке с проектом я создаю папку Application:
megalloid@megalloid-lenovo:~$ cd Zynq/Projects/10.LinuxMulticounter/ megalloid@megalloid-lenovo:~/Zynq/Projects/10.LinuxMulticounter$ mkdir Application megalloid@megalloid-lenovo:~/Zynq/Projects/10.LinuxMulticounter$ cd Application/
В этой папке я создаю два файла, первый файл исходного кода counter_mgmt.c и Make-файл для удобной кросс-компиляции:
megalloid@megalloid-lenovo:~/Zynq/Projects/10.LinuxMulticounter/Application$ touch counter_mgmt.c Makefile
В файл counter_mgmt.c пишем следующее содержимое:
#include <stdio.h> #include <unistd.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <signal.h> #define BRAM_CTRL_0 0x40000000 #define DATA_LEN 6 int fd; unsigned int *map_base0; int sigintHandler(int sig_num) { printf("\n Terminating using Ctrl+C \n"); fflush(stdout); close(fd); munmap(map_base0, DATA_LEN); return 0; } int main(int argc, char **argv) { signal(SIGINT, sigintHandler); fd = open("/dev/mem", O_RDWR | O_SYNC); if (fd < 0) { printf("can not open /dev/mem \n"); return (-1); } printf("/dev/mem is open \n"); map_base0 = mmap(NULL, DATA_LEN * 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, BRAM_CTRL_0); if (map_base0 == 0) { printf("NULL pointer\n"); } else { printf("mmap successful\n"); } unsigned long addr; unsigned int content; int i = 0; addr = (unsigned long)(map_base0 + 1); content = 0x7; map_base0[1] = content; printf("%2dth data, address: 0x%lx data_write: 0x%x\t\t\n", i, addr, content); addr = (unsigned long)(map_base0 + 0); content = 0x1; map_base0[0] = content; printf("%2dth data, address: 0x%lx data_write: 0x%x\t\t\n", i, addr, content); addr = (unsigned long)(map_base0 + 2); content = 0x7; map_base0[2] = content; printf("%2dth data, address: 0x%lx data_write: 0x%x\t\t\n", i, addr, content); addr = (unsigned long)(map_base0 + 0); content = 0x2; map_base0[0] = content; printf("%2dth data, address: 0x%lx data_write: 0x%x\t\t\n", i, addr, content); while(1) { addr = (unsigned long)(map_base0 + 0); content = 0x3; map_base0[0] = content; printf("%2dth data, address: 0x%lx data_write: 0x%x\t\t\n", i, addr, content); sleep(1); printf("\nread data from bram\n"); for (i = 0; i < DATA_LEN; i++) { addr = (unsigned long)(map_base0 + i); content = map_base0[i]; printf("%2dth data, address: 0x%lx data_read: 0x%x\t\t\n", i, addr, content); } } }
Ссылка на исходный код.
После редактирования сохраняем файл и открываем файл Makefile. В него записываем следующее:
CC=arm-linux-gnueabihf-gcc CFLAGS ?= -O2 -static objects = counter_mgmt.o CHECKFLAGS = -Wall -Wuninitialized -Wundef override CFLAGS := $(CHECKFLAGS) $(CFLAGS) progs = counter_mgmt counter_mgmt: $(objects) $(CC) $(CFLAGS) -o $@ $(objects) clean: rm -f $(progs) $(objects) $(MAKE) -C clean .PHONY: clean
После этого можно запустить процесс компиляции и увидеть что у нас появился исполняемый файл:
# make # file counter_mgmt counter_mgmt: ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux), statically linked, BuildID[sha1]=adfec945dc44cf0b8a702405ca2a2ae9af1b06dd, for GNU/Linux 3.2.0, not stripped
После этого можно загрузить файл на плату и проверить работает ли наша программа. Вариантов загрузки может быть несколько:
-
Предварительно положить файлы на SD-карту и после загрузки Linux примонтировать раздел на котором она будет расположена;
-
Стянуть файл по локальной сети подключив Ethernet-кабель к плате;
Первый способ достаточно прост. Скидываем через картридер откомпилированный файл на SD-карту. Открываем UART-консоль нашей отладочной платы и пишем:
# mount /dev/mmcblk0p1 /media # ls /media/ BOOT.bin counter_mgmt uImage uramdisk.image.gz system.dtb uboot.env
Второй способ тоже очень простой. Подключаем кабель Ethernet от роутера к плате. Смотрим появился ли IP-адрес:
# ifconfig eth0 eth0 Link encap:Ethernet HWaddr 82:A4:28:8C:74:0A inet addr:192.168.2.188 Bcast:192.168.2.255 Mask:255.255.255.0 inet6 addr: fe80::2a2f:f8c1:f353:e8b/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:6 errors:0 dropped:0 overruns:0 frame:0 TX packets:13 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:1404 (1.3 KiB) TX bytes:1562 (1.5 KiB) Interrupt:33 Base address:0xb000
В buildroot который мы собирали в предыдущем уроке уже настроен режим автоконфигурирования сетевого интерфейса и запущена служба DHCP для получения IP-адреса. Если у вас другой Linux — тоже присвоить IP-адрес вручную через команду:
# ifconfig eth0 192.168.2.188 netmask 255.255.255.0 up
После этого смотрим IP-адрес на компьютере, пингуем его из консоли Zynq, тем самым проверяя что связь есть и скачиваем файл по SSH (на PC должен быть включен SSH-сервер):
# ping 192.168.2.121 PING 192.168.2.121 (192.168.2.121): 56 data bytes 64 bytes from 192.168.2.121: seq=0 ttl=64 time=892.638 ms 64 bytes from 192.168.2.121: seq=1 ttl=64 time=1.057 ms 64 bytes from 192.168.2.121: seq=2 ttl=64 time=2.897 ms 64 bytes from 192.168.2.121: seq=3 ttl=64 time=2.661 ms 64 bytes from 192.168.2.121: seq=4 ttl=64 time=147.555 ms ^C --- 192.168.2.121 ping statistics --- 5 packets transmitted, 5 packets received, 0% packet loss round-trip min/avg/max = 1.057/209.361/892.638 ms # scp megalloid@192.168.2.121:/home/megalloid/Zynq/Projects/10.LinuxMulticounter/Application/counter_mgmt . Host '192.168.2.121' is not in the trusted hosts file. (ssh-ed25519 fingerprint sha1!! 78:97:ca:b2:0d:fb:49:3a:3d:d3:6a:69:a1:10:b5:92:69:95:e1:46) Do you want to continue connecting? (y/n) y counter_mgmt 100% 3490KB 3.4MB/s 00:01 # chmod +x counter_mgmt # ./counter_mgmt
Результат вывода программы должен выглядеть следующим образом:
# ./counter_mgmt /dev/mem is open mmap successful 0th data, address: 0xb6fd0004 data_write: 0x7 0th data, address: 0xb6fd0000 data_write: 0x1 0th data, address: 0xb6fd0008 data_write: 0x7 0th data, address: 0xb6fd0000 data_write: 0x2 0th data, address: 0xb6fd0000 data_write: 0x3 read data from bram 0th data, address: 0xb6fd0000 data_read: 0x0 1th data, address: 0xb6fd0004 data_read: 0x7 2th data, address: 0xb6fd0008 data_read: 0x7 3th data, address: 0xb6fd000c data_read: 0xb 4th data, address: 0xb6fd0010 data_read: 0x12 5th data, address: 0xb6fd0014 data_read: 0xa 6th data, address: 0xb6fd0000 data_write: 0x3
Видно, что у нас работают все счётчики и при подаче импульсов (в моем случае это кнопки из предыдущих уроков) — у нас наращивается значение счётчика.
Задачу можем считать решенной. Увидимся в следующих уроках! =)
P.S. Если у вас есть какие-либо интересные и не слишком сложные задачки, которые можно было бы попробовать решить с помощью Zynq — пишите мне в Telegram @megalloid. И мне будет интересно поразбираться, и возможно чем-то помогу вам. Если есть желание — можете сделать донат как скромную оплату за ту уйму времени что я потратил на подготовку этого материала…
ссылка на оригинал статьи https://habr.com/ru/articles/573066/
Добавить комментарий