Winbond flash memories provide an excellent method of storing up to 16MB of data in a tiny SPI driven chip. At around $0.50 each (varies) these offer an amazing storage/price density and are used in many devices to store configuration and firmware. Throwing out some old electronics? Check the board - there is likely a Flash memory that can be recovered and re-used.
SPI is very much faster and easier to drive than I
2C. The routines here work flawlessly at 20MHz and were inspired by work MatherP published on the BackShed here
https://www.thebackshed.com/forum/ViewTopic.php?TID=8492That work used CSubs to access the Flash and whereas they are very quick, they exclude users of the bigger 'Mite platforms - the MMX and ST's Arm base processors - the H7/F4 platforms. The routines below are a purely MMBasic solution and so can be used on any MMBasic platform supporting the SPI bus.
The approach with the code here is comparable to a sequential file. Data is written using one function and read back with another. When data is exhausted a marker, CHR$(255), is reached. This is analagous with the EOF character of disk filing systems. This code provides a simple system of strings in, strings out. It handles just this - what you actually store is your own decision. Using a structured logging method e.g.
Ultra-Compact Logging with Flash Storage on small MicroMites it would be possible to log continuously at one second intervals for eight days straight on a 4MB chip. With arbitrary strings of average length 50 bytes, in excess of 83,000 lines could be stored.
During research for this software pack, some solutions were found where a single string occupies an entire page with corresponding wastage or "slack space". If such a method were employed, a 1MB flash could store just 4096 strings(!).Organisation of Strings in Memory
The Flash technology provides continuous reading starting from a single 24 bit address. Once latched, the address increments with each read and so the entire memory could be rapidly read out with only a single address setup - at 20MHz this could be little more than 400nS per
byte!
Writing the device is more involved. Internally, Flash memory is arranged into pages of 256 bytes. When writing, you can only do so within a given page and although you have to latch a full 24 bit address, the top 16 bits are fixed so only the bottom 8 bits can be worked with - you are locked into a page. Attempts to write beyond simply roll the bottom eight bits of the address back to zero and over-write what was already in that page. Note also that because of the way flash memory is constructed, only zero bits can be written. You cannot set a previously zero bit to one without formatting. This is why a formatted chip has all locations sets to 255 (0FFh). When writing a byte to any given location, the result stored is a logical AND of the original value and the value you wrote. So supposing you wish to write 55h: If the memory is 0FFh, FF AND 55 = 55... all good, but now suppose you want to write 0AAh in that same location afterwards; 55 AND AA = 0. So you can see the data becomes unreliable unless you are writing in formatted cells. Any Random access writing you might try needs to be aware of this. Grey beards may remember burning EPROMS years back. It was the same approach there and fast programmers would read out an EPROM then AND it with your target code to see if it could be burned in without having to erase the EPROM beforehand. Similar thing really.
The write routine takes care of paging making the process transparent to the user. You can see from the grab below that a string (red box) traverses a page boundary with no problem - the two bytes in the yellow boxes are the last and first bytes in different pages (00FFh = page 0 and 0100h = page 1) but it's all handled by the write function. This means the entire memory is available for use.
The below diagram shows the structure of strings in memory in more detail - each cell represents a byte in Flash. Here the string "IIIII" is stored as the first and only string in the memory
Strings may be 0 to 255 characters long and different length strings can be freely mixed in memory but must conform to the following rules for any one character:
- CHR$(0) - Forbidden. This is the string delimiter byte used for organising the stored strings.
- CHR$(1) to CHR$(254) - May be freely used within your strings.
- CHR$(255) - Forbidden. All Flash memory locations are set to 255 during a format. The location of the start of blank memory (where data may be safely stored) is determined from the first such byte.
The initialization routine (WB.Init) performs several useful and vital functions:
- It identifies the type of Flash attached (if any) and sets the maximum address variable WB.Top to the last available memory location.
- It rapidly determines the first unused byte in the memory (more on this later) and sets the WB.WRptr variable so as not to trample on previously stored data
Method
There are two address pointers, one for read and another for write. This permits an easy access method where the memory can be read and written simultaneously with no special measures. In normal use, the pointers are of no concern and any program will use the WB.WRstr() and WB.RDstr$() routines.
Writing:
An erased Flash has all locations set to 255 so it follows the first such byte (lowest address) is the first place we can write data without trampling on earlier data (now it is clear why CHR$(255) is prohibited in strings). NOTE: Brand new Flash chips are not necessarily in an erased state and should be formatted first for reliable use - unless the garbage is of some curiosity value.
WB.WRptr points to the first such memory location i.e. the position new data can be reliably stored. Flash memories retain data for up to 20 years and with such large capacities, it is highly likely that data will be aggregated over power-cycles. At initialization, WB.WRPtr is ascertained by a rapid successive approximation algorithm that will return the first 255 byte in around 80mS on a 4MB chip with 48MHz MicroMite. This happens each time WB.Init is called - you can do this as often as you like but once at the start of your program is usual.
Reading:
Each byte is read from memory and built up in a string until a 0 is read whence the string is returned (the 0 does not form part of that string). The read pointer, WB.RDptr, is incremented to keep track of where reading occurs and will always point to the start of the next string to be read, thus you can continually read from the memory with successive calls to WB.ReadStr$. If an attempt is made to read past valid data, CHR$(255) is returned by WB.ReadStr$ and WB.RDptr is not incremented (because no string was read). When additional data is written, the next read will return the new data and so on...
Thus, WB.RDptr will happily trot along behind WB.WRPtr but never pass it. It is entirely normal for the two pointers to have the same value - when you have read out all the data. This can be used as an end-of-file (EOF) indication if you prefer.
Global Variables
Integer
WB.RDptrThe pointer for the next string READ operation
Integer
WB.WRptrThe pointer for the next string WRITE operation
Integer
WB.IDThe JEDEC identifier of the Flash - the manufacturer and 2 byte device ID.
String
WB$A human friendly version of the ID e.g. W25Q32 etc.
Integer
WB.TopThe top-most available memory location
The Routines
WB.InitDiscover what type of Flash and Initialise the internal pointers.
WB.FormatErase the entire Flash; sets all memory locations to contain 0FFh - does an initialization afterwards to reset all the pointers. New Flash chips should be formatted before use as they may contain random data that may confuse WB.Init and return dubious usage figures.
=WB.Stat1RD()=WB.Stat2RD()=WB.Stat3RD()Read the three status registers.
WB.WaitBusyWait for the Flash to become idle. Beware, a format can take several seconds depending on the Flash capacity. If you are using WATCHDOG in your code, you may need to put one in here so your code doesn't restart on long waits.
=WB.TestBusy()Check if the Flash is busy without waiting.
=WB.Peek(address)Peek a byte at the given address in Flash. Flash is erased to FF so when we peek the next address after a string we can tell if we are at EOF.
=WB.ReadStr$()Read a string from the current read pointer and increment the pointer. Returns Chr$(255) if we are at the end of the data.
=WB.WriteStr(a$)Write a string to the current write pointer and increment the pointer. Returns a zero if successful and 1 if the write would exceed available free space.
Support routines - unlikely to be useful in your code:
WB.Addr(address)Latch a three byte address in the Flash.
WB.WREnable(0|1)Enable/Disable write function.
WB.SetPageSet up the page address for a pending write operation.
Example
'*****************************************************************
' your setup code goes here
'*****************************************************************
Init:
CPU 48
'Option Autorun On
'Option Baudrate 9600
Option Explicit
Option Base 0
Const Ver$="0.20 of 24-NOV-2019"
'-------------------------------------------------------------------
'The only config you should need to do:
'change the following to the CS pin for the Flash memory
Const WB.CS=23
'change the following to the SPI channel where your Flash chip is (it's usually OK to leave it open
'so long as nothing else is competing and closes it after use - e.g. LCD panels)
SPI Open 20000000,0,8'20MHz, mode 0, 8 bits... seems happy at 20MHz but drop this down if you get probs
'-------------------------------------------------------------------
'mandatory variables in your program:
'-------------------------------------------------------------------
Dim Integer WB.RDptr,WB.WRptr,WB.Top,WB.ID,x,n
Dim WB$
'-------------------------------------------------------------------
'*****************************************************************
' your program goes here
'*****************************************************************
'A little demonstration code...
x=WB.Init() 'single function id's the device, sets mem size and finds first usable address
'your code should check WB.Top after this call. if no Flash found or couldn't be identified, WB.Top=-1
If x<>0 Then
Print"Flash chip not recognized: ";Hex$(x,6)
End
EndIf
Dim y$
Dim Integer m
Print"Found ";WB$;" (";Hex$(WB.ID,6);")"
Print"memory size is";(WB.Top+1)/1024;"KB (0-"+Hex$(WB.Top)+")"
Print"First blank address is ";Hex$(WB.WRptr);"h"
Input"Format before testing (Y/N)?",y$
If y$="y" then
Print "Formatting..."
WB.Format
Print"First blank address is ";Hex$(WB.WRptr);"h"
EndIf
Randomize Timer ' delete this line on MMX/Arm etc
'write some random strings to Flash with timings
Print
Print"","WR Time"
Print"--------------------------------------"
For n=1 To 10
Print n,
Timer=0:x=WB.WriteStr(Date$+" "+Time$+" "+String$(65,48+Rnd*70)):Print Timer;"mS"
Next
'read back the strings - demonstrates chr$(255) if attempting to read
Print:Print"Reading back all strings..."
Print"Addr","Time","Data"
Print"--------------------------------------"
Do
Print Hex$(WB.RDptr,4);"h",
Timer=0:y$=WB.ReadStr$():Print Timer,y$
Loop While y$<>Chr$(255)
For n=0 To 1023 Step 16
y$=""
Print Hex$(n,4);" ";
For m=0 To 15
x=WB.Peek(n+m)
Print Hex$(x,2);" ";
If x<32 Or x>126 Then x=&h2e
y$=y$+Chr$(x)
Next
Print y$
Next
Input"Format the Flash (Y/N)?",y$
y$=UCase$(y$)
If y$="Y" Then
Print"wait a moment"
timer=0:WB.Format:Print timer;"mS"
EndIf
End
The routines are fairly brisk and numerous testing/adjustment phases have resulted in some tricks to make them as fast as possible. The Write function was a bottle-neck and has been re-written. It is now very quick and because of the SPI Bulk Write methods available, paradoxically, writes substantially out-perform reads by about a factor of 3 worst case (255 character strings) at ~20mS Vs ~70mS. With 3 character strings, Reads take around 5mS and Writes are still around the 20mS mark.
Here are some timings for sequential reads & writes on a 48MHz '170 with 24MHz SPI bus.
Random 254 char strings (worst case)
old write method:
1 89mS
2 97mS
3 97mS
4 96mS
5 97mS
6 96mS
7 96mS
8 97mS
9 96mS
10 97mS
new write method:
1 11mS
2 19mS
3 19mS
4 19mS
5 20mS
6 19mS
7 19mS
8 19mS
9 19mS
10 20mS
Read 254 character string
73mS
72mS
72mS
71mS
72mS
72mS
72mS
72mS
Read 3 character string
5mS
5mS
4mS
5mS
5mS
5mS
5mS
4mS
5mS
The Code
'*****************************************************************
'Flash Subs & Functions
'Discover the attached flash memory and initialize all the pointers and stuff
'sets up the CS pin and opens the SPI channel
'returns 0 if the chip is IDed with all the variables correctly set up.
'else returns the ID read from the chip = &FFFFFF is open bus (no chip)
Function WB.Init() As Integer
Local Integer n
WB.Top=-1:WB.WRptr=-1
'identify the Flash ram by reading the JEDEC ID and setting some key values
Pin(WB.CS)=0
x=SPI(&h9F)
WB.ID=65536*SPI(0)+256*SPI(0)+SPI(0)
Pin(WB.CS)=1
'if your device is not listed or comes up unknown, you'll have to determine the correct ID
'from the device PDF and add it below. Good news is, it looks like all the Winbond W25* are
'4KB sectors and 256B page size so the code *should* work without any changes - no guarantees
'only known problem is the code assumes a 24 bit address which breaks for >16MB devices and maybe
'others which might only provide a 16 bit address - haven't looked at the PDF.
'The Winbond product selector shows these chips but I couldn't find PDFs for them:
'W25Q02JV 256MB
'W25Q01JV 128MB
Select Case WB.ID
'*** Tested
Case &hEF4018:WB.Top=16384:WB$="W25Q128"'16MB
Case &hEF4017:WB.Top=8192:WB$="W25Q64"'8MB
Case &hEF4016:WB.Top=4096:WB$="W25Q32"'4MB
Case &hEF4015:WB.Top=2048:WB$="W25Q16"'2MB
Case &hEF4014:WB.Top=1024:WB$="W25Q80"'1MB
'*** UnTested
Case &hEF4020:WB.Top=65536:WB.Top=16384:WB$="W25Q512"'64MB - With these two devices, the code should work as they default to
Case &hEF4019:WB.Top=32768:WB.Top=16384:WB$="W25Q256"'32MB - 3 byte addresses but you'll only be able to use the bottom 16MB.
'May support the higher capacities later if there is a demand.
Case &hEF4013:WB.Top=512:WB$="W25Q40"'512KB
Case &hEF5012:WB.Top=256:WB$="W25Q20"'256KB
Case &hEF6011:WB.Top=128:WB$="W25Q10"'128KB
Case &hEF3010:WB.Top=64:WB$="W25X05"'64KB
'Case &hBF2642:WB.Top=4096:WB$="SST26VF032"'Microchip 4MB pin and code compatible - needs work on global enable
Case Else
SPI Close:WB.Top=-1:WB.Init=WB.ID:Exit Function'can't identify
End Select
WB.Top=(WB.Top*1024)-1'set the top memory location
'find the first (=lowest address with) FF byte
If WB.Peek(0)=255 Then WB.WRptr=0:WB.RDptr=0:Exit Function'blank Flash; early bath
'otherwise find by successive approximation - very fast, searches 4MB in 80mS
WB.WRptr=(WB.Top+1)\2:n=WB.WRptr\2'start at the middle and go in smaller and smaller halves
Do
If WB.Peek(WB.WRptr)=255 Then
WB.WRptr=WB.WRptr-n'still in void so go down by half the remainder
Else
WB.WRptr=WB.WRptr+n'we are in the strings so go up by half the remainder
EndIf
If n<>1 Then n=n\2'smaller and smaller halves
Loop Until WB.Peek(WB.WRptr)=255 And WB.Peek(WB.WRptr-1)<>255'is the byte before the FF !FF? if so, we are done
End Function
'set or clear flash write enable flag
Sub WB.WREnable(a As Integer)
If WB.Top=-1 Then Exit Sub
Local Integer x
Pin(WB.CS)=0
If a=0 Then
x=SPI(4)
Else
x=SPI(6)
EndIf
Pin(WB.CS)=1
WB.WaitBusy
End Sub
'wait while Flash is busy
Sub WB.WaitBusy
If WB.Top=-1 Then Exit Sub
Do While WB.TestBusy()
'you might want to put a WATCHDOG here. Long operations could break your program
Loop
End Sub
'test BUSY flag in STAT1
Function WB.TestBusy() As Integer
If WB.Top=-1 Then Exit Function
WB.TestBusy=(WB.Stat1RD() And 1)
End Function
Function WB.Stat1RD() As Integer
If WB.Top=-1 Then Exit Function
Local Integer x
Pin(WB.CS)=0
x=SPI(5)
WB.Stat1RD=SPI(0)
Pin(WB.CS)=1
End Function
Function WB.Stat2RD() As Integer
If WB.Top=-1 Then Exit Function
Local Integer x
Pin(WB.CS)=0
x=SPI(&h35)
WB.Stat2RD=SPI(0)
Pin(WB.CS)=1
End Function
Function WB.Stat3RD() As Integer
If WB.Top=-1 Then Exit Function
Local Integer x
Pin(WB.CS)=0
x=SPI(&h15)
WB.Stat3RD=SPI(0)
Pin(WB.CS)=1
End Function
'send a 3 byte address to Flash
Sub WB.Addr(a As Integer)
If WB.Top=-1 Then Exit Sub
Local Integer x
x=SPI((a>>16) And 255)
x=SPI((a>>8) And 255)
x=SPI(a And 255)
End Sub
'set the address for page writing
Sub WB.SetPage
If WB.Top=-1 Then Exit Sub
Local Integer x
Pin(WB.CS)=1
WB.WaitBusy
WB.WREnable 1
WB.WaitBusy
Pin(WB.CS)=0
x=SPI(2)
WB.Addr WB.WRptr'setup the address
End Sub
'erase the entire chip - Beware; can take several seconds
Sub WB.Format()
If WB.Top=-1 Then Exit Sub
Local Integer x
WB.WREnable 1
WB.WaitBusy
Pin(WB.CS)=0
x=SPI(&h60)'start the erase - will take some time
Pin(WB.CS)=1
WB.WaitBusy
x=WB.Init()
End Sub
'Peek any address in Flash
Function WB.Peek(a As Integer) As Integer
If WB.Top=-1 Then Exit Function
Local Integer x
Pin(WB.CS)=0
x=SPI(3)'read at the given address
WB.Addr a
WB.Peek=SPI(0)'grab one byte
Pin(WB.CS)=1
End Function
'Read a string from the Read Address using the global WB.RDptr variable
Function WB.ReadStr$()
If WB.Top=-1 Then Exit Function
Local Integer x,z
Local a$
WB.WaitBusy'wait for the Flash to be idle
Pin(WB.CS)=0
x=SPI(3)
WB.Addr WB.RDptr
For z=1 to 256'stream the data from Flash into a$, 256 covers max length+chr$(0), we bail early if needed
x=SPI(0)'get the character
Select case x
Case 0 'break out on the delimiter
Poke Var a$,0,z-1
WB.RDptr=WB.RDptr+Len(a$)+1'bump the address along by the number of bytes we read +1 for the delimiter
Exit For
Case 255 'break out on EOF, should never happen
WB.RDptr=WB.RDptr+Len(a$)
a$=Chr$(x)'single char?
Exit For
Case Else
Poke Var a$,z,x
End select
Next
Pin(WB.CS)=1
WB.ReadStr$=a$
End Function
'write a string to the next available position. Return 0 if successful
Function WB.WriteStr(a$) As Integer
If WB.Top=-1 Then WB.WriteStr=1:Exit Function
Local Integer n,x
n=Len(a$)+1
If WB.WRptr+n>WB.Top Then WB.WriteStr=1:Exit Function
x=256-(WB.WRPtr Mod 256)' remaining space in this page
WB.SetPage
If n<=x Then 'will fit in current page
SPI Write n-1,a$
x=SPI(0)
Pin(WB.CS)=1
WB.WrPtr=WB.WrPtr+n
Else 'have to split across pages
'first half
Local l$
l$=Left$(a$,x)
SPI Write len(l$),l$
Pin(WB.CS)=1
WB.WrPtr=WB.WrPtr+x
'second half
l$=Mid$(a$,x+1)+Chr$(0)
x=Len(l$)
WB.SetPage
SPI Write x,l$
Pin(WB.CS)=1
WB.WrPtr=WB.WrPtr+x
EndIf
End Function
'*****************************************************************