ubuntu20.04 快发布了,作为一个爱折腾新系统的人,自然要使用最新的系统了。
这里就发帖记录一下从头制作Licheepi V3s Zero的SPI Flash系统镜像(32M)的过程,把每个步骤的个人理解和疑惑记录下来,给我一样的新手作为参考。
(PS:ubuntu下需要的工具软件安装就不记录了,以下过程如果运行报错,可能是某个工具没有安装,直接sudo apt install xxx就可以了。)
内容目录大致包括:
1. sunxi-fel
2. uboot (2017.01)
3. Linux kernel(使用的5.2)
4. rootfs制作(使用的buildroot 2020.02)
5. 烧录镜像制作
6. 后记
离线
Sunxi-tools的安装
1. 依赖包
$ apt-get install libusb-1.0-0-dev
2. clone代码包
官网给出的地址如下
git clone https://github.com/linux-sunxi/sunxi-tools
但以上地址,V3s上实验发现不支持SPI Flash,应从以下地址下载
git clone https://github.com/Icenowy/sunxi-tools.git -b spi-rebase
注意使用spi-rebase分支
3. 支持32M的SPI Flash
由于SPI flash 的地址是24bit,也就是最大16M 地址空间,所以对于32M flash,需要增加bank切换支持。
uboot中有CONFIG_SPI_FLASH_BAR选项可以使能bank切换。
但是sunxi-fel中尚未支持,所以下载的时候超出16M会循环覆盖掉。
这里介绍对sunxi-fel增加16M以上flash支持的方法。
Filename: fel-spiflash.c
/*
* Write data to the SPI flash. Use the first 4KiB of SRAM as the data buffer.
*/
#define CMD_WRITE_ENABLE 0x06
#define SPI_FLASH_16MB_BOUN 0x1000000
# define CMD_BANKADDR_BRWR 0x17 //only SPANSION flash use it
# define CMD_BANKADDR_BRRD 0x16
# define CMD_EXTNADDR_WREAR 0xC5
# define CMD_EXTNADDR_RDEAR 0xC8
size_t bank_curr = 0;
void aw_fel_spiflash_write_helper(feldev_handle *dev,
uint32_t offset, void *buf, size_t len,
size_t erase_size, uint8_t erase_cmd,
size_t program_size, uint8_t program_cmd)
{
uint8_t *buf8 = (uint8_t *)buf;
size_t max_chunk_size = dev->soc_info->scratch_addr - dev->soc_info->spl_addr;
size_t cmd_idx, bank_sel;
if (max_chunk_size > 0x1000)
max_chunk_size = 0x1000;
uint8_t *cmdbuf = malloc(max_chunk_size);
cmd_idx = 0;
prepare_spi_batch_data_transfer(dev, dev->soc_info->spl_addr);
//add bank support
{
cmd_idx = 0;
bank_sel = offset /SPI_FLASH_16MB_BOUN;
if (bank_sel == bank_curr)
goto bar_end;
/* Emit write enable command */
cmdbuf[cmd_idx++] = 0;
cmdbuf[cmd_idx++] = 1;
cmdbuf[cmd_idx++] = CMD_WRITE_ENABLE;
/* Emit write bank */
cmdbuf[cmd_idx++] = 0;
cmdbuf[cmd_idx++] = 2;
cmdbuf[cmd_idx++] = CMD_EXTNADDR_WREAR;
cmdbuf[cmd_idx++] = offset >> 24;
/* Emit wait for completion */
cmdbuf[cmd_idx++] = 0xFF;
cmdbuf[cmd_idx++] = 0xFF;
/* Emit the end marker */
cmdbuf[cmd_idx++] = 0;
cmdbuf[cmd_idx++] = 0;
aw_fel_write(dev, cmdbuf, dev->soc_info->spl_addr, cmd_idx);
aw_fel_remotefunc_execute(dev, NULL);
bar_end:
bank_curr = bank_sel;
}
cmd_idx = 0;
prepare_spi_batch_data_transfer(dev, dev->soc_info->spl_addr);
while (len > 0) {
while (len > 0 && max_chunk_size - cmd_idx > program_size + 64) {
if (offset % erase_size == 0) {
/* Emit write enable command */
cmdbuf[cmd_idx++] = 0;
cmdbuf[cmd_idx++] = 1;
cmdbuf[cmd_idx++] = CMD_WRITE_ENABLE;
/* Emit erase command */
cmdbuf[cmd_idx++] = 0;
cmdbuf[cmd_idx++] = 4;
cmdbuf[cmd_idx++] = erase_cmd;
cmdbuf[cmd_idx++] = offset >> 16;
cmdbuf[cmd_idx++] = offset >> 8;
cmdbuf[cmd_idx++] = offset;
/* Emit wait for completion */
cmdbuf[cmd_idx++] = 0xFF;
cmdbuf[cmd_idx++] = 0xFF;
}
/* Emit write enable command */
cmdbuf[cmd_idx++] = 0;
cmdbuf[cmd_idx++] = 1;
cmdbuf[cmd_idx++] = CMD_WRITE_ENABLE;
/* Emit page program command */
size_t write_count = program_size;
if (write_count > len)
write_count = len;
cmdbuf[cmd_idx++] = (4 + write_count) >> 8;
cmdbuf[cmd_idx++] = 4 + write_count;
cmdbuf[cmd_idx++] = program_cmd;
cmdbuf[cmd_idx++] = offset >> 16;
cmdbuf[cmd_idx++] = offset >> 8;
cmdbuf[cmd_idx++] = offset;
memcpy(cmdbuf + cmd_idx, buf8, write_count);
cmd_idx += write_count;
buf8 += write_count;
len -= write_count;
offset += write_count;
/* Emit wait for completion */
cmdbuf[cmd_idx++] = 0xFF;
cmdbuf[cmd_idx++] = 0xFF;
}
/* Emit the end marker */
cmdbuf[cmd_idx++] = 0;
cmdbuf[cmd_idx++] = 0;
/* Flush */
aw_fel_write(dev, cmdbuf, dev->soc_info->spl_addr, cmd_idx);
aw_fel_remotefunc_execute(dev, NULL);
cmd_idx = 0;
}
free(cmdbuf);
}
4. 编译安装
$ sudo make
$ sudo make install
5. 烧录系统的方法
烧录整个flash:
$ sudo sunxi-fel -p spiflash-write 0 v3s_flash_32m.bin
独立烧录:
$ sudo sunxi-fel -p spiflash-write 0 u-boot-sunxi-with-spl.bin
$ sudo sunxi-fel -p spiflash-write 0x50000 whycan.bmp.gz
$ sudo sunxi-fel -p spiflash-write 0xF0000 sun8i-v3s-licheepi-zero-dock.dtb
$ sudo sunxi-fel -p spiflash-write 0x100000 zImage
$ sudo sunxi-fel -p spiflash-write 0x600000 jffs2.bin
读取flash
# 读取flash从0地址开始100个字节,读到read.bin文件
$ sudo sunxi-fel -p spiflash-read 0 0x100 read.bin
6. 如何查看是否进入FEL模式
方法一,查看是否检测到一个新的USB设备
在命令行输入lsusb,将看到一下输出
Bus 001 Device 074: ID 1f3a:efe8
关注设备号 1f3a:efe8
V3s看到的是如下信息
Bus 002 Device 005: ID 1f3a:efe8 Onda (unverified) V972 tablet in flashing mode
方法二,运行sunxi-fel工具
$ sudo sunxi-fel version
AWUSBFEX soc=00162500(A13) 00000001 ver=0001 44 08 scratchpad=00007e00 00000000 00000000
7. 如何进入FEL模式
7.1. 通过一个特殊的FEL 按钮
7.2. 按住standard button
7.3 通过串口
上电启动过程中,通过发送字符’1’(有的设备是’2’),采用该方法调用了Boot1代码。
在较新的SoCs,U-Boot支持一个 “efex” 命令,可以进入到FEL模式
如果不支持efex命令,可以使用 go命令
Go 0xffff0020
7.4 通过一个特殊的SD 卡映像。
官方的sunxi-tools仓库中,有一个小的SD卡启动镜像,其功能仅仅是跳到FEL
wget https://github.com/linux-sunxi/sunxi-tools/raw/master/bin/fel-sdboot.sunxi
dd if=fel-sdboot.sunxi of=/dev/sdX bs=1024 seek=8
7.5 没有找到有效的boot image
如果BROM没有找到有效的启动镜像,会自动进入FEL模式
离线
原理及分区规划
制作原理
一个系统镜像由BootLoader、Kernel Image、dtb file、Root Filesystem构成,所以制作一个系统镜像第一步就是要制作这几个文件。
-rw-r--r-- 1 xlee xlee 28311552 Apr 23 11:17 jffs2.bin 根文件系统制作的磁盘镜像
drwxr-xr-x 17 xlee xlee 4096 Apr 21 21:04 rootfs 根文件系统
-rw-rw-r-- 1 xlee xlee 11413 Apr 22 23:34 sun8i-v3s-licheepi-zero.dtb 设备树文件
-rw-rw-r-- 1 xlee xlee 418388 Apr 22 23:03 u-boot-sunxi-with-spl.bin uboot启动镜像
-rwxrwxr-x 1 xlee xlee 4315384 Apr 23 11:17 zImage Linux Kernel内核
分区规划
这个系统镜像最终是存储在SPI Flash上的,通常只有16M、32M的大小,需要仔细规划有限的存储空间。
# 总共32MiB 0x000000 - 0x1FFFFFF
uboot 0x0000 - 0x7FFFF (512KiB)
dtb 0x80000 - 0x8FFFF (64KiB)
zImage 0x90000 - 0x4FFFFF (4MiB+448Kib)
rootfs 0x500000 - 0x1FFFFFF (27MiB = 32MiB - 5MiB)
最近编辑记录 ifree64 (2020-04-23 11:47:51)
离线
uboot的配置和编译
V3s启动时,首先是芯片内的BROM运行,这个启动代码会搜索SD卡,SPI Flash特定位置,加载BootLoader中的SPL部分,
再由SPL加载完整的uboot镜像,由uboot加载内核和设备树文件。
1. 获取支持SPI Flash的uboot代码
由于制作完成的镜像存放在SPI Flash中,uboot需要支持从SPI Flash设备,以实现加载过程。从以下仓库中获取代码,并切换到v3s-spi-experimental分支。
$ git clone https://github.com/Lichee-Pi/u-boot.git -b v3s-spi-experimental
可通过`git branch -r `命令查看代码仓库所有的分支
$ git branch -r
origin/HEAD -> origin/master
origin/master
origin/nano-lcd800480
origin/nano-v2018.01
origin/next
origin/origin
origin/s3-l0p-exp
origin/u-boot-2009.11.y
origin/u-boot-2013.01.y
origin/u-boot-2016.09.y
origin/v3s-current
origin/v3s-spi-experimental
2. 配置uboot
该仓库的代码与Licheepi开发板有关的配置文件有以下三个,可根据需要选择,我选择支持800x480LCD的这个配置
$ cd u-boot
$ ls -1 configs/LicheePi*
configs/LicheePi_Zero_480x272LCD_defconfig
configs/LicheePi_Zero_800x480LCD_defconfig
configs/LicheePi_Zero_defconfig
$ make LicheePi_Zero_800x480LCD_defconfig
使用32M Flash需要加入Bank/Extended支持,make menuconfig,加入以下选项
Device Drivers --->
SPI Flash Support --->
[*] Enable Driver Model for SPI flash
[*] Legacy SPI Flash Interface support
[*] SPI flash Bank/Extended address register support
[*] Macronix SPI flash support
[*] Spansion SPI flash support
[*] STMicro SPI flash support
[*] Winbond SPI flash support
[*] Support for SPI Flash on Allwinner SoCs in SPL
3. 修改uboot,设置bootcmd和bootargs参数
uboot加载完毕后,会运行环境变量bootcmd中存放的命令,通过这些命令加载内核和设备树文件到内存,并启动内核。Uboot可以通过读取启动分区(?)中的boot.scr脚本文件来设置bootcmd、bootargs变量,也可以修改代码,硬编码设置这几个变量的值。这里是通过修改`include/configs/sun8i.h`这个头文件,硬编码设置这两个变量的值(实际上,我不懂如何做才能让uboot搜索分区中的文件,以加载变量的值,显然通过文件加载变量的值更好,因为不需要重新编译uboot就可以修改bootcmd变量,更加灵活)。
#define CONFIG_BOOTCOMMAND "sf probe 0:0 6000000; " \
"sf read 0x41800000 0x80000 0x4000; " \
"sf read 0x41000000 0x90000 0x470000;" \
"bootz 0x41000000 - 0x41800000;"
#define CONFIG_BOOTARGS "console=ttyS0,115200 earlyprintk panic=5 rootwait mtdparts=spi0.0:512k(uboot),64k(dtb)ro,4544k(kernel)ro,-(rootfs) root=/dev/mtdblock3 rw rootfstype=jffs2 init=/linuxrc vt.global_cursor_default=0"
命令的功能解释如下:
* sf probe 0:0 60000000 挂载SPI Flash设备(PS:后面这个60000000不知道什么意思,好像不加也可以)
* sf read 0x4100000 0x90000 0x470000,该命令加载内核, 从SPI Flash的0x90000地址处,读取0x470000字节内容到内存0x41000000处,完成内核加载;
* sf read 0x41800000 0x80000 0x4000 该命令加载设备树文件到0x41800000内存地址处。
由此可见,根据分区规划的不同,内核文件的尺寸的不同,需要修改这些数值。
4. 设置bootargs参数
这里仍然选择硬编码的方法,修改bootargs参数,该参数是启动内核时,传递给内核的命令行参数,目的在于将SPI的分区结构传递给内核,以便于内核从正确的位置加载根文件系统。
console=ttyS0,115200 earlyprintk panic=5 rootwait mtdparts=spi32766.0:512k(uboot),64k(dtb)ro,4544k(kernel)ro,-(rootfs) root=/dev/mtdblock3 rw rootfstype=jffs2 init=/linuxrc vt.global_cursor_default=0
在这个命令行参数中,使用mtdparts参数传递了flash的分区信息,其参数的语法格式在文件linux/drivers/mtd/cmdlinepart.c中描述如下:
/*
* mtdparts=<mtddef>[;<mtddef]
* <mtddef> := <mtd-id>:<partdef>[,<partdef>]
* <partdef> := <size>[@<offset>][<name>][ro][lk]
* <mtd-id> := unique name used in mapping driver/device (mtd->name)
* <size> := standard linux memsize OR "-" to denote all remaining space
* size is automatically truncated at end of device
* if specified or truncated size is 0 the part is skipped
* <offset> := standard linux memsize
* if omitted the part will immediately follow the previous part
* or 0 if the first part
* <name> := '(' NAME ')'
* NAME will appear in /proc/mtd
*
* <size> and <offset> can be specified such that the parts are out of order
* in physical memory and may even overlap.
*
* The parts are assigned MTD numbers in the order they are specified in the
* command line regardless of their order in physical memory.
*/
由上可知,命令行
mtdparts=spi0.0:512k(uboot),64k(dtb)ro,4544k(kernel)ro,-(rootfs)
可以解释为:
* spi0.0 <mtd-id>,这个是内核识别到的spi flash设备号
参考[荔枝派zero linux5.2,spi flash启动识别不到分区](https://whycan.cn/t_4119.html),从内核启动信息中,可以看到这个mtd-id为spi0.0
[ 0.769344] m25p80 spi0.0: mx25l25635e (32768 Kbytes)
[ 0.774436] 4 cmdlinepart partitions found on MTD device spi0.0
[ 0.780416] Creating 4 MTD partitions on "spi0.0":
[ 0.785214] 0x000000000000-0x000000080000 : "uboot"
[ 0.792040] 0x000000080000-0x000000090000 : "dtb"
[ 0.798618] 0x000000090000-0x000000500000 : "kernel"
[ 0.805369] 0x000000500000-0x000002000000 : "rootfs"
* 512k(uboot) 这个部分定义了一个partdef,size为512k,name为uboot,其余类推
进入Linux后,可以看到如下信息
# cat /proc/mtd
dev: size erasesize name
mtd0: 00080000 00010000 "uboot"
mtd1: 00010000 00010000 "dtb"
mtd2: 00470000 00010000 "kernel"
mtd3: 01b00000 00010000 "rootfs"
离线
Linux内核编译
1. 获取源代码
git clone https://github.com/Lichee-Pi/linux.git -b zero-5.2.y
这里切换到了该仓库中,内核最新的分支,经测试该分支已经包含了Ethernet的支持,满足我自己的需要。
2. 配置源代码
2.1 首先导入默认配置
CROSS_COMPILE=arm-linux-gnueabihf- ARCH=arm make licheepi_zero_defconfig
2.2 修改设备树,加入SPI Flash和Ethernet的支持
修改文件`arch/arm/boot/dts/sun8i-v3s-licheepi-zero.dts`,在该文件适当的位置加入以下内容。(PS:我现在也不懂设备树,但是打开这个文件后,按照以下内容提示应该知道改哪些部分吧?)
aliases {
serial0 = &uart0;
ethernet0 = &emac;
};
&emac {
phy-handle = <&int_mii_phy>;
phy-mode = "mii";
allwinner,leds-active-low;
status = "okay";
};
&spi0 {
status ="okay";
mx25l25635e:mx25l25635e@0 {
compatible = "jedec,spi-nor";
reg = <0x0>;
spi-max-frequency = <50000000>;
#address-cells = <1>;
#size-cells = <1>;
};
};
2.3 配置选项
* 加入SPI Flash的支持
Device Drivers --->
<*> Memory Technology Device (MTD) support --->
<*> Command line partition table parsing (用以支持命令行参数 mtdparts=spi0.0:512k(uboot)ro, … )
<*> Caching block device access to MTD devices
<*> SPI-NOR device support --->
[ ] Use small 4096 B erase sectors (取消这个选型,否则jffs2文件系统会报错)
特别注意这里的两个选项Command line partition table parsing,如果不选中这个,加无法识别mtd分区,启动系统时回报错
Wait for root filesystem /dev/mtdblock3
第二个选项是Use small 4096 B erase sectors,必须取消选中它,否则jffs2文件系统会大量报错,而无法加载进入系统。
* 加入jffs2文件系统支持
File systems --->
[*] Miscellaneous filesystems --->
<*> Journalling Flash File System v2 (JFFS2) support
(0) JFFS2 debugging verbosity (0 = quiet, 2 = noisy)
[*] JFFS2 write-buffering support
[ ] Verify JFFS2 write-buffer reads
[ ] JFFS2 summary support
[ ] JFFS2 XATTR support
[ ] Advanced compression options for JFFS2
* 加入Ethernet支持
Device Drivers --->
[*] Network device support --->
[*] Ethernet driver support --->
[*] STMicroelectronics devices
<*> STMicroelectronics Multi-Gigabit Ethernet driver
[ ] Support for STMMAC Selftests
<*> STMMAC Platform bus support
< > Support for snps,dwc-qos-ethernet.txt DT binding.
<*> Generic driver for DWMAC
<*> Allwinner GMAC support
<*> Allwinner sun8i GMAC support
(很奇怪,全志的芯片网络支持会在STMicroelectronics deveices配置菜单下面。)
3. 编译
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
最后生成的文件在下面路径
$ ls -lh arch/arm/boot/zImage
-rwxrwxr-x 1 xlee xlee 4.2M Apr 23 10:41 arch/arm/boot/zImage
$ ls -lh arch/arm/boot/dts/sun8i-v3s-licheepi-zero.dtb
-rw-rw-r-- 1 xlee xlee 12K Apr 22 23:33 arch/arm/boot/dts/sun8i-v3s-licheepi-zero.dtb
离线
使用buildroot制作根文件系统
其实buildroot可以直接制作uboot和内核的,但是国内通过git下载uboot和kernel的代码实在太慢,几乎无法成功,所以前面已经独立的下载了这两个仓库,所以这里就不使用buildroot来生成uboot和kernel了。
1. 获取buildroot源代码
git clone git://git.busybox.net/buildroot
2. 配置
在最新的buildroot有一个licheepi的默认配置文件
$ make licheepi_zero_defconfig
通过这个配置文件,可以得到一个sdcard.img的文件,直接dd烧录进入sd卡,就可以启动Linux系统。
使用这个默认配置的好处在于,如果从零开始,对我这样的初学者实在不知道该选择那些选项,不该选择那些选项,使用默认配置至少可以得到一个基本能用的起点,后面在这个基础上慢慢改就可以了。
但是这个默认配置的uboot不能支持spi(其实是我不会),linux内核使用的5.3.5版本,默认配置还不支持网卡(我也不会添加修改),暂时先用前面两个步骤得到的uboot和kernel,只用其得到的根文件系统。
3. 工具链设置
默认的配置使用ulibc作为C库,我猜测这个库不能支持Qt,所以要换掉。
可以使用buildroot来生成工具链,但是下载实在是太慢了,几乎不会下载成功。干脆配置buildroot使用系统自带的工具链。(其实前面编译uboot和kernel也是使用系统自带的工具链。)
更新:工具链最终还是使用的buildroot构建的工具链。系统自带工具链会产生错误。
4. 软件包
软件包目前还没有太多需求,默认配置能够得到一个可用的rootfs,先把系统启动起来,后面再研究一下Qt如何配置。
最近编辑记录 ifree64 (2020-04-23 17:20:08)
离线
打包生成Flash镜像
1. 打包脚本
按照分区规划,将上面得到的文件,用dd写入到指定的位置就可以得到需要的镜像文件。打包命令如下:
#!/bin/bash
dd if=/dev/zero bs=1MiB count=32 | tr "\000" "\377" > flash_32m.bin
mkfs.jffs2 --pad=0x1B00000 -d rootfs/ -o jffs2.bin
dd if=notrunc if=u-boot-sunxi-with-spl.bin of=flash_32m.bin seek=0
dd if=notrunc if=sun8i-v3s-licheepi-zero.dtb of=flash_32m.bin bs=$((0x80000)) seek=1
dd if=notrunc if=zImage of=flash_32m.bin bs=$((0x90000)) seek=1
dd if=notrunc if=jffs2.bin of=flash_32m.bin bs=$((0x500000)) seek=1
*PS: 不懂 bs=$((0x80000))这个地方的语法,为啥要用$符号还要加两个括号,照着大神的抄过来的。*
2. 烧录Flash镜像
让开发板进入FEL模式,使用sunxi-fel工具,将镜像写入SPI Flash。
2.1 烧录整个flash
$ sudo sunxi-fel -p spiflash-write 0 flash_32m.bin
2.2 也可以独立烧录
下面的命令是根据前面的分区规划来的,如果你的分区规划不一样,需要修改一下地址。独立烧录的价值在于调试阶段,因为sunxi-fel的烧录非常的慢,如果能少写入一点数据可以提高不少的性能。
$sudo sunxi-fel -p spiflash-write 0 u-boot-sunxi-with-spl.bin
$sudo sunxi-fel -p spiflash-write 0xF0000 sun8i-v3s-licheepi-zero-dock.dtb
$sudo sunxi-fel -p spiflash-write 0x100000 zImage
$sudo sunxi-fel -p spiflash-write 0x600000 jffs2.bin
3. 编写自己的Hello,World
系统启动来以后,总是要自己写点程序试一下的,先来一个Hello,World
$ cat myapp/hello.c
#include <stdio.h>
int main(void)
{
printf("Hello,World!\n");
return 0;
}
$ arm-linux-gnueabihf-gcc myapp/hello.c -o myapp/hello
将交叉编译得到的hello直接拷贝到rootfs目录里面,重新生成根文件系统,烧录进去就OK了。
但文件系统非常大,烧录太慢,看来还得支持scp拷贝才行。后面慢慢折腾吧。
离线
后记
本笔记资料来源于whycan网的多位大神,感谢晕哥这个平台提供了大量资料。我这篇帖子只能算一篇笔记吧,把这几天各种尝试、爬楼的结果记录下来,一来给自己做一个备忘,如果能帮助到更多向我这样的新手,就不胜荣幸了。
另目前还有一些自觉不够完美的地方,现在自己的技术实力又搞不定,留坑待以后再填吧。
1. 是不是能够通过boot.scr脚本文件设置bootcmd、bootargs参数;不能老是去改uboot的代码吧?
2. 创建UBIFS文件系统,存放除了uboot以外所有需要的文件;用文件系统存放启动脚本boot.scr和设备树文件,分区规划应该更自由,只需要1个分区存放uboot镜像,剩下的都留给根文件系统,修改起来好像更方便?
3. 使用buildroot的genimage工具生成系统镜像,这个方法感觉更好,genimage的脚本制作启动镜像,看上去更易懂一些。
4. 如果完全从主线uboot、kernel、buildroot拉代码,要在荔枝派上跑起来,该如何修改呢,原理又是啥,这个坑好大。
离线
我,你这个标题,我怎么感觉这么熟悉呢?感谢分享,/笑。
其实我就是根你学的,不好意思侵权了。
离线
是不是设备树没有选对?
离线