你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【STM32U3 评测】基于 CAN 的 UDS OTA 不完全实现指南

[复制链接]
stmCortex 发布时间:2026-6-27 18:26

【STM32U3 评测】基于 CAN 的 UDS OTA 实现不完全指南

1. 背景

在上篇文章中介绍并评测了STM32U3 CANFD外设,这篇文章聊一聊CAN UDS OTA。 在现代汽车电子和高可靠性嵌入式系统中,UDS(Unified Diagnostic Services)协议因其标准化、自带清晰状态机及错误响应机制,成为了在线升级(OTA/IAP)的绝对主流方案。

本次评测基于STM32U3平台,构建一个基于 UDS over ISO-TP 协议的 CAN IAP Bootloader,并通过 PC 端 Python 脚本完成 APP 固件的下载、Flash 擦写、CRC 校验和复位启动。

受限于当前 PCAN 工具链,本轮测试采用Classic CAN 2.0 (1 Mbps)进行基础逻辑验证,后续可无缝迁移至 CAN FD 以获取极速下载体验。

2. 测试平台与开发环境

项目 规格说明 项目 规格说明
MCU STM32U3 PC 工具 PCAN-USB / PCAN-View
CAN 外设 FDCAN Python Libray python-can + udsoncan
当前通信模式 Classic CAN 2.0 @ 1 Mbps 协议栈 UDS over ISO-TP
固件结构 Bootloader + APP APP 固件大小 12,280 bytes
CAN ID (物理) Req:0x736 / Rsp: 0x7B6 CAN ID (功能) Req:0x7DF

3. UDS OTA实现

为了实现安全可靠的 IAP 升级,首先需要对 STM32U3 的片内 Flash 进行的物理与逻辑隔离。

3.1 Flash 分区映射

STM32U3 内部 Flash 划分为 Bootloader、APP 和 Metadata 三个区域,两者使用独立的 Linker Script: bootloader.ldapp.ld

0x0800_0000 ┌──────────────────────────────┐
            │ Bootloader (128 KB)          │
0x0802_0000 ├──────────────────────────────┤
            │ Application                  │
0x081F_0000 ├──────────────────────────────┤
            │ IAP Metadata / Valid Flag    │
0x0820_0000 └──────────────────────────────┘

注:Bootloader 跳转前会重定向向量表 SCB->VTOR = 0x08020000UL;,并重置 MSP。

booloader.ld

/* CANFD_IAP bootloader memory map. Bootloader @ 0x08000000, 128 KB. */
ENTRY(Reset_Handler)

_Min_Heap_Size  = 0x400;
_Min_Stack_Size = 0x4000;  /* 16 KB stack for UDS/ISO-TP/Flash path */

MEMORY
{
  RAM            (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
  RAM2           (xrw) : ORIGIN = 0x20030000, LENGTH = 64K
  RAM3           (xrw) : ORIGIN = 0x20040000, LENGTH = 320K
  RAM4           (xrw) : ORIGIN = 0x20090000, LENGTH = 64K
  HSP_DATA_BRAM  (rw)  : ORIGIN = 0x200A0000, LENGTH = 16K
  FLASH          (rx)  : ORIGIN = 0x08000000, LENGTH = 128K
}

INCLUDE common.ld

app.ld

/* CANFD_IAP application memory map. APP @ 0x08020000, metadata reserved at 0x081F0000. */
ENTRY(Reset_Handler)

_Min_Heap_Size  = 0x200;
_Min_Stack_Size = 0x1000;

MEMORY
{
  RAM            (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
  RAM2           (xrw) : ORIGIN = 0x20030000, LENGTH = 64K
  RAM3           (xrw) : ORIGIN = 0x20040000, LENGTH = 320K
  RAM4           (xrw) : ORIGIN = 0x20090000, LENGTH = 64K
  HSP_DATA_BRAM  (rw)  : ORIGIN = 0x200A0000, LENGTH = 16K
  FLASH          (rx)  : ORIGIN = 0x08020000, LENGTH = 1856K
}

INCLUDE common.ld

3.2 Metadata 数据结构

Metadata 位于 0x081F0000,记录 APP 的基本信息与合法性标志,用于 Bootloader 启动时的合法性判定。

typedef struct {
    uint32_t magic;         // 0x55445349 ("ISDU")
    uint32_t app_start;     // 0x08020000
    uint32_t app_size;      // 固件真实大小
    uint32_t app_crc32;     // 固件 CRC32 校验值
    uint32_t image_counter; // 升级次数
    uint32_t flags;         // APP 有效标志
    uint32_t reserved[10];
} iap_metadata_t;

3.3 bootloader跳转

在 ARM Cortex-M 架构中,固件的中断向量表都存放着两个主要的 Word:第 0 个字是主栈指针 MSP ,第 1 个字是复位入口地址 PC, Reset_Handler,bootloader的工作主要是:

读出这两个值 -> 转移中断向量表 -> 重新设置栈指针 -> 强制跳转到 PC 地址

这样就可以实现从bootloader跳转到App,此外标准的 HAL_DeInit() 不会重置ICACHE,bootloader需要主动关闭 ICACHE。bootloader跳转的核心实现如下:

void Boot_JumpToApp(void)
{
    if (!Boot_IsAppVectorValid()) return;

    uint32_t app_sp = *(volatile uint32_t *)IAP_APP_BASE_ADDR;
    uint32_t app_pc = *(volatile uint32_t *)(IAP_APP_BASE_ADDR + 4U);

    __disable_irq();

    HAL_FDCAN_DeInit(&hfdcan1);

#if defined(ICACHE)
    CLEAR_BIT(ICACHE->CR, ICACHE_CR_EN);
    __DSB();
    __ISB();
    WRITE_REG(ICACHE->FCR, ICACHE_FCR_CBSYENDF | ICACHE_FCR_CERRF);
#endif

    HAL_DeInit();

    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL  = 0;

    for (uint32_t i = 0; i < 8U; ++i) {
        NVIC->ICER[i] = 0xFFFFFFFFUL;
        NVIC->ICPR[i] = 0xFFFFFFFFUL;
    }

    SCB->VTOR = IAP_APP_BASE_ADDR;
    __DSB();
    __ISB();
    __set_MSP(app_sp);

    __enable_irq();

    ((app_entry_t)app_pc)();

    while (1) {}
}

4. UDS OTA 协议交互

固件升级不仅是把数据写入 Flash,而是一个包含会话控制、安全解锁、环境校验、数据流控的状态机。本 Demo 实现了以下标准 UDS 服务组合:

OTA 状态机流程:

  1. 10 03 进入扩展会话 (Extended Session)
  2. 31 0203 升级条件检查 (Routine: Program Condition Check)
  3. 85 02 & 28 00 关闭 DTC 与控制通信
  4. 10 02 进入编程会话 (Programming Session)
  5. 27 11 & 27 12 安全解锁 (Security Access - 请求 Seed 与发送 Key)
  6. 2E F184 写应用软件指纹 (WriteDataByIdentifier)
  7. 31 FF00 擦除 APP 分区 (Routine: Erase)
  8. 34 / 36 / 37 下载请求 -> 分块数据传输 -> 退出传输
  9. 31 0202 CRC32 校验 (Routine: CRC Check)
  10. 31 FF01 写入 Metadata 并标记 APP 有效 (Routine: Mark Valid)
  11. 11 01 硬件复位 (ECU Reset),随后 Bootloader 自动跳转至 APP。

5. OTA 实测数据与分析

我们使用 Python 脚本执行了全自动化的 OTA 流程测试。由于 ISO-TP 的存在,大块数据传输能够有效提升下载效率,测试分为小 Chunk (0x3C) 和大 Chunk (0x400) 两种模式。

from __future__ import annotations
import argparse
import datetime
import os
import sys
import zlib

import can
import isotp
import udsoncan
from udsoncan import Routine, services, DidCodec
from udsoncan.client import Client
from udsoncan.connections import PythonIsoTpConnection
from udsoncan.exceptions import TimeoutException
from udsoncan.services import DiagnosticSessionControl, RoutineControl, CommunicationControl, ECUReset
from udsoncan import MemoryLocation

from ihex import IHexFile

class RawDidCodec(DidCodec):
    def encode(self, did_value):
        return did_value
    def decode(self, did_payload):
        return did_payload
    def __len__(self):
        return 1

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

config = dict(udsoncan.configs.default_client_config)
config['use_server_timing'] = False
config['request_timeout'] = 30
config['p2_timeout'] = 1.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

isotp_params = {
    'stmin': 0,
    'blocksize': 8,
    'wftmax': 0,
    'tx_data_length': 8,
    'tx_padding': 0x00,
    'rx_flowcontrol_timeout': 1000,
    'rx_consecutive_frame_timeout': 1000,
    'max_frame_size': 4095,
}

def make_conn(bus, txid=REQ_PHYS, rxid=RESP_ID):
    addr = isotp.Address(isotp.AddressingMode.Normal_11bits, txid=txid, rxid=rxid)
    stack = isotp.CanStack(bus=bus, address=addr, params=isotp_params)
    return PythonIsoTpConnection(stack)

def validate_app_range(addr: int, length: int):
    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(channel: str, bitrate: int):
    return can.interface.Bus(interface='pcan', channel=channel, bitrate=bitrate)

def uds_download(app_path: str, bitrate: int, channel: str, chunk_size: int):
    app = IHexFile(app_path)
    addr = app.start_address
    length = app.length
    validate_app_range(addr, length)

    print(f'APP: {app_path}')
    print(f'APP address: 0x{addr:08X}, length: {length} bytes, chunks: {len(app.chunks(chunk_size))}')
    print(f'PCAN: channel={channel}, bitrate={bitrate}')

    bus = open_pcan(channel, bitrate)

    # Optional functional extended-session wake-up.
    try:
        with Client(make_conn(bus, 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, 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
        chunks = app.chunks(chunk_size)
        for i, payload in enumerate(chunks):
            bsc = (i + 1) & 0xFF
            client.transfer_data(bsc, payload)
            crc = zlib.crc32(payload, crc)
            if (i + 1) % 16 == 0 or i + 1 == len(chunks):
                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')

    bus.shutdown()
    print('Firmware updated successfully.')

def main():
    parser = argparse.ArgumentParser(description='STM32 CAN 2.0 UDS IAP PCAN tool v0.1')
    parser.add_argument('-a', '--app', default='app.hex', help='Application Intel HEX linked at 0x08020000')
    parser.add_argument('-b', '--bitrate', default=1000000, type=int, help='Classic CAN bitrate')
    parser.add_argument('-c', '--channel', default='PCAN_USBBUS1', help='PCAN channel')
    parser.add_argument('-s', '--chunk-size', default=0x400, type=lambda x: int(x, 0), help='UDS TransferData payload bytes')
    args = parser.parse_args()
    uds_download(args.app, args.bitrate, args.channel, args.chunk_size)

if __name__ == '__main__':
    main()

5.1 基础链路连通性测试

通过 PCAN-View 手动发送 TesterPresent (0x3E)DiagnosticSessionControl (0x10)

  • Req: 02 3E 00 00 00 00 00 00 -> Rsp: 02 7E 00 00 00 00 00 00
  • Req: 02 10 03 00 00 00 00 00 -> Rsp: 06 50 03 00 32 13 88 00 (P2=50ms, P2*=5000ms) 结论:CAN 物理层、1 Mbps 速率以及 ISO-TP 单帧/多帧解包完全正常。

test_can_ping.png

test_can_connection.png

5.2 OTA 下载测试

执行命令:python tools\uds_can_iap.py -a build\All-Release\CANFD_IAP_App.hex -b 1000000 -c PCAN_USBBUS1 -s 0x400

uds_ota_400.png

ota_app.png

12,304 Bytes 的 APP 仅需 13 个 0x400 的 Chunk 即可完成传输。最终生成的 CRC32 (0x1341B335) PC 端与 Bootloader 端完全一致,APP 被正确引导。

Video

6. 小结

本次评测成功在STM32U3 上跑通了UDS over ISO-TP CAN OTA 架构。该架构实现了通信层、诊断层、业务层及存储层的解耦,并实现了OTA主要功能,在实际使用时还需要加入A/B 双分区冗余,断点续传,异常掉电保护等功能,进一步提升OTA稳定性。

最后,力有不逮,还请指出,多多担待。

收藏 评论0 发布时间:2026-6-27 18:26

举报

0个回答

所属标签

相似分享

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版