Skip to content

Commit b5ebe2e

Browse files
committed
Initial add for support of lps2x sensors.
1 parent f009056 commit b5ebe2e

3 files changed

Lines changed: 550 additions & 0 deletions

File tree

lps2x/example_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2026 The Periph Authors. All rights reserved.
2+
// Use of this source code is governed under the Apache License, Version 2.0
3+
// that can be found in the LICENSE file.
4+
5+
package lps2x_test
6+
7+
import (
8+
"fmt"
9+
"log"
10+
"time"
11+
12+
"periph.io/x/conn/v3/i2c/i2creg"
13+
"periph.io/x/conn/v3/physic"
14+
"periph.io/x/devices/v3/lps2x"
15+
"periph.io/x/host/v3"
16+
)
17+
18+
func Example() {
19+
// Make sure periph is initialized.
20+
if _, err := host.Init(); err != nil {
21+
log.Fatal(err)
22+
}
23+
24+
// Use i2creg I²C bus registry to find the first available I²C bus.
25+
b, err := i2creg.Open("")
26+
if err != nil {
27+
log.Fatalf("failed to open I²C: %v", err)
28+
}
29+
defer b.Close()
30+
31+
// Initialize the device.
32+
// Use default address, 25Hz sample rate, and average over 16 readings.
33+
dev, err := lps2x.New(b, lps2x.DefaultAddress, lps2x.SampleRate25Hertz, lps2x.AverageReadings16)
34+
if err != nil {
35+
log.Fatalf("failed to initialize lps2x: %v", err)
36+
}
37+
time.Sleep(time.Second)
38+
39+
// Read environment data.
40+
e := physic.Env{}
41+
if err := dev.Sense(&e); err != nil {
42+
log.Fatal(err)
43+
}
44+
fmt.Printf("%8s %10s\n", e.Temperature, e.Pressure)
45+
}

lps2x/lps2x.go

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
// Copyright 2026 The Periph Authors. All rights reserved.
2+
// Use of this source code is governed under the Apache License, Version 2.0
3+
// that can be found in the LICENSE file.
4+
5+
// This package is driver for the STMicroelectronics LPS series of pressure
6+
// sensors. It supports the LPS22HB, LPS25HB, and LPS28DFW sensors.
7+
//
8+
// # Datasheets
9+
//
10+
// LPS22HB
11+
// https://www.st.com/resource/en/datasheet/lps22hb.pdf
12+
//
13+
// LPS25HB
14+
// https://www.st.com/resource/en/datasheet/lps25hb.pdf
15+
//
16+
// LPS28DFW
17+
// https://www.st.com/resource/en/datasheet/lps28dfw.pdf
18+
package lps2x
19+
20+
import (
21+
"errors"
22+
"fmt"
23+
"sync"
24+
"time"
25+
26+
"periph.io/x/conn/v3"
27+
"periph.io/x/conn/v3/i2c"
28+
"periph.io/x/conn/v3/physic"
29+
)
30+
31+
const (
32+
DefaultAddress i2c.Addr = 0x5c
33+
34+
// The default measuring scale for these devices is HectoPascal, which is
35+
// 100 Pa.
36+
HectoPascal physic.Pressure = 100 * physic.Pascal
37+
38+
// These devices implement an identify command that returns the model ID.
39+
LPS22HB byte = 0xb1
40+
LPS25HB byte = 0xbd
41+
LPS28DFW byte = 0xb4
42+
)
43+
44+
type SampleRate byte
45+
type AverageRate byte
46+
47+
const (
48+
lps22hb = "LPS22HB"
49+
lps25hb = "LPS25HB"
50+
lps28dfw = "LPS28DFW"
51+
52+
cmdWhoAmI = 0x0f
53+
cmdStatus = 0x27
54+
cmdSampleRate = 0x10
55+
cmdResConfLPS25HB = 0x10
56+
cmdSampleRateLPS25HB = 0x20
57+
dataReady byte = 0x03
58+
minTemperature = physic.ZeroCelsius - 40*physic.Kelvin
59+
maxTemperature = physic.ZeroCelsius + 85*physic.Kelvin
60+
61+
minPressure = 260 * HectoPascal
62+
63+
minSampleDuration = time.Microsecond
64+
)
65+
const (
66+
SampleRateOneShot SampleRate = iota
67+
SampleRateHertz
68+
SampleRate4Hertz
69+
SampleRate10Hertz
70+
SampleRate25Hertz
71+
SampleRate50Hertz
72+
SampleRate75Hertz
73+
SampleRate100Hertz
74+
SampleRate200Hertz
75+
)
76+
77+
const (
78+
SampleRateLPS25HBHertz = iota
79+
SampleRateLPS25HB7Hertz
80+
SampleRateLPS25HB12_5Hertz
81+
SampleRateLPS25HB25Hertz
82+
)
83+
84+
const (
85+
AverageNone AverageRate = iota
86+
AverageReadings4
87+
AverageReadings8
88+
AverageReadings16
89+
AverageReadings32
90+
AverageReadings64
91+
AverageReadings128
92+
AverageReadings512
93+
)
94+
95+
var (
96+
sampleRateTimes = []time.Duration{
97+
0,
98+
time.Second,
99+
time.Second / 4,
100+
time.Second / 10,
101+
time.Second / 25,
102+
time.Second / 50,
103+
time.Second / 75,
104+
time.Second / 100,
105+
time.Second / 200,
106+
}
107+
averageMultiple = []int{
108+
1,
109+
4,
110+
8,
111+
16,
112+
32,
113+
64,
114+
128,
115+
512,
116+
}
117+
)
118+
119+
type Dev struct {
120+
conn conn.Conn
121+
mu sync.Mutex
122+
shutdown chan struct{}
123+
deviceID byte
124+
fsMode byte
125+
sampleRate SampleRate
126+
averageReadings AverageRate
127+
}
128+
129+
// New creates a new LPS2x device on the specified I²C bus.
130+
// addr is the I²C address (typically DefaultAddress or AlternateAddress).
131+
// sampleRate controls measurement frequency, averageReadings controls internal averaging.
132+
func New(bus i2c.Bus, address i2c.Addr, sampleRate SampleRate, averageRate AverageRate) (*Dev, error) {
133+
dev := &Dev{conn: &i2c.Dev{Bus: bus, Addr: uint16(address)}, sampleRate: sampleRate, averageReadings: averageRate}
134+
135+
return dev, dev.start()
136+
}
137+
138+
// start does an i2c transaction to read the device id and returns the error
139+
// if any.
140+
func (dev *Dev) start() error {
141+
142+
r := []byte{0}
143+
err := dev.conn.Tx([]byte{cmdWhoAmI}, r)
144+
if err != nil {
145+
return err
146+
}
147+
148+
dev.deviceID = r[0]
149+
if err == nil {
150+
if dev.deviceID == LPS25HB {
151+
// There are some key differences for this model. In this case, the Average Rate
152+
// is in the 0x10 register, and the sample rate is in the 0x20 register.
153+
// Also, the lps25hb supports different sample rates than other members of the
154+
// family.
155+
if dev.sampleRate > SampleRate25Hertz {
156+
return fmt.Errorf("lps2x: invalid sample rate %d, max: %d", dev.sampleRate, SampleRate25Hertz)
157+
}
158+
var tAvg, pAvg byte
159+
switch dev.averageReadings {
160+
case AverageReadings4:
161+
case AverageReadings8:
162+
// the default 0 value is correct.
163+
case AverageReadings16:
164+
tAvg = 1
165+
pAvg = 1
166+
case AverageReadings32:
167+
tAvg = 1
168+
pAvg = 2
169+
case AverageReadings64:
170+
tAvg = 1
171+
pAvg = 3
172+
case AverageReadings128:
173+
tAvg = 2
174+
pAvg = 3
175+
case AverageReadings512:
176+
tAvg = 3
177+
pAvg = 3
178+
}
179+
180+
err = dev.conn.Tx([]byte{cmdResConfLPS25HB, tAvg<<2 | pAvg}, nil)
181+
if err != nil {
182+
err = fmt.Errorf("lps2x: error setting average rates %w", err)
183+
} else {
184+
odr := byte(0x80 | (dev.sampleRate << 4))
185+
err = dev.conn.Tx([]byte{cmdSampleRateLPS25HB, odr}, nil)
186+
}
187+
188+
} else {
189+
err = dev.conn.Tx([]byte{cmdSampleRate, byte(dev.sampleRate<<3) | byte(dev.averageReadings)}, nil)
190+
}
191+
}
192+
return err
193+
}
194+
195+
func (dev *Dev) Halt() error {
196+
dev.mu.Lock()
197+
defer dev.mu.Unlock()
198+
if dev.shutdown != nil {
199+
close(dev.shutdown)
200+
}
201+
return nil
202+
}
203+
204+
func (dev *Dev) Precision(env *physic.Env) {
205+
env.Humidity = 0
206+
env.Temperature = physic.Kelvin / 100
207+
env.Pressure = HectoPascal
208+
}
209+
210+
func (dev *Dev) Sense(env *physic.Env) error {
211+
env.Humidity = 0
212+
213+
// We're reading the status byte, and the following 5 bytes: 3 bytes of
214+
// pressure data, and 2 temperature bytes.
215+
w := []byte{cmdStatus}
216+
r := make([]byte, 6)
217+
218+
err := dev.conn.Tx(w, r)
219+
if err != nil {
220+
env.Temperature = minTemperature
221+
env.Pressure = minPressure
222+
return fmt.Errorf("lps2x: error reading device %w", err)
223+
}
224+
if r[0]&dataReady != dataReady {
225+
env.Temperature = minTemperature
226+
env.Pressure = minPressure
227+
return errors.New("lps2x: data not ready, was sampling started?")
228+
}
229+
230+
env.Temperature = dev.countToTemp(int16(r[5])<<8 | int16(r[4]))
231+
env.Pressure = dev.countToPressure(convert24BitTo64Bit(r[1:4]))
232+
return nil
233+
}
234+
235+
func (dev *Dev) SenseContinuous(interval time.Duration) (<-chan physic.Env, error) {
236+
d := sampleRateTimes[dev.sampleRate]
237+
d *= time.Duration(averageMultiple[dev.averageReadings])
238+
if interval < d {
239+
return nil, fmt.Errorf("Invalid duration. Minimum Duration: %v", d)
240+
}
241+
dev.mu.Lock()
242+
if dev.shutdown != nil {
243+
dev.mu.Unlock()
244+
return nil, errors.New("lps2x: SenseContinuous already running")
245+
}
246+
dev.mu.Unlock()
247+
248+
if interval < minSampleDuration {
249+
// TODO: Verify
250+
return nil, errors.New("lps2x: sample interval is < device sample rate")
251+
}
252+
dev.shutdown = make(chan struct{})
253+
ch := make(chan physic.Env, 16)
254+
go func(ch chan<- physic.Env) {
255+
ticker := time.NewTicker(interval)
256+
defer ticker.Stop()
257+
defer close(ch)
258+
for {
259+
select {
260+
case <-dev.shutdown:
261+
dev.mu.Lock()
262+
defer dev.mu.Unlock()
263+
dev.shutdown = nil
264+
return
265+
case <-ticker.C:
266+
env := physic.Env{}
267+
if err := dev.Sense(&env); err == nil {
268+
ch <- env
269+
}
270+
}
271+
}
272+
}(ch)
273+
return ch, nil
274+
}
275+
276+
// String returns the device model name.
277+
func (dev *Dev) String() string {
278+
switch dev.deviceID {
279+
case LPS22HB:
280+
return lps22hb
281+
case LPS25HB:
282+
return lps25hb
283+
case LPS28DFW:
284+
return lps28dfw
285+
default:
286+
return "unknown"
287+
}
288+
}
289+
290+
func convert24BitTo64Bit(bytes []byte) int64 {
291+
// Mask to isolate the lower 24 bits (0x00FFFFFF)
292+
// This ensures we only consider the 24-bit value if it was derived from a larger type
293+
val := uint32(bytes[0]) | uint32(bytes[1])<<8 | uint32(bytes[2])<<16
294+
295+
// Check if the 24th bit (the sign bit) is set (0x00800000)
296+
if (val & 0x00800000) != 0 {
297+
// If the sign bit is set, it's a negative number.
298+
// Sign-extend by filling the upper 8 bits with ones (0xFF000000).
299+
val |= 0xFF000000
300+
}
301+
302+
return int64(val)
303+
}
304+
305+
func (dev *Dev) countToTemp(count int16) physic.Temperature {
306+
temp := physic.Temperature(count)*10*physic.MilliKelvin + physic.ZeroCelsius
307+
if temp < minTemperature {
308+
temp = minTemperature
309+
} else if temp > maxTemperature {
310+
temp = maxTemperature
311+
}
312+
return temp
313+
}
314+
315+
func (dev *Dev) countToPressure(count int64) physic.Pressure {
316+
if dev.fsMode == 0 {
317+
return (physic.Pressure(count) * HectoPascal) / 4096
318+
}
319+
return (physic.Pressure(count) * HectoPascal) / 2048
320+
}
321+
322+
var _ conn.Resource = &Dev{}
323+
var _ physic.SenseEnv = &Dev{}

0 commit comments

Comments
 (0)