The problem
Fleet operators and service engineers need trip data to understand how an EV is used and why a fault happened. Logging to external flash on a small controller must survive power loss, wear the flash evenly and keep each record traceable in time.
Objectives
- Write a reliable SPI NOR flash driver (read, page program, sector erase, status polling).
- Design a fixed-size, CRC-protected record format with RTC timestamps.
- Implement a circular log that survives power cycles and finds its write position on boot.
- Provide UART commands to list, dump and erase trips, and a Python tool to export CSV.
What you need
From the EV Embedded Development Kit
- SPI flash memory
- RTC with CR2032 backup cell
- Serial UART interface
- Analog potentiometer input
- 1S battery with connector and T-sense input
- 16×2 LCD
- STM32F103C8T6 main controller
Software
- STM32CubeIDE with HAL drivers
- Embedded C
- Python 3 with pyserial and pandas
Extra parts
None. Everything this project needs is on the kit.
This project uses the kit's lithium cell. Respect polarity, never short the terminals, and don't leave the cell charging unattended.
Key concepts
- NOR flash erase/program rules
- Flash bits can only be programmed from 1 to 0; returning to 1 needs a sector erase (typically 4 KB). Page programs are limited to 256-byte pages and must not cross a page boundary.
- Circular log
- Records are written sequentially and, when the end of the region is reached, the oldest sector is erased and reused. Each sector header carries a sequence number so the newest data can be found after reboot.
- CRC per record
- A short checksum stored with each record detects partly written or corrupted entries, for example after power was lost mid-write.
- Emulated speed
- The kit has no wheel-speed sensor, so speed is derived from the potentiometer acting as throttle through a simple vehicle model.
Step-by-step guide
- 1
Specify what to log
Choose the signals and rate: cell voltage and cell temperature (from the BMS side or the T-sense input, depending on which MCU your kit routes them to; check the schematic), potentiometer throttle, emulated speed and a status byte, at 1 Hz with an optional 10 Hz burst mode. Define a trip as the period between a keypad 'start' and 'stop' or between ignition-on and ignition-off states. Estimate storage: a 16-byte record at 1 Hz is under 60 KB per hour.
Checkpoint: A signal table with units, scaling, rate and bytes per record, and a capacity estimate in hours for your flash size.
- 2
Set up SPI, RTC and UART in CubeMX
Set the DIP switches to connect the SPI flash and RTC blocks. Configure SPI in full-duplex master mode, mode 0, at a conservative prescaler first (for example 4.5 MHz), with the flash chip-select on a GPIO output. Enable the RTC (if your kit's RTC is the STM32's internal RTC on VBAT, enable the LSE and HAL RTC; if it is an external chip, configure the bus it sits on), a USART at 115200 baud and ADC1 for the analog inputs.
Checkpoint: The project builds and HAL_RTC_GetTime (or the external RTC read) returns a time that keeps counting across a reset.
- 3
Write the SPI flash driver
Implement JEDEC ID read (0x9F) and print it to confirm the chip and capacity. Then add write enable (0x06), read status register (0x05, poll the BUSY bit), read data (0x03), page program (0x02, up to 256 bytes within one page) and 4 KB sector erase (0x20). Wrap chip-select handling so every command is atomic. Test by erasing a sector, writing a known pattern, reading it back and comparing.
Checkpoint: The JEDEC ID prints correctly and a write-read-compare test passes on several sectors, including a write that ends exactly at a page boundary.
- 4
Design the record and sector format
Define a packed 16-byte record: 4-byte Unix time, 2-byte cell voltage in mV, 2-byte temperature in 0.1 °C, 1-byte throttle per cent, 2-byte speed in 0.1 km/h, 1-byte status, 2-byte trip ID and a 2-byte CRC-16. Start each 4 KB sector with a header holding a magic value and a 32-bit sequence number. Records never straddle sectors, which makes recovery simple.
Checkpoint: A C struct with static assertions on its size, and a Python struct format string that decodes the same bytes.
- 5
Implement the circular logger and boot recovery
On boot, scan sector headers to find the highest sequence number, then scan that sector for the first erased (0xFF) record slot to resume writing. When a sector fills, erase the next one, write its header with sequence plus one and continue. Buffer records in RAM and write a page at a time to reduce flash operations. Treat any record whose CRC fails as invalid but keep going.
Checkpoint: After 50 random power-offs during logging, the logger always resumes at the right place and no valid record is lost except, at most, the one being written.
- 6
Sample the signals and build the speed model
Read the ADC channels with HAL_ADC_Start_DMA and average several samples. Convert raw counts to millivolts using the divider ratio from the schematic, calibrated against a multimeter reading if one is available. Model speed as a first-order lag driven by throttle, for example speed approaching throttle times a maximum speed with a few seconds' time constant, implemented in integer maths. Show live values on the LCD.
Checkpoint: LCD shows voltage, temperature, throttle and emulated speed that respond sensibly to the potentiometer.
- 7
Add trip control and UART commands
Use keypad keys for trip start and stop, and an incrementing trip ID stored in the sector header area. Add a UART command shell: LIST (trip IDs with start time, end time and record count), DUMP n (stream that trip's records as binary or hex with a final checksum), ERASE ALL (with a confirmation step) and STATUS (free space, oldest and newest times).
Checkpoint: Commands respond correctly from a serial terminal and DUMP of a known trip returns the expected record count.
- 8
Write the Python export tool
Write a script that sends LIST and DUMP over pyserial, decodes records with the struct format, verifies each CRC, converts Unix time to local time and saves one CSV per trip. Add a quick matplotlib plot of speed, throttle and voltage against time so each trip can be inspected immediately. Report any CRC failures and gaps in the time column at the end of each export, so a bad download is noticed before the data is analysed.
Checkpoint: A downloaded trip opens as a tidy CSV and its plot matches what was seen on the LCD during the run.
- 9
Test endurance, wrap-around and timing
Run the logger in fast mode until the circular log wraps at least once and confirm the oldest data is overwritten in order. Measure page-program and sector-erase times with GPIO toggles, and make sure a sector erase (which can take tens of milliseconds) does not stall sampling: start the erase early or run it in a lower-priority loop. Check RTC timestamps stay monotonic across resets.
Checkpoint: A wrap-around test log and a timing table for program and erase operations, with no missed samples during erases.
- 10
Document the logger
Write up the record format, recovery algorithm, command set, timing measurements and capacity calculation. Include the Python tool and a sample trip dataset. Add the flash memory map with addresses of every sector type, the boot recovery flowchart, and the timing table from the endurance test. Note the limits of the design, such as how many hours fit before wrap-around at each sample rate, and what a production logger would add, such as wear statistics and encryption.
Checkpoint: The report includes a format specification that another team could use to decode the flash without your source code.
Testing & validation
- Write-read-compare tests pass across all sectors used.
- Power-cut test: repeated resets during logging lose no more than the in-progress record.
- CRC detects deliberately corrupted records in the export tool.
- Timestamps from the RTC are continuous across resets and match PC time within the RTC's drift.
Expected results
A power-safe trip logger that stores timestamped records in SPI flash, recovers its position after any reset and exports trips to CSV with plots for analysis.
Take it further
- Add a 10 Hz burst mode triggered by a fault or harsh-braking event detected with the MPU6050.
- Compress records with delta encoding to extend logging time.
- Stream the live log over a Bluetooth module (HC-05 add-on) to a phone.
Viva and review questions
Why must a sector be erased before rewriting?
NOR flash programming can only change bits from 1 to 0. An erase sets the whole sector back to 0xFF so it can be programmed again.
How does your logger find where to continue after a reset?
It reads every sector header, picks the highest sequence number and scans that sector for the first erased record slot.
How do you know a record is valid?
Each record carries a CRC-16; a mismatch marks it invalid, which catches half-written records after a power cut.
Where does speed come from?
It is emulated from the potentiometer through a first-order vehicle model, because the kit has no speed sensor.