CMSIS STM32 Урок 6. Работа с шиной I2C в режиме Master

Это синхронная шина обмена данными в которой присутствует один Master (ведущий) и от одного до нескольких Slave (ведомых). Соединены все по трем проводам GND, SDA (данные), SCL (тактирование).

Конфигурирование периферии I2C начинается с разрешения ее тактирования, затем нужно настроить GPIO, к которым будет подключена I2C. В примере ниже, I2C1 расположена на PB7 (SDA) и PB6 (SCL). Оба этих пина настраиваются как Alternate output Open-Drain (CNF=11, MODE=11).

Далее я приведу пример настройки интерфейса I2C в режиме мастера на скорости 100 кГц. Данный режим будет использоваться мной для работы с EEPROM памятью серии 24Сxx.

Подпишитесь на канал, чтобы получать больше информации по электронике

Настройка GPIO

RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // Разрешить тактирование GPIOB
__NOP();
// PB7 - AF SDA, PB6 - AF SCL (OpenDrain CNF=11, MODE=11) ; PB5 - in (CNF0=01 MODE0=00); PB4,PB3,PB1 - AF PWM (CNF=10, MODE=11); PB2 - out (CNF1=00 MODE1=01), PB0 - analog
GPIOB->CRL = 0xFF4BB1B0; // 1111 1111 0100 1011 1011 0001 1011 0000

I2C мастер — устройство, которое формирует события на шине и принимает события от подчиненных устройств. Но бывает так, что по какой-то причине на шине могут происходить сбои и вся последовательность событий (секвенция) нарушается. Чтобы сбросить шину в первоначальное состояние используется Software Reset. Этот программный сброс можно вынести в отдельную функцию. Если вы читаете данные, а данные не читаются, значит шина глючит, надо будет ее сбросить.

// Программный сброс I2C
void I2C_Reset(void) {
	I2C1->CR1 |= I2C_CR1_SWRST; 	// Установка и сброс SWRST
	I2C1->CR1 &= ~I2C_CR1_SWRST;	// для перезапуска I2C
}

Настройка I2C не составляет особого труда, если вы не планируете работать с прерываниями.

// Настройка I2C
void I2C_Setup(void) {
	RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; // Разрешить тактирование I2C1
	__NOP();
	I2C_Reset();
	I2C1->CR2 = 32; // Частота APB1 32 MHz
	I2C1->CCR = 16000000 / 100000; // (F_APB1/2) / желаемая скорость
	I2C1->CR1 |= I2C_CR1_PE; // Включить I2C
}

В комментариях к коду вы можете увидеть все необходимые пояснения. В конкретном примере системная частота 64 МГц, шина APB1 тактируется частотой 32 МГц. В CR2 записывается как раз таки эта частота в МГц. Допустимы значения от 2 до 36 МГц. А вот для значения CCR, делимое записывается ½ от частоты APB1, а делитель — желаемая скорость I2C. Типовая, так сказать, скорость I2C, 100 кГц. Повышенная скорость (максимальная), 400 кГц. Есть дисплеи такие, с контроллером SSD1309, работают по I2C шине (в том числе), где мне удавалось передавать данные со скоростью и 400, и 600, и даже 800 кГц успешно. Если вы видите в документации максимально допустимую частоту шины I2C 400 кГц, то это значит, что производитель гарантирует работу на этой частоте без сбоев. На сколько высоко вы можете «залезть», вам решать.

События start и stop

// Передача события Start I2C
void I2C_Start(void) {
  I2C1->CR1 |= I2C_CR1_START | I2C_CR1_ACK;
  // Ждать окончания события START
  while(!(I2C1->SR1 & I2C_SR1_SB)) {}
}

// Передача события Stop I2C
void I2C_Stop(void) {
  I2C1->CR1 |= I2C_CR1_STOP; 
  // Ждать завершения события STOP
  while(!(I2C1->CR1 & I2C_CR1_STOP)) {}  
}

Все операции с устройствами по шине I2C начинаются с события start и заканчиваются событием stop. После события start происходит «выбор» подчиненного, device select.

// Передача адреса I2C (выбор адресата)
void I2C_Select(uint8_t address, uint8_t readBit) {
  // Поместить адрес и бит чтения/записи в регистр DR
  I2C1->DR = (address << 1) | (1 & readBit);
  // Ждать окончания передачи адреса
  while(!(I2C1->SR1 & I2C_SR1_ADDR)) {}
  // Прочесть SR1 и SR2 для стирания бита ADDR
  uint8_t temp __attribute__((unused)) = I2C1->SR1 | I2C1->SR2;
}

Да, на вашей I2C шине может находиться несколько разных устройств. Например: датчик освещенности BH1750, память M24C32, OLED дисплей SSD1306. У каждого устройства свой адрес. Они все слушают мастера и отвечают лишь те, к кому обращается мастер.

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

// Запись байта по I2C
void I2C_Write(uint8_t data) { 
  // Дождаться освобождения регистра данных
  while(!(I2C1->SR1 & I2C_SR1_TXE)) {}
  // Загрузка байта в регистр DR
  I2C1->DR = data;                        
  // Дождаться окончания передачи
  while(!(I2C1->SR1 & I2C_SR1_BTF)) {}
}

Слегка уже напоминает механизм передачи данных по UART. Прием делает немного больше операций за счет того, что на шине активно используется бит подтверждения ACK.

// Прием байта по I2C (с ожиданием бита ACK и без него)
uint8_t I2C_Read(uint8_t * byte, uint8_t ack) {
  uint32_t timeout = 5000000;
  while(!(I2C1->SR1 & (I2C_SR1_RXNE))) { // Дождаться окончания приема байта
    if (!timeout--) {
	I2C_Reset();
	return 1; // Ошибка: нет ответа
    }
  }
  *byte = I2C1->DR; // Забрать прочитанный байт
  if (ack) I2C1->CR1 |= I2C_CR1_ACK; 	// Если нужно подтверждение
  else I2C1->CR1 &= ~(I2C_CR1_ACK); 	// без подтверждения
  return 0; // Без ошибок
}

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

В качестве большого примера, здесь мы разберем работу с EEPROM памятью. Большинство микросхем EEPROM могут «садиться» на одну I2C шину путем установки адреса на стадии конструирования платы. Подключая пины A0, A1, A2 то к 0, то к 1, вы задаете адрес микросхемы на шине.

Адрес EEPROM начинается с битов 01010XXX, а последние три бита XXX и есть аппаратно установленный адрес. Допустим, на одну шину I2C вы сажаете две EEPROM'ки. У первой A0, A1, A2 будут замкнуты на GND. У второй A0, A1 на GND, а A2 на +VDD. Получается, у первой микросхемы будет адрес устройства 0b1010000 (0x50), тогда у второй этот адрес будет 0b1010100 (0x54). Адрес 7-битный!

А бывают еще микросхемы без адресных лапок. Например, M24C16 от STMicroelectronics. Особенность этой памяти состоит в том, что на I2C шине она может быть только одна, и никакие другие I2C микросхемы EEPROM памяти недопустимы. Адрес устройства для этой микросхемы очень необычный. Он традиционно начинается с 0b1010XXX, но XXX – это старшие три бита адреса в области памяти! То бишь, при выборе устройства мы уже заранее передаем старшие три бита адреса в памяти, с которой планируем работать. Зачем это сделано — одному разработчику известно, но догадываюсь, для экономии трафика и ускорения обмена данными. А дальше вы поймете, почему.

Чтобы читать из памяти или писать в память, нужно 
1) создать событие старт и выбрать микросхему
2) передать микросхеме адрес в памяти, по которому будут проходить операции
3) читать или писать
4) создать событие стоп
У микросхем с лапками, задающими адрес микросхемы на шине, адрес в массиве памяти передается двумя байтами: сначала старший, затем младший. 

У микросхем без адресных лапок, первые три бита адреса (A10, A9, A8) в массиве памяти передаются вместе с командой, а последние семь бит (A7-A0) передаются одним байтом. Такой способ может использоваться в EEPROM объемом до 2 кБ, так как адрес больше 2048 задать не получится.

Вы можете определить программно, какая EEPROM установлена! Допустим у вас стоит одна EEPROM, если она отвечает только по адресу 0x50, а по 0x51 уже не отвечает, значит она с лапками.

Далее, я приведу пример работы с EEPROM памятью M24C16 (та, что без адресных лапок).

// Чтение байта из памяти. Вернет 1, если ошибка
uint8_t EEPROM_ReadByte(uint16_t addr, uint8_t * data) {
	I2C_Start();
	// Передача адреса с запросом на запись
	I2C_Select(0x50 | ((addr >> 8) & 0x07), 0); 
	I2C_Write(addr & 0xFF);
	I2C_Stop();
	I2C_Start();
	I2C_Select(0x50, 1); // Передача адреса с запросом на чтение
	if (I2C_Read(data, 0)) return 1; // Последний байт без ACK
	I2C_Stop();
	return 0;
}
// Запись 1 байта
void EEPROM_WriteByte(uint16_t addr, uint8_t data) {
	I2C_Start();
	// Передача адреса с запросом на запись
	I2C_Select(0x50 | ((addr >> 8) & 0x07), 0); 
	I2C_Write(addr & 0xFF);
	I2C_Write(data);
	I2C_Stop();
	wait_ticks32(106000); // Задержка на 5+ мс
}

В документации на все EEPROM пишут, что после операции записи, после посылки события STOP, необходимо дать как минимум 5 мс на завершение операции внутри микросхемы. Именно это и делает функция примитивной задержки wait_ticks32()

// Примитивная задержка
void wait_ticks32(uint32_t ticks) {
	while(ticks--) __NOP();
}

Ну и напоследок операции чтения и записи 16 битных значений

// Чтение 2 байтов из памяти. Вернет 1, если ошибка
uint8_t EEPROM_ReadWord(uint16_t addr, uint16_t * data) {
	uint8_t temp[2];
	I2C_Start();
	// Передача адреса с запросом на запись
	I2C_Select(0x50 | ((addr >> 8) & 0x07), 0); 
	I2C_Write(addr & 0xFF);
	I2C_Stop();
	I2C_Start();
	I2C_Select(0x50, 1); // Передача адреса с запросом на чтение
	if (I2C_Read(&temp[1], 1)) return 1; // Первый байт с ACK
	if (I2C_Read(&temp[0], 0)) return 1; // Последний байт без ACK
	I2C_Stop();
	*data = temp[0] | ((uint16_t)temp[1] << 8);
	return 0;
}
// Запись 2 байтов (слова)
void EEPROM_WriteWord(uint16_t addr, uint16_t data) {
	I2C_Start();
	// Передача адреса с запросом на запись
	I2C_Select(0x50 | ((addr >> 8) & 0x07), 0); 
	I2C_Write(addr & 0xFF);
	I2C_Write(data >> 8);
	I2C_Write(data & 0xFF);
	I2C_Stop();
	wait_ticks32(106000); // Задержка на 5+ мс
}

Эти операции просты и эффективны. А самое главное, что это и есть жесткая оптимизация, где используется минимум плюшек для решения поставленной задачи.

Напоследок я хотел бы поделиться приемом определения, какая микросхема EEPROM установлена в устройстве: с адресными ножками или без них.

Когда мы передаем адрес микросхеме (процедура выбора микросхемы), искомая микросхема должна «дернуть» шину SDA — эта реакция называется подтверждением (ACK). Когда микросхема ответила, в периферии I2C возведется флаг I2C_SR1_AF (ACK FLAG), а вместе с ним и флаг I2C_SR1_ADDR. Выходит, если мы получили флаг AF или ADDR, значит микросхема отвечает. На основании этих сведений создадим функцию определения готовности микросхемы:

// Проверка готовности устройства. вернет 0, если Ок
uint8_t I2C_IsDeviceReady(uint8_t address) {
  uint32_t cntr = 0;
  I2C_Start();
  I2C1->DR = (address << 1);
  // Ждать окончания передачи адреса
  while(!(I2C1->SR1 & I2C_SR1_ADDR)) {
    if (I2C1->SR1 & I2C_SR1_AF) return 0;
    if (++cntr >= 10000000) { // Таймаут, устройство не отвечает
	I2C_Reset();
	return 1;
    }
  }
  // Прочесть SR1 и SR2 для стирания бита ADDR
  uint8_t temp __attribute__((unused)) = I2C1->SR1 | I2C1->SR2;
  return 0;
}

У микросхемы EEPROM с адресными лапками адрес фиксированный, допустим 0x50. Она не ответит на обращение к 0x51. А вот M24C16 (без адресных лапок) ответит, разумеется. Таким образом вы понимаете, с какой микросхемой надо работать. Возможно вы будете делать прошивку устройства, поддерживающего любую EEPROM.

Обсудить эту статью

<< Предыдущий урок | Следующий урок >>

Tags: ,