A bare-metal STM32 firmware project that reads real-time electrical parameters from a PZEM-004T v3.0 energy monitoring sensor and logs them to an SD card, built on STM32CubeIDE with HAL and FatFs.
This started as a port of a working ESP32/Arduino implementation, rebuilt from the ground up in C for a bare-metal STM32 target — partly as a learning exercise in CubeIDE, partly as the foundation for a standalone energy-logging device.
- MCU: STM32F103C8T6 ("Blue Pill"), 64KB Flash / 20KB RAM
- Sensor: PZEM-004T v3.0 (Modbus-RTU over UART, 9600 baud, 8N1)
- Storage: microSD card via SPI, FAT filesystem (FatFs)
| Signal | MCU Pin | Peripheral |
|---|---|---|
| PZEM TX/RX | PA9 (TX) / PA10 (RX) | USART1 |
| SD Card | SPI (CS/MOSI/MISO/SCK per your config) | SPI1 (or as configured) |
- Modular driver design — sensor and storage logic are fully decoupled, each behind a small, focused API:
pzem.c/pzem.h— PZEM-004T v3.0 driver (Modbus-RTU frame construction, CRC16 validation, register parsing)storage.c/storage.h— SD card mount and append-only logging via FatFs
- Interrupt-driven UART RX for PZEM communication — the CPU isn't blocked polling the UART peripheral; incoming bytes are captured via
HAL_UART_RxCpltCallbackinto a ring buffer - CRC16-Modbus validation on every received frame, rejecting corrupted reads
- Cached sensor readings, rate-limited internally to avoid over-polling the sensor
- Append-based SD card logging using FatFs (
f_open/f_write/f_close)
Each log entry captures, per read cycle:
- Voltage (V)
- Current (A)
- Active Power (W)
- Energy (kWh, cumulative since last reset)
- Frequency (Hz)
- Power Factor
Core/
├── Inc/
│ ├── pzem.h # PZEM-004T driver interface
│ └── storage.h # SD card storage interface
└── Src/
├── pzem.c # PZEM-004T driver implementation
├── storage.c # SD card storage implementation
└── main.c # Initialization + main polling loop
storage_init()mounts the SD card filesystem via FatFs.pzem_init()configures the driver with the USART1 handle and arms the first interrupt-driven UART read.- In the main loop,
pzem_update()requests a register read from the sensor (rate-limited to once every 200ms internally), waits for the response via the interrupt-filled ring buffer, and validates it with CRC16. - On a successful read, the cached values are formatted into a line and appended to the SD card via
storage_write_data().
- Open the project in STM32CubeIDE.
- Ensure USART1 (PA9/PA10) is configured for 9600 baud, 8N1, with the USART1 global interrupt enabled in NVIC.
- Ensure your SPI/SD card peripheral and FatFs middleware are configured per your wiring.
- Project Properties → C/C++ Build → Settings → MCU Settings — enable
-u _printf_floatif usingprintf/snprintfwith floating-point values (required for logging sensor readings, since newlib-nano strips float support fromprintfby default). - Build and flash.
pzem_reset_energy()currently mirrors a return-logic quirk present in the original Arduino library it was ported from; this has not yet been corrected pending hardware verification.- Logging currently writes to a single fixed filename rather than a per-session file.
- No file header / column labels are written on first file creation.
This project is being actively developed toward a more capable, RTOS-based architecture:
- Migrate to FreeRTOS (CMSIS-V2) — move sensor polling and SD logging into separate tasks, decoupled via queues, to support multiple independent timing domains as more peripherals are added
- RTC module integration — timestamp each log entry with real date/time
- LCD display support — live readout of current sensor values
- Per-session log file naming
- CSV header row on first file write
- Re-evaluate and fix
pzem_reset_energy()return logic
The PZEM-004T communication protocol implementation is a C port of the PZEM-004T-v30 Arduino library by Jakub Mandula (MIT License), adapted from C++/Arduino Stream-based I/O to bare-metal C using STM32 HAL and interrupt-driven UART.
MIT — see LICENSE for details (note: includes attribution requirements stemming from the original PZEM-004T-v30 library license).