Author Topic: AVR: Inline assembly to read LUT  (Read 2341 times)

0 Members and 1 Guest are viewing this topic.

Offline VinzzTopic starter

  • Newbie
  • Posts: 1
AVR: Inline assembly to read LUT
« on: June 18, 2019, 07:28:31 am »
Hi,

I am developing a FM RDS encoder based on ATMega8 (RDS stream generator) and ATTiny2313 (RDS biphase modulator).

To generate the biphase signal modulated on 57kHz I have stored a lookup table in FLASH which holds the precalculated waveform which is output using a 8bit R2R D/A connected on PORTB.

I have setup a timer which fires a interrupt with a interval of 8.77uS.  The interrupt takes a value from the LUT and outputs it on PORTB:

Code: [Select]

const uint8_t ubTable[48] PROGMEM = {
{   //1-1-1
0x82,0x91,0xA1,0xB1,0xC0,0xCE,0xDA,0xE5,
0xEE,0xF6,0xFB,0xFE,0xFF,0xFE,0xFB,0xF5,
0xEE,0xE5,0xDA,0xCD,0xBF,0xB0,0xA1,0x90,
0x80,0x6F,0x5F,0x4F,0x40,0x32,0x26,0x1B,
0x12,0x0A,0x05,0x02,0x01,0x02,0x05,0x0B,
0x12,0x1C,0x27,0x33,0x41,0x51,0x61,0x72
}

volatile uint8_t ubWavePosition = 0;

ISR (TIMER1_OVF_vect)
{
    PORTB = pgm_read_byte(&ubTable[ubWavePosition++]);

    if (ubWavePosition == 48) ubWavePosition = 0;
}

Reading a value from flash like this is not fast enough and using inline assembly might improve speed. Unfortunately  I`m not into AVR assembly.

Can someone point me to the right direction using inline assembly to output the waveform?

Thank you.
« Last Edit: June 18, 2019, 07:39:41 am by Vinzz »
 

Offline Fire Doger

  • Frequent Contributor
  • **
  • Posts: 301
  • Country: gr
  • Stefanos
Re: AVR: Inline assembly to read LUT
« Reply #1 on: June 18, 2019, 07:42:52 am »
 

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2861
  • Country: nz
Re: AVR: Inline assembly to read LUT
« Reply #2 on: June 18, 2019, 08:31:39 am »
Can someone point me to the right direction using inline assembly to output the waveform?

Thank you.

Maybe going full assembly on it would be a bit harsh. I think this would be a really good project to use a little inline assembly.

Assuming you don't want the micro to do anything but generate this signal I would code a really simple loop, that you know will run in a fixed time (so no 'if()" statements):

Code: [Select]
    for(i = 0; i < 48; i++) {
       index_next[i] = i+1;
    }
    index_next[47] = 0;

    while(1) {
        index = index_next[index];
        PORTB = lookup[index]
    }

Have a look at the compiler output, and check that it doesn't do something stupid like unroll the loop.

Then just make sure you disable interrupts before you hit that loop, and add NOPs into the loop until you have the time per iteration that you need.
Code: [Select]
    for(i = 0; i < 48; i++) {
       index_next[i] = i+1;
    }
    index_next[47] = 0;

    asm volatile(
      " cli \n\t" // Disable interrupts
    }
    while(1) {
       asm volatile(
          " NOP \n\t" // A pause
          " NOP \n\t" // A pause
          " NOP \n\t" // A pause
          " NOP \n\t" // A pause
        }
        index = index_next[index];
        PORTB = lookup[index];
    }

You could then enhance the loop a little to allow it to send data streams, removing NOPs to keep timing.
« Last Edit: June 18, 2019, 09:45:38 am by hamster_nz »
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 

Offline magic

  • Super Contributor
  • ***
  • Posts: 8070
  • Country: pl
Re: AVR: Inline assembly to read LUT
« Reply #3 on: June 18, 2019, 09:21:23 am »
I would start with loading that table to RAM and replacing flash reads with array accesses. At one sample per 9µs that's still tens of cycles per sample even at 10MHz clock, should be enough.
I wouldn't trust C code for timing, it will run faster or slower when you compile it next year with some other compiler version. If you need accurate frequency it's either pure ASM with good understanding of AVR pipelining (which thankfully is simple) or timers.
 

Offline mfro

  • Regular Contributor
  • *
  • Posts: 226
  • Country: de
Re: AVR: Inline assembly to read LUT
« Reply #4 on: June 18, 2019, 09:39:00 am »
as it appears you are walking through the table sequentially anyway, why don't you just (pre)read the *next* value into a static or global variable at the end of the interrupt?

This way you'd have that value available right away when you need it (and I would assume you won't be able to optimize
Code: [Select]
PORTB = static_value; by much with inline assembly)
Beethoven wrote his first symphony in C.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: AVR: Inline assembly to read LUT
« Reply #5 on: June 18, 2019, 10:02:49 am »
At 20 MHz, each cycle takes 0.05µs, so 8.77µs is about 175 cycles.  On ATtiny2313, the PORTB update including the lookup takes something like 15 to 20 cycles. Interrupt overhead is about 20-30 cycles in practice (depending on how many registers need to be pushed to the stack), so using an ISR might or might not work.

For reference, the following code:
Code: [Select]
#define __AVR_ATtiny2313__
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>

static const unsigned char  table[48] = {
    0x82, 0x91, 0xA1, 0xB1, 0xC0, 0xCE, 0xDA, 0xE5,
    0xEE, 0xF6, 0xFB, 0xFE, 0xFF, 0xFE, 0xFB, 0xF5,
    0xEE, 0xE5, 0xDA, 0xCD, 0xBF, 0xB0, 0xA1, 0x90,
    0x80, 0x6F, 0x5F, 0x4F, 0x40, 0x32, 0x26, 0x1B,
    0x12, 0x0A, 0x05, 0x02, 0x01, 0x02, 0x05, 0x0B,
    0x12, 0x1C, 0x27, 0x33, 0x41, 0x51, 0x61, 0x72
};

static unsigned char  timer1_index = 0;
ISR (TIMER1_OVF_vect)
{
    PORTB = pgm_read_byte(table + timer1_index);
    timer1_index++;
    if (timer1_index >= sizeof table / sizeof table[0])
        timer1_index = 0;
}
using avr-gcc-4.9.2 -mmcu=avr25 -O2 compiles the ISR into
Code: [Select]
__vector_5:
    push  r1                    ; Interrupt prologue
    push  r0                    ;
    in    r0, __SREG__          ;
    push  r0                    ;
    clr   __zero_reg__          ;
    push  r24                   ;
    push  r30                   ;
    push  r31                   ;

    lds   r30, timer1_index     ; Load table+timer1_index
    ldi   r31, 0                ; into Z (r31*256+r30)
    subi  r30, lo8(-(table))    ;
    sbci  r31, hi8(-(table))    ;

    lpm   r30, Z                ; Load flash *Z into r30

    out   0x18, r30             ; PORTB = r30

    lds   r24, timer1_index     ; Update timer1_index
    subi  r24, lo8(-(1))        ;
    cpi   r24, lo8(48)          ;
    brlo .L10                   ; If not over, jump to .L10

    sts   timer1_index,__zero_reg__ ; Clear timer1_index to zero

    pop   r31                   ; Interrupt epilogue
    pop   r30                   ;
    pop   r24                   ;
    pop   r0                    ;
    out   __SREG__,r0           ;
    pop   r0                    ;
    pop   r1                    ;
    reti                        ; Return from interrupt

.L10:
    sts   timer1_index, r24     ; Save updated timer1_index.

    pop   r31                   ; Interrupt epilogue
    pop   r30                   ;
    pop   r24                   ;
    pop   r0                    ;
    out   __SREG__,r0           ;
    pop   r0                    ;
    pop   r1                    ;
    reti                        ; Return from interrupt
You do not need to know assembly to see that there isn't much there you can avoid.  On ATtiny2313, LPM takes 3 cycles, so moving the array to RAM would save you just 1 cycle per sample.

So, if you are having difficulty with that code not being fast enough, it is not because there is something wrong with the C code; it is just that there is too much overhead in using an interrupt in the first place.

Let's look at the same code, but written as a loop, with a 8µs delay:
Code: [Select]
#define __AVR_ATtiny2313__
#define F_CPU 20000000
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <util/delay.h>

static const unsigned char  table[48] = {
    0x82, 0x91, 0xA1, 0xB1, 0xC0, 0xCE, 0xDA, 0xE5,
    0xEE, 0xF6, 0xFB, 0xFE, 0xFF, 0xFE, 0xFB, 0xF5,
    0xEE, 0xE5, 0xDA, 0xCD, 0xBF, 0xB0, 0xA1, 0x90,
    0x80, 0x6F, 0x5F, 0x4F, 0x40, 0x32, 0x26, 0x1B,
    0x12, 0x0A, 0x05, 0x02, 0x01, 0x02, 0x05, 0x0B,
    0x12, 0x1C, 0x27, 0x33, 0x41, 0x51, 0x61, 0x72
};

void loop(void)
{
    unsigned char  index = 0;

    while (1) {
        PORTB = pgm_read_byte(table + index);
        if (++index >= sizeof table / sizeof table[0])
            index = 0;
        _delay_us(8);
    }
}
which generates
Code: [Select]
loop:
    ldi  r24, 0             ; r24 = index
.L3:
    mov  r30, r24           ; r31*256+r30 = Z = table+index
    ldi  r31, 0
    subi r30, lo8(-(table))
    sbci r31, hi8(-(table))

    lpm  r30, Z             ; r30 = *Z from progmem
   
    out  0x18, r30          ; PORTB = r30

    subi r24, lo8(-(1))     ; index++
    cpi  r24, lo8(48)       ; if index is too high,
    brlo .L2                ;   jump to .L2
    ldi  r24, 0             ; otherwise clear index
.L2:

    ldi  r25, lo8(53)       ; _delay_us(8);
1:                          ;
    dec  r25                ;
    brne 1b                 ;
    nop                     ;

    rjmp .L3
i.e., the _delay_us(8) simplifies to a loop that simply decrements a value (from 53 to 0), followed by a single no-operation.

If you have interrupts enabled when this code is running, you'll see occasional glitches in the output (if using e.g. an oscilloscope).  This is because interrupts will interrupt the loop, and do other stuff while samples should be output.
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4668
  • Country: us
Re: AVR: Inline assembly to read LUT
« Reply #6 on: June 18, 2019, 06:06:07 pm »
I’m not convinced that asm will be faster.   Pgm_read_byte() is already a macro that uses inline asm...
Show us your disassembled object code...


(Oops - nominal animal already wrote a much more detailed response along the same lines...)

 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf

 

- - -
-->