Skip to content

Commit 73637bc

Browse files
authored
Merge pull request #22 from OpenwaterHealth/integration/pdc-deploy-odometer
Integrate per-frame PDC buffer, odometers, and DFU deploy tooling
2 parents 719b94f + a709055 commit 73637bc

26 files changed

Lines changed: 2655 additions & 18 deletions

CMakeLists.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE
118118
Core/Src/motion_config.c
119119
Core/Src/fpga_register_map.c
120120
Core/Src/utils.c
121+
Core/Src/pdc_buffer.c
122+
Core/Src/pdc_poll.c
123+
Core/Src/odometer.c
121124
)
122125

123126
# Add include paths
@@ -147,3 +150,22 @@ add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
147150
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${CMAKE_PROJECT_NAME}> ${CMAKE_PROJECT_NAME}.bin
148151
COMMENT "Generating .hex and .bin files"
149152
)
153+
154+
# Build + DFU-flash target. Invokes scripts/deploy.py which:
155+
# 1. enters DFU via the omotion SDK
156+
# 2. waits for the STM32 ROM bootloader to enumerate
157+
# 3. flashes the .bin via dfu-util with :leave for auto-reset
158+
# Requires: dfu-util on PATH, omotion installed in the active Python env.
159+
find_program(PYTHON_FOR_DEPLOY NAMES python3 python)
160+
if(PYTHON_FOR_DEPLOY)
161+
add_custom_target(flash
162+
COMMAND ${PYTHON_FOR_DEPLOY} ${CMAKE_SOURCE_DIR}/scripts/deploy.py
163+
--config $<CONFIG> --no-build
164+
DEPENDS ${CMAKE_PROJECT_NAME}
165+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
166+
USES_TERMINAL
167+
COMMENT "Flashing ${CMAKE_PROJECT_NAME} via DFU"
168+
)
169+
else()
170+
message(WARNING "Python not found — `flash` target will not be available")
171+
endif()

CommandHandling.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ Examples include:
140140
| `OW_CTRL_TEC_STATUS` | Read TEC controller status and faults |
141141
| `OW_CTRL_BOARDID` | Read board identification information |
142142
| `OW_CTRL_PDUMON` | Read power distribution monitoring data |
143+
| `OW_CTRL_GET_PDC_BUFFER` | Drain up to N per-frame PDC samples from the SRAM ring buffer. Request payload: 1 byte = max_samples (1..64). Response: 2-byte LE drop counter + 1-byte sample count + N × 7-byte packed `{u32 frame_idx, u16 pdc_raw, u8 flags}`. `flags` bit 0 = `dark_slot`. |
143144

144145
Each command defines its own payload format and response payload.
145146

Core/Inc/common.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ typedef enum {
110110
OW_CTRL_TEC_STATUS = 0x22,
111111
OW_CTRL_BOARDID = 0x23,
112112
OW_CTRL_PDUMON = 0x24,
113+
OW_CTRL_GET_PDC_BUFFER = 0x25,
114+
OW_CTRL_GET_SYSTEM_ODO = 0x26,
115+
OW_CTRL_GET_LASER_ODO = 0x27,
116+
OW_CTRL_RESET_ODO = 0x28,
113117
} MotionControllerCommands;
114118

115119
typedef enum {

Core/Inc/memory_map.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,14 @@
3535
#define ADDR_FLASH_SECTOR_6_BANK2 ((uint32_t)0x081C0000) /* Base @ of Sector 6, 128 Kbytes */
3636
#define ADDR_FLASH_SECTOR_7_BANK2 ((uint32_t)0x081E0000) /* Base @ of Sector 7, 128 Kbytes */
3737

38-
#define ADDR_FLASH_END_ADDRESS ((uint32_t)0x08200000)
39-
#define FLASH_USER_START_ADDR ADDR_FLASH_SECTOR_7_BANK2
38+
#define ADDR_FLASH_END_ADDRESS ((uint32_t)0x08200000)
39+
40+
/* Reserved persistent-data sectors. Each persists via full-sector erase
41+
* (the STM32H7 flash sector is 128 KB and that is the minimum erase
42+
* granularity), so two distinct tenants need two distinct sectors. The
43+
* linker script reserves both by capping FLASH at 1792 KB. */
44+
#define FLASH_USER_START_ADDR ADDR_FLASH_SECTOR_7_BANK2 /* motion_config / calibration */
45+
#define FLASH_ODOMETER_START_ADDR ADDR_FLASH_SECTOR_6_BANK2 /* system + laser odometer */
4046

4147
#ifdef __cplusplus
4248
}

Core/Inc/odometer.h

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* odometer.h
3+
*
4+
* Created on: Apr 29, 2026
5+
* Author: Claude
6+
*/
7+
8+
#ifndef INC_ODOMETER_H_
9+
#define INC_ODOMETER_H_
10+
11+
#include "stm32h7xx_hal.h"
12+
#include "memory_map.h"
13+
#include <stdint.h>
14+
#include <stdbool.h>
15+
16+
/* Flash memory addresses for odometer storage.
17+
*
18+
* Lives in its own 128 KB sector (sector 6 bank 2 via FLASH_ODOMETER_START_ADDR),
19+
* distinct from FLASH_USER_START_ADDR which is owned by motion_config. The
20+
* STM32H7 minimum erase granularity is one 128 KB sector, so the two stores
21+
* must not co-tenant — otherwise either Odometer_Persist_Both() or
22+
* motion_cfg flash writes would wipe the other tenant. */
23+
#define ODOMETER_FLASH_BASE_ADDR (FLASH_ODOMETER_START_ADDR)
24+
#define SYSTEM_ODO_FLASH_ADDR (ODOMETER_FLASH_BASE_ADDR)
25+
#define LASER_ODO_FLASH_ADDR (ODOMETER_FLASH_BASE_ADDR + 32)
26+
27+
/* Magic number identifying a valid odometer flash block. Reading anything other
28+
* than this magic + matching CRC at boot means the block is invalid (uninitialised
29+
* flash, stale data from a previous firmware that used this sector for something
30+
* else, partial-erase corruption) — the init path treats that as "first boot"
31+
* and writes a fresh zero block. */
32+
#define ODOMETER_MAGIC 0x4F444F31u /* "ODO1" */
33+
34+
/* On-flash format. 16 bytes per odometer; written as a 32-byte zero-padded block. */
35+
typedef struct __attribute__((packed)) {
36+
uint32_t magic; /* ODOMETER_MAGIC */
37+
uint32_t value; /* total_minutes for system, total_pulses for laser */
38+
uint32_t reserved; /* 0 for now, future fields */
39+
uint32_t crc32; /* CRC32 of magic + value + reserved */
40+
} OdoFlashBlock_t;
41+
42+
/* Update interval: how often we erase+rewrite the flash sector during normal
43+
* runtime to persist the system odometer. STM32H7 flash sector endurance is
44+
* ~10K cycles, so we keep this conservative — 6 hours = ~4 writes/day, giving
45+
* roughly 7 years of continuous-run wear life per device. The laser odometer
46+
* is only persisted on scan-finish (a natural rate-limiter), so it doesn't
47+
* need its own interval. */
48+
#define SYSTEM_ODO_UPDATE_INTERVAL_MS (6UL * 60UL * 60UL * 1000UL) /* 6 hours */
49+
50+
/* Reset targets for Odometer_Reset() / OW_CTRL_RESET_ODO. */
51+
typedef enum {
52+
ODO_RESET_SYSTEM = 0,
53+
ODO_RESET_LASER = 1,
54+
ODO_RESET_BOTH = 2,
55+
} OdoResetTarget;
56+
57+
/* In-memory state — kept opaque, accessed via Odometer_Get_*() */
58+
typedef struct {
59+
uint32_t total_minutes; /* Total accumulated minutes */
60+
uint32_t last_update_tick; /* Last HAL_GetTick() value when saved */
61+
} SystemOdometer_t;
62+
63+
typedef struct {
64+
uint32_t total_pulses; /* Total laser sync pulses */
65+
uint32_t scan_start_pulses; /* Pulse count at scan start */
66+
} LaserOdometer_t;
67+
68+
/* Function prototypes */
69+
HAL_StatusTypeDef Odometer_Init(void);
70+
HAL_StatusTypeDef Odometer_Update_System(void);
71+
HAL_StatusTypeDef Odometer_Scan_Start(void);
72+
HAL_StatusTypeDef Odometer_Scan_Finish(void);
73+
uint32_t Odometer_Get_System_Minutes(void);
74+
uint32_t Odometer_Get_Laser_Pulses(void);
75+
76+
/* Reset one or both odometers to zero and persist the cleared state to flash.
77+
* target ∈ ODO_RESET_SYSTEM / _LASER / _BOTH. Returns HAL_OK on success. */
78+
HAL_StatusTypeDef Odometer_Reset(OdoResetTarget target);
79+
80+
#endif /* INC_ODOMETER_H_ */

Core/Inc/pdc_buffer.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* Core/Inc/pdc_buffer.h */
2+
#ifndef INC_PDC_BUFFER_H_
3+
#define INC_PDC_BUFFER_H_
4+
5+
#include <stdint.h>
6+
#include <stdbool.h>
7+
#include <stddef.h>
8+
9+
#define PDC_BUFFER_CAPACITY 256
10+
11+
typedef struct __attribute__((packed)) {
12+
uint32_t frame_idx;
13+
uint16_t pdc_raw;
14+
uint8_t flags; /* bit 0 = dark_slot */
15+
} pdc_sample_t;
16+
17+
#define PDC_FLAG_DARK_SLOT (1u << 0)
18+
19+
void pdc_buffer_reset(void);
20+
bool pdc_buffer_push(const pdc_sample_t *sample); /* drop-oldest on overflow, returns true if a drop occurred */
21+
size_t pdc_buffer_drain(pdc_sample_t *out, size_t max_samples);
22+
uint16_t pdc_buffer_dropped_since_last_drain(void);
23+
size_t pdc_buffer_count(void);
24+
25+
/* Add external drops (e.g. producer-side ISR overflow) to the pending counter
26+
* so they surface through the same drained-drop-count channel. */
27+
void pdc_buffer_account_drops(uint16_t n);
28+
29+
#endif

Core/Inc/pdc_poll.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/* Core/Inc/pdc_poll.h */
2+
#ifndef INC_PDC_POLL_H_
3+
#define INC_PDC_POLL_H_
4+
5+
#include <stdint.h>
6+
7+
void pdc_poll_init(void);
8+
/* Call from the main loop. Performs the I2C read of the safety FPGA peak-power
9+
* register and pushes a pdc_sample_t to the buffer when one is pending and the
10+
* post-pulse settling time has elapsed. Non-blocking otherwise. */
11+
void pdc_poll_tick(void);
12+
13+
#endif

Core/Inc/trigger.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ void FSYNC_DelayElapsedCallback(TIM_HandleTypeDef *htim);
4545
void LSYNC_DelayElapsedCallback(TIM_HandleTypeDef *htim);
4646

4747
void FSYNC_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
48+
void LSYNC_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
49+
bool get_current_slot_is_dark(void);
50+
bool consume_pdc_sample_pending(bool *out_dark_slot, uint32_t *out_frame_idx);
51+
uint16_t consume_pdc_pending_overwrites(void);
4852

4953
extern Trigger_Config_t trigger_config;
5054

Core/Src/if_commands.c

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#include "led_driver.h"
2121
#include "motion_config.h"
2222
#include "msg_queue.h"
23+
#include "pdc_buffer.h"
24+
#include "odometer.h"
2325

2426
#include <string.h>
2527

@@ -114,6 +116,8 @@ static uint8_t i2c_list[10] = {0};
114116
static uint8_t i2c_data[0xff] = {0};
115117
static uint32_t last_fsync_count = 0;
116118
static uint32_t last_lsync_count = 0;
119+
static uint32_t last_system_odo = 0;
120+
static uint32_t last_laser_odo = 0;
117121

118122
static float tecadc_last_volts[4];
119123
static uint16_t tecadc_last_raw[4];
@@ -497,6 +501,68 @@ static _Bool process_controller_command(UartPacket *uartResp, UartPacket *cmd)
497501
uartResp->data_len = (uint16_t)sizeof(pdu_frame);
498502
uartResp->data = pdu_frame.bytes;
499503
break;
504+
case OW_CTRL_GET_PDC_BUFFER: {
505+
uartResp->command = OW_CTRL_GET_PDC_BUFFER;
506+
uartResp->addr = cmd->addr;
507+
uartResp->reserved = cmd->reserved;
508+
509+
/* Request payload: 1 byte = max_samples (clamped to 64). */
510+
uint8_t requested = (cmd->data_len >= 1) ? cmd->data[0] : 64;
511+
if (requested == 0 || requested > 64) requested = 64;
512+
513+
/* Response layout: [dropped:u16 LE][count:u8][count * sizeof(pdc_sample_t)] */
514+
static pdc_sample_t drained[64];
515+
size_t n = pdc_buffer_drain(drained, requested);
516+
uint16_t dropped = pdc_buffer_dropped_since_last_drain();
517+
518+
static uint8_t resp[3 + 64 * sizeof(pdc_sample_t)];
519+
resp[0] = (uint8_t)(dropped & 0xFF);
520+
resp[1] = (uint8_t)((dropped >> 8) & 0xFF);
521+
resp[2] = (uint8_t)n;
522+
memcpy(&resp[3], drained, n * sizeof(pdc_sample_t));
523+
524+
uartResp->data_len = (uint16_t)(3 + n * sizeof(pdc_sample_t));
525+
uartResp->data = resp;
526+
} break;
527+
case OW_CTRL_GET_SYSTEM_ODO:
528+
uartResp->command = OW_CTRL_GET_SYSTEM_ODO;
529+
uartResp->addr = cmd->addr;
530+
uartResp->reserved = cmd->reserved;
531+
uartResp->data_len = 4;
532+
last_system_odo = Odometer_Get_System_Minutes();
533+
uartResp->data = (uint8_t *)&last_system_odo;
534+
break;
535+
case OW_CTRL_GET_LASER_ODO:
536+
uartResp->command = OW_CTRL_GET_LASER_ODO;
537+
uartResp->addr = cmd->addr;
538+
uartResp->reserved = cmd->reserved;
539+
uartResp->data_len = 4;
540+
last_laser_odo = Odometer_Get_Laser_Pulses();
541+
uartResp->data = (uint8_t *)&last_laser_odo;
542+
break;
543+
case OW_CTRL_RESET_ODO: {
544+
uartResp->command = OW_CTRL_RESET_ODO;
545+
uartResp->addr = cmd->addr;
546+
uartResp->reserved = cmd->reserved;
547+
/* Request payload: 1 byte = target (0=system, 1=laser, 2=both).
548+
* No payload defaults to "both" for caller convenience. */
549+
OdoResetTarget target = ODO_RESET_BOTH;
550+
if (cmd->data_len >= 1) {
551+
if (cmd->data[0] > (uint8_t)ODO_RESET_BOTH) {
552+
uartResp->data_len = 0;
553+
uartResp->packet_type = OW_ERROR;
554+
break;
555+
}
556+
target = (OdoResetTarget)cmd->data[0];
557+
}
558+
static uint8_t reset_resp;
559+
reset_resp = (Odometer_Reset(target) == HAL_OK) ? 0 : 1;
560+
uartResp->data_len = 1;
561+
uartResp->data = &reset_resp;
562+
if (reset_resp != 0) {
563+
uartResp->packet_type = OW_ERROR;
564+
}
565+
} break;
500566
default:
501567
uartResp->data_len = 0;
502568
uartResp->packet_type = OW_UNKNOWN;

Core/Src/main.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "usbd_cdc_if.h"
2929
#include "uart_comms.h"
3030
#include "trigger.h"
31+
#include "pdc_poll.h"
3132
#include "led_driver.h"
3233
#include "tca9548a.h"
3334
#include "pca9535.h"
@@ -44,6 +45,7 @@
4445
#include "jsmn.h"
4546
#include "utils.h"
4647
#include "msg_queue.h"
48+
#include "odometer.h"
4749

4850
#include <stdio.h>
4951
#include <string.h>
@@ -562,13 +564,20 @@ int main(void)
562564
motion_cfg_get();
563565
motion_cfg_apply_settings();
564566

567+
// Initialize odometers
568+
printf("Initialize odometers\r\n");
569+
if(Odometer_Init() != HAL_OK){
570+
printf("Failed to initialize odometers\r\n");
571+
}
572+
565573
HAL_Delay(100);
566574

567575
// Enable USB HUB
568576
HAL_GPIO_WritePin(HUB_RESET_GPIO_Port, HUB_RESET_Pin, GPIO_PIN_SET);
569577
HAL_Delay(100);
570578

571579
comms_init();
580+
pdc_poll_init();
572581
/* Start TIM4 interrupt for telemetry polling (250 ms) */
573582
HAL_TIM_Base_Start_IT(&htim4);
574583

@@ -584,6 +593,8 @@ int main(void)
584593
comms_process();
585594
telemetry_poll();
586595
usb_recovery_task();
596+
pdc_poll_tick();
597+
Odometer_Update_System();
587598
HAL_Delay(1);
588599
}
589600
/* USER CODE END 3 */
@@ -1742,6 +1753,9 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
17421753
if (htim->Instance == FSYNC_TIMER.Instance) {
17431754
FSYNC_PeriodElapsedCallback(htim);
17441755
}
1756+
if (htim->Instance == LASER_TIMER.Instance) {
1757+
LSYNC_PeriodElapsedCallback(htim);
1758+
}
17451759

17461760
if (htim->Instance == TIM12) {
17471761
CDC_Idle_Timer_Handler();

0 commit comments

Comments
 (0)