Initial commit: demo-epb v1.0 — Elektrische Parkbremse Demo

Vollstaendige Demo des slohmaier Dev Process anhand einer EPB-Steuergeraet-
Software. Zeigt ASPICE 4.0 / ISO 26262-konforme Entwicklung im Monorepo.

Inhalte:
- 5 Plaene (PID, PM-, QA-, SWE-, Test-Plan) in Word, ausgefuellt mit
  EPB-spezifischen Inhalten
- 10 System-Anforderungen + 25 Software-Anforderungen (Doorstop-MD)
- 5 System-Architektur-Elemente + 10 Software-Architektur-Elemente
  mit PlantUML-Diagrammen und vollstaendigem Mapping
- 3 implementierte Komponenten (Apply Controller D, Actuator Driver B,
  Switch Debouncer QM) plus 7 Header-Stubs
- 28 Unit-Tests, alle gruen, mit Coverage- und MISRA-Build-Targets
- Audit-Artefakte: 1 Review-Protokoll, 1 Non-Conformity, 1 MISRA-Record
- Gitea-Actions-CI-Pipeline (validate.yml)
- Doorstop-Konfiguration fuer bidirektionale Traceability
- Generator-Skript fuer alle 50 Reqs/Arch-Elemente aus Strukturdaten
- README mit gefuehrter Tour fuer Prospects
This commit is contained in:
Stefan Lohmaier
2026-05-11 13:51:02 -07:00
commit 1855162e6d
92 changed files with 4116 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
/**
* @file test_actuator_driver.c
* @brief Unit-Tests fuer den Actuator-Driver.
*
* @reqs SWE-006 SWE-013 SWE-014 SWE-015
* @arch SWA-003
*/
#include "../unit_test_framework.h"
#include "../../src/actuator_driver.h"
TestStats g_test_stats = {0, 0};
static void test_init(void)
{
TEST_BEGIN("init -> beide Aktoren STOP, 0 mA");
ASSERT_EQ(actuator_init(), EPB_OK);
ActuatorStatus l = actuator_get_status(ACTUATOR_LEFT);
ActuatorStatus r = actuator_get_status(ACTUATOR_RIGHT);
ASSERT_EQ(l.direction, ACT_DIR_STOP);
ASSERT_EQ(r.direction, ACT_DIR_STOP);
ASSERT_EQ(l.current_ma, 0);
TEST_END();
}
static void test_apply_invalid_id(void)
{
TEST_BEGIN("apply: invalid ID -> EINVAL");
(void)actuator_init();
ASSERT_EQ(actuator_apply((ActuatorId)42, 50), EPB_EINVAL);
TEST_END();
}
static void test_apply_pwm_out_of_range(void)
{
TEST_BEGIN("apply: PWM > 100% -> EINVAL");
(void)actuator_init();
ASSERT_EQ(actuator_apply(ACTUATOR_LEFT, 101), EPB_EINVAL);
TEST_END();
}
static void test_apply_normal(void)
{
TEST_BEGIN("apply normal -> direction=APPLY, pwm=80");
(void)actuator_init();
ASSERT_EQ(actuator_apply(ACTUATOR_LEFT, 80), EPB_OK);
ActuatorStatus s = actuator_get_status(ACTUATOR_LEFT);
ASSERT_EQ(s.direction, ACT_DIR_APPLY);
ASSERT_EQ(s.pwm_percent, 80);
TEST_END();
}
static void test_release_normal(void)
{
TEST_BEGIN("release normal -> direction=RELEASE");
(void)actuator_init();
ASSERT_EQ(actuator_release(ACTUATOR_LEFT, 70), EPB_OK);
ActuatorStatus s = actuator_get_status(ACTUATOR_LEFT);
ASSERT_EQ(s.direction, ACT_DIR_RELEASE);
TEST_END();
}
static void test_isr_samples_current(void)
{
TEST_BEGIN("ISR-Sample setzt current_ma und peak");
(void)actuator_init();
(void)actuator_apply(ACTUATOR_LEFT, 80);
actuator_isr_1khz(ACTUATOR_LEFT, 3000);
ActuatorStatus s = actuator_get_status(ACTUATOR_LEFT);
ASSERT_EQ(s.current_ma, 3000);
ASSERT_EQ(s.peak_current_ma, 3000);
TEST_END();
}
static void test_overcurrent_cutoff_after_100ms(void)
{
TEST_BEGIN("Overcurrent > 8 A fuer 100 ms -> STOP + overcurrent flag");
(void)actuator_init();
(void)actuator_apply(ACTUATOR_LEFT, 80);
/* 100 Samples (= 100 ms bei 1 kHz) ueber 8 A */
for (int i = 0; i < 100; ++i) {
actuator_isr_1khz(ACTUATOR_LEFT, 8500);
}
ActuatorStatus s = actuator_get_status(ACTUATOR_LEFT);
ASSERT_EQ(s.direction, ACT_DIR_STOP);
ASSERT_TRUE(s.overcurrent);
ASSERT_EQ(s.last_error, EPB_EOVERCURRENT);
TEST_END();
}
static void test_overcurrent_below_window_no_cutoff(void)
{
TEST_BEGIN("Overcurrent < 100 ms loest noch nicht aus");
(void)actuator_init();
(void)actuator_apply(ACTUATOR_LEFT, 80);
for (int i = 0; i < 50; ++i) {
actuator_isr_1khz(ACTUATOR_LEFT, 8500);
}
ActuatorStatus s = actuator_get_status(ACTUATOR_LEFT);
ASSERT_EQ(s.direction, ACT_DIR_APPLY);
ASSERT_TRUE(!s.overcurrent);
TEST_END();
}
static void test_overcurrent_blocks_subsequent_apply(void)
{
TEST_BEGIN("nach Overcurrent: weiterer apply -> EOVERCURRENT");
(void)actuator_init();
(void)actuator_apply(ACTUATOR_LEFT, 80);
for (int i = 0; i < 120; ++i) {
actuator_isr_1khz(ACTUATOR_LEFT, 9000);
}
EpbStatus rc = actuator_apply(ACTUATOR_LEFT, 80);
ASSERT_EQ(rc, EPB_EOVERCURRENT);
TEST_END();
}
static void test_clamping_force_estimate(void)
{
TEST_BEGIN("Klemmkraft-Schaetzung aus Peak-Strom (SWE-015)");
(void)actuator_init();
(void)actuator_apply(ACTUATOR_LEFT, 80);
actuator_isr_1khz(ACTUATOR_LEFT, 4000); /* 4 A Peak */
actuator_isr_1khz(ACTUATOR_LEFT, 3500); /* danach niedriger */
ActuatorStatus s = actuator_get_status(ACTUATOR_LEFT);
/* F = (4000 * 2500) / 1000 = 10000 N */
ASSERT_EQ(s.clamping_force_n, 10000);
TEST_END();
}
static void test_stop_clears_pwm(void)
{
TEST_BEGIN("stop -> direction=STOP, pwm=0");
(void)actuator_init();
(void)actuator_apply(ACTUATOR_LEFT, 80);
(void)actuator_stop(ACTUATOR_LEFT);
ActuatorStatus s = actuator_get_status(ACTUATOR_LEFT);
ASSERT_EQ(s.direction, ACT_DIR_STOP);
ASSERT_EQ(s.pwm_percent, 0);
TEST_END();
}
int main(void)
{
printf("== test_actuator_driver ==\n");
test_init();
test_apply_invalid_id();
test_apply_pwm_out_of_range();
test_apply_normal();
test_release_normal();
test_isr_samples_current();
test_overcurrent_cutoff_after_100ms();
test_overcurrent_below_window_no_cutoff();
test_overcurrent_blocks_subsequent_apply();
test_clamping_force_estimate();
test_stop_clears_pwm();
TEST_SUMMARY();
}
+240
View File
@@ -0,0 +1,240 @@
/**
* @file test_apply_controller.c
* @brief Unit-Tests fuer den Apply-Controller (ASIL-D Kern).
*
* @reqs SWE-001 SWE-002 SWE-003 SWE-004
* @arch SWA-002
*/
#include "../unit_test_framework.h"
#include "../../src/apply_controller.h"
#include "../../src/actuator_driver.h"
TestStats g_test_stats = {0, 0};
static ApplyInputs make_idle_inputs(void)
{
ApplyInputs in = {0};
in.sw_state = SWITCH_NEUTRAL;
in.standstill = true;
in.engine_running = true;
in.brake_pedal_pressed = true;
in.gear_engaged = true;
in.safety_apply_request = false;
in.left_force_n = 0;
in.right_force_n = 0;
return in;
}
static void apply_full_cycle_until_state(ApplyInputs* in, EpbState target, int max_steps)
{
for (int i = 0; i < max_steps; ++i) {
apply_ctrl_step_50ms(in);
if (apply_ctrl_get_state() == target) {
return;
}
}
}
static void test_init(void)
{
TEST_BEGIN("init -> RELEASED");
(void)actuator_init();
ASSERT_EQ(apply_ctrl_init(), EPB_OK);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_RELEASED);
TEST_END();
}
static void test_null_input(void)
{
TEST_BEGIN("step_50ms(NULL) -> last_error=EINVAL");
(void)actuator_init();
(void)apply_ctrl_init();
apply_ctrl_step_50ms(NULL);
ASSERT_EQ(apply_ctrl_last_error(), EPB_EINVAL);
TEST_END();
}
static void test_apply_request_starts_applying(void)
{
TEST_BEGIN("Apply-Request bei Stillstand -> APPLYING");
(void)actuator_init();
(void)apply_ctrl_init();
ApplyInputs in = make_idle_inputs();
in.sw_state = SWITCH_APPLY;
apply_ctrl_step_50ms(&in);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_APPLYING);
TEST_END();
}
static void test_no_apply_without_standstill(void)
{
TEST_BEGIN("Apply-Request ohne Stillstand wird ignoriert");
(void)actuator_init();
(void)apply_ctrl_init();
ApplyInputs in = make_idle_inputs();
in.standstill = false;
in.sw_state = SWITCH_APPLY;
apply_ctrl_step_50ms(&in);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_RELEASED);
TEST_END();
}
static void test_applying_reaches_applied_on_target_force(void)
{
TEST_BEGIN("APPLYING -> APPLIED wenn beide Klemmkraefte >= Target");
(void)actuator_init();
(void)apply_ctrl_init();
ApplyInputs in = make_idle_inputs();
in.sw_state = SWITCH_APPLY;
apply_ctrl_step_50ms(&in); /* -> APPLYING */
in.sw_state = SWITCH_NEUTRAL;
in.left_force_n = 12000;
in.right_force_n = 12000;
apply_ctrl_step_50ms(&in);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_APPLIED);
TEST_END();
}
static void test_applying_timeout_to_error(void)
{
TEST_BEGIN("APPLYING-Timeout (>30 Steps) -> ERROR");
(void)actuator_init();
(void)apply_ctrl_init();
ApplyInputs in = make_idle_inputs();
in.sw_state = SWITCH_APPLY;
apply_ctrl_step_50ms(&in); /* -> APPLYING */
in.sw_state = SWITCH_NEUTRAL;
/* Klemmkraefte bleiben bei 0 -> Timeout nach 30 Steps */
apply_full_cycle_until_state(&in, EPB_STATE_ERROR, 35);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_ERROR);
ASSERT_EQ(apply_ctrl_last_error(), EPB_ETIMEOUT);
TEST_END();
}
static void test_applied_holds_force(void)
{
TEST_BEGIN("APPLIED haelt Klemmkraft (SWE-001)");
(void)actuator_init();
(void)apply_ctrl_init();
ApplyInputs in = make_idle_inputs();
in.sw_state = SWITCH_APPLY;
apply_ctrl_step_50ms(&in);
in.sw_state = SWITCH_NEUTRAL;
in.left_force_n = 12000;
in.right_force_n = 12000;
apply_ctrl_step_50ms(&in); /* -> APPLIED */
/* Klemmkraft sinkt auf 10000 (unter Toleranz-Schwelle 10800) */
in.left_force_n = 10000;
in.right_force_n = 10000;
apply_ctrl_step_50ms(&in);
/* Apply-Controller muss nachregeln -> APPLYING */
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_APPLYING);
TEST_END();
}
static void test_release_requires_preconditions(void)
{
TEST_BEGIN("Release ohne Engine wird abgelehnt");
(void)actuator_init();
(void)apply_ctrl_init();
ApplyInputs in = make_idle_inputs();
in.sw_state = SWITCH_APPLY;
apply_ctrl_step_50ms(&in);
in.sw_state = SWITCH_NEUTRAL;
in.left_force_n = 12000;
in.right_force_n = 12000;
apply_ctrl_step_50ms(&in); /* -> APPLIED */
in.sw_state = SWITCH_RELEASE;
in.engine_running = false;
apply_ctrl_step_50ms(&in);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_APPLIED);
TEST_END();
}
static void test_release_with_preconditions(void)
{
TEST_BEGIN("Release mit allen Voraussetzungen -> RELEASING -> RELEASED");
(void)actuator_init();
(void)apply_ctrl_init();
ApplyInputs in = make_idle_inputs();
in.sw_state = SWITCH_APPLY;
apply_ctrl_step_50ms(&in);
in.sw_state = SWITCH_NEUTRAL;
in.left_force_n = 12000;
in.right_force_n = 12000;
apply_ctrl_step_50ms(&in); /* APPLIED */
in.sw_state = SWITCH_RELEASE;
apply_ctrl_step_50ms(&in);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_RELEASING);
in.sw_state = SWITCH_NEUTRAL;
in.left_force_n = 0;
in.right_force_n = 0;
apply_ctrl_step_50ms(&in);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_RELEASED);
TEST_END();
}
static void test_safety_apply_request(void)
{
TEST_BEGIN("Safety-Manager Apply-Request (Hill-Hold/Auto-Apply)");
(void)actuator_init();
(void)apply_ctrl_init();
ApplyInputs in = make_idle_inputs();
in.sw_state = SWITCH_NEUTRAL;
in.safety_apply_request = true;
apply_ctrl_step_50ms(&in);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_APPLYING);
TEST_END();
}
static void test_watchdog_alive_counter(void)
{
TEST_BEGIN("Step-Counter steigt monoton (SWE-002 Watchdog-Alive)");
(void)actuator_init();
(void)apply_ctrl_init();
ApplyInputs in = make_idle_inputs();
uint32_t before = apply_ctrl_get_step_count();
apply_ctrl_step_50ms(&in);
apply_ctrl_step_50ms(&in);
apply_ctrl_step_50ms(&in);
ASSERT_EQ(apply_ctrl_get_step_count(), before + 3);
TEST_END();
}
static void test_error_state_recoverable(void)
{
TEST_BEGIN("ERROR -> RELEASED bei neutralem Switch");
(void)actuator_init();
(void)apply_ctrl_init();
ApplyInputs in = make_idle_inputs();
in.sw_state = SWITCH_APPLY;
apply_ctrl_step_50ms(&in);
in.sw_state = SWITCH_NEUTRAL;
apply_full_cycle_until_state(&in, EPB_STATE_ERROR, 35);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_ERROR);
apply_ctrl_step_50ms(&in);
ASSERT_EQ(apply_ctrl_get_state(), EPB_STATE_RELEASED);
TEST_END();
}
int main(void)
{
printf("== test_apply_controller ==\n");
test_init();
test_null_input();
test_apply_request_starts_applying();
test_no_apply_without_standstill();
test_applying_reaches_applied_on_target_force();
test_applying_timeout_to_error();
test_applied_holds_force();
test_release_requires_preconditions();
test_release_with_preconditions();
test_safety_apply_request();
test_watchdog_alive_counter();
test_error_state_recoverable();
TEST_SUMMARY();
}
+91
View File
@@ -0,0 +1,91 @@
/**
* @file test_switch_debouncer.c
* @brief Unit-Tests fuer den Switch-Debouncer.
*
* @reqs SWE-025
* @arch SWA-006
*/
#include "../unit_test_framework.h"
#include "../../src/switch_debouncer.h"
TestStats g_test_stats = {0, 0};
static SwitchRaw mk(bool apply, bool release)
{
SwitchRaw r = {0};
r.apply_raw = apply ? 1U : 0U;
r.release_raw = release ? 1U : 0U;
return r;
}
static void test_init_state_is_neutral(void)
{
TEST_BEGIN("init -> neutral");
(void)switch_init();
ASSERT_EQ(switch_get_state(), SWITCH_NEUTRAL);
TEST_END();
}
static void test_debounce_apply_takes_5_samples(void)
{
TEST_BEGIN("apply needs 5 stable samples (50 ms)");
(void)switch_init();
for (int i = 0; i < 4; ++i) {
switch_step_10ms(mk(true, false));
ASSERT_EQ(switch_get_state(), SWITCH_NEUTRAL);
}
switch_step_10ms(mk(true, false));
ASSERT_EQ(switch_get_state(), SWITCH_APPLY);
TEST_END();
}
static void test_debounce_release_takes_5_samples(void)
{
TEST_BEGIN("release needs 5 stable samples (50 ms)");
(void)switch_init();
for (int i = 0; i < 5; ++i) {
switch_step_10ms(mk(false, true));
}
ASSERT_EQ(switch_get_state(), SWITCH_RELEASE);
TEST_END();
}
static void test_bounce_does_not_change_state(void)
{
TEST_BEGIN("bouncing input keeps current state");
(void)switch_init();
for (int i = 0; i < 5; ++i) {
switch_step_10ms(mk(true, false));
}
ASSERT_EQ(switch_get_state(), SWITCH_APPLY);
/* Bounce: 1 sample apply, 1 sample neutral, 1 sample release ... */
switch_step_10ms(mk(true, false));
switch_step_10ms(mk(false, false));
switch_step_10ms(mk(false, true));
switch_step_10ms(mk(true, false));
switch_step_10ms(mk(false, false));
ASSERT_EQ(switch_get_state(), SWITCH_APPLY);
TEST_END();
}
static void test_both_pressed_is_neutral(void)
{
TEST_BEGIN("both apply+release pressed -> neutral");
(void)switch_init();
for (int i = 0; i < 5; ++i) {
switch_step_10ms(mk(true, true));
}
ASSERT_EQ(switch_get_state(), SWITCH_NEUTRAL);
TEST_END();
}
int main(void)
{
printf("== test_switch_debouncer ==\n");
test_init_state_is_neutral();
test_debounce_apply_takes_5_samples();
test_debounce_release_takes_5_samples();
test_bounce_does_not_change_state();
test_both_pressed_is_neutral();
TEST_SUMMARY();
}
+62
View File
@@ -0,0 +1,62 @@
/**
* @file unit_test_framework.h
* @brief Minimaler Test-Runner fuer demo-epb.
*
* In Produktion wuerde hier CppUTest oder Google Test stehen
* (siehe docs/SWE-Plan.docx, Abschnitt 7). Fuer die Demo
* bleibt das Framework klein, damit es ohne externe Abhaengigkeiten baut.
*/
#ifndef UNIT_TEST_FRAMEWORK_H
#define UNIT_TEST_FRAMEWORK_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int tests_run;
int tests_failed;
} TestStats;
extern TestStats g_test_stats;
#define TEST_BEGIN(name) \
do { \
++g_test_stats.tests_run; \
const char* _test_name = name; \
int _test_failed = 0; \
printf(" TEST %s ... ", _test_name);
#define TEST_END() \
if (_test_failed) { ++g_test_stats.tests_failed; \
printf("FAIL\n"); } \
else { printf("ok\n"); } \
} while (0)
#define ASSERT_TRUE(expr) \
do { if (!(expr)) { _test_failed = 1; \
printf("\n ASSERT_TRUE failed: %s (%s:%d)\n", \
#expr, __FILE__, __LINE__); } } while (0)
#define ASSERT_EQ(a, b) \
do { long long _a = (long long)(a), _b = (long long)(b); \
if (_a != _b) { _test_failed = 1; \
printf("\n ASSERT_EQ failed: %s=%lld != %s=%lld (%s:%d)\n", \
#a, _a, #b, _b, __FILE__, __LINE__); } \
} while (0)
#define ASSERT_NE(a, b) \
do { long long _a = (long long)(a), _b = (long long)(b); \
if (_a == _b) { _test_failed = 1; \
printf("\n ASSERT_NE failed: %s==%s==%lld (%s:%d)\n", \
#a, #b, _a, __FILE__, __LINE__); } \
} while (0)
#define TEST_SUMMARY() \
do { \
printf("\n%d tests run, %d failed.\n", \
g_test_stats.tests_run, g_test_stats.tests_failed); \
return (g_test_stats.tests_failed == 0) ? 0 : 1; \
} while (0)
#endif /* UNIT_TEST_FRAMEWORK_H */