世界的本质是旋转(6)-在复平面上借助软件无线电SDR解调BPSK波形

在上一篇文章中,已经完成了BPSK波形的发射。

相对于BPSK波形的生成总共就4行代码,接收要略微复杂一些,算上各种同步、锁相环,约80行。完整版参考Git仓库。

设备连接:

电磁波
射频
基带
环境
天线
B210
PC

USRP B210 基于 AD9361芯片,把放大、变频、滤波、AD、数字变频都做完了。输入的是射频信号,输出的就是基带(0中频)复平面上的X、Y坐标。因此,解调代码输入的就是XY坐标(IQ路信号)。这里给出其C语言核心代码如下:

vector<char> demod_psk(const short (*iqdata)[2], const int splen)
{static const double fir_rcos_q25[25] = {-0.018773,0.0030136,0.032677,0.047094,0.02655,-0.027522,-0.085225,-0.099447,-0.032147,0.11904,0.31118,0.472,0.53463,0.472,0.31118,0.11904,-0.032147,-0.099447,-0.085225,-0.027522,0.02655,0.047094,0.032677,0.0030136,-0.018773};static double fir_cache_i[25] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};static double fir_cache_q[25] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};//fir filterstatic unsigned long long clk = 0, bad_angle = 0, last_synclk = 0;static const int WINSZ = 4000;static double stati_win[WINSZ][2], sum_win[2] = {0,0};static double cache_win[WINSZ][2];//smp best posstatic double clk_offset[4] = {0,0,0,0};static bool bInited = false;static double curr_dp = 0;static int bestOff = 0;//stati_win,cache_win 归零初始化略去//...//bpsk bit outvector<char> dp_bitbuf;//逐个点步进for (int i=0;i<splen;++i){//1. 滤波去带外噪声fir_cache_i[clk % 25] = iqdata[i][0];fir_cache_q[clk % 25] = iqdata[i][1];double fval_i = 0,fval_q = 0;for (int f=0;f<25;++f){fval_i += fir_cache_i[(clk+f+1)  % 25] * fir_rcos_q25[f];fval_q += fir_cache_q[(clk+f+1)  % 25] * fir_rcos_q25[f];}//2. 窗口统计去直流分量sum_win[0] -= stati_win[clk%WINSZ][0];sum_win[1] -= stati_win[clk%WINSZ][1];sum_win[0] += fval_i;sum_win[1] += fval_q;stati_win[clk%WINSZ][0] = fval_i;stati_win[clk%WINSZ][1] = fval_q;const double dcremoved[2] {fval_i - sum_win[0]/WINSZ, fval_q - sum_win[1]/WINSZ };cache_win[clk%WINSZ][0] = dcremoved[0];cache_win[clk%WINSZ][1] = dcremoved[1];//3. 最佳采样位置跟踪const int curr_offv = clk % 4;clk_offset[curr_offv] += (dcremoved[0]*dcremoved[0] + dcremoved[1] * dcremoved[1]); //符号时钟位置同步double phy = 0;//锁相环误差if (curr_offv == bestOff){//锁相旋转double rt[2]{cos(-curr_dp),sin(-curr_dp)};const double out[2]{cache_win[(clk+4)%WINSZ][0],cache_win[(clk+4)%WINSZ][1]};double rp[2]{out[0] * rt[0] - out[1] * rt[1],out[0] * rt[1] + out[1] * rt[0]};//失锁重置,即重新测量相位误差phy = atan_r(rp[1],rp[0]);if (phy >= c_pi/4 || phy <=-c_pi/4){curr_dp = atan_r(out[1],out[0]);rt[0] = cos(-curr_dp);rt[1] = -sin(curr_dp);rp[0] = out[0] * rt[0] - out[1] * rt[1];rp[1] = out[0] * rt[1] + out[1] * rt[0];phy = atan_r(rp[1],rp[0]);++bad_angle;}//更新环路curr_dp += (phy/2);curr_dp =	wrapto2pi(curr_dp );//解调后的01比特dp_bitbuf.push_back( rp[0] < 0? 0: 1 );}//错开最佳点2符号进行时钟误差补偿if (last_synclk + WINSZ < clk && ((clk+2) % 4)==bestOff){last_synclk = clk;int maxo = 0;double maxv = clk_offset[0];if (clk_offset[1] > maxv) { maxo = 1; maxv = clk_offset[1];}if (clk_offset[2] > maxv) { maxo = 2; maxv = clk_offset[2];}if (clk_offset[3] > maxv) { maxo = 3; maxv = clk_offset[3];}bestOff = maxo;clk_offset[0] = 0;	clk_offset[1] = 0;clk_offset[2] = 0;	clk_offset[3] = 0;}++clk;}return dp_bitbuf;
}

不停地调用上述代码,输入一段一段接收到的X\Y坐标(IQ路信号),输出一串串01比特。上述代码采用全静态内存、无任何的搬移、拷贝,基本满足一般低速信号的处理要求。

下面我们逐一介绍每一步干了什么。在此之前,先讨论一个题外话。

0. 题外话:软硬件的思维大不相同

先放一张图:

WT
有多少人在看到这个BPSK 科斯塔斯环时,会思考这个东西的C语言如何实现VCO、LF、LPF,这么多滤波如何提速等等。其实我也困扰了很久.,其实若从复平面、X\Y坐标去理解,会发现用PC计算机实现BPSK解调,完全不必有VCO以及这些设施。

上面的示意图,是在上个世纪使用模拟器件处理解调工作的最佳方案。硬件记不住太多的历史数据,因此无法统计一些信息;硬件无法方便的计算各种角度,比如atan。所以它用一个压控震荡源产生一个对消载波,去环路匹配残差频率。在C语言里,我们可以直接测量角度,从而跟踪上向量的旋转,对消载波是不必要的。PC的思路是:

符号时刻t0,观察到向量(x,y)在131度,对BPSK,则下一个符号时刻 t1 要么是 131+180,要么还是131度。因此,直接测量 atan(y,x),而后稍微和预计位置比较一下,就可以得到残差相位了,太直接了。整个环路就等效为一个角度变量的跟踪。

旋转打地鼠

如果把接收BPSK的两个可能的符号位置想象成一个黑色幕布下的两组地鼠洞,地鼠会随时从任意洞口钻出来。更要命的是,整个地鼠机放在一个转台上,转台的转速就是各级模拟/数字下变频(ADC、DDC)到0频(基带,或者复平面xy坐标)时保留的频率误差。现在,要准确的打地鼠,就是和解调非常类似的活动。

按照4倍速率XY坐标运动的地鼠角度跟踪最佳状态的地鼠
在这里插入图片描述在这里插入图片描述

传统教材的科斯塔斯环,等于是要产生一个和残差相位相反的旋转矢量,叠加到输入载波,让整体的地鼠盘子不动。而我们的C语言解调,只要测量当前的残差相位,并不断跟踪即可。盘子继续转动,多了一个测量杆子(curr_dp),这个杆子和地鼠一起转动,每次根据地鼠的位置,跟踪调整杆子的角度即可。

1. 滤波去带外噪声

由于要进行时钟同步,以及奈奎斯特限制,接收时往往采用比发射速率高几倍的速率进行接收。此时,信号带宽外的噪声也进来了。去除带外噪声,可以一定程度良化波形。

上文的代码:

	//1. 滤波去带外噪声fir_cache_i[clk % 25] = iqdata[i][0];fir_cache_q[clk % 25] = iqdata[i][1];double fval_i = 0,fval_q = 0;for (int f=0;f<25;++f){fval_i += fir_cache_i[(clk+f+1)  % 25] * fir_rcos_q25[f];fval_q += fir_cache_q[(clk+f+1)  % 25] * fir_rcos_q25[f];}

构造了一个环形结构,实现时域滤波的卷积计算。由于FIR是对称的,导致卷积反转不再需要。只要用时钟咬紧当前的位置即可,搞一个25长度的环状缓存,只记忆滤波需要的部分。同时,时钟推进后,用取余替代memcpy,不进行内存搬移,几乎没有开销。

这部分的效果,可以通过等效的Octave代码处理真实数据来展示:
filter左图是滤波前的向量位置打点图,右图是滤波后的。显然滤波后,0、1两坨坐标分的更开了。下面是仿真代码:

pkg load communications;
clear;
clc;
mulrate = 4;
symlen = 1000;
dta1_bits = randint (1, symlen, 2);
dta2_symbols = pskmod (dta1_bits, 2, 0, "gray");
#dta2_symbols = qammod (dta1_bits, 4);
#figure(); plot (dta2_symbols, "bx");dta3_sig = zeros(1,symlen*mulrate);
dta3_sig(1:mulrate:end) = dta2_symbols;#figure(); plot (dta3_sig, "r+");##fir send
[fir_rcos]=rcosfir(0.25,[-3,3],mulrate,1,'sqrt');dta4_baseband = conv(dta3_sig,fir_rcos,'same');#plot (dta4_baseband(1:mulrate:end), "g*");
#figure; plot(fir_rcos);
##Trans# to rx
rf_sym_rate = 1000;
rf_sample_rate = rf_sym_rate * mulrate;dta5_recv = awgn (dta4_baseband, 12);figure();plot (dta5_recv(1:mulrate:end), "r.");##fir recv
dta6_filted = dta5_recv;
dta6_filted = conv(dta5_recv,fir_rcos,'same');figure();plot (dta6_filted(1:mulrate:end), "b.");dta7_sampled = dta6_filted([1:mulrate:end]);
#figure();plot (dta7_sampled, "b.");#dta8_est = qamdemod (dta7_sampled, 4);
dta8_est = pskdemod (dta7_sampled, 2,0,"gray");biterr (dta1_bits, dta8_est)#figure();plot (real(dta4_baseband),'r');
#figure();plot (real(dta6_filted),'b');

2 窗口统计去直流分量

有些时候,收到的向量具有一个整体的直流分量,导致其不再围绕原点分布。表现在复平面上,就是一坨位置跑到别处去。这样导致基于原点进行角度测量的三角函数都没有办法使用了。

解决此问题的方法很简单,就是统计一个窗口里的坐标的均值,并搬移到原点去。上文代码:

//2. 窗口统计去直流分量sum_win[0] -= stati_win[clk%WINSZ][0];sum_win[1] -= stati_win[clk%WINSZ][1];sum_win[0] += fval_i;sum_win[1] += fval_q;stati_win[clk%WINSZ][0] = fval_i;stati_win[clk%WINSZ][1] = fval_q;const double dcremoved[2] {fval_i - sum_win[0]/WINSZ, fval_q - sum_win[1]/WINSZ };

就干了这个事情。等一等,为什么没有for循环?这里面又使用了第二个环状统计缓存器,即均值统计环状缓存。

  1. 缓存记住最新的WINSZ = 4000个坐标,以及他们的和sum_win。
  2. 新的数据到来,sum_win 先减去最老的那个时刻的值,并加上新时刻的值。
  3. 如此循环,每个时钟下,窗口都能跟踪当前的均值了。

3 时钟和频率同步

这是最关键的部分,首先是时钟同步,而后是频率同步。

时钟同步是为了获得地鼠盘,让地鼠稳定的出现在一个看不见的圆圈上,而不是乱窜。

频率同步就是打地鼠,用一个角度跟踪器(软件实现)或者锁相环(硬件)跟踪残差频率造成的相位移动。

3.1 在恰当的时刻获取符号

在4倍采样率下,由于不知道下图红色的正确位置在哪里,所以随便抽取一路作为x,y的坐标值是不行的。

recv

如何获取正确的位置呢?我们通过观察,这些正确位置上,矢量均远离坐标原点;在过渡位置上,一半以上的点贴近坐标原点。因此,只要统计当前四个偏移位置(01,2,3)上,4000个样点的矢量长度和,和最大的哪个就是最佳位置。

//3. 最佳采样位置跟踪const int curr_offv = clk % 4;clk_offset[curr_offv] += (dcremoved[0]*dcremoved[0] + dcremoved[1] * dcremoved[1]); //符号时钟位置同步

这里求矢量长度没有取SQRT,为了省计算。其实是一样的,只是比大小,sqrt不改变其单调性。

由于收发双方的时钟存在误差,这个最佳位置是会不断移动的。一小会是2,后面可能就成了3,0,1, 或者反方向编程了1,0,3,2的重复。所以,每隔一段时间,我们要重新评估这个最佳位置,并只在最佳位置上抽取。其逻辑片段:

	static unsigned long long clk = 0, last_synclk = 0;static int bestOff = 0;//逐个点步进for (int i=0;i<splen;++i){//...//3. 最佳采样位置跟踪const int curr_offv = clk % 4;clk_offset[curr_offv] += (dcremoved[0]*dcremoved[0] + dcremoved[1] * dcremoved[1]); //符号时钟位置同步//在最佳位置输出if (curr_offv == bestOff){//...//解调后的01比特dp_bitbuf.push_back( rp[0] < 0? 0: 1 );}//错开最佳点2符号进行重新评估if (last_synclk + WINSZ < clk && ((clk+2) % 4)==bestOff){last_synclk = clk;int maxo = 0;double maxv = clk_offset[0];if (clk_offset[1] > maxv) { maxo = 1; maxv = clk_offset[1];}if (clk_offset[2] > maxv) { maxo = 2; maxv = clk_offset[2];}if (clk_offset[3] > maxv) { maxo = 3; maxv = clk_offset[3];}bestOff = maxo;clk_offset[0] = 0;	clk_offset[1] = 0;clk_offset[2] = 0;	clk_offset[3] = 0;}++clk;}

错开最佳点2符号进行重新评估,是为了稳定、隐含的解决双方钟差不同步需要进行符号填补、扣除的操作。比如收方每收10000个符号,发方发了10017或者9983个符号,那就要悄默默的追赶/剔除17个,否则就乱套了。这种追赶/剔除行为,就是在 bestOff调整时顺带完成的。bestOff变了,导致下一次输出 if (curr_offv == bestOff) 判断会提前、退后一个点。为了这个行为的稳定,对bestOff的调整要避开当前最佳点,越远越好,于是时机选择在 ((clk+2) % 4)==bestOff。

3.2 测量并咬住相位

一旦找到了正确的时钟位置,测量相位就是一个 x,y坐标到角度的变化了:

	static unsigned long long clk = 0, last_synclk = 0;static int bestOff = 0;static double curr_dp = 0;double phy = 0;//锁相环误差if (curr_offv == bestOff){//锁相旋转double rt[2]{cos(-curr_dp),sin(-curr_dp)};const double out[2]{cache_win[(clk+4)%WINSZ][0],cache_win[(clk+4)%WINSZ][1]};double rp[2]{out[0] * rt[0] - out[1] * rt[1],out[0] * rt[1] + out[1] * rt[0]};//失败重置//...//更新环路curr_dp += (phy/2);curr_dp =	wrapto2pi(curr_dp );//解调后的01比特dp_bitbuf.push_back( rp[0] < 0? 0: 1 );}//...++clk;}

当然,如果角度误差巨大,说明跟踪失败了,可以重置:

			//失锁重置,即重新测量相位误差phy = atan_r(rp[1],rp[0]);if (phy >= c_pi/4 || phy <=-c_pi/4){curr_dp = atan_r(out[1],out[0]);rt[0] = cos(-curr_dp);rt[1] = -sin(curr_dp);rp[0] = out[0] * rt[0] - out[1] * rt[1];rp[1] = out[0] * rt[1] + out[1] * rt[0];phy = atan_r(rp[1],rp[0]);++bad_angle;}

4. 最终效果

通过上述操作,最终紧紧咬住了BPSK 180度的相位变化,获得了正确的数据流。由于不知道确切的相位是0还是180,可能存在极性颠倒。如何抗颠倒,交给上层协议去做了。

完整的SDR实现了双工的协议栈,用主对方两个频率实现全通的IP网络连接。对于从0开始实现的纯C/C++ SDR大玩具,可以非常细致的体会真正的通信系统设计实现环节的各种坑。目前还没有涉及到精确定时。精确定时在粗大的颗粒度下,SDR是可以来做的。要在20ms以下进行定时、TDMA操作,恐怕需要再踩一些坑。不过能够用有限的知识,控制低成本的实验设备完成全套流程,可玩性已经非常高了。

SDR
完整代码参考前文的Git仓库。发行版参考前文连接。

5.思考体会

通过这个实验,我们发现对于基带信号,直接从复平面的特性去思考,要比从受限于模拟器件的角度得到的原理图去思考更直接。使用通用计算机进行操作,或许有更为简便的算法等待开发。应该有教材在介绍传统的解调思路前,把这些思考说清楚,这样学生在学习时,就更为有针对性了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/721168.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

探索Python编程世界:从入门到精通

一.Python 从入门到精通 随着计算机科学的发展&#xff0c;编程已经成为了一种必备的技能。而 Python 作为一种简单易学、功能强大的编程语言&#xff0c;越来越受到人们的喜爱。本文将为初学者介绍 Python 编程的基础知识&#xff0c;帮助他们踏入 Python 编程的大门&#xf…

Linux 驱动 中断(二)

中断下半部 在 Linux 内核中&#xff0c;中断下半部&#xff08;也称为中断下半场&#xff09;是指在中断服务程序&#xff08;Top Half&#xff09;执行完毕后&#xff0c;在上下文之外延迟执行的一些操作。中断下半部通常用于处理那些不适合在中断上下文中立即执行的任务&am…

O2OA(翱途)移动端如何查看系统日志?

O2OA(翱途)移动端如何查看系统日志&#xff1f; 一、进入APP移动端应用&#xff0c;点击右下角“设置” 二、点击“系统日志”&#xff0c;如下图所示 点开日志列表即可查看。

关于 CTF 中 php 考点与绕过那些事的总结

关于 CTF 中常见 php 绕过的总结可以参考我之前的博客&#xff1a; CTF之PHP特性与绕过 PHP特性之CTF中常见的PHP绕过-CSDN博客 其中主要介绍了 md5()、sha1()、strcmp、switch、intval、$_SERVER 函数、三元运算符、strpos() 、数组、非法参数名传参等相关的绕过。 在此基础上…

Linux系统使用宝塔面板安装MySQL服务并实现公网远程访问本地数据库【内网穿透】

文章目录 前言1.Mysql服务安装2.创建数据库3.安装cpolar3.2 创建HTTP隧道 4.远程连接5.固定TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 前言 宝塔面板的简易操作性,使得运维难度降低,简化了Linux命令行进行繁琐的配置,下面简单几步,通过宝塔面板cp…

在Java中处理JSON数据:Jackson与Gson库比较

引言 JSON&#xff0c;作为一种轻量级的数据交换格式&#xff0c;因其易于人阅读和编写&#xff0c;同时也易于机器解析和生成&#xff0c;而被广泛应用于网络通信和配置文件中。在Java中&#xff0c;有两个强大的工具帮助咱们处理JSON数据——Jackson和Gson。这两个库各有千秋…

从零开始写 Docker(四)---使用 pivotRoot 切换 rootfs 实现文件系统隔离

本文为从零开始写 Docker 系列第四篇&#xff0c;在mydocker run 基础上使用 pivotRoot 系统调用切换 rootfs 实现容器和宿主机之间的文件系统隔离。 完整代码见&#xff1a;https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实现有一个大致认识&…

javascript作用域编译浅析

作用域思维导图 1&#xff1a;编译原理 分词/词法分析 如果词法单元生成器在判断a是一个独立的词法单元还是其他词法单元的一部分时&#xff0c;调用的是有状态的解析规则&#xff0c;那么这个过程就被称为词法分析。 解析/语法分析 由词法单元流转换成一个由元素逐级嵌套所组…

期货开户交易软件如何下单?

一、手机和电脑使用的交易软件 目前期货市场常用的软件有文华、博弈、快期、易盛、同花顺等&#xff0c;这5款电脑软件对应的手机端是文华随身行、博弈手机版、快期小Q、易盛易星、同花顺期货通&#xff0c;这些常用软件大部分期货公司都是支持的。 二、交易软件如何下单 软…

C打印内存16进制

下面是一段C代码打印16进制 void print_hex(const char *msg, void *addr, int len) {uint8_t *p (uint8_t *)addr;printf("%s ,stat:%0x8, len:%d\n", msg, addr, len);for (int i 0; i < len / 16; i) {printf("0x%08x: ", p i * 16);for (int j …

【音视频开发好书推荐】《RTC程序设计:实时音视频权威指南》

1、WebRTC概述 WebRTC&#xff08;Web Real-Time Communication&#xff09;是一个由Google发起的实时音视频通讯C开源库&#xff0c;其提供了音视频采集、编码、网络传输&#xff0c;解码显示等一整套音视频解决方案&#xff0c;我们可以通过该开源库快速地构建出一个音视频通…

【小黑嵌入式系统第十七课】结课总结(一)——硬件部分(系统总线处理器外设通信)

上一课&#xff1a; 【小黑嵌入式系统第十六课】PSoC 5LP第三个实验——μC/OS-III 综合实验 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff1a;人工智能 文章目录 一、基础知识…

2024新版SonarQube+JenKins+Github联动代码扫描(1)-JenKins安装与配置

文章目录 前言一、官网下载二、访问ip:port三、安装推荐插件-自动下载四、创建用户名密码五、安装SonarQube插件六、配置全局工具总结 前言 Sonar是一个半开源的静态代码扫描工具&#xff0c;试用过一次觉得功能还算可以&#xff0c;所以记录一下SonarQube扫描的用法以及在中大…

IO接口 2月5日学习笔记

1.fgetc 用于从文件中读取一个字符&#xff0c;fgetc 函数每次调用将会返回当前文件指针所指向的字符&#xff0c;并将文件指针指向下一个字符。 int fgetc(FILE *stream); 功能: 从流中读取下一个字符 参数: stream:文件流指针 返回值: …

5分钟速成渐变色css

色彩的分支——渐变色定义&#xff1a;按照一定规律做阶段性变化的色彩&#xff08;抽象&#xff01;&#xff01;&#xff01;&#xff09; 我们可以将图片分为两块 以中心线为参考&#xff0c;再来看渐变色的定义&#xff1a;按照一定规律做阶段性变化的色彩 既然是按一定的…

使用Android Native Hook技术解决VLC播放器闪退的问题

文章目录 1.概述2.问题描述3.问题分析4.问题解决5.总结 1.概述 在做公司的一个TOB的需求时&#xff0c;发现调起Unity提供的3D播放器播放网络在线视频时闪退了&#xff0c;然后就拉着相关部门的人一起分析问题&#xff0c;最后定位到是VLC里面用到的系统日志打印函数在部分的系…

动态代理IP的并发处理技巧

目录 前言 一、什么是动态代理IP&#xff1f; 二、动态代理IP的并发处理技巧 1. 获取代理IP 2. 动态生成代理对象 3. 并发处理 总结 前言 在进行网络爬虫开发时&#xff0c;经常会遇到限制IP访问频率的情况。为了突破这个限制&#xff0c;我们可以通过使用代理IP来实现…

华为配置基于VLAN限速示例

华为配置基于VLAN限速示例 组网图形 图1 流量监管配置组网图 表1 Switch为上行流量提供的QoS保障 流量类型 CIR(kbps) PIR(kbps) DSCP优先级 语音 2000 10000 46 视频 4000 10000 30 数据 4000 10000 14 ^^^ 流分类简介配置注意事项组网需求配置思路操作步…

C#与欧姆龙PLC实现CIP通讯

参考文档&#xff1a; 欧姆龙PLC使用-CSDN博客 CIP通讯介绍&#xff08;欧姆龙PLC&#xff09;-CSDN博客 使用NuGet添加引用&#xff1a;CIPCompolet 基础参考我的CIP协议介绍&#xff0c;默认TCP端口为&#xff1a;44818 类NXCompolet 类的功能可以在安装PLC开发软件后帮…

vue svelte solid 虚拟滚动性能对比

前言 由于svelte solid 两大无虚拟DOM框架&#xff0c;由于其性能好&#xff0c;在前端越来越有影响力。 因此本次想要验证&#xff0c;这三个框架关于实现表格虚拟滚动的性能。 比较版本 vue3.4.21svelte4.2.12solid-js1.8.15 比较代码 这里使用了我的 stk-table-vue(np…