Добавить загрузку F28335 по ProtoCAN и сервисные команды клиента

This commit is contained in:
2026-10-11 03:24:44 +03:00
parent f1a7bcd8bc
commit 3c0f6fd9e6
23 changed files with 1702 additions and 97 deletions
+164 -59
View File
@@ -1,21 +1,16 @@
/*
* Автомат адресной загрузки прошивки по ProtoCAN с сессией, контрольной суммой и A/B-слотами.
* Приём блоков, проверка образа и активация разделены на этапы; операции Flash и метаданные
* слотов предоставляет платформа.
*/
#include "pcan_boot.h"
/* ProtoCAN update state machine; platform owns storage and reset. */
#include "../include/pcan_boot.h"
#include <string.h>
#define PCAN_BOOT_ID_MASK 0x1FFFFFFFUL
static uint16_t get_u16(const uint8_t *p)
static uint16_t get_u16(const pcan_boot_byte_t *p)
{
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static uint32_t get_u32(const uint8_t *p)
static uint32_t get_u32(const pcan_boot_byte_t *p)
{
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
@@ -23,26 +18,26 @@ static uint32_t get_u32(const uint8_t *p)
| ((uint32_t)p[3] << 24);
}
static void put_u16(uint8_t *p, uint16_t value)
static void put_u16(pcan_boot_byte_t *p, uint16_t value)
{
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
p[0] = (pcan_boot_byte_t)(value & 0xFFU);
p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU);
}
static void put_u32(uint8_t *p, uint32_t value)
static void put_u32(pcan_boot_byte_t *p, uint32_t value)
{
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
p[2] = (uint8_t)(value >> 16);
p[3] = (uint8_t)(value >> 24);
p[0] = (pcan_boot_byte_t)(value & 0xFFU);
p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU);
p[2] = (pcan_boot_byte_t)((value >> 16) & 0xFFU);
p[3] = (pcan_boot_byte_t)(value >> 24);
}
static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length)
static uint32_t crc32_update(uint32_t crc, const pcan_boot_byte_t *data, size_t length)
{
size_t i;
crc = ~crc;
for (i = 0U; i < length; ++i) {
uint8_t bit;
pcan_boot_byte_t bit;
crc ^= data[i];
for (bit = 0U; bit < 8U; ++bit) {
crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U);
@@ -51,9 +46,9 @@ static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length)
return ~crc;
}
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
uint8_t device_type, uint8_t device_id,
uint8_t msg_type, uint16_t msg_body)
uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
pcan_boot_byte_t device_type, pcan_boot_byte_t device_id,
pcan_boot_byte_t msg_type, uint16_t msg_body)
{
return ((((uint32_t)priority & 1U) << 28)
| (((uint32_t)route & 1U) << 27)
@@ -63,14 +58,14 @@ uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
| msg_body) & PCAN_BOOT_ID_MASK;
}
uint8_t pcan_boot_id_route(uint32_t can_id) { return (uint8_t)((can_id >> 27) & 1U); }
uint8_t pcan_boot_id_device_type(uint32_t can_id) { return (uint8_t)((can_id >> 24) & 7U); }
uint8_t pcan_boot_id_device_id(uint32_t can_id) { return (uint8_t)((can_id >> 20) & 15U); }
uint8_t pcan_boot_id_msg_type(uint32_t can_id) { return (uint8_t)((can_id >> 16) & 15U); }
pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 27) & 1U); }
pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 24) & 7U); }
pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 20) & 15U); }
pcan_boot_byte_t pcan_boot_id_msg_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 16) & 15U); }
uint16_t pcan_boot_id_msg_body(uint32_t can_id) { return (uint16_t)can_id; }
static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body,
const uint8_t *data, uint8_t dlc)
static bool send_frame(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t body,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
{
uint32_t id = pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_DEVICE,
boot->config.device_type,
@@ -79,9 +74,12 @@ static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body,
return boot->port.send(boot->port_user, id, data, dlc);
}
static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status)
/* STATUS всегда содержит состояние передачи: status, target_slot, next_block, CRC32.
* CRC здесь относится к уже принятому потоку, а не заменяет проверку EEPROM.
* Номер следующего блока помогает клиенту продолжить обмен после потери ACK. */
static bool send_status(pcan_boot_t *boot, pcan_boot_byte_t command, pcan_boot_byte_t status)
{
uint8_t data[8];
pcan_boot_byte_t data[8];
uint16_t body = (uint16_t)(((uint16_t)boot->session_id << 8) | command);
data[0] = status;
data[1] = boot->target_slot;
@@ -91,9 +89,11 @@ static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status)
return send_frame(boot, PCAN_BOOT_MSG_STATUS, body, data, 8U);
}
/* IDENTIFY имеет отдельный тип сообщения DISCOVERY и Body=1. Он доступен
* без открытия сессии, поэтому обнаружение устройства не должно запускать обновление. */
static bool send_identity(pcan_boot_t *boot)
{
uint8_t data[8];
pcan_boot_byte_t data[8];
put_u16(&data[0], boot->config.product_type);
data[2] = boot->config.hardware_revision;
data[3] = PCAN_BOOT_VERSION;
@@ -101,6 +101,9 @@ static bool send_identity(pcan_boot_t *boot)
return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, 1U, data, 8U);
}
/* Отменяется только состояние протокола в RAM: manifest, счётчики и session.
* Функция не стирает EEPROM и не снимает ранее опубликованный pending.
* Ответственность за постоянное состояние слотов остаётся у платформы. */
void pcan_boot_abort(pcan_boot_t *boot)
{
if (boot == NULL) {
@@ -118,19 +121,24 @@ void pcan_boot_abort(pcan_boot_t *boot)
boot->have_compat = false;
}
/* Конфигурация копируется в экземпляр; указатели callbacks остаются привязаны
* к port_user. Значения slot_count=0 и max_image_size=0 сохраняют исторические
* настройки по умолчанию. Для пустого A/B-хранилища допустим active_slot=NONE. */
bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
const pcan_boot_port_t *port, void *port_user)
{
uint8_t slot_count;
pcan_boot_byte_t slot_count;
if ((boot == NULL) || (config == NULL) || (port == NULL)
|| (config->device_type > 7U) || (config->device_id > 15U)
|| (config->active_slot > 1U)
|| ((config->active_slot > 1U) && (config->active_slot != PCAN_BOOT_SLOT_NONE))
|| (port->send == NULL) || (port->erase_slot == NULL)
|| (port->write_slot == NULL)) {
return false;
}
slot_count = config->slot_count == 0U ? 2U : config->slot_count;
if ((slot_count > 2U) || (config->active_slot >= slot_count)
if ((slot_count > 2U) || ((config->active_slot >= slot_count) &&
(config->active_slot != PCAN_BOOT_SLOT_NONE || slot_count != 2U))
|| config->max_image_size > PCAN_BOOT_SLOT_SIZE
|| ((slot_count == 2U) && (port->set_pending_slot == NULL))) {
return false;
}
@@ -149,7 +157,10 @@ bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
return true;
}
static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
/* BEGIN_IMAGE и BEGIN_COMPAT приходят отдельными кадрами. До получения обоих
* не разрешаем ERASE. Проверяем размер, Product, HW и необязательную политику
* authorize. В A/B выбираем противоположный active слот; для первого образа — A. */
static bool metadata_complete(pcan_boot_t *boot, pcan_boot_byte_t command)
{
if (!boot->have_image || !boot->have_compat) {
boot->state = PCAN_BOOT_STATE_METADATA;
@@ -160,8 +171,11 @@ static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
}
if ((boot->manifest.product_type != boot->config.product_type)
|| (boot->config.hardware_revision < boot->manifest.hardware_revision_min)
if (boot->manifest.product_type != boot->config.product_type) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_DEVICE);
}
if ((boot->config.hardware_revision < boot->manifest.hardware_revision_min)
|| (boot->config.hardware_revision > boot->manifest.hardware_revision_max)) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_HARDWARE);
@@ -171,19 +185,51 @@ static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR);
}
boot->target_slot = boot->config.slot_count == 1U
boot->target_slot = boot->config.active_slot == PCAN_BOOT_SLOT_NONE ? 0U :
boot->config.slot_count == 1U
? boot->config.active_slot
: (uint8_t)(boot->config.active_slot ^ 1U);
: (pcan_boot_byte_t)(boot->config.active_slot ^ 1U);
boot->state = PCAN_BOOT_STATE_READY_TO_ERASE;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
/* Body состоит из session в старшем октете и command в младшем.
* Сначала проверяем DLC, чтобы обработчики не читали отсутствующие данные.
* Идентификация и паспорт не открывают сессию; CONFIRM обрабатывается отдельно,
* поскольку приложение не должно входить в загрузчик ради подтверждения себя.
* Остальные команды изменяют RAM-автомат только в допустимой сессии/состоянии. */
static bool process_control(pcan_boot_t *boot, uint16_t body,
const uint8_t *data, uint8_t dlc)
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
{
uint8_t session = (uint8_t)(body >> 8);
uint8_t command = (uint8_t)body;
pcan_boot_byte_t session = (pcan_boot_byte_t)(body >> 8);
pcan_boot_byte_t command = (pcan_boot_byte_t)(body & 0xFFU);
if (dlc != ((command == PCAN_BOOT_CMD_BEGIN_IMAGE || command == PCAN_BOOT_CMD_BEGIN_COMPAT) ? 8U :
(command == PCAN_BOOT_CMD_DEVICE_NAME || command == PCAN_BOOT_CMD_MEMORY_INFO) ? 1U : 0U)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
}
if (command == PCAN_BOOT_CMD_PING) {
pcan_boot_byte_t reply[8] = {0};
reply[0] = PCAN_BOOT_VERSION; reply[1] = boot->state;
reply[2] = boot->config.active_slot; reply[3] = boot->target_slot;
reply[4] = boot->session_id; reply[5] = boot->config.slot_count;
reply[6] = boot->last_status;
return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, command, reply, 8);
}
if (command >= PCAN_BOOT_CMD_CAPABILITIES && command <= PCAN_BOOT_CMD_MEMORY_INFO) {
pcan_boot_byte_t reply[8] = {0};
pcan_boot_byte_t page = dlc ? data[0] : 0;
if (!boot->port.service_info || !boot->port.service_info(boot->port_user,command,page,reply))
return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_COMMAND);
return send_frame(boot,PCAN_BOOT_MSG_DISCOVERY,(uint16_t)((page<<8)|command),reply,8);
}
if (command == PCAN_BOOT_CMD_CONFIRM) {
pcan_boot_byte_t saved=boot->session_id; bool sent;
boot->session_id=session;
sent=send_status(boot,command,session && boot->port.confirm_running_slot &&
boot->port.confirm_running_slot(boot->port_user) ? PCAN_BOOT_STATUS_OK : PCAN_BOOT_STATUS_INVALID_STATE);
boot->session_id=saved; return sent;
}
if (command == PCAN_BOOT_CMD_IDENTIFY) {
return send_identity(boot);
}
@@ -191,8 +237,18 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
if (session == 0U) {
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
}
if (boot->state != PCAN_BOOT_STATE_IDLE && boot->session_id != session)
return send_status(boot,command,PCAN_BOOT_STATUS_BUSY);
if (boot->port.enter_boot) {
unsigned int result=boot->port.enter_boot(boot->port_user,session);
if(result==1) return true;
if(result!=0) return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_STATE);
}
if (boot->session_id==session && boot->state!=PCAN_BOOT_STATE_IDLE)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
pcan_boot_abort(boot);
boot->session_id = session;
boot->last_activity_ms=boot->now_ms;
boot->state = PCAN_BOOT_STATE_METADATA;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
@@ -200,8 +256,11 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
}
boot->last_activity_ms=boot->now_ms;
switch (command) {
case PCAN_BOOT_CMD_BEGIN_IMAGE:
if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.image_size==get_u32(data) &&
boot->manifest.image_crc32==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -211,6 +270,9 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return metadata_complete(boot, command);
case PCAN_BOOT_CMD_BEGIN_COMPAT:
if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.product_type==get_u16(data) &&
boot->manifest.hardware_revision_min==data[2] && boot->manifest.hardware_revision_max==data[3] &&
boot->manifest.firmware_version==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -222,6 +284,8 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return metadata_complete(boot, command);
case PCAN_BOOT_CMD_ERASE:
if(boot->state==PCAN_BOOT_STATE_RECEIVING && boot->bytes_received==0)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if (boot->state != PCAN_BOOT_STATE_READY_TO_ERASE) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -236,6 +300,8 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_VERIFY:
if(boot->state==PCAN_BOOT_STATE_VERIFIED || boot->state==PCAN_BOOT_STATE_COMMITTED)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((boot->state != PCAN_BOOT_STATE_RECEIVING)
|| (boot->bytes_received != boot->manifest.image_size)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
@@ -244,7 +310,13 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_CRC_ERROR);
}
if ((boot->port.verify_image != NULL)
if (boot->port.verify_status) {
pcan_boot_status_t status=boot->port.verify_status(boot->port_user,boot->target_slot,&boot->manifest);
if(status!=PCAN_BOOT_STATUS_OK) {
boot->state=PCAN_BOOT_STATE_FAILED; return send_status(boot,command,status);
}
}
if (!boot->port.verify_status && (boot->port.verify_image != NULL)
&& !boot->port.verify_image(boot->port_user, boot->target_slot,
&boot->manifest)) {
boot->state = PCAN_BOOT_STATE_FAILED;
@@ -254,21 +326,16 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_COMMIT:
if(boot->state==PCAN_BOOT_STATE_COMMITTED) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if (boot->state != PCAN_BOOT_STATE_VERIFIED) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
if ((boot->config.slot_count == 2U)
if ((boot->port.set_pending_slot != NULL)
&& !boot->port.set_pending_slot(boot->port_user, boot->target_slot,
&boot->manifest)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_CONFIRM:
if ((boot->port.confirm_running_slot == NULL)
|| !boot->port.confirm_running_slot(boot->port_user)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
boot->state=PCAN_BOOT_STATE_COMMITTED;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_QUERY_PROGRESS:
@@ -280,7 +347,9 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_REBOOT:
(void)send_status(boot, command, PCAN_BOOT_STATUS_OK);
if(!boot->port.reboot || (boot->port.can_reboot && !boot->port.can_reboot(boot->port_user)))
return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_STATE);
if(!send_status(boot,command,PCAN_BOOT_STATUS_OK)) return false;
if (boot->port.reboot != NULL) {
boot->port.reboot(boot->port_user);
}
@@ -291,11 +360,16 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
}
}
static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
const uint8_t *data, uint8_t dlc)
/* DATA не содержит session в Body: все 16 бит заняты номером блока. Поэтому
* кадры данных принимаются только в состоянии RECEIVING и для выбранного A/B.
* При неверном номере ничего не записываем и возвращаем ожидаемый next_block.
* Последний кадр дополняется клиентом до 8 октетов, но запись и CRC учитывают
* только оставшиеся октеты image_size. ACK выдаётся на границе окна или в конце. */
static bool process_data(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t block,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
{
uint8_t expected_type;
uint8_t write_length;
pcan_boot_byte_t expected_type;
pcan_boot_byte_t write_length;
uint32_t remaining;
if (boot->state != PCAN_BOOT_STATE_RECEIVING) {
@@ -309,8 +383,9 @@ static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
if ((block != boot->next_block) || (dlc != PCAN_BOOT_BLOCK_SIZE)) {
return send_status(boot, 0U, PCAN_BOOT_STATUS_SEQUENCE_ERROR);
}
boot->last_activity_ms=boot->now_ms;
remaining = boot->manifest.image_size - boot->bytes_received;
write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (uint8_t)remaining
write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (pcan_boot_byte_t)remaining
: PCAN_BOOT_BLOCK_SIZE;
if ((write_length == 0U)
|| !boot->port.write_slot(boot->port_user, boot->target_slot,
@@ -329,10 +404,14 @@ static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
return true;
}
/* Общая точка входа для одного кадра после аппаратной фильтрации.
* Проверки адреса повторяются программно, чтобы широкая маска CAN не разрешила
* обновление чужого устройства. Broadcast допускается только для DISCOVERY
* с Body=0/DLC=0; ответ откладывается до poll для разведения устройств по времени. */
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
const uint8_t *data, uint8_t dlc)
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
{
uint8_t msg_type;
pcan_boot_byte_t msg_type;
if ((boot == NULL) || (dlc > 8U) || ((dlc != 0U) && (data == NULL))) {
return false;
}
@@ -340,6 +419,17 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
if ((msg_type < PCAN_BOOT_MSG_CONTROL) || (msg_type > PCAN_BOOT_MSG_DISCOVERY)) {
return false;
}
if(msg_type==PCAN_BOOT_MSG_DISCOVERY && pcan_boot_id_route(can_id)==0 &&
pcan_boot_id_msg_body(can_id)==0 && dlc==0 &&
((pcan_boot_id_device_type(can_id)==7 && pcan_boot_id_device_id(can_id)==15) ||
(pcan_boot_id_device_type(can_id)==boot->config.device_type &&
pcan_boot_id_device_id(can_id)==boot->config.device_id))) {
if(!boot->discovery_pending) {
boot->discovery_at=boot->now_ms+2U*(1U+16U*boot->config.device_type+boot->config.device_id);
boot->discovery_pending=true;
}
return true;
}
if ((pcan_boot_id_route(can_id) != PCAN_BOOT_ROUTE_FROM_PM)
|| (pcan_boot_id_device_type(can_id) != boot->config.device_type)
|| (pcan_boot_id_device_id(can_id) != boot->config.device_id)) {
@@ -353,3 +443,18 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
}
return false;
}
/* Обслуживание времени не требует входящего кадра. Регулярно вызывать из main
* с монотонным счётчиком миллисекунд: иначе не истечёт сессия и не выйдет discovery.
* При занятом TX discovery остаётся pending до успешного send_identity.
* Вычитание unsigned учитывает переход счётчика через 0xFFFFFFFF. */
void pcan_boot_poll(pcan_boot_t *boot,uint32_t now_ms)
{
if(!boot) return;
boot->now_ms=now_ms;
if(boot->config.session_timeout_ms && boot->state!=PCAN_BOOT_STATE_IDLE &&
(uint32_t)(now_ms-boot->last_activity_ms)>=boot->config.session_timeout_ms)
pcan_boot_abort(boot);
if(boot->discovery_pending && (uint32_t)(now_ms-boot->discovery_at)<0x80000000UL && send_identity(boot))
boot->discovery_pending=false;
}