Создаем I2C Master Controller на Verilog. FSM, Clock, Output Logic, etc

После длительного перерыва я продолжил разработку I2C Master Controller на Verilog. В прошлых статьях я рассмотрел основной теоретический материал, необходимый для реализации изначальной задумки. В этом материале переходим к более интересному содержанию: я последовательно расскажу про процесс проектирования конечного автомата I2C, расскажу про тактирование и как организована логика выходных сигналов и многое другое.

Всем, кому интересно — добро пожаловать под кат!

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


Главный контроллер и FSM


Прежде чем, я перейду к проектированию и реализации — коротко резюмируем опорную информацию из прошлых статей (раз, два). В них я описал, каким образом организована передача данных, как определяются условия и формат установки служебных команд:

  • START-команда — снижаем SDA, когда высокий уровень SCL;
  • STOP-команда — повышаем SDA, когда высокий уровень SCL;
  • RESTART-команда — посылаем START команду на шину, без завершения сеанса.


Также я упомянул, то, каким образом выставляются данные, а в частности:

  • данные выставляются в момент, когда SCL находится в значении логического нуля;
  • данные считываются в момент, когда SCL находится в значении логической единицы;
  • примерные тайминги на операции (с ними поглубже разберемся позже).


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

По сути, нам нужен конечный автомат который управляется отдельными командами для каждой операции и формирует сигналы на шине:

  • генерирует START-команду, для объявления начала передачи на шине;
  • генерирует STOP-команду, для завершения сеанса связи;
  • генерирует RESTART-команду, в случае если она требуется для реализации того или иного требования спецификации Slave-устройства;
  • записывает 8 бит в Slave-устройство;
  • читает 8 бит из Slave-устройства;
  • проверяет выставлен ли ACK-бит (или выставляет его в зависимости от ситуации).


Это именно те действия, комбинируя которые, можно (в большинстве случаев, не учитывая ситуацию когда срабатывает например Clock Stretching) обеспечить выполнение целостного сеанса связи, вне зависимости от количества байт и специфики устройства в которое нужно записать или прочитать какую-либо информацию. Такой принцип построения позволит автомату более высокого уровня или прикладному софту (например, программе в baremetal) выполнять те или иные действия просто собирая целостную транзакцию из этих кирпичиков.

Давайте рассмотрим, каждое из этих действий отдельно.

Пояснения по графикам


Когда я начинал моделировать на временной сетке тайминги той или иной команды или процесса — я посчитал, что этим стоит поделиться с читателями, возможно это поможет вникнуть в суть того, как я конструировал конечный автомат.

Ссылку на drawio-диаграмму, в которой я проводил расчеты я приложу в конце статьи.


Но для того, чтобы понимание было наиболее полным — стоит ввести несколько пояснений.

Посмотрим, например, на результат моделирования операции RESTART:

image


Всю операцию от начала и до конца я поместил на сетку. Я взял за основу масштаб в одну клетку и условно обозначил ее длительность в 100 нс.

После я прикинул, что если задать общую частоту тактирования автомата в 10 МГц, т.е. как результат деления основной частоты в 50 МГц на 5, то при такой частоте можно будет комфортно разбить изменения сигнала и вписать их в требования спецификации. Ниже, в пояснениях по каждой команде вы увидите, что из этого получилось.

Внесу также пояснения, что еще изображено на данных графиках-таймингах:

  1. На верхней части графика показан сигнал SDA и его изменения во времени;
  2. На нижнем, в соответствии с той же логикой, отмечен сигнал SCL;
  3. Серой пунктирной вертикальной линией отмечен полупериод тактирования, на каждый отсчет которого мы можем производить какие-либо манипуляции с сигналом;
  4. Жирная красная пунктирная линия указывает моменты выставления или считывания сигнала (в зависимости от команды);
  5. Также областями (красным, желтым) я постарался выделить периоды, которые имеют конкретные временные ограничения. Красным — те, которые ограничены максимальным временем длительности, а желтым — те, которые должны быть выдержаны как минимум столько-то времени;
  6. Красной пунктирной линией обозначены максимальные длительности перехода сигнала из одного состояния в другое;
  7. Под общим графиком сигнала дал описание конкретных ограничений сигналов по таймингу и что имеем по факту при текущей раскладке по длительностям тактовых импульсов.


Такой вариант начертания помог мне более наглядно взглянуть на то, как должен быть организовано изменения сигнала относительно основного тактирования и конкретных ограничений спецификации на тайминги. И так как планируется переключаемый режим Standard и Fast Mode — я решил, что сделаю графики для обоих вариантов.

Итак, перейдем к рассмотрению самих команд и их раскладке по таймингам.

Команда START на шине I2C


Вспоминая временную диаграмму START-команды из прошлой статьи — ее можно разбить на три отдельных этапа, которые заканчиваются состоянием Hold:

  1. Idle — шина свободна, т.е. SCL и SDA выставлены по умолчанию в высокий уровень;
  2. Start 1 — выставляем SDA в ноль, SCL оставляем в единице;
  3. Start 2 — оставляем SDA в нуле, SCL тоже притягиваем к нулю;
  4. Hold — состояние в котором ожидается передача следующей команды.


Поясню основные обозначения под графиком:

  • T HD; STA — длительность сигнала START. После окончания этого периода может быть сгенерирован первый синхроимпульс;
  • Tf  — длительность спада фронта сигнала.


Когда моделировал возможные варианты тактирования — картина по таймингам у меня выглядела следующим образом для Standard режима:

image

И для Fast режима:

image


Что касается таймингов:

  • нужно успевать опускать линию SDA и SCL быстрее чем за 300 нс вне зависимости от текущего режима (Standard или Fast);
  • нужно удержать линию SDA как минимум на 4000 и 600 нс для Standard и Fast Mode соответственно;
  • после этого необходимо удержать линию SCL еще 4700 и 600 нс перед тем как начать передавать следующую порцию данных.


В нашем случае:

  • ожидается, что при напряжении питания в 3.3V, емкости шины около 50 pF и подтягивающих резисторах в 4.7kΩ постоянная времени RC-цепи будет не больше чем 235 нс. А если говорить о уровне на котором происходит защелкивание измененного цифрового уровня — то еще быстрее. (скорость спада фронта сигнала в ноль у линий SDA и SCL после проверим на осциллографе при тесте в железе);
  • если будем удерживать шаг Start 1 в течение 4 тактовых импульсов — то в случае Standard Mode 100 kHz — будет 4700 ns, а в случае Fast Mode 400 kHz — 700 ns, чего в обоих случаях будет с запасом;
  • шаг Start 2 можно удерживать достаточно долгое время и его длительность не определена спецификацией, мы взяли с небольшим запасом время в ожидании следующей команды.


Тут всё ок, значит идём дальше.

Команда RESTART на шине I2C


Команда RESTART очень похожа на команду START, но с тем лишь отличием, что до выставления команды необходимо выдержать очень короткий промежуток времени соответствующий половине длительности тактового периода:

  1. Hold — этап, когда был выставлен и уже считан ACK/NACK сигнал и после этого Master начинает удерживать линии SCL и SDA на низком уровне;
  2. Restart 1 — Master поднимает SDA в высокий уровень;
  3. Restart 2 — Master поднимает SCL в высокий уровень и выдерживает тайминг;
  4. Start 1 — выставляем SDA в ноль, SCL оставляем в единице;
  5. Start 2 — оставляем SDA в нуле, SCL тоже притягиваем к нулю и выдерживаем в таком состоянии как в команде START (специально не указал на графике Standard Mode, потому что не влезло).


Рассмотрим так же тайминг, аналогично, как в предыдущей команде. Синий в данном случае обозначает то, что пришел NACK на 9-й тактовый сигнал, а зеленый — ACK.

Дам обозначения по графику:

  • T hold — это время ожидания, которое не определено спецификацией;
  • Tf и Tr — длительность нарастания и спада фронта сигнала;
  • T SU; STA — время установления сигнала RESTART;
  • T HD; STA — длительность сигнала RESTART. После окончания этого периода может быть сгенерирован первый синхроимпульс SCL.


Для Standard Mode:

image

Для Fast Mode:

image


Очень заметно, что использование Fast Mode гораздо выгоднее, чем Standard Mode ввиду отличия длительности. В дальнейшем не буду давать отдельных пояснений на изложенное на диаграмме, скажу лишь, что вполне укладываюсь в обозначенные тайминги.

Команда STOP на шине I2C


Команда STOP не менее проста, как и две предыдущие:

  1. Hold — этап, когда Master удерживает линии SCL и SDA на низком уровне;
  2. Stop 1 — оставляем SDA в логическом нуле, SCL поднимаем к единице;
  3. Stop 2 — поднимаем SDA в единицу, SCL оставляем в единице;
  4. Idle — шина свободна, т.е. SCL и SDA выставлены по умолчанию в высокий уровень.


Обозначу, что T SU; STO — означает время установления сигнала STOP, и отражу это так же в виде временной диаграммы сначала для Standard Mode:

image

И для Fast Mode:

image


Биты данных. Выставление данных на шине


Теперь рассмотрим ситуацию когда происходит выставление битов данных на шине, вне зависимости от того, кто выставляет данные, Master или Slave устройство. Происходит это в момент когда SCL находится в значении логического нуля, в два этапа:

  1. Data 1 — этап, когда SCL уходит в значение логического нуля и по завершению которого происходит выставление новых данных на линии SDA;
  2. Data 2 — этап, когда SCL по прежнему остается в значении логического нуля, выдерживается необходимое время и повышается до высокого уровня, при котором происходит считывание значения линии SDA.


Этапы Data 3 и Data 4 я объяснил чуть ниже. Поясню, что означают подписи под графиком:

  • T HD; DAT — это параметр data valid time, т.е. время в течение которого должно быть осуществлено выставление данных на SDA;
  • T LOW — время в течение котором SCL может находиться в значении нуля;
  • T SU; DAT — параметр data set-up time, обозначает длительность при которой должен быть выдержано стабильное значение SDA для корректного считывания;
  • Tf и Tr — длительность нарастания и спада фронта сигнала.


Рассмотрим ограничения по таймингам и как это ложится в картину нашего тактирования в случае Standard Mode:

image

И так же в случае Fast Mode:

image


Биты данных. Получение данных


Теперь рассмотрим, как организовать момент считывания битов данных из шины. Как вы помните из предыдущих статей — фиксация бита данных происходит в момент когда SCL выставлен в высоком уровне. Этот момент гарантирует, что данные на SDA изменяться не будут. На диаграмме я отразил момент защелкивания данных с линии SDA:

image


И в случае Fast Mode:

image


Все в целом очевидно. По таймингам вполне комфортно укладываемся в т. ч. с моментом защелкивания получаемых данных.

Подведем промежуточный итог


Теперь, разметив все что нужно на временной плоскости и убедившись, что получаемые тайминги не противоречат спецификации I2C — можно составить диаграмму конечного автомата:

image


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

Для начала введем переменную ready, которая будет отвечать за состояние автомата и установим ее по умолчанию в значение 1 — это будет говорить нам о том, что контроллер может принимать команды на вход.

После я ввел несколько команд для управления конечным автоматом:

  • START_CMD — для генерации на шине события Start;
  • STOP_CMD — для генерации на шине события Stop;
  • RESTART_CMD — для генерации на шине события Restart;
  • RD_CMD — для начала считывания данных с шины;
  • WR_CMD — для записи на шину данных.


Чтобы начать транзакцию в шину I2C необходимо сначала проверить, является ли переменная ready в значении 1, если да — то нужно, в первую очередь, отправить команду START_CMD и изменить ready на 0 и осуществить отправку на шину того, что мы отразили на временной диаграмме в зависимости от выбранного режима (Standard или Fast).

После этого мы переходим в состояние Hold, выставляем переменную ready снова в значение 1 и далее автомат готов принимать следующую команду, теоретически мы там можем находиться достаточно долго.

После START-команды конечно же не стоит останавливаться и в зависимости от команды выполнить указанную команду, чаще всего это будут команды RD_CMD или WR_CMD — после которых необходимо 9 раз выполнить этапы Data 1, Data 2, Data 3, Data 4 о которых я рассказывал выше. В этих 9 битах будут содержаться и полезные данные и бит подтверждения. В то же время происходит инкремент счетчика бит данных которые были считаны\записаны на шину. В эти 9 бит входят и отправка адреса устройства, направления обмена данными и сам обмен этими данными. Данный этап — универсальный.

Далее в зависимости от ситуации происходит выполнение команды RESTART_CMD или STOP_CMD в соответствии с графиками которые были показаны выше. Кажется все достаточно просто и очевидно.

Отмечу пару важных моментов. Данный контроллер не будет учитывать наличие еще одного Master-устройства на шине или каких-то других особых случаев и данная реализация является достаточно простой и примитивной. Также на данном этапе не взято во в обработку ситуации Clock Stretching.

Data Output Control Logic


Теперь коротко расскажу про то, как будет организован прием и передача данных на шину и рассмотрим логику Data Output Control для SDA-линии. Схематически бы я его изобразил следующим образом:

image

Объясню какую роль играет каждый из отмеченных тут блоков:

  • CMD — это интерфейс с которого поступает команда от вышестоящего управляющего устройства;
  • FSM — это то, о чем я рассказал выше про автомат конечных состояний;
  • Output Control Logic — это простая комбинационная схема, которая будет управлять буфером с тремя состояниями при выполнении команд чтения или записи;
  • Tri-state буфер — это буфер который управляется одним управляющим пином и позволяет пропускать данные через него или нет;
  • RX Shift Register — это буфер в который будут складываться данные с шины;
  • TX Shift Register — это буфер из которого происходит отправка данных на шину.


Проще всего объяснить как работает схема — на конкретном примере. Например, мы хотим отправить некоторые данные на линию SDA. На FSM приходит соответствующая команда из интерфейса CMD и он в свою очередь сообщает на Output Control Logic управляющий сигнал который открывает буфер и происходит последовательная отправка данных на линию SDA и такая же последовательная фиксация отправляемого значения в RX Shift Buffer, там мы как бы сохраняем копию отправленных данных. В завершение транзакции, на чтение 9 бита ACK мы закрываем буфер и ACK-сигнал попадает только в RX Shift Buffer.

Чтение происходит по аналогичному принципу. Сначала мы закрываем буфер с тремя состояниями и производим считывание данных, данные при этом не проходят через буфер и на последний 9-й бит мы открываем буфер и отправляем значение 0 в качестве подтверждения считанных данных и сигнализируем о том, что транзакция прошла успешно.

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

Тактирование


Коротко, еще раз объясню, по какому принципу мы выстроим схему тактирования. На старте для отладки на плате Altera подразумевается, что частота системного тактирования будет составлять 50 МГц. Как я говорил выше — из этой частоты путем деления на 5 можно получить комфортную частоту в 10 МГц чтобы сделать ее основной для конечного автомата. При моделировании на временной сетке получилось так, что эта частота как раз подходит для формирования подходящих нам длительностей для изменения сигналов SDA и SCL. Напомню, что эти полупериоды я отметил серыми пунктирными линиями на графике.

Единственный, важный момент — это то, что количество таких полупериодов в разных режимах работы будут отличаться и Standard режим будет в 4 раза медленнее чем Fast.

Я пересчитал итоговую тактовую частоту которая будет получаться при обмене данными — и получилось, что итоговая частота в Standard Mode — будет в районе 94.02 кГц и в Fast Mode — 280.11 кГц. Выглядит как очень приближенные значения, но вероятно путем тонкого тюнинга можно будет прийти к более точной частоте. Запишу это в план по оптимизации, но пока что сконцентрируемся на основной задаче.

Непосредственно Controller


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

image


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

  • reset_n — это вход для общего сигнала асинхронного сброса всей схемы и приведения ее к исходному состоянию;
  • i_clk — это вход уже готового тактового сигнала в 10 МГц;
  • i_mode — это входной сигнал, отвечающий за то, какой режим работы I2C будет выбран. Это может быть либо Standard Mode или Fast Mode. Сделан специально шириной в 2 бита, на случай если появится третий режим в будущем;
  • i_cmd — это входной сигнал для установки текущей команды, которая должна будет исполнена когда будет подан сигнал i_req_trans;
  • i_addr_rw — это входной сигнал для того, чтобы указать адрес Slave-устройства и указания направления обмена данными т.е. тут же передается бит на чтение или на запись данных;
  • i_byte_len — это входной сигнал, который указывает количество байт данных которые должны быть записаны в текущем наборе транзакций;
  • o_data_read — это выходной сигнал, который отдаёт прочитанные 8 бит данных;
  • o_data_valid — это выходной сигнал, который указывает что полученные данные валидны;
  • o_hold — это выходной сигнал указывающий что текущее состояние автомата находится в Hold-стадии;
  • o_ready — это выходной сигнал указывающий, что текущее состояние автомата находится либо в режиме Idle и готово к новым транзакциям, либо в режиме Hold и готово принимать следующую команду на вход;
  • o_connect_nack — это отладочный сигнал показывающий, что запрашиваемое устройство на шине не ответило (т. е. отсутствует).


Вероятно, для отладки я введу еще несколько дополнительных сигналов, но кажется что этого пока достаточно. Учитывая задуманную логику, на текущем этапе этого достаточно, чтобы начать продумывать внутреннюю логику автомата.

Заключение


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

В этой статье я описал:

  • как организовать передачу на уровне отдельных бит и команд;
  • как выстроить логику их передачи на шину I2C;
  • как организовать считывание и передачу данных в буферы с помощью tri-state буфера.


В следующей статье я расскажу о том как:

  • организовать логику конечного автомата при выполнении всех эти операций в HDL-коде;
  • как протестировать этот код используя testbench;
  • организуем вышестоящий автомат, которым будем производить обмен данными с реальным устройством, например I2C EEPROM;
  • и если статья получится не сильно большая — расскажу о результатах тестирования всего этого хозяйства во взаимодействии с реальным устройством, подключим осциллограф и логический анализатор, посмотрим реальную картину и, возможно, даже подебажим найденные проблемы.


После этого уже можно будет переходить к адаптации полученного результата на Xilinx Zynq и подготовке Linux-драйвера для управления автоматом через AXI-интерфейс. Спасибо за внимание и до встречи в следующих статьях!

Ссылка на DrawIO-файл: drive.google.com/file/d/1uJzIqLpJEuJ2PEbqFPl_c-Rbj_dKLU85/view? usp=sharing


Возможно, захочется почитать и это:
b5pjofdoxth14ro-rjsrn7sbmiy.png

© Habrahabr.ru