Why Use the STM32 Arduino IDE Core?

If you have outgrown the 8-bit limitations of the Arduino Uno or the Wi-Fi-centric ESP32, the STM32 family offers a massive leap in raw compute, memory, and peripheral density. Using the official STM32Core package bridges the gap between the complex STM32CubeIDE HAL ecosystem and the accessible Arduino framework. You get 32-bit ARM Cortex-M performance, hardware floating-point units (FPU), and multiple I2C/SPI buses while keeping the familiar setup() and loop() structure.

Difficulty Rating: Intermediate | Time to Build: 45 minutes
Target Board Variant: WeAct Studio STM32F401CCU6 'Black Pill'. This code and wiring guide specifically target the F401 variant, not the older F103 'Blue Pill'.

Hardware Spec Sheet & Parts List

For new builds in 2026, skip the legacy STM32F103C8T6 'Blue Pill'. It lacks a hardware FPU, has limited flash (64KB), and requires an external CH340 serial adapter for USB communication. The STM32F401CCU6 'Black Pill' runs at 84MHz, includes a hardware FPU, boasts 256KB of flash, and features native USB pins routed directly to the Type-C connector.

ComponentExact Variant / ModelNotes & Pricing (Approx.)
MicrocontrollerWeAct Studio STM32F401CCU6 Core BoardEnsure it is the WeAct v3.1 layout. (~$6.50)
ProgrammerST-Link V2 (Genuine or high-quality clone)Required for SWD flashing and hardware debugging. (~$4.00)
SensorBosch BME280 Breakout (3.3V I2C)Must be 3.3V logic. Do not use 5V-tolerant BME280 modules. (~$5.00)
Wiring28 AWG silicone jumper wires, 4-pin SWD ribbonKeep SWD traces under 10cm to prevent signal reflection.

Pin Mapping & Wiring Guide

The STM32 Arduino IDE core maps physical pins to logical names. Unlike the Uno, hardware I2C1 on the Black Pill defaults to PB6 (SCL) and PB7 (SDA). Hardware Serial1 (UART1) defaults to PA9 (TX) and PA10 (RX).

Black Pill PinSTM32 NameFunctionConnect To
3V3VDD3.3V PowerST-Link 3.3V & BME280 VIN
GNDVSSGroundST-Link GND & BME280 GND
D21PB6I2C1 SCLBME280 SCL
D22PB7I2C1 SDABME280 SDA
D8PA9USART1 TXUSB-Serial RX (if not using ST-Link VCOM)
D2PA14SWCLKST-Link SWCLK
D4PA13SWDIOST-Link SWDIO

Numbered Wiring Steps

  1. Power the Target: Connect the ST-Link 3.3V pin to the Black Pill 3V3 pin. Never connect the ST-Link 5V pin to the 3V3 rail; the F401 I/O is not 5V tolerant and will instantly destroy the silicon.
  2. Wire SWD: Connect SWDIO to PA13 and SWCLK to PA14. Connect GND to GND.
  3. Wire I2C: Connect the BME280 SDA to PB7 and SCL to PB6. The Black Pill has internal pull-ups on the I2C lines, but adding external 4.7kΩ pull-ups to 3.3V is recommended for cable runs over 5cm.
  4. Verify Boot Jumpers: Ensure the BOOT0 jumper on the Black Pill is set to 0 (Flash boot) for normal operation, not 1 (System memory/DFU boot).

Complete Code: BME280 I2C with Hardware Serial & Error Handling

This sketch targets the WeAct STM32F401 Black Pill. It initializes hardware I2C1, reads the BME280, and outputs data via Hardware Serial1 (UART1). It includes explicit timeout handling to prevent the microcontroller from hanging if the serial monitor is disconnected or the I2C bus locks up.

#include 
#include 
#include 

// --- PIN DEFINITIONS (WeAct STM32F401 Black Pill) ---
#define I2C_SDA_PIN PB7
#define I2C_SCL_PIN PB6
#define SERIAL_TX_PIN PA9
#define SERIAL_RX_PIN PA10
#define STATUS_LED PC13 // Active LOW on Black Pill

// --- OBJECTS ---
Adafruit_BME280 bme;
TwoWire myWire(I2C_SDA_PIN, I2C_SCL_PIN); // Explicit I2C1 mapping

void setup() {
  pinMode(STATUS_LED, OUTPUT);
  digitalWrite(STATUS_LED, HIGH); // LED OFF (Active LOW)

  // Initialize Hardware Serial1 (UART1)
  Serial1.setRx(SERIAL_RX_PIN);
  Serial1.setTx(SERIAL_TX_PIN);
  Serial1.begin(115200);
  
  // Non-blocking serial wait with 3-second timeout
  unsigned long startMillis = millis();
  while (!Serial1 && (millis() - startMillis < 3000)) {
    delay(10);
  }
  
  Serial1.println("STM32F401 Black Pill Booting...");

  // Initialize I2C with explicit 400kHz Fast Mode
  myWire.setClock(400000);
  myWire.begin();

  // BME280 Initialization with error handling
  // 0x76 is the default I2C address for most Adafruit/generic BME280 modules
  if (!bme.begin(0x76, &myWire)) {
    Serial1.println("FATAL: Could not find a valid BME280 sensor on I2C1!");
    Serial1.println("Check PB6/PB7 wiring and 3.3V power.");
    // Blink LED rapidly to indicate hardware fault
    while (1) {
      digitalWrite(STATUS_LED, !digitalRead(STATUS_LED));
      delay(100);
    }
  }
  
  Serial1.println("BME280 initialized successfully.");
  bme.setSampling(Adafruit_BME280::MODE_NORMAL,
                  Adafruit_BME280::SAMPLING_X2,  // Temp
                  Adafruit_BME280::SAMPLING_X16, // Pressure
                  Adafruit_BME280::SAMPLING_X1,  // Humidity
                  Adafruit_BME280::FILTER_X16,
                  Adafruit_BME280::STANDBY_MS_500);
}

void loop() {
  // Read sensors and check for NaN (Not a Number) errors
  float temp = bme.readTemperature();
  float pressure = bme.readPressure() / 100.0F;
  float humidity = bme.readHumidity();

  if (isnan(temp) || isnan(pressure) || isnan(humidity)) {
    Serial1.println("ERROR: I2C read timeout or NaN received. Resetting I2C bus...");
    myWire.end();
    delay(50);
    myWire.begin();
    myWire.setClock(400000);
  } else {
    Serial1.print("Temp: "); Serial1.print(temp); Serial1.print(" C | ");
    Serial1.print("Press: "); Serial1.print(pressure); Serial1.print(" hPa | ");
    Serial1.print("Hum: "); Serial1.print(humidity); Serial1.println(" %");
    
    // Toggle LED to show heartbeat
    digitalWrite(STATUS_LED, LOW); 
    delay(50);
    digitalWrite(STATUS_LED, HIGH);
  }
  
  delay(2000);
}

Debugging: 'ST-LINK error' and Upload Failures

The most common roadblock when adopting the STM32 Arduino IDE is the upload process. Unlike AVRs, STM32s use the SWD (Serial Wire Debug) protocol. If your upload fails, you will likely see this exact error string in the Arduino IDE console:

Exact Error String:
ST-LINK error (dev_1): USB init failed
OR
Failed to connect to target / No STM32 target found!

The First 3 Things to Check When It Fails

  1. Power Delivery: Is the 3.3V pin connected? The ST-Link cannot communicate with the SWD controller if the STM32 core is unpowered. Do not rely on parasitic power from the SWDIO line.
  2. SWD Wiring Swap: SWDIO and SWCLK are frequently swapped on clone ST-Link dongles. Verify the pinout printed on your specific dongle's PCB, not just the plastic case.
  3. Target Reset State: If the STM32 is in a deep sleep mode or has SWD pins disabled in firmware, the ST-Link cannot connect. Fix: Press and hold the Black Pill's RESET button, click 'Upload' in the Arduino IDE, and release the RESET button the exact moment the console says 'Connecting to target'.

Ranked Causes for Persistent Upload Failures

RankCauseFix / Action Required
1Windows USB Driver MismatchDownload Zadig. Select 'STLink Debug', and replace the driver with WinUSB. The default Windows driver often blocks libusb access.
2Read-Out Protection (ROP) EnabledDownload STM32CubeProgrammer. Connect via ST-Link, go to the 'OBK' (Option Bytes) tab, and disable Read-Out Protection. This will mass-erase the chip.
3ST-Link Firmware OutdatedUse the official ST-Link Upgrade utility (included with STM32CubeIDE) to flash the latest V2 JTAG/SWD firmware to your programmer dongle.
4Board Variant Mismatch in IDEIn Arduino IDE Tools menu, ensure 'Board part number' is set to BlackPill F401CC and 'Upload method' is set to STLink, not 'Serial' or 'DFU'.

STM32 Arduino IDE FAQ

How do I install the official STM32 Arduino IDE core?

Open the Arduino IDE and navigate to File > Preferences. In the 'Additional Boards Manager URLs' field, paste: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json. Next, open the Boards Manager (Tools > Board > Boards Manager), search for 'STM32 MCU based boards', and install the package published by STMicroelectronics. Restart the IDE, and select your specific Black Pill variant under Tools > Board > STM32 boards groups.

Why does my STM32 Arduino code compile but fail to run on the Black Pill?

This is almost always caused by the BOOT0 jumper. If BOOT0 is bridged to 3.3V (position 1), the STM32 boots into its internal system memory (DFU/Serial bootloader) rather than executing your compiled code from Flash. Move the BOOT0 jumper to 0 (GND), press the hardware RESET button, and your sketch will execute normally. Additionally, ensure your code isn't hanging in setup() waiting for a Serial connection that isn't physically wired.

How can I extend this build to use the Black Pill's native USB CDC?

To simplify the build and eliminate the need for an external USB-to-Serial adapter on PA9/PA10, you can use the STM32F401's native USB port. In the Arduino IDE Tools menu, change 'USB support' to CDC (generic 'Serial' supersede U(S)ART). In your code, replace all instances of Serial1 with Serial. The Arduino core will automatically route Serial.print() through the Type-C connector. Note: You still need the ST-Link to flash the initial firmware via SWD, but subsequent updates can be done via the USB DFU bootloader.

Can I use the STM32 Arduino IDE core with PlatformIO instead of the Arduino IDE?

Yes, and it is highly recommended for complex projects. PlatformIO uses the exact same Arduino_Core_STM32 backend but offers vastly superior build caching, integrated SWD debugging via Cortex-Debug, and automated library management. To set it up in VS Code, create a new PlatformIO project, select the blackpill_f401cc board, and set the framework to arduino in your platformio.ini file. The pin mappings and code remain identical to what is shown in this guide.