Embedded HandbookПриложения
Все разделы › I. Основы
STM32F103, GD32F103 · уровень базовый · 12 мин чтения
Открыть в интерактивном справочнике

2а. Клоны и подделки STM32: как отличить

Дешёвая плата BluePill с надписью STM32F103C8 — не обязательно STM32. Есть производители, выпускающие совместимые по выводам и регистрам камни под своей маркой, и есть продавцы, которые ставят на такие камни (или на отбраковку) чужую надпись. Первое — честный товар со своими особенностями, второе — подделка. Этот раздел о том, чем они отличаются от оригинала и как это проверить: программой, отладчиком и глазами.

Опорные значения взяты из справочного руководства на STM32F1 и базы устройств STM32CubeProgrammer, сведения о клоне GD32F10x — из его собственного пакета поддержки для Keil. Классификатор ниже прогнан на компьютере, чтение CPUID — в эмуляторе QEMU. Сверяет _verify/fake2a_check.py.

Как это называется в документации

Оригинал Перевод
device electronic signature электронная подпись устройства
device ID, revision ID (DEV_ID, REV_ID) код устройства и код ревизии
unique device ID уникальный номер кристалла
flash size register регистр размера Flash
CPUID base register регистр опознания ядра
SW-DP IDCODE код отладочного порта SWD
remarking, counterfeit перемаркировка, подделка
pin-to-pin compatible совместимый по выводам

2а.1 Три случая

Что на плате Что это Чем опасно
оригинал ST то, что написано ничем — это эталон
совместимый клон под своей маркой (например, GD32F103) другой кристалл, повторяющий выводы и большую часть регистров отличается в деталях: частоты, тайминги Flash, мелочи периферии
клон или отбраковка под маркой ST подделка код «для STM32» работает, пока не упрётся в отличие; гарантий нет никаких

Совместимый клон — нормальный выбор, если брать его осознанно и читать его документацию. Проблема именно в третьем случае: вы думаете, что работаете с STM32, и ищете ошибку в своём коде, а она — в камне.


2а.2 Насколько совместим клон: GD32F10x по его собственным файлам

Сравнение описаний регистров (SVD-файлов) STM32F103 и GD32F10x из паков для Keil:

Что сравнили Итог
блоков периферии в описании GD32F10x 37
из них по тем же адресам, что у STM32F103 35
нумерация в библиотеке клона с нуля: TIMER1 = TIM2, USART0 = USART1, I2C0 = I2C1, ADC0 = ADC1, SPI0 = SPI1
частота по умолчанию в стартовом коде клона 108 МГц от кварца
частота по умолчанию у STM32F103 72 МГц
такты ожидания Flash в стартовом коде у ST — запись в FLASH->ACR; у клона — ни одной записи

Отсюда правило: по адресам клон совместим, по поведению — нет. Программа, собранная для STM32F103, на GD32F103 обычно запускается: GPIOA лежит там же, RCC клона (RCU) — там же. Но всё, что зависит от времени, — циклы задержки, расчёт частоты таймеров и UART, делитель для USB — надо сверять с документацией клона: частоты другие, память работает иначе. И имена в примерах производителя смещены на единицу — USART0 клона и USART1 у ST это один и тот же блок по адресу 0x40013800.

Клон GD32F10x против STM32F103: 35 блоков периферии из 37 лежат по тем же адресам, поэтому программа обычно запускается, но имена в библиотеке клона смещены на единицу — TIMER1 это TIM2, USART0 это USART1; стартовый код клона даёт 108 МГц против 72 у ST и не пишет такты ожидания Flash, так что всё зависящее от времени надо сверять с документацией клона


2а.3 Опознавательные знаки внутри камня

Где лежат опознавательные знаки STM32F1 и какие значения у оригинала

У каждого STM32F1 есть «электронная подпись», записанная на заводе. Её читают и программой, и отладчиком; код устройства у F1 программа видит только под отладчиком (2а.4).

Где Что У оригинала
0xE0042000, DBGMCU_IDCODE код устройства DEV_ID (биты 11:0) и ревизии REV_ID (31:16) см. таблицу ниже
0x1FFFF7E0 размер Flash в килобайтах, 16 бит 0x0080 — 128 КБ
0x1FFFF7E8 уникальный номер, 96 бит у каждого кристалла свой
0xE000ED00, SCB->CPUID ядро и его ревизия 0x411FC231 — Cortex-M3 r1p1
порт SWD, IDCODE код отладочного порта 0x1BA01477
порт JTAG, IDCODE то же для JTAG 0x3BA00477

Коды устройств семейства F1:

DEV_ID Плотность Flash
0x412 low-density 16–32 КБ
0x410 medium-density — F103C8, F103CB 64–128 КБ
0x414 high-density 256–512 КБ
0x430 XL-density 768 КБ – 1 МБ
0x418 connectivity line — F105, F107 64–256 КБ

У medium-density ревизии такие: 0x0000 — A, 0x2000 — B, 0x2001 — Z, 0x2003 — 1, 2, 3, X или Y. Значит, слово DBGMCU_IDCODE = 0x20036410 означает «F103 средней плотности, ревизия 0x2003». По тому же коду 0x410 STM32CubeProgrammer называет камень «STM32F101/F102/F103 Medium-density».

CPUID разбирается по полям: 0x41 — производитель ARM, вариант 1, архитектура 0xF, номер ядра 0xC23 (Cortex-M3), ревизия 1. Всё семейство F1 построено на ядре r1p1, поэтому у оригинала CPUID один для всех: 0x411FC231.


2а.4 Функция опознания

Четыре признака дают пять проверок — размер Flash сверяется и с плотностью, и с маркировкой. Идут они по очереди, и первая несовпавшая называет причину:

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;
}

На плате её вызывают со значениями, прочитанными по адресам из 2а.3, и с размером по маркировке: C8 — 64 КБ, CB — 128 КБ.

⚠️ Без отладчика DBGMCU_IDCODE у STM32F1 читается нулём. Так устроен кристалл: регистры DBGMCU_IDCODE и DBGMCU_CR доступны программе только в режиме отладки — это записано в перечне ошибок кристалла и в комментарии к HAL_GetDEVID. Поэтому код устройства надёжно проверяют под отладчиком, а в автономной прошивке ноль считают «неизвестно», а не «чужой»: функция пропускает эту проверку и судит по CPUID, размеру Flash и уникальному номеру.

На компьютере её прогнали на девяти наборах:

Набор Вердикт
F103C8, ревизия 0x2003 похож на STM32F1
F103CB, ревизия 0x2003 похож на STM32F1
надпись C8, а Flash 128 размер Flash не совпадает с маркировкой
код 0x410, ядро r2p1 ядро не то, что у F1
код 0x000 — чтение без отладчика похож на STM32F1
код устройства 0x123 код устройства не из таблицы ST
high-density, Flash 64 размер Flash не для этой плотности
номер одни нули уникальный номер пустой
ядро Cortex-M4 r0p0 ядро не то, что у F1

Проверка не доказывает подлинность — подделка может повторить все четыре значения. Но каждое несовпадение надёжно говорит: это не тот камень, за который его выдают, и искать причину странностей надо не в коде.


2а.5 То же на «ненастоящем» камне

Первый признак проверили по-настоящему — в эмуляторе QEMU с машиной netduinoplus2 (он изображает STM32F405). Программа прочитала SCB->CPUID и напечатала в UART:

qemu_cpuid=410FC240
qemu_partno=C24
qemu_is_f1=0

0xC24 — Cortex-M4, вариант и ревизия нулевые: r0p0. Для проверки «это STM32F1?» ответ «нет» — на первом же признаке. Именно так выглядит встреча с камнем не того семейства.


2а.6 Без программы: отладчик и глаза

Отладчик. При подключении по SWD отладчик первым делом читает код порта. У оригинального F1 это 0x1BA01477; другой код — повод насторожиться. Частый пример — клоны вроде CS32F103: они отвечают 0x2BA01477, и OpenOCD с настройкой stm32f1x пишет «UNEXPECTED idcode» и не подключается. Работать с таким камнем можно, если до файла настройки задать ожидаемый код: -c "set CPUTAPID 0x2ba01477". Второе, что показывают программы для прошивки, — код устройства из DBGMCU_IDCODE: STM32CubeProgrammer по нему называет серию.

Глаза. Подозрительны надпись, которая выглядит иначе, чем на других камнях той же партии, следы шлифовки поверхности под новой надписью и маркировка, не совпадающая с тем, что камень сообщает о себе сам. Глаза ошибаются чаще программы, поэтому решает всё-таки проверка из 2а.4.

Где покупать. Оригинал гарантируют только официальные дистрибьюторы. Для макета клон под своей маркой — нормальный выбор; для серии — только осознанно и с его документацией.


2а.7 Грабли

«Программа для STM32 на этой плате глючит». Сначала — функция опознания. Если камень не ST, ошибки ищут в различиях, а не в своём коде.

Настоящий F103 «не из таблицы ST». Программа запущена без отладчика: DBGMCU_IDCODE у F1 тогда читается нулём. Код устройства проверяют под отладчиком, а ноль считают «неизвестно» (2а.4).

Задержки и UART «поплыли» на клоне. Стартовый код клона поднимает частоту до 108 МГц вместо 72: всё, что считалось от 72 МГц, ошибается в полтора раза.

Взяли пример производителя клона — и перепутали блоки. USART0 клона — это USART1 у ST, TIMER1 — это TIM2: нумерация с нуля.

Размер Flash «больше, чем написано». Регистр по 0x1FFFF7E0 показывает то, на что камень рассчитан; расчёт на недокументированную память — лотерея даже на оригинале, а на подделке тем более.

Проверка прошла — значит, оригинал. Нет: совпадение четырёх признаков не доказывает подлинность, оно лишь не находит противоречий.


2а.8 Итоги

Полные исходники

ident.c — функция опознания камня по коду устройства, размеру Flash, уникальному номеру и CPUID с прогоном на наборах значений.

/* Раздел 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;
}

Дальше: раздел 2б «STM32U5 и TrustZone» — новое поколение на Cortex-M33 и то, как одна прошивка прячет секреты от другой на том же ядре.

Открыть в интерактивном справочнике