用哪个BSP?

ST为他们STM3210E-EVAL的评估板制作了uClinux-dist-20080808的补丁。bootloader是一个莫名其妙的bin,kernel和rootfs放在片外nor flash里面,并且带有jffs。官方评估板又贵又买不到,不过我手上一块淘宝上一百多块钱的相似配置的板子也能玩。shell跑起来太慢了,跑个ls都要卡一两秒。

Emcraft为一些ST的板子做了第三方的BSP,其中包括STM32F429I-disc的uclinux BSP。虽然是给STM32F4板子写的,但是它里面也包括一些STM32F1的驱动,比方说USART、DMA。做一些修改还是可以用的。这份BSP血统比较纯正,用uboot作为bootloader,uclinux内核单独抽出来编译,应用程序用的busybox,包装为ROMFS的镜像。这些东西全部都在片内flash里就地执行,性能颇高,在429 discovery板子上跑的很欢快。

既然要build it from scratch,那就应该好好研究Emcraft的BSP,好好学学社区的玩法吧。github上有一个makefile,从头开始为429 discovery编译所有东西,它从robutest的仓库里面下载的uboot和uclinux,我猜Emcraft的东西来源也是这个,因为里面的驱动感觉是一样的。

首先在我买的板子上跑通整个系统,然后再移植到自己的板子上。别人的板子配置颇高,有2MB的片外SRAM跟4MB的片外NOR flash,还有64MB的nand flash,完全可以跑通。我的板子只有1MB的SRAM,加上一个256MB的nand flash;配置之所以那么诡异是因为我芯片只能淘宝。。。

淘宝上的板子
淘宝上的板子

淘宝上的板子

自己的板子1](uclinux-on-stm32f103/board2.jpg) ![自己的板子2
自己的板子1](uclinux-on-stm32f103/board2.jpg) ![自己的板子2

自己做的板子

工具链

老代码用老编译器,新代码用新编译器。比方说新版的uboot编译时候如果用gcc 4.4,就会抱怨说将来不会支持小于6的gcc了。但是6点几的gcc编译老uboot时候,就会报错说,别名不行,以及链接文件里面不能使用变量。
目前的编译工具链是Emcraft推荐的arm-uclinuxeabi-2010q1

跑通基本uboot

正统的uboot跟robutest的都有STM32F429的配置,不过并没有STM32F1的驱动。后者是老版本的uboot,还没有引入Kbuild,新增板子要改Makefile,配置全靠写头文件宏定义,但是编译出来的镜像更小,也比较容易裁剪。

Makefile里面添加:

stm32f103zet6_config : unconfig
    @$(MKCONFIG) $(@:_config=) arm arm_cortexm3 stm32f103zet6 \
    stm stm32

要想在F1板子上跑起来,首先得搞定基本的驱动:时钟、串口、GPIO。为方便起见,直接在ST官方库包装一层便是了。不过ST库跟uboot都定义了一些类型,比方说u8、u32等等;如果include了两方的头文件,就会说类型重复定义。所以封装层只能够包含ST库头文件。

RCC封装层:

void clock_init(void);
unsigned long clock_get(int);

USART封装层:

s32 serial_init(void);
void serial_setbrg(void); // 设置波特率
s32 serial_getc(void);
void serial_putc(const char c);
void serial_puts(const char *s);
s32 serial_tstc(void); // 获取串口接收状态

板级初始化里面,初始化所有的GPIO:

int board_init(void);

这个函数会在lib_arm/board.c:start_armboot()里面通过函数指针列表来调用。

初始化FSMC的函数,改为sram_init(void),并且照搬ST的例程。

配置文件include/configs/stm32f103zet6.h里面,基本上照搬了429 discovery的配置,值得修改的地方有,时钟:

#define CONFIG_STM32_SYS_CLK_PLL
#define CONFIG_STM32_PLL_SRC_HSE
#define CONFIG_STM32_HSE_HZ        8000000        /* 8 MHz */
#define CONFIG_STM32_PLL_M        4
#define CONFIG_STM32_PLL_N        360
#define CONFIG_STM32_PLL_P        4
#define CONFIG_STM32_PLL_Q        15

片内flash大小:

#define CONFIG_MEM_NVM_BASE        0x08000000
#define CONFIG_MEM_NVM_LEN        (1024 * 512)

片内SRAM:

#define CONFIG_MEM_RAM_BASE        0x20000000
#define CONFIG_MEM_RAM_LEN        (20 * 1024)
#define CONFIG_MEM_RAM_BUF_LEN        (24 * 1024)
#define CONFIG_MEM_MALLOC_LEN        (16 * 1024)
#define CONFIG_MEM_STACK_LEN        (4 * 1024)

接在FSMC bank0 CS3的片外SRAM:

#define CONFIG_NR_DRAM_BANKS        1
#define CONFIG_SYS_RAM_SIZE        (2 * 1024 * 1024)
#define CONFIG_SYS_RAM_CS        1
#define CONFIG_SYS_RAM_FREQ_DIV        2
#define CONFIG_SYS_RAM_BASE        0x68000000

串口配置,改为USART1:

#define CONFIG_STM32_USART_PORT        1    /* USART1 */
#define CONFIG_STM32_USART_TX_IO_PORT    0    /* PORTA */
#define CONFIG_STM32_USART_TX_IO_PIN    9    /* GPIO9 */
#define CONFIG_STM32_USART_RX_IO_PORT    0    /* PORTA */
#define CONFIG_STM32_USART_RX_IO_PIN    10    /* GPIO10 */
#define CONFIG_BAUDRATE            115200
#define CONFIG_SYS_BAUDRATE_TABLE    { 9600, 19200, 38400, 57600, 115200 }

然后是一些uboot的基本设置,比方说CONFIG_BOOTDELAY是开始数秒的秒数,CONFIG_EXTRA_ENV_SETTINGS是环境变量的设置,CONFIG_BOOTCOMMAND是默认启动的命令。

#define CONFIG_BOOTDELAY        3
#define CONFIG_BOOTCOMMAND        "run envmboot"
#define CONFIG_EXTRA_ENV_SETTINGS                \
        ...
        "envmboot=bootm ${envmaddr}\0"        \
        ...

然后编译通过之后,启动时在读秒的时候按一下回车,可以进入uboot命令行了。可以测试一下基本命令,比方说md,mm这些读写内存的命令,来问候片外SRAM是否安好。

编译进去的命令
编译进去的命令

编译进去的命令

内存测试
内存测试

内存测试

为uboot添加一些功能

比方说,环境变量存储在片内flash里面,nand命令等等。

环境变量

封装一下ST库的flash读写:

void envm_init(void); // 空函数
u32 envm_write(u32 offset, void * buf, u32 size); // 检测地址合法性,然后写入flash

然后include/configs/stm32f103zet6.h里面添加一下配置。下面设置片内flash 128k之后的4k空间作为环境变量的存储区:

#define CONFIG_ENV_IS_IN_ENVM
#define CONFIG_ENV_SIZE            (4 * 1024)
#define CONFIG_ENV_ADDR         \
    (CONFIG_SYS_ENVM_BASE + (128 * 1024))
#define CONFIG_INFERNO            1
#define CONFIG_ENV_OVERWRITE        1

然后就可以用setenvsave命令了。

nand flash

uboot里面有简化了的mtd系统,基本上只要写了底层初始化、读写的函数就行了。

首先在board_init()里面补充FSMC nand flash的初始化和相应GPIO的初始化。

我在drivers/mtd/nand/stm32f103zet6_nand.c里面实现的底层驱动。这些都要老老实实去翻手册。以下是基本读写函数:

/* 发送一条命令,或写一个字节数据 */
static void stm32f103_fsmc_send_cmd(struct mtd_info *mtd, int cmd, unsigned int ctrl);

/* nand flash是否忙。读一下忙引脚的电平即可 */
static int stm32f103_fsmc_read_rb(struct mtd_info *mtd);

然后还要告诉mtd层读写函数:

int board_nand_init(struct nand_chip *nand)
{
    // 设置读写寄存器的地址
    nand->IO_ADDR_R = nand->IO_ADDR_W = 0x70000000;
    // 前面定义的两个static函数
    nand->cmd_ctrl = stm32f103_fsmc_send_cmd;
    nand->dev_ready = stm32f103_fsmc_read_rb;
    // mtd层帮忙做ECC
    nand->ecc.mode = NAND_ECC_SOFT;
    // 指定位宽:8位
    nand->options = 0;

    return 0;
}

最后在include/configs/stm32f103zet6.h里面添加一下配置:

#define CONFIG_CMD_NAND 1
#define CONGIG_MTD_DEVICE
#define CONFIG_SYS_MAX_NAND_DEVICE  1
#define CONFIG_SYS_NAND_BASE 0x70000000

然后启动时候就可以观察到NAND的东西了。mtd里面有个数组记录了很多常用nand芯片的ID和大小,并且通用的初始化什么的都做好了,相当方便。现在就可以用nand指令去问候nand flash了。比方说nand dumpnand erasenand readnand write等等。

其他的可玩配置:

uboot命令行里面可以用上下方向键看历史命令:

#define CONFIG_CMDLINE_EDITING

tab补全:

#define CONFIG_AUTO_COMPLETE

想要在编译时候裁剪掉没有被引用的函数或数组,要在makefile里面加gcc编译参数-ffunction-sections -fdata-sections链接参数-Wl,--gc-sections。因为ST库的函数太多了,可以裁掉数十kb的大小。

uclinux内核配置

好事情是,robutest的uclinux里面有STM32F1的驱动,改改配置,编译出来的东西就能启动。

  • 居然有CONFIG_ARCH_STM32F1
    见arch/arm/mach-stm32里面的函数、头文件
    还有drivers/serial/stm32_usart.c的CONFIG_STM32_USART1
  • 还有CONFIG_STM32_DMA
    不过grep出来的dma貌似只在drivers/mmc/host/mmci.c里面有实际作用
  • 平台的定义
    arch/arm/mach-stm32/stm32_platform.c
    内核启动传参stm32_platform=stm32f1-se-comm

坏事情是,uclinux真是太大了orz。。。按照常规配置,内核大小1MB左右,只能放在片外存储器上跑。以FSMC那渣渣带宽跑起来真的很慢:淘宝上能以正常价格买到的SRAM是70ns的,换算一下也就十几兆带宽,STM32内核72MHz也没用。所以问题就在于怎样极限裁剪内核,使之能够塞进片内flash。

跑起内核

  • 驱动方面,裁掉除了USART之外的其他驱动。。。
  • 选择SLOB (Simple Allocator)作为slab分配器
  • 去掉模块支持CONFIG_MODULES
  • 去掉sysfs和procfs。。。
  • 去掉网络支持
  • 去掉加密解密之类的支持
  • 配置内存、XIP的地址
    内存地址
    内存地址
    内存地址和长度
XIP地址
XIP地址

XIP地址

内核镜像要在它前面留有64字节的空,这是uImage的头部。需要用uboot编译出来的工具mkimage来生成最终的uImage。

$ ../uboot/tools/mkimage -x -A arm -O linux -T kernel \
  -C none -a 0x08015040 -e 0x08015041 -n "linux"  \
  -d xipImage.bin \
  xipuImage.bin

注意stm32运行thumb2代码,PC最低位为1,所以是-e 0x08015041这个地址。

将生成的镜像烧进0x08015000,就可以启动到找不到根文件系统的kernel panic了。内核算是跑起来了。

摆弄根文件系统

根文件系统的根vfs的根不是一回事,后者是内核启动时自己凭空生成的,根据配置,会挂载有tmpfs、devtmpfs、sysfs、procfs等等,这是内核真正的根;而前者是内核启动最后一步chroot到的根文件系统,我们做的应用程序比如busybox就放在那里,可以配置启动文件,将sysfs之类的文件系统挂载到这里来。

我们做的根文件系统可以选择各种各样的文件系统,比方说romfs、yaffs、jffs等等,也可以用initramfs来编译进内核。uclinux的惯例是用romfs,因为它够小够简单(主流的Linux内核里面也有romfs的支持,那个模块就叫uclinux可以参考主流内核源码drivers/mtd/maps/uclinux.c

从busybox开始弄rootfs

busybox源码从官网上下载就行了。

  • 它有CONFIG_NOMMU的配置
  • CONFIG_CROSS_COMPILER_PREFIX设置交叉编译工具链
  • CONFIG_EXTRA_CFLAGS设置为-march=armv7-m -mthumb,使之生成thumb2的代码
  • CONFIG_PREFIX设置安装目录
  • 编译时,需要SKIP_STRIP:

    $ make SKIP_STRIP=y
    ...
    $ make install
    
  • 然后,那个安装目录下面就有了bin, sbin, usr目录了。补充上etcdev,然后就是比较规范的根目录了。规范叫“Filesystem Hierarchy Standard”

    $ ls ../rootfs
    bin  dev  etc  sbin  usr
    
  • 最后,通过genromfs命令来生成romfs镜像。genromfs通过包管理来安装。
    ``` bash
    $ genromfs -v
    -V “ROM Disk”
    -f romfs.bin
    -x placeholder
    -d ./rootfs/

    然后输出一堆东西。。。

$ ls romfs.bin
romfs.bin

这个romfs.bin就可以烧进对应地址的存储器了。

### 内核开启romfs的支持
* `CONFIG_BLOCK`、`CONFIG_MTD`、`CONFIG_MTD_UCLINUX`、`CONFIG_MTD_BLOCK_RO`这些是必选项。
* `CONFIG_MTD_UCLINUX_PHYADDR`配置romfs的地址
{% image uclinux-on-stm32f103/romfs-addr.jpeg 'romfs地址' '' %}

### romfs设备文件
uclinux 2.6.33的devtmpfs依赖tmpfs,后者又依赖MMU,所以不可能弄到动态生成/dev下面的设备文件节点。而romfs又是只读的,又不能mknod,所以需要将那些设备文件写死在rootfs的/dev目录下面。
man genromfs可以看到:
`If a file begins with the @ sign (and is empty otherwise), it refers to a device special node in the format: @name,type,major,minor. type can be b for block devices, c for character devices, and p for fifos.  The linux virtual console 1 can thus be included as a file with the name: @tty1,c,4,1`
于是/dev目录下面就可以有这些东西:
``` bash
$ ls rootfs/dev
@console,c,5,1     @rom0,b,31,0 
@mem,c,1,1         @tty,c,5,0   
@mtd0,c,90,0       @ttyS0,c,4,64
@mtd1,c,90,2       @ttyS1,c,4,65
@mtdblock1,b,31,1  @ttyS2,c,4,66
@null,c,1,3        @zero,c,1,5  
@ram0,b,1,0

生成的romfs烧进去对应的地址,然后启动时候可以看到这些printk:

ROMFS MTD (C) 2007 Red Hat, Inc.
...
uclinux[mtd]: ROM probe address=0x681bd000 size=0x43000
Creating 1 MTD partitions on "ROM":
0x000000000000-0x000000043000 : "ROMfs"

裁剪!

现在片内flash里已经没有空间了,只能用片外SRAM来曲线救国,通过uboot来把东西搬到SRAM,然后内核配置中romfs的地址写到那里去,然后内核参数中将内存大小缩减一点。可惜的是,这样配置的uboot、uclinux、busybox可以在人家2MB内存的板子上跑起来了,但是在我1MB内存的板子上显然是跑不动的,它会爆内存。所以这些东西都得往死里裁剪,才能塞进片内flash和1MB SRAM。

uboot裁剪

uboot只能放在片内flash。唯一的裁剪方式是改编译参数。

  • gcc优化参数有,O0, O1, O2, O3, Os。Os是optimize size的意思。试验发现,O2加上Os生成的镜像最小。
  • 为了删去没有用到的函数和数组,要添加gcc编译参数-ffunction-sections -fdata-sections链接参数-Wl,--gc-sections
    然后,uboot镜像可以小至80kb,可以塞进前0x15000的片内flash。

uclinux裁剪

极限裁剪之后整个镜像有460k左右,加上uboot就塞不进512kb的片内flash了。我从Run parts of the kernel in built-in eNVM of Cortex-M3这个内核选项里面得到灵感,观察到编译出来的镜像里面有40k左右是data段,刚好可以砍出来,剩下的text段恰好挤得进去。。。而data段可以由uboot来初始化,uclinux开始那段初始化就可以屏蔽掉了。为此我添加了一个配置:

砍为两半
砍为两半

在arch/arm/kernel/head-common.S:__mmap_switched函数里面屏蔽掉data段初始化:

        /*             
         * Copy .data segment and clear BSS    
         * For stm32f1 device, it shuold be done in uboot beforehand,                          
         * because eNVM is too small to hold them...                                           
         */            
#ifndef CONFIG_STM32_DATA_SEC_IS_IN_NAND       
        cmp     r4, r5                          @ Copy data segment if needed                  
1:      cmpne   r5, r6 
        ldrne   fp, [r4], #4
        strne   fp, [r5], #4
        bne     1b
#endif

然后,利用uboot将data段烧进nand flash里面,启动时候从nand flash里面搬运到SRAM。

busybox裁剪

  • init都不要了
  • busybox只剩下这些东西:
    hush, ls, dmesg, cat, free, uname

最终romfs大小是110k左右,只能够靠uboot来初始化了。

启动

启动
启动