目次: OpenOCD
OpenOCDにRISC-Vの独自(もしくは標準に準拠しているものの新しすぎるなど)のCSR(Control and Status Register)を定義してアクセスする方法をメモしておきます。
前回はexpose_csrsを使って独自のレジスタを定義しました。この機能はOpenOCDの改造が不要で手軽な反面、2つの問題があります。1つ目の問題はSMPモードだと使えないことで、SMPモードと併用すると下記のように怒られCSRにアクセスできません。
Warn : Register csrNNN does not exist in riscv.cpu.0, which is part of an SMP group where this register does exist.
2つ目の問題点は名前がわかりづらいことです。csrNNNのようなほぼ番号同然の名前を暗記するのは正直言って辛いですよね。
CSR名を新たに追加するにはOpenOCDにパッチを当てて、再ビルドする必要があります。OpenOCDのビルド方法は以前書きました(2023年6月28日の日記参照)のでそちらに任せるとして、今回はパッチについて紹介しましょう。
前回同様、題材はRNMI CSRを使います。RISC-V Privileged Architectures V20211203(RISC-V Instruction Set Manual の2023-05-23のリリースページからダウンロードできます)を見ると、RNMIでは4つのCSRが定義されています。
書き起こしておくと、
となります。OpenOCDを変更すべき箇所はsrc/target/riscv/encoding.hというヘッダファイルだけです。
diff --git a/src/target/riscv/encoding.h b/src/target/riscv/encoding.h
index c2da4e676..6c3f9cc12 100644
--- a/src/target/riscv/encoding.h
+++ b/src/target/riscv/encoding.h
@@ -2992,6 +2992,10 @@
#define CSR_PMPADDR61 0x3ed
#define CSR_PMPADDR62 0x3ee
#define CSR_PMPADDR63 0x3ef
+#define CSR_MNSCRATCH 0x740
+#define CSR_MNEPC 0x741
+#define CSR_MNCAUSE 0x742
+#define CSR_MNSTATUS 0x744
#define CSR_MSECCFG 0x747
#define CSR_TSELECT 0x7a0
#define CSR_TDATA1 0x7a1
@@ -4714,6 +4718,10 @@ DECLARE_CSR(pmpaddr60, CSR_PMPADDR60)
DECLARE_CSR(pmpaddr61, CSR_PMPADDR61)
DECLARE_CSR(pmpaddr62, CSR_PMPADDR62)
DECLARE_CSR(pmpaddr63, CSR_PMPADDR63)
+DECLARE_CSR(mnscratch, CSR_MNSCRATCH)
+DECLARE_CSR(mnepc, CSR_MNEPC)
+DECLARE_CSR(mncause, CSR_MNCAUSE)
+DECLARE_CSR(mnstatus, CSR_MNSTATUS)
DECLARE_CSR(mseccfg, CSR_MSECCFG)
DECLARE_CSR(tselect, CSR_TSELECT)
DECLARE_CSR(tdata1, CSR_TDATA1)
CSR番号とDECLARE_CSRを追加するだけで良いみたいです。さすがOpenOCD便利な作りですね。
前回同様、RNMIを実装しているCPUの例としてNSITEXE NS31を用いてRNMI CSRを読み出してみましょう。
(gdb) info reg mnscratch mnepc mncause mnstatus mnscratch 0x0 0 mnepc 0x0 0 mncause 0x80000000 -2147483648 mnstatus 0x8 8
無事読み出すことができました。やはり名前が付いているとわかりやすいですね。
目次: RISC-V
RISC-Vは最後発の命令セットだけあって、従来の命令セットで評判の悪かった部分は改善されている場合が多いです。しかし人の作るものですからミスや見落としはあります。
例としてわかりやすいのがRV64I命令セットの32bit unsignedと64bit unsigned加算処理です。下記のようなコードを書いたとします。
unsigned int __attribute__((noinline)) something(int n)
{
return n * n;
}
int test(unsigned int num)
{
unsigned long long sum = 0;
for (int i = 0; i < num; i++) {
sum += something(i); //32bit unsigned + 64bit unsigned
}
return sum;
}
下記のようにコンパイルします。RV64GCはRV64IMAFDCの略(M: Multiplication and Division, A: Atomic, F: Single-Precision Floating-Point, D: Double-Precision Floating-Point, C: Compressed)です。RV64I以外のMAFDCの各命令も出てきますが、話題と関係ないので気にしないでください。RV64GCが基本的な命令セットくらいの認識でOKです。
$ riscv64-unknown-elf-gcc -g -O2 -march=rv64gc -mabi=lp64d -c -o rv64gc.o a.c
逆アセンブルを見ると変なシフト命令(アドレス24, 26)が2つ出力されます。
000000000000001a <.L5>:
sum += something(i);
1a: 8522 mv a0,s0 # s0: i, s1: sum
1c: 00000097 auipc ra,0x0
20: 000080e7 jalr ra # call something()
24: 1502 slli a0,a0,0x20 # a0: something() の返り値
26: 9101 srli a0,a0,0x20 # shift x 2で上位32ビットを0埋め
for (int i = 0; i < num; i++) {
28: 2405 addiw s0,s0,1
sum += something(i);
2a: 94aa add s1,s1,a0 # shift x 2 + add
for (int i = 0; i < num; i++) {
2c: ff2417e3 bne s0,s2,1a <.L5>
RV64Iには32bit unsigned向けの加算命令がなく、32bit unsignedを64bit unsignedにゼロ拡張してから加算する必要があるためです。さらに悲しいことにゼロ拡張する命令もなく、32bit左シフト命令+32bit右論理シフト命令でゼロ拡張するヘボい処理になります。
シフト命令2つくらい何だというのか?ケチケチするなよ?という感覚が普通かもしれませんが、余計な命令が出ると特にローエンドのCPUでは性能への影響が無視できません。どうやらCoreMarkのような典型的なベンチマークにも影響が出ていたようで、
// GitHub: sifive/benchmark-coremark
// freedom-metal/core_portme.h
typedef signed short ee_s16;
typedef unsigned short ee_u16;
typedef signed int ee_s32;
typedef double ee_f32;
typedef unsigned char ee_u8;
typedef signed int ee_u32; //★★★u32なのに "signed" intになっている★★★
typedef signed long ee_u64; //★★★u64なのに "signed" longになっている★★★
#if __riscv_xlen == 32
typedef ee_u32 ee_ptr_int;
#else
typedef ee_u64 ee_ptr_int;
#endif
typedef signed int ee_size_t;
RISC-Vの盟主たるSiFiveすらも「unsigned型をsigned型にすりかえて性能を上げるぞい!」というCoreMarkハックを行っていた(該当箇所へのリンク)ほどです……。
当然RV64Iのまずい点はRISC-Vの方々も気づいており、B拡張(Bit-manipulation extensions)を追加したときに上記の問題は修正されました(RISC-V Bitmanipulation extension規格書へのリンク)。
B拡張はZba, Zbb, Zbc, Zbsの4つがあります。
この中のZba拡張にて32bit unsigned加算命令であるadd.uw命令が追加されました。他にも1, 2, 3bitシフト&加算命令なんかも追加されています。unsigned加算や1, 2, 3bitシフト&加算は配列の要素のアドレスを計算する際に頻出で、Address generation instructionsというグループ名にしたのでしょう。
Zba拡張を使うとどのように改善されるか確認します。
$ riscv64-unknown-elf-gcc -g -O2 -march=rv64gc_zba -mabi=lp64d -c -o rv64gcb.o a.c
逆アセンブルを見ると変なシフト命令は消滅し、新たにadd.uw命令が出力されていることが分かると思います。
000000000000001a <.L5>:
sum += something(i);
1a: 8522 mv a0,s0 # s0: i, s1: sum
1c: 00000097 auipc ra,0x0
20: 000080e7 jalr ra # call something()
for (int i = 0; i < num; i++) {
24: 2405 addiw s0,s0,1
sum += something(i);
26: 089504bb add.uw s1,a0,s1 # shift x 2は消滅、add.uwのみ
for (int i = 0; i < num; i++) {
2a: ff2418e3 bne s0,s2,1a <.L5>
めでたしめでたし。なんですけど、人によっては色々言いたいこともあると思います。Bit-manipulationとAddress generation全然関係ないぞ?とかね。
しかし冒頭にも書いたとおり、何事も最初から完璧なものはないです。命令セットが汚くなっていくのはRISC-Vが実用段階に入った証であり、むしろ良いことだと個人的には思います。
< | 2023 | > | ||||
<< | < | 07 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | - | - | - | - | - | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | - | - | - | - | - |
合計:
本日: