Compare commits

..
10 Commits
Author SHA1 Message Date
aZholtikov 1bc908eb50 doc: added wiki 2026-10-17 21:36:38 +03:00
aZholtikov 938eb7735d doc: updated readme 2026-10-17 21:36:06 +03:00
aZholtikov 2da2d309ce perf: updated error checks 2026-10-11 20:13:39 +03:00
aZholtikov 8fe5aa7a03 style: updated comments 2026-09-12 13:01:53 +03:00
aZholtikov 07d8133d44 doc: updated readme 2026-08-30 17:11:10 +03:00
aZholtikov 4cbd742102 doc: updated readme 2026-08-16 08:53:14 +03:00
aZholtikov 8da6d528b2 doc: updated wiki 2026-08-16 08:53:06 +03:00
aZholtikov b5b31ec7c4 doc: updated comments 2026-08-16 06:35:38 +03:00
aZholtikov 5876c50229 doc: updated wiki 2026-08-02 08:27:53 +03:00
aZholtikov 6e763158d7 doc: updated readme 2026-08-02 08:13:56 +03:00
6 changed files with 548 additions and 930 deletions

No files matched your search

+212 -20
View File
@@ -1,31 +1,30 @@
# ESP32 ESP-IDF zh_aht - Humidity & Temperature Sensor Component
## Wiki
[EN](WIKI_EN.md) | [RU](WIKI_RU.md)
## Tested on
1. [ESP32 ESP-IDF v6.0.0](https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/index.html)
## SAST Tools
[PVS-Studio](https://pvs-studio.com/pvs-studio/?utm_source=website&utm_medium=github&utm_campaign=open_source) - static analyzer for C, C++, C#, and Java code.
# ESP32 ESP-IDF component for AHT series humidity & temperature sensor (I2C)
## Features
1. Support of AHT10/AHT15/AHT20/AHT21/AHT25/AHT30/AHT40.
2. Support multiple sensors on one device with [zh_pca9548a](https://github.com/aZholtikov/zh_pca9548a).
3. CRC8 data integrity verification.
4. Error statistics tracking.
1. Support for the whole AHT family including AHT10, AHT15, AHT20, AHT21, AHT25, AHT30 and AHT40.
2. Measurement of relative humidity in range 0 to 100 percent and temperature in range -50 to 150 degrees Celsius.
3. Data integrity verification with CRC8 algorithm on every measurement frame.
4. Automatic sensor detection and initialization during setup.
5. Software reset of the sensor over I2C.
6. Tracking of I2C driver error statistics with a resettable counter.
7. I2C clock frequency up to 400000 Hz on addresses 0x38, 0x39 and 0x44.
8. Multiple sensors on one bus through external multiplexer zh_pca9548a.
## Attention
1. The component is not thread safe, one sensor handle must be served by a single task.
2. I2C bus is created by the user and the component does not manage it.
3. Statistics counters are global and shared between all sensor instances.
4. Read and reset functions contain blocking delays and cannot be called from a context without task switching.
## Using
In an existing project, run the following command to install the component:
```text
```bash
cd ../your_project/components
git clone https://github.com/aZholtikov/zh_aht.git
git clone https://github.com/aZholtikov/zh_aht
```
In the application, add the component:
@@ -36,4 +35,197 @@ In the application, add the component:
## Examples
See Wiki [EN](WIKI_EN.md#usage-examples) | [RU](WIKI_RU.md#примеры-использования)
### Basic Example: Read Humidity and Temperature
```c
#include "zh_aht.h"
#define I2C_PORT (I2C_NUM_MAX - 1)
i2c_master_bus_handle_t i2c_bus_handle = NULL;
zh_aht_handle_t *aht_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_aht", ESP_LOG_ERROR);
i2c_master_bus_config_t i2c_bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_PORT,
.scl_io_num = GPIO_NUM_22,
.sda_io_num = GPIO_NUM_21,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle);
zh_aht_init_config_t config = ZH_AHT_INIT_CONFIG_DEFAULT();
config.i2c_handle = i2c_bus_handle;
esp_err_t ret = zh_aht_init(&config, &aht_handle);
if (ret != ESP_OK)
{
printf("AHT initialization error: %s\n", esp_err_to_name(ret));
return;
}
float humidity = 0.0;
float temperature = 0.0;
ret = zh_aht_read(&aht_handle, &humidity, &temperature);
if (ret == ESP_OK)
{
printf("Humidity: %.2f%%, Temperature: %.2f°C\n", humidity, temperature);
}
else
{
printf("Read error: %s\n", esp_err_to_name(ret));
}
zh_aht_deinit(&aht_handle);
}
```
---
### Read In A Loop Example
```c
#include "zh_aht.h"
#define I2C_PORT (I2C_NUM_MAX - 1)
i2c_master_bus_handle_t i2c_bus_handle = NULL;
zh_aht_handle_t *aht_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_aht", ESP_LOG_ERROR);
i2c_master_bus_config_t i2c_bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_PORT,
.scl_io_num = GPIO_NUM_22,
.sda_io_num = GPIO_NUM_21,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle);
zh_aht_init_config_t config = ZH_AHT_INIT_CONFIG_DEFAULT();
config.i2c_handle = i2c_bus_handle;
esp_err_t ret = zh_aht_init(&config, &aht_handle);
if (ret != ESP_OK)
{
printf("AHT initialization error: %s\n", esp_err_to_name(ret));
return;
}
float humidity = 0.0;
float temperature = 0.0;
for (int i = 0; i < 10; i++)
{
ret = zh_aht_read(&aht_handle, &humidity, &temperature);
if (ret == ESP_OK)
{
printf("Humidity: %.2f%%, Temperature: %.2f°C\n", humidity, temperature);
}
else
{
printf("Read error: %s\n", esp_err_to_name(ret));
}
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
zh_aht_deinit(&aht_handle);
}
```
---
### Reset Sensor Example
```c
#include "zh_aht.h"
#define I2C_PORT (I2C_NUM_MAX - 1)
i2c_master_bus_handle_t i2c_bus_handle = NULL;
zh_aht_handle_t *aht_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_aht", ESP_LOG_ERROR);
i2c_master_bus_config_t i2c_bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_PORT,
.scl_io_num = GPIO_NUM_22,
.sda_io_num = GPIO_NUM_21,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle);
zh_aht_init_config_t config = ZH_AHT_INIT_CONFIG_DEFAULT();
config.i2c_handle = i2c_bus_handle;
esp_err_t ret = zh_aht_init(&config, &aht_handle);
if (ret != ESP_OK)
{
printf("AHT initialization error: %s\n", esp_err_to_name(ret));
return;
}
ret = zh_aht_reset(&aht_handle);
if (ret == ESP_OK)
{
printf("Sensor reset ok\n");
}
else
{
printf("Reset error: %s\n", esp_err_to_name(ret));
}
float humidity = 0.0;
float temperature = 0.0;
ret = zh_aht_read(&aht_handle, &humidity, &temperature);
if (ret == ESP_OK)
{
printf("Humidity: %.2f%%, Temperature: %.2f°C\n", humidity, temperature);
}
zh_aht_deinit(&aht_handle);
}
```
---
### Error Statistics Example
```c
#include "zh_aht.h"
#define I2C_PORT (I2C_NUM_MAX - 1)
i2c_master_bus_handle_t i2c_bus_handle = NULL;
zh_aht_handle_t *aht_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_aht", ESP_LOG_ERROR);
i2c_master_bus_config_t i2c_bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_PORT,
.scl_io_num = GPIO_NUM_22,
.sda_io_num = GPIO_NUM_21,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle);
zh_aht_init_config_t config = ZH_AHT_INIT_CONFIG_DEFAULT();
config.i2c_handle = i2c_bus_handle;
esp_err_t ret = zh_aht_init(&config, &aht_handle);
if (ret != ESP_OK)
{
printf("AHT initialization error: %s\n", esp_err_to_name(ret));
return;
}
float humidity = 0.0;
float temperature = 0.0;
for (int i = 0; i < 10; i++)
{
zh_aht_read(&aht_handle, &humidity, &temperature);
}
const zh_aht_stats_t *stats = zh_aht_get_stats();
printf("I2C errors: %lu\n", (unsigned long)stats->i2c_driver_error);
zh_aht_reset_stats();
stats = zh_aht_get_stats();
printf("I2C errors after reset: %lu\n", (unsigned long)stats->i2c_driver_error);
zh_aht_deinit(&aht_handle);
}
```
-441
View File
@@ -1,441 +0,0 @@
# zh_aht — AHT Humidity & Temperature Sensor Component for ESP-IDF
## Table of Contents
- [Overview](#overview)
- [Features](#features)
- [Installation](#installation)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Technical Specifications](#technical-specifications)
- [Error Codes](#error-codes)
- [Contributing](#contributing)
- [License](#license)
---
## Overview
`zh_aht` is a lightweight ESP-IDF component for AHT10/AHT15/AHT20/AHT21/AHT25/AHT30/AHT40 humidity and temperature sensors. It provides a simple API to read humidity values from 0 to 100% and temperature values from -40 to +85°C. The component supports multiple sensors on a single I2C bus using the [zh_pca9548a](https://github.com/aZholtikov/zh_pca9548a) I2C multiplexer.
The component is designed specifically for ESP32 microcontrollers and uses ESP-IDF v5.0+ I2C driver API.
---
## Features
1. **Humidity and Temperature Measurement**: Reads humidity from 0 to 100% and temperature from -40 to +85°C
2. **I2C Interface**: Uses standard I2C protocol (400 kHz max frequency)
3. **Configurable I2C Address**: Supports 0x38, 0x39 and 0x44 addresses
4. **Multiple Model Support**: AHT10, AHT15, AHT20, AHT21, AHT25, AHT30, AHT40
5. **Error Statistics**: Built-in error counter for I2C driver errors
6. **Low Power**: Works with ESP-IDF power management
7. **Thread-Safe**: Uses ESP-IDF I2C driver (thread-safe)
8. **Minimal Overhead**: Low memory and CPU overhead
9. **Multiple Sensor Support**: Compatible with I2C multiplexer (zh_pca9548a)
---
## Installation
Navigate to your project's components directory:
```bash
cd ../your_project/components
```
Clone the repository:
```bash
git clone https://github.com/aZholtikov/zh_aht.git
```
In your application, include the header:
```c
#include "zh_aht.h"
```
The component will be automatically built with your project.
### Required menuconfig Settings
For correct operation, enable the following settings in menuconfig:
```text
I2C_ISR_IRAM_SAFE
I2C_MASTER_ISR_HANDLER_IN_IRAM
```
### Optional: Using with I2C Multiplexer (zh_pca9548a)
To use multiple AHT sensors on the same I2C bus, also install the [zh_pca9548a](https://github.com/aZholtikov/zh_pca9548a) component.
---
## API Reference
All functions in this library use double pointer (`zh_aht_handle_t **`) for the sensor handler to allow for proper memory management and thread-safe operations.
### zh_aht_init_config_t Structure
**Description:**
Initialization configuration for AHT sensor (I2C bus, address, frequency).
**Fields (internal):**
| Field | Type | Description / Описание |
| ------- | ------ | ------------------------ |
| `i2c_handle` | `i2c_master_bus_handle_t` | Unique I2C bus handle. |
| `i2c_address` | `uint8_t` | Sensor I2C address (0x38, 0x39, or 0x44). |
| `i2c_frequency` | `uint32_t` | Sensor I2C frequency (max 400000 Hz). |
Use `ZH_AHT_INIT_CONFIG_DEFAULT()` macro to initialize with default values:
- `i2c_frequency`: 400000 Hz
- `i2c_address`: 0x38
---
### zh_aht_handle_t Structure
The structure is declared as `typedef struct _zh_aht_handle_t zh_aht_handle_t;` and encapsulates internal implementation details.
**Fields (internal):**
| Field | Type | Description / Описание |
|-------|------|------------------------|
| `dev_handle` | `i2c_master_dev_handle_t` | Pointer to I2C device handle. |
---
### zh_aht_stats_t Structure
**Description:**
Sensor error statistics structure.
**Fields (internal):**
| Field | Type | Description / Описание |
|-------|------|------------------------|
| `i2c_driver_error` | `uint32_t` | Number of I2C driver errors. |
---
### zh_aht_init()
Initializes the AHT sensor.
**Parameters:**
- `config` - Pointer to AHT initialization configuration structure
- `handle` - Double pointer to unique AHT handle (`zh_aht_handle_t **`). If the pointer is NULL, memory will be allocated.
**Returns:**
- `ESP_OK` - Success
- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL config or handle)
- `ESP_ERR_INVALID_STATE` - Sensor is already initialized
- `ESP_ERR_NO_MEM` - Memory allocation failed
- `ESP_FAIL` - Initialization I2C device failed
- `ESP_ERR_NOT_FOUND` - Sensor did not respond
**Example:**
```c
zh_aht_handle_t *aht_handle = NULL;
zh_aht_init_config_t config = ZH_AHT_INIT_CONFIG_DEFAULT();
config.i2c_handle = i2c_bus_handle;
zh_aht_init(&config, &aht_handle);
```
---
### zh_aht_deinit()
Deinitializes the AHT sensor and removes it from the I2C bus. Memory for handle is automatically freed and the pointer is set to NULL.
**Parameters:**
- `handle` - Double pointer to unique AHT handle (`zh_aht_handle_t **`). Must not be NULL
**Returns:**
- `ESP_OK` - Success
- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL handle pointer)
- `ESP_FAIL` - I2C device removal failed
---
### zh_aht_read()
Reads humidity and temperature values from the sensor.
**Parameters:**
- `handle` - Double pointer to unique AHT handle (`zh_aht_handle_t **`). Must not be NULL
- `humidity` - Pointer to store humidity value (in %). Must not be NULL
- `temperature` - Pointer to store temperature value (in °C). Must not be NULL
**Returns:**
- `ESP_OK` - Success
- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL handle pointer or humidity pointer or temperature pointer)
- `ESP_FAIL` - I2C communication error
- `ESP_ERR_TIMEOUT` - Timeout waiting for data ready
- `ESP_ERR_INVALID_CRC` - Invalid CRC checksum
**Note:** The function performs measurement and waits ~80ms for data ready.
---
### zh_aht_reset()
Resets the AHT sensor.
**Parameters:**
- `handle` - Double pointer to unique AHT handle (`zh_aht_handle_t **`). Must not be NULL
**Returns:**
- `ESP_OK` - Success
- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL handle pointer)
- `ESP_FAIL` - I2C communication error
---
### zh_aht_get_stats()
Gets error statistics since last reset.
**Returns:**
- Pointer to const `zh_aht_stats_t` structure (valid until `zh_aht_reset_stats()` is called)
---
### zh_aht_reset_stats()
Resets all error statistics counters to zero.
---
## Usage Examples
### Example: Single Sensor
```c
#include "zh_aht.h"
static const int I2C_PORT = I2C_NUM_MAX - 1;
static const int SDA_IO = GPIO_NUM_21;
static const int SCL_IO = GPIO_NUM_22;
static zh_aht_handle_t *aht_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_aht", ESP_LOG_ERROR);
i2c_master_bus_config_t bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_PORT,
.scl_io_num = SCL_IO,
.sda_io_num = SDA_IO,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_master_bus_handle_t bus_handle = NULL;
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus_handle));
zh_aht_init_config_t config = ZH_AHT_INIT_CONFIG_DEFAULT();
config.i2c_handle = bus_handle;
ESP_ERROR_CHECK(zh_aht_init(&config, &aht_handle));
float humidity = 0.0f;
float temperature = 0.0f;
for (;;)
{
esp_err_t err = zh_aht_read(&aht_handle, &humidity, &temperature);
if (err == ESP_OK)
{
printf("Humidity: %.2f%%\n", humidity);
printf("Temperature: %.2f°C\n", temperature);
const zh_aht_stats_t *stats = zh_aht_get_stats();
printf("I2C errors: %lu\n", stats->i2c_driver_error);
}
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
// Resource cleanup (not reached with for(;;))
// zh_aht_deinit(&aht_handle);
// i2c_del_master_bus(bus_handle);
}
```
---
### Example: Multiple Sensors via I2C Multiplexer (zh_pca9548a)
```c
#include "zh_pca9548a.h"
#include "zh_aht.h"
static const int I2C_PORT = I2C_NUM_MAX - 1;
static const int SDA_IO = GPIO_NUM_21;
static const int SCL_IO = GPIO_NUM_22;
static zh_pca9548a_handle_t pca9548a_handle = {0};
static zh_aht_handle_t *aht_handle_chan_0 = NULL;
static zh_aht_handle_t *aht_handle_chan_1 = NULL;
static zh_aht_handle_t *aht_handle_chan_2 = NULL;
void app_main(void)
{
esp_log_level_set("zh_pca9548a", ESP_LOG_ERROR);
esp_log_level_set("zh_aht", ESP_LOG_ERROR);
i2c_master_bus_config_t bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_PORT,
.scl_io_num = SCL_IO,
.sda_io_num = SDA_IO,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_master_bus_handle_t bus_handle = NULL;
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus_handle));
// Initialize I2C multiplexer
zh_pca9548a_init_config_t pca_config = ZH_PCA9548A_INIT_CONFIG_DEFAULT();
pca_config.i2c_handle = bus_handle;
pca_config.i2c_address = 0x70;
ESP_ERROR_CHECK(zh_pca9548a_init(&pca_config, &pca9548a_handle));
// Initialize AHT sensors on different channels
zh_aht_init_config_t aht_config = ZH_AHT_INIT_CONFIG_DEFAULT();
aht_config.i2c_handle = bus_handle;
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_0);
ESP_ERROR_CHECK(zh_aht_init(&aht_config, &aht_handle_chan_0));
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_1);
ESP_ERROR_CHECK(zh_aht_init(&aht_config, &aht_handle_chan_1));
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_2);
ESP_ERROR_CHECK(zh_aht_init(&aht_config, &aht_handle_chan_2));
float humidity = 0.0f;
float temperature = 0.0f;
for (;;)
{
// Read sensor on channel 0
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_0);
zh_aht_read(&aht_handle_chan_0, &humidity, &temperature);
printf("Sensor 1. Humidity: %.2f%%\n", humidity);
printf("Sensor 1. Temperature: %.2f°C\n", temperature);
// Read sensor on channel 1
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_1);
zh_aht_read(&aht_handle_chan_1, &humidity, &temperature);
printf("Sensor 2. Humidity: %.2f%%\n", humidity);
printf("Sensor 2. Temperature: %.2f°C\n", temperature);
// Read sensor on channel 2
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_2);
zh_aht_read(&aht_handle_chan_2, &humidity, &temperature);
printf("Sensor 3. Humidity: %.2f%%\n", humidity);
printf("Sensor 3. Temperature: %.2f°C\n", temperature);
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
```
---
## Technical Specifications
| Parameter | Value |
| ----------- | ------- |
| **Humidity Range** | 0 - 100% |
| **Temperature Range** | -40 - +85°C. |
| **Humidity Resolution** | 0.01% |
| **Temperature Resolution** | 0.01°C |
| **I2C Address** | 0x38, 0x39, 0x44 |
| **I2C Frequency** | Up to 400 kHz |
| **Measurement Time** | ~80 ms |
| **Supported Models** | AHT10, AHT15, AHT20, AHT21, AHT25, AHT30, AHT40 |
| **ESP-IDF Version** | >= 5.0 |
| **Platform** | ESP32 series |
| **Language** | C (C99) |
---
## Error Codes
| Error Code | Description |
| ------------ | ------------- |
| `ESP_OK` | Operation successful |
| `ESP_ERR_INVALID_ARG` | Invalid argument (NULL pointer or invalid configuration) |
| `ESP_ERR_INVALID_STATE` | Sensor is already initialized |
| `ESP_ERR_NO_MEM` | Memory allocation failed |
| `ESP_ERR_NOT_FOUND` | Sensor did not respond |
| `ESP_ERR_TIMEOUT` | Timeout waiting for data ready |
| `ESP_ERR_INVALID_CRC` | Invalid CRC checksum |
| `ESP_FAIL` | General failure (I2C communication error) |
---
## Contributing
Contributions are welcome! To contribute:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
Please ensure your code follows the existing style and includes appropriate documentation.
---
## License
This project is licensed under the Apache License, Version 2.0 - see the [LICENSE](LICENSE) file for details.
### Apache License, Version 2.0
Copyright (c) 2026 Alexey Zholtikov
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---
## Additional Notes
- **I2C Pull-up Resistors**: Ensure proper pull-up resistors are connected to SDA and SCL lines
- **Measurement Time**: The sensor requires ~80ms for each measurement
- **I2C_ISR_IRAM_SAFE**: For correct operation, enable `I2C_ISR_IRAM_SAFE` and `I2C_MASTER_ISR_HANDLER_IN_IRAM` in menuconfig
- **Thread Safety**: The component uses ESP-IDF I2C driver which is thread-safe
- **Memory Management**: All API functions now use double pointers for handle, which allows automatic memory management (freeing on deinitialization, setting to NULL)
---
*Generated for zh_aht v4.0.0*
-441
View File
@@ -1,441 +0,0 @@
# zh_aht — Компонент датчика влажности и температуры AHT для ESP-IDF
## Содержание
- [Обзор](#обзор)
- [Возможности](#возможности)
- [Установка](#установка)
- [Справочник API](#справочник-api)
- [Примеры использования](#примеры-использования)
- [Технические характеристики](#технические-характеристики)
- [Коды ошибок](#коды-ошибок)
- [Вклад в проект](#вклад-в-проект)
- [Лицензия](#лицензия)
---
## Обзор
`zh_aht` - это легковесный компонент ESP-IDF для датчиков влажности и температуры AHT10/AHT15/AHT20/AHT21/AHT25/AHT30/AHT40. Он предоставляет простой API для считывания значений влажности от 0 до 100% и температуры от -40 до +85°C. Компонент поддерживает несколько датчиков на одной шине I2C с использованием I2C мультиплексатора [zh_pca9548a](https://github.com/aZholtikov/zh_pca9548a).
Компонент разработан специально для микроконтроллеров ESP32 и использует API драйвера I2C ESP-IDF v5.0+.
---
## Возможности
1. **Измерение влажности и температуры**: Считывание значений влажности от 0 до 100% и температуры от -40 до +85°C
2. **Интерфейс I2C**: Использует стандартный протокол I2C (макс. частота 400 кГц)
3. **Настраиваемый адрес I2C**: Поддерживает адреса 0x38, 0x39 и 0x44
4. **Поддержка нескольких моделей**: AHT10, AHT15, AHT20, AHT21, AHT25, AHT30, AHT40
5. **Статистика ошибок**: Встроенный счетчик ошибок драйвера I2C
6. **Низкое энергопотребление**: Работает с системой управления питанием ESP-IDF
7. **Потокобезопасность**: Использует драйвер I2C ESP-IDF (потокобезопасен)
8. **Минимальные накладные расходы**: Низкие накладные расходы на память и процессор
9. **Поддержка нескольких датчиков**: Совместим с I2C мультиплексатором (zh_pca9548a)
---
## Установка
Перейдите в каталог компонентов вашего проекта:
```bash
cd ../ваш_проект/components
```
Клонируйте репозиторий:
```bash
git clone https://github.com/aZholtikov/zh_aht.git
```
В вашем приложении подключите заголовочный файл:
```c
#include "zh_aht.h"
```
Компонент будет автоматически собран вместе с вашим проектом.
### Обязательные настройки в menuconfig
Для корректной работы компонента включите следующие настройки в menuconfig:
```text
I2C_ISR_IRAM_SAFE
I2C_MASTER_ISR_HANDLER_IN_IRAM
```
### Опционально: Использование с I2C мультиплексатором (zh_pca9548a)
Для использования нескольких датчиков AHT на одной шине I2C, также установите компонент [zh_pca9548a](https://github.com/aZholtikov/zh_pca9548a).
---
## Справочник API
Все функции в этой библиотеке используют двойной указатель (`zh_aht_handle_t **`) для дескриптора датчика, чтобы обеспечить правильное управление памятью и потокобезопасность.
### Структура zh_aht_init_config_t
**Описание:**
Конфигурация для инициализации датчика AHT (шина I2C, адрес, частота).
**Поля (внутренние):**
| Поле | Тип | Description / Описание |
| ------ | ----- | ------------------------ |
| `i2c_handle` | `i2c_master_bus_handle_t` | Уникальный дескриптор шины I2C. |
| `i2c_address` | `uint8_t` | Адрес устройства датчика I2C (0x38, 0x39 или 0x44). |
| `i2c_frequency` | `uint32_t` | Частота I2C датчика (макс. 400000 Гц). |
Используйте макрос `ZH_AHT_INIT_CONFIG_DEFAULT()` для инициализации значениями по умолчанию:
- `i2c_frequency`: 400000 Гц
- `i2c_address`: 0x38
---
### Структура zh_aht_handle_t
Структура объявляется как `typedef struct _zh_aht_handle_t zh_aht_handle_t;` и инкапсулирует детали внутренней реализации.
**Поля (внутренние):**
| Поле | Тип | Description / Описание |
|------|-----|------------------------|
| `dev_handle` | `i2c_master_dev_handle_t` | Указатель на дескриптор устройства I2C. |
---
### Структура zh_aht_stats_t
**Описание:**
Структура статистики ошибок датчика.
**Поля (внутренние):**
| Поле | Тип | Description / Описание |
|------|-----|------------------------|
| `i2c_driver_error` | `uint32_t` | Количество ошибок драйвера I2C. |
---
### zh_aht_init()
Инициализирует датчик AHT.
**Параметры:**
- `config` - Указатель на структуру конфигурации инициализации AHT. Не должен быть NULL
- `handle` - Двойной указатель на уникальный дескриптор AHT (`zh_aht_handle_t **`). Должен быть NULL
**Возвращает:**
- `ESP_OK` - Успех
- `ESP_ERR_INVALID_ARG` - Неверный аргумент (NULL config или handle)
- `ESP_ERR_INVALID_STATE` - Датчик уже инициализирован
- `ESP_ERR_NO_MEM` - Ошибка выделения памяти
- `ESP_FAIL` - Ошибка инициализации устройства I2C
- `ESP_ERR_NOT_FOUND` - Датчик не отвечает
**Пример:**
```c
zh_aht_handle_t *aht_handle = NULL;
zh_aht_init_config_t config = ZH_AHT_INIT_CONFIG_DEFAULT();
config.i2c_handle = i2c_bus_handle;
zh_aht_init(&config, &aht_handle);
```
---
### zh_aht_deinit()
Деинициализирует датчик AHT и удаляет его из шины I2C. Память для handle автоматически освобождается, а указатель устанавливается в NULL.
**Параметры:**
- `handle` - Двойной указатель на уникальный дескриптор AHT (`zh_aht_handle_t **`). Не должен быть NULL
**Возвращает:**
- `ESP_OK` - Успех
- `ESP_ERR_INVALID_ARG` - Неверный аргумент (NULL handle)
- `ESP_FAIL` - Ошибка удаления устройства I2C
---
### zh_aht_read()
Считывает значения влажности и температуры с датчика.
**Параметры:**
- `handle` - Двойной указатель на уникальный дескриптор AHT (`zh_aht_handle_t **`). Не должен быть NULL
- `humidity` - Указатель для хранения значения влажности (в %). Не должен быть NULL
- `temperature` - Указатель для хранения значения температуры (в °C). Не должен быть NULL
**Возвращает:**
- `ESP_OK` - Успех
- `ESP_ERR_INVALID_ARG` - Неверный аргумент (NULL handle, humidity или temperature)
- `ESP_FAIL` - Ошибка связи I2C
- `ESP_ERR_TIMEOUT` - Превышено время ожидания готовности данных
- `ESP_ERR_INVALID_CRC` - Неверная CRC контрольная сумма
**Примечание:** Функция выполняет измерение и ожидает ~80 мс готовности данных.
---
### zh_aht_reset()
Сбрасывает датчик AHT.
**Параметры:**
- `handle` - Двойной указатель на уникальный дескриптор AHT (`zh_aht_handle_t **`). Не должен быть NULL.
**Возвращает:**
- `ESP_OK` - Успех
- `ESP_ERR_INVALID_ARG` - Неверный аргумент (NULL handle)
- `ESP_FAIL` - Ошибка связи I2C
---
### zh_aht_get_stats()
Получает статистику ошибок с момента последнего сброса.
**Возвращает:**
- Указатель на константную структуру `zh_aht_stats_t` (действителен до вызова `zh_aht_reset_stats()`)
---
### zh_aht_reset_stats()
Сбрасывает все счётчики статистики ошибок, устанавливая их в ноль.
---
## Usage Examples / Примеры использования
### Пример: Один датчик
```c
#include "zh_aht.h"
static const int I2C_PORT = I2C_NUM_MAX - 1;
static const int SDA_IO = GPIO_NUM_21;
static const int SCL_IO = GPIO_NUM_22;
static zh_aht_handle_t *aht_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_aht", ESP_LOG_ERROR);
i2c_master_bus_config_t bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_PORT,
.scl_io_num = SCL_IO,
.sda_io_num = SDA_IO,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_master_bus_handle_t bus_handle = NULL;
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus_handle));
zh_aht_init_config_t config = ZH_AHT_INIT_CONFIG_DEFAULT();
config.i2c_handle = bus_handle;
ESP_ERROR_CHECK(zh_aht_init(&config, &aht_handle));
float humidity = 0.0f;
float temperature = 0.0f;
for (;;)
{
esp_err_t err = zh_aht_read(&aht_handle, &humidity, &temperature);
if (err == ESP_OK)
{
printf("Влажность: %.2f%%\n", humidity);
printf("Температура: %.2f°C\n", temperature);
const zh_aht_stats_t *stats = zh_aht_get_stats();
printf("Ошибки I2C: %lu\n", stats->i2c_driver_error);
}
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
// Очистка ресурсов (не достигается в примере с for(;;))
// zh_aht_deinit(&aht_handle);
// i2c_del_master_bus(bus_handle);
}
```
---
### Пример: Несколько датчиков через I2C мультиплексор (zh_pca9548a)
```c
#include "zh_pca9548a.h"
#include "zh_aht.h"
static const int I2C_PORT = I2C_NUM_MAX - 1;
static const int SDA_IO = GPIO_NUM_21;
static const int SCL_IO = GPIO_NUM_22;
static zh_pca9548a_handle_t pca9548a_handle = {0};
static zh_aht_handle_t *aht_handle_chan_0 = NULL;
static zh_aht_handle_t *aht_handle_chan_1 = NULL;
static zh_aht_handle_t *aht_handle_chan_2 = NULL;
void app_main(void)
{
esp_log_level_set("zh_pca9548a", ESP_LOG_ERROR);
esp_log_level_set("zh_aht", ESP_LOG_ERROR);
i2c_master_bus_config_t bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_PORT,
.scl_io_num = SCL_IO,
.sda_io_num = SDA_IO,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_master_bus_handle_t bus_handle = NULL;
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus_handle));
// Инициализация I2C мультиплексора
zh_pca9548a_init_config_t pca_config = ZH_PCA9548A_INIT_CONFIG_DEFAULT();
pca_config.i2c_handle = bus_handle;
pca_config.i2c_address = 0x70;
ESP_ERROR_CHECK(zh_pca9548a_init(&pca_config, &pca9548a_handle));
// Инициализация датчиков AHT на разных каналах
zh_aht_init_config_t aht_config = ZH_AHT_INIT_CONFIG_DEFAULT();
aht_config.i2c_handle = bus_handle;
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_0);
ESP_ERROR_CHECK(zh_aht_init(&aht_config, &aht_handle_chan_0));
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_1);
ESP_ERROR_CHECK(zh_aht_init(&aht_config, &aht_handle_chan_1));
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_2);
ESP_ERROR_CHECK(zh_aht_init(&aht_config, &aht_handle_chan_2));
float humidity = 0.0f;
float temperature = 0.0f;
for (;;)
{
// Считывание с датчика на канале 0
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_0);
zh_aht_read(&aht_handle_chan_0, &humidity, &temperature);
printf("Датчик 1. Влажность: %.2f%%\n", humidity);
printf("Датчик 1. Температура: %.2f°C\n", temperature);
// Считывание с датчика на канале 1
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_1);
zh_aht_read(&aht_handle_chan_1, &humidity, &temperature);
printf("Датчик 2. Влажность: %.2f%%\n", humidity);
printf("Датчик 2. Температура: %.2f°C\n", temperature);
// Считывание с датчика на канале 2
zh_pca9548a_set(&pca9548a_handle, ZH_PCA9548A_CHAN_NUM_2);
zh_aht_read(&aht_handle_chan_2, &humidity, &temperature);
printf("Датчик 3. Влажность: %.2f%%\n", humidity);
printf("Датчик 3. Температура: %.2f°C\n", temperature);
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
```
---
## Технические характеристики
| Параметр | Значение |
| ---------- | ---------- |
| **Диапазон влажности** | 0 - 100% |
| **Диапазон температуры** | -40 - +85°C |
| **Разрешение измерения влажности** | 0.01% |
| **Разрешение измерения температуры** | 0.01°C |
| **Адрес I2C** | 0x38, 0x39, 0x44 |
| **Частота I2C** | До 400 кГц |
| **Время измерения** | ~80 мс |
| **Поддерживаемые модели** | AHT10, AHT15, AHT20, AHT21, AHT25, AHT30, AHT40 |
| **Версия ESP-IDF** | >= 5.0 |
| **Платформа** | Семейство ESP32 |
| **Язык** | C (C99) |
---
## Коды ошибок
| Код ошибки | Описание |
| ------------ | ---------- |
| `ESP_OK` | Операция выполнена успешно |
| `ESP_ERR_INVALID_ARG` | Неверный аргумент (NULL указатель или неверная конфигурация) |
| `ESP_ERR_INVALID_STATE` | Датчик уже инициализирован |
| `ESP_ERR_NO_MEM` | Ошибка выделения памяти |
| `ESP_ERR_NOT_FOUND` | Датчик не отвечает |
| `ESP_ERR_TIMEOUT` | Превышено время ожидания готовности данных |
| `ESP_ERR_INVALID_CRC` | Неверная CRC контрольная сумма |
| `ESP_FAIL` | Общая ошибка (ошибка связи I2C) |
---
## Вклад в проект
Вклад приветствуется! Чтобы внести свой вклад:
1. Сделайте форк репозитория
2. Создайте ветку функции (`git checkout -b feature/AmazingFeature`)
3. Закоммитьте ваши изменения (`git commit -m 'Add some AmazingFeature'`)
4. Отправьте в ветку (`git push origin feature/AmazingFeature`)
5. Откройте Pull Request
Пожалуйста, убедитесь, что ваш код следует существующему стилю и включает соответствующую документацию.
---
## Лицензия
Этот проект лицензирован по лицензии Apache, версия 2.0 - см. файл [LICENSE](LICENSE) для подробной информации.
### Apache License, Version 2.0
Авторское право (c) 2026 Алексей Жолтиков
Лицензировано по лицензии Apache License, Version 2.0 (далее — "Лицензия");
вы не можете использовать этот файл, кроме случаев, предусмотренных Лицензией.
Копию Лицензии можно получить по адресу:
http://www.apache.org/licenses/LICENSE-2.0
Если иное не требуется действующим законодательством или не согласовано в письменном виде,
программное обеспечение, распространяемое по Лицензии, распространяется на условиях "КАК ЕСТЬ",
БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, явных или подразумеваемых, включая, но не ограничиваясь, гарантии
ТОВАРНОГО СОСТОЯНИЯ, ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ И НЕНАРУШЕНИЯ ПРАВ.
Смотрите Лицензию для получения конкретных прав и ограничений.
---
## Дополнительные заметки
- **Подтягивающие резисторы I2C**: Убедитесь, что к линиям SDA и SCL подключены соответствующие подтягивающие резисторы
- **Время измерения**: Датчик требует ~80 мс для каждого измерения
- **I2C_ISR_IRAM_SAFE**: Для правильной работы включите `I2C_ISR_IRAM_SAFE` и `I2C_MASTER_ISR_HANDLER_IN_IRAM` в menuconfig
- **Потокобезопасность**: Компонент использует драйвер I2C ESP-IDF, который является потокобезопасным
- **Управление памятью**: Все функции API теперь используют двойные указатели для handle, что позволяет автоматически управлять памятью (освобождение при деинициализации, установка в NULL)
---
*Сгенерировано для zh_aht v4.0.0*
+13 -13
View File
@@ -1,7 +1,7 @@
/**
* @file zh_aht.h
*
* @brief Driver for AHT series temperature and humidity sensors (AHT10/AHT15/AHT20/AHT21/AHT25/AHT30/AHT40)
* @brief Driver for AHT series temperature and humidity sensors (AHT10/AHT15/AHT20/AHT21/AHT25/AHT30/AHT40).
*
* Provides I2C-based communication with AHT sensors including initialization,
* data reading with CRC validation, reset functionality, and error statistics.
@@ -11,9 +11,6 @@
* - Automatic sensor initialization and probe detection
* - CRC8 data integrity verification
* - Error statistics tracking
*
* @note Requires ESP-IDF v5.0+ with I2C master driver
* @note Enable I2C_ISR_IRAM_SAFE and I2C_MASTER_ISR_HANDLER_IN_IRAM in menuconfig
*/
#pragma once
@@ -22,6 +19,8 @@
#include "esp_log.h"
#include "driver/i2c_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_heap_caps.h"
#define ZH_AHT_INIT_CONFIG_DEFAULT() \
{ \
@@ -37,12 +36,12 @@ extern "C"
* @brief Opaque handle for AHT sensor driver.
*
* Incomplete type that encapsulates internal sensor state.
* Users interact with this handle exclusively through the public API functions.c
* Users interact with this handle exclusively through the public API functions.
*/
typedef struct _zh_aht_handle_t zh_aht_handle_t;
/**
* @brief Configuration structure for AHT sensor initialization
* @brief Configuration structure for AHT sensor initialization.
*/
typedef struct
{
@@ -52,7 +51,7 @@ extern "C"
} zh_aht_init_config_t;
/**
* @brief Structure containing sensor error statistics
* @brief Structure containing sensor error statistics.
*/
typedef struct
{
@@ -60,7 +59,7 @@ extern "C"
} zh_aht_stats_t;
/**
* @brief Initialize AHT sensor and establish I2C communication
* @brief Initialize AHT sensor and establish I2C communication.
*
* Allocates handle, configures I2C device, probes sensor presence,
* and sends initialization command if required.
@@ -77,7 +76,7 @@ extern "C"
esp_err_t zh_aht_init(const zh_aht_init_config_t *config, zh_aht_handle_t **handle);
/**
* @brief Deinitialize AHT sensor and release resources
* @brief Deinitialize AHT sensor and release resources.
*
* Removes I2C device and frees the sensor handle.
*
@@ -85,11 +84,12 @@ extern "C"
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle is NULL
* @return ESP_FAIL on I2C communication errors
*/
esp_err_t zh_aht_deinit(zh_aht_handle_t **handle);
/**
* @brief Read humidity and temperature from AHT sensor
* @brief Read humidity and temperature from AHT sensor.
*
* Sends read command, receives 7-byte response with CRC validation,
* and converts raw data to engineering units.
@@ -107,7 +107,7 @@ extern "C"
esp_err_t zh_aht_read(zh_aht_handle_t **handle, float *humidity, float *temperature);
/**
* @brief Reset AHT sensor via I2C command
* @brief Reset AHT sensor via I2C command.
*
* Sends reset command and waits for sensor recovery.
*
@@ -120,7 +120,7 @@ extern "C"
esp_err_t zh_aht_reset(zh_aht_handle_t **handle);
/**
* @brief Get pointer to sensor error statistics
* @brief Get pointer to sensor error statistics.
*
* Returns read-only pointer to global statistics structure.
*
@@ -129,7 +129,7 @@ extern "C"
const zh_aht_stats_t *zh_aht_get_stats(void);
/**
* @brief Reset all error statistics to zero
* @brief Reset all error statistics to zero.
*
* Clears all error counters tracked in the global statistics structure.
*/
+283
View File
@@ -0,0 +1,283 @@
# Описание компонента zh_aht
## Общие сведения
**zh_aht** — это компонент для ESP-IDF, реализующий драйвер цифровых датчиков влажности и температуры серии AHT (AHT10/AHT15/AHT20/AHT21/AHT25/AHT30/AHT40) по интерфейсу I2C.
Компонент предоставляет высокоуровневый API для инициализации датчика, чтения влажности и температуры с проверкой целостности данных, а также для отслеживания статистики ошибок связи.
## Основные возможности
1. **Поддержка всей серии AHT** — один драйвер работает со всеми датчиками семейства без различий в модели.
2. **Новый драйвер I2C Master** — использует API `i2c_master` (driver/i2c_master.h) вместо устаревшего i2c_driver.
3. **Автоматическая инициализация датчика** — проба устройства и отправка команды инициализации, если датчик не откалиброван.
4. **Проверка целостности данных (CRC8)** — каждый кадр данных проверяется алгоритмом CRC8 с полиномом `0x31`.
5. **Валидация состояния датчика** — контроль битов калибровки и готовности в ответе датчика.
6. **Статистика ошибок** — глобальный счётчик ошибок драйвера I2C с возможностью сброса.
7. **Внешнее управление шиной I2C** — шина создаётся пользователем, что позволяет подключать несколько устройств на одну шину.
8. **Поддержка нескольких датчиков** — через внешние расширители (например, мультиплексор `zh_pca9548a`).
9. **Скрытая реализация (opaque handle)** — внутреннее состояние недоступно пользователю, работа только через публичный API.
## Внутреннее устройство
### Структура `zh_aht_handle_t`
Дескриптор датчика — непрозрачный тип (`struct _zh_aht_handle_t`), полный определитель которого скрыт в `zh_aht.c`. Пользователь оперирует только указателем на него.
| Поле | Тип | Описание |
|------|-----|----------|
| `dev_handle` | `i2c_master_dev_handle_t` | Дескриптор I2C-устройства, через который выполняется вся связь с датчиком |
### Структура `zh_aht_init_config_t` (публичная)
Конфигурация инициализации, заполняется пользователем. Для значений по умолчанию используется макрос `ZH_AHT_INIT_CONFIG_DEFAULT()` (частота `400000` Гц, адрес `0x38`), при этом поле `i2c_handle` заполняется обязательно.
| Поле | Тип | Описание |
| ------ | ----- | ---------- |
| `i2c_handle` | `i2c_master_bus_handle_t` | Дескриптор I2C-шины, созданный пользователем |
| `i2c_address` | `uint8_t` | Адрес устройства на шине: `0x38`, `0x39` или `0x44` |
| `i2c_frequency` | `uint32_t` | Тактовая частота SCL, от `1` до `400000` Гц |
### Структура `zh_aht_stats_t` (публичная)
Глобальная статистика ошибок, доступна только на чтение через `zh_aht_get_stats()`.
| Поле | Тип | Описание |
|------|-----|----------|
| `i2c_driver_error` | `uint32_t` | Счётчик ошибок драйвера I2C (transmit/receive/remove device) |
### Команды датчика
Вспомогательные константы протокола AHT, расположенные в `zh_aht.c`:
| Константа | Значение | Описание |
| ----------- | ---------- | ---------- |
| `_aht_reset_command` | `0xBA` | Команда программного сброса датчика |
| `_aht_read_command` | `0xAC 0x33 0x00` | Последовательность запуска измерения и чтения данных |
| `_aht_init_command` | `0xBE 0x08 0x00` | Команда инициализации (калибровки) для AHT20 и новее |
| `status_cmd` | `0x71` | Команда чтения байта статуса датчика |
### Управление памятью
- Дескриптор выделяется одной операцией `heap_caps_calloc(1, sizeof(zh_aht_handle_t), MALLOC_CAP_8BIT)` при инициализации, поэтому все поля гарантированно обнулены.
- Освобождение — только через `heap_caps_free()` внутри `zh_aht_deinit()`, после чего указатель в копии вызывающего кода обнуляется.
- При любой ошибке инициализации уже выделенная память освобождается в макросах проверки ошибок (`ZH_ERROR_CHECK` с блоком `cleanup`), утечки исключены.
- Статистика ошибок `_stats` — статическая глобальная переменная, живущая всё время работы программы.
### Макросы обработки ошибок
Внутренние макросы `ZH_ERROR_CHECK` и `ZH_ERROR_CHECK_CONT` инкапсулируют типовую схему «проверка условия → логирование с именем функции, строкой и кодом ошибки → очистка ресурсов → возврат ошибки». Это устраняет дублирование кода обработки ошибок во всех функциях компонента.
## API (интерфейс)
### Инициализация и освобождение
| Функция | Описание |
|---------|----------|
| `zh_aht_init()` | Инициализация датчика: валидация конфигурации, выделение дескриптора, регистрация устройства на I2C-шине, проба датчика и отправка команды инициализации при необходимости. Указатель на дескриптор должен быть `NULL`. |
| `zh_aht_deinit()` | Удаление устройства с I2C-шины и освобождение дескриптора. Входной указатель обнуляется. |
### Чтение данных
| Функция | Описание |
|---------|----------|
| `zh_aht_read()` | Запуск измерения (команда `0xAC`), приём 7-байтного ответа, проверка статуса, CRC и преобразование сырых байтов во влажность (0–100 %) и температуру (−50…+150 °C). |
### Сброс датчика
| Функция | Описание |
|---------|----------|
| `zh_aht_reset()` | Отправка команды сброса `0xBA`, проверка статуса по `0x71` и повторная инициализация датчика, если бит калибровки сброшен. |
### Получение информации
| Функция | Описание |
|---------|----------|
| `zh_aht_get_stats()` | Возвращает указатель на глобальную структуру статистики ошибок `zh_aht_stats_t` (только для чтения). |
### Управление статистикой
| Функция | Описание |
|---------|----------|
| `zh_aht_reset_stats()` | Сбрасывает все счётчики ошибок статистики в ноль. |
## Потокобезопасность
Компонент **не является потокобезопасным**: механизмы синхронизации (мьютексы, критические секции, атомарные операции) не используются.
1. **Отсутствие защиты дескриптора** — одновременный вызов API-функций с одним дескриптором из нескольких задач может привести к некорректному обмену по I2C.
2. **Глобальная статистика `_stats`** — инкремент счётчика `i2c_driver_error` не атомарен. На практике на одной шине I2C операции передаются последовательно драйвером шины, но при многозадачном доступе к одному датчику синхронизацию должен обеспечить пользователь.
## Алгоритмы
### Преобразование сырых данных в инженерные единицы
Датчик возвращает 20-битные значения влажности и температуры в 7-байтном кадре:
- **Влажность**: байты `[1][2][3]` конкатенируются в 24-битное слово, старшие 4 бита отбрасываются (`>> 4`), затем `raw / 1048576 * 100` — результат в процентах.
- **Температура**: байты `[3][4][5]` конкатенируются в 24-битное слово, маскируются младшие 20 бит (`& 0xFFFFF`), затем `raw / 1048576 * 200 - 50` — результат в °C.
- Байт `[3]` используется обоими значениями: его младшие 4 бита содержат старшие биты температуры.
### Проверка состояния датчика в кадре
- Бит `0x08` байта статуса `[0]` — признак откалиброванности датчика; если он сброшен, возвращается `ESP_ERR_INVALID_STATE`.
- Бит `0x80` байта статуса `[0]` — флаг занятости/таймаута измерения; если он установлен, возвращается `ESP_ERR_TIMEOUT`.
### Алгоритм CRC8
- Начальное значение — `0xFF`, полином — `0x31`, без инвертирования результата.
- Для каждого байта данных: XOR с `crc`, затем 8 итераций сдвига влево с условным XOR полинома при выходе единицы за старший бит.
- Проверятся первые 6 байт кадра, 7-й байт — эталонная CRC от датчика.
### Процедура инициализации I2C-подключения
1. Регистрация устройства на шине (`i2c_master_bus_add_device`) с 7-битным адресом и заданной частотой.
2. Задержка 100 мс, затем проба датчика (`i2c_master_probe`) — при отсутствии ответа устройство удаляется с шины и возвращается `ESP_ERR_NOT_FOUND`.
3. Чтение байта статуса командой `0x71`.
4. Если бит калибровки `0x08` сброшен (характерно для AHT10/AHT15), отправляется команда инициализации `0xBE 0x08 0x00` и выдерживается 20 мс.
## Примеры использования
### Базовый пример
```c
#include "zh_aht.h"
zh_aht_handle_t *aht_handle = NULL;
void app_main(void)
{
// Создаём I2C-шину (шину создаёт пользователь)
i2c_master_bus_config_t i2c_bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_NUM_MAX - 1,
.scl_io_num = GPIO_NUM_22,
.sda_io_num = GPIO_NUM_21,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_master_bus_handle_t i2c_bus_handle = NULL;
i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle);
// Инициализируем датчик конфигурацией по умолчанию
zh_aht_init_config_t config = ZH_AHT_INIT_CONFIG_DEFAULT();
config.i2c_handle = i2c_bus_handle;
zh_aht_init(&config, &aht_handle);
float humidity = 0.0;
float temperature = 0.0;
for (;;)
{
// Читаем влажность и температуру
if (zh_aht_read(&aht_handle, &humidity, &temperature) == ESP_OK)
{
printf("Humidity: %.2f%%, Temperature: %.2f°C\n", humidity, temperature);
}
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
// Освобождение ресурсов
zh_aht_deinit(&aht_handle);
}
```
### Обработка ошибок и сброс датчика
```c
#include "zh_aht.h"
static void read_sensor(zh_aht_handle_t **handle)
{
float humidity = 0.0;
float temperature = 0.0;
esp_err_t err = zh_aht_read(handle, &humidity, &temperature);
switch (err)
{
case ESP_OK:
printf("H: %.2f%%, T: %.2f°C\n", humidity, temperature);
break;
case ESP_ERR_INVALID_CRC:
// Повреждение данных — пробуем сбросить датчик
zh_aht_reset(handle);
break;
case ESP_ERR_TIMEOUT:
// Датчик не завершил измерение — читаем позже
break;
default:
printf("Read error: %s\n", esp_err_to_name(err));
break;
}
}
```
### Мониторинг статистики ошибок
```c
#include "zh_aht.h"
void monitor_errors(zh_aht_handle_t **handle)
{
// Получаем глобальную статистику (указатель всегда валиден)
const zh_aht_stats_t *stats = zh_aht_get_stats();
printf("I2C errors: %lu\n", (unsigned long)stats->i2c_driver_error);
// Пороговая реакция на накопленные ошибки
if (stats->i2c_driver_error > 10)
{
zh_aht_reset(handle); // сбрасываем датчик
zh_aht_reset_stats(); // обнуляем счётчики
}
}
```
## Логирование
Компонент использует ESP-IDF логирование с тегом `"zh_aht"`. Сообщения `INFO` сопровождаются ключевые операции, сообщения `ERROR` содержат имя файла, строку и имя кода ошибки.
```c
#include "esp_log.h"
// Уменьшить verbosity компонента до уровня ошибок
esp_log_level_set("zh_aht", ESP_LOG_ERROR);
```
## Ошибки
| Код | Описание |
| ----- | ---------- |
| `ESP_OK` | Операция выполнена успешно |
| `ESP_ERR_INVALID_ARG` | Неверный аргумент: `NULL`-указатель, недопустимый адрес/частота I2C или конфигурация |
| `ESP_ERR_INVALID_STATE` | Дескриптор уже инициализирован, либо датчик не откалиброван (бит `0x08` статуса) |
| `ESP_ERR_NO_MEM` | Не удалось выделить память под дескриптор |
| `ESP_ERR_NOT_FOUND` | Датчик не обнаружен на шине (проба не ответила) |
| `ESP_ERR_TIMEOUT` | Датчик не завершил измерение (флаг занятости в байте статуса) |
| `ESP_ERR_INVALID_CRC` | Проверка CRC8 кадра данных не прошла |
| `ESP_FAIL` | Ошибка обмена по I2C (transmit/receive/remove device) |
## Ограничения
1. **Потокобезопасность отсутствует** — компонент не использует мьютексы; один дескриптор должен обслуживаться одной задачей либо синхронизироваться пользователем.
2. **Допустимые адреса I2C жёстко заданы** — валидация принимает только `0x38`, `0x39`, `0x44`, так как это аппаратные адреса серии AHT.
3. **Частота I2C не выше 400 кГц** — датчики серии AHT не поддерживают Fast-mode Plus.
4. **Шина I2C создаётся пользователем** — компонент не управляет шиной, `i2c_handle` в конфигурации обязателен; без него `zh_aht_init()` вернёт `ESP_ERR_INVALID_ARG`.
5. **Статистика глобальна** — `_stats` общая для всех экземпляров датчиков: невозможно определить, какой именно датчик вызвал ошибку I2C.
6. **Блокирующие задержки** — `zh_aht_read()` содержит `vTaskDelay(80 мс)` на время измерения, `zh_aht_reset()` — до 30 мс; функции нельзя вызывать из контекста, где недопусто переключение задач.
7. **Необходим откалиброванный датчик** — если после инициализации и сброса бит калибровки `0x08` остаётся сброшенным, `zh_aht_read()` будет стабильно возвращать `ESP_ERR_INVALID_STATE`.
## Требования для сборки
- ESP-IDF версии `>=5.0` (используется новый API `driver/i2c_master.h`).
- Компонент `driver` (ESP-IDF 5.x) либо `esp_driver_i2c` (ESP-IDF 6.x) — выбирается автоматически в `CMakeLists.txt` по версии фреймворка.
- FreeRTOS (входит в состав ESP-IDF) — используется `vTaskDelay`.
## Установка
```bash
cd ваш_проект/components
git clone https://github.com/aZholtikov/zh_aht
```
```c
#include "zh_aht.h"
```
+40 -15
View File
@@ -13,6 +13,13 @@ static const char *TAG = "zh_aht";
return err; \
}
#define ZH_ERROR_CHECK_CONT(cond, cleanup, msg, ...) \
if (!(cond)) \
{ \
ZH_LOGE(msg, ESP_FAIL, ##__VA_ARGS__); \
cleanup; \
}
/**
* @brief Handle structure for AHT sensor.
*
@@ -25,7 +32,7 @@ struct _zh_aht_handle_t
static const uint8_t _aht_reset_command = 0xBA; /*!< Reset command for AHT sensor */
static const uint8_t _aht_read_command[] = {0xAC, 0x33, 0x00}; /*!< Command sequence to initiate sensor data read */
static const uint8_t _aht_init_command[] = {0xBE, 0x08, 0x00}; /*!< Command sequence to configure sensor for normal operation */
static const uint8_t _aht_init_command[] = {0xBE, 0x08, 0x00}; /*!< Init command */
static zh_aht_stats_t _stats = {0}; /*!< Global statistics structure for error tracking */
/**
@@ -66,7 +73,7 @@ static esp_err_t _zh_aht_i2c_init(const zh_aht_init_config_t *config, zh_aht_han
*/
static uint8_t _zh_aht_calc_crc(const uint8_t *buf, size_t len);
esp_err_t zh_aht_init(const zh_aht_init_config_t *config, zh_aht_handle_t **handle) // -V2008
esp_err_t zh_aht_init(const zh_aht_init_config_t *config, zh_aht_handle_t **handle)
{
ZH_LOGI("AHT initialization begin.");
ZH_ERROR_CHECK(config != NULL && handle != NULL, ESP_ERR_INVALID_ARG, NULL, "AHT initialization failed. Invalid argument.");
@@ -90,16 +97,17 @@ esp_err_t zh_aht_deinit(zh_aht_handle_t **handle)
return ESP_OK;
}
esp_err_t zh_aht_read(zh_aht_handle_t **handle, float *humidity, float *temperature) // -V2008
esp_err_t zh_aht_read(zh_aht_handle_t **handle, float *humidity, float *temperature)
{
ZH_LOGI("AHT read begin.");
ZH_ERROR_CHECK(handle != NULL && *handle != NULL && humidity != NULL && temperature != NULL, ESP_ERR_INVALID_ARG, NULL, "AHT read fail. Invalid argument.");
uint8_t sensor_data[7] = {0};
ZH_ERROR_CHECK(i2c_master_transmit((*handle)->dev_handle, _aht_read_command, sizeof(_aht_read_command), 1000 / portTICK_PERIOD_MS) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(i2c_master_transmit((*handle)->dev_handle, _aht_read_command, sizeof(_aht_read_command), 100) == ESP_OK, ESP_FAIL,
++_stats.i2c_driver_error, "AHT read fail. I2C driver error.");
vTaskDelay(80 / portTICK_PERIOD_MS);
ZH_ERROR_CHECK(i2c_master_receive((*handle)->dev_handle, sensor_data, sizeof(sensor_data), 1000 / portTICK_PERIOD_MS) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(i2c_master_receive((*handle)->dev_handle, sensor_data, sizeof(sensor_data), 100) == ESP_OK, ESP_FAIL,
++_stats.i2c_driver_error, "AHT read fail. I2C driver error.");
ZH_ERROR_CHECK((sensor_data[0] & 0x08) != 0, ESP_ERR_INVALID_STATE, NULL, "AHT read fail. Sensor not calibrated.");
ZH_ERROR_CHECK((sensor_data[0] & 0x80) == 0, ESP_ERR_TIMEOUT, NULL, "AHT read fail. Timeout exceeded.");
ZH_ERROR_CHECK(_zh_aht_calc_crc(sensor_data, 6) == sensor_data[6], ESP_ERR_INVALID_CRC, NULL, "AHT read fail. Invalid CRC.");
*humidity = (((((uint32_t)sensor_data[1]) << 16) | (((uint32_t)sensor_data[2]) << 8) | (((uint32_t)sensor_data[3]) << 0)) >> 4) / 1048576.0 * 100.0;
@@ -112,9 +120,22 @@ esp_err_t zh_aht_reset(zh_aht_handle_t **handle)
{
ZH_LOGI("AHT reset begin.");
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "AHT reset fail. Invalid argument.");
ZH_ERROR_CHECK(i2c_master_transmit((*handle)->dev_handle, &_aht_reset_command, sizeof(_aht_reset_command), 1000 / portTICK_PERIOD_MS) == ESP_OK, ESP_FAIL,
++_stats.i2c_driver_error, "AHT reset fail. I2C driver error.");
ZH_ERROR_CHECK(i2c_master_transmit((*handle)->dev_handle, &_aht_reset_command, sizeof(_aht_reset_command), 100) == ESP_OK,
ESP_FAIL, ++_stats.i2c_driver_error, "AHT reset fail. I2C driver error (reset cmd).");
vTaskDelay(20 / portTICK_PERIOD_MS);
uint8_t status_cmd = 0x71;
uint8_t status = 0;
ZH_ERROR_CHECK(i2c_master_transmit((*handle)->dev_handle, &status_cmd, sizeof(status_cmd), 100) == ESP_OK, ESP_FAIL,
++_stats.i2c_driver_error, "AHT reset fail. I2C driver error (status cmd).");
vTaskDelay(10 / portTICK_PERIOD_MS);
ZH_ERROR_CHECK(i2c_master_receive((*handle)->dev_handle, &status, sizeof(status), 100) == ESP_OK, ESP_FAIL,
++_stats.i2c_driver_error, "AHT reset fail. I2C driver error (status read).");
if ((status & 0x08) == 0)
{
ZH_ERROR_CHECK(i2c_master_transmit((*handle)->dev_handle, _aht_init_command, sizeof(_aht_init_command), 100) == ESP_OK, ESP_FAIL,
++_stats.i2c_driver_error, "AHT reset fail. I2C driver error (init cmd AHT20+).");
vTaskDelay(20 / portTICK_PERIOD_MS);
}
ZH_LOGI("AHT reset completed successfully.");
return ESP_OK;
}
@@ -134,12 +155,12 @@ void zh_aht_reset_stats(void)
static esp_err_t _zh_aht_validate_config(const zh_aht_init_config_t *config)
{
ZH_ERROR_CHECK(config->i2c_address == 0x38 || config->i2c_address == 0x39 || config->i2c_address == 0x44, ESP_ERR_INVALID_ARG, NULL, "Invalid I2C address.");
ZH_ERROR_CHECK(config->i2c_frequency <= 400000, ESP_ERR_INVALID_ARG, NULL, "Invalid I2C frequency.");
ZH_ERROR_CHECK(config->i2c_frequency <= 400000 && config->i2c_frequency > 0, ESP_ERR_INVALID_ARG, NULL, "Invalid I2C frequency.");
ZH_ERROR_CHECK(config->i2c_handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Invalid I2C handle.");
return ESP_OK;
}
static esp_err_t _zh_aht_i2c_init(const zh_aht_init_config_t *config, zh_aht_handle_t *handle) // -V2008
static esp_err_t _zh_aht_i2c_init(const zh_aht_init_config_t *config, zh_aht_handle_t *handle)
{
i2c_device_config_t aht_config = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
@@ -149,15 +170,19 @@ static esp_err_t _zh_aht_i2c_init(const zh_aht_init_config_t *config, zh_aht_han
i2c_master_dev_handle_t _dev_handle = NULL;
ZH_ERROR_CHECK(i2c_master_bus_add_device(config->i2c_handle, &aht_config, &_dev_handle) == ESP_OK, ESP_FAIL, NULL, "Add I2C device failed.");
vTaskDelay(100 / portTICK_PERIOD_MS);
ZH_ERROR_CHECK(i2c_master_probe(config->i2c_handle, config->i2c_address, 1000 / portTICK_PERIOD_MS) == ESP_OK, ESP_ERR_NOT_FOUND,
{ZH_ERROR_CHECK(i2c_master_bus_rm_device(_dev_handle) == ESP_OK, ESP_FAIL, NULL, "I2C remove device failed.")}, "Sensor not connected or not responded.");
ZH_ERROR_CHECK(i2c_master_probe(config->i2c_handle, config->i2c_address, 100) == ESP_OK, ESP_ERR_NOT_FOUND,
{ZH_ERROR_CHECK_CONT(i2c_master_bus_rm_device(_dev_handle) == ESP_OK, NULL, "I2C remove device failed.")}, "Sensor not connected or not responded.");
uint8_t status_cmd = 0x71;
uint8_t sensor_data = 0;
ZH_ERROR_CHECK(i2c_master_receive(_dev_handle, &sensor_data, sizeof(sensor_data), 1000 / portTICK_PERIOD_MS) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(i2c_master_bus_rm_device(_dev_handle) == ESP_OK, ESP_FAIL, NULL, "I2C remove device failed.")}, "I2C driver error.");
ZH_ERROR_CHECK(i2c_master_transmit(_dev_handle, &status_cmd, sizeof(status_cmd), 100) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK_CONT(i2c_master_bus_rm_device(_dev_handle) == ESP_OK, NULL, "I2C remove device failed.")}, "I2C driver error (status cmd).");
vTaskDelay(10 / portTICK_PERIOD_MS);
ZH_ERROR_CHECK(i2c_master_receive(_dev_handle, &sensor_data, sizeof(sensor_data), 100) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK_CONT(i2c_master_bus_rm_device(_dev_handle) == ESP_OK, NULL, "I2C remove device failed.")}, "I2C driver error (status read).");
if ((sensor_data & 0x08) == 0)
{
ZH_ERROR_CHECK(i2c_master_transmit(_dev_handle, _aht_init_command, sizeof(_aht_init_command), 1000 / portTICK_PERIOD_MS) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(i2c_master_bus_rm_device(_dev_handle) == ESP_OK, ESP_FAIL, NULL, "I2C remove device failed.")}, "I2C driver error.");
ZH_ERROR_CHECK(i2c_master_transmit(_dev_handle, _aht_init_command, sizeof(_aht_init_command), 100) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK_CONT(i2c_master_bus_rm_device(_dev_handle) == ESP_OK, NULL, "I2C remove device failed.")}, "I2C driver error (init cmd).");
vTaskDelay(20 / portTICK_PERIOD_MS);
}
handle->dev_handle = _dev_handle;