[Chisel] 1 - Introduction to Scala

YumeIroVillain·2023년 7월 13일
0

Chisel 독학

목록 보기
1/44

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로 해당 셀만 실행시킬 수 있다.

스칼라의 장점

  1. embedded DSL 호스팅에 좋다.(뭔소리야? DSL이 뭔데? 위키봐도 모르겠는데?)
  2. 큰 데이터를 처리하는데 강력한 라이브러리가 있다.
  3. 철저한 type system이 있다(마음에 드는군). 따라서 큰 오류를 조기에 잡아낼 수 있다.
  4. 함수 표현 및 넘겨주는게 강력하다(뭔소리야).

스칼라의 변수와 상수: var와 val

변수는 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
}
  1. 소괄호 내에 조건문이 들어가고, 블록은 중괄호로 생성하는 점은 C와 같다.
  2. command가 한 줄일 경우, 중괄호가 필요없다는 점은 같다.
  3. elif가 아니라 else if를 사용한다.
  4. +=를 지원한다(++는 지원하지 않는다)

여기서 놓친것이 있는데..

Scala의 if문은 반환값이 존재한다!

무슨 반환값? 이라고 생각할 수 있는데,
마지막으로 선택된 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한 함수 선언

// 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) 여야지 왜 이렇게 해놨지?

scala의 함수 오버로딩

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의 승을 구현하였다.

Scala의 재귀함수

뭐...재귀라고 별거 있겠나? 재귀 자체는 어려운 개념이지만, 문법적으로는 딱히 뭐...

/** 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)
  1. 중괄호는 C와 마찬가지로 local 변수의 나와바리를 의미한다(고급진말로 scope).
  2. 재귀함수 안에 함수를 선언하고, 또 사용할 수 있다. 즉, 메서드 안에 메서드가 들어갈 수 있다.
    잠깐만, 이거 걍 함수가 객체인가? 함수 안에 함수가 선언되고 사용되는건 희귀한데? 람다함수가 아니라 진짜 함수도 선언할수있는모양인데 이거..?

Scala의 리스트

바로 4가지 의문이 들었다.

  1. 파이썬처럼 리스트에 자료형이 짬뽕으로 들어가지는가? => 안되는듯
    1-2. 리스트 안에 리스트가 들어갈 수 있는가? => 1이 기각이므로 당연히 안되는듯
  2. 튜플같은 불변리스트가 존재하는가? => 없는듯
  3. 함수 인자로 리스트를 넘길때 Reference로 넘기는가 Value로 넘기는가? => 값이 변할지도 모른다고 했으니 Reference 아닐까..? 이건 좀더 알아봐야할듯.
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)
  1. list concatenate는 ::를 통해서 가능하다.
    1-2. ++로도 되는 모양이다.
  2. Nil은 Nothing in List를 의미한다. 걍 빈 리스트이다.(아니 걍 List() 하면 되지, 굳이 왜만든거야)
  3. length와 size를 지원한다. 용도는 같은 모양이다.
  4. head와 tail을 지원한다. 0번이 head다.

Scala의 for 반복문

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 변수까지 돈다.

  • to 7이면 7을 포함해서 돌고(;i<=7;)
  • until 7이면 7을 미포함해서 돈다(;i<7;)
  • by는 변화주는 값을 의미한다(;i+=2).
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 트릭의 편한점은 이따 알게될거라고 한다(모르겠는데?)


Reading Scala

스칼라 코드를 읽기 위해서는 naming convention, design pattern, 연습(어?)은 필수적이다.
코드 재사용은 Chisel의 장점이다.
아무튼, 재사용을 위해서는 convention을 지켜야 한다.

package와 import

package명은 소문자로 쓴다.
과 같은 separator를 쓰지 않는다. (차라리, good_tools 대신 good.tools같은 계층을 둬라.)
모든 class, method를 가져오고싶을때는
를 쓴다.

import chisel3._

일부 클래스만 가져오고자 한다면 아래처럼 쓴다.

import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}

OOP로써의 스칼라

  1. var은 객체이다.
  2. val도 객체이다.
  3. literal value도 객체이다.
  4. function도 객체이다.
  5. 객체는 Class의 instance이다.
  6. Scala의 모든 것은 객체이며, instance화되어 호출될 것이다.
  7. 클래스 정의 시, 프로그래머는
    1. 해당 클래스와 연관된 val, var를 정의하고
    2. 해당 클래스의 인스턴스가 쓸 동작인 함수(메서드)를 정의하고
    3. 클래스는 다른 클래스를 연장할 수 있다(상속 말하는듯)
    4. 연장된 부모클래스는 superclass 라고 한다. 반대는 subclass이다.
    5. subclass는 superclass의 데이터와 메소드를 상속한다.
    6. 클래스가 extend와 override를 할 수 있는 많은 유용하고 통제된 방법이 존재한다.
    7. Class는 trait를 상속할 수 있다. (trait을 lightweight class로 생각해라.)
    8. 객체는 Scala Class의 일종이다(Singleton)
      1. 그들은 객체가 아니다. 우리는 얘들을 인스턴스라고 부른다.

Scala 클래스 예제

// 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")
}
  1. 딱봐도 counterBits:Int를 통해, 클래스의 인스턴스화시 1개의 정수형 인자를 요구하는걸 확인가능하다

  2. val max: Long = (1 << counterBits) - 1
    부분에서 큰 인상을 받았다. Verilog와 매우 유사하다.
    아까 말했듯이, wiring한 것 마냥 자동으로 변한다.

  3. max는 val로 선언되었기에, 그 정의는 불변한다. 그러나 그 값은 변한다. 마치 wire처럼.

  4. counter로 끝나는 문장 부분이 중요하다.
    마지막 코드블록(중괄호)이 끝나기 전에 표현된 마지막 값이 그 코드블럭의 반환값이 된다. 반환값은 calling statement에 의해 사용될수도 있고, 그냥 버려질수도 있다.

  5. println 문은 defining codeblock에 있다. 따라서, 이것은 class initialization code에 사용된다. 즉 class의 instance가 제작될때마다 호출된다.

  6. println(s"doubled max is ${max + max}")라고 써도 된다.
    그렇다면, 중괄호는 code block이고, 이것이 evaluate 된 값은 내부의 리턴값이다.(참고로, scala의 모든 클래스나 자료형은 결국 string으로 묵시적 형변환이 가능하도록 되어있다)


Code Block이란?

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 를 참조하자.


Named Parameter와 Parameter Default

아마 파이썬과 유사해보인다.
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 된게 아니라면, 기본값 사용가능하다
profile
HW SW 둘다 공부하는 혼종의 넋두리 블로그 / SKKU SSE 17 / SWM 11th

4개의 댓글

comment-user-thumbnail
2023년 7월 15일

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 까지 출시됨)

Chipyard는 RISC-V 기반 오픈소스 SoC 설계 프레임워크 입니다.
다양한 RISC-V 코어와 NoC Interconnect 같은걸 무료로 사용 할 수 있어서 참 좋죠
Rocket이나 Boom의 경우에는 Instruction 확장을 통해 coprocessor를 설계 할 수도 있습니다.
https://chipyard.readthedocs.io/en/stable/
https://www.youtube.com/@FireSimChipyard

1개의 답글
comment-user-thumbnail
2023년 9월 26일

글과 댓글 둘다 너무 유익합니다.
Chisel 공부할 예정인데, 해당 내용들 같이 따라가면서 같이 공부해보겠습니다.
양질의 자료와 기록들 감사합니다.

답글 달기
comment-user-thumbnail
2024년 6월 17일

안녕하세요 이제 입학 앞둔 석사 생인데 질문이 있어 드립니다 1. 치젤을 현업에서 많이 사용하나요? 어떤 방향으로 어디에 사용하나요?

  1. 왜치젤을 배우기 시작하셨나요?

감사합니다 ㅎㅎ 항상 글잘보고 있습니다

답글 달기