Skip to content

Latest commit

 

History

History

photocell

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

How to use a Photocell with RIOT

Measure the intensity of ambient light with an STM32 Nucleo-64 F401RE development board and the RIOT operating system.

A video tutorial is available on YouTube.

For this mapplication we will use

  • STM32 Nucleo-64 F401RE
  • Breadboard
  • 1KOhm resistor
  • Photocell (or Photoresistor, or Light-Dependent-Resistor LDR)
  • 3 Male to male jumper wires

STM32 Nucleo-64 F401RE development board

The STM32 Nucleo-64 F401RE is a low-cost development board that utilizes a 32-bit ARM Cortex-M4 processor to power various combinations of performance and power consumption. The CPU frequency can go as high as 84 MHz while the power consumption can go as low as 2.4uA at standby without RTC. The STM32 Nucleo board supports the ARDUINO® Uno V3 connectivity headers and the ST morpho headers allowing the easy expansion of the functionality with a wide choice of specialized shields.

The RIOT operating system

The RIOT is an open-source microkernel-based operating system designed for a very low memory and energy footprint suitable for embedded devices that depend on real-time capabilities. RIOT provides out-of-the-box support for a very wide low-power wireless and communication stacks, making it an ideal choice to build Internet of Things (IoT) platforms.

Hardware setup of the Photocell with the STM32 Nucleo-64 F401RE board

The photocell is a light-controlled variable resistor. When the photocell is struck by light it drastically decreases its resistance until it reaches 500Ohm. In the absence of ambient light, the resistance of the photocell will become so high as 50KOhm which practically is non-conductive.

In order to monitor the varying resistance of the photocell we will use the STM32 Nucleo board's analog input and the 5V supply. We wish use the analog input to measure the analog voltage, that range from 5V to 0V, depending on the internal resistance of the photocell. To do this, we will combine the photocell with a fixed 1KOhm resistor to create a potentiometer-like behavior. When the light is very bright, then the resistance of the photocell is very low compared with the fixed value resistor (also known as pulldown resistor), and so it is as if the potentiometer was turned to maximum. When the photocell is in dull light, the resistance becomes greater than the fixed 1KOhm resistor and it is as if the potentiometer was being turned towards GND.

Position the photocell and the 1KOhm resistor in series using three vertical columns of the breadboard. Use a jumper wire to connect the top connector of the photocell with the 5V pin of the STM32 Nucleo board. Connect the bottom connector of the photocell - the one that is connected with the 1KOhm resistor - with the A0 pin of the STM32 Nucleo board. Finally connect the bottom connector of the 1KOhm resistor with the GND pin of the STM32 Nucleo board.

The wiring of the components is shown in the figure below.

Wiring of hardware components

Note on Fixed Resistance
In the above circuit we use a fixed 1KOhm resistor in combination with the 5V power supply. Note that we can also use the 3.3V supply. We can also use a different fixed resistor. The selection of the fixed resistance and the power supply depends on the intensity of the ambient light that we wish to measure. For example if we deploy our sensor in an ambient that usually is brightly illuminated, the 1KOhm resistor will quickly saturate. This means that as the internal resistance of the photocell reaches lower values, the difference in the voltage between the photocell and the fixed resistor (voltage measured by the A0 pin) will reach very fast the maximum voltage of 5V (or 3.3V). Therefore, if we wish to be able to differentiate the intensity of ambient light it might be more convenient to increase the fixed resistor to 10KOhm or even higher values. Similarly, if we wish to be able to differentiate between a dark and a very dark ambient, a smaller resistance (e.g., 500Ohm or 300Ohm) will work better. The AdaFruit tutorial on Photocells provides a more detailed discussion on how this circuit works, and also gives insights on how to use the "Axel Benz" formula to determine the resistor value that is more suitable for our application.

Setting up the ADC in the RIOT operating system

RIOT provides a very simple ADC interface to allow platform independent access to an MCU's ADC unit. We need to specify in the Makefile that we wish to use the ADC peripheral of the board.

FEATURES_REQUIRED += periph_adc

In the main.c we also need to include the following header files:

#include "periph/adc.h"
#include "periph/gpio.h"

The ADC driver interface is built around the concept of ADC lines. An ADC line in this context is a tuple consisting out of a hardware ADC device (an ADC functional unit on the MCU) and an ADC channel connected to pin.

For the STM32 Nucleo F401RE board the A0 pin is mapped into the 0 line. Moreover, the ADC resolution is 12Bit.

Therefore in the main.c we define two constants for the ADC line and the resolution that we will use to sample:

#define ADC_IN_USE  ADC_LINE(0)
#define ADC_RES     ADC_RES_12BIT

The ADC is initialized using the adc_init() function as follows:

/* initialize the ADC line */
if (adc_init(ADC_IN_USE) < 0) {
    printf("Initialization of ADC_LINE(%u) failed\n", ADC_IN_USE);
    return 1;

} else {
    printf("Successfully initialized ADC_LINE(%u)\n", ADC_IN_USE);
}

Sampling the ADC

The ADC is sampled using the adc_sample() function as follows:

int sample = 0;

sample = adc_sample(ADC_IN_USE, RES);

The sample value ranges from 0 ... 4095. We can convert the analog value into lux by using then Analog data conversion utilities provided by RIOT. To do so, we need to specify in the Makefile that we wish to use the analog_util module as follows:

USEMODULE += analog_util

Moreover, within the main.c file we need to include the analog_util.h header as follows:

#include "analog_util.h"

Now we can map the channel resolution to the lux range 10..100. This is done using the utility function adc_util_map() as follows:

int lux = 0;

lux = adc_util_map(sample, ADC_RES, 10, 100);

Periodic sampling of the analog sensor

In a normal computing environment, executing a piece of code periodically can be implemented in many different ways. However for IoT devices, we need to make sure that time-based functions are implemented in an energy efficient way, always making sure that threads are not keeping the CPU busy or block it from entering a low power consumption state. It is therefore recommended to avoid using POSIX functions, and use the native RIOT functions.

RIOT provides a high-level API to multiplex the available timers. In order to use this API we need to specify in the Makefile that we wish to use the xtimer module as follows:

USEMODULE += xtimer

Moreover, within the main.c file we need to include the xtimer.h header as follows:

#include "xtimer.h"

We would like to sample the photocell every 100ms. For this reason we declare a constant DELAY as follows:

#define DELAY  (100LU * US_PER_MS) /* 100 ms */

We can access the current system time as 32bit time stamp value as follows:

xtimer_ticks32_t last = xtimer_now();

The xtimer_periodic_wakeup() function can be used to suspend the calling thread until the absolute time (last_wakeup + period).

xtimer_periodic_wakeup(&last, DELAY);

Running the code

To build and use the applications you need to make sure that you have a local copy of the RIOT main code. For detailed instructions on how to clone and build the RIOT OS follow the instructions in the RIOT repository and the READMEs within the respective application directory.

From the command line you can compile the code, upload it to the MCU and then open the serial port to monitor the debug output as follows:

make BOARD=nucleo-f401re flash term

At this point, given that RIOT along with the STM32 tool-chain are properly installed in your system, you should start getting debug messages like the following:

RIOT Photocell application
Measure the intensity of ambient light using a photocell
connected to the Analog input of the STM32 Nucleo-64 board.
The sensor is sampled through the ADC lines once every 100ms
with a 12-bit resolution and print the sampled results to STDIO
Successfully initialized ADC_LINE(0)
ADC_LINE(0): raw value: 3048, lux: 76
ADC_LINE(0): raw value: 3051, lux: 77

Technical details on using timer in the RIOT operating system

When we wish to suspend the execution of a thread based on the xtimer module using the xtimer_tsleep() function, the following actions will take place:

  1. A new MUTEX is created,
  2. An alarm is configured to fire after the desired period of time to UNLOCK the MUTEX,
  3. LOCK the MUTEX,
  4. Remove the active thread from the thread queue,
  5. Block it by setting the thread's status to STATUS_MUTEX_BLOCKED,
  6. When the hardware interrupt is raised, the call back function of the alarm will be invoked,
  7. The call back function will UNLOCK the MUTEX and set the user's thread's status to STATUS_PENDING.

The code for the above logic can be found in the xtimer implementation:

xtimer_t timer;
mutex_t mutex = MUTEX_INIT;

timer.callback = _callback_unlock_mutex;
timer.arg = (void*) &mutex;

mutex_lock(&mutex);
_xtimer_set64(&timer, offset, long_offset);
mutex_lock(&mutex);

The xtimer_set64() will insert the timer into a singly linked list, thus incurring an O(n) complexity with (n) being the number of active timers. The same complexity is required for the removal of the timer when the alarm fires.

Notice that if the xtimer_tsleep() function is used for very short periods, e.g., less than XTIMER_BACKOFF - which is typically set to 50us, then the above mechanism will not be used. Instead the thread will be blocked using the xtimer_spin() function as follows:

uint32_t start = _xtimer_lltimer_now();
while ((_xtimer_lltimer_now() - start) < offset);

If we wish to use long periods of time in microseconds, 32bit can hold 71 minutes. When using milliseconds, 32bit can hold 49days. On the other hand a 64bit unsigned value can hold >584years in nanoseconds, thus clearly simplifying our code.

Technical details on the configuration of an MCU in the RIOT operating system

RIOT supports a very broad range of low-power IoT devices and microcontroller architectures (32-bit, 16-bit, 8-bit). The ADC interface is intentionally designed as simple as possible, to allow for very easy implementation and maximal portability.

The ADC is configured using the adc_conf_t structure. Since each different MCU may support different ADC capabilities, for the family of STM32 boards the definition can be found in the file periph_cpu.h.

Given the above structure, the capabilities and configuration of each platform supported by RIOT is located within the boards folder of the RIOT source tree.

For the STM32 Nucleo F401RE board in particular, the mapping of the internal pins of the MCU with the ADC lines of the RIOT operating system are defined within the boards/nucleo-f401re folder under the periph_conf.h file.

static const adc_conf_t adc_config[] = {
    {GPIO_PIN(PORT_A, 0), 0, 0},
    {GPIO_PIN(PORT_A, 1), 0, 1},
    {GPIO_PIN(PORT_A, 4), 0, 4},
    {GPIO_PIN(PORT_B, 0), 0, 8},
    {GPIO_PIN(PORT_C, 1), 0, 11},
    {GPIO_PIN(PORT_C, 0), 0, 10},
};

#define ADC_NUMOF           ARRAY_SIZE(adc_config)

The above configuration defines only the ADC channels available through the ARDUINO® Uno V3 connectivity header CN8, pins A0..A5.

The mapping of the ARDUINO® Uno V3 connectivity headers with the MCU's internal pins as well as the resolution of the ADC can be found in the user manual UM1724 of the STM32 Nucleo F401RE board. In particular Figure 18, page 32 identifies that pin A0 is connected to the MCU's GPIO pin PA0.

Looking into the specifications of the STM32F401RE MCU that is used by the Nucleo-64 F401RE development board we identify that the PA0 port of the MCU is using channel 1.