1. 前言
在上一篇《【STM32U3 评测】基于 CAN 的 UDS OTA 实现不完全指南》中,我们基于 NUCLEO-U3C5ZI-Q 上的 STM32U3C5 实现了一个完整的 CAN UDS OTA Bootloader。
该版本使用 Classic CAN 1 Mbps,已经完成了稳定性测试和 OTA 升级测试,验证了如下链路:
CAN -> ISO-TP -> UDS -> 擦除 APP -> 下载 APP -> CRC 校验 -> 标记有效 -> ECUReset -> 跳转 APP
最近我们拿到了一个新的 USB CAN FD 模块,因此可以进一步将通信链路从 Classic CAN 升级到 CAN FD。CAN FD 支持更大的数据帧长度,单帧最多可承载 64 字节数据,同时支持 BRS,也就是在仲裁段使用较低波特率,在数据段切换到更高波特率。
2. 硬件条件
本次测试开发板依然为 NUCLEO-U3C5ZI-Q。该板级设计中,为了追求低功耗和简化 BOM,并没有实际放置原理图中的 16 MHz HSE 高速无源晶振,仅保留了 32.768 kHz LSE。


当前时钟条件如下:
LSE: 32.768 kHz
SYSCLK: MSIS 96 MHz
FDCAN kernel clock: SYSCLK 96 MHz
这带来一个问题:96 MHz FDCAN clock 无法精确整除 5 Mbps。
CAN FD data phase 的 bit timing 计算公式为:
$$
Baudrate = \frac{FDCAN_CLK}{Prescaler \times (1 + TimeSeg1 + TimeSeg2)}
$$
当前:
FDCAN_CLK = 96 MHz
目标 Data bitrate = 5 Mbps
则:
$$
\frac{96 MHz}{5 Mbps} = 19.2
$$
也就是说,如果要精确得到 5 Mbps,一个 bit 需要 19.2 个 tq,但硬件只能配置整数 tq,无法配置 19.2 tq。
如果向下取整为 19 tq:
Actual bitrate = 96 MHz / 19 ≈ 5.0526 Mbps
Error ≈ +1.05%
如果取 20 tq:
Actual bitrate = 96 MHz / 20 = 4.8 Mbps
Error = -4.0%
因此,在当前 96 MHz FDCAN clock 条件下,NUCLEO-U3C5ZI-Q 并不适合配置标准 5 Mbps CAN FD data bitrate。
在当前硬件条件下,理论上更合适的 CAN FD 配置主要有两种:
CAN FD 500K / 4M
CAN FD 500K / 2M
其中 500K 是仲裁段波特率,2M / 4M 是 BRS 打开后的数据段波特率。
本次稳定性测试的核心代码如下:
FDCAN_FilterTypeDef sFilterConfig;
FDCAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[64];
for (uint8_t i = 0; i < 64; i++) {
TxData[i] = i;
}
sFilterConfig.IdType = FDCAN_STANDARD_ID;
sFilterConfig.FilterIndex = 0;
sFilterConfig.FilterType = FDCAN_FILTER_MASK;
sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
sFilterConfig.FilterID1 = 0x000;
sFilterConfig.FilterID2 = 0x000;
if (HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig) != HAL_OK) {
Error_Handler();
}
if (HAL_FDCAN_Start(&hfdcan1) != HAL_OK) {
Error_Handler();
}
TxHeader.Identifier = 0x111;
TxHeader.IdType = FDCAN_STANDARD_ID;
TxHeader.TxFrameType = FDCAN_DATA_FRAME;
TxHeader.DataLength = FDCAN_DLC_BYTES_64;
TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
TxHeader.BitRateSwitch = FDCAN_BRS_ON;
TxHeader.FDFormat = FDCAN_FD_CAN;
TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
TxHeader.MessageMarker = 0;
while (1) {
uint32_t free_level = HAL_FDCAN_GetTxFifoFreeLevel(&hfdcan1);
if (free_level > 0U) {
HAL_StatusTypeDef st = HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &TxHeader, TxData);
if (st != HAL_OK) {
FDCAN_ProtocolStatusTypeDef ps;
FDCAN_ErrorCountersTypeDef ec;
HAL_FDCAN_GetProtocolStatus(&hfdcan1, &ps);
HAL_FDCAN_GetErrorCounters(&hfdcan1, &ec);
printf("[TX] Add fail st=%d free=%lu LEC=%lu DLEC=%lu ACT=%lu BO=%lu EP=%lu EW=%lu TEC=%lu REC=%lu\r\n",
st,
free_level,
ps.LastErrorCode,
ps.DataLastErrorCode,
ps.Activity,
ps.BusOff,
ps.ErrorPassive,
ps.Warning,
ec.TxErrorCnt,
ec.RxErrorCnt);
}
} else {
FDCAN_ProtocolStatusTypeDef ps;
FDCAN_ErrorCountersTypeDef ec;
HAL_FDCAN_GetProtocolStatus(&hfdcan1, &ps);
HAL_FDCAN_GetErrorCounters(&hfdcan1, &ec);
printf("[TX] FIFO full LEC=%lu DLEC=%lu ACT=%lu BO=%lu EP=%lu EW=%lu TEC=%lu REC=%lu\r\n",
ps.LastErrorCode,
ps.DataLastErrorCode,
ps.Activity,
ps.BusOff,
ps.ErrorPassive,
ps.Warning,
ec.TxErrorCnt,
ec.RxErrorCnt);
}
HAL_Delay(10);
}
为了提高内部 MSIS 时钟的稳定性,我们在 CubeMX 和代码中启用了 LSE,并使用 LSE 对 MSIS RC0 进行 PLL / 校准。

HAL_PWR_EnableBkUpAccess();
__HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE | RCC_OSCILLATORTYPE_MSIS;
RCC_OscInitStruct.LSEState = RCC_LSE_ON;
RCC_OscInitStruct.MSISState = RCC_MSI_ON;
RCC_OscInitStruct.MSISSource = RCC_MSI_RC0;
RCC_OscInitStruct.MSISDiv = RCC_MSI_DIV1;
启用 MSIS RC0 的 LSE 校准:
RCC_MSIRCxPLLTypeDef pll_config = {0};
pll_config.State = RCC_MSIRCx_PLL_ON;
pll_config.InputSrce = RCC_MSIRCx_PLL_INPUT_LSE;
if (HAL_RCCEx_MSIRCxPLLModeConfig(RCC_MSI_RC0, &pll_config) != HAL_OK)
{
Error_Handler();
}
CAN FD引脚的 GPIO 速度也需要设置为 Very High。

GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF9_FDCAN1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
3. 稳定性测试
在升级 UDS OTA 之前,我们先单独做了 CAN FD 发送稳定性测试。
Classic CAN 1 Mbps 作为基线测试非常稳定,连续发送超过 40 万帧无明显错误。因此,后续 CAN FD 问题并不是普通 CAN 通道完全不通,而是集中在 CAN FD BRS 高速数据段。
3.1 CAN FD 500K / 4M
在 96 MHz FDCAN clock 下,为了达到 500K / 4M,STM32U3 FDCAN timing 可以设置为:
hfdcan1.Init.NominalPrescaler = 12;
hfdcan1.Init.NominalSyncJumpWidth = 4;
hfdcan1.Init.NominalTimeSeg1 = 11;
hfdcan1.Init.NominalTimeSeg2 = 4;
hfdcan1.Init.DataPrescaler = 2;
hfdcan1.Init.DataSyncJumpWidth = 3;
hfdcan1.Init.DataTimeSeg1 = 8;
hfdcan1.Init.DataTimeSeg2 = 3;
这样可达到:
Nominal = 96 MHz / 12 / (1 + 11 + 4) = 500 kbit/s
Data = 96 MHz / 2 / (1 + 8 + 3) = 4 Mbit/s
从数学整除性看,500K / 4M 是可以配置出来的。但在实际测试中,发送一段时间后 STM32U3 会进入 Bus-Off。

关键字段含义如下:
| 字段 |
含义 |
| BO=1 |
FDCAN 已进入 Bus-Off |
| EP=1 |
Error Passive |
| EW=1 |
Error Warning |
| TEC≈253 |
发送错误计数接近 Bus-Off 阈值 |
| REC=0 |
接收错误计数为 0 |
| FIFO full |
发送失败帧占满 TX FIFO |
TEC 快速增加而 REC=0,说明主要问题发生在发送方向。也就是说,STM32U3 发出的 CAN FD 帧没有被对端稳定正确接收和 ACK,或者在 CAN FD data phase 中出现错误,最终导致发送错误计数持续累加并进入 Bus-Off。
在这个阶段,我们也调整过终端阻抗、GPIO 速度和 TDC,但 4M 仍然无法长时间稳定运行。因此,在当前无 HSE 的硬件条件下,500K / 4M 不适合作为默认 OTA 通信配置。
3.2 CAN FD 500K / 2M
随后将 data bitrate 降为 2M:
Nominal bitrate: 500 kbit/s
Data bitrate: 2 Mbit/s
对应 STM32U3 timing:
hfdcan1.Init.NominalPrescaler = 12;
hfdcan1.Init.NominalSyncJumpWidth = 4;
hfdcan1.Init.NominalTimeSeg1 = 11;
hfdcan1.Init.NominalTimeSeg2 = 4;
hfdcan1.Init.DataPrescaler = 3;
hfdcan1.Init.DataSyncJumpWidth = 4;
hfdcan1.Init.DataTimeSeg1 = 11;
hfdcan1.Init.DataTimeSeg2 = 4;
计算结果为:
Nominal = 96 MHz / 12 / (1 + 11 + 4) = 500 kbit/s
Data = 96 MHz / 3 / (1 + 11 + 4) = 2 Mbit/s
PCAN-View结果:


这说明在当前硬件条件下,500K / 2M 是一个更可靠的 CAN FD 配置。
4. UDS OTA
4.1 Bootloader
CAN FD 相比 Classic CAN 最大的变化是:链路层单帧最大长度从 8 字节提升到了 64 字节。因此,Bootloader 中的 CAN IF 层需要完整支持 FDCAN DLC 映射、CAN FD 帧发送,以及接收端 DLC 到实际长度的转换。
can_if.c 代码如下:
#include "can_if.h"
#include "can_iap_config.h"
#include <string.h>
static FDCAN_HandleTypeDef *s_hfdcan;
static can_if_rx_cb_t s_rx_cb;
static uint8_t fdcan_dlc_to_len(uint32_t dlc)
{
switch (dlc) {
case FDCAN_DLC_BYTES_0: return 0;
case FDCAN_DLC_BYTES_1: return 1;
case FDCAN_DLC_BYTES_2: return 2;
case FDCAN_DLC_BYTES_3: return 3;
case FDCAN_DLC_BYTES_4: return 4;
case FDCAN_DLC_BYTES_5: return 5;
case FDCAN_DLC_BYTES_6: return 6;
case FDCAN_DLC_BYTES_7: return 7;
case FDCAN_DLC_BYTES_8: return 8;
case FDCAN_DLC_BYTES_12: return 12;
case FDCAN_DLC_BYTES_16: return 16;
case FDCAN_DLC_BYTES_20: return 20;
case FDCAN_DLC_BYTES_24: return 24;
case FDCAN_DLC_BYTES_32: return 32;
case FDCAN_DLC_BYTES_48: return 48;
case FDCAN_DLC_BYTES_64: return 64;
default: return 0;
}
}
static uint32_t len_to_fdcan_dlc(uint8_t len)
{
if (len <= 0U) return FDCAN_DLC_BYTES_0;
if (len <= 1U) return FDCAN_DLC_BYTES_1;
if (len <= 2U) return FDCAN_DLC_BYTES_2;
if (len <= 3U) return FDCAN_DLC_BYTES_3;
if (len <= 4U) return FDCAN_DLC_BYTES_4;
if (len <= 5U) return FDCAN_DLC_BYTES_5;
if (len <= 6U) return FDCAN_DLC_BYTES_6;
if (len <= 7U) return FDCAN_DLC_BYTES_7;
if (len <= 8U) return FDCAN_DLC_BYTES_8;
if (len <= 12U) return FDCAN_DLC_BYTES_12;
if (len <= 16U) return FDCAN_DLC_BYTES_16;
if (len <= 20U) return FDCAN_DLC_BYTES_20;
if (len <= 24U) return FDCAN_DLC_BYTES_24;
if (len <= 32U) return FDCAN_DLC_BYTES_32;
if (len <= 48U) return FDCAN_DLC_BYTES_48;
return FDCAN_DLC_BYTES_64;
}
void CanIf_Init(FDCAN_HandleTypeDef *hfdcan, can_if_rx_cb_t cb)
{
s_hfdcan = hfdcan;
s_rx_cb = cb;
FDCAN_FilterTypeDef f = {0};
f.IdType = FDCAN_STANDARD_ID;
f.FilterIndex = 0;
f.FilterType = FDCAN_FILTER_MASK;
f.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
/* Accept all standard IDs. UDS request IDs are filtered in IsoTp_OnCanFrame(). */
f.FilterID1 = 0x000;
f.FilterID2 = 0x000;
(void)HAL_FDCAN_ConfigFilter(s_hfdcan, &f);
#if defined(HAL_FDCAN_REJECT_REMOTE)
(void)HAL_FDCAN_ConfigGlobalFilter(s_hfdcan,
FDCAN_ACCEPT_IN_RX_FIFO0,
FDCAN_REJECT,
FDCAN_REJECT_REMOTE,
FDCAN_REJECT_REMOTE);
#endif
(void)HAL_FDCAN_Start(s_hfdcan);
}
int CanIf_Send(uint32_t std_id, const uint8_t *data, uint8_t len)
{
if (s_hfdcan == NULL || data == NULL || len > ISOTP_LL_DL || ISOTP_LL_DL > 64U) {
return -1;
}
FDCAN_TxHeaderTypeDef h = {0};
uint8_t buf[64];
memset(buf, 0, sizeof(buf));
memcpy(buf, data, len);
h.Identifier = std_id;
h.IdType = FDCAN_STANDARD_ID;
h.TxFrameType = FDCAN_DATA_FRAME;
#if (IAP_CANFD_ENABLE != 0U)
/*
* In CAN FD mode always send 64-byte frames.
* This keeps ISO-TP link-layer length deterministic and matches
* PC-side tx_data_length=64.
*/
h.DataLength = len_to_fdcan_dlc((uint8_t)ISOTP_LL_DL);
h.BitRateSwitch = (IAP_CANFD_BRS_ENABLE != 0U) ? FDCAN_BRS_ON : FDCAN_BRS_OFF;
h.FDFormat = FDCAN_FD_CAN;
#else
h.DataLength = len_to_fdcan_dlc(len);
h.BitRateSwitch = FDCAN_BRS_OFF;
h.FDFormat = FDCAN_CLASSIC_CAN;
#endif
h.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
h.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
h.MessageMarker = 0;
return (HAL_FDCAN_AddMessageToTxFifoQ(s_hfdcan, &h, buf) == HAL_OK) ? 0 : -1;
}
void CanIf_Poll(void)
{
if (s_hfdcan == NULL || s_rx_cb == NULL) {
return;
}
while (HAL_FDCAN_GetRxFifoFillLevel(s_hfdcan, FDCAN_RX_FIFO0) > 0U) {
FDCAN_RxHeaderTypeDef h = {0};
can_if_frame_t frame;
memset(&frame, 0, sizeof(frame));
if (HAL_FDCAN_GetRxMessage(s_hfdcan, FDCAN_RX_FIFO0, &h, frame.data) != HAL_OK) {
return;
}
if (h.IdType != FDCAN_STANDARD_ID) {
continue;
}
if (h.RxFrameType != FDCAN_DATA_FRAME) {
continue;
}
frame.id = h.Identifier;
frame.dlc = fdcan_dlc_to_len(h.DataLength);
if (frame.dlc > 64U) {
frame.dlc = 64U;
}
s_rx_cb(&frame);
}
}
在 CAN FD 模式下,我们固定使用 64-byte CAN FD frame,这样 PC 端 tx_data_length=64 与 Bootloader 端 ISOTP_LL_DL=64 保持一致。
测试脚本如下:
from __future__ import annotations
import argparse
import datetime
import time
import zlib
import can
import isotp
import udsoncan
from udsoncan import services, DidCodec, MemoryLocation
from udsoncan.client import Client
from udsoncan.connections import PythonIsoTpConnection
from udsoncan.exceptions import TimeoutException
from udsoncan.services import DiagnosticSessionControl, RoutineControl, CommunicationControl, ECUReset
from ihex import IHexFile
REQ_PHYS = 0x736
REQ_FUNC = 0x7DF
RESP_ID = 0x7B6
APP_BASE = 0x08020000
APP_LIMIT = 0x081F0000
ROUTINE_PROGRAM_COND_CHECK = 0x0203
ROUTINE_CHECK_CRC = 0x0202
ROUTINE_ERASE_MEMORY = 0xFF00
ROUTINE_CHECK_DEPENDENCIES = 0xFF01
PCAN_INTERFACE = "pcan"
PCAN_DEFAULT_CHANNEL = "PCAN_USBBUS1"
# PCAN FD timing for nominal 500k / data 2M.
PCAN_FD_TIMING = dict(
fd=True,
f_clock_mhz=80,
nom_brp=10,
nom_tseg1=11,
nom_tseg2=4,
nom_sjw=4,
data_brp=2,
data_tseg1=14,
data_tseg2=5,
data_sjw=4,
)
class RawDidCodec(DidCodec):
def encode(self, did_value):
return did_value
def decode(self, did_payload):
return did_payload
def __len__(self):
return 1
config = dict(udsoncan.configs.default_client_config)
config["use_server_timing"] = False
config["request_timeout"] = 30
config["p2_timeout"] = 5.0
config["p2_star_timeout"] = 5.0
config["data_identifiers"] = {
0xF184: RawDidCodec,
0xF199: RawDidCodec,
}
config["security_algo"] = lambda level, seed, params: b"\x00\x00\x00\x00"
config["security_algo_params"] = None
def make_isotp_params(stmin: int, blocksize: int) -> dict:
return {
"stmin": stmin,
"blocksize": blocksize,
"wftmax": 0,
"tx_data_length": 64,
"tx_data_min_length": 64,
"tx_padding": 0x00,
"rx_flowcontrol_timeout": 1000,
"rx_consecutive_frame_timeout": 1000,
"max_frame_size": 4095,
"can_fd": True,
"bitrate_switch": True,
}
def make_conn(bus: can.BusABC, params: dict, txid: int = REQ_PHYS, rxid: int = RESP_ID):
addr = isotp.Address(isotp.AddressingMode.Normal_11bits, txid=txid, rxid=rxid)
stack = isotp.CanStack(bus=bus, address=addr, params=params)
return PythonIsoTpConnection(stack)
def validate_app_range(addr: int, length: int) -> None:
if addr != APP_BASE:
raise RuntimeError(f"APP HEX must start at 0x{APP_BASE:08X}; got 0x{addr:08X}")
if length <= 0 or addr + length > APP_LIMIT:
raise RuntimeError(f"APP HEX out of range: 0x{addr:08X}+0x{length:X}")
def open_pcan_fd_bus(channel: str) -> can.BusABC:
# Do not pass bitrate/data_bitrate here. For PCAN FD, use explicit PCAN timing.
return can.Bus(interface=PCAN_INTERFACE, channel=channel, **PCAN_FD_TIMING)
def print_pcan_fd_config(channel: str) -> None:
print(f"PCAN: interface=pcan, channel={channel}")
print("CAN FD: enabled, BRS: enabled, DLC: 64")
print("Timing: nominal=500k, data=2M")
print(
"PCAN timing: f_clock=80MHz, "
"nom_brp=10,nom_tseg1=11,nom_tseg2=4,nom_sjw=4, "
"data_brp=2,data_tseg1=14,data_tseg2=5,data_sjw=4"
)
def uds_download(args: argparse.Namespace) -> None:
app = IHexFile(args.app)
addr = app.start_address
length = app.length
validate_app_range(addr, length)
chunks = app.chunks(args.chunk_size)
isotp_params = make_isotp_params(args.stmin, args.blocksize)
print(f"APP: {args.app}")
print(f"APP address: 0x{addr:08X}, length: {length} bytes, chunks: {len(chunks)}, chunk_size=0x{args.chunk_size:X}")
print_pcan_fd_config(args.channel)
t0 = time.perf_counter()
bus = open_pcan_fd_bus(args.channel)
try:
try:
with Client(make_conn(bus, isotp_params, REQ_FUNC, RESP_ID), config=config) as client:
try:
client.change_session(DiagnosticSessionControl.Session.extendedDiagnosticSession)
print("10 03 functional: ok")
except TimeoutException:
print("10 03 functional: timeout, continue physical")
except Exception as e:
print(f"functional wakeup skipped: {e}")
with Client(make_conn(bus, isotp_params, REQ_PHYS, RESP_ID), config=config) as client:
client.change_session(DiagnosticSessionControl.Session.extendedDiagnosticSession)
print("10 03 extended: ok")
client.routine_control(ROUTINE_PROGRAM_COND_CHECK, RoutineControl.ControlType.startRoutine)
print("31 01 0203 condition check: ok")
try:
client.control_dtc_setting(services.ControlDTCSetting.SettingType.off)
print("85 02 DTC off: ok")
except Exception as e:
print(f"85 DTC off ignored: {e}")
try:
client.communication_control(0x03, CommunicationControl.ControlType.enableRxAndDisableTx)
print("28 communication control: ok")
except Exception as e:
print(f"28 communication control ignored: {e}")
client.change_session(DiagnosticSessionControl.Session.programmingSession)
print("10 02 programming: ok")
client.unlock_security_access(0x11)
print("27 security access: ok")
today = datetime.date.today().strftime("%y%m%d").encode("ascii")
client.write_data_by_identifier(0xF184, today + b"\x00\x00\x00\x00")
print("2E F184 fingerprint: ok")
erase_info = b"\x44" + addr.to_bytes(4, "big") + length.to_bytes(4, "big")
client.routine_control(ROUTINE_ERASE_MEMORY, RoutineControl.ControlType.startRoutine, erase_info)
print("31 01 FF00 erase APP: ok")
memloc = MemoryLocation(address=addr, memorysize=length, address_format=32, memorysize_format=32)
client.request_download(memloc)
print("34 request download: ok")
crc = 0
for i, payload in enumerate(chunks):
bsc = (i + 1) & 0xFF
client.transfer_data(bsc, payload)
crc = zlib.crc32(payload, crc)
# In CAN FD mode the number of chunks is small; print every chunk for bring-up visibility.
print(f"36 transfer: {i + 1}/{len(chunks)}, crc=0x{crc:08X}")
client.request_transfer_exit()
print("37 transfer exit: ok")
client.routine_control(ROUTINE_CHECK_CRC, RoutineControl.ControlType.startRoutine, crc.to_bytes(4, "big"))
print(f"31 01 0202 CRC check: ok, crc=0x{crc:08X}")
client.routine_control(ROUTINE_CHECK_DEPENDENCIES, RoutineControl.ControlType.startRoutine)
print("31 01 FF01 dependencies / mark valid: ok")
prg_date = datetime.date.today().strftime("%y%m%d").encode("ascii")
client.write_data_by_identifier(0xF199, prg_date)
print("2E F199 programming date: ok")
client.ecu_reset(ECUReset.ResetType.hardReset)
print("11 01 reset: ok")
finally:
try:
bus.shutdown()
except Exception:
pass
elapsed = time.perf_counter() - t0
if elapsed > 0:
print(f"Firmware updated successfully. elapsed={elapsed:.3f}s, app_rate={length / elapsed:.1f} B/s")
else:
print("Firmware updated successfully.")
def main() -> None:
parser = argparse.ArgumentParser(
description="STM32U3 PCAN-USB FD UDS IAP tool, fixed at CAN FD 500k/2M BRS DLC=64"
)
parser.add_argument("-a", "--app", default="app.hex", help="Application Intel HEX linked at 0x08020000")
parser.add_argument("-c", "--channel", default=PCAN_DEFAULT_CHANNEL, help="PCAN channel, default PCAN_USBBUS1")
parser.add_argument(
"-s", "--chunk-size", default=0x800, type=lambda x: int(x, 0),
help="UDS TransferData payload bytes. Use 0x400 for first test, then 0x800 or 0xF00. Must keep SID+BSC+data <= 4095."
)
parser.add_argument("--stmin", default=0, type=lambda x: int(x, 0), help="ISO-TP STmin requested by receiver, ms 0..127")
parser.add_argument("--blocksize", default=8, type=lambda x: int(x, 0), help="ISO-TP flow-control block size")
args = parser.parse_args()
uds_download(args)
if __name__ == "__main__":
main()
4.2 UDS OTA:chunk size 0x400
测试结果:

4.3 UDS OTA:chunk size 0x800
测试结果:

0x400 与 0x800 的耗时接近,是因为当前 APP 镜像只有约 12.5 KB,整体耗时主要由 UDS 会话切换、擦除、RoutineControl 和 reset 等固定流程组成。后续当固件体积变大时,0x800 相比 0x400 的优势会更加明显。
5. 一些小问题
5.1 终端阻抗
最初 CAN FD 4M 不稳定时,我们补充了一个额外的 120Ω 终端电阻,使总线等效阻抗达到约 60Ω。调整后,CAN FD 通信持续时间明显改善。
这说明 CAN FD data phase 对终端阻抗和总线物理条件非常敏感。Classic CAN 1M 能正常运行,并不代表 CAN FD 2M / 4M 也一定稳定。
需要注意的是,终端阻抗正确只是 CAN FD 稳定通信的必要条件。我们的测试中,即使总线等效阻抗达到 60Ω,未校准 MSIS 时仍然会出现 Bus-Off。因此最终还需要解决时钟精度问题。
5.2 LSE 校准 MSIS RC0
一开始我们没有启用 LSE,也没有使用 LSE 对 MSIS RC0 进行校准。此时 2M 下通信持续时间比 4M 更长,但最终仍会停下并进入 Bus-Off。
这说明问题不只是 4M 太快,而是当前时钟源和物理层裕量在 CAN FD BRS data phase 下仍然不足。
在 CubeMX 和代码中启用 LSE,并使用 LSE 对 MSIS RC0 进行 PLL / 校准后,500K / 2M CAN FD 恢复稳定。这也是本次调试过程中最关键的发现。
因此,在无 HSE 的 STM32U3 硬件设计中,如果希望使用 CAN FD,建议至少保留 LSE,并启用 LSE 对 MSIS 的校准。
6. 总结
本次评测说明,STM32U3 的 FDCAN 外设本身可以完成 CAN FD + UDS OTA,但 CAN FD data phase 对时钟源和物理层条件非常敏感。
在没有 HSE 高速晶振的板级条件下,直接使用未校准的 MSIS 96 MHz 作为 FDCAN clock 时,Classic CAN 1 Mbps 可以稳定运行,但 CAN FD 2M / 4M BRS 长时间稳定性不足。
启用 LSE,并使用 LSE 对 MSIS RC0 进行 PLL / 校准后,CAN FD 500K / 2M BRS DLC64 恢复稳定,并成功完成 UDS OTA 升级流程。
对于当前开发板NUCLEO-U3C5ZI-Q,CAN FD 500K / 2M 是一个兼顾稳定性和传输效率的工程配置。后续如果需要使用CAN FD 5M可能需要把HSE焊上。