Embedded HandbookApps
All sections › I. Basics
STM32F103, GD32F103 · level basic · 12 min read
Open in the interactive handbook

2а. STM32 clones and fakes: how to tell them apart

A cheap BluePill board marked STM32F103C8 is not necessarily an STM32. There are manufacturers that produce pin- and register-compatible chips under their own brand, and there are sellers who put someone else's marking on such chips (or on rejects). The first is an honest product with its own peculiarities, the second is a fake. This section is about how they differ from the original and how to check it: with a program, with a debugger and by eye.

The reference values are taken from the STM32F1 reference manual and the device database of STM32CubeProgrammer, the information about the GD32F10x clone from its own Keil support pack. The classifier below was run on a computer, the CPUID read in the QEMU emulator. Checked by _verify/fake2a_check.py.

Terms used in the documentation

Term Meaning
device electronic signature identification data written into the chip at the factory
device ID, revision ID (DEV_ID, REV_ID) the device code and the revision code
unique device ID the unique number of the die
flash size register the register holding the Flash size
CPUID base register the register that identifies the core
SW-DP IDCODE the identification code of the SWD debug port
remarking, counterfeit a new marking on someone else's chip, a fake
pin-to-pin compatible compatible pin for pin

2а.1 Three cases

What is on the board What it is What the danger is
an ST original what the marking says none — it is the reference
a compatible clone under its own brand (for example, GD32F103) a different die that repeats the pinout and most of the registers differs in details: frequencies, Flash timings, small peripheral details
a clone or a reject under the ST brand a fake code "for STM32" works until it runs into a difference; there are no guarantees at all

A compatible clone is a normal choice if you take it knowingly and read its documentation. The problem is precisely the third case: you think you are working with an STM32 and look for the bug in your code, while it is in the chip.


2а.2 How compatible a clone is: the GD32F10x by its own files

A comparison of the register descriptions (SVD files) of the STM32F103 and the GD32F10x from the Keil packs:

What was compared Result
peripheral blocks in the GD32F10x description 37
of them at the same addresses as on the STM32F103 35
numbering in the clone's library from zero: TIMER1 = TIM2, USART0 = USART1, I2C0 = I2C1, ADC0 = ADC1, SPI0 = SPI1
default frequency in the clone's startup code 108 MHz from the crystal
default frequency on the STM32F103 72 MHz
Flash wait states in the startup code ST writes to FLASH->ACR; the clone has not a single write

Hence the rule: by addresses the clone is compatible, by behaviour it is not. A program built for the STM32F103 usually starts on a GD32F103: GPIOA sits in the same place, the clone's RCC (RCU) too. But everything that depends on time — delay loops, timer and UART frequency calculations, the USB prescaler — must be checked against the clone's documentation: the frequencies are different, the memory works differently. And the names in the manufacturer's examples are shifted by one — the clone's USART0 and ST's USART1 are one and the same block at address 0x40013800.

The GD32F10x clone versus the STM32F103: 35 peripheral blocks out of 37 are at the same addresses, so a program usually starts, but the names in the clone's library are shifted by one — TIMER1 is TIM2, USART0 is USART1; the clone's startup code gives 108 MHz against ST's 72 and does not write the Flash wait states, so everything that depends on time must be checked against the clone's documentation


2а.3 Identification marks inside the chip

Where the identification marks of the STM32F1 are and what values the original has

Every STM32F1 has an "electronic signature" written at the factory. It can be read both by a program and by a debugger; on F1 the program sees the device code only under a debugger (2а.4).

Where What On the original
0xE0042000, DBGMCU_IDCODE the device code DEV_ID (bits 11:0) and revision REV_ID (31:16) see the table below
0x1FFFF7E0 the Flash size in kilobytes, 16 bits 0x0080 — 128 KB
0x1FFFF7E8 the unique number, 96 bits each die has its own
0xE000ED00, SCB->CPUID the core and its revision 0x411FC231 — Cortex-M3 r1p1
SWD port, IDCODE the debug port code 0x1BA01477
JTAG port, IDCODE the same for JTAG 0x3BA00477

Device codes of the F1 family:

DEV_ID Density Flash
0x412 low-density 16–32 KB
0x410 medium-density — F103C8, F103CB 64–128 KB
0x414 high-density 256–512 KB
0x430 XL-density 768 KB – 1 MB
0x418 connectivity line — F105, F107 64–256 KB

For medium-density the revisions are: 0x0000 — A, 0x2000 — B, 0x2001 — Z, 0x2003 — 1, 2, 3, X or Y. So the word DBGMCU_IDCODE = 0x20036410 means "medium-density F103, revision 0x2003". From the same code 0x410 STM32CubeProgrammer names the chip "STM32F101/F102/F103 Medium-density".

CPUID is decoded by fields: 0x41 — manufacturer ARM, variant 1, architecture 0xF, part number 0xC23 (Cortex-M3), revision 1. The whole F1 family is built on the r1p1 core, so the original has one CPUID for all: 0x411FC231.


2а.4 The identification function

Four marks give five checks — the Flash size is compared both with the density and with the marking. They run in turn, and the first mismatch names the reason:

static enum verdict classify(uint32_t idcode, uint16_t fsize, uint32_t cpuid, const uint32_t uid[3],
                             uint16_t marking_kb)
{
    /* CPUID: производитель 0x41 (ARM), архитектура 0xF, номер ядра 0xC23 — Cortex-M3,
     * у всего семейства F1 ревизия r1p1: вариант 1, ревизия 1 → 0x411FC231 */
    if (cpuid != 0x411FC231u)
        return V_NOT_F1_CORE;
    uint16_t dev = idcode & 0xFFFu;
    const f1_dev_t *d = NULL;
    for (unsigned i = 0; i < sizeof F1 / sizeof F1[0]; i++)
        if (F1[i].dev == dev)
            d = &F1[i];
    /* у F1 без отладчика DBGMCU_IDCODE читается нулём: код неизвестен, но не чужой */
    if (!d && dev != 0)
        return V_UNKNOWN_DEV;
    if (d && (fsize < d->kb_min || fsize > d->kb_max))
        return V_SIZE_MISMATCH;
    if (marking_kb && fsize != marking_kb)
        return V_MARK_MISMATCH;
    if ((uid[0] == 0 && uid[1] == 0 && uid[2] == 0) || (uid[0] == 0xFFFFFFFFu && uid[1] == 0xFFFFFFFFu && uid[2] == 0xFFFFFFFFu))
        return V_UID_BLANK;
    return V_ST;
}

On the board it is called with the values read from the addresses in 2а.3, and with the size from the marking: C8 — 64 KB, CB — 128 KB.

⚠️ Without a debugger DBGMCU_IDCODE on the STM32F1 reads as zero. That is how the die is made: the DBGMCU_IDCODE and DBGMCU_CR registers are accessible to the program only in debug mode — this is stated in the device errata and in the comment to HAL_GetDEVID. So the device code is reliably checked under a debugger, while in standalone firmware a zero is treated as "unknown", not "foreign": the function skips this check and judges by the CPUID, the Flash size and the unique number.

On a computer it was run on nine sets:

Set Verdict
F103C8, revision 0x2003 looks like an STM32F1
F103CB, revision 0x2003 looks like an STM32F1
marked C8, but Flash 128 the Flash size does not match the marking
code 0x410, core r2p1 the core is not the one in the F1
code 0x000 — read without a debugger looks like an STM32F1
device code 0x123 the device code is not in the ST table
high-density, Flash 64 the Flash size does not fit this density
the number is all zeros the unique number is blank
Cortex-M4 r0p0 core the core is not the one in the F1

The check does not prove authenticity — a fake can repeat all four values. But every mismatch says reliably: this is not the chip it is passed off as, and the cause of the oddities must be sought outside the code.


2а.5 The same on a "non-genuine" chip

The first mark was tested for real — in the QEMU emulator with the netduinoplus2 machine (it emulates an STM32F405). The program read SCB->CPUID and printed to the UART:

qemu_cpuid=410FC240
qemu_partno=C24
qemu_is_f1=0

0xC24 is a Cortex-M4, variant and revision zero: r0p0. For the check "is this an STM32F1?" the answer is "no" — on the very first mark. This is exactly what meeting a chip of the wrong family looks like.


2а.6 Without a program: the debugger and your eyes

The debugger. On connecting over SWD the debugger first of all reads the port code. On an original F1 it is 0x1BA01477; a different code is a reason to be wary. A common example is clones like the CS32F103: they answer 0x2BA01477, and OpenOCD with the stm32f1x configuration prints "UNEXPECTED idcode" and does not connect. You can work with such a chip if you set the expected code before the configuration file: -c "set CPUTAPID 0x2ba01477". The second thing flashing programs show is the device code from DBGMCU_IDCODE: STM32CubeProgrammer names the series from it.

Your eyes. Suspicious signs are a marking that looks different from that on other chips of the same batch, traces of the surface being ground under a new marking and a marking that does not match what the chip reports about itself. Eyes make mistakes more often than a program, so the deciding factor is still the check from 2а.4.

Where to buy. Only official distributors guarantee the original. For a prototype a clone under its own brand is a normal choice; for production — only knowingly and with its documentation.


2а.7 Pitfalls

"A program for STM32 is glitchy on this board". First — the identification function. If the chip is not ST, the bugs are sought in the differences, not in your own code.

A genuine F103 "not in the ST table". The program is run without a debugger: DBGMCU_IDCODE on F1 then reads as zero. The device code is checked under a debugger, and a zero is treated as "unknown" (2а.4).

Delays and the UART "drift" on a clone. The clone's startup code raises the frequency to 108 MHz instead of 72: everything computed from 72 MHz is off by a factor of one and a half.

You took the clone maker's example — and mixed up the blocks. The clone's USART0 is ST's USART1, TIMER1 is TIM2: numbering from zero.

The Flash size is "bigger than written". The register at 0x1FFFF7E0 shows what the chip is rated for; relying on undocumented memory is a lottery even on an original, let alone on a fake.

The check passed — so it is an original. No: four matching marks do not prove authenticity, they merely find no contradictions.


2а.8 Summary

Full sources

ident.c — the function that identifies a chip by the device code, Flash size, unique number and CPUID, with a run on sets of values.

/* Раздел 2а: опознание камня по четырём признакам — коду устройства, размеру Flash,
 * уникальному номеру и CPUID ядра. Функция чистая: на плате ей дают прочитанные регистры,
 * здесь — наборы значений. Опорные значения — из справочного руководства STM32F1. */
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#define DBGMCU_IDCODE_ADDR 0xE0042000u   /* код устройства и ревизии */
#define FLASH_SIZE_ADDR    0x1FFFF7E0u   /* размер Flash в килобайтах, 16 бит */
#define UID_ADDR           0x1FFFF7E8u   /* 96-битный уникальный номер */
#define CPUID_ADDR         0xE000ED00u   /* SCB->CPUID ядра */

typedef struct {
    uint16_t dev;                /* DEV_ID[11:0] */
    const char *name;
    uint16_t kb_min, kb_max;     /* какой размер Flash допустим для этой плотности */
} f1_dev_t;

/* DEV_ID семейства F1 — таблица из справочного руководства */
static const f1_dev_t F1[] = {
    {0x412, "low-density", 16, 32},
    {0x410, "medium-density", 64, 128},
    {0x414, "high-density", 256, 512},
    {0x430, "XL-density", 768, 1024},
    {0x418, "connectivity", 64, 256},
};

enum verdict { V_ST = 0, V_NOT_F1_CORE, V_UNKNOWN_DEV, V_SIZE_MISMATCH, V_MARK_MISMATCH, V_UID_BLANK };

static const char *VNAME[] = {"похож на STM32F1", "ядро не то, что у F1", "код устройства не из таблицы ST",
                              "размер Flash не для этой плотности", "размер Flash не совпадает с маркировкой",
                              "уникальный номер пустой"};

/* marking_kb — размер Flash по маркировке на корпусе (C8 — 64, CB — 128), 0 — не проверять */
static enum verdict classify(uint32_t idcode, uint16_t fsize, uint32_t cpuid, const uint32_t uid[3],
                             uint16_t marking_kb)
{
    /* CPUID: производитель 0x41 (ARM), архитектура 0xF, номер ядра 0xC23 — Cortex-M3,
     * у всего семейства F1 ревизия r1p1: вариант 1, ревизия 1 → 0x411FC231 */
    if (cpuid != 0x411FC231u)
        return V_NOT_F1_CORE;
    uint16_t dev = idcode & 0xFFFu;
    const f1_dev_t *d = NULL;
    for (unsigned i = 0; i < sizeof F1 / sizeof F1[0]; i++)
        if (F1[i].dev == dev)
            d = &F1[i];
    /* у F1 без отладчика DBGMCU_IDCODE читается нулём: код неизвестен, но не чужой */
    if (!d && dev != 0)
        return V_UNKNOWN_DEV;
    if (d && (fsize < d->kb_min || fsize > d->kb_max))
        return V_SIZE_MISMATCH;
    if (marking_kb && fsize != marking_kb)
        return V_MARK_MISMATCH;
    if ((uid[0] == 0 && uid[1] == 0 && uid[2] == 0) || (uid[0] == 0xFFFFFFFFu && uid[1] == 0xFFFFFFFFu && uid[2] == 0xFFFFFFFFu))
        return V_UID_BLANK;
    return V_ST;
}

static void cpuid_fields(uint32_t c)
{
    printf("cpuid_fields=%02X,%X,%X,%03X,%X\n", (unsigned)(c >> 24), (unsigned)((c >> 20) & 0xF),
           (unsigned)((c >> 16) & 0xF), (unsigned)((c >> 4) & 0xFFF), (unsigned)(c & 0xF));
}

int main(void)
{
    const uint32_t uid_ok[3] = {0x0667FF48u, 0x49574949u, 0x87121137u};  /* образец: любые ненулевые слова */
    const uint32_t uid_zero[3] = {0, 0, 0};
    struct { const char *case_name; uint32_t id; uint16_t fs; uint32_t cpu; const uint32_t *uid; uint16_t mark; } T[] = {
        {"F103C8, ревизия 0x2003", 0x20036410u, 64, 0x411FC231u, uid_ok, 64},
        {"F103CB, ревизия 0x2003", 0x20036410u, 128, 0x411FC231u, uid_ok, 128},
        {"надпись C8, а Flash 128", 0x20036410u, 128, 0x411FC231u, uid_ok, 64},
        {"код 0x410, ядро r2p1", 0x20036410u, 64, 0x412FC231u, uid_ok, 64},
        {"код 0x000 — чтение без отладчика", 0x00000000u, 64, 0x411FC231u, uid_ok, 64},
        {"код устройства 0x123", 0x20036123u, 64, 0x411FC231u, uid_ok, 64},
        {"high-density, Flash 64", 0x10036414u, 64, 0x411FC231u, uid_ok, 0},
        {"номер одни нули", 0x20036410u, 64, 0x411FC231u, uid_zero, 64},
        {"ядро Cortex-M4 r0p0", 0x20036410u, 64, 0x410FC240u, uid_ok, 64},
    };
    for (unsigned i = 0; i < sizeof T / sizeof T[0]; i++) {
        enum verdict v = classify(T[i].id, T[i].fs, T[i].cpu, T[i].uid, T[i].mark);
        printf("case=%s|%d|%s\n", T[i].case_name, (int)v, VNAME[v]);
    }
    printf("addr=%08X,%08X,%08X,%08X\n", DBGMCU_IDCODE_ADDR, FLASH_SIZE_ADDR, UID_ADDR, CPUID_ADDR);
    printf("rev_dev=%04X,%03X\n", (unsigned)(0x20036410u >> 16), (unsigned)(0x20036410u & 0xFFF));
    cpuid_fields(0x411FC231u);
    cpuid_fields(0x410FC240u);
    printf("done=1\n");
    return 0;
}

Next: section 2б "STM32U5 and TrustZone" — the new generation on the Cortex-M33 and how one firmware hides secrets from another on the same core.

Open in the interactive handbook