Reading data from Si7021 temperature and humidity sensor using Raspberry Pi

The Si7021is an excellent little device for measuring temperature and humidity, communicating with the host controller over the I2C bus. This is a quick tutorial on using the Raspberry Pi to talk to this device. If you are unfamiliar with the conceptual framework of I2C or how to enable I2C access on the Raspberry Pi, I suggest starting here. Otherwise, let’s jump in.

You are probably working with the device mounted on a breakout board. I used this one from Adafruit. There are no surprises on the pins that it breaks out - Vin, 3v out, GND, SCL and SDA. One the 40-pin P1 header of the Raspberry Pi, SDA and SCL for I2C bus 1 occupy pins 2 and 3.

Once you’ve wired it all up (don’t forget common ground connections to the Pi,) then we’re ready to write some code. First, we need to study the device a little bit.

Si7021 Humidity and temperature sensor

The device is quite accurate for temperature, typically ±0.4 degrees C. The humidity is ±3%. It can operate down to -40 degrees C, which is important in Canada where I live!

The I2C implementation on the device is straightforward. It has a fixed I2C hardware address of 0b01000000 (0x40). The instruction set is not large, but there’s some nuance which we’ll explain. First, let’s get some #define statements out of the way in our code:

#define SI7021_ADDR 0x40

//  I2C COMMANDS
#define SI7021_MRH_HOLD     0xE5
#define SI7021_MRH_NOHOLD   0xF5
#define SI7021_MT_HOLD      0xE3    //  measure temp, hold master
#define SI7021_MT_NOHOLD    0xF3    //  measure temp, no hold master
#define SI7021_RT_PREV      0xE0    //  read temp from last RH measurement
#define SI7021_RESET        0xFE    //  reset
#define SI7021_WR_USER1     0xE6    //  write RH/T user register 1
#define SI7021_RD_USER1     0xE7    //  read RH/T user register 1
#define SI7021_WR_HCTL      0x51    //  write heater control register
#define SI7021_RD_HCTL      0x11    //  read heater control register
#define SI7021_RD_ID1       0xFA 0x0F   //  read electronic ID 1st byte
#define SI7021_RD_ID2       0xFC 0xC9   //  read electronic ID 2nd byte
#define SI7021_RD_REV       0x84 0xB8   //  read firmware revision

Simple register read

Let’s perform a simple register read on the chip. This is 16 bytes long and requires two separate reads. We can read the first group of bytes by writing 0xFA 0x0F and the second group of bytes by writing 0xFC 0xC9. With each opcode, we read in 8 bytes. The BCM2835 C library makes this quite simple. Here’s the code:

//
//  Read the device serial number and return as string
//
uint8_t readSerialNumber(char *instr) {
    uint8_t buf[8] = {0xFA,0x0F};
    char *str = (char *) malloc(25);
    char *str2 = (char * ) malloc(13);
    if( bcm2835_i2c_write(buf,2) != BCM2835_I2C_REASON_OK ) {
        return SI7021_FAIL;
    }
    if( bcm2835_i2c_read(buf,8) != BCM2835_I2C_REASON_OK ) {
        printf("Read failed\n" );
        return SI7021_FAIL;
    }
    sprintf(str,"%02X %02X %02X %02X ",buf[0],buf[2],buf[4],buf[6]);
    buf[0] = 0xFC; buf[1] = 0xC9;
    bcm2835_i2c_write(buf,2);
    bcm2835_i2c_read(buf,8);
    sprintf(str2,"%02X %02X %02X %02X\0",buf[0],buf[2],buf[4],buf[6]);
    strcpy(instr, strcat(str,str2));
    return SI7021_OK;
}

Notice how we allocate memory on the stack for two character pointers that we use as intermediate steps which compiling a string of bytes as characters. To call the function, we need to allocate memory for the pointer to the string on the caller’s side, too. Then we pass the pointer to the function:

//  read the device serial number
char * sstr = (char *) malloc(26);
readSerialNumber(sstr);
printf("%s\n",sstr);

Reading the temperature

Notice that we have two I2C commands to read the temperature which we define as SI7021_MT_HOLD and SI7021_MT_NOHOLD. Since the device cannot take a reading in real-time, there is a delay between receiving the command and writing a value back to the host. To do this, we have two choices. We can either hold the host waiting for the data or we can poll the device to see when the data is ready. For the temperature conversion, we’ll opt to wait, so the opcode will be SI7021_MT_HOLD. The way to do this is by stretching the clock long enough to cover the conversion latency. But how long do we have to wait? From table 2 in the datasheet, we see that a 12-bit relative humidity measurement takes 10-12 ms. The maximum conversion latency for 14 bit temperature (the default resolution) is almost 11 ms for a total latency of up to 23 ms.

How do we set the clock stretch timeout?

Fortunately, there’s a BCM2835 register that we can set. The lower 16 bits of the CLKT register represents the TOUT field. Here, we can set the number of SCL clock cycles (not ms) to wait.

CLKT register

If we use the default I2C clock divider of 150 (BCM2835_I2C_CLOCK_DIVIDER_150) then our clock speed is 1.666 MHz or a period of 60 ns. If we wanted to wait for, say, 40 ms to provide a safety factor, then we would have to wait for 4 x 10^7 ns or 666,666 clock cycles. Since we can only represent numbers in 16 bits, we simply cannot wait that long for the slave device to hold the clock. A different strategy is required. Instead, we’ll just slow the clock down even more. By lowering the overall I2C clock rate, we should be able to squeeze the wait cycle count down to the necessary level. So what if we reduced the I2C clock speed using BCM2835_I2C_CLOCK_DIVIDER_626? Then we have an I2C clock speed of about 399 kHz with a period of about 2.5 us. At that clock speed, we would wait 40000 us/2.5 us/cycle or 16000 cycles. We can easily manage that in the lower 16 bits of CLKT:

void setTimeout(uint16_t timeout) {
     volatile uint32_t* stimeout = bcm2835_bsc1 + BCM2835_BSC_CLKT / 4;
     bcm2835_peri_write(stimeout, timeout);
}

This code is straight out of this tutorial. What took me a while to understand is why the offset to the CLKT register at the BSC1 base address gets divided by 4. Well, the offset in bytes is 0x1C, but our addresses are 32 bits wide, so we divide the offset by 4 to get the actual address. The rest is self-explanatory.

Now that we’ve seen how to increase the clock waiting time to compensate for the conversion latency on the slave device, we can actually read the data. We’ve opted to hold the master. Reading the temperature requires simply writing the appropriate opcode and reading in two bytes of data (and a checksum, which we’ll ignore for now.) The conversion to readable data is covered in the {% asset_link Si7021-A20.pdf Si7021 datasheet %}. It requires some floating point calculations:

Converting raw data to temperature

To read the temperature from the device, we’ll employ the following function:

//
//  Read the current temperature
//
float readTemperature(uint8_t *status) {
    uint8_t buf[4] = { SI7021_MT_HOLD };
    if( bcm2835_i2c_read_register_rs(buf,buf,3) != BCM2835_I2C_REASON_OK ) {
        *status = SI7021_FAIL;
        return 0.0;
    }
    uint8_t msb = buf[0];
    uint8_t lsb = buf[1];
    unsigned int data16 = ((unsigned int) msb << 8) | (unsigned int) (lsb & 0xFC);
    float temp = (float) (-46.85 + (175.72 * data16 / (float) 65536));
    *status = SI7021_OK;
    return temp;
}

Reading the humidity

Reading the relative humidity is not much different, but as with this tutorial it’s written so as to keep the master “on hold” until the device is ready to read. The conversion of raw data to meaningful RH is also given in the manual.

//
//  Read the humidity
//
float readHumidity() {
    uint8_t buf[4] = { SI7021_MRH_NOHOLD };
    bcm2835_i2c_write(buf,1);
    while( bcm2835_i2c_read(buf,3) == BCM2835_I2C_REASON_ERROR_NACK ) {
        bcm2835_delayMicroseconds(500);
    }
    uint8_t msb = buf[0]; uint8_t lsb = buf[1];
    uint16_t data16 = ((unsigned int) msb << 8) | (unsigned int) (lsb & 0xFC);
    float hum = -6 + (125.0 * (float) data16) / 65536;
    return hum;
}

Notice here that we keep the master held waiting while we poll the device waiting for a response.

That’s it. There’s more to the device; but you should now have a good base for exploring more. My entire code is [here].

References

RF communication between Arduino Nanos using nRF24L01

In this tutorial I’ll go through a simple example of how to get two Arduino Nano devices to talk to one another.

Materials

You’ll need the following materials. I’ve posted Amazon links just so that you can see the items, but they can be purchased in a variety of locations.

  • Arduino Nano 5V/16 MHz, or equivalent (Amazon)
  • Kuman rRF24L01+PA+LNA, or equivalent (Amazon)

About the nRF24L01+

The nRF24L01+ is an appealing device to work with because it packs a lot of functionality on-chip as opposed to having to do it all in software. There is still a lot of work to be done in code; but it’s a good balance between simplicity and functionality. It’s also inexpensive.

What follows is a lengthy description of the nRF24L01+ device. If you just want to connect up your devices, then you can skip to the device hookup section.

nRF24L01+ theory of operation

There are several libraries for the nRF24L01 in the public domain that seek to simplify interactions with a variety of MCU’s. While they are fine (and we’ll make use of one here) you should understand how the device works so that when you inevitably branch out from the basic demonstration projects, you know how to achieve what you want. Read the nRF24L01 datasheet. I’ll start out here by reviewing it at a high level.

More than likely you are working with a breakout board for this surface-mount device. So you will concern yourself only with the following pins: Vcc, GND, CE, CSN, IRQ, MISO, MOSI, SCK. For the purposes of this example, we won’t be using the interrupt line IRQ so you can leave it disconnected.

The nRFL01 has a relatively simple instruction set for the SPI interface.

Table 8: nRF24L01 SPI instruction set

Reading from nRF24L01 registers

The protocol for addressing the transceiver via the SPI instruction set begins by bringing the CSN line from high to low, thus informing the nRFL01 that an instruction is about to be clocked into it. Next you clock in the command byte for the instruction you which to execute, followed by the data relevant to that instruction. Before we talk about how to configure the instructions, we should glance at the register addresses because we will refer to their addresses in the examples. You will find the memory map of the addresses on pages 22-26 of the nRF24L01 datasheet. For our example, let’s assume we want to read the RF_SETUP register. From page 23 of the nRF24L01 datasheet we see that the memory address is 0x06. To read that register, we bring CSN low, send an R_REGISTER instruction encoded with the register address. From Table 8, we see that the format for this instruction is 0b000aaaaa where the 5 bits aaaaa represent the memory address of the register. In this case we would send 0b00000110 (0x06). For this instruction, we expect a single byte return. To get that return we have to clock in a dummy byte on MOSI. Note that every command also returns the STATUS register.

Writing to nRF24L01 registers

The device would be useless if we couldn’t write to any registers, so we should talk about how to do that. Table 8 shows the W_REGISTER command has the following format: 0b001aaaaa where the 5 lower bits represent the address of the register we want to write to. Let’s say we want to set the data rate to 1 Mps and the RF output power to minimum. First let’s take a look at the format for the RF_SETUP register:

The upper 3 bits are reserved and should be 000. The PLL_LOCK bit is only used in testing. So that leaves us with bits 3:0. Bit 3 RF_DR sets the data rate. Since we want 1 Mbps, that bit gets unset. The next 2 bits set the RF_PWR. Minimum power is 00 for these two bits. The last bit LNA_HCURR sets the low noise amplifier gain. The nRF24L01+ datasheet discusses the LNA gain in more depth. The LNA gain allows the device to reduce the current consumption in receive mode at the expense of some receiver sensitivity. Since we’ll be sufficiently powered, it’s OK to leave that bit unset.

So, the write protocol is to drop CSN and send the W_REGISTER command configured for the address of interest. So, we’ll send 0b00100110 (0x26) followed by 0b00000000 (0x00) to congure it in the way we describe above (1 Mbps, low output power.)

Read receive payload

The next command of interest is the R_RX_PAYLOAD which as the name implies reads a payload of bytes that were received by the device. The command format requires no configuration; it is simply 0b01100001 (0x61.) There is some “choreography” involved in using this command because you must manipulate the CE pin also. How do you know you’ve received a packet? You know a packet has been received when an RX_DR (data ready) interrupt has been triggered. We’ll get to this later but for now, you should know that this interrupt exists as a bit in the STATUS register (yes, the register that we constantly get returned when we send any command.) We can also choose to configure the transceiver to send a hardware interrupt when it receives a package. When a unit is receiving, the CE pin must be high, once you’ve received a package, you have to bring the CE pin low, the send the R_RX_PAYLOAD command as usual. Next, you clock in the same number of dummy bytes as your payload size in order to read out the payload. What happens if you’ve received multiple payloads? The device keeps multiple payloads (3 per pipe) in a first-in, first-out (FIFO) stack. When you’re done receiving, you should clear the RX_DR interrupt and bring the CE pin high again to start receiving.

Write receive payload

To write a payload, we’ll use the W_TX_PAYLOAD. When your device is transmitting, you hold CE low (opposite of read). The “choreography” here is a little different. First we send a W_TX_PAYLOAD (0b10100000 = 0xA0) along with the number of bytes specified by the payload size. Next, we signal a transmit by toggling the CE pin from low to high to low over at least 10 µs. The ТХ_DS interrupt will be set if the packet is sent. Actually, the behaviour is a little more complicated than that. This interrupt actually depends on whether you have auto-acknowledge enabled for the pipe. If you do, then the TX_DS interrupt is only set if you get an ack from the receiver on the pipe. If you are auto-acknowledging on that pipe then, then you also have to look for the MAX_RT flag on the STATUS register to see whether the maximum number of retries has been reached. As with the receiver, there’s also a FIFO transmit stack, so you can stack up to three packets before sending (by toggling CE.)

Flushing the TX and RX stacks

There are two SPI commands that clear the TX and RX FIFO stacks, FLUSH_TX and FLUSH_RX respectively. Neither has has any associated bytes.

NOP command reads STATUS register

There is a NOP (no operation) command that takes no additional bytes and whose only purpose is to read back the STATUS register quickly. It is faster than R_REGISTER because you don’t have to pass the address of the STATUS register.

nRF24L01 registers

Next, I’ll be talking about some of the nRFL01 registers. Since the nRF24L01 datasheet covers everything, we’ll just go over the high points and any gray areas. As always, if you just want to get two Arduinos talking to each other, you can skip to the device hookup section.

Configuration register

The CONFIG register at address 0x00, has a number of useful bits.

CONFIG register

Bits 6,5,4 control how we use the IRQ pin. If we want the RX_DR (packet received) interrupt to show up on the IRQ pin, then we would set the MASK_RX_DR bit. Then it will show up as an active low state on the IRQ pin. Likewise for the MASK_TX_DS interrupt. Remember that the TX_DS flag behaviour depends on whether we’ve enabled auto-acknowledge for the pipe we’re using. The MASK_MAX_RT bit determines whether the MAX_RT state is reflected on the IRQ pin or not. The EN_CRC enables CRC error detection and its default value is 1 (enabled.) You can control power to the transceiver by manipulating the PWR_UP bit. The last bit is the PRIM_RX. If set, your device is a receiver; otherwise it’s a transmitter.

Enable auto-acknowledgment registers

EN_AA register

You can enable or disable auto-acknowledgment on any of the 6 data pipes via this register. For the most fault-tolerant system design, you should enable the auto-acknowledgment on the pipes you are using.

Enable RX addresses

EN_RX_ADDR register

To enable receiving on a given pipe, set its bit in this register.

Set address width register

SETUP_AW

The width of each address across all data pipes, both receive and transmit is set via this register. The width can be configured to be from 3-5 bytes in length and it must be consistent between all devices. Longer is better.

Setup automatic retransmission

SETUP_RETR

In this register you can set up how many times to retry transmission after an initial failure. The number of tries is setup in the lower 4 ARC bits and can therefore range from 0x00 to 0x0F. If automatic retransmission is enabled, the upper three bits specify the delay in microseconds. Each unit from 0-15 increases the delay by 250 µs.

OBSERVE_TX register

OBSERVE_TX

The register is a sort of quality-control register. The upper 4 bits count the number of lost packets and is reset by writing to the the RF_CH register. The lower 4 ARC_CNT bits provide a count of the number of retransmissions. It is reset with each new packet.

Received power detector registers

RPD

The RPD register was previously called the CD (carrier detect) register on the nRF24L01. Only the lower bit is relevant. It triggers to 1 if the received power is above -64 dBm currently receiving, or zero if less than -64 dBm.

Receive address registers

The receive address registers occupy the memory offsets from 0x0A to 0x0F for data pipes 0-5. They are known by the RX_ADDR_Px where x is 0-5. Note that RX_ADDR_P0 is 40 bits wide with a reset value 0xE7E7E7E7E7. RX_ADDR_P is also 40 bits wide but has a reset value of 0xC2C2C2C2C2. The remaining addresses are only a single byte register because they must differ from the base address only by the LSB, the one that is stored.

Transmit address register

Register TX_ADDR occupies memory address 0x10 and is used only on a primary transmitter PTX device. If you want to use auto-ack, this address must be the same as RX_ADDR_P0.

Receive channel payload widths

Receive channel payload width

Each of the six data pipes can have its own payload width. The registers that specify these widths all follow the same format as the RX_PW_P0 register depicted about. The occupy memory slots 0x11 to 0x16. Only the lower 6 bits are used and therefore can express numbers from 1-32.

FIFO status register

FIFO_STATUS register

The FIFO status register at 0x17 reports the status of the FIFO receive and transmit stacks along with related information. The TX_REUSE flag is set when a transmit payload has been reused by pulsing the CE high and using the REUSE_TX_PL command. The rest of the bits relate to the current state (empty or full) of the receive and transmit payload stacks.

Status register

STATUS register

We see a lot of the status register because it gets returned to us, remember, when we clock in a command, whether we ask for it or not. The RX_DR bit is set new data arrives in the receive stack. You can clear this flag by writing a 1 to it. Similarly, the TX_DR bit is set when a packet gets transmitted. If you have enabled AUTO_ACK then this bit gets flipped only when you receive and ACK signal. The MAX_RT flag is set when the maximum number of retransmit retries had been reached. If it gets set, you must reset this flag manually by writing 1 to it. Otherwise you cannot go on transmitting. RX_P_NO these bits reflect the number of the data pipe that has data in the receive FIFO. Finally, the TX_FULL flag is set when the transmit FIFO is full.

nRF24L01+ registers

Some registers are unique to the newer nRF24L01+ device.

DYNPD enable dynamic payload length

The DYNPD register at memory offset 0x1C allows you to enable dynamic payload length on specific data pipes. We will be using fixed payload length in our example.

FEATURE register

The FEATURE register allows you to set features related to dynamic payload length. We’ll leave this to a later discussion since we won’t use this in our example.

Are you finally ready to start connecting everything?

Setting up the devices

Attach the transceiver breakout board to the Arduino Nano in the following fashion:

  • Vcc to 3.3v (not 5v!)
  • GND to ground, shared by Nano
  • CE to D9
  • CSN to D10
  • SCK to D13
  • MOSI to D11
  • MISO to D12

Note that the GettingStarted example code for the RF24 library specifies pin D7 for CE and pin D8 for CSN. Since mine were connected different, you’ll have to modify the GettingStarted example sketch accordingly.

Wire up your Nano to the transceiver breakout board as above. Modify the GettingStarted sketch so that the line RF24 radio(7,8); reads RF24 radio(9,10); instead, corresponding to our wiring differences. Also insure that the transmit power is set at minimum since the antennas are going to inches apart on the breadboard. Just ensure that the line radio.setPALevel(RF24_PA_MIN); is present in the setup() function. Now just load this sketch onto the first Nano. Then do the same with the second Nano. Using the serial monitor you will designate the first Nano as the transmitter by entering T. For the second Nano, it will default to primary receive mode.

If you’ve hooked up everything correctly, you should be seeing the Nano’s pass data back and forth.

References

Using the Raspberry Pi to communicate over the I2C bus using C

I recently wrote about using the excellent bcm2835 library to communicate with peripheral devices over the SPI bus using C. In this post, I’ll talk about using the same library to communicate over the I2C bus. Nothing particularly fancy, but you’ll need to pay careful attention to the datasheet of the device we’re using. TheTSL2561 is a sophisticated little light sensor that has a very high dynamic range and is available on a breakout board from Adafruit. I’m not going to delve into the hookup of this device as you can take a look at the Adafruit tutorial for that. Note that we’re not going to use their library. (Well, I borrowed a bunch of their #define statements for device constants.)

Implementing ADC using Raspberry Pi and MCP3008

Several years ago I wrote about adding analog-to-digital capabilities to the Raspberry Pi. At that time, I used an ATtinyx61 series MCU to provide ADC capabilities, communicating with the RPi via an I2C interface. In retrospect it was much more complicated than necessary. What follows is an attempt to re-do that project using an MCP3008, a 10 bit ADC that communicates on the SPI bus.

MCP3008 device

The MCP3008 is an 8-channel 10-bit ADC with an SPI interface^[Datasheet can be found here.]. It has a 4 channel cousin, the MCP3004 that has similar operating characteristics. The device is capable of performing single-ended or differential measurements. For the purposes of this write-up, we’ll only concern ourselves with single-ended measurement. A few pertinent details about the MCP3008:

2018: Experiment No. 1

2018 is my year of experiments (Why? TL;DR: New Year’s resolutions are over-rated and have a high failure rate. Anyone can run an experiment for a month.) My first experiment (No news for a month) is nearly done and I’ll declare it a success.

Background

The round-the-clock sensational news cycle exists in large part to create wealth for the already-too-wealthy. Little of it is actionable, leaving us at the same time both outraged and impotent. Mostly I decided to give up on the news because of Donald Trump, the demented psychopathic moron who managed to get elected president.^[I use these terms very carefully. Many have speculated that he suffers from some form of dementia owing to events where he slurs his words and perseverates. His sociopathic or psychopathic behaviours are well-documented; he is man devoid of empathy. And finally, his lack of reading is well-known. For all I can tell, the man is a functional illiterate. In contrast, his predecessor is a bibliophile and read widely and voraciously throughout his tenure.] Since Trump took office, like others, I’ve found myself cycling repeatedly through the stages of grief. But mostly I’ve been stuck on anger. There’s something about willful ignorance that does that to me.

2018: A year of experiments

New Year’s resolution time is at hand. But not for me; at least not in a traditional sense. I was inspired by David Cain’s experiments. In short, he conducts monthly experiments in self-improvement.

The idea of an experiment is appealing in ways that a resolution is not. A resolution presumes an outcome and relies only on the long application of will to see it through. An experiment on the other hand, makes only a conjecture about the outcome and can be conducted for a shorter period.

Peering into Anki using R

Yet another diversion to keep me from focusing on actually using Anki to learn Russian. I stumbled on the R programming language, a language that focuses on statistical analysis.

Here’s a couple snippets that begin to scratch the surface of what’s possible. Important caveat: I’m an R novice at best. There are probably much better ways of doing some of this…

Counting notes with a particular model type

Here we’ll use R to do what we did previously with Python.

Language word frequencies

Since one of the cornerstones of my approach to learning the Russian language has been to track how many words I’ve learned and their frequencies, I was intrigued by reading the following statistics today:

  • The 15 most frequent words in the language account for 25% of all the words in typical texts.
  • The first 100 words account for 60% of the words appearing in texts.
  • 97% of the words one encounters in a ordinary text will be among the first 4000 most frequent words.

In other words, if you learn the first 4000 words of a language, you’ll be able to understand nearly everything.