Path:
Home =>
AVR-EN =>
Assembler introduction => Ring-buffer
(Diese Seite in Deutsch:
)
Beginner's introduction to AVR assembler language
A ring-buffer in assembler language
What if you have two or more sources of data generation and you'd like to
transmit the data over a serial interface, such as a synchronized or
unsynchronized transmitter? Then you need a control over the data generation
process: the different data come in at different times, and have to be
stored (and later transmitted) in a row. Whenever data comes in, the
transmitter has to be started (if not already active), when all data
has been sent the transmitter has to be turned off in a controlled
procedure: if asynchronous the transmit interrupt has to be disabled
and the All-Sent-Interrupt has to be enabled. If the All-Sent-Interrupt
comes in, the Transmitter-Ready-Flag has to be finally set.
All this needs a space, where the generation routines can put their data
into a storage place: whenever those data will be send, will not influence
the generation process any more. And: the send-process of the data will
also be independant from the generation process.
Of course: the generation of data must not be faster than the
send-procedure allows. Otherwise a buffer overflow would occur: the storage
space does not provide enough space for the collected data. To avoid this
kind of overrun: estimate the data generation speed at its fastest mode
and make sure that the send-speed exceeds this.
As an example for this we assume that
- an ADC measures an analog input voltage every 10 milli-seconds and
converts this to an "A", followed by a four-digit-decimal and
a Carriage-Return- plus a Line-Feed-character, plus
- a PCINT-interrupt measures the time between two rising edges of an
input pin with a maximum of 100 Hz, converts this to a
"P", followed by a 7-digit number as µs, with two
thousand-separators and CR/LF at the end.
As both events can occur at any time, measuring and sending the messages can
not be organized in a serial way because waiting until all data has been sent
would block the other event for a too long time.
The first part sends 7 characters, with 8 bits each, 100 times per second. The
second part sends a maximum of 10 characters with 8 bits each whenever the
PCINT pin has changed its polarity twice. At a maximum of 100 Hz this
can be 100 times per second (the transmitted numbers at 100 Hz are
shorter if the leading zeros are omitted). Together 700 + 1,000 bytes are to
be send, makes 8 * 1,700 bits per second. A baud rate of 9k6 would be
slightly insufficient, 19.2 kBd would fit to the max. In async mode the
additional start- and stop-bits would require slightly more bits per second,
but would also fit into the 19k2 scheme.
The size of the ring-buffer in bytes can be limited to the two parts, which
means a minimum of 7 + 10 = 17 characters. That size can be implemented in
nearly any AVR device (not in a AT90S1200, of course). The following shows
how to do it.
What is a ring-buffer?
A ring-buffer is a number of n bytes in SRAM. If writing or reading reaches
the end of the buffer area, it simply restarts from the beginning. That makes
it a round ring.
Two pointers into the ring are necessary: they point to the input- and the
output-addresses. Whenever one byte is written, the input address increases.
Whenever one byte has been read, the output address increases. Both pointers
are 16 bit wide, so the whole SRAM can be addressed and used as a ring-buffer.
The two pointers can be located either in two register pairs, such as X and
Y, if freely available, or they can be located in SRAM. If you are short in
registers you can use the second location.
The buffer is created in AVR assembly language as follows:
.dseg
.org SRAM_START
sRingBuffer:
.byte (RAMEND - sRingBuffer - 15)
sRingBufferEnd:
This uses the whole SRAM as ring-buffer, except 16 bytes are left for
stack operation.
If you want to use SRAM for the two pointers as well, the formulation
for that would be:
.dseg
.org SRAM_START
sIn:
.byte 2 ; Input address to ring-buffer
sOut:
.byte 2 ; Output address of ring-buffer
sRingBuffer:
.byte (RAMEND - sRingBuffer - 15)
sRingBufferEnd:
This places the two 16-bit pointers in front of the buffer area.
At start-up the two pointers point to the beginning of the ring-buffer.
This can be done with the following code:
ldi R16,Low(sRingBuffer) ; LSB first
sts sIn,R16 ; to input pointer
sts sOut,R16 ; to output pointer
ldi R16,High(sRingBuffer) ; MSB next
sts sIn+1,R16 ; to input pointer
sts sOut+1,R16 ; to output pointer
It is not necessary to init the buffer area as such, because only
positions that have been written prior can be read afterwards.
This shows the ring-buffer after its initiation, as simulated with
avr_sim.
The first four bytes of the SRAM hold the two pointers (in dark green
frames).
The buffer area (in blue frames) has been written to zero here to
demonstrate its exact extent.
The 16 bytes at the end of the SRAM (light green frame) are reserved
for stack operation.
Writing to the ring-buffer
Writing to the ring buffer writes the byte to the location where
the input pointer points to:
InputBuffer:
lds ZL,sIn ; Read the input pointer
lds ZH,sIn+1
st Z+,R16 ; Write character in R16 to buffer and increase address
When writing the byte, the address in Z was already increased and
points to the next location in the buffer. Now it has to be checked
whether the pointer is now outside the buffer area:
cpi ZL,Low(sRingBufferEnd) ; LSB at ring-buffer end?
brne InputBuffer1
cpi ZH,High(sRingBufferEnd) ; MSB at ring-buffer end?
brne InputBuffer1
ldi ZH,High(sRingBuffer) ; End reached, restart at the beginning
ldi ZL,Low(sRingBuffer)
InputBuffer1:
Before writing the new pointer address back to sIn and
sIn+1 it has to be checked whether the output address is
reached. If that is a case, the write process would lead to an
overrun, because not enough characters have been sent. If that
is the case, the routine return with the carry flag set. If not
the carry flag is cleared and the increased pointer address is
written to the input pointer.
lds R16,sOut ; Read LSB output address
cp R16,ZL ; Compare with the LSB of the input address
brne InputBuffer2
lds R16,sOut+1 ; Read MSB output address
cp R16,ZH ; Compare with the MSB of the input address
brne InputBuffer2
sec ; Overflow of the buffer, return with carry flag set
ret
InputBuffer2:
sts sIn,ZL ; Store new input address, LSB
sts sin+1,ZH ; dto., ZH
clc ; Clear carry, input succesful
ret
That is already all: the data generator has to call the
InputBuffer routine with R16 byte-by-byte to transfer his
data or text to the buffer.
Here one byte with 0x01 has been written to the buffer.
The input pointer has been increased by one and now points
to the next location in the buffer. The output pointer
has not been touched, and the stack area is still protected.
Repeatedly the whole buffer has been floated with increasing
data until the carry flag says "Full buffer". The
last byte has been written, but the input pointer hasn't been
increased: it still points to loaction 0x00CF. Subsequent
writes (without removing bytes by read operations) would
overwrite the last byte, the written data will get lost.
So your write routines should always check whether a set
carry indicates a buffer overflow.
And: your write routines should, after the last data byte or
character has been written, check whether the transmission is
already started. If not: it is a good idea to start it then.
Reading from the ring-buffer
Reading starts with reading the output pointer. First it has
to be checked that the output pointer does not point to the
input pointer. If so, no new data is in the buffer and the
carry flag is set.
The code looks like that:
OutBuffer:
lds ZL,sOut ; Read LSB of the output pointer
lds ZH,sOut+1 ; dto., MSB
lds R16,sIn ; Read LSB of the input pointer
cp R16,ZL ; Compare LSBs
brne OutBuffer1
lds R16,sIn+1 ; Read MSB of the input pointer
cp R16,ZH ; Compare MSBs
brne OutBuffer1
sec ; No data in buffer
ret
OutBuffer1:
If the LSB and MSB of the pointers differs, a byte is read
from the buffer. The output pointer is increased, and after
checking for the end of the buffer, is written back. In
assembly language:
OutBuffer1:
ld R16,Z+ ; Read byte at address, increase address
cpi ZL,Low(sRingBufferEnd) ; Compare LSB with end
brne OutBuffer2
cpi ZH,High(sRingBufferEnd) ; Compare MSB with end
brne OutBuffer2
ldi ZH,High(sRingBuffer) ; Restart at the beginning
ldi ZL,Low(sRingBuffer)
OutBuffer2:
sts sOut,ZL ; Store new position, LSB
sts sOut+1,ZH ; dto., MSB
clc ; Carry clear, data in R16 ok
ret
Again, the simulation with
avr_sim
yields this when reading the first byte:
- The carry flag is clear, the data in R16 is valid.
- The data in R16 is 0x01, which is the byte in address
0x0064.
- The pointer in 0x0063:0x0062 is increased, allowing the
input routine to re-use the next byte for write access.
This is the situation if we read all stored data until the
carry flag is set. The last byte read without carry has been
written to R17: it is of course 0x6B. The address pointer in Z
points to 0x00CF, which is the location where the next write
operation would take place.
Software for the ring-buffer
The complete assembler software for accessing the ring-buffer
can be downloaded from here.
That file can be simulated.
Conclusion of the ring-buffer
That was rather simple: the SRAM and the SREG's carry flag provide
all necessary hardware for a ring-buffer and for managing its
write- and read-accesses. All you need is a data register and a
pointer register pair and some SRAM. Simple, effective, reliable
software, useful for many different purposes for cases where data
generation and transmission need separation.
Now: try this in a so-called "higher" language. It is
getting more complicated than these simple lines in assembler:
- Which data type should the buffer array hold? Is your data
in Bytes, are they character strings or rather 64-bit floating
point numbers?
- Reading from the buffer needs two components to be returned:
a boolean and a byte (or whatever data type you assigned).
- It is rather complicated to check that your transmission speed
fits the maximum of your data generation. So-called
"Higher" languages tend to hide their effective
execution speed, nothing is known reliably.
None of those problem appear in assembly language. It is all under
your control, and no compiler disturbs your creative design.
To the page top
©2022 by http://www.avr-asm-tutorial.net