Arduino IDE的编译执行过程解读

avr-gcc

1997年ATMEL公司的A先生和V先生推出了全新配置的8位精简指令集微处理器(RISC-Reduced Instrction Sot CPU),起名为AVR。AVR是一种指令内核的统称,内部又分为ATtiny、AT90S、ATmega三大系列,分别对应AVR的低、中、高档产品。对于开发者而言,关注更多的是AVR单片机的开发方式,而AVR单片机最初在设计的时候的目的就是为了迎合采用高级编程语言来开发这一需求。AVR单片机高级语言开发工具有很多,其中WINAVR是个免费的AVR开发程序集,它以著名的自由软件GCC 为C/C++编译器。GCC可以编译多种语言,比如说C、C++、Objective-C、Fortran、Java和Ada等。AVR也得到了GCC的支持,它也是GCC 支持的唯一一种8 位处理器。

Ref

回到问题上来,Arduino作为一款很火的开源硬件,其编程环境Arduino IDE是processing IDE开发的,简单易用,关键是,这个IDE也是开源的。Arduino语言基于wiring语言开发的,也是对 AVR-GCC库的二次封装,所以Arduino的编程实现非常简单,即使没有单片机基础也可以去做Arduino开发。但是,在这些简单的编程语言背后的执行过程又是什么样的呢?

可以到github上去找Arduino的源码,再结合Arduino的官网发布的信息,自己去分析,下面给出几个有用的链接:

Arduino Builder

https://github.com/arduino/arduino-builder

Arduino IDE源码

https://github.com/arduino/Arduino/

Arduino Build Process

https://www.arduino.cc/en/Hacking/BuildProcess

其实本身也可以直接利用avr-gcc为Arduino编写程序,因为Arduino本身就是对avr-gcc的二次封装,只需要一个终端、文本编辑器和avr-gcc的工具链就可以了。

编译执行原理解读

当你在写自己的Arduino库文件的时候,会发现Arduino确实很奇葩,自己写好的.h和.cpp文件用#include<…>的形式直接引用的时候,Arduino IDE会认为这个文件不存在而报错。而且在一个项目中可以建立多个Sketch文件,不用#include<…>就可以直接合并到一块。

不扯了,那么在Arduino IDE中,究竟是如何编译执行的呢?其实,官网上有一篇很好的说明文档,Arduino Build Process

https://www.arduino.cc/en/Hacking/BuildProcess

从刚开始在Arduino IDE上书写的类C语言代码,再到最终可以在Arduino开发板子上运行的程序,其实,大概经过了下面几个步骤:

首先,Arduino IDE对代码进行转换,确保生成正确的C/C++代码(两种常用的编程语言);

再通过avr-gcc编译器将上一步生成的代码编译成机器能识别的指令,或者可以称之为目标文件;

然后,通过链接器将上一步生成的目标文件与标准的Arduino库文件(比如说,digitalWrite()等)共同链接,生成一个.hex文件,这个hex文件中的指定内容将被写入到Arduino开发板上的单片机的闪存中。

最后,再将hex文件上传到Arduino的板子上,比如说用USB或者串口,通过板子上已经有的bootloader传输到Arduino板,当然也可以通过其他的工具直接烧写。

文件预处理

对于在Arduino IDE中写好的源码,即sketch文件,以.ino为后缀,在把这个文件传给avr-gcc编译器之前,Arduino IDE会对这个sketch文件进行一系列预处理。在一开头加入#include "WProgram.h"(0023版本)或者 #include"Arduino.h"(1.0版本),Arduino.h的源码见附录,主要包含Arduino标准核心库所需的所有声明;Arduino IDE再遍历所有的.ino 文件中定义的函数,为它们创建函数原型,这些声明将被插在最前面的的注释、预处理语句(#include或#define)之后,其它语句(包括类型定义)之前。若在函数中使用了自定义类型,则需要将该类型的定义单独放入一个头文件中。然后将所有 .ino 文件拼接起来,最终当前目标板的main.cxx文件中的所有内容,追加在主sketch文件之后。

此外,Arduino IDE支持多种目标板与多种芯片、CPU频率、bootloader等,这些都是在板配置文件中进行定义,配置文件中包含name、mcu、单片机的时钟频率、链接时的核心库等信息。

编译及链接

Arduino IDE使用avr-gcc来编译程序文件。

首先,程序文件所在目录、目标板目录(/hardware/cores/<CORE>)和avr的include目录(/hardware/tools/avr/avr/include/),以及主程序文件引用的头文件所在的函数库目录(/hardware/libraries)会被加入到一个引用目录列表。

从Arduino IDE的源码中就可以知道:Arduino/app/src/processing/app/目录下的Sketch.java文件中,有下面这么一段代码:

public void importLibrary(UserLibrary lib) throws IOException {
    importLibrary(lib.getSrcFolder());
  }

  /**
   * Add import statements to the current tab for all of packages inside
   * the specified jar file.
   */
  private void importLibrary(File jarPath) throws IOException {
    // make sure the user didn't hide the sketch folder
    ensureExistence();

    String list[] = Base.headerListFromIncludePath(jarPath);
    if (list == null || list.length == 0) {
      return;
    }

    // import statements into the main sketch file (code[0])
    // if the current code is a .java file, insert into current
    //if (current.flavor == PDE) {
    if (hasDefaultExtension(current.getCode())) {
      setCurrentCode(0);
    }
    // could also scan the text in the file to see if each import
    // statement is already in there, but if the user has the import
    // commented out, then this will be a problem.
    StringBuilder buffer = new StringBuilder();
    for (String aList : list) {
      buffer.append("#include <");
      buffer.append(aList);
      buffer.append(">\n");
    }
    buffer.append('\n');
    buffer.append(editor.getText());
    editor.setText(buffer.toString());
    editor.setSelection(0, 0);  // scroll to start
    setModified(true);
  }

可以很清晰地看到Arduino IDE将库的路径添加到Compiler.headerListFromIncludePath中,然后在Editor中写入"#include"中。那些标准的库文件、 avr 头文件、 C 标准库文件都会被加入到引用目录列表中。

Arduino IDE 会扫描 main.cxx (即所有的 .ino 文件)中的#include < ...> ,如果无法在当前引用目录里找到的话,就会去 library 目录下查找所有子目录(只找一级),如果找到,就将其加入引用目录列表之中。

当编译一个sketch程序文件时,将在系统临时目录(如Mac里的/tmp)中进行构建。当上传一个sketch程序文件时,将在程序文件所在目录(可通过Sketch > Show Sketch Folder菜单进行访问)的applet/子目录中构建。

目标板核心的.c与.cpp文件将在同级目录下被编译生成.o文件,主程序文件、程序其它.c和.cpp文件及#include包含的函数库中的.c或.cpp文件同样处理。

这些.o文件将最终生成一个静态库,主程序文件与之链接。只有主程序中使用到的库代码才会被写入到最终的.hex文件中,所以这样做就减少了绝大多数程序的大小。

也就是说,对于普通的 C/C++ 文件,则会单独编译成一个静态库,最后和 main.cxx 以及各个 library 的编译结果链接起来。.hex文件是编译的最终文件,然后被上传到Arduino板。

附录

Arduino.h的源码:

/*
  Arduino.h - Main include file for the Arduino SDK
  Copyright (c) 2005-2013 Arduino Team.  All right reserved.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#ifndef Arduino_h
#define Arduino_h

#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>

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

#include "binary.h"

#ifdef __cplusplus
extern "C"{
#endif

void yield(void);

#define HIGH 0x1
#define LOW  0x0

#define INPUT 0x0
#define OUTPUT 0x1
#define INPUT_PULLUP 0x2

#define PI 3.1415926535897932384626433832795
#define HALF_PI 1.5707963267948966192313216916398
#define TWO_PI 6.283185307179586476925286766559
#define DEG_TO_RAD 0.017453292519943295769236907684886
#define RAD_TO_DEG 57.295779513082320876798154814105
#define EULER 2.718281828459045235360287471352

#define SERIAL  0x0
#define DISPLAY 0x1

#define LSBFIRST 0
#define MSBFIRST 1

#define CHANGE 1
#define FALLING 2
#define RISING 3

#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__) || defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
#define DEFAULT 0
#define EXTERNAL 1
#define INTERNAL 2
#else  
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1284__) || defined(__AVR_ATmega1284P__) || defined(__AVR_ATmega644__) || defined(__AVR_ATmega644A__) || defined(__AVR_ATmega644P__) || defined(__AVR_ATmega644PA__)
#define INTERNAL1V1 2
#define INTERNAL2V56 3
#else
#define INTERNAL 3
#endif
#define DEFAULT 1
#define EXTERNAL 0
#endif

// undefine stdlib's abs if encountered
#ifdef abs
#undef abs
#endif

#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
#define abs(x) ((x)>0?(x):-(x))
#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
#define round(x)     ((x)>=0?(long)((x)+0.5):(long)((x)-0.5))
#define radians(deg) ((deg)*DEG_TO_RAD)
#define degrees(rad) ((rad)*RAD_TO_DEG)
#define sq(x) ((x)*(x))

#define interrupts() sei()
#define noInterrupts() cli()

#define clockCyclesPerMicrosecond() ( F_CPU / 1000000L )
#define clockCyclesToMicroseconds(a) ( (a) / clockCyclesPerMicrosecond() )
#define microsecondsToClockCycles(a) ( (a) * clockCyclesPerMicrosecond() )

#define lowByte(w) ((uint8_t) ((w) & 0xff))
#define highByte(w) ((uint8_t) ((w) >> 8))

#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) : bitClear(value, bit))

// avr-libc defines _NOP() since 1.6.2
#ifndef _NOP
#define _NOP() do { __asm__ volatile ("nop"); } while (0)
#endif

typedef unsigned int word;

#define bit(b) (1UL << (b))

typedef bool boolean;
typedef uint8_t byte;

void init(void);
void initVariant(void);

int atexit(void (*func)()) __attribute__((weak));

void pinMode(uint8_t, uint8_t);
void digitalWrite(uint8_t, uint8_t);
int digitalRead(uint8_t);
int analogRead(uint8_t);
void analogReference(uint8_t mode);
void analogWrite(uint8_t, int);

unsigned long millis(void);
unsigned long micros(void);
void delay(unsigned long);
void delayMicroseconds(unsigned int us);
unsigned long pulseIn(uint8_t pin, uint8_t state, unsigned long timeout);
unsigned long pulseInLong(uint8_t pin, uint8_t state, unsigned long timeout);

void shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val);
uint8_t shiftIn(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder);

void attachInterrupt(uint8_t, void (*)(void), int mode);
void detachInterrupt(uint8_t);

void setup(void);
void loop(void);

// Get the bit location within the hardware port of the given virtual pin.
// This comes from the pins_*.c file for the active board configuration.

#define analogInPinToBit(P) (P)

// On the ATmega1280, the addresses of some of the port registers are
// greater than 255, so we can't store them in uint8_t's.
extern const uint16_t PROGMEM port_to_mode_PGM[];
extern const uint16_t PROGMEM port_to_input_PGM[];
extern const uint16_t PROGMEM port_to_output_PGM[];

extern const uint8_t PROGMEM digital_pin_to_port_PGM[];
// extern const uint8_t PROGMEM digital_pin_to_bit_PGM[];
extern const uint8_t PROGMEM digital_pin_to_bit_mask_PGM[];
extern const uint8_t PROGMEM digital_pin_to_timer_PGM[];

// Get the bit location within the hardware port of the given virtual pin.
// This comes from the pins_*.c file for the active board configuration.
// 
// These perform slightly better as macros compared to inline functions
//
#define digitalPinToPort(P) ( pgm_read_byte( digital_pin_to_port_PGM + (P) ) )
#define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) )
#define digitalPinToTimer(P) ( pgm_read_byte( digital_pin_to_timer_PGM + (P) ) )
#define analogInPinToBit(P) (P)
#define portOutputRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_output_PGM + (P))) )
#define portInputRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_input_PGM + (P))) )
#define portModeRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_mode_PGM + (P))) )

#define NOT_A_PIN 0
#define NOT_A_PORT 0

#define NOT_AN_INTERRUPT -1

#ifdef ARDUINO_MAIN
#define PA 1
#define PB 2
#define PC 3
#define PD 4
#define PE 5
#define PF 6
#define PG 7
#define PH 8
#define PJ 10
#define PK 11
#define PL 12
#endif

#define NOT_ON_TIMER 0
#define TIMER0A 1
#define TIMER0B 2
#define TIMER1A 3
#define TIMER1B 4
#define TIMER1C 5
#define TIMER2  6
#define TIMER2A 7
#define TIMER2B 8

#define TIMER3A 9
#define TIMER3B 10
#define TIMER3C 11
#define TIMER4A 12
#define TIMER4B 13
#define TIMER4C 14
#define TIMER4D 15
#define TIMER5A 16
#define TIMER5B 17
#define TIMER5C 18

#ifdef __cplusplus
} // extern "C"
#endif

#ifdef __cplusplus
#include "WCharacter.h"
#include "WString.h"
#include "HardwareSerial.h"
#include "USBAPI.h"
#if defined(HAVE_HWSERIAL0) && defined(HAVE_CDCSERIAL)
#error "Targets with both UART0 and CDC serial not supported"
#endif

uint16_t makeWord(uint16_t w);
uint16_t makeWord(byte h, byte l);

#define word(...) makeWord(__VA_ARGS__)

unsigned long pulseIn(uint8_t pin, uint8_t state, unsigned long timeout = 1000000L);
unsigned long pulseInLong(uint8_t pin, uint8_t state, unsigned long timeout = 1000000L);

void tone(uint8_t _pin, unsigned int frequency, unsigned long duration = 0);
void noTone(uint8_t _pin);

// WMath prototypes
long random(long);
long random(long, long);
void randomSeed(unsigned long);
long map(long, long, long, long, long);

#endif

#include "pins_arduino.h"

#endif

  • 12
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值