大概八月份就開始想總結下控制邏輯的寫法了,然后開始找資料,沒有直接講這個的,零零散散的看了很多,斷斷續續的寫了很長時間,自閉無歲月···
就先寫一篇發出來吧,抓住2020的尾巴
所以上兩篇嘛,第一篇總結下控制邏輯,第二篇寫FSM狀態機,敬請期待 :)
全程都是菜鳥的理解,代碼也未經驗證,如果覺得不對歡迎指出來!
規模較大的設計一般劃分為若干子模塊,子模塊之間通過FIFO連接。FIFO的作用就是實現模塊間的rate ,匹配不同模塊的處理速率,從而實現模塊間的解耦,這樣每個模塊可以單獨設計控制邏輯,不需要考慮其他模塊的影響。
這些子模塊可以是一些實現具體功能的模塊,也可以是再劃分為若干子模塊,然后通過模塊級的流水實現控制,無論哪種方式,都需要和FIFO的空滿狀態打交道。
我們知道FIFO的空滿其實就是Valid/ready,所以對于模塊級流水而言,這些流水模塊一定是處于兩個FIFO之間的,這兩個FIFO就是流水的發端和收端。而且發端FIFO的Valid很獨立,沒有依賴于Ready;收端FIFO的Ready很獨立,沒有依賴于Valid;所以他們是完美的發端和收端,而中間這些流水模塊的Valid/ready有依賴關系是完全沒問題的,帶來的后果也僅僅是當Valid依賴ready時是ready valid,會有氣泡;而當ready依賴valid時是valid ready,沒有氣泡。而且如同我們下面介紹的ready valid時還可以有方法擠掉氣泡。
而如果兩個FIFO之間沒有再劃分子模塊,則可以將其視為模塊流水中的一級。
此外,對于Valid/ready傳輸協議,若是在一堆組合邏輯模塊傳來傳去,則都不用打拍的,直接一根線貫穿到底即可,但是這樣組合邏輯太長,會緊張,所以有了(模塊)流水線,在每一級模塊對數據和插入寄存器切斷組合邏輯,以跑更高的時鐘頻率;而一般不用打拍,但是由于它是一根信號驅動中間所有模塊的ready,所以如果流水級數多了會比較大邏輯驅動器是什么意思,而且還有gate ,進而也可能會有問題,所以這時也需要對ready打拍。
1 基本流水中間單元
如上所講,流水線的和分別是兩邊的FIFO,所以下面介紹的都是中間的單元邏輯驅動器是什么意思,他們傳遞到,傳遞到,在和握上手時對做一些處理,比如我們這里將其乘3。它們的模塊接口都如下:
module MiddlePipe #(parameter
DW = 10
)
(
//Interface
input Clk ,
input Clear ,
input Rstn ,
//In interface
input [DW-1:0] DataIn ,
input DataInVld ,
output DataInRdy ,
//Out interface
output [DW+1:0] DataOut ,
output DataOutVld ,
input DataOutRdy
);
//---------------------------------------------------------------------
reg data_in_rdy;
assign DataInRdy = data_in_rdy;
reg [DW+1:0] data_out;
assign DataOut = data_out;
reg data_out_vld;
assign DataOutVld = data_out_vld;
//---------------------------------------------------------------------
1.1 恒為1的情況
恒為1意味著不會有來自后級模塊的反壓,也即此時的模塊級流水和模塊內的流水線運算是一樣的,寫法如下:
//------Version 1: if DataOutRdy = 1-----------
always @ *
begin
data_in_rdy = 'h1;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out <= 'h0;
else if( Clear )
data_out <= 'h0;
else if( DataInVld )
data_out <= DataIn << 1 + DataIn;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out_vld <= 'h0;
else if( Clear )
data_out_vld <= 'h0;
else
data_out_vld <= DataInVld;
end
//------Version 1: if DataOutRdy = 1-----------
1.2 有 反壓的情況1.2.1 推理邏輯一
此時來自后級模塊的反壓,其實追跟到底是來自后面那個作為的FIFO的反壓,比如它滿了,則傳給前面的Ready都要拉下來以防丟數據。
前面我們提過,模塊級流水就是在valid/ready協議的valid和數據通路中插入寄存器切斷組合邏輯,ready要具有反壓能力自然要控制這些寄存器,以在ready=0時使其停下來,而且ready可以不打拍,所以邏輯如下:
//----------------------Version 2:without bubble collapse--------------------------

always @ *
begin
data_in_rdy = DataOutRdy;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out <= 'h0;
else if( Clear )
data_out <= 'h0;
else if( data_in_rdy && DataInVld )//backpressure:data_in_rdy &&
data_out <= DataIn << 1 + DataIn;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out_vld <= 'h0;
else if( Clear )
data_out_vld <= 'h0;
else if( data_in_rdy )//backpressure:data_in_rdy
data_out_vld <= DataInVld;
end
//------------------------------------------------------------------------------------
但是這樣valid通路插入了寄存器而ready沒有,所以是會有氣泡的,什么是氣泡?
我們看整個流水剛剛啟動時,此時中間插入的這些寄存器沒有任何有效數據,這時ready從0到1啟動流水,但卻不能立刻拿到數據,因為數據從出發往后傳,必須經過中間這些寄存器一級一級往后傳,這期間要一直拉高ready等待數據傳過來,等待的這些clk就是氣泡。
若是把ready拉下去是中間模塊還有沒排干凈的流水的話,那下次再把ready從0到1啟動時是可以立刻拿到上一次殘留的數據的,所以此時沒有氣泡。
但是一次傳輸一定會把流水排干凈的,比如一次傳輸16個數據,的valid累計16次和ready握上手以后就會拉低等待下一次傳輸,而端要真正收完16個才能把ready拉低,然后再開啟下一次傳輸,也即除了和的valid累計握手16次,還需繼續維持k-1個clk作為流水線排空時間(假設中間有K級流水),這樣才算完成一次傳輸。 所以我們可以說每一次傳輸啟動時都會有K個氣泡。
那怎么消除氣泡呢?由上可知每一級流水對應一個氣泡,所以只要每一個中間模塊都擠掉自己的氣泡即可。類似沒排干凈流水的狀態,即使的ready不來,前面這幾級流水也還是可先處理并存下K個數據的,所以只需要將寄存器工作的條件改為發了ready或者當前寄存器狀態為空(即=0),對應到代碼上就是只需改一下的邏輯:
//----------------------Version 3:with bubble collapse-----------------------------
always @ *
begin
data_in_rdy = DataOutRdy || ~data_out_vld;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out <= 'h0;
else if( Clear )
data_out <= 'h0;
else if( data_in_rdy && DataInVld )//backpressure:data_in_rdy &&
data_out <= DataIn << 1 + DataIn;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out_vld <= 'h0;
else if( Clear )
data_out_vld <= 'h0;
else if( data_in_rdy )//backpressure:data_in_rdy
data_out_vld <= DataInVld;
end
//------------------------------------------------------------------------------------
OK,上述理解思路是比較正統的,RTL寫法上變化一下就得到下面的代碼:
//----------------------Version 4: with bubble collapse-----------------------------
always @ *
begin
data_in_rdy = DataOutRdy || ~data_out_vld;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out <= 'h0;
else if( Clear )
data_out <= 'h0;
else
data_out <= ( data_in_rdy && DataInVld ) ? DataIn << 1 + DataIn : data_out;//backpressure:data_in_rdy
end
always @( posedge Clk or negedge Rstn )

begin
if( ~Rstn )
data_out_vld <= 'h0;
else if( Clear )
data_out_vld <= 'h0;
else
data_out_vld <= data_in_rdy ? DataInVld : data_out_vld;
end
//------------------------------------------------------------------------------------
兩種寫法綜合出來的面積不相上下,綜合出的電路的實現基本一致,而有些區別: 3是通過MUX選擇/作為D寄存器的輸入, 4是把信號寫成組合表達式作為D寄存器的輸入。
但是 3/4中的實現是不等價于下面這個的:
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out_vld <= 'h0;
else if( Clear )
data_out_vld <= 'h0;
else
data_out_vld <= ( data_in_rdy && DataInVld ) ? DataInVld : data_out_vld;
end
控制寄存器的是具有反壓能力的,與無關,與是否握手無關。
如果你不這樣覺得的話,畫一下這個寄存器的次態卡諾圖就明白了。
1.2.1 推理邏輯二
上面那個推理思路開始我是有點不明白的,因為我覺得只要有效了就可以往后傳了,為啥要等來了才能傳給?而且等不就是valid依賴了ready嗎?所以我開始寫的是這樣的:
//----------------------Version 5: Wrong--------------------------------------------------
always @ *
begin
data_in_rdy = DataOutRdy;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out <= 'h0;
else if( Clear )
data_out <= 'h0;
else if( data_in_rdy && DataInVld )//backpressure:data_in_rdy &&
data_out <= DataIn << 1 + DataIn;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out_vld <= 'h0;
else if( Clear )
data_out_vld <= 'h0;
else if( DataInVld )
data_out_vld <= 'h1;
else if( DataOutRdy && data_out_vld )
data_out_vld <= 'h0;
end
//------------------------------------------------------------------------------------
上面 5的寫法是有問題的,單從的邏輯來看是沒有問題的,它實現是擠掉氣泡以后的valid傳遞,和 3、4中功能一致。但是它往前傳的不對,在后面沒來要數據而且本級為空(因為雖然InVld傳過來了,但是并傳不到,所以本級是空的)時把往后傳了,但沒有向前面要數據,也即沒有往前傳ready;
修改一下,它就和擠掉氣泡之前的 2等價了,如下:
//----------------------Version 6: without bubble collapse-----------------------------
always @ *
begin
data_in_rdy = DataOutRdy;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out <= 'h0;
else if( Clear )
data_out <= 'h0;
else if( data_in_rdy && DataInVld )//backpressure:data_in_rdy &&
data_out <= DataIn << 1 + DataIn;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )

data_out_vld <= 'h0;
else if( Clear )
data_out_vld <= 'h0;
else if( data_in_rdy && DataInVld )//backpressure:data_in_rdy
data_out_vld <= 'h1;
else if( DataOutRdy && data_out_vld )
data_out_vld <= 'h0;
end
//------------------------------------------------------------------------------------
或者也可以將其修改為擠掉氣泡:
//----------------------Version 7: with bubble collapse-----------------------------
always @ *
begin
data_in_rdy = DataOutRdy || ~data_out_vld;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out <= 'h0;
else if( Clear )
data_out <= 'h0;
else if( data_in_rdy && DataInVld )//backpressure:data_in_rdy &&
data_out <= DataIn << 1 + DataIn;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out_vld <= 'h0;
else if( Clear )
data_out_vld <= 'h0;
else if( DataInVld )
data_out_vld <= 'h1;
else if( DataOutRdy && data_out_vld )
data_out_vld <= 'h0;
end
//------------------------------------------------------------------------------------
我開始之所以會這么想有兩個原因:
1、我沒能理解valid/ready不能相互依賴是針對兩端的/的,中間級即使依賴了(如上valid依賴ready)也不過是ready valid,產生氣泡,但不會死鎖;
2、誤解了
data_out_vld <= ( data_in_rdy && DataInVld ) ? DataInVld : data_out_vld;
可以簡化為
data_out_vld <= data_in_rdy ? DataInVld : data_out_vld;
也即沒有正確理解因為后級的ready要反壓,所以它就需要有能力控制寄存器在后級ready=0時可以將寄存器停下,注意是具有反壓能力的ready而不是握手成功( && )。
1.3 多個輸入模塊對多個輸出模塊
實際應用中模塊間握手信號的傳遞總是要比上面1.2介紹的基本模型要復雜,比如會有控制信號介入到valid/ready的傳遞,還會有多對多握手的情況。
下面我們由簡到繁,再往下走一走。
1.3.1 在valid/ready中加入一些控制信號
1.2中往后傳的和往前傳的基本上都是把前后級的和原封不動的傳了過去,只是控制了傳輸的時機,但實際中還會有控制信號介入到valid/ready的傳遞,所謂的控制信號就是本中間模塊的控制邏輯,是的模塊級流水中的一級流水不只是打一拍那么簡單,比如在一個中間模塊里還可以再有一個流水線運算,所以我們抽象出一個hold信號來參與valid/ready的傳遞,如下:
哦,對了,既然我們理解了推理思路一,下面的基本模型都用它了。
//----------------------Version 3:with bubble collapse-----------------------------
always @ *
begin
data_in_rdy = ( DataOutRdy || ~data_out_vld ) && ~hold;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out <= 'h0;
else if( Clear )
data_out <= 'h0;
else if( data_in_rdy && DataInVld )//backpressure:data_in_rdy &&
data_out <= DataIn << 1 + DataIn;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out_vld <= 'h0;
else if( Clear )

data_out_vld <= 'h0;
else if( data_in_rdy )//backpressure:data_in_rdy
data_out_vld <= DataInVld;
end
//------------------------------------------------------------------------------------
上面簡單的例子說明了除了兩端的/可以控制節奏外,中間模塊的控制邏輯也有能力喊停。
并且注意到模塊內部控制信號的是同時作用于往后傳的和往前傳的的。
1.3.2 多對多握手
我們假設N個前級模塊和M個后級模塊,顯然我們的問題還是->和->傳輸的問題,只不過現在是N個->M個和M個->N個。
為了明白N個->M個,我們先來看N個->1個:這意味著這一個后級模塊向N個前級模塊同時送了同一個ready,并期望與它們同時都握上手以便同時使用他們的數據,所以這就需要N個前級模塊的同時為1時才能和后級這個模塊握手。你可能會有點不太理解為啥N個前級模塊的要同時為1,因為這個后級模塊想同時使用他們的數據,所以它只發了一個ready前去,否則他就需要發N個ready分別給N個前級模塊,并且這時對于每個ready而言其實都是獨立的(使用它們數據的時刻也是獨立的)、一對一的,而不是N對1的握手。
所以N個->M個也是當N個都為1時才能向后傳,也即&[N-1:0],由于有M個后級模塊,所以要把它賦值M份[M-1:0]={M{&[N-1:0]}};
同理對于M個->N個,我們先來看M個->1個:這意味著M個后級模塊向這個前級模塊各自發送了一個ready,但是期望拿到同一筆數據,也就是和這個前級模塊的同一筆數據的valid握上手,那一種簡單的辦法就是M個后級模塊的ready同時來,這樣前級模塊的valid只需同時和M個ready握一下手即可;但其實M個后級模塊的ready也不必同時來,只要前級模塊在他們都握上手之前一直保持同一個數據的valid不更新即可,這就要求做一個寄存器統計M個模塊是否都成功握手,只有都成功時才傳給前級模塊的讓它更新下一個數據。
看到這里你也許會想為啥上面N個->1個時不用這種方法,用也是可以用的:類似的,后級的這個ready也在和N個前級模塊都握手手以后才算完成一次傳輸,但是對于前級模塊而言一旦握上手就意味著它可以更新數據了,但是后級模塊真正使用數據還要等和所有模塊都握手成功,這就要求還得做reg將數據存起來······這樣一比就知道為啥不用這種方法了,明明只要在N個前級模塊都準備好以前不和他們握手他們就可以自己保持數據,干嘛還要自己麻煩再存數據呢? 所以對于M個->N個,也是可以有兩種方法產生一個1bit信號:將M個與起來或者統計M個都握手成功,然后再將這1bit信號復制N份就可以傳給N個了。
另外要注意,由于多對多其M個有可能不會同時來,所以前級模塊的會一直維持到所有后級模塊都握上手才更新狀態,所以就要避免早到的多次握手。對于對于第一種方法(要求在所有都為1時才發生傳輸),那么在所有都為1之前的這段時間里不應該把前級的傳給后面,即使所以前級模塊的都為1;對于第二種方法(不要求所有模塊都在所有都為1時才發生傳輸),就需要在早到的握上手以后將傳到該模塊的拉下去,防止繼續握手。
上面我們總是將前級的所有valid都與起來才傳給后級,這樣做使用于前級N個模塊可以被后級任一模塊同時使用。
多對多的最終目的是確保后級都拿到同一批前級數據,然后前級數據才能更新。為做到這一點,可以用上述兩種方法往前傳更新ready;而前級的valid并不一定都需要與起來才能往后傳的,因為后級的一個模塊并不一定會用所有前級模塊的數據,所以當有確定的對應關系時,直接去對應的模塊握手就行,只是也要注意避免多次握手。
2 skid
skid有滑行的意思,在這里就是不是那種立即停下的急剎車,而是有緩沖的滑行一段時間才停下。
何時用到skid 呢?前面我們也提到模塊級流水所有模塊的ready都是來自,所以若是中間模塊太多或者中ready的邏輯太長,都會造成ready的緊張,這時就需要對Ready也打拍。但是對Ready也打拍后就會出現,后級想停下(拉低ready)但傳給前級會慢一拍,這樣前級就多握一次手,多向后傳一個數據,但后級已經停下了,所以就在本級做一個深度為1的緩沖將其存下來,等后級再啟動時先把它傳過去即可,這樣就避免了由于ready打拍造成數據丟失。
//----------------------Version 8: skid buffer-----------------------------
assign DataOut = DataInRdy ? data_out[0] : data_out[1] ;
assign DataOutVld = DataInRdy ? data_out_vld[0] : data_out_vld[1];
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
DataInRdy <= 'h0;
else if( Clear )
DataInRdy <= 'h0;
else
DataInRdy <= DataOutRdy;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out <= 'h0;
else if( Clear )
data_out <= 'h0;
else
begin
if( DataInVld && DataInRdy )
data_out[0] <= DataIn;
if( ~DataOutRdy && DataInRdy )//skid-buffer
data_out[1] <= data_out[0];
end
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out_vld <= 'h0;
else if( Clear )
data_out_vld <= 'h0;
else
begin
if( DataInRdy )
data_out_vld[0] <= DataInVld;
if( DataOutRdy && data_out_vld[1] )//skid-buffer
data_out_vld[1] <= 'h0;
else if( ~DataOutRdy && DataInRdy )//skid-buffer
data_out_vld[1] <= data_out_vld[0];
end
end
//------------------------------------------------------------------------------------
[0]在拉起來后接收數據,而[1]在~ && 時把[0]拿過來,因為此時[0]還會再接收下一個,這個就會被丟掉,所以放進[1]這個skid buf中正好。下一次再拉起來時,也要[1]先走。
注意到傳給時并沒有受到的控制,也即沒有受到握手的控制;而前面我們說傳給時一定要受到握手的控制,這是因為在沒有握上手時Vld自己是可以傳過去但沒有意義,因為并傳不過去,所以一定要受握手控制,保證Data和Vld的同步。
再注意到加上,那我們一級其實可以存兩個數據的,那對應著其實也會有兩個氣泡,上面這種寫法一個也沒有消去。
氣泡0:當=1而=0也即一段傳輸剛啟動時,這時沒有,傳不到到[0],下一拍就拿不到數據。
但是還有一個氣泡1:當和都等于0,也即一整段傳輸的開頭處,其實可以先放一個數據進,不然來的時候就需要等一拍才能拿到數據。
加上消除這兩個氣泡的邏輯后,代碼如下:
//----------------------Version 9: skid buffer and bubble collapse-----------------------------
assign DataOut = data_in_rdy ? data_out[0] : data_out[1] ;

assign DataOutVld = data_in_rdy ? data_out_vld[0] : data_out_vld[1];
assign DataInRdy = data_in_rdy || ~data_out_vld[1] || ( DataOutRdy && ~data_out_vld[0] );
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_in_rdy <= 'h0;
else if( Clear )
data_in_rdy <= 'h0;
else
data_in_rdy <= DataOutRdy;
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out <= 'h0;
else if( Clear )
data_out <= 'h0;
else
begin
if( DataInVld && ( data_in_rdy || ( DataOutRdy && ~data_out_vld[0] ) )//Bubble_0 clamping
data_out[0] <= DataIn;
if( ~DataOutRdy && ~data_out_vld[1] )//Bubble_1 clamping
data_out[1] <= data_in_rdy ? data_out[0] : DataIn;
end
end
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_out_vld <= 'h0;
else if( Clear )
data_out_vld <= 'h0;
else
begin
if( data_in_rdy || ( DataOutRdy && ~data_out_vld[0] ) )//Bubble_0 clamping
data_out_vld[0] <= DataInVld;
if( DataOutRdy && data_out_vld[1] )//skid-buffer
data_out_vld[1] <= 'h0;
else if( ~DataOutRdy && ~data_out_vld[1] )//skid-buffer
data_out_vld[1] <= data_in_rdy ? data_out_vld[0] : DataInVld;
end
end
//------------------------------------------------------------------------------------
關于上面兩個氣泡,簡單畫個時序圖示意如下:
上述同時消除了兩個氣泡,但是是把后級傳來的rdy打拍后又加了組合邏輯。要把消去氣泡的邏輯加到打拍處,一個可以,兩個同時加的話會出現問題:有兩個位置,先進哪個?怎么保證?我沒想到好辦法···歡迎賜教!
要是只消除一個,比如消除氣泡1,可以這么寫:
always @( posedge Clk or negedge Rstn )
begin
if( ~Rstn )
data_in_rdy <= 'h0;
else if( Clear )
data_in_rdy <= 'h0;
else
data_in_rdy <= DataOutRdy || (~data_out_vld[1] && ~data_in_rdy);
end
3 死鎖
我們知道對于valid/ready協議的兩端,如果的valid依賴于的ready,而的ready依賴于的valid,那整個通路就會死鎖。我們上面說我們模塊級流水是以FIFO為邊界的,也就是我們的和都是FIFO,所以我們的valid不依賴與的ready,的ready也不依賴于的valid,這是最好的情況,但是也是會死鎖的。
FIFO就是用來匹配前后兩個大單元的處理速度的,滿了就使前面停一停,等后面消耗一下再開始;空了就使后面停一停,等前面生產一些再開始。但是如果FIFO前面生產單元的工作需要后面消費單元的工作,或者后面消耗單元的工作需要依賴前面生產單元的工作,也就是形成環路的時候,那么在FIFO空/滿時就會產生死鎖。增加FIFO容量避免空滿當然可以解決問題,但是FIFO的容量是根據前后處理速度以及最大burst持續時間決定的,為解決死鎖而增大容量性價比不高,況且還是會存在死鎖的可能,除非能確定增加多少容量可以保證FIFO不空/不滿。所以切斷環路,讓生產單元和消費單元不要相互依賴才是更好的解決辦法。
4 Valid/ready 撤銷
1.Valid 拉起來就不允許撤銷;
2.而Ready卻可以隨時撤銷,待再準備好再拉起來。
所以如果從后級Ready拉起來(發來請求)到和它握上手(給他vld)所需要的時間大于1拍,比如從RAM讀數據或者模塊級流水的某以中間級有內部流水,就需要考慮rdy撤銷的情況。比如內部有流水時,就需要在每一個流水級都與上Ready控制,這樣當Ready撤銷時,大家都停下來等它。
?
注:這篇讀起來也像是一個系列,嘗試找了一下,好像是原來發在論壇上的,然而這個論壇已經沒了···
[設計Valid-Ready握手協議]
[ 11: for Links, μ, ](CS250, UC , Fall 2011)
最后還要感謝工作中前輩的不吝賜教!
?