We’re just around the corner from writing code for the modules on the I2C and SPI Education Shield, but before we get into that, I wanted to quickly explain how I structure my Arduino code for legibility and ease of use. There are a number of conventions that are followed and some very specific locations some parts need to show up in, but how the code is written and how it’s formatted can take your code from mess to magnificent very easily.
Objectives
- Learn that Dan is not a professional developer.
- Understand how constants are used to establish names for registers.
- See how indentation and white space can be used to increase ease of reading.
- Learn general ideas for creating your own functions.
Background
The Arduino Wire Library
Official Arduino Wire Library Reference
Schematic
No schematic is associated with this module.
Setup
No setup is required.
Basic Locations
Some things just need to go in specific places. When you’re dealing with libraries, you’ll need to make sure that you include the library at the top of your code, and if you need to initialize things, you’ll need to do that in the setup. You’ll also want to create a title block at the top of your code to remind yourself a year down the road what it is your code does. My personal preference is to place any functions I write, below the main loop(), but that’s not a hard and fast rule. I have not found any useful way of organizing the functions themselves in any way that isn’t just as difficult to work with in the Arduino IDE as any other.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/* This is some title block of code to explain what this sketch does. Typically people include some basic description and then possibly licensing information too. If you need to attribute ownership of the code or any part of the code to another author to adhere to the code licensing, you would put the attribution here. Including name, date and a website wouldn't hurt. */ #include "Wire.h" // Here we need to include the Arduino Wire Library for I2C stuff. void setup() { Wire.begin(); // This is initializing all the buffers behind the scenes initMyChip(); // If you have a list of initializations necessary, call those functions initMyOtherChip(); //Serial.begin(9600); // Chances are you'll need it, so at least have it here commented out. } void loop() { // This is where your main code goes } void initMyChip() { //Some Initialization Code } void initMyOtherChip() { //Some Initialization Code } |
Making Register Values Stand Out In Arduino Code Structure
When you start working with more advanced chips, you’ll discover that they have all kinds of registers, and each of them will have a discrete hex address that you won’t be able to remember. These will never change when your code is processing, so it’s best to create constants to use in your code. I also use this for establishing the I2C address of the chip I’m using. The constant declarations go between your includes and your setup.
When you’re looking at the following code, notice that the constant names are all different lengths and the constant type definitions are different lengths. If you don’t use whitespace to help your eyes, it looks like this…
1 2 3 4 5 |
#include "Wire.h" // Here we need to include the Arduino Wire Library for I2C stuff. const int MYFUNKYCHIP_I2C = 0x48; // I2C Address for the Rheingold Heavy Funk Sensor const byte REG_FUNKINESS_LEVEL = 0x00; // Register containing level of funk const byte REG_FUNK_CONFIG = 0x01; // Register containing funk configuration values |
Now if we use a little tab’ing and a little extra space, we can make this way more easy to read by eliminating the jumbled mess…
1 2 3 4 5 |
#include "Wire.h" // Here we need to include the Arduino Wire Library for I2C stuff. const int MYFUNKYCHIP_I2C = 0x48; // I2C Address for the Rheingold Heavy Funk Sensor const byte REG_FUNKINESS_LEVEL = 0x00; // Register containing level of funk const byte REG_FUNK_CONFIG = 0x01; // Register containing funk configuration values |
As we move into different chips on the Education Shield, you’ll discover Operation Codes as well as registers that will also need their own constants assigned. I prepend the constant names with “REG_” or “OPCODE_” to tell them apart.
More Examples Of White Space
I use the white space trick all over when I can. It makes everything much easier to comprehend. Here is an example of debug serial output before white space and after white space.
1 2 3 4 5 6 7 8 9 10 11 12 |
void loop() { intFunk = getFunkLevel(); Serial.print ("The raw funk level is: "); Serial.print (intFunk, HEX); Serial.println (" reported from the ADC."); Serial.print ("The adjusted funk level is: "); Serial.print ((intFunk * (1.8)) + 32); Serial.println (" Bootsys"); Serial.println (); delay (100); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void loop() { intFunk = getFunkLevel(); Serial.print ("The raw funk level is: "); Serial.print (intFunk, HEX); Serial.println (" reported from the ADC."); Serial.print ("The adjusted funk level is: "); Serial.print ((intFunk * (1.8)) + 32); Serial.println (" Degrees Bootsy"); Serial.println (); delay (100); } |
Yes. Funk levels are measured in degrees of Bootsy Collins.
When to Function and When Not to Function
Functions are magic creatures that can sometimes be too small, sometimes be too big, and very occasionally, be exactly the write size to bring order out of chaos.
Typically, when you write to an I2C chip, you’ll need to issue four commands: You send the I2C address, you tell the chip which register you’ll be writing to, you send the register value, and then you kick it all off by issuing the endTransmission() command. Again, notice the use of white space…
1 2 3 4 5 6 7 |
void setup_FunkSensor() { Wire.beginTransmission (MYFUNKYCHIP_I2C); // Start comms Wire.write (REG_FUNK_CONFIG); // Select configuration register Wire.write (0x1F); // Set 16-bit funk resolution Wire.endTransmission (); // Write values to Funk Sensor } |
General if a discrete bunch of commands is only four well packaged lines long, I’m ok with leaving it in the body of the loop, because it’s easy to recognize, especially if I only use it once. However, if I call it multiple times in different areas, that’s a good reason to break it off into it’s own function.
Additionally, if you’re going to need to use the code when your Arduino is acting like a slave component, then you must have a function ready to declare in the onReceive() and onRequest() commands.
Finally, any setup routine that I need, which typically involves multiple configuration assignments across several registers and conceivably several chips, will always receive it’s own function called either “SetupMyChip” or “InitMyChip” so that it is neatly contained away from everything else, including intrusive variables that aren’t properly scoped.