Modern technology gives us many things.

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

149

Уровень сложности Средний Время на прочтение 22 мин Количество просмотров 2.6K Блог компании Timeweb Cloud FPGA *Производство и разработка электроники *DIY или Сделай сам Электроника для начинающих Туториал После того, как Я реализовал битовый контроллер I2C Master — уж очень чесались руки опробовать его в реальной задаче. Теперь можно начинать строить уровни абстракции от манипуляции отдельными битами и уже формировать полноценные транзакции, которые приводят к какому-либо действию с подчиненным устройством. Я подумал, что было бы классно сделать такую проверку своего автомата во взаимодействии с простейшей I2C 2K-bit EEPROM.

Идея простая — читаем и записываем данные по нажатию клавиш на одной из отладок с Cyclone IV, которые я рассматривал в одном из своих обзоров.

Если материал вам кажется интересным — добро пожаловать, с удовольствием и в свойственной мне манере расскажу, чего мне удалось добиться, а чего не удалось. 🙂

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

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

❯ Давайте для начала определимся с общей идеей

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

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

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

Необходимо также выводить ACK-сигнал на плату, чтобы увидеть что транзакция выполнена успешно.

Раз мы хотим организовать общение с EEPROM — то:

  1. Первой клавишей будет активировано действие на чтение данных из ячейки с заданным адресом и вывод прочитанных данных на семисегментный дисплей;
  2. Вторая клавиша будет производить запись выбранного значения в заданную ячейку памяти;
  3. Третья клавиша будет предназначена для инкремента значения текущего адреса памяти EEPROM в который будет произведена запись значения в диапазоне от 0x00 до 0xFF, ну или чтение;
  4. Четвертая клавиша будет декрементировать значение адреса памяти;
  5. Пятая клавиша будет предназначена для инкремента значения полезных данных, которые будут записаны в выбранный адрес ячейки памяти;
  6. Шестая, соответственно, будет декрементировать это значение;
  7. На плате клавиша RESET будет отвечать за асинхронный сброс.

Выглядит как набор небольших задач, при выполнении которых получится то что нужно. Поехали.

❯ Что необходимо для выполнения задачи?

Итак, для реализации задачи понадобится:

  1. Отладочная плата Saylinx с Cyclone IV, которую я обозревал в этой статье. Она подходит для моей цели как раз потому что на плате есть EEPROM и семисегментный индикатор;
  2. Программатор Altera USB Blaster для прошивки платы и отладки;
  3. На плате понадобится семисегментный индикатор. У индикатора есть 8 разрядов, первые два из которых мы задействуем под указание того, какой адрес памяти сейчас выбран, третий и четвертый — под выбор полезных данных для записи в ячейку, пятый и шестой — под вывод считанных данных из EEPROM;
  4. На плате должен быть EEPROM. И он там есть 🙂;
  5. Платка с кнопками и соединительными проводками для PLS-гребенки, потому что на отладке их не так много как хочется;
  6. Логический анализатор, типа какого-нибудь DSLogic Basic и программа DSView (можно найти на Али). Он понадобится также для наблюдения за транзакциями в реальном железе.

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

❯ Шаг нулевой. Создаем проект и размечаем пины

Этот шаг я описывать отдельно не буду, думаю вы уже научились из прошлых статей создавать новый проект и добавлять в него новые файлы. После этого необходимо добавить Top-Level Design File и указать его в настройках проекта.

Следующий подготовительный этап — создать главный модуль и определить входные и выходные сигналы:

module top_module ( input clk_i, // Входной сигнал для тактирования input rstn_i, // Входной сигнал для сброса, 0 — сброс input [2:0] btn_main_i, // Входной сигнал от кнопок на плате input [5:0] btn_brd_i, // Входные сигналы от платы с кнопками output [3:0] led_brd_o, // Выходные сигналы для светодиодов output [5:0] seg_sel_o, // Выбор активного разряда семисегментного дисплея output [7:0] seg_data_o, // Данные для отображения на семисегментном дисплее inout sda_io, // Линия I2C SDA output scl_io // Линия I2C SCL ); endmodule
После этого можно сразу обозначить пины, которые будут передавать эти сигналы, чтобы не возвращаться к этому вопросу. Для этого необходимо открыть схематик платы и определить пины, к которым подключено то, что нам нужно. Сразу же стоит обратить внимание каким образом осуществлено подключение, чтобы выбрать правильный способ управления сигналами.

Начнем с сигнала системного тактирования. Видим, что на плате установлен кварцевый генератор с частотой 50MHz:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

И подключен CLK к ножке E1:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Найдем в схеме светодиоды. На плате у нас их всего 4 штуки:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Переходим к разделу схемы где цепи LEDx подключаются к ПЛИС:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Отлично. LED0 — E10, LED1 — F9, LED2 — C9, LED3 — D9. Я обычно записываю эти данные на на отдельный листочек, чтобы потом в Pin Planner сразу указать нужные пины, не перерывая схематик снова.

Идём дальше. Кнопки. Качество китайских схематиков как обычно “на высоте” и ссылок по цепям нет, поэтому включаем поиск и ищем по ключевым словам.

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Находим кнопки KEY1, KEY2, KEY3 на ножках M15, M16, E16 соответственно:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

И кнопку RESET — тут, на N13:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

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

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

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

  • кнопка записи — подключаем к пину N2;
  • кнопка чтения — подключаем к пину P1;
  • кнопка прибавления единицы к значению адреса ячейки памяти — пин P2;
  • кнопка вычитания единицы из значения адреса выбранной ячейки памяти — пин R1;
  • кнопка прибавления единицы к записываемому числу в ячейку памяти — пин P8;
  • кнопка вычитания единицы из записываемого числа в ячейку памяти — пин K9.

Далее переходим к семисегментному индикатору:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Тут пины все подписаны. Не буду их дополнительно перечислять. Хоть где-то сделали нормальное указание цепей, чтобы не блуждать по схематику 😀 в поисках пина, к которому подключена периферия. О принципе работы семисегментника я расскажу чуть позже.

И остается последний штрих — пины SDA и SCL микросхемы EEPROM:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

И самые внимательные читатели заметят — что на пинах D1, E6 находятся сигналы выбора SEL6 и SEL7 и сигналы SCL, SDA. Поэтому последние два разряда мы не сможем задействовать в нашем проекте. И они будут постоянно показывать всякую хрень, будем держать это во внимании.

Теперь можно скомпилировать проект и перейти в Pin Planner (Assignments — Pin Planner) чтобы занести значения пинов. У меня получился вот такой внушительный список:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Обратите внимание, что I/O Standard указан 3.3-V LVTTL. Указываем и закрываем данное окно. Теперь можно к этому этапу больше не возвращаться, если не собираетесь менять имена цепей, иначе их придется размечать заново на новые имена.

❯ Шаг первый. Драйвер LED-ов

В первую очередь стоит начать с самых простых задач, чтобы раскачать энтузиазм — сделаем простой драйвер для LED-индикаторов.

Добавляем в проект файл led_driver.v и в нём мы пишем простую логику управления сигналами. Думаю в дополнительном комментировании она не нуждается:

module led_driver ( input clk_i, // Сигнал тактирования input rstn_i, // Сигнал асинхронного сброса input state_i, // Входное значение, 1 — горит, 0 — не горит output led_o // Выходной сигнал для LED ); reg led_r; always @ (posedge clk_i or negedge rstn_i) begin if (~rstn_i) begin led_r <= 0; end else if(state_i) begin led_r <= 1’b1; end else begin led_r <= 1’b0; end end assign led_o = led_r; endmodule
После этого можно добавить эти модули в Top Level Design и идти дальше:

Читать на TechLife:  Сети 5G появятся в крупных российских городах с 2026 года

//################################################# // LED Drivers //################################################# reg ack_bit_r; // Регистр для хранения значения ACK wire ack_bit_w; // Провод который будет идти из I2C Bit Controller reg led_write_pulse_r; // Для отладки: индикатор нажатия кнопки Write reg led_read_pulse_r; // Для отладки: индикатор нажатия кнопки Read // ACK bit LED led_driver led_driver_m0 ( .clk_i (clk_i), .rstn_i (rstn_i), .state_i (ack_bit_w), .led_o (led_brd_o[0]) ); // Pulse Write LED led_driver led_driver_m1 ( .clk_i (clk_i), .rstn_i (rstn_i), .state_i (led_read_pulse_r), .led_o (led_brd_o[1]) ); // Pulse Read LED led_driver led_driver_m2 ( .clk_i (clk_i), .rstn_i (rstn_i), .state_i (led_write_pulse_r), .led_o (led_brd_o[2]) );
Последний, 4-й светодиод оставим незадействованным. Идем дальше.

❯ Шаг второй. Драйвер кнопок

Следующим шагом необходимо накидать модуль обработки входных сигналов с ножек GPIO для того, чтобы использовать их потом в качестве “рычагов” для определенных экшенов.

Все знают, что дребезг механических кнопок и переключателей — это стандартная проблема, которая требует дополнительного модуля обработки и фильтрации. Об этом я писал в этой статье, в главе “Модуль Debouncer”.

Выглядит эта ситуация вот таким образом:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Итак. Добавим в проект файл с именем gpio_debouncer.v. В этот раз я решил накидать несколько видоизмененный модуль. Общий принцип остается таким же как и в прошлых статьях — когда нажата кнопка и удерживается необходимое количество времени (очень короткий по человеческим ощущениям период) — запускается счетчик который достигая определенного значения — передает значение на выходной порт. Если кнопка отжимается — то сигнал устанавливается в ноль. В дополнение к этому генерируется импульс на нажатие кнопки, и импульс на момент отжатия кнопки. Они, точнее один из импульсов — нам очень пригодится в будущем.

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

module gpio_debouncer // Порты ( input clk_i, // Сигнал тактирования input rstn_i, // Сигнал асинхронного сброса input button_i, // Сигнал с физической кнопки output reg button_posedge_r, // Импульс на нажатие кнопки output reg button_negedge_r, // Импульс на отжатие кнопки output reg button_out_r // Фильтрованный сигнал с кнопки ); endmodule
Следующим шагом необходимо определить какой величины будет счётчик, какова минимальная длительность нажатия, чтобы ее считать стабильным нажатием а не набором импульсов, которым из себя представляет момент нажатия механической кнопки.

Для этого я сделал ряд служебных параметров у данного модуля:

// Глобальные параметры #( parameter CNT_WIDTH = 32, // Разрядность счетчика таймера parameter FREQ = 50, // Глобальная частота тактирования parameter MAX_TIME = 20 // Длительность стабильного удержание кнопки ) // Локальные параметры localparam TIMER_MAX_VAL = MAX_TIME * 1000 * FREQ; // Максимальное значение таймера
Далее необходимо объявить несколько вспомогательных регистров (D-триггеров) и флаг сброса счётчика если детектирован дребезг. Они будут выполнять функцию сброса счетчика, если их значения будут отличаться, т.е. когда будет дребезг контактов в виде хаотичного изменения входного сигнала — сбрасываем счётчик:

// Input flip-flops reg DFF1; reg DFF2; wire q_reset; assign q_reset = (DFF1 ^ DFF2); // XOR для наблюдения за дребезгом
Еще нужно объявить два регистра для счётчика и один вспомогательный флаг:

// Timing regs reg [CNT_WIDTH-1:0] q_reg; // Регистр счетчика reg [CNT_WIDTH-1:0] q_next; // Вспомогательный регистр wire q_add; assign q_add = ~(q_reg == TIMER_MAX_VAL); // Флаг на разрешение инкремента счётчика
Теперь нужно сделать поведенческий блок, который будет осуществлять прибавление счётчика. Сигналов на изменение поведения будет несколько:

  • q_reset — это сигнал о том, что нужно сбросить счетчик;
  • q_add — это сигнал о том, что можно производить инкремент счётчика;
  • q_reg — это сам счётчик.

Общая идея заключается в том, что если сигнала q_reset нет, а есть сигнал на q_add, т.е. не достигнут максимум счётчика — то прибавляем значение.

always @(q_reset, q_add, q_reg) begin case({q_reset , q_add}) 2’b00 : // Достигнут максимум счётчика q_next <= q_reg; // Оставляем значение тем же 2’b01 : // Не достигнут максимум q_next <= q_reg + 1; // Добавляем к значению +1 default : // Все остальные случаи q_next <= {CNT_WIDTH {1’b0}}; // Обнуляем значение endcase end
И добавим блок, который будет обновлять значение q_reg и реагировать на входной сигнал:

always @(posedge clk_i or negedge rstn_i) begin if(rstn_i == 1’b0) begin // Если произошел сброс то обнуляем значения DFF1 <= 1’b0; DFF2 <= 1’b0; q_reg <= {CNT_WIDTH {1’b0}}; end else begin DFF1 <= button_i; // Фиксируем входное значение DFF2 <= DFF1; // Передаем его второму D-триггеру q_reg <= q_next; // Обновляем значение счётчика end end
Добавим блок для формирования выходного сигнала в случае если достигнут предел счёта:

always @(posedge clk_i or negedge rstn_i) begin if(rstn_i == 1’b0) button_out_r <= 1’b1; else if(q_reg == TIMER_MAX_VAL) button_out_r <= DFF2; else button_out_r <= button_out_r; end
И добавим блок для формирования импульсов:

reg button_out_d0_r; always @(posedge clk_i or negedge rstn_i) begin if(rstn_i == 1’b0) begin button_out_d0_r <= 1’b1; button_posedge_r <= 1’b0; button_negedge_r <= 1’b0; end else begin button_out_d0_r <= button_out_r; button_posedge_r <= ~button_out_d0_r & button_out_r; button_negedge_r <= button_out_d0_r & ~button_out_r; end end
В итоге получился следующий модуль:

module gpio_debouncer // Global parameters #( parameter CNT_WIDTH = 32, // Debounce timer bitwidth parameter FREQ = 50, // Global clock (MHz) parameter MAX_TIME = 20 // Total delay time in ms ) // Ports ( input clk_i, // Clock input input rstn_i, // Reset input input button_i, output reg button_posedge_r, output reg button_negedge_r, output reg button_out_r ); localparam TIMER_MAX_VAL = MAX_TIME * 1000 * FREQ; // Maximum timer value // Timing regs reg [CNT_WIDTH-1:0] q_reg; reg [CNT_WIDTH-1:0] q_next; // Input flip-flops reg DFF1; reg DFF2; // Control flags wire q_add; wire q_reset; reg button_out_d0_r; // Continous assignment for counter control assign q_reset = (DFF1 ^ DFF2); assign q_add = ~(q_reg == TIMER_MAX_VAL); // Combo counter to manage q_next always @(q_reset, q_add, q_reg) begin case({q_reset , q_add}) 2’b00 : q_next <= q_reg; 2’b01 : q_next <= q_reg + 1; default : q_next <= {CNT_WIDTH {1’b0}}; endcase end // Flip flop inputs and q_reg update always @(posedge clk_i or negedge rstn_i) begin if(rstn_i == 1’b0) begin DFF1 <= 1’b0; DFF2 <= 1’b0; q_reg <= {CNT_WIDTH {1’b0}}; end else begin DFF1 <= button_i; DFF2 <= DFF1; q_reg <= q_next; end end // Counter control always @(posedge clk_i or negedge rstn_i) begin if(rstn_i == 1’b0) button_out_r <= 1’b1; else if(q_reg == TIMER_MAX_VAL) button_out_r <= DFF2; else button_out_r <= button_out_r; end always @(posedge clk_i or negedge rstn_i) begin if(rstn_i == 1’b0) begin button_out_d0_r <= 1’b1; button_posedge_r <= 1’b0; button_negedge_r <= 1’b0; end else begin button_out_d0_r <= button_out_r; button_posedge_r <= ~button_out_d0_r & button_out_r; button_negedge_r <= button_out_d0_r & ~button_out_r; end end endmodule
В итоге можете сделать testbench-файл в котором можно поэкспериментировать с входными сигналами и отследить как работает данный модуль. Но если останавливаться на этом в этой статье, она получится крайне объемной, поэтому идем дальше.

Сразу же добавим в Top Level Design все экземпляры модуля для обработки сигналов с кнопок:

//################################################# // GPIO Buttons Debouncers //################################################# wire btn_read_negedge_w; wire btn_write_negedge_w; wire btn_reg_p_negedge_w; wire btn_reg_n_negedge_w; wire btn_data_p_negedge_w; wire btn_data_n_negedge_w; gpio_debouncer gpio_debouncer_m0 ( .clk_i (clk_i), .rstn_i (rstn_i), .button_i (btn_brd_i[0]), .button_out_r (), .button_negedge_r (), .button_posedge_r (btn_read_negedge_w) ); gpio_debouncer gpio_debouncer_m1 ( .clk_i (clk_i), .rstn_i (rstn_i), .button_i (btn_brd_i[1]), .button_out_r (), .button_negedge_r (), .button_posedge_r (btn_write_negedge_w) ); gpio_debouncer gpio_debouncer_m2 ( .clk_i (clk_i), .rstn_i (rstn_i), .button_i (btn_brd_i[2]), .button_out_r (), .button_negedge_r (), .button_posedge_r (btn_reg_p_negedge_w) ); gpio_debouncer gpio_debouncer_m3 ( .clk_i (clk_i), .rstn_i (rstn_i), .button_i (btn_brd_i[3]), .button_out_r (), .button_negedge_r (), .button_posedge_r (btn_reg_n_negedge_w) ); gpio_debouncer gpio_debouncer_m4 ( .clk_i (clk_i), .rstn_i (rstn_i), .button_i (btn_brd_i[4]), .button_out_r (), .button_negedge_r (), .button_posedge_r (btn_data_p_negedge_w) ); gpio_debouncer gpio_debouncer_m5 ( .clk_i (clk_i), .rstn_i (rstn_i), .button_i (btn_brd_i[5]), .button_out_r (), .button_negedge_r (), .button_posedge_r (btn_data_n_negedge_w) );
Перейдем дальше к следующему элементу нашей конструкции.

❯ Шаг третий. Управление семисегментным индикатором

Данная задача делится на два этапа. Первый — это декодер 8-битных значений регистров на символы для каждого из сегментов. Второй — это главный драйвер, который будет осуществлять вывод данных на сегменты.

Разберемся сначала со схемотехникой индикатора, откроем схему:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

У семисегментников, в каждом разряде используются одни и те же пины для включения конкретно взятых сегментов. И 4 пина которые отвечают за зажигание отдельно взятого разряда.

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

Сделаем декодер бинарных данных в формат для вывода на дисплей. Создаем файл seg_decoder.v и пишем код модуля. Тут все просто и очевидно:

Читать на TechLife:  Заправка бензином Аи-92, дешевый ремонт при обрыве ремня ГРМ, не потеющая ГБЦ и расчет на «экстремальное» вождение. Все подробности о новом моторе Lada 1.8 Evo

module seg_decoder ( input[3:0] bin_data_i, // Binary data input output reg[6:0] seg_data_o // Seven segments LED output ); always@(*) begin case(bin_data_i) 4’d0: seg_data_o <= 7’b1000000; 4’d1: seg_data_o <= 7’b1111001; 4’d2: seg_data_o <= 7’b0100100; 4’d3: seg_data_o <= 7’b0110000; 4’d4: seg_data_o <= 7’b0011001; 4’d5: seg_data_o <= 7’b0010010; 4’d6: seg_data_o <= 7’b0000010; 4’d7: seg_data_o <= 7’b1111000; 4’d8: seg_data_o <= 7’b0000000; 4’d9: seg_data_o <= 7’b0010000; 4’hA: seg_data_o <= 7’b0001000; 4’hB: seg_data_o <= 7’b0000011; 4’hC: seg_data_o <= 7’b1000110; 4’hD: seg_data_o <= 7’b0100001; 4’hE: seg_data_o <= 7’b0000110; 4’hF: seg_data_o <= 7’b0001110; default:seg_data_o <= 7’b1111111; endcase end endmodule
Создадим модуль для вывода данных. Создаем файл seg_scan.v и сделаем заготовку модуля. В целом он также достаточно простой:

module seg_scan ( input clk_i, input rstn_i, output reg[7:0] seg_sel_o, // Выбор разряда output reg[7:0] seg_data_o, // Выбор сегмента input[7:0] seg_data_0_i, input[7:0] seg_data_1_i, input[7:0] seg_data_2_i, input[7:0] seg_data_3_i, input[7:0] seg_data_4_i, input[7:0] seg_data_5_i, input[7:0] seg_data_6_i, input[7:0] seg_data_7_i ); endmodule
Добавляем параметры для тонкой настройки:

// Global parameters #( parameter SCAN_FREQ = 200; // Частота обновления данных parameter CLK_FREQ = 50000000; // Системная частота тактирования parameter SCAN_COUNT = CLK_FREQ / (SCAN_FREQ * 8) — 1; )
Введем несколько вспомогательных регистров:

reg [31:0] scan_timer_r; // Scan time counter reg [3:0] scan_sel_r; // Scan select counter
Добавляем поведенческий блок, который будет с определенным таймаутом включать сегменты:

always@(posedge clk_i or negedge rstn_i) begin if(~rstn_i) begin scan_timer_r <= 32’d0; scan_sel_r <= 4’d0; end else if(scan_timer_r >= SCAN_COUNT) begin scan_timer_r <= 32’d0; if(scan_sel_r == 4’d5) scan_sel_r <= 4’d0; else scan_sel_r <= scan_sel_r + 4’d1; end else begin scan_timer_r <= scan_timer_r + 32’d1; end end
И добавляем управление пином выбора сегмента с параллельным выставлением данных на сегменты:

always@(posedge clk_i or negedge rstn_i) begin if(~rstn_i) begin seg_sel_o <= 8’b1111_1111; seg_data_o <= 8’hFF; end else begin // Digital LEDs choose case(scan_sel_r) 4’d0: begin seg_sel_o <= 8’b1111_1110; seg_data_o <= seg_data_0_i; end 4’d1: begin seg_sel_o <= 8’b1111_1101; seg_data_o <= seg_data_1_i; end 4’d2: begin seg_sel_o <= 8’b1111_1011; seg_data_o <= seg_data_2_i; end 4’d3: begin seg_sel_o <= 8’b1111_0111; seg_data_o <= seg_data_3_i; end 4’d4: begin seg_sel_o <= 8’b1110_1111; seg_data_o <= seg_data_4_i; end 4’d5: begin seg_sel_o <= 8’b1101_1111; seg_data_o <= seg_data_5_i; end 4’d6: begin seg_sel_o <= 8’b1011_1111; seg_data_o <= seg_data_6_i; end 4’d7: begin seg_sel_o <= 8’b0111_1111; seg_data_o <= seg_data_7_i; end default: begin seg_sel_o <= 8’b1111_1111; seg_data_o <= 8’hFF; end endcase end end
Тут тоже все очень просто, кажется что комментировать тут нечего. Значение 0 в конкретном разряде выбирает конкретный сегмент, потому что установлены PNP-транзисторы для управления сопротивлением канала. При этом выставляется соответствующее значение для набора сегментов, в соответствии с данными.

Добавим в Top Level модуль экземпляры вышеописанных модулей для работы с семисегментным дисплеем:

//################################################# // 7 Segments Display Drivers //################################################# wire[6:0] seg_data_0_w; wire[6:0] seg_data_1_w; wire[6:0] seg_data_2_w; wire[6:0] seg_data_3_w; wire[6:0] seg_data_4_w; wire[6:0] seg_data_5_w; seg_decoder seg_decoder_m0 ( .bin_data_i (reg_addr_r[7:4]), .seg_data_o (seg_data_0_w) ); seg_decoder seg_decoder_m1 ( .bin_data_i (reg_addr_r[3:0]), .seg_data_o (seg_data_1_w) ); seg_decoder seg_decoder_m2 ( .bin_data_i (data_write_r[7:4]), .seg_data_o (seg_data_2_w) ); seg_decoder seg_decoder_m3 ( .bin_data_i (data_write_r[3:0]), .seg_data_o (seg_data_3_w) ); seg_decoder seg_decoder_m4 ( .bin_data_i (read_data_r[7:4]), .seg_data_o (seg_data_4_w) ); seg_decoder seg_decoder_m5 ( .bin_data_i (read_data_r[3:0]), .seg_data_o (seg_data_5_w) ); // Main driver for 7-seg display seg_scan seg_scan_m0 ( .clk_i (clk_i), .rstn_i (rstn_i), .seg_sel_o (seg_sel_o), .seg_data_o (seg_data_o), .seg_data_0_i ({1’b1, seg_data_0_w}), .seg_data_1_i ({1’b1, seg_data_1_w}), .seg_data_2_i ({1’b1, seg_data_2_w}), .seg_data_3_i ({1’b1, seg_data_3_w}), .seg_data_4_i ({1’b1, seg_data_4_w}), .seg_data_5_i ({1’b1, seg_data_5_w}), .seg_data_6_i ({1’b1, 7’b1111_111}), // Don’t use this segments, busy by I2C .seg_data_7_i ({1’b1, 7’b1111_111}) // Don’t use this segments, busy by I2C );
Данный HDL-код легко читаем и в дополнительном комментировании, уверен, не нуждается. Идём дальше.

r

❯ Шаг четвертый. Делитель частоты для I2C Bit Controlle

Теперь необходимо подготовить делитель частоты для основного контроллера I2C, который мы создавали в прошлой статье. Создадим файл clock_divider.v и создадим модуль делителя:

module clock_divider ( input clk_i, output reg clk_o ); endmodule
Добавляем параметры для тонкой настройки:

// Global parameters #( parameter DIVISOR = 32; // Частота обновления данных, )
Логика делителя очень простая. На вход модуля подается основной тактовый сигнал в 50 MHz и с помощью счетчика, при достижении определенного значения параметра DIVISOR, производится инверсия выходного сигнала.

Добавляем регистр счётчика и поведенческий блок:

reg[27:0] counter = 28’d0; always @(posedge clk_i) begin counter <= counter + 28’d1; if(counter >= (DIVISOR — 1)) counter <= 28’d0; clk_o <= (counter < DIVISOR / 2) ? 1’b1 : 1’b0; end
Вставляем данный модуль в Top Level Design модуль:

//################################################# // Clock Divider for I2C Bit Controller //################################################# wire clk_div_w; clock_divider clock_divider_m0 ( .clk_i (clk_i), .clk_o (clk_div_w), );
Так. Со всеми простыми элементами дизайна мы разобрались — осталось самое сложное (для меня), с чем я дольше всего ломал голову.

❯ Шаг пятый. Главный модуль управления I2C сигналами

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

Решение я “рожал” достаточно долго, потому что приходилось учитывать целую совокупность факторов, которые должны были сойтись и синхронно отрабатывать то что мне нужно. И главный вопрос, который нужно было решить — каким образом детектировать момент когда можно переходить к выставлению следующей команды и очередной порции данных. Самый простой и очевидный способ — это ввести счетчик завершения выполнения отдельных транзакций, который будет инкрементироваться от изменения сигнала ready.

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

Итак, опишу, что в итоге получилось. Добавим модуль и вспомогательные элементы сразу же в Top Level Design модуль. Первый элемент это адрес Slave-устройства, т.е. нашей EEPROM. Посмотрев в Datasheet данной EEPROM и на подключение ее ножек адреса, стало ясно, что адрес на чтение будет 0xA1, а на запись 0xA0. Поэтому Contol Byte мы будем клеить из двух частей 7’b1010000 и бита операции. Далее увидите как это выглядит.

Добавим это в модуль:

//################################################# // Main Operation FSM //################################################# // Адрес EEPROM localparam SLAVE_ADDR = 7’b1010000;
Я постоянно забывал, какой бит выставляется в Control Byte с адресом Slave-устройства для чтения, а какой бит для записи. В итоге просто записал константы для удобства использования:

// Биты для подставления в Control Byte I2C localparam READ_BIT = 1’b1; localparam WRITE_BIT = 1’b0;
Для управления транзакциями — мне потребовался отдельный автомат с конечными состояниями и его возможные варианты состояний и регистр для их хранения сразу же и объявим:

// Состояния FSM и регистр для них localparam IDLE_STATE = 4’d1; localparam WRITE_STATE = 4’d2; localparam READ_STATE = 4’d3; localparam WAIT_STATE = 4’d4; reg [3:0] state_r = IDLE_STATE;
Набор команд, которые мы будем подавать на вход I2C Bit Controller — тоже заранее объявим тут:

// Те самые команды из прошлого урока localparam START_CMD = 4’d1; localparam WR_CMD = 4’d2; localparam RD_CMD = 3’d3; localparam STOP_CMD = 4’d4; localparam RESTART_CMD = 4’d5; reg [2:0] cmd_r = START_CMD;
В определенных местах мне понадобилась искусственная задержка и пришлось подставить костыль, когда не срабатывала последняя команда STOP. Позже покажу где я его поставил, может вы придумаете более изящное решение получившейся у меня проблемы. Объявим регистр для хранения значения таймера:

// Счетчик таймера reg [31:0] timer_r;
Для старта операций в I2C Bit Controller необходим специальный сигнал — wr_i2c. Объявим для него свой регистр:

// Команда для старта операций в I2C Bit Ctrl reg wr_i2c_r;
Для передачи адреса Slave устройства, адреса ячейки памяти и значения — объявим три регистра:

reg [6:0] slave_addr_r = SLAVE_ADDR; reg [7:0] reg_addr_r = 0; reg [7:0] data_write_r = 0;
Для читаемых и записываемых данных объявим регистры-буферы:

// Data buffers reg [7:0] read_data_r; wire [7:0] read_data_w; reg [7:0] write_data_r;
Для бита ACK-так же необходимо своё хранилище и провод который будет идти от модуля I2C Bit Controller:

reg ack_bit_r; wire ack_bit_w;
Для сигнала готовности модуля — тоже необходим отдельный сигнал:

wire ready_w;
Добавим в Top Level Design модуль из прошлого урока (отладочный сигнал state_o, который я использовал в прошлом уроке — я убрал):

//################################################# // I2C Bit Controller //################################################# i2c_bit_controller i2c_bit_controller_m0 ( .rstn_i(rstn_i), // Сигнал асинхронного сброса .clk_i(clk_div_w), // Поделенная частота .wr_i2c_i(wr_i2c_r), // Сигнал на старт транзакций .cmd_i(cmd_r), // Команда для исполнения .din_i(write_data_r), // Данные для записи .dout_o(read_data_w), // Прочитанные данные .ack_o(ack_bit_w), // ACK-бит .ready_o(ready_w), // Сигнал готовности модуля .sda_io(sda_io), // Линия данных SDA .scl_io(scl_io) // Линия тактового сигнала SCL );
Поскольку напрямую регистры для данных и ACK-бита подключить к модулю не получится (на самом деле не понял до конца почему), необходимо сделать поведенческий блок, который будет сохранять значение прочитанных данных и ACK-бита в регистр:

Читать на TechLife:  Huawei победила американские санкции: у китайской компании появился собственный 5-нанометровый процессор

always @(*) begin read_data_r <= read_data_w; ack_bit_r <= ack_bit_w; end
Следующим шагом сделаем обработчик действий на кнопки для выбора регистра записи и данных для записи:

always @(posedge clk_i or negedge rstn_i) begin if(rstn_i == 1’b0) begin reg_addr_r <= 0; data_write_r <= 0; end else begin if(btn_reg_p_negedge_w) begin reg_addr_r = reg_addr_r + 1; end if(btn_reg_n_negedge_w) begin reg_addr_r = reg_addr_r — 1; end if(btn_data_p_negedge_w) begin data_write_r = data_write_r + 1; end if(btn_data_n_negedge_w) begin data_write_r = data_write_r — 1; end end end
Выглядит очень просто, и кажется что никаких дополнительных пояснений тут не требуется.

Добавим поведенческий блок, который будет подсчитывать количество выполненных транзакций в случае записи или чтения данных. Каждое возведение сигнала ready_w в значение логической единицы — будет основным сигналом для поведенческого блока и счётчик будет увеличиваться. У каждой из операций — есть конечное количество отдельных транзакций, которые нужно сделать, послеих выполнения — нужно сбросить счётчик.

Далее вы увидите как это было использовано, а пока добавим HDL-код в Top-модуль:

// Counter for transactions reg [4:0] counter_r = 0; always @(posedge ready_w or negedge rstn_i) begin if(rstn_i == 1’b0) begin counter_r = 0; end else begin counter_r = counter_r + 1; case(state_r) READ_STATE: begin if (counter_r == 7) begin counter_r = 0; end end WRITE_STATE: begin if (counter_r == 5) begin counter_r = 0; end end default: begin counter_r = 0; end endcase end end
Перейдем к созданию основного блока, который будет реализовывать транзакции и делаем сброс значений при асинхронном сбросе:

always @(posedge clk_i or negedge rstn_i) begin if(rstn_i == 1’b0) begin led_write_pulse_r <= 0; led_read_pulse_r <= 0; cmd_r <= START_CMD; state_r <= IDLE_STATE; write_data_r <= 0; wr_i2c_r <= 0; end else begin end end
В основном блоке создаем простую State-машину:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Описываем ее следующим образом и расставим управление сигналом старта транзакций wr_i2c_r:

case(state_r) IDLE_STATE: begin wr_i2c_r = 0; end READ_STATE: begin wr_i2c_r = 1; end WRITE_STATE: begin wr_i2c_r = 1; end WAIT_STATE: begin wr_i2c_r = 1; end default: begin wr_i2c_r = 0; end endcase
В первую очередь сделаем обработку импульсов на исполнение команды записи или чтения с кнопок, если автомат готов. Если приходит импульс — то приходим в следующее состояние, в зависимости от того с какой кнопки пришла команда:

IDLE_STATE: begin wr_i2c_r = 0; // Button for Read operation if(btn_read_negedge_w) begin if(ready_w) begin state_r = READ_STATE; end end // Button for Write operation if(btn_write_negedge_w) begin if(ready_w) begin state_r = WRITE_STATE; end end end
Опишем операции на READ_STATE. Тут все просто — реагируем на каждое увеличение счетчика counter_r, это означает, что автомат готов выполнять следующую операцию. Получилось следующее:

READ_STATE: begin led_read_pulse_r <= ~led_read_pulse_r; // Для отладки wr_i2c_r = 1; // Стартуем транзакции case(counter_r) 0: begin end 1: begin write_data_r = {slave_addr_r, WRITE_BIT}; // Выставляем данные end 2: begin write_data_r = reg_addr_r; end 3: begin cmd_r = RESTART_CMD; write_data_r = {slave_addr_r, READ_BIT}; end 4: begin cmd_r = WR_CMD; write_data_r = {slave_addr_r, READ_BIT}; end 5: begin cmd_r = RD_CMD; end 6: begin cmd_r = STOP_CMD; state_r = WAIT_STATE; timer_r = 0; end default: begin cmd_r = START_CMD; state_r = IDLE_STATE; write_data_r = 0; end endcase end
Тут в целом все легко читается, понятно что происходит каждую посылку. Открыв даташит на EEPROM видно, каким образом осуществляется чтение:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Вы можете самостоятельно сопоставить то, что происходит в коде с тем, как должна быть организована транзакция на Random Read, т.е. на чтение рандомной ячейки памяти.

Перейдем к операции Random Write в Datasheet:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Тоже достаточно очевидно что дожно происходить при записи. Опишем секцию WRITE_STATE. Тут даже несколько проще чем в READ_STATE:

WRITE_STATE: begin led_write_pulse_r <= ~led_write_pulse_r; wr_i2c_r = 1; case(counter_r) 0: begin end 1: begin write_data_r = {slave_addr_r, WRITE_BIT}; end 2: begin write_data_r = reg_addr_r; end 3: begin write_data_r <= data_write_r; end 4: begin cmd_r = STOP_CMD; state_r = WAIT_STATE; timer_r = 0; end default: begin cmd_r = START_CMD; state_r = IDLE_STATE; write_data_r = 0; end endcase end
Следующий state, который необходимо добавить, в основном для покостыливания невыполнения STOP-команды — это WAIT_STATE:

WAIT_STATE: begin wr_i2c_r = 1; if(timer_r >= 32’d1000) begin state_r <= IDLE_STATE; write_data_r = 0; cmd_r = START_CMD; end else timer_r <= timer_r + 32’d1; end
Ждём условно 1000 тактов и переходим в IDLE_STATE.

Добавим также обработчик для всех остальных случаев:

default: begin wr_i2c_r = 0; state_r <= IDLE_STATE; end
Полный текст исходного когда главного модуля — вы можете найти в моем Github-репозитории.

❯ Шаг шестой. Проверяем как работает, ищем баги

Итак. Мы собрали все необходимое и теперь можно провести проверку и простейший дебаг. Способов вижу два — припаяться к ножкам SDA и SCL у EEPROM и подключить DSLogic или сделать через встроенный в Quartus SignalTap логический анализатор и по JTAG посмотреть, что происходит.

Коротко расскажу про второй способ. Запустить Signal Tap можно через главное меню:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Основные кнопки в окне я выделил красным:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

В первую очередь необходимо выбрать источник тактирования в секции Clock. Тут я выбрал поделенную частоту, чтобы охватить необходимое количество семплов т.к. память захвата ограничена и надо чтобы всё влезло. Этот параметр устанавливается через параметр Sample depth.

После необходимо накидать во вкладке Setup наблюдаемые сигналы и выбрать логическую функцию Basic OR для триггеров от этих сигналов. После добавления изменений — необходимо, чтобы модуль наблюдения попал в прошивку. Для этого необходимо перекомпилировать ее и прошить в плату.

После компиляции нужно выбрать триггер, я выбрал от сигнала READ_STATE и WRITE_STATE. Можно запустить Run Analysis для единичного захвата и перейти в секцию Data нажать кнопку, которой присвоено действие на чтение:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Подробно рассмотрев, видим, что все сигналы на команду Read отрабатывают как нужно т.е. читаем из регистра 0x02 заранее записанное значение 0xCE. Теперь можно посмотреть что происходит на команду Write. Запишем в ячейку 0x02 новое значение 0xF1:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Кажется все работает как нужно. Подключим для проверки DSLogic к ножкам EEPROM и с помощью программы DSView и декодера I2C протокола проверить правильность транзакций.

На чтение:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

На запись:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

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

❯ Баг №1. Некорректная частота SCL

Рассмотрению частоту тактирования, получилось значение 390.62kHz:

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Получилось конечно не 400kHz ровно, но кажется что этого для данного уровня “развития” автомата будет достаточно. Можно считать за первый баг.

❯ Баг №2. Некорректная установка ACK-бита

В ходе просмотра транзакций — Я обнаружил хаотическую установку ACK-бита. Пока не понятно откуда берется во взаимодействии с железом этот косяк. Надо будет разбираться после.

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

В качестве неприятного дополнения к этому — при подаче адреса для Slave-устройства которого нет на шине — один фиг приходит сигнал ACK.

Других проблем я пока не обнаружил. Думаю в коммитах в репозитории можно будет отслеживать прогресс по доработке.

❯ Заключение

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

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Этот материал мне дался достаточно большой ценой — куча вариантов реализации, куча времени на отладку. Но результат стоил того. Куча опыта в отладке, в разборе вариантов реализации, часы просмотра результатов RTL-синтеза. Кажется базовый Verilog-кодинг стал одной из моих компетенций, но безусловно есть куда расти.

Сейчас стоит обозначить дальнейшие планы:

  • пофиксить баги;
  • перенести код в Xilinx Vivado, чтобы запустить его на плате Zynq Mini (которую я обозревал в этой статье);
  • подключить данный модуль к AXI шине чтобы можно было взаимодействовать с ним из Linux.

Поэтому с этими планами можно идти дальше. До встречи в следующих статьях!

P. S. Забыл сказать. Самое дурацкое, что выяснилось сравнительно недавно — OLED-дисплей SSD1306, с которым планировалось взаимодействие полученного автомата оказывается подключен по SPI и необходимо будет придумать SPI-автомат 😀. Так что буду по всей видимости писать еще и его 😀

Возможно, захочется почитать и это:

  • ➤ IR remote control, а без микроконтроллеров можно? Да не вопрос
  • ➤ Как запустить сотовую сеть стандарта AMPS при помощи SDR
  • ➤ Hello World на Tang Primer 20K под Linux
  • ➤ Выполняем сторонние программы на микроконтроллерах с Гарвардской архитектурой: как загружать программы без знания ABI?
  • ➤ Atomic Heart или как забилось сердце русского геймдева

Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе

Теги:

  • timeweb_статьи
  • FPGA
  • I2C
  • I2C Master
  • I2C master controller
  • verilog
  • quartus
  • signaltap
  • dsview
  • dslogic

Хабы:

  • Блог компании Timeweb Cloud
  • FPGA
  • Производство и разработка электроники
  • DIY или Сделай сам
  • Электроника для начинающих

Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку Задонатить
Источник

Оставьте ответ

Ваш электронный адрес не будет опубликован.

©Купоно-Мания.ру