Весь этот процесс перехода от идеи и результатов моделирования к написанию кода — я и хотел бы описать в данной статье.
Всем интересующимся — добро пожаловать под кат! =)
Дисклеймер. Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи — рассказать о своем опыте. Я не являюсь профессиональным разработчиком под ПЛИС на языке Verilog и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется. Что ж, поехали…
❯ Шаг нулевой. Что в итоге делаем и к чему стремимся?
В ходе реализации конечного автомата я пришел к выводу, что я не буду дополнительно усложнять и так непростую для себя задачу и заморачиваться над возможностью работы автомата в Standard Mode т.к. подавляющее большинство I2C Slave устройств умеют работать в Fast Mode.
В результате написания HDL-кода — я хочу получить конечный автомат который:
- имеет возможность асинхронного сброса;
- исполняет указанные команды при наличии разрешающего сигнала, в четком соответствии с задумкой из прошлой статьи;
- записывает на шину данные выставленные на входных портах модуля (адрес, байты данных);
- выставляет прочитанные данные после окончания операции на выходной порт;
- выставляет сигнал ACK/NACK при завершении транзакции на соответствующий порт;
- управляется внешним автоматом, который командует когда и какую команду нужно выполнить.
После написания кода — протестируем полученный результат в ModelSim, как это делать я рассказывал в этой статье.
Я не стал перегружать дополнительными функциями данный автомат и получилась просто логика считывания и записи отдельных бит информации на шину на уровне простых транзакций. Это будет первый уровень абстракции, которым уже можно будет управлять через вышестоящий автомат, уже непосредственно решая конкретные прикладные задачи. Но об этом позже.
❯ Шаг первый. Начинаем с модуля и его интерфейса.
Когда я писал код, пришлось немного пересмотреть структуру входных и выходных сигналов, потому что я не стал нагружать мой первый мало-мальски сложный проект дополнительным функционалом который бы повлек за собой длительную отладку и ломание головы, особенно если учесть, что у меня опыта в Verilog около нуля.
Перейдем к перечислению входных и выходных сигналов модуля, обозвав его i2c_bit_controller:
`timescale 1ns/1ps
module i2c_bit_controller (
input rstn_i, // Входной сигнал для асинхронный сброс
input clk_i, // Входной сигнал тактирования
input wr_i2c_i, // Входной сигнал на включение записи
input [2:0] cmd_i, // Входной сигнал с командой
input [7:0] din_i, // Входной сигнал с полезными данными
output [7:0] dout_o, // Выходной сигнал с полезными данными
output ack_o, // Выходной сигнал с сигналом ACK/NACK
output [3:0] state_o, // Выходной сигнал текущего состояния автомата
output ready_o, // Выходной сигнал сообщающий о готовности автомата
output [4:0] bit_count_o, // Счетчик количества уже выставленных бит в транзакции
inout tri sda_io, // Вход/выход для сигнала SDA
output tri scl_io // Выходной сигнал SCL
);
Коротко поясню про каждый из них:
- rstn_i — это вход для общего сигнала асинхронного сброса всей схемы и приведения ее к исходному состоянию;
- clk_i — это вход уже готового тактового сигнала в 10 МГц;
- wr_i2c_i— это сигнал разрешающий начало выполнения транзакций, соответственно сначала выставляем команду и потом дергаем этот спусковой крючок;
- cmd_i — это как раз порт для выставления команды;
- din_i — это порт для выставления данных которые должны быть отправлены на шину;
- dout_o — это порт на который будут выставлены данные, которые получены при выполнении транзакции;
- ack_o — это порт на который выставляется ACK/NACK сигнал после завершения транзакции;
- state_o — этот отладочный порт, на который выставляется текущей стадии работы конечного автомата;
- ready_o — это сигнал означающий, что конечный автомат находится в стадии либо Idle, либо Hold;
- bit_count_o — это отладочный сигнал, который показывает сколько бит уже отправлено в текущей транзакции;
- sda_io — это интерфейс с тремя состояниями, которой считывает и записывает данные на шине SDA;
- scl_io — это выходной интерфейс с тремя состояниями для выдачи на шину сигнала SCL.
Кажется тут все предельно ясно, идем дальше.
❯ Шаг второй. Параметры и необходимые регистры.
На этом этапе необходимо определиться, какие локальные параметры и регистры нам понадобятся. При написании кода я конечно же не все из них объявлял заранее и большую часть я дописывал уже когда вел разработку и отладку. Но это лирика, давайте посмотрим какие локальные параметры я ввел в оборот:
// Константы для обозначения команд
localparam START_CMD = 3'b001;
localparam WR_CMD = 3'b010;
localparam RD_CMD = 3'b011;
localparam STOP_CMD = 3'b100;
localparam RESTART_CMD = 3'b101;
// Возможные состояния FSM
localparam IDLE_STATE = 4'b0001; // 1
localparam START1_STATE = 4'b0010; // 2
localparam START2_STATE = 4'b0011; // 3
localparam HOLD_STATE = 4'b0100; // 4
localparam RESTART1_STATE = 4'b0101; // 5
localparam RESTART2_STATE = 4'b0110; // 6
localparam STOP1_STATE = 4'b0111; // 7
localparam STOP2_STATE = 4'b1000; // 8
localparam STOP3_STATE = 4'b1001; // 9
localparam DATA1_STATE = 4'b1010; // 10
localparam DATA2_STATE = 4'b1011; // 11
localparam DATA3_STATE = 4'b1100; // 12
localparam DATA4_STATE = 4'b1101; // 13
localparam DATAEND_STATE = 4'b1110; // 14
Первым делом я обозначил возможные команды, которые могут быть использованы при работе. После этого я ввел обозначения для состояний FSM.
❯ Шаг третий. “Ногодрыг”
Теперь перейдем к логике управления выходными сигналами. Коротко объясню логику формирования сигналов на шинах SDA и SCL. Поскольку сигнал на шине, из-за наличия подтягивающих резисторов, всегда находится в логической единице, если не притянут к нулю — то мы будем выставлять только логический ноль, не заморачиваясь о том, когда нужно будет выставить единицу — она сама автоматически будет выставлена если мы отпустим шину выставив Z-состояние на выходном сигнале.
Для начала введем две пары регистров для SDA, SCL которые будут представлять собой драйверы выходных сигналов:
reg sda_out_r;
reg scl_out_r;
reg sda_r;
reg scl_r;
Чтобы управлять сигналами необходимо ввести поведенческий блок, который на каждый положительный фронт тактового сигнала будет производить неблокирующее присваивание из регистра который мы подключим к выходному порту:
always @(posedge clk_i, negedge rstn_i)
begin
if (~rstn_i)
begin
sda_r <= 1'b1;
scl_r <= 1'b1;
end else
begin
sda_r <= sda_out_r;
scl_r <= scl_out_r;
end
end
После этого назначим соответствие между портами и их регистрами. Для SCL получается следующее:
assign scl_io = (scl_r) ? 1'bz : 1'b0;
Получается, если значение scl_r будет равно 1 — то выставляем порт в Z-состояние, если нужно выставить 0 — то выставляем 0. Кажется все очевидно. После синтеза получится следующая конструкция:
В случае с SDA — все немного сложнее. В фазы, когда данные должны быть считаны — необходимо ввести дополнительное состояние, которое обозначало данную фазу обмена, назвал я ее into_w. Для этого нужно ввести несколько регистров и выражений:
reg data_phase_r; // Регистр c индикацией процесса передачи полезных данных
reg [3:0] cmd_r; // Регистр для хранения текущей команды
reg [3:0] cmd_next_r; // Вспомогательный регистр для FSM
reg [4:0] bit_r; // Регистр для хранения номера текущего бита
wire into_w; // Сигнал-проводник, обозначающий момент получения данных
assign into_w = (data_phase_r && cmd_r == RD_CMD && bit_r < 8) || (data_phase_r && cmd_r == WR_CMD && bit_r == 8);
assign sda_io = (into_w || sda_r) ? 1'bz : 1'b0;
Таким образом получается достаточно объемная конструкция, которая сообщает, что когда идет data_phase_r (когда FSM в одной из DATA_PHASE, дальше будет понятно о чем идет речь) и когда выполняется команда RD_CMD, и были прочитаны не все биты в текущей транзакции или когда выполняется команда WR_CMD и записаны все 8 бит и ожидаем считывание ACK-бита.
Если данные условия выполняются — значит однозначно идет процесс считывания данных с шины и нужно занять Z-состояние. Ну или если нужно выставить 0 на SDA — общее правило срабатывает и на шине выставляем ноль.
После там еще будет накручена логика входного буфера но об этом позже. Надеюсь не сильно сложно расписал ????.
❯ Шаг четвертый. State-машина
Для того, чтобы перемещаться по стадиям для выполнения атомарных действий и для формирования транзакций, которые я описывал в предыдущих статьях — необходимо создать State-машину с драйвером обновления текущих значений регистров.
Для того, чтобы ее реализовать нужно объявить регистры для управления текущим состоянием и назначить его на отладочный выход:
reg [7:0] state_r; // Регистр состояния
reg [7:0] state_next_r; // Вспомогательный регистр для переходов
assign state_o = state_r; // Назначаем регистр к выходному сигналу
После необходимо добавить поведенческий блок, который будет постоянно обновлять текущее состояние регистра:
always @(posedge clk_i, negedge rstn_i)
begin
if (~rstn_i)
begin
state_r <= IDLE_STATE;
end else
begin
state_r <= state_next_r;
end
end
И можно создать поведенческую Next-state логику, которая будет чувствительна ко всем изменениям переменных и будет осуществляться переход от состояния к состоянию.
Также на этом этапе я ввел в оборот регистры обозначающие переменную Ready, переменную data_phase_r и счетчик переданных битов bit_r:
reg ready_r; // Переменная для определения готовности автомата
reg [4:0] bit_next_r; // Вспомогательная переменная для счетчика битов
assign ready_o = ready_r; // Вывод состояние готовности FSM для отладки
assign bit_count_o = bit_r; // Вывод для отладки текущего бита транзакции
// Next-state машина
always @(*)
begin
state_next_r = state_r; // Задаем для переменных значения по умолчанию
ready_r = 1'b0; // Для переменной состояния
data_phase_r = 1'b0; // Для фазы передачи данных
cmd_next_r = cmd_r; // Для регистра текущей команды
bit_next_r = bit_r; // Для регистра значения счетчика
case (state_r)
IDLE_STATE: begin
ready_r = 1'b1; // Обозначаем, что автомат готов
if(wr_i2c_i && cmd_i == START_CMD) // Если разрешены транзакции
begin // И подана команда START
state_next_r = START1_STATE; // Переходим в новое состояние
end
end
START1_STATE: begin
state_next_r = START2_STATE; // Идем к следующему шагу
end
START2_STATE: begin
state_next_r = HOLD_STATE; // Идем к следующему шагу
end
HOLD_STATE: begin
ready_r = 1'b1; // Обозначаем что автомат готов
if (wr_i2c_i) // Если разрешены транзакции
begin
cmd_next_r = cmd_i;
case (cmd_i) // Идем в шаг который указан на входе автомата
RESTART_CMD:
state_next_r = RESTART1_STATE;
STOP_CMD:
state_next_r = STOP1_STATE;
default: begin
bit_next_r = 0; // Обнуляем счетчик битов
state_next_r = DATA1_STATE;
end
endcase
end
end
DATA1_STATE: begin
data_phase_r = 1'b1; // Обозначаем что идет фаза данных
state_next_r = DATA2_STATE; // Идем к следующему шагу
end
DATA2_STATE: begin
data_phase_r = 1'b1; // Обозначаем что идет фаза данных
state_next_r = DATA3_STATE; // Идем к следующему шагу
end
DATA3_STATE: begin
data_phase_r = 1'b1; // Обозначаем что идет фаза данных
state_next_r = DATA4_STATE; // Идем к следующему шагу
end
DATA4_STATE: begin
data_phase_r = 1'b1; // Обозначаем что идет фаза данных
if (bit_r == 8) // Если переданы все биты
begin
state_next_r = DATAEND_STATE; // Переходим к фазе завершения
end else
begin
bit_next_r = bit_r + 1; // Инкрементируем счетчик
state_next_r = DATA1_STATE; // Идем к следующему шагу
end
end
DATAEND_STATE: begin
state_next_r = HOLD_STATE; // Идем к следующему шагу
end
RESTART1_STATE: begin
state_next_r = RESTART2_STATE; // Идем к следующему шагу
end
RESTART2_STATE: begin
state_next_r = START1_STATE; // Идем к следующему шагу
end
STOP1_STATE: begin
state_next_r = STOP2_STATE; // Идем к следующему шагу
end
STOP2_STATE: begin
state_next_r = STOP3_STATE; // Идем к следующему шагу
end
default: begin // А-ля STOP3 состояние
state_next_r = IDLE_STATE; // Возвращаемся в Idle
end
endcase
end
Добавим в поведенческий блок для обновления регистров новые конструкции и получится следующее:
always @(posedge clk_i, negedge rstn_i)
begin
if (~rstn_i)
begin
state_r <= IDLE_STATE;
bit_r <= 0; // Добавляем
cmd_r <= 0; // Добавляем
end else
begin
state_r <= state_next_r;
bit_r <= bit_next_r; // Добавляем
cmd_r <= cmd_next_r; // Добавляем
end
end
По результатам синтеза получается FSM, выглядит как прям то что нужно:
Понятно, что масштаб картинки не самый удобный — лучше открыть ее на полную и просмотреть флоу работы State-машины. Если прочитать Verilog-код — то можно сверить с тем, что планировалось в предыдущей статье.
Код кажется великолепно читаемым и по всей видимости не нуждается в дополнительном комментировании. Вдумчиво прочитайте его и думаю, вопросов не должно возникать на этом этапе.
❯ Шаг пятый. Управление сигналом SCL
Поскольку сигналом SCL управляет только Master-устройство — тут вообще ничего сложного. Расставим в нужных местах значения сигнала SCL когда он должен быть в значении логического нуля в соответствии со стадиями, как это было описано в прошлой статье. Обратим внимание на изображения с таймингами, где SCL принимал данное значение. Для удобства я выделил жирным изменение существующем в коде.
Зададим сначала значение по умолчанию в Next-state машину:
// Next-state машина
always @(*)
begin
state_next_r = state_r; // Задаем для переменных значения по умолчанию
ready_r = 1'b0; // Для переменной состояния
data_phase_r = 1'b0; // Для фазы передачи данных
cmd_next_r = cmd_r; // Для регистра текущей команды
bit_next_r = bit_r; // Для регистра значения счетчика
scl_out_r = 1'b1; // Добавляем
Во время исполнения этапа START2_STATE:
START2_STATE: begin
scl_out_r = 1'b0; // Добавляем
state_next_r = HOLD_STATE; // Идем к следующему шагу
end
Во время этапа HOLD_STATE:
HOLD_STATE: begin
ready_r = 1'b1; // Обозначаем что автомат готов
scl_out_r = 1'b0; // Добавляем
if (wr_i2c_i) // Если разрешены транзакции
begin
cmd_next_r = cmd_i;
case (cmd_i) // Идем в шаг который указан на входе автомата
RESTART_CMD:
state_next_r = RESTART1_STATE;
STOP_CMD:
state_next_r = STOP1_STATE;
default: begin
bit_next_r = 0; // Обнуляем счетчик битов
state_next_r = DATA1_STATE;
end
endcase
end
end
Для этапа DATA1_STATE:
DATA1_STATE: begin
scl_out_r = 1'b0; // Добавляем
data_phase_r = 1'b1; // Обозначаем что идет фаза данных
state_next_r = DATA2_STATE; // Идем к следующему шагу
end
Для этапа DATA4_STATE:
DATA4_STATE: begin
scl_out_r = 1'b0; // Добавляем
data_phase_r = 1'b1; // Обозначаем что идет фаза данных
if (bit_r == 8) // Если переданы все биты
begin
state_next_r = DATAEND_STATE; // Переходим к фазе завершения
end else
begin
bit_next_r = bit_r + 1; // Инкрементируем счетчик
state_next_r = DATA1_STATE; // Идем к следующему шагу
end
end
В этап DATAEND_STATE:
DATAEND_STATE: begin
scl_out_r = 1'b0; // Добавляем
state_next_r = HOLD_STATE; // Идем к следующему шагу
end
В этап RESTART1_STATE:
RESTART1_STATE: begin
scl_out_r = 1'b0; // Добавляем
state_next_r = RESTART2_STATE; // Идем к следующему шагу
end
И в этап STOP1_STATE:
STOP1_STATE: begin
scl_out_r = 1'b0; // Добавляем
state_next_r = STOP2_STATE; // Идем к следующему шагу
end
Внимательные читатели заметят, что я поменял значения сигналов в DATA-фазах, это пришлось сделать для того чтобы сметчить сигналы разных фаз между собой, длительности остались такими же. Плюсом к этому добавилась фаза DATAEND_STATE.
❯ Шаг шестой. Управление сигналом SDA.
Теперь самый интересный и важный этап — выставление данных и считывание. Как вы помните из предыдущей статьи мной была предложена концепция наличия двух сдвиговых регистров для Tx и Rx для оперирования данными на шине.
В первую очередь определим этапы в которых SDA выставляется безусловно в значение логического нуля.
Это происходит во время этапа START1_STATE:
START1_STATE: begin
sda_out_r = 1'b0; // Добавляем
state_next_r = START2_STATE; // Идем к следующему шагу
end
Во время этапа START2_STATE:
START2_STATE: begin
scl_out_r = 1'b0;
sda_out_r = 1'b0; // Добавляем
state_next_r = HOLD_STATE; // Идем к следующему шагу
end
Во время этапа HOLD_STATE:
HOLD_STATE: begin
ready_r = 1'b1; // Обозначаем, что автомат готов
scl_out_r = 1'b0;
sda_out_r = 1'b0; // Добавляем
if (wr_i2c_i) // Если разрешены транзакции
begin
cmd_next_r = cmd_i;
case (cmd_i) // Идем в шаг который указан на входе автомата
RESTART_CMD:
state_next_r = RESTART1_STATE;
STOP_CMD:
state_next_r = STOP1_STATE;
default: begin
bit_next_r = 0; // Обнуляем счетчик битов
state_next_r = DATA1_STATE;
end
endcase
end
end
И во время этапа DATAEND_STATE:
DATAEND_STATE: begin
scl_out_r = 1'b0;
sda_out_r = 1'b0; // Добавляем
state_next_r = HOLD_STATE; // Идем к следующему шагу
end
Добавим также значение по умолчанию в Next-state машину:
// Next-state машина
always @(*)
begin
state_next_r = state_r; // Задаем для переменных значения по умолчанию
ready_r = 1'b0; // Для переменной состояния
data_phase_r = 1'b0; // Для фазы передачи данных
cmd_next_r = cmd_r; // Для регистра текущей команды
bit_next_r = bit_r; // Для регистра значения счетчика
scl_out_r = 1'b1;
sda_out_r = 1'b0; // Добавляем
Теперь перейдем к объявлению нужных нам 9-битных регистров из которых 8 бит полезных данных и 1 бит для сигнала ACK:
reg [8:0] tx_r;
reg [8:0] tx_next_r;
reg [8:0] rx_r;
reg [8:0] rx_next_r;
Добавим в поведенческий блок соответствующие выражения:
always @(posedge clk_i, negedge rstn_i)
begin
if (~rstn_i)
begin
state_r <= IDLE_STATE;
bit_r <= 0;
cmd_r <= 0;
tx_r <= 0; // Добавляем
rx_r <= 0; // Добавляем
end else
begin
state_r <= state_next_r;
bit_r <= bit_next_r;
cmd_r <= cmd_next_r;
tx_r <= tx_next_r; // Добавляем
rx_r <= rx_next_r; // Добавляем
end
end
И в Next-state машину:
// Next-state машина
always @(*)
begin
state_next_r = state_r; // Задаем для переменных значения по умолчанию
ready_r = 1'b0; // Для переменной состояния
data_phase_r = 1'b0; // Для фазы передачи данных
cmd_next_r = cmd_r; // Для регистра текущей команды
bit_next_r = bit_r; // Для регистра значения счетчика
scl_out_r = 1'b1;
sda_out_r = 1'b0;
tx_next_r = tx_r; // Добавляем
rx_next_r = rx_r; // Добавляем
Подключим их сразу к выходным портам модуля:
assign dout_o = rx_r[8:1]; // 8 бит полезных данных
assign ack_o = rx_r[0]; // Разряд в который помещается ACK-сигнал
Не забываем про NACK-сигнал который должен быть выставлен когда происходит чтение данных из Slave:
wire nack_w;
assign nack_w = din_i[0];
Для того, чтобы определить, что отправить нужно собрать переменную tx_next_r из двух частей и подготовить эти данные для отправки на этапе HOLD_STATE:
HOLD_STATE: begin
ready_r = 1'b1; // Обозначаем что автомат готов
scl_out_r = 1'b0;
sda_out_r = 1'b0;
if (wr_i2c_i) // Если разрешены транзакции
begin
cmd_next_r = cmd_i;
case (cmd_i) // Идем в шаг который указан на входе автомата
RESTART_CMD:
state_next_r = RESTART1_STATE;
STOP_CMD:
state_next_r = STOP1_STATE;
default: begin
bit_next_r = 0; // Обнуляем счетчик битов
state_next_r = DATA1_STATE;
tx_next_r = {din_i, nack_w}; // Добавляем
end
endcase
end
end
Сигнал SDA на шине будем выставлять всегда 9-й бит tx_r в DATA-стадиях и путем сдвига будем каждую итерацию данных обновлять значение этого бита:
DATA1_STATE: begin
sda_out_r = tx_r[8]; // Добавляем
scl_out_r = 1'b0; // Добавляем
data_phase_r = 1'b1; // Обозначаем, что идет фаза данных
state_next_r = DATA2_STATE; // Идем к следующему шагу
end
DATA2_STATE: begin
sda_out_r = tx_r[8]; // Добавляем
scl_out_r = 1'b0; // Добавляем
data_phase_r = 1'b1; // Обозначаем, что идет фаза данных
state_next_r = DATA3_STATE; // Идем к следующему шагу
end
DATA3_STATE: begin
sda_out_r = tx_r[8]; // Добавляем
scl_out_r = 1'b0; // Добавляем
data_phase_r = 1'b1; // Обозначаем, что идет фаза данных
state_next_r = DATA4_STATE; // Идем к следующему шагу
end
А в стадии DATA4_STATE помимо этой конструкции добавим еще в условие сдвиг:
DATA4_STATE: begin
sda_out_r = tx_r[8]; // Добавляем
scl_out_r = 1'b0;
data_phase_r = 1'b1; // Обозначаем, что идет фаза данных
if (bit_r == 8) // Если переданы все биты
begin
state_next_r = DATAEND_STATE; // Переходим к фазе завершения
end else
begin
tx_next_r = {tx_r[7:0], 1'b0}; // Добавляем
bit_next_r = bit_r + 1; // Инкрементируем счетчик
state_next_r = DATA1_STATE; // Идем к следующему шагу
end
end
Поскольку размер регистра ограничен 9 битами, прибавляя к нему каждую итерацию логический ноль — мы будем сдвигать значение бит в регистре. Всё очень просто.
Осталось добавить в фазу DATA2_STATE таким же образом считывание данных со сдвигом:
DATA2_STATE: begin
sda_out_r = tx_r[8];
scl_out_r = 1'b0;
data_phase_r = 1'b1; // Обозначаем, что идет фаза данных
state_next_r = DATA3_STATE; // Идем к следующему шагу
// Добавляем
rx_next_r = {rx_r[7:0], sda_io}; // Сдвигаем данные с шины в регистр
end
Вот и все. Выглядит очень незамысловато. Полученный результат я залил на GitHub: github.com/megalloid/I2C_Master_Controller.
❯ Шаг седьмой. Проверка полученного результата
Теперь можно создать testbench в проект и проверить, что все происходит так как ожидается. Очень сильно может помочь моя предыдущая статья на эту тему. Я накидал очень быстро простую тестовую программу, которая просимулирует нужные сигналы и покажет нам поведение автомата.
До навыков профессионального верификатора мне еще далеко, поэтому не судите строго:
`timescale 1ns / 1ps
module i2c_bit_controller_tb;
// Clock
reg clk_r;
localparam CLK_PERIOD = 10;
always #(CLK_PERIOD/2) clk_r = ~clk_r;
// Registers
reg rstn_r = 1'b1;
reg [2:0] cmd_r;
reg [3:0] state_r;
reg ready_r;
reg wr_i2c_r = 0;
reg [4:0] bit_count_r;
reg [4:0] counter_r = 0;
reg [7:0] din_r;
// Wires
wire scl_w;
wire sda_w;
// UUT
i2c_master_controller uut(
.rstn_i(rstn_r),
.clk_i(clk_r),
.wr_i2c_i(wr_i2c_r),
.cmd_i(cmd_r),
.din_i(din_r),
.state_o(state_r),
.ready_o(ready_r),
.bit_count_o(bit_count_r),
.sda_io(sda_w),
.scl_io(scl_w)
);
// Commands constants
localparam START_CMD = 3'b001;
localparam WR_CMD = 3'b010;
localparam RD_CMD = 3'b011;
localparam STOP_CMD = 3'b100;
localparam RESTART_CMD = 3'b101;
initial begin
rstn_r = 0;
clk_r = 0;
#10;
rstn_r = 1;
#10;
cmd_r = START_CMD;
#10;
wr_i2c_r = 1;
din_r = 8'b11111111;
#400;
din_r = 8'b10101010;
#1000;
$stop;
end
always @(posedge ready_r) begin
counter_r = counter_r + 1;
if (counter_r == 3)
begin
cmd_r = RESTART_CMD;
din_r = 8'b10101010;
end else if (counter_r == 4)
begin
cmd_r = WR_CMD;
end else if (counter_r == 5)
begin
cmd_r = STOP_CMD;
end
end
endmodule
Запускаем RTL-симуляцию и видим следующее:
Рассмотрим полученное несколько более пристально и обратим внимание на то, как происходит управление линиями SCL, SDA при выполнении команды START после IDLE_STATE:
Четко видно START-сигнал, данные передаются только путем прижатия линии к нулю, четко видно как работает State-машина и начинается обмен информацией. Вся посылка из 11111111 верно выставлена на шине:
Также видно адекватное исполнение команды RESTART:
Ну и сигнал STOP — также работает верно, кажется все соответствует ожидаемому результату.
Что ж, теперь остается проверить все в реальном железе. Словом, успех!
❯ Заключение
Теперь у вас есть возможность самостоятельно поиграться с автоматом, почитать код и, при желании, подробно вникнуть в суть происходящего. Дополнительно можете рассмотреть уже у себя в ModelSIm значения отладочных сигналов и то как работает данный конечный автомат.
А следующим шагом на пути к поставленной цели — необходимо будет проверить работу данного автомата уже при взаимодействии с реальным железом. Теперь я хочу создать управляющий автомат для модуля, чтобы произвести простые операции чтения и записи в EEPROM на плате с Cyclone IV и сделать простую логику записи и чтения произвольных значений в ячейки памяти при помощи кнопок на плате, но об этом уже в следующей статье.
Большое спасибо за внимание! До встречи в следующих статьях! ????
Ссылка на репозиторий с кодом: github.com/megalloid/I2C_Master_Controller/tree/main
Возможно, захочется почитать и это:
- ➤ KC868-AM: мини мы или ESP32 IO Expansion Board
- ➤ Нейросеть мне в помощь или как я сделал телеграм бота, который умеет переводить песни
- ➤ Крутейший КПК из конца 90-х — каким был Casio Cassiopeia E-105?
- ➤ Измерение скорости чтения-записи носителей с помощью утилиты dd
- ➤ Как создавался Halo
Комментарии (24)
Ivanii
29.11.2023 08:44Интересно насколько сложно написать скалер монитора Full HD?
Ведь даже небольшой организации это под силу?
Dima_Sharihin
29.11.2023 08:44Скалеру монитора нужна, в первую очередь, память хотя бы под один фрейм, а 6 мегабайт на плиссину обычно никто не кладет. Есть HyperBus RAM, и совмещенные варианты в духе Gowin GW1NDR, но это уже экзотика
Скалеру с VGA-входом нужно скоростное АЦП, которого не бывать почти никогда в ПЛИС
ПЛИС - это как правило не сильно тиражируемое решение (дорого, жрет много), поэтому в массовые приборы их не так часто кладут
SIISII
29.11.2023 08:44Только почему до сих пор Verilog? Уже, пожалуй, все средства разработки под ПЛИС поддерживают SystemVerilog, а он имеет изрядное число преимуществ, и не только для верификации, но и для синтеза. В частности, используя вместо always специализированные always_ff, always_latch, always_comb, можно сказать компилятору, что ты хочешь получить, и это поможет избежать неявных ошибок.
AlexanderS
29.11.2023 08:44Автору так привычнее/удобнее. Я вообще за VHDL - в нём контроля за пользователем больше и вероятность себе ногу отстрелить ниже)
fpga500
29.11.2023 08:44+2Про выстрел в ногу. Недавно у нас была забавная проблема. У меня был модуль, на входе которого 4-х байтная (32-х битная) шина данных. Коллега написал модуль, передающий мне данные. И что то мы закрутились, и в итоге он сделал у себя на выходе шину 8 бит (1 байт). Всё подключилось, собралось, и даже иногда что то работало, но конечно же не то и так. Верилог такие вещи пропускает, в то время как VHDL такой косяк на этапе компиляции сразу увидит и зарубит.
Но по большому счету - без разницы на чем писать, Verilog или VHDL. Прежде всего должно быть хорошее понимание цифровой схемотехники
AlexanderS
29.11.2023 08:44+1В верилоге присвоение шин разных разрядностей - это классика косяка. Особенно интересно получается, когда старшие разряды редко используются и в целом оно работает хорошо, но иногда ничего непонятно)
В VHDL контроля больше, но и писать на нём нудно с его объявлениями, требованиями соответствия разрядностей и типов. Хотя это дело привычки, конечно.
megalloid Автор
29.11.2023 08:44+1Warning-и на эту тему Quartus пишет всегда. А вот забить на это или нет - уже выбор лично каждого. Плюсом если открыть результаты синтеза - видно когда лишние разряды тупо на "землю" коннектятся и все.
fpga500
29.11.2023 08:44+1В большом проекте, с несколькими десятками, а то и сотней различных модулей, ворнингов всегда много. И их уже нет времени по отдельности разбирать.
И по моему мнению, несоответствие разрядностей шин - это уже не ворнинг, а именно ошибка. Представьте ситуацию - у вас есть физическая вилка, которую нужно воткнуть в розетку. У розетки 20 контактов, у вилки 30. Сразу понятно - что то тут не то)
p.s. кстати, по поводу Квартуса. Раньше он писал ворнинг на защелки, теперь не пишет. А ее как раз очень легко можно получить, забыв написать какой-нить else
AlexanderS
29.11.2023 08:44Так обычно это косяк получается от невнимательности. Человек отвлёкся или в одном месте поправил, закопался, а другое место "уплыло".
SIISII
29.11.2023 08:44+1Как уже отметили, предупреждений в проектах очень много, но, в отличие от обычного программирования, от них невозможно избавиться. Скажем, если пишешь на це++ и у тебя вылетает предупреждение, ты вполне можешь проверить не понравившееся компилятору место и внести ту или иную корректировку. А с HDLами -- облом-с :( И в результате пустопорожних предупреждений появляется море, в котором тонут редкие реально важные.
AlexanderS
29.11.2023 08:44+1У меня как-то был небольшой проект, в котором варнингов практически не было. Я прямо прямо эстетическое удовольствие испытывал от этого =)
Выше писали о куче варнингов в больших проектах. В принципе, большие проекты можно сделать без варнингов, если делаешь всё сам. Но иногда от них просто так не избавишься. Например, берешь какой-то сильно параметрический крупный сторонний модуль и этих варнингов может быть вагон с тележкой - где-то за разрядности вылазит, где-то что-то не используется, что-то меж тактовых доменов без контроля попадает, где-то ещё что-то - но всё это рабочее при данных параметрах (но может задействоваться при других наборах параметров). Понятно дело, что в идеале нужно писать "резиновые" модули где всё рассчитано параметрически (разрядности, задержки, таймауты и прочее). Но это может быть очень трудоёмко, а иногда и невыполнимо, когда требуется обязательно оставлять какой-нибудь резерв.
fpga500
29.11.2023 08:44Именно так. Чем больше параметризован модуль, тем сложнее избавиться от ворнингов. И даже не только в параметризации дело. Вот для примера - у меня есть модуль пакетного FIFO на основе кольцевого буфера. Полностью самописный. На выходе кроме самих пакетов он еще выдает информацию о размере пакета, количестве принятых, переданных, дропнутых и т.д. В одном и том же проекте это FIFO подключается в нескольких местах. Где то мне важен размер читаемого из FIFO пакета, а где то - нет. Там где не важен - этот выход остается неподключенным. Вот и ворнинг. А еще разные среды разработки могут разные вещи ворнингами считать, потому я уже давно не обращаю на них внимания. Кто то скажет, что это плохо, но вот так)
p.s. Само собой, когда я модуль пишу и отлаживаю - то я на ворнинги внимания обращаю и отслеживаю их. Но когда всё собирается вместе, то там мне важнее что и как происходит на модели, как передаются данные между модулями
fpga500
29.11.2023 08:44+2Да, у каждого языка есть свои плюсы и минусы, что поделать. Мне больше нравится VHDL из-за своей строгости. Но это дело вкуса. Грамотный инженер может писать на обоих языках
megalloid Автор
29.11.2023 08:44Да, на определенной планке требований - да, все так. Но у меня скорее хоум-проект, который определяется уровнем очень-очень низкого уровня компетенций, который едва ли будет достаточно для того чтобы оценить во всей красе преимущества SystemVerilog.
Но совет я услышал, это очень ценно. Я попробую одним глазком подглядеть в ту сторону. Благодарствую :)
Flammmable
29.11.2023 08:44+1Вот-вот. В SV есть enum и нет нужды вручную писать перечисление, вроде этого:
localparam IDLE_STATE = 4'b0001; // 1 localparam START1_STATE = 4'b0010; // 2 localparam START2_STATE = 4'b0011; // 3 localparam HOLD_STATE = 4'b0100; // 4 ...
Abo73
29.11.2023 08:44Поскольку сигналом SCL управляет только Master-устройство — тут вообще ничего сложного.
Вообще то это не так, что усложняет полную реализацию мастера I2C.
megalloid Автор
29.11.2023 08:44Я понимаю, что не во всех случаев. Это было сказано про те случаи которые я реализую. А clock stretching я не реализую в своем варианте ввиду сложности. Это в предыдущих статьях мной отдельно проговаривалось.
Мне бы с моим уровнем просто что-то реализовать бы :)
MaxPro33
29.11.2023 08:44Какие основные преимущества вы видите в использовании Verilog для разработки I2C Master Controller по сравнению с другими языками программирования?
megalloid Автор
29.11.2023 08:44+1VHDL показался на мой субъективный и дилетантский взгляд сильно перегруженным с точки зрения синтаксиса.
Первые книги по языкам описания цифровой архитектуры по Verilog.
Поэтому это скорее то, на чем быстрее всего было возможно реализовать задумку и не более того. Да и предыдущие статьи писал как раз на Verilog.
Ну и в Quartus что-то не смог сходу разобраться как пересесть на SystemVerilog, были какие-то грабли.
В общем все мои доводы актуальны только для меня. Когда придёт пора совершенствоваться в этом - разумеется можно будет присмотреться к другим IDE, уйти в OpenSource, на SystemVerilog и прочее. Пока что есть что есть :)
megalloid Автор
29.11.2023 08:44Имею первые книги читал именно про Verilog. Торопился, фразу не дописал)
DX168B
29.11.2023 08:44+2Однозначно одобряю такие статьи. Всегда интересно, как коллеги по цеху реализуют интерфейсы для общения с внешним миром для ПЛИС. Где-то свои выводы подтвердишь чужим опытом, где-то что-то новое узнаешь.
megalloid Автор
29.11.2023 08:44Я думаю, что мой опыт далек от профессионализма и вряд ли можно считать эталоном.
Я тут скорее рассказываю, о том как именно я смог достичь маленькой победы, собрав горы граблей, описав это так, чтобы какой-нибудь новичок мог взять повторить и возможно заинтересоваться темой ПЛИС-ов.
Но тем не менее - спасибо за хорошую оценку моих материалов! Я стараюсь
KeisN13
из такого цикла статей пора делать методичку. Молодец
megalloid Автор
Стараюсь. Может где-то кому-то неопределившимуся в жизни - поможет пробудить интерес и провести профессиональное самоопределение :)