MMBasic code-pack to Read and Write Winbond Flash Memories

Modified on 2021/10/25 12:42 by CaptainBoing — Categorized as: ARMMite, Electronics, File Management, Filesystem, Hardware, Memory, MMX, Program Management, Strings

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 I2C. 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=8492

That 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.

Image

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
Image
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:

The initialization routine (WB.Init) performs several useful and vital functions:


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.
Image

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.
Image

Global Variables

Integer WB.RDptr
The pointer for the next string READ operation

Integer WB.WRptr
The pointer for the next string WRITE operation

Integer WB.ID
The 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.Top
The top-most available memory location

The Routines

WB.Init
Discover what type of Flash and Initialise the internal pointers.

WB.Format
Erase 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.WaitBusy
Wait 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.SetPage
Set 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
'*****************************************************************