There is a second way to use the CAN bus: in “single wire” mode (SW-CAN). But before I go into that, let me set up a dual-node bus, using standard MCP2551 transceivers for a “normal” CAN bus with CAN-HI and CAN-LO signals. Here is my little breadboard setup:

On the left: a HyTiny from Haoyu Electronics. It’s a nice alternative to the Blue Pill with a convenient 6-pin programming header on the end (power, SWD, and serial). For all intents and purposes, it’s the same as a Blue Pill - just smaller (and with fewer I/O pins).

On the right: a Nucleo-L432 board from STM. This is a different STM32 family (lower power, but also higher performance and Cortex M4 iso M3). Just to pick a different µC.

One reason for choosing different µCs, is that it lets me test and develop more CAN h/w drivers in JeeH, alongside the STM32F4xx. And it shows that CAN is not µC-specific.


This also lets me show how a PlatformIO project can be used for multiple configurations. So here we go again, with a new project folder and two files in it. First platformio.ini:

build_flags = -D STM32F1
platform = ststm32
board = bluepill_f103c8
framework = stm32cube
upload_protocol = blackmagic
upload_port = /dev/cu.usbmodemBDC8CDF1
monitor_speed = 115200
lib_deps = jeeh

build_flags = -D STM32L4
platform = ststm32
board = nucleo_l432kc
framework = stm32cube
upload_protocol = stlink
monitor_speed = 115200
lib_deps = jeeh

Unlike my previous example projects, this configuration defines two build “environments”. The other file is of course src/main.cpp, which contains the following demo code:

#include <jee.h>

#if STM32F1
PinA<1> led;
UartBufDev< PinA<9>, PinA<10> > console;
#elif STM32L4
PinB<3> led;
UartBufDev< PinA<2>, PinA<3> > console;

int printf(const char* fmt, ...) {
    va_list ap; va_start(ap, fmt); veprintf(console.putc, fmt, ap); va_end(ap);
    return 0;

CanDev can;

int main() {
    console.baud(115200, fullSpeedClock());


    uint32_t last = 0;
    while (true) {
        if (ticks / 500 != last) {
            last = ticks / 500;
#if STM32F1
            bool ok = can.transmit(0x654, "87654321", 8);
#elif STM32L4
            bool ok = can.transmit(0x321, "ABCDEFGH", 8);
            printf("T %d ok %d\n", ticks, ok);

        int len, id, dat[2];
        len = can.receive(&id, dat);
        if (len >= 0) {
            printf("R %d @%x #%d: %08x %08x\n", ticks, id, len, dat[0], dat[1]);

The basic logic is to send a CAN bus packet every 500 ms and to report each incoming packet. The code is slightly more involved because it adapts to two different µC boards. See the #ifdef STM32F1 and #elif STM32L4 conditionals.

Trying it out

Each board reports its activity on a different serial port. Here is the Nucleo output:

R 0 @654 #8: 35363738 31323334
R 412 @654 #8: 35363738 31323334
T 500 ok 1
R 913 @654 #8: 35363738 31323334
T 1000 ok 1
R 1414 @654 #8: 35363738 31323334
T 1500 ok 1
R 1915 @654 #8: 35363738 31323334
T 2000 ok 1
R 2416 @654 #8: 35363738 31323334
T 2500 ok 1
R 2917 @654 #8: 35363738 31323334

Both on-board LEDs will blink, toggled by messages sent from the other board.

Single Wire CAN

But as I mentioned earlier, there is another way to use the CAN bus. We can do away with the CAN bus driver chips and perform all data communication over a single signal wire. Note that this still needs a common ground level, as every electrical signal does.

The trick is possible because a CAN bus acts as one long “passive AND gate”. That’s what makes CAN tick, with all its address resolution, prioritisation, and its real-time guarantees. All we need, is that wired-OR property of the bus.

The idea is to add a pull-up and to put all the transmit pins in “open drain” mode. For µCs where that’s not possible, a diode can be added so each transmitter can only pull down.

Here’s the new – and fairly dramatically – simplified setup:

All the extra circuitry is gone. Just three connections left: the SW-CAN signal, and power (5V and GND, through two power rails). Also, there’s now a 1 kΩ pull-up to 5V (or 3.3V).

The one crucial change in the application code, is that we need to configure the transmit pins as open-drain outputs. Which is trivial, since JeeH provides an init option. Change:


… to this:


Sooo… does this work? Sure, the LEDs still blink as before. It’s still a bi-directional shared CAN bus, even though it’s now a single signal wire. Will it run as fast? Yes, 1 Mbps works fine (but probably only over short distances). Is it as robust? No, the voltage levels must stay within the permitted limits of the GPIO pins (which are 5V-tolerant, but that’s about it). There’s no driver chip to optimally condition the signal or protect the µC from any spikes.

And there you have it: with three wires, an “interconnect” can be created which distributes 5V power and communicates in a pub-sub fashion over a shared real-time bus.