每周分享第 34 期
- macOS Mojave 10.14.6 iOS 12.4 正式版更新发布,还有老版本的更新 https://www.macrumors.com/2019/07/22/apple-releases-gps-bug-fix-older-iphones-ipads/
- CLion 2019.2 添加了 GDB Server 的调试支持,可以用 OpenOCD。
之前做过一个在前端解析上传的 CSV 的功能,但是只能支持部分的 encoding,遇到 gbk 就傻眼了。一番研究以后,找到了比较科学的方案:
import * as Chardet from 'chardet';
import * as Iconv from 'iconv-lite';
const reader = new FileReader();
reader.onload = (e) => {
const data = e.target.result;
const view = Buffer.from(data);
// detect encoding and convert
const encoding = Chardet.detect(view);
const result = Iconv.decode(view, encoding);
const csvData = Papa.parse(result).data;
// do anything with it
};
reader.readAsArrayBuffer(blob_here);
依赖了两个库:chardet
和 iconv-lite
,测试了一下,解析 UTF-8 GBK UTF-16BE 都没问题。
P.S. 在生成 csv 的时候,也会出现 Excel 打开后乱码的问题,一开始我以为需要转 UTF-16 然后再添加 BOM Mark,后来发现只要在最前面加上 0xEF 0xBB 0xFB(UTF-8 编码下的 BOM Mark)即可。
一个可参考的防止 side channel attack 的方法 https://www.undeadly.org/cgi?action=article;sid=20190621081455
物理上镜像百兆的方法 实测可用 https://greatscottgadgets.com/throwingstar/
微软员工写的 malloc https://github.com/microsoft/mimalloc
超标量乱序执行的 x86 软核 https://tspace.library.utoronto.ca/bitstream/1807/80713/1/Wong_Henry_T_201711_PhD_thesis.pdf
Rust 的 wireshark 替代 https://github.com/kpcyrd/sniffglue
Rpi 4 发布 https://readhacker.news/s/45nK9
iOS/macOS Catalina PB1 发布(= DB2)
dbg! 移植到了 C++ https://github.com/sharkdp/dbg-macro
类似于 Elm 的 Rust 前端框架 https://github.com/David-OConnor/seed
访问 VMware 虚拟机串口的方法 https://thewayeye.net/2009/december/4/connecting-virtual-machines-serial-console-os-x-and-vmware-fusion/
最近发现了两个很神奇的 Undefined Behavior,出现在 Prefix Len 和 Netmask 的转换的问题下。一个简单思路可能是:
#define PREFIX_BIN2DEC(bin) (32 - __builtin_ctz((bin)))
#define PREFIX_DEC2BIN(hex) (((~0) >> (32 - (hex))) << (32 - (hex))
乍一看,似乎没有什么问题。但是,在一些平台下,可能会出现这样的结果:
而且只能在一些平台上不确定地复现,最后发现其实是 Undefined Behavior,在 C 的标准中:
In any case, the behavior is undefined if rhs is negative or is greater or equal the number of bits in the promoted lhs.
意味着, 0xFFFFFFFF >> 32
是一个 UB,所以出现了上面的问题。
另外,__builtin_ctz
有这样的说明:
Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
意味着,__builtin_ctz(0)
也是一个 UB,所以得到了错误的结果。
解决方案也很简单,下面提供一个参考的解决方法:
#define PREFIX_BIN2DEC(bin) ((bin) ? (32 - __builtin_ctz((bin))) : 0)
#define PREFIX_DEC2BIN(hex) (((uint64_t)0xFFFFFFFF << (32 - (hex))) & 0xFFFFFFFF)
Quagga 的实现:
/* Convert masklen into IP address's netmask (network byte order). */
void
masklen2ip (const int masklen, struct in_addr *netmask)
{
assert (masklen >= 0 && masklen <= IPV4_MAX_BITLEN);
/* left shift is only defined for less than the size of the type.
* we unconditionally use long long in case the target platform
* has defined behaviour for << 32 (or has a 64-bit left shift) */
if (sizeof(unsigned long long) > 4)
netmask->s_addr = htonl(0xffffffffULL << (32 - masklen));
else
netmask->s_addr = htonl(masklen ? 0xffffffffU << (32 - masklen) : 0);
}
/* Convert IP address's netmask into integer. We assume netmask is
sequential one. Argument netmask should be network byte order. */
u_char
ip_masklen (struct in_addr netmask)
{
uint32_t tmp = ~ntohl(netmask.s_addr);
if (tmp)
/* clz: count leading zeroes. sadly, the behaviour of this builtin
* is undefined for a 0 argument, even though most CPUs give 32 */
return __builtin_clz(tmp);
else
return 32;
}
BIRD 的解决方法:
/**
* u32_mkmask - create a bit mask
* @n: number of bits
*
* u32_mkmask() returns an unsigned 32-bit integer which binary
* representation consists of @n ones followed by zeroes.
*/
u32
u32_mkmask(uint n)
{
return n ? ~((1 << (32 - n)) - 1) : 0;
}
/**
* u32_masklen - calculate length of a bit mask
* @x: bit mask
*
* This function checks whether the given integer @x represents
* a valid bit mask (binary representation contains first ones, then
* zeroes) and returns the number of ones or 255 if the mask is invalid.
*/
uint
u32_masklen(u32 x)
{
int l = 0;
u32 n = ~x;
if (n & (n+1)) return 255;
if (x & 0x0000ffff) { x &= 0x0000ffff; l += 16; }
if (x & 0x00ff00ff) { x &= 0x00ff00ff; l += 8; }
if (x & 0x0f0f0f0f) { x &= 0x0f0f0f0f; l += 4; }
if (x & 0x33333333) { x &= 0x33333333; l += 2; }
if (x & 0x55555555) l++;
if (x & 0xaaaaaaaa) l++;
return l;
}
高考加油!
又半个月过去了,在写了上篇系列博文之后也是做了很多新的更改。上次做的主要是关于性能方面的提升,怎么提高频率,从而达到比较大的流量,而这段时间做的则是功能,做实现 RIP 协议和转发表的动态更新。
软件部分目前是用 C 代码写的,用 Xilinx SDK 提供的各个 AXI 外设的驱动和 PS 自己的驱动,实现了所需要的,RIP 协议的处理,转发表的更新和统计信息的读取。
实际上做的时候比较粗暴,主要是通过三种 AXI 外设与硬件部分进行交互:AXI Stream FIFO,AXI GPIO 和 AXI BRAM Controller。其中 AXI Stream FIFO 是用来接收和发送需要 CPU 处理的以太网帧的,AXI GPIO 则是用来读取统计的信息,AXI BRAM Controller 是用来读写转发表的。最后在顶层设计中把这些外设连接起来。
硬件部分还是继续之前的部分往下写,添加了统计信息,直接暴露出去,让 CPU 走 AXI GPIO 读,因为不需要很高的精确度;转发表本身,一开始想的是自己写一些接口转换,后来发现,直接用 True Dual Port RAM 然后把一个 port 暴露给 AXI BRAM Controller 即可,免去了各种麻烦,PS 可以直接进行修改,不需要额外的工作。
为了测试这套东西是否正常工作,就开了两个 Arch Linux 的虚拟机,分别 Bridge 到两个千兆的 USB 网卡上,都连到 FPGA 上。然后在两边都配上了 BIRD,配置 RIP 和一些路由,确实能更新硬件的转发表,并两边的 RIP 可以学习到对方的路由。
之前在写 IP Checksum 的增量更新,就是当 TTL -= 1 的时候,Checksum 应该增加 0x0100,但是这样会有问题,在于,如果按照原来的 IP Checksum 计算方法,是不会出现 0xFFFF 的(求和,进位,然后取反写入),这种加法就有可能出现 0xFFFF。于是翻阅了相关的 RFC:
首先是 RFC 1141 相关部分:
unsigned long sum;
ipptr->ttl--; /* decrement ttl */
sum = ipptr->Checksum + 0x100; /* increment checksum high byte*/
ipptr->Checksum = (sum + (sum>>16)) /* add carry */
这也是比较直接能想到的一种方法,但是会出现刚才提到的问题。于是 RFC 1624 纠正了这个问题:
Although this equation appears to work, there are boundary conditions
under which it produces a result which differs from the one obtained
by checksum computation from scratch. This is due to the way zero is
handled in one's complement arithmetic.
In one's complement, there are two representations of zero: the all
zero and the all one bit values, often referred to as +0 and -0.
One's complement addition of non-zero inputs can produce -0 as a
result, but never +0. Since there is guaranteed to be at least one
Rijsinghani [Page 2]
RFC 1624 Incremental Internet Checksum May 1994
non-zero field in the IP header, and the checksum field in the
protocol header is the complement of the sum, the checksum field can
never contain ~(+0), which is -0 (0xFFFF). It can, however, contain
~(-0), which is +0 (0x0000).
RFC 1141 yields an updated header checksum of -0 when it should be
+0. This is because it assumed that one's complement has a
distributive property, which does not hold when the result is 0 (see
derivation of [Eqn. 2]).
The problem is avoided by not assuming this property. The correct
equation is given below:
HC' = ~(C + (-m) + m') -- [Eqn. 3]
= ~(~HC + ~m + m')
只要把代码简单修改一下就可以了,或者遇到 0xFFFF 时设为 0,这时候就解决了这个问题。
但是,仔细研究了一下发现,UDP Checksum 又是这么定义的(RFC 768):
If the computed checksum is zero, it is transmitted as all ones (the
equivalent in one's complement arithmetic). An all zero transmitted
checksum value means that the transmitter generated no checksum (for
debugging or for higher level protocols that don't care).
刚好和 IP Checksum 相反,这就很有意思了。
最近用 ntpdate 的时候遇到了一些麻烦,时间同步总是遇到各种问题。后来搜了搜,发现了一个解决方案:htpdate,它通过 HTTP 头里的 Date 字段获取时间,虽然没有 ntp 那么精确,但是大多时候都够用。
用法见 htpdate(8) 。
这次遇到一个需求,要反代到不在内网的地址,为了保证安全,还是得上 HTTPS,所以尝试了一下怎么给 upstream 配置自签名 HTTPS 证书的验证。
upstream subpath {
server 4.3.2.1:4321;
}
server {
listen 443 ssl;
server_name test.example.com;
location /abc {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_trusted_certificate /path/to/self_signed_cert.crt;
proxy_ssl_name 1.2.3.4; // to override server name checking
proxy_ssl_verify on;
proxy_ssl_depth 2;
proxy_ssl_reuse on;
proxy_pass https://subpath;
}
}
可以用 openssl
获得自签名的 cert :
echo | openssl s_client -showcerts -connect 4.3.2.1:4321 2>/dev/null | \
openssl x509 -text > /path/to/self_signed_cert.crt
ref: https://stackoverflow.com/questions/7885785/using-openssl-to-get-the-certificate-from-a-server
月初的时候,有了一个完整可用的路由器(上一篇系列博文),但当时测了一下速度,只有几十 Mb/s,只要往上提就会失效,得 reset 才能继续。当时也先没管性能的事情,先把和 OS 交互的部分做了。现在又回头来做性能调优。
之前,逻辑部分的主频只有 10 MHz,这自然不行,不提高肯定做不到千兆。于是试着把主频拉高,FIFO 加大,然后遇到了很多问题,慢慢修复了,学到了很多新知识,目前也接近千兆的水平了吧,贴图:
TCP 测速:
UDP 测速:
测试环境是 macOS 虚拟机外打虚拟机内,走网桥把虚拟机和一个 USB 网卡接起来,然后从另一个 USB 网卡打到路由器。
接下来讲讲,在这个过程中遇到了什么问题,怎么解决的。第一个是速度过快就会挂,这肯定是丢包逻辑没写对,后来在仿真里开够了时间,于是就找到了一个 BUG,其实就是一行的修复。接着就是提高主频,但大家也知道,CPU 不能随便超频,由于各种延迟的原因,比如 Setup 时间,如果超了一个时钟周期的时间,本来应该下个周期就得到新数据的,结果到了下下周期才有,那有的状态可能就乱了,我目前遇到的也主要就是这个问题。
于是就对着 Timing 里汇报的各种问题修啊修,发现了很多以前没有注意到的问题,它们不影响功能,但是会让逻辑变慢。第一个问题是 High Fanout,以上就是说一个输出接到了很多输入,这看起来没啥问题,但数设课上也讲过,每个门的输入输出电流是有限制的,例如按书上的数据,一个门输出只能带十个门,更多只能级联一层。级联的话,延迟自然就高了。后来发现,这里的原因是,开了一个大的数组,但是没有变成 RAM,综合出了几千个逻辑单元,自然是出问题。解决方法很简单,用 xpm_memory_tpdram 即可。这样一搞,主频就能上 200MHz 了。
这个时候测了一下,发现 UDP 能打到 700Mb/s 了,TCP 由于丢包率比较高,只有 400Mb/s,距离预期还有一段距离。于是继续进行优化。
要继续提速,自然要提高主频。下一个主频目标就是 250MHz。随着提高主频,时序的要求也会更高,自然也出现了新的问题。
这次的问题主要在于,一个路径上逻辑门数过多,多的有 7 到 10 个,每一步零点几到一点几纳秒,叠起来 4 纳秒哪里够用。于是把一些不需要依赖条件的逻辑挪到条件外面,这样就减少了一些路径的依赖。
解决了这个以后,现在的 WNS(Worst Negative Slack)只剩下 0.6 ns 了。这时候的问题一部分还是来自于逻辑门过多,但这个时候就没这么简单了,只能继续细化流水线,打一拍,这样才能把延迟降下来。
虽然 Timing 没有完全解决,但还是写进了 FPGA 中。幸好工作一切正常,就得到了上面那个图片的结果,接近千兆的速度了。
后续优化途径的话,一方面是继续对逻辑进行细化和拆分,另一方面是对架构进行优化。当初设计的时候,可能没有考虑地那么周全,例如现在发现,其实 ARP Cache 可以放到每个网口一份,这样会降低花在仲裁上的时间。另外,路由表其实也可以分多份存,只要改的时候同一改就可以了。这样应该会更快。
最近在给 rCore 添加 Rocket Chip 支持。下面讲讲最近做了哪些工作,遇到了哪些坑,都是怎么解决的。
首先分析了一下已有的代码和工作方式,这个 Rocket Chip(ucb-bar/fpga-zynq)的设计大概是这样的:在 PS 上通过 fesvr 向 Rocket Chip 写入程序。Rocket Chip 本身暴露出一个 TSI,一个串口的调试接口,通过 Zynq Adapter 挂到了 PS 下的 AXI 总线,暴露出若干个寄存器,大概如下:
/**
* Address Map
* 0x00 - serial out FIFO data
* 0x04 - serial out FIFO data available (words)
* 0x08 - serial in FIFO data
* 0x0C - serial in FIFO space available (words)
* 0x10 - system reset
* 0x20 - req FIFO data
* 0x24 - req FIFO data available (words)
* 0x28 - data FIFO data
* 0x2C - data FIFO data available (words)
* 0x30 - resp FIFO data
* 0x34 - resp FIFO space available (words)
* 0x38 - nsectors
* 0x3C - max request length
*/
前面的是调试接口,后面的是 block device 和 network,我们暂时还没有用到这些 UCB BAR 做的私货。在 Vivado 中,地址 Offset 是 0x43C00000,所以代码中就这样访问对应的物理地址:
#define ZYNQ_BASE_PADDR 0x43C00000L
fd = open("/dev/mem", O_RDWR|O_SYNC);
assert(fd != -1);
dev = (uint8_t *) mmap(
0, sysconf(_SC_PAGESIZE),
PROT_READ|PROT_WRITE, MAP_SHARED, fd, ZYNQ_BASE_PADDR);
assert(dev != MAP_FAILED);
这块地址在 Device Tree 里也有对应的项,于是 PS 在访问的时候就会找到总线上的 Rocket Chip 的 Slave,也就对应到了上面的那个寄存器的 Map。接着就是由 fesvr 向 Rocket Chip 里写程序,然后跑起来了。
接着就需要先把输入输出做起来,需要移植一个 bootloader。相比之下,OpenSBI 明显比 bbl 更适合于这个用途,于是拿着 OpenSBI 就拿来改了。考虑到 fesvr 采用的是 htif 进行输入输出,于是从 bbl 里抄了相关代码过来,得到了正确的输出:
接下来,就想着怎么把 rCore 丢进去跑。把 payload 替换掉,丢进去,gg 了。一直出现一个玄学的问题,只要我修改页表,就出现 instruction access fault。也一直没有找到真实的原因,最后把 PMP 保护关了就好了。。怀疑是 Rocket Chip 实现有误。
又做了一些小的修改和适配,用户态也可以正常跑起来了。但是,现在串口只能轮询读取,而 htif 是通过读写内存进行的,也没有类似 MSI 的机制(现在想了想,其实可以,给 PS 挂一个 AXI Interrupt Controller,采用软件产生中断模式,然后接到 Rocket Chip 上,其实也是可以的),另外 Rocket Chip 原来的 Config 还没有向外暴露中断,我想挂一个串口也得让 Rocket Chip 访问得到。于是就开始了阅读 Chisel 代码然后魔改的过程了。
其实算不上魔改,克服了对 Rocket Chip 的恐惧,仔细阅读代码以后,发现还是比较容易理解的。譬如,我要添加一个外部总线,只需要添加一句:
那么对应地,就多了一片地址映射:
意味着,只要我在 Rocket Chip 里访问这片地址,那么就会访问这个 AXI 总线上的外设了。但是事情没有这么简单,在踩了很久的坑以后才最终解决。。
首先谈谈 AXI 总线上地址是怎么计算的。AXI 总线是一个星形结构,一个 Master 多个 Slave,在这里出现的例子是:
之前省略没说的是上面的第二点,就是让 Rocket Chip 也可以拿到内存用。那问题来了,怎么让 Rocket Chip 和 ARM 上的 Linux 不要打架?把地址空间分成两块就好了:
wire [31:0] mem_araddr;
wire [31:0] mem_awaddr;
// Memory given to Rocket is the upper 256 MB of the 512 MB DRAM
assign S_AXI_araddr = {4'd1, mem_araddr[27:0]};
assign S_AXI_awaddr = {4'd1, mem_awaddr[27:0]};
这样做,就透明地把 Rocket Chip 看到的内存空间都映射到了实际内存的 [256MB, 512MB) 这片空间上了。
注:我实验用的板子实际上有 1GB 的 DRAM,但实际上已经足够用了,所以就没有改原来 zedboard 的配置。
我在前面也提到,在 PS 上访问 0x43C00000 就是对应了 Zynq Adapter 的 0x0 地址。这里也是,在 Rocket Chip 上访问 0x0 的地址,我强行改成了 0x10000000,然后 Offset 是 0,所以最后到内存就是 0x10000000 的地址了。
所以 AXI 总线上 Slave Register 的地址 = Master 地址 - 匹配到的 Slave 的 Offset 地址。但是,如果只有单个 Slave 的时候,AXI Interconnect 可能不检查地址范围,而是直接截断,但在有多个 Slave 的时候,会先检查地址范围,如果找不到就返回错误。这个问题让我困惑了许久,直到我挂上了 System ILA
看。。
既然有了外设总线,第一个想到的外设就是 UART 咯,于是加了一个 AXI Uartlite,用 AXI Interconnect 连到外设总线上。写了程序简单测试了一下,确实读取到了数据,然后也很快就可以成功地从串口读数据,写数据。于是我又加了一个串口,拉到 AXI Interconnect 第二个接口上,结果就不工作了。
细心的读者可能已经从我上面讲到的一些内容上猜到了问题所在。。但这一点直到后面我才明白发生了什么,后面会再讲到这里的问题。
接着,回到我们最初的目标,既然可以输入输出了,还要真的串口干啥?添加中断或者拉一路 JTAG 出来,脱离 PS 来进行调试。所以接下来需要从 Rocket Chip 中暴露外部中断。在原来的代码中,是直接关掉了的:
它的功能很简单,直接把中断置 0。但我要的是拉到外面,经过了一番挣扎:
val io = IO(new Bundle {
val ps_axi_slave = Flipped(adapter.axi.cloneType)
val mem_axi = target.mem_axi4.head.cloneType
val mmio_axi = target.mmio_axi4.head.cloneType
val interrupts = Input(UInt(2.W))
})
io.mem_axi <> target.mem_axi4.head
io.mmio_axi <> target.mmio_axi4.head
target.interrupts := io.interrupts
终于找到了正确的方法,拉出来两路中断信号。于是很开心地直接接到了 Uartlite 上,但是事情肯定不会这么简单:
Uartlite 是正边沿触发的中断,但是 Rocket Chip 期望的是高电平触发的中断。所以我们需要一个中断控制器来实现这个,加了一个 AXI Interrupt Controller。但是问题来了,我们还没解决那个只要加俩 Slave 就都访问不到的问题呢!于是到处挂 System ILA 调试,发现了问题所在:
我们之前提到,0x60000000 - 0x80000000 地址段是对应到了外部 AXI 总线的,这没错,但是,如果在 Rocket Chip 上读 0x60000000 的地址,到总线上还是 0x60000000 的地址而不是 0x0,而之前只有一个 Slave 的时候,没有真的去检查这个地址范围,所以找到了正确的寄存器,现在不行了,于是出错了。
解决方法也很简单,乖乖地把 Offset 调成 0x60000000 和 0x61200000,然后再映射到 rCore 的内核态里。一番挣扎,又参照了 Linux 的 Uartlite 驱动(发现了遗忘设置的 MER 寄存器,不认真阅读文档的后果),终于在 ILA 上看到 uartlite 的输出上有一个脉冲,紧接着看到中断控制器输出了一个 irq,然后 Rocket Chip 收到了 external interrupt。Cheers!
事情虽然差不多解决了,但是还没有结束。收到了一个 interrupt 以后,打印了一下寄存器状态,马上又来下一个 interrupt 了。现在不仅要在 Rocket Chip 的 PLIC 上把中断给确认了(吐槽一下,Rocket 的 PLIC 文档也比较欠缺,我是照着 U54 核的地址找到,但是 U54 呢是一个 4+1 的组合,hart0 没有 M 态,所以我为了打开 hart0 的 S 态中断,要找 hart1 的 M 态地址,详见 SiFive 文档),在 AXI Interrupt Controller 上也得把中断给确认了。但还是不行,每次 interrupt controller 输出的 irq 拉低不久,uartlite 又出现了新的一次中断。
这次的问题很有戏剧性:处理中断的时候输出调试信息,调试信息把 tx fifo 写满了,uartlite 也会产生一个中断。。于是没完了。解决方法很简单,牺牲一些性能,每次输出的时候都等到 tx fifo 空了才写,然后在处理串口中断的时候不要输出调试信息。
这下没有新的问题了,串口中断终于是工作了。
这么一番搞下来,对 Vivado 和 AXI 的相关知识都比较熟悉了吧,也踩了很多的坑。需要特别感谢 @cq_z4yx 提供的技术支持。
相关文档和链接:
Jekyll 生成一次实在太慢,忍受不了,于是换成了 Hugo。为了保持链接不变,花了不少的时间在重命名上,不过目前似乎都完成了,希望没有导致 404 的问题。
新的主题还是好看很多的。Enjoy!
最近在做一个作业的时候,发现里面有个步骤是获取 Linux 系统中的 DNS 服务器列表,它的方法很粗暴,直接 cat grep cut 再处理。我就在想有没有完全代码的实现,然后搜了一下,果然有:
用到了全局变量 _res
,虽然很 hacky,但是至少是工作的,不清楚兼容性几何。
来了来了
最近在做 FPGA 上硬件的路由器,感觉接近一个基本可用的阶段了吧,大概谈一谈做这个的思路、过程和踩过的坑。
首先,做实验用的板子是 Alinx AX7021,FPGA 是 Xilinx xc7z020clg484-2,扩展板上有 4PL+1PS 个网口和千兆 KSZ9031RNX PHY,采用的接口是 RGMII。一开始做的自然是做 RGMII,但是遇到了困难,RGMII 在千兆模式下传输的是 DDR 信号,而时序和延迟就是个比较麻烦的事情。一开始先直接拿 Xilinx 的 AXI Ethernet IP 来用,然后上 ILA 看到了 IDDR 后的信号,第一次看到了完整的以太网帧,从 Preamble 和 SFD 到最后的 FCS。于是就特别振奋,想着手写 RGMII,先做收,再做发。确实,收很容易,很快就做出来了,但是写总是出问题,当时也不懂跨时钟域的一些问题,总之各种没调出来。于是就退而求其次,选择了 Xilinx 的 Tri Mode Ethernet IP 了。
Tri Mode Ethernet IP 有很多选项,为了简单,直接采用了 AXI-Stream 的接口,不要 AXI4-Lite 什么的,都不要,因为我需要直接写剩余的逻辑。其他东西能省也都省掉了。这个 IP 确实很给力,很快就可以完成收和发的操作了,这次终于知道了怎么处理跨时钟域的问题 — XPM FIFO ASYNC,一下推进了很大的进度。
既然可以收,也可以发了,就扩展到多个网口。这个 IP 中可以选择 Shared Logic 在内部,也可以在外部,研究了一下发现,应该是一个放内部,其余选外部,然后接起来就可以了。不过目前为了简单,还是只用了俩端口。在这个基础上,就开始解析收进来的以太网帧了。
第一步自然是填 ARP 表,自然问题来了,如果多个网口同时进来数据,怎么保证 ARP 表读写的正确性?自然就想到总线上需要做仲裁,于是写了一个简单的总线仲裁,顺带学习到了 unique case(z)
和 priority case(z)
的语法。然后 ARP 表怎么实现呢,大概就是一个哈希表,然后表里维护了(IP,MAC,PORT)三元组,然后实现了一些冲突和覆盖的处理逻辑,做这些的同时也对各个模块编写相应的测试。有了 ARP 表,就可以在解析以太网帧的时候,拆解出里面的信息,然后请求 ARP 表总线,然后写入。
第二步则是相应 ARP 请求,这就需要发出以太网帧。由于 4 个端口都可能向 4 个端口发出以太网帧,这就需要一个 4x4 matrix + 仲裁。不过目前为了简单,就还没有上 FIFO,直接仲裁进到目的端口的 TX FIFO 中了。这一步并不难,不过在最后 AXI-Stream 的一步遇到了一些困难。由于 Tri Mode Ethernet IP 对 tready 和 tvalid 有特定的要求,所以这里只能用 FWFT FIFO 进行,然后进行了一波神奇的操作,最后搞定了这个事情。成果就是可以从电脑上 arping
通指定的地址了。
第三步,也是正在做的一步,就是真正实现 IP 包的转发,这需要三个步骤:解析目的地址,查询路由表,查询 ARP 表。于是需要照着 ARP 表的方案同样做了路由表的仲裁,目前为了简单也还是把路由表设置为静态的。这里就需要做一些特殊的考虑,例如上面三步是串行的,但是我需要同时把 IP 包存一份,最后转发的时候修改一点就发出去了,所以需要等两步都做完,才能继续下一个包的处理。目前做到了第二小步,正在向最后一步查询 ARP 表进发。
UPDATE:现在最后一步也做好了,但是遇到了小问题,还是不能偷懒,需要写一个 XPM_MEMORY_SPRAM,直接写一个大的数组太浪费 LUT 了。
UPDATE-2019-04-27:It WORKS now. 不过也发现了之前写的 ARP 表有点问题。