Embedded HandbookApps
All sections › III. Peripherals
STM32F407, STM32G474 · level advanced · 26 min read
Open in the interactive handbook

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.

Flashing an ECU over UDS: programming session 10 02, unlocking access with 27 01 and 27 02, erasing sector 4 with request 31 01 FF 00 and the interim response 7F 31 78, download permission 34 with a block of 258 bytes, 256 blocks 36 with a counter from 01 to FF and 00, exit 37, CRC check 31 01 FF 01 and reset 11 01


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

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.

Open in the interactive handbook