DM646x启动机制深度解析:从ROM Bootloader到多模式引导实战
1. 项目概述:深入理解DM646x的启动基石
在嵌入式系统开发中,系统上电后的第一行代码决定了整个设备的命运。对于基于德州仪器(TI)TMS320DM646x这类高度集成的数字媒体片上系统(DMSoC)而言,其内置的ARM ROM Bootloader(RBL)就是这片混沌中的第一缕曙光。它不是一段普通的固件,而是连接芯片物理世界与用户应用逻辑的桥梁,负责在电源接通、时钟稳定后,接管系统控制权,并根据硬件工程师预先设定的“指令”,从茫茫存储介质中精准定位并加载用户的应用程序。这个过程,我们称之为“引导”(Booting)。
我接触过不少基于DM6467的视频处理项目,从早期的DVR到复杂的视频分析服务器,几乎每一个项目的成败,在硬件焊接完成、首次上电的那一刻,就已经埋下了伏笔。很多工程师,尤其是软件背景的同事,常常把Bootloader视为一个“黑盒”——只要按照参考设计连接好NAND Flash,配置好启动引脚,似乎就能理所当然地启动。然而,当项目进入调试阶段,遇到启动失败、代码加载异常时,往往需要花费大量时间回溯到这个最基础的环节。因此,透彻理解DM646x的RBL工作机制,不仅仅是阅读一份技术文档,更是掌握了一种快速定位系统级问题的核心能力。
DM646x的RBL固化在芯片内部ROM的0x00008000地址开始处,这是一段出厂即烧写、不可更改的可靠代码。它的核心逻辑清晰而强大:读取芯片特定的启动模式配置引脚(BTMODE[3:0]和PCIEN),解码出一个具体的启动模式,然后按照该模式预设的流程,去指定的“地方”寻找用户的引导程序。这个“地方”可以是外部的NAND Flash芯片、串口(UART)、I2C EEPROM、SPI Flash,也可以是等待外部主机通过HPI或PCI接口来“投喂”代码。这种多模式引导的设计,为产品开发提供了极大的灵活性。例如,在量产阶段,我们可以使用成本低廉、容量大的NAND Flash作为启动设备;在工厂烧录和测试时,可以通过UART进行快速下载和验证;而在需要与主机紧密耦合的系统中,则可以采用HPI或PCI从模式启动。
注意:RBL在引导过程中会禁用看门狗定时器。这意味着,你的用户引导加载程序(UBL)或后续应用程序在初始化阶段,绝对不要尝试去配置或使能看门狗,否则可能导致系统在启动完成前意外复位。这是一个非常容易忽略却可能导致诡异启动故障的细节。
2. 启动模式全解析与硬件配置要点
启动模式的选择,本质上是通过硬件电路给芯片的“大脑”下达的第一条命令。对于DM646x,这条命令通过BTMODE[3:0]和PCIEN这五个引脚的电平状态来编码。理解这张启动模式表,是硬件设计和调试的起点。
2.1 启动模式引脚解码与配置陷阱
根据官方文档,主要的启动模式配置如下表所示。硬件设计时,必须通过上拉或下拉电阻,将这些引脚固定在所需电平。
| BTMODE[3:0] | PCIEN | ARM启动模式 | 启动设备 | 关键说明 |
|---|---|---|---|---|
| 0000 | 0或1 | 仿真启动 | ROM | 进入无限循环,等待调试器(如CCS)连接和控制。 |
| 0010 | 0 | HPI启动 (16位) | ROM | 等待外部主机通过16位HPI接口下载代码。 |
| 0010 | 1 | PCI启动 (无自动初始化) | ROM | 等待外部主机通过PCI接口配置并下载代码。 |
| 0011 | 0 | EMIFA直接启动 | EMIFA CS2 | 直接从EMIFA CS2空间获取第一条指令。 |
| 0011 | 1 | 错误 | ROM | 回退到UART0启动。 |
| 0110 | 0 | I2C主模式启动 | ROM | RBL作为I2C主机,从EEPROM读取AIS格式镜像。 |
| 0110 | 1 | NAND Flash启动 | ROM | RBL从NAND Flash读取UBL。 |
| 1000 | 0或1 | UART0启动 | ROM | RBL通过UART0与主机通信,下载UBL。 |
| 1110 | 0或1 | SPI主模式启动 | ROM | RBL作为SPI主机,从SPI Flash读取AIS格式镜像。 |
这里有几个极易出错的硬件配置陷阱需要特别强调:
PCIEN引脚的隐性风险:
PCIEN引脚不仅用于选择PCI启动,它还与EM_A22/ATA_H2/GP13引脚复用。当你使用EMIFA或ATA功能,并且将PCIEN上拉为高电平时,实际上你是在驱动PCI_RST信号。如果此时EM_A22/ATA_H2被配置为输出(在使用EMIFA/ATA时很常见),PCI_RST就无法被拉高,可能导致整个DM6467芯片一直处于复位状态,根本无法启动。因此,一个重要的经验法则是:如果系统中不使用PCI功能,务必确保PCIEN引脚通过电阻下拉到低电平(0)。未定义模式的处理:对于表中未列出的
BTMODE组合(如0100, 0101等),RBL会将其视为“保留”模式,并默认回退到UART0启动模式。这意味着如果你的硬件电路存在虚焊、短路或电阻值漂移,导致BTMODE引脚电平处于未定义状态,系统可能会意外进入UART启动,并等待主机发送数据,从而表现为“系统卡死,无任何输出”。在调试时,用万用表或示波器确认这几个引脚的实际上电电平,是排除此类问题的第一步。上拉/下拉电阻的选择:为了保证电平稳定,避免因引脚内部状态不确定导致的误判,建议为每个
BTMODE和PCIEN引脚配置明确的上拉或下拉电阻。阻值通常在4.7KΩ到10KΩ之间,需要根据具体的电源轨和驱动能力计算确认,确保在高速或噪声环境下也能保持稳定的逻辑电平。
2.2 各启动模式的核心流程与适用场景
不同的启动模式对应着不同的产品需求和开发阶段。
- 仿真启动 (Emulation Boot):这是开发者的“安全门”。当
BTMODE[3:0]=0000时,RBL会执行一个空循环。此时,系统的完全控制权交给了外部调试器(如TI的Code Composer Studio)。你可以随意地暂停CPU、查看修改寄存器、下载代码到任何内存地址。这是进行底层驱动调试、崩溃分析和早期代码开发的必备模式。 - HPI/PCI启动 (Host Boot):这两种模式本质都是“从模式”启动。RBL初始化相应的接口(HPI或PCI),然后便进入等待状态,将自身置于从属地位。它等待外部的主机处理器通过该接口来配置DM646x的内存空间、下载用户引导程序(UBL)、并最终触发UBL的执行。这种模式常用于多处理器系统中,DM646x作为协处理器,由主CPU负责其启动和任务调度。
- EMIFA直接启动 (EMIFA Direct Boot):这是一种最“原始”的启动方式。它完全绕过了RBL。系统上电后,硬件逻辑会将ARM的初始取指地址0x00000000重映射到EMIFA的CS2空间起始地址(0x42000000)。因此,你必须将可执行代码直接存放在连接在EMIFA CS2上的ROM或Flash的起始位置。这种方式几乎没有软件开销,但缺乏灵活性,通常用于对启动时间有极端要求的场合,或者作为二级引导的跳板。
- NAND Flash启动:这是量产产品中最常见、最经济的启动方式。RBL会扮演一个简单的NAND Flash驱动,从芯片指定的块(Block 1之后)中搜索特定的UBL描述符,并将其加载到内部RAM中执行。它支持坏块跳过和有限次数的重试,提供了基本的可靠性。
- UART启动:这是开发和工厂烧录的利器。通过串口工具,你可以快速地将编译好的UBL镜像下载到板卡的内存中并运行,无需预先烧录Flash。这对于快速验证软件修改、进行黑盒测试或小批量生产烧录极其方便。
- I2C/SPI主模式启动:这两种模式要求外部存储器(通常是EEPROM或串行Flash)中存储的必须是符合AIS(Application Image Script)格式的镜像。RBL作为总线主机,主动读取AIS脚本,并解析其中的命令(如加载段、跳转、CRC校验等)来加载用户代码。这种方式引脚占用少,电路简单,适合代码量较小(通常小于64KB)的应用。
3. 核心启动流程的深度拆解与实操
理解了模式选择,我们深入到两种最常用模式——NAND启动和UART启动的内部,看看RBL到底做了哪些“脏活累活”。
3.1 NAND Flash启动:从存储颗粒到内存执行
NAND启动的流程看似直白:读ID、找数据、拷内存、跳转执行。但魔鬼藏在细节里,任何一个环节的误解都可能导致启动失败。
3.1.1 RBL的NAND启动探秘流程
- 硬件接口初始化:RBL首先根据
CS2BW引脚的状态,确定EMIFA与NAND Flash通信的数据位宽(8位或16位)。然后,它会按照固定的时序初始化EMIFA的CS2空间,用于访问NAND。 - 读取设备ID:RBL向NAND Flash发送
0x90命令(Read ID),从数据线上读取制造商ID和设备ID。这个ID至关重要,RBL内部维护了一个支持列表(如原文表4),根据ID来决定后续操作的时序参数,例如页大小(512+16字节或2048+64字节)、块大小(16页/块或32页/块等)、以及寻址周期数(3周期或4周期)。 - 搜索UBL描述符:这是最关键的步骤。RBL从**块1(Block 1)的第0页(Page 0)**开始搜索。为什么是块1?因为NAND Flash的块0通常用于存储厂家的坏块标记或其它信息(CIS/IDI),RBL默认跳过块0。它在页数据区的前16个字节寻找一个特定的“魔法数字”(Magic Number)
0xA1ACED00,作为UBL描述符的起始标志。 - 解析描述符与数据加载:一旦找到魔法数字,RBL会将其后的16字节作为UBL描述符解析。这个描述符定义了用户程序的“地图”:
UBL Entry Point(4字节):UBL在ARM内部RAM中的入口地址,必须是0x00000020到0x000074FF之间。Code Size (Pages)(4字节):UBL占用的总页数。UBL Starting Block #(4字节):UBL数据实际存放的起始块号。UBL Starting Page #(4字节):UBL数据在实际起始块内的起始页号。 RBL根据这张“地图”,从指定的块和页开始,连续读取指定页数的数据,将其拷贝到ARM内部RAM(ITCM)中,起始地址为0x00000020。
- 错误处理与冗余备份:如果在读取UBL数据过程中发生ECC错误(通过读取NAND的额外空间状态字节判断),RBL会立即中止当前拷贝,并从找到魔法数字的那个块的下一个块开始,重新搜索魔法数字。这个过程最多重复5次。这意味着你可以在连续的几个块中重复存储多份UBL镜像,以应对NAND的坏块问题,提高启动成功率。
- 跳转执行:数据拷贝完成后,RBL直接跳转到描述符中指定的入口地址,将CPU控制权交给UBL。
3.1.2 UBL镜像的制备与烧录实操
要让RBL成功加载,你的UBL二进制文件必须经过正确格式化。假设你的UBL代码链接地址(Load Address)是0x00000020,你需要生成一个纯二进制(binary)文件,然后按照以下结构将其写入NAND Flash:
准备UBL描述符:你需要创建一个包含5个32位整数(小端格式)的描述符头。
// 示例:UBL描述符数据结构 typedef struct _UBL_Descriptor { uint32_t magicNumber; // 必须为 0xA1ACED00 uint32_t entryPoint; // 例如 0x00000020 uint32_t pageCount; // UBL镜像占用的总页数 uint32_t startBlock; // UBL数据起始块号,例如 1 uint32_t startPage; // UBL数据在起始块内的页号,例如 0 } UBL_Descriptor;计算
pageCount:pageCount = ceil(UBL_binary_size / NAND_page_data_size)。例如,UBL大小为10KB,NAND页数据区为512字节,则pageCount = ceil(10240 / 512) = 20。构建完整的NAND镜像:将描述符和UBL二进制数据拼接起来。假设你决定将UBL存放在块1、页0开始的位置。
- 在块1、页0的数据区,写入上述描述符。
- 紧接着,从块1、页0的剩余空间开始(或从页1开始,如果描述符占满页0),连续写入你的UBL二进制数据。
烧录到NAND:使用编程器或通过JTAG/UART引导的临时烧录程序,将构建好的镜像写入NAND Flash的物理地址。务必注意:你需要将逻辑上的“块1、页0”转换为NAND Flash芯片的物理地址,这通常需要根据芯片的寻址周期(3-cycle或4-cycle)来计算行地址(Row Address)。
实操心得:在实际项目中,我强烈建议不要将UBL描述符和UBL数据紧密存放在一起。更好的做法是:将UBL描述符写在块1页0,而将UBL数据本体写在块2页0开始。这样,即使块1因频繁更新描述符而损坏,你的UBL数据仍然是安全的。只需在描述符中修改
startBlock为2即可。此外,务必利用RBL的冗余搜索特性,在连续的3-5个块中都写入完整的UBL镜像(包括描述符和数据),这能极大提升量产产品的启动鲁棒性。
3.2 UART启动:串口协议下的代码下载
UART启动是交互式调试和快速迭代的利器。其本质是一个基于特定握手协议的文件传输过程。
3.2.1 RBL的UART启动握手协议
- 发送BOOTME:RBL初始化UART0(115200, 8N1),然后持续发送字符串“
^BOOTME”(注意开头是空格)直到超时或收到响应。这是它在向世界宣告:“我准备好了,可以接收代码!” - 等待ACK:主机端(通常是PC上的串口工具)在收到“BOOTME”后,必须在一个超时时间内(RBL会轮询定时器中断标志)回复一个28字节的ACK数据包。这个数据包包含:
- 8字节ASCII字符串“
^^^^ACK”(开头四个空格)。 - UBL的4字节CRC32校验和。
- UBL的4字节数据长度(字节数)。
- UBL的4字节入口地址(必须是
0x00000020-0x000074FF)。 - 4字节的零填充(TBD)。
- 8字节ASCII字符串“
- 发送BEGIN与传输CRC表:RBL校验ACK包格式后,发送“
^^BEGIN”字符串,通知主机开始传输。主机首先发送一个1KB(1024字节)的CRC32查找表。RBL接收后计算这1KB数据的8位校验和(Checksum8)。如果正确,回复“^^^DONE”;如果错误,回复“CORRUPT”并回到第1步。 - 传输UBL数据:CRC表传输成功后,主机开始发送真正的UBL二进制数据。RBL在接收过程中会使用之前传来的CRC32值进行校验。同样,每接收完一段数据(具体大小由协议决定),会进行Checksum8校验,正确则回复“DONE”,错误则回复“CORRUPT”。
- 跳转执行:全部UBL数据接收并校验通过后,RBL跳转到ACK包中指定的入口地址执行。
3.2.2 主机端工具的使用与镜像生成
TI通常会提供配套的串口下载工具(如serial_flasher或uart_boot)。你需要做的是:
- 生成UBL镜像:与NAND启动一样,你需要一个链接地址在
0x00000020,大小不超过30KB的纯二进制(.bin)文件。 - 计算CRC32:使用工具(如
genAIS.pl或自定义脚本)为你的UBL.bin文件计算CRC32校验和。这个值需要填入主机的ACK包中。 - 执行下载:运行主机工具,指定串口号、UBL文件路径、入口地址等参数。工具会自动完成上述握手协��,将代码下载到板卡并运行。
避坑指南:UART启动失败,十有八九是握手协议不对。首先,确保串口参数绝对正确:115200波特率、8数据位、无校验、1停止位、无流控。其次,很多工程师自己编写串口工具时,容易忽略ACK包中字符串的精确格式(包括空格和结束符
\0)以及字节序(CRC32、长度、地址均为小端格式)。一个有效的调试方法是,先用TI官方工具成功启动一次,然后用逻辑分析仪或带串口监控功能的调试器,捕获整个通信过程的数据流,作为自己开发工具的黄金参考。
4. 高级启动模式与AIS格式精讲
对于I2C和SPI启动模式,RBL不再直接处理原始的二进制文件,而是要求一种名为AIS(Application Image Script)的脚本格式。这相当于给启动过程增加了一个灵活的“指挥棒”。
4.1 AIS格式:启动脚本的构成
AIS是一个包含命令和数据的二进制序列。RBL支持的AIS命令是有限的子集,主要包括:
SECTION_LOAD:将一段数据加载到指定的内存地址。JUMP_CLOSE:跳转到指定地址执行,并关闭AIS解析器。ENABLE_CRC/DISABLE_CRC:启用或禁用后续数据的CRC校验。REQUEST_CRC:请求对之前加载的数据进行CRC校验。
一个典型的AIS脚本流程是:
- 以一个“魔法数字”(Magic Number)
0x41504954(ASCII “TIPA”)开始。 - 发送
ENABLE_CRC命令,开启CRC校验以确保数据完整性。 - 发送一个或多个
SECTION_LOAD命令,将不同的代码段或数据段加载到内存的不同位置(如将初始化代码段加载到ITCM,将数据段加载到DDR2)。 - 发送
REQUEST_CRC命令,并附带期望的CRC值,RBL会进行计算和比对。 - 最后,发送
JUMP_CLOSE命令,指定应用程序的入口地址,RBL跳转后任务完成。
4.2 I2C/SPI启动实操与镜像生成
4.2.1 生成AIS镜像
你需要使用TI提供的genAIS.plPerl脚本(通常位于DVSDK工具链中)来将你的.out或.bin文件转换为AIS格式。
对于I2C启动,生成ASCII格式的AIS文件,便于烧录器识别:
genAIS.pl -i my_application.out -o my_application.ais -otype ascii -bootmode i2cmaster -addrsz 16对于SPI启动,生成二进制格式的AIS文件:
genAIS.pl -i my_application.out -o my_application.ais -otype bin -bootmode spimaster -addrsz 16这里的-addrsz 16指定了EEPROM/Flash的地址总线宽度为16位(即最大寻址64KB)。
4.2.2 硬件连接与烧录
- I2C EEPROM:通常选用24LC系列等,支持标准I2C协议。RBL固定使用从机地址
0x50(7位地址),并以约90kHz的时钟频率进行读取。你需要将EEPROM连接到DM646x的I2C总线(如I2C0)上,并将AIS文件烧录到EEPROM的起始地址(通常为0x0000)。 - SPI Flash:如AT25系列、W25Q系列等。RBL使用SPI0接口,主模式,时钟频率约990kHz,仅使用
SPI_CLK,SPI_SIMO,SPI_SOMI,SPI_CS0四根线。同样,将AIS文件烧录到SPI Flash的起始地址。
烧录工具可以是通用的编程器,也可以利用DM646x的JTAG接口,通过CCS运行TI提供的i2c_eeprom_writer.out或spi_eeprom_writer.out程序来完成烧录,这在开发阶段非常方便。
注意事项:I2C/SPI启动模式对代码体积有严格限制,因为RBL在解析AIS脚本和加载数据时,使用的是芯片内部的ROM代码空间和有限的栈/数据区。它无法在加载过程中初始化DDR2等外部内存。因此,你的AIS脚本中
SECTION_LOAD命令加载的所有数据,其目标地址必须在ARM内部RAM(ITCM/DTCM,即0x00000000-0x0000FFFF)的范围内。你的第一段引导代码必须足够小,且职责明确:初始化关键外设(如PLL、DDR2控制器),然后将更大的应用程序从外部存储器(如NAND, SPI Flash)搬运到已初始化的DDR2中,最后跳转到DDR2中执行。这是一种典型的二级引导策略。
5. 实战疑难排查与经验总结
即使理解了所有原理,在实际硬件上调试Bootloader仍然可能遇到各种问题。以下是我在多个项目中总结的常见故障排查思路。
5.1 通用启动失败排查流程
- 确认电源与时钟:最基础也最易忽略。用示波器测量核心电压(CVDD)、DDR电压、PLL电源是否稳定且达到额定值。测量晶振是否起振,时钟输出是否正常。
- 确认启动模式引脚:在上电瞬间,用示波器捕获
BTMODE[3:0]和PCIEN引脚的电平,确保与原理图设计一致,且没有毛刺。特别注意上拉/下拉电阻是否焊接正确。 - 检查复位信号:确保芯片的复位引脚(
RESET)在上电后有一个从低到高的正确跳变,并且保持高电平。检查是否有其他器件误拉低了复位。 - 追踪RBL最初执行:如果条件允许,使用JTAG仿真器连接芯片,设置为“仿真启动”模式。在CCS中,在RBL的入口地址(
0x00008000)或ARM的复位向量处设置断点。如果能命中,说明芯片已脱困复位状态,可以单步跟踪RBL的最初执行路径,查看它读取了哪个启动模式,以及是否进入了对应的分支。 - 利用UART回退信息:这是最宝贵的调试手段。如果RBL在所选启动模式中失败(如NAND读取错误、I2C无应答),它会回退到UART0启动模式,并持续发送“BOOTME”字符串。因此,即使你配置的是NAND启动,也请务必连接UART0到PC串口,打开串口助手(115200, 8N1)。如果上电后收到“BOOTME”,说明RBL已运行,但首选启动模式失败。这大大缩小了问题范围。
5.2 NAND启动专项排查
- 现象:无任何输出,或UART打印“BOOTME”。
- 排查步骤:
- 检查硬件连接:确认NAND Flash的
CE#、WE#、RE#、ALE、CLE、R/B#等信号线是否正确连接到DM646x的EMIFA对应引脚,尤其是ALE和CLE对应EM_A16和EM_A17。检查上拉电阻。 - 确认NAND型号兼容性:这是最大的坑!DM6467的AEMIF不支持“非CE无关”(non-CE don‘t-care)型NAND Flash。这种Flash在进行读操作时,要求片选信号
CE#在整个tR(数据输出延迟)期间保持有效(低电平)。而DM6467的硬件在发出读命令后就会释放CE#。务必查阅你的NAND Flash数据手册的“读操作时序图”,确认在tR期间,CE#是“无关”(Don‘t Care)状态。例如,三星K9F1208U0C的TSOP封装版本是“非CE无关”的,不支持;而意法半导体NAND512W3A2C或三星同型号的FBGA封装版本是“CE无关”的,支持。 - 检查UBL镜像格式:确认UBL描述符的魔法数字、入口地址、页数计算是否正确。确认镜像是否烧录到了正确的物理块/页(跳过块0)。使用编程器读取NAND内容,核对前几个字节。
- 检查坏块:新的NAND Flash也可能有出厂坏块。确保你没有将UBL烧录到标记为坏块的区域。RBL只会跳过搜索过程中的坏块,但你的烧录工具需要主动避开它们。
- 检查硬件连接:确认NAND Flash的
5.3 UART启动专项排查
- 现象:串口无“BOOTME”输出,或主机工具握手失败。
- 排查步骤:
- 检查串口线路:TX、RX是否接反?电平是否是3.3V?串口工具参数是否绝对匹配(115200, 8N1, 无流控)?
- 捕获通信波形:使用逻辑分析仪同时抓取DM646x的
UART0_TX和UART0_RX引脚。看RBL是否发出了“BOOTME”字符串(ASCII码可解码)。如果发出了,但PC没收到,检查电平转换电路。如果没发出,回到通用排查流程。 - 核对握手协议:如果RBL发送了“BOOTME”且PC收到了,但后续失败,请逐字节比对你的主机工具发送的ACK包与协议规定是否完全一致。特别注意字符串中的空格字符和结尾的NULL字符。CRC32、长度、地址的值要用小端字节序发送。
5.4 I2C/SPI启动专项排查
- 现象:启动失败,回退到UART模式。
- 排查步骤:
- 检查AIS文件:用二进制查看工具打开生成的
.ais文件,确认开头是否是0x41504954(“TIPA”)。可以使用genAIS.pl的-verbose选项生成更详细的输出,检查命令序列。 - 检查EEPROM/Flash内容:用编程器读取已烧录的存储器内容,与生成的AIS文件进行二进制比较,确保烧录无误。
- 检查硬件连接与地址:确认I2C/SPI的线序正确,上拉电阻已安装。对于I2C,确认EEPROM的7位从机地址是否为
0x50(通常由A0/A1/A2引脚决定)。用示波器或逻辑分析仪抓取总线波形,看RBL是否发出了正确的读命令和地址,以及存储器是否回复了ACK和数据。
- 检查AIS文件:用二进制查看工具打开生成的
调试Bootloader是一个需要耐心和系统方法的过程。它横跨硬件、底层软件和工具链。我的习惯是,在新板卡第一次上电前,就准备好JTAG仿真器、示波器、逻辑分析仪和串口调试工具。从最基础的电源、时钟、复位查起,再到启动引脚,最后深入到具体的启动协议。每一次成功的启动,都是对系统底层理解的一次深化。DM646x的这套多模式引导机制虽然复杂,但一旦掌握,就能为你的嵌入式系统设计带来巨大的可靠性和灵活性。记住,清晰的逻辑、正确的工具和细致的验证,是攻克任何启动难题的不二法门。