コグノスケ


link 未来から過去へ表示  link 過去から未来へ表示(*)

link もっと前
2021年6月28日 >>> 2021年7月11日
link もっと後

2021年7月1日

OpenCLのOSS実装poclを調べる その6 - OpenCLカーネルのビルド

目次: OpenCL

引き続き、独自アクセラレータのテンプレート実装pocl/lib/CL/devices/accelの細かな問題を調べます。初期化を突破するとカーネルのビルドclBuildProgram() でコケます。

clBuildProgram() でエラー
POCL: in fn compile_and_link_program at line 664:
  |     ERROR | CL_COMPILER_NOT_AVAILABLE Cannot build a program from sources with pocl that does not have online compiler support

原因はdev->compiler_availableがCL_TRUEになっていないことです。初期化が足りないようです。

compiler_availableがfalseのときの動作

// pocl/lib/CL/pocl_build.c

cl_int
compile_and_link_program(int compile_program,
                         int link_program,
                         cl_program program,
                         cl_uint num_devices,
                         const cl_device_id *device_list,
                         const char *options,
                         cl_uint num_input_headers,
                         const cl_program *input_headers,
                         const char **header_include_names,
                         cl_uint num_input_programs,
                         const cl_program *input_programs,
                         void (CL_CALLBACK *pfn_notify) (cl_program program,
                                                         void *user_data),
                         void *user_data)
{

...

      /* clCreateProgramWithBuiltinKernels */
      /* No build step supported at the moment for built-in kernels. */
      if (program->builtin_kernel_names)
        continue;

      /* clCreateProgramWithSource */
      else if (program->source)
        {
#ifdef OCS_AVAILABLE
          if (device->compiler_available == CL_TRUE)    //★このif文が成立せず
            {
              POCL_MSG_PRINT_INFO ("building from sources for device %d\n",
                                   device_i);
              error = pocl_llvm_build_program (
                  program, device_i, program->compiler_options,
                  program_bc_path, num_input_headers, input_headers,
                  header_include_names, (create_library ? 0 : link_program));
              POCL_GOTO_ERROR_ON ((error != 0), build_error_code,
                                  "pocl_llvm_build_program() failed\n");
            }
          else
#endif
            {    //★こちらにきてエラーになってしまう
              APPEND_TO_MAIN_BUILD_LOG (
                  "Cannot build a program from sources with pocl "
                  "that does not have online compiler support\n");
              POCL_GOTO_ERROR_ON (1, CL_COMPILER_NOT_AVAILABLE, "%s",
                                  program->main_build_log);
            }
        }

他の実装を眺めるとpocl_init_default_device_infos() を呼んで解決しているようなので、先達に習いpocl_accel_init() でpocl_init_default_device_infos() を呼び出すように書き換えましょう。

その他のビルド関連の初期化

もともとあったdev->version, dev->available, dev->profileの初期化はpocl_init_default_device_infos() が行いますから、削除しても良いかもしれません。残っていても特に害はないと思いますけど。

現状では拡張機能dev->extensions = "cl_khr_fp64" を設定しないとdouble型を使ったときにビルドエラーになります。最終的に必要な拡張機能がはっきりするまでは必要になったものを順次追加する形で、次に進みましょう。

拡張機能はただ書けば動くわけではなくOpenCL Cコンパイラが対応している必要があります。当たり前ですね。poclが使っているコンパイラは大御所LLVMですから、RISC-V向けだけ機能が欠けていることはないでしょう。たぶん。

ビルド対象はRISC-Vにしたいので、

llvm_traget_tripletとllvm_cpuの設定値

dev->llvm_target_triplet = "riscv32";
dev->llvm_cpu = "generic-rv32";

としました。llvm_target_tripletはclang -cc1の -tripleオプションに渡されます。またllvm_cpuは -target-cpuオプションに渡されます。有効な値を調べる方法は、

llvm_target_tripletの調べ方
$ clang -print-targets

  Registered Targets:
    aarch64    - AArch64 (little endian)
    aarch64_32 - AArch64 (little endian ILP32)
    aarch64_be - AArch64 (big endian)
    amdgcn     - AMD GCN GPUs
...
    riscv32    - 32-bit RISC-V
    riscv64    - 64-bit RISC-V
    sparc      - Sparc
    sparcel    - Sparc LE
    sparcv9    - Sparc V9
    systemz    - SystemZ
...
llvm_cpuの調べ方
$ clang -target=riscv32 -print-supported-cpus

Debian clang version 11.0.1-2
Target: riscv32
Thread model: posix
InstalledDir: /usr/bin
Available CPUs for this target:

        generic-rv32
        generic-rv64
        rocket-rv32
        rocket-rv64
        sifive-e31
        sifive-u54

Use -mcpu or -mtune to specify the target's processor.
For example, clang --target=aarch64-unknown-linux-gui -mcpu=cortex-a35

GCCはコマンド名がアーキテクチャ名そのものですし、LLVMはヘルプで対応アーキテクチャがわかります。どちらも親切で良いですね。

仮実装

ここまでの変更を反映すると、

初期化関数(仮)

// pocl/lib/CL/device/accel/accel.cc

cl_int pocl_accel_init(unsigned j, cl_device_id dev, const char *parameters) {

  AccelData *D = new AccelData;
  dev->data = (void *)D;

  pocl_init_default_device_infos (dev);

  //SETUP_DEVICE_CL_VERSION(1, 2);    //★pocl_init_default_device_infosが初期化するのでいらない
  dev->type = CL_DEVICE_TYPE_CUSTOM;
  dev->long_name = (char *)"memory mapped custom device";
  dev->vendor = "pocl";
  //dev->version = "1.2";        //★pocl_init_default_device_infosが初期化するのでいらない
  //dev->available = CL_TRUE;    //★pocl_init_default_device_infosが初期化するのでいらない

  dev->extensions = "cl_khr_fp64";    //★cl_khr_fp64を追加
  //dev->profile = "FULL_PROFILE";     //★pocl_init_default_device_infosが初期化するのでいらない
  dev->max_mem_alloc_size = 100 * 1024 * 1024;

  dev->llvm_target_triplet = "riscv32";    //★追加
  dev->llvm_cpu = "generic-rv32";          //★追加

  dev->final_linkage_flags = final_ld_flags;

  if (!parameters) {
    POCL_ABORT("accel: parameters were not given\n");
  }

まだまだ変更が必要ですが、こだわるのは後にして次に進みます。

編集者:すずき(2023/09/24 11:58)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年7月2日

OpenCLのOSS実装poclを調べる その7 - poclとLLVMとOpenCLカーネルビルド

目次: OpenCL

今回は少し話題を変えてpoclとClang/LLVMの関係について説明します。poclはOpenCL C言語をビルドするため内部的にClang/LLVMを呼び出します。ビルドはおおまかに2段階に分かれています。設定次第で多少変わりますが、ざっくり各段階で何をしているのか説明します。まだ話題にしていない関数も出てきますが、その話は追々やっていこうと思います。

第1段階OpenCL C -> LLVM IR

最初はclBuildProgram() です。やっていることはOpenCL C -> LLVM IR Bitcode(コンパイル+OpenCLランタイムとのリンク)です。処理に関連する関数と呼び出し関係は下記のとおりです。

clBuildProgram() とLLVM IR Bitcode生成関数の呼び出し関係
clBuildProgram()
  compile_and_link_program()
    pocl_llvm_build_program(): プリプロセス、コンパイル(出力LLVM IR Bitcode)
      link(): OpenCLランタイムとリンク(未定義シンボルの検出)
      pocl_write_module(): 出力LLVM IR Bitcode

この関数の実行中に3つ一時ファイルが出力されます。本来は消されてしまって残らないファイルもありますが、コードを変更し一時ファイルをあえて残すと、

  • tempfile-xx-xx-xx-xx-xx.cl: オリジナルのOpenCL Cコード(xx-xx-... の部分はハッシュ値)
  • tempfile-xx-xx-xx-xx-xx.preproc.cl: プリプロセス後のOpenCL Cコード(xx-xx-... の部分はハッシュ値)
  • ~/.cache/pocl/kcache/XX/XXXXXXXX/program.bc: リンク後のLLVM IR Bitcode(XXはハッシュ値なので名前は様々)

が作成されます。link() は全てのシンボルを問答無用で追加するのではなくて、OpenCLカーネルコードから参照されているシンボル(=未定義のシンボル)のみを追加します。

第2段階LLVM IR -> バイナリ

次はclEnqueueNDRangeKernel() です。やることはLLVM IR Bitcode -> ターゲットバイナリです。

API名はコンパイルやビルドと関係なさそうに見えますが、poclのホストCPU向け実装を見る限り、NDRangeKernelから起因してLLVM IR Bitcodeからターゲットデバイスのバイナリに変換しています(あと何度も変換しなくて良いように生成したバイナリをキャッシュする)。

処理に関連する関数と呼び出し関係は下記のとおりです。

ホストCPU向け実装の呼び出し関係
clEnqueueNDRangeKernel()
  pocl_command_enqueue()
    pocl_pthread_submit()
      pthread_scheduler_push_command(): キューにコマンドを追加する、ワーカースレッドが起床してコマンドを得る

start_thread(): ワーカースレッド
  pocl_pthread_driver_thread()
    pthread_scheduler_get_work()
    check_cmd_queue_for_device(): キューからコマンドを得る、コマンドがNDRangeKernelの実行要求だったら、下記を呼ぶ
      pocl_pthread_prepare_kernel()
        pocl_check_kernel_dlhandle_cache(): dlopen()
          pocl_check_kernel_disk_cache(): キャッシュ
            llvm_codegen(): ターゲットデバイスバイナリ生成
              pocl_llvm_codegen(): .so.oバイナリ作成
              pocl_invoke_clang(): .soバイナリ作成

独自アクセラレータ向け実装もホストCPU向け実装に習いclEnqueueNDRangeKernel() を起点にバイナリに変換を行います。現状はキャッシュ機構はスキップし、毎回ターゲットバイナリを生成する実装です。このままだと非常に遅いので今後、改善する必要があります。

独自アクセラレータ向け実装(仮)の呼び出し関係
clEnqueueNDRangeKernel()
  pocl_command_enqueue()
    pocl_accel_submit(): ここはターゲットデバイスごとに実装が異なる
      scheduleCommands()
        pocl_exec_command()
          pocl_accel_run(): ここから先はターゲットデバイスごとに実装が異なる
            llvm_codegen(): ターゲットデバイスバイナリ生成
              pocl_llvm_codegen(): .so.oバイナリ作成
              pocl_invoke_clang(): .soバイナリ作成

この関数の実行中に2つ一時ファイルが出力されます。本来は消されてしまって残らないファイルもありますが、コードを変更し一時ファイルをあえて残すと、

  • tempfile-xx-xx-xx-xx-xx.so.o: 中間バイナリ
  • XX/XXXXXXXX/kernelName/xx-x-x/kernelName.so: 最終的なOpenCLカーネルバイナリ

が作成されます。中間バイナリと最終バイナリの差は、C言語のコンパイルのときに生成する *.oと実行ファイルとの違いと同様に、前者はアドレスが解決されておらず、後者はアドレス解決済みという差がありました。他にも何か違いがあるかもしれません。

第1段階で紹介した通りclBuildProgram() のlink() にてOpenCLカーネルに必要なシンボルが集められます。そのため中間バイナリと最終バイナリを比較しても、含まれる関数やシンボルはほぼ変わりません(例外はclang_rt.builtinsに依存した関数、例えば __adddf3() など)。

編集者:すずき(2023/09/24 11:58)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年7月3日

OpenCLのOSS実装poclを調べる その8 - poclとClang/LLVMの引数

目次: OpenCL

前回に続いてpoclとClang/LLVMの関係について説明します。Clangは引数の体系が2つあります。1つは通常使用するGCCと互換があるオプション、もう1つはclang -cc1オプションを指定したときに使う内部用オプションです。正式な名前がわからないので適当に呼んでいます。正式な名前をご存知の方は教えていただけると嬉しいです。

前者と後者は -Iや -oのように同じ名前の場合もありますし、顕著に異なる場合もあります。RISC-V向けのmarchやmabiは差が顕著なので、例として紹介します。

GCC互換オプション
$ riscv32-unknown-elf-gcc b.c -c -march=rv32gc -mabi=ilp32f

$ clang --target=riscv32 b.c -c -march=rv32gc -mabi=ilp32f

GCCとClangのオプションがほぼ同じですね。次にClang内部用のオプションを確認します。内部用のオプションは -vを指定すると表示できます。

Clang内部用オプションを表示
$ clang --target=riscv32 b.c -c -march=rv32gc -mabi=ilp32f -v

Debian clang version 11.0.1-2
Target: riscv32--
Thread model: posix
InstalledDir: /usr/bin
 (in-process)
 "/usr/lib/llvm-11/bin/clang" -cc1 -triple riscv32-- -emit-obj -mrelax-all -disable-free -disable-llvm-verifier -discard-value-names -main-file-name b.c -mrelocation-model static -mframe-pointer=all -fmath-errno -fno-rounding-math -mconstructor-aliases -nostdsysteminc -target-feature +m -target-feature +a -target-feature +f -target-feature +d -target-feature +c -target-feature +relax -target-feature -save-restore -target-abi ilp32f -msmall-data-limit 8 -fno-split-dwarf-inlining -debugger-tuning=gdb -v -resource-dir /usr/lib/llvm-11/lib/clang/11.0.1 -internal-isystem include -fdebug-compilation-dir /home/katsuhiro/share/projects/c/clang_test -ferror-limit 19 -fno-signed-char -fgnuc-version=4.2.1 -fcolor-diagnostics -faddrsig -o b.o -x c b.c
clang -cc1 version 11.0.1 based upon LLVM 11.0.1 default target x86_64-pc-linux-gnu
ignoring nonexistent directory "include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/llvm-11/lib/clang/11.0.1/include
End of search list.

先ほどと全く違うことがわかると思います。このままだと色々ごちゃごちゃ表示されていてわかりにくいので、対応するオプションだけ抜粋します。

Clang内部用オプション(対応部分のみ抜粋)
--target=riscv32: -triple riscv32--
-march=rv32gc   : -target-feature +m -target-feature +a -target-feature +f -target-feature +d -target-feature +c
-mabi=ilp32f    : -target-abi ilp32f

なお -target-featureはカンマ区切りで複数指定することもできるようです。上記で言えば -target-feature +m,+a,+f,+d,+cにしても良いです。

poclと何の関係が?

なぜこの話をしたのかというとpocl内でLLVMを呼び出す際は、Clang内部用オプションを使わなければならない箇所があるからです。一番わかりやすい(文字列の形でオプションを指定している)のはpocl_llvm_build_program() です。

poclがClang内部用オプションを使っている箇所(文字列で設定するタイプ)

// pocl/lib/CL/pocl_llvm_build.cc

int pocl_llvm_build_program(cl_program program,
                            unsigned device_i,
                            const char *user_options_cstr,
                            char *program_bc_path,
                            cl_uint num_input_headers,
                            const cl_program *input_headers,
                            const char **header_include_names,
                            int linking_program)

{

...

  CompilerInvocation &pocl_build = CI.getInvocation();

...

  // This is required otherwise the initialization fails with
  // unknown triple ''
  ss << "-triple=" << device->llvm_target_triplet << " ";    //★これ
  if (device->llvm_cpu != NULL)
    ss << "-target-cpu " << device->llvm_cpu << " ";    //★これも同様

...

  std::istream_iterator<std::string> begin(ss);
  std::istream_iterator<std::string> end;
  std::istream_iterator<std::string> i = begin;
  std::vector<const char*> itemcstrs;
  std::vector<std::string> itemstrs;
  while (i != end) {
    itemstrs.push_back(*i);    //★std::vectorにオプションのstd::stringを並べる
    ++i;
  }

  for (unsigned idx = 0; idx < itemstrs.size(); idx++) {
    // note: if itemstrs is modified after this, itemcstrs will be full
    // of invalid pointers! Could make copies, but would have to clean up then...
    itemcstrs.push_back(itemstrs[idx].c_str());    //★std::vectorにオプションの文字列のポインタを並べる
  }

...

  //★コンパイラに上記のオプションを指定する(この時点ではまだコンパイラは起動しない)
  if (!CompilerInvocation::CreateFromArgs(
          pocl_build,    //★CompilerInvocation
#ifndef LLVM_OLDER_THAN_10_0
          ArrayRef<const char *>(itemcstrs.data(),
                                 itemcstrs.data() + itemcstrs.size()),
#else
          itemcstrs.data(), itemcstrs.data() + itemcstrs.size(),
#endif
          diags)) {

あとはpocl_llvm_codegen() も同様ですが、指定の方法が違います。-target-feature +mのような文字列ではなく、TargetMachineのメソッドを呼び出して指定します。コードで見たほうがわかりやすいでしょう。

poclがClang内部用オプションを使っている箇所(関数で設定するタイプ)

// pocl/lib/CL/pocl_llvm_wg.cc

int pocl_llvm_codegen(cl_device_id Device, void *Modp, char **Output,
                      uint64_t *OutputSize) {

...

  llvm::Triple Triple(Device->llvm_target_triplet);
  llvm::TargetMachine *Target = GetTargetMachine(Device, Triple);

  // First try direct object code generation from LLVM, if supported by the
  // LLVM backend for the target.
  bool LLVMGeneratesObjectFiles = true;

  SmallVector<char, 4096> Data;
  llvm::raw_svector_ostream SOS(Data);
  bool cannotEmitFile;

  cannotEmitFile = Target->addPassesToEmitFile(PMObj, SOS,
#ifndef LLVM_OLDER_THAN_7_0
                                  nullptr,
#endif
                                  CODEGEN_FILE_TYPE_NS::CGFT_ObjectFile);

  Target->setTargetFeatureString("+m,+a,+f");    //★-target-feature +m,+a,+fに相当する

独自アクセラレータ向けの実装を行う際にClang/LLVMのオプションを変えたくなることは多々ありますから、今回のオプションの違いは今後の説明でも登場するはずです。たぶん。

編集者:すずき(2023/09/24 11:58)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年7月5日

OpenOCDとHiFive UnleashedのSPI Flashその1 - UnleashedとOpenOCD

目次: OpenOCD

OpenOCDではSiFive HiFive1のSPI Flashへの書き込み&消去ができます。しかしSiFive HiFive UnleashedのSPI Flashに対しては正常に動作しません。書き込みは成功しますが、消去はできません。一見すると成功しているのに一切データが消去できない謎の現象が発生します。

HiFive UnleashedのSPI FlashにOpenOCDで書き込み&消去を行った例を示します。OpenOCDの制御方法は何通りかあります。直接制御したい場合はポート4444にtelnetすると良いです。

OpenOCDにtelnetで接続

$ telnet localhost 4444

Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger

> reset halt
JTAG tap: riscv.cpu tap/device found: 0x20000913 (mfg: 0x489 (SiFive Inc), part: 0x0000, ver: 0x2)

私はGDB経由で制御するほうが慣れているので、GDB経由で制御します。GDBの場合はポート3333に接続します。この例ではZephyr用のツールチェーンに含まれるGDBを使っていますが、RV64に対応していれば何でも良いです(RV32専用ではダメです、UnleashedのCPUはRV64なので)。

OpenOCDにGDBで接続
$ riscv64-zephyr-elf-gdb

(gdb) set arch riscv:rv64

The target architecture is assumed to be riscv:rv64

(gdb) target remote :3333

Remote debugging using :3333

(gdb) monitor reset halt

JTAG tap: riscv.cpu tap/device found: 0x20000913 (mfg: 0x489 (SiFive Inc), part: 0x0000, ver: 0x2)

★★monitor xxxxはremote monitor(この場合OpenOCD)へのコマンドと解釈される

(gdb) monitor flash probe 0

Found flash device 'issi is25wp256d' (ID 0x0019709d)
device needs paging or 4-byte addresses - not implemented    ★★ん?なんだこれ
flash 'fespi' found at 0x20000000

(gdb) x/32x 0x20000000

0x20000000:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000010:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000020:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000030:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000040:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000050:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000060:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000070:     0xffffffff      0xffffffff      0xffffffff      0xffffffff

(gdb) monitor flash fillw 0x20000000 0xddccbbaa 0x10

Disabling abstract command writes to CSRs.
wrote 64 bytes to 0x20000000 in 0.203977s (0.306 KiB/s)

★★書き込みに成功


(gdb) x/32x 0x20000000

0x20000000:     0xddccbbaa      0xddccbbaa      0xddccbbaa      0xddccbbaa
0x20000010:     0xddccbbaa      0xddccbbaa      0xddccbbaa      0xddccbbaa
0x20000020:     0xddccbbaa      0xddccbbaa      0xddccbbaa      0xddccbbaa
0x20000030:     0xddccbbaa      0xddccbbaa      0xddccbbaa      0xddccbbaa
0x20000040:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000050:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000060:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000070:     0xffffffff      0xffffffff      0xffffffff      0xffffffff

★★書き込めた


(gdb) monitor flash erase_address 0x20000000 0x10000

erased address 0x20000000 (length 65536) in 0.204166s (313.470 KiB/s)

★★消去も成功したように見えるものの


(gdb) x/32x 0x20000000

0x20000000:     0xddccbbaa      0xddccbbaa      0xddccbbaa      0xddccbbaa
0x20000010:     0xddccbbaa      0xddccbbaa      0xddccbbaa      0xddccbbaa
0x20000020:     0xddccbbaa      0xddccbbaa      0xddccbbaa      0xddccbbaa
0x20000030:     0xddccbbaa      0xddccbbaa      0xddccbbaa      0xddccbbaa
0x20000040:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000050:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000060:     0xffffffff      0xffffffff      0xffffffff      0xffffffff
0x20000070:     0xffffffff      0xffffffff      0xffffffff      0xffffffff

★★消えないぞ……?

書き込めるのに消せないとは?これいかに?この謎を追います。最初にOpenOCDがどうやってSPI Flashを書き換えているのか?次にどうやって消去しているのか?を追います。妙な警告メッセージが出ている点も気になります。

SPI Flashとは?

その名の通りSPIで接続されたFlashメモリデバイスのことです。SPIはSerial Peripheral Interfaceの略で、CE#(Chip EnableもしくはChip Select, CS# とも)、SCLK、SDI、SDOの4線で構成される非常にシンプルなI/Oバスです。

個人的には「入力と出力が常に同時に行われるのがSPIの大きな特徴」に思います。


SPIの入出力波形の例(SPI FlashのRead Product Identificationコマンド)

例えばこの波形図ではSoCからSPI FlashにInstruction(0xAB) と3バイトのダミーデータを出力(SI側の信号)しているとき、SPI FlashからSoCに意味のないデータが返ってきます(SO側の信号)。

入出力が別々に行われるバス(Ethernetなど)に慣れていると、無意味なデータを受け取る=無駄なリクエストを相手に送っている、ことを示しますから、無駄な入力はやめてくれ〜としばらく混乱しました。しかしまあ、わかってしまえば非常に単純な話でして、常に入出力が同時に行われる仕様なので、無駄は発生していません。また、無意味な入力データは受け取った後に無視すれば良いだけです。

続きはまた次回。

編集者:すずき(2023/09/24 09:17)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年7月6日

OpenOCDとHiFive UnleashedのSPI Flashその2 - OpenOCDのSPI Flashドライバの仕組み(書き込み編、通常版)

目次: OpenOCD

SPI Flashにどのように書き込みするか見ていきます。OpenOCDのSPI Flash書き込みは大きく2つの方法に分けられます。ヘルパープログラムを使う方法と、SPIを直接制御する方法です。順に紹介します。

ヘルパープログラム

後述しますがJTAGからSPIインタフェースを制御すればSPI Flashの書き換えは可能です。しかし速度が出ません。そのためOpenOCDはヘルパープログラムを使った高速な書き込みを実装しています。

SiFive SoC向けのSPI Flashインタフェースのコードを見ると、下記のような謎の #includeが出てきます。参照先のファイルにはバイナリが羅列されています。

ヘルパープログラムのバイナリを保持する配列の定義

// openocd/src/flash/nor/fespi.c

static const uint8_t algorithm_bin[] = {
#include "../../../contrib/loaders/flash/fespi/fespi.inc"
};


// openocd/contrib/loaders/flash/fespi/fespi.inc

/* Autogenerated with ../../../../src/helper/bin2char.sh */
0x6f,0x00,0xc0,0x01,0x73,0x00,0x10,0x00,0x6f,0x00,0xc0,0x02,0x6f,0x00,0x00,0x05,
0x6f,0x00,0xc0,0x05,0x6f,0x00,0x00,0x07,0x6f,0x00,0x00,0x0a,0x83,0xc2,0x05,0x00,

...

このファイルの元となるコード(アセンブラです)は下記のディレクトリにあります。RISC-V向けのクロスコンパイラを用意して、このディレクトリでmakeを実行するとfespi.incを自分で作成することも可能です。

ヘルパープログラムのソースコードの位置
$ ls openocd/contrib/loaders/flash/fespi/

Makefile  fespi.S  fespi.inc

Makefileではfespi.Sをコンパイルしてバイナリをfespi.incに変換します。バイナリからfespi.incへの変換にはsrc/helper/bin2char.shというヘルパースクリプトを使います。

SPI Flashへの書き込み

前置きが長くなりましたが、フラッシュの書き込みルーチンは下記のとおりです。

OpenOCDのSPI Flash書き込みドライバ(SiFive SPI用)

// openocd/src/flash/nor/fespi.c

static int fespi_write(struct flash_bank *bank, const uint8_t *buffer,
		uint32_t offset, uint32_t count)
{

...

	struct working_area *algorithm_wa;
	if (target_alloc_working_area(target, sizeof(algorithm_bin),
				&algorithm_wa) != ERROR_OK) {
		LOG_WARNING("Couldn't allocate %zd-byte working area.",
				sizeof(algorithm_bin));
		algorithm_wa = NULL;
	} else {
		retval = target_write_buffer(target, algorithm_wa->address,
				sizeof(algorithm_bin), algorithm_bin);    //★ヘルパープログラム本体algorithm_binをターゲットのメモリに書く
		if (retval != ERROR_OK) {
			LOG_ERROR("Failed to write code to " TARGET_ADDR_FMT ": %d",
					algorithm_wa->address, retval);
			target_free_working_area(target, algorithm_wa);
			algorithm_wa = NULL;
		}
	}

...

	page_offset = offset % page_size;
	/* central part, aligned words */
	while (count > 0) {
		/* clip block at page boundary */
		if (page_offset + count > page_size)
			cur_count = page_size - page_offset;
		else
			cur_count = count;

		if (algorithm_wa)
			retval = steps_add_buffer_write(as, buffer, offset, cur_count);    //★バッファに書き込むデータを追加する(通常はこちら)
		else
			retval = slow_fespi_write_buffer(bank, buffer, offset, cur_count);    //★JTAGからSPIを直接操作して書き込むモード(遅い)
		if (retval != ERROR_OK)
			goto err;

		page_offset = 0;
		buffer += cur_count;
		offset += cur_count;
		count -= cur_count;
	}

	//★ヘルパープログラムに書き込むデータを全部渡し、フラッシュに書き込みしてもらう
	if (algorithm_wa)
		retval = steps_execute(as, bank, algorithm_wa, data_wa);

...


static int steps_execute(struct algorithm_steps *as,
		struct flash_bank *bank, struct working_area *algorithm_wa,
		struct working_area *data_wa)
{

...

	while (!as_empty(as)) {
		keep_alive();
		uint8_t *data_buf = malloc(data_wa->size);
		unsigned bytes = as_compile(as, data_buf, data_wa->size);
		retval = target_write_buffer(target, data_wa->address, bytes,
				data_buf);
		free(data_buf);
		if (retval != ERROR_OK) {
			LOG_ERROR("Failed to write data to " TARGET_ADDR_FMT ": %d",
					data_wa->address, retval);
			goto exit;
		}

		retval = target_run_algorithm(target, 0, NULL, 2, reg_params,
				algorithm_wa->address, algorithm_wa->address + 4,
				10000, NULL);    //★ヘルパープログラムでflash write

...


// openocd/src/target/target.c

int target_run_algorithm(struct target *target,
		int num_mem_params, struct mem_param *mem_params,
		int num_reg_params, struct reg_param *reg_param,
		uint32_t entry_point, uint32_t exit_point,
		int timeout_ms, void *arch_info)
{
	int retval = ERROR_FAIL;

	if (!target_was_examined(target)) {
		LOG_ERROR("Target not examined yet");
		goto done;
	}
	if (!target->type->run_algorithm) {
		LOG_ERROR("Target type '%s' does not support %s",
				target_type_name(target), __func__);
		goto done;
	}

	target->running_alg = true;
	retval = target->type->run_algorithm(target,
			num_mem_params, mem_params,
			num_reg_params, reg_param,
			entry_point, exit_point, timeout_ms, arch_info);    //★riscv_run_algorithmへ


// openocd/src/target/riscv/riscv.c

/* Algorithm must end with a software breakpoint instruction. */
static int riscv_run_algorithm(struct target *target, int num_mem_params,
		struct mem_param *mem_params, int num_reg_params,
		struct reg_param *reg_params, target_addr_t entry_point,
		target_addr_t exit_point, int timeout_ms, void *arch_info)
{

...

	/* Save registers */
	struct reg *reg_pc = register_get_by_name(target->reg_cache, "pc", true);
	if (!reg_pc || reg_pc->type->get(reg_pc) != ERROR_OK)
		return ERROR_FAIL;
	uint64_t saved_pc = buf_get_u64(reg_pc->value, 0, reg_pc->size);    //★復帰するときに使う
	LOG_DEBUG("saved_pc=0x%" PRIx64, saved_pc);

...

	/* Run algorithm */
	LOG_DEBUG("resume at 0x%" TARGET_PRIxADDR, entry_point);
	if (riscv_resume(target, 0, entry_point, 0, 0, true) != ERROR_OK)    //★ヘルパープログラムを起動する
		return ERROR_FAIL;

...

かなり複雑で全部説明するのは難しいですが、処理の大まかな流れとしては下記のようになっています。

  • ヘルパープログラムをターゲットのメモリに書く
  • ヘルパープログラムに渡す制御データを作成する(SPIの制御コマンド+書き込むデータ列で構成されています)
  • ヘルパープログラムの後ろに制御データを書く
  • ヘルパープログラムを実行する

あとはデータが尽きるまで3つ目と4つ目の処理を実行し続けます。関数でいうとsteps_execute() です。1回で書き込む量は「ページ」のサイズに依存します。ページサイズはSPI Flashデバイスによって違いますが、128バイトか256バイトが多いです。

OpenOCDが対応しているSPI Flashは下記のファイルにまとまっていて、

SPI Flashのパラメータを保持している配列の定義

// openocd/src/flash/nor/spi.c

const struct flash_device flash_devices[] = {
	/* name, read_cmd, qread_cmd, pprog_cmd, erase_cmd, chip_erase_cmd, device_id,
	 * pagesize, sectorsize, size_in_bytes */
	FLASH_ID("st m25p05",           0x03, 0x00, 0x02, 0xd8, 0xc7, 0x00102020, 0x80,  0x8000,  0x10000),
	FLASH_ID("st m25p10",           0x03, 0x00, 0x02, 0xd8, 0xc7, 0x00112020, 0x80,  0x8000,  0x20000),
	FLASH_ID("st m25p20",           0x03, 0x00, 0x02, 0xd8, 0xc7, 0x00122020, 0x100, 0x10000, 0x40000),

...

	FLASH_ID("issi is25lp128d",     0x03, 0xeb, 0x02, 0xd8, 0xc7, 0x0018609d, 0x100, 0x10000, 0x1000000),
	FLASH_ID("issi is25wp128d",     0x03, 0xeb, 0x02, 0xd8, 0xc7, 0x0018709d, 0x100, 0x10000, 0x1000000),
	FLASH_ID("issi is25lp256d",     0x13, 0xec, 0x12, 0xdc, 0xc7, 0x0019609d, 0x100, 0x10000, 0x2000000),
	FLASH_ID("issi is25wp256d",     0x13, 0xec, 0x12, 0xdc, 0xc7, 0x0019709d, 0x100, 0x10000, 0x2000000),

...

例えばHiFive Unleashedに搭載されているSPI FlashはISSI is25wp256dですから、ページサイズは0x100 = 256バイトとわかります。

この情報だけでは不安ならデータシートの8.10 PAGE PROGRAM OPERATION (PP, 02h or 4PP, 12h) を見ていただくと確実です。"The Page Program (PP/4PP) instruction allows up to 256 bytes data ..." とあります。

SPI Flashの書き込みにはもっと簡単な(ただし遅い)方法もあります。今回の処理は実用的な反面、書き込みとは直接関係ない機構(ヘルパープログラム)が登場してやや複雑でした。じっくり見るのは面倒だなと思った方は、次回の簡易版を見ていただくほうがわかりやすいかもしれません。

続きはまた今度。

編集者:すずき(2023/09/24 09:17)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年7月7日

OpenOCDとHiFive UnleashedのSPI Flashその3 - OpenOCDのSPI Flashドライバの仕組み(書き込み編、簡易版)

目次: OpenOCD

フラッシュの書き込みは、ヘルパープログラムを使うタイプ(上記で追いかけた方)と、SPIを直接制御する方があります。高速に書き込めるのはヘルパープログラムを使うタイプですが、わかりやすいのはSPIを直接制御するタイプです。そちらも見てみましょう。

OpenOCDのSPI Flash書き込みドライバ(SiFive SPI用)

// openocd/src/flash/nor/fespi.c

static int fespi_write(struct flash_bank *bank, const uint8_t *buffer,
		uint32_t offset, uint32_t count)
{

...

	struct working_area *algorithm_wa;
	if (target_alloc_working_area(target, sizeof(algorithm_bin),
				&algorithm_wa) != ERROR_OK) {
		LOG_WARNING("Couldn't allocate %zd-byte working area.",
				sizeof(algorithm_bin));
		algorithm_wa = NULL;
	} else {
		retval = target_write_buffer(target, algorithm_wa->address,
				sizeof(algorithm_bin), algorithm_bin);    //★ヘルパープログラム本体algorithm_binをターゲットのメモリに書く
		if (retval != ERROR_OK) {
			LOG_ERROR("Failed to write code to " TARGET_ADDR_FMT ": %d",
					algorithm_wa->address, retval);
			target_free_working_area(target, algorithm_wa);
			algorithm_wa = NULL;
		}
	}

...

	page_offset = offset % page_size;
	/* central part, aligned words */
	while (count > 0) {
		/* clip block at page boundary */
		if (page_offset + count > page_size)
			cur_count = page_size - page_offset;
		else
			cur_count = count;

		if (algorithm_wa)
			retval = steps_add_buffer_write(bank, as, buffer, offset, cur_count);    //★バッファに書き込むデータを追加する(通常はこちら)
		else
			retval = slow_fespi_write_buffer(bank, buffer, offset, cur_count);    //★JTAGからSPIを直接操作して書き込むモード(遅い)
		if (retval != ERROR_OK)
			goto err;

		page_offset = 0;
		buffer += cur_count;
		offset += cur_count;
		count -= cur_count;
	}

	//★ヘルパープログラムに書き込むデータを全部渡し、フラッシュに書き込みしてもらう
	if (algorithm_wa)
		retval = steps_execute(as, bank, algorithm_wa, data_wa);

...

ここまでは前回と同じです。前回はsteps_add_buffer_write() とsteps_execute() が活躍しましたが、今回はslow_fespi_write_buffer() の方を見ます。

JTAGからSPIを直接操作して書き込むモード(簡単、遅い)

static int slow_fespi_write_buffer(struct flash_bank *bank,
		const uint8_t *buffer, uint32_t offset, uint32_t len)
{
	uint32_t ii;

	if (offset & 0xFF000000) {
		LOG_ERROR("FESPI interface does not support greater than 3B addressing, can't write to offset 0x%" PRIx32,
				offset);
		return ERROR_FAIL;
	}

	/* TODO!!! assert that len < page size */

	fespi_tx(bank, SPIFLASH_WRITE_ENABLE);
	fespi_txwm_wait(bank);

	if (fespi_write_reg(bank, FESPI_REG_CSMODE, FESPI_CSMODE_HOLD) != ERROR_OK)    //★CE# 信号をLowにする(負論理、処理の開始を示す)
		return ERROR_FAIL;

	fespi_tx(bank, SPIFLASH_PAGE_PROGRAM);    //★PPコマンド = 0x02

	fespi_tx(bank, offset >> 16);    //★アドレス
	fespi_tx(bank, offset >> 8);
	fespi_tx(bank, offset);

	for (ii = 0; ii < len; ii++)    //★データ
		fespi_tx(bank, buffer[ii]);

	fespi_txwm_wait(bank);    //★送信し終わるまで待つ

	if (fespi_write_reg(bank, FESPI_REG_CSMODE, FESPI_CSMODE_AUTO) != ERROR_OK)    //★CE# 信号をHighにする(負論理、処理の終了を示す)
		return ERROR_FAIL;

	keep_alive();

	return ERROR_OK;
}


// openocd/src/flash/nor/spi.h

/* SPI Flash Commands */
#define SPIFLASH_READ_ID		0x9F /* Read Flash Identification */
#define SPIFLASH_READ_MID		0xAF /* Read Flash Identification, multi-io */
#define SPIFLASH_READ_STATUS	0x05 /* Read Status Register */
#define SPIFLASH_WRITE_ENABLE	0x06 /* Write Enable */
#define SPIFLASH_PAGE_PROGRAM	0x02 /* Page Program */    //★コマンドはこれ

...

コマンド、データ、待機、たったこれだけです。前回はヘルパープログラムの制御などが出てきて複雑でしたが、実はSPI Flashの制御だけ見ると非常にシンプルです。

編集者:すずき(2023/09/24 09:17)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年7月8日

OpenOCDとHiFive UnleashedのSPI Flashその4 - OpenOCDのSPI Flashドライバの仕組み(消去編)

目次: OpenOCD

いよいよ、正常に動かないSPI Flashの消去を見ます。消去は書き込みと違い大量にデータを書く必要がなく、ヘルパープログラムを使った実装はありません。前回紹介したJTAGでSPIを直接制御する方法(簡易版)と似た処理です。

消去の方法

SPI Flashの消去には3種類あります。ブロック消去に対応していないデバイスもあるようです。ややこしいことに統一された名前がないらしくて、メーカーによっては大きな消去単位をセクタと呼んでいます(Micronがそうだった)。なんじゃそりゃ、訳がわからんから統一してくれ〜。

  • 全消去
  • ブロック(セクタ)消去: いくつかのセクタをまとめたブロック単位で消去します、64KBか32KBです
  • セクタ(サブセクタ)消去: セクタ単位で消去します、デバイスによりますが4KB程度です

細かい名前は覚えても意味がない(メーカーによって違うのでどうでも良い)ので、全部消す、大サイズで消す、小サイズで消す、くらいの理解で十分だと思います。

消去の処理

前置きはこれくらいにして、消去処理のコードを見ましょう。

消去処理

// openocd/src/flash/nor/fespi.c

static int fespi_erase_sector(struct flash_bank *bank, int sector)
{
	struct fespi_flash_bank *fespi_info = bank->driver_priv;
	int retval;

	retval = fespi_tx(bank, SPIFLASH_WRITE_ENABLE);
	if (retval != ERROR_OK)
		return retval;
	retval = fespi_txwm_wait(bank);
	if (retval != ERROR_OK)
		return retval;

	if (fespi_write_reg(bank, FESPI_REG_CSMODE, FESPI_CSMODE_HOLD) != ERROR_OK)    //★CE# 信号をLowにする(負論理、処理の開始を示す)
		return ERROR_FAIL;
	retval = fespi_tx(bank, fespi_info->dev->erase_cmd);    //★ブロック64K消去コマンド
	if (retval != ERROR_OK)
		return retval;
	sector = bank->sectors[sector].offset;
	retval = fespi_tx(bank, sector >> 16);    //★アドレス
	if (retval != ERROR_OK)
		return retval;
	retval = fespi_tx(bank, sector >> 8);
	if (retval != ERROR_OK)
		return retval;
	retval = fespi_tx(bank, sector);
	if (retval != ERROR_OK)
		return retval;
	retval = fespi_txwm_wait(bank);    //★送信し終わるまで待つ
	if (retval != ERROR_OK)
		return retval;
	if (fespi_write_reg(bank, FESPI_REG_CSMODE, FESPI_CSMODE_AUTO) != ERROR_OK)    //★CE# 信号をHighにする(負論理、処理の終了を示す)
		return ERROR_FAIL;

	retval = fespi_wip(bank, FESPI_MAX_TIMEOUT);
	if (retval != ERROR_OK)
		return retval;

	return ERROR_OK;
}

ページ書き込みのときはコマンドが決め打ちでしたが、セクター消去ではコマンドが変数になっています。この変数が持つ値は、前回も紹介したSPI Flashのパラメータリストに載っています。もう一度掲載します。

SPI Flashのパラメータを保持している配列の定義

// openocd/src/flash/nor/spi.c

const struct flash_device flash_devices[] = {
	/* name, read_cmd, qread_cmd, pprog_cmd, erase_cmd, chip_erase_cmd, device_id,
	 * pagesize, sectorsize, size_in_bytes */
	FLASH_ID("st m25p05",           0x03, 0x00, 0x02, 0xd8, 0xc7, 0x00102020, 0x80,  0x8000,  0x10000),

...

	//★HiFive1が搭載しているFlash
	FLASH_ID("issi is25lp032",      0x03, 0x00, 0x02, 0xd8, 0xc7, 0x0016609d, 0x100, 0x10000, 0x400000),
	FLASH_ID("issi is25lp064",      0x03, 0x00, 0x02, 0xd8, 0xc7, 0x0017609d, 0x100, 0x10000, 0x800000),
	FLASH_ID("issi is25lp128d",     0x03, 0xeb, 0x02, 0xd8, 0xc7, 0x0018609d, 0x100, 0x10000, 0x1000000),
	FLASH_ID("issi is25wp128d",     0x03, 0xeb, 0x02, 0xd8, 0xc7, 0x0018709d, 0x100, 0x10000, 0x1000000),
	FLASH_ID("issi is25lp256d",     0x13, 0xec, 0x12, 0xdc, 0xc7, 0x0019609d, 0x100, 0x10000, 0x2000000),
	//★HiFive Unleashedが搭載しているFlash
	FLASH_ID("issi is25wp256d",     0x13, 0xec, 0x12, 0xdc, 0xc7, 0x0019709d, 0x100, 0x10000, 0x2000000),

...

HiFive Unleashedが搭載しているSPI Flashの品番はISSI IS25WP256Dですから、erase_cmdは0xdcです。SPI Flashのデータシートを見ると0xdcはBLOCK ERASE OPERATIONの4BER64Kコマンドです。

コマンドとアドレス指定

コードを見たときにお気づきかもしれませんが、SPI Flashの書き込みや消去で渡しているアドレスは3バイトしかなく、最大で16MBまでしか扱えません。一方HiFive Unleashedが搭載しているSPI Flashの容量は256Mbit = 32MBです。

足りないですね?どうやって16MB以降の消去や書き込みを行うのでしょうか?方法は3つあるようです。

  • バンク切替: アドレスの始点を変えて3バイトアドレスコマンドを使う
  • モード切替: 3バイトアドレスコマンドに4バイトアドレスを渡せる
  • 別コマンド: 4バイトアドレスコマンドを使う

OpenOCDは3番目を期待しているようです。なぜかというと4BER64Kコマンドは4バイトアドレスを期待するコマンドだからです。


4BER64Kコマンド仕様

データシートから抜粋したコマンドの仕様は上記の通りです。アドレスを4バイト送ってくることを期待しています。しかし実装は3バイトしかアドレスを送っていません。うーん、何かおかしいですね?

編集者:すずき(2023/09/24 09:17)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年7月9日

OpenOCDとHiFive UnleashedのSPI Flashその5 - UnleashedとOpenOCD消去の謎を追う

目次: OpenOCD

これまでに書き込み、消去の仕組みを調べました。その過程でどうしてHiFive UnleashedでSPI Flashの消去が正常に動作しないのかわかりました。アドレスの指定が間違っているからです。

SPI Flashの消去処理

// openocd/src/flash/nor/fespi.c

static int fespi_erase_sector(struct flash_bank *bank, int sector)
{

...

	retval = fespi_tx(bank, fespi_info->dev->erase_cmd);    //★ブロック64K消去コマンド(4バイトアドレスを期待)
	if (retval != ERROR_OK)
		return retval;
	sector = bank->sectors[sector].offset;
	retval = fespi_tx(bank, sector >> 16);    //★アドレス(3バイトしか送っていない)
	if (retval != ERROR_OK)
		return retval;
	retval = fespi_tx(bank, sector >> 8);
	if (retval != ERROR_OK)
		return retval;
	retval = fespi_tx(bank, sector);
	if (retval != ERROR_OK)
		return retval;

消去コマンドは0xdc(4BER64Kコマンド)で4バイトアドレスを期待していますが、今のOpenOCDの実装は3バイトアドレスを渡しています。これでは動作しません。最初に動作確認したログを覚えているでしょうか?改めて見直すと、

flash probe実行時の警告
$ riscv64-zephyr-elf-gdb

(gdb) set arch riscv:rv64

The target architecture is assumed to be riscv:rv64

(gdb) target remote :3333

Remote debugging using :3333

(gdb) monitor reset halt

JTAG tap: riscv.cpu tap/device found: 0x20000913 (mfg: 0x489 (SiFive Inc), part: 0x0000, ver: 0x2)

★★monitor xxxxはremote monitor(この場合OpenOCD)へのコマンドと解釈される

(gdb) monitor flash probe 0

Found flash device 'issi is25wp256d' (ID 0x0019709d)
device needs paging or 4-byte addresses - not implemented    ★★ん?なんだこれ
flash 'fespi' found at 0x20000000

OpenOCDが4バイトアドレスには対応していないよ、という警告を出していました。なるほど、やっと警告の意味がわかりました。

書き込みはどうして動くのか?

Unleashedの消去コマンドが動作しない理由はわかりましたが、まだいくつか不思議な点が残っています。

Unleashedで書き込みできるのはなぜ?
書き込みコマンドとして0x02(PPコマンド)を決め打ちしているためです。PPコマンドは3バイトアドレスを期待するので正常に動作します。ただし16MB以降には書き込みできないので、今の実装は十分とは言えません。
HiFive1が正常に書き換え、消去ができるのはなぜ?
搭載されているSPI Flashの型番が違うためです。HiFive1はISSI IS25LP032を搭載しています。SPI Flashのパラメータリストを見るとerase_cmd = 0xd8(BER64Kコマンド)になっていて、BER64Kコマンドは3バイトアドレスを期待するので正常に動作します。容量も小さいので3バイトアドレスで十分です。

比較しやすいようにSPI FlashのパラメータリストのうちHiFive1とHiFive Unleashedに関わるところだけ抜粋しておきます。

SPI Flashのパラメータを保持している配列の定義

// openocd/src/flash/nor/spi.c

const struct flash_device flash_devices[] = {
	/* name, read_cmd, qread_cmd, pprog_cmd, erase_cmd, chip_erase_cmd, device_id,
	 * pagesize, sectorsize, size_in_bytes */
	FLASH_ID("st m25p05",           0x03, 0x00, 0x02, 0xd8, 0xc7, 0x00102020, 0x80,  0x8000,  0x10000),

...

	//★HiFive1が搭載しているFlash
	FLASH_ID("issi is25lp032",      0x03, 0x00, 0x02, 0xd8, 0xc7, 0x0016609d, 0x100, 0x10000, 0x400000),

...

	//★HiFive Unleashedが搭載しているFlash
	FLASH_ID("issi is25wp256d",     0x13, 0xec, 0x12, 0xdc, 0xc7, 0x0019709d, 0x100, 0x10000, 0x2000000),

...

結論としてはOpenOCDの実装を変えないとHiFive UnleashedのSPI Flashの書き込み、消去は正常に行えないことがわかりました。手順では回避不可能です。

編集者:すずき(2023/09/24 09:18)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする




link もっと前
2021年6月28日 >>> 2021年7月11日
link もっと後

管理用メニュー

link 記事を新規作成

<2021>
<<<06>>>
--12345
6789101112
13141516171819
20212223242526
27282930---

最近のコメント5件

  • link 24年10月1日
    すずきさん (10/06 03:41)
    「xrdpで十分動作しているので、Wayl...」
  • link 24年10月1日
    hdkさん (10/03 19:05)
    「GNOMEをお使いでしたら今はWayla...」
  • link 24年10月1日
    すずきさん (10/03 10:12)
    「私は逆にVNCサーバーに繋ぐ使い方をした...」
  • link 24年10月1日
    hdkさん (10/03 08:30)
    「おー、面白いですね。xrdpはすでに立ち...」
  • link 14年6月13日
    2048player...さん (09/26 01:04)
    「最後に、この式を出すのに紙4枚(A4)も...」

最近の記事3件

  • link 24年10月28日
    すずき (10/30 23:49)
    「[Linuxからリモートデスクトップ] 目次: Linux開発用のLinuxマシンの画面を見るにはいろいろな手段がありますが、...」
  • link 23年4月10日
    すずき (10/30 23:46)
    「[Linux - まとめリンク] 目次: Linux関係の深いまとめリンク。目次: RISC-V目次: ROCK64/ROCK...」
  • link 24年10月24日
    すずき (10/25 02:35)
    「[ONKYOからM-AUDIOのUSB DACへ] 目次: PCかれこれ10年以上(2013年3月16日の日記参照)活躍してく...」
link もっとみる

こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報

合計:  counter total
本日:  counter today

link About www.katsuster.net
RDFファイル RSS 1.0

最終更新: 10/30 23:49