RK3399 新设计工控机配置指南
用时两周,总算将新的 RK3399 工控机配置好了,期间遇到各种坑,在此记录一二。
RK3399 工控机硬件配置
首先看下硬件配置。
接口 | 数量 | 说明 |
---|---|---|
TypeC | 1个 | OTG 口,可用于烧录固件 |
USB3.0 | 4个 | |
USB2.0 | 2个 | |
RTC芯片 hym8563 | 1个 | 用于RTC时钟存储 |
SD | 1个 | 可插入SD/TF 卡 |
HDMI | 1个 | 可接显示器 |
以太网口 | 2个 | 1个千兆,1个百兆 |
串口 | 3个 | UART0, UART2, UART4, 其中UART2 作为console,波特率1500000 |
cp210x | 1个 | 对应 /dev/ttyUSB0 |
pwm | 1个 | 对应 pwm3 |
led | 2个 | 电源指示灯,状态指示灯 |
按键 | 2个 | 1个复位按键,1个recover按键 |
DC-12 电源 | 1个 | 12V直流电源,rk808 电源芯片 |
kernel 启动失败
首次编译SDK通过后,烧录固件至工控机,uboot正常执行,但是kernel执行3s左右停止。停止的位置固定,但是没有错误信息,感觉像是突然停止,停止前面是USB驱动加载信息,停止后不会自动重启。这个问题困扰我很久。
[ 1.963510] hub 4-0:1.0: USB hub found
[ 1.964024] hub 4-0:1.0: 1 port detected
[ 1.966154] dwmmc_rockchip fe310000.dwmmc: IDMAC supports 32-bit address mode.
[ 1.966842] dwmmc_rockchip fe310000.dwmmc: Using internal DMA controller.
[ 1.967451] dwmmc_rockchip fe310000.dwmmc: Version ID is 270a
[ 1.968004] dwmmc_rockchip fe310000.dwmmc: DW MMC controller at irq 25,32 bit host data width,256 deep fifo
[ 1.968898] dwmmc_rockchip fe310000.dwmmc: 'clock-freq-min-max' property was deprecated.
[ 1.969733] dwmmc_rockchip fe310000.dwmmc: No vmmc regulator found
[ 1.970282] dwmmc_rockchip fe310000.dwmmc: No vqmmc regulator found
[ 1.971302] dwmmc_rockchip fe310000.dwmmc: allocated mmc-pwrseq
[ 1.985263] mmc_host mmc1: Bus speed (slot 0) = 400000Hz (slot req 400000Hz, actual 400000HZ div = 0)
[ 1.999140] dwmmc_rockchip fe310000.dwmmc: 1 slots initialized
[ 2.001576] dwmmc_rockchip fe320000.dwmmc: IDMAC supports 32-bit address mode.
[ 2.002303] dwmmc_rockchip fe320000.dwmmc: Using internal DMA controller.
[ 2.002958] dwmmc_rockchip fe320000.dwmmc: Version ID is 270a
[ 2.003537] dwmmc_rockchip fe320000.dwmmc: DW MMC controller at irq 26,32 bit host data width,256 deep fifo
[ 2.004488] dwmmc_rockchip fe320000.dwmmc: 'clock-freq-min-max' property was deprecated.
[ 2.005456] dwmmc_rockchip fe320000.dwmmc: No vmmc regulator found
[ 2.006611] dwmmc_rockchip fe320000.dwmmc: allocated mmc-pwrseq
[ 2.007435] rockchip-iodomain ff770000.syscon:io-domains: Setting to 3300000 done
[ 2.008482] rockchip-iodomain ff770000.syscon:io-domains: Setting to 3300000 done
[ 2.022019] mmc_host mmc2: Bus speed (slot 0) = 400000Hz (slot req 400000Hz, actual 400000HZ div = 0)
[ 2.026205] mmc_host mmc1: Bus speed (slot 0) = 300000Hz (slot req 300000Hz, actual 300000HZ div = 0)
[ 2.035890] dwmmc_rockchip fe320000.dwmmc: 1 slots initialized
[ 2.040194] input: gpio-keys as /devices/platform/gpio-keys/input/input2
[ 2.041572] ==gsl_ts_init==
[ 2.041975] ret=0
[ 2.043363] rk808-rtc rk808-rtc: setting system clock to 2013-01-19 05:34:15 UTC (1358573655)
[ 2.066790] mmc_host mmc1: Bus speed (slot 0) = 200000Hz (slot req 200000Hz, actual 200000HZ div = 0)
[ 2.078907] u?
[ 2.111206] phy phy-ff770000.syscon:usb2-phy@e450.6: charger = USB_SDP_CHARGER
[ 2.115781] rockchip-dwc3 usb0: USB peripheral connected
[ 2.151998] usb 1-1.2: new full-speed USB device number 3 using ehci-platform
[ 2.175032] usb 5-1: new high-speed USB device number 2 using xhci-hcd
由于是新设计的板子,所以无法确定是硬件问题还是软件问题,假定硬件没问题,那就是软件配置问题,于是我逐一排查DTS中gpio的配置,比对工控机原理图,发现 SYR837
CPU 电源管理芯片的 gpio不匹配。原理图中配置如下:
CPU_B_SLEEP_H
对应管脚如下:
这里是GPIO1_C1, 在dts中表示为 GPIO1_17
, 实际dts中配置为 GPIO1_18
, 所以不匹配,我将io修改后还是不行,怀疑是不是SPI功能复用所致,所以也确保SPI功能都disabled
了,但是还是失败。
不过gpio修改后,发现log变了,kernel出现了 crash。log忘记保存了,根据crash信息,根据错误码排查到设备busy,说明有可能gpio被其它设备占用了,最后排查发现是 vcc3v3_pcie
节点也使用了 GPIO1_17
, got you! 把这个disabled掉就正常了!而且这个pcie电源也没有被其它节点引用,所以禁用也不影响其它功能。
小结: kernel 启动失败的原因是cpu电源芯片的gpio配置错误,同时正确gpio还被
vcc3v3_pcie
占用了,导致CPU供电异常,最后kernel停止运行。解决方案是修改syr837
gpio配置,禁用vcc3v3_pcie
--- a/arch/arm64/boot/dts/rockchip/rk3399-firefly-linux.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3399-firefly-linux.dts
@@ -239,6 +239,7 @@
vcc3v3_pcie: vcc3v3-pcie-regulator {
compatible = "regulator-fixed";
+ status = "disabled";
enable-active-high;
regulator-always-on;
regulator-boot-on;
@@ -447,7 +448,7 @@
regulator-min-microvolt = <712500>;
regulator-max-microvolt = <1500000>;
regulator-ramp-delay = <1000>;
- vsel-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;
+ vsel-gpios = <&gpio1 17 GPIO_ACTIVE_HIGH>;
fcs,suspend-voltage-selector = <1>;
regulator-always-on;
regulator-boot-on;
@@ -945,7 +946,7 @@
&rockchip_suspend {
rockchip,power-ctrl =
- <&gpio1 18 GPIO_ACTIVE_LOW>,
+ <&gpio1 17 GPIO_ACTIVE_LOW>,
<&gpio1 14 GPIO_ACTIVE_HIGH>;
};
USB 3.0 供电失败
kernel起来了,系统也跑起来了,接下来就是硬件接口配置了,首先测了测USB功能,使用串口设备插入USB接口,发现电源指示灯不亮,说明没有供电。根据原理图找到所有usb接口的电源使能管脚。
gpio | gpio1_num | io_num | description |
---|---|---|---|
GPIO1_B1 |
gpio1_9 | 41 | VCC5V0_USB3-1_EN |
GPIO1_B2 |
gpio1_10 | 42 | VCC5V0_USB3-2_EN |
GPIO1_B5 |
gpio1_13 | 45 | VCC5V0_USB3-3_EN |
GPIO1_C2 |
gpio1_18 | 50 | VCC5V0_USB3-4_EN |
然后通过 /sys/class/gpio 去使能。
#!/bin/bash
cd /sys/class/gpio
# usb3.0-1~4: 41 42 45 50
gpios=(41 42 45 50)
for io in ${gpios[@]}
do
echo $io > export
echo out > gpio$io/direction
echo 1 > gpio$io/value
done
发现每次写入 1 后,读出来还是0,说明IO的输出不受控制,于是我测试了 gpio1
的其它io,部分可控,部分不可控。
为此,我使用 io
指令,结合 RK3399
寄存器手册,查看 gpio 的复用情况和寄存器配置情况是否正常。
查询usb gpio复用功能
以 gpio1_Bx
接口为例,在 TRM
文档中搜索 gpio1b
, 找到 io 复用寄存器地址。
这里有几个关键信息, PMUGRF
将用于查找基地址,offset 0x00014
是 PMUGRF_GPIO1B_IOMUX
的偏移量。
那么如何查找基地址呢?其实文档的第一张图就是地址映射图 Address Mapping
。从中可以找到 PMUGRF
的基地址,当然也可以找到其它寄存器的基地址。
从图中知道基地址为 0xFF320000
, 加上偏移量 0x00014
就等于 0xFF320014
.
最后通过 io
指令去查询。
# iomux
# PMUGRF Base: 0xff320000
# PMUGRF_GPIO1B_IOMUX offset: 0x00014
# read iomux
root@firefly:~# io -4 -r 0xff320014
ff320014: 00008141
查询结果是 0x00008141
, 这个值该怎么读呢,这个是32位数据,对应gpio1_b0~gpio1_b7 共8个gpio的复用情况。每两个bit代表一个gpio。详情查看TRM文档中 PMUGRF_GPIO1B_IOMUX
寄存器说明。
对应 0x00008141
, 可以解析出每个gpio的复用情况。
# 0x8141: 10 00 00 01 01 00 00 01
# gpio1_B7~B0: i2c0pmu_scl, gpio, gpio, i2c4sensor_scl, i2c4sensor_sda, gpio, gpio, uart4m0_sout
# B7 B6 B5 B4 B3 B2 B1 B0
最终查询到USB 3.0 gpio1_b1
, gpio1_b2
, gpio1_b5
均为 GPIO
模式,没有被复用,说明配置正常。
查询USB gpio value
既然复用寄存器没有问题,那再来看看 gpio value配置,看看使用 sys/class/gpio/gpioxx/value
配置有没有生效。
与查询复用情况类似,首先找到 gpio1
的基地址。
基地址为 0xFF730000
, 接着查看gpio 数据寄存器 GPIO_SWPORTA_DR
的偏移量。
DR
寄存器偏移量为 0x0000
, 32位寄存器对应gpio1 32个gpio的数据。接下来使用 io
指令查询。
# gpio1 base: 0xff730000
# DR offset: 0x00000000
# DDR offset: 0x00000004
# read DR
root@firefly:~# io -4 -r 0xff730000
ff730000: 00042602
# 0x42602: 0x0100 0010 0110 0000 0010
# C0 B0 A0
# read DDR
root@firefly:~# io -4 -r 0xff730004
ff730004: 00062602
# 0x62602: 0x0110 0010 0110 0000 0010
# C0 B0 A0
# 说明 gpio 的 direction, value 均配置正常
以上查询到 DR
和 DDR
的配置情况,均正常。
USB 3.0 gpio 电源域配置
到此为止,io配置、io查询、复用功能确认都正常,但是实际输出就是不对。最后只能在 RK
redmine 系统中提交issues,根据他们提供的技术文档,发现io输出不受控的问题可能与电源域配置有关。
首先查看原理图,gpio1 的电源接口。
gpio1 使用 PMUIO2
电源,电压1.8v。然后查看 dts 中的配置。发现是 3.0v
, 果然不一样!
uboot-set = <RK3399_PMU1830_VDD_3V0>;
pmu1830-supply = <&vcc_3v0>;
既然问题找到了,那解决方案就很明显了。
解决方案
修改 pmu1830
电源域配置。修改后,USB3.0 供电一切正常。
--- a/arch/arm64/boot/dts/rockchip/rk3399-firefly-linux.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3399-firefly-linux.dts
@@ -866,8 +866,8 @@
* also list here for more convenient configuration:
* pmu1830-supply: RK3399_PMU1830_VDD_1V8 or RK3399_PMU1830_VDD_3V0
*/
- uboot-set = <RK3399_PMU1830_VDD_3V0>;
- pmu1830-supply = <&vcc_3v0>;
+ uboot-set = <RK3399_PMU1830_VDD_1V8>;
+ pmu1830-supply = <&vcc_1v8>;
};
&pinctrl {
USB 2.0 无法识别串口设备
USB3.0
问题是电源域的问题, USB2.0
则不太一样, USB2.0
供电正常,但是无法识别设备,而且不是所有接口都不能识别,两个接口,一个可以,一个不可以,但是它们共用一个usb hub,没道理会这样。最终由我老大发现是硬件问题,老大通过修改硬件电路解决了,perfect!
TypeC 接口配置
DTS默认的typec的电源使能接口 vbus-5v-gpios
配置与原理图中是不匹配的,需要更正一下。
--- a/arch/arm64/boot/dts/rockchip/rk3399-firefly-linux.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3399-firefly-linux.dts
@@ -739,7 +739,7 @@
pinctrl-names = "default";
pinctrl-0 = <&fusb0_int>;
int-n-gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>;
- vbus-5v-gpios = <&gpio2 0 GPIO_ACTIVE_HIGH>;
+ vbus-5v-gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>;
status = "okay";
};
以太网配置
工控机包含两个以太网,默认千兆网没有打开,需要使能 gmac
, 使能后,两个网口皆开,对应 eth0, eth1. 但是eth0 打开up失败。
root@firefly:~# ifconfig eth0 up
[ 128.132426] stmmac_open: Cannot attach to PHY (error: -19)
SIOCSIFFLAGS: No such device
最后排查是被其它设备占用io,禁用 uart1
, uart3
后正常。
brctl 失败
由于包含两个网口,可能会用到网桥功能,我安装 brctl
后却发现无法使用, 提示参数错误。使用 strace
排查。
root@firefly:~# brctl addbr br0
add bridge failed: Invalid argument
root@firefly:~# strace brctl addbr br0
execve("/sbin/brctl", ["brctl", "addbr", "br0"], [/* 18 vars */]) = 0
brk(0) = 0x766000
uname({sys="Linux", node="firefly", ...}) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7237000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=62149, ...}) = 0
mmap2(NULL, 62149, PROT_READ, MAP_PRIVATE, 3, 0) = 0xf720a000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/arm-linux-gnueabihf/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0(\0\1\0\0\0\215w\1\0004\0\0\0"..., 512) = 512
lseek(3, 908188, SEEK_SET) = 908188
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 2880) = 2880
lseek(3, 904740, SEEK_SET) = 904740
read(3, "A4\0\0\0aeabi\0\1*\0\0\0\0057-A\0\6\n\7A\10\1\t\2\n\3\f"..., 53) = 53
fstat64(3, {st_mode=S_IFREG|0755, st_size=911068, ...}) = 0
mmap2(NULL, 947624, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xf7122000
mprotect(0xf71fd000, 28672, PROT_NONE) = 0
mmap2(0xf7204000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xda000) = 0xf7204000
mmap2(0xf7207000, 9640, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xf7207000
close(3) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7236000
set_tls(0xf7236850, 0xf723a058, 0xf7236f38, 0xf7236850, 0xf723a058) = 0
mprotect(0xf7204000, 8192, PROT_READ) = 0
mprotect(0x14000, 4096, PROT_READ) = 0
mprotect(0xf7239000, 4096, PROT_READ) = 0
munmap(0xf720a000, 62149) = 0
socket(PF_LOCAL, SOCK_STREAM, 0) = 3
ioctl(3, SIOCBRADDBR, 0xffb29954) = -1 ENOPKG (Package not installed)
ioctl(3, SIOCSIFBR, 0xffb28848) = -1 EINVAL (Invalid argument)
write(2, "add bridge failed: Invalid argum"..., 36add bridge failed: Invalid argument
) = 36
exit_group(1) = ?
+++ exited with 1 +++
root@firefly:~#
定位到 ENOPKG
错误,提示 package not installed
socket(PF_LOCAL, SOCK_STREAM, 0) = 3
ioctl(3, SIOCBRADDBR, 0xffb29954) = -1 ENOPKG (Package not installed)
ioctl(3, SIOCSIFBR, 0xffb28848) = -1 EINVAL (Invalid argument)
搜索相关信息得知是内核配置不包含 CONFIG_BRIDGE
所致,添加该选项并重新编译内核后解决。 brctl
正常执行。
brctl addbr br0
brctl addif br0 eth0
brctl addif br0 eth1
ifconfig br0 192.168.64.20 up
添加 USB wifi 支持
由于工控机不自带wifi模块,可能会用到usb wifi网卡,所以需要添加驱动支持。
--- a/arch/arm64/configs/rockchip_linux_defconfig
+++ b/arch/arm64/configs/rockchip_linux_defconfig
@@ -173,12 +173,13 @@ CONFIG_USB_NET_CDC_MBIM=y
# CONFIG_USB_NET_ZAURUS is not set
CONFIG_LIBERTAS_THINFIRM=y
CONFIG_USB_NET_RNDIS_WLAN=y
+CONFIG_RTL8188EE=y
CONFIG_RTL8192CU=y
CONFIG_WL_ROCKCHIP=y
CONFIG_WIFI_BUILD_MODULE=y
CONFIG_WIFI_LOAD_DRIVER_WHEN_KERNEL_BOOTUP=y
CONFIG_AP6XXX=y
-CONFIG_RTL8188EU=m
+CONFIG_RTL8188EU=y
CONFIG_MWIFIEX=y
CONFIG_MWIFIEX_SDIO=y
CONFIG_LTE=y
添加状态指示灯
工控机包含两个led,其中一个电源指示灯,常亮,无需配置;另一个user led,需要修改dts支持。结合原理图找到对应管脚 gpio4_c2
。
--- a/arch/arm64/boot/dts/rockchip/rk3399-firefly-linux.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3399-firefly-linux.dts
@@ -152,6 +152,20 @@
};
};
+ leds {
+ status = "okay";
+ compatible = "gpio-leds";
+
+ user {
+ label = "led_ctl";
+ linux,default-trigger = "ir-user-click";
+ gpios = <&gpio4 18 GPIO_ACTIVE_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&led_user>;
+ default-state = "on";
+ };
+ };
+
rt5640-sound {
compatible = "simple-audio-card";
simple-audio-card,format = "i2s";
@@ -857,6 +871,12 @@
};
&pinctrl {
+ leds {
+ led_user: led-user {
+ rockchip,pins = <4 18 RK_FUNC_GPIO &pcfg_pull_up>;
+ };
+ };
+
buttons {
pwrbtn: pwrbtn {
rockchip,pins = <0 5 RK_FUNC_GPIO &pcfg_pull_up>;
使能 gpio-leds
驱动后,可以通过 /sys/class/leds/led_ctrl/brightness
文件修改亮灯状态,为了让其闪烁,可以添加开机启动脚本。
#!/bin/bash
ctl_file=/sys/class/leds/led_ctl/brightness
delay_sec=0.3
while true
do
echo 1 > $ctl_file
sleep $delay_sec
echo 0 > $ctl_file
sleep $delay_sec
done
RTC 配置
工控机外挂了 hym8563
RTC芯片,需要外接一个纽扣电池,并在 DTS
添加支持。
--- a/arch/arm64/boot/dts/rockchip/rk3399-firefly-linux.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3399-firefly-linux.dts
@@ -453,6 +453,16 @@
i2c-scl-falling-time-ns = <4>;
clock-frequency = <400000>;
+ hym8563: hym8563@51 {
+ compatible = "haoyu,hym8563";
+ reg = <0x51>;
+ #clock-cells = <0>;
+
+ clock-frequency = <32768>;
+ /* rtc_int is connected GPIO1_C6 */
+ };
+
vdd_cpu_b: syr837@40 {
compatible = "silergy,syr837";
reg = <0x40>;
添加后无法正常使用,修改时间后无法保存,最终发现是硬件问题。
PWM配置
根据原理图可知,工控机引出的 pwm
采用 pwm3a
, 所以dts
中需要enable pwm3
. 然后可以通过以下指令对 pwm
波进行配置。
cd /sys/class/pwm/pwmchip1
echo 0 > export
echo 50000 > pwm0/period
echo 25000 > pwm0/duty_cycle
echo 1 > pwm0/enable
其中 period
代表 pwm 波的周期,单位 ns
, duty_cycle
对应占空比,以上配置为 50%
.
注意,这里用的是
pwmchip1
, 而不是pwmchip0
, 因为pwmchip0
对应pwm2
, 而pwm2
被vdd_log
引用,用于电源电压管理。
HDMI 无法显示
最后来看个大问题,HDMI
无法显示,首先确保hdmi
, route_hdmi
, display_subsystem
等相关配置已经使能。使用 Firefly
固件编译是可以正常显示的,但是 RK
固件编译却不行。
一开始想着可能是io配置问题,但是禁用了其它外设后还是不行。不过既然 Firefly
的可以,那就可以对比了,为了确认是dts问题,还是内核驱动配置问题。我将 Firefly
的dts移植到 RK
固件,修改了部分选项后编译通过,而且 HDMI
正常显示了。这说明的确是 DTS
配置问题,与内核驱动配置无关。
接下来我将 firefly
的 dts
检查了一遍,禁用了其中的可疑选项后,HDMI
又不能显示了,这说明我禁用的选项与 HDMI
显示有关,于是我将这个改动也添加到 RK
的 dts
中。
vcca1v8_codec: LDO_REG7 {
regulator-always-on;
regulator-boot-on;
- regulator-min-microvolt = <1800000>;
- regulator-max-microvolt = <1800000>;
+ regulator-min-microvolt = <900000>;
+ regulator-max-microvolt = <900000>;
regulator-name = "vcca1v8_codec";
regulator-state-mem {
regulator-off-in-suspend;
也就是 rk808
电源管理芯片中的 vcca1v8_codec
配置,将默认的 1.8v
改为 0.9v
,至此,hdmi
正常显示。
总结
耗时两周左右的配置过程,让我遇到不少坑,以上总结的是其中的主要部分,还有很多小问题没有记录。总的来说,问题不少,但总算都解决了,也学到不少知识。
- 假定软件问题的情况下,优先排查
dts
配置,再看驱动配置 - 新设计的板子,可以将
gpio
配置列表整理,方便查阅 - 在dts中添加新设备时,可以参考当前dts外其它设备的配置信息,或者参考
Documents/devicetree/bindings/
中的文档 - 在出现io输出不可控的时候,优先考虑io复用及电源域配置问题
- 通过查询寄存器手册,结合
io
指令可以查看或者修改寄存器配置 - 在确保软件配置无误情况下,如果仍旧异常,考虑硬件问题
-
Firefly SDK
编译后始终无法启动,但RK SDK
中使用Firefly
的dts
却可以正常启动,所以优先使用RK
官方SDK
版权声明:本博客所有文章除特殊声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明出处 litreily的博客!