diff --git a/Inc/faults.h b/Inc/faults.h index d73425922..724ca85ec 100644 --- a/Inc/faults.h +++ b/Inc/faults.h @@ -74,4 +74,34 @@ void faultDesyncEpisodeTick1kHz(void); /* True while a post-desync coast is mandatory (caller must not re-start). */ uint8_t faultDesyncRestartHoldoffActive(void); +/* + * Monotonic since-boot count of stall-rail trips on an ESTABLISHED run - the + * same zero_crosses > 100 gate the episode charge uses, so the many legitimate + * kicks a heavy prop needs at low throttle are not counted as errors. + * + * volatile: read from the main loop (telemetry) while the grind rail that + * forces this trip runs in tenKhzRoutine (ISR context). + */ +extern volatile uint32_t fault_stall_trips; + +/* + * Total hard-error events since boot, for uavcan.equipment.esc.Status + * error_count ("errors since start-up"). + * + * Sums the two PRIMITIVE run-killers, which are mutually exclusive: + * - desync_happened : the jump-desync check in runtimeProcessDesyncCheck + * - fault_stall_trips : the INTERVAL_TIMER stall rail + * + * Deliberately NOT additional addends, because each already funnels into the + * stall rail and would double-count one physical failure: + * - blind-grind rail (faultDesyncEpisodeTick1kHz) kicks INTERVAL_TIMER to + * 46000 so the stall rail is guaranteed to trip on the next main pass + * - blind-step / miss-bucket limit (bemf_zc.c) hands off the same way + * - the episode-rail latch is a CONSEQUENCE of accumulating the above, and + * is a state (ESC_FAULT_STUCK), not an independent event + * Attribution between those sub-causes belongs in the AM32-reserved FlexDebug + * payload, not in this scalar. + */ +uint32_t faultErrorCount(void); + #endif /* FAULTS_H_ */ diff --git a/Mcu/SITL/tests/test_dronecan.py b/Mcu/SITL/tests/test_dronecan.py index ef3d3e090..5a23108ca 100644 --- a/Mcu/SITL/tests/test_dronecan.py +++ b/Mcu/SITL/tests/test_dronecan.py @@ -35,6 +35,7 @@ def test_dronecan_throttle_and_esc_status(sitl_can_factory, state_stream, mcast_ def on_esc(e): status['rpm'] = e.message.rpm status['voltage'] = e.message.voltage + status['errors'] = e.message.error_count status['count'] = status.get('count', 0) + 1 node.add_handler(dronecan.uavcan.equipment.esc.Status, on_esc) @@ -59,6 +60,12 @@ def on_esc(e): assert 3500 <= status.get('rpm', -1) <= 6500, status assert 15 < status.get('voltage', 0) < 18, status assert status.get('count', 0) >= 3, 'too few esc.Status: %s' % status + # error_count aggregates hard-error events (jump desync + stall rail). + # An honest spool-up to a steady 4-6k rpm must report none: the stall + # rail is gated on zero_crosses > 100 precisely so the kicks a normal + # start needs are not counted as errors. + assert status.get('errors', -1) == 0, \ + 'healthy run reported error_count=%s' % status.get('errors') finally: # stop motor for _ in range(10): diff --git a/Src/DroneCAN/DroneCAN.c b/Src/DroneCAN/DroneCAN.c index 5390ee5f7..94472b12c 100644 --- a/Src/DroneCAN/DroneCAN.c +++ b/Src/DroneCAN/DroneCAN.c @@ -14,6 +14,7 @@ # include "signal.h" # include "version.h" # include "eeprom.h" +# include "faults.h" # include # include # include @@ -107,7 +108,6 @@ extern volatile char armed; extern volatile uint32_t commutation_interval; extern uint8_t auto_advance_level; extern uint16_t low_cell_volt_cutoff; -extern uint32_t desync_happened; static uint16_t last_can_input; static uint64_t last_heartbeat_us; @@ -1006,8 +1006,11 @@ static void send_ESCStatus(void) struct uavcan_equipment_esc_Status pkt; uint8_t buffer[UAVCAN_EQUIPMENT_ESC_STATUS_MAX_SIZE]; - // make up some synthetic status data - pkt.error_count = desync_happened; // fill desync count here + /* Hard-error events since boot. Was desync_happened alone, which missed + * every run killed by the stall rail (and so also the blind-grind and + * blind/miss-limit paths that funnel into it) - a grinding motor + * reported error_count 0. See faultErrorCount(). */ + pkt.error_count = faultErrorCount(); pkt.voltage = battery_voltage * 0.01; pkt.current = (current.sum / (float)current.count) * 0.01; diff --git a/Src/faults.c b/Src/faults.c index f70f0ef47..619c76ab0 100644 --- a/Src/faults.c +++ b/Src/faults.c @@ -27,6 +27,14 @@ extern volatile uint16_t zero_input_count; extern volatile uint32_t dma_buffer[64]; extern void resetInputCaptureTimer(void); +/* See the comments on the declarations in faults.h. */ +volatile uint32_t fault_stall_trips = 0; + +uint32_t faultErrorCount(void) +{ + return desync_happened + fault_stall_trips; +} + uint8_t faultHandleStuckRotorIfNeeded(void) { #ifndef BRUSHED_MODE @@ -327,6 +335,12 @@ void faultHandleBemfIntervalStall(void) // this rail is guaranteed to run next pass): blind stepping only // arms at zero_crosses >= 100, so those episodes always charge. if (zero_crosses > 100) { + /* Established run died here. This is the ONLY place the + * stall rail is counted, and it is the aggregation point + * for the grind rail and the blind/miss-limit handoff, + * both of which reach the loop through this trip - see + * faultErrorCount(). */ + fault_stall_trips++; faultDesyncEpisodeCharge(DESYNC_EPISODE_STALL_RAIL); } if (escIsFault()) {