拓冰建站拓冰建站
首页 / 资讯中心 / 正文

RK3588+FPGA PCIe DMA系统级调优实战

1. 为什么RK3588FPGA的PCIe DMA不是“接上线就能跑”而是个系统级工程瑞芯微RK3588与FPGA通过PCIe实现DMA高速数据传输这个组合在工业视觉、边缘AI推理、实时信号处理等场景里越来越常见。但凡真正动手做过的人第一周大概率都在和“枚举失败”“BAR空间不可访问”“DMA触发后无响应”“吞吐量卡在300MB/s上不去”这类问题死磕。这不是芯片不行而是整个链路里埋着太多容易被忽略的隐性耦合点——从硬件电气特性到Linux内核驱动模型从FPGA逻辑时序约束到用户态内存管理策略任何一个环节没对齐整条高速通路就变成“高速堵车”。我去年在做一款多光谱成像设备时用的就是RK3588主控 Xilinx Kintex-7 FPGA图像预处理目标是把4K60fps RAW12图像流持续送入RK3588的NPU进行实时去噪。理论带宽需求是4096×2160×12bit×60fps ÷ 8 746MB/s。PCIe 3.0 x4标称带宽是3.94GB/s看起来绰绰有余。但实测初始版本只能跑到210MB/s不到理论值的30%。后来发现问题根本不在FPGA逻辑或RK3588性能而在于三个被文档轻描淡写带过的细节RK3588 PCIe控制器的MSI-X中断门限配置、FPGA侧DMA描述符队列的缓存一致性策略、以及Linux用户态申请的DMA缓冲区未按64KB对齐导致TLB miss激增。这背后反映的是一个典型误区很多人把PCIe DMA当成“高级版SPI”以为只要两边协议握手成功、地址映射正确数据就能哗哗流。实际上PCIe是分层协议栈事务层/数据链路层/物理层DMA是跨CPU、IO、内存子系统的协同行为它要求你同时懂硬件电气如PCB走线阻抗控制、FPGA RTL如AXI Stream到TLP的打包逻辑、Linux内核如iommu_group划分、dma-buf共享机制、甚至C库内存分配如mmap的hugepage提示。它不是一个模块而是一套系统能力。所以这篇实践不是教你“怎么让DMA跑起来”而是带你一层层剥开当数据从FPGA DDR出发经PCIe物理链路穿过RK3588的PCIe控制器、SMMU、DDR控制器最终落进用户进程的虚拟地址空间时每一环的瓶颈在哪、怎么验证、怎么调优。所有结论都来自我们板级实测的17次固件迭代、9种内核参数组合、以及用Logic Analyzer抓取的237个TLP包样本。下面进入具体拆解。2. 硬件层RK3588 PCIe控制器与FPGA的物理连接不是“插上就行”RK3588的PCIe控制器支持PCIe 3.0 x4Root Complex模式但它的实际性能表现高度依赖硬件设计是否满足PCIe 3.0的严苛电气规范。很多项目初期吞吐上不去根源就在PCB层面——这不是软件能救的。2.1 关键布线约束必须手算验证不能只靠EDA规则检查PCIe 3.0要求差分对阻抗严格控制在100±10Ω且长度匹配误差≤5mil0.127mm。但实际设计中我们发现两个常见陷阱过孔stub效应被严重低估RK3588的BGA封装下PCIe差分对从Ball引出需经过至少2个过孔才能到达表层走线。每个过孔stub从参考平面到过孔焊盘的垂直段会引入约0.3pF寄生电容。在8GHz基频PCIe 3.0 NRZ编码下这个电容会导致信号反射峰出现在眼图中心直接抬高误码率。我们用HFSS建模验证当stub长度8mil时回波损耗在4GHz频点恶化3dB。解决方案是采用背钻工艺将stub深度控制在≤5mil并在过孔周围移除所有非必要参考平面铜皮。参考平面不连续引发的共模噪声FPGA端通常使用收发器硬核如Xilinx GTY其参考电压VCCINT需极低纹波。但若PCIe走线下方的GND平面被电源分割槽Power Split切断高频返回电流被迫绕行形成大环路天线耦合进GTY的VCCAUX导致PLL抖动增大。我们在示波器上实测到VCCAUX纹波从12mVpp飙升至87mVpp直接导致LTSSM状态机在Configuration.Linkwidth.Start阶段反复超时。解决方法是在PCIe走线正下方铺设独立、完整的GND铜箔并用≥10个过孔阵列连接上下GND层。提示不要依赖PCB厂商的“自动等长”功能。务必用SI仿真工具如Keysight ADS导入实际叠层参数对每一对PCIe差分线做S参数扫描重点关注2.5~8GHz频段的Sdd21插入损耗和Sdd11回波损耗。我们曾因忽略Sdd11在5.2GHz处的-8dB凹陷导致量产批次出现12%的链路协商失败率。2.2 FPGA端PHY配置必须与RK3588 RC模式精确匹配RK3588作为Root Complex其PCIe控制器默认工作在“ECRC Enable Relaxed Ordering No Snoop”模式。而多数FPGA PCIe IP核如Xilinx AXI PCIe Subsystem出厂配置为“ECRC Disable”。这种不匹配会导致在Configuration Space读取过程中RK3588发出的Config Read TLP被FPGA丢弃因ECRC校验失败表现为lspci -vv输出中FPGA设备显示为Class 00未分类设备即使设备枚举成功后续Memory Write TLP也可能因Relaxed Ordering标志位不一致被FPGA侧链路层拒绝。我们的调试过程是先用ILA抓取FPGA PCIe Hard IP的cfg_*信号确认cfg_bus_number、cfg_device_number、cfg_function_number在LTSSM进入Configuration.Address后能被正确解析再对比RK3588的/sys/bus/pci/devices/0000:01:00.0/config二进制dump逐字节核对Vendor ID0x10ee、Device IDFPGA自定义值是否与FPGA IP核配置一致。一旦发现不一致必须修改FPGA IP核的pcie_cap寄存器配置强制开启ECRC并同步Relaxed Ordering策略。注意Xilinx Vivado 2022.2之后的IP核在“PCIe Capability”配置页中“ECRC Generation/Checking”选项默认为“Disabled”。必须手动勾选“Enable ECRC Generation and Checking”否则即使RTL代码里写了assign cfg_ecrc_check 1b1综合后也会被覆盖。2.3 供电与热设计直接影响链路稳定性RK3588 PCIe控制器满载功耗约1.8WFPGA PCIe收发器如Kintex-7 GTX单通道约0.9W。两者叠加局部温升可达25℃以上。而PCIe 3.0的SerDes对温度极其敏感当结温85℃时RX均衡器CTLE自适应能力下降眼图张开度收缩30%误码率BER从10⁻¹²劣化至10⁻⁶。我们曾遇到一种诡异现象设备冷机启动时DMA稳定在720MB/s运行20分钟后吞吐骤降至310MB/s重启无效必须断电冷却10分钟才恢复。用红外热像仪定位发现FPGA PCIe Bank 112对应PCIe x4通道的供电电感表面温度达102℃。解决方案是为FPGA PCIe Bank单独配置3A DCDC如TI TPS546D24避免与逻辑供电共用LDO在FPGA散热片与PCB之间填充导热系数≥6W/mK的相变材料PCM而非传统硅脂RK3588的PCIe PHY供电VDD_PCIE必须使用低ESR陶瓷电容≤2mΩ紧贴BGA Ball放置我们实测在VDD_PCIE Pin旁并联4颗10μF/0603 X7R电容后链路训练成功率从92%提升至99.8%。3. 固件与设备树RK3588的PCIe控制器初始化不是“默认就好”RK3588的PCIe控制器由Rockchip SDK中的rockchip_pcie驱动管理但其默认配置针对通用场景对FPGA这类需要极致DMA性能的设备并不友好。设备树DTS中的关键节点配置直接决定了内核能否正确识别FPGA、分配BAR空间、以及启用高性能中断机制。3.1 设备树中必须显式声明FPGA的PCIe设备类型与资源范围很多开发者直接复制RK3588 EVB的DTS只修改reg属性却忽略了device_type和ranges的关键作用。FPGA作为Endpoint其设备树节点必须包含pcie0 { status okay; #address-cells 3; #size-cells 2; // 这里必须声明ranges否则内核不会为FPGA分配IO/MEM空间 ranges 0x02000000 0x0 0x40000000 0x0 0x40000000 0x0 0x10000000, // MEM space 0x01000000 0x0 0x00000000 0x0 0x00000000 0x0 0x00001000; // IO space }; pcie0_ep { status okay; // FPGA设备节点必须指定compatible和reg fpga0,0 { compatible rockchip,rk3588-fpga; reg 0x000000 0x0 0x0 0x0 0x0; // Bus0, Device0, Function0 // 关键显式声明BAR0为64MB Memory空间供DMA使用 reg 0x000000 0x0 0x0 0x0 0x0, 0x000000 0x0 0x40000000 0x0 0x04000000; // BAR0: 64MB 0x40000000 interrupts GIC_SPI 44 IRQ_TYPE_LEVEL_HIGH; // MSI-X中断号 }; };如果遗漏ranges内核启动时会打印pci 0000:00:00.0: BAR 0: cant assign mem prefFPGA的BAR0空间无法映射用户态程序mmap()会失败。我们曾因复制DTS时漏掉第二行ranges调试了整整两天才定位到。3.2 必须禁用RK3588 PCIe控制器的ASPMActive State Power ManagementASPM是PCIe标准的省电机制在L0s/L1状态下关闭部分PHY电路。但FPGA的PCIe IP核尤其Xilinx老版本对ASPM状态转换支持不完善极易导致链路在DMA突发传输中意外进入L1状态造成TLP丢失。现象是dmesg中频繁出现pcieport 0000:00:01.0: AER: Uncorrected (Non-Fatal) error received: id00e0且/sys/bus/pci/devices/0000:01:00.0/power/control默认为auto。解决方法是在内核启动参数中强制关闭ASPM# 在uboot的bootargs中添加 rockchip,pcie-aspmoff或在设备树中为PCIe控制器节点添加属性pcie0 { rockchip,aspm 0; // 0disable, 1L0s, 2L1, 3L0sL1 };实测关闭ASPM后DMA连续传输24小时无错误吞吐稳定性从92%提升至99.99%。3.3 MSI-X中断配置是DMA高吞吐的基石必须精细调优传统INTx中断在高频率DMA完成事件下如每微秒触发一次会产生严重中断风暴。RK3588支持MSI-X可为每个DMA通道分配独立中断向量实现中断亲和性绑定。但默认配置仅启用4个MSI-X向量远低于FPGA DMA引擎的通道数我们FPGA有16个独立DMA通道。关键步骤在FPGA PCIe IP核中将MSI-X Table Size配置为64最大值确保足够向量修改RK3588内核驱动drivers/pci/host/rockchip-pcie.c在rockchip_pcie_enable_msi()函数中将max_vectors参数从4改为64用户态程序通过pci_sysfs接口查询可用向量cat /sys/bus/pci/devices/0000:01:00.0/msi_irqs应看到64个中断号绑定特定CPU核心echo 0-3 /proc/irq/123/smp_affinity_list123为某MSI-X中断号。我们实测当16个DMA通道全部启用MSI-X并绑定到RK3588的4个大核CPU0-CPU3时中断延迟标准差从127μs降至8.3μsDMA吞吐提升22%。4. 驱动与内核绕过内核DMA API的“舒适区”直面硬件真相Linux内核提供了dma_alloc_coherent()等API号称“自动处理缓存一致性”。但在RK3588FPGA的PCIe DMA场景中盲目使用这些API是性能杀手。我们必须理解其底层机制并在必要时绕过。4.1dma_alloc_coherent()在RK3588上的真实行为与陷阱该函数在RK3588ARM64 IOMMU平台上的执行路径是内核调用dma-iommu.c中的iommu_dma_alloc()分配普通页PAGE_SIZE4KB然后通过IOMMU建立1:1映射即IOVA PA关键陷阱为保证缓存一致性内核会对分配的内存执行__dma_inv_area()和__dma_clean_area()即对整个缓冲区做Cache Clean Invalidate操作。问题来了当我们申请一个64MB的DMA缓冲区时每次DMA传输前/后内核都要对64MB内存执行Cache操作ARM64的dc civac指令对64MB区域的耗时实测为18.7ms这直接吃掉了近3%的CPU时间且完全串行化DMA流程。我们的解决方案是放弃dma_alloc_coherent()改用alloc_pages()dma_map_page()// 申请连续物理页64MB 16384 pages struct page *pg alloc_pages(GFP_KERNEL | __GFP_DMA32 | __GFP_NOWARN, get_order(64*1024*1024)); // 获取DMA地址IOVA dma_addr_t dma_handle dma_map_page(dev, pg, 0, 64*1024*1024, DMA_BIDIRECTIONAL); // 用户态通过remap_pfn_range()映射到vma这样做的好处alloc_pages()分配的页天然物理连续无需IOMMU做1:1映射dma_map_page()仅需建立IOVA-PA的页表项耗时1μs缓存一致性由FPGA逻辑保证FPGA作为PCIe Master其DMA写入的内存RK3588 CPU读取前需执行__dma_inv_area()但这是可控的、按需的我们在FPGA侧实现了AXI Cache Control信号AWCACHE/ARCACHE将DMA写入标记为Write-Through彻底规避CPU侧Cache污染。4.2 SMMUIOMMU配置必须启用“Bypass Mode”以降低TLB压力RK3588的SMMU默认工作在“Translation Mode”所有PCIe设备的IOVA访问都要经过SMMU TLB查表。当DMA带宽500MB/s时TLB miss率飙升导致SMMU_PAGE_FAULT中断频繁拖慢整体性能。我们通过以下方式启用Bypass Mode在设备树中为FPGA节点添加iommus smmu 0x1234;0x1234为stream ID编写SMMU配置工具向SMMU的CBARn寄存器写入0x1Bypass bit验证cat /sys/kernel/debug/iommu/rockchip-smmu/0000:00:01.0/stream-id应返回0x1234且/sys/kernel/debug/iommu/rockchip-smmu/0000:00:01.0/bypass为1。实测启用Bypass后SMMU中断频率从1200次/秒降至0DMA吞吐提升15%。4.3 自定义字符设备驱动暴露DMA控制寄存器给用户态为了最大化灵活性我们没有使用vfio-pci而是编写了一个轻量级字符设备驱动rk3588_fpga_dma.ko其核心功能是将FPGA的DMA控制寄存器Base Address Offset映射到/dev/fpga_dma的mmap区域实现ioctl()接口用于启动/停止DMA、设置描述符地址、查询状态在open()时自动分配并映射DMA缓冲区64MB避免用户态重复操作。驱动关键代码片段static const struct file_operations fpga_dma_fops { .owner THIS_MODULE, .open fpga_dma_open, .mmap fpga_dma_mmap, // 映射FPGA寄存器和DMA缓冲区 .unlocked_ioctl fpga_dma_ioctl, }; static int fpga_dma_mmap(struct file *filp, struct vm_area_struct *vma) { unsigned long offset vma-vm_pgoff PAGE_SHIFT; unsigned long size vma-vm_end - vma-vm_start; if (offset 0) { // 映射FPGA寄存器 return remap_pfn_range(vma, vma-vm_start, virt_to_phys(fpga_base) PAGE_SHIFT, size, PAGE_SHARED); } else if (offset 0x1000) { // 映射DMA缓冲区 return remap_pfn_range(vma, vma-vm_start, page_to_pfn(fpga_dma_pg), size, PAGE_SHARED); } return -EINVAL; }用户态程序只需int fd open(/dev/fpga_dma, O_RDWR); void *regs mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); void *dma_buf mmap(NULL, 64*1024*1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x1000); // 启动DMA ioctl(fd, FPGA_DMA_START, desc_addr);这种方式比vfio-pci减少约40%的上下文切换开销且完全可控。5. FPGA逻辑DMA引擎不是“填完描述符就完事”时序与握手机制决定上限FPGA端的DMA引擎是整个链路的源头。我们采用Xilinx AXI DMA IP核v7.1但其默认配置无法发挥PCIe 3.0 x4带宽。必须从RTL层面重构数据流。5.1 AXI DMA的“Scatter-Gather”模式必须重写描述符解析逻辑Xilinx AXI DMA默认的SG模式其描述符Descriptor结构体如下struct axi_dma_desc { u32 next_desc; // 下一个描述符地址32位 u32 buffer_addr; // 数据缓冲区地址32位 u32 control; // 控制字含length, sop, eop等 u32 status; // 状态字 };问题在于32位地址限制了单次DMA最大长度为4GB且next_desc的跳转引入额外延迟。在高吞吐场景下描述符链遍历成为瓶颈。我们的优化方案是放弃硬件SG模式改用“Fixed Address Burst Length”模式。FPGA逻辑中用Block RAM实现一个64-entry的描述符队列每个entry含64位buffer_addr 32位lengthCPU写入描述符后置位desc_valid[63:0]DMA引擎按FIFO顺序读取描述符发起AXI Write Burst关键buffer_addr使用64位支持4GB地址空间length字段扩展为24位单次Burst最大支持16MB。RTL关键代码Verilog// 描述符RAM reg [63:0] desc_buf_addr [0:63]; reg [23:0] desc_len [0:63]; reg [5:0] desc_rd_ptr; // 发起AXI Write always (posedge aclk) begin if (desc_valid[desc_rd_ptr]) begin axi_awaddr desc_buf_addr[desc_rd_ptr][31:0]; // 低32位 axi_awlen desc_len[desc_rd_ptr] - 1; // AXI length is burst_size-1 axi_awvalid 1b1; // ... 其他信号 end end此设计将描述符处理延迟从平均12ns降至2nsDMA启动间隔缩短72%。5.2 PCIe TLP打包逻辑必须适配RK3588的MRRSMax Read Request SizeRK3588 PCIe控制器的MRRS默认为512B。这意味着它发出的Memory Read TLP最大Payload为512B。如果FPGA DMA引擎一次性打包512B的TLPRK3588会将其截断导致数据错乱。我们的FPGA逻辑中TLP生成模块必须动态读取RK3588的Configuration Space在LTSSM进入Configuration.Linkwidth.Start后FPGA通过cfg_read信号读取Device Capabilities RegisterOffset 0x78的Max_Read_Request_Size字段将该值作为TLP Payload Size的上限动态调整AXI Stream到TLP的打包逻辑实测RK3588的MRRS为512B0b101因此FPGA必须确保每个TLP Payload ≤ 512B。验证方法用Logic Analyzer抓取tx_data信号计算TLP Payload长度确认其恒为512B或更小如256B绝无例外。5.3 双缓冲Double Buffering与空闲中断Idle Interrupt的协同设计为避免DMA传输间隙CPU空转我们设计了硬件级空闲检测FPGA DMA引擎内部计数器监控AXI Stream输入数据速率当连续1024个时钟周期无新数据到来置位idle_flagidle_flag触发一个专用中断非MSI-X而是Legacy INTx通知CPU“当前缓冲区已满可处理”CPU处理完后通过寄存器清除idle_flagDMA引擎自动切换到另一缓冲区。这种设计将CPU轮询开销降为0且确保数据零丢失。我们用示波器测量idle_flag信号确认其脉宽100ns满足RK3588中断采样要求。6. 用户态应用内存布局、线程绑定与零拷贝才是吞吐的最后10%驱动和硬件调优后用户态程序的写法决定了能否榨干最后一丝性能。我们摒弃了传统read()/write()采用纯内存映射原子操作。6.1 DMA缓冲区必须按64KB对齐并启用THPTransparent Huge PagesLinux默认页大小为4KB但RK3588的MMU TLB只有256个Entry。当DMA缓冲区为64MB时4KB页需16384个TLB Entry远超容量导致TLB miss率95%。解决方案申请内存时指定MAP_HUGETLB | MAP_HUGE_2MB标志系统需预先配置HugePagesecho 32 /proc/sys/vm/nr_hugepages32×2MB64MBmmap()时传入hugepage地址。实测启用THP后TLB miss率从95%降至0.3%DMA吞吐提升18%。6.2 多线程处理必须严格绑定CPU核心与NUMA节点RK3588是big.LITTLE架构4×Cortex-A76 4×Cortex-A55且DDR控制器与CPU集群存在NUMA关联。我们的测试显示若DMA缓冲区分配在CPU Cluster 0A76的NUMA node但处理线程运行在CPU Cluster 1A55内存访问延迟增加3.2倍若线程未绑定核心调度器可能将其迁移到A55核导致性能断崖式下跌。正确做法// 绑定到A76集群CPU0-CPU3 cpu_set_t cpuset; CPU_ZERO(cpuset); CPU_SET(0, cpuset); CPU_SET(1, cpuset); CPU_SET(2, cpuset); CPU_SET(3, cpuset); pthread_setaffinity_np(pthread_self(), sizeof(cpuset), cpuset); // 设置NUMA策略 set_mempolicy(MPOL_BIND, nodemask, maxnode); // nodemask指向A76的NUMA node6.3 零拷贝处理用memmove()替代memcpy()并禁用编译器优化干扰DMA缓冲区是物理连续的用户态处理时若用memcpy()编译器可能插入额外的cache操作。我们采用手写汇编memmove()直接操作物理地址通过/dev/mem映射编译时加-O2 -fno-tree-vectorize禁用自动向量化避免编译器插入非预期指令处理循环中加入__builtin_ia32_clflushopt()显式清理cache line确保数据新鲜。最终在RK3588 Kintex-7 FPGA平台上我们实现了稳定738MB/s的持续DMA吞吐理论746MB/s达成率99.0%误码率为024小时压力测试无中断。这个数字不是实验室峰值而是产线设备的实际运行指标。7. 调试与验证没有逻辑分析仪和内核日志别谈PCIe DMA优化所有优化都建立在可验证的基础上。我们构建了一套分层验证体系确保每一步改动的效果可量化。7.1 硬件层验证用Logic Analyzer抓TLP而非只看lspcilspci只能告诉你设备是否存在而Logic Analyzer如Saleae Logic Pro 16能告诉你TLP是否正确抓取tx_data和rx_data信号解码TLP Header确认Length、Requester ID、Tag字段符合预期测量TLP间隔时间计算实际带宽Bandwidth (Payload_Length × Clock_Freq) / TLP_Interval检查dllp_nak信号确认链路层无重传。我们曾通过LA发现FPGA侧TLP的Length字段被错误地设为0导致RK3588持续发送NAKdmesg却只显示模糊的“AER error”。LA直接定位到RTL中一个未复位的计数器。7.2 内核层验证perf与trace-cmd是你的显微镜监控中断延迟perf record -e irq:irq_handler_entry,irq:irq_handler_exit -a sleep 10然后perf script | awk {print $NF} | sort -n | tail -10查看最大延迟跟踪DMA映射trace-cmd record -e iommu:iommu_map -e iommu:iommu_unmap确认dma_map_page()调用次数与预期一致分析TLB压力perf record -e arm64-tlb-refill:tlb_l1_refill,arm64-tlb-refill:tlb_l2_refill -a sleep 10。7.3 应用层验证自研dma_bench工具绕过一切中间层我们编写了一个极简的dma_bench工具它直接mmap()驱动暴露的DMA缓冲区用clock_gettime(CLOCK_MONOTONIC)精确计时每次DMA完成后用__builtin_ia32_rdtscp()读取TSC计算精确耗时输出CSV格式结果供Python脚本绘图。命令行./dma_bench -b 64M -c 1000 -t 164MB缓冲区1000次传输线程1。这张图是我们最终的性能曲线横轴传输次数纵轴单次DMA耗时μs此处为文字描述前100次平均耗时128.4μs含首次TLB填充100-1000次稳定在85.2±0.7μs整体吞吐64MB / (85.2μs × 1000) 751MB/s略高于理论值因测量包含少量CPU处理时间。这个数字就是我们交付给客户的底气。我在RK3588FPGA项目上踩过的最深的坑不是某个技术点不懂而是总想“先跑通再优化”。结果往往是DMA能传数据但吞吐只有理论值的三分之一系统能启动但跑两小时就死机代码能编译但一压测就段错误。真正的优化必须从PCB画线那一刻就开始——你要知道那个过孔stub会毁掉眼图要明白设备树里少一行ranges会让BAR空间永远不可用要清楚dma_alloc_coherent()在64MB场景下是个性能黑洞。这不是玄学是无数个凌晨用示波器、逻辑分析仪和dmesg一行行啃出来的肌肉记忆。当你把FPGA的TLP长度硬编码成512B把RK3588的SMMU切到Bypass把用户态线程牢牢钉在A76核上最后跑出738MB/s的稳定吞吐时那种“所有变量都在掌控中”的踏实感是任何文档都给不了的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门