programowanie Arduino Micro w gołym C

programming Arduino Micro in pure C

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.