# Load Cell

Load cells measure force/weight to detect object presence, evaluate structural loading, or monitor mechanical stress. The system supports a dual load cell configuration, each read through its own HX711 24-bit ADC using GPIO bit-banging. This is the most complete sensor driver on the board: it talks to real hardware with no compile gate.

### Hardware Specifications

<table id="bkmrk-load-cell-hw-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Parameter

</th><th>Value

</th></tr><tr><td>Sensor Count

</td><td>2 (independent)

</td></tr><tr><td>ADC

</td><td>HX711 24-bit, one per load cell

</td></tr><tr><td>Interface

</td><td>GPIO bit-bang (DOUT input, SCK output)

</td></tr><tr><td>Gain

</td><td>Channel A, gain 128 (LOAD\_CELL\_GAIN\_PULSES = 1)

</td></tr><tr><td>Measurement

</td><td>Force (Newtons) / Mass (grams) after calibration; raw counts always available

</td></tr><tr><td>Ready timeout

</td><td>200 ms (LOAD\_CELL\_READY\_TIMEOUT\_MS)

</td></tr><tr><td>Supply

</td><td>HX711 VCC 2.7 to 5.5 V, GND common with the STM32

</td></tr></tbody></table>

### Wiring and Pin Map (from firmware.ioc)

<table id="bkmrk-load-cell-pins-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Unit

</th><th>HX711 DOUT (data, low = ready)

</th><th>HX711 SCK (clock)

</th></tr><tr><td>0

</td><td>PA5 (WEIGHT\_INPUT\_1)

</td><td>PC7 (WEIGHT\_CLOCK\_1)

</td></tr><tr><td>1

</td><td>PA6 (WEIGHT\_INPUT\_2)

</td><td>PB5 (WEIGHT\_CLOCK\_2)

</td></tr></tbody></table>

## Data Structure

```c
typedef struct {
  int32_t raw_counts;               /* 24-bit two's complement reading */
  float force_newtons;
  float mass_grams;
  float scale_newtons_per_count;    /* default 1.0 (passthrough) */
  int32_t tare_offset_counts;
  bool is_calibrated;               /* true once load_cell_set_scale() called */
  bool read_ok;                     /* true if the last poll succeeded */

  /* HX711 hardware binding (set by load_cell_sensor_init_hw) */
  GPIO_TypeDef *dout_port;
  uint16_t dout_pin;
  GPIO_TypeDef *sck_port;
  uint16_t sck_pin;
  uint8_t gain_pulses;
} load_cell_data_t;
```

## Initialization

### Initialize Load Cells (as in main.c)

```c
load_cell_data_t load_cell_data[2];

/* Unit 0: DOUT = PA5, SCK = PC7. Unit 1: DOUT = PA6, SCK = PB5. */
load_cell_sensor_init_hw(&load_cell_data[0], GPIOA, GPIO_PIN_5, GPIOC, GPIO_PIN_7);
load_cell_sensor_init_hw(&load_cell_data[1], GPIOA, GPIO_PIN_6, GPIOB, GPIO_PIN_5);
/* init_hw powers up the HX711 and auto-tares */

/* Alternative: load_cell_sensor_init(&data) zero-initialises WITHOUT a
 * hardware binding; poll() then returns RESULT_ERR_UNIMPLEMENTED */
```

### Poll Load Cell Sensor

```c
result_t lc_result = poll_load_cell_sensor(&load_cell_data[i]);
```

## Data Access Functions

```c
float force, scale;
float mass;
int32_t counts, tare;
bool valid;

load_cell_get_force_newtons(&cell, &force);
load_cell_get_mass_grams(&cell, &mass);
load_cell_get_raw_counts(&cell, &counts);
load_cell_get_calibration(&cell, &scale, &tare);
load_cell_sensor_is_valid(&cell, &valid);
```

## Calibration Procedure

### Two-Step Calibration

#### Step 1: Tare (Zero Load)

```c
/* With nothing on the cell: average N raw reads and store as zero offset.
 * init_hw already does this automatically at startup. */
load_cell_tare(&cell, 10);
```

#### Step 2: Span (Known Weight)

```c
/* Place a known mass, read raw counts, then:
 *   scale = known_force_newtons / (raw_counts - tare_offset_counts)   */
load_cell_set_scale(&cell, newtons_per_count);   /* sets is_calibrated = true */
```

### Measurement Formulas

```
force_newtons = (raw_counts - tare_offset_counts) × scale_newtons_per_count
mass_grams    = force_newtons / 9.81 × 1000
```

**Note:**<span style="white-space: pre-wrap;"> before calibration the default scale is 1.0 (passthrough): raw\_counts is trustworthy but force\_newtons and mass\_grams are not physical units yet.</span>

## Protobuf Message Format

```protobuf
message SensorBoardLoadCellInfo {
    uint32 sensor_index;            /* 0 or 1 */
    float force_newtons;
    float mass_grams;
    int32 raw_counts;
    float scale_newtons_per_count;
    int32 tare_offset_counts;
    bool is_calibrated;
    SensorState state;
    LoadCellErrorCode error_code;   /* NO_ERROR, COMMUNICATION_FAILURE, INVALID_DATA */
}
```

## Unit Conversions

<table id="bkmrk-load-cell-conv-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>From

</th><th>To

</th><th>Factor

</th></tr><tr><td>Newtons

</td><td>kilograms-force (kgf)

</td><td>÷ 9.81

</td></tr><tr><td>Newtons

</td><td>pounds-force (lbf)

</td><td>÷ 4.448

</td></tr><tr><td>grams

</td><td>kilograms

</td><td>÷ 1000

</td></tr></tbody></table>

## Integration Notes

- Two independent units polled every main loop iteration; each is transmitted in its own envelope with its sensor\_index
- Each sensor maintains separate calibration (tare + scale) and independent error reporting
- HX711 gain/channel is selected by extra SCK pulses after the 24 data bits (1 = channel A gain 128, 2 = channel B gain 32, 3 = channel A gain 64)
- The blocking wait for data-ready can take up to 200 ms per cell per poll; keep this in mind when reducing the loop interval
- A failed read maps to state ERROR with LOAD\_CELL\_COMMUNICATION\_FAILURE; a read with implausible data maps to LOAD\_CELL\_INVALID\_DATA