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

推荐订阅源

小众软件
小众软件
N
News and Events Feed by Topic
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
The Cloudflare Blog
H
Heimdal Security Blog
Schneier on Security
Schneier on Security
Engineering at Meta
Engineering at Meta
Google Online Security Blog
Google Online Security Blog
宝玉的分享
宝玉的分享
AI
AI
The GitHub Blog
The GitHub Blog
MongoDB | Blog
MongoDB | Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
The Last Watchdog
The Last Watchdog
T
Troy Hunt's Blog
S
Security @ Cisco Blogs
H
Hacker News: Front Page
F
Fortinet All Blogs
博客园_首页
S
Secure Thoughts
N
News and Events Feed by Topic
P
Proofpoint News Feed
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
Spread Privacy
Spread Privacy
Hacker News - Newest:
Hacker News - Newest: "LLM"
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Hugging Face - Blog
Hugging Face - Blog
Hacker News: Ask HN
Hacker News: Ask HN
C
CXSECURITY Database RSS Feed - CXSecurity.com
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
L
LINUX DO - 最新话题
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Schneier on Security
Know Your Adversary
Know Your Adversary
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Scott Helme
Scott Helme
P
Privacy & Cybersecurity Law Blog
S
Securelist
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
O
OpenAI News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
PCI Perspectives
PCI Perspectives
L
LangChain Blog
雷峰网
雷峰网
Security Archives - TechRepublic
Security Archives - TechRepublic
V2EX - 技术
V2EX - 技术

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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. Тестируем ядро
andreyzaostr · 2026-05-08 · via Все публикации подряд на Хабре

По результатам написания прошлой статьи у нас получился объемный модуль для реализации функций низкоуровневого управления шиной I2C, который формирует управление линиями SCL/SDA, поддерживает мониторинг шины, ведет передачу и прием данных. В этой статье я предлагаю организовать полноценное вдумчивое тестирование всего что получилось.

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

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

Зачем тестировать и что такое тестбенч?

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

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

Поэтому мы моделируем всё окружение заранее программно - в так называемом тестбенче (testbench, сокращённо TB). Тестбенч - это модуль-обёртка, который существует только для целей моделирования. Он не синтезируется в реальное железо. Его задача - подать на входы тестируемого модуля (DUT - Design Under Test) нужные сигналы, дождаться ответов на выходах, и проверить, совпадают ли они с ожиданием.

Тестбенч для i2c_master_core состоит из целого ряда компонентов:

  1. Генератор клока и ena - создаёт тактовый сигнал clk и разрешающий импульс ena_i

  2. Управляющая логика - initial-блок с 10 тестовыми сценариями

  3. DUT - сам i2c_master_core

  4. Две модели slave-устройств - обычный на адресе 0x50 и slave с clock stretching на адресе 0x51

  5. SCL-hold логика - удерживает SCL в LOW для имитации clock stretching

  6. ext_sda_drive - внешний драйвер SDA для имитации потери арбитража;

Выдает результат - вердикт PASS или FAIL для каждого сценария, плюс файл осциллограмм (VCD), который можно открыть в GTKWave и увидеть каждый сигнал по тактам.

В модуль i2c_master_core мы будем подавать прямые команды, сигналы и смотреть, все ли работает корректно:

  1. Single WRITE + ACK

  2. Single READ + NACK (write 0xA5, read back)

  3. Full transaction START→WRITE→WRITE→STOP, проверка busy_o

  4. Repeated START (RESTART), чтение через RESTART

  5. NACK от slave (неправильный адрес) + восстановление

  6. Clock stretching (через отдельный slave на адресе 0x51)

  7. Arbitration lost (внешний интерферер на SDA) + блокировка + сброс

  8. Reset посередине передачи + проверка работоспособности после

  9. CMD_NOP (не меняет состояние)

  10. Последовательное чтение 4 байт (ACK, ACK, ACK, NACK)

Все просто и понятно. Идем дальше.

Исходники, описание и текущие наработки лежат в этом репозитории: https://github.com/megalloid/I2C_Master_Controller

Инструменты которыми воспользуемся. Icarus Verilog

Опишу, то, какими инструментами мы будем пользоваться, при верификации i2c_master_core. Для каждого инструмента: что он делает, как устроен внутри, как установить, как запускать, какие ключи важны, и какие грабли поджидают.

Первый из них -  Icarus Verilog (iverilog + vvp) - симулятор. Icarus Verilog - open-source симулятор языков Verilog и SystemVerilog. Это наш основной инструмент для запуска тестбенчей. Он состоит из двух программ:

  • iverilog - компилятор: читает .v / .sv файлы и собирает бинарный файл .vvp

  • vvp - runtime-движок: исполняет .vvp и генерирует вывод в консоль + файлы осциллограмм (VCD)

Устанавливается очень просто (Ubuntu):

sudo apt install iverilog

Можно установить из исходников:

git clone https://github.com/steveicarus/iverilog.git
cd iverilog
sh autoconf.sh
./configure --prefix=/usr/local
make -j$(nproc)
sudo make install

Полная команда для нашего ядра:  

iverilog -g2012 -Wall -o sim/i2c_core_tb.vvp \
    rtl/i2c_master_core.v \
    tb/i2c_slave_model.sv \
    tb/i2c_core_tb.sv

Будем использовать со следующими ключами:

  • -g2012 - определяет стандарт IEEE 1800-2012 (SystemVerilog). Наши тестбенчи используют logic, always_ff, именованные блоки, begin : label и другие SV-конструкции;

  • -Wall - выводим все предупреждения. Ловит неподключённые порты, несовпадения ширин, неиспользуемые сигналы и т.п;

  • -o <file> - Выходной файл .vvp. Указывает куда положить скомпилированный байткод;

Порядок файлов важен: RTL перед тестбенчем, иначе компилятор может не найти модули, на которые ссылается тестбенч.

Далее можно запускать скомпилированный .vpp. Ключи:

  • -vcd - Принудительно включить VCD-дамп (даже если $dumpfile/$dumpvars не вызваны в коде);

  • -lxt2 - Дамп в формате LXT2 (компактнее VCD);

Команда: 

cd sim && vvp ../sim/i2c_core_tb.vvp

Отдельно отмечу, что в коде $dumpfile("i2c_core_tb.vcd") в тестбенче создаёт файл относительно текущей директории. Если запускать из корня проекта, VCD окажется в корне, а не в sim/. 

Но помимо этого стоит держать в голове, что Icarus Verilog имеет ряд ограничений:

  • Поддержка SystemVerilog неполная: нет interface, class, randomize, covergroup, assertion с property. Для нашего проекта это не проблема - мы используем только процедурный стиль.

  • Производительность ниже коммерческих симуляторов (Questa, VCS) в 10-100 раз на больших дизайнах. Но для нашего ядра (~700 строк RTL) это будет незаметно.

  • Нет встроенного wave-viewer - нужен GTKWave или аналог.

Инструменты которыми воспользуемся. Verilator

Следующий инструмент - это Verilator. Это open-source инструмент двойного назначения, выполняющий статический анализ Verilog/SystemVerilog без симуляции, и который может компилировать RTL в C++/SystemC для очень быстрой симуляции.

В нашем проекте мы будем использовать только lint-режим. Verilator проверяет синтаксис, стилистику, ширины сигналов, мёртвый код и десятки других потенциальных проблем, но без запуска симуляции.

Устанавливается тоже очень просто:

sudo apt install verilator

Можно установить из исходников:

git clone https://github.com/verilator/verilator.git
cd verilator
git checkout stable
autoconf
./configure --prefix=/usr/local
make -j$(nproc)
sudo make install

Полная команда для нашего ядра:  

verilator --lint-only -Wall -Wno-UNUSEDSIGNAL --top-module i2c_master_core \
    rtl/i2c_master_core.v

Будем использовать со следующими ключами:

  • --lint-only - только проверка, без генерации C++

  • -Wall - включить все предупреждения

  • -Wno-UNUSEDSIGNAL - подавить предупреждения о неиспользуемых сигналах (часто ложные в RTL с конфигурируемыми параметрами)

  • --top-module <name> - явно указать top-level модуль (иначе Verilator пытается угадать и может ошибиться)

Есть список ошибок, которые ловит Verilator, а Icarus - нет. Например, несовпадение ширин при присваивании, комбинаторные петли, неиспользуемые биты, знаковые ошибки и так далее. Перед каждой симуляцией лучше всего запускать линтер:

make lint-core && make sim-core

Инструменты которыми воспользуемся. GTKWave

Что такое GTKWave? Это open-source вьюер файлов осциллограмм (VCD, LXT, LXT2, FST, GHW). Это графическое приложение, в котором можно рассмотреть каждый сигнал по тактам - аналог логического анализатора, только для симуляции.

Устанавливается очень просто:

sudo apt install gtkwave

Запускается просто: 

gtkwave sim/i2c_core_tb.vcd

Если VCD-файл ещё не создан - сначала выполните make sim-core. После запуска откроется окно с тремя основными панелями:

Пошаговые действия:

  1. Добавить сигналы. В левой панели (SST - Signal Search Tree) раскройте иерархию: i2c_core_tb → dut → интересующие сигналы. Выделите сигнал и нажмите Append (или перетащите мышью в область диаграмм).

  2. Навигация по времени:

    1. Колёсико мыши - масштаб (zoom in/out)

    2. Средняя кнопка (drag) - перемещение по времени

    3. Клавиши + / - - zoom in / zoom out

    4. Ctrl+Home / Ctrl+End - начало / конец симуляции

  3. Маркеры. Клик левой кнопкой в области диаграмм ставит основной маркер (жёлтая вертикальная линия). Время маркера показано в статусной строке. Это удобно для измерения длительности - поставьте маркер на начало бита, затем на конец, и посмотрите разницу.

  4. Формат отображения. Правый клик на имени сигнала → Data Format:

    1. Hex - для данных (tx_shift_r, dout_o)

    2. Unsigned Decimal - для счётчиков (bit_cnt_r, phase_r)

    3. Binary - для побитового анализа

    4. ASCII - для отладки строковых данных

  5. Группировка. Выделите несколько сигналов → правый клик → Combine Down - объединение в группу. Удобно для шины (SDA + SCL) или FSM (state + phase + bit_cnt).

  6. Поиск переходов. Выберите сигнал, затем:

    1. → (стрелка вправо) - следующий переход (фронт)

    2. ← (стрелка влево) - предыдущий переход

    3. Быстрый поиск конкретного значения: Edit → Find Value → введите значение

  7. Сохранение конфигурации. После настройки сигналов: File → Write Save File → core_debug.gtkw. В следующий раз откройте так: gtkwave sim/i2c_core_tb.vcd core_debug.gtkw

GTKWave восстановит все добавленные сигналы, их порядок, форматы и масштаб.

Make оркестрация

Для упрощения жизни и избавления себя от необходимости запомнинания длинных команд я использую make. Одна команда - один осмысленный шаг:

make sim-core       # Скомпилировать и запустить тесты ядра
make lint-core      # Lint ядра через Verilator
make wave-core      # Скомпилировать, запустить, подсказать как открыть VCD
make sim            # Все симуляции
make lint           # Все lint-проверки
make clean          # Удалить все артефакты

В Makefile инструменты могут задаваться через ?= - можно переопределить извне:

# Использовать другой путь к iverilog
IVERILOG=/opt/iverilog-13/bin/iverilog make sim-core

# Использовать другой симулятор Questa
VSIM=/opt/questa/2024.1/bin/vsim make questa

Типовой workflow:

# 1. Lint - проверить синтаксис за <1 секунды
make lint-core

# 2. Симуляция - запустить тесты
make sim-core

# 3. Если FAIL - открыть осциллограммы
gtkwave sim/i2c_core_tb.vcd

# 4. Исправить RTL, повторить с шага 1

# 5. Всё зелёное - очистить артефакты
make clean

Все для вашего удобства.

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

Возвращаемся к тестированию нашего модуля. 

Интерфейс - наш “пульт управления” модулем

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

Команды

Пять команд которые будем посылать на cmd_i:

  1. CMD_NOP (3'd0) - Не должно ничего происходить (игнорируется) 

  2. CMD_START (3'd1) - Генерирует START-условие на шине;

  3. CMD_WRITE (3'd2) - Передаёт 8 бит из din_i, принимает ACK/NACK от slave

  4. CMD_READ (3'd3) - Принимает 8 бит от slave, отправляет ACK или NACK (зависит от din_i[0])

  5. CMD_STOP (3'd4) - Генерирует STOP-условие

  6. CMD_RESTART (3'd5) - Генерирует повторный START (без промежуточного STOP)

Сигнал ena_i - источник тактирования фаз SCL

Это ключевой сигнал, который определяет скорость I2C. Ядро продвигает свой автомат только на тактах, когда приходит импульсena_i = 1. На всех остальных тактах ядро “стоит”. Каждый бит на I2C шине занимает 4 такта ena_i (4 фазы). Один байт - 9 бит (8 данных + 1 ACK/NACK) = 36 тактов ena_i. Реальный период SCL = 4 x период ena_i

В конечном устройстве ena_i будет генерироваться прескалером - делителем частоты. Но мы ещё не написали прескалер. 

На этом этапе мы создадим простой счётчик прямо в тестбенче:

localparam ENA_DIV = 4;   // ena каждые 4 такта clk

reg [7:0] ena_cnt;
always @(posedge clk or negedge rstn) begin
    if (!rstn) begin
        ena_cnt <= 0;
        ena     <= 0;
    end else begin
        if (ena_cnt == ENA_DIV - 1) begin
            ena_cnt <= 0;
            ena     <= 1;         // Один тик!
        end else begin
            ena_cnt <= ena_cnt + 1;
            ena     <= 0;
        end
    end
end

При clk = 100 МГц и ENA_DIV = 4 получаем ena каждые 40 нс. Период SCL = 4 x 40 нс = 160 нс. Это быстрее реальных 100 кГц (10 мкс), но для моделирования это удобно - тесты завершаются за микросекунды.

Протокол рукопожатия (handshake)

Управление ядром работает по простой схеме.

Это классический ready/valid протокол. Преимущество: тестбенч не привязан к тайминг-деталям ядра. Он просто ждёт ready_o и подаёт следующую команду.

Каркас тестбенча

Для формирования базового каркаса для последующих тестов объявим несколько параметров.

module i2c_core_tb;

// ----- Parameters -----

localparam CLK_PERIOD = 10;                   // 100 МГц
localparam ENA_DIV    = 4;                    // ena каждые 4 такта
localparam [6:0] SLAVE_ADDR     = 7'h50;      // Обычный slave
localparam [6:0] SLAVE_ADDR_STR = 7'h51;      // Slave с clock stretching
localparam       STRETCH_CYCLES = 80;         // Сколько тактов clk держать SCL
localparam       TIMEOUT_LIMIT  = 200_000;    // Защита от зависания

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;
  • Два адреса slave: обычный 0x50 для тестов 1-5, 7-10 и slave с clock stretching 0x51 для теста 6.

  • TIMEOUT_LIMIT: все ожидания в task-ах защищены таймаутом. Если ядро «зависнет», тест не застрянет навечно - через 200 000 тактов выдаст FAIL.

  • STRETCH_CYCLES = 80: slave удерживает SCL на 80 тактов clk (= 800 нс при 100 МГц).

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

    // ----- Signals -----
    reg        clk, rstn;
    reg        ena;
    reg        cmd_valid;
    reg  [2:0] cmd;
    reg  [7:0] din;
    wire [7:0] dout;
    wire       rx_ack, ready;
    wire       arb_lost, busy;
    reg        arb_lost_clear;
    wire       scl_oen, sda_oen;

Далее всё, что касается I2C шины и open-drain выводов:

    // ----- I2C bus with pull-ups -----
    wire sda, scl;
    pullup (sda);
    pullup (scl);

    assign scl = scl_oen ? 1'bz : 1'b0;
    assign sda = sda_oen ? 1'bz : 1'b0;

    // External interferer for arbitration-lost test
    reg ext_sda_drive;
    assign sda = (ext_sda_drive) ? 1'b0 : 1'bz;

    wire scl_i = scl;
    wire sda_i = sda;

На шине sda работают три устройства:

  1. Мастер (DUT): scl_oen/sda_oen

  2. Slave-модели: внутренний sda_out_en

  3. Интерферер: ext_sda_drive

Все три - open-drain (тянут к 0 или отпускают). Результирующее значение на шине - wired-AND: если хотя бы один тянет к 0, шина = 0.

Основное системное тактирование:

    // ----- Clock -----
    initial clk = 0;
    always #(CLK_PERIOD/2) clk = ~clk;

ENA-генератор: 

    // ----- ENA generator -----
    reg [7:0] ena_cnt;
    always @(posedge clk or negedge rstn) begin
        if (!rstn) begin
            ena_cnt <= 0;
            ena     <= 0;
        end else begin
            if (ena_cnt == ENA_DIV - 1) begin
                ena_cnt <= 0;
                ena     <= 1;
            end else begin
                ena_cnt <= ena_cnt + 1;
                ena     <= 0;
            end
        end
    end

Подключаем DUT:

    // ----- DUT -----
    i2c_master_core dut (
        .clk_i            (clk),
        .rstn_i           (rstn),
        .ena_i            (ena),
        .cmd_valid_i      (cmd_valid),
        .cmd_i            (cmd),
        .din_i            (din),
        .dout_o           (dout),
        .rx_ack_o         (rx_ack),
        .ready_o          (ready),
        .arb_lost_o       (arb_lost),
        .arb_lost_clear_i (arb_lost_clear),
        .busy_o           (busy),
        .scl_i            (scl_i),
        .scl_oen_o        (scl_oen),
        .sda_i            (sda_i),
        .sda_oen_o        (sda_oen)
    );

Подключаем несколько slave-устройства (их возьмите в репозитории):

    // ----- Normal slave (addr 0x50) -----
    i2c_slave_model #(.I2C_ADDR(SLAVE_ADDR)) slave (
        .sda_io (sda),
        .scl_io (scl)
    );

    // ----- Stretching slave (addr 0x51) -----
    i2c_slave_model #(.I2C_ADDR(SLAVE_ADDR_STR)) slave_str (
        .sda_io (sda),
        .scl_io (scl)
    );

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

Далее блок касающийся логики clock stretching. Clock stretching реализован прямо в тестбенче, а не внутри slave-модели:

    // SCL-hold logic for stretching slave
    reg scl_hold;
    assign scl = scl_hold ? 1'b0 : 1'bz;

    integer stretch_cnt;
    initial begin
        scl_hold    = 0;
        stretch_cnt = 0;
    end

    always @(negedge scl) begin
        if (slave_str.state == 4'd2 ||   // S_ADDR_ACK
            slave_str.state == 4'd4 ||   // S_REG_ACK
            slave_str.state == 4'd6) begin // S_WR_ACK
            scl_hold    <= 1;
            stretch_cnt <= STRETCH_CYCLES;
        end
    end

    always @(posedge clk) begin
        if (scl_hold && stretch_cnt > 0)
            stretch_cnt <= stretch_cnt - 1;
        else if (scl_hold && stretch_cnt == 0)
            scl_hold <= 0;
    end

Когда slave_str переходит в состояние ACK (после адреса, регистра или данных), мы захватываем SCL - тянем к 0 на STRETCH_CYCLES тактов. Мастер в это время пытается отпустить SCL, но не может из-за логики wired-AND. Через 80 тактов мы отпускаем SCL, мастер видит scl_i = 1 и продолжает свою работу. Мы подглядываем во внутренний state slave-модели через иерархический путь slave_str.state. Это допустимо в тестбенче - мы не синтезируем этот код.

Введем также счетчики для подсчета успешно пройденных и заваленных тестов:

    // ----- Counters -----
    integer pass_cnt, fail_cnt;

Далее формируем вспомогательные task-и:

    // =====================================================================
    // Helper tasks
    // =====================================================================

    task test_pass(input [80*8-1:0] msg);
        begin
            $display("  PASS: %0s", msg);
            pass_cnt = pass_cnt + 1;
        end
    endtask

    task test_fail(input [80*8-1:0] msg);
        begin
            $display("  FAIL: %0s", msg);
            fail_cnt = fail_cnt + 1;
        end
    endtask

Счётчики pass_cnt и fail_cnt инкрементируются при каждой проверке. В конце теста выводится итог: PASS=N FAIL=M.

Базовый task отправки команды — с защитой от зависания:

    task send_cmd(input [2:0] c, input [7:0] d);
        integer wcnt;
        begin
            @(posedge clk);
            wcnt = 0;
            while (!ready) begin				// 1. Ждём готовности
                @(posedge clk);
                wcnt = wcnt + 1;
                if (wcnt > TIMEOUT_LIMIT) begin
                    test_fail("TIMEOUT waiting for ready before cmd");
                    disable send_cmd;
                end
            end
            cmd       <= c;					// 2. Выставляем команду
            din       <= d;
            cmd_valid <= 1;
            @(posedge clk);
            wcnt = 0;
            while (ready) begin				// 3. Ждём, пока ядро примет
                @(posedge clk);
                wcnt = wcnt + 1;
                if (wcnt > TIMEOUT_LIMIT) begin
                    test_fail("TIMEOUT: ready never fell");
                    cmd_valid <= 0;
                    disable send_cmd;
                end
            end
            cmd_valid <= 0;					// 4. Снимаем запрос
            cmd       <= CMD_NOP;
            wcnt = 0;
            while (!ready) begin				// 5. Ждём завершения
                @(posedge clk);
                wcnt = wcnt + 1;
                if (wcnt > TIMEOUT_LIMIT) begin
                    test_fail("TIMEOUT waiting for ready after cmd");
                    disable send_cmd;
                end
            end
        end
    endtask

Каждое ожидание защищено: если ready не изменится за TIMEOUT_LIMIT тактов, task выходит через disable с сообщением FAIL. Без этого баг в ядре мог бы превратить тест в бесконечный цикл.

Добавим обертки:

    task do_start;
        begin send_cmd(CMD_START, 8'd0); end
    endtask

          
    task do_stop;
        begin send_cmd(CMD_STOP, 8'd0); end
    endtask

          
    task do_restart;
        begin send_cmd(CMD_RESTART, 8'd0); end
    endtask

          
    task do_write(input [7:0] data, output ack);
        begin
            send_cmd(CMD_WRITE, data);
            ack = rx_ack;
        end
    endtask

          
    task do_read(input nack_bit, output [7:0] data);
        begin
            send_cmd(CMD_READ, {7'd0, nack_bit});
            data = dout;
        end
    endtask

Задаем каркас который будем наполнять тестами:

    // =====================================================================
    // Main test sequence
    // =====================================================================
    initial begin

        $dumpfile("i2c_core_tb.vcd");
        $dumpvars(0, i2c_core_tb);

        pass_cnt = 0;
        fail_cnt = 0;
        ext_sda_drive = 0;

        rstn           = 0;
        cmd_valid      = 0;
        cmd            = CMD_NOP;
        din            = 8'd0;
        arb_lost_clear = 0;

        repeat (20) @(posedge clk);
        rstn = 1;
        repeat (20) @(posedge clk);

	    // =============================================================
        // TEST ...
        // =============================================================

    end

И сделаем завершающий блок с подведением итогов и watchdog-секции: 

        // =============================================================
        // SUMMARY
        // =============================================================
        $display("\n========================================");
        $display("  TEST SUMMARY:  PASS=%0d  FAIL=%0d", pass_cnt, fail_cnt);
        if (fail_cnt == 0)
            $display("  All tests PASSED");
        else
            $display("  *** FAILURES DETECTED ***");
        $display("========================================\n");

        $finish;
    end

Добавляем Watchdog. Если вся симуляция займёт больше 200_000 x 10 x 20 = 40 000 000 000 пс = 40 мс модельного времени, watchdog принудительно завершит её. Это защита от бесконечных циклов.

    // Watchdog
    initial begin
        #(TIMEOUT_LIMIT * CLK_PERIOD * 20);
        $display("WATCHDOG: simulation timeout");
        $finish;
    end

endmodule

Перейдем к составлению перечня тестовых кейсов. 

Тест 1. Single WRITE + ACK

Цель: Самый первый и самый простой тест. Убедиться, что ядро может передать один байт и услышать ACK от slave.

Что происходит внутри ядра по тактам: Когда мы подаём CMD_WRITE с din_i = 0xA0 (адресный байт: slave 0x50 + бит записи):

Такт 0 (ena): Ядро защёлкивает команду

    tx_shift_r <= {0xA0, 1'b0} = 9'b_1_0100_000_0
    bit_cnt_r  <= 0
    state_r    <= ST_DATA
    ready_o    <= 0 ← “Я занят”

Далее 9 бит-слотов × 4 фазы = 36 тактов ena:

Бит 0 (MSB = 1):
  Фаза 0: SCL=0, SDA=tx_shift_r[8]=1  (отпустил)
  Фаза 1: SCL=1, семплирование sda_i
  Фаза 2: SCL=1, удержание
  Фаза 3: SCL=0, сдвиг регистра, bit_cnt_r <= 1

...

Бит 8 (ACK-слот):
  Фаза 0: SCL=0, sda_input_mode=1 → sda_oen_o=1 (отпускаем SDA для slave)
  Фаза 1: SCL=1, семплируем sda_i → rx_shift_r[0]
  Фаза 3: bit_cnt_r == 8 → rx_ack_o <= rx_shift_r[0], ready_o <= 1

Итоговый тест получается следующий:

        // =============================================================
        // TEST 1: Single WRITE + ACK
        // =============================================================
        $display("\n=== TEST 1: Single WRITE + ACK ===");
        begin : test1
            reg ack;
            do_start;
            do_write({SLAVE_ADDR, 1'b0}, ack);

            if (ack == 1'b0)
                test_pass("Slave ACK received (rx_ack_o = 0)");
            else
                test_fail("Expected ACK, got NACK");

            do_stop;
        end

Получается такая осциллограмма:

Необходимо обращать внимание на осциллограмме на следующее:

  • dut.state_r - 0=IDLE, 2=DATA

  • dut.phase_r - 0→1→2→3 в каждом бите

  • dut.bit_cnt_r - 0…8

  • dut.tx_shift_r - сдвиговый регистр, убывает побитно

  • sda, scl - сигналы на шине

  • dut.rx_ack_o - 0=ACK, 1=NACK

  • dut.ready_o - 0 во время работы, 1 по завершении

Что может пойти не так? 

  1. rx_ack_o = 1 (NACK) - sda_input_mode не активируется на 9-м бите → ядро само держит SDA=1

  2. Тайм-аут (ready не поднимается) - bit_cnt_r не доходит до 8

  3. Данные на шине неправильные - tx_shift_r сдвигается не в ту сторону

Тест 2. Single READ + NACK

Цель: Убедиться, что ядро корректно принимает 8 бит данных от slave и отправляет NACK.

Ключевая механика: 

  • При WRITE ядро передаёт (управляет SDA 8 бит, слушает 1 бит ACK);

  • При READ ядро принимает (слушает SDA 8 бит, управляет 1 бит ACK/NACK);

Переключение определяется проводом sda_input_mode:

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)
);

tx_shift_r для READ инициализируется как {8'hFF, din_i[0]} - все единицы (отпускаем SDA) + бит ACK/NACK от мастера. din_i[0] = 1 → NACK, din_i[0] = 0 → ACK.

Сначала записываем 0xA5 в ячейку 0x10, потом читаем обратно:

        // =============================================================
        // TEST 2: Single READ + NACK (write 0xA5, read back)
        // =============================================================
        $display("\n=== TEST 2: Single READ + NACK ===");
        begin : test2
            reg ack;
            reg [7:0] rdata;

            do_start;
            do_write({SLAVE_ADDR, 1'b0}, ack);
            do_write(8'h10, ack);
            do_write(8'hA5, ack);
            do_stop;

            repeat (50) @(posedge clk);

            do_start;
            do_write({SLAVE_ADDR, 1'b0}, ack);
            do_write(8'h10, ack);
            do_restart;
            do_write({SLAVE_ADDR, 1'b1}, ack);
            do_read(1'b1, rdata);
            do_stop;

            if (rdata === 8'hA5)
                test_pass("Read 0xA5 matches written value");
            else
                test_fail("Read mismatch");
        end

Тут есть одна неочевидная деталь. tx_shift_r[8] одновременно означает и “значение бита на SDA” и “output enable”. Это работает благодаря инверсной логике open-drain: “хочу передать 1” = “не тяну линию” = oen = 1. Для NACK: din_i[0] = 1 → oen = 1 → pull-up → SDA = 1 → NACK.

Тест 3. Полная транзакция START + WRITE addr + WRITE data + STOP.

Цель: Проверить полный цикл записи, отслеживая переходы FSM и флаг busy_o.

Что проверяем: 

  • busy_o устанавливается после START и сбрасывается после STOP

  • Между командами в IDLE ядро не отпускает линии при busy_o = 1

  • Оба байта (адрес + данные) получают ACK от slave

Критичный момент: IDLE между командами

ST_IDLE: begin
    if (cmd_valid_i && !arb_lost_o) begin
        ...
    end else if (!busy_o) begin
        scl_oen_o <= 1'b1;    // Отпускаем только если шина свободна
        sda_oen_o <= 1'b1;
    end
    // busy_o=1 → линии не трогаем!
end

Если бы ядро отпустило SDA при SCL=1 внутри транзакции, slave увидел бы ложный STOP.

Код теста:

// =============================================================
// TEST 3: Full transaction START + WRITE + WRITE + STOP
// =============================================================
$display("\n=== TEST 3: Full transaction ===");
begin : test3
    reg ack1, ack2;

    do_start;

    if (busy !== 1'b1)
        test_fail("busy_o should be 1 after START");

    do_write({SLAVE_ADDR, 1'b0}, ack1);
    if (ack1 !== 1'b0)
        test_fail("Expected ACK on address byte");

    do_write(8'h42, ack2);
    if (ack2 !== 1'b0)
        test_fail("Expected ACK on data byte");

    do_stop;

    repeat (10) @(posedge clk);
    if (busy !== 1'b0)
        test_fail("busy_o should be 0 after STOP");
    else
        test_pass("Full transaction OK, busy cleared");
end

Тест 4. Repeated START (RESTART)

Цель: Проверить генерацию повторного START без освобождения шины.

Напомню, чем RESTART отличается от START:

RESTART:                              START:
  Фаза 0: SDA=1, SCL=0                  Фаза 0: SDA=1, SCL=1 (ждём scl_i)
  Фаза 1: SDA=1, SCL=1 (ждём)           Фаза 1: SDA=1, SCL=1 (удержание)
  Фаза 2: SDA=0, SCL=1 (START!)         Фаза 2: SDA=0, SCL=1 (START!)
  Фаза 3: SDA=0, SCL=0                  Фаза 3: SDA=0, SCL=0

START начинает с обоих линий HIGH (шина свободна). RESTART начинает с SCL=0 (мы только что передавали данные) - сначала поднимает SDA, потом отпускает SCL.

Код теста:

// =============================================================
// TEST 4: Repeated START (RESTART)
// =============================================================

$display("\n=== TEST 4: Repeated START (RESTART) ===");
begin : test4
    reg ack;
    reg [7:0] rdata;

    // Записываем 0xBE в ячейку 0x20
    do_start;
    do_write({SLAVE_ADDR, 1'b0}, ack);
    do_write(8'h20, ack);
    do_write(8'hBE, ack);
    do_stop;

    repeat (50) @(posedge clk);

    // Читаем обратно через RESTART
    do_start;
    do_write({SLAVE_ADDR, 1'b0}, ack);
    do_write(8'h20, ack);
    do_restart;

    if (busy !== 1'b1)
        test_fail("busy_o dropped during RESTART");

    do_write({SLAVE_ADDR, 1'b1}, ack);
    do_read(1'b1, rdata);
    do_stop;

    if (rdata === 8'hBE)
        test_pass("RESTART read-back OK");
    else
        test_fail("RESTART read-back mismatch");
end

Ключевая проверка заключается в том, что на осциллограмме busy_o должен быть непрерывной “1” от первого START до финального STOP. Если busy_o мигнёт в 0 - значит, ядро сгенерировало ложный STOP.

Тест 5. NACK от Slave + восстановление

Цель: Убедиться, что ядро фиксирует NACK, не зависает, и нормально работает после этого.

Код теста:

// =============================================================
// TEST 5: NACK from slave (wrong address) + recovery
// =============================================================
$display("\n=== TEST 5: NACK from slave ===");
begin : test5
    reg ack;

    do_start;
    do_write({7'h3F, 1'b0}, ack);         // Адрес 0x3F — нет такого slave

    if (ack === 1'b1)
        test_pass("Got NACK for nonexistent address 0x3F");
    else
        test_fail("Expected NACK, got ACK for 0x3F");

    do_stop;

    repeat (10) @(posedge clk);
    if (busy !== 1'b0)
        test_fail("busy_o not cleared after NACK + STOP");

    // Восстановление: правильный адрес после NACK
    do_start;
    do_write({SLAVE_ADDR, 1'b0}, ack);
    if (ack === 1'b0)
        test_pass("Normal ACK after NACK recovery");
    else
        test_fail("Controller stuck after NACK");
    do_stop;
end 

Тест проверяет две вещи: NACK на неправильном адресе и корректную работу после NACK + STOP. Это важно - ядро не должно «застревать» после ошибочной адресации.

Тест 6. Clock stretching

Цель: Убедиться, что ядро корректно ожидает, когда slave удерживает SCL в 0.

Как это работает в тестбенче: Вместо обычного slave (0x50) тест использует slave на адресе 0x51 (SLAVE_ADDR_STR). После каждого ACK от slave_str, SCL-hold логика тянет SCL к 0 на 80 тактов. Ядро отпускает SCL (scl_oen = 1), но scl_i = 0 (slave держит). Ядро ждёт в фазе 1 пока scl_i не станет 1.

Код теста:

// =============================================================
// TEST 6: Clock stretching (via stretching slave at 0x51)
// =============================================================
$display("\n=== TEST 6: Clock stretching ===");
begin : test6
    reg ack;
    reg [7:0] rdata;

    do_start;
    do_write({SLAVE_ADDR_STR, 1'b0}, ack);    // Slave 0x51
    if (ack !== 1'b0) begin
        test_fail("Stretching slave NACK on address");
    end else begin
        do_write(8'h30, ack);
        do_write(8'hCD, ack);
        do_stop;

        repeat (50) @(posedge clk);

        // Читаем обратно
        do_start;
        do_write({SLAVE_ADDR_STR, 1'b0}, ack);
        do_write(8'h30, ack);
        do_restart;
        do_write({SLAVE_ADDR_STR, 1'b1}, ack);
        do_read(1'b1, rdata);
        do_stop;

        if (rdata === 8'hCD)
            test_pass("Clock stretching handled OK");
        else
            test_fail("Data corrupted after stretching");
    end
end

Обратите внимание на if/else - если stretching-slave не ответит ACK на свой адрес, тест не будет пытаться продолжать (записывать/читать), а сразу зафиксирует FAIL.

Тест 7. Arbitration lost

Цель: Убедиться, что ядро обнаруживает конфликт на шине и немедленно отпускает её.

Как будем имитировать потерю арбитража: Используем ext_sda_drive - когда он = 1, SDA принудительно тянется к 0. Если ядро в этот момент отпустило SDA (ожидает 1), оно увидит sda_i = 0 → арбитраж потерян.

Тест 7 - самый сложный, потому что нужно “вмешаться” в нужный момент. Тест выдаёт 4 проверки: обнаружение, освобождение шины, блокировка команд, сброс флага. В send_cmd при таймауте используется disable send_cmd. Но в тесте 7 мы управляем командами вручную (не через send_cmd), поэтому используем disable test7 - это выход из всего блока begin : test7 ... end. Каждый while-цикл обёрнут в именованный блок с собственным счётчиком wc.

Код теста:

    // =============================================================
    // TEST 7: Arbitration lost
    // =============================================================
    $display("\n=== TEST 7: Arbitration lost ===");
        begin : test7
            do_start;

            // Issue WRITE command manually to catch the right moment
            cmd       <= CMD_WRITE;
            din       <= {SLAVE_ADDR, 1'b0};   // 0xA0, MSB=1
            cmd_valid <= 1;
            @(posedge clk);
            begin : test7_wait_accept
                integer wc;
                wc = 0;
                while (ready) begin
                    @(posedge clk);
                    wc = wc + 1;
                    if (wc > TIMEOUT_LIMIT) begin
                        test_fail("TIMEOUT waiting for core to accept WRITE");
                        disable test7;
                    end
                end
            end
            cmd_valid <= 0;

            // Wait for DATA state phase 0 (core sets up SDA)
            begin : test7_wait_data
                integer wc;
                wc = 0;
                while (!(dut.state_r == 3'd2 && dut.phase_r == 2'd0)) begin
                    @(posedge clk);
                    wc = wc + 1;
                    if (wc > TIMEOUT_LIMIT) begin
                        test_fail("TIMEOUT waiting for DATA phase 0");
                        disable test7;
                    end
                end
            end
            @(posedge clk);

            // Interfere: pull SDA low externally
            ext_sda_drive <= 1;

            begin : test7_wait_arb
                integer wc;
                wc = 0;
                while (arb_lost !== 1'b1) begin
                    @(posedge clk);
                    wc = wc + 1;
                    if (wc > TIMEOUT_LIMIT) begin
                        test_fail("TIMEOUT waiting for arb_lost");
                        ext_sda_drive <= 0;
                        disable test7;
                    end
                end
            end
            ext_sda_drive <= 0;

            if (arb_lost === 1'b1)
                test_pass("Arbitration lost detected");
            else
                test_fail("Arbitration lost NOT detected");

            if (dut.scl_oen_o === 1'b1 && dut.sda_oen_o === 1'b1)
                test_pass("Bus released after arb_lost");
            else
                test_fail("Bus NOT released after arb_lost");

            // Core should ignore commands while arb_lost=1
            cmd_valid <= 1;
            cmd       <= CMD_START;
            repeat (20) @(posedge clk);
            if (ready === 1'b1)
                test_pass("Core ignores commands while arb_lost=1");
            else
                test_fail("Core accepted command despite arb_lost=1");
            cmd_valid <= 0;
            cmd       <= CMD_NOP;

            // Clear arb_lost
            arb_lost_clear <= 1;
            @(posedge clk);
            arb_lost_clear <= 0;
            repeat (5) @(posedge clk);

            if (arb_lost === 1'b0)
                test_pass("arb_lost cleared");
            else
                test_fail("arb_lost NOT cleared");

            do_stop;
        end

Тест 8. Reset во время транзакции

Цель: убедиться, что аппаратный сброс возвращает ядро в начальное состояние из середины передачи, и после сброса ядро работает нормально.

Тонкость: sda_d_r сбрасывается в 1. Регистр sda_d_r (задержанная копия sda_i) сбрасывается в 1, а не в 0. Это исключает ложные фронты на SDA после снятия сброса: sda_rising = sda_i & ~sda_d_r. Если бы sda_d_r = 0 и sda_i = 1 (pull-up), то sda_rising = 1 при scl_i = 1 → ложный STOP → некорректный busy_o.

Код теста:


        // =============================================================
        // TEST 8: Reset during transaction
        // =============================================================
        $display("\n=== TEST 8: Reset during transaction ===");
        begin : test8
            reg ack;
            reg [7:0] rdata;

            do_start;

            cmd       <= CMD_WRITE;
            din       <= {SLAVE_ADDR, 1'b0};
            cmd_valid <= 1;
            @(posedge clk);
            begin : test8_wait
                integer wc;
                wc = 0;
                while (ready) begin
                    @(posedge clk);
                    wc = wc + 1;
                    if (wc > TIMEOUT_LIMIT) begin
                        test_fail("TIMEOUT in reset test setup");
                        disable test8;
                    end
                end
            end
            cmd_valid <= 0;

            // Let 3-4 bits transmit
            repeat (4) begin : test8_bits
                integer wc;
                wc = 0;
                while (dut.phase_r != 2'd3) begin
                    @(posedge clk);
                    wc = wc + 1;
                    if (wc > TIMEOUT_LIMIT) begin
                        test_fail("TIMEOUT waiting for phase 3");
                        disable test8;
                    end
                end
                @(posedge clk);
            end

            // Assert reset
            rstn <= 0;
            repeat (10) @(posedge clk);
            rstn <= 1;
            repeat (20) @(posedge clk);

            // Check post-reset state
            if (dut.state_r !== 3'd0)
                test_fail("state_r not IDLE after reset");
            if (dut.scl_oen_o !== 1'b1 || dut.sda_oen_o !== 1'b1)
                test_fail("Bus not released after reset");
            if (ready !== 1'b1)
                test_fail("ready_o not 1 after reset");
            if (busy !== 1'b0)
                test_fail("busy_o not 0 after reset");
            if (arb_lost !== 1'b0)
                test_fail("arb_lost_o not 0 after reset");

            // Verify core works after reset
            do_start;
            do_write({SLAVE_ADDR, 1'b0}, ack);
            if (ack !== 1'b0)
                test_fail("NACK after reset — controller broken");

            do_write(8'h70, ack);
            do_write(8'hEE, ack);
            do_stop;

            repeat (50) @(posedge clk);

            do_start;
            do_write({SLAVE_ADDR, 1'b0}, ack);
            do_write(8'h70, ack);
            do_restart;
            do_write({SLAVE_ADDR, 1'b1}, ack);
            do_read(1'b1, rdata);
            do_stop;

            if (rdata === 8'hEE)
                test_pass("Post-reset write/read OK");
            else
                test_fail("Post-reset data mismatch");
        end

Тест 9. CMD_NOP

Цель: Убедиться, что NOP не вызывает никаких побочных эффектов.

Код теста: 

      // =============================================================
      // TEST 9: CMD_NOP does nothing
      // =============================================================
      $display("\n=== TEST 9: CMD_NOP ===");
      begin : test9
            // NOP is ignored: ready stays 1, state stays IDLE
            @(posedge clk);
            cmd       <= CMD_NOP;
            din       <= 8'hFF;
            cmd_valid <= 1;
            repeat (20) @(posedge clk);
            cmd_valid <= 0;
            cmd       <= CMD_NOP;

            if (dut.state_r === 3'd0 && ready === 1'b1)
                test_pass("NOP: state stayed IDLE, ready=1");
            else
                test_fail("NOP: unexpected state change");
      end

NOP не проходит через send_cmd, потому что send_cmd ждёт падения ready. Но NOP - это “ничего не делать”, ядро его игнорирует, ready никогда не упадёт. Поэтому тут мы вручную держим cmd_valid = 1 с CMD_NOP 20 тактов и проверяем, что ничего не изменилось.

Тест 10. Последовательное чтение (4 байта)

Цель: Проверить, что ядро корректно выполняет серию READ с ACK, завершая NACK-ом.

Slave-модель при инициализации заполняет память mem[i] = i. Поэтому чтение с адреса 0x00 должно вернуть 0x00, 0x01, 0x02, 0x03. При ошибке тест выводит реально полученные значения - удобно для диагностики.

Код теста:

      // =============================================================
      // TEST 10: Sequential read (4 bytes)
      // =============================================================
      $display("\n=== TEST 10: Sequential read (4 bytes) ===");
      begin : test10
            reg ack;
            reg [7:0] r0, r1, r2, r3;
            integer seq_ok;

            // Slave memory is initialized as mem[i] = i.
            // Read from address 0x00.
            do_start;
            do_write({SLAVE_ADDR, 1'b0}, ack);
            do_write(8'h00, ack);
            do_restart;
            do_write({SLAVE_ADDR, 1'b1}, ack);

            do_read(1'b0, r0);  // ACK
            do_read(1'b0, r1);  // ACK
            do_read(1'b0, r2);  // ACK
            do_read(1'b1, r3);  // NACK

            do_stop;

            seq_ok = (r0 === 8'h00) && (r1 === 8'h01) &&
                     (r2 === 8'h02) && (r3 === 8'h03);

            if (seq_ok)
                test_pass("Sequential read 00,01,02,03 OK");
            else begin
                $display("    got: %02h %02h %02h %02h", r0, r1, r2, r3);
                test_fail("Sequential read mismatch");
            end
      end

А как запустить?

Берем тесты с репозитория https://github.com/megalloid/I2C_Master_Controller или руками заполняем тесты и запускаем:

make sim-core

...или...

mkdir -p sim
iverilog -g2012 -Wall -o sim/i2c_core_tb.vvp \
    rtl/i2c_master_core.v \
    tb/i2c_slave_model.sv \
    tb/i2c_core_tb.sv
cd sim && vvp ../sim/i2c_core_tb.vvp

Можно провести Lint-проверку ядра:

make lint-core

После этого можно еще и просмотреть временные диаграммы через GTK Wave:

gtkwave sim/i2c_core_tb.vcd

Сигналы накидываются во вьювер самостоятельно: 

Основные сигналы которые рекомендуются к просмотру для отладки и отсмотра:

  • Шина - sda, scl

  • Управление - cmd_valid, cmd, din, ready, ena

  • Результат - dout, rx_ack

  • Статус - busy, arb_lost

  • FSM (DUT) - dut.state_r, dut.phase_r, dut.bit_cnt_r

  • Сдвиговые - dut.tx_shift_r, dut.rx_shift_r

  • Open-drain - dut.scl_oen_o, dut.sda_oen_o

  • Slave - slave.state, slave.sr, slave.bcnt, slave.sda_out_en

  • Stretching - scl_hold, stretch_cnt, slave_str.state

  • Арбитраж - ext_sda_drive, arb_lost

В итоге после запуска будет ожидаемый ввод:

=== TEST 1: Single WRITE + ACK ===
  PASS: Slave ACK received (rx_ack_o = 0)

=== TEST 2: Single READ + NACK ===
  PASS: Read 0xA5 matches written value

=== TEST 3: Full transaction ===
  PASS: Full transaction OK, busy cleared

=== TEST 4: Repeated START (RESTART) ===
  PASS: RESTART read-back OK

=== TEST 5: NACK from slave ===
  PASS: Got NACK for nonexistent address 0x3F
  PASS: Normal ACK after NACK recovery

=== TEST 6: Clock stretching ===
  PASS: Clock stretching handled OK

=== TEST 7: Arbitration lost ===
  PASS: Arbitration lost detected
  PASS: Bus released after arb_lost
  PASS: Core ignores commands while arb_lost=1
  PASS: arb_lost cleared

=== TEST 8: Reset during transaction ===
  PASS: Post-reset write/read OK

=== TEST 9: CMD_NOP ===
  PASS: NOP: state stayed IDLE, ready=1

=== TEST 10: Sequential read (4 bytes) ===
  PASS: Sequential read 00,01,02,03 OK

========================================
  TEST SUMMARY:  PASS=14  FAIL=0
  All tests PASSED
========================================

Все тесты закончены. Ура! =)

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

После того как все 10 тестов проходят - ядро i2c_master_core можно считать проверенным на базовом уровне. Можно переходить к следующему шагу проектирования для реализации вышестоящих модулей:

  1. Написать прескалер - делитель частоты, генерирующий ena_i из системного клока.
    Формула: f_SCL = f_CLK / (4 × (PRESCALE + 1))

  2. Написать регистровую обёртку - набор memory-mapped регистров, через которые софт или процессор будут управлять ядром (CTRL, STATUS, CMD, TX_DATA, RX_DATA, PRESCALE)

  3. Написать секвенсер - логику составных команд (например, “START + WRITE” одним регистровым доступом);

  4. Написать тестбенч для всей системы - уже через регистровый интерфейс.

Фундамент - проверен. Можно строить дальше. Об этом и многом другом - в остальных статьях расскажу позже.


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

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

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

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