/* ============================================================================= * ethernetif.c - LwIP <-> STM32 HAL_ETH 글루 (STM32F407 + LAN8720 RMII) * * ST 공식 LwIP "ethernetif.c" 템플릿(STM32F4 + LAN8720)을 RTOS(NO_SYS=0)용으로 * 적응. 핵심 구성: * - low_level_init(): HAL_ETH 핸들/MAC(STM32 UID 유도)/DMA 디스크립터/버퍼 * 설정 후 ETH 시작. * - low_level_output(): pbuf 체인 -> HAL_ETH_Transmit (Tx 디스크립터). * - ethernetif_input task: RX 인터럽트 -> 세마포어 -> 태스크가 프레임을 꺼내 * pbuf 로 복사 후 netif->input(=tcpip_input) 호출. * - HAL_ETH_MspInit(): RMII GPIO/클럭/NVIC 설정. * - PHY 링크 폴링 태스크: LAN8720 BSR 를 읽어 링크/속도/듀플렉스 통지. * * 디스크립터/버퍼는 일반 SRAM(0x2000xxxx) 에 두며 4바이트 정렬. ETH DMA 는 * CCM(0x10000000) 에 접근할 수 없으므로 CCM 배치 금지. * * 참고 핀맵(RMII, app/하드웨어 명세): * REF_CLK PA1, MDIO PA2, MDC PC1, CRS_DV PA7, * RXD0 PC4, RXD1 PC5, TXEN PB11, TXD0 PB12, TXD1 PB13 * LAN8720 PHY 주소 = 0 (TODO(hw) 보드에서 확인). * ===========================================================================*/ #include "lwip/opt.h" #include "lwip/timeouts.h" #include "lwip/netif.h" #include "lwip/netifapi.h" /* netifapi_netif_set_link_up/down: 코어락 직렬화(tcpip 스레드 외부에서 호출) */ #include "lwip/pbuf.h" #include "lwip/stats.h" #include "lwip/snmp.h" #include "lwip/ethip6.h" #include "netif/etharp.h" #include "netif/ethernet.h" #include "app_config.h" #include "applog.h" #include "stm32f4xx_hal.h" #include "FreeRTOS.h" #include "task.h" #include "semphr.h" #include /* ── 컴파일타임 상수 ────────────────────────────────────────────────────── */ /* LAN8720 PHY 주소(RMII 스트랩). 기본 0. TODO(hw): 보드 스트랩 핀으로 확정. */ #ifndef LAN8720_PHY_ADDRESS #define LAN8720_PHY_ADDRESS 0x00U #endif /* DMA 디스크립터/버퍼 개수. RAM 절약 + 1 HTTPS 커넥션 처리에 충분. * 각 버퍼는 ETH_RX_BUF_SIZE(=ETH_MAX_PACKET_SIZE, 1524) 바이트. */ #define ETH_RXBUFNB 4U #define ETH_TXBUFNB 4U /* 이더넷 프레임 처리 정의가 헤더(stm32f4xx_hal_eth.h)에 없을 수 있어 보강. */ #ifndef ETH_MAX_PACKET_SIZE #define ETH_MAX_PACKET_SIZE 1524U #endif #ifndef ETH_RX_BUF_SIZE #define ETH_RX_BUF_SIZE ETH_MAX_PACKET_SIZE #endif #ifndef ETH_TX_BUF_SIZE #define ETH_TX_BUF_SIZE ETH_MAX_PACKET_SIZE #endif /* netif 식별자. */ #define IFNAME0 'e' #define IFNAME1 'n' /* PHY 링크 폴링 주기. */ #define PHY_LINK_POLL_MS 500U /* ── DMA 디스크립터/버퍼 (4바이트 정렬, 일반 SRAM) ──────────────────────── */ /* ETH DMA 는 CCM 접근 불가 -> 링커가 .bss(일반 SRAM)에 두도록 한다. * 일부 프로젝트는 별도 섹션(.RxDecripSection 등)에 배치하지만, 기본 .bss 면 충분. */ __attribute__((aligned(4))) static ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB]; __attribute__((aligned(4))) static ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB]; __attribute__((aligned(4))) static uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE]; __attribute__((aligned(4))) static uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE]; /* ── HAL ETH 핸들 + RTOS 동기 객체 ──────────────────────────────────────── */ static ETH_HandleTypeDef s_heth; static SemaphoreHandle_t s_rx_sem; /* RX IRQ -> input 태스크 통지 */ static struct netif *s_netif; /* 콜백/태스크에서 참조 */ /* ── 전방 선언 ──────────────────────────────────────────────────────────── */ static void ethernetif_input_task(void *arg); static void phy_link_task(void *arg); static struct pbuf *low_level_input(struct netif *netif); /* ============================================================================= * HAL MSP: RMII GPIO / 클럭 / NVIC * HAL_ETH_Init() 내부에서 HAL 이 자동 호출한다. * ===========================================================================*/ void HAL_ETH_MspInit(ETH_HandleTypeDef *heth) { GPIO_InitTypeDef gpio = {0}; if (heth->Instance != ETH) { return; } /* GPIO 포트 클럭. */ __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); /* SYSCFG 는 RMII/MII 선택에 필요. ETH 클럭(MAC + Tx + Rx)을 모두 켠다. * STM32F4 HAL 의 표준 ETH 클럭 매크로명. */ __HAL_RCC_SYSCFG_CLK_ENABLE(); __HAL_RCC_ETHMAC_CLK_ENABLE(); __HAL_RCC_ETHMACTX_CLK_ENABLE(); __HAL_RCC_ETHMACRX_CLK_ENABLE(); /* RMII 인터페이스 선택(반드시 ETH 클럭 활성화 후, MAC reset 전). * HAL_ETH_Init 의 MediaInterface=RMII 가 SYSCFG 를 설정하지만 명시적으로 보장. */ SYSCFG->PMC |= SYSCFG_PMC_MII_RMII_SEL; /* 1 = RMII */ /* 공통 RMII AF 설정 헬퍼. 모든 핀은 AF11_ETH, push-pull, very-high speed. */ gpio.Mode = GPIO_MODE_AF_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate = GPIO_AF11_ETH; /* PORTA: PA1(REF_CLK), PA2(MDIO), PA7(CRS_DV) */ gpio.Pin = GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_7; HAL_GPIO_Init(GPIOA, &gpio); /* PORTB: PB11(TX_EN), PB12(TXD0), PB13(TXD1) */ gpio.Pin = GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13; HAL_GPIO_Init(GPIOB, &gpio); /* PORTC: PC1(MDC), PC4(RXD0), PC5(RXD1) */ gpio.Pin = GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5; HAL_GPIO_Init(GPIOC, &gpio); /* ETH 전역 인터럽트 활성화(RX 완료 시 input 태스크 깨움). */ HAL_NVIC_SetPriority(ETH_IRQn, 7, 0); /* FreeRTOS 안전 우선순위 범위 내 */ HAL_NVIC_EnableIRQ(ETH_IRQn); } /* ============================================================================= * ETH 인터럽트: HAL 디스패치 -> RxCpltCallback 에서 세마포어 give * ===========================================================================*/ void ETH_IRQHandler(void) { HAL_ETH_IRQHandler(&s_heth); } void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { (void)heth; BaseType_t hp_woken = pdFALSE; if (s_rx_sem != NULL) { xSemaphoreGiveFromISR(s_rx_sem, &hp_woken); } portYIELD_FROM_ISR(hp_woken); } /* ============================================================================= * low_level_init: HAL_ETH 초기화 + MAC 주소 + DMA 디스크립터 + 시작 * ===========================================================================*/ static void mac_from_uid(uint8_t mac[6]) { /* STM32 96bit 고유 ID 에서 MAC 유도. 로컬 관리(LAA) 비트 set, 멀티캐스트 clear. */ uint32_t u0 = HAL_GetUIDw0(); uint32_t u1 = HAL_GetUIDw1(); uint32_t u2 = HAL_GetUIDw2(); uint32_t mix = u0 ^ u1 ^ u2; mac[0] = 0x02; /* locally administered, unicast */ mac[1] = (uint8_t)(u2 & 0xFF); mac[2] = (uint8_t)((u1 >> 8) & 0xFF); mac[3] = (uint8_t)((mix >> 16) & 0xFF); mac[4] = (uint8_t)((mix >> 8) & 0xFF); mac[5] = (uint8_t)(mix & 0xFF); } static void low_level_init(struct netif *netif) { static uint8_t mac[6]; mac_from_uid(mac); s_heth.Instance = ETH; s_heth.Init.MACAddr = mac; s_heth.Init.AutoNegotiation= ETH_AUTONEGOTIATION_ENABLE; s_heth.Init.Speed = ETH_SPEED_100M; /* AutoNeg 시 무시 */ s_heth.Init.DuplexMode = ETH_MODE_FULLDUPLEX; /* AutoNeg 시 무시 */ s_heth.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII; s_heth.Init.RxMode = ETH_RXINTERRUPT_MODE; /* 인터럽트 기반 RX */ s_heth.Init.ChecksumMode = ETH_CHECKSUM_BY_SOFTWARE; /* lwipopts SW 체크섬과 일치 */ s_heth.Init.PhyAddress = LAN8720_PHY_ADDRESS; if (HAL_ETH_Init(&s_heth) != HAL_OK) { /* AutoNeg 실패(케이블 없음 등)도 HAL_TIMEOUT 을 줄 수 있음 -> 계속 진행하고 * PHY 폴링 태스크가 이후 링크를 처리하게 한다. */ LOGW("eth: HAL_ETH_Init 경고(링크 없음 가능)"); } /* MAC 길이/타입. */ netif->hwaddr_len = ETH_HWADDR_LEN; /* = 6 */ memcpy(netif->hwaddr, mac, 6); netif->mtu = 1500; netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP; /* 링크 업/다운은 PHY 폴링 태스크가 netif_set_link_up/down 으로 통지하므로 * 여기서 NETIF_FLAG_LINK_UP 플래그를 미리 세우지 않는다. */ /* DMA 디스크립터 체인 초기화(Rx/Tx). */ HAL_ETH_DMATxDescListInit(&s_heth, DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB); HAL_ETH_DMARxDescListInit(&s_heth, DMARxDscrTab, &Rx_Buff[0][0], ETH_RXBUFNB); /* MAC/DMA 시작(Tx/Rx enable). */ if (HAL_ETH_Start(&s_heth) != HAL_OK) { LOGE("eth: HAL_ETH_Start 실패"); } LOGI("eth: MAC=%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); } /* ============================================================================= * low_level_output: pbuf 체인 -> HAL_ETH_Transmit * ===========================================================================*/ static err_t low_level_output(struct netif *netif, struct pbuf *p) { (void)netif; /* 현재 Tx 디스크립터의 버퍼로 pbuf 체인을 선형 복사. * (lwipopts: LWIP_NETIF_TX_SINGLE_PBUF=1 이라 보통 단일 조각이지만 안전하게 순회) */ uint8_t *buf = (uint8_t *)(s_heth.TxDesc->Buffer1Addr); if (buf == NULL) { return ERR_BUF; } uint32_t framelen = 0; for (struct pbuf *q = p; q != NULL; q = q->next) { if (framelen + q->len > ETH_TX_BUF_SIZE) { LOGW("eth: TX 프레임 과대(%lu)", (unsigned long)(framelen + q->len)); return ERR_BUF; } memcpy(&buf[framelen], q->payload, q->len); framelen += q->len; } if (HAL_ETH_TransmitFrame(&s_heth, framelen) != HAL_OK) { /* 디스크립터가 CPU 소유가 아니면(Tx underflow) 복구: 디스크립터 리셋. */ if ((s_heth.DMASR & ETH_DMASR_TUS) != (uint32_t)RESET) { s_heth.Instance->DMASR = ETH_DMASR_TUS; s_heth.Instance->DMATPDR = 0; /* Transmit Poll Demand 로 재개 */ } LINK_STATS_INC(link.drop); return ERR_IF; } LINK_STATS_INC(link.xmit); return ERR_OK; } /* ============================================================================= * low_level_input: HAL_ETH RX 디스크립터 -> 새 pbuf * ===========================================================================*/ static struct pbuf *low_level_input(struct netif *netif) { (void)netif; struct pbuf *p = NULL; /* 수신 프레임 1개 가져오기. 없으면 NULL. */ if (HAL_ETH_GetReceivedFrame_IT(&s_heth) != HAL_OK) { return NULL; } uint32_t len = s_heth.RxFrameInfos.length; uint8_t *src = (uint8_t *)s_heth.RxFrameInfos.buffer; if (len > 0 && len <= ETH_RX_BUF_SIZE) { p = pbuf_alloc(PBUF_RAW, (u16_t)len, PBUF_POOL); if (p != NULL) { /* pbuf 체인 각 조각으로 복사(POOL 버퍼가 분할될 수 있음). */ uint32_t copied = 0; for (struct pbuf *q = p; q != NULL && copied < len; q = q->next) { uint32_t n = q->len; if (copied + n > len) n = len - copied; memcpy(q->payload, &src[copied], n); copied += n; } LINK_STATS_INC(link.recv); } else { LINK_STATS_INC(link.memerr); LINK_STATS_INC(link.drop); } } else { LINK_STATS_INC(link.lenerr); LINK_STATS_INC(link.drop); } /* DMA 디스크립터를 다시 DMA 소유로 반환(여러 디스크립터에 걸친 프레임 포함). */ { __IO ETH_DMADescTypeDef *dmarxdesc = s_heth.RxFrameInfos.FSRxDesc; for (uint32_t i = 0; i < s_heth.RxFrameInfos.SegCount; i++) { dmarxdesc->Status |= ETH_DMARXDESC_OWN; dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr); } s_heth.RxFrameInfos.SegCount = 0; } /* RX 버퍼 언어베일러블(RBUS) 비트가 섰으면 RX 재개. */ if ((s_heth.Instance->DMASR & ETH_DMASR_RBUS) != (uint32_t)RESET) { s_heth.Instance->DMASR = ETH_DMASR_RBUS; s_heth.Instance->DMARPDR = 0; /* Receive Poll Demand */ } return p; } /* ============================================================================= * ethernetif_input_task: RX 세마포어 대기 -> 가용 프레임 모두 input 으로 전달 * ===========================================================================*/ static void ethernetif_input_task(void *arg) { struct netif *netif = (struct netif *)arg; for (;;) { /* RX IRQ 통지 대기. 타임아웃을 둬서 IRQ 누락 시에도 주기적으로 * 디스크립터를 비워(폴링) 록업을 방지한다. 반환값과 무관하게 매 주기 * 가용 프레임을 모두 처리한다. */ (void)xSemaphoreTake(s_rx_sem, pdMS_TO_TICKS(100)); struct pbuf *p; while ((p = low_level_input(netif)) != NULL) { /* netif->input 은 netif_add 등록상 tcpip_input(=ethernet_input 경유). */ if (netif->input(p, netif) != ERR_OK) { pbuf_free(p); } } } } /* ============================================================================= * phy_link_task: LAN8720 BSR 폴링 -> 링크 업/다운 + 속도/듀플렉스 반영 * ===========================================================================*/ static void phy_link_task(void *arg) { struct netif *netif = (struct netif *)arg; int prev_up = -1; for (;;) { uint32_t bsr = 0; int link_up = 0; if (HAL_ETH_ReadPHYRegister(&s_heth, PHY_BSR, &bsr) == HAL_OK) { link_up = (bsr & PHY_LINKED_STATUS) ? 1 : 0; } if (link_up != prev_up) { if (link_up) { /* 자동협상 결과로 MAC 속도/듀플렉스 갱신 후 ETH 재시작. */ uint32_t sr = 0; /* LAN8720 special control/status 레지스터(0x1F)에서 속도 추출. * 비표준 매크로 회피 위해 BCR/BSR 기반 보수적 처리. */ if (HAL_ETH_ReadPHYRegister(&s_heth, PHY_SR, &sr) == HAL_OK) { /* LAN8720 PHY_SR(0x1F): bit4=duplex(1=full), bit2..3=speed * (10=10M,100=100M). 보드 PHY 별 상이 가능 -> TODO(hw) 확인. */ uint32_t speed = ((sr >> 2) & 0x03); /* 01=10,10=100 (LAN8720) */ uint32_t duplex = (sr >> 4) & 0x01; s_heth.Init.DuplexMode = duplex ? ETH_MODE_FULLDUPLEX : ETH_MODE_HALFDUPLEX; s_heth.Init.Speed = (speed == 0x02) ? ETH_SPEED_100M : ETH_SPEED_10M; } HAL_ETH_ConfigMAC(&s_heth, NULL); HAL_ETH_Start(&s_heth); /* LWIP_TCPIP_CORE_LOCKING=1: 이 태스크는 tcpip 스레드가 아니므로 * raw netif_set_link_up 을 직접 부르면 코어 상태 레이스가 난다. * netifapi_* 로 호출하면 tcpip 스레드 문맥(코어락 보유)에서 실행되어 * 안전하다. (링크 콜백 net_link_cb 도 그 문맥에서 raw netif_set_up * 을 호출하므로 중첩 락 데드락이 없다.) */ netifapi_netif_set_link_up(netif); LOGI("eth: PHY 링크 업 (speed=%s)", (s_heth.Init.Speed == ETH_SPEED_100M) ? "100M" : "10M"); } else { HAL_ETH_Stop(&s_heth); netifapi_netif_set_link_down(netif); LOGW("eth: PHY 링크 다운"); } prev_up = link_up; } vTaskDelay(pdMS_TO_TICKS(PHY_LINK_POLL_MS)); } } /* ============================================================================= * ethernetif_init: LwIP netif_add 콜백 (net.c 가 등록) * ===========================================================================*/ err_t ethernetif_init(struct netif *netif) { LWIP_ASSERT("netif != NULL", (netif != NULL)); s_netif = netif; #if LWIP_NETIF_HOSTNAME netif->hostname = BOARD_DEVICE_ID; #endif netif->name[0] = IFNAME0; netif->name[1] = IFNAME1; /* IPv4 출력 = etharp_output, 링크 출력 = low_level_output. * netif->input 은 netif_add 의 마지막 인자(tcpip_input)로 설정됨. */ netif->output = etharp_output; netif->linkoutput = low_level_output; MIB2_INIT_NETIF(netif, snmp_ifType_ethernet_csmacd, 100000000); /* RX 통지 세마포어. */ s_rx_sem = xSemaphoreCreateBinary(); if (s_rx_sem == NULL) { LOGE("eth: RX 세마포어 생성 실패"); return ERR_MEM; } /* HW 초기화(MAC/PHY/DMA/시작). */ low_level_init(netif); /* RX 처리 태스크 + PHY 링크 폴링 태스크 기동. * 우선순위/스택은 lwipopts.h 의 ETHIF_RX_* 사용. */ if (xTaskCreate(ethernetif_input_task, ETHIF_RX_THREAD_NAME, ETHIF_RX_THREAD_STACKSIZE, netif, ETHIF_RX_THREAD_PRIO, NULL) != pdPASS) { LOGE("eth: RX 태스크 생성 실패"); return ERR_MEM; } if (xTaskCreate(phy_link_task, "eth_phy", 256, netif, DEFAULT_THREAD_PRIO, NULL) != pdPASS) { LOGE("eth: PHY 태스크 생성 실패"); return ERR_MEM; } return ERR_OK; }