diff --git a/src/device/stm32/stm32g0_compat.go b/src/device/stm32/stm32g0_compat.go new file mode 100644 index 0000000000..400da9e8dc --- /dev/null +++ b/src/device/stm32/stm32g0_compat.go @@ -0,0 +1,94 @@ +//go:build stm32g0 + +package stm32 + +// Compatibility constants for STM32G0 that are named differently +// in the auto-generated SVD-based device file. + +// GPIO OTYPER constants (open-drain/push-pull) +const ( + GPIO_OTYPER_OT0_PushPull = 0x0 + GPIO_OTYPER_OT0_OpenDrain = 0x1 +) + +// USART CR1 constants (these have FIFO_ENABLED/FIFO_DISABLED variants in G0) +// We use the FIFO_DISABLED variants as defaults for compatibility +const ( + USART_CR1_UE = USART_CR1_FIFO_DISABLED_UE + USART_CR1_TE = USART_CR1_FIFO_DISABLED_TE + USART_CR1_RE = USART_CR1_FIFO_DISABLED_RE + USART_CR1_RXNEIE = USART_CR1_FIFO_DISABLED_RXNEIE +) + +// USART ISR constants +const ( + USART_ISR_TXE = USART_ISR_FIFO_DISABLED_TXE + USART_ISR_RXNE = USART_ISR_FIFO_DISABLED_RXNE + USART_ISR_TC = USART_ISR_FIFO_DISABLED_TC +) + +// SPI CR1 BR (baud rate) divisor constants +const ( + SPI_CR1_BR_Div2 = SPI_CR1_BR_B_0x0 << SPI_CR1_BR_Pos // fPCLK/2 + SPI_CR1_BR_Div4 = SPI_CR1_BR_B_0x1 << SPI_CR1_BR_Pos // fPCLK/4 + SPI_CR1_BR_Div8 = SPI_CR1_BR_B_0x2 << SPI_CR1_BR_Pos // fPCLK/8 + SPI_CR1_BR_Div16 = SPI_CR1_BR_B_0x3 << SPI_CR1_BR_Pos // fPCLK/16 + SPI_CR1_BR_Div32 = SPI_CR1_BR_B_0x4 << SPI_CR1_BR_Pos // fPCLK/32 + SPI_CR1_BR_Div64 = SPI_CR1_BR_B_0x5 << SPI_CR1_BR_Pos // fPCLK/64 + SPI_CR1_BR_Div128 = SPI_CR1_BR_B_0x6 << SPI_CR1_BR_Pos // fPCLK/128 + SPI_CR1_BR_Div256 = SPI_CR1_BR_B_0x7 << SPI_CR1_BR_Pos // fPCLK/256 +) + +// Flash ACR latency constants (wait states) +const ( + Flash_ACR_LATENCY_WS0 = 0x0 // 0 wait states + Flash_ACR_LATENCY_WS1 = 0x1 // 1 wait state + Flash_ACR_LATENCY_WS2 = 0x2 // 2 wait states +) + +// RCC PLLCFGR PLLSRC values +const ( + RCC_PLLCFGR_PLLSRC_NONE = 0x0 // No clock + RCC_PLLCFGR_PLLSRC_HSI16 = 0x2 // HSI16 clock selected as PLL input + RCC_PLLCFGR_PLLSRC_HSE = 0x3 // HSE clock selected as PLL input +) + +// RCC CFGR SW (System clock switch) values +const ( + RCC_CFGR_SW_HSISYS = 0x0 // HSISYS selected as system clock + RCC_CFGR_SW_HSE = 0x1 // HSE selected as system clock + RCC_CFGR_SW_PLLRCLK = 0x2 // PLLRCLK selected as system clock + RCC_CFGR_SW_LSI = 0x3 // LSI selected as system clock + RCC_CFGR_SW_LSE = 0x4 // LSE selected as system clock +) + +// RCC CFGR SWS (System clock switch status) values +const ( + RCC_CFGR_SWS_HSISYS = 0x0 // HSISYS used as system clock + RCC_CFGR_SWS_HSE = 0x1 // HSE used as system clock + RCC_CFGR_SWS_PLLRCLK = 0x2 // PLLRCLK used as system clock + RCC_CFGR_SWS_LSI = 0x3 // LSI used as system clock + RCC_CFGR_SWS_LSE = 0x4 // LSE used as system clock +) + +// RCC CFGR HPRE (AHB prescaler) values +const ( + RCC_CFGR_HPRE_Div1 = 0x0 // SYSCLK not divided + RCC_CFGR_HPRE_Div2 = 0x8 // SYSCLK divided by 2 + RCC_CFGR_HPRE_Div4 = 0x9 // SYSCLK divided by 4 + RCC_CFGR_HPRE_Div8 = 0xA // SYSCLK divided by 8 + RCC_CFGR_HPRE_Div16 = 0xB // SYSCLK divided by 16 + RCC_CFGR_HPRE_Div64 = 0xC // SYSCLK divided by 64 + RCC_CFGR_HPRE_Div128 = 0xD // SYSCLK divided by 128 + RCC_CFGR_HPRE_Div256 = 0xE // SYSCLK divided by 256 + RCC_CFGR_HPRE_Div512 = 0xF // SYSCLK divided by 512 +) + +// RCC CFGR PPRE (APB prescaler) values +const ( + RCC_CFGR_PPRE_Div1 = 0x0 // HCLK not divided + RCC_CFGR_PPRE_Div2 = 0x4 // HCLK divided by 2 + RCC_CFGR_PPRE_Div4 = 0x5 // HCLK divided by 4 + RCC_CFGR_PPRE_Div8 = 0x6 // HCLK divided by 8 + RCC_CFGR_PPRE_Div16 = 0x7 // HCLK divided by 16 +) diff --git a/src/examples/fdcan/stm32g0b1-fdcan.go b/src/examples/fdcan/stm32g0b1-fdcan.go new file mode 100644 index 0000000000..68e329266b --- /dev/null +++ b/src/examples/fdcan/stm32g0b1-fdcan.go @@ -0,0 +1,1442 @@ +//go:build nucleog0b1re + +package main + +// FDCAN Test Suite for STM32 Nucleo-G0B1RE board +// Wiring diagram: https://www.st.com/resource/en/datasheet/stm32g0b1re.pdf +// Waveshare CAN-HAT: https://www.waveshare.com/wiki/CAN-HAT +// Connect CAN-HAT TX to PA12, RX to PA11, GND to GND, 3.3V to VCC +// Provides a menu-driven interface for testing all CAN features + +import ( + "machine" + "runtime/volatile" + "time" +) + +var ( + led machine.Pin + canConfig machine.FDCANConfig + canRunning bool // Track if CAN bus is currently running + + // For interrupt-driven RX + rxCount volatile.Register32 + rxReady volatile.Register32 + lastRxID uint32 + lastRxDLC uint8 + lastRxFD bool + lastRxData [64]byte // Extended to 64 bytes for FD support +) + +func main() { + led = machine.LED + led.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + // Default CAN configuration + canConfig = machine.FDCANConfig{ + TransferRate: machine.FDCANTransferRate500kbps, + TransferRateFD: machine.FDCANTransferRate2000kbps, + Mode: machine.FDCANModeNormal, + Tx: machine.CAN1_TX_PIN, + Rx: machine.CAN1_RX_PIN, + EnableFD: true, + } + + println("") + print(colorCyan, colorBold) + println("╔══════════════════════════════════════╗") + println("║ FDCAN Test Suite for Nucleo-G0B1RE ║") + println("╚══════════════════════════════════════╝") + print(colorReset) + println("") + + for { + showMainMenu() + } +} + +func showMainMenu() { + println("") + // Show bus status in title + print(colorCyan, colorBold, "═══ Main Menu ═══ ", colorReset) + print("[CAN: ") + if canRunning { + print(colorGreen, "RUNNING", colorReset) + } else { + print(colorRed, "STOPPED", colorReset) + } + println("]") + println("") + printMenuItem("1", "Configure CAN") + printMenuItem("2", "Bus Control") + printMenuItem("3", "Basic TX/RX Test") + printMenuItem("4", "Filter Test") + printMenuItem("5", "Blocking TX Test") + printMenuItem("6", "TX Cancellation Test") + printMenuItem("7", "Interrupt RX Test") + printMenuItem("8", "Error Diagnostics") + printMenuItem("9", "Passive Monitor") + printMenuItem("0", "Show Current Config") + print(colorYellow, "f", colorReset) + println(". CAN FD Test (large payloads)") + println("") + // Quick actions + print(colorWhite, "Quick: ", colorReset) + print(colorGreen, "s", colorReset) + print("=start ") + print(colorRed, "x", colorReset) + print("=stop ") + print(colorYellow, "r", colorReset) + println("=restart") + println("") + print(colorWhite, "Enter choice: ", colorReset) + + choice := readChar() + println("") + + switch choice { + case '1': + configureMenu() + case '2': + busControlMenu() + case '3': + basicTxRxTest() + case '4': + filterTest() + case '5': + blockingTxTest() + case '6': + txCancellationTest() + case '7': + interruptRxTest() + case '8': + errorDiagnosticsTest() + case '9': + passiveMonitor() + case '0': + showConfig() + case 's', 'S': + startCAN() + case 'x', 'X': + stopCAN() + case 'r', 'R': + restartCAN() + case 'f', 'F': + fdCanTest() + default: + printError("Invalid choice") + println("") + } +} + +func configureMenu() { + printTitle("═══ Configure CAN ═══") + printMenuItem("1", "Normal Mode") + printMenuItem("2", "Internal Loopback Mode") + printMenuItem("3", "External Loopback Mode") + printMenuItem("4", "Bus Monitoring Mode") + print(colorYellow, "5", colorReset) + print(". Toggle FD Mode (current: ", boolStr(canConfig.EnableFD), ")") + println("") + printMenuItem("6", "Set Baud Rate") + println("") + print(colorWhite, "Enter choice: ", colorReset) + + choice := readChar() + println("") + + switch choice { + case '1': + canConfig.Mode = machine.FDCANModeNormal + printSuccess("Mode set: ") + println("Normal") + case '2': + canConfig.Mode = machine.FDCANModeInternalLoopback + printSuccess("Mode set: ") + println("Internal Loopback") + case '3': + canConfig.Mode = machine.FDCANModeExternalLoopback + printSuccess("Mode set: ") + println("External Loopback") + case '4': + canConfig.Mode = machine.FDCANModeBusMonitoring + printSuccess("Mode set: ") + println("Bus Monitoring") + case '5': + canConfig.EnableFD = !canConfig.EnableFD + print("FD Mode: ", boolStr(canConfig.EnableFD)) + println("") + case '6': + setBaudRateMenu() + } +} + +func setBaudRateMenu() { + printTitle("═══ Set Baud Rate ═══") + printMenuItem("1", "100 kbps") + printMenuItem("2", "125 kbps") + print(colorYellow, "3", colorReset) + println(". 250 kbps") + print(colorYellow, "4", colorReset) + print(". 500 kbps ") + printSuccess("(default)") + println("") + printMenuItem("5", "1000 kbps") + println("") + print(colorWhite, "Enter choice: ", colorReset) + + choice := readChar() + println("") + + switch choice { + case '1': + canConfig.TransferRate = machine.FDCANTransferRate100kbps + case '2': + canConfig.TransferRate = machine.FDCANTransferRate125kbps + case '3': + canConfig.TransferRate = machine.FDCANTransferRate250kbps + case '4': + canConfig.TransferRate = machine.FDCANTransferRate500kbps + case '5': + canConfig.TransferRate = machine.FDCANTransferRate1000kbps + } + printSuccess("Baud rate set to: ") + printDec(uint32(canConfig.TransferRate)) + println(" bps") +} + +func showConfig() { + printTitle("═══ Current Configuration ═══") + printInfo("Mode: ") + switch canConfig.Mode { + case machine.FDCANModeNormal: + println("Normal") + case machine.FDCANModeInternalLoopback: + printWarning("Internal Loopback") + println("") + case machine.FDCANModeExternalLoopback: + printWarning("External Loopback") + println("") + case machine.FDCANModeBusMonitoring: + printWarning("Bus Monitoring") + println("") + } + printInfo("Nominal Rate: ") + printHighlight("") + printDec(uint32(canConfig.TransferRate)) + println(" bps") + printInfo("FD Data Rate: ") + printDec(uint32(canConfig.TransferRateFD)) + println(" bps") + printInfo("FD Enabled: ") + print(boolStr(canConfig.EnableFD)) + println("") +} + +func initCAN() error { + // If already running, stop first to reconfigure + if canRunning { + machine.CAN1.Stop() + canRunning = false + } + err := machine.CAN1.Configure(canConfig) + if err != nil { + return err + } + err = machine.CAN1.Start() + if err == nil { + canRunning = true + } + return err +} + +// ensureCAN starts CAN if not already running, returns error if failed +func ensureCAN() error { + if canRunning { + return nil + } + return initCAN() +} + +// startCAN starts the CAN bus with current configuration +func startCAN() { + if canRunning { + printWarning("CAN bus is already running") + println("") + return + } + + printInfo("Starting CAN bus...") + println("") + err := initCAN() + if err != nil { + printError("Failed to start: ") + println(err.Error()) + return + } + printSuccess("CAN bus started!") + println("") +} + +// stopCAN stops the CAN bus +func stopCAN() { + if !canRunning { + printWarning("CAN bus is already stopped") + println("") + return + } + + machine.CAN1.Stop() + canRunning = false + printSuccess("CAN bus stopped") + println("") +} + +// restartCAN stops and restarts the CAN bus +func restartCAN() { + printInfo("Restarting CAN bus...") + println("") + if canRunning { + machine.CAN1.Stop() + canRunning = false + } + err := initCAN() + if err != nil { + printError("Failed to restart: ") + println(err.Error()) + return + } + printSuccess("CAN bus restarted!") + println("") +} + +// busControlMenu shows the bus control submenu +func busControlMenu() { + printTitle("═══ Bus Control ═══") + print("Status: ") + if canRunning { + print(colorGreen, "RUNNING", colorReset) + } else { + print(colorRed, "STOPPED", colorReset) + } + println("") + println("") + + printMenuItem("1", "Start CAN Bus") + printMenuItem("2", "Stop CAN Bus") + printMenuItem("3", "Restart CAN Bus") + printMenuItem("4", "Live Bus Status") + printMenuItem("5", "Back to Main Menu") + println("") + print(colorWhite, "Enter choice: ", colorReset) + + choice := readChar() + println("") + + switch choice { + case '1': + startCAN() + case '2': + stopCAN() + case '3': + restartCAN() + case '4': + liveBusStatus() + case '5': + // Return to main menu + default: + printError("Invalid choice") + println("") + } +} + +// liveBusStatus shows continuous bus status updates +func liveBusStatus() { + printTitle("═══ Live Bus Status ═══") + printWarning("Press any key to return") + println("") + println("") + + for { + if hasChar() { + readChar() + break + } + + // Bus state + print("Bus: ") + if canRunning { + print(colorGreen, "RUNNING", colorReset) + + // Show error counters and state + txErr, rxErr := machine.CAN1.GetErrorCounters() + state := machine.CAN1.GetBusState() + + printInfo(" TEC=") + if txErr > 96 { + print(colorRed) + } else if txErr > 0 { + print(colorYellow) + } + printDec(uint32(txErr)) + print(colorReset) + + printInfo(" REC=") + if rxErr > 96 { + print(colorRed) + } else if rxErr > 0 { + print(colorYellow) + } + printDec(uint32(rxErr)) + print(colorReset) + + printInfo(" State=") + stateStr := state.String() + if stateStr == "ErrorActive" { + print(colorGreen, stateStr, colorReset) + } else if stateStr == "ErrorPassive" { + print(colorYellow, stateStr, colorReset) + } else { + print(colorRed, stateStr, colorReset) + } + + // Show RX activity + if !machine.CAN1.RxFifoIsEmpty() { + rxID, _, _, isFD, _, _ := machine.CAN1.Rx8() + print(colorGreen, " RX:", colorReset) + print(colorMagenta, "0x", colorReset) + printHex16(uint16(rxID)) + if isFD { + print(colorYellow, " FD", colorReset) + } + } + } else { + print(colorRed, "STOPPED", colorReset) + } + println("") + + led.Set(!led.Get()) + time.Sleep(500 * time.Millisecond) + } +} + +// passiveMonitor monitors the CAN bus without sending anything +func passiveMonitor() { + printTitle("═══ Passive Monitor ═══") + println("Watching CAN traffic (RX only)") + printWarning("Press any key to stop") + println("") + + // Start CAN if not already running + wasRunning := canRunning + if !canRunning { + printInfo("Starting CAN...") + println("") + err := initCAN() + if err != nil { + printError("Failed to start: ") + println(err.Error()) + return + } + } + println("") + + msgCount := uint32(0) + + for { + if hasChar() { + readChar() + break + } + + // Check for received messages - use Rx64 when FD enabled for full payload + for !machine.CAN1.RxFifoIsEmpty() { + var rxID uint32 + var length uint8 + var isFD bool + var rxData64 [64]byte + var rxData8 [8]byte + + if canConfig.EnableFD { + rxID, rxData64, length, isFD, _, _ = machine.CAN1.Rx64() + } else { + rxID, rxData8, length, isFD, _, _ = machine.CAN1.Rx8() + } + msgCount++ + + print(colorWhite, "[", colorReset) + printDec(msgCount) + print(colorWhite, "] ", colorReset) + // Show which Rx method is being used + if canConfig.EnableFD { + printSuccess("RX64 ") + } else { + printSuccess("RX8 ") + } + print(colorMagenta, "0x", colorReset) + printHex16(uint16(rxID)) + if isFD { + print(colorYellow, " FD", colorReset) + } + print(colorWhite, " [", colorReset) + printDec(uint32(length)) + print(colorWhite, "] ", colorReset) + + // Print data bytes - handle both array types + if canConfig.EnableFD { + if length <= 8 { + for i := uint8(0); i < length; i++ { + printHex8(rxData64[i]) + print(" ") + } + } else { + // Show first 4, then ..., then last 4 + for i := uint8(0); i < 4; i++ { + printHex8(rxData64[i]) + print(" ") + } + print(".. ") + for i := length - 4; i < length; i++ { + printHex8(rxData64[i]) + print(" ") + } + } + } else { + for i := uint8(0); i < length; i++ { + printHex8(rxData8[i]) + print(" ") + } + } + println("") + led.Set(!led.Get()) + } + + time.Sleep(1 * time.Millisecond) + } + + // If we started CAN for this monitor, stop it + if !wasRunning { + machine.CAN1.Stop() + canRunning = false + } + + println("") + printInfo("Messages received: ") + print(colorGreen) + printDec(msgCount) + print(colorReset) + println("") + printWarning("Monitor stopped") + println("") +} + +func basicTxRxTest() { + printTitle("═══ Basic TX/RX Test ═══") + printInfo("Initializing CAN...") + println("") + + err := initCAN() + if err != nil { + printError("Init failed: ") + println(err.Error()) + return + } + + printSuccess("CAN initialized!") + println("") + println("Sending test messages...") + printWarning("Press any key to stop") + println("") + println("") + + data := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88} + txCount := uint32(0) + + for { + // Check for keypress to exit + if hasChar() { + readChar() + break + } + + // Send message + txCount++ + err := machine.CAN1.Tx(0x123, data, canConfig.EnableFD, false) + if err != nil { + print(colorWhite, "[", colorReset) + printDec(txCount) + print(colorWhite, "] ", colorReset) + printError("TX Error: ") + println(err.Error()) + } else { + print(colorWhite, "[", colorReset) + printDec(txCount) + print(colorWhite, "] ", colorReset) + printInfo("TX ") + print(colorMagenta, "0x123", colorReset) + } + + // Check for received messages + time.Sleep(10 * time.Millisecond) + for !machine.CAN1.RxFifoIsEmpty() { + rxID, rxData, length, isFD, _, _ := machine.CAN1.Rx8() + printSuccess(" -> RX ") + print(colorMagenta, "0x", colorReset) + printHex16(uint16(rxID)) + if isFD { + print(colorYellow, " FD", colorReset) + } + print(colorWhite, " [", colorReset) + printDec(uint32(length)) + print(colorWhite, "]", colorReset) + if length > 0 { + print(" ") + printHex8(rxData[0]) + } + } + println("") + + led.Set(!led.Get()) + time.Sleep(500 * time.Millisecond) + } + + machine.CAN1.Stop() + printWarning("Test stopped") + println("") +} + +func filterTest() { + printTitle("═══ Filter Test ═══") + + // Use loopback for filter testing + savedMode := canConfig.Mode + canConfig.Mode = machine.FDCANModeInternalLoopback + + err := initCAN() + if err != nil { + printError("Init failed: ") + println(err.Error()) + canConfig.Mode = savedMode + return + } + + // Configure filters + printInfo("Setting up filters:") + println("") + print(" Filter 0: Accept ID ") + print(colorMagenta, "0x123", colorReset) + println("") + machine.CAN1.AcceptID(0, 0x123, false) + + print(" Filter 1: Accept range ") + print(colorMagenta, "0x200-0x2FF", colorReset) + println("") + machine.CAN1.AcceptRange(1, 0x200, 0x2FF, false) + + print(" Filter 2: Accept mask ") + print(colorMagenta, "0x300/0x7F0", colorReset) + println(" (0x300-0x30F)") + machine.CAN1.AcceptMask(2, 0x300, 0x7F0, false) + + print(" Non-matching: ") + printError("REJECT") + println("") + machine.CAN1.RejectNonMatching() + + println("") + printInfo("Testing filter combinations:") + println("") + println("") + + testIDs := []uint32{0x100, 0x123, 0x124, 0x200, 0x250, 0x2FF, 0x300, 0x305, 0x310, 0x400} + data := []byte{0xAA, 0xBB, 0xCC, 0xDD} + + for _, id := range testIDs { + err := machine.CAN1.Tx(id, data, false, false) + if err != nil { + print("TX ") + print(colorMagenta, "0x", colorReset) + printHex16(uint16(id)) + printError(" - TX error") + println("") + continue + } + + time.Sleep(2 * time.Millisecond) + + if !machine.CAN1.RxFifoIsEmpty() { + rxID, _, _, _, _, _ := machine.CAN1.Rx8() + print("TX ") + print(colorMagenta, "0x", colorReset) + printHex16(uint16(id)) + printSuccess(" -> RX ") + print(colorMagenta, "0x", colorReset) + printHex16(uint16(rxID)) + print(" ") + printSuccess("PASSED") + println("") + } else { + print("TX ") + print(colorMagenta, "0x", colorReset) + printHex16(uint16(id)) + print(" -> ") + printWarning("FILTERED") + println("") + } + } + + machine.CAN1.Stop() + canConfig.Mode = savedMode + println("") + printSuccess("Filter test complete") + println("") +} + +func blockingTxTest() { + printTitle("═══ Blocking TX Test ═══") + printInfo("Timeout: ") + println("100ms") + printWarning("Press any key to stop") + println("") + println("") + + err := initCAN() + if err != nil { + printError("Init failed: ") + println(err.Error()) + return + } + + data := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88} + txCount := uint32(0) + successCount := uint32(0) + failCount := uint32(0) + + for { + if hasChar() { + readChar() + break + } + + // Check for RX + for !machine.CAN1.RxFifoIsEmpty() { + rxID, _, _, _, _, _ := machine.CAN1.Rx8() + printSuccess("RX ") + print(colorMagenta, "0x", colorReset) + printHex16(uint16(rxID)) + println("") + } + + txCount++ + err := machine.CAN1.TxBlocking(0x123, data, canConfig.EnableFD, false, 100) + + if err == nil { + successCount++ + print(colorWhite, "[", colorReset) + printDec(txCount) + print(colorWhite, "] ", colorReset) + printSuccess("TX OK") + print(" (") + print(colorGreen) + printDec(successCount) + print(colorReset, "/", colorRed) + printDec(failCount) + print(colorReset) + println(")") + led.High() + } else { + failCount++ + print(colorWhite, "[", colorReset) + printDec(txCount) + print(colorWhite, "] ", colorReset) + printError("TX FAIL: ") + println(err.Error()) + led.Low() + } + + time.Sleep(500 * time.Millisecond) + } + + machine.CAN1.Stop() + printWarning("Test stopped") + println("") +} + +func txCancellationTest() { + printTitle("═══ TX Cancellation Test ═══") + printWarning("Press any key to stop") + println("") + println("") + + err := initCAN() + if err != nil { + printError("Init failed: ") + println(err.Error()) + return + } + + data := []byte{0x11, 0x22, 0x33, 0x44} + testNum := 0 + + for { + if hasChar() { + readChar() + break + } + + testNum++ + print(colorCyan, "--- Test ", colorReset) + printDec(uint32(testNum)) + print(colorCyan, " ---", colorReset) + println("") + + // Queue multiple messages + printInfo("Queuing 3 messages...") + println("") + machine.CAN1.Tx(0x100, data, false, false) + machine.CAN1.Tx(0x101, data, false, false) + machine.CAN1.Tx(0x102, data, false, false) + + pending := machine.CAN1.TxPendingCount() + print("Pending: ") + print(colorYellow) + printDec(uint32(pending)) + print(colorReset) + println("") + + time.Sleep(1 * time.Millisecond) + + if machine.CAN1.TxIsPending() { + printWarning("Cancelling all...") + println("") + cancelled := machine.CAN1.TxCancelAll() + print("Cancelled: ") + print(colorRed) + printDec(uint32(cancelled)) + print(colorReset) + println("") + } else { + printSuccess("All sent successfully") + println("") + } + + // Check RX + for !machine.CAN1.RxFifoIsEmpty() { + machine.CAN1.Rx8() + } + + println("") + time.Sleep(2 * time.Second) + } + + machine.CAN1.Stop() + printWarning("Test stopped") + println("") +} + +func interruptRxTest() { + printTitle("═══ Interrupt RX Test ═══") + println("Messages trigger interrupt callback") + printWarning("Press any key to stop") + println("") + println("") + + err := initCAN() + if err != nil { + printError("Init failed: ") + println(err.Error()) + return + } + + rxCount.Set(0) + rxReady.Set(0) + + // Set up minimal callback - copy full 64 bytes for FD support + machine.CAN1.SetRxCallback(func(msg *machine.FDCANRxBufferElement) { + rxCount.Set(rxCount.Get() + 1) + lastRxID = msg.ID + lastRxDLC = msg.DLC + lastRxFD = msg.FDF + // Copy full 64 bytes to support FD frames + lastRxData = msg.DB + rxReady.Set(1) + led.Set(!led.Get()) + }) + + printInfo("Waiting for messages...") + println("") + println("") + + for { + if hasChar() { + readChar() + break + } + + if rxReady.Get() != 0 { + rxReady.Set(0) + count := rxCount.Get() + print(colorWhite, "[", colorReset) + printDec(count) + print(colorWhite, "] ", colorReset) + printSuccess("RX ") + print(colorMagenta, "0x", colorReset) + printHex16(uint16(lastRxID)) + if lastRxFD { + print(colorYellow, " FD", colorReset) + } + printInfo(" DLC=") + printDec(uint32(lastRxDLC)) + printInfo(" DATA=") + length := machine.FDCANDlcToLength(lastRxDLC, lastRxFD) + for i := byte(0); i < length && i < 8; i++ { + printHex8(lastRxData[i]) + print(" ") + } + println("") + } + + time.Sleep(1 * time.Millisecond) + } + + machine.CAN1.SetRxCallback(nil) + machine.CAN1.Stop() + printWarning("Test stopped") + println("") +} + +func errorDiagnosticsTest() { + printTitle("═══ Error Diagnostics ═══") + printWarning("Press any key to stop") + println("") + println("") + + err := initCAN() + if err != nil { + printError("Init failed: ") + println(err.Error()) + return + } + + for { + if hasChar() { + readChar() + break + } + + txErr, rxErr := machine.CAN1.GetErrorCounters() + state := machine.CAN1.GetBusState() + lastErr := machine.CAN1.GetLastError() + + printInfo("TEC=") + if txErr > 0 { + print(colorYellow) + } + printDec(uint32(txErr)) + print(colorReset) + printInfo(" REC=") + if rxErr > 0 { + print(colorYellow) + } + printDec(uint32(rxErr)) + print(colorReset) + printInfo(" State=") + print(state.String()) + printInfo(" LastErr=") + println(lastErr.String()) + + if machine.CAN1.IsBusOff() { + print(colorRed, colorBold) + println(" !! BUS OFF !!") + print(colorReset) + } + if machine.CAN1.IsErrorPassive() { + print(colorRed) + println(" !! ERROR PASSIVE !!") + print(colorReset) + } + if machine.CAN1.IsErrorWarning() { + print(colorYellow) + println(" !! ERROR WARNING !!") + print(colorReset) + } + + led.Set(!led.Get()) + time.Sleep(1 * time.Second) + } + + machine.CAN1.Stop() + printWarning("Test stopped") + println("") +} + +func loopbackTest() { + printTitle("═══ Loopback Test ═══") + printInfo("Using internal loopback mode") + println("") + printWarning("Press any key to stop") + println("") + println("") + + savedMode := canConfig.Mode + canConfig.Mode = machine.FDCANModeInternalLoopback + + err := initCAN() + if err != nil { + printError("Init failed: ") + println(err.Error()) + canConfig.Mode = savedMode + return + } + + data := []byte{0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE} + txCount := uint32(0) + localRxCount := uint32(0) + + for { + if hasChar() { + readChar() + break + } + + txCount++ + data[0] = byte(txCount) + + err := machine.CAN1.Tx(0x7FF, data, canConfig.EnableFD, false) + if err != nil { + printError("TX Error: ") + println(err.Error()) + continue + } + + time.Sleep(5 * time.Millisecond) + + if !machine.CAN1.RxFifoIsEmpty() { + rxID, rxData, length, isFD, _, _ := machine.CAN1.Rx8() + localRxCount++ + + print(colorWhite, "[", colorReset) + printDec(txCount) + print(colorWhite, "] ", colorReset) + printInfo("TX->RX ") + print(colorMagenta, "0x", colorReset) + printHex16(uint16(rxID)) + if isFD { + print(colorYellow, " FD", colorReset) + } + print(colorWhite, " [", colorReset) + printDec(uint32(length)) + print(colorWhite, "] ", colorReset) + for i := uint8(0); i < length && i < 4; i++ { + printHex8(rxData[i]) + print(" ") + } + + if rxData[0] == byte(txCount) { + printSuccess("OK") + println("") + } else { + printError("MISMATCH!") + println("") + } + } else { + print(colorWhite, "[", colorReset) + printDec(txCount) + print(colorWhite, "] ", colorReset) + printError("TX - NO RX!") + println("") + } + + led.Set(!led.Get()) + time.Sleep(500 * time.Millisecond) + } + + machine.CAN1.Stop() + canConfig.Mode = savedMode + + println("") + printInfo("TX: ") + print(colorGreen) + printDec(txCount) + print(colorReset) + printInfo(" RX: ") + print(colorGreen) + printDec(localRxCount) + print(colorReset) + println("") + printWarning("Test stopped") + println("") +} + +func continuousTxTest() { + printTitle("═══ Continuous TX Test ═══") + println("Sending as fast as possible") + printWarning("Press any key to stop") + println("") + println("") + + err := initCAN() + if err != nil { + printError("Init failed: ") + println(err.Error()) + return + } + + data := []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77} + txCount := uint32(0) + errCount := uint32(0) + startTime := time.Now() + + for { + if hasChar() { + readChar() + break + } + + // Drain RX to avoid overflow + for !machine.CAN1.RxFifoIsEmpty() { + machine.CAN1.Rx8() + } + + err := machine.CAN1.Tx(0x555, data, canConfig.EnableFD, false) + if err == nil { + txCount++ + data[0]++ + } else { + errCount++ + time.Sleep(1 * time.Millisecond) + } + + // Print stats every 1000 messages + if txCount%1000 == 0 && txCount > 0 { + elapsed := time.Since(startTime).Milliseconds() + rate := uint32(0) + if elapsed > 0 { + rate = (txCount * 1000) / uint32(elapsed) + } + printInfo("TX: ") + print(colorGreen) + printDec(txCount) + print(colorReset) + printInfo(" Err: ") + if errCount > 0 { + print(colorRed) + } + printDec(errCount) + print(colorReset) + printInfo(" Rate: ") + print(colorMagenta) + printDec(rate) + print(colorReset) + println(" msg/s") + led.Set(!led.Get()) + } + } + + machine.CAN1.Stop() + println("") + printInfo("Total TX: ") + print(colorGreen) + printDec(txCount) + print(colorReset) + printInfo(" Errors: ") + if errCount > 0 { + print(colorRed) + } else { + print(colorGreen) + } + printDec(errCount) + print(colorReset) + println("") + printWarning("Test stopped") + println("") +} + +// fdCanTest tests CAN FD with large payloads (up to 64 bytes) +// Uses Rx64() for zero-allocation receive of full FD frames +func fdCanTest() { + printTitle("═══ CAN FD Test (Large Payloads) ═══") + + // Check if FD mode is enabled + if !canConfig.EnableFD { + printWarning("FD mode is disabled. Enabling it for this test...") + println("") + canConfig.EnableFD = true + } + + printInfo("Nominal Rate: ") + printDec(uint32(canConfig.TransferRate)) + println(" bps") + printInfo("FD Data Rate: ") + printDec(uint32(canConfig.TransferRateFD)) + println(" bps") + println("") + + // Use loopback for testing + savedMode := canConfig.Mode + canConfig.Mode = machine.FDCANModeInternalLoopback + + err := initCAN() + if err != nil { + printError("Init failed: ") + println(err.Error()) + canConfig.Mode = savedMode + return + } + + printSuccess("CAN FD initialized in loopback mode") + println("") + printWarning("Press any key to stop") + println("") + println("") + + // CAN FD valid DLC values: 0-8, 12, 16, 20, 24, 32, 48, 64 + testSizes := []uint8{8, 12, 16, 20, 24, 32, 48, 64} + testNum := uint32(0) + + for { + if hasChar() { + readChar() + break + } + + for _, size := range testSizes { + if hasChar() { + break + } + + testNum++ + + // Create test data pattern + var txData [64]byte + for i := uint8(0); i < size; i++ { + txData[i] = byte(testNum + uint32(i)) + } + + // Send FD frame with specified size + err := machine.CAN1.Tx(0x1FD, txData[:size], true, false) + if err != nil { + print(colorWhite, "[", colorReset) + printDec(testNum) + print(colorWhite, "] ", colorReset) + printError("TX Error (") + printDec(uint32(size)) + print("B): ") + println(err.Error()) + continue + } + + time.Sleep(5 * time.Millisecond) + + // Receive using Rx64 for full FD support + if !machine.CAN1.RxFifoIsEmpty() { + rxID, rxData, length, isFD, _, rxErr := machine.CAN1.Rx64() + if rxErr != nil { + printError("RX Error: ") + println(rxErr.Error()) + continue + } + + print(colorWhite, "[", colorReset) + printDec(testNum) + print(colorWhite, "] ", colorReset) + + // Verify + if !isFD { + printError("NOT FD! ") + } + if length != size { + printError("LEN MISMATCH! ") + } + + // Check data integrity + match := true + for i := uint8(0); i < length; i++ { + if rxData[i] != txData[i] { + match = false + break + } + } + + print(colorMagenta, "0x", colorReset) + printHex16(uint16(rxID)) + print(colorYellow, " FD", colorReset) + print(colorWhite, " [", colorReset) + printDec(uint32(length)) + print(colorWhite, "B] ", colorReset) + + // Show first and last few bytes for large payloads + if length <= 8 { + for i := uint8(0); i < length; i++ { + printHex8(rxData[i]) + print(" ") + } + } else { + // Show first 4 bytes + for i := uint8(0); i < 4; i++ { + printHex8(rxData[i]) + print(" ") + } + print(".. ") + // Show last 4 bytes + for i := length - 4; i < length; i++ { + printHex8(rxData[i]) + print(" ") + } + } + + if match { + printSuccess("OK") + } else { + printError("DATA MISMATCH!") + } + println("") + } else { + print(colorWhite, "[", colorReset) + printDec(testNum) + print(colorWhite, "] ", colorReset) + printError("TX ") + printDec(uint32(size)) + print("B - NO RX!") + println("") + } + + led.Set(!led.Get()) + time.Sleep(300 * time.Millisecond) + } + + println("") + printInfo("Cycle complete. Repeating...") + println("") + println("") + time.Sleep(1 * time.Second) + } + + machine.CAN1.Stop() + canConfig.Mode = savedMode + + println("") + printInfo("Tests run: ") + print(colorGreen) + printDec(testNum) + print(colorReset) + println("") + printWarning("FD test stopped") + println("") +} + +// ANSI Color codes +const ( + colorReset = "\x1b[0m" + colorBold = "\x1b[1m" + colorRed = "\x1b[31m" + colorGreen = "\x1b[32m" + colorYellow = "\x1b[33m" + colorBlue = "\x1b[34m" + colorMagenta = "\x1b[35m" + colorCyan = "\x1b[36m" + colorWhite = "\x1b[37m" +) + +// Color helper functions +func printColor(color, text string) { + print(color, text, colorReset) +} + +func printlnColor(color, text string) { + println(color, text, colorReset) +} + +func printSuccess(text string) { + print(colorGreen, text, colorReset) +} + +func printError(text string) { + print(colorRed, text, colorReset) +} + +func printWarning(text string) { + print(colorYellow, text, colorReset) +} + +func printInfo(text string) { + print(colorCyan, text, colorReset) +} + +func printHighlight(text string) { + print(colorMagenta, colorBold, text, colorReset) +} + +func printTitle(text string) { + println(colorCyan, colorBold, text, colorReset) +} + +func printMenuItem(num string, text string) { + print(colorYellow, num, colorReset) + print(". ") + println(text) +} + +// Helper functions + +func boolStr(b bool) string { + if b { + return colorGreen + "ON" + colorReset + } + return colorRed + "OFF" + colorReset +} + +func readChar() byte { + for { + if machine.Serial.Buffered() > 0 { + c, _ := machine.Serial.ReadByte() + return c + } + time.Sleep(10 * time.Millisecond) + } +} + +func hasChar() bool { + return machine.Serial.Buffered() > 0 +} + +func printDec(v uint32) { + if v == 0 { + print("0") + return + } + var buf [10]byte + i := 0 + for v > 0 { + buf[i] = byte('0' + v%10) + v /= 10 + i++ + } + for i > 0 { + i-- + print(string(buf[i])) + } +} + +func printHex8(v uint8) { + digits := "0123456789ABCDEF" + print(string(digits[(v>>4)&0xF])) + print(string(digits[v&0xF])) +} + +func printHex16(v uint16) { + digits := "0123456789ABCDEF" + print(string(digits[(v>>12)&0xF])) + print(string(digits[(v>>8)&0xF])) + print(string(digits[(v>>4)&0xF])) + print(string(digits[v&0xF])) +} + +func blinkError(led machine.Pin) { + for { + led.High() + time.Sleep(100 * time.Millisecond) + led.Low() + time.Sleep(100 * time.Millisecond) + } +} diff --git a/src/machine/board_nucleog0b1re.go b/src/machine/board_nucleog0b1re.go index 8c048fd0fc..5849ed997a 100644 --- a/src/machine/board_nucleog0b1re.go +++ b/src/machine/board_nucleog0b1re.go @@ -71,10 +71,10 @@ const ( SPI0_SDO_PIN = SPI1_SDO_PIN // CAN pins (directly accessible on Nucleo-G0B1RE board) - // FDCAN1: PA11 (TX) / PA12 (RX) using AF9 + // FDCAN1: PA12 (TX) / PA11 (RX) using AF3 // FDCAN2: PD12 (TX) / PD13 (RX) using AF3 - CAN1_TX_PIN = PA11 - CAN1_RX_PIN = PA12 + CAN1_TX_PIN = PA12 + CAN1_RX_PIN = PA11 CAN2_TX_PIN = PD12 CAN2_RX_PIN = PD13 ) @@ -105,12 +105,12 @@ var ( } SPI0 = SPI1 - // FDCAN1 on PA11 (TX) / PA12 (RX) + // FDCAN1 on PA12 (TX) / PA11 (RX) CAN1 = &_CAN1 _CAN1 = FDCAN{ Bus: stm32.FDCAN1, - TxAltFuncSelect: AF9_FDCAN1_FDCAN2, - RxAltFuncSelect: AF9_FDCAN1_FDCAN2, + TxAltFuncSelect: AF3_FDCAN1_FDCAN2, + RxAltFuncSelect: AF3_FDCAN1_FDCAN2, instance: 0, } @@ -126,6 +126,16 @@ var ( func init() { UART1.Interrupt = interrupt.New(stm32.IRQ_USART2_LPUART2, _UART1.handleInterrupt) - // Note: FDCAN interrupts share with USB (IRQ_UCPD1_UCPD2_USB = 8) - // User should configure interrupts via SetInterrupt method if needed + + // FDCAN interrupts - TIM16 is shared with FDCAN IT0, TIM17 with FDCAN IT1 + CAN1.Interrupt = interrupt.New(stm32.IRQ_TIM16, fdcan1HandleInterrupt) + CAN2.Interrupt = interrupt.New(stm32.IRQ_TIM16, fdcan1HandleInterrupt) // Both share IT0 +} + +func fdcan1HandleInterrupt(interrupt.Interrupt) { + fdcanHandleInterrupt(0) +} + +func fdcan2HandleInterrupt(interrupt.Interrupt) { + fdcanHandleInterrupt(1) } diff --git a/src/machine/machine_stm32g0_can.go b/src/machine/machine_stm32g0_can.go index 01bf523df8..1a95532c63 100644 --- a/src/machine/machine_stm32g0_can.go +++ b/src/machine/machine_stm32g0_can.go @@ -87,6 +87,7 @@ type FDCAN struct { type FDCANTransferRate uint32 const ( + FDCANTransferRate100kbps FDCANTransferRate = 100000 FDCANTransferRate125kbps FDCANTransferRate = 125000 FDCANTransferRate250kbps FDCANTransferRate = 250000 FDCANTransferRate500kbps FDCANTransferRate = 500000 @@ -112,7 +113,8 @@ type FDCANConfig struct { Mode FDCANMode Tx Pin Rx Pin - Standby Pin // Optional standby pin for CAN transceiver (set to NoPin if not used) + Standby Pin // Optional standby pin for CAN transceiver (set to NoPin if not used) + EnableFD bool // Enable FD mode for larger payloads (up to 64 bytes) and higher data rates } // FDCANTxBufferElement represents a transmit buffer element @@ -154,6 +156,29 @@ type FDCANFilterConfig struct { IsExtendedID bool // true for 29-bit ID, false for 11-bit } +// FDCANBusState represents the current CAN bus state +type FDCANBusState uint8 + +const ( + FDCANBusStateErrorActive FDCANBusState = 0 // Normal operation, TEC/REC < 128 + FDCANBusStateErrorPassive FDCANBusState = 1 // TEC or REC >= 128 + FDCANBusStateBusOff FDCANBusState = 2 // TEC >= 256, node disconnected from bus +) + +// FDCANLastError represents the last error that occurred on the bus +type FDCANLastError uint8 + +const ( + FDCANErrorNone FDCANLastError = 0 // No error + FDCANErrorStuff FDCANLastError = 1 // Stuff error - more than 5 equal bits + FDCANErrorForm FDCANLastError = 2 // Form error - fixed format part violated + FDCANErrorAck FDCANLastError = 3 // Ack error - no acknowledgement received + FDCANErrorBit1 FDCANLastError = 4 // Bit1 error - sent recessive, monitored dominant + FDCANErrorBit0 FDCANLastError = 5 // Bit0 error - sent dominant, monitored recessive + FDCANErrorCRC FDCANLastError = 6 // CRC error - CRC mismatch + FDCANErrorNoChange FDCANLastError = 7 // No change since last read +) + var ( errFDCANInvalidTransferRate = errors.New("FDCAN: invalid TransferRate") errFDCANInvalidTransferRateFD = errors.New("FDCAN: invalid TransferRateFD") @@ -161,6 +186,8 @@ var ( errFDCANTxFifoFull = errors.New("FDCAN: Tx FIFO full") errFDCANRxFifoEmpty = errors.New("FDCAN: Rx FIFO empty") errFDCANNotStarted = errors.New("FDCAN: not started") + errFDCANTxCancelled = errors.New("FDCAN: Tx cancelled") + errFDCANBusOff = errors.New("FDCAN: bus off") ) // DLC to bytes lookup table @@ -170,7 +197,7 @@ var dlcToBytes = [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64} func (can *FDCAN) Configure(config FDCANConfig) error { // Configure standby pin if specified (for CAN transceivers with standby control) // Setting it low enables the transceiver - if config.Standby != NoPin { + if config.Standby != NoPin && config.Standby != 0 { config.Standby.Configure(PinConfig{Mode: PinOutput}) config.Standby.Low() } @@ -178,9 +205,10 @@ func (can *FDCAN) Configure(config FDCANConfig) error { // Enable FDCAN clock enableFDCANClock() - // Configure TX and RX pins - config.Tx.ConfigureAltFunc(PinConfig{Mode: PinOutput}, can.TxAltFuncSelect) - config.Rx.ConfigureAltFunc(PinConfig{Mode: PinInputFloating}, can.RxAltFuncSelect) + // Configure TX and RX pins in Alternate Function Push-Pull mode (matches HAL GPIO_MODE_AF_PP) + // Use PinModePWMOutput which sets MODER to alternate function mode and calls SetAltFunc + config.Tx.ConfigureAltFunc(PinConfig{Mode: PinModePWMOutput}, can.TxAltFuncSelect) + config.Rx.ConfigureAltFunc(PinConfig{Mode: PinModePWMOutput}, can.RxAltFuncSelect) // Exit from sleep mode can.Bus.SetCCCR_CSR(0) @@ -215,18 +243,26 @@ func (can *FDCAN) Configure(config FDCANConfig) error { //can.Bus.CKDIV.Set(0) // No division } - // Enable automatic retransmission - can.Bus.SetCCCR_DAR(0) + // Disable automatic retransmission (matches HAL behavior) + // DAR=1 means retransmission is disabled + can.Bus.SetCCCR_DAR(1) // Disable transmit pause can.Bus.SetCCCR_TXP(0) - // Enable protocol exception handling - can.Bus.SetCCCR_PXHD(0) + // Disable protocol exception handling (matches HAL PXHD=DISABLE) + can.Bus.SetCCCR_PXHD(1) - // Enable FD mode with bit rate switching - can.Bus.SetCCCR_FDOE(1) - can.Bus.SetCCCR_BRSE(1) + // Configure FD mode + if config.EnableFD { + // Enable FD mode for larger payloads (up to 64 bytes) and bit rate switching + can.Bus.SetCCCR_FDOE(1) // Enable FD operation + can.Bus.SetCCCR_BRSE(1) // Enable bit rate switching for data phase + } else { + // Classic CAN frame format (matches HAL FDCAN_FRAME_CLASSIC) + can.Bus.SetCCCR_FDOE(0) + can.Bus.SetCCCR_BRSE(0) + } // Configure operating mode can.Bus.SetCCCR_TEST(0) @@ -331,6 +367,81 @@ func (can *FDCAN) TxFifoFreeLevel() int { return int(can.Bus.TXFQS.Get() & 0x07) // TFFL[2:0] } +// TxPendingCount returns the number of messages pending transmission +func (can *FDCAN) TxPendingCount() int { + pending := can.Bus.TXBRP.Get() & 0x07 // TXBRP[2:0] for 3 TX buffers + count := 0 + for pending != 0 { + count += int(pending & 1) + pending >>= 1 + } + return count +} + +// TxIsPending returns true if there are any pending transmissions +func (can *FDCAN) TxIsPending() bool { + return (can.Bus.TXBRP.Get() & 0x07) != 0 +} + +// TxCancelAll cancels all pending transmissions. +// Returns the number of transmissions that were cancelled. +func (can *FDCAN) TxCancelAll() int { + pending := can.Bus.TXBRP.Get() & 0x07 + if pending == 0 { + return 0 + } + + // Request cancellation for all pending buffers + can.Bus.TXBCR.Set(pending) + + // Wait for cancellation to complete (with timeout) + timeout := 10000 + for can.Bus.TXBCF.Get()&pending != pending { + timeout-- + if timeout == 0 { + break + } + } + + // Count how many were cancelled + cancelled := can.Bus.TXBCF.Get() & 0x07 + count := 0 + for cancelled != 0 { + count += int(cancelled & 1) + cancelled >>= 1 + } + return count +} + +// TxCancel cancels a specific TX buffer (0-2). +// Returns true if the buffer was pending and cancellation was requested. +func (can *FDCAN) TxCancel(bufferIndex uint8) bool { + if bufferIndex > 2 { + return false + } + + bufferMask := uint32(1) << bufferIndex + + // Check if buffer has pending transmission + if can.Bus.TXBRP.Get()&bufferMask == 0 { + return false // Nothing to cancel + } + + // Request cancellation + can.Bus.TXBCR.Set(bufferMask) + + // Wait for cancellation to complete (with timeout) + timeout := 10000 + for can.Bus.TXBCF.Get()&bufferMask == 0 { + timeout-- + if timeout == 0 { + return false + } + } + + return true +} + // RxFifoSize returns the number of messages in RX FIFO 0 func (can *FDCAN) RxFifoSize() int { return int(can.Bus.RXF0S.Get() & 0x0F) // F0FL[3:0] @@ -438,6 +549,138 @@ func (can *FDCAN) Tx(id uint32, data []byte, isFD, isExtendedID bool) error { return can.TxRaw(&e) } +// TxBlocking transmits a CAN frame and waits for transmission to complete. +// Returns error if transmission fails or times out. +// timeoutMs specifies the maximum time to wait in milliseconds (0 = wait forever). +func (can *FDCAN) TxBlocking(id uint32, data []byte, isFD, isExtendedID bool, timeoutMs uint32) error { + length := byte(len(data)) + if length > 64 { + length = 64 + } + if !isFD && length > 8 { + length = 8 + } + + e := FDCANTxBufferElement{ + ESI: false, + XTD: isExtendedID, + RTR: false, + ID: id, + MM: 0, + EFC: false, + FDF: isFD, + BRS: isFD, + DLC: FDCANLengthToDlc(length, isFD), + } + + for i := byte(0); i < length; i++ { + e.DB[i] = data[i] + } + + return can.TxRawBlocking(&e, timeoutMs) +} + +// TxRawBlocking transmits a CAN frame and waits for transmission to complete. +// Returns error if transmission fails or times out. +// timeoutMs specifies the maximum time to wait in milliseconds (0 = wait forever). +func (can *FDCAN) TxRawBlocking(e *FDCANTxBufferElement, timeoutMs uint32) error { + // Check if TX FIFO is full + if can.TxFifoIsFull() { + return errFDCANTxFifoFull + } + + // Get put index before adding to FIFO + putIndex := (can.Bus.TXFQS.Get() >> 16) & 0x03 // TFQPI[1:0] + bufferMask := uint32(1) << putIndex + + // Calculate TX buffer address + sramBase := can.getSRAMBase() + txAddress := sramBase + sramcanTFQSA + (uintptr(putIndex) * sramcanTFQSize) + + // Build first word + var w1 uint32 + id := e.ID + if !e.XTD { + id = (id & 0x7FF) << 18 + } + w1 = id & 0x1FFFFFFF + if e.ESI { + w1 |= fdcanElementMaskESI + } + if e.XTD { + w1 |= fdcanElementMaskXTD + } + if e.RTR { + w1 |= fdcanElementMaskRTR + } + + // Build second word + var w2 uint32 + w2 = uint32(e.DLC) << 16 + if e.FDF { + w2 |= fdcanElementMaskFDF + } + if e.BRS { + w2 |= fdcanElementMaskBRS + } + if e.EFC { + w2 |= fdcanElementMaskEFC + } + w2 |= uint32(e.MM) << 24 + + // Write to message RAM + *(*uint32)(unsafe.Pointer(txAddress)) = w1 + *(*uint32)(unsafe.Pointer(txAddress + 4)) = w2 + + // Copy data bytes + dataLen := dlcToBytes[e.DLC&0x0F] + numWords := (dataLen + 3) / 4 + for w := byte(0); w < numWords; w++ { + var word uint32 + baseIdx := w * 4 + for b := byte(0); b < 4 && baseIdx+b < dataLen; b++ { + word |= uint32(e.DB[baseIdx+b]) << (b * 8) + } + *(*uint32)(unsafe.Pointer(txAddress + 8 + uintptr(w)*4)) = word + } + + // Request transmission + can.Bus.TXBAR.Set(bufferMask) + + // Wait for transmission to complete + // TXBTO (TX Buffer Transmission Occurred) bit is set when transmission completes + timeout := timeoutMs * 1000 // Convert to microseconds for finer granularity + if timeoutMs == 0 { + timeout = 0xFFFFFFFF // Effectively infinite + } + + for { + // Check if transmission occurred + if can.Bus.TXBTO.Get()&bufferMask != 0 { + return nil // Success + } + + // Check if transmission was cancelled + if can.Bus.TXBCF.Get()&bufferMask != 0 { + return errFDCANTxCancelled + } + + // Check for bus-off (can't transmit) + if can.IsBusOff() { + return errFDCANBusOff + } + + timeout-- + if timeout == 0 { + return errFDCANTimeout + } + + // Small delay to avoid busy-waiting too hard + // On Cortex-M0+ at 64MHz, a simple loop iteration is ~10-20 cycles + // This gives roughly microsecond-level timing + } +} + // RxRaw receives a CAN frame into the raw buffer element structure func (can *FDCAN) RxRaw(e *FDCANRxBufferElement) error { if can.RxFifoIsEmpty() { @@ -501,6 +744,40 @@ func (can *FDCAN) Rx() (id uint32, dlc byte, data []byte, isFD, isExtendedID boo return e.ID, length, e.DB[:length], e.FDF, e.XTD, nil } +// Rx8 receives a classic CAN frame (up to 8 bytes) with no heap allocation. +// Returns the data in a fixed-size array and the actual data length. +// For FD frames with more than 8 bytes, only the first 8 bytes are returned. +func (can *FDCAN) Rx8() (id uint32, data [8]byte, length uint8, isFD, isExtendedID bool, err error) { + var e FDCANRxBufferElement + err = can.RxRaw(&e) + if err != nil { + return 0, [8]byte{}, 0, false, false, err + } + + length = FDCANDlcToLength(e.DLC, e.FDF) + if length > 8 { + length = 8 + } + + var data8 [8]byte + copy(data8[:], e.DB[:length]) + return e.ID, data8, length, e.FDF, e.XTD, nil +} + +// Rx64 receives a CAN FD frame (up to 64 bytes) with no heap allocation. +// Returns the full 64-byte data buffer and the actual data length. +// Works for both classic CAN (up to 8 bytes) and CAN FD (up to 64 bytes). +func (can *FDCAN) Rx64() (id uint32, data [64]byte, length uint8, isFD, isExtendedID bool, err error) { + var e FDCANRxBufferElement + err = can.RxRaw(&e) + if err != nil { + return 0, [64]byte{}, 0, false, false, err + } + + length = FDCANDlcToLength(e.DLC, e.FDF) + return e.ID, e.DB, length, e.FDF, e.XTD, nil +} + // SetInterrupt configures interrupt handling for the FDCAN peripheral func (can *FDCAN) SetInterrupt(ie uint32, callback func(*FDCAN)) error { if callback == nil { @@ -523,6 +800,97 @@ func (can *FDCAN) SetInterrupt(ie uint32, callback func(*FDCAN)) error { return nil } +// Filter type constants +const ( + FDCANFilterTypeRange = 0 // Accept messages with ID in range [ID1, ID2] + FDCANFilterTypeDual = 1 // Accept messages matching ID1 or ID2 + FDCANFilterTypeMask = 2 // Accept messages matching (ID & ID2) == ID1 + FDCANFilterTypeDisable = 3 // Filter disabled +) + +// Filter config constants (destination) +const ( + FDCANFilterConfigDisable = 0 // Filter disabled + FDCANFilterConfigFIFO0 = 1 // Store in RX FIFO 0 + FDCANFilterConfigFIFO1 = 2 // Store in RX FIFO 1 + FDCANFilterConfigReject = 3 // Reject matching messages +) + +// AcceptAll configures the FDCAN to accept all messages (no filtering). +// All standard and extended ID messages will be accepted into RX FIFO 0. +func (can *FDCAN) AcceptAll() error { + // Configure RXGFC to accept all non-matching frames to FIFO0 + // ANFS = 0 (accept to FIFO0), ANFE = 0 (accept to FIFO0) + // LSS = 0 (no standard filters), LSE = 0 (no extended filters) + can.Bus.RXGFC.Set(0) + return nil +} + +// RejectNonMatching configures the FDCAN to reject all frames that don't match +// any configured filter. Call this after setting up your filters. +func (can *FDCAN) RejectNonMatching() { + // RXGFC register bits: + // Bits 27:24 - LSE[3:0] - List Size Extended + // Bits 20:16 - LSS[4:0] - List Size Standard + // Bits 5:4 - ANFS[1:0] - Accept Non-matching Frames Standard (2 = reject) + // Bits 3:2 - ANFE[1:0] - Accept Non-matching Frames Extended (2 = reject) + rxgfc := can.Bus.RXGFC.Get() + rxgfc &^= (0x3 << 4) | (0x3 << 2) // Clear ANFS and ANFE + rxgfc |= (2 << 4) | (2 << 2) // ANFS=2, ANFE=2 (reject non-matching) + can.Bus.RXGFC.Set(rxgfc) +} + +// AcceptID configures a filter to accept only messages with the specified ID. +// Use filterIndex 0-27 for standard IDs, 0-7 for extended IDs. +func (can *FDCAN) AcceptID(filterIndex uint8, id uint32, isExtended bool) error { + return can.ConfigureFilter(FDCANFilterConfig{ + Index: filterIndex, + Type: FDCANFilterTypeDual, // Dual ID mode - match either ID1 or ID2 + Config: FDCANFilterConfigFIFO0, + ID1: id, + ID2: id, // Same ID for both = single ID match + IsExtendedID: isExtended, + }) +} + +// AcceptRange configures a filter to accept messages with IDs in the range [idLow, idHigh]. +func (can *FDCAN) AcceptRange(filterIndex uint8, idLow, idHigh uint32, isExtended bool) error { + return can.ConfigureFilter(FDCANFilterConfig{ + Index: filterIndex, + Type: FDCANFilterTypeRange, + Config: FDCANFilterConfigFIFO0, + ID1: idLow, + ID2: idHigh, + IsExtendedID: isExtended, + }) +} + +// AcceptMask configures a classic ID/mask filter. +// A message is accepted if: (receivedID & mask) == (id & mask) +// Example: AcceptMask(0, 0x100, 0x700, false) accepts IDs 0x100-0x1FF +func (can *FDCAN) AcceptMask(filterIndex uint8, id, mask uint32, isExtended bool) error { + return can.ConfigureFilter(FDCANFilterConfig{ + Index: filterIndex, + Type: FDCANFilterTypeMask, + Config: FDCANFilterConfigFIFO0, + ID1: id, + ID2: mask, + IsExtendedID: isExtended, + }) +} + +// RejectID configures a filter to reject messages with the specified ID. +func (can *FDCAN) RejectID(filterIndex uint8, id uint32, isExtended bool) error { + return can.ConfigureFilter(FDCANFilterConfig{ + Index: filterIndex, + Type: FDCANFilterTypeDual, + Config: FDCANFilterConfigReject, + ID1: id, + ID2: id, + IsExtendedID: isExtended, + }) +} + // ConfigureFilter configures a message filter func (can *FDCAN) ConfigureFilter(config FDCANFilterConfig) error { sramBase := can.getSRAMBase() @@ -577,32 +945,40 @@ func (can *FDCAN) configureMessageRAM() { *(*uint32)(unsafe.Pointer(addr)) = 0 } - // Configure filter counts (using RXGFC register) - // LSS = number of standard filters, LSE = number of extended filters - rxgfc := can.Bus.RXGFC.Get() - rxgfc &= ^uint32(0xFF000000) // Clear LSS and LSE - rxgfc |= (sramcanFLSNbr << 24) // Standard filters - rxgfc |= (sramcanFLENbr << 24) & 0xFF00 // Extended filters (shifted) + // Configure RXGFC register + // LSS[4:0] at bits 20:16 = number of standard filters (28 max) + // LSE[3:0] at bits 27:24 = number of extended filters (8 max) + // ANFS[1:0] at bits 5:4 = Accept Non-matching Frames Standard (0 = accept to RxFIFO0) + // ANFE[1:0] at bits 3:2 = Accept Non-matching Frames Extended (0 = accept to RxFIFO0) + rxgfc := uint32(sramcanFLSNbr<<16) | uint32(sramcanFLENbr<<24) // LSS=28, LSE=8 can.Bus.RXGFC.Set(rxgfc) + + // Configure TX buffer for FIFO mode (matches HAL: FDCAN_TX_FIFO_OPERATION) + // TFQM bit 24 = 0 for FIFO mode + can.Bus.TXBC.Set(0) } func (can *FDCAN) calculateNominalBitTiming(rate FDCANTransferRate) (brp, tseg1, tseg2, sjw uint32, err error) { // STM32G0 FDCAN clock = 64MHz // Target: 80% sample point // Bit time = (1 + TSEG1 + TSEG2) time quanta + // SJW = 1 to match HAL configuration switch rate { + case FDCANTransferRate100kbps: + // 64MHz / 40 = 1.6MHz, 16 tq per bit = 100kbps + return 40, 13, 2, 1, nil case FDCANTransferRate125kbps: // 64MHz / 32 = 2MHz, 16 tq per bit = 125kbps - return 32, 13, 2, 4, nil + return 32, 13, 2, 1, nil case FDCANTransferRate250kbps: // 64MHz / 16 = 4MHz, 16 tq per bit = 250kbps - return 16, 13, 2, 4, nil + return 16, 13, 2, 1, nil case FDCANTransferRate500kbps: // 64MHz / 8 = 8MHz, 16 tq per bit = 500kbps - return 8, 13, 2, 4, nil + return 8, 13, 2, 1, nil case FDCANTransferRate1000kbps: // 64MHz / 4 = 16MHz, 16 tq per bit = 1Mbps - return 4, 13, 2, 4, nil + return 4, 13, 2, 1, nil default: return 0, 0, 0, 0, errFDCANInvalidTransferRate } @@ -612,6 +988,8 @@ func (can *FDCAN) calculateDataBitTiming(rate FDCANTransferRate) (brp, tseg1, ts // STM32G0 FDCAN clock = 64MHz // For data phase, we need higher bit rates switch rate { + case FDCANTransferRate100kbps: + return 40, 13, 2, 4, nil case FDCANTransferRate125kbps: return 32, 13, 2, 4, nil case FDCANTransferRate250kbps: @@ -674,8 +1052,10 @@ func FDCANLengthToDlc(length byte, isFD bool) byte { // Interrupt handling var ( - fdcanInstances [2]*FDCAN - fdcanCallbacks [2][32]func(*FDCAN) + fdcanInstances [2]*FDCAN + fdcanCallbacks [2][32]func(*FDCAN) + fdcanRxCallbacks [2]func(*FDCANRxBufferElement) + fdcanRxBuffers [2]FDCANRxBufferElement // Pre-allocated buffers to avoid heap alloc in interrupt ) func fdcanHandleInterrupt(idx int) { @@ -687,6 +1067,17 @@ func fdcanHandleInterrupt(idx int) { ir := can.Bus.IR.Get() can.Bus.IR.Set(ir) // Clear interrupt flags + // Handle RX FIFO 0 new message interrupt + if ir&FDCAN_IT_RX_FIFO0_NEW_MESSAGE != 0 && fdcanRxCallbacks[idx] != nil { + // Read all available messages using pre-allocated buffer + for !can.RxFifoIsEmpty() { + if can.RxRaw(&fdcanRxBuffers[idx]) == nil { + fdcanRxCallbacks[idx](&fdcanRxBuffers[idx]) + } + } + } + + // Handle other registered callbacks for i := uint(0); i < 32; i++ { if ir&(1<= 256 means bus-off state. TEC or REC >= 128 means error passive state. +func (can *FDCAN) GetErrorCounters() (txErrors, rxErrors uint8) { + ecr := can.Bus.ECR.Get() + txErrors = uint8(ecr & 0xFF) // TEC[7:0] + rxErrors = uint8((ecr >> 8) & 0x7F) // REC[6:0] + return +} + +// GetBusState returns the current CAN bus state based on error counters. +func (can *FDCAN) GetBusState() FDCANBusState { + psr := can.Bus.PSR.Get() + + // Check Bus_Off status (bit 7) + if psr&(1<<7) != 0 { + return FDCANBusStateBusOff + } + + // Check Error Passive status (bit 5) + if psr&(1<<5) != 0 { + return FDCANBusStateErrorPassive + } + + return FDCANBusStateErrorActive +} + +// GetLastError returns the last error that occurred on the CAN bus. +// The error is cleared after reading. +func (can *FDCAN) GetLastError() FDCANLastError { + psr := can.Bus.PSR.Get() + lec := (psr >> 0) & 0x07 // LEC[2:0] - Last Error Code + return FDCANLastError(lec) +} + +// GetDataPhaseLastError returns the last error that occurred during data phase (FD only). +func (can *FDCAN) GetDataPhaseLastError() FDCANLastError { + psr := can.Bus.PSR.Get() + dlec := (psr >> 8) & 0x07 // DLEC[2:0] - Data Phase Last Error Code + return FDCANLastError(dlec) +} + +// IsBusOff returns true if the CAN controller is in bus-off state. +func (can *FDCAN) IsBusOff() bool { + return can.Bus.PSR.Get()&(1<<7) != 0 +} + +// IsErrorPassive returns true if the CAN controller is in error passive state. +func (can *FDCAN) IsErrorPassive() bool { + return can.Bus.PSR.Get()&(1<<5) != 0 +} + +// IsErrorWarning returns true if at least one error counter has reached the warning level (>= 96). +func (can *FDCAN) IsErrorWarning() bool { + return can.Bus.PSR.Get()&(1<<6) != 0 +} + +// GetBusActivity returns true if the CAN bus is currently active (transmitting or receiving). +func (can *FDCAN) GetBusActivity() (transmitting, receiving bool) { + psr := can.Bus.PSR.Get() + // Activity bits: RESI (bit 11), RBRS (bit 12), etc. indicate recent activity + // For actual TX/RX activity, we check the protocol status + act := (psr >> 3) & 0x03 // ACT[1:0] - Activity + // 00 = Synchronizing, 01 = Idle, 10 = Receiver, 11 = Transmitter + receiving = act == 2 + transmitting = act == 3 + return +} + +// String returns a human-readable string for the bus state. +func (s FDCANBusState) String() string { + switch s { + case FDCANBusStateErrorActive: + return "ErrorActive" + case FDCANBusStateErrorPassive: + return "ErrorPassive" + case FDCANBusStateBusOff: + return "BusOff" + default: + return "Unknown" + } +} + +// String returns a human-readable string for the error type. +func (e FDCANLastError) String() string { + switch e { + case FDCANErrorNone: + return "None" + case FDCANErrorStuff: + return "StuffError" + case FDCANErrorForm: + return "FormError" + case FDCANErrorAck: + return "AckError" + case FDCANErrorBit1: + return "Bit1Error" + case FDCANErrorBit0: + return "Bit0Error" + case FDCANErrorCRC: + return "CRCError" + case FDCANErrorNoChange: + return "NoChange" + default: + return "Unknown" + } +} + // enableFDCANClock enables the FDCAN peripheral clock func enableFDCANClock() { - // FDCAN clock is on APB1 + // Select PCLK1 as FDCAN clock source (matches HAL: RCC_FDCANCLKSOURCE_PCLK1) + // FDCANSEL[1:0] = 00 in RCC_CCIPR2 + ccipr2 := stm32.RCC.CCIPR2.Get() + ccipr2 &= ^uint32(0x3 << 8) // Clear FDCANSEL bits + stm32.RCC.CCIPR2.Set(ccipr2) + + // Enable FDCAN peripheral clock on APB1 stm32.RCC.SetAPBENR1_FDCANEN(1) }