A拡張の実装
本章では、メモリの不可分操作を実現するA拡張を実装します。 A拡張にはLoad-Reserved、Store-Conditionalを実現するZalrsc拡張(表2)、 ロードした値を加工し、その結果をメモリにストアする操作を単一の命令で実装するZaamo拡張(表1)が含まれています。 A拡張の命令を利用すると、同じメモリ空間で複数のソフトウェアを並列、並行して実行するとき、 ソフトウェア間で同期をとりながら実行できます。
アトミック操作
アトミック操作とは何か?
アトミック操作(Atomic operation、不可分操作)とは、他のシステムからその操作を観測するとき、1つの操作として観測される操作のことです。 つまり、他のシステムは、アトミック操作を行う前、アトミック操作を行った後の状態しか観測できません。

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

▲図2: 1つのCPUでメモリ上の値を2回インクリメントする (Xは12になる)
アトミック操作は実行、観測される順序が重要なアプリケーションで利用します。 例えば、アドレスXの値をロードして1を足した値を書き戻すプログラムを、 2つのコアで同時に実行するとします(図1)。 このとき命令の実行順序によっては、最終的な値が1つのコアで2回プログラムを実行した場合と異なってしまいます(図2)。 この状態を避けるためにはロード、加算、ストアをアトミックに行う必要があります。 このアトミック操作の実現方法として、A拡張はAMOADD命令、LR命令とSC命令を提供します。
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(符号無し)の値と比べて大きい値を書き込む |
Zalrsc拡張
Zalrsc拡張は、LR命令とSC命令を定義しています。 LR、SC命令は、それぞれLoad-Reserved、Store-Conditional操作を実現する命令です。 それぞれ次のように動作します。
- LR命令
- 指定されたアドレスのデータを読み込み、指定されたアドレスを予約セット(Reservation set)に登録します。 ロードしたデータをレジスタにライトバックします。
- SC命令
- 指定されたアドレスが予約セットに存在する場合、指定されたアドレスにデータを書き込みます(ストア成功)。 予約セットにアドレスが存在しない場合は書き込みません(ストア失敗)。 ストアに成功したら`0`、失敗したら`0`以外の値をレジスタにライトバックします。 命令の実行後に必ず予約セットを空にします。
LR、SC命令を使うことで、アトミックなロード、加算、ストアを次のように記述できます (リスト1)。
▼リスト12.1: LR、SC命令によるアトミックな加算
atomic_add:
LR.W x2, (x3) ← アドレスx3の値をx2にロード
ADDI x2, x2, 1 ← x2に1を足す
SC.W x4, x2, (x3) ← ストアを試行し、結果をx4に格納
BNEZ x4, atomic_add ← SC命令が失敗していたらやり直す
例えば同時に2つのコアがリスト1を実行するとき、同期をとれていない書き込みはSC命令で失敗します。 失敗したらLR命令からやり直すことで、1つのコアで2回実行した場合と同一の結果(1を2回加算)になります。
予約セットのサイズは実装によって異なります。
表12.2: Zalrsc拡張の命令
| 命令 | 動作 |
|---|---|
| LR.W/D | メモリから32/64ビット読み込み、予約セットにアドレスを登録する 読み込んだ値をレジスタにライトバックする |
| SC.W/D | 予約セットにrs1の値が登録されている場合、メモリにrs2の値を書き込み 0をレジスタにライトバックする。予約セットにアドレスが登録されていない場合 メモリに書き込まず、0以外の値をレジスタにライトバックする。 命令の実行後に予約セットを空にする |
命令の順序
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]。
命令のデコード
A拡張の命令はすべてR形式で、opcodeはOP-AMO(7'b0101111)です。 それぞれの命令はfunct5(リスト3)とfunct3(Wは2、Dは3)で区別できます。
eeiパッケージにOP-AMOの定数を定義します (リスト2)。
▼リスト12.2: OP-AMOの定義 (eei.veryl) 差分をみる
const OP_AMO : logic<7> = 7'b0101111;
A拡張の命令を区別するための列挙型AMOOpを定義します (リスト3)。 それぞれ命令のfunct5と対応しています。
▼リスト12.3: AMOOp型の定義 (eei.veryl) 差分をみる
enum AMOOp: logic<5> {
LR = 5'b00010,
SC = 5'b00011,
SWAP = 5'b00001,
ADD = 5'b00000,
XOR = 5'b00100,
AND = 5'b01100,
OR = 5'b01000,
MIN = 5'b10000,
MAX = 5'b10100,
MINU = 5'b11000,
MAXU = 5'b11100,
}
is_amoフラグを実装する
InstCtrl構造体に、 A拡張の命令であることを示すis_amoフラグを追加します (リスト4)。
▼リスト12.4: InstCtrlにis_amoを定義する (corectrl.veryl) 差分をみる
struct InstCtrl {
itype : InstType , // 命令の形式
rwb_en : logic , // レジスタに書き込むかどうか
is_lui : logic , // LUI命令である
is_aluop : logic , // ALUを利用する命令である
is_muldiv: logic , // M拡張の命令である
is_op32 : logic , // OP-32またはOP-IMM-32である
is_jump : logic , // ジャンプ命令である
is_load : logic , // ロード命令である
is_csr : logic , // CSR命令である
is_amo : logic , // AMO instruction
funct3 : logic <3>, // 命令のfunct3フィールド
funct7 : logic <7>, // 命令のfunct7フィールド
}
命令がメモリにアクセスするかを判定するinst_is_memop関数を、is_amoフラグを利用するように変更します (リスト5)。
▼リスト12.5: A拡張の命令がメモリにアクセスする命令と判定する (corectrl.veryl) 差分をみる
function inst_is_memop (
ctrl: input InstCtrl,
) -> logic {
return ctrl.itype == InstType::S || ctrl.is_load || ctrl.is_amo;
}
inst_decoderモジュールのInstCtrlを生成している部分を変更します。 opcodeがOP-AMOのとき、is_amoをTに設定します (リスト6)。 その他のopcodeのis_amoはFに設定してください。
▼リスト12.6: is_amoフラグを追加する (inst_decoder.veryl) 差分をみる
OP_SYSTEM: {
InstType::I, T, F, F, F, F, F, F, T, F
},
OP_AMO: {
InstType::R, T, F, F, F, F, F, F, F, T
},
default: {
InstType::X, F, F, F, F, F, F, F, F, F
},
また、A拡張の命令が有効な命令として判断されるようにします (リスト7)。
▼リスト12.7: A拡張の命令のとき、validフラグを立てる (inst_decoder.veryl) 差分をみる
OP_MISC_MEM: T, // FENCE
OP_AMO : f3 == 3'b010 || f3 == 3'b011, // AMO
default : F,
アドレスを変更する
A拡張でアクセスするメモリのアドレスはrs1で指定されたレジスタの値です。 これは基本整数命令セットのロードストア命令のアドレス指定方法(rs1と即値を足し合わせる)とは異なるため、 memunitモジュールのaddrポートに割り当てる値をis_amoフラグによって切り替えます (リスト8)。
▼リスト12.8: メモリアドレスをrs1レジスタの値にする (core.veryl) 差分をみる
var memu_rdata: UIntX;
var memu_stall: logic;
let memu_addr : Addr = if mems_ctrl.is_amo ? memq_rdata.rs1_data : memq_rdata.alu_result;
inst memu: memunit (
clk ,
rst ,
valid : mems_valid && !mems_expt.valid,
is_new: mems_is_new ,
ctrl : mems_ctrl ,
addr : memu_addr ,
rs2 : memq_rdata.rs2_data ,
rdata : memu_rdata ,
stall : memu_stall ,
membus: d_membus ,
);
A拡張の命令のメモリアドレスが、 操作するデータの幅に整列されていないとき、 Store/AMO address misaligned例外が発生します。 この例外はストア命令の場合の例外と同じです。
EXステージの例外判定でアドレスを使っている部分を変更します (リスト9)。 causeとtvalの割り当てがストア命令の場合と同じになっていることを確認してください。
▼リスト12.9: 例外を判定するアドレスを変更する (core.veryl) 差分をみる
let memaddr : Addr = if exs_ctrl.is_amo ? exs_rs1_data : exs_alu_result;
let loadstore_address_misaligned : logic = inst_is_memop(exs_ctrl) && case exs_ctrl.funct3[1:0] {
2'b00 : 0, // B
2'b01 : memaddr[0] != 1'b0, // H
2'b10 : memaddr[1:0] != 2'b0, // W
2'b11 : memaddr[2:0] != 3'b0, // D
default: 0,
};
ライトバックする条件を変更する
A拡張の命令を実行するとき、 ロードした値をレジスタにライトバックするように変更します (リスト10)。
▼リスト12.10: メモリからロードした値をライトバックする (core.veryl) 差分をみる
let wbs_wb_data: UIntX = switch {
wbs_ctrl.is_lui : wbs_imm,
wbs_ctrl.is_jump : wbs_pc + 4,
wbs_ctrl.is_load || wbs_ctrl.is_amo: wbq_rdata.mem_rdata,
wbs_ctrl.is_csr : wbq_rdata.csr_rdata,
default : wbq_rdata.alu_result
};
amounitモジュールの作成
A拡張は他のコア、ハードウェアスレッドと同期してメモリ操作を行うためのものであるため、 A拡張の操作はcoreモジュールの外、メモリよりも前で行います。 本書では、coreモジュールとmmio_controllerモジュールの間に、 A拡張の命令を処理するamounitモジュールを実装します(図3)。

▲図3: amounitモジュールと他のモジュールの接続
インターフェースを作成する
amounitモジュールにA拡張の操作を指示するために、 is_amoフラグ、aqビット、rlビット、AMOOp型をmembus_ifインターフェースに追加で定義したインターフェースを作成します。
src/core_data_if.verylを作成し、次のように記述します (リスト11)。
▼リスト12.11: core_data_if.veryl 差分をみる
import eei::*;
interface core_data_if {
var valid : logic ;
var ready : logic ;
var addr : logic<XLEN> ;
var wen : logic ;
var wdata : logic<MEMBUS_DATA_WIDTH> ;
var wmask : logic<MEMBUS_DATA_WIDTH / 8>;
var rvalid: logic ;
var rdata : logic<MEMBUS_DATA_WIDTH> ;
var is_amo: logic ;
var aq : logic ;
var rl : logic ;
var amoop : AMOOp ;
var funct3: logic<3>;
modport master {
valid : output,
ready : input ,
addr : output,
wen : output,
wdata : output,
wmask : output,
rvalid: input ,
rdata : input ,
is_amo: output,
aq : output,
rl : output,
amoop : output,
funct3: output,
}
modport slave {
..converse(master)
}
modport all_input {
..input
}
}
amounitモジュールの作成
メモリ操作をcoreモジュールからそのままmmio_controllerモジュールに受け渡しするだけのモジュールを作成します。 src/amounit.verylを作成し、次のように記述します (リスト12)。
▼リスト12.12: amounit.veryl 差分をみる
import eei::*;
module amounit (
clk : input clock ,
rst : input reset ,
slave : modport core_data_if::slave,
master: modport Membus::master ,
) {
enum State {
Init,
WaitReady,
WaitValid,
}
var state : State;
inst slave_saved: core_data_if;
// masterをリセットする
function reset_master () {
master.valid = 0;
master.addr = 0;
master.wen = 0;
master.wdata = 0;
master.wmask = 0;
}
// masterに要求を割り当てる
function assign_master (
addr : input Addr ,
wen : input logic ,
wdata: input UIntX ,
wmask: input logic<$size(UIntX) / 8>,
) {
master.valid = 1;
master.addr = addr;
master.wen = wen;
master.wdata = wdata;
master.wmask = wmask;
}
// 新しく要求を受け入れる
function accept_request_comb () {
if slave.ready && slave.valid {
assign_master(slave.addr, slave.wen, slave.wdata, slave.wmask);
}
}
// slaveに結果を割り当てる
always_comb {
slave.ready = 0;
slave.rvalid = 0;
slave.rdata = 0;
case state {
State::Init: {
slave.ready = 1;
}
State::WaitValid: {
slave.ready = master.rvalid;
slave.rvalid = master.rvalid;
slave.rdata = master.rdata;
}
default: {}
}
}
// masterに要求を割り当てる
always_comb {
reset_master();
case state {
State::Init : accept_request_comb();
State::WaitReady: {
assign_master(slave_saved.addr, slave_saved.wen, slave_saved.wdata, slave_saved.wmask);
}
State::WaitValid: accept_request_comb();
default : {}
}
}
// 新しく要求を受け入れる
function accept_request_ff () {
slave_saved.valid = slave.ready && slave.valid;
if slave.ready && slave.valid {
slave_saved.addr = slave.addr;
slave_saved.wen = slave.wen;
slave_saved.wdata = slave.wdata;
slave_saved.wmask = slave.wmask;
slave_saved.is_amo = slave.is_amo;
slave_saved.amoop = slave.amoop;
slave_saved.aq = slave.aq;
slave_saved.rl = slave.rl;
slave_saved.funct3 = slave.funct3;
state = if master.ready ? State::WaitValid : State::WaitReady;
} else {
state = State::Init;
}
}
function on_clock () {
case state {
State::Init : accept_request_ff();
State::WaitReady: if master.ready {
state = State::WaitValid;
}
State::WaitValid: if master.rvalid {
accept_request_ff();
}
default: {}
}
}
function on_reset () {
state = State::Init;
slave_saved.addr = 0;
slave_saved.wen = 0;
slave_saved.wdata = 0;
slave_saved.wmask = 0;
slave_saved.is_amo = 0;
slave_saved.amoop = 0 as AMOOp;
slave_saved.aq = 0;
slave_saved.rl = 0;
slave_saved.funct3 = 0;
}
always_ff {
if_reset {
on_reset();
} else {
on_clock();
}
}
}
amounitモジュールは State::Init、 (State::WaitReady、) State::WaitValidの順に状態を移動し、 通常のロードストア命令を処理します。
coreモジュールのロードストア用のインターフェースをmembus_ifからcore_data_ifに変更します (リスト13、 リスト14、 リスト15)。
▼リスト12.13: d_membusの型を変更する (core.veryl) 差分をみる
i_membus: modport membus_if::<ILEN, XLEN>::master,
d_membus: modport core_data_if::master ,
led : output UIntX ,
▼リスト12.14: core_data_ifインターフェースのインスタンス化 (top.veryl) 差分をみる
inst d_membus_core: core_data_if;
▼リスト12.15: ポートに割り当てるインターフェースを変更する (top.veryl) 差分をみる
inst c: core (
clk ,
rst ,
i_membus ,
d_membus: d_membus_core,
led ,
);
memunitモジュールのインターフェースも変更し、 is_amo、aq、rl、amoopに値を割り当てます (リスト16、 リスト17、 リスト19、 リスト18、 リスト20)。
▼リスト12.16: membusの型を変更する (memunit.veryl) 差分をみる
stall : output logic , // メモリアクセス命令が完了していない
membus: modport core_data_if::master, // メモリとのinterface
) {
▼リスト12.17: 一時保存するレジスタの定義 (memunit.veryl) 差分をみる
var req_wen : logic ;
var req_addr : Addr ;
var req_wdata : logic<MEMBUS_DATA_WIDTH> ;
var req_wmask : logic<MEMBUS_DATA_WIDTH / 8>;
var req_is_amo: logic ;
var req_amoop : AMOOp ;
var req_aq : logic ;
var req_rl : logic ;
var req_funct3: logic<3> ;
▼リスト12.18: レジスタをリセットする (memunit.veryl) 差分をみる
always_ff {
if_reset {
state = State::Init;
req_wen = 0;
req_addr = 0;
req_wdata = 0;
req_wmask = 0;
req_is_amo = 0;
req_amoop = 0 as AMOOp;
req_aq = 0;
req_rl = 0;
req_funct3 = 0;
} else {
▼リスト12.19: membusにレジスタの値を割り当てる (memunit.veryl) 差分をみる
always_comb {
// メモリアクセス
membus.valid = state == State::WaitReady;
membus.addr = req_addr;
membus.wen = req_wen;
membus.wdata = req_wdata;
membus.wmask = req_wmask;
membus.is_amo = req_is_amo;
membus.amoop = req_amoop;
membus.aq = req_aq;
membus.rl = req_rl;
membus.funct3 = req_funct3;
▼リスト12.20: メモリにアクセスする命令のとき、レジスタに情報を設定する (memunit.veryl) 差分をみる
case state {
State::Init: if is_new & inst_is_memop(ctrl) {
state = State::WaitReady;
req_wen = inst_is_store(ctrl);
req_addr = addr;
req_wdata = rs2 << {addr[2:0], 3'b0};
req_wmask = case ctrl.funct3[1:0] {
2'b00: 8'b1 << addr[2:0],
2'b01: case addr[2:0] {
6 : 8'b11000000,
4 : 8'b00110000,
2 : 8'b00001100,
0 : 8'b00000011,
default: 'x,
},
2'b10: case addr[2:0] {
0 : 8'b00001111,
4 : 8'b11110000,
default: 'x,
},
2'b11 : 8'b11111111,
default: 'x,
};
req_is_amo = ctrl.is_amo;
req_amoop = ctrl.funct7[6:2] as AMOOp;
req_aq = ctrl.funct7[1];
req_rl = ctrl.funct7[0];
req_funct3 = ctrl.funct3;
}
State::WaitReady: if membus.ready {
amounitモジュールをtopモジュールでインスタンス化し、 coreモジュールとmmio_controllerモジュールのインターフェースを接続します (リスト21)。
▼リスト12.21: amounitモジュールをインスタンス化する (top.veryl) 差分をみる
inst amou: amounit (
clk ,
rst ,
slave : d_membus_core,
master: d_membus ,
);
Zalrsc拡張の実装
Zalrsc拡張の命令を実装します。 予約セットのサイズは実装が自由に決めることができるため、 本書では1つのアドレスのみ保持できるようにします。
LR.W、LR.D命令を実装する
32ビット幅、64ビット幅のLR命令を実装します。 LR.W命令はmemunitモジュールで64ビットに符号拡張されるため、 amounitモジュールでLR.W命令とLR.D命令を区別する必要はありません。
amounitモジュールに予約セットを作成します (リスト22、 リスト23)。 is_addr_reservedで、予約セットに有効なアドレスが格納されているかを管理します。
▼リスト12.22: 予約セットの定義 (amounit.veryl) 差分をみる
// lr/sc
var is_addr_reserved: logic;
var reserved_addr : Addr ;
▼リスト12.23: レジスタをリセットする (amounit.veryl) 差分をみる
is_addr_reserved = 0;
reserved_addr = 0;
LR命令を実行するとき、予約セットにアドレスを登録してロード結果を返すようにします (リスト24、 リスト25、 リスト26)。 既に予約セットが使われている場合はアドレスを上書きします。
▼リスト12.24: accept_request_comb関数の実装 (amounit.veryl) 差分をみる
function accept_request_comb () {
if slave.ready && slave.valid {
if slave.is_amo {
case slave.amoop {
AMOOp::LR: assign_master(slave.addr, 0, 0, 0);
default : {}
}
} else {
assign_master(slave.addr, slave.wen, slave.wdata, slave.wmask);
}
}
}
▼リスト12.25: LR命令のときにmasterにロード要求を割り当てる (amounit.veryl) 差分をみる
always_comb {
reset_master();
case state {
State::Init : accept_request_comb();
State::WaitReady: if slave_saved.is_amo {
case slave_saved.amoop {
AMOOp::LR: assign_master(slave_saved.addr, 0, 0, 0);
default : {}
}
} else {
assign_master(slave_saved.addr, slave_saved.wen, slave_saved.wdata, slave_saved.wmask);
}
▼リスト12.26: LR命令のときに予約セットを設定する (amounit.veryl) 差分をみる
function accept_request_ff () {
slave_saved.valid = slave.ready && slave.valid;
if slave.ready && slave.valid {
slave_saved.addr = slave.addr;
slave_saved.wen = slave.wen;
slave_saved.wdata = slave.wdata;
slave_saved.wmask = slave.wmask;
slave_saved.is_amo = slave.is_amo;
slave_saved.amoop = slave.amoop;
slave_saved.aq = slave.aq;
slave_saved.rl = slave.rl;
slave_saved.funct3 = slave.funct3;
if slave.is_amo {
case slave.amoop {
AMOOp::LR: {
// reserve address
is_addr_reserved = 1;
reserved_addr = slave.addr;
state = if master.ready ? State::WaitValid : State::WaitReady;
}
default: {}
}
} else {
state = if master.ready ? State::WaitValid : State::WaitReady;
}
SC.W、SC.D命令を実装する
32ビット幅、64ビット幅のSC命令を実装します。 SC.W命令はmemunitモジュールで書き込みマスクを設定しているため、 amounitモジュールでSC.W命令とSC.D命令を区別する必要はありません。
SC命令が成功、失敗したときに結果を返すための状態をState型に追加します (リスト27)。
▼リスト12.27: SC命令用の状態の定義 (amounit.veryl) 差分をみる
enum State {
Init,
WaitReady,
WaitValid,
SCSuccess,
SCFail,
}
それぞれの状態で結果を返し、新しく要求を受け入れるようにします (リスト28)。 State::SCSuccessはSC命令に成功してストアが終わったときに結果を返します。 成功したら0、失敗したら1を返します。
▼リスト12.28: slaveにSC命令の結果を割り当てる (amounit.veryl) 差分をみる
State::SCSuccess: {
slave.ready = master.rvalid;
slave.rvalid = master.rvalid;
slave.rdata = 0;
}
State::SCFail: {
slave.ready = 1;
slave.rvalid = 1;
slave.rdata = 1;
}
SC命令を受け入れるときに予約セットを確認し、アドレスが予約セットのアドレスと異なる場合は状態をState::SCFailに移動します (リスト29)。 成功、失敗に関係なく、予約セットを空にします。
▼リスト12.29: accept_request_ff関数で予約セットを確認する (amounit.veryl) 差分をみる
AMOOp::SC: {
// reset reserved
let prev : logic = is_addr_reserved;
is_addr_reserved = 0;
// check
if prev && slave.addr == reserved_addr {
state = if master.ready ? State::SCSuccess : State::WaitReady;
} else {
state = State::SCFail;
}
}
SC命令でメモリのreadyが1になるのを待っているとき、 readyが1になったら状態をState::SCSuccessに移動します (リスト30)。 また、命令の実行が終了したときに新しく要求を受け入れるようにします。
▼リスト12.30: SC命令の状態遷移 (amounit.veryl) 差分をみる
function on_clock () {
case state {
State::Init : accept_request_ff();
State::WaitReady: if master.ready {
if slave_saved.is_amo && slave_saved.amoop == AMOOp::SC {
state = State::SCSuccess;
} else {
state = State::WaitValid;
}
}
State::WaitValid: if master.rvalid {
accept_request_ff();
}
State::SCSuccess: if master.rvalid {
accept_request_ff();
}
State::SCFail: accept_request_ff();
default : {}
}
}
SC命令によるメモリへの書き込みを実装します ( リスト31、 リスト32 )。
▼リスト12.31: accept_request_comb関数で、予約セットをチェックしてからストアを要求する (amounit.veryl) 差分をみる
case slave.amoop {
AMOOp::LR: assign_master(slave.addr, 0, 0, 0);
AMOOp::SC: if is_addr_reserved && slave.addr == reserved_addr {
@<b> assign_master(slave.addr, 1, slave.wdata, slave.wmask);|
@<b> }|
default: {}
}
▼リスト12.32: masterに値を割り当てる (amounit.veryl) 差分をみる
always_comb {
reset_master();
case state {
State::Init : accept_request_comb();
State::WaitReady: if slave_saved.is_amo {
case slave_saved.amoop {
AMOOp::LR: assign_master(slave_saved.addr, 0, 0, 0);
AMOOp::SC: assign_master(slave_saved.addr, 1, slave_saved.wdata, slave_saved.wmask);
default : {}
}
} else {
assign_master(slave_saved.addr, slave_saved.wen, slave_saved.wdata, slave_saved.wmask);
}
State::WaitValid : accept_request_comb();
State::SCFail, State::SCSuccess: accept_request_comb();
default : {}
}
}
Zaamo拡張の実装
Zaamo拡張の命令はロード、演算、ストアを行います。 本章では、Zaamo拡張の命令を State::Init (、State::AMOLoadReady) 、State::AMOLoadValid (、State::AMOStoreReady) 、State::AMOStoreValid という状態遷移で処理するように実装します。
State型に新しい状態を定義してください (リスト33)。
▼リスト12.33: Zaamo拡張の命令用の状態の定義 (amounit.veryl) 差分をみる
enum State {
Init,
WaitReady,
WaitValid,
SCSuccess,
SCFail,
AMOLoadReady,
AMOLoadValid,
AMOStoreReady,
AMOStoreValid,
}
簡単にZalrsc拡張と区別するために、 Zaamo拡張による要求かどうかを判定する関数(is_Zaamo)をcore_data_ifインターフェースに作成します ( リスト34、 リスト35 )。 modportにimport宣言を追加してください。
▼リスト12.34: is_Zaamo関数の定義 (core_data_if.veryl) 差分をみる
function is_Zaamo () -> logic {
return is_amo && (amoop != AMOOp::LR && amoop != AMOOp::SC);
}
▼リスト12.35: masterにis_Zaamo関数をimportする (core_data_if.veryl) 差分をみる
amoop : output,
funct3 : output,
is_Zaamo: import,
}
ロードした値とwdata、フラグを利用して、ストアする値を生成する関数を作成します (リスト36)。 32ビット演算のとき、下位32ビットと上位32ビットのどちらを使うかをアドレスによって判別しています。
▼リスト12.36: Zaamo拡張の命令の計算を行う関数の定義 (amounit.veryl) 差分をみる
// AMO ALU
function calc_amo::<W: u32> (
amoop: input AMOOp ,
wdata: input logic<W>,
rdata: input logic<W>,
) -> logic<W> {
let lts: logic = $signed(wdata) <: $signed(rdata);
let ltu: logic = wdata <: rdata;
return case amoop {
AMOOp::SWAP: wdata,
AMOOp::ADD : rdata + wdata,
AMOOp::XOR : rdata ^ wdata,
AMOOp::AND : rdata & wdata,
AMOOp::OR : rdata | wdata,
AMOOp::MIN : if lts ? wdata : rdata,
AMOOp::MAX : if !lts ? wdata : rdata,
AMOOp::MINU: if ltu ? wdata : rdata,
AMOOp::MAXU: if !ltu ? wdata : rdata,
default : 0,
};
}
// Zaamo拡張の命令のwdataを生成する
function gen_amo_wdata (
req : modport core_data_if::all_input,
rdata: input UIntX ,
) -> UIntX {
case req.funct3 {
3'b010: { // word
let low : logic = req.addr[2] == 0;
let rdata32: UInt32 = if low ? rdata[31:0] : rdata[63:32];
let wdata32: UInt32 = if low ? req.wdata[31:0] : req.wdata[63:32];
let result : UInt32 = calc_amo::<32>(req.amoop, wdata32, rdata32);
return if low ? {rdata[63:32], result} : {result, rdata[31:0]};
}
3'b011 : return calc_amo::<64>(req.amoop, req.wdata, rdata); // double
default: return 0;
}
}
ロードした値が命令の結果になるため、 値を保持するためのレジスタを作成します ( リスト37、 リスト38 )。
▼リスト12.37: ロードしたデータを格納するレジスタの定義 (amounit.veryl) 差分をみる
// amo
var zaamo_fetched_data: UIntX;
▼リスト12.38: レジスタのリセット (amounit.veryl) 差分をみる
reserved_addr = 0;
zaamo_fetched_data = 0;
}
メモリアクセスが終了したら、ロードした値を返します (リスト39)。
▼リスト12.39: 命令の結果を返す (amounit.veryl) 差分をみる
State::AMOStoreValid: {
slave.ready = master.rvalid;
slave.rvalid = master.rvalid;
slave.rdata = zaamo_fetched_data;
}
状態に基づいて、メモリへのロード、ストア要求を割り当てます ( リスト40、 リスト41 )。
▼リスト12.40: accept_request_comb関数で、まずロード要求を行う (amounit.veryl) 差分をみる
default: if slave.is_Zaamo() {
assign_master(slave.addr, 0, 0, 0);
}
▼リスト12.41: 状態に基づいてロード、ストア要求を行う (amounit.veryl) 差分をみる
State::AMOLoadReady : assign_master (slave_saved.addr, 0, 0, 0);
State::AMOLoadValid, State::AMOStoreReady: {
let rdata : UIntX = if state == State::AMOLoadValid ? master.rdata : zaamo_fetched_data;
let wdata : UIntX = gen_amo_wdata(slave_saved, rdata);
assign_master(slave_saved.addr, 1, wdata, slave_saved.wmask);
}
State::AMOStoreValid: accept_request_comb();
master、slaveの状態によってstateを遷移します (リスト42)。
▼リスト12.42: accept_request_ff関数で、masterのreadyによって次のstateを決める (amounit.veryl) 差分をみる
default: if slave.is_Zaamo() {
state = if master.ready ? State::AMOLoadValid : State::AMOLoadReady;
}
▼リスト12.43: Zaamo拡張の命令の状態の遷移 (amounit.veryl) 差分をみる
State::AMOLoadReady: if master.ready {
state = State::AMOLoadValid;
}
State::AMOLoadValid: if master.rvalid {
zaamo_fetched_data = master.rdata;
state = if slave.ready ? State::AMOStoreValid : State::AMOStoreReady;
}
State::AMOStoreReady: if master.ready {
state = State::AMOStoreValid;
}
State::AMOStoreValid: if master.rvalid {
accept_request_ff();
}
riscv-testsのrv64ua-p-から始まるテストを実行し、成功することを確認してください。
メモリ操作の並び替えによる高速化は応用編で検討します。 ↩︎