惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Cisco Blogs
爱范儿
爱范儿
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
Jina AI
Jina AI
Project Zero
Project Zero
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
Simon Willison's Weblog
Simon Willison's Weblog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tenable Blog
F
Fortinet All Blogs
大猫的无限游戏
大猫的无限游戏
Last Week in AI
Last Week in AI
月光博客
月光博客
雷峰网
雷峰网
G
Google Developers Blog
V
V2EX
T
Tor Project blog
罗磊的独立博客
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
W
WeLiveSecurity
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy International News Feed
S
Securelist
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Proofpoint News Feed
Blog — PlanetScale
Blog — PlanetScale
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
小众软件
小众软件
Scott Helme
Scott Helme
I
Intezer
T
Threat Research - Cisco Blogs
The GitHub Blog
The GitHub Blog
N
Netflix TechBlog - Medium
C
CERT Recently Published Vulnerability Notes
Security Archives - TechRepublic
Security Archives - TechRepublic
酷 壳 – CoolShell
酷 壳 – CoolShell
L
LINUX DO - 最新话题
N
News | PayPal Newsroom
L
Lohrmann on Cybersecurity
T
Troy Hunt's Blog
Google DeepMind News
Google DeepMind News
P
Proofpoint News Feed
人人都是产品经理
人人都是产品经理
Latest news
Latest news
AWS News Blog
AWS News Blog
Apple Machine Learning Research
Apple Machine Learning Research

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Создаем I2C Master Controller на Verilog. Создаем контроллер ядра I2C
andreyzaostr · 2026-05-14 · via Все публикации подряд на Хабре

Я продолжаю описывать создание I2C-контроллера на Verilog. В предыдущих статьях мы протестировали ядро контроллера который выполняет атомарные функции работы с шиной в т.ч. в пограничных ситуациях типа clock stretching и пр. Теперь необходимо разработать управляющий контроллер для этого ядра, чтобы выполнять необходимые нам функции, но уже на следующем уровне абстракции и стать на шаг ближе к нашей цели - к рабочему коду I2C Controller который мы будем использовать с EEPROM, а в последующих статьях все это переиспользуем в Zynq и подключим к Linux.

Всем заинтересовавшимся - добро пожаловать под кат!

Дисклеймер. Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи — рассказать о своем опыте. Я не являюсь профессиональным разработчиком под ПЛИС на языке Verilog и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется. Что ж, поехали…

На чем мы там остановились…

На первом шаге проекта мы написали i2c_master_core - ядро I2C мастер-контроллера - и проверили его в симуляции. Десять тестов в Icarus Verilog подтвердили: FSM переключает состояния правильно, биты на шине формируются корректно, ACK/NACK обрабатываются, clock stretching и арбитраж работают. Казалось бы, дело сделано.

Но симуляция - это модель. Реальная микросхема FPGA, реальная шина I2C с физическими проводниками, реальная микросхема EEPROM - всё это вносит факторы, которых не было в симуляции: задержки распространения сигналов, ёмкость шины, реальные pull-up резисторы, электрические помехи, нюансы тайминга конкретной EEPROM. Единственный способ убедиться, что контроллер работает на железе - загрузить его в FPGA и подключить к настоящему I2C устройству.

Для этого нужна тестовая оболочка (test shell) - небольшой проект, который оборачивает голое ядро i2c_master_core во всё необходимое для работы на конкретной плате: генератор тактирования, управление кнопками, индикация результатов на дисплее и светодиодах. Тестовая оболочка в данном случае не является финальной точкой, это будет очередным инструметом разработчика и демонстрационным стендом для проверки и подтверждения что ядро действительно работает на железе.

Проверять будем на плате Alinx AX301 - это учебная плата с ПЛИС на базе Intel (Altera) Cyclone IV EP4CE6F17C8. Это одна из самых доступных и распространённых плат для обучения FPGA-разработке, обзор на нее я делал в одной из своих прошлых статей. И на борту есть всё необходимое для нашего теста.

Что понадобится на плате, перечислю списком:

  • Cyclone IV FPGA. Там будет вся основная логика, без комментариев;

  • 50 МГц кварцевый осциллятор. Системное тактирование, будет использован в качестве источника клока для всех модулей;

  • 24LC04 EEPROM. Используется в качестве цели для тестирования, как I2C slave-устройства, записываем и читаем байты;

  • 7-сегментный дисплей из 6 разрядов. Отображение результатов тестов, с номером, pass/fail. Два из семи разрядов не используется, потому что они подключены к тем же выводам что и EEPROM.

  • 4 светодиода будут использоваться в качестве индикаторов успешных тестов, дублирует статус.

  • Кнопка KEY1 будет использоваться для запуска и перезапуска тестов;

  • Кнопка RESET будет использоваться для аппаратного сброса, для возврата всего в начальное состояние.

Помимо этого нужен USB Blaster (JTAG) для загрузки прошивки. 

Теперь чуть подробнее взглянем на slave-устройство EEPROM 24LC04. Это энергонезависимая память на 512 байт (4 Кбит) от Microchip с I2C интерфейсом. 

Ключевые характеристики:

  • Объем памяти 512 байт (2 блока по 256 байт);

  • Адрес устройства - 0xA0 для записи, 0xA1 для чтения;

  • Максимальная частота SCL - 400 кГц;

  • Время записи одного байта - до 5 мс;

  • Напряжение питания 2.5-5.5 V;

Особенность 24LC04 - цикл записи. После того как мастер отправил STOP, EEPROM внутренне записывает данные в ячейку. Этот процесс занимает до 5 мс, и в течение этого времени микросхема не отвечает на I2C запросы (NACK). Наш тестовый контроллер учитывает это: после записи он выжидает 6 мс перед чтением.

Протокол обмена с 24LC04 для записи одного байта:

Протокол для чтения одного байта (random read):

Обратите внимание: для чтения из определённого адреса нужно сначала “установить указатель” записью адреса ячейки, а затем выполнить переключение направления через RESTART и повторную адресацию с битом чтения (0xA1 вместо 0xA0). 

Общая архитектура тестовой оболочки

Тестовая оболочка состоит из пяти модулей. Каждый решает свою задачу и общается с остальными через чётко определённые интерфейсы:

Поток данных, то есть как информация проходит через систему при одном нажатии кнопки:

Разберем код

Теперь разберем внутреннюю механику из которой построен проект. Итоговая иерархия модулей выглядит следующим образом:

i2c_test_top                           ← top-level
├── прескалер (встроен в top)          ← генерация ena для ядра
├── ax_debounce                        ← подавление дребезга кнопки
├── i2c_master_core                    ← I2C ядро (из rtl/)
├── i2c_test_ctrl                      ← FSM управления тестами
└── seg_scan                           ← мультиплексор дисплея

В проекте top-level файл - i2c_test_top.v. Он не содержит собственной логики обработки данных, а выполняет роль “скелета”: объявляет внешние порты FPGA, создает прескалер и соединяет четыре подмодуля между собой. Разберем каждый блок.

Объявляем модуль и порты:

module i2c_test_top (
    input  wire       clk,         // 50 MHz
    input  wire       rst_n,       // Active-low reset (active-low button)
    input  wire       key1,        // Start button (active-low, active = pressed)
    output wire [3:0] led,         // 4 LEDs
    output wire [5:0] seg_sel,     // 7-segment digit select (active-low)
    output wire [7:0] seg_data,    // 7-segment data (active-low, bit 7 = DP)
    inout  wire       i2c_sda,     // I2C data (open-drain)
    inout  wire       i2c_scl      // I2C clock (open-drain)
);

endmodule

Каждый порт в этом списке соответствует физическому пину (или группе пинов) FPGA, привязанному через .qsf-файл, который мы сформируем в конце статьи когда будем создавать проект:

# Clock 50 MHz
set_location_assignment PIN_E1  -to clk

# Reset (active-low push button)
set_location_assignment PIN_N13 -to rst_n

# Start button (active-low)
set_location_assignment PIN_M15 -to key1

# LEDs
set_location_assignment PIN_E10 -to led[0]
set_location_assignment PIN_F9  -to led[1]
set_location_assignment PIN_C9  -to led[2]
set_location_assignment PIN_D9  -to led[3]

# I2C bus (24LC04 EEPROM)
set_location_assignment PIN_D1  -to i2c_scl
set_location_assignment PIN_E6  -to i2c_sda

# 7-segment display: digit select (active-low, 6 digits)
set_location_assignment PIN_M11 -to seg_sel[0]
set_location_assignment PIN_P11 -to seg_sel[1]
set_location_assignment PIN_N11 -to seg_sel[2]
set_location_assignment PIN_M10 -to seg_sel[3]
set_location_assignment PIN_P9 -to seg_sel[4]
set_location_assignment PIN_N9 -to seg_sel[5]

# 7-segment display: segment data (active-low, bit 7 = DP)
set_location_assignment PIN_R14 -to seg_data[0]
set_location_assignment PIN_N16 -to seg_data[1]
set_location_assignment PIN_P16 -to seg_data[2]
set_location_assignment PIN_T15 -to seg_data[3]
set_location_assignment PIN_P15 -to seg_data[4]
set_location_assignment PIN_N12 -to seg_data[5]
set_location_assignment PIN_N15 -to seg_data[6]
set_location_assignment PIN_R16 -to seg_data[7]

Ключевое слово inout для I2C-линий означает двунаправленный порт. В Quartus синтезатор превращает его в триаду: входной буфер, выходной буфер и сигнал output-enable. Для input/output портов направление фиксировано, и синтезатор конфигурирует IO-ячейку FPGA соответственно.

Все выходы объявлены как wire (не reg), потому что они управляются подмодулями через непрерывные соединения, а не присваиваются в always-блоках top-level модуля.

Прескалер. Генерация тактирования для I2C ядра

I2C ядро i2c_master_core продвигает свой автомат только на тактах, когда вход ena_i = 1. Один импульс ena соответствует четверти периода SCL - за 4 импульса ena происходит один полный цикл SCL (LOW-HIGH-HIGH-LOW). Чтобы получить SCL на нужной частоте, нужно подавать ena с определённой периодичностью.

Стандартный режим I2C - 100 кГц. Период SCL = 10 мкс. Четверть периода = 2.5 мкс. При тактовой частоте FPGA 50 МГц (период 20 нс) нужен один импульс ena каждые 2500 нс / 20 нс = 125 тактов.

Формула:   PRESCALE = f_CLK / (4 × f_SCL) − 1 = 50 000 000 / (4 × 100 000) − 1 = 124

localparam вместо parameter в объявлении PRESCALE - осознанный выбор. parameter можно переопределить при инстанциации модуля (#(...)), а localparam - нельзя. Значение прескалера жёстко зафиксировано, потому что на реальном железе оно определяется парой “тактовая частота + целевая частота SCL”. Менять его извне бессмысленно - тактовая частота платы фиксирована.

Прескалер - это обычный счётчик с обратным отсчётом, который мы поместим в top-level модуль:

// ---------------------------------------------------------------
// Prescaler for I2C core
//   SCL = 50 MHz / (4 × (PRESCALE+1)) = 100 kHz
// ---------------------------------------------------------------

localparam [15:0] PRESCALE = 16'd124;

reg [15:0] pre_cnt;
reg        core_ena;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        pre_cnt  <= 16'd0;
        core_ena <= 1'b0;
    end else if (pre_cnt == 16'd0) begin
        pre_cnt  <= PRESCALE;       // перезагрузить: 124
        core_ena <= 1'b1;           // импульс!
    end else begin
        pre_cnt  <= pre_cnt - 16'd1;
        core_ena <= 1'b0;
    end
end

Каждые 125 тактов (2.5 мкс) ядро получает один ena-импульс. Четыре таких импульса образуют один бит на шине. 9 бит (8 данных + 1 ACK) x 4 фазы = 36 импульсов = один байт. Время передачи одного байта: 36 × 2.5 мкс = 90 мкс.

В симуляции мы использовали ENA_DIV = 4 для ускорения. На реальном железе используется PRESCALE = 124 для получения стандартных 100 кГц SCL. Временная диаграмма выглядит следующим образом:

Подавление дребезга кнопки. Модуль ax_debounce

Классическая история - фильтрация шума с механической кнопки. Механическая кнопка - это два кусочка металла, которые при нажатии соударяются. В момент контакта происходит дребезг (bouncing): контакт многократно замыкается и размыкается за несколько миллисекунд. FPGA на частоте 50 МГц “видит” каждое размыкание как отдельное событие. Без подавления дребезга одно нажатие кнопки превращается в десятки “нажатий”. Для нашего проекта это означало бы: одно нажатие KEY1 запустило бы тесты несколько раз подряд.

Модуль ax_debounce работает по принципу “ждать стабильности”. Два D-триггера формируют задержку для детектирования изменений. Счётчик считает, сколько тактов сигнал остаётся стабильным. Только когда счетчик доходит до порога (20 мс x 50 МГц = 1 000 000 тактов), модуль фиксирует новое состояние кнопки.

Модуль генерирует три выходных сигнала:

  1. button_out - стабильное текущее состояние кнопки;

  2. button_posedge - за 1 такт выдает импульс при отпускании кнопки (0 →1);

  3. button_negedge - за 1 такт выдает импульс при нажатии кнопки (1→0);

На плате AX301 кнопки active-low: нажатие = LOW, отпускание = HIGH. Мы используем button_negedge — однотактный импульс в момент нажатия кнопки. Именно этот сигнал подается в контроллер тестов как start.

module ax_debounce #(
    parameter N        = 32,
    parameter FREQ     = 50,    // MHz
    parameter MAX_TIME = 20     // ms
)(
    input  wire clk,
    input  wire rst,
    input  wire button_in,
    output reg  button_posedge,
    output reg  button_negedge,
    output reg  button_out
);

    localparam TIMER_MAX_VAL = MAX_TIME * 1000 * FREQ;

    reg [N-1:0] q_reg, q_next;
    reg         DFF1, DFF2;
    reg         button_out_d0;

    wire q_reset = (DFF1 ^ DFF2);
    wire q_add   = ~(q_reg == TIMER_MAX_VAL);

    always @(*) begin
        case ({q_reset, q_add})
            2'b00:   q_next = q_reg;
            2'b01:   q_next = q_reg + 1;
            default: q_next = {N{1'b0}};
        endcase
    end

    always @(posedge clk or posedge rst) begin
        if (rst) begin
            DFF1  <= 1'b0;
            DFF2  <= 1'b0;
            q_reg <= {N{1'b0}};
        end else begin
            DFF1  <= button_in;
            DFF2  <= DFF1;
            q_reg <= q_next;
        end
    end

    always @(posedge clk or posedge rst) begin
        if (rst)
            button_out <= 1'b1;
        else if (q_reg == TIMER_MAX_VAL)
            button_out <= DFF2;
    end

    always @(posedge clk or posedge rst) begin
        if (rst) begin
            button_out_d0  <= 1'b1;
            button_posedge <= 1'b0;
            button_negedge <= 1'b0;
        end else begin
            button_out_d0  <= button_out;
            button_posedge <= ~button_out_d0 &  button_out;
            button_negedge <=  button_out_d0 & ~button_out;
        end
    end

endmodule

В top-level модуле подключаем его следующим образом:

wire btn_negedge;

ax_debounce u_debounce (
    .clk            (clk),
    .rst            (~rst_n),      // ← инверсия!
    .button_in      (key1),
    .button_posedge (),            // ← не используется
    .button_negedge (btn_negedge),
    .button_out     ()             // ← не используется
);

 Модуль ax_debounce имеет active-HIGH сброс (rst), а наш системный сброс active-LOW (rst_n). Поэтому при подключении сигнал инвертируется: .rst(~rst_n). Когда rst_n = 0 (кнопка сброса нажата), ~rst_n = 1, и debounce сбрасывается.

Два порта .button_posedge() и .button_out() подключены к пустым скобкам - эти выходы не используются. В Verilog пустые скобки означают “подключить к ничему” (dangling wire). Синтезатор Quartus оптимизирует неиспользуемую логику и просто не создаёт для неё схему.

Имя экземпляра u_debounce - конвенция “u_-prefix” распространена в RTL-дизайне. Она помогает отличать экземпляры модулей от обычных сигналов при чтении иерархических путей: u_debounce.button_out очевидно относится к подмодулю, а не к регистру верхнего уровня.

Семисегментный дисплей

Семисегментный индикатор - это набор из 7 светодиодов (сегментов), расположенных в форме цифры “8”, плюс восьмой сегмент - десятичная точка (DP). Каждый сегмент обозначается буквой от a до g:

Чтобы показать цифру, нужно зажечь определённую комбинацию сегментов. На плате AX301 дисплей active-low: 0 на сегменте = сегмент горит, 1 = погашен. Поэтому при формировании символа: 8'b0000_0000 - все сегменты горят (цифра 8), а 8'b1111_1111 - всё погашено (пусто).

На плате 6 разрядов дисплея, но все сегменты (a-g, DP) соединены параллельно - одни и те же 8 проводов seg_data[7:0] идут ко всем шести цифрам. Какая именно цифра активна в данный момент, определяется сигналом выбора seg_sel[5:0]. Это сделано для экономии пинов FPGA: вместо 6 х 8 = 48 проводов нужно всего 8 + 6 = 14.

Принцип отображения цифр - быстрое последовательное переключение. Если переключение происходит достаточно быстро (> 60 Гц на каждый разряд), человеческий глаз не замечает мерцания - все цифры, даже если они разные, кажутся горящими одновременно. В нашем проекте частота сканирования - 200 Гц на полный цикл из 6 разрядов. Каждый разряд обновляется ~33 раза в секунду. Этого достаточно для комфортного восприятия.

Счётчик scan_cnt считает от 0 до 41 665 и по переполнению переключает scan_idx (0 → 1 → 2 → 3 → 4 → 5 → 0 → …). В каждый момент активен один разряд.

Код модуля:

`timescale 1ns / 1ps
// ---------------------------------------------------------------------------
// 7-Segment Display Scanner — 6 digits, active-low select and data
// ---------------------------------------------------------------------------
module seg_scan #(
    parameter CLK_FREQ  = 50_000_000,
    parameter SCAN_FREQ = 200
)(
    input  wire       clk,
    input  wire       rst_n,
    output reg  [5:0] seg_sel,       // digit select (active-low)
    output reg  [7:0] seg_data,      // segment data (active-low, bit 7 = DP)
    input  wire [7:0] seg_data_0,    // rightmost digit
    input  wire [7:0] seg_data_1,
    input  wire [7:0] seg_data_2,
    input  wire [7:0] seg_data_3,
    input  wire [7:0] seg_data_4,
    input  wire [7:0] seg_data_5     // leftmost digit
);

    localparam SCAN_MAX = CLK_FREQ / (SCAN_FREQ * 6) - 1; // = 50_000_000 / (200 × 6) - 1 = 41 665

    reg [31:0] scan_cnt;
    reg [2:0]  scan_idx;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            scan_cnt <= 32'd0;
            scan_idx <= 3'd0;
        end else if (scan_cnt >= SCAN_MAX) begin
            scan_cnt <= 32'd0;
            scan_idx <= (scan_idx == 3'd5) ? 3'd0 : scan_idx + 3'd1;
        end else begin
            scan_cnt <= scan_cnt + 32'd1;
        end
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            seg_sel  <= 6'b111_111;
            seg_data <= 8'hFF;
        end else begin
            case (scan_idx)
                3'd0: begin seg_sel <= 6'b111_110; seg_data <= seg_data_0; end
                3'd1: begin seg_sel <= 6'b111_101; seg_data <= seg_data_1; end
                3'd2: begin seg_sel <= 6'b111_011; seg_data <= seg_data_2; end
                3'd3: begin seg_sel <= 6'b110_111; seg_data <= seg_data_3; end
                3'd4: begin seg_sel <= 6'b101_111; seg_data <= seg_data_4; end
                3'd5: begin seg_sel <= 6'b011_111; seg_data <= seg_data_5; end
                default: begin seg_sel <= 6'b111_111; seg_data <= 8'hFF; end
            endcase
        end
    end

endmodule

Контроллер тестов формирует содержимое шести разрядов в зависимости от состояния FSM:

  • в ожидании IDLE будет отображать прочерки;

  • во время выполнения теста:

  • результат теста:

    Пример при успешном тесте 1: 1 P A 5 A 5 - записали 0xA5, прочитали 0xA5, Pass.
    Пример при ошибке теста 3: 3 F F F E E - записали 0xFF, прочитали 0xEE (ошибка), Fail.

  • итоговый результат:

    Пример при всех успешных тестах: P 4 F 0 - 4 Pass, 0 Fail.

В модуле top-level поключим его следующим образом:

// ---------------------------------------------------------------
// 7-segment display scanner (6 digits)
// ---------------------------------------------------------------

    seg_scan #(
    .CLK_FREQ  (50_000_000),
    .SCAN_FREQ (200)
    ) u_seg (
       .clk        (clk),
       .rst_n      (rst_n),
       .seg_sel    (seg_sel),     // → напрямую к выходным пинам
       .seg_data   (seg_data),    // → напрямую к выходным пинам
       .seg_data_0 (dig0),        // ← от контроллера тестов
       .seg_data_1 (dig1),
       .seg_data_2 (dig2),
       .seg_data_3 (dig3),
       .seg_data_4 (dig4),
       .seg_data_5 (dig5)
    );

Параметры CLK_FREQ и SCAN_FREQ передаются явно. CLK_FREQ = 50_000_000 (50 МГц) нужен для расчёта делителя частоты сканирования. Подчёркивания в числах (50_000_000) - синтаксический сахар Verilog для читаемости, компилятор их игнорирует.

Выходы seg_sel и seg_data подключены напрямую к одноимённым output-портам top-level модуля - сигналы идут прямиком на пины FPGA без промежуточных регистров.

Светодиоды

Четыре светодиода led[3:0] индицируют результат каждого теста: led[k] = 1 означает, что тест k+1 пройден. При успешном завершении всех тестов горят все 4 LED. При сбросе все LED гаснут.

Все тесты OK:      ● ● ● ●     (led = 4'b1111)
Тест 3 не прошёл:  ● ● · ●     (led = 4'b1011)
До запуска:        · · · ·    (led = 4'b0000)

Выводы шины I2C

В симуляции I2C шина моделировалась через wire с pullup. На реальном железе всё немного иначе - FPGA подключается к физическим проводам SDA и SCL, на которых стоят резисторы подтяжки к питанию (на плате AX301 они уже распаяны).

Два оператора assign - единственное место во всём проекте, где inout-пины I2C получают значение. 1'bz — литерал «высокий импеданс» (Hi-Z). Как уже было описано выше - для синтезатора Quartus эта конструкция однозначно описывает tri-state буфер: когда scl_oen = 1, выходной драйвер отключён (Hi-Z), когда scl_oen = 0 - драйвер выводит логический 0.

assign i2c_scl = scl_oen ? 1'bz : 1'b0;
assign i2c_sda = sda_oen ? 1'bz : 1'b0;

В Quartus такой inout порт автоматически превращается в три сигнала: выход данных, сигнал output-enable и вход данных. Фиттер правильно разводит это на IO-ячейку FPGA. Это и есть open-drain: мы можем только притянуть к нулю или отпустить. Мы никогда не “выталкиваем” единицу. Pull-up резистор делает единицу за нас. Плюсом не забываем, что I2C - мультимастерная шина. На одной шине может быть несколько мастеров и несколько slave-устройств. Open-drain гарантирует, что если хотя бы одно устройство тянет линию к нулю, вся шина в нуле (wired-AND). 

I2C-ядро в составе тестовой оболочки

Контроллер тестов i2c_test_ctrl общается с ядром i2c_master_core через тот же интерфейс, который мы тестировали в симуляции.

Протокол взаимодействия такой же, как в тестбенче: ждём ready = 1, подаем команду через cmd_valid, ждём ready = 0 (ядро приняло), ждём ready = 1 (ядро завершило работу), читаем dout/rx_ack. Разница только в том, что в тестбенче мы описывали это через task в initial-блоке, а здесь - через синтезируемый автомат (FSM).

Внутри top-level модуля соединения эти объявляются с помощью wire:

wire        core_cmd_valid;
wire [2:0]  core_cmd;
wire [7:0]  core_din;
wire [7:0]  core_dout;
wire        core_rx_ack;
wire        core_ready;
wire        core_arb_lost;
wire        core_arb_lost_clr;
wire        core_busy;

wire        scl_oen, sda_oen;

Блок объявлений wire перед инстанциацией - это внутренние “провода” top-level модуля. Они соединяют выходы одного подмодуля со входами другого. Все объявлены как wire, а не reg, потому что их значения определяются подмодулями, а не always-блоками текущего модуля.

Префикс “core_” выделяет сигналы, принадлежащие интерфейсу i2c_master_core, и отделяет их от одноименных портов контроллера тестов.

i2c_master_core u_core (
    .clk_i            (clk),
    .rstn_i           (rst_n),
    .ena_i            (core_ena),       // ← от прескалера
    .cmd_valid_i      (core_cmd_valid), // ← от i2c_test_ctrl
    .cmd_i            (core_cmd),       // ← от i2c_test_ctrl
    .din_i            (core_din),       // ← от i2c_test_ctrl
    .dout_o           (core_dout),      // → к i2c_test_ctrl
    .rx_ack_o         (core_rx_ack),    // → к i2c_test_ctrl
    .ready_o          (core_ready),     // → к i2c_test_ctrl
    .arb_lost_o       (core_arb_lost),  // → к i2c_test_ctrl
    .arb_lost_clear_i (core_arb_lost_clr), // ← от i2c_test_ctrl
    .busy_o           (core_busy),      // не подключён дальше (информационный)
    .scl_i            (i2c_scl),        // ← от inout пина
    .scl_oen_o        (scl_oen),        // → к tri-state assign
    .sda_i            (i2c_sda),        // ← от inout пина
    .sda_oen_o        (sda_oen)         // → к tri-state assign
);

Подключение портов происходит через именованное сопоставление (.port_name(signal_name)). Это безопаснее, чем позиционное подключение, потому что порядок портов не имеет значения - важно только имя. Если перепутать порядок, компилятор выдаст ошибку “port not found”, а не молча подключит неправильный сигнал.

Обратите внимание на соединения .scl_i(i2c_scl) и .sda_i(i2c_sda) - ядро читает состояние шины напрямую с inout-пина FPGA. Это работает, потому что входной буфер inout-пина активен всегда, независимо от output-enable.

И объявляем модуль, который будет управлять тестами с параметрами. Его мы разберем ниже в отдельной главе:

wire [7:0] dig5, dig4, dig3, dig2, dig1, dig0;

i2c_test_ctrl #(
    .SHOW_TICKS (25_000_000),  // 500 ms @ 50 MHz
    .WR_TICKS   (300_000)      // 6 ms @ 50 MHz
) u_ctrl (
    .clk          (clk),
    .rst_n        (rst_n),
    .start        (btn_negedge),      // ← от debounce
    .cmd_valid    (core_cmd_valid),    // → к ядру
    .cmd          (core_cmd),
    .din          (core_din),
    .dout         (core_dout),         // ← от ядра
    .rx_ack       (core_rx_ack),
    .ready        (core_ready),
    .arb_lost     (core_arb_lost),
    .arb_lost_clr (core_arb_lost_clr), // → к ядру
    .seg5         (dig5),              // → к seg_scan
    .seg4         (dig4),
    .seg3         (dig3),
    .seg2         (dig2),
    .seg1         (dig1),
    .seg0         (dig0),
    .led          (led)                // → напрямую к выходным пинам
);

Конструкция #(.SHOW_TICKS(25_000_000), .WR_TICKS(300_000)) - переопределение параметров при инстанциации. Модуль i2c_test_ctrl объявляет эти параметры со значениями по умолчанию, но при подключении мы можем задать другие значения. Это ключевой механизм для симуляции: в тестбенче мы подставляем SHOW_TICKS = 200 и WR_TICKS = 100, чтобы тесты выполнялись за микросекунды вместо секунд.

Шесть wire [7:0] dig5...dig0- промежуточные провода, несущие 7-сегментные паттерны от контроллера тестов к мультиплексору дисплея. Каждый провод - 8 бит (7 сегментов + десятичная точка).

Сигнал led - единственный output top-level модуля, который подключён напрямую к выходному порту подмодуля. Промежуточный wire не нужен - Verilog позволяет подключить порт подмодуля напрямую к порту вышестоящего модуля.

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

Основа тестовой оболочки

Разберем самый большой и сложный модуль проекта (~420 строк), который выполняет все необходимые действия. Объявляем модуль и параметры:

module i2c_test_ctrl #(
    parameter SHOW_TICKS = 25_000_000,
    parameter WR_TICKS   = 300_000
)(
    input  wire        clk,
    input  wire        rst_n,
    input  wire        start,         // однотактный импульс от debounce

    output reg         cmd_valid,     // → к ядру I2C
    output reg  [2:0]  cmd,
    output reg  [7:0]  din,
    input  wire [7:0]  dout,          // ← от ядра I2C
    input  wire        rx_ack,
    input  wire        ready,
    input  wire        arb_lost,
    output reg         arb_lost_clr,

    output reg  [7:0]  seg5, seg4, seg3, seg2, seg1, seg0,
    output reg  [3:0]  led
);

endmodule;

parameter (а не localparam) - эти значения можно и нужно менять при объявлении. На реальном железе SHOW_TICKS = 25_000_000 (500 мс), в тестбенче - 200. WR_TICKS = 300_000 (6 мс) на железе, 100 в тестбенче. Это единственная точка настройки, которая позволяет одному и тому же коду работать и на плате, и в симуляции.

Выходы к ядру I2C (cmd_valid, cmd, din) объявлены как output reg - они присваиваются в always-блоке FSM. Входы от ядра (dout, rx_ack, ready) - input wire. Это типичный паттерн: управляющий модуль имеет reg-выходы (генерирует команды), а контролируемый модуль - wire-входы (принимает их).

Шесть выходов seg5...seg0 тоже output reg - они присваиваются через task display_* внутри FSM.

Далее объявляем константы I2C-команд:

localparam [2:0]
    I2C_START   = 3'd1,
    I2C_WRITE   = 3'd2,
    I2C_READ    = 3'd3,
    I2C_STOP    = 3'd4,
    I2C_RESTART = 3'd5;

localparam [7:0] SLAVE_W = 8'hA0;
localparam [7:0] SLAVE_R = 8'hA1;

Коды команд дублируют аналогичные из i2c_master_core. Это осознанный дубликат, а не ошибка - контроллер тестов не зависит от исходного кода ядра. Пока числовые значения совпадают, всё работает. Альтернатива - include-файл с общими определениями, но для такого маленького проекта явные константы проще.

SLAVE_W = 8'hA0 - полный адресный байт EEPROM 24LC04 для записи. 8'hA0 = 1010_000_0 - верхние 7 бит (1010_000 = 0x50) - это 7-битный I2C-адрес, младший бит 0 - бит направления (write). SLAVE_R = 8'hA1 = 1010_000_1 - тот же адрес, но с битом чтения.

Для работы с семисегментными паттернами тоже объявляем предопределенные символы:

localparam [7:0]
    S_BLK = 8'hFF,                       // blank (все сегменты погашены)
    S_DSH = {1'b1, 7'b011_1111},         // '-'  (только сегмент g)
    S_P   = {1'b1, 7'b000_1100},         // 'P'  (a, b, e, f, g)
    S_FC  = {1'b1, 7'b000_1110};         // 'F'  (a, e, f, g)

Каждый паттерн - 8 бит. Старший бит (бит 7) - десятичная точка (DP), всегда 1 (погашена). Младшие 7 бит - сегменты g f e d c b a. Поскольку дисплей active-low, 0 = горит, 1 = погашен.

Конструкция {1'b1, 7'b011_1111} - конкатенация битов. Фигурные скобки {} в Verilog склеивают значения в одну шину. Здесь: 1 бит DP + 7 бит сегментов = 8 бит.

    function [7:0] seg_hex;
        input [3:0] h;
        reg [6:0] s;
        begin
            case (h) // synopsys full_case parallel_case
                4'h0: s = 7'b100_0000;  // verilator lint_off BLKSEQ
                4'h1: s = 7'b111_1001;
                4'h2: s = 7'b010_0100;
                4'h3: s = 7'b011_0000;
                4'h4: s = 7'b001_1001;
                4'h5: s = 7'b001_0010;
                4'h6: s = 7'b000_0010;
                4'h7: s = 7'b111_1000;
                4'h8: s = 7'b000_0000;
                4'h9: s = 7'b001_0000;
                4'hA: s = 7'b000_1000;
                4'hB: s = 7'b000_0011;
                4'hC: s = 7'b100_0110;
                4'hD: s = 7'b010_0001;
                4'hE: s = 7'b000_0110;
                4'hF: s = 7'b000_1110;  // verilator lint_on BLKSEQ
                default: s = 7'b111_1111;
            endcase
            seg_hex = {1'b1, s};        // verilator lint_off BLKSEQ
        end                             // verilator lint_on BLKSEQ
    endfunction

function в Verilog - это комбинаторная подпрограмма. В отличие от task, функция не может содержать временных задержек (#, @), не может модифицировать внешние reg-переменные и всегда возвращает значение. Она превращается в чистую комбинаторную логику при синтезе - мультиплексор 16:1, на входе 4-битный hex-nibble, на выходе 7-битный паттерн сегментов.

Последняя строка seg_hex = {1'b1, s} - присваивание возвращаемого значения. Имя функции seg_hex одновременно является и именем выходной “переменной”. DP всегда погашен (1'b1).

В реальном коде case снабжен директивой “// synopsys full_case parallel_case”. Это указание для синтезатора Synopsys (и совместимых): full_case говорит “все варианты покрыты, не создавай защёлку”, parallel_case - “варианты взаимоисключающие, оптимизируй как параллельный MUX”. Для hex-декодера это безопасно - 4-битный вход имеет ровно 16 значений, и все 16 покрыты.

Комментарии // verilator lint_off BLKSEQ и // verilator lint_on BLKSEQ - прагмы для линтера Verilator. Он предупреждает о блокирующих присваиваниях (=) внутри функций, хотя это абсолютно корректно - функции должны использовать =, а не <=. Прагмы подавляют ложное предупреждение.

Далее объявление данных тестовых ROM - адреса и данные.

    // ----- Test ROM: {reg_addr[7:0], write_data[7:0]} -----

    localparam NUM_TESTS = 4;
    reg [7:0] tst_addr;
    reg [7:0] tst_data;

    always @(*) begin
        case (test_idx)
            2'd0: begin tst_addr = 8'h00; tst_data = 8'hA5; end
            2'd1: begin tst_addr = 8'h01; tst_data = 8'h5A; end
            2'd2: begin tst_addr = 8'h10; tst_data = 8'hFF; end
            2'd3: begin tst_addr = 8'h11; tst_data = 8'h00; end
            default: begin tst_addr = 8'h00; tst_data = 8'h00; end
        endcase
    end

always @(*) - комбинаторный блок. Звёздочка означает “реагировать на изменение любого сигнала, от которого зависит результат”. Синтезатор превращает это в мультиплексор: при test_idx = 0 на выходе tst_addr = 0x00, tst_data = 0xA5, при test_idx = 1 - 0x01, 0x5A, и т.д.

Ветка default обязательна - без неё синтезатор Quartus создаст защёлку (latch), потому что не для всех значений test_idx определен выход. Защёлки в FPGA-дизайне - источник тонких ошибок тайминга, их следует избегать.

Хотя test_idx - 2-битный регистр ([1:0]), и 2'd0..2'd3 покрывают все 4 значения, default служит страховкой: если когда-нибудь ширину test_idx расширят, код не сломается.

Далее объявляем состояние FSM и подавтомата:

localparam [3:0]
    ST_IDLE     = 4'd0,
    ST_PREP     = 4'd1,
    ST_WR_SEQ   = 4'd2,
    ST_WR_DELAY = 4'd3,
    ST_RD_SEQ   = 4'd4,
    ST_VERIFY   = 4'd5,
    ST_SHOW     = 4'd6,
    ST_NEXT     = 4'd7,
    ST_SUMMARY  = 4'd8,
    ST_ERR_STOP = 4'd9;

localparam [1:0]
    SS_ISSUE  = 2'd0,
    SS_ACCEPT = 2'd1,
    SS_DONE   = 2'd2;

10 состояний основного автомата кодируются 4 битами (вмещает до 16 значений). 3 подсостояния - 2 битами. Именные localparam вместо “магических чисел” делают код самодокументирующимся: state <= ST_WR_DELAY читается как предложение на английском.

Объявляем регистры состояния:

reg [3:0]  state;        // текущее состояние FSM
reg [1:0]  sub;          // подсостояние (issue/accept/done)
reg [2:0]  step;         // шаг внутри write- или read-последовательности (0..6)
reg [1:0]  test_idx;     // номер текущего теста (0..3)
reg [31:0] delay_cnt;    // универсальный счётчик задержки
reg [7:0]  rd_data;      // данные, прочитанные из EEPROM
reg [3:0]  pass_flags;   // бит k = 1, если тест k пройден
reg [3:0]  fail_flags;   // бит k = 1, если тест k провален
reg        test_error;   // флаг: текущий тест ошибочен

delay_cnt используется дважды: в ST_WR_DELAY считает до WR_TICKS (задержка записи EEPROM), в ST_SHOW считает до SHOW_TICKS (задержка отображения результата). Один 32-битный счётчик для обеих задач - экономия регистров. Каждый раз при входе в состояние с задержкой счётчик обнуляется.

pass_flags и fail_flags - 4-битные регистры, где каждый бит соответствует одному тесту. pass_flags[0] = 1 означает “тест 0 прошёл”. Для итогового дисплея нужно подсчитать количество единиц:

wire [2:0] pass_count = {2'd0, pass_flags[0]} + {2'd0, pass_flags[1]}
                      + {2'd0, pass_flags[2]} + {2'd0, pass_flags[3]};
wire [2:0] fail_count = {2'd0, fail_flags[0]} + {2'd0, fail_flags[1]}
                      + {2'd0, fail_flags[2]} + {2'd0, fail_flags[3]};

Это population count (подсчет единичных бит). Каждый бит флага расширяется до 3 бит ({2'd0, flag[k]} → 3'b00x), затем четыре 3-битных значения складываются. Результат - от 0 до 4. Синтезатор превращает это в дерево однобитных сумматоров.

Далее. Генераторы команд write и read:

reg [2:0] wr_cmd;
reg [7:0] wr_din;
always @(*) begin
    case (step)
        3'd0: begin wr_cmd = I2C_START; wr_din = 8'h00;    end
        3'd1: begin wr_cmd = I2C_WRITE; wr_din = SLAVE_W;   end  // 0xA0
        3'd2: begin wr_cmd = I2C_WRITE; wr_din = tst_addr;  end  // адрес ячейки
        3'd3: begin wr_cmd = I2C_WRITE; wr_din = tst_data;  end  // данные
        3'd4: begin wr_cmd = I2C_STOP;  wr_din = 8'h00;     end
        default: begin wr_cmd = I2C_STOP; wr_din = 8'h00;   end
    endcase
end

Ещё один комбинаторный мультиплексор. По значению step (номер текущего шага) выбираются команда и данные для I2C ядра. FSM в состоянии ST_WR_SEQ просто инкрементирует step от 0 до 4 - сам мультиплексор подставляет нужную команду.

Этот паттерн - разделение данных и управления. Данные (какие команды подавать) описаны в комбинаторном блоке-таблице. Управление (когда переходить к следующему шагу) - в FSM. Такое разделение упрощает модификацию: чтобы добавить шаг, достаточно добавить строку в case и увеличить границу step в FSM.

Аналогичный генератор для чтения - 7 шагов:

reg [2:0] rd_cmd;
reg [7:0] rd_din;
always @(*) begin
    case (step)
        3'd0: begin rd_cmd = I2C_START;    rd_din = 8'h00;           end
        3'd1: begin rd_cmd = I2C_WRITE;    rd_din = SLAVE_W;         end
        3'd2: begin rd_cmd = I2C_WRITE;    rd_din = tst_addr;        end
        3'd3: begin rd_cmd = I2C_RESTART;  rd_din = 8'h00;           end
        3'd4: begin rd_cmd = I2C_WRITE;    rd_din = SLAVE_R;         end  // 0xA1
        3'd5: begin rd_cmd = I2C_READ;     rd_din = {7'd0, 1'b1};    end  // NACK
        3'd6: begin rd_cmd = I2C_STOP;     rd_din = 8'h00;           end
        default: begin rd_cmd = I2C_STOP;  rd_din = 8'h00;           end
    endcase
end

На шаге 5 rd_din = {7'd0, 1'b1} - конкатенация: 7 нулевых бит + 1 единичный бит. Ядро i2c_master_core интерпретирует din_i[0] как “ACK/NACK от мастера при чтении”: 1 = NACK (конец чтения), 0 = ACK (продолжить).

После объявляем провода проверки ACK:

wire wr_check_ack = (step == 3'd1 || step == 3'd2 || step == 3'd3);
wire rd_check_ack = (step == 3'd1 || step == 3'd2 || step == 3'd4);

Не все шаги требуют проверки ACK. START и STOP - управляющие условия, ACK для них не существует. READ с NACK - мы сами решаем послать NACK, проверять нечего. ACK нужно проверять только на шагах WRITE с адресом или данными - именно там EEPROM подтверждает приём.

Эти провода используются в FSM: if (wr_check_ack && rx_ack) - если на текущем шаге нужна проверка ACK и ядро сообщает rx_ack = 1 (NACK) - это ошибка.

Таски и отображение данных

Создадим таски для удобства и . В Verilog task внутри always-блока - это просто встроенная подпрограмма для группировки присваиваний. Она не создаёт отдельную аппаратную сущность - это синтаксическое удобство. Все <= (неблокирующие присваивания) внутри task-а выполняются в контексте вызывающего always-блока.

    // ----- Display update task (active display depends on state) -----
    task display_idle;
        begin
            seg5 <= S_DSH; seg4 <= S_DSH; seg3 <= S_DSH;
            seg2 <= S_DSH; seg1 <= S_DSH; seg0 <= S_DSH;
        end
    endtask

    task display_running;
        begin
            seg5 <= seg_hex({2'b0, test_idx} + 4'd1);
            seg4 <= S_DSH;
            seg3 <= seg_hex(tst_data[7:4]);
            seg2 <= seg_hex(tst_data[3:0]);
            seg1 <= S_DSH;
            seg0 <= S_DSH;
        end
    endtask

    task display_result;
        begin
            seg5 <= seg_hex({2'b0, test_idx} + 4'd1);
            seg4 <= test_error ? S_FC : S_P;
            seg3 <= seg_hex(tst_data[7:4]);
            seg2 <= seg_hex(tst_data[3:0]);
            seg1 <= seg_hex(rd_data[7:4]);
            seg0 <= seg_hex(rd_data[3:0]);
        end
    endtask

Выражение {2'b0, test_idx} + 4'd1 - расширяет 2-битный test_idx до 4 бит и прибавляет 1. При test_idx = 0 результат 4'd1seg_hex покажет “1”. При “test_idx = 3” → “4”. Человеку удобнее видеть номера тестов 1-4, а не 0-3.

tst_data[7:4] и tst_data[3:0] - старший и младший nibble (полубайт). Каждый преобразуется в hex-цифру через seg_hex. Так 0xA5 отображается как два разряда: A и 5.

Тернарный оператор test_error ? S_FC : S_P - если ошибка, показать “F” (Fail), иначе “P” (Pass).

Основной FSM - always-блок

Разберем код основного FSM-блока:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state       <= ST_IDLE;
        sub         <= SS_ISSUE;
        step        <= 3'd0;
        test_idx    <= 2'd0;
        delay_cnt   <= 32'd0;
        cmd_valid   <= 1'b0;
        cmd         <= 3'd0;
        din         <= 8'd0;
        arb_lost_clr <= 1'b0;
        rd_data     <= 8'd0;
        pass_flags  <= 4'd0;
        fail_flags  <= 4'd0;
        test_error  <= 1'b0;
        led         <= 4'b0000;
        seg5 <= S_DSH; seg4 <= S_DSH; seg3 <= S_DSH;
        seg2 <= S_DSH; seg1 <= S_DSH; seg0 <= S_DSH;
    end else begin
        arb_lost_clr <= 1'b0;   // ← auto-clear: импульс длится один такт
        case (state)
            ...
        endcase
    end
end

Блок сброса инициализирует все регистры модуля. Это критически важно для FPGA - в отличие от ASIC, регистры Cyclone IV могут стартовать в произвольном состоянии (зависит от конфигурации), и без явного сброса поведение будет недетерминированным.

Строка arb_lost_clr <= 1'b0 вне case - сброс флага по умолчанию в каждом такте. Если ни одно состояние не выставит arb_lost_clr <= 1'b1, он останется 0. Это паттерн “однотактный импульс”: в ST_PREP устанавливается arb_lost_clr <= 1'b1, а уже в следующем такте “авто-clear” возвращает его в 0. Ядро видит импульс длиной 1 такт.

Состояние ST_IDLE

ST_IDLE: begin
    display_idle;
    if (start) begin
        test_idx   <= 2'd0;
        pass_flags <= 4'd0;
        fail_flags <= 4'd0;
        led        <= 4'b0000;
        state      <= ST_PREP;
    end
end

Каждый такт вызывается display_idle - дисплей непрерывно обновляется паттерном ------. При получении импульса start (от debounce) - обнуляются счетчики тестов, гасятся LED, и FSM переходит в ST_PREP.

Состояние ST_PREP - подготовка к тесту

ST_PREP: begin
    step       <= 3'd0;
    sub        <= SS_ISSUE;
    test_error <= 1'b0;
    arb_lost_clr <= 1'b1;
    display_running;
    state      <= ST_WR_SEQ;
end

ST_PREP - транзитное состояние (1 такт). Оно выполняет “домашнюю уборку” перед каждым тестом:

  • step <= 3'd0 - сброс шага I2C-последовательности на начало

  • sub <= SS_ISSUE - подавтомат начнёт с ожидания ready

  • test_error <= 1'b0 - очистка флага ошибки от предыдущего теста

  • arb_lost_clr <= 1'b1 - импульс очистки arb_lost в ядре (сбросится в 0 следующим тактом благодаря auto-clear в строке arb_lost_clr <= 1'b0 вне case)

  • display_running - показать на дисплее номер теста и записываемые данные;

  • безусловный переход в ST_WR_SEQ.

Без ST_PREP пришлось бы дублировать эти инициализации в ST_IDLE (для первого теста) и в ST_NEXT (для последующих). Выделение в отдельное состояние убирает дублирование.

Состояние ST_WR_SEQ - ядро взаимодействия с подавтоматом

ST_WR_SEQ: begin
    case (sub)
        SS_ISSUE: begin
            if (ready) begin
                cmd_valid <= 1'b1;
                cmd       <= wr_cmd;     // ← из комбинаторного генератора
                din       <= wr_din;
                sub       <= SS_ACCEPT;
            end
        end
        SS_ACCEPT: begin
            if (!ready) begin
                cmd_valid <= 1'b0;
                sub       <= SS_DONE;
            end
        end
        SS_DONE: begin
            if (ready) begin
                if (wr_check_ack && rx_ack) begin
                    test_error <= 1'b1;
                    state <= ST_ERR_STOP;
                    sub   <= SS_ISSUE;
                end else if (step == 3'd4) begin
                    delay_cnt <= 32'd0;
                    state     <= ST_WR_DELAY;
                end else begin
                    step <= step + 3'd1;
                    sub  <= SS_ISSUE;
                end
            end
        end
        default: sub <= SS_ISSUE;
    endcase
end

Это двухуровневый автомат: внешний уровень (state) определяет фазу теста, внутренний (sub) - этап рукопожатия с I2C-ядром.

SS_ISSUE: Ждёт ready = 1 от ядра. Как только ядро готово - выставляет cmd_valid, cmd и din. Значения wr_cmd/wr_din автоматически подставлены комбинаторным генератором в зависимости от step.

SS_ACCEPT: Ждёт ready = 0 - подтверждение, что ядро приняло команду и начало выполнение. Снимает cmd_valid.

SS_DONE: Ждёт ready = 1 - команда выполнена. Здесь развилка:

  • Если ACK-проверка активна и пришёл NACK (rx_ack = 1) → ошибка, переход в ST_ERR_STOP

  • Если текущий шаг последний (step == 4) → переход к задержке записи ST_WR_DELAY

  • Иначе → инкремент step, возврат в SS_ISSUE для следующего шага

Состояние ST_WR_DELAY - ожидание цикла записи EEPROM

ST_WR_DELAY: begin
    if (delay_cnt >= WR_TICKS) begin
        step  <= 3'd0;
        sub   <= SS_ISSUE;
        state <= ST_RD_SEQ;
    end else begin
        delay_cnt <= delay_cnt + 32'd1;
    end
end

После отправки STOP (конец записи) EEPROM занята внутренней записью в энергонезависимую память. В это время она не отвечает на I2C-запросы. Счётчик delay_cnt считает от 0 до WR_TICKS (300 000 тактов = 6 мс), после чего переходит к чтению. Перед переходом обнуляются step и sub для следующей (read) последовательности.

ST_RD_SEQ устроен аналогично ST_WR_SEQ, с двумя отличиями: используются rd_cmd/rd_din вместо wr_cmd/wr_din, и на шаге 5 (READ) сохраняются прочитанные данные:

if (step == 3'd5)
    rd_data <= dout;        // ← сохранить прочитанный из EEPROM байт
if (step == 3'd6) begin
    state <= ST_VERIFY;     // все шаги выполнены — сравнить
end

Состояния ST_VERIFY и ST_SHOW

ST_VERIFY: begin
    if (rd_data != tst_data)
        test_error <= 1'b1;
    display_result;
    delay_cnt <= 32'd0;
    state     <= ST_SHOW;
end

ST_VERIFY - транзитное состояние, занимающее ровно 1 такт. Оно сравнивает прочитанное значение rd_data с ожидаемым tst_data. Если не совпадают - устанавливает флаг ошибки. Затем безусловный переход в ST_SHOW.

ST_SHOW: begin
    if (!test_error)
        display_result;
    if (test_error) begin
        fail_flags[test_idx] <= 1'b1;
        display_result;
    end else begin
        pass_flags[test_idx] <= 1'b1;
        led[test_idx]        <= 1'b1;
    end
    if (delay_cnt >= SHOW_TICKS)
        state <= ST_NEXT;
    else
        delay_cnt <= delay_cnt + 32'd1;
end

ST_SHOW задерживается на SHOW_TICKS тактов для того, чтобы пользователь успел прочитать результат на дисплее. Одновременно записываются флаги pass/fail и зажигается LED при успехе.

Обратите внимание на led[test_idx] <= 1'b1 - это побитовое присваивание: зажигается только светодиод текущего теста, остальные LED сохраняют значение от предыдущих тестов. К концу 4-го теста горят все 4 LED (при условии успеха).

Состояния ST_NEXT и ST_SUMMARY

ST_NEXT: begin
    if (test_idx == 2'd3)
        state <= ST_SUMMARY;
    else begin
        test_idx <= test_idx + 2'd1;
        state    <= ST_PREP;
    end
end

Точка ветвления: если выполнены все 4 теста (test_idx == 3) - итоговый экран. Иначе - увеличить индекс и перейти к следующему тесту.

ST_SUMMARY: begin
    seg5 <= S_P;
    seg4 <= seg_hex({1'b0, pass_count});
    seg3 <= S_BLK;
    seg2 <= S_FC;
    seg1 <= seg_hex({1'b0, fail_count});
    seg0 <= S_BLK;
    if (start)
        state <= ST_IDLE;
end

Итоговый экран обновляет дисплей каждый такт и ждёт повторного нажатия кнопки. {1'b0, pass_count} расширяет 3-битный pass_count до 4 бит для seg_hex.

Состояние ST_ERR_STOP - обработка ошибок

ST_ERR_STOP: begin
    case (sub)
        SS_ISSUE: begin
            if (ready) begin
                cmd_valid <= 1'b1;
                cmd       <= I2C_STOP;
                din       <= 8'd0;
                sub       <= SS_ACCEPT;
            end
        end
        SS_ACCEPT: begin
            if (!ready) begin
                cmd_valid <= 1'b0;
                sub       <= SS_DONE;
            end
        end
        SS_DONE: begin
            if (ready) begin
                rd_data <= 8'hEE;       // ← маркер ошибки
                display_result;
                delay_cnt <= 32'd0;
                state <= ST_SHOW;
                sub   <= SS_ISSUE;
            end
        end
        default: sub <= SS_ISSUE;
    endcase
end

При ошибке (NACK) FSM отправляет I2C_STOP через тот же подавтомат sub. После завершения STOP записывает 0xEE в rd_data - визуальный маркер ошибки на дисплее. Затем переходит в ST_SHOW, где тест будет помечен как FAIL.

Значение 0xEE выбрано специально: оно визуально отличимо от любых тестовых данных (0xA5, 0x5A, 0xFF, 0x00). Увидев на дисплее EE вместо ожидаемых данных, пользователь сразу поймёт, что произошла ошибка I2C-коммуникации, а не ошибка данных.

Проект в Quartus - файлы и настройки

Теперь создадим проект и проверим его в железе. Структура проекта будет содержать следующий набор уже знакомых файлов:

quartus/
├── i2c_test.qpf               ← Файл проекта Quartus (метаданные)
├── i2c_test_top.qsf           ← Настройки: устройство, исходники, пины
├── i2c_test_top.sdc           ← Временные ограничения
└── src/
    ├── i2c_test_top.v         ← Top-level модуль
    ├── i2c_test_ctrl.v        ← Контроллер тестов (FSM)
    ├── seg_scan.v             ← Мультиплексор дисплея
    └── ax_debounce.v          ← Подавление дребезга кнопки

../rtl/
    └── i2c_master_core.v      ← I2C ядро (используется из основного RTL) 

Первый файл, который мы сделаем сами, уже без мастера проектов - i2c_test.qpf. Это минимальный файл, который Quartus использует для идентификации проекта. Ключевая строка:

PROJECT_REVISION = "i2c_test_top"

Итоговое типовое содержание:

QUARTUS_VERSION = "25.1"
DATE = "21:44:27  April 07, 2026"

# Revisions
PROJECT_REVISION = "i2c_test_top"

Ревизия - это набор настроек (QSF), привязанный к конкретному top-level модулю. В нашем случае ревизия одна: i2c_test_top.

Следующий важный файл - QSF (Quartus Settings File) - основной конфигурационный файл. Он содержит директиву выбора устройства:

# ---- Device ----
set_global_assignment -name FAMILY "Cyclone IV E"
set_global_assignment -name DEVICE EP4CE6F17C8

EP4CE6F17C8 расшифровывается:

  • EP4CE6 - Cyclone IV E, 6 272 логических элементов

  • F1 - корпус FBGA 256 пинов

  • C8 - коммерческая температура (0-85°C), speed grade 8

Далее перечисляются константы:

set_global_assignment -name TOP_LEVEL_ENTITY               i2c_test_top
set_global_assignment -name PROJECT_OUTPUT_DIRECTORY       output_files
set_global_assignment -name MIN_CORE_JUNCTION_TEMP         0
set_global_assignment -name MAX_CORE_JUNCTION_TEMP         85
set_global_assignment -name ERROR_CHECK_FREQUENCY_DIVISOR  1
set_global_assignment -name NOMINAL_CORE_SUPPLY_VOLTAGE    1.2V
set_global_assignment -name STRATIX_DEVICE_IO_STANDARD    "2.5 V"

Исходные файлы:

# ---- Source files ----
set_global_assignment -name VERILOG_FILE src/i2c_test_top.v
set_global_assignment -name VERILOG_FILE src/i2c_test_ctrl.v
set_global_assignment -name VERILOG_FILE src/seg_scan.v
set_global_assignment -name VERILOG_FILE src/ax_debounce.v
set_global_assignment -name VERILOG_FILE ../rtl/i2c_master_core.v
set_global_assignment -name SDC_FILE i2c_test_top.sdc

Обратите внимание: i2c_master_core.v подключается из директории ./rtl/ - это то самое ядро, которое мы верифицировали на первом шаге. Мы не копируем его в папку проекта Quartus, а ссылаемся на оригинал. Это гарантирует, что при обновлении ядра проект Quartus автоматически получает обновлённую версию.

# ---- Partitions ----
set_global_assignment -name PARTITION_NETLIST_TYPE SOURCE -section_id Top
set_global_assignment -name PARTITION_FITTER_PRESERVATION_LEVEL PLACEMENT_AND_ROUTING -section_id Top
set_global_assignment -name PARTITION_COLOR 16764057 -section_id Top

Стандарты ввода-вывода:

# ---- IO Standards ----
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to clk
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to rst_n
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to key1
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to i2c_scl
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to i2c_sda
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to led[0]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to led[1]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to led[2]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to led[3]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_sel[0]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_sel[1]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_sel[2]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_sel[3]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_sel[4]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_sel[5]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_data[0]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_data[1]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_data[2]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_data[3]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_data[4]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_data[5]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_data[6]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_data[7]

Все пины настроены на стандарт 3.3V LVTTL - это логические уровни, совместимые с EEPROM 24LC04 и периферией платы. Каждый сигнал top-level модуля привязан к конкретному физическому пину FPGA:

# ---- Pin assignments (ALINX AX301 v2 board) ----

# Clock 50 MHz
set_location_assignment PIN_E1  -to clk

# Reset (active-low push button)
set_location_assignment PIN_N13 -to rst_n

# Start button (active-low)
set_location_assignment PIN_M15 -to key1

# LEDs
set_location_assignment PIN_E10 -to led[0]
set_location_assignment PIN_F9  -to led[1]
set_location_assignment PIN_C9  -to led[2]
set_location_assignment PIN_D9  -to led[3]

# I2C bus (24LC04 EEPROM)
set_location_assignment PIN_D1  -to i2c_scl
set_location_assignment PIN_E6  -to i2c_sda

# 7-segment display: digit select (active-low, 6 digits)
set_location_assignment PIN_M11 -to seg_sel[0]
set_location_assignment PIN_P11 -to seg_sel[1]
set_location_assignment PIN_N11 -to seg_sel[2]
set_location_assignment PIN_M10 -to seg_sel[3]
set_location_assignment PIN_P9 -to seg_sel[4]
set_location_assignment PIN_N9 -to seg_sel[5]

# 7-segment display: segment data (active-low, bit 7 = DP)
set_location_assignment PIN_R14 -to seg_data[0]
set_location_assignment PIN_N16 -to seg_data[1]
set_location_assignment PIN_P16 -to seg_data[2]
set_location_assignment PIN_T15 -to seg_data[3]
set_location_assignment PIN_P15 -to seg_data[4]
set_location_assignment PIN_N12 -to seg_data[5]
set_location_assignment PIN_N15 -to seg_data[6]
set_location_assignment PIN_R16 -to seg_data[7]

# Altera config pins
set_location_assignment PIN_C1  -to ~ALTERA_ASDO_DATA1~
set_location_assignment PIN_D2  -to ~ALTERA_FLASH_nCE_nCSO~
set_location_assignment PIN_H1  -to ~ALTERA_DCLK~
set_location_assignment PIN_H2  -to ~ALTERA_DATA0~
set_location_assignment PIN_F16 -to ~ALTERA_nCEO~

Ну и служебные данные:

set_global_assignment -name LAST_QUARTUS_VERSION "25.1std.0 Standard Edition"
set_instance_assignment -name PARTITION_HIERARCHY root_partition -to | -section_id Top%  

Следующий файл это i2c_test_top.sdc - Synopsys Design Constraints, файл, который объясняет инструменту временного анализа (TimeQuest), какие требования предъявляются к тактированию:

# SDC constraints for I2C EEPROM Test — AX301 board
# 50 MHz oscillator
create_clock -name clk -period 20.000 [get_ports {clk}]

# I2C is slow (100 kHz) — relax I/O timing
set_false_path -from [get_ports {i2c_sda i2c_scl key1 rst_n}]
set_false_path -to   [get_ports {i2c_sda i2c_scl led[*] seg_sel[*] seg_data[*]}]

create_clock - определяет такт с периодом 20 нс (50 МГц). Quartus будет проверять, что вся внутренняя логика укладывается в этот период.

set_false_path - говорит анализатору: “не проверяй тайминг на этих путях”. Это допустимо потому что:

  • I2C работает на 100 кГц - в 500 раз медленнее тактовой. Любая задержка IO незначительна.

  • Кнопки - асинхронные входы, переключаются раз в секунды.

  • LED и дисплей - выходы для человеческого глаза, обновляются с частотой ~200 Гц.

Без set_false_path Quartus мог бы выдать ложные ошибки тайминга на этих путях.

Сборка и загрузка прошивки

Теперь необходимо открыть Quartus. Открывам проект: File → Open Project → выбрать quartus/i2c_test.qpf. Запускаем компиляцию: Processing → Start Compilation (или Ctrl+L). Компиляция проходит 4 этапа:

  • Analysis & Synthesis: Verilog-код преобразуется в таблицу соединений (netlist) из логических элементов, регистров и памяти Cyclone IV.

  • Fitter: Каждый элемент нетлиста размещается в конкретной ячейке FPGA, и между ними прокладываются межсоединения.

  • Assembler: Генерируется файл прошивки output_files/i2c_test_top.sof.

  • TimeQuest: Проверяется, что все пути укладываются в заданные временные ограничения.

Загружаем в FPGA: Tools → Programmer. Подключаем плату через USB-Blaster к ПК. Нажимаем Start. Через ~2 секунды осуществляется прошивка в FPGA и происходит запуск нашего кода.

Помимо этого можно сделать загрузку через консоль:

LIB=$(find ~/altera/25.1std/quartus/linux64 -name libdb_asgn.so -print -quit)
LIBDIR=$(dirname "$LIB")
export LD_LIBRARY_PATH="$LIBDIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"

export PATH=$PATH:/home/megalloid/altera/25.1std/quartus/linux64

cd ~/sources/I2C_Master_Controller/quartus/

# Полная компиляция (синтез + размещение + сборка + тайминг)
quartus_sh --flow compile i2c_test_top

# Загрузка .sof в FPGA через JTAG
quartus_pgm -m jtag -o "P;output_files/i2c_test_top.sof"

Командная строка удобна для автоматизации и повторяемости. По итогу получаем следующий набор файлов:

  • output_files/i2c_test_top.sof - размер ~270 КБ - прошивка SRAM (загружается через JTAG, теряется при выключении);

  • output_files/i2c_test_top.pof - размер ~270 КБ - прошивка для заливки в Flash (сохраняется при выключении);

  • output_files/i2c_test_top.fit.summary - отчёт о ресурсах (сколько LE, памяти, пинов использовано)

  • output_files/i2c_test_top.sta.summary - отчёт тайминга (Fmax, slack)

Итоговый расход ресурсов:

Ресурс

Использовано

Доступно

Процент

Logic Elements

549

6 272

~10%

Registers

276

6 272

~5%

Memory bits

0

276 480

0%

IO pins

23

180

12%

Проект занимает около 10% ресурсов FPGA - маленькая и простая конструкция, оставляющая 90% ресурсов свободными.

Работа на плате - что ожидаем увидеть

Этап 1. Включение. После загрузки прошивки (или подачи питания, если прошивка во Flash). Все шесть разрядов показывают дефисы. Светодиоды погашены. Контроллер в состоянии ST_IDLE - ждёт нажатия кнопки.

После нажатия на KEY1 - происходит запуск тестов. 

Тест 1 (запись 0xA5 в адрес 0x00):
  Шаг «running»:   1 — A 5 — —      (~90 мс — время I2C транзакции)
  Шаг «delay»:     (не виден)        (6 мс — ожидание записи EEPROM)
  Шаг «read»:      (не виден)        (~90 мс — чтение обратно)
  Шаг «result»:    1 P A 5 A 5       (500 мс — отображение результата)
  LED:              ● · · ·

Тест 2 (запись 0x5A в адрес 0x01):
  result:           2 P 5 A 5 A
  LED:              ● ● · ·

Тест 3 (запись 0xFF в адрес 0x10):
  result:           3 P F F F F
  LED:              ● ● ● ·

Тест 4 (запись 0x00 в адрес 0x11):
  result:           4 P 0 0 0 0
  LED:              ● ● ● ●

Итог:
  summary:          P 4   F 0
  LED:              ● ● ● ●

Каждый тест отображается ~500 мс (параметр SHOW_TICKS = 25_000_000 при 50 МГц). Полный цикл из 4 тестов занимает ~4 × (0.18 + 0.006 + 0.18 + 0.5) ≈ 3.5 секунды.

Чтобы повторить заново после отображения итогов (P4 F0) - нажмите KEY1 снова. Контроллер вернётся в ST_IDLE, погасит LED, покажет 6 прочерков и начнёт тесты заново. Нажатие кнопки RESET немедленно возвращает всё в начальное состояние в т.ч. индикацию на дисплее, все LED погашены, I2C шина отпущена.

Посмотрим что на логическом анализаторе

Тест 1. Выглядит как и полагается. Первым этапом производится запись значения 0xA5 в адрес ячейки 0x00. Судя по трем ACK - успешно: 

Производим контрольное чтение:

Тест 2. Запись 0х5А в адрес 0х01:

И делаем контрольное чтение:

Тест 3. Запись 0xFF по адресу 0x10

И производим контрольное чтение:

И заключительный тест 4. Записываем 0x00 в ячейку 0x11:

С контрольным чтением:

О том же успехе нам сообщает и сама плата: 

Отладка типичных проблем

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

Если все тесты FAIL:

Возможная причина

Диагностика

Решение

EEPROM не установлена или повреждена

Дисплей показывает EE в позициях прочитанных данных (маркер ошибки NACK)

Проверить наличие 24LC04 на плате

Неправильные пины I2C

EEPROM не отвечает (NACK)

Проверить .qsf — пины D1 (SCL) и E6 (SDA)

Отсутствуют pull-up резисторы

SDA/SCL "висят" в неопределённом состоянии

На AX301 они распаяны; если используете внешнюю EEPROM — добавить 4.7 кОм к 3.3V

Неправильный адрес EEPROM

EEPROM другого типа (не 24LC04 или другой блок)

Изменить SLAVE_W/SLAVE_R в i2c_test_ctrl.v

Дисплей не светится:

Возможная причина

Решение

Неверная полярность

Плата AX301 v1 и v2 могут отличаться; проверить схему

Неправильные пины seg_sel /seg_data

Перепроверить .qsf по схеме платы

Частота сканирования слишком низкая

Увеличить SCAN_FREQ (по умолчанию 200 — достаточно)

Нестабильные результаты, иногда PASS, иногда FAIL:

Возможная причина

Решение

Слишком малая задержка после записи

Увеличить WR_TICKS (по умолчанию 300 000 = 6 мс)

Помехи на I2C шине

Укоротить провода, проверить pull-up

Проблемы с питанием EEPROM

Проверить напряжение

Помимо этого для отладки можно использовать SignalTap. Если базовая отладка по дисплею и LED недостаточна, Quartus предоставляет встроенный логический анализатор SignalTap II. Он позволяет захватить реальные сигналы внутри FPGA и просмотреть их в виде осциллограмм - аналог GTKWave, но для реального железа:

  1. Tools → SignalTap II Logic Analyzer

  2. Добавить сигналы: scl_oen, sda_oen, i2c_scl, i2c_sda, core_cmd, core_ready, u_ctrl.state

  3. Настроить триггер: например, core_cmd_valid == 1

  4. Перекомпилировать проект (SignalTap добавляет логику захвата в FPGA)

  5. Запустить захват и нажать KEY1

  6. Просмотреть осциллограмму реальных сигналов

SignalTap использует внутреннюю BRAM FPGA для хранения захваченных данных и JTAG для передачи на PC. Глубина захвата ограничена объемом доступной памяти. Глубоко не буду разбирать этот вопрос в рамках этой статьи.

В качестве заключения

На первом шаге проекта мы написали и верифицировали i2c_master_core в симуляции. На этом, втором шаге, мы:

  1. Выбрали аппаратную платформу - плата ALINX AX301 с Cyclone IV FPGA и EEPROM 24LC04 на борту.

  2. Спроектировали тестовую оболочку из пяти модулей:

    1. Прескалер для генерации 100 кГц SCL из 50 МГц тактовой

    2. Debounce для надёжного считывания кнопки

    3. I2C ядро (без изменений из rtl/)

    4. FSM контроллер тестов: 4 теста записи-чтения EEPROM

    5. Мультиплексор дисплея для 6-разрядного семисегментного индикатора

  3. Создали Quartus проект с привязкой пинов, IO стандартами и временными ограничениями.

  4. Получили прошивку, которая при загрузке в FPGA запускает автоматические тесты I2C по нажатию кнопки и показывает результат на дисплее и светодиодах.

Два вида верификации дополняют друг друга. Симуляция находит логические ошибки быстро и дёшево. Аппаратный тест подтверждает, что логика работает в реальных физических условиях. Следующий шаг - сделаем потоковую запись в дисплей OLED с контроллером SSD1306 и проверим работоспособность burst-записи с использованием нашего кода.


Размещайте облачную инфраструктуру и масштабируйте сервисы с надежным облачным провайдером Beget.

Эксклюзивно для читателей Хабра мы даем бонус 10% при первом пополнении.

Воспользоваться

Воспользоваться