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

в 8:01, , рубрики: fpga, how-to, I2C, i2c master, i2c master controller, timeweb_статьи, tutorial, Verilog
Закончив в предыдущей статье описание того, как должны осуществляться атомарные операции и каким образом осуществляется выполнение команд я бодро перешел к написанию HDL-кода. Пришлось разобраться с тем, как организовать FSM, как организовать считывание и выставление данных на шине.  

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

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

image

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

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

После там еще будет накручена логика входного буфера но об этом позже. Надеюсь не сильно сложно расписал 🙂.

Шаг четвертый. 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, выглядит как прям то что нужно:

image

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

Шаг пятый. Управление сигналом 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-симуляцию и видим следующее:

image

Рассмотрим полученное несколько более пристально и обратим внимание на то, как происходит управление линиями SCL, SDA при выполнении команды START после IDLE_STATE: 

image

Четко видно START-сигнал, данные передаются только путем прижатия линии к нулю, четко видно как работает State-машина и начинается обмен информацией. Вся посылка из 11111111 верно выставлена на шине:

image

Также видно адекватное исполнение команды RESTART:

image

Ну и сигнал STOP — также работает верно, кажется все соответствует ожидаемому результату.

image

Что ж, теперь остается проверить все в реальном железе. Словом, успех!

Заключение

Теперь у вас есть возможность самостоятельно поиграться с автоматом, почитать код и, при желании, подробно вникнуть в суть происходящего. Дополнительно можете рассмотреть уже у себя в ModelSIm значения отладочных сигналов и то как работает данный конечный автомат.

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

Большое спасибо за внимание! До встречи в следующих статьях! 🙂 

Ссылка на репозиторий с кодом: github.com/megalloid/I2C_Master_Controller/tree/main


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

Автор: Андрей

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js