稍微复杂一点的的嵌入式系统,底层初始化和操作系统的引导都由bootloader完成。bootloader可以很简单,只需要将内存初始化好、将操作系统加载好就行;也可以比较复杂,比如大名鼎鼎的u-boot,或者不那么有名的barebox等等,都有大量的功能,管理着大量的外设,可以从各种各样的地方启动系统(flash、SD卡、U盘、网络服务器等等),并且有一个简单的或许可以跑脚本的shell。相当复杂的bootloader比如BIOS、UEFI等等甚至都有图形界面,能键盘鼠标操作。

对于我们的板子而言,只需要启动存储在片内flash的Linux就够了,所以bootloader只需要初始化好内存,然后直接跳转到Linux。但是为了初步验证板子各部分的功能,首先应该跑通一个比较完整的uboot;所幸的是,github上有人为stm32f429-disc移植了uboot,驱动已经十分全面,剩下的就是要细心地改GPIO口,并且查漏补缺。

(手机请横屏看代码)

uboot改动

这个uboot是老版本的,没有引入kbuild,配置板子靠的是include/configs/下面一系列头文件里面的宏定义。这里直接沿用include/configs/stm32429-disco.h的配置头文件。

时钟配置

改配置文件即可。将其配置为168MHz:

#define CONFIG_STM32_SYS_CLK_PLL
#define CONFIG_STM32_PLL_SRC_HSE
#define CONFIG_STM32_HSE_HZ 25000000
#define CONFIG_STM32_PLL_M  25
#define CONFIG_STM32_PLL_N  336
#define CONFIG_STM32_PLL_P  2
#define CONFIG_STM32_PLL_Q  7

其实,不管外面的晶振频率几何,进PLL第一步就是将频率分频降为1MHz,然后再倍频。这里倍频到336MHz。然后2分频为168MHz到HCLK,7分频为48MHz给USB。

SDRAM初始化

cpu/arm_cortexm3/stm32/fmc.c里,首先修改ext_ram_fmc_gpio数组中的引脚。

然后ExtMemInit()函数里面改内存时序参数:

// burst, 84MHz, CAS=2, 4 banks, 16 bits data, 13 bit RA, 10 bit CA
STM32_FMC_DRAM->sdcr[0] = 0x0000195A;
// TRCD=2, TRP=2, TWR=2, TRC=6, TRAS=4, TXSR=6, TMRD=2
STM32_FMC_DRAM->sdtr[0] = 0x01115351;

内存MRS和刷新频率设置。

// single write burst, CAS = 2, burst length = 2
STM32_FMC_DRAM->sdcmr = 0x00044214;
STM32_FMC_DRAM->sdrtr = ((636*8) << 1);

SDRAM的MRS设置都是通用的,可参考这个wiki

SDRAM的MRS设置
SDRAM的MRS设置

最后是SDRAM重定位!原本SDRAM bank1的地址是0xC0000000以上的,在所谓片外外设区里,按照ARM的意思是不可以执行代码的;STM32可以通过SYSCFG寄存器将它重定位到0x80000000以及0x00000000地址上,就可以在上面执行代码了。不过诡异的是,我的429 discovery板子即使不重定位也能执行代码,但是我买的芯片不重定位就不能执行代码(会引发busfault)。

/*
 * Remapping SDRAM bank1 to 0x80000000 and 0x00000000
 */
STM32_RCC->apb2enr  |= ((uint32_t)0x00004000);
STM32_SYSCFG->memrmp = ((uint32_t)0x00000404);
uboot启动之后。。。

可以用一系列内存读写的命令测试SDRAM是否安好:

STM32429-DISCO> md 0x80000000
80000000: 00000000 00000000 00000000 00000000    ................
80000010: 00000000 00000000 00000000 00000000    ................
80000020: 00000000 00000000 00000000 00000000    ................
80000030: 00000000 00000000 00000000 00000000    ................
...
STM32429-DISCO> mw 0x80000000 0x12348765 4
STM32429-DISCO> mw 0x80000010 0x87654321 4
STM32429-DISCO> md 0x80000000             
80000000: 12348765 12348765 12348765 12348765    e.4.e.4.e.4.e.4.
80000010: 87654321 87654321 87654321 87654321    !Ce.!Ce.!Ce.!Ce.
80000020: 00000000 00000000 00000000 00000000    ................
80000030: 00000000 00000000 00000000 00000000    ................
...

如果读写数据不对,那么多半是芯片没焊好。。简单测得SDRAM顺序读写速度约70MB每秒。

增加nand-flash驱动

首先在SDRAM初始化完成之后,顺便初始化nand flash:

// ECC 2k, TAR=TCLR=1, ECC disable, 8 bits, NAND flash, wait feature
STM32_FMC_NAND->pcrx  = 0x00060008;
// SET = 2, WAIT = 4, HOLD = 2, HIZ = 2
STM32_FMC_NAND->pmemx = 0x02020402;
STM32_FMC_NAND->pattx = 0x02020402;

// enable NAND bank
STM32_FMC_NAND->pcrx |= 0x00000004;

然后,自行添加必要函数。我在drivers/mtd/nand/stm32_nand.c里面添加:

#define FSMC_NAND_REG_DATA ((volatile uint8_t*)0x70000000)
#define FSMC_NAND_REG_CMD  ((volatile uint8_t*)0x70010000)
#define FSMC_NAND_REG_ADDR ((volatile uint8_t*)0x70020000)

#define GPIOD_IDR ((volatile uint32_t*)0x40020c10)

static void fmc_send_cmd(struct mtd_info *mtd, int cmd, unsigned int ctrl)
{
    (void)mtd;

    if(cmd != NAND_CMD_NONE) {
        if(ctrl & NAND_CLE)
            *FSMC_NAND_REG_CMD = (uint8_t)cmd;
        else if(ctrl & NAND_ALE)
            *FSMC_NAND_REG_ADDR = (uint8_t)cmd;
    }
}

static int fmc_read_rb(struct mtd_info *mtd)
{
    (void)mtd;
    // read PD6 for BUSY
    return *GPIOD_IDR & (1L << 6);
}

其实STM32的nand flash控制器并没有忙的标志,所以只好读GPIO的忙电平;ST官方例程也是这么干的。。

随后,向MTD层注册上面的两个函数:

int board_nand_init(struct nand_chip *nand)
{
    // 设置读写寄存器的地址
    nand->IO_ADDR_R = nand->IO_ADDR_W = FSMC_NAND_REG_DATA;
    // 设置那两个函数
    nand->cmd_ctrl = fmc_send_cmd;
    nand->dev_ready = fmc_read_rb;
    // 软件ECC
    nand->ecc.mode = NAND_ECC_SOFT;
    // 指定位宽:8位
    nand->options = 0;

    return 0;
}

最后,在配置文件里面添加nand命令的宏定义:

#define CONFIG_CMD_NAND
#define CONFIG_NAND_STM32_UCPC
#define CONFIG_MTD_DEVICE
#define CONFIG_SYS_MAX_NAND_DEVICE  1
#define CONFIG_SYS_NAND_BASE 0x70000000

自定义了CONFIG_NAND_STM32_UCPC这个宏,为的是在drivers/mtd/nand/Makefile里面添加这个自己写的stm32_nand.c:

COBJS-$(CONFIG_NAND_STM32_UCPC) += stm32_nand.o
nand效果

可以看到识别到nand:

U-Boot 2010.03-00003-g934021a-dirty (11月 07 2017 - 14:27:07) 

CPU  : STM32F4 (Cortex-M4)
Freqs: SYSCLK=168MHz,HCLK=168MHz,PCLK1=42MHz,PCLK2=84MHz
Board: STM32F429I-DISCOVERY board,Rev 1.0
DRAM:  64 MB                 
NAND:  128 MiB

可以用nand系列的命令去读写nand flash。下面罗列一些常用的命令:

查看nand信息:

STM32429-DISCO> nand info

Device 0: nand0, sector size 128 KiB

查看坏块:

STM32429-DISCO> nand bad

Device 0 bad blocks:
  000e0000
  00140000
  001a0000
  001e0000
  00200000
  00240000
  002e0000
  04f20000
  07fa0000

擦除:

STM32429-DISCO> nand erase 0

NAND erase: device 0 whole chip  
Skipping bad block at  0x000e0000
Skipping bad block at  0x00140000
Skipping bad block at  0x001a0000
Skipping bad block at  0x001e0000
Skipping bad block at  0x00200000
Skipping bad block at  0x00240000
Skipping bad block at  0x002e0000
Skipping bad block at  0x04f20000
Skipping bad block at  0x07fa0000
Erasing at 0x7fe0000 -- 100% complete.
OK

写入第一个扇区:

STM32429-DISCO> md 0x80000000             
80000000: 12348765 12348765 12348765 12348765    e.4.e.4.e.4.e.4.
80000010: 87654321 87654321 87654321 87654321    !Ce.!Ce.!Ce.!Ce.
80000020: 00000000 00000000 00000000 00000000    ................
80000030: 00000000 00000000 00000000 00000000    ................
...
STM32429-DISCO> nand write 0x80000000 0 800

NAND write: device 0 offset 0x0, size 0x800
 2048 bytes written: OK

dump一下刚刚写入的东西:

STM32429-DISCO> nand dump 0
Page 00000000 dump:
    65 87 34 12 65 87 34 12  65 87 34 12 65 87 34 12
    21 43 65 87 21 43 65 87  21 43 65 87 21 43 65 87
    00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
...

读一下第一个扇区:

STM32429-DISCO> nand read 0x80000000 0 800     

NAND read: device 0 offset 0x0, size 0x800
 2048 bytes read: OK
STM32429-DISCO> md 0x80000000             
80000000: 12348765 12348765 12348765 12348765    e.4.e.4.e.4.e.4.
80000010: 87654321 87654321 87654321 87654321    !Ce.!Ce.!Ce.!Ce.
80000020: 00000000 00000000 00000000 00000000    ................
80000030: 00000000 00000000 00000000 00000000    ................
...

LTDC显示接口

原有的驱动在cpu/arm_cortexm3/stm32/disp.c这里。

首先,改ext_tft_gpio数组里定义的GPIO。需要注意的是,GPIO有两个AF对应到LTDC,分别是AF9AF14;要老老实实翻DS9405数据手册来检查。

然后,因为429 discovery板子上有个液晶屏,其控制芯片需要通过SPI来初始化;但是uc-PC上并不用这样做,只要有RGB信号就行(称为哑显示);所以将LCD_SPIlcd_init()等相关内容删去。

最后,配置时序,使之适合标准VGA时序。设置的行列信号依据就是下图两个红框的参数:

640x480@60Hz VGA时序
640x480@60Hz VGA时序

可以对比VGA时序图以及LTDC的时序图。下图是VGA的时序图:
640x480@60Hz VGA时序
640x480@60Hz VGA时序

下图是LTDC的时序图:

LTDC时序图
LTDC时序图

最终设置的LTDC寄存器如下。首先是时钟配置,像素时钟设为25MHz:

// Configure PLLSAI prescalers for LCD
// Enable Pixel Clock
// PLLSAI_VCO Input = HSE_VALUE/PLL_M = 1 Mhz
// PLLSAI_VCO Output = PLLSAI_VCO Input * PLLSAI_N = 200 Mhz
// PLLLCDCLK = PLLSAI_VCO Output/PLLSAI_R = 200/4 = 50 Mhz
// LTDC clock frequency = PLLLCDCLK / RCC_PLLSAIDivR = 50/2 = 25 Mhz
STM32_RCC->pllsaicfgr = (204 << 6) | (7 << 24) | (4 << 28);

STM32_RCC->dckcfgr &= ~0x00030000;
STM32_RCC->dckcfgr |=  0x00000000;      // /2

// Enable PLLSAI Clock
*((volatile uint32_t *)CR_PLLSAION_BB) = 1;

// Wait for PLLSAI activation
while ((STM32_RCC->cr & 0x20000000) == 0);

然后是VGA时序的设置:

STM32_LTDC->sscr = ((96) << 16)           | (2);
STM32_LTDC->bpcr = ((96+48) << 16)        | (2+33);
STM32_LTDC->awcr = ((96+48+640) << 16)    | (2+33+480); // 640 x 480
STM32_LTDC->twcr = ((96+48+640+16) << 16) | (2+33+480+10);

只用LAYER1。主要是设置640x480的窗口,以及帧缓存的地址:

STM32_LTDC_LAYER1->whpcr  = ((95+48+641) << 16) | (95+48+2);
STM32_LTDC_LAYER1->wvpcr  = ((1+33+480) << 16) | (1+33);
STM32_LTDC_LAYER1->pfcr   = 2;  // RGB565
...
STM32_LTDC_LAYER1->cfbar  = FBAddr; // 帧缓存地址
...

最后使能LTDC。

显示器效果。

接上一个显示器,然后用内存读写命令来往帧缓存里面写入像素值。比方说帧缓存地址是0x80400000,则可以:

STM32429-DISCO> mw 80400000 f800f800 5000
STM32429-DISCO> mw 80414000 07e007e0 5000
STM32429-DISCO> mw 80428000 001f001f 5000
STM32429-DISCO> mw 8043c000 f81ff81f 5000
STM32429-DISCO> mw 80450000 ffe0ffe0 5000
STM32429-DISCO> mw 80464000 07ff07ff 5000
STM32429-DISCO> mw 80478000 ffffffff 5000

分别是红绿蓝以及组合颜色。显示效果如下:

VGA显示效果
VGA显示效果

网络

首先在配置文件里面使能网络命令:

#define  CONFIG_CMD_NET
#define  CONFIG_NET_MULTI
#define  CONFIG_STM32_ETH
#define  CONFIG_STM32_ETH_RMII

然后在drivers/net/stm32_eth.c里面更改GPIO。包括在mac_gpio里面修改RMII的GPIO,以及以太网复位的GPIO。然后其他可以照旧。

在上位机上搭建tftp服务器
  • 如果上位机上运行着Ubuntu,则可以直接通过包管理安装tftp服务器:
    $ sudo apt-get install tftp-hpa tftpd-hpa
    
  • 新建你的tftp目录,修改权限+rw(或者直接设为777,反正没人攻击你)。
  • 更改配置文件/etc/default/tftpd-hpa
    TFTP_DIRECTORY="<那个目录>"
    
  • 重启服务:
    $ sudo service tftpd-hpa restart
    
网卡效果。

可以看到识别到STM32的mac:

U-Boot 2010.03-00003-g934021a-dirty (11月 07 2017 - 14:27:07)

CPU  : STM32F4 (Cortex-M4)
Freqs: SYSCLK=168MHz,HCLK=168MHz,PCLK1=42MHz,PCLK2=84MHz
Board: STM32F429I-DISCOVERY board,Rev 1.0
DRAM:  64 MB
NAND:  128 MiB
Using default environment

Net:   STM32_MAC
Hit any key to stop autoboot:  0 
STM32429-DISCO> 

下载之前,板子要设置3个环境变量:随便设置一个mac地址、本机静态IP地址,以及tftp服务器的IP地址

setenv ethaddr C0:B1:3D:88:88:89
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.177

然后,在上位机将镜像复制到你的tftp目录下面,在板子上通过tftp命令下载镜像:

STM32429-DISCO> tftp 81000000 xipImage
Auto-negotiation...completed.
STM32_MAC: link UP (100/Full)
Using STM32_MAC device
TFTP from server 192.168.1.177; our IP address is 192.168.1.100
Filename 'xipImage'.
Load address: 0x81000000
Loading: #################################################################
         ##################################################
done
Bytes transferred = 1685760 (19b900 hex)
STM32429-DISCO> 

可见已经下载成功了。

其实网卡的驱动、协议栈都已经相当成熟,如果出现问题,多半是网卡没焊好。。。或者是忘记拉高网卡的RESET引脚了。这些都会造成网卡的50MHz参考时钟没输出,而STM32的mac时钟是靠外面网卡输入的,没时钟的话就会说,mac不能复位。

跳转Linux

uboot编译完后,会在tools/目录下面生成一个mkimage工具,用它来将Linux镜像打包成uImage镜像。uboot的bootm命令只能启动uImage镜像。调试时候可以用tftp命令下载镜像,甚至用jlink直接下载到内存,不过后者的下载速度比较慢。

如果像我那样懒得每次调试要敲一遍mkimage的话,可以直接使用go命令跳转到内核。不过这个版本的go命令传参有些问题,需要修改common/cmd_boot.cdo_go_exec函数。

给STM32支持设备树的Linux内核要传3个参数,R0设为0,R1设为0xffffffff,R2设为设备树的地址。函数调用时候依次传参就行。或者简单起见,将那些地址写死也行。

另外值得一提的是,在SDRAM里面跑Linux内核性能简直感人,而且如果根文件系统挂载的是nfs,STM32芯片以及SDRAM芯片都会有点烫手——外设火力全开之后功耗真高。。。

afboot-stm32

afboot-stm32是专门启动STM32上面Linux系统的最小bootloader,编译完之后大小只有2kb左右。Linux内核跑在片内flash里面,性能远胜于跑在SDRAM里。

原版的afboot已经移植到429 discovery上,对于uc-PC来说,需要修改GPIO引脚、修改SDRAM初始化、增加nand flash初始化、增加LTDC初始化。这些东西都与上文详述的uboot移植过程大同小异,不再赘述。况且afboot中所有的初始化都写在同一个c文件里面,改起来也相当方便。

跳转Linux

start_kernel.c里面是最终跳转到Linux的代码,很简单,就一个跳转。

按照afboot的逻辑,bootloader烧写在片内flash第一个扇区里面(0~16k),然后设备树烧写在第二个扇区(16~32k),剩下的所有空间都留给Linux内核。但是新的内核里设备树越来越庞大,以至于超过了一个扇区的16k大小,如果接着占用下一个扇区就要多占用32k了。所以最好便是设备树紧挨着afboot,一同放在头两个扇区里面。为此start_kernel.c更改如下:

#include <stdlib.h>
#include <stdint.h>

extern unsigned int _end_text;
extern unsigned int _start_data;
extern unsigned int _end_data;
void start_kernel(void)
{
        void (*kernel)(uint32_t reserved, uint32_t mach, uint32_t dt) = (void (*)(uint32_t, uint32_t, uint32_t))(KERNEL_ADDR | 1);

        /* the DTB is just after the bootloader */
        kernel(0, ~0UL, (uint32_t)&_end_text + (uint32_t)&_end_data - (uint32_t)&_start_data);
}

其中,_end_text、_start_data、_end_data都是在链接脚本stm32f429.lds里面定义的,分别是代码段的末端、存储在flash里面的数据段的首末两端的地址。

.text :
{
  /*. = ALIGN(4);*/
  _end_text = .;
} >FLASH

.data : 
{
  _start_data = .;
  *(.data)
  _end_data = .;
} >SRAM1 AT >FLASH

所以设备树的地址就是_end_text + _end_data - _start_data了。

afboot和内核编译之后,用下面的命令将bootloader和设备树组合到loader.bin一个文件里面:

$ cat stm32429i-ucpc.bin \
  内核/arch/arm/boot/dts/stm32f429-disco.dtb \
  > loader.bin
$ ls loader.bin -l
-rw-r--r-- 1 hyq hyq 19057 2月  10 18:24 loader.bin

一共约18kb,能烧进前32kb的空间里面。

另外内核里有个CONFIG_ARM_APPENDED_DTP的配置,解释为专门将dtb接到zImage后面,但“并不推荐在正式产品中使用”,于是就没做这种尝试了。