📟 pmme-device: Reading PM 1.0, 2.5, and 10.0. Split sensor and OLED display to separate tasks

This commit is contained in:
Joseph Ferano 2025-06-07 16:13:31 +07:00
parent 52b7a7258c
commit 2c1ab09770
2 changed files with 295 additions and 14 deletions

View File

@ -1,25 +1,97 @@
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "driver/i2c_master.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_log.h"
#include <string.h>
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_vendor.h"
#include "driver/i2c_master.h"
#include "driver/uart.h"
static const char *TAG = "PMME-Device";
#define I2C_HOST 0
#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22
#define I2C_CLOCK_HZ 400000 // Reduced from 400000 to a safer 100KHz
#define SSD1306_ADDR 0x3C
#define SSD1306_WIDTH 128
#define SSD1306_HEIGHT 64
#define UART_NUM UART_NUM_2
#define BUF_SIZE 32
#define UART_BUF_SIZE 128
#define TXD_PIN 17
#define RXD_PIN 16
void app_main(void)
{
#define SWAP_BYTES(x) ((x >> 8) | (x << 8))
typedef struct __attribute__((packed)) {
// PM concentrations (CF=1, standard particle) in μg/m³
uint16_t pm1_0_cf1;
uint16_t pm2_5_cf1;
uint16_t pm10_cf1;
// PM concentrations (atmospheric environment) in μg/m³
uint16_t pm1_0_atm;
uint16_t pm2_5_atm;
uint16_t pm10_atm;
// Particle counts (particles per 0.1L air)
uint16_t particles_0_3um;
uint16_t particles_0_5um;
uint16_t particles_1_0um;
uint16_t particles_2_5um;
uint16_t particles_5_0um;
uint16_t particles_10um;
} pms7003_data_t;
void send_pms7003_command(uint8_t cmd, uint8_t datah, uint8_t datal) {
uint8_t command[7] = {0x42, 0x4D, cmd, datah, datal, 0, 0};
uint16_t checksum = 0;
for (int i = 0; i < 5; i++) {
checksum += command[i];
}
command[5] = (checksum >> 8) & 0xFF;
command[6] = checksum & 0xFF;
uart_write_bytes(UART_NUM, command, 7);
}
void read_pm_data(uint8_t *buf) {
uint16_t checksum = 0;
for (int i = 0; i <= 29; i++) {
checksum += buf[i];
}
uint16_t c = (((uint16_t)buf[30]) << 8) | ((uint16_t)buf[31]);
if (checksum != c) {
ESP_LOGW(TAG, "Checksum from READ does not match, data corrupted");
return;
}
ESP_LOGI(TAG, "SIZEOF pms7003_data_t: %d", sizeof(pms7003_data_t));
if (sizeof(pms7003_data_t) == 24) {
pms7003_data_t sensor_data = {0};
memcpy(&sensor_data, &buf[4], sizeof(pms7003_data_t));
uint16_t *sd = (uint16_t*)&sensor_data;
for (int i = 0; i < sizeof(pms7003_data_t) / sizeof(uint16_t); i++) {
sd[i] = SWAP_BYTES(sd[i]);
}
ESP_LOGI(TAG, "PM Readings:\nPM 1.0: %d\nPM 2.5: %d\nPM 10.0:%d",
sensor_data.pm1_0_atm,
sensor_data.pm2_5_atm,
sensor_data.pm10_atm);
} else {
ESP_LOGW(TAG, "Misaligned struct, ignoring memcpy");
}
}
void pm_sensor_task(void *pvParameters) {
ESP_LOGI(TAG, "Initialize UART, Wait 1 second");
vTaskDelay(pdMS_TO_TICKS(1000));
ESP_LOGI(TAG, "Initialize UART, ok now go...");
@ -35,18 +107,92 @@ void app_main(void)
ESP_ERROR_CHECK(uart_set_pin(UART_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_driver_install(UART_NUM, UART_BUF_SIZE * 2, 0, 0, NULL, 0));
ESP_LOGI(TAG, "Create a simple pattern");
vTaskDelay(pdMS_TO_TICKS(100));
send_pms7003_command(0xE1, 0x00, 0x00);
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "Alloc %d buffer for UART data", BUF_SIZE);
uint8_t uart_data[BUF_SIZE];
int frequency_check = 2000;
while (1) {
int len = uart_read_bytes(UART_NUM, uart_data, BUF_SIZE, frequency_check / portTICK_PERIOD_MS);
send_pms7003_command(0xE2, 0x00, 0x00);
int len = uart_read_bytes(UART_NUM, uart_data, BUF_SIZE, 100 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "Received %d bytes from sensor", len);
if (len > 0) {
for(int i = 0; i < len && i < 16; i++) {
ESP_LOGI(TAG, "0x%02X", uart_data[i]);
if (len == 32 && uart_data[0] != 0x42 && uart_data[1] != 0x4D) {
ESP_LOGW(TAG, "Frame Start does not match 0x42, 0x4D, instead got %x %x", uart_data[0], uart_data[1]);
} else {
read_pm_data(uart_data);
// for(int i = 0; i < len && i < BUF_SIZE; i++) {
// ESP_LOGI(TAG, "0x%02X", uart_data[i]);
// }
}
}
vTaskDelay(pdMS_TO_TICKS(frequency_check));
uart_flush(UART_NUM);
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
void oled_display_task(void *pvParameters) {
ESP_LOGI(TAG, "Initialize I2C bus");
i2c_master_bus_handle_t i2c_bus = NULL;
i2c_master_bus_config_t bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_HOST,
.sda_io_num = I2C_SDA_PIN,
.scl_io_num = I2C_SCL_PIN,
.flags.enable_internal_pullup = true,
};
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &i2c_bus));
ESP_LOGI(TAG, "Install panel IO");
esp_lcd_panel_io_handle_t io_handle = NULL;
esp_lcd_panel_io_i2c_config_t io_config = {
.dev_addr = SSD1306_ADDR,
.scl_speed_hz = I2C_CLOCK_HZ, // Added this line to set the clock speed
.control_phase_bytes = 1,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.dc_bit_offset = 6,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus, &io_config, &io_handle));
ESP_LOGI(TAG, "Install SSD1306 panel driver");
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_ssd1306_config_t vendor_config = {
.height = SSD1306_HEIGHT,
};
esp_lcd_panel_dev_config_t panel_config = {
.bits_per_pixel = 1,
.reset_gpio_num = -1,
.vendor_config = &vendor_config,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(io_handle, &panel_config, &panel_handle));
ESP_LOGI(TAG, "Initialize the display");
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
ESP_LOGI(TAG, "Create a simple pattern");
uint8_t *fb = heap_caps_malloc(SSD1306_WIDTH * SSD1306_HEIGHT / 8, MALLOC_CAP_DMA);
int idx = 0;
while (1) {
memset(fb, 0, SSD1306_WIDTH * SSD1306_HEIGHT / 8);
taskYIELD();
fb[idx] = 0xFF;
esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, SSD1306_WIDTH, SSD1306_HEIGHT, fb);
idx = (idx + 4) % (SSD1306_WIDTH * SSD1306_HEIGHT / 8);
// shift = (shift + 1) % 8;
vTaskDelay(pdMS_TO_TICKS(100));
}
memset(fb, 0, SSD1306_WIDTH * SSD1306_HEIGHT / 8);
free(fb);
}
void app_main(void) {
xTaskCreate(pm_sensor_task, "pm_sensor_task", 4096, NULL, 5, NULL);
// xTaskCreate(oled_display_task, "oled_display_task", 4096, NULL, 10, NULL);
}

View File

@ -0,0 +1,135 @@
Output result
Mainly output as the quality and number of each particles with different size
per unit volume, the unit volume of particle number is 0.1L and the unit of
mass concentration is μ g/m³.
There are two options for digital output: passive and active. Default mode
is active after power up. In this mode sensor would send serial data to the
host automatically .The active mode is divided into two sub-modes: stable
mode and fast mode. If the concentration change is small the sensor
would run at stable mode with the real interval of 2.3s.And if the change is
big the sensor would be changed to fast mode automatically with the
interval of 200~800ms, the higher of the concentration, the shorter of the
interval.
Circuit Attentions
1) DC 5V power supply is needed because the FAN should be driven by 5V.
But the high level of data pin is 3.3V. Level conversion unit should be
used if the power of host MCU is 5V.
2) The SET and RESET pins are pulled up inside so they should not be
connected if without usage.
3) PIN7 and PIN8 should not be connected.
4) Stable data should be got at least 30 seconds after the sensor wakeup
from the sleep mode because of the fans performance.
Installation Attentions
1) Metal shell is connected to the GND so be careful not to let it shorted with
the other parts of circuit except GND.
2) The best way of install is making the plane of inset and outset closely to
the plane of the host. Or some shield should be placed between inset and
outset in order to prevent the air flow from inner loop.
3) The blowhole in the shell of the host should not be smaller than the inset.
4) The sensor should not be installed in the air flow way of the air cleaner or
should be shielded by some structure.
5) The sensor should be installed at least 20cm higher than the grand in
order to prevent it from blocking by the flock dust.
6) Do not break up the sensor.
Other Attentions
1) Only the consistency of all the PM sensors of PLANTOWER is promised
and ensured. And the sensor should not be checked with any third party
equipment.
2) The sensor is usually used in the common indoor environment. So some
protection must be added if using in the conditions as followed:
a) The time of concentration ≥300μ g/m³ is longer than 50% of the
whole year or concentration≥500μ g/m³ is longer than20% of the
whole year.
b) Kitchen
c) Water mist condition such as bathroom or hot spring.
d) outdoor
Appendix IPMS7003 transport protocol-Active Mode
Default baud rate9600bps Check bitNone Stop bit1 bit
32 Bytes
Start character 1 0x42 (Fixed)
Start character2 0x4d (Fixed)
Frame length high
8 bits
…… Frame length=2x13+2(data+check bytes)
Frame length low 8
bits
……
Data 1 high 8 bits …… Data1 refers to PM1.0 concentration unit
μ g/m3CF=1standard particle*
Data 1 low 8 bits ……
Data2 high 8 bits …… Data2 refers to PM2.5 concentration unit
μ g/m3CF=1standard particle
Data2 low 8 bits ……
Data3 high 8 bits …… Data3 refers to PM10 concentration unit
μ g/m3CF=1standard particle
Data3 low 8 bits ……
Data4 high 8 bits …… Data4 refers to PM1.0 concentration unit *
μ g/m3under atmospheric environment
Data4 low 8 bits ……
Data5 high 8 bits …… Data 5 refers to PM2.5 concentration unit
μ g/m3under atmospheric environment
Data5 low 8 bits ……
Data6 high 8 bits ……. Data 6 refers to concentration unit (under
atmospheric environment) μ g/m3 Data6 low 8 bits ……
Data7 high 8 bits …… Data7 indicates the number of
particles with diameter beyond 0.3 um
in 0.1 L of air. Data7 low 8 bits ……
Data8 high 8 bits …… Data 8 indicates the number of
particles with diameter beyond 0.5 um
in 0.1 L of air. Data8 low 8 bits ……
Data9 high 8 bits …… Data 9 indicates the number of
particles with diameter beyond 1.0 um
in 0.1 L of air.
Data9 low 8 bits ……
2016 product data manual of PLANTOWER
Data10 high 8 bits …… Data10 indicates the number of
particles with diameter beyond 2.5 um
in 0.1 L of air.
Data10 low 8 bits ……
Data11 high 8 bits …… Data11 indicates the number of
particles with diameter beyond 5.0 um
Data11 low 8 bits …… in 0.1 L of air.
Data12 high 8 bits …… Data12 indicates the number of
particles with diameter beyond 10 um
Data12 low 8 bits …… in 0.1 L of air.
Data13 high 8 bits …… Data13 Reserved
Data13 low 8 bits ……
Data and check
high 8 bits
…… Check code=Start character1+ Start
character2+……..+data13
Low 8 bits
Data and check
low 8 bits
……
Note: CF=1 should be used in the factory environment
.
2016 product data manual of PLANTOWER
Appendix IIPMS7003 transport protocol-Passive Mode
Default baud rate9600bps Check bitNone Stop bit1 bit
Host Protocol
Start Byte
1
Start Byte
2
Command Data 1 Data 2 Verify Byte
1
Verify Byte
2
0x42 0x4d CMD DATAH DATAL LRCH LRCL
1. Command Definition
CMD DATAH DATAL
0xe2 X X Read in passive
mode
0xe1 X 00H-passive
01H-active
Change mode
0xe4 X 00H-sleep
01H-wakeup
Sleep set
2. Answer
0xe2: 32 bytes , same as appendix I
3. Verify Bytes :
Add of all the bytes except verify bytes.