用户自定义升级通道(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.c | UART 通道底层:选择升级串口、DMA 收发、单 IO 半双工切换 | uart_update_driver.c |
hid.c | HID 通道底层:Vendor-Defined HID 描述符、中断 IN/OUT 端点收发 | hid.c |
upgrade.h | jlup 升级库头文件,声明升级接口(两通道均包含) | uart_user_update/upgrade.h |
update_main.h / norflash.h | 升级主流程与 Flash 擦写 | uart_user_update/loader_main.c |
架构上的关键设计点:
- 弱符号钩子
loader_before_main:loader_main.c中声明了__attribute__((weak))的loader_before_main(void *arg),默认返回 0,允许用户在进入升级主流程前插入自定义初始化(如检测按键进入升级模式)。 - 驱动通过回调与主流程解耦:
uart_update_driver.c维护uart_rx_handler/uart_tx_handler两个静态函数指针,升级主流程注册回调后即可在中断/DMA 收齐数据时被通知,驱动本身不感知帧内容。 - 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_SEL | uart_update_driver.c L43 | 宏,UART1_SEL | 选择哪组 UART 作为升级串口(UART0/1/2) |
UART_DMA_LEN | uart_update_driver.c L29 | 宏,544 | UART DMA 缓冲长度,需对齐 4 字节 |
FLASH_ID_VALID_CHECK_EN | uart_update_driver.c L27 | 宏,0 | Flash ID 校验使能,置 0 跳过以加快产测 |
uart_one_io_mode | uart_update_driver.c L36 | 静态变量,0 | 单 IO 半双工模式开关;使能后由 uart_open(mode) 切换收发方向 |
uart_rx_handler / uart_tx_handler | uart_update_driver.c L37-L38 | 函数指针,NULL | 驱动层收发回调注册点,升级主流程通过它们接收/通知数据 |
MAXP_SIZE_HIDIN / MAXP_SIZE_HIDOUT | hid.c L43/L50 | 宏(usb_config.h) | HID IN/OUT 中断端点最大包长,决定单次 Report 承载的升级数据量 |
CONFIG_CPU_WL83 | loader_main.c 编译选项 | 宏 | 目标 CPU 型号;main() 中部分初始化按 CPU 分支(如 port_init 在 WL83 下跳过) |
SUPPORT_LCD | loader_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, ...)通知模式。
扩展点
loader_before_main弱符号:两个工程均预留,最适合插入"进入升级模式"的判定逻辑(按键、GPIO 电平、外部命令),且不影响框架代码。uart_rx_handler/uart_tx_handler回调:UART 驱动的协议无关收发钩子,可替换为自定义帧协议,升级核心(jlup)无需改动。UPDATE_UART_SEL/ crossbar 宏:换串口、换引脚只改宏;如需新的 UART 外设实例,按现有#if块模式扩展即可。- HID Report 描述符:
sHIDReportDesc与sHIDDescriptor均为静态数组,可调整 Report 长度/端点数量来适配不同上位机工具,但需与usb_config.h的MAXP_SIZE_HIDIN/OUT保持一致。 - 新增通道:复制
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)