-
Notifications
You must be signed in to change notification settings - Fork 239
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
base: public
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) |
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"] | ||
} | ||
} |
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. |
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() | ||
} | ||
|
||
get format() { | ||
return "number" | ||
} | ||
|
||
readable() { | ||
//return this.#din.read() == 0 | ||
} | ||
Comment on lines
+40
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
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 | ||
} | ||
*/ | ||
} |
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 | ||
} |
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"; | ||
} |
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"] | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If any of these throw, 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The destructor can be called with |
||
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); | ||
} |
There was a problem hiding this comment.
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 whenconfigure
is called, since that property is for the constructor. The simplest solution is to move these two lines to the constructor and leaveconfigure
empty.