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 машины обновляют прошивку блоков. Порядок запросов показан на рисунке, дальше разберём каждый шаг на прогоне.
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 Итоги
- UDS — заводская диагностика поверх ISO-TP: ответ SID + 0x40, отказ
7F SID NRC; в прогоне вызваны 11 разных кодов, от 0x11 до 0x7F. 50 03 00 32 01 F4: P2 = 50 мс, P2* = 5000 мс; S3 в iso14229 — 5100 мс, держится запросом3E 80.- VIN (DID 0xF190) — 20 байт ответа в трёх кадрах ISO-TP;
19 02 08вернул 2 из 3 неисправностей по биту 0x08. - Seed/key — задержки 1000 мс против перебора (0x37, 0x36), но не криптография.
- 64 КБ в сектор 4 F407: 256 блоков по 256 байт, один переход счётчика FF → 00, 11 008 кадров, 2,632 с при P2 сервера 1 мс и 13,362 с при 50 мс.
- Стирание на 1100 мс — через 0x78; без
3E 80загрузка оборвалась на блоке 74. - Сервер на F407: 10 656 байт флеш-памяти и 16 792 байта ОЗУ.
Дальше: раздел 19е «Файлы DBC: как расшифровать чужую шину CAN» — от диагностики по запросу к обычному трафику машины: какие кадры что значат.