stm8l программный IIC (I2C)

Часто возникает необходимость подключить к микроконтроллеру какой-нибудь датчик по протоколу I2C. Для этого можно использовать встроенный I2C микроконтроллера или написать свой, программный. Для начала надо ознакомиться с теорией. Теория очень подробно описана тут. Ознакомившись с теорией, переходим к практике. Для микроконтроллера STM8L152C6T6 напишем простой пример, когда на шине находится один master. Пример будет для IAR.

Для аппаратной реализации I2C в нашем микроконтроллере используются ножки C0 и C1. Их мы и используем в нашем примере. С0 будет линией данных SDA, С1 будет линией тактирования SCL. Из теории видно, что каждая из этих линий может работать как на приём, так и на передачу. При старте микроконтроллера в выходном регистре порта C записаны все нули. Если мы переключим, например C0, на выход, то она притянется к «0». А если мы переключим её на вход, то на ней появится «1» за счёт подтягивающих резисторов. Поэтому, передавать «0» или «1» мы будем переключением ножки с выхода на вход. Читать состояние ножки будем, как обычно, настроив её на вход. Все эти режимы для SDA и SCL пропишем в соответствующие дефайны:

#define SDA_UP     (PC_DDR_bit.DDR0 = 0)   // отпустил SDA в "1"
#define SDA_DOWN   (PC_DDR_bit.DDR0 = 1)   // установил SDA в "0"
#define SDA         PC_IDR_bit.IDR0        // состояние SDA (чтение)

#define SCL_UP     (PC_DDR_bit.DDR1 = 0)   // отпустил SCL в "1"
#define SCL_DOWN   (PC_DDR_bit.DDR1 = 1)   // установил SCL в "0"
#define SCL         PC_IDR_bit.IDR1        // состояние SCL (чтение)

Если потом мы захотим перебросить наш I2C на другие ножки, то надо будет просто подправить эти дефайны. Внимание, грабли! C0 и C1 никогда не сгорят на шине I2C т.к. у них просто нет режима Push-pull. Если использовать другие ножки, надо строго следить за тем, чтобы по ошибке не записать в них «1». Подробно про ножки можно прочитать тут.

Для реализации протокола I2C нам потребуется всего 4 команды:
— START; (начало обмена данными)
— STOP; (конец обмена данными)
— WRITE; (отправить байт на slave)
— READ; (прочитать байт из slave)

Т.к. часто I2C устройства довольно медленные, то придётся принудительно ограничить скорость обмена данными. Для этого понадобится маленькая временная задержка Short_Delay. При тактировании ядра 2 МГц задержка составит 17 us, при тактировании ядра 16 МГц задержка составит 2 us. При необходимости задержку можно легко увеличить или убрать совсем.

Снова открываем теорию и прописываем все эти 4 команды:

START

формирует старт-условие.

void I2C_START()
{
      Short_Delay(1);
      
      SDA_UP;    // отпустил SDA в "1"
      Short_Delay(1);
      
      SCL_UP;    // отпустил SCL в "1"            
      Short_Delay(1);
      
      SDA_DOWN; // SDA в "0"
      Short_Delay(1);
      
      SCL_DOWN; // SCL в "0"
      Short_Delay(1);
}

STOP

формирует стоп-условие.

void I2C_STOP()
{  
      SCL_DOWN; // SCL в "0"
      Short_Delay(1);
      
      SDA_DOWN; // SDA в "0"            
      Short_Delay(1);
      
      SCL_UP;     // отпустил SCL в "1"          
      Short_Delay(1);
      
      SDA_UP;     // отпустил SDA в "1"
}

WRITE

отправляет байт на slave. Data_out — тот байт, который будет отправляться. Это может быть адрес слэйва или собственно данные. Попутно контролируем ответ ведомого AСK.

void I2C_WRITE(uint8_t Data_out)  
{
      Short_Delay(1);
      for (uint8_t n=0; n<8; n++) 
      {                
         if(Data_out & 0x80){           
         SDA_UP;      // отпустил SDA в "1"
         while(!SDA); // ждёт освобождения SDA в "1"   
         }
         else {  
         SDA_DOWN; // SDA в "0"        
         } 
  
         Short_Delay(1);
         
         SCL_UP;      // отпустил SCL в "1"
         while(!SCL); // ждёт освобождения SCL в "1"
         Short_Delay(1);
         
         SCL_DOWN; // SCL в "0"
         Short_Delay(1);
         
         Data_out <<= 1;   //  сдвиг влево         
      }
      
      SDA_UP;     // отпустил SDA в "1"  
      Short_Delay(1);
      SCL_UP;      // отпустил SCL в "1"
      while(!SCL); // ждёт освобождения SCL в "1"
      Short_Delay(1);
      
      
      if(SDA){   // читает состояние SDA       
         Error = 1;  // нет подтверждения ACK
         }
         else {  
         Error = 0;  // есть подтверждение ACK   
         } 
      
      SCL_DOWN; // SCL в "0"
      Short_Delay(1);
      SDA_DOWN; // SDA в "0"
      Short_Delay(1);
}

READ

читает байт из slave. Возвращаемое значение Data_in — тот байт, который принимается. Ack_NoAck говорит о том, что данный байт последний и пора отправлять слэйву NACK.

uint8_t I2C_READ(uint8_t Ack_NoAck) 
{  
      uint8_t Data_in = 0;

      Short_Delay(1);
           
      for (uint8_t n=0; n<8; n++) 
      {       
        Data_in <<= 1;
        
        SDA_UP;  // отпустил SDA в "1"
        Short_Delay(1);
        
        SCL_UP;      // отпустил SCL в "1"
        while(!SCL); // ждёт освобождения SCL в "1"
        Short_Delay(1);
        
        Data_in |= SDA;   // читает состояние SDA
        
        SCL_DOWN; // SCL в "0"
        Short_Delay(1);
      }

      if(!Ack_NoAck){  // ACK          
                  
        SDA_DOWN; // SDA в "0"
        Short_Delay(1);
        
        SCL_UP;  // отпустил SCL в "1"
        while(!SCL); // ждёт освобождения SCL в "1"
        Short_Delay(1);
           
        SCL_DOWN; // SCL в "0"
        Short_Delay(1);
        
      }   
      else {  // NACK ЭТОТ БАЙТ ПРИНИМАЕТСЯ ПОСЛЕДНИМ
        
        SDA_UP;  // отпустил SDA в "1"
        while(!SDA); // ждёт освобождения SDA в "1"  
        Short_Delay(1);
        
        SCL_UP;  // отпустил SCL в "1"
        while(!SCL); // ждёт освобождения SCL в "1"
        Short_Delay(1);
           
        SCL_DOWN; // SCL в "0"
        Short_Delay(1);
        
        SDA_DOWN; // SDA в "0"
        Short_Delay(1);                
      } 
      
return(Data_in); 
}

Short_Delay

маленькая временная задержка.

void Short_Delay(volatile uint8_t delay_time)  
{
  while(delay_time-- > 0);   
}


Очень советую подключить к микроконтроллеру осциллограф или логический анализатор и посмотреть каждую команду по отдельности. Для подключения внешней синхронизации осциллографа в примере я запрограммировал ножку PF0.

В нашем примере мы задействуем TIM4, чтобы опрашивать наш I2C датчик раз в 3 секунды. Настраиваем TIM4.


  CLK_PCKENR1_bit.PCKEN12 = 1; //Включаем тактирование таймера 4
  TIM4_PSCR = 0x0F; // Предделитель на  32768 0x0F
  TIM4_ARR = 0xB6;  // Считать до 183-1 -1 вычесть 1 чтобы деление было правильно 
  TIM4_IER_bit.UIE = 1;  // Разрешаем прерывание
  TIM4_CR1_bit.URS = 1;  //Прерывание только по переполнению счетчика
  TIM4_EGR_bit.UG = 1;   //Вызываем Update Event, чтобы обновился предделитель  
  TIM4_CR1_bit.CEN = 1;  // Активируем таймер получаем 2 000 000 / 32768 /183  
  // таймер срабатывает каждые 3 S

Таймер будет срабатывать по переполнению и поднимать флаг готовности в основном цикле.

#pragma vector = TIM4_UIF_vector // Прерывание по переполнению TIM4
__interrupt void TIM4_UIF(void)
{
 TIM4_Start = 1; // поднимаем флаг готовности TIM4 
 TIM4_SR1_bit.UIF = 0; // Обнуляем бит выхода из прерывания  
}

Теперь давайте подключим I2C термодатчик. Возьмём самый простой и недорогой TC74A0. На 3.3V или на 5.0V — можно любой. Скачать даташит на TC74A0 можно тут. Датчик TC74A0 имеет адрес 72 и по умолчанию сразу готов передавать мастеру температуру. Открываем протокол Read Byte Format:
TC74
Как прочитать температуру с TC74A0? Она хранится по адресу 0x00. При помощи четырёх заранее заготовленных нами команд реализуем то, что видим на картинке:
while(1)
{
        
  if(TIM4_Start){  // таймер сработал           
    
      Error = 0;   // флаг ошибки сброшен
      uint8_t Slave_Address = 72;   // 1001 000
      Slave_Address <<= 1;          // 1001 0000
      int16_t temperature;  // температура с датчика
     
      I2C_START();  // старт       
      I2C_WRITE(Slave_Address);  // Адрес для последующей ЗАПИСИ      
      I2C_WRITE(0x00);   // БАЙТ НА ОТПРАВКУ 0000 0000     
      Short_Delay(5); // чтобы увидеть зазор межну командами      
      I2C_START();      
      I2C_WRITE(Slave_Address | 1);  // Адрес для последующего ЧТЕНИЯ     
      temperature = I2C_READ(1); // ЧИТАЕТ БАЙТ  (1) - последний     
      I2C_STOP();
                        
      if(Error == 0){                 
      printf("t = %d\n",temperature);   // печатает температуру      
      }
      else {  // Error = 1 нет подтверждения ACK         
      printf("Error = %d\n",Error);   // всякий раз с новой строки
      } 
      
      TIM4_Start = 0;  // обнуляем флаг готовности TIM4
  } 
};

Полученное значение температуры выводим на печать или на Ваш индикатор. На логическом анализаторе получаем вот такую аккуратную картинку:

stm8l программный I2C

Теперь давайте подключим I2C термодатчик покруче — TMP116. У него точность уже +/- 0.2 градуса, а диапазон температур от -40 до +125 градусов! Скачать даташит на TMP116 можно тут. Подключаем датчик TMP116 к микроконтроллеру, как показано на фотографии:

1 нога SCL
2 нога GND
3 нога не используем подключить на GND
4 нога выбор адреса. Подключить на GND адрес будет 72
5 нога +питания 3.0V Подключить к питанию микроконтроллера.
6 нога SDA
Termo Pad — можно подключить куда угодно, я подключил к GND

Не забываем подключить подтягивающие резисторы на SCL и SDA (я поставил по 10Ком). Обязательно установите блокирующий конденсатор по питанию. У меня он подпаян с обратной стороны картонки. Датчик TMP116 небольшой, но подпаяться можно.

tmp116

Теперь датчик TMP116 имеет адрес 72 и по умолчанию сразу готов передавать мастеру температуру. В даташите находим протокол Read Word Command Timing Diagramm:
TMP116

Как прочитать температуру с TMP116? Она хранится по адресу 0x00. При помощи четырёх заранее заготовленных нами команд реализуем то, что видим на картинке:


      I2C_START();  // старт       
      I2C_WRITE(Slave_Address);  // Адрес для последующей ЗАПИСИ      
      I2C_WRITE(0x00);   // БАЙТ НА ОТПРАВКУ 0000 0000      
      Short_Delay(5); // чтобы увидеть зазор межну командами      
      I2C_START();  // повторный старт     
      I2C_WRITE(Slave_Address | 1);  // Адрес для последующего ЧТЕНИЯ      
      temperature = I2C_READ(0); // ЧИТАЕТ БАЙТ  (0)      
      temperature <<= 8;      
      temperature |= I2C_READ(1); // ЧИТАЕТ БАЙТ  (1) - последний      
      I2C_STOP();
                
      temperature = (int32_t)temperature * 100 / 128;

Вставляем этот код вместо кода предыдущего примера. Значение температуры теперь занимает уже 2 байта. На печать выводится температура с точностью до сотых долей градуса! Если Вы выводите это значение на индикатор, зажгите точку во втором разряде. На логическом анализаторе получаем не менее аккуратную картинку:

stm8l программный I2C
TMP116 — достаточно сложный и навороченный датчик. Но теперь Вы сможете программировать его так, как захотите, используя даташит и набор из 4-х нехитрых команд. Надеюсь, теперь подключение любого I2C слэйва к STM8L не составит для Вас большого труда. Если будет интерес, в следующий раз разберём аппаратный I2C. Всем удачи! Пример во вложении.
  • +4
  • 29 мая 2019, 17:22
  • CreLis
  • 1
Файлы в топике: I2C_TC74A0_PRINT.zip

Комментарии (6)

RSS свернуть / развернуть
Спасибо. Интересно, нужно, полезно. Но мне не очень нравится I2C геморойный он слегка. Проще с юарт или spi. Но по факту чаще всего с I2C и приходится работать.
0
при 2 мГц задержка на 17 us
А я думал, только китайцы на алиэкспрессе частоту радиомодулей мерят в миллигерцах. Для радиолюбителя — стыд и позор…
0
Поправил, спасибо!
0
ответ ведомого ASK
пора отправлять слэйву NASK
Вообще-то, они называются ACK и NAK.

Зачем в начале процедур START и STOP перевод шины в то состояние, в котором она и так уже должна быть? И тем более — зачем задержки при этом?

Тела макросов SDA*/SCL* я бы взял в скобки.
+1
  • avatar
  • Vga
  • 30 мая 2019, 02:02
Все замечания по делу. Поправил. Спасибо! В процедурах START и STOP есть лишние переводы шин, согласен. Это чтобы можно было наблюдать все команды на осциллографе по отдельности. Эти переводы шин, конечно, можно убрать и уменьшить время обращения к слэйву.
0
О, как-раз делал то-же самое для китайских LoRa-модулей с таким-же процом внрутрях.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.