Simple Ring Buffer

#1
Hi all, I am a relative Picaxe noob, and this is my first real post on the forum....

This may be such a common routine that it hasn't merited prior, specific discussion, but I could not find any information on the forum while searching for the terms "ring buffer, circular buffer, FIFO, LILO, etc." As part of my weather station project, I want to know the "most recent X values" of a particular sensor reading, with "now" being the first value. Specifically, I want to know the following:

1. Max wind gust in the last minute, and last 10 minutes,
2. Average wind direction for the same time periods
3. Last 24 (one-hour) barometer readings

Below is a simple "Ring Buffer" routine that I came up with that will handle these needs. I am running it on a 20x2 so I can start my EEPROM pointers at zero. For non-X2 chips, you would need to consult the EEPROM command doc for usable locations, or use one of the other types of available memory. I welcome comments/criticisms, and any suggestions for more efficient code or methods.

Thanks to all for the wealth of information on this site...., Greg

Code:
#picaxe 20x2
#terminal 9600
#no_data
#no_table

;Simple Ring Buffer for 10 sequential readings of a byte value
;New data enters one end, old data out the other end
;Simply modifiy counter ranges for other data (array) sizes
;Should work with word variables with slight tweaking
; Greg Derda, "doppler", 4/30/2018

symbol data_pointer = b0 ;primary data-array location pointer (e.g.,  0 - 9)
symbol temp_pointer = b1 ;temporary data-array location (e.g., 10 - 19)
symbol in_data = b2 ;incomming data
symbol temp_data = b3 ;temporary data
symbol out_data = b4 ;output data

main:

inc in_data  ;Debug, for generating test incomming data

;Read and copy existing data-array to temporary-array before writing new in_data
for data_pointer = 0 to 9 ;change to indicate data-array range
	read data_pointer, temp_data
		temp_pointer = data_pointer + 10 ;offset to first location in temp data array
	write temp_pointer,temp_data
	write 0, in_data ;now write new data to first location of data array
next data_pointer

;Now write temp-data back to data-array, starting at second element 
for temp_pointer = 10 to 19 ;change to indicate temporary-data range
	data_pointer = temp_pointer - 9 ;start at element 1, not overwriting elelment 0
	read temp_pointer, temp_data
	write data_pointer, temp_data
next temp_pointer	

;Debug Output, to watch the pointer and data movement
sertxd("ptr / data",13,10)
for data_pointer = 0 to 9
	read data_pointer, out_data
	sertxd(#data_pointer,"   /   ",#out_data,13,10)
next data_pointer
sertxd (13,10)
pause 1000  ;Debug, to slow the output down a bit
; End Debug Output

goto main
 
Last edited:
#2
Here is one example of my usage. My remote weather station does three, 3-second loops for the measurement of wind speed. It averages those three readings for a wind speed value, and picks the highest of the three for a wind gust value. Those three loops combined with other routines add up to a once per 10-second broadcast timing (serial out to an Arduino/RFM69HCW assembly for a 100m transmission to the house). For my one-minute wind gust value, I am using a data array of six values (6 x 10sec = 1 minute), and adding the following to my ring buffer code (with appropriate data array size adjustments):


Code:
symbol max_value = b6

for data_pointer = 0 to 5
	read data_pointer, out_data	
		if out_data > max_value then
			max_value = out_data
		endif
next data_pointer

max_value = 0
 

hippy

Technical Support
Staff member
#3
One issue with your code is that you seem to be shuffling your samples around in EEPROM and that only has a limited write capability. Possibly more than you will need but it's best to WRITE as little as possible.

Avoiding multiple writes is usually done by only writing the latest data and keeping a separate variable pointing to where that data was written, determining which were the latest from the pointer. That avoids having to copy or shuffle data.

Keeping the data in RAM or Scratchpad ( using PEEK/POKE, GET/PUT, @bptr/@ptr ) avoids the issue of EEPROM wearing out. That won't retain the data previously read across power-cycles but it is often acceptable to build the data up from noting and it likely will have settled down and be correct when one comes to use the data later.
 
#4
Thank you for the great insight and suggestions hippy! It occurred to me in a "Duh!" moment (after reading your reply) that I don't need the readings in chronological order to obtain the maximum wind gust and average wind direction, I only need the most recent X values. Simply writing the values using a moving pointer that loops back to the beginning will do that. I came up with the following, using 5 data points as an example. I'm guessing that there may be a more elegant way of doing it, but this works:
Code:
symbol data_pointer = b0
symbol temp_pointer = b1
symbol in_data = b2
symbol out_data = b3
data_pointer = 59
	
main:
	inc in_data ;debug data
	inc data_pointer

	poke data_pointer, in_data

		if data_pointer = 64 then
			data_pointer = 59
		endif
		
	for temp_pointer = 60 to 64
		peek temp_pointer, out_data
		sertxd(#temp_pointer," / ",#out_data,13,10) ;debug output
	next temp_pointer
		sertxd(13,10) ;debug output
		
pause 1000
goto main
For chronological readings, like a 24hr barograph, I'll have to "press" my simple brain as to how to have a pointer handle the wrap at the end of the array, for both writing and reading. In the meantime, I simply swapped 'read' and 'write' in my original code, with 'peek' and 'poke, and moved the pointers to 60 and above. I sort of like making computational things work a little, following a stigmatizing experience I had as a graduate student (many years ago): After two years and thousands of hours of field and lab work, when it came time to analyze my data using a popular computer program for my type of research, it only took an old IBM 286 PC about one second to do the job :-0
 

hippy

Technical Support
Staff member
#5
Exactly how I meant. Elegance is always secondary to what works but things you can consider using are 'clever maths' to keep the pointer within the right location areas, for example your 'increment pointer' so it's always between 60 and 64 inclusive without the IF ...

Code:
data_pointer = data_pointer + 1 // 65 Min 60
And you can also use '@bptr' for the data_pointer to avoid PEEK and POKE and even reduce code ...

Code:
@bPtr = latest_data
bPtr = bPtr + 1 // 65 Min 60
Code:
For bPtr = 60 To 64
  SerTxd( "Location ", #bPtr, " holds ", #@bPtr, CR, LF )
Next
That's not always overly useful because it messes with what bPtr is. Probably best to keep bPtr for last data location written.

It can get a bit tricky for chronological data but that can be handled with more 'clever maths' ...

Code:
Symbol latest_data  = b0
Symbol item         = b1
Symbol loc          = b2
Symbol samples      = b3
Symbol average      = w2 ; b5:b4

; Set some data

latest_data = 10 : Gosub AddData
latest_data = 20 : Gosub AddData
latest_data = 30 : Gosub AddData
latest_data = 40 : Gosub AddData
latest_data = 50 : Gosub AddData
latest_data = 60 : Gosub AddData
latest_data = 70 : Gosub AddData

; Now holds 5 items : 30,40,50,60,70

; Show location contents

For loc = 60 To 64
  Peek loc, latest_data
  SerTxd( "Location ", #loc, " holds ", #latest_Data, CR, LF )
Next
SerTxd( CR, LF )

; Show chronological order, last data first 

loc = bPtr
For item = 1 To 5
  Peek loc, latest_data
  SerTxd( "Item ", #item, " location ", #loc, " holds ", #latest_Data, CR, LF )
  loc = loc - 60 - 1 Max 4 + 60
Next
SerTxd( CR, LF )

; Show Average 

average = 0
For loc = 60 To 64
  Peek loc, latest_data
  average = average + latest_Data
Next
average = average / samples
SerTxd( "Average ", #average, CR, LF )

End

AddData:
  bPtr = bPtr + 1 // 65 Min 60
  @bPtr = latest_data
  samples = samples + 1 Max 5
  Return
 
#6
This is great stuff, thank you hippy! I will have to study your chronological code a bit more to fully understand it, but I will definitely use your “average” routine in my future coding.
 
Top