跳转至

hardware

PCB 笔记

记录一下在学习画板子过程中学到的心得。

工具

目前使用过 KiCadlceda

  • KiCad: 开源软件,跨平台。
  • lceda:在线编辑,不需要安装,和 lcsc 有深度集成。

项目 jiegec/HT42B534USB2UART 采用的是 KiCad 5 编写的。目前正在做的另一个项目采用 lceda

流程

  1. 选择所需要使用的芯片,查找芯片的 datasheet。
  2. 寻找采用了芯片的一些设计,特别是看 schematic。
  3. 按照 datasheet 里面推荐的电路,或者是其他人的设计,画自己需要的 schematic。
  4. 设置好各个元件的 footprint,然后转到 PCB 设计。
  5. 在 PCB 里面布线,生成 Gerber 等文件。
  6. 把 Gerber 给到生产商(比如 jlc),交付生产。
  7. 如果是自己焊接,则需要购买元件,比如从 lcsc 购买。
  8. 收到 PCB 和元件后,自己按照 BOM 和 schematic 焊接各个元件。

笔记

  1. 对于一些连接很多元件的信号,比如 GND,可以留作铺铜解决。也就是说,先不管 GND,把其他所有的信号都接好以后,再在顶层铺铜;如果还是有没有连接上的 GND,可以通过过孔(Via)走到底层,在底层再铺一层铜。
  2. 对于外部供电的 VCC 和 GND,在 KiCad 中需要用 PWR_FLAG 标记一下。
  3. 在 KiCad 中设计 PCB 前,要把生产商的工艺参数设置好,不然画了也要重画。
  4. lceda 在选择元件的时候,可以直接从 lcsc 里选择,这样可以保证封装和商品可以对得上,不需要手动进行匹配。
  5. 如果要用 jlc 的 SMT 贴片,先在 SMT 元件列表 里搜索所需要的元件;推荐用基本库,如果用其他库,则要加钱;选好元件以后,用元件编号去 lceda 里搜索并添加到 schematic。
  6. 对于涉及模拟信号的设计,比如音频,需要特别注意模拟信号的电和地都是单独的:AVCCAGND。所以要特别注意 datasheet 里面不同的地的表示方法。最后,再用磁珠把 VCCAVCCGNDAGND 分别连接起来就可以了。可以参考 DE2 板子中第 19 页的音频部分设计Staying well grounded
  7. 在 schematic 里经常会出现在电源附近的电容,那么,在 PCB 中,也尽量把这些电容放在对应的电源的旁边。
  8. 耳机插座里面,一般分三种组成部件:Tip,Ring,Sleeve。只有两段的是 TS,三段的是 TRS,四段的是 TRRS。TS 是单声道,T 是声音,S 是地。TRS 是双声道,T 是左声道(或者单声道),R 是右声道,S 是地。TRRS 则是双声道加录音。一般来说,LINE IN 是双声道,MIC IN 是单声道,它们的阻抗也不同;LINE OUT 和 HEADPHONE OUT 都是双声道,但 HEADPHONE OUT 经过了额外的放大器。
  9. 遇到一个 SPI 协议没有 SPI_MISO 引脚的芯片,可能说明它是 write-only 的。
  10. 手焊的基本元件,一般用 0603 加一些 Padding 的封装;SMT 的话,则建议用 0402 封装。
  11. I2C 的信号线一般需要加一个几 K 欧姆的上拉电阻到 VCC。

JLC SMT 的基础库不需要换料费,如何寻找基础库中的元件:

  1. 电阻品牌是 UNI-ROYAL,型号命名规则是:
    1. 封装:0603/0402
    2. 功率:WA/WG/W8
    3. 误差:F(1%)
    4. 阻值:三位整数 + 一位 exp(J 表示 -1,K 表示 -2,L 表示 -3),例如 2002 表示 200*10^2=20k,1003 表示 100*10^3=100k,3300 表示 330*10^0=330,330J 表示 330*10^-1=33,330K 表示 330*10^-2=3.3 例子:要找 0402 封装的 10k 欧电阻,搜索 0402WGF1003;要找 0603 封装的 33 欧电阻,搜索 0603WAF330。
  2. 电容品牌有风华/三星/国巨,三星的电容型号命名规则是:
    1. 封装:05(0402)/10(0603)
    2. 字母:A/B/C
    3. 电容:两位整数 + 一位 exp,单位是 pF,例如 105 表示 10*10^5pF=10^6pF=1uF,104 表示 10*10^4pF=10^5pF=0.1uF 例子:要找 0402 封装的 100nF 电容,搜索 CL05B104;要找 0603 封装的 1uF 电容,搜索 CL10A105。也可以只搜电容的数字部分,可以找到更多品牌。

阻抗匹配

在传输线上,如果出现阻抗变化,就会导致信号出现反射,质量变差。因此,需要保证传输线的两端和传输线整个过程的阻抗一致。

阻抗设置为多少,一般要看协议的规定。确定好协议定义的阻抗以后,需要查看信号两端的芯片内部的阻抗,如果和协议不一致,需要额外添加电阻,并且电阻要尽量放在接近芯片的位置上。由于传输线在 PCB 上,所以和 PCB 厂商的工艺有关,需要去厂商的阻抗计算器上进行计算,例如 jlc 阻抗计算器。涉及到的参数有:

  1. 板子层数:PCB 层数,最简单的正反面就是 2 层
  2. 成品厚度:整个 PCB 加起来的厚度,例如 1.6mm
  3. 内层铜厚:夹在内部的 PCB 的铜的厚度,例如 0.5 oz,就是 1.37/2=0.685 mil
  4. 外层铜厚:PCB 上下暴露在外面的两层的铜的厚度,常见 1 oz=1.37 mil
  5. 需求阻抗:协议所要求的阻抗,例如单端 50 欧姆(SDIO),差分 90 欧姆(USB)
  6. 阻抗模式:传输线的连接方式,见下(图源 KiCad)
    1. 单端阻抗(Microstrip Line):一根线传输信号,地线在另一个平面,图中上面的长方形就是传输线,底部就是地平面
    2. 差分阻抗(Coupled Microstrip Line):差分线传输信号,地线在另一个平面,图中上方两个长方形就是差分传输线,底部是地平面
    3. 共面单端:一根线传输信号,周围就是地平面
    4. 共面差分:差分线传输信号,周围就是地平面
  7. 阻抗层:传输线所在的层
  8. 参考层:地线所在的层

由于双层 PCB 的两层铜之间距离比较远(例如 57.68 mil),如果采用单端阻抗,那么需要比较大的线宽,例如用 jlc 阻抗计算器,50 欧姆阻抗需要 106.68 mil 的线宽。如果采用四层 PCB,最上面两层之间距离缩小了很多(例如 7.99 mil),此时即使用单端阻抗,用 jlc 计算得出只需要 13.2 mil 的线宽。所以双层 PCB 更适合使用共面单端的方式,此时传输线和地线放在了同一个平面,距离比较小,就不需要那么大的线宽。

这里的单位:1 mil = 0.0254 mm,1 inch = 1000 mil = 0.0254 m,1 oz = 1.37 mil = 0.0348 mm

Skid Buffer

Skid buffer

Skid buffer 指的就是,对于 valid + ready 的握手信号,用空间(更多的逻辑)来换取时间(更好的时序)的一个硬件模块。

简单来说,背景就是,为了解决 valid 和 ready 信号在数据流水线上一路经过组合逻辑导致的时序问题,在中途加上一些寄存器来阻隔。当然了,代价就是延迟和面积,不过吞吐量还是需要保持的。

由于需求的不同,Skid buffer 也有不同的实现。目前,找到了四个实现,实现上有所不同,特性也不大一样。

统一约定

由于我在 SpinalHDL 语言中重新实现了下面的这些 Skid buffer,所以按照 SpinalHDL 的 Stream 定义接口:

class SkidBufferCommon[T <: Data](
    gen: => T
) extends Component {
  val io = new Bundle {
    val s = slave(Stream(gen))
    val m = master(Stream(gen))
  }
}

在这里,io.s 表示从上游取的数据,io.m 表示传递给下游的数据。

输出信号共有:io.s.readyio.m.validio.m.payload

ZipCPU 版本

第一个版本来自 ZipCPU:

博客地址:Building a Skid Buffer for AXI processing 代码地址:skidbuffer.v

它有两个参数,一个表示是否有额外的输出寄存器(outputReg),一个表示是否低功耗(lowPower)。

FPGACPU 版本

第二个版本来自 FPGACPU:

文章地址:Pipeline Skid Buffer

SpinalHDL S2M 版本

第三个版本来自 SpinalHDL Library 的 s2mPipe:

代码地址:Stream.scala L348

SpinalHDL M2S 版本

第四个版本来自 SpinalHDL Library 的 m2sPipe:

代码地址:Stream.scala L327

四个版本的对比

在研究了代码以后,可以看到这四个版本的区别:

版本 ZipCPU w/ outputReg ZipCPU w/o outputReg FPGACPU S2M M2S
io.s.ready Reg Reg Reg Reg Comb
io.m.valid Reg Comb Reg Comb Reg
io.m.payload Reg Comb Reg Comb Reg
latency 1 0 1 0 1
buffer 数量 1 1 2 1 1

注:

  1. Reg 表示从寄存器输出,Comb 表示从组合逻辑输出
  2. Latency 表示从 io.s.fireio.m.fire 的延迟
  3. Buffer 表示缓冲的 payload 个数
  4. ZipCPU w/o outputReg 和 S2M 实现的逻辑是一样的

形式化验证

为了确认上面这些类型的 Skid Buffer 都可以正常工作,按照 ZipCPU Skid Buffer 的文章,也照着写了几个 property:

1: 在 valid && ~ready 的时候,valid 需要继续保持为高,并且 payload 不变:

// When valid goes high, data is stable and valid stays high before ready
when(past(stream.valid && ~stream.ready && ~outerReset)) {
    slaveAssume(stream.valid);
    if (dataStable) {
        slaveAssume(stable(stream.payload.asBits));
    }
}

2: 在 reset 释放的第一个周期里,valid 不能为高:

参考 AXI 标准 (IHI0022E Page 38 A3.1.2) 原文:

The earliest point after reset that a master is permitted to begin driving ARVALID, AWVALID, or WVALID HIGH is at a rising ACLK edge after ARESETn is HIGH.
// Valid is low in the first cycle after reset falls
when(pastValid && past(outerReset) && ~outerReset) {
    slaveAssume(~stream.valid);
}

3: 添加 cover property,要求 io.sio.m 可以连续若干个周期 valid && ready,保证吞吐率:

cover(
    pastValid && genPast(pastValid, null, cycles) && genPast(
        ~outerReset,
        null,
        cycles
    ) && genPast(stream.fire, payload, cycles)
)

采用 yosys-smtbmc 工具验证了以上四种 Skid buffer 都满足这些属性。

以太网的物理接口

本文的内容已经整合到知识库中。

背景

最近逐渐接触到了一些高速的以太网的接口,被一大堆的名字搞得有点懵,所以特意学习了一下并整理成这篇博客。

更新:经 @z4yx 指出,还可以看华为的介绍文档

几几 BASE 杠什么是什么意思

在下文里,经常可以看到类似 100BASE-TX 这种写法,它表示的意思是:

  1. BASE 前面的数字表示速率,比如 10,100,1000,10G 等等
  2. BASE 之后的第一个字母,常见的 T 表示双绞线,S 表示 850nm 光纤,L 表示 1310nm 光纤,C 表示同轴电缆
  3. 之后可能还有别的字母,比如 X 表示 8b/10b 或者 4b/5b(FE)的编码,R 表示 64b/66b 的编码
  4. 之后可能还有别的数字,如果是 LAN PHY 表示的是所使用的 lane 数量;如果是 WAN PHY 表示的是传输的公里数

详见 Wikipedia - Ethernet Physical Layer # Naming Conventions 和 IEEE 802.3 1.2.3 节 Physical Layer and media notation:

The data rate, if only a number, is in Mb/s, and if suffixed by a “G”, is in
Gb/s. The modulation type (e.g., BASE) indicates how encoded data is
transmitted on the medium. The additional distinction may identify
characteristics of transmission or medium and, in some cases, the type of PCS
encoding used (examples of additional distinctions are “T” for twisted pair,
“B” for bidirectional optics, and “X” for a block PCS coding used for that
speed of operation). Expansions for defined Physical Layer types are included
in 1.4.

和 IEEE 802.3 1.4 节 Definitions 中的几个例子:

  • 100BASE-T: IEEE 802.3 Physical Layer specification for a 100 Mb/s CSMA/CD local area network. (See IEEE Std 802.3, Clause 22 and Clause 28.)
  • 100BASE-TX: IEEE 802.3 Physical Layer specification for a 100 Mb/s CSMA/CD local area network over two pairs of Category 5 twisted-pair cabling. (See IEEE Std 802.3, Clause 24 and Clause 25.)
  • 1000BASE-T: IEEE 802.3 Physical Layer specification for a 1000 Mb/s CSMA/CD LAN using four pairs of Category 5 balanced copper cabling. (See IEEE Std 802.3, Clause 40.)
  • 1000BASE-X: IEEE 802.3 Physical Layer specification for a 1000 Mb/s CSMA/CD LAN that uses a Physical Layer derived from ANSI X3.230-1994 (FC-PH) [B21]23. (See IEEE Std 802.3, Clause 36.)
  • 2.5GBASE-T: IEEE 802.3 Physical Layer specification for a 2.5 Gb/s LAN using four pairs of Category 5e/Class D balanced copper cabling. (See IEEE Std 802.3, Clause 126.)
  • 5GBASE-T: IEEE 802.3 Physical Layer specification for a 5 Gb/s LAN using four pairs of Category 5e/Class D balanced copper cabling. (See IEEE Std 802.3, Clause 126.)
  • 10GBASE-T: IEEE 802.3 Physical Layer specification for a 10 Gb/s LAN using four pairs of Class E or Class F balanced copper cabling. (See IEEE Std 802.3, Clause 55.)

各个速率对应的英文单词是什么

  • Fast Ethernet: 100Mbps
  • Gigabit Ethernet: 1Gbps
  • Multi Gigabit Ethernet: 2.5Gbps
  • Ten Gigabit Ethernet: 10Gbps
  • Forty Gigabit Ethernet: 40Gbps
  • Hundred Gigabit Ethernet: 100Gbps

常见的连接器

连接器(connector)一般来说指的就是线缆和网络设备之间的物理接口了。常见的有:

  • 8P8C:一般我们会称之为 RJ45,关于它们俩的关系,可以看 Wikipedia 上面的说明,不过在日常生活中,这两个混用其实也没有什么大问题
  • LC:一种光纤的接口,有两个突出来的插到 SFP 光模块中的突起,比较常见
  • SFP+ DAC:一般是 DAC(Direct Attatched Cable)线,线的两端直接就是 SFP+ 的接口,直接插到 SFP+ 笼子中,不需要光模块;更高速率的也有 DAC 线

对于光纤的接口,注意购买的时候要和光模块对应,不然可能插不进去。常见的有 LC-LC,SC-LC,SC-SC 等等,表示线的两端分别是什么接口。

MDI 和 MDI-X

这其实就是大家常见的 RJ45 里面 8 根线对应的信号,在十兆和百兆的时候,需要区分 MDI 和 MDI-X,在同种类型的端口之间用交叉线,在不同类型的端口之间用直通线。在后来,有了 Auto MDI-X,也就是会按照实际情况自动检测并且匹配。从千兆开始,设备都支持 Auto MDI-X 了,所以线本身是交叉还是直通就无所谓了。

各种 SFP

SFP 是很常见的,特别是在高速的网络之中。而它又分为几种,对应不同的速率:

  • SFP: 1Gbps/100Mbps
  • SFP+: 10Gbps
  • SFP28: 25Gbps
  • SFP56: 50Gbps
  • QSFP: 4Gbps
  • QSFP+: 40Gbps
  • QSFP28: 100Gbps/50Gbps
  • QSFP56: 200Gbps
  • QSFP-DD: 400Gbps/200Gbps
  • QSFP-DD112: 800Gbps
  • OSFP: 800Gbps/400Gbps

可以看到,名字前面加了个 Q(Quad),速率就翻了 4 倍,因为有 4 个 lane,同时物理接口的尺寸也变大了。所以,不带 Q 的 SFP 的物理尺寸都一样,带 Q 的 SFP 物理尺寸都一样大,但后者比前者大一些(SFP 是 113.9 mm^2,QSFP 是 156 mm^2)。OSFP 又比 QSFP 更大一些,O 表示 Octal,就是 8 个 lane 的意思。

可以在 400G QSFP Transceiver Types and Fiber Connections400G OSFP Transceiver Types Overview 看到 QSFP-DD 和 OSFP 的对比。

通常,网络设备也会支持把一个 QSFP 接口拆成多个 SFP 接口来使用,比如有的线,一边是 QSFP28,另一边是 4xSFP28,只要设备支持即可,目的是节省空间。

SFP 标准 SFF INF-8074 规定了 20 根信号线,正反面各 10 根,重要的是下面的这些(括号里写得是 Pin 的编号):

  1. Mod_ABS(6):模块是否插入
  2. RD+(13)、RD-(12):接收数据的差分对
  3. TD+(18)、TD-(19):传输数据的差分对
  4. SDA(4)、SCL(5):模块的 I2C
  5. Tx_Fault(2)、Tx_Disable(3)、Rx_LOS(8):一些状态信号

可以看到,收和发各有一个差分对共 4 条数据线。相对应的,QSFP 收和发各有四对差分对共 16 条数据线,一共 38 根线。并且有一些信号是复用了同样的 pin,这样的设计可以节省一些 pin,是很常见的。

MII

有时候,还会遇到各种 MII 接口,也就是 MAC 和 PHY 之间的接口。有时候,还会伴随着 MDIO 接口,来进行控制信息的传输。它又分不同的类型:

  • Standard MII:速率是 100Mbps(25MHz*4)或者 10Mbps(2.5Mhz*4),TX 7 根线(4 DATA+CLK+EN+ER),RX 7+2 根线(4 DATA+CLK+DV+ER+CRS+COL),加上 MDIO 2 根线共 18 根线
  • RMII:速率是 100Mbps 或者 10Mbps,频率都是 50MHz,一共 10 根线(4 DATA+CLK+TX_EN+CRS_DV+RX_ER+MDIO+MDC),数据线是 TX 和 RX 各 2 根
  • GMII:速率是 1000Mbps(125MHz*8),数据线是 TX 和 RX 各 8 根;也支持速率 100Mbps(25MHz)和 10Mbps(2.5MHz)
  • RGMII:速率是 1000Mbps(125MHz*4*2,DDR),数据线是 TX 和 RX 各 4 根;也支持速率 100Mbps(25MHz*4)和 10Mbps(2.5MHz*4),一共是 5+5+2 根线
  • SGMII:速率是 1000Mbps(625MHz*2*8/10),采用 625MHz DDR 差分对 SerDes,采用 8b/10b 的编码
  • XGMII:支持 2500Mbps/5000Mbps/10000Mbps(156.25 MHz*32*2,DDR)速率,数据线是 TX 和 RX 各 32 根

有的时候,MAC 和 PHY 是独立的,比如很多常见的 FPGA 开发板,在使用千兆网的时候,在板子上是 PHY 芯片,从 FPGA 到 PHY 通过 RGMII 连接,然后 PHY 再连接到 8P8C(RJ45)的连接器上。一般还会把 MDIO 也接到 FPGA 上面。如果有多个 PHY,就会吧 MDIO 通过总线的方式合并起来,给每个 PHY 配置不同的地址(一般是在指定的 PIN 上设置上拉/下拉电阻实现),就可以保证不冲突的访问。

扩展阅读:KXZ9031RNX Datasheet

SGMII

上面比较常见的是 GMII/RGMII/SGMII。其中比较特殊的是 SGMII,首先可以发现它信号很少,只有两对差分线 TX_P TX_N RX_P RX_N,其中时钟是可选的,因为可以从数据中恢复。你可能感到很奇怪,那么其他的信号,比如 DV/ER/CRS 等都去哪里了呢?其实是因为,SGMII 采用了 8b/10b 的编码的同时,把这些控制信号通过一定的方式顺便编码进去了。具体来说,就是从 8 位的数据信号编码为 10 位的时候,有一些特殊的 10 位符号是没有对应 8 位的数据的,因此可以用这些特殊符号来表示一些信号,比如用 SPD(Start_of_Packet Delimiter,对应 /S/)和 EPD(End_of_Packet Delimiter,对应 /T/R/ 等)表示传输数据的开始和结尾,对应 TX_EN/RX_DV 信号;用 Error_Propagation(/V/)表示错误,对应 RX_ER 信号等等。所以,SGMII 其实还是一个 GMII 的变种,只不过采用 SerDes 的方式减少了引脚,MAC 内部或者 PHY 内部也是经过一个 GMII-SGMII 的转换,而其余部分是一样的。

关于 8b/10b 的编码方式,可以阅读 IEEE 802.3 标准中的 Table 36–1a—Valid data code-groups,里面提到了两类的 Code Group:D 打头的,表示数据,有 256 种,从 8b 映射到 10b 的表达方式,并且为了保持直流平衡,有一种到两种表示方法。此外还有 12 个特殊的 Code Group:K 打头,它们的 10b 表达方式不会和数据冲突。表 Table 36–3—Defined ordered sets 中定义了 K 打头的 Code Group 含义:

  • /C/ Configuration:
  • /C1/ Configuration 1: /K28.5/D21.5/Config_Reg
  • /C2/ Configuration 2: /K28.5/D2.2/Config_Reg
  • /I/ IDLE:
  • /I1/ IDLE 1: /K28.5/D5.6/
  • /I2/ IDLE 2: /K28.5/D16.2/
  • Encapsulation:
  • /R/ Carrier_Extend: /K23.7/
  • /S/ Start_of_Packet: /K27.7/
  • /T/ End_of_Packet: /K29.7/
  • /V/ Error_Propagation: /K30.7/
  • /LI/ LPI (Low Power Idle):
  • /LI1/ LPI 1: /K28.5/D6.5/
  • /LI2/ LPI 2: /K28.5/D26.4/

IEEE 802.3 Figure 36-4 中给了一个例子,就是在发送一段数据的时候,首先是 /I/,然后 /S/,接着一系列的 /D/,最后结束的时候 /T/R/I/。

扩展阅读:

1000BASE-X 与 SFP 的关系

1000BASE-X 在 802.3 Clause 36 中定义,它的层级是这样的:

它支持三种不同的介质,对应了三个 PMD 层,也就是 LX、SX 和 CX。这些体现在设备上,其实就是不同的 SFP 模块。SFP 模块实际上就是图中的 PMD 层,SFP 接口上连接的是 1000BASE-X 的 PCS/PMA,这也就是为什么说在带有 SFP 的 FPGA 上,Xilinx 的 IP 叫做 1G/2.5G Ethernet PCS/PMA。在这里,PCS 和 PMA 层在 FPGA 内部通过 IP 实现,通过 PCB 连接到 SFP 上,光模块就是 PMD 层。见下图:

左边通过 GMII 连接到内部的 MAC,右边连接到 SFP 上,通过光模块,连接到光纤。这里光模块只需要负责光电转换。另一种比较常见的形式,就是 MAC 在 FPGA 内部,PHY(包括 PCS/PMA/PMD)都在 FPGA 外部,此时 FPGA IO 上就是各种 MII。

那么 SFP 电口模块是怎么工作的呢?我们知道,电口采用的是 1000BASE-T 标准。实际上,它里面有一个 PHY 芯片,发送的时候,首先解码 1000BASE-X 变回原始数据,再按照 1000BASE-T 的方式编码再发出去;接收的时候,按照 1000BASE-T 进行解码,再重新编码为 1000BASE-X 发送给 PMA 层。

还有一类电口模块,与上面不同的地方在于,SFP 上走的是 SGMII,而不是 1000BASE-X。这两种模式没有太大的区别,都是两对差分线,一收一发,所以很多时候二者是同时支持,可以切换的。例如 Cisco Compatible 10/100/1000BASE-T SFP SGMII Copper RJ-45 100m Industrial Transceiver Module (LOS) 就是在 SFP 上走 SGMII 协议。

推荐阅读 Designing a Copper SFP using the VSC8221 10/100/1000BASE-T PHY,它里面讲了如何将 VSC8221 芯片用于电口模块:VSC8221 芯片一头是 1000BASEX(又称 802.3z SerDes,802.3z 就是 1000BASE-X)或者 SGMII,另一头是 1000BASE-T MDI。

物理层

100BASE-TX

在 IEEE 802.3 的 Clause 24 和 25 中定义。

100BASE-TX 的物理层分为 PCS,PMA,PMD。与 MAC 的连接是 MII 接口,MII 频率是 25MHz,每周期传输 4 bit 的数据。然后 PCS 负责把 4 bit 的数据通过 4B/5B 转换为 5 bit 的 code group;PMA 使用 NRZI 进行编码;PMD 层借用了 FDDI 协议的 PMD 层,只使用 MDI 的 1-3 和 6 四根线传输,两对差分对,一收一发。

1000BASE-T

在 IEEE 802.3ab-1999 中定义,具体位置是 Clause 40。

物理层往上通过 GMII 连接 MAC,往下通过 MDI 连接其他网络设备。物理层又包括 PCS 和 PMA。

1000BASE-T 使用四对差分线,每对差分线上都是全双工传输,波特率 125Mbaud,symbol 的范围是 {2, 1, 0, -1, -2},通过 PAM5 传输。

具体来讲,PCS 从 MAC 的 GMII 接口接收要发送的数据,GMII 是 125MHz,每个周期 8 位数据。这些数据与 scrambler 一起,生成 9 位的 Sd_n[8:0],然后再编码为 (TA_n, TB_n, TC_n, TD_n),也就是在四对差分线上传输的 symbol,取值范围是 [-2, 2]。简单总结一下,就是每个周期 8 位数据,先变成 9 位数据,再变成 4 个 symbol,每个 symbol 取值范围是 -2 到 2,这就叫做 8B1Q4,converting GMII data (8B-8 bits) to four quinary symbols (Q4) that are transmitted during one clock (1Q4),把 8 位的数据转换为四个 symbol,每个 symbol 有五种取值(Quinary 表示 5)。

MDIO

MDIO 是 MAC 和 PHY 之间一个低速的通信接口,定义在 IEEE 802.3 Clause 45,可以用来配置一些寄存器。它支持读和写,多个 PHY 可以共享一个 MDIO 总线,通过 5 位的地址区分。为了让 PHY 分配到不同的地址,PHY 通常会通过某些引脚的上下拉来决定它自己的 MDIO 地址,这样可以避免冲突。以 RTL8201F 为例,它的 PHY 的 MDIO 地址配置与 LED 输出引脚是共享的,根据外部电路的上下拉不同,配置 MDIO 地址的最低两位:

也就是说,这款芯片的 MDIO 地址可以在二进制的 00000 到 00011 之间取。但不建议用 00000 地址,这是因为一些芯片会把 00000 重定义为广播,此时总线上的所有 PHY 芯片都要响应目标地址为自己的地址(非 00000)或者 00000 地址的请求(见 MDIO Addressing)。RTL8211 在它的文档里描述了这个行为:

这样的好处是可以同时往多个 PHY 芯片写入寄存器,但如果要从 00000 地址读寄存器的话,一旦多个 PHY 同时响应,MDIO 总线上就会出现冲突。不过如果只有一个 PHY 芯片连接到 MDIO 总线上,那么让 MAC 通过 00000 地址访问 PHY 也是可以的。

ARM M1 MacBook Air 开箱

购买

我是 11.12 的时候在 Apple Store 上下单的,选的是 MacBookAir,带 M1 芯片,8 核 CPU + 8 核 GPU,加了一些内存和硬盘。今天(11.19)的时候顺丰到货,比 Apple Store 上显示的预计到达时间 21-28 号要更早。另外,我也听朋友说现在一些线下的店也有货,也有朋友直接在京东上买到了 Mac mini,总之第一波 M1 的用户最近应该都可以拿到设备了。

现在这篇博客,就是在 ARM MBA 上编写的,使用的是 Intel 的 VSCode,毕竟 VSCode 的 ARM64 版不久后才正式发布。

开箱

从外观来看,一切都和 Intel MBA 一样,包装上也看不出区别,模具也是一样的。

进了系统才能看得出区别。预装的系统是 macOS Big Sur 11.0,之后手动更新到了目前最新的 11.0.1。

顺带 @FactorialN 同学提醒我在这里提一句:包装里有电源适配器,不太环保。

体验

ARM64

首先自然是传统艺能,证明一下确实是 Apple Silicon:

$ uname -a
Darwin macbookair.lan 20.1.0 Darwin Kernel Version 20.1.0: Sat Oct 31 00:07:10 PDT 2020; root:xnu-7195.50.7~2/RELEASE_ARM64_T8101 x86_64

啊对不起我用错了,上面是在 Rosetta 里面跑的 shell 看到的结果。实际是这样子的:

$ uname -a
Darwin macbookair.lan 20.1.0 Darwin Kernel Version 20.1.0: Sat Oct 31 00:07:10 PDT 2020; root:xnu-7195.50.7~2/RELEASE_ARM64_T8101 arm64

货真价实的 ARM64 内核,系统的很多 binary 也都是 Universal 的:

$ file /bin/bash
/bin/bash: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e]
/bin/bash (for architecture x86_64):    Mach-O 64-bit executable x86_64
/bin/bash (for architecture arm64e):    Mach-O 64-bit executable arm64e

Rosetta

接着,就是重头戏 Rosetta 了。第一次打开 Intel 的程序的时候,会弹出窗口安装 Rosetta,确定以后立马就装好了。接着常用的各种软件啥的,都没有什么问题。

唯一能看出区别的,就是在 Activity Monitor 可以看到架构的区别:

实际体验的时候,其实没有什么感觉。默认情况下,在 Terminal 下打开的是 ARM64 架构的,如果要切换的话,只需要:

$ uname -m
arm64
$ arch -arch x86_64 uname -m
x86_64

这样就可以了。如果开了一个 x86_64 的 shell,在 shell 里面执行的命令就都是 x86_64 架构的了。

Homebrew

目前,Homebrew 的支持是这样子的,Intel 的 Homebrew 工作很正常,没有遇到任何问题。。ARM 的 Homebrew 目前还在进行移植,由于官方的 build farm 还没有支持 ARM,所以各种包都需要自己编译,试了几个常用的软件都没问题。

目前 Homebrew 推荐的方法是,在老地方 /usr/local/Homebrew 下面放 Intel 的 Homebrew,在 /opt/homebrew 下面放 ARM 的 Homebrew。虽然还是有很多警告,但目前来看基本使用都没有什么问题。Homebrew cask 也正常,毕竟基本就是一个下载器。

另外,试了一下用 ARM Homebrew 从源码编译 GCC,编译中途失败了。

其他软件

换到 ARM 上自然会想到,之前的那些软件还能不能跑。答案是,大多都可以,只是很多还是 Intel 版走翻译而已。

目前已经测试过正常使用的:VSCode、Google Chrome、Alacrity、iStat Menus、Alfred、Rectangle、Typora、Microsoft Office、Karabiner Elements、Jetbrains Toolbox、WeChat、CineBench、Dozer、Squirrel、Zoom、Tencent Meeting、Seafile、Skim、Mendeley、1 Password、Wireshark、Slack、iMazing、Office for Mac。

这些里面已经移植到 ARM64 的有 Alfred、iStat Menus、Karabiner Elements、Rectangle、Google Chrome、Slack、Typora、iMazing、Office for Mac、Zoom、VSCode Insiders。

这里有一部分是已经移植到 ARM64 的,有一些也很快就会移植过来。其中 iStat Menus 的电池健康显示有点 BUG,其他没发现问题(更新:已修复)。

另外,大家也知道 ARM Mac 很重要的一点是可以跑 iOS Apps,我们也确实跑了一些,不过都有一些问题:

  • Doodle Jump:跑起来很正常,就是卡关了,别问为什么,没有加速度计,再怎么晃电脑也不会动
  • Bilibili:部分内容可以加载出来,部分不可以,估计是什么组件没有配置好
  • QQ Music:可以跑起来,但是在启动之后的引导页面,期望用户点一下屏幕,但怎么用鼠标点都没反应
  • Weibo:毕竟正常,可以正常浏览啥都,就是 UI 有点错位,估计是因为显示窗口和实际都不大一样,小问题。
  • Network Tools:很正常,各种网络信息都可以正常取出来。
  • NFSee:没有 NFC 读卡功能,自然没法用。
  • 彩云天气(ColorfulClouds Weather):正常使用。

其他还有很多 App 还没有测试。

发热

大家也知道,这款 MBA 是没有风扇的。但我实际测试的过程中发现,确实不大需要。拿 stress 跑了一段时间 CPU 满载运行,也没感觉到电脑发热,只是在更新 macOS Big Sur 11.0.1 的时候稍微热了一点点,也只是一点点,距离烫手还有很长的距离。

续航方面目前来看也挺好的,捣鼓了一个下午,也没耗多少电。

性能测试

在不同平台上进行 OpenSSL 测试:

$ openssl speed -evp aes-128-cbc aes-256-cbc des-ede3 rsa2048 sha256
# M1 MacBookAir
OpenSSL 1.1.1j  16 Feb 2021
built on: Wed Feb 17 12:34:00 2021 UTC
options:bn(64,64) rc4(int) des(int) aes(partial) idea(int) blowfish(ptr) 
compiler: clang -fPIC -arch arm64 -O3 -Wall -DL_ENDIAN -DOPENSSL_PIC -DOPENSSL_CPUID_OBJ -DOPENSSL_BN_ASM_MONT -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DKECCAK1600_ASM -DVPAES_ASM -DECP_NISTZ256_ASM -DPOLY1305_ASM -D_REENTRANT -DNDEBUG
The 'numbers' are in 1000s of bytes per second processed.
type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytes
des ede3         30466.76k    30644.63k    30592.26k    30106.97k    29961.69k    29951.49k
aes-256 cbc     229863.42k   238671.82k   232654.34k   237194.70k   238092.29k   237791.91k
aes-128-cbc    1020384.58k  1427866.73k  1521123.84k  1558199.30k  1569978.99k  1566288.55k
sha256          378646.12k  1140355.52k  1894169.69k  2287211.18k  2445602.42k  2453209.09k
                  sign    verify    sign/s verify/s
rsa 2048 bits 0.000561s 0.000014s   1782.0  69645.9
# AMD EPYC 7742
OpenSSL 1.1.1d  10 Sep 2019
built on: Mon Dec  7 20:44:45 2020 UTC
options:bn(64,64) rc4(8x,int) des(int) aes(partial) blowfish(ptr)
compiler: gcc -fPIC -pthread -m64 -Wa,--noexecstack -Wall -Wa,--noexecstack -g -O2 -fdebug-prefix-map=/build/openssl-CKx7Fo/openssl-1.1.1d=. -fstack-protector-strong -Wformat -Werror=format-security -DOPENSSL_USE_NODELETE -DL_ENDIAN -DOPENSSL_PIC -DOPENSSL_CPUID_OBJ -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DKECCAK1600_ASM -DRC4_ASM -DMD5_ASM -DAESNI_ASM -DVPAES_ASM -DGHASH_ASM -DECP_NISTZ256_ASM -DX25519_ASM -DPOLY1305_ASM -DNDEBUG -Wdate-time -D_FORTIFY_SOURCE=2
The 'numbers' are in 1000s of bytes per second processed.
type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytes
des ede3         28734.07k    28942.08k    28982.78k    29217.91k    29136.21k    29103.45k
aes-256 cbc     176843.84k   183040.83k   183156.82k   184132.61k   184464.73k   184642.22k
aes-128-cbc     602680.15k  1178207.32k  1239931.82k  1251810.30k  1258359.47k  1261316.78k
sha256          201482.20k   513504.00k  1075572.14k  1474850.82k  1648746.50k  1663030.61k
                  sign    verify    sign/s verify/s
rsa 2048 bits 0.000620s 0.000018s   1613.7  54756.4
# AMD EPYC 7282
OpenSSL 1.1.1d  10 Sep 2019
built on: Mon Apr 20 20:23:01 2020 UTC
options:bn(64,64) rc4(8x,int) des(int) aes(partial) blowfish(ptr)
compiler: gcc -fPIC -pthread -m64 -Wa,--noexecstack -Wall -Wa,--noexecstack -g -O2 -fdebug-prefix-map=/build/openssl-8Ocme2/openssl-1.1.1d=. -fstack-protector-strong -Wformat -Werror=format-security -DOPENSSL_USE_NODELETE -DL_ENDIAN -DOPENSSL_PIC -DOPENSSL_CPUID_OBJ -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DKECCAK1600_ASM -DRC4_ASM -DMD5_ASM -DAESNI_ASM -DVPAES_ASM -DGHASH_ASM -DECP_NISTZ256_ASM -DX25519_ASM -DPOLY1305_ASM -DNDEBUG -Wdate-time -D_FORTIFY_SOURCE=2
The 'numbers' are in 1000s of bytes per second processed.
type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytes
des ede3         27052.31k    27392.85k    27455.57k    27569.49k    27503.27k    27514.20k
aes-256 cbc     158578.10k   168502.21k   172365.91k   173904.90k   174391.30k   174429.53k
aes-128-cbc     594506.35k  1111762.07k  1169014.02k  1184384.00k  1192793.56k  1189167.10k
sha256          194382.61k   487875.93k  1017121.56k  1390122.33k  1558735.53k  1572274.18k
                  sign    verify    sign/s verify/s
rsa 2048 bits 0.000655s 0.000019s   1526.8  52089.2
# AMD EPYC 7551
OpenSSL 1.1.1d  10 Sep 2019
built on: Tue Feb 16 22:08:43 2021 UTC
options:bn(64,64) rc4(8x,int) des(int) aes(partial) blowfish(ptr)
compiler: gcc -fPIC -pthread -m64 -Wa,--noexecstack -Wall -Wa,--noexecstack -g -O2 -fdebug-prefix-map=/build/openssl-m9Qnvk/openssl-1.1.1d=. -fstack-protector-strong -Wformat -Werror=format-security -DOPENSSL_USE_NODELETE -DL_ENDIAN -DOPENSSL_PIC -DOPENSSL_CPUID_OBJ -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DKECCAK1600_ASM -DRC4_ASM -DMD5_ASM -DAESNI_ASM -DVPAES_ASM -DGHASH_ASM -DECP_NISTZ256_ASM -DX25519_ASM -DPOLY1305_ASM -DNDEBUG -Wdate-time -D_FORTIFY_SOURCE=2
The 'numbers' are in 1000s of bytes per second processed.
type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytes
des ede3         20850.88k    21260.78k    21315.84k    21368.49k    21321.05k    21392.04k
aes-256 cbc     122059.94k   125701.42k   126591.06k   126770.52k   127049.73k   126937.77k
aes-128-cbc     441625.34k   883733.48k   928208.21k   941480.96k   944889.86k   945307.65k
sha256          151161.13k   388304.60k   809272.15k  1106645.33k  1238966.27k  1249219.93k
                  sign    verify    sign/s verify/s
rsa 2048 bits 0.001096s 0.000033s    912.8  30284.7
# Intel Xeon E5-2699 v4 (Broadwell)
OpenSSL 1.0.2u  20 Dec 2019
built on: reproducible build, date unspecified
options:bn(64,64) rc4(16x,int) des(idx,cisc,16,int) aes(partial) idea(int) blowfish(idx)
compiler: gcc -I. -I.. -I../include  -fPIC -DOPENSSL_PIC -DOPENSSL_THREADS -D_REENTRANT -DDSO_DLFCN -DHAVE_DLFCN_H -Wa,--noexecstack -m64 -DL_ENDIAN -O3 -Wall -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DRC4_ASM -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DMD5_ASM -DAES_ASM -DVPAES_ASM -DBSAES_ASM -DWHIRLPOOL_ASM -DGHASH_ASM -DECP_NISTZ256_ASM
The 'numbers' are in 1000s of bytes per second processed.
type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes
des ede3         29863.80k    30156.69k    30243.07k    30237.70k    30302.21k
aes-256 cbc     103491.45k   110240.94k   112029.95k   112400.38k   112833.88k
aes-128-cbc     734225.68k   788483.88k   802857.39k   805860.69k   807848.62k
sha256           82720.89k   184528.45k   342888.28k   425826.30k   457149.10k
                  sign    verify    sign/s verify/s
rsa 2048 bits 0.000573s 0.000017s   1745.5  60236.3
# IBM POWER8NVL
OpenSSL 1.1.1  11 Sep 2018
built on: Wed Feb 17 12:35:54 2021 UTC
options:bn(64,64) rc4(char) des(int) aes(partial) blowfish(ptr)
compiler: gcc -fPIC -pthread -m64 -Wa,--noexecstack -Wall -Wa,--noexecstack -g -O3 -fdebug-prefix-map=/build/openssl-avwOZX/openssl-1.1.1=. -fstack-protector-strong -Wformat -Werror=format-security -DOPENSSL_USE_NODELETE -DL_ENDIAN -DOPENSSL_PIC -DOPENSSL_CPUID_OBJ -DOPENSSL_BN_ASM_MONT -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DKECCAK1600_ASM -DAES_ASM -DVPAES_ASM -DECP_NISTZ256_ASM -DX25519_ASM -DPOLY1305_ASM -DNDEBUG -Wdate-time -D_FORTIFY_SOURCE=2
The 'numbers' are in 1000s of bytes per second processed.
type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytes
des ede3         25120.65k    25479.70k    25570.13k    25604.10k    25616.38k    25613.65k
aes-256 cbc      79140.44k    82350.23k    83815.94k    84183.72k    84290.22k    84306.60k
aes-128-cbc     310027.28k   647168.64k   890896.81k   984001.19k  1014827.69k  1017096.87k
sha256           58347.98k   151006.68k   286465.28k   373490.69k   411044.52k   414012.76k
                  sign    verify    sign/s verify/s
rsa 2048 bits 0.001442s 0.000040s    693.5  25212.7

总结

总的来说,还是挺香的。不错的性能,没有风扇的喧闹,没有烫手的键盘。可能有少部分软件还不能正常运行,然后很多程序还需要 Rosetta 翻译,但目前来看兼容性还是挺不错的,并且这些应该明年就都适配地差不多了吧。

FIDO U2F、FIDO2 和 CTAP 的关系

背景

2012 年,Yubico 和 Google 设计了 U2F 协议,第二年 U2F 成为 FIDO 组织的标准,之后加入了 NFC 的支持。之后,FIDO2 作为替代 U2F 的新标准产生,原来的 U2F 以兼容的方式成为了 CTAP1,而采用 CBOR 封装格式的 CTAP(CTAP2) 则是 FIDO2 的主要协议。

U2F

命令格式

U2F 定义了它的命令格式,基于 ISO7816-4 APDU(short APDU) :

CLA INS P1 P2 Lc data Le
1 byte 1 byte 1 byte 1 byte 0-1 bytes variable length 0-1 bytes

比如 U2F_VERSION 就是:

CLA INS P1 P2 Lc data Le
00 03 00 00 0 empty 00

返回的数据就是 U2F_V2 的 ASCII 加上 9000 的状态。

除此之外,它还有一种 extended length 格式的 APDU,和上面的是等价的不同表示。

传输方式

实际使用 U2F 的时候,又有三种情况,分别是 USB、Bluetooth 和 NFC。

USB

U2FHID 里面,为了让 U2F 的命令通过 HID 接口传输,它规定了两个 endpoint,分别是 Interrupt IN 和 Interrupt OUT,还有一个固定的 HID Report Descriptor。为了发 U2F 命令,首先会进行一次封装:

CMD BCNT DATA
U2FHID_MSG 4..n n bytes

添加了一个头,表示载荷是一个 U2F 的 command(自然也是 APDU)。

在 cmd 之上,还会封装一层,为了解决 USB 的 packet size 限制等问题,定义了 init packet:

CID CMD BCNTH BCNTL DATA
4 bytes 1 byte 1 byte 1 byte variable length

如果数据太长,就会拆分成一个 init 和 多个 continuation packet:

CID SEQ DATA
4 bytes 1 byte variable length

把 init 和 continuation 里面的 data 组合起来,就是 U2F 的 message,message 里面可能又有 U2F raw command,也就是 APDU。

发送的时候,先 Interrupt OUT 发送请求,再 Interrupt IN 读取回应。

Bluetooth

U2F/Bluetooth 里面,也用了一个类似的封装格式,请求:

CMD HLEN LLEN DATA
1 byte 1 byte 1 byte variable length

这里的 DATA payload 就是 extended length 格式的 APDU

NFC

U2F/NFC 里面,既然 ISO 7816-4 本来就是 NFC-native 的格式,就不要额外的封装了。只需要规定一个 Applet 的 AID 即可:A0000006472F0001

总结

总而言之,U2F raw commands 就是在 APDU 格式上定义了几个命令。在 USB 和 Bluetooth 上都加了几个小的 Header,而 NFC 上则是规定了一个 AID。这对应用程序来说很方便,核心的命令只有一套,需要的时候封装一下即可。

FIDO2

在之后,FIDO2 出现了,在保持 U2F 兼容的基础上添加了新的功能,并且出现了 WebAuthN 作为浏览器使用 FIDO2 的协议。U2F 就变成了第一代的 CTAP,称为 CTAP1,然后 CTAP 默认指的就是 CTAP2。

命令格式

FIDO2 里面,定义了一些 CTAP 命令,比如 authenticatorMakeCredential,对应 U2F 的 U2F_REGISTER 命令。然后,规定了一个 CBOR 的格式,来表示命令附带的数据。CBOR 是 RFC 7049,所以也是借用过来的格式。

传输方式

FIDO2 定义了在 USB 和 NFC 上的传输格式。

USB

在 USB 上传输的时候,定义了 CTAPHID 的协议,与 U2FHID 基本是一样的,规定了 init packet 和 continuation packet,packet 里面也是 CTAPHID 的消息,这部分是兼容 U2F 的。并且,额外添加了 CTAPHID_CBOR 消息:

CMD BCNT DATA DATA + 1
CTAPHID_CBOR 1..(n+1) CTAP command n bytes of CBOR data

它的载荷就是 CBOR 格式的请求。

类似地,它也是通过 Interrupt OUT 发送请求,从 Interrupt IN 读取回应。

NFC

在 NFC 上传输的时候,因为内部的格式是 CBOR,不再是 APDU 了,所以需要一些封装。

首先,它也定义了一个 Applet ID:A0000006472F0001,和 U2F 一样。为了保持兼容,它都支持 U2F 定义的 APDU 命令。

那怎么区分设备是否支持 CTAP1/U2F 和 CTAP2 呢?使用前面提到的 U2F_VERSION 命令即可。如果得到 U2F_V2,说明是支持 CTAP1/U2F 的;如果得到是其他的,说明只支持 CTAP2,不支持 CTAP1/U2F。

如果要发 CTAP2 的命令,就要把 CTAP command 和 CBOR 格式的数据封装到 APDU 里面:

CLA INS P1 P2 Data Le
80 10 00 00 CTAP Command || CBOR Data variable

它规定,如果请求采用的是 extended length 的 APDU,那么响应也要是 extended length 的 APDU;如果请求是 short APDU,那么响应也要支持 short APDU 的 chaining。

兼容性

可以看到,CTAP2 设计的基本都考虑了兼容 U2F,允许用 U2F 的 API 操作 U2F 和 CTAP2 的设备;也允许用 CTAP2 的 API 操作 U2F(只支持部分命令)和 CTAP2 的设备。

总结

可以看到,这里有一堆套娃的过程:

U2F:

USB HID Bluetooth NFC
4 APDU APDU APDU
3 U2F message
2 USB HID packet
1 USB Bluetooth ISO 14443-4/ISO 18092

FIDO2:

USB HID NFC
4 CTAP command + CBOR data CTAP command + CBOR data
3 CTAP message APDU
2 USB HID packet
1 USB ISO 14443-4/ISO 18092

MIFARE Classic 上配置 NDEF

背景

最近买了一堆 NFC 的智能卡拿来测试,其中一张 MIFARE Classic 的总是在 iOS 上读不出来,无论是以 Tag 模式还是 NDEF 模式。于是通过一系列的研究,终于知道上怎么一回事,然后成功地把一个 MIFARE Classic 卡配置成了 NDEF。

背景知识

NFC 有很多协议,其中 MIFARE Classic 基于 ISO 14443-3 Type A 标准,里面有一些 MIFARE 的命令。通过这些命令,就可以控制 MIFARE Classic 卡的内容。具体来说,以我使用的 MIFARE Classic EV1 4K S70 为例,这篇文章会涉及到如下的背景知识:

MIFARE Classic 内存布局

在 MIFARE Classic 中,有 Sector 和 Block 的概念,每个 Sector 有若干个 Block,其中最后一个 Block 是特殊的(称为 Sector Trailer),保存了这个 Sector 的一些信息:Key A、Access Bits、GPB 和 Key B。对于 Classic 4K,首先是 32 个有 4 blocks 的 sector,然后是 8 个 有 16 blocks 的 sector,整体的内存布局大概是:

Sector 0:
    Block 0
    Block 1
    Block 2
    Block 3(Sector Trailer)
Sector 1:
    Block 4
    Block 5
    Block 6
    Block 7(Sector Trailer)
...
Sector 32:
    Block 128
    Block 129
    ...
    Block 143(Sector Trailer)
...
Sector 39:
    ...

每个 Block 有 16 字节,一共 256 个 block,所以是 4K 大小的存储空间。Block 0 比较特殊,保存的是生产商写入的信息,不可更改。

Sector Trailer 的布局如下:

Key A Access Bits GPB Key B
6 字节 3 字节 1 字节 6 字节

其中 Key A 和 Key B 上用于当前 Sector 认证的两个 Key,用相应的 Key 认证以后就可以修改 Sector 里面 Block 的内容。既然有 Key,就会有细粒度的权限控制,就是 Access Bits。它的计算方式比较复杂,首先举个文档AN1305出现过的例子 0x7F 0x07 0x88

  1. 按字节翻转:0x88 0x07 0x7F
  2. 改写成二进制:1000 1000 0000 0111 0111 1111
  3. 拆成前半部分:1000 1000 0000 和后半部分:0111 0111 1111
  4. 如果前后部分互补,说明这是个合法的 Access Bits(这种取反拼接做校验的方法挺常见的)
  5. 取出前半部分:1000 1000 0000
  6. 从后往前取三个字节的最高位:011
  7. 从后往前取第三个字节的次高位,依此类推:000 000 000

这里的 011 表示的是 Sector Trailer 的访问权限,特别地,它表示,不能读出 Key A,只能用 Key B 认证后修改 Key A;用 Key A 或者 Key B 认证后都可以读 Access Bits,但只能在 Key B 认证后修改 Access Bits;不能读出 Key B,只能用 Key B 认证后修改 Key B。也就是说,Key A 认证只能读 Access Bits,而 Key B 认证有权限写入 Key A、Access Bits 和 Key B 字段。完整表格见AN1305 Table 7

之后的三个 000 分别对应前三个 Blocks(又称 Data Blocks,先只考虑带 4 Blocks 的 Sector)的访问权限。000 表示的是,用 Key A 和 Key B 都有完整的读写权限。完整的表格见 AN 1305 Table 8

这里可以给读者留一个练习:0x78 0x77 0x88 对应的权限上什么?

答案:对 Sector Trailer:011;对 Data Blocks:100;此时 Data Blocks 可以用 Key A 或者 Key B 认证读取,但只能用 Key B 认证写入。

如果查看完整的表格就可以发现,Key B 的权限一般是比 Key A 大的,所以 Key B 一般是保密的,而 Key A 可以是公开的。

MIFARE 命令

为了向 MIFARE Classic 卡发送命令,首先需要一个 ISO 14443-3 Type A 的接口,Android 的 NfcA 或者 libnfc 都提供了接口。这里发送的命令实际上会再经过一层解析、用 CRYPTO1 算法加密(猜测是读卡器做的?不是很确定),不过对应用程序来说是透明的。可以参考 MIFARE Classic EV1 1KA Practical Attack on the MIFARE Classic 中的描述。

MIFARE Read

读出一个 Block 的内容,每个 Block 有 16 字节。命令格式如下:

30 XX

如果要读第一个 Block,就是 30 00,如果要读第二个 Block,就是 30 01

返回的数据里刚好是 16 个字节。

MIFARE Write

向一个 Block 写入数据,命令格式如下:

A0 XX YY YY YY YY YY YY YY YY YY YY YY YY YY YY YY YY

这里的 XX 和上面一样,也是 Block 地址;之后是十六字节的数据。

MIFARE Authentiate with A/B

注:这里和 S70 datasheet 里写的不完全一样。

这个命令会进行 Key A 或者 Key B 的认证,如果是对 Key A 认证:

60 XX YY YY YY YY ZZ ZZ ZZ ZZ ZZ ZZ

这里的 XX 也是 Block 地址,但实际上认证的粒度上 Sector,所以只要认证了 Sector 里面的一个 Block,其他 Block 也是同时认证,也是用同一个 Sector Trailer 中的信息进行认证。YY 则是 ISO 14443-3 Type A 中的 UID,如果用 Android 的 API 读取,就可以在 NfcA 中找到这个四字节的信息。ZZ 就是要认证的密钥,六个字节。

如果是对 Key B 认证,把第一个字节的 0x60 改成 0x61 即可。

认证成功后,返回一个 0x00;如果认证失败,则会断开 NFC。

NDEF 是什么

NDEF 实际上是比较高层次的数据,就像 HTML,表示了一个格式化的数组数据,数组的元素可能是文本、URI 等等。它是由若干个 Record 组成的。一个 Record 如下:

03 0B 01 07 54 02 65 6E 61 62 63 64

首先是一个 03 表示类型,然后是长度 0x0B(11,从下一个字节开始数),接着是 0x01 0x07 表示这似乎一个 Well Known 类型的 Record,内容的长度为 7,0x54(ASCII T)表示这是文本格式,0x02 表示编码是 UTF-8,0x65 0x6E (ASCII "en") 表示语言是英语,之后的 0x61 0x62 0x63 0x64(ASCII "abcd")就是文本内容。

很多个 record 连起来,最终一个 0xFE 表示结束,这就是完整的 NDEF 信息了。

在 MIFARE Classic 上使用 NDEF

NDEF 只定义了数据格式,但为了实际使用,还得看具体情况。就好像文件内容保存在硬盘上的时候,并不是直接保存,而是通过文件系统,人为定义一个路径,这样大家才知道要从 /etc/shadow 文件去读 Linux 的用户密码信息,NDEF 也需要人为定义一些规则,再作为数据存放在智能卡里的某个地方,这样大家去读取 metadata,发现上 NDEF Tag,然后才会去解析 NDEF 信息。

有些时候,这个定义很简单,比如直接把 NDEF 数据放在某个 block 里面;有的时候又很复杂,因为可能同时存在很多应用,NDEF 只是其中的一种,所以要有一种类似目录的东西去索引 NDEF“文件”。

MIFARE Classic 上采用的方法上,在特定的 Sector(比如 Sector 0)放一些元数据,元数据里注明了其他的 Sector(从 1 开始的其它 sector)分别用于什么用途,然后 NDEF 是其中一种用途。这个结构叫做 MIFARE Application Directory。具体来说,在 MIFARE Classic 里面,它规定 Block 1 和 Block 2 的内容如下:

0-1 2-3 4-5 6-7 8-9 10-11 12-13 14-15
Info & CRC AID AID AID AID AID AID AID
AID AID AID AID AID AID AID AID

第一个字节是 CRC 8,它的定义可以在这里的 CRC-8/MIFARE-MAD 里找到:初始值 0xC7,多项式上 0x1D。参与 CRC 计算的是按顺序从第二个字节开始的 31 个字节。

第二个字节是 Info Byte,用处不大,见 MAD 的文档。

之后每两个字节对应一个 Sector 的 AID(Application ID),比如 Block 1 的 2-3 字节对应 Sector 1 的 AID,Block 1 的 4-5 字节对应 Sector 2 的 AID,最后 Block 2 的 14-15 字节对应 Sector 15 的 AID。NDEF 的 AID 就是 0x03 0xE1。当软件发现这里的 AID 是 0x03E1 的时候,它就会去相应的 Sector 去读取 NDEF 信息。

一个用 TagInfo 读出来的例子如下:

Sector 0 (0x00)
[skipped]
[01]  F3 01 03 E1 03 E1 00 00
 rW-  00 00 00 00 00 00 00 00
[02]  00 00 00 00 00 00 00 00
 rW-  00 00 00 00 00 00 00 00
[03]  A0:A1:A2:A3:A4:A5  MAD access key
 WXW  78:77:88 C1
      XX:XX:XX:XX:XX:XX  (key unavailable)

可以看到,这里表示的是 Sector 1 和 Sector 2 是 03E1 NDEF。下面 [03] 行表示的是 Key A,下一行是 Access bits、GPD,最后一行是 Key B。TagInfo 会尝试从 well known 里的 Key A 和 Key B 一个个试,直到认证成功为止。常见的如下:

  1. A0 A1 A2 A3 A4 A5:MAD 的 Key A
  2. D3 F7 D3 F7 D3 F7:NDEF 的 Key A
  3. FF FF FF FF FF FF:出场默认的 Key A 和 Key B

如何在 MIFARE Classic 上配置 NDEF

如果看了这么多背景知识,你还有心情看到这里,那要给个掌声。

为什么要在 MIFARE Classic 上配置 NDEF 呢?因为直接买到的 MIFARE Classic(比如我用的 EV1 4K S70)里面都是出厂状态,Key A 和 Key B 都是 FF FF FF FF FF FF,除了 Block 0 以外数据都是 0,所以它并不能用作 NDEF,Android 也只是认为它 NdefFormattable。所以我们要做的就是,Format as NDEF。为啥要自己搞呢,也是因为试了几个现成的工具 format 都失败了。

其实整个流程在 AN1305 的 8.1 章节都写了,但看起来简单,实现起来还是有很多细节,在搞的时候也是来来回回做了很多尝试,同时也利用 TagInfo 强大的 Memory dump 配合调试。

首先复习一下我们可以用哪些命令:

  1. MIFARE Authenticate:对一个 sector 认证,认证成功了才能写操作
  2. MIFARE Read:读取一个 Block
  3. MIFARE Write:写入一个 Block

仔细观察 AN1305 的 Fig.10 和下面的文本描述,大概需要做这些事情:

  1. 修改 Block 1 和 Block 2 中的信息,符合 MAD 的格式
  2. 修改 Sector 0 的 Sector Trailer
  3. 修改 Block 4,填入一个空白的 NDEF,或者直接前面背景知识里的例子。
  4. 修改 Sector 1 和 Sector 2 的 Sector Trailer

但有一些细节:

  1. 修改 Sector Trailer 的时候要谨慎,因为会修改 Key,如果改完又忘了,这卡就废了
  2. 注意用 Key A 还是 Key B 进行认证。上面这些流程结束后,Sector 0 被保护了,需要用 Key B 才能修改数据;而 Sector 1 和 Sector 2 是开放的;如果执行完第一步和第二步以后,发现第一步写错了,就要注意权限的问题,必要时还可以先修改 Access bits 再修改数据
  3. 在这里为了简单,Key B 都用 FF FF FF FF FF FF 了,实际情况下可以用别的自己的密钥,只要记住就行

那么,按照前面的这些知识,就可以构造出每一步的 MIFARE 命令了:

注意:下面的命令不一定能工作,在执行前请仔细理解每条命令的结果,本文作者对卡的损失概不负责

第一步:

60 00 YY YY YY YY FF FF FF FF FF FF
A0 01 F3 01 03 E1 03 E1 00 00 00 00 00 00 00 00 00 00
A0 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

注意 YY 要填入 ID。这一步首先用 FF FF FF FF FF FF 认证了 Sector 0 的 Key A,然后写入了 Block 1 和 Block 2。Info Byte 用的是 0x01,然后用在线工具计算了一下 CRC=F3。

第二步:

A0 03 A0 A1 A2 A3 A4 A5 78 77 88 C1 FF FF FF FF FF FF

这一步设置了 Key A 为 MAD access key,权限是 78 77 88,GPB 是 C1,Key B 为 FF FF FF FF FF FF

第三步:

60 04 YY YY YY YY FF FF FF FF FF FF
A0 04 00 00 03 0B D1 01 07 54 02 65 6E 61 62 63 64 FE

这一步认证了 Sector 1,然后往 Block 4 写入了一个 abcd 的 NDEF 记录。

第四步:

A0 07 D3 F7 D3 F7 D3 F7 7F 07 88 40 FF FF FF FF FF FF
60 08 YY YY YY YY FF FF FF FF FF FF
A0 0B D3 F7 D3 F7 D3 F7 7F 07 88 40 FF FF FF FF FF FF

写入了 Sector 1 的 Sector Trailer,然后认证 Sector 2,再写入 Sector 2 的 Sector Trailer

这样就完成了,再用 TagInfo 等软件,就可以读取出来 NDEF 信息了。此时 iOS 也可以读出来。

上面这些过程,在实际情况下在不同 sector 的时候需要打断,每次重新认证一下。这里默认了一些卡的初始密钥,如果初始情况并不一致,可能并不会工作。

踩的坑

在这个过程中踩过很多的坑:

  1. 在空 NDEF 的时候,NFC Tools 能读出来是 Ndef,并且内容是空,但写入的时候表示 Write error,也读不出来;去 TagInfo 读内存,发现确实写进去了,但内容不对,有一个位置的长度写成了 0,可能是 BUG
  2. 上面也提到过的,就是在修改为只读以后,发现数据写错了,只好重新改成可写,把数据改好了以后再设为只读。
  3. iOS 上用 NFCNDEFReaderSession 可以读出来这个 NDEF 的内容,但 NFCTagReaderSession 并不能 poll 出来。

在命令行中进行 Vivado 仿真

已有 Vivado 项目

想要在命令行里进行 Vivado 仿真,所以查了下 Xilinx 的 UG900 文档,找到了命令行仿真的方法。首先是生成仿真所需的文件:

# assuming batch mode
open_project xxx.xpr
set_property top YOUR_SIM_TOP [current_fileset -simset]
export_ip_user_files -no_script -force
export_simulation -simulator xsim -force

可以把这些语句放到 tcl 文件里然后用 batch mode 执行。执行成功以后,会在 export_sim/xsim 目录下生成一些文件。里面会有生成的脚本以供仿真:

cd export_sim/xsim && ./YOUR_SIM_TOP.sh

默认情况下它会执行 export_sim/xsim/cmd.tcl 里面的命令。如果想要记录 vcd 文件,修改内容为:

open_vcd
log_vcd
run 20us
close_vcd
quit

这样就可以把仿真的波形输出到 dump.vcd 文件,拖到本地然后用 GTKWave 看。更多支持的命令可以到 UG900 里找。

无项目模式

如果没有创建 Vivado 项目,也可以单独进行仿真,具体分为三个步骤:

  1. 第一步,对每个源 Verilog 文件,运行 xvlog module.v 命令
  2. 第二步,生成 snapshot,运行 xelab -debug all --snapshot snapshot_name top_module_name
  3. 第三步,仿真,运行 xsim snapshot_name

如果想要生成波形文件,编辑 xsim.tcl 为以下内容:

open_vcd
log_vcd *
run -all
close_vcd
quit

把第三步运行的命令改为:xsim snapshot_name -tclbatch xsim.tcl 即可。

在 Rocket Chip 上挂接 TLRAM

最近遇到一个需求,需要在 Rocket Chip 里面开辟一块空间,通过 verilog 的 $readmemh 来进行初始化而不是用 BootROM,这样每次修改内容不需要重新跑一次 Chisel -> Verilog 的流程。然后到处研究了一下,找到了解决的方案:

首先是新建一个 TLRAM 然后挂接到 cbus 上:

import freechips.rocketchip.tilelink.TLRAM
import freechips.rocketchip.tilelink.TLFragmenter
import freechips.rocketchip.diplomacy.LazyModule
import freechips.rocketchip.diplomacy.AddressSet

trait HasTestRAM { this: BaseSubsystem =>
  val testRAM = LazyModule(
    new TLRAM(AddressSet(0x40000000, 0x1FFF), beatBytes = cbus.beatBytes)
  )

  testRAM.node := cbus.coupleTo("bootrom") { TLFragmenter(cbus) := _ }
}

这里的地址和大小都可以自由定义。然后添加到自己的 Top Module 中:

class TestTop(implicit p:Parameters)
    extends RocketSystem
    // ...
    with HasTestRAM
    //...
    {
    override lazy ...    
}

实际上这时候 TLRAM 就已经加入到了 TileLink 总线中。接着,为了让 firrtl 生成 $readmemh 的代码,需要两个步骤:

首先是用 chisel3.util.experimental.loadMemoryFromFile 函数(文档在 https://github.com/freechipsproject/chisel3/wiki/Chisel-Memories):

UPDATE:现在的文档在 Loading Memories for simulation or FPGA initialization 处,并且可以用 loadMemoryFromFileInline。

class TestTopImp(outer: TestTop)
    extends RocketSubsystemModuleImp(outer)
    // ...
    {
    loadMemoryFromFile(outer.testRAM.module.mem, "test.hex")    
}

这个函数会生成一个 FIRRTL Annotation,记录了在这里需要对这个 mem 生成对应的 readmemh 调用。然后在 firrtl 的调用里传入 .anno.json 和 transform:

$ runMain firrtl.stage.Main -i xxx -o xxx -X verilog -faf /path/to/xxx.anno.json -fct chisel3.util.experimental.LoadMemoryTransform

UPDATE: 现在不需要 -fct chisel3.util.experimental.LoadMemoryTransform 参数。目前这个功能和生成 blackbox memory 有冲突,不能同时使用,需要等 chisel3 后续修复。

这里的 chisel3.util.experimental.LoadMemoryTransform 会找到 anno.json 里面对应的 Annotation,然后生成类似下面这样的 verilog 代码:

module xxx(
    // ...
);
  // ...
    $readmemh(path, mem_xxx);
endmodule

bind TLRAM xxx xxx(.*);

这里采用了 Verilog 的 bind 功能,可以在不修改模块代码的时候注入,比如上面,就是注入了一个语句 $readmemh,从而达到目的。

通过 BSCAN JTAG 对 Rocket Chip 进行调试

前言

在上一个 post 里研究了原理,今天也是成功在 Artix 7 上实现了调试。效果如下:

OpenOCD 输出:

Info : JTAG tap: riscv.cpu tap/device found: 0x0362d093 (mfg: 0x049 (Xilinx), part: 0x362d, ver: 0x0)
Info : datacount=1 progbufsize=16
Info : Disabling abstract command reads from CSRs.
Info : Examined RISC-V core; found 1 harts
Info :  hart 0: XLEN=32, misa=0x40801105
Info : Listening on port 3333 for gdb connections

GDB 输出:

Remote debugging using localhost:3333
0x0001018c in getc () at bootloader.c:36
36        while (!(*UART_LSR & 0x1))
(gdb) 

这里用的 OpenOCD 和 GDB 都是 riscv 版本,上游的支持尚不完善。对于 Homebrew 用户,我在 jiegec/homebrew-formulas 维护了需要的 Formula。

过程

代码基本借鉴了 sequencer/rocket-playgroundKireinaHoro/rocket-zcu102 而来,代码方面主要是添加了 BscanJTAG.scala,然后在 Top 模块下把它连接到内部的 JTAG 中:

val boardJTAG = Module(new BscanJTAG)
val jtagBundle = target.debug.head.systemjtag.head

// set JTAG parameters
jtagBundle.reset := reset
jtagBundle.mfr_id := 0x233.U(11.W)
jtagBundle.part_number := 0.U(16.W)
jtagBundle.version := 0.U(4.W)
// connect to BSCAN
jtagBundle.jtag.TCK := boardJTAG.tck
jtagBundle.jtag.TMS := boardJTAG.tms
jtagBundle.jtag.TDI := boardJTAG.tdi
boardJTAG.tdo := jtagBundle.jtag.TDO.data
boardJTAG.tdoEnable := jtagBundle.jtag.TDO.driven

代码方面就足够了。然后,需要一个 riscv-openocd 和 riscv-gdb,分别从上游 repo 编译得来。然后采用以下的 openocd.cfg:

adapter_khz 20000
interface ftdi
ftdi_vid_pid 0x0403 0x6014
ftdi_layout_init 0x00e8 0x60eb
ftdi_tdo_sample_edge falling
reset_config none

set _CHIPNAME riscv
jtag newtap $_CHIPNAME cpu -irlen 6

set _TARGETNAME $_CHIPNAME.cpu

target create $_TARGETNAME.0 riscv -chain-position $_TARGETNAME
$_TARGETNAME.0 configure -work-area-phys 0x80000000 -work-area-size 10000 -work-area-backup 1
riscv use_bscan_tunnel 5

然后就可以用 GDB 调试了。

在 Vivado 中对 chisel3 产生的 verilog 代码仿真

默认情况下,chisel3 生成的 verilog 代码在 Vivado 中仿真会出现很多信号大面积变成 X。解决方法在一个不起眼的 Wiki 页面:Randomization flags

`define RANDOMIZE_REG_INIT
`define RANDOMIZE_MEM_INIT
`define RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE_INVALID_ASSIGN

在生成的 verilog 前面加上这四句,就可以正常仿真了。

在 macOS 烧写 Artix7 FPGA

首先安装好 openocd:

brew install openocd --HEAD

测试所用版本为 0.10.0+dev-01052-g09580964 (2020-02-08-15:09)

然后编写如下的 openocd.cfg:

adapter driver ftdi
adapter speed 10000
ftdi_vid_pid 0x0403 0x6014
ftdi_layout_init 0x0008 0x004b

source [find cpld/xilinx-xc7.cfg]
init
xc7_program xc7.tap
pld load 0 /path/to/bitstream.bit
shutdown

上面的 ftdi 开头的两行按照实际的 JTAG Adapter 修改。可以参考 openocd 自带的一些 cfg。

然后在 openocd.cfg 的目录运行 openocd 即可:

$ openocd
Open On-Chip Debugger 0.10.0+dev-01052-g09580964 (2020-02-08-15:09)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'.
Info : ftdi: if you experience problems at higher adapter clocks, try the command "ftdi_tdo_sample_edge falling"
Info : clock speed 10000 kHz
Info : JTAG tap: xc7.tap tap/device found: 0x0362d093 (mfg: 0x049 (Xilinx), part: 0x362d, ver: 0x0)
Warn : gdb services need one or more targets defined
shutdown command invoked
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections

这时 FPGA 已经烧写成功。

参考:

  1. https://pansila.github.io/posts/7db4884d
  2. https://numato.com/kb/programming-mimas-a7-using-openocd-and-xc3sprog/

更新:OpenOCD 已经更新到 0.11.0,对于 Arty A7,采用下面的脚本进行烧写:

# OpenOCD 0.11.0
# Adapted from: interface/ftdi/digilent-hs1.cfg
# See also: board/arty_s7.cfg
adapter driver ftdi
adapter speed 25000
ftdi_vid_pid 0x0403 0x6010
ftdi_channel 0
ftdi_layout_init 0x0088 0x008b
reset_config none

source [find cpld/xilinx-xc7.cfg]
init
pld load 0 ./bitstream.bit
shutdown

成功输出:

$ openocd
Open On-Chip Debugger 0.11.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'.
Info : ftdi: if you experience problems at higher adapter clocks, try the command "ftdi_tdo_sample_edge falling"
Info : clock speed 25000 kHz
Info : JTAG tap: xc7.tap tap/device found: 0x0362d093 (mfg: 0x049 (Xilinx), part: 0x362d, ver: 0x0)
Warn : gdb services need one or more targets defined
shutdown command invoked
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections

研究 Rocket Chip 的 BSCAN 调试原理

前言

最近 @jsteward 在研究如何通过 JTAG 对 FPGA 里的 Rocket Chip 进行调试。之前 @sequencer 已经做了一些实践,我们在重复他的工作,同时也研究了一下这是怎么工作的。

原理

我们从 @sequencer 得到了一份可用的 Scala 代码OpenOCD 配置,并且了解到:

  1. 可以通过 openocd 找到并调试 Rocket Chip
  2. openocd 是通过 JTAG 向 FPGA 的 TAP 的 IR 写入 USER4,然后往 DR 写入特定格式的数据,然后控制 Rocket Chip 的 JTAG。

这里涉及到一个“封装”的过程,在一个仅可以控制 DR 的 JTAG 中控制另一个 JTAG。首先可以找到 OpenOCD 端的操作代码

tunneled_ir[3].num_bits = 3;
tunneled_ir[3].out_value = bscan_zero;
tunneled_ir[3].in_value = NULL;
tunneled_ir[2].num_bits = bscan_tunnel_ir_width;
tunneled_ir[2].out_value = ir_dtmcontrol;
tunneled_ir[1].in_value = NULL;
tunneled_ir[1].num_bits = 7;
tunneled_ir[1].out_value = tunneled_ir_width;
tunneled_ir[2].in_value = NULL;
tunneled_ir[0].num_bits = 1;
tunneled_ir[0].out_value = bscan_zero;
tunneled_ir[0].in_value = NULL;

如果画成图,大概是这个样子(IR):

3 bits IR Width bits 7 bits 1 bit TDI Data Register TDO
0 Payload Tunneled IR Width 0 -> Rocket Chip TAP ->

DR:

3 bits DR Width bits 7 bits 1 bit TDI Data Register TDO
0 Payload Tunneled DR Width 1 -> Rocket Chip TAP ->

这里 TDI 和 TDO 是直接接到 Rocket Chip 的 JTAG 中的,所以我们期望,当 Rocket Chip TAP 在 Shift-IR/Shift-DR 阶段的时候,刚好通过的是 Payload 部分。而控制 TAP 状态机,需要控制 TMS,这个则是通过一段 HDL 来完成的:

always@(*) begin 
        if (counter_neg == 8'h04) begin 
                jtag_tms = TDI_REG; 
        end else if (counter_neg == 8'h05) begin 
                jtag_tms = 1'b1; 
        end else if ((counter_neg == (8'h08 + shiftreg_cnt)) || (counter_neg == (8'h08 + shiftreg_cnt - 8'h01))) begin 
                jtag_tms = 1'b1; 
        end else begin 
                jtag_tms = 1'b0; 
        end 
end

这里 TDI_REG 取的是第一个 bit 的反(也就是上面 IR 对应 0,DR 对应 1 的那一位),shiftreg_cnf 则是之后 7 个 bit,对应上面的 Tunneled IR/DR Width。那么,在选择 IR 时 TMS 的序列为:

4 cycles 1 cycle 1 cycle 2 cycles shiftreg_cnt-1 cycles 2 cycles rest cycles
0 1 1 0 0 1 0
Run-Test/Idle Select-DR-Scan Select-IR-Scan Capture-IR, Shift-IR Shift-IR Exit1-IR, Update-IR Run-Test/Idle

类似地,如果是选择 DR:

4 cycles 1 cycle 1 cycle 2 cycles shiftreg_cnt-1 cycles 2 cycles rest cycles
0 0 1 0 0 1 0
Run-Test/Idle Run-Test/Idle Select-DR-Scan Capture-DR, Shift-DR Shift-DR Exit1-DR, Update-DR Run-Test/Idle

这样,刚好在 Shift-IR/DR 状态下,Payload 会被写入 IR/DR,从而完成了期望的操作。通过规定一个特定格式的 Data Register,可以实现嵌套的 TAP 的 IR 和 DR 的操作。

参考

  1. JTAG Standard
  2. sequencer/rocket-playground
  3. SiFive's JTAG Tunnel: https://github.com/sifive/fpga-shells/blob/c099bd9b4f916bc0ba88030939a9614d0b0daf2d/src/main/scala/ip/xilinx/Xilinx.scala#L13
  4. https://github.com/watz0n/arty_xjtag
  5. https://github.com/riscv/riscv-openocd/blob/7cb8843794a258380b7c37509e5c693977675b2a/src/target/riscv/riscv.c#L361
  6. UG740: 7 Series FPGAs Configuration

用 PulseView 配合 DSLogic 调试 SPI Flash

最近需要用到逻辑分析仪来调试 SPI Flash,设备是 DreamSourceLab 的 DSLogic,最开始用的是官方的 DSView,确实能够抓到 SPI 的信号,也可以解析出一些 SPI Flash 的数据,但是很多是不完整的。

后来把源码下载下来,发现是基于 sigrok 和 PulseView 做的一个魔改版,然后 sigrok 官网上最新的版本已经支持了 DSLogic,于是就用 PulseView 替代 DSView。一开始遇到的问题是没有 firmware,一番搜索找到了解决方案,按照脚本下载好文件即可。

进到 PulseView 以后,把 SPI 的四路信号接上,然后抓了一段信号,解析:

可以看到它正确地解析出来了 Fast Read 命令。由于 DSView 它 fork 自一个比较老的版本,所以它并不能正确解析出来。

P.S. Linux 下它界面显示比 macOS 下好看一些,估计是没有适配好。

在 FPGA 上实现路由器(3)

前言

又半个月过去了,在写了上篇系列博文之后也是做了很多新的更改。上次做的主要是关于性能方面的提升,怎么提高频率,从而达到比较大的流量,而这段时间做的则是功能,做实现 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 可以学习到对方的路由。

在 FPGA 上实现路由器(2)

前言

月初的时候,有了一个完整可用的路由器(上一篇系列博文),但当时测了一下速度,只有几十 Mb/s,只要往上提就会失效,得 reset 才能继续。当时也先没管性能的事情,先把和 OS 交互的部分做了。现在又回头来做性能调优。

之前,逻辑部分的主频只有 10 MHz,这自然不行,不提高肯定做不到千兆。于是试着把主频拉高,FIFO 加大,然后遇到了很多问题,慢慢修复了,学到了很多新知识,目前也接近千兆的水平了吧,贴图:

TCP 测速:

UDP 测速:

测试环境是 macOS 虚拟机外打虚拟机内,走网桥把虚拟机和一个 USB 网卡接起来,然后从另一个 USB 网卡打到路由器。

尝试 700Mb/s

接下来讲讲,在这个过程中遇到了什么问题,怎么解决的。第一个是速度过快就会挂,这肯定是丢包逻辑没写对,后来在仿真里开够了时间,于是就找到了一个 BUG,其实就是一行的修复。接着就是提高主频,但大家也知道,CPU 不能随便超频,由于各种延迟的原因,比如 Setup 时间,如果超了一个时钟周期的时间,本来应该下个周期就得到新数据的,结果到了下下周期才有,那有的状态可能就乱了,我目前遇到的也主要就是这个问题。

于是就对着 Timing 里汇报的各种问题修啊修,发现了很多以前没有注意到的问题,它们不影响功能,但是会让逻辑变慢。第一个问题是 High Fanout,以上就是说一个输出接到了很多输入,这看起来没啥问题,但数设课上也讲过,每个门的输入输出电流是有限制的,例如按书上的数据,一个门输出只能带十个门,更多只能级联一层。级联的话,延迟自然就高了。后来发现,这里的原因是,开了一个大的数组,但是没有变成 RAM,综合出了几千个逻辑单元,自然是出问题。解决方法很简单,用 xpm_memory_tpdram 即可。这样一搞,主频就能上 200MHz 了。

这个时候测了一下,发现 UDP 能打到 700Mb/s 了,TCP 由于丢包率比较高,只有 400Mb/s,距离预期还有一段距离。于是继续进行优化。

向 900Mb/s 进发

要继续提速,自然要提高主频。下一个主频目标就是 250MHz。随着提高主频,时序的要求也会更高,自然也出现了新的问题。

这次的问题主要在于,一个路径上逻辑门数过多,多的有 7 到 10 个,每一步零点几到一点几纳秒,叠起来 4 纳秒哪里够用。于是把一些不需要依赖条件的逻辑挪到条件外面,这样就减少了一些路径的依赖。

解决了这个以后,现在的 WNS(Worst Negative Slack)只剩下 0.6 ns 了。这时候的问题一部分还是来自于逻辑门过多,但这个时候就没这么简单了,只能继续细化流水线,打一拍,这样才能把延迟降下来。

虽然 Timing 没有完全解决,但还是写进了 FPGA 中。幸好工作一切正常,就得到了上面那个图片的结果,接近千兆的速度了。

后续优化途径

后续优化途径的话,一方面是继续对逻辑进行细化和拆分,另一方面是对架构进行优化。当初设计的时候,可能没有考虑地那么周全,例如现在发现,其实 ARP Cache 可以放到每个网口一份,这样会降低花在仲裁上的时间。另外,路由表其实也可以分多份存,只要改的时候同一改就可以了。这样应该会更快。

在 FPGA 上实现路由器

最近在做 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 表有点问题。

向咸鱼派写入 ArchlinuxARM

之前由于我的 macOS 上不知道为啥不能把我的 TF 卡设备放到我的虚拟机里,所以之前就没能刷 ArchLinuxARM 上去。今天我想到了一个方法,完成了这件时期:

$ wget https://mirrors.tuna.tsinghua.edu.cn/archlinuxarm/os/ArchLinuxARM-armv7-latest.tar.gz
$ dd if=/dev/zero of=archlinuxarm.img bs=1M count=1024
$ mkfs.ext4 archlinuxarm.img
$ sudo mkdir -p /mnt/archlinuxarm
$ sudo mount -o loop archlinuxarm.img /mnt/archlinuxarm
$ sudo bsdtar -xpf ArchLinuxARM-armv7-latest.tar.gz -C /mnt/archlinuxarm
$ sudo umount /mnt/archlinuxarm

这样就获得了一个 ext4 的 ArchlinuxARM 镜像。刚好解压出来不到 1G,所以开了 1G 的镜像刚好放得下。然后把 archlinuxarm.img 拷回 macOS,然后用 dd 写进去:

$ sudo dd if=archlinuxarm.img of=/dev/rdisk4s2 bs=1048576

这时候可以确认,我们确实是得到了一个正确的 ext4fs:

$ sudo /usr/local/opt/e2fsprogs/sbin/tune2fs -l /dev/disk4s2

不过,我们实际的分区大小可能不止 1G,所以可以修改一下大小:

$ sudo /usr/local/opt/e2fsprogs/sbin/resize2fs -p /dev/disk4s2

这样就成功地把 ArchlinuxARM 写进去了。默认的用户名和密码都是 root,可以成功通过串口登录。

咸鱼派的启动配置

最近刚拿到了一个咸鱼派的测试板子,准备自己把 U-Boot 和 Linux 内核这一套东西跑通,都用主线的东西,尽量减少魔改的部分。首先是编译 u-boot,我用的是现在的 master 分支的最新版 99431c1c:

$ # Archlinux
$ sudo pacman -Sy arm-none-eabi-gcc
$ make LicheePi_Zero_defconfig
$ make ARCH=arm CROSS_COMPILE=arm-none-eabi- -j24

这时候会得到一个 u-boot-sunxi-with-spl.bin 的文件。我们只要把它写到 SD 卡的 8192 偏移处,就可以把 U-Boot 跑起来了:

$ diskutil unmountDisk /dev/disk4
$ sudo dd if=u-boot-sunxi-with-spl.bin of=/dev/disk4 bs=1024 seek=8

接着我们做一下分区。我采用的是 MBR 分区,这样保证不会和 U-Boot 冲突。使用 fdisk 进行分区,我从 1M 处开始分了一个 10M 的 FAT-32 分区作为启动分区,然后之后都是 EXT4 的系统盘分区。接着就是编译内核。

我用的是八月份时候的 4.18.2 内核,虽然不是很新但也足够新了。一番调整内核参数后,得到了一个可用的内核,然后把 zImage 和 sun8i-v3s-licheepi-zero.dtb 都复制到刚才创建的 FAT-32 启动分区,然后进入 U-Boot 进行启动:

$ setenv bootcmd 'fatload mmc 0 0x41000000 zImage; fatload mmc 0 0x41800000 sun8i-v3s-licheepi-zero.dtb; setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait; bootz 0x41000000 - 0x41800000'
$ saveenv # optional
$ boot

这里一开始遇到了很多坑,比如一直看不到 console,这个是找了 @gaoyichuan 拿到的一份 Kernel Config 进行修改修好的。另一个是进去以后找不到 root,我先是搞了一个有 busybox 的 initrd,进去看发现是能找到 mmc 的,但是有延迟,那么添加上 rootwait 就好了。进去以后就差 rootfs。由于我缺少一个写 ext4 的工具,又发现手上有一个 Raspbian 的镜像,它里面也正好是两个分区,而且架构也同样是 armv7l,我就直接把它烧到 SD 卡中,把 U-Boot 写进去,然后往 boot 分区里写内核和 dtb,然后就成功进去,并且跑起来了。最喜感的就是,进去以后是个 pi@raspberrypi,实际上确是另一个东西。不过,只有当我 apt update 发现用了半小时的时候,我才想起来这其实是是一个嵌入式系统。。

进去以后发现,没有识别到网卡驱动。网上找了 LicheePi Zero 的一个解决方案,但是并不能用,还出现了神奇的 Kernel Oops,怀疑是内核版本太新的问题。我又找到 @icenowy 的一个 Patch ,它终于是解决了这个问题,成功地找到了网卡,并且愉快地 ssh pi@raspberrypi.local 。之后会在咸鱼派那边公布一下我们做的修改。

现在的想法是,把 HomeBridge 搭建到它上面,不过目前来看硬件资源有点紧张,放着会有点慢。可能还是用树莓派做这个事情比较合适。

在荔枝糖(Lichee Tang)上初次体验 FPGA

今天从张宇翔学长那拿到了 荔枝糖(Lichee Tang) 的 FPGA 板子,于是立即开始把前段时间学到的 Verilog 应用上来。不过想到现在我手上没有多少外设,然后又必须远程到 Windows 电脑上去操作,于是先实现了一下 UART 通信。

在网上找到了 ben-marshall/uart 一个简易的实现,很快做到了一直在串口上打印 A 字符。接着我开始尝试实现一个简单的串口回显。一开始,我直接把 UART 读到的数据直接输出,果然可以了,但是一旦传输速率跟不上了,就会丢失数据。于是我添加了 FIFO IP 核,然后把读入的数据存入 FIFO,又从 FIFO 中读取数据写入到 UART 中去。不过发现了一个小 BUG:每次打印的是倒数第二次输入的字符,即丢失了第一个字符。在张宇翔学长的帮助下找到了问题:当 FIFO 的读使能信号为高时,其数据在下一个时钟周期才来,于是解决方案就是等到数据来的时候再向 UART 中写数据:

always @ (posedge clk_in) begin
    uart_tx_en <= uart_fifo_re;
end

这样就解决了这个问题。完整代码在 jiegec/learn_licheetang 中。

在 macOS 上读取移动硬盘的 S.M.A.R.T. 信息

之前想看看自己各个盘的情况,但是发现只能看电脑内置的 SSD 的 S.M.A.R.T 信息,而移动硬盘的都显示:

$ smartctl -a /dev/disk2
smartctl 6.6 2017-11-05 r4594 [Darwin 17.7.0 x86_64] (local build)
Copyright (C) 2002-17, Bruce Allen, Christian Franke, www.smartmontools.org

/dev/disk2: Unable to detect device type
Please specify device type with the -d option.

Use smartctl -h to get a usage summary

一开始我怀疑是个别盘不支持,但换了几个盘都不能工作,问题应该出现在了 USB 上。查了下资料,果然如此。根据 USB devices and smartmontools ,获取 S.M.A.R.T 信息需要直接发送 ATA 命令,但是由于经过了 USB,于是需要进行一个转换,导致无法直接发送 ATA 命令。这个问题自然是有解决方案,大概就是直接把 ATA 命令发送过去(pass-through)。上面这个地址里写到,如果需要在 macOS 上使用,需要安装一个内核驱动。可以找到,源码在 kasbert/OS-X-SAT-SMART-Driver 并且有一个带签名的安装包在 External USB / FireWire drive diagnostics support 中可以下载。丢到 VirusTotal 上没查出问题,用 v0.8 版本安装好后就成功地读取到了移动硬盘的 S.M.A.R.T 信息了。

然后我又简单研究了一下各个 S.M.A.R.T 各个值的含义是什么。 VALUE 代表当前的值, WORST 代表目前检测到的最差的值, THRESH 代表损坏阈值。这些值都是从 RAW_VALUE 进行计算后归一化而来。然后 TYPE 分为两种,一是 Pre-fail ,代表如果这一项的值小于阈值,代表这个机器很危险了,赶紧拷数据丢掉吧。二是 Old_age ,代表如果这一项小于阈值,代表这个机器比较老了,但还没坏。真正要看是否坏了,可以看 When_Failed 一栏。

华为随行 WiFi 2 mini 开箱

前段时间,我办了 4G 升级,移动送了一张副卡,有不少免费的流量,由于我的手机是 iPhone 不支持双卡,老爸就借了我他的 GlocalMe 当成 MiFi 来用,不过呢 GlocalMe 放在这里当然是大材小用了,所以我就网购了华为随行 WiFi 2 mini,把我的副卡装上一个壳放进去就可以了!把这个 MiFi 插入电脑,会弹出一个目录,里面有 Win/Mac 的驱动安装文件,打开后在网络设置里就有 HUAWEI_MOBILE 的连接了,并自动打开网络配置界面。设置一下 SSID 和密码,就能正常使用了,手机连上也很正常,手机上可以下载 HUAWEI HiLink 来配置 MiFi,挺爽的。随赠的有联通的上网卡,不过我准备在北京买个上网卡放 MiFi 里面用。