Зеркало сайта: vivacious-stockings-frog.cyclic.app

ATtiny13a: использование ассемблера GNU-AS в программах на Си

разделы: AVR , АССЕМБЛЕР , дата: 17 февраля 2018г.

То, что Arduino очень медленно обрабатывает внешние прерывания, я заметил еще осенью прошлого года, когда разбирался с RTC. Тогда я пытался тактировать счетчик часов Arduino от 32кHz вывода DS3231, но такие часы у меня отставали секунд на десять в минуту. Для 16 МГц микроконтроллера, это было абсолютное фиаско.

В следующий раз я столкнулся с проблемой на ATtiny13, когда делал счетчик импульсов. В этот раз Arduino досталась роль передатчика, а ATtiny13a работающей на частоте 9.6 МГц был в роли счетчика. Но несмотря на то, что прошивка была написана на чистом Си, а обработчик внешнего прерывания прерывания состоял всего из одной строчки: "value++;", максимальная рабочая частота счетчика достигала всего 200кГц.

Такой результат меня тоже не устроил, и я решил, что обработчик прерывания нужно писать на ассемблере. Забегая вперед, скажу, что это дало мне прирост по максимальной частоте в три раза т.е. до 600кГц.

Ок, ассемблер это хорошая штука в плане скорости, но полностью прошивку писать на нем довольно тоскливое занятие. В AVR нет даже целочисленного деления. Выходом может стать написание смешанного кода на ассемблере и Си, т.к. gnu ассемблер по сути является бэкендом gcc-компилятора.

Но прежде чем начать использовать ассемблерные функции и прерывания в Си-программах, нужно будет познакомится с процессом компиляции, сборки и линковки таких смешанных проектов.

    Содержание статьи:
  1. Blink на ассемблере GNU-AS, создание проекта.
  2. Использование в ассемблерной программе мнемоники из Си для портов и регистров.
  3. Добавление таблицы векторов.
  4. Смешивание Си и ассемблерного кода в одной программе.
  5. Передача и получение параметров из Си-программы в ассемблерную функцию.
  6. Переписываем с Си на ассемблер функцию wait_ms() на прерывании таймера.
  7. Линковка проекта.
  8. Счетчик на 4-x разрядном семисегментном индикаторе.
  9. Счетчик импульсов на ассемблерном прерывании.

Архив с полными исходниками, сборочными файлами и скомпилированными прошивками можно будет скачать по ссылке в конце статьи.

1) Blink на ассемблере GNU-AS, создание проекта

С ассемблера начиналось мое изучение микроконтроллеров четыре года назад. Настало время сдуть пыль со старых записей. Простейшую ассемблерную программу я выкладывал здесь: Blink на ассемблере AVR на примере ATtiny13. Просто скопируем ее для начала, и после начнем доводить до ума.

Итак, создаем рабочий проект:

$ mkdir -p ./01_blink
$ cd ./01_blink/

В каталоге проекта размещаем файл main.S такого содержания:

.equ DDRB,  0x17
.equ PB0,   0x00
.equ PORTB, 0x18
.org 0x00                   ; start address
        sbi     DDRB, PB0   ; DDRB|=(1<<PB0)
        ldi     r25, 0x01   ; r25=1
loop:                       ; main loop
        in      r24, PORTB  ; r24=PORTB
        eor     r24, r25    ; r24 = r24 xor r25
        out     PORTB, r24  ; PORTB=r25
        ldi     r18, 0x3F   ; r18=0x3F
        ldi     r19, 0x0D   ; r19=0x0D
        ldi     r24, 0x03   ; r24=0x03
delay:
        subi    r18, 0x01   ; (r18r19r24)-1  вычитание трехбайтного целого числа
        sbci    r19, 0x00
        sbci    r24, 0x00
        brne    delay       ; если значение в r24 не равно нулю, то переход на начало операции вычитания
        rjmp    loop        ; возврат на начало главного цикла

Первое, что бросается в глаза, - это нотация команд в стиле Intel, т.е. правый операнд присваивается левому. Поддерживается даже директива .org, правда работает она совсем не так вы думаете. В данном случае ее можно вообще убрать, ничего не сломается.

Добавляем Makefile:

MCU=attiny13a
OBJCOPY=avr-objcopy
CC=avr-gcc
SIZE=avr-size
CFLAGS=-mmcu=$(MCU) -Wall
LDFLAGS=
OBJ=main.o
TARGET=blink
.PHONY: all clean

%.o:	%.S
	$(CC) -c -o $@ $< $(CFLAGS)
all:	$(OBJ)
	$(CC) $(LDFLAGS) -o $(TARGET).elf  $(OBJ)
	$(OBJCOPY) -O ihex $(TARGET).elf $(TARGET).hex
	$(SIZE) -C --mcu=$(MCU) $(TARGET).elf
install:
	avrdude  -p$(MCU) -c usbasp -p t13   -U flash:w:./$(TARGET).hex:i
clean:
	@rm -v $(TARGET).elf $(TARGET).hex $(OBJ)

Здесь вместо ассемблера и линковщика используется gcc который по расширению файла - заглавная S, определяет текст программы как ассемблер.

Компилируем:

$ make all
avr-gcc -c -o main.o main.S -mmcu=attiny13a -Wall
avr-gcc  -o blink.elf  main.o
avr-objcopy -O ihex blink.elf blink.hex
avr-size -C --mcu=attiny13a blink.elf
AVR Memory Usage
----------------
Device: Unknown

Program:      26 bytes
(.text + .data + .bootloader)

Data:          0 bytes
(.data + .bss + .noinit)

Итого, имеем 26 байт прошивки.

Проверяем полученную прошивку:

$ avr-objdump -S ./blink.elf

./blink.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__ctors_end>:
   0:   b8 9a           sbi     0x17, 0 ; 23
   2:   91 e0           ldi     r25, 0x01       ; 1

00000004 :
   4:   88 b3           in      r24, 0x18       ; 24
   6:   89 27           eor     r24, r25
   8:   88 bb           out     0x18, r24       ; 24
   a:   2f e3           ldi     r18, 0x3F       ; 63
   c:   3d e0           ldi     r19, 0x0D       ; 13
   e:   83 e0           ldi     r24, 0x03       ; 3

00000010 :
  10:   21 50           subi    r18, 0x01       ; 1
  12:   30 40           sbci    r19, 0x00       ; 0
  14:   80 40           sbci    r24, 0x00       ; 0
  16:   e1 f7           brne    .-8             ; 0x10 
  18:   f5 cf           rjmp    .-22            ; 0x4 

Проверять скомпилированную прошивку надо всегда. К примеру, если заменить первую команду в программе:

        sbi     DDRB, PB0   ; DDRB|=(1<<PB0)

на:

        mov     DDRB, PB0   ; DDRB|=(1<<PB0)

то компилятор вместо выдачи ошибки заменит ее на:

        mov     r23, r0   ; DDRB|=(1<<PB0)

Не то что бы компилятор не определял синтаксические ошибки. Иногда он их определяет, а иногда нет. Зависит от опкода.

2) Использование в ассемблерной программе мнемоники из Си для портов и регистров

Первое, что хотелось бы при программировании на ассемблере, это использовать те же имена регистров ввода-вывода (РВВ), что и в программе на Си.

На самом деле это не сложно. Приведем программу в main.S к такому виду:

#include <avr/io.h>

.org 0x00                               ; start address
    sbi     _SFR_IO_ADDR(DDRB), PB0     ; DDRB|=(1<<PB0)
    ldi     r25, _BV(PB0)               ; r25=1
loop:                                   ; main loop
    in      r24, _SFR_IO_ADDR(PORTB)    ; r24=PORTB
    eor     r24, r25                    ; r24 = r24 xor r25
    out     _SFR_IO_ADDR(PORTB), r24    ; PORTB=r25
    ldi     r18, 0x3F                   ; r18=0x3F
    ldi     r19, 0x0D                   ; r19=0x0D
    ldi     r24, 0x03                   ; r24=0x03
delay:
    subi    r18, 0x01                   ; (r18r19r24)-1  вычитание трехбайтного целого числа
    sbci    r19, 0x00
    sbci    r24, 0x00
    brne    delay                       ; если значение в r24 не равно нулю, то переход на начало операции вычитания
    rjmp    loop                        ; возврат на начало главного цикла

Здесь DDRB и PORTB заданы смещением (offset) от какой-то базы, и используемый в программе макрос _SFR_IO_ADDR необходим для вычисления абсолютного адреса регистра.

3) Добавление таблицы векторов

Пока нашу программу нельзя назвать полноценной. Что бы ей таковой стать, нужно добавить таблицу векторов и процедуру инициализации, т.е. обработчик прерывания RESET.

Если посмотреть на дизассемблерованный листинг Blink'а из статьи Дизассемблирование blink.hex:

$ avr-objdump -S blink13.elf

./blink13.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__vectors>:
   0: 09 c0        rjmp .+18      ; 0x14 <__ctors_end>
   2: 0e c0        rjmp .+28      ; 0x20 <__bad_interrupt>
   4: 0d c0        rjmp .+26      ; 0x20 <__bad_interrupt>
   6: 0c c0        rjmp .+24      ; 0x20 <__bad_interrupt>
   8: 0b c0        rjmp .+22      ; 0x20 <__bad_interrupt>
   a: 0a c0        rjmp .+20      ; 0x20 <__bad_interrupt>
   c: 09 c0        rjmp .+18      ; 0x20 <__bad_interrupt>
   e: 08 c0        rjmp .+16      ; 0x20 <__bad_interrupt>
  10: 07 c0        rjmp .+14      ; 0x20 <__bad_interrupt>
  12: 06 c0        rjmp .+12      ; 0x20 <__bad_interrupt>

00000014 <__ctors_end>:
  14: 11 24        eor r1, r1
  16: 1f be        out 0x3f, r1 ; 63
  18: cf e9        ldi r28, 0x9F ; 159
  1a: cd bf        out 0x3d, r28 ; 61
  1c: 02 d0        rcall .+4       ; 0x22 <main>
  1e: 10 c0        rjmp .+32      ; 0x40 <_exit>

00000020 <__bad_interrupt>:
  20: ef cf        rjmp .-34      ; 0x0 <__vectors>

00000022 <main>:
22: b8 9a sbi 0x17, 0 ; 23 24: 91 e0 ldi r25, 0x01 ; 1 26: 88 b3 in r24, 0x18 ; 24 28: 89 27 eor r24, r25 2a: 88 bb out 0x18, r24 ; 24 2c: 2f e3 ldi r18, 0x3F ; 63 2e: 3d e0 ldi r19, 0x0D ; 13 30: 83 e0 ldi r24, 0x03 ; 3 32: 21 50 subi r18, 0x01 ; 1 34: 30 40 sbci r19, 0x00 ; 0 36: 80 40 sbci r24, 0x00 ; 0 38: e1 f7 brne .-8 ; 0x32 <main+0x10> 3a: 00 c0 rjmp .+0 ; 0x3c <main+0x1a> 3c: 00 00 nop 3e: f3 cf rjmp .-26 ; 0x26 <main+0x4>

-то в начале прошивки видна таблица векторов, после чего идет обработчик прерывания RESET (_ctors_end).

В datasheet на ATtiny13a на странице 45 приведена таблица векторов, а также рекомендуемая процедура инициализации:

Таблицу векторов можно вставить сразу после директивы .org 0x00:

.org 0x00                               ; start address
vectors:
    rjmp   reset                        ; Reset Handler
    rjmp   vectors                      ; IRQ0 Handler
    rjmp   vectors                      ; PCINT0 Handler
    rjmp   vectors                      ; Timer0 Overflow Handler
    rjmp   vectors                      ; EEPROM Ready Handler
    rjmp   vectors                      ; Analog Comparator Handler
    rjmp   vectors                      ; Timer0 CompareA Handler
    rjmp   vectors                      ; Timer0 CompareB Handler
    rjmp   vectors                      ; Watchdog Interrupt Handler
    rjmp   vectors                      ; ADC Conversion Handler

RESET это немаскируемое прерывание, т.е. оно работает вне зависимости от того, запретили или разрешили вы прерывания. Т.к. оно вызывается сразу после подачи питания на микроконтроллер, в его обработчик помещают процедуру инициализации. Сейчас нам проще будет взять эту процедуру из дизассемблированной прошивки. Если присмотреться к его коду, то оно состоит из инициализации пары регистров ввода-вывода, и вызова функции main:

Если взглянуть на карту регистров, то окажется, что по адресам 0x3f и 0x3d располагаются регистры SREG и SPL соответственно:

В SPL заносится число 0x9f это верхняя граница оперативной памяти:

Т.о. в процедуре инициализации микроконтроллера очищается статусный регистр а указатель стека SPL устанавливается на "вершину" оперативной памяти.

В итоге наша программа приобретает такой вид:

.org 0x00                               ; start address
vectors:
    rjmp   reset                        ; Reset Handler
    rjmp   vectors                      ; IRQ0 Handler
    rjmp   vectors                      ; PCINT0 Handler
    rjmp   vectors                      ; Timer0 Overflow Handler
    rjmp   vectors                      ; EEPROM Ready Handler
    rjmp   vectors                      ; Analog Comparator Handler
    rjmp   vectors                      ; Timer0 CompareA Handler
    rjmp   vectors                      ; Timer0 CompareB Handler
    rjmp   vectors                      ; Watchdog Interrupt Handler
    rjmp   vectors                      ; ADC Conversion Handler
reset:
    eor     r16,r16                     ; r=0;
    out     _SFR_IO_ADDR(SREG),r16      ; clean Status Register
    ldi     r16, RAMEND                 ; r16=0x9f
    out     _SFR_IO_ADDR(SPL),r16       ; Set Stack Pointer to top of RAM
main:
    sbi     _SFR_IO_ADDR(DDRB), PB0     ; DDRB|=(1<<PB0)
    ldi     r25, _BV(PB0)               ; r25=1
loop:                                   ; main loop
    in      r24, _SFR_IO_ADDR(PORTB)    ; r24=PORTB
    eor     r24, r25                    ; r24 = r24 xor r25
    out     _SFR_IO_ADDR(PORTB), r24    ; PORTB=r25
    ldi     r18, 0x3F                   ; r18=0x3F
    ldi     r19, 0x0D                   ; r19=0x0D
    ldi     r24, 0x03                   ; r24=0x03
delay:
    subi    r18, 0x01                   ; (r18r19r24)-1  вычитание трехбайтного целого числа
    sbci    r19, 0x00
    sbci    r24, 0x00
    brne    delay                       ; если значение в r24 не равно нулю, то переход на начало операции вычитания
    rjmp    loop                        ; возврат на начало главного цикла

Теперь, когда установили указатель стека на вершину оперативной памяти, мы можем пользоваться инструкциями rcall, push, pop и прерываниями. Но в случае ATtiny13 нужно быть предельно осторожным. Вызов всего нескольких вложенных функций "съест" всю оперативку в 64 байта.

4) Смешивание Си и ассемблерного кода в одной программе

Несмотря на то, что мы научились писать программы на ассемблере, на самом деле мы ничего этим не добились. Фактически мы только переписали своей рукой дизассемблерный вариант Blink скомпилированный GCC. Мы его никак не улучшили, потому что улучшать там нечего. Что бы что-то улучшить нужно уметь писать ассемблерные библиотеки и обработчики прерываний для Си-программ.

Для тренировки возьмем Си-вариант программы Blink и заменим команду PORTB^=(1<<PB0) вызовом ассемблерной функции. В данном случае нам это опять ничего не даст, зато разберемся с тем, как это делать.

Создаем каталог проекта:

$ mkdir -p ./04_mix_blink/{inc,src}
$ cd ./04_mix_blink/

Размещаем там основной Си-файл main.c:

#include <avr/io.h>
#include <util/delay.h>
#include "main.h"

#define LED (1<<PB0)

int main(void) {
    DDRB |= LED;
    for (;;){
        toggle();
        _delay_ms(1000);
    }
    return 0;
}

В директории inc разместим заголовочный файл ассемблерной функции main.h:

#ifndef __MAIN_H
#define __MAIN_H
extern void toggle();
#endif

В директорию проекта добавим Makefile:

MCU=attiny13a
OBJCOPY=avr-objcopy
CC=avr-gcc
SIZE=avr-size
CFLAGS=-mmcu=$(MCU) -Os -Wall -DF_CPU=1200000UL -I ./inc
ASFLAGS=-mmcu=$(MCU) -Wall
LDFLAGS=
OBJ=main.o  init.a gpio.a
TARGET=blink
.PHONY: all clean

%.o:	%.c
	$(CC) -c -o $@ $< $(CFLAGS)
%.a:	./src/%.S
	$(CC) -c -o $@ $< $(ASFLAGS)
all:	$(OBJ)
	$(CC) $(LDFLAGS) -o $(TARGET).elf  $(OBJ)
	$(OBJCOPY) -O ihex $(TARGET).elf $(TARGET).hex
	$(SIZE) -C --mcu=$(MCU) $(TARGET).elf
install:
	avrdude  -p$(MCU) -c usbasp -p t13   -U flash:w:./$(TARGET).hex:i
clean:
	@rm -v $(TARGET).elf $(TARGET).hex $(OBJ)

В директории src размещаем файл gpio.S с ассемблерной функцией void toggle():

#include <avr/io.h>

.global toggle
toggle:
    ldi     r25, _BV(PB0)               ; r25=1
    in      r24, _SFR_IO_ADDR(PORTB)    ; r24=PORTB
    eor     r24, r25                    ; r24 = r24 xor r25
    out     _SFR_IO_ADDR(PORTB), r24    ; PORTB=r24
    ret

Здесь директивой global мы делаем метку toggle видимой линковщику. Про директивы GAS можно почитать в документации: Using as - Assembler Directives.

Остается добавить в src файл init.S с таблицей векторов и обработчиком RESET прерывания:

#include "avr/io.h"
.org 0
.global vectors
vectors:
    rjmp   reset                        ; Reset Handler
    rjmp   vectors                      ; IRQ0 Handler
    rjmp   vectors                      ; PCINT0 Handler
    rjmp   vectors                      ; Timer0 Overflow Handler
    rjmp   vectors                      ; EEPROM Ready Handler
    rjmp   vectors                      ; Analog Comparator Handler
    rjmp   vectors                      ; Timer0 CompareA Handler
    rjmp   vectors                      ; Timer0 CompareB Handler
    rjmp   vectors                      ; Watchdog Interrupt Handler
    rjmp   vectors                      ; ADC Conversion Handler
reset:
    eor     r16,r16                     ; r=0;
    out     _SFR_IO_ADDR(SREG),r16      ; clean Status Register
    ldi     r16, RAMEND                 ; r16=0x9f
    out     _SFR_IO_ADDR(SPL),r16       ; Set Stack Pointer to top of RAM
    rjmp    main

Теперь компилируем:

$ make all
avr-gcc -c -o main.o main.c -mmcu=attiny13a -Os -Wall -DF_CPU=1200000UL -I ./inc
avr-gcc -c -o init.a src/init.S -mmcu=attiny13a -Wall
avr-gcc -c -o gpio.a src/gpio.S -mmcu=attiny13a -Wall
avr-gcc  -o blink.elf  main.o  init.a gpio.a
avr-objcopy -O ihex blink.elf blink.hex
avr-size -C --mcu=attiny13a blink.elf
AVR Memory Usage
----------------
Device: Unknown

Program:      64 bytes
(.text + .data + .bootloader)

Data:          0 bytes
(.data + .bss + .noinit)

Проверяем:

$ avr-objdump -S ./blink.elf

./blink.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__ctors_end>:
   0:   09 c0           rjmp    .+18            ; 0x14 <reset>
   2:   fe cf           rjmp    .-4             ; 0x0 <__ctors_end>
   4:   fd cf           rjmp    .-6             ; 0x0 <__ctors_end>
   6:   fc cf           rjmp    .-8             ; 0x0 <__ctors_end>
   8:   fb cf           rjmp    .-10            ; 0x0 <__ctors_end>
   a:   fa cf           rjmp    .-12            ; 0x0 <__ctors_end>
   c:   f9 cf           rjmp    .-14            ; 0x0 <__ctors_end>
   e:   f8 cf           rjmp    .-16            ; 0x0 <__ctors_end>
  10:   f7 cf           rjmp    .-18            ; 0x0 <__ctors_end>
  12:   f6 cf           rjmp    .-20            ; 0x0 <__ctors_end>

00000014 <reset>:
  14:   00 27           eor     r16, r16
  16:   0f bf           out     0x3f, r16       ; 63
  18:   0f e9           ldi     r16, 0x9F       ; 159
  1a:   0d bf           out     0x3d, r16       ; 61
  1c:   05 c0           rjmp    .+10            ; 0x28 <main>

0000001e <toggle>:
  1e:   91 e0           ldi     r25, 0x01       ; 1
  20:   88 b3           in      r24, 0x18       ; 24
  22:   89 27           eor     r24, r25
  24:   88 bb           out     0x18, r24       ; 24
  26:   08 95           ret

00000028 <main>:
  28:   b8 9a           sbi     0x17, 0 ; 23
  2a:   f9 df           rcall   .-14            ; 0x1e <toggle>
  2c:   2f e7           ldi     r18, 0x7F       ; 127
  2e:   89 ea           ldi     r24, 0xA9       ; 169
  30:   93 e0           ldi     r25, 0x03       ; 3
  32:   21 50           subi    r18, 0x01       ; 1
  34:   80 40           sbci    r24, 0x00       ; 0
  36:   90 40           sbci    r25, 0x00       ; 0
  38:   e1 f7           brne    .-8             ; 0x32 <main+0xa>
  3a:   00 c0           rjmp    .+0             ; 0x3c <main+0x14>
  3c:   00 00           nop
  3e:   f5 cf           rjmp    .-22            ; 0x2a <main+0x2>

Здесь все в порядке.

5) Передача и получение параметров из Си-программы в ассемблерную функцию

Передача параметров в ассемблерную функцию происходит в avr-gcc странным образом - через регистры. На мой взгляд, было бы проще передавать параметры через стек. Об используемых в avr-gcc регистрах можно почитать в официальном FAQ.

Регистры r0 и r1 зарезервированы Си-компилятором для временных переменных r0 и константы нуля в r1.

Регистры r18-r27, r30-r31 используются для передачи параметров в функции.

Регистры r2-r17, r28-r29 используются для локальных переменных.

Т.о. передача параметров в функцию идет через регистры r18-r25. Однобайтная переменная будет передана через регистр r24, двухбайтная через регистровую пару r25:r24. Вторая двухбайтная переменная будет передана через r23:r22 и т.д. Возвращаемый однобайтный параметр будет передан через r24.

Теперь попробуем передать в нашу функцию toggle() номер пина которым требуется помигать и получить от нее код возврата.

Си-часть программы будет выгядеть так:

#include <avr/io.h>
#include <util/delay.h>
#include "main.h"

#define LED (1<<PB0)

int main(void) {
    DDRB |= LED;
    for (;;){
        if (toggle(LED))
            _delay_ms(1000);
    }
    return 0;
}

Изменится объявление функции toggle:

#ifndef __MAIN_H
#define __MAIN_H
extern uint8_t toggle(uint8_t);
#endif

И ассемблерная функция станет такой:

#include <avr/io.h>

.global toggle
toggle:
    in      r25, _SFR_IO_ADDR(PORTB)    ; r25=PORTB
    eor     r25, r24                    ; r25 = r25 xor r24
    out     _SFR_IO_ADDR(PORTB), r25    ; PORTB=r25
    ldi     r24, 0x1                    ; return 1
    ret

Компилируем:

$ make all
avr-gcc -c -o main.o main.c -mmcu=attiny13a -Os -Wall -DF_CPU=1200000UL -I ./inc
avr-gcc  -o blink.elf  main.o  init.a gpio.a
avr-objcopy -O ihex blink.elf blink.hex
avr-size -C --mcu=attiny13a blink.elf
AVR Memory Usage
----------------
Device: Unknown

Program:      70 bytes
(.text + .data + .bootloader)

Data:          0 bytes
(.data + .bss + .noinit)

Проверяем:

$ avr-objdump -S ./blink.elf

./blink.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__ctors_end>:
   0:   09 c0           rjmp    .+18        ; 0x14 <reset>
   2:   fe cf           rjmp    .-4         ; 0x0 <__ctors_end>
   4:   fd cf           rjmp    .-6         ; 0x0 <__ctors_end>
   6:   fc cf           rjmp    .-8         ; 0x0 <__ctors_end>
   8:   fb cf           rjmp    .-10        ; 0x0 <__ctors_end>
   a:   fa cf           rjmp    .-12        ; 0x0 <__ctors_end>
   c:   f9 cf           rjmp    .-14        ; 0x0 <__ctors_end>
   e:   f8 cf           rjmp    .-16        ; 0x0 <__ctors_end>
  10:   f7 cf           rjmp    .-18        ; 0x0 <__ctors_end>
  12:   f6 cf           rjmp    .-20        ; 0x0 <__ctors_end>

00000014 <reset>:
  14:   00 27           eor r16, r16
  16:   0f bf           out 0x3f, r16   ; 63
  18:   0f e9           ldi r16, 0x9F   ; 159
  1a:   0d bf           out 0x3d, r16   ; 61
  1c:   05 c0           rjmp    .+10        ; 0x28 <main>

0000001e <toggle>:
  1e:   98 b3           in  r25, 0x18   ; 24
  20:   98 27           eor r25, r24
  22:   98 bb           out 0x18, r25   ; 24
  24:   81 e0           ldi r24, 0x01   ; 1
  26:   08 95           ret

00000028 <main>:
  28:   b8 9a           sbi 0x17, 0 ; 23
  2a:   81 e0           ldi r24, 0x01   ; 1
  2c:   f8 df           rcall   .-16        ; 0x1e <toggle>
  2e:   88 23           and r24, r24
  30:   e1 f3           breq    .-8         ; 0x2a <main+0x2>
  32:   2f e7           ldi r18, 0x7F   ; 127
  34:   89 ea           ldi r24, 0xA9   ; 169
  36:   93 e0           ldi r25, 0x03   ; 3
  38:   21 50           subi    r18, 0x01   ; 1
  3a:   80 40           sbci    r24, 0x00   ; 0
  3c:   90 40           sbci    r25, 0x00   ; 0
  3e:   e1 f7           brne    .-8         ; 0x38 <main+0x10>
  40:   00 c0           rjmp    .+0         ; 0x42 <__SREG__+0x3>
  42:   00 00           nop
  44:   f2 cf           rjmp    .-28        ; 0x2a <main+0x2>

6) Переписываем с Си на ассемблер функцию wait_ms() на прерывании таймера

Ок, теперь можно взяться за какой-то более приближенный к реальности проект. Попробуем сделать Blink с задержкой на функции wait_ms(), которая будет работать по прерыванию таймера.

На Си программа выглядит так:

#include <avr/io.h>
#include <avr/interrupt.h>

#define LED (1<<PB0)

volatile uint16_t count;

ISR(TIM0_COMPA_vect)
{
    if(count)
        count--;
}

static void wait_ms(uint16_t ms) {
    TCCR0B=(1<<CS01);       // prescaller 1/8
    TIMSK0=(1<<OCIE0A);     // enable interrupt
    count=ms;
    while(count) {
        asm("sleep");
    }
    TIMSK0 = 0;             // disable interrupt
    TCCR0B = 0;             // stop Timer
}

int main(void)
{
    // GPIO setup
    DDRB |= (LED);
    // Timer0 setup
    TCCR0A=(1<<WGM01);  // CTC mode
    OCR0A=150;          // freq 1kHz
    // power mode
    MCUCR |= (1<<SE);   // idle mode
    // let's go
    asm("sei");
    for(;;)
    {
        PORTB ^=(LED);
        wait_ms(1000);
    }
}

Программка занимает 180 байт, и ее полный дизассемблерный листинг можно посмотреть под спойлером:

$ avr-objdump -S ./blink.elf

./blink.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__vectors>:
   0:   09 c0           rjmp    .+18        ; 0x14 <__ctors_end>
   2:   16 c0           rjmp    .+44        ; 0x30 <__bad_interrupt>
   4:   15 c0           rjmp    .+42        ; 0x30 <__bad_interrupt>
   6:   14 c0           rjmp    .+40        ; 0x30 <__bad_interrupt>
   8:   13 c0           rjmp    .+38        ; 0x30 <__bad_interrupt>
   a:   12 c0           rjmp    .+36        ; 0x30 <__bad_interrupt>
   c:   12 c0           rjmp    .+36        ; 0x32 <__vector_6>
   e:   10 c0           rjmp    .+32        ; 0x30 <__bad_interrupt>
  10:   0f c0           rjmp    .+30        ; 0x30 <__bad_interrupt>
  12:   0e c0           rjmp    .+28        ; 0x30 <__bad_interrupt>

00000014 <__ctors_end>:
  14:   11 24           eor r1, r1
  16:   1f be           out 0x3f, r1    ; 63
  18:   cf e9           ldi r28, 0x9F   ; 159
  1a:   cd bf           out 0x3d, r28   ; 61

0000001c <__do_clear_bss>:
  1c:   10 e0           ldi r17, 0x00   ; 0
  1e:   a0 e6           ldi r26, 0x60   ; 96
  20:   b0 e0           ldi r27, 0x00   ; 0
  22:   01 c0           rjmp    .+2         ; 0x26 <.do_clear_bss_start>

00000024 <.do_clear_bss_loop>:
  24:   1d 92           st  X+, r1

00000026 <.do_clear_bss_start>:
  26:   a2 36           cpi r26, 0x62   ; 98
  28:   b1 07           cpc r27, r17
  2a:   e1 f7           brne    .-8         ; 0x24 <.do_clear_bss_loop>
  2c:   1f d0           rcall   .+62        ; 0x6c <main>
  2e:   40 c0           rjmp    .+128       ; 0xb0 <_exit>

00000030 <__bad_interrupt>:
  30:   e7 cf           rjmp    .-50        ; 0x0 <__vectors>

00000032 <__vector_6>:
  32:   1f 92           push    r1
  34:   0f 92           push    r0
  36:   0f b6           in  r0, 0x3f    ; 63
  38:   0f 92           push    r0
  3a:   11 24           eor r1, r1
  3c:   8f 93           push    r24
  3e:   9f 93           push    r25
  40:   80 91 60 00     lds r24, 0x0060
  44:   90 91 61 00     lds r25, 0x0061
  48:   89 2b           or  r24, r25
  4a:   49 f0           breq    .+18        ; 0x5e <__SREG__+0x1f>
  4c:   80 91 60 00     lds r24, 0x0060
  50:   90 91 61 00     lds r25, 0x0061
  54:   01 97           sbiw    r24, 0x01   ; 1
  56:   90 93 61 00     sts 0x0061, r25
  5a:   80 93 60 00     sts 0x0060, r24
  5e:   9f 91           pop r25
  60:   8f 91           pop r24
  62:   0f 90           pop r0
  64:   0f be           out 0x3f, r0    ; 63
  66:   0f 90           pop r0
  68:   1f 90           pop r1
  6a:   18 95           reti

0000006c <main>:
  6c:   b8 9a           sbi 0x17, 0 ; 23
  6e:   82 e0           ldi r24, 0x02   ; 2
  70:   8f bd           out 0x2f, r24   ; 47
  72:   86 e9           ldi r24, 0x96   ; 150
  74:   86 bf           out 0x36, r24   ; 54
  76:   85 b7           in  r24, 0x35   ; 53
  78:   80 62           ori r24, 0x20   ; 32
  7a:   85 bf           out 0x35, r24   ; 53
  7c:   78 94           sei
  7e:   91 e0           ldi r25, 0x01   ; 1
  80:   22 e0           ldi r18, 0x02   ; 2
  82:   34 e0           ldi r19, 0x04   ; 4
  84:   48 ee           ldi r20, 0xE8   ; 232
  86:   53 e0           ldi r21, 0x03   ; 3
  88:   88 b3           in  r24, 0x18   ; 24
  8a:   89 27           eor r24, r25
  8c:   88 bb           out 0x18, r24   ; 24
  8e:   23 bf           out 0x33, r18   ; 51
  90:   39 bf           out 0x39, r19   ; 57
  92:   50 93 61 00     sts 0x0061, r21
  96:   40 93 60 00     sts 0x0060, r20
  9a:   60 91 60 00     lds r22, 0x0060
  9e:   70 91 61 00     lds r23, 0x0061
  a2:   67 2b           or  r22, r23
  a4:   11 f0           breq    .+4         ; 0xaa <__stack+0xb>
  a6:   88 95           sleep
  a8:   f8 cf           rjmp    .-16        ; 0x9a <main+0x2e>
  aa:   19 be           out 0x39, r1    ; 57
  ac:   13 be           out 0x33, r1    ; 51
  ae:   ec cf           rjmp    .-40        ; 0x88 <main+0x1c>

000000b0 <_exit>:
  b0:   f8 94           cli

000000b2 <__stop_program>:
  b2:   ff cf           rjmp    .-2         ; 0xb2 <__stop_program>

Первое что бросается в глаза, это огромное тело обработчика прерывания. Напомню, что в Си-варианте он записан всего одной строчкой кода.

Половину обработчика составляют процедуры входа(push, push, push) и выхода(pop, еще раз pop, etc) из прерывания. "Тяжелые" операции(lds, sts) с волатильной переменной занимают почти весь код обработчика. Функциональная часть занимает всего три инструкции.

На мой взгляд, оптимизировать код прерывания можно отказавшись от использования переменной в RAM, т.е. используя для нее регистры.

Обработчик прерывания можно писать двумя способами. Первый способ будет работать везде, и он не будет отличаться от способа используемого gcc. Т.е. при входе в прерывания нужно будет сохранить в стеке все регистры которые будут изменяться в обработчике, не забывая о статусном регистре SREG. После нужно будет выполнить код, ради которого был вызван обработчик. В завершении, на выходе из прерывания нужно будет восстановить из стека значения сохраненных регистров.

Мне кажется, что этот способ так-себе. Проще в прерываниях использовать набор регистров которые в основной программе изменяются только в блоке cli - sei.

Итак, мой вариант Си программы получился таким:

#include <avr/io.h>
#include "main.h"

#define LED (1<<PB0)

int main(void) {
    // GPIO setup
    DDRB |= (LED);
    // Timer0 setup
    TCCR0A=(1<<WGM01);  // CTC mode
    OCR0A=150;          // freq 1kHz
    // power mode
    MCUCR |= (1<<SE); // idle
    // let's go
    asm("sei");
    for (;;){
        PORTB ^=(LED);
        wait_ms(1000);
    }
    return 0;
}

Объявление функции wait_ms в файле inc/main.h:

#ifndef __MAIN_H
#define __MAIN_H
#include <sys/types.h>

void wait_ms(uint16_t ms);
#endif

Ассемблерная функция wait_ms():

/*
    r25:r24 - input param
    r26:r27 - interrupt registers
    r2,r18      - auxiliary registers
*/

#include <avr/io.h>

.global wait_ms
wait_ms:
    cli
    mov     r26,r24
    mov     r27,r25
;
    ldi     r18, (1<<CS01);
    out     _SFR_IO_ADDR(TCCR0B),r18    ; TCCR0B=(1<<CS8),prescaler =8
    ldi     r18, (1<<OCIE0A)
    out     _SFR_IO_ADDR(TIMSK0),r18    ; TIMSK0=(1<<OCIE0A), enable interrupt

loop:
    sei
    sleep
    cli
    mov r2,r26
    or  r2,r27
    breq away_from_wait_ms
    rjmp loop

away_from_wait_ms:
    eor     r18,r18                     ; r18=0
    out     _SFR_IO_ADDR(TIMSK0),r18    ; TIMSK0=0
    out     _SFR_IO_ADDR(TCCR0B),r18    ; TCCR0B=0
    ret

И само прерывание:

#include "avr/io.h"
/*
    r25:r24 - input param
    r26:r27 - interrupt registers
    r2,r18      - auxiliary register
*/

.org 0
.global vectors
vectors:
    rjmp   reset                        ; Reset Handler
    rjmp   vectors                      ; IRQ0 Handler
    rjmp   vectors                      ; PCINT0 Handler
    rjmp   vectors                      ; Timer0 Overflow Handler
    rjmp   vectors                      ; EEPROM Ready Handler
    rjmp   vectors                      ; Analog Comparator Handler
    rjmp   channel_a                    ; Timer0 CompareA Handler
    rjmp   vectors                      ; Timer0 CompareB Handler
    rjmp   vectors                      ; Watchdog Interrupt Handler
    rjmp   vectors                      ; ADC Conversion Handler

; Reset Handler
reset:
    eor     r16,r16                     ; r=0;
    out     _SFR_IO_ADDR(SREG),r16      ; clean Status Register
    ldi     r16, RAMEND                 ; r16=0x9f
    out     _SFR_IO_ADDR(SPL),r16       ; Set Stack Pointer to top of RAM
    rjmp    main

; Timer0 CompareA Handler
channel_a:
    mov r2,r26
    or  r2,r27
    breq away_from_channel_a
    sbiw r26,0x01

away_from_channel_a:
    reti

Таким образом прерывание сократилось до пяти операций. Прошивка стала весить 110 байт. Но самое главное - это время уменьшенное на обработку прерывания. При рабочей частоте микроконтроллера в 1.2МГц, gcc версия прерывания выполнятся за 24мкс (29 тактов). Прерывание работает с частотой 1кГц, т.е. за одну секунду оно вызывается 1000 раз. В итоге получаем загрузку CPU микроконтроллера ~2.4%. Ассемблерная версия работает быстрее в пять(!) раз.

7) Линковка проекта

Последний технической вопрос который осталось разобрать, - это линковка проекта. К прошивке мы предъявляем простые требования: вначале должна располагаться таблица векторов, а обработчик прерывания reset должен выводить на точку входа проекта.

Пока эти правила соблюдались за счет того, что в Makefile, файл с таблицей векторов - init.S компилировался первым. Однако стоит только объявить в Си-программе глобальную переменную, как наша прошивка придет в полную негодность.

В предыдущем примере, приведем main.c к такому виду:

#include <avr/io.h>
#include "main.h"

#define LED (1<<PB0)

uint8_t state;

int main(void) {
    // GPIO setup
    DDRB |= (LED);
    // Timer0 setup
    TCCR0A=(1<<WGM01);  // CTC mode
    OCR0A=150;          // freq 1kHz
    // power mode
    MCUCR |= (1<<SE); // idle
    // let's go
    asm("sei");
    for (;;){
        state = (state) ? 0 : 1;
        PORTB ^=(LED);
        wait_ms(1000);
    }
    return 0;
}

После чего перекомпилируем проект и взглянем на начало прошивки:

$ avr-objdump -S ./blink.elf

./blink.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__ctors_end>:
   0:   10 e0           ldi     r17, 0x00       ; 0
   2:   a0 e6           ldi     r26, 0x60       ; 96
   4:   b0 e0           ldi     r27, 0x00       ; 0
   6:   01 c0           rjmp    .+2             ; 0xa <.do_clear_bss_start>

00000008 <.do_clear_bss_loop>:
   8:   1d 92           st      X+, r1

0000000a <.do_clear_bss_start>:
   a:   a1 36           cpi     r26, 0x61       ; 97
   c:   b1 07           cpc     r27, r17
   e:   e1 f7           brne    .-8             ; 0x8 <.do_clear_bss_loop>

00000010 <vectors>:
  10:   09 c0           rjmp    .+18            ; 0x24 <reset>
  12:   fe cf           rjmp    .-4             ; 0x10 <vectors>
  14:   fd cf           rjmp    .-6             ; 0x10 <vectors>
  16:   fc cf           rjmp    .-8             ; 0x10 <vectors>
  18:   fb cf           rjmp    .-10            ; 0x10 <vectors>
  1a:   fa cf           rjmp    .-12            ; 0x10 <vectors>
  1c:   08 c0           rjmp    .+16            ; 0x2e <channel_a>
  1e:   f8 cf           rjmp    .-16            ; 0x10 <vectors>
  20:   f7 cf           rjmp    .-18            ; 0x10 <vectors>
  22:   f6 cf           rjmp    .-20            ; 0x10 <vectors>

00000024 <reset>:
  24:   00 27           eor     r16, r16
  26:   0f bf           out     0x3f, r16       ; 63
  28:   0f e9           ldi     r16, 0x9F       ; 159
  2a:   0d bf           out     0x3d, r16       ; 61
  2c:   17 c0           rjmp    .+46            ; 0x5c <main>

0000002e <channel_a>:
  2e:   2a 2e           mov     r2, r26
  30:   2b 2a           or      r2, r27
  32:   09 f0           breq    .+2             ; 0x36 <away_from_channel_a>
  34:   11 97           sbiw    r26, 0x01       ; 1

00000036 <away_from_channel_a>:
  36:   18 95           reti

Как видно, таблица векторов "уехала" вниз, и в самом начале идет инициализация индексного регистра на начало области переменных.

Проблему можно частично решить указав линковщику опцию -nostartfiles. Однако проблема решится удалением кода инициализация индексного регистра. И этот код нам нужно будет дописывать самим. А если указать опции -nostartfiles, -nostdlib, -nodefaultlibs, то дописывать придется еще операции умножения, деления, остатка от деления и т.д.

Более удачным способом решения проблемы, будет прописывание в файле с таблицей векторов - init.S, директивы: ".section .vectors"

.section .vectors
.global vectors
тогда начало прошивки будет выглядеть уже получше:
$ avr-objdump -S ./blink.elf

./blink.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <vectors>:
   0:   09 c0           rjmp    .+18            ; 0x14 <reset>
   2:   fe cf           rjmp    .-4             ; 0x0 <vectors>
   4:   fd cf           rjmp    .-6             ; 0x0 <vectors>
   6:   fc cf           rjmp    .-8             ; 0x0 <vectors>
   8:   fb cf           rjmp    .-10            ; 0x0 <vectors>
   a:   fa cf           rjmp    .-12            ; 0x0 <vectors>
   c:   08 c0           rjmp    .+16            ; 0x1e <channel_a>
   e:   f8 cf           rjmp    .-16            ; 0x0 <vectors>
  10:   f7 cf           rjmp    .-18            ; 0x0 <vectors>
  12:   f6 cf           rjmp    .-20            ; 0x0 <vectors>

00000014 <reset>:
  14:   00 27           eor     r16, r16
  16:   0f bf           out     0x3f, r16       ; 63
  18:   0f e9           ldi     r16, 0x9F       ; 159
  1a:   0d bf           out     0x3d, r16       ; 61
  1c:   1f c0           rjmp    .+62            ; 0x5c <main>

0000001e <channel_a>:
  1e:   2a 2e           mov     r2, r26
  20:   2b 2a           or      r2, r27
  22:   09 f0           breq    .+2             ; 0x26 <away_from_channel_a>
  24:   11 97           sbiw    r26, 0x01       ; 1

00000026 <away_from_channel_a>:
  26:   18 95           reti

00000028 <__ctors_end>:
  28:   10 e0           ldi     r17, 0x00       ; 0
  2a:   a0 e6           ldi     r26, 0x60       ; 96
  2c:   b0 e0           ldi     r27, 0x00       ; 0
  2e:   01 c0           rjmp    .+2             ; 0x32 <.do_clear_bss_start>

00000030 <.do_clear_bss_loop>:
  30:   1d 92           st      X+, r1

00000032 <.do_clear_bss_start>:
  32:   a1 36           cpi     r26, 0x61       ; 97
  34:   b1 07           cpc     r27, r17
  36:   e1 f7           brne    .-8             ; 0x30 <.do_clear_bss_loop>

Но здесь код инициализации "съехал" вниз, он не выполняется, т.е. по сути его нет, т.к. он не работает. Нам же нужно добавить этот генерируемый Си-компилятором код к нашему ассемблерному обработчику прерывания RESET. С другой стороны, это лишь временное решение. Если мы захотим разместить данные во флеш-памяти, таблица векторов снова "съедет" вниз.

Что бы различные области прошивки размещать в нужном нам порядке, потребуется скрипт линковщика. И единственное слово которое нужно будет выучить, - это С Е К Ц И Я, т.к. это основное понятие линкера.

Если посмотреть на структуру ELF-файла прошивки:

$  avr-objdump -h ./blink.elf

./blink.elf:     file format elf32-avr

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000008c  00000000  00000000  00000094  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000000  00800060  0000008c  00000120  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000001  00800060  00800060  00000120  2**0
                  ALLOC
  3 .comment      00000011  00000000  00000000  00000120  2**0
                  CONTENTS, READONLY
  4 .debug_aranges 00000020  00000000  00000000  00000138  2**3
                  CONTENTS, READONLY, DEBUGGING
  5 .debug_info   0000007f  00000000  00000000  00000158  2**0
                  CONTENTS, READONLY, DEBUGGING
  6 .debug_abbrev 00000014  00000000  00000000  000001d7  2**0
                  CONTENTS, READONLY, DEBUGGING
  7 .debug_line   00000068  00000000  00000000  000001eb  2**0
                  CONTENTS, READONLY, DEBUGGING

- то исполняемый код будет расположен в секции .text. В колонке Size даже дан размер прошивки 0x8c, т.е. 140 байт. То, как внутри этой секции будет скомпонованы различные участки программы, зависит от скрипта линковщика.

Перечень скриптов, которые поставлялись вместе с avr-gcc у меня выглядит так:

$ ls  /usr/avr/lib/ldscripts/
avr1.x    avr2.xu    avr3.xr    avr35.xn  avr5.xbn   avr6.x       avrtiny.xu     avrxmega2.xr   avrxmega4.xn   avrxmega6.xbn
avr1.xbn  avr25.x    avr3.xu    avr35.xr  avr5.xn    avr6.xbn     avrxmega1.x    avrxmega2.xu   avrxmega4.xr   avrxmega6.xn
avr1.xn   avr25.xbn  avr31.x    avr35.xu  avr5.xr    avr6.xn      avrxmega1.xbn  avrxmega3.x    avrxmega4.xu   avrxmega6.xr
avr1.xr   avr25.xn   avr31.xbn  avr4.x    avr5.xu    avr6.xr      avrxmega1.xn   avrxmega3.xbn  avrxmega5.x    avrxmega6.xu
avr1.xu   avr25.xr   avr31.xn   avr4.xbn  avr51.x    avr6.xu      avrxmega1.xr   avrxmega3.xn   avrxmega5.xbn  avrxmega7.x
avr2.x    avr25.xu   avr31.xr   avr4.xn   avr51.xbn  avrtiny.x    avrxmega1.xu   avrxmega3.xr   avrxmega5.xn   avrxmega7.xbn
avr2.xbn  avr3.x     avr31.xu   avr4.xr   avr51.xn   avrtiny.xbn  avrxmega2.x    avrxmega3.xu   avrxmega5.xr   avrxmega7.xn
avr2.xn   avr3.xbn   avr35.x    avr4.xu   avr51.xr   avrtiny.xn   avrxmega2.xbn  avrxmega4.x    avrxmega5.xu   avrxmega7.xr
avr2.xr   avr3.xn    avr35.xbn  avr5.x    avr51.xu   avrtiny.xr   avrxmega2.xn   avrxmega4.xbn  avrxmega6.x    avrxmega7.xu

Как я понял из выдаваемых линковщиком ошибок, микроконтроллеру ATtiny13 соответствует скрипт avr25. Я взял avr25.x просто наугад, и скопировал его в директорию проекта.

Открыв его в текстовом редакторе нужно будет найти эту самую секцию .text:

  /* Internal text space or external memory.  */
  .text   :
  {
    *(.vectors)
    KEEP(*(.vectors))
    /* For data that needs to reside in the lower 64k of progmem.  */
     *(.progmem.gcc*)
    /* PR 13812: Placing the trampolines here gives a better chance
       that they will be in range of the code that uses them.  */
    . = ALIGN(2);
     __trampolines_start = . ;
    /* The jump trampolines for the 16-bit limited relocs will reside here.  */
    *(.trampolines)
     *(.trampolines*)
     __trampolines_end = . ;
     *(.progmem*)
    . = ALIGN(2);
    /* For future tablejump instruction arrays for 3 byte pc devices.
       We don't relax jump/call instructions within these sections.  */
    *(.jumptables)
     *(.jumptables*)
    /* For code that needs to reside in the lower 128k progmem.  */
    *(.lowtext)
     *(.lowtext*)
     __ctors_start = . ;
     *(.ctors)
     __ctors_end = . ;
     __dtors_start = . ;
     *(.dtors)
     __dtors_end = . ;
    KEEP(SORT(*)(.ctors))
    KEEP(SORT(*)(.dtors))
    /* From this point on, we don't bother about wether the insns are
       below or above the 16 bits boundary.  */
    *(.init0)  /* Start here after reset.  */
    KEEP (*(.init0))
    *(.init1)
    KEEP (*(.init1))
    *(.init2)  /* Clear __zero_reg__, set up stack pointer.  */
    KEEP (*(.init2))
    *(.init3)
    KEEP (*(.init3))
    *(.init4)  /* Initialize data and BSS.  */
    KEEP (*(.init4))
    *(.init5)
    KEEP (*(.init5))
    *(.init6)  /* C++ constructors.  */
    KEEP (*(.init6))
    *(.init7)
    KEEP (*(.init7))
    *(.init8)
    KEEP (*(.init8))
    *(.init9)  /* Call main().  */
    KEEP (*(.init9))
    *(.text)
    . = ALIGN(2);
     *(.text.*)
    . = ALIGN(2);
    *(.fini9)  /* _exit() starts here.  */
    KEEP (*(.fini9))
    *(.fini8)
    KEEP (*(.fini8))
    *(.fini7)
    KEEP (*(.fini7))
    *(.fini6)  /* C++ destructors.  */
    KEEP (*(.fini6))
    *(.fini5)
    KEEP (*(.fini5))
    *(.fini4)
    KEEP (*(.fini4))
    *(.fini3)
    KEEP (*(.fini3))
    *(.fini2)
    KEEP (*(.fini2))
    *(.fini1)
    KEEP (*(.fini1))
    *(.fini0)  /* Infinite loop after program termination.  */
    KEEP (*(.fini0))
     _etext = . ;
  }  > text

Это запись того, в каком порядке должная компоноваться прошивка. В самом начале стоит секция .vectors с которой мы уже успели познакомится.

Теперь, допустим, нам нужно сделать так, чтобы наш обработчик прерывания RESET после завершения выполнения написанного нами кода "прыгал" на начало инициализации, а после ее завершения, делал rjmp на функцию main. Т.е. нужно добавить две подсекции.

Перед комментарием:

  /* The jump trampolines for the 16-bit limited relocs will reside here.  */

вставим первую подсекцию:

*(.start0*)
    . = ALIGN(2);

После строк:

    *(.init8)
    KEEP (*(.init8))

вставим вторую подсекцию:

*(.start1*)
    . = ALIGN(2);

Переименуем измененный скрипт avr25.x в ld.x, чтобы их имена не пересекались с оригиналом.

В Makefile в флагах линковщика подключим наш скрипт:

LDFLAGS=-T ./ld.x

Файл src/init.S приведем к такому виду:

#include "avr/io.h"
/*
    r25:r24 - input param
    r26:r27 - interrupt registers
    r2      - auxiliary register
*/

.org 0
.section .vectors
.global vectors
vectors:
    rjmp   reset                        ; Reset Handler
    rjmp   vectors                      ; IRQ0 Handler
    rjmp   vectors                      ; PCINT0 Handler
    rjmp   vectors                      ; Timer0 Overflow Handler
    rjmp   vectors                      ; EEPROM Ready Handler
    rjmp   vectors                      ; Analog Comparator Handler
    rjmp   channel_a                    ; Timer0 CompareA Handler
    rjmp   vectors                      ; Timer0 CompareB Handler
    rjmp   vectors                      ; Watchdog Interrupt Handler
    rjmp   vectors                      ; ADC Conversion Handler

; Reset Handler
reset:
    eor     r16,r16                     ; r=0;
    out     _SFR_IO_ADDR(SREG),r16      ; clean Status Register
    ldi     r16, RAMEND                 ; r16=0x9f
    out     _SFR_IO_ADDR(SPL),r16       ; Set Stack Pointer to top of RAM
    rjmp    lets

; Timer0 CompareA Handler
channel_a:
    mov r2,r26
    or  r2,r27
    breq away_from_channel_a
    sbiw r26,0x01
away_from_channel_a:
    reti

.section .start0
lets:

.section .start1
lets_go:
    rjmp main

Здесь в конец файла были добавлены две секции, и обработчик прерывания RESET теперь заканчивается переходом на начало первой секции.

Компилируем, проверяем:

$ avr-objdump -S ./blink.elf 

./blink.elf:     file format elf32-avr



Disassembly of section .text:

00000000 <vectors>:
   0:   09 c0           rjmp    .+18            ; 0x14 <reset>
   2:   fe cf           rjmp    .-4             ; 0x0 <vectors>
   4:   fd cf           rjmp    .-6             ; 0x0 <vectors>
   6:   fc cf           rjmp    .-8             ; 0x0 <vectors>
   8:   fb cf           rjmp    .-10            ; 0x0 <vectors>
   a:   fa cf           rjmp    .-12            ; 0x0 <vectors>
   c:   08 c0           rjmp    .+16            ; 0x1e <channel_a>
   e:   f8 cf           rjmp    .-16            ; 0x0 <vectors>
  10:   f7 cf           rjmp    .-18            ; 0x0 <vectors>
  12:   f6 cf           rjmp    .-20            ; 0x0 <vectors>

00000014 <reset>:
  14:   00 27           eor     r16, r16
  16:   0f bf           out     0x3f, r16       ; 63
  18:   0f e9           ldi     r16, 0x9F       ; 159
  1a:   0d bf           out     0x3d, r16       ; 61
  1c:   05 c0           rjmp    .+10            ; 0x28 <__ctors_end>

0000001e <channel_a>:
  1e:   2a 2e           mov     r2, r26
  20:   2b 2a           or      r2, r27
  22:   09 f0           breq    .+2             ; 0x26 <away_from_channel_a>
  24:   11 97           sbiw    r26, 0x01       ; 1

00000026 <away_from_channel_a>:
  26:   18 95           reti

00000028 <__ctors_end>:
  28:   10 e0           ldi     r17, 0x00       ; 0
  2a:   a0 e6           ldi     r26, 0x60       ; 96
  2c:   b0 e0           ldi     r27, 0x00       ; 0
  2e:   01 c0           rjmp    .+2             ; 0x32 <.do_clear_bss_start>

00000030 <.do_clear_bss_loop>:
  30:   1d 92           st      X+, r1

00000032 <.do_clear_bss_start>:
  32:   a1 36           cpi     r26, 0x61       ; 97
  34:   b1 07           cpc     r27, r17
  36:   e1 f7           brne    .-8             ; 0x30 <.do_clear_bss_loop>

00000038 <lets_go>:
  38:   12 c0           rjmp    .+36            ; 0x5e <main>

Вот теперь все как надо.

8) Счетчик на 4-x разрядном семисегментном индикаторе

Теперь вернемся счетчику импульсов, с которого для меня и началась вся эта история с ассемблером. Так же как и в прошлый раз, сначала сделаем драйвер 4-x разрядного семисегментного индикатора, а потом "прикрутим" к нему сам счетчик.

Создаем каталог проекта:

mkdir -p ./08_led_counter/{asm,inc,src}
cd ./08_led_counter/

Размещаем там Makefile:

MCU=attiny13a
OBJCOPY=avr-objcopy
CC=avr-gcc
AS=avr-as
LD=avr-ld
SIZE=avr-size
CFLAGS=-mmcu=$(MCU) -Os -DF_CPU=9600000UL -I ./inc
ASFLAGS=-mmcu=$(MCU) -Wall
LDFLAGS=-T ld.x
OBJ=init.a  wait.a main.o led.o
TARGET=led
.PHONY: all clean

%.o:	src/%.c
	$(CC) -c -o $@ $< $(CFLAGS)
%.a:	./asm/%.S
	$(CC)  -c -o $@ $< $(ASFLAGS)
all:	$(OBJ)
	$(CC) $(LDFLAGS) -o $(TARGET).elf  $(OBJ)
	$(OBJCOPY) -O ihex $(TARGET).elf $(TARGET).hex
	$(SIZE) -C --mcu=$(MCU) $(TARGET).elf
install:
	avrdude  -p$(MCU) -c usbasp -p t13   -U flash:w:./$(TARGET).hex:i
clean:
	@rm -v $(TARGET).elf $(TARGET).hex $(OBJ)

Добавляем main.c:

#include <avr/io.h>
#include "main.h"
#include "led.h"


int main(void) {
    CLKPR =(1<<CLKPCE);
    CLKPR =0; // set 9.6 MHz CPU freq
    // GPIO setup
    DDRB |= (SCLK | RCLK | DIO);
     // Timer0 setup
    TCCR0A=(1<<WGM01);  // CTC mode
    OCR0A=50;           // freq 5kHz
    // power mode
    MCUCR |= (1<<SE); // idle
    // let's go
    asm("sei");
    for (;;){
    // variables
        static uint16_t count=158;
        static uint8_t i=0;
        show_led(count);
        wait_ms();  // wait 5ms
        if( ++i == 0)
            count++;
    }
    return 0;
}

Здесь сразу поднимается частота ATtiny13 до 9.6MНz. В отличии от оригинала, задержка формируется по таймеру, а не через цикл _delay_ms(). Чтобы прерывание таймера не мешало работе счетчика, оно сразу настроено на задержку в ~5мс.

Заголовочные файлы, ./inc/main.h и ./inc/led.h:

#ifndef __MAIN_H
#define __MAIN_H
#include <sys/types.h>

void wait_ms();
#endif
#ifndef __LED_H__
#define __LED_H__

#define SCLK (1<<PB2)
#define RCLK (1<<PB4)
#define DIO  (1<<PB3)

extern void show_led(uint16_t num);
#endif

Здесь все понятно. Файл с драйвером ./src/led.c:

#include <avr/io.h>
#include <avr/pgmspace.h>
#include "led.h"

uint8_t reg=0;

const unsigned char digit[10] PROGMEM = {
      0b11000000, // 0
      0b11111001, // 1
      0b10100100, // 2
      0b10110000, // 3
      0b10011001, // 4
      0b10010010, // 5
      0b10000010, // 6
      0b11111000, // 7
      0b10000000, // 8
      0b10010000, // 9
};

void spi_transmit(uint8_t data) {
    uint8_t i;
    for (i=0; i<8; i++)
    {
        PORTB=(data & 0x80) ? PORTB | DIO : PORTB & ~DIO;

        data=(data<<1);
        PORTB |= SCLK;
        PORTB &= ~SCLK;
    }
}

void to_led(uint8_t value, uint8_t reg) {
    if (value <10 && reg < 4) {
        char * ptr= (char *)(&digit);
        PORTB &= ~RCLK;
        spi_transmit(pgm_read_byte(ptr+value));
        spi_transmit(1<<reg);
        PORTB |= RCLK;
    }
}

void show_led(uint16_t num) {
    switch (reg) {
    case 0:
        to_led((uint8_t)(num%10),0);
        break;
    case 1:
        if (num>=10)
            to_led((uint8_t)((num%100)/10),1);
        break;
    case 2:
        if (num>=100)
            to_led((uint8_t)(num/100),2);
        break;
    }
    reg = (reg ==  2) ? 0 : reg+1;
}

Его не было смысла переписывать на ассемблере, единственное отличие от оригинала: массив digit[] теперь размещен во флеш-памяти.

Ассемблерная функция wait() в ./asm/wait.S

#include <avr/io.h>

.global wait_ms
wait_ms:
    ldi     r18, (1<<CS02)|(1<<CS00);
    out     _SFR_IO_ADDR(TCCR0B),r18    ; TCCR0B=(1<<CS02)|(1<<CS00),prescaler =1024
    ldi     r18, (1<<OCIE0A)
    out     _SFR_IO_ADDR(TIMSK0),r18    ; TIMSK0=(1<<OCIE0A), enable interrupt
    sleep
    eor     r18,r18                     ; r18=0
    out     _SFR_IO_ADDR(TIMSK0),r18    ; TIMSK0=0
    out     _SFR_IO_ADDR(TCCR0B),r18    ; TCCR0B=0
    ret

и таблица векторов с обработчиками в ./asm/init.S

#include "avr/io.h"

.org 0
.section .vectors
vectors:
    rjmp   reset                        ; Reset Handler
    rjmp   vectors                      ; IRQ0 Handler
    rjmp   vectors                      ; PCINT0 Handler
    rjmp   vectors                      ; Timer0 Overflow Handler
    rjmp   vectors                      ; EEPROM Ready Handler
    rjmp   vectors                      ; Analog Comparator Handler
    rjmp   TIM0_COMPA_vect              ; Timer0 CompareA Handler
    rjmp   vectors                      ; Timer0 CompareB Handler
    rjmp   vectors                      ; Watchdog Interrupt Handler
    rjmp   vectors                      ; ADC Conversion Handler

; Reset Handler
reset:
    eor     r16,r16                     ; r=0;
    out     _SFR_IO_ADDR(SREG),r16      ; clean Status Register
    ldi     r16, RAMEND                 ; r16=0x9f
    out     _SFR_IO_ADDR(SPL),r16       ; Set Stack Pointer to top of RAM
    rjmp    lets

; Timer0 CompareA Handler
.global TIM0_COMPA_vect
TIM0_COMPA_vect:
    nop
    reti

.section .start0
lets:

.section .start1
lets_go:
    rjmp main

Пустое прерывание таймера нужно для того чтобы выйти из спящего режима.

В скрипте линкера ld.x, секцию start0 нужно будет разместить сразу после секции .vectors

    *(.vectors)
    KEEP(*(.vectors))
	*(.start0*)
    . = ALIGN(2);

Если после сборки и прошивки на индикаторе начинают отсчитываться цифры начиная c 158, значит можно переходить непосредственно к счетчику импульсов.

9) Счетчик импульсов на ассемблерном прерывании

Последний пример будет уже поинтереснее. Чтобы "прикрутить" счетчик импульсов к драйверу индикатора потребуется изменить main.c и оба ассемблерных файла.

Я сразу кину под спойлер дизассеблированную итоговою прошивку (свыше 200 инструкций), чтобы далее по тексту было понятно о чем речь.

$ avr-objdump -S ./led.elf

./led.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <vectors>:
   0:   09 c0           rjmp    .+18        ; 0x14 <reset>
   2:   0d c0           rjmp    .+26        ; 0x1e <__vector_1>
   4:   11 c0           rjmp    .+34        ; 0x28 <__vector_2>
   6:   fc cf           rjmp    .-8         ; 0x0 <vectors>
   8:   fb cf           rjmp    .-10        ; 0x0 <vectors>
   a:   fa cf           rjmp    .-12        ; 0x0 <vectors>
   c:   24 c0           rjmp    .+72        ; 0x56 <__vector_6>
   e:   f8 cf           rjmp    .-16        ; 0x0 <vectors>
  10:   f7 cf           rjmp    .-18        ; 0x0 <vectors>
  12:   f6 cf           rjmp    .-20        ; 0x0 <vectors>

00000014 <reset>:
  14:   00 27           eor r16, r16
  16:   0f bf           out 0x3f, r16   ; 63
  18:   0f e9           ldi r16, 0x9F   ; 159
  1a:   0d bf           out 0x3d, r16   ; 61
  1c:   1e c0           rjmp    .+60        ; 0x5a <__trampolines_end>

0000001e <__vector_1>:
  1e:   cf b6           in  r12, 0x3f   ; 63
  20:   4b 0c           add r4, r11
  22:   5a 1c           adc r5, r10
  24:   cf be           out 0x3f, r12   ; 63
  26:   18 95           reti

00000028 <__vector_2>:
  28:   7f b6           in  r7, 0x3f    ; 63
  2a:   7f 92           push    r7
  2c:   8f 93           push    r24
  2e:   b0 9b           sbis    0x16, 0 ; 22
  30:   07 c0           rjmp    .+14        ; 0x40 <m1>
  32:   80 e2           ldi r24, 0x20   ; 32
  34:   8b bf           out 0x3b, r24   ; 59
  36:   40 92 60 00     sts 0x0060, r4
  3a:   50 92 61 00     sts 0x0061, r5
  3e:   07 c0           rjmp    .+14        ; 0x4e <m2>

00000040 <m1>:
  40:   80 e6           ldi r24, 0x60   ; 96
  42:   8b bf           out 0x3b, r24   ; 59
  44:   44 24           eor r4, r4
  46:   55 24           eor r5, r5
  48:   aa 24           eor r10, r10
  4a:   bb 24           eor r11, r11
  4c:   b3 94           inc r11

0000004e <m2>:
  4e:   8f 91           pop r24
  50:   7f 90           pop r7
  52:   7f be           out 0x3f, r7    ; 63
  54:   18 95           reti

00000056 <__vector_6>:
  56:   33 94           inc r3
  58:   18 95           reti

0000005a <__trampolines_end>:
  5a:   c0 f9           bld r28, 0
  5c:   a4 b0           in  r10, 0x04   ; 4
  5e:   99 92           st  Y+, r9
  60:   82 f8           bld r8, 2
  62:   80 90 10 e0     lds r8, 0xE010

00000064 <__ctors_end>:
  64:   10 e0           ldi r17, 0x00   ; 0
  66:   a0 e6           ldi r26, 0x60   ; 96
  68:   b0 e0           ldi r27, 0x00   ; 0
  6a:   e2 ea           ldi r30, 0xA2   ; 162
  6c:   f1 e0           ldi r31, 0x01   ; 1
  6e:   03 c0           rjmp    .+6         ; 0x76 <__ctors_end+0x12>
  70:   c8 95           lpm
  72:   31 96           adiw    r30, 0x01   ; 1
  74:   0d 92           st  X+, r0
  76:   a2 36           cpi r26, 0x62   ; 98
  78:   b1 07           cpc r27, r17
  7a:   d1 f7           brne    .-12        ; 0x70 <__ctors_end+0xc>

0000007c <__do_clear_bss>:
  7c:   10 e0           ldi r17, 0x00   ; 0
  7e:   a2 e6           ldi r26, 0x62   ; 98
  80:   b0 e0           ldi r27, 0x00   ; 0
  82:   01 c0           rjmp    .+2         ; 0x86 <.do_clear_bss_start>

00000084 <.do_clear_bss_loop>:
  84:   1d 92           st  X+, r1

00000086 <.do_clear_bss_start>:
  86:   a3 36           cpi r26, 0x63   ; 99
  88:   b1 07           cpc r27, r17
  8a:   e1 f7           brne    .-8         ; 0x84 <.do_clear_bss_loop>

0000008c <lets_go>:
  8c:   5b c0           rjmp    .+182       ; 0x144 <main>

0000008e <wait_ms>:
  8e:   33 24           eor r3, r3
  90:   25 e0           ldi r18, 0x05   ; 5
  92:   23 bf           out 0x33, r18   ; 51
  94:   24 e0           ldi r18, 0x04   ; 4
  96:   29 bf           out 0x39, r18   ; 57

00000098 <loop_wait_ms>:
  98:   33 20           and r3, r3
  9a:   f1 f3           breq    .-4         ; 0x98 <loop_wait_ms>
  9c:   22 27           eor r18, r18
  9e:   29 bf           out 0x39, r18   ; 57
  a0:   23 bf           out 0x33, r18   ; 51
  a2:   08 95           ret

000000a4 <spi_transmit>:
  a4:   28 e0           ldi r18, 0x08   ; 8
  a6:   98 b3           in  r25, 0x18   ; 24
  a8:   87 ff           sbrs    r24, 7
  aa:   02 c0           rjmp    .+4         ; 0xb0 <spi_transmit+0xc>
  ac:   98 60           ori r25, 0x08   ; 8
  ae:   01 c0           rjmp    .+2         ; 0xb2 <spi_transmit+0xe>
  b0:   97 7f           andi    r25, 0xF7   ; 247
  b2:   98 bb           out 0x18, r25   ; 24
  b4:   88 0f           add r24, r24
  b6:   c2 9a           sbi 0x18, 2 ; 24
  b8:   c2 98           cbi 0x18, 2 ; 24
  ba:   21 50           subi    r18, 0x01   ; 1
  bc:   a1 f7           brne    .-24        ; 0xa6 <spi_transmit+0x2>
  be:   08 95           ret

000000c0 <to_led>:
  c0:   cf 93           push    r28
  c2:   8a 30           cpi r24, 0x0A   ; 10
  c4:   88 f4           brcc    .+34        ; 0xe8 <to_led+0x28>
  c6:   64 30           cpi r22, 0x04   ; 4
  c8:   78 f4           brcc    .+30        ; 0xe8 <to_led+0x28>
  ca:   c6 2f           mov r28, r22
  cc:   c4 98           cbi 0x18, 4 ; 24
  ce:   e8 2f           mov r30, r24
  d0:   f0 e0           ldi r31, 0x00   ; 0
  d2:   e6 5a           subi    r30, 0xA6   ; 166
  d4:   ff 4f           sbci    r31, 0xFF   ; 255
  d6:   84 91           lpm r24, Z
  d8:   e5 df           rcall   .-54        ; 0xa4 <spi_transmit>
  da:   81 e0           ldi r24, 0x01   ; 1
  dc:   01 c0           rjmp    .+2         ; 0xe0 <to_led+0x20>
  de:   88 0f           add r24, r24
  e0:   ca 95           dec r28
  e2:   ea f7           brpl    .-6         ; 0xde <to_led+0x1e>
  e4:   df df           rcall   .-66        ; 0xa4 <spi_transmit>
  e6:   c4 9a           sbi 0x18, 4 ; 24
  e8:   cf 91           pop r28
  ea:   08 95           ret

000000ec <show_led>:
  ec:   20 91 62 00     lds r18, 0x0062
  f0:   21 30           cpi r18, 0x01   ; 1
  f2:   49 f0           breq    .+18        ; 0x106 <show_led+0x1a>
  f4:   18 f0           brcs    .+6         ; 0xfc <show_led+0x10>
  f6:   22 30           cpi r18, 0x02   ; 2
  f8:   91 f0           breq    .+36        ; 0x11e <show_led+0x32>
  fa:   1a c0           rjmp    .+52        ; 0x130 <show_led+0x44>
  fc:   6a e0           ldi r22, 0x0A   ; 10
  fe:   70 e0           ldi r23, 0x00   ; 0
 100:   3a d0           rcall   .+116       ; 0x176 <__udivmodhi4>
 102:   60 e0           ldi r22, 0x00   ; 0
 104:   14 c0           rjmp    .+40        ; 0x12e <show_led+0x42>
 106:   8a 30           cpi r24, 0x0A   ; 10
 108:   91 05           cpc r25, r1
 10a:   90 f0           brcs    .+36        ; 0x130 <show_led+0x44>
 10c:   64 e6           ldi r22, 0x64   ; 100
 10e:   70 e0           ldi r23, 0x00   ; 0
 110:   32 d0           rcall   .+100       ; 0x176 <__udivmodhi4>
 112:   6a e0           ldi r22, 0x0A   ; 10
 114:   70 e0           ldi r23, 0x00   ; 0
 116:   2f d0           rcall   .+94        ; 0x176 <__udivmodhi4>
 118:   86 2f           mov r24, r22
 11a:   61 e0           ldi r22, 0x01   ; 1
 11c:   08 c0           rjmp    .+16        ; 0x12e <show_led+0x42>
 11e:   84 36           cpi r24, 0x64   ; 100
 120:   91 05           cpc r25, r1
 122:   30 f0           brcs    .+12        ; 0x130 <show_led+0x44>
 124:   64 e6           ldi r22, 0x64   ; 100
 126:   70 e0           ldi r23, 0x00   ; 0
 128:   26 d0           rcall   .+76        ; 0x176 <__udivmodhi4>
 12a:   86 2f           mov r24, r22
 12c:   62 e0           ldi r22, 0x02   ; 2
 12e:   c8 df           rcall   .-112       ; 0xc0 <to_led>
 130:   80 91 62 00     lds r24, 0x0062
 134:   82 30           cpi r24, 0x02   ; 2
 136:   11 f0           breq    .+4         ; 0x13c <show_led+0x50>
 138:   8f 5f           subi    r24, 0xFF   ; 255
 13a:   01 c0           rjmp    .+2         ; 0x13e <show_led+0x52>
 13c:   80 e0           ldi r24, 0x00   ; 0
 13e:   80 93 62 00     sts 0x0062, r24
 142:   08 95           ret

00000144 <main>:
 144:   80 e8           ldi r24, 0x80   ; 128
 146:   86 bd           out 0x26, r24   ; 38
 148:   16 bc           out 0x26, r1    ; 38
 14a:   87 b3           in  r24, 0x17   ; 23
 14c:   8c 61           ori r24, 0x1C   ; 28
 14e:   87 bb           out 0x17, r24   ; 23
 150:   85 b7           in  r24, 0x35   ; 53
 152:   83 60           ori r24, 0x03   ; 3
 154:   85 bf           out 0x35, r24   ; 53
 156:   8b b7           in  r24, 0x3b   ; 59
 158:   80 62           ori r24, 0x20   ; 32
 15a:   8b bf           out 0x3b, r24   ; 59
 15c:   a8 9a           sbi 0x15, 0 ; 21
 15e:   82 e0           ldi r24, 0x02   ; 2
 160:   8f bd           out 0x2f, r24   ; 47
 162:   82 e3           ldi r24, 0x32   ; 50
 164:   86 bf           out 0x36, r24   ; 54
 166:   78 94           sei
 168:   80 91 60 00     lds r24, 0x0060
 16c:   90 91 61 00     lds r25, 0x0061
 170:   bd df           rcall   .-134       ; 0xec <show_led>
 172:   8d df           rcall   .-230       ; 0x8e <wait_ms>
 174:   f9 cf           rjmp    .-14        ; 0x168 <main+0x24>

00000176 <__udivmodhi4>:
 176:   aa 1b           sub r26, r26
 178:   bb 1b           sub r27, r27
 17a:   51 e1           ldi r21, 0x11   ; 17
 17c:   07 c0           rjmp    .+14        ; 0x18c <__udivmodhi4_ep>

0000017e <__udivmodhi4_loop>:
 17e:   aa 1f           adc r26, r26
 180:   bb 1f           adc r27, r27
 182:   a6 17           cp  r26, r22
 184:   b7 07           cpc r27, r23
 186:   10 f0           brcs    .+4         ; 0x18c <__udivmodhi4_ep>
 188:   a6 1b           sub r26, r22
 18a:   b7 0b           sbc r27, r23

0000018c <__udivmodhi4_ep>:
 18c:   88 1f           adc r24, r24
 18e:   99 1f           adc r25, r25
 190:   5a 95           dec r21
 192:   a9 f7           brne    .-22        ; 0x17e <__udivmodhi4_loop>
 194:   80 95           com r24
 196:   90 95           com r25
 198:   68 2f           mov r22, r24
 19a:   79 2f           mov r23, r25
 19c:   8a 2f           mov r24, r26
 19e:   9b 2f           mov r25, r27
 1a0:   08 95           ret

Файл main.c изменится немного:

#include <avr/io.h>
#include "main.h"
#include "led.h"

int main(void) {
    CLKPR =(1<<CLKPCE);
    CLKPR =0; // set 9.6 MHz CPU freq
    // GPIO setup
    DDRB |= (SCLK | RCLK | DIO);
    // external interrupt
    MCUCR|=(1<<ISC01)|(1<<ISC00); // IRQ on rising edge
    GIMSK|=(1<<PCIE);
    PCMSK|=(1<<PCINT0);
     // Timer0 setup
    TCCR0A=(1<<WGM01);  // CTC mode
    OCR0A=50;           // freq 5kHz
    // let's go
    asm("sei");
    for (;;){
    // variables
        static uint16_t count=158;
        show_led(count);
        wait_ms();  // wait 5ms
        count--;
        count++;
    }
    return 0;
}

Из программы был убран спящий режим, чтобы переходы из одного состояния в другое не сказывались на скорости работы счетчика.

Друг за другом идущие операторы count++; count--; вставлены для того чтобы Си-компилятор не заменил переменную count константой.

Статические переменные в Си всегда являются глобальными, их видимость ограничена на уровне синтаксиса. Это означает, что к такой переменной всегда можно будет обратиться из прерывания на ассемблере, что мы и будем делать.

Ассемблерная функция wait_ms в ./asm/wait.S

#include <avr/io.h>

.global wait_ms
wait_ms:
    eor     r3,r3
    ldi     r18, (1<<CS02)|(1<<CS00)
    out     _SFR_IO_ADDR(TCCR0B),r18    ; TCCR0B=(1<<CS00)|(1<<CS02),prescaler =1024
    ldi     r18, (1<<OCIE0A)
    out     _SFR_IO_ADDR(TIMSK0),r18    ; TIMSK0=(1<<OCIE0A), enable interrupt
loop_wait_ms:
    and     r3,r3
    breq    loop_wait_ms
    eor     r18,r18                     ; r18=0
    out     _SFR_IO_ADDR(TIMSK0),r18    ; TIMSK0=0
    out     _SFR_IO_ADDR(TCCR0B),r18    ; TCCR0B=0
    ret

Теперь об ./asm/init.S

Обработчик прерывания таймера выглядит так:

; Timer0 CompareA Handler
.global TIM0_COMPA_vect
TIM0_COMPA_vect:
    inc r3;                             ;set flag
    reti

С регистрами в программе дела обстоят так. Все "дефицитные" старшие регистры были заняты Си-компилятором, поэтому я использовал оставшиеся свободными младшие регистры. В данном случае в r3 хранится флаг прерывания таймера. Если прерывание увеличивает его значение, т.е. присваивает ненулевое значение, то происходит выход из цикла с последующим завершением работы функции.

Обработчик прерывания PCINT0, которое запускает и останавливает счетчик:

; PCINT0 Handler
.global PCINT0_vect
PCINT0_vect:
    in  r7,_SFR_IO_ADDR(SREG)
    push r7                             ; save SREG
    push r24
    sbis _SFR_IO_ADDR(PINB),0           ; if (PINB & (1<<PB0))
    rjmp    m1                          ; then
    ldi  r24,(1<<PCIE)
    out _SFR_IO_ADDR(GIMSK),r24         ; GIMSK=0 , disable INT0 interrupt
    sts 0x0060,r4
    sts 0x0061,r5
    rjmp    m2
m1:                                     ; else
    ldi r24,(1<<PCIE)|(1<<INT0)
    out _SFR_IO_ADDR(GIMSK),r24         ; GIMSK=(1<<INT0), enable INIT0 interrupt
    eor r4,r4
    eor r5,r5                           ; (r4:r5)=0
    eor r10,r10
    eor r11,r11
    inc r11
m2:
    pop r24
    pop  r7
    out _SFR_IO_ADDR(SREG), r7          ; restore SREG

    reti

В принципе, его можно было бы оставить и на Си. Оно правда там длиннее раза в три получается, но на скорость это особо не влияете.

Как не трудно догадаться, по адресу 0x0060:0x0061 расположена переменная count.

И остался тот самый счетчик на INT0 который и требовалось оптимизировать:

; IRQ0 Handler
.global INT0_vect
INT0_vect:
    in  r12, _SFR_IO_ADDR(SREG)         ; save SREG
    add r4, r11
    adc r5, r10                         ; (r4:r5)++, r10=0
    out _SFR_IO_ADDR(SREG), r12         ; restore SREG
    reti

В регистрах r11 и r10 содержатся константы: единица и ноль соответственно. Счетчик содержится в регистрах r4:r5. Самый идеальный вариант был бы, - использовать инструкцию adiw вместо пары add:adc. Но как я говорил старшие регистры занял Cи-компилятор под код драйвера. Можно было бы использовать встроенный ассемблер чтобы освободить один индексный регистр, но я решил, что сокращение кода на одну инструкцию существенно на быстродействие не повлияет.

Для сравнения, в оригинальной Си программе код обработчика выглядел так:

0000008e <__vector_1>:
  8e:   1f 92           push    r1
  90:   0f 92           push    r0
  92:   0f b6           in      r0, 0x3f        ; 63
  94:   0f 92           push    r0
  96:   11 24           eor     r1, r1
  98:   8f 93           push    r24
  9a:   9f 93           push    r25
  9c:   80 91 6d 00     lds     r24, 0x006D
  a0:   90 91 6e 00     lds     r25, 0x006E
  a4:   01 96           adiw    r24, 0x01       ; 1
  a6:   90 93 6e 00     sts     0x006E, r25
  aa:   80 93 6d 00     sts     0x006D, r24
  ae:   9f 91           pop     r25
  b0:   8f 91           pop     r24
  b2:   0f 90           pop     r0
  b4:   0f be           out     0x3f, r0        ; 63
  b6:   0f 90           pop     r0
  b8:   1f 90           pop     r1
  ba:   18 95           reti

Т.е. я сократил время его выполнения примерно раза в четыре.

Скетч Arduino для проверки счетчика:

#define PULSE 3
#define LATCH 2

void send_number(uint16_t value);

void setup() {
    Serial.begin(9600);
    // put your setup code here, to run once:
    pinMode(PULSE,OUTPUT);
    pinMode(LATCH,OUTPUT);
    digitalWrite(PULSE, LOW);
    digitalWrite(LATCH, HIGH);
}

void loop() {
    static long previous;
    static uint16_t num;
    num=(num == 573) ? 219 : 573;
    previous=millis();
    send_number(num);
    Serial.print("latency: ");
    Serial.print(millis()-previous);
    Serial.println(" ms");
    delay(1000);
}

void send_number(uint16_t num)
{
    digitalWrite(LATCH, LOW); // START
    delayMicroseconds(10);
    for(int i=0; i<num;i++)
    {
      PORTD |= (1<<PD3);
      PORTD &= ~(1<<PD3);

      for(uint8_t j=0;j<7;j++) // DEALY
          asm volatile("nop");
    }
    digitalWrite(LATCH, HIGH); // STOP
}

В результате оптимизации, время задержки в цикле:

for(uint8_t j=0;j<7;j++) // DEALY
          asm volatile("nop");
    }

удалось уменьшить с 20 до 7.

Сам цикл разворачивается Си-компилятором в три инструкции:

 126:   00 00           nop
 128:   81 50           subi    r24, 0x01   ; 1
 12a:   e9 f7           brne    .-6         ; 0x126 <_Z11send_numberj+0x26>

Три инструкции выполняются семь раз, т.е. 21 тактов. Итого имеем 1.3 мкс минимальное время для срабатывания обработчика внешнего прерывания. Наш обработчик выполняется за пять инструкций или 0.5 мкс. Вычитаем (1.3-0.5) = 0.8 мкс уходит у микроконтроллера чтобы зарегистрировать факт прерывания, установить флаг прерывания, на срабатывание контроллера прерываний, и занесение содержимого PC в стек, с последующем переходом на начало обработчика.

Архив с полными исходниками, сборочными файлами и скомпилированными прошивками можно скачать с сайта: