How to build custom IoT hardware with Arduino

How to build custom IoT hardware with Arduino

Come along on a journey to build an open source IoT device, from breadboarding to manufacturing.

How to build custom IoT hardware with Arduino
Image by : 

Thomas Hawk on Flickr. CC BY-NC 2.0. Modified by Opensource.com

x

Get the newsletter

Join the 85,000 open source advocates who receive our giveaway alerts and article roundups.

Recently I wanted to create an Arduino-based low-power Internet of Things (IoT) device for makers, with built-in sensors that could be used to deliver sensor data from any location to the cloud, and potentially control connected devices such as thermostats, lights, door locks, and other home automation products. Along the way, I learned that creating a new IoT device, from idea to prototype to final product, is not as simple as I thought it would be, and there was no "ready-to-go" development device to start with. However, by figuring out how to do it, I created a new product called Siguino, an open source IoT circuit board, that I hope will make it easier and faster for other people to create their own IoT products.

Siguino is based on a low-power version of the Arduino Pro Mini that has onboard sensors and antenna and runs off a single battery. It also leverages Sigfox, a low-power wide area network designed to connect IoT devices to the cloud.

This article describes the stages to get from a very messy breadboarded (but working) prototype to a final, custom-designed printed circuit board (PCB) that others will hopefully be able to use.

Prototype 1 compared to final prototype

1. Breadboarding

As all good maker projects begin, I started by breadboarding out a conceptual circuit. This involves identifying the functions you want your device to have and what components you will use. I wanted my device to:

  • Be low power and based on Arduino (e.g., an Arduino Pro Mini, which uses an ATmega328P chip)
  • Include a Sigfox chip so it can send messages over the Sigfox network. I chose the WiSOL SFMR10 chip for two reasons:
    • It is a send-only chip rather than a transceiver, and I had no plans for two-way communications
    • There is a DevKit available for it (very useful for breadboarding and prototyping)
  • Have four basic sensors:
    • Temperature (SparkFun DS18B20) for a connected thermostat
    • Light level (a standard photocell) for connected lights
    • Magnetic detection "hall effect" for door open/close, such as if a gate has been opened or left open (DigiKey AH9246-W-7
    • Movement detection for equipment security, fall detection, perimeter motion detection, and much more. I experimented with trip switches, mercury switches, and more, but decided an accelerometer (Adafruit LIS3DH) was the best choice for makers, as it opens up the board's native possibilities. (Note that the breakout board for this component is not low power, although the raw chip is.) 

The result was a rather messy (but functional!) set of components:

Once everything was working, I spent a little time putting together a neater version using breadboard jumper wires:

2. Writing Arduino code

The next step was writing the basic code to get my breadboarded device to do what I wanted it to do. Some of this is standard and included in the existing example code for each component. For example, the code to measure temperature using a DS18B20 looks like this:

#include <DallasTemperature.h>
#include <OneWire.h>

// Data wire is plugged into port 2 on the Arduino
#define ONE_WIRE_BUS 2
// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);
// Pass our oneWire reference to Dallas Temperature.
DallasTemperature temp_sensor(&oneWire);

void setup(){
  Serial.begin(9600);
  temp_sensor.begin();

  Serial.println("DS18B20 Temperature Test\n\n");

  delay(300);//Let system settle

}//end "setup()"

void loop(){

  Serial.print("Requesting temperatures...");
  temp_sensor.requestTemperatures(); // Send the command to get temperatures
 
  Serial.print("Temperature is: ");
  float temp_reading = temp_sensor.getTempCByIndex(0);
  Serial.println(temp_reading);

  delay(1000);
}// end loop()

There are a number of third-party libraries that provide options for low-power usage of an Arduino Pro Mini. I chose the Rocket Scream library, available on GitHub. Home Automation Community and Andreas Rohner provide good information about modifying an Arduino Pro Mini for low-power consumption. The sample usage for this project would be:

// **** INCLUDES *****

#include "LowPower.h"

void setup()

{

// No setup is required for this library

}

void loop()

{

// Enter power down state for 8 s with ADC and BOD module disabled

LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);

// Do something here

// Example: Read sensor, data logging, data transmission.

}

The WiSOL Sigfox chip can be communicated with using standard AT commands (and basic examples are included with the product datasheet). For this project, I needed only two functions:

  • Send message: I wrote a wrapper for the low-level AT commands to make it easier to send commands, e.g., to test the device, and messages:
String send_at_command(String command, int wait_time){
  altSerial.println(command);
  delay(wait_time);
  return recv_from_sigfox();
}

void test_sigfox_chip(){
  Serial.println("Sigfox Comms Test\n\n");
  altSerial.begin(9600);
  delay(300);//Let system settle
 
  Serial.println("Check awake with AT Command...");
  chip_response = send_at_command("AT", 50);  
  Serial.println("Got reponse from sigfox module: " + chip_response);
 
  Serial.println("Sending comms test...");
  chip_response = send_at_command("AT", 50);  
  Serial.println("Comms test reponse from sigfox module: " + chip_response);

  chip_response = send_at_command("AT$I=10", 50);  
  Serial.println("Dev ID reponse from sigfox module: " + chip_response);

  chip_response = send_at_command("AT$I=11", 50);  
  Serial.println("PAC Code reponse from sigfox module: " + chip_response);
}

//message send
chip_response = send_at_command("AT$SF=" + hex_bits, 10000);
Serial.println("Reponse from sigfox module: " + chip_response);
  • Enter low-power (sleep) mode: I opted for the basic sleep mode, although this chip also supports a "deep sleep" option. It just didn't seem worth it to move from ~1.5µA to <1µA, as a 1.5µA quiescent current drain was more than acceptable for my purpose. The sleep/wake cycle code looks like this:
//Sigfox sleep mode enabled via AT$P=1 command
// to wake need to set UART port low (see AX-SIGFOX-MODS-D.PDF for further details)
void set_sigfox_sleep(bool go_sleep){
  String chip_response;
  if (go_sleep){
    //send go sleep AT command
    chip_response = send_at_command("AT$P=1", 100);  
    Serial.println("Set sleep response: " + chip_response);
  }else{
    //wake up sigfox chip
    altSerial.end();
    pinMode(TX_PIN, OUTPUT);
    digitalWrite(TX_PIN, LOW);
    delay(100);
    altSerial.begin(9600);    
  }
}

Bit packing:

I decided to use bit packing for the Sigfox messages; since Sigfox messages are a maximum of 12 bytes, it's good to squash as much data as possible into each one. For example, assume the temperature returned by the temperature sensor will be a float between -40 and +80 degrees Celsius. A float in C++ uses 4 bytes of memory, but you don't want to use up 4 bytes of your 12-byte message sending a number if it isn't necessary. In general, you only need to know a temperature value to a half degree of accuracy, which allows you to compress the entire range of possible temperatures into 8 bits (1 byte) because, by limiting the range of -40 to +80 to half-degree increments, you have only 240 possible values, as follows:

0b00000000 [0] = -40
0b00000001 [1] = -39.5
0b00000010 [2] = -39

0b11101111 [239] = 79.5
0b11110000 [240] = 80

To save even more space, I limited my range to -10 to +50 C with half-degree accuracy, which required 7 bits for temperature, plus 5 bits for light level (from 0 to 1,000), 1 bit for open/close or device move, and 4 bits for a message sequence number so I can spot any missed messages. So, my basic sensors only needed to use 18 bits of my 12 bytes of available message space, packed like this:

I adapted a set of bit packing functions that would take all the sensor data, as well as the number of bits I wanted to use for each, and pack them into a single 12-byte value:

#ifndef BITPACKER_H_INCLUDED
#define BITPACKER_H_INCLUDED

#include <stdint.h>

#define BIT(n)                  ( 1UL<<(n) ) //UL = unsigned long, forces chip to use 32bit int not 16

#define BIT_SET(y, mask)        ( y |=  (mask) )
#define BIT_CLEAR(y, mask)      ( y &= ~(mask) )
#define BIT_FLIP(y, mask)       ( y ^=  (mask) )

/*
        Set bits        Clear bits      Flip bits
y        0x0011          0x0011          0x0011
mask     0x0101 |        0x0101 &~       0x0101 ^
        ---------       ----------      ---------
result   0x0111          0x0010          0x0110
*/

//! Create a bitmask of length \a len.
#define BIT_MASK(len)           ( BIT(len)-1 )

//! Create a bitfield mask of length \a starting at bit \a start.
#define BF_MASK(start, len)     ( BIT_MASK(len)<<(start) )

//! Prepare a bitmask for insertion or combining.
#define BF_PREP(x, start, len)  ( ((x)&BIT_MASK(len)) << (start) )

//! Extract a bitfield of length \a len starting at bit \a start from \a y.
#define BF_GET(y, start, len)   ( ((y)>>(start)) & BIT_MASK(len) )

//! Insert a new bitfield value \a x into \a y.
#define BF_SET(y, x, start, len)    \
    ( y= ((y) &~ BF_MASK(start, len)) | BF_PREP(x, start, len) )

namespace BitPacker {
    static uint32_t get_packed_message_32(unsigned int values[], unsigned int bits_used[], int num_vals){
        uint32_t retval = 0x0;
        int j = 0;
        for (int i=0;i<num_vals;i++){
            BF_SET(retval, values[i], j, j + bits_used[i]);
            j += bits_used[i];
        }
        return retval;
    }

    static uint64_t get_packed_message_64(unsigned int values[], unsigned int bits_used[], int num_vals){
        uint64_t retval = 0x0;
        int j = 0;
        for (int i=0;i<num_vals;i++){
            BF_SET(retval, values[i], j, j + bits_used[i]);
            j += bits_used[i];
        }
        return retval;
    }

}
#endif // BITPACKER_H_INCLUDED

3. Prototyping circuits

Before custom designing a PCB circuit for your device, it's worthwhile to nail down a smaller, neater prototype circuit. I opted for a stripboard version of this circuit. The end result should be a much neater and tighter version of the circuit, which is very useful in helping trim the final PCB design. (This matters because, as a rule of thumb, the larger the PCB, the higher its cost.) It also gives a good idea about what sort of housing may be required for the final product.

I also used Fritzing, a great piece of software for laying out stripboard or Veroboard circuits. It allows you to design a virtual circuit that you can duplicate on your stripboard. My prototype circuit looked like this in Fritzing:

Which led to this actual (working) circuit:

4. Designing and printing the PCB

To design my PCB, I used Autodesk Eagle, an excellent piece of software that's free to use for small boards (<80cm) and has many component libraries (including good third-party libraries, e.g., all of the SparkFun and AdaFruit components).

I learned everything I needed to know about Eagle from these SparkFun tutorials:

Some tips I would suggest from my experience:

  • Save often!
  • Always do design rule checking (and rechecking) after every change, no matter how small. Recheck after ground pour, even if the change should not have affected the ground pour. (Copper ground pours are created by filling open, unused areas with copper, generally on the outer layers of the board, then connecting the copper fill with stitching vias to ground. Poured ground is useful on two-layer boards that lack solid reference planes; it reduces crosstalk due to capacitive coupling.)
  • When routing with very small components (e.g., FPGA surface mount components), try not to have any holes underneath the component to avoid issues when you are hand soldering or surface mounting components for prototype testing in the absence of professional tooling (e.g., solder reflow ovens, pick & place machinery, etc.). It's very hard to make sure hand-applied solder or solder paste doesn't sit under the component and flow into a routing hole underneath (where you cannot see). It's also easy to forget when routing just how small some of these components are.

In other words, don't do this:

Instead do this:

  • With larger components, try not to have routing holes too near the component legs or pads for the same reason as above.

My final, fully routed board layout looked like this:

5. Soldering surface mounted components

A big unknown for me at the start of this project was how to build prototypes that included surface mounted components (SMCs). It's much easier to use plated through hole (PTH) components for prototyping (e.g., breadboarding), but you wouldn't choose PTH components for a final product, as SMCs are smaller and neater.

What happens when you design your PCB layout with your ideal SMC components, you get it printed, and you want to put it all together and test it, but you don't have any surface mount machinery (like a pick & place machine or a solder reflow oven)? You could build your own reflow oven, but if you're building your own circuit, I think that kind of departure from the focus is a bit time-consuming. And, it is mostly unnecessary, as you can hand-solder almost all SMCs with enough practice, and you can use a relatively inexpensive solder air gun to make the job easier.

I used the EEVBlog YouTube channel to learn the basics of SMC soldering, and in the end I was hand soldering everything down to 0402 components (so small you will lose them if you breathe too heavily!). For context, see this component size comparison chart:

I wouldn't recommend using 0402 components in your circuit. (I had no choice, as they were part of a radio-frequency net under the antenna, and larger components could have affected the antenna performance.) In fact, 0602 components are also very small and tricky to solder, but with a bit of practice it's all very doable. I would recommend ordering an extra couple of PCBs with the first batch, purely for soldering practice, as you will very likely make a mess of your first attempt.

The tools required include:

  • Soldering iron: It is definitely worth paying a bit more for a good quality soldering iron. I started with a cheap one, and a couple of weeks later I ditched it for a much better one, and everything was much easier.
  • Hot air soldering gun: I also bought a hot air gun; while it has proved trickier to use than I'd hoped (getting the air pressure right so you don't blow small components off the board is an art form!), it has made soldering some of the smaller (VFLGA) package integrated circuits, like the LIS3DH, much easier. (I'm not even sure how I would have done this with a soldering iron alone, though apparently it is possible.) It also makes it easy to remove components when you mess something up.
  • Tweezers: A good quality, very fine tip set of tweezers is essential for picking up very small components.
  • Eye loupe / magnifying glass: For zooming in on soldering to check for bad solder, solder bridges, blobs, missed pins, etc., I found a jeweler's loupe, preferably with a built-in light, very useful.

6. Measuring power consumption

Power consumption measurement was a surprisingly difficult, but very important, part of the process. I wanted my device to be ultra-low power so it could work for a year on a small battery (i.e., a 900mAh CR2). This meant making sure that the quiescent current (the constant current draw) was as small as possible, down into the low µA range, while accounting for the occasional higher current draw during message sending. Although there are a number of methods to assess a circuit's current requirements, most have poor resolution for the very low end. Manual mechanisms, such as an ammeter connected across the power supply lines, were cumbersome to use and also only gave snapshots of how much current was used at a specific time (and in some cases did not react fast enough for any reliable measurement).

Of the various options I tried, the only one that worked in the end was the Power Profiler Kit (PPK) from Nordic Semiconductor. It wasn't too expensive (around $100 for both it and the baseboard) and it worked really well. (My one gripe is that I couldn't get it to work reliably on Linux, even though it's a Python program, so I had to boot into Windows to use it.)

The PPK produces both a constant view of the power consumption down to a very low resolution (<1µA) and also a running average for a time window (exactly what I needed for my battery life calculations):

7. Programming the ATmega bootloader

A raw ATmega chip that you might have soldered on to your PCB may not come hardcoded with the correct fuse settings (see below) or with a bootloader programmed, so you may need to configure it for correct operation of your board. This is surprisingly confusing for the first-time PCB designer/builder!

There are three main tasks to be addressed when setting up a raw ATmega chip as received from a chip supplier. (Note: The details refer to the ATmega328P, but much of it also applies to other parts in the ATmega range):

Fuse settings:

Fuses are non-volatile bits defining a number of programmable aspects to the way the chip behaves. There are three fuse bytes, each with 8 bits: low byte, high byte, and extended byte. For example, these control what type of clock drives the chip or at what voltage the brown-out-detector (BOD) triggers. The BOD halts code execution at the set voltage to avoid unreliable operation when power is too low.

Default values are set in factory-supplied chips. These may suit the intended use of the chip. But if not, they need to be changed. This is done via the SPI bus using a suitable interface, e.g., an Ardiuno Uno board. There are some good guides for this process here and here.

Bootloader:

The code needed to run the project application needs to be loaded into the chip. Usually an FTDI header device is used to connect the chip via USB to the programming PC. In this case, the chip needs a bootloader program to be installed to facilitate this operation. This, in effect, is a program to load a program, but it is loaded via the SPI bus using a suitable interface.

For this project, I used a separate Arduino UNO to bootload my ATmega chips as follows:

  1. For the bootloader, use the ATmega chip programmer by Nick Gammon
  2. Download the ZIP file
  3. Extract ATmega_Board_Programmer folder (e.g., to Arduino IDE Libraries directory)
  4. Open ATmega_Board_Programmer sketch
  5. Connect the standard Arduino Uno to your PC
  6. Set board to "Arduino/Genuino Uno" and set correct port
  7. Upload ATmega_board_programmer sketch
  8. Disconnect Uno from the PC and connect it the target chip as follows:
Uno Target
D10 Reset
D11 MOSI
D12 MISO
D13 SCK
Gnd Gnd
+5V Vcc
  1. Reconnect the Uno to the PC -> Set port -> Run Serial Monitor 115200 baud
  2. The bootloader should run immediately and show results in the serial monitor window; follow the instructions in the serial window (e.g., "L" for Load Bootloader)
  3. Note that the bootloader will set the chip to use an internal 8MHz clock; this can be modified if you have an external crystal (see notes in sketch)

Program code loading:

Once the chip has a bootloader installed, program code can be loaded via an FTDI interface. The Arduino IDE running on the developer PC can load application code directly to the chip via this interface.

8. Printing the PCB, purchasing components, manufacturing, and assembly

To move from breadboarding to bulk manufacturing, you will need a variety of resources:

  • Hardware components: To breadboard your circuit, you will need components such as various resistors, capacitors, sensors, integrated circuits, etc. You can find some of them on mainstream sites like Amazon, but I recommend some of the hardware-specific sites as a better option. I mainly used DigiKey; Mouser and Farnell are also good.
  • PCB printing: Once you have designed your PCB and created the Gerber files that specify how it's to be printed, you will need find a company to print it. SparkFun has several suggestions under "Picking a PCB Manufacturer" that might be worth a look. I used Multi-CB and found them very good, timely, and competitively priced, although I had to pay via bank transfer as they don't offer an online payment option.
  • PCB manufacturing: Once your PCB is fully designed, your components are purchased and hand-soldered, and your last prototype is tested, now it's time to get it manufactured in bulk. I received a very reasonable quote from PCBCart, which included assembly and ATmega chip programming. Since I haven't yet manufactured the boards, I can't comment on their quality or service.

9. Doing backend development

So you've built your device, and it sends messages on the Sigfox network (essentially to the Sigfox servers)… now what!? How are you going to process those messages and what are you going to do with them?

Sigfox callback

The first thing to do is to have the Sigfox server forward any messages received by your device to a web server or services that you control. There are many options with the Sigfox system on how to do this, but I think the easiest is to build your own RESTful web services (described below), and have the Sigfox servers make a HTTP(S) request to your new services with the message data. This can be done within the Sigfox backend by using a callback mechanism for your device, where you can specify the posted variables or URL parameters, as needed from a list of available variables including the raw message data:

RESTFul web services

RESTful web services are the modern API and ubiquitous on the web. There are many ways to create them, but I decided to use the Go programming language, first, because it was a language I wanted to learn, and second, because it's an easy to deploy via Docker. The basic structure of a web service (saving to a MongoDB database) in Go looks like this:

// Handler for HTTP Post - "/sensordata"
// Register new sensor data
func NewSensorData(w http.ResponseWriter, r *http.Request) {
    var dataResource SensorDataResource
    // Decode the incoming Task json
    err := json.NewDecoder(r.Body).Decode(&dataResource)
    if err != nil {
        common.DisplayAppError(
            w,
            err,
            "Invalid Sensor Data format",
            500,
        )
        return
    }
    sensorData := &dataResource.Data
    context := NewContext()
    defer context.Close()
    c := context.DbCollection("SensorData")
    repo := &db.SensorDataRepository{c}
    // Insert a sensor data document
    repo.Create(sensorData)
    if j, err := json.Marshal(SensorDataResource{Data: *sensorData}); err != nil {
        common.DisplayAppError(
            w,
            err,
            "An unexpected error has occurred",
            500,
        )
        return
    } else {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        w.Write(j)
    }
}

Most of the simple web services you might build for basic processing of raw data from the Sigfox servers would be of a similar structure.

Something that I found particularly useful for Sigfox message parsing was bit unpacking (since I used bit packing earlier with my Arduino code to squash as much data as possible into my Sigfox messages). The corresponding Go code for unpacking the data looks like this:

func bit(n uint64) uint64 {
    return 1<<n
}

func bit_set(y uint64, mask uint64) uint64 {
    return y | mask
}

func bit_clear(y uint64, mask uint64) uint64 {
    return y & ^mask
}

func bit_flip(y uint64, mask uint64) uint64 {
    return y ^ mask
}

func bit_mask(len uint64) uint64 {
    return bit(len) - 1
}

func Bf_mask(start uint64, len uint64) uint64 {
    return bit_mask(len) << start
}

func Bf_prep(x uint64, start uint64, len uint64) uint64 {
    return (x & bit_mask(len)) << start
}

func Bf_get(y uint64, start uint64, len uint64) uint64 {
    return (y>>start) & bit_mask(len)
}

func Bf_set(y uint64, x uint64, start uint64, len uint64) uint64 {
    return (y & ^Bf_mask(start, len)) | Bf_prep(x, start, len)
}

IFTTT integration

Finally, in terms of making your device accomplish something beyond data logging, probably the easiest way to integrate it with other devices or ecosystems is through If This Then That (IFTTT), which is an amalgamation of many different APIs and systems. Once you connect your device to IFTTT, you can access the existing follow-on actions. For example, "If [your device sends X] then [send email to Y], or [make Alexa say Y], or [turn on Philips Hue lights in Y room]," or any myriad of other options.

Moving forward

My next steps for the Siguino project are developing a 3D housing for it, going through the Sigfox device certification program, tweaking the antenna to get the most out of it, and funding and organizing the first production run of the device.

Since my main purpose for this project was to learn about the technology, I've made all the software code and hardware open source on GitHub. If you have questions or find this information of value, please let me know in the comments.

About the author

Scott Tattersall - Business owner, Entrepeneur, technologist I've spent the last 15 years working with the latest technologies and in both my own start-up companies, SMEs and large publicly traded companies. My previous software company for which I was both co-founder and C.T.O. was acquired in 2015 and since then I've been working with the latest technologies such as advanced Machine Learning (e.g. Google's Tensorflow), Computer vision (e.g. OpenCV), IoT (e.g. Arduino, RPi) and Crypto-currencies (e.g. BitCoin,...