이번 시간에는 앞으로의 여정을 도와줄 간단한 입출력 함수와 드라이버를 직접 짜볼 예정이에요. 이번 포스팅을 통해서 실제Peripheral을 동작시키기 위해서 어떤 식으로 코드를 작성해야하는지 대략적인 감을 얻을 수 있을 것 같아요.
1. Prime cell UART(PL011)
우리가 타겟으로 하는 virtual 보드에는 serial 모듈이 붙어 있어요. 이 모듈은 PL011으로 AMBA와 호환되는 peripheral이에요. 해당 모듈의 아래와 같은 몇 가지 특징들을 가지고 있어요.
Transmit fifo 32x8
Receive fifo 32x12
Programmable baud rate generator
Support Direct Memory Access
블록 다이어그램은 아래 그림과 같아요.
왼쪽에 있는 APB interface를 통해 UART 모듈의 레지스터들을 Read/Write할 수 있어요. 그리고 Transmit/Receive FIFO가 있는 것을 확인할 수 있어요.
Transmit FIFO는 8bit 워드를 32개 저장할 수 있는데요. CPU가 UART를 통해 전송하려는 데이터들은 실제로 전송되기 전에는 Transmit FIFO에 저장되어요.
Receive FIFO는 12bit를 32개 저장할 수 있어요. 수신된 데이터 8bit와 각종 status 4bit을 포함하여 12bit인 거에요. 이런 Receiver 모듈에서 수신된 데이터는 CPU가 읽기 전까지 Receive FIFO에 저장되어요.
이제 데이터가 어떻게 전송되고 수신되는지 자세히 알아보도록 할게요.
전송의 경우 데이터들은 Transmit FIFO에 저장되는데요, 이때 UART가 동작 중이라면 해당 버퍼에 있는 값들을 전송하기 시작해서 Transmit FIFO가 비워질 때까지 전송해요.
수신의 경우, Receiver 모듈이 start bit를 발견할 때, 데이터를 샘플링하기 시작해요. 그리고 유효한 stop bit를 받게 된다면, 전송이 완료된 것으로 판단해요. 이때 한 워드가 완성되면 Receive FIFO에 push하는데 전송 시 발생한 error도 같이 묶어서 저장해요. UART의 한 character frame은 아래와 같아요.
일단 여기까지만 확인하고 DMA에는 다음 기회에 다루도록 할게요.
더 자세한 내용은 PL011의 Technical Reference Manual에서 확인할 수 있어요
이제 드라이버 개발자 입장에서 어떻게 하면 해당 모듈을 사용할 수 있을지 살펴볼게요. PL011을 사용하기 위해서는 해당 모듈의 레지스터들을 이용해야 해요. 우리에게 필요한 레지스터들을 나열하면 아래와 같아요.(DMA, ID 레지스터는 제외했어요)
당장은 뭐가 무엇을 뜻하는 것인지 파악하기 어려워보이는데요, 우선 테이블의 각 column들의 의미는 아래와 같아요.
Offset: 해당 레지스터가 base address로 부터 떨어진 위치
Name: 해당 레지스터의 이름
Type: 해당 레지스터에 접근 권한 (RO:Read only, WO: Write only, RW: Read Write)
Reset: 리셋되었을 때의 값
Width: 해당 레지스터의 비트 단위의 길이.
왜 레지스터들이 고정된 위치에 있지 않을까요? SOC 설계자가 해당 모듈을 어떤 어드레스에 배정하냐에 따라 다르기 때문이에요.
2. Register descriptions
이제 차근차근 레지스터들에 대해 알아보도록 할까요?
Data register (UARTDR)
UARTDR 레지스터는 UART를 통해 주고 받는 데이터를 접근할 수 있는 레지스터에요. 해당 레지스터는 width가 12/8 두 가지의 경우를 가지고 있는데요, 읽을 때는 Receive FIFO에 접근하기 때문에 12bit이고, 쓸 때는, Transmit FIFO에 접근하기 때문에 8bit인 것이에요. 그럼 해당 레지스터에 값을 Read/Write를 함으로써 UART를 통해 데이터를 수신/전송할 수 있겠지요?
Receive Status Register/Error Clear Register (UARTRSR/UARTECR)
Receive할 때 발생한 에러들은 Receive FIFO 뿐만 아니라 UARTRSR을 읽으면 확인할 수 있어요. 또한 해당 레지스터에 값을 써서 에러들을 clear할 수 있어요. 각각의 에러들의 설명은 아래의 표로 대체할게요.
Flag Register(UARTFR)
해당 레지스터에는 각각의 FIFO의 상태를 알 수 있는 비트들이 있어요. 즉, FIFO가 비어있는지 또는 FIFO가 다 차있는지를 나타내는 상태를 알 수 있어요. 다른 비트들을 그렇게 중요하지 않으니 설명을 하지는 않을게요.
IrDA Low-Power Counter Register (UARTILPR)
우리가 사용하는 모드에서는 이 레지스터는 사용하지 않아요.
Integer Baud Rate Register (UARTIBRD), Fractional Baud Rate Register (UARTFBRD)
uart는 baud rate를 정해서 데이터를 샘플링하는데요, 이때 baud rate는 해당 UART 모듈에 전달되는 UARTCLK를 이용해서 생성하게 되어요. 이때 UARTIBRD와 UARTFBRD 레지스터에 저장된 값을 사용하여 UARTCLK을 나누게 되어요.
만약, UARTCLK이 4MHz이고 baud rate을 230400으로 세팅하고 싶다면, 각각의 레지스터들의 값을 아래처럼 계산하면 돼요.
결론적으로 UARTIBRD는 1로, UARTFBRD는 5로 정하면 됩니다. (물론 정확한 baud rate를 만들어낸 것은 아니지만...)
Line Control Register, UARTLCR_H
해당 레지스터에서는 이름 그대로 Line control에 관련된 세팅을 저장하고 있어요. 어렵지 않은 내용이므로, 자세한 설명은 표로 대체할게요.
Control Register (UARTCR)
해당 레지스터에서는 실제로 UART 동작을 활성화 비활성화할 수 있는 비트가 있어요. 또한 Transmit/Receive의 기능을 각각 활성화 비활성화하는 비트들도 존재해요. 이 비트들을 제외하면 다른 비트들은 우리의 관심사는 아니에요. 단, UART를 활성화할 때는 다른 설정들을 먼저 다 해주고 활성화시켜야 해요.
Interrupt Mask Set/Clear Register (UARTIMSC)
이제 남은 레지스터들은 인터럽트에 관련된 것들인데요. 우리는 인터럽트를 사용하지 않을 것이기 때문에 모든 인터럽트들을 mask할 것이에요. 그러기 위해서는 UARTIMSC[10:0]의 값을 전부 1로 세팅하면 돼요.
3. Implementation
길고 긴 레지스터에 대한 설명이 끝났어요. 이제 위 내용을 바탕으로 코드를 구현하면 되겠지요? 아래에 제가 구현한 내용을 바로 보기보다는 한 번 직접 시도해보시는 추천해요.
제가 구현한 헤더 파일은 아래와 같아요. 그냥 메뉴얼에 기술된 내용을 단지 코드로 옮겼을 뿐이에요 :) 다만, Virtual 보드에서 동작하기 위해서 보드에 알맞게 UART의 주소와 UARTCLK를 하드 코딩을 했어요. 또한 제공되는 api는 심플하게 init 함수와 바이트 입출력 함수 getch와 putch를 정의해요.
C 코드도 메뉴얼 내용에 따라 작성되었고 어렵지 않아요.
init_uart에서는 baud rate를 계산해서 레지스터에 저장해주고, Line control 레지스터를 설정해주고, interrupt를 전부 마스크한 다음에, UART를 활성화 시키는 함수이에요. 그리고 putchar와 getchar는 FIFO에 있는 값을 읽고/쓰는 함수들이에요. 생각보다 너무 간단해서 놀라지 않으셨나요?
이제 main 함수에 입출력 함수들을 사용해서 Hello world를 출력하고, echo 서비스를 제공하도록 main.c 코드를 수정해볼게요.
물론, 새롭게 만든 uart.c도 컴파일되고 링크되게 build.sh을 수정해야겠지요? 이 부분은 따로 코드를 첨부하지 않을게요~
이렇게 완성된 버전은 WAIOS Repository의 uart 태그에서 찾아 볼 수 있어요.
4. Demo
이제 완성된 버전의 이미지를 qemu로 동작시켜볼게요. 이전과는 다르게 새로운 옵션을 추가할 거에요.
-serial mon:stdio
해당 옵션은 보드의 시리얼 포트와 터미널의 stdio와 연결해줘요. 해당 옵션을 주고 qemu를 동작시켜볼게요.
5. 정리
이번 시간에는 PL011 모듈의 간단한 드라이버를 작성해서, 입출력을 해보았어요. 드라이버를 작성해본 적이 없는 분이시라면 "드라이버는 이런 식으로 작성하는 구나" 라는 감을 얻으셨다면 좋겠네요 :) 이 저수준 입출력을 이용한다면 매번 디버거를 통해 뜯어보지 않아도 될 것 같아요. 제가 작성한 코드는 아래의 링크를 참고하세요!
Kommentare