杰理 SDK 文档中心
首页
首页
  • fw-Bootloader:JL 系列定制 Bootloader

    • Bootloader 架构与芯片适配
    • uboot 升级协议与流程
    • 上位机升级工具
    • 编译环境与快速开始
  • ac792n-ota-loader:AC792N 系列 OTA Loader

    • 工程结构与公共运行时框架
    • SD 卡与 USB 基础升级通道
    • 安全升级通道(SD/USB)
    • 用户自定义升级通道(UART/USB HID)
    • LVGL 图形化升级界面与模拟器
  • ac791n-ota-loader:AC791N 系列 OTA Loader

    • uboot 应用框架与 WiFi 示例
    • 升级通道变体(AP/STA/USB HID)
    • 网络与系统库依赖

用户自定义升级通道(UART/USB HID)

本文档介绍杰理(Jieli)AC792N OTA Loader 体系中两条用户可自定义的固件升级通道:UART 用户升级通道(uart_user_update 工程)与 USB HID 升级通道(usb_hid_ota_update 工程),涵盖入口 main() 流程、底层驱动(UART DMA 驱动与 HID 端点驱动)、与 upgrade.h 升级库的衔接方式,以及配置、失败模式与扩展点。

Purpose and Scope

本页聚焦于"用户自定义升级通道"这一能力边界,即:

  • UART 通道:ac792n-ota-loader/uart_user_update/ 工程,其 CPU 侧驱动位于 cpu/wl83/uart_update_driver.c,负责通过串口 DMA 收发自定义升级帧。
  • USB HID 通道:ac792n-ota-loader/usb_hid_ota_update/ 工程,配合 fw-Bootloader/user_boot/app/src/hid.c 中的 HID 设备描述符与中断端点收发实现。

不包含(属于兄弟页面):

  • SD 卡升级(sd_ota_update / sd_sec_ota_update)、标准 USB MSC 升级(usb_ota_update / usb_sec_ota_update)以及带 LCD 界面的 ota_loader_update_800x480,它们共享同一 upgrade.h 升级库但走不同的通道介质。
  • 升级库内部(jlup 闭源库)的帧格式与加密算法细节——源码中仅暴露 include_lib/driver/jlup/upgrade.h 头文件接口。

若需了解其他通道的整体框架,可参考本目录下各 OTA Loader 工程的兄弟页面。

Overview

在 AC792N 的 OTA 方案中,固件升级被拆分为"通道"与"升级核心"两层:

  • 通道层:决定升级数据从哪条物理链路进入芯片。标准方案提供 SD 卡、USB MSC、USB HID 等;而用户自定义通道允许客户沿用自有协议与上位机工具,通过 UART 或 USB HID 将固件包灌入 Loader。
  • 升级核心层:upgrade.h(jlup 库)与 update_main.h 提供帧解析、校验、擦写 NorFlash 的能力,与通道无关。两个通道工程都 #include "upgrade.h" 与 #include "update_main.h",说明通道驱动只负责"搬数据",协议与落盘由升级核心处理。

两条通道的共同设计意图:把"如何把字节送进芯片"与"如何升级固件"解耦。这样客户可以在不改动升级核心的前提下,仅替换/新增通道驱动即可接入自己的产测工具或 App。

典型使用场景:

  • 产线通过串口工具批量烧录固件(UART 通道,波特率与引脚可配置);
  • 消费类产品通过 USB 连接 PC,用厂商自定义 HID Report(Vendor-Defined Usage Page 0xFF00)完成升级,无需安装驱动(免驱 HID);
  • 二次开发时复用 uart_update_driver.c / hid.c 的收发接口,自行定制帧协议。

Architecture

两条通道共享同一升级核心,其整体架构如下:

flowchart TD
    subgraph sg_Channels["升级通道工程(AC792N Loader)"]
        UART_APP["uart_user_update<br/>app/src/loader_main.c"]
        HID_APP["usb_hid_ota_update<br/>app/src/loader_main.c"]
    end

    subgraph sg_Drv["通道驱动层"]
        UART_DRV["uart_update_driver.c<br/>cpu/wl83(UART DMA)"]
        HID_DRV["hid.c<br/>user_boot(USB 中断端点)"]
    end

    subgraph sg_Core["升级核心(与通道无关)"]
        UPGRADE["upgrade.h / jlup 库"]
        UPDATE_MAIN["update_main.h"]
        NORFLASH["norflash 擦写驱动"]
    end

    PC_UART["PC 上位机(串口)"] -->|"自定义帧协议"| UART_APP
    PC_HID["PC 上位机(USB HID)"] -->|"HID Report"| HID_APP

    UART_APP --> UART_DRV
    HID_APP --> HID_DRV
    UART_DRV --> UPGRADE
    HID_DRV --> UPGRADE
    UPGRADE --> UPDATE_MAIN
    UPDATE_MAIN --> NORFLASH

各组件职责:

组件职责证据来源
loader_main.c(两个工程各一份)Loader 入口:初始化内存、IO、时钟,随后进入升级主流程uart_user_update/loader_main.c、usb_hid_ota_update/loader_main.c
uart_update_driver.cUART 通道底层:选择升级串口、DMA 收发、单 IO 半双工切换uart_update_driver.c
hid.cHID 通道底层:Vendor-Defined HID 描述符、中断 IN/OUT 端点收发hid.c
upgrade.hjlup 升级库头文件,声明升级接口(两通道均包含)uart_user_update/upgrade.h
update_main.h / norflash.h升级主流程与 Flash 擦写uart_user_update/loader_main.c

架构上的关键设计点:

  1. 弱符号钩子 loader_before_main:loader_main.c 中声明了 __attribute__((weak)) 的 loader_before_main(void *arg),默认返回 0,允许用户在进入升级主流程前插入自定义初始化(如检测按键进入升级模式)。
  2. 驱动通过回调与主流程解耦:uart_update_driver.c 维护 uart_rx_handler / uart_tx_handler 两个静态函数指针,升级主流程注册回调后即可在中断/DMA 收齐数据时被通知,驱动本身不感知帧内容。
  3. Flash 校验可裁剪:FLASH_ID_VALID_CHECK_EN 置 0 可跳过 Flash ID 校验,加快产测流程(见 uart_update_driver.c L27)。

UART 通道实现详解

入口:loader_main 的 main()

uart_user_update 工程的入口与其他 Loader 一致,main() 在启动早期完成三件事:调用弱符号钩子 loader_before_main、IO 初始化、以及在使用 malloc 前初始化堆内存。源码中的初始化顺序是刻意安排的——先有堆、后有业务,避免升级流程中的动态内存分配踩空指针:

int main(int arg)
{
    u8 val[16];
    u8 pll_scr = 0;

    loader_before_main((void *)arg);

#if !defined(CONFIG_CPU_WL83)
    port_init(); //io脚初始化
#endif

#if defined(CONFIG_CPU_BD49) || defined(CONFIG_CPU_BD47) || defined(CONFIG_CPU_BD45)
    mask_api_init(putchar, exception_analyze);
#else
    mask_api_init(putchar, NULL);
#endif

#ifdef SUPPORT_LCD
    clk_early_init();
    video_clock_early_init(360000000);
#endif
    // 使用malloc前需要先初始化malloc的memory空间
    extern u32 HEAP_BEGIN[];
    extern u32 MALLOC_SIZE[];
    extern void dynamic_mem_init(void *malloc_pool, int malloc_size);
    printf("HEAP_BEGIN = 0x%x\n", HEAP_BEGIN);
    printf("MALLOC_SIZE = 0x%x\n", MALLOC_SIZE);
    dynamic_mem_init((void *)HEAP_BEGIN, (int)MALLOC_SIZE);

#if defined(CONFIG_CPU_WL83)

Source: loader_main.c

要点解读:

  • loader_before_main 是弱符号,客户工程可重新定义它,在正式升级前做按键检测、显示初始化等,返回非 0 可提前终止流程。
  • mask_api_init(putchar, exception_analyze) 把串口打印与异常分析钩子注册给 mask API——升级过程中的 log 走同一个 UART,方便产测抓 log 定位问题。
  • 文件头部通过 #pragma bss_seg / data_seg / const_seg / code_seg 把 Loader 代码段固定到专用内存区间(见 loader_main.c L1-L6),这是 Bootloader 场景下防止代码被升级数据覆盖的经典做法。

底层驱动:uart_update_driver.c(WL83 CPU)

驱动位于 cpu/wl83/,说明该实现与 WL83 内核的寄存器布局绑定。它用编译期宏选择升级串口,避免运行时分支开销:

#define UART0_SEL  0
#define UART1_SEL  1
#define UART2_SEL  2
#define UPDATE_UART_SEL  UART1_SEL  //选择哪组uart作为升级串口

#if (UPDATE_UART_SEL == UART0_SEL)
//配置对应的寄存器和crossbar
#define UPDATE_UART  JL_UART0
#define UART_CROSSBAR_RX  PFI_UART0_RX
#define UART_CROSSBAR_TX  FO_UART0_TX
#endif

#if (UPDATE_UART_SEL == UART1_SEL)
#define UPDATE_UART  JL_UART1
#define UART_CROSSBAR_RX  PFI_UART1_RX
#define UART_CROSSBAR_TX  FO_UART1_TX
#endif

Source: uart_update_driver.c

设计意图:把"哪组 UART"抽象成 UPDATE_UART_SEL 一个开关,配套的寄存器基址(JL_UARTx)与 crossbar 信号(PFI_UARTx_RX / FO_UARTx_TX)由预处理器自动展开。客户改串口只需改这一处宏,不用动收发逻辑。

驱动内部维护了 DMA 缓冲与收发回调:

#define UART_DMA_LEN        544

static u8 uart_dma_buf[UART_DMA_LEN] __attribute__((aligned(4)));
static u32 uart_ot_cnt  = 0;
static volatile u32 uart_gpio = -1;
static volatile u32 uart_tx_gpio = -1;
static volatile u32 uart_rx_gpio = -1;
static u32 uart_one_io_mode = 0;
static void(*uart_rx_handler)(u8 *buf, u16 len) = NULL;
static void(*uart_tx_handler)(void) = NULL;

Source: uart_update_driver.c

  • UART_DMA_LEN = 544 对应 jlup 升级帧的一个典型传输单元(对齐 4 字节,可直接作为 DMA 目标地址);
  • uart_rx_handler / uart_tx_handler 是驱动对外暴露的回调注册点,升级主流程把收/发完成通知挂进来,驱动与协议解耦;
  • uart_one_io_mode 支持单 IO 半双工模式(一根线分时收/发),对应低引脚数方案。

uart_open(u8 mode) 是单 IO 模式下的方向切换核心:mode == 1 时把 GPIO 配成输入并挂到 RX crossbar,配置 DMA 收数据;否则配成输出挂 TX crossbar:

void uart_open(u8 mode)
{
    if (uart_one_io_mode) {
        UPDATE_UART->CON0 = BIT(13) | BIT(12) | BIT(10);
        if (mode == 1) {//输入
            gpio_set_direction(uart_gpio, 1);;
            gpio_set_die(uart_gpio, 1);
            gpio_set_fun_input_port(uart_gpio, UART_CROSSBAR_RX);

            UPDATE_UART->RXSADR = (u32)uart_dma_buf;
            UPDATE_UART->RXEADR = (u32)(uart_dma_buf + UART_DMA_LEN);
            UPDATE_UART->RXCNT = UART_DMA_LEN;
            UPDATE_UART->OTCNT = uart_ot_cnt;
            UPDATE_UART->CON0 |= BIT(6) | BIT(5) | BIT(3) | BIT(1);
        } else {//输出
            gpio_direction_output(uart_gpio, 1);
            gpio_set_hd(uart_gpio, 1);
            gpio_set_fun_output_port(uart_gpio, UART_CROSSBAR_TX, 1, 1);

            UPDATE_UART->CON0 |= BIT(2);
        }
    }
}

Source: uart_update_driver.c

发送路径则极简——直接把数据地址和长度写进寄存器,由硬件 DMA 完成搬运:

void uart_data_send(u8 *data, u32 len)
{
    UPDATE_UART->TXADR = (u32)data;
    UPDATE_UART->TXCNT = len;
}

Source: uart_update_driver.c

配合 SET_INTERRUPT 修饰的 uart_isr()(见 uart_update_driver.c L119),中断服务程序负责在 DMA 收满/发送完成时触发已注册的回调,从而把升级帧逐段送入 upgrade.h 的解析流程。uart_hw_close()(L64-L79)用于升级完成后关闭 UART 引脚,避免残留上下拉影响后续业务。

USB HID 通道实现详解

入口与公共框架

usb_hid_ota_update/app/src/loader_main.c 的 main() 与 UART 工程结构完全一致(弱符号钩子 → mask_api_init → dynamic_mem_init → WL83 后续初始化),证明两条通道共享同一套 Loader 骨架,差异只在下层驱动。详见 usb_hid_ota_update/loader_main.c L91-L120。

HID 设备描述:Vendor-Defined 免驱通道

fw-Bootloader/user_boot/app/src/hid.c 定义了 厂商自定义 HID 设备:接口类为 USB_CLASS_HID、子类为 0(非 Boot 接口),通过标准 HID 描述符与 Report 描述符向主机声明一对中断端点(IN + OUT),上层 PC 工具无需安装驱动即可访问:

static const u8 sHIDDescriptor[] = {
    //InterfaceDeszcriptor:
    USB_DT_INTERFACE_SIZE,     // Length
    USB_DT_INTERFACE,          // DescriptorType
    0x00,                      // bInterface number
    0x00,                      // AlternateSetting
    0x02,                      // NumEndpoint
    USB_CLASS_HID,             // Class = Human Interface Device
    0x00,                      // Subclass, 0 No subclass, 1 Boot Interface subclass
    0x00,                      // Procotol, 0 None, 1 Keyboard, 2 Mouse
    0x00,                      // Interface Name
    //HIDDescriptor:
    0x09,                      // bLength
    USB_HID_DT_HID,            // bDescriptorType, HID Descriptor
    0x00, 0x01,                // bcdHID, HID Class Specification release NO.
    0x00,                      // bCuntryCode, Country localization (=none)
    0x01,                      // bNumDescriptors, Number of descriptors to follow
    0x22,                      // bDescriptorType, Report Desc. 0x22
    0,                         // LOW(ReportLength)
    0,                         // HIGH(ReportLength)

    USB_DT_ENDPOINT_SIZE,      // bLength
    USB_DT_ENDPOINT,           // bDescriptorType, Type
    USB_DIR_IN | HID_EP_IN,    // bEndpointAddress
    USB_ENDPOINT_XFER_INT,     // Interrupt
    LOBYTE(MAXP_SIZE_HIDIN), HIBYTE(MAXP_SIZE_HIDIN),// Maximum packet size
    1,                         // Poll every 10msec seconds

    USB_DT_ENDPOINT_SIZE,      // bLength
    USB_DT_ENDPOINT,           // bDescriptorType, Type
    USB_DIR_OUT | HID_EP_OUT,  // bEndpointAddress
    USB_ENDPOINT_XFER_INT,     // Interrupt
    LOBYTE(MAXP_SIZE_HIDOUT), HIBYTE(MAXP_SIZE_HIDOUT),// Maximum packet size
    0x01,                      // bInterval
};

Source: hid.c

配套的 Report 描述符把端点声明为**厂商自定义用法页(0xFF00)**下的原始字节流,主机端 HID API 可原样收发数据而不被系统解释为鼠标/键盘事件:

static const u8 sHIDReportDesc[] = {
    USAGE_PAGE(2, 0x00, 0xff),      // vendor-defined 1
    USAGE(1, 0x01),                 // vendor-value 1
    COLLECTION(1, APPLICATION),     // application
    USAGE(1, 0x02),
    LOGICAL_MIN(2, 0, 0),
    LOGICAL_MAX(2, 0xff, 0),
    REPORT_SIZE(1, 0x08),          // 1 bit
    REPORT_COUNT(1, MAXP_SIZE_HIDIN),         // eight 1bit data
    INPUT(1, 0x02),             // data, variabie, absolute
    USAGE(1, 0x03),
    LOGICAL_MIN(2, 0, 0),
    LOGICAL_MAX(2, 0xff, 0),
    REPORT_SIZE(1, 0x08),
    REPORT_COUNT(1, MAXP_SIZE_HIDOUT),
    OUTPUT(1, 0x02),
    END_COLLECTION,
};

Source: hid.c

设计意图:用"原始字节块"(INPUT/OUTPUT Report)承载升级帧,Report 长度由 MAXP_SIZE_HIDIN / MAXP_SIZE_HIDOUT 决定,既满足免驱要求,又不像标准键盘/鼠标 HID 那样受主机输入系统干扰,天然适合固件传输。

数据收发路径

接收路径在中断端点上注册回调,收到 OUT Report 后立即置"新数据"标志通知升级主流程:

static u8 *hid_ep_in_dma;
static u8 *hid_ep_out_dma;
static struct usb_device_t *hid_device;

u32 hid_tx_data(struct usb_device_t *usb_device, const u8 *buffer, u32 len)
{
    const usb_dev usb_id = usb_device2id(usb_device);
    return usb_g_intr_write(usb_id, HID_EP_IN, buffer, len);
}

extern void set_new_data_flag(int flag);
static void hid_rx_data(struct usb_device_t *usb_device, u32 ep)
{
    const usb_dev usb_id = usb_device2id(usb_device);
    set_new_data_flag(1);
    /* log_info_hexdump(hid_ep_out_dma,64); */
}

Source: hid.c

端点初始化时把 DMA 缓冲直接挂到中断端点并使能:

static void hid_endpoint_init(struct usb_device_t *usb_device, u32 itf)
{
    hid_device = usb_device;
    const usb_dev usb_id = usb_device2id(usb_device);
    usb_g_ep_config(usb_id, HID_EP_IN | USB_DIR_IN, USB_ENDPOINT_XFER_INT, 0, hid_ep_in_dma, MAXP_SIZE_HIDIN);
    usb_enable_ep(usb_id, HID_EP_IN);
    ...
}

Source: hid.c

核心流程

UART 通道升级时序

sequenceDiagram
    participant PC as PC 上位机
    participant DRV as uart_update_driver<br/>(DMA + ISR)
    participant APP as loader_main / update_main
    participant UP as upgrade.h / jlup 库
    participant FLASH as NorFlash

    PC->>DRV: 发送升级命令帧(自定义协议)
    DRV->>DRV: uart_open(1) 配置 RX crossbar 与 DMA
    DRV->>APP: uart_rx_handler 回调(DMA 收满一帧)
    APP->>UP: 帧解析 / CRC 校验 / 命令分发
    UP->>FLASH: 擦除升级分区
    UP->>FLASH: 写入固件数据
    UP-->>APP: 返回进度
    APP->>DRV: uart_open(0) 切到 TX,uart_data_send 应答
    DRV-->>PC: 发送应答帧
    PC->>DRV: 下一帧……直至升级完成

Source 说明:驱动收发接口见 uart_update_driver.c,升级入口见 loader_main.c。

USB HID 通道升级时序

sequenceDiagram
    participant PC as PC 上位机(免驱 HID)
    participant USB as USB 中断端点
    participant HID as hid.c
    participant APP as loader_main / update_main
    participant UP as upgrade.h / jlup 库

    PC->>USB: OUT Report(中断传输,含升级帧)
    USB->>HID: hid_rx_data(usb_device, ep)
    HID->>APP: set_new_data_flag(1)
    APP->>UP: 读取 Report 数据并解析升级帧
    UP-->>APP: 处理结果 / 进度
    APP->>HID: hid_tx_data(usb_device, buf, len)
    HID->>USB: usb_g_intr_write(HID_EP_IN, ...)
    USB-->>PC: IN Report 应答

Source 说明:收发实现见 hid.c。

两条时序的共性:通道驱动只负责"中断/标志 → 回调 → 字节流",升级状态机全部收敛在 jlup 升级库中。因此把 UART 换成 HID(或反之)不影响任何升级协议逻辑。

使用示例

示例 1:切换升级串口(UART 通道)

客户想把升级串口从 UART1 改为 UART2,只需修改 uart_update_driver.c 中的选择宏,寄存器基址与 crossbar 信号会随预处理自动切换:

#define UART0_SEL  0
#define UART1_SEL  1
#define UART2_SEL  2
#define UPDATE_UART_SEL  UART2_SEL  //选择哪组uart作为升级串口

#if (UPDATE_UART_SEL == UART2_SEL)
#define UPDATE_UART  JL_UART2
#define UART_CROSSBAR_RX  PFI_UART2_RX
#define UART_CROSSBAR_TX  FO_UART2_TX
#endif

Source: uart_update_driver.c

示例 2:单 IO 半双工方向切换(UART 通道)

低引脚方案下用一根 IO 分时收发:mode == 1 进入接收态(配置 RX crossbar + DMA 收地址),否则切到发送态。驱动对上层只暴露"打开/发送/关闭"三个原语,方向细节全部内聚:

void uart_open(u8 mode)
{
    if (uart_one_io_mode) {
        UPDATE_UART->CON0 = BIT(13) | BIT(12) | BIT(10);
        if (mode == 1) {//输入
            gpio_set_direction(uart_gpio, 1);
            gpio_set_die(uart_gpio, 1);
            gpio_set_fun_input_port(uart_gpio, UART_CROSSBAR_RX);
            UPDATE_UART->RXSADR = (u32)uart_dma_buf;
            UPDATE_UART->RXEADR = (u32)(uart_dma_buf + UART_DMA_LEN);
            UPDATE_UART->RXCNT = UART_DMA_LEN;
            UPDATE_UART->OTCNT = uart_ot_cnt;
            UPDATE_UART->CON0 |= BIT(6) | BIT(5) | BIT(3) | BIT(1);
        } else {//输出
            gpio_direction_output(uart_gpio, 1);
            gpio_set_hd(uart_gpio, 1);
            gpio_set_fun_output_port(uart_gpio, UART_CROSSBAR_TX, 1, 1);
            UPDATE_UART->CON0 |= BIT(2);
        }
    }
}

Source: uart_update_driver.c

示例 3:HID 应答上报(USB HID 通道)

升级主流程需要向 PC 上报进度或应答时,直接调用 hid_tx_data,底层走 usb_g_intr_write 中断 IN 端点:

u32 hid_tx_data(struct usb_device_t *usb_device, const u8 *buffer, u32 len)
{
    const usb_dev usb_id = usb_device2id(usb_device);
    return usb_g_intr_write(usb_id, HID_EP_IN, buffer, len);
}

Source: hid.c

示例 4:升级入口预留钩子

两个 Loader 工程都预留了弱符号 loader_before_main,客户可在不修改框架代码的前提下插入自定义逻辑(如检测升级请求):

__attribute__((weak))
int loader_before_main(void *arg)
{
    return 0;
}
int main(int arg)
{
    u8 val[16];
    u8 pll_scr = 0;

    loader_before_main((void *)arg);
    ...
}

Source: loader_main.c

配置选项

配置项位置类型/默认值说明
UPDATE_UART_SELuart_update_driver.c L43宏,UART1_SEL选择哪组 UART 作为升级串口(UART0/1/2)
UART_DMA_LENuart_update_driver.c L29宏,544UART DMA 缓冲长度,需对齐 4 字节
FLASH_ID_VALID_CHECK_ENuart_update_driver.c L27宏,0Flash ID 校验使能,置 0 跳过以加快产测
uart_one_io_modeuart_update_driver.c L36静态变量,0单 IO 半双工模式开关;使能后由 uart_open(mode) 切换收发方向
uart_rx_handler / uart_tx_handleruart_update_driver.c L37-L38函数指针,NULL驱动层收发回调注册点,升级主流程通过它们接收/通知数据
MAXP_SIZE_HIDIN / MAXP_SIZE_HIDOUThid.c L43/L50宏(usb_config.h)HID IN/OUT 中断端点最大包长,决定单次 Report 承载的升级数据量
CONFIG_CPU_WL83loader_main.c 编译选项宏目标 CPU 型号;main() 中部分初始化按 CPU 分支(如 port_init 在 WL83 下跳过)
SUPPORT_LCDloader_main.c 编译选项宏使能 LCD 时提前初始化视频时钟(video_clock_early_init(360000000))

API 参考

int loader_before_main(void *arg)

  • 描述:弱符号钩子,在 Loader 主流程前执行,供客户工程覆盖以插入自定义初始化(如按键检测、升级条件判断)。
  • 参数:arg — 复位原因等启动参数。
  • 返回:0 表示继续默认流程;非 0 返回值可被覆盖实现用于提前终止。
  • 位置:loader_main.c L86-L90

void uart_open(u8 mode)

  • 描述:单 IO 模式下切换 UART 收发方向;mode == 1 进入接收态(配置 RX crossbar、DMA 地址与使能位),否则进入发送态(配置 TX crossbar 输出)。
  • 参数:mode — 1 为输入(RX),其他值为输出(TX)。
  • 位置:uart_update_driver.c L81-L111

void uart_data_send(u8 *data, u32 len)

  • 描述:通过写 TXADR / TXCNT 寄存器发起 UART DMA 发送。
  • 参数:data — 待发送数据缓冲;len — 发送字节数。
  • 位置:uart_update_driver.c L113-L117

void uart_hw_close(void)

  • 描述:关闭升级 UART 硬件,释放引脚上下拉/驱动配置,用于升级完成后清理。
  • 位置:uart_update_driver.c L64-L79

u32 hid_tx_data(struct usb_device_t *usb_device, const u8 *buffer, u32 len)

  • 描述:通过 HID 中断 IN 端点向主机发送数据(usb_g_intr_write)。
  • 参数:usb_device — USB 设备句柄;buffer — 发送数据;len — 长度。
  • 返回:发送结果(USB 栈返回值)。
  • 位置:hid.c L92-L96

static void hid_rx_data(struct usb_device_t *usb_device, u32 ep)

  • 描述:HID OUT 端点接收回调,收到主机 Report 后调用 set_new_data_flag(1) 通知升级主流程取数据。
  • 参数:usb_device — USB 设备句柄;ep — 触发回调的端点号。
  • 位置:hid.c L99-L112

static void hid_endpoint_init(struct usb_device_t *usb_device, u32 itf)

  • 描述:初始化 HID 中断端点:usb_g_ep_config 绑定 DMA 缓冲与 MAXP_SIZE_HIDIN 包长,随后 usb_enable_ep 使能 IN 端点(OUT 端点在后续代码中对称配置)。
  • 参数:usb_device — USB 设备句柄;itf — 接口号。
  • 位置:hid.c L114-L120

失败模式、边界情况与并发

UART 通道

  • DMA 缓冲越界:uart_dma_buf 长度固定为 UART_DMA_LEN(544),uart_open(1) 将 RXEADR 设为 uart_dma_buf + UART_DMA_LEN,RXCNT 设为 544。若上层帧超过该长度,DMA 会按环形/溢出行为处理(OTCNT 记录溢出计数 uart_ot_cnt),因此升级帧必须按 544 字节以内的传输单元分片。这是协议层与驱动层的隐式契约,改动 UART_DMA_LEN 时需同步调整上位机分片大小。
  • 单 IO 方向竞争:uart_one_io_mode 使能时,收/发共用一根 IO。uart_open(1) 与 uart_open(0) 之间若被中断抢占,可能出现方向未切换就收发的问题。从代码看方向切换在 gpio_set_direction / crossbar 配置处是顺序执行的,但业务侧应保证"先切方向、再发数据"(uart_data_send 前确保处于 TX 态)。
  • 引脚状态残留:升级完成后若不调用 uart_hw_close(),UART 引脚的上下拉/驱动配置会残留,可能干扰后续业务外设。uart_hw_close 中注释掉的 gpio_set_pull_up/down、gpio_set_die、gpio_set_output_value、gpio_set_direction 序列(L68-L77)提示了完整的引脚释放方案,当前默认走空分支,客户可按需放开。
  • Flash ID 校验关闭:FLASH_ID_VALID_CHECK_EN = 0 时跳过 Flash ID 校验,可加快产测,但失去对"插错 Flash"的保护——量产时应评估是否开启。

USB HID 通道

  • Report 分片:单次 IN/OUT Report 长度受 MAXP_SIZE_HIDIN / MAXP_SIZE_HIDOUT 限制,固件包必须分片传输;hid_rx_data 只置 set_new_data_flag(1) 标志而不搬运数据(数据由升级主流程从 hid_ep_out_dma 读取),因此标志消费与数据读取之间不能丢失中断,主循环需及时轮询该标志。
  • 免驱与兼容性:厂商自定义用法页(0xFF00)不属于系统 HID 消费类,Windows/macOS/Linux 均可免驱访问,但需要上位机使用 HID API 打开设备并按 Report 格式读写;若被系统枚举成键盘/鼠标子类则会被拦截。
  • 中断端点轮询间隔:IN 端点 bInterval = 1(全速 1ms、高速 125µs 级),OUT 端点为 0x01(hid.c L44/L51),决定了升级吞吐上限,属于 USB 协议约束而非软件可调项。

并发与中断安全

  • uart_isr() 以 SET_INTERRUPT 声明(uart_update_driver.c L119),回调(uart_rx_handler)在中断上下文执行,回调内部不应做耗时操作(如 Flash 擦写),只应做标志/队列通知,真正的擦写放到主循环或升级库上下文中——这与 HID 侧 set_new_data_flag(1) 的"中断里只置标志"是同一设计原则。
  • 两个通道工程的 main() 都在使用 malloc 前调用 dynamic_mem_init(loader_main.c L112-L118),这是并发/堆安全的前提:堆未初始化前任何 malloc/free 都是未定义行为。

性能与运维建议

  • UART 吞吐:发送路径只写两个寄存器(TXADR/TXCNT)即交给 DMA,CPU 开销极低;吞吐主要受波特率与上位机协议限制。升级大包时建议按 UART_DMA_LEN(544)对齐分片。
  • HID 吞吐:受中断端点轮询间隔约束,全速下约每 1ms 一个 Report,单 Report 大小由 MAXP_SIZE_HIDIN/OUT 决定;如需更高吞吐可评估在保持 HID 类的前提下增大 Report 长度。
  • 日志可观测性:mask_api_init(putchar, ...) 把打印挂到串口(loader_main.c L102-L106),产测可同时抓升级 log 与协议应答,快速定位失败帧。升级进度 UI 方案可参考 ota_loader_update_800x480/custom/custom.c 的 gui_msg_send(GUI_UPDATE_MSG_ID_UPDATE_PROCESS, ...) 通知模式。

扩展点

  1. loader_before_main 弱符号:两个工程均预留,最适合插入"进入升级模式"的判定逻辑(按键、GPIO 电平、外部命令),且不影响框架代码。
  2. uart_rx_handler / uart_tx_handler 回调:UART 驱动的协议无关收发钩子,可替换为自定义帧协议,升级核心(jlup)无需改动。
  3. UPDATE_UART_SEL / crossbar 宏:换串口、换引脚只改宏;如需新的 UART 外设实例,按现有 #if 块模式扩展即可。
  4. HID Report 描述符:sHIDReportDesc 与 sHIDDescriptor 均为静态数组,可调整 Report 长度/端点数量来适配不同上位机工具,但需与 usb_config.h 的 MAXP_SIZE_HIDIN/OUT 保持一致。
  5. 新增通道:复制 uart_user_update 工程骨架(入口 + cpu/<chip>/ 驱动 + include_lib/),只替换驱动层并保持 upgrade.h 调用不变,即可接入 SPI、I2C 等新介质。

测试与验证观察

  • 代码中未发现针对驱动层的独立单元测试,验证方式以实机产测为主:上位机分片发送 → 观察 uart_isr / hid_rx_data 触发 → 升级完成后校验固件版本与 Flash 内容。
  • hid.c 中保留了一段被 #if 0 屏蔽的回环测试代码(hid.c L104-L110):收到数据后用 JL_RAND->R64L 填充并经 IN 端点回发,并 log_info_hexdump 打印,是排查 HID 收发链路的快捷手段,调试时可临时放开。
  • 驱动文件顶部定义了完整的 LOG_TAG_CONST UT_UPDATE_DRIVER 与 LOG_TAG "[UT_UPDATE_DRIVER]"(uart_update_driver.c L14-L21),编译期可裁剪 LOG_DEBUG_ENABLE / LOG_INFO_ENABLE 来控制调试输出量。

Related Links

  • UART 用户升级工程 loader_main.c
  • UART 升级驱动 uart_update_driver.c(WL83)
  • USB HID 升级工程 loader_main.c
  • HID 驱动实现 hid.c(user_boot)
  • 升级库接口 upgrade.h(uart_user_update)
  • 升级库接口 upgrade.h(usb_hid_ota_update)
  • 带 LCD 界面的升级 Loader(进度通知示例)
  • 兄弟页面:SD 卡升级(sd_ota_update / sd_sec_ota_update)、USB 升级(usb_ota_update / usb_sec_ota_update)
Prev
安全升级通道(SD/USB)
Next
LVGL 图形化升级界面与模拟器