mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-06 20:13:40 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e18433a46 | |||
| c5cb58c9b8 | |||
| ef05ba2ea1 | |||
| 25f5f76a48 | |||
| a5292ecb0f | |||
| ab087d6f27 |
@@ -135,7 +135,7 @@ var (
|
|||||||
|
|
||||||
// FDCAN1 on PD0 (RX) / PD1 (TX) with onboard transceiver
|
// FDCAN1 on PD0 (RX) / PD1 (TX) with onboard transceiver
|
||||||
CAN1 = &_CAN1
|
CAN1 = &_CAN1
|
||||||
_CAN1 = FDCAN{
|
_CAN1 = CAN{
|
||||||
Bus: stm32.FDCAN1,
|
Bus: stm32.FDCAN1,
|
||||||
TxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
TxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
||||||
RxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
RxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ var (
|
|||||||
|
|
||||||
// FDCAN1 on PA11 (TX) / PA12 (RX)
|
// FDCAN1 on PA11 (TX) / PA12 (RX)
|
||||||
CAN1 = &_CAN1
|
CAN1 = &_CAN1
|
||||||
_CAN1 = FDCAN{
|
_CAN1 = CAN{
|
||||||
Bus: stm32.FDCAN1,
|
Bus: stm32.FDCAN1,
|
||||||
TxAltFuncSelect: AF9_FDCAN1_FDCAN2,
|
TxAltFuncSelect: AF9_FDCAN1_FDCAN2,
|
||||||
RxAltFuncSelect: AF9_FDCAN1_FDCAN2,
|
RxAltFuncSelect: AF9_FDCAN1_FDCAN2,
|
||||||
@@ -116,7 +116,7 @@ var (
|
|||||||
|
|
||||||
// FDCAN2 on PD12 (TX) / PD13 (RX)
|
// FDCAN2 on PD12 (TX) / PD13 (RX)
|
||||||
CAN2 = &_CAN2
|
CAN2 = &_CAN2
|
||||||
_CAN2 = FDCAN{
|
_CAN2 = CAN{
|
||||||
Bus: stm32.FDCAN2,
|
Bus: stm32.FDCAN2,
|
||||||
TxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
TxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
||||||
RxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
RxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
//go:build stm32g0
|
||||||
|
|
||||||
|
package machine
|
||||||
|
|
||||||
|
// unexported functions here are implemented in the device file
|
||||||
|
// and added to the build tags of this file.
|
||||||
|
|
||||||
|
// These types are an alias for documentation purposes exclusively. We wish
|
||||||
|
// the interface to be used by other ecosystems besides TinyGo which is why
|
||||||
|
// we need these types to be a primitive types at the interface level.
|
||||||
|
// If these types are defined at machine or machine/can level they are not
|
||||||
|
// usable by non-TinyGo projects. This is not good news for fostering wider adoption
|
||||||
|
// of our API in "big-Go" embedded system projects like TamaGo and periph.io
|
||||||
|
type (
|
||||||
|
// CAN IDs in tinygo are represented as 30 bit integers where
|
||||||
|
// bits 1..29 store the actual ID and the 30th bit stores the IDE bit (if extended ID).
|
||||||
|
// We include the extended ID bit in the ID itself to make comparison of IDs easier for users
|
||||||
|
// since two identical IDs where one is extended and one is not are NOT equivalent IDs.
|
||||||
|
canID = uint32
|
||||||
|
// CAN flags bitmask are defined below.
|
||||||
|
canFlags = uint32
|
||||||
|
)
|
||||||
|
|
||||||
|
// CAN ID definitions.
|
||||||
|
const (
|
||||||
|
canIDStdMask canID = (1 << 11) - 1
|
||||||
|
canIDExtendedMask canID = (1 << 29) - 1
|
||||||
|
canIDExtendedBit canID = 1 << 30
|
||||||
|
)
|
||||||
|
|
||||||
|
// CAN Flag bit definitions.
|
||||||
|
const (
|
||||||
|
canFlagBRS canFlags = 1 << 0 // Bit Rate Switch active on tx/rx of frame.
|
||||||
|
canFlagFDF canFlags = 1 << 1 // Is a FD Frame.
|
||||||
|
canFlagRTR canFlags = 1 << 2 // is a retransmission request frame.
|
||||||
|
canFlagESI canFlags = 1 << 3 // Error status indicator active on tx/rx of frame.
|
||||||
|
canFlagIDE canFlags = 1 << 4 // Extended ID.
|
||||||
|
)
|
||||||
|
|
||||||
|
// TxFIFOLevel returns amount of CAN frames stored for transmission and total Tx fifo length.
|
||||||
|
func (can *CAN) TxFIFOLevel() (level int, maxlevel int) {
|
||||||
|
return can.txFIFOLevel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tx puts a CAN frame in TxFIFO for transmission. Returns error if TxFIFO is full.
|
||||||
|
func (can *CAN) Tx(id canID, flags canFlags, data []byte) error {
|
||||||
|
return can.tx(id, flags, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RxFIFOLevel returns amount of CAN frames received and stored and total Rx fifo length.
|
||||||
|
// If the hardware is interrupt driven RxFIFOLevel should return 0,0.
|
||||||
|
func (can *CAN) RxFIFOLevel() (level int, maxlevel int) {
|
||||||
|
return can.rxFIFOLevel()
|
||||||
|
}
|
||||||
|
|
||||||
|
type canRxCallback = func(data []byte, id canID, timestamp uint32, flags canFlags)
|
||||||
|
|
||||||
|
// SetRxCallback sets the receive callback. See [canFlags] for information on how bits are layed out.
|
||||||
|
func (can *CAN) SetRxCallback(cb canRxCallback) {
|
||||||
|
can.setRxCallback(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RxPoll is called periodically for poll driven drivers. If the driver is interrupt driven
|
||||||
|
// then RxPoll is a no-op and may return nil. Users may determine if a CAN is interrupt driven by
|
||||||
|
// checking if RxFIFOLevel returns 0,0.
|
||||||
|
func (can *CAN) RxPoll() error {
|
||||||
|
return can.rxPoll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DLC to bytes lookup table
|
||||||
|
var dlcToBytes = [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64}
|
||||||
|
|
||||||
|
// dlcToLength converts a DLC value to actual byte length
|
||||||
|
func dlcToLength(dlc byte) uint8 {
|
||||||
|
if dlc > 15 {
|
||||||
|
dlc = 15
|
||||||
|
}
|
||||||
|
return dlcToBytes[dlc]
|
||||||
|
}
|
||||||
|
|
||||||
|
// lengthToDLC converts a byte length to DLC value
|
||||||
|
func lengthToDLC(length uint8) (dlc byte) {
|
||||||
|
switch {
|
||||||
|
case length <= 8:
|
||||||
|
dlc = length
|
||||||
|
case length <= 12:
|
||||||
|
dlc = 9
|
||||||
|
case length <= 16:
|
||||||
|
dlc = 10
|
||||||
|
case length <= 20:
|
||||||
|
dlc = 11
|
||||||
|
case length <= 24:
|
||||||
|
dlc = 12
|
||||||
|
case length <= 32:
|
||||||
|
dlc = 13
|
||||||
|
case length <= 48:
|
||||||
|
dlc = 14
|
||||||
|
default:
|
||||||
|
dlc = 15
|
||||||
|
}
|
||||||
|
return dlc
|
||||||
|
}
|
||||||
+252
-431
@@ -9,6 +9,8 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Exported API in src/machine/can.go
|
||||||
|
|
||||||
// FDCAN Message RAM configuration
|
// FDCAN Message RAM configuration
|
||||||
// STM32G0B1 SRAMCAN base address: 0x4000B400
|
// STM32G0B1 SRAMCAN base address: 0x4000B400
|
||||||
// Each FDCAN instance has its own message RAM area
|
// Each FDCAN instance has its own message RAM area
|
||||||
@@ -74,78 +76,53 @@ const (
|
|||||||
FDCAN_IT_ERROR_PASSIVE = 0x00800000
|
FDCAN_IT_ERROR_PASSIVE = 0x00800000
|
||||||
)
|
)
|
||||||
|
|
||||||
// FDCAN represents an FDCAN peripheral
|
// CAN is a STM32G0's CAN/FDCAN peripheral.
|
||||||
type FDCAN struct {
|
type CAN struct {
|
||||||
Bus *stm32.FDCAN_Type
|
Bus *stm32.FDCAN_Type
|
||||||
TxAltFuncSelect uint8
|
TxAltFuncSelect uint8
|
||||||
RxAltFuncSelect uint8
|
RxAltFuncSelect uint8
|
||||||
Interrupt interrupt.Interrupt
|
Interrupt interrupt.Interrupt
|
||||||
instance uint8
|
instance uint8
|
||||||
|
alwaysFD bool
|
||||||
|
rxInterrupt bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// FDCANTransferRate represents CAN bus transfer rates
|
// CANTransferRate represents CAN bus transfer rates
|
||||||
type FDCANTransferRate uint32
|
type CANTransferRate uint32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FDCANTransferRate125kbps FDCANTransferRate = 125000
|
FDCANTransferRate125kbps CANTransferRate = 125000
|
||||||
FDCANTransferRate250kbps FDCANTransferRate = 250000
|
FDCANTransferRate250kbps CANTransferRate = 250000
|
||||||
FDCANTransferRate500kbps FDCANTransferRate = 500000
|
FDCANTransferRate500kbps CANTransferRate = 500000
|
||||||
FDCANTransferRate1000kbps FDCANTransferRate = 1000000
|
FDCANTransferRate1000kbps CANTransferRate = 1000000
|
||||||
FDCANTransferRate2000kbps FDCANTransferRate = 2000000 // FD only
|
FDCANTransferRate2000kbps CANTransferRate = 2000000 // FD only
|
||||||
FDCANTransferRate4000kbps FDCANTransferRate = 4000000 // FD only
|
FDCANTransferRate4000kbps CANTransferRate = 4000000 // FD only
|
||||||
)
|
)
|
||||||
|
|
||||||
// FDCANMode represents the FDCAN operating mode
|
// CANMode represents the FDCAN operating mode
|
||||||
type FDCANMode uint8
|
type CANMode uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FDCANModeNormal FDCANMode = 0
|
CANModeNormal CANMode = 0
|
||||||
FDCANModeBusMonitoring FDCANMode = 1
|
CANModeBusMonitoring CANMode = 1
|
||||||
FDCANModeInternalLoopback FDCANMode = 2
|
CANModeInternalLoopback CANMode = 2
|
||||||
FDCANModeExternalLoopback FDCANMode = 3
|
CANModeExternalLoopback CANMode = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// FDCANConfig holds FDCAN configuration parameters
|
// CANConfig holds FDCAN configuration parameters
|
||||||
type FDCANConfig struct {
|
type CANConfig struct {
|
||||||
TransferRate FDCANTransferRate // Nominal bit rate (arbitration phase)
|
TransferRate CANTransferRate // Nominal bit rate (arbitration phase)
|
||||||
TransferRateFD FDCANTransferRate // Data bit rate (data phase), must be >= TransferRate
|
TransferRateFD CANTransferRate // Data bit rate (data phase), must be >= TransferRate
|
||||||
Mode FDCANMode
|
Mode CANMode
|
||||||
Tx Pin
|
Tx Pin
|
||||||
Rx 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)
|
||||||
|
AlwaysFD bool // Always transmit as FD frames, even when data fits in classic CAN
|
||||||
|
EnableRxInterrupt bool // Enable interrupt-driven receive (messages delivered via SetRxCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FDCANTxBufferElement represents a transmit buffer element
|
// CANFilterConfig represents a message filter configuration
|
||||||
type FDCANTxBufferElement struct {
|
type CANFilterConfig struct {
|
||||||
ESI bool // Error State Indicator
|
|
||||||
XTD bool // Extended ID flag
|
|
||||||
RTR bool // Remote Transmission Request
|
|
||||||
ID uint32 // CAN identifier (11-bit or 29-bit)
|
|
||||||
MM uint8 // Message Marker
|
|
||||||
EFC bool // Event FIFO Control
|
|
||||||
FDF bool // FD Frame indicator
|
|
||||||
BRS bool // Bit Rate Switch
|
|
||||||
DLC uint8 // Data Length Code (0-15)
|
|
||||||
DB [64]byte // Data buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// FDCANRxBufferElement represents a receive buffer element
|
|
||||||
type FDCANRxBufferElement struct {
|
|
||||||
ESI bool // Error State Indicator
|
|
||||||
XTD bool // Extended ID flag
|
|
||||||
RTR bool // Remote Transmission Request
|
|
||||||
ID uint32 // CAN identifier
|
|
||||||
ANMF bool // Accepted Non-matching Frame
|
|
||||||
FIDX uint8 // Filter Index
|
|
||||||
FDF bool // FD Frame
|
|
||||||
BRS bool // Bit Rate Switch
|
|
||||||
DLC uint8 // Data Length Code
|
|
||||||
RXTS uint16 // RX Timestamp
|
|
||||||
DB [64]byte // Data buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// FDCANFilterConfig represents a filter configuration
|
|
||||||
type FDCANFilterConfig struct {
|
|
||||||
Index uint8 // Filter index (0-27 for standard, 0-7 for extended)
|
Index uint8 // Filter index (0-27 for standard, 0-7 for extended)
|
||||||
Type uint8 // 0=Range, 1=Dual, 2=Classic (ID/Mask)
|
Type uint8 // 0=Range, 1=Dual, 2=Classic (ID/Mask)
|
||||||
Config uint8 // 0=Disable, 1=FIFO0, 2=FIFO1, 3=Reject
|
Config uint8 // 0=Disable, 1=FIFO0, 2=FIFO1, 3=Reject
|
||||||
@@ -155,401 +132,350 @@ type FDCANFilterConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errFDCANInvalidTransferRate = errors.New("FDCAN: invalid TransferRate")
|
errCANInvalidTransferRate = errors.New("CAN: invalid TransferRate")
|
||||||
errFDCANInvalidTransferRateFD = errors.New("FDCAN: invalid TransferRateFD")
|
errCANInvalidTransferRateFD = errors.New("CAN: invalid TransferRateFD")
|
||||||
errFDCANTimeout = errors.New("FDCAN: timeout")
|
errCANTimeout = errors.New("CAN: timeout")
|
||||||
errFDCANTxFifoFull = errors.New("FDCAN: Tx FIFO full")
|
errCANTxFifoFull = errors.New("CAN: Tx FIFO full")
|
||||||
errFDCANRxFifoEmpty = errors.New("FDCAN: Rx FIFO empty")
|
|
||||||
errFDCANNotStarted = errors.New("FDCAN: not started")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DLC to bytes lookup table
|
// enableFDCANClock enables the FDCAN peripheral clock
|
||||||
var dlcToBytes = [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64}
|
func enableFDCANClock() {
|
||||||
|
// FDCAN clock is on APB1
|
||||||
|
stm32.RCC.SetAPBENR1_FDCANEN(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// flags implemented as described in [CAN.SetRxCallback]
|
||||||
|
var canRxCB [2]canRxCallback
|
||||||
|
|
||||||
|
// canInstances tracks CAN peripherals with interrupt-driven RX enabled.
|
||||||
|
// A non-nil entry means setRxCallback was called with a non-nil callback.
|
||||||
|
var canInstances [2]*CAN
|
||||||
|
|
||||||
|
// Configure initializes the FDCAN peripheral and starts it.
|
||||||
|
func (can *CAN) Configure(config CANConfig) error {
|
||||||
|
can.alwaysFD = config.AlwaysFD
|
||||||
|
|
||||||
// Configure initializes the FDCAN peripheral
|
|
||||||
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.Configure(PinConfig{Mode: PinOutput})
|
config.Standby.Configure(PinConfig{Mode: PinOutput})
|
||||||
config.Standby.Low()
|
config.Standby.Low()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable FDCAN clock
|
|
||||||
enableFDCANClock()
|
enableFDCANClock()
|
||||||
|
|
||||||
// Configure TX and RX pins
|
|
||||||
config.Tx.ConfigureAltFunc(PinConfig{Mode: PinOutput}, can.TxAltFuncSelect)
|
config.Tx.ConfigureAltFunc(PinConfig{Mode: PinOutput}, can.TxAltFuncSelect)
|
||||||
config.Rx.ConfigureAltFunc(PinConfig{Mode: PinInputFloating}, can.RxAltFuncSelect)
|
config.Rx.ConfigureAltFunc(PinConfig{Mode: PinInputFloating}, can.RxAltFuncSelect)
|
||||||
|
|
||||||
// Exit from sleep mode
|
// Exit sleep mode.
|
||||||
can.Bus.SetCCCR_CSR(0)
|
can.Bus.SetCCCR_CSR(0)
|
||||||
|
|
||||||
// Wait for sleep mode exit
|
|
||||||
timeout := 10000
|
timeout := 10000
|
||||||
for can.Bus.GetCCCR_CSA() != 0 {
|
for can.Bus.GetCCCR_CSA() != 0 {
|
||||||
timeout--
|
timeout--
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
return errFDCANTimeout
|
return errCANTimeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request initialization
|
// Request initialization.
|
||||||
can.Bus.SetCCCR_INIT(1)
|
can.Bus.SetCCCR_INIT(1)
|
||||||
|
|
||||||
// Wait for init mode
|
|
||||||
timeout = 10000
|
timeout = 10000
|
||||||
for can.Bus.GetCCCR_INIT() == 0 {
|
for can.Bus.GetCCCR_INIT() == 0 {
|
||||||
timeout--
|
timeout--
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
return errFDCANTimeout
|
return errCANTimeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable configuration change
|
// Enable configuration change.
|
||||||
can.Bus.SetCCCR_CCE(1)
|
can.Bus.SetCCCR_CCE(1)
|
||||||
|
|
||||||
// Configure clock divider (only for FDCAN1)
|
|
||||||
if can.Bus == stm32.FDCAN1 {
|
if can.Bus == stm32.FDCAN1 {
|
||||||
can.Bus.SetCKDIV_PDIV(0)
|
can.Bus.SetCKDIV_PDIV(0) // No clock division.
|
||||||
//can.Bus.CKDIV.Set(0) // No division
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable automatic retransmission
|
can.Bus.SetCCCR_DAR(0) // Enable auto retransmission.
|
||||||
can.Bus.SetCCCR_DAR(0)
|
can.Bus.SetCCCR_TXP(0) // Disable transmit pause.
|
||||||
|
can.Bus.SetCCCR_PXHD(0) // Enable protocol exception handling.
|
||||||
|
can.Bus.SetCCCR_FDOE(1) // FD operation.
|
||||||
|
can.Bus.SetCCCR_BRSE(1) // Bit rate switching.
|
||||||
|
|
||||||
// Disable transmit pause
|
// Reset mode bits, then apply requested mode.
|
||||||
can.Bus.SetCCCR_TXP(0)
|
|
||||||
|
|
||||||
// Enable protocol exception handling
|
|
||||||
can.Bus.SetCCCR_PXHD(0)
|
|
||||||
|
|
||||||
// Enable FD mode with bit rate switching
|
|
||||||
can.Bus.SetCCCR_FDOE(1)
|
|
||||||
can.Bus.SetCCCR_BRSE(1)
|
|
||||||
|
|
||||||
// Configure operating mode
|
|
||||||
can.Bus.SetCCCR_TEST(0)
|
can.Bus.SetCCCR_TEST(0)
|
||||||
can.Bus.SetCCCR_MON(0)
|
can.Bus.SetCCCR_MON(0)
|
||||||
can.Bus.SetCCCR_ASM(0)
|
can.Bus.SetCCCR_ASM(0)
|
||||||
can.Bus.SetTEST_LBCK(0)
|
can.Bus.SetTEST_LBCK(0)
|
||||||
|
|
||||||
switch config.Mode {
|
switch config.Mode {
|
||||||
case FDCANModeBusMonitoring:
|
case CANModeBusMonitoring:
|
||||||
can.Bus.SetCCCR_MON(1)
|
can.Bus.SetCCCR_MON(1)
|
||||||
case FDCANModeInternalLoopback:
|
case CANModeInternalLoopback:
|
||||||
can.Bus.SetCCCR_TEST(1)
|
can.Bus.SetCCCR_TEST(1)
|
||||||
can.Bus.SetCCCR_MON(1)
|
can.Bus.SetCCCR_MON(1)
|
||||||
can.Bus.SetTEST_LBCK(1)
|
can.Bus.SetTEST_LBCK(1)
|
||||||
case FDCANModeExternalLoopback:
|
case CANModeExternalLoopback:
|
||||||
can.Bus.SetCCCR_TEST(1)
|
can.Bus.SetCCCR_TEST(1)
|
||||||
can.Bus.SetTEST_LBCK(1)
|
can.Bus.SetTEST_LBCK(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set nominal bit timing
|
// Nominal bit timing (64 MHz FDCAN clock, 16 tq/bit, ~80% sample point).
|
||||||
// STM32G0 runs at 64MHz, FDCAN clock = PCLK = 64MHz
|
|
||||||
// Bit time = (1 + NTSEG1 + NTSEG2) * tq
|
|
||||||
// tq = (NBRP + 1) / fCAN_CLK
|
|
||||||
if config.TransferRate == 0 {
|
if config.TransferRate == 0 {
|
||||||
config.TransferRate = FDCANTransferRate500kbps
|
config.TransferRate = FDCANTransferRate500kbps
|
||||||
}
|
}
|
||||||
|
nbrp, ntseg1, ntseg2, nsjw, err := fdcanNominalBitTiming(config.TransferRate)
|
||||||
nbrp, ntseg1, ntseg2, nsjw, err := can.calculateNominalBitTiming(config.TransferRate)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
can.Bus.NBTP.Set(((nsjw - 1) << 25) | ((nbrp - 1) << 16) | ((ntseg1 - 1) << 8) | (ntseg2 - 1))
|
can.Bus.NBTP.Set(((nsjw - 1) << 25) | ((nbrp - 1) << 16) | ((ntseg1 - 1) << 8) | (ntseg2 - 1))
|
||||||
|
|
||||||
// Set data bit timing (for FD mode)
|
// Data bit timing (FD phase).
|
||||||
if config.TransferRateFD == 0 {
|
if config.TransferRateFD == 0 {
|
||||||
config.TransferRateFD = FDCANTransferRate1000kbps
|
config.TransferRateFD = FDCANTransferRate1000kbps
|
||||||
}
|
}
|
||||||
if config.TransferRateFD < config.TransferRate {
|
if config.TransferRateFD < config.TransferRate {
|
||||||
return errFDCANInvalidTransferRateFD
|
return errCANInvalidTransferRateFD
|
||||||
}
|
}
|
||||||
|
dbrp, dtseg1, dtseg2, dsjw, err := fdcanDataBitTiming(config.TransferRateFD)
|
||||||
dbrp, dtseg1, dtseg2, dsjw, err := can.calculateDataBitTiming(config.TransferRateFD)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
can.Bus.DBTP.Set(((dbrp - 1) << 16) | ((dtseg1 - 1) << 8) | ((dtseg2 - 1) << 4) | (dsjw - 1))
|
can.Bus.DBTP.Set(((dbrp - 1) << 16) | ((dtseg1 - 1) << 8) | ((dtseg2 - 1) << 4) | (dsjw - 1))
|
||||||
|
|
||||||
// Configure message RAM
|
// Enable timestamp counter (internal, prescaler=1).
|
||||||
can.configureMessageRAM()
|
can.Bus.TSCC.Set(1)
|
||||||
|
|
||||||
return nil
|
// Clear message RAM.
|
||||||
}
|
base := can.sramBase()
|
||||||
|
for addr := base; addr < base+sramcanSize; addr += 4 {
|
||||||
|
*(*uint32)(unsafe.Pointer(addr)) = 0
|
||||||
|
}
|
||||||
|
|
||||||
// Start enables the FDCAN peripheral for communication
|
// Set filter list sizes: LSS[20:16], LSE[27:24].
|
||||||
func (can *FDCAN) Start() error {
|
rxgfc := can.Bus.RXGFC.Get()
|
||||||
// Disable configuration change
|
rxgfc &= ^uint32(0x0F1F0000)
|
||||||
|
rxgfc |= uint32(sramcanFLSNbr) << 16
|
||||||
|
rxgfc |= uint32(sramcanFLENbr) << 24
|
||||||
|
can.Bus.RXGFC.Set(rxgfc)
|
||||||
|
|
||||||
|
// Start peripheral.
|
||||||
can.Bus.SetCCCR_CCE(0)
|
can.Bus.SetCCCR_CCE(0)
|
||||||
|
|
||||||
// Exit initialization mode
|
|
||||||
can.Bus.SetCCCR_INIT(0)
|
can.Bus.SetCCCR_INIT(0)
|
||||||
|
timeout = 10000
|
||||||
// Wait for normal operation
|
|
||||||
timeout := 10000
|
|
||||||
|
|
||||||
for can.Bus.GetCCCR_INIT() != 0 {
|
for can.Bus.GetCCCR_INIT() != 0 {
|
||||||
timeout--
|
timeout--
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
return errFDCANTimeout
|
return errCANTimeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop disables the FDCAN peripheral
|
// Stop puts the FDCAN peripheral back into initialization mode.
|
||||||
func (can *FDCAN) Stop() error {
|
func (can *CAN) Stop() error {
|
||||||
// Request initialization
|
|
||||||
can.Bus.SetCCCR_INIT(1)
|
can.Bus.SetCCCR_INIT(1)
|
||||||
|
|
||||||
// Wait for init mode
|
|
||||||
timeout := 10000
|
timeout := 10000
|
||||||
for can.Bus.GetCCCR_INIT() == 0 {
|
for can.Bus.GetCCCR_INIT() == 0 {
|
||||||
timeout--
|
timeout--
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
return errFDCANTimeout
|
return errCANTimeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable configuration change
|
|
||||||
can.Bus.SetCCCR_CCE(1)
|
can.Bus.SetCCCR_CCE(1)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TxFifoIsFull returns true if the TX FIFO is full
|
// txFIFOLevel implements [CAN.TxFIFOLevel].
|
||||||
func (can *FDCAN) TxFifoIsFull() bool {
|
func (can *CAN) txFIFOLevel() (int, int) {
|
||||||
return (can.Bus.TXFQS.Get() & 0x00200000) != 0 // TFQF bit
|
free := int(can.Bus.TXFQS.Get() & 0x07) // TFFL[2:0]
|
||||||
|
return sramcanTFQNbr - free, sramcanTFQNbr
|
||||||
}
|
}
|
||||||
|
|
||||||
// TxFifoFreeLevel returns the number of free TX FIFO elements
|
// tx implements [CAN.Tx].
|
||||||
func (can *FDCAN) TxFifoFreeLevel() int {
|
func (can *CAN) tx(id canID, flags canFlags, data []byte) error {
|
||||||
return int(can.Bus.TXFQS.Get() & 0x07) // TFFL[2:0]
|
if can.Bus.TXFQS.Get()&0x00200000 != 0 { // TFQF bit
|
||||||
}
|
return errCANTxFifoFull
|
||||||
|
|
||||||
// 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]
|
|
||||||
}
|
|
||||||
|
|
||||||
// RxFifoIsEmpty returns true if RX FIFO 0 is empty
|
|
||||||
func (can *FDCAN) RxFifoIsEmpty() bool {
|
|
||||||
return (can.Bus.RXF0S.Get() & 0x0F) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// TxRaw transmits a CAN frame using the raw buffer element structure
|
|
||||||
func (can *FDCAN) TxRaw(e *FDCANTxBufferElement) error {
|
|
||||||
// Check if TX FIFO is full
|
|
||||||
if can.TxFifoIsFull() {
|
|
||||||
return errFDCANTxFifoFull
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get put index
|
|
||||||
putIndex := (can.Bus.TXFQS.Get() >> 16) & 0x03 // TFQPI[1:0]
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// Standard ID - shift to bits [28:18]
|
|
||||||
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 - must use 32-bit word access on Cortex-M0+
|
|
||||||
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(1 << putIndex)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tx transmits a CAN frame with the specified ID and data
|
|
||||||
func (can *FDCAN) Tx(id uint32, data []byte, isFD, isExtendedID bool) error {
|
|
||||||
length := byte(len(data))
|
length := byte(len(data))
|
||||||
if length > 64 {
|
if length > 64 {
|
||||||
length = 64
|
length = 64
|
||||||
}
|
}
|
||||||
if !isFD && length > 8 {
|
|
||||||
length = 8
|
|
||||||
}
|
|
||||||
|
|
||||||
e := FDCANTxBufferElement{
|
// Use FD framing if configured to always use FD, or if data exceeds classic CAN max.
|
||||||
ESI: false,
|
isFD := flags&canFlagFDF != 0 || length > 8
|
||||||
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++ {
|
putIndex := (can.Bus.TXFQS.Get() >> 16) & 0x03 // TFQPI[1:0]
|
||||||
e.DB[i] = data[i]
|
txAddr := can.sramBase() + sramcanTFQSA + uintptr(putIndex)*sramcanTFQSize
|
||||||
}
|
|
||||||
|
|
||||||
return can.TxRaw(&e)
|
// Header word 1: identifier and flags.
|
||||||
}
|
var w1 uint32
|
||||||
|
if flags&canFlagESI != 0 {
|
||||||
// RxRaw receives a CAN frame into the raw buffer element structure
|
w1 = (id & 0x1FFFFFFF) | fdcanElementMaskXTD
|
||||||
func (can *FDCAN) RxRaw(e *FDCANRxBufferElement) error {
|
|
||||||
if can.RxFifoIsEmpty() {
|
|
||||||
return errFDCANRxFifoEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get get index
|
|
||||||
getIndex := (can.Bus.RXF0S.Get() >> 8) & 0x03 // F0GI[1:0]
|
|
||||||
|
|
||||||
// Calculate RX buffer address
|
|
||||||
sramBase := can.getSRAMBase()
|
|
||||||
rxAddress := sramBase + sramcanRF0SA + (uintptr(getIndex) * sramcanRF0Size)
|
|
||||||
|
|
||||||
// Read first word
|
|
||||||
w1 := *(*uint32)(unsafe.Pointer(rxAddress))
|
|
||||||
e.ESI = (w1 & fdcanElementMaskESI) != 0
|
|
||||||
e.XTD = (w1 & fdcanElementMaskXTD) != 0
|
|
||||||
e.RTR = (w1 & fdcanElementMaskRTR) != 0
|
|
||||||
|
|
||||||
if e.XTD {
|
|
||||||
e.ID = w1 & fdcanElementMaskEXTID
|
|
||||||
} else {
|
} else {
|
||||||
e.ID = (w1 & fdcanElementMaskSTDID) >> 18
|
w1 = (id & 0x7FF) << 18
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read second word
|
// Header word 2: DLC, FD/BRS flags.
|
||||||
w2 := *(*uint32)(unsafe.Pointer(rxAddress + 4))
|
dlc := lengthToDLC(length)
|
||||||
e.RXTS = uint16(w2 & fdcanElementMaskTS)
|
w2 := uint32(dlc) << 16
|
||||||
e.DLC = uint8((w2 & fdcanElementMaskDLC) >> 16)
|
if isFD {
|
||||||
e.BRS = (w2 & fdcanElementMaskBRS) != 0
|
w2 |= fdcanElementMaskFDF | fdcanElementMaskBRS
|
||||||
e.FDF = (w2 & fdcanElementMaskFDF) != 0
|
}
|
||||||
e.FIDX = uint8((w2 & fdcanElementMaskFIDX) >> 24)
|
|
||||||
e.ANMF = (w2 & fdcanElementMaskANMF) != 0
|
|
||||||
|
|
||||||
// Copy data bytes - must use 32-bit word access on Cortex-M0+
|
*(*uint32)(unsafe.Pointer(txAddr)) = w1
|
||||||
dataLen := dlcToBytes[e.DLC&0x0F]
|
*(*uint32)(unsafe.Pointer(txAddr + 4)) = w2
|
||||||
numWords := (dataLen + 3) / 4
|
|
||||||
for w := byte(0); w < numWords; w++ {
|
// Copy data with 32-bit word access (Cortex-M0+).
|
||||||
word := *(*uint32)(unsafe.Pointer(rxAddress + 8 + uintptr(w)*4))
|
for w := byte(0); w < (length+3)/4; w++ {
|
||||||
baseIdx := w * 4
|
var word uint32
|
||||||
for b := byte(0); b < 4 && baseIdx+b < dataLen; b++ {
|
base := w * 4
|
||||||
e.DB[baseIdx+b] = byte(word >> (b * 8))
|
for b := byte(0); b < 4 && base+b < length; b++ {
|
||||||
|
word |= uint32(data[base+b]) << (b * 8)
|
||||||
}
|
}
|
||||||
|
*(*uint32)(unsafe.Pointer(txAddr + 8 + uintptr(w)*4)) = word
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acknowledge the read
|
can.Bus.TXBAR.Set(1 << putIndex)
|
||||||
can.Bus.RXF0A.Set(uint32(getIndex))
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rx receives a CAN frame and returns its components
|
// rxFIFOLevel implements [CAN.RxFIFOLevel].
|
||||||
func (can *FDCAN) Rx() (id uint32, dlc byte, data []byte, isFD, isExtendedID bool, err error) {
|
// Returns 0,0 when interrupt-driven (messages delivered via callback).
|
||||||
e := FDCANRxBufferElement{}
|
func (can *CAN) rxFIFOLevel() (int, int) {
|
||||||
err = can.RxRaw(&e)
|
if canInstances[can.instance] != nil {
|
||||||
if err != nil {
|
return 0, 0
|
||||||
return 0, 0, nil, false, false, err
|
|
||||||
}
|
}
|
||||||
|
level := int(can.Bus.RXF0S.Get() & 0x0F) // F0FL[3:0]
|
||||||
length := FDCANDlcToLength(e.DLC, e.FDF)
|
return level, sramcanRF0Nbr
|
||||||
return e.ID, length, e.DB[:length], e.FDF, e.XTD, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetInterrupt configures interrupt handling for the FDCAN peripheral
|
// setRxCallback implements [CAN.SetRxCallback].
|
||||||
func (can *FDCAN) SetInterrupt(ie uint32, callback func(*FDCAN)) error {
|
// When cb is non-nil, interrupt-driven receive is enabled on RX FIFO 0.
|
||||||
if callback == nil {
|
// The CAN.Interrupt field must be initialized with interrupt.New in the board file.
|
||||||
can.Bus.IE.ClearBits(ie)
|
func (can *CAN) setRxCallback(cb canRxCallback) {
|
||||||
|
canRxCB[can.instance] = cb
|
||||||
|
if cb != nil {
|
||||||
|
canInstances[can.instance] = can
|
||||||
|
// Enable RX FIFO 0 new message interrupt, routed to interrupt line 0.
|
||||||
|
can.Bus.SetIE_RF0NE(1)
|
||||||
|
can.Bus.SetILS_RxFIFO0(0)
|
||||||
|
can.Bus.SetILE_EINT0(1)
|
||||||
|
can.Interrupt.Enable()
|
||||||
|
} else {
|
||||||
|
can.Bus.SetIE_RF0NE(0)
|
||||||
|
canInstances[can.instance] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rxPoll implements [CAN.RxPoll].
|
||||||
|
// No-op when interrupt-driven receive is active.
|
||||||
|
func (can *CAN) rxPoll() error {
|
||||||
|
if canInstances[can.instance] != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
cb := canRxCB[can.instance]
|
||||||
can.Bus.IE.SetBits(ie)
|
if cb == nil {
|
||||||
|
return nil
|
||||||
idx := can.instance
|
|
||||||
fdcanInstances[idx] = can
|
|
||||||
|
|
||||||
for i := uint(0); i < 32; i++ {
|
|
||||||
if ie&(1<<i) != 0 {
|
|
||||||
fdcanCallbacks[idx][i] = callback
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
processRxFIFO0(can, cb)
|
||||||
can.Interrupt.Enable()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigureFilter configures a message filter
|
// processRxFIFO0 drains RX FIFO 0 and delivers each message to cb.
|
||||||
func (can *FDCAN) ConfigureFilter(config FDCANFilterConfig) error {
|
// Used by both rxPoll (poll mode) and canHandleInterrupt (interrupt mode).
|
||||||
sramBase := can.getSRAMBase()
|
func processRxFIFO0(can *CAN, cb canRxCallback) {
|
||||||
|
for can.Bus.RXF0S.Get()&0x0F != 0 {
|
||||||
|
getIndex := (can.Bus.RXF0S.Get() >> 8) & 0x03 // F0GI[1:0]
|
||||||
|
rxAddr := can.sramBase() + sramcanRF0SA + uintptr(getIndex)*sramcanRF0Size
|
||||||
|
|
||||||
if config.IsExtendedID {
|
w1 := *(*uint32)(unsafe.Pointer(rxAddr))
|
||||||
// Extended filter
|
w2 := *(*uint32)(unsafe.Pointer(rxAddr + 4))
|
||||||
if config.Index >= sramcanFLENbr {
|
|
||||||
return errors.New("FDCAN: filter index out of range")
|
extendedID := w1&fdcanElementMaskXTD != 0
|
||||||
|
var id uint32
|
||||||
|
var flags uint32
|
||||||
|
if extendedID {
|
||||||
|
flags |= canFlagIDE
|
||||||
|
id = w1 & fdcanElementMaskEXTID
|
||||||
|
} else {
|
||||||
|
id = (w1 & fdcanElementMaskSTDID) >> 18
|
||||||
}
|
}
|
||||||
|
|
||||||
filterAddr := sramBase + sramcanFLESA + (uintptr(config.Index) * sramcanFLESize)
|
timestamp := w2 & fdcanElementMaskTS
|
||||||
|
dlc := byte((w2 & fdcanElementMaskDLC) >> 16)
|
||||||
|
isFD := w2&fdcanElementMaskFDF != 0
|
||||||
|
|
||||||
|
if isFD {
|
||||||
|
flags |= canFlagFDF
|
||||||
|
}
|
||||||
|
if w1&fdcanElementMaskRTR != 0 {
|
||||||
|
flags |= canFlagRTR
|
||||||
|
}
|
||||||
|
if w2&fdcanElementMaskBRS != 0 {
|
||||||
|
flags |= canFlagBRS
|
||||||
|
}
|
||||||
|
if w1&fdcanElementMaskESI != 0 {
|
||||||
|
flags |= canFlagESI
|
||||||
|
}
|
||||||
|
|
||||||
|
dataLen := dlcToLength(dlc)
|
||||||
|
if !isFD && dataLen > 8 {
|
||||||
|
dataLen = 8
|
||||||
|
}
|
||||||
|
var buf [64]byte
|
||||||
|
for w := byte(0); w < (dataLen+3)/4; w++ {
|
||||||
|
word := *(*uint32)(unsafe.Pointer(rxAddr + 8 + uintptr(w)*4))
|
||||||
|
base := w * 4
|
||||||
|
for b := byte(0); b < 4 && base+b < dataLen; b++ {
|
||||||
|
buf[base+b] = byte(word >> (b * 8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acknowledge before callback so the FIFO slot is freed.
|
||||||
|
can.Bus.RXF0A.Set(uint32(getIndex))
|
||||||
|
cb(buf[:dataLen], id, timestamp, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// canHandleInterrupt is the shared interrupt handler for FDCAN interrupt line 0 (IRQ_TIM16).
|
||||||
|
// Both FDCAN1 and FDCAN2 share this IRQ vector.
|
||||||
|
func canHandleInterrupt(interrupt.Interrupt) {
|
||||||
|
for i := range canInstances {
|
||||||
|
can := canInstances[i]
|
||||||
|
if can == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ir := can.Bus.IR.Get()
|
||||||
|
if ir&FDCAN_IT_RX_FIFO0_NEW_MESSAGE != 0 {
|
||||||
|
can.Bus.IR.Set(FDCAN_IT_RX_FIFO0_NEW_MESSAGE) // Write 1 to clear
|
||||||
|
if cb := canRxCB[i]; cb != nil {
|
||||||
|
processRxFIFO0(can, cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigureFilter configures a message acceptance filter.
|
||||||
|
func (can *CAN) ConfigureFilter(config CANFilterConfig) error {
|
||||||
|
base := can.sramBase()
|
||||||
|
|
||||||
|
if config.IsExtendedID {
|
||||||
|
if config.Index >= sramcanFLENbr {
|
||||||
|
return errors.New("CAN: filter index out of range")
|
||||||
|
}
|
||||||
|
|
||||||
|
filterAddr := base + sramcanFLESA + (uintptr(config.Index) * sramcanFLESize)
|
||||||
|
|
||||||
// Build filter elements
|
|
||||||
w1 := (uint32(config.Config) << 29) | (config.ID1 & 0x1FFFFFFF)
|
w1 := (uint32(config.Config) << 29) | (config.ID1 & 0x1FFFFFFF)
|
||||||
w2 := (uint32(config.Type) << 30) | (config.ID2 & 0x1FFFFFFF)
|
w2 := (uint32(config.Type) << 30) | (config.ID2 & 0x1FFFFFFF)
|
||||||
|
|
||||||
*(*uint32)(unsafe.Pointer(filterAddr)) = w1
|
*(*uint32)(unsafe.Pointer(filterAddr)) = w1
|
||||||
*(*uint32)(unsafe.Pointer(filterAddr + 4)) = w2
|
*(*uint32)(unsafe.Pointer(filterAddr + 4)) = w2
|
||||||
} else {
|
} else {
|
||||||
// Standard filter
|
|
||||||
if config.Index >= sramcanFLSNbr {
|
if config.Index >= sramcanFLSNbr {
|
||||||
return errors.New("FDCAN: filter index out of range")
|
return errors.New("CAN: filter index out of range")
|
||||||
}
|
}
|
||||||
|
|
||||||
filterAddr := sramBase + sramcanFLSSA + (uintptr(config.Index) * sramcanFLSSize)
|
filterAddr := base + sramcanFLSSA + (uintptr(config.Index) * sramcanFLSSize)
|
||||||
|
|
||||||
// Build filter element
|
|
||||||
w := (uint32(config.Type) << 30) |
|
w := (uint32(config.Type) << 30) |
|
||||||
(uint32(config.Config) << 27) |
|
(uint32(config.Config) << 27) |
|
||||||
((config.ID1 & 0x7FF) << 16) |
|
((config.ID1 & 0x7FF) << 16) |
|
||||||
@@ -561,56 +487,32 @@ func (can *FDCAN) ConfigureFilter(config FDCANFilterConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (can *FDCAN) getSRAMBase() uintptr {
|
func (can *CAN) sramBase() uintptr {
|
||||||
base := uintptr(sramcanBase)
|
|
||||||
if can.Bus == stm32.FDCAN2 {
|
if can.Bus == stm32.FDCAN2 {
|
||||||
base += sramcanSize
|
return uintptr(sramcanBase) + sramcanSize
|
||||||
}
|
}
|
||||||
return base
|
return uintptr(sramcanBase)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (can *FDCAN) configureMessageRAM() {
|
// fdcanNominalBitTiming returns prescaler and segment values for the nominal (arbitration) phase.
|
||||||
sramBase := can.getSRAMBase()
|
// STM32G0 FDCAN clock = 64 MHz, 16 time quanta per bit, ~80% sample point.
|
||||||
|
func fdcanNominalBitTiming(rate CANTransferRate) (brp, tseg1, tseg2, sjw uint32, err error) {
|
||||||
// Clear message RAM
|
|
||||||
for addr := sramBase; addr < sramBase+sramcanSize; addr += 4 {
|
|
||||||
*(*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)
|
|
||||||
can.Bus.RXGFC.Set(rxgfc)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
switch rate {
|
switch rate {
|
||||||
case FDCANTransferRate125kbps:
|
case FDCANTransferRate125kbps:
|
||||||
// 64MHz / 32 = 2MHz, 16 tq per bit = 125kbps
|
|
||||||
return 32, 13, 2, 4, nil
|
return 32, 13, 2, 4, nil
|
||||||
case FDCANTransferRate250kbps:
|
case FDCANTransferRate250kbps:
|
||||||
// 64MHz / 16 = 4MHz, 16 tq per bit = 250kbps
|
|
||||||
return 16, 13, 2, 4, nil
|
return 16, 13, 2, 4, nil
|
||||||
case FDCANTransferRate500kbps:
|
case FDCANTransferRate500kbps:
|
||||||
// 64MHz / 8 = 8MHz, 16 tq per bit = 500kbps
|
|
||||||
return 8, 13, 2, 4, nil
|
return 8, 13, 2, 4, nil
|
||||||
case FDCANTransferRate1000kbps:
|
case FDCANTransferRate1000kbps:
|
||||||
// 64MHz / 4 = 16MHz, 16 tq per bit = 1Mbps
|
|
||||||
return 4, 13, 2, 4, nil
|
return 4, 13, 2, 4, nil
|
||||||
default:
|
default:
|
||||||
return 0, 0, 0, 0, errFDCANInvalidTransferRate
|
return 0, 0, 0, 0, errCANInvalidTransferRate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (can *FDCAN) calculateDataBitTiming(rate FDCANTransferRate) (brp, tseg1, tseg2, sjw uint32, err error) {
|
// fdcanDataBitTiming returns prescaler and segment values for the data phase (FD).
|
||||||
// STM32G0 FDCAN clock = 64MHz
|
func fdcanDataBitTiming(rate CANTransferRate) (brp, tseg1, tseg2, sjw uint32, err error) {
|
||||||
// For data phase, we need higher bit rates
|
|
||||||
switch rate {
|
switch rate {
|
||||||
case FDCANTransferRate125kbps:
|
case FDCANTransferRate125kbps:
|
||||||
return 32, 13, 2, 4, nil
|
return 32, 13, 2, 4, nil
|
||||||
@@ -621,91 +523,10 @@ func (can *FDCAN) calculateDataBitTiming(rate FDCANTransferRate) (brp, tseg1, ts
|
|||||||
case FDCANTransferRate1000kbps:
|
case FDCANTransferRate1000kbps:
|
||||||
return 4, 13, 2, 4, nil
|
return 4, 13, 2, 4, nil
|
||||||
case FDCANTransferRate2000kbps:
|
case FDCANTransferRate2000kbps:
|
||||||
// 64MHz / 2 = 32MHz, 16 tq per bit = 2Mbps
|
|
||||||
return 2, 13, 2, 4, nil
|
return 2, 13, 2, 4, nil
|
||||||
case FDCANTransferRate4000kbps:
|
case FDCANTransferRate4000kbps:
|
||||||
// 64MHz / 1 = 64MHz, 16 tq per bit = 4Mbps
|
|
||||||
return 1, 13, 2, 4, nil
|
return 1, 13, 2, 4, nil
|
||||||
default:
|
default:
|
||||||
return 0, 0, 0, 0, errFDCANInvalidTransferRateFD
|
return 0, 0, 0, 0, errCANInvalidTransferRateFD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FDCANDlcToLength converts a DLC value to actual byte length
|
|
||||||
func FDCANDlcToLength(dlc byte, isFD bool) byte {
|
|
||||||
if dlc > 15 {
|
|
||||||
dlc = 15
|
|
||||||
}
|
|
||||||
length := dlcToBytes[dlc]
|
|
||||||
if !isFD && length > 8 {
|
|
||||||
return 8
|
|
||||||
}
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
|
|
||||||
// FDCANLengthToDlc converts a byte length to DLC value
|
|
||||||
func FDCANLengthToDlc(length byte, isFD bool) byte {
|
|
||||||
if !isFD {
|
|
||||||
if length > 8 {
|
|
||||||
return 8
|
|
||||||
}
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case length <= 8:
|
|
||||||
return length
|
|
||||||
case length <= 12:
|
|
||||||
return 9
|
|
||||||
case length <= 16:
|
|
||||||
return 10
|
|
||||||
case length <= 20:
|
|
||||||
return 11
|
|
||||||
case length <= 24:
|
|
||||||
return 12
|
|
||||||
case length <= 32:
|
|
||||||
return 13
|
|
||||||
case length <= 48:
|
|
||||||
return 14
|
|
||||||
default:
|
|
||||||
return 15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interrupt handling
|
|
||||||
var (
|
|
||||||
fdcanInstances [2]*FDCAN
|
|
||||||
fdcanCallbacks [2][32]func(*FDCAN)
|
|
||||||
)
|
|
||||||
|
|
||||||
func fdcanHandleInterrupt(idx int) {
|
|
||||||
if fdcanInstances[idx] == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
can := fdcanInstances[idx]
|
|
||||||
ir := can.Bus.IR.Get()
|
|
||||||
can.Bus.IR.Set(ir) // Clear interrupt flags
|
|
||||||
|
|
||||||
for i := uint(0); i < 32; i++ {
|
|
||||||
if ir&(1<<i) != 0 && fdcanCallbacks[idx][i] != nil {
|
|
||||||
fdcanCallbacks[idx][i](can)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data returns the received data as a slice
|
|
||||||
func (e *FDCANRxBufferElement) Data() []byte {
|
|
||||||
return e.DB[:FDCANDlcToLength(e.DLC, e.FDF)]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Length returns the actual data length
|
|
||||||
func (e *FDCANRxBufferElement) Length() byte {
|
|
||||||
return FDCANDlcToLength(e.DLC, e.FDF)
|
|
||||||
}
|
|
||||||
|
|
||||||
// enableFDCANClock enables the FDCAN peripheral clock
|
|
||||||
func enableFDCANClock() {
|
|
||||||
// FDCAN clock is on APB1
|
|
||||||
stm32.RCC.SetAPBENR1_FDCANEN(1)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"go/build/constraint"
|
|
||||||
"go/doc"
|
|
||||||
"go/parser"
|
|
||||||
"go/token"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// buildTagSet creates the full set of tags that should evaluate to true
|
|
||||||
// for a given target, matching TinyGo's Config.BuildTags() behavior.
|
|
||||||
func buildTagSet(t Target) map[string]bool {
|
|
||||||
tags := make(map[string]bool)
|
|
||||||
for _, tag := range t.BuildTags {
|
|
||||||
tags[tag] = true
|
|
||||||
}
|
|
||||||
if t.GOOS != "" {
|
|
||||||
tags[t.GOOS] = true
|
|
||||||
}
|
|
||||||
if t.GOARCH != "" {
|
|
||||||
tags[t.GOARCH] = true
|
|
||||||
}
|
|
||||||
tags["tinygo"] = true
|
|
||||||
tags["purego"] = true
|
|
||||||
tags["osusergo"] = true
|
|
||||||
tags["math_big_pure_go"] = true
|
|
||||||
if t.GC != "" {
|
|
||||||
tags["gc."+t.GC] = true
|
|
||||||
}
|
|
||||||
if t.Scheduler != "" {
|
|
||||||
tags["scheduler."+t.Scheduler] = true
|
|
||||||
}
|
|
||||||
if t.Serial != "" {
|
|
||||||
tags["serial."+t.Serial] = true
|
|
||||||
}
|
|
||||||
// Go version tags — TinyGo currently tracks Go 1.22.
|
|
||||||
for i := 1; i <= 22; i++ {
|
|
||||||
tags[fmt.Sprintf("go1.%d", i)] = true
|
|
||||||
}
|
|
||||||
return tags
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractBuildConstraint reads the file header (before the package clause)
|
|
||||||
// looking for a //go:build constraint line.
|
|
||||||
func extractBuildConstraint(data []byte) (constraint.Expr, error) {
|
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if strings.HasPrefix(trimmed, "package ") {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if constraint.IsGoBuild(trimmed) {
|
|
||||||
return constraint.Parse(trimmed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractDocs filters .go files in pkgDir by the target's build tags,
|
|
||||||
// parses them, and returns a *doc.Package via go/doc.NewFromFiles.
|
|
||||||
func ExtractDocs(t Target, pkgDir string, allDecls bool) (*doc.Package, *token.FileSet, error) {
|
|
||||||
tags := buildTagSet(t)
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(pkgDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fset := token.NewFileSet()
|
|
||||||
var files []*ast.File
|
|
||||||
|
|
||||||
for _, e := range entries {
|
|
||||||
name := e.Name()
|
|
||||||
if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
path := filepath.Join(pkgDir, name)
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
expr, err := extractBuildConstraint(data)
|
|
||||||
if err != nil {
|
|
||||||
continue // skip files with unparseable constraints
|
|
||||||
}
|
|
||||||
|
|
||||||
if expr != nil && !expr.Eval(func(tag string) bool { return tags[tag] }) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := parser.ParseFile(fset, path, data, parser.ParseComments)
|
|
||||||
if err != nil {
|
|
||||||
continue // skip files with syntax errors
|
|
||||||
}
|
|
||||||
|
|
||||||
files = append(files, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(files) == 0 {
|
|
||||||
return nil, nil, fmt.Errorf("no matching files for target %s", t.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
var opts []any
|
|
||||||
if allDecls {
|
|
||||||
opts = append(opts, doc.AllDecls)
|
|
||||||
}
|
|
||||||
|
|
||||||
docPkg, err := doc.NewFromFiles(fset, files, "machine", opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return docPkg, fset, nil
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"go/doc"
|
|
||||||
"go/format"
|
|
||||||
"go/token"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Result holds the extracted documentation for a single target.
|
|
||||||
type Result struct {
|
|
||||||
Target Target
|
|
||||||
Pkg *doc.Package
|
|
||||||
Fset *token.FileSet
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON-serializable API types.
|
|
||||||
|
|
||||||
type PackageAPI struct {
|
|
||||||
Target string `json:"target"`
|
|
||||||
Package string `json:"package"`
|
|
||||||
Doc string `json:"doc,omitempty"`
|
|
||||||
Constants []APIValue `json:"constants,omitempty"`
|
|
||||||
Variables []APIValue `json:"variables,omitempty"`
|
|
||||||
Types []APIType `json:"types,omitempty"`
|
|
||||||
Functions []APIFunc `json:"functions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type APIValue struct {
|
|
||||||
Doc string `json:"doc,omitempty"`
|
|
||||||
Names []string `json:"names"`
|
|
||||||
Decl string `json:"decl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type APIType struct {
|
|
||||||
Doc string `json:"doc,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Decl string `json:"decl"`
|
|
||||||
Constants []APIValue `json:"constants,omitempty"`
|
|
||||||
Variables []APIValue `json:"variables,omitempty"`
|
|
||||||
Functions []APIFunc `json:"functions,omitempty"`
|
|
||||||
Methods []APIFunc `json:"methods,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type APIFunc struct {
|
|
||||||
Doc string `json:"doc,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Decl string `json:"decl"`
|
|
||||||
Recv string `json:"recv,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatNode(fset *token.FileSet, node ast.Node) string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
format.Node(&buf, fset, node)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertValues(fset *token.FileSet, vals []*doc.Value) []APIValue {
|
|
||||||
out := make([]APIValue, 0, len(vals))
|
|
||||||
for _, v := range vals {
|
|
||||||
out = append(out, APIValue{
|
|
||||||
Doc: v.Doc,
|
|
||||||
Names: v.Names,
|
|
||||||
Decl: formatNode(fset, v.Decl),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertFuncs(fset *token.FileSet, funcs []*doc.Func) []APIFunc {
|
|
||||||
out := make([]APIFunc, 0, len(funcs))
|
|
||||||
for _, f := range funcs {
|
|
||||||
out = append(out, APIFunc{
|
|
||||||
Doc: f.Doc,
|
|
||||||
Name: f.Name,
|
|
||||||
Decl: formatNode(fset, f.Decl),
|
|
||||||
Recv: f.Recv,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertPackage(targetName string, pkg *doc.Package, fset *token.FileSet) *PackageAPI {
|
|
||||||
api := &PackageAPI{
|
|
||||||
Target: targetName,
|
|
||||||
Package: pkg.Name,
|
|
||||||
Doc: pkg.Doc,
|
|
||||||
Constants: convertValues(fset, pkg.Consts),
|
|
||||||
Variables: convertValues(fset, pkg.Vars),
|
|
||||||
Functions: convertFuncs(fset, pkg.Funcs),
|
|
||||||
}
|
|
||||||
for _, t := range pkg.Types {
|
|
||||||
api.Types = append(api.Types, APIType{
|
|
||||||
Doc: t.Doc,
|
|
||||||
Name: t.Name,
|
|
||||||
Decl: formatNode(fset, t.Decl),
|
|
||||||
Constants: convertValues(fset, t.Consts),
|
|
||||||
Variables: convertValues(fset, t.Vars),
|
|
||||||
Functions: convertFuncs(fset, t.Funcs),
|
|
||||||
Methods: convertFuncs(fset, t.Methods),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return api
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
jsonOutput := flag.Bool("json", false, "output JSON to stdout")
|
|
||||||
httpAddr := flag.String("http", "", "serve HTTP documentation (e.g. :8080)")
|
|
||||||
allDecls := flag.Bool("all", false, "include unexported identifiers")
|
|
||||||
includeBase := flag.Bool("base", false, "include base/parent targets")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
args := flag.Args()
|
|
||||||
if len(args) < 2 {
|
|
||||||
fmt.Fprintf(os.Stderr, "usage: tgdoc [flags] <targets-dir> <package-dir>\n")
|
|
||||||
fmt.Fprintf(os.Stderr, "\nexample: tgdoc -http :8080 ./targets ./src/machine\n")
|
|
||||||
fmt.Fprintf(os.Stderr, "\nFlags:\n")
|
|
||||||
flag.PrintDefaults()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
targetsDir := args[0]
|
|
||||||
pkgDir := args[1]
|
|
||||||
|
|
||||||
if *httpAddr != "" && !strings.Contains(*httpAddr, ":") {
|
|
||||||
fmt.Fprintf(os.Stderr, "error: -http value %q doesn't look like an address (expected e.g. :8080)\n", *httpAddr)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
targets, err := LoadTargets(targetsDir, *includeBase)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []Result
|
|
||||||
for _, t := range targets {
|
|
||||||
pkg, fset, err := ExtractDocs(t, pkgDir, *allDecls)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "warning: %s: %v\n", t.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
results = append(results, Result{t, pkg, fset})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(results, func(i, j int) bool { return results[i].Target.Name < results[j].Target.Name })
|
|
||||||
|
|
||||||
if *httpAddr != "" {
|
|
||||||
serve(*httpAddr, results)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default: JSON output.
|
|
||||||
_ = jsonOutput
|
|
||||||
apis := make([]*PackageAPI, 0, len(results))
|
|
||||||
for _, r := range results {
|
|
||||||
apis = append(apis, convertPackage(r.Target.Name, r.Pkg, r.Fset))
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
if err := enc.Encode(apis); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"go/ast"
|
|
||||||
"go/doc"
|
|
||||||
"go/format"
|
|
||||||
"go/token"
|
|
||||||
"html/template"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IdentIndex is the precomputed identifier index across all targets.
|
|
||||||
type IdentIndex struct {
|
|
||||||
Entries []*IdentEntry
|
|
||||||
ByKey map[string]*IdentEntry
|
|
||||||
Total int // total number of targets
|
|
||||||
}
|
|
||||||
|
|
||||||
// IdentEntry is one identifier (type, func, method, const, or var) across all targets.
|
|
||||||
type IdentEntry struct {
|
|
||||||
Name string
|
|
||||||
Kind string // "type", "func", "method", "const", "var"
|
|
||||||
Count int
|
|
||||||
Key string // URL key: "kind/name"
|
|
||||||
Groups []IdentGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
// IdentGroup is a set of targets with identical declaration for an identifier.
|
|
||||||
type IdentGroup struct {
|
|
||||||
Decl string // representative formatted declaration
|
|
||||||
Targets []string // sorted target names
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildIdentIndex(results []Result) *IdentIndex {
|
|
||||||
type occ struct {
|
|
||||||
target string
|
|
||||||
decl string // grouping key + display
|
|
||||||
}
|
|
||||||
type ikey struct {
|
|
||||||
name, kind string
|
|
||||||
}
|
|
||||||
|
|
||||||
idx := make(map[ikey][]occ)
|
|
||||||
|
|
||||||
addFunc := func(target string, fset *token.FileSet, f *doc.Func, prefix string) {
|
|
||||||
name := f.Name
|
|
||||||
if prefix != "" {
|
|
||||||
name = prefix + "." + f.Name
|
|
||||||
}
|
|
||||||
kind := "func"
|
|
||||||
if f.Recv != "" {
|
|
||||||
kind = "method"
|
|
||||||
}
|
|
||||||
decl := fmtNode(fset, f.Decl)
|
|
||||||
idx[ikey{name, kind}] = append(idx[ikey{name, kind}], occ{target, decl})
|
|
||||||
}
|
|
||||||
|
|
||||||
addValues := func(target string, fset *token.FileSet, vals []*doc.Value, kind string) {
|
|
||||||
for _, v := range vals {
|
|
||||||
for _, name := range v.Names {
|
|
||||||
typeKey := resolveValueType(fset, v.Decl, name)
|
|
||||||
idx[ikey{name, kind}] = append(idx[ikey{name, kind}], occ{target, typeKey})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range results {
|
|
||||||
t := r.Target.Name
|
|
||||||
fset := r.Fset
|
|
||||||
pkg := r.Pkg
|
|
||||||
|
|
||||||
for _, ty := range pkg.Types {
|
|
||||||
decl := fmtNode(fset, ty.Decl)
|
|
||||||
idx[ikey{ty.Name, "type"}] = append(idx[ikey{ty.Name, "type"}], occ{t, decl})
|
|
||||||
|
|
||||||
for _, m := range ty.Methods {
|
|
||||||
addFunc(t, fset, m, ty.Name)
|
|
||||||
}
|
|
||||||
for _, f := range ty.Funcs {
|
|
||||||
addFunc(t, fset, f, "")
|
|
||||||
}
|
|
||||||
addValues(t, fset, ty.Consts, "const")
|
|
||||||
addValues(t, fset, ty.Vars, "var")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range pkg.Funcs {
|
|
||||||
addFunc(t, fset, f, "")
|
|
||||||
}
|
|
||||||
addValues(t, fset, pkg.Consts, "const")
|
|
||||||
addValues(t, fset, pkg.Vars, "var")
|
|
||||||
}
|
|
||||||
|
|
||||||
index := &IdentIndex{
|
|
||||||
ByKey: make(map[string]*IdentEntry),
|
|
||||||
Total: len(results),
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, occs := range idx {
|
|
||||||
groups := make(map[string][]string) // decl -> targets
|
|
||||||
for _, o := range occs {
|
|
||||||
groups[o.decl] = append(groups[o.decl], o.target)
|
|
||||||
}
|
|
||||||
|
|
||||||
var gs []IdentGroup
|
|
||||||
for decl, targets := range groups {
|
|
||||||
sort.Strings(targets)
|
|
||||||
gs = append(gs, IdentGroup{Decl: decl, Targets: targets})
|
|
||||||
}
|
|
||||||
sort.Slice(gs, func(i, j int) bool { return len(gs[i].Targets) > len(gs[j].Targets) })
|
|
||||||
|
|
||||||
urlKey := key.kind + "/" + key.name
|
|
||||||
entry := &IdentEntry{
|
|
||||||
Name: key.name,
|
|
||||||
Kind: key.kind,
|
|
||||||
Count: len(occs),
|
|
||||||
Key: urlKey,
|
|
||||||
Groups: gs,
|
|
||||||
}
|
|
||||||
index.Entries = append(index.Entries, entry)
|
|
||||||
index.ByKey[urlKey] = entry
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(index.Entries, func(i, j int) bool {
|
|
||||||
if index.Entries[i].Count != index.Entries[j].Count {
|
|
||||||
return index.Entries[i].Count > index.Entries[j].Count
|
|
||||||
}
|
|
||||||
return index.Entries[i].Name < index.Entries[j].Name
|
|
||||||
})
|
|
||||||
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveValueType returns the effective type string for a const/var name
|
|
||||||
// within a GenDecl, handling iota type inheritance for constants.
|
|
||||||
func resolveValueType(fset *token.FileSet, gd *ast.GenDecl, name string) string {
|
|
||||||
isConst := gd.Tok == token.CONST
|
|
||||||
var lastType ast.Expr
|
|
||||||
for _, spec := range gd.Specs {
|
|
||||||
vs := spec.(*ast.ValueSpec)
|
|
||||||
if vs.Type != nil {
|
|
||||||
lastType = vs.Type
|
|
||||||
}
|
|
||||||
for _, n := range vs.Names {
|
|
||||||
if n.Name != name {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
effType := vs.Type
|
|
||||||
if effType == nil && isConst {
|
|
||||||
effType = lastType
|
|
||||||
}
|
|
||||||
if effType != nil {
|
|
||||||
return fmtNode(fset, effType)
|
|
||||||
}
|
|
||||||
return "(untyped)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "(unknown)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// fmtNode formats an AST node to Go source. Separate from formatNode in main.go
|
|
||||||
// to make it clear this is the server's version (same logic).
|
|
||||||
func fmtNode(fset *token.FileSet, node ast.Node) string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
format.Node(&buf, fset, node)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- HTTP Server ---
|
|
||||||
|
|
||||||
func serve(addr string, results []Result) {
|
|
||||||
index := buildIdentIndex(results)
|
|
||||||
|
|
||||||
byTarget := make(map[string]Result, len(results))
|
|
||||||
for _, r := range results {
|
|
||||||
byTarget[r.Target.Name] = r
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path != "/" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data := pageData{Index: index, Results: results}
|
|
||||||
pageTmpl.Execute(w, data)
|
|
||||||
})
|
|
||||||
|
|
||||||
mux.HandleFunc("/id/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
key := strings.TrimPrefix(r.URL.Path, "/id/")
|
|
||||||
entry, ok := index.ByKey[key]
|
|
||||||
if !ok {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data := pageData{Index: index, Results: results, Selected: entry, SelectedKey: key}
|
|
||||||
pageTmpl.Execute(w, data)
|
|
||||||
})
|
|
||||||
|
|
||||||
mux.HandleFunc("/target/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
name := strings.TrimPrefix(r.URL.Path, "/target/")
|
|
||||||
res, ok := byTarget[name]
|
|
||||||
if !ok {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
api := convertPackage(res.Target.Name, res.Pkg, res.Fset)
|
|
||||||
targetTmpl.Execute(w, api)
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Printf("serving documentation on http://localhost%s", addr)
|
|
||||||
log.Fatal(http.ListenAndServe(addr, mux))
|
|
||||||
}
|
|
||||||
|
|
||||||
type pageData struct {
|
|
||||||
Index *IdentIndex
|
|
||||||
Results []Result
|
|
||||||
Selected *IdentEntry
|
|
||||||
SelectedKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Templates ---
|
|
||||||
|
|
||||||
var pageTmpl = template.Must(template.New("page").Funcs(template.FuncMap{
|
|
||||||
"kindBadge": func(kind string) string {
|
|
||||||
switch kind {
|
|
||||||
case "type":
|
|
||||||
return "T"
|
|
||||||
case "func":
|
|
||||||
return "F"
|
|
||||||
case "method":
|
|
||||||
return "M"
|
|
||||||
case "const":
|
|
||||||
return "C"
|
|
||||||
case "var":
|
|
||||||
return "V"
|
|
||||||
}
|
|
||||||
return "?"
|
|
||||||
},
|
|
||||||
"pre": func(s string) template.HTML {
|
|
||||||
return template.HTML("<pre>" + template.HTMLEscapeString(s) + "</pre>")
|
|
||||||
},
|
|
||||||
"kindClass": func(kind string) string { return "kind-" + kind },
|
|
||||||
}).Parse(`<!DOCTYPE html>
|
|
||||||
<html><head>
|
|
||||||
<title>tgdoc{{if .Selected}} — {{.Selected.Name}}{{end}}</title>
|
|
||||||
<style>
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
||||||
body { font-family: system-ui, -apple-system, sans-serif; }
|
|
||||||
.layout { display: flex; height: 100vh; }
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
.sidebar { width: 320px; min-width: 320px; border-right: 1px solid #ddd; display: flex; flex-direction: column; background: #fafafa; }
|
|
||||||
.sidebar-header { padding: 12px; border-bottom: 1px solid #ddd; }
|
|
||||||
.sidebar-header h2 { font-size: 1.1em; margin-bottom: 8px; }
|
|
||||||
.sidebar-header input { width: 100%; padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 0.9em; }
|
|
||||||
.sidebar-header .filter-row { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
|
|
||||||
.sidebar-header .filter-btn { font-size: 0.7em; padding: 2px 6px; border: 1px solid #ccc; border-radius: 3px; background: white; cursor: pointer; }
|
|
||||||
.sidebar-header .filter-btn.active { background: #333; color: white; border-color: #333; }
|
|
||||||
.ident-list { overflow-y: auto; flex: 1; }
|
|
||||||
.ident-list a { display: flex; align-items: center; padding: 3px 12px; text-decoration: none; color: #333; font-size: 0.85em; border-left: 3px solid transparent; }
|
|
||||||
.ident-list a:hover { background: #f0f0f0; }
|
|
||||||
.ident-list a.active { background: #e8e8f4; border-left-color: #4444cc; }
|
|
||||||
.ident-list .badge { display: inline-block; width: 18px; height: 18px; line-height: 18px; text-align: center; border-radius: 3px; font-size: 0.65em; font-weight: bold; margin-right: 6px; color: white; flex-shrink: 0; }
|
|
||||||
.ident-list .count { margin-left: auto; color: #999; font-size: 0.8em; padding-left: 8px; flex-shrink: 0; }
|
|
||||||
.ident-list .ngroups { color: salmon; font-size: 0.8em; padding-left: 2px; flex-shrink: 0; }
|
|
||||||
.ident-list .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
.kind-type .badge { background: #2196F3; }
|
|
||||||
.kind-func .badge { background: #4CAF50; }
|
|
||||||
.kind-method .badge { background: #9C27B0; }
|
|
||||||
.kind-const .badge { background: #FF9800; }
|
|
||||||
.kind-var .badge { background: #795548; }
|
|
||||||
|
|
||||||
/* Main content */
|
|
||||||
main { flex: 1; overflow-y: auto; padding: 2em; }
|
|
||||||
main h1 { font-size: 1.5em; border-bottom: 2px solid #333; padding-bottom: 0.3em; margin-bottom: 0.5em; }
|
|
||||||
main .subtitle { color: #666; margin-bottom: 1.5em; }
|
|
||||||
.group { margin-bottom: 1.5em; border: 1px solid #e0e0e0; border-radius: 6px; overflow: hidden; }
|
|
||||||
.group-header { background: #f5f5f5; padding: 10px 14px; font-size: 0.9em; border-bottom: 1px solid #e0e0e0; }
|
|
||||||
.group-header strong { font-size: 1.05em; }
|
|
||||||
.group pre { margin: 0; padding: 14px; background: #fcfcfc; overflow-x: auto; font-size: 0.85em; border-bottom: 1px solid #eee; }
|
|
||||||
.group .targets { padding: 10px 14px; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
||||||
.group .targets a { background: #e8e8e8; padding: 2px 8px; border-radius: 3px; font-size: 0.78em; text-decoration: none; color: #333; }
|
|
||||||
.group .targets a:hover { background: #d0d0d0; }
|
|
||||||
.welcome nav a { margin-right: 1em; }
|
|
||||||
</style>
|
|
||||||
</head><body>
|
|
||||||
<div class="layout">
|
|
||||||
<aside class="sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<h2><a href="/" style="text-decoration:none;color:inherit">tgdoc</a></h2>
|
|
||||||
<input type="text" id="filter" placeholder="Filter identifiers..." oninput="filterList()">
|
|
||||||
<div class="filter-row">
|
|
||||||
<button class="filter-btn active" data-kind="all" onclick="toggleKind(this)">All</button>
|
|
||||||
<button class="filter-btn active" data-kind="type" onclick="toggleKind(this)">T</button>
|
|
||||||
<button class="filter-btn active" data-kind="func" onclick="toggleKind(this)">F</button>
|
|
||||||
<button class="filter-btn active" data-kind="method" onclick="toggleKind(this)">M</button>
|
|
||||||
<button class="filter-btn active" data-kind="const" onclick="toggleKind(this)">C</button>
|
|
||||||
<button class="filter-btn active" data-kind="var" onclick="toggleKind(this)">V</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ident-list">
|
|
||||||
{{range .Index.Entries}}
|
|
||||||
<a href="/id/{{.Key}}" class="{{kindClass .Kind}}{{if eq .Key $.SelectedKey}} active{{end}}" data-name="{{.Name}}" data-kind="{{.Kind}}">
|
|
||||||
<span class="badge">{{kindBadge .Kind}}</span>
|
|
||||||
<span class="name">{{.Name}}</span>
|
|
||||||
<span class="count">{{.Count}}{{if gt (len .Groups) 1}}<span class="ngroups">/{{len .Groups}}</span>{{end}}</span>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<main>
|
|
||||||
{{if .Selected}}
|
|
||||||
<h1><span class="badge" style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.6em;vertical-align:middle;color:white;background:{{if eq .Selected.Kind "type"}}#2196F3{{else if eq .Selected.Kind "func"}}#4CAF50{{else if eq .Selected.Kind "method"}}#9C27B0{{else if eq .Selected.Kind "const"}}#FF9800{{else}}#795548{{end}}">{{.Selected.Kind}}</span> {{.Selected.Name}}</h1>
|
|
||||||
<p class="subtitle">Present in {{.Selected.Count}} / {{.Index.Total}} targets
|
|
||||||
{{if eq (len .Selected.Groups) 1}}— identical across all targets{{else}}— {{len .Selected.Groups}} distinct signatures{{end}}</p>
|
|
||||||
|
|
||||||
{{range .Selected.Groups}}
|
|
||||||
<div class="group">
|
|
||||||
<div class="group-header"><strong>{{len .Targets}} target{{if ne (len .Targets) 1}}s{{end}}</strong></div>
|
|
||||||
{{pre .Decl}}
|
|
||||||
<div class="targets">{{range .Targets}}<a href="/target/{{.}}">{{.}}</a> {{end}}</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{else}}
|
|
||||||
<div class="welcome">
|
|
||||||
<h1>tgdoc</h1>
|
|
||||||
<p class="subtitle">{{len .Index.Entries}} identifiers across {{.Index.Total}} targets</p>
|
|
||||||
<p>Select an identifier from the sidebar to see its appearances across targets.</p>
|
|
||||||
<p style="margin-top:1em">Browse by target:</p>
|
|
||||||
<nav style="margin-top:0.5em;column-count:3">
|
|
||||||
{{range .Results}}<a href="/target/{{.Target.Name}}" style="display:block;padding:2px 0">{{.Target.Name}}</a>
|
|
||||||
{{end}}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
var activeKinds = new Set(["type","func","method","const","var"]);
|
|
||||||
function filterList() {
|
|
||||||
var q = document.getElementById("filter").value.toLowerCase();
|
|
||||||
document.querySelectorAll(".ident-list a").forEach(function(a) {
|
|
||||||
var name = a.dataset.name.toLowerCase();
|
|
||||||
var kind = a.dataset.kind;
|
|
||||||
var textMatch = !q || name.includes(q);
|
|
||||||
var kindMatch = activeKinds.has(kind);
|
|
||||||
a.style.display = (textMatch && kindMatch) ? "" : "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function toggleKind(btn) {
|
|
||||||
var kind = btn.dataset.kind;
|
|
||||||
if (kind === "all") {
|
|
||||||
var allActive = activeKinds.size === 5;
|
|
||||||
document.querySelectorAll(".filter-btn").forEach(function(b) {
|
|
||||||
if (allActive) { b.classList.remove("active"); activeKinds.clear(); }
|
|
||||||
else { b.classList.add("active"); activeKinds.add(b.dataset.kind); }
|
|
||||||
});
|
|
||||||
if (!allActive) activeKinds.delete("all");
|
|
||||||
} else {
|
|
||||||
btn.classList.toggle("active");
|
|
||||||
if (activeKinds.has(kind)) activeKinds.delete(kind); else activeKinds.add(kind);
|
|
||||||
var allBtn = document.querySelector('[data-kind="all"]');
|
|
||||||
if (activeKinds.size === 5) allBtn.classList.add("active"); else allBtn.classList.remove("active");
|
|
||||||
}
|
|
||||||
filterList();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body></html>`))
|
|
||||||
|
|
||||||
var targetTmpl = template.Must(template.New("target").Funcs(template.FuncMap{
|
|
||||||
"pre": func(s string) template.HTML {
|
|
||||||
return template.HTML("<pre>" + template.HTMLEscapeString(s) + "</pre>")
|
|
||||||
},
|
|
||||||
}).Parse(`<!DOCTYPE html>
|
|
||||||
<html><head>
|
|
||||||
<title>{{.Target}} — tgdoc</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 2em auto; padding: 0 1em; }
|
|
||||||
h1 { border-bottom: 2px solid #333; padding-bottom: 0.3em; }
|
|
||||||
h2 { margin-top: 2em; color: #444; }
|
|
||||||
h3 { margin-top: 1.5em; }
|
|
||||||
pre { background: #f5f5f5; padding: 0.8em; overflow-x: auto; font-size: 0.9em; border-radius: 4px; }
|
|
||||||
.doc { color: #555; margin-bottom: 0.5em; white-space: pre-wrap; }
|
|
||||||
.section { margin-left: 1em; }
|
|
||||||
a { text-decoration: none; }
|
|
||||||
a:hover { text-decoration: underline; }
|
|
||||||
</style>
|
|
||||||
</head><body>
|
|
||||||
<p><a href="/">← identifier index</a></p>
|
|
||||||
<h1>{{.Target}}</h1>
|
|
||||||
<p>package {{.Package}}</p>
|
|
||||||
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
|
|
||||||
|
|
||||||
{{if .Constants}}
|
|
||||||
<h2>Constants</h2>
|
|
||||||
{{range .Constants}}
|
|
||||||
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
|
|
||||||
{{pre .Decl}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if .Variables}}
|
|
||||||
<h2>Variables</h2>
|
|
||||||
{{range .Variables}}
|
|
||||||
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
|
|
||||||
{{pre .Decl}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if .Functions}}
|
|
||||||
<h2>Functions</h2>
|
|
||||||
{{range .Functions}}
|
|
||||||
<h3>func {{.Name}}</h3>
|
|
||||||
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
|
|
||||||
{{pre .Decl}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if .Types}}
|
|
||||||
<h2>Types</h2>
|
|
||||||
{{range .Types}}
|
|
||||||
<h3>type {{.Name}}</h3>
|
|
||||||
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
|
|
||||||
{{pre .Decl}}
|
|
||||||
{{if .Constants}}<div class="section"><h4>Associated Constants</h4>
|
|
||||||
{{range .Constants}}{{pre .Decl}}{{end}}</div>{{end}}
|
|
||||||
{{if .Variables}}<div class="section"><h4>Associated Variables</h4>
|
|
||||||
{{range .Variables}}{{pre .Decl}}{{end}}</div>{{end}}
|
|
||||||
{{if .Functions}}<div class="section"><h4>Constructors</h4>
|
|
||||||
{{range .Functions}}
|
|
||||||
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
|
|
||||||
{{pre .Decl}}
|
|
||||||
{{end}}</div>{{end}}
|
|
||||||
{{if .Methods}}<div class="section"><h4>Methods</h4>
|
|
||||||
{{range .Methods}}
|
|
||||||
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
|
|
||||||
{{pre .Decl}}
|
|
||||||
{{end}}</div>{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
</body></html>`))
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// targetSpec is the minimal subset of TinyGo's target JSON we need.
|
|
||||||
type targetSpec struct {
|
|
||||||
Inherits []string `json:"inherits,omitempty"`
|
|
||||||
BuildTags []string `json:"build-tags,omitempty"`
|
|
||||||
GOOS string `json:"goos,omitempty"`
|
|
||||||
GOARCH string `json:"goarch,omitempty"`
|
|
||||||
GC string `json:"gc,omitempty"`
|
|
||||||
Scheduler string `json:"scheduler,omitempty"`
|
|
||||||
Serial string `json:"serial,omitempty"`
|
|
||||||
FlashMethod string `json:"flash-method,omitempty"`
|
|
||||||
FlashCommand string `json:"flash-command,omitempty"`
|
|
||||||
Emulator string `json:"emulator,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Target is a resolved target with inheritance applied.
|
|
||||||
type Target struct {
|
|
||||||
Name string
|
|
||||||
BuildTags []string
|
|
||||||
GOOS string
|
|
||||||
GOARCH string
|
|
||||||
GC string
|
|
||||||
Scheduler string
|
|
||||||
Serial string
|
|
||||||
}
|
|
||||||
|
|
||||||
// override merges src properties into dst following TinyGo's semantics:
|
|
||||||
// strings override if non-empty, slices append with dedup.
|
|
||||||
func (dst *targetSpec) override(src *targetSpec) {
|
|
||||||
if src.GOOS != "" {
|
|
||||||
dst.GOOS = src.GOOS
|
|
||||||
}
|
|
||||||
if src.GOARCH != "" {
|
|
||||||
dst.GOARCH = src.GOARCH
|
|
||||||
}
|
|
||||||
if src.GC != "" {
|
|
||||||
dst.GC = src.GC
|
|
||||||
}
|
|
||||||
if src.Scheduler != "" {
|
|
||||||
dst.Scheduler = src.Scheduler
|
|
||||||
}
|
|
||||||
if src.Serial != "" {
|
|
||||||
dst.Serial = src.Serial
|
|
||||||
}
|
|
||||||
if src.FlashMethod != "" {
|
|
||||||
dst.FlashMethod = src.FlashMethod
|
|
||||||
}
|
|
||||||
if src.FlashCommand != "" {
|
|
||||||
dst.FlashCommand = src.FlashCommand
|
|
||||||
}
|
|
||||||
if src.Emulator != "" {
|
|
||||||
dst.Emulator = src.Emulator
|
|
||||||
}
|
|
||||||
dst.BuildTags = appendUnique(dst.BuildTags, src.BuildTags...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendUnique(dst []string, src ...string) []string {
|
|
||||||
seen := make(map[string]bool, len(dst))
|
|
||||||
for _, s := range dst {
|
|
||||||
seen[s] = true
|
|
||||||
}
|
|
||||||
for _, s := range src {
|
|
||||||
if !seen[s] {
|
|
||||||
dst = append(dst, s)
|
|
||||||
seen[s] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadRawTargets(dir string) (map[string]*targetSpec, error) {
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
specs := make(map[string]*targetSpec)
|
|
||||||
for _, e := range entries {
|
|
||||||
if !e.Type().IsRegular() || !strings.HasSuffix(e.Name(), ".json") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := strings.TrimSuffix(e.Name(), ".json")
|
|
||||||
data, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var spec targetSpec
|
|
||||||
if err := json.Unmarshal(data, &spec); err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing %s: %w", e.Name(), err)
|
|
||||||
}
|
|
||||||
specs[name] = &spec
|
|
||||||
}
|
|
||||||
return specs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveSpec(name string, raw map[string]*targetSpec, cache map[string]*targetSpec, resolving map[string]bool) (*targetSpec, error) {
|
|
||||||
if cached, ok := cache[name]; ok {
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
if resolving[name] {
|
|
||||||
return nil, fmt.Errorf("circular inheritance: %s", name)
|
|
||||||
}
|
|
||||||
spec, ok := raw[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unknown target: %s", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolving[name] = true
|
|
||||||
defer delete(resolving, name)
|
|
||||||
|
|
||||||
result := &targetSpec{}
|
|
||||||
for _, parent := range spec.Inherits {
|
|
||||||
resolved, err := resolveSpec(parent, raw, cache, resolving)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("resolving %s parent %s: %w", name, parent, err)
|
|
||||||
}
|
|
||||||
result.override(resolved)
|
|
||||||
}
|
|
||||||
result.override(spec)
|
|
||||||
|
|
||||||
cache[name] = result
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadTargets reads all target JSONs from dir, resolves inheritance,
|
|
||||||
// and returns the resolved targets. If includeBase is false, targets
|
|
||||||
// without flash/emulator configuration are excluded (base/parent targets).
|
|
||||||
func LoadTargets(dir string, includeBase bool) ([]Target, error) {
|
|
||||||
raw, err := loadRawTargets(dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cache := make(map[string]*targetSpec)
|
|
||||||
resolving := make(map[string]bool)
|
|
||||||
|
|
||||||
var targets []Target
|
|
||||||
for name := range raw {
|
|
||||||
resolved, err := resolveSpec(name, raw, cache, resolving)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !includeBase && resolved.FlashMethod == "" && resolved.FlashCommand == "" && resolved.Emulator == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
targets = append(targets, Target{
|
|
||||||
Name: name,
|
|
||||||
BuildTags: resolved.BuildTags,
|
|
||||||
GOOS: resolved.GOOS,
|
|
||||||
GOARCH: resolved.GOARCH,
|
|
||||||
GC: resolved.GC,
|
|
||||||
Scheduler: resolved.Scheduler,
|
|
||||||
Serial: resolved.Serial,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(targets, func(i, j int) bool { return targets[i].Name < targets[j].Name })
|
|
||||||
return targets, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user