반응형

Chatgpt 열풍이 부는 가운데,

 

갑자기 혜성처럼 등장해 `오픈 소스`를 자칭하는 딥시크.

 

그리고 우리가 먼저 했다도르 수상 엑사원 3.5

 

웹 운영도 하지 않는 엑사원의 성능이 상당히 궁금해졌다. (할 수도 있는데 안 찾아봄)


한국어로 엑사원 3.5를 검색해 봤는데, 놀랍게도 기사만 2페이지 이상 나오고 깃허브 페이지 등은 방문률이 처참한지 검색 엔진이 저 뒤로 밀어버린 모양이다.

 

아무튼 영어로 EXAONE 3.5 검색해서 깃허브 페이지로 들어간다.

(허깅페이스 라는 곳도 있다고 하는데, AI를 주로 하는게 아니라 잘 모르겠다.)

 

GitHub - LG-AI-EXAONE/EXAONE-3.5: Official repository for EXAONE 3.5 built by LG AI Research

 

GitHub - LG-AI-EXAONE/EXAONE-3.5: Official repository for EXAONE 3.5 built by LG AI Research

Official repository for EXAONE 3.5 built by LG AI Research - LG-AI-EXAONE/EXAONE-3.5

github.com

 

퀵스타트를 가져와 한 번 보도록 하자

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_name = "LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct"

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    trust_remote_code=True,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# Choose your prompt
prompt = "Explain how wonderful you are"  # English example
prompt = "스스로를 자랑해 봐"       # Korean example

messages = [
    {"role": "system", "content": "You are EXAONE model from LG AI Research, a helpful assistant."},
    {"role": "user", "content": prompt}
]
input_ids = tokenizer.apply_chat_template(
    messages,
    tokenize=True,
    add_generation_prompt=True,
    return_tensors="pt"
)

output = model.generate(
    input_ids.to("cuda"),
    eos_token_id=tokenizer.eos_token_id,
    max_new_tokens=128,
    do_sample=False,
)
print(tokenizer.decode(output[0]))

pretrained 모델을 다운받아 사용하는 형식일 것 같고, 

 

model name 의 매개변수는 3개 중에 선택 가능하다고 한다

 

뭐.. gpt api 를 쓸 때랑 크게 다르지 않다

근데 온디바이스 모델은 2.4B parameter를 감당 가능하다고 해도 IoT 임베디드 환경에서 쓸 모델은 없나,, 생각하게 된다

 

한 번 실행을 해 보자.

내 실행환경은 anaconda, Python 3.11.7, i7-12700, rtx 3070을 사용 중이다.

코드 중에 to("cuda")가 있는데 Nvidia 계열 gpu가 아닌 경우에 그냥 cpu로 맞춰주면 될 것 같다

(런타임은 얼마나 걸릴지 장담 못한다)

 

> 여러 패키지가 이미 설치된 환경이기 때문에 나는 단 2개의 종속 패키지들만 설치했다

> accelerate==0.26.0

> transformers==4.43.0

 

실행하게 되면 다운로드를 쭉 받는다 (모델 구조가 어떻게 되어 있는지는 모르겠지만 DNN 구조라 parameter가 많은 듯 하다)

 

로컬에서 돌리면 메모리를 상당 부분 차지한다.

메모리 32gb 이상 환경에서 돌리는 것이 권장되나 보다

 

내 환경에서는 첫 시작에 15분 30초가 소요되었다.(다운로드 포함)

 

이후 2번째 실행에서는 gpu를 열심히 갈궈서 2분 30초만에 실행이 완료되었다.

 

그래도 rtx3070인데.. 파라미터 2.4B 짜리로 교체해보자.

model_name 을 교체해 준다.

model_name = "LGAI-EXAONE/EXAONE-3.5-2.4B-Instruct"

 

그런데도 가중치 값에 추가적인 값들이 들어간 텐서 파일이 5기가를 차지한다

확실히 LLM이 용량 압박이 거세다

 

처음 실행은 못 찍었는데, 2분대로 걸렸다.(다운로드 포함)

 

이후 실행에서는 좀 쓸 만한 정도까지 된 듯 하다(15초 소요)

근데 다른 프롬포트 넣어서 하니까(한글) 50초까지도 소요되는 걸 보면 PC 성능 문젠가 싶기도 하고..

2.4B 모델은 챗봇으로 사용하기도 나쁘지 않은 듯 하다(RTX 3070, 32gb ram 급 PC 로컬에서)

 

양자화(Quantization)된 모델의 경우에도 속도는 상당히 빠를 것 같은데 그거까지 하고싶지는 않다

 

근데 깃허브 밑에 다 설명하니까 그냥 보고 하면 될 듯 함

 

흠......


 

지우는 방법(아나콘다)

 

 

여기 다 들어있으니까 알아서 삭제하면 된다

일반 파이썬이면 또 파이썬 캐시파일로 잡혀서 들어가는지는 모르겠는데 경로 따라 가면 있을 듯

 

맛보기 끝

반응형
반응형

공부한 것을 정리하려고 쓰는 글.

 

AMBA 프로토콜들에 대해서 공부해 보려 한다.

 

먼저, 

 

AXI4

AHB

APB

 

로 크게 나뉘는데, 가장 간단한 APB 를 먼저 볼 것이다.

 

데이터시트는 여기서 확인하자.

 

AMBA APB Protocol Specification

 

Documentation – Arm Developer

 

developer.arm.com

 

기본 타이밍 다이어그램

 

Specification 문서를 읽으며 진행한다.

 

1. APB interface에 대해

The APB protocol is a low-cost interface, optimized for minimal power consumption and reduced interface complexity. The APB interface is not pipelined and is a simple, synchronous protocol. Every transfer takes at least two cycles to complete.

-> APB 프로토콜은 저렴하고, 최소한의 전력 소모 및 인터페이스 복잡성을 줄인, 최적화시킨 프로토콜이 되겠다.

파이프라인 되지 않고, 간단하며, 동기식 프로토콜이다. 각 전송은 완료까지 최소 2사이클이 걸린다.

 

2. 사용하는 신호들

* 문서의 앞쪽에 보면 나오겠지만, n이 붙으면 Active-LOW 라는 뜻이다.

한 번 사용하는 신호들을 살펴보자. 

1. 클럭 : (모든 APB 신호들은 posedge clk 으로 동작한다)

2. 리셋 신호 : (Active-LOW)

3. 주소 : (32비트까지 확장 가능, 바이트 주소를 가리킴(다른 곳에서 사용하는 그것과 동일))

4. 프로텍션 타입 : (3-27장에 나오니 나중에 보도록 한다.) + 프로텍션 타입 확장

5. PSELx : (슬레이브 선택 신호)

6. PENABLE : (전송 활성화 신호)

7. PWRITE : (1일 때 write, 0일 때 read)

8. PWDATA : (PWRITE = 1일 때 전송되는 데이터. 8비트, 16비트, 32비트 크기로 지정 가능)

9. PSTRB : 스트로브 신호는 바이트 단위로 쓸 지 결정하는 신호다. 한 메모리 row가 32-bit 로 구성되면, 4개로 나누어 진다.

10. PREADY : 버스가 준비된 상태인지, 현재 work 상태인지 알리기 위함.

11. PRDATA : PWRITE = 0일 때 버스에서 받아올 값

12. PSLVERR : slave error. 슬레이브에서 보내는 에러 신호

13. PWAKEUP : APB 인터페이스와 어떤 활동이라도 있으면 알려주는 신호인듯 함.

14. PAUSER / PWUSER / PRUSER / PBUSER(Optional) : 사용자 정의 신호

 

2-1. Address bus

An APB interface has a single address bus, PADDR, for read and write transfers. PADDR indicates a byte address.

PADDR is permitted to be unaligned with respect to the data width, but the result is UNPREDICTABLE. For example, a Completer might use the unaligned address, aligned address, or signal an error response.

-> APB 인터페이스를 PADDR 하나로 주소를 모두 표현한다. 또한 바이트 주소를 가리킴.

 PADDR은 데이터 폭에 따라 정렬되지 않은 주소(0x001, 0x002, 0x003)를 사용할 수 있지만, 그 결과는 예측할 수 없다.

슬레이브가 비정렬된 주소를 사용할 수 있지만, 이를 처리하는 방식이 명확하지 않기 때문이다.

 

2-2. Data bus

The APB protocol has two independent data buses, PRDATA for read data and PWDATA for write data,. The buses can be 8 bits, 16 bits, or 32 bits wide. The read and write data buses must have the same width. Data transfers cannot occur concurrently because the read data and write data buses do not have their own individual handshake signals.

-> Write/Read에 사용되는 2개의 독립적인 데이터 버스가 있고, 8비트 / 16비트 / 32비트로 사용 가능하다. 데이터 전송은 동시에 사용되면 안되는데, 각각의 핸드셰이크 신호가 없기 때문임.

 

 


 

3.1 Write Transfer

- with no wait state

1 : PSEL이 high 가 되었을 때, PADDR, PWRITE, PWDATA 가 유효한 값을 가지고 있어야 함.

2 write transfer Access phase : T2에서 PENABLE 이 high, 슬레이브가 PREADY high를 보내서 데이터 수신 준비가 완료되었음을 알려야 함. / 다른 제어 신호들은 전송이 완료될 때 까지 안정적이어야 함(자기 값을 유지해야 함)
3 : 전송 완료시 PENABLE 비활성화, PSEL 비활성화(같은 슬레이브에 대해 추가 전송이 있으면 계속 활성화 될 수 있음)

 

 

- with wait state

1 : Access 단계 동안 PENABLE 신호가 high 상태일 때, 슬레이브가 PREADY 를 LOW 로 설정해 전송을 연장할 수 있음.

-> 이는 슬레이브가 데이터 처리 / 수신 준비 되지 않은 상태임을 알리는 방법

2: PREADY 가 LOW 인 동안 유지되는 신호

PADDR

PWRITE

PSELx

PENABLE

PWDATA

PSTRB

PPROT

PAUSER

PWUSER

 

PREADY 는 PENABLE = 0이면 어떤 값을 가져도 상관 없다(Don't care).

고정된 2클럭 주기로 동작하는 슬레이브는 PREADY 를 항상 HIGH 로 고정시킬 수 있다.

 

 

 

3.2 Write strobe

- 각 바이트를 지정해서 들고오는 형태임.

- 읽기 동작에서는, 모든 PSTRB 가 LOW 가 되어야 함

 

 

 

3.3 Read transfer

- with no wait state

: PADDR, PSEL, PENABLE 은 write 할 때와 동일하게 동작.

PWRITE 의 경우 0일 때 read 이므로 이것만 다름.

슬레이브가 읽기 시에 가장 마지막에 데이터를 제공해야 함.

 

 

 

 

- with wait state

write 할 때와 다른 점은, PWRITE와 PRDATA 밖에 없다.

PWRITE = 0이여야 read,

PRDATA : 마지막 데이터만 마스터가 가져옴. (한 클럭 동안 ready signal 이 high)

 

 

 

 

3.4 Error response

PSLVERR : 전송에 있어서 에러가 발생했는지를 알려주기 위해 사용되는 신호.

APB 전송의 마지막 사이클에서만 valid 한지를 판단한다. (PSEL, PENABLE, PREADY 모두 high)

 

권장 사항(필수는 아님) : PSEL, PENABLE 이 LOW인 경우 / PREADY가 LOW인 경우 -> PSLVERR이 LOW로 유지되는게 권장

 

지원 여부 : 슬레이브가 PSLVERR 지원하지 않는 경우, 마스터로 전달되는 PSLVERR 입력은 LOW 로 고정

없어도 잘 동작한다.

 

다른 프로토콜들과의 브릿징 할 때 매핑하는 방법도 있는데 넘어가도록 함.

 

 

 

3.5 Protection unit support

  HIGH(1) LOW(0)
PROT[0] normal privilege
PROT[1] non-secure secure
PROT[2] instruction data

 

보안 및 접근 제어 관련된 신호임. 미구현 해도 동작에는 지장 X

 

 

3.6 Wake-up signal

- APB5 protocol interface 에만 추가되어 있음

- PWAKEUP은 저전력 설계 지원, 데드락 방지, 트랜잭션 지연 최소화 등을 위해 추가됨

 

모든 슬레이브가 항상 활성 상태라면 PWAKEUP 은 불필요.

 

 

 

유저 신호는 패스.

 

 


 

4.1 Operating state

IDLE : PSELx, PENABLE 비활성 상태

SETUP : PSELx 신호 활성화, SETUP 상태는 1클럭 동안 유지, 다음 rising edge 에서 ACCESS 로 자동 전환

ACCESS : 실제 데이터 전송이 이루어짐. (PENABLE 신호에 따름)

 

SETUP -> ACCESS 전환 시 변경되지 않아야 하는 신호

1. PADDR

2. PPROT

3. PWRITE

4. PWDATA

5. PSTRB

6. PAUSER

7. PWUSER

 

ACCESS 에서 PREADY 가 1이 되면 전송 종료

 

 

 


 

 

 

 

 

 

 

 

반응형
반응형

테스트 중

Hipensan/yolov3-tiny-accelerator: [testing] accelerate conv, maxpool in yolov3-tiny w. Verilog, vivado

반응형

'Verilog' 카테고리의 다른 글

[Verilog] AMBA, APB Protocol  (0) 2024.12.23
[Verilog] HDMI interface - 1 (시작 전 RGB 부터)  (0) 2024.11.07
[Verilog] CNN top module (w. dense)  (0) 2024.11.05
[Verilog] maxpool2d w. line buffer  (0) 2024.10.31
[Verilog] Conv2d w. line buffer  (0) 2024.10.30
반응형

오늘은 HDMI 인터페이스를 공부해봐야겠다는 생각이 들어 한 번 정리하면서 작성해 보려고 한다.

코드는 이번에는 없고, 2편에 코드를 작성해서 올릴 것 같다.

 

먼저 가장 처음 배우게 되는, 간단한 RGB 에 대해 생각을 해 보면(본인은 RGB는 구현이 된 상태임, 간단하다), 

대중적인 640x480@60Hz의 경우,

Video Timings: VGA, SVGA, 720p, 1080p - Project F

 

Video Timings: VGA, SVGA, 720p, 1080p

To work with standard monitors and TVs, you need to use the correct video timings. This how to includes the timings for five standard display modes using analogue VGA, DVI, HDMI, or DisplayPort: 640x480 (VGA), 800x600 (SVGA), 1280x720, and 1920x1080 (30 Hz

projectf.io

이 곳에서 볼 수 있듯이 25Mhz 의 클럭 입력을 필요로 한다.

그럼 이 RGB 가 어떻게 동작할까?

 

사진을 보면 알겠지만, 보통 이렇게 4개로 구성된다.

O 가 화면이 나오는 부분이고, X가 화면이 나오지 않는 부분이다.

  active pixels front porch sync pulse back porch
active pixels O X X X
front porch X X X X
sync pulse X X X X
back porch X X X X

 

이렇게 보면 active pixel 이 굉장히 작아 보이는데, 사실은 반대이다. 

active pixel 이 대부분의 크기를 차지하고 나머지 제어 (porch, sync) 들이 작은 픽셀 크기들을 차지한다.

 

 

그럼 VGA@60Hz의 input clock 25Mhz 는 왜 나온 걸까?

일단 VGA 는 다음과 같은 픽셀을 필요로 한다.

640*480 이미지를 출력하는데 결과적으로 800*525 개의 픽셀이 필요하게 되는 것이다.

640*480 = 307,200

800*525 = 420,000

그래서 한 프레임에 걸리는 시간이 총 420,000 클럭이 필요하게 된다.

근데 우리가 1초에 60개의 프레임을 VGA out 으로 보내줘야 하기 때문에 60을 곱해준다.

420,000 * 60 = 25,200,000

클럭 자체는 25.2M 번 필요하게 된다.

그래서 입력 클럭으로 25Mhz 를 받는다.

 

그럼 이제 이 입력을 받을 VGA 단자에 대해 알아보자.

 

VGA 단자 - 위키백과, 우리 모두의 백과사전

 

VGA 단자 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. VGA 케이블 VGA 단자(VGA connector)라고 일반적으로 알려진 것(RGB 커넥터, D-sub 15, mini sub D15와 미니 D15를 포함)은 3열 15핀의 DE-15이다. 총 4개의 버전이 있다: 초기버전

ko.wikipedia.org

핀을 다음과 같이 받는다고 한다.

이것만 봐서는 솔직히 잘 구분하기 힘들다.

Altera 사의 DE1-SoC 보드의 입력이 어떻게 들어가는지 한 번 보도록 하자.

이건 DE1-SoC 보드의 메뉴얼이다.

https://people.ece.cornell.edu/land/courses/ece5760/DE1_SOC/DE1-SoC_User_manualv.1.2.2_revE.pdf

 

p.34를 보자.

들어가는 입력으로 R[7:0], G[7:0], B[7:0], 클럭, 싱크, blank, VS, HS 이 들어가는 것을 볼 수 있다.

이전에 개인적으로 sync 의 경우 없어도 동작하는데는 문제가 없는 것을 확인했다.

 

그런데 VGA 단자 입력으로는 R, G, B 는 모두 하나밖에 들어가지 않는다.

이는 DAC(digital-analog converter) 에 의해 표현된 신호 값이 아날로그 전압 값으로 변환되어 들어가기 때문이다.

정확한지는 모르겠는데 GPT 가 말하기로는, 이 신호 값이 전압 값으로 변환되어 들어가는데, 0V에서 0.7V 사이 값으로 들어간다고 한다. 

그래서 0V일 때는 꺼진 상태, 0.7V에는 최대 밝기 상태가 된다. (라고 한다)

 

blank 의 경우에는 원래 없는 핀이다.

DAC 에 들어가는 하나의 신호가 이 보드(DE1-SoC)에서 추가로 지원을 해 주는 느낌이라 생각하면 되겠다.

blank = 1이면 아마 빈 신호로 입력을 받지 않을까 라고 추측해 본다.(반대일 수도 있다)

 

VS, HS 의 경우에는 수평 라인 동기화 신호다. RGB는 left-top to right-bottom 으로 입력 픽셀이 진행되기 때문에,

한 라인의 끝에 도달하면 HSync 가 먼저 high 로 변하고, 다음 클럭에 다시 low 로 바뀐다.

( 정확하게는 sync pulse 일 때 0이고, 나머지는 모두 1 상태이다.)

그리고 한 프레임의 끝에 도달하면 VSync 가 high 로 변하고, 다음 클럭에 다시 low로 바뀐다.

(Vsync 도 동일하다. sync pulse 일 때 0이고, 나머지 모두 1 상태이다.)

 

hsync, vsync 모두 동일하다.

 

 


그럼 이제 HDMI 에 대해서 알아보자.

HDMI - 위키백과, 우리 모두의 백과사전

 

HDMI - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. HDMI, 고선명 멀티미디어 인터페이스 HDMI 표준 단자, 공식 로고종류 디지털 오디오/비디오 단자설계자 7개 회사설계일 2002년 12월제조사 HDMI 사용자 (1100개 이상)

ko.wikipedia.org

 

HDMI 의 경우, 핀이 굉장히 많다.

우리는 소리 출력 보다는 영상 출력이 좀 더 중요하기 때문에, 1080p@60Hz 출력을 목표로 해 본다.

HDMI 는 또 버전이 많다.

여기서는 가장 기본이 되는 HDMI 1.0 을 기준으로 한 번 배워 본다.

 

GPT 가 알려 주기를,

TMDS DATA 0 : R 채널, 1 : G 채널, 2 : B 채널 이라고 한다.

또한 shield 핀의 경우 각 채널별 접지를 제공해 주어 신호 간섭을 줄이는 역할이며, 입력 데이터가 들어가지 않는다고 한다.

 

그리고 TMDS 가 데이터를 어떻게 인코딩하는지도 알아본다.

이는 8비트 데이터를 10비트로 변환하여 전송하는 방식을 채택하고 있다. (8b/10b encoding)

 

8b/10b 인코딩은 위키를 참고하자.

8b/10b encoding - Wikipedia

 

8b/10b encoding - Wikipedia

From Wikipedia, the free encyclopedia Line code mapping 8-bit words to 10-bit symbols In telecommunications, 8b/10b is a line code that maps 8-bit words to 10-bit symbols to achieve DC balance and bounded disparity, and at the same time provide enough stat

en.wikipedia.org

이 인코딩은, 아래 5비트를 6비트로 만들고, 상위 3비트를 4비트로 만들고 두 개를 합치는 인코딩 방법이다.

데이터 심볼은 다음과 같이 표기될 수도 있다고 한다.

D.x.y (x : 0~31, y:0~7)

왜냐하면, 2^5 = 32, 2^3 = 8 이기 때문에 초기 8비트를 표현하는 방식이라고 봐야되겠다.

또한 데이터 심볼에 넣을 수 있는 12개의 특별한 심볼이 있다고 한다. 이는 아마 컨트롤 용도로 사용이 되겠다.

얘네는 K.x.y 라고 표현된다.

 

또한 8b를 10b로 인코딩하기 때문에, 1024개의 가능 조합 중 일부를 제외할 수 있다고 한다.

1. 연속적으로 동일한 비트가 최대 5개까지만 나타나도록 제한한다.

2. 0과 1의 개수 차이가 최대 2를 넘지 않도록 보장해 신호의 DC 균형을 맞춘다.

3. 256개의 가능한 8비트 워드 중 일부는 두 가지 다른 방식으로 인코딩 될 수 있다.

 

[인코딩 선택]

각각의 6비트 혹은 4비트 코드워드는 다음을 만족한다.

- 0과 1의 개수가 동일한 경우

- 두 가지 형태가 존재하는 경우, 하나는 0이 2개 더 많은 형태 / 다른 하나는 1이 2개 더 많은 형태

불균형이 0이 아닌 6비트 또는 4비트 코드를 사용할 때는 양수, 또는 음수 불균형 중 현재 실행 불균형을 반전시키는 인코딩을 사용해야 한다. - 즉, 불균형이 있는 코드들은 교대로 사용해서 실행 불균형을 조정한다. (??)

 

[실행 불균형]  -- 이후에 계속 적을 예정 --

8b/10b 인코딩은 DC 균형을 유지하여, 장기적으로 전송되는 1과 0의 비율이 정확히 50%가 되도록 합니다. 이를 위해 전송되는 1과 0의 개수 차이는 항상 ±2로 제한되며, 각 심볼이 끝날 때마다 이 차이는 +1 또는 -1 상태가 됩니다. 이 차이를 **실행 불균형(Running Disparity, RD)**이라고 합니다.

이 방식에서는 실행 불균형을 위해 +1과 -1 두 가지 상태만 필요하며, 초기 상태는 -1에서 시작합니다.

각 5b/6b 및 3b/4b 코드에서 1과 0의 개수가 같지 않은 경우, 이를 전송하는 데 사용할 수 있는 두 가지 비트 패턴이 있습니다: 하나는 "1" 비트가 2개 더 많고, 다른 하나는 모든 비트를 반전시켜 "0"이 2개 더 많습니다. 현재 신호의 실행 불균형에 따라 인코딩 엔진은 주어진 데이터에 대해 두 가지 6비트 또는 4비트 패턴 중 하나를 선택하여 전송합니다.

6비트 또는 4비트 코드에서 1과 0의 개수가 같으면 불균형이 유지되지 않기 때문에 선택의 여지가 없으나, 예외적으로 D.07(00111)D.x.3(011) 서브블록에서는 두 가지 선택이 가능합니다.

이 두 서브블록에서는 실행 불균형이 변하지 않지만, 다음 규칙을 따릅니다:

  • RD가 양수(+)일 때 D.07을 만나면 000111을 사용하고, 음수(-)일 때는 111000을 사용합니다.
  • RD가 양수(+)일 때 D.x.3을 만나면 0011을 사용하고, 음수(-)일 때는 1100을 사용합니다

 

 

 

 

잘 설명해 놓은 github 가 있어서 들고 와 봤다.

GitHub - mithro/tmds_encoding: TMDS encoding tools

 

GitHub - mithro/tmds_encoding: TMDS encoding tools

TMDS encoding tools. Contribute to mithro/tmds_encoding development by creating an account on GitHub.

github.com

 

 

반응형

'Verilog' 카테고리의 다른 글

[Verilog] AMBA, APB Protocol  (0) 2024.12.23
[Verilog & Project] Yolov3-tiny  (0) 2024.12.02
[Verilog] CNN top module (w. dense)  (0) 2024.11.05
[Verilog] maxpool2d w. line buffer  (0) 2024.10.31
[Verilog] Conv2d w. line buffer  (0) 2024.10.30
반응형

공부를 하면서 포스팅을 하고 있는 입장이기에, 코드의 정확성은 보장하지 못하거니와

더욱 효율적으로 코드를 작성할 방법도 굉장히 많을 것이다.

하지만 간단한 예시를 들어 모듈을 만들어 보고자 하는 것이 목적이기에 verilog 로 cnn top 모듈을 한 번 만들어 보았다.

참고로, bias 를 더하는게 없다. 직접 만들어 보는 것도 좋을 것 같다.

 

전에 conv2d 와 maxpool 레이어는 작성을 했는데, dense 층 같은 경우에는 포스팅을 하지 않았었다.

module dense #
    (
        parameter DATA_WIDTH = 16,
        parameter IN_SIZE = 10,         // test with small one
        parameter OUT_SIZE = 5
    )
    (
        input i_clk,
        input i_rst,
        input i_start,
        input wire signed [DATA_WIDTH-1:0] i_data,
        output wire signed [DATA_WIDTH-1:0] o_data,
        output o_valid,
        output reg o_done 
    );

// 한 클럭에 입력 데이터가 모든 가중치값과 곱해짐
// 이를 in size 번 만큼 반복

localparam  IDLE = 0,
            WORK = 1,
            DATA_OUT = 2;

reg [1:0] c_state, n_state;
reg signed [DATA_WIDTH - 1:0] weight[0:OUT_SIZE-1];
reg signed [DATA_WIDTH - 1:0] c_result[0:OUT_SIZE-1], n_result[0:OUT_SIZE-1];
reg [$clog2(IN_SIZE):0] c_cnt, n_cnt;

integer j;
initial
begin
    // fixed(1)
    for(j=0; j<OUT_SIZE; j=j+1) weight[j] = 16'h0100 + j * 10;
end

integer i;

always@(posedge i_clk, negedge i_rst)
if(!i_rst) begin
    c_state <= 0;
    for(i = 0; i < OUT_SIZE; i=i+1)             c_result[i] <= 0;
    c_cnt <= 0;
end else begin
    c_state <= n_state;
    for(i = 0; i < OUT_SIZE; i=i+1)             c_result[i] <= n_result[i];
    c_cnt <= n_cnt;
end


always@* begin
    n_state = c_state;
    n_cnt = c_cnt;
    o_done = 0;
    for(i = 0; i < OUT_SIZE; i=i+1)         n_result[i] = c_result[i];
        
    case(c_state)
        IDLE: begin
            n_cnt = 0;
            for(i = 0; i < OUT_SIZE; i=i+1)         n_result[i] = 16'b0;


            if(i_start) begin 
                n_state = WORK;
                for(i = 0; i < OUT_SIZE; i=i+1) begin
                    n_result[i] = i_data * weight[i];     // start flag comes with valid data
                    n_cnt = 1;
                end
            end
        end


        WORK: begin
            n_cnt = c_cnt + 1;
            if(c_cnt == IN_SIZE-1) begin 
                n_state = DATA_OUT;
                n_cnt = 0;          // data out counter;
            end


            // mac unit
            for(i = 0; i < OUT_SIZE; i=i+1) begin
                n_result[i] = c_result[i] + i_data * weight[i];
            end
        end

        DATA_OUT: begin
            n_cnt = c_cnt + 1;
            if(c_cnt == OUT_SIZE-1)  begin 
                n_state = IDLE;
                o_done = 1;
            end
        end
    endcase
end


assign o_valid = c_state == DATA_OUT;
assign o_data = c_state == DATA_OUT ? c_result[c_cnt] : 0;



endmodule

 

dense 의 경우 하드코드된 가중치를 준다.

하나의 값이 들어가서 모두 곱해지는 단순한 형태이다.

 


CNN top file 에 대해서 한 번 보도록 하자.

module CNN_top_valid_tester#
	(
		parameter DATA_WIDTH = 16,
		parameter CONV1_OUT_CH = 4

	)
	(
	/* Data input from bram */
	input i_clk, 			// 25Mhz input clk (vga clk)
	input i_rst,
	input i_start, 					// 채널 하나씩 받아야 함(i_start 신호 ci개 필요)
	input [DATA_WIDTH-1:0] i_data, 	// RGB565 data, data input : 6*6 image

	/* output inform */
	output [DATA_WIDTH-1:0] o_is_apple,
	output [DATA_WIDTH-1:0] o_apple_pix_length,
	output reg c_done

	);

localparam 	INPUT_WIDTH = 128,
			INPUT_HEIGHT = 128;

localparam 	ONE_KERNEL_SIZE = DATA_WIDTH * 3 * 3; 	// 16 * 9 = 144
localparam 	IDLE = 0,
			CONV1 = 1,
			MAXPOOL1 = 2,
			DENSE1 = 3;

reg [1:0] c_state, n_state;
wire [3*DATA_WIDTH-1:0] rgb_o_data;

/*============================\\
			R E G
\\============================*/
wire [CONV1_OUT_CH*DATA_WIDTH*9-1:0] r_conv1_kernel; 			// ci(3) * co(4) * 9 * bit-width(16) = 576 * 3 (ci 없애고 클럭 늘림)


// signed?
wire [CONV1_OUT_CH*DATA_WIDTH-1:0] conv1_o_pixel; 		// co(4) * data_width(16) = 64
wire [CONV1_OUT_CH-1:0] conv1_o_done; 				// co(4) 
wire [CONV1_OUT_CH-1:0] conv1_o_valid;
wire maxpool1_i_start;
wire [CONV1_OUT_CH*DATA_WIDTH-1:0] maxpool1_o_data; 			// co(4) * data_width(16) = 64
wire [CONV1_OUT_CH-1:0] maxpool1_o_done;
wire [CONV1_OUT_CH-1:0] maxpool1_o_valid; 		// co(4)
reg  [$clog2((INPUT_WIDTH/2) * (INPUT_HEIGHT/2))-1:0] c_conv1_cnt, n_conv1_cnt; 	// memory counter ????????????????????????????????????? width/2*height*2
wire [CONV1_OUT_CH*DATA_WIDTH-1:0] conv1_o_bram_data; 	// co(4) * data_width(16) = 64



wire [$clog2(3*DATA_WIDTH)-1:0] rgb_slicer; 		// 안하니까 오류나는데
wire [CONV1_OUT_CH*DATA_WIDTH-1:0] mac_mem_o_bram_data;
reg [$clog2(CONV1_OUT_CH)-1:0] c_rom1_i_cnt, n_rom1_i_cnt;
reg [$clog2(INPUT_WIDTH * INPUT_HEIGHT)-1:0] c_conv1_mac_cnt, n_conv1_mac_cnt;

reg [$clog2(INPUT_WIDTH * INPUT_HEIGHT)-1:0] c_mac_mem_out_cnt, n_mac_mem_out_cnt;


wire dense1_i_start;
wire dense1_o_valid;
wire [DATA_WIDTH-1:0] dense1_o_data;

reg [$clog2((INPUT_WIDTH/2) * (INPUT_HEIGHT/2))-1:0] c_conv1_rd_mem_addr, n_conv1_rd_mem_addr;
reg [$clog2(CONV1_OUT_CH)-1:0] c_dense1_cnt, n_dense1_cnt;
reg [DATA_WIDTH-1:0] dense1_i_data;
wire dense1_o_done;

reg [6*DATA_WIDTH-1:0] buf_dense_out; 			// dense out 개수
reg n_done;
/*============================\\
	S U B _ M O D U L E 
\\============================*/
rgb565_divider #
	(
	.DATA_WIDTH(DATA_WIDTH)
	) 	
	rgb565div
	(
	.i_data(i_data),
	.o_data(rgb_o_data) 		// divided to rgb_o_data[DATA_WIDTH*i+:DATA_WIDTH]
	);

kernel1_rom conv1_rom
	(		
		.i_ci_cnt(c_rom1_i_cnt), 			
		.o_kernel(r_conv1_kernel)
		// .o_bias()
	);


// generate 
// 생긴 모양 : 16 | 16 | 16 으로 비트가 쪼개져서 들어갈듯
// CONV1
genvar ci, co;
generate
	// for(ci = 0; ci < 3; ci=ci+1) begin : conv1_ci 		// 한 차원씩 연산해 누적합
		for(co = 0; co<CONV1_OUT_CH; co=co+1) begin : conv1_co

		conv2d#
			(
				.DATA_WIDTH(DATA_WIDTH),
		    	.IMG_WIDTH(INPUT_WIDTH),
		    	.IMG_HEIGHT(INPUT_HEIGHT),
		    	.PADDING(1),
		    	.KERNEL_SIZE(3)
			)
			conv1_inst_
			(
				.i_Clk(i_clk),
				.i_Rst(i_rst),
				.i_start(i_start),
				.i_pixel(rgb_o_data[rgb_slicer*DATA_WIDTH +: DATA_WIDTH]), 	// 커널 나오는건 무시하고 입력 채널은 하나씩 들어가야 함
				.i_kernel(r_conv1_kernel[co*ONE_KERNEL_SIZE+:ONE_KERNEL_SIZE]), 		// 한 입력 채널 커널 다 돌려야 하니까
				.o_pixel(conv1_o_pixel[co*DATA_WIDTH+:DATA_WIDTH]), 	
				.o_done(conv1_o_done[co]), 		
				.o_valid(conv1_o_valid[co]) 		
			);
		end
	// end
endgenerate
// maxpool 에 들어가기 전 가중치에 따른 conv 연산을 모두 적용
// 개별 ci 에 대해 co를 진행하고, 나온 값들을 저장







generate
	for(co=0; co<CONV1_OUT_CH; co=co+1) begin : mac_mem_conv1 		// 더하는 메모리 (bias 도 더해야 하긴 함)
		mac_mem#
		(
			.DATA_WIDTH(DATA_WIDTH),
			.DEPTH(INPUT_WIDTH * INPUT_HEIGHT)
		)
		conv1_only_mem_inst_
		(
		.i_rst(i_rst),
		.i_wclk(i_clk),
		.i_wr_addr(c_conv1_mac_cnt),
		.i_wr(&conv1_o_valid),
		.i_rclk(i_clk), 		
		.i_rd_addr(c_mac_mem_out_cnt),
		.i_rd(1'b1),
		.i_bram_en(&conv1_o_valid),
		.i_bram_data(conv1_o_pixel[co*DATA_WIDTH+:DATA_WIDTH]),
		.o_bram_data(mac_mem_o_bram_data[co*DATA_WIDTH+:DATA_WIDTH])
		);
	end
endgenerate







// CONV1-maxpool-relu
// should integrate relu module
generate
	for(co = 0; co<CONV1_OUT_CH; co=co+1) begin : maxpool1
		maxpool2d_gpt#
		(
		    .DATA_WIDTH(DATA_WIDTH),
		    .IMG_WIDTH(INPUT_WIDTH),
		    .IMG_HEIGHT(INPUT_HEIGHT)
		)
		maxpool1_inst_
		(
		    .i_clk(i_clk),
		    .i_rst(i_rst),
		    .i_start(maxpool1_i_start),
		    .i_data(mac_mem_o_bram_data[co*DATA_WIDTH+:DATA_WIDTH]),
		    .o_data(maxpool1_o_data[co*DATA_WIDTH+:DATA_WIDTH]),
		    .o_done(maxpool1_o_done[co]),
		    .o_valid(maxpool1_o_valid[co])
		);
	end
endgenerate


// CONV1 memory (should make more memory modules!!)
// save maxpool data, when o_valid == true
generate
	for(co = 0; co<CONV1_OUT_CH; co=co+1) begin : conv1_mem
		conv_mem#
		(
			.DATA_WIDTH(DATA_WIDTH),
			.DEPTH((INPUT_WIDTH/2) * (INPUT_HEIGHT/2))
		)
		conv1_mem_inst_
		(
			.i_rst(i_rst),
			.i_wclk(i_clk),
			.i_wr_addr(c_conv1_cnt),		// counter if &maxpool1_o_valid, then cnt + 1 
			.i_wr(&maxpool1_o_valid), 		
			.i_rclk(i_clk),
			.i_rd_addr(c_conv1_rd_mem_addr), 		 			
			.i_rd(1'b1), 					// if conv1 end(include saving)? and state == CONV2?
			.i_bram_en(&maxpool1_o_valid),		// &maxpool1_o_valid ?
			.i_bram_data(maxpool1_o_data[co*DATA_WIDTH+:DATA_WIDTH]),
			.o_bram_data(conv1_o_bram_data[co*DATA_WIDTH+:DATA_WIDTH])
		);
	end
endgenerate




// dense
dense#
    (
        .DATA_WIDTH(DATA_WIDTH),
        .IN_SIZE((INPUT_WIDTH/2) * (INPUT_HEIGHT/2)*CONV1_OUT_CH), 			// img width * height * co 
        .OUT_SIZE(6)
    )
    dense1_inst_
    (
        .i_clk(i_clk),
        .i_rst(i_rst),
        .i_start(dense1_i_start),
        .i_data(dense1_i_data),
        .o_data(dense1_o_data),
        .o_valid(dense1_o_valid),
        .o_done(dense1_o_done)
    );







/*============================\\
				F F 
\\============================*/
always@(posedge i_clk, negedge i_rst)
	if(!i_rst) begin
		buf_dense_out <= 0;
	end else begin
		if(c_state == DENSE1)
			if(dense1_o_valid)
				buf_dense_out <= {buf_dense_out[79:0], dense1_o_data};
		else buf_dense_out <= 0;
	end


always@(posedge i_clk, negedge i_rst)
	if(!i_rst) begin
		c_state <= 0;
		c_conv1_cnt <= 0;
		c_rom1_i_cnt <= 0;
		c_conv1_mac_cnt <= 0;
		c_mac_mem_out_cnt <= 0;
		c_conv1_rd_mem_addr <= 0;
		c_dense1_cnt <= 0;
		c_done <= 0;
	end else begin
		c_state <= n_state;
		c_conv1_cnt <= n_conv1_cnt;
		c_rom1_i_cnt <= n_rom1_i_cnt;
		c_conv1_mac_cnt <= n_conv1_mac_cnt;
		c_mac_mem_out_cnt <= n_mac_mem_out_cnt;
		c_conv1_rd_mem_addr <= n_conv1_rd_mem_addr;
		c_dense1_cnt <= n_dense1_cnt;
		c_done <= n_done;
	end


/*============================\\
			C O M B 
\\============================*/
always@* begin
	n_state = c_state;
	n_conv1_cnt = c_conv1_cnt;
	n_rom1_i_cnt = c_rom1_i_cnt;
	n_conv1_mac_cnt = c_conv1_mac_cnt;
	n_mac_mem_out_cnt = c_mac_mem_out_cnt;
	n_conv1_rd_mem_addr = c_conv1_rd_mem_addr;
	n_dense1_cnt = c_dense1_cnt;
	dense1_i_data = 0;
	n_done = 0;

	case(c_state)
		IDLE: begin
			if(i_start) 	n_state = CONV1;
		end

		CONV1: begin
			if(&conv1_o_valid)	 									n_conv1_mac_cnt = c_conv1_mac_cnt + 1;

			if(c_conv1_mac_cnt == INPUT_WIDTH * INPUT_HEIGHT-1) 	n_conv1_mac_cnt = 0; 	// mac mem 에 저장하기 위한 카운터, 나중에 값 바꿔야 함


			if(&conv1_o_done)  										n_rom1_i_cnt = c_rom1_i_cnt + 1;


			if(c_rom1_i_cnt == CONV1_OUT_CH-1) 	begin 
				n_rom1_i_cnt = 0; 

				// maxpool start
				n_state = MAXPOOL1;
				n_conv1_mac_cnt = 0;
			end
	
		end

		MAXPOOL1: begin
			n_mac_mem_out_cnt = c_mac_mem_out_cnt + 1;

			// when memory saving
			if(&maxpool1_o_valid) n_conv1_cnt = c_conv1_cnt + 1;

			// memsave end?
			if(&maxpool1_o_done)
			begin
				n_conv1_cnt = 0;
				n_mac_mem_out_cnt = 0;
				n_state = DENSE1;
			end
		end

		DENSE1: begin
			n_dense1_cnt = c_dense1_cnt + 1;
			if(c_dense1_cnt == CONV1_OUT_CH-1) begin
				n_dense1_cnt = 0;
				n_conv1_rd_mem_addr = c_conv1_rd_mem_addr + 1;
			end

			dense1_i_data = conv1_o_bram_data[c_dense1_cnt*DATA_WIDTH+:DATA_WIDTH];

			

			if(dense1_o_done) begin
				n_state = IDLE;
				n_done = 1;
			end

		end
	endcase

end

assign dense1_i_start = c_state == DENSE1;
assign maxpool1_i_start = c_state == MAXPOOL1 ? !(&maxpool1_o_done) : 0;
assign rgb_slicer = c_rom1_i_cnt;
assign o_is_apple = buf_dense_out[95:80];
assign o_apple_pix_length = buf_dense_out[31:16];


endmodule

이건 나중에 다중 레이어 구현을 위해 FPGA 로직 게이트를 최소한으로 차지하게 만들려고 클럭을 많이 사용했다.

단점은, start sign 이 channel input 개 만큼 들어가서 conv2d 를 작동시켜야 하는 단점이 있다.

channel input, channel output 이 합쳐진 CNN 도 있기는 한데, 이건 지금은 패스하고 만든 이 모듈에 대해서만 설명한다.

 

OV7670 에서 가져온 rgb565 데이터를 물려서 CNN 측정 해 보는 것이 지금의 목표이지만, 정말로 FPGA 상에서 잘 작동하는지 확인해야 하기 때문에 간단하게 하나씩 레이어를 만들어서 구현해 놓았다.

 

 

1.0_cnn_top_splitted_in_channel.zip
0.01MB

 

공부하는데 참고가 되었으면 한다.

 

반응형
반응형

이번엔 line buffer 를 이용한 maxpool2d 모듈을 한 번 만들어 보도록 한다.

 

1. Overview

maxpool 은 pool size 중 가장 큰 값을 추출하는 과정이다.

예시를 들어서 하나의 input image 가 있다면,

(0,0) (0,1) (0,2) (0,3) (0,4) (0,5)
(1,0) ... ... ... ... ...
(2,0) ... ... ... ... ...
(3,0) ... ... ... ... ...
... ... ... ... ... ...
... ... ... ... ... ...

 

이 중에서

[(0,0), (0,1),

(1,0), (1,1)] 부터 시작해서 이 중에서 가장 큰 값을 찾아나가는 과정이다.

이 때 pool size 가 2라면 stride 가 2이고, 처음 연산을 진행한 후 윈도우 위치를 2만큼 옮긴다.

 

이 과정을 거치면서, stride 가 2이므로 input image size 가 절반으로 줄어든다.

 

input image size (height, width) : (6, 6) -> (3, 3) 

 

이를 Verilog, line buffer 를 사용해서 구현을 해 보도록 한다.

 


line buffer 에는, 늘 그렇듯이, 데이터가 한 클럭마다 계속 들어오는 구조를 지닌다.

근데 이 데이터가 들어오는 시점을 조절 해야 하는데, 이를 start flag 로 지정한다. (전 모듈이 convolution 이니, output o_valid 이 처음 들어왔다면 시작 신호로 보면 될 것이다.)

start flag 가 들어 왔다면, 이후에는 지속적으로 연산을 진행한다. (이는 clock dependency 를 가진다)

 

따라서 나는 State 를 다음과 같이 설정했다.

Start signal 을 받는 IDLE, 

line buffer 를 채우는 동작인 BUF_FILL,

버퍼가 다 채워지면 실제 maxpool 연산을 수행하는 WORK.

 

초기에 만든 코드는 다음과 같다. (동작하지는 않을 것이다.)

module maxpool2d#
    (
        parameter DATA_WIDTH = 16,  // fixed point 16-bit
        parameter IMG_WIDTH = 6,    // original image size
        parameter IMG_HEIGHT = 6,
    )
    (
        input i_clk,
        input i_rst,
        input i_start,
        input [DATA_WIDTH-1:0] i_data,
        output reg [DATA_WIDTH-1:0] o_data,
        output reg o_valid                      // current o_data validation flag
    );

/*========================================\\
                P A R A M                  
\\========================================*/
localparam  IDLE = 0,
            BUF_FILL = 1,
            WORK = 2;

localparam  LINE_BUF_SIZE   = IMG_WIDTH * 2;
localparam  MAX_WORK        = IMG_WIDTH / 2;
localparam  HALF_BUF_SIZE   = LINE_BUF_SIZE / 2;

/*========================================\\
                R E G                   
\\========================================*/
reg [DATA_WIDTH-1:0] c_line_buf[0:LINE_BUF_SIZE-1];
reg [DATA_WIDTH-1:0] n_line_buf[0:LINE_BUF_SIZE-1];       // 2 lines of image width size buffer


reg [1:0] c_state, n_state;
reg [$clog2(IMG_WIDTH):0] c_x, n_x;         // line buf x-counter
reg [$clog2(IMG_WIDTH):0] c_y, n_y;         // line buf y-counter
reg [$clog2(MAX_WORK):0] c_work_cnt, n_work_cnt;
integer i;
/*========================================\\
                F F                   
\\========================================*/
always@(posedge i_clk, negedge i_rst)
if(!i_rst) begin
    c_state <= 0;
    for(i = 0; i < LINE_BUF_SIZE; i=i+1)
        c_line_buf[i] <= 0;
    c_x <= 0;
    c_y <= 0;
    c_work_cnt <= 0;
end else begin
    c_state <= n_state;
    for(i = 0; i < LINE_BUF_SIZE; i=i+1)
        c_line_buf[i] <= n_line_buf[i];
    c_x <= n_x;
    c_y <= n_y;
    c_work_cnt <= n_work_cnt;
end




always@* begin
    n_state = c_state;
    n_line_buf = c_line_buf;

    // x, y movement (always, buf_filling & working)
    n_x =  (c_x == IMG_WIDTH - 1)  ? c_x + 1 : 0;
    n_y =  (c_y == IMG_HEIGHT)     ? 0 : 
            (c_x == IMG_WIDTH - 1)  ? c_y + 1 : c_y;

    n_work_cnt = 0;        
    o_valid = 0;

    case(c_state)
        IDLE: begin
            n_x = 0;
            n_y = 0;
            for(i = 0; i < LINE_BUF_SIZE; i=i+1)    n_line_buf[i] = 0;
            
            
            if(i_start) begin
                n_state = BUF_FILL;
                n_line_buf[0] = i_data;        // start flag with 1 input data
                n_x = c_x + 1;                 // start flag with 1 input data
            end

        end

        BUF_FILL: begin
            // buffer movement
            n_line_buf[0] = i_data;
            for(i = 1; i < LINE_BUF_SIZE; i=i+1)
                n_line_buf[i] = c_line_buf[i-1];       // buffer left shift 

            

            if(c_x == IMG_WIDTH-1 && c_y[0]) begin      // if buffer filled
                n_state = WORK;


            end

        end

        WORK: begin
            o_valid = 1;   // wire ? reg ? 
            o_data  = result;
            // buffer movement
            n_line_buf[0] = i_data;
            for(i = 1; i < LINE_BUF_SIZE; i=i+1)
                n_line_buf[i] = c_line_buf[i-1];       // buffer left shift 

            // work counter, MAX == IMG_WIDTH / 2
            n_work_cnt = c_work_cnt + 1;

            n_state =  c_work_cnt == MAX_WORK ? 
                        c_y == (IMG_HEIGHT - 1) ? IDLE : BUF_FILL : c_state;

        end
    endcase

end

// make this to register ? or wire ?
wire signed [DATA_WIDTH-1:0] comp1, comp2, result;
assign comp1 = c_line_buf[LINE_BUF_SIZE - c_work_cnt] > c_line_buf[LINE_BUF_SIZE - c_work_cnt - 1] ? 
                c_line_buf[LINE_BUF_SIZE - c_work_cnt] : c_line_buf[LINE_BUF_SIZE - c_work_cnt - 1];
assign comp2 = c_line_buf[HALF_BUF_SIZE - c_work_cnt] > c_line_buf[HALF_BUF_SIZE - c_work_cnt - 1] ?
                c_line_buf[HALF_BUF_SIZE - c_work_cnt] : c_line_buf[HALF_BUF_SIZE - c_work_cnt - 1];
assign result = comp1 > comp2 ? comp1 : comp2;




endmodule

 

 

그리고 gpt 를 돌려서 수정한 코드는 다음과 같다. (이건 동작한다. 하지만 세부적인 검증은 해보지 않았다.)

module maxpool2d_gpt#
(
    parameter DATA_WIDTH = 16,  // fixed point 16-bit
    parameter IMG_WIDTH = 6,    // original image size
    parameter IMG_HEIGHT = 6
)
(
    input i_clk,
    input i_rst,
    input i_start,
    input [DATA_WIDTH-1:0] i_data,
    output [DATA_WIDTH-1:0] o_data,
    output reg o_valid                      // current o_data validation flag
);

/*========================================\\
                P A R A M
\\========================================*/
localparam  IDLE = 0,
            BUF_FILL = 1,
            WORK = 2;

localparam  LINE_BUF_SIZE   = IMG_WIDTH * 2;
localparam  MAX_WORK        = IMG_WIDTH / 2;
localparam  HALF_BUF_SIZE   = LINE_BUF_SIZE / 2;

/*========================================\\
            R E G & W I R E S
\\========================================*/
reg [DATA_WIDTH-1:0] c_line_buf[0:LINE_BUF_SIZE-1];
reg [DATA_WIDTH-1:0] n_line_buf[0:LINE_BUF_SIZE-1];       // 2 lines of image width size buffer

reg [1:0] c_state, n_state;
reg [$clog2(IMG_WIDTH):0] c_x, n_x;         // line buf x-counter
reg [$clog2(IMG_WIDTH):0] c_y, n_y;         // line buf y-counter
reg [$clog2(MAX_WORK):0] c_work_cnt, n_work_cnt;


reg n_o_valid;
reg [DATA_WIDTH-1:0] n_o_data;
integer i;

wire signed [DATA_WIDTH-1:0] comp1, comp2, result;

/*========================================\\
                F F
\\========================================*/
always@(posedge i_clk or negedge i_rst) begin
    if(!i_rst) begin
        c_state <= IDLE;
        for(i = 0; i < LINE_BUF_SIZE; i=i+1)
            c_line_buf[i] <= 0;
        c_x <= 0;
        c_y <= 0;
        c_work_cnt <= 0;
    end else begin
        c_state <= n_state;
        for(i = 0; i < LINE_BUF_SIZE; i=i+1)
            c_line_buf[i] <= n_line_buf[i];
        c_x <= n_x;
        c_y <= n_y;
        c_work_cnt <= n_work_cnt;  
    end
end

/*========================================\\
                C O M B
\\========================================*/


always@* begin
    n_state = c_state;
    for(i = 0; i < LINE_BUF_SIZE; i=i+1)
        n_line_buf[i] = c_line_buf[i];

    n_x = c_x;
    n_y = c_y;
    n_work_cnt = c_work_cnt;    
    o_valid = 0;    
   

    case(c_state)
        IDLE: begin
            n_x = 0;
            n_y = 0;
            for(i = 0; i < LINE_BUF_SIZE; i=i+1)
                n_line_buf[i] = 0;

            if(i_start) begin
                n_state = BUF_FILL;
                n_line_buf[0] = i_data;        // start flag with 1 input data
                n_x = c_x + 1;                 // start flag with 1 input data
            end
        end

        BUF_FILL: begin
            // buffer movement
            n_line_buf[0] = i_data;
            for(i = 1; i < LINE_BUF_SIZE; i=i+1)
                n_line_buf[i] = c_line_buf[i-1];       // buffer left shift 

            // x, y movement
            if (c_x == IMG_WIDTH - 1) begin
                n_x = 0;
                n_y = c_y + 1;
            end else begin
                n_x = c_x + 1;
                n_y = c_y;
            end

            if(c_y == IMG_HEIGHT + 1)
                n_state = IDLE;
            if(c_x == IMG_WIDTH-1 && c_y[0]) begin      // if buffer filled
                n_state = WORK;
                n_work_cnt = 0;
            end

        end

        WORK: begin
            o_valid = 1;
            
            // buffer movement
            n_line_buf[0] = i_data;
            for(i = 1; i < LINE_BUF_SIZE; i=i+1)
                n_line_buf[i] = c_line_buf[i-1];       // buffer left shift 

            // x, y movement
            if (c_x == IMG_WIDTH - 1) begin
                n_x = 0;
                n_y = c_y + 1;
            end else begin
                n_x = c_x + 1;
                n_y = c_y;
            end

            // work counter, MAX == IMG_WIDTH / 2
            n_work_cnt = c_work_cnt + 1;

            if (c_work_cnt == MAX_WORK - 1) begin
                if (c_y == IMG_HEIGHT - 1) begin
                    n_state = IDLE;
                end else begin
                    n_state = BUF_FILL;
                end
            end
        end
    endcase
end

// Result calculation
// cannot be replaced to parameter assign 
wire [$clog2(LINE_BUF_SIZE):0]  idx1_comp1,
                                idx2_comp1,
                                idx1_comp2,
                                idx2_comp2;
assign idx1_comp1 = LINE_BUF_SIZE - c_work_cnt-1;
assign idx2_comp1 = LINE_BUF_SIZE - c_work_cnt-2;
assign idx1_comp2 = HALF_BUF_SIZE - c_work_cnt-1;
assign idx2_comp2 = HALF_BUF_SIZE - c_work_cnt-2;


assign comp1 = (c_line_buf[idx1_comp1] > c_line_buf[idx2_comp1]) ? c_line_buf[idx1_comp1] : c_line_buf[idx2_comp1];
assign comp2 = (c_line_buf[idx1_comp2] > c_line_buf[idx2_comp2]) ? c_line_buf[idx1_comp2] : c_line_buf[idx2_comp2];
assign result = (comp1 > comp2) ? comp1 : comp2;
assign o_data = result;

endmodule

 

Testbench 는 다음과 같다

`timescale 1ns/1ps

module tb_maxpool2d;

    // Parameters
    parameter DATA_WIDTH = 16;
    parameter IMG_WIDTH = 16;
    parameter IMG_HEIGHT = 16;
    parameter CLK_PERIOD = 10;

    // Inputs
    reg i_clk;
    reg i_rst;
    reg i_start;
    reg [DATA_WIDTH-1:0] i_data;

    // Outputs
    wire [DATA_WIDTH-1:0] o_data;
    wire o_valid;

    // Instantiate the Unit Under Test (UUT)
    maxpool2d_gpt #
    (
        .DATA_WIDTH(DATA_WIDTH),
        .IMG_WIDTH(IMG_WIDTH),
        .IMG_HEIGHT(IMG_HEIGHT)
    )
    uut
    (
        .i_clk(i_clk),
        .i_rst(i_rst),
        .i_start(i_start),
        .i_data(i_data),
        .o_data(o_data),
        .o_valid(o_valid)
    );

    // Clock generation
    initial begin
        i_clk = 0;
        forever #(CLK_PERIOD/2) i_clk = ~i_clk;
    end

    // Test vectors
    reg [DATA_WIDTH-1:0] image_data [0:IMG_WIDTH*IMG_HEIGHT-1];

    integer i;

    initial begin
        // Initialize Inputs
        i_rst = 0;
        i_start = 0;
        i_data = 0;

        // Reset sequence
        #(CLK_PERIOD);
        i_rst = 1;
        #(CLK_PERIOD);

        // Load image data
        // Here, we generate a test pattern; you can replace this with actual image data if needed
        for (i = 0; i < IMG_WIDTH*IMG_HEIGHT; i = i + 1) begin
            image_data[i] = (i*3) % 256; // Example data: pixel values from 0, 3, 6, ...
        end

        // Start the maxpool2d operation
        i_start = 1;
            

        // Feed input data
        for (i = 0; i < IMG_WIDTH*IMG_HEIGHT; i = i + 1) begin
            i_data = image_data[i];
            #(CLK_PERIOD);
            i_start = 0;
        end

        // Wait for processing to complete
        #(CLK_PERIOD * (IMG_WIDTH * IMG_HEIGHT));

        // Finish simulation
        $stop;
    end

    // Monitor outputs
    initial begin
        $display("Time\tclk\trst\tstart\tdata_in\tdata_out\tvalid");
        $monitor("%0t\t%b\t%b\t%b\t%h\t%h\t%b", $time, i_clk, i_rst, i_start, i_data, o_data, o_valid);
    end

endmodule

 

Modelsim 돌려본 결과는 다음과 같다

 

이건 16x16 예제인데, valid 가 총 8번 뜨는 것을 확인 가능하다.(line 마다 한번 뜬다고 생각하면 되겠다.)

들어가는 input 값은 0, 3, 6, ... 3n 인데, mod 256 를 적용했다.

그래서 처음 버퍼가 다 찬 경우에

line 1 : 0, 3, 6, ...

line 2 : 48, 51, 54, ...

처음 시작이 51, 그다음은 57, ... 이렇게 나와야 할 것이다.

 

시뮬레이션의 결과도 보게 되면 51부터 시작해서 93까지 총 8개 (하나의 line) 나오는 것을 확인 가능하다.

 

 

반응형
반응형

라인 버퍼를 사용해서 Conv2d 모듈을 생성해 보도록 한다.(unsigned )

 

먼저 Overview 를 작성한다.

 

Convolution 자체는 어떤 값을 커널값과 곱해서 나온 결과를 다시 사용하는 개념이 된다.

 

결국 곱셈 연산을 하게 되는 것이다.

따라서 연산을 할 위치만 조정을 해 주면 된다.

 

나는 다음과 같이 작성해 봤는데, 패딩이 1이고 커널 사이즈가 3인데 이걸 타겟으로 해서 변형하면 아마 작동을 안 할수도 있다.

초기 버전 파일을 가져와서 커널 쪽 오류가 있을 것 같다. 

module conv2d#
	(
		parameter DATA_WIDTH = 8,  // 데이터의 비트 폭
    	parameter IMG_WIDTH = 5,   // 입력 이미지 가로 크기
    	parameter IMG_HEIGHT = 5,  // 입력 이미지 세로 크기
    	parameter PADDING = 1,      // 패딩 크기
    	parameter KERNEL_SIZE = 3   // 커널 크기
	)
	(
		input i_Clk,
		input i_Rst,
		input i_start,
		input [DATA_WIDTH-1:0] i_pixel,
		input [DATA_WIDTH * (KERNEL_SIZE **2)-1 : 0] i_kernel,
		output [DATA_WIDTH-1:0] o_pixel,
		output reg o_done,
		output o_valid
	);


wire [DATA_WIDTH * 2 - 1:0] sum;
localparam 	LINE_BUFF_SIZE = (IMG_WIDTH + 2 * PADDING) * KERNEL_SIZE + 1;
parameter 	IDLE = 0,
			WORK = 1;

// FF registers
reg c_State, n_State;
reg [DATA_WIDTH-1:0] c_line_buff[0:LINE_BUFF_SIZE-1];
reg [DATA_WIDTH-1:0] n_line_buff[0:LINE_BUFF_SIZE-1];
reg [$clog2(IMG_WIDTH):0] c_x, n_x;
reg [$clog2(IMG_HEIGHT):0] c_y, n_y;

reg [DATA_WIDTH-1:0] window[KERNEL_SIZE-1:0][KERNEL_SIZE-1:0];


integer i, j;
integer k, m;
always@(posedge i_Clk, negedge i_Rst)
	if(!i_Rst) begin
		c_State <= 0;
		c_x <= 0;
		c_y <= 1;
		for(i = 0; i<LINE_BUFF_SIZE; i=i+1) 	c_line_buff[i] <= 0;
		for(k=0; k < KERNEL_SIZE; k=k+1) for(m=0; m<KERNEL_SIZE; m=m+1) window[k][m] <= 0;

	end else begin
		c_State <= n_State;
		c_x <= n_x;
		c_y <= n_y;
		for(i = 0; i<LINE_BUFF_SIZE; i=i+1) 	c_line_buff[i] <= n_line_buff[i];
		for(k=0; k < KERNEL_SIZE; k=k+1) for(m=0; m<KERNEL_SIZE; m=m+1) window[k][m] <= i_kernel[DATA_WIDTH * (k*KERNEL_SIZE + m) +: DATA_WIDTH];
	end



always@* begin
	// match n registers to c registers
	n_State <= c_State;
	n_x <= c_x; 
	n_y <= c_y;
	for(i = 0; i<LINE_BUFF_SIZE; i=i+1) 	n_line_buff[i] <= c_line_buff[i];
	
	o_done <= 0;
	// o_valid <= 0;

	case(c_State)
		IDLE: begin
			n_x <= 0;
			n_y <= 1;
			for(i = 0; i<LINE_BUFF_SIZE; i=i+1) 	n_line_buff[i] <= 0;
			if(i_start) begin
                // for data sync, if input data and start sign comes together
                n_line_buff[0] <= i_pixel;
                n_x <= c_x + 1;
				n_State <= WORK;
            end
		end

		WORK: begin
			if(c_x == IMG_WIDTH)
			begin
				n_line_buff[2] <= 0;
				n_line_buff[1] <= 0;
				if(c_y >= IMG_HEIGHT) 	n_line_buff[0] <= 0;
				else 					n_line_buff[0] <= i_pixel;
				for(i = 3; i < LINE_BUFF_SIZE; i = i + 1) 	n_line_buff[i] <= c_line_buff[i-3];
			end
			else begin
				for(i = 1; i < LINE_BUFF_SIZE; i = i+1)	n_line_buff[i] <= c_line_buff[i-1];
				if(c_y > IMG_HEIGHT) 	n_line_buff[0] <= 0;
				else 					n_line_buff[0] <= i_pixel;
			end


			if(c_x < IMG_WIDTH) 	n_x <= c_x + 1;
			else begin
				n_x <= 1;
				if(c_y < IMG_HEIGHT + 2) 	n_y <= c_y + 1;
				else 						n_y <= 1;
			end

			if(c_x == IMG_WIDTH && c_y == IMG_HEIGHT + 2) begin
				o_done <= 1;
				n_State <= IDLE;
			end
		end
	endcase
end

genvar kernel_row, kernel_col;
wire [DATA_WIDTH-1:0] mult_result[KERNEL_SIZE-1:0][KERNEL_SIZE-1:0]; // 곱셈 결과 저장, 3x3
wire [DATA_WIDTH-1:0] partial_sum[KERNEL_SIZE*KERNEL_SIZE-1:0]; // 중간 합산 저장

generate
    for (kernel_row = 0; kernel_row < KERNEL_SIZE; kernel_row = kernel_row + 1) begin : row_gen
        for (kernel_col = 0; kernel_col < KERNEL_SIZE; kernel_col = kernel_col + 1) begin : col_gen
            // 각 커널 위치의 곱셈 결과
            assign mult_result[kernel_row][kernel_col] = 
                (c_y >= 2 && c_x == IMG_WIDTH) ?
                (window[kernel_row][kernel_col] * c_line_buff[3*(IMG_WIDTH + 2*PADDING)-2 - kernel_col - ((IMG_WIDTH + 2*PADDING)*kernel_row)]) :
                (window[kernel_row][kernel_col] * c_line_buff[3*(IMG_WIDTH + 2*PADDING) - kernel_col - ((IMG_WIDTH + 2*PADDING)*kernel_row)]);
            
            // 곱셈 결과를 1차원 배열로 정리
            assign partial_sum[kernel_row * KERNEL_SIZE + kernel_col] = mult_result[kernel_row][kernel_col];
        end
    end
endgenerate

// partial_sum 배열의 값을 모두 합산하여 sum에 할당
assign sum = ((c_y >= 2 && c_x == IMG_WIDTH) || c_y >= 3) ? 
             partial_sum[0] + partial_sum[1] + partial_sum[2] + 
             partial_sum[3] + partial_sum[4] + partial_sum[5] + 
             partial_sum[6] + partial_sum[7] + partial_sum[8] : 0;

assign o_pixel = sum;
assign o_valid = ((c_y >= 2 && c_x == IMG_WIDTH) || c_y >= 3) && !o_done;


endmodule

 

 

이건 Testbench 이다.

`timescale 1ns / 1ps

module tb_conv2d;

    // Parameters
    parameter DATA_WIDTH = 8;
    parameter IMG_WIDTH = 5;
    parameter IMG_HEIGHT = 5;
    parameter PADDING = 1;
    parameter KERNEL_SIZE = 3;

    // Inputs
    reg i_Clk;
    reg i_Rst;
    reg i_start;
    reg [DATA_WIDTH-1:0] i_pixel;
    reg [DATA_WIDTH * (KERNEL_SIZE ** 2) - 1:0] i_kernel;
    reg [DATA_WIDTH-1:0] image[0:IMG_HEIGHT-1][0:IMG_WIDTH-1];

    // Outputs
    wire [DATA_WIDTH-1:0] o_pixel;
    wire o_done;
    wire o_valid;

    // Instantiate the conv2d module
    conv2d #(
        .DATA_WIDTH(DATA_WIDTH),
        .IMG_WIDTH(IMG_WIDTH),
        .IMG_HEIGHT(IMG_HEIGHT),
        .PADDING(PADDING),
        .KERNEL_SIZE(KERNEL_SIZE)
    ) uut (
        .i_Clk(i_Clk),
        .i_Rst(i_Rst),
        .i_start(i_start),
        .i_pixel(i_pixel),
        .i_kernel(i_kernel),
        .o_pixel(o_pixel),
        .o_done(o_done),
        .o_valid(o_valid)
    );

    // Clock generation
    initial begin
        i_Clk = 0;
        forever #5 i_Clk = ~i_Clk;  // 100MHz 클럭

        
    end


    integer x, y;
    // Test sequence
    initial begin
        // Initialize inputs
        i_Rst = 0;
        i_start = 0;
        i_pixel = 0;
        i_kernel = {8'd1, 8'd2, 8'd1,   // 커널 정의 (3x3)
                     8'd2, 8'd1, 8'd2,
                     8'd1, 8'd2, 8'd1};

        // Reset the module
        #5;
        i_Rst = 1;
        #5;

        // Start the convolution operation
        i_start = 1;

        image[0][0] = 8'd1; image[0][1] = 8'd2; image[0][2] = 8'd3; image[0][3] = 8'd4; image[0][4] = 8'd1;
        image[1][0] = 8'd2; image[1][1] = 8'd3; image[1][2] = 8'd4; image[1][3] = 8'd1; image[1][4] = 8'd2;
        image[2][0] = 8'd3; image[2][1] = 8'd4; image[2][2] = 8'd1; image[2][3] = 8'd2; image[2][4] = 8'd3;
        image[3][0] = 8'd4; image[3][1] = 8'd1; image[3][2] = 8'd2; image[3][3] = 8'd3; image[3][4] = 8'd4;
        image[4][0] = 8'd1; image[4][1] = 8'd2; image[4][2] = 8'd3; image[4][3] = 8'd4; image[4][4] = 8'd1;

        // Send pixel data
        for (y = 0; y < IMG_HEIGHT; y = y + 1) begin
            for (x = 0; x < IMG_WIDTH; x = x + 1) begin
                i_pixel = image[y][x]; 
                #10;  // 픽셀 전송 대기
                i_start = 0;
            end
        end
        

        // Wait for the process to complete
        wait(o_done);
        # 100;
        // stop simulation
        $stop;
    end

    // Monitor outputs
    initial begin
        $monitor("Time: %0t | o_pixel: %d | o_done: %b | o_valid: %b", 
                 $time, o_pixel, o_done, o_valid);
    end

endmodule

 

근데 convolution 연산 자체는 입력 이미지 사이즈와 채널이 크기 때문에, 라인 버퍼를 사용하는 방식은 권장되지 않는 것 같다.

대충 버퍼 사용되는 양만 해도 IMG_WIDTH * 3 * output channel 개 사용해야 되니 정말 많다(한 conv2d 모듈 내에서)

이를 이제 다충 채널에 대해서 쓰려면 generate 문을 사용해서 여러개 생성하고, kernel 입력 값을 넣어주게 되면 output 생성이 가능하겠다.

 

간단한 line buffer 를 사용한 conv2d 구현을 알아보았다.

반응형

'Verilog' 카테고리의 다른 글

[Verilog] CNN top module (w. dense)  (0) 2024.11.05
[Verilog] maxpool2d w. line buffer  (0) 2024.10.31
[Verilog] OV7670 카메라 모듈과 FPGA 통신  (0) 2024.09.09
[Verilog] OV7670 - more2  (0) 2024.05.28
[Verilog] SCCB & OV7670 - more1  (0) 2024.05.28
반응형

이전 포스팅에서 SCCB 통신, 카메라 모듈의 동작, Verilog 예제 코드로 기술된 카메라 통신 등을 작성 했었다.

 

이번에는 실제로 FPGA 보드를 사용해 디스플레이에 카메라 화면을 띄우는 것 까지를 목표로 해 본다.

 

사용 환경

보드 : DE2-115

디스플레이 : 삼성 27인치 모니터

툴 : Quartus II 13.1

기타 필요 도구 : VGA 케이블

 


 

먼저, OV7670 은 내부 register 를 가지고 있고, 이를 조절함에 따라 해상도 등과 같은 값들에 영향을 미친다.(VGA, QVGA, QQVGA, CIF, ...)

 

그리고 이 내부 register 들을 조절하기 위한 프로토콜이 SCCB 인데, 이는 I2C 와 비슷한 프로토콜이다. 

그리고, 다른 아두이노에서 OV7670 카메라를 연결할 때 보면 알겠지만, 오픈 컬렉터 방식(default high)이라 코드 상에서는 

High 신호를 보내려면 내부 코드를 0으로 만들어야 하고, LOW 신호를 보내기 위해서는 High-impedance 신호를 보내줘야 한다.

 

wire o_SCL;
assign o_SCL = SCL_en ? 1'b0 : 1'bZ;

 

 


OV7670 카메라, 보드, 디스플레이의 구조는 다음과 같다.

 

1) OV7670 과 보드가 initial register setting 을 SCCB bus 를 통해 통신한다.

2) 카메라에서 설정한 해상도에 맞게 데이터가 나온다.

2-1) 해당 데이터를 보드의 메모리 상에 저장한다.

3) 메모리에 있는 데이터를 화면으로 보여준다.

 

하지만 DE2-115 내부 block ram은 12-bit color 640*480 이미지를 담지 못한다.(3,686,400 bits, place & route 단계에서 불가능하다고 나옴)

따라서 320*240 (qvga) 해상도를 사용하던지, 640*480 에서 일정 부분에서 화면을 잘라서 사용하는 방법을 우회적으로 사용해야 되겠다. 화면을 자르는 경우에는 타이밍 관련해서 코드를 좀 더 자세히 수정할 필요는 있다.

 

아두이노 예제 코드를 보면, QVGA initial register setting 코드들이 나와 있다.

Visual Capturing with OV7670 on Arduino - Hackster.io

 

Visual Capturing with OV7670 on Arduino

This is an Arduino camera module, using the surveillance camera's digital image processing chip-OV0706. By Naman Chauhan.

www.hackster.io

해당 사이트에 코드와 schematic 이 나와 있다.

 

이미지 캡처를 QVGA 사양에서 진행하니, 내부 레지스터 값은 저것과 동일하게 설정한다.

 

하지만 저대로 하면 아마 흑백 사진이 나올 것으로 알고 있다.

OV7670 color settings - Page 1 (eevblog.com)

 

OV7670 color settings - Page 1

Outputting from OV7670 in VGA@30FPS in YUV 4:2:2 (through a frame buffer to a VGA monitor 640x480@60FPS) The only register I changed is 0x11 setting it to 0x00 to enable the PLL with the multiplier of 1 which gives a MUCH better image in terms of noise. Th

www.eevblog.com

그래서 이 VGA color register 를 QVGA 로 재설정 해준다.

참고) Slave address : OV7670 문서에 Write register address = 0x42, Read register address = 0x43 이라 나와 있음

 

 

---

내부 레지스터 설정

---

 

 

 

 

그리고 4-bit 컬러 대역폭에 대해 한 번 짚고 넘어간다. 

List of monochrome and RGB color formats - Wikipedia

 

List of monochrome and RGB color formats - Wikipedia

From Wikipedia, the free encyclopedia This list of monochrome and RGB palettes includes generic repertoires of colors (color palettes) to produce black-and-white and RGB color pictures by a computer's display hardware. RGB is the most common method to prod

en.wikipedia.org

 

12-bit RGB 를 보면 RGB444에 대해 볼 수 있다.

물론, 레지스터 세팅을 변경해서 RGB565 로도 변경할 수 있다.

하지만 RGB444로도 충분한 색 표현이 가능하다. 2^12 = 4096

 

그럼 위에서 세팅한 레지스터 설정값을 camera rom 데이터로 사용한다.


전 포스팅에서 Verilog 코드로 SCCB 및 카메라 세팅까지 적혀 있을 것이다.

[Verilog] OV7670 - more2 :: 블러그 (tistory.com)

 

[Verilog] OV7670 - more2

기존 파일에서 이어서 간다.GitHub - westonb/OV7670-Verilog: Verilog modules required to get the OV7670 camera working GitHub - westonb/OV7670-Verilog: Verilog modules required to get the OV7670 camera workingVerilog modules required to get the O

hipenhw.tistory.com

 

이렇게 카메라 설정을 다 했다면, 다음은 카메라에서 나오는 출력 값을 받아서 메모리로 넣는 작업을 해 줄 차례이다.

 

대략적으로 보면,

 

1) OV7670 카메라의 PCLK 을 기반으로 함.

 

2) 타이밍도에서 VSYNC 가 High 일 때 새 프레임으로 판단

 

3) VSYNC 가 LOW, HREF 가 HIGH 일 때 한 ROW의 시작이라고 판단

 

4) HREF 가 HIGH 인 동안 posedge clk move 에 따라 한 row의 color data[7:0] 값을 메모리에 넣는다.

4-1) 레지스터 세팅으로 값을 바꿔서 RGB 값을 출력하는 경우 2 클럭에 한 pixel data 를 받을 수 있으므로(RGB565, RGB444, ... ) 한 row가 2배의 clock cycle 를 가진다. ex) 640*480 --> grayscale 의 경우 한 row가 640, RGB의 경우 한 row가 1280.

 

5) 다시 VSYNC 가 HIGH 가 될 때 까지 반복한다.

 

 

 

+) 만약 640*480 이미지를 잘라서 넣고 싶은 경우, 카운터를 별도로 둬야 한다.

+) 메모리에 저장을 위해, MEM_i_Addr 과 같은 address input 값을 추가로 필요로 하는데, 이는 posedge PCLK 에 의존해서 1씩 상승하는 카운터인데, RGB의 경우에는 >> 2 를 취해준다.

마우스로 그린 그림

 

이렇게 1차원 배열 메모리에 데이터를 넣었다면, 다음은 이를 다시 가져와서 출력해야 한다.

 


출력은 VGA@60Hz가 기준이다.

+) 카메라 데이터를 바로 디스플레이로 출력 할 수는 없다. 디스플레이가 요구하는 VSYNC, HSYNC 는 카메라에서 나오는 값들로는 애초에 만족이 불가능하고, 타이밍조차 맞지 않는다. 그래서 디스플레이 컨트롤러에 규격화된 신호를 넣어서 출력을 하게 된다.

 

VGA@60Hz (640*480, 60Hz) 는 디스플레이 컨트롤러에 25Mhz 의 clock input 을 요구하고,

Front porch, sync pulse, back porch, visible area 로 구성되어 있다.(x축, y축 모두) (다음 글을 참고하자)

Video Timings: VGA, SVGA, 720p, 1080p - Project F

 

Video Timings: VGA, SVGA, 720p, 1080p

To work with standard monitors and TVs, you need to use the correct video timings. This how to includes the timings for five standard display modes using analogue VGA, DVI, HDMI, or DisplayPort: 640x480 (VGA), 800x600 (SVGA), 1280x720, and 1920x1080 (30 Hz

projectf.io

 

그래서 실제로 640*480 화면을 띄우기 위해서는 저 값들을 모두 더해 800*525 만큼의 카운터를 사용하게 된다.

Making My Own VGA Driver In SystemVerilog — AsyncBit

 

Making My Own VGA Driver In SystemVerilog — AsyncBit

This is a continuation of my posts about my final project for EE271, continued from the Project selection process here , here , and here . The first thing I wanted to get working in this design was the VGA output, as the whole design rests on being able to

www.asyncbit.com

 

 

간단하게 말로 구현을 해 보자면,

출력에 사용하는 25Mhz 로 input clock 을 맞춰 준다.

vsync, hsync 는 모두 보드의 기준에서는 output 인 값이기 때문에, 규칙에 맞게 만들어 주면 된다.

먼저 한 row를 담당하는 hsync의 경우, 

크기가 800인 hcounter 를 생성한다.

hsync 가 0인 경우는 sync pulse 인 경우밖에 없으므로, 나머지 경우는 모두 1이 된다.

hsync = 1, 0~639 픽셀 visible area
hsync = 1, 640 ~ 655 픽셀 front proch
hsync = 0, 656 ~ 751 픽셀 sync pulse
hsync = 1, 752 ~ 799 픽셀 back porch

 

vsync 의 경우, hsync 가 max 값에 다다랐을 때 한 줄 올라가면 된다.

따라서 vsync 는 내부 모듈의 클럭으로 hcounter 에서 나오는 hsync_line_max 값을 사용한다.

table 형태로 보면 조금 더 쉽게 이해 가능하다.

그리고 vsync 내부에도 vcounter 를 둬서 hsync 와 동일하게 설정해준다.(sync pulse에는 vsync 0인 것도 동일)

이제 우리는 각 픽셀을 가리키는 카운터를 얻게 되었다.([9:0] hcounter, [9:0] vcounter)

이걸로 메모리 참조 값을 결정한다.(read address)

 

위 table 에서 연장해서 설명하자면,

hcounter + (length of row pixels)*vcounter 를 하면 1차원 배열에서 불러올 수 있다는 것 정도는 쉽게 파악이 가능하다.

메모리 address 값이 정해졌으니, 클럭에 따라 12bits 픽셀 데이터를 가져와서 R[7:0], G[7:0], B[7:0], hsync, vsync 값을 output 으로 빼 주면 컬러 출력이 가능하다.

 

 


3.5_640x200_cam_final.zip
0.01MB

 

Verilog 파일, DE2-115용 qsf 를 첨부했다.

OV7670-Verilog/src at master · westonb/OV7670-Verilog (github.com)

 

OV7670-Verilog/src at master · westonb/OV7670-Verilog

Verilog modules required to get the OV7670 camera working - westonb/OV7670-Verilog

github.com

이 코드를 참고하여 일부 변형해 사용하였고, 640*480은 bram이 크기가 그만큼 되지 않기 때문에 640*200 크기로 출력 테스트만 진행해 보았다.

 

 

정확한 색상을 나타내지는 않지만, 그럼에도 출력 자체는 정상적으로 동작한다.

 

 


나는 레지스터 값을 어떻게 바꿔야 되는지 솔직히 잘 모르겠어서 일반 640*480 캡처 타이밍을 그냥 줄여서 메모리에 넣어 출력을 해 봤다. (320*240으로 만들었음)

3.6_320x240_cam_final.zip
0.01MB

그리고 이것도 잘 출력 되는 것을 볼 수 있다.

반응형

'Verilog' 카테고리의 다른 글

[Verilog] maxpool2d w. line buffer  (0) 2024.10.31
[Verilog] Conv2d w. line buffer  (0) 2024.10.30
[Verilog] OV7670 - more2  (0) 2024.05.28
[Verilog] SCCB & OV7670 - more1  (0) 2024.05.28
[Verilog] SCCB Interface & OV7670  (0) 2024.04.16

+ Recent posts