Здравствуйте меня зовут Дмитрий сегодня мы продолжим исследование FPGA плат и напишем контроллер для шины i2c, а также подключим камеры ov7670 и ov2640.

Данная статья является продолжение статей Доступ к SDRAM памяти на FPGA и «множество Мандельброта» и Создание видеокарты Бена Итера на FPGA чипе. Ну а мы начинаем.

I2C контроллер.

Интерфейс i2c это последовательная шина, использующая для связи всего две линии. Это scl тактовая шина и sda шина данных, благодаря такой простоте она используется для связи в огромном количестве устройств. Более подробно про шину i2c можно почитать вот здесь.

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

Поэтому первое что сделаем это настроем управление линиями данных.

assign scl_io = (scl_r) ? 1'bz : 1'b0;
assign sda_io = (sda_r) ? 1'bz : 1'b0;

В контроллере я использовал целых три конечных автомата, один управлял переключением состояний, второй линией sda, а третий scl.

Автомат состояний
always @(posedge State_clk) 
begin
	if (rst)
	begin
		
		State_main <= S_IDLE;
		
		
	end 
	else
	begin
	
		case(State_main)
		
		S_IDLE:
		begin
			
			m_ready <= 1'b0;
			RepeatedStart <= 1'b0;
			cnt_bayte_get <= 'd0;
			cnt_bayte_send <= 'd0;
			
			if(m_valid) State_main <= S_START;
		end	
	
		
		
		S_START: if (!sda_r) State_main <= S_SEND_ADDR; 
		
		S_REPETED_START:
		begin
			RepeatedStart <= 1'b1;
			if (RepeatedStart & sda_io ) State_main <= S_START;
		end
		
		S_SEND_ADDR:
		begin
			if (TransmitReady) 
			begin
				if (RepeatedStart) NextState <= S_GET_DATA;
				else NextState <= S_SEND_REG;
				State_main <= S_END_TRANSMIT;
			end
		end
		
		
		S_SEND_REG: 
		begin
			if (TransmitReady) 
			begin
				if (m_we) NextState <= S_SEND_DATA;
				else NextState <= S_REPETED_START;
				State_main <= S_END_TRANSMIT;
			end
		end
		
		
		S_SEND_DATA:
		begin
			if (TransmitReady) 
			begin 
				if (DataNum != cnt_bayte_send)
				begin
					NextState <= S_WAITE_NEXT_BYTE_SEND;
					cnt_bayte_send <= cnt_bayte_send + 1'b1;
				end
				
				else NextState <= S_STOP;
				
				State_main <= S_END_TRANSMIT;
			end
		end
		
		
		
		S_WAITE_NEXT_BYTE_SEND:
		begin
			if(m_valid)
			begin
				m_ready <= 1'b0;
				State_main <= S_SEND_DATA;
			end
			else m_ready <= 1'b1;
			
		end
		
		S_WAITE_NEXT_BYTE_GET:
		begin
			if(m_valid)
			begin
				m_ready <= 1'b0;
				State_main <= S_GET_DATA;
			end
			else m_ready <= 1'b1;
			
		end
		
		S_END_TRANSMIT: 
		begin
			if (!scl_r) 
			begin
				 State_main <= S_GET_ACK;
				
			end
		end
		
		S_GET_ACK: 
		begin
			if (ACKGeted & !scl_io) 
			begin
				if (NACK) State_main <= S_STOP;
				
				else State_main <= NextState;
				
			end
		end
		
		S_SEND_ACK:

		begin
			if (ACKGeted & !scl_io) 
			begin
				
				State_main <= NextState;
				cnt_bayte_get <= cnt_bayte_get + 1'b1;
			end
		end
		
		S_GET_DATA:
		begin
			if (TransmitReady) 
			begin 
				if (DataNum != cnt_bayte_get)
				begin
					NextState <= S_WAITE_NEXT_BYTE_GET;
					
				end
				
				else NextState <= S_STOP;
				
				State_main <= S_END_RESIVE;
			end
		end
		
		S_END_RESIVE: 
		begin
			if (!scl_r) 
			begin
				 State_main <= S_SEND_ACK;
				
			end
		end
		
		

		
		S_STOP:
		begin
			if (scl_io & sda_io)
			begin
				State_main <= S_IDLE;
				
				m_ready <= 1'b1;
			end
		end

		endcase
	end

end
Автомат sda линии
always @(posedge Sda_clk) 
begin
if (rst)
begin
	sda_r <= 1'b1; 
end	
else
begin
	case(State_main)

	S_IDLE:
	begin
		NACK <= 1'b0;
		ACKGeted <= 1'b0;
		sda_r <= 1'b1; 
		cnt_bit <= 'd0;
		TransmitReady <= 1'b0;
	end

	S_START: if (sda_r) sda_r <= 1'b0;
	
	S_REPETED_START: if (!scl_r) sda_r <= 1'b1;

	S_SEND_ADDR, 
	S_SEND_REG, 
	S_SEND_DATA:
	begin
		ACKGeted <= 1'b0;
		if (!scl_r) 
		begin
			if (cnt_bit != 4'd7)
			begin
				sda_r <= DATA_SEND[4'd7 - cnt_bit];
				cnt_bit <= cnt_bit + 1;
			end
			else 
			begin
				sda_r <= DATA_SEND[0];
				cnt_bit <= 0;
				TransmitReady <= 1'b1;
			end
			
		end
	end	

	S_END_TRANSMIT,
	S_END_RESIVE:
	begin
		TransmitReady <= 1'b0;
		if (!scl_r) sda_r <= 1'b0;
	end

	S_GET_DATA:
	begin 
		sda_r <= 1'b1;
		ACKGeted <= 1'b0;
		if (scl_io) 
		begin
			if (cnt_bit != 4'd7)
			begin
				DATA_OUT[4'd7 - cnt_bit] <= sda_io;
				cnt_bit <= cnt_bit + 1;
			end
			else 
			begin
				DATA_OUT[0] <= sda_io;
				cnt_bit <= 0;
				TransmitReady <= 1'b1;
			end
		end
		
	end

	S_WAITE_NEXT_BYTE_GET,
	S_WAITE_NEXT_BYTE_SEND: 
	if (!scl_r) sda_r <= 1'b0;

	S_STOP: 
	begin
		if (scl_io) sda_r <= 1'b1;
		else sda_r <= 1'b0;
	end


	S_GET_ACK: 
		begin
			sda_r <= 1'b1;
			if (scl_io) 
			begin
				NACK <= sda_io;
				ACKGeted <= 1'b1;
			end
		end
		
	S_SEND_ACK:
		begin
			if (!scl_io) 
			begin
				if (DataNum == cnt_bayte_get) sda_r <= 1'b1;
				else sda_r <= 1'b0;
				ACKGeted <= 1'b1;
			end
		end
		



	default:
	begin
		
		sda_r <= 1'b1; 
		cnt_bit <= 0;
		TransmitReady <= 1'b0;
		
	end

	endcase
	
end
end	
Автомат scl линии
always @(negedge Sda_clk) 
begin
	if (rst) Counter = 0;
	else
	begin
		
		case(Counter)
			0:  
			begin
				State_clk <= 1'b1;
				Counter <= Counter + 1'b1;
			end
			1:  
			begin
				Sda_clk <= 1'b1;
				Counter <= Counter + 1'b1;
			end
			2:  
			begin
				State_clk <= 1'b0;
				Counter <= Counter + 1'b1;
			end
			3:  
			begin
				Sda_clk <= 1'b0;
				Counter <= 1'b0;
			end
			
				
		endcase
	end
end
Тактовые сигналы со смещением
Тактовые сигналы со смещением

Причем каждый из них тактируется со смещение относительно остальных (автоматы линий sda и scl хоть и тактируются от одного сигнала, но в первом случае тактирование происходит по фронту, а во втором по спаду).

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

Обратной стороной такова решения является, то что вместо стандартных для i2c 100кгц в модуль придется подавать 400кгц.

Симуляция в Icarus-Verilog

Создание i2c контроллера очень непростое дело. И дело даже не в том что придется постоянно заливать на плату новую прошивку при любом изменении кода. А в том что когда прошивку вы зальёте, то у вас может быть два варианта. Либо плата установила связь по интерфейсу i2c или не установила, а почему это произошло вы не узнаете.

И чтобы понять что-же там происходит необходимо произвести симуляцию. Конечно среда Quartus уже имеет встроенные средства симуляции, но они примитивны. Поэтому большинство разработчиков выбирают для симуляции Icarus-verilog. Вот здесь можно почитать как его установить и как им пользоваться.

Надо отметить что Icarus-verilog никак не привязан к Quartus поэтому на нем можно симулировать любой Verilog код не зависимо от того в какой среде разработки его написали.

Тем не менее для симуляции нужно написать специальный тест файл на языке Verilog. Это очень удобно поскольку большую часть кода для теста можно просто скопировать из своего проекта.

i2c тест
module i2c_controller_Test;

reg clk = 1'b0; 
reg rst = 1'b0;

always #1 clk = ~clk;
	
reg m_valid;
reg m_we;
	
	
reg [6:0]  ADDR;
reg [7:0]  REG;
reg [7:0]  DATA_IN;
reg [4:0] DataNum = 0;
	
wire [7:0] DATA_OUT;
wire m_ready;
wire NACK;
	
wire sda_io;
wire scl_io;

pullup(sda_io);
pullup(scl_io);
/*
initial 
begin
#10 rst = 1;

#10 rst = 0;
end*/
reg [3:0] cnt_byte = 0;


reg [3:0] State_main = S_SEND;
localparam 	S_SEND = 4'd0,
				S_WAIT = 4'd1,
				S_GET = 4'd2,
				S_END = 4'd3, 
				S_GET_2 = 4'd4;

always @(posedge clk) 
begin
	if(rst)
	begin
		State_main <= S_SEND;
	end
	else
	begin
		case (State_main)
		S_SEND:
		begin
			ADDR <= 7'b0100001;
			REG <= 8'h0A;
			DATA_IN <= 8'b00111001;
			m_valid <= 1'b1;
			m_we <= 1'b0;
			State_main <= S_WAIT;
			DataNum <= 0;
		end
		S_WAIT:
		begin
			
			if (m_ready)
			begin
				m_valid <= 1'b0;
				State_main <= S_END;
			end
			
			
		end
		
		
		endcase
	end
		
end

initial  #850 $finish;


//создаем файл VCD для последующего анализа сигналов
initial
begin
  $dumpfile("out.vcd");
  $dumpvars(0,i2c_controller_inst);
  $dumpvars(0,i2c_slave_inst);
end

//наблюдаем на некоторыми сигналами системы
initial $monitor($stime,,, clk,, rst,,, m_valid,, m_ready,, NACK,,, sda_io,, scl_io);



i2c_controller  i2c_controller_inst
(

	.clk(clk), 
	.rst(rst), 
	
	.m_valid(m_valid),
	.m_we(m_we),
	.ADDR(ADDR),
	.REG(REG),
	.DATA_IN(DATA_IN),
	.DataNum(DataNum),
	
	.DATA_OUT(DATA_OUT),
	.m_ready(m_ready),
		
	.sda_io(sda_io),
	.scl_io(scl_io)
);


i2c_slave i2c_slave_inst
(
	.clk(clk), 
	.rst(rst),  
	.sda_io(sda_io), 
	.scl_io(scl_io)
);
	
endmodule

Также мне для теста пришлось написать i2c слейва что-бы контроллеру было с кем обмениваться данными.

Результат симуляции можно посмотреть в программе GTKWave.

Здесь мы сначала отправляем адрес устройства, потом номер регистра, потом производим так называемый "повторный старт" и отправив ещё раз адрес устройства но уже с последним битом равным единице, принимаем данные.

Анализатор сигнала

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

Ну во первых возникает вопрос, а как его подключить. Тут все очень просто, ведь у нас FPGA плата мы можем подключить i2c линии к любым 2 неиспользуемым пинам.

assign Analiz_scl = CAM_scl_io;
assign Analiz_sda = CAM_sda_io;

Ну а к ним подключить анализатор.

У меня правда возникла проблема, изначально шина CAM_scl_io имела атрибут output и из-за этого анализатор ничего не видел на этой шине, но патом я изменил его на inout и все стало работать.

SCCB контроллер

Итак в чем-же была проблема? Дело в том что хоть везде и пишут что камеры подключаются по интерфейсу i2c, на самом деле они подключаются по интерфейсу SCCB. Это интерфейс похож на i2c но им не является.

Во-первых этот интерфейс не требует чтобы слейв сбрасывал бит acknowledge (хотя камера его сбрасывала), здесь этот бит даже называется don't care bit то есть бит который не важен.

Кроме того SCCB не предусматривает повторный старт. Здесь все пакеты нужно явно завершать и начинать новые с полноценного старта. Это собственно и была та проблема почему считать ID камеры не удавалось, когда контроллер делал повторный старт камера впадала в ступор и ничего больше не отсылала.

Также интерфейс SCCB не поддерживает последовательного чтения и записи байтов. Это когда мы посылаем адрес первого регистра, а потом последовательно читаем все байта с этого адреса и далее.

Он поддерживает только 3 возможных пакета. Это передача 3 байт, то есть адрес устройства адрес регистра и байт данных. Передача 2 байт, адрес устройства и адрес регистра. Или чтение когда мы передаем адрес устройства и читаем данные из регистра, адрес регистра нужно передать до этого. И все.

Поэтому мне пришлось быстренько переписать i2c контролер в sccb (в репозитории присутствуют оба контроллера).

Инициализация

За инициализацию камер отвечает модуль OV7670_init (сначала я не думал что будет 2 камеры).

Он пытается сперва прочитать OV7670 ID если его не находит то пытается прочитать OV2640 ID. Если присутствует одна из этих камер, то модуль Config_Write записывает настройки по шине sccb. Настройки камер находятся в модулях M_OV2640_Config_ROM и M_OV7670_Config_ROM.

Отображение информации о шине

Чтобы понимать что происходит на шине я создал модуль который выводит всю информацию в текстовой форме.

OV7673
OV7673

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

ov2640
ov2640

Для вывода текста на экран я написал модуль TextController который и выводит символы. Сам шрифт я нарисовал в PhotoShop. Каждый символ имеет ширину 8 точек высоту 10 точек.

Если вы захотите добавить свои символы. Их нужно сохранить в формат BMP главное не забыть при создание выбрать режим bitmap

После этого нужно положить BMP файл в каталог к программе которую я написал:

Скрытый текст
#include <windows.h>
#include <iostream>
#include <locale.h>
#include <wchar.h>
#include <fstream>
#include <vector>
#include <bitset>
#include <filesystem>


BITMAPFILEHEADER FileHeader;
BITMAPINFOHEADER InfoHeader;

std::vector<std::vector<unsigned char>> Font;
unsigned char buf;

void DrawPixel(unsigned char Pix);

int main()
{ 
   
    std::ifstream fin("Font16.bmp",  std::ios::binary);
    std::ofstream fout("Font16.txt");
    if(fin.fail())
    {
        std::cout << "Can't open bmp file " << std::endl;
        return 0;
    } 
    fin.read ((char*)&FileHeader, sizeof(BITMAPFILEHEADER));

    if(FileHeader.bfType != 0x4d42)
    {
        std::cout << "Wrong file type " << std::endl;
        return 0;
    } 

    fin.read ((char*)&InfoHeader, sizeof(BITMAPINFOHEADER));

    if(InfoHeader.biSize != 0x28)
    {
        std::cout << "Wrong header size " << std::endl;
        return 0;
    } 

    if(InfoHeader.biBitCount != 0x1)
    {
        std::cout << "Wrong Bit depth "  << std::endl;
        return 0;
    } 

    std::cout << "Width = " << InfoHeader.biWidth << std::endl;
    std::cout << "Hidth = " << InfoHeader.biHeight << std::endl;

    std::cout << "bfOffBits = " << (int)FileHeader.bfOffBits << std::endl;
    fin.seekg(FileHeader.bfOffBits, std::ios::beg);
    
    int FontCount = InfoHeader.biWidth/8;

    int alignment = 4 - (FontCount % 4); //Bitmap data alignmented by 4

    Font.resize(10);

    std::cout << "FontCount = " << FontCount << std::endl;
    std::cout << "alignment = " << alignment << std::endl;
    for (int i = 9; i > -1; i--)
    {
        Font[i].resize(FontCount);
        for (int j = 0; j < FontCount; j++)
        {
            fin.read((char*)&buf, sizeof(char));
            buf ^= 0b11111111;

            std::bitset<8> Bits;
            for (int k = 0; k < 8; k++)
            {
                if (buf & (1 << k))
                {
                    Bits.set(7 - k, true);
                    
                }
                 
                
            } 
            Font[i][j] = (unsigned char)Bits.to_ulong(); 
            
            
        }
        fin.seekg(alignment, std::ios::cur);
    }

   
    int count = 0;
    for(int j = 0; j < FontCount; j++)
    {
        
        for (int i = 0; i < 10; i++)
        { 
       
            fout << "Font_mem[" << count++ << "] = 8'b" << std::bitset<8>(Font[i][j]) << ";" <<  std::endl;

             for (int k = 0; k < 8; k++)
            {
                if (Font[i][j] & (1 << k)) DrawPixel(0b111111);
                else DrawPixel(0b000000);
            }

            std::cout << std::endl; 
        }
        fout << std::endl; 
       
    } 
    
    fin.close();
    fout.close();
    return 0;
}



void DrawPixel(unsigned char Pix)
{
    switch (Pix)
    {
        case 0b000000: std::cout << " " ; break;

        case 0b111111: std::cout << (char)219; break;

    }
    
}

И вот эта программа уже создаст файл Font16.txt в котором будет текст который нужно поместить в модуль FONT_ROM.

Отображение картинки с камеры

Кроме модуля TextController который выводит на экран текст, есть ещё модуль Picture_Controller этот модуль выводит изображение с камеры на экран, но выводит он его не прямо из камеры, а из SDRAM памяти (про контролер SDRAM памяти можно почитать вот здесь).

В SDRAM информацию записывает модуль MCameraFrame. А буфер с данными для этого модуля подготавливают модули M_OV2640_BufferFiller и M_OV7670_BufferFiller. Модули разные поскольку у каждой из камер свой формат пикселей.

Причем про буферы памяти хочу сказать отдельно. Сначала я разместил их прямо в модулях которые с ними работают. И на экране появились полосы. И это происходило не регулярно а время от времени. Ну вот вы где-нибудь в коде исправили единицу на двойку потом откомпилировали и хоп у вас полосы на экране, откомпилировали ещё раз полосы пропали. И я сначала вообще не понимал что происходит, но я это поборол за счет вынесения буферов в отдельные модули. В очередной раз убеждаюсь, что Verilog это не язык программирования, а буферы это не массивы, как может поначалу показаться. И от того где они находится зависит как будет работать все устройство.

OV7670

Эта камера интересна тем что начинает работать сразу после включения, и её по идее можно даже не конфигурировать, правда изображение в таком случае будет в формате yuv422. Я же переключил её в RGB444.

Разрешение у камеры всего 640x480 и она сильно зеленит, я даже попытался усилить интенсивность красного и синего но это не помогло.

Вот поближе.

На разводы справа и снизу не обращайте внимания это просто пустая SDRAM память, разрешение экрана 800x600.

OV2640

Формат пикселей этой камеры отличается значительно. Например каждый её пиксель выдает только один цвет.

Поэтому я переключил её в разрешение 1600x1200 и из каждых 4-х пикселей сделал один и получил 800x600. Кроме того я так и не смог понять как понизить её чувствительность, из-за этого вся картинка в "засветах" которые выглядят как синие пятна.

Поближе

Здесь камера находится на хорошо освещённом подоконнике, но если её занести в тень, то картинка будет намного лучше.

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

Вывод

Конечно можно сказать что все эти эксперименты с подключением камер к FPGA бесполезны потому что микроконтроллеры прекрасно с этим справляются, а разница в цене отличается на порядок не в пользу FPGA. Но с другой стороны платы на FPGA чипе дают минимально возможную задержку. Поэтому их можно применять там где важна задержка камеры. Я думаю что люди которые занимаются самостоятельной сборкой квадрокоптеров могли-бы использовать FPGA вместо Arduino и Raspbery PI.

Репозиторий на GitHub

Комментарии (0)