Skip to content

Commit 930a57a

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

File tree

8 files changed

+750
-3
lines changed

8 files changed

+750
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ the `gobot/drivers/gpio` package:
335335
- Grove Touch Sensor (by using driver for Button)
336336
- HC-SR04 Ultrasonic Ranging Module
337337
- HD44780 LCD controller
338+
- HX711 24 bit ADC e.g. used for weight cells
338339
- LED
339340
- Makey Button (by using driver for Button)
340341
- MAX7219 LED Dot Matrix

drivers/gpio/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Gobot has a extensible system for connecting to hardware devices. The following
2525
- Grove Touch Sensor (by using driver for Button)
2626
- HC-SR04 Ultrasonic Ranging Module
2727
- HD44780 LCD controller
28+
- HX711 24 bit ADC e.g. used for weight cells
2829
- LED
2930
- Makey Button (by using driver for Button)
3031
- MAX7219 LED Dot Matrix

drivers/gpio/hx711_driver.go

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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+
// preparation in this way opens the possibility to change the sensor for unit tests before Connect()
102+
d.newSensor = func(clockPin, dataPin gobot.DigitalPinner) sensorer {
103+
clockPinSet := func(v bool) error {
104+
if v {
105+
return clockPin.Write(1)
106+
}
107+
108+
return clockPin.Write(0)
109+
}
110+
111+
dataPinGet := func() (bool, error) {
112+
val, err := dataPin.Read()
113+
114+
return val > 0, err
115+
}
116+
117+
return hx711.New(clockPinSet, dataPinGet, d.hx711Cfg.gainAndChannelCfg)
118+
}
119+
120+
// note: Unexport() of all pins will be done on adaptor.Finalize()
121+
d.afterStart = func() error {
122+
clockPin, err := a.(gobot.DigitalPinnerProvider).DigitalPin(clockPinID) //nolint:forcetypeassert // ok here
123+
if err != nil {
124+
return fmt.Errorf("error on get clock pin: %v", err)
125+
}
126+
if err := clockPin.ApplyOptions(system.WithPinDirectionOutput(0)); err != nil {
127+
return fmt.Errorf("error on apply output for clock pin: %v", err)
128+
}
129+
130+
// pins are inputs by default
131+
dataPin, err := a.(gobot.DigitalPinnerProvider).DigitalPin(dataPinID) //nolint:forcetypeassert // ok here
132+
if err != nil {
133+
return fmt.Errorf("error on get data pin: %v", err)
134+
}
135+
136+
if err := dataPin.ApplyOptions(system.WithPinPullUp()); err != nil {
137+
return fmt.Errorf("error on apply pull up option for data pin: %v", err)
138+
}
139+
140+
d.sensor = d.newSensor(clockPin, dataPin)
141+
142+
cfg := hx711.DefaultConfig
143+
cfg.ReadTimeout = d.hx711Cfg.readTimeout
144+
cfg.ReadTriesMax = d.hx711Cfg.readTriesMax
145+
cfg.TickSleep = d.hx711Cfg.tickSleep
146+
147+
// this leads to an active reset of the sensor hardware, so pins must be ready at this point
148+
return d.sensor.Configure(&cfg)
149+
}
150+
151+
return &d
152+
}
153+
154+
// WithHX711GainAndChannelCfg use the given value for the gain and channel configuration instead the default hx711.A128.
155+
func WithHX711GainAndChannelCfg(gc hx711.GainAndChannelCfg) hx711OptionApplier {
156+
return hx711GainAndChannelCfgOption(gc)
157+
}
158+
159+
// WithHX711ReadTimeout use the given duration for read timeout instead the default of 2 seconds.
160+
func WithHX711ReadTimeout(timeout time.Duration) hx711OptionApplier {
161+
return hx711ReadTimeoutOption(timeout)
162+
}
163+
164+
// WithHX711ReadTriesMax use the given value for maximum read tries instead the default of 100. A value of zero will be
165+
// automatically adjusted to the minimum of 1 try by the underlying sensor driver.
166+
func WithHX711ReadTriesMax(tries uint8) hx711OptionApplier {
167+
return hx711ReadTriesMaxOption(tries)
168+
}
169+
170+
// WithHX711TickSleep use the given duration for the H-level of the clock tick. The default is set to 0, which performs
171+
// best for most CPUs. The value must be smaller than 60us, otherwise a hardware reset on each measure will occur. This
172+
// will be automatically adjusted by the underlying sensor driver to this maximum, but the CPU will not rely on that
173+
// value in any case, depending on its performance and scheduling. E.g. on a tinkerboard a value of 1us leads to a
174+
// real duration of H-level between 10 and 70us, which causes wrong measures with high occurrence.
175+
func WithHX711TickSleep(tickHLevelDuration time.Duration) hx711OptionApplier {
176+
return hx711TickSleepOption(tickHLevelDuration)
177+
}
178+
179+
// Zero sets the offset for the reading. If the given flag is true, this is done for the second reading.
180+
func (d *HX711Driver) Zero(secondReading bool) error {
181+
// locking concurrent calls is done at sensor side
182+
return d.sensor.Zero(secondReading)
183+
}
184+
185+
// Calibrate calculates, after a measurement of the set value is done, a factor for linear scaling the values of the
186+
// subsequent measurements. The unit of the given set value define the unit of the measurement result later. Before
187+
// using this function, the offset value should be obtained by calling Zero() function with no load.
188+
// If the given flag is true, this is done for the second reading.
189+
func (d *HX711Driver) Calibrate(setValue float32, secondReading bool) error {
190+
// locking concurrent calls is done at sensor side
191+
return d.sensor.Calibrate(setValue, secondReading)
192+
}
193+
194+
// OffsetAndCalibrationFactor returns linear correction values, used for reading.
195+
// If the given flag is true, this values are related to the second reading.
196+
func (d *HX711Driver) OffsetAndCalibrationFactor(secondReading bool) (int32, float32) {
197+
// locking concurrent calls is done at sensor side
198+
return d.sensor.OffsetAndCalibrationFactor(secondReading)
199+
}
200+
201+
// SetOffsetAndCalibrationFactor sets linear correction values, used for reading.
202+
// If the given flag is true, this values are related to the second reading.
203+
func (d *HX711Driver) SetOffsetAndCalibrationFactor(offset int32, calibrationFactor float32, secondReading bool) {
204+
// locking concurrent calls is done at sensor side
205+
d.sensor.SetOffsetAndCalibrationFactor(offset, calibrationFactor, secondReading)
206+
}
207+
208+
// Measure retrieves the values of the sensor and returns the measure. If only channel A is configured, channel B just
209+
// returns always zero. Because the timing is somewhat difficult on CPUs regarding the low time of the clock pin (>60us
210+
// leads to a reset of the hardware), it is suggested to implement a validation for the values and drop or repeat the
211+
// measure in case of a value is unexpected.
212+
func (d *HX711Driver) Measure() (float32, float32, error) {
213+
// locking concurrent calls is done at sensor side
214+
if err := d.sensor.Update(0); err != nil {
215+
return 0, 0, err
216+
}
217+
218+
v1, v2 := d.sensor.Values()
219+
220+
return v1, v2, nil
221+
}
222+
223+
func (o hx711GainAndChannelCfgOption) String() string {
224+
return "hx711 gain and channel configuration option"
225+
}
226+
227+
func (o hx711GainAndChannelCfgOption) apply(cfg *hx711Configuration) {
228+
cfg.gainAndChannelCfg = hx711.GainAndChannelCfg(o)
229+
}
230+
231+
func (o hx711ReadTimeoutOption) String() string {
232+
return "hx711 read timeout option"
233+
}
234+
235+
func (o hx711ReadTimeoutOption) apply(cfg *hx711Configuration) {
236+
cfg.readTimeout = time.Duration(o)
237+
}
238+
239+
func (o hx711ReadTriesMaxOption) String() string {
240+
return "hx711 maximum read tries option"
241+
}
242+
243+
func (o hx711ReadTriesMaxOption) apply(cfg *hx711Configuration) {
244+
cfg.readTriesMax = uint8(o)
245+
}
246+
247+
func (o hx711TickSleepOption) String() string {
248+
return "hx711 tick sleep option"
249+
}
250+
251+
func (o hx711TickSleepOption) apply(cfg *hx711Configuration) {
252+
cfg.tickSleep = time.Duration(o)
253+
}

0 commit comments

Comments
 (0)