├── .gitignore ├── BASE_CLASSES.md ├── LICENSE ├── README.md ├── bdevice.py ├── eeprom ├── i2c │ ├── I2C.md │ ├── eep_i2c.py │ ├── eeprom_i2c.py │ └── package.json └── spi │ ├── SPI.md │ ├── eep_spi.py │ ├── eeprom_spi.py │ └── package.json ├── flash ├── FLASH.md ├── flash_spi.py ├── flash_test.py ├── littlefs_test.py └── wemos_flash.py ├── fram ├── FRAM.md ├── FRAM_SPI.md ├── fram_fs_test.py ├── fram_i2c.py ├── fram_spi.py ├── fram_spi_test.py └── fram_test.py └── spiram ├── SPIRAM.md ├── fs_test.py ├── spiram.py └── spiram_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /BASE_CLASSES.md: -------------------------------------------------------------------------------- 1 | # 1. Base classes for memory device drivers 2 | 3 | This doc is primarily to aid those wishing to use these base classes to write 4 | drivers for additional memory devices. It describes the two classes in 5 | `bdevice.py` namely `BlockDevice` and the subclass `FlashDevice`. Both provide 6 | hardware-independent abstractions of memory devices. The base class provides 7 | the API. This has the following characteristics: 8 | 1. Support for single or multiple chips on the same bus. Multiple chips are 9 | automatically configured as a single byte array. 10 | 2. The byte array can be accessed using Python slice syntax. 11 | 3. Alternatively the array can be formatted and mounted as a filesystem using 12 | methods in the `uos` module. Any filesystem supported by the MicroPython build 13 | may be employed: FAT and littlefs have been tested. The latter is recommended. 14 | 15 | The `BlockDevice` class supports byte-addressable technologies such as EEPROM 16 | and FRAM. Such devices can be written on a single byte basis. Where a chip also 17 | offers multi-byte writes this optimisation can be handled in the user driver: 18 | see the EEPROM drivers for examples of this. 19 | 20 | `FlashDevice` subclasses `BlockDevice` to support devices which must buffer a 21 | sector of data for writing. The API continues to support byte addressing: this 22 | is achieved by modifying the buffer contents and writing it out when necessary. 23 | 24 | # 2. The BlockDevice class 25 | 26 | The class provides these characteristics: 27 | 1. An API which represents multiple physical devices as a single byte array. 28 | The physical means of achieving this is provided in the hardware subclass. 29 | 2. An implementation of the `AbstractBlockDev` protocol with extended 30 | interface as required by littlefs as documented 31 | [here](http://docs.micropython.org/en/latest/library/uos.html). 32 | 3. An API based on Python slice notation for byte level access to the array. 33 | 4. Support for the `len` operator. 34 | 35 | ## 2.1 Constructor 36 | 37 | Constructor args - mandatory, positional, integer 38 | 1. `nbits` Block size reported to the filesystem expressed as a number of 39 | bits: the block size is `2^nbits`. The usual value is 9 (512 bit block). 40 | 2. `nchips` Number of chips in the array. 41 | 3. `chip_size` Size of each chip in bytes. 42 | 43 | ## 2.2 Necessary subclass support 44 | 45 | The subclass must provide a method `readwrite` taking the following args: 46 | 1. `addr` Address relative to the start of the array. 47 | 2. `buf` A buffer holding data to write or to contain data to be read. 48 | 3. `read` Boolean: `True` to read, `False` to write. 49 | 50 | The amount of data read or written is defined by the length of the buffer. 51 | 52 | Return value: the buffer. 53 | 54 | The method must handle the case where a buffer crosses chip boundaries. This 55 | involves physical accesses to each chip and reading or writing partial buffer 56 | contents. Addresses are converted by the method to chip-relative addresses. 57 | 58 | ## 2.3 The `AbstractBlockDev` protocol 59 | 60 | This is provided by the following methods: 61 | 1. `sync()` In the `BlockDevice` class this does nothing. It is defined in the 62 | `FlashDevice` class [section 3.3](./BASE_CLASSES.md#33-methods). 63 | 2. `readblocks(blocknum, buf, offset=0)` Converts the block address and offset 64 | to an absolute address into the array and calls `readwrite`. 65 | 3. `writeblocks(blocknum, buf, offset=0` Works as above. 66 | 4. `ioctl` This supports the following operands: 67 | 68 | 3. `sync` Calls the `.sync()` method. 69 | 4. `sector count` Returns `chip_size` * `nchips` // `block_size` 70 | 5. `block size` Returns block size calculated as in section 2.1. 71 | 6. `erase` Necessary for correct filesystem operation: returns 0. 72 | 73 | The drivers make no use of the block size: it exists only for filesystems. The 74 | `readwrite` method hides any physical device structure presenting an array of 75 | bytes. The specified block size must match the intended filesystem. Littlefs 76 | requires >=128 bytes, FATFS requires >=512 bytes. All testing was done with 512 77 | byte blocks. 78 | 79 | ## 2.4 Byte level access 80 | 81 | This is provided by `__getitem__` and `__setitem__`. The `addr` arg can be an 82 | integer or a slice, enabling the following syntax examples: 83 | ```python 84 | a = eep[1000] # Read a single byte 85 | eep[1000] = 42 # write a byte 86 | eep[1000:1004] = b'\x11\x22\x33\x44' # Write 4 consecutive bytes 87 | b = eep[1000:1004] # Read 4 consecutive bytes 88 | ``` 89 | The last example necessarily performs allocation in the form of a buffer for 90 | the resultant data. Applications can perform allocation-free reading by calling 91 | the `readwrite` method directly. 92 | 93 | ## 2.5 The len operator 94 | 95 | This returns the array size in bytes. 96 | 97 | # 3. The FlashDevice class 98 | 99 | By subclassing `BlockDevice`, `FlashDevice` provides the same API for flash 100 | devices. At a hardware level reading is byte addressable in a similar way to 101 | EEPROM and FRAM devices. These chips do not support writing arbitrary data to 102 | individual byte addresses. Writing is done by erasing a block, then rewriting 103 | it with new contents. To provide logical byte level writing it is necessary to 104 | read and buffer the block containing the byte, update the byte, erase the block 105 | and write out the buffer. 106 | 107 | In practice this would be slow and inefficient - erasure is a slow process and 108 | results in wear. The `FlashDevice` class defers writing the buffer until it is 109 | necessary to buffer a different block. 110 | 111 | The class caches a single sector. In currently supported devices this is 4KiB 112 | of RAM. This is adequate for littlefs, however under FATFS wear can be reduced 113 | by cacheing more than one sector. These drivers are primarily intended for 114 | littlefs with its wear levelling design. 115 | 116 | ## 3.1 Constructor 117 | 118 | Constructor args - mandatory, positional, integer 119 | 1. `nbits` Block size reported to the filesystem expressed as a number of 120 | bits: the block size is `2^nbits`. The usual value is 9 (512 bit block). 121 | 2. `nchips` Number of chips in the array. 122 | 3. `chip_size` Size of each chip in bytes. 123 | 4. `sec_size` Physical sector size of the device in bytes. 124 | 125 | ## 3.2 Necessary subclass support 126 | 127 | A subclass supporting a flash device must provide the following methods: 128 | 1. `readwrite(addr, buf, read)` Args as defined in section 2.2. This calls the 129 | `.read` or `.write` methods of `FlashDevice` as required. 130 | 2. `rdchip(addr, mvb)` Args `addr`: address into the array, `mvb` a 131 | `memoryview` into a buffer for read data. This reads from the chip into the 132 | `memoryview`. 133 | 3. `flush(cache, addr)` Args `cache` a buffer holding one sector of data, 134 | `addr` address into the array of the start of a physical sector. Erase the 135 | sector and write out the data in `cache`. 136 | 137 | The constructor must call `initialise()` after the hardware has been 138 | initialised to ensure valid cache contents. 139 | 140 | ## 3.3 Methods 141 | 142 | 1. `read(addr, mvb`) Args `addr` address into array, `mvb` a `memoryview` into 143 | a buffer. Fills the `memoryview` with data read. If some or all of the data is 144 | cached, the cached data is provided. 145 | 2. `write(addr, mvb`) Args `addr` address into array, `mvb` a `memoryview` 146 | into a buffer. If the address range is cached, the cache contents are updated. 147 | More generally the currently cached data is written out using `flush`, a new 148 | sector is cached, and the contents updated. Depending on the size of the data 149 | buffer this may occur multiple times. 150 | 3. `sync()` This flushes the current cache. An optimisation is provided by the 151 | `._dirty` flag. This ensures that the cache is only flushed if its contents 152 | have been modified since it was last written out. 153 | 4. `is_empty(addr, ev=0xff)` Arg: `addr` start address of a sector. Reads the 154 | sector returning `True` if all bytes match `ev`. Enables a subclass to avoid 155 | erasing a sector which is already empty. 156 | 5. `initialise()` Called by the subclass constructor to populate the cache 157 | with the contents of sector 0. 158 | 159 | # 4. References 160 | 161 | [uos docs](http://docs.micropython.org/en/latest/library/uos.html) 162 | [Custom block devices](http://docs.micropython.org/en/latest/reference/filesystem.html#custom-block-devices) 163 | [Littlefs](https://github.com/ARMmbed/littlefs/blob/master/DESIGN.md) 164 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Peter Hinch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1. MicroPython drivers for memory chips 2 | 3 | These drivers support either byte level access or the littlefs filesystem. 4 | Supported technologies are Flash, EEPROM, FRAM and SPIRAM. 5 | 6 | Currently supported devices include technologies having superior performance 7 | compared to flash. Resultant storage has much higher write endurance. In some 8 | cases read and write access times may be shorter. EEPROM and FRAM chips have 9 | much lower standby current than SD cards, benefiting micropower applications. 10 | 11 | The drivers present a common API having the features listed below. 12 | 13 | ## 1.1 Features common to all drivers 14 | 15 | The drivers have the following common features: 16 | 1. Support for single or multiple chips on the same bus. Multiple chips are 17 | automatically configured as a single array. 18 | 2. This can be accessed as an array of bytes, using Python slice syntax or via 19 | a `readwrite` method. 20 | 3. Alternatively the array can be formatted and mounted as a filesystem using 21 | methods in the `os` module. Any filesystem supported by the MicroPython build 22 | may be employed: FAT and littlefs have been tested. The latter is recommended. 23 | 4. Drivers are portable: buses and pins should be instantiated using the 24 | `machine` module. 25 | 5. Buses may be shared with other hardware. This assumes that the application 26 | pays due accord to differing electrical constraints such as baudrate. 27 | 28 | ## 1.2 Technologies 29 | 30 | Currently supported technologies are SPIRAM (PSRAM), Flash, EEPROM, and FRAM 31 | (ferroelectric RAM). The latter two are nonvolatile random access storage 32 | devices with much higher endurance than flash memory. Flash has a typical 33 | endurance of 10-100K writes per page. The figures for EEPROM and FRAM are 1-4M 34 | and 10^12 writes respectively. In the case of the FAT filing system 1M page 35 | writes probably corresponds to 1M filesystem writes because FAT repeatedly 36 | updates the allocation tables in the low numbered sectors. Under `littlefs` I 37 | would expect the endurance to be substantially better owing to its wear 38 | levelling architecture; over-provisioning should enhance this. 39 | 40 | SPIRAM has huge capacity and effectively infinite endurance. Unlike the other 41 | technologies it is volatile: contents are lost after a power cycle. 42 | 43 | ## 1.3 Organisation of this repo 44 | 45 | The directory structure is `technology/interface` where supported chips for a 46 | given technology offer SPI and I2C interfaces; where only one interface exists 47 | the `interface` subdirectory is omitted. The file `bdevice.py` is common to all 48 | drivers and is in the root directory. 49 | 50 | The link in the table below points to the docs relevant to the specific chip. 51 | In that directory may be found test scripts which may need minor adaptation for 52 | the host and interface in use. It is recommended to run these to verify the 53 | hardware configuration. 54 | 55 | ## 1.4 Supported chips 56 | 57 | These currently include Microchip and STM EEPROM chips and 58 | [this Adafruit FRAM board](http://www.adafruit.com/product/1895). Note that the 59 | largest EEPROM chip uses SPI: see [below](./README.md#2-choice-of-interface) 60 | for a discussion of the merits and drawbacks of each interface. 61 | 62 | The EEPROM drivers have been updated to be generic. Page size can be auto 63 | detected and the drivers have been tested with a wide variety of chips in sizes 64 | from 256 bytes to 256KiB. Thanks are due to Abel Deuring for doing much of this 65 | testing. That said, it is not possible to guarantee that all possible device 66 | types will work. 67 | 68 | Supported devices. Microchip manufacture each chip in different variants with 69 | letters denoted by "xx" below. The variants cover parameters such as minimum 70 | Vcc value and do not affect the API. There are two variants of the STM chip, 71 | M95M02-DRMN6TP and M95M02-DWMN3TP/K. The latter has a wider temperature range. 72 | 73 | The interface column includes page size where relevant. The EEPROM driver can 74 | auto-detect this and report it for a given chip. 75 | 76 | | Manufacturer | Part | Interface | Bytes | Technology | Docs | 77 | |:------------:|:---------:|:---------:|:-------:|:----------:|:-----------------------------:| 78 | | Various | Various | SPI 4096 | <=32MiB | Flash | [FLASH.md](./flash/FLASH.md) | 79 | | STM | M95M02-DR | SPI | 256KiB | EEPROM | [SPI.md](./eeprom/spi/SPI.md) | 80 | | Microchip | 25xx1024 | SPI | 128KiB | EEPROM | [SPI.md](./eeprom/spi/SPI.md) | 81 | | Microchip | 25xx512* | SPI | 64KiB | EEPROM | [SPI.md](./eeprom/spi/SPI.md) | 82 | | Microchip | 24xx512 | I2C | 64KiB | EEPROM | [I2C.md](./eeprom/i2c/I2C.md) | 83 | | Microchip | 24xx256 | I2C | 32KiB | EEPROM | [I2C.md](./eeprom/i2c/I2C.md) | 84 | | Microchip | 24xx128 | I2C | 16KiB | EEPROM | [I2C.md](./eeprom/i2c/I2C.md) | 85 | | Microchip | 24xx64 | I2C | 8KiB | EEPROM | [I2C.md](./eeprom/i2c/I2C.md) | 86 | | Microchip | 24xx32 | I2C | 4KiB | EEPROM | [I2C.md](./eeprom/i2c/I2C.md) | 87 | | Adafruit | 4719 | SPI n/a | 512KiB | FRAM | [FRAM_SPI.md](./fram/FRAM_SPI.md) | 88 | | Adafruit | 4718 | SPI n/a | 256KiB | FRAM | [FRAM_SPI.md](./fram/FRAM_SPI.md) | 89 | | Adafruit | 1895 | I2C n/a | 32KiB | FRAM | [FRAM.md](./fram/FRAM.md) | 90 | | Adafruit | 4677 | SPI n/a | 8MiB | SPIRAM | [SPIRAM.md](./spiram/SPIRAM.md) | 91 | 92 | Parts marked * have been tested by users (see below). 93 | The SPIRAM chip is equivalent to Espressif ESP-PSRAM64H. 94 | 95 | The flash driver now has the capability to support a variety of chips. The 96 | following have been tested to date: 97 | 98 | | Chip | Size (MiB) | 99 | |:-----------------:|:----------:| 100 | | Cypress S25FL256L | 32 | 101 | | Cypress S25FL128L | 16 | 102 | | Cypress S25FL064L | 8 | 103 | | Winbond W25Q32JV | 4 | 104 | 105 | 106 | It is likely that other chips with 4096 byte blocks will work but I am unlikely 107 | to be able to support hardware I don't possess. Users should check datasheets 108 | for compatibility. 109 | 110 | ### 1.4.1 Chips tested by users 111 | 112 | If you have success with other chips please raise an issue and I will update 113 | this doc. Please note the `cmd5` arg. It is essential to know whether a chip 114 | uses 4 or 5 byte commands and to set this correctly otherise very confusing 115 | behaviour results. 116 | 117 | CAT24C256LI-G I2C EEPROM 32KiB tested by 118 | [Julien Phalip](https://github.com/peterhinch/micropython_eeprom/issues/6#issuecomment-825801065). 119 | 120 | Winbond W25Q128JV Flash 16MiB tested by 121 | [mweber-bg](https://github.com/peterhinch/micropython_eeprom/issues/8#issuecomment-917603913). 122 | This requires setting `cmd5=False`. 123 | 124 | Winbond W25Q64JV Flash 8MiB tested by 125 | [IlysvlVEizbr](https://github.com/peterhinch/micropython_eeprom/issues/17). 126 | This requires setting `cmd5=False`. 127 | 128 | Microchip 25LC512 SPI EEPROM 64KiB tested by 129 | [ph1lj-6321](https://github.com/peterhinch/micropython_eeprom/issues/10). 130 | 131 | ## 1.5 Performance 132 | 133 | FRAM and SPIRAM are truly byte-addressable: speed is limited only by the speed 134 | of the I2C or SPI interface (SPI being much faster). 135 | 136 | Reading from EEPROM chips is fast. Writing is slower, typically around 5ms. 137 | However where multiple bytes are written, that 5ms applies to a page of data so 138 | the mean time per byte is quicker by a factor of the page size (128 or 256 139 | bytes depending on the device). 140 | 141 | The drivers provide the benefit of page writing in a way which is transparent. 142 | If you write a block of data to an arbitrary address, page writes will be used 143 | to minimise total time. 144 | 145 | In the case of flash, page writing is mandatory: a sector is written by first 146 | erasing it, a process which is slow. This physical limitation means that the 147 | driver must buffer an entire 4096 byte sector. This contrasts with FRAM and 148 | EEPROM drivers where the buffering comprises a few bytes. 149 | 150 | # 2. Choice of interface 151 | 152 | The principal merit of I2C is to minimise pin count. It uses two pins 153 | regardless of the number of chips connected. It requires pullup resistors on 154 | those lines, although these may be provided on the target device. The 155 | supported EEPROM devices limit expansion to a maximum of 8 chips on a bus. 156 | 157 | SPI requires no pullups, but uses three pins plus one for each connected chip. 158 | It is much faster than I2C, but in the case of EEPROMs the benefit is only 159 | apparent on reads: write speed is limited by the EEPROM device. In principle 160 | expansion is limited only by the number of available pins. (In practice 161 | electrical limits may also apply). 162 | 163 | The larger capacity chips generally use SPI. 164 | 165 | # 3. Design details 166 | 167 | A key aim of these drivers is support for littlefs. This requires the extended 168 | block device protocol as described 169 | [here](http://docs.micropython.org/en/latest/reference/filesystem.html) and 170 | [in the uos doc](http://docs.micropython.org/en/latest/library/os.html). 171 | This protocol describes a block structured API capable of handling offsets into 172 | the block. It is therefore necessary for the device driver to deal with any 173 | block structuring inherent in the hardware. The device driver must enable 174 | access to varying amounts of data at arbitrary physical addresses. 175 | 176 | These drivers achieve this by implementing a device-dependent `readwrite` 177 | method which provides read and write access to arbitrary addresses, with data 178 | volumes which can span page and chip boundaries. A benefit of this is that the 179 | array of chips can be presented as a large byte array. This array is accessible 180 | by Python slice notation: behaviour provided by the hardware-independent base 181 | class. 182 | 183 | A consequence of the above is that the page size in the ioctl does not have any 184 | necessary connection with the memory hardware, so the drivers enable the value 185 | to be specified as a constructor argument. Littlefs requires a minimum size of 186 | 128 bytes - 187 | [theoretically 104](https://github.com/ARMmbed/littlefs/blob/master/DESIGN.md). 188 | The drivers only allow powers of 2: in principle 128 bytes could be used. The 189 | default in MicroPython's littlefs implementation is 512 bytes and all testing 190 | was done with this value. FAT requires 512 bytes minimum: FAT testing was done 191 | with the same block size. 192 | 193 | ## 3.1 Developer Documentation 194 | 195 | This [doc](./BASE_CLASSES.md) has information on the base classes for those 196 | wishing to write drivers for other memory devices. 197 | 198 | # 4. Filesystem support 199 | 200 | The test programs use littlefs and therefore require MicroPython V1.12 or 201 | later. On platforms that don't support littlefs the options are either to adapt 202 | the test programs for FAT (code is commented out) or to build firmware with 203 | littlefs support. This can be done by passing `MICROPY_VFS_LFS2=1` to the 204 | `make` command. 205 | 206 | A filesystem can be mounted in `boot.py`: this enables it to be managed on the 207 | PC using `rshell` or `mpremote`. Exact details are hardware dependent (see the 208 | relevant docs) but a typical `mount.py` is as below, called by the last line in 209 | `boot.py`: 210 | ```py 211 | import os 212 | from machine import SPI, Pin, SoftSPI 213 | from eeprom_spi import EEPROM 214 | spi = SoftSPI(baudrate=5_000_000, sck=Pin("Y6"), miso=Pin("Y7"), mosi=Pin("Y8")) 215 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 216 | eep = EEPROM(spi, cspins, 128) 217 | os.mount(eep, "/eeprom") 218 | ``` 219 | The filesystem may then be accessed as follows: 220 | ```bash 221 | $ mpremote cp foo.py :/eeprom/ 222 | ``` 223 | -------------------------------------------------------------------------------- /bdevice.py: -------------------------------------------------------------------------------- 1 | # bdevice.py Hardware-agnostic base classes. 2 | # BlockDevice Base class for general block devices e.g. EEPROM, FRAM. 3 | # FlashDevice Base class for generic Flash memory (subclass of BlockDevice). 4 | # Documentation in BASE_CLASSES.md 5 | 6 | # Released under the MIT License (MIT). See LICENSE. 7 | # Copyright (c) 2019-2024 Peter Hinch 8 | 9 | from micropython import const 10 | 11 | 12 | class BlockDevice: 13 | def __init__(self, nbits, nchips, chip_size): 14 | self._c_bytes = chip_size # Size of chip in bytes 15 | self._a_bytes = chip_size * nchips # Size of array 16 | self._nbits = nbits # Block size in bits 17 | self._block_size = 2 ** nbits 18 | self._rwbuf = bytearray(1) 19 | 20 | def __len__(self): 21 | return self._a_bytes 22 | 23 | def __setitem__(self, addr, value): 24 | if isinstance(addr, slice): 25 | return self._wslice(addr, value) 26 | self._rwbuf[0] = value 27 | self.readwrite(addr, self._rwbuf, False) 28 | 29 | def __getitem__(self, addr): 30 | if isinstance(addr, slice): 31 | return self._rslice(addr) 32 | return self.readwrite(addr, self._rwbuf, True)[0] 33 | 34 | # Handle special cases of a slice. Always return a pair of positive indices. 35 | def _do_slice(self, addr): 36 | if not (addr.step is None or addr.step == 1): 37 | raise NotImplementedError("only slices with step=1 (aka None) are supported") 38 | start = addr.start if addr.start is not None else 0 39 | stop = addr.stop if addr.stop is not None else self._a_bytes 40 | start = start if start >= 0 else self._a_bytes + start 41 | stop = stop if stop >= 0 else self._a_bytes + stop 42 | return start, stop 43 | 44 | def _wslice(self, addr, value): 45 | start, stop = self._do_slice(addr) 46 | try: 47 | if len(value) == (stop - start): 48 | res = self.readwrite(start, value, False) 49 | else: 50 | raise RuntimeError("Slice must have same length as data") 51 | except TypeError: 52 | raise RuntimeError("Can only assign bytes/bytearray to a slice") 53 | return res 54 | 55 | def _rslice(self, addr): 56 | start, stop = self._do_slice(addr) 57 | buf = bytearray(stop - start) 58 | return self.readwrite(start, buf, True) 59 | 60 | # IOCTL protocol. 61 | def sync(self): # Nothing to do for unbuffered devices. Subclass overrides. 62 | return 63 | 64 | def readblocks(self, blocknum, buf, offset=0): 65 | self.readwrite(offset + (blocknum << self._nbits), buf, True) 66 | 67 | def writeblocks(self, blocknum, buf, offset=0): 68 | self.readwrite(offset + (blocknum << self._nbits), buf, False) 69 | 70 | # https://docs.micropython.org/en/latest/library/os.html#os.AbstractBlockDev.ioctl 71 | def ioctl(self, op, arg): # ioctl calls: see extmod/vfs.h 72 | if op == 3: # SYNCHRONISE 73 | self.sync() 74 | return 75 | if op == 4: # BP_IOCTL_SEC_COUNT 76 | return self._a_bytes >> self._nbits 77 | if op == 5: # BP_IOCTL_SEC_SIZE 78 | return self._block_size 79 | if op == 6: # Ignore ERASE because handled by driver. 80 | return 0 81 | 82 | 83 | # Hardware agnostic base class for EEPROM arrays 84 | class EepromDevice(BlockDevice): 85 | def __init__(self, nbits, nchips, chip_size, page_size, verbose): 86 | super().__init__(nbits, nchips, chip_size) 87 | # Handle page size arg 88 | if page_size not in (None, 16, 32, 64, 128, 256): 89 | raise ValueError(f"Invalid page size: {page_size}") 90 | self._set_pagesize(page_size) # Set page size 91 | verbose and print("Page size:", self._page_size) 92 | 93 | def _psize(self, ps): # Set page size and page mask 94 | self._page_size = ps 95 | self._page_mask = ~(ps - 1) 96 | 97 | def get_page_size(self): # For test script 98 | return self._page_size 99 | 100 | # Measuring page size should not be done in production code. See docs. 101 | def _set_pagesize(self, page_size): 102 | if page_size is None: # Measure it. 103 | self._psize(16) # Conservative 104 | old = self[:129] # Save old contents (nonvolatile!) 105 | self._psize(256) # Ambitious 106 | r = (16, 32, 64, 128) # Legal page sizes + 256 107 | for x in r: 108 | self[x] = 255 # Write single bytes, don't invoke page write 109 | self[0:129] = b"\0" * 129 # Zero 129 bytes attempting to use 256 byte pages 110 | try: 111 | ps = next(z for z in r if self[z]) 112 | except StopIteration: 113 | ps = 256 114 | self._psize(ps) 115 | self[:129] = old 116 | else: # Validated page_size was supplied 117 | self._psize(page_size) 118 | 119 | 120 | # Hardware agnostic base class for flash memory. 121 | 122 | _RDBUFSIZE = const(32) # Size of read buffer for erasure test 123 | 124 | 125 | class FlashDevice(BlockDevice): 126 | def __init__(self, nbits, nchips, chip_size, sec_size): 127 | super().__init__(nbits, nchips, chip_size) 128 | self.sec_size = sec_size 129 | self._cache_mask = sec_size - 1 # For 4K sector size: 0xfff 130 | self._fmask = self._cache_mask ^ 0x3FFFFFFF # 4K -> 0x3ffff000 131 | self._buf = bytearray(_RDBUFSIZE) 132 | self._mvbuf = memoryview(self._buf) 133 | self._cache = bytearray(sec_size) # Cache always contains one sector 134 | self._mvd = memoryview(self._cache) 135 | self._acache = 0 # Address in chip of byte 0 of current cached sector. 136 | # A newly cached sector, or one which has been flushed, will be clean, 137 | # so .sync() will do nothing. If cache is modified, dirty will be set. 138 | self._dirty = False 139 | 140 | def read(self, addr, mvb): 141 | nbytes = len(mvb) 142 | next_sec = self._acache + self.sec_size # Start of next sector 143 | if addr >= next_sec or addr + nbytes <= self._acache: 144 | self.rdchip(addr, mvb) # No data is cached: just read from device 145 | else: 146 | # Some of address range is cached 147 | boff = 0 # Offset into buf 148 | if addr < self._acache: # Read data prior to cache from chip 149 | nr = self._acache - addr 150 | self.rdchip(addr, mvb[:nr]) 151 | addr = self._acache # Start of cached data 152 | nbytes -= nr 153 | boff += nr 154 | # addr now >= self._acache: read from cache. 155 | sa = addr - self._acache # Offset into cache 156 | nr = min(nbytes, self._acache + self.sec_size - addr) # No of bytes to read from cache 157 | mvb[boff : boff + nr] = self._mvd[sa : sa + nr] 158 | if nbytes - nr: # Get any remaining data from chip 159 | self.rdchip(addr + nr, mvb[boff + nr :]) 160 | return mvb 161 | 162 | def sync(self): 163 | if self._dirty: 164 | self.flush(self._mvd, self._acache) # Write out old data 165 | self._dirty = False 166 | return 0 167 | 168 | # Performance enhancement: if cache intersects address range, update it first. 169 | # Currently in this case it would be written twice. This may be rare. 170 | def write(self, addr, mvb): 171 | nbytes = len(mvb) 172 | acache = self._acache 173 | boff = 0 # Offset into buf. 174 | while nbytes: 175 | if (addr & self._fmask) != acache: 176 | self.sync() # Erase sector and write out old data 177 | self._fill_cache(addr) # Cache sector which includes addr 178 | offs = addr & self._cache_mask # Offset into cache 179 | npage = min(nbytes, self.sec_size - offs) # No. of bytes in current sector 180 | self._mvd[offs : offs + npage] = mvb[boff : boff + npage] 181 | self._dirty = True # Cache contents do not match those of chip 182 | nbytes -= npage 183 | boff += npage 184 | addr += npage 185 | return mvb 186 | 187 | # Cache the sector which contains a given byte addresss. Save sector 188 | # start address. 189 | def _fill_cache(self, addr): 190 | addr &= self._fmask 191 | self.rdchip(addr, self._mvd) 192 | self._acache = addr 193 | self._dirty = False 194 | 195 | def initialise(self): 196 | self._fill_cache(0) 197 | 198 | # Return True if a sector is erased. 199 | def is_empty(self, addr, ev=0xFF): 200 | mvb = self._mvbuf 201 | erased = True 202 | nbufs = self.sec_size // _RDBUFSIZE # Read buffers per sector 203 | for _ in range(nbufs): 204 | self.rdchip(addr, mvb) 205 | if any(True for x in mvb if x != ev): 206 | erased = False 207 | break 208 | addr += _RDBUFSIZE 209 | return erased 210 | -------------------------------------------------------------------------------- /eeprom/i2c/I2C.md: -------------------------------------------------------------------------------- 1 | # 1. A MicroPython I2C EEPROM driver 2 | 3 | This driver supports chips from the 64KiB 25xx512 series and related chips with 4 | smaller capacities, now including chips as small as 2KiB with single byte 5 | addressing. 6 | 7 | From one to eight chips may be used to construct a nonvolatile memory module 8 | with sizes upto 512KiB. The driver allows the memory either to be mounted in 9 | the target filesystem as a disk device or to be addressed as an array of bytes. 10 | Where multiple chips are used, all must be the same size. 11 | 12 | The work was inspired by [this driver](https://github.com/dda/MicroPython.git). 13 | This was written some five years ago. The driver in this repo employs some of 14 | the subsequent improvements to MicroPython to achieve these advantages: 15 | 1. It supports multiple EEPROM chips to configure a single array. 16 | 2. Writes are up to 1000x faster by using ACK polling and page writes. 17 | 3. Page access improves the speed of multi-byte reads. 18 | 4. It is cross-platform. 19 | 5. The I2C bus can be shared with other chips. 20 | 6. It supports filesystem mounting. 21 | 7. Alternatively it can support byte-level access using Python slice syntax. 22 | 8. RAM allocations are reduced. 23 | 24 | ## 1.1 Release Notes 25 | 26 | January 2024 Fixes a bug whereby incorrect page size caused data corruption. 27 | Thanks are due to Abel Deuring for help in diagnosing and fixing this, also for 28 | educating me on the behaviour of various types of EEPROM chip. This release also 29 | supports some chips of 2KiB and below which store the upper three address bits 30 | in the chip address. See [6. Small chips case study](./I2C.md#6-small-chips-case-study). 31 | 32 | ##### [Main readme](../../README.md) 33 | 34 | # 2. Connections 35 | 36 | Any I2C interface may be used. The table below assumes a Pyboard running I2C(2) 37 | as per the test program. To wire up a single EEPROM chip, connect to a Pyboard 38 | or ESP8266 as below. Any ESP8266 pins may be used, those listed below are as 39 | used in the test program. 40 | 41 | EEPROM Pin numbers assume a PDIP package (8 pin plastic dual-in-line). 42 | 43 | | EEPROM | PB | ESP8266 | 44 | |:------:|:---:|:-------:| 45 | | 1 A0 | Gnd | Gnd | 46 | | 2 A1 | Gnd | Gnd | 47 | | 3 A2 | Gnd | Gnd | 48 | | 4 Vss | Gnd | Gnd | 49 | | 5 Sda | Y10 | 12 D6 | 50 | | 6 Scl | Y9 | 13 D7 | 51 | | 7 WPA1 | Gnd | Gnd | 52 | | 8 Vcc | 3V3 | 3V3 | 53 | 54 | For multiple chips the address lines A0, A1 and A2 of each chip need to be 55 | wired to 3V3 in such a way as to give each device a unique address. In the case 56 | where chips are to form a single array these must start at zero and be 57 | contiguous: 58 | 59 | | Chip no. | A2 | A1 | A0 | 60 | |:--------:|:---:|:---:|:---:| 61 | | 0 | Gnd | Gnd | Gnd | 62 | | 1 | Gnd | Gnd | 3V3 | 63 | | 2 | Gnd | 3V3 | Gnd | 64 | | 3 | Gnd | 3V3 | 3V3 | 65 | | 4 | 3V3 | Gnd | Gnd | 66 | | 5 | 3V3 | Gnd | 3V3 | 67 | | 6 | 3V3 | 3V3 | Gnd | 68 | | 7 | 3V3 | 3V3 | 3V3 | 69 | 70 | Multiple chips should have 3V3, Gnd, SCL and SDA lines wired in parallel. 71 | 72 | The I2C interface requires pullups, typically 3.3KΩ to 3.3V although any value 73 | up to 10KΩ will suffice. The Pyboard 1.x has these on board. The Pyboard D has 74 | them only on I2C(1). Even if boards have pullups, additional externalresistors 75 | will do no harm. 76 | 77 | If you use a Pyboard D and power the EEPROMs from the 3V3 output you will need 78 | to enable the voltage rail by issuing: 79 | ```python 80 | machine.Pin.board.EN_3V3.value(1) 81 | time.sleep(0.1) # Allow decouplers to charge 82 | ``` 83 | Other platforms may vary. 84 | 85 | # 3. Files 86 | 87 | 1. `eeprom_i2c.py` Device driver. 88 | 2. `bdevice.py` (In root directory) Base class for the device driver. 89 | 3. `eep_i2c.py` Pyboard test programs for above (adapt for other hosts). 90 | 91 | ## 3.1 Installation 92 | 93 | This installs the above files in the `lib` directory. 94 | 95 | On networked hardware this may be done with `mip` which is included in recent 96 | firmware. On non-networked hardware this is done using the official 97 | [mpremote utility](http://docs.micropython.org/en/latest/reference/mpremote.html) 98 | which should be installed on the PC as described in this doc. 99 | 100 | #### Any hardware 101 | 102 | On the PC issue: 103 | ```bash 104 | $ mpremote mip install "github:peterhinch/micropython_eeprom/eeprom/i2c" 105 | ``` 106 | 107 | #### Networked hardware 108 | 109 | At the device REPL issue: 110 | ```python 111 | >>> import mip 112 | >>> mip.install("github:peterhinch/micropython_eeprom/eeprom/i2c") 113 | ``` 114 | 115 | # 4. The device driver 116 | 117 | The driver supports mounting the EEPROM chips as a filesystem. Initially the 118 | device will be unformatted so it is necessary to issue code along these lines 119 | to format the device. Code assumes one or more 64KiB devices and also assumes 120 | the littlefs filesystem: 121 | 122 | ```python 123 | import os 124 | from machine import I2C 125 | from eeprom_i2c import EEPROM, T24C512 126 | eep = EEPROM(I2C(2), T24C512) 127 | # Format the filesystem 128 | os.VfsLfs2.mkfs(eep) # Omit this to mount an existing filesystem 129 | os.mount(eep,'/eeprom') 130 | ``` 131 | The above will reformat a drive with an existing filesystem: to mount an 132 | existing filesystem simply omit the commented line. 133 | 134 | Note that, at the outset, you need to decide whether to use the array as a 135 | mounted filesystem or as a byte array. The filesystem is relatively small but 136 | has high integrity owing to the hardware longevity. Typical use-cases involve 137 | files which are frequently updated. These include files used for storing Python 138 | objects serialised using Pickle/ujson or files holding a btree database. 139 | 140 | The I2C bus must be instantiated using the `machine` module. 141 | 142 | ## 4.1 The EEPROM class 143 | 144 | An `EEPROM` instance represents a logical EEPROM: this may consist of multiple 145 | physical devices on a common I2C bus. 146 | 147 | ### 4.1.1 Constructor 148 | 149 | This scans the I2C bus - if one or more correctly addressed chips are detected 150 | an EEPROM array is instantiated. A `RuntimeError` will be raised if no device 151 | is detected or if device address lines are not wired as described in 152 | [Connections](./README.md#2-connections). 153 | 154 | Arguments: 155 | 1. `i2c` Mandatory. An initialised master mode I2C bus created by `machine`. 156 | 2. `chip_size=T24C512` The chip size in bits. The module provides constants 157 | `T24C32`, `T24C64`, `T24C128`, `T24C256`, `T24C512` for the supported 158 | chip sizes. 159 | 3. `verbose=True` If `True`, the constructor issues information on the EEPROM 160 | devices it has detected. 161 | 4. `block_size=9` The block size reported to the filesystem. The size in bytes 162 | is `2**block_size` so is 512 bytes by default. 163 | 5. `addr` Override base address for first chip. See 164 | [4.1.6 Special configurations](./I2C.md#416-special-configurations). 165 | 6. `max_chips_count` Override max_chips_count - see above reference. 166 | 7. `page_size=None` EEPROM chips have a page buffer. By default the driver 167 | determines the size of this automatically. It is possible to override this by 168 | passing an integer being the page size in bytes: 16, 32, 64, 128 or 256. See 169 | [4.1.5 Page size](./I2C.md#415-page-size) for issues surrounding this. 170 | 171 | In most cases only the first two arguments are used, with an array being 172 | instantiated with (for example): 173 | ```python 174 | from machine import I2C 175 | from eeprom_i2c import EEPROM, T24C512 176 | eep = EEPROM(I2C(2), T24C512) 177 | ``` 178 | 179 | ### 4.1.2 Methods providing byte level access 180 | 181 | It is possible to read and write individual bytes or arrays of arbitrary size. 182 | Larger arrays are faster, especially when writing: the driver uses the chip's 183 | hardware page access where possible. Writing a page takes the same time (~5ms) 184 | as writing a single byte. 185 | 186 | #### 4.1.2.1 `__getitem__` and `__setitem__` 187 | 188 | These provides single byte or multi-byte access using slice notation. Example 189 | of single byte access: 190 | 191 | ```python 192 | from machine import I2C 193 | from eeprom_i2c import EEPROM, T24C512 194 | eep = EEPROM(I2C(2), T24C512) 195 | eep[2000] = 42 196 | print(eep[2000]) # Return an integer 197 | ``` 198 | It is also possible to use slice notation to read or write multiple bytes. If 199 | writing, the size of the slice must match the length of the buffer: 200 | ```python 201 | from machine import I2C 202 | from eeprom_i2c import EEPROM, T24C512 203 | eep = EEPROM(I2C(2), T24C512) 204 | eep[2000:2002] = bytearray((42, 43)) 205 | print(eep[2000:2002]) # Returns a bytearray 206 | ``` 207 | Three argument slices are not supported: a third arg (other than 1) will cause 208 | an exception. One argument slices (`eep[:5]` or `eep[13100:]`) and negative 209 | args are supported. See [section 4.2](./I2C.md#42-byte-addressing-usage-example) 210 | for a typical application. 211 | 212 | #### 4.1.2.2 readwrite 213 | 214 | This is a byte-level alternative to slice notation. It has the potential 215 | advantage when reading of using a pre-allocated buffer. Arguments: 216 | 1. `addr` Starting byte address 217 | 2. `buf` A `bytearray` or `bytes` instance containing data to write. In the 218 | read case it must be a (mutable) `bytearray` to hold data read. 219 | 3. `read` If `True`, perform a read otherwise write. The size of the buffer 220 | determines the quantity of data read or written. A `RuntimeError` will be 221 | thrown if the read or write extends beyond the end of the physical space. 222 | 223 | ### 4.1.3 Other methods 224 | 225 | #### The len operator 226 | 227 | The size of the EEPROM array in bytes may be retrieved by issuing `len(eep)` 228 | where `eep` is the `EEPROM` instance. 229 | 230 | #### scan 231 | 232 | Scans the I2C bus and returns the number of EEPROM devices detected. 233 | 234 | Other than for debugging there is no need to call `scan()`: the constructor 235 | will throw a `RuntimeError` if it fails to communicate with and correctly 236 | identify the chip. 237 | 238 | #### get_page_size 239 | 240 | Return the page size in bytes. 241 | 242 | ### 4.1.4 Methods providing the block protocol 243 | 244 | These are provided by the base class. For the protocol definition see 245 | [the pyb documentation](http://docs.micropython.org/en/latest/library/uos.html#uos.AbstractBlockDev) 246 | also [here](http://docs.micropython.org/en/latest/reference/filesystem.html#custom-block-devices). 247 | 248 | These methods exist purely to support the block protocol. They are undocumented: 249 | their use in application code is not recommended. 250 | 251 | `readblocks()` 252 | `writeblocks()` 253 | `ioctl()` 254 | 255 | ### 4.1.5 Page size 256 | 257 | EEPROM chips have a RAM buffer enabling fast writing of data blocks. Writing a 258 | page takes the same time (~5ms) as writing a single byte. The page size may vary 259 | between chips from different manufacturers even for the same storage size. 260 | Specifying too large a value will most likely lead to data corruption in write 261 | operations and will cause the test script's basic test to fail. Too small a 262 | value will impact write performance. The correct value for a device may be found 263 | in in the chip datasheet. It is also reported if `verbose` is set and when 264 | running the test scripts. 265 | 266 | Auto-detecting page size carries a risk of data loss if power fails while 267 | auto-detect is in progress. In production code the value should be specified 268 | explicitly. 269 | 270 | ### 4.1.6 Special configurations 271 | 272 | It is possible to configure multiple chips as multiple arrays. This is done by 273 | means of the `addr` and `max_chips_count` args. Examples: 274 | ```python 275 | eeprom0 = EEPROM(i2c, max_chips_count = 2) 276 | eeprom1 = EEPROM(i2c, addr = 0x52, max_chips_count = 2) 277 | ``` 278 | 1st array uses address 0x50 and 0x51 and 2nd uses address 0x52 and 0x53. 279 | 280 | Individual chip usage: 281 | ```python 282 | eeprom0 = EEPROM(i2c, addr = 0x50, max_chips_count = 1) 283 | eeprom1 = EEPROM(i2c, addr = 0x51, max_chips_count = 1) 284 | ``` 285 | 286 | ## 4.2 Byte addressing usage example 287 | 288 | A sample application: saving a configuration dict (which might be large and 289 | complicated): 290 | ```python 291 | import ujson 292 | from machine import I2C 293 | from eeprom_i2c import EEPROM, T24C512 294 | eep = EEPROM(I2C(2), T24C512) 295 | d = {1:'one', 2:'two'} # Some kind of large object 296 | wdata = ujson.dumps(d).encode('utf8') 297 | sl = '{:10d}'.format(len(wdata)).encode('utf8') 298 | eep[0 : len(sl)] = sl # Save data length in locations 0-9 299 | start = 10 # Data goes in 10: 300 | end = start + len(wdata) 301 | eep[start : end] = wdata 302 | ``` 303 | After a power cycle the data may be read back. Instantiate `eep` as above, then 304 | issue: 305 | ```python 306 | slen = int(eep[:10].decode().strip()) # retrieve object size 307 | start = 10 308 | end = start + slen 309 | d = ujson.loads(eep[start : end]) 310 | ``` 311 | It is much more efficient in space and performance to store data in binary form 312 | but in many cases code simplicity matters, especially where the data structure 313 | is subject to change. An alternative to JSON is the pickle module. It is also 314 | possible to use JSON/pickle to store objects in a filesystem. 315 | 316 | # 5. Test program eep_i2c.py 317 | 318 | This assumes a Pyboard 1.x or Pyboard D with EEPROM(s) wired as above. On other 319 | hardware, adapt `get_eep` at the start of the script. It provides the following. 320 | 321 | ## 5.1 test() 322 | 323 | This performs a basic test of single and multi-byte access to chip 0. The test 324 | reports how many chips can be accessed. The current page size is printed and its 325 | validity is tested. Existing array data will be lost. This primarily tests the 326 | driver: as a hardware test it is not exhaustive. 327 | 328 | ## 5.2 full_test() 329 | 330 | This is a hardware test. Tests the entire array. Fills the array with random 331 | data in blocks of 256 byes. After each block is written, it is read back and the 332 | contents compared to the data written. Existing array data will be lost. 333 | 334 | ## 5.3 fstest(format=False) 335 | 336 | If `True` is passed, formats the EEPROM array as a littlefs filesystem and 337 | mounts the device on `/eeprom`. If no arg is passed it mounts the array and 338 | lists the contents. It also prints the outcome of `uos.statvfs` on the array. 339 | 340 | ## 5.4 cptest() 341 | 342 | Tests copying the source files to the filesystem. The test will fail if the 343 | filesystem was not formatted. Lists the contents of the mountpoint and prints 344 | the outcome of `uos.statvfs`. This test does not run on ESP8266 owing to a 345 | missing Python language feature. Use File Copy or `upysh` as described below to 346 | verify the filesystem. 347 | 348 | ## 5.5 File copy 349 | 350 | A rudimentary `cp(source, dest)` function is provided as a generic file copy 351 | routine for setup and debugging purposes at the REPL. The first argument is the 352 | full pathname to the source file. The second may be a full path to the 353 | destination file or a directory specifier which must have a trailing '/'. If an 354 | OSError is thrown (e.g. by the source file not existing or the EEPROM becoming 355 | full) it is up to the caller to handle it. For example (assuming the EEPROM is 356 | mounted on /eeprom): 357 | 358 | ```python 359 | cp('/flash/main.py','/eeprom/') 360 | ``` 361 | 362 | See `upysh` in [micropython-lib](https://github.com/micropython/micropython-lib/tree/master/micropython/upysh) 363 | for filesystem tools for use at the REPL. 364 | 365 | # 6. Small chips case study 366 | 367 | A generic 2KiB EEPROM was tested. Performing an I2C scan revealed that it 368 | occupied 8 I2C addresses starting at 80 (0x50). Note it would be impossible to 369 | configure such chips in a multi-chip array as all eight addresses are used: the 370 | chip can be regarded as an array of eight 256 byte virtual chips. The driver was 371 | therefore initialised as follows: 372 | ```python 373 | i2c = SoftI2C(scl=Pin(9, Pin.OPEN_DRAIN, value=1), sda=Pin(8, Pin.OPEN_DRAIN, value=1)) 374 | eep = EEPROM(i2c, 256, addr=0x50) 375 | ``` 376 | A hard I2C interface would also work. At risk of stating the obvious it is not 377 | possible to build a filesystem on a chip of this size. Tests `eep_i2c.test` and 378 | `eep_i2c.full_test` should be run and will work if the driver is correctly 379 | configured. 380 | -------------------------------------------------------------------------------- /eeprom/i2c/eep_i2c.py: -------------------------------------------------------------------------------- 1 | # eep_i2c.py MicroPython test program for Microchip I2C EEPROM devices. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2019-2024 Peter Hinch 5 | 6 | import uos 7 | import time 8 | from machine import I2C, Pin, SoftI2C 9 | from eeprom_i2c import EEPROM, T24C512 10 | 11 | # Return an EEPROM array. Adapt for platforms other than Pyboard or chips 12 | # smaller than 64KiB. 13 | def get_eep(): 14 | # Special code for Pyboard D: enable 3.3V output 15 | if uos.uname().machine.split(" ")[0][:4] == "PYBD": 16 | Pin.board.EN_3V3.value(1) 17 | time.sleep(0.1) # Allow decouplers to charge 18 | 19 | if uos.uname().sysname == "esp8266": # ESP8266 test fixture 20 | eep = EEPROM(SoftI2C(scl=Pin(13, Pin.OPEN_DRAIN), sda=Pin(12, Pin.OPEN_DRAIN)), T24C512) 21 | elif uos.uname().sysname == "esp32": # ChronoDot on ESP32-S3 22 | eep = EEPROM(SoftI2C(scl=Pin(9, Pin.OPEN_DRAIN), sda=Pin(8, Pin.OPEN_DRAIN)), 256, addr=0x50) 23 | else: # Pyboard D test fixture 24 | eep = EEPROM(I2C(2), T24C512) 25 | print("Instantiated EEPROM") 26 | return eep 27 | 28 | 29 | # Yield pseudorandom bytes (random module not available on all ports) 30 | def psrand8(x=0x3FBA2): 31 | while True: 32 | x ^= (x & 0x1FFFF) << 13 33 | x ^= x >> 17 34 | x ^= (x & 0x1FFFFFF) << 5 35 | yield x & 0xFF 36 | 37 | 38 | # Given a source of pseudorandom bytes yield pseudorandom 256 byte buffer. 39 | def psrand256(rand, ba=bytearray(256)): 40 | while True: 41 | for z in range(256): 42 | ba[z] = next(rand) 43 | yield ba 44 | 45 | 46 | # Dumb file copy utility to help with managing EEPROM contents at the REPL. 47 | def cp(source, dest): 48 | if dest.endswith("/"): # minimal way to allow 49 | dest = "".join((dest, source.split("/")[-1])) # cp /sd/file /eeprom/ 50 | try: 51 | with open(source, "rb") as infile: # Caller should handle any OSError 52 | with open(dest, "wb") as outfile: # e.g file not found 53 | while True: 54 | buf = infile.read(100) 55 | outfile.write(buf) 56 | if len(buf) < 100: 57 | break 58 | except OSError as e: 59 | if e.errno == 28: 60 | print("Insufficient space for copy.") 61 | else: 62 | raise 63 | 64 | 65 | # ***** TEST OF DRIVER ***** 66 | def _testblock(eep, bs): 67 | if bs >= len(eep): 68 | bs = len(eep) // 2 69 | d0 = b"this >" 70 | d1 = b"xxxxxxxxxxxxx": 84 | return "Block test fail 2:" + str(list(res)) 85 | start = bs 86 | end = bs + len(d1) 87 | eep[start:end] = d1 88 | start = bs - len(d0) 89 | end = start + len(d2) 90 | res = eep[start:end] 91 | if res != d2: 92 | return "Block test fail 3:" + str(list(res)) 93 | 94 | 95 | def test(eep=None): 96 | eep = eep if eep else get_eep() 97 | sa = 1000 98 | address_range = 256 99 | if sa + address_range > len(eep): 100 | sa = (len(eep) - address_range) // 2 101 | for v in range(256): 102 | eep[sa + v] = v 103 | for v in range(256): 104 | if eep[sa + v] != v: 105 | print("Fail at address {} data {} should be {}".format(sa + v, eep[sa + v], v)) 106 | break 107 | else: 108 | print("Test of byte addressing passed") 109 | data = uos.urandom(30) 110 | sa = 2000 111 | if sa + len(data) > len(eep): 112 | sa = (len(eep) - len(data)) // 2 113 | eep[sa : sa + 30] = data 114 | if eep[sa : sa + 30] == data: 115 | print("Test of slice readback passed") 116 | 117 | block = 256 118 | res = _testblock(eep, block) 119 | if res is None: 120 | print("Test block boundary {} passed".format(block)) 121 | else: 122 | print("Test block boundary {} fail".format(block)) 123 | print(res) 124 | block = eep._c_bytes 125 | if eep._a_bytes > block: 126 | res = _testblock(eep, block) 127 | if res is None: 128 | print("Test chip boundary {} passed".format(block)) 129 | else: 130 | print("Test chip boundary {} fail".format(block)) 131 | print(res) 132 | else: 133 | print("Test chip boundary skipped: only one chip!") 134 | pe = eep.get_page_size() + 1 # One byte past page 135 | eep[pe] = 0xFF 136 | write_length = min(257, len(eep)) 137 | eep[:write_length] = b"\0" * write_length 138 | print("Test page size: ", end="") 139 | if eep[pe]: 140 | print("FAIL") 141 | else: 142 | print("passed") 143 | 144 | 145 | # ***** TEST OF FILESYSTEM MOUNT ***** 146 | def fstest(eep=None, format=False): 147 | eep = eep if eep else get_eep() 148 | try: 149 | uos.umount("/eeprom") 150 | except OSError: 151 | pass 152 | # ***** CODE FOR FATFS ***** 153 | # if format: 154 | # os.VfsFat.mkfs(eep) 155 | # ***** CODE FOR LITTLEFS ***** 156 | if format: 157 | uos.VfsLfs2.mkfs(eep) 158 | # General 159 | try: 160 | uos.mount(eep, "/eeprom") 161 | except OSError: 162 | raise OSError("Can't mount device: have you formatted it?") 163 | print('Contents of "/": {}'.format(uos.listdir("/"))) 164 | print('Contents of "/eeprom": {}'.format(uos.listdir("/eeprom"))) 165 | print(uos.statvfs("/eeprom")) 166 | 167 | 168 | def cptest(eep=None): # Assumes pre-existing filesystem of either type 169 | eep = eep if eep else get_eep() 170 | if "eeprom" in uos.listdir("/"): 171 | print("Device already mounted.") 172 | else: 173 | try: 174 | uos.mount(eep, "/eeprom") 175 | except OSError: 176 | print("Fail mounting device. Have you formatted it?") 177 | return 178 | print("Mounted device.") 179 | try: 180 | cp(__file__, "/eeprom/") 181 | # We may have the source file or a precompiled binary (*.mpy) 182 | cp(__file__.replace("eep", "eeprom"), "/eeprom/") 183 | print('Contents of "/eeprom": {}'.format(uos.listdir("/eeprom"))) 184 | print(uos.statvfs("/eeprom")) 185 | except NameError: 186 | print("Test cannot be performed by this MicroPython port. Consider using upysh.") 187 | 188 | 189 | # ***** TEST OF HARDWARE ***** 190 | # Write pseudorandom data to entire array, then read back. Fairly rigorous test. 191 | def full_test(eep=None): 192 | eep = eep if eep else get_eep() 193 | print("Testing with 256 byte blocks of random data...") 194 | r = psrand8() # Instantiate random byte generator 195 | ps = psrand256(r) # Random 256 byte blocks 196 | for sa in range(0, len(eep), 256): 197 | ea = sa + 256 198 | eep[sa:ea] = next(ps) 199 | print(f"Address {sa}..{ea} written\r", end="") 200 | print() 201 | r = psrand8() # Instantiate new random byte generator with same seed 202 | ps = psrand256(r) # Random 256 byte blocks 203 | for sa in range(0, len(eep), 256): 204 | ea = sa + 256 205 | if eep[sa:ea] == next(ps): 206 | print(f"Address {sa}..{ea} readback passed\r", end="") 207 | else: 208 | print(f"Address {sa}..{ea} readback failed.") 209 | print() 210 | 211 | 212 | def help(): 213 | st = """Available commands: 214 | help() Print this text. 215 | test() Basic fuctional test. 216 | full_test() Read-write test of EEPROM chip(s). 217 | fstest() Check or create a filesystem. 218 | cptest() Check a filesystem by copying source files to it. 219 | 220 | Utilities: 221 | get_eep() Initialise and return an EEPROM instance. 222 | cp() Very crude file copy utility. 223 | """ 224 | print(st) 225 | 226 | 227 | help() 228 | -------------------------------------------------------------------------------- /eeprom/i2c/eeprom_i2c.py: -------------------------------------------------------------------------------- 1 | # eeprom_i2c.py MicroPython driver for Microchip I2C EEPROM devices. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2019-2024 Peter Hinch 5 | 6 | # Thanks are due to Abel Deuring for help in diagnosing and fixing a page size issue. 7 | 8 | import time 9 | from micropython import const 10 | from bdevice import EepromDevice 11 | 12 | _ADDR = const(0x50) # Base address of chip 13 | _MAX_CHIPS_COUNT = const(8) # Max number of chips 14 | 15 | T24C512 = const(65536) # 64KiB 512Kbits 16 | T24C256 = const(32768) # 32KiB 256Kbits 17 | T24C128 = const(16384) # 16KiB 128Kbits 18 | T24C64 = const(8192) # 8KiB 64Kbits 19 | T24C32 = const(4096) # 4KiB 32Kbits 20 | 21 | # Logical EEPROM device consists of 1-8 physical chips. Chips must all be the 22 | # same size, and must have contiguous addresses. 23 | class EEPROM(EepromDevice): 24 | def __init__( 25 | self, 26 | i2c, 27 | chip_size=T24C512, 28 | verbose=True, 29 | block_size=9, 30 | addr=_ADDR, 31 | max_chips_count=_MAX_CHIPS_COUNT, 32 | page_size=None, 33 | ): 34 | self._i2c = i2c 35 | if chip_size not in (T24C32, T24C64, T24C128, T24C256, T24C512) and verbose: 36 | print("Warning: possible unsupported chip. Size:", chip_size) 37 | # Get no. of EEPROM chips 38 | nchips, min_chip_address = self.scan(verbose, chip_size, addr, max_chips_count) 39 | self._min_chip_address = min_chip_address 40 | self._i2c_addr = 0 # I2C address of current chip 41 | self._buf1 = bytearray(1) 42 | self._addrbuf = bytearray(2) # Memory offset into current chip 43 | self._onebyte = chip_size <= 256 # Single byte address 44 | # superclass figures out _page_size and _page_mask 45 | super().__init__(block_size, nchips, chip_size, page_size, verbose) 46 | 47 | # Check for a valid hardware configuration 48 | def scan(self, verbose, chip_size, addr, max_chips_count): 49 | devices = self._i2c.scan() # All devices on I2C bus 50 | eeproms = [d for d in devices if addr <= d < addr + max_chips_count] # EEPROM chips 51 | nchips = len(eeproms) 52 | if nchips == 0: 53 | raise RuntimeError("EEPROM not found.") 54 | eeproms = sorted(eeproms) 55 | if len(set(eeproms)) != len(eeproms): 56 | raise RuntimeError("Duplicate addresses were found", eeproms) 57 | if (eeproms[-1] - eeproms[0] + 1) != len(eeproms): 58 | raise RuntimeError("Non-contiguous chip addresses", eeproms) 59 | if verbose: 60 | s = "{} chips detected. Total EEPROM size {}bytes." 61 | print(s.format(nchips, chip_size * nchips)) 62 | return nchips, min(eeproms) 63 | 64 | def _wait_rdy(self): # After a write, wait for device to become ready 65 | self._buf1[0] = 0 66 | while True: 67 | try: 68 | if self._i2c.writeto(self._i2c_addr, self._buf1): # Poll ACK 69 | break 70 | except OSError: 71 | pass 72 | finally: 73 | time.sleep_ms(1) 74 | 75 | # Given an address, set ._i2c_addr and ._addrbuf and return the number of 76 | # bytes that can be processed in the current page 77 | def _getaddr(self, addr, nbytes): # Set up _addrbuf and _i2c_addr 78 | if addr >= self._a_bytes: 79 | raise RuntimeError("EEPROM Address is out of range") 80 | ca, la = divmod(addr, self._c_bytes) # ca == chip no, la == offset into chip 81 | self._addrbuf[0] = (la >> 8) & 0xFF 82 | self._addrbuf[1] = la & 0xFF 83 | self._i2c_addr = self._min_chip_address + ca 84 | pe = (la & self._page_mask) + self._page_size # byte 0 of next page 85 | return min(nbytes, pe - la) 86 | 87 | # Read or write multiple bytes at an arbitrary address 88 | def readwrite(self, addr, buf, read): 89 | nbytes = len(buf) 90 | mvb = memoryview(buf) 91 | start = 0 # Offset into buf. 92 | while nbytes > 0: 93 | npage = self._getaddr(addr, nbytes) # No. of bytes in current page 94 | # assert npage > 0 95 | # Offset address into chip: one or two bytes 96 | vaddr = self._addrbuf[1:] if self._onebyte else self._addrbuf 97 | if read: 98 | self._i2c.writeto(self._i2c_addr, vaddr) 99 | self._i2c.readfrom_into(self._i2c_addr, mvb[start : start + npage]) 100 | else: 101 | self._i2c.writevto(self._i2c_addr, (vaddr, buf[start : start + npage])) 102 | self._wait_rdy() 103 | nbytes -= npage 104 | start += npage 105 | addr += npage 106 | return buf 107 | -------------------------------------------------------------------------------- /eeprom/i2c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["bdevice.py", "github:peterhinch/micropython_eeprom/bdevice.py"], 4 | ["eep_i2c.py", "github:peterhinch/micropython_eeprom/eeprom/i2c/eep_i2c.py"], 5 | ["eeprom_i2c.py", "github:peterhinch/micropython_eeprom/eeprom/i2c/eeprom_i2c.py"] 6 | ], 7 | "version": "0.1" 8 | } 9 | -------------------------------------------------------------------------------- /eeprom/spi/SPI.md: -------------------------------------------------------------------------------- 1 | # 1. A MicroPython SPI EEPROM driver 2 | 3 | This driver supports the Microchip 25xx1024 series of 128KiB SPI EEPROMs and 4 | the STM M95M02-DR 256KiB device. These have 1M and 4M cycles of write endurance 5 | respectively (compared to 10K for Pyboard Flash memory). 6 | 7 | Multiple chips may be used to construct a single logical nonvolatile memory 8 | module. The driver allows the memory either to be mounted in the target 9 | filesystem as a disk device or to be addressed as an array of bytes. 10 | 11 | The driver has the following attributes: 12 | 1. It supports multiple EEPROM chips to configure a single array. 13 | 2. For performance, writes use page writes where possible. 14 | 3. Page access improves the speed of multi-byte reads. 15 | 4. It is cross-platform. 16 | 5. The SPI bus can be shared with other chips. 17 | 6. It supports filesystem mounting. 18 | 7. Alternatively it can support byte-level access using Python slice syntax. 19 | 8. RAM allocations are minimised. Buffer sizes are tiny. 20 | 21 | ## 1.1 Notes 22 | 23 | As of Jan 2024 this driver has been updated to fix a bug where the device page 24 | size was less than 256. A further aim was to make the driver more generic, with 25 | a high chance of working with other SPI EEPROM chips. The constructor has 26 | additional optional args to support this. 27 | 28 | On Pyboard D soft SPI should be used pending resolution of 29 | [this PR](https://github.com/micropython/micropython/pull/13549). 30 | 31 | Code samples assume one or more Microchip devices. If using the STM chip the 32 | SPI baudrate should be 5MHz and the chip size must be specified to the `EEPROM` 33 | constructor, e.g.: 34 | ```python 35 | eep = EEPROM(SPI(2, baudrate=5_000_000), cspins, 256) 36 | ``` 37 | 38 | ##### [Main readme](../../README.md) 39 | 40 | # 2. Connections 41 | 42 | Any SPI interface may be used. The table below assumes a Pyboard running SPI(2) 43 | as per the test program. To wire up a single EEPROM chip, connect to a Pyboard 44 | as below. Pin numbers assume a PDIP package (8 pin plastic dual-in-line) for 45 | the Microchip device and 8 pin SOIC for the STM chip. 46 | 47 | | EEPROM | Signal | PB | Signal | 48 | |:-------:|:------:|:---:|:------:| 49 | | 1 | CS | Y5 | SS/ | 50 | | 2 | SO | Y7 | MISO | 51 | | 3 | WP/ | 3V3 | 3V3 | 52 | | 4 | Vss | Gnd | Gnd | 53 | | 5 | SI | Y8 | MOSI | 54 | | 6 | SCK | Y6 | SCK | 55 | | 7 | HOLD/ | 3V3 | 3V3 | 56 | | 8 | Vcc | 3V3 | 3V3 | 57 | 58 | For multiple chips a separate CS pin must be assigned to each chip: each one 59 | must be wired to a single chip's CS line. Multiple chips should have 3V3, Gnd, 60 | SCL, MOSI and MISO lines wired in parallel. 61 | 62 | If you use a Pyboard D and power the EEPROMs from the 3V3 output you will need 63 | to enable the voltage rail by issuing: 64 | ```python 65 | machine.Pin.board.EN_3V3.value(1) 66 | time.sleep(0.1) # Allow decouplers to charge 67 | ``` 68 | Other platforms may vary. 69 | 70 | It is wise to add a pullup resistor (say 10KΩ) from each CS/ line to 3.3V. This 71 | ensures that chips are deselected at initial power up when the microcontroller 72 | I/O pins are high impedance. 73 | 74 | ## 2.1 SPI Bus 75 | 76 | The Microchip devices support baudrates up to 20MHz. The STM chip has a maximum 77 | of 5MHz. Both support the default SPI mode: simply specify the baudrate to the 78 | constructor. 79 | 80 | The SPI bus is fast: wiring should be short and direct. 81 | 82 | # 3. Files 83 | 84 | 1. `eeprom_spi.py` Device driver. 85 | 2. `bdevice.py` (In root directory) Base class for the device driver. 86 | 3. `eep_spi.py` Test programs for above. 87 | 88 | ## 3.1 Installation 89 | 90 | This installs the first three files in the `lib` directory. 91 | 92 | On networked hardware this may be done with `mip` which is included in recent 93 | firmware. On non-networked hardware this is done using the official 94 | [mpremote utility](http://docs.micropython.org/en/latest/reference/mpremote.html) 95 | which should be installed on the PC as described in this doc. 96 | 97 | #### Any hardware 98 | 99 | On the PC issue: 100 | ```bash 101 | $ mpremote mip install "github:peterhinch/micropython_eeprom/eeprom/spi" 102 | ``` 103 | 104 | #### Networked hardware 105 | 106 | At the device REPL issue: 107 | ```python 108 | >>> import mip 109 | >>> mip.install("github:peterhinch/micropython_eeprom/eeprom/spi") 110 | ``` 111 | 112 | # 4. The device driver 113 | 114 | The driver supports mounting the EEPROM chips as a filesystem. Initially the 115 | device will be unformatted so it is necessary to issue code along these lines 116 | to format the device. Code assumes two Microchip devices and also assumes the 117 | littlefs filesystem: 118 | 119 | ```python 120 | import os 121 | from machine import SPI, Pin 122 | from eeprom_spi import EEPROM 123 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 124 | eep = EEPROM(SPI(2, baudrate=20_000_000), cspins, 128) # 128KiB chips 125 | # Format the filesystem 126 | os.VfsLfs2.mkfs(eep) # Omit this to mount an existing filesystem 127 | os.mount(eep,'/eeprom') 128 | ``` 129 | The above will reformat a drive with an existing filesystem: to mount an 130 | existing filesystem simply omit the commented line. 131 | 132 | Note that, at the outset, you need to decide whether to use the array as a 133 | mounted filesystem or as a byte array. The filesystem is relatively small but 134 | has high integrity owing to the hardware longevity. Typical use-cases involve 135 | files which are frequently updated. These include files used for storing Python 136 | objects serialised using Pickle/json or files holding a btree database. 137 | 138 | The SPI bus must be instantiated using the `machine` module. 139 | 140 | ## 4.1 The EEPROM class 141 | 142 | An `EEPROM` instance represents a logical EEPROM: this may consist of multiple 143 | physical devices on a common SPI bus. Alternatively multiple EEPROM instances 144 | may share the bus, differentiated by their CS pins. 145 | 146 | ### 4.1.1 Constructor 147 | 148 | This test each chip in the list of chip select pins - if a chip is detected on 149 | each chip select line an EEPROM array is instantiated. A `RuntimeError` will be 150 | raised if a device is not detected on a CS line. 151 | 152 | Arguments: 153 | 1. `spi` An initialised SPI bus created by `machine`. 154 | 2. `cspins` A list or tuple of `Pin` instances. Each `Pin` must be initialised 155 | as an output (`Pin.OUT`) and with `value=1` and be created by `machine`. 156 | 3. `size` Chip size in KiB. Set to 256 for the STM chip, 128 for the Microchip. 157 | 4. `verbose=True` If `True`, the constructor performs a presence check for an 158 | EEPROM on each chip select pin and reports devices it has detected. See 159 | [4.1.5 Auto detection](./SPI.md#415-auto-detection) for observations on 160 | production code. 161 | 5. `block_size=9` The block size reported to the filesystem. The size in bytes 162 | is `2**block_size` so is 512 bytes by default. 163 | 6. `page_size=None` EEPROM devices have a RAM buffer enabling fast writes. The 164 | driver determines this automatically by default. It is possible to override 165 | this by passing an integer being the page size in bytes: 16, 32, 64, 128 or 256. 166 | See [4.1.5 Auto detection](./SPI.md#415-auto-detection) for reasons why this 167 | is advised in production code. 168 | 169 | SPI baudrate: The 25LC1024 supports baudrates of upto 20MHz. If this value is 170 | specified the platform will produce the highest available frequency not 171 | exceeding this figure. Note that the STM chip has a maximum rate of 5MHz. 172 | 173 | ### 4.1.2 Methods providing byte level access 174 | 175 | It is possible to read and write individual bytes or arrays of arbitrary size. 176 | Larger arrays are faster, especially when writing: the driver uses the chip's 177 | hardware page access where possible. Writing a page (256 bytes) takes the same 178 | time as writing a single byte. This is 6ms max on the Microchip and 10ms max on 179 | the STM. 180 | 181 | The examples below assume two devices, one with `CS` connected to Pyboard pin 182 | Y4 and the other with `CS` connected to Y5. 183 | 184 | #### 4.1.2.1 `__getitem__` and `__setitem__` 185 | 186 | These provides single byte or multi-byte access using slice notation. Example 187 | of single byte access: 188 | 189 | ```python 190 | from machine import SPI, Pin 191 | from eeprom_spi import EEPROM 192 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 193 | eep = EEPROM(SPI(2, baudrate=20_000_000), cspins, 128) 194 | eep[2000] = 42 195 | print(eep[2000]) # Return an integer 196 | ``` 197 | It is also possible to use slice notation to read or write multiple bytes. If 198 | writing, the size of the slice must match the length of the buffer: 199 | ```python 200 | from machine import SPI, Pin 201 | from eeprom_spi import EEPROM 202 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 203 | eep = EEPROM(SPI(2, baudrate=20_000_000), cspins, 128) 204 | eep[2000:2002] = bytearray((42, 43)) 205 | print(eep[2000:2002]) # Returns a bytearray 206 | ``` 207 | Three argument slices are not supported: a third arg (other than 1) will cause 208 | an exception. One argument slices (`eep[:5]` or `eep[13100:]`) and negative 209 | args are supported. See [section 4.2](./SPI.md#42-byte-addressing-usage-example) 210 | for a typical application. 211 | 212 | #### 4.1.2.2 readwrite 213 | 214 | This is a byte-level alternative to slice notation. It has the potential 215 | advantage when reading of using a pre-allocated buffer. Arguments: 216 | 1. `addr` Starting byte address 217 | 2. `buf` A `bytearray` or `bytes` instance containing data to write. In the 218 | read case it must be a (mutable) `bytearray` to hold data read. 219 | 3. `read` If `True`, perform a read otherwise write. The size of the buffer 220 | determines the quantity of data read or written. A `RuntimeError` will be 221 | thrown if the read or write extends beyond the end of the physical space. 222 | 223 | ### 4.1.3 Other methods 224 | 225 | #### The len operator 226 | 227 | The size of the EEPROM array in bytes may be retrieved by issuing `len(eep)` 228 | where `eep` is an `EEPROM` instance. 229 | 230 | #### scan 231 | 232 | Activate each chip select in turn checking for a valid device and returns the 233 | number of EEPROM devices detected. A `RuntimeError` will be raised if any CS 234 | pin does not correspond to a valid chip. 235 | 236 | Other than for debugging there is no need to call `scan()`: the constructor 237 | will throw a `RuntimeError` if it fails to communicate with and correctly 238 | identify the chip. 239 | 240 | #### erase 241 | 242 | Zero the entire array. Can take several seconds. 243 | 244 | #### get_page_size 245 | 246 | Return the page size in bytes. 247 | 248 | ### 4.1.4 Methods providing the block protocol 249 | 250 | These are provided by the base class. For the protocol definition see 251 | [the pyb documentation](http://docs.micropython.org/en/latest/library/uos.html#uos.AbstractBlockDev) 252 | also [here](http://docs.micropython.org/en/latest/reference/filesystem.html#custom-block-devices). 253 | 254 | These methods exist purely to support the block protocol. They are undocumented: 255 | their use in application code is not recommended. 256 | 257 | `readblocks()` 258 | `writeblocks()` 259 | `ioctl()` 260 | 261 | ### 4.1.5 Auto detection 262 | 263 | The driver constructor uses auto-detection in two circumstances: 264 | * If `verbose` is specified, it checks each chip select for chip presence. 265 | * If `page_size` is set to `None` the value is determined by measurement. 266 | 267 | In both cases data is written to the chips, then restored from RAM. If a power 268 | outage were to occur while either process was in progress, corruption could 269 | occur. It is therefore recommended that, in production code, `verbose` is 270 | `False` and `page_size` is set to an integer. The page size may be determined 271 | from the chip datasheet. It is also printed on instantiation if `verbose` is 272 | set: running any of the test scripts will do this. 273 | 274 | ## 4.2 Byte addressing usage example 275 | 276 | A sample application: saving a configuration dict (which might be large and 277 | complicated): 278 | ```python 279 | import ujson 280 | from machine import SPI, Pin 281 | from eeprom_spi import EEPROM 282 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 283 | eep = EEPROM(SPI(2, baudrate=20_000_000), cspins, 128) 284 | d = {1:'one', 2:'two'} # Some kind of large object 285 | wdata = ujson.dumps(d).encode('utf8') 286 | sl = '{:10d}'.format(len(wdata)).encode('utf8') 287 | eep[0 : len(sl)] = sl # Save data length in locations 0-9 288 | start = 10 # Data goes in 10: 289 | end = start + len(wdata) 290 | eep[start : end] = wdata 291 | ``` 292 | After a power cycle the data may be read back. Instantiate `eep` as above, then 293 | issue: 294 | ```python 295 | slen = int(eep[:10].decode().strip()) # retrieve object size 296 | start = 10 297 | end = start + slen 298 | d = ujson.loads(eep[start : end]) 299 | ``` 300 | It is much more efficient in space and performance to store data in binary form 301 | but in many cases code simplicity matters, especially where the data structure 302 | is subject to change. An alternative to JSON is the pickle module. It is also 303 | possible to use JSON/pickle to store objects in a filesystem. 304 | 305 | # 5. Test program eep_spi.py 306 | 307 | This assumes a Pyboard 1.x or Pyboard D with two EEPROMs wired to SPI(2) as 308 | above with chip selects connected to pins `Y4` and `Y5`. It provides the 309 | following. In all cases the stm arg should be `True` if using the STM chips. 310 | On other hardware, adapt `cspins` and `get_eep` at the start of the script. 311 | 312 | ## 5.1 test(stm=False) 313 | 314 | This performs a basic test of single and multi-byte access to chip 0. The test 315 | reports how many chips can be accessed. The current page size is printed and its 316 | validity is tested. Existing array data will be lost. This primarily tests the 317 | driver: as a hardware test it is not exhaustive. 318 | 319 | ## 5.2 full_test(stm=False) 320 | 321 | This is a hardware test. Tests the entire array. Fills the array with random 322 | data in blocks of 256 byes. After each block is written, it is read back and the 323 | contents compared to the data written. Existing array data will be lost. 324 | 325 | ## 5.3 fstest(format=False, stm=False) 326 | 327 | If `True` is passed, formats the EEPROM array as a littlefs filesystem and 328 | mounts the device on `/eeprom`. If no arg is passed it mounts the array and 329 | lists the contents. It also prints the outcome of `uos.statvfs` on the array. 330 | 331 | ## 5.4 cptest(stm=False) 332 | 333 | Tests copying the source files to the filesystem. The test will fail if the 334 | filesystem was not formatted. Lists the contents of the mountpoint and prints 335 | the outcome of `uos.statvfs`. 336 | 337 | ## 5.5 File copy 338 | 339 | A rudimentary `cp(source, dest)` function is provided as a generic file copy 340 | routine for setup and debugging purposes at the REPL. The first argument is the 341 | full pathname to the source file. The second may be a full path to the 342 | destination file or a directory specifier which must have a trailing '/'. If an 343 | OSError is thrown (e.g. by the source file not existing or the EEPROM becoming 344 | full) it is up to the caller to handle it. For example (assuming the EEPROM is 345 | mounted on /eeprom): 346 | 347 | ```python 348 | cp('/flash/main.py','/eeprom/') 349 | ``` 350 | 351 | See `upysh` in [micropython-lib](https://github.com/micropython/micropython-lib.git) 352 | for other filesystem tools for use at the REPL. 353 | -------------------------------------------------------------------------------- /eeprom/spi/eep_spi.py: -------------------------------------------------------------------------------- 1 | # eep_spi.py MicroPython test program for Microchip SPI EEPROM devices. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2019-2024 Peter Hinch 5 | 6 | import uos 7 | import time 8 | from machine import SPI, Pin, SoftSPI 9 | from eeprom_spi import EEPROM 10 | 11 | ESP8266 = uos.uname().sysname == "esp8266" 12 | # Add extra pins if using multiple chips 13 | if ESP8266: 14 | cspins = (Pin(5, Pin.OUT, value=1), Pin(14, Pin.OUT, value=1)) 15 | else: 16 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 17 | 18 | # Return an EEPROM array. Adapt for platforms other than Pyboard. 19 | def get_eep(stm): 20 | if uos.uname().machine.split(" ")[0][:4] == "PYBD": 21 | Pin.board.EN_3V3.value(1) 22 | time.sleep(0.1) # Allow decouplers to charge 23 | 24 | if stm: 25 | if ESP8266: 26 | spi = SoftSPI(baudrate=5_000_000, sck=Pin(4), miso=Pin(0), mosi=Pin(2)) 27 | else: # Pyboard. See https://github.com/micropython/micropython/pull/13549 28 | # spi = SPI(2, baudrate=5_000_000) 29 | spi = SoftSPI(baudrate=5_000_000, sck=Pin("Y6"), miso=Pin("Y7"), mosi=Pin("Y8")) 30 | eep = EEPROM(spi, cspins, 256) 31 | else: 32 | if ESP8266: 33 | spi = SoftSPI(baudrate=20_000_000, sck=Pin(4), miso=Pin(0), mosi=Pin(2)) 34 | else: 35 | # spi = SPI(2, baudrate=20_000_000) 36 | spi = SoftSPI(baudrate=20_000_000, sck=Pin("Y6"), miso=Pin("Y7"), mosi=Pin("Y8")) 37 | eep = EEPROM(spi, cspins, 128) 38 | print("Instantiated EEPROM") 39 | return eep 40 | 41 | 42 | # Yield pseudorandom bytes (random module not available on all ports) 43 | def psrand8(x=0x3FBA2): 44 | while True: 45 | x ^= (x & 0x1FFFF) << 13 46 | x ^= x >> 17 47 | x ^= (x & 0x1FFFFFF) << 5 48 | yield x & 0xFF 49 | 50 | 51 | # Given a source of pseudorandom bytes yield pseudorandom 256 byte buffer. 52 | def psrand256(rand, ba=bytearray(256)): 53 | while True: 54 | for z in range(256): 55 | ba[z] = next(rand) 56 | yield ba 57 | 58 | 59 | # Dumb file copy utility to help with managing EEPROM contents at the REPL. 60 | def cp(source, dest): 61 | if dest.endswith("/"): # minimal way to allow 62 | dest = "".join((dest, source.split("/")[-1])) # cp /sd/file /eeprom/ 63 | try: 64 | with open(source, "rb") as infile: # Caller should handle any OSError 65 | with open(dest, "wb") as outfile: # e.g file not found 66 | while True: 67 | buf = infile.read(100) 68 | outfile.write(buf) 69 | if len(buf) < 100: 70 | break 71 | except OSError as e: 72 | if e.errno == 28: 73 | print("Insufficient space for copy.") 74 | else: 75 | raise 76 | 77 | 78 | # ***** TEST OF DRIVER ***** 79 | def _testblock(eep, bs): 80 | d0 = b"this >" 81 | d1 = b"xxxxxxxxxxxxx": 95 | return "Block test fail 2:" + str(list(res)) 96 | start = bs 97 | end = bs + len(d1) 98 | eep[start:end] = d1 99 | start = bs - len(d0) 100 | end = start + len(d2) 101 | res = eep[start:end] 102 | if res != d2: 103 | return "Block test fail 3:" + str(list(res)) 104 | 105 | 106 | def test(stm=False): 107 | eep = get_eep(stm) 108 | sa = 1000 109 | for v in range(256): 110 | eep[sa + v] = v 111 | for v in range(256): 112 | if eep[sa + v] != v: 113 | print("Fail at address {} data {} should be {}".format(sa + v, eep[sa + v], v)) 114 | break 115 | else: 116 | print("Test of byte addressing passed") 117 | data = uos.urandom(30) 118 | sa = 2000 119 | eep[sa : sa + 30] = data 120 | if eep[sa : sa + 30] == data: 121 | print("Test of slice readback passed") 122 | 123 | block = 256 124 | res = _testblock(eep, block) 125 | if res is None: 126 | print("Test block boundary {} passed".format(block)) 127 | else: 128 | print("Test block boundary {} fail".format(block)) 129 | print(res) 130 | block = eep._c_bytes 131 | if eep._a_bytes > block: 132 | res = _testblock(eep, block) 133 | if res is None: 134 | print("Test chip boundary {} passed".format(block)) 135 | else: 136 | print("Test chip boundary {} fail".format(block)) 137 | print(res) 138 | else: 139 | print("Test chip boundary skipped: only one chip!") 140 | pe = eep.get_page_size() # One byte past page 141 | eep[pe] = 0xFF 142 | eep[:257] = b"\0" * 257 143 | print("Test page size: ", end="") 144 | if eep[pe]: 145 | print("FAIL") 146 | else: 147 | print("passed") 148 | 149 | 150 | # ***** TEST OF FILESYSTEM MOUNT ***** 151 | def fstest(format=False, stm=False): 152 | eep = get_eep(stm) 153 | try: 154 | uos.umount("/eeprom") 155 | except OSError: 156 | pass 157 | # ***** CODE FOR FATFS ***** 158 | # if format: 159 | # os.VfsFat.mkfs(eep) 160 | # ***** CODE FOR LITTLEFS ***** 161 | if format: 162 | uos.VfsLfs2.mkfs(eep) 163 | # General 164 | try: 165 | uos.mount(eep, "/eeprom") 166 | except OSError: 167 | raise OSError("Can't mount device: have you formatted it?") 168 | print('Contents of "/": {}'.format(uos.listdir("/"))) 169 | print('Contents of "/eeprom": {}'.format(uos.listdir("/eeprom"))) 170 | print(uos.statvfs("/eeprom")) 171 | 172 | 173 | def cptest(stm=False): # Assumes pre-existing filesystem of either type 174 | eep = get_eep(stm) 175 | if "eeprom" in uos.listdir("/"): 176 | print("Device already mounted.") 177 | else: 178 | try: 179 | uos.mount(eep, "/eeprom") 180 | except OSError: 181 | print("Fail mounting device. Have you formatted it?") 182 | return 183 | print("Mounted device.") 184 | try: 185 | cp(__file__, "/eeprom/") 186 | # We may have the source file or a precompiled binary (*.mpy) 187 | cp(__file__.replace("eep", "eeprom"), "/eeprom/") 188 | print('Contents of "/eeprom": {}'.format(uos.listdir("/eeprom"))) 189 | print(uos.statvfs("/eeprom")) 190 | except NameError: 191 | print("Test cannot be performed by this MicroPython port. Consider using upysh.") 192 | 193 | 194 | # ***** TEST OF HARDWARE ***** 195 | def full_test(stm=False): 196 | eep = get_eep(stm) 197 | print("Testing with 256 byte blocks of random data...") 198 | r = psrand8() # Instantiate random byte generator 199 | ps = psrand256(r) # Random 256 byte blocks 200 | for sa in range(0, len(eep), 256): 201 | ea = sa + 256 202 | eep[sa:ea] = next(ps) 203 | print(f"Address {sa}..{ea} written\r", end="") 204 | print() 205 | r = psrand8() # Instantiate new random byte generator with same seed 206 | ps = psrand256(r) # Random 256 byte blocks 207 | for sa in range(0, len(eep), 256): 208 | ea = sa + 256 209 | if eep[sa:ea] == next(ps): 210 | print(f"Address {sa}..{ea} readback passed\r", end="") 211 | else: 212 | print(f"Address {sa}..{ea} readback failed.") 213 | print() 214 | 215 | 216 | def help(): 217 | test_str = """Available commands (see SPI.md): 218 | help() Print this text. 219 | test(stm=False) Basic hardware test. 220 | full_test(stm=False) Thorough hardware test. 221 | fstest(format=False, stm=False) Filesystem test (see doc). 222 | cptest(stm=False) Copy files to filesystem (see doc). 223 | stm: True is 256K chip, 5MHz bus. False is 128K chip, 20MHz bus. 224 | """ 225 | print(test_str) 226 | 227 | 228 | help() 229 | -------------------------------------------------------------------------------- /eeprom/spi/eeprom_spi.py: -------------------------------------------------------------------------------- 1 | # eeprom_spi.py MicroPython driver for EEPROM chips (see README.md for 2 | # tested devices). 3 | 4 | # Released under the MIT License (MIT). See LICENSE. 5 | # Copyright (c) 2019-2024 Peter Hinch 6 | 7 | # Thanks are due to Abel Deuring for help in diagnosing and fixing a page size issue. 8 | 9 | import time 10 | from os import urandom 11 | from micropython import const 12 | from bdevice import EepromDevice 13 | 14 | # Supported instruction set - common to both chips: 15 | _READ = const(3) 16 | _WRITE = const(2) 17 | _WREN = const(6) # Write enable 18 | _RDSR = const(5) # Read status register 19 | 20 | # Logical EEPROM device comprising one or more physical chips sharing an SPI bus. 21 | # args: SPI bus, tuple of CS Pin instances, chip size in KiB 22 | # verbose: Test for chip presence and report 23 | # block_size: Sector size for filesystems. See docs. 24 | # erok: True if chip supports erase. 25 | # page_size: None is auto detect. See docs. 26 | class EEPROM(EepromDevice): 27 | def __init__(self, spi, cspins, size, verbose=True, block_size=9, page_size=None): 28 | if size not in (64, 128, 256): 29 | print(f"Warning: possible unsupported chip. Size: {size}KiB") 30 | self._spi = spi 31 | self._cspins = cspins 32 | self._ccs = None # Chip select Pin object for current chip 33 | self._size = size * 1024 # Chip size in bytes 34 | self._bufp = bytearray(5) # instruction + 3 byte address + 1 byte value 35 | self._mvp = memoryview(self._bufp) # cost-free slicing 36 | if verbose: # Test for presence of devices 37 | self.scan() 38 | # superclass figures out _page_size and _page_mask 39 | super().__init__(block_size, len(cspins), self._size, page_size, verbose) 40 | if verbose: 41 | print(f"Total EEPROM size {self._a_bytes:,} bytes.") 42 | 43 | # Low level device presence detect. Reads a location, then writes to it. If 44 | # a write value is passed, uses that, otherwise writes the one's complement 45 | # of the value read. 46 | def _devtest(self, cs, la, v=None): 47 | buf = bytearray(1) 48 | mvp = self._mvp 49 | # mvp[:] = b"\0" * 5 # test with addr 0 50 | mvp[1] = la >> 16 51 | mvp[2] = (la >> 8) & 0xFF 52 | mvp[3] = la & 0xFF 53 | mvp[0] = _READ 54 | cs(0) 55 | self._spi.write(mvp[:4]) 56 | res = self._spi.read(1) 57 | cs(1) 58 | mvp[0] = _WREN 59 | cs(0) 60 | self._spi.write(mvp[:1]) 61 | cs(1) 62 | mvp[0] = _WRITE 63 | cs(0) 64 | self._spi.write(mvp[:4]) 65 | buf[0] = res[0] ^ 0xFF if v is None else v 66 | self._spi.write(buf) 67 | cs(1) # Trigger write start 68 | self._ccs = cs 69 | self._wait_rdy() # Wait until done (6ms max) 70 | return res[0] 71 | 72 | def scan(self): 73 | # Generate a random address to minimise wear 74 | la = int.from_bytes(urandom(3), "little") % self._size 75 | for n, cs in enumerate(self._cspins): 76 | old = self._devtest(cs, la) 77 | new = self._devtest(cs, la, old) 78 | if old != new ^ 0xFF: 79 | raise RuntimeError(f"Chip not found at cs[{n}]") 80 | print(f"{n + 1} chips detected.") 81 | return n 82 | 83 | def erase(self): 84 | mvp = self._mvp 85 | block = b"\0" * 256 86 | for n in range(0, self._a_bytes, 256): 87 | self[n : n + 256] = block 88 | 89 | def _wait_rdy(self): # After a write, wait for device to become ready 90 | mvp = self._mvp 91 | cs = self._ccs # Chip is already current 92 | tstart = time.ticks_ms() 93 | while True: 94 | mvp[0] = _RDSR 95 | cs(0) 96 | self._spi.write_readinto(mvp[:2], mvp[:2]) 97 | cs(1) 98 | if not mvp[1]: # We never set BP0 or BP1 so ready state is 0. 99 | break 100 | time.sleep_ms(1) 101 | if time.ticks_diff(time.ticks_ms(), tstart) > 1000: 102 | raise OSError("Device ready timeout.") 103 | 104 | # Given an address, set current chip select and address buffer. 105 | # Return the number of bytes that can be processed in the current page. 106 | def _getaddr(self, addr, nbytes): 107 | if addr >= self._a_bytes: 108 | raise RuntimeError("EEPROM Address is out of range") 109 | ca, la = divmod(addr, self._c_bytes) # ca == chip no, la == offset into chip 110 | self._ccs = self._cspins[ca] # Current chip select 111 | mvp = self._mvp 112 | mvp[1] = la >> 16 113 | mvp[2] = (la >> 8) & 0xFF 114 | mvp[3] = la & 0xFF 115 | pe = (la & self._page_mask) + self._page_size # byte 0 of next page 116 | return min(nbytes, pe - la) 117 | 118 | # Read or write multiple bytes at an arbitrary address 119 | def readwrite(self, addr, buf, read): 120 | nbytes = len(buf) 121 | mvb = memoryview(buf) 122 | mvp = self._mvp 123 | start = 0 # Offset into buf. 124 | while nbytes > 0: 125 | npage = self._getaddr(addr, nbytes) # No. of bytes in current page 126 | cs = self._ccs 127 | assert npage > 0 128 | if read: 129 | mvp[0] = _READ 130 | cs(0) 131 | self._spi.write(mvp[:4]) 132 | self._spi.readinto(mvb[start : start + npage]) 133 | cs(1) 134 | else: 135 | mvp[0] = _WREN 136 | cs(0) 137 | self._spi.write(mvp[:1]) 138 | cs(1) 139 | mvp[0] = _WRITE 140 | cs(0) 141 | self._spi.write(mvp[:4]) 142 | self._spi.write(mvb[start : start + npage]) 143 | cs(1) # Trigger write start 144 | self._wait_rdy() # Wait until done (6ms max) 145 | nbytes -= npage 146 | start += npage 147 | addr += npage 148 | return buf 149 | -------------------------------------------------------------------------------- /eeprom/spi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["bdevice.py", "github:peterhinch/micropython_eeprom/bdevice.py"], 4 | ["eep_spi.py", "github:peterhinch/micropython_eeprom/eeprom/spi/eep_spi.py"], 5 | ["eeprom_spi.py", "github:peterhinch/micropython_eeprom/eeprom/spi/eeprom_spi.py"] 6 | ], 7 | "version": "0.1" 8 | } 9 | -------------------------------------------------------------------------------- /flash/FLASH.md: -------------------------------------------------------------------------------- 1 | # 1. A MicroPython Flash memory driver 2 | 3 | ## 1.1 Device support 4 | 5 | This driver supports the Cypress S25FL256L and S25FL128L chips, providing 32MiB 6 | and 16MiB respectively. These have 100K cycles of write endurance (compared to 7 | 10K for Pyboard Flash memory). These were the largest capacity available with a 8 | sector size small enough for microcontroller use. 9 | 10 | Thanks to a patch from Daniel Thompson this now supports a variety of NOR Flash 11 | chips including those with 24-bit addressing. He tested an XPX XT25F32B; I 12 | tested Winbond W25Q32JV 4MiB and Cypress S25FL064L 8MiB devices. 13 | 14 | It is likely that other chips with 4096 byte blocks will work but I am unlikely 15 | to be able to support hardware I don't possess. See 16 | [Section 6](./FLASH.md#6-unsupported-chips) for recommendations on settings. 17 | 18 | ## 1.2 The driver 19 | 20 | Multiple chips may be used to construct a single logical nonvolatile memory 21 | module. The driver allows the memory either to be mounted in the target 22 | filesystem as a disk device or to be addressed as an array of bytes. 23 | 24 | The driver has the following attributes: 25 | 1. It supports multiple Flash chips to configure a single array. 26 | 2. It is cross-platform. 27 | 3. The SPI bus can be shared with other chips. 28 | 4. It supports filesystem mounting. 29 | 5. Alternatively it can support byte-level access using Python slice syntax. 30 | 31 | Supporting byte level access on Flash technology requires a sector buffer. 32 | Consequently this driver uses 4KiB of RAM (compared to minuscule amounts for 33 | the FRAM and EEPROM drivers). This is an inevitable price for the large 34 | capacity of flash chips. 35 | 36 | FAT and littlefs filesystems are supported but the latter is preferred owing to 37 | its resilience and wear levelling characteristics. Please note that this driver 38 | has been tested on LFS2 only. Users requiring a driver with minimum RAM use 39 | may want to consider [this driver](https://github.com/robert-hh/SPI_Flash). 40 | This supports an LFS1 filesystem on a single flash chip. 41 | 42 | Arguably byte level access on such large devices has few use cases other than 43 | for facilitating effective hardware tests and for diagnostics. 44 | 45 | ##### [Main readme](../README.md) 46 | 47 | # 2. Connections 48 | 49 | Any SPI interface may be used. The table below assumes a Pyboard running SPI(2) 50 | as per the test program. To wire up a single flash chip, connect to a Pyboard 51 | as below. Pin numbers relate to an 8 pin SOIC or WSON package. Inputs marked 52 | `nc` may be connected to 3V3 or left unconnected. 53 | 54 | | Flash | Signal | PB | Signal | 55 | |:-----:|:-------:|:---:|:------:| 56 | | 1 | CS/ | Y5 | SS/ | 57 | | 2 | SO | Y7 | MISO | 58 | | 3 | WP/ | nc | - | 59 | | 4 | Vss | Gnd | Gnd | 60 | | 5 | SI | Y8 | MOSI | 61 | | 6 | SCK | Y6 | SCK | 62 | | 7 | RESET/ | nc | - | 63 | | 8 | Vcc | 3V3 | 3V3 | 64 | 65 | For multiple chips a separate CS pin must be assigned to each chip, each one 66 | being wired to a single chip's CS line. The test program assumes a second chip 67 | with CS connected to Y4. Multiple chips should have 3V3, Gnd, SCL, MOSI and 68 | MISO lines wired in parallel. 69 | 70 | If you use a Pyboard D and power the chips from the 3V3 output you will need 71 | to enable the voltage rail by issuing: 72 | ```python 73 | machine.Pin.board.EN_3V3.value(1) 74 | time.sleep(0.1) # Allow decouplers to charge 75 | ``` 76 | Other devices may vary but the Cypress chips require a 3.3V supply. 77 | 78 | It is wise to add a pullup resistor (say 10KΩ) from each CS/ line to 3.3V. This 79 | ensures that chips are deselected at initial power up when the microcontroller 80 | I/O pins are high impedance. 81 | 82 | ## 2.1 SPI Bus 83 | 84 | The devices support baudrates up to 50MHz. In practice MicroPython targets do 85 | not support such high rates. The test programs specify 20MHz, but in practice 86 | the Pyboard D delivers 15MHz. Testing was done at this rate. In testing a 87 | "lashup" breadboard was unsatisfactory: a problem entirely fixed with a PCB. 88 | Bus lines should be short and direct. 89 | 90 | # 3. Files 91 | 92 | 1. `flash_spi.py` Device driver. 93 | 2. `bdevice.py` (In root directory) Base class for the device driver. 94 | 3. `flash_test.py` Test programs for above. 95 | 4. `littlefs_test.py` Torture test for the littlefs filesystem on the flash 96 | array. Requires `flash_test.py` which it uses for hardware configuration. 97 | 5. `wemos_flash.py` Test program running on a Wemos D1 Mini ESP8266 board. 98 | 99 | Installation: copy files 1 and 2 (3 - 5 are optional) to the target filesystem. 100 | The `flash_test` script assumes two chips connected to SPI(2) with CS/ pins 101 | wired to Pyboard pins Y4 and Y5. Device size is detected at runtime. The 102 | `get_device` function may be adapted for other setups and is shared with 103 | `littlefs_test`. 104 | 105 | For a quick check of hardware issue: 106 | ```python 107 | import flash_test 108 | flash_test.test() 109 | ``` 110 | 111 | # 4. The device driver 112 | 113 | The driver supports mounting the Flash chips as a filesystem. Initially the 114 | device will be unformatted so it is necessary to issue code along these lines 115 | to format the device. Code assumes two devices and the (recommended) littlefs 116 | filesystem: 117 | 118 | ```python 119 | import os 120 | from machine import SPI, Pin 121 | from flash_spi import FLASH 122 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 123 | flash = FLASH(SPI(2, baudrate=20_000_000), cspins) 124 | # Format the filesystem 125 | os.VfsLfs2.mkfs(flash) # Omit this to mount an existing filesystem 126 | os.mount(flash,'/fl_ext') 127 | ``` 128 | The above will reformat a drive with an existing filesystem erasing all files: 129 | to mount an existing filesystem omit the commented line. 130 | 131 | Note that, at the outset, you need to decide whether to use the array as a 132 | mounted filesystem or as a byte array. Most use cases for flash will require a 133 | filesystem, although byte level reads may be used to debug filesystem issues. 134 | 135 | The SPI bus must be instantiated using the `machine` module. 136 | 137 | ## 4.1 The FLASH class 138 | 139 | An `FLASH` instance represents a logical flash memory: this may consist of 140 | multiple physical devices on a common SPI bus. 141 | 142 | ### 4.1.1 Constructor 143 | 144 | This tests each chip in the list of chip select pins - if a chip is detected on 145 | each chip select line a flash array is instantiated. A `RuntimeError` will be 146 | raised if a device is not detected on a CS line. The test has no effect on 147 | the array contents. 148 | 149 | Arguments. In most cases only the first two mandatory args are required: 150 | 1. `spi` An initialised SPI bus created by `machine`. 151 | 2. `cspins` A list or tuple of `Pin` instances. Each `Pin` must be initialised 152 | as an output (`Pin.OUT`) and with `value=1` and be created by `machine`. 153 | 3. `size=None` Chip size in KiB. By default the size is read from the chip; a 154 | `ValueError` will occur if chips in the array have differing sizes. See table 155 | below for values of chips tested to date. If a `size` is specified, the driver 156 | will assume that the value given is correct. If no `size` is specified and the 157 | chip returns an unexpected value, a `ValueError` will be raised. 158 | 4. `verbose=True` If `True`, the constructor issues information on the flash 159 | devices it has detected. 160 | 5. `sec_size=4096` Chip sector size. 161 | 6. `block_size=9` The block size reported to the filesystem. The size in bytes 162 | is `2**block_size` so is 512 bytes by default. 163 | 7. `cmd5=None` Flash chips can support two low level command sets, a 4 byte 164 | set and a 5 byte set. By default if the size read from the chip's ID is 165 | <= 4096KiB the 4 byte set is used oterwise the 5 byte set is adopted. This 166 | works for supported chips. Setting `cmd5` `True` forces 5 byte commands, 167 | `False` forces 4 byte. This override is necessary for certain chip types 168 | (e.g. WinBond W25Q64FV). 169 | 170 | Size values (KiB): 171 | | Chip | Size | 172 | |:-----------------:|:-----:| 173 | | Cypress S25FL256L | 32768 | 174 | | Cypress S25FL128L | 16384 | 175 | | Cypress S25FL064L | 8192 | 176 | | Winbond W25Q32JV | 4096 | 177 | 178 | See [main readme](../README.md#141-chips-tested-by-users) for updates to the 179 | list of supported chips. 180 | 181 | ### 4.1.2 Methods providing byte level access 182 | 183 | It is possible to read and write individual bytes or arrays of arbitrary size. 184 | Because of the very large size of the supported devices this mode is most 185 | likely to be of use for debugging. When writing in this mode it is necessary to 186 | be aware of the characteristics of flash devices. The memory is structured in 187 | blocks of 4096 bytes. To write a byte a block has to be read into RAM and the 188 | byte changed. The block on chip is erased then the new data written out. This 189 | process is slow (~300ms). In practice writing is deferred until it is necessary 190 | to access a different block: it is therefore faster to write data to 191 | consecutive addresses. Writing individual bytes to random addresses would be 192 | slow and cause undue wear because of the repeated need to erase and write 193 | sectors. 194 | 195 | The examples below assume two devices, one with `CS` connected to Pyboard pin 196 | Y4 and the other with `CS` connected to Y5. 197 | 198 | #### 4.1.2.1 `__getitem__` and `__setitem__` 199 | 200 | These provides single byte or multi-byte access using slice notation. Example 201 | of single byte access: 202 | 203 | ```python 204 | from machine import SPI, Pin 205 | from flash_spi import FLASH 206 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 207 | flash = FLASH(SPI(2, baudrate=20_000_000), cspins) 208 | flash[2000] = 42 209 | print(flash[2000]) # Return an integer 210 | ``` 211 | It is also possible to use slice notation to read or write multiple bytes. If 212 | writing, the size of the slice must match the length of the buffer: 213 | ```python 214 | from machine import SPI, Pin 215 | from flash_spi import FLASH 216 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 217 | flash = FLASH(SPI(2, baudrate=20_000_000), cspins) 218 | flash[2000:2002] = bytearray((42, 43)) 219 | print(flash[2000:2002]) # Returns a bytearray 220 | ``` 221 | Three argument slices are not supported: a third arg (other than 1) will cause 222 | an exception. One argument slices (`flash[:5]` or `flash[13100:]`) and negative 223 | args are supported. 224 | 225 | #### 4.1.2.2 readwrite 226 | 227 | This is a byte-level alternative to slice notation. It has the potential 228 | advantage when reading of using a pre-allocated buffer. Arguments: 229 | 1. `addr` Starting byte address 230 | 2. `buf` A `bytearray` or `bytes` instance containing data to write. In the 231 | read case it must be a (mutable) `bytearray` to hold data read. 232 | 3. `read` If `True`, perform a read otherwise write. The size of the buffer 233 | determines the quantity of data read or written. A `RuntimeError` will be 234 | thrown if the read or write extends beyond the end of the physical space. 235 | 236 | ### 4.1.3 Other methods 237 | 238 | #### sync 239 | 240 | This causes the cached sector to be written to the device. In normal filesystem 241 | use this need not be called. If byte-level writes have been performed it should 242 | be called prior to power down. 243 | 244 | #### The len operator 245 | 246 | The size of the flash array in bytes may be retrieved by issuing `len(flash)` 247 | where `flash` is the `FLASH` instance. 248 | 249 | #### scan 250 | 251 | Args: 252 | 1. `verbose` `bool`. If `True` print information on chips detected. 253 | 2. `size` `int` or `None`. If an `int` is passed a `ValueError` is thrown if 254 | the detected chip size does not match the passed value. 255 | 256 | Activate each chip select in turn checking for a valid device and returns the 257 | size in KiB of one instance of the flash devices detected. A `RuntimeError` 258 | will be raised if any CS pin does not correspond to a valid chip. A 259 | `ValueError` is thrown if the detected chips are not of the same size. 260 | 261 | Other than for debugging there is no need to call `scan()`: it is called by the 262 | constructor. 263 | 264 | #### erase 265 | 266 | Erases the entire array. Beware: this takes many minutes. 267 | 268 | ### 4.1.4 Methods providing the block protocol 269 | 270 | These are provided by the base class. For the protocol definition see 271 | [the pyb documentation](http://docs.micropython.org/en/latest/library/uos.html#uos.AbstractBlockDev) 272 | also [here](http://docs.micropython.org/en/latest/reference/filesystem.html#custom-block-devices). 273 | 274 | These methods exist purely to support the block protocol. They are undocumented: 275 | their use in application code is not recommended. 276 | 277 | `readblocks()` 278 | `writeblocks()` 279 | `ioctl()` 280 | 281 | # 5. Test program flash_test.py 282 | 283 | This assumes a Pyboard 1.x or Pyboard D with two chips wired to SPI(2) as 284 | above with chip selects connected to pins `Y4` and `Y5`. It provides the 285 | following. 286 | 287 | ## 5.1 test() 288 | 289 | This performs a basic test of single and multi-byte access to chip 0. The test 290 | reports how many chips can be accessed. Existing array data will be lost. This 291 | primarily tests the driver: as a hardware test it is not exhaustive. It does 292 | provide a quick verification that all chips can be accessed. 293 | 294 | ## 5.2 full_test(count=10) 295 | 296 | This is a hardware test. Tests the entire array. Creates an array of 256 bytes 297 | of random data and writes it to a random address. After synchronising the cache 298 | with the hardware, reads it back, and checks the outcome. Existing array data 299 | will be lost. The arg determines the number of passes. 300 | 301 | ## 5.3 fstest(format=False) 302 | 303 | If `True` is passed, formats the flash array as a littlefs filesystem deleting 304 | existing contents. In both cases of the arg it mounts the device on `/fl_ext` 305 | lists the contents of the mountpoint. It also prints the outcome of 306 | `uos.statvfs` on the mountpoint. 307 | 308 | ## 5.4 cptest() 309 | 310 | Tests copying the source files to the filesystem. The test will fail if the 311 | filesystem was not formatted. Lists the contents of the mountpoint and prints 312 | the outcome of `uos.statvfs`. 313 | 314 | ## 5.5 File copy 315 | 316 | A rudimentary `cp(source, dest)` function is provided as a generic file copy 317 | routine for setup and debugging purposes at the REPL. The first argument is the 318 | full pathname to the source file. The second may be a full path to the 319 | destination file or a directory specifier which must have a trailing '/'. If an 320 | OSError is thrown (e.g. by the source file not existing or the flash becoming 321 | full) it is up to the caller to handle it. For example (assuming the flash is 322 | mounted on /fl_ext): 323 | 324 | ```python 325 | cp('/flash/main.py','/fl_ext/') 326 | ``` 327 | 328 | See `upysh` in [micropython-lib](https://github.com/micropython/micropython-lib.git) 329 | for other filesystem tools for use at the REPL. 330 | 331 | # 6. Unsupported chips 332 | 333 | Flash chips have fairly standard commands so there is a good chance that 334 | unsupported chips will work so long as they are specified correctly. 335 | 336 | Automatic size detection for unsupported chips is not guaranteed: some chips 337 | produce nonstandard output on the relevant byte. Specifying the `size` 338 | constructor arg is highly recommended. 339 | 340 | It is also best to establish whether it uses 4 or 5 byte commands. This can be 341 | determined from the datasheet. Look up the code for `READ MEMORY`. If it is 342 | `03H` the device uses 4 byte instructions; if `13H` it uses 5-byte instructions. 343 | 344 | Instantiate with `cmd5` set `True` or `False` appropriately. 345 | 346 | If you have success with a new chip please raise an issue with the part no. and 347 | the `cmd5` setting and I will update the docs. 348 | 349 | # 7. Design notes 350 | 351 | This driver buffers one sector (4KiB) in RAM. This is necessary to support 352 | access as a byte array. I believed the buffer would confer an advantage when 353 | running a filesystem, but testing suggests that performance is essentially 354 | the same as an unbuffered driver. This appears to be true for littlefs 2 and 355 | FAT. Testing was done by comparison with 356 | [this unbuffered driver](https://github.com/robert-hh/SPI_Flash). 357 | 358 | Both filesystem drivers seem to be designed on the assumption that they are 359 | communicating with a Flash chip; they always write one sector at a time. In the 360 | case of littlefs, whenever a byte in a sector is changed, it erases and writes 361 | a new sector. It does this without buffering, reading the contents of the old 362 | sector and modifying it on the fly. 363 | 364 | The FAT driver seems to do something similar, but according to its docs it can 365 | briefly create a buffer for two sectors. 366 | 367 | A possible future project would be an unbuffered Flash driver based on the one 368 | referenced above, with the following characteristics: 369 | 1. No byte array access. Filesystem use only. 370 | 2. A stand-alone driver not based on `bdevice.py`. 371 | 3. Support for littlefs2 and FAT. Littlefs2 is preferred over littlefs1 unless 372 | RAM minimisation is paramount. 373 | 4. Support for multi-chip arrays. 374 | 375 | The advantage would be a saving of somewhere around 6KiB of RAM. 376 | -------------------------------------------------------------------------------- /flash/flash_spi.py: -------------------------------------------------------------------------------- 1 | # flash_spi.py MicroPython driver for SPI NOR flash devices. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2019-2020 Peter Hinch 5 | 6 | import time 7 | from micropython import const 8 | from bdevice import FlashDevice 9 | 10 | # Supported instruction set: 11 | # 3 and 4 byte address commands 12 | _READ = const(0) # Index of _CMDSxBA 13 | _PP = const(1) 14 | _SE = const(2) 15 | _CMDS3BA = b"\x03\x02\x20" 16 | _CMDS4BA = b"\x13\x12\x21" 17 | # No address 18 | _WREN = const(6) # Write enable 19 | _RDSR1 = const(5) # Read status register 1 20 | _RDID = const(0x9F) # Read manufacturer ID 21 | _CE = const(0xC7) # Chip erase (takes minutes) 22 | 23 | _SEC_SIZE = const(4096) # Flash sector size 0x1000 24 | 25 | # Logical Flash device comprising one or more physical chips sharing an SPI bus. 26 | class FLASH(FlashDevice): 27 | def __init__( 28 | self, spi, cspins, size=None, verbose=True, sec_size=_SEC_SIZE, block_size=9, cmd5=None 29 | ): 30 | self._spi = spi 31 | self._cspins = cspins 32 | self._ccs = None # Chip select Pin object for current chip 33 | self._bufp = bytearray(6) # instruction + 4 byte address + 1 byte value 34 | self._mvp = memoryview(self._bufp) # cost-free slicing 35 | self._page_size = 256 # Write uses 256 byte pages. 36 | # Defensive code: application should have done the following. 37 | # Pyboard D 3V3 output may just have been switched on. 38 | for cs in cspins: # Deselect all chips 39 | cs(1) 40 | time.sleep_ms(1) # Meet Tpu 300μs 41 | 42 | if size is None: # Get from chip 43 | size = self.scan(verbose, size) # KiB 44 | super().__init__(block_size, len(cspins), size * 1024, sec_size) 45 | 46 | # Select the correct command set 47 | if (cmd5 is None and size <= 4096) or (cmd5 == False): 48 | self._cmds = _CMDS3BA 49 | self._cmdlen = 4 50 | else: 51 | self._cmds = _CMDS4BA 52 | self._cmdlen = 5 53 | 54 | self.initialise() # Initially cache sector 0 55 | 56 | # **** API SPECIAL METHODS **** 57 | # Scan: return chip size in KiB as read from ID. 58 | def scan(self, verbose, size): 59 | mvp = self._mvp 60 | for n, cs in enumerate(self._cspins): 61 | mvp[:] = b"\0\0\0\0\0\0" 62 | mvp[0] = _RDID 63 | cs(0) 64 | self._spi.write_readinto(mvp[:4], mvp[:4]) 65 | cs(1) 66 | scansize = 1 << (mvp[3] - 10) 67 | if size is None: 68 | size = scansize # Save size of 1st chip 69 | if size != scansize: # Mismatch passed size or 1st chip. 70 | raise ValueError(f"Flash size mismatch: expected {size}KiB, found {scansize}KiB") 71 | if not 0x10 < mvp[3] < 0x22: 72 | raise ValueError(f"Invalid chip size {size}KiB. Specify size arg.") 73 | 74 | if verbose: 75 | s = "{} chips detected. Total flash size {}MiB." 76 | n += 1 77 | print(s.format(n, (n * size) // 1024)) 78 | return size 79 | 80 | # Chip erase. Can take minutes. 81 | def erase(self): 82 | mvp = self._mvp 83 | for cs in self._cspins: # For each chip 84 | mvp[0] = _WREN 85 | cs(0) 86 | self._spi.write(mvp[:1]) # Enable write 87 | cs(1) 88 | mvp[0] = _CE 89 | cs(0) 90 | self._spi.write(mvp[:1]) # Start erase 91 | cs(1) 92 | self._wait_rdy() # Wait for erase to complete 93 | 94 | # **** INTERFACE FOR BASE CLASS **** 95 | # Write cache to a sector starting at byte address addr 96 | def flush(self, cache, addr): # cache is memoryview into buffer 97 | self._sector_erase(addr) 98 | mvp = self._mvp 99 | nbytes = self.sec_size 100 | ps = self._page_size 101 | start = 0 # Current offset into cache buffer 102 | while nbytes > 0: 103 | # write one page at a time 104 | self._getaddr(addr, 1) 105 | cs = self._ccs # Current chip select from _getaddr 106 | mvp[0] = _WREN 107 | cs(0) 108 | self._spi.write(mvp[:1]) # Enable write 109 | cs(1) 110 | mvp[0] = self._cmds[_PP] 111 | cs(0) 112 | self._spi.write(mvp[: self._cmdlen]) # Start write 113 | self._spi.write(cache[start : start + ps]) 114 | cs(1) 115 | self._wait_rdy() # Wait for write to complete 116 | nbytes -= ps 117 | start += ps 118 | addr += ps 119 | 120 | # Read from chip into a memoryview. Address range guaranteed not to be cached. 121 | def rdchip(self, addr, mvb): 122 | nbytes = len(mvb) 123 | mvp = self._mvp 124 | start = 0 # Offset into buf. 125 | while nbytes > 0: 126 | npage = self._getaddr(addr, nbytes) # No. of bytes in current chip 127 | cs = self._ccs 128 | mvp[0] = self._cmds[_READ] 129 | cs(0) 130 | self._spi.write(mvp[: self._cmdlen]) 131 | self._spi.readinto(mvb[start : start + npage]) 132 | cs(1) 133 | nbytes -= npage 134 | start += npage 135 | addr += npage 136 | 137 | # Read or write multiple bytes at an arbitrary address. 138 | # **** Also part of API **** 139 | def readwrite(self, addr, buf, read): 140 | mvb = memoryview(buf) 141 | self.read(addr, mvb) if read else self.write(addr, mvb) 142 | return buf 143 | 144 | # **** INTERNAL METHODS **** 145 | def _wait_rdy(self): # After a write, wait for device to become ready 146 | mvp = self._mvp 147 | cs = self._ccs # Chip is already current 148 | while True: # TODO read status register 2, raise OSError on nonzero. 149 | mvp[0] = _RDSR1 150 | cs(0) 151 | self._spi.write_readinto(mvp[:2], mvp[:2]) 152 | cs(1) 153 | if not (mvp[1] & 1): 154 | break 155 | time.sleep_ms(1) 156 | 157 | # Given an address, set current chip select and address buffer. 158 | # Return the number of bytes that can be processed in the current chip. 159 | def _getaddr(self, addr, nbytes): 160 | if addr >= self._a_bytes: 161 | raise RuntimeError("Flash Address is out of range") 162 | ca, la = divmod(addr, self._c_bytes) # ca == chip no, la == offset into chip 163 | self._ccs = self._cspins[ca] # Current chip select 164 | cmdlen = self._cmdlen 165 | mvp = self._mvp[:cmdlen] 166 | if cmdlen > 3: 167 | mvp[-4] = la >> 24 168 | mvp[-3] = la >> 16 & 0xFF 169 | mvp[-2] = (la >> 8) & 0xFF 170 | mvp[-1] = la & 0xFF 171 | pe = (addr & -self._c_bytes) + self._c_bytes # Byte 0 of next chip 172 | return min(nbytes, pe - la) 173 | 174 | # Erase sector. Address is start byte address of sector. Optimisation: skip 175 | # if sector is already erased. 176 | def _sector_erase(self, addr): 177 | if not self.is_empty(addr): 178 | self._getaddr(addr, 1) 179 | cs = self._ccs # Current chip select from _getaddr 180 | mvp = self._mvp 181 | mvp[0] = _WREN 182 | cs(0) 183 | self._spi.write(mvp[:1]) # Enable write 184 | cs(1) 185 | mvp[0] = self._cmds[_SE] 186 | cs(0) 187 | self._spi.write(mvp[: self._cmdlen]) # Start erase 188 | cs(1) 189 | self._wait_rdy() # Wait for erase to complete 190 | -------------------------------------------------------------------------------- /flash/flash_test.py: -------------------------------------------------------------------------------- 1 | # flash_test.py MicroPython test program for Cypress SPI Flash devices. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2019 Peter Hinch 5 | 6 | import uos 7 | import time 8 | from machine import SPI, Pin 9 | from flash_spi import FLASH 10 | 11 | # **** ADAPT THIS FUNCTION **** 12 | 13 | # Return an EEPROM array. Adapt for platforms other than Pyboard. 14 | # May want to set chip size and baudrate. 15 | def get_device(): 16 | if uos.uname().machine.split(" ")[0][:4] == "PYBD": 17 | Pin.board.EN_3V3.value(1) 18 | time.sleep(0.1) 19 | # Adjust to suit number of chips and their wiring. 20 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 21 | flash = FLASH(SPI(2, baudrate=20_000_000), cspins) 22 | print("Instantiated Flash") 23 | return flash 24 | 25 | 26 | # **** END OF USER-ADAPTED CODE **** 27 | 28 | # Dumb file copy utility to help with managing EEPROM contents at the REPL. 29 | def cp(source, dest): 30 | if dest.endswith("/"): # minimal way to allow 31 | dest = "".join((dest, source.split("/")[-1])) # cp /sd/file /fl_ext/ 32 | with open(source, "rb") as infile: # Caller should handle any OSError 33 | with open(dest, "wb") as outfile: # e.g file not found 34 | while True: 35 | buf = infile.read(100) 36 | outfile.write(buf) 37 | if len(buf) < 100: 38 | break 39 | 40 | 41 | # ***** TEST OF DRIVER ***** 42 | def _testblock(eep, bs): 43 | d0 = b"this >" 44 | d1 = b"xxxxxxxxxxxxx": 58 | return "Block test fail 2:" + str(list(res)) 59 | start = bs 60 | end = bs + len(d1) 61 | eep[start:end] = d1 62 | start = bs - len(d0) 63 | end = start + len(d2) 64 | res = eep[start:end] 65 | if res != d2: 66 | return "Block test fail 3:" + str(list(res)) 67 | 68 | 69 | def test(): 70 | eep = get_device() 71 | sa = 1000 72 | for v in range(256): 73 | eep[sa + v] = v 74 | for v in range(256): 75 | if eep[sa + v] != v: 76 | print( 77 | "Fail at address {} data {} should be {}".format(sa + v, eep[sa + v], v) 78 | ) 79 | break 80 | else: 81 | print("Test of byte addressing passed") 82 | data = uos.urandom(30) 83 | sa = 2000 84 | eep[sa : sa + 30] = data 85 | if eep[sa : sa + 30] == data: 86 | print("Test of slice readback passed") 87 | 88 | block = 256 89 | res = _testblock(eep, block) 90 | if res is None: 91 | print("Test block boundary {} passed".format(block)) 92 | else: 93 | print("Test block boundary {} fail".format(block)) 94 | print(res) 95 | block = eep._c_bytes 96 | if eep._a_bytes > block: 97 | res = _testblock(eep, block) 98 | if res is None: 99 | print("Test chip boundary {} passed".format(block)) 100 | else: 101 | print("Test chip boundary {} fail".format(block)) 102 | print(res) 103 | else: 104 | print("Test chip boundary skipped: only one chip!") 105 | 106 | 107 | # ***** TEST OF FILESYSTEM MOUNT ***** 108 | def fstest(format=False): 109 | eep = get_device() 110 | # ***** CODE FOR LITTLEFS ***** 111 | if format: 112 | uos.VfsLfs2.mkfs(eep) 113 | try: 114 | uos.mount(eep, "/fl_ext") 115 | except OSError: # Already mounted 116 | pass 117 | print('Contents of "/": {}'.format(uos.listdir("/"))) 118 | print('Contents of "/fl_ext": {}'.format(uos.listdir("/fl_ext"))) 119 | print(uos.statvfs("/fl_ext")) 120 | 121 | 122 | def cptest(): 123 | eep = get_device() 124 | if "fl_ext" in uos.listdir("/"): 125 | print("Device already mounted.") 126 | else: 127 | try: 128 | uos.mount(eep, "/fl_ext") 129 | except OSError: 130 | print("Fail mounting device. Have you formatted it?") 131 | return 132 | print("Mounted device.") 133 | cp("flash_test.py", "/fl_ext/") 134 | cp("flash_spi.py", "/fl_ext/") 135 | print('Contents of "/fl_ext": {}'.format(uos.listdir("/fl_ext"))) 136 | print(uos.statvfs("/fl_ext")) 137 | 138 | 139 | # ***** TEST OF HARDWARE ***** 140 | def full_test(count=10): 141 | flash = get_device() 142 | for n in range(count): 143 | data = uos.urandom(256) 144 | while True: 145 | sa = int.from_bytes(uos.urandom(4), "little") & 0x3FFFFFFF 146 | if sa < (flash._a_bytes - 256): 147 | break 148 | flash[sa : sa + 256] = data 149 | flash.sync() 150 | got = flash[sa : sa + 256] 151 | if got == data: 152 | print("Pass {} address {:08x} passed".format(n, sa)) 153 | if sa & 0xFFF > (4096 - 253): 154 | print("cross boundary") 155 | else: 156 | print("Pass {} address {:08x} readback failed.".format(n, sa)) 157 | sa1 = sa & 0xFFF 158 | print("Bounds {} to {}".format(sa1, sa1 + 256)) 159 | # flash.sync() 160 | got1 = flash[sa : sa + 256] 161 | if got1 == data: 162 | print("second attempt OK") 163 | else: 164 | print("second attempt fail", got == got1) 165 | for n, g in enumerate(got): 166 | if g != data[n]: 167 | print("{} {:2x} {:2x} {:2x}".format(n, data[n], g, got1[n])) 168 | break 169 | 170 | 171 | helpstr = """Available tests: 172 | test() Basic hardware test. 173 | full_test(count=10) Full hardware test, count is no. of passes. 174 | fstest(format=False) Check or create littlefs filesystem. 175 | cptest() Copy 2 files to filesystem. 176 | cp() Primitive copy routine. See docs. 177 | """ 178 | print(helpstr) 179 | -------------------------------------------------------------------------------- /flash/littlefs_test.py: -------------------------------------------------------------------------------- 1 | # littlefs_test.py Extended filesystem test of flash devices 2 | # Create multiple binary files of varying length and verify that they can be 3 | # read back correctly. Rewrite files with new lengths then check that all files 4 | # are OK. 5 | 6 | import uos 7 | from machine import SPI, Pin 8 | from flash_spi import FLASH 9 | from flash_test import get_device 10 | 11 | directory = "/fl_ext" 12 | a = bytearray(range(256)) 13 | b = bytearray(256) 14 | files = {} # n:length 15 | errors = 0 16 | 17 | 18 | def fname(n): 19 | return "{}/{:05d}".format(directory, n + 1) # Names start 00001 20 | 21 | 22 | def fcreate(n): # Create a binary file of random length 23 | length = int.from_bytes(uos.urandom(2), "little") + 1 # 1-65536 bytes 24 | linit = length 25 | with open(fname(n), "wb") as f: 26 | while length: 27 | nw = min(length, 256) 28 | f.write(a[:nw]) 29 | length -= nw 30 | files[n] = length 31 | return linit 32 | 33 | 34 | def fcheck(n): 35 | length = files[n] 36 | with open(fname(n), "rb") as f: 37 | while length: 38 | nr = f.readinto(b) 39 | if not nr: 40 | return False 41 | if a[:nr] != b[:nr]: 42 | return False 43 | length -= nr 44 | return True 45 | 46 | 47 | def check_all(): 48 | global errors 49 | for n in files: 50 | if fcheck(n): 51 | print("File {:d} OK".format(n)) 52 | else: 53 | print("Error in file", n) 54 | errors += 1 55 | print("Total errors:", errors) 56 | 57 | 58 | def remove_all(): 59 | for n in files: 60 | uos.remove(fname(n)) 61 | 62 | 63 | def main(): 64 | eep = get_device() 65 | try: 66 | uos.mount(eep, directory) 67 | except OSError: # Already mounted 68 | pass 69 | for n in range(128): 70 | length = fcreate(n) 71 | print("Created", n, length) 72 | print("Created files", files) 73 | check_all() 74 | for _ in range(100): 75 | for x in range(5): # Rewrite 5 files with new lengths 76 | n = int.from_bytes(uos.urandom(1), "little") & 0x7F 77 | length = fcreate(n) 78 | print("Rewrote", n, length) 79 | check_all() 80 | remove_all() 81 | 82 | 83 | print("main() to run littlefs test. Filesystem must exist.") 84 | -------------------------------------------------------------------------------- /flash/wemos_flash.py: -------------------------------------------------------------------------------- 1 | # wemos_flash.py Test flash chips with ESP8266 host 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | import uos 7 | from machine import SPI, Pin 8 | from flash_spi import FLASH 9 | 10 | cspins = (Pin(5, Pin.OUT, value=1), Pin(14, Pin.OUT, value=1)) 11 | 12 | spi = SPI(-1, baudrate=20_000_000, sck=Pin(4), miso=Pin(0), mosi=Pin(2)) 13 | 14 | 15 | def get_flash(): 16 | flash = FLASH(spi, cspins) 17 | print("Instantiated Flash") 18 | return flash 19 | 20 | 21 | directory = "/fl_ext" 22 | a = bytearray(range(256)) # Data to write 23 | b = bytearray(256) # Data to read back 24 | files = {} # n:length 25 | errors = 0 26 | 27 | 28 | def fname(n): 29 | return "{}/{:05d}".format(directory, n + 1) # Names start 00001 30 | 31 | 32 | def fcreate(n): # Create a binary file of random length 33 | length = int.from_bytes(uos.urandom(2), "little") + 1 # 1-65536 bytes 34 | linit = length 35 | with open(fname(n), "wb") as f: 36 | while length: 37 | nw = min(length, 256) 38 | f.write(a[:nw]) 39 | length -= nw 40 | files[n] = length 41 | return linit 42 | 43 | 44 | def fcheck(n): 45 | length = files[n] 46 | with open(fname(n), "rb") as f: 47 | while length: 48 | nr = f.readinto(b) 49 | if not nr: 50 | return False 51 | if a[:nr] != b[:nr]: 52 | return False 53 | length -= nr 54 | return True 55 | 56 | 57 | def check_all(): 58 | global errors 59 | for n in files: 60 | if fcheck(n): 61 | print("File {:d} OK".format(n)) 62 | else: 63 | print("Error in file", n) 64 | errors += 1 65 | print("Total errors:", errors) 66 | 67 | 68 | def remove_all(): 69 | for n in files: 70 | uos.remove(fname(n)) 71 | 72 | 73 | def flash_test(format=False): 74 | eep = get_flash() 75 | if format: 76 | uos.VfsLfs2.mkfs(eep) 77 | try: 78 | uos.mount(eep, "/fl_ext") 79 | except OSError: # Already mounted 80 | pass 81 | for n in range(128): 82 | length = fcreate(n) 83 | print("Created", n, length) 84 | print("Created files", files) 85 | check_all() 86 | for _ in range(100): 87 | for x in range(5): # Rewrite 5 files with new lengths 88 | n = int.from_bytes(uos.urandom(1), "little") & 0x7F 89 | length = fcreate(n) 90 | print("Rewrote", n, length) 91 | check_all() 92 | remove_all() 93 | 94 | 95 | msg = """Run wemos_flash.flash_test(True) to format new array, otherwise 96 | wemos_flash.flash_test() 97 | Runs prolonged test of filesystem.""" 98 | print(msg) 99 | -------------------------------------------------------------------------------- /fram/FRAM.md: -------------------------------------------------------------------------------- 1 | # 1. A MicroPython FRAM driver 2 | 3 | A driver to enable the Pyboard to access the Ferroelectric RAM (FRAM) board from 4 | [Adafruit](http://www.adafruit.com/product/1895). FRAM is a technology offering 5 | nonvolatile memory with extremely long endurance and fast access, avoiding the 6 | limitations of Flash memory. Its endurance is specified as 10**12 writes, 7 | contrasted with 10,000 which is the quoted endurance of the Pyboard's onboard 8 | Flash memory. In data logging applications the latter can be exceeded relatively 9 | rapidly. Flash writes can be slow because of the need for a sector erase: this 10 | is not a fast process. FRAM is byte addressable and is not subject to this 11 | limitation. The downside is limited capacity. Compared to a Micro SD card fitted 12 | to the Pyboard it offers lower power consumption and longer endurance. 13 | 14 | From one to eight boards may be used to construct a nonvolatile memory module 15 | with size ranging from 32KiB to 256KiB. The driver allows the memory either to 16 | be mounted in the Pyboard filesystem as a disk device or to be addressed as an 17 | array of bytes. 18 | 19 | For users interested in the technology [this](https://www.mouser.com/pdfDOCS/cypress-fram-whitepaper.pdf) 20 | is worth reading. Clue: the FRAM cell contains no iron. 21 | 22 | ##### [Main readme](../README.md) 23 | 24 | ## 1.1 Changes compared to the old FRAM driver 25 | 26 | API now matches other devices with support for slice syntax. Reduced RAM 27 | allocation by virtue of `memorview` instances and pre-allocated buffers. Now 28 | supports littlefs or FAT filesystems. 29 | 30 | # 2. Connections 31 | 32 | To wire up a single FRAM module, connect to the Pyboard as below (nc indicates 33 | no connection). 34 | 35 | | FRAM | L | R | 36 | |:-------:|:---:|:---:| 37 | | Vcc | 3V3 | 3V3 | 38 | | Gnd | GND | GND | 39 | | WP | nc | nc | 40 | | SCL | X9 | Y9 | 41 | | SDA | X10 | Y10 | 42 | | A2 | nc | nc | 43 | | A1 | nc | nc | 44 | | A0 | nc | nc | 45 | 46 | For multiple modules the address lines A0, A1 and A2 of each module need to be 47 | wired to 3V3 in such a way as to give each device a unique address. These must 48 | start at zero and be contiguous. Pins are internally pulled down, pins marked 49 | `nc` may be left unconnected or linked to Gnd. 50 | | Chip no. | A2 | A1 | A0 | 51 | |:--------:|:---:|:---:|:---:| 52 | | 0 | nc | nc | nc | 53 | | 1 | nc | nc | 3V3 | 54 | | 2 | nc | 3V3 | nc | 55 | | 3 | nc | 3V3 | 3V3 | 56 | | 4 | 3V3 | nc | nc | 57 | | 5 | 3V3 | nc | 3V3 | 58 | | 6 | 3V3 | 3V3 | nc | 59 | | 7 | 3V3 | 3V3 | Gnd | 60 | 61 | Multiple modules should have 3V3, Gnd, SCL and SDA lines wired in parallel. 62 | 63 | The I2C interface requires pullups: these are provided on the Adafruit board. 64 | 65 | If you use a Pyboard D and power the FRAMs from the 3V3 output you will need 66 | to enable the voltage rail by issuing: 67 | ```python 68 | machine.Pin.board.EN_3V3.value(1) 69 | time.sleep(0.1) # Allow decouplers to charge 70 | ``` 71 | Other platforms may vary. 72 | 73 | # 3. Files 74 | 75 | 1. `fram_i2c.py` Device driver. 76 | 2. `bdevice.py` (In root directory) Base class for the device driver. 77 | 3. `fram_test.py` Test programs for above. 78 | 79 | Installation: copy files 1 and 2 (optionally 3) to the target filesystem. 80 | 81 | # 4. The device driver 82 | 83 | The driver supports mounting the FRAM chips as a filesystem. Initially the 84 | device will be unformatted so it is necessary to issue code along these lines 85 | to format the device. Code assumes one or more devices and also assumes the 86 | littlefs filesystem: 87 | 88 | ```python 89 | import os 90 | from machine import I2C 91 | from fram_i2c import FRAM 92 | fram = FRAM(I2C(2)) 93 | # Format the filesystem 94 | os.VfsLfs2.mkfs(fram) # Omit this to mount an existing filesystem 95 | os.mount(fram,'/fram') 96 | ``` 97 | The above will reformat a drive with an existing filesystem: to mount an 98 | existing filesystem simply omit the commented line. 99 | 100 | Note that, at the outset, you need to decide whether to use the array as a 101 | mounted filesystem or as a byte array. The filesystem is relatively small but 102 | has high integrity owing to the hardware longevity. Typical use-cases involve 103 | files which are frequently updated. These include files used for storing Python 104 | objects serialised using pickle/ujson or files holding a btree database. 105 | 106 | The I2C bus must be instantiated using the `machine` module. 107 | 108 | ## 4.1 The FRAM class 109 | 110 | An `FRAM` instance represents a logical FRAM: this may consist of multiple 111 | physical devices on a common I2C bus. 112 | 113 | ### 4.1.1 Constructor 114 | 115 | This scans the I2C bus and checks if one or more correctly addressed chips are 116 | detected. Each chip is checked for correct ID data. A `RuntimeError` will occur 117 | in case of error, e.g. bad ID, no device detected or device address lines not 118 | wired as described in [Connections](./README.md#2-connections). If all is OK an 119 | FRAM instance is created. 120 | 121 | Arguments: 122 | 1. `i2c` Mandatory. An initialised master mode I2C bus created by `machine`. 123 | 2. `verbose=True` If `True`, the constructor issues information on the FRAM 124 | devices it has detected. 125 | 3. `block_size=9` The block size reported to the filesystem. The size in bytes 126 | is `2**block_size` so is 512 bytes by default. 127 | 128 | ### 4.1.2 Methods providing byte level access 129 | 130 | It is possible to read and write individual bytes or arrays of arbitrary size. 131 | Arrays will be somewhat faster owing to more efficient bus utilisation. 132 | 133 | #### 4.1.2.1 `__getitem__` and `__setitem__` 134 | 135 | These provides single byte or multi-byte access using slice notation. Example 136 | of single byte access: 137 | 138 | ```python 139 | from machine import I2C 140 | from fram_i2c import FRAM 141 | fram = FRAM(I2C(2)) 142 | fram[2000] = 42 143 | print(fram[2000]) # Return an integer 144 | ``` 145 | It is also possible to use slice notation to read or write multiple bytes. If 146 | writing, the size of the slice must match the length of the buffer: 147 | ```python 148 | from machine import I2C 149 | from fram_i2c import FRAM 150 | fram = FRAM(I2C(2)) 151 | fram[2000:2002] = bytearray((42, 43)) 152 | print(fram[2000:2002]) # Returns a bytearray 153 | ``` 154 | Three argument slices are not supported: a third arg (other than 1) will cause 155 | an exception. One argument slices (`fram[:5]` or `fram[32760:]`) and negative 156 | args are supported. 157 | 158 | #### 4.1.2.2 readwrite 159 | 160 | This is a byte-level alternative to slice notation. It has the potential 161 | advantage when reading of using a pre-allocated buffer. Arguments: 162 | 1. `addr` Starting byte address 163 | 2. `buf` A `bytearray` or `bytes` instance containing data to write. In the 164 | read case it must be a (mutable) `bytearray` to hold data read. 165 | 3. `read` If `True`, perform a read otherwise write. The size of the buffer 166 | determines the quantity of data read or written. A `RuntimeError` will be 167 | thrown if the read or write extends beyond the end of the physical space. 168 | 169 | ### 4.1.3 Other methods 170 | 171 | #### The len() operator 172 | 173 | The size of the FRAM array in bytes may be retrieved by issuing `len(fram)` 174 | where `fram` is the `FRAM` instance. 175 | 176 | #### scan 177 | 178 | Scans the I2C bus and returns the number of FRAM devices detected. 179 | 180 | Other than for debugging there is no need to call `scan()`: the constructor 181 | will throw a `RuntimeError` if it fails to communicate with and correctly 182 | identify the chip(s). 183 | 184 | ### 4.1.4 Methods providing the block protocol 185 | 186 | These are provided by the base class. For the protocol definition see 187 | [the pyb documentation](http://docs.micropython.org/en/latest/library/uos.html#uos.AbstractBlockDev) 188 | also [here](http://docs.micropython.org/en/latest/reference/filesystem.html#custom-block-devices). 189 | 190 | These methods exist purely to support the block protocol. They are undocumented: 191 | their use in application code is not recommended. 192 | 193 | `readblocks()` 194 | `writeblocks()` 195 | `ioctl()` 196 | 197 | # 5. Test program fram_test.py 198 | 199 | This assumes a Pyboard 1.x or Pyboard D with FRAM(s) wired as above. It 200 | provides the following. 201 | 202 | ## 5.1 test() 203 | 204 | This performs a basic test of single and multi-byte access to chip 0. The test 205 | reports how many chips can be accessed. Existing array data will be lost. This 206 | primarily tests the driver: as a hardware test it is not exhaustive. 207 | 208 | ## 5.2 full_test() 209 | 210 | This is a hardware test. Tests the entire array. Fills each 128 byte page with 211 | random data, reads it back, and checks the outcome. Existing array data will be 212 | lost. 213 | 214 | ## 5.3 fstest(format=False) 215 | 216 | If `True` is passed, formats the FRAM array as a FAT filesystem and mounts 217 | the device on `/fram`. If no arg is passed it mounts the array and lists the 218 | contents. It also prints the outcome of `uos.statvfs` on the array. 219 | 220 | ## 5.4 cptest() 221 | 222 | Tests copying the source files to the filesystem. The test will fail if the 223 | filesystem was not formatted. Lists the contents of the mountpoint and prints 224 | the outcome of `uos.statvfs`. 225 | 226 | ## 5.5 File copy 227 | 228 | A rudimentary `cp(source, dest)` function is provided as a generic file copy 229 | routine for setup and debugging purposes at the REPL. The first argument is the 230 | full pathname to the source file. The second may be a full path to the 231 | destination file or a directory specifier which must have a trailing '/'. If an 232 | OSError is thrown (e.g. by the source file not existing or the FRAM becoming 233 | full) it is up to the caller to handle it. For example (assuming the FRAM is 234 | mounted on /fram): 235 | 236 | ```python 237 | cp('/flash/main.py','/fram/') 238 | ``` 239 | 240 | See `upysh` in [micropython-lib](https://github.com/micropython/micropython-lib.git) 241 | for other filesystem tools for use at the REPL. 242 | 243 | # 6. References 244 | 245 | [Adafruit board](http://www.adafruit.com/product/1895) 246 | [Chip datasheet](https://cdn-learn.adafruit.com/assets/assets/000/043/904/original/MB85RC256V-DS501-00017-3v0-E.pdf?1500009796) 247 | [Technology](https://www.mouser.com/pdfDOCS/cypress-fram-whitepaper.pdf) 248 | -------------------------------------------------------------------------------- /fram/FRAM_SPI.md: -------------------------------------------------------------------------------- 1 | # 1. A MicroPython SPI FRAM driver 2 | 3 | A driver to enable MicroPython hosts to access Ferroelectric RAM (FRAM) boards 4 | from Adafruit, namely [the 256KiB board](https://www.adafruit.com/product/4718) 5 | and [the 512KiB board](https://www.adafruit.com/product/4719). FRAM is a 6 | technology offering nonvolatile memory with extremely long endurance and fast 7 | access, avoiding the limitations of Flash memory. Its endurance is specified as 8 | 10**13 writes, contrasted with 10,000 which is the quoted endurance of the 9 | Pyboard's onboard Flash memory. In data logging applications the latter can be 10 | exceeded relatively rapidly. Flash writes can be slow because of the need for a 11 | sector erase: this is not a fast process. FRAM is byte addressable and is not 12 | subject to this limitation. Compared to a Micro SD card fitted to the Pyboard 13 | it offers lower power consumption and longer endurance, albeit at a smaller 14 | capacity. 15 | 16 | An arbitrary number of boards may be used to construct a nonvolatile memory 17 | array with size from 256KiB upwards. The driver allows the memory either to be 18 | mounted in the host filesystem as a disk device or to be addressed as an array 19 | of bytes. 20 | 21 | For users interested in the technology [this](https://www.mouser.com/pdfDOCS/cypress-fram-whitepaper.pdf) 22 | is worth reading. Clue: the FRAM cell contains no iron. 23 | 24 | ##### [Main readme](../README.md) 25 | 26 | # 2. Connections 27 | 28 | Any SPI interface may be used. The table below assumes a Pyboard running SPI(2) 29 | as per the test program. To wire up a single FRAM BOARD, connect to a Pyboard 30 | as below (n/c indicates no connection): 31 | 32 | | FRAM Signal | PB | Signal | 33 | |:-----------:|:---:|:------:| 34 | | Vin | 3V3 | 3V3 | 35 | | 3V3 | n/c | n/c | 36 | | Gnd | Gnd | Gnd | 37 | | SCK | Y6 | SCK | 38 | | MISO | Y7 | MISO | 39 | | MOSI | Y8 | MOSI | 40 | | CS | Y5 | SS/ | 41 | | WP/ | n/c | n/c | 42 | | HOLD/ | n/c | n/c | 43 | 44 | For multiple boards a separate CS pin must be assigned to each one: each pin 45 | must be wired to a single board's CS line. Multiple boards should have Vin, Gnd, 46 | SCK, MOSI and MISO lines wired in parallel. 47 | 48 | If you use a Pyboard D and power the devices from the 3V3 output you will need 49 | to enable the voltage rail by issuing: 50 | ```python 51 | machine.Pin.board.EN_3V3.value(1) 52 | time.sleep(0.1) # Allow decouplers to charge 53 | ``` 54 | Other platforms may vary. 55 | 56 | At the time of writing schematics for the Adafruit boards were unavailable but 57 | measurement indicated that CS, WP/ and HOLD/ are pulled up with 10KΩ. It is 58 | therefore safe to leave WP/ and HOLD/ unconnected, and CS will behave properly 59 | at power-up. 60 | 61 | # 3. Files 62 | 63 | 1. `fram_spi.py` Device driver. 64 | 2. `bdevice.py` (In root directory) Base class for the device driver. 65 | 3. `fram_spi_test.py` Test programs for above. Assumes two 512KiB boards with 66 | CS connected to pins Y4 and Y5 respectively. Adapt for other configurations. 67 | 4. `fram_fs_test.py` A torture test for littlefs. 68 | 69 | Installation: copy files 1 and 2 to the target filesystem. `fram_spi_test.py` 70 | has a function `test()` which provides quick verification of hardware, but 71 | `cspins` and `get_fram` at the start of the file may need adaptation to your 72 | hardware. 73 | 74 | # 4. The device driver 75 | 76 | The driver supports mounting the FRAM chips as a filesystem. Initially the 77 | device will be unformatted so it is necessary to issue code along these lines 78 | to format the device. Code assumes one or more devices and also assumes the 79 | littlefs filesystem: 80 | 81 | ```python 82 | import os 83 | from machine import SPI, Pin 84 | from fram_spi import FRAM 85 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1),) 86 | fram = FRAM(SPI(2, baudrate=25_000_000), cspins) 87 | # Format the filesystem 88 | os.VfsLfs2.mkfs(fram) # Omit this to mount an existing filesystem 89 | os.mount(fram,'/fram') 90 | ``` 91 | The above will reformat a drive with an existing filesystem: to mount an 92 | existing filesystem simply omit the commented line. 93 | 94 | Note that, at the outset, you need to decide whether to use the array as a 95 | mounted filesystem or as a byte array. The filesystem is relatively small but 96 | has high integrity owing to the hardware longevity. Typical use-cases involve 97 | files which are frequently updated. These include files used for storing Python 98 | objects serialised using pickle/ujson or files holding a btree database. 99 | 100 | The SPI bus must be instantiated using the `machine` module. The chips are 101 | specified to a baudrate of 40MHz. I tested on a Pyboard D, specifying 25MHz - 102 | this produced an actual baudrate of 18MHz. 103 | 104 | ## 4.1 The FRAM class 105 | 106 | An `FRAM` instance represents a logical FRAM: this may consist of multiple 107 | physical devices on a common SPI bus. 108 | 109 | ### 4.1.1 Constructor 110 | 111 | This checks each CS line for an attached board of the correct type and of the 112 | specified size. A `RuntimeError` will occur in case of error, e.g. bad ID, no 113 | device detected or size not matching that specified to the constructor. If all 114 | is OK an FRAM instance is created. 115 | 116 | Arguments: 117 | 1. `spi` Mandatory. An initialised SPIbus created by `machine`. 118 | 2. `cspins` A list or tuple of `Pin` instances. Each `Pin` must be initialised 119 | as an output (`Pin.OUT`) and with `value=1` and be created by `machine`. 120 | 3. `size=512` Chip size in KiB. 121 | 4. `verbose=True` If `True`, the constructor issues information on the FRAM 122 | devices it has detected. 123 | 5. `block_size=9` The block size reported to the filesystem. The size in bytes 124 | is `2**block_size` so is 512 bytes by default. 125 | 126 | ### 4.1.2 Methods providing byte level access 127 | 128 | It is possible to read and write individual bytes or arrays of arbitrary size. 129 | Arrays will be somewhat faster owing to more efficient bus utilisation. 130 | 131 | #### 4.1.2.1 `__getitem__` and `__setitem__` 132 | 133 | These provides single byte or multi-byte access using slice notation. Example 134 | of single byte access: 135 | 136 | ```python 137 | from machine import SPI, Pin 138 | from fram_spi import FRAM 139 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1),) 140 | fram = FRAM(SPI(2), cspins) 141 | fram[2000] = 42 142 | print(fram[2000]) # Return an integer 143 | ``` 144 | It is also possible to use slice notation to read or write multiple bytes. If 145 | writing, the size of the slice must match the length of the buffer: 146 | ```python 147 | from machine import SPI, Pin 148 | from fram_spi import FRAM 149 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1),) 150 | fram = FRAM(SPI(2), cspins) 151 | fram[2000:2002] = bytearray((42, 43)) 152 | print(fram[2000:2002]) # Returns a bytearray 153 | ``` 154 | Three argument slices are not supported: a third arg (other than 1) will cause 155 | an exception. One argument slices (`fram[:5]` or `fram[32760:]`) and negative 156 | args are supported. 157 | 158 | #### 4.1.2.2 readwrite 159 | 160 | This is a byte-level alternative to slice notation. It has the potential 161 | advantage when reading of using a pre-allocated buffer. Arguments: 162 | 1. `addr` Starting byte address 163 | 2. `buf` A `bytearray` or `bytes` instance containing data to write. In the 164 | read case it must be a (mutable) `bytearray` to hold data read. 165 | 3. `read` If `True`, perform a read otherwise write. The size of the buffer 166 | determines the quantity of data read or written. A `RuntimeError` will be 167 | thrown if the read or write extends beyond the end of the physical space. 168 | 169 | ### 4.1.3 Other methods 170 | 171 | #### The len() operator 172 | 173 | The size of the FRAM array in bytes may be retrieved by issuing `len(fram)` 174 | where `fram` is the `FRAM` instance. 175 | 176 | ### 4.1.4 Methods providing the block protocol 177 | 178 | These are provided by the base class. For the protocol definition see 179 | [the pyb documentation](http://docs.micropython.org/en/latest/library/uos.html#uos.AbstractBlockDev) 180 | also [here](http://docs.micropython.org/en/latest/reference/filesystem.html#custom-block-devices). 181 | 182 | These methods exist purely to support the block protocol. They are undocumented: 183 | their use in application code is not recommended. 184 | 185 | `readblocks()` 186 | `writeblocks()` 187 | `ioctl()` 188 | 189 | # 5. Test program fram_spi_test.py 190 | 191 | This assumes a Pyboard 1.x or Pyboard D with FRAM(s) wired as above. It 192 | provides the following. 193 | 194 | ## 5.1 test() 195 | 196 | This performs a basic test of single and multi-byte access to chip 0. The test 197 | reports how many chips can be accessed. Existing array data will be lost. This 198 | primarily tests the driver: as a hardware test it is not exhaustive. 199 | 200 | ## 5.2 full_test() 201 | 202 | This is a hardware test. Tests the entire array. Fills each 128 byte page with 203 | random data, reads it back, and checks the outcome. Existing array data will be 204 | lost. 205 | 206 | ## 5.3 fstest(format=False) 207 | 208 | If `True` is passed, formats the FRAM array as a FAT filesystem and mounts 209 | the device on `/fram`. If no arg is passed it mounts the array and lists the 210 | contents. It also prints the outcome of `uos.statvfs` on the array. 211 | 212 | ## 5.4 cptest() 213 | 214 | Tests copying the source files to the filesystem. The test will fail if the 215 | filesystem was not formatted. Lists the contents of the mountpoint and prints 216 | the outcome of `uos.statvfs`. 217 | 218 | ## 5.5 File copy 219 | 220 | A rudimentary `cp(source, dest)` function is provided as a generic file copy 221 | routine for setup and debugging purposes at the REPL. The first argument is the 222 | full pathname to the source file. The second may be a full path to the 223 | destination file or a directory specifier which must have a trailing '/'. If an 224 | OSError is thrown (e.g. by the source file not existing or the FRAM becoming 225 | full) it is up to the caller to handle it. For example (assuming the FRAM is 226 | mounted on /fram): 227 | 228 | ```python 229 | cp('/flash/main.py','/fram/') 230 | ``` 231 | 232 | See `upysh` in [micropython-lib](https://github.com/micropython/micropython-lib/tree/master/micropython/upysh) 233 | for more fully developed filesystem tools for use at the REPL. 234 | 235 | # 6. Low power operation 236 | 237 | In the absence of an SPI clock signal the chip is specified to draw 50μA max. 238 | This can be reduced to 8μA max by issuing a sleep command. Code to support this 239 | is provided in `fram_spi.py` but is commented out; it is a somewhat specialised 240 | requirement. 241 | 242 | # 7. References 243 | 244 | [256KiB Adafruit board](http://www.adafruit.com/product/4718) 245 | [512KiB Adafruit board](http://www.adafruit.com/product/4719) 246 | [256KiB Chip datasheet](https://cdn-shop.adafruit.com/product-files/4718/4718_MB85RS2MTA.pdf) 247 | [512KiB Chip datasheet](https://cdn-shop.adafruit.com/product-files/4719/4719_MB85RS4MT.pdf) 248 | [Technology](https://www.mouser.com/pdfDOCS/cypress-fram-whitepaper.pdf) 249 | -------------------------------------------------------------------------------- /fram/fram_fs_test.py: -------------------------------------------------------------------------------- 1 | # littlefs_test.py Extended filesystem test of FRAM devices 2 | # Create multiple binary files of varying length and verify that they can be 3 | # read back correctly. Rewrite files with new lengths then check that all files 4 | # are OK. 5 | 6 | import uos 7 | from machine import SPI, Pin 8 | from fram_spi_test import get_fram 9 | 10 | directory = "/fram" 11 | a = bytearray(range(256)) 12 | b = bytearray(256) 13 | files = {} # n:length 14 | errors = 0 15 | 16 | 17 | def fname(n): 18 | return "{}/{:05d}".format(directory, n + 1) # Names start 00001 19 | 20 | 21 | def fcreate(n): # Create a binary file of random length 22 | length = int.from_bytes(uos.urandom(2), "little") + 1 # 1-65536 bytes 23 | length &= 0x3FF # 1-1023 for FRAM 24 | linit = length 25 | with open(fname(n), "wb") as f: 26 | while length: 27 | nw = min(length, 256) 28 | f.write(a[:nw]) 29 | length -= nw 30 | files[n] = length 31 | return linit 32 | 33 | 34 | def fcheck(n): 35 | length = files[n] 36 | with open(fname(n), "rb") as f: 37 | while length: 38 | nr = f.readinto(b) 39 | if not nr: 40 | return False 41 | if a[:nr] != b[:nr]: 42 | return False 43 | length -= nr 44 | return True 45 | 46 | 47 | def check_all(): 48 | global errors 49 | for n in files: 50 | if fcheck(n): 51 | print("File {:d} OK".format(n)) 52 | else: 53 | print("Error in file", n) 54 | errors += 1 55 | print("Total errors:", errors) 56 | 57 | 58 | def remove_all(): 59 | for n in files: 60 | uos.remove(fname(n)) 61 | 62 | 63 | def main(): 64 | fram = get_fram() 65 | try: 66 | uos.mount(fram, directory) 67 | except OSError: # Already mounted 68 | pass 69 | for n in range(128): 70 | length = fcreate(n) 71 | print("Created", n, length) 72 | print("Created files", files) 73 | check_all() 74 | for _ in range(100): 75 | for x in range(5): # Rewrite 5 files with new lengths 76 | n = int.from_bytes(uos.urandom(1), "little") & 0x7F 77 | length = fcreate(n) 78 | print("Rewrote", n, length) 79 | check_all() 80 | remove_all() 81 | 82 | 83 | print("main() to run littlefs test. Filesystem must exist.") 84 | -------------------------------------------------------------------------------- /fram/fram_i2c.py: -------------------------------------------------------------------------------- 1 | # fram_i2c.py Driver for Adafruit 32K Ferroelectric RAM module (Fujitsu MB85RC256V) 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2019 Peter Hinch 5 | 6 | from micropython import const 7 | from bdevice import BlockDevice 8 | 9 | _SIZE = const(32768) # Chip size 32KiB 10 | _ADDR = const(0x50) # FRAM I2C address 0x50 to 0x57 11 | _FRAM_SLAVE_ID = const(0xF8) # FRAM device ID location 12 | _MANF_ID = const(0x0A) 13 | _PRODUCT_ID = const(0x510) 14 | 15 | 16 | # A logical ferroelectric RAM made up of from 1 to 8 chips 17 | class FRAM(BlockDevice): 18 | def __init__(self, i2c, verbose=True, block_size=9): 19 | self._i2c = i2c 20 | self._buf1 = bytearray(1) 21 | self._addrbuf = bytearray(2) # Memory offset into current chip 22 | self._buf3 = bytearray(3) 23 | self._nchips = self.scan(verbose, _SIZE) 24 | super().__init__(block_size, self._nchips, _SIZE) 25 | self._i2c_addr = None # i2c address of current chip 26 | 27 | def scan(self, verbose, chip_size): 28 | devices = self._i2c.scan() 29 | chips = [d for d in devices if d in range(_ADDR, _ADDR + 8)] 30 | nchips = len(chips) 31 | if nchips == 0: 32 | raise RuntimeError("FRAM not found.") 33 | if min(chips) != _ADDR or (max(chips) - _ADDR) >= nchips: 34 | raise RuntimeError("Non-contiguous chip addresses", chips) 35 | for chip in chips: 36 | if not self._available(chip): 37 | raise RuntimeError( 38 | "FRAM at address 0x{:02x} reports an error".format(chip) 39 | ) 40 | if verbose: 41 | s = "{} chips detected. Total FRAM size {}bytes." 42 | print(s.format(nchips, chip_size * nchips)) 43 | return nchips 44 | 45 | def _available(self, device_addr): 46 | res = self._buf3 47 | self._i2c.readfrom_mem_into(_FRAM_SLAVE_ID >> 1, device_addr << 1, res) 48 | manufacturerID = (res[0] << 4) + (res[1] >> 4) 49 | productID = ((res[1] & 0x0F) << 8) + res[2] 50 | return manufacturerID == _MANF_ID and productID == _PRODUCT_ID 51 | 52 | # In the context of FRAM a page == a chip. 53 | # Args: an address and a no. of bytes. Set ._i2c_addr to correct chip. 54 | # Return the no. of bytes available to access on that chip. 55 | def _getaddr(self, addr, nbytes): # Set up _addrbuf and i2c_addr 56 | if addr >= self._a_bytes: 57 | raise RuntimeError("FRAM Address is out of range") 58 | ca, la = divmod(addr, self._c_bytes) # ca == chip no, la == offset into chip 59 | self._addrbuf[0] = (la >> 8) & 0xFF 60 | self._addrbuf[1] = la & 0xFF 61 | self._i2c_addr = _ADDR + ca 62 | return min(nbytes, self._c_bytes - la) 63 | 64 | def readwrite(self, addr, buf, read): 65 | nbytes = len(buf) 66 | mvb = memoryview(buf) 67 | start = 0 # Offset into buf. 68 | while nbytes > 0: 69 | npage = self._getaddr(addr, nbytes) # No of bytes that fit on current chip 70 | if read: 71 | self._i2c.writeto(self._i2c_addr, self._addrbuf) 72 | self._i2c.readfrom_into( 73 | self._i2c_addr, mvb[start : start + npage] 74 | ) # Sequential read 75 | else: 76 | self._i2c.writevto( 77 | self._i2c_addr, (self._addrbuf, buf[start : start + npage]) 78 | ) 79 | nbytes -= npage 80 | start += npage 81 | addr += npage 82 | return buf 83 | -------------------------------------------------------------------------------- /fram/fram_spi.py: -------------------------------------------------------------------------------- 1 | # fram_spi.py Supports Fujitsu 256KiB and 512KiB FRAM devices 2 | # M85RS2MT Adafruit https://www.adafruit.com/product/4718 3 | # M85RS4MT Adafruit https://www.adafruit.com/product/4719 4 | 5 | # These chips are almost identical. Command sets are identical. 6 | # Product ID 1st byte, LS 4 bits is density 0x8 == 2MiB 0x9 == 4MiB 7 | 8 | # Released under the MIT License (MIT). See LICENSE. 9 | # Copyright (c) 2020 Peter Hinch 10 | 11 | from micropython import const 12 | from bdevice import BlockDevice 13 | 14 | # import time # for sleep command 15 | 16 | # Command set 17 | _WREN = const(6) 18 | _WRDI = const(4) 19 | _RDSR = const(5) # Read status reg 20 | _WRSR = const(1) 21 | _READ = const(3) 22 | _WRITE = const(2) 23 | _RDID = const(0x9F) 24 | # _FSTRD = const(0x0b) No obvious difference to _READ 25 | _SLEEP = const(0xB9) 26 | 27 | 28 | class FRAM(BlockDevice): 29 | def __init__(self, spi, cspins, size=512, verbose=True, block_size=9): 30 | if size not in (256, 512): 31 | raise ValueError("FRAM size must be 256 or 512") 32 | super().__init__(block_size, len(cspins), size * 1024) 33 | self._spi = spi 34 | self._cspins = cspins 35 | self._ccs = None # Chip select Pin object for current chip 36 | self._bufp = bytearray(5) # instruction + 3 byte address + 1 byte value 37 | mvp = memoryview(self._bufp) # cost-free slicing 38 | self._mvp = mvp 39 | # Check hardware 40 | density = 8 if size == 256 else 9 41 | for n, cs in enumerate(cspins): 42 | mvp[:] = b"\0\0\0\0\0" 43 | mvp[0] = _RDID 44 | cs(0) 45 | self._spi.write_readinto(mvp, mvp) 46 | cs(1) 47 | # Ignore bits labelled "proprietary" 48 | if mvp[1] != 4 or mvp[2] != 0x7F: 49 | s = "FRAM not found at cspins[{}]." 50 | raise RuntimeError(s.format(n)) 51 | if (mvp[3] & 0x1F) != density: 52 | s = "FRAM at cspins[{}] is incorrect size." 53 | raise RuntimeError(s.format(n)) 54 | if verbose: 55 | s = "Total FRAM size {} bytes in {} devices." 56 | print(s.format(self._a_bytes, n + 1)) 57 | # Set up status register on each chip 58 | for cs in cspins: 59 | self._wrctrl(cs, True) 60 | mvp[0] = _WRSR 61 | mvp[1] = 0 # No block protect or SR protect 62 | cs(0) 63 | self._spi.write(mvp[:2]) 64 | cs(1) 65 | self._wrctrl(cs, False) # Disable write to array 66 | 67 | for n, cs in enumerate(self._cspins): 68 | mvp[0] = _RDSR 69 | cs(0) 70 | self._spi.write_readinto(mvp[:2], mvp[:2]) 71 | cs(1) 72 | if mvp[1]: 73 | s = "FRAM has bad status at cspins[{}]." 74 | raise RuntimeError(s.format(n)) 75 | 76 | def _wrctrl(self, cs, en): # Enable/Disable device write 77 | mvp = self._mvp 78 | mvp[0] = _WREN if en else _WRDI 79 | cs(0) 80 | self._spi.write(mvp[:1]) 81 | cs(1) 82 | 83 | # def sleep(self, on): 84 | # mvp = self._mvp 85 | # mvp[0] = _SLEEP 86 | # for cs in self._cspins: 87 | # cs(0) 88 | # if on: 89 | # self._spi.write(mvp[:1]) 90 | # else: 91 | # time.sleep_us(500) 92 | # cs(1) 93 | 94 | # Given an address, set current chip select and address buffer. 95 | # Return the number of bytes that can be processed in the current page. 96 | # Use of 256 byte pages may be unnecessary for FRAM but cost is minimal. 97 | def _getaddr(self, addr, nbytes): 98 | if addr >= self._a_bytes: 99 | raise RuntimeError("FRAM Address is out of range") 100 | ca, la = divmod(addr, self._c_bytes) # ca == chip no, la == offset into chip 101 | self._ccs = self._cspins[ca] # Current chip select 102 | mvp = self._mvp 103 | mvp[1] = la >> 16 104 | mvp[2] = (la >> 8) & 0xFF 105 | mvp[3] = la & 0xFF 106 | pe = (addr & ~0xFF) + 0x100 # byte 0 of next page 107 | return min(nbytes, pe - la) 108 | 109 | # Interface to bdevice 110 | def readwrite(self, addr, buf, read): 111 | nbytes = len(buf) 112 | mvb = memoryview(buf) 113 | mvp = self._mvp 114 | start = 0 # Offset into buf. 115 | while nbytes > 0: 116 | npage = self._getaddr(addr, nbytes) # No of bytes that fit on current page 117 | cs = self._ccs 118 | if read: 119 | mvp[0] = _READ 120 | cs(0) 121 | self._spi.write(mvp[:4]) 122 | self._spi.readinto(mvb[start : start + npage]) 123 | cs(1) 124 | else: 125 | self._wrctrl(cs, True) 126 | mvp[0] = _WRITE 127 | cs(0) 128 | self._spi.write(mvp[:4]) 129 | self._spi.write(mvb[start : start + npage]) 130 | cs(1) 131 | self._wrctrl(cs, False) 132 | nbytes -= npage 133 | start += npage 134 | addr += npage 135 | return buf 136 | -------------------------------------------------------------------------------- /fram/fram_spi_test.py: -------------------------------------------------------------------------------- 1 | # fram_spi_ test.py MicroPython test program for Adafruit SPI FRAM devices. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | import uos 7 | import time 8 | from machine import SPI, Pin 9 | from fram_spi import FRAM 10 | 11 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 12 | 13 | # Return an FRAM array. Adapt for platforms other than Pyboard. 14 | def get_fram(): 15 | if uos.uname().machine.split(" ")[0][:4] == "PYBD": 16 | Pin.board.EN_3V3.value(1) 17 | time.sleep(0.1) # Allow decouplers to charge 18 | fram = FRAM( 19 | SPI(2, baudrate=25_000_000), cspins, size=512 20 | ) # Change size as required 21 | print("Instantiated FRAM") 22 | return fram 23 | 24 | 25 | # Dumb file copy utility to help with managing FRAM contents at the REPL. 26 | def cp(source, dest): 27 | if dest.endswith("/"): # minimal way to allow 28 | dest = "".join((dest, source.split("/")[-1])) # cp /sd/file /fram/ 29 | with open(source, "rb") as infile: # Caller should handle any OSError 30 | with open(dest, "wb") as outfile: # e.g file not found 31 | while True: 32 | buf = infile.read(100) 33 | outfile.write(buf) 34 | if len(buf) < 100: 35 | break 36 | 37 | 38 | # ***** TEST OF DRIVER ***** 39 | def _testblock(eep, bs): 40 | d0 = b"this >" 41 | d1 = b"xxxxxxxxxxxxx": 55 | return "Block test fail 2:" + str(list(res)) 56 | start = bs 57 | end = bs + len(d1) 58 | eep[start:end] = d1 59 | start = bs - len(d0) 60 | end = start + len(d2) 61 | res = eep[start:end] 62 | if res != d2: 63 | return "Block test fail 3:" + str(list(res)) 64 | 65 | 66 | def test(): 67 | fram = get_fram() 68 | sa = 1000 69 | for v in range(256): 70 | fram[sa + v] = v 71 | for v in range(256): 72 | if fram[sa + v] != v: 73 | print( 74 | "Fail at address {} data {} should be {}".format( 75 | sa + v, fram[sa + v], v 76 | ) 77 | ) 78 | break 79 | else: 80 | print("Test of byte addressing passed") 81 | data = uos.urandom(30) 82 | sa = 2000 83 | fram[sa : sa + 30] = data 84 | if fram[sa : sa + 30] == data: 85 | print("Test of slice readback passed") 86 | # On FRAM the only meaningful block test is on a chip boundary. 87 | block = fram._c_bytes 88 | if fram._a_bytes > block: 89 | res = _testblock(fram, block) 90 | if res is None: 91 | print("Test chip boundary {} passed".format(block)) 92 | else: 93 | print("Test chip boundary {} fail".format(block)) 94 | print(res) 95 | else: 96 | print("Test chip boundary skipped: only one chip!") 97 | 98 | 99 | # ***** TEST OF FILESYSTEM MOUNT ***** 100 | def fstest(format=False): 101 | fram = get_fram() 102 | if format: 103 | uos.VfsFat.mkfs(fram) 104 | vfs = uos.VfsFat(fram) 105 | try: 106 | uos.mount(vfs, "/fram") 107 | except OSError: # Already mounted 108 | pass 109 | print('Contents of "/": {}'.format(uos.listdir("/"))) 110 | print('Contents of "/fram": {}'.format(uos.listdir("/fram"))) 111 | print(uos.statvfs("/fram")) 112 | 113 | 114 | def cptest(): 115 | fram = get_fram() 116 | if "fram" in uos.listdir("/"): 117 | print("Device already mounted.") 118 | else: 119 | vfs = uos.VfsFat(fram) 120 | try: 121 | uos.mount(vfs, "/fram") 122 | except OSError: 123 | print("Fail mounting device. Have you formatted it?") 124 | return 125 | print("Mounted device.") 126 | cp("fram_spi_test.py", "/fram/") 127 | cp("fram_spi.py", "/fram/") 128 | print('Contents of "/fram": {}'.format(uos.listdir("/fram"))) 129 | print(uos.statvfs("/fram")) 130 | 131 | 132 | # ***** TEST OF HARDWARE ***** 133 | def full_test(): 134 | fram = get_fram() 135 | page = 0 136 | for sa in range(0, len(fram), 256): 137 | data = uos.urandom(256) 138 | fram[sa : sa + 256] = data 139 | if fram[sa : sa + 256] == data: 140 | print("Page {} passed".format(page)) 141 | else: 142 | print("Page {} readback failed.".format(page)) 143 | page += 1 144 | -------------------------------------------------------------------------------- /fram/fram_test.py: -------------------------------------------------------------------------------- 1 | # fram_test.py MicroPython test program for Adafruit FRAM devices. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2019 Peter Hinch 5 | 6 | import uos 7 | import time 8 | from machine import I2C, Pin 9 | from fram_i2c import FRAM 10 | 11 | # Return an FRAM array. Adapt for platforms other than Pyboard. 12 | def get_fram(): 13 | if uos.uname().machine.split(" ")[0][:4] == "PYBD": 14 | Pin.board.EN_3V3.value(1) 15 | time.sleep(0.1) # Allow decouplers to charge 16 | fram = FRAM(I2C(2)) 17 | print("Instantiated FRAM") 18 | return fram 19 | 20 | 21 | # Dumb file copy utility to help with managing FRAM contents at the REPL. 22 | def cp(source, dest): 23 | if dest.endswith("/"): # minimal way to allow 24 | dest = "".join((dest, source.split("/")[-1])) # cp /sd/file /fram/ 25 | with open(source, "rb") as infile: # Caller should handle any OSError 26 | with open(dest, "wb") as outfile: # e.g file not found 27 | while True: 28 | buf = infile.read(100) 29 | outfile.write(buf) 30 | if len(buf) < 100: 31 | break 32 | 33 | 34 | # ***** TEST OF DRIVER ***** 35 | def _testblock(eep, bs): 36 | d0 = b"this >" 37 | d1 = b"xxxxxxxxxxxxx": 51 | return "Block test fail 2:" + str(list(res)) 52 | start = bs 53 | end = bs + len(d1) 54 | eep[start:end] = d1 55 | start = bs - len(d0) 56 | end = start + len(d2) 57 | res = eep[start:end] 58 | if res != d2: 59 | return "Block test fail 3:" + str(list(res)) 60 | 61 | 62 | def test(): 63 | fram = get_fram() 64 | sa = 1000 65 | for v in range(256): 66 | fram[sa + v] = v 67 | for v in range(256): 68 | if fram[sa + v] != v: 69 | print( 70 | "Fail at address {} data {} should be {}".format( 71 | sa + v, fram[sa + v], v 72 | ) 73 | ) 74 | break 75 | else: 76 | print("Test of byte addressing passed") 77 | data = uos.urandom(30) 78 | sa = 2000 79 | fram[sa : sa + 30] = data 80 | if fram[sa : sa + 30] == data: 81 | print("Test of slice readback passed") 82 | # On FRAM the only meaningful block test is on a chip boundary. 83 | block = fram._c_bytes 84 | if fram._a_bytes > block: 85 | res = _testblock(fram, block) 86 | if res is None: 87 | print("Test chip boundary {} passed".format(block)) 88 | else: 89 | print("Test chip boundary {} fail".format(block)) 90 | print(res) 91 | else: 92 | print("Test chip boundary skipped: only one chip!") 93 | 94 | 95 | # ***** TEST OF FILESYSTEM MOUNT ***** 96 | def fstest(format=False): 97 | fram = get_fram() 98 | if format: 99 | uos.VfsFat.mkfs(fram) 100 | vfs = uos.VfsFat(fram) 101 | try: 102 | uos.mount(vfs, "/fram") 103 | except OSError: # Already mounted 104 | pass 105 | print('Contents of "/": {}'.format(uos.listdir("/"))) 106 | print('Contents of "/fram": {}'.format(uos.listdir("/fram"))) 107 | print(uos.statvfs("/fram")) 108 | 109 | 110 | def cptest(): 111 | fram = get_fram() 112 | if "fram" in uos.listdir("/"): 113 | print("Device already mounted.") 114 | else: 115 | vfs = uos.VfsFat(fram) 116 | try: 117 | uos.mount(vfs, "/fram") 118 | except OSError: 119 | print("Fail mounting device. Have you formatted it?") 120 | return 121 | print("Mounted device.") 122 | cp("fram_test.py", "/fram/") 123 | cp("fram_i2c.py", "/fram/") 124 | print('Contents of "/fram": {}'.format(uos.listdir("/fram"))) 125 | print(uos.statvfs("/fram")) 126 | 127 | 128 | # ***** TEST OF HARDWARE ***** 129 | def full_test(): 130 | fram = get_fram() 131 | page = 0 132 | for sa in range(0, len(fram), 256): 133 | data = uos.urandom(256) 134 | fram[sa : sa + 256] = data 135 | if fram[sa : sa + 256] == data: 136 | print("Page {} passed".format(page)) 137 | else: 138 | print("Page {} readback failed.".format(page)) 139 | page += 1 140 | -------------------------------------------------------------------------------- /spiram/SPIRAM.md: -------------------------------------------------------------------------------- 1 | # 1. A MicroPython SPIRAM driver 2 | 3 | A driver to enable MicroPython targets to access the SPIRAM (PSRAM) board from 4 | Adafruit, namely [the 8MiB board](https://www.adafruit.com/product/4677). The 5 | SPIRAM chip is equivalent to Espressif ESP-PSRAM64H. SPIRAM offers infinite 6 | endurance and fast access but is volatile: its contents are lost on power down. 7 | 8 | An arbitrary number of boards may be used to construct a memory array whose 9 | size is a multiple of 8MiB. The driver allows the memory either to be mounted 10 | in the host filesystem as a disk device or to be addressed as an array of 11 | bytes. 12 | 13 | ##### [Main readme](../README.md) 14 | 15 | # 2. Connections 16 | 17 | Any SPI interface may be used. The table below assumes a Pyboard running SPI(2) 18 | as per the test program. To wire up a single RAM chip, connect to a Pyboard as 19 | below (n/c indicates no connection): 20 | 21 | | Pin | Signal | PB | Signal | 22 | |:---:|:------:|:---:|:------:| 23 | | 1 | CE/ | Y5 | SS/ | 24 | | 2 | SO | Y7 | MISO | 25 | | 3 | SIO2 | n/c | | 26 | | 4 | Vss | Gnd | Gnd | 27 | | 5 | SI | Y8 | MOSI | 28 | | 6 | SCLK | Y6 | Sck | 29 | | 7 | SIO3 | n/c | | 30 | | 8 | Vcc | 3V3 | 3V3 | 31 | 32 | For multiple boards a separate CS pin must be assigned to each one: each pin 33 | must be wired to a single board's CS line. Multiple boards should have Vin, Gnd, 34 | SCK, MOSI and MISO lines wired in parallel. 35 | 36 | If you use a Pyboard D and power the devices from the 3V3 output you will need 37 | to enable the voltage rail by issuing: 38 | ```python 39 | machine.Pin.board.EN_3V3.value(1) 40 | time.sleep(0.1) # Allow decouplers to charge 41 | ``` 42 | Other platforms may vary. 43 | 44 | # 3. Files 45 | 46 | 1. `spiram.py` Device driver. 47 | 2. `bdevice.py` (In root directory) Base class for the device driver. 48 | 3. `spiram_test.py` Test programs for above. Assumes two 8MiB boards with CS 49 | connected to pins Y4 and Y5 respectively. Adapt for other configurations. 50 | 4. `fs_test.py` A torture test for littlefs. 51 | 52 | Installation: copy files 1 and 2 to the target filesystem. `spiram_test.py` 53 | has a function `test()` which provides quick verification of hardware, but 54 | `cspins` and `get_spiram` at the start of the file may need adaptation to your 55 | hardware. 56 | 57 | # 4. The device driver 58 | 59 | The driver supports mounting the SPIRAM chips as a filesystem. After power up 60 | the device will be unformatted so it is necessary to issue code along these 61 | lines to format the device. Code assumes one or more devices and also assumes 62 | the littlefs filesystem: 63 | 64 | ```python 65 | import os 66 | from machine import SPI, Pin 67 | from spiram import SPIRAM 68 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1),) 69 | ram = SPIRAM(SPI(2, baudrate=25_000_000), cspins) 70 | # Format the filesystem 71 | os.VfsLfs2.mkfs(ram) # Omit this to mount an existing filesystem 72 | os.mount(ram,"/ram") 73 | ``` 74 | The above will reformat a drive with an existing filesystem: to mount an 75 | existing filesystem simply omit the commented line. 76 | 77 | Note that, at the outset, you need to decide whether to use the array as a 78 | mounted filesystem or as a byte array. Typical use-cases involve temporary 79 | files. These include files used for storing Python objects serialised using 80 | pickle/ujson or files holding a btree database. 81 | 82 | The SPI bus must be instantiated using the `machine` module. In the mode used 83 | by the driver the chips are specified to a baudrate of 33MHz. I tested on a 84 | Pyboard D, specifying 25MHz - this produced an actual baudrate of 18MHz. 85 | 86 | ## 4.1 The SPIRAM class 87 | 88 | An `SPIRAM` instance represents a logical RAM: this may consist of multiple 89 | physical devices on a common SPI bus. 90 | 91 | ### 4.1.1 Constructor 92 | 93 | This checks each CS line for an attached board of the correct type and of the 94 | specified size. A `RuntimeError` will occur in case of error, e.g. bad ID, no 95 | device detected or size not matching that specified to the constructor. If all 96 | is OK an SPIRAM instance is created. 97 | 98 | Arguments: 99 | 1. `spi` Mandatory. An initialised SPIbus created by `machine`. 100 | 2. `cspins` A list or tuple of `Pin` instances. Each `Pin` must be initialised 101 | as an output (`Pin.OUT`) and with `value=1` and be created by `machine`. 102 | 3. `size=8192` Chip size in KiB. 103 | 4. `verbose=True` If `True`, the constructor issues information on the SPIRAM 104 | devices it has detected. 105 | 5. `block_size=9` The block size reported to the filesystem. The size in bytes 106 | is `2**block_size` so is 512 bytes by default. 107 | 108 | ### 4.1.2 Methods providing byte level access 109 | 110 | It is possible to read and write individual bytes or arrays of arbitrary size. 111 | Arrays will be somewhat faster owing to more efficient bus utilisation. Note 112 | that, after power up, initial contents of RAM chips should be assumed to be 113 | random. 114 | 115 | #### 4.1.2.1 `__getitem__` and `__setitem__` 116 | 117 | These provides single byte or multi-byte access using slice notation. Example 118 | of single byte access: 119 | 120 | ```python 121 | from machine import SPI, Pin 122 | from spiram import SPIRAM 123 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1),) 124 | ram = SPIRAM(SPI(2), cspins) 125 | ram[2000] = 42 126 | print(ram[2000]) # Return an integer 127 | ``` 128 | It is also possible to use slice notation to read or write multiple bytes. If 129 | writing, the size of the slice must match the length of the buffer: 130 | ```python 131 | from machine import SPI, Pin 132 | from spiram import SPIRAM 133 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1),) 134 | ram = SPIRAM(SPI(2), cspins) 135 | ram[2000:2003] = "ABC" 136 | print(ram[2000:2003]) # Returns a bytearray 137 | ``` 138 | Three argument slices are not supported: a third arg (other than 1) will cause 139 | an exception. One argument slices (`ram[:5]` or `ram[32760:]`) and negative 140 | args are supported. 141 | 142 | #### 4.1.2.2 readwrite 143 | 144 | This is a byte-level alternative to slice notation. It has the potential 145 | advantage when reading of using a pre-allocated buffer. Arguments: 146 | 1. `addr` Starting byte address 147 | 2. `buf` A `bytearray` or `bytes` instance containing data to write. In the 148 | read case it must be a (mutable) `bytearray` to hold data read. 149 | 3. `read` If `True`, perform a read otherwise write. The size of the buffer 150 | determines the quantity of data read or written. A `RuntimeError` will be 151 | thrown if the read or write extends beyond the end of the physical space. 152 | 153 | ### 4.1.3 Other methods 154 | 155 | #### The len() operator 156 | 157 | The size of the RAM array in bytes may be retrieved by issuing `len(ram)` 158 | where `ram` is the `SPIRAM` instance. 159 | 160 | ### 4.1.4 Methods providing the block protocol 161 | 162 | These are provided by the base class. For the protocol definition see 163 | [the pyb documentation](http://docs.micropython.org/en/latest/library/uos.html#uos.AbstractBlockDev) 164 | also [here](http://docs.micropython.org/en/latest/reference/filesystem.html#custom-block-devices). 165 | 166 | These methods exist purely to support the block protocol. They are undocumented: 167 | their use in application code is not recommended. 168 | 169 | `readblocks()` 170 | `writeblocks()` 171 | `ioctl()` 172 | 173 | # 5. Test program spiram_test.py 174 | 175 | This assumes a Pyboard 1.x or Pyboard D with SPIRAM(s) wired as above. It 176 | provides the following. 177 | 178 | ## 5.1 test() 179 | 180 | This performs a basic test of single and multi-byte access to chip 0. The test 181 | reports how many chips can be accessed. Existing array data will be lost. This 182 | primarily tests the driver: as a hardware test it is not exhaustive. 183 | 184 | ## 5.2 full_test() 185 | 186 | This is a hardware test. Tests the entire array. Fills a 2048 byte block with 187 | random data, reads it back, and checks the outcome before moving to the next 188 | block. Existing data will be lost. This will detect serious hardware errors but 189 | is not a comprehensive RAM chip test. 190 | 191 | ## 5.3 fstest() 192 | 193 | Formats the RAM array as a littlefs filesystem and mounts the device on `/ram`. 194 | Lists the contents (which will be empty) and prints the outcome of `os.statvfs` 195 | on the array. 196 | 197 | ## 5.4 cptest() 198 | 199 | Very simple filesystem test. If a filesystem is already mounted on `/ram`, 200 | prints a message; otherwise formats the array with littlefs and mounts it. 201 | Copies the source files to the filesystem, lists the contents of the mountpoint 202 | and prints the outcome of `os.statvfs`. 203 | 204 | ## 5.5 File copy 205 | 206 | A rudimentary `cp(source, dest)` function is provided as a generic file copy 207 | routine for setup and debugging purposes at the REPL. The first argument is the 208 | full pathname to the source file. The second may be a full path to the 209 | destination file or a directory specifier which must have a trailing '/'. If an 210 | OSError is thrown (e.g. by the source file not existing or the RAM becoming 211 | full) it is up to the caller to handle it. For example (assuming the RAM is 212 | mounted on /ram): 213 | 214 | ```python 215 | cp('/flash/main.py','/ram/') 216 | ``` 217 | 218 | See the official `upysh` in 219 | [micropython-lib](https://github.com/micropython/micropython-lib/tree/master/micropython/upysh) 220 | for more fully developed filesystem tools for use at the REPL. 221 | 222 | # 6. Test program fs_test.py 223 | 224 | This is a torture test for littlefs. It creates many binary files of varying 225 | length and verifies that they can be read back correctly. It rewrites files 226 | with new lengths and checks that all files are OK. Run time is many minutes 227 | depending on platform. 228 | -------------------------------------------------------------------------------- /spiram/fs_test.py: -------------------------------------------------------------------------------- 1 | # fs_test.py Extended filesystem test of SPIRAM devices 2 | # Create multiple binary files of varying length and verify that they can be 3 | # read back correctly. Rewrite files with new lengths then check that all files 4 | # are OK. 5 | 6 | import os 7 | from machine import SPI, Pin 8 | from spiram_test import get_spiram 9 | 10 | directory = "/ram" 11 | a = bytearray(range(256)) 12 | b = bytearray(256) 13 | files = {} # n:length 14 | errors = 0 15 | 16 | 17 | def fname(n): 18 | return "{}/{:05d}".format(directory, n + 1) # Names start 00001 19 | 20 | 21 | def fcreate(n): # Create a binary file of random length 22 | length = int.from_bytes(os.urandom(2), "little") + 1 # 1-65536 bytes 23 | length &= 0x3FF # 1-1023 for FRAM 24 | linit = length 25 | with open(fname(n), "wb") as f: 26 | while length: 27 | nw = min(length, 256) 28 | f.write(a[:nw]) 29 | length -= nw 30 | files[n] = length 31 | return linit 32 | 33 | 34 | def fcheck(n): 35 | length = files[n] 36 | with open(fname(n), "rb") as f: 37 | while length: 38 | nr = f.readinto(b) 39 | if not nr: 40 | return False 41 | if a[:nr] != b[:nr]: 42 | return False 43 | length -= nr 44 | return True 45 | 46 | 47 | def check_all(): 48 | global errors 49 | for n in files: 50 | if fcheck(n): 51 | print("File {:d} OK".format(n)) 52 | else: 53 | print("Error in file", n) 54 | errors += 1 55 | print("Total errors:", errors) 56 | 57 | 58 | def remove_all(): 59 | for n in files: 60 | os.remove(fname(n)) 61 | 62 | 63 | def main(): 64 | ram = get_spiram() 65 | os.VfsLfs2.mkfs(ram) # Format littlefs 66 | try: 67 | os.mount(ram, "/ram") 68 | except OSError: # Already mounted 69 | pass 70 | for n in range(128): 71 | length = fcreate(n) 72 | print("Created", n, length) 73 | print("Created files", files) 74 | check_all() 75 | for _ in range(100): 76 | for x in range(5): # Rewrite 5 files with new lengths 77 | n = int.from_bytes(os.urandom(1), "little") & 0x7F 78 | length = fcreate(n) 79 | print("Rewrote", n, length) 80 | check_all() 81 | remove_all() 82 | 83 | 84 | print("main() to run littlefs test. Erases any data on RAM.") 85 | -------------------------------------------------------------------------------- /spiram/spiram.py: -------------------------------------------------------------------------------- 1 | # spiram.py Supports 8MiB SPI RAM 2 | # Adafruit https://www.adafruit.com/product/4677 3 | 4 | # These chips are almost identical. Command sets are identical. 5 | # Product ID 1st byte, LS 4 bits is density 0x8 == 2MiB 0x9 == 4MiB 6 | 7 | # Released under the MIT License (MIT). See LICENSE. 8 | # Copyright (c) 2020 Peter Hinch 9 | 10 | from micropython import const 11 | from bdevice import BlockDevice 12 | 13 | # Command set 14 | _WRITE = const(2) 15 | _READ = const(3) 16 | _RSTEN = const(0x66) 17 | _RESET = const(0x99) 18 | _RDID = const(0x9F) 19 | 20 | 21 | class SPIRAM(BlockDevice): 22 | def __init__(self, spi, cspins, size=8192, verbose=True, block_size=9): 23 | if size != 8192: 24 | print("SPIRAM size other than 8192KiB may not work.") 25 | super().__init__(block_size, len(cspins), size * 1024) 26 | self._spi = spi 27 | self._cspins = cspins 28 | self._ccs = None # Chip select Pin object for current chip 29 | bufp = bytearray(6) # instruction + 3 byte address + 2 byte value 30 | mvp = memoryview(bufp) # cost-free slicing 31 | self._mvp = mvp 32 | # Check hardware 33 | for n, cs in enumerate(cspins): 34 | mvp[:] = b"\0\0\0\0\0\0" 35 | mvp[0] = _RDID 36 | cs(0) 37 | self._spi.write_readinto(mvp, mvp) 38 | cs(1) 39 | if mvp[4] != 0x0D or mvp[5] != 0x5D: 40 | print("Warning: expected manufacturer ID not found.") 41 | 42 | if verbose: 43 | s = "Total SPIRAM size {} KiB in {} devices." 44 | print(s.format(self._a_bytes // 1024, n + 1)) 45 | 46 | # Given an address, set current chip select and address buffer. 47 | # Return the number of bytes that can be processed in the current chip. 48 | def _getaddr(self, addr, nbytes): 49 | if addr >= self._a_bytes: 50 | raise RuntimeError("SPIRAM Address is out of range") 51 | ca, la = divmod(addr, self._c_bytes) # ca == chip no, la == offset into chip 52 | self._ccs = self._cspins[ca] # Current chip select 53 | mvp = self._mvp 54 | mvp[1] = la >> 16 55 | mvp[2] = (la >> 8) & 0xFF 56 | mvp[3] = la & 0xFF 57 | pe = (addr & -self._c_bytes) + self._c_bytes # Byte 0 of next chip 58 | return min(nbytes, pe - la) 59 | 60 | # Interface to bdevice 61 | def readwrite(self, addr, buf, read): 62 | nbytes = len(buf) 63 | mvb = memoryview(buf) 64 | mvp = self._mvp 65 | start = 0 # Offset into buf. 66 | while nbytes > 0: 67 | nchip = self._getaddr(addr, nbytes) # No of bytes that fit on current chip 68 | cs = self._ccs 69 | if read: 70 | mvp[0] = _READ 71 | cs(0) 72 | self._spi.write(mvp[:4]) 73 | self._spi.readinto(mvb[start : start + nchip]) 74 | cs(1) 75 | else: 76 | mvp[0] = _WRITE 77 | cs(0) 78 | self._spi.write(mvp[:4]) 79 | self._spi.write(mvb[start : start + nchip]) 80 | cs(1) 81 | nbytes -= nchip 82 | start += nchip 83 | addr += nchip 84 | return buf 85 | 86 | 87 | # Reset is unnecessary because it restores the default power-up state. 88 | # def _reset(self, cs, bufr = bytearray(1)): 89 | # cs(0) 90 | # bufr[0] = _RSTEN 91 | # self._spi.write(bufr) 92 | # cs(1) 93 | # cs(0) 94 | # bufr[0] = _RESET 95 | # self._spi.write(bufr) 96 | # cs(1) 97 | -------------------------------------------------------------------------------- /spiram/spiram_test.py: -------------------------------------------------------------------------------- 1 | # spiram_ test.py MicroPython test program for Adafruit SPIRAM device 2 | # Adafruit https://www.adafruit.com/product/4677 3 | 4 | 5 | # Released under the MIT License (MIT). See LICENSE. 6 | # Copyright (c) 2021 Peter Hinch 7 | 8 | import os 9 | import time 10 | from machine import SPI, Pin 11 | from spiram import SPIRAM 12 | 13 | cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) 14 | 15 | # Return an RAM array. Adapt for platforms other than Pyboard. 16 | def get_spiram(): 17 | if os.uname().machine.split(" ")[0][:4] == "PYBD": 18 | Pin.board.EN_3V3.value(1) 19 | time.sleep(0.1) # Allow decouplers to charge 20 | ram = SPIRAM(SPI(2, baudrate=25_000_000), cspins) 21 | print("Instantiated RAM") 22 | return ram 23 | 24 | 25 | # Dumb file copy utility to help with managing FRAM contents at the REPL. 26 | def cp(source, dest): 27 | if dest.endswith("/"): # minimal way to allow 28 | dest = "".join((dest, source.split("/")[-1])) # cp /sd/file /ram/ 29 | with open(source, "rb") as infile: # Caller should handle any OSError 30 | with open(dest, "wb") as outfile: # e.g file not found 31 | while True: 32 | buf = infile.read(100) 33 | outfile.write(buf) 34 | if len(buf) < 100: 35 | break 36 | 37 | 38 | # ***** TEST OF DRIVER ***** 39 | def _testblock(eep, bs): 40 | d0 = b"this >" 41 | d1 = b"xxxxxxxxxxxxx": 55 | return "Block test fail 2:" + str(list(res)) 56 | start = bs 57 | end = bs + len(d1) 58 | eep[start:end] = d1 59 | start = bs - len(d0) 60 | end = start + len(d2) 61 | res = eep[start:end] 62 | if res != d2: 63 | return "Block test fail 3:" + str(list(res)) 64 | 65 | 66 | def test(): 67 | ram = get_spiram() 68 | sa = 1000 69 | for v in range(256): 70 | ram[sa + v] = v 71 | for v in range(256): 72 | if ram[sa + v] != v: 73 | print( 74 | "Fail at address {} data {} should be {}".format(sa + v, ram[sa + v], v) 75 | ) 76 | break 77 | else: 78 | print("Test of byte addressing passed") 79 | data = os.urandom(30) 80 | sa = 2000 81 | ram[sa : sa + 30] = data 82 | if ram[sa : sa + 30] == data: 83 | print("Test of slice readback passed") 84 | # On SPIRAM the only meaningful block test is on a chip boundary. 85 | block = ram._c_bytes 86 | if ram._a_bytes > block: 87 | res = _testblock(ram, block) 88 | if res is None: 89 | print("Test chip boundary {} passed".format(block)) 90 | else: 91 | print("Test chip boundary {} fail".format(block)) 92 | print(res) 93 | else: 94 | print("Test chip boundary skipped: only one chip!") 95 | 96 | 97 | # ***** TEST OF FILESYSTEM MOUNT ***** 98 | def fstest(): 99 | ram = get_spiram() 100 | os.VfsLfs2.mkfs(ram) # Format littlefs 101 | try: 102 | os.mount(ram, "/ram") 103 | except OSError: # Already mounted 104 | pass 105 | print('Contents of "/": {}'.format(os.listdir("/"))) 106 | print('Contents of "/ram": {}'.format(os.listdir("/ram"))) 107 | print(os.statvfs("/ram")) 108 | 109 | 110 | def cptest(): 111 | ram = get_spiram() 112 | if "ram" in os.listdir("/"): 113 | print("Device already mounted.") 114 | else: 115 | os.VfsLfs2.mkfs(ram) # Format littlefs 116 | os.mount(ram, "/ram") 117 | print("Formatted and mounted device.") 118 | cp("/sd/spiram_test.py", "/ram/") 119 | cp("/sd/spiram.py", "/ram/") 120 | print('Contents of "/ram": {}'.format(os.listdir("/ram"))) 121 | print(os.statvfs("/ram")) 122 | 123 | 124 | # ***** TEST OF HARDWARE ***** 125 | def full_test(): 126 | bsize = 2048 127 | ram = get_spiram() 128 | page = 0 129 | for sa in range(0, len(ram), bsize): 130 | data = os.urandom(bsize) 131 | ram[sa : sa + bsize] = data 132 | if ram[sa : sa + bsize] == data: 133 | print("Page {} passed".format(page)) 134 | else: 135 | print("Page {} readback failed.".format(page)) 136 | page += 1 137 | --------------------------------------------------------------------------------