SPI/I2C/CAN처럼 "슬레이브-마스터-슬레이브"를 거쳐야 하는 오버헤드 없이, 노드 간 직접 통신이 가능하면서도 하드웨어 UART 시리얼통신보다 빠른 경량 버스 프로토콜을 Verilog(SystemVerilog)로 설계하고, 최종적으로 아두이노(AVR) 라이브러리로 이식한다.
v2 변경 요지: 실물(Arduino Uno 2대) 검증 단계에서 핀 배정 정정, TX 시작 타이밍 동기화 버그 수정, 내부 풀업 제거, ISR 최적화를 반영. 성능 목표는 확장성(버스 안정성) 우선, 속도는 차순위로 우선순위를 재조정. 클럭 상한 실측치와 목표 미달 격차를 명시.
- 가변 데이터선(WIDTH): 라이브러리 사용자가 코드 작성 단계에서 데이터선 개수를 1~8 사이로 설정할 수 있어야 한다. 핀 수와 속도의 트레이드오프를 사용자가 직접 선택.
- 완전한 마스터리스 구조: CLK은 노드 중 하나가 출력하거나 외부 클럭을 사용할 수 있지만, 클럭을 출력하는 노드가 "마스터" 역할(중재자, 조정자)을 갖지 않는다. 모든 노드는 대등(peer-to-peer)하다. (구현 확인됨: 라이브러리 클래스 레벨에 마스터/슬레이브 구분이 없고,
clkIsOutput파라미터는 순수 배선 역할일 뿐 프로토콜상 권한과 무관함을 실제 테스트로 검증 — 송신측 스케치를 수신 노드에 그대로 꽂아도 동일하게 동작함.) - 최소 라인 구성: BUSY_N, ACK_N, DATA_BUS(WIDTH비트) 만으로 구성. 아두이노 10개 노드를 매달아도 문제없이 동작해야 한다.
- 확장성 최우선, 속도는 차순위 (v2 변경): 원래 "저지연(5~10배)"을 1순위로 뒀으나, 실물 검증 결과 노드 수 확장 시 안정성(전기적 마진)이 속도보다 훨씬 깨지기 쉬운 것으로 확인됨. 이후 개발은 버스 신뢰성/확장성을 1순위, 속도 개선은 2순위로 진행한다.
- 전기적 특성: 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와 달리 브로드캐스트 및 슬레이브 간 임의 통신이 프로토콜 레벨에서 기본 지원되어야 한다.
| 라인 | 핀 | 인터럽트 | 특성 |
|---|---|---|---|
| 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. 송신·수신 노드 전부 이 시점에 반응함)
- WIDTH 파라미터화:
ARB_CYCLES = ceil(8/WIDTH)를 자동 계산. WIDTH=8이면 중재 1클럭(완전 병렬), WIDTH=1이면 8클럭(완전 직렬)로 자연스럽게 양극단을 커버해야 한다. (구현: ISR 핫패스에서 매 엣지 재계산하지 않도록begin()시점에 캐싱함 — AVR은 하드웨어 나눗셈기가 없어/연산이 느림.) - 0xFF sentinel 문제 해결: 데이터 유효성 판단에
data_bus != 0xFF같은 매직값 비교를 쓰지 않는다. 대신 "이 프레임에서 아직 캡처하지 않음" 상태 플래그(captured_this_frame)로 유효성을 판단해, 실제 페이로드가 0xFF/0x00 등 임의값이어도 정상 전송되어야 한다. - 이중 캡처 방지: BUSY_N이 재차 풀릴 때까지 동일 프레임에서 같은 바이트를 두 번 FIFO에 넣지 않도록 프레임 단위 캡처 플래그로 원천 차단한다.
- NODE_ID 기반 주소 지정: 프레임 첫 필드(중재에 사용된 값 자체)가 목적지 주소 역할을 겸한다. 수신 노드는 자신의 NODE_ID와 일치하거나 브로드캐스트 값(예: 8'hFF)일 때만 데이터를 FIFO에 저장하고 ACK를 건다.
- 동시 송신 충돌 방지: 중재 단계에서 패배한 노드는 즉시 버스를 양보(
busy_out<=1)하고, 승리한 노드만 데이터 단계로 진입한다. 데이터 페이즈에서는 유일한 구동자만 남아있으므로 버스 컨텐션이 구조적으로 발생하지 않는다. - NODE_ID 유일성: 마스터가 없어 런타임 검증 수단이 없으므로, 컴파일 타임(파라미터)에서 사용자가 직접 유일한 ID를 부여하도록 강제. (선택 확장: 부팅 시 중재를 이용한 자동 ID 할당 — 미구현)
- Rx FIFO: 노드당 최소 depth 4의 하드웨어 FIFO로 수신 데이터를 버퍼링, CPU(또는 상위 로직)가 비동기로 읽어간다. depth는 반드시 2의 거듭제곱이어야 함(컴파일 타임 static_assert로 강제) — FIFO 인덱스 연산을
%대신&(비트마스크)로 처리해 ISR 오버헤드를 줄이기 위함. - 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 과제로 이월.
- 기준 비교 대상: 아두이노 하드웨어 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): 위 격차를 메우는 하드웨어적 개선(슈미트 트리거 버퍼, 저항값 추가 하향, 버스 세그멘테이션)은 확장성 검증 이후로 순서를 미룬다. 현재는 "노드 수가 늘어도 안정적으로 동작하는가"를 먼저 검증하고, 그 다음에 그 안정성을 유지하며 속도를 올리는 순서로 진행한다.
- 송신측과 수신측의 클럭 상한 비대칭: 동일 풀업 조건에서 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 방침상 채택하지 않기로 결정 — 노드 수가 늘어날수록 다른 노드와의 순간적 전기 충돌 위험이 커져 확장성과 상충되기 때문.
- 최소 10개
bus_node인스턴스 동시 연결 - 시나리오: (a) 단일 노드 단독 송신, (b) 2개 이상 노드 동시 송신 시도 시 중재 정상 동작, (c) 브로드캐스트, (d) 특정 목적지 주소 송신 시 비목적지 노드의 무시 동작, (e) FIFO full 상태에서의 동작
$dumpvars로 파형 검증(BUSY_N, ACK_N, DATA_BUS, 각 노드의 tx_state/rx_state)
- CLK 생성 — Timer/Counter CTC 모드 + OC 핀 하드웨어 자동 토글: 구현 완료. Timer1을 CTC 모드로 설정해 D9(OC1A)가 하드웨어적으로 자동 토글되도록 함. CPU는
begin()에서 초기 레지스터 설정 1회만 수행. - BUSY_N/ACK_N 엣지 감지 — External Interrupt 우선 사용: 부분 구현. CLK(D2)=INT0, BUSY_N(D3)=INT1까지는 원칙대로 하드웨어 엣지 지정 인터럽트를 사용하나, ACK_N(D8)은 PCINT0으로 배정되어 원래 "PCINT 지양" 원칙과는 다르게 구현됨 (배정 사유는 "라인 정의" 섹션 참고).
- 정밀 타이밍이 필요한 이벤트 — Input Capture Unit(ICU, Timer1) 활용: 미구현. 현재는 INT0/INT1/PCINT0 엣지 인터럽트만으로 중재/ACK 타이밍을 처리 중. 추후 타이밍 마진이 부족해지면 도입 검토.
- 제외 대상: 하드웨어 SPI 시프트레지스터: 기각 유지.
- Rx FIFO는 ISR 내에서 최소 작업만 수행: 구현 완료. ISR은
_fifoPush()만 수행하고, FIFO depth를 2의 거듭제곱으로 강제해 인덱스 연산을 비트마스크로 처리,available()/read()는 메인 루프에서 비동기 소비.
- 노드 3개 이상으로 확장 테스트 (중재 충돌 시나리오 실물 검증 — 현재까지는 2노드 단방향만 검증됨)
- 슈미트 트리거 버퍼(74HC14/74HC17) 도입으로 BUSY_N 복귀 시 오탐(
0xFF글리치) 근본 해결 - 안정성 확보 후 저항값/버퍼 조합으로 속도 재측정 및 목표(UART 대비 5~10배) 재도전
- Verilog 테스트벤치 작성 착수 (현재까지 AVR 실물 구현이 스펙보다 앞서 진행됨 — RTL은 미착수 상태)
-> 그러나! 풀업 저항을 1K~4.7K 까지 사용해봤는데 최대 클럭수에는 큰 변화가 없었음.