LPCXpresso Урок 15. I2C. Работа с термометром LM75.
Представляю вашему вниманию ранее отменённую статью курса для начинающих посвященную шине I2C на примере работы с датчиком температуры LM75. Сам датчик имеет небольшую стоимость (25р в Чип-НН на момент покупки мной, да и в наборе I2C от NXP присутствовал), что в 2-3 раза дешевле популярного термометра от Dallas. Про LM75 имеется описание в сообществе и вне его в инете (благо есть поиск).
Подробно I2C контроллер описан в главе 12 UM10375. Возможности у него следующие:
- возможность работы в режимах Мастер, Ведомый, либо Мастер/Ведомый;
- арбитраж при работе нескольких мастеров на шине без повреждения данных на линии данных;
- программируемая скорость взаимодействия;
- двунаправленная передача между мастером и ведомым;
- синхронизация тактовой линии для возможности работы на одной линии устройств с разными скоростями;
- поддержка Fast-mode Plus (FM+);
- опционально задание до четырёх адресов (групп адресов) ведомого устройства;
- режим мониторинга позволяет наблюдать весь трафик в зависимости от адреса ведомого и не зависимо от него.
Отдельно хочу отметить возможность задания до 4-х адресов ведомого с масками сравнения. Зачем столько надо? Например, для слежения за трафиком нескольких устройств на линии, ведь контроллер имеет специальный режим мониторинга, не влияющий на линии и при этом аппаратно реализованный. Ну, или бредовый вариант – заменить с десяток устройств на один контроллер при ремонте какой-нибудь техники (телевизор тот же).
Но в прочем, все эти «высокие» технологии нам пока не пригодятся. Для начала нам нужен только один режим Мастер, только чтение ведомого и никаких прерываний.
Схема подключения
Схема подключения LM75 к LPCXpresso довольно проста и приведена на рисунке:
Светодиод и его резистор на 300 Ом подключать не обязательно. Данный светодиод будет зажигатся при достижении температуры в 80C и гаснуть при её падении до 75C (поведение LM75 по умолчанию).
Выводы A0, A1, A2 могут быть подключены либо к 3V3 либо к GND, тем самым задавая адрес для датчика. Для примера все они подключены к GND.
Код для работы с I2C
Начнем с инициализации:
int LM75_init()
{
LPC_SYSCON->PRESETCTRL |= (1<<1); // Сняли Reset с I2C
LPC_SYSCON->SYSAHBCLKCTRL |= (1<<5); // Разрешили тактирование
LPC_IOCON->PIO0_4 &= ~0x3F; // выбор функции I2C SCL для вывода P0.4
LPC_IOCON->PIO0_4 |= 0x01;
LPC_IOCON->PIO0_5 &= ~0x3F; // выбор функции I2C SDA для вывода P0.5
LPC_IOCON->PIO0_5 |= 0x01;
// Сброс флагов контроллера I2C
LPC_I2C->CONCLR = I2CONCLR_AAC // флаг Assert Acknowledge
| I2CONCLR_SIC // флаг наличия прерывания
| I2CONCLR_STAC // флаг генерации Start
| I2CONCLR_I2ENC // флаг разрешения работы
;
LPC_I2C->SCLL = I2SCLL_SCLL; // Время высокого уровня на линии SCL
LPC_I2C->SCLH = I2SCLH_SCLH; // Время низкого уровня на линии SCL
LPC_I2C->CONSET = I2CONSET_I2EN; // Разрешаем работу I2C
return 1;
}
Скорость взаимодействия задается с помощью двух регистров SCLL и SCLH. В них заносится длительности низкого и высокого уровней (соответственно) на линии SCL шины I2C. Длительность задается в количестве тактов контроллера I2C. Т.к. делитель частоты не устанавливался, то получаем частоту шины равную Fi2c = Fahb / (SCLL + SCLH).
Все константы имеются в примере, там же могут быть подсмотрены и их значения.
Отмечу, что я не разрешал прерывания от I2C в контроллере прерываний NVIC. Сделано это преднамеренно для упрощения кода. Так же я не задавал адрес ведомого, т.к. мы будет работать только в режиме мастера.
Работа котроллера I2C основана на "конечном автомате". Суть его проста: имеется несколько предопределённых состояний, между которыми возможны переходы, обусловленные воздействующими на машину событиями. Таким образом, для передачи/приема данных по шине (для работы с шиной) нам надо провести контроллер I2C по соответствующей цепочке состояний (какой именно можно посмотреть в UM).
Контролер при переходе между состояниями (кроме исходного состояния) генерирует прерывания, в обработчике которого и принимается решение о следующем действии. Но так как я прерывания не использую, то напишем вспомогательную функцию, для упрощения описания цепочки состояний:
int I2Cprocess(uint32_t set, uint32_t clear, uint32_t code) {
// Устанавливаем биты управления
LPC_I2C->CONSET = set;
// Сбрасываем биты управления
LPC_I2C->CONCLR = clear;
// Ожидаем завершения операции
while((LPC_I2C->CONSET & I2CONSET_SI) != I2CONSET_SI);
// Проверяем результат и возвращаем код
return (LPC_I2C->STAT == code) ? 1 : 0;
}
Задача функции: установить требуемый режим перехода, дождаться переключения состояния и проверить, действительно ли мы попали в ожидаемое состояние.
После этого функция чтения температуры становится такой же прямой, как и алгоритм в datasheet’е на LM75:
int16_t LM75_read(int num)
{
int16_t temp = 0;
while(1) {
// Устанавливаем флаг генерации Start
if(!I2Cprocess(I2CONSET_STA, 0, 0x08)) break;
// Занятие шины прошло успешно, помещаем адрес термометра с заданным номером и флаг чтения
LPC_I2C->DAT = 0x90 | ((num<<1)&0x0E) | 0x01;
if(!I2Cprocess(0, I2CONCLR_STAC | I2CONCLR_SIC, 0x40)) break;
// Устройство отозвалось продолжаем обмен и разрешаем подтверждение следующего принятого байта
if(!I2Cprocess(I2CONSET_AA, I2CONCLR_SIC, 0x50)) break;
// Нами было передано подтверждение, сохраняем старший байт результата
temp |= (LPC_I2C->DAT)<<8;
// продолжаем обмен и запрещаем подтверждение следующего принятого байта
if(!I2Cprocess(0, I2CONCLR_AAC | I2CONCLR_SIC, 0x58)) break;
// Нами был передан отказ, сохраняем младший байт результата
temp |= (LPC_I2C->DAT)&0xE0;
break;
}
// Завершаем обмен (нормально или при ошибке)
LPC_I2C->CONSET = I2CONSET_STO;
LPC_I2C->CONCLR = I2CONCLR_SIC;
return temp;
}
Бесконечный цикл здесь на самом деле не будет бесконечным благодаря break в его конце. Данный приём применён для упрощения обработки ошибок. Если машина состояний контроллера I2C переключится в состояние, которое мы не ожидали (например, если термометра с требуемым адресом нет), то цикл просто прерывается досрочно и генерируется Stop последовательность на шину (тем самым освобождая её).
Алгоритм, как отмечалось, прост:
- подождали освобождения линии (аппаратно реализовано при выдаче Start);
- выдали сигнал Start (тем самым заняли линию сами);
- выдали адрес термометра и режим чтерия (по даташиту 1001xxxr);
- по наличию подтвелждения (ACK - Acknowledge) определяем что датчик с таким адресом есть, и он готов передавать нам данные;
- принимаем первый байт результата и отправляем подтверждение;
- приняли второй байт данных и отправили отказ (NACK - Not Acknowledge);
- Выдали в линию сигнал Stop;
Следует отметить, что подтверждение/отказ (ACK/NACK) мы выставляем перед приёмом байта, на который подтверждение/отказ будет передн в линию. Не знаю как остальным, но мне такой вариант не так "очевиден" (привык я знаетели когда всё наглядно видно).
Проверяем в отладчике
Для того, что бы проверить правильность работы, а так же что бы воочию увидеть, как это всё работает, напишем простую функцию main:
int main(void) {
int16_t temp;
int hi, lo;
SysTick_Config(SystemCoreClock / 1000); // настройка таймера на период 1мс
LM75_init();
while(1) {
temp = LM75_read(0);
hi = temp >> 8;
if(temp < 0) {
lo = (8 - (temp >> 5)) & 7;
} else {
lo = ((temp >> 5) & 7);
}
lo *= 125;
printf("Val = 0x%04x Temp = %d.%03d *C\n", temp, hi, lo);
delay_ms(1000);
}
return 0 ;
}
Запуск
Данная программа может работать только под отладчиком, что в своем роде очень хорошо. Запустите её на выполнение и пройдите в пошаговом режиме. Поэкспериментируйте со сменой адреса датчику, с его «горячим» отключением. Это позволит вам лучше понять принцип работы контроллера I2C. Для лучшего понимания User Manual должен быть открыт в соответствующем месте.
Примерно так выглядит программа в действии:
Вариант без отладчика
На дисплей выводить мы научились в восьмом уроке. Почему бы не воспользоваться полученными знаниями и не написать измеритель нескольких каналов, аналогичный предложенному для АЦП. Для этого добавим в проект файлы работы с дисплеем (lcd.c и lcd.h) и подключим библиотеку LPC13xx_Lib. Ну и перепишем основную функцию программы:
int main(void) {
int num, ok;
int16_t temp;
char buffer[16] = "T0:!+199.999*C";
int hi, lo;
char sig;
GPIOInit();
GPIOSetDir(LED_PORT, LED_BIT, 1);
GPIOSetValue(LED_PORT, LED_BIT, LED_OFF);
GPIOSetDir(BUTTON_PORT, BUTTON_BIT, 0);
SysTick_Config(SystemCoreClock / 1000); // настройка таймера на период 1мс
LM75_init();
LCD_init();
while(1) {
GPIOSetValue(LED_PORT, LED_BIT, LED_ON);
for(num = 0; num < 6; num++) {
temp = LM75_read(num, &ok); // Измеряем температуру
if(temp < 0) {
hi = (-temp) >> 8;
lo = 125 * ((8 - (temp >> 5)) & 7);
sig = '-';
} else {
hi = temp >> 8;
lo = 125 * ((temp >> 5) & 7);
sig = temp ? '+' : ' ';
}
// Формирование результата
buffer[1] = '0' + num;
buffer[3] = ok ? ' ' : '!';
// целая часть
buffer[7] = '0' + hi % 10;
buffer[6] = hi < 10 ? sig : '0' + hi / 10 % 10;
buffer[5] = hi < 10 ? ' ' : hi < 100 ? sig : '0' + hi;
buffer[4] = hi < 100 ? ' ' : sig;
// дробная часть
buffer[9] = '0' + lo / 100;
buffer[10] = '0' + (lo / 10) % 10;
buffer[11] = '0' + lo % 10;
// Выводим результат
LCD_gotoXY(0, num);
LCD_writeString(buffer);
}
GPIOSetValue(LED_PORT, LED_BIT, LED_OFF);
delay_ms(500);
}
return 0 ;
}
Здесь я в функцию чтения датчика добавил ещё один параметр: указатель на переменную, содержащую признак успешности завершения чтения температуры с датчика. Добавлен он просто для того, что бы можно было отметить где данные получены правильно, а какой из датчиков не получилось обработать.
Так же вместо простой и удобной printf использовано своё преобразование. Благодаря этому Semihosting библиотека нам в этом примере не нужна. На оптимальность и читабельность не претендую. К тому же при установке символа с индексом 5, я посеял деление на сотню. В результате чего температуры от ста градусов выводятся не правильно. Изменять не буду, просто посмотрю сколько человек заметило сей баг.
В результате запуска полученных 6436 байт кода (4479 байт в релизе) видим примерно следующий результат:Адрес датчика можно «менять на лету», так что «примитивный» алгоритм оказался вполне рабочим. Но в первую очередь рабочей оказалась реализация датчика, выдержавшего такие издевательства.
Возможные баги
В примере от NXP имеется следующий комментарий:
/* It seems to be bit0 is for I2C, different from
UM. To be retested along with SSP reset. SSP and I2C
reset are overlapped, a known bug, for now, both SSP
and I2C use bit 0 for reset enable. Once the problem
is fixed, change to "#if 1". */
#if 1
LPC_SYSCON->PRESETCTRL |= (0x1<<1);
#else
LPC_SYSCON->PRESETCTRL |= (0x1<<0);
#endif
Означает он что некоторые чипы имеют баг: вместо заявленного в User Manual (UM10375) бита 1 в регистре сбросов для выбора сброса I2C используется бит 0 (тот же что и у SSP). Этот «известный» баг в моем контроллере уже исправлен, и вряд ли вам попадется.
Вместо заключения
Это было поверхностное рассмотрение реализации шины I2C в контроллерах LPC13xx. Но я надеюсь, что оно поможет вам самостоятельно разобрать имеющийся пример от NXP, основанный на прерываниях. Я Настоятельно рекомендую вам изучить раздел 12 user manual’а, в нем достаточно подробно описаны состояния контроллера и приведены временные диаграммы с примерами.
На сём позвольте откланяться. Удачи в изучении и применении контроллеров семейства Cortex от NXP.
Файлы: blinky_i2c.zip, blinky_i2c_3310.zip