Firmware for an inline CAN filter that goes between the forward radar and the harness on a 2022+ Mazda CX-5, so openpilot can drive longitudinal control without taking the radar off the bus.
Idle, it is a wire. When openpilot engages, it drops the radar's CRZ_INFO (0x21b) and
CRZ_CTRL (0x21c) on the radar-to-car path and lets openpilot's copies serve the PCM instead.
The radar itself keeps running, so you keep real tracks, stock AEB, and a dash with no Smart
City Brake System warning on it.
Runs on the STM32F105 dual-CAN board sold as an "18 in 1 CAN filter", "MB CAN filter" or
mileage blocker. Around 15 USD. Protocol and state machine live in DESIGN.md.
Read the whole file before you cut anything. This device sits in series with a piece of safety equipment, and there is no bypass relay: if the board dies, the radar is off the bus until you pull it back out.
- The blue STM32F105 dual-CAN filter board. Check the photos before you buy: one LQFP MCU marked STM32F105, two 8-pin CAN transceivers, a 25 MHz crystal, 6-wire pigtail. Ignore the "18 in 1" vehicle list, that is the vendor firmware you are about to erase.
- ST-LINK V2. The 15 dollar metal-shell clones work. Bring female-to-female Dupont jumpers, they usually ship without a cable.
- Two mating halves of the Mazda radar connector, one of each gender, with pigtails.
- Multimeter. Not optional. See the wiring section.
arm-none-eabi-gcc,openocd,stlink. On a Mac:brew install open-ocd stlink.
Connector 0415-108 RADAR UNIT, black, 5 cavities. From the factory diagram 0415-1b
(Dynamic Stability Control, with Smart Brake Support), lettered E D C B A left to right in the
diagram's connector view:
| Pin | Wire | Signal | Expected |
|---|---|---|---|
| E | violet | IG1, off the IG1 relay and the C/U IG1 15 A fuse | B+ ignition on, under 1.0V at LOCK |
| D | white | HS CAN_H | communication, no useful DC value |
| C | red | HS CAN_L | communication, no useful DC value |
| B | black | ground, body ground | under 1.0V under any condition |
| A | none | unused | nothing |
Red is CAN_L. It is not power. Wire it by colour convention instead of by this table and you will put 12V into a CAN transceiver.
The letters above are the harness-side connector as the manual draws it, latch up, looking into the mating face. The pair is twisted from the factory.
That pair is the shared HS-CAN, not a private link to one module: the manual's DSC page shows it
at the FBCM and its PCM page shows the same two wires at PCM 1H/1L (or 2S/2T depending
on build). The radar is a stub hanging off the main bus, so cutting at this connector puts the
board between the radar and every other module. That is also what makes the firmware work at
all, since it watches CRZ_BTNS and ENGINE_DATA arriving from the car side.
Power comes off IG1, which is why a dead board reads as "radar offline" on a parked car rather than a fault at speed.
You are building a Y: one connector matching the radar's face (goes into the car harness), one matching the harness (goes into the radar).
car-side D (CAN_H) ------> CAN-1H
car-side C (CAN_L) ------> CAN-1L
radar-side D (CAN_H) -----> CAN-2H
radar-side C (CAN_L) -----> CAN-2L
car-side E (12V) --------> radar-side E, tap to +12V pad
car-side B (GND) --------> radar-side B, tap to GND pad
CAN-1 is the car side, CAN-2 is the radar side. Only the pair is cut. Power and ground pass straight through so the radar is powered whether or not the board is.
Take the board's supply off the radar's own IG1 feed, not some other 12V source, so it comes up and goes down with the radar.
The board brings out +12V and GND twice so you can daisy-chain power through it. Splice externally anyway and use one pad of each as a tap. Those traces were laid for a filter drawing tens of milliamps, and a thin trace or a cold joint sags the radar's rail under load instead of failing outright, which is a much worse afternoon.
The factory harness twists the CAN pair. The board's pigtail does not. Twist each pair yourself, one or two twists per inch, and keep the two pairs away from each other and from the 12V run. Keep both legs short. You are turning one clean twisted drop into two stubs and this is a 500 kbps bus.
The pigtail's silkscreen colour column does not match the wire colours actually fitted. Trust the pad labels and ring out every wire before it goes near the car.
Read the connector view backwards and E and A swap, which puts 12V on CAN_H. Ten minutes with a meter on the unplugged harness connector:
- Anchor the row before anything else. Ignition on, probe each cavity to chassis: exactly one reads battery, and that is E. Switch to LOCK and it drops under 1.0V. Ground (B) sits three cavities away and reads under 1.0V throughout. If battery turns up where you expected A, the drawing is mirrored against the part in your hand and every other assumption you make is wrong. Anchor first, cut second.
- Ignition off for this one. Resistance across the pair, measured twice, once into the radar's
own pins and once into the harness. You are inserting a break into a bus, so each side has to
end up terminated on its own.
- Into the radar: 120 ohm means it terminates itself and CAN-2 is fine. Open means you put 120 ohm across CAN-2H and CAN-2L on the board side.
- Into the harness: 60 ohm means the main bus still has two terminators without the radar, so CAN-1 is fine. 120 ohm means the radar was one of the bus's two terminators, and moving it behind the board leaves the car under-terminated. Add 120 ohm across CAN-1H and CAN-1L. Getting this wrong does not fail cleanly. It shows up as intermittent bus errors under load.
- Ignition on, plugged back in, backprobed. Both CAN pins idle near 2.5V with CAN_H a little higher. The manual gives no pass/fail voltage for C and D because they carry traffic, so this only tells you the bus is alive, not that it is healthy.
cd board
make
board/build/BootStub.bin is the bootloader, about 9 KB. board/build/CanFilter.bin.signed is
the application, about 13 KB, signed with the debug cert. Both fit the 128 KB RBT6 and the
256 KB RCT6. DEBUG=1 in the Makefile disables the watchdog and turns on debug prints.
First flash is over SWD. The boards ship read-protected with no bootloader.
J1 is the 5-pin SWD header on the right edge. From the square pad, nearest the crystal:
| J1 | Signal | ST-LINK |
|---|---|---|
| 1 (square) | VCC 3.3V | 3.3V |
| 2 | BOOT0 | leave it |
| 3 | GND | GND |
| 4 | PA13 / SWDIO | SWDIO |
| 5 | PA14 / SWCLK | SWCLK |
The ST-LINK's 3.3V powers the board for flashing. You do not need 12V until you are pushing CAN
traffic. There is no NRST on this header, so st-info will warn about it and the NRST-to-GND
unlock trick you will find in older writeups does not apply here. BOOT0 on pin 2 is the way out
if the vendor firmware ever refuses to give up the SWD pins: strap it to VCC, power cycle, and
the ROM bootloader comes up with the pins free.
Header order is not consistent between batches, and the green RCT6 revision is a different order entirely (GND / PA14 / PA13 / VBAT / NRST). If yours does not match, buzz each pin out against MCU pins 46 (PA13) and 49 (PA14), ground, and the 3.3V rail. Do not use a regulator tab as a 3.3V reference on the blue board. U2's tab is ground, and putting 3.3V on it just folds back the programmer's supply and drops its LED, which will send you hunting for a fault that is not there.
The vendor application takes over the SWD pins after it boots, so every st-* command needs
--connect-under-reset. OpenOCD handles this itself.
st-info --probe --connect-under-reset
You want chipid: 0x418, STM32F1xx_CL. A read-protected board reports a nonsense 1 KiB flash
size and hands back the same 16 bytes for every address you read, including the option byte and
flash size registers. That is RDP, not a wiring problem. Clear it:
openocd -f interface/stlink.cfg -c "transport select hla_swd" -f target/stm32f1x.cfg \
-c "reset_config none separate" \
-c "init" -c "stm32f1x unlock 0" -c "reset halt" \
-c "stm32f1x options_read 0" -c "exit"
This mass-erases the vendor firmware. That is the point. After the reset you want
read protection: off and a sane flash size. A software reset halt is enough, no power cycle
needed whatever the tool says. Then write both images:
cd board/build
openocd -f interface/stlink.cfg -c "transport select hla_swd" -f target/stm32f1x.cfg \
-c "reset_config none separate" -c "init" -c "halt" \
-c "program BootStub.bin 0x08000000 verify" \
-c "program CanFilter.bin.signed 0x08004000 verify" \
-c "reset run" -c "exit"
Both should say ** Verified OK **. To prove the application is actually running and not stuck
in the bootloader, halt and read the PC a couple of times: it belongs above 0x08004000 and it
should move. The bootstub only jumps there after the RSA signature checks out, so a PC in range
also tells you the signing worked.
Once the bootloader is on the board you are done with SWD. Updates go over CAN from the comma device with the car in accessory so the filter is powered. Stop pandad first.
scripts/enter_canloader.py build/CanFilter.bin.signed
scripts/get_state.py # live state, timeouts, MODE/FILTERING/RADAR_ENGAGED
scripts/set_features.py PASSTHRU 1 # or NONE
PASSTHRU is the only feature bit that means anything. Set it and the board is a wire that
still accepts firmware updates. Default is 0.
Default is passthrough. Every frame forwarded verbatim both directions, stock MRCC works, and the only thing the board puts on the bus is its own status beacon.
openpilot holds it in op-long mode with FILTER_CTRL keepalives (0x2A0, subcommand 0x2e) at
2 Hz. Five seconds without one and it falls back to passthrough, so a crashed, killed or
unplugged openpilot restores the stock car by itself.
In mode, CRZ_BTNS (0x09d) going to the radar is rewritten to the idle button pattern with the
counter preserved. The radar therefore never sees a SET press and can never arm itself
internally. This is the invariant the whole design rests on: every fallback path has to end with
a standby radar, because a radar that armed itself while the filter was passive would take over
the PCM the moment the filter released. Mode entry is refused outright while the radar reports
an active stock cruise engagement.
Filtering starts on the FILTER bit, or on the first 0x21b or 0x21c seen on the car side, since
only openpilot puts those IDs there. While filtering, the radar's copies are dropped. Stop
injecting and it releases after 250 ms, which is twelve missed frames at 50 Hz; FILTER=0
releases immediately. Either way the PCM sees exactly one cruise master.
openpilot's 0x21b and 0x21c are never forwarded to the radar. The radar transmits those IDs itself and a second transmitter on the same ID means bit errors.
0x2A8, 5 Hz, 10 Hz for the first five seconds after boot so openpilot's fingerprint window catches it. Byte 7:
bits 0-4 op_long_timeout / 10
bit 5 FILTERING
bit 6 MODE
bit 7 RADAR_ENGAGED
Bytes 0-6 carry crash state, both CAN peripheral states, error and overflow counters, and uptime. A board in permanent passthrough goes silent instead, which is how openpilot knows to fall back to the UDS teardown.
get_state.py pulls the full state over ISO-TP, including crash state and the mode timeout.
- Independent watchdog, roughly 250 ms. A wedged filter cuts the radar off, so it reboots fast.
- Crash counter in reserved SRAM. Ten crashes and the board latches into permanent passthrough and stays there.
- Any HAL error resets the part.
PASSTHRUfeature bit in flash for a permanent wire.- Bootloader, softloader and reset commands are refused while the car is moving or while filtering. The bootloader does no forwarding, so entering it at speed would cut the radar off until you reflashed it in a parking lot.
There is no bypass relay. A dead or unpowered board severs the radar from the car. The feed is ignition-switched so the practical failure is a warning light on a car that will not do MRCC, not a control fault in traffic, but plan your install around it.
Two USB-CAN adapters, one playing radar on CAN2, one playing car on CAN1. Do this before the
thing goes anywhere near a vehicle. Full plan in DESIGN.md. The parts you cannot skip:
- Replay real bus traffic and confirm byte-identical passthrough both directions.
- Hold mode with 2 Hz keepalives, inject 50 Hz 0x21b/0x21c on the car side. Radar-side copies
stop inside one frame, resume within 250 ms of the injection stopping, immediately on
FILTER=0. - Send
CRZ_BTNSwith SET pressed and confirm what reaches the radar carries the idle pattern with a live counter, at the original rate. This is the safety invariant. Verify it. - Stop the keepalives and confirm passthrough returns inside 5 seconds.
- Replay radar frames showing an active cruise engagement and confirm mode entry is refused.
- Hang a
DEBUGbuild and confirm the watchdog puts it back. Force crashes ten times and confirm it latches into permanent passthrough.
500 kbps HS-CAN, roughly 115 IDs. Prescaler 9, BS1 6, BS2 1 off the 36 MHz APB1 gives an 87.5% sample point. 0x21b and 0x21c both run at 50 Hz.
The control and status IDs (0x2A0 magic, 0x2A1/0x2A9 ISO-TP, 0x2A8 state) collide with nothing on the Mazda bus.
CanFilter.ioc still claims 1 Mbps. It lies, main.c sets the timing. The one thing worth
reading out of that file is the CAN2 pin assignment, PB5 for RX and PB6 for TX.
The board mounts near the front emblem or the radar connector. It stays cool but it sees road spray and vibration, so aim for IP67. There is no factory case.
Conformal coat plus adhesive-lined heat shrink suits an inline device with a pigtail out each end. A gasketed box with cable glands is the option you can open again later.
| Part | Use | ASIN |
|---|---|---|
| MILAPEAK 1.5in (38mm) ID 3:1 dual-wall adhesive tube | wrap the board | B07VLDW14Z |
| MILAPEAK 130pc 3:1 dual-wall kit (3/32in to 1/2in) | seal the pigtail wires | B071H5XC7C |
| MILAPEAK 36pc large adhesive assortment | one box covering both | B07G9H8Y6T |
| MG Chemicals 419D acrylic conformal coat, aerosol | moisture barrier on the PCB | B06XWGCKX5 |
Coat the board whatever shell you use. The coating handles the condensation the shell will not keep out. Use acrylic, not silicone: you can solder through acrylic and strip it later, which matters the first time you need to get back in. MG 422B is silicone and does not come off cleanly.
A 38mm ID tube lays flat to about 60mm, which covers a 40mm board with margin, and the adhesive flows around the wire jackets. Measure your actual board and check clearance at the mount before you commit to a tube size. Strain-relieve the pigtails where they leave the seal. Vibration at the front of the car is what breaks solder joints.
MIT, see LICENSE. Original canfilter code copyright Smartype. Fork changes copyright
zoompilot. Upstream README kept as UPSTREAM_README.md; its wiring photos are the Toyota
8-pin connector and do not apply here.