Verylで作るCPU
Star

第12章
A拡張の実装

本章では、メモリの不可分操作を実現するA拡張を実装します。A拡張にはLoad-Reserved、Store-Conditionalを実現するZalrsc拡張(表12.2)、ロードした値を加工し、その結果をメモリにストアする操作を単一の命令で実装するZaamo拡張(表12.1)が含まれています。A拡張の命令を利用すると、同じメモリ空間で複数のソフトウェアを並列、並行して実行するとき、ソフトウェア間で同期をとりながら実行できます。

12.1 アトミック操作

12.1.1 アトミック操作とは何か?

アトミック操作(Atomic operation、不可分操作)とは、他のシステムからその操作を観測するとき、1つの操作として観測される操作のことです。つまり、他のシステムは、アトミック操作を行う前、アトミック操作を行った後の状態しか観測できません。

<span class="imgref"><a href="./13-impl-a.html#sampleprogram_1cpu">図12.2</a></span>のプログラムを2つに分割して2つのCPUで実行する (Xは11になる)

図12.1: 図12.2のプログラムを2つに分割して2つのCPUで実行する (Xは11になる)

1つのCPUでメモリ上の値を2回インクリメントする (Xは12になる)

図12.2: 1つのCPUでメモリ上の値を2回インクリメントする (Xは12になる)

アトミック操作は実行、観測される順序が重要なアプリケーションで利用します。例えば、アドレスXの値をロードして1を足した値を書き戻すプログラムを、2つのコアで同時に実行するとします(図12.1)。このとき命令の実行順序によっては、最終的な値が1つのコアで2回プログラムを実行した場合と異なってしまいます(図12.2)。この状態を避けるためにはロード、加算、ストアをアトミックに行う必要があります。このアトミック操作の実現方法として、A拡張はAMOADD命令、LR命令とSC命令を提供します。

12.1.2 Zaamo拡張

Zaamo拡張は、値をロードして、演算した値をストアする操作を1つの命令で行う命令を定義しています。AMOADD命令はロード、加算、ストアを行う単一の命令です。Zaamo拡張は他にも簡単な操作を行う命令も提供しています。

表12.1: Zaamo拡張の命令

命令動作 (読み込んだ値をレジスタにライトバックする)
AMOSWAP.W/Dメモリから32/64ビット読み込み、
rs2の値を書き込む
AMOADD.W/Dメモリから32/64ビット(符号付き)読み込み
rs2(符号付き)の値を足して書き込む
AMOAND.W/Dメモリから32/64ビット読み込み
rs2の値をAND演算して書き込む
AMOOR.W/Dメモリから32/64ビット読み込み
rs2の値をOR演算して書き込む
AMOXOR.W/Dメモリから32/64ビット読み込み
rs2の値をXOR演算して書き込む
AMOMIN.W/Dメモリから32/64ビット(符号付き)読み込み
rs2(符号付き)の値と比べて小さい値を書き込む
AMOMAX.W/Dメモリから32/64ビット(符号付き)読み込み
rs2(符号付き)の値と比べて大きい値をを書き込む
AMOMINU.W/Dメモリから32/64ビット(符号無し)読み込み
rs2(符号無し)の値と比べて小さい値を書き込む
AMOMAXU.W/Dメモリから32/64ビット(符号無し)読み込み
rs2(符号無し)の値と比べて大きい値を書き込む

12.1.3 Zalrsc拡張

Zalrsc拡張は、LR命令とSC命令を定義しています。LR、SC命令は、それぞれLoad-Reserved、Store-Conditional操作を実現する命令です。それぞれ次のように動作します。

LR命令
指定されたアドレスのデータを読み込み、指定されたアドレスを予約セット(Reservation set)に登録します。 ロードしたデータをレジスタにライトバックします。
SC命令
指定されたアドレスが予約セットに存在する場合、指定されたアドレスにデータを書き込みます(ストア成功)。 予約セットにアドレスが存在しない場合は書き込みません(ストア失敗)。 ストアに成功したら0、失敗したら0以外の値をレジスタにライトバックします。 命令の実行後に必ず予約セットを空にします。

LR、SC命令を使うことで、アトミックなロード、加算、ストアを次のように記述できます(リスト12.1)。

リスト12.1: リスト12.1: LR、SC命令によるアトミックな加算
1: atomic_add:
2:     LR.W x2, (x3) ← アドレスx3の値をx2にロード
3:     ADDI x2, x2, 1 ← x2に1を足す
4:     SC.W x4, x2, (x3) ← ストアを試行し、結果をx4に格納
5:     BNEZ x4, atomic_add ← SC命令が失敗していたらやり直す

例えば同時に2つのコアがリスト12.1を実行するとき、同期をとれていない書き込みはSC命令で失敗します。失敗したらLR命令からやり直すことで、1つのコアで2回実行した場合と同一の結果(1を2回加算)になります。

予約セットのサイズは実装によって異なります。

表12.2: Zalrsc拡張の命令

命令動作
LR.W/Dメモリから32/64ビット読み込み、予約セットにアドレスを登録する
読み込んだ値をレジスタにライトバックする
SC.W/D予約セットにrs1の値が登録されている場合、メモリにrs2の値を書き込み
0をレジスタにライトバックする。予約セットにアドレスが登録されていない場合
メモリに書き込まず、0以外の値をレジスタにライトバックする。
命令の実行後に予約セットを空にする

12.1.4 命令の順序

A拡張の命令のビット列は、それぞれ1ビットのaq、rlビットを含んでいます。このビットは、他のコアやハードウェアスレッドからメモリ操作を観測したときにメモリ操作がどのような順序で観測されるかを制御するものです。

A拡張の命令をAとするとき、それぞれのビットの状態に応じて、Aによるメモリ操作は次のように観測されます。

aq=0、rl=0
Aの前後でメモリ操作の順序は保証されません。
aq=1、rl=0
Aの後ろにあるメモリを操作する命令は、Aのメモリ操作の後に観測されることが保証されます。
aq=0、rl=1
Aのメモリ操作は、Aの前にあるメモリを操作する命令が観測できるようになった後に観測されることが保証されます。
aq=1、rl=1
Aのメモリ操作は、Aの前にあるメモリを操作する命令よりも後、Aの後ろにあるメモリを操作する命令よりも前に観測されることが保証されます。

今のところ、CPUはメモリ操作を1命令ずつ直列に実行するため、常にaqが1、rlが1であるように動作します。そのため、本章ではaq、rlビットを考慮しないで実装を行います*1

[*1] メモリ操作の並び替えによる高速化は応用編で検討します。

12.2 命令のデコード

A拡張の命令はすべてR形式で、opcodeはOP-AMO(7'b0101111)です。それぞれの命令はfunct5(リスト12.3)とfunct3(Wは2、Dは3)で区別できます。

eeiパッケージにOP-AMOの定数を定義します(リスト12.2)。

リスト12.2: リスト12.2: OP-AMOの定義 (eei.veryl)
1:     const OP_AMO      : logic<7> = 7'b0101111;

A拡張の命令を区別するための列挙型AMOOpを定義します(リスト12.3)。それぞれ命令のfunct5と対応しています。

リスト12.3: リスト12.3: AMOOp型の定義 (eei.veryl)
1:     enum AMOOp: logic<5> {
2:         LR = 5'b00010,
3:         SC = 5'b00011,
4:         SWAP = 5'b00001,
5:         ADD = 5'b00000,
6:         XOR = 5'b00100,
7:         AND = 5'b01100,
8:         OR = 5'b01000,
9:         MIN = 5'b10000,
10:         MAX = 5'b10100,
11:         MINU = 5'b11000,
12:         MAXU = 5'b11100,
13:     }

12.2.1 is_amoフラグを実装する

InstCtrl構造体に、A拡張の命令であることを示すis_amoフラグを追加します(リスト12.4)。

リスト12.4: リスト12.4: InstCtrlにis_amoを定義する (corectrl.veryl)
1:     struct InstCtrl {
2:         itype    : InstType   , // 命令の形式
3:         rwb_en   : logic      , // レジスタに書き込むかどうか
4:         is_lui   : logic      , // LUI命令である
5:         is_aluop : logic      , // ALUを利用する命令である
6:         is_muldiv: logic      , // M拡張の命令である
7:         is_op32  : logic      , // OP-32またはOP-IMM-32である
8:         is_jump  : logic      , // ジャンプ命令である
9:         is_load  : logic      , // ロード命令である
10:         is_csr   : logic      , // CSR命令である
11:         is_amo   : logic      , // AMO instruction
12:         funct3   : logic   <3>, // 命令のfunct3フィールド
13:         funct7   : logic   <7>, // 命令のfunct7フィールド
14:     }

命令がメモリにアクセスするかを判定するinst_is_memop関数を、is_amoフラグを利用するように変更します(リスト12.5)。

リスト12.5: リスト12.5: A拡張の命令がメモリにアクセスする命令と判定する (corectrl.veryl)
1:     function inst_is_memop (
2:         ctrl: input InstCtrl,
3:     ) -> logic {
4:         return ctrl.itype == InstType::S || ctrl.is_load || ctrl.is_amo;
5:     }

inst_decoderモジュールのInstCtrlを生成している部分を変更します。opcodeがOP-AMOのとき、is_amoTに設定します(リスト12.6)。その他のopcodeのis_amoFに設定してください。

リスト12.6: リスト12.6: is_amoフラグを追加する (inst_decoder.veryl)
1:                 OP_SYSTEM: {
2:                     InstType::I, T, F, F, F, F, F, F, T, F
3:                 },
4:                 OP_AMO: {
5:                     InstType::R, T, F, F, F, F, F, F, F, T
6:                 },
7:                 default: {
8:                     InstType::X, F, F, F, F, F, F, F, F, F
9:                 },

また、A拡張の命令が有効な命令として判断されるようにします(リスト12.7)。

リスト12.7: リスト12.7: A拡張の命令のとき、validフラグを立てる (inst_decoder.veryl)
1:             OP_MISC_MEM: T, // FENCE
2:             OP_AMO     : f3 == 3'b010 || f3 == 3'b011, // AMO
3:             default    : F,

12.2.2 アドレスを変更する

A拡張でアクセスするメモリのアドレスはrs1で指定されたレジスタの値です。これは基本整数命令セットのロードストア命令のアドレス指定方法(rs1と即値を足し合わせる)とは異なるため、memunitモジュールのaddrポートに割り当てる値をis_amoフラグによって切り替えます(リスト12.8)。

リスト12.8: リスト12.8: メモリアドレスをrs1レジスタの値にする (core.veryl)
1:     var memu_rdata: UIntX;
2:     var memu_stall: logic;
3:     let memu_addr : Addr  = if mems_ctrl.is_amo ? memq_rdata.rs1_data : memq_rdata.alu_result;
4: 
5:     inst memu: memunit (
6:         clk                                   ,
7:         rst                                   ,
8:         valid : mems_valid && !mems_expt.valid,
9:         is_new: mems_is_new                   ,
10:         ctrl  : mems_ctrl                     ,
11:         addr  : memu_addr                     ,
12:         rs2   : memq_rdata.rs2_data           ,
13:         rdata : memu_rdata                    ,
14:         stall : memu_stall                    ,
15:         membus: d_membus                      ,
16:     );

A拡張の命令のメモリアドレスが、操作するデータの幅に整列されていないとき、Store/AMO address misaligned例外が発生します。この例外はストア命令の場合の例外と同じです。

EXステージの例外判定でアドレスを使っている部分を変更します(リスト12.9)。causeとtvalの割り当てがストア命令の場合と同じになっていることを確認してください。

リスト12.9: リスト12.9: 例外を判定するアドレスを変更する (core.veryl)
1:         let memaddr                       : Addr  = if exs_ctrl.is_amo ? exs_rs1_data : exs_alu_result;
2:         let loadstore_address_misaligned  : logic = inst_is_memop(exs_ctrl) && case exs_ctrl.funct3[1:0] {
3:             2'b00  : 0, // B
4:             2'b01  : memaddr[0] != 1'b0, // H
5:             2'b10  : memaddr[1:0] != 2'b0, // W
6:             2'b11  : memaddr[2:0] != 3'b0, // D
7:             default: 0,
8:         };

12.2.3 ライトバックする条件を変更する

A拡張の命令を実行するとき、ロードした値をレジスタにライトバックするように変更します(リスト12.10)。

リスト12.10: リスト12.10: メモリからロードした値をライトバックする (core.veryl)
1:     let wbs_wb_data: UIntX    = if wbs_ctrl.is_lui ?
2:         wbs_imm
3:     : if wbs_ctrl.is_jump ?
4:         wbs_pc + 4
5:     : if wbs_ctrl.is_load || wbs_ctrl.is_amo ?
6:         wbq_rdata.mem_rdata
7:     : if wbs_ctrl.is_csr ?

12.3 amounitモジュールの作成

A拡張は他のコア、ハードウェアスレッドと同期してメモリ操作を行うためのものであるため、A拡張の操作はcoreモジュールの外、メモリよりも前で行います。本書では、coreモジュールとmmio_controllerモジュールの間に、A拡張の命令を処理するamounitモジュールを実装します(図12.3)。

amounitモジュールと他のモジュールの接続

図12.3: amounitモジュールと他のモジュールの接続

12.3.1 インターフェースを作成する

amounitモジュールにA拡張の操作を指示するために、is_amoフラグ、aqビット、rlビット、AMOOp型をmembus_ifインターフェースに追加で定義したインターフェースを作成します。

src/core_data_if.verylを作成し、次のように記述します(リスト12.11)。

リスト12.11: リスト12.11: core_data_if.veryl
1: import eei::*;
2: 
3: interface core_data_if {
4:     var valid : logic                       ;
5:     var ready : logic                       ;
6:     var addr  : logic<XLEN>                 ;
7:     var wen   : logic                       ;
8:     var wdata : logic<MEMBUS_DATA_WIDTH>    ;
9:     var wmask : logic<MEMBUS_DATA_WIDTH / 8>;
10:     var rvalid: logic                       ;
11:     var rdata : logic<MEMBUS_DATA_WIDTH>    ;
12: 
13:     var is_amo: logic   ;
14:     var aq    : logic   ;
15:     var rl    : logic   ;
16:     var amoop : AMOOp   ;
17:     var funct3: logic<3>;
18: 
19:     modport master {
20:         valid : output,
21:         ready : input ,
22:         addr  : output,
23:         wen   : output,
24:         wdata : output,
25:         wmask : output,
26:         rvalid: input ,
27:         rdata : input ,
28:         is_amo: output,
29:         aq    : output,
30:         rl    : output,
31:         amoop : output,
32:         funct3: output,
33:     }
34: 
35:     modport slave {
36:         ..converse(master)
37:     }
38: 
39:     modport all_input {
40:         ..input
41:     }
42: }

12.3.2 amounitモジュールの作成

メモリ操作をcoreモジュールからそのままmmio_controllerモジュールに受け渡しするだけのモジュールを作成します。src/amounit.verylを作成し、次のように記述します(リスト12.12)。

リスト12.12: リスト12.12: amounit.veryl
1: import eei::*;
2: 
3: module amounit (
4:     clk   : input   clock              ,
5:     rst   : input   reset              ,
6:     slave : modport core_data_if::slave,
7:     master: modport Membus::master     ,
8: ) {
9: 
10:     enum State {
11:         Init,
12:         WaitReady,
13:         WaitValid,
14:     }
15: 
16:     var state      : State;
17:     inst slave_saved: core_data_if;
18: 
19:     // masterをリセットする
20:     function reset_master () {
21:         master.valid = 0;
22:         master.addr  = 0;
23:         master.wen   = 0;
24:         master.wdata = 0;
25:         master.wmask = 0;
26:     }
27: 
28:     // masterに要求を割り当てる
29:     function assign_master (
30:         addr : input Addr                   ,
31:         wen  : input logic                  ,
32:         wdata: input UIntX                  ,
33:         wmask: input logic<$size(UIntX) / 8>,
34:     ) {
35:         master.valid = 1;
36:         master.addr  = addr;
37:         master.wen   = wen;
38:         master.wdata = wdata;
39:         master.wmask = wmask;
40:     }
41: 
42:     // 新しく要求を受け入れる
43:     function accept_request_comb () {
44:         if slave.ready && slave.valid {
45:             assign_master(slave.addr, slave.wen, slave.wdata, slave.wmask);
46:         }
47:     }
48: 
49:     // slaveに結果を割り当てる
50:     always_comb {
51:         slave.ready  = 0;
52:         slave.rvalid = 0;
53:         slave.rdata  = 0;
54: 
55:         case state {
56:             State::Init: {
57:                 slave.ready = 1;
58:             }
59:             State::WaitValid: {
60:                 slave.ready  = master.rvalid;
61:                 slave.rvalid = master.rvalid;
62:                 slave.rdata  = master.rdata;
63:             }
64:             default: {}
65:         }
66:     }
67: 
68:     // masterに要求を割り当てる
69:     always_comb {
70:         reset_master();
71:         case state {
72:             State::Init     : accept_request_comb();
73:             State::WaitReady: {
74:                 assign_master(slave_saved.addr, slave_saved.wen, slave_saved.wdata, slave_saved.wmask);
75:             }
76:             State::WaitValid: accept_request_comb();
77:             default         : {}
78:         }
79:     }
80: 
81:     // 新しく要求を受け入れる
82:     function accept_request_ff () {
83:         slave_saved.valid = slave.ready && slave.valid;
84:         if slave.ready && slave.valid {
85:             slave_saved.addr   = slave.addr;
86:             slave_saved.wen    = slave.wen;
87:             slave_saved.wdata  = slave.wdata;
88:             slave_saved.wmask  = slave.wmask;
89:             slave_saved.is_amo = slave.is_amo;
90:             slave_saved.amoop  = slave.amoop;
91:             slave_saved.aq     = slave.aq;
92:             slave_saved.rl     = slave.rl;
93:             slave_saved.funct3 = slave.funct3;
94:             state              = if master.ready ? State::WaitValid : State::WaitReady;
95:         } else {
96:             state = State::Init;
97:         }
98:     }
99: 
100:     function on_clock () {
101:         case state {
102:             State::Init     : accept_request_ff();
103:             State::WaitReady: if master.ready {
104:                 state = State::WaitValid;
105:             }
106:             State::WaitValid: if master.rvalid {
107:                 accept_request_ff();
108:             }
109:             default: {}
110:         }
111:     }
112: 
113:     function on_reset () {
114:         state              = State::Init;
115:         slave_saved.addr   = 0;
116:         slave_saved.wen    = 0;
117:         slave_saved.wdata  = 0;
118:         slave_saved.wmask  = 0;
119:         slave_saved.is_amo = 0;
120:         slave_saved.amoop  = 0 as AMOOp;
121:         slave_saved.aq     = 0;
122:         slave_saved.rl     = 0;
123:         slave_saved.funct3 = 0;
124:     }
125: 
126:     always_ff {
127:         if_reset {
128:             on_reset();
129:         } else {
130:             on_clock();
131:         }
132:     }
133: }

amounitモジュールはState::Init、(State::WaitReady、)State::WaitValidの順に状態を移動し、通常のロードストア命令を処理します。

coreモジュールのロードストア用のインターフェースをmembus_ifからcore_data_ifに変更します(リスト12.13リスト12.14リスト12.15)。

リスト12.13: リスト12.13: d_membusの型を変更する (core.veryl)
1:     i_membus: modport membus_if::<ILEN, XLEN>::master,
2:     d_membus: modport core_data_if::master           ,
3:     led     : output  UIntX                          ,
リスト12.14: リスト12.14: core_data_ifインターフェースのインスタンス化 (top.veryl)
1:     inst d_membus_core: core_data_if;
リスト12.15: リスト12.15: ポートに割り当てるインターフェースを変更する (top.veryl)
1:     inst c: core (
2:         clk                    ,
3:         rst                    ,
4:         i_membus               ,
5:         d_membus: d_membus_core,
6:         led                    ,
7:     );

memunitモジュールのインターフェースも変更し、is_amoaqrlamoopに値を割り当てます(リスト12.16リスト12.17リスト12.19リスト12.18リスト12.20)。

リスト12.16: リスト12.16: membusの型を変更する (memunit.veryl)
1:     stall : output  logic               , // メモリアクセス命令が完了していない
2:     membus: modport core_data_if::master, // メモリとのinterface
3: ) {
リスト12.17: リスト12.17: 一時保存するレジスタの定義 (memunit.veryl)
1:     var req_wen   : logic                       ;
2:     var req_addr  : Addr                        ;
3:     var req_wdata : logic<MEMBUS_DATA_WIDTH>    ;
4:     var req_wmask : logic<MEMBUS_DATA_WIDTH / 8>;
5:     var req_is_amo: logic                       ;
6:     var req_amoop : AMOOp                       ;
7:     var req_aq    : logic                       ;
8:     var req_rl    : logic                       ;
9:     var req_funct3: logic<3>                    ;
リスト12.18: リスト12.18: レジスタをリセットする (memunit.veryl)
1:     always_ff {
2:         if_reset {
3:             state      = State::Init;
4:             req_wen    = 0;
5:             req_addr   = 0;
6:             req_wdata  = 0;
7:             req_wmask  = 0;
8:             req_is_amo = 0;
9:             req_amoop  = 0 as AMOOp;
10:             req_aq     = 0;
11:             req_rl     = 0;
12:             req_funct3 = 0;
13:         } else {
リスト12.19: リスト12.19: membusにレジスタの値を割り当てる (memunit.veryl)
1:     always_comb {
2:         // メモリアクセス
3:         membus.valid  = state == State::WaitReady;
4:         membus.addr   = req_addr;
5:         membus.wen    = req_wen;
6:         membus.wdata  = req_wdata;
7:         membus.wmask  = req_wmask;
8:         membus.is_amo = req_is_amo;
9:         membus.amoop  = req_amoop;
10:         membus.aq     = req_aq;
11:         membus.rl     = req_rl;
12:         membus.funct3 = req_funct3;
リスト12.20: リスト12.20: メモリにアクセスする命令のとき、レジスタに情報を設定する (memunit.veryl)
1:                 case state {
2:                     State::Init: if is_new & inst_is_memop(ctrl) {
3:                         ...
4:                         req_is_amo = ctrl.is_amo;
5:                         req_amoop  = ctrl.funct7[6:2] as AMOOp;
6:                         req_aq     = ctrl.funct7[1];
7:                         req_rl     = ctrl.funct7[0];
8:                         req_funct3 = ctrl.funct3;
9:                     }
10:                     State::WaitReady: if membus.ready {

amounitモジュールをtopモジュールでインスタンス化し、coreモジュールとmmio_controllerモジュールのインターフェースを接続します(リスト12.21)。

リスト12.21: リスト12.21: amounitモジュールをインスタンス化する (top.veryl)
1:     inst amou: amounit (
2:         clk                  ,
3:         rst                  ,
4:         slave : d_membus_core,
5:         master: d_membus     ,
6:     );

12.4 Zalrsc拡張の実装

Zalrsc拡張の命令を実装します。予約セットのサイズは実装が自由に決めることができるため、本書では1つのアドレスのみ保持できるようにします。

12.4.1 LR.W、LR.D命令を実装する

32ビット幅、64ビット幅のLR命令を実装します。LR.W命令はmemunitモジュールで64ビットに符号拡張されるため、amounitモジュールでLR.W命令とLR.D命令を区別する必要はありません。

amounitモジュールに予約セットを作成します(リスト12.22リスト12.23)。is_addr_reservedで、予約セットに有効なアドレスが格納されているかを管理します。

リスト12.22: リスト12.22: 予約セットの定義 (amounit.veryl)
1:     // lr/sc
2:     var is_addr_reserved: logic;
3:     var reserved_addr   : Addr ;
リスト12.23: リスト12.23: レジスタをリセットする (amounit.veryl)
1:         is_addr_reserved   = 0;
2:         reserved_addr      = 0;

LR命令を実行するとき、予約セットにアドレスを登録してロード結果を返すようにします(リスト12.24リスト12.25リスト12.26)。既に予約セットが使われている場合はアドレスを上書きします。

リスト12.24: リスト12.24: accept_request_comb関数の実装 (amounit.veryl)
1:     function accept_request_comb () {
2:         if slave.ready && slave.valid {
3:             if slave.is_amo {
4:                 case slave.amoop {
5:                     AMOOp::LR: assign_master(slave.addr, 0, 0, 0);
6:                     default  : {}
7:                 }
8:             } else {
9:                 assign_master(slave.addr, slave.wen, slave.wdata, slave.wmask);
10:             }
11:         }
12:     }
リスト12.25: リスト12.25: LR命令のときにmasterにロード要求を割り当てる (amounit.veryl)
1:     always_comb {
2:         reset_master();
3:         case state {
4:             State::Init     : accept_request_comb();
5:             State::WaitReady: if slave_saved.is_amo {
6:                 case slave_saved.amoop {
7:                     AMOOp::LR: assign_master(slave_saved.addr, 0, 0, 0);
8:                     default  : {}
9:                 }
10:             } else {
11:                 assign_master(slave_saved.addr, slave_saved.wen, slave_saved.wdata, slave_saved.wmask);
12:             }
リスト12.26: リスト12.26: LR命令のときに予約セットを設定する (amounit.veryl)
1:     function accept_request_ff () {
2:         slave_saved.valid = slave.ready && slave.valid;
3:         if slave.ready && slave.valid {
4:             slave_saved.addr   = slave.addr;
5:             ...
6:             slave_saved.funct3 = slave.funct3;
7:             if slave.is_amo {
8:                 case slave.amoop {
9:                     AMOOp::LR: {
10:                         // reserve address
11:                         is_addr_reserved = 1;
12:                         reserved_addr    = slave.addr;
13:                         state            = if master.ready ? State::WaitValid : State::WaitReady;
14:                     }
15:                     default: {}
16:                 }
17:             } else {
18:                 state = if master.ready ? State::WaitValid : State::WaitReady;
19:             }

12.4.2 SC.W、SC.D命令を実装する

32ビット幅、64ビット幅のSC命令を実装します。SC.W命令はmemunitモジュールで書き込みマスクを設定しているため、amounitモジュールでSC.W命令とSC.D命令を区別する必要はありません。

SC命令が成功、失敗したときに結果を返すための状態をState型に追加します(リスト12.27)。

リスト12.27: リスト12.27: SC命令用の状態の定義 (amounit.veryl)
1:     enum State {
2:         Init,
3:         WaitReady,
4:         WaitValid,
5:         SCSuccess,
6:         SCFail,
7:     }

それぞれの状態で結果を返し、新しく要求を受け入れるようにします(リスト12.28)。State::SCSuccessはSC命令に成功してストアが終わったときに結果を返します。成功したら0、失敗したら1を返します。

リスト12.28: リスト12.28: slaveにSC命令の結果を割り当てる (amounit.veryl)
1:     State::SCSuccess: {
2:         slave.ready  = master.rvalid;
3:         slave.rvalid = master.rvalid;
4:         slave.rdata  = 0;
5:     }
6:     State::SCFail: {
7:         slave.ready  = 1;
8:         slave.rvalid = 1;
9:         slave.rdata  = 1;
10:     }

SC命令を受け入れるときに予約セットを確認し、アドレスが予約セットのアドレスと異なる場合は状態をState::SCFailに移動します(リスト12.29)。成功、失敗に関係なく、予約セットを空にします。

リスト12.29: リスト12.29: accept_request_ff関数で予約セットを確認する (amounit.veryl)
1:     AMOOp::SC: {
2:         // reset reserved
3:         let prev            : logic = is_addr_reserved;
4:         is_addr_reserved = 0;
5:         // check
6:         if prev && slave.addr == reserved_addr {
7:             state = if master.ready ? State::SCSuccess : State::WaitReady;
8:         } else {
9:             state = State::SCFail;
10:         }
11:     }

SC命令でメモリのready1になるのを待っているとき、ready1になったら状態をState::SCSuccessに移動します(リスト12.30)。また、命令の実行が終了したときに新しく要求を受け入れるようにします。

リスト12.30: リスト12.30: SC命令の状態遷移 (amounit.veryl)
1:     function on_clock () {
2:         case state {
3:             State::Init     : accept_request_ff();
4:             State::WaitReady: if master.ready {
5:                 if slave_saved.is_amo && slave_saved.amoop == AMOOp::SC {
6:                     state = State::SCSuccess;
7:                 } else {
8:                     state = State::WaitValid;
9:                 }
10:             }
11:             State::WaitValid: if master.rvalid {
12:                 accept_request_ff();
13:             }
14:             State::SCSuccess: if master.rvalid {
15:                 accept_request_ff();
16:             }
17:             State::SCFail: accept_request_ff();
18:             default      : {}
19:         }
20:     }

SC命令によるメモリへの書き込みを実装します(リスト12.31リスト12.32)。

リスト12.31: リスト12.31: accept_request_comb関数で、予約セットをチェックしてからストアを要求する (amounit.veryl)
1:     case slave.amoop {
2:         AMOOp::LR: assign_master(slave.addr, 0, 0, 0);
3:         AMOOp::SC: if is_addr_reserved && slave.addr == reserved_addr {
4:         @<b>     assign_master(slave.addr, 1, slave.wdata, slave.wmask);|
5:         @<b> }|
6:         default: {}
7:     }
リスト12.32: リスト12.32: masterに値を割り当てる (amounit.veryl)
1:     always_comb {
2:         reset_master();
3:         case state {
4:             State::Init     : accept_request_comb();
5:             State::WaitReady: if slave_saved.is_amo {
6:                 case slave_saved.amoop {
7:                     AMOOp::LR: assign_master(slave_saved.addr, 0, 0, 0);
8:                     AMOOp::SC: assign_master(slave_saved.addr, 1, slave_saved.wdata, slave_saved.wmask);
9:                     default  : {}
10:                 }
11:             } else {
12:                 assign_master(slave_saved.addr, slave_saved.wen, slave_saved.wdata, slave_saved.wmask);
13:             }
14:             State::WaitValid               : accept_request_comb();
15:             State::SCFail, State::SCSuccess: accept_request_comb();
16:             default                        : {}
17:         }
18:     }

12.5 Zaamo拡張の実装

Zaamo拡張の命令はロード、演算、ストアを行います。本章では、Zaamo拡張の命令をState::Init(、State::AMOLoadReady)、State::AMOLoadValid(、State::AMOStoreReady)、State::AMOStoreValidという状態遷移で処理するように実装します。

State型に新しい状態を定義してください(リスト12.33)。

リスト12.33: リスト12.33: Zaamo拡張の命令用の状態の定義 (amounit.veryl)
1:     enum State {
2:         Init,
3:         WaitReady,
4:         WaitValid,
5:         SCSuccess,
6:         SCFail,
7:         AMOLoadReady,
8:         AMOLoadValid,
9:         AMOStoreReady,
10:         AMOStoreValid,
11:     }

簡単にZalrsc拡張と区別するために、Zaamo拡張による要求かどうかを判定する関数(is_Zaamo)をcore_data_ifインターフェースに作成します(リスト12.34リスト12.35)。modportにimport宣言を追加してください。

リスト12.34: リスト12.34: is_Zaamo関数の定義 (core_data_if.veryl)
1:     function is_Zaamo () -> logic {
2:         return is_amo && (amoop != AMOOp::LR && amoop != AMOOp::SC);
3:     }
リスト12.35: リスト12.35: masterにis_Zaamo関数をimportする (core_data_if.veryl)
1:     amoop   : output,
2:     funct3  : output,
3:     is_Zaamo: import,
4: }

ロードした値とwdata、フラグを利用して、ストアする値を生成する関数を作成します(リスト12.36)。32ビット演算のとき、下位32ビットと上位32ビットのどちらを使うかをアドレスによって判別しています。

リスト12.36: リスト12.36: Zaamo拡張の命令の計算を行う関数の定義 (amounit.veryl)
1:     // AMO ALU
2:     function calc_amo::<W: u32> (
3:         amoop: input AMOOp   ,
4:         wdata: input logic<W>,
5:         rdata: input logic<W>,
6:     ) -> logic<W> {
7:         let lts: logic = $signed(wdata) <: $signed(rdata);
8:         let ltu: logic = wdata <: rdata;
9: 
10:         return case amoop {
11:             AMOOp::SWAP: wdata,
12:             AMOOp::ADD : rdata + wdata,
13:             AMOOp::XOR : rdata ^ wdata,
14:             AMOOp::AND : rdata & wdata,
15:             AMOOp::OR  : rdata | wdata,
16:             AMOOp::MIN : if lts ? wdata : rdata,
17:             AMOOp::MAX : if !lts ? wdata : rdata,
18:             AMOOp::MINU: if ltu ? wdata : rdata,
19:             AMOOp::MAXU: if !ltu ? wdata : rdata,
20:             default    : 0,
21:         };
22:     }
23: 
24:     // Zaamo拡張の命令のwdataを生成する
25:     function gen_amo_wdata (
26:         req  : modport core_data_if::all_input,
27:         rdata: input   UIntX                  ,
28:     ) -> UIntX {
29:         case req.funct3 {
30:             3'b010: { // word
31:                 let low    : logic  = req.addr[2] == 0;
32:                 let rdata32: UInt32 = if low ? rdata[31:0] : rdata[63:32];
33:                 let wdata32: UInt32 = if low ? req.wdata[31:0] : req.wdata[63:32];
34:                 let result : UInt32 = calc_amo::<32>(req.amoop, wdata32, rdata32);
35:                 return if low ? {rdata[63:32], result} : {result, rdata[31:0]};
36:             }
37:             3'b011 : return calc_amo::<64>(req.amoop, req.wdata, rdata); // double
38:             default: return 0;
39:         }
40:     }

ロードした値が命令の結果になるため、値を保持するためのレジスタを作成します(リスト12.37リスト12.38)。

リスト12.37: リスト12.37: ロードしたデータを格納するレジスタの定義 (amounit.veryl)
1:     // amo
2:     var zaamo_fetched_data: UIntX;
リスト12.38: リスト12.38: レジスタのリセット (amounit.veryl)
1:         reserved_addr      = 0;
2:         zaamo_fetched_data = 0;
3:     }

メモリアクセスが終了したら、ロードした値を返します(リスト12.39)。

リスト12.39: リスト12.39: 命令の結果を返す (amounit.veryl)
1:     State::AMOStoreValid: {
2:         slave.ready  = master.rvalid;
3:         slave.rvalid = master.rvalid;
4:         slave.rdata  = zaamo_fetched_data;
5:     }

状態に基づいて、メモリへのロード、ストア要求を割り当てます(リスト12.40リスト12.41)。

リスト12.40: リスト12.40: accept_request_comb関数で、まずロード要求を行う (amounit.veryl)
1:     default: if slave.is_Zaamo() {
2:         assign_master(slave.addr, 0, 0, 0);
3:     }
リスト12.41: リスト12.41: 状態に基づいてロード、ストア要求を行う (amounit.veryl)
1:     State::AMOLoadReady                      : assign_master      (slave_saved.addr, 0, 0, 0);
2:     State::AMOLoadValid, State::AMOStoreReady: {
3:         let rdata        : UIntX = if state == State::AMOLoadValid ? master.rdata : zaamo_fetched_data;
4:         let wdata        : UIntX = gen_amo_wdata(slave_saved, rdata);
5:         assign_master(slave_saved.addr, 1, wdata, slave_saved.wmask);
6:     }
7:     State::AMOStoreValid: accept_request_comb();

masterslaveの状態によってstateを遷移します(リスト12.42)。

リスト12.42: リスト12.42: accept_request_ff関数で、masterのreadyによって次のstateを決める (amounit.veryl)
1:     default: if slave.is_Zaamo() {
2:         state = if master.ready ? State::AMOLoadValid : State::AMOLoadReady;
3:     }
リスト12.43: リスト12.43: Zaamo拡張の命令の状態の遷移 (amounit.veryl)
1:     State::AMOLoadReady: if master.ready {
2:         state = State::AMOLoadValid;
3:     }
4:     State::AMOLoadValid: if master.rvalid {
5:         zaamo_fetched_data = master.rdata;
6:         state              = if slave.ready ? State::AMOStoreValid : State::AMOStoreReady;
7:     }
8:     State::AMOStoreReady: if master.ready {
9:         state = State::AMOStoreValid;
10:     }
11:     State::AMOStoreValid: if master.rvalid {
12:         accept_request_ff();
13:     }

riscv-testsのrv64ua-p-から始まるテストを実行し、成功することを確認してください。