APDS with STM32

First, log in to your Simuli account and navigate to the Simuli Virtual Lab. Here we will create a new emulated instance of the STM32. Click on the Launch button under STM32 to get started.

This will open the configuration menu. First, provide a name for the project. Then, we will add the APDS 9301 Ambient Light Sensor by selecting it from the list of available components. Finally, review that the name for the project and the selected sensors are correct and click on the Launch button.

Once we have clicked on the Launch button, a new instance of the STM32 will be created. It can take a few minutes, so be patient. Once the new instance is ready, an Open button will be visible. Click on this button to open and start working with our STM32.

A new tab with the built-in development environment opens up. On the left side, we have the code editor window along with the terminal at the bottom. On the right side, we have our STM32 Nucleo board with a terminal for interacting with the board and the APDS 9301 ambient light sensor.

The STM32 instances come with support for Platform IO Core (CLI). We will be using it for working on our projects. To know more about Platform IO Core (CLI) and the different commands go to the Platform IO documentation.

First, we need to initialise a new project using Platform IO. Let's create a new folder called STM32-APDS which will hold all our code related to the project. You can right-click anywhere in the File Explorer pane, select New Folder, name the folder and click on OK. Now that our Folder is ready, let's go to the terminal and open the newly created folder. Use the following command:

cd /workspace/STM32-APDS/

Now we will initialise a new project using Platform IO. Type the following command in the terminal:

pio project init --board nucleo_f411re

The pio project init command is used to initialise the project. Then we need to specify the board which we want to use with the --board flag.

Once the project init command has run successfully, we will see some new folders inside the STM32-APDS folder. We will store the files related to the project in the src folder.

After running the project init command, if you can't see the new files, you may have to reload the project files by clicking on PROJECT in the File Explorer pane.

Create a new file called main.cpp inside the src folder. This file will hold the code for the blink program. Click on the main.cpp file to open it up in our code editor. Now, paste the following code inside the main.cpp file.

main.cpp
#include <Arduino.h>
#include <Wire.h>

class APDS9301 {
public:
  bool begin(int addr = 0x29) {
    Wire.begin();
    i2cAddr = addr;
    if (!setPower(true)) {
      return false;
    }
    if (!setTiming(true, 0)) {
      return false;
    }
    return true;
  }

  bool setPower(bool on) {
    byte regVal = (on ? REG_CONTROL_POWER_ON : 0);

    return setReg(REG_CONTROL, regVal);
  }

  bool setTiming(bool highGain, int integration) {
    byte regVal;

    if (highGain) {
      regVal = REG_TIMING_GAIN_16;
      gain = 1;
    }
    else {
      regVal = REG_TIMING_GAIN_1;
      gain = 1 / 16.0;
    }
    switch (integration) {
    case 0:
      regVal |= REG_TIMING_INTEGRATE_13_7MS;
      this->integration = TIMING_SCALE_13_7MS;
      break;
    case 1:
      regVal |= REG_TIMING_INTEGRATE_101MS;
      this->integration = TIMING_SCALE_101MS;
      break;
    default:
      regVal |= REG_TIMING_INTEGRATE_402MS;
      this->integration = TIMING_SCALE_402MS;
      break;
    }
    return setReg(REG_TIMING, regVal);
  }

  bool readLux(float *lux) {
    uint16_t ch0, ch1;
    if (!getReg16(REG_DATA0LOW, &ch0) || !getReg16(REG_DATA1LOW, &ch1)) {
      return false;
    }
    *lux = calcLux(ch0, ch1);
    return true;
  }

private:
  bool setReg(int reg, byte val) {
    Wire.beginTransmission(i2cAddr);
    int size = Wire.write(reg | REG_COMMAND_CMD);
    if (size == 1) {
      Wire.write(val);
    }
    if ((Wire.endTransmission() != 0) || (size != 1)) {
      return false;
    }
    return true;
  }

  bool getReg16(int reg, uint16_t *val) {
    Wire.beginTransmission(i2cAddr);
    int size = Wire.write(reg | REG_COMMAND_CMD | REG_COMMAND_WORD);
    if ((Wire.endTransmission() != 0) || (size != 1)) {
      return false;
    }
    if (Wire.requestFrom(i2cAddr, 2) != 2) {
      return false;
    }
    byte lsb = Wire.read();
    *val = Wire.read();
    *val = (*val << 8) | lsb;
    return true;
  }

  float calcLux(uint16_t ch0, uint16_t ch1) {
    float ch0f = ch0 / gain / integration;
    float ch1f = ch1 / gain / integration;

    if (ch0f == 0) {
      return 0;
    }
    float d = ch1f / ch0f;
    if (d <= 0.5) {
      return (0.0304 * ch0f - 0.062 * ch0f * pow(d, 1.4));
    }
    else if (d <= 0.61) {
      return (0.0224 * ch0f - 0.031 * ch1f);
    }
    else if (d <= 0.8) {
      return (0.0128 * ch0f - 0.0153 * ch1f);
    }
    else if (d <= 1.3) {
      return (0.00146 * ch0f - 0.00112 * ch1f);
    }
    else {
      return 0;
    }
  }

  static const byte REG_CONTROL = 0x00;
  static const byte REG_TIMING = 0x01;
  static const byte REG_THRESHLOWLOW = 0x02;
  static const byte REG_THRESHLOWHIGH = 0x03;
  static const byte REG_THRESHHIGHLOW = 0x04;
  static const byte REG_THRESHHIGHHIGH = 0x05;
  static const byte REG_INTERRUPT = 0x06;
  static const byte REG_CRC = 0x08;
  static const byte REG_ID = 0x0A;
  static const byte REG_DATA0LOW = 0x0C;
  static const byte REG_DATA0HIGH = 0x0D;
  static const byte REG_DATA1LOW = 0x0E;
  static const byte REG_DATA1HIGH = 0x0F;

  static const byte REG_COMMAND_CMD = 1 << 7;
  static const byte REG_COMMAND_CLEAR = 1 << 6;
  static const byte REG_COMMAND_WORD = 1 << 5;

  static const byte REG_CONTROL_POWER_ON = 0x03;

  static const byte REG_TIMING_GAIN_1 = 0 << 4;
  static const byte REG_TIMING_GAIN_16 = 1 << 4;
  static const byte REG_TIMING_START_CYCLE = 1 << 3;
  static const byte REG_TIMING_INTEGRATE_13_7MS = 0;
  static const byte REG_TIMING_INTEGRATE_101MS = 1;
  static const byte REG_TIMING_INTEGRATE_402MS = 2;

  static constexpr float TIMING_SCALE_13_7MS = 0.034;
  static constexpr float TIMING_SCALE_101MS = 0.252;
  static constexpr float TIMING_SCALE_402MS = 1;

  int i2cAddr;
  float gain;
  float integration;
};

APDS9301 apds9301;

void setup() {
  Serial.begin(115200);
  apds9301.begin();
}

void loop() {
  float lux;

  delay(1000);
  if (apds9301.readLux(&lux)) {
    Serial.println("Luminosity: " + String(lux) + " lux");
  }
  else {
    Serial.println("Cannot measure luminosity!");
  }
}

Save the main.cpp file. Now we need to compile this code. We will use Platform IO for the same. Go to your terminal and type the following command.

pio run

The first time this command is run, Platform IO will download and install the relevant framework and toolchains required for the STM32. This process will take a few seconds.

Now Platform IO will compile our code and output a firmware.bin file. We need to copy this firmware file from the .pio/build/nucleo_f411re to /workspace. Usually you would need to use the cp command, but we have created a custom command called load-firmware for ease of use. Just write the following command and the firmware.bin file will be copied to the workspace folder.

load-firmware

Once we have copied the firmware.bin file, all we need to do is to reset the STM32 by pressing the red button above the STM32. The new firmware will now be loaded and we can see the readings from the APDS-9301 sensor in our terminal.

You can also use the Arduino IDE to work with the STM32. Follow our guide on Using STM32 with Arduino IDE and IoTIFY Virtual Lab.

Now that you know how to get the readings from the APDS 9301 sensor, you could try to make an automatic LED, which would turn on when the Luminosity decreases below a certain threshold. You need an if statement to compare the current luminosity value with the threshold and then switch on the LED if the condition is satisfied. Keep building and have fun! 🎉

Last updated