添加新系统到 OpenBMC
内容: 如何添加一个新的系统到 OpenBMC 版本
受众: 熟悉 OpenBMC 的开发者
需求: 完成了环境配置文档
总览
本文档将描述如下的内容:
- 回顾
Yocto与BitBake的历史 - 创建新的系统层
- 完善这个新的层
- 编译新的系统并使用
QEMU进行测试 - 为
sensor,LED,资产等内容添加配置
背景
OpenBMC 版本基于Yocto项目。Yocto 项目允许开发者创建定制化的 Linux 发行版本。 OpenBMC 使用 Yocto 创建自己的运行在各种设备尚的嵌入式 Linux 版本。
Yocto 具有一个层架构的概念。当你构建一个基于 Yocto 的构建版本时,你会定义关于该版本的一系列的层。OpenBMC 使用一些 Yocto 中通用的层以及自己构建的层。OpenBMC 中定义的层可以在 OpenBMC 的 GitHub项目的 meta-* 目录中找到。
Yocto 的层是定义在这个层中的构成这个层的不同包的组合。其中一个关键的层是BitBake使用的食谱。
BitBake 具有自身完备的功能性。在本文档中,我们将仅专注于添加一个新的系统过程中需要使用的 BitBake 内容。
启动 BitBake 初始化
你需要至少100GB的存储空间的开发环境,尽可能多的内存以及CPU核。第一次构建 OpenBMC 版本可能会消耗几个小时。一旦初次构建完成,未来的构建将会使用第一次构建中生成的缓存数据进行构建,从而大幅加速了后续的构建过程。
首先,跟着 Github 中的项目文档进行初次构建。
创建一个新的系统
如果上面的工作能够顺利完成,让我们开始创建我们的新系统吧。与前面的内容相同,我们将会使用 Romulus 作为我们的参考内容。我们新的系统将称为 romulus-prime。
在之前克隆的openbmc的仓库中,Romulus 等位于 meta-ibm/meta-romulus/ 目录下。 Romulus 层定义在 conf 子目录下。在 conf 目录中,可以看到如下的结构:
meta-ibm/meta-romulus/conf/
├── bblayers.conf.sample
├── conf-notes.txt
├── layer.conf
├── local.conf.sample
└── machine
└── romulus.conf
为了创建我们自己的 romulus-prime 系统,首先复制当前的 romulus 层:
cp -R meta-ibm/meta-romulus meta-ibm/meta-romulus-prime
让我们调整在新的层中需要的每个文件:
-
meta-ibm/meta-romulus-prime/conf/bblayers.conf.sample
这个文件定义了要引入到meta-romulus-prime版本中的层,你可以在这个文件中看到不同的Yocto层(比如meta,meta-openembedded/meta-oe等)。它也有OpenBMC的层,比如meta-phosphor,meta-openpower,meta-ibm以及meta-ibm/meta-romulus。对这个文件要做出的唯一修改是将其中的两个
meta-romulus实例,修改为meta-romulus-prime,这将允许你使用你新构建的层。 -
meta-ibm/meta-romulus-prime/conf/conf-notes.txt
这个文件简单陈述你构建的新层要构建的默认目标,这个文件保留不变,因为这个文件在所有的OpenBMC系统都保持一致。 -
meta-ibm/meta-romulus-prime/conf/layer.conf
这个文件的主要目的是告诉BitBake去哪里查找食谱(*.bb)文件,食谱文件以.bb作为后缀名,包含不同层的打包逻辑。.bbappend文件也是食谱文件,但是却是作为.bb文件的补充。.bbapped文件通常用来添加或移除相关联的.bb文件中的内容。 -
meta-ibm/meta-romulus-prime/conf/local.conf.sample
这个文件中包含你的层中的本地配置设置信息。这个文件中的内容写的很好,值得一读。唯一需要改变的内容是,将MACHINE修改为romulus-prime。 -
meta-ibm/meta-romulus-prime/conf/machine/romulus.conf
这个文件描述了你的机型的规定的内容,你定义使用的内核设备树,你引入的覆写特性,以及其他的系统特性。这个文件是创建新系统变动不同的内容时的好参考(内核设备树,MRW,LED设置,资产访问等)。
首先,你需要将这个文件重命名为romulus-prime.conf。
这个配置数据的主体数据不再使用了,但是在完全移除它之前,你依然需要提供这些内容。
构建新系统
上面的工作顺利完成后,再继续进行后续的操作。后面的操作过程中,可能会出现一些错误,但是这些错误可以帮助你更好的理解构建新系统的过程。
-
为当前的构建调整
conf
在shell中,你进行了bitbake的初始化操作,现在需要为你新构建的系统重置conf文件,你可以手动的复制新文件,或只是移除它,让BitBake帮助你完成:cd .. rm -rf ./build/conf export TEMPLATECONF=meta-ibm/meta-romulus-prime/conf . openbmc-env
运行你想运行的
bitbake命令。 -
没有 RPROVIDES 'romulus-prime-config'
这是你在新系统中运行bitbake obmc-phosphor-image之后出现的第一个错误。
在初始化OpenBMC的原型初始化时,会使用openbmc/skeleton仓库,在这个仓库中是 configs 目录。
既然这个仓库与文件是这样的情况,我们将简单地快速解决这个问题。
创建如下的配置文件:cp meta-ibm/meta-romulus-prime/recipes-phosphor/workbook/romulus-config.bb meta-ibm/meta-romulus-prime/recipes-phosphor/workbook/romulus-prime-config.bb vi meta-ibm/meta-romulus-prime/recipes-phosphor/workbook/romulus-prime-config.bb SUMMARY = "Romulus board wiring" DESCRIPTION = "Board wiring information for the Romulus OpenPOWER system." PR = "r1" inherit config-in-skeleton #Use Romulus config do_make_setup() { cp ${S}/Romulus.py \ ${S}/obmc_system_config.py cat <<EOF > ${S}/setup.py from distutils.core import setup setup(name='${BPN}', version='${PR}', py_modules=['obmc_system_config'], ) EOF }重新运行你的
bitbake命令。 -
提取
URL失败: file://romulus.cfg
这是内核需要的一个配置文件,在这里你可以放置一些额外的内核配置参数。在我们的需求中,只需要调整romulus-prime来使用romulus.cfg文件。我们只需要添加-prime来扩展路径。vi ./meta-ibm/meta-romulus-prime/recipes-kernel/linux/linux-aspeed_%.bbappend FILESEXTRAPATHS_prepend_romulus-prime := "${THISDIR}/${PN}:" SRC_URI += "file://romulus.cfg"现在重新运行你的 `bitbake 命令。
-
没有提供目标
arch/arm/boot/dts/aspeed-bmc-opp-romulus-prime.dtb'.dtb文件是设备树块文件。这个文件在Linux内核基于对应的.dts文件编译过程中生成的文件。当你引入一个新的OpenBMC系统时,你需要发送这些内核更新上游内容。链接邮件 [thread](https://lists.ozlabs.org/pipermail/openbmc/2018-September/013260.html) 是这个过程的例子。在本文档中,我们只简单的使用Romulus` 内核配置文件:vi ./meta-ibm/meta-romulus-prime/conf/machine/romulus-prime.conf # Replace the ${MACHINE} variable in the KERNEL_DEVICETREE # Use romulus device tree KERNEL_DEVICETREE = "${KMACHINE}-bmc-opp-romulus.dtb"重新运行你的
bitbake命令。
启动新系统
现在,你编译了你的新的系统镜像!有很多可以继续定制化的内容,但在现在,我们先验证一下我们的工作吧!
你的新镜像在编译完成后,它会位于相对于你的 bitbake 命令使用的目录于:
./tmp/deploy/images/romulus-prime/obmc-phosphor-image-romulus-prime.static.mtd
复制这个镜像到你配置的 QEMU 位置,重新运行启动 QEMU 命令(在 dev-environment.md 中的 qemu-system-arm 命令),并使用你的新的文件作为输入。
一旦启动,你将会看到如下的登录接口:
romulus-prime login:
好了!现在,你已经成功的实现了初步的创建、启动以及构建一个新的系统。这虽然在实际意义上并不是一个新的系统,但是你现在有了定制自己需要系统的基础。
深度客制化
在创建一个新系统时,有很多可以定制化的内容。
修改内核
本小节介绍如何修改内核,以移植 OpenBMC 到新的设备。设备树位于 linux/arch/arm/boot/dts at dev-4.13 · openbmc/linux · GitHub 中。比如,查看 aspeed-bmc-opp-romulus.dts 或相似的设备。完成下面的步骤来做出内核的修改:
- 添加新机型的设备树
- 描述
GPIOs,即LED,FSI,gpio-keys等内容。你应该可以从硬件原理图中获取到需要配置的信息 - 描述
i2c总线以及设备,通常包含不同的硬件检测hwmonsensor - 描述其他的设备,即
uarts,mac等内容 - 通常
flash布局不需要改变,只需要包含openbmc-flash-layout.dtsi
- 描述
- 调整
Makefile来编译设备树 - 参考 openbmc kernel doc 提交补丁到邮件列表
注意:
- 在
dev-4.10中,arch/arm/mach-aspeed/aspeed.c中具有通用的以及指定设备的代码,这个文件是用来通用的初始化,以及指定的机型的设置。在branchdev-4.13中,没有这样的初始化代码。大多数初始化是与时钟以及重置驱动中完成的 - 如果设备需要特定的配置(即 uart 路由),请发送邮件到邮件列表 the mailing list 进行讨论
工作簿
在旧版的 OpenBMC 中,有一个"工作簿"描述设备的服务、传感器以及FRU等信息。这个工作簿是一个 python 配置文件,且它被 skeleton 的其他服务使用。在最新版的 OpenBMC 中,skeleton 服务大多数被 phosphor-xxx 服务替代,因此skeleton 已经被抛弃了。但是在当前的构建中依旧需要"工作簿"。
meta-quanta是一个在 OpenBMC 树中定义的自有的配置示例,尽管是伪造的,它不再依赖于 skeleton。
在 e0e69be 中,或在 v2.4 标签之前,OpenPOWER 机型使用 GPIO 相关的一些配置,例如,在 Romulus.py 中,配置细节如下:
GPIO_CONFIG['BMC_POWER_UP'] = \
{'gpio_pin': 'D1', 'direction': 'out'}
GPIO_CONFIG['SYS_PWROK_BUFF'] = \
{'gpio_pin': 'D2', 'direction': 'in'}
GPIO_CONFIGS = {
'power_config' : {
'power_good_in' : 'SYS_PWROK_BUFF',
'power_up_outs' : [
('BMC_POWER_UP', True),
],
'reset_outs' : [
],
},
}
编译时需要 PowerUp 以及 PowerOK GPIOs ,来上电机箱并检测上电状态。
在这之后,GPIO 相关的配置从工作簿移除,并被 gpio_defs.json 替换,即 2a80da2 引入了对Romulus 的 GPIO json 配置。
{
"gpio_configs": {
"power_config": {
"power_good_in": "SYS_PWROK_BUFF",
"power_up_outs": [
{ "name": "SOFTWARE_PGOOD", "polarity": true},
{ "name": "BMC_POWER_UP", "polarity": true}
],
"reset_outs": [
]
}
},
"gpio_definitions": [
{
"name": "SOFTWARE_PGOOD",
"pin": "R1",
"direction": "out"
},
{
"name": "BMC_POWER_UP",
"pin": "D1",
"direction": "out"
},
...
}
每一个机型必须定义相似的 json 配置来描述 GPIO 配置。
硬件检测传感器
硬件检测传感器(hwmon sensors)包括板卡上的传感器(即温度传感器、风扇)以及 OCC 传感器。配置文件路径以及名称必须于设备树中的设备匹配。
在下面的文档中具有一些细节: doc/architecture/sensor-architecture。
现在让我们以 Romulus 为例,配置文件为 meta-romulus/recipes-phosphor/sensors,它包含板上传感器以及 OCC 传感器,其中板卡上的传感器通过 i2c 访问,OCC 传感器通过 FSI 访问。
-
w83773g@4c.conf 定义了
w83773温度传感器,包含三个温度LABEL_temp1 = "outlet" ... LABEL_temp2 = "inlet_cpu" ... LABEL_temp3 = "inlet_io"
这些设备定义为它的设备树 w83773g@4c 中,当
BMC启动时,udev规则将会启动phosphor-hwmon,它将基于sysfs属性创建在如下D-Bus温度传感器对象:/xyz/openbmc_project/sensors/temperature/outlet /xyz/openbmc_project/sensors/temperature/inlet_cpu /xyz/openbmc_project/sensors/temperature/inlet_io
-
pwm-tacho-controller@1e786000.conf 定义了风扇以及配置于上面类似,不同点是,它创建
fan_tach传感器 -
occ-hwmon.1.conf 为主
CPU定义了occ硬件检测传感器,这个配置有点不同,它必须告知phosphor-hwmon读取标识,而不是直接获取传感器的索引,因为CPU内核以及DIMMs可以是动态的,即CPU核可以被关闭,DIMMs可以被取出MODE_temp1 = "label" MODE_temp2 = "label" ... MODE_temp31 = "label" MODE_temp32 = "label" LABEL_temp91 = "p0_core0_temp" LABEL_temp92 = "p0_core1_temp" ... LABEL_temp33 = "dimm6_temp" LABEL_temp34 = "dimm7_temp" LABEL_power2 = "p0_power" ...
MODE_temp* = "label"表示,如果要查看tempX,必须读取sensor id即labelLABEL_temp* = "xxx"表示,通过sensor id获取到的sensor name- 比如,如果
temp1_input为 37000 且temp1_label在sysfs中为 91,phosphor-hwmon直到temp1_input是sensor id为 91 的值,即p0_core0_temp,因此它将创建xyz/openbmc_project/sensors/temperatur/p0_core0_temp,且其值为 37000 - 对于
Romulus功率传感器不需要读取标识,因为所有的功率在系统上是可以获取到的 - 对于
Witherspoon,功率传感器与温度传感器相似,它必须告知hwmon读取function_id而不是直接获取到传感器的索引值
LED灯
一些部分涉及到了LED灯。
- 在内核设备树中,必须描述
LEDs,如在 romulus dts 描述了3盏LED灯,故障灯、定位灯以及电源指示灯:leds { compatible = "gpio-leds"; fault { gpios = <&gpio ASPEED_GPIO(N, 2) GPIO_ACTIVE_LOW>; }; identify { gpios = <&gpio ASPEED_GPIO(N, 4) GPIO_ACTIVE_HIGH>; }; power { gpios = <&gpio ASPEED_GPIO(R, 5) GPIO_ACTIVE_LOW>; }; }; - 在机型层,
LEDs必须通过yaml进行配置,用以描述它们的功能,如在 Romulus led yaml 中:bmc_booted: power: Action: 'Blink' DutyOn: 50 Period: 1000 Priority: 'On' power_on: power: Action: 'On' DutyOn: 50 Period: 0 Priority: 'On' ...它表示LED电源指示灯在BMC启动后闪烁,当主机上电之后常量。 - 在运行时,
LED管理基于上面的yaml配置自动设置LEDs亮/灭/闪烁 LED可以通过/xyz/openbmc_project/led/手动的访问,比如:- 获取定位灯状态:
curl -b cjar -k https://$bmc/xyz/openbmc_project/led/physical/identify
- 设置定位灯状态为闪烁:
curl -b cjar -k -X PUT -H "Content-Type: application/json" -d '{"data": "xyz.openbmc_project.Led.Physical.Action.Blink" }' https://$bmc/xyz/openbmc_project/led/physical/identify/attr/State
- 获取定位灯状态:
- 当发生
FRU相关的故障时,将在日志中创建一条带有CALLOUT路径的事件日志,phosphor-fru-fault-monitor 检测日志:- 当带有
CALLOUT路径的故障发生时,修改相关灯的状态 - 当日志显式相关的事件解除或被删除时,修改相关灯的状态
- 当带有
注意: 这个 yaml 配置可以通过 phosphor-mrw-tools 的MRW自动生成,详情查看Witherspoon example
资产信息以及其他传感器
资产信息,其他传感器(比如 CPU/DIMM 温度),以及 FRU 在 ipmi 的 yaml 配置文件中定义。
比如,meta-romulus/recipes-phosphor/ipmi
romulus-ipmi-inventory-map定义了常规的资产信息,如,CPU,内存,母板等phosphor-ipmi-fru-properties定义了额外的资产信息属性phosphor-ipmi-sensor-inventory定义了 IPMI 的传感器romulus-ipmi-inventory-sel定义了 IPMI SEL 使用的资产
对于资产映射以及FRU属性,它们在不同的系统下都十分相似,你可以参考这些例子,并制作自己的系统。
对于 ipmi-sensor-inventory,IPMI 中的传感器因系统而异,因此你需要定义你自己的传感器,比如:
0x08: sensorType: 0x07 path: /org/open_power/control/occ0 ... 0x1e: sensorType: 0x0C path: /system/chassis/motherboard/dimm0 ... 0x22: sensorType: 0x07 path: /system/chassis/motherboard/cpu0/core0
第一个值 0x08,0x1e,0x22 是 IPMI 的 sensor id,在 MRW 中定义。你应该遵从系统的 MRW 来定义上面的配置。
注意: yaml配置可以自动从它的 MRW 的 phosphor-mrw-tools 中自动生成,详情查看 Witherspoon example
风扇
phosphor-fan-presence 管理所有的风扇有关的服务:
phosphor-fan-presence检查风扇是否在位,在资产信息中创建风扇D-Bus对象,并更新Present状态属性phosphor-fan-monitor检测风扇是否有效,并更新D-Bus对象中的Functional资产属性phosphor-fan-control按照条件设置风扇速度目标(如,基于温度)来设置风扇转速phosphor-cooling-type通过设置/xyz/openbmc_project/inventory/system/chassis对象属性,检测并设置系统是风冷还是水冷
所有上面的服务都是可配置的,比如通过 yaml 配置,因此在移植 OpenBMC 到新的系统时,机型相关的配置必须写入。
以 Romulus 为例,它是风冷且具有3个风扇,无 GPIO 在位检测的。
风扇在位
Romulus 没有 GPIO 在位检测,因此它通过检测风扇转速传感器:
- name: fan0
path: /system/chassis/motherboard/fan0
methods:
- type: tach
sensors:
- fan0
yaml配置表示:
- 它必须在资产中创建
/system/chassis/motherboard/fan0对象 - 它必须检测
fan0转速传感器 (/sensors/fan_tach/fan0) 来设置fan0对象的Present属性
风扇监测
Romulus 风扇使用 pwm 实现风扇速度控制,pwm 范围为 0-255,风扇速度为 0-7000。因此它需要一个一个机制将 pwm 转换为 speed
- inventory: /system/chassis/motherboard/fan0
allowed_out_of_range_time: 30
deviation: 15
num_sensors_nonfunc_for_fan_nonfunc: 1
sensors:
- name: fan0
has_target: true
target_interface: xyz.openbmc_project.Control.FanPwm
factor: 21
offset: 1600
这个 yaml 配置表示:
- 它必须使用
FanPwm作为转速传感器的目标接口 - 它必须以
target*21 + 1600计算期望的风扇速度 - 偏差是 15$,因此如果风扇速度在期望的工作范围外工作超过30秒,
fan0必将被设置为无效风扇
风扇控制
风扇控制服务需要4个 yaml 配置文件:
-
zone-condition定义制冷区条件,Romulus总是通过风冷制冷,因此配置比较简单,定义为air-cooled-chassis- name: air_cooled_chassis type: getProperty properties: - property: WaterCooled interface: xyz.openbmc_project.Inventory.Decorator.CoolingType path: /xyz/openbmc_project/inventory/system/chassis type: bool value: false -
zone-config定义制冷区,Romulus只有一个区zones: - zone: 0 full_speed: 255 default_floor: 195 increase_delay: 5 decrease_interval: 30它定义了风扇的满转速度以及默认地板速度,因此,全速状态下,
pwm将会设置为255;默认地板状态下,将会设置为195 -
fan-config定义了在哪个区需要控制哪个风扇,哪个目标接口必须使用,比如,下面的yaml配置中,定义了fan0必须在zone0进行控制,必须使用FanPwm接口:- inventory: /system/chassis/motherboard/fan0 cooling_zone: 0 sensors: - fan0 target_interface: xyz.openbmc_project.Control.FanPwm ... -
event-config定义了不同的事件,及事件处理。比如,在哪个温度必须设置哪个风扇。这个配置有一点复杂,example event yaml 提供了示例与文档,Romulus例子如下:- name: set_air_cooled_speed_boundaries_based_on_ambient groups: - name: zone0_ambient interface: xyz.openbmc_project.Sensor.Value property: name: Value type: int64_t matches: - name: propertiesChanged actions: - name: set_floor_from_average_sensor_value map: value: - 27000: 85 - 32000: 112 - 37000: 126 - 40000: 141 type: std::map<int64_t, uint64_t> - name: set_ceiling_from_average_sensor_value map: value: - 25000: 175 - 27000: 255 type: std::map<int64_t, uint64_t>在上面的
yaml配置中,定义了在zone0_ambient不同温度下的风扇的地板以及天花板速度,如
i. 当温度低于 27 摄氏度,地板速度为 85
ii. 当温度在 27-32 摄氏度之间,地板速度为 112注意:
Romulus风扇比较简单,如果想看复杂的例子,可以参考 Witherspoon fan configurations,在这个例子有如下的额外配置:- 通过 GPIO 监测在位信息
- 通过 GPIO 监测风冷还是水冷
- 具有更多的传感器及更多的事件
GPIOs
本小节主要关注于设备树中必须监控的 GPIOs,比如:
- 一个 GPIO 可能表示一个主机故障点信号(host checkstop)
- 一个 GPIO 可能表示一个按钮按下(button)
- 一个 GPIO 可能表示设备链接(device attach)
有 phosphor-gpio-presense 用来监测设备在位,phosphor-gpio-monitor 用于监测一个 GPIO。
设备树中的 GPIO
所有监测的 GPIOs 必须在设备树中进行描述,比如:
gpio-keys {
compatible = "gpio-keys";
checkstop {
label = "checkstop";
gpios = <&gpio ASPEED_GPIO(J, 2) GPIO_ACTIVE_LOW>;
linux,code = <ASPEED_GPIO(J, 2)>;
};
id-button {
label = "id-button";
gpios = <&gpio ASPEED_GPIO(Q, 7) GPIO_ACTIVE_LOW>;
linux,code = <ASPEED_GPIO(Q, 7)>;
};
};
如下的代码描述两个 GPIO 引脚,一个是 checkstop 另一个是 id-button,引脚代码来在aspeed-gpio.h:
#define ASPEED_GPIO_PORT_A 0 #define ASPEED_GPIO_PORT_B 1 ... #define ASPEED_GPIO_PORT_Y 24 #define ASPEED_GPIO_PORT_Z 25 #define ASPEED_GPIO_PORT_AA 26 ... #define ASPEED_GPIO(port, offset) \ ((ASPEED_GPIO_PORT_##port * 8) + offset)
GPIO 在位
Witherspoon 以及 Zaius 具有 GPIO 在位的例子:
-
INVENTORY=/system/chassis/motherboard/powersupply0 DEVPATH=/dev/input/by-path/platform-gpio-keys-event KEY=104 NAME=powersupply0 DRIVERS=/sys/bus/i2c/drivers/ibm-cffps,3-0069它监测 GPIO 引脚 104,作为
powersupply0的在位情况,创建资产对象,并绑定或解除绑定驱动 -
INVENTORY=/system/chassis/pcie_card_e2b DEVPATH=/dev/input/by-path/platform-gpio-keys-event KEY=39 NAME=pcie_card_e2b它监测 GPIO 引脚 39,作为
pcie_card_e2b的在位情况,创建资产目标
GPIO 监测
通常的 GPIO 监测用来监测主机的故障点事件,或按钮按下。
- checkstop monitor 是一个
OpenPOWER机型的常用服务DEVPATH=/dev/input/by-path/platform-gpio-keys-event KEY=74 POLARITY=1 TARGET=obmc-host-crash@0.target
默认情况下,它监测 GPIO 引脚 74,如果它触发,将告知systemd启动obmc-host-crash@0.target。
对于使用不同 GPIO 引脚作为故障监测点的,它简单在meat-machine层指定自己的配置文件,来覆盖原有的默认的配置即可。
比如 Zaius's checkstop config.
注意: 当引脚触发,phosphor-gpio-monitor启动目标并退出 - id-button monitor 是
Romulus中的服务,用来监测定位按钮按下DEVPATH=/dev/input/by-path/platform-gpio-keys-event KEY=135 POLARITY=1 TARGET=id-button-pressed.service EXTRA_ARGS=--continue
它监测 GPIO 引脚 135 按钮按下,并启动服务id-button-pressed.service,这个服务设置定位指示灯触发相应的Assert动作
注意: 它具有额外的参数--continue,告知phosphor-gpio-monitor按钮按下后,不要退出,继续工作
3093

被折叠的 条评论
为什么被折叠?



