STM32 и бесконтактный датчик температуры MLX90614. Подключение по I2C

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Датчик MLX90614 - это датчик с бесконтактным считыванием температуры объекта посредством приема и преобразования инфракрасного излучения. Он умеет работать в трех режимах: термостат, ШИМ выход и SMBus. В режиме термостат датчику не требуется контроллер, он просто держит температуру в заданных пределах, управляя драйвером нагрузки открытым стоком. В режиме ШИМ на выходе датчика появляется сигнал ШИМ, скважность которого зависит от температуры. В целях подключения к контроллеру наиболее интересен режим SMBus. Так как этот протокол электрически и сигнально совместим с I2C мы будем работать с датчиком, используя аппаратный I2C. О нем и пойдет речь в данной статье. Все режимы датчика настраиваются записью в определенные ячейки EEPROM. По умолчанию датчик находится в режиме SMBus.


Внешний вид и схема подключения

В подключении датчика нет ничего сложного. У меня есть плата "Синяя таблетка" с контроллером STM32F103C8T6 на борту, вот к ней и будем подключать датчик. У этого контроллера 2 аппаратных интерфейса I2C. Для датчика будет использоваться первый на выводах по умолчанию. Это PB6 - SCL, PB7 - SDA. При подключении необходимо не забыть подтянуть эти выводы к питанию внешними резисторами, у меня их сопротивление 4.7 кОм.

Программная часть

Весь код я решил оформить в виде библиотеки, состоящей из двух файлов: mlx90614.h и mlx90614.c . Также в проекте используется библиотека системного таймера для задержек и библиотека LCD дисплея A2004 для вывода температуры. Они описаны в прошлой статье.

Начнем с заголовочного файла.

#ifndef I2C_DEVICES_I2C_H_
#define I2C_DEVICES_I2C_H_

#include "stm32f1xx.h"
#include <stdio.h>
#include "delay.h"

#define F_APB1 36 													// частота шины APB1
#define TPCLK1 ( 1000/F_APB1 ) 							// период частоты APB1 ns. ~ 28
#define CCR_VALUE ( 10000 /(TPCLK1 * 2 ) ) 	// значение регистра CCR Для 36 Мгц ~ 179
#define TRISE_VALUE ( 1000 / TPCLK1)

В начале подключаем заголовочный файл для своего контроллера. У меня это stm32f1xx.h. Стандартная библиотека СИ stdio.h нужна для того, чтобы переводить дробные числа в char массив для вывода на LCD. delay.h - библиотека для организации задержек.

Далее идут константы для инициализации аппаратного I2C. В последствии в коде они подставятся в нужные регистры. Это сделано для того, чтобы меняя частоту тактирования изменить только макроопределение F_APB1, а не копаться в коде и исправлять на новое значение.

Далее идем в даташит и узнаем, что датчик имеет две разные памяти: RAM и EEPROM. RAM используется для считывания температуры. Внутренний процессор датчика считывает температуру с сенсоров, обрабатывает и кладет в ячейки RAM температуру в Кельвинах. В первых двух ячейках хранится "сырая температура". Я не понял что она из себя представляет. В следующих ячейках температура кристалла датчика, температура первого и второго сенсора. Датчики MLX90614 бывают с одним и двумя датчиками. У меня с одним, поэтому температура будет читаться с первого сенсора. В EEPROM конфигурируется работа датчика. Для удобства запишем адресацию памяти в заголовочном файле в виде макроопределений.

//---------------- RAM addresses --------------------------------------------
#define MLX90614_RAW_IR_1 0x04		// сырые данные с сенсоров
#define MLX90614_RAW_IR_2 0x05
#define MLX90614_TA 0x06					// температура кристалла датчика
#define MLX90614_TOBJ_1 0x07			// температура с первого ИК сенсора
#define MLX90614_TOBJ_2 0x08			// температура со второго ИК сенсора

//--------------- EEPROM addresses ------------------------------------------
#define MLX90614_TO_MAX 0x00
#define MLX90614_TO_MIN 0x01
#define MLX90614_PWM_CTRL 0x02
#define MLX90614_TA_RANGE 0x03
#define MLX90614_EMISSIVITY 0x04
#define MLX90614_CONFIG_REGISTER_1 0x05
#define MLX90614_SMBUS_ADDRESS 0x0E // LSByte only
#define MLX90614_ID_NUMBER_1 0x1C
#define MLX90614_ID_NUMBER_2 0x1D
#define MLX90614_ID_NUMBER_3 0x1E
#define MLX90614_ID_NUMBER_4 0x1F

Также из документации к датчику переносим команды, с которыми он умеет работать.

//--------------- Commands ------------------------------------------------
#define MLX90614_RAM_ACCESS 0 					// доступ к RAM
#define MLX90614_EEPROM_ACCESS 0x20 		// доступ к EEPROM
#define MLX90614_READ_FLAGS 0xF0 				// чтение флагов
#define MLX90614_ENTER_SLEEP_MODE 0xFF 	// режим сна
#define MLX90614_READ 1									// режим чтения из датчика
#define MLX90614_WRITE 0								// режим записи в датчик

С макроопределениями закончили. Теперь нужно определить функции, с помощью которых контроллер будет взаимодействовать с датчиком. Чтобы комфортно работать с датчиком нужно уметь считывать температуру и считывать и изменять адрес устройства, так как на шине I2C может быть несколько таких датчиков, а адрес по умолчанию у всех одинаковый - 5A. Я задумывал в своем устройстве использовать два таких датчика, но почитав форумы понял, что они для моих целей не подходят. Так как датчики уже были у меня я решил написать под них библиотеку на будущее.

Итак определяем функции:

void mlx90614Init( void );
double getTemp_Mlx90614_Double( uint16_t address, uint8_t ram_address );
void getTemp_Mlx90614_CharArray( uint16_t address, uint8_t ram_address, char* buf );

uint16_t getAddrFromEEPROM( uint16_t address );
int setAddrToEEPROM ( uint16_t address, uint16_t new_address );

uint16_t readEEPROM( uint16_t address, uint16_t eeprom_address );
void writeEEPROM ( uint16_t address, uint16_t eeprom_address, uint16_t data );

#endif /* I2C_DEVICES_I2C_H_ */

void mlx90614Init( void )

Инициализация I2C для работы с датчиком

double getTempMlx90614Double( uint16t address, uint8t ram_address )

Возвращает температуру в формате double приведенную к градусам Цельсия. Применяется, если нужна дальнейшая обработка численного значения. Принимает адрес датчика и адрес RAM памяти из которого читать данные. В зависимости от адреса RAM вернет температуру кристалла, сенсора 1 или 2.

void getTempMlx90614CharArray( uint16t address, uint8t ram_address, char* buf )

Аналогичная предыдущей функции, за исключение того, что возвращает значение в char массив, переданный по ссылке. Удобно применять для вывода температуры на LCD

uint16t getAddrFromEEPROM( uint16t address )

Возвращает адрес датчика, записанный в его EEPROM. Принимает текущий адрес датчика.

int setAddrToEEPROM ( uint16t address, uint16t new_address )

Записывает адрес датчика в EEPROM. Применяется для изменения адреса датчика.

uint16t readEEPROM( uint16t address, uint16t eepromaddress )

Универсальная функция чтения EEPROM

void writeEEPROM ( uint16t address, uint16t eepromaddress, uint16t data )

Универсальная функция записи в EEPROM

С заголовочным файлом закончили. Самое время написать реализации функций.

void mlx90614Init(void){

	 delay_ms(120);																// на стабилизацию питания и переходные процессы в датчике

RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;							// тактируем порт
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;							// тактируем i2c1

GPIOB->CRL |= GPIO_CRL_MODE6 | GPIO_CRL_MODE7;	// выход 50 мгц
GPIOB->CRL |= GPIO_CRL_CNF6 | GPIO_CRL_CNF7; 		// альтернативная ф-я открытый сток

I2C1->CR2 &= ~I2C_CR2_FREQ; 										// скидываем биты частоты шины тактирования  APB1
I2C1->CR2 |= F_APB1; 														// устанавливаем частоту шины APB1 от которой тактируется I2C модуль
I2C1->CR1 &= ~I2C_CR1_PE; 											// выключаем модуль I2C для настройки регистра CCR
I2C1->CCR &= ~I2C_CCR_CCR;
I2C1->CCR |=  CCR_VALUE;
I2C1->TRISE |=  TRISE_VALUE;

I2C1->CR1 |= I2C_CR1_ENPEC; 										// разрешаем отсылку PEC
I2C1->CR1 |= I2C_CR1_PE;												// включаем модуль I2C
I2C1->CR1 |= I2C_CR1_ACK; 											// разрешаем ACK

}

Из комментариев в функции понятно что в ней делается. Единственное следует обратить внимание на F_APB1, CCR_VALUE и TRISE_VALUE они берутся из заголовочного файла, там и рассчитываются исходя из заданной частоты тактирования. Так же важно отключить модуль I2C перед настройкой регистра CCR ( это указано в документации на контроллер ) и разрешить ACK после запуска модуля I2C , иначе ACK работать не будет.

double getTemp_Mlx90614_Double( uint16_t address,
								uint8_t ram_address){

	 uint16_t	temp ; 																// температура
	 uint8_t	temp_lsb ;														// младшие биты температуры
	 double temp_result ; 													// результирующая пересчитанная температура
	 double temp_double ;														// температура, приведенная к формату double

	 address = address<<1; 													// сдвигаем на один влево (так датчик принимает
	 	 	 	 	 	 	 	 	 	 	 														// адрес плюс 1-й бит запись/чтение)
	 I2C1->CR1 |= I2C_CR1_START;
	 while (!(I2C1->SR1 & I2C_SR1_SB)){}

	 (void) I2C1->SR1;

	 I2C1->DR = address | MLX90614_WRITE; 					// режим записи , передача адреса MLX90614
	 while (!(I2C1->SR1 & I2C_SR1_ADDR)){}

	 (void) I2C1->SR1;
	 (void) I2C1->SR2;

	 I2C1->DR= ram_address;													// передача адреса RAM  датчика MLX90614
	 while (!(I2C1->SR1 & I2C_SR1_TXE)){}

	 I2C1->CR1 |= I2C_CR1_START;										// повторный старт
	 while (!(I2C1->SR1 & I2C_SR1_SB)){}

	 (void) I2C1->SR1;

	 I2C1->DR = address | MLX90614_READ;						// обращение к датчику для чтения
	 while (!(I2C1->SR1 & I2C_SR1_ADDR)){}

	 (void) I2C1->SR1;
	 (void) I2C1->SR2;

	 while(!(I2C1->SR1 & I2C_SR1_RXNE)){}

	 temp_lsb = I2C1->DR;														// читаем младший байт

	 while(!(I2C1->SR1 & I2C_SR1_RXNE)){}

	 temp = I2C1->DR;																// читаем старший байт

	 I2C1->CR1 |= I2C_CR1_STOP;

	 temp = (temp & 0x007F) << 8;										// удаляем бит ошибки, сдвигаем в старший байт
	 temp |= temp_lsb;
	 temp_double = (double) temp; 									// приводим к формату double
	 temp_result =  ((temp_double * 0.02)- 0.01 );	// умножаем на разрешение измерений
	 temp_result = temp_result - 273.15; 						// и приводим к град. Цельсия



return temp_result;


 }

Здесь следует обратить внимание, что адрес датчика сдвигается на 1 бит влево, а потом в первый бит адреса записывается 1 для чтения, 0 - для записи. В документации на датчик не очень очевидно освещен процесс передачи адреса по I2C. И там не понятно почему обращаемся по адресу 5A, а на временной диаграмме B4 для записи и B5 для чтения. Принимая во внимание тот факт, что мы сдвигаем адрес влево и прибавляем бит режима доступа, все встает на свои места. Еще есть одна тонкость. В старшем бите старшего байта передается бит ошибки. Его необходимо удалить перед дальнейшей обработкой, что мы и делаем перед сдвигом в старший байт - (temp & 0x007F).

Получить значение температуры конечно хорошо, но еще лучше вывести это значение на LCD, например. Для этого есть простенькая функция void getTempMlx90614CharArray, которая просто преобразует полученное значение из предыдущей функции в char массив, используя для этого функцию стандартной библиотеки СИ sprintf(), которая объявлена в файле stdio.h

void getTemp_Mlx90614_CharArray( uint16_t address, uint8_t ram_address, char* buf){

	double t;
	t = getTemp_Mlx90614_Double(address,ram_address);

	 sprintf(buf, "%.1f",t);

	return ;

}

Общий алгоритм чтения из RAM датчика выглядит так:

  1. START

  2. Передаем адрес датчика, сдвинутый на 1 бит влево плюс бит записи (первый). Для записи - 0

  3. Передаем адрес RAM откуда читать плюс команда доступа к RAM. Адресом RAM может быть температура кристалла датчика или температура одного из двух инфракрасных сенсоров.

  4. Повторный START

  5. Передаем адрес датчика сдвинутый на 1 влево плюс бит чтения.

  6. Читаем младший байт

  7. Читаем старший байт

  8. STOP

Теперь мы умеем читать температуру из датчика. Осталось реализовать возможность менять адрес, чтобы можно было вешать несколько датчиков на шину. Но перед этим напишем две вспомогательные функции для работы с EEPROM для записи и чтения.

Алгоритм чтения из EEPROM выглядит следующим образом:

  1. START

  2. Передаем адрес датчика, сдвинутый на 1 бит влево плюс бит записи (первый). Для записи - 0

  3. Передаем адрес EEPROM откуда читать плюс команда доступа к EEPROM ( определена в заголовочном файле )

  4. Повторный START

  5. Передаем адрес датчика плюс бит чтения

  6. Читаем младший байт

  7. Читаем старший байт

  8. STOP

Функция чтения из EEPROM

uint16_t readEEPROM( uint16_t address, uint16_t eeprom_address ){

	 uint16_t	data_msb;
	 uint16_t	data_lsb;
	 uint16_t data_result;

	 address = address<<1; 																// сдвигаем на один влево (так датчик принимает адрес + 1 бит чтение/запись)

	 	 I2C1->CR1 |= I2C_CR1_START;
		 while (!(I2C1->SR1 & I2C_SR1_SB)){}

		 (void) I2C1->SR1;

		 I2C1->DR = address | MLX90614_WRITE; 							// режим записи , передача адреса MLX90614
		 while (!(I2C1->SR1 & I2C_SR1_ADDR)){}

		 (void) I2C1->SR1;
		 (void) I2C1->SR2;

		 I2C1->DR= eeprom_address | MLX90614_EEPROM_ACCESS;	// передача адреса EEPROM датчика MLX90614
		 while (!(I2C1->SR1 & I2C_SR1_TXE)){}

		 I2C1->CR1 |= I2C_CR1_START;												// повторный старт
		 while (!(I2C1->SR1 & I2C_SR1_SB)){}

		 (void) I2C1->SR1;

		 I2C1->DR = address | MLX90614_READ;								// обращение к датчику для чтения
		 while (!(I2C1->SR1 & I2C_SR1_ADDR)){}

		 (void) I2C1->SR1;
		 (void) I2C1->SR2;

		 //I2C1->CR1 &= ~I2C_CR1_ACK;
		 while (!(I2C1->SR1 & I2C_SR1_RXNE)){};

		 data_lsb = I2C1->DR;															// читаем младший байт

		 while(!(I2C1->SR1 & I2C_SR1_RXNE)){}

		 data_msb = I2C1->DR;															// читаем старший байт


		 I2C1->CR1 |= I2C_CR1_STOP;


		data_result = ((data_msb << 8) | data_lsb) ;

		 return data_result;


}

Чтение из EEPROM осуществляется по аналогичному алгоритму чтения из RAM. Разница только в командах выбора памяти и адресах этой памяти.

С записью немного иначе. Алгоритм следующий:

  1. START

  2. Передаем адрес датчика, сдвинутый влево плюс бит записи

  3. Передаем адрес EEPROM плюс команда выбора EEPROM памяти

  4. Передаем младший байт

  5. Передаем старший байт

  6. Передаем PEC (байт контрольной суммы )

  7. STOP

Здесь повторный старт не используется, а сразу пишется два байта адреса. Обратите внимание, что адрес использует только младший байт, поэтому в старший пишутся нули.

Функция записи в EEPROM

void writeEEPROM ( uint16_t address, uint16_t eeprom_address, uint16_t data ){


		 address = address<<1; 																// сдвигаем на один влево (т.к. датчик принимает адрес + 1 бит чтение/запись)

		 	 I2C1->CR1 |= I2C_CR1_START;
			 while (!(I2C1->SR1 & I2C_SR1_SB)){}

			 (void) I2C1->SR1;

			 I2C1->DR = address | MLX90614_WRITE; 							// режим записи , передача адреса MLX90614
			 while (!(I2C1->SR1 & I2C_SR1_ADDR)){}

			 (void) I2C1->SR1;
			 (void) I2C1->SR2;

			 I2C1->DR= eeprom_address | MLX90614_EEPROM_ACCESS;	// передача адреса EEPROM датчика MLX90614
			 while (!(I2C1->SR1 & I2C_SR1_TXE)){}

			 I2C1->DR =  ( uint8_t ) ( data & 0x00FF );					// пишем младший байт

			 while(!(I2C1->SR1 & I2C_SR1_BTF)){}

			 I2C1->DR = ( uint8_t ) ( data >> 8 )	;							// пишем старший байт
			 while(!(I2C1->SR1 & I2C_SR1_BTF)){}

			 I2C1->CR1 |= I2C_CR1_PEC;													// посылаем PEC
			 I2C1->CR1 |= I2C_CR1_STOP;


			 return ;
}

Теперь у нас есть две функции для работы с EEPROM. На них основана работа двух следующих функций для чтения и записи адреса датчика. Но они так же могут использоваться для расширения функционала библиотеки, например для изменения режима работы или перехода датчика в спящий режим.

Функция чтения адреса датчика

uint16_t getAddrFromEEPROM ( uint16_t address ){

	uint16_t addr_eeprom;
	addr_eeprom = readEEPROM( address, MLX90614_SMBUS_ADDRESS );

	return addr_eeprom;

}

Тут все просто. Функция принимает текущий адрес датчика, читает с помощью readEEPROM() текущий адрес из EEPROM и возвращает его.

С записью нового адреса в EEPROM немного сложнее. Даташит на MLX90614 рекомендует следующий алгоритм записи в EEPROM:

  1. Включение питания

  2. Запись в ячейку нулей, тем самым эффективно стирая ее

  3. Ждем 10 миллисекунд

  4. Пишем новое значение ячейки

  5. Ждем еще 10 миллисекунд

  6. Читаем ячейку для сравнения с исходным значением, чтобы убедиться, что записано без ошибок

  7. Выключаем питание

От себя дополню. Новый адрес датчика будет использоваться только после выключения и повторного включения питания датчика. В связи с этим необходимо предусмотреть отдельный пин контроллера, который будет управлять включением и выключением датчика через, например, транзистор. Здесь возникает вопрос - зачем вообще нужен режим сна, если все равно для работы с EEPROM необходимо управлять питанием датчика? Не проще ли тогда просто отключать питание? Поэтому я не стал реализовывать режим сна, так как в таких обстоятельствах он не имеет смысла. Так же в этой статье я не рассматриваю управление питанием, так как реализовать его не сложно в реальном проекте.

Функция записи в EEPROM

int setAddrToEEPROM ( uint16_t address, uint16_t new_address ){

	uint16_t addr;

	writeEEPROM ( address, MLX90614_SMBUS_ADDRESS, 0x0); 					// стираем ячейку
	delay_ms(10);
	writeEEPROM (address, MLX90614_SMBUS_ADDRESS, new_address ); 	// пишем новый адрес
	delay_ms(10);
	addr = readEEPROM ( address, MLX90614_SMBUS_ADDRESS );			 	// читаем для сравнения

	if ( addr == new_address){
		return 1;
	}

	else return 0;

}

И наконец пришло время опробовать работу библиотеки. Для этого пишем небольшой скетч в main() функции проекта

#include "main.h"
#include <stdio.h>
int main (void){

	clk_ini(); 										// запускаем тактирование переферии
	lcd_2004a_init(); 						// инициализация дисплея a2004
	mlx90614Init(); 							// инициализация I2C для датчика

	uint16_t geted_eeprom_address;
	char char_eeprom_address[20];
	char crystal_temp[10];   			// массив для строки температуры
	char first_sensor_temp[10];
// читаем адрес датчика из EEPROM и выводим на LCD
	geted_eeprom_address = getAddrFromEEPROM( 0x5A );
	sprintf(char_eeprom_address, "%x", (uint8_t) geted_eeprom_address);
	sendStr("addr value:", 3, 0);
	sendStr (char_eeprom_address, 3, 14 );

	setAddrToEEPROM (0x5A , 0xA); // записываем новый адрес
// снова читаем адрес и выводим на LCD
	geted_eeprom_address = getAddrFromEEPROM( 0x5A );
	sprintf(char_eeprom_address, "%x", (uint8_t) geted_eeprom_address);
	sendStr("new addr :", 4, 0);
	sendStr (char_eeprom_address, 4, 14 );



	while(1){
// читаем и выводим температуру кристалла и сенсора датчика
		 getTemp_Mlx90614_CharArray ( 0x5A,  MLX90614_TA, crystal_temp );
		 sendStr( "Crystal Temp :", 1, 0 );
		 sendStr( crystal_temp, 1, 14 );
    
		delay_s(1);
    
		 getTemp_Mlx90614_CharArray ( 0x5A,  MLX90614_TOBJ_1, first_sensor_temp );
		 sendStr( "Sensor Temp  :", 2, 0 );
		 sendStr( first_sensor_temp, 2, 14 );
    
			delay_s(1);
	}
}

В main.h подключаем

#ifndef CORE_INC_MAIN_H_
#define CORE_INC_MAIN_H_

#include "stm32f1xx.h"
#include "clk_ini.h" // тактирование контроллера
#include "delay.h"		// функции задержки
#include "lcd_20x4.h" // функции для работы с LCD A2004
#include "mlx90614.h" // функции работы с датчиком

#endif /* CORE_INC_MAIN_H_ */

У меня получилось вот так

В заключение полный листинги проекта

mlx90614.h
#ifndef I2C_DEVICES_I2C_H_
#define I2C_DEVICES_I2C_H_

#include "stm32f1xx.h"
#include <stdio.h>
#include "delay.h"

#define F_APB1 36 // частота шины APB1
#define TPCLK1 ( 1000/F_APB1 ) // период частоты APB1 ns. ~ 28
#define CCR_VALUE ( 10000 /(TPCLK1 * 2 ) ) // значение регистра CCR Для 36 Мгц ~ 179
#define TRISE_VALUE ( 1000 / TPCLK1)

//---------------- RAM addresses --------------------------------------------
#define MLX90614_RAW_IR_1 0x04		// сырые данные с сенсоров
#define MLX90614_RAW_IR_2 0x05
#define MLX90614_TA 0x06			// температура кристалла датчика
#define MLX90614_TOBJ_1 0x07		// температура с первого ИК сенсора
#define MLX90614_TOBJ_2 0x08		// температура со второго ИК сенсора

//--------------- EEPROM addresses ------------------------------------------
#define MLX90614_TO_MAX 0x00
#define MLX90614_TO_MIN 0x01
#define MLX90614_PWM_CTRL 0x02
#define MLX90614_TA_RANGE 0x03
#define MLX90614_EMISSIVITY 0x04
#define MLX90614_CONFIG_REGISTER_1 0x05
#define MLX90614_SMBUS_ADDRESS 0x0E // LSByte only
#define MLX90614_ID_NUMBER_1 0x1C
#define MLX90614_ID_NUMBER_2 0x1D
#define MLX90614_ID_NUMBER_3 0x1E
#define MLX90614_ID_NUMBER_4 0x1F

//--------------- Commands ------------------------------------------------
#define MLX90614_RAM_ACCESS 0 			// доступ к RAM
#define MLX90614_EEPROM_ACCESS 0x20 	// доступ к EEPROM
#define MLX90614_READ_FLAGS 0xF0 		// чтение флагов
#define MLX90614_ENTER_SLEEP_MODE 0xFF 	// режим сна
#define MLX90614_READ 1					// режим чтения из датчика
#define MLX90614_WRITE 0				// режим записи в датчик

void mlx90614Init( void );
double getTemp_Mlx90614_Double( uint16_t address, uint8_t ram_address );
void getTemp_Mlx90614_CharArray( uint16_t address, uint8_t ram_address, char* buf );

uint16_t getAddrFromEEPROM( uint16_t address );
int setAddrToEEPROM ( uint16_t address, uint16_t new_address );

uint16_t readEEPROM( uint16_t address, uint16_t eeprom_address );
void writeEEPROM ( uint16_t address, uint16_t eeprom_address, uint16_t data );

#endif /* I2C_DEVICES_I2C_H_ */

mlx90614.c
#include "mlx90614.h"

/********************************************************************************************
 * Функция инициализирует I2C интерфейс для работы с датчиком MLX90614   					*
 * 																		 					*
 ********************************************************************************************/

 void mlx90614Init(void){

	 delay_ms(120);								// на стабилизацию питания и переходные процессы в датчике

RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;				// тактируем порт
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;				// тактируем i2c1

GPIOB->CRL |= GPIO_CRL_MODE6 | GPIO_CRL_MODE7;	// выход 50 мгц
GPIOB->CRL |= GPIO_CRL_CNF6 | GPIO_CRL_CNF7; 	// альтернативная ф-я открытый сток

I2C1->CR2 &= ~I2C_CR2_FREQ; 					// скидываем биты частоты шины тактирования  APB1
I2C1->CR2 |= F_APB1; 							// устанавливаем частоту шины APB1 от которой тактируется I2C модуль
I2C1->CR1 &= ~I2C_CR1_PE; 						// выключаем модуль I2C для настройки регистра CCR
I2C1->CCR &= ~I2C_CCR_CCR;
I2C1->CCR |=  CCR_VALUE;
I2C1->TRISE |=  TRISE_VALUE;

I2C1->CR1 |= I2C_CR1_ENPEC; 					// разрешаем отсылку PEC
I2C1->CR1 |= I2C_CR1_PE;						// включаем модуль I2C
I2C1->CR1 |= I2C_CR1_ACK; 						// разрешаем ACK

}

/********************************************************************************************
 * Функция возвращает значение температуры в град. Цельсия и типом double. 					*
 * 																		  					*
 * Входные данные: 														   					*
 * address - 				адрес датчика MLX90614						   					*
 * 																		   					*
 * ram_address RAM-адрес для чтения ( см. константы в .h файле ) : 		   					*
 * 																		   					*
 * 	MLX90614_TA - 		температура кристалла датчика					   					*
 * 	MLX90614_TOBJ_1 - 	температура первого ИК сенсора					   					*
 * 	MLX90614_TOBJ_2 - 	температура второго ИК сенсора					  					*
  *******************************************************************************************/

double getTemp_Mlx90614_Double( uint16_t address,
								uint8_t ram_address){

	 uint16_t	temp ; 						// температура
	 uint8_t	temp_lsb ;					// младшие биты температуры
	 double temp_result ; 					// результирующая пересчитанная температура
	 double temp_double ;					// температура, приведенная к формату double

	 address = address<<1; 						// сдвигаем на один влево (так датчик принимает
	 	 	 	 	 	 	 	 	 	 	 	// адрес плюс 1-й бит запись/чтение)
	 I2C1->CR1 |= I2C_CR1_START;
	 while (!(I2C1->SR1 & I2C_SR1_SB)){}

	 (void) I2C1->SR1;

	 I2C1->DR = address | MLX90614_WRITE; 		// режим записи , передача адреса MLX90614
	 while (!(I2C1->SR1 & I2C_SR1_ADDR)){}

	 (void) I2C1->SR1;
	 (void) I2C1->SR2;

	 I2C1->DR= ram_address;						// передача адреса RAM  датчика MLX90614
	 while (!(I2C1->SR1 & I2C_SR1_TXE)){}

	 I2C1->CR1 |= I2C_CR1_START;				// повторный старт
	 while (!(I2C1->SR1 & I2C_SR1_SB)){}

	 (void) I2C1->SR1;

	 I2C1->DR = address | MLX90614_READ;			// обращение к датчику для чтения
	 while (!(I2C1->SR1 & I2C_SR1_ADDR)){}

	 (void) I2C1->SR1;
	 (void) I2C1->SR2;

	 while(!(I2C1->SR1 & I2C_SR1_RXNE)){}

	 temp_lsb = I2C1->DR;							// читаем младший байт

	 while(!(I2C1->SR1 & I2C_SR1_RXNE)){}

	 temp = I2C1->DR;								// читаем старший байт

	 I2C1->CR1 |= I2C_CR1_STOP;

	 temp = (temp & 0x007F) << 8;					// удаляем бит ошибки, сдвигаем в старший байт
	 temp |= temp_lsb;
	 temp_double = (double) temp; 					// приводим к формату double
	 temp_result =  ((temp_double * 0.02)- 0.01 );	// умножаем на разрешение измерений
	 temp_result = temp_result - 273.15; 			// и приводим к град. Цельсия



return temp_result;


 }


/********************************************************************************************
 * Функция записывает в, переданный по ссылке, массив типа char температуру в град. Цельсия *
 * 																							*
 * Входные данные: 																			*
 * address - 			адрес датчика MLX90614												*
 * 																							*
 * ram_address RAM-адрес для чтения ( см. константы в .h файле ) : 							*
 * 																							*
 * 	MLX90614_TA - 		температура кристалла датчика										*
 * 	MLX90614_TOBJ_1 - 	температура первого ИК сенсора										*
 * 	MLX90614_TOBJ_2 - 	температура второго ИК сенсора										*
 * 																							*
 * 	*buf - 				ссылка на массив													*
 *******************************************************************************************/

void getTemp_Mlx90614_CharArray( uint16_t address, uint8_t ram_address, char* buf){

	double t;
	t = getTemp_Mlx90614_Double(address,ram_address);

	 sprintf(buf, "%.1f",t);

	return ;

}



/********************************************************************************************
 * Чтение  EEPROM датчика по произвольному адресу											*
 * Входные данные:																			*
 * address - адрес датчика																	*
 * eeprom_address - адрес в EEPROM															*
 * 																							*
 * Выходные данные:																			*
 * значение в ячейке EEPROM формат uint16_t													*
 *																							*
 * ******************************************************************************************/

uint16_t readEEPROM( uint16_t address, uint16_t eeprom_address ){

	 uint16_t	data_msb;
	 uint16_t	data_lsb;
	 uint16_t data_result;

	 address = address<<1; 									// сдвигаем на один влево (так датчик принимает адрес + 1 бит чтение/запись)

	 	 I2C1->CR1 |= I2C_CR1_START;
		 while (!(I2C1->SR1 & I2C_SR1_SB)){}

		 (void) I2C1->SR1;

		 I2C1->DR = address | MLX90614_WRITE; 				// режим записи , передача адреса MLX90614
		 while (!(I2C1->SR1 & I2C_SR1_ADDR)){}

		 (void) I2C1->SR1;
		 (void) I2C1->SR2;

		 I2C1->DR= eeprom_address | MLX90614_EEPROM_ACCESS;	// передача адреса EEPROM датчика MLX90614
		 while (!(I2C1->SR1 & I2C_SR1_TXE)){}

		 I2C1->CR1 |= I2C_CR1_START;						// повторный старт
		 while (!(I2C1->SR1 & I2C_SR1_SB)){}

		 (void) I2C1->SR1;

		 I2C1->DR = address | MLX90614_READ;				// обращение к датчику для чтения
		 while (!(I2C1->SR1 & I2C_SR1_ADDR)){}

		 (void) I2C1->SR1;
		 (void) I2C1->SR2;

		 //I2C1->CR1 &= ~I2C_CR1_ACK;
		 while (!(I2C1->SR1 & I2C_SR1_RXNE)){};

		 data_lsb = I2C1->DR;								// читаем младший байт

		 while(!(I2C1->SR1 & I2C_SR1_RXNE)){}

		 data_msb = I2C1->DR;									// читаем старший байт


		 I2C1->CR1 |= I2C_CR1_STOP;


		data_result = ((data_msb << 8) | data_lsb) ;//& 0x1F;

		 return data_result;


}

/********************************************************************************************
 * Запись в EEPROM по произвольному адресу 												    *
 * 																						    *
 * Входные данные:																		    *
 * address - 		адрес датчика														    *
 * eeprom_address - адрес в EEPROM														    *
 * data - 			данные																    *
 ********************************************************************************************/
void writeEEPROM ( uint16_t address, uint16_t eeprom_address, uint16_t data ){


		 address = address<<1; 									// сдвигаем на один влево (т.к. датчик принимает адрес + 1 бит чтение/запись)

		 	 I2C1->CR1 |= I2C_CR1_START;
			 while (!(I2C1->SR1 & I2C_SR1_SB)){}

			 (void) I2C1->SR1;

			 I2C1->DR = address | MLX90614_WRITE; 				// режим записи , передача адреса MLX90614
			 while (!(I2C1->SR1 & I2C_SR1_ADDR)){}

			 (void) I2C1->SR1;
			 (void) I2C1->SR2;

			 I2C1->DR= eeprom_address | MLX90614_EEPROM_ACCESS;	// передача адреса EEPROM датчика MLX90614
			 while (!(I2C1->SR1 & I2C_SR1_TXE)){}

			 I2C1->DR =  ( uint8_t ) ( data & 0x00FF );			// пишем младший байт

			 while(!(I2C1->SR1 & I2C_SR1_BTF)){}

			 I2C1->DR = ( uint8_t ) ( data >> 8 )	;			// пишем старший байт
			 while(!(I2C1->SR1 & I2C_SR1_BTF)){}

			 I2C1->CR1 |= I2C_CR1_PEC;							// посылаем PEC
			 I2C1->CR1 |= I2C_CR1_STOP;


			 return ;
}

/********************************************************************************************
 * Чтение адреса датчика из EEPROM															*
 * 																							*
 * Входные данные:																			*
 * address - адрес датчика																	*
 *																							*
 * Выходные данные:																			*
 * адрес в формате uint8_t																	*
 *  																						*
 *******************************************************************************************/

uint16_t getAddrFromEEPROM ( uint16_t address ){

	uint16_t addr_eeprom;
	addr_eeprom = readEEPROM( address, MLX90614_SMBUS_ADDRESS );

	return addr_eeprom;

}

/********************************************************************************************
 * Запись нового адреса датчика в EEPROM													*
 * 																							*
 * Входные данные:																			*
 * address - 		текущий адрес 															*
 * new_address -	новый адресс															*
 * 																							*
 * Возвращает 1 - успешно/ 0 - неудача														*
 ********************************************************************************************/

int setAddrToEEPROM ( uint16_t address, uint16_t new_address ){

	uint16_t addr;

	writeEEPROM ( address, MLX90614_SMBUS_ADDRESS, 0x0); // стираем ячейку
	delay_ms(10);
	writeEEPROM (address, MLX90614_SMBUS_ADDRESS, new_address ); // пишем новый адрес
	delay_ms(10);
	addr = readEEPROM ( address, MLX90614_SMBUS_ADDRESS ); // читаем для сравнения

	if ( addr == new_address){
		return 1;
	}

	else return 0;

}

clk_ini.h
#ifndef INC_CLK_INI_H_
#define INC_CLK_INI_H_
#include "stm32f1xx.h"

int clk_ini(void);
#endif /* INC_CLK_INI_H_ */

clk_ini.c
#include "clk_ini.h"

int clk_ini(void){

	RCC->CR |= (1 << RCC_CR_HSEON_Pos);

	__IO int startCounter;
	for(startCounter = 0; ; startCounter++){

		if(RCC->CR & (1 << RCC_CR_HSERDY_Pos)){

			break;
		}// if

		if(startCounter > 0x1000){

			RCC->CR &= ~(1 << RCC_CR_HSEON_Pos);
			return 1;
		}
	}// for

	RCC->CFGR |= (0x07 << RCC_CFGR_PLLMULL_Pos) // PLL x9
			 	 |(0x01 << RCC_CFGR_PLLSRC_Pos); // start clocking PLL of HSE

	RCC->CR |= (1 << RCC_CR_PLLON_Pos);

	for(startCounter = 0; ; startCounter++){

		if(RCC->CR & (1 << RCC_CR_PLLRDY_Pos)){

			break;
		}//if

		if(startCounter > 0x1000){

			RCC->CR &= ~(1 << RCC_CR_HSEON_Pos);
			RCC->CR &= ~(1 << RCC_CR_PLLON_Pos);
			return 2;
		}// if
	}// for

	////////////////////////////////////////////////////////////
	  //Настраиваем FLASH и делители
	  ////////////////////////////////////////////////////////////

	  //Устанавливаем 2 цикла ожидания для Flash
	  //так как частота ядра у нас будет 48 MHz < SYSCLK <= 72 MHz
	  FLASH->ACR |= (0x02<<FLASH_ACR_LATENCY_Pos);

	  //Делители
	  RCC->CFGR |= (0x00<<RCC_CFGR_PPRE2_Pos) //Делитель шины APB2 равен 1
	            | (0x04<<RCC_CFGR_PPRE1_Pos) //Делитель нишы APB1 равен 2
	            | (0x00<<RCC_CFGR_HPRE_Pos); //Делитель AHB отключен


	  RCC->CFGR |= (0x02<<RCC_CFGR_SW_Pos); //Переключаемся на работу от PLL

	  //Ждем, пока переключимся
	  while((RCC->CFGR & RCC_CFGR_SWS_Msk) != (0x02<<RCC_CFGR_SWS_Pos))
	  {
	  }

	  //После того, как переключились на
	  //внешний источник такирования
	  //отключаем внутренний RC-генератор
	  //для экономии энергии
	  RCC->CR &= ~(1<<RCC_CR_HSION_Pos);

	  //Настройка и переклбючение сисемы
	  //на внешний кварцевый генератор
	  //и PLL запершилось успехом.
	  //Выходим

	return 0;
}

delay.h
#ifndef DELAY_DELAY_H_
#define DELAY_DELAY_H_

#include "stm32f1xx.h"

#define F_CPU 72000000UL
#define US F_CPU/1000000
#define MS F_CPU/1000
#define SYSTICK_MAX_VALUE 16777215
#define US_MAX_VALUE SYSTICK_MAX_VALUE/(US)
#define MS_MAX_VALUE SYSTICK_MAX_VALUE/(MS)


void delay_us(uint32_t us); // до 233 мкс
void delay_ms(uint32_t ms); // до 233 мс
void delay_s(uint32_t s);

delay.c
#include "delay.h"


/* Функции задержек на микросекунды и миллисекунды*/

void delay_us(uint32_t us){ // до 233 016 мкс

	if (us > US_MAX_VALUE || us == 0)
		return;

	SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; // запретить прерывания по достижении 0
	SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk; // ставим тактирование от процессора
	SysTick->LOAD = (US * us-1); // устанавливаем в регистр число от которого считать
	SysTick->VAL = 0; // обнуляем текущее значение регистра SYST_CVR
	SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // запускаем счетчик

	while(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)); // ждем установку флага COUNFLAG в регистре SYST_CSR

	SysTick->CTRL &= ~SysTick_CTRL_COUNTFLAG_Msk;	// скидываем бит COUNTFLAG
	SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // выключаем счетчик
	return;

}

void delay_ms(uint32_t ms){ // до 233 мс

	if(ms > MS_MAX_VALUE || ms ==0)
		return;

	SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
	SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk;
	SysTick->LOAD = (MS * ms);
	SysTick->VAL = 0;
	SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;

	while(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk));

	SysTick->CTRL &= ~SysTick_CTRL_COUNTFLAG_Msk;
	SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
return;

}

void delay_s(uint32_t s){

	for(int i=0; i<s*5;i++) delay_ms(200);
	return;
}

lcd_20x4.h
#ifndef LCD_LCD_20X4_2004A_LCD_20X4_H_
#define LCD_LCD_20X4_2004A_LCD_20X4_H_

#include "stm32f1xx.h"
#include "delay.h"

// display commands

#define CLEAR_DISPLAY 0x1
#define RETURN_HOME 0x2
#define ENTRY_MODE_SET 0x6 // mode cursor shift rihgt, display non shift
#define DISPLAY_ON 0xC // non cursor
#define DISPLAY_OFF 0x8
#define CURSOR_SHIFT_LEFT 0x10
#define CURSOR_SHIFT_RIGHT 0x14
#define DISPLAY_SHIFT_LEFT 0x18
#define DISPLAY_SHIFT_RIGHT 0x1C
#define DATA_BUS_4BIT_PAGE0 0x28
#define DATA_BUS_4BIT_PAGE1 0x2A
#define DATA_BUS_8BIT_PAGE0 0x38
#define SET_CGRAM_ADDRESS 0x40 // usage address |= SET_CGRAM_ADDRESS
#define SET_DDRAM_ADDRESS 0x80

// положение битов в порте ODR
#define PIN_RS 0x1
#define PIN_EN 0x2
#define PIN_D4 0x1000
#define PIN_D5 0x2000
#define PIN_D6 0x4000
#define PIN_D7 0x8000

#define     LCD_PORT               	GPIOB
#define		LCD_ODR 				LCD_PORT->ODR

#define     LCD_PIN_RS()     		LCD_PORT->CRL |= GPIO_CRL_MODE0_0;\
									LCD_PORT->CRL &= ~GPIO_CRL_CNF0; // PB0  выход тяни-толкай, частота 10 Мгц

#define     LCD_PIN_EN()            LCD_PORT->CRL |= GPIO_CRL_MODE1_0;\
									LCD_PORT->CRL &= ~GPIO_CRL_CNF1;        // PB1

#define     LCD_PIN_D4()            LCD_PORT->CRH |= GPIO_CRH_MODE12_0;\
									LCD_PORT->CRH &= ~GPIO_CRH_CNF12;          // PB7

#define     LCD_PIN_D5()           	LCD_PORT->CRH |= GPIO_CRH_MODE13_0;\
									LCD_PORT->CRH &= ~GPIO_CRH_CNF13;      // PB6

#define     LCD_PIN_D6()            LCD_PORT->CRH |= GPIO_CRH_MODE14_0;\
									LCD_PORT->CRH &= ~GPIO_CRH_CNF14;         // PB5

#define     LCD_PIN_D7()            LCD_PORT->CRH |= GPIO_CRH_MODE15_0;\
									LCD_PORT->CRH &= ~GPIO_CRH_CNF15;         // PB10

#define     LCD_PIN_MASK   (PIN_RS | PIN_EN | PIN_D7 | PIN_D6 | PIN_D5 | PIN_D4) // 0b0000000011110011 маска пинов для экрана

void lcd_2004a_init(void); // инициализация ножек порта под экран
void sendByte(char byte, int isData);
void sendStr(char *str, int row , int position); // вывод строки


#endif /* LCD_LCD_20X4_2004A_LCD_20X4_H_ */

lcd_20x4.c
#include "lcd_20x4.h"

// посылка байта в порт LCD

void lcdInit(void); // инициализация дисплея


void sendByte(char byte, int isData){

	//обнуляем все пины дисплея
	LCD_ODR &= ~LCD_PIN_MASK;

	if(isData == 1) LCD_ODR |= PIN_RS; // если данные ставмим RS
		else LCD_ODR &= ~(PIN_RS);		   // иначе скидываем RS

	// поднимаем пин E
		LCD_ODR |= PIN_EN;

	// ставим старшую тетраду на порт
	if(byte & 0x80) LCD_ODR |= PIN_D7;
	if(byte & 0x40) LCD_ODR |= PIN_D6;
	if(byte & 0x20) LCD_ODR |= PIN_D5;
	if(byte & 0x10) LCD_ODR |= PIN_D4;
	
	LCD_ODR &= ~PIN_EN; // сбрасываем пин Е

	//обнуляем все пины дисплея кроме RS
	LCD_ODR &= ~(LCD_PIN_MASK & ~PIN_RS);

	// поднимаем пин E

		LCD_ODR |= PIN_EN;
	// ставим младшую тетраду на порт
	if(byte & 0x8) LCD_ODR |= PIN_D7;
	if(byte & 0x4) LCD_ODR |= PIN_D6;
	if(byte & 0x2) LCD_ODR |= PIN_D5;
	if(byte & 0x1) LCD_ODR |= PIN_D4;

	// сбрасываем пин Е

	LCD_ODR &= ~(PIN_EN);
	delay_us(40);


	return;
}

// функция тактирует порт под дисплей и задает пины на выход тяни толкай и частоту 50 Мгц

void lcd_2004a_init(void){

//----------------------включаем тактирование порта----------------------------------------------------

	if(LCD_PORT == GPIOB) RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
	else if (LCD_PORT == GPIOA) RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
	else return;

//--------------------- инициализация пинов для LCD-----------------------------------------------------

		LCD_PIN_RS();
		LCD_PIN_EN();
		LCD_PIN_D7();
		LCD_PIN_D6();
		LCD_PIN_D5();
		LCD_PIN_D4();


		lcdInit();






	return ;
}

//--------------------- инициализация дисплея-----------------------------------------------------------
void lcdInit(void){



			delay_ms(200); // ждем пока стабилизируется питание

			sendByte(0x33, 0); // шлем в одном байте два 0011
			delay_us(120);

			sendByte(0x32, 0); // шлем в одном байте  00110010
			delay_us(40);

			sendByte(DATA_BUS_4BIT_PAGE0, 0); // включаем режим 4 бит
			delay_us(40);
			sendByte(DISPLAY_OFF, 0); // выключаем дисплей
			delay_us(40);
			sendByte(CLEAR_DISPLAY, 0); // очищаем дисплей
			delay_ms(2);
			sendByte(ENTRY_MODE_SET, 0); //ставим режим смещение курсора экран не смещается
			delay_us(40);
			sendByte(DISPLAY_ON, 0);// включаем дисплей и убираем курсор
			delay_us(40);


	return ;
}

void sendStr( char *str, int row , int position ){

	char start_address;

	switch (row) {

	case 1:
		start_address = 0x0; // 1 строка
		break;

	case 2:
		start_address = 0x40; // 2 строка
		break;

	case 3:
		start_address = 0x14; // 3 строка
		break;

	case 4:
		start_address = 0x54; // 4 строка
		break;

	}

	start_address += position; // к началу строки прибавляем позицию в строке

	sendByte((start_address |= SET_DDRAM_ADDRESS), 0); // ставим курсор на начало нужной строки  в DDRAM

	delay_ms(4);
	while(*str != '\0'){

		sendByte(*str, 1);
		str++;


	}// while
}

Источник: https://habr.com/ru/post/528792/


Интересные статьи

Интересные статьи

Не так давно, удалось мне обзавестись известными датчиками температуры и влажности от Xiaomi. Эти датчики заслуженно приобрели широкую известность, так как при своей достаточно низкой цене, доста...
Всем привет. Когда я искал информацию о журналировании (аудите событий) в Bitrix, на Хабре не было ни чего, в остальном рунете кое что было, но кто же там найдёт? Для пополнения базы знаний...
Те, кто собираются открывать интернет-магазин, предварительно начитавшись в интернете о важности уникального контента, о фильтрах, накладываемых поисковиками за копирование материалов с других ресурсо...
История сегодня пойдёт про автосервис в Москве и его продвижении в течении 8 месяцев. Первое знакомство было ещё пару лет назад при странных обстоятельствах. Пришёл автосервис за заявками,...
1С Битрикс: Управление сайтом (БУС) - CMS №1 в России по версии портала “Рейтинг Рунета” за 2018 год. На рынке c 2003 года. За это время БУС не стоял на месте, обрастал новой функциональностью...