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를 같이 봐야 풀린다.
- DMA 완료 시각
- invalidate/clean 호출 시각
- 버퍼 주소와 길이
- wire 상 payload 일부
- CPU가 읽은 payload 일부
이 다섯 줄이 있으면
“상대가 잘못 보냈다”와
“내 CPU가 stale cache를 읽었다”를 빨리 가를 수 있다.
빠른 체크리스트
- RX 버퍼를 CPU가 읽기 전에 cache invalidate를 하는지 확인
- TX 버퍼를 DMA가 읽기 전에 cache clean을 하는지 확인
- invalidate/clean 범위가 cache line 기준으로 정렬되는지 확인
- zero-copy RX/TX 경로와 복사 경로가 서로 다른 캐시 규칙을 쓰지 않는지 확인
- 패킷 길이 문제와 바이트 내용 문제를 분리해서 로그에 남기는지 확인
한 줄 요약
lwIP에서 길이는 맞는데 payload 내용만 가끔 깨지면 pbuf보다 먼저 DMA 버퍼와 D-Cache 일관성을 보고, RX 전 invalidate와 TX 전 clean 순서를 cache line 기준으로 맞추는 편이 빠르다.
추천 키워드
lwIP, DMA, D-Cache, RX buffer, zero-copy, embedded network
DevBJ | 오늘을살자, Log Today