LPCXpresso Урок 15. I2C. Работа с термометром LM75.

Представляю вашему вниманию ранее отменённую статью курса для начинающих посвященную шине I2C на примере работы с датчиком температуры LM75. Сам датчик имеет небольшую стоимость (25р в Чип-НН на момент покупки мной, да и в наборе I2C от NXP присутствовал), что в 2-3 раза дешевле популярного термометра от Dallas. Про LM75 имеется описание в сообществе и вне его в инете (благо есть поиск).

Подробно I2C контроллер описан в главе 12 UM10375. Возможности у него следующие:

  • возможность работы в режимах Мастер, Ведомый, либо Мастер/Ведомый;
  • арбитраж при работе нескольких мастеров на шине без повреждения данных на линии данных;
  • программируемая скорость взаимодействия;
  • двунаправленная передача между мастером и ведомым;
  • синхронизация тактовой линии для возможности работы на одной линии устройств с разными скоростями;
  • поддержка Fast-mode Plus (FM+);
  • опционально задание до четырёх адресов (групп адресов) ведомого устройства;
  • режим мониторинга позволяет наблюдать весь трафик в зависимости от адреса ведомого и не зависимо от него.

Отдельно хочу отметить возможность задания до 4-х адресов ведомого с масками сравнения. Зачем столько надо? Например, для слежения за трафиком нескольких устройств на линии, ведь контроллер имеет специальный режим мониторинга, не влияющий на линии и при этом аппаратно реализованный. Ну, или бредовый вариант – заменить с десяток устройств на один контроллер при ремонте какой-нибудь техники (телевизор тот же).

Но в прочем, все эти «высокие» технологии нам пока не пригодятся. Для начала нам нужен только один режим Мастер, только чтение ведомого и никаких прерываний.

Схема подключения

Схема подключения LM75 к LPCXpresso довольно проста и приведена на рисунке:
Схема подключения термометра LM75

Светодиод и его резистор на 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 последовательность на шину (тем самым освобождая её).

Алгоритм, как отмечалось, прост:

  1. подождали освобождения линии (аппаратно реализовано при выдаче Start);
  2. выдали сигнал Start (тем самым заняли линию сами);
  3. выдали адрес термометра и режим чтерия (по даташиту 1001xxxr);
  4. по наличию подтвелждения (ACK - Acknowledge) определяем что датчик с таким адресом есть, и он готов передавать нам данные;
  5. принимаем первый байт результата и отправляем подтверждение;
  6. приняли второй байт данных и отправили отказ (NACK - Not Acknowledge);
  7. Выдали в линию сигнал 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

Hosted by uCoz