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.
2а.3 Опознавательные знаки внутри камня
У каждого 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 Итоги
- Бывает три случая: оригинал, совместимый клон под своей маркой и клон или отбраковка под маркой ST. Опасен только третий.
- GD32F10x по своим файлам совместим по адресам — 35 блоков из 37 там же, где у STM32F103, — но не по поведению: 108 МГц по умолчанию вместо 72, иная работа Flash, имена в библиотеке с нуля.
- Опознавательные знаки F1:
DBGMCU_IDCODEпо0xE0042000(0x410— medium-density), размер Flash по0x1FFFF7E0, уникальный номер по0x1FFFF7E8, CPUID0x411FC231, код порта SWD0x1BA01477. - Функция из 2а.4 находит противоречия; подлинность она не доказывает.
Без отладчика
DBGMCU_IDCODEу F1 читается нулём — это «неизвестно», а не «чужой». - В эмуляторе CPUID оказался
0x410FC240— Cortex-M4 r0p0, и проверка отказала на первом признаке.
Полные исходники
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 и то, как одна прошивка прячет секреты от другой на том же ядре.