AT91SAM9260 Nor Flash Bootstrap移植实战:从零适配启动引导程序
1. 项目背景与挑战当官方不再支持时最近在做一个基于AT91SAM9260的老项目遇到了一个挺典型的嵌入式开发困境芯片原厂和开发板供应商的技术支持断档。我们公司从百特买了一块AT91SAM9260的开发板板子上预留了Nor Flash的焊盘但出厂时没焊芯片。项目需求明确要求使用Nor Flash作为启动和程序存储介质原因也很直接——Nor Flash支持XIP片上执行上电后CPU可以直接从中取指运行启动速度有保障在一些对启动时间有严格要求的工业场景里这是刚需。本以为照着开发板资料搞就行结果一翻文档心凉了半截。资料包里关于Bootloader的部分只有DataFlash和Nand Flash的驱动和烧录方案压根没提Nor Flash。抱着试试看的心态联系了百特的技术支持对方的回复很干脆“没有”。不死心又给百特的上海技术中心发了封邮件这次回复更“官方”一些“ATMELAT91SAM9260的原厂现已被Microchip收购没有做Nor Flash的相关驱动工作”。这个回复相信很多老嵌入式工程师看了都会心一笑。这背后其实是一种很常见的商业策略。AT91SAM9260系列芯片内部集成了对DataFlash一种串行Flash也是Atmel/Microchip自家的产品的Boot ROM支持上电后可以从DataFlash直接加载第一阶段的Bootloader。推广自家生态链产品无可厚非。至于Nor Flash虽然硬件接口通常是并行总线支持但相关的底层驱动和Bootloader范例官方就是不提供现成的。这就像你买了个品牌手机它告诉你只能用指定的配件才能快充用第三家的就只给慢充道理是相通的。指望原厂“烧自己的钱”去完善竞品存储器的支持确实不现实。没办法求人不如求己官方不给那就自己动手。我们的目标很明确为AT91SAM9260开发一个能从Nor Flash启动的Bootstrap第一级引导程序。我手头用的Nor Flash型号是Spansion现属Cypress的AM29LV160DB一颗16Mbit2MB的并行Nor Flash。下面我就把整个从零开始适配Nor Flash Bootstrap的过程、原理、踩过的坑和最终方案详细拆解一遍。如果你也在折腾类似的旧平台引导问题希望这篇能帮你省下几十个小时的查资料和调试时间。2. AT91SAM9260启动流程深度解析要自己写Bootloader首先得把芯片的“脾气”摸透。AT91SAM9260的启动过程是一个典型的多阶段引导设计得比较精巧也给了开发者一定的灵活性。2.1 上电后的第一步硬件自动映射AT91SAM9260芯片内部有一块16KB的ROM这段ROM的代码是芯片出厂时就固化好的用户无法修改。当芯片上电或复位后硬件会自动执行以下操作初始化基本硬件包括时钟、中断控制器等最基础的模块。检测启动媒介ROM代码会按照预设的顺序去探测外部存储器。这个顺序通常是DataFlash通过SPI接口 - Nand Flash - 外部总线比如Nor Flash。具体探测哪个受芯片某个特定引脚BMSBootstrap Mode Select的上拉或下拉状态控制。在我们的板子上通常会把BMS引脚配置为从外部总线即Nor Flash启动。加载第一级引导程序ROM代码会从检测到的启动媒介的固定地址对于Nor Flash这个地址是0x10000000这是芯片内存映射的一部分开始读取最多4KB的数据将其拷贝到芯片内部的SRAM中。AT91SAM9260的内部SRAM地址从0x200000开始。跳转执行ROM代码将PC程序计数器指针跳转到SRAM的起始地址0x200000开始执行我们刚刚被加载进来的代码。这4KB的代码就是我们常说的Bootstrap或者叫Stage1 Bootloader。关键点理解为什么是4KB因为内部SRAM容量有限AT91SAM9260只有16KB内部SRAM且ROM代码设计如此。这4KB的代码必须“精打细算”它的核心任务不是直接启动复杂的操作系统而是初始化更关键的硬件如SDRAM控制器为加载更大、功能更全的第二阶段引导程序如U-Boot或直接的应用代码做准备。2.2 Bootstrap的核心使命与设计约束这4KB的Bootstrap身负重任但手脚被绑着。它的设计必须遵循以下原则代码尺寸严格受限必须小于4KB包含向量表、初始化代码、驱动等所有内容。这意味着必须用汇编或高度优化的C语言来写不能使用标准库要极度精简。初始化最小化硬件主要完成两件大事初始化时钟将主时钟切换到更快的频率比如从慢速的32.768kHz晶振切换到18.432MHz主晶振再通过PLL倍频到芯片工作的核心频率。初始化SDRAM控制器这是最关键的一步。因为4KB的SRAM远远不够用必须尽快让大容量的外部SDRAM可用才能将后续的大容量代码可能是几十KB的U-Boot或几百KB的应用加载进来。加载下一阶段代码从存储介质Nor Flash的特定偏移地址将下一阶段程序的二进制镜像读取到SDRAM的指定地址。验证与跳转可选地验证镜像的完整性如校验和然后跳转到SDRAM中的入口点将控制权移交。理解了这些我们就明白所谓“开发Nor Flash的Bootstrap”本质就是编写一段不超过4KB的程序它能被芯片ROM从Nor Flash的0x10000000地址正确加载并运行并且这段程序要能初始化SDRAM然后从Nor Flash的另一个位置比如0x10008000把主程序读出来放到SDRAM里最后跳过去执行。3. 从官方Bootstrap工程开始移植完全从零写汇编启动代码和Flash驱动周期长且容易出错。最明智的做法是站在“巨人的肩膀上”——修改ATMEL官方提供的Bootstrap源码。虽然官方不直接支持Nor Flash但其Bootstrap工程框架是现成的包含了时钟、SDRAM初始化等通用部分我们只需要替换或添加存储介质驱动层。3.1 源码结构剖析从ATMEL的官网或开发板资料包中找到AT91SAM9260的Bootstrap源码通常是一个压缩包如at91bootstrap-3.x.x.tar.gz。解压后目录结构大致如下at91bootstrap/ ├── board/ # 板级相关文件 │ └── at91sam9260ek/ # 对应评估板的目录 │ ├── at91sam9260ek.c │ ├── at91sam9260ek.h │ └── ...flash/ # 不同Flash的驱动和配置文件 │ ├── dataflash/ │ ├── nandflash/ │ └── ... # 我们就要在这里创建 norflash/ ├── driver/ # 通用驱动如PMC, SDRAMC等 ├── include/ # 头文件 └── ...其他目录我们的工作主要聚焦在board/at91sam9260ek/这个目录下。我们需要创建一个norflash子目录并实现对应的驱动。3.2 关键文件与配置修改1. 创建Nor Flash驱动文件在board/at91sam9260ek/下创建norflash目录并创建以下核心文件norflash.cNor Flash的底层驱动实现擦除、写入、读取函数。norflash.h驱动头文件定义函数接口和Flash参数。at91sam9260ek_norflash.mk编译该模块的Makefile片段。norflash.h示例#ifndef _NORFLASH_H_ #define _NORFLASH_H_ #include “common.h” /* 定义我们使用的AM29LV160DB的参数 */ #define NORFLASH_SECTOR_SIZE (64 * 1024) /* 64KB 扇区大小 */ #define NORFLASH_SECTOR_NUM 32 /* 共32个扇区2MB */ #define NORFLASH_TOTAL_SIZE (NORFLASH_SECTOR_SIZE * NORFLASH_SECTOR_NUM) /* Nor Flash 在CPU内存空间中的基地址 */ #define NORFLASH_BASE_ADDR 0x10000000 /* 函数声明 */ extern int norflash_init(void); extern int norflash_erase_sector(unsigned int sector_addr); extern int norflash_write_word(unsigned int addr, unsigned short data); extern int norflash_read(unsigned int addr, void *data, unsigned int size); #endif /* _NORFLASH_H_ */norflash.c实现要点Nor Flash的驱动本质是通过向特定的命令序列地址写入特定的数据来发送命令。AM29LV160DB是CFICommon Flash Interface兼容的我们可以先读取CFI信息来适配但为了代码精简在Bootstrap中常采用硬编码命令序列。#include “norflash.h” /* AM29LV160DB 的命令序列定义基于地址-数据对 */ #define NOR_CMD_UNLOCK1_ADDR (NORFLASH_BASE_ADDR 0x555 * 2) /* 注意地址对齐16位设备 */ #define NOR_CMD_UNLOCK1_DATA 0xAA #define NOR_CMD_UNLOCK2_ADDR (NORFLASH_BASE_ADDR 0x2AA * 2) #define NOR_CMD_UNLOCK2_DATA 0x55 #define NOR_CMD_PROGRAM_ADDR NOR_CMD_UNLOCK1_ADDR #define NOR_CMD_PROGRAM_DATA 0xA0 #define NOR_CMD_ERASE_SETUP_ADDR NOR_CMD_UNLOCK1_ADDR #define NOR_CMD_ERASE_SETUP_DATA 0x80 #define NOR_CMD_ERASE_CONFIRM_ADDR NOR_CMD_UNLOCK1_ADDR #define NOR_CMD_ERASE_CONFIRM_DATA 0x10 #define NOR_CMD_READ_RESET_ADDR NORFLASH_BASE_ADDR #define NOR_CMD_READ_RESET_DATA 0xF0 static void norflash_send_cmd(unsigned int addr, unsigned short data) { volatile unsigned short *ptr (unsigned short *)addr; *ptr data; } int norflash_init(void) { /* 发送复位命令使Flash回到读数组模式 */ norflash_send_cmd(NOR_CMD_READ_RESET_ADDR, NOR_CMD_READ_RESET_DATA); return 0; } int norflash_erase_sector(unsigned int sector_addr) { /* 1. 解锁序列 */ norflash_send_cmd(NOR_CMD_UNLOCK1_ADDR, NOR_CMD_UNLOCK1_DATA); norflash_send_cmd(NOR_CMD_UNLOCK2_ADDR, NOR_CMD_UNLOCK2_DATA); /* 2. 擦除设置命令 */ norflash_send_cmd(NOR_CMD_ERASE_SETUP_ADDR, NOR_CMD_ERASE_SETUP_DATA); /* 3. 再次解锁 */ norflash_send_cmd(NOR_CMD_UNLOCK1_ADDR, NOR_CMD_UNLOCK1_DATA); norflash_send_cmd(NOR_CMD_UNLOCK2_ADDR, NOR_CMD_UNLOCK2_DATA); /* 4. 向要擦除的扇区地址写入确认命令 */ norflash_send_cmd(sector_addr, NOR_CMD_ERASE_CONFIRM_DATA); /* 5. 轮询等待擦除完成检查特定地址的数据位 DQ7 (0x80) 是否变为1 */ volatile unsigned short *poll_addr (unsigned short *)sector_addr; unsigned short last_data 0xFFFF; while (1) { unsigned short cur_data *poll_addr; if ((cur_data 0x80) 0x80) { /* DQ7变为1表示擦除完成 */ if ((cur_data 0xFFFF) 0xFFFF) { /* 并且整个字都是0xFFFF */ break; } } /* 可选增加超时检测防止死循环 */ if (cur_data last_data) { /* 可能出错了 */ return -1; } last_data cur_data; } /* 6. 复位到读模式 */ norflash_send_cmd(NOR_CMD_READ_RESET_ADDR, NOR_CMD_READ_RESET_DATA); return 0; } int norflash_write_word(unsigned int addr, unsigned short data) { /* 1. 解锁序列 */ norflash_send_cmd(NOR_CMD_UNLOCK1_ADDR, NOR_CMD_UNLOCK1_DATA); norflash_send_cmd(NOR_CMD_UNLOCK2_ADDR, NOR_CMD_UNLOCK2_DATA); /* 2. 编程命令 */ norflash_send_cmd(NOR_CMD_PROGRAM_ADDR, NOR_CMD_PROGRAM_DATA); /* 3. 向目标地址写入数据 */ volatile unsigned short *ptr (unsigned short *)addr; *ptr data; /* 4. 轮询等待编程完成检查写入地址的值是否等于写入的值 */ while (*ptr ! data) { /* 等待同样可加超时 */ } return 0; } int norflash_read(unsigned int addr, void *data, unsigned int size) { /* Nor Flash支持内存映射读取直接memcpy即可 */ unsigned short *src (unsigned short *)addr; unsigned short *dst (unsigned short *)data; unsigned int word_size size / 2; for (unsigned int i 0; i word_size; i) { dst[i] src[i]; } return 0; }注意事项这里有一个极易出错的细节AM29LV160DB是16位数据宽度的设备。在AT91SAM9260的内存映射中0x10000000开始的地址空间对应到Flash的字节地址。但当我们以unsigned short *指针访问时指针每加1地址会增加2一个word的字节数。所以Flash数据手册中的命令地址如0x555, 0x2AA是字节地址在代码中需要乘以2来转换为CPU访问的地址。上面的代码中*2操作就是为此。如果忽略这一点命令永远发送不对。2. 修改板级配置文件接下来需要修改board/at91sam9260ek/at91sam9260ek.h告诉Bootstrap我们使用Nor Flash。/* at91sam9260ek.h */ #ifndef _AT91SAM9260EK_H_ #define _AT91SAM9260EK_H_ /* ... 其他定义 ... */ /* 定义我们使用的Flash类型 */ #define CONFIG_NORFLASH /* 注释掉CONFIG_DATAFLASH或CONFIG_NANDFLASH */ /* 定义应用程序在Nor Flash中的烧写地址 */ /* Bootstrap自身占用最开始的4KB0x10000000 - 0x10000FFF */ /* 我们将应用程序放在紧接着的地址例如 0x10008000 */ #define IMG_ADDRESS 0x10008000 /* ... 其他定义 ... */ #endif /* _AT91SAM9260EK_H_ */3. 修改主引导逻辑需要修改board/at91sam9260ek/at91sam9260ek.c中关于加载应用程序的部分。通常这里有一个load_image()或类似的函数我们需要根据CONFIG_NORFLASH宏定义调用我们刚实现的norflash_read函数。/* at91sam9260ek.c */ #include “norflash.h” /* 添加头文件 */ /* ... 其他代码 ... */ static int load_image(void) { void *dest (void *)TEXT_BASE; /* TEXT_BASE通常是SDRAM中加载镜像的目标地址如0x23F00000 */ unsigned int image_size …; /* 可以从Flash固定位置读取镜像大小信息 */ #ifdef CONFIG_NORFLASH /* 从Nor Flash的IMG_ADDRESS地址读取image_size大小的数据到SDRAM的dest处 */ if (norflash_read(IMG_ADDRESS, dest, image_size) ! 0) { return -1; } #elif defined(CONFIG_DATAFLASH) /* 原有的DataFlash加载代码 */ #elif defined(CONFIG_NANDFLASH) /* 原有的NandFlash加载代码 */ #endif return 0; }4. 修改编译系统在board/at91sam9260ek/Makefile或相关的编译配置中添加对norflash目录的编译支持。这通常涉及修改OBJS变量添加norflash/norflash.o。4. 编译、烧录与调试实战4.1 编译Bootstrap完成代码修改后进入Bootstrap源码根目录执行编译。通常有一个配置脚本。# 1. 配置指定板型和Flash类型可能需要修改配置脚本支持nor make at91sam9260ek_norflash_config # 2. 编译 make CROSS_COMPILEarm-none-eabi- # 指定你的交叉编译工具链前缀编译成功后会在binaries/目录下生成at91sam9260ek.bin文件这就是我们需要的、不超过4KB的Bootstrap二进制文件。4.2 烧录策略与地址规划烧录需要分两步且地址不能错烧录Bootstrap到Nor Flash的0x10000000这个文件很小约3-4KB必须烧在开头。可以使用JTAG仿真器如J-Link配合烧录软件如J-Flash直接烧写.bin文件到该地址。烧录应用程序到IMG_ADDRESS如0x10008000应用程序可以是编译好的U-Boot.bin文件也可以是你的裸机应用程序。同样使用JTAG烧录。烧录地址规划心得Bootstrap地址固定必须是0x10000000这是芯片硬件决定的。应用程序地址预留空间IMG_ADDRESS需要与Bootstrap的结束地址保持足够距离。Bootstrap本身约4KB但编译后可能不足4K。建议应用程序地址从0x100010004KB对齐后开始更安全。我们设为0x1000800032KB处留下了充足的空间避免因Bootstrap后续版本变大而导致覆盖。应用程序的链接地址你的应用程序或U-Boot在编译时其链接地址TEXT_BASE或CONFIG_SYS_TEXT_BASE必须设置为SDRAM中的地址如0x23F00000而不是Nor Flash中的地址。因为Bootstrap会把它加载到SDRAM运行而不是在Nor Flash中XIP执行除非你的应用程序特别小且设计为XIP。4.3 调试技巧与常见问题排查自己移植Bootloader调试是家常便饭。如果没有串口打印调试会非常困难。因此尽早让串口工作起来是重中之重。技巧1在Bootstrap中初始化最简串口在Bootstrap的早期初始化设置完时钟后初始化SDRAM前就初始化一个UART如DBGU。即使没有完整的printf也能通过发送特定字符到串口助手来判断代码执行到了哪个阶段。例如在关键函数入口和出口发送‘A’、‘B’、‘C’。技巧2利用点灯大法如果连串口都调不通GPIO点灯是最原始的调试手段。在代码的不同阶段控制不同的LED亮灭可以粗略判断死机的位置。常见问题排查表现象可能原因排查思路上电后毫无反应灯也不亮。1. Bootstrap根本没被加载。2. 芯片启动模式BMS引脚设置错误。3. 时钟初始化失败芯片“跑飞”。1. 确认Bootstrap.bin已正确烧录到0x10000000并用编程器回读校验。2. 检查原理图确认BMS引脚的上拉/下拉电阻配置是否符合从Nor Flash启动的要求。3. 在Bootstrap最开始加一条点灯或发串口数据的指令看能否执行。灯闪一下后常亮或熄灭串口无输出。1. 时钟初始化配置错误PLL倍频参数不对。2. SDRAM初始化失败导致后续代码加载或运行出错。1. 简化时钟配置先不使用PLL用主晶振直接分频出一个较低频率工作看串口是否有输出。2. 仔细核对SDRAM芯片型号如K4S561632根据其数据手册检查Bootstrap中SDRAM控制器SDRAMC的配置寄存器值行列地址位数、刷新周期、CAS延迟等。一个参数不对SDRAM就无法正常工作。串口有乱码或固定字符输出。串口波特率设置错误。检查Bootstrap中UART的时钟源和分频器设置确保与PC端串口助手设置的波特率如115200匹配。计算波特率时注意当前系统主频。Bootstrap串口有输出但提示加载应用程序失败。1. Nor Flash驱动读写函数有bug。2. 应用程序烧录地址IMG_ADDRESS不对。3. 应用程序镜像格式或大小不对。1. 在Bootstrap中增加调试信息打印从Nor Flash读取的指定地址的数据与烧录文件对比。2. 确认IMG_ADDRESS宏定义的值并用JTAG工具查看该地址在Flash中的内容是否正确。3. 确认应用程序镜像是否包含正确的向量表或头部信息。应用程序加载成功但跳转后死机。1. 应用程序的链接地址与加载地址不匹配。2. SDRAM初始化不稳定大程序运行后出错。3. 应用程序自身有问题。1. 这是最经典的问题。务必确认应用程序编译时指定的运行地址链接地址就是Bootstrap将其加载到SDRAM的地址TEXT_BASE。2. 优化SDRAM初始化代码增加初始化后的简单读写测试如写入再读出校验。3. 单独用JTAG将应用程序直接加载到SDRAM的TEXT_BASE地址运行看是否正常以排除Bootstrap加载过程的问题。5. 进阶思考与优化建议当基本的Nor Flash Bootstrap能工作后可以考虑以下优化让它更实用、更健壮。5.1 增加镜像完整性校验在Bootstrap中加载应用程序后跳转之前增加一个CRC32或简单的校验和验证。这可以避免因Flash数据损坏导致系统跑飞。可以将校验和存储在应用程序镜像的固定偏移处如文件末尾前4个字节。5.2 实现简单的串口命令交互虽然4KB空间紧张但可以挤出一个简单的命令行实现两个核心功能load通过串口XMODEM/ymodem协议接收新的应用程序镜像并烧写到Nor Flash的IMG_ADDRESS。这样就无需每次更新程序都动用JTAG。go跳转到指定地址执行。 这能极大提升开发效率。5.3 支持多种Nor Flash型号我们的驱动硬编码了AM29LV160DB的命令。更好的做法是在Bootstrap启动时读取Flash的CFI通用闪存接口信息自动识别制造商、容量、扇区布局。根据识别结果选择对应的命令集和参数进行操作。 这样Bootstrap的通用性会强很多。当然这需要更多的代码空间需要更极致的优化。5.4 与U-Boot的衔接更常见的做法是这个4KB的Bootstrap只做最最基础的硬件初始化时钟、SDRAM然后从Nor Flash中加载一个功能完整的U-Boot到SDRAM。U-Boot再负责加载Linux内核。这样分工明确Bootstrap追求极简和稳定U-Boot提供丰富的驱动和功能。你需要做的就是调整IMG_ADDRESS使其指向存放U-Boot.bin的Flash地址并确保U-Boot编译时指定的加载地址与Bootstrap的安排一致。折腾完这一套回头再看当初“官方不支持”的困境反而觉得是个宝贵的学习过程。它逼着你必须去理解芯片从上电第一条指令开始的所有细节理解硬件如何与软件协同。这种对系统底层透彻的掌握是以后解决各种疑难杂症的最强底气。最后附上我调试时最有用的一个习惯一定要用版本管理工具如Git来管理Bootstrap的源码每做一个重大修改或调试到一个稳定阶段就提交一次。这样当修改后系统“变砖”时你能快速回溯到上一个能工作的版本而不是在黑暗中抓瞎。这看似是软件工程的基本要求但在底层硬件调试中它能拯救你的无数个不眠之夜。