Весь этот процесс перехода от идеи и результатов моделирования к написанию кода — я и хотел бы описать в данной статье.
Всем интересующимся — добро пожаловать под кат! =)
Дисклеймер. Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи — рассказать о своем опыте. Я не являюсь профессиональным разработчиком под ПЛИС на языке Verilog и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется. Что ж, поехали…
❯ Шаг нулевой. Что в итоге делаем и к чему стремимся?
В ходе реализации конечного автомата я пришел к выводу, что я не буду дополнительно усложнять и так непростую для себя задачу и заморачиваться над возможностью работы автомата в Standard Mode т.к. подавляющее большинство I2C Slave устройств умеют работать в Fast Mode.
В результате написания HDL-кода — я хочу получить конечный автомат который:
- имеет возможность асинхронного сброса;
- исполняет указанные команды при наличии разрешающего сигнала, в четком соответствии с задумкой из прошлой статьи;
- записывает на шину данные выставленные на входных портах модуля (адрес, байты данных);
- выставляет прочитанные данные после окончания операции на выходной порт;
- выставляет сигнал ACK/NACK при завершении транзакции на соответствующий порт;
- управляется внешним автоматом, который командует когда и какую команду нужно выполнить.
Я не стал перегружать дополнительными функциями данный автомат и получилась просто логика считывания и записи отдельных бит информации на шину на уровне простых транзакций. Это будет первый уровень абстракции, которым уже можно будет управлять через вышестоящий автомат, уже непосредственно решая конкретные прикладные задачи. Но об этом позже.
❯ Шаг первый. Начинаем с модуля и его интерфейса.
Когда я писал код, пришлось немного пересмотреть структуру входных и выходных сигналов, потому что я не стал нагружать мой первый мало-мальски сложный проект дополнительным функционалом который бы повлек за собой длительную отладку и ломание головы, особенно если учесть, что у меня опыта в 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, выглядит как прям то что нужно:
Код кажется великолепно читаемым и по всей видимости не нуждается в дополнительном комментировании. Вдумчиво прочитайте его и думаю, вопросов не должно возникать на этом этапе.
❯ Шаг пятый. Управление сигналом 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.
❯ Шаг седьмой. Проверка полученного результата
До навыков профессионального верификатора мне еще далеко, поэтому не судите строго:
`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
Возможно, захочется почитать и это:
Автор: Андрей