第一章:Go嵌入式开发初探:树莓派与GPIO的相遇
Go语言凭借其静态编译、轻量协程和跨平台能力,正悄然成为嵌入式Linux设备(如树莓派)上高效控制硬件的新选择。与传统C或Python方案相比,Go生成的单二进制文件无需运行时依赖,极大简化了部署流程,特别适合资源受限但需稳定运行的边缘场景。
环境准备与交叉编译配置
在x86_64开发机上安装ARM64交叉编译工具链(如aarch64-linux-gnu-gcc),并设置Go构建环境变量:
export GOOS=linux
export GOARCH=arm64
export CGO_ENABLED=1
export CC=aarch64-linux-gnu-gcc
随后将编译产物(如gpio-blink)拷贝至树莓派(Raspberry Pi OS 64-bit),通过scp直接传输并赋予可执行权限。
GPIO控制原理与驱动支持
树莓派原生不提供用户态GPIO sysfs接口(自5.10内核起默认禁用),推荐使用libgpiod库——它通过/dev/gpiochip*字符设备安全访问硬件。Go生态中,periph.io/x/periph是成熟方案,它抽象了底层驱动差异,支持直接操作引脚电平与中断。
编写LED闪烁程序
以下代码使用periph库控制BCM引脚18(物理引脚12)驱动LED:
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引脚,配置为输出模式
pin := gpio.P18 // BCM 18
if err := pin.Out(gpio.High); err != nil {
log.Fatal(err)
}
// 1秒周期闪烁
for i := 0; i < 10; i++ {
pin.Set(gpio.Low) // LED亮(共阴接法)
time.Sleep(500 * time.Millisecond)
pin.Set(gpio.High) // LED灭
time.Sleep(500 * time.Millisecond)
}
}
注意:确保树莓派已启用
gpiomem权限(sudo usermod -a -G gpio $USER),且重启终端生效。
关键依赖与验证步骤
| 组件 | 验证命令 | 预期输出 |
|---|---|---|
| libgpiod | gpiodetect |
列出gpiochip0等设备 |
| periph库 | go get -u periph.io/x/periph/... |
无错误即成功 |
| 硬件连接 | 万用表测引脚电压 | 高/低电平切换明显 |
第二章:Go语言基础与树莓派环境搭建
2.1 Go语言语法精要与嵌入式编程特性分析
Go 语言凭借简洁语法、静态编译与无虚拟机依赖,天然适配资源受限的嵌入式场景。
内存模型与零拷贝通信
通道(chan)是 Goroutine 间安全通信的核心,避免锁竞争:
// 嵌入式传感器数据流:无缓冲通道确保同步采样
sensorCh := make(chan int, 0) // 零容量,发送即阻塞,强制生产-消费时序
go func() {
for i := 0; i < 5; i++ {
sensorCh <- readADC() // 阻塞直至接收方就绪
}
}()
val := <-sensorCh // 精确捕获单次采样值
逻辑分析:make(chan int, 0) 创建同步通道,readADC() 模拟硬件寄存器读取;发送端在接收方调用 <-sensorCh 前永久阻塞,保障时序确定性。参数 表示无缓冲区,消除内存分配开销。
关键嵌入式特性对比
| 特性 | 标准 Go 应用 | 嵌入式(如 TinyGo/ESP32) |
|---|---|---|
| 运行时依赖 | 全功能 runtime | 可裁剪(禁用 GC/反射) |
| 启动时间 | ~10ms | |
| 最小二进制体积 | ~2MB | -ldflags="-s -w") |
并发模型轻量化演进
graph TD
A[main goroutine] --> B[init hardware]
B --> C[spawn sensor reader]
C --> D[send via sync chan]
D --> E[ISR-safe handler]
2.2 树莓派系统配置与交叉编译环境实战
系统基础配置
首次启动后,执行以下命令更新系统并启用SSH:
sudo apt update && sudo apt full-upgrade -y # 升级内核与固件,确保兼容最新硬件
sudo systemctl enable ssh # 启用SSH服务,支持远程开发
sudo raspi-config # 进入图形化配置,启用I2C/UART等外设接口
full-upgrade 替代 upgrade 可处理依赖变更,避免交叉编译链因glibc版本错配导致链接失败。
交叉编译工具链部署
推荐使用官方预编译工具链(arm-linux-gnueabihf-):
| 工具链版本 | 适用架构 | 安装路径 |
|---|---|---|
| gcc 12.2.0 | ARMv7/ARMv8 | /opt/arm-linux-gnueabihf |
构建流程示意
graph TD
A[宿主机Linux] --> B[配置arm-linux-gnueabihf-gcc]
B --> C[编写Makefile指定CC=arm-linux-gnueabihf-gcc]
C --> D[编译生成arm可执行文件]
D --> E[SCP传输至树莓派运行]
2.3 GPIO硬件原理与Linux sysfs/gpiochip驱动模型解析
GPIO(General Purpose Input/Output)是SoC最基础的外设接口,其硬件本质为可编程的寄存器位:方向控制寄存器(DIR)、数据输入寄存器(IN)、数据输出寄存器(OUT)及上拉/下拉使能寄存器。
Linux内核自4.8起统一采用gpiochip抽象层,通过/sys/class/gpio/(传统sysfs)或更现代的/sys/bus/gpio/devices/gpiochipX/暴露接口。
GPIO芯片抽象模型
# 查看系统中所有gpiochip设备
ls /sys/bus/gpio/devices/
# 输出示例:gpiochip0 gpiochip128
该路径下每个gpiochipX代表一个GPIO控制器,X为基线号(base),可通过label和ngpio属性获知厂商标识与引脚总数。
核心属性表
| 属性 | 含义 | 示例值 |
|---|---|---|
label |
控制器名称 | pinctrl-bcm2835 |
ngpio |
可用GPIO总数 | 54 |
base |
起始全局编号 | |
设备绑定流程(mermaid)
graph TD
A[SoC Pinmux配置] --> B[GPIO Controller初始化]
B --> C[注册gpio_chip结构体]
C --> D[创建sysfs gpiochipX目录]
D --> E[用户空间通过gpiolib接口访问]
2.4 使用gobot库实现LED闪烁控制:从Hello World到时序优化
基础闪烁:Gobot版“Hello World”
package main
import (
"time"
"gobot.io/x/gobot"
"gobot.io/x/gobot/drivers/gpio"
"gobot.io/x/gobot/platforms/raspi" // 树莓派平台
)
func main() {
r := raspi.NewAdaptor()
led := gpio.NewLedDriver(r, "7") // GPIO7(BCM编号)
work := func() {
gobot.Every(1*time.Second, func() {
led.Toggle() // 电平翻转,周期≈2s
})
}
robot := gobot.NewRobot("led-robot", []gobot.Connection{r}, []gobot.Device{led}, work)
robot.Start()
}
逻辑分析:gpio.NewLedDriver(r, "7") 中 "7" 指 BCM 编号 GPIO7;gobot.Every(1s, ...) 触发翻转,实际亮/灭各1秒。Toggle() 封装了写高/低电平,避免手动状态管理。
时序优化路径对比
| 方案 | 周期精度 | CPU占用 | 实时性 | 适用场景 |
|---|---|---|---|---|
Every() 定时器 |
±50ms | 低 | 中 | 快速原型 |
time.Ticker |
±1ms | 极低 | 高 | 多设备同步闪烁 |
| PWM 硬件驱动 | 最低 | 最高 | 呼吸灯、调光 |
状态同步机制
为支持多LED相位错开,需共享计时基准:
ticker := time.NewTicker(500 * time.Millisecond)
go func() {
for t := range ticker.C {
// 统一时间戳驱动各LED相位偏移
led1.Write(t.UnixNano()%4000000000 < 2000000000) // 占空比50%,相位0
led2.Write(t.UnixNano()%4000000000 > 2000000000) // 相位180°
}
}()
逻辑分析:UnixNano() 提供纳秒级单调时钟,模运算实现循环相位;避免多个 Every() 造成漂移累积。
2.5 构建可复用的GPIO封装模块:接口抽象与错误处理实践
统一硬件抽象层(HAL)设计
将寄存器操作、时钟使能、模式配置等平台相关逻辑封装为 gpio_hal_init() 和 gpio_hal_write(),上层仅依赖 GpioPin 结构体与枚举状态。
错误传播策略
采用返回码(enum GpioStatus)替代全局错误变量,支持链式调用与上下文感知:
// 示例:带边界检查与状态回滚的引脚配置
GpioStatus gpio_setup(GpioPort port, uint8_t pin, GpioMode mode) {
if (pin >= GPIO_PIN_COUNT) return GPIO_ERR_INVALID_PIN; // 防御性校验
if (!clock_is_enabled(port)) return GPIO_ERR_CLOCK_OFF;
hal_configure(port, pin, mode);
return GPIO_OK;
}
逻辑分析:
pin >= GPIO_PIN_COUNT防止数组越界;clock_is_enabled()避免硬件未就绪导致静默失败;返回枚举值便于调用方区分GPIO_ERR_INVALID_PIN(参数错误)与GPIO_ERR_CLOCK_OFF(系统级依赖缺失)。
常见错误类型对照表
| 错误码 | 触发条件 | 恢复建议 |
|---|---|---|
GPIO_ERR_INVALID_PIN |
引脚编号超出芯片规格 | 校验设备树或BOM |
GPIO_ERR_PERIPH_BUSY |
复用功能被其他外设占用 | 重配AFIO映射 |
初始化流程(mermaid)
graph TD
A[调用gpio_init] --> B{端口时钟已使能?}
B -- 否 --> C[使能RCC时钟]
B -- 是 --> D[配置MODER/OTYPER/OSPEEDR]
C --> D
D --> E[返回GPIO_OK或具体错误码]
第三章:传感器接入与本地数据闭环控制
3.1 DHT22温湿度传感器驱动开发与校准实践
硬件时序约束解析
DHT22采用单总线异步通信,主机需严格控制电平持续时间:拉低至少1ms启动信号,随后释放总线并延时20–40μs等待传感器响应。时序偏差超±5μs即导致数据帧丢失。
初始化与数据读取代码
// DHT22单字节读取核心逻辑(基于STM32 HAL)
uint8_t dht22_read_byte(void) {
uint8_t byte = 0;
for (int i = 0; i < 8; i++) {
while (HAL_GPIO_ReadPin(DHT_GPIO, DHT_PIN) == GPIO_PIN_SET); // 等待低电平(50μs起始)
HAL_DelayMicroseconds(30); // 采样窗口中点
byte <<= 1;
if (HAL_GPIO_ReadPin(DHT_GPIO, DHT_PIN) == GPIO_PIN_SET) byte |= 1;
while (HAL_GPIO_ReadPin(DHT_GPIO, DHT_PIN) == GPIO_PIN_RESET); // 等待高电平结束
}
return byte;
}
该函数通过微秒级轮询捕获每个bit的高电平持续时长(>28μs为1,HAL_DelayMicroseconds(30)确保在数据位中部采样,规避边沿抖动;循环8次完成一字节解析。
校准参数对照表
| 参数 | 出厂标称值 | 实测偏移 | 推荐补偿公式 |
|---|---|---|---|
| 温度(℃) | ±0.5 | +0.8 | T_cal = T_raw - 0.8 |
| 湿度(%RH) | ±2% | -1.2% | H_cal = H_raw + 1.2 |
数据同步机制
使用环形缓冲区+双缓冲校验避免读写冲突:
- 主循环每2s触发一次采集,写入Buffer A;
- 应用层访问前校验CRC8,成功则原子交换指针至Buffer B供读取。
graph TD
A[主机拉低1ms] --> B[传感器响应80μs低+80μs高]
B --> C[40位数据帧:湿度整数/小数+温度整数/小数+CRC]
C --> D[CRC8校验失败→丢弃重采]
3.2 多通道ADC扩展与模拟信号采样精度提升策略
数据同步机制
多通道ADC需避免通道间时序偏移导致的相位失真。采用硬件触发同步采样(如STM32的EXTI+ADC注入组)可确保μs级对齐。
// 启用ADC1/ADC2同步模式(双ADC模式,规则同步)
ADC_MultiModeTypeDef multimode;
multimode.Mode = ADC_MODE_INDEPENDENT; // 改为 ADC_MODE_REGSIMULT_INJECSIMULT 提升同步性
multimode.DMAAccessMode = ADC_DMAACCESSMODE_DISABLED;
HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode);
逻辑分析:ADC_MODE_REGSIMULT_INJECSIMULT 启用双ADC规则+注入通道同步采样,消除通道间最大±1.5个ADC周期抖动;DMAAccessMode 设为禁用可避免DMA带宽竞争引入的延迟不确定性。
精度增强关键措施
- 使用外部高精度基准源(如REF5025,2.5V±2ppm/℃)替代内部基准
- 对每个通道实施独立校准(偏移+增益),存储于EEPROM
- 采样前执行通道切换延时(≥1.2μs)以稳定模拟多路复用器
| 措施 | 精度提升幅度 | 实施复杂度 |
|---|---|---|
| 外部基准源 | ±0.5 LSB | 中 |
| 单通道独立校准 | ±0.8 LSB | 高 |
| 输入缓冲驱动 | ±0.3 LSB | 低 |
graph TD
A[原始模拟信号] --> B[输入缓冲放大]
B --> C[多路复用器]
C --> D[同步采样触发]
D --> E[双ADC并行转换]
E --> F[数字滤波与校准补偿]
3.3 基于PID思想的本地风扇PWM调速闭环控制系统实现
为实现精准温控响应,系统采用单片机(如STM32F103)采集NTC热敏电阻电压,经ADC转换为温度值,并与设定目标温度构成偏差信号,驱动PWM输出调节直流风扇转速。
核心控制逻辑
- 误差 $ e(t) = T{\text{set}} – T{\text{meas}} $
- 输出占空比:$ u(t) = K_p e(t) + K_i \sum e(t) + K_d [e(t)-e(t-1)] $
- PWM频率固定为25 kHz(避开工频干扰且满足MOSFET开关特性)
PID参数整定参考(单位:℃/℃)
| 参数 | 初始值 | 物理意义 |
|---|---|---|
| $K_p$ | 8.0 | 响应速度与超调权衡 |
| $K_i$ | 0.15 | 消除静态温差 |
| $K_d$ | 2.2 | 抑制温度突变引起的抖动 |
// 简化版位置式PID计算(每100ms执行一次)
float pid_calculate(float setpoint, float measured) {
float error = setpoint - measured;
integral += error * 0.1f; // 采样周期0.1s
float derivative = (error - last_error) / 0.1f;
float output = Kp*error + Ki*integral + Kd*derivative;
last_error = error;
return constrain(output, 0.0f, 100.0f); // 映射为0–100%占空比
}
该函数将温度误差实时映射为PWM占空比;constrain()确保输出不越界,防止风扇启停震荡;积分项带限幅(未展开)可进一步避免饱和积分。
graph TD
A[温度传感器] --> B[ADC采样]
B --> C[误差计算]
C --> D[PID运算]
D --> E[PWM占空比输出]
E --> F[风扇驱动电路]
F --> A
第四章:网络化升级与工业协议集成
4.1 MQTT协议核心机制解析与eclipse-paho-go客户端深度应用
MQTT 的轻量、发布/订阅与 QoS 分层设计,使其成为 IoT 场景的通信基石。其核心依赖 TCP 连接、主题(Topic)路由、遗嘱消息(Last Will)及三种服务质量(QoS 0/1/2)保障机制。
QoS 行为对比
| QoS | 可靠性 | 重传机制 | 典型场景 |
|---|---|---|---|
| 0 | 最多一次 | 无 | 传感器心跳上报 |
| 1 | 至少一次 | PUBACK | 设备状态更新 |
| 2 | 恰好一次 | PUBREC/PUBREL/PUBCOMP | 固件升级确认 |
客户端连接与订阅示例
opts := paho.NewClientOptions().
AddBroker("tcp://broker.hivemq.com:1883").
SetClientID("go-client-001").
SetCleanSession(true).
SetAutoReconnect(true)
client := paho.NewClient(opts)
if token := client.Connect(); token.Wait() && token.Error() != nil {
log.Fatal(token.Error()) // 连接失败时 panic
}
SetCleanSession(true)启用无状态会话,避免服务端保留离线消息;SetAutoReconnect(true)启用指数退避重连,提升弱网鲁棒性。token.Wait()阻塞至连接完成或超时,是同步建连的关键同步点。
数据同步机制
graph TD A[客户端调用 Subscribe] –> B{Broker 路由匹配} B –> C[匹配主题的 QoS 策略] C –> D[下发消息 + QoS 协议握手] D –> E[客户端 ACK 响应链]
4.2 TLS安全连接配置与设备身份认证(Client Cert + CA)实战
在物联网边缘设备接入云平台时,仅靠服务器证书验证不足以防范中间人攻击。启用双向TLS(mTLS)可强制设备出示由可信CA签发的客户端证书,实现强身份绑定。
生成设备证书链
# 1. 使用私有CA签发设备专属证书(非自签名)
openssl x509 -req -in device.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out device.crt -days 365 -sha256 \
-extfile <(printf "subjectAltName=DNS:device-001,IP:192.168.1.100\nextendedKeyUsage=clientAuth")
-extfile 动态注入 SAN 和 clientAuth 扩展,确保证书仅用于客户端身份认证;-CAcreateserial 自动管理序列号,避免重复签发冲突。
Nginx mTLS 配置关键项
| 指令 | 值 | 说明 |
|---|---|---|
ssl_client_certificate |
/etc/nginx/ca.crt |
根CA公钥,用于验证设备证书链 |
ssl_verify_client |
on |
强制校验客户端证书存在性与有效性 |
ssl_verify_depth |
2 |
允许设备证书→中间CA→根CA 的两级链验证 |
认证流程
graph TD
A[设备发起TLS握手] --> B[发送client certificate]
B --> C[Nginx校验签名/有效期/SAN/吊销状态]
C --> D{验证通过?}
D -->|是| E[建立加密通道,透传CN/OU至后端]
D -->|否| F[返回400或495错误]
4.3 温控系统状态机设计与消息发布/订阅模式解耦实践
温控系统需在启动、运行、过热保护、故障停机等状态间可靠切换,同时避免控制逻辑与硬件驱动紧耦合。
状态机核心枚举定义
from enum import Enum
class ThermostatState(Enum):
IDLE = 0 # 待机,未通电
WARMING = 1 # 加热中(PID调节启用)
COOLING = 2 # 制冷中(风扇/压缩机激活)
OVERHEAT = 3 # 温度超阈值,强制切断加热
FAULT = 4 # 传感器失效或通信中断
该枚举明确边界语义,避免魔法数字;IDLE为初始态,OVERHEAT和FAULT为不可逆终态,保障安全兜底。
消息主题拓扑表
| 主题(Topic) | 发布者 | 订阅者 | QoS |
|---|---|---|---|
temp/sensor/raw |
DS18B20驱动 | StateMachine、Logger | 1 |
thermostat/state |
StateMachine | UI、CloudGateway | 2 |
actuator/cmd/heater |
StateMachine | HeaterDriver | 1 |
状态跃迁与事件驱动流程
graph TD
A[IDLE] -->|temp > 25℃| B[WARMING]
B -->|temp ≥ 30℃| C[COOLING]
B -->|sensor_timeout| D[FAULT]
C -->|temp ≤ 26℃| B
C -->|temp ≥ 35℃| E[OVERHEAT]
E -->|manual_reset| A
状态变更通过mqtt.publish("thermostat/state", json.dumps({"state": "WARMING"}))广播,各模块按需响应,实现零耦合协同。
4.4 基于Go的轻量级Web API服务:实时监控页面与远程指令下发
核心路由设计
采用 gorilla/mux 实现语义化路由,分离监控数据流与控制通道:
r := mux.NewRouter()
r.HandleFunc("/api/v1/status", getStatusHandler).Methods("GET") // 实时状态快照
r.HandleFunc("/api/v1/commands", postCommandHandler).Methods("POST") // JSON指令下发
r.HandleFunc("/ws", serveWS).Methods("GET") // WebSocket长连接
getStatusHandler返回设备CPU、内存、网络延迟等指标(采样周期500ms);postCommandHandler解析{"cmd": "reboot", "target": "node-03"}结构,经签名校验后入队;serveWS支持前端订阅动态指标流。
指令安全管控机制
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
cmd |
string | 是 | 预定义指令(reboot/upgrade) |
nonce |
string | 是 | 一次性随机数(防重放) |
signature |
string | 是 | HMAC-SHA256(nonce+secret) |
数据同步机制
graph TD
A[前端WebSocket连接] --> B{心跳保活}
B --> C[服务端推送status事件]
C --> D[指令队列消费]
D --> E[异步执行并回写结果]
第五章:从课程设计到开源贡献:本科生嵌入式Go工程化之路
在浙江大学2023年《嵌入式系统设计》课程中,一支由三名大三学生组成的小组完成了名为“TinyGo-EdgeLogger”的毕业设计项目:一款基于ESP32-C3芯片、使用TinyGo编写的低功耗边缘日志采集器。该设备通过I²C连接BME280传感器,每30秒采集温湿度与气压数据,并通过Wi-Fi将结构化JSON日志推送到本地MQTT代理;当网络中断时,自动启用SPI Flash(Winbond W25Q32)进行环形缓冲存储,恢复后断点续传——整个固件体积严格控制在142 KB以内,符合ESP32-C3的Flash分区限制。
工程化起点:从Makefile到CI流水线
团队摒弃了课程初期的手动tinygo flash命令,转而构建标准化构建脚本:
BOARD ?= esp32-c3-devkitm-1
FLASH_ADDR ?= 0x0
build:
tinygo build -o build/firmware.bin -target=$(BOARD) -ldflags="-s -w" ./main.go
flash:
tinygo flash -target=$(BOARD) -ldflags="-s -w" ./main.go
随后接入GitHub Actions,在.github/workflows/ci.yml中定义交叉编译验证、静态分析(golangci-lint)、二进制尺寸检查(size build/firmware.bin阈值告警)三阶段流水线,确保每次PR提交均通过全链路验证。
开源协同:向TinyGo官方仓库提交PR
项目运行两周后,团队发现TinyGo对ESP32-C3的SPI Flash驱动缺少EraseSector原子操作支持,导致环形缓冲区擦除异常。他们复现问题、定位至src/machine/machine_esp32c3.go,补全了EraseSector方法并添加对应测试用例。2023年11月17日,PR #4289被合并进主干,成为TinyGo v0.33.0正式特性之一。以下是关键补丁片段:
// EraseSector erases one sector (4KB) starting at address.
func (d *Flash) EraseSector(addr uint32) error {
// ESP32-C3 ROM API call sequence with cache invalidation
return d.eraseSectorImpl(addr)
}
硬件抽象层重构实践
为提升可移植性,团队将硬件依赖解耦为接口契约:
| 接口名称 | 实现目标 | 当前适配平台 |
|---|---|---|
SensorReader |
统一读取环境传感器数据 | BME280/I²C |
StorageBackend |
支持Flash/Memory双后端 | W25Q32/SRAM |
NetworkClient |
MQTT连接与重连策略 | ESP-IDF Wi-Fi |
所有接口均通过config.yaml动态注入具体实现,使同一套业务逻辑无缝切换开发板型号——后续成功将固件迁移到Raspberry Pi Pico W仅需替换3个实现文件。
社区反馈驱动的迭代闭环
项目发布至GitHub后,收到德国慕尼黑工业大学嵌入式实验室提出的内存碎片优化建议。团队据此引入sync.Pool管理JSON序列化缓冲区,并在runtime.MemStats监控下将GC暂停时间从平均8.2ms降至1.3ms。这一改进被收录进TinyGo社区最佳实践文档第7节“实时约束下的内存池模式”。
教学反哺:课程实验包升级
该成果已反向集成进课程实验体系:新版Lab5“边缘日志系统”要求学生基于tinygo-edge-logger模板完成自定义传感器接入(如MAX30102心率模块),并强制提交包含go.mod依赖锁定、testdata/模拟传感器响应、docs/architecture.md架构图(Mermaid生成)的完整PR。
flowchart LR
A[main.go] --> B[SensorReader]
A --> C[StorageBackend]
A --> D[NetworkClient]
B --> E[BME280 I²C Driver]
C --> F[W25Q32 SPI Driver]
D --> G[ESP32-C3 WiFi Stack]
课程设计不再止步于功能实现,而是贯穿需求分析、跨平台抽象、CI/CD集成、上游贡献、性能调优与文档沉淀的全生命周期工程训练。
