新闻详情

新闻详情

首页 / 资讯中心 / 详情

GD32F450与FPGA的高速数据交互:EXMC并行总线方案详解

发布时间:2026/9/28 1:18:13来源:尧图网络
GD32F450与FPGA的高速数据交互:EXMC并行总线方案详解
做嵌入式这几年我越来越觉得MCU和FPGA之间的数据通道设计是个容易被低估的环节。之前做一台小型工业视觉设备MCU要把图像处理后的坐标、缺陷标记、统计信息实时送给FPGA做显示叠加最开始图省事用了串口后来又试了SPI结果都被实际吞吐量狠狠教育了一轮。最后改成GD32F450的EXMC接口直连FPGA带宽一下子从几MB/s级别拉到了几十MB/sCPU占用还降了一大截。这篇文章就把这套方案的完整思路、寄存器配置、FPGA端接口设计以及调试中踩过的坑都梳理一遍给同样在GD32和FPGA之间折腾数据交互的朋友做个参考。1. 为什么我最终放弃了串口和SPI转向EXMC并口总线先说个反直觉的结论很多时候你觉得通信速率不够问题不在协议本身而在你让CPU参与了太多次搬运。串口、SPI、I2C这类串行总线只要带宽够用、实时性要求不苛刻其实是很好的选择——简单、稳定、大家都会用。但一旦数据量上来了就开始捉襟见肘。我当时的场景是FPGA侧跑着一套实时视频叠加逻辑MCU需要以30fps的节奏把每一帧的检测结果送过去单帧有效数据大概在200KB左右。算一下就知道纯数据量是30 × 200KB 6MB/s这还没算协议头、校验、应答带来的开销。串口就算开到921600bps理论速率也就90KB/s左右差了整整两个数量级。SPI倒是快一些30MHz时钟大约能跑到3MB/s以上但前提是MCU得持续不断地喂数据而且SPI读写是串行的地址和数据都要逐个字节挪协议开销也不小。当时还有一个更隐蔽的问题SPI从设备需要片选、时钟、数据线FPGA侧要把接收到的串行数据拼成并行字还得处理背压。一旦MCU因为中断、调度卡顿了一下SPI时序就乱了帧就丢了。换句话说串行方案不只是慢它在大数据量强实时场景下是天生吃亏的。EXMCExternal Memory Controller外部存储器控制器就不一样了。GD32F4系列里的EXMC本质上是一个把内部AHB总线扩展到外部并行总线的桥MCU只要往某个内存映射地址read/writeEXMC硬件就会自动产生对应的片选、读写使能、地址信号跟访问片内SRAM一个体验。对FPGA来说它只要把自己模拟成一个异步SRAM挂在总线上剩下的时序、建立时间、保持时间全部由EXMC硬件按照你配置的参数生成CPU不参与逐bit搬运。我最终选定的是32位数据总线、异步NOR/PSRAM模式。实测在合理的时序配置下批量读写峰值能到36MB/s左右虽然离理论极限还有距离但已经足够覆盖6MB/s的需求而且CPU几乎可以完全腾出来干别的。这就是EXMC方案的核心价值不是它比SPI快了多少倍而是它把外设交互这件事从软件层下沉到了硬件层CPU只需要读写地址剩下的交给EXMC和DMA。2. 硬件连接与GD32侧EXMC初始化寄存器级的关键参数2.1 接线规划EXMC不是随便拉几根GPIO就行的它必须用固定的复用功能引脚。GD32F450的EXMC信号分布在多个GPIO端口上典型接法如下信号引脚位置说明EXMC_D0-D15PD0-PD15数据总线低16位EXMC_D16-D31PD16-PD31数据总线高16位EXMC_A0-A5PF0-PF5地址总线低6位EXMC_NOEPF4读使能低有效EXMC_NWEPF5写使能低有效EXMC_NE1PE7或PE8等片选低有效对应Bank0 Region0/1这里有一个新手特别容易踩的坑32位模式下地址线的对应关系不是直观的A0-A5挨个对应外部地址0-63而是地址线会按照数据宽度做偏移。32位数据总线时EXMC的A0对应外部地址的低2位对齐逻辑也就是说一次32位访问占4字节外部地址线A0实际上是字节地址的A2。如果你在FPGA侧直接把A0当作字节地址的最低位去用读出来的数据会完全错位。后面我会细说这个问题的处理方式。硬件上还要注意信号完整性。我之前第一版用杜邦线飞线测试数据线一多串扰和反射直接把波形搞烂了表现为偶发性读错数据特别难排查。建议至少用4层板或者等长走线数据线、地址线、控制线之间保持合理的间距走线长度尽量一致。2.2 GD32F450的EXMC初始化代码GD32固件库的EXMC初始化结构体不同版本略有差异但核心参数是一致的。以下是我这边跑通的初始化片段用的是GD32官方固件库GD32F4xx_Firmware_Library#include gd32f4xx.h #include gd32f4xx_exmc.h #define FPGA_BASE_ADDR ((uint32_t)0x60000000UL) /* Bank0 Region0 */ #define EXMC_REGION EXMC_BANK0_NORSRAM_REGION0 static void gpio_exmc_init(void) { /* 使能相关GPIO时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOD); rcu_periph_clock_enable(RCU_GPIOE); rcu_periph_clock_enable(RCU_GPIOF); /* PD0-PD15: EXMC_D0-D15复用功能 */ gpio_af_set(GPIOD, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); /* PD16-PD31: EXMC_D16-D31同上略 */ /* PF0-PF5: EXMC_A0-A5, EXMC_NOE(NF4), EXMC_NWE(NF5) */ gpio_af_set(GPIOF, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5); gpio_mode_set(GPIOF, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5); /* 片选引脚例如NE1具体以实际板子为准 */ gpio_af_set(GPIOE, GPIO_AF_12, GPIO_PIN_7); gpio_mode_set(GPIOE, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_7); }初始化时有一个小细节GPIO速度等级建议直接选50MHz档不要选低速档。EXMC的总线时序本来就很紧引脚翻转速度不够的话会进一步压缩建立时间导致读到的数据不稳定。接下来是EXMC控制器本身的配置static void exmc_fpga_init(void) { exmc_norsram_parameter_struct exmc_param; exmc_norsram_timing_parameter_struct timing; rcu_periph_clock_enable(RCU_EXMC); /* 读时序地址建立2个HCLK数据建立6个HCLK地址保持1个HCLK */ timing.address_setup_time 2; timing.address_hold_time 1; timing.data_setup_time 6; timing.bus_turnaround_delay 1; timing.read_write_switch_delay 1; timing.read_access_mode EXMC_ACCESS_MODE_A; timing.write_hold_time 1; timing.write_setup_time 2; timing.write_data_setup_time 5; exmc_param.region_select EXMC_REGION; exmc_param.async_wait DISABLE; exmc_param.nwait_signal DISABLE; exmc_param.memory_width EXMC_NORSRAM_MEMORY_WIDTH_32; exmc_param.address_valid EXMC_ADDRESS_2_LINES; exmc_param.write_mode DISABLE; exmc_param.read_write_timing timing; exmc_param.write_timing timing; exmc_norsram_init(exmc_param); exmc_norsram_enable(EXMC_REGION); }有几个参数值得单独解释。memory_width设为32位后一次CPU访问32位数据只需要一次EXMC总线周期这是吞吐量的基础。如果你只需要16位数据总线可以省掉PD16-PD31那组线但带宽也相应减半我建议有条件就直接上32位。address_valid这个字段在GD32固件库里表示地址线有效位数。之所以设为EXMC_ADDRESS_2_LINES是因为我这个应用里FPGA端只需要6条地址线64个32位寄存器窗口而EXMC地址线在32位模式下最低位已经不参与字节对齐了它的A0/A1其实固定对应字节偏移的低2位。如果你不知道该怎么配可以先按EXMC_ADDRESS_2_LINES理解成我用了A0这根线来初始化。read_access_mode选择模式A还是模式1取决于你的外设时序类型。FPGA异步SRAM模拟推荐模式A它是读写分离、地址建立后在NOE/NWE下降沿后数据有效的标准时序最容易对齐。配置完成后访问FPGA就是直接读写内存地址#define FPGA_REG_CTRL (*(volatile uint32_t *)(FPGA_BASE_ADDR 0x00)) #define FPGA_REG_STATUS (*(volatile uint32_t *)(FPGA_BASE_ADDR 0x04)) #define FPGA_DATA_BUF ((volatile uint32_t *)(FPGA_BASE_ADDR 0x10)) /* 往FPGA的0号寄存器写一个控制字 */ FPGA_REG_CTRL 0xA5; /* 读状态寄存器 */ uint32_t st FPGA_REG_STATUS;一定要用volatile否则编译器优化后可能把连续的几次外设访问合并成一次只读这是排错时最容易懵的地方。3. FPGA侧怎么把自己伪装成一个异步SRAM三态总线和读写时序3.1 接口模块设计FPGA侧的核心思路很简单GD32把EXMC总线上的信号发过来FPGA就按照异步SRAM的时序去响应。一个最简的接口模块大致长这样module exmc_slave_ram #( parameter AW 6, parameter DW 32 )( // EXMC 总线侧 input wire exmc_ne, // 片选低有效 input wire exmc_noe, // 读使能低有效 input wire exmc_nwe, // 写使能低有效 input wire [AW-1:0] exmc_addr, // 地址 inout wire [DW-1:0] exmc_data, // 双向数据 // 用户逻辑侧 output reg wr_pulse, // 写脉冲 output reg [AW-1:0] wr_addr, output reg [DW-1:0] wr_data, input wire [AW-1:0] rd_addr, input wire [DW-1:0] rd_data ); // 读数据通路片选和读使能同时有效时输出数据 assign exmc_data (exmc_ne 1b0 exmc_noe 1b0) ? rd_data : {DW{1bz}}; // 写数据通路NWE上升沿锁存数据 always (posedge exmc_nwe) begin if (~exmc_ne) begin wr_addr exmc_addr; wr_data exmc_data; wr_pulse 1b1; end end // 用户侧可以自行清除 wr_pulse或者在下一个片选周期前由外部处理 always (posedge exmc_ne) begin wr_pulse 1b0; end endmodule这里的关键点在于assign exmc_data ... ? rd_data : ...这句。它实现的就是三态总线只有当GD32拉低片选和读使能的时候FPGA才把数据驱动到总线上其他时间全部输出高阻。如果三态控制写反了或者你没有把不读的时候置为高阻就会发生总线冲突轻则读到错数据重则烧引脚这个必须记死。3.2 读写时序的工程实现GD32的EXMC对FPGA来说读写时序大致是这样的读周期里EXMC先拉低片选接着地址在总线上稳定然后拉低NOE。FPGA看到片选和NOE都为低就把rd_addr此时应该直接使用exmc_addr做组合逻辑读取对应的数据放到数据线上之后GD32在NOE上升沿附近采样数据。整个读操作是组合逻辑行为不是时序逻辑所以rd_data的输出路径上最好不要插寄存器否则会多一拍延迟导致GD32采样时数据还没稳定。写周期里EXMC先给出地址和写数据然后拉低NWE持续一段时间后再拉高。FPGA在NWE的上升沿锁存地址和数据因为此时总线上数据已经稳定锁存是最可靠的。如果用下降沿就可能采到总线上还未稳定或者切换的毛刺。另外一个工程细节如果你在FPGA内部用了大量寄存器做读写缓冲最好把exmc_data的三态驱动输出放在顶层模块不要套太多层次不然综合工具对IOBUF的推断容易出问题加了不必要的延迟。3.3 地址线偏移问题的正确打开方式前面提到GD32的32位EXMC总线地址线有偏移具体到FPGA这侧实际看到的现象是MCU侧访问FPGA_BASE_ADDR 0x04时FPGA收到的exmc_addr并不是4d1而是0或者别的值。原因就是32位模式下EXMC的A0/A1不参与外部寻址你可以理解为外部地址线的编号整体右移了两位。对应有两个处理思路第一种在FPGA侧直接忽略这个错位MCU访问时用FPGA_BASE_ADDR (reg_index 2)来保证地址线递增。比如寄存器索引0到5MCU侧offset就写0、4、8、12、20。FPGA侧不用改任何逻辑拿到的地址正好是索引本身。这种方式最直观但每次写代码都要记着左移2位容易忘。第二种通过配置EXMC的address_valid和地址线连接让映射关系尽量自然。GD32F4的EXMC在32位模式下地址线A0对应Byte offset 0-3A1对应4-7以此类推。如果FPGA只接了A0-A5那么MCU侧访问0x60000000时FPGA看到地址0访问0x60000004时FPGA看到地址1。所以只要MCU代码里物理地址按4字节步进FPGA看到的地址就是你期望的寄存器索引。我最终用的就是第二种物理地址按字对齐步进FPGA地址线直接拿来当寄存器索引用。不得不说刚调这个的时候确实费了不少时间一度怀疑是FPGA时序写错了最后翻了参考手册才发现是EXMC地址线本身的偏移规则。这条路走通之后DRAM和FPGA里的寄存器窗口映射就再无违和感了。4. 用DMA把传输效率顶上去从CPU搬运到自动批量传输4.1 为什么不能老让CPU搬数据前文提到CPU读写EXMC就跟访问内存一样简单但简单不代表高效。试想一下如果6MB/s的数据都要靠CPU循环搬运每搬一次要先取地址、再执行store指令一次32位搬运可能需要好几个CPU周期算下来CPU占用率会非常难看而且一旦任务里有中断抖动传输节奏就会乱。DMA的介入逻辑很简单把从EXMC映射地址到内存缓冲区或者从内存缓冲区到EXMC映射地址的搬运交给DMA控制器搬运结束后触发一次中断通知CPU处理。全程CPU只在开始前配置一次DMA传输过程中彻底解放。4.2 GD32F4的DMA配置要点GD32F4系列DMA支持外设请求映射可以把DMA请求和EXMC的外设请求关联起来。具体做法是先配置好DMA的通道参数然后把外设地址设置为FPGA在EXMC上的映射基地址内存地址设置为缓冲区地址。以下是核心配置片段固件库版本不同字段名会有差异请以你手上的头文件为准#include gd32f4xx_dma.h #define FPGA_EXMC_BASE ((uint32_t)0x60000000UL) #define RX_BUFFER_SIZE 512 static uint32_t rx_buffer[RX_BUFFER_SIZE]; static void dma_exmc_read_config(void) { dma_single_data_parameter_struct dma_single; rcu_periph_clock_enable(RCU_DMA0); dma_deinit(DMA0, DMA_CH0); dma_single.periph_addr FPGA_EXMC_BASE; /* 外设地址固定为EXMC映射地址 */ dma_single.periph_inc DMA_PERIPH_INCREASE_DISABLE; /* 外设地址不递增固定访问一个窗口 */ dma_single.memory_addr (uint32_t)rx_buffer; /* 内存缓冲区地址递增 */ dma_single.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_single.periph_memory_width DMA_PERIPHERAL_WIDTH_32; dma_single.memory_width DMA_MEMORY_WIDTH_32; dma_single.direction DMA_PERIPHERAL_TO_MEMORY; dma_single.number RX_BUFFER_SIZE; /* 搬运字数 */ dma_single.priority DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH0, dma_single); /* 如果是带外设请求映射的库还需要设置请求源 */ /* dma_request_config(DMA0, DMA_CH0, DMA_REQUEST_EXMC); */ dma_circulation_disable(DMA0, DMA_CH0); dma_channel_enable(DMA0, DMA_CH0); }几点实践经验periph_inc必须设为DISABLE。这个很关键如果设成地址递增DMA会从0x60000000开始一路递增到0x60001000去读那就完全不是你想要的行为了。外设地址固定才能通过多次DMA传输或者切换地址来访问不同寄存器窗口。number的单位是数据个数不是字节。32位数据宽度下number 512表示搬运512个32位字也就是2KB。换算的时候别搞错。中断处理建议放在DMA传输完成中断里不要在中断里做耗时处理只置标志位主循环或者RTOS任务里再去解析数据。4.3 帧结构和双缓冲设计用DMA搬运的时候最怕的就是上一次数据还没处理完下一次传输就到了。给这6MB/s的持续数据流做缓冲时我直接上了双缓冲#define BUF_SIZE 512 static uint32_t buf_a[BUF_SIZE]; static uint32_t buf_b[BUF_SIZE]; volatile uint8_t active_buf 0; /* 0表示a1表示b */ volatile uint8_t transfer_done 0;DMA传输完成中断里把当前缓冲区切换标志置位然后立刻把DMA的目标地址指到空闲缓冲开始下一轮传输。主循环发现transfer_done标志后处理已经完成搬运的那个缓冲区。这样读数据和搬数据可以并行吞吐量上限就等于DMA带宽而不用等待CPU处理。帧结构方面建议在数据区头部放一个固定格式的帧头typedef struct { uint32_t magic; /* 固定值比如0xGD32F4 */ uint32_t frame_id; /* 帧序号 */ uint32_t data_len; /* 有效数据长度 */ uint32_t payload[512]; /* 数据区 */ } fpga_frame_t;为什么要有magic和frame_id因为在高速DMA搬运下偶尔会出现数据错位的问题这个特性让接收方一眼就能看出数据有没有错位。帧序号还能用来检测丢帧。有了这层校验逻辑调试的时候能省很多事。5. 串行调试中踩过的几个坑和最终验证结果5.1 坑一三态门控制反了数据总线互怼第一次上板测试FPGA的读数据三态控制写成了只有NOE为高时才输出数据结果GD32一发起读周期FPGA总线恰好是高阻读回来的数据全是0x00000000或者0xFFFFFFFF。排查了很久最后用示波器卡NOE和D0的波形才看出来数据线在NOE有效期间根本没有被驱动。判断这个问题的技巧很实用把FPGA读数据宽度改成固定值比如某个寄存器恒返回0x5A5A5A5A然后MCU侧去读这个寄存器。如果读到的值在全0、全1、偶发错值之间跳变多半就是三态控制时序不对。确定控制逻辑正确后再换成真实数据去检查。5.2 坑二时序参数放太紧偶发性错读最初为了追求极致速度我把data_setup_time调到了3个HCLK结果就是10次访问里有那么一两回读到错误值。这种偶发问题最讨厌因为不是必现程序一跑起来有时几秒没事有时连续错好几笔。后来老老实实看了总线时序发现GD32在AHB时钟200MHz下单个HCLK是5nsdata_setup_time 3意味着数据建立时间只有15ns这对FPGA的组合逻辑输出来说太紧了——地址译码、寄存器MUX、三态门导通每一步都有延迟总延迟轻松超过15ns。我把data_setup_time调到6实测稳定了带宽也几乎没降多少。时序不是越紧越好够用才是最好这是这次调试最大的一个感悟。5.3 坑三DMA完成中断里处理数据高优先级任务饿死第一次接入RTOS后我在DMA中断里直接做了数据解析结果高优先级的中断服务函数占用时间太长低优先级的控制任务直接饿死看门狗不断复位。后来把所有耗时处理全部挪到任务上下文DMA中断里只做缓冲切换和标志位更新问题立刻消失。做实时系统中断里只做轻量级工作这个原则在任何MCU上都不会过时。5.4 实测数据一览我最终用这套EXMCDMA方案把数据通道跑通了实测数据如下方案实测带宽CPU占用传输时备注串口 921600bps约90KB/s约15%只能做配置/调试SPI 30MHz约3.5MB/s约60%需要CPU循环喂数据易被中断打断EXMC 32位 CPU循环搬运约18MB/s约40%带宽上来了但CPU占用还是高EXMC 32位 DMA约36MB/s较低传输期间CPU可处理其他任务36MB/s对当前应用来说已经富余很多。理论上如果继续压缩时序、用上更快的写模式还能爬得更高但工程上没有必要为了跑分去牺牲稳定性。真正的瓶颈其实早就不是通信链路了而是FPGA内部逻辑和MCU应用层的处理速度。最后再分享一个小技巧EXMC接口调通之后别急着把初始化代码写死。把关键时序参数做成结构体预留一份调试用的宽松配置和一份激进配置在板子上跑压力测试时来回切换对比能帮你快速定位到底是时序问题还是逻辑问题。这个习惯帮我省了不止一次查错时间你可以直接抄作业。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Sallen-Key低通滤波器设计与LTspice实战指南 2026/9/28 2:04:49

Sallen-Key低通滤波器设计与LTspice实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
ESP32嵌入式开发:-O2优化崩溃根源与实战修复指南 2026/9/28 2:04:49

ESP32嵌入式开发:-O2优化崩溃根源与实战修复指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
基于TI MCU的单相功率分析仪设计与谐波分析实现 2026/9/28 2:04:49

基于TI MCU的单相功率分析仪设计与谐波分析实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
STM32F407 DCMI接口驱动并口ADC实现高速连续采集实战 2026/9/28 2:04:49

STM32F407 DCMI接口驱动并口ADC实现高速连续采集实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
GD32F303 SPI+DMA全双工高速板间通信实战 2026/9/28 2:04:49

GD32F303 SPI+DMA全双工高速板间通信实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
蘑菇图像识别实战:PyTorch迁移学习与12类分类避坑指南 2026/9/28 2:04:42

蘑菇图像识别实战:PyTorch迁移学习与12类分类避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉