Ассемблер считается сложным языком программирования. Хотя, припоминая, к слову, функциональное программирование, где нет ни переменных, ни циклов, а вместо них одни рекурсии и функции, я бы поспорил. Но, мне кажется, что я знаю причину такого мнения. Если взять рядовую книжку по предмету, то прежде чем написать свой blink, сперва придется прочитать добрую сотню страниц, если не больше, про архитектуру, регистры, шины, порты и т.д. Не самое легкое чтиво, надо сказать. Взяв для сравнения книжку Кернигана&Ритчи мы найдем "Hello World!" на первой же странице. Весомый аргумент в пользу Си, на мой взгляд.
В предыдущем посте я упоминал, что размер прошивки с программой Blink написанной на Си для ATtiny45 составляет 82 байта. В AVR каждая инструкция вместе с аргументом занимает два байта, т.е. одно слово. 82 делим на 2, получаем 41 ассемблерная инструкция. Наверно, должен сразу оговориться. Если вы не получаете за это деньги, то изучать Ассемблер нет никакого практического смысла, языка Си хватит "за глаза" для написания своих прошивок. НО... иногда желание заглянуть "под капот" бывает слишком сильным;)
Если уж экспериментировать с ассемблером, то для начала, разумно будет выбрать чип попроще. Мой выбор пал на ATtiny13. Его флеш-память составляет всего 1 Кбайт. Опять же 1024 делим пополам, получаем размер программы до 500 инструкций, что для ассемблера совсем немного. По правде говоря, многие устройства работают на этом чипе, на мой взгляд один из самых популярных микроконтроллеров. По счастливой случайности, распиновка ATtiny13 совпадает с распиновкой ATtiny45, следовательно и исходник blink.c не меняется:
// blink.c for AVR ATtiny 13/25/45/85 #include <avr/io.h> #define F_CPU 1000000UL // частота резонатора 1МГц #include <util/delay.h> int main(void) { // макрос _BV(число) заменяет конструкцию (1 << число) DDRB |= _BV(PB0); // аналог pinMode(PB0,OUTPUT); в Wiring for (;;) { PORTB ^= _BV(PB0); // инвертируем состояние порта PB0 _delay_ms(1000); // ждем 1 секунду } return 0; }
Ах, чуть не забыл. Про книжки по ассемблеру AVR.
Продолжаем. Компиляция:
avr-gcc -mmcu=attiny13 -Wall -Os -o blink13.elf blink13.c avr-objcopy -O ihex blink13.elf blink13.hex
Прошивка:
# avrdude -p attiny13 -c gromov -v -P /dev/ttyS0 -b9600 -U flash:w:blink13.hex avrdude: Version 6.0.1, compiled on Nov 25 2013 at 14:33:17 Copyright (c) 2000-2005 Brian Dean, http://www.bdmicro.com/ Copyright (c) 2007-2009 Joerg Wunsch System wide configuration file is "/etc/avrdude.conf" User configuration file is "/root/.avrduderc" User configuration file does not exist or is not a regular file, skipping Using Port : /dev/ttyS0 Using Programmer : gromov Overriding Baud Rate : 9600 AVR Part : ATtiny13 Chip Erase delay : 4000 us PAGEL : P00 BS2 : P00 RESET disposition : dedicated RETRY pulse : SCK serial program mode : yes parallel program mode : yes Timeout : 200 StabDelay : 100 CmdexeDelay : 25 SyncLoops : 32 ByteDelay : 0 PollIndex : 3 PollValue : 0x53 Memory Detail : Block Poll Page Polled Memory Type Mode Delay Size Indx Paged Size Size #Pages MinW MaxW ReadBack ----------- ---- ----- ----- ---- ------ ------ ---- ------ ----- ----- --------- eeprom 65 5 4 0 no 64 4 0 4000 4000 0xff 0xff flash 65 6 32 0 yes 1024 32 32 4500 4500 0xff 0xff signature 0 0 0 0 no 3 0 0 0 0 0x00 0x00 lock 0 0 0 0 no 1 0 0 4500 4500 0x00 0x00 calibration 0 0 0 0 no 2 0 0 0 0 0x00 0x00 lfuse 0 0 0 0 no 1 0 0 4500 4500 0x00 0x00 hfuse 0 0 0 0 no 1 0 0 4500 4500 0x00 0x00 Programmer Type : SERBB Description : serial port banging, reset=dtr sck=rts mosi=txd miso=cts avrdude: AVR device initialized and ready to accept instructions Reading | ################################################## | 100% 0.00s avrdude: Device signature = 0x1e9007 avrdude: safemode: lfuse reads as 6A avrdude: safemode: hfuse reads as FF avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed To disable this feature, specify the -D option. avrdude: erasing chip avrdude: reading input file "blink13.hex" avrdude: input file blink13.hex auto detected as Intel Hexavrdude: writing flash (68 bytes):Writing | ################################################## | 100% 0.04s avrdude: 68 bytes of flash written avrdude: verifying flash memory against blink13.hex: avrdude: load data flash data from input file blink13.hex: avrdude: input file blink13.hex auto detected as Intel Hex avrdude: input file blink13.hex contains 68 bytes avrdude: reading on-chip flash data: Reading | ################################################## | 100% 0.03s avrdude: verifying ... avrdude: 68 bytes of flash verified avrdude: safemode: lfuse reads as 6A avrdude: safemode: hfuse reads as FF avrdude: safemode: Fuses OK (H:FF, E:FF, L:6A) avrdude done. Thank you.
Как видно, для ATtiny13 blink весит уже всего 68 байт против 82 для ATyiny45. Заглядываем под капот:
$ avr-objdump -m avr -D ./blink13.hex ./blink13.hex: file format ihex Disassembly of section .sec1: 00000000 <.sec1>: 0: 09 c0 rjmp .+18 ; 0x14 2: 0e c0 rjmp .+28 ; 0x20 4: 0d c0 rjmp .+26 ; 0x20 6: 0c c0 rjmp .+24 ; 0x20 8: 0b c0 rjmp .+22 ; 0x20 a: 0a c0 rjmp .+20 ; 0x20 c: 09 c0 rjmp .+18 ; 0x20 e: 08 c0 rjmp .+16 ; 0x20 10: 07 c0 rjmp .+14 ; 0x20 12: 06 c0 rjmp .+12 ; 0x20 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 1e: 10 c0 rjmp .+32 ; 0x40 20: ef cf rjmp .-34 ; 0x0 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 3a: 00 c0 rjmp .+0 ; 0x3c 3c: 00 00 nop 3e: f3 cf rjmp .-26 ; 0x26 40: f8 94 cli 42: ff cf rjmp .-2 ; 0x42
Итого 34 команды если пересчитаете нумерованые строки. т.е 68/2, все сходится. Сначала содержимое скорее всего не очень понятно, поэтому для внесения ясности, будет кстати вспомнить, что у нас имеется исходник программы и объектный файл. Запустив avr-objdump с опцией -S мы получим "смешаный код". Исходник и соответствующую трансляцию на ассемблере:
$ 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>00000040 <_exit>: 40: f8 94 cli 00000042 <__stop_program>: 42: ff cf rjmp .-2 ; 0x42 <__stop_program>
Прямо говоря не густо, ну чтож бывает. Обратите внимание на блок между main и exit, выделенный зеленым. Это и есть наша программа Blink на AVR Assembler. Все что выше зеленого блока, это таблица векторов прерываний, которая по спецификации должна идти перед программой. Это т.н. "шапка".
Теперь если вдуматься, то получим следующую структуру программы:
Как видете, все сводится к дюжине команд. В AVR есть две группы регистров: регистры общего назначения (РОН) и регистры ввода-вывода (РВВ).
Наиболее интересный код здесь во второй фигурной скобке, который реализует задержку по времени. Этот алгоритм подробно разобран в книге Юрия Ревича, глава пятая "задержка". Только берите второе издание книги, в первом содержится опечатка: используется несуществующая команда suci вместо subi.
Вкратце. Нам нужно выполнить задержку на заданное время, мы знаем сколько времени выполняется каждая команда. Задержка выполняется последовательным вычитаним еденицы из числа до достижении нуля. комбинация команд: (subi + sbci + sbci) реализует декремент трехбайтного числа. Сначала вычитается младший байт, если он обнуляется, то вычитается еденица из среднего байта, если и он обнуляется от вычитается еденица из старшего байта. Если старший байт доходит до нуля, то условие перехода для оператора brne перестает выполнятся и программа выходит из цикла. В книге Юрия Ревича, вместо brne используется brcc. При этом операторе, цикл выполняет на одну итерацию больше, т.е. пока из нуля не вычитают еденицу.
Теперь считаем. Частота микроконтроллера 1 000 000 тактов в секунду. Цикл вычитания трехбайтного числа выполняется за: sbui, sbci - по одному такту, brne - два такта. Итого пять тактов. Т.е. надо вычитать число 1000000/5.
Смотрим на программу. Младший байт в r18 - 0x3F, средний байт в r19 - 0х0D, старший байт в r24 - 0x03. Итоговое число - 0x030D3F
$ echo $((0x030D3F*5)) 999995
Думаю, c этим Blink'ом все понятно.