The Verdict: Which STM32 Board and Core Should You Pick?
If you are evaluating hardware to use the Arduino IDE for STM32 development in 2026, stop buying the classic STM32F103C8T6 "Blue Pill". The market is heavily saturated with counterfeit GD32 and CKS32 clones that exhibit erratic ADC behavior and fail during SWD flashing.
Decision Path: Blue Pill vs. Black Pill
| Criteria | STM32F103C8T6 (Blue Pill) | STM32F401CCU6 (Black Pill V3.1) |
|---|---|---|
| Core / Speed | Cortex-M3 @ 72 MHz | Cortex-M4 @ 84 MHz |
| Flash / RAM | 64KB / 20KB | 256KB / 64KB |
| 5V Tolerance | Mostly 5V tolerant (trap for clones) | Strictly 3.3V (forces good habits) |
| Counterfeit Risk | Extremely High (>80% on AliExpress) | Low (WeAct controls supply chain) |
| USB Connector | Micro-USB (fragile) | USB-C (robust) |
Conclusion: Choose the WeAct Black Pill F401CC for any new sensor, IoT, or motor control project. Reserve the F103 Blue Pill only if you are maintaining legacy hardware.
Hardware Spec Sheet and Pin Mapping
For this build, we are creating a robust I2C environmental logger with an OLED display and a hardware watchdog timer (WDT) to recover from I2C bus lockups—a common failure mode in noisy bench environments.
Parts List
- MCU: WeAct Studio STM32F401CCU6 Black Pill V3.1
- Programmer: Genuine STMicroelectronics ST-LINK/V2 (or a verified clone with updated firmware)
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) or generic 3.3V BME280 module
- Display: 0.96" SSD1306 128x64 I2C OLED (3.3V compatible)
- Wiring: 28 AWG silicone stranded wire, 2.54mm header pins
Pin Mapping Table
The STM32duino core allows flexible pin mapping, but using the default hardware I2C1 pins ensures optimal bus speed and reliability.
| Component | STM32F401 Pin | Wire Color (Standard) | Notes |
|---|---|---|---|
| ST-Link SWDIO | PA13 (DIO) | Orange | Do not use as GPIO after boot |
| ST-Link SWCLK | PA14 (CLK) | Yellow | Do not use as GPIO after boot |
| ST-Link GND | GND | Black | Common ground is mandatory |
| ST-Link 3.3V | 3V3 | Red | Only if ST-Link powers the board |
| BME280 SDA | PB7 | Blue | Hardware I2C1 SDA |
| BME280 SCL | PB6 | Purple | Hardware I2C1 SCL |
| OLED SDA | PB7 | Blue | Shared I2C bus |
| OLED SCL | PB6 | Purple | Shared I2C bus |
| Onboard LED | PC13 | N/A | Active LOW on Black Pill |
Setting Up Arduino IDE for STM32 (Step-by-Step)
The official STM32duino core is the only supported path for modern development. Do not use the deprecated Roger Clark Maple core.
- Add the Board Manager URL: Open Arduino IDE > File > Preferences. Paste this exact URL into the "Additional boards manager URLs" field:
https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json - Install the Core: Go to Tools > Board > Boards Manager. Search for "STM32 cores group" and install the latest version (2.8.x or newer).
- Select the Target Board: Go to Tools > Board > STM32 boards groups > BlackPill F401CC.
- Configure Upload Method: Go to Tools > Upload method and select STM32CubeProgrammer (SWD). This requires your ST-Link to be plugged in and recognized by the OS.
- Set USB Support: Go to Tools > USB support (if available) and select CDC (generic 'Serial' supersede U(S)ART) if you want native USB Serial debugging without a separate UART adapter.
install.sh script in the Drivers/rules directory, then reboot.
The Build: I2C Sensor Logger with Watchdog Failsafe
This code targets the Black Pill F401CC. It reads temperature and humidity from the BME280, prints to the OLED, and outputs to the Serial monitor. Crucially, it implements the STM32 hardware Independent Watchdog (IWDG). If the I2C bus hangs (a frequent issue with cheap OLED modules), the watchdog resets the MCU instead of leaving it locked in a frozen state.
Required Libraries: Install Adafruit BME280 Library, Adafruit SSD1306, and Adafruit GFX Library via the Library Manager.
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#include <IWatchdog.h> // STM32duino hardware watchdog
// --- PIN DEFINITIONS ---
#define PIN_SDA PB7
#define PIN_SCL PB6
#define PIN_LED PC13 // Active LOW on WeAct Black Pill
// --- DISPLAY CONFIG ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
// --- OBJECTS ---
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_BME280 bme;
bool sensorFound = false;
void setup() {
Serial.begin(115200);
unsigned long startMillis = millis();
while (!Serial && (millis() - startMillis < 2000)) {
delay(10); // Wait for USB serial to connect, max 2s
}
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, HIGH); // Turn LED OFF (Active LOW)
// Initialize I2C with explicit pins
Wire.setSCL(PIN_SCL);
Wire.setSDA(PIN_SDA);
Wire.setClock(400000); // 400kHz Fast Mode
Wire.begin();
// Initialize OLED
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed. Halting."));
blinkError(5); // Blink 5 times, then halt
for(;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// Initialize BME280
if (!bme.begin(0x76, &Wire)) {
Serial.println(F("Could not find a valid BME280 sensor!"));
display.println(F("BME280 ERR"));
display.display();
} else {
sensorFound = true;
bme.setSampling(Adafruit_BME280::MODE_NORMAL,
Adafruit_BME280::SAMPLING_X2,
Adafruit_BME280::SAMPLING_X16,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::FILTER_OFF,
Adafruit_BME280::STANDBY_MS_1000);
}
// Start Hardware Watchdog: 4,000,000 microseconds = 4 seconds
IWatchdog.begin(4000000);
Serial.println(F("System Initialized. Watchdog Active."));
}
void loop() {
// Reload watchdog immediately to prevent reset during long operations
IWatchdog.reload();
if (sensorFound) {
float tempC = bme.readTemperature();
float humidity = bme.readHumidity();
// Serial Output
Serial.print(F("Temp: "));
Serial.print(tempC);
Serial.print(F(" C | Hum: "));
Serial.print(humidity);
Serial.println(F(" %"));
// OLED Output
display.clearDisplay();
display.setCursor(0, 0);
display.print(F("Temp: "));
display.print(tempC, 1);
display.println(F(" C"));
display.print(F("Hum: "));
display.print(humidity, 1);
display.println(F(" %"));
display.display();
}
digitalWrite(PIN_LED, LOW); // LED ON
delay(500);
digitalWrite(PIN_LED, HIGH); // LED OFF
delay(500);
// Final reload before loop restarts
IWatchdog.reload();
}
void blinkError(int times) {
for (int i = 0; i < times; i++) {
digitalWrite(PIN_LED, LOW);
delay(200);
digitalWrite(PIN_LED, HIGH);
delay(200);
}
}
Debugging: Upload Errors and SWD Failures
When using the Arduino IDE for STM32 via SWD, the underlying toolchain relies on STM32CubeProgrammer CLI. The most common point of failure is the physical connection or the clone programmer firmware.
Exact Error: Error: init mode failed (unable to connect to the target)
This string appears in the Arduino IDE console when the ST-Link cannot establish a Serial Wire Debug handshake with the STM32F401.
Ranked Causes and Fixes
- SWDIO and SWCLK Swapped (60% of cases): The pinout on clone ST-Links varies. Verify with a multimeter. SWDIO must go to PA13, SWCLK to PA14. Swapping them will not damage the F401, but it will halt communication.
- Insufficient 3.3V Power (25% of cases): Clone ST-Link V2 dongles often have weak onboard 3.3V regulators that sag under the OLED display load. Fix: Power the Black Pill via its USB-C port from a wall adapter, and only connect GND, SWDIO, and SWCLK from the ST-Link. Do not connect the ST-Link 3.3V pin.
- Target in Sleep/Bricked State (10% of cases): If previous code put the MCU into deep sleep or disabled SWD pins immediately on boot, the programmer cannot catch the halt. Fix: Hold the RESET button on the Black Pill, click "Upload" in Arduino IDE, and release the RESET button exactly when the console says "Connecting to target".
- Outdated ST-Link Firmware (5% of cases): Fix: Download STM32CubeProgrammer, open the "Firmware Upgrade" tab, and flash the latest ST-Link firmware to your dongle.
The First Three Things to Check When It Fails
2. Voltage Rail Check: Measure the 3.3V pin on the Black Pill relative to GND while powered. It must read between 3.2V and 3.4V. If it reads 2.8V, your power source is browning out.
3. Driver Verification: Open the standalone STM32CubeProgrammer GUI. If it cannot see the ST-Link hardware, your issue is OS-level (Zadig driver replacement needed on Windows, or missing udev rules on Linux), not an Arduino IDE issue.
Extending and Simplifying the Build
Once the baseline logger is flashing reliably, you can scale the project based on your deployment environment.
How to Simplify (For Quick Bench Tests)
- Drop the OLED: Remove the
Adafruit_SSD1306library and display calls. Rely entirely onSerial.printvia the native USB-C CDC port. This saves ~15KB of flash and eliminates I2C address conflicts. - Use Polling over Interrupts: The provided code uses standard polling. Keep it this way for simplicity. Avoid attaching I2C interrupts unless you are reading data at >100Hz.
How to Extend (For Production / Field Deployment)
- Add Deep Sleep (Stop/Standby Modes): The STM32F401 excels at low power. Use the
STM32LowPowerlibrary to put the board into Standby mode between readings, waking via an RTC alarm. This drops current draw from 25mA to under 15µA. - Implement DMA for I2C: If you add a high-speed sensor (like an MPU6050 at 1kHz), switch from standard
Wireto the STM32duino DMA-enabled I2C functions to offload the CPU. - Flash via DFU (USB): If you want to eliminate the ST-Link entirely, change the upload method to DFU. You must install the WeAct DFU bootloader via SWD once. After that, you can flash directly over the USB-C cable using the native ROM bootloader, though you lose hardware debugging capabilities.
By standardizing on the WeAct Black Pill F401 and utilizing the hardware watchdog, you eliminate the two biggest time-sinks in embedded development: chasing counterfeit silicon bugs and recovering from locked I2C buses in the field.






