Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F407, STM32G474 · уровень продвинутый · 26 мин чтения
Открыть в интерактивном справочнике

19д. UDS (ISO 14229): сеансы, чтение данных, ошибки и прошивка через диагностику

Раздел 19г разобрал OBD-II: общий для всех машин набор режимов и кодов, который обязан понимать любой сканер. Но OBD-II — только то, что требуют экологические нормы. Всё остальное — прочитать серийный номер блока, стереть неисправности подушки безопасности, прописать новый ключ, залить прошивку — идёт по UDS, заводской диагностике по ISO 14229. Она едет по тому же ISO-TP поверх CAN, но словарь у неё свой и много шире.

Здесь — UDS со стороны блока управления на STM32: как устроены запрос и ответ, что значат коды отказа, как работают сеансы и таймеры P2 и S3, как читать данные и коды неисправностей, что такое seed и ключ и почему это не защита. В конце — загрузчик, который принимает прошивку 64 КБ через UDS в сектор 4 STM32F407.

Опоры: текст ISO 14229 платный и здесь не использован. Поэтому всё сверено с двумя открытыми реализациями. Первая — библиотека iso14229 0.11.0 (сервер и клиент UDS на C со встроенным isotp-c): её сервер и клиент собраны gcc и обменивались по модели шины CAN 500 кбит/с, каждый байт в тексте — из этого прогона. Вторая — таблицы сервисов, кодов отказа, DID и битов состояния неисправности из opendbc/car/uds.py (проект opendbc). Время стирания и карта секторов — даташит DS8626 и RM0090 из пака Keil. Модель — _verify/uds19d/uds19d_host.c, общий код сервера — uds19d_app.c, каркас для F407 — uds19d_f407.c. Лицензия: iso14229 — MIT (Nick James Kirkby), opendbc — MIT (comma.ai).

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

Оригинал Перевод
UDS (Unified Diagnostic Services) единые диагностические услуги, ISO 14229
tester / server (ECU) тестер (клиент) / блок управления (сервер)
SID (service identifier) номер сервиса, первый байт запроса
NRC (negative response code) код отказа в ответе 0x7F
diagnostic session, S3 диагностический сеанс, таймер его жизни
P2 / P2* server max срок ответа / срок после «ответ позже» (0x78)
DID (data identifier) идентификатор данных, два байта
DTC, status mask код неисправности, маска байта состояния
seed / key «зерно» от блока / ключ от тестера
block sequence counter счётчик блоков TransferData

19д.1 UDS и OBD-II: одна дорога, разный словарь

Обе диагностики едут одинаково: тестер шлёт запрос на идентификатор блока, блок отвечает на свой. В примере iso14229 для STM32G474 это 0x7E0 (запрос этому блоку), 0x7E8 (его ответ) и 0x7DF (запрос сразу всем). Под запросом — ISO-TP: короткое сообщение уходит одним кадром, длинное — первым кадром, потом кадрами-продолжениями, а приёмник между ними шлёт кадр управления потоком. Подробно ISO-TP — в 19в. Здесь главное: UDS видит не кадры, а целые сообщения до 4095 байт.

Разница — в словаре. OBD-II — небольшой набор режимов, одинаковых для всех машин. У UDS сервисов больше (в iso14229 обработчиков 21), а что за ними стоит (какие данные, какие процедуры, какой алгоритм ключа), решает производитель блока. Отсюда и «заводская» диагностика: сканер без описания конкретного блока знает только каркас.

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

Прошивка блока по UDS: сеанс программирования 10 02, открытие доступа 27 01 и 27 02, стирание сектора 4 запросом 31 01 FF 00 с промежуточным ответом 7F 31 78, разрешение загрузки 34 с блоком 258 байт, 256 блоков 36 со счётчиком от 01 до FF и 00, выход 37, проверка CRC 31 01 FF 01 и сброс 11 01


19д.2 Запрос, ответ и отказ

Первый байт запроса — номер сервиса (SID). Положительный ответ начинается с SID + 0x40: на 0x10 — 0x50, на 0x22 — 0x62, на 0x3E — 0x7E. Отказ — всегда три байта: 0x7F, SID запроса и код отказа (NRC). Основные сервисы:

SID Сервис Ответ Что делает
0x10 DiagnosticSessionControl 0x50 сменить сеанс
0x11 ECUReset 0x51 перезапустить блок
0x14 ClearDiagnosticInformation 0x54 стереть неисправности
0x19 ReadDTCInformation 0x59 прочитать неисправности
0x22 ReadDataByIdentifier 0x62 прочитать данные по DID
0x27 SecurityAccess 0x67 открыть доступ: seed и ключ
0x2E WriteDataByIdentifier 0x6E записать данные по DID
0x31 RoutineControl 0x71 запустить процедуру
0x34 RequestDownload 0x74 разрешить загрузку
0x36 TransferData 0x76 принять блок данных
0x37 RequestTransferExit 0x77 закончить загрузку
0x3E TesterPresent 0x7E «тестер на связи»

Коды отказа, которые встретились в прогоне, — каждый вызван нарочно:

NRC Название Чем вызван в прогоне
0x11 serviceNotSupported 83 01 — сервис не реализован
0x12 subFunctionNotSupported 10 05 — такого сеанса нет
0x13 incorrectMessageLengthOrInvalidFormat 22 F1 — DID из одного байта
0x22 conditionsNotCorrect 34 до стирания памяти
0x31 requestOutOfRange 22 12 34 — неизвестный DID
0x33 securityAccessDenied 2E F1 8C без открытого доступа
0x35 invalidKey 27 02 00 00 00 00 — неверный ключ
0x36 exceededNumberOfAttempts 27 01 сразу после неверного ключа
0x37 requiredTimeDelayNotExpired 27 01 через 253 мс после старта
0x78 requestCorrectlyReceived-ResponsePending 31 01 FF 00 — стирание идёт
0x7F serviceNotSupportedInActiveSession 36 после выпадения из сеанса

0x78 — не ошибка, а «принял, ответ позже». Получив его, тестер ждёт уже не P2, а P2*. Блок может прислать 0x78 несколько раз, затем — настоящий ответ.


19д.3 Сеансы, P2 и TesterPresent

После включения блок в сеансе по умолчанию (0x01). Для записи и процедур нужен расширенный (0x03), для прошивки — сеанс программирования (0x02). Ответ на 0x10 несёт два срока блока: P2 — в миллисекундах, P2* — в десятках миллисекунд. В прогоне:

      51.250 мс  -> 10 03
     102.250 мс  <- 50 03 00 32 01 F4

00 32 — P2 = 50 мс, 01 F4 = 500, то есть P2* = 5000 мс. Кадр 02 3E 00 с заполнением до 8 байт — 117 бит со вставками, 234 мкс на шине 500 кбит/с; на фоне P2 шина почти ничего не стоит.

Все сеансы, кроме 0x01, живут по таймеру S3: нет запросов — блок сам возвращается в 0x01 и запирает доступ. В iso14229 S3 = 5100 мс. Держат сеанс запросом 3E 80: 0x3E с битом 0x80 «ответ не нужен». В прогоне три 3E 80 через 2 с сохранили сеанс 03 (62 F1 86 03 на 9172 мс), а после 5,2 с тишины DID 0xF186 (текущий сеанс) вернул 62 F1 86 01.

Бит 0x80 во втором байте работает у всех сервисов с подфункцией: блок выполняет запрос и молчит. Отказ при этом всё равно приходит.


19д.4 Данные и неисправности

0x22 и 0x2E — данные по DID. DID — два байта. Часть номеров стандартна: 0xF190 — VIN, 0xF18C — серийный номер блока, 0xF186 — текущий сеанс (таблица DATA_IDENTIFIER_TYPE в opendbc). Чтение VIN — 20 байт ответа, ISO-TP режет их на три кадра:

    355.480 мс  7E0  03 22 F1 90 AA AA AA AA
    406.240 мс  7E8  10 14 62 F1 90 54 45 53
    406.480 мс  7E0  30 08 00 AA AA AA AA AA
    406.730 мс  7E8  21 54 56 49 4E 30 31 32
    406.960 мс  7E8  22 33 34 35 36 37 38 39

10 14 — первый кадр сообщения 0x014 = 20 байт, 30 08 00 — «шли по 8 кадров без паузы», 21 и 22 — продолжения. В ответе 62 F1 90 и 17 символов VIN (в модели — TESTVIN0123456789). Запись серийного номера 2E F1 8C … без открытого доступа получила 7F 2E 33, после 0x27 — 6E F1 8C, и следующее чтение вернуло новый номер.

0x19 — неисправности. Подфункция 0x02 — «все, у кого в байте состояния есть хоть один бит маски». Ответ: 59 02, маска битов, которые блок вообще ведёт, затем записи по 4 байта: три байта кода и байт состояния. Биты байта состояния (таблица DTC_STATUS_MASK_TYPE в opendbc):

Бит Маска Значение
0 0x01 testFailed — ошибка есть сейчас
1 0x02 testFailedThisOperationCycle — была в этой поездке
2 0x04 pendingDTC — ожидающая
3 0x08 confirmedDTC — подтверждённая
4 0x10 testNotCompletedSinceLastClear
5 0x20 testFailedSinceLastClear
6 0x40 testNotCompletedThisOperationCycle
7 0x80 warningIndicatorRequested — горит лампа

В модели три записи: P0301 с состоянием 0x2F, P0171 с 0x24 и U0100 с 0x28. Запрос подтверждённых:

    661.240 мс  -> 19 02 08
    712.750 мс  <- 59 02 FF 03 01 00 2F C1 00 00 28

P0171 (0x24 — ожидающая, но не подтверждённая) в ответ не попала, с маской 0xFF пришли все три. Первые два бита кода — буква: 00 — P, 11 — U, поэтому 0xC100 — это U0100; третий байт — тип отказа. 14 FF FF FF (группа «все») ответила 54, и 19 02 FF вернул пустой список 59 02 FF.


19д.5 SecurityAccess: seed и ключ

Опасные сервисы (запись, стирание, загрузка) блок открывает только после 0x27. Тестер просит «зерно» нечётной подфункцией (27 01), блок отвечает случайным числом, тестер считает из него ключ и шлёт чётной (27 02). Совпало — 67 02, доступ открыт до смены сеанса. В модели алгоритм игрушечный:

uint32_t toy_key(uint32_t s) {          /* НЕ криптография: ключ вычисляется из прошивки */
    uint32_t k = s ^ 0x5AA5C33Cu;
    k = (k << 7) | (k >> 25);
    return k + 0x1234ABCDu;
}

В прогоне seed 0xDAF8A2AA дал ключ 0x40E5770D. Это не защита, а замок от случайного тестера: алгоритм лежит в прошивке каждого блока и в каждом заводском сканере, извлечь его — вопрос времени. Настоящую защиту дают подпись образа и проверка её загрузчиком (раздел 48а).

Сама библиотека добавляет две задержки против перебора, обе по 1000 мс. После старта блока 27 01 получил 7F 27 37 (прошло 253 мс). После неверного ключа (7F 27 35) повторный 27 01 сразу же — 7F 27 36, а через 1 с — снова seed.


19д.6 Прошивка через UDS: загрузчик на F407

Загрузчик живёт в секторах 0…3 F407 (4 по 16 КБ), приложение — с 0x0801 0000, сектор 4 на 64 КБ (таблица 5 RM0090). Как загрузчик передаёт управление приложению — раздел 48; здесь только приём образа.

Стирание. Процедура 0xFF00 (eraseMemory) с адресом и длиной. Сектор 64 КБ при разрядности x32 стирается 550 мс типично и до 1100 мс (DS8626, таблица 40); модель берёт худшие 1100 мс. Столько ждать без ответа нельзя — P2 всего 50 мс. Поэтому обработчик запускает стирание и сразу отвечает 0x78, а библиотека вызывает его снова, пока не придёт результат:

        if (!erase_started) {            /* первый вызов: запустить и сразу ответить 0x78 */
            erase_started = true;
            erased = false;
            flash_erase_start();
            return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        }
        if (flash_busy()) return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
  35735.230 мс  <- 7F 31 78  (ответ позже)
  стирание: запрос 35733.240 мс, ответ 71 через 1103.010 мс

Это прогон с P2 сервера 1 мс: «ответ позже» ушёл через 1,99 мс, 71 01 FF 00 00 — когда стирание кончилось.

Загрузка. 34 00 44 08 01 00 00 00 01 00 00: формат данных 00, затем 0x44 — адрес и длина по 4 байта. Ответ 74 20 01 02: длина поля 2 байта, maxNumberOfBlockLength = 0x0102 = 258 — весь запрос 0x36 вместе с SID и счётчиком, то есть 256 байт данных. 64 КБ — 256 блоков. Счётчик начинается с 01, после FF идёт 00 — в прогоне один такой переход, ошибок ноль. Затем 37 → 77 и проверка: процедура 0xFF01 с CRC-32 образа 0x109D10FE ответила 71 01 FF 01 00 — совпало, 11 01 → 51 01, блок перезапустился в сеанс 01.

Что стоит один блок на шине: первый кадр, 36 продолжений, 5 кадров управления потоком (по одному на 8 продолжений) и однокадровый ответ — 43 кадра. На весь образ — 11 008 кадров. Время в модели:

P2 сервера в iso14229 Время 64 КБ Скорость Занятость шины Худший цикл 0x36
50 мс (по умолчанию) 13,362 с 4,79 КБ/с 19,2 % 102 мс
1 мс 2,632 с 24,31 КБ/с 97,4 % 10,54 мс

Сама шина занята 2,56 с — при P2 1 мс загрузку ограничивает уже она. Запись во флеш-память модель не считает: 16 384 слова по 16 мкс (DS8626, типично) — ещё около 262 мс на всю прошивку.

Сервер на F407. Тот же uds19d_app.c собран arm-none-eabi-gcc вместе с bxCAN и флеш-памятью F407. Главный цикл — два вызова:

    for (;;) {
        can_poll();
        UDSServerPoll(&srv);
    }

can_poll отдаёт кадры 0x7E0 и 0x7DF в isotp_on_can_message, а isotp_user_send_can кладёт кадр в свободный из трёх ящиков bxCAN или возвращает ISOTP_RET_NOSPACE — isotp-c повторит позже. Размеры (-Os, Cortex-M4): 10 656 байт флеш-памяти и 16 792 байта ОЗУ, из них 8292 — структура сервера и 8440 — транспорт ISO-TP: буферы на 4095 байт в каждую сторону. В 64 КБ загрузчика помещается с запасом. На G474 тот же код работает с FDCAN — пример examples/stm32g474 в iso14229.


19д.7 Полный код сервера на F407

Сервер — четыре своих файла, скрипт компоновщика и библиотека iso14229 0.11.0: два файла iso14229.c и iso14229.h из выпуска на github.com/driftregion/iso14229/releases, без правок (MIT). ISO-TP (isotp-c) уже внутри iso14229.c. Ниже свои файлы целиком, кроме toy_key() — она в 19д.5 и стоит в uds19d_app.c сразу перед crc32_calc(). Передачи управления приложению в каркасе нет — это раздел 48.

Сборка — та же команда, что в проверке. UDS_SYS_CUSTOM и UDS_CUSTOM_MILLIS говорят библиотеке, что время даёт своя UDSMillis(). CMSIS_F4 — каталог Drivers/CMSIS/Device/ST/STM32F4xx/Include пака Keil STM32F4xx_DFP 2.17.1 (в нём stm32f4xx.h), CMSIS_CORE — CMSIS/Core/Include из CMSIS 5.9.0 (github.com/ARM-software/CMSIS_5):

arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -Os -std=gnu11 -Wall -Wextra -Werror \
  -ffunction-sections -fdata-sections -DSTM32F407xx -DUDS_SYS=UDS_SYS_CUSTOM \
  -DUDS_CUSTOM_MILLIS -DISO_TP_FRAME_PADDING -DNDEBUG -I$CMSIS_F4 -I$CMSIS_CORE \
  start_f407.c uds19d_f407.c uds19d_app.c iso14229.c -T f407.ld -nostartfiles \
  --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections -o u.elf

Заголовок. Адрес и размер приложения, длина блока 0x36, номера процедур и функции, которые даёт платформа:

/* Раздел 19д: прикладная часть сервера UDS (блок управления).
 * Один и тот же код собирается gcc для модели на компьютере (uds19d_host.c)
 * и arm-none-eabi-gcc для STM32F407 (uds19d_f407.c). Разные у них только
 * функции флеш-памяти ниже. Библиотека: iso14229 0.11.0 (MIT). */
#ifndef UDS19D_APP_H
#define UDS19D_APP_H
#include <stdbool.h>
#include <stdint.h>
#include "iso14229.h"

#define APP_BASE   0x08010000u /* сектор 4 F407: 64 КБ сразу за загрузчиком */
#define APP_SIZE   0x00010000u
#define BLOCK_LEN  258u        /* 0x36 + счётчик + 256 байт данных */
#define RID_ERASE  0xFF00u     /* eraseMemory */
#define RID_CHECK  0xFF01u     /* checkProgrammingDependencies */
#define SEC_LEVEL  0x01u

/* зависят от платформы */
void flash_erase_start(void);                        /* начать стирание сектора 4 */
bool flash_busy(void);                               /* стирание ещё идёт */
bool flash_write(uint32_t addr, const uint8_t *p, uint16_t n);
const uint8_t *flash_ptr(uint32_t addr);             /* чтение для CRC */
void app_reset_request(void);                        /* ECUReset: перезапуск */

uint32_t toy_key(uint32_t seed);                     /* игрушечный алгоритм ключа */
uint32_t crc32_calc(const uint8_t *p, uint32_t n);
void app_init(void);
UDSErr_t app_fn(UDSServer_t *srv, UDSEvent_t ev, void *arg);

#endif

Данные блока. VIN, серийный номер, три неисправности с байтом состояния, seed и состояние загрузки:

/* Раздел 19д: обработчик событий сервера UDS — сеансы, DID, DTC,
 * seed/key и загрузка прошивки в сектор 4 STM32F407. */
#include <string.h>
#include "uds19d_app.h"

static const uint8_t VIN[17] = "TESTVIN0123456789";
static uint8_t serial[16] = "SN-000042";
static uint8_t serial_len = 9;

typedef struct { uint8_t dtc[3]; uint8_t status; } dtc_t;
static const dtc_t DTC_INIT[3] = {
    {{0x03, 0x01, 0x00}, 0x2F}, /* P0301: пропуски в цилиндре 1 — активна, подтверждена */
    {{0x01, 0x71, 0x00}, 0x24}, /* P0171: бедная смесь — только ожидающая */
    {{0xC1, 0x00, 0x00}, 0x28}, /* U0100: нет связи с ЭБУ двигателя — подтверждена в прошлом */
};
static dtc_t dtcs[3];
static uint8_t dtc_count;

static uint32_t seed, rng = 0x19D0C0DEu;
static bool erase_started, erased;
static uint32_t dl_addr, dl_end;

CRC-32 и сброс. CRC-32 побитно, как в zlib; app_init() возвращает коды и сбрасывает загрузку; be32() читает число старшим байтом вперёд:

uint32_t crc32_calc(const uint8_t *p, uint32_t n) {  /* CRC-32, как в zlib */
    uint32_t c = 0xFFFFFFFFu;
    while (n--) {
        c ^= *p++;
        for (int i = 0; i < 8; i++) c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1u)));
    }
    return ~c;
}

void app_init(void) {
    memcpy(dtcs, DTC_INIT, sizeof dtcs);
    dtc_count = 3;
    erase_started = erased = false;
    dl_addr = dl_end = 0;
}

static uint32_t be32(const uint8_t *p) {
    return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
}

Процедуры 0x31. Стирание 0xFF00 отвечает 0x78, пока идёт стирание; проверка 0xFF01 сравнивает CRC-32 сектора 4 с присланной:

static UDSErr_t on_routine(UDSServer_t *srv, UDSRoutineCtrlArgs_t *a) {
    if (srv->sessionType != UDS_LEV_DS_PRGS) return UDS_NRC_ServiceNotSupportedInActiveSession;
    if (srv->securityLevel != SEC_LEVEL) return UDS_NRC_SecurityAccessDenied;
    if (a->ctrlType != 0x01) return UDS_NRC_SubFunctionNotSupported;
    if (a->id == RID_ERASE) {
        if (a->len != 9 || a->optionRecord[0] != 0x44 || be32(&a->optionRecord[1]) != APP_BASE ||
            be32(&a->optionRecord[5]) != APP_SIZE)
            return UDS_NRC_RequestOutOfRange;
        if (!erase_started) {            /* первый вызов: запустить и сразу ответить 0x78 */
            erase_started = true;
            erased = false;
            flash_erase_start();
            return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        }
        if (flash_busy()) return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        erase_started = false;
        erased = true;
        const uint8_t ok = 0x00;
        return a->copyStatusRecord(srv, &ok, 1);
    }
    if (a->id == RID_CHECK) {
        if (a->len != 4) return UDS_NRC_IncorrectMessageLengthOrInvalidFormat;
        uint8_t res = crc32_calc(flash_ptr(APP_BASE), APP_SIZE) == be32(a->optionRecord) ? 0x00 : 0x01;
        return a->copyStatusRecord(srv, &res, 1);
    }
    return UDS_NRC_RequestOutOfRange;
}

Обработчик событий. Библиотека разбирает запросы сама и зовёт app_fn() с событием: сеанс 0x10, сброс 0x11, чтение и запись DID 0x22/0x2E, seed и ключ 0x27, неисправности 0x19/0x14, процедуры 0x31, загрузка 0x34/0x36/0x37. Отказ — это возвращённый код NRC:

UDSErr_t app_fn(UDSServer_t *srv, UDSEvent_t ev, void *arg) {
    switch (ev) {
    case UDS_EVT_DiagSessCtrl: {
        UDSDiagSessCtrlArgs_t *a = arg;
        if (a->type < UDS_LEV_DS_DS || a->type > UDS_LEV_DS_EXTDS) return UDS_NRC_SubFunctionNotSupported;
        a->p2_ms = 50;                   /* P2server_max, мс */
        a->p2_star_ms = 5000;            /* P2*server_max, мс (в ответе — десятками мс) */
        srv->securityLevel = 0;          /* смена сеанса снова запирает доступ */
        erased = false;                  /* и требует стереть заново */
        return UDS_PositiveResponse;
    }
    case UDS_EVT_EcuReset: {
        UDSECUResetArgs_t *a = arg;
        return a->type == 0x01 ? UDS_PositiveResponse : UDS_NRC_SubFunctionNotSupported;
    }
    case UDS_EVT_DoScheduledReset:
        app_reset_request();
        return UDS_PositiveResponse;
    case UDS_EVT_ReadDataByIdent: {
        UDSRDBIArgs_t *a = arg;
        switch (a->dataId) {
        case 0xF190: return a->copy(srv, VIN, sizeof VIN);
        case 0xF18C: return a->copy(srv, serial, serial_len);
        case 0xF186: return a->copy(srv, &srv->sessionType, 1);
        default:     return UDS_NRC_RequestOutOfRange;
        }
    }
    case UDS_EVT_WriteDataByIdent: {
        UDSWDBIArgs_t *a = arg;
        if (a->dataId != 0xF18C) return UDS_NRC_RequestOutOfRange;
        if (srv->securityLevel != SEC_LEVEL) return UDS_NRC_SecurityAccessDenied;
        if (a->len < 1 || a->len > sizeof serial) return UDS_NRC_IncorrectMessageLengthOrInvalidFormat;
        memcpy(serial, a->data, a->len);
        serial_len = (uint8_t)a->len;
        return UDS_PositiveResponse;
    }
    case UDS_EVT_SecAccessRequestSeed: {
        UDSSecAccessRequestSeedArgs_t *a = arg;
        if (a->level != SEC_LEVEL) return UDS_NRC_SubFunctionNotSupported;
        if (srv->sessionType == UDS_LEV_DS_DS) return UDS_NRC_ServiceNotSupportedInActiveSession;
        rng ^= UDSMillis();
        rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5;   /* xorshift32 */
        seed = rng;
        const uint8_t s[4] = {(uint8_t)(seed >> 24), (uint8_t)(seed >> 16), (uint8_t)(seed >> 8), (uint8_t)seed};
        return a->copySeed(srv, s, 4);
    }
    case UDS_EVT_SecAccessValidateKey: {
        UDSSecAccessValidateKeyArgs_t *a = arg;
        if (a->len != 4 || seed == 0 || be32(a->key) != toy_key(seed)) return UDS_NRC_InvalidKey;
        seed = 0;                        /* seed одноразовый */
        return UDS_PositiveResponse;
    }
    case UDS_EVT_ReadDTCInformation: {
        UDSRDTCIArgs_t *a = arg;
        if (a->type != 0x02) return UDS_NRC_SubFunctionNotSupported;
        const uint8_t avail = 0xFF;      /* DTCStatusAvailabilityMask */
        a->copy(srv, &avail, 1);
        for (uint8_t i = 0; i < dtc_count; i++)
            if (dtcs[i].status & a->subFuncArgs.dtcStatusByMaskArgs.mask)
                a->copy(srv, &dtcs[i], 4);
        return UDS_PositiveResponse;
    }
    case UDS_EVT_ClearDiagnosticInfo: {
        UDSCDIArgs_t *a = arg;
        if (a->groupOfDTC != 0xFFFFFFu) return UDS_NRC_RequestOutOfRange;
        dtc_count = 0;
        return UDS_PositiveResponse;
    }
    case UDS_EVT_RoutineCtrl:
        return on_routine(srv, arg);
    case UDS_EVT_RequestDownload: {
        UDSRequestDownloadArgs_t *a = arg;
        if (srv->sessionType != UDS_LEV_DS_PRGS || srv->securityLevel != SEC_LEVEL)
            return UDS_NRC_SecurityAccessDenied;
        if (a->addr != APP_BASE || a->size == 0 || a->size > APP_SIZE) return UDS_NRC_RequestOutOfRange;
        if (!erased) return UDS_NRC_ConditionsNotCorrect;   /* сначала стереть */
        dl_addr = (uint32_t)a->addr;
        dl_end = dl_addr + (uint32_t)a->size;
        a->maxNumberOfBlockLength = BLOCK_LEN;
        return UDS_PositiveResponse;
    }
    case UDS_EVT_TransferData: {
        UDSTransferDataArgs_t *a = arg;
        if (srv->sessionType != UDS_LEV_DS_PRGS) return UDS_NRC_ServiceNotSupportedInActiveSession;
        if (dl_addr + a->len > dl_end) return UDS_NRC_TransferDataSuspended;
        if (!flash_write(dl_addr, a->data, a->len)) return UDS_NRC_GeneralProgrammingFailure;
        dl_addr += a->len;
        return UDS_PositiveResponse;
    }
    case UDS_EVT_RequestTransferExit:
        erased = false;
        return dl_addr == dl_end ? UDS_PositiveResponse : UDS_NRC_RequestSequenceError;
    case UDS_EVT_SessionTimeout:
        erased = false;
        return UDS_PositiveResponse;
    default:
        return UDS_NRC_ServiceNotSupported;  /* и все неизвестные SID (UDS_EVT_Custom) */
    }
}

Платформа F407: время. Такт — HSI 16 МГц после сброса, SysTick на 1 мс. isotp-c здесь получает время с точностью до миллисекунды:

/* Раздел 19д: сервер UDS на STM32F407 — загрузчик в секторах 0…3,
 * приложение принимается по CAN1 (PB8 — RX, PB9 — TX, AF9) в сектор 4.
 * Такт — HSI 16 МГц после сброса, CAN 500 кбит/с. Каркас для сборки:
 * проверены компиляция и размеры, не работа на плате. */
#include "stm32f4xx.h"
#include "uds19d_app.h"

static UDSServer_t srv;
static UDSTpISOTpC_t tp;
static volatile uint32_t ms;

void SysTick_Handler(void) { ms++; }
uint32_t UDSMillis(void) { return ms; }
uint32_t isotp_user_get_us(void) { return ms * 1000u; }
void isotp_user_debug(const char *message, ...) { (void)message; }

bxCAN регистрами. CAN1 на PB8/PB9 (AF9), 16 МГц / 2 = 8 МГц, 16 квантов — 500 кбит/с. Фильтр — список из двух идентификаторов: свой 0x7E0 и общий 0x7DF. Приём — опросом: кадр 0x7E0 идёт в физический канал ISO-TP, 0x7DF — в функциональный:

/* ── CAN1: 16 МГц / 2 = 8 МГц, 1 + 13 + 2 = 16 квантов → 500 кбит/с, точка 87,5 % ── */
static void can_init(void) {
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
    RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
    GPIOB->MODER = (GPIOB->MODER & ~(0xFu << 16)) | (0xAu << 16);          /* PB8, PB9 — AF */
    GPIOB->AFR[1] = (GPIOB->AFR[1] & ~0xFFu) | (9u << 0) | (9u << 4);       /* AF9 = CAN1 */
    CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_ABOM;
    while (!(CAN1->MSR & CAN_MSR_INAK)) {}
    CAN1->BTR = (1u << 20) | (12u << 16) | 1u;                             /* BS2 2, BS1 13, BRP 2 */
    CAN1->FMR |= CAN_FMR_FINIT;                                            /* фильтр 0: список из двух ID */
    CAN1->FM1R |= 1u;
    CAN1->FS1R |= 1u;
    CAN1->sFilterRegister[0].FR1 = 0x7E0u << 21;                           /* физический адрес блока */
    CAN1->sFilterRegister[0].FR2 = 0x7DFu << 21;                           /* функциональный (всем) */
    CAN1->FA1R |= 1u;
    CAN1->FMR &= ~CAN_FMR_FINIT;
    CAN1->MCR &= ~CAN_MCR_INRQ;
    while (CAN1->MSR & CAN_MSR_INAK) {}
}

int isotp_user_send_can(const uint32_t id, const uint8_t *d, const uint8_t n, void *arg) {
    (void)arg;
    uint32_t tsr = CAN1->TSR, mb;
    if (tsr & CAN_TSR_TME0) mb = 0; else if (tsr & CAN_TSR_TME1) mb = 1;
    else if (tsr & CAN_TSR_TME2) mb = 2; else return ISOTP_RET_NOSPACE;   /* три ящика заняты */
    uint8_t b[8] = {0};
    for (uint8_t i = 0; i < n && i < 8; i++) b[i] = d[i];
    CAN1->sTxMailBox[mb].TDTR = n;
    CAN1->sTxMailBox[mb].TDLR = b[0] | (b[1] << 8) | (b[2] << 16) | ((uint32_t)b[3] << 24);
    CAN1->sTxMailBox[mb].TDHR = b[4] | (b[5] << 8) | (b[6] << 16) | ((uint32_t)b[7] << 24);
    CAN1->sTxMailBox[mb].TIR = (id << 21) | CAN_TI0R_TXRQ;
    return ISOTP_RET_OK;
}

static void can_poll(void) {
    while (CAN1->RF0R & CAN_RF0R_FMP0) {
        uint32_t id = CAN1->sFIFOMailBox[0].RIR >> 21, lo = CAN1->sFIFOMailBox[0].RDLR,
                 hi = CAN1->sFIFOMailBox[0].RDHR;
        uint8_t n = CAN1->sFIFOMailBox[0].RDTR & 0xFu, d[8];
        for (int i = 0; i < 4; i++) { d[i] = (uint8_t)(lo >> (8 * i)); d[4 + i] = (uint8_t)(hi >> (8 * i)); }
        CAN1->RF0R = CAN_RF0R_RFOM0;                                     /* освободить FIFO */
        if (id == 0x7E0u) isotp_on_can_message(&tp.phys_link, d, n);
        else if (id == 0x7DFu) isotp_on_can_message(&tp.func_link, d, n);
    }
}

Флеш-память регистрами. Разблокировка двумя ключами, стирание сектора 4 без ожидания (ждёт app_fn, отвечая 0x78), запись словами по 32 бита, проверка флагов ошибок:

/* ── флеш-память: сектор 4, разрядность x32 (PSIZE = 10) ── */
static void flash_unlock(void) {
    if (FLASH->CR & FLASH_CR_LOCK) { FLASH->KEYR = 0x45670123u; FLASH->KEYR = 0xCDEF89ABu; }
}
void flash_erase_start(void) {                   /* запуск и сразу возврат: ждать будет app_fn */
    flash_unlock();
    while (FLASH->SR & FLASH_SR_BSY) {}
    FLASH->CR = FLASH_CR_PSIZE_1 | FLASH_CR_SER | (4u << FLASH_CR_SNB_Pos);
    FLASH->CR |= FLASH_CR_STRT;
}
bool flash_busy(void) {
    if (FLASH->SR & FLASH_SR_BSY) return true;
    FLASH->CR &= ~FLASH_CR_SER;
    return false;
}
bool flash_write(uint32_t addr, const uint8_t *p, uint16_t n) {
    if (addr < APP_BASE || addr + n > APP_BASE + APP_SIZE || (n & 3u)) return false;
    flash_unlock();
    FLASH->CR = FLASH_CR_PSIZE_1 | FLASH_CR_PG;
    for (uint16_t i = 0; i < n; i += 4) {        /* слово — 16 мкс типично (DS8626) */
        *(volatile uint32_t *)(addr + i) = p[i] | (p[i + 1] << 8) | (p[i + 2] << 16) | ((uint32_t)p[i + 3] << 24);
        while (FLASH->SR & FLASH_SR_BSY) {}
    }
    FLASH->CR = 0;
    return (FLASH->SR & (FLASH_SR_PGAERR | FLASH_SR_PGPERR | FLASH_SR_PGSERR | FLASH_SR_WRPERR)) == 0;
}
const uint8_t *flash_ptr(uint32_t addr) { return (const uint8_t *)addr; }
void app_reset_request(void) { NVIC_SystemReset(); }

Главный цикл. srv.p2_ms = 1 — сервер не выдерживает 50 мс между ответами; это вариант «P2 1 мс» из таблицы 19д.6:

int main(void) {
    SysTick_Config(16000u);                      /* 1 мс при 16 МГц */
    can_init();
    app_init();
    UDSServerInit(&srv);
    srv.fn = app_fn;
    srv.tp = &tp.hdl;
    srv.p2_ms = 1;                               /* не выдерживать 50 мс между ответами */
    UDSServerTpISOTpCInit(&tp, 0x7E0u, 0x7E8u, 0x7DFu);
    for (;;) {
        can_poll();
        UDSServerPoll(&srv);
    }
}

Старт. Таблица векторов на 16 входов (прерывания периферии не нужны: CAN опрашивается) и Reset_Handler:

/* Минимальный старт F407 для сборки каркаса: таблица векторов и Reset_Handler. */
#include <stdint.h>
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);
void SysTick_Handler(void);

void Reset_Handler(void) {
    uint32_t *s = &_sidata, *d = &_sdata;
    while (d < &_edata) *d++ = *s++;
    for (d = &_sbss; d < &_ebss; ) *d++ = 0;
    main();
    for (;;) {}
}
static void Default_Handler(void) { for (;;) {} }

__attribute__((section(".isr_vector"), used))
void (*const vectors[16])(void) = {
    (void (*)(void))&_estack, Reset_Handler, Default_Handler, Default_Handler, Default_Handler,
    Default_Handler, Default_Handler, 0, 0, 0, 0, Default_Handler, Default_Handler, 0,
    Default_Handler, SysTick_Handler,
};

Скрипт компоновщика f407.ld — загрузчик в первых 64 КБ (секторы 0…3):

/* Загрузчик: секторы 0…3 (64 КБ); приложение — с 0x08010000 (сектор 4). */
MEMORY {
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS {
  .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
  .text : { *(.text*) *(.rodata*) . = ALIGN(4); } > FLASH
  _sidata = LOADADDR(.data);
  .data : { _sdata = .; *(.data*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
  .bss (NOLOAD) : { _sbss = .; *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; } > RAM
}

19д.8 Грабли

Клиент ловит тайм-аут, хотя блок исправен. Клиент iso14229 берёт P2 из ответа на 0x10 буквально — 50 мс. На 22 F1 86 он объявил UDS_ERR_TIMEOUT через 50,75 мс, а ответ сервера той же библиотеки ещё не пришёл. Тестеру нужен запас сверх P2 блока на задержку сети; в модели P2 клиента вернули к 150 мс.

Каждый ответ ровно через 50 мс. В iso14229 p2_ms сервера — это ещё и пауза между ответами: следующий ответ уходит не раньше, чем через P2 после предыдущего. Отсюда 13,4 с на 64 КБ вместо 2,6 с. Для загрузчика ставят srv.p2_ms в 1 мс.

Загрузка оборвалась на блоке 74. В этой версии библиотеки таймер S3 перезапускают только 0x10 и 0x3E, а не каждый запрос. Без TesterPresent сеанс программирования кончился через 5,1 с после 10 02, и блок 0x4A получил 7F 36 7F. Во время длинной загрузки тестер шлёт 3E 80 раз в 2 с — в прогоне их понадобилось 6.

Стирание молчит дольше P2. Блокирующее стирание на 1,1 с — это тайм-аут у тестера. Запустить, ответить 0x78, опрашивать BSY; первый 0x78 в модели ушёл через 1,99 мс (при P2 1 мс), итог — через 1103 мс. При P2 50 мс тот же 0x78 ушёл только через 50,99 мс — уже позже P2.

Повтор блока — это отказ. Если ответ на 0x36 потерялся и тестер повторил тот же счётчик, iso14229 не узнаёт повтор, а отвечает 0x24 (requestSequenceError) и сбрасывает загрузку. Тестер должен начинать с 0x34 заново.

Доступ открыт «навсегда». Библиотека сама не запирает 0x27 при смене сеанса — это делает обработчик (srv->securityLevel = 0 в 0x10). Иначе ключ, введённый в расширенном сеансе, откроет и программирование.

Seed/key считают защитой. Алгоритм ключа в прошивке блока — это пароль, записанный в самом замке. Защищает только проверка подписи образа загрузчиком.


19д.9 Итоги

Дальше: раздел 19е «Файлы DBC: как расшифровать чужую шину CAN» — от диагностики по запросу к обычному трафику машины: какие кадры что значат.

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