Chapter 15 · CAN 总线 / 汽车电子

AUTOSAR 通信栈 + Bootloader 刷写

真实量产 ECU 不会手写 bxCAN 寄存器,而是运行在 AUTOSAR 标准化软件架构之上。本章从应用层一路追到 CAN 控制器,看一个信号如何被打包成帧;再拆解 CAN Bootloader——车辆 4S 店"刷程序"背后的 UDS 0x34/0x36/0x37 流程、Flash 驱动、校验与双 Bank 回滚。

课程进度94%

关键概念

AUTOSAR 分层架构
AUTOSAR(AUTomotive Open System ARchitecture)是汽车软件标准化架构,Classic Platform 分三层:应用层(SWC 软件组件,写业务逻辑)、RTE(Runtime Environment,胶水层,屏蔽通信细节)、BSW(Basic Software 基础软件,含通信/诊断/OS/驱动)。目标是让应用与硬件解耦,同一份 SWC 可在不同 MCU 上复用。
COM 模块
通信栈最上层,处理"信号 (Signal)"层面:从 RTE 收到应用写入的信号值,按 DBC 定义打包进 I-PDU(Interaction PDU)的对应位段,并处理发送模式(周期/事件/混合)、超时监控、更新位。收到帧则反向解包成信号交给 RTE。它是 DBC 语义在运行时的落地。
PDU Router (PduR)
PDU 的"交通枢纽"。它把上层(COM、Dcm 诊断、CanTp)的 PDU 按配置路由到下层接口(CanIf),也做上下行分发与网关转发。一个从 A 总线收到需转发到 B 总线的报文,就是 PduR 在做路由——它不理解信号内容,只搬运 PDU。
CanIf / CanDrv
CanIf(CAN Interface)是硬件无关的抽象层,管理硬件对象句柄 (HOH)、L-PDU 与 CAN ID 的映射、收发缓冲与回调。CanDrv(CAN Driver)是硬件相关层,直接操作 MCU 的 CAN 控制器寄存器(如 STM32 bxCAN 邮箱、FIFO)。换 MCU 只需换 CanDrv,上层不动——这正是分层的价值。
CanTp (ISO-TP / ISO 15765-2)
CAN 传输层协议,解决"一帧最多 8 字节,但诊断/刷写要传几百上千字节"的矛盾。它把长数据拆成首帧 FF(First Frame)+ 多个连续帧 CF(Consecutive Frame),接收方回流控帧 FC(Flow Control)控制块大小 BS 与帧间隔 STmin。UDS 诊断和 Bootloader 刷写都跑在 CanTp 之上。
诊断栈 Dcm / Dem
Dcm(Diagnostic Communication Manager)实现 UDS 服务(会话控制 0x10、安全访问 0x27、读写 DID 0x22/0x2E、例程 0x31、刷写 0x34/0x36/0x37 等),管理诊断会话与 P2 超时。Dem(Diagnostic Event Manager)管理故障码 DTC:事件去抖、置位/清除、冻结帧存储、老化,对应仪表上的故障灯。
CAN Bootloader(刷写引导程序)
独立于应用程序、常驻 Flash 起始扇区的一段小程序。上电先运行 Bootloader,若无刷写请求且应用有效则跳转到应用区;否则停在 Bootloader 里通过 UDS 接收新固件并写入 Flash。它让 ECU 无需拆机、通过 OBD 口即可远程升级 (OTA/FOTA 的车端基础)。
双 Bank 与回滚
把应用 Flash 分成 Bank A / Bank B 两块。新固件写入空闲 Bank,校验通过后才切换启动指针,旧 Bank 保留。刷写中途掉电或新固件校验失败时可回滚到旧 Bank,保证 ECU 永远有一份可运行的固件——这是量产刷写的安全底线,ISO 26262 功能安全强烈推荐。

AUTOSAR 通信栈分层

一个应用写下"车速 = 120",到它变成总线上的电平,中间穿过整个通信栈。下图是信号自上而下的打包路径:

AUTOSAR CAN 通信栈:信号 → PDU → 帧 的旅程 ═══════════════════════════════════════════════════════════════ ┌────────────────────────────────────────────┐ │ 应用层 SWC Rte_Write_Speed(120) │ 写"信号" └───────────────────┬────────────────────────┘ │ RTE(屏蔽通信细节) ┌───────────────────▼────────────────────────┐ │ COM 按 DBC 把信号打包进 I-PDU 位段 │ 信号→PDU │ 处理周期/事件发送、更新位、超时 │ └───────────────────┬────────────────────────┘ │ ┌───────────────────▼────────────────────────┐ │ PduR PDU 路由 / 网关转发 │ PDU 分发 └──────┬───────────────────────────┬─────────┘ │(普通报文) │(长诊断报文) │ ┌───────▼────────┐ │ │ CanTp 分段 │ FF+CF+FC │ └───────┬────────┘ ┌──────▼───────────────────────────▼─────────┐ │ CanIf L-PDU ↔ CAN ID 映射、HOH 管理 │ 硬件无关 └───────────────────┬────────────────────────┘ ┌───────────────────▼────────────────────────┐ │ CanDrv 操作 bxCAN 邮箱/FIFO 寄存器 │ 硬件相关 └───────────────────┬────────────────────────┘ │ ┌───────────────────▼────────────────────────┐ │ CAN 控制器 + 收发器 → 差分电平上总线 │ └─────────────────────────────────────────────┘
诊断报文走"旁路"

注意上图两条路径:普通信号报文走 COM → PduR → CanIf;而 UDS 诊断/刷写这类长报文,在 PduR 处分流进 CanTp 做分段(一帧 8 字节装不下),再汇入 CanIf。理解这个分叉,就能读懂为什么诊断请求要用 ISO-TP 而不是裸 CAN 帧。

BSW 配置:ARXML

AUTOSAR 不写代码而是"配置"。所有模块的参数(报文列表、信号布局、CanTp 通道、Dcm 服务表)都存在 ARXML(AUTOSAR XML)文件里,由 DaVinci Configurator、EB tresos 等工具生成 BSW 代码。DBC 通常先转成 ARXML 的 ECU 提取 (System Extract) 再导入。开发者面对的是配置界面,而非手写寄存器。

模块层级职责关键概念
COM服务层信号打包/解包Signal / I-PDU / 发送模式
PduR服务层PDU 路由/网关路由表 / 转发
CanTp服务层长报文分段FF / CF / FC / STmin
Dcm / Dem服务层诊断/故障码UDS 服务 / DTC
CanIfECU 抽象硬件无关接口HOH / L-PDU 映射
CanDrvMCAL操作 CAN 外设邮箱 / FIFO / 中断

CAN Bootloader 原理

Bootloader 与应用共享 Flash,但各占独立区域。上电后 Reset 向量指向 Bootloader,由它决定去留:

Flash 布局与启动决策 ═══════════════════════════════════════════════════════════════ Flash 布局 上电启动流程 ───────────────── ───────────────── 0x0800_0000 ┐ ┌──────────────┐ Bootloader │ 常驻不擦除 │ 复位 Reset │ (32KB) │ └──────┬───────┘ 0x0800_8000 ┘ │ 0x0800_8000 ┐ ┌──────▼───────┐ App 区 A │ 可刷写 │ 有刷写请求? │ (480KB) │ │(标志位/超时) │ 0x0808_0000 ┘ └──┬────────┬──┘ 0x0808_0000 ┐ 是 │ │ 否 App 区 B │ 双Bank备份 ▼ ▼ (480KB) │ ┌──────────┐ ┌──────────────┐ 0x0810_0000 ┘ │停在 BL │ │ App 有效? │ │等 UDS 刷 │ │(CRC/签名) │ 0x080F_C000 ┐ └──────────┘ └──┬───────┬───┘ App 有效标志 │ 有效│ │无效 + CRC/签名 │ ▼ ▼ 0x0810_0000 ┘ ┌──────────┐ ┌────────┐ │跳转 App │ │停 BL 等 │ └──────────┘ └────────┘

Bootloader 跳转到应用

跳转的本质:关外设与中断、重定位向量表、加载应用的初始栈指针 (MSP),再跳到应用的 Reset_Handler。

/* bootloader.c — 从 Bootloader 跳转到应用程序 */
#include "stm32f4xx.h"

#define APP_BASE_ADDR   0x08008000UL   /* 应用区起始地址 */
#define APP_MAGIC_ADDR  0x080FC000UL   /* 有效标志所在地址 */
#define APP_VALID_MAGIC 0xA5A5A5A5UL

typedef void (*app_entry_t)(void);

/* 校验应用是否有效:栈顶合法 + 标志位正确 */
static int app_is_valid(void) {
    uint32_t sp = *(volatile uint32_t *)APP_BASE_ADDR;
    uint32_t magic = *(volatile uint32_t *)APP_MAGIC_ADDR;
    /* 栈指针应落在 SRAM 区间 (0x2000_0000 ~ 0x2002_0000) */
    if ((sp & 0x2FFE0000) != 0x20000000) return 0;
    return (magic == APP_VALID_MAGIC);
}

void jump_to_application(void) {
    uint32_t app_sp    = *(volatile uint32_t *)(APP_BASE_ADDR);
    uint32_t app_reset = *(volatile uint32_t *)(APP_BASE_ADDR + 4);
    app_entry_t app_entry = (app_entry_t)app_reset;

    /* ① 关闭所有中断、复位外设时钟到默认态 */
    __disable_irq();
    HAL_RCC_DeInit();
    HAL_DeInit();
    SysTick->CTRL = 0;  SysTick->LOAD = 0;  SysTick->VAL = 0;

    /* ② 重定位中断向量表到应用区 */
    SCB->VTOR = APP_BASE_ADDR;

    /* ③ 设置主栈指针为应用的初始 SP,再跳转 */
    __set_MSP(app_sp);
    __enable_irq();
    app_entry();   /* 跳入应用 Reset_Handler,不再返回 */
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    CAN_Bootloader_Init();   /* 初始化 bxCAN + UDS 会话 */

    /* 无刷写请求且应用有效 → 直接进应用 */
    if (!reprogram_requested() && app_is_valid()) {
        jump_to_application();
    }
    /* 否则停在 Bootloader,等待 UDS 刷写 */
    while (1) {
        CanTp_MainFunction();   /* 处理分段 */
        Dcm_ProcessRequest();   /* 处理 UDS 服务 */
    }
}

UDS 刷写时序

刷写不是一条命令,而是一套严格的 UDS 服务序列。诊断仪(Tester)与 Bootloader 之间的完整握手如下:

UDS 刷写流程时序(Tester ⇄ ECU Bootloader) ═══════════════════════════════════════════════════════════════ Tester(诊断仪) ECU(Bootloader) │ │ │ 0x10 03 进入扩展会话 ──────▶ │ │ ◀──────────────── 50 03 (肯定响应) │ │ │ │ 0x85 关闭 DTC 记录 ──────▶ │ │ 0x28 关闭正常通信(禁 COM) ──────▶ │ │ │ │ 0x27 01 请求 Seed ──────▶ │ │ ◀────────── 67 01 <seed> 返回种子 │ │ 0x27 02 发送 Key(算法算出) ──────▶ │ 安全解锁 │ ◀──────────────── 67 02 (解锁成功) │ │ │ │ 0x31 01 FF00 例程:擦除 Flash ──────▶ │ 擦除应用区 │ ◀──────────────── 71 01 FF00 │ │ │ │ 0x34 RequestDownload ──────▶ │ 协商地址+长度 │ ◀────── 74 <maxBlockLen> 返回块大小 │ │ 0x36 01 TransferData 块#1 ──────▶ │ ┐ │ 0x36 02 TransferData 块#2 ──────▶ │ │ 循环写 Flash │ ......(CanTp 分段传输)...... │ │ │ 0x36 NN TransferData 块#N ──────▶ │ ┘ │ 0x37 RequestTransferExit ──────▶ │ 写完收尾 │ ◀──────────────── 77 (传输结束) │ │ │ │ 0x31 01 FF01 例程:校验CRC/签名 ──────▶ │ 完整性校验 │ ◀──────────────── 71 01 FF01 通过 │ │ 0x11 01 ECU 复位 ──────▶ │ 重启进新App

Flash 写入与校验(TransferData 处理)

/* uds_flash.c — 处理 0x36 TransferData:把数据块写入 Flash */
#include "stm32f4xx_hal.h"

static uint32_t flash_write_addr;   /* 由 0x34 协商得到的起始地址 */
static uint8_t  expected_bsc;       /* 期望的块序号 blockSequenceCounter */

/* UDS 0x36 回调:data 已由 CanTp 重组为完整块 */
uint8_t Uds_TransferData(uint8_t bsc, const uint8_t *data, uint16_t len) {
    /* ① 校验块序号连续,防丢包/重发 */
    if (bsc != expected_bsc) return 0x73;  /* wrongBlockSequenceCounter */

    /* ② 解锁 Flash 并按字 (32bit) 写入 */
    HAL_FLASH_Unlock();
    for (uint16_t i = 0; i < len; i += 4) {
        uint32_t word = *(const uint32_t *)(data + i);
        if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
                              flash_write_addr, word) != HAL_OK) {
            HAL_FLASH_Lock();
            return 0x72;  /* generalProgrammingFailure */
        }
        flash_write_addr += 4;
    }
    HAL_FLASH_Lock();

    /* ③ 回读校验:Flash 内容应与源数据一致 */
    if (memcmp((void *)(flash_write_addr - len), data, len) != 0)
        return 0x72;

    expected_bsc++;         /* 期望下一个块序号 */
    return 0x00;             /* 肯定响应 */
}
刷写安全:掉电保护与回滚

刷写过程若在擦除后、写完前掉电,应用区会残缺,ECU 变砖。量产方案的三道防线:① 双 Bank——新固件写空闲 Bank,校验通过前不动旧 Bank,掉电后旧 Bank 仍可启动;② 完整性校验——传完用 0x31 例程算 CRC32 或验数字签名,只有通过才把"有效标志"写入,Bootloader 靠它判断能否跳转;③ 有效标志最后写——先擦标志、再写固件、最后写标志,任何中断都不会误跳一个半成品固件。

安全访问 (0x27) 不是摆设

刷写前必须过 0x27 SecurityAccess:ECU 发 Seed,Tester 用只有授权方掌握的算法(AES/自定义)算出 Key 回传,匹配才解锁。这防止未授权刷写恶意固件——ISO 21434 信息安全要求的一环。真正量产还会叠加固件签名 (RSA/ECDSA),Bootloader 用公钥验签,仅接受厂商签名的固件。

本章小结

量产 ECU 跑在 AUTOSAR 上:应用层 SWC / RTE / BSW 三层解耦;通信栈自上而下为 COM(信号↔PDU 打包)→ PduR(PDU 路由与网关)→ CanIf(硬件无关接口,HOH/ID 映射)→ CanDrv(操作 bxCAN 寄存器),换 MCU 只改 CanDrv。长诊断报文在 PduR 处分流进 CanTp(ISO-TP:FF/CF/FC/STmin 分段),诊断由 Dcm(UDS 服务)+ Dem(DTC 故障码)处理;一切参数用 ARXML 配置生成而非手写。CAN Bootloader 常驻 Flash 首扇区,上电判断刷写请求与应用有效性(栈指针+CRC/签名+标志位),无请求且有效则关外设、重定位 VTOR、设 MSP 后跳转应用。UDS 刷写序列:0x10 会话 → 0x85/0x28 关 DTC 与通信 → 0x27 安全解锁 → 0x31 擦除 → 0x34 RequestDownload 协商 → 0x36 TransferData 循环写块(校验块序号+回读)→ 0x37 结束 → 0x31 校验 CRC/签名 → 0x11 复位。刷写安全靠双 Bank 回滚、完整性校验、有效标志最后写三道防线,加上 0x27 安全访问与固件签名。

⚒️ 配套开发者工具箱 · 17 款免费在线工具