Zynq 7000. Обмен информацией между PS и PL

от автора

Продолжаю повествование о том, как проходит мое изучение возможностей отладочной платы с 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! 

И тут в голове оформилась интересная задача:

  1. Нужно организовать внутри ПЛИС набор счетчиков, которые будут считать импульсы с частотой до 2МГц;

  2. Счетчики должны быть включаемыми\выключаемыми;

  3. Счетчики должны быть сбрасываемыми;

  4. Для включения, выключения и сброса счетчиков должны быть отдельные регистры управления;

  5. Счётчики должны работать независимо друг от друга и синхронно;

  6. Управление и считывание данных из счётчиков должно быть доступно из PS или Linux;

  7. Счётчики должны считать точное количество импульсов (допустимое отклонение 1-2 импульса).

Исходя из постановки задачи был сформирован четкий план действий:

  1. Создаем проект, настраиваем Processing System и Processor System Reset. 

  2. Настраиваем соответствующие пины ПЛИС, которые будут входами для импульсов;

  3. В ПЛИС делаем блок ограничения импульсов частотой более 2 МГц и проверяем генератором сигналов или другой ПЛИС;

  4. Добавляем счетчик импульсов в ПЛИС и проверяем корректность счёта сгенерировав конкретное количество импульсов другой ПЛИС;

  5. Организуем некую память в которую можно записывать и из которой можно читать данные как из ПЛИС, так и из PS-части. В этой же памяти внутри ПЛИС организуем две ячейки памяти которые будут выступать в качестве двух регистров управления (сброс, включение и выключение) и одну в качестве регистра хранения команд;

  6. Организуем блок управления счетчиками внутри ПЛИС, который сможет опрашивать память на предмет наличия команды из PS, и по команде “забирать” данные из счётчиков и сохранять их в память, сбрасывать значение и включаться\выключаться.

  7. Протестировать полученный результат с использованием 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 в размере . Нам этого будет достаточно для записи. Оставим значение по умолчанию.

Блок управления счетчиками

Для того, чтобы осуществить доступ к 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.  Итого нам нужно сделать целый список портов:

  1. Вход для сигнала тактирования;

  2. Вход для сброса;

  3. Входные 32-битные шины с текущим значением счётчиков для последующей записи в память;

  4. Выходные сигналы включения\выключения счётчиков;

  5. Выходные сигналы для сброса значения счётчиков;

  6. Выходной сигнал Write Enable для Block Memory Generator, сигнализирующий ему о том, что будет производиться запись в память;

  7. Выходная 32-битная шина Address будет сообщать в какую ячейку памяти нужно будет проводить запись или из которой нужно производить чтение;

  8. 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 штук

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

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

На будущее отмечу общую последовательность работы с лог.анализатором:

  1. Заливаем из Hardware manager в Vivado bitstream-файл в ПЛИС;

  2. Открываем SDK и делаем Clean Project;

  3. Запускаем приложение (в случае если мы отлаживаемся в baremetal-приложении);

  4. Нажимаем кнопку Refresh device и откроется логический анализатор;

  5. Расставляем нужные триггеры на нужных линиях и наблюдаем за результатом;

Попробуйте подебажить самостоятельно. Там ничего сложного нет.

Проверим, что получилось

Теперь можно подготовить проект к синтезу и имплементации с генерацией bitstream-файла. 

  1. На основном поле дизайна нажимаем Validate Design и проверяем, всё ли хорошо. 

  2. В левом меню Hierarchy правой кнопкой кликаем на файле zynq.bd и делаем команду Create HDL Wrapper, выбираем Let Vivado manage wrapper and auto-update и нажимаем ОК

  3. После нажимаем команду в левой части окна 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; } 

Исходный код можно взять тут.

Если описать то, что делает этот код то получается следующий алгоритм:

  1. Инициализируем платформу;

  2. Записываем новые значения в регистр ENA, чтобы включились первые три счётчика;

  3. Отправляем команду ENA в регистр команд;

  4. Записываем новые значения в регистр RST, чтобы был отключен сигнал сброса на трех счётчиках (0 — сброс активен, 1 — не активен);

  5. Отправляем команду RST в регистр команд;

  6. Теперь наши счетчики могут считать входящие импульсы и мы можем проводить чтение. Делаем это чтение с помощью команды WRT в цикле;

  7. Выводим значения ячеек памяти в цикле;

После загружаем 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-программы на Си.

Для этого нам нужно:

  1. Пересобрать образ BOOT.BIN собранный в прошлых занятиях с новым bitstream-файлом собранным в этом проекте и залить его на SD-карту заменив предыдущий.  Прочитать об этом можно в этой статье, а готовый BOOT.BIN и все остальные файлы можно тут;

  2. Установить кросс-компилятор и откомпилировать исходный файл нашей программы для Linux;

  3. Загрузить программу на плату и запустить её;

  4. Включив счетчики, отключив сигнал сброса, послать определенное количество импульсов и убедиться, что всё работает как задумано изначально;

Перейдем к реализации намеченного. Устанавливаем кросс-компилятор:

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

После этого можно загрузить файл на плату и проверить работает ли наша программа. Вариантов загрузки может быть несколько:

  1. Предварительно положить файлы на SD-карту и после загрузки Linux примонтировать раздел на котором она будет расположена;

  2. Стянуть файл по локальной сети подключив 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *