以前「ESP32でMicro SDカードへのSPIでの書き込み速度の調査」をした。
M5Stack Core の Grey で同様の調査をしようとしたが、Visual Studio Code (VSC) の ESP-IDF 環境でビルドしようとして、プログラムの修正に手間取ったので、備忘録として残しておく。
また、SDカードのマウントで失敗することがあり、原因が不明確なので、記録を残しておく。
ハードウェア
内蔵SDカードスロットへのアクセス方法
M5Stackでは、内蔵SDカードスロットのハードウェアは 制御できるのは SPI mode のみになっている。
SD bus modeは1-bit 、4-bit ともに、内蔵SDカードスロットのD1、D2 が ESP32 と結線されていないために使えない。
M5Stack ESP32 pin | SD Bus mode | SPI mode pin |
---|---|---|
GPIO18 | CLK | SCK |
GPIO23 | CMD | MOSI |
GPIO19 | D0 | MISO |
GPIO4 | D3 | CS |
また、ESP32のSPIは、HSPIと VSPI の2組あるが、上記の結線の場合には VSPI で制御することになる。ちなみに、M5StackではHSPIは LCDディスプレイ制御に使っている。
ESP32でのSDカードの接続については次の記事が参考になった。
開発環境
- Windows10
- Visual Studio Code
- September 2020 (Version 1.50)
- ESP-IDF Visual Studio Code Extension
- Espressif IDF 0.5.1
- ESP-IDF
- v4.1
プログラム
以前と同様に、次にある、ESP-IDFのSDカード制御のサンプルプログラムを修正した。
https://github.com/espressif/esp-idf/tree/master/examples/storage/sd_card
次の GitHub に ESP-IDF Visual Studio Code Extension で開発した際のファイルを置いてある。
https://github.com/kunsen-an/m5stack_espidf_sd_card_write_test
修正点
以前「ESP32でMicro SDカードへのSPIでの書き込み速度の調査」からの主な修正点は、SPIの初期化の部分である。
ESP32に2組あるSPIのうち、SDカードではVSPIを使うことから、sdmmc_host_t host に対して host.slot = VSPI_HOST と設定する必要がある。
MOSI、MISO、SCKは、M5Stackに合わせて、それぞれ、GPIO23, GPIO19, GPIO18とする。
また、SDカードスロットの CSにつながっている GPIO4 でデバイスの選択を制御する。ちなみに、 GPIO14 でLCDディスプレイの選択を制御するが、SDカードスロットでは使わない。
sdspi_slot_config_t slot_config にこれらのPIN番号を設定する。
また、LCDディスプレイが DMAチャネルの1を使っているので、SDカードスロットは DMAチャネルの2を使う必要がある。このため、slot_config.dma_channel = 2 としてDMAチャネルを2に指定する必要がある。
コード
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
/* This file is based on main/sd_card_example_main.c in the following directory: https://github.com/espressif/esp-idf/tree/master/examples/storage/sd_card The file 'sd_card_example_main.c' is modified to adapt to m5Stack. */ /* SD card and FAT filesystem example. This example code is in the Public Domain (or CC0 licensed, at your option.) Unless required by applicable law or agreed to in writing, this software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ #include <stdio.h> #include <string.h> #include <sys/unistd.h> #include <sys/stat.h> #include "esp_err.h" #include "esp_log.h" #include "esp_vfs_fat.h" #include "driver/sdmmc_host.h" #include "driver/sdspi_host.h" #include "sdmmc_cmd.h" static const char *TAG = "example"; #include "esp_timer.h" // This example can use SDMMC and SPI peripherals to communicate with SD card. // By default, SDMMC peripheral is used. // To enable SPI mode, uncomment the following line: #define USE_SPI_MODE // When testing SD and SPI modes, keep in mind that once the card has been // initialized in SPI mode, it can not be reinitialized in SD mode without // toggling power to the card. #ifdef USE_SPI_MODE // Pin mapping when using SPI mode. // With this mapping, SD card can be used both in SPI and 1-line SD mode. // Note that a pull-up on CS line is required in SD mode. // M5Stack #define PIN_NUM_MISO 19 #define PIN_NUM_MOSI 23 #define PIN_NUM_CLK 18 #define SD_PIN_NUM_CS 4 // M5Stack #define LCD_PIN_NUM_CS 14 // M5Stack #endif //USE_SPI_MODE #define TIME_ARRAY_SIZE 10000 #define WRITE_BUFFER_SIZE (16*1024) #define PRINT_DIFF 0 uint64_t time_array[TIME_ARRAY_SIZE]; char write_buffer[WRITE_BUFFER_SIZE]; void app_main(void) { ESP_LOGI(TAG, "Initializing SD card"); #ifndef USE_SPI_MODE ESP_LOGI(TAG, "Using SDMMC peripheral"); sdmmc_host_t host = SDMMC_HOST_DEFAULT(); // This initializes the slot without card detect (CD) and write protect (WP) signals. // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); // To use 1-line SD mode, uncomment the following line: // slot_config.width = 1; // GPIOs 15, 2, 4, 12, 13 should have external 10k pull-ups. // Internal pull-ups are not sufficient. However, enabling internal pull-ups // does make a difference some boards, so we do that here. gpio_set_pull_mode(15, GPIO_PULLUP_ONLY); // CMD, needed in 4- and 1- line modes gpio_set_pull_mode(2, GPIO_PULLUP_ONLY); // D0, needed in 4- and 1-line modes gpio_set_pull_mode(4, GPIO_PULLUP_ONLY); // D1, needed in 4-line mode only gpio_set_pull_mode(12, GPIO_PULLUP_ONLY); // D2, needed in 4-line mode only gpio_set_pull_mode(13, GPIO_PULLUP_ONLY); // D3, needed in 4- and 1-line modes #else ESP_LOGI(TAG, "Using SPI peripheral"); esp_err_t ret; sdmmc_host_t host = SDSPI_HOST_DEFAULT(); host.slot = VSPI_HOST; // M5Stack sdspi_slot_config_t slot_config = SDSPI_SLOT_CONFIG_DEFAULT(); slot_config.gpio_miso = PIN_NUM_MISO; slot_config.gpio_mosi = PIN_NUM_MOSI; slot_config.gpio_sck = PIN_NUM_CLK; slot_config.gpio_cs = SD_PIN_NUM_CS; slot_config.dma_channel = 2; // M5Stack // This initializes the slot without card detect (CD) and write protect (WP) signals. // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. #endif //USE_SPI_MODE // Options for mounting the filesystem. // If format_if_mount_failed is set to true, SD card will be partitioned and // formatted in case when mounting fails. esp_vfs_fat_sdmmc_mount_config_t mount_config = { .format_if_mount_failed = false, .max_files = 5, .allocation_unit_size = 16 * 1024 }; // Use settings defined above to initialize SD card and mount FAT filesystem. // Note: esp_vfs_fat_sdmmc_mount is an all-in-one convenience function. // Please check its source code and implement error recovery when developing // production applications. sdmmc_card_t* card; ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card); if (ret != ESP_OK) { if (ret == ESP_FAIL) { ESP_LOGE(TAG, "Failed to mount filesystem. " "If you want the card to be formatted, set format_if_mount_failed = true."); } else { ESP_LOGE(TAG, "Failed to initialize the card (%s). " "Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret)); } return; } // Card has been initialized, print its properties sdmmc_card_print_info(stdout, card); // Use POSIX and C standard library functions to work with files. // First create a file. ESP_LOGI(TAG, "Opening file"); FILE* f = fopen("/sdcard/hello.txt", "w"); if (f == NULL) { ESP_LOGE(TAG, "Failed to open file for writing"); return; } // initialize write buffer for ( int i = 0 ; i < WRITE_BUFFER_SIZE ; i++ ) { write_buffer[i] = ' ' + (i % 64); } uint64_t start = esp_timer_get_time(); for ( int counter = 0 ; counter < TIME_ARRAY_SIZE ; counter++) { fwrite(write_buffer, 1, WRITE_BUFFER_SIZE, f); time_array[counter] = esp_timer_get_time(); } fclose(f); ESP_LOGI(TAG, "File written"); uint64_t sum = 0; uint64_t maximum = 0; uint64_t minimum = UINT64_MAX; for ( int i=0 ; i < TIME_ARRAY_SIZE; i++ ) { uint64_t end = time_array[i]; uint64_t diff = end - start; #if PRINT_DIFF printf("%d: start=%llu, end=%llu, diff=%llu\n", i, start, end, diff); #endif maximum = (diff > maximum) ? diff : maximum; minimum = (diff < minimum) ? diff : minimum; sum += diff; start = end; } uint64_t average = sum / TIME_ARRAY_SIZE; printf("write buffer size = %d\n", WRITE_BUFFER_SIZE); printf("sum=%llu microseconds, average=%llu microseconds\n", sum, average); printf("maximum=%llu microseconds, minimum=%llu microseconds\n", maximum, minimum); printf("highest write speed = %llu byte/s\n", ((uint64_t)WRITE_BUFFER_SIZE)*1000*1000/minimum); printf("average write speed = %llu byte/s\n", ((uint64_t)WRITE_BUFFER_SIZE)*1000*1000/average); printf("lowest write speed = %llu byte/s\n", ((uint64_t)WRITE_BUFFER_SIZE)*1000*1000/maximum); // Check if destination file exists before renaming struct stat st; if (stat("/sdcard/foo.txt", &st) == 0) { // Delete it if it exists unlink("/sdcard/foo.txt"); } // Rename original file ESP_LOGI(TAG, "Renaming file"); if (rename("/sdcard/hello.txt", "/sdcard/foo.txt") != 0) { ESP_LOGE(TAG, "Rename failed"); return; } // Open renamed file for reading ESP_LOGI(TAG, "Reading file"); f = fopen("/sdcard/foo.txt", "r"); if (f == NULL) { ESP_LOGE(TAG, "Failed to open file for reading"); return; } char line[64]; fgets(line, sizeof(line), f); fclose(f); // strip newline char* pos = strchr(line, '\n'); if (pos) { *pos = '\0'; } ESP_LOGI(TAG, "Read from file: '%s'", line); // All done, unmount partition and disable SDMMC or SPI peripheral esp_vfs_fat_sdmmc_unmount(); ESP_LOGI(TAG, "Card unmounted"); |
実行結果例
VSC から フラッシュに書き込んで、自動でリセットがかかった場合には、SDカードのマウントに失敗した。リセットボタンを押して再度実行すれば動作することが多いが、SDカードのマウントで失敗することがある。安定していない感じである。タイミングなどの問題があるのかもしれない。
成功時のログ
GitHub にも example.log.txt として置いてあるが、以下にうまくマウントができて、その後の処理もできたログ出力例を示す。
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
Microsoft Windows [Version 10.0.18363.1139] (c) 2019 Microsoft Corporation. All rights reserved. D:\esp\projects\m5stack_espidf_sd_card_write_test>set IDF_PATH=d:\esp\esp-idf D:\esp\projects\m5stack_espidf_sd_card_write_test>d:\.espressif\python_env\idf4.1_py3.8_env\Scripts\python.exe d:\esp\esp-idf\tools\idf.py -p COM7 monitor Executing action: monitor Running idf_monitor in directory d:\esp\projects\m5stack_espidf_sd_card_write_test Executing "d:\.espressif\python_env\idf4.1_py3.8_env\Scripts\python.exe d:\esp\esp-idf\tools/idf_monitor.py -p COM7 -b 115200 --toolchain-prefix xtensa-esp32-elf- d:\esp\projects\m5stack_espidf_sd_card_write_test\build\m5stack_espidf_sd_card_write_test.elf -m 'd:\.espressif\python_env\idf4.1_py3.8_env\Scripts\python.exe' 'd:\esp\esp-idf\tools\idf.py' '-p' 'COM7'"... --- idf_monitor on COM7 115200 --- --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H --- ets Jun 8 2016 00:22:57 rst:0x1 (POWERON_RESET),boot:0x17 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:2 load:0x3fff0030,len:4 load:0x3fff0034,len:6872 load:0x40078000,len:13072 ho 0 tail 12 room 4 load:0x40080400,len:3896 0x40080400: _init at ??:? entry 0x40080688 I (30) boot: ESP-IDF v4.1-dirty 2nd stage bootloader I (30) boot: compile time 16:59:38 I (30) boot: chip revision: 1 I (33) boot_comm: chip revision: 1, min. bootloader chip revision: 0 I (41) boot.esp32: SPI Speed : 40MHz I (45) boot.esp32: SPI Mode : DIO I (50) boot.esp32: SPI Flash Size : 16MB I (54) boot: Enabling RNG early entropy source... I (60) boot: Partition Table: I (63) boot: ## Label Usage Type ST Offset Length I (71) boot: 0 nvs WiFi data 01 02 00009000 00006000 I (78) boot: 1 phy_init RF data 01 01 0000f000 00001000 I (86) boot: 2 factory factory app 00 00 00010000 00100000 I (93) boot: End of partition table I (97) boot_comm: chip revision: 1, min. application chip revision: 0 I (104) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x09790 ( 38800) map I (128) esp_image: segment 1: paddr=0x000197b8 vaddr=0x3ffb0000 size=0x0217c ( 8572) load I (132) esp_image: segment 2: paddr=0x0001b93c vaddr=0x40080000 size=0x00404 ( 1028) load 0x40080000: _WindowOverflow4 at D:/esp/esp-idf/components/freertos/xtensa_vectors.S:1778 I (135) esp_image: segment 3: paddr=0x0001bd48 vaddr=0x40080404 size=0x042d0 ( 17104) load I (151) esp_image: segment 4: paddr=0x00020020 vaddr=0x400d0020 size=0x1d408 (119816) map 0x400d0020: _stext at ??:? I (198) esp_image: segment 5: paddr=0x0003d430 vaddr=0x400846d4 size=0x064ac ( 25772) load 0x400846d4: esp_panic_dig_reset at D:/esp/esp-idf/components/esp32/panic.c:441 I (216) boot: Loaded app from partition at offset 0x10000 I (216) boot: Disabling RNG early entropy source... I (217) cpu_start: Pro cpu up. I (220) cpu_start: Application information: I (225) cpu_start: Project name: m5stack_espidf_sd_card_write_te I (232) cpu_start: App version: d97e66b-dirty I (237) cpu_start: Compile time: Oct 25 2020 17:02:48 I (243) cpu_start: ELF file SHA256: 5c1589d412b43499... I (249) cpu_start: ESP-IDF: v4.1-dirty I (254) cpu_start: Starting app cpu, entry point is 0x400810c4 0x400810c4: call_start_cpu1 at D:/esp/esp-idf/components/esp32/cpu_start.c:271 I (0) cpu_start: App cpu up. I (265) heap_init: Initializing. RAM available for dynamic allocation: I (272) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM I (278) heap_init: At 3FFCA2B0 len 00015D50 (87 KiB): DRAM I (284) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM I (290) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM I (297) heap_init: At 4008AB80 len 00015480 (85 KiB): IRAM I (303) cpu_start: Pro cpu start user code I (321) spi_flash: detected chip: gd I (322) spi_flash: flash io: dio I (322) cpu_start: Starting scheduler on PRO CPU. I (0) cpu_start: Starting scheduler on APP CPU. I (329) example: Initializing SD card I (329) example: Using SPI peripheral I (339) gpio: GPIO[4]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 I (389) sdspi_transaction: cmd=5, R1 response: command not supported Name: USD Type: SDHC/SDXC Speed: 20 MHz Size: 7695MB I (409) example: Opening file I (275009) example: File written write buffer size = 16384 sum=274589916 microseconds, average=27458 microseconds maximum=173854 microseconds, minimum=22767 microseconds highest write speed = 719638 byte/s average write speed = 596693 byte/s lowest write speed = 94239 byte/s I (277349) example: Renaming file I (277359) example: Reading file I (277359) example: Read from file: ' !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^' I (277359) gpio: GPIO[23]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 I (277369) gpio: GPIO[19]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 I (277379) gpio: GPIO[18]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 I (277389) gpio: GPIO[4]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 I (277399) example: Card unmounted |
失敗時のログ
以下にマウントに失敗した場合のログ出力例を示す。GitHub にも error.log.txt として置いてある。うまくいかない場合の原因は、はっきりしていない。
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
ets Jun 8 2016 00:22:57 rst:0x1 (POWERON_RESET),boot:0x17 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:2 load:0x3fff0030,len:4 load:0x3fff0034,len:6872 load:0x40078000,len:13072 ho 0 tail 12 room 4 load:0x40080400,len:3896 0x40080400: _init at ??:? entry 0x40080688 I (30) boot: ESP-IDF v4.1-dirty 2nd stage bootloader I (30) boot: compile time 17:45:48 I (30) boot: chip revision: 1 I (33) boot_comm: chip revision: 1, min. bootloader chip revision: 0 I (40) boot.esp32: SPI Speed : 40MHz I (45) boot.esp32: SPI Mode : DIO I (50) boot.esp32: SPI Flash Size : 16MB I (54) boot: Enabling RNG early entropy source... I (60) boot: Partition Table: I (63) boot: ## Label Usage Type ST Offset Length I (71) boot: 0 nvs WiFi data 01 02 00009000 00006000 I (78) boot: 1 phy_init RF data 01 01 0000f000 00001000 I (86) boot: 2 factory factory app 00 00 00010000 00100000 I (93) boot: End of partition table I (97) boot_comm: chip revision: 1, min. application chip revision: 0 I (104) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x09790 ( 38800) map I (128) esp_image: segment 1: paddr=0x000197b8 vaddr=0x3ffb0000 size=0x0217c ( 8572) load I (132) esp_image: segment 2: paddr=0x0001b93c vaddr=0x40080000 size=0x00404 ( 1028) load 0x40080000: _WindowOverflow4 at D:/esp/esp-idf/components/freertos/xtensa_vectors.S:1778 I (135) esp_image: segment 3: paddr=0x0001bd48 vaddr=0x40080404 size=0x042d0 ( 17104) load I (151) esp_image: segment 4: paddr=0x00020020 vaddr=0x400d0020 size=0x1d408 (119816) map 0x400d0020: _stext at ??:? I (198) esp_image: segment 5: paddr=0x0003d430 vaddr=0x400846d4 size=0x064ac ( 25772) load 0x400846d4: esp_panic_dig_reset at D:/esp/esp-idf/components/esp32/panic.c:441 I (216) boot: Loaded app from partition at offset 0x10000 I (216) boot: Disabling RNG early entropy source... I (217) cpu_start: Pro cpu up. I (220) cpu_start: Application information: I (225) cpu_start: Project name: m5stack_espidf_sd_card_write_te I (232) cpu_start: App version: c8ce568 I (237) cpu_start: Compile time: Oct 25 2020 20:10:14 I (243) cpu_start: ELF file SHA256: 80439fa4be578f77... I (249) cpu_start: ESP-IDF: v4.1-dirty I (254) cpu_start: Starting app cpu, entry point is 0x400810c4 0x400810c4: call_start_cpu1 at D:/esp/esp-idf/components/esp32/cpu_start.c:271 I (0) cpu_start: App cpu up. I (264) heap_init: Initializing. RAM available for dynamic allocation: I (271) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM I (277) heap_init: At 3FFCA2B0 len 00015D50 (87 KiB): DRAM I (283) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM I (290) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM I (296) heap_init: At 4008AB80 len 00015480 (85 KiB): IRAM I (302) cpu_start: Pro cpu start user code I (321) spi_flash: detected chip: gd I (321) spi_flash: flash io: dio I (321) cpu_start: Starting scheduler on PRO CPU. I (0) cpu_start: Starting scheduler on APP CPU. I (329) example: Initializing SD card I (329) example: Using SPI peripheral I (339) gpio: GPIO[4]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 I (369) gpio: GPIO[23]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 I (369) gpio: GPIO[19]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 I (369) gpio: GPIO[18]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 I (379) gpio: GPIO[4]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 E (389) example: Failed to initialize the card (ESP_ERR_TIMEOUT). Make sure SD card lines have pull-up resistors in place. |