系统移植喝喝

目录

系统移植的目的    

系统移植过程

开发板启动过程

网络配置

tftp(主打传输)

nfs (主打共享)

uboot概述

Bootloader

Bootloader基本功能 

常见的Bootloader

SD卡存储结构

uboot的使用

uboot模式

uboot命令

帮助命令

 环境变量命令

网络传输命令

存储器访问命令(内存<=>外存)

uboot常用环境变量

uboot自启动环境变量

Linux内核的安装与加载

uboot内核启动命令

uboot自启动参数环境变量

EMMC加载Linux内核及rootfs 

tftp加载Linux内核nfs挂载rootfs

交叉编译工具链

编译原理

GCC编译过程

交叉编译  

交叉编译工具链

ELF文件格式

ELF    

ELF文件相关命令

BIN文件格式

交叉编译工具链常用工具

uboot源码结构

uboot源码获取(开源)

uboot特点 

uboot源码结构

uboot的配置与编译

uboot配置

 uboot编译

uboot移植深化

一、建立自己的平台

二、uboot添加三星加密引导

三、实现串口输出

四、网卡移植

五、EMMC移植

六、电源管理移植

Linux内核

内核与操作系统

 Linux层次结构

Linux内核特点

Linux内核源码结构

Linux内核源码获取

平台相关代码

平台无关代码

帮助文档、示例程序、工具等    

Linux内核的配置与编译

Linux内核源码配置

指定处理器架构及编译工具    

导入当前处理器的默认配置    

修改配置    

make menuconfig

Linux内核源码编译

Linux内核驱动移植

设备树

设备树文件    

设备树语法

根文件系统

根文件系统内容

BusyBox


实现内容:在基于ARM处理器的开发板上安装Linux系统

Linux提供:进程管理 内存管理 网络接口 文件管理 设备管理

系统移植的目的    

        不同架构的处理器指令集不兼容,即便是相同的处理器架构,板卡不同驱动代码也不兼容(平台相关代码)

        Linux是一个通用的内核,并不是为某一个特定的处理器架构或板卡设计的,所以从官方获取Linux源码后,我们要先经过相应的配置,使其与我们当前的硬件平台相匹配后,才能进行编译和安装。

系统移植过程

Windows装机  

  1. 准备Windows系统镜像、U盘启动盘
  2. 进入BIOS(Basic Input Output System)选择启动方式(U盘启动)
  3. 通过U盘中的引导程序安装系统  
  4. 安装Windows驱动程序  
  5. 安装Windows应用程序

Linux系统移植  

  1. 准备Linux内核镜像、SD卡启动盘
  2.  通过拨码开关选择启动方式(SD启动)
  3. 通过SD卡中的引导程序安装系统
  4. 安装Linux驱动程序
  5. 安装Linux应用程序

开发板启动过程

  • 开发板上电后首先运行SOC内部iROM中固化的代码(BL0),这段代码先对基本的软硬件环境(时钟等...)进行初始化,然后再检测拨码开关位置获取启动方式(SD/EMMC),然后再将对应存储器中的uboot搬移到内存,然后跳转到uboot运行  
  • uboot开始运行后首先对开发板上的软硬件环境做进一步初始化,然后将linux内核、设备树(dtb)【Linux + 设备树 才是一个完整的Linux内核】、根文件系统(rootfs)从外部存储器(或网络)搬移到内存,然后跳转到linux运行  
  • linux开始运行后先对系统环境做初始化,当系统启动完成后,Linux再从内存中(或网络)挂载根文件系统

系统移植步骤

  • uboot移植  
  • linux内核移植(包含设备树)  
  • 根文件系统移植

网络配置

tftp(主打传输)

tftp(Trivial File Transfer Protocol)即简单文件传输协议 是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为69

nfs (主打共享)

nfs(Network File System)即网络文件系统,其基于UDP/IP使用nfs能够在不同计算机之间通过网络进行文件共享,能使使用 者访问网络上其它计算机中的文件就像在访问自己的计算机一样。

uboot概述

Bootloader

在操作系统运行之前运行的一小段代码,用于将软硬件环境初始化到一个合适的状态,为操作系统的加载和运行做准备(其本身不是操作系统)

Bootloader基本功能 

  • 初始化软硬件环境
  • 引导加载linux内核
  • 给linux内核传参
  • 执行用户命令

注:bootloader是启动引导程序的统称,嵌入式linux常用的bootloader是uboot

常见的Bootloader

SD卡存储结构

SD卡的存储以扇区为单位,每个扇区的大小为512Byte, 其中零扇区存储分区表(即分区信息),后续的扇区可自行分区和格式化;

若选择SD卡启动,处理器上电后从第一个扇区开始将其中的内容搬移到内存,所以我们把uboot放到从第一个扇区开始之后的空间, 之后的空间根据个人需求可进行分区和格式化

uboot的使用

uboot启动流程分析

启动流程

第一阶段:汇编阶段

  •     设置CPU工作模式,关中断,关MMU,关看门狗....
  •     初始化时钟,内存
  •     自搬运
  •     设置堆栈,清BSS段
  •     跳转到C环境执行

第二阶段:C阶段

  •     初始化GPIO
  •     初始化用于调试的网口或串口等必要的硬件
  •     执行CMD命令
  •     加载内核

启动分析

【uboot代码流程 version:2013】
1、查找uboot入口函数
   1)通过链接文件(lds)文件查找启动入口函数,路径在(arch\arm\cpu\u-boot.lds),查找到入口函数是_start (ENTRY(_start))
   2)通过全局搜索(或者看链接文件里面的text断里面的start.o),找到_start在start.S下(arch\arm\cpu\armv7\start.S)
   ps:这里之所以取armv7下面的start,主要是由于目标板cpu是armv7下面的cpu。
2、分析代码流程 
   _start
      --reset
        --设置cpu模式,关中断
        --异常表基地址设置
        --cpu_init_cp15(mmu关闭)
        --cpu_init_crit
          --lowlevel_init(board\samsung\origen\lowlevel_init.S)
            --system_clock_init 时钟初始化(这里不执行)
--mem_ctrl_asm_init 内存初始化(这里不执行)
            --uart_asm_init  串口初始化
--tzpc_init  tzpc初始化
        --_main(arch\arm\lib\crt0.S)
          --为进入C做准备
          --relocated(自搬移,uboot没有在内存里面运行)
          --栈的初始化及 bss的清理
          --board_init_r(arch\arm\lib\board.c)
            --目标板硬件相关初始化
            --main_loop(for循环进入等待处理命令,待输入加载内核命令后,uboot将操作权限交给内核)
【uboot代码流程version:2016】
找_start   在 \arch\arm\lib\vectors.S:定义异常向量表
找reset   在arch\arm\cpu\armv7\start.S 启动代码
         ldr    r0, =_start  获取uboot加载地址
         mcr    p15, 0, r0, c12, c0, 0    @设置vector向量表到start地址。

uboot模式

  • 自启动模式 :uboot启动后若没有用户介入,倒计时结束后会自动执行自启动环境变量(bootcmd)中设置的命令(一般作加载和启动内核)       
  • 交互模式 :倒计时结束之前按下任意按键uboot会进入交互模式,交互模式下用户可输入uboot命令

uboot命令

帮助命令

help     查看uboot支持的所有命令       

        help  [命令]     查看当前命令的使用方法    

 环境变量命令

printenv     打印uboot中所有的环境变量       

setenv       设置指定的环境变量(保存在RAM中,断电会丢失,重启会重置)   

         setenv [环境变量] [环境变量的值]      

saveenv    保存所有环境变量到EMMC中(会保存至外部存储器中,断电不丢失)

网络传输命令

loadb     通过Kermit协议下载文件到指定的内存地址       loadb  [地址]    

tftp         通过tftp协议下载文件到指定的内存地址            tftp  [地址]  [(服务器ip:)文件名]   

        例:tftp 0x41000000 interface.bin 

 注:使用tftp之前要配置好网络tftp服务器   【网络:桥接模式 选择网卡(桥接到pcie)】

        要在同一网段中,并且serverip就是Ubuntu的ip地址。如果未联通试试关闭防火墙

存储器访问命令(内存<=>外存)

mmc read    

将EMMC中指定扇区中的内容读取到内存中指定的地址     

mmc read  <addr> <blk#> <cnt>         

        addr:    内存地址         

        blk#:    EMMC中的扇区编号         

        cnt:    读取的扇区的个数

mmc write    

将内存中指定地址中的内容写入到EMMC中指定的扇区        

        mmc write <addr> <blk#> <cnt>

 

uboot常用环境变量

  • ipaddr           uboot的IP地址  
  • serverip        服务器的IP地址(即ubuntu的IP)      
  • bootdelay     进入自启动模式之前倒计时的秒数

uboot自启动环境变量

bootcmd     自启动的环境变量

该环境变量可以设置成一到多个uboot命令的集合(若有多个使用\;分割)

自启动模式下uboot就会按照bootcmd中命令的顺序逐条执行    (无需干预自动执行) 

eg:     setenv bootcmd tftp 40008000 interface.bin\;go 40008000         

          saveenv

eg:  setenv bootcmd mmc read 0 40008000 0x800 0x1\;go 40008000

          saveenv

Linux内核的安装与加载

uboot内核启动命令

bootm     启动指定内存地址上的Linux内核并为内核传递参数     

bootm kernel-addr ramdisk-addr dtb-addr     

注:       

  • kernel-addr:    内核的下载地址           
  • ramdisk-addr:    根文件系统的下载地址               
  • dtb-addr:        设备树的下载地址         

若不使用相应的地址,对应的位置写“-”   

eg:         bootm 0x41000000 - 0x42000000    

uboot自启动参数环境变量

bootargs

是uboot用来给Linux内核传递参数的环境变量。

eg:     setenv bootargs root=/dev/nfs nfsroot=xxx.xxx.xxx.xxx(Ubuntu的ip):/opt/4412/rootfs   rw console=ttySAC2,115200 init=/linuxrc ip=***.***.***.***(ipaddr)

setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/4412/rootfs rw console=ttySAC2,115200 init=/linuxrc ip=192.168.1.13

注:     

  • root        根文件系统类型(nfs)     
  • nfsroot        网络文件系统路径(xxx.xxx.xxx.xxx:/opt/4412/rootfs)     
  • rw          操作网络文件系统的权限(rw)     
  • console        控制台(使用串口2,波特率115200)     
  • init            init进程的位置(/linuxrc)     
  • ip              linux启动后自身的IP(***.***.***.***)

EMMC加载Linux内核及rootfs 

通过上述方式我们虽然可以加载 Linux 内核和根文件系统并正常运行,但上述方式我们是把
内核镜像、设备树、根文件系统镜像都放到了 tftp 服务器上,然后开发板上电之后再通过
tftp 去下载这些文件到开发板的内存中运行;但在实际做一个产品的时候我们不可能每次开
机都通过网络去服务器上下载这些镜像,所以以下步骤我们就将这些镜像安装到开发板上
的 EMMC 中,然后从 EMMC 启动内核。

1.给开发板重新上电,在 uboot 交互模式下,去下载并安装这些镜像
下载内核镜像到内存中
# tftp 0x41000000 uImage
将内核镜像写入到 EMMC 中指定的扇区
# mmc write 0 0x41000000 0x800 0x2000

 下载设备树到内存中

# tftp 0x41000000 exynos4412-fs4412.dtb
将设备树写入到 EMMC 中指定的扇区
# mmc write 0 0x41000000 0x2800 0x800

 下载根文件系统镜像到内存中

# tftp 0x41000000 ramdisk.img
将根文件系统镜像写入到 EMMC 中指定的扇区
# mmc write 0 0x41000000 0x3000 0x2000

 

 2.因为要从 EMMC 驱动,所以还要修改 uboot 的启动参数

# setenv bootcmd mmc read 0 0x41000000 0x800 0x2000\;mmc read 0 0x42000000 0x2800 0x800\;mmc read 0 0x43000000 0x3000 0x2000\;bootm 0x41000000 0x43000000

0x42000000

 若设置无法保存可尝试如下格式

# setenv bootcmd 'mmc read 0 0x41000000 0x800 0x2000;mmc read 0 0x42000000 0x2800
0x800;mmc read 0 0x43000000 0x3000 0x2000;bootm 0x41000000 0x43000000
0x42000000'

 设置完成后保存这些参数

# saveenv

3.给开发板重新上电,上电之后观察内核是否能通过 EMMC 加载和启动内核,启动完成

后是否能挂载根文件系统(ext2),在终端上输入 linux 下的 shell 命令测试是否正常

tftp加载Linux内核nfs挂载rootfs

后续我们会经常修改内核和设备数的源码,也会经常向根文件系统中添加一些驱动或应用程序,这样每次修改之后我们都要重新将内核或根文件系统安装到 EMMC 中, 步骤会比较繁琐,开发效率也比较低,所以在开发阶段我们经常使用的方式是通过 tftp 加载内核和设备树再通过 nfs 挂载根文件系统(后续课程都是使用这种方式),这样步骤简单,效率也比较高,待产品定型之后在安装到 EMMC中。
1、 将资料中“根文件系统镜像”目录下的“rootfs.tar”拷贝到 ubuntu 中 nfs 工作目录下
解压跟文件系统到 nfs 工作目录
$ sudo tar xvf rootfs.tar.xz
解压完成后删除原有压缩包
$ sudo rm rootfs.tar.xz
2、 修改 uboot 的启动参数
#setenv bootcmd tftp 0x41000000 uImage\;tftp 0x42000000 exynos4412-fs4412.dtb\;bootm 0x41000000 - 0x42000000
设置完成后保存这些参数
# saveenv
3、 重启 nfs 服务器
$ sudo service nfs-kernel-server restart
4. 给开发板重新上电,上电之后观察内核是否能通过 tftp 加载和启动内核,启动完成后是
否能挂载根文件系统(nfs),在终端上输入 linux 下的 shell 命令测试是否正常,若启
动正常我们向 nfs 的工作目录中添加了新的文件之后可以在开发板直接看到。

将 uboot 安装到 EMMC
以上的几种方式中我们既可以通过网络启动内核也可以将内核安装到 EMMC 中从本地
启动,但是对于 uboot 我们一直使用的是从 SD 卡启动,所以我们也可以将 uboot 安
装到 EMMC 中然后从 EMMC 启动 uboot,这样就可以不使用 SD 卡了。
1.将资料中“u-boot 镜像”目录下的“u-boot-fs4412.bin”拷贝到 ubuntu 中 tftp 工作目录
2、 给开发板重新上电,在 uboot 交互模式下,去下载并安装 uboot
# tftp 0x41000000 u-boot-fs4412.bin
将内核镜像安装到 EMMC 中指定的扇区
# emmc open 0
# mmc write 0 0x41000000 0x0 0x800
# emmc close 0

显示如下信息表示安装成功

 3、 关闭开发板电源,调整拨码开关位置为 EMMC 启动(SD卡启动为反向)

↑此为EMMC启动

 ↑此为SD卡启动

4.给开发板重新上电,观察 uboot 是否能正常启动

因为启动的是 EMMC 中的 uboot 所以环境变量还需要重新设置

 

后续研究思路:开源->移植->编译 

交叉编译工具链

编译原理

  • 机器码(二进制)是处理器能直接识别的语言,不同的机器码代表不同的运算指令,处理器能够识别哪些机器码是由处理器的硬件设计所决定的,不同的处理器机器码不同,所以机器码不可移植
  • 汇编语言是机器码的符号化,即汇编就是用一个符号来代替一条机器码,所以不同的处理器汇编也不一样,即汇编语言也不可移植  
  • C语言在编译时我们可以使用不同的编译器将C源码编译成不同架构处理器的汇编,所以C语言可以移植 

GCC编译过程

预处理(.c ->.i):注释删除 宏定义替换之后删除 头文件内容展开

编译(.i->.s):把处理过的c语言 编译成汇编语言

汇编(.s ->.o):把汇编语言文件编译为目标文件

链接:将目标文件和其他附加文件(库)链接起来,生成可执行文件。

        例:gcc tset.o v.o -o test

交叉编译  

指程序的编译和运行不在同一台机器上

比如我们现在:在 ubuntu 上编辑、编译。  在 开发板 上运行。

交叉编译工具链

交叉编译工具链的获取:     

        1) 官网获取(不推荐,需要自己进行复杂配置与编译)         http://ftp.gnu.org/gnu/gcc/     

        2) BSP板级开发支持包(推荐)         samsung、全志...  

交叉编译工具链的内容     

        1) 交叉编译工具         gcc/readelf/size/nm/strip/objcopy/objdump/addr2line     

        2) 库(lib)                     ARM架构的库

注:库是分平台的 具有平台属性

ELF文件格式

ELF    

ELF格式是Linux平台上应用最广泛的二进制工业标准之一(可执行文件) 。

ELF格式的文件内包含了很多个段不同的段存储了不同的信息;因为ELF格式的文件要通过Linux系统的加载和管理才能运行,所以除了最基本的代码段和数据段之外,其中还存储了很多其它的信息,如符号表、调试信息等。

ELF文件相关命令

  1. file     file + 文件名        查看文件的详细信息
  2. readelf    
    1. readelf -h + 文件名     列出elf文件的头部信息    
    2. readelf -a + 文件名     列出elf文件的所有信息

BIN文件格式

BIN文件一般是直接运行在CPU之上 的可执行文件,文件内只包含了CPU能够直接识别和运行的指令和数据,不包含其它系统相关的信息。

交叉编译工具链常用工具

size      列出目标文件每一段的大小以及总体的大小     size + 文件名            

        未初始化的全局变量存储在bss中 如 int a;(global)  后 bss=8

        初始化的全局变量存储在data中 如若int a=100;(global) 则 data=280

        局部变量不占用空间 ,但static int 同全局变量一样。未初始化的存储在bss中,初始化的存储在data中。

        语句存放在text中。如 a++;

nm        列出目标文件中的.symtab符号表(标示符)     nm + 文件名        

strip(瘦身命令)    丢弃目标文件中的符号,但是并不影响程序正常执行     strip + 文件名            

              注:对于嵌入式开发,这个命令很重要,节省磁盘空间。

objdump            从目标文件中显示信息        

eg:      objdump -d + 文件名    将目标文件反汇编(机器码->汇编)      

objcopy            对目标文件进行复制和转换    

eg:      objcopy --gap-fill=0xff -O binary a.out a.bin     将目标文件转换为bin格式

上述指令对于ARM架构来说,要使用交叉编译工具链中的 加个前缀arm-none-linux-gnueabi-***

 

uboot源码结构

uboot源码获取(开源)

  • uboot源码下载     http://www.denx.de/wiki/U-Boot/  
  • uboot版本命名     前期:uboot-1.2.3     现在:uboot-2008.01  
  • uboot版本选择     支持对应的硬件平台、相对成熟的版本(资料多)

uboot特点 

  • 代码结构清晰  
  • 支持丰富的处理器与开发板,易于移植  
  • 支持丰富的用户命令  
  • 支持丰富的网络协议  
  • 支持丰富的文件系统  
  • 支持丰富的设备驱动  
  • 更新活跃、用户较多、资料丰富  
  • 开放源代码  
  • 较高的稳定性  
  • 不具有通用性(不同的处理器、开发板uboot不可通用)

uboot源码结构

平台相关代码    

        即与CPU架构或开发板硬件相关的源码,硬件的改动对应的代码也需要进行修改    

                arch:  与CPU架构相关的源代码    

                board:与开发板相关的源代码,包含各种官方评估板对应的源码

平台无关代码

  • api:             应用接口    
  • common:    uboot命令源码    
  • disk:           对磁盘设备的支持    
  • drivers:       设备驱动源码    
  • fs:               对文件系统的支持        
  • include:      头文件    
  • lib:              库    
  • net:             对网络协议的支持    
  • post:           上电自检程序

配置文件、帮助文档、示例程序、工具等:

  • README:    说明文档    
  • doc:    帮助文档    
  • Makefile:    编译管理    
  • CREDITS:    开发者    
  • COPYING:    版权    
  • examples:    示例程序    
  • tools:    工具

uboot的配置与编译

uboot配置

  1. 指定当前使用的硬件平台     
    1. 格式:make <board_name>_config     
    2. 注1:<board_name>为当前使用的开发板的名字     
    3. 注2:执行该命令的前提是uboot源码支持该开发板     
    4. 注3:该命令必须在uboot源码的顶层目录下执行       
  2. 指定编译uboot源码使用的编译器     
    1. 在uboot源码顶层目录下的Makefile中指定(CROSS_COMPILE变量)

例:make origen_config

       CROSS_COMPILE ?=arm-none-linux-gnueabi-

 uboot编译

  1. 编译uboot   
    1.  make     
    2. 注1:该命令必须在uboot源码的顶层目录下执行     
    3. 注2:该命令执行后在uboot源码顶层目录下生成u-boot.bin
  2. 清除编译过程中生成的中间文件
    1. make clean(.o)
    2. make distclean     
    3. 注1:该命令必须在uboot源码的顶层目录下执行

uboot 经过 arm-none-linux-gnueabi-objcopy 变成bin文件

uboot移植深化

一、建立自己的平台

1.下载uboot源码

在uboot官网下载uboot源码(这里我们选择u-boot-2013.01.tar.bz2)

ftp://ftp.denx.de/pub/u-boot/

2.解压uboot源码

拷贝uboot源码包到ubuntu的家目录下,解压并进入其顶层目录

$ tar  xvf  u-boot-2013.01.tar.bz2

$ cd  u-boot-2013.01/

3.指定交叉编译工具信息

uboot源码并不知道我们使用的处理器架构及交叉编译工具是什么,这里我们需要自己 在Makefile中指定

$ vi  Makefile

ifeq ($(HOSTARCH),$(ARCH))

CROSS_COMPILE ?=

endif

修改为如下内容(注意后边不要有多余的空格),然后保存退出

ifeq (arm,arm)

CROSS_COMPILE ?= arm-none-linux-gnueabi-

endif

4.添加Board信息

因为uboot源码并不支持我们的开发板,这里我们需要从源码支持的开发板中找一个硬件与我们最类似的,在其基础上进行修改,这里我们参考的是samsung公司的origen

$ cp  -rf  board/samsung/origen/  board/samsung/fs4412

$ mv  board/samsung/fs4412/origen.c  board/samsung/fs4412/fs4412.c

因为修改了文件名,所以对应的Makefile也要修改

$ vi  board/samsung/fs4412/Makefile

ifndef CONFIG_SPL_BUILD

COBJS += origen.o

endif

修改为如下内容,然后保存退出

ifndef CONFIG_SPL_BUILD

COBJS += fs4412.o

endif

拷贝origen相关的头文件并将其重命名

$ cp  include/configs/origen.h  include/configs/fs4412.h

修改文件中的信息

$ vi  include/configs/fs4412.h

#define CONFIG_SYS_PROMPT "ORIGEN # "

修改为如下内容

#define CONFIG_SYS_PROMPT "fs4412 # "

再将

#define CONFIG_IDENT_STRING " for ORIGEN"

修改为如下内容,然后保存退出

#define CONFIG_IDENT_STRING " for fs4412"

打开uboot源码顶层目录下的boards.cfg文件

$ vi  boards.cfg

origen      arm  armv7      origen  samsung exynos

后添加如下内容(FS4412的相关信息),然后保存退出

fs4412      arm  armv7      fs4412  samsung exynos

至此我们在uboot源码中给我们的板子添加了“档案”,源码就支持我们的开发板了

5.编译uboot

在uboot源码顶层目录下执行如下命令,指定当前使用的Board信息

$ make  fs4412_config

编译uboot

$ make

编译完成后会在源码顶层目录下生成u-boot.bin文件,但该文件还不能在我们的开发板上运行,因为以上操作我们只是把origen相关的文件的名字改成了fs4412,使uboot能识别fs4412开发板,但文件中的代码还是origen的,和我们的开发板不匹配,所以我们还需要进一步进行修改和配置

二、uboot添加三星加密引导

考虑芯片启动的安全性,Exynos4412需要三星提供的初始引导加密后我们的u-boot才 能被引导运行,所以我们需要在uboot源码中添加三星提供的加密处理代码

1.添加三星加密引导方式

将资料中“移植相关文件”下的sdfuse_q和CodeSign4SecureBoot目录拷贝到uboot源 码的顶层目录下(这之后不要执行make clean或make distclean,这会将加密文件清除)

因为添加的加密文件也要编译,所以对应的Makefile也要修改

$ vi  Makefile 

$(obj)u-boot.bin: $(obj)u-boot

$(OBJCOPY) ${OBJCFLAGS} -O binary $< $@

$(BOARD_SIZE_CHECK)

后添加如下内容(添加的内容需要tab键缩进,否则编译报错),然后保存退出

@#./mkuboot

@split -b 14336 u-boot.bin bl2

@+make -C sdfuse_q/

@#cp u-boot.bin u-boot-4212.bin

@#cp u-boot.bin u-boot-4412.bin

@#./sdfuse_q/add_sign

@./sdfuse_q/chksum

@./sdfuse_q/add_padding

@rm bl2a*

@echo

2.添加调试代码(点灯法)

很多时候我们不确定uboot是否已经在板子上运行,所以我们在uboot源码中添加一段 代码使板子上的LED点亮,这样如果看到LED亮的话就表示uboot已经在运行了

打开uboot启动后的第一段代码

$ vi  arch/arm/cpu/armv7/start.S

在第134行后添加如下代码(即点亮LED2),然后保存退出

ldr r0, =0x11000c40

ldr r1, [r0]

bic r1, r1, #0xf0000000

orr r1, r1, #0x10000000

str r1, [r0]

ldr r0, =0x11000c44

mov r1, #0xff

str r1, [r0]

3.添加编译脚本

使用make命令编译时只链接uboot源码中的相关代码,而我们添加的初始引导加密的 代码不会被连接到u-boot.bin中,所以这里我们自己编写编译脚本build.sh,这个脚本 中除了对uboot源码进行配置和编译外还将初始引导加密代码链接到了u-boot.bin上, 最终生成一个完成的uboot镜像u-boot-fs4412.bin

将资料中“移植相关文件”下的build.sh拷贝到uboot源码的顶层目录下

给编译脚本添加可执行权限

$ chmod  777  build.sh

4.编译uboot

通过脚本编译uboot源码

$ ./build.sh

编译完成后在源码的顶层目录下会生成“u-boot-fs4412.bin”

5.测试uboot

参照之前的实验将生成的u-boot-fs4412.bin烧写到SD卡中,开发板选择SD卡启动, 然后上电查看现象,若LED2点亮则说明我自己移植的u-boot已经能够被加载运行

三、实现串口输出

虽然uboot已经能在开发板上加载运行,但是此时的uboot还不能在终端上打印信息, 原因在于uboot源码中对UART的配置与我们实际的硬件不匹配

1.修改UART源码

$ vi  board/samsung/fs4412/lowlevel_init.S

lowlevel_init:

后添加如下内容(初始化临时栈)

ldr  sp,=0x02060000

beq wakeup_reset

后添加如下内容(关闭看门狗)

#if 1

ldr r0, =0x1002330c

ldr r1, [r0]

orr r1, r1, #0x300

str r1, [r0]

ldr r0, =0x11000c08

ldr r1, =0x0

str r1, [r0]

/* Clear  MASK_WDT_RESET_REQUEST  */

ldr r0, =0x1002040c

ldr r1, =0x00

str r1, [r0]

#endif

uart_asm_init:

/* setup UART0-UART3 GPIOs (part1) */

mov r0, r7

ldr r1, =EXYNOS4_GPIO_A0_CON_VAL

str r1, [r0, #EXYNOS4_GPIO_A0_CON_OFFSET]

ldr r1, =EXYNOS4_GPIO_A1_CON_VAL

str r1, [r0, #EXYNOS4_GPIO_A1_CON_OFFSET]

后添加如下内容(UART初始化)

ldr r0, =0x10030000

ldr r1, =0x666666

ldr r2, =CLK_SRC_PERIL0_OFFSET

str r1, [r0, r2]

ldr r1, =0x777777

ldr r2, =CLK_DIV_PERIL0_OFFSET

str r1, [r0, r2]

注释掉

bl uart_asm_init

后的一条语句,然后保存退出

#if 0

bl tzpc_init

#endif

2.编译uboot

通过脚本编译uboot源码

$ ./build.sh

3.测试uboot

参照之前的实验将生成的u-boot-fs4412.bin烧写到SD卡中,开发板选择SD卡启动, 然后上电查看现象,若终端有打印信息则说明UART移植成功。

四、网卡移植

虽然可以通过终端输入命令,但此时的uboot还不能使用ping、tftp等命令,原因在于命令都是操作网络的,而uboot源码中网卡的相关配置与我们当前的板子不匹配,所以 我们还要对网卡进行移植

1.修改网络初始化代码

$ vi  board/samsung/fs4412/fs4412.c

struct exynos4_gpio_part2 *gpio2;

后添加如下内容

#ifdef  CONFIG_DRIVER_DM9000

#define EXYNOS4412_SROMC_BASE 0X12570000

#define DM9000_Tacs     (0x1)

#define DM9000_Tcos     (0x1)

#define DM9000_Tacc     (0x5)

#define DM9000_Tcoh     (0x1)

#define DM9000_Tah      (0xC)

#define DM9000_Tacp     (0x9)   

#define DM9000_PMC     (0x1)  

struct exynos_sromc {

unsigned int bw;

unsigned int bc[6];

};

void exynos_config_sromc(u32 srom_bank, u32 srom_bw_conf, u32 srom_bc_conf)

{

unsigned int tmp;

struct exynos_sromc *srom = (struct exynos_sromc *)(EXYNOS4412_SROMC_BASE);

/* Configure SMC_BW register to handle proper SROMC bank */

tmp = srom->bw;

tmp &= ~(0xF << (srom_bank * 4));

tmp |= srom_bw_conf;

srom->bw = tmp;

/* Configure SMC_BC register */

srom->bc[srom_bank] = srom_bc_conf;

}

static void dm9000aep_pre_init(void)

{

unsigned int tmp;

unsigned char smc_bank_num = 1;

unsigned int     smc_bw_conf=0;

unsigned int     smc_bc_conf=0;

       

/* gpio configuration */

writel(0x00220020, 0x11000000 + 0x120);

writel(0x00002222, 0x11000000 + 0x140);

/* 16 Bit bus width */

writel(0x22222222, 0x11000000 + 0x180);

writel(0x0000FFFF, 0x11000000 + 0x188);

writel(0x22222222, 0x11000000 + 0x1C0);

writel(0x0000FFFF, 0x11000000 + 0x1C8);

writel(0x22222222, 0x11000000 + 0x1E0);

writel(0x0000FFFF, 0x11000000 + 0x1E8);              

smc_bw_conf &= ~(0xf<<4);

smc_bw_conf |= (1<<7) | (1<<6) | (1<<5) | (1<<4);

smc_bc_conf = ((DM9000_Tacs << 28)

 | (DM9000_Tcos << 24)

 | (DM9000_Tacc << 16)

 | (DM9000_Tcoh << 12)

 | (DM9000_Tah  << 8)

 | (DM9000_Tacp << 4)

 | (DM9000_PMC));

exynos_config_sromc(smc_bank_num,smc_bw_conf,smc_bc_conf);

}

#endif

gd->bd->bi_boot_params = (PHYS_SDRAM_1 + 0x100UL);

后添加如下内容

#ifdef CONFIG_DRIVER_DM9000

dm9000aep_pre_init();

#endif

在文件末尾添加如下内容,然后保存退出

#ifdef CONFIG_CMD_NET

int board_eth_init(bd_t *bis)                                                  

{      

int rc = 0;

#ifdef CONFIG_DRIVER_DM9000

rc = dm9000_initialize(bis);                                            

#endif                                                                         

return rc;                                                              

}  

#endif

2.修改网络配置代码

$ vi  include/configs/fs4412.h

#undef CONFIG_CMD_PING

修改为

#define CONFIG_CMD_PING

再将

#undef CONFIG_CMD_NET

修改为

#define CONFIG_CMD_NET

在文件末尾

#endif /* __CONFIG_H */

前添加如下内容,然后保存退出

#ifdef  CONFIG_CMD_NET

#define CONFIG_NET_MULTI

#define CONFIG_DRIVER_DM9000 1

#define CONFIG_DM9000_BASE 0x05000000

#define DM9000_IO CONFIG_DM9000_BASE

#define DM9000_DATA (CONFIG_DM9000_BASE + 4)

#define CONFIG_DM9000_USE_16BIT

#define CONFIG_DM9000_NO_SROM 1

#define CONFIG_ETHADDR 11:22:33:44:55:66

#define CONFIG_IPADDR 192.168.9.200

#define CONFIG_SERVERIP 192.168.9.120

#define CONFIG_GATEWAYIP 192.168.9.1

#define CONFIG_NETMASK 255.255.255.0

#endif

3.编译uboot

通过脚本编译uboot源码

$ ./build.sh

4.测试uboot

参照之前的实验将生成的u-boot-fs4412.bin烧写到SD卡中,开发板选择SD卡启动, 然后上电查看现象;设置好相关的环境变量,使用网线连接开发板与开发主机,使用 ping命令连接ubuntu,若显示“host xxx.xxx.xxx.xxx is alive”则表示网卡移植成功

五、EMMC移植

因为uboot源码中对EMMC的配置与我们的板子不匹配,这里还需要对EMMC相关的代码进行修改和配置

1.修改EMMC初始化代码

将资料中“移植相关文件”下的movi.c拷贝到uboot源码的arch/arm/cpu/armv7/exynos/ 目录下

因为添加的新文件也要编译,所以对应的Makefile也要修改

$ vi  arch/arm/cpu/armv7/exynos/Makefile

COBJS += clock.o power.o soc.o system.o pinmux.o

修改为如下内容,然后保存退出

COBJS += clock.o power.o soc.o system.o pinmux.o movi.o

修改板级文件

$ vi  board/samsung/fs4412/fs4412.c

#include <asm/arch/mmc.h>

后添加如下内容

#include <asm/arch/clk.h>

#include "origen_setup.h"

#ifdef CONFIG_GENERIC_MMC

后添加如下内容

u32 sclk_mmc4;  /*clock source for emmc controller*/

#define __REGMY(x) (*((volatile u32 *)(x)))

#define CLK_SRC_FSYS  __REGMY(EXYNOS4_CLOCK_BASE + CLK_SRC_FSYS_OFFSET)

#define CLK_DIV_FSYS3 __REGMY(EXYNOS4_CLOCK_BASE + CLK_DIV_FSYS3_OFFSET)

int emmc_init()

{

u32 tmp;

u32 clock;

u32 i;

/* setup_hsmmc_clock */

/* MMC4 clock src = SCLKMPLL */

tmp = CLK_SRC_FSYS & ~(0x000f0000);

CLK_SRC_FSYS = tmp | 0x00060000;

/* MMC4 clock div */

tmp = CLK_DIV_FSYS3 & ~(0x0000ff0f);

clock = get_pll_clk(MPLL)/1000000;

for(i=0 ; i<=0xf; i++)  {

sclk_mmc4=(clock/(i+1));

if(sclk_mmc4 <= 160) //200

{

CLK_DIV_FSYS3 = tmp | (i<<0);

break;

}

}

emmcdbg("[mjdbg] sclk_mmc4:%d MHZ; mmc_ratio: %d\n",sclk_mmc4,i);

sclk_mmc4 *= 1000000;

/*

* MMC4 EMMC GPIO CONFIG

*

* GPK0[0] SD_4_CLK

* GPK0[1] SD_4_CMD

* GPK0[2] SD_4_CDn

* GPK0[3:6] SD_4_DATA[0:3]

*/

writel(readl(0x11000048)&~(0xf),0x11000048); //SD_4_CLK/SD_4_CMD pull-down enable

writel(readl(0x11000040)&~(0xff),0x11000040);//cdn set to be output

writel(readl(0x11000048)&~(3<<4),0x11000048); //cdn pull-down disable

writel(readl(0x11000044)&~(1<<2),0x11000044); //cdn output 0 to shutdown the emmc power

writel(readl(0x11000040)&~(0xf<<8)|(1<<8),0x11000040);//cdn set to be output

udelay(100*1000);

writel(readl(0x11000044)|(1<<2),0x11000044); //cdn output 1

writel(0x03333133, 0x11000040);

writel(0x00003FF0, 0x11000048);

writel(0x00002AAA, 0x1100004C);

#ifdef CONFIG_EMMC_8Bit

writel(0x04444000, 0x11000060);

writel(0x00003FC0, 0x11000068);

writel(0x00002AAA, 0x1100006C);

#endif

#ifdef USE_MMC4

smdk_s5p_mshc_init();

#endif

}

将board_mmc_init函数中的内容修改为(之前的内容删除即可)如下内容

int board_mmc_init(bd_t *bis)

{

int i, err;

#ifdef CONFIG_EMMC

err = emmc_init();

#endif

return err;

}

在文件的最末尾添加如下内容,然后保存退出

#ifdef CONFIG_BOARD_LATE_INIT

#include <movi.h>

int  chk_bootdev(void)//mj for boot device check

{

char run_cmd[100];

struct mmc *mmc;

int boot_dev = 0;

int cmp_off = 0x10;

ulong  start_blk, blkcnt;

mmc = find_mmc_device(0);

if (mmc == NULL)

{

printf("There is no eMMC card, Booting device is SD card\n");

boot_dev = 1;

return boot_dev;

  }

start_blk = (24*1024/MOVI_BLKSIZE);

blkcnt = 0x10;

sprintf(run_cmd,"emmc open 0");

run_command(run_cmd, 0);

sprintf(run_cmd,"mmc read 0 %lx %lx %lx",CFG_PHY_KERNEL_BASE,start_blk,blkcnt);

run_command(run_cmd, 0);

/* switch mmc to normal paritition */

sprintf(run_cmd,"emmc close 0");

run_command(run_cmd, 0);

return 0;

}

int board_late_init (void)

{

int boot_dev =0 ;

char boot_cmd[100];

boot_dev = chk_bootdev();

if(!boot_dev)

{

printf("\n\nChecking Boot Mode ... EMMC4.41\n");

}

return 0;

}

#endif

2.添加EMMC命令

将资料中“移植相关文件”下的cmd_movi.c、cmd_mmc.c、cmd_mmc_fdisk.c拷贝到uboot 源码的common/目录下

因为添加的新文件也要编译,所以对应的Makefile也要修改

$ vi  common/Makefile

COBJS-$(CONFIG_CMD_MMC) += cmd_mmc.o

后添加如下内容,然后保存退出

COBJS-$(CONFIG_CMD_MMC) += cmd_mmc_fdisk.o

COBJS-$(CONFIG_CMD_MOVINAND) += cmd_movi.o

将资料中“移植相关文件”下的mmc.c、s5p_mshc.c拷贝到uboot源码的drivers/mmc/ 目录下

将资料中“移植相关文件”下的mmc.h、movi.h、s5p_mshc.h拷贝到uboot源码的include/ 目录下

因为添加的新文件也要编译,所以对应的Makefile也要修改

$ vi  drivers/mmc/Makefile

COBJS-$(CONFIG_S5P_SDHCI) += s5p_sdhci.o

后添加如下内容,然后保存退出

COBJS-$(CONFIG_S5P_MSHC) += s5p_mshc.o

3.修改EMMC配置代码

 $ vi  include/configs/fs4412.h

在文件的末尾

 #endif /* __CONFIG_H */

前添加如下内容,然后保存退出

#define CONFIG_EVT1     1       /* EVT1 */

#ifdef CONFIG_EVT1

#define CONFIG_EMMC44_CH4 //eMMC44_CH4 (OMPIN[5:1] = 4)

#ifdef CONFIG_SDMMC_CH2

#define CONFIG_S3C_HSMMC

#undef DEBUG_S3C_HSMMC

#define USE_MMC2  

#endif

#ifdef CONFIG_EMMC44_CH4

#define CONFIG_S5P_MSHC

#define CONFIG_EMMC             1

#define USE_MMC4  

/* #define CONFIG_EMMC_8Bit */

#define CONFIG_EMMC_EMERGENCY

/*#define emmcdbg(fmt,args...) printf(fmt ,##args) */

#define emmcdbg(fmt,args...)

#endif

#endif /*end CONFIG_EVT1*/

#define CONFIG_CMD_MOVINAND

#define CONFIG_CLK_1000_400_200

#define CFG_PHY_UBOOT_BASE      CONFIG_SYS_SDRAM_BASE + 0x3e00000

#define CFG_PHY_KERNEL_BASE     CONFIG_SYS_SDRAM_BASE + 0x8000

#define BOOT_MMCSD      0x3

#define BOOT_EMMC43     0x6

#define BOOT_EMMC441    0x7

#define CONFIG_BOARD_LATE_INIT

4.编译uboot

通过脚本编译uboot源码

$ ./build.sh

5.测试uboot

参照之前的实验将生成的u-boot-fs4412.bin烧写到SD卡中,开发板选择SD卡启动, 然后上电查看现象;若显示EMMC的相关信息则表示EMMC移植成功

六、电源管理移植

因为uboot源码中对电源管理芯片的配置与我们的板子不匹配,后续有可能会导致内核 启动卡死,这里还需要对电源管理芯片相关的代码进行修改和配置

1.修改电源管理相关代码

将资料中“移植相关文件”下的pmic_s5m8767.c拷贝到uboot源码的drivers/power/pmic/ 目录下

因为添加的新文件也要编译,所以对应的Makefile也要修改

$ vi  drivers/power/pmic/Makefile

COBJS-$(CONFIG_POWER_MAX77686) += pmic_max77686.o

后添加如下内容,然后保存退出

COBJS-$(CONFIG_POWER_S5M8767) += pmic_s5m8767.o

将添加的函数在头文件中声明

$ vi  include/power/pmic.h

int pmic_set_output(struct pmic *p, u32 reg, int ldo, int on);

后添加如下内容,然后保存退出

void pmic_s5m8767_init(void);

修改配置文件

$ vi  include/configs/fs4412.h

在文件末尾

#endif /* __CONFIG_H */

前添加如下内容,然后保存退出

#define CONFIG_POWER_S5M8767

修改板级文件

$ vi  board/samsung/fs4412/fs4412.c

在board_init函数中

#ifdef CONFIG_DRIVER_DM9000

dm9000aep_pre_init();

#endif

后添加如下内容,然后保存退出

#ifdef CONFIG_POWER_S5M8767

pmic_s5m8767_init();

#endif

注释原有的代码

$ vi  drivers/power/Makefile

COBJS-$(CONFIG_POWER) += power_core.o

修改为(即注释掉)

#COBJS-$(CONFIG_POWER) += power_core.o

修改架构文件

$ vi  arch/arm/cpu/armv7/s5p-common/cpu_info.c

#include <asm/arch/clk.h>

后添加如下内容,然后保存退出

#include <power/pmic.h>

2.编译uboot

通过脚本编译uboot源码

$ ./build.sh

3、测试uboot

参照之前的实验将生成的u-boot-fs4412.bin烧写到SD卡中,开发板选择SD卡启动,然后上电查看现象

至此,uboot移植完成

Linux内核

内核与操作系统

  • 内核  :内核是一个操作系统的核心,提供了操作系统最基本的功能,是操作系统工作的基础,决定着整个系统的性能和稳定性  
  • 操作系统 :操作系统是在内核的基础上添加了各种工具集、桌面管理器、库、shell、应用程序等

如我们平时用的Ubuntu是系统 Linux是内核。 

系统调用是内核提供的功能,而调用库是来自于操作系统在内核的基础上添加的。

 Linux层次结构

Linux内核特点

  • Linux是微内核
  • 代码结构清晰、模块化设计

  • 支持丰富的硬件平台

  • 较高的稳定性

  • 轻量化及较强的裁剪性

  • 开放源代码

  • 更新活跃、用户较多、资料丰富

  • 支持丰富的网络协议

Linux内核源码结构

Linux内核源码获取
  • Linux内核源码下载     https://www.kernel.org/  
  • Linux内核版本命名     主版本号.次版本号.修订版本
  •  Linux内核版本选择    
    • 支持对应的硬件平台    
    • 相对成熟的版本(资料多)
    • 稳定版本(次版本号为偶数的版本一般都是稳定版)
平台相关代码

arch:与CPU架构相关的源代码

平台无关代码
  • block:磁盘设备的支持               crypto:加密相关    
  • drivers:设备驱动                       firmware:固件    
  • fs:文件系统                               include:头文件    
  • init:内核初始化                          ipc:进程间通信    
  • kernel:内核核心调度机制等      lib:库    
  • mm:内存管理                            net:网络协议    
  • scripts:工具、脚本等                security:安全    
  • usr:打包与压缩                         virt:虚拟
帮助文档、示例程序、工具等    
  • COPYING:        版权    
  • CREDITS:        内核贡献者    
  • README:        说明文档    
  • Documentation:    帮助文档    
  • Makefile:        编译管理    
  • samples:        示例    
  • tools:        工具

Linux内核的配置与编译

Linux内核源码配置

指定处理器架构及编译工具    

在Linux内核源码顶层目录下的Makefile中指定(ARCH、CROSS_COMPILE)  

导入当前处理器的默认配置    

make <soc_name>_defconfig    

注1:soc_name为当前使用的处理器的名字    

注2:内核源码的arch/arm/configs下对各个厂商的soc都有一个默认配置文件。执行该命令后就会将对应的配置文件中的信息导入到源码顶层目录下的.config。文件中CONFIG_xxx=y表示内核选中了该功能,内核编译时就会将该功能对应的代码编译,内核的体积也会增大。#CONFIG_xxx is not set表示内核没有选中该功能,内核编译时该功能对应的代码不会被编译,内核的体积也会减小。

修改配置    

默认配置只能保证内核拥有最基本的功能,我们需要根据自己的实际需求对内核做进一步的配置    

  • 方法1:直接修改.config文件(不推荐)
  • 方法2:make menuconfig
make menuconfig

修改配置    

[ ] 有两种状态         

  •         输入Y,显示“*”,内核中该功能被选中,相关代码会被编译进内核     
  •         输入N,显示“ ”,内核中该功能不被选中,相关代码不会被编译进内核    

< > 有三种状态         

  •         输入Y,显示“*”,内核中该功能被选中,相关代码会被编译进内核     
  •         输入N,显示“ ”,内核中该功能不被选中,相关代码不会被编译进内核        
  •         输入M,显示“M”,内核中该功能被选为模块(被编译为独立的模块)    

注:使用make menuconfig配置的本质还是修改.config文件

Linux内核源码编译

内核编译(以下命令均在内核源码的顶层目录下执行)    

  • make uImage         编译内核(编译选为“*”的选项到内核)    
  • make modules        编译内核模块(编译选为“M”的选项为独立模块)    
  • make dtbs               编译设备树(将设备树源文件dts编译为二进制文件dtb)    
  • make clean              删除编译过程中产生的中间文件

Linux内核驱动移植

  1. 在make menuconfig界面中选中要安装的驱动     
  2. 在设备树中添加/修改相应的设备信息     
  3. 重新编译内核/设备树

设备树

        设备树是一种描述硬件信息的数据结构,Linux内核运行时可以通过设备树将硬件信息直接传递给Linux内核,而不再需要在Linux内核中包含大量的冗余编码。

设备树文件    

  • dts  设备树源文件    
  • dtsi 类似于头文件,包含一些公共的信息,可被其它设备树文件引用    
  • dtb  编译后的设备树文件

设备树语法

        设备树的语法为树状结构,由一系列的节点和属性组成,根节点下包含子节点。子节点下还可以包含子节点,节点内部包含了对应设备的属性。

/{//根节点的孩子结点;
    memory{
        0x40000000(起始地址) 0x40000000(空间大小)
    };
    dm9000{
        0x05000000;//网卡地址
    };
    keypad{
        up{
            0x00000020;
            0x10000020;
            //两个可以控制上的引脚 也就是根结点的孙子结点
        };
        down;

        };
}

根文件系统

根文件系统是内核启动后挂载的第一个文件系统系统。引导程序会在根文件系统挂载后从中把一些基本的初始化脚本和服务等加载到内存中去运行。

根文件系统内容

  • bin         shell命令(elf格式)(通过busybox编译生成)  内核不包含shell命令 想使用这些命令则放进了根文件系统
  • dev        设备文件(内核启动后会将设备信息写入该目录)
  • etc         内核配置文件(启动后要读取配置信息)
  • lib          共享库(elf格式)(从交叉编译工具链中获取)  想在开发板上执行程序,就要把库放进根文件系统
  • linuxrc   内核运行的第一个应用程序(通过busybox编译生成)
  • mnt       挂载目录(非必要)
  • proc      进程相关文件(内核启动后会将进程信息写入该目录)
  • root       超级用户家目录(非必要)
  • sbin       系统管理shell命令(elf格式)(通过busybox编译生成)(super bin)
  • sys        驱动相关文件(内核启动后会将驱动信息写入该目录)
  • usr         shell命令(elf格式)(通过busybox编译生成)

BusyBox

        BusyBox将很多常用的工具集成到一个很小的可执行文件中,为普通用户提供大多数常用的命令,BusyBox实现的命令都是精简版的,很多扩展都不支持。BusyBox被称为Linux工具里的瑞士军刀 。

BusyBox的获取     https://busybox.net/downloads/

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值