[Перевод] Миниатюрные I2C процедуры для всех микроконтроллеров AVR
Простой матричный термометр на основе ATtiny84, использующем библиотеку TinyI2C
В статье описывается набор минимальных процедур, позволяющих любому процессору Microchip/Atmel AVR — подключаться к периферии по протоколу I2C. Для их демонстрации я спроектировал сканер портов, отображающий I2C-адрес сенсора на матричном дисплее, а также цифровой термометр, считывающий и отображающий температуру с I2C-датчика. Основное различие между моими процедурами и стандартной библиотекой Arduino Wire в том, что в них не используется буфер, то есть они не так требовательны к памяти и не накладывают каких-либо ограничений на передачу.
Совместимость
Задача моей библиотеки предоставить функциональность мастера для всех процессоров Microchip/Atmel AVR. За время своего существования различные поколения микросхем AVR обзавелись для обработки I2C-подключений тремя несовместимыми видами периферийных устройств:
Универсальный последовательный интерфейс (USI)
USI обеспечивает работу I2C-мастера для процессоров ATtiny с соответствующей поддержкой, а именно:
- ATtiny25/45/85 и ATtiny24/44/84;
- ATtiny261/461/861;
- ATtiny87/167;
- ATtiny2313/4313;
- ATtiny1634.
Программы для поддержки USI основаны на коде, описанном в Atmel Application Note AVR310.
Двухпроводной последовательный интерфейс (TWI)
Этот интерфейс обеспечивает полноценную поддержку I2C-мастера и используется в:
- большинстве оригинальных процессоров ATmega, таких как ATmega328P (Arduino Uno), ATmega2560 (Arduino Mega 2560) и ATmega1284P;
- двух необычных процессорах с TWI: ATtiny84 и 88.
Двухпроводной интерфейс (TWI)
Новая версия TWI используется в:
- последних процессорах ATtiny 0-, 1- и 2-серии, например, в ATtiny414;
- микросхемах ATmega 0-серии, например, в ATmega4809;
- семействе AVR DA DB, например, в AVR128DA48.
Универсальные процедуры TinyI2C
Эти универсальные процедуры предоставляют поддержку I2C-мастера для всех трёх поколений процессоров AVR за счёт трёх раздельных блоков кода. Подходящий блок выбирается автоматически через инструкции #if defined
в зависимости от используемых вами установок ядра Arduino и Board.
Я протестировал библиотеку на выборочных процессорах из каждой категории, но всё же не на всех без исключения, так что в случае обнаружения несовместимости просьба отписать на GitHub.
Написанные мной процедуры включают и расширяют две прежние версии:
Отличия от Arduino Wire
Я назвал свою библиотеку TinyI2C по двум причинам: чтобы отличать её от существующих библиотек TinyWire, таких как включённые в ядра от Arduino и Spence Konde, а также чтобы подчеркнуть её несоответствие соглашению об именовании библиотеки Arduino Wire.
При этом она отличается от Arduino Wire ещё и следующим:
Низкие требования к памяти
В моих процедурах не используются буферы, что сокращает их требования к RAM до нескольких байт. Стандартная библиотека Arduino задействует буферы отправки/получения размером 128 или 32 байта. Насколько я понимаю, здесь нет необходимости в буферизации, поскольку протокол I2C включает механизм рукопожатий с использованием сигналов ACK/NACK.
Неограниченная длина транзакции
Эти процедуры не накладывают никаких ограничений на размер передачи. Стандартные же библиотеки Wire ограничивают её длину размером буфера. Это не является проблемой во многих случаях применения I2C, таких как считывание температуры с датчика, но создаёт сложности в сценариях вроде управления I2C OLED-дисплеем, когда требуется отправлять 1024 байта для обновления всего дисплея.
Гибкое считывание
TinyI2C позволяет заранее указывать, сколько байт нужно считать с подключённого устройства, а также оставлять эту величину открытой, отметив считывание до последнего байта. Это, оказывается, очень удобно, когда количество считываемых байт наперёд неизвестно.
Опрос
С целью упрощения написанные мной процедуры используют не прерывания, а опрос, что исключает их вмешательство в работу других процессов.
Производительность
Я протестировал TinyI2C и Arduino Wire на ряде платформ и ниже привёл сравнительную таблицу потребления ими общей (флэш) и динамической (RAM) памяти в байтах в приложении матричного термометра. Информация получена из Arduino IDE.
В зависимости от платформы код TinyI2C обычно вдвое меньше кода Arduino Wire, а потребление RAM составляет всего около 5%.
Описание
Скачайте TinyI2C с GitHub.
Установите её в подкаталог libraries каталога Arduino и внесите в начало программы:
#include
Вот описание самих процедур TinyI2C:
TinyI2C.init ()
Инициализирует TinyI2C. Должна вызываться в setup()
.
TinyI2C.start (address, type)
Запускает транзакцию с подчинённым устройством по указанному адресу, а также устанавливает характер этой транзакции — считывание или запись. В случае успеха возвращает true
, а в случае ошибки false
.
Параметр type
может содержать следующие значения:
0
: запись на устройство;1
— 32767: считывание с устройства. Это число указывает, сколько байт нужно будет считать;-1
: считывание с устройства неустановленного количества байт.
Если в type
установлено -1
, необходимо определить последний считываемый байт, вызвав TinyI2C.readlast()
вместо TinyI2C.read()
.
TinyI2C.write (data)
Записывает байт данных на подчинённое устройство. В случае успеха возвращает true
, а в случае ошибки — false
.
TinyI2C.read ()
Считывает байт с подчинённого устройства и возвращает его.
TinyI2C.readLast ()
Считывает байт с подчинённого устройства и даёт команду завершить отправку.
TinyI2C.readlast()
нужно использовать только в случае вызова TinyI2C.start()
либо TinyI2C.restart()
с type
, установленным на -1
.
TinyI2C.restart (address, type)
Делает перезапуск. Параметр type
остаётся такой же, как для TinyI2C.start()
.
TinyI2C.stop ()
Завершает транзакцию, возвращаемого значения не имеет.
Для каждой TinyI2C.start()
должна присутствовать соответствующая TinyI2C.stop()
.
Использование библиотеки TinyI2C
Подтягивающие резисторы
Для надёжной работы I2C у вас на линиях SCL и SDA должны находиться подтягивающие резисторы с рекомендованным сопротивлением 4.7кОм или 10кОм. На тех платформах, где это возможно, процедуры TinyI2C активируют внутренние подтягивания на линиях SCL и SDA, поскольку навредить это не может, но полагаться на них не стоит.
Запись на устройство I2C
Это довольно простой процесс. Вот пример записи одного байта:
TinyI2C.start(Address, 0);
TinyI2C.write(byte);
TinyI2C.stop();
Считывание с устройства I2C
Процедуры TinyI2C позволяют определить считывание до последнего байта одним из двух способов:
1. Указать общее число байт, которые нужно считать в качестве второго параметра функции TinyI2C.start()
. В этом случае TinyI2C.read()
автоматически завершит последний вызов сигналом NAСK:
TinyI2C.start(Address, 2);
int mins = TinyI2C.read();
int hrs = TinyI2C.read();
TinyI2C.stop();
2. Просто установить второй параметр TinyI2C.start()
как -1
и явно определить последнее считывание TinyI2C.read
вызовом TinyI2C.readlast()
.
TinyI2C.start(Address, -1);
int mins = TinyI2C.read();
int hrs = TinyI2C.readLast();
TinyI2C.stop();
Запись и считывание
Многие устройства I2C требуют перед считыванием выполнить запись одного или нескольких байт, чтобы указать целевой регистр. Такое считывание должно производиться вызовом TinyI2C.restart()
.
Например:
TinyI2C.start(Address, 0);
TinyI2C.write(1);
TinyI2C.restart(Address, 2);
int mins = TinyI2C.read();
int hrs = TinyI2C.read();
TinyI2C.stop();
Образцы программ
Я протестировал процедуры на двух программах:
- на сканере портов I2C, отображающем адрес всех подключённых к I2C-шине устройств в виде точки на двух матричных дисплеях 8×8;
- на простом цифровом термометре, показывающем температуру на тех же матричных дисплеях 8×8.
Эти примеры я выбрал, потому что они демонстрируют и запись, и считывание данных по I2C.
Схема
Тестовые программы работают на одной и той же схеме. Вот версия с ATtiny84:
Схема матричного термометра на основе ATtiny84
В качестве дисплеев я использовал Adafruit I2C mini 8×8, которые в UK можно приобрести на сайте Pi-Hut. Выпускаются эти дисплеи в разных цветах. Можно также использовать I2C-дисплеи Keyestudio. Адреса этих двух дисплеев нужно будет установить на разные значения, напаяв перемычку на линии A0 с задней стороны одного из дисплеев. Матричные дисплеи имеют подтягивающие резисторы I2C, так что внешние вам не потребуются.
Данные о температуре передаются датчиком PCT2075 SOIC-8 I2C. По выводам он полностью совместим с популярным LM75, но при этом обеспечивает не 9-, а 11-битную точность. Кроме того, PCT2075 позволяет выводам адресации работать в трёх состояниях, давая возможность присваивать один из 27 адресов I2C. Если же три линии адреса не подключены, он определяется как 0х37
.
Программы
Матричные дисплеи настраиваются через InitDisplay()
:
void InitDisplay (int address) {
TinyI2C.start(address, 0);
TinyI2C.write(0x21);
TinyI2C.restart(address, 0);
TinyI2C.write(0x81);
TinyI2C.restart(address, 0);
TinyI2C.write(0xe0 + Brightness);
TinyI2C.stop();
}
Процедура ClearDisplay()
очищает указанный дисплей, записывая нули:
void ClearDisplay (int address) {
TinyI2C.start(address, 0);
for (int i=0; i<17; i++) TinyI2C.write(0);
TinyI2C.stop();
}
I2C сканер портов
Работает I2C сканер следующим образом. Сначала дисплеи очищаются. Затем по ходу проверки каждого адреса он зажигает соответствующую этому адресу точку, где 0 — это верхняя левая, 15 — верхняя правая, а 127 — нижняя правая. Довольно удобно, что на дисплеях есть 128 точек, так как это соответствует количеству поддерживаемых I2C адресов.
При обнаружении на адресе устройства точка гаснет. В случае со схемой выше отображаться будут адреса дисплеев 0x70
и 0x71
, а также адрес 0x37
, используемый температурным датчиком:
Схема матричного I2C сканера портов на основе ATtiny84
Для проверки других I2C устройств просто подключите их к шине по линиям SCL или SDA.
Программа вызывает процедуру Plot()
для отображения на соответствующем дисплее пикселя по координатам (x, y), где x простирается от 0 до 15, y от 0 до 7, а точка (0, 0) соответствует верхнему левому пикселю:
void Plot (int x, int y) {
uint8_t address;
if (x > 7) address = DisplayRight; else address = DisplayLeft;
TinyI2C.start(address, 0);
TinyI2C.write(y * 2);
TinyI2C.restart(address, 1);
uint8_t row = TinyI2C.read();
TinyI2C.restart(address, 0);
TinyI2C.write(y * 2);
TinyI2C.write(row | 1<<((x + 7) & 7));
TinyI2C.stop();
}
Сканирование выполняет функция loop()
:
void loop () {
for (int p=0; p<128; p++) {
if (!TinyI2C.start(p, 0)) Plot(p&15, p>>4);
delay(50);
}
for(;;);
}
Матричный термометр
Матричный термометр ежесекундно считывает температуру с датчика и обновляет дисплеи, отображая её в градусах Цельсия с точностью до одного десятичного знака.
Для цифр от 0 до 9 дисплей использует символы размером 3×7 пикселей, которые определяются так:
const uint32_t PROGMEM Digits [8] = {
0b010010111010111001010010010010,
0b101101100101001001101101011101,
0b101101100001001101100100010101,
0b110010010011011111010010010101,
0b100101001101100100100001010101,
0b101101001101101100101001010101,
0b010010001010010100010111111010 };
С целью экономии RAM определения символов сохраняются в память программы. Эти 32-битные слова соответствуют следующей битовой карте, которая содержит цифры от 0 до 9 размером 3×7 пикселей, отражённые справа налево:
Процедура PlotDigit()
рисует цифру n на указанном дисплее со смещением x:
void PlotDigit (int address, uint8_t x, uint8_t n) {
for (int y=0; y<7; y++) {
TinyI2C.start(address, 0);
TinyI2C.write(y * 2);
TinyI2C.restart(address, 1);
uint8_t row = TinyI2C.read();
TinyI2C.restart(address, 0);
TinyI2C.write(y * 2);
uint8_t b = (pgm_read_dword(&Digits[y])>>(3*n) & 7);
TinyI2C.write((row & ~(7<<(x - 1))) | (b<>1);
TinyI2C.stop();
}
}
Она считывает предыдущее состояние каждой строки, очищает три пикселя в смещении x, после чего рисует соответствующие биты из набора символов. Выражение (b<
компенсирует тот факт, что ввиду особенности подключения дисплеев, биты каждой строки нумеруются справа налево, в порядке 7, 0, 1, 2, 3, 4, 5, 6.
Процедура Symbols()
рисует символ градуса и десятичную точку:
void Symbols (int address) {
TinyI2C.start(address, 0);
TinyI2C.write(0);
TinyI2C.write(0x60);
TinyI2C.write(0);
TinyI2C.write(0x60);
TinyI2C.restart(address, 0);
TinyI2C.write(6*2);
TinyI2C.write(0x80);
TinyI2C.stop();
}
Показания датчика PCT2075 считывает следующая процедура, которая возвращает значение температуры в виде целого числа в 1/8 долях градуса Цельсия:
int PCT2075Temp (int address) {
TinyI2C.start(address, 0);
TinyI2C.write(0);
TinyI2C.restart(address, 2);
uint8_t hi = TinyI2C.read();
uint8_t lo = TinyI2C.read();
int temp = hi<<3 | lo>>5;
return (temp & 0x03FF) - (temp & 0x0400);
}
Наконец, температура ежесекундно считывается и отображается на дисплеях в виде трёх цифр функцией loop()
:
void loop () {
int temp = PCT2075Temp(ThermometerAddress);
PlotDigit(DisplayLeft,0,temp/80);
PlotDigit(DisplayLeft,4,temp/8 % 10);
PlotDigit(DisplayRight,2,(temp%8 * 10 + 4)/8);
delay(1000);
}
Запуск программ
Устройство USI
Для тестирования программы с устройством USI я использовал ATtiny84, 14-пиновую микросхему с 8КБ программной памяти и 512Б RAM. Дополнительно я протестировал её с использованием ATtiny85.
Для компиляции использовалась ATTinyCore от Spence Konde. Имейте в виду, что после загрузки программы на ATtiny84 (или ATtiny85) через ISP нужно будет отключить ISP MOSI на выводе 7 (или 5) и повторно подать питание, поскольку цепь ISP сталкивается с проходящим по тому же выводу сигналом SDA.
Устройство с двухпроводным последовательным интерфейсом (TWI)
Для тестирования работы программ с 2-Wire Serial Interface в оригинальных микросхемах ATmega я использовал Arduino Uno, подключённый к макетной плате четырьмя проводами (GND, VDD, SCL и SDA), а также задействовал ядро Arduino AVR Boards.
Я протестировал её и на ATtiny88, оснащённом именно этим периферийным модулем, а не USI, как в прочих ATtiny. В этом случае в качестве ядра использовал ATTinyCore.
Кроме того, я проверил программу на ATmega1284P при помощи MightyCore от MCUDude.
Устройство с двухпроводным интерфейсом
Для тестирования программы с подобным представителем последних микросхем ATtiny 0- и 1-серий я использовал ATtiny414, 14-пиновый процессор с 4КБ программной памяти и 256Б RAM:
Версия матричного термометра с ATtiny414
Схема матричного термометра на ATtiny414
Программу я скомпилировал при помощи megaTinyCore от Spence Konde и загрузил её через программатор UPDI.
Я также протестировал её на отдельной макетке с ATmega4809, скомпилировал с помощью MegaCoreX от MCUDude и загрузил на микросхему через UPDI.
Ну и напоследок я провёл тест с AVR128DA28, скомпилировал программу с помощью DxCore от Spence Konde и снова загрузил на микросхему через UPDI.