Compare commits

..

3 Commits

Author SHA1 Message Date
Patricio Whittingslow cc959c5aab fix targets list 2026-02-25 12:49:07 -03:00
Patricio Whittingslow ef77ed3103 show number of different APIs in red 2026-02-25 12:29:37 -03:00
Patricio Whittingslow ca2694c5d9 add tgdoc tool 2026-02-25 12:21:09 -03:00
8 changed files with 1344 additions and 355 deletions
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
-102
View File
@@ -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
View File
@@ -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)
}
+124
View File
@@ -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
}
+172
View File
@@ -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)
}
}
+448
View File
@@ -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>`))
+168
View File
@@ -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
}