mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 22:58:41 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc959c5aab | |||
| ef77ed3103 | |||
| ca2694c5d9 |
@@ -135,7 +135,7 @@ var (
|
||||
|
||||
// FDCAN1 on PD0 (RX) / PD1 (TX) with onboard transceiver
|
||||
CAN1 = &_CAN1
|
||||
_CAN1 = CAN{
|
||||
_CAN1 = FDCAN{
|
||||
Bus: stm32.FDCAN1,
|
||||
TxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
||||
RxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
||||
|
||||
@@ -107,7 +107,7 @@ var (
|
||||
|
||||
// FDCAN1 on PA11 (TX) / PA12 (RX)
|
||||
CAN1 = &_CAN1
|
||||
_CAN1 = CAN{
|
||||
_CAN1 = FDCAN{
|
||||
Bus: stm32.FDCAN1,
|
||||
TxAltFuncSelect: AF9_FDCAN1_FDCAN2,
|
||||
RxAltFuncSelect: AF9_FDCAN1_FDCAN2,
|
||||
@@ -116,7 +116,7 @@ var (
|
||||
|
||||
// FDCAN2 on PD12 (TX) / PD13 (RX)
|
||||
CAN2 = &_CAN2
|
||||
_CAN2 = CAN{
|
||||
_CAN2 = FDCAN{
|
||||
Bus: stm32.FDCAN2,
|
||||
TxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
||||
RxAltFuncSelect: AF3_FDCAN1_FDCAN2,
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
//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
|
||||
}
|
||||
+429
-250
@@ -9,8 +9,6 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Exported API in src/machine/can.go
|
||||
|
||||
// FDCAN Message RAM configuration
|
||||
// STM32G0B1 SRAMCAN base address: 0x4000B400
|
||||
// Each FDCAN instance has its own message RAM area
|
||||
@@ -76,53 +74,78 @@ const (
|
||||
FDCAN_IT_ERROR_PASSIVE = 0x00800000
|
||||
)
|
||||
|
||||
// CAN is a STM32G0's CAN/FDCAN peripheral.
|
||||
type CAN struct {
|
||||
// FDCAN represents an FDCAN peripheral
|
||||
type FDCAN struct {
|
||||
Bus *stm32.FDCAN_Type
|
||||
TxAltFuncSelect uint8
|
||||
RxAltFuncSelect uint8
|
||||
Interrupt interrupt.Interrupt
|
||||
instance uint8
|
||||
alwaysFD bool
|
||||
rxInterrupt bool
|
||||
}
|
||||
|
||||
// CANTransferRate represents CAN bus transfer rates
|
||||
type CANTransferRate uint32
|
||||
// FDCANTransferRate represents CAN bus transfer rates
|
||||
type FDCANTransferRate uint32
|
||||
|
||||
const (
|
||||
FDCANTransferRate125kbps CANTransferRate = 125000
|
||||
FDCANTransferRate250kbps CANTransferRate = 250000
|
||||
FDCANTransferRate500kbps CANTransferRate = 500000
|
||||
FDCANTransferRate1000kbps CANTransferRate = 1000000
|
||||
FDCANTransferRate2000kbps CANTransferRate = 2000000 // FD only
|
||||
FDCANTransferRate4000kbps CANTransferRate = 4000000 // FD only
|
||||
FDCANTransferRate125kbps FDCANTransferRate = 125000
|
||||
FDCANTransferRate250kbps FDCANTransferRate = 250000
|
||||
FDCANTransferRate500kbps FDCANTransferRate = 500000
|
||||
FDCANTransferRate1000kbps FDCANTransferRate = 1000000
|
||||
FDCANTransferRate2000kbps FDCANTransferRate = 2000000 // FD only
|
||||
FDCANTransferRate4000kbps FDCANTransferRate = 4000000 // FD only
|
||||
)
|
||||
|
||||
// CANMode represents the FDCAN operating mode
|
||||
type CANMode uint8
|
||||
// FDCANMode represents the FDCAN operating mode
|
||||
type FDCANMode uint8
|
||||
|
||||
const (
|
||||
CANModeNormal CANMode = 0
|
||||
CANModeBusMonitoring CANMode = 1
|
||||
CANModeInternalLoopback CANMode = 2
|
||||
CANModeExternalLoopback CANMode = 3
|
||||
FDCANModeNormal FDCANMode = 0
|
||||
FDCANModeBusMonitoring FDCANMode = 1
|
||||
FDCANModeInternalLoopback FDCANMode = 2
|
||||
FDCANModeExternalLoopback FDCANMode = 3
|
||||
)
|
||||
|
||||
// CANConfig holds FDCAN configuration parameters
|
||||
type CANConfig struct {
|
||||
TransferRate CANTransferRate // Nominal bit rate (arbitration phase)
|
||||
TransferRateFD CANTransferRate // Data bit rate (data phase), must be >= TransferRate
|
||||
Mode CANMode
|
||||
Tx Pin
|
||||
Rx Pin
|
||||
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)
|
||||
// FDCANConfig holds FDCAN configuration parameters
|
||||
type FDCANConfig struct {
|
||||
TransferRate FDCANTransferRate // Nominal bit rate (arbitration phase)
|
||||
TransferRateFD FDCANTransferRate // Data bit rate (data phase), must be >= TransferRate
|
||||
Mode FDCANMode
|
||||
Tx Pin
|
||||
Rx Pin
|
||||
Standby Pin // Optional standby pin for CAN transceiver (set to NoPin if not used)
|
||||
}
|
||||
|
||||
// CANFilterConfig represents a message filter configuration
|
||||
type CANFilterConfig struct {
|
||||
// FDCANTxBufferElement represents a transmit buffer element
|
||||
type FDCANTxBufferElement 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)
|
||||
Type uint8 // 0=Range, 1=Dual, 2=Classic (ID/Mask)
|
||||
Config uint8 // 0=Disable, 1=FIFO0, 2=FIFO1, 3=Reject
|
||||
@@ -132,350 +155,401 @@ type CANFilterConfig struct {
|
||||
}
|
||||
|
||||
var (
|
||||
errCANInvalidTransferRate = errors.New("CAN: invalid TransferRate")
|
||||
errCANInvalidTransferRateFD = errors.New("CAN: invalid TransferRateFD")
|
||||
errCANTimeout = errors.New("CAN: timeout")
|
||||
errCANTxFifoFull = errors.New("CAN: Tx FIFO full")
|
||||
errFDCANInvalidTransferRate = errors.New("FDCAN: invalid TransferRate")
|
||||
errFDCANInvalidTransferRateFD = errors.New("FDCAN: invalid TransferRateFD")
|
||||
errFDCANTimeout = errors.New("FDCAN: timeout")
|
||||
errFDCANTxFifoFull = errors.New("FDCAN: Tx FIFO full")
|
||||
errFDCANRxFifoEmpty = errors.New("FDCAN: Rx FIFO empty")
|
||||
errFDCANNotStarted = errors.New("FDCAN: not started")
|
||||
)
|
||||
|
||||
// enableFDCANClock enables the FDCAN peripheral clock
|
||||
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
|
||||
// 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}
|
||||
|
||||
// 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 {
|
||||
config.Standby.Configure(PinConfig{Mode: PinOutput})
|
||||
config.Standby.Low()
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// Exit sleep mode.
|
||||
// Exit from sleep mode
|
||||
can.Bus.SetCCCR_CSR(0)
|
||||
|
||||
// Wait for sleep mode exit
|
||||
timeout := 10000
|
||||
for can.Bus.GetCCCR_CSA() != 0 {
|
||||
timeout--
|
||||
if timeout == 0 {
|
||||
return errCANTimeout
|
||||
return errFDCANTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// Request initialization.
|
||||
// Request initialization
|
||||
can.Bus.SetCCCR_INIT(1)
|
||||
|
||||
// Wait for init mode
|
||||
timeout = 10000
|
||||
for can.Bus.GetCCCR_INIT() == 0 {
|
||||
timeout--
|
||||
if timeout == 0 {
|
||||
return errCANTimeout
|
||||
return errFDCANTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// Enable configuration change.
|
||||
// Enable configuration change
|
||||
can.Bus.SetCCCR_CCE(1)
|
||||
|
||||
// Configure clock divider (only for FDCAN1)
|
||||
if can.Bus == stm32.FDCAN1 {
|
||||
can.Bus.SetCKDIV_PDIV(0) // No clock division.
|
||||
can.Bus.SetCKDIV_PDIV(0)
|
||||
//can.Bus.CKDIV.Set(0) // No division
|
||||
}
|
||||
|
||||
can.Bus.SetCCCR_DAR(0) // Enable auto retransmission.
|
||||
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.
|
||||
// Enable automatic retransmission
|
||||
can.Bus.SetCCCR_DAR(0)
|
||||
|
||||
// Reset mode bits, then apply requested mode.
|
||||
// Disable transmit pause
|
||||
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_MON(0)
|
||||
can.Bus.SetCCCR_ASM(0)
|
||||
can.Bus.SetTEST_LBCK(0)
|
||||
|
||||
switch config.Mode {
|
||||
case CANModeBusMonitoring:
|
||||
case FDCANModeBusMonitoring:
|
||||
can.Bus.SetCCCR_MON(1)
|
||||
case CANModeInternalLoopback:
|
||||
case FDCANModeInternalLoopback:
|
||||
can.Bus.SetCCCR_TEST(1)
|
||||
can.Bus.SetCCCR_MON(1)
|
||||
can.Bus.SetTEST_LBCK(1)
|
||||
case CANModeExternalLoopback:
|
||||
case FDCANModeExternalLoopback:
|
||||
can.Bus.SetCCCR_TEST(1)
|
||||
can.Bus.SetTEST_LBCK(1)
|
||||
}
|
||||
|
||||
// Nominal bit timing (64 MHz FDCAN clock, 16 tq/bit, ~80% sample point).
|
||||
// Set nominal bit timing
|
||||
// STM32G0 runs at 64MHz, FDCAN clock = PCLK = 64MHz
|
||||
// Bit time = (1 + NTSEG1 + NTSEG2) * tq
|
||||
// tq = (NBRP + 1) / fCAN_CLK
|
||||
if config.TransferRate == 0 {
|
||||
config.TransferRate = FDCANTransferRate500kbps
|
||||
}
|
||||
nbrp, ntseg1, ntseg2, nsjw, err := fdcanNominalBitTiming(config.TransferRate)
|
||||
|
||||
nbrp, ntseg1, ntseg2, nsjw, err := can.calculateNominalBitTiming(config.TransferRate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
can.Bus.NBTP.Set(((nsjw - 1) << 25) | ((nbrp - 1) << 16) | ((ntseg1 - 1) << 8) | (ntseg2 - 1))
|
||||
|
||||
// Data bit timing (FD phase).
|
||||
// Set data bit timing (for FD mode)
|
||||
if config.TransferRateFD == 0 {
|
||||
config.TransferRateFD = FDCANTransferRate1000kbps
|
||||
}
|
||||
if config.TransferRateFD < config.TransferRate {
|
||||
return errCANInvalidTransferRateFD
|
||||
return errFDCANInvalidTransferRateFD
|
||||
}
|
||||
dbrp, dtseg1, dtseg2, dsjw, err := fdcanDataBitTiming(config.TransferRateFD)
|
||||
|
||||
dbrp, dtseg1, dtseg2, dsjw, err := can.calculateDataBitTiming(config.TransferRateFD)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
can.Bus.DBTP.Set(((dbrp - 1) << 16) | ((dtseg1 - 1) << 8) | ((dtseg2 - 1) << 4) | (dsjw - 1))
|
||||
|
||||
// Enable timestamp counter (internal, prescaler=1).
|
||||
can.Bus.TSCC.Set(1)
|
||||
// Configure message RAM
|
||||
can.configureMessageRAM()
|
||||
|
||||
// Clear message RAM.
|
||||
base := can.sramBase()
|
||||
for addr := base; addr < base+sramcanSize; addr += 4 {
|
||||
*(*uint32)(unsafe.Pointer(addr)) = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set filter list sizes: LSS[20:16], LSE[27:24].
|
||||
rxgfc := can.Bus.RXGFC.Get()
|
||||
rxgfc &= ^uint32(0x0F1F0000)
|
||||
rxgfc |= uint32(sramcanFLSNbr) << 16
|
||||
rxgfc |= uint32(sramcanFLENbr) << 24
|
||||
can.Bus.RXGFC.Set(rxgfc)
|
||||
|
||||
// Start peripheral.
|
||||
// Start enables the FDCAN peripheral for communication
|
||||
func (can *FDCAN) Start() error {
|
||||
// Disable configuration change
|
||||
can.Bus.SetCCCR_CCE(0)
|
||||
|
||||
// Exit initialization mode
|
||||
can.Bus.SetCCCR_INIT(0)
|
||||
timeout = 10000
|
||||
|
||||
// Wait for normal operation
|
||||
timeout := 10000
|
||||
|
||||
for can.Bus.GetCCCR_INIT() != 0 {
|
||||
timeout--
|
||||
if timeout == 0 {
|
||||
return errCANTimeout
|
||||
return errFDCANTimeout
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop puts the FDCAN peripheral back into initialization mode.
|
||||
func (can *CAN) Stop() error {
|
||||
// Stop disables the FDCAN peripheral
|
||||
func (can *FDCAN) Stop() error {
|
||||
// Request initialization
|
||||
can.Bus.SetCCCR_INIT(1)
|
||||
|
||||
// Wait for init mode
|
||||
timeout := 10000
|
||||
for can.Bus.GetCCCR_INIT() == 0 {
|
||||
timeout--
|
||||
if timeout == 0 {
|
||||
return errCANTimeout
|
||||
return errFDCANTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// Enable configuration change
|
||||
can.Bus.SetCCCR_CCE(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// txFIFOLevel implements [CAN.TxFIFOLevel].
|
||||
func (can *CAN) txFIFOLevel() (int, int) {
|
||||
free := int(can.Bus.TXFQS.Get() & 0x07) // TFFL[2:0]
|
||||
return sramcanTFQNbr - free, sramcanTFQNbr
|
||||
// TxFifoIsFull returns true if the TX FIFO is full
|
||||
func (can *FDCAN) TxFifoIsFull() bool {
|
||||
return (can.Bus.TXFQS.Get() & 0x00200000) != 0 // TFQF bit
|
||||
}
|
||||
|
||||
// tx implements [CAN.Tx].
|
||||
func (can *CAN) tx(id canID, flags canFlags, data []byte) error {
|
||||
if can.Bus.TXFQS.Get()&0x00200000 != 0 { // TFQF bit
|
||||
return errCANTxFifoFull
|
||||
// TxFifoFreeLevel returns the number of free TX FIFO elements
|
||||
func (can *FDCAN) TxFifoFreeLevel() int {
|
||||
return int(can.Bus.TXFQS.Get() & 0x07) // TFFL[2:0]
|
||||
}
|
||||
|
||||
// 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))
|
||||
if length > 64 {
|
||||
length = 64
|
||||
}
|
||||
if !isFD && length > 8 {
|
||||
length = 8
|
||||
}
|
||||
|
||||
// Use FD framing if configured to always use FD, or if data exceeds classic CAN max.
|
||||
isFD := flags&canFlagFDF != 0 || length > 8
|
||||
e := FDCANTxBufferElement{
|
||||
ESI: false,
|
||||
XTD: isExtendedID,
|
||||
RTR: false,
|
||||
ID: id,
|
||||
MM: 0,
|
||||
EFC: false,
|
||||
FDF: isFD,
|
||||
BRS: isFD,
|
||||
DLC: FDCANLengthToDlc(length, isFD),
|
||||
}
|
||||
|
||||
putIndex := (can.Bus.TXFQS.Get() >> 16) & 0x03 // TFQPI[1:0]
|
||||
txAddr := can.sramBase() + sramcanTFQSA + uintptr(putIndex)*sramcanTFQSize
|
||||
for i := byte(0); i < length; i++ {
|
||||
e.DB[i] = data[i]
|
||||
}
|
||||
|
||||
// Header word 1: identifier and flags.
|
||||
var w1 uint32
|
||||
if flags&canFlagESI != 0 {
|
||||
w1 = (id & 0x1FFFFFFF) | fdcanElementMaskXTD
|
||||
return can.TxRaw(&e)
|
||||
}
|
||||
|
||||
// RxRaw receives a CAN frame into the raw buffer element structure
|
||||
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 {
|
||||
w1 = (id & 0x7FF) << 18
|
||||
e.ID = (w1 & fdcanElementMaskSTDID) >> 18
|
||||
}
|
||||
|
||||
// Header word 2: DLC, FD/BRS flags.
|
||||
dlc := lengthToDLC(length)
|
||||
w2 := uint32(dlc) << 16
|
||||
if isFD {
|
||||
w2 |= fdcanElementMaskFDF | fdcanElementMaskBRS
|
||||
}
|
||||
// Read second word
|
||||
w2 := *(*uint32)(unsafe.Pointer(rxAddress + 4))
|
||||
e.RXTS = uint16(w2 & fdcanElementMaskTS)
|
||||
e.DLC = uint8((w2 & fdcanElementMaskDLC) >> 16)
|
||||
e.BRS = (w2 & fdcanElementMaskBRS) != 0
|
||||
e.FDF = (w2 & fdcanElementMaskFDF) != 0
|
||||
e.FIDX = uint8((w2 & fdcanElementMaskFIDX) >> 24)
|
||||
e.ANMF = (w2 & fdcanElementMaskANMF) != 0
|
||||
|
||||
*(*uint32)(unsafe.Pointer(txAddr)) = w1
|
||||
*(*uint32)(unsafe.Pointer(txAddr + 4)) = w2
|
||||
|
||||
// Copy data with 32-bit word access (Cortex-M0+).
|
||||
for w := byte(0); w < (length+3)/4; w++ {
|
||||
var word uint32
|
||||
base := w * 4
|
||||
for b := byte(0); b < 4 && base+b < length; b++ {
|
||||
word |= uint32(data[base+b]) << (b * 8)
|
||||
// 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++ {
|
||||
word := *(*uint32)(unsafe.Pointer(rxAddress + 8 + uintptr(w)*4))
|
||||
baseIdx := w * 4
|
||||
for b := byte(0); b < 4 && baseIdx+b < dataLen; b++ {
|
||||
e.DB[baseIdx+b] = byte(word >> (b * 8))
|
||||
}
|
||||
*(*uint32)(unsafe.Pointer(txAddr + 8 + uintptr(w)*4)) = word
|
||||
}
|
||||
|
||||
can.Bus.TXBAR.Set(1 << putIndex)
|
||||
// Acknowledge the read
|
||||
can.Bus.RXF0A.Set(uint32(getIndex))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// rxFIFOLevel implements [CAN.RxFIFOLevel].
|
||||
// Returns 0,0 when interrupt-driven (messages delivered via callback).
|
||||
func (can *CAN) rxFIFOLevel() (int, int) {
|
||||
if canInstances[can.instance] != nil {
|
||||
return 0, 0
|
||||
// Rx receives a CAN frame and returns its components
|
||||
func (can *FDCAN) Rx() (id uint32, dlc byte, data []byte, isFD, isExtendedID bool, err error) {
|
||||
e := FDCANRxBufferElement{}
|
||||
err = can.RxRaw(&e)
|
||||
if err != nil {
|
||||
return 0, 0, nil, false, false, err
|
||||
}
|
||||
level := int(can.Bus.RXF0S.Get() & 0x0F) // F0FL[3:0]
|
||||
return level, sramcanRF0Nbr
|
||||
|
||||
length := FDCANDlcToLength(e.DLC, e.FDF)
|
||||
return e.ID, length, e.DB[:length], e.FDF, e.XTD, nil
|
||||
}
|
||||
|
||||
// setRxCallback implements [CAN.SetRxCallback].
|
||||
// When cb is non-nil, interrupt-driven receive is enabled on RX FIFO 0.
|
||||
// The CAN.Interrupt field must be initialized with interrupt.New in the board file.
|
||||
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
|
||||
// SetInterrupt configures interrupt handling for the FDCAN peripheral
|
||||
func (can *FDCAN) SetInterrupt(ie uint32, callback func(*FDCAN)) error {
|
||||
if callback == nil {
|
||||
can.Bus.IE.ClearBits(ie)
|
||||
return 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
|
||||
can.Bus.IE.SetBits(ie)
|
||||
|
||||
idx := can.instance
|
||||
fdcanInstances[idx] = can
|
||||
|
||||
for i := uint(0); i < 32; i++ {
|
||||
if ie&(1<<i) != 0 {
|
||||
fdcanCallbacks[idx][i] = callback
|
||||
}
|
||||
}
|
||||
cb := canRxCB[can.instance]
|
||||
if cb == nil {
|
||||
return nil
|
||||
}
|
||||
processRxFIFO0(can, cb)
|
||||
|
||||
can.Interrupt.Enable()
|
||||
return nil
|
||||
}
|
||||
|
||||
// processRxFIFO0 drains RX FIFO 0 and delivers each message to cb.
|
||||
// Used by both rxPoll (poll mode) and canHandleInterrupt (interrupt mode).
|
||||
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
|
||||
|
||||
w1 := *(*uint32)(unsafe.Pointer(rxAddr))
|
||||
w2 := *(*uint32)(unsafe.Pointer(rxAddr + 4))
|
||||
|
||||
extendedID := w1&fdcanElementMaskXTD != 0
|
||||
var id uint32
|
||||
var flags uint32
|
||||
if extendedID {
|
||||
flags |= canFlagIDE
|
||||
id = w1 & fdcanElementMaskEXTID
|
||||
} else {
|
||||
id = (w1 & fdcanElementMaskSTDID) >> 18
|
||||
}
|
||||
|
||||
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()
|
||||
// ConfigureFilter configures a message filter
|
||||
func (can *FDCAN) ConfigureFilter(config FDCANFilterConfig) error {
|
||||
sramBase := can.getSRAMBase()
|
||||
|
||||
if config.IsExtendedID {
|
||||
// Extended filter
|
||||
if config.Index >= sramcanFLENbr {
|
||||
return errors.New("CAN: filter index out of range")
|
||||
return errors.New("FDCAN: filter index out of range")
|
||||
}
|
||||
|
||||
filterAddr := base + sramcanFLESA + (uintptr(config.Index) * sramcanFLESize)
|
||||
filterAddr := sramBase + sramcanFLESA + (uintptr(config.Index) * sramcanFLESize)
|
||||
|
||||
// Build filter elements
|
||||
w1 := (uint32(config.Config) << 29) | (config.ID1 & 0x1FFFFFFF)
|
||||
w2 := (uint32(config.Type) << 30) | (config.ID2 & 0x1FFFFFFF)
|
||||
|
||||
*(*uint32)(unsafe.Pointer(filterAddr)) = w1
|
||||
*(*uint32)(unsafe.Pointer(filterAddr + 4)) = w2
|
||||
} else {
|
||||
// Standard filter
|
||||
if config.Index >= sramcanFLSNbr {
|
||||
return errors.New("CAN: filter index out of range")
|
||||
return errors.New("FDCAN: filter index out of range")
|
||||
}
|
||||
|
||||
filterAddr := base + sramcanFLSSA + (uintptr(config.Index) * sramcanFLSSize)
|
||||
filterAddr := sramBase + sramcanFLSSA + (uintptr(config.Index) * sramcanFLSSize)
|
||||
|
||||
// Build filter element
|
||||
w := (uint32(config.Type) << 30) |
|
||||
(uint32(config.Config) << 27) |
|
||||
((config.ID1 & 0x7FF) << 16) |
|
||||
@@ -487,32 +561,56 @@ func (can *CAN) ConfigureFilter(config CANFilterConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (can *CAN) sramBase() uintptr {
|
||||
func (can *FDCAN) getSRAMBase() uintptr {
|
||||
base := uintptr(sramcanBase)
|
||||
if can.Bus == stm32.FDCAN2 {
|
||||
return uintptr(sramcanBase) + sramcanSize
|
||||
base += sramcanSize
|
||||
}
|
||||
return uintptr(sramcanBase)
|
||||
return base
|
||||
}
|
||||
|
||||
// fdcanNominalBitTiming returns prescaler and segment values for the nominal (arbitration) phase.
|
||||
// STM32G0 FDCAN clock = 64 MHz, 16 time quanta per bit, ~80% sample point.
|
||||
func fdcanNominalBitTiming(rate CANTransferRate) (brp, tseg1, tseg2, sjw uint32, err error) {
|
||||
func (can *FDCAN) configureMessageRAM() {
|
||||
sramBase := can.getSRAMBase()
|
||||
|
||||
// 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 {
|
||||
case FDCANTransferRate125kbps:
|
||||
// 64MHz / 32 = 2MHz, 16 tq per bit = 125kbps
|
||||
return 32, 13, 2, 4, nil
|
||||
case FDCANTransferRate250kbps:
|
||||
// 64MHz / 16 = 4MHz, 16 tq per bit = 250kbps
|
||||
return 16, 13, 2, 4, nil
|
||||
case FDCANTransferRate500kbps:
|
||||
// 64MHz / 8 = 8MHz, 16 tq per bit = 500kbps
|
||||
return 8, 13, 2, 4, nil
|
||||
case FDCANTransferRate1000kbps:
|
||||
// 64MHz / 4 = 16MHz, 16 tq per bit = 1Mbps
|
||||
return 4, 13, 2, 4, nil
|
||||
default:
|
||||
return 0, 0, 0, 0, errCANInvalidTransferRate
|
||||
return 0, 0, 0, 0, errFDCANInvalidTransferRate
|
||||
}
|
||||
}
|
||||
|
||||
// fdcanDataBitTiming returns prescaler and segment values for the data phase (FD).
|
||||
func fdcanDataBitTiming(rate CANTransferRate) (brp, tseg1, tseg2, sjw uint32, err error) {
|
||||
func (can *FDCAN) calculateDataBitTiming(rate FDCANTransferRate) (brp, tseg1, tseg2, sjw uint32, err error) {
|
||||
// STM32G0 FDCAN clock = 64MHz
|
||||
// For data phase, we need higher bit rates
|
||||
switch rate {
|
||||
case FDCANTransferRate125kbps:
|
||||
return 32, 13, 2, 4, nil
|
||||
@@ -523,10 +621,91 @@ func fdcanDataBitTiming(rate CANTransferRate) (brp, tseg1, tseg2, sjw uint32, er
|
||||
case FDCANTransferRate1000kbps:
|
||||
return 4, 13, 2, 4, nil
|
||||
case FDCANTransferRate2000kbps:
|
||||
// 64MHz / 2 = 32MHz, 16 tq per bit = 2Mbps
|
||||
return 2, 13, 2, 4, nil
|
||||
case FDCANTransferRate4000kbps:
|
||||
// 64MHz / 1 = 64MHz, 16 tq per bit = 4Mbps
|
||||
return 1, 13, 2, 4, nil
|
||||
default:
|
||||
return 0, 0, 0, 0, errCANInvalidTransferRateFD
|
||||
return 0, 0, 0, 0, errFDCANInvalidTransferRateFD
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
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>`))
|
||||
@@ -0,0 +1,168 @@
|
||||
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