目次: FreeRTOS
前回UARTドライバとmain() 関数を実装しました。今回はmain() に至るまでのブート部分を実装します。ざっくり言うとアセンブラで書いたスタートコードと、リンカスクリプトが必要です。
RISC-V QEMU virtマシンのメモリマップは下記のようになっています。使わないハードウェアは載せていません。
今回はRAMの一部をROMの代わりとして使います。0x80000000は本来RAMですがROMの代わりとして扱います。0x80080000以降をRAMとして扱います。本当はSPI Flash ROMを使ったほうが良いですが、手抜き実装です。リンカスクリプトは下記のようにしました。
OUTPUT_ARCH( "riscv" )
ENTRY( _start )
MEMORY
{
rom (rxa) : ORIGIN = 0x80000000, LENGTH = 512K
ram (wxa) : ORIGIN = 0x80080000, LENGTH = 512K
}
SECTIONS
{
.init :
{
_text = .;
KEEP (*(SORT_NONE(.init)))
} >rom AT>rom
.text :
{
*(.text.unlikely .text.unlikely.*)
*(.text.startup .text.startup.*)
*(.text .text.*)
*(.gnu.linkonce.t.*)
} >rom AT>rom
.fini :
{
KEEP (*(SORT_NONE(.fini)))
_etext = .;
} >rom AT>rom
.rodata.align :
{
. = ALIGN(4);
_rodata = .;
} >rom AT>rom
.rodata.start :
{
_rodata_lma = LOADADDR(.rodata.start);
} >rom AT>rom
.rodata :
{
*(.rdata)
*(.rodata .rodata.*)
*(.gnu.linkonce.r.*)
. = ALIGN(4);
_erodata = .;
} >rom AT>rom
.data.align :
{
. = ALIGN(4);
_data = .;
} >ram AT>rom
.data.start :
{
_data_lma = LOADADDR(.data.start);
} >ram AT>rom
.data :
{
*(.data .data.*)
*(.gnu.linkonce.d.*)
. = ALIGN(8);
PROVIDE( __global_pointer$ = . + 0x800 );
*(.sdata .sdata.*)
*(.sdata2 .sdata2.*)
*(.gnu.linkonce.s.*)
. = ALIGN(8);
*(.srodata.cst16)
*(.srodata.cst8)
*(.srodata.cst4)
*(.srodata.cst2)
*(.srodata .srodata.*)
. = ALIGN(4);
_edata = .;
} >ram AT>rom
.bss.align :
{
. = ALIGN(4);
_bss = .;
} >ram AT>rom
.bss.start :
{
_bss_lma = LOADADDR(.bss.start);
} >ram AT>rom
.bss :
{
*(.sbss*)
*(.gnu.linkonce.sb.*)
*(.bss .bss.*)
*(.gnu.linkonce.b.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} >ram AT>rom
. = ALIGN(8);
_end = .;
.stack :
{
. = ALIGN(16);
_stack0_bottom = .;
. += __stack_size;
_stack0_top = .;
} >ram AT>ram
}
ビルドしたバイナリをnmやreadelfで見るときわかりやすくするために、あえて変なセクション(.*.align, .*.start)をいくつか作っています。このように見えます。
$ riscv64-unknown-elf-nm -n a.out | less ★.bss.alignにALIGN(4) と書いたとき 80002ea8 R __clz_tab 80002fa8 A _data_lma 80002fa8 R _erodata ★.rodataの終わり(ROM領域を0x80000000としている) 80002fb8 A _bss_lma 80080000 D _data ★.dataの始まり(RAM領域を0x80080000としている) 80080000 D pullNextTime 80080008 D uxTimerIncrementsForOneTick 8008000c D xISRStackTop 80080010 B _bss ★.bssの始まり ★4bytes alignになっている(.dataの終わりと連続している) 80080010 D _edata ★.dataの終わり 80080010 b xQueue ... ★.data.alignにALIGN(2048) と書いたとき 80002ca8 R __clz_tab 80002da8 A _data_lma 80002da8 R _erodata ★.rodataの終わり(ROM領域を0x80000000としている) 80002db8 A _bss_lma 80080000 D _data ★.dataの始まり(RAM領域を0x80080000としている) 80080000 D pullNextTime 80080008 D uxTimerIncrementsForOneTick 8008000c D xISRStackTop 80080010 D _edata ★.dataの終わり 80080800 D __global_pointer$ 80080800 B _bss ★.bssの始まり ★2KB alignになっている(.dataの終わりと連続して「いない」) 80080800 b xQueue ... $ riscv64-unknown-elf-readelf -a a.out | less ★.bss.alignにALIGN(4) と書いたとき Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .init PROGBITS 80000000 001000 00006c 00 AX 0 0 1 [ 2] .text PROGBITS 80000100 001100 002d2c 00 AX 0 0 256 [ 3] .rodata.align PROGBITS 80002e2c 004010 000000 00 WA 0 0 1 [ 4] .rodata PROGBITS 80002e2c 003e2c 00017c 00 A 0 0 4 [ 5] .data.align PROGBITS 80080000 004010 000000 00 WA 0 0 1 [ 6] .data PROGBITS 80080000 004000 000010 00 WA 0 0 4 [ 7] .bss.align NOBITS 80080010 000000 000000 00 WA 0 0 1 [ 8] .bss NOBITS 80080010 004010 0040c0 00 WA 0 0 16 ★4bytes alignになっている [ 9] .stack NOBITS 800840d0 0040d0 00012c 00 WA 0 0 1 ★.data.alignにALIGN(2048) と書いたとき ection Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .init PROGBITS 80000000 001000 000068 00 AX 0 0 1 [ 2] .text PROGBITS 80000100 001100 002b2c 00 AX 0 0 256 [ 3] .rodata.align PROGBITS 80002c2c 004010 000000 00 WA 0 0 1 [ 4] .rodata PROGBITS 80002c2c 003c2c 00017c 00 A 0 0 4 [ 5] .data.align PROGBITS 80080000 004010 000000 00 WA 0 0 1 [ 6] .data PROGBITS 80080000 004000 000010 00 WA 0 0 4 [ 7] .bss.align NOBITS 80080010 004010 0007f0 00 WA 0 0 1 [ 8] .bss NOBITS 80080800 004800 0040c0 00 WA 0 0 16 ★2KB alignになっている [ 9] .stack NOBITS 800848c0 0048c0 00012c 00 WA 0 0 1
ALIGN(4) をALIGN(2048) など、大きめの値に変えたときの様子を載せました。変なセクション .*.alignがアドレスのアラインメントをしている様子が nmでもreadelfでもわかりやすいですよね?だめ?上記は .bssの例ですが、他のセクションでも同様です。
ブートコードはアセンブラで書く必要があります。
// freertos/FreeRTOS/Demo/RISC-V-Qemu-virt_GCC/start.S
#include "riscv-reg.h"
#if __riscv_xlen == 32
#define REGSIZE 4
#define LOAD lw
#define STOR sw
#elif __riscv_xlen == 64
#define REGSIZE 8
#define LOAD ld
#define STOR sd
#endif /* __riscv_xlen */
.section .init
.globl _start
.type _start,@function
_start:
.cfi_startproc
.cfi_undefined ra
.option push
.option norelax
la gp, __global_pointer$
.option pop
la sp, _stack0_top
# Load data section
la a0, _data_lma
la a1, _data
la a2, _edata
bgeu a1, a2, 2f
1:
LOAD t0, (a0)
STOR t0, (a1)
addi a0, a0, REGSIZE
addi a1, a1, REGSIZE
bltu a1, a2, 1b
2:
# Clear bss section
la a0, _bss
la a1, _ebss
bgeu a0, a1, 2f
1:
STOR zero, (a0)
addi a0, a0, REGSIZE
bltu a0, a1, 1b
2:
/* argc, argv, envp is 0 */
li a0, 0
li a1, 0
li a2, 0
j main
.cfi_endproc
実装は非常に単純です。.dataをROM領域からRAM領域にコピーし、.bssを0クリアしてmain() に飛ぶだけです。
以上の実装でビルドして(makeするだけ)、動かします。
$ qemu-system-riscv32 -nographic -machine virt -net none -chardev stdio,id=con,mux=on -serial chardev:con -mon chardev=con,mode=readline -bios none -kernel a.out Hello FreeRTOS! Blink Blink Blink ...
動きましたね。良かった良かった。 改行が余計に入っちゃってるのが気になる場合はmain.cの "Blink\r\n" を "Blink\n" にすると治ります。
自動車業界(だけじゃないですが)で重宝されているMATLABを家で使えることになったので、インストールしてみました。
ダウンロードは Mathworksのサイトからできますが、たぶん会社や学校からもらえるアカウント、アクティベーション番号が必要です。MATLABは個人でも買えますがメチャクチャ値段が高く、私は買う気は起きません……。
MATLABはDebian 9, Debian 10には対応しています(System Requirements for MATLAB R2020a - MATLAB & Simulink)が、Debian Testingには対応していません。インストーラを起動した瞬間にクラッシュします。
$ cd matlab_archive $ unzip matlab_R2020a_glnxa64.zip $ ./install terminate called after throwing an instance of 'std::runtime_error' what(): Unable to launch the MATLABWindow application Aborted
ありがたいことに、Linux向けのMATLABインストーラは通常版とlegacy版が同梱されています。
$ cd matlab_archive $ unzip matlab_R2020a_glnxa64.zip $ bin/glnxa64/install_unix_legacy
推奨された使い方ではないと思いますがlegacy版ならばインストーラが動きます。質問にはハイハイ答えておけば、そんなに問題ないはずです。
MATLABを起動するときに変なエラーが出ます。
$ matlab MATLAB is selecting SOFTWARE OPENGL rendering. matlab_install_dir/bin/glnxa64/jcef_helper: symbol lookup error: /usr/lib/x86_64-linux-gnu/libpango-1.0.so.0: undefined symbol: g_ptr_array_copy
このエラーはMATLABの動的ライブラリの構成がおかしいことが原因です。libglib-2.0.soをMATLAB内部に抱えているのですが、libpango-1.0.soはシステム側を使うため、バージョンの非互換が発生します。libpango-1.0.soも内部に抱えれば良いのに??何だか中途半端な作りですね。
$ ldd bin/glnxa64/jcef_helper | grep glib libglib-2.0.so.0 => matlab_install_dir/bin/glnxa64/../../cefclient/sys/os/glnxa64/libglib-2.0.so.0 (0x00007f43ac828000) ★MATLAB内部に抱えているライブラリをダイナミックリンクする $ cd matlab_install_dir/cefclient/sys/os/glnxa64 $ mv libglib-2.0.so _libglib-2.0.so $ mv libglib-2.0.so.0 _libglib-2.0.so.0 $ mv libglib-2.0.so.0.5600.1 _libglib-2.0.so.0.5600.1 $ ldd bin/glnxa64/jcef_helper | grep glib libglib-2.0.so.0 => /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0 (0x00007f10b2e6d000) ★システム側のライブラリをダイナミックリンクする
MATLABの内部で抱えているlibglib-2.0.soを無視して、システム側のlibglib-2.0.soをダイナミックリンクすればエラーは出ません。これも推奨された使い方ではないと思いますが、とりあえず動いたのでめでたしめでたし。
目次: FreeRTOS
FreeRTOSにRISC-V QEMU virtpcのパッチをぶん投げてみました。やり方はGitHubでプルリクエストを送れば良いみたいです(説明へのリンク(FreeRTOS/CONTRIBUTING.md))。
FreeRTOSには既にQMEUのプロジェクトはある(SiFive HiFive1エミュレーション環境向けのEclipseプロジェクトがある)から要らないよ!?と言われることが目に浮かびますが、それならそれで良し。今後なにかの役に立つでしょう。
FreeRTOSはインデントやコード記法が非常に特徴的で、とても書きにくかったです。
void vFunc( int aaa )
{
int xBbb;
if( aaa )
{
/* do something */
xBbb = 0;
}
}
クセが強すぎる。何でこんな記法にしたのやら……??
メモ: 技術系?の話はFacebookから転記しておくことにした。色々と加筆。
NVIDIAがArm全株式買収、ソフトバンクGから最大400億ドルで - ケータイWatch を読んで。
NVIDIAがArm買収です。買収額の2/3は株式交換で、ソフトバンクはNVIDIAの大株主になります。買収が成立すれば、ソフトバンクは1兆円超の利益を得ます。
ArmがNVIDIA配下となれば、NVIDIAと思い切り食い合う、Ethos(エッジAIプロセッサ)とMali(モバイル向けGPU)はゴミ箱行きですかね?ディスコンにしなくても、まともに開発しなくなりそうです。
さらにNVIDIAはNVIDIA GPUをライセンスするメリットがない(自分で作れる)ので、組み込み業界はImagination PowerVRとArm Maliという2大モバイルGPU IPを揃って失ってしまう悲しい未来になりそうです。
他陣営(RISC-Vとか)はまともなGPU IPがありませんし、Armがどんな地獄になろうとも、グラフィック必須のスマホ系SoCベンダーに逃げ場はなく、Armで作り続けるしかありません。
Armの築いたエコシステムがArmごとひっくり返されるとは思いませんでしたね。耐震構造のビルを建てていたら、いきなり一面の海になるような……バグったゲームみたいですが、現実なんですよね。
中国はImaginationごとPowerVRを買ってるので、ArmもしくはRISC-V用にPowerVRを売り出す可能性はあります(もう売ってる?)けど、PowerVRって性能的に今の時代のGPUに追いついているんでしょうか??
メモ: 技術系?の話はFacebookから転記しておくことにした。
目次: Windows
今に始まったことではないのですが、Windows 10が突然高負荷に陥ってしまうことがあります。大抵Windows Updateが裏で動いているだけのことが多いですが、今日は様子が違ったのでメモしておきます。
いつもと違う点、その1は、Systemプロセスが高負荷に陥っていることです。Windows Updateが裏で実行されている場合、マウスを動かしたりキーボードに触ったりすると負荷が下がります。今回はマウスを触っても負荷が下がりません。
Process Explorerで見るとPsReturnProcessNonPagedPoolQuota() という関数がめちゃくちゃ頑張っていました。なんでしょうね?これ?
目次: Zephyr
Zephyr RISC-V SMP対応にチャレンジしてます。
Zephyrは既に他のアーキテクチャでSMP対応されていますから、真似するだけの何がチャレンジなの?簡単でしょ?と思われるかもしれません。その考え方は間違ってないんですが、少なくとも私にとっては「余裕」と言えるほど簡単ではなさそうです。
RISC-V向けの実装はAArch32をパクっているようで「お前、シングルコアだろ?な?」的な実装がそこら中にあり、SMPを有効にした途端、猛烈にコンパイルエラーが出ます。
一個一個見てますが、先ほど、コンテキストスイッチから直さないとダメなことに気づきました。こりゃ先は長そうだ〜……。
Google翻訳で「October」を翻訳すると、正しい訳の「10月」が出てくるのですが、
残念ながら読みが間違っていて「じゅうがつ」ではなく「じゅうつき」になっています。惜しい!
「コミュニティ確認済み」のバッジが付いていますが、読みはチェック漏れ?ですかねえ?日本語は同じ文字でも読みが違って難しいですね。
今となっては、高級マスクになってしまったSHARP MA-1050が届きました。マスクはその辺で買えるので、もはやSHARPから購入する必要はないんですけど、まあ、一種の記念です。
参考までに、先日マツキヨで買った中国製のマスクは30枚入り1,000円程度で、1枚33円です。MA-1050は50枚入り2,980円(送料別)で、1枚60円ですから、かなり高級品の部類です。送料も含めると1枚80円近いです。
届いた箱を見たときに「異様に綺麗な段ボールだな……」って思いました。宛名シールも真っ直ぐはみ出ず貼ってあります。近所(千葉県)から送られていることも要因ですが、それにしても綺麗です。
開けてさらにびっくりしたんですけど、段ボールとマスクの箱が全く同じ大きさでした。包装材は省けますけど、入れにくそうですね。
DigiKeyくらいのマトモな海外セラーから買っても、段ボールなんて大抵ベコベコで汚ないし、宛名やインボイスも斜めに貼られてます。でも、中身が無事で、宛先に正しく届けば良いんですよ。段ボールは包装材なんですから。それ以上こだわる意味がありません。
日本人はどうでも良いところにこだわりすぎで、コスト高になってるんじゃないの……??なんてことを段ボールを片付けながら思いました。
以前(2012年11月8日の日記参照)買ったヘッドフォン(audio-technica ATH-TAD500)が壊れました。
イヤーパッドは無事ですが、ヘッドバンドの合皮部分が破れて、内部のプラスチックの芯が見えています。まさかイヤーパッドより先にヘッドバンドが壊れるとは思わなんだ。
8年もの長きにわたって頑張ってくれて本当にありがたかったです。とても良い製品でした。
家や会社でいくつかヘッドフォンを使ってきてわかりましたが、長時間の使用において楽なのは圧倒的に「イヤーパッドが布地」のヘッドフォンです。合皮的な材質は汗で肌にくっついて嫌なのと、傷んでくるとバリバリ剥がれてきて不快です。
今使っているヘッドフォンの後継者としてはaudio-technica ATH-AD500X ですかね。上位グレードのATH-AD700X, ATH-AD900Xも布地のイヤーパッドで良さそうです。
しかし形が今使っているヘッドフォンとかなり似ているため、おそらく耳が痛くなる欠点も共通だろうと思われます。耳が痛いのは何とかしたいため、他社でも探すことにしました。
オーディオの世界は上を見るとキリがないですけど、10万とかするヘッドフォンは検討しません。違いが全くわからん。
SENNHEISER HD 599を買いました。Amazonで22,000円くらいでした。以前購入(2020年8月1日の日記参照)したSONY MDR-HW700DSもそうでしたけど、購入に勇気が要るお値段です。
付け心地はとても軽く、耳も痛くなりません。耳が痛いのは辛かったので、非常にありがたいです。あと、色が良いですね。ヘッドフォンはたいてい真っ黒けですけど、HD 599は白と茶という面白い色です。
難点は長時間使っていると頭のてっぺんが痛くなることです。ヘッドバンドは柔らかいクッションが入っていますが、カーブが自分の頭の形と合わないようです。
耳の問題が解決したと思ったら、今度は頭が痛くなるとは……想定外でした。うーん。
正直、音質は良いね!ってくらいしかわからんですが、ドスドスした低音や、キンキンした高音は目立たず、フラットな印象です。
キンキンした音は断然Superlux HD681B(2019年8月25日の日記参照)の方が強いですし、低音はaudio-technica ATH-TAD500の方がやや強いです。
鳴らないわけじゃなくて、目立たないバランスになっているだけなので、気に入らないならイコライザでいじれば好きなテイストにできます。能力は高いからカスタマイズ自由って感じですね。
目次: ゲーム
Steamのセールで3DMarkが意味不明なほど安くなっていたので、買いました。Steamは突然90%OFFとか平然とやってくるので、定価とは……??という気分になります。
新しいマシンを買う(もしくはパーツを買う)時に役に立ちそうです。
目次: Zephyr
以前(2020年2月21日の日記参照)ご紹介したとおり、ZephyrのRISC-V向け実装はSMPに対応していません。Zephyrのマルチコア対応を有効にするコンフィグはCONFIG_SMPですが、有効にすると大量のエラーが出て、何から直したら良いのかわからなくなります。
コンフィグや実装を見た限りでは、下記の4つのステップで対応していくと良さそうです。
このコンフィグを有効にすその前に対応すべきコンフィグがあります。CONFIG_USE_SWITCHとCONFIG_USE_SWITCH_SUPPORTEDです。
// zephyr/kernel/Kconfig
config USE_SWITCH
bool "Use new-style _arch_switch instead of arch_swap"
depends on USE_SWITCH_SUPPORTED
...
config SMP
bool "Enable symmetric multithreading support"
depends on USE_SWITCH
既存の各アーキテクチャの対応状況を見ると、
// ARM
// Aarch32はUSE_SWITCHは未サポート
// AArch64はCortex-AのみUSE_SWITCHをサポート
// zephyr/arch/arm/core/aarch64/Kconfig
config CPU_CORTEX_A
bool
select CPU_CORTEX
select HAS_FLASH_LOAD_OFFSET
select USE_SWITCH
select USE_SWITCH_SUPPORTED
// ARC
// ARCV2はUSE_SWITCHをサポート
// zephyr/arch/arc/Kconfig
config CPU_ARCV2
bool
select ARCH_HAS_STACK_PROTECTION if ARC_HAS_STACK_CHECKING || ARC_MPU
select ARCH_HAS_USERSPACE if ARC_MPU
select USE_SWITCH
select USE_SWITCH_SUPPORTED
default y
他は載せませんがAArch64, ARC, x86_64, xtensaが対応しているようです。
Zephyrにはコンテキストスイッチが2種類存在しています。違いは下記の通りです。
CONFIG_USE_SWITCH_SUPPORTEDを有効にするにはarch_switchを実装する必要がありますが、RISC-Vではシングルコアもマルチコアもあり得ますから、CONFIG_USE_SWITCHを無条件に有効にするのは得策ではないと考えられます。
SoC側のKconfigで有効にしても良いですし、ユーザーにmenuconfigから選んでもらう手もあります。あまり悩んでも仕方ないので、ユーザーが選べるようにして先に進めます。RISC-VのKconfigにSMP対応かどうか?を選べるコンフィグを一つ追加します。
// zephyr/arch/riscv/Kconfig
config RISCV_SMP
bool "Does SOC has SMP"
select USE_SWITCH
select USE_SWITCH_SUPPORTED
有効にするとビルドエラーだらけになります。続きはまた今度。
目次: Zephyr
CONFIG_USE_SWITCHを有効にするとビルドエラーが発生しますので、都度対処します。
最初はarch_thread_return_value_set() が二重定義だと怒られます。
../kernel/include/kernel_internal.h: At top level: ../kernel/include/kernel_internal.h:84:1: error: redefinition of 'arch_thread_return_value_set' arch_thread_return_value_set(struct k_thread *thread, unsigned int value) ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
USE_SWITCHを有効にした場合、arch_thread_return_value_set() はカーネル側で定義されるので、アーキテクチャ側では実装不要です。
// zephyr/arch/riscv/include/kernel_arch_func.h
static ALWAYS_INLINE void
arch_thread_return_value_set(struct k_thread *thread, unsigned int value)
{
thread->arch.swap_return_value = value;
}
// zephyr/include/arch/riscv/thread.h
struct _thread_arch {
uint32_t swap_return_value; /* Return value of z_swap() */
};
// zephyr/arch/riscv/core/offsets/offsets.c
GEN_OFFSET_SYM(_thread_arch_t, swap_return_value);
この辺りは要らないので #ifndef CONFIG_USE_SWITCHで囲って無効化します。
次はarch_switch() が未定義だと言われます。
../kernel/include/kernel_arch_interface.h:126:20: warning: 'arch_switch' declared 'static' but never defined [-Wunused-function] static inline void arch_switch(void *switch_to, void **switched_from); ^~~~~~~~~~~
この関数がUSE_SWITCHの本体です。他のアーキテクチャを見ると、arch_switch() はラッパー関数で、本体はアセンブラで書かれていることが多いようです。他の流儀に習っておきます。
// arch/riscv/include/kernel_arch_func.h
#ifdef CONFIG_USE_SWITCH
extern void z_riscv_switch(void *switch_to, void **switched_from);
static inline void arch_switch(void *switch_to, void **switched_from)
{
z_riscv_switch(switch_to, switched_from);
}
#else
...
肝心の中身はまた今度実装します。
目次: Zephyr
前回は、新しい形式のコンテキストスイッチ関数arch_switch() のラッパー関数まで実装しました。RISC-V向け実装をする前に、既に新しいコンテキストスイッチ関数に対応しているAArch64の実装を調べます。
コンテキストスイッチ処理の共通部分(do_swap())から、アーキテクチャ依存部分(AArch64のarch_switch())に至るまでを見ます。
// zephyr/kernel/include/kswap.h
/* New style context switching. arch_switch() is a lower level
* primitive that doesn't know about the scheduler or return value.
* Needed for SMP, where the scheduler requires spinlocking that we
* don't want to have to do in per-architecture assembly.
*
* Note that is_spinlock is a compile-time construct which will be
* optimized out when this function is expanded.
*/
static ALWAYS_INLINE unsigned int do_swap(unsigned int key,
struct k_spinlock *lock,
int is_spinlock)
{
...
if (new_thread != old_thread) {
#ifdef CONFIG_TIMESLICING
z_reset_time_slice();
#endif
old_thread->swap_retval = -EAGAIN;
#ifdef CONFIG_SMP
_current_cpu->swap_ok = 0;
new_thread->base.cpu = arch_curr_cpu()->id;
if (!is_spinlock) {
z_smp_release_global_lock(new_thread);
}
#endif
sys_trace_thread_switched_out();
_current_cpu->current = new_thread;
wait_for_switch(new_thread);
arch_switch(new_thread->switch_handle,
&old_thread->switch_handle); //★コンテキストスイッチ★
}
...
前々回に紹介した新しいコンテキストスイッチの方式(arch_switch())を使っていることがわかります。
// zephyr/arch/arm/include/aarch64/arch_func.h
static inline void arch_switch(void *switch_to, void **switched_from)
{
z_arm64_call_svc(switch_to, switched_from); //★アセンブラ実装へ★
return;
}
// zephyr/arch/arm/core/aarch64/switch.S
GTEXT(z_arm64_call_svc)
SECTION_FUNC(TEXT, z_arm64_call_svc)
svc #_SVC_CALL_CONTEXT_SWITCH //★スーパーバイザーコール命令★
ret
アセンブラ側の実装では、いきなりスーパーバイザーコール命令(svc命令)をぶっ放して、スーパーバイザーコール例外を起こすだけになっています。AArch64は例外ハンドラ内でコンテキストスイッチを行う仕組みのようです。
関数arch_switch() はスレッドを2つ受け取ります。old_threadは切り替え元のスレッド、new_threadは切り替え先のスレッドです。old_thread -> new_threadにコンテキストスイッチするわけです。ただし直接スレッド構造体を受け取るわけではなく、若干クセのある渡し方をします。
第1引数new_thread->switch_handle(void * 型)は切り替え先のスレッド構造体へのポインタ(struct k_thread * 型)new_threadが入っています。このポインタをキャストするとnew_threadが求められます。
実はここには罠があり、何も実装せずにいるとnew_thread->switch_handleにはNULLが入ります。すると、あとでCONFIG_SMPを有効にした段階でwait_for_switch() 関数にてハングアップします。こんなのパッと見ではわかりません……。
// zephyr/kernel/include/kswap.h
/* New style context switching. arch_switch() is a lower level
* primitive that doesn't know about the scheduler or return value.
* Needed for SMP, where the scheduler requires spinlocking that we
* don't want to have to do in per-architecture assembly.
*
* Note that is_spinlock is a compile-time construct which will be
* optimized out when this function is expanded.
*/
static ALWAYS_INLINE unsigned int do_swap(unsigned int key,
struct k_spinlock *lock,
int is_spinlock)
{
...
if (new_thread != old_thread) {
...
sys_trace_thread_switched_out();
_current_cpu->current = new_thread;
wait_for_switch(new_thread); //★これ★
arch_switch(new_thread->switch_handle,
&old_thread->switch_handle);
}
if (is_spinlock) {
arch_irq_unlock(key);
} else {
irq_unlock(key);
}
return _current->swap_retval;
}
/* There is an unavoidable SMP race when threads swap -- their thread
* record is in the queue (and visible to other CPUs) before
* arch_switch() finishes saving state. We must spin for the switch
* handle before entering a new thread. See docs on arch_switch().
*
* Note: future SMP architectures may need a fence/barrier or cache
* invalidation here. Current ones don't, and sadly Zephyr doesn't
* have a framework for that yet.
*/
static inline void wait_for_switch(struct k_thread *thread)
{
#ifdef CONFIG_SMP
volatile void **shp = (void *)&thread->switch_handle;
while (*shp == NULL) { //★CONFIG_SMP有効でnew_thread->switch_handleがNULLだとこのループでハング★
k_busy_wait(1);
}
#endif
}
AArch64向けのスレッド作成する関数を良く見ると、しれっとswitch_handleを初期化しています。この処理はRISC-V向けには存在しないため、追加する必要がありそうです。
// zephyr/arch/arm/core/aarch64/thread.c
/*
* An initial context, to be "restored" by z_arm64_context_switch(), is put at
* the other end of the stack, and thus reusable by the stack when not needed
* anymore.
*/
void arch_new_thread(struct k_thread *thread, k_thread_stack_t *stack,
char *stack_ptr, k_thread_entry_t entry,
void *p1, void *p2, void *p3)
{
...
/*
* We are saving:
*
* - SP: to pop out entry and parameters when going through
* z_thread_entry_wrapper().
* - x30: to be used by ret in z_arm64_context_switch() when the new
* task is first scheduled.
*/
thread->callee_saved.sp = (uint64_t)pInitCtx;
thread->callee_saved.x30 = (uint64_t)z_thread_entry_wrapper;
thread->switch_handle = thread; //★ここで初期化している★
}
関数arch_switch() の第2引数 &old_thread->switch_handle(void ** 型)は切り替え元のスレッド構造体のswitch_handleのポインタです。switch_handleの値(たいていNULL)自体には意味がなくて、このポインタからold_threadが計算できることが大事です。
長くなってきたので続きは次回。
目次: Zephyr
前回は、新しいコンテキストスイッチ関数に対応しているAArch64の実装のうち、共通部分からアーキテクチャ依存部分に至るまでを調べました。引き続きアーキテクチャ依存部分を調べます。
コンテキストスイッチのアーキテクチャ依存部分(AArch64向けarch_switch())の実装はほぼ全てアセンブラで実装されています。
コンテキストスイッチの本体はz_arm64_context_switchです。x0がnew_thread, x1がold_threadだと思って動きます。コンテキストスイッチの仕事はレジスタの値を切り替え前のスレッド構造体(old_thread)に退避し、切り替え後のスレッド構造体(new_thread)からレジスタの値を復旧させることです。
// zephyr/arch/arm/core/aarch64/switch.S
GTEXT(z_arm64_svc)
SECTION_FUNC(TEXT, z_arm64_svc)
z_arm64_enter_exc x2, x3, x4 /* ★レジスタをスタックに退避(切り替え前のスレッド)★ */
switch_el x1, 3f, 2f, 1f
3:
mrs x0, esr_el3
b 0f
2:
mrs x0, esr_el2
b 0f
1:
mrs x0, esr_el1
0:
lsr x1, x0, #26
cmp x1, #0x15 /* 0x15 = SVC */
bne inv
/* Demux the SVC call */
and x1, x0, #0xff
cmp x1, #_SVC_CALL_CONTEXT_SWITCH
beq context_switch /* ★以下参照★ */
...
context_switch:
/*
* Retrieve x0 and x1 from the stack:
* - x0 = new_thread->switch_handle = switch_to thread
* - x1 = x1 = &old_thread->switch_handle = current thread
*/
ldp x0, x1, [sp, #(16 * 10)] /* ★x1 = 切り替え前、x0 = 切り替え後のスレッド★ */
/* Get old thread from x1 */
sub x1, x1, ___thread_t_switch_handle_OFFSET
/* Switch thread */
bl z_arm64_context_switch /* ★コンテキストスイッチ本体(スタックが切り替わる)★ */
exit:
z_arm64_exit_exc x0, x1, x2 /* ★レジスタをスタックから復旧(切り替え後のスレッド)★ */
...
/**
* @brief Routine to handle context switches
*
* This function is directly called either by _isr_wrapper() in case of
* preemption, or z_arm64_svc() in case of cooperative switching.
*/
GTEXT(z_arm64_context_switch)
SECTION_FUNC(TEXT, z_arm64_context_switch)
/* addr of callee-saved regs in thread in x2 */
ldr x2, =_thread_offset_to_callee_saved
add x2, x2, x1
/* Store rest of process context including x30 */
stp x19, x20, [x2], #16
stp x21, x22, [x2], #16
stp x23, x24, [x2], #16
stp x25, x26, [x2], #16
stp x27, x28, [x2], #16
stp x29, x30, [x2], #16
/* Save the current SP */
mov x1, sp
str x1, [x2]
/* addr of callee-saved regs in thread in x2 */
ldr x2, =_thread_offset_to_callee_saved
add x2, x2, x0
/* Restore x19-x29 plus x30 */
ldp x19, x20, [x2], #16
ldp x21, x22, [x2], #16
ldp x23, x24, [x2], #16
ldp x25, x26, [x2], #16
ldp x27, x28, [x2], #16
ldp x29, x30, [x2], #16
ldr x1, [x2]
mov sp, x1 /* ★ここで切り替え後のスレッドのスタックに変わる★ */
#ifdef CONFIG_TRACING
stp xzr, x30, [sp, #-16]!
bl sys_trace_thread_switched_in
ldp xzr, x30, [sp], #16
#endif
/* We restored x30 from the process stack. There are three possible
* cases:
*
* - We return to z_arm64_svc() when swapping in a thread that was
* swapped out by z_arm64_svc() before jumping into
* z_arm64_exit_exc()
* - We return to _isr_wrapper() when swapping in a thread that was
* swapped out by _isr_wrapper() before jumping into
* z_arm64_exit_exc()
* - We return (jump) into z_thread_entry_wrapper() for new threads
* (see thread.c)
*/
ret
// zephyr/include/arm/aarch64/thread.h
struct _callee_saved {
uint64_t x19;
uint64_t x20;
uint64_t x21;
uint64_t x22;
uint64_t x23;
uint64_t x24;
uint64_t x25;
uint64_t x26;
uint64_t x27;
uint64_t x28;
uint64_t x29; /* FP */
uint64_t x30; /* LR */
uint64_t sp;
};
コードを見て一発で理解するのは厳しいので、処理の概要を書いておきます。
コンテキストスイッチの際にスタックが切り替わります。z_arm64_exit_exc x0, x1, x2はシステムコールを呼んだスレッド(=切り替え前のスレッド)ではなく、コンテキストスイッチ後のスレッドのスタックからレジスタを復旧します。
これも文章だと何だかわからないので、紙芝居を書いておきます。
Zephyr AArch64のコンテキストスイッチ: 例外ハンドラ先頭
Zephyr AArch64のコンテキストスイッチ: 例外ハンドラ入り口、スタックへレジスタ退避
Zephyr AArch64のコンテキストスイッチ: コンテキストスイッチ前半、切り替え前スレッド構造体へレジスタ退避
Zephyr AArch64のコンテキストスイッチ: コンテキストスイッチ前半、切り替え前スレッド構造体へスタックポインタ退避
Zephyr AArch64のコンテキストスイッチ: コンテキストスイッチ後半、切り替え後スレッド構造体からレジスタ復旧
Zephyr AArch64のコンテキストスイッチ: コンテキストスイッチ後半、切り替え後スレッド構造体からスタックポインタ復旧(=スタック切り替え)
Zephyr AArch64のコンテキストスイッチ: 例外ハンドラ入り口、切り替え後のスタックからレジスタ復旧
図には書きませんでしたが、例外からリターンする命令(eret命令)が参照するレジスタ(SPSR, ELRレジスタ)もスタックに退避、復旧しています。従ってeretが戻る先は切り替え後のスレッドのコードです。
< | 2020 | > | ||||
<< | < | 09 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | - | 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 | - | - | - |
合計:
本日: