tinygo.org/x/drivers@v0.27.1-0.20240509133757-7dbca2a54349/ndir/ndir.go (about) 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 }