21.7.3.1.2. 멀티 코어(Multi-core) H7 프로세서 환경에서 데이터를 기록할 때 발생하는 메모리 배리어(Memory Barrier) 및 캐시 코히런시(Cache Coherency) 문제 해결

21.7.3.1.2. 멀티 코어(Multi-core) H7 프로세서 환경에서 데이터를 기록할 때 발생하는 메모리 배리어(Memory Barrier) 및 캐시 코히런시(Cache Coherency) 문제 해결

픽스호크 6X나 최신 비행 제어기들에 들어가는 STM32H7 칩셋은 기존 픽스호크 4(FMUv5) 시절의 단일 코어 칩셋과는 아예 차원이 다른 괴물이다.
단순히 클럭만 빠른 것이 아니라, CPU 코어가 여러 개(예: Cortex-M7 + Cortex-M4 듀얼 코어) 달린 구조를 띠며, 여기에 각 코어마다 초고속 임시 기억 장치인 **L1 캐시(L1 Cache)**가 별도로 붙어있다.

이 L1 캐시는 비행 제어 연산 속도를 미친 듯이 끌어올려 주었지만, 반대로 공유 메모리를 쓰는 uORB 시스템에게는 역사상 가장 끔찍한 버그 제조기, ‘캐시 코히런시(Cache Coherency, 캐시 불일치)’ 현상을 몰고 왔다.

1. 캐시 코히런시: “내가 방금 쓴 글씨가 왜 안 보이지?”

듀얼 코어 시스템에서 센서 드라이버는 코어 A(M7)에서 돌고 있고, 자세 제어기(mc_rate_control)는 코어 B(M4)에서 돌고 있다고 상상해 보자.

  1. 코어 A(센서)의 발행: 센서 드라이버가 자이로 구조체에 데이터를 쓴 뒤 orb_publish()를 호출한다. 코어 A는 메인 메모리(RAM)까지 멀고 느리게 갈 필요 없이, 자기 바로 옆에 붙어있는 L1 캐시 저장소에만 쓱 쓰고 치워버린다.
  2. 코어 B(제어기)의 구독: 즉시 인터럽트 콜백이 터지며 코어 B의 자세 제어기가 큐에 꽂혀 update()를 호출한다. 코어 B는 메인 RAM을 뒤지거나 자신의 L1 캐시를 열어본다.

여기서 대참사가 터진다! 코어 A가 쓴 데이터는 메인 RAM으로 내려가지 않고 코어 A의 캐시에만 갇혀 있다!
결국 코어 B의 자세 제어기는 코어 A가 방금 쓴 따끈따끈한 데이터를 보지 못한 채 (데이터 가시성 상실), **옛날에 썼던 구닥다리 쓰레기 값(Stale Data)**을 읽어오고 드론은 그 자리에서 미쳐 날뛰게 된다.

2. 캐시 무효화(Invalidate)와 캐시 밀어내기(Clean)

이 지옥 같은 캐시 불일치 사태를 막으려면, uORB 시스템이 데이터를 교환할 때마다 물리적인 강제 동기화 명령을 내려주어야 한다.

  1. 출판자쪽 강제 밀어내기 (Cache Clean): 코어 A가 orb_publish()를 통해 uORB 게시판에 データを 다 썼다면, “내 캐시에만 담아두지 말고 지금 당장 메인 RAM으로 강제로 쏟아 부어라(Flush)!” 라는 명령을 하드웨어에 날린다.
    // NuttX 커널 레벨의 캐시 정리 명령
    up_clean_dcache((uintptr_t)data_address, (uintptr_t)data_address + data_size);
    
2.  **구독자쪽 강제 무효화 (Cache Invalidate):** 코어 B가 `orb_copy()`를 통해 게시판을 읽으려 할 때는, **"내 L1 캐시에 저장된 복사본은 못 믿는다! 싹 다 지우고 무조건 메인 RAM에서부터 진짜 최신 데이터를 긁어와라!"** 라고 하드웨어에 명령한다.
    ```c
    // NuttX 커널 레벨의 캐시 무효화 명령
    up_invalidate_dcache((uintptr_t)data_address, (uintptr_t)data_address + data_size);

3. 메모리 배리어 (Memory Barrier): 기록 순서의 엄수

캐시를 강제로 비우고 채우는 것 외에도, 멀티 코어 환경의 똑똑한 컴파일러(GCC)와 CPU 파이프라인이 저지르는 만행을 제어해야 한다.
CPU는 최적화를 한답시고 코드의 실행 순서(Instruction Order)를 자기 마음대로 뒤죽박죽 섞어서 실행해 버린다.

만약 CPU가 “메인 포인터를 신버전 버퍼로 교체하라“는 명령을 “데이터를 다 기록해라“보다 먼저 실행해 버린다면? 구독자(코어 B)는 아직 반밖에 안 적힌 부서진 데이터를 읽어버린다.

이 순서 뒤바뀜(Out-of-order execution)을 철벽 방어하기 위해 uORB 커널 소스 군데군데에는 DMB (Data Memory Barrier) 라는 어셈블리어 철책이 꽂혀있다.

// uORB 내부 코어 데이터 기록 구간
write_data_to_buffer(data);

// [중요!] 메모리 배리어 (Memory Barrier)
// "이 선을 넘기 전까지 위의 쓰기 작입이 100% RAM에 도달했음을 보장하라!"
__DMB(); 

// 이제 안심하고 메인 데이터 포인터를 스위칭한다.
switch_buffer_pointer(); 

이 엄청난 캐시 동기화 마법(Clean, Invalidate)과 하드웨어 철책(DMB)이 uORB 매니저 뱃속 단단히 구축되어 있기 때문에, PX4 코드를 짜는 앱 개발자들은 멀티 코어의 공포를 전혀 느끼지 않고 그저 publish() 한 줄만 딸깍 호출하며 평온하게 코드를 짤 수 있는 것이다.

모듈들이 이렇게 안전하고 비동기적으로 데이터를 흩뿌리고 받아먹는 세계를 구축했다.
하지만 때로는, 여러 개의 우체통(토픽)에 동시다발적으로 도착하는 데이터들을 “가장 마지막 데이터가 올 때까지” 가만히 기다렸다가 한꺼번에 처리해야 할 때도 있다.
이러한 **동기화 융합(Synchronized Fusion)**을 가능케 하는 옛 기술이지만 여전히 강력한 무기, px4_poll을 활용한 다중 토픽 폴링(Polling) 기법을 다음 21.7.4장에서 해부해 보자.