Skip to content

Commit a452275

Browse files
authored
add Sandbox Electronics NDIR CO2 sensor driver (2) (#580)
add sandbox electronics NDIR CO2 sensor
1 parent 4f789fb commit a452275

File tree

4 files changed

+336
-1
lines changed

4 files changed

+336
-1
lines changed

examples/ndir/main_ndir.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import (
4+
"machine"
5+
"time"
6+
7+
"tinygo.org/x/drivers"
8+
"tinygo.org/x/drivers/ndir"
9+
)
10+
11+
var (
12+
ndirBus = machine.I2C0
13+
)
14+
15+
func main() {
16+
err := ndirBus.Configure(machine.I2CConfig{
17+
Frequency: 100_000,
18+
})
19+
if err != nil {
20+
panic("i2c config fail:" + err.Error())
21+
}
22+
// Set the address based on how the resistors are soldered.
23+
// True means the left and middle pads are joined.
24+
ndirAddr := ndir.Addr(true, false)
25+
dev := ndir.NewDevI2C(ndirBus, ndirAddr)
26+
err = dev.Init()
27+
if err != nil {
28+
panic("ndir init fail:" + err.Error())
29+
}
30+
// Datasheet tells us to wait 12 seconds before reading from the sensor.
31+
time.Sleep(12 * time.Second)
32+
for {
33+
time.Sleep(time.Second)
34+
err := dev.Update(drivers.AllMeasurements)
35+
if err != nil {
36+
println(err.Error())
37+
continue
38+
}
39+
println("PPM:", dev.PPMCO2())
40+
}
41+
}

ndir/ndir.go

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package ndir
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"runtime"
7+
"time"
8+
9+
"tinygo.org/x/drivers"
10+
)
11+
12+
// Addr returns the I2C address given the solder pad configuration on the Sandbox Electronics i2c/uart converter.
13+
// When the resistor is connected between the left and middle pads the bit is said to be set
14+
// and a0 or a1 should be passed in as true.
15+
func Addr(a0, a1 bool) uint8 {
16+
return 0b1001000 | b2u8(a0) | b2u8(a1)<<2
17+
}
18+
19+
func b2u8(b bool) uint8 {
20+
if b {
21+
return 1
22+
}
23+
return 0
24+
}
25+
26+
// See https://github.com/SandboxElectronics/NDIR/blob/master/NDIR_I2C/NDIR_I2C.cpp
27+
28+
// General Registers
29+
const (
30+
addrRHR = 0x00
31+
addrTHR = 0x00
32+
addrIER = 0x01
33+
addrFCR = 0x02
34+
addrIIR = 0x02
35+
addrLCR = 0x03
36+
addrMCR = 0x04
37+
addrLSR = 0x05
38+
addrMSR = 0x06
39+
addrSPR = 0x07
40+
addrTCR = 0x06
41+
addrTLR = 0x07
42+
addrTXLVL = 0x08
43+
addrRXLVL = 0x09
44+
addrIODIR = 0x0A
45+
addrIOSTATE = 0x0B
46+
addrIOINTENA = 0x0C
47+
addrIOCONTROL = 0x0E // This addr fails on write of 0x08?
48+
addrEFCR = 0x0F
49+
)
50+
51+
// Special registers
52+
const (
53+
addrDLL = 0x00
54+
addrDLH = 1
55+
)
56+
57+
const (
58+
shortTxCooldown = time.Millisecond
59+
longTxCooldown = 10 * time.Millisecond
60+
rxTimeout = 100 * time.Millisecond
61+
)
62+
63+
var (
64+
cmd_readCO2 = [...]byte{0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}
65+
cmd_measure = [...]byte{0xFF, 0x01, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63}
66+
cmd_calibrateZero = [...]byte{0xFF, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78}
67+
cmd_enableAutoCalibration = [...]byte{0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0xE6}
68+
cmd_disableAutoCalibration = [...]byte{0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86}
69+
)
70+
71+
// DevI2C is a handle to a MH-Z16 NDIR CO2 Sensor using the I2C interface.
72+
type DevI2C struct {
73+
bus drivers.I2C
74+
addr uint8
75+
nextAvail time.Time
76+
initTime time.Time
77+
lastMeasurement int32
78+
}
79+
80+
// NewDevI2C returns a new NDIR device ready for use. It performs no I/O.
81+
func NewDevI2C(bus drivers.I2C, addr uint8) *DevI2C {
82+
return &DevI2C{
83+
bus: bus,
84+
addr: addr,
85+
lastMeasurement: -1,
86+
}
87+
}
88+
89+
// PPM returns the CO2 parts per million read in the last Update call.
90+
func (d *DevI2C) PPMCO2() int32 {
91+
return d.lastMeasurement
92+
}
93+
94+
var errInitWait = errors.New("ndir: must wait 12 seconds after init before reading concentration")
95+
96+
// Update reads the CO2 concentration from the NDIR and stores it ready for the
97+
// PPM() method.
98+
func (d *DevI2C) Update(which drivers.Measurement) (err error) {
99+
if which&drivers.Concentration == 0 {
100+
return nil // NDIR only measures concentration, so nothing to do here.
101+
}
102+
if time.Since(d.initTime) < 12*time.Second {
103+
// Wait 12 seconds before performing first read.
104+
return nil
105+
}
106+
err = d.writeRegister(addrFCR, 0x07)
107+
if err != nil {
108+
return err
109+
}
110+
err = d.send(cmd_measure[:])
111+
if err != nil {
112+
return fmt.Errorf("sending cmd_measure: %w", err)
113+
}
114+
time.Sleep(11 * time.Millisecond)
115+
var buf [9]byte
116+
buf, err = d.receive()
117+
118+
if err != nil {
119+
return fmt.Errorf("receiving during measure: %w", err)
120+
}
121+
if buf[0] != 0xff && buf[1] != 0x9c {
122+
return fmt.Errorf("buffer rx bad values: %q", string(buf[:]))
123+
}
124+
var sum uint16
125+
for i := 0; i < len(buf); i++ {
126+
sum += uint16(buf[i])
127+
}
128+
mod := sum % 256
129+
if mod != 0xff {
130+
return fmt.Errorf("ndir checksum modulus got %#x, expected 0xff", mod)
131+
}
132+
ppm := uint32(buf[2])<<24 | uint32(buf[3])<<16 | uint32(buf[4])<<8 | uint32(buf[5])
133+
d.lastMeasurement = int32(ppm)
134+
return nil
135+
}
136+
137+
func (d *DevI2C) Init() (err error) {
138+
// AddrIOCONTROL write is always NACKed so ignore
139+
// error here.
140+
d.writeRegister(addrIOCONTROL, 0x08)
141+
142+
err = d.writeRegister(addrFCR, 0x07)
143+
if err != nil {
144+
return err
145+
}
146+
err = d.writeRegister(addrLCR, 0x83)
147+
if err != nil {
148+
return err
149+
}
150+
err = d.writeRegister(addrDLL, 0x60)
151+
if err != nil {
152+
return err
153+
}
154+
err = d.writeRegister(addrDLH, 0x00)
155+
if err != nil {
156+
return err
157+
}
158+
err = d.writeRegister(addrLCR, 0x03)
159+
if err != nil {
160+
return err
161+
}
162+
d.initTime = time.Now()
163+
return nil
164+
}
165+
166+
// CalibrateZero calibrates the NDIR to around 412ppm.
167+
func (d *DevI2C) CalibrateZero() error {
168+
return d.enactCommand(cmd_calibrateZero[:])
169+
}
170+
171+
// SetAutoCalibration can enable or disable the NDIR's auto calibration mode.
172+
func (d *DevI2C) SetAutoCalibration(enable bool) (err error) {
173+
if enable {
174+
err = d.enactCommand(cmd_enableAutoCalibration[:])
175+
} else {
176+
err = d.enactCommand(cmd_disableAutoCalibration[:])
177+
}
178+
return err
179+
}
180+
181+
func (d *DevI2C) send(cmd []byte) error {
182+
txlvl, err := d.ReadRegister(addrTXLVL)
183+
if err != nil {
184+
return err
185+
}
186+
if int(txlvl) < len(cmd) {
187+
return fmt.Errorf("txlvl=%d less than length of command %d", txlvl, len(cmd))
188+
}
189+
return d.tx(append([]byte{addrTHR}, cmd...), nil)
190+
}
191+
192+
func (d *DevI2C) receive() (cmd [9]byte, err error) {
193+
start := time.Now()
194+
n := uint8(9)
195+
for n > 0 {
196+
if time.Since(start) > rxTimeout {
197+
return [9]byte{}, errors.New("NDIR rx timeout")
198+
}
199+
rxlvl, err := d.ReadRegister(addrRXLVL)
200+
if err != nil {
201+
return [9]byte{}, err
202+
}
203+
if rxlvl > n {
204+
rxlvl = n
205+
}
206+
ptr := 9 - n
207+
err = d.tx([]byte{addrRHR << 3}, cmd[ptr:ptr+rxlvl])
208+
n -= rxlvl
209+
if err != nil {
210+
return [9]byte{}, err
211+
}
212+
}
213+
return cmd, nil
214+
}
215+
216+
func (d *DevI2C) enactCommand(cmd []byte) error {
217+
if len(cmd) > 31 {
218+
return errors.New("ndir: command too long")
219+
}
220+
// Most commands always start with the same FCR write here.
221+
err := d.writeRegister(addrFCR, 0x07)
222+
if err != nil {
223+
return err
224+
}
225+
time.Sleep(longTxCooldown)
226+
227+
// C++ send method begins here.
228+
got, err := d.ReadRegister(addrTXLVL)
229+
if err != nil {
230+
return err
231+
}
232+
if got < uint8(len(cmd)) {
233+
return fmt.Errorf("ndir: txlevel=%d too low for command of length %d", got, len(cmd))
234+
}
235+
var buf [32]byte
236+
buf[0] = addrTHR
237+
n := 1 + copy(buf[1:], cmd)
238+
err = d.tx(buf[:n], nil)
239+
if err != nil {
240+
return err
241+
}
242+
d.nextAvail.Add(longTxCooldown) // add some extra time.
243+
return nil
244+
}
245+
246+
func (d *DevI2C) writeRegister(addr, val uint8) (err error) {
247+
return d.WriteRegisters(addr, []byte{val})
248+
}
249+
250+
func (d *DevI2C) WriteRegisters(addr uint8, vals []byte) (err error) {
251+
var buf [32]byte
252+
if len(vals) > 31 {
253+
return errors.New("can only write up to 31 bytes")
254+
}
255+
buf[0] = addr << 3
256+
n := copy(buf[1:], vals)
257+
err = d.tx(buf[:n+1], nil)
258+
if err != nil {
259+
err = fmt.Errorf("NDIR write %#x (%d) to %#x: %w", buf[1], len(vals), buf[0], err)
260+
}
261+
return err
262+
}
263+
264+
func (d *DevI2C) ReadRegister(addr uint8) (uint8, error) {
265+
var buf [2]byte
266+
buf[0] = addr << 3
267+
err := d.tx(buf[:1], buf[1:2])
268+
if err != nil {
269+
err = fmt.Errorf("NDIR read from %#x: %w", buf[0], err)
270+
}
271+
return buf[1], err
272+
}
273+
274+
func (d *DevI2C) tx(w, r []byte) error {
275+
wait := time.Until(d.nextAvail)
276+
if wait > 0 {
277+
// Try yielding process first, maybe there's a short time to wait and a schedule call is enough delay.
278+
runtime.Gosched()
279+
wait = time.Until(d.nextAvail)
280+
if wait > 0 {
281+
// If yielding did not work then perform sleep
282+
time.Sleep(wait)
283+
}
284+
}
285+
err := d.bus.Tx(uint16(d.addr), w, r)
286+
d.nextAvail = time.Now().Add(shortTxCooldown)
287+
return err
288+
}

sensor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const (
1616
MagneticField
1717
Luminosity
1818
Time
19+
// Gas or liquid concentration, usually measured in ppm (parts per million).
20+
Concentration
1921
// Add Measurements above AllMeasurements.
2022

2123
// AllMeasurements is the OR of all Measurement values. It ensures all measurements are done.

smoketest.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# avoid a race condition between writing the output and reading the result to
55
# get an md5sum).
66

7+
78
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/adt7410/main.go
89
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/adxl345/main.go
910
tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/amg88xx
@@ -127,4 +128,7 @@ tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/d
127128
tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/lora/lorawan/atcmd/
128129
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/as560x/main.go
129130
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mpu6886/main.go
130-
tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ttp229/main.go
131+
tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ttp229/main.go
132+
tinygo build -size short -o ./build/test.hex -target=pico ./examples/ndir/main_ndir.go
133+
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ndir/main_ndir.go
134+
tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ndir/main_ndir.go

0 commit comments

Comments
 (0)