Top.Mail.Ru

Часть 12. STM32 и C++. I2C в Cortex-M4.

Сегодня расскажу, как работать с I2C на ядре Cortex-M4. Для других ядер алгоритм будет другой. Почему так сделано, не знаю.

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

Библиотека содержит 4 основные функции, на которых построено всё. В ней рассмотрено только подключение устройств с 7-ми битным адресом. Этого вполне достаточно для подключения широкого спектра устройств и датчиков. Я не стал делать адреса более 7-ми бит, так как не нашёл на чём проверить.

int16_t   MasterRead(uint8_t DevAddress, uint8_t* DataBuff, uint16_t Size, uint16_t TimeOut);
int16_t   MasterWrite(uint8_t DevAddress, uint8_t* DataBuff, uint16_t Size, uint16_t TimeOut);
int16_t   MasterMemRead(uint8_t DevAddress, uint16_t MemAddress, uint8_t MemAddSise, uint8_t* DataBuff, uint16_t Size, uint16_t TimeOut);
int16_t   MasterMemWrite(uint8_t DevAddress, uint16_t MemAddress, uint8_t MemAddSise, uint8_t* DataBuff, uint16_t Size, uint16_t TimeOut);

По названию функций можно понять, что они делают. Так же как и в HAL я ввёл функции MasterMemRead() и MasterMemWrite(). Они предназначены для работы с EEPROM по интерфейсу I2C. Так как они немного отличаются от MasterRead() и MasterWrite(), я не стал ломать голову, а просто повторил HAL. Slave функции я не писал. На данный момент они мне не нужны, и надеюсь никогда не понадобятся. Проходить через этот ад я больше не хочу. Меня ждут ещё другие ядра.

Все четыре функции описывать не буду, они похожи, поэтому покажу алгоритм только для функции MasterRead(<Адрес>, <Указатель на буфер>, <Количество байт>, <Таймаут>), где:

  1. <Адрес> - адрес нашей железки, в которую мы будем писать.
  2. <Указатель на буфер> - адрес буфера, в котором мы должны приготовить записываемые данные.
  3. <Количество байт> - то количество байт, которые хранятся в буфере.
  4. <Таймаут> - время, в течение которого происходит ожидание какого-либо флага. Даже если у вас зависло устройство, находящееся на шине I2C, через время указанное в этой переменной мы выйдем из функции с номером ошибки. И библиотека не завесит всё устройство.

А теперь сам алгоритм:

  1. Ждём сброса флага Busy;
  2. Если передатчик не включен, включаем;
  3. Запрос готовности на чтение. _RequestDataRead(DevAddress, TimeOut) - будет описана позже;
  4. Проверяем <Количество байт>;
    1. Если 0 - cбрасываем флаги и посылаем STOP;
    2. Если 1 - сбрасываем ACK, сбрасываем флаги, посылаем STOP;
    3. Если 2 - сбрасываем ACK, устанавливаем POS, сбрасываем флаги;
    4. Если больше 2-х - устанавливаем ACK, сбрасываем флаги;
  5. Цикл пока есть передаваемые байты;
    1. Если остался один байт;
      1. Ждём RXNE флаг;
      2. Передаём байт;
      3. Передвигаем <Указатель на буфер>;
      4. Декрементируем <Количество байт>;
    2. Если осталось два байта;
      1. Ждём сброса флага BTF;
      2. Посылаем STOP;
      3. Передаём один байт;
      4. Передвигаем <Указатель на буфер>;
      5. Декрементируем <Количество байт>;
      6. Передаём один байт;
      7. Передвигаем <Указатель на буфер>;
      8. Декрементируем <Количество байт>;
    3. Если осталось три байта;
      1. Ждём сброса флага BTF;
      2. Сбрасываем ACK;
      3. Передаём байт;
      4. Передвигаем <Указатель на буфер>;
      5. Декрементируем <Количество байт>;
      6. Ждём сброса флага BTF;
      7. Посылаем STOP;
      8. Передаём один байт;
      9. Передвигаем <Указатель на буфер>;
      10. Декрементируем <Количество байт>;
      11. Передаём один байт;
      12. Передвигаем <Указатель на буфер>;
      13. Декрементируем <Количество байт>;
    4. Осталось больше трёх байт;
      1. Ждём сброса флага RXNE;
      2. Передаём один байт;
      3. Передвигаем <Указатель на буфер>;
      4. Декрементируем <Количество байт>;
      5. Если флаг BTF установлен;
        1. Передаём один байт;
        2. Передвигаем <Указатель на буфер>;
        3. Декрементируем <Количество байт>;
  6. Зацикливаемся на пункте "5";
  7. Возвращаем "0". У нас получилось;

Как видите, жуть жуткая. Как реализован сам код, смотрите в библиотеке. Здесь его приводить не буду. Админ расстреляет 🙂

Рассмотрим алгоритм int16_t _RequestDataRead(uint8_t DevAddress, uint16_t TimeOut), которая используется в приведённой выше функции:

  1. Выдаём на шину ACK;
  2. Выдаём на шину START;
  3. Если флаг SB сбросился, мониторим бит START. Если не поднялся - вываливаемся с ошибкой;
  4. Сдвигаем адрес периферии на один бит влево, и бит RD обнуляем;
  5. Выкидываем пакет на шину I2C;
  6. Ждём флаг AF. Если нет его, вываливаемся;
  7. Очищаем все флаги;
  8. Выдаём на шину START;
  9. Если флаг SB сбросился, мониторим бит START. Если не поднялся - вываливаемся с ошибкой;
  10. Сдвигаем адрес периферии на один бит влево, и бит RD устанавливаем;
  11. Ждём флаг AF. Если нет его, вываливаемся;
  12. Вроде всё получилось, выходим без ошибки.

В оставшихся функциях алгоритм легко проследить по коду. Кроме этого в private находятся служебные функции, которые мониторят некоторые флаги. Вынесены они в функции из-за того, что иногда необходимо мониторить, поднят флаг или опущен, поэтому, чтобы не запутаться я сделал как в HAL. И это упростило написание драйвера. Кроме этого, ни в одной библиотеке, что я видел, нет поиска устройств на шине. Это на Ардуине делается просто, здесь же не очень. Поэтому я дополнил библиотеку функцией ScanDevice(uint8_t *BuffDevice), которой передаётся адрес буфера размером не менее 127 байт, в котором перечисляются все найденные устройства.

Инициализация I2C производится простой парой команд. До функции main() необходимо обратиться к конструктору:

I2C myI2C(I2C2, false);

Здесь указываем периферию, которой будем пользоваться, а также нужна альтернативная распиновка или нет. Инициализация самого устройства производится в функции main() строкой:

myI2C.Init(Mode_Slow);

На данный момент инициализация работает только в "медленном режиме". В "быстром" пока нет. Инициализацию описывать не буду, у всей периферии она одинакова, только регистры разные. Хочу только предупредить. В данном случае используется режим выводов "открытый сток", поэтому внутренние подтягивающие резисторы в конечном устройстве лучше не использовать. Помех наловитесь. Обязательно ставьте внешние на 4.7 КОм - 5.1 КОм.

Позже выложу библиотеку с использованием I2C для работы с памятью типа AT24Cxx.

Ссылка на проекты на Яндекс Диске и архивом WorkDevel.

Подписаться
Уведомление о
guest
1 Комментарий
старее
новее
Inline Feedbacks
View all comments
1
0
Would love your thoughts, please comment.x
()
x