diff --git a/experimental/devices/hx711/hx711.go b/experimental/devices/hx711/hx711.go index cdcf10f..680933a 100644 --- a/experimental/devices/hx711/hx711.go +++ b/experimental/devices/hx711/hx711.go @@ -2,7 +2,8 @@ // Use of this source code is governed under the Apache License, Version 2.0 // that can be found in the LICENSE file. -// Package hx711 implements an interface to the HX711 analog to digital converter. +// Package hx711 implements an interface to the 24-bits HX711 analog to digital +// converter. // // Datasheet // @@ -11,9 +12,11 @@ package hx711 import ( "errors" + "sync" "time" "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/experimental/conn/analog" ) var ( @@ -33,27 +36,32 @@ const ( CHANNEL_B_GAIN_32 InputMode = 2 ) +// Dev is a handle to a hx711. type Dev struct { + // Immutable. + name string + clk gpio.PinOut + data gpio.PinIn + + // Mutable. + mu sync.Mutex inputMode InputMode - clk gpio.PinOut - data gpio.PinIn done chan struct{} } // New creates a new HX711 device. -// The data pin must support edge detection. If your pin doesn't natively -// support edge detection you can use PollEdge from -// periph.io/x/periph/experimental/conn/gpio/gpioutil +// +// The data pin must support edge detection. If your pin doesn't natively +// support edge detection you can use PollEdge from gpioutil. func New(clk gpio.PinOut, data gpio.PinIn) (*Dev, error) { if err := data.In(gpio.PullDown, gpio.FallingEdge); err != nil { return nil, err } - if err := clk.Out(gpio.Low); err != nil { return nil, err } - return &Dev{ + name: "hx711{" + clk.Name() + ", " + data.Name() + "}", inputMode: CHANNEL_A_GAIN_128, clk: clk, data: data, @@ -61,68 +69,63 @@ func New(clk gpio.PinOut, data gpio.PinIn) (*Dev, error) { }, nil } -// SetInputMode changes the voltage gain and channel multiplexer mode. -func (d *Dev) SetInputMode(inputMode InputMode) { - d.inputMode = inputMode - d.readImmediately() +// String implements analog.PinADC. +func (d *Dev) String() string { + return d.name } -// IsReady returns true if there is data ready to be read from the ADC. -func (d *Dev) IsReady() bool { - return d.data.Read() == gpio.Low +// Name implements analog.PinADC. +func (d *Dev) Name() string { + return d.String() } -// Read reads a single value from the ADC. It blocks until the ADC indicates -// there is data ready for retrieval. If the ADC doesn't pull its Data pin low -// to indicate there is data ready before the timeout is reached, TimeoutError -// is returned. -func (d *Dev) Read(timeout time.Duration) (int32, error) { - // Wait for the falling edge that indicates the ADC has data. - if !d.IsReady() { - if !d.data.WaitForEdge(timeout) { - return 0, TimeoutError - } - } +// Number implements analog.PinADC. +func (d *Dev) Number() int { + return -1 +} - return d.readImmediately(), nil +// Function implements analog.PinADC. +func (d *Dev) Function() string { + return "ADC" } -func (d *Dev) readImmediately() int32 { - // Shift the 24-bit 2's compliment value. - var value uint32 - for i := 0; i < 24; i++ { - d.clk.Out(gpio.High) - level := d.data.Read() - d.clk.Out(gpio.Low) +// SetInputMode changes the voltage gain and channel multiplexer mode. +func (d *Dev) SetInputMode(inputMode InputMode) error { + d.mu.Lock() + defer d.mu.Unlock() + d.inputMode = inputMode + _, err := d.readRaw() + return err +} - value <<= 1 - if level { - value |= 1 - } - } +// Range implements analog.PinADC. +func (d *Dev) Range() (analog.Sample, analog.Sample) { + return analog.Sample{Raw: -(1 << 23)}, analog.Sample{Raw: 1 << 23} +} - // Pulse the clock 1-3 more times to set the new ADC mode. - for i := 0; i < int(d.inputMode); i++ { - d.clk.Out(gpio.High) - d.clk.Out(gpio.Low) - } - // Convert the 24-bit 2's compliment value to a 32-bit signed value. - return int32(value<<8) >> 8 +// Read implements analog.PinADC. +func (d *Dev) Read() (analog.Sample, error) { + d.mu.Lock() + defer d.mu.Unlock() + raw, err := d.readRaw() + return analog.Sample{Raw: raw}, err } -// StartContinuousRead starts reading values continuously from the ADC. It +// ReadContinuous starts reading values continuously from the ADC. It // returns a channel that you can use to receive these values. // // You must call Halt to stop reading. // -// Calling StartContinuousRead again before Halt is an error, +// Calling ReadContinuous again before Halt is an error, // and nil will be returned. -func (d *Dev) StartContinuousRead() <-chan int32 { +func (d *Dev) ReadContinuous() <-chan analog.Sample { + d.mu.Lock() + defer d.mu.Unlock() if d.done != nil { return nil } done := make(chan struct{}) - ret := make(chan int32) + ret := make(chan analog.Sample) go func() { for { @@ -131,9 +134,9 @@ func (d *Dev) StartContinuousRead() <-chan int32 { close(ret) return default: - value, err := d.Read(time.Second) + value, err := d.ReadTimeout(time.Second) if err == nil { - ret <- value + ret <- analog.Sample{Raw: value} } } } @@ -143,11 +146,70 @@ func (d *Dev) StartContinuousRead() <-chan int32 { return ret } -// Halt stops a continuous read that was started with StartContinuousRead. -// This will close the channel that was returned by StartContinuousRead. -func (d *Dev) Halt() { +// Halt stops a continuous read that was started with ReadContinuous. +// +// This will close the channel that was returned by ReadContinuous. +func (d *Dev) Halt() error { + d.mu.Lock() + defer d.mu.Unlock() if d.done != nil { close(d.done) d.done = nil } + return nil +} + +// IsReady returns true if there is data ready to be read from the ADC. +func (d *Dev) IsReady() bool { + return d.data.Read() == gpio.Low +} + +// ReadTimeout reads a single value from the ADC. +// +// It blocks until the ADC indicates there is data ready for retrieval. If the +// ADC doesn't pull its Data pin low to indicate there is data ready before the +// timeout is reached, TimeoutError is returned. +func (d *Dev) ReadTimeout(timeout time.Duration) (int32, error) { + // Wait for the falling edge that indicates the ADC has data. + d.mu.Lock() + defer d.mu.Unlock() + if !d.IsReady() { + if !d.data.WaitForEdge(timeout) { + return 0, TimeoutError + } + } + return d.readRaw() } + +func (d *Dev) readRaw() (int32, error) { + // Shift the 24-bit 2's compliment value. + var value uint32 + for i := 0; i < 24; i++ { + if err := d.clk.Out(gpio.High); err != nil { + return 0, err + } + level := d.data.Read() + if err := d.clk.Out(gpio.Low); err != nil { + return 0, err + } + + value <<= 1 + if level { + value |= 1 + } + } + + // Pulse the clock 1-3 more times to set the new ADC mode. + for i := 0; i < int(d.inputMode); i++ { + if err := d.clk.Out(gpio.High); err != nil { + return 0, err + } + if err := d.clk.Out(gpio.Low); err != nil { + return 0, err + } + } + // Convert the 24-bit 2's compliment value to a 32-bit signed value. + return int32(value<<8) >> 8, nil +} + +var _ analog.PinADC = &Dev{} diff --git a/experimental/devices/hx711/hx711_test.go b/experimental/devices/hx711/hx711_test.go new file mode 100644 index 0000000..26643bf --- /dev/null +++ b/experimental/devices/hx711/hx711_test.go @@ -0,0 +1,128 @@ +// Copyright 2018 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package hx711 + +import ( + "errors" + "testing" + "time" + + "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/conn/gpio/gpiotest" +) + +func TestNew(t *testing.T) { + clk := gpiotest.Pin{N: "clk"} + data := gpiotest.Pin{N: "data", EdgesChan: make(chan gpio.Level)} + d, err := New(&clk, &data) + if err != nil { + t.Fatal(err) + } + if s := d.String(); s != "hx711{clk, data}" { + t.Fatal(s) + } + if s := d.Name(); s != "hx711{clk, data}" { + t.Fatal(s) + } + if n := d.Number(); n != -1 { + t.Fatal(n) + } + if f := d.Function(); f != "ADC" { + t.Fatal(f) + } + min, max := d.Range() + // TODO(davidsansome): Is that the right values? + if min.Raw != -8388608 { + t.Fatal(min.Raw) + } + if max.Raw != 8388608 { + t.Fatal(max.Raw) + } + if !d.IsReady() { + t.Fatal("data is low") + } + if err := d.SetInputMode(CHANNEL_A_GAIN_128); err != nil { + t.Fatal(err) + } + if err := d.Halt(); err != nil { + t.Fatal(err) + } +} + +func TestNew_Fail(t *testing.T) { + ok := gpiotest.Pin{N: "ok", EdgesChan: make(chan gpio.Level)} + fail := failPin{gpiotest.Pin{N: "fail"}} + if _, err := New(&fail, &ok); err == nil { + t.Fatal("expected failure") + } + if _, err := New(&ok, &fail); err == nil { + t.Fatal("expected failure") + } +} + +func TestRead(t *testing.T) { + clk := gpiotest.Pin{N: "clk"} + data := gpiotest.Pin{N: "data", EdgesChan: make(chan gpio.Level)} + d, err := New(&clk, &data) + if err != nil { + t.Fatal(err) + } + // TODO(davidsansome): Real testing. + r, err := d.Read() + if err != nil { + t.Fatal(err) + } + if r.Raw != 0 { + t.Fatal("we should implement something") + } +} + +func TestReadTimeout(t *testing.T) { + clk := gpiotest.Pin{N: "clk"} + data := gpiotest.Pin{N: "data", EdgesChan: make(chan gpio.Level)} + d, err := New(&clk, &data) + if err != nil { + t.Fatal(err) + } + // TODO(davidsansome): Real testing. + r, err := d.ReadTimeout(time.Second) + if err != nil { + t.Fatal(err) + } + if r != 0 { + t.Fatal("we should implement something") + } +} + +func TestReadContinuous(t *testing.T) { + clk := gpiotest.Pin{N: "clk"} + data := gpiotest.Pin{N: "data", EdgesChan: make(chan gpio.Level)} + d, err := New(&clk, &data) + if err != nil { + t.Fatal(err) + } + // TODO(davidsansome): Real testing. + c := d.ReadContinuous() + if c == nil { + t.Fatal("expected chan") + } + if err := d.Halt(); err != nil { + t.Fatal(err) + } +} + +// + +type failPin struct { + gpiotest.Pin +} + +func (f *failPin) In(pull gpio.Pull, edge gpio.Edge) error { + return errors.New("fail") +} + +func (f *failPin) Out(l gpio.Level) error { + return errors.New("fail") +}