Strain Gauge/Load Cell based Endstops

TIL: The RESET pin on the ADS1263 must be held high for the chip to run. If its low, the chip goes into power down mode and wont respond to commands. This is explained in the fine manual.

I was trying to do a reset without using the reset pin, using the RESET Command (0x06/0x07). All I ever got out of the chip was [0x00, 0x00, ...]. There is a day of :interrobang: that I wont get back.

I was wrong about the reset pin. It must be high but its hooked up to a pull-up resistor on the board, so that wasn’t my issue. The problem was power. The chip must see both 5V power and 3.3V to work. The connector on the board has power wired into the VCC/3.3V line. This seems like a mistake as this cant be used to power the chip and there is no other 5V input on the board besides the one for the RPI Hat header. So that lead me to this ugly but functional wiring job:

Now that I have gotten it to talk, I should be able to make some actual progress!

23:53:14  // Klipper state: Ready
23:53:25  $ ADSPROBE_RESET
23:53:28  $ ADSPROBE_DEBUG_READ REG=0 COUNT=25
23:53:28  // ADS1263 REG[0x0] = [0x23,0x11,0x5,0x0,0x80,0x4,0x1,0x0,0x0,0x0,0x0,0x0,0x40,0xbb,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0]
2 Likes

Some progress: DUMP, CONFIGURE, RESET, START_CAPTURE, STOP_CAPTURE, READ and WRITE commands implemented. CONFIGURE can set: INPUT, RATE, PGA, GAIN, FILTER, CHOP, VREF, OFFSET and RANGE. Calibration is in there too but not yet fully implemented, I’ll wait on building a way to visualize the impact of that before finishing it out.
Code is on this branch: GitHub - garethky/klipper at adc-endstop
Here is the code for the sensor chip so far: klipper/ads1263.py at adc-endstop · garethky/klipper · GitHub

This thing has a LOT of bit packed registers with settings that don’t neatly follow some mathematical formula. So I basically put all the strings from the manual in there and have them dumped out via lookup tables. If anyone has a better method for reading/writing 24bit little endian integers or building pretty hex strings let me know.

Here is the DUMP output:

$ DUMP_ADS1263
// DeviceId [0x0]:[0x23]
// \---- Model: ADS1263
// \---- Revision: 3
// Power [0x1]:[0x11]
// \---- Reset Indicator: Yes
// \---- Level Shift Voltage Enabled: No
// \---- Internal Reference Enabled: Yes
// Interface [0x2]:[0x5]
// \---- Interface Timeout Enabled: No
// \---- Include Staus Byte: Yes
// \---- Checksum Mode: Enabled, Checksum mode (default)
// Mode 0 [0x3]:[0x0]
// \---- ADC1 Reference Mux Polarity: Normal
// \---- Run Mode: Continuous (default)
// \---- Chop Mode: Disabled (default)
// \---- Delay: no delay (default)
// Mode 1 [0x4]:[0x80]
// \---- Filter: FIR mode (default)
// \---- Sensor Bias ADC Connection: ADC1 (default)
// \---- Sensor Bias Polarity: Pull up (default)
// \---- Sensor Bias Magnitued: No sensor bias current or resistor (default)
// Mode 2 [0x5]:[0x4]
// \---- PGA: Enabled (default)
// \---- PGA Gain: 1 V/V (default)
// \---- Data Rate: 20 SPS (default)
// Inputs [0x6]:[0x1]
// \---- Positive: AIN0 (default)
// \---- Negative: AIN1 (default)
// Offset Cal [0x7]:[0x0,0x0,0x0] = 0
// Full Scale Cal [0xa]:[0x0,0x0,0x40] = 4194304
// Reference Voltage [0xf]:[0x24]
// \---- Positive: Internal analog supply (VAVDD)
// \---- Negative: Internal analog supply (VAVSS)

Whenever you CONFIGURE something it re-uses the dump code to explain what is now in the register(s) that changed:

$ CONFIGURE_ADS1263 INPUT=4 RATE=15
19:00:16  // Inputs [0x6]:[0x89]
// \---- Positive: AIN8
// \---- Negative: AIN9
// Mode 2 [0x5]:[0xf]
// \---- PGA: Enabled (default)
// \---- PGA Gain: 1 V/V (default)
// \---- Data Rate: 38400 SPS

Next step is to implement capturing the ADC counts in c. I’m going to start with sensor_angle.c from Kevin and mutate that to work. It seems like there is probably a way to make a generic ‘sensor_host.c’ that could support plugins but that’s an idea for another time.

FWIW, the tmc.py:FieldHelper() class can be used for this purpose. (Take a look at the top of tmc2130.py for an example of how to define the registers and bits.) Although that class is in the tmc.py file it isn’t specific to the Trinamic devices.

-Kevin

1 Like

A lot of investigation this week and not a lot to show for it:

I want to design a strain gauge that can be retrofit into most hotend designs. Aluminum bar strain gauges are just too big. Some sort of Diaphragm Flexure is needed to mount the strain gauges on that can fit under an extruder. I’ve seen 2 diaphragm flexure strain gauges for 3D printing:

Both designs use large format resistors instead of true strain gauges. The attraction of a PCB that can be mass manufactured and doesn’t require tricky hand application of strain gauges is pretty obvious. Also strain gauges are expansive.

I don’t know if these designs would work well for extrusion force measurement. Classically strain gauges feature a flexure that provides linear response to force. These designs have the components in tension and that might not be linear. Testing is needed.

I did some looking around for diaphragm flexure designs and found this paper. The design is simple enough. The tricky part is cutting slots in a PCB. Board houses don’t like to do that, especially long, narrow, curved ones. A laser or waterjet could do it. This whole topic probably needs its own thread. I made a drawing in CAD and did a strain simulation on it so we have a pretty picture to look at:

Back on topic: I got angle.c to build, deploy and work. At least as far as looking up the commands on the MCU. So I spent the rest of my weekend on the c code to read data from the ADS1263 and send it back to the host. Still too much left to do to show anything yet.

Keep in mind that it has to withstand some forces and that actual deformations can have an impact on print quality. This depends on the printer construction. If these homemade load cells are seeing the extrusion force, they will bend also in dependence of this force. This effect is already visible and sometimes problematic with my printer model which has two 5kg load cells. With the original (not well designed) hotend, the extrusion force sometimes became that high, that the layer height got significantly reduced (which in turn leads to even hight forces). I have a E3Dv6 for some years now which has lower extrusion forces and this issue is gone, but I doubt a PCB load cell can withstand those forces.

If it shall not see the extrusion force, you either need to pack the entire extruder incl. the motor onto the load cells, which likely is too heavy, or you have to use a Bowden extruder.

So to summarise, this surly interesting construction with the PCB will likely only work well with a Bowden extruder.

1 Like

I hear you about the flexibility. I’m very interested in testing different thickness PCB material. You can get up to 3mm but it costs extra. The size of that plate is only 35mm square, small size should mean less flex.

I updated my branch with my work in progress. I think I have all of the c and about 1/2 of the python written. The module that does the sample extraction is called sensor_load_cell.c. It is basically sensor_angle with some names changed. adc_endstop is in as well and it is fed data by sensor_load_cell. And then back in klippy I can create both objects on the MCU and set up a PrinterProbe with the adc_endstop. [load_cell] ends up working a lot like how BLTouch is implemented.

I did a lot of thinking about how we can support drift, noise suppression, triggering and so on inside the endstop code on the mcu without it being overly complex. What I came up with is a pair of Exponential Moving Average filters. These filters are tunable with an alpha coefficient that controls how much impact new points have on the filter’s output. One filter tracks the latest sample using a rather large coefficient, making it very responsive. A second filter, the trend filter, track the trend by using a smaller coefficient. We can then check for the trigger condition by testing the output of the sample filter against the trend filter +/- a deadband value.

The one enhancement I made is that the trend filter is only updated with new samples when the sample is determined NOT to be a trigger sample. This freezes the trend during a triggering event until the sample crosses back into the deadband range. This should happen very quickly in a probing scenario.

All of this is tunable at runtime via a reset command. My hope is that we wont need that. Ideally this gives us enough flexibility to tune in a variety of load cells and sensors without having to constantly babysit the endstop. The algorithm is pretty straightforward to read. Its simple enough that I made a spreadsheet so you can play with this idea: Endstop Double EMA Test - Google Sheets

The EMA filter raises performance questions because the ADC produces 32 bit signed integers. A division is required to produce an average. We can either choose to live with 32 bit divide operations or try to optimize with bit shifting. I found an implementation that uses uint64 type which should be fast but I wont trust it until I do lots of testing. I’m probably in “premature optimization” land on this one.

Next step is to get some sort of live visualization going.

AFAIK you can also get different kind of base material, like aluminium (normally used when heat dissipation is critical). Surely it will cost even more. Also you could try gluing the PCB on top of some 3D printed basis of the same shape to increase the stiffness. Or you just glue together multiple PCBs with only the top one populated with the components (often, ordering multiple PCBs of the same layout is cheap). You will get nowhere near the stiffness of a pair of 5kg load cell bars made from aluminium though…

This looks like a very interesting idea. My feeing is that with such filtering it will get hard to make any high-level post processing (like the linear fit) to work reliably, but as long as those algorithms will still get the unfiltered data it may work. I think I would still like to do the fit in the end, since it gives you the best precision in absolute terms (i.e. no residual offset to calibrate away manually). Still, with such filtering in the MCU code the approach would be sped up by a lot. I could imagine, if we feed just the last few samples before triggering to the linear fit, we could already achieve a good enough precision.

You will get the raw unfiltered data back in Klippy to do that. When the endstop fires we will get the time of the sample that triggered first. We will be able to use that to isolate the samples from the event and perform the linear regression.

I did not do the math on _calc_endstop_rate() right when I got started on this project. That block of code basically says that the sample rate is the microsteps per mm of the stepper with the highest step rate * the movement speed. The default probe speed is 5mm/s. So on a Voron Trident, as an example, the result should be ((200 *16) / 4) * 5mm/s = 4000 SPS. Which we can easily do on the ADS1263. The 66K SPS rate only happens after the first triggering point to try and get 4 good triggering samples before the next microstep executes. But you could also do at 20K SPS.

I will be taking a couple weeks of from this project for a vacation. When I get back I’ll be diving into load cells and validating the endstop code.

Interesting, the BIQU B1 SE PLUS comes with a strain gauge.

1 Like

Hello,
I’ve been researching the sensor design, as I don’t find the metal prebuilt ones to be practical for typical toolhead designs. There’s two things that I want to consider here: thermal compensation and installation technique. From what i’ve learned, if you want to compensate the thermal effects you have to place 4 gages in a wheatstone bridge, and have one half of the bridge not under strain, but in a similar/nearby thermal environment. The other half of the bridge should have one gage on each side of the “bend”, one under compression and the other under expansion. I’m still waiting for my order to arrive so I’ve made no tests, but my question for @mhier and others who are experiencing so much drift is… Is your hardware not following this design or is it just that in practice this doesnt work as well as in theory?

About installation, there’s dozens of videos in youtube by the manufacturers of the gages themselves about optimal procedure, cleanup, adhesive choice, etc… depending on the material you’re sticking the gage to. But there’s a method I haven’t seen discussed anywhere and it’s specific to our field… I want to try to embed the gages between layers of a 3D print, the way we sometimes do with magnets, or color changes. Printing something something like this, having a pause, placing the sensor with cables laying down, and then a flat layer on top.


I haven’t measured the width of the sensor yet, is this an awful idea? It’ll still be months until I can do any tests so I wanted to ask you first.
Looking around aliexpress there seems to be a big difference between getting the “common” model BF350 - 3AA at around 2€ for a pack of 10, and custom sizes and even materials (there’s peek and polyimide film substrates for temperature resistance) at up to 4€ per single gage. It might hard to accomodate the size of four of the “typical” ones in the toolhead I have in mind., but it’s nice that there are options.

1 Like

My printer has two TAL220B type load cells. They are temperature compensated, but only up to 40 deg Celsius. So one potential issue is that even at PLA temperatures the heat bed is hotter than the temperature compensation works and quite close to the load cells (at the beginning of the print at least). I am not so sure though how high the temperature really gets at the load cells itself, but anyway these 40 deg won’t be a hard limit and we have quite high demands on precision/stability. So in any case, temperature can be an issue despite the existence of temperature compensation.

On the other hand I am also seeing “drifting” readouts when no heater is involved (e.g. in my CNC mill setup). I put “drifting” in quotes here, since in that case the readout is not really drifting on its own, but the baseline is moving e.g. when the carriage moves or when comparing before and after the tool head has contact with the bed/workpiece. My suspicion is that there are mechanical tensions, which can either directly apply (internal) forces to the sensors or lead to an imbalance how the external forces are distributed to the two sensors (which likely won’t be perfectly identical).

So I would recommend to take special care on these two points when designing a sensor:

  • temperature compensation should work also for high temperatures (like maximum bed temperature, which is more like 100-130 Celsius), and
  • avoid mechanical “loops” which can have internal tensions.

Both points might be challenging to solve, though… You will also need good mechanical stability, so attaching everything just to a single load cell might be too flimsy (will spoil print quality). If you have ideas, please let us know. Compensating the drift in software works and probably we can even speed things up, but fixing an error is always better than compensating for it. :slight_smile:

@RebelPhoton & @mhier shall we start a separate thread to explore different design ideas? I think we have a long way to go, particularly in the area of quantifying the performance of these designs. I would like to focus this thread on the software to support the probing use-case. Other use cases like extrusion pressure sensing will be possible too, but that depends more on the gauge design than the software.

1 Like

If you wish I can move the hardware design posts to a new topic:
25, 26, 27, 29?, 31, 32, 33, 34

Thanks Sineo, but I think we will just start fresh.

Progress this week:

  • Created a web pages that connects to Moonraker and plots the load cell data live using a custom chart setup that helps explain whats going on.
  • Updated the c code to send back the values of the 2 Exponential Moving Average (EMA) filters.
  • Implemented automatic EMA filter initialization and settling
  • 90% time spend debugging

When I got the graph working the values I was getting were all wrong. Much was wrong with the EMA code in C. There were also some glaring bugs with structs not being initialized. Then there was a witch hunt to solve filter initialization. But after all that I have a screenshot to show you:

That is my index filter tapping on a 1Kg load cell which is being sampled 400 times a second! The red line is the “smoothed” sample. The light blue line in the background is the raw value from the ADC. The gray band is the deadband. It is actually adaptive, but its coefficient is set such that it wont move for normal triggering events. I tried using a heat gun on the load cell and I can actually see it moving around. So we should be good for heat creep.

I’m sure @mhier is going to be excited to see that we can track the collision event back into the deadband and find its origin in time. There is probably 40ms from collision start to trigger, I could optimize my deadband value to improve on that here. And of course my finger is not a toolhead.

Anyway, I’m excited, this looks really promising! My next adventure will be trying to “home” my Z axis by tapping on the load cell. Pretty soon I’m going to need an actual toolhead load cell to continue testing.

3 Likes

Thanks, this is really interesting! If the 40 ms are similar for a real toolhead-on-heatbed scenario (which yet has to be shown of course), doesn’t this mean sampling at 400 Hz is not making much of a difference compared to much slower sampling rates? 40 ms equals to 25 Hz, so the 80 Hz of a HX711 should be sufficient to get similar reaction speeds. Or am I missing something here? (Yes, it could be much faster because materials are harder, I know… :-))

I think my finger is squishy at these time scales. If I uses a metal Allen Key I can get basically an instant trigger:

But what was my mm/s etc? No idea. We need real hardware to test on.

1 Like

I just realized that the MCU Awake % display is wrong. It’s 5x too big. So 400SPS costs about 1% awake (this is on a 180Mhz STM chip). 7.2K SPS costs about 16%. It works at 38K SPS but the Pi actually overheats going that fast :sweat_smile:

This sounds very promising.

One potential (but solvable) issue just came to my mind: I have observed sometimes hysteresis-like effects on the real hardware setup. I am not 100% sure, but I think they are caused by internal tension of the system formed by the load cells with their support and the hotend holder, which may change when external forces are applied. I could also imagine similar systems being caused by temperature effects, especially, if homing is done while heating up the printer.

I wonder how the EMA filter would deal with such situation. If the baseline has drifted away during the contact time so far that it does not return into the dead bend, we are lost.

I think a possible solution is to make sure the hotend never stays in contact with the bed for an extended period of time. Once the threshold is exceeded and the virtual end stop has triggered, the tool head needs to be lifted again immediately. This is not what a normal homing move would do - it might back off and retry the homing with a slower speed, but then it will usually stay in the homed position with the end switch pressed.

There is also one other reason why this should be avoided: If homing is performed and next the hotend is heated up, the hotend will elongate due to thermal expansion. This will increase the force applied to the bed surface and the hotend itself. Depending on overall stiffness and the amount of heat expansion of the hotend this might result in dangerously high forces.

To prevent these mechanical hysteresis-like effects from confusing the EMA filter, I would recommend to reset the filter after backing off the hotend. You can safely do that because directly after backing off the tool head by a sufficient distance you know for sure the hotend is not in contact with the bed, so you can assume the current measurement is the baseline again.

Btw: Is the code in your fork you posted some time ago up to date? I am thinking whether I can get it running on my printer with real hardware. I would have to write a driver for my ADC, and my ADC is a lot slower than yours (measly 8 SPS at full resolution), but the basic principle should still work. I don’t really understand where the ADC driver is - I can see a file “ads1263.py”, but shouldn’t the driver be in the microcontroller?