
Здравствуйте меня зовут Дмитрий сегодня мы продолжим исследование 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.
Отображение информации о шине
Чтобы понимать что происходит на шине я создал модуль который выводит всю информацию в текстовой форме.

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

Для вывода текста на экран я написал модуль 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.