Internal Temperature Sensor on BL602

📝 14 Oct 2021

This may surprise most folks… The BL602 and BL604 RISC-V SoCs have an Internal Temperature Sensor!

The Internal Temperature Sensor is not documented in the BL602 / BL604 Datasheet. But it’s buried deep inside the BL602 / BL604 Reference Manual.

(Under “Analog-to-Digital Converter”)

Today we shall…

  1. Read the Internal Temperature Sensor on BL602 and BL604

  2. Transmit the temperature over LoRaWAN to The Things Network (with CBOR Encoding)

  3. Chart the temperature with Grafana (the open-source visualisation tool)

Internal Temperature Sensor visualised with Grafana

The firmware has been tested on PineDio Stack BL604 (pic below). But it should work on any BL602 or BL604 Board: PineCone BL602, Pinenut, DT-BL10, MagicHome BL602, …

PineDio Stack BL604 RISC-V Board (foreground) talking to The Things Network via RAKWireless RAK7248 LoRaWAN Gateway (background)

1 Where’s the Internal Temperature Sensor?

The Internal Temperature Sensor is inside the Analog-to-Digital Converter (ADC) on BL602 and BL604…

Internal Temperature Sensor in ADC

(From BL602 / BL604 Reference Manual)

The Internal Temperature Sensor behaves like an Analog Input. Which we call the ADC to measure.

(More about BL602 ADC)

The steps for reading the Internal Temperature Sensor seem complicated…

Reading the Internal Temperature Sensor

(From BL602 / BL604 Reference Manual)

But thankfully there’s an (undocumented) function in the BL602 IoT SDK that reads the Internal Temperature Sensor!

Let’s call the function now.

(Internal Temperature Sensors based on ADC are available on many microcontrollers, like STM32 Blue Pill)

Reading the Internal Temperature Sensor the Quick Way

2 The Quick Way

To read the Internal Temperature Sensor the Quick Way, we call bl_tsen_adc_get from the ADC Hardware Abstraction Layer (HAL): pinedio_tsen/demo.c

#include <bl_adc.h>  //  For BL602 Internal Temperature Sensor

/// Read BL602 / BL604's Internal Temperature Sensor as Integer
void read_tsen(char *buf, int len, int argc, char **argv) {
  //  Temperature in Celsius
  int16_t temp = 0;

  //  Read the Internal Temperature Sensor as Integer
  int rc = bl_tsen_adc_get(
    &temp,  //  Temperature in Celsius
    1       //  0 to disable logging, 1 to enable logging
  );
  assert(rc == 0);

  //  Show the temperature
  printf("Returned Temperature = %d Celsius\r\n", temp);
}

Let’s build, flash and run the pinedio_tsen demo firmware…

At the BL602 / BL604 Command Prompt, enter this command…

read_tsen

The first result will look odd…

temperature = -90.932541 Celsius
Returned Temperature = -90 Celsius

Running read_tsen again will produce the right result…

temperature = 43.467045 Celsius
Returned Temperature = 43 Celsius

2.1 Quick But Inaccurate

We discover two issues with the Quick Way of reading the Internal Temperature Sensor…

  1. First Result is way too low

    temperature = -90.932541 Celsius
    Returned Temperature = -90 Celsius

    (Workaround: Discard the first result returned by bl_tsen_adc_get)

  2. According to the internal log, the temperature is a Floating-Point Number

    temperature = 43.467045 Celsius
    Returned Temperature = 43 Celsius

    But the returned value is a Truncated Integer!

    (Sorry, no workaround for this)

Yep our Quick Way is also the Inaccurate Way!

Let’s fix both issues.

Reading the Internal Temperature Sensor the Quick Way

3 The Accurate Way

To read the Internal Temperature Sensor the Accurate Way, we copy the bl_tsen_adc_get function and change two things

  1. Wait a while as we initialise the ADC for the first time

    (100 milliseconds)

  2. Return the temperature as Float

    (Instead of Integer)

Below is get_tsen_adc, our modded function (with all the fixings): pinedio_tsen/demo.c

#include <bl_adc.h>     //  For BL602 ADC HAL
#include <bl602_adc.h>  //  For BL602 ADC Standard Driver
#include <bl602_glb.h>  //  For BL602 Global Register Standard Driver
#include <FreeRTOS.h>   //  For FreeRTOS
#include <task.h>       //  For vTaskDelay

/// Read the Internal Temperature Sensor as Float. Returns 0 if successful.
/// Based on bl_tsen_adc_get in https://github.com/lupyuen/bl_iot_sdk/blob/master/components/hal_drv/bl602_hal/bl_adc.c#L224-L282
static int get_tsen_adc(
  float *temp,      //  Pointer to float to store the temperature
  uint8_t log_flag  //  0 to disable logging, 1 to enable logging
) {
  assert(temp != NULL);
  static uint16_t tsen_offset = 0xFFFF;
  float val = 0.0;

  //  If the offset has not been fetched...
  if (0xFFFF == tsen_offset) {
    //  Define the ADC configuration
    tsen_offset = 0;
    ADC_CFG_Type adcCfg = {
      .v18Sel=ADC_V18_SEL_1P82V,                /*!< ADC 1.8V select */
      .v11Sel=ADC_V11_SEL_1P1V,                 /*!< ADC 1.1V select */
      .clkDiv=ADC_CLK_DIV_32,                   /*!< Clock divider */
      .gain1=ADC_PGA_GAIN_1,                    /*!< PGA gain 1 */
      .gain2=ADC_PGA_GAIN_1,                    /*!< PGA gain 2 */
      .chopMode=ADC_CHOP_MOD_AZ_PGA_ON,         /*!< ADC chop mode select */
      .biasSel=ADC_BIAS_SEL_MAIN_BANDGAP,       /*!< ADC current form main bandgap or aon bandgap */
      .vcm=ADC_PGA_VCM_1V,                      /*!< ADC VCM value */
      .vref=ADC_VREF_2V,                        /*!< ADC voltage reference */
      .inputMode=ADC_INPUT_SINGLE_END,          /*!< ADC input signal type */
      .resWidth=ADC_DATA_WIDTH_16_WITH_256_AVERAGE,  /*!< ADC resolution and oversample rate */
      .offsetCalibEn=0,                         /*!< Offset calibration enable */
      .offsetCalibVal=0,                        /*!< Offset calibration value */
    };
    ADC_FIFO_Cfg_Type adcFifoCfg = {
      .fifoThreshold = ADC_FIFO_THRESHOLD_1,
      .dmaEn = DISABLE,
    };

    //  Enable and reset the ADC
    GLB_Set_ADC_CLK(ENABLE,GLB_ADC_CLK_96M, 7);
    ADC_Disable();
    ADC_Enable();
    ADC_Reset();

    //  Configure the ADC and Internal Temperature Sensor
    ADC_Init(&adcCfg);
    ADC_Channel_Config(ADC_CHAN_TSEN_P, ADC_CHAN_GND, 0);
    ADC_Tsen_Init(ADC_TSEN_MOD_INTERNAL_DIODE);
    ADC_FIFO_Cfg(&adcFifoCfg);

    //  Fetch the offset
    BL_Err_Type rc = ADC_Trim_TSEN(&tsen_offset);
    assert(rc != ERROR);  //  Read efuse data failed

    //  Must wait 100 milliseconds or returned temperature will be negative
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
  //  Read the temperature based on the offset
  val = TSEN_Get_Temp(tsen_offset);
  if (log_flag) {
    printf("offset = %d\r\n", tsen_offset);
    printf("temperature = %f Celsius\r\n", val);
  }
  //  Return the temperature
  *temp = val;
  return 0;
}

Note that get_tsen_adc now returns the temperature as Float (instead of Integer)…

static int get_tsen_adc(
  float *temp,      //  Pointer to float to store the temperature
  uint8_t log_flag  //  0 to disable logging, 1 to enable logging
);

And we added a 100-millisecond delay when initialising the ADC for the first time…

//  If the offset has not been fetched...
if (0xFFFF == tsen_offset) {
  ...
  //  Must wait 100 milliseconds or 
  //  returned temperature will be negative
  vTaskDelay(100 / portTICK_PERIOD_MS);

Let’s call get_tsen_adc now.

Reading the Internal Temperature Sensor the Accurate Way

3.1 Read Temperature as Float

We’re ready to read the Internal Temperature Sensor the Accurate Way!

The code below looks similar to the earlier code except…

  1. We now call our modded function get_tsen_adc

    (Instead of the BL602 ADC HAL)

  2. Which returns a Float

    (Instead of Integer)

From pinedio_tsen/demo.c

/// Read BL602 / BL604's Internal Temperature Sensor as Float
void read_tsen2(char *buf, int len, int argc, char **argv) {
  //  Temperature in Celsius
  float temp = 0;

  //  Read the Internal Temperature Sensor as Float
  int rc = get_tsen_adc(
    &temp,  //  Temperature in Celsius
    1       //  0 to disable logging, 1 to enable logging
  );
  assert(rc == 0);

  //  Show the temperature
  printf("Returned Temperature = %f Celsius\r\n", temp);
}

Let’s build, flash and run the pinedio_tsen demo firmware…

At the BL602 / BL604 Command Prompt, enter this command a few times…

read_tsen2

The results look consistent…

offset = 2175
temperature = 44.369923 Celsius
Returned Temperature = 44.369923 Celsius

offset = 2175
temperature = 43.596027 Celsius
Returned Temperature = 43.596027 Celsius

offset = 2175
temperature = 43.596027 Celsius
Returned Temperature = 43.596027 Celsius

(No more Sub-Zero Temperatures!)

And the temperature is returned as Float.

(No more Integers!)

Reading the Internal Temperature Sensor the Accurate Way

4 LoRaWAN and The Things Network

Since we have an Onboard Temperature Sensor (though it runs a little hot), let’s turn BL602 and BL604 into an IoT Sensor Device for LoRaWAN and The Things Network!

We’ll create this LoRaWAN Command for BL602 and BL604…

las_app_tx_tsen 2 0 4000 10 60

Which means…

(More about CBOR)

(More about The Things Network)

Transmit internal temperature to LoRaWAN

4.1 LoRaWAN Command

las_app_tx_tsen is defined like so: pinedio_lorawan/lorawan.c

/// Transmit Internal Temperature Sensor Data to LoRaWAN, encoded with CBOR. The command
///   las_app_tx_tsen 2 0 2345 10 60
/// Will transmit the CBOR payload
///   { "t": 1234, "l": 2345 }
/// To port 2, unconfirmed (0), for 10 times, with a 60 second interval.
/// Assuming that the Internal Temperature Sensor returns 12.34 degrees Celsius.
void las_cmd_app_tx_tsen(char *buf0, int len0, int argc, char **argv) {
  //  Get port number
  uint8_t port = parse_ull_bounds(argv[1], 1, 255, &rc);

  //  Get unconfirmed / confirmed packet type
  uint8_t pkt_type = parse_ull_bounds(argv[2], 0, 1, &rc);

  //  Get l value
  uint16_t l = parse_ull_bounds(argv[3], 0, 65535, &rc);

  //  Get count
  uint16_t count = parse_ull_bounds(argv[4], 0, 65535, &rc);

  //  Get interval
  uint16_t interval = parse_ull_bounds(argv[5], 0, 65535, &rc);

We begin by fetching the command-line arguments.

For each message that we shall transmit…

  //  Repeat count times
  for (int i = 0; i < count; i++) {

    //  Wait for interval seconds
    if (i > 0) { vTaskDelay(interval * 1000 / portTICK_PERIOD_MS); }

    //  Read Internal Temperature Sensor as a Float
    float temp = 0;
    int rc = get_tsen_adc(
      &temp,  //  Temperature in Celsius
      1       //  0 to disable logging, 1 to enable logging
    );
    assert(rc == 0);

We read the Internal Temperature Sensor as a Float.

Next we scale up the temperature 100 times and truncate as Integer…

    //  Scale the temperature up 100 times and truncate as integer:
    //  12.34 ºC becomes 1234
    int16_t t = temp * 100;

(Because encoding the temperature as 1234 requires fewer bytes than 12.34)

We encode the temperature (and light level) with CBOR and transmit as a LoRaWAN message

    //  Omitted: Encode into CBOR for { "t": ????, "l": ???? }
    uint8_t output[50];
    ...

    //  Allocate a pbuf
    struct pbuf *om = lora_pkt_alloc(output_len);

    //  Copy the encoded CBOR into the pbuf
    rc = pbuf_copyinto(om, 0, output, output_len);

    //  Send the pbuf
    rc = lora_app_port_send(port, mcps_type, om);

Which goes all the way to The Things Network! Assuming that we have configured our LoRaWAN settings for The Things Network.

(CBOR Encoding is explained here)

(Sending a LoRaWAN Packet is explained here)

4.2 Run the LoRaWAN Firmware

Let’s build, flash and run the updated LoRaWAN Firmware: pinedio_lorawan

At the BL602 / BL604 Command Prompt, enter this command…

las_app_tx_tsen 2 0 4000 10 60

Which means…

We should see the Internal Temperature transmitted over LoRaWAN every 60 seconds…

temperature = 44.885849 Celsius
Encode CBOR: { t: 4488, l: 4000 }
CBOR Output: 11 bytes
  0xa2 0x61 0x74 0x19 0x11 0x88 0x61 0x6c 0x19 0x0f 0xa0
  ...
temperature = 47.207531 Celsius
Encode CBOR: { t: 4720, l: 4000 }
CBOR Output: 11 bytes
  0xa2 0x61 0x74 0x19 0x12 0x70 0x61 0x6c 0x19 0x0f 0xa0
  ...

(See the complete log)

Let’s check the transmitted Sensor Data with Grafana and Roblox.

Visualising The Things Network Sensor Data with Grafana

(Source)

5 Grafana and Roblox

In an earlier article we have configured Grafana (the open source visualisation tool) to read Sensor Data from The Things Network. And chart the Sensor Data in real time…

Follow the instructions below to install and configure Grafana

Start the Grafana service and run the las_app_tx_tsen command from the previous chapter.

We should see this chart in Grafana after 10 minutes…

PineDio Stack BL604 Internal Temperature rendered with Grafana

(Note that the temperatures have been scaled up 100 times)

5.1 The Fun Way

There’s another way to see the Sensor Data (in a fun way): Roblox

(Yep the multiplayer 3D world!)

Follow the instructions below to install and configure Roblox

We should see the temperature rendered by Roblox as a glowing thing…

PineDio Stack BL604 Internal Temperature rendered with Roblox

And the output log shows our temperature, scaled by 100 times.

(Like 4875 for 48.75 ºC)

(Sounds rather warm, even for Sunny Singapore. Is it correct? 🤔 Lemme know what’s your BL602 / BL604 temperature!)

Storing The Things Network Sensor Data with Prometheus

(Source)

6 What’s Next

Today we have turned BL602 and BL604 into a basic IoT Sensor Device that transmits its Internal Temperature to LoRaWAN and The Things Network.

In the next article we shall build a better IoT Monitoring System that stores the Sensor Data with Prometheus and visualises the data in a Grafana Dashboard

Many Thanks to my GitHub Sponsors for supporting my work! This article wouldn’t have been possible without your support.

Got a question, comment or suggestion? Create an Issue or submit a Pull Request here…

lupyuen.github.io/src/tsen.md

7 Notes

  1. This article is the expanded version of this Twitter Thread

8 Appendix: Build and Run Internal Temperature Sensor Firmware

Here are the steps to build, flash and run the Internal Temperature Sensor Firmware for BL602 and BL604

8.1 Build Internal Temperature Sensor Firmware

Download the firmware…

# Download the master branch of lupyuen's bl_iot_sdk
git clone --recursive --branch master https://github.com/lupyuen/bl_iot_sdk

Build the Firmware Binary File pinedio_tsen.bin

# TODO: Change this to the full path of bl_iot_sdk
export BL60X_SDK_PATH=$HOME/bl_iot_sdk
export CONFIG_CHIP_NAME=BL602

cd bl_iot_sdk/customer_app/pinedio_tsen
make

# TODO: Change ~/blflash to the full path of blflash
cp build_out/pinedio_tsen.bin ~/blflash

More details on building bl_iot_sdk

8.2 Flash Internal Temperature Sensor Firmware

Follow these steps to install blflash

  1. “Install rustup”

  2. “Download and build blflash”

We assume that our Firmware Binary File pinedio_tsen.bin has been copied to the blflash folder.

Set BL602 / BL604 to Flashing Mode and restart the board…

For PineDio Stack BL604:

  1. Set the GPIO 8 Jumper to High (Like this)

  2. Press the Reset Button

For PineCone BL602:

  1. Set the PineCone Jumper (IO 8) to the H Position (Like this)

  2. Press the Reset Button

For BL10:

  1. Connect BL10 to the USB port

  2. Press and hold the D8 Button (GPIO 8)

  3. Press and release the EN Button (Reset)

  4. Release the D8 Button

For Pinenut and MagicHome BL602:

  1. Disconnect the board from the USB Port

  2. Connect GPIO 8 to 3.3V

  3. Reconnect the board to the USB port

Enter these commands to flash pinedio_tsen.bin to BL602 / BL604 over UART…

# TODO: Change ~/blflash to the full path of blflash
cd ~/blflash

# For Linux:
sudo cargo run flash pinedio_tsen.bin \
    --port /dev/ttyUSB0

# For macOS:
cargo run flash pinedio_tsen.bin \
    --port /dev/tty.usbserial-1420 \
    --initial-baud-rate 230400 \
    --baud-rate 230400

# For Windows: Change COM5 to the BL602 / BL604 Serial Port
cargo run flash pinedio_tsen.bin --port COM5

(For WSL: Do this under plain old Windows CMD, not WSL, because blflash needs to access the COM port)

More details on flashing firmware

8.3 Run Internal Temperature Sensor Firmware

Set BL602 / BL604 to Normal Mode (Non-Flashing) and restart the board…

For PineDio Stack BL604:

  1. Set the GPIO 8 Jumper to Low (Like this)

  2. Press the Reset Button

For PineCone BL602:

  1. Set the PineCone Jumper (IO 8) to the L Position (Like this)

  2. Press the Reset Button

For BL10:

  1. Press and release the EN Button (Reset)

For Pinenut and MagicHome BL602:

  1. Disconnect the board from the USB Port

  2. Connect GPIO 8 to GND

  3. Reconnect the board to the USB port

After restarting, connect to BL602 / BL604’s UART Port at 2 Mbps like so…

For Linux:

sudo screen /dev/ttyUSB0 2000000

For macOS: Use CoolTerm (See this)

For Windows: Use putty (See this)

Alternatively: Use the Web Serial Terminal (See this)

More details on connecting to BL602 / BL604

9 Appendix: Build and Run LoRaWAN Firmware

Here are the steps to build, flash and run the LoRaWAN Firmware for PineDio Stack BL604

9.1 Build LoRaWAN Firmware

Download the LoRaWAN firmware and driver source code

# Download the master branch of lupyuen's bl_iot_sdk
git clone --recursive --branch master https://github.com/lupyuen/bl_iot_sdk

In the customer_app/pinedio_lorawan folder, edit Makefile and find this setting…

CFLAGS += -DCONFIG_LORA_NODE_REGION=1

Change “1” to your LoRa Region…

ValueRegion
0No region
1AS band on 923MHz
2Australian band on 915MHz
3Chinese band on 470MHz
4Chinese band on 779MHz
5European band on 433MHz
6European band on 868MHz
7South Korean band on 920MHz
8India band on 865MHz
9North American band on 915MHz
10North American band on 915MHz with a maximum of 16 channels

The GPIO Pin Numbers for LoRa SX1262 are defined in…

components/3rdparty/lora-sx1262/include/sx126x-board.h

They have been configured for PineDio Stack. (So no changes needed)

Build the Firmware Binary File pinedio_lorawan.bin

# TODO: Change this to the full path of bl_iot_sdk
export BL60X_SDK_PATH=$HOME/bl_iot_sdk
export CONFIG_CHIP_NAME=BL602

cd bl_iot_sdk/customer_app/pinedio_lorawan
make

# TODO: Change ~/blflash to the full path of blflash
cp build_out/pinedio_lorawan.bin ~/blflash

More details on building bl_iot_sdk

9.2 Flash LoRaWAN Firmware

Follow these steps to install blflash

  1. “Install rustup”

  2. “Download and build blflash”

We assume that our Firmware Binary File pinedio_lorawan.bin has been copied to the blflash folder.

Set BL602 / BL604 to Flashing Mode and restart the board…

For PineDio Stack BL604:

  1. Set the GPIO 8 Jumper to High (Like this)

  2. Press the Reset Button

For PineCone BL602:

  1. Set the PineCone Jumper (IO 8) to the H Position (Like this)

  2. Press the Reset Button

For BL10:

  1. Connect BL10 to the USB port

  2. Press and hold the D8 Button (GPIO 8)

  3. Press and release the EN Button (Reset)

  4. Release the D8 Button

For Pinenut and MagicHome BL602:

  1. Disconnect the board from the USB Port

  2. Connect GPIO 8 to 3.3V

  3. Reconnect the board to the USB port

Enter these commands to flash pinedio_lorawan.bin to BL602 / BL604 over UART…

# TODO: Change ~/blflash to the full path of blflash
cd ~/blflash

# For Linux:
sudo cargo run flash pinedio_lorawan.bin \
    --port /dev/ttyUSB0

# For macOS:
cargo run flash pinedio_lorawan.bin \
    --port /dev/tty.usbserial-1420 \
    --initial-baud-rate 230400 \
    --baud-rate 230400

# For Windows: Change COM5 to the BL602 / BL604 Serial Port
cargo run flash pinedio_lorawan.bin --port COM5

(For WSL: Do this under plain old Windows CMD, not WSL, because blflash needs to access the COM port)

More details on flashing firmware

9.3 Run LoRaWAN Firmware

Set BL602 / BL604 to Normal Mode (Non-Flashing) and restart the board…

For PineDio Stack BL604:

  1. Set the GPIO 8 Jumper to Low (Like this)

  2. Press the Reset Button

For PineCone BL602:

  1. Set the PineCone Jumper (IO 8) to the L Position (Like this)

  2. Press the Reset Button

For BL10:

  1. Press and release the EN Button (Reset)

For Pinenut and MagicHome BL602:

  1. Disconnect the board from the USB Port

  2. Connect GPIO 8 to GND

  3. Reconnect the board to the USB port

After restarting, connect to BL602 / BL604’s UART Port at 2 Mbps like so…

For Linux:

sudo screen /dev/ttyUSB0 2000000

For macOS: Use CoolTerm (See this)

For Windows: Use putty (See this)

Alternatively: Use the Web Serial Terminal (See this)

More details on connecting to BL602 / BL604

9.4 Enter LoRaWAN Commands

Let’s enter the LoRaWAN Commands to join The Things Network and transmit a Data Packet!

  1. Log on to The Things Network. Browse to our Device and copy these values…

    JoinEUI (Join Extended Unique Identifier)

    DevEUI (Device Extended Unique Identifier)

    AppKey (Application Key)

    (Instructions here)

  2. In the BL602 / BL604 terminal, press Enter to reveal the command prompt.

  3. First we start the Background Task that will handle LoRa packets…

    Enter this command…

    create_task

    (create_task is explained here)

  4. Next we initialise the LoRa SX1262 and LoRaWAN Drivers

    init_lorawan

    (init_lorawan is defined here)

  5. Set the DevEUI

    las_wr_dev_eui 0xAB:0xBA:0xDA:0xBA:0xAB:0xBA:0xDA:0xBA

    Change “0xAB:0xBA:...” to your DevEUI

    (Remember to change the , delimiter to :)

  6. Set the JoinEUI

    las_wr_app_eui 0x00:0x00:0x00:0x00:0x00:0x00:0x00:0x00

    Change “0x00:0x00:...” to your JoinEUI

    (Yep change the , delimiter to :)

  7. Set the AppKey

    las_wr_app_key 0xAB:0xBA:0xDA:0xBA:0xAB:0xBA:0xDA:0xBA0xAB:0xBA:0xDA:0xBA:0xAB:0xBA:0xDA:0xBA

    Change “0xAB:0xBA:...” to your AppKey

    (Again change , to :)

  8. We send a request to join The Things Network

    las_join 1

    1” means try only once.

    (las_join is explained here)

  9. Finally we open an Application Port that will connect to The Things Network…

    las_app_port open 2

    2” is the Application Port Number

    (las_app_port is explained here)

    (See the complete log)