From e5b4252c1b97181fc1265370cf6f70431d1a8586 Mon Sep 17 00:00:00 2001 From: SamrutGadde Date: Sat, 11 Oct 2025 17:34:12 -0500 Subject: [PATCH 1/7] Adds basic sync state and calculation. --- CMakeLists.txt | 6 +- Core/Inc/sampling.h | 8 - Core/Inc/sampling.hpp | 71 +++++ Core/Inc/stm32f4xx_it.h | 2 +- Core/Inc/tasks.h | 24 +- Core/Inc/us_timer.h | 7 + Core/Src/gpio.c | 2 +- Core/Src/main.c | 10 +- Core/Src/sampling.c | 20 -- Core/Src/sampling.cpp | 90 ++++++ Core/Src/stm32f4xx_hal_timebase_tim.c | 13 +- Core/Src/tasks.c | 27 -- Core/Src/tasks.cpp | 77 +++++ CustomECU.ioc | 13 +- .../STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c | 4 +- .../Src/stm32f4xx_hal_flash.c | 9 +- STM32F407XX_FLASH.ld | 269 ++++++++++++++++++ cmake/gcc-arm-none-eabi.cmake | 1 + cmake/starm-clang.cmake | 65 +++++ cmake/stm32cubemx/CMakeLists.txt | 158 ++++++---- 20 files changed, 732 insertions(+), 144 deletions(-) delete mode 100644 Core/Inc/sampling.h create mode 100644 Core/Inc/sampling.hpp delete mode 100644 Core/Src/sampling.c create mode 100644 Core/Src/sampling.cpp delete mode 100644 Core/Src/tasks.c create mode 100644 Core/Src/tasks.cpp create mode 100644 STM32F407XX_FLASH.ld create mode 100644 cmake/starm-clang.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index edb260d..a9b5619 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,8 +21,6 @@ endif() # Set the project name set(CMAKE_PROJECT_NAME CustomECU) -# Include toolchain file -include("cmake/gcc-arm-none-eabi.cmake") # Enable compile command to ease indexing with e.g. clangd set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) @@ -48,10 +46,10 @@ target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE # Add sources to executable target_sources(${CMAKE_PROJECT_NAME} PRIVATE # Add user sources here - Core/Src/tasks.c Core/Src/us_timer.c Core/Src/ulog.c - Core/Src/sampling.c + Core/Src/sampling.cpp + Core/Src/tasks.cpp ) # Add include paths diff --git a/Core/Inc/sampling.h b/Core/Inc/sampling.h deleted file mode 100644 index 88c8625..0000000 --- a/Core/Inc/sampling.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef __SAMPLING_H -#define __SAMPLING_H -#include - -extern volatile uint32_t last_crank_time_us; -extern volatile uint32_t last_cam_time_us; - -#endif diff --git a/Core/Inc/sampling.hpp b/Core/Inc/sampling.hpp new file mode 100644 index 0000000..53b7c34 --- /dev/null +++ b/Core/Inc/sampling.hpp @@ -0,0 +1,71 @@ +#ifndef __SAMPLING_H +#define __SAMPLING_H +#include +#include + +/** + * @brief Callback for when a cam tooth is detected. + * @param None + * @retval None + */ +void on_cam_tooth(); + +/** + * @brief Callback for when a crank tooth is detected. + * @param None + * @retval None + */ +void on_crank_tooth(); + +/** + * @brief Get the current fraction of tooth passed since last crank tooth. + * @param None + * @retval Fraction of tooth passed (0.0 to 1.0) + */ +float get_current_fraction_of_tooth(); + +/** + * @brief Get the current engine angle in degrees. + * @param None + * @retval Current engine angle in degrees (0.0 to 720.0) + */ +float get_current_engine_angle(); + +// General Engine Synchronization State. +struct SyncState { + // Whether we have locked synchronization or not. + bool synced; + + // Crank index. + volatile uint8_t crank_index = 0; + + // Monotonic crank counter. + volatile uint64_t crank_counter = 0; + + // Monotonic cam crank counter. + volatile uint64_t cam_crank_counter = 0; + + // Monotonic cam crank counter for last cam seen. + volatile uint64_t last_cam_crank_counter = 0; + + // Last time in micros when we saw a crank tooth. + volatile uint32_t last_crank_time_us = 0; + + // Last time in micros when we saw a cam tooth. + volatile uint32_t last_cam_time_us = 0; + + // Instantaneous period between crank teeth (in microseconds). + volatile double tooth_period_us = 0.0; + + // Engine phase (0 = 0-360, 1 = 360-720) + bool engine_phase; + + // Gets the delta in crank teeth between the last two cam teeth. + inline uint8_t get_cam_delta() { + return cam_crank_counter - last_cam_crank_counter; + }; +}; + +extern struct SyncState syncState; + +#endif diff --git a/Core/Inc/stm32f4xx_it.h b/Core/Inc/stm32f4xx_it.h index 4f4ba78..acb947a 100644 --- a/Core/Inc/stm32f4xx_it.h +++ b/Core/Inc/stm32f4xx_it.h @@ -22,7 +22,7 @@ #define __STM32F4xx_IT_H #ifdef __cplusplus - extern "C" { +extern "C" { #endif /* Private includes ----------------------------------------------------------*/ diff --git a/Core/Inc/tasks.h b/Core/Inc/tasks.h index 9fe1939..ea68a0b 100644 --- a/Core/Inc/tasks.h +++ b/Core/Inc/tasks.h @@ -1,5 +1,10 @@ #ifndef __TASKS_H #define __TASKS_H +#ifdef __cplusplus +extern "C" { +#endif + +#include // Function Prototypes @@ -9,9 +14,24 @@ * spark and fuel injection events. * * This task is critical for engine operation and should run at a high priority. - * @param argument: Not used - * @retval None + * @param argument: Not used. + * @retval None. */ void criticalEngineTask(void *argument); +/** + * @brief Handles synchronization detection with cam and crank signals. + * Specifically for the Honda CBR600CC engine with 12 equally-spaced + * crank teeth and 3 cam teeth, with one offset 30deg. + * + * Updates synced bool in SyncState struct. + * + * @param None. + * @retval None. + */ +void detectSync(void); + +#ifdef __cplusplus +} #endif +#endif /* __TASKS_H */ diff --git a/Core/Inc/us_timer.h b/Core/Inc/us_timer.h index 3f41d74..bf37f40 100644 --- a/Core/Inc/us_timer.h +++ b/Core/Inc/us_timer.h @@ -1,5 +1,9 @@ #ifndef __US_TIMER_H #define __US_TIMER_H +#ifdef __cplusplus +extern "C" { +#endif + #include /** @@ -14,4 +18,7 @@ void init_us_timer(void); */ uint32_t get_micros(void); +#ifdef __cplusplus +} #endif +#endif /* __US_TIMER_H */ diff --git a/Core/Src/gpio.c b/Core/Src/gpio.c index 0ac3258..298e527 100644 --- a/Core/Src/gpio.c +++ b/Core/Src/gpio.c @@ -49,7 +49,7 @@ void MX_GPIO_Init(void) /*Configure GPIO pins : CAM_SIGNAL_Pin CRANK_SIGNAL_Pin */ GPIO_InitStruct.Pin = CAM_SIGNAL_Pin|CRANK_SIGNAL_Pin; - GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; + GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); diff --git a/Core/Src/main.c b/Core/Src/main.c index 994945d..37df40d 100644 --- a/Core/Src/main.c +++ b/Core/Src/main.c @@ -96,9 +96,7 @@ int main(void) /* USER CODE END 2 */ /* Init scheduler */ - osKernelInitialize(); - - /* Call init function for freertos objects (in cmsis_os2.c) */ + osKernelInitialize(); /* Call init function for freertos objects (in cmsis_os2.c) */ MX_FREERTOS_Init(); /* Start scheduler */ @@ -180,7 +178,8 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) /* USER CODE BEGIN Callback 0 */ /* USER CODE END Callback 0 */ - if (htim->Instance == TIM1) { + if (htim->Instance == TIM1) + { HAL_IncTick(); } /* USER CODE BEGIN Callback 1 */ @@ -199,8 +198,7 @@ void Error_Handler(void) /* USER CODE END Error_Handler_Debug */ } - -#ifdef USE_FULL_ASSERT +#ifdef USE_FULL_ASSERT /** * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. diff --git a/Core/Src/sampling.c b/Core/Src/sampling.c deleted file mode 100644 index c4046fb..0000000 --- a/Core/Src/sampling.c +++ /dev/null @@ -1,20 +0,0 @@ -#include "sampling.h" -#include "main.h" -#include "stm32f4xx_hal_gpio.h" -#include "us_timer.h" - -volatile uint32_t last_crank_time_us = 0; -volatile uint32_t last_cam_time_us = 0; - -void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { - switch (GPIO_Pin) { - case CAM_SIGNAL_Pin: - last_cam_time_us = get_micros(); - break; - case CRANK_SIGNAL_Pin: - last_crank_time_us = get_micros(); - break; - default: - break; - } -} diff --git a/Core/Src/sampling.cpp b/Core/Src/sampling.cpp new file mode 100644 index 0000000..3865a39 --- /dev/null +++ b/Core/Src/sampling.cpp @@ -0,0 +1,90 @@ +#include "sampling.hpp" +#include "main.h" +#include "stm32f4xx_hal_gpio.h" +#include "us_timer.h" + +// Tooth pattern is defined as 12 equally spaced +// crank teeth and 3 cam teeth, two opposing one +// 30deg offset. +#define NUM_CRANK_TEETH 12 +#define DEG_BTWN_TEETH (uint8_t)(360 / NUM_CRANK_TEETH) + +// Low-pass smoothing for tooth period calc. Range [0-1], smaller = more +// smoothing. +#define ALPHA 0.8 + +struct SyncState syncState = {}; + +void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { + switch (GPIO_Pin) { + case CAM_SIGNAL_Pin: + on_cam_tooth(); + break; + case CRANK_SIGNAL_Pin: + on_crank_tooth(); + break; + default: + break; + } +} + +void on_crank_tooth() { + uint32_t current_time = get_micros(); + double dt = double(current_time - syncState.last_crank_time_us); + + if (syncState.tooth_period_us <= 0.0) { + syncState.tooth_period_us = dt; + } else { + // Run a exponential moving average to smooth out jitter. + syncState.tooth_period_us = + ALPHA * dt + (1.0 - ALPHA) * syncState.tooth_period_us; + } + + syncState.last_crank_time_us = current_time; + syncState.crank_counter++; + + if (syncState.synced) { + if (syncState.crank_index == NUM_CRANK_TEETH - 1) { + syncState.engine_phase = !syncState.engine_phase; + } + syncState.crank_index = (syncState.crank_index + 1) % NUM_CRANK_TEETH; + } +} + +void on_cam_tooth() { + syncState.last_cam_time_us = get_micros(); + syncState.last_cam_crank_counter = syncState.cam_crank_counter; + syncState.cam_crank_counter = syncState.crank_counter; +} + +float get_current_fraction_of_tooth() { + if (syncState.tooth_period_us <= 0.0) { + return 0.0f; + } + + uint32_t current_time = get_micros(); + float dt = (float)(current_time - syncState.last_crank_time_us); + float fraction = dt / (float)syncState.tooth_period_us; + + // Clamp between 0 and 1 + if (fraction > 1.0f) { + fraction = 1.0f; + } else if (fraction < 0.0f) { + fraction = 0.0f; + } + + return fraction; +} + +float get_current_engine_angle() { + if (!syncState.synced) { + return 0.0f; + } + + float current_fraction = get_current_fraction_of_tooth(); + float angle = (syncState.engine_phase * 360.0f) + + (syncState.crank_index * DEG_BTWN_TEETH) + + (current_fraction * DEG_BTWN_TEETH); + + return angle; +} diff --git a/Core/Src/stm32f4xx_hal_timebase_tim.c b/Core/Src/stm32f4xx_hal_timebase_tim.c index d8b96e6..7cc397b 100644 --- a/Core/Src/stm32f4xx_hal_timebase_tim.c +++ b/Core/Src/stm32f4xx_hal_timebase_tim.c @@ -51,7 +51,7 @@ HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) /* Enable TIM1 clock */ __HAL_RCC_TIM1_CLK_ENABLE(); -/* Get clock configuration */ + /* Get clock configuration */ HAL_RCC_GetClockConfig(&clkconfig, &pFLatency); /* Compute TIM1 clock */ @@ -64,12 +64,11 @@ HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) htim1.Instance = TIM1; /* Initialize TIMx peripheral as follow: - - + Period = [(TIM1CLK/1000) - 1]. to have a (1/1000) s time base. - + Prescaler = (uwTimclock/1000000 - 1) to have a 1MHz counter clock. - + ClockDivision = 0 - + Counter direction = Up - */ + * Period = [(TIM1CLK/1000) - 1]. to have a (1/1000) s time base. + * Prescaler = (uwTimclock/1000000 - 1) to have a 1MHz counter clock. + * ClockDivision = 0 + * Counter direction = Up + */ htim1.Init.Period = (1000000U / 1000U) - 1U; htim1.Init.Prescaler = uwPrescalerValue; htim1.Init.ClockDivision = 0; diff --git a/Core/Src/tasks.c b/Core/Src/tasks.c deleted file mode 100644 index 4d59522..0000000 --- a/Core/Src/tasks.c +++ /dev/null @@ -1,27 +0,0 @@ -#include -#include -#include - -#include "sampling.h" -#include "tasks.h" -#include "us_timer.h" -#include "ulog.h" - -const uint8_t cranking_rpm_threshold = (uint8_t) 400; -const uint32_t cranking_rpm_threshold_us = (cranking_rpm_threshold / 60) * 1000000; - -void criticalEngineTask(void *argument) { - for (;;) { - const uint32_t cur_micros = get_micros(); - bool is_engine_turning = - cur_micros - last_crank_time_us > cranking_rpm_threshold_us; - - ULOG_DEBUG("CRANK TIME: %d", last_crank_time_us); - - if (!is_engine_turning) { - // TODO: Run wasted spark/semi-sequential. - } - - // TODO: Run nominal engine injection and spark. - } -} diff --git a/Core/Src/tasks.cpp b/Core/Src/tasks.cpp new file mode 100644 index 0000000..1d401b1 --- /dev/null +++ b/Core/Src/tasks.cpp @@ -0,0 +1,77 @@ +#include +#include + +#include "sampling.hpp" +#include "tasks.h" +#include "ulog.h" +#include "us_timer.h" + +const uint8_t cranking_rpm_threshold = (uint8_t)400; +const uint32_t cranking_rpm_threshold_us = + (cranking_rpm_threshold / 60) * 1000000; + +uint32_t last_time = 0; +uint32_t time = 0; + +void detectSync(void) { + // Synchronization is determined when we see first three cam teeth. + // Use num crank pulses between cam teeth detections to determine engine + // cycle. + // + // diff btwn teeth == 330deg in crank: 0deg in cycle. + // diff btwn teeth == 360deg in crank: 360deg into cycle. + // diff btwn teeth == 30deg in crank: 390deg into cycle. + uint8_t delta_teeth = syncState.get_cam_delta(); + ULOG_DEBUG("Delta teeth: %d", delta_teeth); + switch (delta_teeth) { + case 1: + syncState.crank_index = 1; + syncState.engine_phase = 1; + syncState.synced = true; + break; + case 11: + syncState.crank_index = 10; + syncState.engine_phase = 0; + syncState.synced = true; + break; + case 12: + syncState.crank_index = 0; + syncState.engine_phase = 0; + syncState.synced = true; + break; + default: + syncState.synced = false; + break; + } +} + +void criticalEngineTask(void *argument) { + for (;;) { + while (!syncState.synced) { + detectSync(); + } + + time = get_micros(); + uint32_t dt = time - last_time; + uint32_t dt_ms = dt / 1000; + + // Print every second. + // if (dt >= 1000000) { + // last_time = time; + // ULOG_INFO("Detected Sync! Current Crank Angle: %f", + // syncState.current_engine_angle); + // ULOG_INFO("Fraction of tooth: %f", syncState.fraction_of_tooth); + // ULOG_INFO("Tooth period (us): %f", syncState.tooth_period_us); + // ULOG_INFO("Crank index: %d", syncState.crank_index); + // ULOG_INFO("Engine phase: %d", syncState.engine_phase); + // } + + if (dt_ms >= 10) { + last_time = time; + float current_angle = get_current_engine_angle(); + float current_fraction = get_current_fraction_of_tooth(); + ULOG_DEBUG("Crank Angle: %f // Tooth Period: %f // Fraction: %f", + current_angle, syncState.tooth_period_us, current_fraction); + } + } +} diff --git a/CustomECU.ioc b/CustomECU.ioc index eaee3c6..49bf000 100644 --- a/CustomECU.ioc +++ b/CustomECU.ioc @@ -36,8 +36,8 @@ Mcu.PinsNb=13 Mcu.ThirdPartyNb=0 Mcu.UserConstants= Mcu.UserName=STM32F407VETx -MxCube.Version=6.13.0 -MxDb.Version=DB.6.0.130 +MxCube.Version=6.15.0 +MxDb.Version=DB.6.0.150 NVIC.BusFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false NVIC.DebugMonitor_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false NVIC.EXTI15_10_IRQn=true\:5\:0\:false\:false\:true\:true\:true\:true\:true @@ -68,12 +68,14 @@ PA3.Mode=Asynchronous PA3.Signal=USART2_RX PA9.Mode=Asynchronous PA9.Signal=USART1_TX -PB13.GPIOParameters=GPIO_Label +PB13.GPIOParameters=GPIO_Label,GPIO_ModeDefaultEXTI PB13.GPIO_Label=CAM_SIGNAL +PB13.GPIO_ModeDefaultEXTI=GPIO_MODE_IT_FALLING PB13.Locked=true PB13.Signal=GPXTI13 -PB14.GPIOParameters=GPIO_Label +PB14.GPIOParameters=GPIO_Label,GPIO_ModeDefaultEXTI PB14.GPIO_Label=CRANK_SIGNAL +PB14.GPIO_ModeDefaultEXTI=GPIO_MODE_IT_FALLING PB14.Locked=true PB14.Signal=GPXTI14 PH0-OSC_IN.Mode=HSE-External-Oscillator @@ -83,6 +85,7 @@ PH1-OSC_OUT.Signal=RCC_OSC_OUT PinOutPanel.RotationAngle=0 ProjectManager.AskForMigrate=true ProjectManager.BackupPrevious=false +ProjectManager.CompilerLinker=GCC ProjectManager.CompilerOptimize=6 ProjectManager.ComputerToolchain=false ProjectManager.CoupleFile=true @@ -90,7 +93,7 @@ ProjectManager.CustomerFirmwarePackage= ProjectManager.DefaultFWLocation=true ProjectManager.DeletePrevious=true ProjectManager.DeviceId=STM32F407VETx -ProjectManager.FirmwarePackage=STM32Cube FW_F4 V1.28.2 +ProjectManager.FirmwarePackage=STM32Cube FW_F4 V1.28.3 ProjectManager.FreePins=false ProjectManager.HalAssertFull=false ProjectManager.HeapSize=0x200 diff --git a/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c b/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c index d186ce5..862ec73 100644 --- a/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c +++ b/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c @@ -50,11 +50,11 @@ * @{ */ /** - * @brief STM32F4xx HAL Driver version number V1.8.4 + * @brief STM32F4xx HAL Driver version number V1.8.5 */ #define __STM32F4xx_HAL_VERSION_MAIN (0x01U) /*!< [31:24] main version */ #define __STM32F4xx_HAL_VERSION_SUB1 (0x08U) /*!< [23:16] sub1 version */ -#define __STM32F4xx_HAL_VERSION_SUB2 (0x04U) /*!< [15:8] sub2 version */ +#define __STM32F4xx_HAL_VERSION_SUB2 (0x05U) /*!< [15:8] sub2 version */ #define __STM32F4xx_HAL_VERSION_RC (0x00U) /*!< [7:0] release candidate */ #define __STM32F4xx_HAL_VERSION ((__STM32F4xx_HAL_VERSION_MAIN << 24U)\ |(__STM32F4xx_HAL_VERSION_SUB1 << 16U)\ diff --git a/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.c b/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.c index 29c60e3..808949e 100644 --- a/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.c +++ b/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.c @@ -101,7 +101,14 @@ * @{ */ /* Variable used for Erase sectors under interruption */ -FLASH_ProcessTypeDef pFlash; +FLASH_ProcessTypeDef pFlash = {.ProcedureOnGoing = FLASH_PROC_NONE, + .NbSectorsToErase = 0U, + .VoltageForErase= FLASH_VOLTAGE_RANGE_1, + .Sector = 0U, + .Bank = FLASH_BANK_1, + .Address = 0U, + .Lock = HAL_UNLOCKED, + .ErrorCode = HAL_FLASH_ERROR_NONE}; /** * @} */ diff --git a/STM32F407XX_FLASH.ld b/STM32F407XX_FLASH.ld new file mode 100644 index 0000000..db159fd --- /dev/null +++ b/STM32F407XX_FLASH.ld @@ -0,0 +1,269 @@ +/* +****************************************************************************** +** + +** File : LinkerScript.ld +** +** Author : STM32CubeMX +** +** Abstract : Linker script for STM32F407VETx series +** 512Kbytes FLASH and 192Kbytes RAM +** +** Set heap size, stack size and stack location according +** to application requirements. +** +** Set memory bank area and size if external memory is used. +** +** Target : STMicroelectronics STM32 +** +** Distribution: The file is distributed “as is,” without any warranty +** of any kind. +** +***************************************************************************** +** @attention +** +**

© COPYRIGHT(c) 2025 STMicroelectronics

+** +** Redistribution and use in source and binary forms, with or without modification, +** are permitted provided that the following conditions are met: +** 1. Redistributions of source code must retain the above copyright notice, +** this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** 3. Neither the name of STMicroelectronics nor the names of its contributors +** may be used to endorse or promote products derived from this software +** without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +***************************************************************************** +*/ + +/* Entry Point */ +ENTRY(Reset_Handler) + +/* Specify the memory areas */ +MEMORY +{ +RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K +CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K +FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K +} + +/* Highest address of the user mode stack */ +_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of RAM */ +/* Generate a link error if heap and stack don't fit into RAM */ +_Min_Heap_Size = 0x200; /* required amount of heap */ +_Min_Stack_Size = 0x400; /* required amount of stack */ + +/* Define output sections */ +SECTIONS +{ + /* The startup code goes first into FLASH */ + .isr_vector : + { + . = ALIGN(4); + KEEP(*(.isr_vector)) /* Startup code */ + . = ALIGN(4); + } >FLASH + + /* The program code and other data goes into FLASH */ + .text : + { + . = ALIGN(4); + *(.text) /* .text sections (code) */ + *(.text*) /* .text* sections (code) */ + *(.glue_7) /* glue arm to thumb code */ + *(.glue_7t) /* glue thumb to arm code */ + *(.eh_frame) + + KEEP (*(.init)) + KEEP (*(.fini)) + + . = ALIGN(4); + _etext = .; /* define a global symbols at end of code */ + } >FLASH + + /* Constant data goes into FLASH */ + .rodata : + { + . = ALIGN(4); + *(.rodata) /* .rodata sections (constants, strings, etc.) */ + *(.rodata*) /* .rodata* sections (constants, strings, etc.) */ + . = ALIGN(4); + } >FLASH + + .ARM.extab (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ + { + . = ALIGN(4); + *(.ARM.extab* .gnu.linkonce.armextab.*) + . = ALIGN(4); + } >FLASH + + .ARM (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ + { + . = ALIGN(4); + __exidx_start = .; + *(.ARM.exidx*) + __exidx_end = .; + . = ALIGN(4); + } >FLASH + + .preinit_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ + { + . = ALIGN(4); + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP (*(.preinit_array*)) + PROVIDE_HIDDEN (__preinit_array_end = .); + . = ALIGN(4); + } >FLASH + + .init_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ + { + . = ALIGN(4); + PROVIDE_HIDDEN (__init_array_start = .); + KEEP (*(SORT(.init_array.*))) + KEEP (*(.init_array*)) + PROVIDE_HIDDEN (__init_array_end = .); + . = ALIGN(4); + } >FLASH + + .fini_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ + { + . = ALIGN(4); + PROVIDE_HIDDEN (__fini_array_start = .); + KEEP (*(SORT(.fini_array.*))) + KEEP (*(.fini_array*)) + PROVIDE_HIDDEN (__fini_array_end = .); + . = ALIGN(4); + } >FLASH + + _siccmram = LOADADDR(.ccmram); + + /* CCM-RAM section + * + * IMPORTANT NOTE! + * If initialized variables will be placed in this section, + * the startup code needs to be modified to copy the init-values. + */ + .ccmram : + { + . = ALIGN(4); + _sccmram = .; /* create a global symbol at ccmram start */ + *(.ccmram) + *(.ccmram*) + + . = ALIGN(4); + _eccmram = .; /* create a global symbol at ccmram end */ + } >CCMRAM AT> FLASH + + /* used by the startup to initialize data */ + _sidata = LOADADDR(.data); + + /* Initialized data sections goes into RAM, load LMA copy after code */ + .data : + { + . = ALIGN(4); + _sdata = .; /* create a global symbol at data start */ + *(.data) /* .data sections */ + *(.data*) /* .data* sections */ + *(.RamFunc) /* .RamFunc sections */ + *(.RamFunc*) /* .RamFunc* sections */ + + . = ALIGN(4); + } >RAM AT> FLASH + + /* Initialized TLS data section */ + .tdata : ALIGN(4) + { + *(.tdata .tdata.* .gnu.linkonce.td.*) + . = ALIGN(4); + _edata = .; /* define a global symbol at data end */ + PROVIDE(__data_end = .); + PROVIDE(__tdata_end = .); + } >RAM AT> FLASH + + PROVIDE( __tdata_start = ADDR(.tdata) ); + PROVIDE( __tdata_size = __tdata_end - __tdata_start ); + + PROVIDE( __data_start = ADDR(.data) ); + PROVIDE( __data_size = __data_end - __data_start ); + + PROVIDE( __tdata_source = LOADADDR(.tdata) ); + PROVIDE( __tdata_source_end = LOADADDR(.tdata) + SIZEOF(.tdata) ); + PROVIDE( __tdata_source_size = __tdata_source_end - __tdata_source ); + + PROVIDE( __data_source = LOADADDR(.data) ); + PROVIDE( __data_source_end = __tdata_source_end ); + PROVIDE( __data_source_size = __data_source_end - __data_source ); + /* Uninitialized data section */ + .tbss (NOLOAD) : ALIGN(4) + { + /* This is used by the startup in order to initialize the .bss secion */ + _sbss = .; /* define a global symbol at bss start */ + __bss_start__ = _sbss; + *(.tbss .tbss.*) + . = ALIGN(4); + PROVIDE( __tbss_end = . ); + } >RAM + + PROVIDE( __tbss_start = ADDR(.tbss) ); + PROVIDE( __tbss_size = __tbss_end - __tbss_start ); + PROVIDE( __tbss_offset = ADDR(.tbss) - ADDR(.tdata) ); + + PROVIDE( __tls_base = __tdata_start ); + PROVIDE( __tls_end = __tbss_end ); + PROVIDE( __tls_size = __tls_end - __tls_base ); + PROVIDE( __tls_align = MAX(ALIGNOF(.tdata), ALIGNOF(.tbss)) ); + PROVIDE( __tls_size_align = (__tls_size + __tls_align - 1) & ~(__tls_align - 1) ); + PROVIDE( __arm32_tls_tcb_offset = MAX(8, __tls_align) ); + PROVIDE( __arm64_tls_tcb_offset = MAX(16, __tls_align) ); + + .bss (NOLOAD) : ALIGN(4) + { + *(.bss) + *(.bss*) + *(COMMON) + + . = ALIGN(4); + _ebss = .; /* define a global symbol at bss end */ + __bss_end__ = _ebss; + PROVIDE( __bss_end = .); + } >RAM + PROVIDE( __non_tls_bss_start = ADDR(.bss) ); + + PROVIDE( __bss_start = __tbss_start ); + PROVIDE( __bss_size = __bss_end - __bss_start ); + + /* User_heap_stack section, used to check that there is enough RAM left */ + ._user_heap_stack (NOLOAD) : + { + . = ALIGN(8); + PROVIDE ( end = . ); + PROVIDE ( _end = . ); + . = . + _Min_Heap_Size; + . = . + _Min_Stack_Size; + . = ALIGN(8); + } >RAM + + + + /* Remove information from the standard libraries */ + /DISCARD/ : + { + libc.a:* ( * ) + libm.a:* ( * ) + libgcc.a:* ( * ) + } + +} diff --git a/cmake/gcc-arm-none-eabi.cmake b/cmake/gcc-arm-none-eabi.cmake index 188cc77..a52897a 100644 --- a/cmake/gcc-arm-none-eabi.cmake +++ b/cmake/gcc-arm-none-eabi.cmake @@ -41,6 +41,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fno-rtti -fno-exceptions -fno-threadsafe- set(CMAKE_C_LINK_FLAGS "${TARGET_FLAGS}") set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -T \"${CMAKE_SOURCE_DIR}/stm32f407vetx_flash.ld\"") set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} --specs=nano.specs") +set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -u _printf_float -u _scanf_float") set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,-Map=${CMAKE_PROJECT_NAME}.map -Wl,--gc-sections") set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group -lc -lm -Wl,--end-group") set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--print-memory-usage") diff --git a/cmake/starm-clang.cmake b/cmake/starm-clang.cmake new file mode 100644 index 0000000..36a65ee --- /dev/null +++ b/cmake/starm-clang.cmake @@ -0,0 +1,65 @@ +set(CMAKE_SYSTEM_NAME Generic) +set(CMAKE_SYSTEM_PROCESSOR arm) + +set(CMAKE_C_COMPILER_ID Clang) +set(CMAKE_CXX_COMPILER_ID Clang) + +# Some default llvm settings +set(TOOLCHAIN_PREFIX starm-) + +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}clang) +set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}clang++) +set(CMAKE_LINKER ${TOOLCHAIN_PREFIX}clang) +set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) +set(CMAKE_SIZE ${TOOLCHAIN_PREFIX}size) + +set(CMAKE_EXECUTABLE_SUFFIX_ASM ".elf") +set(CMAKE_EXECUTABLE_SUFFIX_C ".elf") +set(CMAKE_EXECUTABLE_SUFFIX_CXX ".elf") + +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + +# STARM_TOOLCHAIN_CONFIG allows you to choose the toolchain configuration. +# Possible values are: +# "STARM_HYBRID" : Hybrid configuration using starm-clang Assemler and Compiler and GNU Linker +# "STARM_NEWLIB" : starm-clang toolchain with NEWLIB C library +# "STARM_PICOLIBC" : starm-clang toolchain with PICOLIBC C library +set(STARM_TOOLCHAIN_CONFIG "STARM_HYBRID") + +if(STARM_TOOLCHAIN_CONFIG STREQUAL "STARM_HYBRID") + set(TOOLCHAIN_MULTILIBS "--multi-lib-config=\"$ENV{CLANG_GCC_CMSIS_COMPILER}/multilib.gnu_tools_for_stm32.yaml\" --gcc-toolchain=\"$ENV{GCC_TOOLCHAIN_ROOT}/..\"") +elseif (STARM_TOOLCHAIN_CONFIG STREQUAL "STARM_NEWLIB") + set(TOOLCHAIN_MULTILIBS "--config=newlib.cfg") +endif() + +# MCU specific flags +set(TARGET_FLAGS "-mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard ${TOOLCHAIN_MULTILIBS}") + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${TARGET_FLAGS}") +set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} -x assembler-with-cpp -MP") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -fdata-sections -ffunction-sections") + +set(CMAKE_C_FLAGS_DEBUG "-O0 -g3") +set(CMAKE_C_FLAGS_RELEASE "-Os -g0") +set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g3") +set(CMAKE_CXX_FLAGS_RELEASE "-Os -g0") + +set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fno-rtti -fno-exceptions -fno-threadsafe-statics") + +set(CMAKE_EXE_LINKER_FLAGS "${TARGET_FLAGS}") + +if (STARM_TOOLCHAIN_CONFIG STREQUAL "STARM_HYBRID") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --gcc-specs=nano.specs") + set(TOOLCHAIN_LINK_LIBRARIES "m") +elseif(STARM_TOOLCHAIN_CONFIG STREQUAL "STARM_NEWLIB") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lcrt0-nosys") +elseif(STARM_TOOLCHAIN_CONFIG STREQUAL "STARM_PICOLIBC") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lcrt0-hosted") + +endif() + +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T \"${CMAKE_SOURCE_DIR}/STM32F407XX_FLASH.ld\"") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Map=${CMAKE_PROJECT_NAME}.map -Wl,--gc-sections") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -z noexecstack") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--print-memory-usage ") diff --git a/cmake/stm32cubemx/CMakeLists.txt b/cmake/stm32cubemx/CMakeLists.txt index 6303d58..9879c00 100644 --- a/cmake/stm32cubemx/CMakeLists.txt +++ b/cmake/stm32cubemx/CMakeLists.txt @@ -1,78 +1,116 @@ cmake_minimum_required(VERSION 3.22) - -project(stm32cubemx) -add_library(stm32cubemx INTERFACE) - # Enable CMake support for ASM and C languages enable_language(C ASM) - -target_compile_definitions(stm32cubemx INTERFACE +# STM32CubeMX generated symbols (macros) +set(MX_Defines_Syms USE_HAL_DRIVER STM32F407xx $<$:DEBUG> ) -target_include_directories(stm32cubemx INTERFACE - ../../Core/Inc - ../../Drivers/STM32F4xx_HAL_Driver/Inc - ../../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy - ../../Middlewares/Third_Party/FreeRTOS/Source/include - ../../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2 - ../../Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F - ../../Drivers/CMSIS/Device/ST/STM32F4xx/Include - ../../Drivers/CMSIS/Include +# STM32CubeMX generated include paths +set(MX_Include_Dirs + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Inc + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Inc + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2 + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/CMSIS/Device/ST/STM32F4xx/Include + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/CMSIS/Include ) -target_sources(stm32cubemx INTERFACE - ../../Core/Src/main.c - ../../Core/Src/gpio.c - ../../Core/Src/freertos.c - ../../Core/Src/tim.c - ../../Core/Src/usart.c - ../../Core/Src/stm32f4xx_it.c - ../../Core/Src/stm32f4xx_hal_msp.c - ../../Core/Src/stm32f4xx_hal_timebase_tim.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim_ex.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc_ex.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ex.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ramfunc.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma_ex.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr_ex.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_cortex.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_exti.c - ../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_uart.c - ../../Core/Src/system_stm32f4xx.c - ../../Middlewares/Third_Party/FreeRTOS/Source/croutine.c - ../../Middlewares/Third_Party/FreeRTOS/Source/event_groups.c - ../../Middlewares/Third_Party/FreeRTOS/Source/list.c - ../../Middlewares/Third_Party/FreeRTOS/Source/queue.c - ../../Middlewares/Third_Party/FreeRTOS/Source/stream_buffer.c - ../../Middlewares/Third_Party/FreeRTOS/Source/tasks.c - ../../Middlewares/Third_Party/FreeRTOS/Source/timers.c - ../../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.c - ../../Middlewares/Third_Party/FreeRTOS/Source/portable/MemMang/heap_4.c - ../../Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c - ../../Core/Src/sysmem.c - ../../Core/Src/syscalls.c - ../../startup_stm32f407xx.s -) +# STM32CubeMX generated application sources +set(MX_Application_Src + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/main.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/gpio.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/freertos.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/tim.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/usart.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/stm32f4xx_it.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/stm32f4xx_hal_msp.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/stm32f4xx_hal_timebase_tim.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/sysmem.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/syscalls.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../startup_stm32f407xx.s +) -target_link_directories(stm32cubemx INTERFACE +# STM32 HAL/LL Drivers +set(STM32_Drivers_Src + ${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Src/system_stm32f4xx.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim_ex.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc_ex.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ex.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ramfunc.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma_ex.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr_ex.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_cortex.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_exti.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_uart.c +) + +# Drivers Midllewares + + +set(FreeRTOS_Src + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/croutine.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/event_groups.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/list.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/queue.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/stream_buffer.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/tasks.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/timers.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/portable/MemMang/heap_4.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c ) -target_link_libraries(stm32cubemx INTERFACE +# Link directories setup +set(MX_LINK_DIRS + +) +# Project static libraries +set(MX_LINK_LIBS + STM32_Drivers + ${TOOLCHAIN_LINK_LIBRARIES} + FreeRTOS ) +# Interface library for includes and symbols +add_library(stm32cubemx INTERFACE) +target_include_directories(stm32cubemx INTERFACE ${MX_Include_Dirs}) +target_compile_definitions(stm32cubemx INTERFACE ${MX_Defines_Syms}) + +# Create STM32_Drivers static library +add_library(STM32_Drivers OBJECT) +target_sources(STM32_Drivers PRIVATE ${STM32_Drivers_Src}) +target_link_libraries(STM32_Drivers PUBLIC stm32cubemx) + + +# Create FreeRTOS static library +add_library(FreeRTOS OBJECT) +target_sources(FreeRTOS PRIVATE ${FreeRTOS_Src}) +target_link_libraries(FreeRTOS PUBLIC stm32cubemx) + +# Add STM32CubeMX generated application sources to the project +target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${MX_Application_Src}) + +# Link directories setup +target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${MX_LINK_DIRS}) + +# Add libraries to the project +target_link_libraries(${CMAKE_PROJECT_NAME} ${MX_LINK_LIBS}) + +# Add the map file to the list of files to be removed with 'clean' target +set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES ADDITIONAL_CLEAN_FILES ${CMAKE_PROJECT_NAME}.map) # Validate that STM32CubeMX code is compatible with C standard -if(CMAKE_C_STANDARD LESS 11) +if((CMAKE_C_STANDARD EQUAL 90) OR (CMAKE_C_STANDARD EQUAL 99)) message(ERROR "Generated code requires C11 or higher") endif() - - From 3fa73d7a3f2b34282d3f6abb3ab1db62b306d493 Mon Sep 17 00:00:00 2001 From: SamrutGadde Date: Sat, 11 Oct 2025 18:46:16 -0500 Subject: [PATCH 2/7] Adds unity tests for sampling and sync detection. --- CMakeLists.txt | 17 +- Core/Src/sampling.cpp | 28 ++- Core/Src/tasks.cpp | 5 - run_tests.sh | 10 +- test/CMakeLists.txt | 83 ++++++- test/README.md | 191 ++++++++++++++++ test/mocks/cmsis_os2.c | 37 +++ test/mocks/cmsis_os2.h | 74 ++++++ test/mocks/mock_hal.h | 35 +++ test/mocks/mock_main.h | 24 ++ test/mocks/mock_us_timer.c | 14 ++ test/mocks/mock_us_timer.h | 22 ++ test/mocks/stm32f4xx_hal.c | 10 + test/mocks/stm32f4xx_hal.h | 63 ++++++ test/mocks/stm32f4xx_hal_gpio.c | 39 ++++ test/mocks/stm32f4xx_hal_gpio.h | 59 +++++ test/mocks/ulog.c | 6 + test/mocks/ulog.h | 31 +++ test/unit/test_sample.c | 12 - test/unit/test_sampling.cpp | 362 ++++++++++++++++++++++++++++++ test/unit/test_sync_detection.cpp | 210 +++++++++++++++++ 21 files changed, 1291 insertions(+), 41 deletions(-) create mode 100644 test/README.md create mode 100644 test/mocks/cmsis_os2.c create mode 100644 test/mocks/cmsis_os2.h create mode 100644 test/mocks/mock_hal.h create mode 100644 test/mocks/mock_main.h create mode 100644 test/mocks/mock_us_timer.c create mode 100644 test/mocks/mock_us_timer.h create mode 100644 test/mocks/stm32f4xx_hal.c create mode 100644 test/mocks/stm32f4xx_hal.h create mode 100644 test/mocks/stm32f4xx_hal_gpio.c create mode 100644 test/mocks/stm32f4xx_hal_gpio.h create mode 100644 test/mocks/ulog.c create mode 100644 test/mocks/ulog.h delete mode 100644 test/unit/test_sample.c create mode 100644 test/unit/test_sampling.cpp create mode 100644 test/unit/test_sync_detection.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a9b5619..865e224 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,12 @@ cmake_minimum_required(VERSION 3.22) # User is free to modify the file as much as necessary # +# Include toolchain file before project() if not already set +# (CMake presets will set this automatically) +if(NOT CMAKE_TOOLCHAIN_FILE) + set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/gcc-arm-none-eabi.cmake") +endif() + # Setup compiler settings set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -21,16 +27,15 @@ endif() # Set the project name set(CMAKE_PROJECT_NAME CustomECU) +# Core project settings +project(${CMAKE_PROJECT_NAME}) +message("Build type: " ${CMAKE_BUILD_TYPE}) # Enable compile command to ease indexing with e.g. clangd set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) -# Enable CMake support for ASM and C languages -enable_language(C ASM) - -# Core project settings -project(${CMAKE_PROJECT_NAME}) -message("Build type: " ${CMAKE_BUILD_TYPE}) +# Enable CMake support for ASM language (after project) +enable_language(ASM) # Create an executable object type add_executable(${CMAKE_PROJECT_NAME}) diff --git a/Core/Src/sampling.cpp b/Core/Src/sampling.cpp index 3865a39..5323073 100644 --- a/Core/Src/sampling.cpp +++ b/Core/Src/sampling.cpp @@ -30,14 +30,19 @@ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { void on_crank_tooth() { uint32_t current_time = get_micros(); - double dt = double(current_time - syncState.last_crank_time_us); - if (syncState.tooth_period_us <= 0.0) { - syncState.tooth_period_us = dt; - } else { - // Run a exponential moving average to smooth out jitter. - syncState.tooth_period_us = - ALPHA * dt + (1.0 - ALPHA) * syncState.tooth_period_us; + // Only calculate period if we've seen at least one tooth before + if (syncState.crank_counter > 0) { + double dt = double(current_time - syncState.last_crank_time_us); + + if (syncState.tooth_period_us <= 0.0) { + // First valid period measurement + syncState.tooth_period_us = dt; + } else { + // Run a exponential moving average to smooth out jitter. + syncState.tooth_period_us = + ALPHA * dt + (1.0 - ALPHA) * syncState.tooth_period_us; + } } syncState.last_crank_time_us = current_time; @@ -63,14 +68,19 @@ float get_current_fraction_of_tooth() { } uint32_t current_time = get_micros(); + + // Handle case where current_time is less than last_crank_time_us + // (shouldn't happen in normal operation, but guard against it) + if (current_time < syncState.last_crank_time_us) { + return 0.0f; + } + float dt = (float)(current_time - syncState.last_crank_time_us); float fraction = dt / (float)syncState.tooth_period_us; // Clamp between 0 and 1 if (fraction > 1.0f) { fraction = 1.0f; - } else if (fraction < 0.0f) { - fraction = 0.0f; } return fraction; diff --git a/Core/Src/tasks.cpp b/Core/Src/tasks.cpp index 1d401b1..f08d489 100644 --- a/Core/Src/tasks.cpp +++ b/Core/Src/tasks.cpp @@ -6,10 +6,6 @@ #include "ulog.h" #include "us_timer.h" -const uint8_t cranking_rpm_threshold = (uint8_t)400; -const uint32_t cranking_rpm_threshold_us = - (cranking_rpm_threshold / 60) * 1000000; - uint32_t last_time = 0; uint32_t time = 0; @@ -22,7 +18,6 @@ void detectSync(void) { // diff btwn teeth == 360deg in crank: 360deg into cycle. // diff btwn teeth == 30deg in crank: 390deg into cycle. uint8_t delta_teeth = syncState.get_cam_delta(); - ULOG_DEBUG("Delta teeth: %d", delta_teeth); switch (delta_teeth) { case 1: syncState.crank_index = 1; diff --git a/run_tests.sh b/run_tests.sh index 4e3a82b..b74236f 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -32,9 +32,15 @@ cmake .. echo -e "${YELLOW}Building tests...${NC}" make +# Create symlink to compile_commands.json for IDE support +if [ -f "compile_commands.json" ]; then + echo -e "${YELLOW}Creating symlink to compile_commands.json...${NC}" + ln -sf build/compile_commands.json ../compile_commands.json +fi + # Run tests -echo -e "\n${YELLOW}Running tests...${NC}\n" -if ./run_tests; then +echo -e "\n${YELLOW}Running tests with CTest...${NC}\n" +if ctest --output-on-failure --verbose; then echo -e "\n${GREEN}✓ All tests passed!${NC}" exit 0 else diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 20ef0b0..0baaac8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,20 +1,89 @@ cmake_minimum_required(VERSION 3.16) # Use host compiler for tests (set before project()) +# Don't use CACHE to avoid contaminating the main build if(NOT CMAKE_C_COMPILER) set(CMAKE_C_COMPILER gcc) endif() +if(NOT CMAKE_CXX_COMPILER) + set(CMAKE_CXX_COMPILER g++) +endif() + +project(CustomECU_Tests C CXX) + +# C++ standard for sampling.cpp +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Generate compile_commands.json for IDE support +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -project(CustomECU_Tests C) +# Configure Unity before adding it +set(UNITY_EXTENSION_FIXTURE OFF CACHE BOOL "Unity fixture extension") +set(UNITY_EXTENSION_MEMORY OFF CACHE BOOL "Unity memory extension") # Add Unity add_subdirectory(frameworks/Unity) -# Test sources -file(GLOB TEST_SOURCES unit/*.c) +# Enable double precision in Unity library +target_compile_definitions(unity PUBLIC + UNITY_INCLUDE_DOUBLE + UNITY_SUPPORT_64 + UNITY_INCLUDE_FLOAT +) + +# Include directories (mocks first to override real headers) +include_directories( + ${CMAKE_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/mocks + ${CMAKE_SOURCE_DIR}/../Core/Inc + frameworks/Unity/src +) + +# Mock sources +set(MOCK_SOURCES + mocks/mock_us_timer.c + mocks/stm32f4xx_hal.c + mocks/stm32f4xx_hal_gpio.c + mocks/cmsis_os2.c + mocks/ulog.c +) + +# Source under test (sampling.cpp) +set(SOURCE_UNDER_TEST + ${CMAKE_SOURCE_DIR}/../Core/Src/sampling.cpp +) + +# Test 1: Sampling tests +add_executable(test_sampling + unit/test_sampling.cpp + ${MOCK_SOURCES} + ${SOURCE_UNDER_TEST} +) +target_include_directories(test_sampling PRIVATE + ${CMAKE_SOURCE_DIR}/../Core/Inc + ${CMAKE_SOURCE_DIR}/mocks + frameworks/Unity/src +) +target_link_libraries(test_sampling unity) +target_compile_options(test_sampling PRIVATE -DUNIT_TEST) -add_executable(run_tests ${TEST_SOURCES}) -target_include_directories(run_tests PRIVATE frameworks/Unity/src) -target_link_libraries(run_tests unity) +# Test 2: Sync detection tests +add_executable(test_sync_detection + unit/test_sync_detection.cpp + ${MOCK_SOURCES} + ${SOURCE_UNDER_TEST} + ${CMAKE_SOURCE_DIR}/../Core/Src/tasks.cpp +) +target_include_directories(test_sync_detection PRIVATE + ${CMAKE_SOURCE_DIR}/../Core/Inc + ${CMAKE_SOURCE_DIR}/mocks + frameworks/Unity/src +) +target_link_libraries(test_sync_detection unity) +target_compile_options(test_sync_detection PRIVATE -DUNIT_TEST) -add_test(NAME CustomECU_Tests COMMAND run_tests) +# Add tests to CTest +enable_testing() +add_test(NAME SamplingTests COMMAND test_sampling) +add_test(NAME SyncDetectionTests COMMAND test_sync_detection) diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..cc902d6 --- /dev/null +++ b/test/README.md @@ -0,0 +1,191 @@ +# Unit Testing Setup for CustomECU + +This project uses Unity test framework for unit testing the synchronization components. + +## Directory Structure + +``` +test/ +├── CMakeLists.txt # Test build configuration +├── frameworks/ # Test frameworks (submodules) +│ ├── Unity/ # Unity test framework +│ └── CMock/ # CMock mocking framework +├── mocks/ # Mock implementations for embedded dependencies +│ ├── cmsis_os2.h/c # CMSIS-RTOS2 mocks +│ ├── stm32f4xx_hal.h/c # STM32 HAL mocks +│ ├── mock_us_timer.h/c # Microsecond timer mocks +│ ├── mock_main.h/c # Main header mocks +│ └── ulog.h/c # Logging system mocks +└── unit/ # Unit test files + ├── test_sampling.cpp # Tests for sampling.cpp + └── test_sync_detection.cpp # Tests for sync detection logic +``` + +## Building and Running Tests + +### Quick Start + +From the project root directory: + +```bash +./run_tests.sh +``` + +### Manual Build + +```bash +cd test +mkdir -p build +cd build +cmake .. +make +ctest --verbose +``` + +Or run individual test executables: + +```bash +./test_sampling +./test_sync_detection +``` + +## Test Coverage + +### test_sampling.cpp +Tests the core sampling functionality: +- Initial state validation +- Crank tooth detection and counter increment +- Tooth period calculation with exponential moving average +- Cam tooth detection and counter tracking +- Real-time fraction of tooth calculation +- Real-time engine angle calculation +- Crank index rollover behavior +- Engine phase transitions + +### test_sync_detection.cpp +Tests the synchronization detection logic: +- Sync detection with valid cam deltas (1, 11, 12 teeth) +- Sync rejection with invalid cam deltas +- Full synchronization sequence +- Sync loss and re-establishment +- Edge cases and counter overflow protection + +## Mock Strategy + +The test environment mocks all embedded dependencies: + +1. **HAL Layer**: STM32 Hardware Abstraction Layer functions +2. **CMSIS-RTOS**: FreeRTOS CMSIS wrapper functions +3. **Timers**: Microsecond timer with controllable time +4. **Logging**: ULOG macros redirect to printf for test visibility + +### Controllable Mock Timer + +The `mock_us_timer` provides full control over time in tests: + +```cpp +// Set specific time +mock_us_timer_set_time(1000); + +// Advance time +mock_us_timer_advance(500); + +// Reset to zero +mock_us_timer_reset(); +``` + +This allows deterministic testing of time-dependent behavior. + +## Adding New Tests + +1. Create a new test file in `test/unit/`: + +```cpp +#include "unity.h" +#include "mock_us_timer.h" +#include "sampling.hpp" + +extern "C" void setUp(void) { + // Reset state before each test + syncState.synced = false; + // ... reset other fields +} + +extern "C" void tearDown(void) { + // Cleanup after each test +} + +extern "C" void test_my_new_feature(void) { + // Arrange + mock_us_timer_set_time(1000); + + // Act + on_crank_tooth(); + + // Assert + TEST_ASSERT_EQUAL_UINT64(1, syncState.crank_counter); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_my_new_feature); + return UNITY_END(); +} +``` + +2. Add the test executable to `test/CMakeLists.txt`: + +```cmake +add_executable(test_my_feature + unit/test_my_feature.cpp + ${MOCK_SOURCES} + ${SOURCE_UNDER_TEST} +) +target_include_directories(test_my_feature PRIVATE + ${CMAKE_SOURCE_DIR}/../Core/Inc + ${CMAKE_SOURCE_DIR}/mocks + frameworks/Unity/src +) +target_link_libraries(test_my_feature unity) +target_compile_options(test_my_feature PRIVATE -DUNIT_TEST) + +add_test(NAME MyFeatureTests COMMAND test_my_feature) +``` + +3. Rebuild and run tests + +## Continuous Integration + +The tests are designed to run on any x86/x64 system with gcc/g++, making them suitable for CI/CD pipelines. + +## Troubleshooting + +### Build Errors + +If you see CMake cache errors: +```bash +cd test/build +rm -rf * +cmake .. +make +``` + +### Missing Submodules + +If Unity or CMock are not found: +```bash +git submodule update --init --recursive +``` + +### Test Failures + +Run tests with verbose output: +```bash +cd test/build +ctest --verbose +``` + +Or run individual tests directly to see full output: +```bash +./test_sampling +``` diff --git a/test/mocks/cmsis_os2.c b/test/mocks/cmsis_os2.c new file mode 100644 index 0000000..bfb95ad --- /dev/null +++ b/test/mocks/cmsis_os2.c @@ -0,0 +1,37 @@ +#include "cmsis_os2.h" + +// Mock implementations +osStatus_t osDelay(uint32_t ticks) { + (void)ticks; + return osOK; +} + +osThreadId_t osThreadNew(osThreadFunc_t func, void *argument, + const osThreadAttr_t *attr) { + (void)func; + (void)argument; + (void)attr; + return (osThreadId_t)1; // Return dummy thread ID +} + +const char *osThreadGetName(osThreadId_t thread_id) { + (void)thread_id; + return "MockThread"; +} + +osThreadId_t osThreadGetId(void) { return (osThreadId_t)1; } + +osStatus_t osThreadTerminate(osThreadId_t thread_id) { + (void)thread_id; + return osOK; +} + +osStatus_t osKernelInitialize(void) { return osOK; } + +osStatus_t osKernelStart(void) { return osOK; } + +uint32_t osKernelGetTickCount(void) { return 0; } + +uint32_t osKernelGetTickFreq(void) { + return 1000; // 1kHz +} diff --git a/test/mocks/cmsis_os2.h b/test/mocks/cmsis_os2.h new file mode 100644 index 0000000..54584ac --- /dev/null +++ b/test/mocks/cmsis_os2.h @@ -0,0 +1,74 @@ +#ifndef CMSIS_OS2_H +#define CMSIS_OS2_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +// CMSIS-RTOS2 status codes +typedef enum { + osOK = 0, + osError = -1, + osErrorTimeout = -2, + osErrorResource = -3, + osErrorParameter = -4, + osErrorNoMemory = -5, + osErrorISR = -6, +} osStatus_t; + +// CMSIS-RTOS2 thread priority +typedef enum { + osPriorityNone = 0, + osPriorityIdle = 1, + osPriorityLow = 8, + osPriorityBelowNormal = 16, + osPriorityNormal = 24, + osPriorityAboveNormal = 32, + osPriorityHigh = 40, + osPriorityRealtime = 48, + osPriorityISR = 56, + osPriorityError = -1, +} osPriority_t; + +// CMSIS-RTOS2 thread ID +typedef void *osThreadId_t; + +// CMSIS-RTOS2 thread attributes +typedef struct { + const char *name; + uint32_t attr_bits; + void *cb_mem; + uint32_t cb_size; + void *stack_mem; + uint32_t stack_size; + osPriority_t priority; + uint32_t reserved; +} osThreadAttr_t; + +// CMSIS-RTOS2 function type for thread +typedef void (*osThreadFunc_t)(void *argument); + +// Mock delay function +osStatus_t osDelay(uint32_t ticks); + +// Mock thread functions +osThreadId_t osThreadNew(osThreadFunc_t func, void *argument, + const osThreadAttr_t *attr); +const char *osThreadGetName(osThreadId_t thread_id); +osThreadId_t osThreadGetId(void); +osStatus_t osThreadTerminate(osThreadId_t thread_id); + +// Mock kernel functions +osStatus_t osKernelInitialize(void); +osStatus_t osKernelStart(void); +uint32_t osKernelGetTickCount(void); +uint32_t osKernelGetTickFreq(void); + +#ifdef __cplusplus +} +#endif + +#endif /* CMSIS_OS2_H */ diff --git a/test/mocks/mock_hal.h b/test/mocks/mock_hal.h new file mode 100644 index 0000000..558abd4 --- /dev/null +++ b/test/mocks/mock_hal.h @@ -0,0 +1,35 @@ +#ifndef MOCK_HAL_H +#define MOCK_HAL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Mock GPIO pin definitions +#define GPIO_PIN_0 0x0001 +#define GPIO_PIN_1 0x0002 +#define GPIO_PIN_2 0x0004 + +// HAL Status structures +typedef enum { + HAL_OK = 0x00U, + HAL_ERROR = 0x01U, + HAL_BUSY = 0x02U, + HAL_TIMEOUT = 0x03U +} HAL_StatusTypeDef; + +// Mock GPIO typedef +typedef struct { + uint32_t dummy; +} GPIO_TypeDef; + +// Mock functions +void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin); + +#ifdef __cplusplus +} +#endif + +#endif // MOCK_HAL_H diff --git a/test/mocks/mock_main.h b/test/mocks/mock_main.h new file mode 100644 index 0000000..a71a5d4 --- /dev/null +++ b/test/mocks/mock_main.h @@ -0,0 +1,24 @@ +#ifndef MOCK_MAIN_H +#define MOCK_MAIN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Mock pin definitions +#define CAM_SIGNAL_Pin GPIO_PIN_0 +#define CRANK_SIGNAL_Pin GPIO_PIN_1 + +// Mock GPIO definitions +#define GPIO_PIN_0 0x0001 +#define GPIO_PIN_1 0x0002 + +// void Error_Handler(void); + +#ifdef __cplusplus +} +#endif + +#endif // MOCK_MAIN_H diff --git a/test/mocks/mock_us_timer.c b/test/mocks/mock_us_timer.c new file mode 100644 index 0000000..3c7d3ee --- /dev/null +++ b/test/mocks/mock_us_timer.c @@ -0,0 +1,14 @@ +#include "mock_us_timer.h" + +static uint32_t mock_time_us = 0; + +void mock_us_timer_set_time(uint32_t time_us) { mock_time_us = time_us; } + +uint32_t mock_us_timer_get_time(void) { return mock_time_us; } + +uint32_t get_micros(void) { return mock_time_us; } + +void init_us_timer(void) { + // Mock implementation - does nothing + mock_time_us = 0; +} diff --git a/test/mocks/mock_us_timer.h b/test/mocks/mock_us_timer.h new file mode 100644 index 0000000..a724ced --- /dev/null +++ b/test/mocks/mock_us_timer.h @@ -0,0 +1,22 @@ +#ifndef MOCK_US_TIMER_H +#define MOCK_US_TIMER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Mock function to control time in tests +void mock_us_timer_set_time(uint32_t time_us); +uint32_t mock_us_timer_get_time(void); + +// Actual functions that will be called by code under test +uint32_t get_micros(void); +void init_us_timer(void); + +#ifdef __cplusplus +} +#endif + +#endif // MOCK_US_TIMER_H diff --git a/test/mocks/stm32f4xx_hal.c b/test/mocks/stm32f4xx_hal.c new file mode 100644 index 0000000..7a45003 --- /dev/null +++ b/test/mocks/stm32f4xx_hal.c @@ -0,0 +1,10 @@ +#include "stm32f4xx_hal.h" + +// Mock implementations +uint32_t HAL_GetTick(void) { return 0; } + +void HAL_Delay(uint32_t Delay) { (void)Delay; } + +void Error_Handler(void) { + // Do nothing in tests +} diff --git a/test/mocks/stm32f4xx_hal.h b/test/mocks/stm32f4xx_hal.h new file mode 100644 index 0000000..71e9454 --- /dev/null +++ b/test/mocks/stm32f4xx_hal.h @@ -0,0 +1,63 @@ +#ifndef STM32F4XX_HAL_H +#define STM32F4XX_HAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +// Mock HAL status definitions +typedef enum { + HAL_OK = 0x00U, + HAL_ERROR = 0x01U, + HAL_BUSY = 0x02U, + HAL_TIMEOUT = 0x03U +} HAL_StatusTypeDef; + +// Mock GPIO definitions (minimal for testing) +typedef struct { + volatile uint32_t MODER; + volatile uint32_t OTYPER; + volatile uint32_t OSPEEDR; + volatile uint32_t PUPDR; + volatile uint32_t IDR; + volatile uint32_t ODR; + volatile uint32_t BSRR; + volatile uint32_t LCKR; + volatile uint32_t AFR[2]; +} GPIO_TypeDef; + +// Mock GPIO pin definitions +#define GPIO_PIN_0 ((uint16_t)0x0001) +#define GPIO_PIN_1 ((uint16_t)0x0002) +#define GPIO_PIN_2 ((uint16_t)0x0004) +#define GPIO_PIN_3 ((uint16_t)0x0008) +#define GPIO_PIN_4 ((uint16_t)0x0010) +#define GPIO_PIN_5 ((uint16_t)0x0020) +#define GPIO_PIN_6 ((uint16_t)0x0040) +#define GPIO_PIN_7 ((uint16_t)0x0080) +#define GPIO_PIN_8 ((uint16_t)0x0100) +#define GPIO_PIN_9 ((uint16_t)0x0200) +#define GPIO_PIN_10 ((uint16_t)0x0400) +#define GPIO_PIN_11 ((uint16_t)0x0800) +#define GPIO_PIN_12 ((uint16_t)0x1000) +#define GPIO_PIN_13 ((uint16_t)0x2000) +#define GPIO_PIN_14 ((uint16_t)0x4000) +#define GPIO_PIN_15 ((uint16_t)0x8000) +#define GPIO_PIN_All ((uint16_t)0xFFFF) + +// GPIO state +typedef enum { GPIO_PIN_RESET = 0, GPIO_PIN_SET } GPIO_PinState; + +// Mock function declarations +uint32_t HAL_GetTick(void); +void HAL_Delay(uint32_t Delay); +void Error_Handler(void); + +#ifdef __cplusplus +} +#endif + +#endif /* STM32F4XX_HAL_H */ diff --git a/test/mocks/stm32f4xx_hal_gpio.c b/test/mocks/stm32f4xx_hal_gpio.c new file mode 100644 index 0000000..d2badc2 --- /dev/null +++ b/test/mocks/stm32f4xx_hal_gpio.c @@ -0,0 +1,39 @@ +#include "stm32f4xx_hal_gpio.h" + +// Mock implementations +void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init) { + (void)GPIOx; + (void)GPIO_Init; +} + +void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin) { + (void)GPIOx; + (void)GPIO_Pin; +} + +GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) { + (void)GPIOx; + (void)GPIO_Pin; + return GPIO_PIN_RESET; +} + +void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, + GPIO_PinState PinState) { + (void)GPIOx; + (void)GPIO_Pin; + (void)PinState; +} + +void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) { + (void)GPIOx; + (void)GPIO_Pin; +} + +void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) { + HAL_GPIO_EXTI_Callback(GPIO_Pin); +} + +// Weak implementation - can be overridden in tests +__attribute__((weak)) void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { + (void)GPIO_Pin; +} diff --git a/test/mocks/stm32f4xx_hal_gpio.h b/test/mocks/stm32f4xx_hal_gpio.h new file mode 100644 index 0000000..a4bf0a3 --- /dev/null +++ b/test/mocks/stm32f4xx_hal_gpio.h @@ -0,0 +1,59 @@ +#ifndef STM32F4XX_HAL_GPIO_H +#define STM32F4XX_HAL_GPIO_H + +#include "stm32f4xx_hal.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// GPIO Init structure definition +typedef struct { + uint32_t Pin; + uint32_t Mode; + uint32_t Pull; + uint32_t Speed; + uint32_t Alternate; +} GPIO_InitTypeDef; + +// GPIO mode definitions +#define GPIO_MODE_INPUT 0x00000000U +#define GPIO_MODE_OUTPUT_PP 0x00000001U +#define GPIO_MODE_OUTPUT_OD 0x00000011U +#define GPIO_MODE_AF_PP 0x00000002U +#define GPIO_MODE_AF_OD 0x00000012U +#define GPIO_MODE_ANALOG 0x00000003U +#define GPIO_MODE_IT_RISING 0x10110000U +#define GPIO_MODE_IT_FALLING 0x10210000U +#define GPIO_MODE_IT_RISING_FALLING 0x10310000U +#define GPIO_MODE_EVT_RISING 0x10120000U +#define GPIO_MODE_EVT_FALLING 0x10220000U +#define GPIO_MODE_EVT_RISING_FALLING 0x10320000U + +// GPIO pull definitions +#define GPIO_NOPULL 0x00000000U +#define GPIO_PULLUP 0x00000001U +#define GPIO_PULLDOWN 0x00000002U + +// GPIO speed definitions +#define GPIO_SPEED_FREQ_LOW 0x00000000U +#define GPIO_SPEED_FREQ_MEDIUM 0x00000001U +#define GPIO_SPEED_FREQ_HIGH 0x00000002U +#define GPIO_SPEED_FREQ_VERY_HIGH 0x00000003U + +// Mock function declarations +void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init); +void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin); +GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); +void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, + GPIO_PinState PinState); +void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); +void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin); +void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin); + +#ifdef __cplusplus +} +#endif + +#endif /* STM32F4XX_HAL_GPIO_H */ diff --git a/test/mocks/ulog.c b/test/mocks/ulog.c new file mode 100644 index 0000000..18cdecc --- /dev/null +++ b/test/mocks/ulog.c @@ -0,0 +1,6 @@ +#include "ulog.h" + +// Mock implementation +void ulog_init(void) { + // Do nothing in tests +} diff --git a/test/mocks/ulog.h b/test/mocks/ulog.h new file mode 100644 index 0000000..3bb9dde --- /dev/null +++ b/test/mocks/ulog.h @@ -0,0 +1,31 @@ +#ifndef ULOG_H +#define ULOG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +// Mock ULOG macros for testing - just use printf +#define ULOG_DEBUG(...) \ + printf("[DEBUG] " __VA_ARGS__); \ + printf("\n") +#define ULOG_INFO(...) \ + printf("[INFO] " __VA_ARGS__); \ + printf("\n") +#define ULOG_WARN(...) \ + printf("[WARN] " __VA_ARGS__); \ + printf("\n") +#define ULOG_ERROR(...) \ + printf("[ERROR] " __VA_ARGS__); \ + printf("\n") + +// Mock function declarations (if any) +void ulog_init(void); + +#ifdef __cplusplus +} +#endif + +#endif /* ULOG_H */ diff --git a/test/unit/test_sample.c b/test/unit/test_sample.c deleted file mode 100644 index bb90727..0000000 --- a/test/unit/test_sample.c +++ /dev/null @@ -1,12 +0,0 @@ -#include "unity.h" - -void setUp(void) {} -void tearDown(void) {} - -void test_basic_addition(void) { TEST_ASSERT_EQUAL_INT(4, 2 + 2); } - -int main(void) { - UNITY_BEGIN(); - RUN_TEST(test_basic_addition); - return UNITY_END(); -} diff --git a/test/unit/test_sampling.cpp b/test/unit/test_sampling.cpp new file mode 100644 index 0000000..91cb9a1 --- /dev/null +++ b/test/unit/test_sampling.cpp @@ -0,0 +1,362 @@ +#include "mock_hal.h" +#include "mock_main.h" +#include "mock_us_timer.h" +#include "unity.h" + +// Define main.h before including sampling +#define main_h +#include "sampling.hpp" + +void setUp(void) { + // Reset sync state before each test + syncState.synced = false; + syncState.crank_index = 0; + syncState.crank_counter = 0; + syncState.cam_crank_counter = 0; + syncState.last_cam_crank_counter = 0; + syncState.last_crank_time_us = 0; + syncState.last_cam_time_us = 0; + syncState.tooth_period_us = 0.0; + syncState.engine_phase = false; + + // Reset mock timer + mock_us_timer_set_time(0); +} + +void tearDown(void) { + // Cleanup after each test +} + +// ============================================================================ +// Test: Initial State +// ============================================================================ +void test_initial_state_is_not_synced(void) { + TEST_ASSERT_FALSE(syncState.synced); + TEST_ASSERT_EQUAL_UINT8(0, syncState.crank_index); + TEST_ASSERT_EQUAL_UINT64(0, syncState.crank_counter); + TEST_ASSERT_EQUAL_DOUBLE(0.0, syncState.tooth_period_us); +} + +// ============================================================================ +// Test: Crank Tooth Detection +// ============================================================================ +void test_on_crank_tooth_increments_counter(void) { + mock_us_timer_set_time(1000); + on_crank_tooth(); + + TEST_ASSERT_EQUAL_UINT64(1, syncState.crank_counter); + TEST_ASSERT_EQUAL_UINT32(1000, syncState.last_crank_time_us); +} + +void test_on_crank_tooth_calculates_initial_period(void) { + // First tooth at 1000us + mock_us_timer_set_time(1000); + on_crank_tooth(); + + // Second tooth at 2500us (1500us period) + mock_us_timer_set_time(2500); + on_crank_tooth(); + + TEST_ASSERT_EQUAL_DOUBLE(1500.0, syncState.tooth_period_us); +} + +void test_on_crank_tooth_applies_exponential_moving_average(void) { + // First tooth - establishes initial period + mock_us_timer_set_time(1000); + on_crank_tooth(); + + mock_us_timer_set_time(2500); // dt = 1500us + on_crank_tooth(); + TEST_ASSERT_EQUAL_DOUBLE(1500.0, syncState.tooth_period_us); + + // Third tooth with different period + mock_us_timer_set_time(4200); // dt = 1700us + on_crank_tooth(); + + // Expected: 0.8 * 1700 + 0.2 * 1500 = 1360 + 300 = 1660 + TEST_ASSERT_DOUBLE_WITHIN(0.1, 1660.0, syncState.tooth_period_us); +} + +void test_on_crank_tooth_wraps_crank_index_when_synced(void) { + syncState.synced = true; + syncState.crank_index = 11; // Last tooth (0-indexed) + + mock_us_timer_set_time(1000); + on_crank_tooth(); + + TEST_ASSERT_EQUAL_UINT8(0, syncState.crank_index); +} + +void test_on_crank_tooth_toggles_engine_phase_at_tooth_11(void) { + syncState.synced = true; + syncState.crank_index = 11; + syncState.engine_phase = false; + + mock_us_timer_set_time(1000); + on_crank_tooth(); + + TEST_ASSERT_TRUE(syncState.engine_phase); + TEST_ASSERT_EQUAL_UINT8(0, syncState.crank_index); +} + +void test_on_crank_tooth_does_not_change_index_when_not_synced(void) { + syncState.synced = false; + syncState.crank_index = 5; + + mock_us_timer_set_time(1000); + on_crank_tooth(); + + // Index should still be 5 since we're not synced + TEST_ASSERT_EQUAL_UINT8(5, syncState.crank_index); +} + +// ============================================================================ +// Test: Cam Tooth Detection +// ============================================================================ +void test_on_cam_tooth_updates_counters(void) { + syncState.crank_counter = 10; + syncState.cam_crank_counter = 5; + + mock_us_timer_set_time(5000); + on_cam_tooth(); + + TEST_ASSERT_EQUAL_UINT64(5, syncState.last_cam_crank_counter); + TEST_ASSERT_EQUAL_UINT64(10, syncState.cam_crank_counter); + TEST_ASSERT_EQUAL_UINT32(5000, syncState.last_cam_time_us); +} + +void test_get_cam_delta_calculates_difference(void) { + syncState.cam_crank_counter = 15; + syncState.last_cam_crank_counter = 3; + + uint8_t delta = syncState.get_cam_delta(); + + TEST_ASSERT_EQUAL_UINT8(12, delta); +} + +// ============================================================================ +// Test: Fraction of Tooth Calculation +// ============================================================================ +void test_get_current_fraction_when_no_period_set(void) { + syncState.tooth_period_us = 0.0; + + float fraction = get_current_fraction_of_tooth(); + + TEST_ASSERT_EQUAL_FLOAT(0.0f, fraction); +} + +void test_get_current_fraction_at_quarter_period(void) { + syncState.tooth_period_us = 1000.0; + syncState.last_crank_time_us = 1000; + + mock_us_timer_set_time(1250); // 250us after last tooth = 0.25 fraction + + float fraction = get_current_fraction_of_tooth(); + + TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.25f, fraction); +} + +void test_get_current_fraction_at_half_period(void) { + syncState.tooth_period_us = 1000.0; + syncState.last_crank_time_us = 2000; + + mock_us_timer_set_time(2500); // 500us after last tooth = 0.5 fraction + + float fraction = get_current_fraction_of_tooth(); + + TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.5f, fraction); +} + +void test_get_current_fraction_clamps_to_one(void) { + syncState.tooth_period_us = 1000.0; + syncState.last_crank_time_us = 1000; + + mock_us_timer_set_time(3000); // 2000us after = 2.0, should clamp to 1.0 + + float fraction = get_current_fraction_of_tooth(); + + TEST_ASSERT_EQUAL_FLOAT(1.0f, fraction); +} + +void test_get_current_fraction_clamps_negative_to_zero(void) { + syncState.tooth_period_us = 1000.0; + syncState.last_crank_time_us = 5000; + + mock_us_timer_set_time( + 4000); // Negative dt (shouldn't happen but test robustness) + + float fraction = get_current_fraction_of_tooth(); + + TEST_ASSERT_EQUAL_FLOAT(0.0f, fraction); +} + +// ============================================================================ +// Test: Engine Angle Calculation +// ============================================================================ +void test_get_current_engine_angle_returns_zero_when_not_synced(void) { + syncState.synced = false; + syncState.crank_index = 5; + syncState.engine_phase = 1; + + float angle = get_current_engine_angle(); + + TEST_ASSERT_EQUAL_FLOAT(0.0f, angle); +} + +void test_get_current_engine_angle_at_tooth_zero_phase_zero(void) { + syncState.synced = true; + syncState.crank_index = 0; + syncState.engine_phase = 0; + syncState.tooth_period_us = 1000.0; + syncState.last_crank_time_us = 1000; + + mock_us_timer_set_time(1000); // Right at tooth, fraction = 0 + + float angle = get_current_engine_angle(); + + // Phase 0 * 360 + Index 0 * 30 + Fraction 0 * 30 = 0 + TEST_ASSERT_FLOAT_WITHIN(0.1f, 0.0f, angle); +} + +void test_get_current_engine_angle_at_tooth_one_phase_zero(void) { + syncState.synced = true; + syncState.crank_index = 1; + syncState.engine_phase = 0; + syncState.tooth_period_us = 1000.0; + syncState.last_crank_time_us = 1000; + + mock_us_timer_set_time(1000); // Right at tooth, fraction = 0 + + float angle = get_current_engine_angle(); + + // Phase 0 * 360 + Index 1 * 30 + Fraction 0 * 30 = 30 + TEST_ASSERT_FLOAT_WITHIN(0.1f, 30.0f, angle); +} + +void test_get_current_engine_angle_at_tooth_six_phase_zero(void) { + syncState.synced = true; + syncState.crank_index = 6; + syncState.engine_phase = 0; + syncState.tooth_period_us = 1000.0; + syncState.last_crank_time_us = 1000; + + mock_us_timer_set_time(1000); + + float angle = get_current_engine_angle(); + + // Phase 0 * 360 + Index 6 * 30 + Fraction 0 * 30 = 180 + TEST_ASSERT_FLOAT_WITHIN(0.1f, 180.0f, angle); +} + +void test_get_current_engine_angle_at_tooth_zero_phase_one(void) { + syncState.synced = true; + syncState.crank_index = 0; + syncState.engine_phase = 1; + syncState.tooth_period_us = 1000.0; + syncState.last_crank_time_us = 1000; + + mock_us_timer_set_time(1000); + + float angle = get_current_engine_angle(); + + // Phase 1 * 360 + Index 0 * 30 + Fraction 0 * 30 = 360 + TEST_ASSERT_FLOAT_WITHIN(0.1f, 360.0f, angle); +} + +void test_get_current_engine_angle_with_fraction(void) { + syncState.synced = true; + syncState.crank_index = 2; + syncState.engine_phase = 0; + syncState.tooth_period_us = 1000.0; + syncState.last_crank_time_us = 1000; + + mock_us_timer_set_time(1500); // Fraction = 0.5 + + float angle = get_current_engine_angle(); + + // Phase 0 * 360 + Index 2 * 30 + Fraction 0.5 * 30 = 60 + 15 = 75 + TEST_ASSERT_FLOAT_WITHIN(0.1f, 75.0f, angle); +} + +void test_get_current_engine_angle_near_end_of_cycle(void) { + syncState.synced = true; + syncState.crank_index = 11; + syncState.engine_phase = 1; + syncState.tooth_period_us = 1000.0; + syncState.last_crank_time_us = 1000; + + mock_us_timer_set_time(1750); // Fraction = 0.75 + + float angle = get_current_engine_angle(); + + // Phase 1 * 360 + Index 11 * 30 + Fraction 0.75 * 30 = 360 + 330 + 22.5 = + // 712.5 + TEST_ASSERT_FLOAT_WITHIN(0.1f, 712.5f, angle); +} + +// ============================================================================ +// Test: Complete Rotation Scenario +// ============================================================================ +void test_complete_rotation_scenario(void) { + uint32_t time = 0; + uint32_t tooth_period = 1000; // 1000us between teeth + + syncState.synced = true; + syncState.crank_index = 0; + syncState.engine_phase = 0; + + // Simulate 12 crank teeth (one full rotation) + for (int i = 0; i < 12; i++) { + mock_us_timer_set_time(time); + on_crank_tooth(); + time += tooth_period; + } + + // After 12 teeth, we should be back at index 0 with phase toggled + TEST_ASSERT_EQUAL_UINT8(0, syncState.crank_index); + TEST_ASSERT_TRUE(syncState.engine_phase); + TEST_ASSERT_EQUAL_UINT64(12, syncState.crank_counter); +} + +// ============================================================================ +// Main test runner +// ============================================================================ +int main(void) { + UNITY_BEGIN(); + + // Initial state tests + RUN_TEST(test_initial_state_is_not_synced); + + // Crank tooth detection tests + RUN_TEST(test_on_crank_tooth_increments_counter); + RUN_TEST(test_on_crank_tooth_calculates_initial_period); + RUN_TEST(test_on_crank_tooth_applies_exponential_moving_average); + RUN_TEST(test_on_crank_tooth_wraps_crank_index_when_synced); + RUN_TEST(test_on_crank_tooth_toggles_engine_phase_at_tooth_11); + RUN_TEST(test_on_crank_tooth_does_not_change_index_when_not_synced); + + // Cam tooth detection tests + RUN_TEST(test_on_cam_tooth_updates_counters); + RUN_TEST(test_get_cam_delta_calculates_difference); + + // Fraction of tooth tests + RUN_TEST(test_get_current_fraction_when_no_period_set); + RUN_TEST(test_get_current_fraction_at_quarter_period); + RUN_TEST(test_get_current_fraction_at_half_period); + RUN_TEST(test_get_current_fraction_clamps_to_one); + RUN_TEST(test_get_current_fraction_clamps_negative_to_zero); + + // Engine angle calculation tests + RUN_TEST(test_get_current_engine_angle_returns_zero_when_not_synced); + RUN_TEST(test_get_current_engine_angle_at_tooth_zero_phase_zero); + RUN_TEST(test_get_current_engine_angle_at_tooth_one_phase_zero); + RUN_TEST(test_get_current_engine_angle_at_tooth_six_phase_zero); + RUN_TEST(test_get_current_engine_angle_at_tooth_zero_phase_one); + RUN_TEST(test_get_current_engine_angle_with_fraction); + RUN_TEST(test_get_current_engine_angle_near_end_of_cycle); + + // Integration tests + RUN_TEST(test_complete_rotation_scenario); + + return UNITY_END(); +} diff --git a/test/unit/test_sync_detection.cpp b/test/unit/test_sync_detection.cpp new file mode 100644 index 0000000..a7edf96 --- /dev/null +++ b/test/unit/test_sync_detection.cpp @@ -0,0 +1,210 @@ +#include "mock_main.h" +#include "mock_us_timer.h" +#include "unity.h" + +// Mock ULOG macros for testing +#define ULOG_DEBUG(...) +#define ULOG_INFO(...) +#define ULOG_ERROR(...) + +// Include sampling header +#include "sampling.hpp" + +// Declare the function we're testing (needs C linkage) +extern "C" void detectSync(void); + +void setUp(void) { + // Reset sync state before each test + syncState.synced = false; + syncState.crank_index = 0; + syncState.crank_counter = 0; + syncState.cam_crank_counter = 0; + syncState.last_cam_crank_counter = 0; + syncState.last_crank_time_us = 0; + syncState.last_cam_time_us = 0; + syncState.tooth_period_us = 0.0; + syncState.engine_phase = false; +} + +void tearDown(void) { + // Cleanup +} + +// ============================================================================ +// Test: Sync Detection with Delta = 1 +// ============================================================================ +void test_detect_sync_with_delta_1_sets_correct_state(void) { + // Setup: cam teeth with delta of 1 crank tooth + syncState.cam_crank_counter = 5; + syncState.last_cam_crank_counter = 4; // Delta = 1 + + detectSync(); + + TEST_ASSERT_TRUE(syncState.synced); + TEST_ASSERT_EQUAL_UINT8(1, syncState.crank_index); + TEST_ASSERT_TRUE(syncState.engine_phase); // Phase 1 +} + +// ============================================================================ +// Test: Sync Detection with Delta = 11 +// ============================================================================ +void test_detect_sync_with_delta_11_sets_correct_state(void) { + // Setup: cam teeth with delta of 11 crank teeth + syncState.cam_crank_counter = 20; + syncState.last_cam_crank_counter = 9; // Delta = 11 + + detectSync(); + + TEST_ASSERT_TRUE(syncState.synced); + TEST_ASSERT_EQUAL_UINT8(10, syncState.crank_index); + TEST_ASSERT_FALSE(syncState.engine_phase); // Phase 0 +} + +// ============================================================================ +// Test: Sync Detection with Delta = 12 +// ============================================================================ +void test_detect_sync_with_delta_12_sets_correct_state(void) { + // Setup: cam teeth with delta of 12 crank teeth + syncState.cam_crank_counter = 25; + syncState.last_cam_crank_counter = 13; // Delta = 12 + + detectSync(); + + TEST_ASSERT_TRUE(syncState.synced); + TEST_ASSERT_EQUAL_UINT8(0, syncState.crank_index); + TEST_ASSERT_FALSE(syncState.engine_phase); // Phase 0 +} + +// ============================================================================ +// Test: Sync Detection with Invalid Delta +// ============================================================================ +void test_detect_sync_with_invalid_delta_stays_not_synced(void) { + // Setup: cam teeth with invalid delta + syncState.cam_crank_counter = 10; + syncState.last_cam_crank_counter = 5; // Delta = 5 (invalid) + + detectSync(); + + TEST_ASSERT_FALSE(syncState.synced); +} + +void test_detect_sync_with_delta_0_stays_not_synced(void) { + // Setup: no change in cam counter + syncState.cam_crank_counter = 10; + syncState.last_cam_crank_counter = 10; // Delta = 0 + + detectSync(); + + TEST_ASSERT_FALSE(syncState.synced); +} + +void test_detect_sync_with_delta_13_stays_not_synced(void) { + // Setup: delta too large + syncState.cam_crank_counter = 20; + syncState.last_cam_crank_counter = 7; // Delta = 13 + + detectSync(); + + TEST_ASSERT_FALSE(syncState.synced); +} + +// ============================================================================ +// Test: Sync Detection Sequence +// ============================================================================ +void test_sync_detection_full_sequence(void) { + uint32_t time = 0; + + // Simulate crank teeth arriving + for (int i = 0; i < 12; i++) { + mock_us_timer_set_time(time); + on_crank_tooth(); + time += 1000; + } + + // First cam tooth (at crank counter 12) + mock_us_timer_set_time(time); + on_cam_tooth(); + + // More crank teeth + for (int i = 0; i < 12; i++) { + mock_us_timer_set_time(time); + on_crank_tooth(); + time += 1000; + } + + // Second cam tooth (at crank counter 24, delta = 12) + mock_us_timer_set_time(time); + on_cam_tooth(); + + // Now detect sync + detectSync(); + + TEST_ASSERT_TRUE(syncState.synced); + TEST_ASSERT_EQUAL_UINT8(0, syncState.crank_index); + TEST_ASSERT_FALSE(syncState.engine_phase); +} + +// ============================================================================ +// Test: Sync Re-Detection After Loss +// ============================================================================ +void test_sync_can_be_reestablished_after_loss(void) { + // First, establish sync + syncState.cam_crank_counter = 12; + syncState.last_cam_crank_counter = 0; // Delta = 12 + detectSync(); + TEST_ASSERT_TRUE(syncState.synced); + + // Lose sync with invalid delta + syncState.cam_crank_counter = 20; + syncState.last_cam_crank_counter = 15; // Delta = 5 (invalid) + detectSync(); + TEST_ASSERT_FALSE(syncState.synced); + + // Reestablish sync + syncState.cam_crank_counter = 31; + syncState.last_cam_crank_counter = 20; // Delta = 11 + detectSync(); + TEST_ASSERT_TRUE(syncState.synced); + TEST_ASSERT_EQUAL_UINT8(10, syncState.crank_index); +} + +// ============================================================================ +// Test: Edge Cases +// ============================================================================ +void test_sync_with_counter_overflow_protection(void) { + // Test with large counter values (near overflow) + syncState.cam_crank_counter = 0xFFFFFFFFFFFFFFFF; // Max uint64 + syncState.last_cam_crank_counter = 0xFFFFFFFFFFFFFFFF - 12; // Delta = 12 + + uint8_t delta = syncState.get_cam_delta(); + TEST_ASSERT_EQUAL_UINT8(12, delta); + + detectSync(); + TEST_ASSERT_TRUE(syncState.synced); +} + +// ============================================================================ +// Main test runner +// ============================================================================ +int main(void) { + UNITY_BEGIN(); + + // Sync detection with valid deltas + RUN_TEST(test_detect_sync_with_delta_1_sets_correct_state); + RUN_TEST(test_detect_sync_with_delta_11_sets_correct_state); + RUN_TEST(test_detect_sync_with_delta_12_sets_correct_state); + + // Sync detection with invalid deltas + RUN_TEST(test_detect_sync_with_invalid_delta_stays_not_synced); + RUN_TEST(test_detect_sync_with_delta_0_stays_not_synced); + RUN_TEST(test_detect_sync_with_delta_13_stays_not_synced); + + // Integration tests + RUN_TEST(test_sync_detection_full_sequence); + RUN_TEST(test_sync_can_be_reestablished_after_loss); + + // Edge cases + RUN_TEST(test_sync_with_counter_overflow_protection); + + return UNITY_END(); +} From 3f236e84cd665f7e68c93686d784142f100f4db0 Mon Sep 17 00:00:00 2001 From: SamrutGadde Date: Sat, 11 Oct 2025 18:48:00 -0500 Subject: [PATCH 3/7] Remove print --- Core/Src/tasks.cpp | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/Core/Src/tasks.cpp b/Core/Src/tasks.cpp index f08d489..afda227 100644 --- a/Core/Src/tasks.cpp +++ b/Core/Src/tasks.cpp @@ -46,27 +46,7 @@ void criticalEngineTask(void *argument) { detectSync(); } - time = get_micros(); - uint32_t dt = time - last_time; - uint32_t dt_ms = dt / 1000; - - // Print every second. - // if (dt >= 1000000) { - // last_time = time; - // ULOG_INFO("Detected Sync! Current Crank Angle: %f", - // syncState.current_engine_angle); - // ULOG_INFO("Fraction of tooth: %f", syncState.fraction_of_tooth); - // ULOG_INFO("Tooth period (us): %f", syncState.tooth_period_us); - // ULOG_INFO("Crank index: %d", syncState.crank_index); - // ULOG_INFO("Engine phase: %d", syncState.engine_phase); - // } - - if (dt_ms >= 10) { - last_time = time; - float current_angle = get_current_engine_angle(); - float current_fraction = get_current_fraction_of_tooth(); - ULOG_DEBUG("Crank Angle: %f // Tooth Period: %f // Fraction: %f", - current_angle, syncState.tooth_period_us, current_fraction); - } + // TODO: Schedule fuel/ignition events based on current angle + osDelay(1); } } From 100aceccb5a56b2517776741c5d64310102becf3 Mon Sep 17 00:00:00 2001 From: SamrutGadde Date: Sat, 11 Oct 2025 18:58:44 -0500 Subject: [PATCH 4/7] Rename tasks.cpp to engine.cpp --- CMakeLists.txt | 2 +- Core/Inc/{tasks.h => engine.h} | 6 +++--- Core/Src/{tasks.cpp => engine.cpp} | 2 +- Core/Src/freertos.c | 32 +++++++++++++++--------------- test/CMakeLists.txt | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) rename Core/Inc/{tasks.h => engine.h} (92%) rename Core/Src/{tasks.cpp => engine.cpp} (98%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 865e224..b122b91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,7 +54,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE Core/Src/us_timer.c Core/Src/ulog.c Core/Src/sampling.cpp - Core/Src/tasks.cpp + Core/Src/engine.cpp ) # Add include paths diff --git a/Core/Inc/tasks.h b/Core/Inc/engine.h similarity index 92% rename from Core/Inc/tasks.h rename to Core/Inc/engine.h index ea68a0b..9e648f4 100644 --- a/Core/Inc/tasks.h +++ b/Core/Inc/engine.h @@ -1,5 +1,5 @@ -#ifndef __TASKS_H -#define __TASKS_H +#ifndef __ENGINE_H +#define __ENGINE_H #ifdef __cplusplus extern "C" { #endif @@ -34,4 +34,4 @@ void detectSync(void); #ifdef __cplusplus } #endif -#endif /* __TASKS_H */ +#endif /* __ENGINE_H */ diff --git a/Core/Src/tasks.cpp b/Core/Src/engine.cpp similarity index 98% rename from Core/Src/tasks.cpp rename to Core/Src/engine.cpp index afda227..7fdbc48 100644 --- a/Core/Src/tasks.cpp +++ b/Core/Src/engine.cpp @@ -1,8 +1,8 @@ #include #include +#include "engine.h" #include "sampling.hpp" -#include "tasks.h" #include "ulog.h" #include "us_timer.h" diff --git a/Core/Src/freertos.c b/Core/Src/freertos.c index 34b5afd..0beca23 100644 --- a/Core/Src/freertos.c +++ b/Core/Src/freertos.c @@ -19,15 +19,17 @@ /* Includes ------------------------------------------------------------------*/ #include "FreeRTOS.h" -#include "task.h" -#include "main.h" #include "cmsis_os.h" +#include "main.h" +#include "task.h" + /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ -#include "us_timer.h" -#include "tasks.h" +#include "engine.h" #include "ulog.h" +#include "us_timer.h" + /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -58,9 +60,9 @@ const osThreadAttr_t criticalEngineTaskAttributes = { /* Definitions for defaultTask */ osThreadId_t defaultTaskHandle; const osThreadAttr_t defaultTask_attributes = { - .name = "defaultTask", - .stack_size = 128 * 4, - .priority = (osPriority_t) osPriorityNormal, + .name = "defaultTask", + .stack_size = 128 * 4, + .priority = (osPriority_t)osPriorityNormal, }; /* Private function prototypes -----------------------------------------------*/ @@ -72,10 +74,10 @@ void StartDefaultTask(void *argument); void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */ /** - * @brief FreeRTOS initialization - * @param None - * @retval None - */ + * @brief FreeRTOS initialization + * @param None + * @retval None + */ void MX_FREERTOS_Init(void) { /* USER CODE BEGIN Init */ ULOG_INIT(); @@ -102,7 +104,8 @@ void MX_FREERTOS_Init(void) { /* Create the thread(s) */ /* creation of defaultTask */ - defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); + defaultTaskHandle = + osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); /* USER CODE BEGIN RTOS_THREADS */ criticalEngineTaskHandle = @@ -113,7 +116,6 @@ void MX_FREERTOS_Init(void) { /* USER CODE BEGIN RTOS_EVENTS */ /* add events, ... */ /* USER CODE END RTOS_EVENTS */ - } /* USER CODE BEGIN Header_StartDefaultTask */ @@ -123,8 +125,7 @@ void MX_FREERTOS_Init(void) { * @retval None */ /* USER CODE END Header_StartDefaultTask */ -void StartDefaultTask(void *argument) -{ +void StartDefaultTask(void *argument) { /* USER CODE BEGIN StartDefaultTask */ /* Infinite loop */ for (;;) { @@ -137,4 +138,3 @@ void StartDefaultTask(void *argument) /* USER CODE BEGIN Application */ /* USER CODE END Application */ - diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0baaac8..1cc7c88 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -73,7 +73,7 @@ add_executable(test_sync_detection unit/test_sync_detection.cpp ${MOCK_SOURCES} ${SOURCE_UNDER_TEST} - ${CMAKE_SOURCE_DIR}/../Core/Src/tasks.cpp + ${CMAKE_SOURCE_DIR}/../Core/Src/engine.cpp ) target_include_directories(test_sync_detection PRIVATE ${CMAKE_SOURCE_DIR}/../Core/Inc From 1c9eef4a1ce79e74025ecf645d15d6ad31f4dad7 Mon Sep 17 00:00:00 2001 From: SamrutGadde Date: Sat, 11 Oct 2025 19:16:58 -0500 Subject: [PATCH 5/7] Adds CI workflows and unit testing --- .github/workflows/build.yml | 45 ++++++++++++++ .github/workflows/ci.yml | 81 +++++++++++++++++++++++++ .github/workflows/test.yml | 40 ++++++++++++ README.md | 118 ++++++++++++++++++++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/test.yml create mode 100644 README.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c912f4b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,45 @@ +name: Build Firmware + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install ARM toolchain + run: | + sudo apt-get update + sudo apt-get install -y \ + gcc-arm-none-eabi \ + binutils-arm-none-eabi \ + libnewlib-arm-none-eabi \ + cmake \ + ninja-build + + - name: Build firmware + run: | + chmod +x build_firmware.sh + ./build_firmware.sh + + - name: Upload firmware artifacts + uses: actions/upload-artifact@v4 + with: + name: firmware + path: | + build/CustomECU.elf + build/CustomECU.map + if-no-files-found: error + + - name: Display memory usage + run: | + arm-none-eabi-size build/CustomECU.elf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..55ff899 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + name: Run Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + cmake \ + gcc \ + g++ \ + ninja-build + + - name: Run unit tests + run: | + chmod +x run_tests.sh + ./run_tests.sh + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: test/build/Testing/ + if-no-files-found: ignore + + build: + name: Build Firmware + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install ARM toolchain and build tools + run: | + sudo apt-get update + sudo apt-get install -y \ + gcc-arm-none-eabi \ + binutils-arm-none-eabi \ + libnewlib-arm-none-eabi \ + cmake \ + ninja-build + + - name: Build firmware + run: | + chmod +x build_firmware.sh + ./build_firmware.sh + + - name: Display memory usage + run: | + echo "=== Memory Usage ===" + arm-none-eabi-size build/CustomECU.elf + + - name: Upload firmware artifacts + uses: actions/upload-artifact@v4 + with: + name: firmware + path: | + build/CustomECU.elf + build/CustomECU.map + if-no-files-found: error diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9c84dfb --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: Unit Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + cmake \ + gcc \ + g++ \ + ninja-build + + - name: Run tests + run: | + chmod +x run_tests.sh + ./run_tests.sh + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: test/build/Testing/ + if-no-files-found: ignore diff --git a/README.md b/README.md new file mode 100644 index 0000000..b76ef06 --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +# CustomECU + +[![CI](https://github.com/LHRIC/CustomECU/actions/workflows/ci.yml/badge.svg)](https://github.com/LHRIC/CustomECU/actions/workflows/ci.yml) +[![Unit Tests](https://github.com/LHRIC/CustomECU/actions/workflows/test.yml/badge.svg)](https://github.com/LHRIC/CustomECU/actions/workflows/test.yml) +[![Build Firmware](https://github.com/LHRIC/CustomECU/actions/workflows/build.yml/badge.svg)](https://github.com/LHRIC/CustomECU/actions/workflows/build.yml) + +Custom ECU firmware for Honda CBR600CC engine control using STM32F407VET6. + +## Features + +- **Crank/Cam Synchronization**: Detects engine position using 12-tooth crank wheel and 3-tooth cam wheel +- **Real-time Engine Angle Calculation**: High-precision angle tracking with sub-tooth resolution +- **FreeRTOS**: Real-time operating system for deterministic task scheduling +- **Comprehensive Testing**: Unity-based unit tests with full mock infrastructure + +## Hardware + +- **MCU**: STM32F407VET6 (ARM Cortex-M4F, 168MHz) +- **Engine**: Honda CBR600CC +- **Sensors**: + - Crank position sensor (12 equally-spaced teeth) + - Cam position sensor (3 teeth, one offset 30°) + +## Building + +### Firmware + +```bash +./build_firmware.sh +``` + +Or manually: + +```bash +cd build +cmake .. +make +``` + +### Unit Tests + +```bash +./run_tests.sh +``` + +Or manually: + +```bash +cd test/build +cmake .. +make +ctest --verbose +``` + +## Project Structure + +``` +CustomECU/ +├── Core/ +│ ├── Inc/ # Header files +│ │ ├── engine.h # Engine control functions +│ │ ├── sampling.hpp # Sensor sampling and sync +│ │ └── ... +│ └── Src/ # Source files +│ ├── engine.cpp # Engine control implementation +│ ├── sampling.cpp # Sensor sampling implementation +│ └── ... +├── test/ # Unit tests +│ ├── unit/ # Test files +│ ├── mocks/ # Mock implementations +│ └── frameworks/ # Unity & CMock +├── Drivers/ # STM32 HAL drivers +└── Middlewares/ # FreeRTOS +``` + +## Development + +### Prerequisites + +- ARM GCC toolchain (`gcc-arm-none-eabi`) +- CMake (≥ 3.22) +- Make or Ninja +- Python 3 (for code generation tools) + +### Debugging + +Debug configuration is available for VS Code with Cortex-Debug extension: + +```bash +# Launch debug session +# Press F5 in VS Code +``` + +### Testing + +See [test/README.md](test/README.md) for detailed testing documentation. + +## License + +See [LICENSE](LICENSE) file for details. + +## Contributing + +1. Create a feature branch +2. Make your changes +3. Ensure tests pass: `./run_tests.sh` +4. Ensure firmware builds: `./build_firmware.sh` +5. Submit a pull request + +## CI/CD + +GitHub Actions automatically: +- Runs unit tests on every push +- Builds firmware +- Uploads artifacts +- Reports test results + +Check the Actions tab for build status. From 26885cb3e3344ff3f15e4587f10e0b209d1f6b6b Mon Sep 17 00:00:00 2001 From: SamrutGadde Date: Sat, 11 Oct 2025 19:22:30 -0500 Subject: [PATCH 6/7] Remove other non-test workflows for now --- .github/workflows/build.yml | 45 --------------------- .github/workflows/ci.yml | 81 ------------------------------------- README.md | 1 + 3 files changed, 1 insertion(+), 126 deletions(-) delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index c912f4b..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Build Firmware - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install ARM toolchain - run: | - sudo apt-get update - sudo apt-get install -y \ - gcc-arm-none-eabi \ - binutils-arm-none-eabi \ - libnewlib-arm-none-eabi \ - cmake \ - ninja-build - - - name: Build firmware - run: | - chmod +x build_firmware.sh - ./build_firmware.sh - - - name: Upload firmware artifacts - uses: actions/upload-artifact@v4 - with: - name: firmware - path: | - build/CustomECU.elf - build/CustomECU.map - if-no-files-found: error - - - name: Display memory usage - run: | - arm-none-eabi-size build/CustomECU.elf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 55ff899..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: CI - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test: - name: Run Unit Tests - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install build dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential \ - cmake \ - gcc \ - g++ \ - ninja-build - - - name: Run unit tests - run: | - chmod +x run_tests.sh - ./run_tests.sh - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: test/build/Testing/ - if-no-files-found: ignore - - build: - name: Build Firmware - runs-on: ubuntu-latest - needs: test - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install ARM toolchain and build tools - run: | - sudo apt-get update - sudo apt-get install -y \ - gcc-arm-none-eabi \ - binutils-arm-none-eabi \ - libnewlib-arm-none-eabi \ - cmake \ - ninja-build - - - name: Build firmware - run: | - chmod +x build_firmware.sh - ./build_firmware.sh - - - name: Display memory usage - run: | - echo "=== Memory Usage ===" - arm-none-eabi-size build/CustomECU.elf - - - name: Upload firmware artifacts - uses: actions/upload-artifact@v4 - with: - name: firmware - path: | - build/CustomECU.elf - build/CustomECU.map - if-no-files-found: error diff --git a/README.md b/README.md index b76ef06..f8461f5 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ See [LICENSE](LICENSE) file for details. ## CI/CD GitHub Actions automatically: + - Runs unit tests on every push - Builds firmware - Uploads artifacts From 75e708f15763b72ebcd15ed15243d7d29894abc4 Mon Sep 17 00:00:00 2001 From: SamrutGadde Date: Sat, 18 Oct 2025 21:55:03 -0500 Subject: [PATCH 7/7] Working Synchronization --- Core/Inc/engine.h | 12 -- Core/Inc/sampling.hpp | 15 +- Core/Src/engine.cpp | 57 +++----- Core/Src/freertos.c | 27 ++-- Core/Src/gpio.c | 2 +- Core/Src/sampling.cpp | 110 +++++++++++++- CustomECU.ioc | 4 +- test/CMakeLists.txt | 15 ++ test/mocks/ulog.h | 2 +- test/unit/test_sync_detection.cpp | 26 ++-- test/unit/test_sync_validation.cpp | 225 +++++++++++++++++++++++++++++ 11 files changed, 411 insertions(+), 84 deletions(-) create mode 100644 test/unit/test_sync_validation.cpp diff --git a/Core/Inc/engine.h b/Core/Inc/engine.h index 9e648f4..737563d 100644 --- a/Core/Inc/engine.h +++ b/Core/Inc/engine.h @@ -19,18 +19,6 @@ extern "C" { */ void criticalEngineTask(void *argument); -/** - * @brief Handles synchronization detection with cam and crank signals. - * Specifically for the Honda CBR600CC engine with 12 equally-spaced - * crank teeth and 3 cam teeth, with one offset 30deg. - * - * Updates synced bool in SyncState struct. - * - * @param None. - * @retval None. - */ -void detectSync(void); - #ifdef __cplusplus } #endif diff --git a/Core/Inc/sampling.hpp b/Core/Inc/sampling.hpp index 53b7c34..bc209eb 100644 --- a/Core/Inc/sampling.hpp +++ b/Core/Inc/sampling.hpp @@ -31,6 +31,19 @@ float get_current_fraction_of_tooth(); */ float get_current_engine_angle(); + +/** + * @brief Handles synchronization detection with cam and crank signals. + * Specifically for the Honda CBR600CC engine with 12 equally-spaced + * crank teeth and 3 cam teeth, with one offset 30deg. + * + * Updates synced bool in SyncState struct. + * + * @param None. + * @retval None. + */ +void detectSync(void); + // General Engine Synchronization State. struct SyncState { // Whether we have locked synchronization or not. @@ -57,7 +70,7 @@ struct SyncState { // Instantaneous period between crank teeth (in microseconds). volatile double tooth_period_us = 0.0; - // Engine phase (0 = 0-360, 1 = 360-720) + // Engine phase (0 = 0-360, 1 = 360-720). bool engine_phase; // Gets the delta in crank teeth between the last two cam teeth. diff --git a/Core/Src/engine.cpp b/Core/Src/engine.cpp index 7fdbc48..869dfa5 100644 --- a/Core/Src/engine.cpp +++ b/Core/Src/engine.cpp @@ -9,44 +9,33 @@ uint32_t last_time = 0; uint32_t time = 0; -void detectSync(void) { - // Synchronization is determined when we see first three cam teeth. - // Use num crank pulses between cam teeth detections to determine engine - // cycle. - // - // diff btwn teeth == 330deg in crank: 0deg in cycle. - // diff btwn teeth == 360deg in crank: 360deg into cycle. - // diff btwn teeth == 30deg in crank: 390deg into cycle. - uint8_t delta_teeth = syncState.get_cam_delta(); - switch (delta_teeth) { - case 1: - syncState.crank_index = 1; - syncState.engine_phase = 1; - syncState.synced = true; - break; - case 11: - syncState.crank_index = 10; - syncState.engine_phase = 0; - syncState.synced = true; - break; - case 12: - syncState.crank_index = 0; - syncState.engine_phase = 0; - syncState.synced = true; - break; - default: - syncState.synced = false; - break; - } -} - void criticalEngineTask(void *argument) { + (void)argument; // Unused parameter + + ULOG_INFO("Engine task started - waiting for sync..."); + for (;;) { - while (!syncState.synced) { - detectSync(); + // Wait for sync to be acquired by cam tooth interrupts + if (!syncState.synced) { + // Sleep briefly to avoid busy-waiting + osDelay(1); + continue; + } + + // We're synced - perform engine control operations + float current_angle = get_current_engine_angle(); + + // TODO: Schedule fuel injection events + // TODO: Schedule ignition events + + // ULOG_INFO("EngAngle: %.2f", current_angle); + + if (!syncState.synced) { + ULOG_WARNING("Lost sync! Re-acquiring..."); + continue; } - // TODO: Schedule fuel/ignition events based on current angle + // Sleep briefly - actual timing is interrupt-driven osDelay(1); } } diff --git a/Core/Src/freertos.c b/Core/Src/freertos.c index 0beca23..61514e4 100644 --- a/Core/Src/freertos.c +++ b/Core/Src/freertos.c @@ -19,10 +19,9 @@ /* Includes ------------------------------------------------------------------*/ #include "FreeRTOS.h" -#include "cmsis_os.h" -#include "main.h" #include "task.h" - +#include "main.h" +#include "cmsis_os.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ @@ -60,9 +59,9 @@ const osThreadAttr_t criticalEngineTaskAttributes = { /* Definitions for defaultTask */ osThreadId_t defaultTaskHandle; const osThreadAttr_t defaultTask_attributes = { - .name = "defaultTask", - .stack_size = 128 * 4, - .priority = (osPriority_t)osPriorityNormal, + .name = "defaultTask", + .stack_size = 128 * 4, + .priority = (osPriority_t) osPriorityNormal, }; /* Private function prototypes -----------------------------------------------*/ @@ -74,10 +73,10 @@ void StartDefaultTask(void *argument); void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */ /** - * @brief FreeRTOS initialization - * @param None - * @retval None - */ + * @brief FreeRTOS initialization + * @param None + * @retval None + */ void MX_FREERTOS_Init(void) { /* USER CODE BEGIN Init */ ULOG_INIT(); @@ -104,8 +103,7 @@ void MX_FREERTOS_Init(void) { /* Create the thread(s) */ /* creation of defaultTask */ - defaultTaskHandle = - osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); + defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); /* USER CODE BEGIN RTOS_THREADS */ criticalEngineTaskHandle = @@ -116,6 +114,7 @@ void MX_FREERTOS_Init(void) { /* USER CODE BEGIN RTOS_EVENTS */ /* add events, ... */ /* USER CODE END RTOS_EVENTS */ + } /* USER CODE BEGIN Header_StartDefaultTask */ @@ -125,7 +124,8 @@ void MX_FREERTOS_Init(void) { * @retval None */ /* USER CODE END Header_StartDefaultTask */ -void StartDefaultTask(void *argument) { +void StartDefaultTask(void *argument) +{ /* USER CODE BEGIN StartDefaultTask */ /* Infinite loop */ for (;;) { @@ -138,3 +138,4 @@ void StartDefaultTask(void *argument) { /* USER CODE BEGIN Application */ /* USER CODE END Application */ + diff --git a/Core/Src/gpio.c b/Core/Src/gpio.c index 298e527..0ac3258 100644 --- a/Core/Src/gpio.c +++ b/Core/Src/gpio.c @@ -49,7 +49,7 @@ void MX_GPIO_Init(void) /*Configure GPIO pins : CAM_SIGNAL_Pin CRANK_SIGNAL_Pin */ GPIO_InitStruct.Pin = CAM_SIGNAL_Pin|CRANK_SIGNAL_Pin; - GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; + GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); diff --git a/Core/Src/sampling.cpp b/Core/Src/sampling.cpp index 5323073..0630167 100644 --- a/Core/Src/sampling.cpp +++ b/Core/Src/sampling.cpp @@ -1,6 +1,8 @@ #include "sampling.hpp" +#include "engine.h" #include "main.h" #include "stm32f4xx_hal_gpio.h" +#include "ulog.h" #include "us_timer.h" // Tooth pattern is defined as 12 equally spaced @@ -13,8 +15,26 @@ // smoothing. #define ALPHA 0.8 +// Sync validation tolerance - tooth period can vary +/- this percentage +#define TOOTH_PERIOD_TOLERANCE 0.25 // 25% tolerance + struct SyncState syncState = {}; +// Helper function to validate if current tooth timing is reasonable +static bool validate_tooth_timing(double dt) { + if (syncState.tooth_period_us <= 0.0) { + return true; // No reference period yet + } + + // Check if the time between teeth is within tolerance + double lower_bound = + syncState.tooth_period_us * (1.0 - TOOTH_PERIOD_TOLERANCE); + double upper_bound = + syncState.tooth_period_us * (1.0 + TOOTH_PERIOD_TOLERANCE); + + return (dt >= lower_bound && dt <= upper_bound); +} + void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { switch (GPIO_Pin) { case CAM_SIGNAL_Pin: @@ -29,25 +49,47 @@ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { } void on_crank_tooth() { + // ULOG_DEBUG("Crank tooth detected"); uint32_t current_time = get_micros(); + // Calculate time since last tooth + bool timing_valid = true; + // Only calculate period if we've seen at least one tooth before if (syncState.crank_counter > 0) { double dt = double(current_time - syncState.last_crank_time_us); - if (syncState.tooth_period_us <= 0.0) { - // First valid period measurement - syncState.tooth_period_us = dt; - } else { - // Run a exponential moving average to smooth out jitter. - syncState.tooth_period_us = - ALPHA * dt + (1.0 - ALPHA) * syncState.tooth_period_us; + // If synced, validate the tooth timing + if (syncState.synced) { + timing_valid = validate_tooth_timing(dt); + + if (!timing_valid) { + // Lost sync due to missed tooth or timing anomaly + syncState.synced = false; + syncState.crank_index = 0; + syncState.last_cam_crank_counter = 0; + syncState.cam_crank_counter = 0; + // Note: tooth_period_us is kept to help with re-sync + } + } + + // Update tooth period if timing is valid + if (timing_valid) { + if (syncState.tooth_period_us <= 0.0) { + // First valid period measurement + syncState.tooth_period_us = dt; + } else { + // Run a exponential moving average to smooth out jitter. + syncState.tooth_period_us = + ALPHA * dt + (1.0 - ALPHA) * syncState.tooth_period_us; + } } } syncState.last_crank_time_us = current_time; syncState.crank_counter++; + // Only update engine position if still synced if (syncState.synced) { if (syncState.crank_index == NUM_CRANK_TEETH - 1) { syncState.engine_phase = !syncState.engine_phase; @@ -60,6 +102,28 @@ void on_cam_tooth() { syncState.last_cam_time_us = get_micros(); syncState.last_cam_crank_counter = syncState.cam_crank_counter; syncState.cam_crank_counter = syncState.crank_counter; + + uint8_t delta = syncState.get_cam_delta(); + + // If we're synced, validate that the cam delta matches expectations + if (syncState.synced && syncState.last_cam_crank_counter > 0) { + // Valid deltas are 2, 10, or 12 teeth between cam pulses + // Any other delta means we lost sync (missed teeth) + if (delta != 2 && delta != 10 && delta != 12) { + syncState.synced = false; + syncState.crank_index = 0; + syncState.last_cam_crank_counter = 0; + syncState.cam_crank_counter = 0; + } + } else if (!syncState.synced && syncState.last_cam_crank_counter > 0) { + // If we're not synced, try to acquire sync with this cam tooth + detectSync(); + + if (syncState.synced) { + ULOG_INFO("Sync acquired! Crank index: %u, Phase: %u", + syncState.crank_index, syncState.engine_phase); + } + } } float get_current_fraction_of_tooth() { @@ -98,3 +162,35 @@ float get_current_engine_angle() { return angle; } + + +void detectSync(void) { + // Synchronization is determined when we see first three cam teeth. + // Use num crank pulses between cam teeth detections to determine engine + // cycle. + // + // diff btwn teeth == 330deg in crank: 0deg in cycle. + // diff btwn teeth == 360deg in crank: 360deg into cycle. + // diff btwn teeth == 30deg in crank: 390deg into cycle. + uint8_t delta_teeth = syncState.get_cam_delta(); + switch (delta_teeth) { + case 2: + syncState.crank_index = 1; + syncState.engine_phase = 1; + syncState.synced = true; + break; + case 10: + syncState.crank_index = 10; + syncState.engine_phase = 0; + syncState.synced = true; + break; + case 12: + syncState.crank_index = 0; + syncState.engine_phase = 0; + syncState.synced = true; + break; + default: + syncState.synced = false; + break; + } +} diff --git a/CustomECU.ioc b/CustomECU.ioc index 49bf000..e98ade0 100644 --- a/CustomECU.ioc +++ b/CustomECU.ioc @@ -70,12 +70,12 @@ PA9.Mode=Asynchronous PA9.Signal=USART1_TX PB13.GPIOParameters=GPIO_Label,GPIO_ModeDefaultEXTI PB13.GPIO_Label=CAM_SIGNAL -PB13.GPIO_ModeDefaultEXTI=GPIO_MODE_IT_FALLING +PB13.GPIO_ModeDefaultEXTI=GPIO_MODE_IT_RISING PB13.Locked=true PB13.Signal=GPXTI13 PB14.GPIOParameters=GPIO_Label,GPIO_ModeDefaultEXTI PB14.GPIO_Label=CRANK_SIGNAL -PB14.GPIO_ModeDefaultEXTI=GPIO_MODE_IT_FALLING +PB14.GPIO_ModeDefaultEXTI=GPIO_MODE_IT_RISING PB14.Locked=true PB14.Signal=GPXTI14 PH0-OSC_IN.Mode=HSE-External-Oscillator diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1cc7c88..8b19d46 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -83,7 +83,22 @@ target_include_directories(test_sync_detection PRIVATE target_link_libraries(test_sync_detection unity) target_compile_options(test_sync_detection PRIVATE -DUNIT_TEST) +# Test 3: Sync validation tests +add_executable(test_sync_validation + unit/test_sync_validation.cpp + ${MOCK_SOURCES} + ${SOURCE_UNDER_TEST} +) +target_include_directories(test_sync_validation PRIVATE + ${CMAKE_SOURCE_DIR}/../Core/Inc + ${CMAKE_SOURCE_DIR}/mocks + frameworks/Unity/src +) +target_link_libraries(test_sync_validation unity) +target_compile_options(test_sync_validation PRIVATE -DUNIT_TEST) + # Add tests to CTest enable_testing() add_test(NAME SamplingTests COMMAND test_sampling) add_test(NAME SyncDetectionTests COMMAND test_sync_detection) +add_test(NAME SyncValidationTests COMMAND test_sync_validation) diff --git a/test/mocks/ulog.h b/test/mocks/ulog.h index 3bb9dde..9d4b8e4 100644 --- a/test/mocks/ulog.h +++ b/test/mocks/ulog.h @@ -14,7 +14,7 @@ extern "C" { #define ULOG_INFO(...) \ printf("[INFO] " __VA_ARGS__); \ printf("\n") -#define ULOG_WARN(...) \ +#define ULOG_WARNING(...) \ printf("[WARN] " __VA_ARGS__); \ printf("\n") #define ULOG_ERROR(...) \ diff --git a/test/unit/test_sync_detection.cpp b/test/unit/test_sync_detection.cpp index a7edf96..5788fa8 100644 --- a/test/unit/test_sync_detection.cpp +++ b/test/unit/test_sync_detection.cpp @@ -10,8 +10,8 @@ // Include sampling header #include "sampling.hpp" -// Declare the function we're testing (needs C linkage) -extern "C" void detectSync(void); +// Declare the function we're testing +void detectSync(void); void setUp(void) { // Reset sync state before each test @@ -31,12 +31,12 @@ void tearDown(void) { } // ============================================================================ -// Test: Sync Detection with Delta = 1 +// Test: Sync Detection with Delta = 2 // ============================================================================ -void test_detect_sync_with_delta_1_sets_correct_state(void) { - // Setup: cam teeth with delta of 1 crank tooth +void test_detect_sync_with_delta_2_sets_correct_state(void) { + // Setup: cam teeth with delta of 2 crank teeth syncState.cam_crank_counter = 5; - syncState.last_cam_crank_counter = 4; // Delta = 1 + syncState.last_cam_crank_counter = 3; // Delta = 2 detectSync(); @@ -46,12 +46,12 @@ void test_detect_sync_with_delta_1_sets_correct_state(void) { } // ============================================================================ -// Test: Sync Detection with Delta = 11 +// Test: Sync Detection with Delta = 10 // ============================================================================ -void test_detect_sync_with_delta_11_sets_correct_state(void) { - // Setup: cam teeth with delta of 11 crank teeth +void test_detect_sync_with_delta_10_sets_correct_state(void) { + // Setup: cam teeth with delta of 10 crank teeth syncState.cam_crank_counter = 20; - syncState.last_cam_crank_counter = 9; // Delta = 11 + syncState.last_cam_crank_counter = 10; // Delta = 10 detectSync(); @@ -162,7 +162,7 @@ void test_sync_can_be_reestablished_after_loss(void) { // Reestablish sync syncState.cam_crank_counter = 31; - syncState.last_cam_crank_counter = 20; // Delta = 11 + syncState.last_cam_crank_counter = 21; // Delta = 10 detectSync(); TEST_ASSERT_TRUE(syncState.synced); TEST_ASSERT_EQUAL_UINT8(10, syncState.crank_index); @@ -190,8 +190,8 @@ int main(void) { UNITY_BEGIN(); // Sync detection with valid deltas - RUN_TEST(test_detect_sync_with_delta_1_sets_correct_state); - RUN_TEST(test_detect_sync_with_delta_11_sets_correct_state); + RUN_TEST(test_detect_sync_with_delta_2_sets_correct_state); + RUN_TEST(test_detect_sync_with_delta_10_sets_correct_state); RUN_TEST(test_detect_sync_with_delta_12_sets_correct_state); // Sync detection with invalid deltas diff --git a/test/unit/test_sync_validation.cpp b/test/unit/test_sync_validation.cpp new file mode 100644 index 0000000..8657871 --- /dev/null +++ b/test/unit/test_sync_validation.cpp @@ -0,0 +1,225 @@ +#include "mock_main.h" +#include "mock_us_timer.h" +#include "unity.h" + +// Include sampling header +#include "sampling.hpp" + +void setUp(void) { + // Reset sync state before each test + syncState.synced = false; + syncState.crank_index = 0; + syncState.crank_counter = 0; + syncState.cam_crank_counter = 0; + syncState.last_cam_crank_counter = 0; + syncState.last_crank_time_us = 0; + syncState.last_cam_time_us = 0; + syncState.tooth_period_us = 0.0; + syncState.engine_phase = false; + + mock_us_timer_set_time(0); +} + +void tearDown(void) { + // Cleanup +} + +// ============================================================================ +// Test: Sync Loss on Missed Crank Tooth +// ============================================================================ +void test_sync_lost_when_tooth_period_too_long(void) { + // Establish sync + mock_us_timer_set_time(0); + on_crank_tooth(); + + mock_us_timer_set_time(1000); + on_crank_tooth(); + + // Establish period of 1000us + mock_us_timer_set_time(2000); + on_crank_tooth(); + + // Establish sync + syncState.synced = true; + syncState.crank_index = 5; + + // Next tooth comes way too late (2x expected) - missed a tooth + mock_us_timer_set_time(4100); // 2100us instead of 1000us + on_crank_tooth(); + + // Should have lost sync + TEST_ASSERT_FALSE(syncState.synced); + TEST_ASSERT_EQUAL_UINT8(0, syncState.crank_index); +} + +void test_sync_lost_when_tooth_period_too_short(void) { + // Establish sync + mock_us_timer_set_time(0); + on_crank_tooth(); + + mock_us_timer_set_time(1000); + on_crank_tooth(); + + mock_us_timer_set_time(2000); + on_crank_tooth(); + + syncState.synced = true; + syncState.crank_index = 5; + + // Next tooth comes way too early (half expected) + mock_us_timer_set_time(2400); // 400us instead of 1000us + on_crank_tooth(); + + // Should have lost sync + TEST_ASSERT_FALSE(syncState.synced); +} + +void test_sync_maintained_with_small_timing_variations(void) { + // Establish period + mock_us_timer_set_time(0); + on_crank_tooth(); + + mock_us_timer_set_time(1000); + on_crank_tooth(); + + mock_us_timer_set_time(2000); + on_crank_tooth(); + + syncState.synced = true; + syncState.crank_index = 5; + + // Small variation within tolerance (10% faster) + mock_us_timer_set_time(2900); // 900us (within 25% tolerance) + on_crank_tooth(); + + TEST_ASSERT_TRUE(syncState.synced); + TEST_ASSERT_EQUAL_UINT8(6, syncState.crank_index); +} + +// ============================================================================ +// Test: Sync Loss on Invalid Cam Delta +// ============================================================================ +void test_sync_lost_on_invalid_cam_delta(void) { + // Set up synced state with valid tooth period + mock_us_timer_set_time(1000); + on_crank_tooth(); + mock_us_timer_set_time(2000); + on_crank_tooth(); + + syncState.synced = true; + syncState.crank_index = 3; + + // First cam tooth + syncState.cam_crank_counter = 10; + syncState.last_cam_crank_counter = 0; + on_cam_tooth(); + + // Continue with crank teeth + uint32_t time = 2000; + for (int i = 0; i < 5; i++) { + time += 1000; + mock_us_timer_set_time(time); + on_crank_tooth(); + } + + // Second cam tooth with invalid delta (5 teeth - invalid!) + syncState.cam_crank_counter = syncState.crank_counter; + on_cam_tooth(); + + // Should have lost sync + TEST_ASSERT_FALSE(syncState.synced); + TEST_ASSERT_EQUAL_UINT8(0, syncState.crank_index); +} + +void test_sync_maintained_on_valid_cam_delta(void) { + // Set up synced state + mock_us_timer_set_time(1000); + on_crank_tooth(); + mock_us_timer_set_time(2000); + on_crank_tooth(); + + syncState.synced = true; + syncState.crank_index = 0; + + // Simulate first cam tooth at current position + syncState.last_cam_crank_counter = 0; + syncState.cam_crank_counter = + syncState.crank_counter; // Record current position + + // Continue with exactly 12 crank teeth + uint32_t time = 2000; + for (int i = 0; i < 12; i++) { + time += 1000; + mock_us_timer_set_time(time); + on_crank_tooth(); + } + + // Second cam tooth with valid delta (12 teeth) + on_cam_tooth(); + + // Should maintain sync (delta should be 12) + TEST_ASSERT_TRUE(syncState.synced); +} + +// ============================================================================ +// Test: Sync Re-establishment After Loss +// ============================================================================ +void test_can_regain_sync_after_loss(void) { + // Establish initial sync + mock_us_timer_set_time(0); + on_crank_tooth(); + mock_us_timer_set_time(1000); + on_crank_tooth(); + mock_us_timer_set_time(2000); + on_crank_tooth(); + + syncState.synced = true; + syncState.crank_index = 5; + + // Lose sync due to timing + mock_us_timer_set_time(4500); // Way too long + on_crank_tooth(); + + TEST_ASSERT_FALSE(syncState.synced); + + // Now continue with regular teeth to re-establish pattern + uint32_t time = 5500; + for (int i = 0; i < 15; i++) { + mock_us_timer_set_time(time); + on_crank_tooth(); + time += 1000; + } + + // Period should be re-established + TEST_ASSERT_DOUBLE_WITHIN(100.0, 1000.0, syncState.tooth_period_us); + + // Manually re-sync (this would be done by detectSync in real system) + syncState.synced = true; + + // Verify sync is maintained with good timing + mock_us_timer_set_time(time); + on_crank_tooth(); + + TEST_ASSERT_TRUE(syncState.synced); +} + +// ============================================================================ +// Main test runner +// ============================================================================ +int main(void) { + UNITY_BEGIN(); + + // Sync loss tests + RUN_TEST(test_sync_lost_when_tooth_period_too_long); + RUN_TEST(test_sync_lost_when_tooth_period_too_short); + RUN_TEST(test_sync_maintained_with_small_timing_variations); + + // Cam delta validation tests + RUN_TEST(test_sync_lost_on_invalid_cam_delta); + RUN_TEST(test_sync_maintained_on_valid_cam_delta); + + // Recovery tests + RUN_TEST(test_can_regain_sync_after_loss); + + return UNITY_END(); +}