Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SWP2P 통신 버스 프로토콜 설계 스펙 (v2)

프로젝트 목적

SPI/I2C/CAN처럼 "슬레이브-마스터-슬레이브"를 거쳐야 하는 오버헤드 없이, 노드 간 직접 통신이 가능하면서도 하드웨어 UART 시리얼통신보다 빠른 경량 버스 프로토콜을 Verilog(SystemVerilog)로 설계하고, 최종적으로 아두이노(AVR) 라이브러리로 이식한다.

v2 변경 요지: 실물(Arduino Uno 2대) 검증 단계에서 핀 배정 정정, TX 시작 타이밍 동기화 버그 수정, 내부 풀업 제거, ISR 최적화를 반영. 성능 목표는 확장성(버스 안정성) 우선, 속도는 차순위로 우선순위를 재조정. 클럭 상한 실측치와 목표 미달 격차를 명시.

설계 철학 (핵심 원칙)

  1. 가변 데이터선(WIDTH): 라이브러리 사용자가 코드 작성 단계에서 데이터선 개수를 1~8 사이로 설정할 수 있어야 한다. 핀 수와 속도의 트레이드오프를 사용자가 직접 선택.
  2. 완전한 마스터리스 구조: CLK은 노드 중 하나가 출력하거나 외부 클럭을 사용할 수 있지만, 클럭을 출력하는 노드가 "마스터" 역할(중재자, 조정자)을 갖지 않는다. 모든 노드는 대등(peer-to-peer)하다. (구현 확인됨: 라이브러리 클래스 레벨에 마스터/슬레이브 구분이 없고, clkIsOutput 파라미터는 순수 배선 역할일 뿐 프로토콜상 권한과 무관함을 실제 테스트로 검증 — 송신측 스케치를 수신 노드에 그대로 꽂아도 동일하게 동작함.)
  3. 최소 라인 구성: BUSY_N, ACK_N, DATA_BUS(WIDTH비트) 만으로 구성. 아두이노 10개 노드를 매달아도 문제없이 동작해야 한다.
  4. 확장성 최우선, 속도는 차순위 (v2 변경): 원래 "저지연(5~10배)"을 1순위로 뒀으나, 실물 검증 결과 노드 수 확장 시 안정성(전기적 마진)이 속도보다 훨씬 깨지기 쉬운 것으로 확인됨. 이후 개발은 버스 신뢰성/확장성을 1순위, 속도 개선은 2순위로 진행한다.
  5. 전기적 특성: BUSY_N, ACK_N, DATA_BUS 전 라인 open-drain(wired-AND) + 외부 pull-up. 내부 풀업(INPUT_PULLUP)은 사용하지 않는다 — 노드 수가 늘어날수록 각 노드의 내부 풀업(2050kΩ)이 전부 병렬로 걸려 유효 저항이 외부 저항값과 무관하게 예측 불가능해지는 문제가 확인되어, 순수 INPUT(플로팅) + 외부 풀업 저항 하나로만 버스 RC 특성을 결정하도록 통일함.

참고 모델과의 관계

  • 개념적으로 CAN 버스의 비트단위 우선순위 중재(wired-AND arbitration) 를 차용한다. "마스터 없는 대등 통신"이라는 아이디어 자체는 CAN과 동일 계열이며, 이 프로토콜이 CAN보다 가벼운 이유는 CAN의 11~29비트 ID, CRC, 여러 필드 등 무거운 프레임 구조를 걷어내고 8비트 주소 + 최소 필드로 극단적으로 경량화했기 때문이다.
  • I2C/SPI와 달리 브로드캐스트 및 슬레이브 간 임의 통신이 프로토콜 레벨에서 기본 지원되어야 한다.

라인 정의 (v2: 실제 구현 기준으로 정정)

라인 인터럽트 특성
CLK D2 (input) / D9 (output, Timer1 OC1A) INT0 공유 클럭. CLK을 출력하는 노드는 D9→D2 점퍼선으로 자기 자신에게도 루프백 필요. posedge=Tx 스텝, negedge=Rx 스텝.
BUSY_N D3 INT1 양방향, open-drain. 버스 점유(프레임 진행 중) 표시. Idle=1(release), 점유 중=0.
ACK_N D8 PCINT0 양방향, open-drain. 수신 확인, 정확히 1클럭 폭의 LOW 펄스.
DATA_BUS[WIDTH-1:0] 사용자 지정(예: D4~D7) 양방향, open-drain, WIDTH=1~8 (사용자 설정)

⚠️ v1과의 차이 (중요): v1 스펙 문서는 "BUSY_N, ACK_N을 각각 INT0, INT1에 배치"하고 "PCINT는 오버헤드 때문에 지양"한다고 명시했었으나, 실제 구현은 CLK=INT0, BUSY_N=INT1, ACK_N=PCINT0으로 배정되어 있다. CLK이 매 엣지(posedge/negedge)마다 Tx/Rx 스텝 진행의 핵심 트리거 역할을 하기 때문에 하드웨어 엣지 지정이 가능한 INT0 슬롯이 BUSY_N보다 CLK에 더 필요하다고 판단해 이렇게 배정되었다. ACK_N은 상대적으로 지연 허용치가 크다고 보고 PCINT0(D8)으로 밀려났다. 이 문서를 최신 기준으로 삼는다.

프레임 구조

[Idle 감지: BUSY_N==1]
  → [Pending: send() 호출 시점]
      · 실제 BUSY_N 하강과 첫 주소 청크 구동은 즉시 실행되지 않고,
        "다음 CLK posedge"까지 대기(TX_PENDING 상태)
      · main loop(비동기 컨텍스트)에서 곧바로 busy_out을 내리면 CLK 위상과
        무관한 임의 시점에 프레임이 시작되어, 수신측이 몇 번째 청크가
        첫 청크인지 오판하는 레이스 컨디션이 발생함이 실물 테스트로 확인됨
        (→ 아래 "알려진 이슈 및 수정 이력" 참고)
  → [중재(Arbitration) 단계: ceil(8/WIDTH) 클럭]
      · BUSY_N 하강 + 첫 주소 청크 구동이 같은 CLK posedge에서 동시에 실행됨
      · 송신 예정 노드들이 자신의 NODE_ID를 MSB부터 WIDTH비트씩 동시에 구동
      · open-drain 특성상 0을 미는 노드가 항상 우선(dominant bit)
      · 자신이 1(release)로 보낸 비트인데 버스 값이 0으로 읽히면 즉시 패배 → busy_out<=1, 송신 포기(재시도는 TX_PENDING으로 복귀 후 다음 posedge에 재동기화)
      · 되읽기 검증은 구동한 같은 엣지가 아닌 반클럭 뒤(negedge)에서 수행 — 풀업 RC 정착시간을 확보하기 위함
      · 마지막 청크까지 살아남은 노드가 유일한 송신자로 확정
  → [데이터 단계: ceil(8/WIDTH) 클럭]
      · 확정된 송신자가 실제 데이터 바이트를 WIDTH비트씩 구동
  → [Release: 1클럭] (data_out을 idle값으로 복귀, posedge)
  → [ACK 단계: 1클럭]
      · 수신자(주소가 일치하는 노드, 혹은 브로드캐스트 시 전체)가 negedge에서 ACK_N을 LOW로 구동 시작
  → [BUSY_N 복귀: 1클럭] (다음 posedge에서 busy_release, 능동적으로 트리거됨)
  → [ACK_N 복귀: 비동기] (CLK 엣지와 무관, BUSY_N이 물리적으로 HIGH로 올라온 것을 각 노드가 INT1로 감지한 시점에 개별적으로 ack_release. 송신·수신 노드 전부 이 시점에 반응함)

핵심 로직 요구사항

  1. WIDTH 파라미터화: ARB_CYCLES = ceil(8/WIDTH)를 자동 계산. WIDTH=8이면 중재 1클럭(완전 병렬), WIDTH=1이면 8클럭(완전 직렬)로 자연스럽게 양극단을 커버해야 한다. (구현: ISR 핫패스에서 매 엣지 재계산하지 않도록 begin() 시점에 캐싱함 — AVR은 하드웨어 나눗셈기가 없어 / 연산이 느림.)
  2. 0xFF sentinel 문제 해결: 데이터 유효성 판단에 data_bus != 0xFF 같은 매직값 비교를 쓰지 않는다. 대신 "이 프레임에서 아직 캡처하지 않음" 상태 플래그(captured_this_frame)로 유효성을 판단해, 실제 페이로드가 0xFF/0x00 등 임의값이어도 정상 전송되어야 한다.
  3. 이중 캡처 방지: BUSY_N이 재차 풀릴 때까지 동일 프레임에서 같은 바이트를 두 번 FIFO에 넣지 않도록 프레임 단위 캡처 플래그로 원천 차단한다.
  4. NODE_ID 기반 주소 지정: 프레임 첫 필드(중재에 사용된 값 자체)가 목적지 주소 역할을 겸한다. 수신 노드는 자신의 NODE_ID와 일치하거나 브로드캐스트 값(예: 8'hFF)일 때만 데이터를 FIFO에 저장하고 ACK를 건다.
  5. 동시 송신 충돌 방지: 중재 단계에서 패배한 노드는 즉시 버스를 양보(busy_out<=1)하고, 승리한 노드만 데이터 단계로 진입한다. 데이터 페이즈에서는 유일한 구동자만 남아있으므로 버스 컨텐션이 구조적으로 발생하지 않는다.
  6. NODE_ID 유일성: 마스터가 없어 런타임 검증 수단이 없으므로, 컴파일 타임(파라미터)에서 사용자가 직접 유일한 ID를 부여하도록 강제. (선택 확장: 부팅 시 중재를 이용한 자동 ID 할당 — 미구현)
  7. Rx FIFO: 노드당 최소 depth 4의 하드웨어 FIFO로 수신 데이터를 버퍼링, CPU(또는 상위 로직)가 비동기로 읽어간다. depth는 반드시 2의 거듭제곱이어야 함(컴파일 타임 static_assert로 강제) — FIFO 인덱스 연산을 % 대신 &(비트마스크)로 처리해 ISR 오버헤드를 줄이기 위함.
  8. TX 시작 시점의 CLK 동기화 (v2 신규 항목): send() 호출은 즉시 버스를 점유하지 않고 TX_PENDING 상태만 세팅한다. 실제 BUSY_N 하강과 중재 첫 청크 구동은 반드시 다음 CLK posedge에서 동시에 이루어져야 한다. (근거: 알려진 이슈 및 수정 이력 참고)

알려진 이슈 및 수정 이력

  • [수정됨] TX 시작 레이스 컨디션: send()에서 main loop 컨텍스트 즉시 busy_out을 내리면, CLK이 자유토글(free-running) 중일 때 BUSY_N 하강 시점과 CLK 위상이 어긋나 수신측이 프레임 청크 정렬을 한 칸씩 밀려서 읽는 문제가 실물 테스트로 확인됨. TX_PENDING 중간 상태를 도입해 CLK posedge와 동기화하여 해결.
  • [수정됨] 위 수정의 2차 버그: 초기 패치 시 TX_PENDING 블록에서 첫 청크 구동 직후 청크 인덱스를 즉시 감소시켜, negedge에서 한 번 더 감소되며 이중으로 줄어드는 문제 발생 → 중재 청크 1개를 덜 구동한 채 데이터 단계로 조기 진입 → 수신측이 프레임 전체를 못 읽고 완전히 침묵하는 증상으로 나타남. 감소 로직을 negedge 단일 지점으로 통일해 수정.
  • [확인됨, 미수정] 자기 오탐 가능성: BUSY_N 복귀 시 라인 바운스/링잉이 발생하면, 이미 idle로 전환된 노드가 이를 "새 프레임 시작"으로 오인해 아무도 구동하지 않는 데이터선을 전부 1로 읽어 0xFF(브로드캐스트 주소)로 오판, FIFO에 쓰레기 값이 들어가는 경우가 실물 테스트에서 드물게(약 10회 중 1회) 관측됨. 외부 풀업 도입 및 내부 풀업 제거로 빈도는 감소했으나 완전히 제거되지는 않음. 근본 해결책(슈미트 트리거 버퍼, 소프트웨어 디바운스 가드)은 v3 과제로 이월.

성능 목표 (v2: 실측 반영 및 우선순위 하향 조정)

  • 기준 비교 대상: 아두이노 하드웨어 UART, 115200bps(바이트당 ≈86.8us) 및 1Mbps(≈10us)
  • 아두이노 구현 시 digitalWrite() 대신 포트 레지스터 직접 제어(PORTB/PORTD 등) 사용 — 구현 완료 (핀별 DDR/PORT/PIN 레지스터를 begin()에서 캐싱, ISR은 포인터 역참조만 수행)
  • 원래 목표 (v1): WIDTH=1 구성만으로도 UART 115200bps 대비 약 10배 이상 빠른 프레임 전송시간 목표 (18클럭 내외). WIDTH≥4 구성에서는 UART 1Mbps보다 빠른 것이 목표.
  • 실측 결과 (2대 Uno, 브레드보드 배선 기준):
조건 CLK 상한 WIDTH=8 바이트당 시간 WIDTH=4 바이트당 시간 UART 115200 대비 (WIDTH=8)
외부 풀업 없음(내부 풀업만), 송신 노드 단독 측정 ≈76kHz ≈52.6us ≈78.9us 약 1.65배 빠름
외부 풀업 4.7kΩ, 수신 노드 포함 실동작 기준 ≈26kHz ≈153.8us ≈230.8us 약 1.8배 느림
외부 풀업 4.7kΩ + 내부 풀업 제거 후 ≈70kHz ≈57.1us ≈85.7us 약 1.5배 빠름
  • 목표 대비 격차: WIDTH=8 기준 UART 115200 대비 10배(바이트당 8.68us)를 달성하려면 CLK 약 460kHz가 필요. 현재 실측 최대치(≈70kHz) 대비 약 6.6배 추가 상승이 필요한 상태이며, 이는 로직/코드 최적화가 아니라 전기적 마진(RC 정착시간, 배선 전파지연, 인터럽트 레이턴시) 문제로 진단됨.
  • 우선순위 조정 (v2): 위 격차를 메우는 하드웨어적 개선(슈미트 트리거 버퍼, 저항값 추가 하향, 버스 세그멘테이션)은 확장성 검증 이후로 순서를 미룬다. 현재는 "노드 수가 늘어도 안정적으로 동작하는가"를 먼저 검증하고, 그 다음에 그 안정성을 유지하며 속도를 올리는 순서로 진행한다.

전기적 특성 및 확장성 관련 실측 지식 (v2 신규)

  • 송신측과 수신측의 클럭 상한 비대칭: 동일 풀업 조건에서 CLK을 생성/구동하는 노드(송신측)가 그 신호를 수동으로 읽어야 하는 노드(수신측)보다 훨씬 높은 클럭에서도 안정적으로 동작함이 확인됨 (예: 76kHz vs 26kHz). open-drain 버스에서 "0으로 끌어내리는 것"은 능동적이라 빠르지만 "1로 복귀하는 것"은 풀업 RC 충전에 의존하는 수동 과정이라 근본적으로 느리며, 프로토콜상 수신 노드는 이 "1로 복귀하는 신호"를 읽어야 하는 구간이 송신 노드보다 훨씬 많기 때문으로 추정. 추가로 배선 전파지연(마스터→슬레이브 물리적 거리)도 기여 요인으로 추정됨.
  • 내부 풀업 병렬 문제: 노드가 N개 붙으면 각 노드의 내부 풀업(비활성 상태에서도 병렬로 걸림)이 합쳐져 유효 저항이 외부 저항과 무관하게 낮아짐 → 외부 저항값을 정확히 튜닝해도 노드 수에 따라 실제 RC 특성이 달라지는 문제. 전 노드 내부 풀업 비활성화로 해결, 이제 외부 저항 하나만 버스 RC를 결정.
  • 저항값 하한의 안전 근거: 이 프로토콜은 중재 메커니즘상 항상 정확히 1개 노드만 LOW를 구동하도록 보장되므로(동시에 여러 노드가 같은 라인을 LOW로 미는 상황이 구조적으로 없음), 저항값을 낮출 때 고려할 전류 한계는 "AVR 핀 1개가 견디는 한도(권장 ~20mA)"만 보면 되고 노드 수와 무관하게 동일한 안전 마진이 유지됨. 이는 확장성과 완전히 양립하는 성질로, 저항값을 1kΩ 근처까지 안전하게 낮춰볼 수 있는 근거가 됨.
  • 확장성 우선 개선 후보 (검토 완료, 미착수):
    • 슈미트 트리거 버퍼(74HC14 / 74HC17): 노이즈 마진 개선. 노드 수 증가와 무관하게 성능 유지되는 방향이라 확장성과 부합. 프로토콜/코드 변경 불필요.
    • 74HC125(3-state 버퍼, 보유 중): 슈미트 트리거 기능은 없어 노이즈 마진 개선 효과는 없음. 대신 노드별 부하 격리 용도로 활용 가능 — 단, wired-AND 특성을 유지하려면 "LOW만 능동 구동, HIGH는 완전히 Hi-Z로 두어 풀업에 위임"하는 방식으로만 배선해야 함 (HIGH를 능동으로 밀면 push-pull 충돌 위험).
    • 버스 세그멘테이션/리피터: 노드 수가 크게 늘어날 경우(수십 개 단위) 세그먼트별로 커패시턴스를 격리해 노드 수 증가에도 속도 저하 없이 확장 가능. 현 규모(10개 목표)에서는 우선순위 낮음, 장기 과제.
    • (기각) 능동 가속 펄스(active speed-up pulse): release 순간에 짧게 push-pull로 HIGH를 밀어 RC를 우회하는 기법. 확장성을 1순위로 재조정한 v2 방침상 채택하지 않기로 결정 — 노드 수가 늘어날수록 다른 노드와의 순간적 전기 충돌 위험이 커져 확장성과 상충되기 때문.

테스트벤치 요구사항 (Verilog 시뮬레이션, 미착수)

  • 최소 10개 bus_node 인스턴스 동시 연결
  • 시나리오: (a) 단일 노드 단독 송신, (b) 2개 이상 노드 동시 송신 시도 시 중재 정상 동작, (c) 브로드캐스트, (d) 특정 목적지 주소 송신 시 비목적지 노드의 무시 동작, (e) FIFO full 상태에서의 동작
  • $dumpvars로 파형 검증(BUSY_N, ACK_N, DATA_BUS, 각 노드의 tx_state/rx_state)

아두이노(AVR) 이식 시 CPU 부담 최소화 원칙

  1. CLK 생성 — Timer/Counter CTC 모드 + OC 핀 하드웨어 자동 토글: 구현 완료. Timer1을 CTC 모드로 설정해 D9(OC1A)가 하드웨어적으로 자동 토글되도록 함. CPU는 begin()에서 초기 레지스터 설정 1회만 수행.
  2. BUSY_N/ACK_N 엣지 감지 — External Interrupt 우선 사용: 부분 구현. CLK(D2)=INT0, BUSY_N(D3)=INT1까지는 원칙대로 하드웨어 엣지 지정 인터럽트를 사용하나, ACK_N(D8)은 PCINT0으로 배정되어 원래 "PCINT 지양" 원칙과는 다르게 구현됨 (배정 사유는 "라인 정의" 섹션 참고).
  3. 정밀 타이밍이 필요한 이벤트 — Input Capture Unit(ICU, Timer1) 활용: 미구현. 현재는 INT0/INT1/PCINT0 엣지 인터럽트만으로 중재/ACK 타이밍을 처리 중. 추후 타이밍 마진이 부족해지면 도입 검토.
  4. 제외 대상: 하드웨어 SPI 시프트레지스터: 기각 유지.
  5. Rx FIFO는 ISR 내에서 최소 작업만 수행: 구현 완료. ISR은 _fifoPush()만 수행하고, FIFO depth를 2의 거듭제곱으로 강제해 인덱스 연산을 비트마스크로 처리, available()/read()는 메인 루프에서 비동기 소비.

다음 단계 (우선순위순)

  1. 노드 3개 이상으로 확장 테스트 (중재 충돌 시나리오 실물 검증 — 현재까지는 2노드 단방향만 검증됨)
  2. 슈미트 트리거 버퍼(74HC14/74HC17) 도입으로 BUSY_N 복귀 시 오탐(0xFF 글리치) 근본 해결
  3. 안정성 확보 후 저항값/버퍼 조합으로 속도 재측정 및 목표(UART 대비 5~10배) 재도전
  4. Verilog 테스트벤치 작성 착수 (현재까지 AVR 실물 구현이 스펙보다 앞서 진행됨 — RTL은 미착수 상태)

-> 그러나! 풀업 저항을 1K~4.7K 까지 사용해봤는데 최대 클럭수에는 큰 변화가 없었음.

About

A custom software-based peer-to-peer bus communication library for Arduino.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages