目次: Linux
PCIデバイスを使った実験をする際にコンフィグ空間やメモリ空間を読み書きしたいときがあります。メモリ空間の読み書き程度ならば、わざわざPCIデバイスドライバを書かなくても /sys/bus/pci/devices/*/ にあるresourceNファイルにアクセスすれば良いです。
PCIデバイスはコンフィグ空間にBAR (Base Address Register) という32bitレジスタが6個あり、メモリ空間をPC側のアドレスのどこにマッピングするかを指定します。BARがそのままresourceNに対応しています。
最近は64bitアドレスでマッピングするデバイスも多いですが、その際は連続するBARを2つ使ってマッピング先のアドレスを指定します。実際に見たほうが早いと思うので、私の使っているマシンのグラフィックカードNVIDIA GeForce GT 1030を例にしましょう。
# lspci | grep VGA 08:00.0 VGA compatible controller: NVIDIA Corporation GP108 [GeForce GT 1030] (rev a1) # lspci -s 08:00.0 -v 08:00.0 VGA compatible controller: NVIDIA Corporation GP108 [GeForce GT 1030] (rev a1) (prog-if 00 [VGA controller]) Subsystem: Micro-Star International Co., Ltd. [MSI] GP108 [GeForce GT 1030] Flags: bus master, fast devsel, latency 0, IRQ 84, IOMMU group 14 Memory at f6000000 (32-bit, non-prefetchable) [size=16M] ★BAR0: resource0 Memory at e0000000 (64-bit, prefetchable) [size=256M] ★BAR1: resource1(64bitアドレス空間なのでBAR2も使われる) Memory at f0000000 (64-bit, prefetchable) [size=32M] ★BAR3: resource3(64bitアドレス空間なのでBAR4も使われる) I/O ports at d000 [size=128] ★BAR5: resource5 Expansion ROM at 000c0000 [virtual] [disabled] [size=128K] Capabilities: [60] Power Management version 3 Capabilities: [68] MSI: Enable- Count=1/1 Maskable- 64bit+ Capabilities: [78] Express Legacy Endpoint, MSI 00 Capabilities: [100] Virtual Channel Capabilities: [128] Power Budgeting <?> Capabilities: [420] Advanced Error Reporting Capabilities: [600] Vendor Specific Information: ID=0001 Rev=1 Len=024 <?> Capabilities: [900] Secondary PCI Express Kernel driver in use: nvidia Kernel modules: nvidia
デバイスの位置(08:00.0)と、マッピングしているアドレスがBAR0, 1, 3, 5の4つ あることがわかります。/sys/bus/pci/devices経由でPCIデバイスのBAR1でマップされているメモリを読み出します。
# cd /sys/bus/pci/devices/0000\:08\:00.0/ # ma -k resource1 dd 0 00000000 ff000020 ff00082c ff000020 ff00082c 00000010 ff3c0020 ff42082c ff990020 ffa50027 00000020 ffa10020 ffa10027 ffa50020 ff990027 00000030 ff420020 ff3c0027 ff000020 ff000027 00000040 ff000020 ff000027 ff000020 ff000027 00000050 ff000020 ff000027 ff000020 ff000027 00000060 ff000020 ff000027 ff000020 ff000027 00000070 ff000020 ff000027 ff000020 ff000027 00000080 ff000020 ff000027 ff000020 ff000027 00000090 ff3c0020 ff420027 ffb90020 ffa50027 000000a0 ffa50020 ffb90027 ffa90020 ffa50027 000000b0 ff420020 ff3c0027 ff000020 ff000027 000000c0 ff000020 ff000027 ff000020 ff000027 000000d0 ff000020 ff000027 ff000020 ff000027 000000e0 ff000020 ff000027 ff000020 ff000027 000000f0 ff000020 ff000027 ff000020 ff000027 00000100
読み出しツールは何を使っても構いませんが、サイズがめっちゃでかい(256MB)ので「頭から読む系のツール(hexdumpやテキストエディタ)」は使わないほうが無難です。今回は拙作のmemaccess(GitHubへのリンク)を使いました。
ちなみにresourceNを読みだそうとすると怒られるときは、NVIDIAのドライバをロードしていないかチェックしてみてください。
# ma -k resource0 dd 0 mmap: Invalid argument # cat /proc/iomem ... e0000000-fec2ffff : PCI Bus 0000:00 e0000000-f1ffffff : PCI Bus 0000:08 e0000000-efffffff : 0000:08:00.0 f0000000-f1ffffff : 0000:08:00.0 f6000000-f70fffff : PCI Bus 0000:08 f6000000-f6ffffff : 0000:08:00.0 f6000000-f6ffffff : nvidia ★BAR0をnvidiaドライバが使用中なのでmmapできない f7080000-f7083fff : 0000:08:00.1 f7080000-f7083fff : ICH HD audio ... # rmmod nvidia_uvm nvidia_drm nvidia_modeset nvidia # cat /proc/iomem ... e0000000-fec2ffff : PCI Bus 0000:00 e0000000-f1ffffff : PCI Bus 0000:08 e0000000-efffffff : 0000:08:00.0 f0000000-f1ffffff : 0000:08:00.0 f6000000-f70fffff : PCI Bus 0000:08 f6000000-f6ffffff : 0000:08:00.0 ★nvidiaドライバがアンロードされた f7080000-f7083fff : 0000:08:00.1 f7080000-f7083fff : ICH HD audio ... # ma -k resource0 dd 0 00000000 138000a1 00000000 00000000 bad00200 00000010 bad00200 bad00200 bad00200 bad00200 00000020 bad00200 bad00200 bad00200 bad00200 00000030 bad00200 bad00200 bad00200 bad00200 00000040 bad00200 bad00200 bad00200 bad00200 00000050 bad00200 bad00200 bad00200 bad00200 00000060 bad00200 bad00200 bad00200 bad00200 00000070 bad00200 bad00200 bad00200 bad00200 00000080 0000008f bad00200 bad00200 bad00200 00000090 bad00200 bad00200 bad00200 bad00200 000000a0 bad00200 bad00200 bad00200 bad00200 000000b0 bad00200 bad00200 bad00200 bad00200 000000c0 bad00200 bad00200 bad00200 bad00200 000000d0 bad00200 bad00200 bad00200 bad00200 000000e0 bad00200 bad00200 bad00200 bad00200 000000f0 bad00200 bad00200 bad00200 bad00200 00000100
ドライバが管理しているメモリを横から読み書きされたらたまったもんじゃないですから、この挙動は当然ですね。え?読み出しなら良いのでは?と思われるかもしれませんが、読み出しをトリガーに動作したり、読み出すと値が変化するレジスタなどは珍しくないので、勝手に読み出すのも実はダメなのです。
先ほど /sys/bus/pci/devices以下のファイルをls -lなどで見た方はお気づきかと思いますが、rootしか読めないようなパーミッションになっています。
# cd /sys/bus/pci/devices/0000\:08\:00.0/ # ls -l resource* -r--r--r-- 1 root root 4096 8月 2 22:08 resource -rw------- 1 root root 16777216 8月 2 22:10 resource0 -rw------- 1 root root 268435456 8月 2 22:10 resource1 -rw------- 1 root root 268435456 8月 2 22:10 resource1_wc -rw------- 1 root root 33554432 8月 2 22:10 resource3 -rw------- 1 root root 33554432 8月 2 22:10 resource3_wc -rw------- 1 root root 128 8月 2 22:10 resource5
誰でもデバイスのメモリにアクセスできるのはマズいですから、セキュリティ上この設定は真っ当です。真っ当ですが、実験に使うときはいちいちsudoするのが面倒なのも事実です。手動でchmodやchownしてパーミッションを変えればsudo不要にできますが、いくつか欠点があります。
どこにいるかわからないデバイスを操作したいときは、udevの出番です。
前置きが長くなりましたが、やっと本題です。/etc/udev/rules.dに99-hogehoge.rulesのようなファイルを作ります。全てのPCIデバイスのパーミッションを全開にする必要はないので、ベンダーIDとデバイスIDでGeForce GT 1030だけ該当するようにフィルタリングしましょう。
# lspci -s 08:00.0 08:00.0 VGA compatible controller: NVIDIA Corporation GP108 [GeForce GT 1030] (rev a1) # lspci -s 08:00.0 -n 08:00.0 0300: 10de:1d01 (rev a1) | `--- Device ID `--- Vendor ID
他の条件をフィルタリングに使いたいときは、udevadm infoを使うと、vendorやdeviceも含んだ全ての属性を見ることができます。
# udevadm info -a -p /sys/bus/pci/devices/0000:08:00.0 | less ... looking at device '/devices/pci0000:00/0000:00:03.1/0000:08:00.0': KERNEL=="0000:08:00.0" SUBSYSTEM=="pci" DRIVER=="" ATTR{ari_enabled}=="0" ... ATTR{revision}=="0xa1" ATTR{subsystem_device}=="0x8c98" ATTR{subsystem_vendor}=="0x1462" ATTR{vendor}=="0x10de" ★ベンダーID ...
IDが判明したらルールファイルを書きます。/sys/bus/pci/devices/* のディレクトリと配下のファイル全てにgroupとotherのRead/Writeパーミッションを付加します。%pは設定中のデバイスのパスが入る変数です。
SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{device}=="0x1d01", RUN+="/bin/chmod -R go+rw /sys%p"
注意点は「RUNはシェルじゃない」ことです。パスの補完、ワイルドカード、パイプ、リダイレクトなどは使えません。私は最初resource* と書いて「そんなファイルはない」と言われたり、/bin/chmodなのに /usr/bin/chmodと書いていて、何も実行されなくて悩みました(Ubuntuは /usr/bin/chmodだったりするので、さらにややこしいです)。
# udevadm trigger # cd /sys/bus/pci/devices/0000\:08\:00.0/ # ls -l resource* -r--rw-rw- 1 root root 4096 8月 2 23:10 resource -rw-rw-rw- 1 root root 16777216 8月 2 23:10 resource0 -rw-rw-rw- 1 root root 268435456 8月 2 23:10 resource1 -rw-rw-rw- 1 root root 268435456 8月 2 23:10 resource1_wc -rw-rw-rw- 1 root root 33554432 8月 2 23:10 resource3 -rw-rw-rw- 1 root root 33554432 8月 2 23:10 resource3_wc -rw-rw-rw- 1 root root 128 8月 2 23:10 resource5
ルールファイルを書いたら設定を反映させると、resourceNファイルのパーミッションが変わっているはずです。変わっていない場合はおそらくルールの書き方が間違っているので、修正が必要です。
むしろこちらが本題かもしれません。udevのRUNルールは「シェルじゃない」ので、普段シェル上で使っているコマンドをコピペしてもまず動かないです。間違っている場合は主に2パターンで、
前者に関してはRUNでログを出すとわかります。例えばRUN+="/usr/bin/logger AAAAAAAA" のようにすると、udevadm triggerを実行した後に /var/log/syslogにメッセージが出ます。
ログが出なければフィルタリングの条件を全部削って試してください。それでもログが出なければおそらくloggerのパスが間違っています。which loggerで出てくるパスとRUNに書いたパスが合っているか確認してください。特に /binと /usr/binは間違えやすいです。
後者に関してはRUNに書いたコマンドが失敗するとsyslogにエラーが記録されるはずですので、間違ってワイルドカードやパイプなどを書いていないかチェックしてみると良いです。
Aug 1 22:56:51 blackbird systemd-udevd[2775846]: 0000:08:00.0: Process '/bin/chmod -R go+rw /sys/devices/pci0000:00/0000:00:03.1/0000:08:00.0*' failed with exit code 1.
ログを見てもすぐにわからないような場合は、地道に「コマンド名だけにして実行されるか?」「引数を1つずつ足していって動くか?」を確認するのが一番良いです。
コードのキーワードハイライトに使っているhighlight.js(公式サイトへのリンク)を11.2.0にしました。導入したのは7年も前(2014年9月11日の日記参照)ですが、バージョンアップはスムーズにできました。
APIに変更があってinitHighlightingOnLoad() がdeprecatedになりました。代わりにhighlightAll() を呼べばOKです。ファイル名も微妙に変わっていました。スクリプトはhighlight.pack.jsからhighlight.min.jsに、CSSはvs.cssからvs.min.cssになっていました(Visual StudioスタイルのCSSを使う場合の設定、この他にもスタイルはたくさんあります)。
目次: OpenOCD
一覧が欲しくなったので作りました。
目次: OpenOCD
これまでに書き込み、消去の仕組みを調べました。その過程でどうしてHiFive Unleashedで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バイトアドレスを渡しています。これでは動作しません。最初に動作確認したログを覚えているでしょうか?改めて見直すと、
$ 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の消去コマンドが動作しない理由はわかりましたが、まだいくつか不思議な点が残っています。
比較しやすいようにSPI FlashのパラメータリストのうちHiFive1とHiFive Unleashedに関わるところだけ抜粋しておきます。
// 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の書き込み、消去は正常に行えないことがわかりました。手順では回避不可能です。
目次: OpenOCD
いよいよ、正常に動かないSPI Flashの消去を見ます。消去は書き込みと違い大量にデータを書く必要がなく、ヘルパープログラムを使った実装はありません。前回紹介したJTAGでSPIを直接制御する方法(簡易版)と似た処理です。
SPI Flashの消去には3種類あります。ブロック消去に対応していないデバイスもあるようです。ややこしいことに統一された名前がないらしくて、メーカーによっては大きな消去単位をセクタと呼んでいます(Micronがそうだった)。なんじゃそりゃ、訳がわからんから統一してくれ〜。
細かい名前は覚えても意味がない(メーカーによって違うのでどうでも良い)ので、全部消す、大サイズで消す、小サイズで消す、くらいの理解で十分だと思います。
前置きはこれくらいにして、消去処理のコードを見ましょう。
// 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のパラメータリストに載っています。もう一度掲載します。
// 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つあるようです。
OpenOCDは3番目を期待しているようです。なぜかというと4BER64Kコマンドは4バイトアドレスを期待するコマンドだからです。
データシートから抜粋したコマンドの仕様は上記の通りです。アドレスを4バイト送ってくることを期待しています。しかし実装は3バイトしかアドレスを送っていません。うーん、何かおかしいですね?
目次: OpenOCD
フラッシュの書き込みは、ヘルパープログラムを使うタイプ(上記で追いかけた方)と、SPIを直接制御する方があります。高速に書き込めるのはヘルパープログラムを使うタイプですが、わかりやすいのは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() の方を見ます。
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の制御だけ見ると非常にシンプルです。
< | 2021 | > | ||||
<< | < | 08 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
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 | - | - | - | - |
合計:
本日: