То, что Arduino очень медленно обрабатывает внешние прерывания, я заметил еще осенью прошлого года, когда разбирался с RTC. Тогда я пытался тактировать счетчик часов Arduino от 32кHz вывода DS3231, но такие часы у меня отставали секунд на десять в минуту. Для 16 МГц микроконтроллера, это было абсолютное фиаско.
В следующий раз я столкнулся с проблемой на ATtiny13, когда делал счетчик импульсов. В этот раз Arduino досталась роль передатчика, а ATtiny13a работающей на частоте 9.6 МГц был в роли счетчика. Но несмотря на то, что прошивка была написана на чистом Си, а обработчик внешнего прерывания прерывания состоял всего из одной строчки: "value++;", максимальная рабочая частота счетчика достигала всего 200кГц.
Такой результат меня тоже не устроил, и я решил, что обработчик прерывания нужно писать на ассемблере. Забегая вперед, скажу, что это дало мне прирост по максимальной частоте в три раза т.е. до 600кГц.
Ок, ассемблер это хорошая штука в плане скорости, но полностью прошивку писать на нем довольно тоскливое занятие. В AVR нет даже целочисленного деления. Выходом может стать написание смешанного кода на ассемблере и Си, т.к. gnu ассемблер по сути является бэкендом gcc-компилятора.
Но прежде чем начать использовать ассемблерные функции и прерывания в Си-программах, нужно будет познакомится с процессом компиляции, сборки и линковки таких смешанных проектов.
Архив с полными исходниками, сборочными файлами и скомпилированными прошивками можно будет скачать по ссылке в конце статьи.
С ассемблера начиналось мое изучение микроконтроллеров четыре года назад. Настало время сдуть пыль со старых записей. Простейшую ассемблерную программу я выкладывал здесь: 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)
Не то что бы компилятор не определял синтаксические ошибки. Иногда он их определяет, а иногда нет. Зависит от опкода.
Первое, что хотелось бы при программировании на ассемблере, это использовать те же имена регистров ввода-вывода (РВВ), что и в программе на Си.
На самом деле это не сложно. Приведем программу в 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 необходим для вычисления абсолютного адреса регистра.
Пока нашу программу нельзя назвать полноценной. Что бы ей таковой стать, нужно добавить таблицу векторов и процедуру инициализации, т.е. обработчик прерывания 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 байта.
Несмотря на то, что мы научились писать программы на ассемблере, на самом деле мы ничего этим не добились. Фактически мы только переписали своей рукой дизассемблерный вариант 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>
Здесь все в порядке.
Передача параметров в ассемблерную функцию происходит в 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>
Ок, теперь можно взяться за какой-то более приближенный к реальности проект. Попробуем сделать 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%. Ассемблерная версия работает быстрее в пять(!) раз.
Последний технической вопрос который осталось разобрать, - это линковка проекта. К прошивке мы предъявляем простые требования: вначале должна располагаться таблица векторов, а обработчик прерывания 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>
Вот теперь все как надо.
Теперь вернемся счетчику импульсов, с которого для меня и началась вся эта история с ассемблером. Так же как и в прошлый раз, сначала сделаем драйвер 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, значит можно переходить непосредственно к счетчику импульсов.
Последний пример будет уже поинтереснее. Чтобы "прикрутить" счетчик импульсов к драйверу индикатора потребуется изменить 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 в стек, с последующем переходом на начало обработчика.
Архив с полными исходниками, сборочными файлами и скомпилированными прошивками можно скачать с сайта: