Инструменты пользователя

Инструменты сайта


prog:i2c:timersorfi2c

Программный I2C на таймере

Появилась необходимость в интерфейсе I2C на плате с микроконтроллером 1986ВЕ1Т, но аппаратного блока I2C в этом МК нет. Остается реализовать его программно. Интерфейс I2C медленный и если реализовывать его "в лоб" с синхронными задержками, то куча времени потратится даром. Поэтому появилась идея реализовать протокол на событиях таймера.

Хочется реализовать сразу оба режима - мастер и слэйв. Слэйв режим может быть полезен и для других МК, потому что аппаратный блок I2С, там где он есть, реализует только режим мастера. (Аппаратный пример работы с I2C можно найти в примерах Pack_v6 на GitHub)

В сети полно библиотек программной реализации I2C и особого смысла затевать еще одну реализацию казалось бы нет. Но возможно новый велосипед получится чуть более шустрым, потому что для генерации CLK мастером планируется использовать таймер в режиме ШИМ. (Тактовый сигнал в I2C называется SCL, но мне привычней тактовую частоту называть CLK, поэтому в данной статье CLK - это SCL.)

Код тестового проекта мастер-слэйв доступен на GitHub

Схема событий при записи данных

Предполагается, что трансфер записи будет работать как-то так (запись 16-битных данных):

Отдельно описывать протокол I2C нет смысла, потому что в сети уже очень много хороших описаний, например это - easyelectronics.ru: "Интерфейсная шина IIC (I2C)"

Таймер Master:

Для генерации CLK используется таймер в режиме ШИМ (PWM). Таймер запускается в режиме счета вперед-назад, т.е. регистр CNT считает от 0 до значения регистра ARR, и затем обратно от ARR до 0. Каждый раз, при достижении регистром CNT концевых значений 0 и ARR генерируются прерывания, в которых необходимо выполнить некоторую обработку линии SDA. Переключения сигнала CLK происходят, когда когда регистр CNT становится равен регистру CCR. Таким образом, значение регистра ARR регулирует период сигнала CLK, а значение CCR - скважность сигнала. Подробнее про таймеры написано тут - Таймеры общего назначения

Трансфер записи от мастера состоит из двух этапов:

  1. Передача адреса ведомого устройства (7 бит) и бита операции: чтение (SDA = 1) / запись (SDA = 0)
  2. Передача байта данных. Передача повторяется необходимое количество раз.

При выполнении обоих этих этапов, обработчики событий таймера используются почти одинаково:

  • EventARR (событие при котором CNT = ARR): Событие соответствует середине периода удержания сигнала CLK в 0.
    • 8 событий: На SDA выводится бит данных адреса или данных.
    • 9-е событие (последнее): Линию SDA необходимо отпустить, чтобы ведомый мог перетянуть в ноль внешнюю подтяжку линии, подтвердив тем самым прием байта.
  • Event0 (событие при котором CNT = 0): Событие соответствует середине периода удержания сигнала CLK в 1.
    • 8 событий: При выдаче бит данных можно проверять отсутствие коллизий, сравнивая текущей уровень SDA со значением поданным на SDA при EventARR. Но Если абонентов на линии только двое, мастер и ведомый, и мастер всегда один, то смысла в этом нет.
    • 9-е событие (последнее): Необходимо проверить наличие подтверждения от ведомого.
      • ACK (SDA=0): Слово принято ведомым, можно послать следующее слово или перейти к СТОП-последовательности для окончания трансфера.
      • NACK (SDA=1): Ведомый не выставил подтверждения, значит что-то пошло не так и продолжать передачу нельзя. Необходимо перейти к СТОП-последовательности для окончания трансфера.

Таймер Slave:

В ведомом устройстве таймер используется в режиме захвата и генерирует прерывания по фронту и спаду сигнала CLK. Для захвата необходимо использовать пин который имеет прямую функцию канала таймера, например TMR1_CH1. Для отслеживания условий Старт/Стоп желательно использовать такой-же пин, например TMR1_CH2. Иначе, например, при использовании поллинга, Старт/Стоп можно и пропустить.

Старт/Стоп приема

При событии SDA Fall, (SDA = 0):

  • CLK = 1: Начинаем обработку трансфера по приему байт.
  • CLK = 0: Ничего не делаем. Идет трансфер, если был начат или это нештатная ситуация.

При событии SDA Rise, (SDA = 1):

  • CLK = 1: Прекращаем обработку трансфера по приему байт.
  • CLK = 0: Ничего не делаем. Идет трансфер, если был начат или это нештатная ситуация.

Прием данных

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

Обработчики событий:

  • CLK Rise:
    • 8 событий: Считываем биты байта. Если это первый байт, то трактуем его как адрес и признак операции "Запись/Чтение". Если это последующие байты, то это данные. После приема 8-ми бит, записываем байт в массив принятых данных.
    • 9-е событие (последнее): Ничего не делаем. В этот момент мастер считывает подтверждение.
  • CLK Fall:
    • 1-е событие: Отпускаем линию, чтобы снять ACK выставленный нами при приеме предыдущего слова. (При приеме адреса в этом нет необходимости, но чтобы не терять время на проверку условия - ставили мы АСК или нет, быстрее будет отпустить пин.)
    • 2-8 события: Ничего не делаем.
    • 9-е событие (последнее): Выставляем подтверждение SDA = 0 (ACK). Если не можем больше принимать данные, то выставляем NACK - отпускаем линию и подтяжка вытягивает ее в 1.

Схема событий при чтении данных

Трансфер чтения выглядит чуть сложнее (чтение 16-битных данных):

Старт/Стоп трансфера здесь обрабатываются так-же как при трансфере записи. Абсолютно так-же отрабатывает трансфер первого байта (адреса), с той лишь разницей, что последний бит здесь передается мастером как 1. Различия начинаются с трансфером второго и последующего байт. Теперь ведомый выставляет данные на линию SDA, а мастер их считывает. Последний бит, как обычно, является подтверждением. Только теперь мастер выставляет подтверждение о том, что принял байт, а ведомый освобождает для этого линию SDA.

Таймер Master (со второго байта):

  • EventARR: Середина CLK = 0.
    • 1-е событие: Отпускаем линию, чтобы снять ACK выставленный нами при приеме предыдущего слова. (Как и в трансфере записи, для первого слова в этом нет необходимости, но так будет универсальнее.)
    • 2-8 события: Пропускаем.
    • 9-е событие:
      • Выставляем SDA = 0 (ACK), для подтверждения приема - Если читаемое слово не последнее
      • Выставляем SDA = 1 (NACK), если читаемое слово последнее.
  • Event0: Середина CLK = 1.
    • 8 событий: Считываем биты байта.
    • 9-е событие: Ничего не делаем.

Таймер Slave (со второго байта):

  • CLK Fall:
    • 8 событий: На SDA выводим биты байта данных
    • 9-е событие(последнее): Освобождаем линию SDA, чтобы мастер мог выставить подтверждение.
  • CLK Rise:
    • 8 событий: Пропускаем.
    • 9-е событие (последнее): Проверяем наличие ACK от мастера.
      • ACK (SDA = 0): Ничего не делаем или готовим следующий байт для вывода.
      • NACK (SDA = 1): Прекращаем обработку ведомого.

Транзакция Запись-Чтение (чтение регистров)

Как было рассказано в статье easyelectronics.ru: "Интерфейсная шина IIC (I2C)", довольно часто требуется читать внутренние регистры какой-нибудь микросхемы, подключенной по I2C. Происходит это за несколько тактов:

  1. СТАРТ
  2. Запись адреса микросхемы с признаком WR - 1 байт
  3. Запись адреса регистра - один или несколько байт
  4. РЕСТАРТ (подача СТАРТ, без подачи СТОП)
  5. Запись адреса микросхемы с признаком RD - 1 байт
  6. Чтение значения регистра - один или несколько байт
  7. СТОП

Необходимо обратить внимание, что после записи адреса регистра, линия SDA остается в значении 0, потому что это ведомый удерживает ACK. Но для того, чтобы сформировать СТАРТ надо опустить SDA из 1 в 0, при CLK = 1. Чтобы вернуть SDA в 1, необходимо подать один период CLK, чтобы ведомый отпустил линию SDA - это произойдет по спаду CLK. Затем, при поднятии CLK ведомый решит, что пришел следующий бит из транзакции записи, поэтому он засемплирует этот первый бит. Но следующим этапом на шине I2C появится условие СТАРТ, которое прервет чтение байта в ведомом и ведомый переключится на прием адреса и признака WR-RW.

Вот как это выглядит на схеме сигналов:

По картинке видно, что никаких обработок Мастером при восстановлении SDA производить не нужно. Необходимо лишь подать один период CLK и ведомый освободит шину.

Так-же по картинке видно, что если потребуется устроить РЕСТАРТ после трансфера чтения, то никаких дополнительных действий не требуется. Обе линии CLK и SDA уже находятся в 1 и можно сразу переходить к СТАРТ. Однако, с необходимостью рестарта после чтения мне сталкиваться не приходилось.

Про реализацию

В реализации мастера сделано два режима, когда CLK переключается программно в прерываниях от таймера и когда CLK переключается от PWM канала таймера. Программный CLK может понадобится, когда пин использованный в плате под CLK не имеет функции канала таймера. В случае, когда используется PWM для генерации CLK, прерывания от таймера возникают в два раза реже, соответственно не тратится время на вход-выход в прерывание и отработку логики прерывания. В тестовом примере, режимы переключаются параметром I2C_MASTER_BY_PWM.

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

Файлы драйверов программного I2C состоят из следующих файлов:

  • MDR_SoftI2C_MasterStates.c - автомат состояний мастера I2C
  • MDR_SoftI2C_SlaveStates.c - автомат состояний ведомого I2C
  • MDR_SoftI2C_States.c - декларация типов и структур для автоматов состояний
  • MDR_SoftI2C_byTimer.c / MDR_SoftI2C_byTimer.h - подключение автоматов состояний к событиям таймера.

Подобное разделение сделано для возможности подключения автомата состояний к другому источнику событий.

В драйверах не реализована поддержка режима медленного I2C, когда ведомый не дает поднять линию CLK, потому-что занят отработкой запроса. Кроме этого, в коде нет проверки на наличие коллизий. Все это сделано осознанно, потому-что в будущей плате мастер на линии будет только один, а отвечающие модули не "тормозят". А поскольку нет потребности, то и усложнять код желания не возникло. С другой стороны, дописать эти опции думаю не составит большого труда.

Текущая реализация хранит свой "стек задачи" в структурах MDR_I2Cst_MasterObj и MDR_I2Cst_SlaveObj. Структуры содержат все переменные, необходимые для отработки функций I2C. Это позволяет реализовать несколько задач по управлению I2C в одном приложении. Но выборка переменных из указателя на структуру делает код несколько медленнее, чем если бы переменные были глобальные и лежали сразу в известных адресах памяти. Если потребуется ускорение, то в коде необходимо избавиться от структур или завести обе структуры глобальными.

Так получилось, что в файле MDR_SoftI2C_byTimer.с находится код под режимы и мастер, и ведомый. Но обычно необходим только один режим. В таком случае какой-то из драйверов MDR_SoftI2C_MasterStates.c или MDR_SoftI2C_SalveStates.c не подключаются к проекту потому что не нужен, но при этом MDR_SoftI2C_byTimer.с перестает собираться. В этом есть проблема. Можно было бы разделить код MDR_SoftI2C_byTimer.с на такие же части мастер и ведомый, но тогда не понятно что делать с функцией инициализации пинов, коротая нужна в обоих режимах. Если реализовать эту функцию в обоих файлах, то это будет дублирование кода, что тоже не хорошо. Что-бы как-то "разрулить" ситуацию в файл MDR_ConfigVE1.h были вынесены макроопределения, с помощью которых можно отключить ненужную реализацию:

// "Отключение" программного SlaveI2C, чтобы собирался MDR_SoftI2C_byTimer, если драйвер MDR_SoftI2C_SlaveStates не подключен
#define I2C_SOFT_SLAVE_DISABLE   1

// "Отключение" программного MasterI2C, чтобы собирался MDR_SoftI2C_byTimer, если драйвер MDR_SoftI2C_MasterStates не подключен
#define I2C_SOFT_MASTER_DISABLE  0

Чаще всего микроконтроллер выступает как мастер и опрашивает всякие датчики по I2C, поэтому по умолчанию оставлен активным режим мастера.

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

В заключении, несколько графиков с осциллографа, для визуализации работы I2C в тестовом проекте.

Транзакция записи

Транзакция чтения

Транзакция записи регистра

Собственно эта транзакция ничем не отличается от обычной записи. Это лишь ведомый рассматривает слово за адресом как индекс или адрес регистра. Количество байт в адресе регистра зависит от ведомой микросхемы.

Транзакция чтения регистра

Пример чтения информации SFP модулей оптического Ethernet

Добавлен GitHub - Пример опроса модулей SFP с помощью программного I2C

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

Чтобы разделить во времени СТОР-СТАРТ был добавлен еще один цикл таймера после формирования СТОП. Для данного цикла отключается вывод CLK чтобы не формировать лишних переключений. (Отключение пока проверено не проверено на PWM режиме.)

Если в приложении нет последовательного вычитывания I2C, а между трансферами производится еще какая-то деятельность, то необходимость в задержке отпадает. Поэтому возможность формирования задержки вынесена как опция в MDR_ConfigXX.h в параметр I2C_STOP_EN. Картинка внизу показывает разницу в работе этого параметра.

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

Особенности работы с пинами

При реализации программного I2C проявилась особенность работы с пинами SCL и SDA, когда они расположены в одном порту. Например, когда SCL и SDA это PC0 и PC1. При маскировании этих бит, снятии и установке, может возникнуть проблема с операцией "чтение-модификация-запись". Проблема эта проявляется в 1986ВЕ9х микроконтроллерах, поскольку для того чтобы выставить бит надо:

  • считать регистр RXTX, который возвращает текущее состояние линии,
  • наложить на значение регистра маску
  • записать новое значение обратно в RXTX

Но на линию SDA работает сразу два абонента. И тогда, в ситуации с подтверждением в 9-м бите возникает ситуация:

  • мастер выставляет RXTX.bitSCL = 0, RXTX.bitSDA - в предыдущем значении линии.
  • мастер выводит 1, чтобы ведомый смог выставить подтверждение: RXTX.bitSDA = 1
  • ведомый выводит подтверждение: SDA = 0
  • мастер выставляет SCL = 1, по которому будет проверять наличие подтверждения от ведомого: RXTX = RXTX | bitSCL. Но ведь в RXTX бит bitSDA равен 0!

Вот на последней операции и возникает ошибка, т.к. RXTX.bitSDA считается как 0, ведь это подтверждение от ведомого. А при выставлении бита RXTX.bitSCL, мы этот ноль на SDA сами прописываем в регистр RXTX. И теперь когда ведомый по следующему спаду CLK отпустит линию SDA, то она не вернется в 1, потому что ее ошибочно теперь держит мастер.

Поэтому при работе с пинами в режиме OpenDrain важно отслеживать, что мы пишем на линию и совершать свои действия с битами относительно этого значения, а не того что в данный момент находится на шине!

Поддержка в библиотеке MDR_Pack_v6

В микроконтроллерах 1986ВЕ1Т, 1986ВЕ3Т, 1986ВЕ8Т, 1986ВЕ4, 1023ВК214(234), "Электросила" есть отдельные регистры Set и Get которые работают через регистр RDTX. Этот регистр хранит значения, которые мы пишем в порт, а не то, что находится в данный момент на пинах. Поэтому для данных микроконтроллеров, описанной выше проблемы не возникает. Функции библиотеки MDR_Port_SetPins() и MDR_Port_ClearPins() могут использоваться напрямую, для управления линиями SDA и CLK.

Но в микроконтроллерах семейства 1986ВЕ9х и 1901ВЦ1Т регистра RDTX нет. Поэтому пришлось добавить аналог подобного регистра программно. В структуру, которая хранит настройки порта MDR_GPIO_Port, добавились два поля - одно для разрешения отслеживания состояния бита (enaPinMask) и второе - само состояние битов (pinStateMask).

typedef struct {
  uint32_t enaPinMask;
  uint32_t pinStateMask;
} MDR_GPIO_SoftRDTX;

typedef struct {
  // GPIO Port
  MDR_PORT_Type        *PORTx;
  // Clock Enable
  volatile uint32_t*    RST_ClockEn_Addr;
  uint32_t              ClockEnaMask;
  
#ifndef MDR_GPIO_HAS_SET_CLEAR
  MDR_GPIO_SoftRDTX    *pSoftRDTX;
#endif
} MDR_GPIO_Port;


//  Вывод значения пинов
__STATIC_INLINE void MDR_GPIO_TxApply(MDR_PORT_Type *PORTx, MDR_GPIO_SoftRDTX *pSoftRDTX) 
  { PORTx->RXTX = (PORTx->RXTX & ~pSoftRDTX->enaPinMask) | pSoftRDTX->pinStateMask; }

Поле enaPinMask необходимо чтобы сбрасывать отслеживаемые биты в 0, а значение бита в pinStateMask накладывается по маске OR в RXTX. Соответственно, для того чтобы разрешить отслеживание пина, необходимо вызвать для него функцию MDR_GPIO_TxPinEnable(). После этого можно пользоваться функциями MDR_Port_TxSetPins() и MDR_Port_TxClearPins(), которые являются аналогами функций MDR_Port_SetPins() и MDR_Port_ClearPins(), но работают с запоминанием ранее выведенного значения. В названии новых функций добавился только префикс Tx.

    __STATIC_INLINE     void MDR_GPIO_TxSetPins   (const MDR_GPIO_Port *GPIO_Port, uint32_t pinSelect) 
      { 
        MDR_GPIO_SoftRDTX *pSoftRDTX = GPIO_Port->pSoftRDTX;
        MDR_PORT_Type *PORTx = GPIO_Port->PORTx;
  
        pSoftRDTX->pinStateMask |= pinSelect; 
        MDR_GPIO_TxApply(PORTx, pSoftRDTX);        
      }
      
    __STATIC_INLINE     void MDR_GPIO_TxClearPins (const MDR_GPIO_Port *GPIO_Port, uint32_t pinSelect) 
      { 
        MDR_GPIO_SoftRDTX *pSoftRDTX = GPIO_Port->pSoftRDTX;
        MDR_PORT_Type *PORTx = GPIO_Port->PORTx;        
        
        pSoftRDTX->pinStateMask &= ~pinSelect; 
        MDR_GPIO_TxApply(PORTx, pSoftRDTX);
      }

Чтобы код был универсален для всех микроконтроллеров, функции с Tx для 1986ВЕ1Т и сотоварищей заведены так:

   #define MDR_GPIO_TxEnable(a, b)   UNUSED(0)
   #define  MDR_GPIO_TxSetPins       MDR_GPIO_SetPins
   #define  MDR_GPIO_TxClearPins     MDR_GPIO_ClearPins

Соответственно, когда идет настройка пинов для работы с I2C в файле MDR_SoftI2C_byTimer.с, то делается это так:

    MDR_GPIO_TxPinEnable(GPIO_Port, pinInd);  // разрешает функциям с Tx модифицировать бит
    MDR_GPIO_TxSetPins(GPIO_Port, pinSelCLK); // вывод на линию начального значения пина. (1 для I2C)

Т.е. никакой условной компиляции под разные МК не требуется.

prog/i2c/timersorfi2c.txt · Последние изменения: 2020/08/14 18:47 — vasco