I2C и SPI: когда что
I2C (в AVR называется TWI — Two-Wire Interface) — две линии SDA/SCL с подтяжкой, адресация до 127 устройств на одной паре проводов, 100/400 кГц. Медленнее, но экономит ножки: датчики, RTC, EEPROM, OLED-дисплеи.
SPI — 4 линии (MOSI, MISO, SCK + CS на каждое устройство), полный дуплекс, мегагерцы скорости. Быстрее и проще, но на каждое устройство — свой CS: дисплеи, SD-карты, радиомодули nRF24, сдвиговые регистры.
TWI-мастер на ATmega328P: драйвер с нуля
Аппаратный TWI висит на PC4 (SDA) и PC5 (SCL). Частота задаётся TWBR и предделителем в TWSR:
// F_scl = F_CPU / (16 + 2*TWBR * prescaler), prescaler=1:
// для 100 кГц при 16 МГц: TWBR = (16000000/100000 - 16)/2 = 72
#define TWI_FREQ 100000UL
#define TWBR_VAL ((F_CPU / TWI_FREQ - 16) / 2)
static void twi_init(void)
{
TWSR = 0; // prescaler = 1
TWBR = (uint8_t)TWBR_VAL; // 72 -> 100 кГц
}
Примитивы транзакции — start, stop, запись байта, чтение с ACK/NACK. Каждый шаг — ждём сброса флага TWINT:
#define TWI_TIMEOUT 10000
static uint8_t twi_wait(void)
{
uint16_t t = TWI_TIMEOUT;
while (!(TWCR & (1 << TWINT)) && --t)
;
return t != 0; // 0 = таймаут, шина зависла
}
static void twi_start(void)
{
TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
twi_wait();
}
static void twi_stop(void)
{
TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN);
}
static uint8_t twi_write(uint8_t data)
{
TWDR = data;
TWCR = (1 << TWINT) | (1 << TWEN);
if (!twi_wait()) return 0;
// TWSR & 0xF8: 0x18/0x28 = ACK, 0x20/0x30 = NACK
return ((TWSR & 0xF8) != 0x20) && ((TWSR & 0xF8) != 0x30);
}
static uint8_t twi_read_ack(void)
{
TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA);
twi_wait();
return TWDR;
}
static uint8_t twi_read_nack(void)
{
TWCR = (1 << TWINT) | (1 << TWEN); // без TWEA -> NACK -> stop
twi_wait();
return TWDR;
}
Последний байт при чтении всегда подтверждаем NACK — иначе слейв продолжит слать данные после нашего STOP.
Пример: EEPROM AT24C32 на шине I2C
Запись байта по адресу ячейки: START → адрес устройства+W (0xAE для AT24C32) → старший/младший байт адреса → данные → STOP. После записи чипу нужно ~5 мс на внутренний цикл — либо ждём, либо опрашиваем ACK (acknowledge polling):
#define EEPROM_W 0xAE
#define EEPROM_R 0xAF
static void ee24_write(uint16_t addr, uint8_t data)
{
twi_start();
twi_write(EEPROM_W);
twi_write(addr >> 8);
twi_write(addr & 0xFF);
twi_write(data);
twi_stop();
// acknowledge polling: крутим start+W, пока не придёт ACK
do {
twi_start();
} while (!twi_write(EEPROM_W));
twi_stop();
}
static uint8_t ee24_read(uint16_t addr)
{
// «фиктивная запись» — установить внутренний указатель
twi_start();
twi_write(EEPROM_W);
twi_write(addr >> 8);
twi_write(addr & 0xFF);
// повторный старт и чтение
twi_start();
twi_write(EEPROM_R);
uint8_t d = twi_read_nack();
twi_stop();
return d;
}
Пример: чтение MPU6050 (акселерометр)
Адрес MPU6050 — 0x68 (W=0xD0, R=0xD1). Аксель X лежит в регистрах 0x3B (старший) и 0x3C:
static uint8_t mpu_read_reg(uint8_t reg)
{
twi_start();
twi_write(0xD0);
twi_write(reg);
twi_start();
twi_write(0xD1);
uint8_t v = twi_read_nack();
twi_stop();
return v;
}
static int16_t mpu_accel_x(void)
{
return (int16_t)((mpu_read_reg(0x3B) << 8) | mpu_read_reg(0x3C));
}
// инициализация: вывести из сна
static void mpu_init(void)
{
twi_start();
twi_write(0xD0);
twi_write(0x6B); // PWR_MGMT_1
twi_write(0x00); // clock = internal 8 MHz, sleep off
twi_stop();
}
Паттерн «записать номер регистра → повторный start → прочитать» универсален для 99% I2C-датчиков: BMP280, DS1307, SSD1306, HMC5883L работают ровно так же.
Сканер шины I2C
Полезнейшая отладочная утилита — перебрать все адреса и посмотреть, кто отвечает ACK:
for (uint8_t a = 1; a < 127; a++) {
twi_start();
if (twi_write(a << 1))
printf("device at 0x%02X\r\n", a);
twi_stop();
}
Не отвечает ни одно устройство? В 90% случаев нет (или сгорели) подтягивающих резисторов 4,7 кОм на SDA/SCL, либо перепутаны линии.
SPI-мастер: проще и быстрее
SPI на ножках: PB3 MOSI, PB4 MISO, PB5 SCK, PB2 SS. Мастер задаёт частоту битами SPR0/1 (и SPI2X):
static void spi_init(void)
{
DDRB |= (1 << PB3) | (1 << PB5) | (1 << PB2); // MOSI, SCK, SS — выходы
DDRB &= ~(1 << PB4); // MISO — вход
PORTB |= (1 << PB2); // CS неактивен (высокий)
SPCR = (1 << SPE) | (1 << MSTR) // включить, мастер
| (1 << SPR0); // f/16 = 1 МГц при 16 МГц
}
static uint8_t spi_transfer(uint8_t data)
{
SPDR = data;
while (!(SPSR & (1 << SPIF)))
;
return SPDR; // принятый байт
}
SPI — сдвиговый регистр: каждый переданный байт одновременно принимает байт от слейва. «Прочитать» = передать 0xFF/0x00 и забрать возвращённое.
Пример: дисплей MAX7219 (8×8 LED / семисегменты)
MAX7219 принимает 16 бит: адрес регистра + данные. Линия LOAD (=SS) фиксирует передачу:
static void max7219_write(uint8_t reg, uint8_t data)
{
PORTB &= ~(1 << PB2); // CS low
spi_transfer(reg);
spi_transfer(data);
PORTB |= (1 << PB2); // CS high — защёлкнуть
}
static void max7219_init(void)
{
max7219_write(0x0C, 0x01); // shutdown off
max7219_write(0x0F, 0x00); // test off
max7219_write(0x0B, 0x07); // scan all 8 digits
max7219_write(0x09, 0x00); // no decode (матрица)
max7219_write(0x0A, 0x05); // яркость
}
// строка i (1..8) матрицы 8x8:
static void max7219_row(uint8_t row, uint8_t pixels)
{
max7219_write(row, pixels);
}
Пример: 74HC595 — 8 выходов по трём проводам
Сдвиговый регистр превращает SPI в параллельные выходы. Данные в DS(PB3/MOSI), такты в SHCP(PB5/SCK), защёлка STCP — любая ножка, здесь PB2:
static void sr595_write(uint8_t data)
{
PORTB &= ~(1 << PB2); // защёлка вниз
spi_transfer(data);
PORTB |= (1 << PB2); // фронт — данные на выходах Q0..Q7
}
8 байт = 64 выхода каскадом: spi_transfer в цикле от последнего регистра к первому — цепочка сдвигает данные через все 595-х.
Типовые ошибки на шинах
- Нет pull-up на I2C — обязательны 4,7 кОм на обе линии (на платах модулей обычно уже есть — два модуля = 2,35 кОм, ещё терпимо).
- Забыли TWEN в TWCR — мастер «нажал start», а аппаратура выключена.
- Чтение без NACK в конце — слейв держит шину, следующая транзакция мертва.
- SS мастера как вход — у AVR ножка SS в режиме мастера должна быть выходом или подтянута; случайный ноль на ней переключает SPI в slave — классический «SPI умер сам».
- CS не поднят между транзакциями — многие слейвы (MAX7219, SD) требуют фронта CS.
- Разные режимы CPOL/CPHA — у SPI 4 режима; если слейв отвечает мусором, сверьте биты CPOL/CPHA в SPCR с даташитом устройства.
- 3,3 В слейв на 5 В шине — I2C изолирован подтяжкой к 3,3 В спасёт, а вот SPI напрямую жжёт входы: нужен level shifter или делитель.
Итог
TWI и SPI покрывают почти всю внешнюю периферию AVR: сенсоры, память, дисплеи, радио. Свой драйвер вместо Arduino Wire — это понимание каждого такта на шине и полный контроль над таймаутами и ошибками. На этом базовый набор периферии закрыт: GPIO, таймеры, прерывания, UART, АЦП, EEPROM, сон, I2C, SPI — дальше только комбинировать в свои проекты.