В данной статье мы рассмотрим один из наиболее часто используемых датчиков — BME280, для получения показаний температуры, давления и влажности с помощью ESP32 и ESP-IDF. Будет кратко описано устройство датчика, его подключение к плате ESP32, а также настройка проекта в VS Code с расширением ESP-IDF для сбора данных с датчика и их отображения как на консоли, так и на OLED-дисплее SSD1306.
Про то, как подключать OLED-дисплей SSD1306 к плате ESP32 с помощью ESP-IDF, вы можете прочитать в этой статье.
Также на нашем сайте вы можете прочитать статью про подключение датчика BME280 к ESP32 с помощью Arduino IDE.
Датчик BME280
Датчик BME280 широко используется в электронных проектах для измерения температуры окружающей среды, барометрического давления и относительной влажности, где требуется низкое энергопотребление. Для передачи данных на микроконтроллер можно использовать протоколы связи I2C и SPI. Хотя датчик BME280 выпускается в различных версиях, мы будем использовать связь I2C для обмена данными с платой ESP32. Этот протокол основан на синхронной многомастерной системе, где плата ESP32 выступает в роли ведущего устройства, а датчик BME280 — в роли ведомого.
На приведенной ниже схеме показана распиновка датчика BME280, который мы будем использовать.
Как видно на приведенной выше схеме, датчик BME280 имеет четыре контакта. Два из них — это контакты питания, а два — контакты для связи по протоколу I2C.
| VIN | Это контакт питания, к которому подключено напряжение 3,3 В. |
| GND | Это контакт заземления (общего провода, земли) |
| SCL | Это контакт последовательного тактового сигнала, который будет генерировать тактовый сигнал. |
| SDA | Этот контакт используется для отправки и приема данных. |
Как видите, распиновка датчика BME280 интуитивно понятна и проста.
Контакты I2C ESP32
На приведенной ниже схеме показана распиновка ESP32, где стандартные контакты I2C выделены красным цветом. Это стандартные контакты I2C модуля ESP32 DEVKIT. Однако, если вы хотите использовать другие контакты GPIO в качестве контактов I2C, вам придется задать их в коде.
Также на нашем сайте вы можете прочитать статью про использование интерфейса I2C в ESP32 с помощью ESP-IDF.
Подключение BME280 к ESP32
Для этого проекта нам понадобятся следующие компоненты:
- Плата ESP32.
- Датчик BME280.
- Соединительные провода.
- Макетная плата.
Подключение датчика BME280 к плате ESP32 достаточно простое, так как оно включает в себя лишь соединение 4 контактов (GND, VCC, SDA и SCL) с ESP32. Необходимо соединить вывод VCC с контактом 3,3 В, заземление с общим заземлением, контакт SCL датчика с контактом SCL платы ESP32, а контакт SDA датчика с контактом SDA платы ESP32.
В ESP32 по умолчанию для вывода SDA используется GPIO21, а для SCL — GPIO22.
Соединения между устройствами указаны в таблице ниже:
| Датчик BME280 | ESP32 |
| GND | GND |
| VCC | 3.3V |
| SDA | GPIO21 (I2C SDA) |
| SCL | GPIO22 (I2C SCL) |
Измерение показателей BME280 с помощью ESP32 и ESP-IDF
Мы создадим проект в VS Code с расширением ESP-IDF. Код будет считывать значения температуры, влажности и давления, используя драйвер I2C, предоставляемый ESP-IDF. Мы будем использовать библиотеку BME280, разработанную BoschSensortec.
Создание примера проекта
Откройте VS Code и перейдите в меню View > Command Palette («Вид» > «Палитра команд»). Введите в строке поиска ESP-IDF: New Project и нажмите Enter.
Укажите название проекта и каталог. Мы назвали наш проект «BME280». Для платы ESP-IDF мы выбрали опцию custom board («пользовательская плата»). В качестве целевой платформы ESP-IDF мы выбрали модуль ESP32. Нажмите кнопку ‘Choose Template’ («Выбрать шаблон»), чтобы продолжить.
В разделе «Расширение» выберите опцию ESP-IDF:
На вкладке get-started («Начало работы») мы щелкнем по пункту «sample_project». Затем нажмем ‘Create project using template sample_project’) («Создать проект, используя шаблон sample_project»).
Вы получите уведомление о создании проекта. Чтобы открыть проект в новом окне, нажмите «Да».
Это откроет проект, который мы создали на вкладке «Проводник». Внутри папки проекта находится несколько папок. Это справедливо для каждого проекта, который вы будете создавать с помощью ESP-IDF Explorer.
- Для начала добавим необходимые заголовочные файлы для библиотек, требуемых для этого проекта. Создайте новую папку с именем ‘components/bme280’ и добавьте в неё следующие файлы, доступные по этой ссылке. Сюда входят все файлы, перечисленные ниже:
- Теперь перейдите к файлу main.c. В папке main находится исходный код, то есть файл main.c будет находиться здесь. Перейдите в папку main > main.c и откройте его. Скопируйте приведенный ниже код из этого файла и сохраните его.
Код программы ESP32 для отображения показаний с BME280
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
#include "driver/gpio.h" #include "driver/i2c.h" #include "esp_err.h" #include "esp_log.h" #include "freertos/task.h" #include "sdkconfig.h" // generated by "make menuconfig" #include "bme280.h" #define SDA_PIN GPIO_NUM_21 #define SCL_PIN GPIO_NUM_22 #define TAG_BME280 "BME280" #define I2C_MASTER_ACK 0 #define I2C_MASTER_NACK 1 void i2c_master_init() { i2c_config_t i2c_config = { .mode = I2C_MODE_MASTER, .sda_io_num = SDA_PIN, .scl_io_num = SCL_PIN, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = 1000000 }; i2c_param_config(I2C_NUM_0, &i2c_config); i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0); } s8 BME280_I2C_bus_write(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt) { s32 iError = BME280_INIT_VALUE; esp_err_t espRc; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true); i2c_master_write_byte(cmd, reg_addr, true); i2c_master_write(cmd, reg_data, cnt, true); i2c_master_stop(cmd); espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS); if (espRc == ESP_OK) { iError = SUCCESS; } else { iError = FAIL; } i2c_cmd_link_delete(cmd); return (s8)iError; } s8 BME280_I2C_bus_read(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt) { s32 iError = BME280_INIT_VALUE; esp_err_t espRc; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true); i2c_master_write_byte(cmd, reg_addr, true); i2c_master_start(cmd); i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_READ, true); if (cnt > 1) { i2c_master_read(cmd, reg_data, cnt-1, I2C_MASTER_ACK); } i2c_master_read_byte(cmd, reg_data+cnt-1, I2C_MASTER_NACK); i2c_master_stop(cmd); espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS); if (espRc == ESP_OK) { iError = SUCCESS; } else { iError = FAIL; } i2c_cmd_link_delete(cmd); return (s8)iError; } void BME280_delay_msek(u32 msek) { vTaskDelay(msek/portTICK_PERIOD_MS); } void bme280_reader_task(void *ignore) { struct bme280_t bme280 = { .bus_write = BME280_I2C_bus_write, .bus_read = BME280_I2C_bus_read, .dev_addr = BME280_I2C_ADDRESS1, .delay_msec = BME280_delay_msek }; s32 com_rslt; s32 v_uncomp_pressure_s32; s32 v_uncomp_temperature_s32; s32 v_uncomp_humidity_s32; com_rslt = bme280_init(&bme280); com_rslt += bme280_set_oversamp_pressure(BME280_OVERSAMP_16X); com_rslt += bme280_set_oversamp_temperature(BME280_OVERSAMP_2X); com_rslt += bme280_set_oversamp_humidity(BME280_OVERSAMP_1X); com_rslt += bme280_set_standby_durn(BME280_STANDBY_TIME_1_MS); com_rslt += bme280_set_filter(BME280_FILTER_COEFF_16); com_rslt += bme280_set_power_mode(BME280_NORMAL_MODE); if (com_rslt == SUCCESS) { while(true) { vTaskDelay(40/portTICK_PERIOD_MS); com_rslt = bme280_read_uncomp_pressure_temperature_humidity( &v_uncomp_pressure_s32, &v_uncomp_temperature_s32, &v_uncomp_humidity_s32); if (com_rslt == SUCCESS) { ESP_LOGI(TAG_BME280, "%.2f degC / %.3f hPa / %.3f %%", bme280_compensate_temperature_double(v_uncomp_temperature_s32), bme280_compensate_pressure_double(v_uncomp_pressure_s32)/100, // Pa -> hPa bme280_compensate_humidity_double(v_uncomp_humidity_s32)); } else { ESP_LOGE(TAG_BME280, "measure error. code: %d", com_rslt); } } } else { ESP_LOGE(TAG_BME280, "init or setting error. code: %d", com_rslt); } vTaskDelete(NULL); } void app_main(void) { i2c_master_init(); xTaskCreate(&bme280_reader_task, "bme280_reader_task", 2048, NULL, 6, NULL); } |
Как работает код?
Для начала мы подключим необходимые библиотеки для этого проекта. Сюда входят драйвер i2c, драйвер gpio, библиотека логирования ESP, библиотека BME280 и библиотека FreeRTOS. Библиотека driver/i2c.h позволит ESP32 взаимодействовать с другими устройствами I2C, в нашем случае это датчик BME280.
|
1 2 3 4 5 6 7 8 9 |
#include "driver/gpio.h" #include "driver/i2c.h" #include "esp_err.h" #include "esp_log.h" #include "freertos/task.h" #include "sdkconfig.h" // generated by "make menuconfig" #include "bme280.h" |
Далее мы определили контакты SCL и SDA для связи по протоколу I2C. Используйте эти контакты GPIO для соединения двух устройств. Вы также можете изменить эти контакты.
|
1 2 |
#define SDA_PIN GPIO_NUM_21 #define SCL_PIN GPIO_NUM_22 |
Этот код использует информационное логирование. Функция логирования принимает два аргумента. Первый аргумент — это тег, а второй — отформатированная строка. Поэтому эта глобальная переменная будет полезна при вызове функций ESP_LOGI(). Здесь «BME280» — это тег, который будет использоваться при логировании.
|
1 |
#define TAG_BME280 "BME280" |
Инициализация ведущего устройства I2C (ESP32)
Следующая функция отвечает за инициализацию ведущего устройства I2C.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void i2c_master_init() { i2c_config_t i2c_config = { .mode = I2C_MODE_MASTER, .sda_io_num = SDA_PIN, .scl_io_num = SCL_PIN, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = 1000000 }; i2c_param_config(I2C_NUM_0, &i2c_config); i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0); } |
Сначала определяются параметры структуры i2c_config_t. Это включает в себя режим работы (в данном случае — ведущий), контакты связи I2C, настройку внутренних подтягивающих резисторов ESP32 и установку тактовой частоты I2C.
|
1 2 3 4 5 6 7 8 |
i2c_config_t i2c_config = { .mode = I2C_MODE_MASTER, .sda_io_num = SDA_PIN, .scl_io_num = SCL_PIN, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = 1000000 }; |
После установки параметров выполняется инициализация конфигурации I2C для конкретного порта. Это достигается путем вызова функции i2c_param_config(), которая принимает два параметра. Первый параметр — это порт I2C, а второй — указатель на константную структуру конфигурации типа i2c_config_t. На этом этапе выполняется настройка порта I2C.
|
1 |
i2c_param_config(I2C_NUM_0, &i2c_config); |
Следующий шаг — установка драйвера I2C. Он принимает несколько параметров, перечисленных ниже:
- Номер порта I2C.
- Режим конфигурации, который мы устанавливаем (ведущий/ведомый)
- Отключение буфера приема. Это размер буфера приема, применяемый к ведомому устройству.
- Отключение буфера TX. Это размер буфера отправки, который также применяется к ведомому устройству.
- Последний параметр — ‘intr_alloc_flags’, который задает флаги для выделенного прерывания. В нашем случае он равен ‘0’, поскольку в этом примере мы не используем прерывания.
|
1 |
i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0); |
Функции записи/чтения по шине I2C микроконтроллера BME280
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
s8 BME280_I2C_bus_write(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt) { s32 iError = BME280_INIT_VALUE; esp_err_t espRc; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true); i2c_master_write_byte(cmd, reg_addr, true); i2c_master_write(cmd, reg_data, cnt, true); i2c_master_stop(cmd); espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS); if (espRc == ESP_OK) { iError = SUCCESS; } else { iError = FAIL; } i2c_cmd_link_delete(cmd); return (s8)iError; } |
Внутри функции BME280_I2C_bus_write() мы начинаем с создания и инициализации канала управления I2C с помощью функции i2c_cmd_link_create(). Эта функция вернет обработчик канала управления I2C.
|
1 |
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); |
После создания командной связи мы вызовем функцию i2c_master_start() и передадим ей командную связь в качестве параметра. Это позволит ведущему устройству I2C сгенерировать стартовый сигнал. Оно будет передавать все команды из очереди.
Примечание: эта функция вызывается только тогда, когда устройство выступает в роли ведущего устройства.
|
1 |
i2c_master_start(cmd); |
Затем вызывается функция i2c_master_write_byte(), которая используется для указания адреса ведомого устройства. Эта функция принимает три параметра: команду I2C link, однобайтовую команду I2C, которая будет записана в шину, и подтверждение разрешения для ведущего устройства. Здесь «(dev_addr << 1) | I2C_MASTER_WRITE» — это адрес ведомого устройства, который указывает, что ведущее устройство будет записывать данные в ведомое.
|
1 |
i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true); |
Далее мы вызываем функцию i2c_master_write() для ведущего устройства I2C, чтобы записать буфер в шину I2C. Эта функция принимает четыре параметра: обработчик команд I2C, передаваемые данные, длину данных и подтверждение разрешения для ведущего устройства.
|
1 2 |
i2c_master_write_byte(cmd, reg_addr, true); i2c_master_write(cmd, reg_data, cnt, true); |
Затем мы вызываем функцию i2c_master_cmd_begin(), которая позволит ведущему устройству I2C начать отправку команд из очереди. Эта функция принимает три параметра. Первый параметр — номер порта I2C, второй — обработчик команд I2C, а третий — максимальное время ожидания, которое в данном случае установлено на 0,01 секунды.
|
1 2 3 4 5 6 |
espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS); if (espRc == ESP_OK) { iError = SUCCESS; } else { iError = FAIL; } |
После этого мы освободим канал связи, когда закончим отправку команд. Это достигается путем вызова функции i2c_cmd_link() и передачи в нее обработчика команд I2C в качестве параметра.
|
1 |
i2c_cmd_link_delete(cmd); |
Внутри функции BME280_I2C_bus_read() выполняются те же шаги для создания связи и запуска шины I2C Master и записи байтов, что и в функции BME280_I2C_bus_write(). На этот раз команда, отправленная ведущим устройством для чтения данных с ведомого устройства, содержит адрес ESP_SLAVE_ADDR << 1) | I2C_MASTER_READ, который указан в качестве второго параметра в функции i2c_master_write_byte(). Однако теперь мы добавим команду в условие if, которое проверяет, если длина данных больше 1, то вызывается функция i2c_master_read() для чтения нескольких байтов по шине I2C. Она принимает четыре параметра. Первый параметр — обработчик команд I2C. Второй параметр — данные для передачи. Третий параметр — длина буфера данных в байтах. И, наконец, четвертый параметр — разрешение сигнала ACK для ведущего устройства.
Далее вызовите функцию i2c_master_read_byte() для чтения одного байта по шине I2C. При этом настройка такова, что ведущее устройство не передает бит ACK. После этого вызовите функцию i2c_master_stop(), чтобы добавить сигнал «STOP» в список заданных команд.
Затем мы вызываем функцию i2c_master_cmd_begin(), которая позволит ведущему устройству I2C начать отправку команд из очереди. После этого мы освободим канал передачи команд, когда закончим их отправку.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
s8 BME280_I2C_bus_read(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt) { s32 iError = BME280_INIT_VALUE; esp_err_t espRc; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true); i2c_master_write_byte(cmd, reg_addr, true); i2c_master_start(cmd); i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_READ, true); if (cnt > 1) { i2c_master_read(cmd, reg_data, cnt-1, I2C_MASTER_ACK); } i2c_master_read_byte(cmd, reg_data+cnt-1, I2C_MASTER_NACK); i2c_master_stop(cmd); espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS); if (espRc == ESP_OK) { iError = SUCCESS; } else { iError = FAIL; } i2c_cmd_link_delete(cmd); return (s8)iError; } |
Чтение из BME280
Внутри функции bme280_reader_task() инициализируется датчик BME280, после чего данные с датчика непрерывно собираются и выводятся на последовательный терминал ESP-IDF.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
void bme280_reader_task(void *ignore) { struct bme280_t bme280 = { .bus_write = BME280_I2C_bus_write, .bus_read = BME280_I2C_bus_read, .dev_addr = BME280_I2C_ADDRESS1, .delay_msec = BME280_delay_msek }; s32 com_rslt; s32 v_uncomp_pressure_s32; s32 v_uncomp_temperature_s32; s32 v_uncomp_humidity_s32; com_rslt = bme280_init(&bme280); com_rslt += bme280_set_oversamp_pressure(BME280_OVERSAMP_16X); com_rslt += bme280_set_oversamp_temperature(BME280_OVERSAMP_2X); com_rslt += bme280_set_oversamp_humidity(BME280_OVERSAMP_1X); com_rslt += bme280_set_standby_durn(BME280_STANDBY_TIME_1_MS); com_rslt += bme280_set_filter(BME280_FILTER_COEFF_16); com_rslt += bme280_set_power_mode(BME280_NORMAL_MODE); if (com_rslt == SUCCESS) { while(true) { vTaskDelay(40/portTICK_PERIOD_MS); com_rslt = bme280_read_uncomp_pressure_temperature_humidity( &v_uncomp_pressure_s32, &v_uncomp_temperature_s32, &v_uncomp_humidity_s32); if (com_rslt == SUCCESS) { ESP_LOGI(TAG_BME280, "%.2f degC / %.3f hPa / %.3f %%", bme280_compensate_temperature_double(v_uncomp_temperature_s32), bme280_compensate_pressure_double(v_uncomp_pressure_s32)/100, // Pa -> hPa bme280_compensate_humidity_double(v_uncomp_humidity_s32)); } else { ESP_LOGE(TAG_BME280, "measure error. code: %d", com_rslt); } } } else { ESP_LOGE(TAG_BME280, "init or setting error. code: %d", com_rslt); } vTaskDelete(NULL); } |
Сначала определяются параметры структуры bme280_t. Это включает в себя запись по I2C, чтение по I2C, адрес ведомого устройства и задержку.
|
1 2 3 4 5 6 |
struct bme280_t bme280 = { .bus_write = BME280_I2C_bus_write, .bus_read = BME280_I2C_bus_read, .dev_addr = BME280_I2C_ADDRESS1, .delay_msec = BME280_delay_msek }; |
Объявите следующие переменные для хранения данных датчика BME280:
|
1 2 3 4 |
s32 com_rslt; s32 v_uncomp_pressure_s32; s32 v_uncomp_temperature_s32; s32 v_uncomp_humidity_s32; |
Инициализируйте датчик BME280, вызвав функцию bme280_init() и указав в качестве параметра указатель на структуру bme280_t.
|
1 |
com_rslt = bme280_init(&bme280); |
Далее мы устанавливаем значение параметра передискретизации давления в регистре 0xF4 с битов 2 на 4, вызывая функцию bme280_set_overamp_pressure(). Эта функция принимает один параметр — значение передискретизации давления. Она может принимать любое из перечисленных ниже значений:
- Пропущено: значение равно 0x00
- BME280_OVERSAMP_1X: Значение равно 0x01
- BME280_OVERSAMP_2X: Значение равно 0x02
- BME280_OVERSAMP_4X: Значение равно 0x03
- BME280_OVERSAMP_8X: Значение равно 0x04
- BME280_OVERSAMP_16X: Значения равны 0x05, 0x06 и 0x07.
|
1 |
com_rslt += bme280_set_oversamp_pressure(BME280_OVERSAMP_16X); |
Аналогичным образом, мы устанавливаем значение параметра передискретизации температуры в битах регистра 0xF4 с 5 на 7, вызывая функцию bme280_set_overamp_temperature(). Эта функция принимает один параметр — значение передискретизации температуры. Она может принимать любое из перечисленных ниже значений:
- Пропущено: значение равно 0x00
- BME280_OVERSAMP_1X: Значение равно 0x01
- BME280_OVERSAMP_2X: Значение равно 0x02
- BME280_OVERSAMP_4X: Значение равно 0x03
- BME280_OVERSAMP_8X: Значение равно 0x04
- BME280_OVERSAMP_16X: Значения равны 0x05, 0x06 и 0x07.
|
1 |
com_rslt += bme280_set_oversamp_temperature(BME280_OVERSAMP_2X); |
Аналогичным образом, мы устанавливаем параметр передискретизации влажности в битах регистра 0xF2 с 0 на 2, вызывая функцию bme280_set_overamp_humidity(). Эта функция принимает один параметр — значение передискретизации влажности. Она может принимать любое из перечисленных ниже значений:
- Пропущено: значение равно 0x00
- BME280_OVERSAMP_1X
- BME280_OVERSAMP_2X
- BME280_OVERSAMP_4X
- BME280_OVERSAMP_8X
- BME280_OVERSAMP_16X
|
1 |
com_rslt += bme280_set_oversamp_humidity(BME280_OVERSAMP_1X); |
После этого мы запишем время работы в режиме ожидания с датчика в регистр 0xF5, биты 5-7, вызвав функцию bme280_set_standby_durn(). Эта функция принимает один параметр — значение времени работы в режиме ожидания. Она может принимать любое из перечисленных ниже значений:
- BME280_STANDBY_TIME_1_MS
- BME280_STANDBY_TIME_63_MS
- BME280_STANDBY_TIME_125_MS
- BME280_STANDBY_TIME_250_MS
- BME280_STANDBY_TIME_500_MS
- BME280_STANDBY_TIME_1000_MS
- BME280_STANDBY_TIME_2000_MS
- BME280_STANDBY_TIME_4000_MS
|
1 |
com_rslt += bme280_set_standby_durn(BME280_STANDBY_TIME_1_MS); |
Затем мы зададим параметры фильтра в регистре 0xF5, биты 3 и 4, вызвав функцию bme280_set_filter(). Эта функция также принимает один параметр — значение коэффициента IIR-фильтра. Она может принимать любое из перечисленных ниже значений:
- BME280_FILTER_COEFF_OFF
- BME280_FILTER_COEFF_2
- BME280_FILTER_COEFF_4
- BME280_FILTER_COEFF_8
- BME280_FILTER_COEFF_16
|
1 |
com_rslt += bme280_set_filter(BME280_FILTER_COEFF_16); |
Наконец, мы установим режим энергопотребления датчика, вызвав функцию bme280_set_power_mode(). Эта функция принимает один параметр, обозначающий режим работы в регистре 0xF4, биты 0 и 1. Он может принимать любое из перечисленных ниже значений:
- BME280_SLEEP_MODE
- BME280_FORCED_MODE
- BME280_NORMAL_MODE
|
1 |
com_rslt += bme280_set_power_mode(BME280_NORMAL_MODE); |
Если все параметры датчика установлены успешно, мы запустим бесконечный цикл, который считывает и выводит данные датчика на консоль. Для считывания нескомпенсированных показаний давления, температуры и влажности мы вызываем функцию bme280_read_uncomp_pressure_temperature_humidity(). Эта функция принимает три параметра: значения нескомпенсированного давления, температуры и влажности соответственно. Если датчик BME280 измеряет правильно, показания датчика будут записаны на терминал. Функция bme280_compensate_temperature_double() выводит фактическую температуру из нескомпенсированной температуры. Функция bme280_compensate_pressure_double() выводит фактическое давление из нескомпенсированного давления. Аналогично, функция bme280_compensate_humidity_double() выводит фактическую влажность из нескомпенсированной влажности. В случае ошибки измерения или инициализации датчика будет выведено соответствующее сообщение.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
if (com_rslt == SUCCESS) { while(true) { vTaskDelay(40/portTICK_PERIOD_MS); com_rslt = bme280_read_uncomp_pressure_temperature_humidity( &v_uncomp_pressure_s32, &v_uncomp_temperature_s32, &v_uncomp_humidity_s32); if (com_rslt == SUCCESS) { ESP_LOGI(TAG_BME280, "%.2f degC / %.3f hPa / %.3f %%", bme280_compensate_temperature_double(v_uncomp_temperature_s32), bme280_compensate_pressure_double(v_uncomp_pressure_s32)/100, // Pa -> hPa bme280_compensate_humidity_double(v_uncomp_humidity_s32)); } else { ESP_LOGE(TAG_BME280, "measure error. code: %d", com_rslt); } } } else { ESP_LOGE(TAG_BME280, "init or setting error. code: %d", com_rslt); } |
После этого мы удалим задачу, используя функцию vTaskDelete(). Передача значения NULL в качестве параметра приведет к удалению вызывающей задачи.
|
1 |
vTaskDelete(NULL); |
app_main()
Внутри функции app_main() мы вызовем функцию i2c_master_init() для инициализации ведущего устройства I2C. Затем используем xTaskCreate() для создания задачи чтения данных с датчика BME280, которая будет получать показания датчика BME280 и непрерывно выводить их на консоль.
|
1 2 3 4 5 |
void app_main(void) { i2c_master_init(); xTaskCreate(&bme280_reader_task, "bme280_reader_task", 2048, NULL, 6, NULL); } |
Компиляция скетча
Для прошивки микросхемы введите следующую команду в последовательном терминале. Не забудьте заменить COM-порт на тот, через который подключена ваша плата.
|
1 |
idf.py -p COMX flash monitor |
После успешной прошивки кода вы сможете просмотреть показания температуры, давления и влажности на консоли.
Отображение показаний BME280 на OLED-дисплее
Теперь давайте создадим ещё один проект, в котором мы запрограммируем нашу плату ESP32 для сбора показаний датчика BME280 и их отображения на 0,96-дюймовом OLED-дисплее SSD1306.
Создание проекта
Выполните все те же шаги для создания аналогичного проекта, что и раньше. Однако на этот раз нам также необходимо добавить библиотеку для OLED-дисплея.
- Для начала добавим необходимые заголовочные файлы для библиотек, требуемых для этого проекта. После добавления библиотек BME280 перейдём к добавлению библиотек ssd1306. В папке components создайте новую папку с именем ‘ssd1306’ и добавьте все файлы, доступные по этой ссылке. Это включает в себя все файлы, перечисленные ниже:
Настройки MenuConfig в ESP-IDF для OLED дисплея
Для начала перейдем к menuconfig. Откройте редактор конфигурации ESP-IDF SDK. Прокрутите вниз и откройте раздел SSD1306 Configuration (конфигурации SSD1306). Здесь мы можем установить параметры конфигурации SSD1306 в соответствии с нашими потребностями. Это включает в себя интерфейс UART, тип панели, контакты SCL GPIO, SDA GPIO и контакт сброса GPIO. Здесь вы можете увидеть, что по умолчанию ESP-IDF использует интерфейс I2C, тип панели 128×64, контакт SCL GPIO 22, контакт SDA GPIO 21 и контакт сброса GPIO 33. Вы можете изменить эти параметры в соответствии с вашим OLED-дисплеем, а затем нажать кнопку «Сохранить» вверху. Мы оставим настройки конфигурации по умолчанию, поскольку они соответствуют нашему модулю.
Поскольку наш датчик BME280 также взаимодействует через интерфейс I2C, для обоих устройств будут использоваться одни и те же контакты связи.
Теперь перейдите к файлу main.c. В папке main находится исходный код, то есть файл main.c будет находиться здесь. Перейдите в папку main > main.c и откройте его. Скопируйте приведенный ниже код из этого файла и сохраните его.
Код ESP32 для отображения показаний BME280 на OLED-дисплее
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
#include "driver/gpio.h" #include "driver/i2c.h" #include "esp_err.h" #include "esp_log.h" #include "freertos/task.h" #include "sdkconfig.h" // generated by "make menuconfig" #include "bme280.h" #include "ssd1306.h" #define TAG_BME280 "BME280" #define I2C_MASTER_ACK 0 #define I2C_MASTER_NACK 1 s8 BME280_I2C_bus_write(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt) { s32 iError = BME280_INIT_VALUE; esp_err_t espRc; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true); i2c_master_write_byte(cmd, reg_addr, true); i2c_master_write(cmd, reg_data, cnt, true); i2c_master_stop(cmd); espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS); if (espRc == ESP_OK) { iError = SUCCESS; } else { iError = FAIL; } i2c_cmd_link_delete(cmd); return (s8)iError; } s8 BME280_I2C_bus_read(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt) { s32 iError = BME280_INIT_VALUE; esp_err_t espRc; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true); i2c_master_write_byte(cmd, reg_addr, true); i2c_master_start(cmd); i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_READ, true); if (cnt > 1) { i2c_master_read(cmd, reg_data, cnt-1, I2C_MASTER_ACK); } i2c_master_read_byte(cmd, reg_data+cnt-1, I2C_MASTER_NACK); i2c_master_stop(cmd); espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS); if (espRc == ESP_OK) { iError = SUCCESS; } else { iError = FAIL; } i2c_cmd_link_delete(cmd); return (s8)iError; } void BME280_delay_msek(u32 msek) { vTaskDelay(msek/portTICK_PERIOD_MS); } void bme280_oled_task(void *ignore) { SSD1306_t dev; i2c_master_init(&dev, CONFIG_SDA_GPIO, CONFIG_SCL_GPIO, CONFIG_RESET_GPIO); ssd1306_init(&dev, 128, 64); ssd1306_clear_screen(&dev, false); ssd1306_contrast(&dev, 0xff); struct bme280_t bme280 = { .bus_write = BME280_I2C_bus_write, .bus_read = BME280_I2C_bus_read, .dev_addr = BME280_I2C_ADDRESS1, .delay_msec = BME280_delay_msek }; s32 com_rslt; s32 v_uncomp_pressure_s32; s32 v_uncomp_temperature_s32; s32 v_uncomp_humidity_s32; com_rslt = bme280_init(&bme280); com_rslt += bme280_set_oversamp_pressure(BME280_OVERSAMP_16X); com_rslt += bme280_set_oversamp_temperature(BME280_OVERSAMP_2X); com_rslt += bme280_set_oversamp_humidity(BME280_OVERSAMP_1X); com_rslt += bme280_set_standby_durn(BME280_STANDBY_TIME_1_MS); com_rslt += bme280_set_filter(BME280_FILTER_COEFF_16); com_rslt += bme280_set_power_mode(BME280_NORMAL_MODE); if (com_rslt == SUCCESS) { while(true) { vTaskDelay(1000/portTICK_PERIOD_MS); com_rslt = bme280_read_uncomp_pressure_temperature_humidity( &v_uncomp_pressure_s32, &v_uncomp_temperature_s32, &v_uncomp_humidity_s32); double temp = bme280_compensate_temperature_double(v_uncomp_temperature_s32); char temperature[12]; sprintf(temperature, "%.2f degC", temp); double press = bme280_compensate_pressure_double(v_uncomp_pressure_s32)/100; // Pa -> hPa char pressure[10]; sprintf(pressure, "%.2f hPa", press); double hum = bme280_compensate_humidity_double(v_uncomp_humidity_s32); char humidity[10]; sprintf(humidity, "%.2f %%", hum); if (com_rslt == SUCCESS) { ssd1306_display_text(&dev, 0, temperature, 12, false); ssd1306_display_text(&dev, 2, pressure, 10, false); ssd1306_display_text(&dev, 4, humidity, 10, false); } else { ESP_LOGE(TAG_BME280, "measure error. code: %d", com_rslt); } } } else { ESP_LOGE(TAG_BME280, "init or setting error. code: %d", com_rslt); } vTaskDelete(NULL); } void app_main(void) { xTaskCreate(&bme280_oled_task, "bme280_oled_task", 2048*2, NULL, 6, NULL); } |
Как работает код?
Большая часть кода идентична коду предыдущего проекта, где мы непрерывно получали показания датчика BME280. Однако на этот раз вместо отображения показаний на терминале ESP-IDF мы стремимся отображать их на OLED-дисплее. Давайте рассмотрим части кода, где мы используем OLED-дисплей. Остальные функции для получения показаний температуры, давления и влажности от датчика BME280 остались прежними.
Внутри функции bme280_oled_task() мы сначала инициализируем интерфейс I2C. Интерфейс I2C будет сконфигурирован путем вызова функции i2c_master_init() и передачи в качестве параметров адреса структуры SSD1306_t, выводов SDA, SCL и Reset. Это настроит ведущее устройство ESP32 на прослушивание ведомых устройств на шине I2C.
|
1 2 |
SSD1306_t dev; i2c_master_init(&dev, CONFIG_SDA_GPIO, CONFIG_SCL_GPIO, CONFIG_RESET_GPIO); |
После этого мы инициализируем OLED-дисплей, вызвав функцию ssd1306_init(). Она принимает три параметра. Первый параметр — это адрес структуры SSD1306_t, второй — ширина, а третий — высота дисплея в пикселях. Мы используем дисплей 128×64, поэтому ширина равна 128, а высота — 64.
|
1 |
ssd1306_init(&dev, 128, 64); |
Давайте очистим экран OLED-дисплея с помощью функции ssd1306_clear_screen(). Эта функция принимает два параметра. Первый — это адрес структуры SSD1306_t, а второй — переменная типа invert, которая имеет значение false. Это означает, что фон будет темным, а текст будет отображаться белым.
|
1 |
ssd1306_clear_screen(&dev, false); |
Во-вторых, контрастность экрана устанавливается с помощью функции ssd1306_contrast(). Эта функция принимает два параметра. Первый — это адрес структуры SSD1306_t, а второй — значение контрастности, которое представляет собой целочисленную переменную. Оно устанавливается равным 0xff.
|
1 |
ssd1306_contrast(&dev, 0xff); |
После этого датчик BME280 настраивается и инициализируется. Если все параметры датчика установлены успешно, мы запускаем бесконечный цикл, который считывает и выводит данные датчика на OLED-дисплей. Для считывания нескомпенсированных показаний давления, температуры и влажности мы вызываем функцию bme280_read_uncomp_pressure_temperature_humidity().
|
1 |
com_rslt = bme280_read_uncomp_pressure_temperature_humidity(&v_uncomp_pressure_s32, &v_uncomp_temperature_s32, &v_uncomp_humidity_s32); |
Функция bme280_compensate_temperature_double() выводит фактическую температуру из нескомпенсированной. Функция bme280_compensate_pressure_double() выводит фактическое давление из нескомпенсированного давления. Аналогично, функция bme280_compensate_humidity_double() выводит фактическую влажность из нескомпенсированной влажности. Сначала мы преобразуем тип данных double в массив символов, округлим до двух десятичных знаков и добавим единицы измерения для каждого показания.
|
1 2 3 4 5 6 7 8 9 10 11 |
double temp = bme280_compensate_temperature_double(v_uncomp_temperature_s32); char temperature[12]; sprintf(temperature, "%.2f degC", temp); double press = bme280_compensate_pressure_double(v_uncomp_pressure_s32)/100; // Pa -> hPa char pressure[10]; sprintf(pressure, "%.2f hPa", press); double hum = bme280_compensate_humidity_double(v_uncomp_humidity_s32); char humidity[10]; sprintf(humidity, "%.2f %%", hum); |
Если датчик BME280 измеряет правильно, показания датчика будут отображаться на OLED-дисплее. В случае ошибки измерения или инициализации датчика на консоль будет выведено соответствующее сообщение.
Для отображения показаний каждого датчика на OLED-экране мы вызовем функцию ssd1306_display_text. Она принимает пять параметров. Первый параметр — это адрес структуры SSD1306_t. Второй параметр — это номер страницы данных о составе, который устанавливается как ‘0’ для показаний температуры, ‘2’ для показаний давления и ‘4’ для показаний влажности. Третий параметр — это текст, который мы хотим отобразить. Это массив символов для каждого показания датчика. Четвертый параметр — это длина текста. Наконец, пятый параметр — это переменная типа invert, которая является логической переменной. Она устанавливается в значение false. Это означает, что у нас темный фон, и текст будет отображаться белым цветом. Эта функция будет вызываться отдельно для каждого показания датчика и будет обновляться каждую секунду.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if (com_rslt == SUCCESS) { ssd1306_display_text(&dev, 0, temperature, 12, false); ssd1306_display_text(&dev, 2, pressure, 10, false); ssd1306_display_text(&dev, 4, humidity, 10, false); } else { ESP_LOGE(TAG_BME280, "measure error. code: %d", com_rslt); } } } else { ESP_LOGE(TAG_BME280, "init or setting error. code: %d", com_rslt); } |
Внутри функции app_main() мы вызовем xTaskCreate() для создания задачи bme280 oled, которая будет получать показания датчика BME280 и выводить их на OLED-дисплей каждую секунду.
|
1 2 3 4 |
void app_main(void) { xTaskCreate(&bme280_oled_task, "bme280_oled_task", 2048*2, NULL, 6, NULL); } |
Аппаратная конфигурация: OLED-дисплей с ESP32 и BME280.
Подключите три устройства, как показано в таблице и на принципиальной схеме ниже:
| OLED-дисплей | ESP32 | BME280 |
| VCC | VCC=3.3V | VCC |
| GND | GND | GND |
| SCL | GPIO22 | SCL |
| SDA | GPIO21 | SDA |
Компиляция скетча
Для прошивки микросхемы введите следующую команду в последовательном терминале. Не забудьте заменить COM-порт на тот, через который подключена ваша плата.
|
1 |
idf.py -p COMX flash monitor |
После успешной прошивки кода вы сможете увидеть в терминале ESP-IDF, что OLED-дисплей успешно сконфигурирован.
На OLED-экране начнут отображаться показания датчика BME280, включая температуру в градусах Цельсия, давление в гПа и влажность в процентах.
