198э. Мини-проект: датчик температуры по Bluetooth LE на P-NUCLEO-WB55 — два ядра, IPCC и свой сервис GATT
Bluetooth LE в справочнике уже был трижды. В разделе 158 сервер GATT поднимался на ESP32 со стеком NimBLE: там стек и программа живут на одном процессоре. В разделе 189 BLE разобран со стороны телефона — MTU, одна запись за раз, разрешения. В разделе 94б STM32WB55 встретился внутри Flipper Zero, но только как «процессор с радио»: что внутри стека и как до него достучаться, там не разбиралось.
Здесь — законченный прибор на одной плате P-NUCLEO-WB55: датчик температуры, который телефон находит по имени, читает и на который подписывается. Главное отличие от ESP32 — у STM32WB55 два ядра. Программа живёт на Cortex-M4, стек Bluetooth — на отдельном Cortex-M0+, и разговаривают они через общую память и блок IPCC. Раздел о том, как устроен этот разговор, как выглядит свой сервис GATT и что именно летит в эфир, когда телефон обходит таблицу прибора.
Все прогоны раздела сделаны на компьютере. Код ядра M4 — сервис, команды радиоядру, драйвер IPCC — настоящий, тот же, что собирается под STM32WB55. Под ним вместо кристалла три модели: блок IPCC по справочному руководству, радиоядро со своей таблицей GATT и «телефон» — клиент GATT, который ведёт журнал всех пакетов ATT. Обмен между ядрами в модели — свой простой формат, а не формат библиотеки ST: что на его месте в настоящем приборе, показано в 198э.4.
Опоры: справочное руководство RM0434 (rev. 12) — IPCC, загрузка второго
ядра, флеш и ОЗУ, АЦП и датчик температуры; даташит DS11929 (rev. 16) —
частоты, память, калибровки; описание платы DB3659 и файлы платы
stm32wbxx_nucleo.h из пака Keil STM32WBxx_DFP 1.4.0; заголовок CMSIS
stm32wb55xx.h, шаблон компоновки stm32wb55xx_flash_cm4.ld того же пака.
Как устроен разговор со стеком ST в живом приборе — по исходникам Flipper
Zero, github.com/flipperdevices/flipperzero-firmware (коммит 7f0b6e1,
GPL-3.0), каталог targets/f7/ble_glue. Коды ATT, номера типов и
полей рекламы сверены с заголовками NimBLE (github.com/espressif/esp-nimble,
коммит a9a4c70, Apache-2.0) и ESP-IDF v5.4.1 (коммит 4c2820d, Apache-2.0).
Сборка на компьютере — gcc 12.2.0 в stm-sys:1, прошивка —
arm-none-eabi-gcc 12.2.1 в stm-arm:1. Регистры STM32WB — приложение A17.
Как это называется в документации
Оригинал Перевод CPU1, CPU2 ядро M4 с программой и ядро M0+ с радиостеком wireless stack, FUS (Firmware Upgrade Service) прошивка радиоядра от ST и её установщик IPCC, channel, occupied / free блок связи процессоров, канал, «занят» / «свободен» Simplex, Half-duplex канал в одну сторону; команда и ответ в одном буфере mailbox, shared memory (SRAM2a) общая память, в которой лежат сообщения ACI, HCI команды стеку ST и стандартный интерфейс контроллера Bluetooth GATT, ATT, attribute handle таблица сервисов, протокол атрибутов, номер атрибута CCCD (0x2902) описатель подписки: клиент пишет 01 00, чтобы получать уведомления notification / indication уведомление без подтверждения / индикация с подтверждением Environmental Sensing (0x181A), Temperature (0x2A6E) стандартные сервис датчиков среды и характеристика температуры advertising, AD type реклама — посылки до подключения; поле рекламы
198э.1 Плата и замысел
P-NUCLEO-WB55 — набор из двух плат (описание DB3659): Nucleo-68 со STM32WB55RG в корпусе VFQFPN68 и маленький USB-донгл со STM32WB55CG. Здесь нужна только Nucleo. На ней:
| Что | Где | Откуда |
|---|---|---|
| три светодиода | LED1 синий — PB5, LED2 зелёный — PB0, LED3 красный — PB1 | stm32wbxx_nucleo.h |
| три кнопки и сброс | SW1 — PC4, SW2 — PD0, SW3 — PD1 | stm32wbxx_nucleo.h |
| консоль | USART1: TX — PB6, RX — PB7, AF7 (COM1 платы) | stm32wbxx_nucleo.h |
| программатор | ST-LINK/V2-1: флеш-накопитель, виртуальный COM-порт, отладка | DB3659 |
| антенна | на плате или разъём SMA под неё | DB3659 |
| питание | от USB ST-LINK или внешнее, гнездо под CR2032 | DB3659 |
Схемы платы в документах нет — только описание и файлы платы, поэтому
что к какому выводу идёт, взято из stm32wbxx_nucleo.h и с самой платой
не сверялось.
Чем WB55 отличается от опорных плат справочника — BluePill на F103 и Discovery на F407:
| F103 | F407 | STM32WB55RG | |
|---|---|---|---|
| ядро программы | Cortex-M3, 72 МГц | Cortex-M4F, 168 МГц | Cortex-M4F, до 64 МГц |
| второе ядро | нет | нет | Cortex-M0+, 32 МГц, только для радио |
| флеш / ОЗУ | 64 КБ / 20 КБ | 1 МБ / 192 КБ | 1 МБ / 256 КБ, часть занимает радиоядро |
| радио | нет | нет | 2,4 ГГц: Bluetooth LE 5.4 и 802.15.4 |
Частоты ядер и память WB55 — по DS11929. Радио: чувствительность −96 дБм для BLE на 1 Мбит/с, мощность до +6 дБм с шагом 1 дБ (DS11929, первая страница) — в разделе это не измерялось.
Что делает прибор:
- раз в 125 мс меряет АЦП датчик температуры кристалла и опорное напряжение VREFINT, раз в секунду считает среднее из 8 отсчётов;
- рекламирует себя именем
WB55-T198и номером сервиса 0x181A; - держит сервис Environmental Sensing с характеристикой Temperature (чтение и уведомления) и свой сервис со «светодиодом» (запись одного байта зажигает LED1);
- шлёт уведомление не каждую секунду, а когда температура ушла на порог или давно ничего не слал (198э.7).
Датчик кристалла выбран, чтобы не паять ничего: он меряет нагрев самого чипа, а не воздух. Для настоящей метеостанции его меняют на внешний датчик, как в 198м, — остальная часть прибора не меняется.
198э.2 Два ядра: кто за что отвечает
Стек — чужая прошивка. На ядре M0+ работает двоичная прошивка ST,
исходников у неё нет. У Flipper Zero в fbt_options.py выбран облегчённый
вариант:
COPRO_STACK_BIN = "stm32wb5x_BLE_Stack_light_fw.bin"
Кроме стека у радиоядра есть установщик FUS — он ставит, удаляет и
обновляет стек. Ядро M4 узнаёт, кто сейчас работает, из первого события
после запуска: в ble_glue.c Flipper сравнивает поле sysevt_ready_rsp с
WIRELESS_FW_RUNNING (работает стек) и FUS_FW_RUNNING (работает
установщик). Команды установщику — SHCI_C2_FUS_FwUpgrade,
SHCI_C2_FUS_FwDelete, SHCI_C2_FUS_StartWs — там же.
Защищённая память. Стек лежит в верхней части флеша, и ядро M4 его не
видит. Где начинается защищённая часть, говорит байт настройки SFSA —
номер первой 4-килобайтной страницы (RM0434, 3.4.1, адрес 0x1FFF 8070).
Так же устроено ОЗУ: поля SBRSA и SNBRSA (адрес 0x1FFF 8078) задают
начало защищённых частей SRAM2a и SRAM2b с шагом 1 КБ. Как это выглядит со
стороны прошивки Flipper, разобрано в 94б.2.
Память на двоих. По DS11929 у WB55 три блока ОЗУ: SRAM1 до 192 КБ с
адреса 0x2000 0000, SRAM2a — 32 КБ с 0x2003 0000 (сохраняется в Standby)
и SRAM2b — 32 КБ с 0x2003 8000. Сообщения между ядрами лежат в SRAM2a.
Шаблон компоновки ST для ядра M4 (stm32wb55xx_flash_cm4.ld) делит
память так:
| Область | Начало | Размер | Что там |
|---|---|---|---|
FLASH |
0x0800 0000 | 512K | программа M4; выше — место под стек радиоядра |
RAM1 |
0x2000 0008 | 0x2FFF8 | данные и стек M4 |
RAM_SHARED |
0x2003 0000 | 10K | MAPPING_TABLE, MB_MEM1, MB_MEM2 — общая с M0+ память |
Как радиоядро находит эту таблицу? В байтах настройки есть поле
IPCCDBA (адрес 0x1FFF 8068): смещение начала буфера в SRAM2 в двойных
словах. Его заводское значение — 0 (RM0434, 3.4.1), то есть начало
SRAM2a — ровно туда шаблон кладёт MAPPING_TABLE. Поэтому в своих
сценариях компоновки эту секцию держат первой.
Запуск второго ядра. После сброса M0+ стоит. Он стартует, только
когда ядро M4 поставит бит C2BOOT в PWR_CR4 (RM0434, 2.4). Flipper
перед этим посылает событие — на случай, если бит уже поставил загрузчик
и повторная запись ничего не даст (комментарий в hw_ipcc.c). Своя
функция раздела делает то же:
void cpu2_boot(void)
{
__SEV();
__WFE();
PWR->CR4 |= PWR_CR4_C2BOOT;
}
Порядок запуска в живом приборе — по ble_glue.c и ble_app.c
Flipper: общая таблица и очереди (TL_Init, shci_init, TL_MM_Init,
TL_Enable), запуск ядра, ожидание события «готов», настройка стека
SHCI_C2_BLE_Init и только потом GAP, GATT, свои сервисы и реклама.
Параметры стека — одна структура; Flipper проверяет её размер
_Static_assert(sizeof(SHCI_C2_Ble_Init_Cmd_Packet_t) == 58, …) с
припиской, что поля сверены со стеком v1.17.3. Сколько чего заказано у
Flipper (app_conf.h):
| Параметр | Значение | Что это |
|---|---|---|
CFG_BLE_NUM_LINK |
2 | одновременных подключений |
CFG_BLE_NUM_GATT_SERVICES |
8 | сервисов; по комментарию — «2 плюс свои», GAP и GATT стек добавляет сам |
CFG_BLE_NUM_GATT_ATTRIBUTES |
68 | атрибутов без объявлений сервисов; «9 плюс свои» |
CFG_BLE_MAX_ATT_MTU |
256 + 128 + 16 + 8 + 4 + 2 = 414 | наибольший MTU |
CFG_BLE_ATT_VALUE_ARRAY_SIZE |
1344 | байт под все значения |
По тем же правилам нашему прибору хватит 2 + 2 = 4 сервисов и 9 + 5 = 14 атрибутов: у температуры объявление, значение и подписка, у светодиода — объявление и значение.
198э.3 IPCC: шесть флагов в каждую сторону
IPCC не передаёт данные. Он только поднимает флаги: «я положил сообщение»
и «я его забрал». Сами сообщения лежат в общей памяти, и где именно,
блок не знает (RM0434, 37.1). Каналов шесть в каждую сторону, у каждого
канала — флаг CHnF: 1 — занят, данные можно читать получателю, 0 —
свободен, можно писать отправителю.
Регистры со стороны ядра M4 (stm32wb55xx.h: IPCC_C1 — с адреса
IPCC_BASE, IPCC_C2 — на 0x10 дальше):
| Регистр | Смещение | Биты | Что делает |
|---|---|---|---|
IPCC_C1CR |
0x000 | 16 TXFIE, 0 RXOIE |
разрешить прерывания «свободен» и «занят» |
IPCC_C1MR |
0x004 | 21:16 CHnFM, 5:0 CHnOM |
маски по каналам; после сброса 0xFFFF FFFF — всё закрыто |
IPCC_C1SCR |
0x008 | 21:16 CHnS, 5:0 CHnC |
занять свой канал; освободить канал от M0+; читается как 0 |
IPCC_C1TOC2SR |
0x00C | 5:0 CHnF |
флаги каналов M4 → M0+ |
IPCC_C2TOC1SR |
0x01C | 5:0 CHnF |
флаги каналов M0+ → M4 |
Писать в регистры IPCC можно только словами по 32 бита: байт или
полуслово не сработают и даже не вызовут ошибку шины (RM0434, 37.4).
У ядра M4 два прерывания: IPCC_C1_RX_IRQn = 44 («какой-то канал от M0+
занят и не замаскирован») и IPCC_C1_TX_IRQn = 45 («какой-то мой канал
освободился и не замаскирован»).
Режимов у канала два (RM0434, 37.3.2–37.3.3):
- симплекс — отправитель кладёт данные и ставит
CHnS; получатель читает и снимает флагCHnC; так в модели ходят события от M0+; - полудуплекс — получатель, прочитав команду, пишет ответ в тот же
буфер и только потом снимает флаг; для отправителя «канал свободен» и
значит «ответ готов». Так в модели ходят команды. У Flipper так же:
HW_IPCC_Tx_Handlerразбирает ответ системного канала по прерыванию «свободен».
Отправка команды и ожидание ответа — ipcc_cmd из ipcc198e.c:
int ipcc_cmd(const uint8_t *msg, uint8_t *rsp, unsigned rsp_max)
{
unsigned len = 2u + msg[1];
if (len > MSG_MAX)
return -2;
if (ipcc_hw_tx_flags() & CH_BIT(CH_CMD))
return -1; /* канал ещё занят: буфер трогать нельзя */
memcpy(mem->cmd, msg, len);
cmd_wait = 1;
ipcc_hw_scr(CH_BIT(CH_CMD) << 16); /* CHnS: «занят» — у M0+ прерывание RX occupied */
mr_set(0, CH_BIT(CH_CMD) << 16); /* открыть «свободен» (CHnFM = 0): это будет ответ */
for (unsigned n = 0; cmd_wait; n++) {
if (n >= IPCC_WAIT_MAX)
return -3; /* радиоядро молчит */
ipcc_hw_wait();
}
unsigned rl = 2u + mem->cmd[1];
if (rl > rsp_max || mem->cmd[0] != (msg[0] | RSP_FLAG))
return -4;
memcpy(rsp, mem->cmd, rl);
return (int)rl;
}
Порядок важен: сначала данные в память, потом флаг. Маску «свободен»
открываем после постановки флага — иначе прерывание сработает сразу, ведь
до CHnS канал был свободен. Обработчики двух прерываний:
void ipcc_tx_isr(void)
{
uint32_t mr = ipcc_hw_mr();
uint32_t freed = ~ipcc_hw_tx_flags() & ~(mr >> 16) & 0x3Fu;
if (freed & CH_BIT(CH_CMD)) {
ipcc_hw_set_mr(mr | (CH_BIT(CH_CMD) << 16)); /* закрыть «свободен», иначе прерывание не кончится */
cmd_wait = 0;
}
}
void ipcc_rx_isr(void)
{
uint32_t mr = ipcc_hw_mr();
uint32_t busy = ipcc_hw_rx_flags() & ~mr & 0x3Fu;
if (!(busy & CH_BIT(CH_EVT)))
return;
unsigned depth = evq_head - evq_tail;
if (depth >= EVQ_LEN) { /* очередь полна: событие оставить в канале */
ipcc_hw_set_mr(mr | CH_BIT(CH_EVT)); /* CHnOM = 1 — M0+ подождёт, ничего не теряется */
return;
}
unsigned len = 2u + mem->evt[1];
memcpy(evq[evq_head % EVQ_LEN], mem->evt, len > MSG_MAX ? MSG_MAX : len);
evq_head++;
if (depth + 1u > ipcc_evq_max)
ipcc_evq_max = depth + 1u;
ipcc_hw_scr(CH_BIT(CH_EVT)); /* CHnC: забрали — канал свободен для M0+ */
}
Событие в обработчике только копируется в очередь на 8 сообщений, а разбирает его главный цикл. Канал при этом сразу освобождается, и M0+ может класть следующее. Если очередь полна, канал остаётся занятым, а прерывание «занят» закрывается маской: радиоядро подождёт, пока главный цикл не разгребёт очередь, и событие не пропадёт.
Драйвер не трогает регистры напрямую: все обращения — через семь
функций ipcc_hw_*. На кристалле они пишут в IPCC_C1->SCR и соседей
(ipcc_hw_wb198e.c), на компьютере — в модель (ipcc_model198e.c). В
модели линия прерывания — уровень, как в кристалле: пока условие держится,
обработчик вызывается снова.
Опыты с моделью IPCC:
| Что | Результат |
|---|---|
| старт приложения: 5 команд радиоядру | 5 ответов, 10 шагов модели, 5 вызовов обработчиков |
| 3 команды подряд через драйвер | выполнены 3 из 3 |
| 3 команды подряд «в лоб», без проверки флага | выполнена 1 из 3 — последняя, значение 0x0912 |
| обработчик «свободен» не закрывает маску | команда выполнена, но 1000 вызовов за шаг — «буря»; и так на каждом из 10 следующих шагов |
| 12 событий подряд, главный цикл занят | у M4 в очереди 8, у M0+ ждут 12, дошло 9; после разбора — 12 из 12, потеряно 0 |
| радиоядро не запущено | ipcc_cmd вернул −3 после 1000 ожиданий |
Отправка «в лоб» из опыта — так писать нельзя:
for (int i = 0; i < 3; i++) {
uint8_t m[7] = {CMD_UPDATE, 5, (uint8_t)ts.ch_temp, (uint8_t)(ts.ch_temp >> 8), 2, (uint8_t)(0x10 + i), 0x09};
memcpy(shm.cmd, m, sizeof m); /* не глядя на CH1F */
ipcc_hw_scr(CH_BIT(CH_CMD) << 16);
}
Повторная запись CHnS в уже занятый канал ничего не делает: флаг один.
Радиоядро увидело одну команду и прочитало буфер, в котором к тому
времени лежала последняя. Две команды пропали без следа и без ошибки.
198э.4 Команды радиоядру: своя модель и что на её месте у ST
Приложению нужно от радиоядра четыре вещи: завести сервис, завести
характеристику, обновить значение и включить рекламу. В обратную сторону
идут четыре события: подключились, отключились, клиент записал атрибут,
договорились о MTU. В модели это свои сообщения (msg198e.h): байт кода,
байт длины, параметры младшим байтом вперёд. У ST — команды ACI и
события HCI поверх своей транспортной библиотеки. Соответствие — по тому,
что вызывает и разбирает Flipper:
Модель (msg198e.h) |
Стек ST, как у Flipper | Где у Flipper |
|---|---|---|
CMD_ADD_SERVICE |
aci_gatt_add_service |
furi_ble/gatt.c |
CMD_ADD_CHAR |
aci_gatt_add_char (+ aci_gatt_add_char_desc) |
furi_ble/gatt.c |
CMD_UPDATE |
aci_gatt_update_char_value |
furi_ble/gatt.c |
CMD_ADVERTISE |
aci_gap_set_discoverable |
gap.c |
EVT_CONNECTED |
HCI_LE_CONNECTION_COMPLETE_SUBEVT_CODE |
gap.c |
EVT_DISCONNECTED |
HCI_DISCONNECTION_COMPLETE_EVT_CODE |
gap.c |
EVT_ATTR_WRITTEN |
ACI_GATT_ATTRIBUTE_MODIFIED_VSEVT_CODE |
services/serial_service.c |
EVT_MTU |
ACI_ATT_EXCHANGE_MTU_RESP_VSEVT_CODE |
gap.c |
Приложение (tsvc198e.c) зовёт только bleif_* и не знает, что под ними.
Чтобы перенести его на стек ST, переписывается один bleif198e.c — сами
функции ACI берутся из пакета STM32CubeWB (у Flipper он подключён как
lib/stm32wb_copro, версия 1.20.0 в fbt_options.py). С пакетом ST
раздел не собирался.
У Flipper есть важная мелочь про номера атрибутов. Обработчик записи в
serial_service.c различает, куда писал клиент, так:
if(attribute_modified->Attr_Handle ==
serial_svc->chars[SerialSvcGattCharacteristicRx].handle + 2) {
То есть aci_gatt_add_char возвращает номер объявления характеристики,
значение лежит на номер дальше, описатель — на два. Модель раздаёт номера
так же, и приложение пользуется тем же правилом: подписка на температуру —
ts.ch_temp + 2, значение светодиода — ts.ch_led + 1.
Заводит сервисы tsvc_init:
int tsvc_init(uint16_t thr_c100, uint16_t heartbeat_s)
{
int r = 0;
memset(&ts, 0, sizeof ts);
ts.thr = thr_c100;
ts.heartbeat = heartbeat_s;
ts.mtu = 23;
r |= bleif_add_service(U_ESS, 2, 4, &ts.svc_ess); /* объявление + характеристика из трёх */
r |= bleif_add_char(ts.svc_ess, U_TEMP, 2, 0x02 | 0x10, 2, 1, 0, &ts.ch_temp); /* чтение, уведомление */
r |= bleif_add_service(U_CTL, 16, 3, &ts.svc_ctl);
r |= bleif_add_char(ts.svc_ctl, U_LED, 16, 0x02 | 0x08, 1, 1, 1, &ts.ch_led); /* чтение, запись */
r |= bleif_advertise(TS_ADV_MIN, TS_ADV_MAX, TS_NAME);
return r;
}
Третий параметр bleif_add_service — сколько номеров занимает сервис,
как Max_Attribute_Records у ST. Сервису температуры нужно 4: объявление
сервиса, объявление характеристики, значение и подписка. Flipper для
батареи с двумя характеристиками заказывает 8 при нужных 7. В модели
сервис занимает ровно столько номеров, сколько заказано; как раздаёт
номера сам стек ST, здесь не сверялось. Свойства 0x02 (чтение), 0x08
(запись) и 0x10 (уведомление) — биты объявления характеристики, как
BLE_GATT_CHR_PROP_* в ble_gatt.h NimBLE.
198э.5 Таблица прибора и реклама
После старта в модели 16 атрибутов. Первые два сервиса заводит само
радиоядро, как и стек ST (aci_gap_init и aci_gatt_init у Flipper):
| Номер | Что | UUID |
|---|---|---|
| 0x0001–0x0005 | сервис GAP: имя (чтение), внешний вид (чтение) | 0x1800; 0x2A00, 0x2A01 |
| 0x0006–0x0009 | сервис GATT: Service Changed (индикация) и его подписка | 0x1801; 0x2A05, 0x2902 |
| 0x000A–0x000D | Environmental Sensing: объявление 0x000B, значение 0x000C, подписка 0x000D | 0x181A; 0x2A6E, 0x2902 |
| 0x000E–0x0010 | свой сервис: объявление 0x000F, значение «светодиода» 0x0010 | 128-битные |
Номера 16-битных UUID сверены с заголовками: 0x1800, 0x2A00, 0x2A01 —
ble_svc_gap.h, 0x1801 — ble_gatt.h, 0x2A05 — ble_svc_gatt.h,
0x2902 — ble_gatt.h NimBLE; 0x181A — esp_gatt_defs.h, 0x2A6E —
mesh/uuid.h ESP-IDF. Значение характеристики температуры — sint16 в
сотых градуса младшим байтом вперёд: 23,00 °C — это fc 08. Так это
значение описывает реестр характеристик Bluetooth SIG; с самим реестром
текст не сверялся, а раздел пользуется этим форматом и на стороне прибора,
и на стороне «телефона».
128-битный UUID свой: сервис 7a1e0198-3c55-4b0e-9d1f-2f5ab3c0e101,
светодиод — тот же номер с …e102 на конце. В эфире все 16 байт идут
задом наперёд, от последнего к первому:
static const uint8_t U_CTL[16] = {0x01, 0xe1, 0xc0, 0xb3, 0x5a, 0x2f, 0x1f, 0x9d,
0x0e, 0x4b, 0x55, 0x3c, 0x98, 0x01, 0x1e, 0x7a};
Так же записаны UUID в serial_service_uuid.inc у Flipper. Это не тот
порядок, что у GUID в Windows или uuid.bytes_le в Python: там
переставлены только первые три поля, а остальное идёт как в строке.
Реклама — 18 байт из 31 возможного (BLE_HCI_MAX_ADV_DATA_LEN в
hci_common.h NimBLE):
02 01 06 0a 09 57 42 35 35 2d 54 31 39 38 03 03 1a 18
Три поля по схеме «длина, тип, данные»: флаги 0x06 (общий поиск, без
классического Bluetooth), полное имя WB55-T198 (тип 0x09) и полный
список 16-битных сервисов — 0x181A (тип 0x03). Типы — как
BLE_HS_ADV_TYPE_* в ble_hs_adv.h. Список сервисов в рекламе нужен,
чтобы телефон мог искать приборы по сервису, не подключаясь. Запас
небольшой: имя из 22 символов модель ещё принимает, из 23 — отказ
ST_BAD_PARAM, реклама не влезает в 31 байт. Длинное имя переносят в
ответ на запрос сканера — Flipper кладёт туда свои данные функцией
hci_le_set_scan_response_data.
Интервал рекламы — 0x0080…0x00A0 в шагах 0,625 мс, то есть 80…100 мс.
Это быстрый режим Flipper (gap.c); после него Flipper переходит на
медленный — 0x0640…0x0FA0, 1000…2500 мс. Сколько тока это стоит, в
разделе не мерялось.
198э.6 Что видит телефон: пакеты ATT
После подключения телефон ничего не знает о приборе и обходит таблицу. «Телефон» в модели делает это так же, как типичный клиент GATT: один запрос, ответ, следующий запрос. Первые шесть пакетов из журнала прогона (C> — запрос клиента, S< — ответ сервера):
C> 10 01 00 ff ff 00 28
S< 11 06 01 00 05 00 00 18 06 00 09 00 01 18 0a 00 0d 00 1a 18
C> 10 0e 00 ff ff 00 28
S< 11 14 0e 00 10 00 01 e1 c0 b3 5a 2f 1f 9d 0e 4b 55 3c 98 01 1e 7a
C> 10 11 00 ff ff 00 28
S< 01 10 11 00 0a
Разбор:
10— Read By Group Type: «дай сервисы от 0x0001 до 0xFFFF», тип 0x2800 — первичный сервис.11 06 …— ответ, каждая запись по 6 байт: начало, конец, 16-битный UUID. Три сервиса сразу: 0x0001–0x0005 GAP, 0x0006–0x0009 GATT, 0x000A–0x000D 0x181A.- Свой 128-битный сервис в этот ответ не попал: в одном ответе все записи одной длины. Телефон спрашивает дальше с 0x000E и получает запись в 20 байт.
01 10 11 00 0a— Error Response на запрос10с номером 0x0011, код 0x0A «не найдено»: сервисы кончились. Ошибка здесь — обычный конец поиска, а не сбой.
Дальше то же для характеристик (Read By Type 08 с типом 0x2803 внутри
каждого сервиса) и для описателей (Find Information 04) — только у тех
характеристик, что умеют уведомлять. Объявление температуры в ответе
выглядит как 0b 00 12 0c 00 6e 2a: номер 0x000B, свойства 0x12 (чтение
и уведомление), значение в 0x000C, UUID 0x2A6E. Коды операций и ошибок —
как BLE_ATT_OP_* и BLE_ATT_ERR_* в ble_att.h NimBLE, форматы ответов
Find Information (1 — 16-битные типы, 2 — 128-битные) — как в
ble_att_cmd_priv.h.
| Обход таблицы | Запросов | Байт от телефона | Байт от прибора |
|---|---|---|---|
| MTU 23, без обмена MTU | 13 | 87 | 136 |
| сначала обмен MTU 247 | 14 (13 + обмен) | 90 | 139 |
Для такой маленькой таблицы MTU ничего не меняет: все ответы и так короче 23 байт, а 16-битные и 128-битные записи в один ответ не складываются. MTU важен для длинных значений — раздел 158.5. Сервер модели готов принять 247 байт, телефон в модели просит столько же.
Чтение, запись и ошибки — из того же журнала:
| Запрос | Ответ |
|---|---|
| чтение 0x0003 (имя) | 0b и WB55-T198 |
| чтение 0x000C до первого измерения | 0b 00 00 |
| чтение 0x0030 — такого номера нет | ошибка 0x01, Invalid Handle |
| запись в значение температуры 0x000C | ошибка 0x03, Write Not Permitted |
| запись 3 байт в подписку 0x000D | ошибка 0x0D, Invalid Attribute Value Length |
| запись 2 байт в «светодиод» (нужен 1) | ошибка 0x0D |
| Read By Group Type с типом 0x2803 | ошибка 0x10, Unsupported Group Type |
| код 0x0E (Read Multiple), модель его не знает | ошибка 0x06, Request Not Supported |
Write Command 52 с 2 байтами в «светодиод» |
ответа нет, события у M4 нет |
запись 01 в «светодиод» 0x0010 |
13 — Write Response; у M4 событие, LED1 горит |
Предпоследняя строка — ловушка: у команды записи без ответа (52) ответа
нет вообще, и ошибочная запись просто теряется. Телефон не узнает, что
«светодиод» не включился.
198э.7 Подписка, уведомления и температура кристалла
Подписка. Клиент пишет 01 00 в описатель 0x000D. Пока этого нет,
обновления значения в эфир не уходят — сервер модели проверяет подписку
сам:
int att_update(uint16_t char_handle, const uint8_t *val, unsigned len)
{
struct attr *d = at(char_handle), *v = at((uint16_t)(char_handle + 1u));
if (!d || !is16(d, UUID_CHARACTERISTIC) || !v || len > v->max_len)
return -2;
memcpy(v->val, val, len);
v->len = (uint8_t)len;
struct attr *c = at((uint16_t)(char_handle + 2u));
if (c && is16(c, UUID_CCCD) && (get16(c->val) & 1u)) { /* клиент подписан на уведомления */
uint8_t p[ATT_MTU_SERVER];
unsigned n = len < mtu - 3u ? len : mtu - 3u; /* уведомление не длиннее MTU − 3 */
p[0] = OP_NOTIFY;
put16(&p[1], (uint16_t)(char_handle + 1u));
memcpy(&p[3], val, n);
tx(p, 3u + n);
att_notifs++;
}
return 0;
}
В прогоне: 5 обновлений до подписки — 0 уведомлений; после записи
01 00 — уведомление 1b 0c 00 fc 08 (код 0x1B, номер 0x000C,
2300 сотых — 23,00 °C); после записи 00 00 — снова тишина. После
разрыва связи и нового подключения подписка в модели сброшена и
уведомлений нет, пока телефон не подпишется снова. Хранит ли стек ST
подписку связанного (bonded) телефона между подключениями — не проверено.
Температура кристалла. Формула — RM0434, 16.4.30: линейная
интерполяция между заводскими точками TS_CAL1 (30 °C) и TS_CAL2
(130 °C). Адреса калибровок — DS11929, таблицы 11 и 12: TS_CAL1 —
0x1FFF 75A8, TS_CAL2 — 0x1FFF 75CA, VREFINT — 0x1FFF 75AA. Подвох в том,
что датчик калибровали при VREF+ = 3,0 В, VREFINT — при 3,6 В, а
питание на плате обычно другое — в опытах 3,3 В (по схеме Nucleo это не
сверялось). RM прямо требует пересчитать код датчика к питанию
калибровки. Само питание узнаём по VREFINT:
int32_t tsvc_temp_c100(uint32_t ts_data, uint32_t vref_data, uint16_t ts_cal1, uint16_t ts_cal2, uint16_t vref_cal)
{
int64_t num = ((int64_t)ts_data * vref_cal * 36 - (int64_t)ts_cal1 * vref_data * 30) * (130 - 30) * 100;
int64_t den = (int64_t)(ts_cal2 - ts_cal1) * vref_data * 30;
if (den <= 0)
return INT32_MIN;
num += num >= 0 ? den / 2 : -den / 2; /* округление до ближайшего */
return (int32_t)(num / den) + 30 * 100;
}
Формула зависит только от отношения кода датчика к коду VREFINT, поэтому в неё можно подавать сразу суммы 8 отсчётов — точность от этого только выше. Модель АЦП в прогоне взята по типовым значениям DS11929, таблица 85 и раздел про VREFINT: 0,76 В при 30 °C, 2,5 мВ/°C, VREFINT 1,212 В. Заводские коды модели вышли 1037, 1379 и 1379. Что получилось:
| Опыт | Результат |
|---|---|
| −20…85 °C через 5 °C, питание 3,3 В, без шума | ошибка не больше 0,42 °C |
| один шаг АЦП при 3,3 В | 0,322 °C |
| забыть про питание (считать, что 3,0 В), 25 °C | −1,87 °C, ошибка −26,87 °C |
| питание 3,0 В, 25 °C | 25,17 °C |
Один шаг АЦП — треть градуса: 3,3 В / 4095 = 0,806 мВ, а датчик даёт 2,5 мВ на градус. Отсюда и среднее из 8 отсчётов, и порог в правиле уведомлений.
Правило уведомлений — tsvc_second, раз в секунду:
void tsvc_second(int32_t t)
{
ts.sec++;
if (t > 32767)
t = 32767;
if (t < -32768)
t = -32768;
int32_t d = t > ts.sent ? t - ts.sent : ts.sent - t;
if (!ts.subscribed) {
if (t != ts.sent || ts.updates == 0)
send(t);
} else if (ts.force || d >= ts.thr || ts.sec - ts.sent_at >= ts.heartbeat) {
ts.force = 0;
send(t);
}
}
Без подписки значение просто держится свежим для чтения — в эфир оно не
уходит. С подпиской уведомление идёт, если температура ушла на порог или
с прошлого прошло heartbeat секунд — тогда телефон отличит «ничего не
меняется» от «прибор пропал». Сразу после подписки — одно уведомление
вне очереди (force).
Опыт — 600 с: 120 с при 22 °C, за 240 с рост до 30 °C, 240 с при 30 °C; шум АЦП в модели — случайный ±2 шага на каждый отсчёт; телефон подписывается на 10-й секунде. Ошибка — расстояние от последнего полученного телефоном значения до настоящей температуры:
| Порог | Раз в | Шум | Уведомлений | Ошибка: наибольшая / средняя |
|---|---|---|---|---|
| 0 (каждую секунду) | 10 с | ±2 | 590 | 0,79 / 0,213 °C |
| 0,10 °C | 10 с | ±2 | 433 | 0,81 / 0,216 °C |
| 0,50 °C | 10 с | ±2 | 88 | 0,76 / 0,196 °C |
| 0,50 °C | 10 с | нет | 59 | 0,33 / 0,132 °C |
| 1,00 °C | 10 с | ±2 | 59 | 0,60 / 0,198 °C |
| 0,50 °C | 60 с | ±2 | 61 | 0,65 / 0,233 °C |
Порог 0,10 °C ниже шума: 433 уведомления, почти столько же, сколько при отправке каждую секунду, а точнее не стало. Порог 0,50 °C срезает уведомления в 6,7 раза (590 → 88) при той же ошибке. При пороге 1,00 °C уведомлений 59 — это ровно «раз в 10 секунд» за 590 секунд: порог почти не срабатывает, работает только напоминание.
198э.8 Прошивка ядра M4 на регистрах и размеры
fw198e.c — прошивка без HAL на CMSIS: такт MSI 4 МГц, как после сброса
(RM0434, 8.2), SysTick на 1 мс, LED1 на PB5, консоль USART1 на PB6, АЦП.
Свой начальный код и таблица векторов на 16 + 46 записей — до
IPCC_C1_TX_IRQn = 45 включительно. АЦП включается по шагам RM0434,
16.4.6–16.4.9:
static void adc_init(void)
{
RCC->AHB2ENR |= RCC_AHB2ENR_ADCEN;
(void)RCC->AHB2ENR;
ADC1_COMMON->CCR = ADC_CCR_CKMODE_0 | ADC_CCR_TSEN | ADC_CCR_VREFEN; /* HCLK/1, датчик и VREFINT */
ADC1->CR &= ~ADC_CR_DEEPPWD;
ADC1->CR |= ADC_CR_ADVREGEN;
delay_ms(1); /* tADCVREG_STUP — до 20 мкс (DS11929) */
ADC1->CR |= ADC_CR_ADCAL;
while (ADC1->CR & ADC_CR_ADCAL)
;
ADC1->SMPR1 = 4u << ADC_SMPR1_SMP0_Pos; /* 47,5 такта = 11,9 мкс при 4 МГц */
ADC1->SMPR2 = 4u << ADC_SMPR2_SMP17_Pos; /* датчику нужно не меньше 5 мкс */
ADC1->ISR = ADC_ISR_ADRDY;
ADC1->CR |= ADC_CR_ADEN;
while (!(ADC1->ISR & ADC_ISR_ADRDY))
;
}
Датчик — канал 17 (RM0434: ADC1_INP17), VREFINT — канал 0. Время
выборки для датчика — не меньше 5 мкс (tS_temp, DS11929, таблица 85);
47,5 такта при 4 МГц — 11,9 мкс. Такт АЦП берётся прямо от шины
(CKMODE = 01). RM0434 (16.4.3) разрешает это, когда в HPRE записано
0xxx; после сброса RCC_CFGR = 0x0007 0000, то есть HPRE = 0000 (8.4.3).
Сборок две:
- FW_UART — без радиоядра:
bleif_*печатают значения в консоль. Её можно залить в плату и смотреть температуру в терминале ST-LINK на 115 200 бит/с (делитель 35 даёт 114 286 бит/с, −0,8 %); - FW_IPCC — с драйвером IPCC и командами
bleif198e.c, общая памятьshm— в секции.shmпо адресу 0x2003 0000. Она собрана только ради размера: настоящее радиоядро с прошивкой ST таких сообщений не понимает.
Размеры (arm-none-eabi-gcc 12.2.1, -Os, Cortex-M4 без FPU):
| Что | Код, байт | ОЗУ, байт |
|---|---|---|
tsvc198e.o — сервис и формула |
630 | 52 |
bleif198e.o — команды |
398 | 0 |
ipcc198e.o — драйвер |
496 | 529 (очередь 8 × 64) |
ipcc_hw_wb198e.o — регистры, запуск M0+ |
162 | 0 |
| прошивка FW_UART целиком | 2800 + 4 данных | 56 |
| прошивка FW_IPCC целиком | 4056 | 716 + 128 в SRAM2a |
Из функций: ipcc_cmd — 176 байт, ipcc_rx_isr — 132, ipcc_tx_isr —
40, tsvc_temp_c100 — 148, tsvc_second — 80. Деление 64-битных чисел в
формуле тянет из библиотеки __aeabi_ldivmod. Образ FW_UART в .bin —
2804 байта. На фоне 512 КБ, отданных программе, это ничто: место в
приборе на WB55 занимает стек радиоядра и то, что ему заказано в ОЗУ.
Как прошить: FW_UART — как любую программу, через ST-LINK (раздел 35б), с адреса 0x0800 0000. Стек радиоядра ставится отдельно, через установщик FUS, и приложение его не трогает; на плате это в разделе не делалось.
198э.9 Грабли
Команды пропадают без ошибки. Запись в общую память, пока канал ещё
занят, затирает непрочитанную команду, а повторный CHnS ничего не
добавляет. В опыте из трёх команд выполнилась одна — последняя. Перед
записью — проверка CHnF, после — ожидание ответа.
Прерывание «свободен» не кончается. Канал свободен почти всё время, и
если обработчик не закрыл маску CHnFM, линия держится. В модели — 1000
вызовов за шаг на каждом шаге; в кристалле программа дальше обработчика
не уйдёт. Маску открывают только на время ожидания ответа.
Маска — общая для главного цикла и обработчиков. IPCC_C1MR меняют
оба. Чтение-изменение-запись из главного цикла без запрета прерываний
может затереть то, что успел сделать обработчик; в драйвере это mr_set
под __disable_irq. В модели прерывания вызываются только между шагами,
поэтому эта гонка в прогоне не воспроизводится.
Радиоядро молчит. Не поставлен C2BOOT, не установлен стек, или
работает установщик FUS вместо стека. Ответа не будет никогда, поэтому у
ожидания — предел: в модели ipcc_cmd вернул −3 после 1000 ожиданий.
Первое событие после запуска говорит, кто работает: стек или установщик.
Общая память не там. Радиоядро ищет таблицу по полю IPCCDBA
(заводское значение 0 — начало SRAM2a). Если свой сценарий компоновки
положит туда что-то другое, M0+ прочитает мусор.
Подписка «пропала» после переподключения. Без связывания подписка
живёт одно подключение: в модели после разрыва уведомлений нет, пока
телефон снова не запишет 01 00. Приложение сбрасывает свой флаг по
событию «отключились».
Температура −2 °C в комнате. Калибровки сняты при 3,0 В, а питание 3,3 В: без пересчёта по VREFINT ошибка в модели — 26,87 °C.
Порог ниже шума. Шаг АЦП — 0,32 °C, порог 0,10 °C срабатывает от шума: 433 уведомления из 590 возможных. Порог берут больше шума после усреднения.
Байты UUID перевёрнуты не так. В эфире весь 128-битный номер идёт
задом наперёд. Python uuid.bytes_le и GUID в Windows переставляют
только первые три поля — номер с такими байтами телефон не узнает.
Write Command молча теряется. На запись без ответа (52) с неверной
длиной сервер ничего не отвечает, приложение ничего не получает. Для
команд, которые важно выполнить, — запись с ответом (12).
Длинное имя не влезает в рекламу. С флагами и номером сервиса в 31 байт входит имя до 22 символов. Длиннее — в ответ на запрос сканера.
198э.10 Итоги
- У STM32WB55 два ядра: Cortex-M4 до 64 МГц для программы и Cortex-M0+
на 32 МГц со стеком BLE от ST; стек — двоичная прошивка в защищённой
части флеша (
SFSA), ставится установщиком FUS. M0+ стартует только послеC2BOOTвPWR_CR4. - Общая память — SRAM2a с 0x2003 0000: шаблон ST отдаёт ей 10 КБ, программе
— 512 КБ флеша. Радиоядро находит таблицу по
IPCCDBA, по умолчанию — начало SRAM2a. - IPCC — 6 флагов в каждую сторону, маски и два прерывания у M4 (44 и 45); данных он не передаёт. Команда с ответом — полудуплекс, события — симплекс; регистры — только словами по 32 бита.
- Опыты с моделью: 3 команды «в лоб» — выполнена 1; маска «свободен» не закрыта — 1000 вызовов за шаг; пачка из 12 событий при занятом главном цикле — 12 из 12 без потерь; нет радиоядра — −3 после 1000 ожиданий.
- Таблица прибора — 16 атрибутов: GAP 0x0001–0x0005, GATT 0x0006–0x0009, температура 0x000A–0x000D (значение 0x000C, подписка 0x000D), светодиод 0x000E–0x0010. Реклама — 18 байт из 31, интервал 80…100 мс.
- Обход таблицы — 13 запросов, 87 байт от телефона и 136 от прибора; обмен MTU 247 для такой таблицы ничего не меняет.
- Уведомление
1b 0c 00 fc 08— 23,00 °C; до подписки и после отписки — ни одного. - Датчик кристалла: шаг АЦП 0,322 °C при 3,3 В; без пересчёта по VREFINT ошибка 26,87 °C. Порог 0,50 °C и напоминание раз в 10 с — 88 уведомлений за 590 с вместо 590 при той же точности.
- Прошивка M4: FW_UART — 2800 байт кода, FW_IPCC — 4056 байт и 716 байт ОЗУ.
Полные исходники
Код ядра M4 целиком — тот, что собирается под STM32WB55 и прогоняется на компьютере, — и сценарий прогона. Модели для компьютера (сервер ATT, радиоядро, блок IPCC, «телефон» и сами опыты — att198e.c, cpu2_198e.c, ipcc_model198e.c, phone198e.c, host198e.c) лежат в каталоге проверки; их ключевые функции приведены выше.
msg198e.h — формат сообщений между ядрами в модели и раскладка общей памяти.
/* Раздел 198э: сообщения между ядрами в учебной модели — свой формат, НЕ формат библиотеки ST.
* Канал 1 (M4 → M0+) — полудуплекс: команда и ответ в одном буфере shm.cmd.
* Канал 2 (M0+ → M4) — симплекс: событие в буфере shm.evt.
* Сообщение: код, длина параметров, параметры (многобайтовые поля — младшим байтом вперёд). */
#ifndef MSG198E_H
#define MSG198E_H
#include <stdint.h>
#define CH_CMD 1u /* номер канала IPCC для команд */
#define CH_EVT 2u /* номер канала IPCC для событий */
#define MSG_MAX 64u
/* команды M4 → M0+ */
#define CMD_ADD_SERVICE 0x01 /* uuid_len, uuid[uuid_len], max_records → status, svc_handle[2] */
#define CMD_ADD_CHAR 0x02 /* svc[2], uuid_len, uuid, props, max_len, events → status, char_handle[2] */
#define CMD_UPDATE 0x03 /* char[2], len, data[len] → status */
#define CMD_ADVERTISE 0x04 /* itv_min[2], itv_max[2], name_len, name → status */
#define RSP_FLAG 0x80 /* в ответе к коду команды добавляется этот бит */
/* события M0+ → M4 */
#define EVT_CONNECTED 0x81 /* conn[2], interval[2] (шаг 1,25 мс) */
#define EVT_DISCONNECTED 0x82 /* conn[2], reason */
#define EVT_ATTR_WRITTEN 0x83 /* attr_handle[2], len, data[len] — клиент записал значение или подписку */
#define EVT_MTU 0x84 /* mtu[2] */
#define ST_OK 0x00
#define ST_NO_SPACE 0x01 /* в таблице атрибутов или в сервисе нет места */
#define ST_BAD_PARAM 0x02
#define ST_BAD_CMD 0x03
/* общая память: в настоящем кристалле — SRAM2a, видимая обоим ядрам */
struct shm198e {
uint8_t cmd[MSG_MAX]; /* команда, затем ответ на неё */
uint8_t evt[MSG_MAX]; /* событие */
};
#endif
ipcc198e.h — драйвер IPCC ядра M4: интерфейс и функции доступа к регистрам.
/* Раздел 198э: драйвер IPCC ядра M4 — команды с ответом по каналу CH_CMD и события по CH_EVT.
* Работа с регистрами — через ipcc_hw_* (на кристалле — ipcc_hw_wb198e.c, на компьютере — модель). */
#ifndef IPCC198E_H
#define IPCC198E_H
#include <stdint.h>
#include "msg198e.h"
#define CH_BIT(n) (1u << ((n) - 1u)) /* канал n = 1…6 → бит n − 1 */
#define IPCC_CR_RXOIE_BIT (1u << 0) /* IPCC_C1CR.RXOIE */
#define IPCC_CR_TXFIE_BIT (1u << 16) /* IPCC_C1CR.TXFIE */
#define IPCC_WAIT_MAX 1000u /* столько ожиданий ответа, потом отказ */
#define EVQ_LEN 8u /* очередь событий у M4 */
/* регистры IPCC со стороны процессора 1 (M4) */
uint32_t ipcc_hw_tx_flags(void); /* IPCC_C1TOC2SR: какие каналы M4 → M0+ заняты */
uint32_t ipcc_hw_rx_flags(void); /* IPCC_C2TOC1SR: какие каналы M0+ → M4 заняты */
void ipcc_hw_scr(uint32_t v); /* IPCC_C1SCR: биты 16+ — CHnS (занять), биты 0+ — CHnC (освободить) */
uint32_t ipcc_hw_mr(void); /* IPCC_C1MR */
void ipcc_hw_set_mr(uint32_t v);
void ipcc_hw_set_cr(uint32_t v); /* IPCC_C1CR */
void ipcc_hw_wait(void); /* ждать прерывания: __WFI на кристалле, шаг модели на компьютере */
void ipcc_init(struct shm198e *shm);
int ipcc_cmd(const uint8_t *msg, uint8_t *rsp, unsigned rsp_max);
int ipcc_next_event(uint8_t *out); /* 1 — событие скопировано в out, 0 — очередь пуста */
void ipcc_rx_isr(void); /* IPCC_C1_RX_IRQn = 44 */
void ipcc_tx_isr(void); /* IPCC_C1_TX_IRQn = 45 */
extern volatile unsigned ipcc_evq_max; /* наибольшая глубина очереди событий */
#endif
ipcc198e.c — драйвер IPCC: команда с ответом, очередь событий, обработчики прерываний.
/* Раздел 198э: драйвер IPCC ядра M4 по RM0434, раздел 37.
* Канал CH_CMD — полудуплекс: M4 кладёт команду и ставит CHnS; M0+ пишет ответ в тот же буфер и снимает
* флаг — у M4 срабатывает «канал свободен» (TX free), это и есть «ответ готов».
* Канал CH_EVT — симплекс: M0+ кладёт событие и ставит флаг; у M4 срабатывает «канал занят» (RX occupied),
* M4 копирует событие в свою очередь и снимает флаг CHnC — M0+ может класть следующее. */
#include "ipcc198e.h"
#include <string.h>
uint32_t ipcc_hw_irq_off(void); /* запретить прерывания, вернуть прежнее состояние */
void ipcc_hw_irq_restore(uint32_t s);
static struct shm198e *mem; /* общая память: SRAM2a на кристалле */
static volatile uint8_t cmd_wait; /* команда ушла, ответа ещё нет */
static uint8_t evq[EVQ_LEN][MSG_MAX];
static volatile unsigned evq_head, evq_tail;
volatile unsigned ipcc_evq_max;
/* IPCC_C1MR меняют и главный цикл, и обработчики — менять только под запретом прерываний */
static void mr_set(uint32_t set, uint32_t clr)
{
uint32_t s = ipcc_hw_irq_off();
ipcc_hw_set_mr((ipcc_hw_mr() | set) & ~clr);
ipcc_hw_irq_restore(s);
}
void ipcc_init(struct shm198e *s)
{
mem = s;
cmd_wait = 0;
evq_head = evq_tail = 0;
ipcc_evq_max = 0;
ipcc_hw_set_mr(0xFFFFFFFFu & ~CH_BIT(CH_EVT)); /* после сброса всё замаскировано; открыть «занят» событий */
ipcc_hw_set_cr(IPCC_CR_RXOIE_BIT | IPCC_CR_TXFIE_BIT);
}
int ipcc_cmd(const uint8_t *msg, uint8_t *rsp, unsigned rsp_max)
{
unsigned len = 2u + msg[1];
if (len > MSG_MAX)
return -2;
if (ipcc_hw_tx_flags() & CH_BIT(CH_CMD))
return -1; /* канал ещё занят: буфер трогать нельзя */
memcpy(mem->cmd, msg, len);
cmd_wait = 1;
ipcc_hw_scr(CH_BIT(CH_CMD) << 16); /* CHnS: «занят» — у M0+ прерывание RX occupied */
mr_set(0, CH_BIT(CH_CMD) << 16); /* открыть «свободен» (CHnFM = 0): это будет ответ */
for (unsigned n = 0; cmd_wait; n++) {
if (n >= IPCC_WAIT_MAX)
return -3; /* радиоядро молчит */
ipcc_hw_wait();
}
unsigned rl = 2u + mem->cmd[1];
if (rl > rsp_max || mem->cmd[0] != (msg[0] | RSP_FLAG))
return -4;
memcpy(rsp, mem->cmd, rl);
return (int)rl;
}
void ipcc_tx_isr(void)
{
uint32_t mr = ipcc_hw_mr();
uint32_t freed = ~ipcc_hw_tx_flags() & ~(mr >> 16) & 0x3Fu;
if (freed & CH_BIT(CH_CMD)) {
ipcc_hw_set_mr(mr | (CH_BIT(CH_CMD) << 16)); /* закрыть «свободен», иначе прерывание не кончится */
cmd_wait = 0;
}
}
void ipcc_rx_isr(void)
{
uint32_t mr = ipcc_hw_mr();
uint32_t busy = ipcc_hw_rx_flags() & ~mr & 0x3Fu;
if (!(busy & CH_BIT(CH_EVT)))
return;
unsigned depth = evq_head - evq_tail;
if (depth >= EVQ_LEN) { /* очередь полна: событие оставить в канале */
ipcc_hw_set_mr(mr | CH_BIT(CH_EVT)); /* CHnOM = 1 — M0+ подождёт, ничего не теряется */
return;
}
unsigned len = 2u + mem->evt[1];
memcpy(evq[evq_head % EVQ_LEN], mem->evt, len > MSG_MAX ? MSG_MAX : len);
evq_head++;
if (depth + 1u > ipcc_evq_max)
ipcc_evq_max = depth + 1u;
ipcc_hw_scr(CH_BIT(CH_EVT)); /* CHnC: забрали — канал свободен для M0+ */
}
int ipcc_next_event(uint8_t *out)
{
if (evq_head == evq_tail)
return 0;
const uint8_t *m = evq[evq_tail % EVQ_LEN];
memcpy(out, m, 2u + m[1]);
evq_tail++;
mr_set(0, CH_BIT(CH_EVT)); /* место есть — снова принимать события */
return 1;
}
ipcc_hw_wb198e.c — регистры IPCC на кристалле, включение блока, запуск ядра M0+.
/* Раздел 198э: регистры IPCC на STM32WB55 (CMSIS stm32wb55xx.h). Со стороны M4:
* IPCC_C1->CR/MR/SCR/SR — это IPCC_C1CR, C1MR, C1SCR, C1TOC2SR; IPCC_C2->SR — C2TOC1SR.
* Доступ к регистрам IPCC — только словами по 32 бита (RM0434, 37.4). */
#include "stm32wb55xx.h"
#include "ipcc198e.h"
uint32_t ipcc_hw_tx_flags(void) { return IPCC_C1->SR; }
uint32_t ipcc_hw_rx_flags(void) { return IPCC_C2->SR; }
void ipcc_hw_scr(uint32_t v) { IPCC_C1->SCR = v; }
uint32_t ipcc_hw_mr(void) { return IPCC_C1->MR; }
void ipcc_hw_set_mr(uint32_t v) { IPCC_C1->MR = v; }
void ipcc_hw_set_cr(uint32_t v) { IPCC_C1->CR = v; }
void ipcc_hw_wait(void) { __WFI(); }
uint32_t ipcc_hw_irq_off(void)
{
uint32_t s = __get_PRIMASK();
__disable_irq();
return s;
}
void ipcc_hw_irq_restore(uint32_t s) { __set_PRIMASK(s); }
/* включить блок IPCC и его прерывания у M4 */
void ipcc_hw_start(void)
{
RCC->AHB3ENR |= RCC_AHB3ENR_IPCCEN;
(void)RCC->AHB3ENR;
NVIC_SetPriority(IPCC_C1_RX_IRQn, 5);
NVIC_SetPriority(IPCC_C1_TX_IRQn, 5);
NVIC_EnableIRQ(IPCC_C1_RX_IRQn);
NVIC_EnableIRQ(IPCC_C1_TX_IRQn);
}
/* запустить ядро M0+: без C2BOOT после сброса оно не стартует (RM0434, 2.4). Как в hw_ipcc.c
* Flipper Zero, сначала событие SEV/WFE: если бит уже ставил загрузчик, повторная запись ничего не даст */
void cpu2_boot(void)
{
__SEV();
__WFE();
PWR->CR4 |= PWR_CR4_C2BOOT;
}
void IPCC_C1_RX_IRQHandler(void) { ipcc_rx_isr(); }
void IPCC_C1_TX_IRQHandler(void) { ipcc_tx_isr(); }
bleif198e.h — что приложение просит у радиоядра.
/* Раздел 198э: что приложение на M4 просит у радиоядра. С прошивкой ST этим функциям отвечают
* aci_gatt_add_service, aci_gatt_add_char, aci_gatt_update_char_value, aci_gap_set_discoverable. */
#ifndef BLEIF198E_H
#define BLEIF198E_H
#include <stdint.h>
int bleif_add_service(const uint8_t *uuid, unsigned uuid_len, unsigned max_records, uint16_t *handle);
int bleif_add_char(uint16_t svc, const uint8_t *uuid, unsigned uuid_len, uint8_t props, unsigned max_len,
uint8_t fixed_len, uint8_t events, uint16_t *char_handle);
int bleif_update(uint16_t char_handle, const uint8_t *val, unsigned len);
int bleif_advertise(uint16_t itv_min, uint16_t itv_max, const char *name);
#endif
bleif198e.c — команды радиоядру поверх драйвера IPCC.
/* Раздел 198э: команды радиоядру через IPCC — свой формат msg198e.h поверх драйвера ipcc198e.c. */
#include "bleif198e.h"
#include "ipcc198e.h"
#include <string.h>
static int call(uint8_t *m, uint16_t *handle)
{
uint8_t r[MSG_MAX];
int n = ipcc_cmd(m, r, sizeof r);
if (n < 3)
return n < 0 ? n : -5;
if (handle && n >= 5)
*handle = (uint16_t)(r[3] | (r[4] << 8));
return r[2]; /* код состояния: 0 — выполнено */
}
int bleif_add_service(const uint8_t *uuid, unsigned uuid_len, unsigned max_records, uint16_t *handle)
{
uint8_t m[MSG_MAX] = {CMD_ADD_SERVICE, (uint8_t)(2u + uuid_len), (uint8_t)uuid_len};
memcpy(&m[3], uuid, uuid_len);
m[3 + uuid_len] = (uint8_t)max_records;
return call(m, handle);
}
int bleif_add_char(uint16_t svc, const uint8_t *uuid, unsigned uuid_len, uint8_t props, unsigned max_len,
uint8_t fixed_len, uint8_t events, uint16_t *char_handle)
{
uint8_t m[MSG_MAX] = {CMD_ADD_CHAR, (uint8_t)(7u + uuid_len), (uint8_t)svc, (uint8_t)(svc >> 8), (uint8_t)uuid_len};
memcpy(&m[5], uuid, uuid_len);
m[5 + uuid_len] = props;
m[6 + uuid_len] = (uint8_t)max_len;
m[7 + uuid_len] = fixed_len;
m[8 + uuid_len] = events;
return call(m, char_handle);
}
int bleif_update(uint16_t char_handle, const uint8_t *val, unsigned len)
{
uint8_t m[MSG_MAX] = {CMD_UPDATE, (uint8_t)(3u + len), (uint8_t)char_handle, (uint8_t)(char_handle >> 8), (uint8_t)len};
if (len > MSG_MAX - 5u)
return -2;
memcpy(&m[5], val, len);
return call(m, 0);
}
int bleif_advertise(uint16_t itv_min, uint16_t itv_max, const char *name)
{
unsigned nl = (unsigned)strlen(name);
uint8_t m[MSG_MAX] = {CMD_ADVERTISE, (uint8_t)(5u + nl), (uint8_t)itv_min, (uint8_t)(itv_min >> 8),
(uint8_t)itv_max, (uint8_t)(itv_max >> 8), (uint8_t)nl};
if (nl > MSG_MAX - 7u)
return -2;
memcpy(&m[7], name, nl);
return call(m, 0);
}
tsvc198e.h — сервис температуры: состояние и функции.
/* Раздел 198э: приложение на M4 — сервис температуры (Environmental Sensing 0x181A, Temperature 0x2A6E)
* и свой сервис управления со светодиодом (128-битный UUID). Не знает, кто под ним: модель или стек ST. */
#ifndef TSVC198E_H
#define TSVC198E_H
#include <stdint.h>
#define TS_AVG 8u /* отсчётов АЦП на одно значение (раз в секунду) */
#define TS_NAME "WB55-T198"
#define TS_ADV_MIN 0x0080u /* 128 × 0,625 мс = 80 мс */
#define TS_ADV_MAX 0x00A0u /* 160 × 0,625 мс = 100 мс */
struct tsvc {
uint16_t svc_ess, ch_temp, svc_ctl, ch_led;
uint8_t connected, subscribed, force, led;
uint16_t mtu, thr, heartbeat;
int32_t sent; /* последнее отправленное значение, сотые °C */
uint32_t sec, sent_at, updates, errors;
uint32_t ts_sum, vref_sum;
unsigned n;
};
extern struct tsvc ts;
int32_t tsvc_temp_c100(uint32_t ts_data, uint32_t vref_data, uint16_t ts_cal1, uint16_t ts_cal2, uint16_t vref_cal);
int tsvc_init(uint16_t thr_c100, uint16_t heartbeat_s);
int tsvc_sample(uint16_t ts_code, uint16_t vref_code); /* 1 — накопилось TS_AVG отсчётов */
void tsvc_second(int32_t t_c100);
void tsvc_on_event(const uint8_t *msg);
void tsvc_led(int on); /* светодиод: на кристалле PB5, в модели — запись в журнал */
#endif
tsvc198e.c — сервис температуры: таблица GATT, формула, правило уведомлений, светодиод.
/* Раздел 198э: логика прибора на ядре M4 — измерение, правило уведомлений, светодиод. */
#include "tsvc198e.h"
#include "bleif198e.h"
#include "msg198e.h"
#include <string.h>
struct tsvc ts;
static const uint8_t U_ESS[2] = {0x1A, 0x18}; /* 0x181A — младшим байтом вперёд */
static const uint8_t U_TEMP[2] = {0x6E, 0x2A}; /* 0x2A6E */
/* 7a1e0198-3c55-4b0e-9d1f-2f5ab3c0e101 и …e102 — в эфире все 16 байт задом наперёд */
static const uint8_t U_CTL[16] = {0x01, 0xe1, 0xc0, 0xb3, 0x5a, 0x2f, 0x1f, 0x9d,
0x0e, 0x4b, 0x55, 0x3c, 0x98, 0x01, 0x1e, 0x7a};
static const uint8_t U_LED[16] = {0x02, 0xe1, 0xc0, 0xb3, 0x5a, 0x2f, 0x1f, 0x9d,
0x0e, 0x4b, 0x55, 0x3c, 0x98, 0x01, 0x1e, 0x7a};
/* Температура кристалла в сотых °C (RM0434, 16.4.30; DS11929, таблицы 11 и 12).
* Заводские TS_CAL1 (30 °C) и TS_CAL2 (130 °C) сняты при VREF+ = 3,0 В, VREFINT_CAL — при 3,6 В.
* VDDA = 3,6 · VREFINT_CAL / VREFINT_DATA; код датчика приводится к 3,0 В: TS · VDDA / 3,0.
* Можно подать суммы N отсчётов датчика и VREFINT — важно только их отношение. */
int32_t tsvc_temp_c100(uint32_t ts_data, uint32_t vref_data, uint16_t ts_cal1, uint16_t ts_cal2, uint16_t vref_cal)
{
int64_t num = ((int64_t)ts_data * vref_cal * 36 - (int64_t)ts_cal1 * vref_data * 30) * (130 - 30) * 100;
int64_t den = (int64_t)(ts_cal2 - ts_cal1) * vref_data * 30;
if (den <= 0)
return INT32_MIN;
num += num >= 0 ? den / 2 : -den / 2; /* округление до ближайшего */
return (int32_t)(num / den) + 30 * 100;
}
int tsvc_init(uint16_t thr_c100, uint16_t heartbeat_s)
{
int r = 0;
memset(&ts, 0, sizeof ts);
ts.thr = thr_c100;
ts.heartbeat = heartbeat_s;
ts.mtu = 23;
r |= bleif_add_service(U_ESS, 2, 4, &ts.svc_ess); /* объявление + характеристика из трёх */
r |= bleif_add_char(ts.svc_ess, U_TEMP, 2, 0x02 | 0x10, 2, 1, 0, &ts.ch_temp); /* чтение, уведомление */
r |= bleif_add_service(U_CTL, 16, 3, &ts.svc_ctl);
r |= bleif_add_char(ts.svc_ctl, U_LED, 16, 0x02 | 0x08, 1, 1, 1, &ts.ch_led); /* чтение, запись */
r |= bleif_advertise(TS_ADV_MIN, TS_ADV_MAX, TS_NAME);
return r;
}
int tsvc_sample(uint16_t ts_code, uint16_t vref_code)
{
if (ts.n == 0)
ts.ts_sum = ts.vref_sum = 0;
ts.ts_sum += ts_code;
ts.vref_sum += vref_code;
if (++ts.n < TS_AVG)
return 0;
ts.n = 0;
return 1;
}
static void send(int32_t t)
{
uint8_t v[2] = {(uint8_t)t, (uint8_t)((uint32_t)t >> 8)}; /* sint16, сотые °C, младшим байтом вперёд */
if (bleif_update(ts.ch_temp, v, 2) != 0) {
ts.errors++;
return;
}
ts.sent = t;
ts.sent_at = ts.sec;
ts.updates++;
}
/* раз в секунду: без подписки — просто держать значение свежим для чтения;
* с подпиской — слать, только если ушло на порог или давно не слали */
void tsvc_second(int32_t t)
{
ts.sec++;
if (t > 32767)
t = 32767;
if (t < -32768)
t = -32768;
int32_t d = t > ts.sent ? t - ts.sent : ts.sent - t;
if (!ts.subscribed) {
if (t != ts.sent || ts.updates == 0)
send(t);
} else if (ts.force || d >= ts.thr || ts.sec - ts.sent_at >= ts.heartbeat) {
ts.force = 0;
send(t);
}
}
void tsvc_on_event(const uint8_t *m)
{
uint16_t h = (uint16_t)(m[2] | (m[3] << 8));
switch (m[0]) {
case EVT_CONNECTED:
ts.connected = 1;
break;
case EVT_DISCONNECTED:
ts.connected = ts.subscribed = 0;
ts.mtu = 23;
break;
case EVT_MTU:
ts.mtu = h;
break;
case EVT_ATTR_WRITTEN:
if (h == ts.ch_temp + 2u && m[4] == 2) { /* CCCD температуры: бит 0 — уведомления */
ts.subscribed = (m[5] & 1u) != 0;
ts.force = ts.subscribed;
} else if (h == ts.ch_led + 1u && m[4] == 1) { /* значение «светодиода» */
ts.led = m[5] != 0;
tsvc_led(ts.led);
}
break;
}
}
fw198e.c — прошивка ядра M4 на регистрах: такт, консоль, светодиод, АЦП, таблица векторов.
/* Раздел 198э: прошивка ядра M4 для P-NUCLEO-WB55 (STM32WB55RG) на регистрах CMSIS.
* Такт — MSI 4 МГц, как после сброса (RM0434, 8.2). Светодиод LED1 (синий) — PB5, консоль — USART1:
* PB6 TX, AF7 (так в BSP платы: LED1_PIN, COM1_TX_PIN). Температура кристалла — АЦП, каналы 17 (датчик)
* и 0 (VREFINT), раз в 125 мс; среднее из 8 — раз в секунду в tsvc_second.
* Сборка FW_UART: «радио» — печать в консоль. Сборка FW_IPCC: команды по IPCC через bleif198e.c —
* только для размера: прошивка радиоядра ST таких сообщений не понимает. */
#include "stm32wb55xx.h"
#include "tsvc198e.h"
#include "bleif198e.h"
#include "msg198e.h"
#include <string.h>
#define HCLK 4000000u
#define TS_CAL1 (*(const uint16_t *)0x1FFF75A8u) /* DS11929, таблица 11: 30 °C, 3,0 В */
#define TS_CAL2 (*(const uint16_t *)0x1FFF75CAu) /* 130 °C, 3,0 В */
#define VREF_CAL (*(const uint16_t *)0x1FFF75AAu) /* DS11929, таблица 12: 30 °C, 3,6 В */
static volatile uint32_t ms;
void SysTick_Handler(void) { ms++; }
static void delay_ms(uint32_t n)
{
uint32_t t0 = ms;
while (ms - t0 < n) {
}
}
static void uart_init(void)
{
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOBEN;
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; /* USART1SEL = 00 после сброса: такт PCLK2 */
(void)RCC->APB2ENR;
GPIOB->MODER = (GPIOB->MODER & ~(3u << GPIO_MODER_MODE6_Pos)) | (2u << GPIO_MODER_MODE6_Pos);
GPIOB->AFR[0] = (GPIOB->AFR[0] & ~(15u << GPIO_AFRL_AFSEL6_Pos)) | (7u << GPIO_AFRL_AFSEL6_Pos);
USART1->BRR = (HCLK + 115200u / 2u) / 115200u; /* 35: 114 286 бит/с, −0,8 % */
USART1->CR1 = USART_CR1_TE | USART_CR1_UE;
}
static void uart_puts(const char *s)
{
while (*s) {
while (!(USART1->ISR & USART_ISR_TXE))
;
USART1->TDR = (uint8_t)*s++;
}
}
static void led_init(void)
{
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOBEN;
(void)RCC->AHB2ENR;
GPIOB->MODER = (GPIOB->MODER & ~(3u << GPIO_MODER_MODE5_Pos)) | (1u << GPIO_MODER_MODE5_Pos);
}
void tsvc_led(int on) { GPIOB->BSRR = on ? (1u << 5) : (1u << (5 + 16)); }
/* АЦП: выйти из глубокого сна, включить регулятор, откалибровать, включить (RM0434, 16.4.6–16.4.9) */
static void adc_init(void)
{
RCC->AHB2ENR |= RCC_AHB2ENR_ADCEN;
(void)RCC->AHB2ENR;
ADC1_COMMON->CCR = ADC_CCR_CKMODE_0 | ADC_CCR_TSEN | ADC_CCR_VREFEN; /* HCLK/1, датчик и VREFINT */
ADC1->CR &= ~ADC_CR_DEEPPWD;
ADC1->CR |= ADC_CR_ADVREGEN;
delay_ms(1); /* tADCVREG_STUP — до 20 мкс (DS11929) */
ADC1->CR |= ADC_CR_ADCAL;
while (ADC1->CR & ADC_CR_ADCAL)
;
ADC1->SMPR1 = 4u << ADC_SMPR1_SMP0_Pos; /* 47,5 такта = 11,9 мкс при 4 МГц */
ADC1->SMPR2 = 4u << ADC_SMPR2_SMP17_Pos; /* датчику нужно не меньше 5 мкс */
ADC1->ISR = ADC_ISR_ADRDY;
ADC1->CR |= ADC_CR_ADEN;
while (!(ADC1->ISR & ADC_ISR_ADRDY))
;
}
static uint16_t adc_read(unsigned ch)
{
ADC1->SQR1 = ch << ADC_SQR1_SQ1_Pos; /* одно преобразование, канал ch */
ADC1->CR |= ADC_CR_ADSTART;
while (!(ADC1->ISR & ADC_ISR_EOC))
;
return (uint16_t)ADC1->DR;
}
#ifdef FW_UART
static void uart_dec(int32_t v)
{
char b[12];
int i = 11;
uint32_t u = v < 0 ? (uint32_t)-v : (uint32_t)v;
b[i] = 0;
do {
b[--i] = (char)('0' + u % 10u);
u /= 10u;
} while (u && i > 1);
if (v < 0)
b[--i] = '-';
uart_puts(&b[i]);
}
/* без радиоядра: «характеристики» — номера по порядку, обновления — в консоль */
static uint16_t next_h = 0x0A;
int bleif_add_service(const uint8_t *uuid, unsigned uuid_len, unsigned max_records, uint16_t *handle)
{
(void)uuid, (void)uuid_len;
*handle = next_h;
next_h = (uint16_t)(next_h + max_records);
return 0;
}
int bleif_add_char(uint16_t svc, const uint8_t *uuid, unsigned uuid_len, uint8_t props, unsigned max_len,
uint8_t fixed_len, uint8_t events, uint16_t *char_handle)
{
(void)uuid, (void)uuid_len, (void)props, (void)max_len, (void)fixed_len, (void)events;
*char_handle = (uint16_t)(svc + 1u);
return 0;
}
int bleif_update(uint16_t char_handle, const uint8_t *val, unsigned len)
{
(void)char_handle;
if (len == 2) {
uart_puts("t=");
uart_dec((int16_t)(val[0] | (val[1] << 8)));
uart_puts(" (0,01 C)\r\n");
}
return 0;
}
int bleif_advertise(uint16_t itv_min, uint16_t itv_max, const char *name)
{
(void)itv_min, (void)itv_max;
uart_puts("adv ");
uart_puts(name);
uart_puts("\r\n");
return 0;
}
#endif
#ifdef FW_IPCC
#include "ipcc198e.h"
void ipcc_hw_start(void);
void cpu2_boot(void);
__attribute__((section(".shm"))) static struct shm198e shm; /* SRAM2a: её видят оба ядра */
#endif
int main(void)
{
SysTick->LOAD = HCLK / 1000u - 1u;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
led_init();
uart_init();
adc_init();
uart_puts("boot 198e\r\n");
#ifdef FW_IPCC
ipcc_hw_start();
ipcc_init(&shm);
cpu2_boot();
#endif
tsvc_init(50, 10);
uint32_t t = ms;
for (;;) {
#ifdef FW_IPCC
uint8_t m[MSG_MAX];
while (ipcc_next_event(m))
tsvc_on_event(m);
#endif
if (ms - t < 125u)
continue;
t += 125u;
if (tsvc_sample(adc_read(17), adc_read(0)))
tsvc_second(tsvc_temp_c100(ts.ts_sum, ts.vref_sum, TS_CAL1, TS_CAL2, VREF_CAL));
}
}
/* ── начальный код и таблица векторов: 16 исключений ядра + 46 прерываний (до IPCC_C1_TX = 45) ── */
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss, _estack;
void Reset_Handler(void);
static void Default_Handler(void)
{
for (;;) {
}
}
void IPCC_C1_RX_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void IPCC_C1_TX_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
__attribute__((section(".isr_vector"), used)) static void (*const vectors[16 + 46])(void) = {
[0] = (void (*)(void))&_estack,
[1] = Reset_Handler,
[2] = Default_Handler, /* NMI */
[3] = Default_Handler, /* HardFault */
[15] = SysTick_Handler,
[16 + IPCC_C1_RX_IRQn] = IPCC_C1_RX_IRQHandler,
[16 + IPCC_C1_TX_IRQn] = IPCC_C1_TX_IRQHandler,
};
void Reset_Handler(void)
{
uint32_t *s = &_sidata, *d = &_sdata;
while (d < &_edata)
*d++ = *s++;
for (d = &_sbss; d < &_ebss;)
*d++ = 0;
main();
for (;;) {
}
}
fw198e.ld — компоновка: 512 КБ флеша, SRAM1, общая память в SRAM2a.
/* Раздел 198э: компоновка ядра M4 STM32WB55RG. Как в шаблоне ST stm32wb55xx_flash_cm4.ld:
* флеш приложения — первые 512 КБ (вторая половина — под прошивку радиоядра),
* ОЗУ — SRAM1 с 0x20000008, общая с радиоядром память — первые 10 КБ SRAM2a. */
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM1 (xrw) : ORIGIN = 0x20000008, LENGTH = 0x2FFF8
RAM_SHARED (xrw) : ORIGIN = 0x20030000, LENGTH = 10K
}
_estack = 0x20030000;
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text*) *(.rodata*) . = ALIGN(4); } > FLASH
_sidata = LOADADDR(.data);
.data : { _sdata = .; *(.data*) . = ALIGN(4); _edata = .; } > RAM1 AT > FLASH
.bss (NOLOAD) : { _sbss = .; *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; } > RAM1
.shm (NOLOAD) : { *(.shm) } > RAM_SHARED
}
run198e.sh — весь прогон: модель в stm-sys:1, прошивка в stm-arm:1.
#!/bin/sh
# Раздел 198э: прогон. /v — этот каталог (только чтение).
# sh /v/run198e.sh host в stm-sys:1 — приложение M4 против моделей IPCC, радиоядра и телефона
# sh /v/run198e.sh build в stm-arm:1 — прошивка M4 под STM32WB55; /p — CMSIS STM32WBxx (Include), /c — CMSIS Core
set -u
export LC_ALL=C.UTF-8
B=$(mktemp -d)
cd $B
case "$1" in
host)
echo "== gcc $(gcc -dumpfullversion)"
gcc -std=c11 -O2 -Wall -Wextra -Werror -o host198e /v/host198e.c /v/tsvc198e.c /v/bleif198e.c /v/ipcc198e.c \
/v/ipcc_model198e.c /v/cpu2_198e.c /v/att198e.c /v/phone198e.c -lm 2>&1
echo "build_host=$?"
./host198e
;;
build)
echo "== $(arm-none-eabi-gcc --version | head -1)"
CPU="-mcpu=cortex-m4 -mthumb -mfloat-abi=soft"
W="$CPU -Os -std=c11 -Wall -Wextra -Werror -ffunction-sections -fdata-sections -DSTM32WB55xx -I/v -I/p -I/c"
LD="$CPU -nostartfiles --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections -T /v/fw198e.ld"
for f in tsvc198e bleif198e ipcc198e ipcc_hw_wb198e; do
arm-none-eabi-gcc $W -c /v/$f.c -o $f.o 2>&1
echo "build_$f=$?"
done
arm-none-eabi-gcc $W -DFW_UART -c /v/fw198e.c -o fw_uart.o 2>&1
echo "build_fw_uart=$?"
arm-none-eabi-gcc $W -DFW_IPCC -c /v/fw198e.c -o fw_ipcc.o 2>&1
echo "build_fw_ipcc=$?"
arm-none-eabi-gcc $LD -Wl,-Map=fw_uart.map -o fw_uart.elf fw_uart.o tsvc198e.o 2>&1
echo "link_uart=$?"
arm-none-eabi-gcc $LD -Wl,-Map=fw_ipcc.map -o fw_ipcc.elf fw_ipcc.o tsvc198e.o bleif198e.o ipcc198e.o ipcc_hw_wb198e.o 2>&1
echo "link_ipcc=$?"
arm-none-eabi-size tsvc198e.o bleif198e.o ipcc198e.o ipcc_hw_wb198e.o fw_uart.elf fw_ipcc.elf
arm-none-eabi-nm -S -t d tsvc198e.o ipcc198e.o | awk '$3 ~ /^[Tt]$/ {print "fn", $4, $2 + 0}' | sort
arm-none-eabi-nm fw_ipcc.elf | grep -E " (shm|vectors|__aeabi_ldivmod)$" | sed 's/^/sym /'
arm-none-eabi-objdump -h fw_ipcc.elf | awk '$2 ~ /^\.(isr_vector|text|data|bss|shm)$/ {print "sec", $2, $3, $4}'
arm-none-eabi-objcopy -O binary fw_uart.elf fw_uart.bin
echo "bin_uart=$(wc -c < fw_uart.bin)"
;;
esac
rm -rf $B
Дальше: часть XXVI, раздел 199 «Сборка платы на заводе: перечень, файл установки и гербер» — плата разведена, пора на завод.