Path: Home => AVR-EN => Assembler introduction => Ring-buffer    (Diese Seite in Deutsch: Flag DE) Logo

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
  1. 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
  2. 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?

Construction of 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:

  .byte (RAMEND - sRingBuffer - 15)

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:

  .byte 2 ; Input address to ring-buffer
  .byte 2 ; Output address of ring-buffer
  .byte (RAMEND - sRingBuffer - 15)

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.

The ring buffer after init 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:

  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)

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
  sts sIn,ZL ; Store new input address, LSB
  sts sin+1,ZH ; dto., ZH
  clc ; Clear carry, input succesful

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.

Writing a byte 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.

Writing the complete buffer until carry 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.

The carry flag indicates buffer overflow 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:
  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

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:

  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)
  sts sOut,ZL ; Store new position, LSB
  sts sOut+1,ZH ; dto., MSB
  clc ; Carry clear, data in R16 ok

Reading the first byte from the buffer Again, the simulation with avr_sim yields this when reading the first byte:

Reading all bytes from the buffer until carry 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: 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