First driver in Toit

Toit is a language for embedded development on microcontrollers, targeting the ESP32. Like CircuitPython or TinyGo, language libraries provide access to microcontroller peripherals and developers are insulated from the OS. Toit goes significantly further, by addressing device onboarding/lifecycle, monitoring and fleet management, with both a command line and web console interfaces.

Programming on the device is in the Toit language, including drivers for devices connected over SPI or I2C, as described in the Write a driver guide. Drivers are provided in the SDK for several devices, as documented in the drivers page.

To get a sense of the driver development process, I thought it would be sensible to re-implement a fragment of the BME280 driver, which is provided in the SDK (without source). A complete application sample is included in the tutorial, outlining code and deployment. The circuit was easily wired on a protoboard as shown below:

Searching on GitHub for “BME280” yields over 1,700 repositories, but I reviewed the first result Adafruit_BME280_Library. Simply re-coding parts of the .cpp and .h files associated with temperature, yielded the following code:

/*
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// 
// This fragment driver code was derived from the Adafruit BME Library
//      https://github.com/adafruit/Adafruit_BME280_Library
//        Copyright (c) 2015, Limor Fried & Kevin Townsend for Adafruit Industries 
//        All rights reserved.
//    The Adafruit BME Library is subject to the conditions and disclaimer:  
//      https://github.com/adafruit/Adafruit_BME280_Library/blob/master/LICENSE 
//
// To utilize a BME280 sensor, use the Toitware driver included in the SDK
//    https://libs.toit.io/drivers/bme280/library-summary
// 

import binary
import serial

class BME280x:
  static BME280_ADDRESS               ::= 0x77
  static BME280_ADDRESS_ALTERNATE     ::= 0x76

  static BME280_REGISTER_DIG_T1       ::= 0x88
  static BME280_REGISTER_DIG_T2       ::= 0x8A
  static BME280_REGISTER_DIG_T3       ::= 0x8C


  static BME280_REGISTER_CHIPID       ::= 0xD0
  static BME280_REGISTER_VERSION      ::= 0xD1
  static BME280_REGISTER_SOFTRESET    ::= 0xE0

  static BME280_REGISTER_CONTROLHUMID ::= 0xF2
  static BME280_REGISTER_STATUS       ::= 0xF3
  static BME280_REGISTER_CONTROL      ::= 0xF4
  static BME280_REGISTER_CONFIG       ::= 0xF5
  static BME280_REGISTER_TEMPDATA     ::= 0xFA
  
  static REG_DEFAULT_ADDRESS_         ::= 0x00

  calib_dig_T1 := null
  calib_dig_T2 := null
  calib_dig_T3 := null

  registers_/serial.Registers

  constructor device/serial.Device:
    registers_ = device.registers

  on:
    sensorID_ := registers_.read_u8 BME280_REGISTER_CHIPID
    if sensorID_ != 0x60: throw "INVALID_CHIP"
    registers_.write_u8 BME280_REGISTER_SOFTRESET 0xB6
    sleep --ms=10
    while isReadingCalibration:
      sleep --ms=10
    readCoefficients
    setSampling
    sleep --ms=100

  off:

  isReadingCalibration -> bool:

    rStatus := registers_.read_u8 BME280_REGISTER_STATUS
    return (rStatus & (1 << 0)) != 0

  readCoefficients:
    calib_dig_T1 = registers_.read_u16_le BME280_REGISTER_DIG_T1
    calib_dig_T2 = registers_.read_i16_le BME280_REGISTER_DIG_T2
    calib_dig_T3 = registers_.read_i16_le BME280_REGISTER_DIG_T3

  setSampling:

    setSampling Sensor_Mode.MODE_NORMAL 
                Sensor_Sampling.SAMPLING_X16 
                Sensor_Sampling.SAMPLING_X16 
                Sensor_Sampling.SAMPLING_X16 
                Sensor_Filter.FILTER_OFF 
                Standby_Duration.STANDBY_MS_0_5
                --lines

  setSampling mode/Sensor_Mode                   -> none
              tempSampling/Sensor_Sampling
              pressSampling/Sensor_Sampling
              humSampling/Sensor_Sampling
              filter/Sensor_Filter
              standby_duration/Standby_Duration
              --lines:

    measurementRegistor := Control_Measurement tempSampling pressSampling mode
    humidityRegistor := Control_Humidity humSampling
    configurationRegistor := Configuration standby_duration filter

    // making sure sensor is in sleep mode before setting configuration
    // as it otherwise may be ignored
    registers_.write_u8 BME280_REGISTER_CONTROL Sensor_Mode.MODE_SLEEP.x

    // you must make sure to also set REGISTER_CONTROL after setting the
    // CONTROLHUMID register, otherwise the values won't be applied (see
    // DS 5.4.3)
    registers_.write_u8 BME280_REGISTER_CONTROLHUMID humidityRegistor.x
    registers_.write_u8 BME280_REGISTER_CONFIG configurationRegistor.x
    registers_.write_u8 BME280_REGISTER_CONTROL measurementRegistor.x

  read_temperature -> float:

    adc_T := registers_.read_u24_be BME280_REGISTER_TEMPDATA
    if adc_T == 0x800000: // value in case temp measurement was disabled
      return float.NAN
    adc_T >>= 4
//  var1 = ((((adc_T >> 3) - ((int32_t)_bme280_calib.dig_T1 << 1))) * ((int32_t)_bme280_calib.dig_T2)) >> 11;
    var1 := (((adc_T >> 3) - (calib_dig_T1 << 1)) * (calib_dig_T2)) >> 11;
//  var2 = (((((adc_T >> 4) - ((int32_t)_bme280_calib.dig_T1)) * ((adc_T >> 4) - ((int32_t)_bme280_calib.dig_T1))) >> 12) * ((int32_t)_bme280_calib.dig_T3)) >> 14;
    var2 := (((((adc_T >> 4) - calib_dig_T1) * ((adc_T >> 4) - calib_dig_T1)) >> 12) * calib_dig_T3) >> 14;
    t_fine := var1 + var2 + 0 // t_fine_adjust // not supported in this sample code
    T := (t_fine * 5 + 128) >> 8;
    return T / 100.0;

class Sensor_Mode:
  x/int
  constructor .x:

  constructor.MODE_SLEEP:
    return Sensor_Mode 0x0b00
  constructor.MODE_FORCED:
    return Sensor_Mode 0x0b01
  constructor.MODE_NORMAL:
    return Sensor_Mode 0x0b11

class Sensor_Sampling:
  x/int
  constructor .x:

  constructor.SAMPLING_NONE:
    return Sensor_Sampling 0x0b000
  constructor.SAMPLING_X1:
    return Sensor_Sampling 0x0b001
  constructor.SAMPLING_X2:
    return Sensor_Sampling 0x0b010
  constructor.SAMPLING_X4:
    return Sensor_Sampling 0x0b011
  constructor.SAMPLING_X8:
    return Sensor_Sampling 0x0b100
  constructor.SAMPLING_X16:
    return Sensor_Sampling 0x0b101

class Sensor_Filter:
  x/int
  constructor .x:

  constructor.FILTER_OFF:
    return Sensor_Filter 0x0b000
  constructor.FILTER_X2:
    return Sensor_Filter 0x0b001
  constructor.FILTER_X4:
    return Sensor_Filter 0x0b010
  constructor.FILTER_X8:
    return Sensor_Filter 0x0b011
  constructor.FILTER_X16:
    return Sensor_Filter 0x0b100

class Standby_Duration:
  x/int
  constructor .x:

  constructor.STANDBY_MS_0_5:
    return Standby_Duration 0x0b000
  constructor.STANDBY_MS_10:
    return Standby_Duration 0x0b110
  constructor.STANDBY_MS_20:
    return Standby_Duration 0x0b111
  constructor.STANDBY_MS_62_5:
    return Standby_Duration 0x0b001
  constructor.STANDBY_MS_125:
    return Standby_Duration 0x0b010
  constructor.STANDBY_MS_250:
    return Standby_Duration 0x0b011
  constructor.STANDBY_MS_500:
    return Standby_Duration 0x0b100
  constructor.STANDBY_MS_1000:
    return Standby_Duration 0x0b101

class Control_Measurement:
  osrs_t/Sensor_Sampling
  osrs_p/Sensor_Sampling
  mode/Sensor_Mode

  constructor .osrs_t .osrs_p .mode:

  x -> int:
    return (osrs_t.x << 5) | (osrs_p.x << 2) | mode.x

class Control_Humidity:
  osrs_h/Sensor_Sampling

  constructor .osrs_h:

  none_ ::= 5 // unused, don't set

  x -> int:
    return osrs_h.x

class Configuration:
  t_sb/Standby_Duration
  filter/Sensor_Filter

  constructor .t_sb .filter:

  none_ ::= 1      // unused, don't set
  spi3w_en_ ::= 1  // unused, don't set

  x -> int:
    return (t_sb.x << 5) | (filter.x << 2) | spi3w_en_

Several observations:

  • naive re-coding is simple, readable and fast, thanks to Toit’s clean syntax
  • working in Visual Studio Code, with the Toit plugin, is pleasant … the syntax highlighting and static analysis really helps learning
  • the “toy driver” worked first time, what happens when you crib good code
  • results match the production driver to at least 1 decimal place (note, t_fine_adjust is ignored)
  • there is no test code as would be necessary in any production driver … the purpose here was only to get a sense of the process
  • classes (such as Sensor_Sampling) and named constructors was modeled after Attaching Values to Java Enum. The getter .x was a terse alternative to the Smalltalk <instance> value idiom. Comments welcome.

Overall, the experience was very good for an environment that has only been generally available for several months. The Time Of Flight sensor I want to use does not have a driver in the Toit SDK , so it will be “coding without a net”.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.