如何在uCLinux上面使用MTD/JFFS2

引用中国Linux公社

偉大的Liang Alei先生翻譯了此文


原文:
http://www.enseirb.fr/~kadionik/embedded/uclinux/mtd/howto_mtd.html
(中文没有版权,英文参考原文版权)
=======================================

HOW TO USE MTD/JFFS2 UNDER µClinux

(translated by Liang Alei)
Patrice KADIONIK, Professor Assistant at the ENSEIRB School of Electrical Engineering, Telecommunication, and Computer Science
kadionik@enseirb.fr
http://www.enseirb.fr/~kadionik
1. MOTIVATIONS
本文将详细描述:怎样在µClinux 中step-by-step地构建一个MTD/JFFS2。
I'm currently teaching embedded system programming and propose to my students practical exercices on. In every
我目前在大学讲授“嵌入式系统编程”,并指导学生“µClinux on Motorola M5407C3 ColFire boards”的实验。一般在每个嵌入式系统中都存在有FLASH memory。通常可以用来存放配置参数(in a raw binary format)。而现在在embedded Linux中,我们可以在FLASH中放置一个文件系统,并在embedded Linux OS启动之后将其mount。这里采用的技术是MTD (Memory Technology Device),MTD将隐藏有关“物理(physical) FLASH”的编程细节(如:读/写/擦除扇区),并在MTD之上放置JFFS(2) 文件系统(Journalling Flash File System)----一种被Linux和µClinux支持的文件系统。JFFS2很robust,即在意外掉电(power failure)之后,再启动(reboot)之时不需要fsck检查。

JFFS JFFS2
MTD
FLASH, RAM

另外一种使用FLASH memory的情况(或用途)是:将Linux kernel放在FLASH中,由bootloader在启动时将Linux kernel解压缩至RAM中,然后转移至RAM引导kernel。(在此我们不将深入讨论该问题)
强烈推荐:在阅读本文这个HOWTO之前,读--再读--多读以下文档:
· The Memory Technology Device (MTD) Subsystem for Linux site. The Linux MTD, JFFS HOWTO.
· JFFS2: The Journalling Flash File System, version 2. What is JFFS2?
· A paper from David Woodhouse on JFFS2
· The David Woodhouse's presentation on JFFS2. He explains how works the Garbage Collector with JFFS2
· The www.embeddedlinuxworks.com site. JFFS- A practical guide. The list of FLASH memories supporting the MTD technology.
· Using Flash Memory with µClinux by Greg Ungerer.
· Find your Root File System with MTD by Phil Wilshire.
· CFI presentation from AMD site
I will also thank here Massimo Calo who helped me through his threads posted to the µClinux news server and his mails on MTD/JFFS.

2. MTD/JFFS/JFFS2 OVERVIEW
下面的内容摘录自我之前阅读的一篇文档(from the www.embeddedlinuxworks.com site, an article from Vipin Malik)。
" 2000年, Axis Communications AB (www.axis.com),发布了第一版的 JFFS文件系统,也是开源的(Open Sourced)。这是一个完全为嵌入式Linux系统设计的文件系统, JFFS直接设计在FLASH设备之上,能判断获知可擦除扇区的边界,以及FLASH的大小。
MTD可以看作是原始的FLASH芯片的“翻译层(translation layer)”,也正是由于MTD的存在,使得JFFS在Linux中能快速发展。基于MTD这个硬件抽象层(HAL),JFFS几乎可以被mount在任何可随机访问的设备上(如: RAM, FLASH(各种厂家的))-----只要MTD支持这些设备。
如果遇到新设备且驱动程序不存在,则找一个现存的驱动程序(做模板),然后修改其中的有关“read/write/erase”之函数,之后就可以将 JFFS mount到“新设备”上了(关键是你根本不需要知道JFFS是怎么工作的)。换句话说,将JFFS文件系统与设备相关的MTD (Memory Technology Device)(包含了很多“raw FLASH chip”之Linux驱动程序)相结合,你就得到了一个完整的解决方案----即由MTD向JFFS文件系统层提供一个抽象的设备层。
在这种方式下,JFFS不关心任何特定的存储(memory)技术。任何支持随机访问的设备(甚至象NAND FLASH这样的伪随机设备)都可以与MTD接口,从而在之上实现JFFS。在设计方面,JFFS(以及JFFS2)在文件系统层中保证一个“meta- data”(或文件系统的“格式”可靠性--- "formatting" reliability),这意味只要你的 write()系统调用返回了,则可以保证数据一定是“记录”下来的。换句话说,如果在write()命令的执行期间突然掉电了,而此时数据还没有完全写入FLASH芯片,则芯片内记录的数据要么是older数据、要么是 newer数据,或者是两者的混合,但关键是你的文件不会因此而“坏(corrupted)”了。
最初的JFFS是设计成 "append only"类型的文件系统,即“好”数据重来不会被“覆盖”( overwritten)(如:对一个已打开的文件先做rewind()操作,然后 fwrite(),没用!),新的数据总是被添加在上次“文件系统”(而不是文件)写操作的位置之后。与块数据同时被写入的“meta”数据将保证块数据在“逻辑”上已被写入文件的正确位置。当重启动(或mount)之时,整个文件系统将被扫描,零散的数据块被重新排列,以使得在读取文件之时,最新被“标记(stamped)”数据块----即那些在逻辑上覆盖了“older数据块”----被读出。而“older数据”则被标记为“回收(garbage collection)-----在适当的时候将被删除。这种“ append-only结构”的优点是“natural wear leveling on the FLASH”(译注:减少FLASH的擦除次数)。有关JFFS的“掉电可靠性(Power Down Reliability)”,我已经做一些扩展并提交了fixes(已包含在最新版本的 CVS之中),原本大概掉電10次就有一次失敗,在我的修正之後可以成長到超過500次才有一次的失敗。另外,系统中还存在一些bug,使得JFFS会随机地丢掉一些文件(甚至是静态文件)!我将 NOT推荐在产品中使用该文件系统(至少是当前版本)。
问:解决方案?答: JFFS2。
JFFS2是JFFS技术的第二版,它基于JFFS的设计思想,但是由Redhat (www.redhat.com)实现的。它采用了一种不同的方法实现“可靠性”,所有的 "erase sector"被独立管理,且可以“乱序(out-of-order)”寻址,因而当创建新文件或覆盖老文件之时,可以统一地申请一块“已擦除扇区(erase sector)”。为了保证“掉电可靠性”,在被确认已被成功写入FLASH之前(通过CRC和版本标签),文件的任何部分不会被真正覆盖。之后,老的数据块被标记为“回收”,则待到其所有的邻居(同一个扇区内的)也有相似的标记之时,该扇区被擦除。
好消息,JFFS2还支持压缩。文件数据在被写入时,通过zlib(可调整mod's)压缩;数据在被读出之时在线(on the fly)解压缩;所以事实上你无法感知你的数据是否被压缩了。因而现在你可以采用ASCII文件格式(而不是binary文件)来做日志(log)或配置(config)文件,当然二进制文件也会被压缩的。如果你的文件很“松散(sparse)”(如:其中有很多空格),呵呵,不用担心空间会浪费了。但缺点是,如果你将已经压缩过的数据写入时,系统仍将花费大量时间试图再压缩它。而此时你又无法动态关闭压缩功能。目前有些计划正在试图实现相关功能(即使是基于目录级别,对单个目录实现压缩的开关选项或属性)。

3. STEP 1: ENABLING MTD/JFFS2 UNDER µClinux
我的M5407C3 Motorola板子上,有一个16 bit FLASH内存(AMD Am29PL160C),内存映射空间是$7FE00000~$7FFFFFFF。
首先,你要认真、仔细地阅读有关你所使用的FLASH芯片的“数据手册”,查看它的扇区地址表:

我的FLASH芯片有11个扇区(大小不等)。在第一个256KB空间(SA0~SA3)中包含“dBUG Motorola monitor”代码,其余空间供用户使用(SA4~SA10,每扇区256 KB)。当然你也可以擦除“dBUG monitor”而使用整个地址空间(dBUG monitor代码可以通过BDM线缆重新安装)。我选择的方案是定义两个“MTD分区(partition)”:
· dBUG分区:$7FE00000~$7FE3FFFF (256 Kbyte size)。(我不想将monitor去除。。。)
· user分区:$7FE40000~$7FFFFFFF (1792 Kbyte size)。
如果你擦除“dBUG monitor”,则拥有一个大的user分区:
· user分区:$7FE00000~$7FFFFFFF (2 Mbyte size).
注意:(参考µClinux文档)如果你使用的是JFFS(2),每个MTD分区至少包含6个连续扇区(原因:garbage collection)。
首先,配置µClinux之时,打开“MTD/JFFS2”:
% cd uClinux-dist
% make xconfig

看一下我的有关MTD的选项:
% grep MTD linux-2.4.x/.config
# Memory Technology Devices (MTD)
CONFIG_MTD=y
CONFIG_MTD_DEBUG=y
CONFIG_MTD_DEBUG_VERBOSE=3
CONFIG_MTD_PARTITIONS=y
CONFIG_MTD_CHAR=y
CONFIG_MTD_BLOCK=y
CONFIG_MTD_CFI=y
CONFIG_MTD_JEDECPROBE=y
CONFIG_MTD_GEN_PROBE=y
CONFIG_MTD_CFI_AMDSTD=y
CONFIG_MTD_PHYSMAP=y
CONFIG_MTD_PHYSMAP_START=0x7fe00000
CONFIG_MTD_PHYSMAP_LEN=0x200000
CONFIG_MTD_PHYSMAP_BUSWIDTH=2


其中,你已经打开CONFIG_MTD_PHYSMAP(参考uClinux-dist/linux-2.4.x/drivers/mtd/maps/physmap.c),这意味着基于一个MTD分区,你可以访问整个FLASH空间了。
为了定义多个MTD分区,我又创建了一个特殊的physmap.c文件(m5407c3.c,uClinux-dist/linux-2.4.x/drivers/mtd/maps/目录)。

之后,打开JFFS2(µClinux内核配置):
· File systems menu: JFFS2 support, JFFS2 debugging verbosity 2.

选择MTD/JFFS2工具(userland area):
· Flash Tools menu: mtd-utils with erase, mkfs.jff2.
· BusyBox menu: BusyBox with dd, mount, mount: loop devices, umount.

创建自己的MTD之physmap文件
如果你需要多个MTD分区,你可以修改uClinux-dist/linux-2.4.x/drivers/mtd/maps/physmap.c 文件(参考:Phil Wildshire's document 之解释)。
我选择在µClinux distribution中创建自己的文件(在uClinux-dist/linux-2.4.x/drivers/mtd/maps/目录下又很多参考例子,我以m5272c3.c为例,这是一个专用于M5272C3 Motorola 开发板的)。
我的m5407c3.c 文件
#include
#include
#include
#include
#include
#include
#include
#include

#define WINDOW_ADDR 0x7fe00000
#define WINDOW_SIZE 0x200000
#define BUSWIDTH 2

static struct mtd_info *mymtd;
__u8 m5407c3_read8(struct map_info *map, unsigned long ofs)
{
return __raw_readb(map->map_priv_1 + ofs);
}
__u16 m5407c3_read16(struct map_info *map, unsigned long ofs)
{
return __raw_readw(map->map_priv_1 + ofs);
}
__u32 m5407c3_read32(struct map_info *map, unsigned long ofs)
{
return __raw_readl(map->map_priv_1 + ofs);
}
void m5407c3_copy_from(struct map_info *map, void *to, unsigned long from, ssize_t len)
{
memcpy_fromio(to, map->map_priv_1 + from, len);
}
void m5407c3_write8(struct map_info *map, __u8 d, unsigned long adr)
{
__raw_writeb(d, map->map_priv_1 + adr);
mb();
}
void m5407c3_write16(struct map_info *map, __u16 d, unsigned long adr)
{
__raw_writew(d, map->map_priv_1 + adr);
mb();
}
void m5407c3_write32(struct map_info *map, __u32 d, unsigned long adr)
{
__raw_writel(d, map->map_priv_1 + adr);
mb();
}
void m5407c3_copy_to(struct map_info *map, unsigned long to, const void *from, ssize_t len)
{
memcpy_toio(map->map_priv_1 + to, from, len);
}
struct map_info m5407c3_map = {
name: "MCF5407C3 flash device",
size: WINDOW_SIZE,
buswidth: BUSWIDTH,
read8: m5407c3_read8,
read16: m5407c3_read16,
read32: m5407c3_read32,
copy_from: m5407c3_copy_from,
write8: m5407c3_write8,
write16: m5407c3_write16,
write32: m5407c3_write32,
copy_to: m5407c3_copy_to
};
/*
* MTD 'PARTITIONING' STUFF
*/
static struct mtd_partition m5407c3_partitions[] = {
{
name: "dBUG (256K)",
size: 0x40000,
offset: 0x0
},
{
name: "user (1792K)",
size: 0x1c0000,
offset: 0x40000
}
};
int __init init_m5407c3(void)
{
printk(KERN_NOTICE "m5407c3 flash device: %x at %x\n", WINDOW_SIZE, WINDOW_ADDR);
m5407c3_map.map_priv_1 = (unsigned long)ioremap(WINDOW_ADDR, WINDOW_SIZE);
if (!m5407c3_map.map_priv_1) {
printk("Failed to ioremap\n");
return -EIO;
}
mymtd = do_map_probe("cfi_probe", &m5407c3_map);
if (mymtd) {
mymtd->module = THIS_MODULE;
mymtd->erasesize = 0x40000;
return add_mtd_partitions(mymtd, m5407c3_partitions,
sizeof(m5407c3_partitions) /
sizeof(struct mtd_partition));
}
iounmap((void *)m5407c3_map.map_priv_1);
return -ENXIO;
}
static void __exit cleanup_m5407c3(void)
{
if (mymtd) {
del_mtd_partitions(mymtd);
map_destroy(mymtd);
}
if (m5407c3_map.map_priv_1) {
iounmap((void *)m5407c3_map.map_priv_1);
m5407c3_map.map_priv_1 = 0;
}
}
module_init(init_m5407c3);
module_exit(cleanup_m5407c3);

最重要的是在mtd_partition m5407c3_partitions[]结构中,我定义了两个MTD分区。(其它内容我都是通过“替换”来修改的,即基于vi执行sed命令 :-))。
问:怎样将我的文件集成到µClinux distribution之中?我的做法是:
1. 在uClinux-dist/linux-2.4.x/drivers/mtd/maps/Config.in 文件中,添加下面行:
if [ "$CONFIG_M5407C3" ]; then dep_tristate ' CFI Flash device mapped on Motorola M5407C3' CONFIG_MTD_M5407C3 $CONFIG_MTD_CFI fi
2. 在uClinux-dist/linux-2.4.x/drivers/mtd/maps/Makefile 文件中添加:
obj-$(CONFIG_MTD_M5407C3) += m5407c3.o

于是,再次执行make xconfig,你将看到:
% cd uClinux-dist
% make xconfig


现在,我没有采用CONFIG_MTD_PHYSMAP默认选项:
# Memory Technology Devices (MTD)
CONFIG_MTD=y
CONFIG_MTD_DEBUG=y
CONFIG_MTD_DEBUG_VERBOSE=3
CONFIG_MTD_PARTITIONS=y
CONFIG_MTD_CHAR=y
CONFIG_MTD_BLOCK=y
CONFIG_MTD_CFI=y
CONFIG_MTD_JEDECPROBE=y
CONFIG_MTD_GEN_PROBE=y
CONFIG_MTD_CFI_AMDSTD=y
CONFIG_MTD_M5407C3=y

3. STEP 2: MODIFYING THE µClinux KERNEL
3.1. Adding files from the Linux kernel distribution(添加文件)
你可以从某个地方(如:here )下载“Linux kernel version 2.4.19”,然后将linux-2.4.19/fs/jffs2/之中的pushpull.c, zlib.c, zlib.h文件拷贝到uClinux-dist/linux-2.4.x/fs/jffs2/目录下。

3.2. Modifying files from the µClinux kernel distribution(修改文件)
将这些文件中的BLKMEM_MAJOR值从“31”修改为“30”:
· uClinux-dist/linux-2.4.x/drivers/block/blkmem.c (line 41).
· uClinux-dist/linux-2.4.x/include/linux/major.h (line 66).
目的是避免造成MTD and BLKMEM之间“major”号的冲突。

3.3. Modifying files from the µClinux M5407C3 BSP port
在文件uClinux-dist/vendors/Motorola/M5407C3/Makefile 之中添加以下内容:
DEVICES = \
tty,c,5,0 console,c,5,1 cua0,c,5,64 cua1,c,5,65 \
mtd0,c,90,0 mtd1,c,90,2 mtd2,c,90,4 mtd3,c,90,6 \
mtd4,c,90,8 mtd5,c,90,10 mtd6,c,90,12 mtd7,c,90,14 \
mtdblock0,b,31,0 mtdblock1,b,31,1 mtdblock2,b,31,2 mtdblock3,b,31,3 \
mtdblock4,b,31,4 mtdblock5,b,31,5 mtdblock6,b,31,6 mtdblock7,b,31,7 \
mem,c,1,1 kmem,c,1,2 null,c,1,3 \
. . .
目的是在目标设备的uClinux目录结构之/dev目录下创建MTD and JFFS2所需要的设备文件。
4. STEP3: COMPILING(编译)
开始编译整个µClinux distribution:
% cd uClinux-dist
% make dep
% make

4. STEP4: TESTING(测试)
最后,通过网络下载µClinux的“image”文件至M5407C3板,并启动µClinux内核:
Hard Reset
DRAM Size: 32M
Copyright 1995-2001 Motorola, Inc. All Rights Reserved.
ColdFire MCF5407 EVS Firmware v2e.1a.1b (Build 18 on Apr 20 2001 11:57:55)
Enter 'help' for help.
dBUG> dn (下载)
Eth Mac Addr is 00:00:00:00:00:01
Downloading Image 'image.bin' from 192.168.4.1
Read 1891552 bytes (3695 blocks)
dBUG> go 20000 (启动)

启动显示:
Linux version 2.4.19-uc1 (root@localhost.localdomain) (gcc version 2.95.3
uClinux/COLDFIRE(m5407)
COLDFIRE port done by Greg Ungerer, gerg@snapgear.com
Flat model support (C) 1998,1999 Kenneth Albanowski, D. Jeff Dionne
. . .
Starting kswapd
kmem_create: Forcing size word alignment - file lock cache
JFFS2 version 2.1. (C) 2001 Red Hat, Inc., designed by Axis Communications AB.
ColdFire internal UART serial driver version 1.00
ttyS0 at 0x100001c0 (irq = 73) is a builtin ColdFire UART
ttyS1 at 0x10000200 (irq = 74) is a builtin ColdFire UART
kmem_create: Forcing size word alignment - blkdev_requests
ne.c:v1.10 9/23/94 Donald Becker (becker@scyld.com)
Last modified Nov 1, 2000 by Paul Gortmaker
NE*000 ethercard probe at 0x40000300: 00 00 00 00 00 01
eth0: NE2000 found at 0x40000300, using IRQ 27.
SLIP: version 0.8.4-NET3.019-NEWTTY (dynamic channels, max=256).
CSLIP: code copyright 1989 Regents of the University of California.
Blkmem copyright 1998,1999 D. Jeff Dionne
Blkmem copyright 1998 Kenneth Albanowski
Blkmem 1 disk images:
0: 107FA4-2013A3 [VIRTUAL 107FA4-2013A3] (RO)
RAMDISK driver initialized: 16 RAM disks of 4096K size 1024 blocksize
PPP generic driver version 2.4.2
m5407c3 flash device: 200000 at 7fe00000
Amd/Fujitsu Extended Query Table v1.0 at 0x0040
number of CFI chips: 1
Creating 2 MTD partitions on "MCF5407C3 flash device":
0x00000000-0x00040000 : "dBUG (256K)"
mtd: partition "dBUG (256K)" doesn't end on an erase block -- force read-only
mtd: Giving out device 0 to dBUG (256K)
0x00040000-0x00200000 : "user (1792K)"
mtd: Giving out device 1 to user (1792K)
init_mtdchar: allocated major number 90.
init_mtdblock: allocated major number 31.
NET4: Linux TCP/IP 1.0 for NET4.0
IP Protocols: ICMP, UDP, TCP
. . .
Command: dhcpcd -p -a eth0 &
[14]
Command: cat /etc/motd
. . .
For further information check:
http://www.uclinux.org/
Execution Finished, Exiting
Sash command shell (version 1.1.1)

看到了?JFFS2和MTD都打开了,FLASH内存也被“CFI探测”发现了,2个 MTD分区也被创建了(/dev/mtd0和/dev/mtd1)。
查看一下MTD的分区列表:
/> cd /proc
/proc> cat mtd
dev: size erasesize name
mtd0: 00040000 00038000 "dBUG (256K)"
mtd1: 001c0000 00040000 "user (1792K)"

尝试着在MTD的user分区(/dev/mtd1)上创建一个JFFS2 image:
/proc> cd /tmp
/var/tmp> mkdir fs
/var/tmp> mkdir jffs2
/var/tmp> mkdir jffs2/bin
/var/tmp> cd jffs2
/var/tmp/jffs2> vi file1
/var/tmp/jffs2> cat file1
coucou
/var/tmp/jffs2> cd ..
/var/tmp> mkfs.jffs2 -d jffs2 -o jffs2.img
/var/tmp> erase /dev/mtd1
/var/tmp> cp jffs2.img /dev/mtd1
MTD_open
MTD_write
MTD_close

“Mount”JFFS2分区:
/var/tmp> mount -t jffs2 /dev/mtdblock1 /mnt
mtdblock_open
ok
/var/tmp> cd /mnt
/mnt> ls
bin
file1
toto
/mnt> cd /proc
/proc> cat mounts
rootfs / rootfs rw 0 0
/dev/root / romfs ro 0 0
/proc /proc proc rw 0 0
/dev/ram1 /var ext2 rw 0 0
/dev/mtdblock1 /mnt jffs2 rw 0 0

查看一下所有相关的设备文件(/dev/目录):
/proc> ls -l /dev
. . .
crw------- 1 0 0 90, 0 Jan 01 1970 mtd0
crw------- 1 0 0 90, 2 Jan 01 1970 mtd1
crw------- 1 0 0 90, 6 Jan 01 1970 mtd3
crw------- 1 0 0 90, 8 Jan 01 1970 mtd4
crw------- 1 0 0 90, 10 Jan 01 1970 mtd5
crw------- 1 0 0 90, 12 Jan 01 1970 mtd6
crw------- 1 0 0 90, 14 Jan 01 1970 mtd7
brw------- 1 0 0 31, 0 Jan 01 1970 mtdblock0
brw------- 1 0 0 31, 1 Jan 01 1970 mtdblock1
brw------- 1 0 0 31, 2 Jan 01 1970 mtdblock2
brw------- 1 0 0 31, 3 Jan 01 1970 mtdblock3
brw------- 1 0 0 31, 4 Jan 01 1970 mtdblock4
brw------- 1 0 0 31, 5 Jan 01 1970 mtdblock5
brw------- 1 0 0 31, 6 Jan 01 1970 mtdblock6
brw------- 1 0 0 31, 7 Jan 01 1970 mtdblock7
. . .

最后,你也可以“umount”JFFS2分区:
/proc> umount /mnt
mtdblock_release
ok
就是这样了。

5. CONCLUSION(结论)
本文中,我描述了在µClinux 中怎样使用MTD/JFFS2 的方法(step by step),并在M5407C3板上通过测试:
相关的测试条件是:
· Linux kernel version 2.4.19.
· µClinux version 20020927.
· m68k-elf-tools version 20020410.
另外,我使用的PC是Dell laptop (Inspiron 8200, P IV at 1,7 GHz) under RedHat 8.0.

没有人是完美的,如果你发现了“不正确的”内容,请与 me 联系。

留言

  1. 大大您好,最近在玩模擬 NAND Flash 的讀寫,不過有些觀念不太正確,所以想請教您,糾正一下我的觀念。
    目前我已經能對 NAND Flash 做讀寫這些動作,接下來我希望能夠像一般 Pen Driver 一樣的存放資料。在這一段,我是要將 Flash Format 成什麼格式呢 ?!
    我上網查有 MTD、TFFS、還有您提到 JFFS ………… 這些也可以用嗎 ?! 有點搞迷糊了 !!
    希望大大能糾正一下我的觀念,謝謝 !!

    回覆刪除
  2. 先聲明我不是"大大"。

    既然你可以讀寫了,那就可以存放資料了啊。我很喜歡jffs2和yafs這兩個。

    我簡略說明一下,jffs是建構在MTD上面的Filesystem,而MTD又是架構在flash上面的一個中介層。

    我個人建議你看完這篇對岸網友翻譯的文章。極有幫助。

    回覆刪除
  3. 感謝您了、對 MTD & JFFS 彼此間的關係比較了解了。
    謝謝 !!

    回覆刪除

張貼留言

這個網誌中的熱門文章

大車人生: Yamaha FZ6N更換TPS、火星塞以及引擎調校

感恩啊!漢音輸入法!

[鳳梨外貿社] 1988 Austin Mini Automatic 奧斯丁 迷你上架銷售