基於ARM的嵌入式Linux移植真實體驗(2)――BootLoader

 BootLoader指系統啓動後,在操作系統內核運行之前運行的一段小程序。通過BootLoader,我們可以初始化硬件設備、建立內存空間的映射圖,從而將系統的軟硬件環境帶到一個合適的狀態,以便爲最終調用操作系統內核準備好正確的環境。通常,BootLoader是嚴重地依賴於硬件而實現的,特別是在嵌入式世界。因此,在嵌入式世界裏建立一個通用的 BootLoader 幾乎是不可能的。儘管如此,我們仍然可以對BootLoader歸納出一些通用的概念來,以指導用戶特定的BootLoader設計與實現。

  BootLoader 的實現依賴於CPU的體系結構,因此大多數 BootLoader 都分爲stage1 和stage2 兩大部分。依賴於CPU體系結構的代碼,比如設備初始化代碼等,通常都放在 stage1中,而且通常都用匯編語言來實現,以達到短小精悍的目的。而stage2 則通常用C 語言來實現,這樣可以實現更復雜的功能,而且代碼會具有更好的可讀性和可移植性。

  BootLoader 的 stage1 通常包括以下步驟:

  Ø     硬件設備初始化;

  Ø     爲加載Boot Loader的stage2準備 RAM 空間;

  Ø     拷貝Boot Loader的stage2 到RAM空間中;

  Ø     設置好堆棧;

  Ø     跳轉到 stage2 的 C 入口點。

  Boot Loader的stage2通常包括以下步驟:      

  Ø     初始化本階段要使用到的硬件設備;

  Ø     檢測系統內存映射(memory map);

  Ø     將kernel 映像和根文件系統映像從flash上讀到 RAM 空間中;

  Ø     爲內核設置啓動參數;

  Ø     調用內核。

  本系統中的BootLoader參照韓國mizi公司的vivi進行修改。 1.開發環境

  我們購買了武漢創維特信息技術有限公司開發的具有自主知識產權的應用於嵌入式軟件開發的集成軟、硬件開發平臺ADT(ARM Development Tools)它爲基於ARM 核的嵌入式應用提供了一整套完備的開發方案,包括程序編輯、工程管理和設置、程序編譯、程序調試等。

  ADT嵌入式開發環境由ADT Emulator for ARM 和ADT IDE for ARM組成。ADT Emulator for ARM 通過JTAG 實現主機和目標機之間的調試支持功能。它無需目標存儲器,不佔用目標系統的任何端口資源。目標程序直接在目標板上運行,通過ARM 芯片的JTAG 邊界掃描口進行調試,屬於完全非插入式調試,其仿真效果接近真實系統。

  ADT IDE for ARM 爲用戶提供高效明晰的圖形化嵌入式應用軟件開發環境,包括一整套完備的面向嵌入式系統的開發和調試工具:源碼編輯器、工程管理器、工程編譯器(編譯器、彙編器和連接器)、集成調試環境、ADT Emulator for ARM 調試接口等。其界面同Microsoft Visual Studio 環境相似,用戶可以在ADT IDE for ARM 集成開發環境中創建工程、打開工程,建立、打開和編輯文件,編譯、連接、設置、運行、調試嵌入式應用程序。

  ADT嵌入式軟件開發環境採用主機-目標機交叉開發模型。ADT IDE for ARM 運行於主機端,而ADT Emulator for ARM 實現ADT IDE for ARM 與目標機之間的連接。開發時,首先由ADT IDE for ARM 編譯連接生成目標代碼,然後建立與ADT Emulator for ARM 之間的調試通道,調試通道建立成功後,就可以在ADT IDE for ARM 中通過ADT Emulator for ARM 控制目標板實現目標程序的調試,包括將目標代碼下載到目標機中,控制程序運行,調試信息觀察等等。

基於ARM的嵌入式Linux移植真實體驗(2)――BootLoader2.ARM彙編

  ARM本身屬於RISC指令系統,指令條數就很少,而其編程又以C等高級語言爲主,我們僅需要在Bootloader的第一階段用到少量彙編指令:

  (1)+-運算

ADD  r0, r1, r2 
―― r0 := r1 + r2
SUB  r0, r1, r2 
―― r0 := r1 - r2

  其中的第二個操作數可以是一個立即數:

ADD r3, r3, #1
―― r3 := r3 + 1

  第二個操作數還可以是位移操作後的結果:

ADD r3, r2, r1, LSL #3
―― r3 := r2 + 8.r1

  (2)位運算

AND r0, r1, r2 
―― r0 := r1 and r2
ORR r0, r1, r2
―― r0 := r1 or  r2
EOR r0, r1, r2
―― r0 := r1 xor r2
BIC  r0, r1, r2 
―― r0 := r1 and not r2

  (3)寄存器搬移

MOV r0, r2
―― r0 := r2
MVN r0, r2     
―― r0 := not r2

  (4)比較

CMP r1, r2
―― set cc on r1 - r2
CMN r1, r2
―― set cc on r1 + r2
TST r1, r2
―― set cc on r1 and r2
TEQ r1, r2
―― set cc on r1 or r2

  這些指令影響CPSR寄存器中的 (N, Z, C, V) 位

  (5)內存操作

LDR r0, [r1]
―― r0 := mem [r1]
STR r0, [r1]
―― mem [r1] := r0
LDR r0, [r1, #4]
―― r0 := mem [r1+4]
LDR r0, [r1, #4] !
―― r0 := mem [r1+4]   r1 := r1 + 4
LDR r0, [r1], #4
―― r0 := mem [r1]    r1 := r1 +4
LDRB r0 , [r1]
―― r0 := mem8 [r1]
LDMIA r1, {r0, r2, r5}
―― r0 := mem [r1]  r2 := mem [r1+4]  r5 := mem [r1+8] {..} 可以包括r0~r15中的所有寄存器,若包括r15 (PC)將導致程序的跳轉。

  (6)控制流

  例1:

MOV r0, #0 ; initialize counter
LOOP:
 ADD r0, r0, #1 ; increment counter
 CMP r0, #10 ; compare with limit
 BNE LOOP ; repeat if not equal

  例2:

CMP r0, #5
ADDNE r1, r1, r0
SUBNE r1, r1, r2
――
if (r0 != 5) {
r1 := r1 + r0 - r2
}

  3.BootLoader第一階段

  3.1硬件設備初始化

  基本的硬件初始化工作包括:

  Ø     屏蔽所有的中斷;

  Ø     設置CPU的速度和時鐘頻率;

  Ø     RAM初始化;

  Ø     初始化LED

  ARM的中斷向量表設置在0地址開始的8個字空間中,如下表:

  每當其中的某個異常發生後即將PC值置到相應的中斷向量處,每個中斷向量處放置一個跳轉指令到相應的中斷服務程序去進行處理,中斷向量表的程序如下:

@ 0x00: Reset
    b   Reset
@ 0x04: Undefined instruction exception
UndefEntryPoint:
    b   HandleUndef
@ 0x08: Software interrupt exception
SWIEntryPoint:
    b   HandleSWI
@ 0x0c: Prefetch Abort (Instruction Fetch Memory Abort)
PrefetchAbortEnteryPoint:
    b   HandlePrefetchAbort
@ 0x10: Data Access Memory Abort
DataAbortEntryPoint:
    b   HandleDataAbort
@ 0x14: Not used
NotUsedEntryPoint:
    b   HandleNotUsed
@ 0x18: IRQ(Interrupt Request) exception
IRQEntryPoint:
    b   HandleIRQ
@ 0x1c: FIQ(Fast Interrupt Request) exception
FIQEntryPoint:
    b   HandleFIQ

Reset:
    @ disable watch dog timer
    mov r1, #0x53000000
    mov r2, #0x0
    str  r2, [r1]
  
    @ disable all interrupts
    mov r1, #INT_CTL_BASE
    mov r2, #0xffffffff
    str  r2, [r1, #oINTMSK]
    ldr  r2, =0x7ff
    str  r2, [r1, #oINTSUBMSK]

  設置系統時鐘:

  @init clk
  @ 1:2:4
  mov r1, #CLK_CTL_BASE
  mov r2, #0x3 
  str  r2, [r1, #oCLKDIVN]
  mrc p15, 0, r1, c1, c0, 0       @ read ctrl register
  orr  r1, r1, #0xc0000000       @ Asynchronous 
  mcr p15, 0, r1, c1, c0, 0       @ write ctrl register
  @ now, CPU clock is 200 Mhz
  mov r1, #CLK_CTL_BASE
  ldr  r2, mpll_200mhz
  str  r2, [r1, #oMPLLCON]

  點亮所有的用戶LED:

    @ All LED on
    mov r1, #GPIO_CTL_BASE
    add r1, r1, #oGPIO_F
    ldr  r2,=0x55aa
    str  r2, [r1, #oGPIO_CON]
    mov r2, #0xff
    str  r2, [r1, #oGPIO_UP]
    mov r2, #0x00
    str  r2, [r1, #oGPIO_DAT]

  設置(初始化)內存映射:

ENTRY(memsetup)
    @ initialise the static memory
  
    @ set memory control registers
    mov r1, #MEM_CTL_BASE
    adrl r2, mem_cfg_val
    add r3, r1, #52
1:   ldr  r4, [r2], #4
    str  r4, [r1], #4
    cmp r1, r3
    bne 1b
  
    mov pc, lr @ set GPIO for UART
    mov r1, #GPIO_CTL_BASE
    add r1, r1, #oGPIO_H
    ldr  r2, gpio_con_uart 
    str  r2, [r1, #oGPIO_CON]
    ldr  r2, gpio_up_uart
    str  r2, [r1, #oGPIO_UP]  
    bl   InitUART
  
@ Initialize UART
@
@ r0 = number of UART port
InitUART:
    ldr  r1, SerBase
    mov r2, #0x0
    str  r2, [r1, #oUFCON]
    str  r2, [r1, #oUMCON]
    mov r2, #0x3
    str  r2, [r1, #oULCON]
    ldr  r2, =0x245
    str  r2, [r1, #oUCON]
#define UART_BRD ((50000000 / (UART_BAUD_RATE * 16)) - 1)
    mov r2, #UART_BRD
    str  r2, [r1, #oUBRDIV]
    mov r3, #100
    mov r2, #0x0
1:   sub r3, r3, #0x1
    tst  r2, r3
    bne 1b
  
#if 0
    mov r2, #'U'
    str  r2, [r1, #oUTXHL]
  
1:   ldr  r3, [r1, #oUTRSTAT]
    and r3, r3, #UTRSTAT_TX_EMPTY
    tst  r3, #UTRSTAT_TX_EMPTY
    bne 1b  
  
    mov r2, #'0'
    str  r2, [r1, #oUTXHL]
  
1:   ldr  r3, [r1, #oUTRSTAT]
    and r3, r3, #UTRSTAT_TX_EMPTY
    tst  r3, #UTRSTAT_TX_EMPTY
    bne 1b  
#endif
  
    mov pc, lr@ PrintChar : prints the character in R0
@  r0 contains the character
@  r1 contains base of serial port
@  writes ro with XXX, modifies r0,r1,r2
@  TODO : write ro with XXX reg to error handling
PrintChar:
TXBusy:
    ldr  r2, [r1, #oUTRSTAT]
    and r2, r2, #UTRSTAT_TX_EMPTY
    tst  r2, #UTRSTAT_TX_EMPTY
    beq TXBusy 
    str  r0, [r1, #oUTXHL]
    mov pc, lr
  
@ PrintWord : prints the 4 characters in R0
@  r0 contains the binary word
@  r1 contains the base of the serial port
@  writes ro with XXX, modifies r0,r1,r2
@  TODO : write ro with XXX reg to error handling
PrintWord:
    mov r3, r0
    mov r4, lr
    bl   PrintChar
  
    mov r0, r3, LSR #8       /* shift word right 8 bits */
    bl   PrintChar
  
    mov r0, r3, LSR #16       /* shift word right 16 bits */
    bl   PrintChar
    mov r0, r3, LSR #24       /* shift word right 24 bits */
    bl   PrintChar
  
    mov r0, #'r'
    bl   PrintChar
  
    mov r0, #'n'
    bl   PrintChar
  
    mov pc, r4
  
@ PrintHexWord : prints the 4 bytes in R0 as 8 hex ascii characters
@ followed by a newline
@  r0 contains the binary word
@  r1 contains the base of the serial port
@  writes ro with XXX, modifies r0,r1,r2
@  TODO : write ro with XXX reg to error handling
PrintHexWord:
    mov r4, lr
    mov r3, r0
    mov r0, r3, LSR #28
    bl   PrintHexNibble
    mov r0, r3, LSR #24
    bl   PrintHexNibble
    mov r0, r3, LSR #20
    bl   PrintHexNibble
    mov r0, r3, LSR #16
    bl   PrintHexNibble
    mov r0, r3, LSR #12
    bl   PrintHexNibble
    mov r0, r3, LSR #8
    bl   PrintHexNibble
    mov r0, r3, LSR #4
    bl   PrintHexNibble
    mov r0, r3
    bl   PrintHexNibble
  
    mov r0, #'r'
    bl   PrintChar
  
    mov r0, #'n'
    bl   PrintChar
  
    mov pc, r4 3.2Bootloader拷貝

  配置爲從NAND FLASH啓動,需要將NAND FLASH中的vivi代碼copy到RAM中:

#ifdef CONFIG_S3C2410_NAND_BOOT
    bl   copy_myself
  
    @ jump to ram
    ldr  r1, =on_the_ram
    add pc, r1, #0
    nop
    nop
1:   b   1b      @ infinite loop
  
#ifdef CONFIG_S3C2410_NAND_BOOT
@
@ copy_myself: copy vivi to ram
@
copy_myself:
    mov r10, lr
  
    @ reset NAND
    mov r1, #NAND_CTL_BASE
    ldr  r2, =0xf830      @ initial value
    str  r2, [r1, #oNFCONF]
    ldr  r2, [r1, #oNFCONF]
    bic  r2, r2, #0x800    @ enable chip
    str  r2, [r1, #oNFCONF]
    mov r2, #0xff    @ RESET command
    strb r2, [r1, #oNFCMD]
    mov r3, #0          @ wait
1:   add r3, r3, #0x1
    cmp r3, #0xa
    blt  1b
2:   ldr  r2, [r1, #oNFSTAT]    @ wait ready
    tst  r2, #0x1
    beq 2b
    ldr  r2, [r1, #oNFCONF]
    orr  r2, r2, #0x800    @ disable chip
    str  r2, [r1, #oNFCONF]
  
    @ get read to call C functions (for nand_read())
    ldr  sp, DW_STACK_START   @ setup stack pointer
    mov fp, #0          @ no previous frame, so fp=0
  
    @ copy vivi to RAM
    ldr  r0, =VIVI_RAM_BASE
    mov   r1, #0x0
    mov r2, #0x20000
    bl   nand_read_ll
  
    tst  r0, #0x0
    beq ok_nand_read
#ifdef CONFIG_DEBUG_LL
bad_nand_read:
    ldr  r0, STR_FAIL
    ldr  r1, SerBase
    bl   PrintWord
1:   b   1b      @ infinite loop
#endif
ok_nand_read:
#ifdef CONFIG_DEBUG_LL
    ldr  r0, STR_OK
    ldr  r1, SerBase
    bl   PrintWord
#endif
  
    @ verify
    mov r0, #0
    ldr  r1, =0x33f00000
    mov r2, #0x400   @ 4 bytes * 1024 = 4K-bytes
go_next:
    ldr  r3, [r0], #4
    ldr  r4, [r1], #4
    teq  r3, r4
    bne notmatch
    subs r2, r2, #4
    beq done_nand_read  
    bne go_next
notmatch:
#ifdef CONFIG_DEBUG_LL
    sub r0, r0, #4
    ldr  r1, SerBase
    bl   PrintHexWord
    ldr  r0, STR_FAIL
    ldr  r1, SerBase
    bl   PrintWord
#endif
1:   b   1b
done_nand_read:
  
#ifdef CONFIG_DEBUG_LL
    ldr  r0, STR_OK
    ldr  r1, SerBase
    bl   PrintWord
#endif
  
    mov pc, r10
  
@ clear memory
@ r0: start address
@ r1: length
mem_clear:
    mov r2, #0
    mov r3, r2
    mov r4, r2
    mov r5, r2
    mov r6, r2
    mov r7, r2
    mov r8, r2
    mov r9, r2
  
clear_loop:
    stmia    r0!, {r2-r9}
    subs r1, r1, #(8 * 4)
    bne clear_loop
  
    mov pc, lr
  
#endif @ CONFIG_S3C2410_NAND_BOOT3.3進入C代碼

  首先要設置堆棧指針sp,堆棧指針的設置是爲了執行C語言代碼作好準備。設置好堆棧後,調用C語言的main函數:

@ get read to call C functions
ldr  sp, DW_STACK_START   @ setup stack pointer
mov fp, #0          @ no previous frame, so fp=0
mov a2, #0          @ set argv to NULL
  
bl   main           @ call main
  
mov pc, #FLASH_BASE       @ otherwise, reboot

  4. BootLoader第二階段

  vivi Bootloader的第二階段又分成了八個小階段,在main函數中分別調用這幾個小階段的相關函數:

int main(int argc, char *argv[])
{
    int ret;
  
    /*
    * Step 1:
    */
    putstr("rn");
    putstr(vivi_banner);
  
    reset_handler();
  
    /*
    * Step 2:
    */
    ret = board_init();
    if (ret) {
       putstr("Failed a board_init() procedurern");
       error();
    }
  
    /*
    * Step 3:
    */
    mem_map_init();
    mmu_init();
    putstr("Succeed memory mapping.rn");
  
    /*
    * Now, vivi is running on the ram. MMU is enabled.
    */
  
    /*
    * Step 4:
    */
    /* initialize the heap area*/
    ret = heap_init();
    if (ret) {
       putstr("Failed initailizing heap regionrn");
       error();
    }
  
    /* Step 5:
    */
    ret = mtd_dev_init();
  
    /* Step 6:
    */
    init_priv_data();
  
    /* Step 7:
    */
    misc();
  
    init_builtin_cmds();
  
    /* Step 8:
    */
    boot_or_vivi();
  
    return 0;
} STEP1的putstr(vivi_banner)語句在串口輸出一段字符說明vivi的版本、作者等信息,vivi_banner定義爲:

const char *vivi_banner =
            "VIVI version " VIVI_RELEASE " (" VIVI_COMPILE_BY "@"
            VIVI_COMPILE_HOST ") (" VIVI_COMPILER ") " UTS_VERSION "rn";

  reset_handler進行相應的復位處理:

void
reset_handler(void)
{
    int pressed;
  
    pressed = is_pressed_pw_btn();
  
    if (pressed == PWBT_PRESS_LEVEL) {
       DPRINTK("HARD RESETrn");
       hard_reset_handle();
    } else {
       DPRINTK("SOFT RESETrn");
       soft_reset_handle();
    }
}

  hard_reset_handle會clear內存,而軟件復位處理則什麼都不做:

static void
hard_reset_handle(void)
{
    clear_mem((unsigned long)USER_RAM_BASE, (unsigned long)USER_RAM_SIZE);
}

  STEP2進行板初始化,設置時間和可編程I/O口:

int board_init(void)
{
    init_time();
    set_gpios();
  
    return 0;
}

  STEP3進行內存映射及MMU初始化:

void mem_map_init(void)
{
#ifdef CONFIG_S3C2410_NAND_BOOT
    mem_map_nand_boot();
#else
    mem_map_nor();
#endif
    cache_clean_invalidate();
    tlb_invalidate();
}要調用通用的arm920 MMU初始化函數:

static inline void arm920_setup(void)
{
    unsigned long ttb = MMU_TABLE_BASE;
  
__asm__(
    /* Invalidate caches */
    "mov    r0, #0n"
    "mcr    p15, 0, r0, c7, c7, 0n" /* invalidate I,D caches on v4 */
    "mcr    p15, 0, r0, c7, c10, 4n" /* drain write buffer on v4 */
    "mcr    p15, 0, r0, c8, c7, 0n" /* invalidate I,D TLBs on v4 */
    /* Load page table pointer */
    "mov    r4, %0n"
    "mcr    p15, 0, r4, c2, c0, 0n" /* load page table pointer */
    /* Write domain id (cp15_r3) */
    "mvn    r0, #0n"        /* Domains 0, 1 = client */
    "mcr    p15, 0, r0, c3, c0, 0n" /* load domain access register */
    /* Set control register v4 */
    "mrc    p15, 0, r0, c1, c0, 0n" /* get control register v4 */
    /* Clear out 'unwanted' bits (then put them in if we need them) */
                     /* .RVI ..RS B... .CAM */
    "bic r0, r0, #0x3000n"     /* ..11 .... .... .... */
    "bic r0, r0, #0x0300n"     /* .... ..11 .... .... */
    "bic r0, r0, #0x0087n"     /* .... .... 1... .111 */
    /* Turn on what we want */
    /* Fault checking enabled */
    "orr r0, r0, #0x0002n"     /* .... .... .... ..1. */
#ifdef CONFIG_CPU_D_CACHE_ON
    "orr r0, r0, #0x0004n"     /* .... .... .... .1.. */
#endif 
#ifdef CONFIG_CPU_I_CACHE_ON
    "orr r0, r0, #0x1000n"     /* ...1 .... .... .... */
#endif 
    /* MMU enabled */
    "orr r0, r0, #0x0001n"     /* .... .... .... ...1 */
    "mcr    p15, 0, r0, c1, c0, 0n" /* write control register */
    : /* no outputs */
    : "r" (ttb) );
}STEP4設置堆棧;STEP5進行mtd設備的初始化,記錄MTD分區信息;STEP6設置私有數據;STEP7初始化內建命令。

  STEP8啓動一個SHELL,等待用戶輸出命令並進行相應處理。在SHELL退出的情況下,啓動操作系統:

#define DEFAULT_BOOT_DELAY    0x30000000
void boot_or_vivi(void)
{
    char c;
    int ret;
    ulong boot_delay;
  
    boot_delay = get_param_value("boot_delay", &ret);
    if (ret) boot_delay = DEFAULT_BOOT_DELAY;
    /* If a value of boot_delay is zero,
    * unconditionally call vivi shell */
    if (boot_delay == 0) vivi_shell();
  
    /*
    * wait for a keystroke (or a button press if you want.)
    */
    printk("Press Return to start the LINUX now, any other key for vivin");
    c = awaitkey(boot_delay, NULL);
    if (((c != 'r') && (c != 'n') && (c != ''))) {
       printk("type "help" for help.n");
       vivi_shell();
    }
    run_autoboot();
  
    return;
}

  SHELL中讀取用戶從串口輸出的命令字符串,執行該命令:

void
vivi_shell(void)
{
#ifdef CONFIG_SERIAL_TERM
    serial_term();
#else
#error there is no terminal.
#endif
}
void serial_term(void)
{
    char cmd_buf[MAX_CMDBUF_SIZE];
  
    for (;;) {
       printk("%s> ", prompt);
  
       getcmd(cmd_buf, MAX_CMDBUF_SIZE);
  
       /* execute a user command */
       if (cmd_buf[0])
           exec_string(cmd_buf);
    }
}5.電路板調試

  在電路板的調試過程中,我們首先要在ADT新建的工程中添加第一階段的彙編代碼head.S文件,修改Link腳本,將代碼和數據映射到S3C2410A自帶的0x40000000開始的4KB內存空間內:

SECTIONS
{
    . = 0x40000000;
    .text : { *(.text) }
    Image_RO_Limit = .;
    Image_RW_Base = .;
    .data : { *(.data) }
    .rodata : { *(.rodata) }
    Image_ZI_Base = .;
    .bss : { *(.bss) }
    Image_ZI_Limit = .;
    __bss_start__ = .;
    __bss_end__ = .;
    __EH_FRAME_BEGIN__ = .;
    __EH_FRAME_END__ = .;
PROVIDE (__stack = .);
    end = .;
    _end = .;
    .debug_info   0 : { *(.debug_info) }
   .debug_line      0 : { *(.debug_line) }
   .debug_abbrev  0 : { *(.debug_abbrev)}
   .debug_frame  0 : { *(.debug_frame) }
}

  藉助萬用表、示波器等儀器儀表,調通SDRAM,並將vivi中自帶的串口、NAND FLASH驅動添加到工程中,調試通過板上的串口和FLASH。如果板電路的原理與三星公司DEMO板有差距,則vivi中硬件的操作要進行相應的修改。全部調試通過後,修改vivi源代碼,重新編譯vivi,將其燒錄入NAND FLASH就可以在復位後啓動這個Bootloader了。

  調試板上的新增硬件時,宜在ADT中添加相應的代碼,在不加載操作系統的情況下,單純地操作這些硬件。如果電路板設計有誤,要進行飛線和割線等處理。

  6.小結

  本章講解了ARM彙編、Bootloader的功能,Bootloader的調試環境及ARM電路板的調試方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章