Embedded HandbookApps
All sections › XXV. Mini-projects
STM32F103, STM32G474 · level advanced · 54 min read
Open in the interactive handbook

198у. Mini-project: your own unit on the CAN bus — sensor, DBC, UDS diagnostics and flashing over the bus

The scanner from 198р queried other units, and the dashboard from 198с and the logger from 198т listened to someone else's bus. Here is the last step: your own unit that lives on the bus on equal terms with the factory ones. It is an oil temperature and oil pressure sensor on an STM32F103: it broadcasts its values every 100 ms, answers UDS diagnostics, keeps its own trouble codes, survives bus-off and accepts new firmware directly over CAN.

Each part on its own has already appeared: ISO-TP in 19в, UDS and a bootloader on the F407 in 19д, DBC, the counter and the CRC in 19е, your own bootloader in 48. Here they are assembled into one device, and you can see where they get in each other's way: erasing flash memory stalls the CPU in the middle of a diagnostic response, the UDS library resets the transfer when a block is repeated, and the tester's ISO-TP channel gets stuck after a lost frame.

Verified without a board and without a bus. The node code (node.c, ee.c, the packing generated from the DBC) was built with gcc on a PC together with a UDS server and client and run on a model of a 500 kbit/s bus: bit by bit, with bit stuffing, with another unit on the bus and with F103 flash memory that stalls during erase. The bootloader and the application for the F103 were built with arm-none-eabi-gcc on CMSIS registers; they have not been run on a board.

References: RM0008 (bxCAN: 24.7.6 "Error management", register CAN_ESR, bit ABOM in CAN_MCR; flash memory 3.3.3 — 1 KB pages on medium-density devices), DS5319 (DocID13587, STM32F103x8/xB: table 28 — page erase 20…40 ms, half-word programming 40…70 µs; table 29 — 10000 cycles). UDS library — iso14229 0.11.0 (github.com/driftregion/iso14229, commit a165a7d, with the built-in isotp-c), DBC parser — dbcmini.py from 19е, trouble code names — the codes.py table from python-OBD (github.com/brendan-w/python-OBD, commit a378bdd). Licenses: iso14229 is MIT, python-OBD is GPL-2.0; copies of the needed files are kept in the check together with their licenses. The texts of ISO 11898 and ISO 14229 were not checked here: the rule "TEC + 8 per transmit error" is taken as a model assumption (RM0008 refers to the standard here).

Terms used in the documentation

Term Meaning
ECU, node a control unit, a node on the bus
cyclic message, cycle time a message sent periodically, and its period
alive counter / rolling counter a message counter, 0…15 wrapping around
E2E protection, Data ID protecting a frame with a CRC and a counter; a "salt" chosen by the counter value
Transmit Error Counter (TEC), Receive Error Counter (REC) transmit and receive error counters
error warning, error passive, bus-off warning, passive state, disconnection from the bus
ABOM (automatic bus-off management) automatic recovery from bus-off
128 occurrences of 11 consecutive recessive bits 128 times 11 recessive bits in a row
debounce, confirmed DTC confirming a fault, a confirmed code
EEPROM emulation emulating an EEPROM on flash memory pages
page erase, half-word programming erasing a page, writing a half-word

198у.1 What the unit is and what it is made of

The unit is an STM32F103C8 with a CAN transceiver (19.2) and two sensors on ADC inputs. The sensor curves here are assumptions, not datasheet data:

Your own unit on the CAN bus: the NTC and pressure sensors on the ADC inputs of the F103, the node code converts them and every 100 ms sends the message OilSens_01 0x3A1 according to the DBC with a counter and CRC-8 through bxCAN with ABOM equal to 0 onto a 500 kbit/s bus; the tester at 0x741 and 0x749 talks to the iso14229 UDS server, which hands control to the bootloader on 10 02; on bus-off the node recovers three times at once, then after 200 ms; at the bottom, the 64 KB flash layout: bootloader 16 KB, application 45 KB, a page for the valid-image record and two EEPROM emulation pages; the run summary

What the unit does:

  1. Every 10 ms it reads both ADC inputs, converts the codes into temperature and pressure and checks them for open circuit and short circuit (198у.3).
  2. Every 100 ms it sends the message OilSens_01 at 0x3A0 + node number, that is 0x3A1 for node 1. Its layout is in the DBC file (198у.2).
  3. It answers UDS over ISO-TP: request at 0x740 + number, response from 0x748 + number, functional request at 0x7DF. The number and the period are changed with 0x2E and stored in the EEPROM emulation (198у.4).
  4. It survives bus-off and counts such events (198у.5).
  5. On 0x10 02 it goes to the bootloader, and the bootloader accepts an image over UDS (198у.6).

198у.2 The message in the DBC: layout, counter and CRC-8

First the DBC file, then the code — not the other way round. The file describes the message for everyone who will listen to it: the dashboard from 198с or the logger from 198т will decode it without our source code.

BO_ 929 OilSens_01: 8 OILNODE
 SG_ OilSens_CRC : 0|8@1+ (1,0) [0|255] "" TESTER
 SG_ OilSens_BZ : 8|4@1+ (1,0) [0|15] "" TESTER
 SG_ OilSens_Status : 12|4@1+ (1,0) [0|15] "" TESTER
 SG_ OilSens_Temp : 16|10@1+ (0.25,-40) [-40|215.5] "degC" TESTER
 SG_ OilSens_Press : 26|12@1+ (0.005,0) [0|20.47] "bar" TESTER
 SG_ OilSens_BusOff : 38|8@1+ (1,0) [0|255] "" TESTER

Temperature is 10 bits with a step of 0.25 °C from −40, pressure is 12 bits with a step of 0.005 bar. The raw values 1023 and 4095 mean "value not valid": this is how the unit reports a broken sensor without making up numbers. OilSens_Status is the fault bits, OilSens_BusOff is how many times the unit has gone bus-off since power-up.

The packing code is not written by hand: gen.py takes the generator from dbcmini.py (19е.5) and produces oil_dbc.h and oil_dbc.c. Byte 0 and the low nibble of byte 1 hold the CRC and the counter, following the VW MQB scheme from 19е.4: CRC-8 with polynomial 0x2F over bytes 1…7 and over a "salt" — a byte from a 16-entry table selected by the counter. This message has its own table. The whole send path:

static uint8_t crc8_2f(uint8_t crc, uint8_t b) {
    crc ^= b;
    for (int k = 0; k < 8; k++)
        crc = (crc & 0x80u) ? (uint8_t)((crc << 1) ^ 0x2Fu) : (uint8_t)(crc << 1);
    return crc;
}
uint8_t e2e_crc(const uint8_t *d) {
    uint8_t crc = 0xFF;
    for (int i = 1; i < 8; i++) crc = crc8_2f(crc, d[i]);
    crc = crc8_2f(crc, DATA_ID[d[1] & 0x0Fu]);
    return crc ^ 0xFFu;
}
static void send_cyclic(void) {
    oil_oilsens_01_t m;
    uint8_t d[8], st = 0;
    for (int k = 0; k < DTC_N; k++) if (node.dtc_status[k] & 0x08) st |= 0x04;
    if (node.dtc_status[DTC_T_LOW] & 0x01 || node.dtc_status[DTC_T_HIGH] & 0x01) st |= 0x01;
    if (node.dtc_status[DTC_P_LOW] & 0x01 || node.dtc_status[DTC_P_HIGH] & 0x01) st |= 0x02;
    if (node.busoff_cnt) st |= 0x08;
    memset(&m, 0, sizeof m);
    m.oilsens_bz = node.bz;
    m.oilsens_status = st;
    m.oilsens_temp = node.t_raw;
    m.oilsens_press = node.p_raw;
    m.oilsens_busoff = node.busoff_cnt;
    oil_oilsens_01_pack(d, &m);
    d[0] = e2e_crc(d);
    if (plat_can_send(ID_CYCLIC_BASE + node.node_id, d, 8) == 0) {
        node.bz = (uint8_t)((node.bz + 1u) & 0x0Fu);   /* счётчик растёт только у ушедшего кадра */
        node.sent++;
    } else {
        node.skipped++;
    }
}

The counter increases only for a frame that actually went into a mailbox. If all three bxCAN mailboxes are busy, the frame is skipped, and the receiver sees it as a gap, not as a repeat with an old counter.

Checking against the DBC. The model recorded all 134 cyclic frames of the run together with the "truth" — the temperature and pressure applied to the input. The Python decoding takes a separate path: dbcmini.decode on oilnode.dbc and its own CRC-8:

кадров OilSens_01: 134
CRC-8 не сошёлся: 0
разрывов счётчика: 0
распаковка и упаковка по DBC вернули те же байты: 134 из 134
годных кадров 130, с недостоверным значением (1023 или 4095) 4
наибольшее расхождение с правдой: температура 0.200 °C, давление 0.0050 бар
первый кадр: 100.472 мс 3A1 3D 00 F4 3C 03 00 00 00 -> OilSens_CRC=61 OilSens_BZ=0 OilSens_Status=0 OilSens_Temp=244 OilSens_Press=207 OilSens_BusOff=0

Unpacking and repacking in Python gave the same bytes that the C code packed, in all 134 frames. The difference from the truth is no more than 0.2 °C and 0.005 bar: this comes from the NTC table with a 5 °C step, the ADC step and the signal step.


198у.3 Sensors and your own trouble codes

ADC. Both inputs are read in software every 10 ms, with the longest sample time of 239.5 cycles — the NTC with a 10 kΩ pull-up has a high source impedance. Calibration is done once after power-up:

static void adc_init(void) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN;
    GPIOA->CRL &= ~0xFFu;                                 /* PA0, PA1 — аналоговые входы */
    ADC1->SMPR2 = (7u << 0) | (7u << 3);                  /* 239,5 такта: высокоомный NTC */
    ADC1->CR2 = ADC_CR2_ADON;
    for (volatile int i = 0; i < 1000; i++) {
    }
    ADC1->CR2 |= ADC_CR2_RSTCAL;
    while (ADC1->CR2 & ADC_CR2_RSTCAL) {
    }
    ADC1->CR2 |= ADC_CR2_CAL;                             /* калибровка после включения */
    while (ADC1->CR2 & ADC_CR2_CAL) {
    }
}
uint16_t plat_adc(int ch) {
    ADC1->SQR3 = (uint32_t)ch;
    ADC1->CR2 |= ADC_CR2_ADON;                            /* повторная запись ADON — старт */
    while (!(ADC1->SR & ADC_SR_EOC)) {
    }
    return (uint16_t)ADC1->DR;
}

Conversion. The F103 has no floating point, so temperature comes from a table of ADC codes from −40 to 150 °C with a 5 °C step and a straight line between the points. The table is computed by gen.py from the same NTC curve. Pressure is in integers: the ADC full scale corresponds to 4950 mV at the sensor output, before the divider; 2 mV per signal step of 0.005 bar.

Codes. Each fault has its own standard code from the python-OBD table, so that any scanner shows a clear name:

Code Name in the table When
P0197 Engine Oil Temperature Sensor Low NTC code below 40: short circuit
P0198 Engine Oil Temperature Sensor High NTC code above 4050: open circuit
P0522 Engine Oil Pressure Sensor/Switch Low Voltage below 0.25 V
P0523 Engine Oil Pressure Sensor/Switch High Voltage above 4.75 V
U0073 Control Module Communication Bus Off bus-off

The threshold 4050 is above the coldest genuine code 3996, and the threshold 40 is below the hottest one, 80: a healthy sensor in the −40…150 °C range raises no alarm. A fault is recognized after five samples in a row, that is 50 ms, and cleared the same way — so contact bounce does not produce a code:

static void dtc_eval(int k, bool bad) {
    uint8_t *st = &node.dtc_status[k];
    if (bad) {
        deb_good[k] = 0;
        if (deb_bad[k] < DEBOUNCE && ++deb_bad[k] == DEBOUNCE)
            *st |= 0x01 | 0x02 | 0x04 | 0x08 | 0x20;   /* сейчас, в этом цикле, ожидающий, подтверждённый, после сброса */
    } else {
        deb_bad[k] = 0;
        if (deb_good[k] < DEBOUNCE && ++deb_good[k] == DEBOUNCE)
            *st &= (uint8_t)~0x01;                     /* ошибки сейчас нет, история остаётся */
    }
}

The status byte bits are the same as in 19д.4. The unit maintains five of them, so the availability mask in the response is 0x2F.

A session on the model. The tester reads the serial number, the version and the live values (DID 0x0200: the ADC code and the raw signal of both sensors), then the NTC is disconnected:

  10000.000 мс  -> 22 F1 8C
  10001.622 мс  <- 62 F1 8C 4F 49 4C 2D 30 30 30 30 31 32 33
  10001.622 мс  -> 22 F1 95
  10051.926 мс  <- 62 F1 95 4F 49 4C 2D 41 50 50 20 31 2E 30 2E 30
  10051.926 мс  -> 22 02 00
  10102.704 мс  <- 62 02 00 00 CE 02 58 07 6F 03 84
  живые: АЦП NTC 206 -> 110.00 °C, АЦП давления 1903 -> 4.500 бар (правда 109.991 °C, 4.4996 бар)
  10102.704 мс  -> 19 02 FF
  10153.230 мс  <- 59 02 2F
  обрыв NTC
  10273.230 мс  -> 19 02 08
  10273.704 мс  <- 59 02 2F 01 98 00 2F
  10273.704 мс  -> 22 02 00
  10324.706 мс  <- 62 02 00 0F FF 03 FF 07 6F 03 84
  NTC снова на месте
  10424.706 мс  -> 19 02 FF
  10425.176 мс  <- 59 02 2F 01 98 00 2E
  10425.176 мс  -> 14 FF FF FF
  10475.230 мс  <- 54

Live values: code 206 → 110.00 °C with a truth of 109.991 °C, code 1903 → 4.500 bar with a truth of 4.4996. After the break, 19 02 08 (confirmed) returned P0198 with status 0x2F, and the live value became 0x3FF — not valid. After the sensor is back the status is 0x2E: there is no error now, but the history remains. 14 FF FF FF cleared everything. If the fault persists during the clear, the code comes back after the 50 ms confirmation:

  обрыв датчика давления держится во время сброса
  10626.230 мс  -> 14 FF FF FF
  10626.702 мс  <- 54
  10626.702 мс  -> 19 02 FF
  10677.230 мс  <- 59 02 2F
  10737.230 мс  -> 19 02 FF
  10737.698 мс  <- 59 02 2F 05 22 00 2F

198у.4 Configuration through 0x2E and EEPROM emulation

Two parameters are written over UDS in the extended session (0x10 03): DID 0x0100 is the node number 1…15, DID 0x0101 is the broadcast period 20…1000 ms. The period takes effect immediately, the number after a restart, because the bxCAN filter and the ISO-TP identifiers depend on it:

  10838.170 мс  -> 10 03
  10888.238 мс  <- 50 03 00 32 01 F4
  10888.238 мс  -> 2E 01 01 00 32
  10939.232 мс  <- 6E 01 01
  11139.232 мс  -> 2E 01 01 00 05
  11139.700 мс  <- 7F 2E 31  NRC 0x31
  11139.700 мс  -> 2E 01 00 02
  11190.456 мс  <- 6E 01 00
  11190.456 мс  -> 11 01
  11241.228 мс  <- 51 01
  тестер переходит на 0x742/0x74A
  11441.228 мс  -> 22 01 00
  11441.692 мс  <- 62 01 00 02
  11441.692 мс  -> 22 01 01
  11492.236 мс  <- 62 01 01 00 32

The 5 ms period was rejected with 0x31 (out of range). After the restart node 2 already answered at 0x742 and sent OilSens_01 at 0x3A2 — there are nine such frames in the log, and the 0x3A1 interval changed to 50 ms.

Where to store it. The F103 has no separate EEPROM, and a flash page is erased as a whole and withstands 10000 cycles (DS5319, table 29). Two 1 KB pages at the end of memory work in turns. A record is 4 bytes: first the value, then the parameter number; a record without the number (power was lost between them) is not read. The fresh value is the last one on the page. When the space runs out, the fresh values are moved to the other page:

static bool transfer(uint16_t id, uint16_t v) {
    uint32_t dst = other(act);
    uint16_t gen = rd(act + 4u);
    if (!erase_if_dirty(dst) || !fl_prog16(dst, H_RECV) || !fl_prog16(dst + 4u, (uint16_t)(gen + 1u)))
        return false;
    uint32_t s = 0;
    for (uint16_t k = EE_NODE_ID; k <= EE_PERIOD; k++) {
        uint16_t x;
        if (k == id) x = v;
        else if (!lookup(act, k, &x)) continue;
        uint32_t a = dst + SLOT0 + 4u * s++;
        if (!fl_prog16(a, x) || !fl_prog16(a + 2u, k)) return false;
    }
    if (!fl_prog16(dst + 2u, H_VALID)) return false;   /* только теперь новая страница главная */
    if (!fl_erase_page(act)) return false;
    act = dst;
    ee_transfers++;
    find_free();
    return true;
}
bool ee_write(uint16_t id, uint16_t v) {
    uint16_t old;
    if (lookup(act, id, &old) && old == v) return true;   /* то же значение — страницу не тратим */
    if (next_slot >= NSLOT) return transfer(id, v);
    uint32_t a = act + SLOT0 + 4u * next_slot++;
    return fl_prog16(a, v) && fl_prog16(a + 2u, id);
}

The page header is three half-words, each written once into an erased location: 0xEEEE "receiving a copy", 0x0000 "active", the generation number. You never need to overwrite a half-word that has already been written.

A page holds 254 records. 1000 period writes in a row gave 3 transfers and 3 erases. Transferring the two fresh values frees 252 slots, and each page is erased once every two transfers, so 10000 cycles are enough for 10000 × 2 × 252 = 5040000 writes. A transfer is 8 flash memory operations. The model cuts the power after each of them and powers the unit up again:

  переносов 3, стираний 3; на страницу — записей 254
  обрыв после 0 операций из 8: восстановление 0, номер 3, период 200
  обрыв после 1 операций из 8: восстановление 2, номер 3, период 200
  обрыв после 2 операций из 8: восстановление 2, номер 3, период 200
  обрыв после 3 операций из 8: восстановление 2, номер 3, период 200
  обрыв после 4 операций из 8: восстановление 2, номер 3, период 200
  обрыв после 5 операций из 8: восстановление 2, номер 3, период 200
  обрыв после 6 операций из 8: восстановление 2, номер 3, период 200
  обрыв после 7 операций из 8: восстановление 3, номер 3, период 777
  обрыв после 8 операций из 8: восстановление 0, номер 3, период 777

Before the "active" mark (0…6 operations) the old values remain, after it the new ones (7 and 8). There is no intermediate state in which a parameter would be lost: with two active pages ee_init takes the one whose generation is one higher, and erases the unfinished copy.


198у.5 Bus-off: TEC, ABOM and 128 × 11 bits

bxCAN rules. According to RM0008 (register CAN_ESR): the EWGF warning is set when TEC or REC ≥ 96, the EPVF passive state when > 127, the BOFF bus-off when TEC > 255. In the bus-off state bxCAN neither transmits nor receives. Recovery (24.7.6) happens only after 128 occurrences of 11 recessive bits in a row on the CANRX input; with ABOM = 1 bxCAN starts counting by itself, with ABOM = 0 after the software sets and clears INRQ. At 500 kbit/s on an idle bus this is 1408 bits = 2.816 ms.

Your own policy. A unit that goes bus-off again and again disturbs everyone: each of its attempts is an error frame on the bus. So here ABOM = 0 and recovery is in the hands of the software: three times at once, then with a 200 ms pause, until at least one frame gets through. The bxCAN setup is done on registers, as in 19в.6, but without ABOM, with error and bus-off interrupts and with a list-mode filter for two identifiers — your own physical request and 0x7DF:

/* ABOM = 0: из отключения выходим сами (node_on_bus_off); ERRIE + BOFIE — прерывание SCE */
void can_init(uint8_t node_id) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
    AFIO->MAPR |= AFIO_MAPR_CAN_REMAP_REMAP2;             /* CAN на PB8/PB9 */
    GPIOB->CRH = (GPIOB->CRH & ~0xFFu) | 0x4u | (0xBu << 4);

    CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_TXFP;              /* без ABOM, ящики по очереди записи */
    while (!(CAN1->MSR & CAN_MSR_INAK)) {
    }
    CAN1->BTR = ((TS2 - 1u) << 20) | ((TS1 - 1u) << 16) | (BRP - 1u);

    CAN1->FMR |= CAN_FMR_FINIT;
    CAN1->FA1R = 0;
    CAN1->FS1R = 1u;                                      /* фильтр 0 — 32 бита, режим списка */
    CAN1->FM1R = 1u;
    CAN1->sFilterRegister[0].FR1 = (ID_DIAG_REQ + node_id) << 21;   /* свой физический запрос */
    CAN1->sFilterRegister[0].FR2 = ID_DIAG_FUNC << 21;              /* и функциональный 0x7DF */
    CAN1->FFA1R = 0;                                      /* оба — в FIFO 0 */
    CAN1->FA1R = 1u;
    CAN1->FMR &= ~CAN_FMR_FINIT;

    CAN1->IER = CAN_IER_FMPIE0 | CAN_IER_ERRIE | CAN_IER_BOFIE | CAN_IER_TMEIE;
    NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
    NVIC_EnableIRQ(CAN1_SCE_IRQn);
    NVIC_EnableIRQ(USB_HP_CAN1_TX_IRQn);
    CAN1->MCR &= ~CAN_MCR_INRQ;
    while (CAN1->MSR & CAN_MSR_INAK) {
    }
}

Each event goes into the counter and into code U0073:

/* bxCAN сообщил BOFF (прерывание ошибок). ABOM = 0: восстанавливаем сами —
 * три раза сразу, потом с паузой 200 мс, чтобы неисправный узел не долбил шину. */
void node_on_bus_off(void) {
    node.bus_off = true;
    if (node.busoff_cnt < 255u) node.busoff_cnt++;
    if (node.busoff_row < 255u) node.busoff_row++;
    node.dtc_status[DTC_BUSOFF] |= 0x01 | 0x02 | 0x04 | 0x08 | 0x20;
    node.recover_at = plat_ms() + (node.busoff_row <= 3u ? 1u : 200u);
}
void node_on_tx_ok(void) {
    node.bus_off = false;
    node.busoff_row = 0;
    node.dtc_status[DTC_BUSOFF] &= (uint8_t)~0x01;
}
void CAN1_SCE_IRQHandler(void) {                          /* ошибки: BOFF — отключены от шины */
    if (CAN1->ESR & CAN_ESR_BOFF) {
        CAN1->IER &= ~CAN_IER_BOFIE;                      /* флаг держится до восстановления */
        node_on_bus_off();
    }
    CAN1->MSR = CAN_MSR_ERRI;
}
void plat_can_recover(void) {                             /* INRQ 1 → 0; дальше 128 × 11 бит сам bxCAN */
    CAN1->MCR |= CAN_MCR_INRQ;
    while (!(CAN1->MSR & CAN_MSR_INAK)) {
    }
    CAN1->MCR &= ~CAN_MCR_INRQ;
    CAN1->IER |= CAN_IER_BOFIE;
}

Experiment. In the model, interference switches on for 30 ms: every frame of the node is corrupted at the 20th bit. Each error means TEC + 8 (an assumption based on the CAN standard) and an error frame on the bus; after a successful frame, TEC − 1:

  помеха с 12098.000 до 12128.000 мс
  12101.236 мс  TEC 96: предупреждение (EWGF, ≥96), ошибка №12
  12101.588 мс  TEC 128: пассивный (EPVF, >127), ошибка №16
  12103.206 мс  TEC 256: ОТКЛЮЧЁН от шины (BOFF), ошибка №32
  12104.000 мс  программа: INRQ 1→0, ждём 128 × 11 рецессивных бит
  12106.816 мс  128 × 11 бит набрано: снова активен, TEC 0
  12108.178 мс  TEC 128: пассивный (EPVF, >127), ошибка №48
  12109.796 мс  TEC 256: ОТКЛЮЧЁН от шины (BOFF), ошибка №64
  12110.000 мс  программа: INRQ 1→0, ждём 128 × 11 рецессивных бит
  12113.020 мс  128 × 11 бит набрано: снова активен, TEC 0
  12114.382 мс  TEC 128: пассивный (EPVF, >127), ошибка №80
  12116.000 мс  TEC 256: ОТКЛЮЧЁН от шины (BOFF), ошибка №96
  12117.000 мс  программа: INRQ 1→0, ждём 128 × 11 рецессивных бит
  12119.816 мс  128 × 11 бит набрано: снова активен, TEC 0
  12121.406 мс  TEC 128: пассивный (EPVF, >127), ошибка №112
  12123.024 мс  TEC 256: ОТКЛЮЧЁН от шины (BOFF), ошибка №128
  12323.000 мс  программа: INRQ 1→0, ждём 128 × 11 рецессивных бит
  12325.816 мс  128 × 11 бит набрано: снова активен, TEC 0
  ошибок 128, предупреждение на 12-й, пассивный на 16-й; отключений 4

Before bus-off there are 32 errors: the warning at the 12th (TEC 96), the passive state at the 16th (TEC 128), bus-off at the 32nd (TEC 256). The first series took 3.206 ms from the first frame under interference. Recovery on an idle bus takes exactly 2.816 ms, as the 128 × 11 rule says; in the second case it took 3.020 ms — a frame from another unit passed on the bus, and its dominant bits broke the series of recessive ones. The interference was still present, so the node went bus-off four times, and the fourth recovery waited for the 200 ms pause — by then the interference was over. The node did not queue up the frames it could not send: in the bus-off state the broadcast simply stays silent, one frame remains in the mailbox, and it went out first after recovery (in the log at 12326.056 ms).


198у.6 Flashing over the bus: bootloader, layout and fallback

Layout. The F103C8 has 64 KB in 1 KB pages (RM0008, 3.3.3):

Address Size What
0x0800 0000 16 KB bootloader, pages 0…15
0x0800 4000 45 KB application, pages 16…60
0x0800 F400 1 KB valid-image record: marker, length, CRC-32 and its inverse
0x0800 F800 2 × 1 KB EEPROM emulation
/* Раздел 198у: STM32F103C8, boot. Флеш-память 64 КБ страницами по 1 КБ:
 *   0x08000000 загрузчик 16 КБ | 0x08004000 приложение 45 КБ |
 *   0x0800F400 запись о годном образе 1 КБ | 0x0800F800 эмуляция EEPROM 2 × 1 КБ.
 * ОЗУ 20 КБ; последнее слово 0x20004FFC — просьба «остаться в загрузчике». */
ENTRY(Reset_Handler)

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 16K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K - 4
}

One image or two. Two copies of the application of 22 KB each would be cramped here, and rolling back to the previous version, as in MCUboot (48б), needs room for both. One image was chosen, but with a guarantee: the bootloader speaks UDS itself and never erases anything of its own. The first thing erased during flashing is the validity record, and the last thing written is a new one, only after the CRC-32 of the whole image matches. If power is lost at any step, there is no record, the bootloader stays in itself and waits for the tester. The old version is gone, but the unit does not turn into a brick, and the flashing can be repeated.

Application. The main loop: frames from the interrupt ring buffer go into ISO-TP, the UDS server is polled, the node tasks run once for every millisecond, and the restart happens if it was requested and the response has already gone out:

int main(void) {
    can_rx_t f;
    uint32_t last = 0;
    SCB->VTOR = APP_BASE;                                 /* таблица векторов приложения */
    clock_72mhz();
    adc_init();
    node_init(false);
    can_init(node.node_id);
    UDSServerInit(&srv);
    srv.fn = node_uds;
    srv.tp = &tp.hdl;
    UDSServerTpISOTpCInit(&tp, ID_DIAG_REQ + node.node_id, ID_DIAG_RESP + node.node_id, ID_DIAG_FUNC);
    __enable_irq();
    for (;;) {
        while (can_rx(&f))
            isotp_on_can_message(f.id == ID_DIAG_FUNC ? &tp.func_link : &tp.phys_link, f.d, f.len);
        UDSServerPoll(&srv);
        while (last != ms) {                              /* каждая миллисекунда по разу */
            last++;
            node_tick_1ms();
        }
        reset_if_pending(&srv);
    }
}

Start-up. The word in the last 4 bytes of RAM survives a reset: the application writes a marker there when it receives 0x10 02, and restarts. The bootloader:

static void jump_to_app(void) {
    uint32_t sp = *(const volatile uint32_t *)APP_BASE;
    uint32_t pc = *(const volatile uint32_t *)(APP_BASE + 4u);
    __disable_irq();
    SysTick->CTRL = 0;                                    /* ничего от загрузчика не тикает */
    NVIC->ICER[0] = 0xFFFFFFFFu;
    NVIC->ICER[1] = 0xFFFFFFFFu;
    NVIC->ICPR[0] = 0xFFFFFFFFu;
    NVIC->ICPR[1] = 0xFFFFFFFFu;
    RCC->APB1RSTR |= RCC_APB1RSTR_CAN1RST;                /* bxCAN — в состояние после сброса */
    RCC->APB1RSTR &= ~RCC_APB1RSTR_CAN1RST;
    SCB->VTOR = APP_BASE;
    __set_MSP(sp);
    __enable_irq();
    ((void (*)(void))pc)();
}

int main(void) {
    can_rx_t f;
    bool stay = BOOT_REQ == BOOT_REQ_MAGIC;
    BOOT_REQ = 0;
    if (!stay && boot_image_ok())                         /* запись о годности + CRC-32 образа */
        jump_to_app();
    clock_72mhz();
    node_init(true);
    can_init(node.node_id);
    UDSServerInit(&srv);
    srv.fn = node_uds;
    srv.tp = &tp.hdl;
    UDSServerTpISOTpCInit(&tp, ID_DIAG_REQ + node.node_id, ID_DIAG_RESP + node.node_id, ID_DIAG_FUNC);
    if (stay) srv.sessionType = UDS_LEV_DS_PRGS;          /* пришли по 10 02 из приложения */
    __enable_irq();
    for (;;) {
        while (can_rx(&f))
            isotp_on_can_message(f.id == ID_DIAG_FUNC ? &tp.func_link : &tp.phys_link, f.d, f.len);
        UDSServerPoll(&srv);
        reset_if_pending(&srv);
    }
}

Flash memory on registers. Page erase according to RM0008: keys into FLASH_KEYR, the PER bit, the address into FLASH_AR, STRT, wait for BSY. Programming is done by half-word with the PG bit set. While an erase is in progress, the CPU, which fetches code from the same flash memory, stalls for 20…40 ms (DS5319):

static void fl_unlock(void) {
    if (FLASH->CR & FLASH_CR_LOCK) {
        FLASH->KEYR = 0x45670123u;
        FLASH->KEYR = 0xCDEF89ABu;
    }
}
static bool fl_done(void) {
    while (FLASH->SR & FLASH_SR_BSY) {
    }
    bool ok = (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) == 0u;
    FLASH->SR = FLASH_SR_EOP | FLASH_SR_PGERR | FLASH_SR_WRPRTERR;
    return ok;
}
bool fl_erase_page(uint32_t addr) {
    if (addr < BOOT_BASE + BOOT_SIZE || addr >= 0x08010000u) return false;   /* загрузчик не трогаем */
    fl_unlock();
    FLASH->CR = FLASH_CR_PER;
    FLASH->AR = addr;
    FLASH->CR = FLASH_CR_PER | FLASH_CR_STRT;             /* процессор стоит 20…40 мс */
    bool ok = fl_done();
    FLASH->CR = FLASH_CR_LOCK;
    return ok;
}
bool fl_prog16(uint32_t addr, uint16_t v) {
    if (addr < BOOT_BASE + BOOT_SIZE || addr >= 0x08010000u) return false;
    fl_unlock();
    FLASH->CR = FLASH_CR_PG;
    *(volatile uint16_t *)addr = v;                       /* 40…70 мкс */
    bool ok = fl_done() && *(volatile uint16_t *)addr == v;
    FLASH->CR = FLASH_CR_LOCK;
    return ok;
}

Erase and 0x78. Routine 0xFF00 erases the record page and the pages for the image — 21 pages for 20 KB. The model counts the worst case of 40 ms per page, that is 840 ms during which the CPU stalls. Answering within 50 ms is impossible, so the handler answers 0x78 "response pending" and erases one page per call. But in iso14229 0.11.0 the 0x78 itself is not sent immediately but by the P2 timer, and after it the timer is reset to 0.3 × P2*. If you start erasing at once, the CPU stalls before the 0x78 gets onto the bus:

  13966.228 мс  -> 31 01 FF 00 44 08 00 40 00 00 00 50 … (13 байт)
  14047.178 мс  <- 7F 31 78 (ответ позже), через 80.950 мс после запроса
  14857.236 мс  <- 71 01 FF 00 00
  16459.228 мс  -> 31 01 FF 00 44 08 00 40 00 00 00 50 … (13 байт)
  16510.454 мс  <- 7F 31 78 (ответ позже), через 51.226 мс после запроса

Without waiting, 0x78 went out after 80.950 ms — later than the 50 ms that the unit itself announced in its 50 02 00 32 response. So the erase waits until the library timer moves beyond P2, that is until the 0x78 has been sent:

static UDSErr_t on_routine(UDSServer_t *srv, UDSRoutineCtrlArgs_t *a) {
    if (!node.boot_mode) 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) {            /* 44 адрес длина; стираем запись, потом страницы образа */
        if (a->len != 9 || a->optionRecord[0] != 0x44 || be32(&a->optionRecord[1]) != APP_BASE ||
            be32(&a->optionRecord[5]) == 0 || be32(&a->optionRecord[5]) > APP_SIZE)
            return UDS_NRC_RequestOutOfRange;
        if (er_end == 0) {               /* первый вызов: только запомнить и ответить 0x78 */
            erased = false;
            er_next = REC_BASE;          /* первой стирается запись о годности */
            er_end = APP_BASE + ((be32(&a->optionRecord[5]) + FL_PAGE - 1u) & ~(FL_PAGE - 1u));
            return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        }
        /* стирание останавливает процессор: сперва пусть уйдёт 0x78. Пока он не ушёл, таймер
         * библиотеки ближе P2; после него iso14229 ставит таймер на 0,3 × P2* */
        if (erase_after_78 && (int32_t)(srv->p2_timer - UDSMillis()) <= (int32_t)srv->p2_ms)
            return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        if (er_next == REC_BASE) {
            if (!fl_erase_page(REC_BASE)) return UDS_NRC_GeneralProgrammingFailure;
            er_next = APP_BASE;
            return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        }
        if (er_next < er_end) {          /* по странице за вызов, 20…40 мс каждая (DS5319) */
            if (!fl_erase_page(er_next)) return UDS_NRC_GeneralProgrammingFailure;
            er_next += FL_PAGE;
            return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        }
        er_end = 0;
        erased = true;
        const uint8_t ok = 0x00;
        return a->copyStatusRecord(srv, &ok, 1);
    }
    if (a->id == RID_CHECK) {            /* CRC-32 образа от тестера; совпал — пишем запись */
        if (a->len != 4 || dl_end == 0) return UDS_NRC_RequestSequenceError;
        uint32_t len = dl_end - APP_BASE, crc = crc32_calc(fl_ptr(APP_BASE), len);
        uint8_t res = 0x01;
        if (crc == be32(a->optionRecord) && rec_write(len, crc)) res = 0x00;
        return a->copyStatusRecord(srv, &res, 1);
    }
    return UDS_NRC_RequestOutOfRange;
}

Downloading 20 KB with losses. The image is 80 blocks of 256 bytes, each written as 128 half-words. In the model, power is first lost after 40 blocks, then on the next attempt a consecutive frame of block 0x0B and the response to block 0x21 are lost:

  17401.236 мс  -> 34 00 44 08 00 40 00 00 00 50 00
  17452.234 мс  <- 74 20 01 02
  19492.228 мс  !! питание пропало после 40 блоков
  program: -2
  19492.228 мс  загрузчик: годного образа нет — остаюсь в загрузчике
  19512.228 мс  -> 22 F1 95
  19543.920 мс  <- 62 F1 95 4F 49 4C 2D 42 4F 4F 54 20 31 2E 30 2E 30
  21588.234 мс  <- 74 20 01 02
  22099.162 мс  !! потерян кадр продолжения 23 блока 0B
  22199.628 мс  блок 0B: UDS_ERR_TPORT, повтор
  23340.228 мс  !! потерян ответ 76 21
  23449.000 мс  блок 21: UDS_ERR_TIMEOUT, повтор
  23459.220 мс  блок 21: 7F 36 24 (RequestSequenceError), повтор
             блок уже был принят; передача сброшена сервером — всё заново
  28582.230 мс  -> 31 01 FF 01 4F 9B 42 C5
  28633.234 мс  <- 71 01 FF 01 00
  загрузка: 80 блоков по 256 байт, повторов 0, 0x24 на повтор 0, CRC-32 образа 0x4F9B42C5, результат 00
  загрузка: 5.174 с от стирания до проверки, кадров на шине 3973, худший цикл 0x36 51.230 мс
  28633.234 мс  -> 11 01
  28684.228 мс  <- 51 01
  28704.000 мс  узел перезапущен
  28704.000 мс  загрузчик: образ 20480 байт, CRC-32 0x4F9B42C5 сошёлся — переход на 0x08004000
  28784.228 мс  -> 22 F1 95
  28785.386 мс  <- 62 F1 95 4F 49 4C 2D 41 50 50 20 31 2E 30 2E 30

What you can see:

The worst 0x36 cycle is 51.23 ms, almost all of it waiting for P2 in the library, as in 19д.6; writing 128 half-words at 70 µs each takes 8.96 ms of it.


198у.7 Pitfalls

0x78 came too late, and the tester gave up on a timeout. A page erase on the F103 stalls the CPU for tens of milliseconds. If you start erasing in the handler right away, 0x78 goes out after 80.950 ms. First 0x78, then the erase.

A repeated block got 0x24, and then nothing. iso14229 0.11.0 treats a repeat with the same counter as a sequence error and on any negative response resets the transfer. If a response is lost, start again from the erase. The final CRC-32 is still needed: it also catches what the repeat check missed.

After a lost frame the tester sends nothing more. In iso14229 0.11.0 the ISO-TP channel stays in the error state after a timeout waiting for FC, and every following request immediately returns UDS_ERR_TPORT. The tester has to re-initialize the channel.

The 50 02 response did not arrive, and the unit is already in the bootloader. Restart into the bootloader only after the response has gone out: the request is processed and all three bxCAN mailboxes are empty (reset_if_pending).

27 01 in the bootloader gives 7F 27 37. The library refuses a seed for 1 s after start-up (19д.5): after a restart into the bootloader the tester must wait.

The unit goes bus-off again and again. With ABOM = 1 bxCAN recovers by itself after 2.816 ms and disrupts the bus with error frames again. Use ABOM = 0 and your own pause — three quick attempts, then 200 ms.

The node number was changed, and the unit is silent. The bxCAN filter and the ISO-TP identifiers are taken from the EEPROM at start-up. The new number applies only after 11 01, and the tester switches to the new identifiers.

A parameter was lost after a power cut. Writing "number, then value" leaves, after a power loss, the number with old garbage. The order is value, then number; a page becomes the main one only after all values have been copied.


198у.8 Summary

Full source code

node.c — the node logic: sensors, trouble codes, the cyclic message and the UDS handler.

/* Раздел 198у: логика узла — датчики, коды неисправностей, циклическое сообщение
 * по oilnode.dbc, обработчик UDS для приложения и для загрузчика. */
#include <string.h>
#include "node.h"
#include "oil_dbc.h"
#include "ntc_table.h"

node_t node;
static const char SW_APP[] = "OIL-APP 1.0.0";
static const char SW_BOOT[] = "OIL-BOOT 1.0.0";
static const char SERIAL[] = "OIL-0000123";
static const uint8_t DTC_CODE[DTC_N][3] = {
    {0x01, 0x97, 0x00},   /* P0197 Engine Oil Temperature Sensor Low  — NTC замкнут */
    {0x01, 0x98, 0x00},   /* P0198 Engine Oil Temperature Sensor High — NTC оборван */
    {0x05, 0x22, 0x00},   /* P0522 Oil Pressure Sensor Low Voltage */
    {0x05, 0x23, 0x00},   /* P0523 Oil Pressure Sensor High Voltage */
    {0xC0, 0x73, 0x00},   /* U0073 Control Module Communication Bus Off */
};
static uint8_t deb_bad[DTC_N], deb_good[DTC_N];

/* DataID на каждое значение счётчика: у этого сообщения свой набор, как у VW в 19е */
static const uint8_t DATA_ID[16] = {0x3A, 0x51, 0x7C, 0x0E, 0x93, 0xD4, 0x28, 0xB6,
                                    0x61, 0xEF, 0x05, 0x4B, 0xC2, 0x9D, 0x17, 0x80};

static uint8_t crc8_2f(uint8_t crc, uint8_t b) {
    crc ^= b;
    for (int k = 0; k < 8; k++)
        crc = (crc & 0x80u) ? (uint8_t)((crc << 1) ^ 0x2Fu) : (uint8_t)(crc << 1);
    return crc;
}
uint8_t e2e_crc(const uint8_t *d) {
    uint8_t crc = 0xFF;
    for (int i = 1; i < 8; i++) crc = crc8_2f(crc, d[i]);
    crc = crc8_2f(crc, DATA_ID[d[1] & 0x0Fu]);
    return crc ^ 0xFFu;
}

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;
}

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

/* ── датчики: целочисленно, без плавающей точки (у F103 её нет) ── */
int16_t ntc_temp_q4(uint16_t code) {    /* код падает с ростом температуры */
    if (code >= ntc_code[0]) return (int16_t)(NTC_T0 * 4);
    for (int i = 1; i < NTC_N; i++)
        if (code >= ntc_code[i]) {      /* между точками i-1 и i — прямая */
            int32_t span = ntc_code[i - 1] - ntc_code[i];
            int32_t part = ntc_code[i - 1] - code;
            return (int16_t)((NTC_T0 + (i - 1) * NTC_STEP) * 4 + (part * NTC_STEP * 4 + span / 2) / span);
        }
    return (int16_t)((NTC_T0 + (NTC_N - 1) * NTC_STEP) * 4);
}
static uint16_t press_mv(uint16_t code) { return (uint16_t)(((uint32_t)code * 4950u + 2047u) / 4095u); }
uint16_t press_raw(uint16_t code) {     /* 0,5…4,5 В = 0…10 бар; шаг DBC 5 мбар = 2 мВ */
    uint16_t mv = press_mv(code);
    return mv <= 500u ? 0u : (uint16_t)((mv - 500u) / 2u);
}

static void dtc_eval(int k, bool bad) {
    uint8_t *st = &node.dtc_status[k];
    if (bad) {
        deb_good[k] = 0;
        if (deb_bad[k] < DEBOUNCE && ++deb_bad[k] == DEBOUNCE)
            *st |= 0x01 | 0x02 | 0x04 | 0x08 | 0x20;   /* сейчас, в этом цикле, ожидающий, подтверждённый, после сброса */
    } else {
        deb_bad[k] = 0;
        if (deb_good[k] < DEBOUNCE && ++deb_good[k] == DEBOUNCE)
            *st &= (uint8_t)~0x01;                     /* ошибки сейчас нет, история остаётся */
    }
}

static void sample(void) {
    node.t_adc = plat_adc(0);
    node.p_adc = plat_adc(1);
    uint16_t mv = press_mv(node.p_adc);
    dtc_eval(DTC_T_LOW, node.t_adc < T_SHORT_CODE);
    dtc_eval(DTC_T_HIGH, node.t_adc > T_OPEN_CODE);
    dtc_eval(DTC_P_LOW, mv < P_LOW_MV);
    dtc_eval(DTC_P_HIGH, mv > P_HIGH_MV);
    bool t_bad = node.t_adc < T_SHORT_CODE || node.t_adc > T_OPEN_CODE;
    bool p_bad = mv < P_LOW_MV || mv > P_HIGH_MV;
    node.t_raw = t_bad ? 0x3FFu : (uint16_t)(ntc_temp_q4(node.t_adc) + 160);  /* (T + 40) × 4 */
    node.p_raw = p_bad ? 0xFFFu : press_raw(node.p_adc);
}

static void send_cyclic(void) {
    oil_oilsens_01_t m;
    uint8_t d[8], st = 0;
    for (int k = 0; k < DTC_N; k++) if (node.dtc_status[k] & 0x08) st |= 0x04;
    if (node.dtc_status[DTC_T_LOW] & 0x01 || node.dtc_status[DTC_T_HIGH] & 0x01) st |= 0x01;
    if (node.dtc_status[DTC_P_LOW] & 0x01 || node.dtc_status[DTC_P_HIGH] & 0x01) st |= 0x02;
    if (node.busoff_cnt) st |= 0x08;
    memset(&m, 0, sizeof m);
    m.oilsens_bz = node.bz;
    m.oilsens_status = st;
    m.oilsens_temp = node.t_raw;
    m.oilsens_press = node.p_raw;
    m.oilsens_busoff = node.busoff_cnt;
    oil_oilsens_01_pack(d, &m);
    d[0] = e2e_crc(d);
    if (plat_can_send(ID_CYCLIC_BASE + node.node_id, d, 8) == 0) {
        node.bz = (uint8_t)((node.bz + 1u) & 0x0Fu);   /* счётчик растёт только у ушедшего кадра */
        node.sent++;
    } else {
        node.skipped++;
    }
}

void node_init(bool boot_mode) {
    uint16_t v;
    memset(&node, 0, sizeof node);
    memset(deb_bad, 0, sizeof deb_bad);
    memset(deb_good, 0, sizeof deb_good);
    node.boot_mode = boot_mode;
    ee_init();
    node.node_id = (ee_read(EE_NODE_ID, &v) && v >= 1u && v <= 15u) ? (uint8_t)v : 1u;
    node.period_ms = (ee_read(EE_PERIOD, &v) && v >= 20u && v <= 1000u) ? v : 100u;
}

void node_tick_1ms(void) {
    uint32_t t = plat_ms();
    if (node.boot_mode) return;
    if (t % 10u == 0) sample();
    if (node.bus_off) {
        if (node.recover_at && (int32_t)(t - node.recover_at) >= 0) {
            node.recover_at = 0;
            plat_can_recover();                        /* INRQ 1 → 0; дальше 128 × 11 бит сам bxCAN */
        }
        return;                                        /* на отключённую шину не кладём */
    }
    if (t % node.period_ms == 0) send_cyclic();
}

/* bxCAN сообщил BOFF (прерывание ошибок). ABOM = 0: восстанавливаем сами —
 * три раза сразу, потом с паузой 200 мс, чтобы неисправный узел не долбил шину. */
void node_on_bus_off(void) {
    node.bus_off = true;
    if (node.busoff_cnt < 255u) node.busoff_cnt++;
    if (node.busoff_row < 255u) node.busoff_row++;
    node.dtc_status[DTC_BUSOFF] |= 0x01 | 0x02 | 0x04 | 0x08 | 0x20;
    node.recover_at = plat_ms() + (node.busoff_row <= 3u ? 1u : 200u);
}
void node_on_tx_ok(void) {
    node.bus_off = false;
    node.busoff_row = 0;
    node.dtc_status[DTC_BUSOFF] &= (uint8_t)~0x01;
}

/* ── загрузчик: запись о годном образе в странице REC_BASE ── */
typedef struct { uint32_t magic, len, crc, inv; } rec_t;
bool boot_image_ok(void) {
    rec_t r;
    memcpy(&r, fl_ptr(REC_BASE), sizeof r);
    if (r.magic != REC_MAGIC || r.inv != ~r.crc || r.len == 0 || r.len > APP_SIZE) return false;
    return crc32_calc(fl_ptr(APP_BASE), r.len) == r.crc;
}
static bool rec_write(uint32_t len, uint32_t crc) {
    uint32_t w[4] = {REC_MAGIC, len, crc, ~crc};
    for (int i = 0; i < 8; i++)
        if (!fl_prog16(REC_BASE + 2u * (uint32_t)i, (uint16_t)(w[i / 2] >> (16 * (i % 2))))) return false;
    return true;
}

static uint32_t seed, rng = 0x198C0DEu;
static uint32_t er_next, er_end, dl_addr, dl_end;
bool erase_after_78 = true;
static bool erased;

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];
}

static UDSErr_t on_routine(UDSServer_t *srv, UDSRoutineCtrlArgs_t *a) {
    if (!node.boot_mode) 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) {            /* 44 адрес длина; стираем запись, потом страницы образа */
        if (a->len != 9 || a->optionRecord[0] != 0x44 || be32(&a->optionRecord[1]) != APP_BASE ||
            be32(&a->optionRecord[5]) == 0 || be32(&a->optionRecord[5]) > APP_SIZE)
            return UDS_NRC_RequestOutOfRange;
        if (er_end == 0) {               /* первый вызов: только запомнить и ответить 0x78 */
            erased = false;
            er_next = REC_BASE;          /* первой стирается запись о годности */
            er_end = APP_BASE + ((be32(&a->optionRecord[5]) + FL_PAGE - 1u) & ~(FL_PAGE - 1u));
            return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        }
        /* стирание останавливает процессор: сперва пусть уйдёт 0x78. Пока он не ушёл, таймер
         * библиотеки ближе P2; после него iso14229 ставит таймер на 0,3 × P2* */
        if (erase_after_78 && (int32_t)(srv->p2_timer - UDSMillis()) <= (int32_t)srv->p2_ms)
            return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        if (er_next == REC_BASE) {
            if (!fl_erase_page(REC_BASE)) return UDS_NRC_GeneralProgrammingFailure;
            er_next = APP_BASE;
            return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        }
        if (er_next < er_end) {          /* по странице за вызов, 20…40 мс каждая (DS5319) */
            if (!fl_erase_page(er_next)) return UDS_NRC_GeneralProgrammingFailure;
            er_next += FL_PAGE;
            return UDS_NRC_RequestCorrectlyReceived_ResponsePending;
        }
        er_end = 0;
        erased = true;
        const uint8_t ok = 0x00;
        return a->copyStatusRecord(srv, &ok, 1);
    }
    if (a->id == RID_CHECK) {            /* CRC-32 образа от тестера; совпал — пишем запись */
        if (a->len != 4 || dl_end == 0) return UDS_NRC_RequestSequenceError;
        uint32_t len = dl_end - APP_BASE, crc = crc32_calc(fl_ptr(APP_BASE), len);
        uint8_t res = 0x01;
        if (crc == be32(a->optionRecord) && rec_write(len, crc)) res = 0x00;
        return a->copyStatusRecord(srv, &res, 1);
    }
    return UDS_NRC_RequestOutOfRange;
}

static UDSErr_t read_did(UDSServer_t *srv, UDSRDBIArgs_t *a) {
    uint8_t b[8];
    switch (a->dataId) {
    case 0xF18C: return a->copy(srv, SERIAL, sizeof SERIAL - 1);
    case 0xF195: return node.boot_mode ? a->copy(srv, SW_BOOT, sizeof SW_BOOT - 1) : a->copy(srv, SW_APP, sizeof SW_APP - 1);
    case 0xF186: return a->copy(srv, &srv->sessionType, 1);
    case 0x0100: return a->copy(srv, &node.node_id, 1);
    case 0x0101: b[0] = (uint8_t)(node.period_ms >> 8); b[1] = (uint8_t)node.period_ms; return a->copy(srv, b, 2);
    case 0x0200: {                       /* живые значения: коды АЦП и сырые сигналы DBC */
        if (node.boot_mode) return UDS_NRC_RequestOutOfRange;
        b[0] = (uint8_t)(node.t_adc >> 8); b[1] = (uint8_t)node.t_adc;
        b[2] = (uint8_t)(node.t_raw >> 8); b[3] = (uint8_t)node.t_raw;
        b[4] = (uint8_t)(node.p_adc >> 8); b[5] = (uint8_t)node.p_adc;
        b[6] = (uint8_t)(node.p_raw >> 8); b[7] = (uint8_t)node.p_raw;
        return a->copy(srv, b, 8);
    }
    case 0x0201: return a->copy(srv, &node.busoff_cnt, 1);
    default: return UDS_NRC_RequestOutOfRange;
    }
}

static UDSErr_t write_did(UDSServer_t *srv, UDSWDBIArgs_t *a) {
    if (srv->sessionType != UDS_LEV_DS_EXTDS) return UDS_NRC_ServiceNotSupportedInActiveSession;
    if (a->dataId == 0x0100) {           /* номер узла: вступит в силу после перезапуска */
        if (a->len != 1) return UDS_NRC_IncorrectMessageLengthOrInvalidFormat;
        if (a->data[0] < 1u || a->data[0] > 15u) return UDS_NRC_RequestOutOfRange;
        return ee_write(EE_NODE_ID, a->data[0]) ? UDS_PositiveResponse : UDS_NRC_GeneralProgrammingFailure;
    }
    if (a->dataId == 0x0101) {           /* период рассылки, мс: сразу */
        if (a->len != 2) return UDS_NRC_IncorrectMessageLengthOrInvalidFormat;
        uint16_t v = (uint16_t)((a->data[0] << 8) | a->data[1]);
        if (v < 20u || v > 1000u) return UDS_NRC_RequestOutOfRange;
        if (!ee_write(EE_PERIOD, v)) return UDS_NRC_GeneralProgrammingFailure;
        node.period_ms = v;
        return UDS_PositiveResponse;
    }
    return UDS_NRC_RequestOutOfRange;
}

UDSErr_t node_uds(UDSServer_t *srv, UDSEvent_t ev, void *arg) {
    switch (ev) {
    case UDS_EVT_DiagSessCtrl: {
        UDSDiagSessCtrlArgs_t *a = arg;
        a->p2_ms = 50;
        a->p2_star_ms = 5000;
        srv->securityLevel = 0;
        if (a->type == UDS_LEV_DS_PRGS && !node.boot_mode) {
            plat_reset(true);            /* ответ 50 02 уйдёт, потом перезапуск в загрузчик */
            return UDS_PositiveResponse;
        }
        if (a->type == UDS_LEV_DS_PRGS || a->type == UDS_LEV_DS_DS || a->type == UDS_LEV_DS_EXTDS)
            return UDS_PositiveResponse;
        return UDS_NRC_SubFunctionNotSupported;
    }
    case UDS_EVT_EcuReset: {
        UDSECUResetArgs_t *a = arg;
        return a->type == 0x01 ? UDS_PositiveResponse : UDS_NRC_SubFunctionNotSupported;
    }
    case UDS_EVT_DoScheduledReset:
        plat_reset(false);
        return UDS_PositiveResponse;
    case UDS_EVT_ReadDataByIdent:
        return read_did(srv, arg);
    case UDS_EVT_WriteDataByIdent:
        return node.boot_mode ? UDS_NRC_ServiceNotSupported : write_did(srv, arg);
    case UDS_EVT_SecAccessRequestSeed: {
        UDSSecAccessRequestSeedArgs_t *a = arg;
        if (a->level != SEC_LEVEL) return UDS_NRC_SubFunctionNotSupported;
        if (!node.boot_mode) return UDS_NRC_ServiceNotSupportedInActiveSession;
        rng ^= UDSMillis();
        rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5;
        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;
        return UDS_PositiveResponse;
    }
    case UDS_EVT_ReadDTCInformation: {
        UDSRDTCIArgs_t *a = arg;
        if (node.boot_mode || a->type != 0x02) return UDS_NRC_SubFunctionNotSupported;
        const uint8_t avail = 0x2F;      /* ведём биты 0, 1, 2, 3 и 5 */
        a->copy(srv, &avail, 1);
        for (int k = 0; k < DTC_N; k++)
            if (node.dtc_status[k] & a->subFuncArgs.dtcStatusByMaskArgs.mask) {
                uint8_t r[4] = {DTC_CODE[k][0], DTC_CODE[k][1], DTC_CODE[k][2], node.dtc_status[k]};
                a->copy(srv, r, 4);
            }
        return UDS_PositiveResponse;
    }
    case UDS_EVT_ClearDiagnosticInfo: {
        UDSCDIArgs_t *a = arg;
        if (node.boot_mode) return UDS_NRC_ServiceNotSupported;
        if (a->groupOfDTC != 0xFFFFFFu) return UDS_NRC_RequestOutOfRange;
        memset(node.dtc_status, 0, sizeof node.dtc_status);
        memset(deb_bad, 0, sizeof deb_bad);   /* неисправность, что есть сейчас, вернётся через 50 мс */
        return UDS_PositiveResponse;
    }
    case UDS_EVT_RoutineCtrl:
        return on_routine(srv, arg);
    case UDS_EVT_RequestDownload: {
        UDSRequestDownloadArgs_t *a = arg;
        if (!node.boot_mode || 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 = APP_BASE;
        dl_end = APP_BASE + (uint32_t)a->size;
        a->maxNumberOfBlockLength = BLOCK_LEN;
        return UDS_PositiveResponse;
    }
    case UDS_EVT_TransferData: {
        UDSTransferDataArgs_t *a = arg;
        if (dl_addr + a->len > dl_end || (a->len & 1u)) return UDS_NRC_TransferDataSuspended;
        for (uint16_t i = 0; i < a->len; i += 2)   /* F103 пишет полусловами */
            if (!fl_prog16(dl_addr + i, (uint16_t)(a->data[i] | (a->data[i + 1] << 8))))
                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;
    }
}

ee.c — EEPROM emulation on two flash memory pages.

/* Раздел 198у: эмуляция EEPROM на двух страницах флеш-памяти F103 по 1 КБ.
 * Заголовок страницы — 4 полуслова, каждое пишется один раз по стёртому месту:
 *   h0 = 0xEEEE — страница принимает копию, h1 = 0x0000 — страница действующая,
 *   h2 — поколение (растёт при каждом переносе). Записи с байта 8 по 4 байта:
 *   сначала значение, потом номер — запись без номера (обрыв между ними) не считается. */
#include "node.h"

#define H_RECV   0xEEEEu
#define H_VALID  0x0000u
#define SLOT0    8u
#define NSLOT    ((FL_PAGE - SLOT0) / 4u)       /* 254 записи на страницу */

uint32_t ee_transfers;
static uint32_t act;                           /* адрес действующей страницы */
static uint32_t next_slot;

static uint16_t rd(uint32_t a) { const uint8_t *p = fl_ptr(a); return (uint16_t)(p[0] | (p[1] << 8)); }
static int state(uint32_t pg) {                /* 0 — стёрта, 1 — принимает, 2 — действующая, 3 — мусор */
    uint16_t h0 = rd(pg), h1 = rd(pg + 2);
    if (h1 == H_VALID) return 2;
    if (h1 != 0xFFFFu) return 3;
    if (h0 == H_RECV) return 1;
    return h0 == 0xFFFFu ? 0 : 3;
}
static uint32_t other(uint32_t pg) { return pg == EE_PAGE0 ? EE_PAGE1 : EE_PAGE0; }

static void find_free(void) {
    uint32_t i = NSLOT;
    while (i > 0 && rd(act + SLOT0 + 4u * (i - 1u)) == 0xFFFFu && rd(act + SLOT0 + 4u * (i - 1u) + 2u) == 0xFFFFu)
        i--;
    next_slot = i;                             /* после последней занятой (даже битой) */
}

static bool erase_if_dirty(uint32_t pg) {
    for (uint32_t a = pg; a < pg + FL_PAGE; a += 2)
        if (rd(a) != 0xFFFFu) return fl_erase_page(pg);
    return true;
}

static bool lookup(uint32_t pg, uint16_t id, uint16_t *v) {
    for (uint32_t i = NSLOT; i > 0; i--) {     /* с конца: последняя запись — свежая */
        uint32_t a = pg + SLOT0 + 4u * (i - 1u);
        if (rd(a + 2u) == id) { *v = rd(a); return true; }
    }
    return false;
}

/* перенос на другую страницу: свежие значения всех номеров + новое (id, v) */
static bool transfer(uint16_t id, uint16_t v) {
    uint32_t dst = other(act);
    uint16_t gen = rd(act + 4u);
    if (!erase_if_dirty(dst) || !fl_prog16(dst, H_RECV) || !fl_prog16(dst + 4u, (uint16_t)(gen + 1u)))
        return false;
    uint32_t s = 0;
    for (uint16_t k = EE_NODE_ID; k <= EE_PERIOD; k++) {
        uint16_t x;
        if (k == id) x = v;
        else if (!lookup(act, k, &x)) continue;
        uint32_t a = dst + SLOT0 + 4u * s++;
        if (!fl_prog16(a, x) || !fl_prog16(a + 2u, k)) return false;
    }
    if (!fl_prog16(dst + 2u, H_VALID)) return false;   /* только теперь новая страница главная */
    if (!fl_erase_page(act)) return false;
    act = dst;
    ee_transfers++;
    find_free();
    return true;
}

int ee_init(void) {
    int s0 = state(EE_PAGE0), s1 = state(EE_PAGE1), fix = 0;
    if (s0 == 2 && s1 == 2) {                  /* обрыв после отметки новой, до стирания старой */
        act = (uint16_t)(rd(EE_PAGE1 + 4u) - rd(EE_PAGE0 + 4u)) == 1u ? EE_PAGE1 : EE_PAGE0;
        fl_erase_page(other(act));
        fix = 3;
    } else if (s0 == 2 || s1 == 2) {
        act = s0 == 2 ? EE_PAGE0 : EE_PAGE1;
        if (state(other(act)) != 0) { erase_if_dirty(other(act)); fix = 2; }   /* недоделанная копия */
    } else {                                   /* ни одной действующей: первое включение */
        erase_if_dirty(EE_PAGE0);
        erase_if_dirty(EE_PAGE1);
        fl_prog16(EE_PAGE0, H_RECV);
        fl_prog16(EE_PAGE0 + 4u, 1u);
        fl_prog16(EE_PAGE0 + 2u, H_VALID);
        act = EE_PAGE0;
        fix = 1;
    }
    find_free();
    return fix;
}

bool ee_read(uint16_t id, uint16_t *v) { return lookup(act, id, v); }

bool ee_write(uint16_t id, uint16_t v) {
    uint16_t old;
    if (lookup(act, id, &old) && old == v) return true;   /* то же значение — страницу не тратим */
    if (next_slot >= NSLOT) return transfer(id, v);
    uint32_t a = act + SLOT0 + 4u * next_slot++;
    return fl_prog16(a, v) && fl_prog16(a + 2u, id);
}

f103_hw.c — the common STM32F103C8 hardware layer for the bootloader and the application.

/* Раздел 198у: общее железо STM32F103C8 для загрузчика и приложения — регистры CMSIS, без HAL.
 * Кварц 8 МГц × 9 = 72 МГц, APB1 = 36 МГц; CAN на PB8/PB9 (ремап), 500 кбит/с:
 * 36 МГц / 4 = 9 МГц, 1 + 15 + 2 = 18 квантов. Флеш-память — страницами по 1 КБ. */
#include "stm32f10x.h"
#include "node.h"
#include "f103_hw.h"

#define BRP 4u
#define TS1 15u
#define TS2 2u
_Static_assert(36000000u / (BRP * (1u + TS1 + TS2)) == 500000u, "500 кбит/с ровно");

volatile uint32_t ms;
static can_rx_t ring[16];
static volatile uint32_t head, tail;

void SysTick_Handler(void) { ms++; }
uint32_t plat_ms(void) { return ms; }
uint32_t UDSMillis(void) { return ms; }
uint32_t isotp_user_get_us(void) {
    uint32_t m, v;
    do { m = ms; v = SysTick->VAL; } while (m != ms);
    return m * 1000u + (71999u - v) / 72u;
}
void isotp_user_debug(const char *message, ...) { (void)message; }

void clock_72mhz(void) {
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY)) {
    }
    FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;
    RCC->CFGR = RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9 | RCC_CFGR_PPRE1_DIV2 | RCC_CFGR_ADCPRE_DIV6;
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY)) {
    }
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
    }
    SysTick_Config(72000u);                               /* 1 мс */
}

/* ABOM = 0: из отключения выходим сами (node_on_bus_off); ERRIE + BOFIE — прерывание SCE */
void can_init(uint8_t node_id) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
    AFIO->MAPR |= AFIO_MAPR_CAN_REMAP_REMAP2;             /* CAN на PB8/PB9 */
    GPIOB->CRH = (GPIOB->CRH & ~0xFFu) | 0x4u | (0xBu << 4);

    CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_TXFP;              /* без ABOM, ящики по очереди записи */
    while (!(CAN1->MSR & CAN_MSR_INAK)) {
    }
    CAN1->BTR = ((TS2 - 1u) << 20) | ((TS1 - 1u) << 16) | (BRP - 1u);

    CAN1->FMR |= CAN_FMR_FINIT;
    CAN1->FA1R = 0;
    CAN1->FS1R = 1u;                                      /* фильтр 0 — 32 бита, режим списка */
    CAN1->FM1R = 1u;
    CAN1->sFilterRegister[0].FR1 = (ID_DIAG_REQ + node_id) << 21;   /* свой физический запрос */
    CAN1->sFilterRegister[0].FR2 = ID_DIAG_FUNC << 21;              /* и функциональный 0x7DF */
    CAN1->FFA1R = 0;                                      /* оба — в FIFO 0 */
    CAN1->FA1R = 1u;
    CAN1->FMR &= ~CAN_FMR_FINIT;

    CAN1->IER = CAN_IER_FMPIE0 | CAN_IER_ERRIE | CAN_IER_BOFIE | CAN_IER_TMEIE;
    NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
    NVIC_EnableIRQ(CAN1_SCE_IRQn);
    NVIC_EnableIRQ(USB_HP_CAN1_TX_IRQn);
    CAN1->MCR &= ~CAN_MCR_INRQ;
    while (CAN1->MSR & CAN_MSR_INAK) {
    }
}

int plat_can_send(uint32_t id, const uint8_t *d, uint8_t n) {
    uint32_t box = (CAN1->TSR & CAN_TSR_CODE) >> 24, lo = 0, hi = 0;
    if ((CAN1->TSR & (CAN_TSR_TME0 << box)) == 0u) return -1;   /* все три ящика заняты */
    for (uint32_t i = 0; i < n; i++) {
        if (i < 4u) lo |= (uint32_t)d[i] << (8u * i);
        else        hi |= (uint32_t)d[i] << (8u * (i - 4u));
    }
    CAN1->sTxMailBox[box].TDTR = n;
    CAN1->sTxMailBox[box].TDLR = lo;
    CAN1->sTxMailBox[box].TDHR = hi;
    CAN1->sTxMailBox[box].TIR = (id << 21) | CAN_TI0R_TXRQ;
    return 0;
}
int isotp_user_send_can(const uint32_t id, const uint8_t *data, const uint8_t size, void *arg) {
    (void)arg;
    return plat_can_send(id, data, size) == 0 ? ISOTP_RET_OK : ISOTP_RET_NOSPACE;
}

void USB_LP_CAN1_RX0_IRQHandler(void) {                  /* кадр — в кольцо, разбор в цикле */
    while (CAN1->RF0R & CAN_RF0R_FMP0) {
        can_rx_t *f = &ring[head % 16u];
        uint32_t lo = CAN1->sFIFOMailBox[0].RDLR, hi = CAN1->sFIFOMailBox[0].RDHR;
        f->id = CAN1->sFIFOMailBox[0].RIR >> 21;
        f->len = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 0xFu);
        for (uint32_t i = 0; i < 4u; i++) {
            f->d[i] = (uint8_t)(lo >> (8u * i));
            f->d[i + 4u] = (uint8_t)(hi >> (8u * i));
        }
        CAN1->RF0R = CAN_RF0R_RFOM0;
        if (head - tail < 16u) head++;
    }
}
bool can_rx(can_rx_t *out) {
    if (tail == head) return false;
    *out = ring[tail % 16u];
    tail++;
    return true;
}

void USB_HP_CAN1_TX_IRQHandler(void) {                   /* кадр ушёл: связь есть */
    uint32_t tsr = CAN1->TSR;
    CAN1->TSR = CAN_TSR_RQCP0 | CAN_TSR_RQCP1 | CAN_TSR_RQCP2;
    if (tsr & (CAN_TSR_TXOK0 | CAN_TSR_TXOK1 | CAN_TSR_TXOK2)) node_on_tx_ok();
}

void CAN1_SCE_IRQHandler(void) {                          /* ошибки: BOFF — отключены от шины */
    if (CAN1->ESR & CAN_ESR_BOFF) {
        CAN1->IER &= ~CAN_IER_BOFIE;                      /* флаг держится до восстановления */
        node_on_bus_off();
    }
    CAN1->MSR = CAN_MSR_ERRI;
}
void plat_can_recover(void) {                             /* INRQ 1 → 0; дальше 128 × 11 бит сам bxCAN */
    CAN1->MCR |= CAN_MCR_INRQ;
    while (!(CAN1->MSR & CAN_MSR_INAK)) {
    }
    CAN1->MCR &= ~CAN_MCR_INRQ;
    CAN1->IER |= CAN_IER_BOFIE;
}

/* ── флеш-память: ключи, стирание страницы, запись полусловами (RM0008 3.3.3) ── */
static void fl_unlock(void) {
    if (FLASH->CR & FLASH_CR_LOCK) {
        FLASH->KEYR = 0x45670123u;
        FLASH->KEYR = 0xCDEF89ABu;
    }
}
static bool fl_done(void) {
    while (FLASH->SR & FLASH_SR_BSY) {
    }
    bool ok = (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) == 0u;
    FLASH->SR = FLASH_SR_EOP | FLASH_SR_PGERR | FLASH_SR_WRPRTERR;
    return ok;
}
bool fl_erase_page(uint32_t addr) {
    if (addr < BOOT_BASE + BOOT_SIZE || addr >= 0x08010000u) return false;   /* загрузчик не трогаем */
    fl_unlock();
    FLASH->CR = FLASH_CR_PER;
    FLASH->AR = addr;
    FLASH->CR = FLASH_CR_PER | FLASH_CR_STRT;             /* процессор стоит 20…40 мс */
    bool ok = fl_done();
    FLASH->CR = FLASH_CR_LOCK;
    return ok;
}
bool fl_prog16(uint32_t addr, uint16_t v) {
    if (addr < BOOT_BASE + BOOT_SIZE || addr >= 0x08010000u) return false;
    fl_unlock();
    FLASH->CR = FLASH_CR_PG;
    *(volatile uint16_t *)addr = v;                       /* 40…70 мкс */
    bool ok = fl_done() && *(volatile uint16_t *)addr == v;
    FLASH->CR = FLASH_CR_LOCK;
    return ok;
}
const uint8_t *fl_ptr(uint32_t addr) { return (const uint8_t *)addr; }

/* перезапуск: слово в конце ОЗУ переживает сброс (startup его не трогает) */
void plat_reset(bool to_boot) {
    BOOT_REQ = to_boot ? BOOT_REQ_MAGIC : 0u;
    reset_pending = 1u;
}
void reset_if_pending(const UDSServer_t *srv) {           /* только когда ответ ушёл */
    if (reset_pending && !srv->requestInProgress && (CAN1->TSR & CAN_TSR_TME) == CAN_TSR_TME)
        NVIC_SystemReset();
}
volatile uint32_t reset_pending;

f103_app.c — the node application at address 0x08004000.

/* Раздел 198у: приложение узла на STM32F103C8 — с адреса 0x08004000.
 * PA0 — NTC (подтяжка 10 кОм), PA1 — датчик давления через делитель 2/3.
 * АЦП1 опрашивается программно раз в 10 мс (node_tick_1ms → plat_adc). */
#include "stm32f10x.h"
#include "node.h"
#include "f103_hw.h"

static UDSServer_t srv;
static UDSTpISOTpC_t tp;

static void adc_init(void) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN;
    GPIOA->CRL &= ~0xFFu;                                 /* PA0, PA1 — аналоговые входы */
    ADC1->SMPR2 = (7u << 0) | (7u << 3);                  /* 239,5 такта: высокоомный NTC */
    ADC1->CR2 = ADC_CR2_ADON;
    for (volatile int i = 0; i < 1000; i++) {
    }
    ADC1->CR2 |= ADC_CR2_RSTCAL;
    while (ADC1->CR2 & ADC_CR2_RSTCAL) {
    }
    ADC1->CR2 |= ADC_CR2_CAL;                             /* калибровка после включения */
    while (ADC1->CR2 & ADC_CR2_CAL) {
    }
}
uint16_t plat_adc(int ch) {
    ADC1->SQR3 = (uint32_t)ch;
    ADC1->CR2 |= ADC_CR2_ADON;                            /* повторная запись ADON — старт */
    while (!(ADC1->SR & ADC_SR_EOC)) {
    }
    return (uint16_t)ADC1->DR;
}

int main(void) {
    can_rx_t f;
    uint32_t last = 0;
    SCB->VTOR = APP_BASE;                                 /* таблица векторов приложения */
    clock_72mhz();
    adc_init();
    node_init(false);
    can_init(node.node_id);
    UDSServerInit(&srv);
    srv.fn = node_uds;
    srv.tp = &tp.hdl;
    UDSServerTpISOTpCInit(&tp, ID_DIAG_REQ + node.node_id, ID_DIAG_RESP + node.node_id, ID_DIAG_FUNC);
    __enable_irq();
    for (;;) {
        while (can_rx(&f))
            isotp_on_can_message(f.id == ID_DIAG_FUNC ? &tp.func_link : &tp.phys_link, f.d, f.len);
        UDSServerPoll(&srv);
        while (last != ms) {                              /* каждая миллисекунда по разу */
            last++;
            node_tick_1ms();
        }
        reset_if_pending(&srv);
    }
}

f103_boot.c — the node bootloader.

/* Раздел 198у: загрузчик узла на STM32F103C8 — страницы 0…15 (16 КБ).
 * Годный образ и нет просьбы остаться — сразу переход в приложение.
 * Иначе — сервер UDS на тех же идентификаторах, сеанс программирования. */
#include "stm32f10x.h"
#include "node.h"
#include "f103_hw.h"

static UDSServer_t srv;
static UDSTpISOTpC_t tp;

uint16_t plat_adc(int ch) { (void)ch; return 0; }         /* в загрузчике датчиков нет */

static void jump_to_app(void) {
    uint32_t sp = *(const volatile uint32_t *)APP_BASE;
    uint32_t pc = *(const volatile uint32_t *)(APP_BASE + 4u);
    __disable_irq();
    SysTick->CTRL = 0;                                    /* ничего от загрузчика не тикает */
    NVIC->ICER[0] = 0xFFFFFFFFu;
    NVIC->ICER[1] = 0xFFFFFFFFu;
    NVIC->ICPR[0] = 0xFFFFFFFFu;
    NVIC->ICPR[1] = 0xFFFFFFFFu;
    RCC->APB1RSTR |= RCC_APB1RSTR_CAN1RST;                /* bxCAN — в состояние после сброса */
    RCC->APB1RSTR &= ~RCC_APB1RSTR_CAN1RST;
    SCB->VTOR = APP_BASE;
    __set_MSP(sp);
    __enable_irq();
    ((void (*)(void))pc)();
}

int main(void) {
    can_rx_t f;
    bool stay = BOOT_REQ == BOOT_REQ_MAGIC;
    BOOT_REQ = 0;
    if (!stay && boot_image_ok())                         /* запись о годности + CRC-32 образа */
        jump_to_app();
    clock_72mhz();
    node_init(true);
    can_init(node.node_id);
    UDSServerInit(&srv);
    srv.fn = node_uds;
    srv.tp = &tp.hdl;
    UDSServerTpISOTpCInit(&tp, ID_DIAG_REQ + node.node_id, ID_DIAG_RESP + node.node_id, ID_DIAG_FUNC);
    if (stay) srv.sessionType = UDS_LEV_DS_PRGS;          /* пришли по 10 02 из приложения */
    __enable_irq();
    for (;;) {
        while (can_rx(&f))
            isotp_on_can_message(f.id == ID_DIAG_FUNC ? &tp.func_link : &tp.phys_link, f.d, f.len);
        UDSServerPoll(&srv);
        reset_if_pending(&srv);
    }
}

boot.ld — the bootloader linker script.

/* Раздел 198у: STM32F103C8, boot. Флеш-память 64 КБ страницами по 1 КБ:
 *   0x08000000 загрузчик 16 КБ | 0x08004000 приложение 45 КБ |
 *   0x0800F400 запись о годном образе 1 КБ | 0x0800F800 эмуляция EEPROM 2 × 1 КБ.
 * ОЗУ 20 КБ; последнее слово 0x20004FFC — просьба «остаться в загрузчике». */
ENTRY(Reset_Handler)

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 16K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K - 4
}

_estack     = ORIGIN(RAM) + LENGTH(RAM);
_stack_size = 0x600;

SECTIONS
{
  .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
  .text :
  {
    *(.text .text.*)
    *(.rodata .rodata.*)
    . = ALIGN(4);
  } > FLASH
  .data :
  {
    _sdata = .;
    *(.data .data.*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT > FLASH
  _sidata = LOADADDR(.data);
  .bss (NOLOAD) :
  {
    _sbss = .;
    *(.bss .bss.*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > RAM
  ASSERT(_ebss + _stack_size <= _estack, "RAM too small for .data + .bss + stack")
}

app.ld — the application linker script.

/* Раздел 198у: STM32F103C8, app. Флеш-память 64 КБ страницами по 1 КБ:
 *   0x08000000 загрузчик 16 КБ | 0x08004000 приложение 45 КБ |
 *   0x0800F400 запись о годном образе 1 КБ | 0x0800F800 эмуляция EEPROM 2 × 1 КБ.
 * ОЗУ 20 КБ; последнее слово 0x20004FFC — просьба «остаться в загрузчике». */
ENTRY(Reset_Handler)

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08004000, LENGTH = 45K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K - 4
}

_estack     = ORIGIN(RAM) + LENGTH(RAM);
_stack_size = 0x600;

SECTIONS
{
  .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
  .text :
  {
    *(.text .text.*)
    *(.rodata .rodata.*)
    . = ALIGN(4);
  } > FLASH
  .data :
  {
    _sdata = .;
    *(.data .data.*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT > FLASH
  _sidata = LOADADDR(.data);
  .bss (NOLOAD) :
  {
    _sbss = .;
    *(.bss .bss.*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > RAM
  ASSERT(_ebss + _stack_size <= _estack, "RAM too small for .data + .bss + stack")
}

Next: section 198ф "Mini-project: a USB flash drive on the B-U585I-IOT02A — the device serves its log as a disk" — a device without drivers: plug in the cable, and the log shows up as a file on a disk.

Open in the interactive handbook