Verilog基本电路设计_摘

Verilog基本电路设计之一:bit跨时钟域同步

(帖子链接:bbs.eetop.cn/thread-605419-1-1.html)

看到坛子里不少朋友,对于基本数字电路存在这样那样的疑惑,本人决定开贴,介绍数字电路最常见的模块单元,希望给初学者带来帮助,也欢迎大佬们前来拍砖。如果想要做数字设计,下面这些电路是一定会碰到的,也是所有大型IP,SOC设计必不可少的基础,主要包括异步信号的同步处理,同步FIFO,异步FIFO,时钟无缝切换,信号滤波debounce等等,后面会根据大家反馈情况再介绍新电路。

首先介绍异步信号的跨时钟域同步问题。一般分为单bit的控制信号同步,以及多bit的数据信号同步。多bit的信号同步会使用异步FIFO完成,而单bit的信号同步,又是时钟无缝切换电路以及异步FIFO电路的设计基础,这里先介绍单bit信号同步处理。

clka域下的信号signal_a,向异步的clkb域传递时,会产生亚稳态问题。所有的亚稳态,归根结底就是setup/hold时间不满足导致。在同一个时钟域下的信号,综合以及布线工具可以在data路径或者clock路径上插入buffer使得每一个DFF的setup/hold时间都满足;但是当signal_a在clkb域下使用时,由于clka与clkb异步,它们的相位关系不确定,那么在clkb的时钟沿到来时,无法确定signal_a此时是否处于稳定无变化状态,也即setup/hold时间无法确定,从而产生亚稳态。这种异步信号在前后端流程里面是无法做时序分析的,也就是静态时序分析里常说的false_path。

消除亚稳态,就是采用多级DFF来采样来自另一个时钟域的信号,级数越多,同步过来的信号越稳定。对于频率很高的设计,建议至少用三级DFF,而两级DFF同步则是所有异步信号处理的最基本要求。

单bit的信号跨时钟域同步,又分成电平信号同步以及脉冲信号同步。电平信号,就是说clka下的信号signal_a在clkb看来,是一个很宽的信号,会保持多个clkb的时钟周期,一定能被clkb采到。这种情况,只需要使用clkb用至少两级DFF连续抓signal_a即可,特别需要强调的是,此时signal_a必须是clka下的寄存器信号,如果signal_a是clka下的组合逻辑信号,一定要先在clka下用DFF抓一拍,再使用两级DFF向clkb传递。这是因为clka下的组合逻辑信号会有毛刺,在clka下使用时会由setup/hold时间保证毛刺不会被clka采到,但由于异步相位不确定,组合逻辑的毛刺却极有可能被clkb采到。电平信号的同步处理,一般用于知道确定的时钟频率大小关系或者极慢时钟下的信号向极快时钟域传递时使用,简单处理如下:

always @ (posedge clkb or negedgerst_n)begin

if (!rst_n) begin

levl_b_d1 <= #DLY 1'b0;

levl_b_d2 <= #DLY 1'b0;

levl_b_d3 <= #DLY 1'b0;

end

else begin

levl_b_d1 <= #DLY levl_a_in;

levl_b_d2 <= #DLY levl_b_d1;

levl_b_d3 <= #DLY levl_b_d2;

endend

assign puls_b_pos = levl_b_d2 & (~levl_b_d3);

assign puls_b_neg = levl_b_d3 &(~levl_b_d2);

assign levl_b_out  = levl_b_d2;

上面三个输出分别是经过同步之后,clkb下可以使用的0变1脉冲信号,1变0脉冲信号以及电平信号。再次强调:levl_a_in必须是clka的DFF信号!下面是更常见的,clka下的脉冲信号,同步到clkb时钟域下,它对于clka与clkb的时钟频率关系没有任何限制,快到慢,慢到快都没问题。其主要原理就是先把脉冲信号在clka下展宽,变成电平信号,再向clkb传递,当确认clkb已经“看见”信号同步过去之后,再清掉clka下的电平信号。脉冲信号同步处理电路,有两个地方使用了上面的电平信号同步处理原则,请仔细揣摩原因。详细见下面的RTL,其中省略了信号定义声明:

module sync_pulse (

// input

rst_n,        // system reset

clka,         // clockA

clkb,         // clockB

puls_a_in,    // pulse input from clka

// output

puls_b_out,  // pulse output in clkb

levl_b_out    // level output in clkb

);

parameter DLY =  1;  //

always @ (posedge clka or negedge rst_n)

begin

if (rst_n == 1'b0)

signal_a <= # DLY 1'b0 ;

else if (puls_a_in)

signal_a <= # DLY 1'b1 ;

else if (signal_b1_a2)

signal_a <= # DLY 1'b0 ;

else ;

end

always @ (posedge clkb or negedge rst_n)

begin

if (rst_n == 1'b0)

signal_b <= # DLY 1'b0 ;

else

signal_b <= # DLY signal_a ;

end

always @ (posedge clkb or negedge rst_n)

begin

if (rst_n == 1'b0) begin

signal_b_b1 <= # DLY 1'b0 ;

signal_b_b2 <= # DLY 1'b0 ;

end

else begin

signal_b_b1 <= # DLY signal_b ;

signal_b_b2 <= # DLY signal_b_b1 ;

end

end

always @ (posedge clka or negedge rst_n)

begin

if (rst_n == 1'b0) begin

signal_b1_a1 <= # DLY 1'b0 ;

signal_b1_a2 <= # DLY 1'b0 ;

end

else begin

signal_b1_a1 <= # DLY signal_b_b1 ;

signal_b1_a2 <= # DLY signal_b1_a1 ;

end

end

assign puls_b_out = signal_b_b1 & (~signal_b_b2) ;

assign levl_b_out = signal_b_b1 ;

endmodule

下一篇讲时钟切换电路。

留下一个思考题:clka下的同一个寄存器信号signal_a,电平宽度对clkb而言足够长,如果同时调用两个相同的电平同步模块向clkb时钟传递,分别得到levl_b1和levl_b2,那么在clkb时钟域下看到的lev_b1和levl_b2信号是否一样?

这个问题是实际设计中一不小心就会犯错的,如果能够想明白正确回答这个问题,异步信号的理解就可以过关了。

Verilog基本电路设计之二:时钟无缝切换

(帖子链接:http://bbs.eetop.cn/thread-605514-1-1.html)

时钟切换分成两种方式,普通切换和去毛刺无缝切换。

普通切换,就是不关心切出的时钟是否存在毛刺,这种方式电路成本小。如果时钟切换时,使用此时钟的模块电路处于非工作状态,或者模块内电路被全局复位信号reset住的,即使切出毛刺也不会导致DFF误触发,这样的模块可以选择用此种切换方式。

写法很简单 assign clk_o = sel_clkb ? clkb : clka ,当sel_clkb为1时选择clkb,否则选择clka。不过在实际设计中,建议直接调用库里的MUX单元set_dont_touch,不要采用这里的assign写法,因为这种写法最后综合得到的可能不是MUX而是复杂组合逻辑,给前后端流程的时钟约束和分析带来不便。

无缝切换,就是切换时无毛刺时钟平稳过渡。在时钟切换中,只要出现比clka或者clkb频率更高的窄脉冲,不论是窄的高电平还是窄的低电平,都叫时钟毛刺。工作在切换后时钟clk_o下的电路模块,综合约束是在max{clka,clkb}频率下的,也就是说设计最后signoff的时候,只保证电路可以稳定工作的最高频率是max{clka,clkb},如果切换中出现更高频的时钟毛刺,电路可能出现无法预知的结果而出错。无缝切换,一般用在处于工作状态的模块需要调频或者切换时钟源,比如内部系统总线,CPU等。你刚用手机打完游戏后马上关屏听音乐,这两种场景中,CPU在满足性能前提下为了控制功耗,其工作频率会动态地从很高调至较低,此时就可能是在CPU一直处于工作状态下,通过无缝切换时钟源头实现的。

在无缝切换电路中,切换信号sel_clkb可以是任意时钟域下的信号,包括但不限于clka或者clkb域,但是sel_clkb必须是一个DFF输出 信号;clka与clkb的频率大小相位关系可以任意。无缝切换需要解决两个问题,一是异步切换信号的跨时钟域同步问题,这里需要使用《Verilog基本电路设计之一》里的同步电路原理消除亚稳态;二是同步好了的切换信号与时钟信号如何做逻辑,才能实现无毛刺。

下面写出无缝切换电路的主体部分,忽略了内部信号的定义声明等。

module clk_switch (

rst_n,

clka,

clkb,

sel_clkb,

clk_o

);

always @ (posedge clka or negedge rst_n)

begin

if (!rst_n) begin

sel_clka_d0 <= 1'b0;

sel_clka_d1 <= 1'b0;

end

else begin

sel_clka_d0 <= (~sel_clkb) &(~sel_clkb_dly3) ;

sel_clka_d1 <= sel_clka_d0 ;

end

end

// part2

//always @ (posedge clka_n or negedge rst_n)

always @ (posedge clka or negedge rst_n)

begin

if (!rst_n) begin

sel_clka_dly1 <= 1'b0;

sel_clka_dly2 <= 1'b0;

sel_clka_dly3 <= 1'b0;

end

else begin

sel_clka_dly1 <= sel_clka_d1;

sel_clka_dly2 <= sel_clka_dly1 ;

sel_clka_dly3 <= sel_clka_dly2 ;

end

end

// part3

//always @ (posedge clkb_n or negedge rst_n)

always @ (posedge clkb or negedge rst_n)

begin

if (!rst_n) begin

sel_clkb_d0 <= 1'b0;

sel_clkb_d1 <= 1'b0;

end

else begin

sel_clkb_d0 <= sel_clkb &(~sel_clka_dly3) ;

sel_clkb_d1 <= sel_clkb_d0 ;

end

end

// part4

//always @ (posedge clkb_n or negedge rst_n)

always @ (posedge clkb or negedge rst_n)

begin

if (!rst_n) begin

sel_clkb_dly1 <= 1'b0;

sel_clkb_dly2 <= 1'b0;

sel_clkb_dly3 <= 1'b0;

end

else begin

sel_clkb_dly1 <= sel_clkb_d1  ;

sel_clkb_dly2 <= sel_clkb_dly1 ;

sel_clkb_dly3 <= sel_clkb_dly2 ;

end

end

// part5

clk_gate_xxx clk_gate_a ( .CP(clka), .EN(sel_clka_dly3),.Q(clka_g)  .TE(1'b0) );

clk_gate_xxx clk_gate_b ( .CP(clkb), .EN(sel_clkb_dly3),.Q(clkb_g)  .TE(1'b0) );

//assign clka_g = clka & sel_clka_dly3 ;

//assign clkb_g = clkb & sel_clkb_dly3 ;

assign clk_o = clka_g | clkb_g ;

endmodule

上面是我认为比较合理的无缝切换电路,其他切换方式跟这个会有些许出入,但基本大同小异原理是一样的。有几点说明:

1、抛开注释掉的电路不看,由于part5部分直接调用库里的clockgating cell,使得整个切换电路全部只需要用到时钟上升沿,无需额外定义反向时钟,精简了DC综合的时钟约束;直接调用gating cell的 另一个好处是,前后端工具会自动检查gating cell的CP信号与EN信号的setup/hold时间,使得gating后的Q时钟输出无毛刺尖峰。TE端可以根据实际需要接上scan测试模式信号。如果使用part5部分的gating cell实现,前面的part1,2,3,4全部替换成注释掉的反相时钟也是没有问题。

2、part2和part4部分,具体需要多少级DFF,甚至完全不要也是可以的,这就回到了《Verilog基本电路设计之一》里讨论的到底多少级DFF消除亚稳态才算合理的问题。时钟频率很低可能无所谓,如果时钟频率达到GHz,这部分建议至少保留三级DFF,因为三级DFF延时也仅仅只有3ns的时间裕度。没必要为了省这么几个DFF降低电路可靠性,在复杂IP以及大型SOC系统中,你会发现多几十个DFF,面积上可以忽略,系统可靠性和稳定性才是首要的。

3、如果part5部分希望使用注释掉的两行“与”逻辑实现时钟gating,此时part1与part3使用正相或者反相时钟都可以,但是必须把part2和part4部分改为注释掉的反相时钟实现,目的是初步从RTL设计上避免“与”逻辑的毛刺,同时还需要后端配合,因为很多后端工具对时钟“与”逻辑的clockgating check未必会检查。用clk下降沿拍出的en信号,再跟clk做与逻辑得到的门控时钟,在RTL仿真阶段看到的一定不会有毛刺,但是布线完成后,如果clk相对en后移,那与逻辑得到的门控时钟就有毛刺了。这就是用与逻辑做门控的缺点,由于后端工具可能不会去检查这个与门的时序关系而导致出错。但直接调用库里的gating cell,工具天然就会去检查这个时序,免去人工确认的后顾之忧。

最后,请大家仔细看看sel_clka_d0 <= (~sel_clkb) & (~sel_clkb_dly3)  和sel_clkb_d0 <= sel_clkb & (~sel_clka_dly3) 这两处逻辑,按理说,sel_clkb跟sel_clka_dly3以及sel_clkb_dly3之间相互都是异步的,而按照异步信号同步处理原则,两个不同时钟域下的信号是不允许直接做组合逻辑的,为什么这里可以这样使用?

Verilog基本电路设计之三:异步FIFO

(帖子链接:http://bbs.eetop.cn/thread-605632-1-1.html)

FIFO用于为匹配读写速度而设置的数据缓冲buffer,当读写时钟异步时,就是异步FIFO。多bit的数据信号,并 不是直接从写时钟域同步到读时钟域的,而是读写时钟域分别派遣了一个信使,去通知对方时钟域,当前本方所处的读写情况,来判断还能不能写以及可不可以读,这两个信使就是读写指针。

在《Verilog基本电路设计之一》里已讨论过,即使单bit的异步信号,通过两个相同的同步电路,达到clkb域时都可能“长”的不是一个模样,更加不用说多bit的异步信号同时传递到clkb域会变成什么五花八门的模样了。这里读写指针不是单bit信号,它们如何向对方时钟域去同步呢?格雷码!它的特点是每次只有一个bit发生变化,这样就把多bit信号同步转变为了单bit信号同步,这也是为什么多bit的格雷码信号,可以类似于单bit信号那样,直接使用两级DFF去同步的根本原因。

下面给出异步FIFO的主体部分,同样,省略了信号声明定义。

module asyn_fifo (

// input

af_wclk , // async-FIFO clear in write clock

af_rclk , // async-FIFO clear in read clock

rst_n,    // system reset

af_wr_en, // async-FIFO write enable

af_rd_en, // async-FIFO read enable

af_dati,  // async-FIFO data in

//output

af_full , // Async-FIFO full flag

af_empty, // Async-FIFO empty flag

af_dato   // Async-FIFO data out

);

//------------------------- data input --------------------------

assign nxt_wptr_wclk = (af_wr_en && !af_full) ? (wptr_wclk + 1'b1) :wptr_wclk ;

assign nxt_wptr_gray = (nxt_wptr_wclk >> 1) ^ nxt_wptr_wclk ;

always @ (posedge af_wclk or negedge rst_n)

begin

if (rst_n == 1'b0) begin

wptr_wclk <= # DLY 5'b0 ;

wptr_gray <= # DLY 5'b0 ;

end

else begin

wptr_wclk <= # DLY nxt_wptr_wclk ;

wptr_gray <= # DLY nxt_wptr_gray ;

end

end

reg  [31:0]     ram[15:0]    ; //

always @ (posedge af_wclk)

begin

if (af_wr_en == 1'b1)

ram[wptr_wclk[3:0]] <= # DLY af_dati ;

else ;

end

//------------------------ data output ---------------------------

assign nxt_rptr_rclk = (af_rd_en && !af_empty) ? (rptr_rclk + 1'b1) :rptr_rclk ;

assign nxt_rptr_gray = (nxt_rptr_rclk >> 1) ^ nxt_rptr_rclk ;

always @ (posedge af_rclk or negedge rst_n)

begin

if (rst_n == 1'b0) begin

rptr_rclk <= # DLY 5'b0 ;

rptr_gray <= # DLY 5'b0 ;

end

else begin

rptr_rclk <= # DLY nxt_rptr_rclk ;

rptr_gray <= # DLY nxt_rptr_gray ;

end

end

assign af_dato = ram[rptr_rclk[3:0]] ;

// sync read pointer

always @ (posedge af_wclk or negedge rst_n)

begin

if (rst_n == 1'b0) begin

rptr_sp1  <= # DLY 5'b0 ;

rptr_sp2  <= # DLY 5'b0 ;

end

else begin

rptr_sp1  <= # DLY rptr_gray;

rptr_sp2  <= # DLYrptr_sp1  ;

end

end

// sync write pointer

always @ (posedge af_rclk or negedge rst_n)

begin

if (rst_n == 1'b0) begin

wptr_sp1  <= # DLY 5'b0 ;

wptr_sp2  <= # DLY 5'b0 ;

end

else begin

wptr_sp1  <= # DLY wptr_gray;

wptr_sp2  <= # DLY wptr_sp1  ;

end

end

assign af_full  = (wptr_gray =={~rptr_sp2[4],~rptr_sp2[3],rptr_sp2[2:0]}) ;

assign af_empty = (rptr_gray == wptr_sp2) ;

assign wptr_bin[4]  =   wptr_sp2[4] ;

assign wptr_bin[3]  =   (^wptr_sp2[4:3]) ;

assign wptr_bin[2]  =   (^wptr_sp2[4:2]) ;

assign wptr_bin[1]  =   (^wptr_sp2[4:1]) ;

assign wptr_bin[0]  =   (^wptr_sp2[4:0]) ;

assign rptr_bin[4]  =   rptr_sp2[4] ;

assign rptr_bin[3]  =   (^rptr_sp2[4:3]) ;

assign rptr_bin[2]  =   (^rptr_sp2[4:2]) ;

assign rptr_bin[1]  =   (^rptr_sp2[4:1]) ;

assign rptr_bin[0]  =   (^rptr_sp2[4:0]) ;

assign af_wlevel = wptr_wclk - rptr_bin ;

assign af_rlevel = wptr_bin - rptr_rclk ;

assign af_half_full = (af_rlevel >= 5'h7) ;

endmodule

上面给出的是深度16,宽度32的示例,大家可以使用parameter参数化定义深度和宽度,方便不同需求下的调用。除了空满信号标志,也可以根据需要做出半空半满之类信号。上面需要注意的一点就是,格雷码必须在本时钟域下DFF输出,再往另一个时钟域同步。同步FIFO呢,就不用有格雷码转换,设计更加简单,就不专门开贴描述了。

Verilog基本电路设计之四:去抖滤波

(帖子链接:http://bbs.eetop.cn/thread-605729-1-1.html)

debounce电路,就是常说的去抖滤波,主要用在芯片的PAD输入信号,或者模拟电路输出给数字电路的信号上。

parameter BIT_NUM  = 4 ;

reg [BIT_NUM-1 : 0] signal_deb ; //

always @ (posedge clk or negedge rst_n)

begin

if (rst_n == 1'b0)

signal_deb <= {BIT_NUM{1'b0}} ;

else

signal_deb <= # DLY {signal_deb[BIT_NUM-2:0],signal_i} ;

end

always @ (posedge clk or negedge rst_n)

begin

if (rst_n == 1'b0)

signal_o <= 1'b1 ;

else if (signal_deb[3:1]==3'b111)

signal_o <= # DLY 1'b1 ;

else if (signal_deb[3:1]==3'b000)

signal_o <= # DLY 1'b0 ;

else ;

end

上面的电路,第一个always,还兼顾了去亚稳态作用。它可以滤掉的宽度是两个clk的cycle,对于大于两个cycle而小于三个cycle的信号,有些可以滤掉,有些不能滤掉,这与signal_i相对clk的相位有关。

根据希望滤除的宽度相关,换算到clk下是多少个cycle数,从而决定使用多少级DFF。如果希望滤除的宽度相对 cycle数而言较大,可以先在clk下做一个计数器,产生固定间隔的脉冲,再在脉冲信号有效时使用多级DFF去抓signal_i;或者直接将clk分 频后再使用。

摘自网上资源,侵删。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 一个VHDL程序代码包含实体(entity)、结构体(architecture)、配置(configuration...
    洛神红梅花果茶丿阅读 7,642评论 0 2
  • fpga规范 工作过的朋友肯定知道,公司里是很强调规范的,特别是对于大的设计(无论软件还是硬件),不按照规范走几乎...
    Michael_Johnson阅读 1,869评论 1 4
  • testbench 1. 激励的产生 对于testbench而言,端口应当和被测试的module一一对应。端口分为...
    Michael_Johnson阅读 2,653评论 0 1
  • 1 人有时 ,一半是天使,一半是魔鬼。 沉静的时候,枉如安静的婴儿那么美;愤怒的时候,像一只张牙舞爪的狮子,逮了谁...
    YIBAO阅读 473评论 0 6
  • 《美人为馅》中,徐司白令人记忆犹新。他,是温柔高贵的医生,是血腥残忍的黑暗帝王,是白锦曦的好闺蜜。可是,那又如何?...
    上官冰儿阅读 394评论 3 4