Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new driver for HX711 load cell ADC #1251

Open
wants to merge 1 commit into
base: public
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions examples/drivers/sensors/hx711/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright © 2023 by Thorsten von Eicken.
import HX711 from "embedded:sensor/ADC/HX711"
import Timer from "timer"
import Time from "time"

trace("===== HX711 TEST =====\n")

let hx711 = new HX711({
sensor: {
io: device.io,
din: 7,
clk: 6,
},
gain: 1, // 128x
})

let v = 0
Timer.repeat(() => {
const t0 = Time.ticks
const raw = hx711.read()
const dt = Time.ticks - t0
trace(`Raw: ${raw} in ${dt}ms\n`)
}, 1000)
11 changes: 11 additions & 0 deletions examples/drivers/sensors/hx711/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"include": [
"$(MODDABLE)/examples/manifest_base.json",
"$(MODDABLE)/examples/manifest_typings.json",
"$(MODDABLE)/modules/io/manifest.json",
"$(MODDABLE)/modules/drivers/sensors/hx711/manifest.json"
],
"modules": {
"*": ["./main"]
}
}
14 changes: 14 additions & 0 deletions modules/drivers/sensors/hx711/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Avia HX711 24-bit ADC for weight scales
=======================================

The HX711 chip is an ADC designed for weight scales or load cells.
It has a 24-bit ADC and a programmable gain amplifier (PGA) with gain up to 128.
The interface requires a clock wire and a data wire: the clock is pulsed 25-27 times and
24 bits of data are read from the data line. A number of extra clock pulses are required and
tell the chip what gain to use.

This driver is very simple and basically provides a single function to read the ADC. This
function is implemented in C using modGPIO in order to perform the read as quickly as possible
and meet the timing requirements of the chip. A read takes a couple of milliseconds.

This driver is intended to conform to ECMA-419. An example is provided in the examples directory.
76 changes: 76 additions & 0 deletions modules/drivers/sensors/hx711/hx711.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Avia HX711 24-bit ADC for weight scales
// Copyright © 2023 by Thorsten von Eicken.

import Time from "time"
import HX711c from "embedded:sensor/ADC/HX711c"

export interface Options {
sensor: {
clk: number
din: number
}
onError?: (error: string) => void

// gain selects the gain of the ADC as well as the analog input channel
gain?: number // 1:128chA, 2:32chB, 3:64chA (default: 1)
}

export default class HX711 {
#hx711c: HX711c

constructor(options) {
this.configure(options)
}

close() {
this.#hx711c = undefined
}

// To "configure" the HX711 we have to perform a read, which ends up setting up the gain
// for the next read
configure(options: Options) {
this.#hx711c = new HX711c(options.sensor.clk, options.sensor.din, options.gain || 1)
this.#hx711c.read()
Comment on lines +32 to +33
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configure() can be called more than once. If it is, the first HX711c instance will be replaced with another. But, there's also no guarantee that options.sensor will be present when configure is called, since that property is for the constructor. The simplest solution is to move these two lines to the constructor and leave configure empty.

}

get format() {
return "number"
}

readable() {
//return this.#din.read() == 0
}
Comment on lines +40 to +42
Copy link
Collaborator

@phoddie phoddie Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function necessary? It isn't in the standard. The usual behavior is to have sample() return undefined if no reading is available. (It looks like this is what you implemented in your native read function already)


read() {
return this.#hx711c.read()
}

/* read() has to be implemented in C to meet the timing requirements (clk high < 50us)

// read the 24-bit signed value from the sensor and perform additional pulses to set-up the gain
read() {
const clk_w = this.#clk.write.bind(this.#clk)
const din_r = this.#din.read.bind(this.#din)
if (din_r() != 0) return undefined
// read 24 bits: din is stable 100ns after clk rising edge until next rising edge, so we read
// after producing the falling edge
// ugh: clk high must be less than 50us or the chip enters power-down mode
let val = 0
clk_w(0) // preload caches...
for (let i = 24; i > 0; i--) {
clk_w(1)
clk_w(0)
val = (val << 1) | (din_r() & 1)
}
// sign extend
val = (val << 8) >> 8
if (val == -1) return undefined // chip entered power-down mode
// pulse the clock to set the gain
for (let i = this.#gain; i > 0; i--) {
clk_w(1)
clk_w(0)
}
return val
}
*/
}
7 changes: 7 additions & 0 deletions modules/drivers/sensors/hx711/hx711c.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright © 2023 by Thorsten von Eicken.
export default class {
constructor(clk: number, din: number, gain: number)
read(): number
readable(): boolean
close(): void
}
6 changes: 6 additions & 0 deletions modules/drivers/sensors/hx711/hx711c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright © 2023 by Thorsten von Eicken.
export default class @ "xs_HX711_destructor" {
constructor(a, b, c) @ "xs_HX711_init";
read() @ "xs_HX711_read";
readable() @ "xs_HX711_readable";
}
8 changes: 8 additions & 0 deletions modules/drivers/sensors/hx711/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"include": [],
"modules": {
"embedded:sensor/ADC/HX711": "./hx711",
"embedded:sensor/ADC/HX711c": ["./hx711c", "./modHX711c.c"]
},
"preload": ["embedded:sensor/ADC/HX711"]
}
76 changes: 76 additions & 0 deletions modules/drivers/sensors/hx711/modHX711c.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright © 2023 by Thorsten von Eicken.

#include "xsPlatform.h"
#include "xsmc.h"
#include "modGPIO.h"
#include "mc.xs.h" // for xsID_* constants

#define xsmcVar(x) xsVar(x)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused. what was the idea?


typedef struct {
modGPIOConfigurationRecord clk;
modGPIOConfigurationRecord din;
int gain;
} hx711_data;

void xs_HX711_init(xsMachine *the) {
if (xsmcArgc != 3) xsUnknownError("invalid arguments");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check isn't strictly necessary. Using xsArg on an argument that doesn't exist will throw. All this guarantees is that there aren't more than 3 arguments. No harm in the check, but it is common for JavaScript functions to simply ignore extra arguments.

int clk_pin = xsmcToInteger(xsArg(0));
int din_pin = xsmcToInteger(xsArg(1));

hx711_data *data = c_malloc(sizeof(hx711_data));
if (data == NULL) xsUnknownError("can't allocate data");
data->gain = xsmcToInteger(xsArg(2));
if (modGPIOInit(&data->clk, NULL, clk_pin, kModGPIOOutput))
xsUnknownError("can't init clk pin");
modGPIOWrite(&data->clk, 0);
if (modGPIOInit(&data->din, NULL, din_pin, kModGPIOInput))
xsUnknownError("can't init dat pin");
Comment on lines +22 to +28
Copy link
Collaborator

@phoddie phoddie Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any of these throw, data is orphaned (even the call to xsmcToInteger can call in type coercion cases). Error clean-up isn't pretty, but here's what I'd probably do here:

  int clk_pin = xsmcToInteger(xsArg(0));
  int din_pin = xsmcToInteger(xsArg(1));
  hx711_data d; 
  d.gain = xsmcToInteger(xsArg(2));

  if (modGPIOInit(&d.clk, NULL, clk_pin, kModGPIOOutput))
    xsUnknownError("can't init clk pin");
  if (modGPIOInit(&d.din, NULL, din_pin, kModGPIOOutput)) {
    modGPIOUninit(&d.clk);
    xsUnknownError("can't init din pin");
  }
  hx711_data *data = c_malloc(sizeof(hx711_data));
  if (data == NULL) {
    modGPIOUninit(&d.clk);
    modGPIOUninit(&d.din);
    xsUnknownError("can't allocate data");
  }
  data = d;


xsmcSetHostData(xsThis, data);
}

void xs_HX711_destructor(void *hostData) {
hx711_data *data = hostData;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The destructor can be called with hostData set to NULL. This happens when the virtual machine is deleted. It is rare. ;) It can also happen if an exception occurs in the constructor before the host data is set. So, the best practice is to always check for NULL

modGPIOUninit(&data->clk);
modGPIOUninit(&data->din);
c_free(data);
}

void xs_HX711_readable(xsMachine *the) {
hx711_data *data = xsmcGetHostData(xsThis);
xsmcSetBoolean(xsResult, modGPIORead(&data->din) == 0);
}

void xs_HX711_read(xsMachine *the) {
hx711_data *data = xsmcGetHostData(xsThis);

// check data is ready
if (modGPIORead(&data->din) != 0) {
xsmcSetUndefined(xsResult);
return;
}
modCriticalSectionBegin();

// read 24 bits
int32_t value = 0;
for (int i = 0; i < 24; i++) {
modGPIOWrite(&data->clk, 1);
modDelayMicroseconds(1);
modGPIOWrite(&data->clk, 0);
value = (value<<1) | (modGPIORead(&data->din) & 1);
}

// sign-extend 24->32 bits
value = (value << 8) >> 8;

// signal gain
for (int i = 0; i < data->gain; i++) {
modGPIOWrite(&data->clk, 1);
modGPIOWrite(&data->clk, 0);
}
modCriticalSectionEnd();

// return value
xsmcSetInteger(xsResult, value);
}