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 последовательность на шину (тем самым освобождая её).
Алгоритм, как отмечалось, прост:
  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.
  • +1
  • 17 октября 2011, 11:01
  • angel5a
  • 2
Файлы в топике: blinky_i2c.zip, blinky_i2c_3310.zip

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

RSS свернуть / развернуть
ура!!! И2С вышла)
0
while(1) {
   break;
   break;
   break;
}
Зачем этот изврат? Юзай goto.
0
Этот изврат какраз что бы goto не было. Он тут излишен. А так как в локальных переменных нет ничего требующего освобождения, то и просто:
if(error) return stop();

Не смотря на свою «дикость» такой код «лучше понимается» (для меня во всяком случае) и кроме всего прочего позволяет легко и просто сделать continue для «второй попытки».
Ну и «низкоуровневый/высокоуровневый» код тоже берём во внимание.
0
Если ты решил безусловно перейти в определённое место, то goto не излишен. Он сделан именно для этого и понятен всем.
0
Для goto понимаю применение в инициализации драйверов в никсах. Невероятно сильно уменьшает левенку, не теряя при этом в читабельности. Сам тоже их писал и считаю что это было правильно. В данном же случае тут цикл даже лучше вписывается: «начинаем процесс передачи, не получилось — прерываем (break) процесс».
Те же вложенные if'ы будут создавать довольно большую лесницу и при этом потеряют в наглядности. Ведь все обращения к функции «процесс» равноправны, и их очерёдность определена просто последовательным расположением в коде. Алгоритм получается прямолинейным. Вложенные же ифы отражают «это должно быть выполнено только если это, а это только если это,… И ушли в алгоритме в лесенку.
По сути такой способ является несколько вида „исключений“. Только вместо try у нас while, вместо throw у нас break, только finally ну и нет классов.
0
Да, вспомнилось. Я тут как-то комментарием касательно goto обидил OlegG'а некогда. Так чдо полагаю моя позиция известна относительно сего дела.
Повторюсь, я не противник goto, но когда он действительно нужен. тот же C++ уже не потерпит goto при наличие объектов, а это уже плачевно. Или уже пофиксили и деструкторы уже вызываются? я что то не интересовался даже этим вопросом.
0
Я тут мимо проходил, смотрю вы статью про датчик найти не можете. Не эта?
0
Да, благодарю. что-то поиск по LM75 не выдал в результате. Жаль, т.к. в наборе LM75B идет, и «общим» запросом их получается не охватить. Это к Ди вопрос: если сделать проверку частичного совпадения? Лишь бы мусора не повываливалось.
0
А чем A и B отличаются?
0
Вроде только точностью (точнее, количеством значащих бит в регистре температуры). Могу ошибаться
0
http://visualbooster.com/share/20111019084952747.png
Время измерения, ток потребления, защита от статики, защита от блокировки линии данных и по мелочи ещё.
0
Нагрузочная способность вывода OS 10mA?
0
  • avatar
  • Zov
  • 19 октября 2011, 18:15
Да. Светодиод весит — держит.
0
Попробуем, возьмем из семплов что пришли от NXP.
Корпус правда не очень нравиться -TSSOP8 лучше уж SO8.
0
Да, в целом, и SO-8 не пряник. У DS1820 интереснее корпус — паять удобнее. Да и в термоусадку или ещё куда засунуть легче.
Сам тоже хочу попробовать присовокупить LM75. Только под CoIDE это всё причесать.
А вот нагружать данный девайс током 10мА не стал бы. И другим бы не рекомендовал. Лучше внешний ключ прибзднуть. А уж им — хоть ЛЕДы поджигать, хоть тиристоры коммутировать.
Саморазогрев кристалла будет при этом. Лано, если бы постоянно он был бы. Можно учесть его при калибровке, как систематическую погрешность. А тут — то 10мА через него, то 0мА. Плюс инерционность. В общем. кака-бяка будет, на мой взгляд. Хотя, если тупо делать термометр, чтобы утром посмотреть — надевать шубу с валенками или трусняки с сандалиями… Для этого сойдёт.
0
Да. самонагрев присутствует и при работе. по тому полезно в шутдаун загонять. но об этом всём дулаю логичнее в топиуке про лм-ку обсуждать. Тут я писал про и2ц, потому весь код по перегонам состояний убрал (хотя там делов то 2 строки дописать).
0
Точность +-3градуса у LM75 не радует. Гляжу на сэмплы. Ну и корпуса. Особенно у LCD-драйверов
0
Да, прибзднул LM75A к самопальной плате на 1343 — брешет градуса на 4, если не на 5. Думаю, тут нагрев от 3.3V-стабилизатора ещё имеет место быть. CoIDE свежая вышла там теперь тоже семихост есть. «Позаимствовал» у автора пример :)
0
Использовал CoLinkEx с CoIDE.
На кокосовском форуме совет, как пользоваться этим семихостингом: www.coocox.org/Forum/topic.php?id=828
0
Попробовал запустить пример. Подключил датчик lm75 к LPCXpresso с lpc1114. Постоянно программа уходила на прерывание в бесконечный цикл системного таймера.
Вот где зависала прога.
void SysTick_Handler(void)
{
    while(1)
    {
    }
}

После того как закоментил данную строку все заработало и в консоль начали поступать показания термодатчика.
//SysTick_Config(SystemCoreClock / 1000); // настройка таймера на период 1мс

Для чего нужна эта строка в данной программе?

Так начало выводить
Val = 0x0000 Temp = 0.000 *C
Val = 0x1900 Temp = 25.000 *C
Val = 0x1920 Temp = 25.125 *C
Val = 0x1940 Temp = 25.250 *C
Val = 0x1920 Temp = 25.125 *C
0
Эта строка включает системный таймер, который я использую для отсчета задержек времени в delay_ms(). Про него описано в LPCXpresso Урок 4. Systick. Использование таймера для отсчета времени.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.