Chisel 독학을 위한 타래를 만들었다.
Reference
^ 위 레퍼런스를 따라갈 생각이다.
ucb-bar/chisel-tutorial
freechipsproject/chisel-bootcamp => 가장 메인이 될 것 같다. 주피터 노트북 튜토리얼이 웹에서 참 잘 구현되어있다. 이거 서버비는 누가 내는거야?
RISC-VとChiselで学ぶ はじめてのCPU自作 => 일본어 서적이기는 하지만, 난 일어가 가능하기도 하고 나름 정리되어있어보여서 읽어볼만해보인다. 다만, 가격이 약 3.5만원으로 비싸다(...). 일단 소스코드는 공개되어있긴하다. 서평을 보면, RTL 레벨이 아니라 아예 Chisel만 사용하고 있기 때문에, 오히려 겉핥기같다는 일본인의 평가가 있었다.
Chipyard는 뭐지? chisel based SoC의 agile한 개발을 위한 open source framework라는데, 잘 와닿지 않는다.
(아니 근데 일본에도 System Verilog랑 Chisel, RISC-V 관련 책이 있기는 한데 어떻게 한국은 단 한권도 없냐?)
일단 Chisel은 Scala 언어로 되어있다.
Scala 언어는 객체지향 언어의 특성과 함수형 언어의 특성을 함께 가진다고 한다.
혹자의 말로는, Python과 어느정도 유사해서, Python을 숙달했으면 금방 배운다고는 한다(근데 난 Python을 Pythonic하게 못짜잖아?)
여담으로, 프로그래머 연봉 1위 언어라고 한다. Rust인줄 알았는데, 아니었구만.
Java와 마찬가지로, compile시 byte code를 생성한다고 한다. 즉, interpreter 언어이지만 compile이 필요하다.
어쨌든, Scala부터 배워야 한다.
오! 놀랍게도 백준에 scala가 있긴 하다.
본격적으로 보는건 따로 책을 사서 독학을 해야할테고,
일단 ipynb를 따라가는 것으로 한다.
예제는 shift enter로 한 셀 씩 전진, ctrl enter로 해당 셀만 실행시킬 수 있다.
변수는 var와 val로 표현될 수 있다.
val(상수): immutable label(불변), eager하게 eval된다. => 즉, 상수(내부적으로 getter만 생성된다고 한다)
var(변수): mutable variable => 즉, 변수(내부적으로 getter setter 모두 생성된다고 한다)
덤으로 def도 있는 모양인데, 메소드에 사용되며, lazy하게 eval된다고 한다.
var numberOfKittens = 6
val kittensPerHouse = 101
val alphabet = "abcdefghijklmnopqrstuvwxyz"
var done = false
Scala에서는 자바와 C와 다르게, 세미콜론은 필요없다. scala에서 세미콜론이 쓰인다면 그것은 리눅스마냥 여러 커맨드를 한 줄에 우겨넣는다는것을 의미한다.
// A simple conditional; by the way, this is a comment
if (numberOfKittens > kittensPerHouse) {
println("Too many kittens!!!")
}
// The braces are not required when all branches are one liners. However, the
// Scala Style Guide prefers brace omission only if an "else" clause is included.
// (Preferably not this, even though it compiles...)
if (numberOfKittens > kittensPerHouse)
println("Too many kittens!!!")
// ifs have else clauses, of course
// This is where you can omit braces!
if (done)
println("we are done")
else
numberOfKittens += 1
// And else ifs
// For style, keep braces because not all branches are one liners.
if (done) {
println("we are done")
}
else if (numberOfKittens < kittensPerHouse) {
println("more kittens!")
numberOfKittens += 1
}
else {
done = true
}
여기서 놓친것이 있는데..
무슨 반환값? 이라고 생각할 수 있는데,
마지막으로 선택된 branch에 따라 반환값이 달라진다.
꽤 강력한 기능인데, 함수 또는 클래스 내에서 값을 초기화할때 유용하다고 한다. 아래를 봐보자.
val likelyCharactersSet = if (alphabet.length == 26)
"english"
else
"not english"
println(likelyCharactersSet)
"상수"인 likelyCharacterSet은, 상수임에도 불구하고 runtime에 따라 값이 달라지게 된다!(헉)
사실 합리적으로 생각해보면, 상수에 대한 뜻이 바뀔 일은 사실 없고, 도출공식을 명시했다면, 굳이 상수에 값을 박을 필요가 없다. wire해서 그때그때 eval하면 되니까. 참 현명하다. C였다면 define 변수로 구현했을테지만 말이다. 그러나 그것은 precompile 시간에 결정되고, 이것은 runtime에 결정된다는 점이 다르다.
메서드는 앞서 말한대로 def 키워드로 정의된다.
이 단원에선 편의상 일단 메서드와 함수를 섞어쓴다고 한다(왜?!).
함수 파라미터는 콤마로 구분되어 들어가며, 자료형은 반드시 선택되어야 한다.
인수를 안받는 스칼라 함수는 소괄호가 필요없다. 이것은 클래스가 멤버함수를 가지는 구조를 짤 때 편리한데, 멤버함수를 reference 할 일이 자주 생길때 쏠쏠하게 편리하다고 한다(아니 원래라도 걍 함수포인터 넘기면 되지, 뭐 있나?)
전통적으로, 인자를 안받는 함수(즉 부작용이 없다. 아무것도 바꾸지 않을 것이므로)는 소괄호가 없다. 인자를 받는 함수(부작용의 가능성이 있다. 값을 바꿀 수 있다.)는 소괄호가 있다. => 어 이건 좀 편한데? c에서 const 키워드 argument가 하는 역할이랑 비슷하네.
요약해서, Scala에서 함수가 값을 건드릴지의 여부는 소괄호 여부로 판단하면 된다.
// Simple scaling function with an input argument, e.g., times2(3) returns 6
// Curly braces can be omitted for short one-line functions.
def times2(x: Int): Int = 2 * x
// More complicated function
def distance(x: Int, y: Int, returnPositive: Boolean): Int = {
val xy = x * y
if (returnPositive) xy.abs else -xy.abs
}
int를 받는 2의 x승 함수를 만들었다.
근데 distance는 sqrt(x^2 + y^2) 여야지 왜 이렇게 해놨지?
C++과 마찬가지로, scala는 오버로딩이 된다.
// Overloaded function
def times2(x: Int): Int = 2 * x
def times2(x: String): Int = 2 * x.toInt
times2(5)
times2("7")
넘겨진 argument의 종류에 따라 함수의 실질적 동작이 달라지게 짤 수 있다는 말이다.
위처럼 string형태로 들어온 숫자에 대해서도 2의 승을 구현하였다.
뭐...재귀라고 별거 있겠나? 재귀 자체는 어려운 개념이지만, 문법적으로는 딱히 뭐...
/** Prints a triangle made of "X"s
* This is another style of comment
*/
def asciiTriangle(rows: Int) {
// This is cute: multiplying "X" makes a string with many copies of "X"
def printRow(columns: Int): Unit = println("X" * columns)
if(rows > 0) {
printRow(rows)
asciiTriangle(rows - 1) // Here is the recursive call
}
}
// printRow(1) // This would not work, since we're calling printRow outside its scope
asciiTriangle(6)
바로 4가지 의문이 들었다.
val x = 7
val y = 14
val list1 = List(1, 2, 3)
val list2 = x :: y :: y :: Nil // An alternate notation for assembling a list
val list3 = list1 ++ list2 // Appends the second list to the first list
val m = list2.length
val s = list2.size
val headOfList = list1.head // Gets the first element of the list
val restOfList = list1.tail // Get a new list with first element removed
val third = list1(2) // Gets the third element of a list (0-indexed)
scala의 for에는 to와 until, by라는 세 가지 키워드가 존재한다.
for (i <- 0 to 7) { print(i + " ") }
println()
// 0 1 2 3 4 5 6 7
for(i <- 0 to 10 by 2) { print(i + " ") }
println()
// 0 1 2 3 4 5 6
for(i <- 0 to 10 by 2) { print(i + " ") }
println()
// 0 2 4 6 8 10
<-로 iterate index(?)를 초기화하고, to 변수까지 돈다.
val randomList = List(scala.util.Random.nextInt(), scala.util.Random.nextInt(), scala.util.Random.nextInt(), scala.util.Random.nextInt())
var listSum = 0
for (value <- randomList) {
listSum += value
}
println("sum is " + listSum)
//sum is -152348177
//randomList: List[Int] = List(-999092763, 312842179, 1403705056, -869802649)
//listSum: Int = -152348177
(이게뭔코드야)
Scala의 for는 더 많은 트릭이 있다고 한다.
전통적 for문으로는 번잡할 일을 짧고 직관적으로 해낼 수 있다고 한다.
sum같은 기능은 걍 주어지는 comprehension같은 함수로 가능해서 사실 for의 장점을 설명하시는 부적절한데, for 트릭의 편한점은 이따 알게될거라고 한다(모르겠는데?)
스칼라 코드를 읽기 위해서는 naming convention, design pattern, 연습(어?)은 필수적이다.
코드 재사용은 Chisel의 장점이다.
아무튼, 재사용을 위해서는 convention을 지켜야 한다.
package명은 소문자로 쓴다.
과 같은 separator를 쓰지 않는다. (차라리, good_tools 대신 good.tools같은 계층을 둬라.)
모든 class, method를 가져오고싶을때는 를 쓴다.
import chisel3._
일부 클래스만 가져오고자 한다면 아래처럼 쓴다.
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
// WrapCounter counts up to a max value based on a bit size
class WrapCounter(counterBits: Int) {
val max: Long = (1 << counterBits) - 1
var counter = 0L
def inc(): Long = {
counter = counter + 1
if (counter > max) {
counter = 0
}
counter
}
println(s"counter created with max value $max")
}
딱봐도 counterBits:Int를 통해, 클래스의 인스턴스화시 1개의 정수형 인자를 요구하는걸 확인가능하다
val max: Long = (1 << counterBits) - 1
부분에서 큰 인상을 받았다. Verilog와 매우 유사하다.
아까 말했듯이, wiring한 것 마냥 자동으로 변한다.
max는 val로 선언되었기에, 그 정의는 불변한다. 그러나 그 값은 변한다. 마치 wire처럼.
counter로 끝나는 문장 부분이 중요하다.
마지막 코드블록(중괄호)이 끝나기 전에 표현된 마지막 값이 그 코드블럭의 반환값이 된다. 반환값은 calling statement에 의해 사용될수도 있고, 그냥 버려질수도 있다.
println 문은 defining codeblock에 있다. 따라서, 이것은 class initialization code에 사용된다. 즉 class의 instance가 제작될때마다 호출된다.
println(s"doubled max is ${max + max}")라고 써도 된다.
그렇다면, 중괄호는 code block이고, 이것이 evaluate 된 값은 내부의 리턴값이다.(참고로, scala의 모든 클래스나 자료형은 결국 string으로 묵시적 형변환이 가능하도록 되어있다)
code Block은 parameter를 가져갈 수 있고(argument와 차이가 뭔데?), 기존 언어와 유사하다.
한줄짜리면 굳이 중괄호가 필요없다.
// A one-line code block doesn't need to be enclosed in {}
def add1(c: Int): Int = c + 1
class RepeatString(s: String) {
val repeatedString = s + s
}
즉, 중괄호없는 한줄짜리 코드블럭도 있다. 즉 중괄호는 코드블럭의 필요조건이 아니다.
val intList = List(1, 2, 3)
val stringList = intList.map { i =>
i.toString
}
Code block이 list 클래스의 map 메서드로 넘겨진다.
map은 하나의 parameter를 받는다.
list의 각각의 member가 list로 들어가며, code block은 string으로 변환된 멤버를 반환하고, 그것을 map에 넣는다.
이것으로 유추하다시피, Scala는 이러한 문법을 매우 관대하게 허용한다.
이러한 codeblock은 Anonymous Function이라고 불린다.
더 자세한 것은 Scala Style Guide 를 참조하자.
아마 파이썬과 유사해보인다.
Verilog처럼, named parameter 사용시 인수의 순서를 바꿔도 되고,
Python처럼 기본 인자값을 지정할 수 있다.
def myMethod(count: Int, wrap: Boolean, wrapValue: Int = 24): Unit = { ... } // 기본값 설정가능하다
myMethod(count = 10, wrap = false, wrapValue = 23) // 정석
myMethod(wrapValue = 23, wrap = false, count = 10) // 순서바꿔도 된다
myMethod(wrap = false, count = 10) // override 된게 아니라면, 기본값 사용가능하다
Chisel 학습시 chisel-bootcamp도 너무 좋지만 옛날 자료라 아래 자료로 시작하는것을 추천합니다.
https://github.com/agile-hw/lectures/tree/main
책으로 보고 싶으시다면 이런 자료도 있습니다.
https://github.com/schoeberl/chisel-book
또한 Scala의 컬렉션을 활용한 함수형 프로그래밍에대해 공부해보는걸 추천합니다.
Array 형태로 배열된 모듈들 사이에 복잡한 connection이 필요할 경우 매우 유용합니다.
https://github.com/agile-hw/lectures/tree/main <- 본 자료의 10장~12장
https://www.youtube.com/watch?v=dbOi_Gboi_0
이후 추가 학습이 하고 싶다면
Chisel-bootcamp의 typeclass 파트를 읽어보는걸 추천합니다.
Chisel을 사용한다면 H/W Generator를 개발한다는 건데… 타입시스템을 구축해야할일이 종종 있으므로…
https://github.com/freechipsproject/chisel-bootcamp/blob/master/3.6_types.ipynb
https://www.chisel-lang.org/chisel3/docs/explanations/dataview.html#type-classes
Chisel 3.6.0+ 부터 툴체인에 큰 변화가 있어 이후 자료는 공식문서와 컨퍼런스 영상을 참고해보시면 좋습니다. (현재 6.0.0-M2 까지 출시됨)
https://www.chisel-lang.org/chisel3/docs/introduction.html
https://javadoc.io/doc/org.chipsalliance/chisel_2.13/latest/index.html
https://github.com/freechipsproject/chisel-cheatsheet/releases/tag/3.6.0
https://www.youtube.com/@Chisel-lang
https://www.youtube.com/@chipsalliance8321
Chipyard는 RISC-V 기반 오픈소스 SoC 설계 프레임워크 입니다.
다양한 RISC-V 코어와 NoC Interconnect 같은걸 무료로 사용 할 수 있어서 참 좋죠
Rocket이나 Boom의 경우에는 Instruction 확장을 통해 coprocessor를 설계 할 수도 있습니다.
https://chipyard.readthedocs.io/en/stable/
https://www.youtube.com/@FireSimChipyard