From 9e18a7a6977db020c109595a43f82398bb2cbca5 Mon Sep 17 00:00:00 2001 From: dzid26 Date: Tue, 16 Jul 2024 13:13:35 +0100 Subject: [PATCH 1/2] Speed limit PWM cut off - fixes E09. Without it pwm is set high by one of the assist function leading to diagnostic errors. Use 8bit addition and multiplication. --- src/ebike_app.c | 21 ++++++++++++-------- tests/test_speed_limit.py | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 tests/test_speed_limit.py diff --git a/src/ebike_app.c b/src/ebike_app.c index 95c866eb..014f96a0 100644 --- a/src/ebike_app.c +++ b/src/ebike_app.c @@ -1444,15 +1444,20 @@ static void apply_temperature_limiting(void) } -static void apply_speed_limit(void) -{ - if (m_configuration_variables.ui8_wheel_speed_max) { - // set battery current target - ui8_adc_battery_current_target = map_ui16((uint16_t) ui16_wheel_speed_x10, - (uint16_t) (((uint8_t)(m_configuration_variables.ui8_wheel_speed_max) * (uint8_t)10U) - (uint8_t)20U), - (uint16_t) (((uint8_t)(m_configuration_variables.ui8_wheel_speed_max) * (uint8_t)10U) + (uint8_t)20U), +static void apply_speed_limit(void) { + if (m_configuration_variables.ui8_wheel_speed_max > 0U) { + uint16_t speed_limit_low = (uint16_t)((uint8_t)(m_configuration_variables.ui8_wheel_speed_max - 2U) * (uint8_t)10U); // casting literal to uint8_t ensures usage of MUL X,A + uint16_t speed_limit_high = (uint16_t)((uint8_t)(m_configuration_variables.ui8_wheel_speed_max + 2U) * (uint8_t)10U); + + ui8_adc_battery_current_target = (uint8_t)map_ui16(ui16_wheel_speed_x10, + speed_limit_low, + speed_limit_high, ui8_adc_battery_current_target, - 0); + 0U); + + if (ui16_wheel_speed_x10 > speed_limit_high) { + ui8_duty_cycle_target = 0; + } } } diff --git a/tests/test_speed_limit.py b/tests/test_speed_limit.py new file mode 100644 index 00000000..6ca6cb9f --- /dev/null +++ b/tests/test_speed_limit.py @@ -0,0 +1,41 @@ +import pytest +from sim._tsdz2 import ffi, lib as ebike # module generated from c-code +import numpy as np + +BATTERY_CURRENT_PER_10_BIT_ADC_STEP_X100 = 16 +mA_to_ADC = 100/BATTERY_CURRENT_PER_10_BIT_ADC_STEP_X100 / 1000 + +# Set up initial values before each test +@pytest.fixture(autouse=True) +def setup_ebike(): + # Set up initial values before each test + ebike.m_configuration_variables.ui8_wheel_speed_max = 25 + ebike.ui8_duty_cycle_target = 255 # set by assistance function + ebike.ui8_adc_battery_current_target = int(5000 * mA_to_ADC) # 5000mA set by assistance function + yield + # Teardown after each test (optional) + + +def apply_speed_limit_float(speed): + speed_max = ebike.m_configuration_variables.ui8_wheel_speed_max + speed_lo = speed_max - 2 + speed_hi = speed_max + 2 + curr_target = ebike.ui8_adc_battery_current_target + current_lim = np.interp(speed, [speed_lo, speed_hi], [curr_target, 0]) + return current_lim + +# Parameterized test function with different ticks values +@pytest.mark.parametrize("speed", [0, 22.9, 23, 23.5, 24, 24.5, 25, 25.5, 26, 26.5, 27, 27.1, 30]) +def test_apply_speed_limit(speed): + ebike.ui16_wheel_speed_x10 = int(speed * 10) + + expected = apply_speed_limit_float(speed) # this has to run first + ebike.apply_speed_limit() + result = ebike.ui8_adc_battery_current_target + + assert result ==pytest.approx(expected, rel=1e-1, abs=0.1), f'Expected target {expected/mA_to_ADC}mA, got {result/mA_to_ADC}mA' + + +# Run the tests +if __name__ == '__main__': + pytest.main() From 0261ca324a735b33d8f148f3cd5d2132f4072453 Mon Sep 17 00:00:00 2001 From: dzid26 Date: Sat, 13 Jul 2024 18:16:14 +0100 Subject: [PATCH 2/2] perf: map_ui16 - round to nearest for the precision, map_ui16: allow for inverted range similar to map_ui8, map_ui8/16, fix div by zero edge case map_ui16: uint16 argument types, Tests - hypothesis framework, covers whole range of values (cherry picked from commit 3f2058d1c909a4512caf882a0fe9907fea865956) --- src/common.c | 76 +++++++++++++++++++++++++++-------------------- src/common.h | 4 +-- tests/test_map.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 34 deletions(-) create mode 100644 tests/test_map.py diff --git a/src/common.c b/src/common.c index 34978c82..c2ac2202 100644 --- a/src/common.c +++ b/src/common.c @@ -10,45 +10,57 @@ #include "stm8s.h" #include "common.h" -int16_t map_ui16(int16_t x, int16_t in_min, int16_t in_max, int16_t out_min, int16_t out_max) { - // if input min is smaller than output min, return the output min value - if (x < in_min) { - return out_min; - } - - // if input max is bigger than output max, return the output max value - else if (x > in_max) { - return out_max; - } - // map the input to the output range, round up if mapping bigger ranges to smaller ranges - else if ((in_max - in_min) > (out_max - out_min)) { - return (int16_t)(((int32_t)(x - in_min) * (out_max - out_min + 1)) / (in_max - in_min + 1)) + out_min; - } - - // map the input to the output range, round down if mapping smaller ranges to bigger ranges - else { - return (int16_t)(((int32_t)(x - in_min) * (out_max - out_min)) / (in_max - in_min)) + out_min; +// Function to map a value from one range to another based on given input and output ranges. +// Uses nearest integer rounding for precision. +// Note: Input min has to be smaller than input max. +// Parameters: +// - in: Value to be mapped. +// - in_min: Minimum value of the input range. +// - in_max: Maximum value of the input range. +// - out_min: Minimum value of the output range. +// - out_max: Maximum value of the output range. +// Returns the mapped value within the specified output range. +uint16_t map_ui16(uint16_t in, uint16_t in_min, uint16_t in_max, uint16_t out_min, uint16_t out_max) { + // If input is out of bounds, clamp it to the nearest boundary value + if (in < in_min) {return out_min;} + if (in >= in_max) {return out_max;} + + // Calculate the input and output ranges + uint16_t in_range = in_max - in_min; + + uint16_t out; + if (out_max < out_min) { + out = out_min - (uint16_t)(uint32_t)(((uint32_t)((uint32_t)(uint16_t)(in - in_min) * (uint32_t)(uint16_t)(out_min - out_max)) + (uint32_t)(uint16_t)(in_range/2U)) / in_range); + } else { + out = out_min + (uint16_t)(uint32_t)(((uint32_t)((uint32_t)(uint16_t)(in - in_min) * (uint32_t)(uint16_t)(out_max - out_min)) + (uint32_t)(uint16_t)(in_range/2U)) / in_range); } + return out; } -uint8_t map_ui8(uint8_t x, uint8_t in_min, uint8_t in_max, uint8_t out_min, uint8_t out_max) { - // if input min is smaller than output min, return the output min value - if (x <= in_min) { - return out_min; - } - - // if input max is bigger than output max, return the output max value - if (x >= in_max) { - return out_max; +// Function to map 8bit a values from one range to another based on given input and output ranges. +// Uses floor integer rounding for maximum performance. +// Note: Input min has to be smaller than input max. +// Parameters: +// - in: Value to be mapped. +// - in_min: Minimum value of the input range. +// - in_max: Maximum value of the input range. +// - out_min: Minimum value of the output range. +// - out_max: Maximum value of the output range. +// Returns the mapped value within the specified output range. +uint8_t map_ui8(uint8_t in, uint8_t in_min, uint8_t in_max, uint8_t out_min, uint8_t out_max) { + // If input is out of bounds, clamp it to the nearest boundary value + if (in < in_min) {return out_min;} + if (in >= in_max) {return out_max;} + + if (out_max < out_min) { + return out_min - (uint8_t)(uint16_t)((uint16_t)((uint8_t)(in - in_min) * (uint8_t)(out_min - out_max)) / (uint8_t)(in_max - in_min)); // cppcheck-suppress misra-c2012-10.8 ; direct cast to a wider essential to ensure mul in,a usage + } else { + return out_min + (uint8_t)(uint16_t)((uint16_t)((uint8_t)(in - in_min) * (uint8_t)(out_max - out_min)) / (uint8_t)(in_max - in_min)); // cppcheck-suppress misra-c2012-10.8 ; direct cast to a wider essential to ensure mul in,a usage } - - if (out_max < out_min) - return (uint16_t)out_min - (uint16_t)((uint8_t)(x - in_min) * (uint8_t)(out_min - out_max)) / (uint8_t)(in_max - in_min); - else - return (uint16_t)out_min + (uint16_t)((uint8_t)(x - in_min) * (uint8_t)(out_max - out_min)) / (uint8_t)(in_max - in_min); } + uint8_t ui8_min(uint8_t value_a, uint8_t value_b) { if (value_a < value_b) { return value_a; diff --git a/src/common.h b/src/common.h index 1cb23c32..de862ab5 100644 --- a/src/common.h +++ b/src/common.h @@ -49,8 +49,8 @@ //#define ADVANCED_MODE 1 //#define CALIBRATION_MODE 2 -int16_t map_ui16(int16_t x, int16_t in_min, int16_t in_max, int16_t out_min, int16_t out_max); -uint8_t map_ui8(uint8_t x, uint8_t in_min, uint8_t in_max, uint8_t out_max, uint8_t out_min); +uint16_t map_ui16(uint16_t in, uint16_t in_min, uint16_t in_max, uint16_t out_min, uint16_t out_max); +uint8_t map_ui8(uint8_t in, uint8_t in_min, uint8_t in_max, uint8_t out_min, uint8_t out_max); uint8_t ui8_max(uint8_t value_a, uint8_t value_b); uint8_t ui8_min(uint8_t value_a, uint8_t value_b); uint16_t filter(uint16_t ui16_new_value, uint16_t ui16_old_value, uint8_t ui8_alpha); diff --git a/tests/test_map.py b/tests/test_map.py new file mode 100644 index 00000000..f0aeea50 --- /dev/null +++ b/tests/test_map.py @@ -0,0 +1,75 @@ +import pytest +from sim._tsdz2 import ffi, lib as ebike # module generated from c-code +import numpy as np +from hypothesis import given, assume, strategies as st + + +@pytest.mark.parametrize( + "x, in_min, in_max, out_min, out_max, expected", [ + ( 4, 0, 16, 16, 0, 12), + ( 1, 0, 2, 3, 0, 1), + ( 1, 0, 3, 0, 2, 1), + ]) +def test_maps_simple(x, in_min, in_max, out_min, out_max, expected): + map_ui8_result = ebike.map_ui8(x, in_min, in_max, out_min, out_max) + map_ui16_result = ebike.map_ui16(x, in_min, in_max, out_min, out_max) + assert map_ui8_result == pytest.approx(expected, abs=1), f'Expected map_ui8_result {expected}, got {map_ui8_result}' + assert map_ui16_result == expected, f'Expected map_ui16_result {expected}, got {map_ui16_result}' + + +# Parameterized test function with different ticks values +@pytest.mark.parametrize("x", range(20, 45)) +def test_compare_ui8_ui16_map_input_smaller_than_output(x): + in_min = 23 + in_max = 43 + out_min = 5 + out_max = 250 + map_ui8_result = ebike.map_ui8(x, in_min, in_max, out_min, out_max) + map_ui16_result = ebike.map_ui16(x, in_min, in_max, out_min, out_max) + # ! map_ui8 has lower precision so allow for an error of 1 + assert map_ui16_result == pytest.approx(map_ui8_result, abs=1), f'Expected map_ui8_result {map_ui8_result} == map_ui16_result {map_ui16_result}' + +@pytest.mark.parametrize("x", range(20, 90)) +def test_compare_ui8_ui16_map_input_greater_than_output(x): + in_min = 23 + in_max = 87 + out_min = 5 + out_max = 50 + map_ui8_result = ebike.map_ui8(x, in_min, in_max, out_min, out_max) + map_ui16_result = ebike.map_ui16(x, in_min, in_max, out_min, out_max) + + # ! map_ui8 has lower precision so allow for an error of 1 + assert map_ui16_result == pytest.approx(map_ui8_result, abs=1), f'Expected map_ui8_result {map_ui8_result} == map_ui16_result {map_ui16_result}' + + + +# Define the hypothesis test for map_ui8 +@given( + x=st.integers(min_value=0, max_value=65535), + in_min=st.integers(min_value=0, max_value=65535), + in_max=st.integers(min_value=0, max_value=65535), + out_min=st.integers(min_value=0, max_value=65535), + out_max=st.integers(min_value=0, max_value=65535)) +def test_maps_full_ranges(x, in_min, in_max, out_min, out_max): + assume(in_min <= in_max) + + expected = np.interp(x, [in_min, in_max], [out_min, out_max]) + # !test map_ui8 only for 8 bit ranges + if max(x, in_min, in_max, out_min, out_max) < 2^8: + map_ui8_result = ebike.map_ui8(x, in_min, in_max, out_min, out_max) + # ! map_ui8 lowest precision is 1 + assert map_ui8_result == pytest.approx(expected, abs=1), \ + f"map_ui8({x}, {in_min}, {in_max}, {out_min}, {out_max}) returned {map_ui8_result}, expected {expected}" + else: + print(f'x={x}, in_min={in_min}, in_max={in_max}, out_min={out_min}, out_max={out_max}') + + map_ui16_result = ebike.map_ui16(x, in_min, in_max, out_min, out_max) + # ! map_ui16 lowest precision is 0.5 (thanks to nearest rounding) + assert map_ui16_result == pytest.approx(expected, abs=.5), \ + f"map_ui16({x}, {in_min}, {in_max}, {out_min}, {out_max}) returned {map_ui16_result}, expected {expected}" + + + +# Run the tests +if __name__ == '__main__': + pytest.main()