Forth ってなんだっけ?
Forth はプログラミング言語の一つです。どういうわけか Wikipedia の
Forth の説明が充実しています。
何が出来るの?
(わたしの使い方は)主にインタラクティブにテストするときに使ってます。
gforth を使った例
4 5 + ok
. 9 ok
逆ポーランド記法です。Forth じゃないけど dc とか Linux で使いますよね。
LED 点灯例
足し算したいわけじゃないと思うので、他に何が出来るか?特に FPGA の開発時に。
次のようにすると LED が光りますよ!すげー便利。
>4 leds
ok
すごい人がいるもんだ
Forth というと bootstrap に技術の粋が集められている一面もあったりするのですが、世の中にはすごい人がいるもんで、それをばっちり実装・解説してくれている記事があります。興味のある人は、読むだけでなく、ぜひ実装しましょう!!
忘れ去られている?
とはいえ、いまとなってはフォーカスする人も少なく雑誌インタフェースでまともに扱ったのは 1980 年代だったりして、もはや過去の言語となりつつある感もあります。組込みシステムの世界では Rust も充実しているし、組み込むなら lua 組み込んだほうが汎用性があったりするのでわざわざ Forth を思い出す必要もないかもしれません。
FPGA と組み合わせると微妙に便利
FPGA の世界にソフトウェアの便利さを持ってこようとすると RISC-V とか(昔なら MicroBlaze や Nios-II)を持ってくればいいので、これも Forth の出番がないようにも思えます。FPGA での Forth は C のコンパイラを必要としないので、ソフトウェアの対応が比較的早くなります。拡張性もあるので、開発時にちょこっと便利になります。
Swapforth があるじゃないか
今回使ったのは FPGA 用の Forth である Swapforth です。作者は James Bowman さん。
2種類の Forth を用意していて 16bit 版と 32bit 版がある。32 bit 版は Zynq でも動くみたい。今回使ったのは 16bit 版の j1a
- J1a - minimal 16-bit FPGA CPU with 8K of memory
- J1b - 32-bit FPGA CPU with 32K of memory
ターゲットは Tang Nano 9K
移植の注意点は UART。これさえ動けば Forth が動きます。最初はもともとの j1a にくっついていた簡単 UART を使っていたのですが、ちゃんと Gowin の UART IP コアを使うようにしました。トップはこんな感じ。
module top(
input wire clock,
input wire sw0_i,
input wire sw1_i,
output wire [CS_WIDTH-1:0] O_psram_ck,
output wire [CS_WIDTH-1:0] O_psram_ck_n,
inout wire [CS_WIDTH-1:0] IO_psram_rwds,
inout wire [DQ_WIDTH-1:0] IO_psram_dq,
output wire [CS_WIDTH-1:0] O_psram_reset_n,
output wire [CS_WIDTH-1:0] O_psram_cs_n,
output wire tmds_clk_n,
output wire tmds_clk_p,
output wire [2:0] tmds_d_n,
output wire [2:0] tmds_d_p,
output wire uart_tx,
input wire uart_rx,
output wire [5:0] led_o
);
PSRAM は動いたけど、Forth からはまだ使えていません。UART の出力を TMDS にも流しているので無駄に HDMI コネクタ経由で Forth の入力が表示されます。
ミニマム Forth を動かす。
まずはバイナリをダウンロードします。
Jtag frequency : requested 6.00MHz -> real 6.00MHz
Parse file Parse impl/pnr/try_forth.fs:
Done
DONE
Jtag frequency : requested 2.50MHz -> real 2.00MHz
erase SRAM Done
Flash SRAM: [==================================================] 100.00%
Done
SRAM Flash: Success
うまくいくと HDMI はブルーの画面になります。この時点で、もう Forth は動いている。cu で通信できます。
$ cu -115200 -l /dev/ttyUSB1
Connected.
ok
ok
ok
ok
0 ok
ところが、ミニマムな Forth が動いているだけなので . で表示することが出来ない!! 48 emit とすると 0 が表示されるけど、echo バックされます。ディスプレーをみると入力文字も見えるけど、これでは甚だ不便。なお、この時点では 4K bytes 未満の SRAM(ブロックRAM) で OK。leds とかも使えます。のでミニマムでいいならこれでも良いでしょう。
ANS Forth にする
swapforth の便利ルーチンの go というシェルスクリプトを実行すると、ちゃんとした ANS Forth が使えるようになります。この go は python のシリアル用のライブラリを利用していて、ヒストリも使えるから cu を直接使うより便利になっています。その上、include というキーワードが来るとホストコンピュータ(Linux)からファイルを Forth におくって実行してくれます。
: bs_status $0001 io@ $C00 and $a rshift . ;
: fps
$0020 io@ dup $FF00 and 8 rshift .
CR
$00FF and .
$0010 io@ .
CR
;
上の2つのソースは実際に使ったソース。bf.fs が FPGA で開発中のとある status を表示するもの。fps.fs は 表示(DVI)の fps を表示するものです。
FPGA 的な話:簡易バス
j1 のトップは次のようになっています。
j1 j10 (
.clk(clk_d_w),
.resetq(1'b1),
.io_rd(io_rd_w),
.io_wr(io_wr_w),
.mem_addr(mem_addr_w),
.mem_wr(mem_wr_w),
.dout(dout_w),
.io_din(io_din_w),
.code_addr(code_addr_w),
.insn(insn_w)
);
mem_addr というのがアドレスで BRAM にも繋がっています。そして、io_rd とか io_wr がアサートされたときは IO 空間をアクセスするようになっています。メモリマップド?ではなく IO 空間があるような感じです。ANS Forth 動かすには 8K の BRAM が必要でした。
assign io_din_w =
(io_addr_r[0] ? io_addr_01_w[15:0] : 16'd0) |
(io_addr_r[1] ? io_addr_01_w[31:16] : 16'd0) |
(io_addr_r[2] ? {{(16-ILA_ADDR_WIDTH){1'd0}}, ila_wr_addr_w} : 16'd0) |
(io_addr_r[3] ? ila_mem_data_w[15:0] : 16'd0) |
(io_addr_r[4] ? io_addr_45_w[15:0] : 16'd0) |
(io_addr_r[5] ? io_addr_45_w[31:16] : 16'd0) |
/*
(io_addr_r[4] ? video_cap_data_w[15:0] : 16'd0 ) |
(io_addr_r[5] ? {video_cap_done_w, ~video_capturing_r, 6'd0, video_cap_data_
w[23:16]} : 16'd0 ) |
*/
(io_addr_r[6] ? {14'd0, from_psram_r} : 16'd0 ) |
(io_addr_r[7] ? ps_data_w : 16'd0) |
(io_addr_r[8] ? ps_test_rd_data_w : 16'd0) |
(io_addr_r[9] ? {wait_counter_w[4:0], ps_done_get_rd_w, ps_done_rd_w, ps_don
e_wr_w, rd_error_w,
ps_do_wr_r, ps_do_rd_r, psraman_status_w} : 16'd0) |
(io_addr_r[12] ? {8'd0, uart0_data_w} : 16'd0) |
(io_addr_r[13] ? {11'd0, io_init_calib_w, rd_error_w, sw1_i, uart0_valid_w, !uart0_busy_w} : 16'd0);
(CPU からみて)読み込み用の IO空間は14bitあるアドレスをワンホット的に使ってます。排他が面倒だからね。video_cap とか psraman_status とかが泣けます。一応、ビデオキャプチャー出来るようになったのですが、よくよく考えると 115200 bps で一画面を UART で送ると偉い時間がかかります。ので、途中でやめちゃいました。オリジナルの swapforth では画像を圧縮して送る仕組みが入っていましたが、それを入れるのは挫折してしまいました。
書き込み用の IO 空間の記述は次の通り。これもワンホット的に書いてます。
always @(posedge clk_d_w) begin
if (io_wr_r & io_addr_r[3]) begin
ila_mem_addr_r <= dout_r[ILA_ADDR_WIDTH-1:0];
end
if (io_wr_r & io_addr_r[2]) begin
led_r <= dout_r[5:0];
end
/*
if ( io_wr_r & io_addr_r[4] ) begin
video_capturing_r <= ~dout_r[15];
video_cap_vcounter_r <= dout_r[14:0];
end
if ( io_wr_r & io_addr_r[5] ) begin
video_cap_req_r <= dout_r[15];
video_cap_hcounter_r <= dout_r[14:0];
end
*/
if ( io_wr_r & io_addr_r[4] ) begin
fps_sw_r <= dout_r[0];
end
if ( io_wr_r & io_addr_r[5] ) begin
ps_test_wr_data_r = dout_r;
end
if ( io_wr_r & io_addr_r[6] ) begin
from_psram_r <= dout_r[1:0];
end
if ( io_wr_r & io_addr_r[9] ) begin
ps_do_wr_r <= dout_r[8];
ps_do_rd_r <= dout_r[9];
ps_get_rd_r <= dout_r[10];
end
if ( io_wr_r & io_addr_r[7] ) begin
ps_addr_r[15:0] <= dout_r;
end
if ( io_wr_r & io_addr_r[8] ) begin
ps_addr_r[20:16] <= dout_r[4:0];
end
end
もうちょい頑張れば
せっかく VRAM(Video Buffer)作ったので Forth と連携させて絵を書かせたいですね。タートルグラフィックを動かすことのできる Forth のコードを見つけました。手元で gforth(8086版 わざわざつくった) + dos のエミュレータで動くことは確認したんですが、そこで止まっています。
最後にもうちょい Forth のはなし
全体的に Forth を扱うプログラマは少ないという印象です。この間、いろいろチェックして gforth も整理されていないことがわかりました。中身はだいぶ理解したので近いうちにより詳しくまとめたいと思っています。Forth というと逆ポーランド記法とかスタック型言語みたいな捉え方をしますが、それは表面的な話です。逆ポーランド記法は Forth の本質ではありませんし、ちょっとしたワードを登録しようとする、バッファを読みながら評価するから"逆"にしてないじゃんみたいなところもあるわけです(つまり混在している)。スタックもいわゆる C 言語のスタックではありません。レジスタの退避域としてのスタックであり、どちらかというとチューリングマシンの紙テープの使い方に似ているかもしれません。複雑な Forth 独特のコンパイル方式に関連する immediate とか postpone もバッファの使い方から編み出された手法であるようです。スレッデッドコードは primitive と登録されたワードの混在をどのように実現するかという手法です。個人の印象ですが、それらはあくまで手法であって Forth という言語の本質ではない気がします。Forth の本質はワードの連結構造であると思います。実に単純。そんな、ファイルシステムもなくUART入出力 IO があるだけの状態から、高級(!)なCコンパイラもアセンブラも必要とせずにプログラマブルな環境を構築できるようにした便利さが Forth の提供する"価値"なのでしょう。