diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..d3150e416 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Run tests + +on: + workflow_dispatch: + pull_request: + branches: [ development ] + +jobs: + tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build build-essential + + - name: Configure (CMake) + run: | + cmake --preset simulator + + - name: Build + run: | + cmake --build out/build/simulator + + - name: Run tests (ctest) + working-directory: out/build/simulator + run: | + ctest --output-on-failure diff --git a/.vscode/settings.json b/.vscode/settings.json index b2abce05a..e7e2e0a5b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,4 @@ "tinydir.h": "c", "dirent.h": "c" } - } - \ No newline at end of file +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 037e4f115..57955f586 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,10 +21,28 @@ message(STATUS "${PROJECT_NAME} Ethernet: ${USE_ETHERNET}") message(STATUS "${PROJECT_NAME} Nucleo: ${TARGET_NUCLEO}") message(STATUS "${PROJECT_NAME} Crosscompiling: ${CMAKE_CROSSCOMPILING}") +if(NOT EXISTS ${CMAKE_CURRENT_LIST_DIR}/STM32CubeH7/Drivers) + execute_process( + COMMAND git submodule update --init --depth 1 + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + ) +endif() + +if(NOT EXISTS ${CMAKE_CURRENT_LIST_DIR}/STM32CubeH7/Drivers/CMSIS/Device/ST/STM32H7xx/Include) + execute_process( + COMMAND git submodule update --init --depth 1 Drivers/CMSIS/Device/ST/STM32H7xx Drivers/STM32H7xx_HAL_Driver + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/STM32CubeH7 + ) +endif() if(NOT CMAKE_CROSSCOMPILING) message(STATUS "Compiling for simulator") add_subdirectory(Tests) +else() + execute_process( + COMMAND git submodule update --init --depth 1 Drivers/BSP/Components/lan8742 + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/STM32CubeH7 + ) endif() # ============================ @@ -222,6 +240,12 @@ add_library(${STLIB_LIBRARY} STATIC $<$,$>:${STLIB_HIGH_CPP_ETH}> $<$:${CMAKE_CURRENT_LIST_DIR}/Src/ST-LIB.cpp> + + $<$>:${CMAKE_CURRENT_LIST_DIR}/Src/HALAL/Services/Time/Scheduler.cpp> + $<$>:${CMAKE_CURRENT_LIST_DIR}/Src/MockedDrivers/mocked_ll_tim.cpp> + $<$>:${CMAKE_CURRENT_LIST_DIR}/Src/MockedDrivers/mocked_system_stm32h7xx.c> + $<$>:${CMAKE_CURRENT_LIST_DIR}/Src/MockedDrivers/stm32h723xx_wrapper.c> + $<$>:${CMAKE_CURRENT_LIST_DIR}/Src/MockedDrivers/NVIC.cpp> ) set_target_properties(${STLIB_LIBRARY} PROPERTIES @@ -268,10 +292,13 @@ target_compile_options(${STLIB_LIBRARY} PRIVATE target_include_directories(${STLIB_LIBRARY} PUBLIC # CMSIS + HAL - $<$:${STM32CUBEH7}/Drivers/CMSIS/Device/ST/STM32H7xx/Include> - $<$:${STM32CUBEH7}/Drivers/CMSIS/Include> + #$<$:${STM32CUBEH7}/Drivers/CMSIS/Device/ST/STM32H7xx/Include> + #$<$:${STM32CUBEH7}/Drivers/CMSIS/Include> + ${STM32CUBEH7}/Drivers/CMSIS/Device/ST/STM32H7xx/Include + ${STM32CUBEH7}/Drivers/CMSIS/Include $<$:${STM32CUBEH7}/Drivers/CMSIS/Core_A/Include> - $<$:${STM32CUBEH7}/Drivers/STM32H7xx_HAL_Driver/Inc> + #$<$:${STM32CUBEH7}/Drivers/STM32H7xx_HAL_Driver/Inc> + ${STM32CUBEH7}/Drivers/STM32H7xx_HAL_Driver/Inc $<$:${STM32CUBEH7}/Drivers/STM32H7xx_HAL_Driver/Inc/Legacy> # LWIP includes diff --git a/Inc/HALAL/HALAL.hpp b/Inc/HALAL/HALAL.hpp index 316c9605a..bc896cce3 100644 --- a/Inc/HALAL/HALAL.hpp +++ b/Inc/HALAL/HALAL.hpp @@ -21,6 +21,7 @@ #include "HALAL/Services/PWM/DualPhasedPWM/DualPhasedPWM.hpp" #include "HALAL/Services/Time/Time.hpp" +#include "HALAL/Services/Time/Scheduler.hpp" #include "HALAL/Services/Time/RTC.hpp" #include "HALAL/Services/InputCapture/InputCapture.hpp" diff --git a/Inc/HALAL/Services/Time/Scheduler.hpp b/Inc/HALAL/Services/Time/Scheduler.hpp new file mode 100644 index 000000000..3c15f27b1 --- /dev/null +++ b/Inc/HALAL/Services/Time/Scheduler.hpp @@ -0,0 +1,99 @@ +/* + * Scheduler.hpp + * + * Created on: 17 nov. 2025 + * Author: Victor (coauthor Stephan) + */ +#pragma once + +/* Uso del scheduler, descrito en la wiki: https://wiki.hyperloopupv.com/es/firmware/Timing/Scheduler */ + +#ifndef TESTING_ENV +#include "stm32h7xx_ll_tim.h" +#else +#include "MockedDrivers/stm32h7xx_ll_tim_wrapper.h" +#endif +#include +#include +#include + +/* NOTE(vic): Esto cambiará pronto */ +#ifndef SCHEDULER_TIMER_IDX +# define SCHEDULER_TIMER_IDX 2 +#endif + +#define glue_(a,b) a ## b +#define glue(a,b) glue_(a,b) +#define SCHEDULER_TIMER_BASE glue(TIM, glue(SCHEDULER_TIMER_IDX, _BASE)) + +// Used to reserve a TimerPeripheral +#ifndef TESTING_ENV +#include "stm32h7xx_hal_tim.h" +#define SCHEDULER_HAL_TIM glue(htim, SCHEDULER_TIMER_IDX) +extern TIM_HandleTypeDef SCHEDULER_HAL_TIM; +#endif + +struct Scheduler { + using callback_t = void (*)(); + static constexpr uint32_t INVALID_ID = 0xFFu; + + static void start(); + static void update(); + static inline uint64_t get_global_tick(); + + static uint16_t register_task(uint32_t period_us, callback_t func); + static bool unregister_task(uint16_t id); + + static uint16_t set_timeout(uint32_t microseconds, callback_t func); + static bool cancel_timeout(uint16_t id); + + // static void global_timer_callback(); + + // Have to be public because SCHEDULER_GLOBAL_TIMER_CALLBACK won't work otherwise + //static const uint32_t global_timer_base = SCHEDULER_TIMER_BASE; + static void on_timer_update(); + +#ifndef TESTING_ENV + private: +#endif + struct Task { + uint32_t next_fire_us{0}; + callback_t callback{}; + uint32_t period_us{0}; + uint16_t id; + bool repeating{false}; + }; + + static constexpr std::size_t kMaxTasks = 16; + static_assert((kMaxTasks & (kMaxTasks - 1)) == 0, "kMaxTasks must be a power of two"); + static constexpr uint32_t FREQUENCY = 1'000'000u; // 1 MHz -> 1us precision + + static std::array tasks_; + static_assert(kMaxTasks == 16, "kMaxTasks must be 16, if more is needed, sorted_task_ids_ must change"); + /* sorted_task_ids_ is a sorted queue with 4bits for each id in the scheduler's current ids */ + static uint64_t sorted_task_ids_; + + static uint32_t active_task_count_; + static_assert(kMaxTasks <= 32, "kMaxTasks must be <= 32, if more is needed, the bitmaps must change"); + static uint32_t ready_bitmap_; + static uint32_t free_bitmap_; + static uint64_t global_tick_us_; + static uint32_t current_interval_us_; + static uint16_t timeout_idx_; + + static inline uint8_t allocate_slot(); + static inline void release_slot(uint8_t id); + static void insert_sorted(uint8_t id); + static void remove_sorted(uint8_t id); + static void schedule_next_interval(); + static inline void configure_timer_for_interval(uint32_t microseconds); + + // helpers + static inline uint8_t get_at(uint8_t idx); + static inline void set_at(uint8_t idx, uint8_t id); + static inline void pop_front(); + static inline uint8_t front_id(); + + static inline void global_timer_disable(); + static inline void global_timer_enable(); +}; diff --git a/Inc/MockedDrivers/NVIC.hpp b/Inc/MockedDrivers/NVIC.hpp new file mode 100644 index 000000000..4694f9011 --- /dev/null +++ b/Inc/MockedDrivers/NVIC.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "stm32h723xx_wrapper.h" +#include "MockedDrivers/common.hpp" +#include "MockedDrivers/compiler_specific.hpp" + +class NVIC_Type +{ + public: + volatile uint32_t ISER[8U]; /*!< Offset: 0x000 (R/W) Interrupt Set Enable Register */ + uint32_t RESERVED0[24U]; + volatile uint32_t ICER[8U]; /*!< Offset: 0x080 (R/W) Interrupt Clear Enable Register */ + uint32_t RESERVED1[24U]; + volatile uint32_t ISPR[8U]; /*!< Offset: 0x100 (R/W) Interrupt Set Pending Register */ + uint32_t RESERVED2[24U]; + volatile uint32_t ICPR[8U]; /*!< Offset: 0x180 (R/W) Interrupt Clear Pending Register */ + uint32_t RESERVED3[24U]; + volatile uint32_t IABR[8U]; /*!< Offset: 0x200 (R/W) Interrupt Active bit Register */ + uint32_t RESERVED4[56U]; + volatile uint8_t IP[240U]; /*!< Offset: 0x300 (R/W) Interrupt Priority Register (8Bit wide) */ + uint32_t RESERVED5[644U]; + volatile uint32_t STIR; /*!< Offset: 0xE00 ( /W) Software Trigger Interrupt Register */ +}; + +extern NVIC_Type* NVIC; + +extern "C"{ +void NVIC_EnableIRQ(IRQn_Type IRQn); + +uint32_t NVIC_GetEnableIRQ(IRQn_Type IRQn); + +void NVIC_DisableIRQ(IRQn_Type IRQn); + +} \ No newline at end of file diff --git a/Inc/MockedDrivers/Register.hpp b/Inc/MockedDrivers/Register.hpp new file mode 100644 index 000000000..bce0d5dba --- /dev/null +++ b/Inc/MockedDrivers/Register.hpp @@ -0,0 +1,117 @@ +#pragma once +#include +#include + + +template +struct RegisterTraits { + static void write(uint32_t& target, uint32_t val) { + target = val; + } +}; + + +template +class RegisterBase { +public: + uint32_t reg = 0; + + RegisterBase() = default; + RegisterBase(uint32_t val) : reg(val) {} + + RegisterBase& operator=(uint32_t val) { + set(val); + return *this; + } + RegisterBase& operator+=(uint32_t val){ + set(reg+val); + return *this; + } + RegisterBase& operator-=(uint32_t val){ + set(reg-val); + return *this; + } + + RegisterBase& operator&=(uint32_t mask) { + set(reg & mask); + return *this; + } + + RegisterBase& operator|=(uint32_t mask) { + set(reg | mask); + return *this; + } + + RegisterBase& operator^=(uint32_t mask) { + set(reg ^ mask); + return *this; + } + RegisterBase& operator<<=(int shift) { + set(reg << shift); // Triggers Traits + return *this; + } + + RegisterBase& operator>>=(int shift) { + set(reg >> shift); // Triggers Traits + return *this; + } + + // --- Shift Read (Does NOT modify Register) --- + // Usage: uint32_t val = REG >> 4; + uint32_t operator<<(int shift) const { + return reg << shift; + } + + uint32_t operator>>(int shift) const { + return reg >> shift; + } + RegisterBase& operator++() { + set(reg + 1); + return *this; + } + + // --- Postfix Increment (REG++) --- + // int argument is a dummy flag for C++ to distinguish postfix + uint32_t operator++(int) { + uint32_t old_val = reg; + set(reg + 1); + return old_val; + } + + RegisterBase& operator--() { + set(reg - 1); + return *this; + } + + uint32_t operator--(int) { + uint32_t old_val = reg; + set(reg - 1); + return old_val; + } + /** + * COMPARISON + */ + bool operator!=(uint32_t val) const { + return reg != val; + } + bool operator==(uint32_t val) const { + return reg == val; + } + /** + * BITWISE COMPARISONS + */ + operator uint32_t(){ + return reg; + } + operator uint32_t() const volatile { + return reg; + } + operator uint32_t() const { + return reg; + } + +protected: + void set(uint32_t val) { + RegisterTraits::write(this->reg, val); + } +}; \ No newline at end of file diff --git a/Inc/MockedDrivers/common.hpp b/Inc/MockedDrivers/common.hpp new file mode 100644 index 000000000..def8838ec --- /dev/null +++ b/Inc/MockedDrivers/common.hpp @@ -0,0 +1,48 @@ +#pragma once +#include "MockedDrivers/compiler_specific.hpp" +#ifdef __cplusplus + #define __I volatile /*!< Defines 'read only' permissions */ +#else + #define __I volatile const /*!< Defines 'read only' permissions */ +#endif +#define __O volatile /*!< Defines 'write only' permissions */ +#define __IO volatile /*!< Defines 'read / write' permissions */ + +/* following defines should be used for structure members */ +#define __IM volatile const /*! Defines 'read only' structure member permissions */ +#define __OM volatile /*! Defines 'write only' structure member permissions */ +#define __IOM volatile /*! Defines 'read / write' structure member permissions */ + +#define IS_FUNCTIONAL_STATE(STATE) (((STATE) == DISABLE) || ((STATE) == ENABLE)) + +#ifdef SET_BIT +# undef SET_BIT +#endif +#ifdef CLEAR_BIT +# undef CLEAR_BIT +#endif +#ifdef WRITE_REG +# undef WRITE_REG +#endif +#ifdef MODIFY_REG +# undef MODIFY_REG +#endif + +// this is needed because later I will do a #define uint32_t size_t +typedef uint32_t u32; + +#define SET_BIT(REG, BIT) ((REG) |= (BIT)) + +#define CLEAR_BIT(REG, BIT) ((REG) = static_cast(static_cast(REG) & ~static_cast(BIT))) + +#define READ_BIT(REG, BIT) ((REG) & (BIT)) + +#define CLEAR_REG(REG) ((REG) = (0x0)) + +#define WRITE_REG(REG, VAL) ((REG) = static_cast(VAL)) + +#define READ_REG(REG) ((REG)) + +#define MODIFY_REG(REG, CLEARMASK, SETMASK) WRITE_REG((REG), (((READ_REG(REG)) & (~(u32)(CLEARMASK))) | (SETMASK))) + +#define POSITION_VAL(VAL) (__CLZ(__RBIT(VAL))) diff --git a/Inc/MockedDrivers/compiler_specific.hpp b/Inc/MockedDrivers/compiler_specific.hpp new file mode 100644 index 000000000..bca6c858c --- /dev/null +++ b/Inc/MockedDrivers/compiler_specific.hpp @@ -0,0 +1,73 @@ +#pragma once +#include + +/* + * This file contains implementations or alternate names for + * ARM GCC intrinsics that don't work on x86_64 / host platforms. + */ + +////////////////////////////////////////////////////////////////// + +#ifdef __COMPILER_BARRIER +#undef __COMPILER_BARRIER +#endif + +////////////////////////////////////////////////////////////////// + +static inline uint32_t __RBIT(uint32_t val) { + // 1. Hardware Byte Swap (Optimization: handles the large movements) + // MSVC uses _byteswap_ulong, GCC/Clang uses __builtin_bswap32 +#if defined(_MSC_VER) + val = _byteswap_ulong(val); +#else + val = __builtin_bswap32(val); +#endif + + // 2. Swap Nibbles (within bytes) + // 0xF0 = 1111 0000 -> shifts to 0000 1111 + val = ((val & 0xF0F0F0F0u) >> 4) | ((val & 0x0F0F0F0Fu) << 4); + + // 3. Swap Bit-Pairs (within nibbles) + // 0xCC = 1100 1100 -> shifts to 0011 0011 + val = ((val & 0xCCCCCCCCu) >> 2) | ((val & 0x33333333u) << 2); + + // 4. Swap Single Bits (within pairs) + // 0xAA = 1010 1010 -> shifts to 0101 0101 + val = ((val & 0xAAAAAAAAu) >> 1) | ((val & 0x55555555u) << 1); + + return val; +} + +#define __CLZ __builtin_clz + +#if defined(_MSC_VER) && (_MSC_VER > 1200) && !defined(__clang__) +void _ReadWriteBarrier(void); +#pragma intrinsic(_ReadWriteBarrier) +#define __COMPILER_BARRIER() _ReadWriteBarrier() +#define __DSB() __COMPILER_BARRIER() +#define __ISB() __COMPILER_BARRIER() +#else + +#define __COMPILER_BARRIER() asm volatile("" ::: "memory") + +// Architecture-specific definitions for barrier intrinsics used in mocks +#if defined(__x86_64__) || defined(_M_X64) + +// Host x86_64 +# define __DSB() __asm__ volatile("mfence" ::: "memory") +# define __ISB() __asm__ volatile("lfence" ::: "memory") + +#elif defined(__aarch64__) || defined(_M_ARM64) + +// Host ARM64 +# define __DSB() __asm__ volatile("dmb ish" ::: "memory") +# define __ISB() __asm__ volatile("isb" ::: "memory") + +#else + +// Any other host architecture: compiler barrier only +# define __DSB() __COMPILER_BARRIER() +# define __ISB() __COMPILER_BARRIER() + +#endif +#endif \ No newline at end of file diff --git a/Inc/MockedDrivers/mocked_ll_tim.hpp b/Inc/MockedDrivers/mocked_ll_tim.hpp new file mode 100644 index 000000000..eb5a8b3bf --- /dev/null +++ b/Inc/MockedDrivers/mocked_ll_tim.hpp @@ -0,0 +1,149 @@ +#pragma once +#include +#include "MockedDrivers/stm32h723xx_wrapper.h" +#include "stm32h7xx.h" +#include "MockedDrivers/common.hpp" +#include "MockedDrivers/NVIC.hpp" +#include "MockedDrivers/Register.hpp" +#include +enum class TimReg { + Reg_CR1, Reg_CR2, Reg_SMCR, Reg_DIER, Reg_SR, Reg_EGR, + Reg_CCMR1, Reg_CCMR2, Reg_CCER, Reg_CNT, Reg_PSC, Reg_ARR, Reg_RCR, + Reg_CCR1, Reg_CCR2, Reg_CCR3, Reg_CCR4, Reg_BDTR, Reg_DCR, + Reg_DMAR, Reg_CCMR3, Reg_CCR5, Reg_CCR6, Reg_AF1, Reg_AF2, Reg_TISEL +}; +using enum TimReg; + + + +template +class TimerRegister : public RegisterBase { +public: + using RegisterBase::RegisterBase; + using RegisterBase::operator=; +}; + + +static_assert(sizeof(TimerRegister) == sizeof(uint32_t) ); + + +struct TIM_TypeDef{ + TIM_TypeDef(void(* irq_handler)(void),IRQn_Type irq_n): +// PSC(*this), callback{irq_handler}, irq_n{irq_n} + callback{irq_handler}, irq_n{irq_n} + {} + + template + struct PrescalerRegister : public RegisterBase { + PrescalerRegister& operator=(uint32_t val) { + this->set(val); + // esto es lo más feo que he hecho en mucho tiempo pero no he conseguido otra cosa + TIM_TypeDef *parent = (TIM_TypeDef*)((uint8_t*)&this->reg - offsetof(TIM_TypeDef, PSC)); + parent->active_PSC = val; + return *this; + } + }; + + void generate_update(); + TimerRegister CR1; /*!< TIM control register 1, Address offset: 0x00 */ + TimerRegister CR2; /*!< TIM control register 2, Address offset: 0x04 */ + TimerRegister SMCR; /*!< TIM slave mode control register, Address offset: 0x08 */ + TimerRegister DIER; /*!< TIM DMA/interrupt enable register, Address offset: 0x0C */ + TimerRegister SR; /*!< TIM status register, Address offset: 0x10 */ + TimerRegister EGR; /*!< TIM event generation register, Address offset: 0x14 */ + TimerRegister CCMR1; /*!< TIM capture/compare mode register 1, Address offset: 0x18 */ + TimerRegister CCMR2; /*!< TIM capture/compare mode register 2, Address offset: 0x1C */ + TimerRegister CCER; /*!< TIM capture/compare enable register, Address offset: 0x20 */ + TimerRegister CNT; /*!< TIM counter register, Address offset: 0x24 */ + PrescalerRegister PSC; /*!< TIM prescaler, Address offset: 0x28 */ + TimerRegister ARR; /*!< TIM auto-reload register, Address offset: 0x2C */ + TimerRegister RCR; /*!< TIM repetition counter register, Address offset: 0x30 */ + TimerRegister CCR1; /*!< TIM capture/compare register 1, Address offset: 0x34 */ + TimerRegister CCR2; /*!< TIM capture/compare register 2, Address offset: 0x38 */ + TimerRegister CCR3; /*!< TIM capture/compare register 3, Address offset: 0x3C */ + TimerRegister CCR4; /*!< TIM capture/compare register 4, Address offset: 0x40 */ + TimerRegister BDTR; /*!< TIM break and dead-time register, Address offset: 0x44 */ + TimerRegister DCR; /*!< TIM DMA control register, Address offset: 0x48 */ + TimerRegister DMAR; /*!< TIM DMA address for full transfer, Address offset: 0x4C */ + uint32_t RESERVED1; /*!< Reserved, 0x50 */ + TimerRegister CCMR3; /*!< TIM capture/compare mode register 3, Address offset: 0x54 */ + TimerRegister CCR5; /*!< TIM capture/compare register5, Address offset: 0x58 */ + TimerRegister CCR6; /*!< TIM capture/compare register6, Address offset: 0x5C */ + TimerRegister AF1; /*!< TIM alternate function option register 1, Address offset: 0x60 */ + TimerRegister AF2; /*!< TIM alternate function option register 2, Address offset: 0x64 */ + TimerRegister TISEL; /*!< TIM Input Selection register, Address offset: 0x68 */ + // ======================================================================== + // Internal Hardware State (Shadow Registers & Hidden Counters) + // ======================================================================== + + // Internal counter for the prescaler (counts 0 to active_PSC) + uint32_t internal_psc_cnt = 0; + + // Internal counter for repetition (counts down from active_RCR to 0) + uint32_t internal_rcr_cnt = 0; + + // "Shadow" registers. These hold the values currently being used by the hardware logic. + // They are updated from the public registers (Preload registers) only on an Update Event (UEV). + uint32_t active_PSC = 0; + uint32_t active_ARR = 0; + uint32_t active_RCR = 0; + + void(*callback)(); + IRQn_Type irq_n; + bool check_CNT_increase_preconditions(){ + // Bit definitions for clarity + const uint32_t CR1_CEN = (1U << 0); // Counter Enable + + // 1. Check if Counter is Enabled + if (!(CR1 & CR1_CEN)) { + std::cout<<"TIMER IS NOT ENABLED!!\n"; + return false; + } + // 2. Prescaler Logic + // The internal prescaler counts from 0 up to active_PSC. + // We check if we reached the limit *before* incrementing to ensure accurate + // behavior for PSC=0 or PSC=1. + bool main_counter_tick = false; + + if (internal_psc_cnt >= active_PSC) { + internal_psc_cnt = 0; // Rollover + main_counter_tick = true; + } else { + internal_psc_cnt++; // Increment + } + + // If prescaler didn't overflow, the main counter doesn't move. + return main_counter_tick; + } + + void inc_cnt_and_check(uint32_t val); +}; +static_assert(sizeof(TimerRegister) == sizeof(uint32_t)); +void simulate_ticks(TIM_TypeDef* tim); + +/* +template<> +struct RegisterTraits { + static void write(uint32_t& target, uint32_t val) { + TIM_TypeDef* timer = (TIM_TypeDef*)(((uint8_t*)&target)-offsetof(TIM_TypeDef, CNT)); + target = val; + if(val != 0 && timer->check_CNT_increase_preconditions()){ + simulate_ticks(timer); + } + } +}; +*/ + +#define DECLARE_TIMER(TIM_IDX) \ + extern TIM_TypeDef* TIM_IDX##_BASE; \ + extern "C"{ \ + void TIM_IDX##_IRQHandler(void); \ + } +#define INSTANTIATE_TIMER(TIM_IDX) \ + TIM_TypeDef __htim##TIM_IDX{TIM_IDX##_IRQHandler,TIM_IDX##_IRQn}; \ + TIM_TypeDef* TIM_IDX##_BASE = &__htim##TIM_IDX; + +#undef TIM1_BASE +DECLARE_TIMER(TIM1) +#undef TIM2_BASE +DECLARE_TIMER(TIM2) diff --git a/Inc/MockedDrivers/stm32h723xx_wrapper.h b/Inc/MockedDrivers/stm32h723xx_wrapper.h new file mode 100644 index 000000000..6d5850340 --- /dev/null +++ b/Inc/MockedDrivers/stm32h723xx_wrapper.h @@ -0,0 +1,34 @@ +#ifndef STM32H723xx_WRAPPER_H +#define STM32H723xx_WRAPPER_H + +#include + +#define STM32H723xx + +#if defined(__GNUC__) || defined(__GNUG__) +# define __STATIC_INLINE static inline +#else +# error :) +#endif + +#ifdef __cplusplus + #define __I volatile /*!< Defines 'read only' permissions */ +#else + #define __I volatile const /*!< Defines 'read only' permissions */ +#endif +#define __IO volatile + +#define __RBIT __RBIT__CMSIS +#define TIM_TypeDef TIM_TypeDef__CMSIS + +// only do __CORE_CM7_H_GENERIC in "core_cm7.h" +#define __CORE_CM7_H_DEPENDANT +#include "stm32h723xx.h" + +#undef __RBIT +#undef TIM_TypeDef + +#undef RCC +extern RCC_TypeDef *RCC; + +#endif // STM32H723xx_WRAPPER_H diff --git a/Inc/MockedDrivers/stm32h7xx_ll_tim_wrapper.h b/Inc/MockedDrivers/stm32h7xx_ll_tim_wrapper.h new file mode 100644 index 000000000..904f5bb5f --- /dev/null +++ b/Inc/MockedDrivers/stm32h7xx_ll_tim_wrapper.h @@ -0,0 +1,18 @@ +#ifndef STM32H7xx_LL_TIM_WRAPPER_H +#define STM32H7xx_LL_TIM_WRAPPER_H + +#include + +#include "MockedDrivers/stm32h723xx_wrapper.h" + +#include "stm32h7xx.h" + +#include "MockedDrivers/mocked_ll_tim.hpp" + +#define uint32_t size_t +#include "stm32h7xx_ll_tim.h" +#undef uint32_t + +#include "MockedDrivers/common.hpp" + +#endif // STM32H7xx_LL_TIM_WRAPPER_H diff --git a/Src/HALAL/HALAL.cpp b/Src/HALAL/HALAL.cpp index 2ee470ed9..3a309fb2d 100644 --- a/Src/HALAL/HALAL.cpp +++ b/Src/HALAL/HALAL.cpp @@ -72,8 +72,8 @@ static void common_start(UART::Peripheral &printf_peripheral) { #ifdef HAL_TIM_MODULE_ENABLED Encoder::start(); Global_RTC::start_rtc(); - TimerPeripheral::start(); - Time::start(); + //TimerPeripheral::start(); + //Time::start(); #endif #ifdef HAL_EXTI_MODULE_ENABLED diff --git a/Src/HALAL/Services/Time/Scheduler.cpp b/Src/HALAL/Services/Time/Scheduler.cpp new file mode 100644 index 000000000..c4909b3d5 --- /dev/null +++ b/Src/HALAL/Services/Time/Scheduler.cpp @@ -0,0 +1,376 @@ +/* + * Scheduler.hpp + * + * Created on: 17 nov. 2025 + * Author: Victor (coauthor Stephan) + */ +#include "HALAL/Services/Time/Scheduler.hpp" + +#ifndef TESTING_ENV + // This is needed to register a TimerPeripheral + #include "HALAL/Models/TimerPeripheral/TimerPeripheral.hpp" +#endif +#include +#include + + +/* NOTE(vic): Pido perdón a Boris pero es la mejor manera que se me ha ocurrido hacer esto */ +#define SCHEDULER_RCC_TIMER_ENABLE \ + glue(glue(RCC_APB1LENR_TIM, SCHEDULER_TIMER_IDX), EN) +#define SCHEDULER_GLOBAL_TIMER_IRQn \ + glue(TIM, glue(SCHEDULER_TIMER_IDX, _IRQn)) +#define SCHEDULER_GLOBAL_TIMER_CALLBACK() \ + extern "C" void glue(TIM, glue(SCHEDULER_TIMER_IDX, _IRQHandler))(void) + +#define Scheduler_global_timer ((TIM_TypeDef*)SCHEDULER_TIMER_BASE) +namespace { +constexpr uint64_t kMaxIntervalUs = + static_cast(std::numeric_limits::max())/2 + 1ULL; +} + +std::array Scheduler::tasks_{}; +uint64_t Scheduler::sorted_task_ids_ = 0; +uint32_t Scheduler::active_task_count_{0}; + +uint32_t Scheduler::ready_bitmap_{0}; +uint32_t Scheduler::free_bitmap_{0xFFFF'FFFF}; +uint64_t Scheduler::global_tick_us_{0}; +uint32_t Scheduler::current_interval_us_{0}; +uint16_t Scheduler::timeout_idx_{1}; + +inline uint8_t Scheduler::get_at(uint8_t idx) { + int word_idx = idx > 7; + uint32_t shift = (idx & 7) << 2; + return (((uint32_t*)&sorted_task_ids_)[word_idx] & (0x0F << shift)) >> shift; +} +inline void Scheduler::set_at(uint8_t idx, uint8_t id) { + uint32_t shift = idx*4; + uint64_t clearmask = ~(0xFF << shift); + Scheduler::sorted_task_ids_ = (sorted_task_ids_ & clearmask) | (id << shift); + // sorted_task_ids_ |= ((id & 0x0F) << shift); // This is also an option in case id is incorrect, I don't think it's necessary though +} +inline uint8_t Scheduler::front_id() { + return *((uint8_t*)&sorted_task_ids_) & 0xF; +} +inline void Scheduler::pop_front() { + // O(1) remove of logical index 0 + Scheduler::active_task_count_--; + Scheduler::sorted_task_ids_ >>= 4; +} + +// ---------------------------- + +inline void Scheduler::global_timer_disable() { + LL_TIM_DisableCounter(Scheduler_global_timer); + //Scheduler_global_timer->CR1 &= ~TIM_CR1_CEN; +} +inline void Scheduler::global_timer_enable() { + LL_TIM_EnableCounter(Scheduler_global_timer); + //Scheduler_global_timer->CR1 |= TIM_CR1_CEN; +} + +// ---------------------------- +void Scheduler::start() { + static_assert((Scheduler::FREQUENCY % 1'000'000) == 0u, "frequenct must be a multiple of 1MHz"); + + + uint32_t prescaler = (SystemCoreClock / Scheduler::FREQUENCY); + // setup prescaler + { + // ref manual: section 8.7.7 RCC domain 1 clock configuration register + uint32_t ahb_prescaler = RCC->D1CFGR & RCC_D1CFGR_HPRE_Msk; + if((ahb_prescaler & 0b1000) != 0) { + switch(ahb_prescaler) { + case 0b1000: prescaler /= 2; break; + case 0b1001: prescaler /= 4; break; + case 0b1010: prescaler /= 8; break; + case 0b1011: prescaler /= 16; break; + case 0b1100: prescaler /= 64; break; + case 0b1101: prescaler /= 128; break; + case 0b1110: prescaler /= 256; break; + case 0b1111: prescaler /= 512; break; + } + } + + // ref manual: section 8.7.8: RCC domain 2 clock configuration register + uint32_t apb1_prescaler = (RCC->D2CFGR & RCC_D2CFGR_D2PPRE1_Msk) >> RCC_D2CFGR_D2PPRE1_Pos; + if((apb1_prescaler & 0b100) != 0) { + switch(apb1_prescaler) { + case 0b100: prescaler /= 2; break; + case 0b101: prescaler /= 4; break; + case 0b110: prescaler /= 8; break; + case 0b111: prescaler /= 16; break; + } + } + // tim2clk = 2 x pclk1 when apb1_prescaler != 1 + if(apb1_prescaler != 1) { + prescaler *= 2; + } + + if(prescaler > 1) { + prescaler--; + } + } + + // TODO: Fault when any of the next 2 static asserts happen (needs to be runtime bcos of SystemCoreClock) + if(prescaler == 0 || prescaler > 0xFFFF) { + // error here + } + + //static_assert(prescaler < 0xFFFF, "Prescaler is 16 bit, so it must be in that range"); + //static_assert(prescaler != 0, "Prescaler must be in the range [1, 65535]"); +#ifndef TESTING_ENV + + // Register a TimerPeripheral so it's not used anywhere else + // hopefully we can move to something better than TimerPeripheral + TimerPeripheral::InitData init_data(TimerPeripheral::BASE); + TimerPeripheral perif_reserve(&SCHEDULER_HAL_TIM, std::move(init_data), (std::string)"timer2"); + + RCC->APB1LENR |= SCHEDULER_RCC_TIMER_ENABLE; +#endif + Scheduler_global_timer->PSC = (uint16_t)prescaler; + Scheduler_global_timer->ARR = 0; + Scheduler_global_timer->DIER |= LL_TIM_DIER_UIE; + Scheduler_global_timer->CR1 = LL_TIM_CLOCKDIVISION_DIV1 | (Scheduler_global_timer->CR1 & ~TIM_CR1_CKD); + // I think this should be cleared at startup. TODO: Look it up in ref manual + // LL_TIM_DisableExternalClock(Scheduler_global_timer); + // |-> does this: Scheduler_global_timer->SMCR &= ~TIM_SMCR_ECE; /* Disable external clock */ + + Scheduler_global_timer->CNT = 0; /* Clear counter value */ + + NVIC_EnableIRQ(SCHEDULER_GLOBAL_TIMER_IRQn); + CLEAR_BIT(Scheduler_global_timer->SR, LL_TIM_SR_UIF); /* clear update interrupt flag */ + + Scheduler::schedule_next_interval(); +} + +SCHEDULER_GLOBAL_TIMER_CALLBACK() { + CLEAR_BIT(Scheduler_global_timer->SR, TIM_SR_UIF); + Scheduler::on_timer_update(); +} + +void Scheduler::update() { + while(ready_bitmap_ != 0u) { + uint32_t bit_index = static_cast(__builtin_ctz(ready_bitmap_)); + ready_bitmap_ &= ~(1u << bit_index); // Clear the bit + Task& task = tasks_[bit_index]; + task.callback(); + if (!task.repeating) [[unlikely]] { + release_slot(static_cast(bit_index)); + } + } +} + +// void Scheduler::global_timer_callback() { on_timer_update(); } + +inline uint8_t Scheduler::allocate_slot() { + uint32_t idx = __builtin_ffs(Scheduler::free_bitmap_) - 1; + if(idx > static_cast(Scheduler::kMaxTasks)) [[unlikely]] + return static_cast(Scheduler::INVALID_ID); + Scheduler::free_bitmap_ &= ~(1UL << idx); + return static_cast(idx); +} + +inline void Scheduler::release_slot(uint8_t id) { + ready_bitmap_ &= ~(1u << id); + free_bitmap_ |= (1u << id); +} + +void Scheduler::insert_sorted(uint8_t id) { + Task& task = tasks_[id]; + + // binary search on logical range [0, active_task_count_) + std::size_t left = 0; + std::size_t right = Scheduler::active_task_count_; + while (left < right) { + std::size_t mid = left + ((right - left) / 2); + const Task& mid_task = tasks_[Scheduler::get_at(mid)]; + if ((int32_t)(task.next_fire_us - mid_task.next_fire_us) >= 0) { + left = mid + 1; + } else { + right = mid; + } + } + const std::size_t pos = left; + + uint32_t lo = (uint32_t)sorted_task_ids_; + uint32_t hi = (uint32_t)(sorted_task_ids_ >> 32); + + // take the shift for only high or low 32 bits + uint32_t shift = (pos & 7) << 2; + uint32_t id_shifted = id << shift; + + uint32_t mask = (1UL << shift) - 1; + uint32_t inv_mask = ~mask; //Hole mask + + //Calculate both posibilities + uint32_t lo_modified = ((lo & inv_mask) << 4) | (lo & mask) | id_shifted; + uint32_t hi_modified = ((hi & inv_mask) << 4) | (hi & mask) | id_shifted; + + uint32_t hi_spilled = (hi << 4) | (lo >> 28); + + if (pos >= 8) { + hi = hi_modified; + // lo remains unchanged + } else { + hi = hi_spilled; + lo = lo_modified; + } + + sorted_task_ids_ = ((uint64_t)hi << 32) | lo; + Scheduler::active_task_count_++; +} + +void Scheduler::remove_sorted(uint8_t id) { + uint64_t nibble_lsb = 0x1111'1111'1111'1111ULL; + + // pattern = nibble_lsb * id (para obtener id en cada nibble) + uint32_t pattern_32 = id + (id << 4); + pattern_32 = pattern_32 + (pattern_32 << 8); + pattern_32 = pattern_32 + (pattern_32 << 16); + uint64_t pattern = pattern_32; + ((uint32_t*)&pattern)[1] = pattern_32; + + // diff becomes 0xid..id_0_id..id where 0 is the nibble where id is in sorted_task_ids + uint64_t diff = Scheduler::sorted_task_ids_ ^ pattern; + + //https://stackoverflow.com/questions/79058066/finding-position-of-zero-nibble-in-64-bits + //https://stackoverflow.com/questions/59480527/fast-lookup-of-a-null-nibble-in-a-64-bit-unsigned + uint64_t nibble_msb = 0x8888'8888'8888'8888ULL; + uint64_t matches = (diff - nibble_lsb) & (~diff & nibble_msb); + + if(matches == 0) [[unlikely]] return; // not found + + /* split the bm in two 0x0000...FFFFFF where removal index is placed + * then invert to keep both sides that surround the discarded index + */ + uint32_t pos_msb = __builtin_ctzll(matches); + uint32_t pos_lsb = pos_msb - 3; + + uint64_t mask = (1ULL << pos_lsb) - 1; + + // Remove element (lower part | higher pushing nibble out of mask) + Scheduler::sorted_task_ids_ = (Scheduler::sorted_task_ids_ & mask) | ((Scheduler::sorted_task_ids_ >> 4) & ~mask); + Scheduler::active_task_count_--; +} + +void Scheduler::schedule_next_interval() { + if (active_task_count_ == 0) [[unlikely]] { + Scheduler::global_timer_disable(); + current_interval_us_ = 0; + return; + } + + uint8_t next_id = Scheduler::front_id(); // sorted_task_ids_[0] + Task& next_task = tasks_[next_id]; + int32_t diff = (int32_t)(next_task.next_fire_us - static_cast(global_tick_us_)); + if (diff >= -1 && diff <= 1) [[unlikely]] { + current_interval_us_ = 1; + Scheduler_global_timer->ARR = 1; + Scheduler_global_timer->CNT = 1; + Scheduler::global_timer_enable(); + } else { + if (diff < -1) [[unlikely]]{ + current_interval_us_ = static_cast(0 - diff); + } else { + current_interval_us_ = static_cast(diff); + } + configure_timer_for_interval(current_interval_us_); + } +} + +inline void Scheduler::configure_timer_for_interval(uint32_t microseconds) { + // NOTE(vic): disabling the timer _might_ be necessary to prevent the timer from firing in the middle of configuring it, highly unlikely since it has a period of at least 1 microsecond + // TODO(vic): Validation: check arr is set correctly here: https://github.com/HyperloopUPV-H8/ST-LIB/pull/534#pullrequestreview-3529132356 + Scheduler_global_timer->ARR = static_cast(microseconds - 1u); + Scheduler_global_timer->CNT = 0; + Scheduler::global_timer_enable(); +} + +void Scheduler::on_timer_update() { + global_tick_us_ += current_interval_us_; + + while (active_task_count_ > 0) { //Pop all due tasks, several might be due in the same tick + uint8_t candidate_id = Scheduler::front_id(); + Task& task = tasks_[candidate_id]; + int32_t diff = (int32_t)(task.next_fire_us - static_cast(global_tick_us_)); + if (diff > 0) [[likely]]{ + break; // Task is in the future, stop processing + } + pop_front(); + ready_bitmap_ |= (1u << candidate_id); // mark task as ready + + if (task.repeating) [[likely]] { + task.next_fire_us = static_cast(global_tick_us_ + task.period_us); + insert_sorted(candidate_id); + } + } + + schedule_next_interval(); +} + +uint16_t Scheduler::register_task(uint32_t period_us, callback_t func) { + if(func == nullptr) [[unlikely]] return static_cast(Scheduler::INVALID_ID); + if(period_us == 0) [[unlikely]] period_us = 1; + if(period_us >= kMaxIntervalUs) [[unlikely]] return static_cast(Scheduler::INVALID_ID); + + uint8_t slot = allocate_slot(); + if(slot == Scheduler::INVALID_ID) return slot; + + Task& task = tasks_[slot]; + task.callback = func; + task.period_us = period_us; + task.repeating = true; + task.next_fire_us = static_cast(global_tick_us_ + period_us); + task.id = static_cast(slot); + insert_sorted(slot); + schedule_next_interval(); + return task.id; +} + +uint16_t Scheduler::set_timeout(uint32_t microseconds, callback_t func) { + if (func == nullptr) [[unlikely]] return static_cast(Scheduler::INVALID_ID); + if(microseconds == 0) [[unlikely]] microseconds = 1; + if(microseconds >= kMaxIntervalUs) [[unlikely]] return static_cast(Scheduler::INVALID_ID); + + uint8_t slot = allocate_slot(); + if(slot == Scheduler::INVALID_ID) return slot; + + Task& task = tasks_[slot]; + task.callback = func; + task.period_us = microseconds; + task.repeating = false; + task.next_fire_us = static_cast(global_tick_us_ + microseconds); + task.id = slot + Scheduler::timeout_idx_ * Scheduler::kMaxTasks; + + // Add 2 instead of 1 so overflow doesn't make timeout_idx == 0, + // we need it to never be 0 + Scheduler::timeout_idx_ += 2; + + insert_sorted(slot); + schedule_next_interval(); + return task.id; +} + +bool Scheduler::unregister_task(uint16_t id) { + if(id >= kMaxTasks) return false; + if(free_bitmap_ & (1UL << id)) return false; + + remove_sorted(id); + release_slot(id); + schedule_next_interval(); + return true; +} + +bool Scheduler::cancel_timeout(uint16_t id) { + static_assert((kMaxTasks & (kMaxTasks - 1)) == 0, "kMaxTasks must be a power of two"); + uint32_t idx = id & (Scheduler::kMaxTasks - 1UL); + if(tasks_[idx].repeating) return false; + if(tasks_[idx].id != id) return false; + if(free_bitmap_ & (1UL << idx)) return false; + + remove_sorted(idx); + release_slot(idx); + schedule_next_interval(); + return true; +} diff --git a/Src/MockedDrivers/NVIC.cpp b/Src/MockedDrivers/NVIC.cpp new file mode 100644 index 000000000..cb5646615 --- /dev/null +++ b/Src/MockedDrivers/NVIC.cpp @@ -0,0 +1,38 @@ +#include "MockedDrivers/NVIC.hpp" + + +NVIC_Type __NVIC; +NVIC_Type* NVIC = &__NVIC; + + +void NVIC_EnableIRQ(IRQn_Type IRQn) +{ + if ((int32_t)(IRQn) >= 0) + { + __COMPILER_BARRIER(); + NVIC->ISER[(((uint32_t)IRQn) >> 5UL)] = (uint32_t)(1UL << (((uint32_t)IRQn) & 0x1FUL)); + __COMPILER_BARRIER(); + } +} + +uint32_t NVIC_GetEnableIRQ(IRQn_Type IRQn) +{ + if ((int32_t)(IRQn) >= 0) + { + return((uint32_t)(((NVIC->ISER[(((uint32_t)IRQn) >> 5UL)] & (1UL << (((uint32_t)IRQn) & 0x1FUL))) != 0UL) ? 1UL : 0UL)); + } + else + { + return(0U); + } +} + +void NVIC_DisableIRQ(IRQn_Type IRQn) +{ + if ((int32_t)(IRQn) >= 0) + { + NVIC->ICER[(((uint32_t)IRQn) >> 5UL)] = (uint32_t)(1UL << (((uint32_t)IRQn) & 0x1FUL)); + __DSB(); + __ISB(); + } +} \ No newline at end of file diff --git a/Src/MockedDrivers/mocked_ll_tim.cpp b/Src/MockedDrivers/mocked_ll_tim.cpp new file mode 100644 index 000000000..2d1dd0edb --- /dev/null +++ b/Src/MockedDrivers/mocked_ll_tim.cpp @@ -0,0 +1,68 @@ +#include "MockedDrivers/mocked_ll_tim.hpp" + +#include + +INSTANTIATE_TIMER(TIM2) + +void TIM_TypeDef::generate_update() { + active_PSC = PSC; + active_ARR = ARR; + active_RCR = RCR; + internal_rcr_cnt = active_RCR; + internal_psc_cnt = 0; + *((uint32_t*)&CNT) = 0; // Usually UEV also resets CNT unless configured otherwise + SR &= ~(1U << 0); // Clear UIF if needed, or set it depending on CR1 +} +void simulate_ticks(TIM_TypeDef* tim){ + + // Bit definitions for clarity + const uint32_t CR1_UDIS = (1U << 1); // Update Disable + const uint32_t CR1_ARPE = (1U << 7); // Auto-Reload Preload Enable + const uint32_t SR_UIF = (1U << 0); // Update Interrupt Flag + + // Determine the current Auto-Reload limit. + // If ARPE is set, use the buffered (shadow) value. + // If ARPE is clear, use the immediate register value. + uint32_t current_limit = (tim->CR1 & CR1_ARPE) ? tim->active_ARR : tim->ARR.reg; + + // Check for Overflow + if (tim->CNT > current_limit) { + std::cout<<"timer overflow\n"; + tim->CNT = 0; // Rollover main counter + + // 4. Repetition Counter & Update Event Logic + // The Update Event (UEV) is generated when the Repetition Counter underflows. + if (tim->internal_rcr_cnt == 0) { + + // --- GENERATE UPDATE EVENT (UEV) --- + + // A. Update Shadow Registers from Preload Registers + tim->active_PSC = tim->PSC; + tim->active_ARR = tim->ARR; + tim->active_RCR = tim->RCR; + + // B. Set Update Interrupt Flag (UIF) + // Only if UDIS (Update Disable) is NOT set + if (!(tim->CR1 & CR1_UDIS)) { + tim->SR |= SR_UIF; + if(NVIC_GetEnableIRQ(tim->irq_n)){ + tim->callback(); + } + } + + // C. Reload Repetition Counter with new value + tim->internal_rcr_cnt = tim->active_RCR; + + } else { + // No UEV yet, just decrement Repetition Counter + tim->internal_rcr_cnt--; + } + } +} + +void TIM_TypeDef::inc_cnt_and_check(uint32_t val) { + if(val != 0 && this->check_CNT_increase_preconditions()){ + this->CNT += val; + simulate_ticks(this); + } +} \ No newline at end of file diff --git a/Src/MockedDrivers/mocked_system_stm32h7xx.c b/Src/MockedDrivers/mocked_system_stm32h7xx.c new file mode 100644 index 000000000..869e20d3c --- /dev/null +++ b/Src/MockedDrivers/mocked_system_stm32h7xx.c @@ -0,0 +1,3 @@ +#include + +uint32_t SystemCoreClock = 64000000; diff --git a/Src/MockedDrivers/stm32h723xx_wrapper.c b/Src/MockedDrivers/stm32h723xx_wrapper.c new file mode 100644 index 000000000..f8da31ebb --- /dev/null +++ b/Src/MockedDrivers/stm32h723xx_wrapper.c @@ -0,0 +1,4 @@ +#include "MockedDrivers/stm32h723xx_wrapper.h" + +static RCC_TypeDef RCC_struct; +RCC_TypeDef *RCC = &RCC_struct; diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 4ee61c8b6..c06522432 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -14,7 +14,7 @@ FetchContent_MakeAvailable(googletest) message(STATUS "Generating test executable for ST-LIB") add_executable(${STLIB_TEST_EXECUTABLE} - # ${CMAKE_CURRENT_LIST_DIR}/Time/scheduler_test.cpp + ${CMAKE_CURRENT_LIST_DIR}/Time/scheduler_test.cpp ) set_target_properties(${STLIB_TEST_EXECUTABLE} PROPERTIES diff --git a/Tests/Time/scheduler_test.cpp b/Tests/Time/scheduler_test.cpp new file mode 100644 index 000000000..e1cc34435 --- /dev/null +++ b/Tests/Time/scheduler_test.cpp @@ -0,0 +1,227 @@ +#include +#include +#include + +#include "HALAL/Services/Time/Scheduler.hpp" + +int count = 0; +void fake_workload(){ + count++; +} + +class SchedulerTests : public ::testing::Test { +protected: + void SetUp() override { + Scheduler::active_task_count_ = 0; + Scheduler::free_bitmap_ = 0xFFFF'FFFF; + Scheduler::ready_bitmap_ = 0; + Scheduler::sorted_task_ids_ = 0; + Scheduler::global_tick_us_ = 0; + Scheduler::current_interval_us_ = 0; + + // Reset global callback task count + count = 0; + + // Reset Timer + TIM2_BASE->CNT = 0; + TIM2_BASE->ARR = 0; + TIM2_BASE->SR = 0; + TIM2_BASE->CR1 = 0; + TIM2_BASE->DIER = 0; + } +}; + +TEST_F(SchedulerTests, FreeBitmap) { + Scheduler::register_task(10,&fake_workload); + EXPECT_EQ(Scheduler::free_bitmap_, 0xFFFF'FFFE); +} + +TEST_F(SchedulerTests, TaskRegistration) { + Scheduler::register_task(10,&fake_workload); + EXPECT_EQ(Scheduler::tasks_[0].callback,fake_workload); +} + +TEST_F(SchedulerTests, TaskExecutionShort) { + Scheduler::register_task(10,&fake_workload); + Scheduler::start(); + TIM2_BASE->PSC = 2; // quicker test + + constexpr int NUM_TICKS = 1'000; + for(int i = 0; i < NUM_TICKS; i++){ + for(int j = 0; j <= TIM2_BASE->PSC; j++) TIM2_BASE->inc_cnt_and_check(1); + Scheduler::update(); + } + // 1000 ticks / 10 ticks/task = 100 executions. + EXPECT_EQ(count, 100); +} + +TEST_F(SchedulerTests, TaskExecutionLong) { + Scheduler::register_task(10,&fake_workload); + Scheduler::start(); + // TIM2_BASE->ARR = 500; + TIM2_BASE->generate_update(); + TIM2_BASE->PSC = 2; // quicker test + + constexpr int NUM_TICKS = 1'000'000; + for(int i = 0; i < NUM_TICKS; i++){ + for(int j = 0; j <= TIM2_BASE->PSC; j++) TIM2_BASE->inc_cnt_and_check(1); + Scheduler::update(); + } + EXPECT_EQ(count, 100'000); +} + +TEST_F(SchedulerTests, SetTimeout) { + Scheduler::set_timeout(10, &fake_workload); + Scheduler::start(); + TIM2_BASE->PSC = 2; // quicker test + + constexpr int NUM_TICKS = 100; + for(int i = 0; i < NUM_TICKS; i++){ + for(int j = 0; j <= TIM2_BASE->PSC; j++) TIM2_BASE->inc_cnt_and_check(1); + Scheduler::update(); + } + EXPECT_EQ(count, 1); +} + +TEST_F(SchedulerTests, GlobalTickOverflow) { + Scheduler::global_tick_us_ = 0xFFFFFFF0ULL; // Near 32-bit max + Scheduler::register_task(20, &fake_workload); + Scheduler::start(); + TIM2_BASE->PSC = 2; // quicker test + + constexpr int NUM_TICKS = 100; + for(int i = 0; i < NUM_TICKS; i++){ + for(int j = 0; j <= TIM2_BASE->PSC; j++) TIM2_BASE->inc_cnt_and_check(1); + + Scheduler::update(); + } + // 100 ticks /20 ticks/task = 5 executions. + EXPECT_EQ(count, 5); +} + +TEST_F(SchedulerTests, TimeoutClearAddTask) { + uint8_t timeout_id = Scheduler::set_timeout(10, &fake_workload); + Scheduler::start(); + TIM2_BASE->PSC = 2; // quicker test + + constexpr int NUM_TICKS = 100; + for(int i = 0; i < NUM_TICKS; i++) { + for(int j = 0; j <= TIM2_BASE->PSC; j++) TIM2_BASE->inc_cnt_and_check(1); + Scheduler::update(); + } + + // timeout is already done here + uint8_t timeout_id_2 = Scheduler::set_timeout(20, &fake_workload); + + // after timeout, cancel task + Scheduler::cancel_timeout(timeout_id); + + EXPECT_EQ(Scheduler::active_task_count_, 1); +} + +static volatile int connecting_execs{0}; +static volatile int operational_execs{0}; +static volatile int fault_execs{0}; +void connecting_cyclic(){ + auto next_connecting_execs = connecting_execs + 1; + connecting_execs = next_connecting_execs; +} +void operational_cyclic(){ + auto next_operational_execs = operational_execs + 1; + operational_execs = next_operational_execs; +} +void fault_cyclic(){ + auto next_fault_execs = fault_execs + 1; + fault_execs = next_fault_execs; +} +TEST_F(SchedulerTests, TaskDe_ReRegistration) { + uint8_t connecting_task = Scheduler::register_task(10, &connecting_cyclic); + uint8_t operational_task = 0; + uint8_t fault_task = 0; + Scheduler::start(); + TIM2_BASE->PSC = 2; // quicker test + + constexpr int NUM_TICKS = 100; + for(int i = 0; i < NUM_TICKS; i++) { + for(int j = 0; j <= TIM2_BASE->PSC; j++) TIM2_BASE->inc_cnt_and_check(1); + if(i == 21){ + Scheduler::unregister_task(connecting_task); + operational_task = Scheduler::register_task(10,operational_cyclic); + } + if(i == 45){ + Scheduler::unregister_task(operational_task); + fault_task = Scheduler::register_task(10,fault_cyclic); + } + if( i == 70){ + Scheduler::unregister_task(fault_task); + i = 100; // finish test + } + Scheduler::update(); + } + EXPECT_EQ(connecting_execs, 2); + EXPECT_EQ(operational_execs, 2); + EXPECT_EQ(fault_execs, 2); +} + +#define multiple_tasks \ + X(1) \ + X(2) \ + X(3) \ + X(4) \ + X(5) \ + X(6) \ + X(7) \ + X(8) \ + X(9) \ + X(10) \ + X(11) \ + X(12) \ + X(13) \ + X(14) \ + X(15) \ + X(16) + +#define X(n) \ + int multiple_task##n##count = 0; \ + void multiple_task_##n(void) { \ + multiple_task##n##count++; \ + } +multiple_tasks +#undef X +TEST_F(SchedulerTests, MultipleTasks) { +#define X(n) uint8_t taskid##n = Scheduler::register_task(n, &multiple_task_##n); \ + (void)taskid##n; + multiple_tasks +#undef X + + Scheduler::start(); + TIM2_BASE->PSC = 2; // quicker test + constexpr int NUM_TICKS = 300; + for(int i = 0; i < NUM_TICKS; i++) { + for(int j = 0; j <= TIM2_BASE->PSC; j++) TIM2_BASE->inc_cnt_and_check(1); + Scheduler::update(); + } + +#define X(n) EXPECT_EQ(multiple_task##n##count, NUM_TICKS / n); + multiple_tasks +#undef X +} + +TEST_F(SchedulerTests, SameTaskMultipleTimes) { +#define X(n) uint8_t taskid_##n = Scheduler::register_task(n, &multiple_task_1); \ + (void)taskid_##n; + multiple_tasks +#undef X + + multiple_task1count = 0; + Scheduler::start(); + TIM2_BASE->PSC = 2; // quicker test + constexpr int NUM_TICKS = 300; + for(int i = 0; i < NUM_TICKS; i++) { + for(int j = 0; j <= TIM2_BASE->PSC; j++) TIM2_BASE->inc_cnt_and_check(1); + Scheduler::update(); + } + +#define X(n) NUM_TICKS / n + + EXPECT_EQ(multiple_task1count, multiple_tasks 0); +}