RaceTemp_BLE is an STM32WB55 based Bluetooth Low Energy sensor bridge for RaceChrono. It reads local sensors, packages the measurements as RaceChrono DIY CAN/BLE messages, and lets RaceChrono log them as if they came from a CAN bus.
The project is based on the RaceChrono BLE DIY device idea from Aollin's demo project:
https://github.com/aollin/racechrono-ble-diy-device
RaceTemp adds the temperature, pressure, thermocouple, RPM, engine-hours, and engine-revolutions logic used by this project.
This is still a work in progress, but has som "track record" as it's the only logger we have used for track days and races in 2026.
Implemented:
- BLE device name:
RaceTemp - RaceChrono-compatible CAN message characteristic
- Optional RaceChrono CAN filter handling
- ADC based MAP and NTC readings
- MAX31855 thermocouple support
- MAX31856 thermocouple support
- Ignition input based RPM measurement
- Engine operating hours counter
- Engine revolutions counter
- Persistent storage of engine counters after the engine stops
- Optional raw measurement snapshot message
You can download the binary (Release/RaceTemp_BLE.elf) and flash it to an STM32WB55. Android field flashing has been tested with STlinkP (Android app), STLINK-V3MINIE, and the WeAct STM32WB board. One can of course also flash it via ST-Link and STM32CubeProgrammer on a PC.
If you want to compile yourself on a PC with STM32CubeMX and STM32CubeIDE, then follow this 4-step guide:
- Open the project in STM32CubeMX and click "GENERATE CODE". This should generate some code files and copy some library files.
- Run the script: .\tools\fix-cubemx-ble-duplicates.ps1 See also my CubeMX-notes at the bottom of this page.
- Open the project in STM32CubeIDE and build the
DebugorReleaseconfiguration. - Connect the STLink debug probe and flash the binary.
The intended BLE security setup is:
- Bonding enabled
- No passkey
- No MITM requirement
- No input/output capability
- Secure connections mandatory
- GATT caching enabled
- Static random address
C0:52:54:42:4C:45 - Advertisement includes the complete local name
RaceTemp - Advertisement includes the RaceChrono DIY service UUID
0x1ff8
If pairing fails after a firmware or security setting change, delete/forget the
old RaceTemp pairing on the phone and pair again.
Target MCU:
- STM32WB55CGU
The project has been developed and tested on a WeAct STM32WB55 board.
Thermocouple IC options:
- MAX31855
- MAX31856
The thermocouple IC is selected at compile time in:
Core/Inc/RaceTemp_config.h
Do not trust wire color blindly. For many Type K thermocouples using US/ANSI colors, yellow is positive and red is negative. Other color standards and probe suppliers may differ.
A quick practical TC polarity check:
- Connect the thermocouple.
- Read the thermocouple channel in RaceChrono.
- Warm the thermocouple tip with your fingers, hot air, or a lighter from a safe distance.
- If the temperature goes up, the polarity is correct.
- If the temperature goes down or becomes negative, swap the two thermocouple wires.
Edit Core/Inc/RaceTemp_config.h and set:
#define RACETEMP_THERMOCOUPLE_IC RACETEMP_THERMOCOUPLE_IC_MAX31855or:
#define RACETEMP_THERMOCOUPLE_IC RACETEMP_THERMOCOUPLE_IC_MAX31856For MAX31856, also select the thermocouple type. K-Type is most common (for motorsport and exhaust temperature).
The firmware sends the thermocouple data as raw IC bytes on CAN ID 0x13.
RaceChrono then decodes those bytes with an equation. This keeps the firmware
simple and makes it possible to inspect status and fault bits from the IC.
PID 21 / CAN ID 0x15 is the stored engine operating time in hours. It is a
lifetime counter, not time since boot. PID 22 / CAN ID 0x16 is the stored
engine revolution counter.
The counters are stored in the last two 4 KB flash pages:
0x0807E0000x0807F000
The linker script reserves those pages by limiting normal application flash to
504K. This makes the firmware image fail to link before it can overlap the
counter storage.
Expected flashing behavior:
- Flashing with "erase necessary pages only" should preserve the counters.
- Flashing with mass erase / full chip erase resets the counters to zero.
- Manually erasing pages
0x0807E000or0x0807F000resets the counters.
The firmware writes the counters to flash when RPM times out after the engine stops. If the RPM input is left floating during bench testing, false pulses can increase the counters, so keep the RPM input at a defined level when it is not connected to a signal source.
- Power the RaceTemp device.
- Pair the phone with the BLE device named
RaceTemp. - Open RaceChrono.
- Add/connect it as a RaceChrono BLE DIY/CAN device.
- Add custom CAN channels for the CAN IDs listed below.
- Enable the channels you want to log.
RaceChrono may show CAN IDs in either hexadecimal or decimal form. For example,
0x13 is decimal 19.
RaceChrono equations are documented here:
https://racechrono.com/support/equations
The thermocouple equations below use RaceChrono's bitsToInt() function. In
RaceChrono, bit offset 0 is the most significant bit of payload byte 0.
For the MAX31855 and MAX31856 temperature channels, the recommended equations
below use bytesToInt() because they are easier to enter correctly on a phone.
Most RaceTemp channels are sent as little-endian 32-bit floats. For those channels, use this RaceChrono equation:
bytesToFloatLe(raw, 0, 4)
| CAN ID | Decimal | Channel | Payload | Unit | RaceChrono equation |
|---|---|---|---|---|---|
0x0B |
11 | MAP pressure | float32 little-endian | kPa | bytesToFloatLe(raw, 0, 4) |
0x0C |
12 | MAP NTC temperature | float32 little-endian | deg C | bytesToFloatLe(raw, 0, 4) |
0x0D |
13 | Volvo NTC temperature | float32 little-endian | deg C | bytesToFloatLe(raw, 0, 4) |
0x0E |
14 | AC NTC temperature | float32 little-endian | deg C | bytesToFloatLe(raw, 0, 4) |
0x0F |
15 | KOSO NTC temperature | float32 little-endian | deg C | bytesToFloatLe(raw, 0, 4) |
0x10 |
16 | STM32 MCU temperature | float32 little-endian | deg C | bytesToFloatLe(raw, 0, 4) |
0x11 |
17 | MAP raw ADC value | float32 little-endian | ADC count | bytesToFloatLe(raw, 0, 4) |
0x12 |
18 | NTC raw ADC value | float32 little-endian | ADC count | bytesToFloatLe(raw, 0, 4) |
0x13 |
19 | Thermocouple raw data | IC-specific raw bytes | deg C | See thermocouple sections below |
0x14 |
20 | Engine speed | float32 little-endian | RPM | bytesToFloatLe(raw, 0, 4) |
0x15 |
21 | Engine operating time | float32 little-endian | hours | bytesToFloatLe(raw, 0, 4) |
0x16 |
22 | Engine revolutions | float32 little-endian | rev | bytesToFloatLe(raw, 0, 4) |
0x17 |
23 | Ignition period | float32 little-endian | timer ticks | bytesToFloatLe(raw, 0, 4) |
0x18 |
24 | Ignition captures | float32 little-endian | count | bytesToFloatLe(raw, 0, 4) |
0x19 |
25 | Ignition rejects | float32 little-endian | count | bytesToFloatLe(raw, 0, 4) |
0x1A |
26 | EGT temperature | float32 little-endian | deg C | bytesToFloatLe(raw, 0, 4) |
0x1B |
27 | Engine speed peak hold | float32 little-endian | RPM | bytesToFloatLe(raw, 0, 4) |
0x1C |
28 | EGT peak hold | float32 little-endian | deg C | bytesToFloatLe(raw, 0, 4) |
0x1D |
29 | Engine speed filtered | float32 little-endian | RPM | bytesToFloatLe(raw, 0, 4) |
0x1E |
30 | Thermocouple status | float32 little-endian | bitmask | bytesToFloatLe(raw, 0, 4) |
0x1F |
31 | Thermocouple recoveries | float32 little-endian | count | bytesToFloatLe(raw, 0, 4) |
0x20 |
32 | Raw measurement snapshot | packed raw bytes | debug | See source layout |
0x21 |
33 | RPM fast packet | packed float32 little-endian values | RPM | See RPM fast packet below |
The peak hold channels keep a new maximum value visible for 5 seconds. The RPM peak hold uses the filtered RPM channel so single noisy periods do not create false peaks. After 5 seconds without a higher sample, the held channels return to the current live value.
The filtered RPM channel is calculated from a short recent ignition-period window with outlier trimming. It is intended for the driver display. The raw RPM channel remains useful for ignition pickup debugging.
CAN ID 0x21 is sent at 25 Hz. It packs the driver-facing RPM values into one
BLE notification:
| Offset | Channel | Unit | RaceChrono equation |
|---|---|---|---|
| 0 | RPM raw | RPM | bytesToFloatLe(raw, 0, 4) |
| 4 | RPM filtered | RPM | bytesToFloatLe(raw, 4, 4) |
| 8 | RPM peak hold | RPM | bytesToFloatLe(raw, 8, 4) |
The individual RPM and ignition debug channels remain available at the slower sensor publishing rate for comparison and troubleshooting.
Thermocouple status is a bitmask:
1: current EGT sample is valid2: SPI/read error4: thermocouple IC fault bit set8: MAX31856 config readback mismatch/error16: recovery was triggered on this sample32: implausible thermocouple temperature
When the thermocouple sample is invalid, the EGT channel sends 0 C. This
is intentionally obvious in the driver display and logs; the firmware does not
hold the last good EGT value.
When RACETEMP_THERMOCOUPLE_IC_MAX31855 is selected, CAN ID 0x13 contains
the four raw bytes read from the MAX31855. The byte order is the same as the IC
SPI transfer: most significant byte first.
The MAX31855 thermocouple temperature is a signed 14-bit value with 0.25 deg C per bit.
Recommended RaceChrono equation:
bytesToInt(raw, 0, 2) / 16.0
Equivalent bitfield equation:
bitsToInt(raw, 0, 14) * 0.25
The bytesToInt() version works because the MAX31855 thermocouple value is
left-aligned in the first two raw bytes.
Useful notes:
- The MAX31855 fault flag is bit D16 of the 32-bit raw value.
- The low bits contain the IC-specific fault details.
- A separate RaceChrono status channel can use
bytesToUint(raw, 3, 1)to show the lowest raw byte. - If the thermocouple channel shows unrealistic values, first confirm that the RaceTemp firmware is built for MAX31855 and not MAX31856.
When RACETEMP_THERMOCOUPLE_IC_MAX31856 is selected, CAN ID 0x13 contains
four bytes:
| Payload byte | Contents |
|---|---|
| 0 | MAX31856 LTCBH register |
| 1 | MAX31856 LTCBM register |
| 2 | MAX31856 LTCBL register |
| 3 | MAX31856 fault/status register |
The MAX31856 thermocouple temperature is a signed 19-bit value with 0.0078125 deg C per bit.
Recommended RaceChrono equation:
bytesToInt(raw, 0, 3) / 4096.0
Equivalent bitfield equation:
bitsToInt(raw, 0, 19) * 0.0078125
The bytesToInt() version works because the MAX31856 thermocouple value is
left-aligned in the first three raw bytes.
Useful notes:
- Select the correct thermocouple type in
RaceTemp_config.h. - Payload byte 3 is the MAX31856 fault/status register.
- A separate RaceChrono status channel can use
bytesToUint(raw, 3, 1). - If the temperature is wrong but changes with heat, check the selected thermocouple type first.
- If the value is completely wrong, confirm that RaceChrono is using the MAX31856 equation and not the MAX31855 equation.
CAN ID 0x20 is intended for development and debugging. It sends one packed
snapshot of the raw measurement buffer:
- ADC samples
- Thermocouple SPI bytes
- Ignition timing data
Normal RaceChrono logging should usually use the individual channels instead. The raw snapshot is useful when checking new sensors or debugging conversion logic.
Unfortunately the current version of CubeMX can make some mistakes (it can duplicate user-section content in the BLE files), and some generated files contain project-specific settings. After regenerating the code, run the cleanup script before building:
powershell -ExecutionPolicy Bypass -File .\tools\fix-cubemx-ble-duplicates.ps1The script removes duplicate CubeMX generated BLE functions, restores the RaceChrono BLE callbacks, restores the BLE security/address settings used by this project, and repairs the local linker script.
Important local linker-script repairs:
- Normal application flash is limited to
504K. PERSISTreserves the last two 4 KB flash pages for engine counters.MAPPING_TABLEis placed at0x20030000.MB_MEM1and.MB_MEM2are placed in the CPU2 shared RAMB area..MB_MEM2is 4-byte aligned so startup initialization does not fault.
The script also removes the extra generated HSEM lock before MX_APPE_Init().
Leaving that duplicate lock in place can stall BLE startup or lead to an early
HardFault.
To re-generate the code from CubeMX:
- Open the .ioc file in CubeMX and click "GENERATE CODE"
- Run
tools/fix-cubemx-ble-duplicates.ps1 - Build in STM32CubeIDE
- Check that pairing still works with
RaceTemp
Keep project-specific RaceTemp logic in the RaceTemp source files where possible. This reduces the amount of code that lives inside CubeMX user sections.