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

в 20:28, , рубрики: AXI, bram, diy или сделай сам, fpga, howto, linux, qmtech, SDK, vivado, xilinx, zynq, Процессоры, Электроника для начинающих

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

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

Дисклеймер

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

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

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

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

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

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

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

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

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

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

Исходный код модуля 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-ов, переименуем их и получим следующее:

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

Теперь переходим к организации памяти для хранения данных счетчиков.

AXI-интерфейс, BRAM и вот это всё

Для того, чтобы осуществить обмен информации между PS и PL используется универсальный интерфейс AXI. Данный интерфейс предлагает широкий спектр функций, для организации информационного обмена внутри Zynq и по сути является универсальной шиной для самых разнообразных применений, с высокой пропускной способностью и минимальными задержками.

Глубокое рассмотрение принципов работы интерфейса AXI не планировалось в этой статье, и в Интернете очень много информации на этот счёт, и я решил не углубляться в описание возможностей AXI. Но за то расскажу о том, как применение этого интерфейса помогло мне решить поставленную задачу.

Для организации нужного нам обмена мы задействуем уже готовый, предложенный компанией Xilinx, блок AXI SmartConnect, который является преемником AXI Interconnect. В нашем случае будет достаточно использовать самый простой вариант - AXI4-Lite, который достаточно прост, и подходит для передачи небольших порций информации и позволяет только читать и записывать 32-битные слова за раз. Используется обычно для доступа к низкоскоростным периферийным устройствам. Самое то для получения и отправки команд счетчиками и выгрузки значений трёх счётчиков.

Не будем останавливаться на теории и перейдем к выполнению задачи. Добавим в наш дизайн IP-блок AXI SmartConnect и сразу зайдем в его настройки и выберем количество Slave-интерфейсов равным 1. Подключаем сигнал сброса и тактирования следующим образом:

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

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

Двойным кликом заходим в настройки AXI BRAM Controller-а и настраиваем настройки следующим образом:

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

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

Соединяем интерфейсы Master AXI с Slave AXI и подключаем тактовый сигнал и сигнал сброса к AXI BRAM Controller:

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

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

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

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

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

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

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

После этого произойдет автоматическая разметка по адресу смещения 0x4000_0000 в размере . Нам этого будет достаточно для записи. Оставим значение по умолчанию.

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

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

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

Собственно все эти сигналы мы должны сформировать для того, чтобы работать с памятью. В первую очередь необходимо добавить два сигнала для Enable B Port (enb) и Reset B Port (rstb).

Для enb сделаем константу как сигнал разрешающий постоянную работу. Добавим IP-блок Contsant и откроем настройки. Выставляем ширину шины в 1 и сигнал по умолчанию - 1.

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

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

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

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

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

Теперь можно перейти к созданию 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 следующим образом:

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

После определения набора входныхвыходных сигналов можно подумать об общей логике работы все автомата в целом.

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

Карта памяти получается следующая:

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

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

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

Таким образом максимальное количество счётчиков которое можно сделать при таком планировании расходования памяти - 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];

Следующим шагом нужно определиться с набором команд, которые мы будем посылать на исполнение. Тут всё достаточно очевидно:

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

После этого можно в целом ввести соответствующие режимы в которых будет находиться данный модуль:

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

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

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

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

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

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

  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.

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

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

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

И редактируем его следующим образом:

#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 nr");                

	Xil_Out32(XPAR_BRAM_0_BASEADDR + 8, 0x7);	// Регистр RST и значение 0b111
	Xil_Out32(XPAR_BRAM_0_BASEADDR + 0, 0x2);	// Команда RST

	xil_printf("Counting...nr");


	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 nr", XPAR_BRAM_0_BASEADDR + num * 4, rev);
		}

		xil_printf("nr");

		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.

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

После можно стартовать проект. Кликаем правой кнопкой по проекту, выбираем пункт 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 pointern");
     }   
     else
     {
          printf("mmap successfuln");
     }   

     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%xttn", i, addr, content);

     addr = (unsigned long)(map_base0 + 0);
     content = 0x1;
     map_base0[0] = content;

     printf("%2dth data, address: 0x%lx data_write: 0x%xttn", i, addr, content);
     
     addr = (unsigned long)(map_base0 + 2);
     content = 0x7;
     map_base0[2] = content;

     printf("%2dth data, address: 0x%lx data_write: 0x%xttn", i, addr, content);

     addr = (unsigned long)(map_base0 + 0);
     content = 0x2;
     map_base0[0] = content;

     printf("%2dth data, address: 0x%lx data_write: 0x%xttn", 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%xttn", i, addr, content);

          sleep(1);

          printf("nread data from bramn");
          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%xttn", 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://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js