#ifndef _CH32V307GIGABIT_H #define _CH32V307GIGABIT_H /* This file is written against the RTL8211E */ // #define CH32V307GIGABIT_MCO25 1 // #define CH32V307GIGABIT_PHYADDRESS 0 #define CH32V307GIGABIT_RXBUFNB 8 #define CH32V307GIGABIT_TXBUFNB 8 #define CH32V307GIGABIT_BUFFSIZE 1524 // 1518 + 4, Rounded up. #define CH32V307GIGABIT_CFG_CLOCK_DELAY 4 // 0..7 #define CH32V307GIGABIT_CFG_CLOCK_PHASE 0 #ifndef CH32V307GIGABIT_MCO25 #define CH32V307GIGABIT_MCO25 1 #endif #ifndef CH32V307GIGABIT_PHYADDRESS #define CH32V307GIGABIT_PHYADDRESS 0 #endif #ifndef CH32V307GIGABIT_PHY_TIMEOUT #define CH32V307GIGABIT_PHY_TIMEOUT 0x10000 #endif // Additional definitions, not part of ch32v003fun.h #ifndef CH32V307GIGABIT_PHY_RSTB #define CH32V307GIGABIT_PHY_RSTB PA10 #endif // ETH DMA structure definition (From ch32v30x_eth.c typedef struct { uint32_t volatile Status; /* Status */ uint32_t ControlBufferSize; /* Control and Buffer1, Buffer2 lengths */ uint32_t Buffer1Addr; /* Buffer1 address pointer */ uint32_t Buffer2NextDescAddr; /* Buffer2 or next descriptor address pointer */ } ETH_DMADESCTypeDef; // You must provide: void ch32v307ethHandleReconfig(int link, int speed, int duplex); // Return non-zero to suppress OWN return (for if you are still holding onto the buffer) int ch32v307ethInitHandlePacket(uint8_t *data, int frame_length, ETH_DMADESCTypeDef *dmadesc); void ch32v307ethInitHandleTXC(void); // This library provides: static void ch32v307ethGetMacInUC(uint8_t *mac); static int ch32v307ethInit(void); static int ch32v307ethTransmitStatic(uint8_t *buffer, uint32_t length, int enable_txc); // Does not copy. static int ch32v307ethTickPhy(void); // Data pursuent to ethernet. uint8_t ch32v307eth_mac[6] = {0}; uint16_t ch32v307eth_phyid = 0; // 0xc916 = RTL8211FS / 0xc915 = RTL8211E-VB ETH_DMADESCTypeDef ch32v307eth_DMARxDscrTab[CH32V307GIGABIT_RXBUFNB] __attribute__((aligned(4))); // MAC receive descriptor, 4-byte aligned ETH_DMADESCTypeDef ch32v307eth_DMATxDscrTab[CH32V307GIGABIT_TXBUFNB] __attribute__((aligned(4))); // MAC send descriptor, 4-byte aligned uint8_t ch32v307eth_MACRxBuf[CH32V307GIGABIT_RXBUFNB * CH32V307GIGABIT_BUFFSIZE] __attribute__((aligned(4))); // MAC receive buffer, 4-byte aligned ETH_DMADESCTypeDef *pDMARxGet; ETH_DMADESCTypeDef *pDMATxSet; // Internal functions static int ch32v307ethPHYRegWrite(uint32_t reg, uint32_t val); static int ch32v307ethPHYRegAsyncRead(int reg, int *value); static int ch32v307ethPHYRegRead(uint32_t reg); static int ch32v307ethPHYRegAsyncRead(int reg, int *value) { static uint8_t reg_request_count = 0; uint32_t miiar = ETH->MACMIIAR; if (miiar & ETH_MACMIIAR_MB) { return -1; } if (((miiar & ETH_MACMIIAR_MR) >> 6) != reg || reg_request_count < 2) { ETH->MACMIIAR = ETH_MACMIIAR_CR_Div42 /* = 0, per 27.1.8.1.4 */ | ((uint32_t)CH32V307GIGABIT_PHYADDRESS << 11) | // ETH_MACMIIAR_PA (((uint32_t)reg << 6) & ETH_MACMIIAR_MR) | (0 /*!ETH_MACMIIAR_MW*/) | ETH_MACMIIAR_MB; reg_request_count++; return -1; } reg_request_count = 0; *value = ETH->MACMIIDR; ETH->MACMIIAR |= ETH_MACMIIAR_MR; // Poison register. return 0; } static int ch32v307ethTickPhy(void) { int speed, linked, duplex; const int reg = (ch32v307eth_phyid == 0xc916) ? 0x1a : 0x11; // PHYSR (different on each part) int miidr; if (ch32v307ethPHYRegAsyncRead(reg, &miidr)) return -1; printf("REG: %02x / %04x / %04x\n", reg, miidr, ch32v307eth_phyid); if (reg == 0x1a) { speed = ((miidr >> 4) & 3); linked = ((miidr >> 2) & 1); duplex = ((miidr >> 3) & 1); } else { speed = ((miidr >> 14) & 3); linked = ((miidr >> 10) & 1); duplex = ((miidr >> 13) & 1); } printf("LINK INFO: %d %d %d\n", speed, linked, duplex); if (linked) { uint32_t oldmaccr = ETH->MACCR; uint32_t newmaccr = (oldmaccr & ~((1 << 11) | (3 << 14))) | (speed << 14) | (duplex << 11); if (newmaccr != oldmaccr) { ETH->MACCR = newmaccr; ch32v307ethHandleReconfig(linked, speed, duplex); return 1; } } return 0; } // Based on ETH_WritePHYRegister static int ch32v307ethPHYRegWrite(uint32_t reg, uint32_t val) { ETH->MACMIIDR = val; ETH->MACMIIAR = ETH_MACMIIAR_CR_Div42 /* = 0, per 27.1.8.1.4 */ | (((uint32_t)CH32V307GIGABIT_PHYADDRESS << 11)) | // ETH_MACMIIAR_PA (((uint32_t)reg << 6) & ETH_MACMIIAR_MR) | ETH_MACMIIAR_MW | ETH_MACMIIAR_MB; uint32_t timeout = 0x100000; while ((ETH->MACMIIAR & ETH_MACMIIAR_MB) && --timeout) ; // If timeout = 0, is an error. return timeout ? 0 : -1; } static int ch32v307ethPHYRegRead(uint32_t reg) { ETH->MACMIIAR = ETH_MACMIIAR_CR_Div42 /* = 0, per 27.1.8.1.4 */ | ((uint32_t)CH32V307GIGABIT_PHYADDRESS << 11) | // ETH_MACMIIAR_PA (((uint32_t)reg << 6) & ETH_MACMIIAR_MR) | (0 /*!ETH_MACMIIAR_MW*/) | ETH_MACMIIAR_MB; uint32_t timeout = 0x100000; while ((ETH->MACMIIAR & ETH_MACMIIAR_MB) && --timeout) ; // If timeout = 0, is an error. return timeout ? ETH->MACMIIDR : -1; } static void ch32v307ethGetMacInUC(uint8_t *mac) { // Mac is backwards. const uint8_t *macaddr = (const uint8_t *)(ROM_CFG_USERADR_ID + 5); for (int i = 0; i < 6; i++) { mac[i] = *(macaddr--); } } static int ch32v307ethInit(void) { int i; #ifdef CH32V307GIGABIT_PHY_RSTB funPinMode(CH32V307GIGABIT_PHY_RSTB, GPIO_CFGLR_OUT_50Mhz_PP); // PHY_RSTB (For reset) funDigitalWrite(CH32V307GIGABIT_PHY_RSTB, FUN_LOW); #endif // Configure strapping. funPinMode(PA1, GPIO_CFGLR_IN_PUPD); // GMII_RXD3 funPinMode(PA0, GPIO_CFGLR_IN_PUPD); // GMII_RXD2 funPinMode(PC3, GPIO_CFGLR_IN_PUPD); // GMII_RXD1 funPinMode(PC2, GPIO_CFGLR_IN_PUPD); // GMII_RXD0 funDigitalWrite(PA1, FUN_HIGH); funDigitalWrite(PA0, FUN_HIGH); funDigitalWrite(PC3, FUN_HIGH); // No TX Delay funDigitalWrite(PC2, FUN_HIGH); // Pull-up MDIO funPinMode(PD9, GPIO_CFGLR_OUT_50Mhz_PP); // Pull-up control (DO NOT CHECK IN, ADD RESISTOR) funDigitalWrite(PD9, FUN_HIGH); // Will be required later. RCC->APB2PCENR |= RCC_APB2Periph_AFIO; // https://cnlohr.github.io/microclockoptimizer/?chipSelect=ch32vx05_7%2Cd8c&HSI=1,8&HSE=0,8&PREDIV2=1,1&PLL2CLK=1,7&PLL2VCO=0,72&PLL3CLK=1,1&PLL3VCO=0,100&PREDIV1SRC=1,0&PREDIV1=1,2&PLLSRC=1,0&PLL=0,4&PLLVCO=1,144&SYSCLK=1,2& // Clock Tree: // 8MHz Input // PREDIV2 = 2 (1 in register) = 4MHz // PLL2 = 9 (7 in register) = 36MHz / PLL2VCO = 72MHz // PLL3CLK = 12.5 (1 in register) = 50MHz = 100MHz VCO // PREDIV1SRC = HSE (1 in register) = 8MHz // PREDIV1 = 2 (1 in register). // PLLSRC = PREDIV1 (0 in register) = 4MHz // PLL = 18 (0 in register) = 72MHz // PLLVCO = 144MHz // SYSCLK = PLLVCO = 144MHz // Use EXT_125M (ETH1G_SRC) // Switch processor back to HSI so we don't eat dirt. RCC->CFGR0 = (RCC->CFGR0 & ~RCC_SW) | RCC_SW_HSI; // Setup clock tree. RCC->CFGR2 |= (1 << RCC_PREDIV2_OFFSET) | // PREDIV = /2; Prediv Freq = 4MHz (1 << RCC_PLL3MUL_OFFSET) | // PLL3 = x12.5 (PLL3 = 50MHz) (2 << RCC_ETH1GSRC_OFFSET) | // External source for RGMII (7 << RCC_PLL2MUL_OFFSET) | // PLL2 = x9 (PLL2 = 36MHz) (1 << RCC_PREDIV1_OFFSET) | // PREDIV1 = /2; Prediv freq = 50MHz 0; // Power on PLLs RCC->CTLR |= RCC_PLL3ON | RCC_PLL2ON; int timeout; for (timeout = 10000; timeout > 0; timeout--) if (RCC->CTLR & RCC_PLL3RDY) break; if (timeout == 0) return -5; for (timeout = 10000; timeout > 0; timeout--) if (RCC->CTLR & RCC_PLL2RDY) break; if (timeout == 0) return -6; // PLL = x18 (0 in register) RCC->CFGR0 = (RCC->CFGR0 & ~(0xf << 18)) | 0; RCC->CTLR |= RCC_PLLON; for (timeout = 10000; timeout > 0; timeout--) if (RCC->CTLR & RCC_PLLRDY) break; if (timeout == 0) return -7; // Switch to PLL. #ifdef CH32V307GIGABIT_MCO25 RCC->CFGR0 = (RCC->CFGR0 & ~RCC_SW) | RCC_SW_PLL | (9 << 24); // And output clock on PA8 #else RCC->CFGR0 = (RCC->CFGR0 & ~RCC_SW) | RCC_SW_PLL; #endif // For clock in. funPinMode(PB1, GPIO_CFGLR_IN_FLOAT); // GMII_CLK125 RCC->CFGR2 |= RCC_ETH1G_125M_EN; // Enable 125MHz clock. // Power on and reset. RCC->AHBPCENR |= RCC_ETHMACEN | RCC_ETHMACTXEN | RCC_ETHMACRXEN; RCC->AHBRSTR |= RCC_ETHMACRST; RCC->AHBRSTR &= ~RCC_ETHMACRST; ETH->DMABMR |= ETH_DMABMR_SR; // Wait for reset to complete. for (timeout = 10000; timeout > 0 && (ETH->DMABMR & ETH_DMABMR_SR); timeout--) { Delay_Us(10); } // Use RGMII EXTEN->EXTEN_CTR |= EXTEN_ETH_RGMII_SEL; // EXTEN_ETH_RGMII_SEL; funPinMode(PB13, GPIO_CFGLR_OUT_50Mhz_AF_PP); // GMII_MDIO funPinMode(PB12, GPIO_CFGLR_OUT_50Mhz_AF_PP); // GMII_MDC // For clock output to Ethernet module. funPinMode(PA8, GPIO_CFGLR_OUT_50Mhz_AF_PP); // PHY_CKTAL // Release PHY from reset. #ifdef CH32V307GIGABIT_PHY_RSTB funDigitalWrite(CH32V307GIGABIT_PHY_RSTB, FUN_HIGH); #endif Delay_Ms(25); // Waiting for PHY to exit sleep. This is inconsistent at 23ms (But only on the RTL8211FS) None is needed on the RTL8211E funPinMode(PB0, GPIO_CFGLR_OUT_50Mhz_AF_PP); // GMII_TXD3 funPinMode(PC5, GPIO_CFGLR_OUT_50Mhz_AF_PP); // GMII_TXD2 funPinMode(PC4, GPIO_CFGLR_OUT_50Mhz_AF_PP); // GMII_TXD1 funPinMode(PA7, GPIO_CFGLR_OUT_50Mhz_AF_PP); // GMII_TXD0 funPinMode(PA3, GPIO_CFGLR_OUT_50Mhz_AF_PP); // GMII_TXCTL funPinMode(PA2, GPIO_CFGLR_OUT_50Mhz_AF_PP); // GMII_TXC funPinMode(PA1, GPIO_CFGLR_IN_PUPD); // GMII_RXD3 funPinMode(PA0, GPIO_CFGLR_IN_PUPD); // GMII_RXD2 funPinMode(PC3, GPIO_CFGLR_IN_PUPD); // GMII_RXD1 funPinMode(PC2, GPIO_CFGLR_IN_PUPD); // GMII_RXD0 funPinMode(PC1, GPIO_CFGLR_IN_PUPD); // GMII_RXCTL funPinMode(PC0, GPIO_CFGLR_IN_FLOAT); // GMII_RXC funDigitalWrite(PA1, FUN_HIGH); // SELGRV = 3.3V funDigitalWrite(PA0, FUN_HIGH); // TXDelay = 1 funDigitalWrite(PC3, FUN_HIGH); // AN[0] = 1 funDigitalWrite(PC2, FUN_HIGH); // AN[1] = 1 funDigitalWrite(PC1, FUN_LOW); // PHYAD[0] // Configure MDC/MDIO // Conflicting notes - some say /42, others don't. ETH->MACMIIAR = ETH_MACMIIAR_CR_Div42; // Update MACCR ETH->MACCR = (CH32V307GIGABIT_CFG_CLOCK_DELAY << 29) | // No clock delay (0 << 23) | // Max RX = 2kB (Revisit if looking into jumbo frames) (0 << 22) | // Max TX = 2kB (Revisit if looking into jumbo frames) (0 << 21) | // Rated Drive (instead of energy savings mode) (10M PHY only) (1 << 20) | // Bizarre re-use of termination resistor terminology? (10M PHY Only) (0 << 17) | // IFG = 0, 96-bit guard time. (0 << 14) | // FES = 2 = GBE, 1=100MBit/s (UNSET TO START) (0 << 12) | // Self Loop = 0 (0 << 11) | // Full-Duplex Mode (UNSET TO START) (1 << 10) | // IPCO = 1, Check TCP, UDP, ICMP header checksums. (1 << 7) | // APCS (automatically strip frames) (1 << 3) | // TE (Transmit enable!) (1 << 2) | // RE (Receive Enable) (CH32V307GIGABIT_CFG_CLOCK_PHASE << 1) | // TCF = 0 (Potentailly change if clocking is wrong) 0; Delay_Ms(25); // Waiting for PHY to exit sleep. This is inconsistent at 19ms. // Reset the physical layer ch32v307ethPHYRegWrite(PHY_BCR, PHY_Reset | 1 << 12 | // Auto negotiate 1 << 8 | // Duplex 1 << 6 | // Speed Bit. 0); // De-assert reset. ch32v307ethPHYRegWrite(PHY_BCR, 1 << 12 | // Auto negotiate 1 << 8 | // Duplex 1 << 6 | // Speed Bit. 0); ch32v307ethPHYRegRead(0x03); ch32v307eth_phyid = ch32v307ethPHYRegRead(0x03); // Read twice to be safe. if (ch32v307eth_phyid == 0xc916) ch32v307ethPHYRegWrite(0x1F, 0x0a43); // RTL8211FS needs page select. ch32v307ethGetMacInUC(ch32v307eth_mac); ETH->MACA0HR = (uint32_t)((ch32v307eth_mac[5] << 8) | ch32v307eth_mac[4]); ETH->MACA0LR = (uint32_t)(ch32v307eth_mac[0] | (ch32v307eth_mac[1] << 8) | (ch32v307eth_mac[2] << 16) | (ch32v307eth_mac[3] << 24)); ETH->MACFFR = (uint32_t)(ETH_ReceiveAll_Disable | ETH_SourceAddrFilter_Disable | ETH_PassControlFrames_BlockAll | ETH_BroadcastFramesReception_Enable | ETH_DestinationAddrFilter_Normal | ETH_PromiscuousMode_Disable | ETH_MulticastFramesFilter_Perfect | ETH_UnicastFramesFilter_Perfect); ETH->MACHTHR = (uint32_t)0; ETH->MACHTLR = (uint32_t)0; ETH->MACVLANTR = (uint32_t)(ETH_VLANTagComparison_16Bit); ETH->MACFCR = 0; // No pause frames. // Configure RX/TX chains. ETH_DMADESCTypeDef *tdesc; for (i = 0; i < CH32V307GIGABIT_TXBUFNB; i++) { tdesc = ch32v307eth_DMATxDscrTab + i; tdesc->ControlBufferSize = 0; tdesc->Status = ETH_DMATxDesc_TCH | ETH_DMATxDesc_IC | ETH_DMATxDesc_FS; tdesc->Buffer1Addr = (uint32_t)0; // Populate with data. tdesc->Buffer2NextDescAddr = (i < CH32V307GIGABIT_TXBUFNB - 1) ? ((uint32_t)(ch32v307eth_DMATxDscrTab + i + 1)) : (uint32_t)ch32v307eth_DMATxDscrTab; } ETH->DMATDLAR = (uint32_t)ch32v307eth_DMATxDscrTab; for (i = 0; i < CH32V307GIGABIT_RXBUFNB; i++) { tdesc = ch32v307eth_DMARxDscrTab + i; tdesc->Status = ETH_DMARxDesc_OWN; tdesc->ControlBufferSize = ETH_DMARxDesc_RCH | (uint32_t)CH32V307GIGABIT_BUFFSIZE; tdesc->Buffer1Addr = (uint32_t)(&ch32v307eth_MACRxBuf[i * CH32V307GIGABIT_BUFFSIZE]); tdesc->Buffer2NextDescAddr = (i < CH32V307GIGABIT_RXBUFNB - 1) ? (uint32_t)(ch32v307eth_DMARxDscrTab + i + 1) : (uint32_t)(ch32v307eth_DMARxDscrTab); } ETH->DMARDLAR = (uint32_t)ch32v307eth_DMARxDscrTab; pDMARxGet = ch32v307eth_DMARxDscrTab; pDMATxSet = ch32v307eth_DMATxDscrTab; // Receive a good frame half interrupt mask. // Receive CRC error frame half interrupt mask. // For the future: Why do we want this? ETH->MMCTIMR = ETH_MMCTIMR_TGFM; ETH->MMCRIMR = ETH_MMCRIMR_RGUFM | ETH_MMCRIMR_RFCEM; ETH->DMAIER = ETH_DMA_IT_NIS | // Normal interrupt enable. ETH_DMA_IT_R | // Receive ETH_DMA_IT_T | // Transmit ETH_DMA_IT_AIS | // Abnormal interrupt ETH_DMA_IT_RBU; // Receive buffer unavailable interrupt enable NVIC_EnableIRQ(ETH_IRQn); // Actually enable receiving process. ETH->DMAOMR = ETH_DMAOMR_SR | ETH_DMAOMR_ST | ETH_DMAOMR_TSF | ETH_DMAOMR_FEF; return 0; } void ETH_IRQHandler(void) __attribute__((interrupt)); void ETH_IRQHandler(void) { uint32_t int_sta; do { int_sta = ETH->DMASR; if ((int_sta & (ETH_DMA_IT_AIS | ETH_DMA_IT_NIS)) == 0) { break; } // Off nominal situations. if (int_sta & ETH_DMA_IT_AIS) { // Receive buffer unavailable interrupt enable. if (int_sta & ETH_DMA_IT_RBU) { ETH->DMASR = ETH_DMA_IT_RBU; if ((INFO->CHIPID & 0xf0) == 0x10) { ((ETH_DMADESCTypeDef *)(((ETH_DMADESCTypeDef *)(ETH->DMACHRDR))->Buffer2NextDescAddr))->Status = ETH_DMARxDesc_OWN; ETH->DMARPDR = 0; } } ETH->DMASR = ETH_DMA_IT_AIS; } // Nominal interrupts. if (int_sta & ETH_DMA_IT_NIS) { if (int_sta & ETH_DMA_IT_R) { // Received a packet, normally. // Status is in Table 27-17 Definitions of RDes0 do { // XXX TODO: Is this a good place to acknowledge? REVISIT: Should this go lower? // XXX TODO: Restructure this to allow for ETH->DMASR = ETH_DMA_IT_R; uint32_t status = pDMARxGet->Status; if (status & ETH_DMARxDesc_OWN) break; // We only have a valid packet in a specific situation. // So, we take the status, then mask off the bits we care about // And see if they're equal to the ones that need to be set/unset. const uint32_t mask = ETH_DMARxDesc_OWN | ETH_DMARxDesc_LS | ETH_DMARxDesc_ES | ETH_DMARxDesc_FS; const uint32_t eq = 0 | ETH_DMARxDesc_LS | 0 | ETH_DMARxDesc_FS; int suppress_own = 0; if ((status & mask) == eq) { int32_t frame_length = ((status & ETH_DMARxDesc_FL) >> ETH_DMARXDESC_FRAME_LENGTHSHIFT) - 4; if (frame_length > 0) { uint8_t *data = (uint8_t *)pDMARxGet->Buffer1Addr; suppress_own = ch32v307ethInitHandlePacket(data, frame_length, pDMARxGet); } } // Otherwise, Invalid Packet // Relinquish control back to underlying hardware. if (!suppress_own) pDMARxGet->Status = ETH_DMARxDesc_OWN; // Tricky logic for figuring out the next packet. Originally // discussed in ch32v30x_eth.c in ETH_DropRxPkt if ((pDMARxGet->ControlBufferSize & ETH_DMARxDesc_RCH) != (uint32_t)RESET) pDMARxGet = (ETH_DMADESCTypeDef *)(pDMARxGet->Buffer2NextDescAddr); else { if ((pDMARxGet->ControlBufferSize & ETH_DMARxDesc_RER) != (uint32_t)RESET) pDMARxGet = (ETH_DMADESCTypeDef *)(ETH->DMARDLAR); else pDMARxGet = (ETH_DMADESCTypeDef *)((uint32_t)pDMARxGet + 0x10 + ((ETH->DMABMR & ETH_DMABMR_DSL) >> 2)); } } while (1); } if (int_sta & ETH_DMA_IT_T) { ch32v307ethInitHandleTXC(); ETH->DMASR = ETH_DMA_IT_T; } ETH->DMASR = ETH_DMA_IT_NIS; } } while (1); } static int ch32v307ethTransmitStatic(uint8_t *buffer, uint32_t length, int enable_txc) { // The official SDK waits until ETH_DMATxDesc_TTSS is set. // This also provides a transmit timestamp, which could be // used for PTP. // But we don't want to do that. // We just want to go. If anyone cares, they can check later. if (pDMATxSet->Status & ETH_DMATxDesc_OWN) { ETH->DMATPDR = 0; return -1; } pDMATxSet->ControlBufferSize = (length & ETH_DMATxDesc_TBS1); pDMATxSet->Buffer1Addr = (uint32_t)buffer; // Status is in Table 27-12 "Definitions of TDes0 bits" enable_txc = enable_txc ? ETH_DMATxDesc_IC : 0; pDMATxSet->Status = ETH_DMATxDesc_LS | // Last Segment (This is all you need to have to transmit) ETH_DMATxDesc_FS | // First Segment (Beginning of transmission) enable_txc | // Interrupt when complete ETH_DMATxDesc_TCH | // Next Descriptor Address Valid ETH_DMATxDesc_CIC_TCPUDPICMP_Full | // Do all header checksums. ETH_DMATxDesc_OWN; // Own back to hardware pDMATxSet = (ETH_DMADESCTypeDef *)pDMATxSet->Buffer2NextDescAddr; ETH->DMASR = ETH_DMASR_TBUS; // This resets the transmit process (or "starts" it) ETH->DMATPDR = 0; return 0; } #endif