#tech 수정: 2026-06-16

lwIP에서 RX는 되는데 payload가 가끔 깨진다: DMA 버퍼와 D-Cache 순서를 같이 봐야 한다

lwIP 포팅에서 링크는 정상이고 인터럽트도 도는데 UDP payload나 ARP 내용이 가끔 깨져 보이면, pbuf보다 먼저 DMA 버퍼와 CPU D-Cache 일관성을 확인해야 한다. RX 전 invalidate, TX 후 clean 순서가 어긋나면 랜덤 패킷 손상처럼 보이는 증상이 반복된다.

lwIP를 Cortex-M7이나 A 계열 코어에 포팅하면
이상하게 패킷 내용만 가끔 틀어지는 경우가 있다.

  • 링크는 정상이다
  • 인터럽트도 잘 들어온다
  • 길이도 얼추 맞아 보인다
  • 그런데 payload 몇 바이트가 예전 값처럼 보인다

이럴 때 pbuf나 프로토콜 파서부터 오래 파기 쉽다.
하지만 먼저 볼 곳은 종종 더 아래다.

오늘 메모는 lwIP zero-copy 또는 DMA 버퍼 경로에서 D-Cache 일관성이 깨질 때 보이는 전형적인 증상이다.

결론부터

DMA와 CPU가 같은 메모리를 같이 만질 때는
"메모리에 썼다"와 "CPU가 그 값을 본다"가 같은 말이 아니다.

대략 이런 규칙을 먼저 떠올리는 편이 빠르다.

RX:
DMA가 메모리에 쓴 뒤
CPU가 읽기 전에 cache invalidate 필요

TX:
CPU가 메모리에 쓴 뒤
DMA가 읽기 전에 cache clean 필요

이 순서가 빠지면 현상은 네트워크 버그처럼 보이지만,
실제로는 캐시된 오래된 바이트를 읽는 문제일 수 있다.

왜 "가끔만" 터지나

캐시 이슈는 항상 100% 재현되지 않는다.

  • 작은 패킷은 우연히 같은 cache line 안에서 덜 보인다
  • 디버그 빌드는 타이밍이 달라 덜 터진다
  • 메모리 주소 정렬에 따라 증상이 바뀐다
  • 로그를 넣으면 오히려 재현 빈도가 달라진다

그래서 처음에는 DMA 드라이버보다
"상대가 이상한 값을 보냈나?" 쪽으로 의심이 가기 쉽다.

전형적인 RX 증상

수신에서는 보통 이런 모양으로 나타난다.

  • UDP payload 앞부분은 맞는데 중간이 예전 값이다
  • ARP 패킷이 가끔 말이 안 되는 MAC으로 보인다
  • 헤더 길이는 맞는데 checksum이나 application CRC가 틀린다
  • Wireshark 상의 송신 데이터와 장비 메모리 dump가 다르다

흔한 흐름은 이렇다.

DMA writes RX buffer
-> CPU reads cached old line
-> pbuf payload가 가끔 깨진 것처럼 보임

이 경우 pbuf->len, tot_len, 프로토콜 파싱은 모두 맞아도
바이트 값 자체가 틀려 보인다.

전형적인 TX 증상

송신 쪽은 반대로 이런 식이다.

  • 앱은 새 payload를 채웠는데 상대는 이전 값 일부를 받는다
  • ARP reply, UDP telemetry가 몇 바이트만 예전 프레임과 섞인다
  • 재부팅 직후보다 장시간 실행 후 더 잘 보인다

흐름으로 보면:

CPU updates TX buffer
-> cache line stays dirty in CPU
-> DMA reads old memory image
-> wire에는 이전 값이 섞여 나감

이때 lwIP API는 정상처럼 보인다.
udp_send()나 low_level_output() 반환값도 멀쩡하다.

그래서 송신 성공 여부보다
DMA가 실제로 읽은 메모리 이미지를 의심해야 한다.

pbuf 문제와 어떻게 구분하나

둘 다 "랜덤하게 깨진다"로 보여서 헷갈린다.

대략 이렇게 나눠 보면 빠르다.

  • pbuf 수명주기 문제: 크래시, use-after-free, pool 고갈, 재현 위치가 크게 흔들림
  • cache 일관성 문제: 길이는 맞는데 바이트 내용이 과거 값처럼 섞임

즉 이번 이슈는
메모리 ownership보다 메모리 가시성 쪽에 가깝다.

어디서 순서를 놓치기 쉬운가

1. RX descriptor 완료 직후 invalidate를 안 한다

드라이버가 DMA 완료 플래그만 보고
곧바로 CPU가 payload를 읽게 하면 위험하다.

ETH RX done
-> descriptor 확인
-> invalidate 없이 payload parse
-> 가끔 이전 cache line 값 사용

2. TX enqueue 뒤 clean을 늦게 하거나 빼먹는다

앱이 버퍼를 채운 뒤
DMA start 전에 clean이 필요할 수 있다.

특히 scatter-gather나 chained buffer에서는
한 조각만 clean 하고 끝내는 실수도 잘 나온다.

3. cache line 정렬을 무시한다

invalidate/clean을 했는데도 문제가 남는 경우가 있다.
이때는 범위 정렬이 흔한 원인이다.

  • 버퍼 시작 주소가 cache line 경계와 안 맞음
  • 길이를 exact byte만 처리해서 인접 line 일부가 빠짐

즉 "API를 불렀다"보다
어느 주소부터 어느 길이까지 처리했는지가 더 중요하다.

구현 쪽에서 무난한 패턴

프로젝트마다 HAL 이름은 다르지만,
경계는 보통 이렇게 두는 편이 낫다.

RX path:
DMA complete
-> invalidate aligned buffer range
-> pbuf 또는 app parser에 전달

TX path:
app fills buffer
-> clean aligned buffer range
-> DMA kick

그리고 이 규칙은 lwIP 바깥까지 포함해서 한 군데에 모아두는 편이 좋다.

  • low_level_input()
  • low_level_output()
  • DMA descriptor recycle path

세 군데가 각자 제각각 처리하면
몇 주 뒤 다시 같은 버그가 돌아온다.

로그를 이렇게 남기면 빨라진다

이 이슈는 패킷 캡처와 메모리 dump를 같이 봐야 풀린다.

  1. DMA 완료 시각
  2. invalidate/clean 호출 시각
  3. 버퍼 주소와 길이
  4. wire 상 payload 일부
  5. CPU가 읽은 payload 일부

이 다섯 줄이 있으면
"상대가 잘못 보냈다"와
"내 CPU가 stale cache를 읽었다"를 빨리 가를 수 있다.

빠른 체크리스트

  1. RX 버퍼를 CPU가 읽기 전에 cache invalidate를 하는지 확인
  2. TX 버퍼를 DMA가 읽기 전에 cache clean을 하는지 확인
  3. invalidate/clean 범위가 cache line 기준으로 정렬되는지 확인
  4. zero-copy RX/TX 경로와 복사 경로가 서로 다른 캐시 규칙을 쓰지 않는지 확인
  5. 패킷 길이 문제와 바이트 내용 문제를 분리해서 로그에 남기는지 확인

한 줄 요약

lwIP에서 길이는 맞는데 payload 내용만 가끔 깨지면 pbuf보다 먼저 DMA 버퍼와 D-Cache 일관성을 보고, RX 전 invalidate와 TX 전 clean 순서를 cache line 기준으로 맞추는 편이 빠르다.

추천 키워드

lwIP, DMA, D-Cache, RX buffer, zero-copy, embedded network


광고