19д. UDS (ISO 14229): sessions, reading data, trouble codes and flashing through diagnostics
Section 19г covered OBD-II: the set of modes and codes common to all cars that any scanner must understand. But OBD-II is only what emission regulations require. Everything else — reading the ECU serial number, clearing airbag faults, programming a new key, flashing firmware — goes over UDS, the manufacturer diagnostics of ISO 14229. It travels over the same ISO-TP on top of CAN, but it has its own, much wider vocabulary.
Here: UDS from the side of an ECU on an STM32 — how a request and a response are built, what the negative response codes mean, how sessions and the P2 and S3 timers work, how to read data and trouble codes, what seed and key are and why they are not protection. At the end — a bootloader that receives 64 KB of firmware over UDS into sector 4 of an STM32F407.
Sources: the text of ISO 14229 is paid and was not used here. So everything was cross-checked against two
open-source implementations. The first is the iso14229 0.11.0 library (a UDS server and
client in C with isotp-c built in): its server and client were built with gcc and
exchanged data over a 500 kbit/s CAN bus model; every byte in the text comes from
that run. The second is the tables of services, negative response codes, DIDs and DTC
status bits from opendbc/car/uds.py (the opendbc project). The erase
time and the sector map are from the DS8626 datasheet and RM0090 from the Keil pack. The model is
_verify/uds19d/uds19d_host.c, the common server code is uds19d_app.c, the skeleton
for the F407 is uds19d_f407.c. Licenses: iso14229 — MIT (Nick James Kirkby),
opendbc — MIT (comma.ai).
Terms used in the documentation
Term Meaning UDS (Unified Diagnostic Services) the unified diagnostic services, ISO 14229 tester / server (ECU) the tester (client) / the control unit (server) SID (service identifier) the service number, the first byte of a request NRC (negative response code) the refusal code in a 0x7F response diagnostic session, S3 a diagnostic session, the timer that keeps it alive P2 / P2* server max the response deadline / the deadline after "response pending" (0x78) DID (data identifier) a data identifier, two bytes DTC, status mask a trouble code, the mask of its status byte seed / key the random value from the ECU / the key from the tester block sequence counter the TransferData block counter
19д.1 UDS and OBD-II: one road, a different vocabulary
Both kinds of diagnostics travel the same way: the tester sends a request to the ECU's identifier, and the ECU answers on its own. In the iso14229 example for the STM32G474 these are 0x7E0 (a request to this ECU), 0x7E8 (its response) and 0x7DF (a request to everyone at once). Underneath the request is ISO-TP: a short message goes as a single frame, a long one as a first frame followed by consecutive frames, and the receiver sends a flow control frame between them. ISO-TP in detail is in 19в. What matters here: UDS sees not frames but whole messages of up to 4095 bytes.
The difference is in the vocabulary. OBD-II is a small set of modes, the same for all cars. UDS has more services (iso14229 has 21 handlers), and what stands behind them (what data, what routines, what key algorithm) is decided by the ECU manufacturer. Hence "manufacturer" diagnostics: a scanner without a description of the specific ECU knows only the framework.
The most valuable thing for an STM32 developer is that cars use UDS to update ECU firmware. The order of requests is shown in the figure; below we go through each step in the run.
19д.2 Request, response and refusal
The first byte of a request is the service number (SID). A positive response starts with SID + 0x40: to 0x10 — 0x50, to 0x22 — 0x62, to 0x3E — 0x7E. A refusal is always three bytes: 0x7F, the SID of the request and the negative response code (NRC). The main services:
| SID | Service | Response | What it does |
|---|---|---|---|
| 0x10 | DiagnosticSessionControl | 0x50 | change the session |
| 0x11 | ECUReset | 0x51 | restart the ECU |
| 0x14 | ClearDiagnosticInformation | 0x54 | clear trouble codes |
| 0x19 | ReadDTCInformation | 0x59 | read trouble codes |
| 0x22 | ReadDataByIdentifier | 0x62 | read data by DID |
| 0x27 | SecurityAccess | 0x67 | unlock access: seed and key |
| 0x2E | WriteDataByIdentifier | 0x6E | write data by DID |
| 0x31 | RoutineControl | 0x71 | run a routine |
| 0x34 | RequestDownload | 0x74 | allow a download |
| 0x36 | TransferData | 0x76 | receive a data block |
| 0x37 | RequestTransferExit | 0x77 | finish the download |
| 0x3E | TesterPresent | 0x7E | "the tester is still here" |
The negative response codes that occurred in the run — each one triggered on purpose:
| NRC | Name | What triggered it in the run |
|---|---|---|
| 0x11 | serviceNotSupported | 83 01 — the service is not implemented |
| 0x12 | subFunctionNotSupported | 10 05 — no such session |
| 0x13 | incorrectMessageLengthOrInvalidFormat | 22 F1 — a one-byte DID |
| 0x22 | conditionsNotCorrect | 34 before the memory is erased |
| 0x31 | requestOutOfRange | 22 12 34 — an unknown DID |
| 0x33 | securityAccessDenied | 2E F1 8C without unlocked access |
| 0x35 | invalidKey | 27 02 00 00 00 00 — a wrong key |
| 0x36 | exceededNumberOfAttempts | 27 01 right after a wrong key |
| 0x37 | requiredTimeDelayNotExpired | 27 01 253 ms after startup |
| 0x78 | requestCorrectlyReceived-ResponsePending | 31 01 FF 00 — erasing in progress |
| 0x7F | serviceNotSupportedInActiveSession | 36 after dropping out of the session |
0x78 is not an error but "received, answer later". Having received it, the tester waits not for P2 but for P2*. The ECU may send 0x78 several times, and then the real response.
19д.3 Sessions, P2 and TesterPresent
After power-up the ECU is in the default session (0x01). Writing and routines need the extended session (0x03), flashing needs the programming session (0x02). The response to 0x10 carries two ECU deadlines: P2 — in milliseconds, P2* — in tens of milliseconds. In the run:
51.250 мс -> 10 03
102.250 мс <- 50 03 00 32 01 F4
00 32 — P2 = 50 ms, 01 F4 = 500, that is P2* = 5000 ms. The frame
02 3E 00 padded to 8 bytes is 117 bits with stuff bits, 234 µs on
a 500 kbit/s bus; compared with P2, the bus costs almost nothing.
All sessions except 0x01 live on the S3 timer: no requests — the ECU returns
to 0x01 by itself and locks access. In iso14229 S3 = 5100 ms. The
session is kept alive with the request 3E 80: 0x3E with bit 0x80, "no response needed". In the run three
3E 80 sent 2 s apart kept session 03 (62 F1 86 03 at 9172 ms), and after
5.2 s of silence DID 0xF186 (the current session) returned 62 F1 86 01.
Bit 0x80 in the second byte works for all services with a sub-function: the ECU executes the request and stays silent. A refusal still comes back, though.
19д.4 Data and trouble codes
0x22 and 0x2E — data by DID. A DID is two bytes. Some of the numbers
are standard: 0xF190 — VIN, 0xF18C — ECU serial number, 0xF186 — current
session (the DATA_IDENTIFIER_TYPE table in opendbc). Reading the VIN gives 20 bytes
of response, which ISO-TP cuts into three frames:
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 is the first frame of a message of 0x014 = 20 bytes, 30 08 00 — "send 8
frames at a time without a gap", 21 and 22 are consecutive frames. The response holds 62 F1 90 and the 17
VIN characters (in the model — TESTVIN0123456789). Writing the serial number
2E F1 8C … without unlocked access got 7F 2E 33, after 0x27 —
6E F1 8C, and the next read returned the new number.
0x19 — trouble codes. Sub-function 0x02 — "all codes whose status
byte has at least one bit of the mask". Response: 59 02, the mask of the bits the
ECU supports at all, then 4-byte records: three bytes of the code and a status
byte. The bits of the status byte (the DTC_STATUS_MASK_TYPE table in
opendbc):
| Bit | Mask | Meaning |
|---|---|---|
| 0 | 0x01 | testFailed — the fault is present now |
| 1 | 0x02 | testFailedThisOperationCycle — occurred during this drive |
| 2 | 0x04 | pendingDTC — pending |
| 3 | 0x08 | confirmedDTC — confirmed |
| 4 | 0x10 | testNotCompletedSinceLastClear |
| 5 | 0x20 | testFailedSinceLastClear |
| 6 | 0x40 | testNotCompletedThisOperationCycle |
| 7 | 0x80 | warningIndicatorRequested — the lamp is lit |
The model has three records: P0301 with status 0x2F, P0171 with 0x24 and U0100 with 0x28. A request for the confirmed ones:
661.240 мс -> 19 02 08
712.750 мс <- 59 02 FF 03 01 00 2F C1 00 00 28
P0171 (0x24 — pending but not confirmed) did not make it into the response; with
mask 0xFF all three came back. The first two bits of the code are the letter: 00 — P, 11 — U,
so 0xC100 is U0100; the third byte is the failure type. 14 FF FF FF
(the "all" group) answered 54, and 19 02 FF returned the empty list
59 02 FF.
19д.5 SecurityAccess: seed and key
The ECU unlocks dangerous services (writing, erasing, downloading) only
after 0x27. The tester asks for a "seed" with an odd sub-function (27 01), the ECU
answers with a random number, the tester calculates the key from it and sends it with an even one
(27 02). If it matches — 67 02, access is unlocked until the session changes. In the model
the algorithm is a toy:
uint32_t toy_key(uint32_t s) { /* НЕ криптография: ключ вычисляется из прошивки */
uint32_t k = s ^ 0x5AA5C33Cu;
k = (k << 7) | (k >> 25);
return k + 0x1234ABCDu;
}
In the run, seed 0xDAF8A2AA gave key 0x40E5770D. This is not protection but a lock against a random tester: the algorithm sits in the firmware of every ECU and in every manufacturer scanner, and extracting it is a matter of time. Real protection comes from signing the image and having the bootloader verify the signature (section 48а).
The library itself adds two delays against brute force, both of 1000 ms.
After the ECU started, 27 01 got 7F 27 37 (253 ms had passed). After
a wrong key (7F 27 35) a repeated 27 01 right away got 7F 27 36, and
1 s later — a seed again.
19д.6 Flashing over UDS: a bootloader on the F407
The bootloader lives in sectors 0…3 of the F407 (4 of 16 KB), the application starts at 0x0801 0000, sector 4 of 64 KB (table 5 of RM0090). How the bootloader hands control to the application is section 48; here we cover only receiving the image.
Erasing. Routine 0xFF00 (eraseMemory) with an address and a length. A 64 KB sector with x32 parallelism takes 550 ms to erase typically and up to 1100 ms (DS8626, table 40); the model takes the worst case, 1100 ms. You cannot wait that long without answering — P2 is only 50 ms. So the handler starts the erase and immediately answers 0x78, and the library calls it again until the result arrives:
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 мс
This is a run with a server P2 of 1 ms: "answer later" went out after 1.99 ms,
71 01 FF 00 00 — when the erase finished.
Download. 34 00 44 08 01 00 00 00 01 00 00: data format 00, then 0x44 —
address and length, 4 bytes each. Response 74 20 01 02: the length
field is 2 bytes, maxNumberOfBlockLength = 0x0102 = 258 — the whole 0x36 request
including the SID and the counter, that is 256 bytes of data. 64 KB is 256 blocks.
The counter starts at 01, after FF comes 00 — in the run there is one such
wrap, zero errors. Then 37 → 77 and the check: routine 0xFF01 with
the image CRC-32 0x109D10FE answered 71 01 FF 01 00 — it matched, 11 01 →
51 01, the ECU restarted into session 01.
What one block costs on the bus: a first frame, 36 consecutive frames, 5 flow control frames (one per 8 consecutive frames) and a single-frame response — 43 frames. For the whole image — 11008 frames. Time in the model:
| Server P2 in iso14229 | Time for 64 KB | Rate | Bus load | Worst 0x36 cycle |
|---|---|---|---|---|
| 50 ms (default) | 13.362 s | 4.79 KB/s | 19.2 % | 102 ms |
| 1 ms | 2.632 s | 24.31 KB/s | 97.4 % | 10.54 ms |
The bus itself is busy for 2.56 s — with P2 at 1 ms it is the bus that limits the download. The model does not count writing to flash: 16384 words of 16 µs each (DS8626, typical) — about another 262 ms for the whole firmware.
The server on the F407. The same uds19d_app.c was built with arm-none-eabi-gcc together
with the F407 bxCAN and flash code. The main loop is two calls:
for (;;) {
can_poll();
UDSServerPoll(&srv);
}
can_poll hands frames 0x7E0 and 0x7DF to isotp_on_can_message, and
isotp_user_send_can puts a frame into a free one of the three bxCAN mailboxes or
returns ISOTP_RET_NOSPACE — isotp-c will retry later. Sizes (-Os,
Cortex-M4): 10656 bytes of flash and 16792 bytes of RAM, of which
8292 are the server structure and 8440 the ISO-TP transport: buffers of 4095 bytes
in each direction. It fits into the 64 KB of the bootloader with room to spare. On the G474 the same
code works with FDCAN — the examples/stm32g474 example in iso14229.
19д.7 The full server code for the F407
The server is four files of our own, a linker script and the iso14229
0.11.0 library: two files iso14229.c and iso14229.h from the release at
github.com/driftregion/iso14229/releases, unmodified (MIT). ISO-TP
(isotp-c) is already inside iso14229.c. Below are our files in full, except
toy_key() — it is in 19д.5 and sits in uds19d_app.c right before
crc32_calc(). The skeleton does not hand control to the application — that is
section 48.
The build is the same command as in the check. UDS_SYS_CUSTOM and
UDS_CUSTOM_MILLIS tell the library that time comes from our own UDSMillis().
CMSIS_F4 is the Drivers/CMSIS/Device/ST/STM32F4xx/Include directory of the
Keil STM32F4xx_DFP 2.17.1 pack (it contains stm32f4xx.h), CMSIS_CORE is
CMSIS/Core/Include from 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
The header. The application address and size, the 0x36 block length, the routine numbers and the functions provided by the platform:
/* Раздел 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
ECU data. VIN, serial number, three trouble codes with a status byte, the seed and the download state:
/* Раздел 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 and reset. Bitwise CRC-32, as in zlib; app_init() restores
the codes and resets the download; be32() reads a number most significant byte first:
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 routines. Erase 0xFF00 answers 0x78 while the erase is in progress; check 0xFF01 compares the CRC-32 of sector 4 with the one sent:
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;
}
The event handler. The library parses the requests itself and calls
app_fn() with an event: session 0x10, reset 0x11, reading and writing a DID
0x22/0x2E, seed and key 0x27, trouble codes 0x19/0x14, routines 0x31,
download 0x34/0x36/0x37. A refusal is the returned NRC code:
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 platform: time. The clock is HSI 16 MHz after reset, SysTick at 1 ms. isotp-c gets the time here with millisecond precision:
/* Раздел 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 through registers. CAN1 on PB8/PB9 (AF9), 16 MHz / 2 = 8 MHz, 16 quanta — 500 kbit/s. The filter is a list of two identifiers: our own 0x7E0 and the functional 0x7DF. Reception is by polling: frame 0x7E0 goes to the physical ISO-TP link, 0x7DF — to the functional one:
/* ── 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);
}
}
Flash through registers. Unlocking with two keys, erasing
sector 4 without waiting (app_fn waits, answering 0x78), writing in
32-bit words, checking the error flags:
/* ── флеш-память: сектор 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(); }
The main loop. srv.p2_ms = 1 — the server does not hold 50 ms between
responses; this is the "P2 1 ms" option from the table in 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);
}
}
Startup. A vector table of 16 entries (peripheral interrupts are not needed:
CAN is polled) and 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,
};
The linker script f407.ld — the bootloader in the first 64 KB (sectors 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 Pitfalls
The client hits a timeout although the ECU is fine. The iso14229 client takes P2
from the response to 0x10 literally — 50 ms. On 22 F1 86 it declared
UDS_ERR_TIMEOUT after 50.75 ms, while the response from the server of the same library had not
arrived yet. A tester needs a margin beyond the
ECU's P2 for network delay; in the model the client's P2 was set back to 150 ms.
Every response comes exactly 50 ms later. In iso14229 the server's p2_ms is also
the gap between responses: the next response goes out no earlier than P2
after the previous one. Hence 13.4 s for 64 KB instead of 2.6 s. For a bootloader
you set srv.p2_ms to 1 ms.
The download broke off at block 74. In this version of the library the S3 timer
is restarted only by 0x10 and 0x3E, not by every request. Without TesterPresent
the programming session ended 5.1 s after 10 02, and block 0x4A
got 7F 36 7F. During a long download the tester sends 3E 80 once every
2 s — in the run 6 of them were needed.
The erase stays silent longer than P2. A blocking erase of 1.1 s means a timeout at the tester. Start it, answer 0x78, poll BSY; the first 0x78 in the model went out after 1.99 ms (with P2 at 1 ms), the result — after 1103 ms. With P2 at 50 ms the same 0x78 went out only after 50.99 ms — already later than P2.
A repeated block is a refusal. If the response to 0x36 was lost and the tester repeated the same counter, iso14229 does not recognize the repeat but answers 0x24 (requestSequenceError) and resets the download. The tester must start again from 0x34.
Access unlocked "forever". The library itself does not lock 0x27 when the
session changes — the handler does that (srv->securityLevel = 0 in 0x10). Otherwise
a key entered in the extended session will also unlock programming.
Seed/key treated as protection. A key algorithm in the ECU firmware is a password written on the lock itself. Only the bootloader verifying the image signature protects anything.
19д.9 Summary
- UDS is manufacturer diagnostics on top of ISO-TP: response SID + 0x40, refusal
7F SID NRC; the run triggered 11 different codes, from 0x11 to 0x7F. 50 03 00 32 01 F4: P2 = 50 ms, P2* = 5000 ms; S3 in iso14229 is 5100 ms, kept alive with the request3E 80.- VIN (DID 0xF190) — 20 bytes of response in three ISO-TP frames;
19 02 08returned 2 of the 3 trouble codes by bit 0x08. - Seed/key — 1000 ms delays against brute force (0x37, 0x36), but not cryptography.
- 64 KB into sector 4 of the F407: 256 blocks of 256 bytes, one counter wrap FF → 00, 11008 frames, 2.632 s with a server P2 of 1 ms and 13.362 s with 50 ms.
- A 1100 ms erase — via 0x78; without
3E 80the download broke off at block 74. - The server on the F407: 10656 bytes of flash and 16792 bytes of RAM.
Next: section 19е "DBC files: how to decode someone else's CAN bus" — from on-request diagnostics to the car's normal traffic: which frames mean what.