Architecture
Complete system overview: FreeRTOS task model, the sensor polling loop, protobuf encoding, UDP transmission, inbound packet dispatch, and the memory layout. All application logic lives in a single FreeRTOS task (MainTask) defined in src/sensor_board/main.c.
Initialization Sequence
Phase 1: Hardware Setup (init_board, before the kernel starts)
void init_board() {
MPU_Config_wrapper();
SCB_EnableICache();
SCB_EnableDCache();
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
/* NOTE: no threads here, kernel not initialized yet.
* osKernelInitialize() is called by cubemx_main.c afterwards. */
}
Phase 2: Driver Initialization (start of MainTask)
- BSP LEDs (Green, Blue, Red)
- Logging over the ST-Link VCP UART (LOG_init(&hcom_uart[COM1]), 115200 baud)
- IMU (imu_sensor_init)
- pH sensor (ph_sensor_init with 3.3 V reference)
- Two HX711 load cells with their GPIO map (PA5/PC7 and PA6/PB5), each powered up and auto-tared
- Two pressure/FSR sensors (pressure_sensor_init)
- Flow sensor (flow_sensor_init, pulse counting via EXTI callback)
- Pump (pump_init on TIM3 CH3 PWM, then commanded to 50 percent and enabled as a startup default)
Phase 3: Communication Setup
- ETH_init with static IP 192.168.0.111, netmask 255.255.255.0, gateway 192.168.0.1 and a link status callback that re-adds the static ARP entry when the link comes up
- MAC address filtering for three allowed source MACs (ETH_setup_MAC_address_filtering)
- Two statically allocated prioritised UDP transmit queues (80 entries each)
- Packet dispatcher registration for five inbound message types (pH, IMU, load cell, pressure, pump)
- ETH_udp_init(2, send_queues, DispatchPacket) and a static ARP entry for the destination board 192.168.0.222
Phase 4: Main Loop
The loop runs forever with a 5000 ms period (MAIN_TASK_DELAY_MS). Each iteration:
- Read free heap; if below 4096 bytes log CRITICAL and sleep 10 s instead of polling
- Toggle the three LEDs (visual heartbeat)
- Build a SensorBoardDiagnostics struct (state OPERATING)
- Poll pH, IMU, then load cells and pressure sensors (index loop over both units)
- Poll the flow sensor (rate computed from pulses counted by the EXTI ISR since the last poll)
- Build the pump status from commanded state, cross-checked against measured flow
- Wrap each sensor message in a PBEnvelope and send it as a UDP datagram to the sample board
- osDelay(MAIN_TASK_DELAY_MS)
Protobuf Encoding and UDP Transmission
Every outbound message is one PBEnvelope with a oneof payload. Encoding uses nanopb via pb_message_encode, which allocates a heap buffer that is freed after sending.
static void udp_send_envelope(uint8_t dest_ip[4], PBEnvelope *env) {
if (!sendUDP) { return; } /* transmit gate, false by default */
uint8_t *encoded = NULL;
size_t size = 0;
result_t result = pb_message_encode(env, PBEnvelope_fields, &encoded, &size);
if (result == RESULT_OK) {
ETH_udp_send(dest_ip, PORT, encoded, (uint16_t)size, 1);
}
free(encoded);
}
Important: the static flag sendUDP in main.c is currently false, so envelope encoding and transmission are skipped entirely. Set it to true to actually transmit. There is a similar development flag skip_sensor_polling (currently false) that disables all sensor polling when true.
Inbound Packet Dispatcher
Received UDP packets are decoded by the shared packet dispatcher (components/common/packet_dispatcher). Handlers are registered with PACKET_HANDLER_CONFIG_STATIC per envelope payload tag:
Envelope tag | Handler | Behavior |
|---|---|---|
ph_info | handle_sensor_ph_info | Log only |
imu_info | handle_sensor_imu_info | Log only |
load_cell_info | handle_sensor_load_cell_info | Log only |
pressure_info | handle_sensor_pressure_info | Log only |
pump_info | handle_sensor_pump_command | Actuates the pump: applies enabled, direction, and speed_percent to the hardware |
Sensor Status Model
SensorState (operating state)
Code | Meaning |
|---|---|
SENSOR_IDLE | Not connected, not implemented, or intentionally off |
SENSOR_OPERATING | Normal operation, valid data |
SENSOR_ERROR | Communication failure or invalid data |
SensorStatus (connection status)
Code | Meaning |
|---|---|
STATUS_OK | Healthy |
STATUS_DISCONNECTED | No hardware detected (poll returned UNIMPLEMENTED or COMMS) |
STATUS_ERROR | Unexpected failure |
STATUS_INITIALIZING | Warming up (flow sensor first sample window) |
Poll Result Mapping
handle_sensor_poll_result() maps driver results uniformly:
- RESULT_ERR_UNIMPLEMENTED or RESULT_ERR_COMMS: state IDLE, status DISCONNECTED (sensor not connected or driver not wired to hardware yet)
- Any other non-OK result: state ERROR, status ERROR
- RESULT_OK: caller then validates the data and picks OPERATING or ERROR plus a driver-specific error code
Every sensor produces one uniform log line per loop: name | STATUS | STATE | detail.
Pump Health Cross-Check
The pump is an open loop actuator (no current sense or fault line), so firmware cannot directly detect a connected pump. The only on-board proof that fluid is moving is the inline flow sensor, so the main loop derives pump status from it:
- Not initialised: ERROR / ERROR
- Commanded off (disabled or 0 percent): IDLE / OK (healthy, intentionally off)
- Commanded on and flow detected: OPERATING / OK
- Commanded on and no flow: OPERATING / DISCONNECTED (pump absent, dry, stalled, or the flow sensor is not installed)
Memory Layout
Region | Use |
|---|---|
FreeRTOS heap, 64 KB | Task stacks, queues, protobuf encode buffers (malloc/free per message) |
0x30000000, 32 KB, MPU non-cacheable | Ethernet DMA descriptors and buffers |
0x30004900, 16 KB | LwIP RAM heap (MEM_SIZE) |
Static queues | Two UDP send queues, 80 entries each, allocated at compile time (xQueueCreateStatic) |
Error Handling Strategy
- Drivers never crash the loop: missing hardware degrades to IDLE/DISCONNECTED and the loop continues
- Each sensor unit reports independently (separate envelope, separate error code enum)
- Heap exhaustion protection: below 4096 bytes free, polling pauses for 10 s per iteration
- Encode failures are logged with result_to_short_str / result_to_desc_str and the buffer is freed in all paths
No comments to display
No comments to display