Saturday, March 21, 2020

0000 0000 0010 0111

A dabble in AVR Assembly (I)

I have a project that I have been working on for a few years now. It entails a small 2V solar panel collecting the meagre rays we get down here in Tasmania (42.9° S, 147.3° E), and then with the help of a QX5252 IC charging an NiMH battery. 

The battery discharges at night to an Attiny13A running anaemically at 128kHz on low voltage and minimal current providing "candle" light via some beautiful 5050 yellow LEDs.

With a few hours of winter light on the panel, the candle runs pretty well through the night - a triumph of resource allocation and what a colleague of mine likes to call "running on the energy of a warm fart".

Along the way there have been quite a few challenges. Let's start with the whole idea of an AVR running at 128kHz, which according to the MicroCore documentation should be quite fine for the Attiny13A. That said, I "bricked" a few chips and spent countless hours trying to use this frequency reliably, even purchasing an "unbricker" which gave me my chips back, but did not deal with the cause of the problem.

I actually gave up for a year or so (using instead a more reliable 1.2MHz as internal clock on the chips) before I happened upon a blog by Łukasz Podkalicki which exactly described the solution - what a treasure. The key was having a USBasp geared so slow that it is able to converse with the μC at this speed. I have configured two such USBasp devices and have labelled them "The Slow Guys" - they actually take a noticeably longer time to upload to the μC.


A standard Attiny13A chip galloping along at 9.6MHz consumes 4.3mA doing night-time candley-goodness, but then the current requirements reduce to 1.3mA at 1.2MHz, and finally a miserly 400μA at 128kHz. All of these savings have a direct impact on the length of time the candle runs during the big dark.

Setting the clock fuses using this very useful site results in a command line for avrdude along the lines of "avrdude -c usbasp -p t13 -P /dev/ttyS0 -b 250 -U lfuse:w:0x7b:m -U hfuse:w:0xff:m". That's all a bit scary but I actually don't use avrdude on the command line anyway, but rather prefer to use the GUI version AVRdudess to load the hex file, and the Arduino IDE to set the fuses.

It was a remarkable experience the day I was able to get a blinky sketch running correctly at 1Hz from an Attiny13A ambling along 128kHz.

I then became consumed with the idea of a deep sleep (and hence minimal current) for my Attiny13A babies during the day when not needed to be "candles". Scouring the interwebs revealed some wonderful advice and tutorials from Nick Gammon. So I dived in with firstly an ATmega328p powered Arduino, then a bare Atmega328p and eventually an Attiny85.

When it came to the Attiny13A I could certainly get the creature to sleep well with mods based on Nick's work, but I was by that time reading the datasheet late at night and thinking that perhaps <gulp> assembler was the next step.

Now I hadn't written any assembler since the late 1990s when I was in way too deep writing Motorola 68000 code on the Amiga. First I tried to dive straight in and modify/combine various bits of AVR code found by trawling online, but in the end I bit the bullet and wrote functional lines of code for every single assembler instruction found for the Attiny13A, The result was my poo-litzer prize winning tome "AVR Assembler by Example". 

Despite not topping the best seller charts, I love this little booklet because it now means that I can more easily understand how to put the Attiny13A to sleep, plus heaps of other cool stuff that seems only possible down at the bare metal level of programming.

I have to give a huge shout-out here to Gerhard Schmidt and his simulator software which has meant that I can compile and debug avr asm stuff on the fly, before committing the code (via the generated hex file) to the chip (see video below).

My trial program for deep sleep blinks an LED for one second, then sleeps for one minute. Here is the code:


;
    .nolist
    .include "tn13adef.inc" ; Define device ATtiny13A
    .list

; **********************************
;        H A R D W A R E
; **********************************

; Device: ATtiny13A, Package: 8 - pin - PDIP_SOIC
;
;           _________
;        1 /         | 8
;      o-- | RESET  VCC | --o
;      o-- | PB3    PB2 | --o
;      o-- | PB4    PB1 | --o
;      o-- | GND    PB0 | --o
;       4 | __________ | 5
;

; **********************************
;         C O D E
; **********************************

    .cseg
    .org 000000

; **********************************
; R E S E T  &  I N T - V E C T O R S
; **********************************
    rjmp Main               ; Reset vector
    reti ; INT0
    reti ; PCI0
    reti ; OVF0
    reti ; ERDY
    reti ; ACI
    reti ; OC0A
    reti ; OC0B
    rjmp mytimer_ISR        ; my interrupt
    reti ; ADCC

; **********************************
;  I N T - S E R V I C E   R O U T .
; **********************************

mytimer_ISR:

    ;  sleep is interrupted, program returns
    reti

; **********************************
;  M A I N   P R O G R A M   I N I T
; **********************************
;
Main:
    ldi r16, Low(RAMEND)
    out SPL, r16              ; Init LSB stack pointer

    ; output mode for PB4, input for the rest
    ldi r16, 0b00010000
    out DDRB, r16

    ; turn on PB4, enable internal pullups
    ; on the rest of the pins
    ldi r16, 0b11111111
    out PORTB, r16

; **********************************
;    P R O G R A M   L O O P
; **********************************

Loop:

    rcall waitabit            ; one second
    rcall waitalot            ; one minute

    rjmp loop

waitabit:

    ; reset watchdog timer
    wdr

    ; 1 second timer
    ldi r16, 0b01000110       ; see datasheet
    out WDTCR, r16

    rcall sleepnow
    rcall flipbit
    ret

waitalot:

    ; reset watchdog timer
    wdr

    ; 8 second timer
    ldi r16, 0b01100001       ; see datasheet
    out WDTCR, r16

    ldi r21, 6                ; 6,5,4,3,2,1,0: 7x8=56 seconds

countdown:

    ; count down to zero and sleep in lots of 8
    rcall sleepnow
    dec r21
    brbc 1, countdown

    ; finished long sleep, so flip LED
    rcall flipbit

    ret

sleepnow:

    ; see datasheet MCUCR, ADCSRA, BODCR

    ; sleep enable, power down mode
    ldi r16, 0b00110000
    out MCUCR, r16

    ; disable ADC
    ldi r16, 0b00010000
    out ADCSRA, r16

    ; disable interrupts to make sure the following
    ; timed sequence is not interrupted
    cli

    ; disable BOD - must be done in a timed sequence
    ; as follows, write 1 to BODS and BODSE then within
    ; four clock cycles write 1 to BODS and 0 to BODSE
    ; then within three clock cycles enable sleep
    ; page 33 on the datasheet

    ldi r16, 0b00000011
    ldi r17, 0b00000010
    out BODCR, r16
    out BODCR, r17

    sei

    sleep

    ; sleep disable
    ldi r16, 0b00000000
    out MCUCR, r16

    ret

flipbit:

    in r16, portb
    ldi r17, 0b00010000
    eor r16, r17
    out portb, r16

ret

; End of source code

So now that we have a hex file from the AVR simulator, we burn the fuses (either Arduino IDE or avrdudess), load the hex file to avrdudess and Program!






No comments:

Post a Comment