top of page

Understanding the Arduino Sketch Language

Updated: Apr 30, 2023

When I first started playing with Sketch and Arduino, it seemed to me that Sketch was some sort of pseudo language, a trans-language using some sort of cross-language compiler, meaning that it looked like it was compiled from Sketch C into C++. But after the Bald Engineer (www.baldengineer.com) pointed out that it was fully C++ and suggested that I take a look at the source to really understand it, I finally caught on. After taking his recommendation and cracking it open, I found it pretty interesting. In my opinion, understanding what is going on isn't just interesting, but helps you better under the design, and that helps you to better approach your solutions and write better code.


I wasn't too far off in thinking it was some sort of cross-language compiler from sketch to C++. I think that way because I have been writing cross-language or source-to-source compilers for the past 15-years. Sketch is actually a subset of C++ that is passed through a pre-processor where it combines the source, picks up dependencies, and for AVR based boards, compiles the code using avr-gcc and avr-g++. The final hex file is generated and upload to the board using a bootloader called avrdude.


Knowing how it works, we can take advantage of a few things to make our projects more powerful and more interesting. Here's a few things I found interesting that I think you should be aware of.

Multiple sketch files are compiled into a single...

If you click on the small down arrow on the right side of the IDE, you can add additional tabs. What is actually happening is that a new .ino sketch file is added to your sketch folder. When the sketch is compiled, all of the .ino files are combined into a single file. This is a good way to break up large and complex sketches into smaller files that are easier to manage. It also gives you an easy way to copy the .ino files between sketch's without having to copy and paste code. While not as nicely managed as it could be, it does work. I think this also explains Arduino's calling the language Sketch when it is really just C++.


But don't get confused about what is going on. While it creates multiple .ino files, it is really just breaking a single .ino file into multiple parts. So if you define a variable on tab one, it will be usable on tab two. So unlike creating a separate C++ file and a new class, all of the tabs are actually breaking up a single .ino file and will be merged together by the pre-processor during the compile.



Function Prototypes

Function prototypes (function declarations) are generated for your sketch's setup() and loop() functions, as well as any other function definitions in your sketch. However, you can provide your own prototypes if you wish. Here's an example of prototypes for the setup() and loop() functions. This will compile without error. The first three lines are the prototype declarations and are optiona, as Arduino will actually create the prototypes for you. But occasionaly that process will fail and you will get an error. If you ecounter that error, you can add the prototype yourself to resolve.

void setup();
void loop();
int add(int x, int y);

void setup() {
  int total = add(1,2);
}

void loop() {  
}

int add(int x, int y) {
  return x + y;
}

Arduino's Build Process

To find out more information about the build process, you can take a look at Arduino's documentation at https://arduino.github.io/arduino-cli/sketch-build-process/.


Looking at Main.cpp

Sketch is fully C++. In fact, you can take a look at the main.cpp class to see what it going on. Taking out the comments, we get the following. Note that we have a couple of functions, setup() and loop(). So as you might expect, the Arduino IDE takes your Sketch's functions and appends them to main.cpp. Pretty simple really.

#include <Arduino.h>

int atexit(void (* /*func*/ )()) { return 0; }
void initVariant() __attribute__((weak));
void initVariant() { }

void setupUSB() __attribute__((weak));
void setupUSB() { }

int main(void)
{
	init();

	initVariant();

#if defined(USBCON)
	USBDevice.attach();
#endif
	
	setup();    
	for (;;) 
	{
		loop();
		if (serialEventRun) serialEventRun();
	}
        
	return 0;
}

If you are like me, the first question I had was, can I change the core classes? Well, you can. But that doesn't mean you should. But lets see what we can do.


Start by locating the Arduino install folder on your system. On my system, I found it at:

...\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino\


This is where you will find the source used by the Arduino pre-processor. But why would you change main.cpp? Well, if you have some common code you want to include but you do not want to create a library, you could update main.cpp to include it. Maybe you don't like how setup() and loop() function and you want to adjust it. While I would not recommend that, at least you will know how if you ever find a reason to do so.


However, consider that if you do, when you upgrade the Arduino IDE, you could lose these changes. So make a backup copy if you change anything. A better approach might be adding additional .ino files to your sketch or creating your own libraries. But you may still find that you want to edit the core files. Just be sure to backup a copy.


Here's an example of where I pulled out the loop function from main.cpp so I can control it myself. I also added two additional libraries that I might want to include in all of my sketches. I decided to use "execute" as my function name and I added a prototype for the function at the top of the code. Note: You cannot use the Arduino IDE to edit the main.cpp. I use either Visual Studio Code or Notepad++ to edit.


A better option might be to leave the loop alone and add an execute function outside of the loop. That way I can use either or both as needed.

#include <string.h>
#include <avr/sleep.h>
#include <Arduino.h>

void execute(); // function prototype

// Declared weak in Arduino.h to allow user redefinitions.
int atexit(void (* /*func*/ )()) { return 0; }

// Weak empty variant initialization function.
// May be redefined by variant files.
void initVariant() __attribute__((weak));
void initVariant() { }
void setupUSB() __attribute__((weak));
void setupUSB() { }

int main(void)
{
	init();
	initVariant();
	
#if defined(USBCON)
	USBDevice.attach();
#endif

	setup();
	//for (;;) {
		//loop();
	execute();
		if (serialEventRun) serialEventRun();
	//}
	return 0;
}

Now in the sketch itself, I will need to provide the code for the execute() and loop() functions.

void setup() {}

void execute() 
{    
  for (;;) {  
      loop();    
  }
}

void loop() { }      

You should be asking yourself, "Why?"

To start, I just wanted to see what I could and could not do. But in the back of my head, I am thinking about ways to better handle a low-power mode and having the board go to sleep. For this, I'd like the loop to only run once, then go into low-power or sleep mode. It seems that to have a loop that doesn't loop, is a bit odd. So I'm wondering what my options are if I remove it... or I could just not use it.


While at this time I have not worked through how to do this, it is what got me thinking this way. If I can put the board to sleep and wake it up with a reset button or maybe a button click or something else that I can program, I might be able to create a sensor that turns itself off and on every few hours or days and reports back to a parent website, and doesn't sit and monitor 24/7 and run my battery down. One thought I had was to use a solar panel to charge a capacitor and use a comparator to monitor the voltage. Once the battery gets to a given voltage, the comparator would enable a reset on the board and run through the setup() and execute() functions.


I'm weighing my options between using sleep.h functions and not having the code perform the loop. I suspect I can just run it out of setup, but that would require a reboot everything time. That may work fine, but I want to know my options and what sort of tools I have at my disposal.


Here's one why you could implement it. Just add the execute() function and if you use it, great, if not, just return false and it will be ignored.

    setup();
    if (execute()) // if true the sketch is handling the loop.
    {
    	for (;;) {
        	loop();		
                if (serialEventRun) serialEventRun();
        }		
    }
    
    if (serialEventRun) serialEventRun(); // if loop is not ran.
    return 0;

I'm not sure where I will take this, but I'm looking at my options. While I don't have to modify main.cpp to accomplish this, I want to have it as an option. For me, it is another tool at my disposal.


If you have some ideas or experience modifying main, post a comment and explain what you used it for.

13 views0 comments
bottom of page