July 4

Weather Station Project: ESP32 with LoRa Telemetry

Heltec ESP32 with LoRa radio

Monsoon season is rapidly approaching. Last year my neighbors were heavily affected by the weather when we received over a foot of rainfall in just a few hours. Flooding was well over 8 feet in some low lying areas. For my neighbors down the road, who park under their apartment complex, over 30 vehicles were lost during the storm.

In an effort to help my neighbors, I am working on an early warning storm detection system. Ultimately, I have two separate projects in mind: a weather station and a storm warning sentinel. As alluded to in my previous post on project management, I have split the ultimate goal of the early warning detection system up into the two separate projects. The prerequisite steps learned in the weather station project will enable the storm warning sentinel project.

Today marks an exciting day for The Engineer’s Workshop. Today, I am kicking off a new feature of this blog- a recurring series of projects. Each project will start with a design overview followed by a timeline of the intermediate steps devised to build the project. Subsequent posts for the project will then be dedicated to each intermediate step. Let’s get started on building our weather station!

The first step you should take on any engineering project should be researching the design. Time spent designing your project up front will pay dividends later. When I design an engineering solution, I start off with clearly defining the goal. After that, I define the requirements/criteria. With microcontroller projects such as this, I also explicitly list the inputs and outputs involved so that I don’t forget them in my design. In the case of this weather station project, I came up with the following design overview:

Outdoor Weather Station:

Goal: Create an outdoor weather station to detect ambient weather conditions and transmit to a listening station. The listening station will record the data to a SQL table which will then be made visible via an internet dashboard.

Requirements:

- Weatherproof
- Self-powered
- Wireless radio transmission

Inputs: 

1) Temperature
2) Barometric pressure
3) Humidity

Outputs:

- Transmit data to listening station over LoRa radio.
Weather station design document

I am a visual thinker so I also like to sketch out a rough high-level design:

Weather monitoring station transmits to listening station over LoRa which then saves off the information to my Web/SQL server.
Weather monitoring station transmits to listening station over LoRa which then saves off the information to my Web/SQL server.

The design I’ve come up with is that I will have an outdoor weather monitoring station which will transmit (over LoRa) to a listening station I have indoors. The listening station will be connected to a web/SQL server via wifi. Any information it receives from the weather monitoring station, it will turn over to the SQL server for storage.

With the above design for my weather station, the project checkpoints easily reveal themselves:

Project plan for weather station
Project plan for weather station

Breaking down the project, we see the following steps are necessary:

  1. Create a web dashboard to display the data
    • For this, I have chosen to use Django
  2. Interface basic sensors with ESP32 for the Weather Monitoring Station
  3. Interface listening Station with SQL Server to log data
  4. Connect Weather Monitoring Station to Listening Station via LoRa
  5. Polish Django dashboard

Stay tuned! In subsequent posts, we will build out each objective in turn.

April 28

Expand Your Arduino’s Storage with an External EEPROM Part II: Reading from the AT24C256 – A Tutorial in How to Use the I2C Protocol Continued

Wiring Diagram Showing Connections between AT24C256 and Arduino

We first began our journey into learning the I2C protocol three weeks ago. In that post, we learned to write to an external EEPROM over the I2C protocol using nothing more than a datasheet and the Arduino’s built-in Wire library. Before learning to read from that EEPROM, which we will do today, we needed to gain the prerequisite knowledge of how data is stored in memory and how pointers work. From there, we learned how the data stored in these variables is passed along through to functions and what an array really is.

It’s been a daunting few weeks, but we’re finally there. Let’s read the data we wrote to our EEPROM armed with nothing more than the datasheet and the I2C protocol.

1. Review the Datasheet

Using the same strategy as before, we look for the command we’re interested in on the datasheet. Since we last wrote to the EEPROM using a page write, it should be pretty easy to guess that to read off our same data, we probably want a page read (also known as a sequential read).

Going to the Sequential Read section of the datasheet, we’re given the following description:

SEQUENTIAL READ: Sequential reads are initiated by either a current address read or a random address read. After the microcontroller receives a data word, it responds with an acknowledge. As long as the EEPROM receives an acknowledge, it will continue to increment the data word address and serially clock out sequential data words. When the memory address limit is reached, the data word address will “roll over” and the sequential read will continue. The sequential read operation is terminated when the microcontroller does not respond with a zero but does generate a following stop condition (see Figure 12 on page 12).

Atmel: https://www.mouser.com/datasheet/2/268/doc0670-1180619.pdf

From here, the code practically writes itself, we just need to follow the directions Atmel has given us.

2. Write the Preamble:

Per the datasheet: “Sequential reads are initiated by either a current address read or a random address read.” Well, we wrote our data to a specific address, so we want to read from that address, so we’ll initiate the sequential read by using the random read. Referring to the Random Read section of the datasheet:

RANDOM READ: A random read requires a “dummy” byte write sequence to load in the data word address. Once the device address word and data word address are clocked in and acknowledged by the EEPROM, the microcontroller must generate another start condition. The microcontroller now initiates a current address read by sending a device address with the read/write select bit high. The EEPROM acknowledges the device address and serially clocks out the data word. The microcontroller does not respond with a zero but does generate a following stop condition (see Figure 11 on page 12).

Atmel: https://www.mouser.com/datasheet/2/268/doc0670-1180619.pdf
Random Read Preamble: dummy byte write with “the device address word and data word address are clocked in and acknowledged by the EEPROM”

The above-boxed section represents the “the device address word and data word address are clocked in and acknowledged by the EEPROM” portion. We’ll start by coding this section. Thankfully, we’ve already done it in the Page Write post- it’s everything in section 2:

Wire.beginTransmission(0b1010000);
Wire.write(0b0000000); // 7 bits of 0s; this method takes a byte though so it will still transmit a byte's worth of 0s.
Wire.write(0b00000000);
Wire.endTransmission();

Boom. Header done.

3. Read the Data:

RANDOM READ: A random read requires a “dummy” byte write sequence to load in the data word address. Once the device address word and data word address are clocked in and acknowledged by the EEPROM, the microcontroller must generate another start condition. The microcontroller now initiates a current address read by sending a device address with the read/write select bit high. The EEPROM acknowledges the device address and serially clocks out the data word. The microcontroller does not respond with a zero but does generate a following stop condition (see Figure 11 on page 12).

Atmel: https://www.mouser.com/datasheet/2/268/doc0670-1180619.pdf

The start condition (with sending a device address) is common to the I2C protocol and is thankfully handled by the Wire library with the simple Wire.requestFrom method, where the first argument is the device address and the second argument is the number of bytes to request from the EEPROM:

Wire.requestFrom(deviceaddress,2);

Since I only wrote two bytes “Hi” to the address, I’m only requesting two bytes back.

Now to actually read the data, we’ll use a simple while loop:

while(Wire.available()) {
    Serial.print((char)Wire.read());
}

Putting together the full code in its entirety from the past two tutorials:

C source code for Arduino implementing the I2C protocol with an EEPROM.
Source code for both writePage and readPage.

Here is what she looks like on the Serial Monitor:

Serial monitor showing EEPROM read and write.
Serial output showing EEPROM read and write.

In the future, we’ll eventually revise this code to make it more versatile by putting it in a library.

April 25

Variables, Pointers, and Indirection in Arduino C

Before we continue on with learning about the I2C protocol and our EEPROM project, we need to discuss variables: what they are and what goes on behind the scenes. Knowledge of how variables work and the use of pointers and indirection with arrays will serve us well when it comes time to read from our EEPROM. Let’s begin.

Anatomy of a Variable:

1. What is a variable?

Simply put, variables hold data. More specifically, a variable holds data of a specific data type. For example, an int holds an integer, a string contains a collection of chars, etc.

2. What goes on behind the scenes when a variable is defined and when it is assigned?

When you define a variable, the compiler goes and checks the symbol table (basically a list of variables that have previously been declared) to see if that variable already exists. If it doesn’t, the compiler goes ahead and adds the new variable to the list.

Say, for example, you add the following statement:

int myVar;

Since our variable has not already been declared (it doesn’t already exist in the table), the compiler updates the symbol table so it now looks like this:

Symbol table with myVar declared (but not yet defined) since it lacks a location in memory (lvalue).
Symbol table after myVar declared- note the lack of an lvalue. This is because myVar is not yet defined. rvalue is also unknown because we haven’t assigned a value to myVar yet.

Now, technically, the variable has only been declared at this point- it’s missing an actual location in memory. To get this location in memory, the compiler requests a place to put this variable from the system’s memory manager. The memory manager then responds with a memory address which the compiler then adds to the symbol table for that variable. This memory address is known as an lvalue (lvalue = location value) and it merely represents where the variable can be found in memory. With this addition of the lvalue to the symbol table, our variable is now defined:

myVar now defined in the symbol table (myVar now has an lvalue).
Symbol table with myVar defined- this means that the variable now has a location in memory (lvalue).

With our new variable defined, we can now move on to storing a value in it. Fortunately, assigning a value to a variable is rather straightforward. When we assign a value to a variable, we directly navigate to the variable’s location in memory (the lvalue) and update the memory at that address with the new value. The data that’s actually stored in memory is known as the rvalue (rvalue = register value).

Continuing our example with the following assignment statement:

myVar = 10;

With this assignment, our symbol table now looks like this:

myVar after rvalue assignment
Symbol table after assignment- note the updated rvalue which holds our data value.

Another way to visualize what we have just gone over is with an lvalue-rvalue diagram:

lvalue-rvalue diagram for a value type variable
lvalue-rvalue diagram

This diagram is why you will see some people refer to the memory address as the “left value” and the actual data value as the “right value”.

  • There’s also an important caveat here: in Arduino, and C in general, there is no duty to clear that rvalue at our variable’s lvalue when we define it. Therefore you should always assume that a variable’s value contains whatever garbage was originally in that memory location unless we’ve explicitly assigned a value to the variable. (i.e., Don’t assume it’s 0 or null). Therefore it’s probably best to go ahead and initialize your variable with a value when you define it.
    Let’s summarize: Whenever your program needs to use the value stored in a variable, it uses the variable’s lvalue to go to that memory address and retrieves the data (rvalue) from that memory location.

Pointers:

Now that we’ve covered what variables are and how they really work, we’re ready to understand pointers. Simply put, a pointer is nothing more than a variable that references the memory address of another variable. Using the terminology that we’ve just learned, a pointer is a variable whose rvalue is the lvalue of another variable.

To visualize this, let’s take a look at two lvalue-rvalue diagrams representing the value type variable myVar and the reference type variable myPointer:

myPointer referencing myVar - Notice how the rvalue of myPointer is the memory address of myVar.
myPointer referencing myVar – Notice how the rvalue of myPointer is the memory address of myVar.

Declaring a Pointer:

Declaring a pointer variable is rather straightforward:

int *myPointer;

The type specifier (int in this case) must match the data type of the variable the pointer is to be used with. The asterisk indicates to the compiler that myPointer is a pointer. Since whitespace doesn’t really matter in C, the asterisk can be placed anywhere between the type specifier and the pointer variable name so you will sometimes also see: int* myPointer, int * myPointer, etc.

The Address-Of Operator:

By itself, a pointer that is defined but does not actually point to anything is a pretty pointless pointer (ha!). To point it to the memory address of another variable we simply need to assign the pointer the memory address of that variable. But where do we get the memory address from? That is, where do we get the lvalue of myVar from? Enter the address-of operator (&).

The address-of operator is a unary operator that returns the lvalue of a variable.

Pointer Assignment:

To point our new pointer at the memory location of our value type variable, myVar, we simply call the following statement:

myPointer = &myVar;

This completes the link shown in the previous diagram and is known as referencing. It is for this same reason that the address-of operator (&) is also known as the “referencing operator“.

Whenever you are learning a new concept, it’s a good idea to try it out yourself to prove to yourself what you’ve read. Let’s mock up an example of what we’ve learned so far in the Arduino IDE:

void setup() {
  Serial.begin(9600);
  
  int myVar = 10;  // Initialize a variable.
  
  Serial.print("myVar's lvalue: ");
  Serial.println((long) &myVar, DEC);  // Grab myVar's lvalue
  Serial.print("myVar's rvalue: ");
  Serial.println(myVar, DEC);
  Serial.println();
  
  int *myPointer;   // Declare your pointer.
  myPointer = &myVar; //Assign myVar's memory address to pointer.
  
  Serial.print("myPointer's lvalue: ");
  Serial.println((long) &myPointer, DEC);  //myPointer's lvalue
  Serial.print("myPointer's rvalue: ");
  Serial.println((long) myPointer, DEC);  //myPointer's rvalue
}

void loop() {
}

Watching the serial monitor, what you should see is something like this:

Serial log showing that the rvalue of a pointer is the memory address of the value type variable it references.
Note that the rvalue of myPointer is the same as myVar’s lvalue.

Notice that myPointer’s rvalue is the memory address of myVar (i.e. myVar’s lvalue), just like it shows in the diagram.

Indirection (Dereferencing):

We just saw that a pointer can reference a location in memory by assigning that pointer a variable’s memory address using the reference operator (&). We can take this a step further and obtain the actual value stored at that memory address by dereferencing the pointer. This is also known as indirection and is accomplished via the indirection operator (*) with your pointer. Example:

*myPointer = 5; // Go to memory addressed stored in myPointer's rvalue (myVar's lvalue) and place the value 5 in that memory address.

Continuing off our previous Arduino code example:

void setup() {
  Serial.begin(9600);
  
  int myVar = 10;
  
  Serial.print("myVar's lvalue: ");
  Serial.println((long) &myVar, DEC);
  Serial.print("myVar's rvalue: ");
  Serial.println(myVar, DEC);
  Serial.println();
  
  int *myPointer;
  myPointer = &myVar;
  
  Serial.print("myPointer's lvalue: ");
  Serial.println((long) &myPointer, DEC);
  Serial.print("myPointer's rvalue: ");
  Serial.println((long) myPointer, DEC);
  Serial.println();

  *myPointer = 5;  //THIS IS OUR DEREFRENCING ADDITION.
  Serial.println("-----------------------");
  Serial.println("Updating *myPointer = 5");
  Serial.println();

  Serial.print("myPointer's lvalue: ");
  Serial.println((long) &myPointer, DEC);
  Serial.print("myPointer's rvalue: ");
  Serial.println((long) myPointer, DEC);
  Serial.println();

  Serial.print("myVar's lvalue: ");
  Serial.println((long) &myVar, DEC);
  Serial.print("myVar's rvalue: ");
  Serial.println(myVar, DEC);
  Serial.println();

}

void loop() {
}
dereferencing the pointer and assigning a value; we are able to manipulate the data stored in myVar
Notice that by dereferencing the pointer and assigning a value, we are able to manipulate the data stored in myVar.

Notice that nothing changed to myPointer at all (blue). Neither its lvalue nor its rvalue changed. Contrast that with myVar (red) which had it’s rvalue changed to 5 by the indirection operator we applied to our pointer.

That is the power of pointers and indirection. In my next journal entry, I will discuss pointers and arrays which will then allow us to finally move on to the last part of our EEPROM I2C project!

April 6

Expand Your Arduino’s Storage with an External EEPROM (AT24C256): A Tutorial in How to Use the I2C Protocol

In a previous post, we covered how to expand your number of analog inputs by using an external ADC over the SPI bus. Today I want to demonstrate how to use the I2C protocol while simultaneously teaching you how to read a datasheet. After all, there will come a day when you want to use a new device and a library won’t already exist for it.

First of all, let’s establish the Arduino library we will be implementing to communicate with the AT24C256 EEPROM. For this tutorial, we will be using the Wire library to implement the I2C protocol (Mental shortcut: I2C protocol = Wire library). Let’s begin!

1. Review the Datasheet

First, let’s survey the land and have a look at the AT24C256 datasheet. When I look at a datasheet, one of the first things I do is figure out what commands I want to use. In this case, I know I want to do a page write. A page write allows you to write multiple bytes of data all at once, as opposed to a byte write which is all sorts of tedious. I’m a visual learner so once I know what command I want to use, I jump to the figure that parses it out. The figure for Page Write is shown below:

Source: Atmel https://www.mouser.com/datasheet/2/268/doc0670-1180619.pdf

Just looking at this figure I see that every I2C message starts with three words: a device address, a first word address, and a second word address. This preamble is then followed with the actual data you want to write.

2. Writing Out the Code for the Address (i.e. the preamble)

From that diagram alone the code practically writes itself. It’s just a matter of knowing which methods to call in the Wire library. Let’s implement the code one word at a time:

Device Address:
Device Address Message
Device Address Highlighted in Red; Source: Atmel https://www.mouser.com/datasheet/2/268/doc0670-1180619.pdf

Focusing on this part of the diagram, we see that the device address begins with a 1 (the line is high), followed by a 0 (line is low), 1, and so on so that we ultimately end up with 0b1010000. The last two least significant bits (LSBs) are 0s but can actually be changed to allow for up to four of these EEPROMs to be on the I2C bus. This is also shown separately in the datasheet:

Source: Atmel https://www.mouser.com/datasheet/2/268/doc0670-1180619.pdf

“But wait!” you say, “That’s only 7 bits. A byte is eight bits. What gives?!”

Well, you are technically correct- to make the full byte, we should have an eighth bit that tells the device whether we want to read or write (the R/W in the LSB position shown above), but we are using the Arduino Wire library which expects a 7-bit address. If you are given an 8-bit address like in Figure 7 above, you need to bit shift one position to the right to get a 7-bit address. Notice that you don’t have to do this minor bit of mental gymnastics if you just look at the original diagram in Figure 9. The Wire library will handle appending the R/W bit based on what method you end up calling.

So to begin our I2C transmission, we simply call the following:

Wire.beginTransmission(0b1010000);
First Word Address:
First Word Address Message
Source: Atmel https://www.mouser.com/datasheet/2/268/doc0670-1180619.pdf

Let’s just assume we’re starting off with a blank slate. So we’re just going to start from an initial address of 0. Honestly, we might as well go ahead and address the second word address here while we’re at it. We can think of the first and second word address as one giant address: i.e. two bytes together (a.k.a. an int). Looking at where the MSB of this “giant” 16-bit would fit in the diagram above (where the * is), we see that this value is a “DON’T CARE BIT”. Since I’m using a 256K EEPROM, our MSB for our address begins where the cross is (so we actually have a 15-bit address). Therefore our 15-bit address comes out to: 0b000000000000000 (i.e. 15 0’s).

Getting back to the first word address, we can see that that the AT24C256 still expects a full 8-bit byte even if it doesn’t particularly care what that first bit actually is. We can accomplish this with the following code:

Wire.write(0b0000000); // 7 bits of 0s; this method takes a byte though so it will still transmit a byte's worth of 0s.
Second Word Address:
Second Word Address Message
Source: Atmel https://www.mouser.com/datasheet/2/268/doc0670-1180619.pdf

Now we just transmit the second word address (i.e. the remaining 8 bits of our 15 bits from above):

Wire.write(0b00000000);

Granted this was a pretty trivial address, but I think you get the idea.

3. Write the Data

Now, I assume you probably didn’t come here to just address the EEPROM. You want to actually write some data to it right? Well, now you can do that using the same Wire.write calls we’ve done before. We’re finally in this part of the Page Write diagram:

Data Message
Source: Atmel https://www.mouser.com/datasheet/2/268/doc0670-1180619.pdf

This part is rather simple compared to what we’ve done so far. To write data we simply use Wire.write again:

Wire.write(15);

The beauty of using Page Write is that we aren’t limited to just writing one byte at a time (which is what happens when you use Byte Write). With Page Write, once you’ve queued up the preamble, you can then queue up an entire array all at once. For example:

Wire.write("ABC");

4. Putting It All Together:

Putting it all together, the hard-coded Page Write function we just wrote could look something like this:

void writePage(){
  Wire.beginTransmission(0b1010000);
  Wire.write(0b0000000);
  Wire.write(0b00000000);
  byte a = Wire.write("Hi");
  Wire.endTransmission();
}

Don’t forget the Wire.endTransmission(); at the end so that we actually send out our data over the I2C bus. Also don’t forget to designate your Arduino as the master device in your setup block with Wire.begin();.

This tutorial has become a bit longer than I originally intended. Let’s call it a night and we’ll pick back up in the next post with how to request (read) data back off the EEPROM!

March 31

Interfacing an 8-bit Microcontroller with a 10-bit Device over the SPI Protocol

10-Bit ADC MCP3008 Interfacing with Arduino over SPI
10-Bit ADC MCP3008 Interfacing with Arduino over SPI

In Simon Monk’s Programming Arduino Next Steps: Going Further with Sketches, Simon introduces the SPI protocol by way of interfacing the Arduino with a 10-bit ADC (specifically the MCP3008). Unfortunately, by choosing a 10-bit ADC to introduce the SPI protocol, he ultimately fails the main objective of getting students started with the SPI protocol. Additionally, without much explanation or reference to the MCP3008’s datasheet, his implementation of the algorithm feels awkward and clumsy. Mostly because the code is just unintuitive.

In this tutorial, I offer an improved implementation of the SPI protocol (at least in terms of readability for the new learner) for interfacing a 10-bit ADC with an 8-bit microcontroller such as the Arduino. Additionally, I will attempt a more thorough explanation behind how the code works. This tutorial assumes an understanding of basic SPI protocol implementation (I’ll save that for a future tutorial).

Background Information:

First off, let’s tease out the big assumptions. What does a 10-bit ADC mean? Simply put, it means that the analog-to-digital converter takes an analog voltage and represents it as a 10-bit digital value. What exactly does that mean? It means that it represents the analog input as a value between 0 (at 0 V) and 1024 (at saturation- i.e. your max voltage). Where does the number 1023 come from? Simple- it’s the maximum value that can be represented in binary by 10 bits all set to 1: (2^9)+(2^8)+(2^7)+(2^6)+(2^5)+(2^4)+(2^3)+(2^2)+(2^1)+(2^0). Add them all up and you get 1023.

Now, why is that a problem with a microcontroller such as an Arduino? Arduino, like most microcontrollers, speak in 8-bit (i.e. byte)-sized words. If our master device (the Arduino) is speaking on over the SPI protocol in byte-sized words and the slave chip speaks back in 10-bit words, we have an offset problem. The Arduino is done talking after 8 bits but our 10-bit ADC still has two bits left to transmit.

Outlining a Rough Algorithm:

As always, the best way to learn is by example. Let’s start by taking a look at MCP3008’s datasheet. Specifically read Section 5.0- Serial Communication and 6.1- Using the MCP3004/3008 with Microcontroller (MCU) SPI Ports.

Yeah, yeah, I know you aren’t going to read them, but you really should, I promise you they’re worth it. Here are the highlights:

The first clock received with CS low and DIN high will
constitute a start bit. The SGL/DIFF bit follows the start
bit and will determine if the conversion will be done
using single-ended or differential input mode. The next
three bits (D0, D1 and D2) are used to select the input
channel configuration.

Microchip: https://cdn-shop.adafruit.com/datasheets/MCP3008.pdf

Terminology notes: CS = “Chip Select” (aka SS- “Slave Select” in SPI nomenclature); Din = MOSI (Master Output Slave Input) in SPI nomenclature.

From just this part of the datasheet, we can start thinking about how we want to implement our algorithm for getting data off the ADC over the SPI bus. We are told that the chip recognizes the start bit based off SS going low and the first high MOSI bit it receives. Therefore our code could look something like the following:

digitalWrite(chipSelectPin, LOW); // Select the ADC by setting SS low.
SPI.transfer(0b00000001); // Fire off the start bit.

Now that we’ve woken up the ADC, we need to tell it what we want. We know from our datasheet that the next bit the ADC expects to see is the mode selection bit- in our case, we want to pass 1 since we want single-mode. The remaining three bits the ADC expects to see are the analog input address on the ADC. In my example, I am using input 0 so the three-bit address is simply 000. Knowing this we can then simply add the following line to our code:

SPI.transfer(0b10000000);

But wait, it turns out that our ADC will start responding and giving us an output within this byte, so we actually need what the transfer method returns. Therefore we need something more like this:

byte readingH = SPI.transfer(0b10000000);

Now that we’ve told our ADC what we want, and we’ve even started to receive data from the ADC, we’ll need to continue letting the ADC continue to speak its 10-bit message. We can accomplish this by flushing the ADC with a byte of zeros:

byte readingL = SPI.transfer(0b00000000);

Now the question is, at what point in the transfer’s output does an individual bit start to represent something useful? (A professor might ask this same question as, “What bit in the byte output is our MSB (most significant bit)?” You can read the paragraphs in the datasheet but honestly it’s surefire path to driving yourself mad; speech is too imprecise. As usual, a picture is worth a thousand words:

SPI Communication with the MCP3008 using 8-bit Segments: https://cdn-shop.adafruit.com/datasheets/MCP3008.pdf

The part we’re really interested in are the rows labeled “MCU Transmitted Data” (highlighted in red) and “MCU Received Data” (highlighted in green). This datasheet demonstrates the cleanest way to communicate with the ADC over SPI by using three one-byte words.

As you can see, we’ve already covered sending the first 8-bit word when we transmitted our start word (SPI.transfer(0b10000000);) and we have now just transmitted our controlByte (byte readingH = SPI.transfer(0b10000000);).

If you look at the corresponding byte (the second one) in MCU Received Data row, you’ll see that the last two bits of that byte (readingH) are our MSBs: B9 and B8. We then flushed the ADC with our third byte with nothing but zeros to get the remaining 8 bits.

Now it’s time to reconstruct the output message from the ADC in our Arduino. This can be accomplished with a combination of masking and bit shifting (You see?! Bit masking and shifting aren’t just a useless academic exercise! They actually have a purpose!)

Starting with readingH we identified the MSB as being the last two bits. Therefore, we simply need to mask readingH as so with a bitwise & so we only get the last two bits:

readingH = (readingH & 0b00000011)

Since these two bits represent the two most significant bits of our 10-bit value, we need to shuffle them over to the appropriate bit position with << 8.

readingL is even easier since the entire byte is the LSB. Now all we need to do is add readingH and readingL together to get our output. Remember, the data type of our addition operation is an int (i.e. two bytes) because we now have a 10-bit output. The whole operation can be done in a single line:

int reading = ((readingH & 0b00000011) << 8) + (readingL);

Putting It All Together:

Putting the above together in a single program:

#include <SPI.h>
const int chipSelectPin = 10;

void setup() {
  Serial.begin(9600);
  SPI.begin();
  pinMode(chipSelectPin, OUTPUT);
  digitalWrite(chipSelectPin, HIGH);  //Immediately set CS (slave select) high so the ADC isn't selected on startup.
}

void loop() {
  int reading = readADC(0);
  Serial.println(reading);
  delay(1000);
}

int readADC(byte channel){
  byte startBit = 0b00000001;
  byte controlByte = 0b10000000 | (channel << 4); // First bit 1 gives us single-ended mode on ADC; the next three bits represent the ADC's analog input
  byte flushByte = 0b00000000; // Flush the ADC to get the remaining byte output.
  
  digitalWrite(chipSelectPin, LOW); //Initiate SPI protocol by dropping slave select low.
  SPI.transfer(startBit); // Fire off our start bit.
  byte readingH = SPI.transfer(controlByte);  // Push in our control byte which tells the ADC what mode to use and what channel we want.
  byte readingL = SPI.transfer(flushByte);  // Get the rest of our output from the ADC by flushing it with a byte of 0s.
  digitalWrite(chipSelectPin, HIGH);  // After flushing, immediately deselect the chip so that it doesn't continue.

  int reading = ((readingH & 0b00000011) << 8) + (readingL); // Per datasheet, we know that only the last two bits of our first transfer contain useful info. The second byte is all useful.

  return reading;
}

When you pull up the serial monitor and start moving the potentiometer around, you should see something like this:

Arduino serial monitor displaying ADC input in decimal format
Arduino serial monitor displaying ADC input in decimal format

I hope this helps clear up any confusion with using the SPI protocol and especially when using a 10-bit ADC with an 8-bit microcontroller.

March 2

How to Fix ESP32 Compiler Error in Arduino IDE: /heltec/esp32/tools/esptool/esptool.py No module named serial.tools.list_ports

So you’ve installed one of the various ESP32 board managers for the Arduino IDE. You go to compile your code and you get the following message:

Trackback arduino line 35, in <module> import serial.tools.list_ports as list_ports

/heltec/esp32/tools/esptool/esptool.py No module named serial.tools.list_ports

You’ve already installed pyserial so what the heck is going on? Well, fun fact, your Arduino ESP32 board manager uses Python 2.7 and you probably have Python 3.+ installed. The Arduino IDE is trying to use pyserial and it can’t find it because it’s using Python 2.7 and you installed pyserial for use with Python 3. Thankfully the fix is simple enough.

  1. First, we need to download pyserial. Go to https://pypi.org/project/pyserial/#files and download the latest .tar.gz file you see there.
  2. Unpack the .tar.gz file you just downloaded. This will give you your install folder.
  3. Open this folder in your terminal and run the following command so we install pyserial for Python 2.x:
sudo python2 setup.py install

4. That’s it! Close the Arduino IDE and reopen it and you should be good to go!