Сегодня расскажу, как работать с 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(<Адрес>, <Указатель на буфер>, <Количество байт>, <Таймаут>)
, где:
- <Адрес> - адрес нашей железки, в которую мы будем писать.
- <Указатель на буфер> - адрес буфера, в котором мы должны приготовить записываемые данные.
- <Количество байт> - то количество байт, которые хранятся в буфере.
- <Таймаут> - время, в течение которого происходит ожидание какого-либо флага. Даже если у вас зависло устройство, находящееся на шине I2C, через время указанное в этой переменной мы выйдем из функции с номером ошибки. И библиотека не завесит всё устройство.
А теперь сам алгоритм:
- Ждём сброса флага Busy;
- Если передатчик не включен, включаем;
- Запрос готовности на чтение.
_RequestDataRead(DevAddress, TimeOut)
- будет описана позже; - Проверяем <Количество байт>;
- Если 0 - cбрасываем флаги и посылаем STOP;
- Если 1 - сбрасываем ACK, сбрасываем флаги, посылаем STOP;
- Если 2 - сбрасываем ACK, устанавливаем POS, сбрасываем флаги;
- Если больше 2-х - устанавливаем ACK, сбрасываем флаги;
- Цикл пока есть передаваемые байты;
- Если остался один байт;
- Ждём RXNE флаг;
- Передаём байт;
- Передвигаем <Указатель на буфер>;
- Декрементируем <Количество байт>;
- Если осталось два байта;
- Ждём сброса флага BTF;
- Посылаем STOP;
- Передаём один байт;
- Передвигаем <Указатель на буфер>;
- Декрементируем <Количество байт>;
- Передаём один байт;
- Передвигаем <Указатель на буфер>;
- Декрементируем <Количество байт>;
- Если осталось три байта;
- Ждём сброса флага BTF;
- Сбрасываем ACK;
- Передаём байт;
- Передвигаем <Указатель на буфер>;
- Декрементируем <Количество байт>;
- Ждём сброса флага BTF;
- Посылаем STOP;
- Передаём один байт;
- Передвигаем <Указатель на буфер>;
- Декрементируем <Количество байт>;
- Передаём один байт;
- Передвигаем <Указатель на буфер>;
- Декрементируем <Количество байт>;
- Осталось больше трёх байт;
- Ждём сброса флага RXNE;
- Передаём один байт;
- Передвигаем <Указатель на буфер>;
- Декрементируем <Количество байт>;
- Если флаг BTF установлен;
- Передаём один байт;
- Передвигаем <Указатель на буфер>;
- Декрементируем <Количество байт>;
- Если остался один байт;
- Зацикливаемся на пункте "5";
- Возвращаем "0". У нас получилось;
Как видите, жуть жуткая. Как реализован сам код, смотрите в библиотеке. Здесь его приводить не буду. Админ расстреляет 🙂
Рассмотрим алгоритм int16_t _RequestDataRead(uint8_t DevAddress, uint16_t TimeOut)
, которая используется в приведённой выше функции:
- Выдаём на шину ACK;
- Выдаём на шину START;
- Если флаг SB сбросился, мониторим бит START. Если не поднялся - вываливаемся с ошибкой;
- Сдвигаем адрес периферии на один бит влево, и бит RD обнуляем;
- Выкидываем пакет на шину I2C;
- Ждём флаг AF. Если нет его, вываливаемся;
- Очищаем все флаги;
- Выдаём на шину START;
- Если флаг SB сбросился, мониторим бит START. Если не поднялся - вываливаемся с ошибкой;
- Сдвигаем адрес периферии на один бит влево, и бит RD устанавливаем;
- Ждём флаг AF. Если нет его, вываливаемся;
- Вроде всё получилось, выходим без ошибки.
В оставшихся функциях алгоритм легко проследить по коду. Кроме этого в 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.
Тема на форуме - перейти.
Статья написана давно. Выложил только сейчас. За прошедшее время проверил режим Fast. Он то же работает. Просто микруха, на которой я всё отлаживал, Fast режим не поддерживает.
Поступают вопросы по другим каналам.
Почему не сделал на прерываниях. Почему без DMA.
Ответ прост. Я не пользуюсь HAL, поэтому приходится со всем разбираться, а не использовать готовые шаблоны от производителя. А лишнее время не купишь, поэтому делаю именно то, что на данный момент мне нужно.
На горизонте возникает задача, сделать то же самое с прерываниями. Как с ней разберусь, так статья и появится.
И учитывайте, что задача имеет 100500 решений. Невозможно про всё написать статьи и выложить все решения.
А любителей покритиковать, очень прошу, лучше помогите, чем в след плевать.