Skip to content

Commit 06f7da7

Browse files
committed
gpio(hx711): add driver for 24 bit ADC e.g. measure with load cells
1 parent 4747884 commit 06f7da7

File tree

6 files changed

+749
-3
lines changed

6 files changed

+749
-3
lines changed

drivers/gpio/hx711_driver.go

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package gpio
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"tinygo.org/x/drivers"
8+
"tinygo.org/x/drivers/hx711"
9+
10+
gobot "gobot.io/x/gobot/v2"
11+
"gobot.io/x/gobot/v2/system"
12+
)
13+
14+
const (
15+
defaultGainAndChannelCfg = hx711.A128
16+
defaultReadTimeout = 2 * time.Second
17+
defaultReadTriesMax = 100
18+
defaultTickSleep = 0 * time.Microsecond // 0..50 us, depending on CPU performance and scheduling
19+
)
20+
21+
// hx711OptionApplier needs to be implemented by each configurable option type
22+
type hx711OptionApplier interface {
23+
apply(cfg *hx711Configuration)
24+
}
25+
26+
// hx711Configuration contains all changeable attributes of the driver.
27+
type hx711Configuration struct {
28+
gainAndChannelCfg hx711.GainAndChannelCfg
29+
readTimeout time.Duration
30+
readTriesMax uint8 // how often a check for ready state is done until the read timeout is reached
31+
tickSleep time.Duration
32+
}
33+
34+
// hx711GainAndChannelCfgOption is the type for applying another gain and channel configuration.
35+
type hx711GainAndChannelCfgOption hx711.GainAndChannelCfg
36+
37+
// hx711ReadTimeoutOption is the type for applying another duration for the read timeout.
38+
type hx711ReadTimeoutOption time.Duration
39+
40+
// hx711ReadTriesMaxOption is the type for applying another value for maximum read tries.
41+
type hx711ReadTriesMaxOption uint8
42+
43+
// hx711TickSleepOption is the type for applying another duration for the tick H-level-time.
44+
type hx711TickSleepOption time.Duration
45+
46+
type sensorer interface {
47+
Configure(cfg *hx711.DeviceConfig) error
48+
Zero(secondReading bool) error
49+
Calibrate(setValue float32, secondReading bool) error
50+
Update(m drivers.Measurement) error
51+
Values() (float32, float32)
52+
OffsetAndCalibrationFactor(secondReading bool) (int32, float32)
53+
SetOffsetAndCalibrationFactor(offset int32, calibrationFactor float32, secondReading bool)
54+
}
55+
56+
// HX711 is the gobot driver for the HX711 chip.
57+
type HX711Driver struct {
58+
*driver
59+
60+
hx711Cfg *hx711Configuration
61+
newSensor func(clockPin, dataPin gobot.DigitalPinner) sensorer
62+
sensor sensorer
63+
}
64+
65+
// NewHX711 creates a new driver for HX711 24 bit, 2 channel, configurable ADC with serial output to measure small
66+
// differential voltages. The device is handy for load cells but can be used to read all kind of Wheatstone bridges.
67+
// Therefore the usage of the phrases "mass", "weight" or "load" are prevented in this driver - "value" is used instead.
68+
// The implementation just wraps the according TinyGo-driver.
69+
//
70+
// Datasheet: https://cdn.sparkfun.com/datasheets/Sensors/ForceFlex/hx711_english.pdf
71+
//
72+
// Supported options:
73+
//
74+
// "WithName"
75+
// "WithHX711GainAndChannelCfg"
76+
// "WithHX711ReadTimeout"
77+
// "WithHX711ReadTriesMax"
78+
// "WithHX711TickSleep"
79+
func NewHX711Driver(a gobot.Connection, clockPinID, dataPinID string, opts ...interface{}) *HX711Driver {
80+
d := HX711Driver{
81+
driver: newDriver(a, "HX711"),
82+
hx711Cfg: &hx711Configuration{
83+
gainAndChannelCfg: defaultGainAndChannelCfg,
84+
readTimeout: defaultReadTimeout,
85+
readTriesMax: defaultReadTriesMax,
86+
tickSleep: defaultTickSleep,
87+
},
88+
}
89+
90+
for _, opt := range opts {
91+
switch o := opt.(type) {
92+
case optionApplier:
93+
o.apply(d.driverCfg)
94+
case hx711OptionApplier:
95+
o.apply(d.hx711Cfg)
96+
default:
97+
panic(fmt.Sprintf("'%s' can not be applied on '%s'", opt, d.driverCfg.name))
98+
}
99+
}
100+
101+
// TODO: sets the channels from constructor or by "With" (default is A128)
102+
// preparation in this way opens the possibility to change the sensor for unit tests before Connect()
103+
d.newSensor = func(clockPin, dataPin gobot.DigitalPinner) sensorer {
104+
clockPinSet := func(v bool) error {
105+
if v {
106+
return clockPin.Write(1)
107+
}
108+
109+
return clockPin.Write(0)
110+
}
111+
112+
dataPinGet := func() (bool, error) {
113+
val, err := dataPin.Read()
114+
115+
return val > 0, err
116+
}
117+
118+
return hx711.New(clockPinSet, dataPinGet, d.hx711Cfg.gainAndChannelCfg)
119+
}
120+
121+
// note: Unexport() of all pins will be done on adaptor.Finalize()
122+
d.afterStart = func() error {
123+
clockPin, err := a.(gobot.DigitalPinnerProvider).DigitalPin(clockPinID) //nolint:forcetypeassert // ok here
124+
if err != nil {
125+
return fmt.Errorf("error on get clock pin: %v", err)
126+
}
127+
if err := clockPin.ApplyOptions(system.WithPinDirectionOutput(0)); err != nil {
128+
return fmt.Errorf("error on apply output for trigger pin: %v", err)
129+
}
130+
131+
// pins are inputs by default
132+
dataPin, err := a.(gobot.DigitalPinnerProvider).DigitalPin(dataPinID) //nolint:forcetypeassert // ok here
133+
if err != nil {
134+
return fmt.Errorf("error on get data pin: %v", err)
135+
}
136+
137+
if err := dataPin.ApplyOptions(system.WithPinPullUp()); err != nil {
138+
return fmt.Errorf("error on apply pull up option for data pin: %v", err)
139+
}
140+
141+
d.sensor = d.newSensor(clockPin, dataPin)
142+
143+
cfg := hx711.DefaultConfig
144+
cfg.ReadTimeout = d.hx711Cfg.readTimeout
145+
cfg.ReadTriesMax = d.hx711Cfg.readTriesMax
146+
cfg.TickSleep = d.hx711Cfg.tickSleep
147+
148+
// this leads to an active reset of the sensor hardware, so pins must be ready at this point
149+
return d.sensor.Configure(&cfg)
150+
}
151+
152+
return &d
153+
}
154+
155+
// WithHX711GainAndChannelCfg use the given value for the gain and channel configuration instead the default hx711.A128.
156+
func WithHX711GainAndChannelCfg(gc hx711.GainAndChannelCfg) hx711OptionApplier {
157+
return hx711GainAndChannelCfgOption(gc)
158+
}
159+
160+
// WithHX711ReadTimeout use the given duration for read timeout instead the default of 2 seconds.
161+
func WithHX711ReadTimeout(timeout time.Duration) hx711OptionApplier {
162+
return hx711ReadTimeoutOption(timeout)
163+
}
164+
165+
// WithHX711ReadTriesMax use the given value for maximum read tries instead the default of 100. A value of zero will be
166+
// automatically adjusted to the minimum of 1 try by the underlying sensor driver.
167+
func WithHX711ReadTriesMax(tries uint8) hx711OptionApplier {
168+
return hx711ReadTriesMaxOption(tries)
169+
}
170+
171+
// WithHX711TickSleep use the given duration for the H-level of the clock tick. The default is set to 0, which performs
172+
// best for most CPUs. The value must be smaller than 60us, otherwise a hardware reset on each measure will occur. This
173+
// will be automatically adjusted by the underlying sensor driver to this maximum, but the CPU will not rely on that
174+
// value in any case, depending on its performance and scheduling. E.g. on a tinkerboard a value of 1us leads to a
175+
// real duration of H-level between 10 and 70us, which causes wrong measures with high occurrence.
176+
func WithHX711TickSleep(tickHLevelDuration time.Duration) hx711OptionApplier {
177+
return hx711TickSleepOption(tickHLevelDuration)
178+
}
179+
180+
// Zero sets the offset for the reading. If the given flag is true, this is done for the second reading.
181+
func (d *HX711Driver) Zero(secondReading bool) error {
182+
// locking concurrent calls is done at sensor side
183+
return d.sensor.Zero(secondReading)
184+
}
185+
186+
// Calibrate calculates, after a measurement of the set value is done, a factor for linear scaling the values of the
187+
// subsequent measurements. The unit of the given set value define the unit of the measurement result later. Before
188+
// using this function, the offset value should be obtained by calling Zero() function with no load.
189+
// If the given flag is true, this is done for the second reading.
190+
func (d *HX711Driver) Calibrate(setValue float32, secondReading bool) error {
191+
// locking concurrent calls is done at sensor side
192+
return d.sensor.Calibrate(setValue, secondReading)
193+
}
194+
195+
// OffsetAndCalibrationFactor returns linear correction values, used for reading.
196+
// If the given flag is true, this values are related to the second reading.
197+
func (d *HX711Driver) OffsetAndCalibrationFactor(secondReading bool) (int32, float32) {
198+
// locking concurrent calls is done at sensor side
199+
return d.sensor.OffsetAndCalibrationFactor(secondReading)
200+
}
201+
202+
// SetOffsetAndCalibrationFactor sets linear correction values, used for reading.
203+
// If the given flag is true, this values are related to the second reading.
204+
func (d *HX711Driver) SetOffsetAndCalibrationFactor(offset int32, calibrationFactor float32, secondReading bool) {
205+
// locking concurrent calls is done at sensor side
206+
d.sensor.SetOffsetAndCalibrationFactor(offset, calibrationFactor, secondReading)
207+
}
208+
209+
// Measure retrieves the values of the sensor and returns the measure. If only channel A is configured, channel B just
210+
// returns always zero. Because the timing is somewhat difficult on CPUs regarding the low time of the clock pin (>60us
211+
// leads to a reset of the hardware), it is suggested to implement a validation for the values and drop or repeat the
212+
// measure in case of a value is unexpected.
213+
func (d *HX711Driver) Measure() (float32, float32, error) {
214+
// locking concurrent calls is done at sensor side
215+
if err := d.sensor.Update(0); err != nil {
216+
return 0, 0, err
217+
}
218+
219+
v1, v2 := d.sensor.Values()
220+
221+
return v1, v2, nil
222+
}
223+
224+
func (o hx711GainAndChannelCfgOption) String() string {
225+
return "hx711 gain and channel configuration option"
226+
}
227+
228+
func (o hx711GainAndChannelCfgOption) apply(cfg *hx711Configuration) {
229+
cfg.gainAndChannelCfg = hx711.GainAndChannelCfg(o)
230+
}
231+
232+
func (o hx711ReadTimeoutOption) String() string {
233+
return "hx711 read timeout option"
234+
}
235+
236+
func (o hx711ReadTimeoutOption) apply(cfg *hx711Configuration) {
237+
cfg.readTimeout = time.Duration(o)
238+
}
239+
240+
func (o hx711ReadTriesMaxOption) String() string {
241+
return "hx711 maximum read tries option"
242+
}
243+
244+
func (o hx711ReadTriesMaxOption) apply(cfg *hx711Configuration) {
245+
cfg.readTriesMax = uint8(o)
246+
}
247+
248+
func (o hx711TickSleepOption) String() string {
249+
return "hx711 tick sleep option"
250+
}
251+
252+
func (o hx711TickSleepOption) apply(cfg *hx711Configuration) {
253+
cfg.tickSleep = time.Duration(o)
254+
}

0 commit comments

Comments
 (0)