diff --git a/.gitignore b/.gitignore index 255c946..a2dc304 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ cooling_tests # Editor folders .vscode/ + +# MATLAB analysis outputs (regenerated locally) +results/ diff --git a/Makefile b/Makefile index 138c151..63ee538 100644 --- a/Makefile +++ b/Makefile @@ -29,3 +29,9 @@ test: $(TST_BIN) clean: > rm -f $(SIM_BIN) $(TST_BIN) *.exe *.o *.obj *.gcda *.gcno *.gcov + +# Export the deterministic C simulation as data for MATLAB. +.PHONY: trace +trace: $(SIM_BIN) +> mkdir -p results +> ./$(SIM_BIN) --csv results/cooling_trace.csv diff --git a/README.md b/README.md index ae92380..9cee018 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,36 @@ A new invalid reading during recovery restarts the counter. The HIGH fallback is The simulation prints the step, heating rate, measured temperature, sensor status, fan state, PWM command, and fault indicator. The thermal model is intentionally simple; it is not a physically calibrated engine model. +## MATLAB-based validation (R2024a) + +The C simulation can export a 20-step trace for independent checking and +visualization in MATLAB. The C controller is unchanged; MATLAB serves as an +offline reference model, not as an ECU runtime component. + +1. From **Git Bash** at the repository root, run `make trace`. This generates + `results/cooling_trace.csv` (ignored by Git). +2. In **MATLAB**, change to the repository folder and run: + + ```matlab + addpath('matlab') + analyze_cooling + ``` + +The MATLAB script reads the CSV, calculates a separate FSM/fault-recovery +reference, checks every recorded state and output, and saves a four-panel +figure to `results/cooling_analysis.png`. MATLAB R2024a is supported; no +Simulink or other add-on is required for this workflow. + +The reference model checks the supplied 20-step test scenario, not all +possible input sequences. This is **offline cross-checking**, not real-time +co-simulation, hardware-in-the-loop testing, or functional-safety evidence. + +### MATLAB validation results + +The following plots show temperature evolution, injected sensor faults, PWM commands, and fault recovery. + +![Engine Cooling Controller - MATLAB Validation](docs/cooling_analysis.png) + ## Project structure ```text @@ -165,7 +195,3 @@ This is a **PC-based educational simulation**, not a production-ready ECU implem - Calls to `cooling_update()` are simulated steps, not a real-time scheduler with timing guarantees. - No CAN communication, diagnostics protocol, or physical fan-failure monitoring is implemented. - The project does not implement or claim AUTOSAR or ISO 26262 compliance. - -## License - -MIT — see [LICENSE](LICENSE). diff --git a/docs/cooling_analysis.png b/docs/cooling_analysis.png new file mode 100644 index 0000000..c0c0a79 Binary files /dev/null and b/docs/cooling_analysis.png differ diff --git a/matlab/analyze_cooling.m b/matlab/analyze_cooling.m new file mode 100644 index 0000000..8138d3b --- /dev/null +++ b/matlab/analyze_cooling.m @@ -0,0 +1,139 @@ +function analyze_cooling(csvPath) +%ANALYZE_COOLING Plot and independently check Engine Cooling Controller output. +% Compatible with MATLAB R2024a; no optional toolboxes required. +% Run from the repository root: +% addpath('matlab'); analyze_cooling + + root = fileparts(fileparts(mfilename('fullpath'))); + if nargin < 1 + csvPath = fullfile(root, 'results', 'cooling_trace.csv'); + end + assert(isfile(csvPath), ... + 'CSV not found. In Git Bash, run: make trace'); + data = readtable(csvPath); + + required = {'step','heat_rate_c_per_step','plant_temp_c', ... + 'measured_temp_c','signal_valid','fan_state', ... + 'pwm_pct','sensor_fault','fault_led','valid_streak'}; + assert(all(ismember(required, data.Properties.VariableNames)), ... + 'CSV is missing one or more expected columns.'); + assert(height(data) == 20 && isequal(data.step, (1:20)'), ... + 'Expected 20 consecutive simulation steps.'); + + % Build the expected result separately from the C implementation. + [refState, refFault, refStreak] = reference_controller( ... + data.measured_temp_c, data.signal_valid); + assert(isequal(data.fan_state, refState), ... + 'C state sequence differs from the MATLAB reference model.'); + assert(isequal(data.sensor_fault, refFault), ... + 'C sensor fault flags differ from the MATLAB reference model.'); + assert(isequal(data.valid_streak, refStreak), ... + 'C recovery counters differ from the MATLAB reference model.'); + assert(all(data.pwm_pct == 50 .* data.fan_state), ... + 'PWM does not match OFF/LOW/HIGH mapping.'); + assert(isequal(data.fault_led, data.sensor_fault), ... + 'Fault LED must match sensor_fault.'); + assert(any(data.signal_valid == 0) && ... + any(data.measured_temp_c < -40 | data.measured_temp_c > 150), ... + 'Both signal and range fault injections must be exercised.'); + assert(all(data.fan_state(data.sensor_fault == 1) == 2), ... + 'Sensor fault must command HIGH cooling.'); + + fprintf('[PASS] 20 C states match independent MATLAB FSM\n'); + fprintf('[PASS] Sensor fault flags and three-sample recovery match\n'); + fprintf('[PASS] PWM stages and fault LED agree with controller outputs\n'); + fprintf('[PASS] Both fault injection scenarios are present\n'); + + fig = figure('Name','Engine Cooling Controller Validation', ... + 'Color','w','Position',[100 80 1150 900]); + layout = tiledlayout(fig,4,1,'TileSpacing','compact','Padding','compact'); + title(layout,'Engine Cooling Controller | C trace validated by MATLAB'); + + nexttile; + plot(data.step,data.plant_temp_c,'-o','LineWidth',1.5,'MarkerSize',4); + hold on; + yline(95,':','LOW OFF 95 C'); + yline(100,'--','LOW ON 100 C'); + yline(105,':','HIGH OFF 105 C'); + yline(110,'--','HIGH ON 110 C'); + ylabel('Plant temp (C)'); grid on; + title('Actual simulated plant temperature (not the injected sensor reading)'); + + nexttile; + plot(data.step,data.measured_temp_c,'-o','LineWidth',1.4,'MarkerSize',4); + hold on; + bad = data.signal_valid == 0 | ... + data.measured_temp_c < -40 | data.measured_temp_c > 150; + scatter(data.step(bad),data.measured_temp_c(bad),70,'r','filled'); + ylabel('Sensor (C)'); grid on; + title('Measurement, including injected faults at steps 12 and 13'); + + nexttile; + stairs(data.step,data.pwm_pct,'LineWidth',2); + ylim([-10 110]); yticks([0 50 100]); + ylabel('PWM (%)'); grid on; + title('Simulated PWM command (0 / 50 / 100 percent)'); + + nexttile; + stairs(data.step,data.sensor_fault,'LineWidth',2); + ylim([-0.15 1.15]); yticks([0 1]); + ylabel('Fault'); xlabel('Simulation step'); grid on; + title('Sensor fault flag and recovery'); + + output = fullfile(root,'results','cooling_analysis.png'); + exportgraphics(layout, output, 'Resolution', 160); + fprintf('[PASS] Plot saved: %s\n', output); +end + +function [states, faults, streaks] = reference_controller(temps, signalValid) +% Independent reference behavior based on the documented requirements. + n = numel(temps); + states = zeros(n,1); faults = zeros(n,1); streaks = zeros(n,1); + current = 0; fault = false; streak = 0; + + for k = 1:n + temp = temps(k); + good = signalValid(k) ~= 0 && temp >= -40 && temp <= 150; + if ~good + current = 2; fault = true; streak = 0; + else + if fault + streak = streak + 1; + if streak < 3 + current = 2; + states(k) = current; + faults(k) = 1; + streaks(k) = streak; + continue; + end + fault = false; + streak = 0; + end + switch current + case 0 % OFF + if temp >= 110 + current = 2; + elseif temp >= 100 + current = 1; + end + case 1 % LOW + if temp >= 110 + current = 2; + elseif temp <= 95 + current = 0; + end + case 2 % HIGH + if temp <= 95 + current = 0; + elseif temp <= 105 + current = 1; + end + otherwise + error('Unexpected reference state'); + end + end + states(k) = current; + faults(k) = double(fault); + streaks(k) = streak; + end +end diff --git a/src/main.c b/src/main.c index 3ffca06..4d2aea6 100644 --- a/src/main.c +++ b/src/main.c @@ -1,84 +1,144 @@ #include +#include #include "cooling_controller.h" -typedef struct { +typedef struct +{ uint8_t pwm_duty_pct; bool fault_led_on; } SimHardwareState; -static void sim_set_fan_pwm(uint8_t duty_pct, void *user_ctx) { +static void sim_set_fan_pwm(uint8_t duty_pct, void *user_ctx) +{ SimHardwareState *hw = (SimHardwareState *)user_ctx; - if (hw != NULL) { + if (hw != NULL) + { hw->pwm_duty_pct = duty_pct; } } -static void sim_set_fault_led(bool fault_active, void *user_ctx) { +static void sim_set_fault_led(bool fault_active, void *user_ctx) +{ SimHardwareState *hw = (SimHardwareState *)user_ctx; - if (hw != NULL) { + if (hw != NULL) + { hw->fault_led_on = fault_active; } } -static const char *sensor_status_label(int16_t temp_c, bool sensor_ok) { - if (!sensor_ok) { +static const char *sensor_status_label(int16_t temp_c, bool sensor_ok) +{ + if (!sensor_ok) + { return "SIG_ERR"; } - if (temp_c < COOLING_SENSOR_MIN_C || temp_c > COOLING_SENSOR_MAX_C) { + if (temp_c < COOLING_SENSOR_MIN_C || temp_c > COOLING_SENSOR_MAX_C) + { return "RNG_ERR"; } return "OK"; } -int main(void) { +int main(int argc, const char *const argv[]) +{ + FILE *csv = NULL; + + if (argc != 1) + { + if (argc != 3 || strcmp(argv[1], "--csv") != 0) + { + fprintf(stderr, "Usage: %s [--csv output_file.csv]\n", argv[0]); + return 1; + } + + csv = fopen(argv[2], "w"); + if (csv == NULL) + { + perror("Could not open CSV output file"); + return 1; + } + + fprintf(csv, "step,heat_rate_c_per_step,plant_temp_c,measured_temp_c," + "signal_valid,fan_state,pwm_pct,sensor_fault,fault_led,valid_streak\n"); + } + CoolingConfig config = { .low_on_c = 100, .low_off_c = 95, .high_on_c = 110, - .high_off_c = 105 - }; + .high_off_c = 105}; + + SimHardwareState hw_state = { + .pwm_duty_pct = 0U, + .fault_led_on = false}; - SimHardwareState hw_state = { .pwm_duty_pct = 0U, .fault_led_on = false }; CoolingHal hal = { .set_fan_pwm = sim_set_fan_pwm, .set_fault_led = sim_set_fault_led, - .user_ctx = &hw_state - }; + .user_ctx = &hw_state}; CoolingController controller; - if (cooling_init(&controller, &config) != COOLING_OK) { + + if (cooling_init(&controller, &config) != COOLING_OK) + { fprintf(stderr, "Failed to initialize controller\n"); + if (csv != NULL) + { + fclose(csv); + } + return 1; + } + + if (cooling_set_hal(&controller, &hal) != COOLING_OK) + { + fprintf(stderr, "Failed to install HAL callbacks\n"); + if (csv != NULL) + { + fclose(csv); + } return 1; } - cooling_set_hal(&controller, &hal); double plant_temp_c = 88.0; + printf("Step | Load (C/s) | Temp (C) | Sensor | Fan State | PWM | Fault LED\n"); printf("-----+------------+----------+---------+-----------+------+----------\n"); - for (int step = 1; step <= 20; step++) { + for (int step = 1; step <= 20; step++) + { double heat_rate = 2.5; bool sensor_ok = true; - if (step >= 7 && step <= 11) { + if (step >= 7 && step <= 11) + { heat_rate = 4.0; - } else if (step >= 15) { + } + else if (step >= 15) + { heat_rate = 0.8; } double cooling_rate = 0.0; - if (cooling_get_state(&controller) == FAN_LOW) { + + if (cooling_get_state(&controller) == FAN_LOW) + { cooling_rate = 2.2; - } else if (cooling_get_state(&controller) == FAN_HIGH) { + } + else if (cooling_get_state(&controller) == FAN_HIGH) + { cooling_rate = 4.8; } plant_temp_c += (heat_rate - cooling_rate); + int16_t measured_temp = (int16_t)(plant_temp_c + 0.5); - if (step == 12) { + if (step == 12) + { sensor_ok = false; - } else if (step == 13) { + } + else if (step == 13) + { measured_temp = 185; } @@ -90,9 +150,40 @@ int main(void) { measured_temp, sensor_status_label(measured_temp, sensor_ok), cooling_state_name(state), - hw_state.pwm_duty_pct, + (unsigned int)hw_state.pwm_duty_pct, hw_state.fault_led_on ? "ON" : "OFF"); + + if (csv != NULL) + { + if (fprintf(csv, "%d,%.2f,%.2f,%d,%d,%d,%u,%d,%d,%u\n", + step, + heat_rate, + plant_temp_c, + (int)measured_temp, + sensor_ok ? 1 : 0, + (int)state, + (unsigned int)hw_state.pwm_duty_pct, + controller.sensor_fault ? 1 : 0, + hw_state.fault_led_on ? 1 : 0, + (unsigned int)controller.valid_streak) < 0) + { + perror("Could not write CSV trace"); + fclose(csv); + return 1; + } + } + } + + if (csv != NULL) + { + if (fclose(csv) != 0) + { + perror("Could not close CSV trace"); + return 1; + } + + printf("CSV trace written to %s\n", argv[2]); } return 0; -} +} \ No newline at end of file