fivesixzero / CircuitPython_HX711

A CircuitPython driver for the HX711 load cell amplifer and ADC
MIT License
5 stars 1 forks source link

GPIO, NRF52840: Unstable, unusable read_raw() results #2

Closed fivesixzero closed 2 years ago

fivesixzero commented 2 years ago

While testing in #1 the only port that encountered any issues was the nrf port while testing on an Adafruit Feather nRF52840 Sense board.

Looking at traffic on a logic analyzer helps to understand more aobut why the nrf port causes such weird looking data.

For a test, I captured pulses from a best-case example, the RP2040 running in PIO, to compare to the NRF52840's behavior.

The top of the image is the pulses from the nRF52840 Feather using the GPIO driver. The bottom is the RP2040 using the PIO mode driver, which is the most consistent.

Zoomed in on the successful, healthy PIO-mode reading:

Basically, the pulses from the nrf board are very slow compared to the PIO mode RP2040 pulses.

This is definitely the kind of behavior that can be expected when bit-banging from the highest level of the stack, way up in the Python VM. However, the HX711 is typically very really tolerant when it comes to timing, making this a bit of an oddity.

fivesixzero commented 2 years ago

It looks like at least one other library may have similar timing issues on the nrf platform:

https://github.com/adafruit/Adafruit_CircuitPython_LC709203F/issues/3

Interesting.

fivesixzero commented 2 years ago

For good measure, I followed up with a capture from a functional port, in this case the stm port running on an Adafruit Feather STM32F405 Express.

In this image, I tried to scale the visualization to similar timeframes to make it easy to see any deviations.

fivesixzero commented 2 years ago

A few observations.

First, timing is definitely different between the stm and nrf port, and also between those and the RP2040 PIO driver.

At first glance, these timings look mostly ok (except the gain pulse timings, which we'll get to later) and within the timing constraints laid out in the HX711 datasheet.

Here are some 'typical' numbers covering the primary burst of 24 clock pulses, since the final gain pulse can someitmes lag behind these quite a bit.

port clock high clock low rise-to-fall max delta
PIO 1.00 µs 1.00 µs 2.00 µs +- 10.00 ns
stm 5.00 µs 18.50 µs 23.600 µs +- 100.00 ns
nrf 9.40 µs 39.50 µs 48.90 µs +- 100.00 ns

Here's a screenshot of the final pulse with timing measurements applied for the clock timings.

However, we've definitely got a big deviation from timings when we look at the gain pulse times.

port gain clock high gain clock low increase
PIO 1.00 µs 1.01 µs +0.01 µs
stm 5.00 µs 46.70 µs +23.1 µs
nrf 9.40 µs 106.50 µs + 67.0 µs

Whoa, yeah, this isn't good. That gain pulse on the NRF is definitely way, way too long for the HX and it's going to throw off any future frames. Without registering a gain pulse, its likely that the HX711 is still waiting in a "ready" state. The next read-in pulse train will end up causing the next ADC count to be generated with a gain of 3, with the following 22 pulses being ignored. Whoops.

Also, we can see that the stm port is dangerously close to hitting the timing limit - only 300 ns away from the datasheet's boundary. So its not that the nrf port is broken, it just has the longest timing between bit-banged clock pulses when there's higher level decision-making logic in between the intended pulse edges.

At the very least, we'll want to see if we can tighten up the timing on the gain pulse. If at all possible it'd be nice to get that pulse to follow the 24 "read" pulses with an identical delay. It's also worth investigating whether bitbangio or other lower-level CircuitPython libraries can help us get more deterministic results from a GPIO driver. And it also reinforces value of having a PIO driver on boards that can support it, since that gets us very deterministic results.

fivesixzero commented 2 years ago

Testing a change to read_raw() which simply adds gain pulses to the read pulse loop then bit-shifts out the gain pulses after the loop's done.

Since Python ints are 32-bit, we've got plenty of room for 1-3 gain pulses to be stacked during read then popped off. This is a lot more time-efficient than using a separate function for gain pulses. The primary upshot of this is the fact that the gain pulses will always have the same timing as the rest of the pulses, which is good.

Taking a look with the logic analyzer it looks like this works as expected. Clock pulse timing looks identical to the timing before this change. The 'time-variable' portion of the work now takes place entirely outside of the bit-bang loop, which only contains three bits of work aside from the clock pin value flips - data pin value read-in, bit-shift of the raw_reading value, and save of the bit-shifted value.

For now this might be the best we can do when flipping bits at a high level, will commit the change in a bit after some more testing.

fivesixzero commented 2 years ago

This may be fixed, or at least partially addressed, via 41831e6.

fivesixzero commented 2 years ago

Found the problem!

My testing setup was pulling power from the pin directly to the left of ground on the Feather boards rather than the dedicated 3V pin. On some boards, including the Feather nRF52840 I've been testing with, this is an AREF pin. On the STM32F405 board this pin is tied to the 3V pin by a cuttable jumper, so that was fine. But on the nRF board this pin's default state doesn't provide enough voltage to fully power on the HX711, resulting in the data edge instability visible on the logic analyzer visualizations.

In the test script I set up AREF as a DigitalInOut then set its value to True. Now it's working as expected. :)

fivesixzero commented 2 years ago

Testing on all platforms with the new timings is complete and everything looks good.

Will add to the documentation the caveat that the GPIO driver may not operate properly on esp or nrf platforms if other board functions, like WiFi or Bluetooth, are used.

Discussed the timing issues in the circuitpython-dev channel on the Adafruit Discord and danh suggested looking into using SPI, since the protocol is similar, which may be worth persuing in the future if further timing issues emerge.

At first glance it looks like the CircuitPython 7.x SPI handlers in busio and bitbangio may not work, given that the data reads we need don't fit typical 'word' boundaries, since we need to clock out those gain pulses. But it's worth investigating in the future if the need arises.