Skip to content

About

Cheap CAN <-> USB Transceiver

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

CanUsbTransceiver

CI License: MIT MCU: STM32G474RET6 RTOS: FreeRTOS

A CAN ↔ USB transceiver built from a NUCLEO-G474RE board, a Waveshare SN65HVD230 CAN transceiver and any FT232 USB↔UART adapter. The firmware bridges the CAN bus onto a serial link using a small framed binary protocol with a CRC-16 per frame.

The bus runs at a hardcoded 500 kbit/s; the serial link runs at 921600 baud, 8N1. Changing the bitrate means editing MX_FDCAN1_Init() (see Changing the CAN bitrate).

Desktop application: https://github.com/kurta999/CustomKeyboard (this is the project formerly known as WindowsAddon — the repository was renamed, so any older links pointing at WindowsAddon should be updated).

Just want a CAN adapter and not a project? Buy a Lawicel CANUSB instead. This repository exists because building one is more fun.


Table of contents


Features

  • Bidirectional CAN ↔ UART bridge, 11-bit and 29-bit identifiers.
  • CRC-16 protected framing in both directions, so a corrupted serial byte is dropped rather than injected onto the bus.
  • Idle-gap framing on the serial link — no escaping, no start/stop bytes.
  • FreeRTOS (CMSIS-RTOS v1) with a single worker task; the CAN RX path runs straight from the FDCAN interrupt for low latency.
  • Independent watchdog (2 s) recovers the bridge from any hang.
  • Status LED (Nucleo LD2) toggles on every forwarded CAN frame; a rapid blink burst signals Error_Handler().
  • Builds both in STM32CubeIDE and from a plain CMake + arm-none-eabi-gcc command line, with CI building both configurations on every push.

Hardware

Part Notes
NUCLEO-G474RE STM32G474RET6, Cortex-M4F @ 80 MHz, on-board ST-LINK
Waveshare SN65HVD230 CAN board 3.3 V CAN transceiver, has a 120 Ω termination jumper
Any FT232 USB↔UART adapter Must be switched to 3.3 V logic levels
120 Ω termination One at each end of the bus

The MCU runs from the internal HSI16 oscillator through the PLL (M=1, N=10, R=2) at 80 MHz — no external crystal is required.

Wiring

STM32 pin Function Connect to
PA11 FDCAN1_RX (AF9) SN65HVD230 RXD
PA12 FDCAN1_TX (AF9) SN65HVD230 TXD
PC10 UART4_TX (AF5) FT232 RXD
PC11 UART4_RX (AF5) FT232 TXD
PA5 LED_OK output Nucleo LD2 (on board)
PC13 USER_Button input Nucleo B1 (on board)
3V3 / GND Power SN65HVD230 and FT232

On the Nucleo board PA12 is shared with the ST morpho connector. Make sure nothing else drives PA11/PA12, and keep the FT232 at 3.3 V logic — a 5 V adapter will damage the MCU pins.

The CAN side is a normal two-wire differential bus: CANH ↔ CANH, CANL ↔ CANL, with 120 Ω across the pair at both physical ends.

Wire protocol

Every message in either direction is the same fixed 19-byte packed structure, transmitted little endian:

Offset Size Field Description
0 4 magic_number 0xAABBCCDD host → device, 0xAABBCCDE device → host
4 4 frame_id CAN identifier. > 0x7FF is sent as a 29-bit extended ID
8 1 data_len Payload length, 0–8
9 8 data Payload. Bytes beyond data_len are transmitted but ignored
17 2 crc CRC-16 over bytes 0–16

CRC-16 is the reflected CRC-16/IBM (MODBUS) variant: polynomial 0xA001, initial value 0xFFFF, no final XOR, computed over the first 17 bytes of the structure. See Core/Src/crc.c — the desktop application uses the identical routine.

Framing. There are no start or stop delimiters. Host → device frames are delimited by an idle gap on the line: TIM16 is reset on every received byte and fires after roughly 102 µs of silence (~9 byte times at 921600 baud), which tells the worker task that a complete frame has arrived. Send the 19 bytes of a frame back to back, then pause before the next one.

A frame is dropped silently when the magic number does not match, fewer than 19 bytes arrived, the CRC does not verify, or data_len exceeds 8.

How it works

   CAN bus ── SN65HVD230 ──> FDCAN1 RX FIFO0 (11-bit) / FIFO1 (29-bit)
                                    │  interrupt
                                    ▼
                       FDCAN_ForwardRxFrameToUart()
                       build 19-byte frame + CRC-16
                                    │
                                    ▼
   Host  <──  FT232  <── UART4 TX  (921600 8N1)


   Host  ──>  FT232  ──> UART4 RX interrupt ──> 512-byte circular buffer
                                    │  TIM16 idle-gap timeout (~102 µs)
                                    ▼
                          StartDefaultTask()
                     verify magic + length + CRC-16
                                    │
                                    ▼
              FDCAN1 TX FIFO queue ──> SN65HVD230 ──> CAN bus

CAN bit timing. The FDCAN kernel clock is PCLK1 = 80 MHz. The nominal phase uses prescaler 1, tseg1 119, tseg2 40 → 160 time quanta → 500 kbit/s with a 75 % sample point. The controller is initialised in FDCAN_FRAME_FD_BRS mode so it can receive CAN FD frames, but everything it transmits is classic CAN with BRS off. Auto-retransmission is disabled: a frame that loses arbitration or is not acknowledged is not retried.

Filters. Standard IDs are routed to RX FIFO 0 and extended IDs to RX FIFO 1; the global filter is configured to accept non-matching frames as well, so the bridge is effectively promiscuous.

Watchdog. The IWDG is configured with prescaler 16 and reload 3999 against the ~32 kHz LSI, giving a 2 second timeout. It is refreshed once per millisecond from the worker task, and frozen while the debugger has the core halted.

Building the firmware

STM32CubeIDE

  1. File → Import → Existing Projects into Workspace, select this repository.
  2. Pick the Debug or Release configuration and build.

The CubeMX configuration lives in CAN.ioc. Regenerating code is safe as long as edits stay inside the /* USER CODE BEGIN ... */ markers.

CMake (command line / CI)

Requires CMake ≥ 3.20, Ninja and the Arm GNU Toolchain (arm-none-eabi-gcc, 10 or newer; CI uses 13.2).

cmake -B build -G Ninja \
      -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi-gcc.cmake \
      -DCMAKE_BUILD_TYPE=Release
cmake --build build

This produces build/CanUsbTransceiver.elf, .hex, .bin and a .map, and prints the flash/RAM usage. The CMake project uses the same MCU flags, defines and linker script as the CubeIDE build — both build systems share the same sources, so you can use whichever you prefer.

If the toolchain is not on PATH:

cmake -B build -G Ninja \
      -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi-gcc.cmake \
      -DTOOLCHAIN_PREFIX=/opt/gcc-arm-none-eabi-13.2
CMake option Default Meaning
CMAKE_BUILD_TYPE Release Release = -Os, Debug = -Og -g3 + DEBUG define
ENABLE_DEBUG_PRINTF OFF Compile the DBG_PRINTF() traces in (see Configuration)

Continuous integration

  • .github/workflows/ci.yml builds Debug and Release on every push and pull request, uploads the binaries as artifacts and runs cppcheck over the application sources.

  • .github/workflows/release.yml builds a Release image, generates SHA256SUMS and publishes a GitHub Release whenever a v* tag is pushed:

    git tag v1.0.0 && git push origin v1.0.0

Flashing

With the on-board ST-LINK connected over USB:

# stlink-tools
st-flash --reset write build/CanUsbTransceiver.bin 0x08000000

# or the CMake convenience target
cmake --build build --target flash

# or STM32CubeProgrammer
STM32_Programmer_CLI -c port=SWD -w build/CanUsbTransceiver.hex -rst

Alternatively, drag CanUsbTransceiver.bin onto the NODE_G474RE mass-storage drive that the ST-LINK exposes.

Configuration

Everything tunable lives at the top of Core/Src/main.c:

Define Default Meaning
FDCAN_RX_DATA_SIZE 64 RX payload buffer; must stay ≥ the largest FD frame the controller may deliver
UART_RX_FRAME_MAX 64 Longest serial burst kept by the parser
UART_TX_TIMEOUT_MS 100 Blocking UART transmit timeout for one frame
FDCAN_TX_PENDING_TIMEOUT_MS 10 Upper bound on waiting for a CAN frame to leave the controller
MAGIC_HOST_TO_CAN / MAGIC_CAN_TO_HOST 0xAABBCCDD / 0xAABBCCDE Protocol frame markers
ENABLE_DEBUG_PRINTF unset Enables DBG_PRINTF() traces

DBG_PRINTF() is compiled out by default on purpose: __io_putchar() is not implemented on this board, and printf() would allocate newlib's stdout buffer from the heap and format on a 1.5 kB task stack — from interrupt context in some call sites. Provide an __io_putchar() first, then build with -DENABLE_DEBUG_PRINTF=ON.

Changing the CAN bitrate

Edit MX_FDCAN1_Init() in Core/Src/main.c. With a 80 MHz kernel clock:

bitrate = 80 MHz / (NominalPrescaler * (1 + NominalTimeSeg1 + NominalTimeSeg2))

The shipped values — prescaler 1, tseg1 119, tseg2 40 — give 500 kbit/s. For 250 kbit/s, double the prescaler to 2. Keep NominalSyncJumpWidth ≤ NominalTimeSeg2. The desktop application must use the same bitrate.

Changing the UART baudrate

Edit huart4.Init.BaudRate in MX_UART4_Init(). If you go much slower, also raise the TIM16 period in MX_TIM16_Init() so the idle-gap timeout stays longer than one byte time.

Project layout

├── CMakeLists.txt              CMake build (mirrors the CubeIDE settings)
├── cmake/arm-none-eabi-gcc.cmake   Toolchain file
├── CAN.ioc                     STM32CubeMX configuration
├── STM32G474RETX_FLASH.ld      Linker script (flash image)
├── STM32G474RETX_RAM.ld        Linker script (RAM image, unused by default)
├── Core/
│   ├── Inc/                    Application headers, FreeRTOSConfig.h, HAL conf
│   ├── Src/
│   │   ├── main.c              Peripheral init, CAN/UART bridging, worker task
│   │   ├── circbuff.c          Lock-free single-producer/consumer byte FIFO
│   │   ├── crc.c               CRC-16 (0xA001, init 0xFFFF)
│   │   ├── app_freertos.c      FreeRTOS hooks
│   │   └── stm32g4xx_*.c       CubeMX generated MSP / interrupt handlers
│   └── Startup/                Vector table and reset handler
├── Drivers/                    STM32G4 HAL + CMSIS (vendor code)
├── Middlewares/Third_Party/FreeRTOS/   FreeRTOS kernel (vendor code)
└── github_screens/             Photos of the assembled hardware

Known limitations

  • The CAN bitrate (500 kbit/s) and the UART baudrate (921600) are compile-time constants; there is no host command to change them at runtime.
  • Only classic CAN frames with up to 8 payload bytes are bridged. CAN FD frames longer than 8 bytes are received by the controller but reported to the host with data_len = 0.
  • Remote frames, error frames and bus-off state are not reported to the host.
  • Auto-retransmission is disabled, so a frame that loses arbitration is dropped rather than retried.
  • The serial link has no flow control; a host that stops reading will lose frames rather than back-pressure the bridge.

Troubleshooting

Symptom Likely cause
No CAN frames reach the host CANH/CANL swapped, missing 120 Ω termination, or a bitrate mismatch with the other node
LED blinks a fast burst, then the board resets Error_Handler() was reached; the watchdog then resets the MCU after 2 s
Host receives garbage FT232 not at 921600 8N1, or 5 V logic levels on the UART pins
Frames are rejected as bad CRC The host is not sending the 19 bytes back to back, so the idle-gap timeout split the frame
Nothing on the bus at all The SN65HVD230 RS pin must be low (high-speed mode), not left floating high

Contributing

Bug reports and pull requests are welcome — see CONTRIBUTING.md. Keep hand-written code inside the /* USER CODE BEGIN ... END */ markers so STM32CubeMX regeneration does not wipe it.

Screenshots

Everything

Everything

Nucleo

Nucleo

Waveshare CAN Transceiver

Waveshare CAN Transceiver

FT232

FT232

RS232

RS232

License

MIT — see LICENSE.md.

The Drivers/ and Middlewares/ directories contain STMicroelectronics HAL / CMSIS code and the FreeRTOS kernel, which carry their own licenses (see the LICENSE.txt files in those directories).

About

Cheap CAN <-> USB Transceiver

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages