Создаем I2C Master Controller на Verilog. Пишем HDL код

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Закончив в предыдущей статье описание того, как должны осуществляться атомарные операции и каким образом осуществляется выполнение команд я бодро перешел к написанию HDL-кода. Пришлось разобраться с тем, как организовать FSM, как организовать считывание и выставление данных на шине.  

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

Всем интересующимся — добро пожаловать под кат! =)

image

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

image

В случае с 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 — общее правило срабатывает и на шине выставляем ноль.

После там еще будет накручена логика входного буфера но об этом позже. Надеюсь не сильно сложно расписал
Источник: https://habr.com/ru/companies/timeweb/articles/776992/


Интересные статьи

Интересные статьи

В данной серии статей я подробно расскажу о том, как написать на Java собственный интерпретатор объектно-ориентированного диалекта SQL с использованием Spark RDD API, заточенный на задачи подготовки...
Всем привет! Меня зовут Кирилл Быков, и я — наставник по направлению «Python-разработчик» в Яндекс-Практикуме. Тема передачи знаний меня интересовала всегда, ещё со школьных олимпиад, продолжилас...
Если вам не нравится тратить много времени на создание шаблонов для VIPER можно воспользоваться Generamba.
После выхода 4й части, где мы подключили мобильное Flutter приложение к сервису Umka, я получил много вопросов от читателей, которые попробовали запустить Web версию приложения и оно в браузере не за...
Вступление KolibriOS – миниатюрная операционная система, ядро и большинство программ которой написано на ассемблере. Это, конечно же, не значит, что на других языках программирования пис...