用哪个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;配置之所以那么诡异是因为我芯片只能淘宝。。。
淘宝上的板子
自己做的板子
工具链
老代码用老编译器,新代码用新编译器。比方说新版的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
然后就可以用setenv
和save
命令了。
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 dump
、nand erase
、nand read
、nand 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地址
内核镜像要在它前面留有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
目录了。补充上etc
、dev
,然后就是比较规范的根目录了。规范叫“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来初始化了。