相关文章:
Xilinx IP 解析之 Block Memory Generator v8.4 ——01-手册重点解读(仅Native RAM) – 徐晓康的博客
Xilinx IP 解析之 Block Memory Generator v8.4 ——02-如何配置 IP(仅 Native RAM) – 徐晓康的博客
Verilog功能模块–RAM和ROM(01)–功能说明与关键代码解析 – 徐晓康的博客
Verilog功能模块–RAM和ROM(02)–同步写-写冲突与读-写冲突实测 – 徐晓康的博客
Verilog 功能模块–RAM 和 ROM(03)–自编 RAM 与 Vivado RAM IP 功能对比实测 – 徐晓康的博客
前言
要理解Vivado RAM IP,并在自编RAM时保证读写行为和Vivado RAM IP完全一致,一个重点就是理解冲突行为。根据pg058的描述,Vivado RAM并没有任何的冲突抑制逻辑,它只是在仿真时会报一个冲突警告,同样自编RAM也没有冲突处理,那么,当冲突发生时,两个RAM的行为是否一致呢?或者说,如果冲突时写入数据或读取数据可能是随机的,那么要做的就不是去保证自编RAM的冲突行为和Vivado RAM一致了,而应该是在调用时避免冲突发生。
因为仿真有局限性,并不能准确反应真实的硬件行为,这里选择上板实测的方式,通过ILA抓取信号,来研究一下RAM的冲突行为。
异步时钟没必要测,冲突时数据必然是随机的,其它类型的RAM冲突行为应和TDPRAM相同,所以,本文仅实测了同步时钟下的TDPRAM的冲突行为,包括写-写冲突和读-写冲突。
一、上板实测实验设计思路
1.1 需要进行哪些实验?
主要进行 TDPRAM 的实验测试,它的功能是最复杂的,其它类型 RAM 或 ROM 的底层逻辑和 TDPRAM 基本相同。
对于 SPRAM、SDPRAM、SPROM、DPROM 仅进行一次或两次实验验证。
几个需要实验验证的点:
-
运行尽量高的频率,以验证高频性能 -
验证 en 信号是否能正常工作 -
验证 WF、RD、NC 三种操作模式是否能正常工作 -
验证无输出寄存、一级输出寄存、二级输出寄存是否能正常工作 -
验证 COE 文件初始化功能是否能正常工作 -
验证初始化默认值功能(无 COE 或 COE 数据少于存储深度)是否能正常工作
1.2 需要观察哪些实验结果?
1.2.1 写-写冲突时写入数据是什么?
顶层文件设计了两个用于观测的指示信号
//* 检测写冲突后,没有对地址写,而是去读此地址的情况
(* mark_debug *)reg write_collision_clka_locked;
(* mark_debug *)reg [ADDR_WIDTH-1:0] addr_locked;
(* mark_debug *)reg rd_collision_addr_flag;
always @(posedge clka) begin
if (addra == addrb && (true_ena && wea) && (true_enb && web)) begin
write_collision_clka_locked <= 1'b1;
addr_locked <= addra;
end
if (write_collision_clka_locked) begin
if (addr_locked == addra)
if (true_ena && wea)
write_collision_clka_locked <= 1'b0;
else if (true_ena && ~wea)
rd_collision_addr_flag <= 1'b1;
else
;
else if (addr_locked == addrb)
if (true_enb && web)
write_collision_clka_locked <= 1'b0;
else if (true_enb && ~web)
rd_collision_addr_flag <= 1'b1;
else
;
end
else
rd_collision_addr_flag <= 1'b0;
if (rd_collision_addr_flag)
write_collision_clka_locked <= 1'b0;
end
其中,write_collision_clka_locked 为 1 表示写冲突发生了,但没有再一次对相同地址的写入;
rd_collision_addr_flag 为 1,表示写冲突发生后,相同地址没有被写入,而是先被读取了,这样观察读数据就能知道到底写入了什么数据。
在 ILA 中设置 rd_collision_addr_flag = 1 为触发条件即可观测写-写冲突的情况。
注意,这里仅使用了 clka,所以,实验仅做了同步时钟写-写冲突的情况
;对于异步时钟的写-写冲突,这里认为没有任何规律,到底写入什么数据是随机的,没有设置信号去判断是否发生了异步的写-写冲突。
1.2.2 读-写冲突时,读出数据是什么?
设计了 a_wr_b_rd_collision 与 a_rd_b_wr_collision 信号用于指示读写冲突
always @(posedge clka) begin
if (addra == addrb && (true_ena && wea)) a_wr_b_rd_collision <= 1'b1;
else a_wr_b_rd_collision <= 1'b0;
end
always @(posedge clkb) begin
if (addra == addrb && (true_enb && web)) a_rd_b_wr_collision <= 1'b1;
else a_rd_b_wr_collision <= 1'b0;
end
1.2.3 自编 RAM 读数据是否和 Vivado IP 读数据一直保持一致?
设计了一下两个信号,用于判断读数据是否一致。
(* mark_debug *)wire douta_is_not_equal_vivado_douta = douta != vivado_douta;
(* mark_debug *)wire doutb_is_not_equal_vivado_doutb = doutb != vivado_doutb;
1.3 如何观测实验波形
通过 ILA 观测波形图,并保证用高频时钟作为唯一的采样时钟,这样所有波形才能在一个 ILA 窗口,如 300MHz 和 120MHz 都使用时,选择 300MHz 作为采样时钟。
1.4 如何保证 en、we、din、addr 信号的随机性
使用线性反馈移位寄存器(Linear Feedback Shift Register,简称 lfsr)作为伪随机数发生器,代码如下,此模块可综合。可通过VIO控制复位和seed,以方便在运行阶段实时改变seed
。
module lfsr (
output reg [15:0] random_num,
input wire [15:0] seed,
input wire clk,
input wire rstn // 复位时设置种子
);
// LFSR反馈多项式:x^16 + x^14 + x^13 + x^11 + 1
wire feedback = random_num[15] ^ random_num[13] ^ random_num[12] ^ random_num[10];
always @(posedge clk) begin
if (~rstn)
if (seed == 0)
random_num <= 16'hACE1; // 初始化种子(非零值)
else
random_num <= seed;
else
random_num <= {random_num[14:0], feedback};
end
endmodule
用随机数的部分位给 en、we、din、addr 赋值,保证输入是随机的。
二、同步时钟下的写-写冲突实验设计
pg058对于同步写-写冲突的描述:
同步写-写冲突:如果两个端口都尝试向内存中的同一位置写入数据,就会发生
写-写冲突。此时该内存位置最终的内容是未知的
。请注意,写-写冲突会影响内存内容,而与之不同的是,写-读冲突仅会影响数据输出。
本章通过实测自编RAM和Vivado RAM的同步写-写冲突行为,看实际情况是否和pg058描述相符,并加深对同步写-写冲突的理解。
序号 | 实测实验条件 | 波形图 | 结论 |
---|---|---|---|
1 | 1.TDPRAM 2.同步时钟–50MHz 3.A 端口和 B 端口均 Write First 模式 4.无输出寄存器 5.无 en 信号 6.不使用 COE |
同步写-写冲突波形图-1 | 同步写-写冲突时: 自编 RAM 写入数据是 A 端口的 Vivado RAM 写入数据是端口 B 的 |
2 | 2.同步时钟–100MHz 其余不变 |
同步写-写冲突波形图-2 | 结论与 1 一样 |
3 | 2.同步时钟–200MHz 其余不变 |
同步写-写冲突波形图-3 | 同步写-写冲突时: 自编 RAM 写入数据是 B 端口的 Vivado RAM 写入数据是端口 A 的 结论与 1 相反 |
4 | 2.同步时钟–300MHz 其余不变 |
同步写-写冲突波形图-4 同步写-写冲突波形图-4-1 |
结论与 1 相反; 小概率情形:自编 RAM 在冲突发生时,写入数据随机,既不是 A 输入也不是 B 输入 |
5 | 2.同步时钟–150MHz 其余不变 |
同步写-写冲突波形图-5 | 结论与 1 相反; 小概率情形:Vivado RAM 在冲突发生时,写入数据随机,既不是 A 输入也不是 B 输入 |
6 | 2.同步时钟–120MHz 其余不变 |
同步写-写冲突波形图-6 | 结论与 1 相反; 小概率情形:Vivado RAM 在冲突发生时,写入数据随机,既不是 A 输入也不是 B 输入 |
7 | 2.同步时钟–110MHz 其余不变 |
同步写-写冲突波形图-7 | 结论与 1 一样 |
8 | 2.同步时钟–115MHz 其余不变 |
同步写-写冲突波形图-8 同步写-写冲突波形图-8-1 |
某些冲突点两个RAM写入一样,但总是可能在冲突发生时写入错乱值。结论: 同步写-写冲突没有规律,可能写入A端口的值,也可能写入B端口的值,还可能写入错乱值。 应在调用TDPRAM的上层模块来控制避免发生写-写冲突 |
相关代码:
always @(posedge clka) begin
if (write_collision_clka_locked)
wea <= 1'b0;
else
wea <= random_num[0];
addra <= random_num[ADDR_WIDTH-1 : 0];
dina <= random_num[DATA_WIDTH-1 : 0];
end
always @(posedge clkb) begin
if (write_collision_clka_locked)
web <= 1'b0;
else
web <= random_num[8];
addrb <= random_num[ADDR_WIDTH-1+8 : 8];
dinb <= random_num[DATA_WIDTH-1+2 : 2];
end
当 write_collision_clka_locked 检测到写-写冲突发生时,we 信号始终为 0,直到读此地址的动作发生,这是为了保证,写-写冲突后,不会发生再一次的同地址写,防止写-写冲突被覆盖。
同步写-写冲突波形图-1
:
箭头 1 处,A 和 B 同时对地址 1 进行写入,dina = 0x161,dinb = 0x058;
箭头 2 处,B 端口读取地址 1,doutb = 0x161,vivado_doutb = 0x058。
这说明,这一次的实验,同时写-写冲突发生时,自编 RAM 写入的是 A 端口数据,而 Vivado RAM 写入的是 B 端口数据。
我们继续看 ILA 抓取到的波形,看下一次发生写-写冲突的情况:
箭头 3 处,A 和 B 同时对地址 d 进行写入,dina = 0x13d,dinb = 0x34f;
箭头 4 处,B 端口读取地址 d,doutb = 0x13d,vivado_doutb = 0x34f。
还是相同的结论:同时写-写冲突发生时,自编 RAM 写入的是 A 端口数据,而 Vivado RAM 写入的是 B 端口数据。
继续观察下一次写-写冲突的情况,如下图所示,结论还是一样。
那是不是能说明,写-写冲突有规律呢?自编 RAM 的 A 端口写优先级高于 B,而 Vivado RAM 则反过来呢?其实不是,这只是当前布局布线是这样的情况,什么条件都不变,重新生成 bit 文件试一下。结论还是一样。
接下来改变同步时钟频率,从 50MHz 改为 100MHz。
同步写-写冲突波形图-2
:结论一样。
什么条件都不变,重新生成 bit 文件试一下。结论还是一样。
继续改变同步时钟频率,从 100MHz 改为 200MHz。
同步写-写冲突波形图-3
:
箭头 1 处,A 和 B 同时对地址 1 进行写入,dina = 0x161,dinb = 0x058;
箭头 2 处,B 端口读取地址 1,doutb = 0x058,vivado_doutb = 0x161。
好吧,结论反过来了,自编 RAM 的 A 端口优先级低于 B,Vivado RAM 的 A 端口优先级高于 B。
箭头 3 和 4 处,结论也是一样反过来。什么条件都不变,重新生成 bit 文件试一下。结论还是一样反过来。
继续改变同步时钟频率,从 200MHz 改为 300MHz。
同步写-写冲突波形图-4
:
这里可能因为频率过高,组合逻辑的 doutb_is_not_equal_vivado_doutb 信号有问题,从 ILA 看两个输出是一样的。重新将 doutb_is_not_equal_vivado_doutb 和 douta_is_not_equal_vivado_douta 都改为时序逻辑:
(* mark_debug *)reg douta_is_not_equal_vivado_douta;
(* mark_debug *)reg doutb_is_not_equal_vivado_doutb;
always @(posedge clka) begin
douta_is_not_equal_vivado_douta = douta != vivado_douta;
doutb_is_not_equal_vivado_doutb = doutb != vivado_doutb;
end
重新生成 bit,再观察 ILA,同步写-写冲突波形图-4-1
:
结论与 3 一样,与 1 相反。什么条件都不变,重新生成 bit 文件试一下。同步写-写冲突波形图-4-2
:
前 2 个箭头处,输出与之前一样,注意第四个箭头,doutb = 0x13f,而第三个箭头处 A 端口的 dina = 13d,显然这个 13f 既不是 A 端口写入数据,也不是 B 端口写入数据 0x34f,显然数据错乱了。
这个数据错乱只发生在了写-写冲突的位置,其它位置数据是正常的,说明,写-写冲突的实际写入是不确定的,可能既不是 dina 也不是 dinb,这和 Xilinx pg058 BRAM 手册上关于同步时钟下的写-写冲突的描述是一样的,即实际写入数据随机。
再重新生成 bit 文件试一下,同步写-写冲突波形图-4-3
:又回到了 同步写-写冲突波形图-4-1
的状态。
结论:同步写-写冲突发生时,写入有小概率是随机的。
基于之前的结论,100MHz 的结论与 200MHz 相反,那么 100~200MHz,是否有个区间,自编 RAM 的在写入冲突时的写入值与 Vivado RAM 相同呢?基于这个思路,改变时钟频率为 150MHz,同步写-写冲突波形图-5
:
注意,第二个箭头处,Vivado_doutb 的输出为 0x169,它既不等于写-写冲突时,A 端口输入 0x161 也不等于 B 端口输入 0x058,可见,之前发现的现象写-写冲突时,自编 RAM 写入错乱并不是自编 RAM 才有,Vivado RAM 也是有的。这里几乎能得到结论,同步写-写冲突,写入数据是随机的,并不一定等于A或B的输入
。
什么条件都不变,重新生成 bit 文件试一下。结论:Vivado_doutb 还是出现了输出为 0x169 的现象。
继续改变时钟频率为 120MHz,同步写-写冲突波形图-6
:
注意第二个箭头,Vivado_doutb 的输出为 0x149,发生了错乱。结论与 150MHz 时相同。重新生成 bit 文件,结论还是一样。
继续改变时钟频率为 110MHz,同步写-写冲突波形图-7
:
结论与100MHz时相同,与120MHz时相反。这时就考虑是不是110~120MHz的某个频率,自编RAM和Vivado RAM的写-写冲突行为会一致呢?虽然说这个一致与否并不是很有意义,因为从理论上来说写-写冲突的写入数据是随机的,一致也可能只是这一次布局布线凑巧而已,但是可以尝试一下,找到一致的频率点。
继续更改时钟频率为为115MHz,同步写-写冲突波形图-8
:
从波形图上看好像两个RAM的写-写冲突写入值是一样的,但也是某些时刻一样,在其它时刻又会不一样,同步写-写冲突波形图-8-1
:
显然,第二个箭头处,Vivado_doutb的值为0x03e,这个也是随机的错乱值,所以说,想找到一个频率点,让两个RAM对写-写冲突的写入是一样的,是不可能的,某几次冲突可能一样,但当写-写冲突导致写入随机值时,就会不一样了。
到这里,我们可以得到结论,无论是自编TDPRAM,还是Vivado的TDPRAM IP,只要发生写-写冲突,就可能写入错乱值,也就是说不需要去设定某些逻辑去限制当写-写冲突发生时,应该以A端口为准还是B端口为准
,而应该在调用TDPRAM的上层逻辑去避免发生写-写冲突
,因为写-写冲突可能造成该地址的存储值错乱。
三、同步时钟下的读-写冲突实验设计
PG058对于同步读写冲突的描述:
同步写-读冲突:如果一个端口尝试向某一内存位置写入数据,而另一个端口读取该相同位置的数据,就可能会发生同步写-读冲突。在写-读冲突中,虽然内存内容不会被破坏,但输出数据的有效性取决于写端口的操作模式。
如果写端口处于读优先(READ_FIRST)模式,另一个端口能够可靠地读取旧的内存内容。
如果写端口处于写优先(WRITE_FIRST)模式或无变化(NO_CHANGE)模式,读端口输出的数据是无效的。
本章通过实测自编RAM和Vivado RAM的同步读-写冲突行为,看实际情况是否和pg058描述相符,并加深对同步读-写冲突的理解。
本章只研究同步读-写冲突,需要避免同步写-写冲突干扰观察
,怎么避免呢?代码如下:
always @(posedge clka) begin
wea <= random_num[13];
addra <= random_num[ADDR_WIDTH-1+8 : 8];
dina <= random_num[DATA_WIDTH-1+2 : 2];
end
reg web_tmp;
always @(posedge clkb) begin
web_tmp <= random_num[5];
addrb <= random_num[ADDR_WIDTH-1 : 0];
dinb <= random_num[DATA_WIDTH-1 : 0];
end
always @(*) begin
if (addra == addrb && wea)
web = 1'b0;
else
web = web_tmp;
end
A端口信号保持随机,B端口的写使能web在检测到A端口正在写时赋值0,避免写-写冲突发生。
这里必须使用组合逻辑进行判断,否则判断延迟一个时钟周期,写-写冲突还是可能发生。
同步读-写冲突实验设计:
序号 | 实测实验条件 | 波形图 | 结论 |
---|---|---|---|
1 | 1.TDPRAM 2.同步时钟–50MHz 3.A 端口和 B 端口均 Write First 模式 4.无输出寄存器 5.无 en 信号 6.不使用 COE |
同步读-写冲突波形图-1 | 同步读-写冲突时: Vivado RAM的A端口读数据读到了B端口的最新输入,而自编RAM没有; 但自编RAM的B端口读到了A端口的最新输入,而Vivado RAM没有 |
2 | 2.同步时钟–100MHz 其余不变 |
同步读-写冲突波形图-2 同步读-写冲突波形图-2-1 |
同步读-写冲突时: 小概率情形: Vivado RAM可能读数据错乱 自编RAM也可能读数据错乱 |
3 | 2.同步时钟–100MHz 3.A端口 Read First模式,B端口No Change模式 其余不变 |
同步读-写冲突波形图-3 同步读-写冲突波形图-3-1 |
B端口读正常, A端口读可能出错, 与PG058对读-写冲突的描述吻合 |
4 | 2.同步时钟–100MHz 3.A端口 Read First模式,B端口Read First模式 其余不变 |
同步读-写冲突波形图-4 | A,B端口读均正常 与PG058对读-写冲突的描述吻合 |
同步读-写冲突波形图-1
:
箭头1:A读B写,地址c,douta=0x338,vivado_douta=0x03c=dinb,说明,Vivado RAM的A端口读数据读到了B端口的最新输入,而自编RAM没有;
箭头2:A读B写,地址d,douta=0x245,vivado_douta=0x13d=dinb,说明,与箭头1相同;
箭头3:A写B读,地址a,doutb=0x29e=dina,vivado_doutb=0x03a,说明,自编RAM的B端口读到了A端口的最新输入,而Vivado RAM没有,与箭头1相反;
箭头4,A写B读,地址4,doutb=0x13d=dina,vivado_doutb=0x074,说明,与箭头3相反
箭头5,A写B读,地址9,doutb=0x27a=dina,vivado_doutb=0x0e9,说明,与箭头3相反
结论,Vivado RAM的A端口读数据读到了B端口的最新输入,而自编RAM没有;但自编RAM的B端口读到了A端口的最新输入,而Vivado RAM没有。
这应该就是pg058的Vivado RAM IP手册中对同步读-写冲突的描述:如果写端口处于写优先(WRITE_FIRST)模式或无变化(NO_CHANGE)模式,读端口输出的数据是无效的。
最终结论可能是,同步读-写冲突发生时,读数据是无效的,可能是存储器旧数据,也可能是新写入数据,或者是随机数据?这个还需要再观察。
改变时钟频率为100MHz,ILA波形图,同步读-写冲突波形图-2
:
箭头1,A读B写,地址d,douta=0x205,vivado_douta=0x13d=dinb,说明,Vivado RAM的A端口能读数据能读出B端口最新输入,而自编RAM不行;
箭头2,A写B读,地址a,doutb=0x29e=vivado_doutb=dina,说明Vivado RAM和自编RAM的B端口都能读出A端口的最新输入;
箭头3,A写B读,地址4,doutb=0x13d=dina,vivado_doutb=0x034,说明自编RAM的B端口能读出A端口的最新输入,而Vivado RAM不行;
箭头4,结论和箭头3相同。
再继续观察ILA,同步读-写冲突波形图-2-1
:
箭头1:端口A写入地址2,数据0x0af;
箭头2,端口B写入地址1,数据0x1e1;
箭头3,A写B读,地址1,doutb=0x054=dina,而vivado_doutb=0x044,这既不等于dina也不等于旧存储器值0x1e1,这就是所谓的读取数据错乱,说明,vivado RAM的B端口在读-写冲突时发生了读数据错乱;
箭头4,A读B写,地址2,vivado_dina=0x2a2=dinb,而dina=0x0a2,这既不等于dinb也不等于旧存储器值0x0af,说明,自编RAM的A端口在读-写冲突时发生了读数据错乱。
根据PG058对于同步读-写冲突的描述,当写端口设置为Read First时,读端口应能稳定读取到存储器旧数据;当写端口设置为No Change时,读端口读数据无效。为此将A端口模式改为Read First,B端口改为No Change
,观察ILA,同步读-写冲突波形图-3
:
这是一个整理视图,可以看到douta_is_not_equal_vivado_douta有置高的,说明A端口读数据时,自编RAM和Vivado RAM有不同的;而doutb_is_not_equal_vivado_doutb没有置高,说明B端口读数据时,自编RAM和Vivado RAM始终相同,上述将A端口设置为了Read First,这个设置保证了B端口读数据始终能读到存储器旧数据,实测证明了PG058的这一说法。
继续看其他读-写冲突时刻,同步读-写冲突波形图-3-1
:
第一个箭头,B端口写入地址7,dinb=0x177;
第二个箭头,B写A读,B端口因为是No Change所以读数据不变,没问题;A端口读数据,doutb=0x377,vivado_doutb=0x177,自编RAM读出的是B正在写入的数据,而Vivado RAM读出的是旧存储器数据,这里就不一样了。这说明了,B端口设置为No_Change时功能正常,当读-写冲突发生时,写端口为No Change时,读端口数据是无效的,进一步证明了PG058描述是符合实际的。
那么继续进行实现,将A和B端口都设置为Read First,那么根据PG058描述,即使冲突发生,RAM读数据应该是稳定的旧存储器数据,那么两个RAM应该不会出现读数据不一样的情况,观察ILA,同步读-写冲突波形图-4
:
可以看到,再改了Read First之后,两个RAM的读取数据始终一致了。可见,PG058关于同步读-写冲突的描述是准确的,我们实测也是一样的结果,还说明了自编RAM的同步读-写冲突行为和Vivado RAM也是一致的。
到这里我们得到结论
:
-
如果写端口处于读优先(READ_FIRST)模式,另一个端口能够可靠地读取旧的内存内容。
-
如果写端口处于写优先(WRITE_FIRST)模式或无变化(NO_CHANGE)模式,读端口输出的数据是无效的。
-
自编RAM和Vivado RAM对于同步读-写冲突的行为完全一致。
四、总结与分享
通过对同步时钟下的自编TDPRAM和Vivado TDPRAM IP的上板实测观察,我们可以得到以下结论:
-
同步写-写冲突发生时,两个TDPRAM写入值都可能是错乱的,它可能是A端口输入,也可能是B端口输入,也可能是随机值;
-
同步读-写冲突发生时,写入没有问题,但读数据可能是错乱的,读端口数据可能等于写端口正在写入的数据,可能等于旧存储器数据,也可能是随机值。
所以,在使用TDPRAM时,应在调用模块保证不会发生同步冲突,同步写-写冲突可能破坏存储器数据,应避免;读-写冲突可能导致读数据出错,也应该避免。
这里没有去实测异步时钟,显然,无论是根据Vivado RAM IP的手册pg058,还是自编RAM的代码逻辑,异步时钟下,也一样会发生写-写或读-写冲突,也需要在调用时避免冲突的发生,但异步时钟的冲突控制会更困难一些。
本模块开源,两处仓库同步。
Gitee: Verilog 功能模块–RAM 和 ROM https://gitee.com/xuxiaokang/verilog-function-module–RAM_ROM
仿真和实测的Vivado 2024.2工程通过网盘分享(本系列文章所有分享均相同,均需获取一次即可):
欢迎大家关注我的微信公众号:徐晓康的博客,回复以下6位数字获取网盘链接。
402368
建议复制过去不会码错字!
如果本文对你有所帮助,欢迎点赞、转发、收藏、评论让更多人看到,赞赏支持就更好了。
如果对文章内容有疑问,请务必清楚描述问题,留言评论或私信告知我,我看到会回复。

徐晓康的博客持续分享高质量硬件、FPGA与嵌入式知识,软件,工具等内容,欢迎大家关注。