Use custom target (blackpill) with CLI2

Hello.
I’m new in MbedOs programming and have a problem with creating cmake project for custom target.
What i did:
Created new project with

mbed-tools new test

command

then I imported custom target from https://os.mbed.com/users/hudakz/code/BLACKPILL_Custom_Target/ and copy it to project root folder.

after this i tryed to compile:
mbed-tools compile -m BLACKPILL_F411CE -t GCC_ARM

project configuration goes ok, but when compiling starts i have errors like theese:

/home/me/projects/mbed/test/mbed-os/cmsis/device/rtos/include/mbed_rtx_conf.h:26:10: fatal error: mbed_rtx.h: No such file or directory
   26 | #include "mbed_rtx.h"
      |          ^~~~~~~~~~~~
compilation terminated.

what is wrong in my way?)

Hello Pavel,

I’m sorry but neither the BLACKPILL_F401CC nor the BLACKPILL_F411CE custom targets support CLI2. However, CLI1 should work.

Edit1: Try to add a CMakeLists.txt file with the following content to the root directory of your program:

add_subdirectory(BLACKPILL_F411CE)

Edit2:

then I imported custom target from BLACKPILL_Custom_Target and copy it to project root folder. After this i tried to compile.

That’s just the first step before compilation!

  • Open the BLACKPILL_Custom_Target folder and according to you board drag&drop the TARGET_BLACKPILL_F401CC or the TARGET_BLACKPILL_F411CE folder and the custom_targets.json file one by one to the root folder of your program

  • Delete the BLACKPILL_Custom_Target folder from your project.

(As described at https://os.mbed.com/users/hudakz/code/BLACKPILL_Custom_Target/)

Hello.
I resolved this issue. Soon I’ll try to make an article how to use CLI2 with custom targets, for example you blackpill targets.
Also i found a problem in TARGET_BLACKPILL_F411CE. If you check end of flash_data.h, you can find:

/* Base address of the Flash sectors Bank 1 */
#define ADDR_FLASH_SECTOR_0     ((uint32_t)0x08000000) /* Base @ of Sector 0, 16 Kbytes */
#define ADDR_FLASH_SECTOR_1     ((uint32_t)0x08004000) /* Base @ of Sector 1, 16 Kbytes */
#define ADDR_FLASH_SECTOR_2     ((uint32_t)0x08008000) /* Base @ of Sector 2, 16 Kbytes */
#define ADDR_FLASH_SECTOR_3     ((uint32_t)0x0800C000) /* Base @ of Sector 3, 16 Kbytes */
#define ADDR_FLASH_SECTOR_4     ((uint32_t)0x08010000) /* Base @ of Sector 4, 64 Kbytes */
#define ADDR_FLASH_SECTOR_5     ((uint32_t)0x08020000) /* Base @ of Sector 5, 128 Kbytes */
#define ADDR_FLASH_SECTOR_5     ((uint32_t)0x08040000) /* Base @ of Sector 6, 128 Kbytes */
#define ADDR_FLASH_SECTOR_5     ((uint32_t)0x08060000) /* Base @ of Sector 7, 128 Kbytes */

ADDR_FLASH_SECTOR_5 is defined for 3 times and there are error messages while compiling project.

I fixed it to

#define ADDR_FLASH_SECTOR_5     ((uint32_t)0x08020000) /* Base @ of Sector 5, 128 Kbytes */
#define ADDR_FLASH_SECTOR_6     ((uint32_t)0x08040000) /* Base @ of Sector 6, 128 Kbytes */
#define ADDR_FLASH_SECTOR_7     ((uint32_t)0x08060000) /* Base @ of Sector 7, 128 Kbytes */

and everything goes ok!)

Hello,

I appreciate your valuable feedback!

  • A repository of STM32 custom targets is available at GitHub - ARMmbed/stm32customtargets: Enable the support of your custom boards in mbed-os 6. Majority of them already support CLI2/CMake. But to be honest with you I’m not a big fan of the CMake. However, I’m certain that a guide “How to add a CLI2 support for custom targets” will be a big help for the Mbed community.

  • What concerns the flash_data.h you are right. The file distributed by STMicroelectronics for the TARGET_STM32F411xE is correct and I have no idea when and why I changed that :frowning:

BlackPill STM32F411CE — Mbed Studio Setup Guide

Board: WeAct STM32F411CE BlackPill v3.1
Framework: Mbed OS 5 (bare-metal, no RTOS)
Toolchain: ARMC6 (Arm Compiler 6, included in Mbed Studio)
Status: :white_check_mark: Confirmed working — USB HID keyboard :white_check_mark: USB Serial CDC :white_check_mark: Clock 96 MHz :white_check_mark:


Overview

The WeAct BlackPill is not a supported Mbed target. It is made to work by telling Mbed Studio it is a NUCLEO-F411RE (same STM32F411CE chip) and providing two project-level overrides:

  1. A custom_targets.json that names the board and sets the right macros

  2. A TARGET_BLACKPILL_F411CE/system_clock.c that configures the 25 MHz HSE crystal and derives 96 MHz SYSCLK + 48 MHz USB clock correctly

Two additional library patches are required for USB to enumerate:

  1. USBKeyboard.h — adds a connect_blocking parameter

  2. USBHAL_STM32F4.cpp — disables the VBUS comparator (NOVBUSSENS)

Nothing inside mbed-os/ itself needs changing (the USB patches are in the standalone USBDevice library, not in mbed-os).


Project Structure

Every BlackPill project must have this layout:

MyProject/
├── main.cpp
├── custom_targets.json                    ← tells Mbed Studio about the board
├── mbed_app.json                          ← sets clock macros for this project
├── TARGET_BLACKPILL_F411CE/
│   └── system_clock.c                     ← 25 MHz HSE + 96/48 MHz PLL config
├── USBDevice/                             ← standalone USB library (if using USB)
│   └── USBHID/
│       └── USBKeyboard.h                  ← patched (connect_blocking)
│   └── targets/TARGET_STM/
│       └── USBHAL_STM32F4.cpp             ← patched (NOVBUSSENS)
└── mbed-os/                               ← unmodified


File 1 — custom_targets.json

Place in the project root (same level as main.cpp).

{
    "BLACKPILL_F411CE": {
        "inherits": ["NUCLEO_F411RE"],
        "macros_add": [
            "HSE_VALUE=25000000",
            "CLOCK_SOURCE_USB=1",
            "BLACKPILL"
        ],
        "overrides": {
            "clock_source": "USE_PLL_HSE_XTAL|USE_PLL_HSI"
        },
        "device_has_add": ["USBDEVICE"],
        "release_versions": ["5"]
    }
}

What each field does:

Field Purpose
inherits: NUCLEO_F411RE Reuses the F411RE pin map, linker script, startup files — the chips are identical
HSE_VALUE=25000000 Tells the clock driver the crystal is 25 MHz (BlackPill), not 8 MHz (Nucleo)
CLOCK_SOURCE_USB=1 Selects the PLL configuration that produces exactly 48 MHz for USB
BLACKPILL Guards platform-specific code in common.h, QS_DRO_KBD.h, etc.
clock_source override USE_PLL_HSE_XTAL = use crystal. USE_PLL_HSI = fallback to internal RC if crystal fails
device_has_add: USBDEVICE Enables USB peripheral support in the build

File 2 — mbed_app.json

Place in the project root.

{
    "config": {},
    "target_overrides": {
        "*": {
            "platform.stdio-baud-rate": 115200
        },
        "NUCLEO_F411RE": {
            "target.clock_source": "USE_PLL_HSE_XTAL|USE_PLL_HSI",
            "target.macros_add": [
                "HSE_VALUE=25000000",
                "CLOCK_SOURCE_USB=1",
                "BLACKPILL"
            ]
        }
    }
}

Why this is needed in addition to custom_targets.json:
custom_targets.json is read by Mbed Studio’s target selector. mbed_app.json is read by the compiler. Both must agree on HSE_VALUE or clock setup silently uses the wrong value.

The NUCLEO_F411RE key applies because custom_targets.json declares the board as inheriting from NUCLEO_F411RE — the build system resolves overrides through the inheritance chain.


File 3 — TARGET_BLACKPILL_F411CE/system_clock.c

Why a separate folder?

Mbed OS 5 has a built-in system_clock.c for the NUCLEO-F411RE target located deep inside mbed-os/. That file assumes an 8 MHz HSE crystal — wrong for the BlackPill’s 25 MHz crystal. Editing the file inside mbed-os/ would work but is fragile: it gets overwritten if mbed-os is updated, and it breaks every other project that uses NUCLEO-F411RE correctly.

Mbed OS provides a clean override mechanism: if a folder named TARGET_<boardname>/ exists in the project root, any file inside it silently replaces the matching file from mbed-os for that build only. The mbed-os original is untouched. Other projects are unaffected.

The folder name must exactly match the active target

The folder must be named TARGET_BLACKPILL_F411CE — exactly as defined in custom_targets.json. Case sensitive. No spaces.

MyProject/
├── custom_targets.json          ← defines "BLACKPILL_F411CE"
├── TARGET_BLACKPILL_F411CE/     ← folder name = "TARGET_" + board name
│   └── system_clock.c           ← overrides mbed-os/targets/.../system_clock.c
└── mbed-os/                     ← unmodified

If the folder is named anything else (TARGET_NUCLEO_F411RE/, blackpill_clocks/, etc.) Mbed OS ignores it and uses the wrong 8 MHz file. The symptom is the firmware hanging silently before main() — HSE never locks, HAL_RCC_OscConfig() blocks forever.

What the file does

Input:  HSE = 25 MHz crystal (BlackPill)
PLLM  = 25   → VCO input  = 1 MHz
PLLN  = 192  → VCO output = 192 MHz
PLLP  = /2   → SYSCLK     = 96 MHz
PLLQ  = /4   → USB clock  = 48 MHz  ← exactly required by USB spec

Mode SYSCLK AHBCLK USB clock
HSE + USB (CLOCK_SOURCE_USB=1) 96 MHz 96 MHz 48 MHz :white_check_mark:
HSE, no USB 100 MHz 100 MHz
HSI fallback + USB 96 MHz 96 MHz 48 MHz :white_check_mark:
HSI fallback, no USB 100 MHz 100 MHz

The HSI fallback path (USE_PLL_HSI in custom_targets.json) means if the 25 MHz crystal fails to start, the firmware falls back to the internal 16 MHz RC oscillator, still reaches main(), and the LED blinks fast — you know the board is alive even though USB won’t work on HSI.

The file is based on hudakz BLACKPILL_Custom_Target (rev 6, Oct 2021) with one correction: HAL_RCC_ClockConfig() was missing in earlier revisions, causing AHB/APB dividers to remain at reset values.

See system_clock.c in the outputs folder for the full file.


COM Port Setup — Two Ports, Two Purposes

This project uses two separate COM ports simultaneously. They are completely independent and both must be open at the same time during development.

Port Source Purpose Always active?
COM6 (ST-Link) USART2 on PA_2/PA_3, via ST-Link USB Debug printf output — boot messages, state changes, error logging Yes — active from first line of main(), regardless of USB state
COM3 (USB-C) STM32 OTG_FS USB peripheral USBSerial CDC virtual COM port — application data Only when USB_MODE_SERIAL is flashed and a terminal opens it

The port numbers (COM3, COM6) are assigned by Windows and will differ on each PC. Find your actual numbers in Device Manager → Ports (COM & LPT):

  • STMicroelectronics STLink Virtual COM Port = your ST-Link debug port

  • USB Serial Device = your BlackPill USB-C serial port

Setting up the ST-Link debug port (COM6 equivalent)

This port carries all dbg.printf() output and is essential for diagnosing USB enumeration problems. It is active from the very first line of main() regardless of USB state — if USB is broken, this port tells you why.

  1. Plug the ST-Link into the PC via USB

  2. Open Device Manager → Ports (COM & LPT) — note the STLink Virtual COM Port number

  3. Open TeraTerm → Setup → Serial Port:

    • Port: your ST-Link COM number

    • Speed: 115200

    • Data: 8 bit, Parity: none, Stop: 1 bit

    • Flow control: None

  4. Click OK — power on or reset the BlackPill, boot messages appear immediately

Setting up the USB-C serial port (COM3 equivalent)

Only needed when USB_MODE_SERIAL is flashed.

  1. Plug the USB-C cable from the BlackPill directly into the PC

  2. Windows installs usbser.sys automatically — USB Serial Device (COMx) appears in Device Manager within a few seconds

  3. Open a second TeraTerm window → Setup → Serial Port:

    • Port: the USB Serial Device COM number

    • Speed: 9600 (any value — CDC ignores baud rate entirely)

    • Flow control: DTR/DSRcritical — without this connected() stays false and the firmware never sends output

  4. Text appears as soon as DTR is asserted by TeraTerm on open

Why two TeraTerm windows at the same time?

The ST-Link port shows what the firmware is doing before USB enumerates — exactly when enumeration problems occur. You cannot diagnose USB issues with only the USB-C port open because if USB is broken, that port shows nothing. Always have both open during development.


How to Select the Board in Mbed Studio

After placing custom_targets.json in the project root:

  1. In the bottom toolbar, click the chip icon (next to the target name).
    This opens “Manage custom targets”.

  2. In the “USB device” dropdown, select your ST-Link programmer (e.g. STM32 STLink).

  3. In the “Build target” dropdown, scroll down and select
    BLACKPILL_F411CE — it appears because custom_targets.json is present.

  4. Click Save All.

  5. Build (hammer icon). Flash (play icon or drag .bin to NUCLEO virtual disk).

Note: The ST-Link connects to the BlackPill via SWD (4 wires: SWDIO, SWDCLK, GND, 3.3V). The BlackPill’s own USB-C port is separate — it is the USB HID device port, not the programming port.


USB Library Patches

These patches apply to the standalone USBDevice library (c:\mbed_DesktopStudio\USBKeyboard\USBDevice\), not to mbed-os itself.

Patch 1 — USBDevice/USBHID/USBKeyboard.h

Problem: The original constructor calls connect() unconditionally, blocking forever if the USB host is not present at boot time.

Fix: Add connect_blocking as a 4th parameter defaulting to true (backwards compatible). Pass false for non-blocking hot-plug operation.

// BEFORE (always blocks):
USBKeyboard(uint16_t vendor_id = 0x1235,
            uint16_t product_id = 0x0050,
            uint16_t product_release = 0x0001):
        USBHID(0, 0, vendor_id, product_id, product_release, false) {
    lock_status = 0;
    connect();   // ← always blocks until host enumerates
};

// AFTER (caller chooses):
USBKeyboard(uint16_t vendor_id = 0x1235,
            uint16_t product_id = 0x0050,
            uint16_t product_release = 0x0001,
            bool connect_blocking = true):          // ← new parameter
        USBHID(0, 0, vendor_id, product_id, product_release, false) {
    lock_status = 0;
    if (connect_blocking) { connect(); }            // ← guarded
};

Patch 2 — USBDevice/targets/TARGET_STM/USBHAL_STM32F4.cpp

Problem: GCCFG bit 19 (VBUSBSEN) enables the OTG_FS internal VBUS comparator. The comparator requires >4.75V on PA_9 to assert VBUS valid. The WeAct BlackPill v3.1 has a 5.1kΩ/5.1kΩ voltage divider on PA_9, presenting only ~2.5V. The comparator never fires → D+ never asserted → host never sees the device → configured() stays false forever.

Fix: Replace bit 19 with bit 21 (NOVBUSSENS) — bypasses the comparator entirely. Also change PA_9’s pin_function from OTG_FS_VBUS to plain GPIO so the OTG peripheral does not own the pin.

// BEFORE — blocks enumeration on BlackPill:
pin_function(PA_9, STM_PIN_DATA(STM_MODE_INPUT, GPIO_PULLDOWN, GPIO_AF10_OTG_FS));
// ...
OTG_FS->GREGS.GCCFG |= (1 << 19) | // Enable VBUS sensing  ← WRONG for BlackPill
                       (1 << 16);   // Power Up

// AFTER — enumerates correctly:
pin_function(PA_9, STM_PIN_DATA(STM_MODE_INPUT, GPIO_PULLDOWN, 0));  // GPIO only
// ...
OTG_FS->GREGS.GCCFG |= (1 << 21) | // NOVBUSSENS — bypass comparator  ← FIXED
                       (1 << 16);   // PWRDWN — power up PHY

Why PA_9 still works as VBUS indicator:
The firmware reads PA_9 as a plain DigitalIn for software VBUS detection (DigitalIn vbus(PA_9, PullDown)). The divider presents a valid logic HIGH when USB-C is plugged in. Only the OTG hardware comparator (which needs full 5V) is bypassed. Software VBUS sensing continues to work normally.


Confirmed Working — Serial Output

With all four changes in place:

=== BOOT ===
SystemCoreClock = 96000000 Hz
Creating USBKeyboard(VID, PID, REL)...
Object created. Calling connect(false)...
connect(false) returned. Entering loop...
[USB] Configured — sending test keystroke
Hello!Hello!Hello!Hello!Hello!Hello!...

Clock confirmed: 96 MHz (HSE 25 MHz, PLL ×192 ÷2).
USB confirmed: HID keyboard device enumerated, keystrokes received on host PC.


Master File List — What to Change and What to Leave Alone

This is the complete and definitive list of every file involved in the BlackPill port. Nothing outside this list needs touching.

Files you CREATE (do not exist yet — add to project root)

File Location Change
custom_targets.json MyProject/ Create — defines BLACKPILL_F411CE target inheriting from NUCLEO_F411RE
mbed_app.json MyProject/ Create — sets HSE_VALUE=25000000, CLOCK_SOURCE_USB=1, BLACKPILL for compiler
TARGET_BLACKPILL_F411CE/system_clock.c MyProject/TARGET_BLACKPILL_F411CE/ Create folder + file — 25 MHz HSE → 96/48 MHz PLL; overrides mbed-os built-in

Files you PATCH (exist in USBDevice library — modify in place)

File Location Change Why
USBKeyboard.h USBDevice/USBHID/ Add bool connect_blocking = true as 4th constructor parameter; guard connect() call Original always blocks in constructor — device hangs if USB-C not plugged in at boot
USBHAL_STM32F4.cpp USBDevice/targets/TARGET_STM/ Line ~59: PA_9 AF from GPIO_AF10_OTG_FS0. Line ~105: GCCFG bit 19 → bit 21 (NOVBUSSENS) BlackPill 5.1k/5.1k VBUS divider gives 2.5V; OTG comparator needs >4.75V; D+ never asserts without this fix

Files in mbed-os you DO NOT touch

File Location Why untouched
system_clock.c (original) mbed-os/targets/TARGET_STM/TARGET_STM32F4/TARGET_STM32F411xE/ Overridden by project-local TARGET_BLACKPILL_F411CE/system_clock.c — original never compiled
USBPhy_STM32.cpp mbed-os/targets/TARGET_STM/TARGET_STM32/ This is the mbed-os 5.14 USB stack — not used; this project uses the standalone USBDevice library instead
PinNames.h mbed-os/targets/.../NUCLEO_F411RE/ BlackPill pin names are compatible with NUCLEO_F411RE — inherited as-is
All other mbed-os files mbed-os/ Unmodified — mbed-os is used as a library only

Files in USBDevice you DO NOT touch

File Why untouched
USBDevice.cpp / USBDevice.h connect(bool blocking) already exists and works correctly
USBHAL.h Interface header — no changes needed
USBHAL_STM_144_64pins.h Never compiled — only included when USB_STM_HAL is defined, which this project does not define
USBCDC.cpp / USBCDC.h Works correctly as-is — connect_blocking already present
USBSerial.cpp / USBSerial.h Works correctly as-is — connect_blocking already present
USBAudio/, USBMIDI/, USBMSD/ Not used by this project

Summary count

Category Count Files
Files to create 3 custom_targets.json, mbed_app.json, system_clock.c
Files to patch 2 USBKeyboard.h, USBHAL_STM32F4.cpp
mbed-os files touched 0
Total changes 5

Troubleshooting

Symptom Cause Fix
No blink, no serial output system_clock.c not found — hanging in HSE startup Verify TARGET_BLACKPILL_F411CE/system_clock.c exists in project root
Serial output but wrong clock speed mbed_app.json missing or wrong HSE_VALUE Check HSE_VALUE=25000000 in mbed_app.json
Constructor hangs (“Calling USBKeyboard…” never appears) Old USBKeyboard.h without connect_blocking Apply Patch 1 above
“Not configured yet” forever VBUS comparator blocking (VBUSBSEN bit 19) Apply Patch 2 above
BLACKPILL_F411CE not in target dropdown custom_targets.json not in project root Move it to root, restart Mbed Studio, use chip icon
Build errors about duplicate symbols USBHAL_STM_144_64pins.h included twice Ensure USB_STM_HAL is NOT defined — this project must use the USBHAL_STM32F4.cpp path


USBSerial — CDC Virtual COM Port

USBSerial is in the same standalone USBDevice library and works with the same NOVBUSSENS patch already applied. No additional library patches are needed.

What you get

Plugging the USB-C connector into a PC creates a virtual COM port (COMx on Windows, /dev/ttyACMx on Linux). Any terminal application can open it. The ST-Link UART on PA_2/PA_3 continues to work simultaneously as a separate debug channel.

Windows driver

Windows 10/11 installs the CDC driver automatically (Microsoft usbser.sys). No INF file required. The device appears as “USB Serial Device (COMx)” in Device Manager under “Ports (COM & LPT)”.

Critical rule — always gate writes on connected()

USBSerial::writeBlock(), printf(), and _putc() are blocking calls. They wait forever for the host to ACK if nobody is draining the endpoint. If you call any write function when no terminal is open, the firmware freezes.

configured() = true   → COM port exists in Device Manager, safe for reads
connected()  = true   → terminal has asserted DTR, safe to write

Always gate every write on connected(), never on configured() alone.

if (usbser->connected())          // ← DTR asserted — safe to write
{
    usbser->printf("data\r\n");
}

connected() requires DTR

connected() goes true only when the terminal asserts the DTR control line. Different terminals handle this differently:

Terminal DTR behaviour What to do
TeraTerm DTR off by default Setup → Serial Port → Flow Control: DTR/DSR
PuTTY DTR on automatically Open port — works immediately
Arduino Serial Monitor DTR on automatically Open port — works immediately

Constructor — do NOT call connect() again

USBCDC (base of USBSerial) already calls USBDevice::connect() inside its constructor. Calling connect() a second time resets the USB state machine mid-enumeration and prevents configured() from ever becoming true.

// CORRECT:
USBSerial *usbser = new USBSerial(0x1f00, 0x2012, 0x0001, false);
// ← do NOT call usbser->connect() here

// WRONG — resets USB mid-enumeration, configured() stays 0:
USBSerial *usbser = new USBSerial(0x1f00, 0x2012, 0x0001, false);
usbser->connect(false);  // ← kills enumeration

This is the opposite of USBKeyboard (which has our patch removing connect from the constructor — so USBKeyboard requires an explicit connect() call).

Class Constructor calls connect()? Explicit connect() needed?
USBSerial / USBCDC Yes (always) No — never call it
USBKeyboard (patched) No (patched out) Yes — call it once

Minimal working pattern

#include "mbed.h"
#include "USBSerial.h"

USBSerial *usbser = new USBSerial(0x1f00, 0x2012, 0x0001, false);
// No connect() call here

while (true)
{
    if (usbser->connected())          // terminal has DTR — safe to write
    {
        usbser->printf("hello\r\n");
    }
    wait_us(1000000);
}

Confirmed working output

On TeraTerm (COM3, Flow Control: DTR/DSR):

=== BlackPill USBSerial ===
Clock = 96000000 Hz
Type anything to echo.
[106 s] alive
[107 s] alive

On ST-Link UART (COM6):

[USB] configured -> 1
[USB] connected(DTR) -> 1


Choosing Between USBSerial and USBKeyboard

The USB peripheral can only be one device at a time. You cannot run both simultaneously — switch by reflashing.

Use case Choose
DRO sending keystrokes to CAM software USBKeyboard
Debug / position data logging to PC terminal USBSerial
Bench development USBSerial (easier to read)
Production deployment USBKeyboard


HID Keyboard — Keystroke Timing

USBKeyboard::printf() sends characters in a tight loop with no gap. The HID polling interval is 8 ms (bInterval=8 in the endpoint descriptor). Sending faster than the poll interval causes the host to see overlapping key-down events before key-up is processed, producing garbled output like GrHelelo instead of Hello.

Always insert an 8 ms delay between keystrokes:

// WRONG — garbles on fast hosts:
kbd->printf("Hello!\n");

// CORRECT — clean output on all hosts:
const char *msg = "Hello!\n";
for (const char *p = msg; *p; p++)
{
    kbd->putc(*p);
    wait_us(8000);   // 8 ms = one HID report interval
}

This applies to every string send in the real DRO — position digits, units, newlines. 8 ms per character is imperceptible to a human but essential for reliable HID delivery.


Multiple Source Files — Linker Error

Mbed OS 5 compiles every .cpp file found anywhere in the project tree. If more than one file contains main(), the linker dies with:

L6200E: Symbol $Super$$main multiply defined

Only one .cpp file in the entire project tree may contain main(). Keep old test files outside the project folder or delete them. Never leave main_test.cpp, main_old.cpp, etc. inside the project directory.


Switching Between USB Modes

The main_usb_dual.cpp file selects the USB mode at compile time:

#define USB_MODE_SERIAL      // USB-C = virtual COM port (CDC)
// #define USB_MODE_KEYBOARD // USB-C = HID keyboard

Change the define, rebuild, reflash. Both modes confirmed working:

Mode Host sees Open with
USB_MODE_SERIAL COMx / ttyACMx TeraTerm (Flow: DTR/DSR), PuTTY, Arduino Monitor
USB_MODE_KEYBOARD HID keyboard Any text editor — keystrokes appear at cursor

Document created: 2026-03-08. Confirmed working on WeAct BlackPill v3.1, Mbed Studio 1.4, ARMC6, Mbed OS 5.14, standalone USBDevice library.