멀쩡한 출력 뒤에 숨은 X 낙관성
reset이 끝나지 않은 제어 신호에 X가 보이는데 출력은 멀쩡하게 0이다. 출력만 확인한 테스트는 통과한다. 하지만 출력 0은 회로가 올바르게 초기화되었다는 증거일까. X 낙관성은 불확실한 제어 값을 시뮬레이션이 특정 경로로 처리하면서, 실제로는 여러 결과가 가능하다는 사실을 숨기는 현상이다.
X는 칩 내부에 별도의 전압으로 존재하는 세 번째 논리값이 아니다. 디지털 모델이 어떤 비트를 0인지 1인지 확정할 수 없다는 표시다. reset되지 않은 상태, 충돌하는 드라이버, 정의되지 않은 접근 등 서로 다른 원인이 X로 나타날 수 있다. 따라서 X를 만났을 때 필요한 것은 화면에서 없애는 일이 아니라, 불확실성이 어떤 동작에 영향을 미치는지 확인하는 일이다.
if가 조용히 골라 버린 한쪽 경로
1비트 sel로 두 4비트 값을 선택하는 설명용 회로를 생각하자.
always_comb begin
if (sel)
y_if = a;
else
y_if = b;
end
assign y_mux = sel ? a : b;
4-state 시뮬레이션에서 sel이 X이면 if의 조건은 참으로 확정되지 않으므로 else가 실행된다. a=4'b1010, b=4'b1000이라면 y_if는 1000이다. 한쪽 경로를 결정한 결과만 보인다.
같은 조건의 삼항 연산자는 1비트 선택자가 불확실할 때 두 후보를 비트별로 합친다. 두 값이 같은 비트는 유지하고 다른 비트는 X로 만든다. 그래서 y_mux는 10X0이 된다. 두 후보에서 확정되는 정보는 남기고, 선택자에 따라 달라지는 부분만 불확실하다고 표시한 것이다. 이 결과는 위 가정에서의 언어 의미를 직접 추적한 것이며 특정 시뮬레이터에서 측정한 파형은 아니다.
실제 sel이 0이면 결과는 1000, 1이면 1010이다. sel을 모르는 모델이 결과 전체를 1000으로 보여 주면 둘 중 하나를 이유 없이 확정한 셈이다. 이 점에서 if의 동작을 낙관적이라고 부른다. 다만 삼항 연산자를 썼다고 모든 초기화 문제가 해결되는 것도 아니다. 후보 a와 b가 우연히 같으면 결과는 확정된 값으로 보이므로, 제어 신호의 원인까지 밝혀 주지는 않는다.
출력의 X만 검사하면 놓치는 것
지금 예제에서 y_if가 X인지 검사하는 assertion은 통과할 수 있다. y_if는 이미 확정된 b를 받았기 때문이다. 반면 선택이 실제로 의미 있는 순간에 sel이 알려진 값이어야 한다는 조건을 검사하면 문제를 더 가까운 곳에서 발견할 수 있다.
assert property (@(posedge clk) disable iff (!rst_n)
use_select |-> !$isunknown(sel));
여기서 use_select는 이 선택 결과가 유효한 동작에 쓰이는 사이클을 표시하는 가상의 제어 신호다. 입력을 늘 상시 검사하는 대신 의미 있는 사용 지점을 검사한다. 출력이 무효인 동안 데이터가 X인 것은 허용할 수 있어도, 유효한 거래의 목적지를 고르는 sel이 X인 것은 허용하지 않는다는 계약이다.
lowRISC의 X 처리 지침은 RTL에서 X를 오류 표식으로 무작정 퍼뜨리기보다, 알려진 출력과 assertion으로 불법 조건을 표현하는 방식을 권한다. 그 접근은 X가 우연히 다음 논리에서 사라지기를 기대하지 않고, 잘못된 상태가 사용되는 지점을 직접 확인하도록 만든다.
불확실성과 '아무 값이나 괜찮다'는 다르다
디코더의 불법 입력에서 y = 'x라고 쓰면 시뮬레이션에서는 문제를 눈에 띄게 만드는 것처럼 보일 수 있다. 하지만 합성 도구는 이를 최적화를 위한 don't-care로 해석할 수 있다. 시뮬레이션의 “알 수 없다”와 합성 계약의 “어느 값을 만들어도 상관없다”가 같은 문자로 쓰이는 것이다.
이 둘을 섞으면, 시뮬레이션에서는 큰 X 덩어리가 나오는 불법 입력이 실제 회로에서는 정상 명령처럼 해석될 수 있다. 합성기는 허용한 자유도 안에서 회로를 단순화했을 뿐이다. 불법 상태에서 쓰기 금지나 오류 응답이 반드시 필요하다면, 그 동작을 정의하고 검증해야 한다. X가 안전한 중단을 의미한다고 가정해서는 안 된다.
반대로 불확실성을 모두 0으로 대입하는 것도 해결책이 아니다. reset 누락을 0으로 덮으면 결과는 보기 좋아지지만 초기 상태의 보장이 새로 생기지 않는다. X를 발생시키는 원인을 없앴는지, 아니면 표시만 없앴는지를 구별해야 한다.
시뮬레이터가 같은 X를 보고 있다는 보장도 없다
앞의 if와 삼항 비교는 4-state 의미를 전제로 한다. 속도를 위해 주로 2-state 모델을 사용하는 도구에서는 X가 0이나 1 또는 실행별 초기화 값으로 치환될 수 있다. Verilator의 X 옵션 설명은 명시적인 X를 처리하는 --x-assign과 초기 상태를 처리하는 --x-initial을 구분한다. 두 설정은 같은 종류의 불확실성을 다루는 것이 아니다.
따라서 2-state 실행에서 $isunknown 검사가 아무 문제를 찾지 못했다고 해서 4-state 모델에서의 X 낙관성이 없다고 결론 내릴 수 없다. 여러 초기값을 사용하는 실행은 reset 누락을 드러내는 데 도움이 될 수 있지만, 실제로 불확실한 모든 비트 조합을 탐색했다는 뜻도 아니다. 검증 결과를 읽을 때 모델과 설정을 함께 알아야 한다.
여기에 반대 방향의 현상도 있다. 게이트 수준 모델이나 보수적인 논리 표현이 실제로는 같은 신호에 묶여 있는 경로의 관계를 잃으면, 실제 회로에서는 확정되는 값에 X를 표시할 수 있다. 그래서 X가 보이면 무조건 물리적인 실패라고 읽는 태도도 정확하지 않다. 원인을 분류하고, 그 불확실성이 출력에 영향을 줄 수 있는 조건을 따져야 한다.
처음의 정상처럼 보이는 출력은 이제 다르게 읽힌다. sel을 모르는 상태에서 출력이 0으로 보였다는 사실은, sel이 올바르게 초기화되었다는 답을 주지 않는다. 제어 값이 의미 있게 사용되는 순간을 찾고 그때의 확정성을 확인해야 한다. X 낙관성을 줄이는 핵심은 X를 많이 퍼뜨리는 데 있지 않고, 시뮬레이션이 감추기 전에 불확실한 제어의 사용을 붙잡는 데 있다.