1. Bool은 비트 하나이고 UInt는 비트의 열입니다
Chisel의 모든 포트와 wire는 Chisel 타입의 값을 전달합니다. 이 장에 필요한 타입은 둘입니다. Bool()은
비트 하나이고 리터럴은 true.B와 false.B입니다. UInt(n.W)는 폭이 n비트인 부호 없는 수이고
리터럴은 5.U나 "hc5".U처럼 씁니다. Bool은 1비트 UInt이기도 하므로 UInt 받는 쪽에 연결할
수 있습니다. 두 타입은 모두 하드웨어 신호를 기술하며, 시뮬레이션에서는 그 신호의 값을 모델링합니다. elaboration을
이끄는 Scala Boolean과 Int는
첫 장에서 본
대로 다른 것입니다.
| 타입 | 폭 | 리터럴 | 용도 |
|---|---|---|---|
Bool() | 1 | true.B, false.B | 조건, 플래그, 선 하나 |
UInt(8.W) | 8 | 197.U, "hc5".U, "b1100_0101".U | 데이터, 개수, 주소 |
폭, 부호 있는 타입 SInt, 리터럴마다 정해지는 폭은
폭·SInt·산술 장에서 다룹니다. 이 장에서는 8비트
UInt가 7번부터 0번까지 번호가 붙은 선 여덟 개라는 사실만 있으면 됩니다.
2. 식과 회로
조합 회로는 이전 사이클의 상태를 기억하지 않고 현재 입력만으로 출력을 계산합니다. Chisel에서는
식과 연결로 조합 회로를 기술하고, elaboration이 연산자마다 회로 그래프의 노드를 하나씩 만듭니다.
(a & b) | (~c & d)는 입력 wire가 잎이고, 연산자가 내부 노드이며, 마지막 |가 뿌리인 트리입니다.
elaboration 중에 연산자를 호출할 때마다 회로 그래프에 노드가 하나 추가되고, 호출은 그 노드의
출력을 반환합니다.
val t = io.a & io.b // AND 노드 하나
io.x := t | io.c // t가 OR 노드의 입력이 됨
io.y := t ^ io.d // t가 XOR 노드의 입력도 됨| 코드 | 만들어지는 것 |
|---|---|
io.a & io.b | AND 노드 하나 |
val t = ... | 하드웨어가 아니라 AND 노드 출력에 붙인 Scala 이름 |
t | io.c, t ^ io.d | t를 함께 입력으로 쓰는 노드 두 개 |
val로 이름을 붙이면 노드 하나를 뒤의 여러 식에서 사용할 수 있습니다. 이렇게 한 노드가 여러 곳으로
연결되는 것을 팬아웃이라고 하며, 팬아웃이 있으면 트리가 일반적인 방향성 비순환 그래프가 됩니다.
시뮬레이션 중에는 입력이 바뀔 때마다 그래프 전체가 다시 계산됩니다.
3. 연산자
아래의 연산자는 모두 하드웨어 조각이고 결과 폭이 정해져 있습니다. 결과 폭은 elaboration 중에
피연산자의 폭만으로 정해집니다. 값이나 나중에 결과를 연결할 포트의 폭과는 상관이 없습니다. 각 절에서
연산자가 만드는 하드웨어와 결과 폭을 정리하고, 흔히 쓰는 표기가 뜻과 다르게 동작할 때 대신 무엇을
써야 하는지 설명합니다. 3.5절에서 대부분을 한 번에 실행해 봅니다.
3.1 비트 연산자와 논리 연산자
비트 연산자는 비트 자리마다 게이트를 하나씩 만듭니다. 8비트 값 두 개에 x & y를 쓰면 AND 게이트
여덟 개가 나란히 놓이고, 결과의 비트 3은 두 피연산자의 비트 3에만 영향을 받습니다. 논리 연산자는
피연산자를 각각 참·거짓 값 하나로 다룹니다.
형식
~x x & y x | y x ^ y // 비트 연산
!b b && c b || c // 논리 연산val enable = io.valid && !io.stall // Bool
val masked = io.data & "h0f".U // 8비트 값의 하위 4비트만 남김
val flipped = ~io.data // 8비트를 모두 반전| 연산자 | 하드웨어 | 피연산자 | 결과 폭 |
|---|---|---|---|
~x | 비트마다 인버터 하나 | UInt, SInt, Bool | w(x). Bool이면 결과도 Bool |
x & y, x | y, x ^ y | 비트 자리마다 게이트 하나 | UInt 둘, SInt 둘, 또는 Bool | max(w(x), w(y)) |
b && c, b || c | AND 또는 OR 게이트 하나 | Bool만 | 1 |
!x | Bool에서는 인버터, UInt에서는 0인지 검사 | Bool, UInt | 1 |
폭이 다른 피연산자. 비트 연산자는 두 피연산자를 합치기 전에 좁은 쪽을 넓은 쪽 폭으로 늘립니다.
UInt는 위에 0을 채웁니다. 그래서 io.data & "h0f".U에서 4비트 리터럴 1111은 0000_1111이
되고, data의 상위 4비트를 0으로 만듭니다. SInt는 위에 부호 비트를 복사해 채웁니다. 4비트
-1(1111)을 8비트 0과 OR하면 0000_1111이 아니라 1111_1111이 됩니다.
UInt에 쓴 !와 &&. &&와 ||는 Bool에만 있습니다. 피연산자가 UInt 둘이면 Scala
컴파일러가 value && is not a member of chisel3.UInt를 보고하며 멈춥니다. !는 이보다 느슨해서
UInt에 써도 컴파일되며, 모든 비트가 0일 때만 참인 "0인가" 검사가 됩니다. 이런 경우에는 어떤
검사인지 식에 직접 드러내세요.
val isZero = io.count === 0.U // !io.count가 계산하는 값
val nonZero = io.count =/= 0.U // io.count.orR과 같음. 1인 비트가 있음Bool에서는 ~b와 !b가 같은 인버터를 만들고 둘 다 Bool을 반환합니다. 조건식에는 논리로
읽히도록 !를 쓰세요.
3.2 산술 연산자
산술 연산자는 덧셈기, 뺄셈기, 곱셈기, 나눗셈기를 만듭니다.
형식
x + y x - y x +% y x -% y // 한 바퀴 도는 연산: max(w(x), w(y))비트
x +& y x -& y // 넓히는 연산: 1비트 더
x * y x / y x % y| 연산자 | 하드웨어 | 결과 폭 | 결과가 폭에 들어가지 않으면 |
|---|---|---|---|
x + y, x - y | 덧셈기, 뺄셈기 | max(w(x), w(y)) | 올림이나 빌림이 버려지고 결과가 한 바퀴 돕니다 |
x +% y, x -% y | 같은 회로 | max(w(x), w(y)) | 한 바퀴 돕니다. %는 그것이 의도임을 밝히는 표기입니다 |
x +& y, x -& y | 올림 출력까지 내는 덧셈기·뺄셈기 | max(w(x), w(y)) + 1 | 늘어난 비트에 남습니다 |
x * y | 곱셈기 | w(x) + w(y) | 생기지 않습니다. 모든 곱이 들어갑니다 |
x / y | 나눗셈기 | UInt는 w(x), SInt는 w(x) + 1 | 생기지 않습니다 |
x % y | 나눗셈기의 나머지 출력 | min(w(x), w(y)) | 생기지 않습니다 |
한 바퀴 도는 덧셈. 200.U(8.W) + 100.U(8.W)는 44입니다. 실제 합 300에는 9비트가 필요한데
+는 8비트만 남깁니다. +&는 아홉 번째 비트까지 남겨 300을 냅니다. := 왼쪽의 포트를 넓혀도
올림은 돌아오지 않습니다. 연결보다 먼저 8비트 합이 만들어지기 때문입니다.
부호 없는 뺄셈의 언더플로. UInt에는 음수가 없으므로 0.U(4.W) - 1.U는 -1이 아니라 한 바퀴
돌아 1111, 즉 15가 됩니다. -&는 5비트 1_1111 = 31을 냅니다. 늘어난 비트는 부호가 아니라
빌림이고, 결과는 여전히 부호 없는 수입니다. 음수 결과가 필요하면 다음 장에서 다루는 SInt
피연산자를 써야 합니다.
부호 있는 나눗셈이 1비트 넓은 이유는 -128 / -1이 +128이고, 이 값은 부호 있는 8비트에 들어가지
않기 때문입니다.
*, /, %는 큰 회로입니다. 소스에서는 문자 하나지만, 곱셈기는 부분곱 덧셈기를 배열한
회로이고, 나누는 수가 신호인 나눗셈기는 덧셈기보다 훨씬 크고 느립니다. 설계에 정말 필요한 곳에만
쓰고, 줄임 표기처럼 쓰지 마세요. x가 부호 없는 값이고 나누는 수가 2의 거듭제곱인 상수라면 나눗셈기가 필요 없습니다.
x / 4.U는 배선뿐인 x >> 2와 값이 같으므로 시프트로 쓰세요. 3 같은 수로 나누는 것은 시프트로
바꿀 수 없습니다.
이 폭 규칙은 다음 장에서
표 하나로 다시 모으고, +와 +&를 따로 실행해 비교합니다.
3.3 시프트
시프트가 어떤 하드웨어가 되는지는 이동량의 타입이 정합니다.
형식
x << 3 x >> 3 // Scala Int 이동량. elaboration 중에 정해짐
x << n x >> n // UInt 이동량. 시뮬레이션 중에 정해짐식, x는 8비트, n은 UInt(3.W) | 하드웨어 | 결과 폭 |
|---|---|---|
x << 3 | 배선. 모든 비트가 3자리 올라가고 아래는 0으로 채움 | w(x) + 3 = 11 |
x >> 3 | 배선. 하위 3비트를 버림 | w(x) - 3 = 5 |
x << n | 시프터. 출력 비트마다 멀티플렉서 하나 | w(x) + 2^w(n) - 1 = 15 |
x >> n | 시프터 | w(x) = 8 |
시프트는 스스로 상위 비트를 버리지 않습니다. <<는 대신 결과 폭을 늘립니다. 원래 폭의 결과가
필요하면 (x << 3)(7, 0)처럼 원하는 비트를 선택하세요. Scala Int만큼 오른쪽으로 시프트하면 그만큼
비트가 사라집니다. Chisel 6.7에서 8비트 값을 8 이상 오른쪽으로 시프트하면 폭이 0비트인 결과가
나옵니다.
UInt만큼 시프트할 때는 그 신호가 가질 수 있는 가장 큰 이동량까지 담을 자리가 필요합니다. 3비트
n이면 최대 7이므로 8 + 7 = 15비트입니다. n을 4비트로 하면 8 + 15 = 23비트로 늘어납니다. 이동량
신호는 실제로 쓰는 가장 큰 값이 들어갈 만큼만 넓게 잡으세요.
SInt의 >>는 산술 시프트입니다. 비게 된 상위 비트를 부호 비트로 채우므로 음수는 음수로
남습니다. 1100_0101이라는 비트는 UInt로 읽으면 197, SInt로 읽으면 -59이고, 오른쪽으로 2만큼
시프트하면 각각 49와 -15가 됩니다. <<는 두 타입 모두 0으로 채웁니다. 아래 예제의 x.asSInt는
UInt의 8비트를 그대로 SInt로 읽는 변환이며,
다음 장에서 자세히
다룹니다.
x.rotateLeft(k)와 x.rotateRight(k)는 UInt를 회전합니다. 한쪽 끝으로 밀려난 비트가 반대쪽
끝으로 다시 들어오고, 폭은 w(x) 그대로입니다.
3.4 비교 연산자
비교 연산자는 비교기를 만들며, 피연산자 폭과 상관없이 항상 Bool을 반환합니다.
형식
x === y x =/= y x < y x <= y x > y x >= yval isZero = io.count === 0.U
val notThree = io.x =/= 3.U
val inRange = io.temp >= 20.U && io.temp < 30.U같음은 ===, 다름은 =/=로, 문자 세 개입니다. Scala는 ==를 Scala 객체 비교에 사용합니다.
io.x == io.y는 elaboration 중에 두 포트 객체가 같은 객체인지 확인해 Scala Boolean을 반환하며,
Boolean은 하드웨어를 구동할 수 없습니다.
부호 있는 비교와 부호 없는 비교. 비트를 어떻게 비교할지는 타입이 정합니다. 1111_1000과
0000_1111을 UInt 둘로 비교하면 248 < 15이므로 거짓이고, SInt 둘로 비교하면 -8 < 15이므로
참입니다. Chisel은 UInt와 SInt를 직접 비교하지 않습니다. io.u < io.s를 쓰면 Scala 컴파일러가
type mismatch; found: chisel3.SInt, required: chisel3.UInt를 보고하며 멈춥니다. io.u.zext < io.s처럼
한쪽을 먼저 변환해, 어떤 해석으로 비교하는지 코드에 드러내세요.
3.5 실행해 보기: 시프트·산술·비교
ShiftArith.scala는 8비트 입력 x, y와 3비트 이동량 n에 3.2절부터 3.4절까지의 연산자를
적용합니다. elaboration 중에는 각 결과의 폭을 WIDTH 두 줄로 출력합니다. 이어서 테스트벤치가 입력
네 묶음을 넣고, 연산자 묶음마다 이름표가 붙은 SIMULATE 줄을 하나씩 출력합니다.
import chisel3._
class ShiftArith extends Module {
val io = IO(new Bundle {
val x = Input(UInt(8.W))
val y = Input(UInt(8.W))
val n = Input(UInt(3.W))
val shlConst = Output(UInt(11.W))
val shrConst = Output(UInt(5.W))
val shlDyn = Output(UInt(15.W))
val shrDyn = Output(UInt(8.W))
val sraConst = Output(SInt(6.W))
val sraDyn = Output(SInt(8.W))
val sum = Output(UInt(8.W))
val sumWide = Output(UInt(9.W))
val diff = Output(UInt(8.W))
val diffWide = Output(UInt(9.W))
val prod = Output(UInt(16.W))
val quot = Output(UInt(8.W))
val rem = Output(UInt(8.W))
val quarter = Output(UInt(8.W))
val ltU = Output(Bool())
val ltS = Output(Bool())
})
// Scala Int 이동량은 elaboration 중에 정해지므로 배선만 생긴다.
val shlConst = io.x << 3
val shrConst = io.x >> 3
// UInt 이동량은 시프터를 만든다. <<는 가장 큰 이동량까지 담을 자리를 남긴다.
val shlDyn = io.x << io.n
val shrDyn = io.x >> io.n
// SInt의 >>는 비어 있는 상위 비트를 부호 비트로 채운다.
val sraConst = io.x.asSInt >> 2
val sraDyn = io.x.asSInt >> io.n
// +와 -는 결과를 더 넓은 피연산자의 폭으로 자르고, +&와 -&는 비트를 하나 더 남긴다.
val sum = io.x + io.y
val sumWide = io.x +& io.y
val diff = io.x - io.y
val diffWide = io.x -& io.y
// *는 곱의 모든 비트를 남기고, /와 %는 나눗셈기를 만든다.
val prod = io.x * io.y
val quot = io.x / io.y
val rem = io.x % io.y
val quarter = io.x / 4.U
// 같은 비트도 UInt와 SInt로 비교하면 결과가 다르다.
val ltU = io.x < io.y
val ltS = io.x.asSInt < io.y.asSInt
println(s"WIDTH x<<3=${shlConst.getWidth} x>>3=${shrConst.getWidth} x<<n=${shlDyn.getWidth} " +
s"x>>n=${shrDyn.getWidth} s>>2=${sraConst.getWidth} s>>n=${sraDyn.getWidth}")
println(s"WIDTH x+y=${sum.getWidth} x+&y=${sumWide.getWidth} x-y=${diff.getWidth} x-&y=${diffWide.getWidth} " +
s"x*y=${prod.getWidth} x/y=${quot.getWidth} x%y=${rem.getWidth} x<y=${ltU.getWidth}")
io.shlConst := shlConst
io.shrConst := shrConst
io.shlDyn := shlDyn
io.shrDyn := shrDyn
io.sraConst := sraConst
io.sraDyn := sraDyn
io.sum := sum
io.sumWide := sumWide
io.diff := diff
io.diffWide := diffWide
io.prod := prod
io.quot := quot
io.rem := rem
io.quarter := quarter
io.ltU := ltU
io.ltS := ltS
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new ShiftArith) { dut =>
def set(x: Int, y: Int, n: Int): Unit = {
dut.io.x.poke(x.U)
dut.io.y.poke(y.U)
dut.io.n.poke(n.U)
}
def v(p: Data) = p.peek().litValue
set(0xc5, 3, 2)
println(s"SIMULATE shift x=197 n=2 x<<3=${v(dut.io.shlConst)} x>>3=${v(dut.io.shrConst)} " +
s"x<<n=${v(dut.io.shlDyn)} x>>n=${v(dut.io.shrDyn)}")
println(s"SIMULATE sint x.asSInt=-59 n=2 s>>2=${v(dut.io.sraConst)} s>>n=${v(dut.io.sraDyn)}")
println(s"SIMULATE divide x=197 y=3 x/y=${v(dut.io.quot)} x%y=${v(dut.io.rem)} " +
s"x/4=${v(dut.io.quarter)} x>>n=${v(dut.io.shrDyn)}")
set(200, 100, 0)
println(s"SIMULATE add x=200 y=100 x+y=${v(dut.io.sum)} x+&y=${v(dut.io.sumWide)} x*y=${v(dut.io.prod)}")
set(0, 1, 0)
println(s"SIMULATE sub x=0 y=1 x-y=${v(dut.io.diff)} x-&y=${v(dut.io.diffWide)}")
set(0xf8, 0x0f, 0)
println(s"SIMULATE compare x=f8 y=0f x<y=${v(dut.io.ltU)} x.asSInt<y.asSInt=${v(dut.io.ltS)}")
}
}확인 문제
sub 줄은 8비트 UInt 입력에 x = 0, y = 1을 넣습니다. x-y에는 무엇이 출력될까요?
출력에서 볼 곳. 실행을 누르고 출력의 각 줄을 다음 표와 맞춰 읽으세요.
| 줄 | 볼 곳 | 보여 주는 것 |
|---|---|---|
첫 WIDTH | x<<3=11, x<<n=15, x>>n=8 | 상수 <<는 이동량만큼, UInt 이동량은 2^w(n) - 1만큼 넓어지고, UInt만큼의 >>는 w(x)를 유지합니다 |
둘째 WIDTH | x+y=8, x+&y=9, x*y=16, x<y=1 | 결과 폭은 피연산자 폭만으로 정해집니다 |
shift | x<<3=1576, x<<n=788 | 상수 시프트는 고정되어 있고, UInt 이동량(여기서는 2)은 시뮬레이션 중에 정해집니다 |
sint | s>>2=-15와 shift 줄의 x>>n=49 | 같은 비트를 SInt와 UInt로 오른쪽 시프트한 결과입니다. 부호 비트가 복사됩니다 |
divide | x/y=65, x%y=2, x/4=49, x>>n=49 | 197 = 3 × 65 + 2이고, 4로 나누기는 2만큼 시프트와 같습니다 |
add | x+y=44, x+&y=300, x*y=20000 | +는 한 바퀴 돌았고, +&는 올림을 남겼고, *는 16비트를 모두 남겼습니다 |
sub | x-y=255, x-&y=511 | UInt 언더플로는 한 바퀴 돌고, -&의 늘어난 비트는 부호가 아닙니다 |
compare | x<y=0, x.asSInt<y.asSInt=1 | 같은 비트를 부호 없이 비교하면 248과 15, 부호 있게 비교하면 -8과 15입니다 |
elaboration이 끝난 뒤에 시뮬레이션이 시작되므로 WIDTH 줄이 먼저 나옵니다.
직접 바꿔 보세요. n을 UInt(4.W)로 바꾸고 다시 실행하세요. 가장 큰 이동량이 15가 되므로 첫
WIDTH 줄이 x<<n=23을 보고합니다. shlDyn 포트는 여전히 15비트이므로 연결에서 상위 8비트가 경고
없이 버려집니다. 788은 15비트에 들어가므로 SIMULATE 값은 바뀌지 않지만, n이 더 크면 비트가
아무 경고 없이 사라집니다.
3.6 연산자 우선순위
Chisel 연산자는 Scala method이므로 Verilog가 아니라 Scala의 우선순위를 따릅니다. Scala는 연산자의 첫 글자로 우선순위를 정합니다. 먼저 묶이는 것부터 차례로 나열하면 다음과 같습니다.
| 첫 글자 | 이 묶음에 속하는 Chisel 연산자 |
|---|---|
앞에 붙는 ~, ! | ~x, !b가 가장 먼저 묶입니다 |
*, /, % | *, /, % |
+, - | +, -, +&, -&, +%, -% |
<, > | <<, >>, <, <=, >, >= |
= | ===, =/= |
& | &, && |
^ | ^ |
| | |, || |
| 대입 | :=가 가장 나중에 묶이므로 오른쪽 식 전체가 먼저 계산됩니다 |
같은 묶음의 연산자는 왼쪽부터 묶입니다. 다음 경우에는 괄호가 필요합니다.
| 쓴 식 | Scala의 해석 | 대신 쓸 식 |
|---|---|---|
io.x & io.y === io.y | io.x & (io.y === io.y) | (io.x & io.y) === io.y |
a < b << 1 | (a < b) << 1. 시프트와 비교가 같은 묶음입니다 | a < (b << 1) |
x << n + 1, n은 Scala Int | x << (n + 1) | (x << n) + 1.U |
평균을 구하는 a + b >> 1 | 의도대로 (a + b) >> 1이지만 +가 이미 올림을 버렸습니다 | (a +& b) >> 1 |
주의. 위의 잘못된 식은 모두 컴파일되며 경고 없이 다른 값을 계산합니다. a = 200, b = 100이면
a + b >> 1은 22이고 (a +& b) >> 1은 150입니다. a < b << 1을 Bool에 연결하면
(a < b) << 1의 비트 0만 남으므로 항상 0입니다. 4절에서 첫 번째 경우를 직접 실행합니다.
3.7 비트 필드
비트 필드는 값의 일부 비트를 선택합니다. 비트 번호는 최하위 비트가 0입니다.
형식
x(i) // 비트 i 하나. 결과는 Bool
x(hi, lo) // 비트 hi부터 lo까지. 폭은 hi - lo + 1val data = io.data // 8비트
data(0) // 최하위 비트
data(7) // 최상위 비트
data(7, 4) // 상위 4비트번호는 상위 번호를 먼저 씁니다. Scala Int로 번호를 지정하면 선택이 elaboration 중에 정해지므로
논리 없이 배선만 생깁니다.
3.8 Cat과 Fill
Cat은 여러 값을 이어 붙여 더 넓은 값을 만들고, Fill은 값을 반복합니다. 둘 다 chisel3.util에
있으므로 이를 쓰는 파일은 그 패키지를 import합니다.
형식
import chisel3.util._
Cat(a, b, ...) // a가 최상위 비트 쪽에 놓임
Fill(n, x) // x를 n번 반복Cat("b11".U, "b0001".U(4.W)) // 11_0001, 6비트
Fill(3, "b10".U) // 10_10_10, 6비트| 식 | 결과 폭 |
|---|---|
Cat(a, b) | w(a) + w(b) |
Fill(n, x) | n * w(x) |
Cat은 소스를 읽는 순서를 그대로 따릅니다. 이진수를 왼쪽에서 오른쪽으로 읽듯이 첫 번째 인자가 가장
왼쪽, 즉 최상위 부분이 됩니다.
3.9 축약 연산
축약 연산은 값의 모든 비트를 Bool 하나로 합칩니다.
형식
x.andR // 모든 비트가 1이면 참
x.orR // 1인 비트가 하나라도 있으면 참
x.xorR // 1의 개수가 홀수이면 참val allOnes = io.data.andR
val nonZero = io.data.orR
val oddCount = io.data.xorR // io.data의 짝수 패리티 비트4. 예제: 연산자 추적하기
Ops.scala는 8비트 입력 x, y에 출력마다 연산자 하나를 적용합니다. 주석으로 나눈 세 묶음인 비트
필드, Cat, 축약 연산을 읽으세요. 테스트벤치는 입력 쌍 세 개를 넣고 모든 출력을 16진수로
출력합니다. 8비트 값은 16진수 두 자리입니다.
import chisel3._
import chisel3.util.Cat
class Ops extends Module {
val io = IO(new Bundle {
val x = Input(UInt(8.W))
val y = Input(UInt(8.W))
val and = Output(UInt(8.W))
val or = Output(UInt(8.W))
val xor = Output(UInt(8.W))
val notX = Output(UInt(8.W))
val hiX = Output(UInt(4.W))
val msbX = Output(Bool())
val swap = Output(UInt(8.W))
val allX = Output(Bool())
val anyX = Output(Bool())
val parX = Output(Bool())
val same = Output(Bool())
})
io.and := io.x & io.y
io.or := io.x | io.y
io.xor := io.x ^ io.y
io.notX := ~io.x
// 비트 필드는 (상위, 하위) 순으로 쓰며 비트 0이 최하위 비트다.
io.hiX := io.x(7, 4)
io.msbX := io.x(7)
// Cat은 첫 번째 인자를 최상위 비트에 배치한다.
io.swap := Cat(io.x(3, 0), io.x(7, 4))
// 축약 연산은 x의 모든 비트를 Bool 하나로 합친다.
io.allX := io.x.andR
io.anyX := io.x.orR
io.parX := io.x.xorR
// ===는 비교기를 만든다. Scala ==는 Scala 객체를 비교한다.
io.same := io.x === io.y
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Ops) { dut =>
for ((x, y) <- Seq((0xc5, 0x0f), (0xff, 0xff), (0x00, 0x80))) {
dut.io.x.poke(x.U)
dut.io.y.poke(y.U)
def v(p: Data) = p.peek().litValue
println(f"SIMULATE x=$x%02x y=$y%02x and=${v(dut.io.and)}%02x or=${v(dut.io.or)}%02x " +
f"xor=${v(dut.io.xor)}%02x not=${v(dut.io.notX)}%02x hi=${v(dut.io.hiX)}%x " +
f"msb=${v(dut.io.msbX)} swap=${v(dut.io.swap)}%02x all=${v(dut.io.allX)} " +
f"any=${v(dut.io.anyX)} par=${v(dut.io.parX)} same=${v(dut.io.same)}")
}
}
}이 개념 실습하기
다음 단계로 짧은 Chisel 문제에 이 개념을 적용해 보세요.
실행하기 전에 예측해 보세요. x = 0xc5, y = 0x0f인 첫 행을 손으로 계산하세요. 이진수로 x는
1100_0101, y는 0000_1111입니다. and, or, xor, not은 각각 무엇일까요? Cat이 첫 인자를
최상위에 둔다면 swap은 무엇이 될까요? x에는 1이 네 개 있습니다. all, any, par은 각각
무엇일까요?
확인 문제
x = 0xc5, y = 0x0f일 때 x ^ y는 16진수로 무엇입니까?
확인 문제
swap은 Cat(io.x(3, 0), io.x(7, 4))입니다. x = 0xc5일 때 무엇이 출력됩니까?
확인 문제
x = 0xc5에는 1인 비트가 네 개 있습니다. par, 즉 x.xorR는 무엇입니까?
결과 확인. 실행을 누르고 첫 출력 행을 다음 표와 비교하세요.
| 출력 | 계산 | 값 |
|---|---|---|
and | 1100_0101 & 0000_1111 | 05 |
or | 1100_0101 | 0000_1111 | cf |
xor | 1100_0101 ^ 0000_1111 | ca |
not | ~1100_0101 | 3a |
hi | x(7, 4) | c |
msb | x(7) | 1 |
swap | Cat(x(3, 0), x(7, 4)). 0101 뒤에 1100 | 5c |
all, any, par | 1이 네 개. 전부는 아니고, 하나 이상이며, 짝수 개 | 0, 1, 0 |
same | x === y | 0 |
나머지 두 행은 경계값을 확인합니다. 0xff에서는 andR이 참이고 xor가 0입니다. 0x00에서는
orR이 거짓이고 not이 모두 1입니다. swap은 Cat의 순서를 그대로 보여 줍니다. 인자 목록에서
먼저 쓴 하위 4비트가 결과의 상위 4비트로 옮겨졌습니다.
직접 바꿔 보세요.
===를==로 바꾸세요. Scala 컴파일러가found: Boolean,required: chisel3.Data를 보고하며 멈춥니다. Scala가 두 포트 객체를 비교해Boolean을 만들었고,:=는 그 값을 하드웨어에 연결할 수 없습니다.- 마지막 줄을
io.same := io.x & io.y === io.y로 바꾸세요. "y의 모든 비트가x에도 있다"는 뜻으로 쓴 식입니다.0x05와0x0f가 다른데도 첫 행이same=1이 됩니다. 우선순위 때문에 식이io.x & (io.y === io.y)가 되었고, 비교는 항상 참이므로 결과는x & 1입니다. 이 값을Bool에 연결하면x의 비트 0만 남으며0xc5에서는 1입니다.(io.x & io.y) === io.y로 괄호를 쓰면 기대한 0이 나옵니다.
5. Cat의 인자 순서를 바꿔 보세요
실행 패널에서 swap 줄을 Cat(io.x(7, 4), io.x(3, 0))으로 바꾸고 다시 실행하세요.
확인 문제
Cat(io.x(7, 4), io.x(3, 0))일 때 x = 0xc5의 swap은 무엇입니까?
Cat의 인자 순서는 아무것도 검사하지 않습니다. 순서가 뒤집히면 틀린 값으로만 드러납니다. Cat은
이진수를 왼쪽에서 오른쪽으로 읽듯이, 상위 자리를 먼저 읽습니다.
6. 적용: 패리티 비트가 붙은 프레임
아래 실습 문제는 7비트 data로 8비트 frame을 만드는 문제입니다. 비트 6부터 0에는 data를 그대로
두고, 비트 7에는 프레임 전체의 1 개수가 짝수가 되도록 고른 짝수 패리티 비트를 둡니다. 이 장의 연산자
둘로 충분합니다. 축약 연산 하나가 패리티 비트를 정하고, Cat이 그 비트를 데이터 위에 놓습니다.
확인 문제
data = 0x55, 즉 101_0101일 때 프레임에 필요한 패리티 비트는 무엇입니까?
7. 자주 하는 실수
7.1 === 대신 == 쓰기
// 잘못된 코드: Scala 객체를 비교해 Scala Boolean을 반환함
io.same := io.x == io.y
// 바른 코드: 비교기를 만듦
io.same := io.x === io.y컴파일러는 type mismatch; found: Boolean, required: chisel3.Data를 보고합니다.
7.2 비교식에 괄호 빠뜨리기
// 잘못된 코드: io.x & (io.y === io.y)로 해석됨
io.covers := io.x & io.y === io.y
// 바른 코드
io.covers := (io.x & io.y) === io.yScala에서 ===는 &, ^, |보다 먼저 묶입니다. 잘못된 식도 컴파일되며 x의 비트 0을 반환합니다.
7.3 Cat의 순서 거꾸로 읽기
// 잘못된 코드: lo를 하위 비트에 두려 했지만 lo가 최상위에 놓임
val word = Cat(lo, hi)
// 바른 코드: 첫 인자가 최상위 부분
val word = Cat(hi, lo)Cat은 왼쪽에서 오른쪽으로 쓴 2진수처럼 읽힙니다. 순서는 아무도 검사하지 않으므로 실수는 틀린
값으로만 드러납니다.
8. 정리
| 주제 | 핵심 |
|---|---|
Bool(), UInt(n.W) | 비트 하나. 부호 없는 n비트. Bool은 1비트 UInt이기도 합니다 |
| 식 | 연산자마다 노드가 하나 생깁니다. val은 노드에 이름을 붙여 여러 식이 공유하게 합니다 |
&, |, ^, ~ | 비트 자리마다 게이트. 결과 폭은 더 넓은 피연산자의 폭입니다 |
&&, ||, ! | Bool 위의 게이트. UInt에 쓴 !는 0인지 검사하므로 === 0.U로 씁니다 |
| 산술 | +, -는 max 폭에서 한 바퀴 돌고, +&, -&는 1비트를 더 남깁니다. *는 w(x) + w(y), /, %는 나눗셈기를 만듭니다 |
| 시프트 | Scala Int 이동량은 배선이고 <<는 그만큼 넓어집니다. UInt 이동량은 시프터를 만듭니다. SInt의 >>는 부호를 복사합니다 |
| 비교 | Bool을 반환합니다. Scala ==는 객체를 비교하므로 ===, =/=를 씁니다. UInt와 SInt는 직접 비교하지 않습니다 |
| 우선순위 | 첫 글자로 정하는 Scala 규칙을 따릅니다. ===는 &보다 먼저 묶이고 <<는 <와 같은 묶음이므로 괄호를 씁니다 |
| 비트 필드 | x(i), x(hi, lo). 비트 0이 최하위 비트이고 상위 번호를 먼저 씁니다. 논리가 아니라 배선입니다 |
Cat, Fill | Cat은 첫 인자를 최상위에 두고, Fill(n, x)는 x를 반복합니다 |
| 축약 연산 | andR, orR, xorR이 모든 비트를 Bool 하나로 합칩니다. 모두, 하나라도, 홀수 개 |
9. 확인 문제
-
Cat(x(3, 0), x(7, 4))에서 비트 7–4에 놓이는 인자는 무엇이며, 이유는 무엇일까요?정답 보기
첫 번째 인자인
x(3, 0)입니다.Cat은 첫 번째 인자를 최상위 비트 쪽에 배치합니다. -
Fill(2, "b101".U)의 폭과 값은 무엇일까요?정답 보기
6비트
101101입니다.Fill이 3비트 값을 두 번 반복합니다. -
Scala는
io.x & io.y === io.y를 어떻게 해석하며, 8비트x에 대해 이 식은 무엇을 계산할까요?정답 보기
===가&보다 먼저 묶이므로io.x & (io.y === io.y)로 해석합니다. 비교는 항상 참이므로 결과는x & 1, 즉 0으로 확장된x의 비트 0입니다.io.x & io.y를 괄호로 묶으면 의도한 비교가 됩니다. -
io.x == io.y는 무엇을 반환하며, 왜io.same을 구동할 수 없을까요?정답 보기
Scala
Boolean입니다.==는elaboration중에 두 포트 객체가 같은 객체인지 묻습니다.:=의 오른쪽에는 하드웨어가 와야 하므로 컴파일러가found: Boolean, required: chisel3.Data를 보고합니다. 비교기는===가 만듭니다.
아래 실습 문제는 데이터 7비트와 비트 7의 짝수 패리티 비트로 8비트 프레임을 만듭니다. 시작 코드는 데이터를 그대로 연결하므로 비트 7이 항상 0이고, 1의 개수가 홀수인 데이터의 프레임은 패리티 규칙을 어깁니다.