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.
2а.3 Identification marks inside the chip
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_IDCODEon the STM32F1 reads as zero. That is how the die is made: theDBGMCU_IDCODEandDBGMCU_CRregisters are accessible to the program only in debug mode — this is stated in the device errata and in the comment toHAL_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
- There are three cases: the original, a compatible clone under its own brand and a clone or reject under the ST brand. Only the third is dangerous.
- The GD32F10x by its own files is compatible by address — 35 blocks out of 37 in the same place as on the STM32F103 — but not by behaviour: 108 MHz by default instead of 72, different Flash operation, library names from zero.
- F1 identification marks:
DBGMCU_IDCODEat0xE0042000(0x410— medium-density), the Flash size at0x1FFFF7E0, the unique number at0x1FFFF7E8, CPUID0x411FC231, the SWD port code0x1BA01477. - The function from 2а.4 finds contradictions; it does not prove authenticity.
Without a debugger
DBGMCU_IDCODEon F1 reads as zero — that is "unknown", not "foreign". - In the emulator the CPUID turned out to be
0x410FC240— Cortex-M4 r0p0, and the check failed on the first mark.
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.