Oto opis, jak napisać program, który spowoduje, że w Arduino Micro zaświeci wbudowana dioda świecąca, w gołym C, bez użycia żadnych bibliotek, i jak go skompilować i wrzucić na Arduino z linii poleceń.
This document describes how to write a program which makes Arduino Micro to light its built in LED, in pure C, without using any libraries, and how to build it and upload it to Arduino using command line tools.
W Arduino Micro wbudowana dioda świecąca jest podłączona do pina, który nazywa się digital pin 13. Więc program musi zrobić dwie rzeczy: przełączyć ten pin w stan output, a potem wysłać na niego prąd. Żeby przełączyć digital pin 13 w stan output, w Arduino Micro trzeba wpisać 1 w siódmy bit komórki pamięci o adresie 0x27 (siódmy bit to ten bit, który ma wagę 128). Pozostałe bity tej komórki pamięci albo rządzą innymi pinami, albo nie robią nic. Żeby wysłać prąd na pin 13, w Arduino Micro trzeba wpisać 1 w siódmy bit komórki pamięci o adresie 0x28. Pozostałe bity tej komórki pamięci albo rządzą innymi pinami, albo nie robią nic. Ponieważ w moim prostym programie nie dbam o to, co będzie działo się z innymi pinami, wystarczy, jeśli mój program wpisze liczbę 128 w komórki 0x27 i 0x28.
In Arduino Micro the built it LED is connected to digital pin 13. So the program has to do two things: it has to switch this pin into output mode and then it has to send electricity to this pin. In order to swith the digital pin 13 in output mode in Arduino, we have to set bit number seven (it is, the bit which weight is 128) of address 0x27. Other bits of this address rule other pins or do nothing. In order to send electricity to the digital pin 13, we have to set bit number seven of address 0x28. Other bits of this address rule other pins or do nothing. In our program we don't care about other pins, so the program has to put number 128 in address 0x27 and 0x28.
void main() { *((char *)(0x27)) = 128; *((char *)(0x28)) = 128; }
Jeśli martwisz się, że metoda main nic nie zwraca lub jeśli martwisz się, że po wykonaniu dwu poleceń nasz program nie będzie miał co robić i że wtedy procesor pójdzie w krzaki (i myślisz: może by tam na końcu dać nieskończoną pętlę?), nie martw się. Zaraz zobaczysz, że wszystko w porządku.
If you worry because main does not return anything or if you worry that after those two lines CPU will do nobody knows what (and you think: maybe we should have an endless loop there?), don't worry – soon you will see that it is correct.
Arduino Micro ma w środku mikrokontroler ATmega32U4 (źródło). To jest mikrokontroler z rodziny AVR (źródło). Więc trzeba ten program skompilować kompilatorem, który umie kompilować na AVR. Pod Debianem taki kompilator można zainstalować poleceniem apt-get install gcc-avr. Z tym pakietem instalują się między innymi dwa narzędzia: avr-gcc (kompilator do C) i avr-g++ (kompilator do C++). Mój program kompiluję poleceniem:
Arduino Micro has ATmega32U4 microcontroller inside it (source). This microcontroller is from AVR family (source). So we have to compile this program with a compiler which can compile for AVR. On Debian I can install such compiler with apt-get install gcc-avr. This package containsi, among others, two tools: avr-gcc (C compiler) and avr-g++ (C++ compiler). I compile my program with:
$ avr-gcc -c -Os -Wall -mmcu=atmega32u4 test.c -o test.o
Przy kompilacji poleciało ostrzeżenie, że funkcja main nic nie zwraca, ale ja sie nie przejmuję:
During the compilation I got a warning because my main function does not return anything, but I don't care:
test.c:1:6: warning: return type of ‘main’ is not ‘int’ [-Wmain] void main() { ^
To polecenie tworzy plik obiektowy test.o. Jeśli chciałbym obejrzeć sobie, co jest w tym pliku obiektowym, to pod Debianem muszę zainstalować paczkę binutils-avr. Ta paczka instaluje mi między innymi narzędzie avr-objdump. Żeby obejrzeć tym narzędziem plik test.o, wydaję polecenie:
This command creates object file test.o. If I want to look inside this object file, I need avr-objdump tool. On Debian I install this tool with binutils-avr package. When I have this tool, I can look inside test.o with:
$ avr-objdump -x -d test.o
Kiedy wykonam to polecenie, widzę:
When I issue this command, I see:
test.o: file format elf32-avr test.o architecture: avr:5, flags 0x00000010: HAS_SYMS start address 0x00000000 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000000 00000000 00000000 00000034 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .data 00000000 00000000 00000000 00000034 2**0 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 00000000 00000000 00000034 2**0 ALLOC 3 .text.startup 00000008 00000000 00000000 00000034 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 4 .comment 00000012 00000000 00000000 0000003c 2**0 CONTENTS, READONLY SYMBOL TABLE: 00000000 l df *ABS* 00000000 test.c 00000000 l d .text 00000000 .text 00000000 l d .data 00000000 .data 00000000 l d .bss 00000000 .bss 0000003e l *ABS* 00000000 __SP_H__ 0000003d l *ABS* 00000000 __SP_L__ 0000003f l *ABS* 00000000 __SREG__ 00000000 l *ABS* 00000000 __tmp_reg__ 00000001 l *ABS* 00000000 __zero_reg__ 00000000 l d .text.startup 00000000 .text.startup 00000000 l d .comment 00000000 .comment 00000000 g F .text.startup 00000008 main Disassembly of section .text.startup: 00000000 <main>: 0: 80 e8 ldi r24, 0x80 ; 128 2: 87 b9 out 0x07, r24 ; 7 4: 88 b9 out 0x08, r24 ; 8 6: 08 95 ret
Może się dziwisz, dlaczego tam w zdeasemblowanym kodzie metody main widzisz polecenie out 0x07, r24. Bo przecież mieliśmy wstawiać liczbę pod adres 0x27 a nie 0x07? A to dlatego, że w kodzie maszynowym AVR są dwa różne polecenia, których można użyć do rządzenia wejściem i wyjściem. Jest polecenie sts, które służy do wpisywania liczb do pamięci danych. Większość tej pamięci to zwykły RAM, ale część tej pamięci (dokładnie, obszar od 0x0000 do 0x00ff) jest zamapowana na wejście wyjście – to znaczy, że jak piszemy liczby do tej pamięci, to nie zostają one zapamiętane w ramie, tylko powodują, że dzieje się coś z wejściem i wyjściem. Adresy 0x27 i 0x28 tej pamięci danych są, jak pamiętamy, zamapowane (między innymi) na digital pin 13. Ale są też osobne polecenia, które służą tylko do rządzenia wejściem i wyjściem: na przykład polecenie out służy do wysyłania prądu na wyjście. Kiedy używa się tych specjalnych poleceń (na przykład polecenia out), trzeba używać innych adresów. Dokładnie o 0x20 mniejszych. Więc polecenie sts 0x27, r24 i polecenie out 0x07, r24 robią to samo. I widocznie jak kompilator zobaczył, że próbuję pisać do pamięci danych pod adres 0x27, to uznał (słusznie), że zamiast polecenia sts 0x27, r24 może zrobić polecenie out 0x07, r24, i będzie to samo. Oto źródło tej wiedzy.
Looking at the disassembled function main you may wonder why we have out 0x07, r24 (in our C code we put a number at 0x27 and not at 0x07). It is because in AVR machine code there are two ways to deal with i/o. There is sts command which is a general purpose command which puts a number in data memory. Most of the data memory is just a plain RAM, but a part of it (between 0x0000 and 0x00ff) is mapped to i/o registers – it means that when we try to put a number into this part of data memory it is not stored in RAM but it does something with i/o. For instance, as I already told, addresses 0x27 and 0x28 of data memory rule (among others) digital pin 13. But there is also a special purpose command out which sends signal to output. When we use this out command, we have to use different addresses: 0x20 lower than addresses which we use with sts. So sts 0x27, r24 and out 0x07, r24 do the same. So it seems that when compiler saw that we try to write to address 0x27 it decided that instead sts 0x27, r24 it will generate out 0x07, r24, because it will give the same effect. You can read about it on wikipedia.
A teraz linkuję ten plik obiektowy (linkuję go z niczym), tworząc plik elf:
Now I link object file (with nothing) and generate elf file:
$ avr-gcc -Os -Wl,--gc-sections -mmcu=atmega32u4 -o test.elf test.o -lm
Warto obejrzeć ten wygenerowany plik elf. Robię to poleceniem:
It is a good idea to look at this generated elf file. I do it with this command:
$ avr-objdump -x -d test.elf
Wynik tego polecenia wygląda tak:
This is the result of this command:
test.elf: file format elf32-avr test.elf architecture: avr:5, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED start address 0x00000000 Program Header: LOAD off 0x00000074 vaddr 0x00000000 paddr 0x00000000 align 2**1 filesz 0x000000d0 memsz 0x000000d0 flags r-x LOAD off 0x00000144 vaddr 0x00800100 paddr 0x000000d0 align 2**0 filesz 0x00000000 memsz 0x00000000 flags rw- Sections: Idx Name Size VMA LMA File off Algn 0 .data 00000000 00800100 000000d0 00000144 2**0 CONTENTS, ALLOC, LOAD, DATA 1 .text 000000d0 00000000 00000000 00000074 2**1 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .comment 00000011 00000000 00000000 00000144 2**0 CONTENTS, READONLY SYMBOL TABLE: 00800100 l d .data 00000000 .data 00000000 l d .text 00000000 .text 00000000 l d .comment 00000000 .comment 00000000 l df *ABS* 00000000 test.c 0000003e l *ABS* 00000000 __SP_H__ 0000003d l *ABS* 00000000 __SP_L__ 0000003f l *ABS* 00000000 __SREG__ 00000000 l *ABS* 00000000 __tmp_reg__ 00000001 l *ABS* 00000000 __zero_reg__ 00000000 l df *ABS* 00000000 _exit.o 000000ce l .text 00000000 __stop_program 00000000 l df *ABS* 00000000 000000d0 l *ABS* 00000000 __data_load_start 000000c0 w .text 00000000 __vector_38 000000c0 w .text 00000000 __vector_22 000000c0 w .text 00000000 __vector_28 000000c0 w .text 00000000 __vector_1 000000c0 w .text 00000000 __vector_32 000000c0 w .text 00000000 __vector_34 000000ac g .text 00000000 __trampolines_start 000000d0 g .text 00000000 _etext 000000c0 w .text 00000000 __vector_24 000000c0 w .text 00000000 __vector_12 000000c0 g .text 00000000 __bad_interrupt 000000d0 g *ABS* 00000000 __data_load_end 000000c0 w .text 00000000 __vector_6 000000c0 w .text 00000000 __vector_31 000000c0 w .text 00000000 __vector_35 000000ac g .text 00000000 __trampolines_end 000000c0 w .text 00000000 __vector_39 000000c0 w .text 00000000 __vector_3 000000c0 w .text 00000000 __vector_23 000000ac g .text 00000000 __dtors_end 000000c0 w .text 00000000 __vector_30 000000c0 w .text 00000000 __vector_25 000000c0 w .text 00000000 __vector_11 000000ac w .text 00000000 __init 000000c0 w .text 00000000 __vector_13 000000c0 w .text 00000000 __vector_17 000000c0 w .text 00000000 __vector_19 000000c0 w .text 00000000 __vector_7 000000c0 w .text 00000000 __vector_41 00810000 g .text 00000000 __eeprom_end 00000000 g .text 00000000 __vectors 000000c0 w .text 00000000 __vector_27 00000000 w .text 00000000 __vector_default 000000c0 w .text 00000000 __vector_5 000000c0 w .text 00000000 __vector_33 000000ac g .text 00000000 __ctors_start 000000c0 w .text 00000000 __vector_37 000000c4 g F .text 00000008 main 000000c0 w .text 00000000 __vector_4 00000000 w *ABS* 00000000 __heap_end 000000c0 w .text 00000000 __vector_9 000000c0 w .text 00000000 __vector_2 000000c0 w .text 00000000 __vector_21 000000c0 w .text 00000000 __vector_15 000000c0 w .text 00000000 __vector_36 000000c0 w .text 00000000 __vector_29 000000ac g .text 00000000 __dtors_start 000000ac g .text 00000000 __ctors_end 00000aff w *ABS* 00000000 __stack 000000c0 w .text 00000000 __vector_40 00800100 g .data 00000000 _edata 00800100 g .text 00000000 _end 000000c0 w .text 00000000 __vector_8 000000c0 w .text 00000000 __vector_26 000000cc w .text 00000000 .hidden exit 000000cc g .text 00000000 .hidden _exit 000000c0 w .text 00000000 __vector_14 000000c0 w .text 00000000 __vector_10 000000c0 w .text 00000000 __vector_16 000000c0 w .text 00000000 __vector_18 000000c0 w .text 00000000 __vector_20 000000c0 w .text 00000000 __vector_42 Disassembly of section .text: 00000000 <__vectors>: 0: 0c 94 56 00 jmp 0xac ; 0xac <__ctors_end> 4: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 8: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> c: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 10: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 14: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 18: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 1c: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 20: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 24: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 28: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 2c: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 30: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 34: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 38: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 3c: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 40: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 44: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 48: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 4c: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 50: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 54: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 58: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 5c: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 60: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 64: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 68: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 6c: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 70: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 74: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 78: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 7c: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 80: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 84: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 88: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 8c: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 90: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 94: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 98: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 9c: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> a0: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> a4: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> a8: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__bad_interrupt> 000000ac <__ctors_end>: ac: 11 24 eor r1, r1 ae: 1f be out 0x3f, r1 ; 63 b0: cf ef ldi r28, 0xFF ; 255 b2: da e0 ldi r29, 0x0A ; 10 b4: de bf out 0x3e, r29 ; 62 b6: cd bf out 0x3d, r28 ; 61 b8: 0e 94 62 00 call 0xc4 ; 0xc4 <main> bc: 0c 94 66 00 jmp 0xcc ; 0xcc <_exit> 000000c0 <__bad_interrupt>: c0: 0c 94 00 00 jmp 0 ; 0x0 <__vectors> 000000c4 <main>: c4: 80 e8 ldi r24, 0x80 ; 128 c6: 87 b9 out 0x07, r24 ; 7 c8: 88 b9 out 0x08, r24 ; 8 ca: 08 95 ret 000000cc <_exit>: cc: f8 94 cli 000000ce <__stop_program>: ce: ff cf rjmp .-2 ; 0xce <__stop_program>
Ciekawie jest poczytać sobie ten plik elf, bo jest w nim już wszystek kod, który wrzucimy wkrótce na Arduino. Różne kawałki tego pliku pokolorowałem na różne kolory, żeby się łatwiej oglądało.
It is interesting to read this elf file, because it contains all the code that we will soon upload to Arduino. I highlighted different parts of this file with different colors, so it is more easy to read it.
Potem konwertuję ten plik elf na format intel hex. Służy do tego narzędzie avr-objcopy. Jest ono częścią wspomnianego już pakietu binutils-avr. No to wydaję polecenie:
Then I have to convert this elf file to intel hex format. I can do it with avr-objcopy tool. This tool is a part of aforementioned binutils-avr package. So I issue this command:
$ avr-objcopy -O ihex -R .eeprom test.elf test.hex
W wyniku tego polecenia powstał plik test.hex, który zawiera dokładnie, bajt po bajcie, to, co zostanie umieszczone w pamięci flash Arduina. Zobacz sobie poniżej – oto ten plik pokolorowany na te same kolory, na które kolorowałem chwilę temu plik elf:
This command has created test.hex file. This file contains exactly the data that will be put in Arduino flash memory. Here you have this file highlighted with the same colors with which I highlighted elf file:
:100000000C9456000C9460000C9460000C946000FA :100010000C9460000C9460000C9460000C946000E0 :100020000C9460000C9460000C9460000C946000D0 :100030000C9460000C9460000C9460000C946000C0 :100040000C9460000C9460000C9460000C946000B0 :100050000C9460000C9460000C9460000C946000A0 :100060000C9460000C9460000C9460000C94600090 :100070000C9460000C9460000C9460000C94600080 :100080000C9460000C9460000C9460000C94600070 :100090000C9460000C9460000C9460000C94600060 :1000A0000C9460000C9460000C94600011241FBE3E :1000B000CFEFDAE0DEBFCDBF0E9462000C94660095 :1000C0000C94000080E887B988B90895F894FFCF7F :00000001FF
Teraz wyślę plik test.hex do Arduino. Wciskam przycisk reset i sprawdzam, czy zostało stworzone urządzenie /dev/ttyACM0:
Now I will upload test.hex to Arduino. I press reset button and verify that /dev/ttyACM0 device exists:
$ ls -l /dev/ttyACM0
Jeśli widzę, że urządzenie istnieje (a powinno istnieć), wrzucam test.hex do Arduino:
If it exists (it should) I upload test.hex to Arduino:
$ avrdude -patmega32u4 -cavr109 -P/dev/ttyACM0 -b57600 -D -Uflash:w:test.hex:i
Program ładuje się na Arduino. LED na Arduino zaczyna świecić. To znaczy, że program działa. Sukces.
Program gets uploaded to Arduino. Built in LED lights. It means that my program works. Success.
Może zastanawiasz się, skąd wiedziałem, że digitalnym pinem 13 steruje akurat siódmy bit komórek 0x27 i 0x28. Opowiem teraz, skąd to wiedziałem. Piny są podzielone na grupy zwane portami. Każdy port ma jednoliterową nazwę. Oglądam ten rysunek. Widzę że na nim przy jednej nóżce jest napisane PC7 digital pin 13. To znaczy, że ta nóżka to jest digital pin 13, że należy ona do portu C i że rządzi nią siódmy bit tego portu. Jak programujemy Arduino w C korzystając z gotowych bibliotek, to mamy dwie niby-zmienne służące do rządzenia pinami należącymi do portu C: PORTC i DDRC (źródło). Definicje tych niby-zmiennych dla ATmega 32U4 mogę znaleźć w pliku /usr/lib/avr/include/avr/iom32u4.h (który w Debianie jest częścią pakietu avr-libc). Oglądam ten plik i widzę:
Now I will describe how I know which bit of which byte rules digital pin 13. Pins are grouped into ports: each port is a group of pins. Each pin has one letter name. I look at this picture. I see that one pin is captioned as PC7 digital pin 13. It means that this pin is digital pin 13, it belongs to port C and it is ruled by seventh bit of this port. When I program Arduino in C using libraries I have two pseudovariables ruling port C: PORTC and DDRC (source). Those pseudoariables are defined for Arduino Micro in file /usr/lib/avr/include/avr/iom32u4.h (in Debian this file belongs to avr-libc package). I look at this file and see:
#define DDRC _SFR_IO8(0x07) (...) #define PORTC _SFR_IO8(0x08)
Fajnie, tylko skąd mogę wiedzieć, co robi to makro _SFR_IO8? Mógłbym poszukać dalej w plikach nagłówkowych z pakietu avr-libc, ale ja zrobiłem inaczej. Stworzyłem taki plik test2.c:
Great, but how can I know how is _SFR_IO8 macro defined? I could hunt its definition in header files from avr-libc package, but I did something else. I created such test2.c file:
#include "Arduino.h" void main() { DDRC = 0; PORTC = 0; }
Po czym rozwinąłem w nim makra takim poleceniem:
Then I expanded macros:
$ avr-gcc -E -mmcu=atmega32u4 -I/usr/share/arduino/hardware/arduino/cores/arduino -I/usr/share/arduino/hardware/arduino/variants/micro test2.c
Zobaczyłem to:
I saw this:
(...) void main() { (*(volatile uint8_t *)((0x07) + 0x20)) = 0; (*(volatile uint8_t *)((0x08) + 0x20)) = 0; }
I wszystko jasne.
And everything is clear.