Датчик 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 датчика выглядит так:
-
START
-
Передаем адрес датчика, сдвинутый на 1 бит влево плюс бит записи (первый). Для записи — 0
-
Передаем адрес RAM откуда читать плюс команда доступа к RAM. Адресом RAM может быть температура кристалла датчика или температура одного из двух инфракрасных сенсоров.
-
Повторный START
-
Передаем адрес датчика сдвинутый на 1 влево плюс бит чтения.
-
Читаем младший байт
-
Читаем старший байт
-
STOP
Теперь мы умеем читать температуру из датчика. Осталось реализовать возможность менять адрес, чтобы можно было вешать несколько датчиков на шину. Но перед этим напишем две вспомогательные функции для работы с EEPROM для записи и чтения.
Алгоритм чтения из EEPROM выглядит следующим образом:
-
START
-
Передаем адрес датчика, сдвинутый на 1 бит влево плюс бит записи (первый). Для записи — 0
-
Передаем адрес EEPROM откуда читать плюс команда доступа к EEPROM ( определена в заголовочном файле )
-
Повторный START
-
Передаем адрес датчика плюс бит чтения
-
Читаем младший байт
-
Читаем старший байт
-
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. Разница только в командах выбора памяти и адресах этой памяти.
С записью немного иначе. Алгоритм следующий:
-
START
-
Передаем адрес датчика, сдвинутый влево плюс бит записи
-
Передаем адрес EEPROM плюс команда выбора EEPROM памяти
-
Передаем младший байт
-
Передаем старший байт
-
Передаем PEC (байт контрольной суммы )
-
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:
-
Включение питания
-
Запись в ячейку нулей, тем самым эффективно стирая ее
-
Ждем 10 миллисекунд
-
Пишем новое значение ячейки
-
Ждем еще 10 миллисекунд
-
Читаем ячейку для сравнения с исходным значением, чтобы убедиться, что записано без ошибок
-
Выключаем питание
От себя дополню. Новый адрес датчика будет использоваться только после выключения и повторного включения питания датчика. В связи с этим необходимо предусмотреть отдельный пин контроллера, который будет управлять включением и выключением датчика через, например, транзистор. Здесь возникает вопрос — зачем вообще нужен режим сна, если все равно для работы с 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/
Добавить комментарий