Skip to content

Commit 6184e66

Browse files
committed
tour: add I2C tour component explaining the LIS3DH
1 parent 9d0a8d5 commit 6184e66

File tree

10 files changed

+402
-1
lines changed

10 files changed

+402
-1
lines changed

content/tour/imu/_index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: "I2C: Accelerometer"
3+
type: "docs"
4+
description: "Learn the I2C protocol by reading the physical orientation using an accelerometer."
5+
weight: 5
6+
---
7+
8+
This tour will give an introduction into using I2C devices, using the LIS3DH accelerometer. This is a cheap and low-power accelerometer that is used on a lot of prototyping boards. It explains how to connect to an accelerometer and how to read its values.
9+
10+
Officially these are called I²C devices, but for simplicitly we're using I2C for readability and searchability instead.

content/tour/imu/configure.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
title: "Configuring the accelerometer"
3+
description: "Turn on the accelerometer and start reading values"
4+
weight: 2
5+
---
6+
7+
The accelerometer that we are using, LIS3DH, starts in a low power mode. That's not the case for all such sensors, many start up in an already enabled state that uses more power. Starting up to a low power mode means that we can ignore the accelerometer when we don't use it and it won't use more power than necessary, but it does mean we need to configure it before we can use it.
8+
9+
The beginning of the code on the right is the same as before, but now we do some magic:
10+
11+
```go
12+
err = i2c.Tx(0x18, []byte{0x20, 0b0100_0111}, nil)
13+
if err != nil {
14+
println("got error while configuring the LIS3DH:", err.Error())
15+
return
16+
}
17+
````
18+
19+
That's a lot of magic numbers! Let's go through them:
20+
21+
* 0x18 is the I2C address, like before. It varies a bit depending on the electronics, but it will always be 0x18 or 0x19 for this particular sensor.
22+
* 0x20 is the register address.
23+
* 0b0100_0111 (8 bits of binary data) is the value that we're going to write to register 0x20.
24+
25+
Internally, most I2C devices are organized as a number of registers. You can think of it as a slice of bytes, one that's typically just 128 or so bytes long. This slice contains both configuration data (which can typically be read and written), and sensor output data (which is typically read-only). In one transaction, you can either read or write data to the I2C device:
26+
27+
* For reading, you send a single byte (the register address) to the device and then read the response values into the `r` byte slice. This is what we did in the previous step of this tour, to read the `WHO_AM_I` register.
28+
* For writing, you send a slice of bytes to the device and read nothing back. The first byte in the slice is the address, the subsequent bytes are the values to write to the registers starting at the given address. This is what we do above.
29+
30+
To know what register 0x20 is, and what the value 0b0100_0111 means, we need to take another look at the [datasheet](https://cdn-shop.adafruit.com/datasheets/LIS3DH.pdf#page=29):
31+
32+
![Section of the LIS3DH datasheet describing the "CTRL_REG1" register](/images/i2c-lis3dh-config.png)
33+
34+
This section describes register CTRL_REG1 at address 20h. "20h" means "hexadecimal 20", which is the same as 0x20 in Go.
35+
36+
The register itself contains 8 bits: 4 bits for "ODR", one for "LPen", and 3 bits for the three axes (Z, Y, X).
37+
38+
Decoding our value 0b0100_0111 (starting from the lowest bits on the right), we see the following:
39+
40+
* The lowest 3 bits are the three axes: X, Y, and Z. Of course we want to enable all of them but it is possible to disable some of them if you need. Who knows, it might reduce power usage a little (even though this chip already uses very little power).
41+
* The next bit is LPen, or "low power mode enable". This bit is zero, so that means low power mode is disabled (if we enable it, we get less precision from the sensor).
42+
* The upper bits, 0100, are the four ODR bits. These four bits are described in table 26 above. Looking at the table, "0 1 0 0" means that we will enable the accelerometer to run at 50Hz (measuring acceleration 50 times per second). This seems like a reasonable speed for our purposes.
43+
44+
Now if you look at the "Properties" tab on the right, you can see that the LIS3DH is indeed configured as "Normal mode 50Hz, enabled axes X, Y, Z", exactly what we wanted! If you go back one step (where we read the `WHO_AM_I` register) you can see it is set to "Power down mode" instead since that is the power-on default. Also, if you go to the "Power" tab, you can see that the LIS3DH now uses 11µA instead of 0.5µA as it did in power down mode.
45+
46+
Try changing the value 0b0100_0111 to something else, and see what happens in the simulator!
47+
48+
<script type="module">
49+
let code = `
50+
package main
51+
52+
import (
53+
"machine"
54+
)
55+
56+
func main() {
57+
i2c := machine.I2C0
58+
err := i2c.Configure(machine.I2CConfig{
59+
SCL: machine.SCL_PIN,
60+
SDA: machine.SDA_PIN,
61+
})
62+
if err != nil {
63+
println("could not configure I2C:", err.Error())
64+
return
65+
}
66+
67+
err = i2c.Tx(0x18, []byte{0x20, 0b0100_0111}, nil)
68+
if err != nil {
69+
println("got error while configuring the LIS3DH:", err.Error())
70+
return
71+
}
72+
73+
println("accelerometer configured!")
74+
}
75+
`;
76+
import {setupLIS3DH} from "/tour-lis3dh.js";
77+
setupLIS3DH(code);
78+
</script>

content/tour/imu/connected.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
title: "Get started with I2C"
3+
description: "Get started with the LIS3DH by verifying the connection."
4+
weight: 1
5+
---
6+
7+
The LIS3DH sensor can measure acceleration and changes in temperature. But to get this data from the sensor, we need a way to establish communication. We'll be using the I2C protocol, which is the most common way to communicate with low-speed external devices.
8+
9+
```go
10+
i2c := machine.I2C0
11+
err := i2c.Configure(machine.I2CConfig{
12+
SCL: machine.GP17,
13+
SDA: machine.GP16,
14+
})
15+
```
16+
17+
To start using I2C, we need to configure I2C on the microcontroller. This code (for the Raspberry Pi Pico) configures GP17 and GP16 as the pins to be used. Note that you can't use all pins! Many microcontrollers only allow using specific pins for I2C. We'll get to that later.
18+
19+
```go
20+
w := []byte{0x0f}
21+
r := make([]byte, 1)
22+
err := i2c.Tx(0x18, w, r)
23+
```
24+
25+
This does an actual transaction. This code will first write the I2C peripheral address (0x18 or 0x19 depending on the electronic design), then write the byte 0x0f in the `w` byte slice, and then read a single byte back to store in the `r` byte slice. The peripheral address is the one for the LIS3DH, and 0x0f is a register called `WHO_AM_I` which when read returns a fixed value to identify the chip.
26+
27+
![WHO_AM_I register description, with the bits in the register being "0 0 1 1 0 0 1 1"](/images/lis3dh-who-am-i.png)
28+
29+
Here is the description from the [datasheet](https://cdn-shop.adafruit.com/datasheets/LIS3DH.pdf#page=29). It says the returned bits are "0 0 1 1 0 0 1 1". Decoding this to decimal, we get the number 51. And that's exactly what we get back from the chip, which shows that we managed to communicate with the chip!
30+
31+
32+
## Connecting I2C devices
33+
34+
I2C uses two wires to communicate: a clock wire (SCL) and a data wire (SDA). Connect the SCL pin on the microcontroller to the SCL pin on the sensor, and the SDA pin on the microcontroller to the SDA pin on the sensor. Also, make sure there is a ground connection between the two, otherwise communication won't work!
35+
36+
Apart from SCL and SDA (and a shared ground), the sensor also needs some power. This is often 3.3V, but check the datasheet to be sure. If the sensor needs 3.3V and you provide it 5V, it will likely burn out! In the simulator we skip this requirement to avoid clutter.
37+
38+
I2C may also need some external pullup registers, that make sure the wires stay high unless they are set to a low voltage by the controlling device. In practice, many sensors already include pullup resistors and microcontrollers often configure I2C wires with a pullup, so this may not be required in practice. It's something to be aware of when you design your own board though.
39+
40+
## I2C basics
41+
42+
I2C allows using multiple devices with just one pair of wires! This pair of wires (SCL and SDA) is called a bus, and can be connected to multiple sensors and actuators. It typically only allows one controlling device (the main microcontroller), but on this bus up to 127 or so devices can be used. These are usually sensors or other low-speed devices, but you can also configure another microcontroller as a peripheral device to allow communication with the controlling device.
43+
44+
To support multiple devices on a single bus, every I2C peripheral device has an address. In the case of the LIS3DH, that is 0x18 or 0x19 in hexadecimal. You can change the address by connecting the SA0/SDO pin to VCC or GND.
45+
46+
Communication is always initiated from the controller device (typically the microcontroller), and happens in a single transaction with a peripheral device. The controller sends the address, then sends a few bytes (which can be zero-length), and then receives a number of bytes. That all happens in a single operation called a "transaction".
47+
48+
In other documentation you may see the term "master" and "slave", but we chose to use the [less problematic](https://oshwa.org/resources/a-resolution-to-redefine-spi-signal-names/) "controller" and "peripheral" instead.
49+
50+
As usual, [SparkFun has an excellent article explaining all the ins and outs of I2C](https://learn.sparkfun.com/tutorials/i2c/all) if you want to learn more.
51+
52+
53+
<script type="module">
54+
let code = `
55+
package main
56+
57+
import (
58+
"machine"
59+
"time"
60+
)
61+
62+
func main() {
63+
i2c := machine.I2C0
64+
err := i2c.Configure(machine.I2CConfig{
65+
SCL: machine.SCL_PIN,
66+
SDA: machine.SDA_PIN,
67+
})
68+
if err != nil {
69+
println("could not configure I2C:", err.Error())
70+
return
71+
}
72+
73+
for {
74+
// Read the "who am I" register.
75+
w := []byte{0x0f}
76+
r := make([]byte, 1)
77+
err := i2c.Tx(0x18, w, r)
78+
if err != nil {
79+
println("got error while reading 'who am I' register:", err.Error())
80+
} else {
81+
println("chip says:", r[0])
82+
}
83+
84+
time.Sleep(time.Second)
85+
}
86+
}
87+
`;
88+
import {setupLIS3DH} from "/tour-lis3dh.js";
89+
setupLIS3DH(code);
90+
</script>

content/tour/imu/reading.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
title: "Reading acceleration values"
3+
description: "Read the X, Y, and Z values from the accelerometer."
4+
weight: 3
5+
---
6+
7+
Now that we have the accelerometer configured, we can start reading the values it reads.
8+
9+
Most of the code is the same as before, but the reading part is new:
10+
11+
```go
12+
// Read the acceleration data.
13+
w := []byte{0x28|0x80}
14+
r := make([]byte, 6)
15+
err := i2c.Tx(0x18, w, r)
16+
```
17+
18+
Here we read 6 bytes starting with register 0x28. We'll get back to why we need to set the highest bit later (through `|0x80`). First let's take a look at the [datasheet](https://cdn-shop.adafruit.com/datasheets/LIS3DH.pdf#page=33) again:
19+
20+
![...](/images/i2c-lis3dh-acceldata.png)
21+
22+
So here we have 6 registers that contain the acceleration data in the 3 directions (X, Y, Z) where each direction takes up two registers. Since each register is 8 bits, and the data itself is 10 bits, two registers are needed for each direction.
23+
24+
Decoding it is a little bit tricky though, if you're not used to bitwise operations:
25+
26+
```go
27+
x := int16(r[0]) | int16(r[1])<<8
28+
y := int16(r[2]) | int16(r[3])<<8
29+
z := int16(r[4]) | int16(r[5])<<8
30+
println("x, y, z:", x, y, z)
31+
````
32+
33+
This combines the two 8 bit values into a single 16-bit signed integer. If we take the X axis for example, the datasheet implies the first register (`OUT_X_L`) is low, since it has "L" in the name. (Yes, it's not the most helpful datasheet in this regard). The next register (`OUT_X_H`) contains the upper bits. So the way to convert them to a single 16-bit value is to convert them both individually to a 16-bit value (which keeps the lower bits but fills the missing bits with zero), then _shift_ the bits of `OUT_X_H` to the high position, and OR them together. This gives us a single 16-bit value, which is the signed X axis.
34+
35+
On the right you can see the values we read from the accelerometer. On a desktop computer you will see simulated values, as if the board is lying flat on a surface. However, if you are on a supported mobile device (phone, table), you will actually be able to see the real acceleration values of your device!
36+
37+
Some things to note:
38+
39+
* **Gravity** is also measured. This is just how physics works: when the sensor is lying still, it shows values as if it was accelerating at *g* speed away from earth. (Unless you're doing this in a zero-gravity environment, of course). There are various algorithms to filter out the gravity component, which will usually be needed for practical applications.
40+
* There is **noise** in the output. Every sensor has noise, this one included. The simulator includes the noise to be more realistic, and if you use such a sensor in a project you need to be aware that such noise exists.
41+
* The sensor can be put on a board in various ways, for example it can either be on top or at the bottom. This will of course impact the measurements.
42+
43+
## What's up with that upper bit?
44+
45+
While for I2C devices you can read multiple sequential registers by writing the register address and then reading multiple values back, the LIS3DH is a bit different. By default it will return the same register on every read. To get the behavior we want, we need to set the highest bit of the register address to one, which gets the sensor to return values incrementally.
46+
47+
<script type="module">
48+
let code = `
49+
package main
50+
51+
import (
52+
"machine"
53+
"time"
54+
)
55+
56+
func main() {
57+
i2c := machine.I2C0
58+
err := i2c.Configure(machine.I2CConfig{
59+
SCL: machine.SCL_PIN,
60+
SDA: machine.SDA_PIN,
61+
})
62+
if err != nil {
63+
println("could not configure I2C:", err.Error())
64+
return
65+
}
66+
67+
err = i2c.Tx(0x18, []byte{0x20, 0b0100_0111}, nil)
68+
if err != nil {
69+
println("got error while configuring the LIS3DH:", err.Error())
70+
return
71+
}
72+
73+
for {
74+
// Read the acceleration data.
75+
w := []byte{0x28|0x80}
76+
r := make([]byte, 6)
77+
err := i2c.Tx(0x18, w, r)
78+
if err != nil {
79+
println("got error while reading values:", err.Error())
80+
} else {
81+
x := int16(r[0]) | int16(r[1])<<8
82+
y := int16(r[2]) | int16(r[3])<<8
83+
z := int16(r[4]) | int16(r[5])<<8
84+
println("x, y, z:", x, y, z)
85+
}
86+
87+
time.Sleep(time.Second)
88+
}
89+
}
90+
`;
91+
import {setupLIS3DH} from "/tour-lis3dh.js";
92+
setupLIS3DH(code);
93+
</script>
52.7 KB
Loading
184 KB
Loading
11.7 KB
Loading

0 commit comments

Comments
 (0)