Asynchronous EEPROM Write Function for AVR

The TR-909 clone firmware I’m currently developing writes sequence patterns to eeprom. The avr-libc library provides eeprom writing function, but it is synchronous.

https://www.nongnu.org/avr-libc/user-manual/group__avr__eeprom.html

However, many of the real-time processing in the firmware are done without interrupts, then these processes stall during the slow (synchronous) function. So I decided to develop the asynchronous version of eeprom writing function by my own.

The Functional Specification

The firmware does not have its task scheduler, so the EEPROM writing has to be done by polling. Following points have to be checked in the polling function:

  • whether there is data to write
  • whether the EEPROM writer is ready to accept new request

Since this function is meant to be used to save sequence patterns, the data is a byte array. So I use a variable to keep the data address to save next. If the address is negative, the data is not available. The signature of the async function and polling function looks as follows:

extern eeprom_write_async(uint16_t address, uint8_t data);

static int16_t address = -1;
void Poll() {
  if (address < 0 || !eeprom_is_ready()) {
    return;
  }
  eeprom_write_async(ADDRESS_OFFSET + address, data[address]);
  if (++address >= MAX_ADDRESS) {
    address = -1;
  }
}

where eeprom_write_async() is the asynchronous EEPROM writing function. This function places a request to write one byte at the specified address then returns immediately. The function eeprom_is_ready() is a macro provided by avr-libc that checks whether the device is ready to write to the EEPROM.

Procedure to Write to EEPROM

The procedure to write a byte to EEPROM is as follows when a boot loader is not used. See the device’s data sheet for details.

  1. Wait until EEWE bit in the EEPROM control register EECR becomes zero.
  2. Write new EEPROM address to EEAR.
  3. Write new EEPROM data to EEDR.
  4. Write a logical one to the EEMWE bit while writing a zero to EEWE in EECR.
  5. Within four clock cycles after setting EEMWE, write a logical one to EEWE.

The C code example in the datasheet is:

void EEPROM_write(unsigned int uiAddress, unsigned char ucData)
 {
  /* Wait for completion of previous write */
  while (EECR & (1 << EEWE))
;
  /* Set up address and data registers */
  EEAR = uiAddress;
  EEDR = ucData;
  /* Write logical one to EEMWE */
  EECR |= (1 << EEMWE);
  /* Start eeprom write by setting EEWE */
  EECR |= (1 << EEWE);
}

Differences in the async function to be written are:

  • You must not wait for EEWE becoming 0 which would cause blocking in the function.
  • Interrupts have to be prevented since the requirement of “proceed within 4 clock cycles after setting EEMWE” would be broken by them.

Following was the initial implementation of the function.

void eeprom_write_async(uint16_t address, uint8_t data) {
  EEAR = uiAddress;
  EEDR = ucData;
  cli();
  EECR |= (1 << EEMWE);
  EECR |= (1 << EEWE);
  sei();
}

This function was unstable for some reason. I am not using debugger in this development, it’s hard to troubleshoot a timing issue of a C function. I’ve translated the function to assembly.

Construct of an assembly function used by C/C++

Following is the synchronous EEPROM writing function in the ATMega 64 datasheet. But this actually does not work with AVR-GCC.

EEPROM_write:
  ; Wait for completion of previous write
  sbic EECR,EEWE
  rjmp EEPROM_write
  ; Set up address (r18:r17) in address register
  out EEARH, r18
  out EEARL, r17
  ; Write data (r16) to data register
  out EEDR,r16
  ; Write logical one to EEMWE
  sbi EECR,EEMWE
  ; Start eeprom write by setting EEWE
  sbi EECR,EEWE
  ret

The problem is registers for the function arguments; registers r17 and r18 are used for the address, register r16 is used for the data. However, they are different from what a function for avr-gcc should be.

First of all, in C/C++ languages, the way to use binary objects is standardized for reusability. Binary objects are, for example, functions, global variables, etc. The standard is called “Application Binary Interface (ABI)”. In an ABI, the rule to use registers in calling a function is “Calling Convention”. A calling convention decides which registers would be used for arguments and the return value.

According to the AVR-GCC Calling Convention, arguments should be allocated left to right, r25 to r8. All arguments are aligned to start in even-numbered registers (odd-sized arguments, including char, have one free register above them).

The register assignments for the function

The register assignments for the function eeprom_write_async() are:

  • uint16_t address: r24:r25
  • uint8_t data: r22

eeprom_write_async Source Code

 #define __SFR_OFFSET 0
 #include <avr/io.h>

.global eeprom_write_async
eeprom_write_async:
    ; Disable interrupts
    cli
    ; Set up the address (r24:r25) to address register
    out     EEARH, r25
    out     EEARL, r24
    ; Write data (r22) to Data register
    out     EEDR, r22
    ; Write logical one to EEMWE
    sbi     EECR, EEMWE
    ; Start eeprom write by setting EEWE
    sbi     EECR, EEWE
    ; Enable interrupts
    sei
    ret

-> Github

There are a few more notes apart from following the AVR-GCC calling convention:

  • Use the file extension .S. With this extension, the compiler pre-processes the source code which enables us to include files.
  • Include <avr/io.h>. The compiler would fail to recognize control register names without this.
  • Define __SFR_OFFSET 0 before including <avr/io.h>.

The background for the last note is explained in the file sfr_def.h of the avr-libc source code:
https://github.com/vancegroup-mirrors/avr-libc/blob/master/avr-libc/include/avr/sfr_defs.h

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.