From 5212e8d6a8dd1110fc9d31273340ed23e05bf297 Mon Sep 17 00:00:00 2001 From: Stanislav Yaranov Date: Wed, 13 May 2026 14:16:35 +0300 Subject: [PATCH] adding SPI and CAN simulation --- libs/stm32/sim/src/sim.cpp | 2 + libs/stm32/sim/src/sim.hpp | 2 + libs/stm32/sim/src/sim/base.cpp | 96 ++++++++++ libs/stm32/sim/src/sim/can.cpp | 314 ++++++++++++++++++++++++++++++++ libs/stm32/sim/src/sim/can.hpp | 82 +++++++++ libs/stm32/sim/src/sim/core.cpp | 11 ++ libs/stm32/sim/src/sim/core.hpp | 6 + libs/stm32/sim/src/sim/def.hpp | 2 + libs/stm32/sim/src/sim/spi.cpp | 240 ++++++++++++++++++++++++ libs/stm32/sim/src/sim/spi.hpp | 91 +++++++++ 10 files changed, 846 insertions(+) create mode 100644 libs/stm32/sim/src/sim/can.cpp create mode 100644 libs/stm32/sim/src/sim/can.hpp create mode 100644 libs/stm32/sim/src/sim/spi.cpp create mode 100644 libs/stm32/sim/src/sim/spi.hpp diff --git a/libs/stm32/sim/src/sim.cpp b/libs/stm32/sim/src/sim.cpp index 527b084..9111489 100644 --- a/libs/stm32/sim/src/sim.cpp +++ b/libs/stm32/sim/src/sim.cpp @@ -17,7 +17,9 @@ reset() { Core::reset(); Base::reset(); + CAN::reset(); I2C::reset(); + SPI::reset(); Uart::reset(); } diff --git a/libs/stm32/sim/src/sim.hpp b/libs/stm32/sim/src/sim.hpp index 5d6ab31..dcac7a3 100644 --- a/libs/stm32/sim/src/sim.hpp +++ b/libs/stm32/sim/src/sim.hpp @@ -10,10 +10,12 @@ #pragma once #include "sim/base.hpp" +#include "sim/can.hpp" #include "sim/core.hpp" #include "sim/gpio.hpp" #include "sim/i2c.hpp" #include "sim/signal.hpp" +#include "sim/spi.hpp" #include "sim/uart.hpp" namespace Embys::Stm32::Sim diff --git a/libs/stm32/sim/src/sim/base.cpp b/libs/stm32/sim/src/sim/base.cpp index 2fcd5ce..d6fb64c 100644 --- a/libs/stm32/sim/src/sim/base.cpp +++ b/libs/stm32/sim/src/sim/base.cpp @@ -141,6 +141,95 @@ check_irq_i2c(I2C_TypeDef *i2c_instance, void (**I2C_EV_IRQHandler_ptr)(), } } +/** + * @brief Check for CAN interrupts and call the corresponding handlers if + * conditions are met. + * `interrupted` is set to true if any CAN interrupt was handled. + * @param can_instance Pointer to the CAN instance to check. + * @param TX_IRQHandler_ptr Pointer to the CAN TX interrupt handler pointer. + * @param RX0_IRQHandler_ptr Pointer to the CAN RX0 interrupt handler pointer. + * @param RX1_IRQHandler_ptr Pointer to the CAN RX1 interrupt handler pointer. + * @param SCE_IRQHandler_ptr Pointer to the CAN SCE interrupt handler pointer. + */ +void +check_irq_can(CAN_TypeDef *can_instance, void (**TX_IRQHandler_ptr)(), + void (**RX0_IRQHandler_ptr)(), void (**RX1_IRQHandler_ptr)(), + void (**SCE_IRQHandler_ptr)()) +{ + const uint32_t ier = can_instance->IER; + + // TX: at least one mailbox became empty + if (*TX_IRQHandler_ptr && (ier & CAN_IER_TMEIE_Msk) && + (can_instance->TSR & + (CAN_TSR_RQCP0_Msk | CAN_TSR_RQCP1_Msk | CAN_TSR_RQCP2_Msk))) + { + (*TX_IRQHandler_ptr)(); + interrupted = true; + } + + // RX FIFO 0: message pending + if (*RX0_IRQHandler_ptr && (ier & CAN_IER_FMPIE0_Msk) && + (can_instance->RF0R & CAN_RF0R_FMP0_Msk)) + { + (*RX0_IRQHandler_ptr)(); + interrupted = true; + } + + // RX FIFO 1: message pending + if (*RX1_IRQHandler_ptr && (ier & CAN_IER_FMPIE1_Msk) && + (can_instance->RF1R & CAN_RF1R_FMP1_Msk)) + { + (*RX1_IRQHandler_ptr)(); + interrupted = true; + } + + // SCE: error / bus-off / wakeup + if (*SCE_IRQHandler_ptr) + { + const bool boff = + (can_instance->ESR & CAN_ESR_BOFF_Msk) && (ier & CAN_IER_BOFIE_Msk); + const bool epv = + (can_instance->ESR & CAN_ESR_EPVF_Msk) && (ier & CAN_IER_EPVIE_Msk); + const bool ewg = + (can_instance->ESR & CAN_ESR_EWGF_Msk) && (ier & CAN_IER_EWGIE_Msk); + + if (boff || epv || ewg) + { + (*SCE_IRQHandler_ptr)(); + interrupted = true; + } + } +} + +/** + * @brief Check for SPI interrupts and call the corresponding handler if + * conditions are met. + * `interrupted` is set to true if any SPI interrupt was handled. + * @param spi_instance Pointer to the SPI instance to check. + * @param SPI_IRQHandler_ptr Pointer to the SPI interrupt handler function + * pointer. + */ +void +check_irq_spi(SPI_TypeDef *spi_instance, void (**SPI_IRQHandler_ptr)()) +{ + if (!*SPI_IRQHandler_ptr) + return; + + const uint32_t sr = spi_instance->SR; + const uint32_t cr2 = spi_instance->CR2; + + const bool rxne_irq = (sr & SPI_SR_RXNE) && (cr2 & SPI_CR2_RXNEIE); + const bool txe_irq = (sr & SPI_SR_TXE) && (cr2 & SPI_CR2_TXEIE); + const bool err_irq = (sr & (SPI_SR_MODF | SPI_SR_OVR | SPI_SR_CRCERR)) && + (cr2 & SPI_CR2_ERRIE); + + if (rxne_irq || txe_irq || err_irq) + { + (*SPI_IRQHandler_ptr)(); + interrupted = true; + } +} + /** * @brief Check for USART interrupts and call the corresponding handler if * conditions are met. @@ -407,6 +496,13 @@ cycle() check_irq_i2c(&i2c2_instance, &I2C2_EV_IRQHandler_ptr, &I2C2_ER_IRQHandler_ptr); + check_irq_can(&can1_instance, &CAN1_TX_IRQHandler_ptr, + &CAN1_RX0_IRQHandler_ptr, &CAN1_RX1_IRQHandler_ptr, + &CAN1_SCE_IRQHandler_ptr); + + check_irq_spi(&spi1_instance, &SPI1_IRQHandler_ptr); + check_irq_spi(&spi2_instance, &SPI2_IRQHandler_ptr); + check_irq_usart(&usart1_instance, &USART1_IRQHandler_ptr); check_irq_usart(&usart2_instance, &USART2_IRQHandler_ptr); check_irq_usart(&usart3_instance, &USART3_IRQHandler_ptr); diff --git a/libs/stm32/sim/src/sim/can.cpp b/libs/stm32/sim/src/sim/can.cpp new file mode 100644 index 0000000..2368064 --- /dev/null +++ b/libs/stm32/sim/src/sim/can.cpp @@ -0,0 +1,314 @@ +/** + * @file can.cpp + * @author Stanislav Yaranov (stanislav.yaranov@gmail.com) + * @brief CAN simulation for the STM32 simulated environment. + * @version 0.1 + * @date 2026-05-11 + * @copyright Copyright (c) 2026 + */ + +#include "can.hpp" + +#include + +#include "base.hpp" + +namespace Embys::Stm32::Sim::CAN +{ + +CAN_TypeDef *can = &can1_instance; // Default to can1 + +std::vector tx_frames; + +Callable on_tx; + +/** + * @brief Number of cycles between TXRQ detection and setting TME/RQCP/TXOK, + * simulating bus transmission time. + */ +static constexpr uint32_t TX_DELAY = 20; + +/** + * @brief Per-FIFO receive queues (FIFO 0 and FIFO 1). + */ +static std::queue rx_fifo[2]; + +/** + * @brief Load the front frame of @p fifo_idx into the peripheral's FIFO + * mailbox registers and update FMP / FULL flags. + */ +static void +load_fifo_mailbox(uint8_t fifo_idx) +{ + if (rx_fifo[fifo_idx].empty()) + return; + + const Frame &f = rx_fifo[fifo_idx].front(); + CAN_FIFOMailBox_TypeDef &mb = can->sFIFOMailBox[fifo_idx]; + + // Build RIR from local accumulator to avoid compound assignment on volatile + uint32_t rir = 0; + + if (f.ide) + { + rir |= (f.id << CAN_RI0R_EXID_Pos) & CAN_RI0R_EXID_Msk; + rir |= CAN_RI0R_IDE_Msk; + } + else + { + rir |= (f.id << CAN_RI0R_STID_Pos) & CAN_RI0R_STID_Msk; + } + + if (f.rtr) + rir |= CAN_RI0R_RTR_Msk; + + mb.RIR = rir; + + // DLC + mb.RDTR = (f.dlc & 0xFU); + + // Data bytes + mb.RDLR = (static_cast(f.data[0])) | + (static_cast(f.data[1]) << 8) | + (static_cast(f.data[2]) << 16) | + (static_cast(f.data[3]) << 24); + + mb.RDHR = (static_cast(f.data[4])) | + (static_cast(f.data[5]) << 8) | + (static_cast(f.data[6]) << 16) | + (static_cast(f.data[7]) << 24); + + // Update FMP and FULL + const uint32_t fmp = static_cast(rx_fifo[fifo_idx].size()); + + if (fifo_idx == 0) + { + can->RF0R = (can->RF0R & ~CAN_RF0R_FMP0_Msk) | (fmp & 0x3U); + + if (fmp >= 3) + SET_BIT_V(can->RF0R, CAN_RF0R_FULL0_Msk); + else + CLEAR_BIT_V(can->RF0R, CAN_RF0R_FULL0_Msk); + } + else + { + can->RF1R = (can->RF1R & ~CAN_RF1R_FMP1_Msk) | (fmp & 0x3U); + + if (fmp >= 3) + SET_BIT_V(can->RF1R, CAN_RF1R_FULL1_Msk); + else + CLEAR_BIT_V(can->RF1R, CAN_RF1R_FULL1_Msk); + } +} + +void +simulate_rx(Frame frame, uint8_t fifo) +{ + if (fifo > 1) + return; + + const bool was_empty = rx_fifo[fifo].empty(); + rx_fifo[fifo].push(frame); + + // If this is the first frame, load it immediately into the mailbox registers + if (was_empty) + load_fifo_mailbox(fifo); + else + { + // Just update FMP count + const uint32_t fmp = static_cast(rx_fifo[fifo].size()); + + if (fifo == 0) + { + can->RF0R = (can->RF0R & ~CAN_RF0R_FMP0_Msk) | (fmp & 0x3U); + + if (fmp >= 3) + SET_BIT_V(can->RF0R, CAN_RF0R_FULL0_Msk); + } + else + { + can->RF1R = (can->RF1R & ~CAN_RF1R_FMP1_Msk) | (fmp & 0x3U); + + if (fmp >= 3) + SET_BIT_V(can->RF1R, CAN_RF1R_FULL1_Msk); + } + } +} + +/** + * @brief Read a Frame out of a TX mailbox register set. + * @param mb Reference to the mailbox. + * @return The decoded Frame. + */ +static Frame +read_tx_mailbox(const CAN_TxMailBox_TypeDef &mb) +{ + Frame f = {}; + + const bool ide = (mb.TIR & CAN_TI0R_IDE_Msk) != 0; + + f.ide = ide; + f.rtr = (mb.TIR & CAN_TI0R_RTR_Msk) != 0; + + if (ide) + f.id = (mb.TIR & CAN_TI0R_EXID_Msk) >> CAN_TI0R_EXID_Pos; + else + f.id = (mb.TIR & CAN_TI0R_STID_Msk) >> CAN_TI0R_STID_Pos; + + f.dlc = static_cast(mb.TDTR & CAN_TDT0R_DLC_Msk); + + f.data[0] = static_cast(mb.TDLR & 0xFFU); + f.data[1] = static_cast((mb.TDLR >> 8) & 0xFFU); + f.data[2] = static_cast((mb.TDLR >> 16) & 0xFFU); + f.data[3] = static_cast((mb.TDLR >> 24) & 0xFFU); + f.data[4] = static_cast(mb.TDHR & 0xFFU); + f.data[5] = static_cast((mb.TDHR >> 8) & 0xFFU); + f.data[6] = static_cast((mb.TDHR >> 16) & 0xFFU); + f.data[7] = static_cast((mb.TDHR >> 24) & 0xFFU); + + return f; +} + +// Per-mailbox TX pending state: stores the cycle at which transmission should +// complete, or 0 if no transmission is pending. +static uint32_t tx_complete_cyc[3] = {}; + +// TXRQ bits per mailbox, indexed 0-2 +static constexpr uint32_t TXRQ_BITS[3] = { + CAN_TI0R_TXRQ_Msk, + CAN_TI0R_TXRQ_Msk, // same bit position in every TIR + CAN_TI0R_TXRQ_Msk, +}; + +// Per-mailbox TSR bits +struct MailboxTsrBits +{ + uint32_t tme; + uint32_t rqcp; + uint32_t txok; +}; + +static constexpr MailboxTsrBits TSR_BITS[3] = { + {CAN_TSR_TME0_Msk, CAN_TSR_RQCP0_Msk, CAN_TSR_TXOK0_Msk}, + {CAN_TSR_TME1_Msk, CAN_TSR_RQCP1_Msk, CAN_TSR_TXOK1_Msk}, + {CAN_TSR_TME2_Msk, CAN_TSR_RQCP2_Msk, CAN_TSR_TXOK2_Msk}, +}; + +/** + * @brief Persistent hook called every cycle. + * Handles init/sleep mode acknowledgement, TX TXRQ detection and completion, + * and RFOM (Release FIFO Output Mailbox) handling. + */ +void +peripheral_hook(uint32_t cyc) +{ + // Init mode handshake: MCR_INRQ set → acknowledge with MSR_INAK + if (can->MCR & CAN_MCR_INRQ_Msk) + SET_BIT_V(can->MSR, CAN_MSR_INAK_Msk); + else + CLEAR_BIT_V(can->MSR, CAN_MSR_INAK_Msk); + + // Sleep mode handshake: MCR_SLEEP set → acknowledge with MSR_SLAK + if (can->MCR & CAN_MCR_SLEEP_Msk) + SET_BIT_V(can->MSR, CAN_MSR_SLAK_Msk); + else + CLEAR_BIT_V(can->MSR, CAN_MSR_SLAK_Msk); + + // TX: scan mailboxes for TXRQ + for (uint8_t i = 0; i < 3; ++i) + { + CAN_TxMailBox_TypeDef &mb = can->sTxMailBox[i]; + + if ((mb.TIR & TXRQ_BITS[i]) && tx_complete_cyc[i] == 0) + { + // Schedule completion after TX_DELAY cycles + tx_complete_cyc[i] = cyc + TX_DELAY; + + // Mark mailbox as busy (TME cleared) + CLEAR_BIT_V(can->TSR, TSR_BITS[i].tme); + } + + if (tx_complete_cyc[i] != 0 && cyc >= tx_complete_cyc[i]) + { + tx_complete_cyc[i] = 0; + + Frame f = read_tx_mailbox(mb); + tx_frames.push_back(f); + + // Clear TXRQ, set RQCP and TXOK, mark mailbox empty + CLEAR_BIT_V(mb.TIR, TXRQ_BITS[i]); + SET_BIT_V(can->TSR, + TSR_BITS[i].rqcp | TSR_BITS[i].txok | TSR_BITS[i].tme); + + on_tx(f); + } + } + + // RX FIFO 0: release output mailbox when RFOM0 is set by firmware + if (can->RF0R & CAN_RF0R_RFOM0_Msk) + { + CLEAR_BIT_V(can->RF0R, CAN_RF0R_RFOM0_Msk); + + if (!rx_fifo[0].empty()) + { + rx_fifo[0].pop(); + + if (rx_fifo[0].empty()) + { + can->RF0R = 0; + } + else + { + load_fifo_mailbox(0); + } + } + } + + // RX FIFO 1: release output mailbox when RFOM1 is set by firmware + if (can->RF1R & CAN_RF1R_RFOM1_Msk) + { + CLEAR_BIT_V(can->RF1R, CAN_RF1R_RFOM1_Msk); + + if (!rx_fifo[1].empty()) + { + rx_fifo[1].pop(); + + if (rx_fifo[1].empty()) + { + can->RF1R = 0; + } + else + { + load_fifo_mailbox(1); + } + } + } +} + +void +reset() +{ + can = &can1_instance; + + tx_frames.clear(); + on_tx.clear(); + + rx_fifo[0] = {}; + rx_fifo[1] = {}; + + tx_complete_cyc[0] = 0; + tx_complete_cyc[1] = 0; + tx_complete_cyc[2] = 0; + + // All TX mailboxes start empty; CAN starts in sleep mode (after reset, + // hardware sets SLEEP and SLAK by default). + can->MCR = CAN_MCR_SLEEP_Msk; + can->MSR = CAN_MSR_SLAK_Msk; + can->TSR = CAN_TSR_TME0_Msk | CAN_TSR_TME1_Msk | CAN_TSR_TME2_Msk; + can->RF0R = 0; + can->RF1R = 0; + + Base::add_hook(peripheral_hook); +} + +} // namespace Embys::Stm32::Sim::CAN diff --git a/libs/stm32/sim/src/sim/can.hpp b/libs/stm32/sim/src/sim/can.hpp new file mode 100644 index 0000000..a3e0588 --- /dev/null +++ b/libs/stm32/sim/src/sim/can.hpp @@ -0,0 +1,82 @@ +/** + * @file can.hpp + * @author Stanislav Yaranov (stanislav.yaranov@gmail.com) + * @brief CAN simulation for the STM32 mock environment. + * + * No test hooks are required from firmware code — the simulation detects + * hardware-level events by monitoring register changes in the persistent + * hook: + * - TX: firmware sets CAN_TI0R_TXRQ in a mailbox TIR → frame is captured, + * mailbox is marked empty (TMEn), RQCP/TXOK bits are set in TSR, and + * on_tx is invoked. + * - RX: call simulate_rx() from test code to inject a CAN frame into FIFO 0 + * or FIFO 1; the FMP count is updated and the FMPIE interrupt fires. + * - Init/Sleep mode: the simulation acknowledges MCR_INRQ with MSR_INAK, + * and MCR_SLEEP with MSR_SLAK, on the next cycle. + * - RFOM: firmware sets RF0R_RFOM0 or RF1R_RFOM1 to release the current + * FIFO slot; the simulation dequeues the front entry and clears the bit. + * + * @version 0.1 + * @date 2026-05-11 + * @copyright Copyright (c) 2026 + */ + +#pragma once + +#include +#include + +#include + +#include "core.hpp" + +namespace Embys::Stm32::Sim::CAN +{ + +/** + * @brief A CAN bus frame. + */ +struct Frame +{ + uint32_t id; ///< 11-bit (standard) or 29-bit (extended) identifier + bool ide; ///< true = extended frame (29-bit ID) + bool rtr; ///< true = remote transmission request + uint8_t dlc; ///< data length code (0–8) + uint8_t data[8]; ///< payload bytes (only [0..dlc-1] are meaningful) +}; + +/** + * @brief Pointer to the CAN peripheral instance used in the mock environment. + */ +extern CAN_TypeDef *can; + +/** + * @brief Frames transmitted by the firmware (captured from TX mailboxes). + * Appended each time a mailbox TXRQ is detected and the frame is processed. + */ +extern std::vector tx_frames; + +/** + * @brief Callback invoked when the firmware successfully transmits a CAN + * frame. Receives a copy of the transmitted Frame. + */ +extern Callable on_tx; + +/** + * @brief Inject a CAN frame into a receive FIFO. + * The frame will be placed at the back of the specified FIFO queue. + * A FMPIE interrupt will fire on the next cycle if enabled. + * @param frame The CAN frame to inject. + * @param fifo The receive FIFO to inject into (0 or 1). Defaults to 0. + */ +void +simulate_rx(Frame frame, uint8_t fifo = 0); + +/** + * @brief Reset the CAN simulation state and re-register all hooks. + * Resets the CAN pointer to can1. + */ +void +reset(); + +} // namespace Embys::Stm32::Sim::CAN diff --git a/libs/stm32/sim/src/sim/core.cpp b/libs/stm32/sim/src/sim/core.cpp index 23601e3..d3145dc 100644 --- a/libs/stm32/sim/src/sim/core.cpp +++ b/libs/stm32/sim/src/sim/core.cpp @@ -38,6 +38,7 @@ GPIO_TypeDef gpiob_instance = {}; GPIO_TypeDef gpioc_instance = {}; I2C_TypeDef i2c1_instance = {}; I2C_TypeDef i2c2_instance = {}; +CAN_TypeDef can1_instance = {}; SPI_TypeDef spi1_instance = {}; SPI_TypeDef spi2_instance = {}; USART_TypeDef usart1_instance = {}; @@ -61,6 +62,7 @@ GPIO_TypeDef *gpiob = &gpiob_instance; GPIO_TypeDef *gpioc = &gpioc_instance; I2C_TypeDef *i2c1 = &i2c1_instance; I2C_TypeDef *i2c2 = &i2c2_instance; +CAN_TypeDef *can1 = &can1_instance; SPI_TypeDef *spi1 = &spi1_instance; SPI_TypeDef *spi2 = &spi2_instance; USART_TypeDef *usart1 = &usart1_instance; @@ -88,6 +90,10 @@ void (*I2C1_EV_IRQHandler_ptr)() = nullptr; void (*I2C1_ER_IRQHandler_ptr)() = nullptr; void (*I2C2_EV_IRQHandler_ptr)() = nullptr; void (*I2C2_ER_IRQHandler_ptr)() = nullptr; +void (*CAN1_TX_IRQHandler_ptr)() = nullptr; +void (*CAN1_RX0_IRQHandler_ptr)() = nullptr; +void (*CAN1_RX1_IRQHandler_ptr)() = nullptr; +void (*CAN1_SCE_IRQHandler_ptr)() = nullptr; void (*SPI1_IRQHandler_ptr)() = nullptr; void (*SPI2_IRQHandler_ptr)() = nullptr; void (*USART1_IRQHandler_ptr)() = nullptr; @@ -120,6 +126,7 @@ reset() memset(&gpioc_instance, 0, sizeof(gpioc_instance)); memset(&i2c1_instance, 0, sizeof(i2c1_instance)); memset(&i2c2_instance, 0, sizeof(i2c2_instance)); + memset(&can1_instance, 0, sizeof(can1_instance)); memset(&spi1_instance, 0, sizeof(spi1_instance)); memset(&spi2_instance, 0, sizeof(spi2_instance)); memset(&usart1_instance, 0, sizeof(usart1_instance)); @@ -144,6 +151,10 @@ reset() I2C1_ER_IRQHandler_ptr = nullptr; I2C2_EV_IRQHandler_ptr = nullptr; I2C2_ER_IRQHandler_ptr = nullptr; + CAN1_TX_IRQHandler_ptr = nullptr; + CAN1_RX0_IRQHandler_ptr = nullptr; + CAN1_RX1_IRQHandler_ptr = nullptr; + CAN1_SCE_IRQHandler_ptr = nullptr; SPI1_IRQHandler_ptr = nullptr; SPI2_IRQHandler_ptr = nullptr; USART1_IRQHandler_ptr = nullptr; diff --git a/libs/stm32/sim/src/sim/core.hpp b/libs/stm32/sim/src/sim/core.hpp index d113d91..1fbfc89 100644 --- a/libs/stm32/sim/src/sim/core.hpp +++ b/libs/stm32/sim/src/sim/core.hpp @@ -33,6 +33,7 @@ extern GPIO_TypeDef gpiob_instance; extern GPIO_TypeDef gpioc_instance; extern I2C_TypeDef i2c1_instance; extern I2C_TypeDef i2c2_instance; +extern CAN_TypeDef can1_instance; extern SPI_TypeDef spi1_instance; extern SPI_TypeDef spi2_instance; extern USART_TypeDef usart1_instance; @@ -61,6 +62,7 @@ extern GPIO_TypeDef *gpiob; extern GPIO_TypeDef *gpioc; extern I2C_TypeDef *i2c1; extern I2C_TypeDef *i2c2; +extern CAN_TypeDef *can1; extern SPI_TypeDef *spi1; extern SPI_TypeDef *spi2; extern USART_TypeDef *usart1; @@ -95,6 +97,10 @@ extern void (*I2C1_EV_IRQHandler_ptr)(); extern void (*I2C1_ER_IRQHandler_ptr)(); extern void (*I2C2_EV_IRQHandler_ptr)(); extern void (*I2C2_ER_IRQHandler_ptr)(); +extern void (*CAN1_TX_IRQHandler_ptr)(); +extern void (*CAN1_RX0_IRQHandler_ptr)(); +extern void (*CAN1_RX1_IRQHandler_ptr)(); +extern void (*CAN1_SCE_IRQHandler_ptr)(); extern void (*SPI1_IRQHandler_ptr)(); extern void (*SPI2_IRQHandler_ptr)(); extern void (*USART1_IRQHandler_ptr)(); diff --git a/libs/stm32/sim/src/sim/def.hpp b/libs/stm32/sim/src/sim/def.hpp index f2ea7a3..4d07f81 100644 --- a/libs/stm32/sim/src/sim/def.hpp +++ b/libs/stm32/sim/src/sim/def.hpp @@ -28,6 +28,7 @@ #undef GPIOC #undef I2C1 #undef I2C2 +#undef CAN1 #undef SPI1 #undef SPI2 #undef USART1 @@ -50,6 +51,7 @@ #define GPIOC (Embys::Stm32::Sim::gpioc) #define I2C1 (Embys::Stm32::Sim::i2c1) #define I2C2 (Embys::Stm32::Sim::i2c2) +#define CAN1 (Embys::Stm32::Sim::can1) #define SPI1 (Embys::Stm32::Sim::spi1) #define SPI2 (Embys::Stm32::Sim::spi2) #define USART1 (Embys::Stm32::Sim::usart1) diff --git a/libs/stm32/sim/src/sim/spi.cpp b/libs/stm32/sim/src/sim/spi.cpp new file mode 100644 index 0000000..9e99939 --- /dev/null +++ b/libs/stm32/sim/src/sim/spi.cpp @@ -0,0 +1,240 @@ +/** + * @file spi.cpp + * @author Stanislav Yaranov (stanislav.yaranov@gmail.com) + * @brief SPI simulation for the STM32 simulated environment. + * @version 0.1 + * @date 2026-05-11 + * @copyright Copyright (c) 2026 + */ + +#include "spi.hpp" + +#include + +#include "base.hpp" + +namespace Embys::Stm32::Sim::SPI +{ + +SPI_TypeDef *spi = &spi1_instance; // Default to spi1 + +std::vector> tx_buffers; + +Callable> on_tx; + +/** + * @brief Number of cycles after a DR write before TXE is set again, + * simulating the time to shift the byte out. + */ +static constexpr uint32_t TXE_DELAY = 5; + +/** + * @brief Number of cycles after a DR write before RXNE is set, + * simulating the full-duplex receive completing. + */ +static constexpr uint32_t RXNE_DELAY = 10; + +/** + * @brief Number of cycles after the last activity before BSY is cleared. + */ +static constexpr uint32_t BSY_DELAY = 15; + +/** + * @brief Condition of the SPI peripheral, tracking whether it is idle or + * actively transferring (SPE bit set). + */ +enum Condition +{ + Idle, + Transferring +}; + +static Condition condition = Idle; + +/** + * @brief Target cycle at which TXE should be set again after a write to DR. + * std::nullopt when no TXE restoration is pending. + */ +static std::optional txe_cyc; + +/** + * @brief Target cycle at which RXNE should be set after a write to DR. + * std::nullopt when no RXNE is pending. + */ +static std::optional rxne_cyc; + +/** + * @brief Target cycle at which BSY should be cleared after activity. + * std::nullopt when no BSY clear is pending. + */ +static std::optional bsy_cyc; + +/** + * @brief Queue of receive buffers to be returned during DR reads. + * Each entry corresponds to one simulated transaction response. + */ +static std::vector> rx_buffers; + +/** + * @brief Byte position within the current transaction, incremented on each + * DR write and used to index the matching byte in the rx_buffer. + */ +static uint16_t buffer_pos = 0; + +void +simulate_rx(std::vector data) +{ + rx_buffers.push_back(std::move(data)); +} + +/** + * @brief Hook called when "spi_begin" is triggered (CS asserted). + * Starts a new transaction buffer and updates SR flags. + */ +void +begin_hook(uint32_t) +{ + if (condition != Transferring) + return; + + tx_buffers.emplace_back(); + buffer_pos = 0; + + CLEAR_BIT_V(spi->SR, SPI_SR_RXNE); + SET_BIT_V(spi->SR, SPI_SR_BSY); +} + +/** + * @brief Hook called when "spi_end" is triggered (CS deasserted). + * Finalises the transaction and fires the on_tx callback. + */ +void +end_hook(uint32_t) +{ + if (condition != Transferring || tx_buffers.empty()) + return; + + if (!rx_buffers.empty()) + rx_buffers.erase(rx_buffers.begin()); + + on_tx(tx_buffers.back()); +} + +/** + * @brief Hook called when "spi_write_dr" is triggered (byte written to DR). + * Captures the transmitted byte and schedules flag updates. + */ +void +write_dr_hook(uint32_t cyc) +{ + if (condition != Transferring) + return; + + if (tx_buffers.empty()) + tx_buffers.emplace_back(); + + tx_buffers.back().push_back(static_cast(spi->DR)); + INC_V(buffer_pos); + + CLEAR_BIT_V(spi->SR, SPI_SR_TXE); + SET_BIT_V(spi->SR, SPI_SR_BSY); + + txe_cyc = cyc + TXE_DELAY; + rxne_cyc = cyc + RXNE_DELAY; + bsy_cyc = cyc + BSY_DELAY; +} + +/** + * @brief Hook called when "spi_read_dr" is triggered (DR register read). + * Provides the next simulated receive byte and updates flags. + */ +void +read_dr_hook(uint32_t cyc) +{ + if (condition != Transferring) + return; + + if (rx_buffers.empty()) + { + spi->DR = 0; + return; + } + + auto &rx_buffer = rx_buffers.front(); + auto pos = buffer_pos > 0 ? static_cast(buffer_pos - 1) : 0u; + + spi->DR = pos < rx_buffer.size() ? static_cast(rx_buffer[pos]) : 0u; + + CLEAR_BIT_V(spi->SR, SPI_SR_RXNE); + bsy_cyc = cyc + BSY_DELAY; +} + +/** + * @brief Persistent hook called every cycle. + * Detects SPE enable/disable transitions and restores delayed flags. + */ +void +peripheral_hook(uint32_t cyc) +{ + if ((spi->CR1 & SPI_CR1_SPE) && condition == Idle) + { + condition = Transferring; + SET_BIT_V(spi->SR, SPI_SR_TXE); + CLEAR_BIT_V(spi->SR, SPI_SR_RXNE | SPI_SR_BSY); + } + else if (!(spi->CR1 & SPI_CR1_SPE) && condition == Transferring) + { + condition = Idle; + CLEAR_BIT_V(spi->SR, SPI_SR_TXE | SPI_SR_RXNE | SPI_SR_BSY); + txe_cyc = std::nullopt; + rxne_cyc = std::nullopt; + bsy_cyc = std::nullopt; + } + + if (condition != Transferring) + return; + + if (txe_cyc.has_value() && cyc >= txe_cyc.value()) + { + SET_BIT_V(spi->SR, SPI_SR_TXE); + txe_cyc = std::nullopt; + } + + if (rxne_cyc.has_value() && cyc >= rxne_cyc.value()) + { + SET_BIT_V(spi->SR, SPI_SR_RXNE); + rxne_cyc = std::nullopt; + } + + if (bsy_cyc.has_value() && cyc >= bsy_cyc.value()) + { + CLEAR_BIT_V(spi->SR, SPI_SR_BSY); + bsy_cyc = std::nullopt; + } +} + +void +reset() +{ + spi = &spi1_instance; + condition = Idle; + txe_cyc = std::nullopt; + rxne_cyc = std::nullopt; + bsy_cyc = std::nullopt; + buffer_pos = 0; + rx_buffers.clear(); + tx_buffers.clear(); + + on_tx.clear(); + + spi->DR = 0; + spi->SR = SPI_SR_TXE; // Idle: transmit buffer empty + + Base::add_test_hook("spi_begin", begin_hook); + Base::add_test_hook("spi_end", end_hook); + Base::add_test_hook("spi_write_dr", write_dr_hook); + Base::add_test_hook("spi_read_dr", read_dr_hook); + Base::add_hook(peripheral_hook); +} + +} // namespace Embys::Stm32::Sim::SPI diff --git a/libs/stm32/sim/src/sim/spi.hpp b/libs/stm32/sim/src/sim/spi.hpp new file mode 100644 index 0000000..d359595 --- /dev/null +++ b/libs/stm32/sim/src/sim/spi.hpp @@ -0,0 +1,91 @@ +/** + * @file spi.hpp + * @author Stanislav Yaranov (stanislav.yaranov@gmail.com) + * @brief SPI simulation for the STM32 mock environment. + * + * Requires: + * - "spi_write_dr" test hook to be called when DR register is written to + * - "spi_read_dr" test hook to be called when DR register is read + * - "spi_begin" test hook to be called when CS is asserted (transaction start) + * - "spi_end" test hook to be called when CS is deasserted (transaction end) + * + * Example: + * If you have defined in your code: + * ```cpp + * #ifdef MOCK_STM32 + * #define TEST_HOOK(key) Embys::Stm32::Sim::Base::trigger_test_hook(key) + * #else + * #define TEST_HOOK(key) + * #endif + * ``` + * And then you signal CS assert / DR write with: + * ```cpp + * void begin() { + * cs_low(); + * TEST_HOOK("spi_begin"); + * } + * uint8_t transfer(uint8_t byte) { + * SPI1->DR = byte; + * TEST_HOOK("spi_write_dr"); + * while (!(SPI1->SR & SPI_SR_RXNE)) {} + * auto rx = SPI1->DR; + * TEST_HOOK("spi_read_dr"); + * return rx; + * } + * void end() { + * TEST_HOOK("spi_end"); + * cs_high(); + * } + * ``` + * + * @version 0.1 + * @date 2026-05-11 + * @copyright Copyright (c) 2026 + */ + +#pragma once + +#include + +#include + +#include "core.hpp" + +namespace Embys::Stm32::Sim::SPI +{ + +/** + * @brief Pointer to the SPI peripheral instance being used in the mock + * environment. + */ +extern SPI_TypeDef *spi; + +/** + * @brief Buffers for simulating data transmission. Each buffer represents a + * separate CS transaction initiated by a "spi_begin" hook. + */ +extern std::vector> tx_buffers; + +/** + * @brief Callback invoked when a SPI transaction ends ("spi_end" hook fires). + * Receives the bytes that were transmitted during the transaction. + */ +extern Callable> on_tx; + +/** + * @brief Simulate receiving data on the SPI bus. + * Bytes in @p data will be returned (one per DR read, indexed by the current + * byte position within the transaction) during the next transaction. + * @param data The data to be received. + */ +void +simulate_rx(std::vector data); + +/** + * @brief Reset the SPI simulation state, including the runtime state and + * resetting the SPI pointer to the default instance (spi1). + */ +void +reset(); + +} // namespace Embys::Stm32::Sim::SPI