두 모듈을 연결한 뒤 드러나는 조합 논리 루프
각 모듈은 따로 시뮬레이션할 때 정상인데, 두 모듈을 연결하자 시간이 진행되지 않거나 신호가 안정되지 않는다. 입력과 출력 방향도 맞고 폭도 맞다. 그럼 무엇이 잘못됐을까. 조합 논리 루프는 값이 저장 경계 없이 자기 자신에게 돌아오는 의존 관계다. 연결의 모양이 정상이어도 이 관계가 생기면 전체 회로의 의미가 달라진다.
일반적인 동기 데이터 경로는 현재 입력과 레지스터 출력에서 계산을 시작해 다음 레지스터 입력에서 끝난다. 계산 중간에 자신에게 돌아오는 경로가 없으면 신호의 평가 순서를 정할 수 있다. 반대로 순환이 있으면 어느 값을 먼저 정해야 할지가 불명확해지거나, 반복 평가를 해도 안정된 값에 도달하지 못할 수 있다.
해가 없는 식은 클록을 기다리지 않는다
설명용으로 다음 두 식을 생각해 보자.
assign left = ~right;
assign right = left;
1비트 0/1 모델에서 right=0으로 시작하면 left=1, right=1, left=0, right=0이라는 변화가 반복된다. 두 식을 합치면 left=~left다. 0도 1도 이 방정식을 만족하지 않는다. 시뮬레이터가 같은 시각의 조합 값을 안정시키려고 계속 재평가하면, 시간은 앞으로 가지 않고 값만 바뀔 수 있다.
Verilator의 DIDNOTCONVERGE 설명은 이런 안정화 실패를 다룬다. 실제 물리 회로에서는 게이트와 배선의 지연, 전압 전환이 존재하므로 RTL의 무한 반복과 똑같이 동작한다고 말할 수는 없다. 특히 zero-delay RTL에서 반복 횟수를 세어 발진 주파수를 계산하는 해석은 성립하지 않는다. 이 코드는 동기 데이터 경로로 사용할 수 있는 값을 정의하지 못했다는 사실을 보여 준다.
4-state 모델에서는 초기 X가 그대로 남을 수도 있다. 물리 회로의 가능한 동작을 완전히 표현했다는 뜻은 아니며, 반복 오류가 안 났다는 이유로 루프가 안전해지는 것도 아니다. 안정화 여부와 회로의 의도는 나눠 확인해야 한다.
안정될 수 있어도 좋은 계약은 아니다
모든 조합 루프가 반전식처럼 진동하는 것은 아니다. ready-valid 연결에서 더 조용한 문제가 생길 수 있다. 생산자가 ready가 있을 때만 valid를 내고, 소비자가 valid가 있을 때만 ready를 내는 가상의 두 블록을 생각하자.
assign valid = have_data && ready;
assign ready = have_room && valid;
have_data=have_room=1이면 valid=ready라는 관계가 남는다. (valid, ready)=(0,0)도 두 식을 만족하고 (1,1)도 만족한다. 데이터와 공간이 모두 있는데도 초기 평가가 0,0에 머물면 거래가 일어나지 않는다. 원하는 동작을 입력 조건만으로 한 가지로 결정하지 못한 것이다.
이 예제의 해결은 계산 반복 횟수를 늘리는 것이 아니다. 생산자의 valid는 데이터가 있다는 사실에 따라 독립적으로 제시하고, 소비자의 ready는 수락 능력을 나타내도록 계약을 바꾸어야 한다. 필요하면 저장 경계도 추가한다. 모듈 내부 식이 단순하다는 사실보다, 상대 모듈의 출력에 의존하는 방향이 서로 물렸는지가 중요하다.
실제 스트림에서는 ready의 조합 전달 자체가 늘 잘못된 것은 아니다. PULP의 stream register는 하류 ready와 현재 저장 상태로 상류 ready를 계산한다. 이 경로는 한 모듈만 보면 정상이다. 연결된 전체 경로가 다시 출발점의 valid나 ready로 돌아오는지를 따로 봐야 한다.
경고에 '루프'가 보이면 비트까지 내려가야 한다
도구의 순환 경고가 항상 실제 게이트의 피드백을 뜻하는 것은 아니다. 신호 벡터 전체를 하나의 의존 노드로 분석하면, 같은 벡터의 서로 다른 비트 사이에 있는 정상적인 경로를 순환으로 볼 수 있다.
logic [2:0] chain;
assign chain = {chain[1:0], incoming};
비트별로 풀면 chain[0]=incoming, chain[1]=chain[0], chain[2]=chain[1]이다. incoming에서 bit0, bit1, bit2로 진행하고 끝나므로 비트 수준의 피드백은 없다. 반면 벡터 전체 수준에서는 chain이 chain에 의존하는 것처럼 보인다. 이 예제는 레지스터 이동이 아니며, 클록 없이 세 비트 모두 incoming의 값으로 이어지는 조합 연결이다.
Verilator의 UNOPTFLAT 설명은 이런 신호 단위 의존성과 반복 평가의 관계도 다룬다. 따라서 경고를 보고 곧바로 플립플롭을 넣기 전에, 실제 비트 연결 그래프에 순환이 있는지 확인해야 한다. 무조건 경고를 무시하는 것도, 모든 경고를 실제 발진으로 읽는 것도 정확하지 않다.
레지스터를 넣으면 무엇이 바뀌는가
실제 순환을 끊기 위해 플립플롭을 넣으면 의존 관계에 시간 경계가 생긴다. 현재 계산은 이전 에지의 상태에서 출발하고, 그 결과는 다음 상태로 저장된다. 계산을 한 번에 해결해야 했던 순환이 여러 사이클에 걸친 상태 전이로 바뀐다.
그러나 회로가 더 잘 평가된다는 이유만으로 수정이 끝나지는 않는다. ready 경로에 레지스터를 넣으면 하류의 정지가 상류에 늦게 전달되고, 그 사이 이미 허용한 데이터를 저장할 공간이 필요하다. valid나 데이터에만 단계를 추가하면 제어와 데이터의 정렬도 바뀔 수 있다. 루프를 끊는 수정은 기능과 거래 타이밍의 변화이기도 하다.
반대로 의도적인 래치나 특수 발진 회로처럼 피드백을 사용하는 설계도 있다. 이 글의 기준은 일반적인 동기 조합 경로다. 특수 구조에는 별도의 구현과 검증 모델이 필요하며, 조합 루프 경고를 끄는 것만으로 그 모델이 생기지는 않는다. Yosys 합성 문서에서 설명하는 동작의 회로 변환 과정에서도, 프로세스가 어떤 구조로 해석되는지 확인해야 하는 이유다.
처음의 연결 문제를 풀 때에는 데이터가 무엇인지보다 어떤 신호가 어떤 신호를 기다리는지를 그려 보는 편이 빠르다. 저장 경계 없이 출발점으로 돌아온다면 루프이고, 그 루프가 해를 갖는지, 해가 하나인지, 의도한 거래를 진행시키는지는 다시 다른 질문이다. 이 구분을 하면 시뮬레이터의 반복 실패와 조용한 정지, 벡터 단위 경고를 같은 문제로 섞지 않고 해결할 수 있다.