Posted in

Go语言数控机硬件抽象层HAL设计(统一x86/ARM64/RISC-V平台,GPIO/PWM/ADC驱动模板库)

第一章:Go语言数控机硬件抽象层HAL设计概览

在现代数控(CNC)系统中,硬件抽象层(HAL)是连接上层控制逻辑与底层物理设备(如步进驱动器、伺服电机、限位开关、主轴变频器等)的关键枢纽。Go语言凭借其并发模型、内存安全性、跨平台编译能力及简洁的接口机制,成为构建高可靠性、可维护HAL的理想选择。本章聚焦于以Go为核心实现的HAL架构设计原则与核心组件构成。

设计目标与核心理念

  • 统一设备建模:所有硬件外设均实现 Device 接口,包含 Init(), Read(), Write(), Close() 方法;
  • 实时性保障:通过 time.Ticker 驱动周期性状态轮询,并结合 runtime.LockOSThread() 绑定关键任务至专用OS线程;
  • 故障隔离:每个设备驱动运行于独立 goroutine,panic 由 recover() 捕获并上报至中央诊断总线。

接口定义示例

// Device 表示可被HAL管理的任意硬件单元
type Device interface {
    Init(ctx context.Context) error          // 初始化硬件寄存器/通信链路
    Read() (map[string]interface{}, error)    // 返回当前状态快照(如位置、温度、IO电平)
    Write(cmd Command) error                  // 下发控制指令(如“移动X轴10mm”)
    Close() error                             // 安全断开硬件连接
}

// Command 是标准化指令载体,支持JSON序列化便于日志与调试
type Command struct {
    Target string                 `json:"target"`   // "axis_x", "spindle"
    Action string                 `json:"action"`   // "move", "enable", "stop"
    Params map[string]interface{} `json:"params"`   // {"distance_mm": 10.0, "speed_rpm": 1200}
}

典型驱动集成流程

  1. 创建设备实例(如 newStepperDriver("/dev/ttyUSB0", 9600));
  2. 调用 device.Init(context.WithTimeout(ctx, 3*time.Second)) 建立串口通信并校验固件版本;
  3. 启动状态同步协程:go func() { for range ticker.C { _ = device.Read() } }()
  4. 通过 device.Write(Command{Target: "axis_z", Action: "move", Params: map[string]any{"distance_mm": -5.2}}) 发起运动指令。
组件 职责说明 Go实现要点
HAL Core 设备注册、生命周期管理、事件分发 使用 sync.Map 存储设备引用
Protocol Adaptor 封装Modbus RTU/ASCII、CANopen等协议 基于 gobitcan" library 扩展
Safety Monitor 实时监控急停信号、过温、超程 独立 goroutine + select 超时检测

第二章:跨平台硬件抽象架构设计与实现

2.1 x86/ARM64/RISC-V指令集差异建模与统一寄存器映射策略

不同ISA在寄存器语义、数量与调用约定上存在根本性差异:x86-64有16个通用寄存器(RAX–R15),但隐含用途多;ARM64固定31个X0–X30通用寄存器,X29/X30专用于帧指针/链接寄存器;RISC-V(RV64GC)则采用32个x0–x31,其中x0恒为零,x1/x5/x10–x17有ABI约定用途。

寄存器语义对齐表

ISA 物理寄存器数 ABI保留寄存器 调用者保存范围 被调用者保存范围
x86-64 16 RSP, RBP, RFLAGS RAX–RDX, RSI, RDI RBX, RBP, R12–R15
ARM64 31 SP, X29, X30 X0–X7, X16–X17 X19–X29
RISC-V 32 (x0–x31) x0, x1, x3, x5 x1, x3–x7, x10–x17 x8–x9, x18–x27

统一映射核心逻辑(伪代码)

// 将目标ISA寄存器名映射到统一抽象寄存器ID(0–31)
int isa_reg_to_unified(const char* isa_name, ISA_T isa) {
  static const int x86_map[] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; // RAX→0, RCX→1...
  static const int a64_map[] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30}; // X0→0...X30→30
  static const int rv_map[]  = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31}; // x0→0...x31→31
  // 注:实际实现需结合寄存器名字符串解析(如"RAX"→0, "X29"→29, "x10"→10)
  // 参数isa_name为原始寄存器标识符,isa指定源架构类型,返回全局唯一RegID用于后续SSA重命名
}

数据同步机制

跨ISA寄存器状态迁移需在函数入口/出口插入显式save/restore stub,依据统一ID索引物理寄存器池。

2.2 基于Go interface{}与unsafe.Pointer的零拷贝硬件地址空间抽象

在嵌入式驱动开发中,直接映射PCIe BAR或MMIO区域需绕过Go运行时内存管理,同时保持类型安全与零拷贝语义。

核心抽象模式

  • interface{}承载任意硬件寄存器结构体(如*DeviceCtrlReg
  • unsafe.Pointer实现物理地址到虚拟地址的无开销转换
  • 配合runtime.KeepAlive()防止GC过早回收映射页

寄存器访问示例

type DeviceCtrlReg struct {
    Ctrl  uint32
    Stat  uint32
    Data  [64]byte
}

func MapBAR(paddr uintptr, size int) *DeviceCtrlReg {
    mem, _ := syscall.Mmap(-1, int64(paddr), size,
        syscall.PROT_READ|syscall.PROT_WRITE,
        syscall.MAP_SHARED|syscall.MAP_LOCKED)
    return (*DeviceCtrlReg)(unsafe.Pointer(&mem[0]))
}

MapBAR将物理地址paddr映射为可读写内存页,并通过unsafe.Pointer强制转为寄存器结构体指针。syscall.MAP_LOCKED确保页常驻物理内存,避免缺页中断破坏实时性。

特性 interface{}方案 unsafe.Pointer方案
类型安全性 ✅ 编译期检查 ❌ 运行时风险
内存拷贝开销 ❌ 零拷贝 ❌ 零拷贝
GC干扰 ⚠️ 需显式保活 ⚠️ 需显式保活
graph TD
    A[物理地址paddr] --> B[syscall.Mmap]
    B --> C[[]byte slice]
    C --> D[unsafe.Pointer]
    D --> E[*DeviceCtrlReg]

2.3 平台无关时钟树建模与周期性任务调度器(Ticker-based HAL Scheduler)

为解耦硬件定时器差异,HAL 层抽象出时钟树模型:以 Ticker 为根节点,支持多级分频子节点(如 1kHz → 100Hz → 10Hz),所有节点共享统一 tick 基准。

核心数据结构

typedef struct {
    uint32_t period_ticks;   // 相对于父节点的分频系数
    void (*callback)(void);    // 周期触发回调
    bool enabled;             // 运行态开关
} TickerNode;

period_ticks = 1 表示直通父时钟;值越大,频率越低。回调在 HAL 底层中断上下文中安全调用。

调度流程

graph TD
    A[SysTick ISR] --> B[遍历Ticker树]
    B --> C{节点enabled?}
    C -->|Yes| D[累加local_counter]
    D --> E{local_counter ≥ period_ticks?}
    E -->|Yes| F[执行callback; reset counter]

优势对比

特性 传统裸机延时 Ticker-based HAL
可移植性 强依赖 MCU 定时器寄存器 仅需实现 HAL_Ticker_Tick() 接口
多任务协同 需手动管理标志位 自动同步 tick 边沿,天然支持 jitter-free 调度

2.4 中断向量表动态注册机制与协程安全ISR封装实践

传统静态中断向量表难以适配运行时加载的驱动模块。动态注册机制通过 register_isr_handler(uint8_t irq_num, isr_func_t handler) 实现运行时绑定,支持热插拔外设。

协程安全关键约束

  • ISR 不得直接调度协程(破坏原子性)
  • 必须通过 post_to_scheduler() 异步通知协程上下文
  • 所有共享状态需用 atomic_flagspinlock_t 保护

核心封装模式

void safe_uart_isr(void) {
    uint32_t status = UART->STAT & UART_INT_MASK;
    if (status & RX_READY) {
        char c = UART->DATA;
        // ✅ 原子入队:无锁环形缓冲区
        ringbuf_push(&rx_fifo, c); 
        // ✅ 异步唤醒:仅触发调度信号
        post_to_scheduler(UART_RX_EVENT); 
    }
}

逻辑分析:ringbuf_push() 使用 __atomic_store_n() 保证单字节写入原子性;post_to_scheduler() 向协程调度器投递事件ID,由主循环在非中断上下文处理实际读取——彻底规避栈切换与重入风险。

安全维度 静态ISR 动态+协程封装
中断嵌套容忍度 高(事件解耦)
内存占用 固定 按需分配缓冲区
graph TD
    A[硬件中断触发] --> B{ISR执行}
    B --> C[原子读取寄存器]
    C --> D[无锁写入环形缓冲]
    D --> E[发送调度事件]
    E --> F[主循环中协程消费]

2.5 HAL初始化生命周期管理:从Bootloader Handoff到Runtime HAL Ready状态机

HAL(Hardware Abstraction Layer)的初始化并非线性执行,而是一个受事件驱动的状态跃迁过程。其起点是 Bootloader 完成内存映射与早期外设配置后的 handoff 阶段,终点是 HAL_STATE_READY 被原子置位、所有硬件代理可被 Runtime 框架安全调用。

状态机核心跃迁路径

graph TD
    A[BOOTLOADER_HANDOFF] --> B[HAL_PRE_INIT]
    B --> C[HAL_DEVICE_PROBE]
    C --> D[HAL_POWER_ON_SEQUENCE]
    D --> E[HAL_CALIBRATE_AND_SYNC]
    E --> F[HAL_STATE_READY]

关键初始化钩子示例

// hal_core.c: 主状态跃迁调度器
hal_status_t hal_transition_to(hal_state_t target) {
    static const hal_state_t valid_transitions[] = {
        HAL_STATE_BOOTLOADER_HANDOFF,
        HAL_STATE_PRE_INIT,
        HAL_STATE_DEVICE_PROBE,
        HAL_STATE_POWER_ON,
        HAL_STATE_CALIBRATED,
        HAL_STATE_READY
    };
    // 参数说明:
    // - target:目标状态,必须为valid_transitions中定义的合法值
    // - 返回值:HAL_OK表示跃迁成功;HAL_BUSY表示当前处于不可中断的校准阶段
    return hal_state_machine_step(target);
}

该函数通过查表约束状态跃迁合法性,避免非法跳转导致硬件上下文错乱。

初始化阶段依赖关系

阶段 依赖条件 超时阈值 失败后果
DEVICE_PROBE GPIO/CLK已使能 50ms HAL_ABORT,触发看门狗复位
POWER_ON_SEQUENCE 电压轨稳定检测通过 100ms 回滚至PRE_INIT并记录PMIC错误码
CALIBRATE_AND_SYNC ADC基准完成自校准 200ms 降级为HAL_STATE_READY_WITH_WARN

第三章:核心外设驱动模板库设计原理

3.1 GPIO驱动模板:状态机驱动模型与边缘触发原子操作封装

核心设计思想

将GPIO引脚生命周期抽象为四态机:IDLE → ARMED → TRIGGERED → HANDLED,避免轮询开销,确保事件响应的确定性与时序安全。

原子边缘捕获封装

// 原子读-改-写:仅在上升沿置位触发标志,且不可被中断打断
static inline bool gpio_edge_rising_atomic(volatile uint32_t *reg, uint8_t pin) {
    const uint32_t mask = 1U << pin;
    uint32_t old = __LDREXW(reg);           // 获取独占访问
    uint32_t new = old & ~mask;            // 清除原触发位(防重复)
    if (__STREXW(0, reg, new) == 0) {      // 成功提交
        __DMB();                           // 内存屏障保证顺序
        return (old & mask);               // 返回此前是否已置位(用于判别边沿)
    }
    return false;
}

逻辑分析:利用ARM LDREX/STREX指令实现无锁原子操作;pin参数指定物理引脚编号(0–31);返回值为true表示本次检测到有效上升沿,驱动层据此触发状态迁移。

状态迁移约束表

当前状态 输入事件 新状态 是否唤醒线程
IDLE 配置完成 ARMED
ARMED 上升沿捕获成功 TRIGGERED
TRIGGERED ISR处理完毕 HANDLED

数据同步机制

使用内存屏障(__DMB)与状态寄存器双缓冲,确保中断上下文与线程上下文对state变量的访问一致性。

3.2 PWM驱动模板:高精度占空比控制与死区时间(Dead-time)可配置生成器

核心设计目标

  • 占空比分辨率 ≥ 16 bit(65536 级)
  • 死区时间独立可配,支持纳秒级步进(依赖系统时钟)
  • 输出通道成对互补,硬件自动插入死区,避免直通

数据同步机制

双缓冲寄存器确保占空比与死区更新原子生效:

  • CMP_BUF(比较值缓存)与 DT_BUF(死区值缓存)在下个周期起始同步加载
  • 避免运行中突变导致毛刺

配置代码示例

// 初始化一对互补PWM通道(CH0/CH1),死区=200ns(假设PCLK=100MHz → 10ns/cycle)
pwm_set_complementary_pair(PWM0, CH0, CH1);
pwm_set_duty_cycle(PWM0, CH0, 0x8000);     // 50% 占空比(16-bit)
pwm_set_dead_time_ns(PWM0, 200);           // 全局死区基准
pwm_enable(PWM0);

逻辑分析pwm_set_dead_time_ns() 将200ns映射为20个时钟周期,并写入专用DT预分频+计数寄存器;硬件在CH0关断与CH1开启间自动插入该延迟,无需CPU干预。占空比通过影子寄存器更新,确保边沿对齐。

关键寄存器映射

寄存器名 功能 位宽 示例值
DUTY_SHDW 占空比影子寄存器 16 0x8000
DT_PRESCALER 死区时间分频系数 4 0b0001
DT_COUNTER 死区基础计数值(cycles) 12 20

3.3 ADC驱动模板:采样序列引擎(Scan Sequence Engine)与DMA预取缓冲区抽象

数据同步机制

Scan Sequence Engine(SSE)将通道配置、采样时序、触发源封装为可复用的序列描述符,解耦硬件寄存器操作与业务逻辑。

DMA预取缓冲区抽象

采用环形双缓冲区(dma_prebuf_a/b),由硬件自动切换,驱动层通过原子索引 next_read_idx 安全读取:

// 双缓冲区状态管理(伪代码)
static uint16_t dma_prebuf_a[16];
static uint16_t dma_prebuf_b[16];
static volatile uint8_t active_buf = 0; // 0: a, 1: b
static volatile uint8_t next_read_idx = 0;

// 硬件DMA完成中断中调用
void dma_transfer_complete_isr(void) {
    active_buf ^= 1;           // 切换缓冲区
    next_read_idx = 0;         // 重置读索引
}

逻辑分析active_buf 异或翻转实现零拷贝切换;next_read_idx 保证应用层按序消费,避免竞态。缓冲区大小(16)需匹配SSE配置的序列长度。

缓冲区属性 说明
容量 16 对齐最大扫描序列长度
对齐要求 32-byte 满足DMA控制器总线宽度约束
访问模式 生产者-消费者 SSE为生产者,应用线程为消费者
graph TD
    A[SSE配置序列] --> B[启动ADC采样]
    B --> C[DMA写入active_buf]
    C --> D{DMA传输完成?}
    D -->|是| E[切换active_buf & 通知]
    E --> F[应用读取next_read_idx]

第四章:HAL工程化落地与数控场景适配

4.1 数控G代码解析器与HAL实时指令翻译层(G-Code → HAL Action Primitives)

G代码解析器承担语法识别与语义提取双重职责,将文本化NC程序转化为结构化中间表示(IR);HAL翻译层则将其映射为确定性、可调度的底层动作原语(如 hal_pin_write("axis.x.command", value))。

核心翻译流程

// 示例:G01 X10.5 F200 → 线性插补指令转HAL动作
hal_pin_float_t *x_cmd = hal_pin_float_new("motion.x.command", HAL_IN, comp_id);
hal_pin_float_t *vel_cmd = hal_pin_float_new("motion.vel-cmd", HAL_IN, comp_id);
*x_cmd = 10.5;        // 目标位置(mm)
*vel_cmd = 200.0/60.0; // 转换为 mm/s

该代码将G01的位移与进给率解耦为独立HAL引脚写入操作,确保硬实时上下文中的原子性更新;comp_id标识所属HAL组件,保障多任务隔离。

G-Code指令到HAL原语映射表

G代码 HAL Action Primitive 实时约束
G0/G1 motion.traj.pos-cmd + motion.vel-cmd
M3 spindle.0.on = true
G28 motion.home-all = 1

数据同步机制

graph TD
    A[G-Code Parser] -->|AST Node| B[Semantic Validator]
    B -->|Valid IR| C[HAL Translator]
    C -->|Pin Writes| D[HAL Realtime Thread]

4.2 多轴步进/伺服电机同步控制:基于HAL Timer+GPIO+PWM的硬实时协同范式

核心协同机制

HAL定时器(TIMx)作为主时基源,触发更新事件(UEV),同步更新多通道PWM寄存器;GPIO复用为高速同步信号线(如SYNC_OUT),实现跨MCU或驱动器级联对齐。

数据同步机制

// 启用TIM1主模式:UEV作为同步触发源
htim1.Instance = TIM1;
htim1.Init.Period = 999;           // 1ms基准周期(假设100MHz APB2)
htim1.Init.Prescaler = 99;          // 分频后1MHz计数频率
HAL_TIM_Base_Init(&htim1);
HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig);
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; // 关键:以UEV为TRGO
HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig);

逻辑分析:TIM_TRGO_UPDATE使所有从属外设(如TIM2/TIM3的PWM通道、GPIO输出触发)在每次计数器溢出时严格对齐,误差

硬件资源映射表

外设 功能 同步角色
TIM1 主时基 + TRGO源 Master
TIM2/TIM3 多轴PWM波形生成 Slave
GPIOA.8 SYNC_OUT硬同步脉冲 辅助对齐

协同流程

graph TD
    A[TIM1计数溢出] --> B[产生UEV]
    B --> C[TRGO广播至TIM2/TIM3]
    B --> D[GPIOA.8翻转输出SYNC脉冲]
    C --> E[TIM2/TIM3同步更新CCRx]
    D --> F[外部驱动器锁存相位]

4.3 工业IO安全隔离设计:HAL层看门狗联动、过流中断熔断与故障注入测试框架

工业IO子系统需在硬件异常时实现毫秒级自主裁决。核心机制包含三重协同防护:

HAL层看门狗联动机制

// STM32 HAL示例:I/O通道独立喂狗+主WDT级联
HAL_WDG_Start_IT(&hwdg);                    // 启动独立看门狗(窗口模式)
HAL_GPIO_WritePin(FAULT_EN_GPIO_Port, FAULT_EN_Pin, GPIO_PIN_SET); // 使能故障输出通路

逻辑分析:hwdg配置为窗口看门狗(IWDG),超时阈值设为120ms;FAULT_EN引脚直连PLC安全继电器,实现硬件级强制隔离,避免软件死锁导致的失控。

过流中断熔断流程

graph TD
    A[ADC采样电流] --> B{>3.2A?}
    B -->|Yes| C[触发EXTI中断]
    C --> D[禁用PWM输出]
    D --> E[拉低FAULT_EN]

故障注入测试框架能力对比

测试类型 注入粒度 响应延迟 支持IO通道
模拟短路 引脚级 全部
PWM信号毛刺 时序级 6路
电源跌落模拟 电压域 可配置 3轨

4.4 构建可验证HAL固件镜像:eBPF辅助的运行时外设访问审计与内存安全边界检查

传统HAL固件缺乏细粒度访问控制,易因越界写入或非法外设寄存器操作引发系统崩溃。本方案将eBPF程序注入固件启动流程,在MMU页表映射阶段动态注入安全钩子。

安全钩子注入机制

// eBPF程序片段:拦截外设访问(基于bpf_probe_read_kernel)
SEC("kprobe/hal_periph_write")
int BPF_KPROBE(trace_periph_write, void *addr, uint32_t val) {
    if (!is_valid_periph_addr(addr))          // 检查地址白名单
        return 0;                              // 拒绝非法写入
    bpf_printk("HAL write @%px = 0x%x", addr, val);
    return 1;
}

该eBPF程序挂载于HAL写函数入口,通过is_valid_periph_addr()查表校验地址是否属于预注册外设空间(如0x4000_0000–0x400F_FFFF),避免硬编码导致维护困难。

运行时检查策略对比

检查类型 触发时机 开销 可验证性
编译期静态断言 链接时
eBPF运行时审计 每次外设访问 中(需签名eBPF字节码)
MPU硬件防护 内存访问瞬间 极低 弱(配置易被绕过)
graph TD
    A[固件加载] --> B[eBPF验证器校验签名]
    B --> C[加载至内核eBPF VM]
    C --> D[MMU映射完成前注入钩子]
    D --> E[运行时外设/内存访问拦截]

第五章:未来演进方向与开源生态共建

模型轻量化与边缘端协同推理的规模化落地

2024年,OpenMMLab 3.0 发布后,MMDetection v3.3.0 集成 ONNX Runtime Web 后端,实现在 Chrome 浏览器中直接运行 YOLOv8s 检测模型(mmdeploy 工具链统一导出接口——支持 TensorRT、Core ML、RKNN 等 9 类后端一键切换。

开源协议驱动的商业友好型协作机制

Apache 2.0 协议已成为主流 AI 框架标配,但实际落地仍需细化治理。Hugging Face Transformers 于 v4.41.0 引入“CLA Bot”自动校验机制:所有 PR 提交前强制签署贡献者许可协议,并关联 GitHub 组织白名单。截至 2024 年 Q2,该机制拦截了 127 起未授权代码合并请求,同时使企业级用户(如 AWS SageMaker 集成团队)可合法合规地将 transformers 模块嵌入其托管服务 SLA 中。

多模态模型训练基础设施的标准化演进

组件类型 社区主导项目 生产环境验证案例 关键改进点
数据流水线 WebDataset + TorchData Meta Llama 3 训练集群(2048 A100) 支持跨 128 节点并行 shuffle
分布式训练框架 DeepSpeed + FSDP 阿里通义千问 Qwen2-72B 全参数微调 ZeRO-3 内存峰值降低 41%
检查点兼容层 safetensors Stability AI SDXL 1.0 官方权重加载模块 加载速度提升 3.2×,无 pickle 安全风险

可信 AI 工具链的社区共建实践

Ludwig 框架在 v0.9 版本中集成 SHAP 解释器插件,允许用户仅通过 YAML 配置即可生成特征重要性热力图。某银行风控团队基于该能力构建反欺诈模型审计看板:输入样本经 ludwig explain --dataset fraud_test.csv 自动生成 HTML 报告,包含 Top-5 影响因子及局部依赖曲线,满足银保监会《人工智能算法风险管理指引》第 22 条可解释性要求。

flowchart LR
    A[GitHub Issue 提出需求] --> B{社区投票 ≥50+ 👍}
    B -->|Yes| C[CLA 签署 & RFC 提交]
    C --> D[CI 自动触发 benchmark 对比]
    D --> E[Docs/Tests/Changelog 全覆盖 PR]
    E --> F[Maintainer 2FA 合并]
    F --> G[PyPI / Conda-forge 自动发布]
    G --> H[Discord #release-channel 推送]

跨组织模型即服务(MaaS)接口规范推进

Linux 基金会 LF AI & Data 下属的 MLOps WG 正推动 Model Interface Specification v1.0 标准,定义统一的 /v1/models/{id}:predict REST 接口与 OpenAPI 3.1 Schema。目前已有 14 个开源项目实现兼容,包括 Triton Inference Server v24.04、KServe v0.14 和 vLLM v0.4.2。某省级政务云平台据此完成 37 个部门 AI 模型的统一纳管,API 调用成功率从 89.2% 提升至 99.97%。

开源教育与开发者赋能闭环建设

Hugging Face 的 “Model Hub Academy” 已上线 217 个实战课程模块,其中 43 个由企业贡献者共建(如 NVIDIA 的 CUDA Graph 优化实训、Intel 的 OpenVINO 部署沙盒)。2024 年上半年,通过该平台完成认证的开发者中,38% 在 90 天内向对应仓库提交有效 PR,平均修复 issue 响应时间缩短至 11.3 小时。

敏捷如猫,静默编码,偶尔输出技术喵喵叫。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注