Наконец-то нашлось вдохновение и время вернуться к старой статье, в которой я изобретал I2C Master Controller, но так и не довел задачу до логического конца. Спустя почти три года много воды утекло, появилось множество возможностей и ряд компетенций и я хотел бы реанимировать решение этой задачи и продолжить рассказ. Перечитав старый материал, я сформулировал обновленную группу задач: переделать I2C Master Controller, снабдив его функциями, которых не было в первой версии, типа clock stretching и burst-режима при этом сопроводив это детальным описанием процесса реализации и объяснением почему были предприняты те или иные действия. После все это воплотить сначала в симуляции, а потом и на реальном железе, с использованием EEPROM и OLED-дисплея SSD1306.
Вобщем, всем неравнодушным к теме цифровой схемотехники, ПЛИС и шине I2C - добро пожаловать под кат! :)

Дисклеймер. Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи - рассказать о своем опыте. Я не являюсь профессиональным разработчиком под ПЛИС на языке Verilog и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется. Что ж, поехали…
Давайте сформулируем задачу
Итак, прежде чем кидаться в разработку необходимо четко отметить контур круга задач которые мы будем решать.
Необходимо реализовать обновленную версию I2C Master Controller который:
имеет полную поддержку функций I2C Master-устройства с 7-битной адресацией и поддержкой команд START, STOP, WRITE, READ, RESTART;
умеет в запись и чтение байтов с ACK/NACK обработкой;
имеет функцию ожидания медленных Slave-устройств через clock stretching;
умеет обнаруживать потерю арбитража при использовании нескольких Master-устройств на шине;
имеет конфигурируемую частоту SCL через prescaller, чтобы можно было включать в коде режимы Standard / Fast / Fast Mode Plus;
умеет в прерывания на завершение транзакции (DONE) и потери арбитража (AL);
оснащен 2-stage синхронизаторами на входах SDA/SCL;
умеет в составные команды (START+WRITE, READ+NACK+STOP) через секвенсор;
умеет в пакетную передачу с автоматической передачей N байт одной командой;
Реализовывать это все будем используя Verilog и проверять будем сначала в симуляции, потом на плате Saylinx с Cyclone IV EP4CE6F17C8 (на которую я делал обзор в одной из предыдущих статей). Используя логический анализатор протестируем работоспособность с EEPROM 24LC04 и OLED-дисплеем SSD1306. Дополнительно оснастим это все хозяйство возможностью быстрой компиляции-симуляции, lint-проверки через Verilator, генерацию VCD через GTKWave, TCL-скрипты для Quartus/Questa и прочую полезнятину.
Из целей к которым стремлюсь далее:
После сделаем поддержку AXI4-Lite Slave интерфейса с регистрами управления;
Проверим в окружении с интеграцией в Zynq, AXI Interconnect;
Напишем Linux драйвер для управления I2C-устройствами на плате Zynq Mini;
В общем задача объемная и интересная. Поехали ?
Создаем проект в Quartus
Сразу создаем проект под плату на которой мы будет проводить последующие тесты. Открываем Quartus и нажимаем в меню File - New Project Wizard. Назовем проект I2C-Master-Controller-v2:

Выбираем ПЛИС использованную на этой плате EP4CE6F17C8:

Жмякаем Finish и добавим новый файл Verilog HDL File через меню File - New:

Вставим в него заготовку модуля и сохраняем с именем i2c_master_core.v в корневом каталоге проекта:
`timescale 1ns / 1ps module i2c_master_core ( input wire clk_i, input wire rstn_i ); endmodule
Выставим в опциях проекта данный файл Top-level entity, чтобы можно было проверить на корректность компиляции первого созданного файла в проекте (когда мы его напишем, конечно же).

Сохраняем. Переходим к рассмотрению вопросов проектирования.
Коротко вспомним основное про I2C
Давайте коротко вспомним базовые основы I2C и жесткие правила которые в свою очередь определят архитектуру основного модуля:
Open-drain выходы. Мастер может только притянуть линию к 0 или отпустить в high-Z. Подать логическую единицу он не может, за это отвечает по сути внешний pull-up резистор. То есть получается, что модуль будет управлять не самими сигналами SCL/SDA, а output-enable тристейт-буферов.
SDA меняется только при SCL=0. Во время передачи данных SDA может переключаться только когда SCL удерживается мастером в 0. Если SDA меняется при SCL=1 - это условие START или STOP. Это сразу накладывает жесткое ограничение на порядок операций в FSM основного модуля. Мы никогда не сможем менять SDA и SCL одновременно (в одном такте) - это создало бы гонку сигналов, и slave мог бы интерпретировать изменение SDA при ещё-высоком SCL как ложный START/STOP. Отсюда вытекает необходимость разделения каждого бита на несколько фаз, где SDA и SCL меняются строго по очереди. Далее при реализации будет понятно о чем речь.
START = SDA 1→0 при SCL=1 и STOP = SDA 0→1 при SCL=1. START и STOP - это не просто значения на линиях, а по сути переходы (фронты). Это значит, что для генерации START нужно сначала гарантировать SDA=1, потом при SCL=1 перевести SDA в 0. Нельзя просто установить SDA=0 и SCL=1 и нужна последовательность из нескольких шагов.
Данные передаются MSB first (старший бит первым). Тут все просто, сдвиговый регистр должен сдвигать влево, а текущий выдаваемый бит берётся из старшего разряда.
Каждый байт данных = 8 бит + 1 бит ACK/NACK (всего 9 бит-слотов). То есть счётчик битов считает от 0 до 8 (9 значений), а сдвиговый регистр будет иметь ширину 9 бит, а не 8. Девятый бит-слот имеет служебное назначение: при WRITE мастер отпускает SDA и слушает ACK от slave, при READ мастер сам выдаёт ACK/NACK.
Clock stretching. Slave может удерживать SCL в 0, чтобы попросить мастера подождать. Мастер обязан проверять, что SCL действительно поднялся, прежде чем продолжать. Нельзя просто отпустить SCL и двигаться дальше. Нужен вход
scl_i(реальное значение на пине) и условиеif (scl_i)перед продвижением к следующей фазе. Это добавляет вход в модуль и ветвление в каждое состояние, где SCL поднимается.Arbitration. Если несколько мастеров на шине, то мастер, который отпустил SDA (ожидает 1), но видит 0 - потерял арбитраж и должен немедленно освободить шину. Получается, что нужен мониторинг SDA в определённые моменты (когда SCL=1 и мастер ожидает SDA=1). И при обнаружении конфликта необходим аварийный выход из любого состояния в IDLE с освобождением линий. Это добавляет "аварийный путь" в FSM, который обходит нормальную логику переходов.
Вроде бы все основные моменты учел. Подробнее про протокол I2C я писал в предыдущих статьях, можете обратиться к ним, если хотите вспомнить что-то более детально.
Пошаговый рецепт проектирования
Теперь коротко опишу свое видение процесса проектирования ядра с нуля. Учитывая постановку задачи, я приведу порядок шагов, которые построены по принципу “от внешнего к внутреннему” и “от простого к сложному”:
Сначала определяем что модуль делает и его интерфейс - это контракт с внешним миром.
Потом определяем как тактируется модуль и общую модель тактирования - это базовое ограничение для всей логики.
Затем проектируем микроархитектуру и фазы бита - это будет базовым строительным блоком.
Строим FSM поверх фаз - из этого будет выстроена высокоуровневая логика.
Добавляем регистры данных и планируем путь данных.
Добавляем обработку исключений таких как stretching и арбитраж;
Шлифуем граничные случаи - это всегда самое сложное и последнее потому что граничные случаи IDLE зависят от всех остальных компонентов:
busy_o(из мониторинга),arb_lost_o(из арбитража), текущего состояния SCL/SDA (из FSM). Пока эти компоненты не спроектированы, невозможно корректно обработать граничные случаи.
Попытка проектировать в обратном порядке (сначала FSM, потом интерфейс) приводит к переделкам, когда интерфейс не стыкуется с уже реализованной логикой. При этом отдельно рекомендую писать тестбенчи параллельно с реализацией: написал ST_START - протестировал START. Написал ST_DATA - добавил WRITE-тест. Но, рассмотрение тестбенчей я вынес в отдельную статью, там материала очень много, чтобы его рассматривать в рамках этой статьи.
Теперь перейдем к рассмотрению каждого шага в подробностях.
Интерфейсы модуля
Интерфейс - это контракт модуля с окружением. Изменение интерфейса после реализации ядра каскадно затрагивает все модули, которые его используют. Фиксация и детальное продумывание интерфейса на раннем этапе экономит огромное количество времени сводя необходимость переделок к нулю. На этом шаге я выбираю в качеств основных элементов интерфейса 5 атомарных команд вместо составных, open-drain OEN-интерфейс для SCL/SDA вместо прямого управления (потому что open-drain - требование протокола) и ready/valid рукопожатие вместо FIFO или req/ack (см. обоснование ниже).
Судя по логике, которую должно выполнять ядро, основной модуль - оно должно предоставить простой командный интерфейс и интерфейс данных:

Все порты разделены на четыре логические группы: тактирование, командный интерфейс, статус/мониторинг и подключение к шине I2C.
module i2c_master_core ( input wire clk_i, input wire rstn_i, input wire ena_i, // 1-tick pulse per quarter-SCL period // Command interface (active when ready_o == 1) input wire cmd_valid_i, // Level: held high until accepted input wire [2:0] cmd_i, // Command code input wire [7:0] din_i, // TX data (WRITE) / {7'bx, NACK} (READ) output reg [7:0] dout_o, // RX data (valid after READ completes) output reg rx_ack_o, // ACK received from slave (0=ACK,1=NACK) output reg ready_o, // Ready to accept next command // Status output reg arb_lost_o, // Arbitration lost (sticky, clear via _clear_i) input wire arb_lost_clear_i,// Pulse to clear arb_lost_o output reg busy_o, // I2C bus busy (START seen, no STOP yet) // I2C pad interface — directly to tri-state buffers input wire scl_i, // SCL pad input (synchronised externally) output reg scl_oen_o, // SCL output-enable: 1=release, 0=drive low input wire sda_i, // SDA pad input (synchronised externally) output reg sda_oen_o // SDA output-enable: 1=release, 0=drive low ); endmodule
Группа №1. Тактирование и сброс
Входной сигнал
clk_i. Основной тактовый сигнал FPGA. Вся логика ядра синхронна по нарастающему фронту этого клока. Частотаclk_iне связана с частотой SCL - она определяет только скорость реакции на внутренние события (фронты SDA/SCL, арбитраж).Входной сигнал
rstn_i. Асинхронный сброс, активный LOW. Приrstn_i= 0 все регистры ядра сбрасываются: FSM → IDLE, SCL и SDA отпускаются (oen = 1),ready_o = 1. Асинхронный сброс выбран потому, что cинхронный сброс (if (rst) ... внутри always @(posedge clk)) требует работающего клока для срабатывания. Но в момент включения FPGA клок может отсутствовать или быть нестабильным. I2C-шина с pull-up резисторами при включении находится в состоянии HIGH (idle), и ядро должно немедленно перейти в состояние, когда оно не мешает шине (отпустить SCL/SDA). Асинхронный сброс гарантирует это независимо от наличия клока. В FPGA асинхронный сброс хорошо поддерживается аппаратно (dedicated reset path в flip-flops).Входной сигнал
ena_i. Одноцикловый импульс-разрешение (clock enable). FSM продвигается на одну фазу только в те тактыclk_i, когдаena_i = 1. Генерируется внешним прескалером. Частотаena_i = 4 × fSCL(4 фазы на один период SCL). Например, для SCL = 100 кГц нуженena_iс частотой 400 кГц, что приclk_i= 50 МГц означает один импульс каждые 125 тактов.
Группа №2. Командный интерфейс.
Входной сигнал
cmd_valid_i. Уровневый сигнал 1 = вышестоящий контроллер выставил валидную команду. Удерживается высоким, пока ядро не примет команду (сброситready_o). Можно опустить после приёма, можно держать - ядро защёлкивает команду по фронту приёма.Входной сигнал
cmd_i[2:0].Тут выставляется код команды (см. ниже). Должен быть стабилен одновременно сcmd_valid_i.Входной сигнал
din_i[7:0]. Входные данные. Для WRITE - байт для передачи на шину. Для READ - битdin_i[0]задаёт ACK/NACK, который мастер пошлёт slave после приёма байта (0 = ACK = "давай ещё", 1 = NACK = "хватит"). Для START/STOP/RESTART - игнорируется. Должен быть стабилен в момент приёма команды. Можно было бы добавить отдельный портnack_iдля управления ACK/NACK при READ. Но это лишний порт, который используется только при READ, и только для одного бита. Переиспользованиеdin_i[0]экономит порт и полностью описывает сценарий: при WRITE контроллер загружает 8 бит данных, при READ - загружает значение ACK/NACK в младший бит. Семантика определяется текущей командой.Выходной сигнал
dout_o[7:0]. Принятый байт данных. Валиден после завершения команды READ (когдаready_oвозвращается в 1). После WRITE содержит "эхо" переданных данных (игнорируется контроллером). После START/STOP - не определён.Выходной сигнал
rx_ack_o. Принятый бит ACK/NACK от slave. 0 = ACK (slave подтвердил), 1 = NACK (slave не ответил или отказал). Валиден после завершения WRITE или READ. Вышестоящий контроллер проверяет этот сигнал для определения успешности операции.Выходной сигнал
ready_o. Готовность ядра. 1 = ядро свободно, принимает команды. 0 = ядро занято выполнением текущей команды. Переход 1→0 происходит при приеме команды. Переход 0→1 - при завершении. В этот моментdout_oиrx_ack_oсодержат результаты.
Группа №3. Статус и мониторинг шины.
Выходной сигнал
arb_lost_o. Sticky-флаг потери арбитража. Устанавливается в 1, когда ядро обнаружило конфликт: мастер отпустил SDA (ожидал HIGH), но прочитал LOW. Не сбрасывается автоматически - контроллер должен явно сбросить черезarb_lost_clear_i. Пока флаг установлен, ядро не принимает новые команды (защита от повторных попыток без обработки ошибки). Сценарий без sticky: ядро обнаружило арбитраж, установилоarb_lost_o = 1, через один такт сбросило. Контроллер в это время мог быть занят другой логикой и пропустить однотактовый импульс. Результат: контроллер не знает об ошибке и выдаёт следующую команду, которая тоже провалится. Sticky-флаг гарантирует, что информация "дождётся” обработки.Входной сигнал
arb_lost_clear_i. Одноцикловый импульс для сбросаarb_lost_o. Вышестоящий контроллер выдаёт этот импульс после обработки ситуации потери арбитража (обычно перед повторной попыткой транзакции).Выходной сигнал
busy_o. Флаг занятости шины I2C. 1 = на шине обнаружен START (любым мастером), STOP ещё не было. 0 = шина свободна. Определяется по реальным сигналам на пинах SCL/SDA, а не по внутреннему состоянию FSM. Используется внутри ядра (управление линиями в IDLE) и может использоваться вышестоящим контроллером для принятия решений (ждать освобождения шины).
Группа №4. Подключение к шине I2C (pad-интерфейс).
Входной сигнал
scl_i. Текущее значение линии SCL, прочитанное с пина FPGA. Используется для обнаружения clock stretching, детекции START/STOP на шине, проверки арбитража. Должен быть предварительно синхронизирован 2-стадийным синхронизатором (в обертке, не в ядре). Двухстадийный синхронизатор (два последовательных flip-flop для подавления метастабильности) - стандартная практика для асинхронных входов. Но его размещение - архитектурный выбор. Если размещать его внутри ядра, то это проще для пользователя (подключил пин - и забыл). Но если несколько модулей используют одну шину (например, ядро + внешний монитор), каждый имеет свою копию синхронизатора, и они могут давать разные значения в одном такте (из-за метастабильности). Это создаёт несогласованность. Вариант снаружи (наш выбор) дает один синхронизатор на пин, его выход разводится ко всем потребителям. Все видят одно и то же значение. Это надёжнее и стандартнее. Правда есть минус: пользователь обертки должен не забыть добавить синхронизатор.Выходной сигнал
scl_oen_o. Output-enable для SCL. Значение 1 = отпустить (high-Z, pull-up подтянет к VCC). 0 = притянуть к GND (drive low). Это не значение SCL, а управление тристейт-буфером. Шина I2C - open-drain и мастер может только притянуть линию к 0 или отпустить.Входной сигнал
sda_i. Текущее значение линии SDA, прочитанное с пина FPGA. Аналогичноscl_i, должен быть предварительно синхронизирован. Используется для приёма данных (READ), чтения ACK/NACK, детекции START/STOP, проверки арбитража.Выходной сигнал
sda_oen_o. Output-enable для SDA. Семантика та же, что и уscl_oen_o: 1 = отпустить, 0 = притянуть к 0. При передаче данныхsda_oen_oнапрямую отражает значение бита: для передачи 1 мастер отпускает линию (oen = 1), для 0 - притягивает (oen = 0).
Почему интерфейс к шине - OEN, а не прямой data_out? Протокол I2C основан на open-drain. Мастер физически не может выдать логическую единицу на SDA/SCL - он может только притянуть к земле или отпустить. Уровень HIGH создается внешним pull-up резистором.
Если бы интерфейс был scl_o / sda_o (прямое значение), возникла бы двусмысленность:
что значит
sda_o = 1?активно выдать HIGH (push-pull)?
или отпустить?
OEN-интерфейс однозначен:
oen = 1 → high-Z → pull-up → 1
oen = 0 → drive → 0.
На уровне top-level модуля OEN-сигналы подключаются к тристейт-буферам:
// В обертке (top-level): assign i2c_sda = sda_oen ? 1'bz : 1'b0; // oen=1 → high-Z, oen=0 → drive LOW assign i2c_scl = scl_oen ? 1'bz : 1'b0; // Синхронизация входов: reg [1:0] sda_sync, scl_sync; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin sda_sync <= 2'b11; scl_sync <= 2'b11; end else begin sda_sync <= {sda_sync[0], sda_pad_in}; scl_sync <= {scl_sync[0], scl_pad_in}; end end wire sda_s = sda_sync[1]; // → подаётся на sda_i ядра wire scl_s = scl_sync[1]; // → подаётся на scl_i ядра
Значение 1'b0 в тристейт-присваивании - потому что open-drain: когда мы "активны" (oen=0), мы всегда тянем линию к земле, никогда к VCC. Значение 1'bz - это high-impedance, при котором pull-up резистор подтягивает линию к VCC.
Так, с уровнем интерфейсов модуля разобрались. Идём дальше.
Команды
Первым шагом перейдем к проектированию основного модуля, который будет принимать о вышестоящего контроллера (секвенсер, CPU, тестовый FSM) высокоуровневые команды (START, WRITE byte, READ byte, STOP, RESTART) и генерировать корректные сигналы SCL/SDA на шине I2C, соблюдая все требования протокола. Стоит отметить, что данный модуль не будет содержать регистров управления, прескалера, прерываний - это чистое "ядро” для реализации конечного автомата и выдачи сигналов на физические линии подключенные к ногам ПЛИС.
Теперь нужно подумать - каким образом будет реализован интерфейс команд и какие команды будем принимать, с каким кодом, по каким правилам. В голову приходит простое решение:
Делаем 5 команд, покрывающие все I2C-сценарии:
3'd1 - START - генерируем условие START;
3'd2 - WRITE - передаем байт из din_i, получить ACK;
3'd3 - READ - принять байт от Slave, отправить ACK/NACK;
3'd4 - STOP - генерировать условие STOP;
3'd5 - RESTART - генерировать повторный START без предварительного STOP;
По итогу пишем следующий код:
// --------------------------------------------------------------- // Command encoding // --------------------------------------------------------------- localparam [2:0] CMD_NOP = 3'd0, CMD_START = 3'd1, CMD_WRITE = 3'd2, CMD_READ = 3'd3, CMD_STOP = 3'd4, CMD_RESTART = 3'd5;
Можно было бы конечно добавить сюда составные команды STA+WR, RD+NACK+STO и другие комбинации. Но это усложняет ядро без необходимости. Составные команды эффективнее реализовать в слое выше, оставив ядро с атомарным набором действий.
Рукопожатие для работы с вышестоящим контроллером
Далее необходимо сделать возможность рукопожатия ready/valid между основным модулем и внешним контроллером, чтобы корректно обрабатывать подаваемые команды и не реагировать на некорректные данные поступающие на вход модуля. Логика работы заключается в том, что контроллер (источник команд) выставляет valid + data сигналы, а ядро подтверждает выдачу сигнала на линию и готовность обработать следующую порцию данных через сигнал ready:

Одна фаза - один такт на приём команды. Простая реализация и минимальная задержка, которая естественно вписывается в FSM, когда ready_o = 1 в IDLE-режиме, ready_o = 0 во время работы. Получается следующий алгоритм:
Внешний контроллер ждёт
ready_o = 1;Выставляет
cmd_valid_i = 1,cmd_i,din_i;Ядро видит запрос, сбрасывает
ready_o = 0- начинает работу;Контроллер может убрать
cmd_valid_i;Ядро генерирует I2C-сигналы на линии;
После выполнения ядро поднимает
ready_o = 1- исполнение команды завершено;dout_oсодержит принятый байт (для READ),rx_ack_o- полученный ACK;
4-фазная модель бита I2C
Зачем нужны фазы? Один бит I2C - это один полный период SCL. Но внутри этого периода происходят разные вещи: подготовка данных, установка SCL, чтение данных, снятие SCL. Поэтому нам нужно разделить бит на подэтапы. Можно было бы попытаться обработать весь бит в одном такте - выставить SDA, поднять SCL, прочитать данные, опустить SCL. Но это невозможно по трем причинам:
Тайминг. Спецификация I2C требует минимальных задержек между событиями. SCL должен быть HIGH как минимум 4 мкс (Standard Mode). Нельзя поднять и опустить SCL за один такт - slave не успеет среагировать.
Clock stretching. После того как мастер отпускает SCL, нужно проверить, что SCL реально поднялся. Это может занять неопределенное время (slave удерживает SCL). Значит, нужно состояние ожидания - а это уже несколько тактов.
Разделение SDA и SCL изменений. Правило 2 требует, чтобы SDA менялось только при SCL=0. Если в одном такте менять оба сигнала, на реальной плате из-за разницы задержек (routing, буферы) slave может увидеть изменение SDA при ещё-высоком SCL.
Поэтому каждый бит неизбежно требует нескольких тактов. Вопрос только в количестве. Самое естественное деление на 4 четверти периода SCL:

Фаза 0 - установить SDA (данные или release);
Фаза 1 - slave может растянуть clock тут;
Фаза 2 - данные стабильны, семплируем;
Фаза 3 - подготовка к следующему биту;
Можно задать вопрос, мол, а почему именно 4 фазы, а не 2, 3 или 8? Вроде бы 2 фазы (SCL LOW / SCL HIGH) кажутся достаточными на первый взгляд. Но в фазе SCL LOW нужно сделать два дела: установить SDA и подготовиться к следующему биту (сдвиг регистра). А в фазе SCL HIGH нужно дождаться реального SCL=1 (stretching) и дать время для стабилизации данных (hold time). Две фазы не дают достаточно гранулярности для разделения этих подзадач. Например, конкретная проблема: в фазе 0 (SCL LOW) мы сдвигаем регистр и выставляем SDA. Но если объединить сдвиг и выдачу, нужно точно знать, какой бит уже на SDA, а какой ещё нет. При 2 фазах это приводит к тонким ошибкам на последнем бите (бит 8 - ACK).
Вариант с 3 фазами (setup / SCL HIGH / shift) вполне работоспособный и некоторые реализации I2C так и делают. Но спецификация требует минимальный tHIGH (время SCL в HIGH). При 3 фазах SCL HIGH занимает только 1/3 периода, что при граничных частотах может быть впритык. При 4 фазах SCL HIGH занимает 2/4 = 50% периода, что дает хороший запас по таймингу и симметричный сигнал.
8 фаз или больше дают ещё более точный контроль тайминга, но избыточны. Каждая дополнительная фаза - это дополнительный бит в счётчике фаз, усложнение case-блоков и замедление итерации (больше ena_i тиков на бит). Для Standard Mode (100 кГц) и даже Fast Mode (400 кГц) 4 фазы обеспечивают все необходимые тайминги.
По итогу получаем что 4 фазы - оптимальный компромисс. Это минимально достаточное количество для соблюдения всех тайминговых требований, симметричный SCL (50% duty cycle), и простой 2-битный счётчик фаз (phase_r[1:0]), который дёшево ложится на FPGA.
Фаза 0 (SCL=LOW, setup). Безопасно менять SDA - SCL LOW, изменение SDA не создаст ложный START/STOP.
Фаза 1 (SCL→HIGH, sample). Отпускаем SCL. Проверяем, что SCL действительно поднялся (clock stretching). Семплируем SDA.
Фаза 2 (SCL=HIGH, hold). Держим SCL высоким. Данные стабильны. Дополнительное время для slave.
Фаза 3 (SCL→LOW, shift. Притягиваем SCL к 0. Продвигаем сдвиговый регистр. Инкрементируем счётчик бит.
Cемплирование SDA происходит именно в фазе 1, а не в фазе 2 по ряду причин. Обе фазы - SCL HIGH, данные должны быть стабильны. Но фаза 1 - это момент сразу после подъёма SCL. Если slave ответил ACK (притянул SDA к 0), он сделал это еще в фазе 0 предыдущего бита или раньше. К фазе 1 данные уже стабилизировались. Технически можно сэмплировать и в фазе 2 - результат будет тот же. Я выбрал фазу 1 потому что если SCL задержался из-за stretching, мы семплируем сразу, как только SCL поднялся - это минимизирует задержку и фаза 2 остается "свободной", что упрощает код (нет побочных эффектов).
Такое деление я применил единообразно ко всем типам бит (данные, ACK, START, STOP, RESTART), что значительно упрощает архитектуру. Я мог бы оптимизировать START и STOP - им не нужен сдвиговый регистр, у них нет 9 бит-слотов, и теоретически можно обойтись 2-3 фазами. Но разная длительность фаз для разных состояний означает, что прескалер (генератор ena_i) должен знать, в каком состоянии ядро, чтобы генерировать тики с разной частотой. Это ломает модульность. При единообразных 4 фазах прескалер - это просто счётчик, который не зависит от внутреннего состояния ядра. START и STOP занимают 4 такта ena_i (вместо возможных 2-3), но это микроскопическая "потеря" на фоне 36 тактов на байт данных.
Теперь разберемся с тактированием фаз и сигналом ena_i. Тут все просто. Ядро контроллера I2C тактируется основным клоком FPGA, в моем случае это 50 МГц, но фазы формирования сигнала продвигаются по сигналу ena_i от внешнего прескалера и ничего не знает о частоте.
Помимо этого отдельно отмечу, что все построено на схеме основной клок + enable (сигнал ena_i), вместо работы на внешней частоте SCL. Создание отдельного клокового домена для SCL на первый взгляд кажется интуитивным - ядро работает на “родной частоте” I2C, но это влечет за собой ряд проблем:
Clock Domain Cross (CDC): все сигналы между основным клоком и SCL-клоком нуждаются в синхронизаторах.
cmd_valid_i, cmd_i, din_i, ready_o, dout_o- каждый требует 2-стадийного синхронизатора или FIFO для пересечения доменов. Это многократно увеличивает сложность.Jitter: PLL/делитель для формирования частоты вносит джиттер, который в случае I2C не критичен, но создает дополнительные заботы при STA (static timing analysis).
Ресурсы: отдельный клоковый домен на FPGA требует глобальной клоковой сети, которых ограниченное количество.
Clock stretching: если SCL “клок” управляется внешним slave (stretching), PLL не может за ним следовать.
Enable-подход (ena_i) решает все эти проблемы: один клоковый домен, нет CDC, нет PLL, stretching обрабатывается обычным if.
Конкретный расчет будет производиться следующим образом. Поскольку 4 фазы = 1 SCL-период, то частота ena_i = 4 × fSCL. Для Standard Mode (fSCL = 100 кГц): fena = 400 кГц. При clk = 50 МГц: прескалер = 50 000 000 / 400 000 = 125. То есть ena_i = 1 раз в 125 тактов clk. Прескалер в данном случае будет простым счётчиком от 0 до 124 с выдачей импульса при переполнении.
Архитектура FSM (конечный автомат)
На этом этапе необходимо определить состояния в которых будет пребывать наш автомат. Необходимо четко понимать логику до написания кода. Для удобства можно составить таблицы для каждого состояния.
Как пример для ST_START:
Фаза |
SCL_oen |
SDA_oen |
Действие |
Переход |
0 |
1 |
1 |
— |
|
1 |
1 |
1 |
hold |
→ фаза 2 |
2 |
1 |
0 |
START! |
→ фаза 3 |
3 |
0 |
0 |
— |
→ IDLE, ready=1 |
Таблица позволяет проверить логику до написания кода. Каждая строка - одна фаза, каждый столбец - одно решение. С помощью такой таблицы можно проверить нет ли момента, когда SCL и SDA меняются одновременно; везде ли, где SCL отпускается, есть проверка scl_i для stretching; правильный ли порядок фронтов для START/STOP.
Расписывать данные таблицы для каждого этапа не буду, предлагаю вам самостоятельно потренироваться в этом.
Итак. Состояния верхнего уровня предполагаются в таком виде:

Всего будет 5 состояний - это 1:1 отображение I2C-операций на FSM-стейты, и добавим состояние IDLE:
ST_IDLE - состояние ожидания команды.
ST_START - 4 фазы, 1 бит-слот, итого 4 фазы - для реализации условия START;
ST_DATA - 4 фазы, 9 бит-слотов, итого 36 фаз - для передачи/приема байта + ACK;
ST_STOP - 4 фазы, 1 бит-слот, итого 4 фазы - для реализации условия STOP;
ST_RESTART - 4 фазы, 1 бит-слот, итого 4 фазы - для реализации условия повторного START;
Зафиксируем сразу в коде заготовку:
// --------------------------------------------------------------- // FSM states (high-level) + phase counter (0-3 per operation) // --------------------------------------------------------------- localparam [2:0] ST_IDLE = 3'd0, ST_START = 3'd1, ST_DATA = 3'd2, ST_STOP = 3'd3, ST_RESTART = 3'd4; reg [2:0] state_r; // Регистр текущего состояния FSM reg [1:0] phase_r; // Регистр текущей фазы reg [2:0] cmd_r; // Защелкнутая команда (WRITE / READ) для передачи данных reg [3:0] bit_cnt_r; // Счетчик битовых слотов для передаваемого байта reg [8:0] tx_shift_r; // Сдвиговый регистр TX {data[7]…data[0], ack_ctl} reg [8:0] rx_shift_r; // Сдвиговый регистр RX // --------------------------------------------------------------- // Main FSM // --------------------------------------------------------------- always @(posedge clk_i or negedge rstn_i) begin if (!rstn_i) begin state_r <= ST_IDLE; phase_r <= 2'd0; cmd_r <= CMD_NOP; bit_cnt_r <= 4'd0; tx_shift_r <= 9'd0; rx_shift_r <= 9'd0; scl_oen_o <= 1'b1; sda_oen_o <= 1'b1; ready_o <= 1'b1; dout_o <= 8'd0; rx_ack_o <= 1'b0; end else if (ena_i) begin case (state_r) // ======================================================= // IDLE — wait for a command // ======================================================= ST_IDLE: begin end // ======================================================= // START condition: SDA 1→0 while SCL=1 // Phase 0 : SDA=1, SCL=1 — wait for SCL high (stretching) // Phase 1 : SDA=1, SCL=1 — hold // Phase 2 : SDA=0, SCL=1 — START // Phase 3 : SDA=0, SCL=0 — done // ======================================================= ST_START: begin end // ======================================================= // DATA — 9 bit-slots (8 data + 1 ACK/NACK), 4 phases each // Phase 0 : SCL=0, setup SDA // Phase 1 : SCL=1, sample SDA (wait for stretching) // Phase 2 : SCL=1, hold // Phase 3 : SCL=0, advance bit counter // ======================================================= ST_DATA: begin end // ======================================================= // STOP condition: SDA 0→1 while SCL=1 // Phase 0 : SDA=0, SCL=0 // Phase 1 : SDA=0, SCL=1 — wait for stretching // Phase 2 : SDA=1, SCL=1 — STOP // Phase 3 : hold — done // ======================================================= ST_STOP: begin end // ======================================================= // RESTART — repeated START // Phase 0 : SDA=1, SCL=0 — release SDA first // Phase 1 : SDA=1, SCL=1 — wait for stretching // Phase 2 : SDA=0, SCL=1 — START condition // Phase 3 : SDA=0, SCL=0 — done // ======================================================= ST_RESTART: begin end default: begin state_r <= ST_IDLE; scl_oen_o <= 1'b1; sda_oen_o <= 1'b1; ready_o <= 1'b1; end endcase end // ena_i end
При этом при проектировании сделано ключевое упрощение - на уровне I2C-шины WRITE и READ отличаются только одним: кто управляет SDA в каждом бит-слоте:
WRITE (master передаёт):
Бит 0-7: мастер выдаёт → SDA =
tx_shift_r[8]Бит 8: slave отвечает → SDA = release (high-Z), читаем ACK
READ (slave передаёт):
Бит 0-7: slave выдаёт → SDA = release (high-Z), читаем данные
Бит 8: мастер отвечает → SDA = 0(ACK) или 1(NACK)
В каждом бит-слоте SDA либо управляется мастером (sda_oen_o = tx_shift_r[8]), либо отпускается (sda_oen_o = 1). Разница только в том, какие бит-слоты управляются мастером, а какие он отпускает. И получается, что это один бит решения, а не два разных FSM. Это можно выразить одним сигналом sda_input_mode:
// --------------------------------------------------------------- // SDA direction helpers // --------------------------------------------------------------- // sda_input_mode = 1 when the *slave* should be driving SDA: // READ bits 0-7 — slave sends data // WRITE bit 8 — slave sends ACK/NACK wire sda_input_mode = (state_r == ST_DATA) && ( (cmd_r == CMD_READ && bit_cnt_r < 4'd8) || (cmd_r == CMD_WRITE && bit_cnt_r == 4'd8) );
Когда sda_input_mode = 1 - мастер отпускает SDA и слушает. Когда 0 - мастер выдает бит из сдвигового регистра.
Принимая подобное решение в голове еще должна сохраняться идея экономии ресурсов. Два отдельных состояния (ST_WRITE, ST_READ) с почти идентичной логикой фаз означают дублирование case-блоков для фаз 0, 1, 2, 3. На FPGA это: удвоение LUT-ов для управления SCL/SDA, удвоение числа переходов в FSM, удвоение поверхности для ошибок при модификации. Один сигнал sda_input_mode заменяет всё это одним мультиплексором. Один FSM-стейт вместо двух = меньше логики, меньше багов, проще верификация.
Проектирование каждого состояния. ST_IDLE
Первое состояние основного модуля - ST_IDLE. В этом состоянии модуль ждёт, пока вышестоящий контроллер не выставит команду. Когда ядро находится в IDLE - ему нужно решить, что делать с SCL и SDA. Есть несколько ситуаций:
Шина свободна, после STOP или при включении - то нужно отпустить обе линии, которые будут в 1 из-за pull-up;
Шина занята, команда ожидается, то есть между START и TOP - то нужно держать линии как есть и не отпускать, потому что если отпустить SDA при SCL=1, то получится ложный STOP;
Если пришла новая команда то необходимо защелкнуть команду, перейти в указанное состояние.
В IDLE состоянии нужно различать "шина свободна” и "шина занята”. При типичной I2C-транзакции (START → WRITE addr → WRITE data → STOP) ядро возвращается в IDLE три раза: после START, после WRITE addr, после WRITE data (перед STOP).
В каждом из этих "промежуточных IDLE" шина занята - SCL удерживается в LOW (мастер не отпускал его после последней фазы 3 предыдущей команды). Если бы IDLE безусловно отпускал SCL и SDA, после каждой команды slave бы видел:
1. SCL переходит в HIGH (мастер отпустил);
2. SDA в неопределённом состоянии → возможен STOP;
3. Slave сбрасывает свой FSM → транзакция разрушена;
Именно поэтому мы проверяем busy_o: если шина занята, линии остаются как есть. Если свободна - то безопасно отпускаем. Выражается в коде это так:
ST_IDLE: begin if (cmd_valid_i && !arb_lost_o) begin ready_o <= 1'b0; // Начинаем работу case (cmd_i) CMD_START: ... → ST_START CMD_WRITE: ... → ST_DATA CMD_READ: ... → ST_DATA CMD_STOP: ... → ST_STOP CMD_RESTART: ... → ST_RESTART endcase end else if (!busy_o) begin scl_oen_o <= 1'b1; // Шина свободна — отпустить sda_oen_o <= 1'b1; end // Если busy_o=1 но нет команды — ничего не делаем, держим линии end
При этом необходимо проверять !arb_lost_o. Если арбитраж потерян, новые команды не принимаются до явного сброса флага. Это предотвращает ситуацию, когда контроллер пытается повторить транзакцию, не узнав о потере арбитража. Без этой проверки ядро могло бы начать новый START на шине, которая уже захвачена другим мастером.
Необходимо учесть, что безусловно отпускать линии в IDLE нельзя. Это создаёт ложные STOP между командами составной транзакции (START + WRITE + WRITE + STOP). Между WRITE и WRITE ядро ненадолго попадает в IDLE (ждёт следующую команду), и если отпустит SDA при SCL=1 - slave увидит STOP.
Следующий важный момент - загрузка сдвигового регистра, когда мы при приеме команды WRITE или READ сразу загружаем данные в сдвиговый регистр.
CMD_WRITE: begin tx_shift_r <= {din_i, 1'b0}; // 8 бит данных + 0 (мастер ACK не нужен) cmd_r <= CMD_WRITE; bit_cnt_r <= 4'd0; end CMD_READ: begin tx_shift_r <= {8'hFF, din_i[0]}; // FF=отпустить SDA + бит NACK cmd_r <= CMD_READ; bit_cnt_r <= 4'd0; end
Можно было бы загружать tx_shift_r в фазе 0 первого бита ST_DATA, но это создало бы проблему: в фазе 0 мы уже должны выставить SDA из tx_shift_r[8], значит загрузка и чтение регистра должны произойти в один такт. Это возможно в Verilog (неблокирующее присваивание использует старое значение для чтения), но ведет к интуитивному коду и потенциальным ошибкам при модификации. Загрузка в IDLE (при приеме команды) гарантирует, что к моменту перехода в ST_DATA регистр уже содержит правильные данные. Чисто и безопасно.
Для WRITE: tx_shift_r = {D7,D6,D5,D4,D3,D2,D1,D0,0} - 8 бит данных, потом 0 (будем слушать ACK).
Для READ: tx_shift_r = {1,1,1,1,1,1,1,1, NACK} - 8 единиц (отпустить SDA, slave гонит данные), потом бит NACK/ACK который мастер пошлёт. Для READ загружаем 8’hFF потому что sda_oen_o = tx_shift_r[8], и oen = 1 означает “отпустить”. Загружая 1 в старшие 8 бит, мы гарантируем, что в бит-слотах 0-7 мастер отпустит SDA (slave будет управлять ей). А девятый бит (бит 0 регистра) - это значение ACK/NACK, которое мастер хочет послать: din_i[0] = 0 = ACK (продолжить чтение), din_i[0] = 1 = NACK (последний байт).
Проектирование каждого состояния. ST_START
В течение этого этапа SDA переходит из 1 в 0, пока SCL = 1. Тайминг по фазам выглядит следующим образом:

Раскрою суть данного этапа по фазам, логично задать вопрос - почему START занимает 4 фазы, а не две (сначала SDA в 0, затем SCL в 0):
Фаза 0 - гарантия начальных условий. Перед START обе линии должны быть в 1. Если это самый первый START (шина свободна), это выполняется по умолчанию. Но спецификация требует выделить отдельный setup time (tBUF) после предыдущего STOP. Фаза 0 обеспечивает этот промежуток. Кроме того, здесь мы ждём clock stretching if (scl_i) - возможно, предыдущая операция оставила SCL в LOW.
Фаза 1 - hold. Дополнительное время гарантирует, что и SCL, и SDA стабильно в 1 перед тем, как мы положим SDA в 0. Без этого, при высоких частотах тактирования в FPGA, промежуток между отпусканием SCL (фаза 0) и падением SDA (фаза 2) мог бы быть слишком коротким для того чтобы быть однозначно воспринятым slave.
Фаза 2 - собственно START. SDA → 0 при SCL = 1. Это единственный ”полезный” такт.
Фаза 3 - подготовка. SCL → 0. Это необходимо, потому что после START следующая операция - передача данных. Данные передаются при SCL = 0. Если не опустить SCL, мастер начнет менять SDA (для первого бита) при SCL = 1, что slave интерпретирует как ещё один START или непонятную последовательность.
Реализация:
ST_START: begin case (phase_r) 2'd0: begin sda_oen_o <= 1'b1; // SDA отпущена (high) scl_oen_o <= 1'b1; // SCL отпущена (high) if (scl_i) phase_r <= 2'd1; // Ждём, пока SCL реально станет HIGH end 2'd1: begin sda_oen_o <= 1'b1; // SDA всё ещё high scl_oen_o <= 1'b1; // SCL high — hold time phase_r <= 2'd2; end 2'd2: begin sda_oen_o <= 1'b0; // SDA → LOW !!! ← ВОТ ОН, START scl_oen_o <= 1'b1; // SCL всё ещё high phase_r <= 2'd3; end 2'd3: begin sda_oen_o <= 1'b0; // SDA остаётся low scl_oen_o <= 1'b0; // SCL → LOW (подготовка к данным) state_r <= ST_IDLE; ready_o <= 1'b1; // Команда выполнена end endcase end
В фазе 0 мы проверяем scl_i для целей Clock stretching. Если предыдущая команда оставила SCL низким (slave держит), нужно дождаться, пока slave отпустит. scl_i - это реальный уровень на пине. Пока slave держит SCL = 0, мы стоим в фазе 0.
Проектирование каждого состояния. ST_DATA
Это самое сложное состояние. Оно обрабатывает 9 бит-слотов: 8 бит данных + 1 бит ACK. Каждый бит-слот проходит через 4 фазы. Каждый из 9 бит-слотов выполняет абсолютно одну и ту же последовательность действий (setup SDA → raise SCL → sample → lower SCL). Различие только в том, какой бит выдаётся/принимается (определяется bit_cnt_r и sda_input_mode). Создавать 9 отдельных состояний для одинаковой логики - это копипаста, увеличивающая FSM с 5 до 13 состояний без какой-либо пользы. Вместо этого мы используем двухуровневую вложенность: внешний уровень - bit_cnt_r (0…8), внутренний - phase_r (0…3).
Фаза 0: Setup SDA (SCL=LOW)
2'd0: begin scl_oen_o <= 1'b0; // SCL LOW — безопасно менять SDA if (sda_input_mode) sda_oen_o <= 1'b1; // Slave гонит → отпускаем SDA else sda_oen_o <= tx_shift_r[8]; // Мастер гонит → выдаём MSB phase_r <= 2'd1; end
Что такое tx_shift_r[8]? Это старший бит сдвигового регистра. При oen = 0 (бит = 0) мастер притягивает SDA к 0 (передаём 0). При oen = 1 (бит = 1) мастер отпускает SDA, pull-up подтягивает к 1 (передаём 1). По сути, sda_oen_o = tx_shift_r[8] - это XOR-инверсия: мы НЕ выдаем данные напрямую, а управляем тристейт-буфером.
oen = 1→ линия отпущена → pull-up → единица.oen = 0→ линия притянута → ноль.
Почему OEN = данные, а не OEN = ~данные? Это следствие использования open-drain: единственный способ выдать 0 - притянуть линию к земле (oen = 0, drive low). Единственный способ выдать 1 - отпустить линию (oen = 1, release). Таким образом, значение OEN совпадает с передаваемым битом: OEN = 1 → линия = 1, OEN = 0 → линия = 0. Инверсия не нужна. Это удобное совпадение, которое упрощает код.
Фаза 1. Release SCL, sample SDA
2'd1: begin scl_oen_o <= 1'b1; // Отпускаем SCL → HIGH (через pull-up) // SDA продолжаем удерживать как в фазе 0 if (scl_i) begin // SCL реально HIGH? (clock stretching!) rx_shift_r <= {rx_shift_r[7:0], sda_i}; // Семплируем SDA phase_r <= 2'd2; end // Если scl_i = 0 → slave держит SCL → стоим и ждём end
Критический момент: мы читаем sda_i только когда scl_i = 1. Спецификация I2C требует, чтобы данные были стабильны на SDA при SCL=1. Сэмплирование при SCL=0 бессмысленно.
Почему SDA семплируется безусловно (и для WRITE, и для READ)? Для READ это очевидно - мы принимаем данные. Но зачем сэмплировать SDA при WRITE? Потому что:
Бит 8 (ACK): при WRITE мастер отпускает SDA и slave отвечает ACK/NACK. Сэмплирование в бите 8 необходимо.
Арбитраж: даже когда мастер гонит данные (биты 0-7), он должен проверять, совпадает ли значение на SDA с тем, что он выдал. Если мастер выдал 1 (отпустил SDA), а на SDA оказался 0 - значит другой мастер тянет линию. Это детектируется отдельной логикой арбитража, но
rx_shift_rсобирает данные в любом случае.Единообразие: один и тот же путь данных для WRITE и READ упрощает код.
dout_oпосле WRITE будет содержать “мусор” (то, что мастер сам выдал), но вышестоящий контроллер просто игнорируетdout_oдля WRITE.
Фаза 2. Hold SCL HIGH
2'd2: begin scl_oen_o <= 1'b1; // SCL всё ещё HIGH // SDA удерживаем phase_r <= 2'd3; end
Зачем нужна эта фаза? Для соблюдения минимального времени высокого уровня SCL (tHIGH). Спецификация I2C требует tHIGH ≥ 4 мкс для Standard Mode. Две фазы SCL = 1 (фаза 1 + фаза 2) обеспечивают это. Не является ли фаза 2 “пустой”? По коду - да, тело фазы 2 почти пустое (держим SCL = 1, ничего не делаем). Но это не баг, а фича:
Фаза 2 обеспечивает 50% duty cycle SCL (2 фазы LOW + 2 фазы HIGH). Спецификация рекомендует, чтобы tHIGH и tLOW были примерно равны.
Фаза 2 дает дополнительный запас для медленных slave-устройств, которым нужно больше времени для стабилизации SDA.
Фаза 2 сохраняет единообразие: все состояния (START, DATA, STOP, RESTART) используют 4 фазы. Если убрать фазу 2 для DATA, нужно менять прескалер или вводить исключения.
Фаза 3. Pull SCL LOW, advance
2'd3: begin scl_oen_o <= 1'b0; // SCL → LOW if (bit_cnt_r == 4'd8) begin // Все 9 бит переданы (0..8) dout_o <= rx_shift_r[8:1]; // Принятый байт (биты 8..1) rx_ack_o <= rx_shift_r[0]; // ACK/NACK (бит 0 = последний принятый) state_r <= ST_IDLE; ready_o <= 1'b1; // Команда завершена end else begin bit_cnt_r <= bit_cnt_r + 1; tx_shift_r <= {tx_shift_r[7:0], 1'b0}; // Сдвиг влево phase_r <= 2'd0; // Следующий бит end end
Почему завершение байта определяется как bit_cnt_r == 4'd8, а не bit_cnt_r == 4'd9? Счетчик bit_cnt_r начинается с 0. Бит-слоты нумеруются 0, 1, 2, ..., 8. Фаза 3 бита N инкрементирует счетчик: после бита 7 счетчик становится 8, и мы переходим к фазе 0 бита 8 (ACK). После бита 8 мы проверяем bit_cnt_r == 8 и выходим. Это работает потому, что проверка происходит в фазе 3 бита 8 - когда ACK-бит уже полностью обработан (SDA семплирована в фазе 1 бита 8).
Еще один важный нюанс - SDA не изменяется в фазе 3. Заманчиво было бы начать устанавливать SDA для следующего бита прямо в фазе 3. Но это опасно: SCL переходит в 0 одновременно с изменением SDA, и в зависимости от задержек slave может увидеть это как ложный START или STOP. Безопасный подход в этом случае - менять SDA только в фазе 0, когда SCL уже гарантированно 0.
Почему сдвиг tx_shift_r происходит в фазе 3, а не в фазе 0? Сдвиг подготавливает следующий бит в позиции tx_shift_r[8]. В фазе 0 следующего бита мы уже читаем tx_shift_r[8] для управления SDA. Если бы мы сдвигали в фазе 0, то в том же такте пытались бы записать (сдвинуть) и прочитать (выдать) tx_shift_r[8]. В Verilog при неблокирующем присваивании это корректно (читается старое значение), но это нелогично: мы бы выдавали бит, который ещё не сдвинулся. При сдвиге в фазе 3 к моменту фазы 0 следующего бита tx_shift_r[8] уже содержит правильный новый бит.
tx_shift_r (9 бит): [8][7][6][5][4][3][2][1][0] ↑ Выдаётся на SDA
Каждый бит-слот: сдвиг влево, MSB уходит на SDA.
Для WRITE: изначально = {D7,D6,D5,D4,D3,D2,D1,D0,0}.
Бит 0: выдаёт D7, бит 1: D6, ... бит 7: D0, бит 8: 0 (release для ACK)
rx_shift_r (9 бит): [8][7][6][5][4][3][2][1][0] ↑ Принимается с SDA
Каждый бит-слот: сдвиг влево, LSB = sda_i
После 9 бит: rx_shift_r = {D7,D6,D5,D4,D3,D2,D1,D0, ACK}.
dout_o = rx_shift_r[8:1] = {D7..D0}
rx_ack_o = rx_shift_r[0] = ACK
Оба регистра READ и WRITE - 9 бит. Девятый бит-слот (ACK) обрабатывается точно так же, как биты данных - через тот же сдвиг. Для tx_shift_r бит 0 при WRITE загружается нулём: в бит-слоте 8 (ACK) мастер будет отпускать SDA (oen = tx_shift_r[8], к этому моменту бит 0 сдвинулся в позицию 8, и это 0 - мастер не управляет SDA, слушает ACK). Для READ бит 0 загружается значением din_i[0] - это ACK/NACK, который мастер хочет послать. Единый 9-битный регистр и единый 4-фазный цикл для всех 9 бит-слотов - это ключ к простоте ST_DATA. Никаких специальных случаев для ACK-бита в FSM.
Проектирование каждого состояния. ST_STOP
Определение STOP - SDA переходит из 0 в 1, пока SCL = 1.

Реализация:
ST_STOP: begin case (phase_r) 2'd0: begin sda_oen_o <= 1'b0; // SDA = LOW (подготовка) scl_oen_o <= 1'b0; // SCL = LOW phase_r <= 2'd1; end 2'd1: begin sda_oen_o <= 1'b0; // SDA остаётся LOW scl_oen_o <= 1'b1; // Отпускаем SCL → HIGH if (scl_i) phase_r <= 2'd2; // Clock stretching end 2'd2: begin sda_oen_o <= 1'b1; // SDA → HIGH !!! ← ВОТ ОН, STOP scl_oen_o <= 1'b1; // SCL остаётся HIGH phase_r <= 2'd3; end 2'd3: begin // Hold — обе линии высокие, шина свободна state_r <= ST_IDLE; ready_o <= 1'b1; end endcase end
Фаза 0 обязательно необходима. После предыдущей команды (WRITE/READ) SCL = 0, но SDA может быть в любом состоянии. Нам нужно гарантировать SDA = 0 перед STOP, иначе переход SDA 0→1 при SCL=1 не произойдёт. Фаза 0 устанавливает начальное условие.
Рассмотрим ситуацию: предыдущая команда - WRITE, последний бит данных = 1. После фазы 3 ST_DATA: SCL = 0, SDA = 1 (мастер отпустил). Если сразу перейти к фазе 1 STOP (поднять SCL), мы получим: SCL = 1, SDA = 1 → SDA уже 1, и мы не можем сделать переход 0 → 1. Более того, если в этот момент SDA изменится (slave отпустит ACK), slave может увидеть STOP раньше времени. Фаза 0 решает это: принудительно устанавливаем SDA = 0 при SCL = 0. Теперь в фазе 2, когда мы отпускаем SDA при SCL=1, переход 0→1 гарантирован. Можно было бы в ST_DATA фазе 3 принудительно опускать SDA (при cmd_r == CMD_WRITE). Но это “просачивание знания” о следующей команде в текущее состояние, что нарушает модульность. ST_DATA не должно знать, что после него будет STOP.
Проектирование каждого состояния. ST_RESTART
RESTART - это START, который выполняется без предварительного STOP.

При обычном START мы начинаем с SDA=1, SCL=1 (шина свободна). При RESTART мы начинаем с SCL=0 (мастер удерживает после предыдущей передачи данных), SDA может быть в любом состоянии. Ключевое отличие:
ST_START фаза 0: и SCL, и SDA уже 1, нужно только дождаться SCL (stretching).
ST_RESTART фаза 0: SCL = 0, нужно сначала отпустить SDA (HIGH), и только потом поднимать SCL.
Если бы мы использовали ST_START для RESTART, в фазе 0 мы бы отпустили SCL (scl_oen = 1). Но SDA может быть 0. Получается: 1 при SDA=0 → slave видит “данные”, а не подготовку к START. Потом SDA→1 при SCL=1 → slave видит STOP! Не START. Именно поэтому RESTART имеет другой порядок: сначала SDA→1 при SCL=0 (фаза 0), потом SCL→1 при SDA=1 (фаза 1), потом SDA→0 при SCL=1 (фаза 2) - это правильный START.
Порядок в фазе 0 критичен: сначала отпускаем SDA (фаза 0), и только потом SCL (фаза 1). Если отпустить SCL раньше SDA, при SCL=1 и SDA 0→1 получится ложный STOP.
ST_RESTART: begin case (phase_r) 2'd0: begin sda_oen_o <= 1'b1; // Отпустить SDA → 1 scl_oen_o <= 1'b0; // SCL остаётся 0 phase_r <= 2'd1; end 2'd1: begin sda_oen_o <= 1'b1; // SDA 1 scl_oen_o <= 1'b1; // Отпускаем SCL → 1 if (scl_i) phase_r <= 2'd2; // Clock stretching end 2'd2: begin sda_oen_o <= 1'b0; // SDA → 0 !!! ← START scl_oen_o <= 1'b1; // SCL 1 phase_r <= 2'd3; end 2'd3: begin sda_oen_o <= 1'b0; // SDA 0 scl_oen_o <= 1'b0; // SCL → 0 state_r <= ST_IDLE; ready_o <= 1'b1; end endcase end
Мониторинг шины. Отслеживаем занятость (busy)
Мониторинг - это “наблюдательная” логика, которая не влияет на основной путь данных. Она отслеживает побочные эффекты (чужие START/STOP на шине, конфликты SDA) и устанавливает флаги. Основной FSM можно полностью отладить без мониторинга (в single-master конфигурации без stretching), а потом добавить мониторинг поверх.
Но тем не менее, ядро должно знать, занята ли шина. Это нужно для:
Определения, какую команду выдать: START (шина свободна) или RESTART (шина занята).
Управления линиями в IDLE (отпускать или держать)
Будем детектировать START и STOP по реальным сигналам на шине:
// --------------------------------------------------------------- // SDA/SCL edge detection — for bus monitoring // --------------------------------------------------------------- reg sda_d_r; always @(posedge clk_i or negedge rstn_i) begin if (!rstn_i) sda_d_r <= 1'b1; else sda_d_r <= sda_i; end wire sda_rising = sda_i & ~sda_d_r; // SDA: 0 → 1 wire sda_falling = ~sda_i & sda_d_r; // SDA: 1 → 0 // --------------------------------------------------------------- // Bus BUSY tracking (any START/STOP on the bus) // --------------------------------------------------------------- // START = SDA падает при SCL=1 wire start_on_bus = sda_falling & scl_i; // STOP = SDA растёт при SCL=1 wire stop_on_bus = sda_rising & scl_i; // Флаг занятости always @(posedge clk_i or negedge rstn_i) begin if (!rstn_i) busy_o <= 1'b0; else if (start_on_bus) busy_o <= 1'b1; else if (stop_on_bus) busy_o <= 1'b0; end
Данная часть функциональности работает на основном клоке clk_i, а не на ena_i. Фронты SDA/SCL могут быть очень короткими (наносекунды), и мы не должны их пропустить. Сигнал ena_i приходит раз в ~125 тактов (при 100 кГц SCL). Фронт SDA при SCL=1 (START или STOP) длится ≈ один период ena_i (один четвертьпериод SCL). Если детектор работает на ena_i, он может пропустить короткий фронт: например, если START произошёл ровно между двумя импульсами ena_i. Детектирование на clk_i гарантирует, что мы увидим любой фронт SDA длительностью ≥ 1 период clk_i (20 нс при 50 МГц).
Можно было бы сделать отдельный асинхронный детектор на edge-triggered latch, но асинхронная логика плохо синтезируется на FPGA (нет гарантии метастабильности, зависимость от routing delays). Синхронный детектор фронтов на основном клоке - стандартный и надёжный паттерн.
Почему busy_o отслеживает START/STOP на шине, а не просто является флагом FSM? Можно было бы установить busy_o = 1 при входе в ST_START и сбросить при выходе из ST_STOP. Но тогда busy_o отражал бы состояние нашего мастера, а не шины. В multi-master конфигурации другой мастер может генерировать START/STOP, и мы должны это видеть. Кроме того, если мы потеряли арбитраж и вышли в IDLE, но другой мастер продолжает транзакцию - шина всё ещё busy, и мы не должны отпускать линии.
Мониторинг шины. Обнаружение потери арбитража
При обстоятельном рассмотрении ситуаций, в которых может оказаться разрабатываемый нами контроллер, из общего списка можно выделить ситуацию потери арбитража шины при подключении нескольких Master-устройств. Наш Master теряет арбитраж когда он отпустил SDA (ожидает, что линия будет 1), но другой мастер или slave тянет SDA к 0.
Эту ситуацию нужно детектировать в двух ситуация. Первая - во время передачи данных ST_DATA, в фазе 1:
// --------------------------------------------------------------- // Arbitration lost detection // --------------------------------------------------------------- // We lose arbitration when we release SDA (expect high) but read low // while SCL is high. Checked in DATA phase-1 (SCL just went high) // and during START/RESTART when SDA should be high. wire al_data = (state_r == ST_DATA) && (phase_r == 2'd1) && // SCL только поднялся scl_i && // SCL реально HIGH !sda_input_mode && // Мастер должен управлять SDA sda_oen_o && // Мастер отпустил SDA (ожидает 1) !sda_i; // Но SDA = 0! Кто-то тянет!
Проверка происходит именно в фазе 1 - это момент, когда SCL только что стал 1, и данные на SDA стабильны. Это единственный момент, когда проверка арбитража имеет смысл: мастер выдал бит (в фазе 0), поднял SCL (в фазе 1), и теперь может сравнить реальное значение SDA с ожидаемым. При этом необходимо проверять !sda_input_mode т.к. если в данный бит-слот данные гонит slave (READ биты 0-7 или WRITE ACK), то низкий уровень SDA - это нормальная передача от slave, а не конфликт арбитража. Помимо этого, необходимо еще одно условие - sda_oen_o == 1. Мастер проверяет арбитраж только когда он отпустил SDA (выдаёт “1”). Если мастер сам притянул SDA к 0 (sda_oen_o = 0), то SDA = 0 - это его собственная работа, конфликта нет. Потеря арбитража возможна только при передаче бита “1”: мастер отпустил линию, ожидая HIGH, но другой мастер тянет к LOW.
Вторая ситуация - во время команд START/RESTART.
wire al_start = (state_r == ST_START && phase_r == 2'd0 && sda_oen_o && scl_i && !sda_i) || (state_r == ST_RESTART && phase_r == 2'd1 && sda_oen_o && scl_i && !sda_i);
Теоретически два мастера могут одновременно начать генерацию START. В фазе 0 ST_START оба отпускают SDA и SCL. Если один из них увидит SDA=0 при ожидании SDA=1, значит другой мастер уже опустил SDA - уже «начал» START раньше. Поэтому первый мастер должен уступить.
Внимательные читатели могут заметить, что при RESTART проверка происходит в фазе 1, а не в фазе 0. В фазе 0 RESTART SCL=0 (мастер ещё не отпустил). При SCL=0 изменение SDA не является START/STOP, и арбитраж по SDA не имеет смысла. В фазе 1 мастер отпустил и SCL, и SDA - вот теперь можно проверять.
Сделаем блок реакции на потерю арбитража, для подачи сигнала на немедленное освобождение шины:
wire al_event = al_data | al_start; always @(posedge clk_i or negedge rstn_i) begin if (!rstn_i) arb_lost_o <= 1'b0; else if (arb_lost_clear_i) arb_lost_o <= 1'b0; else if (ena_i && al_event) arb_lost_o <= 1'b1; end
И в Main FSM сделаем первым шагом:
// --- arbitration lost: release bus immediately --- if (al_event && state_r != ST_IDLE) begin state_r <= ST_IDLE; scl_oen_o <= 1'b1; // Отпустить SCL sda_oen_o <= 1'b1; // Отпустить SDA ready_o <= 1'b1; // Сообщить контроллеру end
Согласно спецификации I2C при потере арбитража мастер должен немедленно прекратить управление шиной. Продолжение передачи даже одного бита может помешать выигравшему мастеру. Поэтому мы безусловно отпускаем обе линии и возвращаемся в IDLE, не дожидаясь окончания текущей фазы.
Заведем флаг arb_lost_o, который будет т.н. sticky (липкий ?), то есть устанавливается при потере арбитража и не сбрасывается автоматически. Вышестоящий контроллер должен явно сбросить его через arb_lost_clear_i перед новой попыткой. Если бы arb_lost_o сбрасывался автоматически (скажем, при следующем ena_i), вышестоящий контроллер мог бы не успеть его прочитать - и начать новую транзакцию, не зная об ошибке. Sticky-флаг гарантирует, что информация о потере арбитража не будет потеряна, пока контроллер её не обработает.
Clock Stretching
Clock stretching - это очень полезный механизм, позволяющий slave (или другому мастеру) замедлить шину. Slave удерживает SCL в 0, когда ему нужно больше времени, например на проведение внутренней транзакции записи данных:
Мастер отпускает SCL: scl_oen_o <= 1'b1; Ожидает SCL = HIGH: if (scl_i) ... Slave держит SCL LOW: scl_i = 0 → мастер ждёт Slave отпускает: scl_i = 1 → мастер продолжает
Спецификация I2C требует от мастера поддержки clock stretching. Многие реальные устройства (EEPROM при page write, сложные датчики) активно используют stretching для сигнализации “не готов”. Мастер без поддержки stretching нарушит тайминг при работе с такими устройствами и получит поврежденные данные. Спецификация I2C не определяет максимальное время stretching - теоретически slave может держать SCL сколь угодно долго. Однако SMBus (расширение I2C) определяет таймаут 35 мс. Мы не реализуем таймаут в ядре, потому что:
Ядро - универсальное. Не все применения используют SMBus.
Таймаут - это политика, а не протокол. Решение «что делать при зависании» (reset шины? перезапуск? уведомить CPU?) зависит от системы.
Таймаут тривиально реализуется в обёртке (AXI/Avalon/Top-level слой) поверх ядра.
Обработку Clock stretching поместим в фазе 1 каждого состояния - там, где мастер отпускает SCL и ожидает, что он поднимется:
// ST_START, фаза 0: if (scl_i) phase_r <= 2'd1; // Ждём SCL HIGH // ST_DATA, фаза 1: if (scl_i) begin // Ждём SCL HIGH rx_shift_r <= {rx_shift_r[7:0], sda_i}; phase_r <= 2'd2; end // ST_STOP, фаза 1: if (scl_i) phase_r <= 2'd2; // Ждём SCL HIGH // ST_RESTART, фаза 1: if (scl_i) phase_r <= 2'd2; // Ждём SCL HIGH
Паттерн здесь один: if (scl_i) продвигаемся ; else ждём.
Отдельно отмечу, что Clock Stretching в ST_START происходит в фазе 0, а не в фазе 1. В ST_START фаза 0 - это момент, когда мы отпускаем SCL (scl_oen = 1) и ждём его реального подъема. Это аналогично фазе 1 в других состояниях. Нумерация фаз смещена из-за того, что последовательность для START отличается: нам нужно дождаться SCL=1 до того, как мы будем менять SDA. В ST_DATA/ST_STOP/ST_RESTART мы сначала устанавливаем SDA (фаза 0), а потом поднимаем SCL (фаза 1). В ST_START мы сразу начинаем с высокого SCL (фаза 0).
Итоговый промежуточный результат
По итогу получился следующий код:
Полный код полученного модуля i2c_master_core
`timescale 1ns / 1ps // --------------------------------------------------------------------------- // I2C Master Core — bit/byte-level I2C master controller // // Atomic commands: START, WRITE (byte), READ (byte), STOP, RESTART // Each byte = 9 bit-slots (8 data + 1 ACK/NACK). // Each bit-slot = 4 clock-enable phases (quarter-SCL periods). // Clock stretching supported: waits for SCL release in phases where SCL // is expected high. // Arbitration lost detection: monitors SDA when master expects it high. // Open-drain outputs: oen=1 → release (high-Z pulled up), oen=0 → drive low. // --------------------------------------------------------------------------- module i2c_master_core ( input wire clk_i, input wire rstn_i, input wire ena_i, // 1-tick pulse per quarter-SCL period // Command interface (active when ready_o == 1) input wire cmd_valid_i, // Level: held high until accepted input wire [2:0] cmd_i, // Command code input wire [7:0] din_i, // TX data (WRITE) / {7'bx, NACK} (READ) output reg [7:0] dout_o, // RX data (valid after READ completes) output reg rx_ack_o, // ACK received from slave (0=ACK,1=NACK) output reg ready_o, // Ready to accept next command // Status output reg arb_lost_o, // Arbitration lost (sticky, clear via _clear_i) input wire arb_lost_clear_i, // Pulse to clear arb_lost_o output reg busy_o, // I2C bus busy (START seen, no STOP yet) // I2C pad interface — directly to tri-state buffers input wire scl_i, // SCL pad input (synchronised externally) output reg scl_oen_o, // SCL output-enable: 1=release, 0=drive low input wire sda_i, // SDA pad input (synchronised externally) output reg sda_oen_o // SDA output-enable: 1=release, 0=drive low ); // --------------------------------------------------------------- // Command encoding // --------------------------------------------------------------- localparam [2:0] CMD_NOP = 3'd0, CMD_START = 3'd1, CMD_WRITE = 3'd2, CMD_READ = 3'd3, CMD_STOP = 3'd4, CMD_RESTART = 3'd5; // --------------------------------------------------------------- // FSM states (high-level) + phase counter (0-3 per operation) // --------------------------------------------------------------- localparam [2:0] ST_IDLE = 3'd0, ST_START = 3'd1, ST_DATA = 3'd2, ST_STOP = 3'd3, ST_RESTART = 3'd4; reg [2:0] state_r; reg [1:0] phase_r; reg [2:0] cmd_r; // Latched command (WRITE / READ) for data transfer reg [3:0] bit_cnt_r; // 0..8 (9 bit-slots per byte) reg [8:0] tx_shift_r; // TX shift register {data[7]…data[0], ack_ctl} reg [8:0] rx_shift_r; // RX shift register // --------------------------------------------------------------- // SDA/SCL edge detection — for bus monitoring // --------------------------------------------------------------- reg sda_d_r; always @(posedge clk_i or negedge rstn_i) begin if (!rstn_i) sda_d_r <= 1'b1; else sda_d_r <= sda_i; end wire sda_rising = sda_i & ~sda_d_r; wire sda_falling = ~sda_i & sda_d_r; // --------------------------------------------------------------- // Bus BUSY tracking (any START/STOP on the bus) // --------------------------------------------------------------- wire start_on_bus = sda_falling & scl_i; wire stop_on_bus = sda_rising & scl_i; always @(posedge clk_i or negedge rstn_i) begin if (!rstn_i) busy_o <= 1'b0; else if (start_on_bus) busy_o <= 1'b1; else if (stop_on_bus) busy_o <= 1'b0; end // --------------------------------------------------------------- // SDA direction helpers // --------------------------------------------------------------- // sda_input_mode = 1 when the *slave* should be driving SDA: // READ bits 0-7 — slave sends data // WRITE bit 8 — slave sends ACK/NACK wire sda_input_mode = (state_r == ST_DATA) && ( (cmd_r == CMD_READ && bit_cnt_r < 4'd8) || (cmd_r == CMD_WRITE && bit_cnt_r == 4'd8) ); // --------------------------------------------------------------- // Arbitration lost detection // --------------------------------------------------------------- // We lose arbitration when we release SDA (expect high) but read low // while SCL is high. Checked in DATA phase-1 (SCL just went high) // and during START/RESTART when SDA should be high. wire al_data = (state_r == ST_DATA) && (phase_r == 2'd1) && scl_i && !sda_input_mode && sda_oen_o && !sda_i; wire al_start = (state_r == ST_START && phase_r == 2'd0 && sda_oen_o && scl_i && !sda_i) || (state_r == ST_RESTART && phase_r == 2'd1 && sda_oen_o && scl_i && !sda_i); wire al_event = al_data | al_start; always @(posedge clk_i or negedge rstn_i) begin if (!rstn_i) arb_lost_o <= 1'b0; else if (arb_lost_clear_i) arb_lost_o <= 1'b0; else if (ena_i && al_event) arb_lost_o <= 1'b1; end // --------------------------------------------------------------- // Main FSM // --------------------------------------------------------------- always @(posedge clk_i or negedge rstn_i) begin if (!rstn_i) begin state_r <= ST_IDLE; phase_r <= 2'd0; cmd_r <= CMD_NOP; bit_cnt_r <= 4'd0; tx_shift_r <= 9'd0; rx_shift_r <= 9'd0; scl_oen_o <= 1'b1; sda_oen_o <= 1'b1; ready_o <= 1'b1; dout_o <= 8'd0; rx_ack_o <= 1'b0; end else if (ena_i) begin // --- arbitration lost: release bus immediately --- if (al_event && state_r != ST_IDLE) begin state_r <= ST_IDLE; phase_r <= 2'd0; scl_oen_o <= 1'b1; sda_oen_o <= 1'b1; ready_o <= 1'b1; end else begin case (state_r) // ======================================================= // IDLE — wait for a command // ======================================================= ST_IDLE: begin if (cmd_valid_i && !arb_lost_o) begin ready_o <= 1'b0; case (cmd_i) CMD_START: begin state_r <= ST_START; phase_r <= 2'd0; end CMD_WRITE: begin state_r <= ST_DATA; phase_r <= 2'd0; bit_cnt_r <= 4'd0; cmd_r <= CMD_WRITE; tx_shift_r <= {din_i, 1'b0}; end CMD_READ: begin state_r <= ST_DATA; phase_r <= 2'd0; bit_cnt_r <= 4'd0; cmd_r <= CMD_READ; tx_shift_r <= {8'hFF, din_i[0]}; end CMD_STOP: begin state_r <= ST_STOP; phase_r <= 2'd0; end CMD_RESTART: begin state_r <= ST_RESTART; phase_r <= 2'd0; end default: ready_o <= 1'b1; endcase end else if (!busy_o) begin // Bus not busy (after STOP or at power-up) — release scl_oen_o <= 1'b1; sda_oen_o <= 1'b1; end // If busy but no command pending: hold SCL/SDA as-is end // ======================================================= // START condition: SDA 1→0 while SCL=1 // Phase 0 : SDA=1, SCL=1 — wait for SCL high (stretching) // Phase 1 : SDA=1, SCL=1 — hold // Phase 2 : SDA=0, SCL=1 — START // Phase 3 : SDA=0, SCL=0 — done // ======================================================= ST_START: begin case (phase_r) 2'd0: begin sda_oen_o <= 1'b1; scl_oen_o <= 1'b1; if (scl_i) phase_r <= 2'd1; end 2'd1: begin sda_oen_o <= 1'b1; scl_oen_o <= 1'b1; phase_r <= 2'd2; end 2'd2: begin sda_oen_o <= 1'b0; // SDA LOW — START condition scl_oen_o <= 1'b1; phase_r <= 2'd3; end 2'd3: begin sda_oen_o <= 1'b0; scl_oen_o <= 1'b0; // SCL LOW state_r <= ST_IDLE; ready_o <= 1'b1; end endcase end // ======================================================= // DATA — 9 bit-slots (8 data + 1 ACK/NACK), 4 phases each // Phase 0 : SCL=0, setup SDA // Phase 1 : SCL=1, sample SDA (wait for stretching) // Phase 2 : SCL=1, hold // Phase 3 : SCL=0, advance bit counter // ======================================================= ST_DATA: begin case (phase_r) 2'd0: begin scl_oen_o <= 1'b0; if (sda_input_mode) sda_oen_o <= 1'b1; else sda_oen_o <= tx_shift_r[8]; phase_r <= 2'd1; end 2'd1: begin scl_oen_o <= 1'b1; // Release SCL if (sda_input_mode) sda_oen_o <= 1'b1; else sda_oen_o <= tx_shift_r[8]; if (scl_i) begin // SCL actually high (stretching done) rx_shift_r <= {rx_shift_r[7:0], sda_i}; phase_r <= 2'd2; end end 2'd2: begin scl_oen_o <= 1'b1; if (sda_input_mode) sda_oen_o <= 1'b1; else sda_oen_o <= tx_shift_r[8]; phase_r <= 2'd3; end 2'd3: begin scl_oen_o <= 1'b0; // SCL LOW // SDA must NOT change simultaneously with SCL // to avoid spurious START/STOP on the bus. // Phase 0 of the next bit handles SDA setup. if (bit_cnt_r == 4'd8) begin dout_o <= rx_shift_r[8:1]; rx_ack_o <= rx_shift_r[0]; state_r <= ST_IDLE; ready_o <= 1'b1; end else begin bit_cnt_r <= bit_cnt_r + 4'd1; tx_shift_r <= {tx_shift_r[7:0], 1'b0}; phase_r <= 2'd0; end end endcase end // ======================================================= // STOP condition: SDA 0→1 while SCL=1 // Phase 0 : SDA=0, SCL=0 // Phase 1 : SDA=0, SCL=1 — wait for stretching // Phase 2 : SDA=1, SCL=1 — STOP // Phase 3 : hold — done // ======================================================= ST_STOP: begin case (phase_r) 2'd0: begin sda_oen_o <= 1'b0; scl_oen_o <= 1'b0; phase_r <= 2'd1; end 2'd1: begin sda_oen_o <= 1'b0; scl_oen_o <= 1'b1; if (scl_i) phase_r <= 2'd2; end 2'd2: begin sda_oen_o <= 1'b1; // SDA HIGH — STOP condition scl_oen_o <= 1'b1; phase_r <= 2'd3; end 2'd3: begin sda_oen_o <= 1'b1; scl_oen_o <= 1'b1; state_r <= ST_IDLE; ready_o <= 1'b1; end endcase end // ======================================================= // RESTART — repeated START // Phase 0 : SDA=1, SCL=0 — release SDA first // Phase 1 : SDA=1, SCL=1 — wait for stretching // Phase 2 : SDA=0, SCL=1 — START condition // Phase 3 : SDA=0, SCL=0 — done // ======================================================= ST_RESTART: begin case (phase_r) 2'd0: begin sda_oen_o <= 1'b1; scl_oen_o <= 1'b0; phase_r <= 2'd1; end 2'd1: begin sda_oen_o <= 1'b1; scl_oen_o <= 1'b1; if (scl_i) phase_r <= 2'd2; end 2'd2: begin sda_oen_o <= 1'b0; // SDA LOW — START condition scl_oen_o <= 1'b1; phase_r <= 2'd3; end 2'd3: begin sda_oen_o <= 1'b0; scl_oen_o <= 1'b0; state_r <= ST_IDLE; ready_o <= 1'b1; end endcase end default: begin state_r <= ST_IDLE; scl_oen_o <= 1'b1; sda_oen_o <= 1'b1; ready_o <= 1'b1; end endcase end // else (not al_event) end // ena_i end endmodule
Вносим код в Top-level файл проекта и запускаем компиляцию. После компиляции видим, что код получился достаточно компактным:

Не обращайте внимания на занятые пины - сигналы модуля были автоматом раскинуты на подходящие пины ПЛИС.
В качестве заключения
Кажется пришло на этом этапе завершить статью, чтобы она не превращалась в огромный неперевариваемый лонгрид. Подводя итог могу сказать, что мы реализовали ядро контроллера, который в том числе корректно обрабатывает большинство пограничных ситуаций.
В следующей статье мы протестируем его и я опишу то, каким образом это можно сделать и какими инструментами можно будет воспользоваться.
До встречи в следующей статье =)
Размещайте облачную инфраструктуру и масштабируйте сервисы с надежным облачным провайдером Beget.
Эксклюзивно для читателей Хабра мы даем бонус 10% при первом пополнении.
