Lab 8: A Weather Station
In this assignment you will implement a “weather station” using temperature/humidity and pressure sensors on the Nucleo daughter card. Your weather station will consist of firmware executing on the Nucleo card and a node-red interface. There are several objectives for this assignment
- Learn how to use typical sensor devices
- Learn how to adapt off-the-shelf device drivers
- Expand your experience with multi-threaded embedded code.
Overview of Lab
In this assignment you will be using the hts221 temperature/humidity and lps22hh pressure sensors. Both of these devices are interfaced to the host processor through an i2c communication bus and are part of the expansion board we are using, the X-NUCLEO-IKS01A3, which is a “daughter card” for the nucleo board:
This board includes a variety of sensors including:
- LSM6DSO: MEMS 3D accelerometer (±2/±4/±8/±16 g) + 3D gyroscope (±125/±250/±500/±1000/±2000 dps)
- LIS2MDL: MEMS 3D magnetometer (±50 gauss)
- LIS2DW12: MEMS 3D accelerometer (±2/±4/±8/±16 g)
- LPS22HH: MEMS pressure sensor, 260-1260 hPa absolute digital output barometer
- HTS221: capacitive digital relative humidity and temperature
- STTS751: Temperature sensor (–40 °C to +125 °C)
In the next lab you will learn how to use LIS2DW12 accelerometer and LIS2MDL magnetometer as a tilt corrected compass and for activity detection.
An unfortunate aspect of using such sensors is that they are complicated to program; the underlying programming model is a collection of registers and the documentation for these registers is often challenging to understand – especially for sensors with higher-level functionality such as activity detection. The data sheets for the two components we are using are here:
- lps22hh an application note describing how to use the part (from a software perspective) is here
- hts221 An application note describing how to interpret the temperature and humidity readings is here
For this lab you will not have to program the sensors at such a low-level – as a start, I have provided example programs using each of the two sensors as will as configuration and Makefile to assist in building working code.
The skeleton directory for this lab has the following form (the subdirectory contents have been elided):
.
├── cfg
│ └── ... elided ...
├── config.mk
├── hts221-example
│ └── ... elided ...
├── inc
│ ├── react.h
│ └── sensor.h
├── lps22hh-example
│ └── ... elided ...
├── make.mk
└── src
│ ├── messsage.c
│ └── sensor.c
The assignment consists of several parts:
- Build and test the example programs.
- Build a program that combines the functionality of the two examples.
- Expand your program to use separate threads for measurement and communication.
- Integrate your program as an MQTT node through node-red.
- Build a dashboard.
Parts 3-5 build upon your experience from Lab 7, although you will have to expand what you learned in that lab.
At the end of this assignment, you should have a working “weather station” that communicates with an MQTT server; and a dashboard to display the data produced.
Deliverables
As before, your report should document your efforts and describe any code that you created. The report does not need to be verbose, but should be a coherent snapshot of your work and any difficulties/issues that arose.
The grading rubric for this lab is
- Completion of tasks 60%
- Demonstrate example programs running: 10pts
- Combined functionality of examples: 10pts
- Expanded program with MQTT interface: 20pts
- Working program + MQTT communication + Dashboard: 20pts
- Lab report 40%
- Overview: 10 pts
- Description of your code + screen shots: 25 pts
- Issues: 5pts
Deliverables
The specific deliverables are
- Report.md
- combined/ directory with code etc. for combined examples
- weather/ directory with code etc. for expanded program
- node-red/ directory with flows + dashboard
1. Hardware + i2c Code
As mentioned above, both of the sensors used in this assignment are part of a separate daughter card and both are interfaced through an i2c bus as illustrated in the following figure as documented:
The actual bus used is one of three provided by the STM32L476 – by default i2c device 1. The most important configuration detail for accessing these sensor devices is there i2c address:
Sensor | I2C Address |
---|---|
LPS22hh | 0xBA |
HTS221 | 0xBE |
In both example programs, the STM32L476 device must be configured before use; unfortunately, this configuration depends upon several “magic numbers” that
have to be derived, which is out of the scope of this lab. The code fragments relevant to this initialization (in sensor.c
) are:
// I2C configuration parameters
static const I2CConfig i2ccfg = {
STM32_TIMINGR_PRESC(15U) |
STM32_TIMINGR_SCLDEL(4U) |
STM32_TIMINGR_SDADEL(2U) |
STM32_TIMINGR_SCLH(15U) |
STM32_TIMINGR_SCLL(21U),
0, 0};
...
void sensor_init(void) {
// configure I/O pins, i2c device
palSetLineMode(LINE_ARD_D15, PAL_MODE_ALTERNATE(4) |
PAL_STM32_OSPEED_HIGH |
PAL_STM32_OTYPE_OPENDRAIN);
palSetLineMode(LINE_ARD_D14, PAL_MODE_ALTERNATE(4) |
PAL_STM32_OSPEED_HIGH |
PAL_STM32_OTYPE_OPENDRAIN);
i2cStart(&I2CD1, &i2ccfg); // start the i2c device
...
There are three pieces to this fragment – definition of a structure with i2c initialization parameters, configuration of the processor pins to the correct mode, and starting the i2c device with the initialization parameters. This code utilizes the ChibiOS i2c driver.
The two example programs illustrate the use of two different types of drivers. The HTS221 example uses a driver provided as part of ChibiOS which is significantly simpler to use while the LPS22HH example uses a low-level driver distributed by ST Microelectronics. I will begin by describing the HTS221 example.
In addition to configuring the I2C driver, the HTS221 driver requires various configuration parameters that control how the sensor is configured:
--- in sensor.c ---
static const HTS221Config hts221cfg = {
&I2CD1,
&i2ccfg,
NULL,
NULL,
NULL,
NULL,
HTS221_ODR_7HZ,
#if HTS221_USE_ADVANCED || defined(__DOXYGEN__)
HTS221_BDU_CONTINUOUS,
HTS221_AVGH_256,
HTS221_AVGT_256
#endif
};
--- in main.c ---
void main(void) {
...
/* HTS221 Object Initialization.*/
hts221ObjectInit(&HTS221D1);
hts221Start(&HTS221D1, &hts221cfg);
}
In this example, the configuration supports a 7HZ sample rate, but is otherwise relatively generic. The complete documentation for this driver is
here.
In this example, the device is being used in “one-shot” mode – data are produced only upon demand. As illustrated:
while (true) {
hts221HygrometerReadCooked(&HTS221D1, &hygrocooked);
msg_sendf("Hum: %4.2f\n", hygrocooked);
hts221ThermometerReadCooked(&HTS221D1, &thermocooked);
msg_sendf("Temp: %4.2f\n", thermocooked);
chThdSleepMilliseconds(200);
cls(); // clear the screen
}
Thus the example reads the two devices and prints the results to the serial port. After a delay, cls
sends control characters to “clear the screen” if the output is being displayed on a terminal (e.g. through the goshell you
have used in previous labs).
The LPS22HH example does not have the benefit of a nicely integrated driver. Rather, for this example I imported a generic C driver provided by ST – the full set of ST drivers is here.
As is common for such generic drivers, it is necessary to provide a thin “hardware abstraction layer” in order to interface with the underlying communication device driver (the ChibiOS I2C driver).
Sadly, the generic code provided by ST is often unnecessarily verbose and error prone. For example, the HTS221 driver they provide does not correctly perform temperature/humidity calibration.
The example implements this hardware abstraction layer as follows:
--- in sensor.c --
typedef struct {
I2CDriver *driver;
sysinterval_t timeout;
uint8_t address;
} i2c_sensor_t;
--- in main.c --
#define LPS22HH_TIMEOUT OSAL_MS2I(50)
#define LPS22HH_ADDRESS (0xBA>>1)
static i2c_sensor_t lps22hh_handle = { &I2CD1, LPS22HH_TIMEOUT, LPS22HH_ADDRESS };
--- in sensor.c ---
// write a block of data to i2c
int32_t i2c_sensor_write(void *handle, uint8_t reg, uint8_t *bufp, uint16_t len{
... code to interface to ChibiOS i2c driver ...
}
// read a block of data from i2c
int32_t i2c_sensor_read(void *handle, uint8_t reg, uint8_t *bufp, uint16_t len){
... code to interface to ChibiOS i2c driver ...
}
--- in main.c ---
stmdev_ctx_t dev_ctx = {
.write_reg = i2c_sensor_write,
.read_reg = i2c_sensor_read,
.handle = &lps22hh_handle
};
The key requirement of this abstraction layer is to populate a “device
context” (stmdev_ctx_t
) with pointers to the abstraction layer read/write
functions. The driver data required for these functions is passed
as an opaque pointer (void *
). The definition of this opaque pointer is
part of the user interface – in this case I designed a structure that
includes the ChibiOS I2C driver pointer and the i2c address of the sensor.
The ST driver is much lower level than the ChibiOS hts221 driver – it essentially operates at the level of the sensor registers and provides no higher level abstractions (such as the calibration function provided by the hts221 driver). The key initialization steps provided in the example are:
// reset the device
lps22hh_reset_set(&dev_ctx, PROPERTY_ENABLE);
do {
lps22hh_reset_get(&dev_ctx, &rst);
} while (rst);
// Enable Block Data Update
lps22hh_block_data_update_set(&dev_ctx, PROPERTY_ENABLE);
// Set Output Data Rate
lps22hh_data_rate_set(&dev_ctx, LPS22HH_10_Hz_LOW_NOISE);
Once the device has been initialized, reading data follows the following pattern:
- Read the status register
Conditionally read the pressure and temperature readings
while (true) { lps22hh_reg_t reg; lps22hh_read_reg(&dev_ctx, LPS22HH_STATUS, (uint8_t *)®, 1); if (reg.status.p_da) { // pressure data available ? lps22hh_pressure_raw_get(&dev_ctx, (uint8_t *) &raw_pressure); pressure_hPa = lps22hh_from_lsb_to_hpa(raw_pressure); msg_sendf("pressure [hPa]:%6.2f\n", pressure_hPa); } if (reg.status.t_da) { // temperature data available ? lps22hh_temperature_raw_get(&dev_ctx, (uint8_t *) &raw_temperature); temperature_degC = lps22hh_from_lsb_to_celsius(raw_temperature); msg_sendf("temperature [degC]:%6.2f\n", temperature_degC ); } chThdSleepMilliseconds(500); cls(chp); // restore cursor }
1. Building the example programs
Each of the two examples is built and downloaded in an identical manner.
They share a Makefile, and each has a unique file project.mk
that
provides information about the files required for each project. For
the hts221, the project.mk file contains:
# List of all the project related files.
PROJECTSRC = ./src/main.c ../src/sensor.c ../src/message.c
# Required include directories
PROJECTINC = ./inc ../inc
# Shared variables
ALLCSRC += $(PROJECTSRC)
ALLINC += $(PROJECTINC)
while the lps22hh project.mk file differs in one line:
PROJECTSRC = ./src/main.c ../src/lps22hh_reg.c ../src/sensor.c ../src/message.c
This structure will make it easy to create new projects based upon the example code.
From within the project directory (e.g. lps22hh-example) execute the following shell commands:
- to build:
make
- to download:
make download
- to clean:
make clean
Once you have built and downloaded the code, execute the goshell
as described in Lab7. You should take a screenshot of the output for your report.
When executing the lps22hh example, notice that if you raise the board of the table even a few feet, the pressure changes. The pressure sensor is extremely sensitive.
2. Combined Functionality
- Create a directory that includes all of the files from
lps22hh-example
. - Add the necessary code from
hts221-example/src/main.c
to initialize and read the hts221 sensor.
The output of running and building this should be the combined output of the two example programs.
3. Weather Node
Create a new directory by cloning the code from part 2. You should add two threads – one to read the hts221 sensor and the other to read the lps22hh sensor. These threads should (continuously)
- Read the sensor and write mqtt message(s)
- Sleep 1 second
The I2C driver is configured to provide mutual exclusion so you shouldn’t have a problem with interference.
The main thread should accept input to turn on/off the led as in Lab 7.
Choose an appropriate message format; e.g.
- Node/status/ls22hh/[temperature|pressure]
- Node/status/hts221/[temperature|humidity]
Test your node with goshell.
Once you are satisfied it is working, build a node-red flow to interface your node with flespi.
You can save some time by copying your node-red directory from Lab 7 and modifying it as needed
4. Control Panel
The final step for this assignment is to build a control panel that displays weather data – temperature and humidity from the hts221 and pressure from the lps22hh. Your control panel should include, for each measurement:
- A gauge showing the current value.
- A graph showing measurement data over time.