Posted in

Raspberry Pi + Go + GPIO控制,零基础30分钟搞定物联网边缘节点,附12个可直接运行的示例工程

第一章:Raspberry Pi + Go + GPIO开发环境全景概览

Raspberry Pi 作为嵌入式开发的主流硬件平台,结合 Go 语言的并发能力、跨平台编译特性和轻量级运行时,为构建高可靠性 GPIO 控制系统提供了理想组合。本章梳理从物理设备准备到可执行 GPIO 程序落地的完整技术栈视图。

硬件与系统基础

需选用支持 GPIO 的树莓派型号(如 Raspberry Pi 4B/5),搭配官方推荐电源与 microSD 卡;操作系统建议使用 Raspberry Pi OS (64-bit),默认启用 gpio 组权限并预装 libgpiod 工具集。验证基础状态可执行:

# 检查内核是否加载 gpiochip 支持
ls /dev/gpiochip*  # 应输出 /dev/gpiochip0 等设备节点
# 查看当前用户所属组(确保含 gpio 组)
groups | grep gpio

Go 运行时与交叉编译配置

树莓派原生安装 Go(ARM64 架构)最简方式:

sudo apt update && sudo apt install golang-go
go version  # 验证输出类似 go version go1.21.x linux/arm64

若在 x86_64 开发机编译,需启用交叉编译:

GOOS=linux GOARCH=arm64 go build -o led-blink main.go
# 编译后通过 scp 传输至树莓派执行

GPIO 抽象层选型对比

方案 特点 推荐场景
periph.io 纯 Go 实现,无需 CGO,支持多种总线 快速原型、教育项目
tinygo-drivers 专为 TinyGo 优化,部分驱动兼容 Go 资源极度受限设备
gobot 框架级抽象,内置事件驱动模型 多传感器协同控制场景

初始化 GPIO 控制示例

以下代码使用 periph.io 控制 GPIO 17 输出高低电平(需先 go get periph.io/x/periph/...):

package main

import (
    "log"
    "time"
    "periph.io/x/periph/conn/gpio"
    "periph.io/x/periph/host"
)

func main() {
    // 初始化主机驱动(自动检测树莓派平台)
    if _, err := host.Init(); err != nil {
        log.Fatal(err)
    }

    // 获取 GPIO 17 引脚(BCM 编号),设为输出模式
    pin := gpio.MustFind("GPIO17")
    if err := pin.Out(gpio.High); err != nil {
        log.Fatal(err)
    }

    // 闪烁 LED:高电平 500ms → 低电平 500ms
    for i := 0; i < 5; i++ {
        pin.Set(gpio.High)
        time.Sleep(500 * time.Millisecond)
        pin.Set(gpio.Low)
        time.Sleep(500 * time.Millisecond)
    }
}

第二章:Go语言嵌入式编程核心基础

2.1 Go交叉编译与ARM64目标平台适配实践

Go 原生支持跨平台编译,无需额外工具链即可生成 ARM64 可执行文件。

环境准备要点

  • 确保 Go 版本 ≥ 1.16(ARM64 支持已稳定)
  • 目标系统为 Linux/ARM64(如树莓派 4、AWS Graviton 实例)

编译命令与参数解析

GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o app-arm64 .
  • GOOS=linux:指定目标操作系统为 Linux(非默认 darwinwindows
  • GOARCH=arm64:启用 AArch64 指令集生成(非 armamd64
  • CGO_ENABLED=0:禁用 cgo,避免依赖宿主机 C 工具链,提升可移植性

典型适配检查项

检查项 推荐做法
第三方库兼容性 优先选用纯 Go 实现(如 golang.org/x/sys
系统调用差异 避免硬编码 /proc/sys/kernel/ 下的 x86_64 专用路径
性能敏感代码 使用 runtime.GOARCH == "arm64" 条件编译
graph TD
    A[源码] --> B[GOOS=linux GOARCH=arm64]
    B --> C{CGO_ENABLED=0?}
    C -->|是| D[静态链接二进制]
    C -->|否| E[需部署 libc/arm64]

2.2 GPIO抽象层设计原理与sysfs /dev/gpiomem底层机制解析

GPIO抽象层核心目标是解耦硬件寄存器操作与用户空间接口,提供统一的编程视图。Linux内核通过gpiolib实现设备无关的GPIO管理,并向上暴露两类主流用户接口:基于sysfs的传统接口(已标记为deprecated)和基于/dev/gpiochipN的字符设备接口。

sysfs接口的运作本质

访问/sys/class/gpio/gpioX/value时,内核触发gpio_value_store(),最终调用对应chip的.get().set()回调,经由gpio_chip->parent关联到SOC GPIO控制器驱动。

/dev/gpiomem的内存映射机制

该设备节点提供非特权直接访问GPIO寄存器物理地址的能力(仅限Raspberry Pi等平台),绕过内核GPIO子系统:

// 用户空间mmap示例(需root或gpiomem组权限)
int fd = open("/dev/gpiomem", O_RDWR | O_SYNC);
void *gpio_map = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x20200000);
// 地址0x20200000为BCM2835 GPIO基址;PROT_WRITE允许写控制寄存器

逻辑分析:/dev/gpiomem本质是mem设备的受限子集,仅映射GPIO外设物理页;O_SYNC确保写操作立即生效,避免CPU写缓存导致状态不一致;MAP_SHARED使修改对硬件寄存器可见。

接口类型 安全性 实时性 内核介入 适用场景
sysfs 全量 调试、低频控制
/dev/gpiochipN 驱动级 生产环境标准API
/dev/gpiomem 性能敏感裸寄存器操作
graph TD
    A[用户空间应用] -->|open/write/read| B[/sys/class/gpio/]
    A -->|ioctl/mmap| C[/dev/gpiochipN]
    A -->|mmap| D[/dev/gpiomem]
    B --> E[gpiolib sysfs ops]
    C --> F[gpiolib chardev ops]
    D --> G[mem device → physical GPIO region]
    E & F --> H[GPIO Controller Driver]
    G --> I[直接写SOC GPIO registers]

2.3 基于periph.io与gobot双框架选型对比与性能实测

核心差异定位

periph.io 专注底层硬件抽象与零依赖驱动,gobot 侧重跨平台机器人应用层编排,二者设计哲学迥异。

GPIO翻转延迟实测(Raspberry Pi 4B)

框架 平均延迟(μs) 内存占用(MB) 驱动加载耗时(ms)
periph.io 8.2 14.6 23
gobot 47.9 38.1 156

典型初始化代码对比

// periph.io:直接映射寄存器,无运行时开销
p, _ := gpio.OpenPin("GPIO17")
_ = p.Out(gpio.High)

▶ 逻辑分析:OpenPin 返回裸 Pin 接口,Out() 调用经 mmio 直写 BCM2711 GPIO 寄存器;参数 gpio.High 是编译期常量(值为0x1),无反射或调度。

// gobot:经事件总线与适配器抽象层
bot := gobot.NewRobot("rpi",
  gobot.WithAdaptor(raspi.NewAdaptor()),
  gobot.WithDevices(gpio.NewLedDriver("led", "17")),
)
bot.Start()

▶ 逻辑分析:NewAdaptor 构建带 goroutine 调度的 Adaptor 实例;NewLedDriver 注册设备到内部 map 并启动状态监听协程;Start() 触发全链路事件循环——带来可观延迟。

数据同步机制

periph.io 采用内存映射+原子操作实现无锁 GPIO 控制;gobot 依赖 channel + mutex 保障多设备并发安全。

graph TD
  A[用户调用] --> B{periph.io}
  A --> C{gobot}
  B --> D[寄存器直写]
  C --> E[EventBus.Publish]
  E --> F[Adapter.Handle]
  F --> G[Syscall.Write]

2.4 实时性保障:goroutine调度、信号处理与中断响应延迟优化

goroutine 调度延迟压测对比

场景 平均调度延迟(μs) P99 延迟(μs) 是否启用 GOMAXPROCS=1
默认配置(8核) 124 486
单 OS 线程绑定 38 92
runtime.LockOSThread() + GOMAXPROCS=1 21 57

信号处理零拷贝注册

// 将 SIGUSR1 直接映射至专用 goroutine,绕过 runtime 信号转发链路
func setupRealtimeSignal() {
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGUSR1)
    go func() {
        for range sig { // 零分配接收,无 GC 压力
            atomic.StoreUint64(&rtFlag, 1) // 原子标记,避免锁竞争
        }
    }()
}

逻辑分析:signal.Notify 在底层调用 sigfillsetrt_sigprocmask 直接注册内核信号掩码;chan 容量为 1 确保不丢信号,atomic.StoreUint64 替代 mutex 实现纳秒级状态同步。

中断响应关键路径优化

graph TD
    A[硬件中断触发] --> B[内核 IRQ handler]
    B --> C{是否标记为实时中断?}
    C -->|是| D[跳过 softirq 队列,直调 kthread]
    C -->|否| E[走标准 softirq 延迟处理]
    D --> F[唤醒绑定 CPU 的 G-P-M]
    F --> G[goroutine 执行硬实时逻辑]

2.5 安全GPIO操作:权限控制、内存映射保护与硬件级防误触策略

安全GPIO操作需从软件权限、内存隔离与硬件响应三层面协同防御。

权限控制:基于Linux capabilities的细粒度隔离

// 仅允许CAP_SYS_RAWIO能力进程访问GPIO寄存器
if (!capable(CAP_SYS_RAWIO)) {
    return -EPERM; // 拒绝无权访问
}

CAP_SYS_RAWIO限制直接I/O端口和内存映射设备访问,避免普通用户进程绕过驱动层操作GPIO基地址。

内存映射保护机制

保护层级 实现方式 生效范围
MMU页表 设置PTE为只读/非执行 用户空间映射区
SMMU/IOMMU 设备直通DMA地址白名单 外设DMA请求

硬件级防误触:双触发确认电路

graph TD
    A[GPIO写请求] --> B{电平持续≥20ms?}
    B -->|否| C[丢弃]
    B -->|是| D[检查确认引脚高电平]
    D -->|匹配| E[执行写入]
    D -->|不匹配| F[触发中断并锁存状态]

该设计强制物理时序+逻辑双校验,杜绝静电抖动或软件瞬态误写。

第三章:Raspberry Pi硬件接口深度实践

3.1 BCM2711/BCM2837引脚复用(ALT功能)与PWM/UART/SPI/I2C模式切换实操

Raspberry Pi 4B(BCM2711)与Pi 3B+(BCM2837)共用同一套GPIO ALT功能寄存器架构,引脚功能由GPFSEL(GPIO Function Select)寄存器动态配置。

ALT功能映射机制

每个GPIO引脚对应3位GPFSELn字段,取值0–7决定ALT0–ALT5及输入/输出模式。例如GPIO14默认为UART0_TX(ALT0),但可重配为I2C1_SDA(ALT3)或PCM_CLK(ALT5)。

模式切换关键步骤

  • 禁用当前外设驱动(如sudo systemctl stop serial-getty@ttyS0.service
  • 修改/boot/config.txt启用目标接口:
    # 启用硬件I2C并禁用UART0占用GPIO14/15
    dtparam=i2c_arm=on
    dtoverlay=disable-bt  # 释放UART0引脚

常用ALT功能对照表(节选)

GPIO ALT0 ALT3 ALT5
14 UART0_TX I2C1_SDA PCM_CLK
18 PWM0 SPI0_CE0_N

PWM输出实操示例(sysfs接口)

# 将GPIO18设为ALT5(PWM0),需先配置pinmux
echo 18 > /sys/class/pwm/pwmchip0/export
echo 1000000 > /sys/class/pwm/pwmchip0/pwm0/period      # 1MHz周期
echo 500000  > /sys/class/pwm/pwmchip0/pwm0/duty_cycle   # 50%占空比
echo 1       > /sys/class/pwm/pwmchip0/pwm0/enable

逻辑说明:period单位为纳秒,duty_cycle必须≤periodpwmchip0对应PWM0(GPIO18/19),底层通过pwm-bcm2835驱动操作PWM控制寄存器。

3.2 模拟输入采样:ADC扩展方案(MCP3008)与Go驱动封装

MCP3008 是一款 8 通道、10 位分辨率的 SPI 接口 ADC,常用于树莓派等嵌入式平台扩展模拟信号采集能力。

硬件连接要点

  • VDD / VREF 接 3.3V(决定满量程电压)
  • CLK、DOUT、DIN、CS 引脚严格对应主控 SPI 总线
  • 通道 0–7 支持单端或差分输入模式

Go 驱动核心逻辑

func (d *MCP3008) Read(channel uint8) (uint16, error) {
    cmd := []byte{0x01, byte(0x80 | (channel << 4)), 0x00}
    if err := d.spi.Tx(cmd, cmd); err != nil {
        return 0, err
    }
    return uint16(cmd[1]&0x03)<<8 | uint16(cmd[2]), nil // 高2位+低8位拼接
}

cmd[1]&0x03 提取响应中的高两位有效位,<<8 左移构成 10 位结果高位;cmd[2] 为完整低八位。SPI 三字节事务完成一次采样。

参数 说明
分辨率 10-bit 输出范围 0–1023
采样速率 ~200 kSPS 单通道理论最大值
接口协议 SPI Mode 0 CPOL=0, CPHA=0
graph TD
    A[Go 应用调用 Read] --> B[构造SPI命令帧]
    B --> C[硬件SPI传输3字节]
    C --> D[解析MCP3008响应]
    D --> E[组合10位ADC值]

3.3 外设驱动开发范式:从设备树覆盖(dtbo)到用户空间驱动注册流程

设备树覆盖(dtbo)的构建与加载

使用 dtc 编译覆盖片段:

// myspi.dtso
/dts-v1/;
/plugin/;
/ {
    fragment@0 {
        target = <&spi0>;
        __overlay__ {
            status = "okay";
            spidev@0 {
                compatible = "spidev";
                reg = <0>;
                spi-max-frequency = <1000000>;
            };
        };
    };
};

该片段启用 spi0 并挂载 spidev 用户态节点;reg = <0> 指定片选0,spi-max-frequency 限定通信速率上限,避免硬件超频。

用户空间驱动注册流程

Linux 5.10+ 支持 libgpiod + uapi 驱动注册:

  • 加载 dtbo → 内核解析生成 platform_device
  • 用户态通过 ioctl(DEVICETREE_OVERLAY_CREATE) 注入
  • /sys/bus/platform/drivers/xxx/bind 触发 probe
阶段 触发方式 关键接口
覆盖加载 fdt_overlay_apply() CONFIG_OF_OVERLAY=y
设备匹配 of_match_table compatible 字符串匹配
驱动绑定 driver_register() module_platform_driver()
graph TD
    A[dtbo 文件] --> B[dtc 编译为 .dtbo]
    B --> C[内核 overlay_apply]
    C --> D[生成 platform_device]
    D --> E[用户空间 open /dev/spidev0.0]

第四章:12个可直接运行的物联网边缘节点工程详解

4.1 LED呼吸灯(PWM调光)与状态机驱动模型实现

LED呼吸灯本质是通过PWM占空比周期性变化模拟亮度渐变,核心在于时间精度控制状态有序切换

状态机设计要点

  • IDLEFADE_INFULL_ONFADE_OUTFULL_OFF
  • 每个状态维持固定时长,由系统滴答定时器触发迁移
  • 状态转移条件封装为纯函数,无副作用

PWM参数配置表

参数 说明
基频 1 kHz 避免人眼可见闪烁
分辨率 8 bit 256级亮度,满足平滑过渡
周期步进间隔 10 ms 决定呼吸节奏快慢
// 呼吸灯状态机主循环(简化版)
void led_breath_task(void) {
    static uint8_t brightness = 0;
    static uint8_t step = 0;
    static led_state_t state = FADE_IN;

    switch(state) {
        case FADE_IN:   if (++brightness >= 255) { state = FULL_ON; } break;
        case FULL_ON:   if (++step >= 50) { state = FADE_OUT; step = 0; } break;
        case FADE_OUT:  if (--brightness == 0) { state = FULL_OFF; } break;
        case FULL_OFF:  if (++step >= 50) { state = FADE_IN; step = 0; brightness = 0; }
    }
    pwm_set_duty(LED_CH, brightness); // 更新PWM占空比
}

逻辑分析:brightness 直接映射PWM占空比(0–255),step 用于延时计数;状态跳转不依赖全局时钟中断,而是由任务轮询驱动,兼顾实时性与可移植性。所有状态迁移均满足原子性与确定性约束。

graph TD
    A[IDLE] --> B[FADE_IN]
    B --> C[FULL_ON]
    C --> D[FADE_OUT]
    D --> E[FULL_OFF]
    E --> B

4.2 DHT22温湿度采集+本地Web服务(Go HTTP Server + Prometheus指标暴露)

硬件与驱动基础

DHT22通过单总线协议通信,需精确时序控制。Linux下推荐使用 periph.io 库绕过内核限制,避免 w1-gpio 驱动的采样抖动。

Go服务核心结构

func main() {
    http.Handle("/metrics", promhttp.Handler()) // 暴露标准Prometheus端点
    http.HandleFunc("/read", func(w http.ResponseWriter, r *request.Request) {
        temp, hum, err := dht22.Read(context.Background(), "GPIO4")
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        tempGauge.Set(temp)
        humGauge.Set(hum)
        fmt.Fprintf(w, "temp=%.2f°C, hum=%.1f%%", temp, hum)
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

逻辑说明:dht22.Read() 封装了微秒级脉冲采样与校验;tempGauge/humGaugepromauto.NewGauge() 注册的指标,自动绑定默认注册器;/read 接口同步触发传感器读取,兼顾调试与指标更新。

关键指标定义

指标名 类型 含义 单位
dht22_temperature_celsius Gauge 当前摄氏温度 °C
dht22_humidity_percent Gauge 当前相对湿度 %

数据流概览

graph TD
    A[DHT22 Sensor] -->|1-wire, ~2ms pulse| B(Go Worker Goroutine)
    B --> C[Parse & Validate CRC]
    C --> D[Update Prometheus Gauges]
    D --> E[/metrics HTTP Endpoint]
    E --> F[Prometheus Scraping]

4.3 HC-SR04超声波测距+阈值告警与MQTT异步上报

硬件信号时序协同

HC-SR04需严格遵循:触发端(Trig)≥10μs高电平脉冲,回响端(Echo)输出与距离成正比的高电平持续时间(t)。距离计算公式:distance_cm = t × 0.034 / 2(声速340 m/s → 0.034 cm/μs)。

异步告警逻辑

  • 距离 ≤ 阈值(如15 cm)时拉高LED并生成告警事件
  • 事件不阻塞主循环,交由FreeRTOS任务或ESP-IDF esp_event_post()分发

MQTT异步上报结构

// 使用ESP-IDF MQTT client异步发布(非阻塞)
mqtt_event_handler_t mqtt_handler = {
    .on_connected = on_mqtt_connected,
    .on_data = NULL,
};
esp_mqtt_client_publish(client, "sensor/dist", "{\"dist\":12.8,\"alert\":true}", 0, 1, 0);

逻辑分析:qos=1确保至少一次送达;retain=0避免旧数据滞留;payload为JSON格式,含原始距离值与告警标记。参数(keepalive)由连接配置统一管理。

字段 类型 说明
dist float 实测距离(cm),保留1位小数
alert bool 是否触发阈值告警
ts_ms int64 UTC毫秒时间戳(可选扩展)

数据同步机制

graph TD
    A[HC-SR04采样] --> B{距离≤阈值?}
    B -->|是| C[置alert=true<br>触发本地告警]
    B -->|否| D[置alert=false]
    C & D --> E[封装JSON→MQTT队列]
    E --> F[独立线程异步publish]

4.4 RFID-RC522门禁系统(SPI通信+扇区加密认证逻辑)

硬件连接与SPI初始化

RC522通过标准SPI接口与MCU通信:SCK→SCK,MOSI→MOSI,MISO→MISO,NSS→任意GPIO(需软件片选)。关键寄存器配置包括TModeReg(定时器使能)、TPrescalerReg(13.56MHz分频)和TReloadReg(防冲突超时)。

扇区认证流程

// 以扇区1(块4~7)为例,使用Key A认证
byte keyA[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
MFRC522::StatusCode status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 4, &keyA[0], &(mfrc522.uid));

此调用触发三次握手机制:① MCU发送认证指令与块地址;② RC522向卡片发起挑战(随机数NR);③ 卡片用密钥加密NR并返回应答AR;④ RC522本地运算比对。失败则锁死该扇区本次会话。

密钥与访问控制矩阵

块号 访问条件(Key A / Key B) 数据可读性 数据可写性
4 A/B均可认证
5 A认证后可写 ⚠️(需A权限)
6 B认证后可读 ⚠️(需B权限)
7(Trailer) A/B密钥+访问位存储 ✅(仅密钥域加密) ❌(仅可认证后改写)

认证状态机

graph TD
    A[上电复位] --> B[寻卡PICC_REQALL]
    B --> C{是否检测到卡片?}
    C -->|是| D[防冲突获取UID]
    C -->|否| B
    D --> E[选择卡片PICC_SEL_CL1]
    E --> F[扇区认证PCD_Authenticate]
    F --> G{认证成功?}
    G -->|是| H[读写数据块]
    G -->|否| I[报错并重试]

第五章:从边缘节点到云边协同架构演进路径

边缘节点的初始部署形态

某智能工厂在2021年首批部署了23台工业网关(型号:Siemens IOT2050),运行轻量级OpenYurt EdgeCore,仅承担PLC数据采集与本地阈值告警。所有原始时序数据(每秒47个测点×200ms采样)经MQTT直传至中心Kafka集群,未做任何边缘预处理。该阶段单节点CPU平均负载达82%,网络带宽峰值占用率达94%,导致3次产线停机事件——根本原因在于边缘仅作“管道”,未承担计算卸载职能。

云边资源割裂引发的运维困境

运维团队发现:云端训练的缺陷识别模型(YOLOv5s,12MB)每次OTA升级需耗时18分钟/节点,且因证书链不一致导致7台设备升级失败;同时,云端下发的策略规则(如“温度>85℃触发急停”)无法动态适配不同产线工艺参数。下表为典型问题统计:

问题类型 发生频次(月) 平均修复时长 根本原因
模型升级中断 4.2 47分钟 边缘存储空间不足+断网重试机制缺失
策略执行延迟 12.6 3.2秒 云端决策→边缘执行单向链路
日志同步丢失 8.3 不可追溯 边缘日志本地轮转策略与云端ELK不兼容

引入云边协同中间件的架构重构

2023年Q2,该工厂集成KubeEdge v1.12与自研EdgePolicyManager,关键改造包括:

  • 在边缘节点部署轻量级推理服务(TensorRT优化版ResNet18,模型体积压缩至3.2MB)
  • 通过EdgeSite组件实现策略双模下发:静态规则存于边缘ConfigMap,动态策略由云端Kubernetes CRD实时同步
  • 构建双向消息通道:边缘告警事件经EdgeHub优先推送至区域云(时延<150ms),非紧急日志则按策略聚合后批量上传
flowchart LR
    A[PLC传感器] --> B[边缘网关]
    B --> C{EdgePolicyManager}
    C -->|实时策略| D[云端K8s控制面]
    C -->|本地执行| E[TensorRT推理引擎]
    E -->|结构化结果| F[区域云边缘集群]
    F -->|模型增量更新| B

协同治理能力的实际落地效果

上线6个月后,产线异常响应时间从平均8.7秒降至210ms;模型OTA升级成功率提升至99.8%(失败节点自动回滚至前一稳定版本);边缘节点CPU负载均值稳定在31%±5%。更关键的是,当某条焊接产线更换新型号机器人后,运维人员仅需在云端修改CRD中的robot_type: KUKA_KR1000字段,边缘侧自动加载对应运动学补偿算法模块,全程无需现场工程师介入。

安全边界重构的实践挑战

采用SPIFFE/SPIRE实现零信任身份体系时,发现边缘设备TPM芯片固件版本碎片化严重(v2.1/v2.3/v3.0共存),导致12%的节点无法完成SVID签发。最终通过在边缘部署兼容层spire-compat-agent(含3个固件抽象接口),配合云端CA策略动态降级签名强度,使全网设备身份认证成功率从83%提升至99.4%。

擅长定位疑难杂症,用日志和 pprof 找出问题根源。

发表回复

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