Week 09 - Input Devices
This week we have the following tasks to complete:
- probe an input device's analog levels and digital signals
- measure something: add a sensor to a microcontroller board that you have designed and read it
Group Assignment
The weekly group assignment can be accessed here.
I measured the sensors I used in the group assignment. The first and most exciting sensor was the switch of the rotary encoder. The measurement setup looks like that:
The results are visible here:
The measurement setup looks like that: After that, I analyzed the rotary encoder itself, which has two data pins. I connected both to an oscilloscope, resulting in the following signal:
For the Adafruit BME688 breakout board, I used the I2C protocol. To inspect the communication, I connected it to a logic analyzer. Channel 1 is connected to the data line, and Channel 2 is connected to the clock signal.
Enhancing the Dev Board with a Rotary Encoder
I had already planned to integrate a rotary encoder into my dev board, as mentioned in last week's entry. To achieve this, I simply plugged in the rotary encoder module, after modifying the pin header in advance so that the shaft was perpendicular to the PCB.
To integrate the encoder into my software, I modified last week’s code by adding a brightness parameter for the NeoPixel, which can now be adjusted using the rotary encoder. Additionally, I implemented serial communication to send the brightness value to the PC.
This time, I experimented with the "Encoder" library, which I had previously used six years ago.
To handle button presses from the rotary encoder, I followed the guide here to implement interrupts on the Xiao ESP32-C3.
#include <Adafruit_NeoPixel.h> // library for NeoPixels
#include <Encoder.h> // library for rotary encoder
#define PIN_NEO_PIXEL D3 // Xiao pin that connects to NeoPixel
#define NUM_PIXELS 1 // number of LEDs
#define dt D8 // Xiao pin that connects to rotary encoder dt
#define clk D10 // Xiao pin that connects to rotary encoder clk
#define sw D9 // Xiao pin that connects to rotary encoder sw
Adafruit_NeoPixel NeoPixel(NUM_PIXELS, PIN_NEO_PIXEL, NEO_GRB + NEO_KHZ800); // set the NeoPixel initialization
Encoder brightness(dt,clk); // initiate object rotary encoder with the name brightness
int encoder_value = 0; // value of rotary encoder
int brightness_value = 255; // brightness value for the NeoPixel
int brightness_value_old = 255; // previous brightness value for the NeoPixel
bool sw_control = false;
void setup() {
Serial.begin(115200); // initial serial communication with a baud rate of 115200
NeoPixel.begin(); // initialize the NeoPixel strip object
pinMode(sw, INPUT); // set pin for rotary encoder button as an input
}
void IRAM_ATTR fallout()
{
if (sw_control == false){
brightness_value = brightness_value_old;
sw_control = true;
}
else if (sw_control == true){
brightness_value_old = brightness_value;
brightness_value = 0;
sw_control = false;
}
}
void loop() {
for (int i = 0; i < 256; i++) { // go through all possible values to control a Neopixel
byte r = (i < 85) ? i * 3 : (i < 170) ? 255 - (i - 85) * 3 : 0; // set first set of number ranges for a color cycle
byte g = (i < 85) ? 255 - i * 3 : (i < 170) ? 0 : (i - 170) * 3; // set second set of number ranges for a color cycle
byte b = (i < 85) ? 0 : (i < 170) ? (i - 85) * 3 : 255 - (i - 170) * 3; // set third set of number ranges for a color cycle
if (encoder_value != brightness.read()){
encoder_value = brightness.read();
brightness_value = encoder_value;
}
Serial.println(brightness_value);
NeoPixel.setPixelColor(0, r, g, b); // set color
NeoPixel.setBrightness(brightness_value);
NeoPixel.show(); // output the color
attachInterrupt(sw, fallout, HIGH);
delay(10); // short delay for the color change
}
}
The result looks like that:
Designing a Breadboard Expansion Board
The first daughter board is one with an breadboard. To have a universal test platform.
The first daughterboard I designed features a breadboard, providing a universal testing platform.
In my first version, I made two mistakes:
1. The text overlapped with the mounting holes.
2. I exported the trace and outline files at 96 DPI instead of 1000 DPI, which resulted in distorted toolpaths.
Initially, I assumed the preview issues were just artifacts from the vector arrows, but it turned out that the preview was completely accurate—my PNG was incorrectly exported.
Additionally, I encountered a Z-height issue while milling. Since I milled this board immediately after my dev board on the same stock material without repositioning it, I assumed the surface was level. However, during milling, I realized the board was slightly warped.
To compensate for this, I manually adjusted Z=0 to -0.08 mm, achieving a theoretical cutting depth of 0.2 mm.
The result was unsatisfactory, so I re-exported the PNG from Inkscape and used mods with the same settings—just with the corrected file.
Resolving Compatibility Issues
While soldering, I used my main dev board as a reference to ensure all connections were properly aligned. At this point, I noticed a major issue:
My milled breadboard board was not compatible with my dev board’s pin header layout.
- On the dev board, I had split the sensor pins across two pin sockets, with the power delivery socket in between.
- On the breadboard board, I had planned to group them on a single pin socket for easier access.
Additionally, the pin socket positions on the right side (which connect to the main board) were incorrect. I had repositioned the 3-pin and 4-pin sockets for easier routing, which led to misalignment.
To fix this, I corrected the footprint and milled a new PCB the next day.
Everything worked fine—until I noticed a missing trace on the left side.
Ferdi had swapped the 0.8 mm end mill for a 1 mm end mill, which I was unaware of, and it ripped away the trace.
To prevent this issue, I:
- Increased the clearance around the edges.
- Updated the end mill setting in mods to 1 mm.
- Milled the PCB one final time.
After soldering the pin sockets and attaching the breadboard with its pre-installed double-sided tape, the assembly was complete.
Load Cell
For my final project, I need a method to measure weight, so I researched strain gauges and load cells.
Texas Instruments provides an excellent introduction to load cells and data acquisition techniques. However, for my initial testing, I opted for a simpler, more straightforward approach by using an HX711 module.
The HX711 is essentially a breakout board for a 24-bit ADC (Analog-to-Digital Converter) manufactured by Avia—a company I hadn’t encountered before.
I also found a helpful tutorial on Random Nerd Tutorials, which provided additional insights into setting up and calibrating the HX711 with an ESP32.
To calibrate the load cell, I used the following modified version of the code from the tutorial. I adjusted the pin configuration to match my setup and removed unnecessary lines:
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-load-cell-hx711/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
// Calibrating the load cell
#include "HX711.h"
// HX711 circuit wiring
const int LOADCELL_DOUT_PIN = D0;
const int LOADCELL_SCK_PIN = D1;
HX711 scale;
void setup() {
Serial.begin(115200);
scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
}
void loop() {
if (scale.is_ready()) {
scale.set_scale();
Serial.println("Tare... remove any weights from the scale.");
delay(5000);
scale.tare();
Serial.println("Tare done...");
Serial.print("Place a known weight on the scale...");
delay(5000);
long reading = scale.get_units(10);
Serial.print("Result: ");
Serial.println(reading);
}
else {
Serial.println("HX711 not found.");
}
delay(1000);
}
//calibration factor will be the (reading)/(known weight)
To calibrate the load cell, I used a 434 g nectarine, which I first weighed with a kitchen scale. For the connection between the HX711 breakout board and the Xiao ESP32-C3, I utilized my previously assembled breadboard board.
To determine the calibration factor, I took five measurements with the nectarine, resulting in the following values:
Since the first value appeared inaccurate, I excluded it from the calculation. The final calibration factor was determined as follows:
[ \frac{(98170 + 98128 + 98159 + 98162)}{4 \times 434} = 226.16 ]
To verify the setup, I used an example code from bogde, the developer of the HX711 library that I had already used for the calibration test. I made minor modifications, adjusting the pin assignments and rounding the calibration factor to an integer:
/**
*
* HX711 library for Arduino - example file
* https://github.com/bogde/HX711
*
* MIT License
* (c) 2018 Bogdan Necula
*
**/
#include "HX711.h"
// HX711 circuit wiring
const int LOADCELL_DOUT_PIN = D0;
const int LOADCELL_SCK_PIN = D1;
HX711 scale;
void setup() {
Serial.begin(38400);
Serial.println("HX711 Demo");
Serial.println("Initializing the scale");
// Initialize library with data output pin, clock input pin and gain factor.
// Channel selection is made by passing the appropriate gain:
// - With a gain factor of 64 or 128, channel A is selected
// - With a gain factor of 32, channel B is selected
// By omitting the gain factor parameter, the library
// default "128" (Channel A) is used here.
scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
Serial.println("Before setting up the scale:");
Serial.print("read: \t\t");
Serial.println(scale.read()); // print a raw reading from the ADC
Serial.print("read average: \t\t");
Serial.println(scale.read_average(20)); // print the average of 20 readings from the ADC
Serial.print("get value: \t\t");
Serial.println(scale.get_value(5)); // print the average of 5 readings from the ADC minus the tare weight (not set yet)
Serial.print("get units: \t\t");
Serial.println(scale.get_units(5), 1); // print the average of 5 readings from the ADC minus tare weight (not set) divided
// by the SCALE parameter (not set yet)
scale.set_scale(226.f); // this value is obtained by calibrating the scale with known weights; see the README for details
scale.tare(); // reset the scale to 0
Serial.println("After setting up the scale:");
Serial.print("read: \t\t");
Serial.println(scale.read()); // print a raw reading from the ADC
Serial.print("read average: \t\t");
Serial.println(scale.read_average(20)); // print the average of 20 readings from the ADC
Serial.print("get value: \t\t");
Serial.println(scale.get_value(5)); // print the average of 5 readings from the ADC minus the tare weight, set with tare()
Serial.print("get units: \t\t");
Serial.println(scale.get_units(5), 1); // print the average of 5 readings from the ADC minus tare weight, divided
// by the SCALE parameter set with set_scale
Serial.println("Readings:");
}
void loop() {
Serial.print("one reading:\t");
Serial.print(scale.get_units(), 1);
Serial.print("\t| average:\t");
Serial.println(scale.get_units(10), 1);
scale.power_down(); // put the ADC in sleep mode
delay(5000);
scale.power_up();
}
The measurement of the same nectarine worked perfectly, but measurements of other objects were significantly inaccurate. This is likely due to a calibration issue, which could be resolved by using multiple reference objects during calibration.
Pressure, Temperature, and Humidity Sensors
For temperature measurement, the FabInventory includes the NHQ103B375T10 and the PTS120601B1K00P100.
For pressure sensing, we have the DPS310XTSA1.
Additionally, our lab has an Adafruit BME688 breakout board, which integrates a temperature, pressure, humidity, and VOC (Volatile Organic Compounds) sensor in a single module.
Humidity can also be measured using the step response method. Since I wanted to experiment with the BME688 and both thermistors, I designed a PCB that accommodates both thermistors and the Adafruit BME688 on a single board. The step response method can be tried out another time.
To get familiar with the board and how to used I look up some example code from Random Nerd Tutorials which I modified in a for me more suitable way by trying to reduce the amount of code and features needed. In the end the uploaded code looks like that:
/***************************************************************************
This is a library for the BME680 gas, humidity, temperature & pressure sensor
Designed specifically to work with the Adafruit BME680 Breakout
----> http://www.adafruit.com/products/3660
These sensors use I2C or SPI to communicate, 2 or 4 pins are required
to interface.
Adafruit invests time and resources providing this open source code,
please support Adafruit and open-source hardware by purchasing products
from Adafruit!
Written by Limor Fried & Kevin Townsend for Adafruit Industries.
BSD license, all text above must be included in any redistribution
***************************************************************************/
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"
#define SEALEVELPRESSURE_HPA (1013.25)
Adafruit_BME680 bme; // I2C
void setup() {
Serial.begin(9600);
while (!Serial);
Serial.println(F("BME680 async test"));
if (!bme.begin()) {
Serial.println(F("Could not find a valid BME680 sensor, check wiring!"));
while (1);
}
// Set up oversampling and filter initialization
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C for 150 ms
}
void loop() {
// Tell BME680 to begin measurement.
unsigned long endTime = bme.beginReading();
if (endTime == 0) {
Serial.println(F("Failed to begin reading :("));
return;
}
// Obtain measurement results from BME680. Note that this operation isn't
// instantaneous even if milli() >= endTime due to I2C/SPI latency.*/
if (!bme.endReading()) {
Serial.println(F("Failed to complete reading :("));
return;
}
Serial.print(F("Temperature = "));
Serial.print(bme.temperature);
Serial.println(F(" *C"));
Serial.print(F("Pressure = "));
Serial.print(bme.pressure / 100.0);
Serial.println(F(" hPa"));
Serial.print(F("Humidity = "));
Serial.print(bme.humidity);
Serial.println(F(" %"));
Serial.print(F("Gas = "));
Serial.print(bme.gas_resistance / 1000.0);
Serial.println(F(" KOhms"));
Serial.print(F("Approx. Altitude = "));
Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
Serial.println(F(" m"));
Serial.println();
delay(2000);
}
Which result in following behavior:
I couldn't test the NCT and PCT resistor because I was out of time. One thing I found interesting that the sensing of the itself ADC behave nonlinear
What I learn this week
- A load cell is much more complex than I initially thought. I was unaware that they are already configured in a Wheatstone bridge and that a load cell requires multiple strain gauges.
- Calibrating a load cell takes time and requires patience to achieve accurate measurements.
- I became familiar with the F() macro in pseudo C, which enables storing constant strings in flash memory instead of RAM.
What I want to improve next week
- Avoid wasting time on unnecessary tasks, such as the breadboard board this week.
- Speed up the completion of past assignments.
- Focus more intensively on my final project.
- I regret not having enough time to explore step response measurements in more detail—aside from some theoretical research. I still want to experiment with it, so I will make sure to do it in an upcoming week.
Design Files
dev board test rotary encoder
dev board project
bread board board schematic
bread board board traces
bread board board holes outline
ambient board schematic
ambient board traces
ambient board holes outline
To create this page, I used ChatGPT to check my syntax and grammar.
Copyright 2025 < Benedikt Feit > - Creative Commons Attribution Non Commercial
Source code hosted at gitlab.fabcloud.org