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

Для аппаратной реализации 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:

Как прочитать температуру с 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
}
};
Полученное значение температуры выводим на печать или на Ваш индикатор. На логическом анализаторе получаем вот такую аккуратную картинку:

Теперь давайте подключим 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 имеет адрес 72 и по умолчанию сразу готов передавать мастеру температуру. В даташите находим протокол Read Word Command Timing Diagramm:

Как прочитать температуру с 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 байта. На печать выводится температура с точностью до сотых долей градуса! Если Вы выводите это значение на индикатор, зажгите точку во втором разряде. На логическом анализаторе получаем не менее аккуратную картинку:

TMP116 — достаточно сложный и навороченный датчик. Но теперь Вы сможете программировать его так, как захотите, используя даташит и набор из 4-х нехитрых команд. Надеюсь, теперь подключение любого I2C слэйва к STM8L не составит для Вас большого труда. Если будет интерес, в следующий раз разберём аппаратный I2C. Всем удачи! Пример во вложении.
- +4
- 29 мая 2019, 17:22
- CreLis
- 1
Файлы в топике:
I2C_TC74A0_PRINT.zip
Спасибо. Интересно, нужно, полезно. Но мне не очень нравится I2C геморойный он слегка. Проще с юарт или spi. Но по факту чаще всего с I2C и приходится работать.
- Papandopala
- 29 мая 2019, 18:15
- ↓
Комментарии (6)
RSS свернуть / развернуть