01signal.com

Quartus:把寄存器打包进 I/O 单元

我通常更喜欢用这样的方式来处理 I/O 时序:确保所有寄存器都被放进了 I/O 单元(I/O cell)里。当然,这指的是对时序有要求的地方。

看起来 Quartus 默认并不会做 I/O 寄存器打包(I/O register packing)。不管怎样,下面就是懒人应对这种场景的做法。

在本文较早的版本中,我曾建议把全部 I/O 上的时序检查都禁用。这样做确实能消除实现过程中“未约束路径”的警告,尤其是不至于让 Quartus 报告面板里的 TimeQuest Timing Analyzer 一栏变红:

set_false_path -from [get_ports]
set_false_path -to [get_ports]

后来发现,这个主意并不好,尤其对于输入端口而言。关于这一点,下文还会进一步展开。

尽管如此,你还是得说服布局布线器(fitter)把寄存器放进 I/O 模块(I/O block)里。为此,需要在 QSF 文件中加入下面几行:

set_instance_assignment -name FAST_OUTPUT_REGISTER ON -to *
set_instance_assignment -name FAST_INPUT_REGISTER ON -to *
set_instance_assignment -name FAST_OUTPUT_ENABLE_REGISTER ON -to *

这样把所有寄存器一概而论地全部指定,确实有点过于激进,但它确实管用。如果某些 I/O 单元上无法满足这些约束,布局布线器会给出警告,这反而是件好事。

想知道效果如何,可以去布局布线报告里查看 Resource Section 部分(一般可以在 Quartus 的报告面板里找到),注意其中 Input Registers、Output Registers 等相关条目。

两者的差别在涉及 I/O 单元的路径(path)时序报告中非常明显。例如,对比下面这条使用了 I/O 寄存器的路径:

+----------------------------------------------------------------------------------+
; Data Arrival Path                                                                ;
+---------+---------+----+------+--------+-----------------------+-----------------+
; Total   ; Incr    ; RF ; Type ; Fanout ; Location              ; Element         ;
+---------+---------+----+------+--------+-----------------------+-----------------+
; 2.918   ; 2.918   ;    ;      ;        ;                       ; data path       ;
;   0.000 ;   0.000 ;    ;      ; 1      ; DDIOOUTCELL_X3_Y0_N32 ; rst             ;
;   0.465 ;   0.465 ; RR ; CELL ; 1      ; DDIOOUTCELL_X3_Y0_N32 ; rst|q           ;
;   0.465 ;   0.000 ; RR ; IC   ; 1      ; IOOBUF_X3_Y0_N30      ; RESETB~output|i ;
;   2.918 ;   2.453 ; RR ; CELL ; 1      ; IOOBUF_X3_Y0_N30      ; RESETB~output|o ;
;   2.918 ;   0.000 ; RR ; CELL ; 0      ; PIN_P3                ; RESETB          ;
+---------+---------+----+------+--------+-----------------------+-----------------+

请留意报告中的 DDIOOUTCELL 单元,还要注意寄存器到 IOOBUF 之间的那段走线延时增量为 0。

作为对照,下面这条路径则没有用上 I/O 寄存器(因为逻辑上不允许它被打包进去):

+--------------------------------------------------------------------------------+
; Data Arrival Path                                                              ;
+---------+---------+----+------+--------+-----------------+---------------------+
; Total   ; Incr    ; RF ; Type ; Fanout ; Location        ; Element             ;
+---------+---------+----+------+--------+-----------------+---------------------+
; 8.284   ; 8.284   ;    ;      ;        ;                 ; data path           ;
;   0.000 ;   0.000 ;    ;      ; 1      ; FF_X3_Y0_N17    ; Dir_flop_sig        ;
;   0.496 ;   0.496 ; RR ; CELL ; 8      ; FF_X3_Y0_N17    ; Dir_flop_sig|q      ;
;   2.153 ;   1.657 ; RR ; IC   ; 1      ; IOOBUF_X3_Y0_N9 ; DATA[7]~output|oe   ;
;   8.284 ;   6.131 ; RF ; CELL ; 1      ; IOOBUF_X3_Y0_N9 ; DATA[7]~output|o    ;
;   8.284 ;   0.000 ; FF ; CELL ; 1      ; PIN_T3          ; DATA[7]             ;
+---------+---------+----+------+--------+-----------------+---------------------+

这里可以看到,信号是由普通的寄存器(flip-flop)产生的,于是引入了长达 1.657 ns 的布线(routing)延时。主要麻烦在于,每一次编译实现时,这段布线延时都会不同。所以,如果电路板上刚好有信号完整性方面的问题,FPGA 反而可能被怪罪——因为不同的 FPGA 设计版本似乎会时而让问题消失,时而又让它重新冒出来。

时序约束

输入端口和输出端口都应该设置很紧张的时序约束(timing constraints),使得只有充分利用 I/O 寄存器才能满足它们。这样一来,如果期望的寄存器打包没有发生,时序就会报错。而且,如后文所述,要想做到最小的“输入到寄存器”延时,也必须有这样的约束。

下面这段讨论只适用于驱动这些寄存器的时钟与某个外部时钟有直接关系的情况(例如通过 PLL 对时钟做整数倍倍频)。如果驱动寄存器的时钟与外部时钟实际上没什么关系,问题就会变得复杂很多,正如这篇文章里讨论的那样。

为了说明这个问题,来看下面这段 Verilog 代码:

module top
  (
   input        clk,
   input        in,
   output reg   out
   );

   reg 		in_d, in_d2;
   wire  	pll_clk;

   always @(posedge pll_clk)
     begin
	in_d <= in;
	in_d2 <= in_d;
	out <= in_d2;
     end

  /* Here comes an instantiation of a phase-compensating PLL, which
     doesn't change the frequency */
endmodule

再来看 SDC 文件里的下面这条约束:

create_clock -name main_clk -period 10 -waveform { 0 5 } [get_ports {clk}]

derive_pll_clocks
derive_clock_uncertainty

set_input_delay -clock main_clk -max 8.5 [get_ports in*]
set_input_delay -clock main_clk -min 0 [get_ports in*]

正如这篇文章里解释的那样,set_input_delay 指的是信号源从时钟沿到逻辑状态达到有效之间的最大延时。由于时钟周期被设成了 10 ns,再把输入延时约束设为 8.5 ns,就相当于在下一个时钟沿(10 ns 处)到来之前只留出了 1.5 ns。换句话说,FPGA 引脚上的建立时间(setup time)被强制要求不能超过 1.5 ns。

请注意,set_max_delay 也可以用来达到这个目的(在有些情况下甚至是唯一可行的办法),相关讨论见这篇文章

按照上述方式(再加上前面 QSF 里的 FAST_INPUT_REGISTER ON 设定)做一次编译,得到的时序报告片段如下:

+----------------------------------------------------------------------------------+
; Data Arrival Path                                                                ;
+---------+---------+----+------+--------+-------------------+---------------------+
; Total   ; Incr    ; RF ; Type ; Fanout ; Location          ; Element             ;
+---------+---------+----+------+--------+-------------------+---------------------+
; 0.000   ; 0.000   ;    ;      ;        ;                   ; launch edge time    ;
; 0.000   ; 0.000   ;    ;      ;        ;                   ; clock path          ;
;   0.000 ;   0.000 ; R  ;      ;        ;                   ; clock network delay ;
; 8.500   ; 8.500   ; F  ; iExt ; 1      ; PIN_F2            ; in                  ;
; 9.550   ; 1.050   ;    ;      ;        ;                   ; data path           ;
;   8.500 ;   0.000 ; FF ; IC   ; 1      ; IOIBUF_X0_Y22_N15 ; in~input|i          ;
;   9.308 ;   0.808 ; FF ; CELL ; 1      ; IOIBUF_X0_Y22_N15 ; in~input|o          ;
;   9.308 ;   0.000 ; FF ; IC   ; 1      ; FF_X0_Y22_N17     ; in_d|d              ;
;   9.550 ;   0.242 ; FF ; CELL ; 1      ; FF_X0_Y22_N17     ; in_d                ;
+---------+---------+----+------+--------+-------------------+---------------------+

与前面输出寄存器的情况不同,这条路径里没有类型为 DDIOINCELL 的触发器,取而代之的是一个看起来像是普通触发器的元件。不过请注意,通往这个触发器的布线延时为 0(在上面的报告里已被红色标出),这清楚表明触发器其实和输入缓冲器合并在了一起。

器件手册报告(datasheet report)中针对这个输入给出的数据如下:

+---------------------------------------------------------------------------------------------------+
; Setup Times                                                                                       ;
+-----------+------------+-------+-------+------------+---------------------------------------------+
; Data Port ; Clock Port ; Rise  ; Fall  ; Clock Edge ; Clock Reference                             ;
+-----------+------------+-------+-------+------------+---------------------------------------------+
; in        ; main_clk   ; 1.282 ; 1.461 ; Rise       ; altpll_component|auto_generated|pll1|clk[0] ;
+-----------+------------+-------+-------+------------+---------------------------------------------+

+-----------------------------------------------------------------------------------------------------+
; Hold Times                                                                                          ;
+-----------+------------+--------+--------+------------+---------------------------------------------+
; Data Port ; Clock Port ; Rise   ; Fall   ; Clock Edge ; Clock Reference                             ;
+-----------+------------+--------+--------+------------+---------------------------------------------+
; in        ; main_clk   ; -0.683 ; -0.862 ; Rise       ; altpll_component|auto_generated|pll1|clk[0] ;
+-----------+------------+--------+--------+------------+---------------------------------------------+

正如所要求的那样,FPGA 本身需要的建立时间小于约束所限定的 1.5 ns。

现在把输入建立延时约束放松 2 ns,其它一切保持不变,然后重新编译:

set_input_delay -clock main_clk -max 6.5 [get_ports in*]
set_input_delay -clock main_clk -min 0 [get_ports in*]

时序报告里对应的片段变成了这样:

+----------------------------------------------------------------------------------+
; Data Arrival Path                                                                ;
+---------+---------+----+------+--------+-------------------+---------------------+
; Total   ; Incr    ; RF ; Type ; Fanout ; Location          ; Element             ;
+---------+---------+----+------+--------+-------------------+---------------------+
; 0.000   ; 0.000   ;    ;      ;        ;                   ; launch edge time    ;
; 0.000   ; 0.000   ;    ;      ;        ;                   ; clock path          ;
;   0.000 ;   0.000 ; R  ;      ;        ;                   ; clock network delay ;
; 6.500   ; 6.500   ; F  ; iExt ; 1      ; PIN_F2            ; in                  ;
; 8.612   ; 2.112   ;    ;      ;        ;                   ; data path           ;
;   6.500 ;   0.000 ; FF ; IC   ; 1      ; IOIBUF_X0_Y22_N15 ; in~input|i          ;
;   7.308 ;   0.808 ; FF ; CELL ; 1      ; IOIBUF_X0_Y22_N15 ; in~input|o          ;
;   8.370 ;   1.062 ; FF ; IC   ; 1      ; FF_X0_Y22_N17     ; in_d|d              ;
;   8.612 ;   0.242 ; FF ; CELL ; 1      ; FF_X0_Y22_N17     ; in_d                ;
+---------+---------+----+------+--------+-------------------+---------------------+

咦?互连(interconnect)延时怎么突然涨到了 1.062 ns?!请注意,寄存器的布局位置并没有变,所以 in_d 毫无疑问仍然是 I/O 寄存器。那么这段延时到底是从哪儿来的?

要回答这个问题,需要更仔细地看看设计内部的情况。完成一次完整编译后,选择 Tools > Netlist Viewers > Technology Map Viewer (Post-Fitting),会出现下面的示意图(这里只显示了一部分,点击可以放大):

Design diagram

在 in_d(也就是那个寄存器)上点击右键,选择 Locate Node > Locate in Resource Property Editor,就会看到下图(点击可放大):

Property editor view

在这个示意图的右侧(上面的图中没有显示出来),属性 Input Pin to Input Register Delay 被设成了 2。这正是这段延时的来源。而在放松约束之前,这个值是 0。由此可以立刻得到一个教训:

如果建立时间约束没有设置到该器件工艺所能达到的最好水平,Quartus 就可能会消耗掉这部分多余的裕量,转而插入一段延时。

但 Quartus,这究竟是为什么?

于是人们自然会问:Quartus 为什么要在输入引脚和寄存器之间插入这段延时?这样做的初衷不就是要尽可能地早采样吗?要回答这个问题,可以看看更新后的器件手册报告:

---------------------+
; Data Port ; Clock Port ; Rise  ; Fall  ; Clock Edge ; Clock Reference                             ;
+-----------+------------+-------+-------+------------+---------------------------------------------+
; in        ; main_clk   ; 2.205 ; 2.523 ; Rise       ; altpll_component|auto_generated|pll1|clk[0] ;
+-----------+------------+-------+-------+------------+---------------------------------------------+

+-----------------------------------------------------------------------------------------------------+
; Hold Times                                                                                          ;
+-----------+------------+--------+--------+------------+---------------------------------------------+
; Data Port ; Clock Port ; Rise   ; Fall   ; Clock Edge ; Clock Reference                             ;
+-----------+------------+--------+--------+------------+---------------------------------------------+
; in        ; main_clk   ; -1.570 ; -1.882 ; Rise       ; altpll_component|auto_generated|pll1|clk[0] ;
+-----------+------------+--------+--------+------------+---------------------------------------------+

回顾一下,刚才把输入延时约束调低了 2 ns,于是允许的最大建立时间从 1.5 ns 提高到了 3.5 ns。很容易看出,这个要求被满足了,并且还剩下差不多 1 ns 的时序裕量(slack)。

所以 Quartus 心里大概是这样想的:“这个建立要求我轻轻松松就能满足,还有 2 ns 的富余。不如拿出 1 ns 来改善建立时间,另外 1 ns 用来改善保持时间需求(它本来就是 0 ns)。”而事实上,通过加入这 1.062 ns 的延时,保持时间确实从 -0.683 ns 变成了 -1.570 ns(至于为什么差值不是严格精确,麻烦不要跟我抬杠)。

归根结底:Quartus 把建立时间和保持时间的裕量都加大了,从而让这个输入对抖动(jitter)更不敏感。虽然这样做本身算是一件挺合理的事,但在很多场合下,我们既不希望、也没有预料到它会这么做。

结论:如果你想要的是从输入到寄存器的绝对最小延时,那就先故意用一个无法满足的延时约束去编译;等时序报错之后,再把约束放松到刚好能让错误消失的程度。这样一来,Quartus 就不会为了把保持时间做得更好,而试图“优化”时序——也就是偷偷插入上面说的那段输入延时了。

使用 DDR 原语

Intel 的 FPGA 在 I/O 单元上或紧挨着它们的位置,放置了一些专用逻辑,用于支持以双倍时钟速率产生输出或采样输入。这个话题在相关用户指南ug_altddio.pdf里有详细介绍。实例化(instantiation)一个 DDR 原语(primitive),或者使用 ALTDDIO_BIDIR 宏功能(megafunction),是一个很吸引人的办法,可以借此强迫工具把寄存器放进 I/O 单元。然而,这不一定是个好主意。

例如,下面这种实例化:

altddio_bidir ioddr
 (
 .padio(pin),
 .aclr (1'b0),
 .datain_h(datain_h),
 .datain_l(datain_l),
 .inclock(clk),
 .oe(oe),
 .outclock(clk),
 .dataout_h(dataout_h),
 .dataout_l(dataout_l),
 .oe_out (),
 .aset (1'b0),
 .combout(),
 .dqsundelayedout(),
 .inclocken(1'b1),
 .outclocken(1'b1),
 .sclr(1'b0),
 .sset(1'b0));
 defparam
   ioddr.extend_oe_disable = "OFF",
   ioddr.implement_input_in_lcell = "OFF",
   ioddr.intended_device_family = "Cyclone IV E",
   ioddr.invert_output = "OFF",
   ioddr.lpm_hint = "UNUSED",
   ioddr.lpm_type = "altddio_bidir",
   ioddr.oe_reg = "REGISTERED",
   ioddr.power_up_high = "OFF",
   ioddr.width = 1;

这的确会生成一份实现双向 DDR 接口的逻辑,但就时序效果而言,只能说是部分成功——至少 Cyclone IV 上是这样。它从时钟沿到输出有效的时序,与普通输出寄存器被打包进 I/O 单元时完全相同;但上面这个实例化在输入路径上的延时实际上反而更差。换成其它 Intel FPGA 系列,结果可能又会不同。

注意,若想用 DDR 原语去模拟普通的 SDR 寄存器,必须把它的 datain_h 和 datain_l 两个端口接到同一条线上,这样时钟的下降沿才不会改变任何东西。同样地,dataout_l 的值应当被忽略,因为它是在下降沿采样的。另外还要注意,输出使能端口(oe)是一个 SDR 输入——就我目前的理解,在 Intel FPGA 上没法用 DDR 的速率去打开或关闭高阻态,至少现成的逻辑原语做不到这一点。

那为什么它在输出寄存器上效果不错,在输入那边却不行?线索就在上面的时序报告里:哪怕只是一个普通的 I/O 单元寄存器,时序报告也会用一个名为 DDIOOUTCELL_Xn_Ym_Nk 的元件来充当寄存器。换句话说,即使是单倍速率的输出,底层用的仍然是 DDR 输出寄存器,只不过只使用了一个时钟沿而已。但输入路径就不一样了:上面的时序报告显示,输入用的是逻辑阵列里的普通寄存器(FF_Xn_Ym_Nk)。这里的关键在于:DDR 输入逻辑同样是在逻辑阵列里实现的。更糟的是,用到 DDR 输入时,I/O 单元和触发器之间还被塞进了几块组合逻辑。老实说,我不明白为什么要这样,因为每一块这样的组合逻辑都仅仅是把一个输入直通到一个输出。

上述观察既能从时序报告里得到印证,也可以从 Quartus 的 Post-Fit Technology Map Viewer 显示的图形看出来。尤其是那些没用的组合逻辑块,在上述两种信息源中都非常扎眼。

这整件事很可能会随 FPGA 系列的不同而不同。就 Cyclone IV 而言,把 DDR 原语用在输出上还是有意义的。

更重要的在于:当要求一个输出使用寄存器时,底层实际上用的是 DDR 输出原语,这让我们可以生成一个与其它输出对齐的输出时钟。做法是:给一个 DDR 输出原语分别接上恒定的 1(datain_h 端)和 0(datain_l 端)。至于其它输出,则要求它们使用输出寄存器打包。这样,其它输出的翻转时刻就会与来自这个 DDR 输出的时钟上升沿对齐。

嗯,差不多是这么说。不过,输出时钟本身的时序分析会有点不一样,因为它的时钟沿用来控制一个多路选择器(mux),由这个 mux 决定两个输出寄存器中的哪一个去驱动输出引脚(下面这份报告需要横向滚动才能看全细节):

+------------------------------------------------------------------------------------------------------------------------------------+
; Data Arrival Path                                                                                                                  ;
+---------+---------+----+------+--------+-------------------------+-----------------------------------------------------------------+
; Total   ; Incr    ; RF ; Type ; Fanout ; Location                ; Element                                                         ;
+---------+---------+----+------+--------+-------------------------+-----------------------------------------------------------------+
; 0.000   ; 0.000   ;    ;      ;        ;                         ; launch edge time                                                ;
; 0.000   ; 0.000   ;    ;      ;        ;                         ; clock path                                                      ;
;   0.000 ;   0.000 ; R  ;      ;        ;                         ; clock network delay                                             ;
; 0.000   ; 0.000   ; R  ;      ; 1      ; PIN_B12                 ; osc_clock                                                       ;
; 5.610   ; 5.610   ;    ;      ;        ;                         ; data path                                                       ;
;   0.000 ;   0.000 ; RR ; IC   ; 1      ; IOIBUF_X19_Y29_N8       ; osc_clock~input|i                                               ;
;   0.667 ;   0.667 ; RR ; CELL ; 2      ; IOIBUF_X19_Y29_N8       ; osc_clock~input|o                                               ;
;   0.853 ;   0.186 ; RR ; IC   ; 1      ; CLKCTRL_G12             ; osc_clock~inputclkctrl|inclk[0]                                 ;
;   0.853 ;   0.000 ; RR ; CELL ; 165    ; CLKCTRL_G12             ; osc_clock~inputclkctrl|outclk                                   ;
;   1.971 ;   1.118 ; RR ; IC   ; 1      ; DDIOOUTCELL_X16_Y29_N11 ; sram_controller_ins|ddr_clk|auto_generated|ddio_outa[0]|muxsel  ;
;   3.137 ;   1.166 ; RR ; CELL ; 1      ; DDIOOUTCELL_X16_Y29_N11 ; sram_controller_ins|ddr_clk|auto_generated|ddio_outa[0]|dataout ;
;   3.137 ;   0.000 ; RR ; IC   ; 1      ; IOOBUF_X16_Y29_N9       ; sram_clk~output|i                                               ;
;   5.610 ;   2.473 ; RR ; CELL ; 1      ; IOOBUF_X16_Y29_N9       ; sram_clk~output|o                                               ;
;   5.610 ;   0.000 ; RR ; CELL ; 0      ; PIN_E10                 ; sram_clk                                                        ;
+---------+---------+----+------+--------+-------------------------+-----------------------------------------------------------------;

注意,这不算“寄存器到引脚”的分析,而是“时钟到引脚”的分析。尽管如此,set_output_delay 约束仍然会把这条路径算进去。但如果你采用的是从寄存器到端口的 set_max_delay 约束,这条路径并不会被包含,所以必须单独处理它。换句话说,如果使用 set_max_delay,约束得写成下面这种形式:

set_max_delay -from [get_clocks main_clk] -to [get_ports sram_clk] 3.8

现在再拿另一个采用同样电压标准、等等条件的引脚来对比——它只是由一个寄存器来驱动:

+----------------------------------------------------------------------------------------------------------------------+
; Data Arrival Path                                                                                                    ;
+---------+---------+----+------+--------+-------------------------+---------------------------------------------------+
; Total   ; Incr    ; RF ; Type ; Fanout ; Location                ; Element                                           ;
+---------+---------+----+------+--------+-------------------------+---------------------------------------------------+
; 0.000   ; 0.000   ;    ;      ;        ;                         ; launch edge time                                  ;
; 2.507   ; 2.507   ;    ;      ;        ;                         ; clock path                                        ;
;   0.000 ;   0.000 ;    ;      ;        ;                         ; source latency                                    ;
;   0.000 ;   0.000 ;    ;      ; 1      ; PIN_B12                 ; osc_clock                                         ;
;   0.000 ;   0.000 ; RR ; IC   ; 1      ; IOIBUF_X19_Y29_N8       ; osc_clock~input|i                                 ;
;   0.667 ;   0.667 ; RR ; CELL ; 2      ; IOIBUF_X19_Y29_N8       ; osc_clock~input|o                                 ;
;   0.853 ;   0.186 ; RR ; IC   ; 1      ; CLKCTRL_G12             ; osc_clock~inputclkctrl|inclk[0]                   ;
;   0.853 ;   0.000 ; RR ; CELL ; 165    ; CLKCTRL_G12             ; osc_clock~inputclkctrl|outclk                     ;
;   1.970 ;   1.117 ; RR ; IC   ; 1      ; DDIOOUTCELL_X37_Y29_N11 ; sram_controller_ins|dq_wr_data[6]|clk             ;
;   2.507 ;   0.537 ; RR ; CELL ; 1      ; DDIOOUTCELL_X37_Y29_N11 ; sram_controller:sram_controller_ins|dq_wr_data[6] ;
; 5.645   ; 3.138   ;    ;      ;        ;                         ; data path                                         ;
;   2.717 ;   0.210 ;    ; uTco ; 1      ; DDIOOUTCELL_X37_Y29_N11 ; sram_controller:sram_controller_ins|dq_wr_data[6] ;
;   3.182 ;   0.465 ; RR ; CELL ; 1      ; DDIOOUTCELL_X37_Y29_N11 ; sram_controller_ins|dq_wr_data[6]|q               ;
;   3.182 ;   0.000 ; RR ; IC   ; 1      ; IOOBUF_X37_Y29_N9       ; sram_dq[6]~output|i                               ;
;   5.645 ;   2.463 ; RR ; CELL ; 1      ; IOOBUF_X37_Y29_N9       ; sram_dq[6]~output|o                               ;
;   5.645 ;   0.000 ; RR ; CELL ; 1      ; PIN_G14                 ; sram_dq[6]                                        ;
+---------+---------+----+------+--------+-------------------------+---------------------------------------------------;

后一条路径从表面上看与前一条完全不同,但总的“时钟到输出”时间相差却不超过 35 ps。这并非巧合——FPGA 显然在硬件结构上就是按这种相似性来设计的。具体而言,上面的时序分析是在 slow 1200 mV、温度 100°C 的条件下做的;不过,即便换到其它分析条件下,这微小的差别也都保持一致。

本页由机器从英文翻译而来。如有疑问,请参阅原文
Copyright © 2021-2026. All rights reserved. (dcc38493)