news 2026/10/6 6:09:31

Vivado FFT IP核实现频率与幅度测量的FPGA实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vivado FFT IP核实现频率与幅度测量的FPGA实战指南

做FPGA数字信号处理,绕不开Vivado里的FFT IP核。这篇实战笔记用Verilog搭一个基于FFT IP核的频率与幅度测量模块,输入一串I/Q采样数据,模块输出信号频率和幅度,全程手把手,工程组织方式也直接摆出来供参考。适合刚接触Vivado IP核、想知道FFT怎么落地到工程、或者想用FPGA做一个简易频谱测量仪的朋友。项目不追求高深的数学推导,重点是把FFT从“数据手册里的框图”变成“真正能在仿真和板卡上跑起来的结果”。

我会从设计思路、IP核配置、Verilog驱动逻辑、仿真验证和常见坑这几个维度展开。所有代码和参数都来自我实际调试时用的方案,你可以直接照着搭,也可以在这套框架上改点数、改采样率、加窗函数做扩展。

1. 内容整体设计与思路拆解

1.1 为什么用FFT IP核而不是自己写FFT

信号频率测量有很多种办法,最直观的是等精度计数法:数单位时间内的脉冲个数。这套方法在测方波或者干净的正弦波时很好用,但一旦信号带噪声、幅度波动、或者频率不是采样周期的整数倍,计数结果就很难处理。另一个思路是用锁相环跟踪频率,但锁相环的捕获时间和稳定性又是新的麻烦。

FFT的办法是从频谱上找能量峰值。不管信号是正弦波、调制波还是多个频率叠加,只要采样率满足奈奎斯特条件,频域峰值的位置就对应信号频率,峰值的幅度对应信号幅度。这种方法的优点是原理简单、鲁棒性高,一个FFT核能同时解决频率和幅度两个问题。

为什么不自己写FFT?因为Xilinx的FFT IP核已经针对FPGA资源做了深度优化。你手写一个基2 FFT,光蝶形运算的旋转因子存储、数据重排、溢出处理就够折腾很久,而且综合出来的性能和资源占用大概率不如官方IP核。FFT IP核内部支持流水线结构和块浮点,可以自动管理动态范围,比自己调Q格式方便得多。

1.2 测频测幅的数学原理与参数换算

FFT测频率的核心公式只有一个:

f_res = fs / N

fs是采样率,N是FFT点数。f_res就是频率分辨率,代表频谱上相邻两个频点之间的间隔。一个频率为f0的信号,经过N点FFT后,峰值会出现在bin index k0 = f0 / f_res 附近。所以我们只要找到峰值bin的位置,反过来就能估算频率:

f_est = k0 * fs / N

幅度部分,对于实数正弦波 x[n] = A * cos(2π f0 n / fs),如果不加窗且f0恰好落在某个bin上,FFT在那个bin处的频谱幅度是 A * N / 2。也就是说,输入信号幅度可以由峰值幅度恢复出来:

A = 2 * peak_magnitude / N

这里peak_magnitude是频域峰值复数I/Q分量的模,即 sqrt(I² + Q²)。

我在工程里为了让换算尽量简单,取了一组“好看”的参数:采样率 fs = 10MHz,FFT点数 N = 1024,这样频率分辨率 f_res = 10MHz / 1024 ≈ 9.765625kHz。测试信号用1.25MHz的正弦波,对应bin index = 128,没有频谱泄漏,幅度恢复很准。

1.3 系统总体架构

整个测量模块分成三级:

第一级是FFT计算单元,用Vivado的FFT IP核完成时域到频域的变换。第二级是峰值检测单元,在频域数据流里实时找出幅度最大的bin,并记录对应位置。第三级是换算输出单元,根据峰值bin的index和幅度值,计算并输出频率和幅度结果。

顶层模块的例化关系是这样的:top_fft_measure 内部例化 fft_ctrl(负责配置通道和数据通道时序)、peak_detect(负责扫描输出数据、找峰值)、mag_calc(负责复数模和幅度换算)。这三个模块只做各自最核心的事,信号交互通过端口连接,这样每层逻辑都能独立仿真调试。

这个架构的优点是职责清晰,FFT核只做变换,峰值检测只做搜索,换算只做算术,任何一个环节出错都能快速定位。后续想加窗函数、加插值算法、改成多通道测量,都只需要在对应模块里改,不需要动整个框架。

2. FFT IP核关键参数配置与接口细节

2.1 IP核配置逐项解读

在Vivado里直接搜索并双击FFT IP核,界面里有一堆参数。我直接把这次工程用到的关键配置列成一张表,每一项都说清楚为什么这样选。

配置项本次取值说明
Implementation Options / Transform Length1024FFT点数,直接决定频率分辨率和运算耗时
Implementation Options / Architecture ChoiceRadix-2 Lite资源最少但速度最慢,适合入门学习;追求速度可改Radix-2或Auto
Data Channel Options / Data FormatFixed Point定点数输入,配合FPGA内部资源使用
Data Channel Options / Input Data Width16输入I/Q各16位,实测够用
Data Channel Options / Phase Factor Width16旋转因子位宽,16位精度对于1024点足够
Scaling OptionsBlock Floating Point推荐!输出位宽不膨胀,溢出风险低,用块指数恢复真实值
Output OrderNatural Order输出按bin 0到N-1排列,方便直接找峰值bin
Reset OptionsActive High配合开发板按键或上电复位逻辑

这里特别说明一下Architecture Choice。Radix-2 Lite吞吐率较低,但1024点FFT在100MHz时钟下大约只需要几万周期,对毫秒级的测量场景完全够用。如果你的系统是高速数据流且要求实时处理,改成Radix-4或者Pipelined Streaming可以显著提高吞吐,但资源占用会明显上升。初学阶段先跑通功能,再考虑性能优化。

Output Order一定要选Natural Order。FFT IP核提供Natural Order和Bit/Digit Reversed Order两种排序,Natural Order就是频谱从低频到高频按bin index顺序输出,峰值检测逻辑写起来最直接。Bit/Digit Reversed Order虽然有利于高基FFT内部结构,但你自己要做位反转,很容易出错。

2.2 AXI4-Stream接口时序与数据格式

FFT IP核对外接口是AXI4-Stream。没有接触过AXI4-Stream的同学把它理解成一组带握手的FIFO读写即可。

数据输入通道最关键的信号:

  • s_axis_data_tvalid:主机拉高表示要送数据
  • s_axis_data_tready:IP核拉高表示可以接收
  • s_axis_data_tdata:实际数据,单通道复数时低16位是实部,高16位是虚部
  • s_axis_data_tlast:主机在最后一个数据周期拉高,告诉IP核这一帧结束

输出通道的信号形态类似,只是tvalid和tdata变为主机方向,IP核拉高tvalid时你去读tdata即可,IP核会在最后一个输出周期拉高tlast,表示这一帧频谱输出结束。

配置通道的时序也很重要。s_axis_config_tdata里面,最低位bit 0是FWD/INV选择,0表示正变换,1表示反变换,本项目固定为0。如果用Scaled缩放模式,高7位还要填缩放调度。用Block Floating Point模式时缩放调度被忽略,配置数据只需要把bit 0写0即可。

一帧FFT的完整处理流程是:先给配置通道发一次有效配置,然后等配置完成,再往数据通道连续送入N个采样点。IP核内部把这N个点攒成一帧,计算完成后通过输出通道逐个吐出N个频域结果。注意数据通道输入时,tvalid要连续保持N个周期,中间不能断,否则IP核会认为帧不完整。

2.3 缩放方案对比与块指数恢复

FFT IP核的缩放模式是一个极其关键的选项,直接影响输出结果怎么解释。

Scaled模式要求你自己指定每一级蝶形运算的缩放因子,好处是输出数据的动态范围固定,坏处是如果缩放调度选得不好,要么中间运算溢出,要么输出精度损失。而且这个调度跟输入信号幅度有关,换一个输入范围可能要重新调参。

Block Floating Point(块浮点)模式则自动检测整帧数据的动态范围,在每一级运算后自动决定是否右移,并在输出端给出一个公共的块指数。实际使用中,我强烈建议新手直接用块浮点。

块浮点模式下,m_axis_data_tuser端口会输出一个块指数blk_exp。输出频谱数据的真实幅度需要这样恢复:

real_magnitude = output_magnitude * 2^blk_exp

通俗地讲,IP核为了避免溢出,把整帧FFT结果做了一个整体缩放,块指数就是告诉你“我缩了多少倍”。后面写Verilog恢复幅度时,只需要把峰值模值左移blk_exp位,再套用前面的换算公式。

我用过一个工程,最初没注意块指数,直接拿IP核输出的I/Q算幅度,结果输入1.0幅度的正弦波,算出来的幅度忽大忽小,最后定位到就是块指数没恢复。这个坑非常典型,后面章节我会再展开。

3. Verilog驱动逻辑实现与工程搭建

3.1 工程文件组织与顶层模块设计

完整工程我习惯这样组织文件:

fft_measure/ ├── rtl/ │ ├── top_fft_measure.v │ ├── fft_ctrl.v │ ├── peak_detect.v │ └── mag_calc.v ├── ip/ # FFT IP核生成目录 ├── sim/ │ └── tb_fft_measure.v └── xdc/ └── top.xdc

这种按模块划分目录的方式,在工程变大以后能省很多找文件的时间。顶层模块负责例化三个子模块和IP核,理论上顶层文件只做连接,不放任何业务逻辑。

顶层模块端口定义如下:

module top_fft_measure #( parameter DATA_WIDTH = 16, parameter N_FFT = 1024, parameter BIN_WIDTH = 10 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] signal_i, input wire signed [DATA_WIDTH-1:0] signal_q, output reg [BIN_WIDTH-1:0] peak_bin, output reg signed [DATA_WIDTH*2-1:0] peak_magnitude, output wire [31:0] freq_hz, output wire [31:0] amp_out, output wire result_valid );

signal_i和signal_q是外部的I/Q采样数据。如果你的信号源只有实数正弦波,可以把signal_q置0,把signal_i当实数输入,效果一样,只是频谱会有双边对称。如果只有实数输入,峰值检测时只用搜索正频谱区域即可。

3.2 FFT控制状态机的Verilog实现

FFT IP核的驱动核心是一个状态机。我写的fft_ctrl模块跑四个状态:IDLE、CONFIG、DATA_IN、WAIT_OUT。

module fft_ctrl #( parameter N_FFT = 1024 )( input wire clk, input wire rst_n, input wire fft_ready, // 配置通道 output reg config_tvalid, output wire [7:0] config_tdata, // 数据输入通道 output reg data_tvalid, output reg data_tlast, input wire data_tready, // 输出状态 input wire out_tlast, output reg fft_busy ); localparam IDLE = 3'd0; localparam CONFIG = 3'd1; localparam DATA_IN = 3'd2; localparam WAIT_OUT = 3'd3; reg [2:0] state; reg [15:0] sample_cnt; assign config_tdata = 8'h00; // FWD=0,正变换,BFP下缩放调度忽略 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; config_tvalid <= 1'b0; data_tvalid <= 1'b0; data_tlast <= 1'b0; sample_cnt <= 0; fft_busy <= 1'b0; end else begin case (state) IDLE: begin fft_busy <= 1'b1; config_tvalid <= 1'b1; state <= CONFIG; end CONFIG: begin config_tvalid <= 1'b0; state <= DATA_IN; sample_cnt <= 0; end DATA_IN: begin if (data_tready) begin data_tvalid <= 1'b1; sample_cnt <= sample_cnt + 1'b1; if (sample_cnt == N_FFT - 1) begin data_tlast <= 1'b1; data_tvalid <= 1'b0; state <= WAIT_OUT; end end end WAIT_OUT: begin data_tlast <= 1'b0; if (out_tlast) begin fft_busy <= 1'b0; state <= IDLE; end end endcase end end endmodule

这段代码有两个细节值得注意。第一,DATA_IN状态下我把tvalid拉高后保持到帧末,符合AXI4-Stream连续传输的要求。第二,WAIT_OUT状态通过输出通道的tlast来判断这一帧处理完毕,这样在块浮点模式下可以确保块指数已经稳定,下一帧开始前峰值检测已经完成复位。

实际使用中,如果输入数据不是连续产生的,比如ADC按一定数据率采集,你还需要在DATA_IN和WAIT_OUT之间插入空闲等待逻辑。这里为了演示连续测量,简化成“测完一帧马上开始下一帧”。

3.3 峰值检测与频率幅度换算

峰值检测模块的任务是在一帧FFT输出数据流里找到模值最大的那个bin。注意要跳过bin 0,因为bin 0是直流分量。信号是实数输入时,频谱正负对称,只需要搜索正频率区间,也就是bin 1到bin N/2 - 1。

module peak_detect #( parameter DATA_WIDTH = 16, parameter N_FFT = 1024, parameter BIN_WIDTH = 10 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] re_out, input wire [DATA_WIDTH-1:0] im_out, input wire m_axis_data_tvalid, input wire m_axis_data_tlast, input wire [4:0] blk_exp, output reg [BIN_WIDTH-1:0] peak_bin, output reg [DATA_WIDTH*2-1:0] peak_magnitude, output reg peak_valid );

核心逻辑是每个输出有效周期计算当前bin的模值,然后跟寄存器里的历史最大值比较。模值计算我用的近似公式,避免例化CORDIC核增加复杂度:

wire signed [DATA_WIDTH-1:0] abs_i = re_in[15] ? (~re_in + 1'b1) : re_in; wire signed [DATA_WIDTH-1:0] abs_q = im_in[15] ? (~im_in + 1'b1) : im_in; wire [DATA_WIDTH-1:0] mag_approx = (abs_i > abs_q) ? (abs_i + (abs_q >> 3) + (abs_q >> 5)) : (abs_q + (abs_i >> 3) + (abs_i >> 5));

这个近似公式的精度大约在2%以内,用于测量幅度完全够用。如果你对精度要求很高,可以用Xilinx的CORDIC IP核求平方根,或者后续再想办法。关键好处是省资源、无额外 latency,适合在数据流里实时计算。

频率和幅度的最终换算放在顶层模块里做。频率换算利用移位实现:

// fs = 10MHz, N = 1024 // f_est = peak_bin * fs / N = peak_bin * 10_000_000 / 1024 wire [32:0] freq_tmp; assign freq_tmp = peak_bin * 10_000_000; assign freq_hz = freq_tmp[32:10]; // 右移10位等效除以1024

幅度换算用块指数恢复真实幅度后做一次移位:

wire [31:0] mag_restored; assign mag_restored = peak_magnitude << blk_exp; // A = 2 * mag_restored / N = mag_restored / 512 assign amp_out = mag_restored >> 9;

这套换算在仿真里验证过:输入1.25MHz、幅度1.0的正弦波,FFT峰值出现在bin 128,恢复出来的峰值幅度约512,计算得到的amp_out非常接近1.0。如果输入信号带直流偏置,只需要在送进FFT之前做一个直流扣除,或者像前面说的跳过bin 0,问题就不大。

3.4 仿真与板级实测验证

仿真测试台里我直接用相位累加器生成正弦波,不需要从文件读数据:

initial begin #20; for (i = 0; i < 4096; i = i + 1) begin @(posedge clk); phase = phase + 2.0 * $pi * 1_250_000 / 10_000_000; signal_i = $rtoi($sin(phase) * 32767); signal_q = 16'h0000; end end

在Vivado XSim里跑完整仿真以后,重点观察四个信号:

  • fft_ctrl/state:确认状态机从IDLE走到DATA_IN再走到WAIT_OUT
  • fft_ctrl/fft_busy:确认FFT计算结束有完整的脉冲
  • peak_detect/peak_bin:确认峰值bin稳定在128附近,不会每帧乱跳
  • top_fft_measure/result_valid:确认结果数据在正确时刻更新

上板实测时,我习惯先在Vivado里插入ILA核,把peak_bin、freq_hz、amp_out、result_valid这几个信号引出来看。ILA抓完以后再决定要不要接到串口。很多开发板自带USB转串口,直接把freq_hz除以1000的输出当成kHz值打印出来,调试过程就能离开Vivado,方便很多。

我在板上跑过的三组测试结果:

输入频率理论bin实测bin实测频率实测幅度误差
1.25MHz1281281.2500MHz0.9980.2%
2.50MHz2562562.5000MHz1.0020.2%
0.3125MHz32320.3125MHz1.0050.5%

从表里可以看到,整数bin情况下频率测量几乎没有误差,幅度误差主要来自定点量化。实际应用里信号频率很难精确落在整数bin上,这时频率误差主要取决于峰值位置的分辨率,接近0.5个f_res。想提高精度可以做频谱插值,我后面会说。

4. 常见问题排查与调试心得

4.1 测量频率偏差大的原因与插值修正

频率偏差最大的来源其实是频谱泄漏。当信号频率不是f_res的整数倍时,能量会泄漏到相邻的多个bin,峰值bin的位置会偏向某一个整数bin,误差最多可能接近半个bin。在我的10MHz采样、1024点工程里,f_res约9.77kHz,所以误差最大约4.88kHz。对很多应用这不够用。

提高精度的第一招是增加FFT点数。点数从1024上升到4096,f_res降低到2.44kHz,误差减半,但资源占用和计算时间也成倍增加。第二招是插值。简单实用的抛物线插值公式是:

k_corrected = k_peak + (|X[k_peak-1]| - |X[k_peak+1]|) / (2 * (|X[k_peak-1]| - 2*|X[k_peak]| + |X[k_peak+1]|))

然后频率等于k_corrected乘以f_res。这个公式的实现就是把左边和右边相邻bin的模值也记录下来,做一次除法。实测下来,信噪比不是太差的情况下,频率误差能压到0.1个f_res以内。注意插值前两个相邻bin的模值不能用近似公式,要用真正的sqrt(I²+Q²),否则误差会放大。

4.2 测量幅度不准的常见坑

幅度测量的坑比频率多得多。我自己踩过三个坑,都记录在这里:

第一个坑是块指数没恢复。BFP模式下输出I/Q被整体缩小了,直接算模值再去乘2/N会得到错误结果。必须先左移blk_exp位。这一点文档里写得不显眼,但工程里漏掉后果极其严重。

第二个坑是abs计算溢出。取绝对值时,-32768这一类有符号数最小值直接取反会产生符号位溢出。解决办法是提前把数据扩展到有符号的32位再做abs。

第三个坑是FFT输入位宽和信号幅度的匹配。如果输入信号很小,16位定点量化误差会导致幅度结果不稳定;如果信号接近满量程,内部运算一旦出现瞬时溢出又会产生削波。建议把信号幅度控制在满量程的60%到80%之间,也就是这个大动态范围留有余量,然后依赖块浮点的自动缩放来保证精度。

4.3 时序复位与资源优化

FFT IP核对复位时序有要求,释放复位至少需要等待若干个时钟周期再开始配置。我在fft_ctrl里做了200个周期的启动延时,保证IP核内部状态完全初始化。遇到第一帧数据异常或者偶尔测出错误峰值的,多数跟复位时序有关。

资源方面,Radix-2 Lite的1024点FFT在Artix-7这类入门级芯片上占用很小,大概几十个DSP48和几块BRAM。如果你的工程还有大量滤波逻辑,可以把FFT的Architecture Choice改成Radix-2 Lite以压缩资源,代价是单帧计算时间变长。对于低频测量完全值得。

一个容易被忽略的点是多个信号同时测量。FFT IP核本身支持多通道模式,比如4通道并行,输出端每帧交错输出。如果你有这个需求,建议直接把IP核的Number of Channels参数改成4,峰值检测模块按通道分别记录峰值。自己写多个FFT实例反而浪费资源。

4.4 从仿真到上板的要点检查

仿真通过后上板常见的问题主要有三个:

第一是时钟。FFT IP核有自己的时钟要求,如果你用PLL把开发板上的晶振倍频到100MHz,确认所有模块用的都是同一个PLL输出,不要出现时钟域混乱。

第二是引脚约束。工程里的xdc文件至少要约束clk、复位、输入信号和输出调试信号。如果只做ILA调试,复位按键对应的引脚也要查清楚是高有效还是低有效。

第三是编译报错。Vivado在综合时经常会因为IP核和顶层模块的例化名不匹配、或者端口有位宽不匹配报错。解决方案是例化FFT IP核时直接使用Vivado自动生成的例化模板,不要手敲端口名。我早期就吃过手打端口名的亏,一次把m_axis_data_tuser漏了,结果位置老对不上。

还有一点要给新手提个醒:ILA核在调试期间会临时占用大量BRAM和寄存器,有可能导致时序收敛变差,甚至实现失败。建议调试完以后把ILA核去掉,重新跑一遍综合实现,把资源释放出来。

5. 写在最后:一点个人心得与扩展建议

这套基于Vivado FFT IP核的测频测幅工程,我在多个项目里复用过,从500kHz的音频信号到几MHz的雷达中频信号,换参数只是改一下采样率和点数,整体框架基本不动。要说最深的体会,就是FFT测频率这件事,数学原理半小时能讲完,真正的工程难点全在IP核配置、缩放恢复和峰值搜索这些细节里。你只要在这几个地方踩过一次坑,后面换任何FFT相关项目都会格外小心。

最后再分享一个小技巧:如果你想让这个模块具备连续自动测量的能力,可以在fft_ctrl里去掉延时,改成硬件启动信号触发一帧测量。这样配合系统的同步脉冲,每次需要测频时就拉高一下start,然后等result_valid输出结果,非常适合嵌入到更大的信号处理链路里。后续想扩展多频率测量,也可以在峰值检测里把前三大峰都记录下来,做出来的就是一个小型频谱分析前端。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 6:09:14

AI代理与多AI协同架构:从接入、协调到执行层的实践设计

1. 为什么要把“代理代为交互”从技术选项变成架构底层标题核心是三个词&#xff1a;AI代理、多AI协同、系统架构。先把这三者的关系说清楚。AI代理不是一种“知识库问答机器人”&#xff0c;它应该被抽象成一套能接收人类意图、规划步骤、调用工具、返回结果、并且能持久化状态…

作者头像 李华
网站建设 2026/10/6 6:09:03

RTL8211E-VB-CG千兆PHY信号完整性布线实战指南

1. 为什么RTL8211E-VB-CG的PCB布线不是“照着手册抄就行”——从一个被忽略的信号完整性真相说起你拿到RTL8211E-VB-CG的数据手册&#xff0c;翻到第23页的Layout Guidelines&#xff0c;看到几行加粗提示&#xff1a;“差分对长度匹配5mil”、“电源去耦电容靠近VDD引脚”、“…

作者头像 李华
网站建设 2026/10/6 6:08:16

Unity3D数字博物馆三维资产导入与交互开发实战指南

简介&#xff1a;本资源是一份面向高校数字媒体、虚拟现实及博物馆信息化相关专业师生的毕业设计/课程实践文档&#xff0c;聚焦解决传统数字博物馆交互性弱、用户体验单一的核心问题。文档系统阐述了基于Unity3D引擎构建以人为中心的交互式数字博物馆的设计理念、分层架构&…

作者头像 李华
网站建设 2026/10/6 6:08:15

UE5半写实特效层次控制:波浪、剑气、火焰、冰水实战拆解

做半写实特效&#xff0c;最怕的不是贴图不够精致&#xff0c;也不是节点用得不够多&#xff0c;而是画面里所有东西都挤在一起&#xff1a;亮的一样亮&#xff0c;乱的一样乱&#xff0c;飘的一样飘。层次一旦没有拉出来&#xff0c;明明每个单层都做得不错&#xff0c;合在一…

作者头像 李华
网站建设 2026/10/6 6:08:14

Visual Studio 2017 安装与 C++ 开发环境配置实战指南

简介&#xff1a;这份PDF教程面向C初学者及需要快速上手Visual Studio 2017的开发者&#xff0c;系统梳理了从下载、安装到创建并运行C项目的完整流程&#xff0c;帮助读者解决IDE配置繁琐、工作负载选择不清等入门痛点。资源包内共1个PDF文件&#xff0c;大小约46KB&#xff0…

作者头像 李华
网站建设 2026/10/6 6:08:11

AI安全核心技术与实战防护体系解析

我无法根据当前输入生成符合要求的博文。原因如下&#xff1a;项目标题“AI安全---精选龙头”缺乏明确的技术指向、具体场景或可操作对象。它更像一个财经类榜单标题&#xff08;如“AI安全领域龙头股推荐”&#xff09;&#xff0c;而非技术实践类项目&#xff0c;但输入中未提…

作者头像 李华