FPGA실습(2)
2. UART 구현
이전 글에 이어서 이번에는 UART를 구현하여 DMA를 통해 데이터를 전송하고 수신하는 부분에 대해 다룬다.
2.1 요구사항
- AXIS Stream FIFO와 연동되는 UART 구성
- UART 회로에 공급되는 클럭에 따라 BAUD Rate 결정
2.2 UART 개요
위키백과에 나오는 UART의 소개자료는 다음과 같다.
UART(범용 비동기화 송수신기: Universal asynchronous receiver/transmitter)는 병렬 데이터의 형태를 직렬 방식으로 전환하여 데이터를 전송하는 컴퓨터 하드웨어의 일종이다. UART는 일반적으로 EIA RS-232, RS-422, RS-485와 같은 통신 표준과 함께 사용한다. UART의 U는 범용을 가리키는데 이는 자료 형태나 전송 속도를 직접 구성할 수 있고 실제 전기 신호 수준과 방식(이를테면 차분 신호)이 일반적으로 UART 바깥의 특정한 드라이버 회로를 통해 관리를 받는다는 뜻이다.
즉 동기 신호는 전달하지 않고 약정된 통신 속도로 8비트의 데이터를 직렬로 전송한다는 뜻이고 실제 PC 등과 통신을 수행하기 위해서는 선로 인터페이스 소자가 따로 있어야 한다. 여기서 설계한 것은 Zynq에서 선로 인터페이스 소자로 출력하는 송신신호, 선로 인터페이스 소자로부터 수신하는 수신 신호를 생성하거나 수신하여 데이터를 처리하는 과정을 설계하고 시험하는 것이다.
UART의 코딩 규격은 다음과 같다. STOP 앞에 패리티 비트를 추가하기도 하는데 여기서는 패리티 비트를 사용하지 않는 UART를 사용한다.
AXI4 Stream Bus의 프로토콜은 다음 그림과 같다.
즉 VALID 신호를 송신측에서 전달하면 수신 측에서 READY 신호를 출력하여 데이터를 수신하는 방식으로 데이터를 전달 받는다. 그리고 데이터의 마지막임을 알리는 TLAST 신호는 마지막 워드(32 bit의 데이터)일 때 HIGH가 되고 그 외에는 LOW를 유지한다. 또한 TKEEP[3:0] 이라는 신호가 있는데 이신호는 데이터의 유효 바이트를 표시한다. 즉 TKEEP 이 b’0001 일때는 4바이트의 데이터 중 최하위 바이트만 유효한 데이터라는 것이고, b’0011일때는 하위 2바이트, b’0111일때는 하위 3바이트가 유효하다는 의미이다. b’1111일때는 4바이트 모두 유효한 데이터란 의미이며 마지막 워드가 아닐 때는 항상 b’1111이고 마지막 워드일 때 의미가 있다.
FIFO로부터 데이터를 수신할 때는 위의 그림에 따라 VALID 신호가 HIGH일 때 데이터를 수신하면 되는데 FIFO로 데이터를 송신할 때는 TREADY가 항상 HIGH이기 때문에 TVALID 신호를 이용하여 일반 FIFO에 데이터를 Write 하듯이 출력하면 된다.
2.4 UART Logic 설계
먼저 위에 소개한 UART 프로토콜과 AIX4 프로토콜에 따라 데이터를 송수신하는 회로를 설계하여야 한다.
송신측에서는 FIFO의 VALID신호를 수신하면 READY신호를 한 클럭 주기로 출력하여 32 비트의 데이터를 읽어와서 직렬데이터로 변환하며 START 비트와 STOP비트를 추가하여 출력한다. 아래그림은 FIFO에서 데이터를 읽어와서 1 바이트를 출력하는 시뮬레이션 결과 이다. 그림에서 보면 FIFO에서 h’1f8187e7이라는 데이터를 수신하여 직렬로 b’0111001111을 출력하는 것을 확인할 수 있다. 즉 하위 바이트의 하위비트부터 전송되는 것을 확인할 수 있다. 여기서 s_keep이라는 신호를 보면 b’0111인데 이것은 FIFO로부터 수신한 데이터중 h’8187e7만 의미 있는 데이터란 뜻이다. 그래서 위에 언급한 1바이트 외에 2바이트를 더 전송한다
송신부는 설정된 Baud Rate의 클럭에 따라 직렬 데이터를 송신하면 되지만 수신부는 비동기 방식의 데이터이기 때문에 Baud Rate의 클럭에 따라 데이터를 수신하면 에러가 발생할 수 있다 따라서 수신부는 Baud Rate보다 빠른 클럭으로 데이터를 샘플링하여야 한다. 수신 데이터를 샘플링하다가 입력되는 데이터가 LOW로 변경되는 순간부터 LOW를 유지하는 시간이 Baud Rate 클럭 주기의 반 이상 되었을 때 START bit로 인식하고 이후 데이터를 Start bit로 인식한 주기에 따라서 데이터로 인식하여 입력되는 직렬 데이터를 32비트의 병렬 데이터로 변환하여 변환이 종료될 때마다 VALID신호를 출력하여 FIFO에 Write한다.
위 그림은 rxi 로 입력되는 직렬 데이터 h’564e5f3aac이라는 5바이트의 데이터를 수신하여 병렬로 변환하고 dwen 신호를 출력하여 32비트 FIFO에 Write하는 절차를 시뮬레이션 한 결과이다. 그림을 살펴보면 입력되는 직렬 데이터가 LOW로 변경되고 나서 baudclk의 절반정도 sampling 한 데이터가 LOW를 유지하고 있으면 그때부터 주기적으로 rclk을 발생시키고 이를 기반으로 입력데이터를 수신하며 병렬로 변환하고, dwen 이라는 신호를 발생시켜 FIFO에 write하는 과정을 보여 주고 있다.
2.5 AXI 인터페이스를 갖는 UART IP 만들기
이제 UART 기본 회로가 설계 되어 시뮬레이션을 통해 기능을 검증하였으면 이를 AXI 인터페이스를 갖는 IP로 제작하여야 한다. AXI 인터페이스를 갖는 IP를 제작하기 위해서는 VIVADO 설계도구의 상단 메뉴에서 Tool -> Create and Package New IP...를 실행한다.
“Create and Package New IP” 라는 pop-up 창이 나오면 “Next”를 눌러 다음으로 넘어 간다. 그러면 다음 그림으로 회면이 변환되는데 여기서 “Create a new AXI4 peripheral”을 선택하고 “Next”를 누른다.
여기서 우리는 8워드 정도의 레지스터를 사용할 것이기 때문에 위의 그림에서 Number of Registers를 8로 변경하고, AXI Stream Interface를 사용할 것이기 때문에 가운데 Box에서 “+”를 눌러 인터페이스 2개를 추가한다. 그러면 다음과 같이 AXI 인터페이스 2개가 추가된 것을 확인 할 수 있다.
먼저 S01_AXI를 선택하고 오른쪽의 Interface Type눌러 “Stream”으로 변경한다.
그리고 S02_AXI를 선택하여 역시 Interface Type눌러 “Stream”으로 변경하고 Interface Mode를 Master로 변경한다. 그러면 다음 그림과 같이 AXI Slave Port 한 개, AXIS Slave port 한 개, AXIS Master port 한 개를 갖는 IP 블록을 확인 할 수 있다. Slave는 데이터를 전송하기 위해 DMA 모듈로부터 데이터를 수신하는 인터페이스를 의미하고, Master는 UART로부터 수신한 데이터를 DMA 모듈로 전송하기 위한 인터페이스로 사용된다.
이제 “Next”를 눌러 다음으로 진행한다. 마지막 윈도우에서 “Edit IP”를 선택하고 “Finish”를 누르면 새로 생성된 IP에 대해 설정을 진행할 수 있는 새로운 설계 도구창이 활성화 된다.
설계 도구에서 Source를 확인하면 S00_AXI_inst, S00_AXIS_inst, M00_AXIS_inst 파일이 생성된 것을 볼수 있다. 여기서 S00_AXI_inst는 이전에 레지스터 8개로 선언했으므로 8개의 레지스터를 AXI Bus 인터페이스를 이용하여 CPU에서 레지스터에 데이터를 읽고 쓰기 위한 Code가 Template로 제공되었음을 알 수 있고, 나머지 2개는 AXIS 인터페이스를 통해 데이터 스트림을 송수신하기 위한 Code가 생성되어 있음을 확인할 수 있다. 여기에 Source에서 “+”를 눌러 이전에 작성된 UART Code를 포함시켜야 한다. (*여기서 이전에 생성된 File을 Add하는 방법으로 시도했으나 잘 되지 않는다. 원인은 잘 모름. 아무튼 그래서 새로 파일을 적당히 생성하고 이미 작성된 Code를 복사해서 넣는 방식으로 추가하였다.)
이제 최상위 code를 열어보면 S00_AXI, S00_AXIS, M00_AXIS의 3개의 콤포넌트가 포함되어 있고 인터페이스 IO가 정의되어 있는 것을 확인할 수 있는데, 여기서 우리는 S00_AXI만 사용하고 AXIS 두 개는 UART Code로 대체 할 것이기 때문에 S00_AXIS, M00_AXIS component 정의는 삭제한다. IO는 그대로 둔다. 이제 UART Code를 이 파일의 component로 정의 하고 IO를 Mapping 맵핑한다.
code에 다음 포트를 추가한다.
uartclk : in std_logic;
uarttx : out std_logic;
uartrx : in std_logic;
fsts : in std_logic_vector(1 downto 0);
uartclk은 UART Baud Rate를 설정하고 통신의 기준이 되는 clock 입력이고, uarttx, uartrx는 UART 데이터를 송수신하기 위한 포트이고, fsts는 외부 FIFO의 상태를 CPU에 알려주기 위해 입력을 받는 부분이다.
code에 다음 신호를 추가한다.
signal txbyte : std_logic_vector(15 downto 0);
signal txbytes : std_logic_vector(31 downto 0);
signal rxbyte : std_logic_vector(15 downto 0);
signal rxbytes : std_logic_vector(31 downto 0);
signal control : std_logic_vector(31 downto 0);
signal txlen : std_logic_vector(15 downto 0);
signal uartrxi : std_logic;
signal uarttxi : std_logic;
위의 4개는 UART를 통해 송수신된 데이터 바이트 수를 CPU에서 읽어보기 위한 신호이고, control을 LOOPBACK 제어를 설정하기 위한 신호, txlen은 송신부에서 송신할 바이트수를 CPU로부터 수신하기 위한 신호, uartrxi, uarttxi는 내부에서 신호를 연결하기 위한 신호이다.
code에 다음을 추가한다.
uarttx <= uarttxi;
uartrxi <= uarttxi when(control(0) = '1') else uartrx;
첫 번째 줄은 UART logic에서 출력되는 내부 신호를 외부로 연결하기 위한 것이고, 두 번째 줄은 control 신호의 0번째 비트 신호가 1이면 내부 LOOPBACK을 하고 ‘0’이면 외부에서 수신한 신호를 rx 신호를 사용하기 위한 것이다.
이제 S00_AXI_inst를 열어 IO에 다음의 user port를 추가한다.
txbyte : in std_logic_vector(15 downto 0);
txbytes : in std_logic_vector(31 downto 0);
rxbyte : in std_logic_vector(15 downto 0);
rxbytes : in std_logic_vector(31 downto 0);
control : out std_logic_vector(31 downto 0);
txlen : out std_logic_vector(15 downto 0);
fsts : in std_logic_vector(1 downto 0);
그리고 code 중간 237 line 정도에 다음을 추가한다.
slv_reg4 <= "00000000000000" & fsts & txbyte;
slv_reg5 <= txbytes;
slv_reg6 <= x"0000"&rxbyte;
slv_reg7 <= txbytes;
이것은 reg4를 통해 fifo 상태와 송신 바이트를 확인하기 위해 설정하는 것이고, reg5를 통해서는 UART를 통해 전송된 누적 바이트 수를 확인하기 위한 것이다. 또한 reg6과 reg7을 통해서 수신된 바이트수와 수신된 누적 바이트 수를 확인하기 위한 것이다.
그리고 그 아래쪽의 when b“100”이라고 되어 있는 부분부터 when other=> 윗 줄까지 comment 처리한다.
다음에는 맨 아래쪽의 — Add user logic here 부분에 다음을 추가한다.
control <= slv_reg0;
txlen <= slv_reg1(15 downto 0);
이것은 사용자가 Loopback 하도록 reg0에 써 넣은 데이터를 UART instance에 전달하기 위한 것이고, 사용자가 전송하고자 하는 바이트 수를 reg1에 써 놓은 것을 UART instance에 전달하기 위한 것이다.
이제 M00_AXIS 파일과 S00_AXIS 파일은 프로젝트에서 삭제해도 된다.
설계도구의 설계 윈도우에서 Package IP Tab을 눌러 IP를 update한다. 그러면 지정한 디렉토리에 IP가 생성되었음을 확인할 수 있다.
이 과정을 통해 생성된 IP의 register map은 다음과 같다.
reg | 31 24 | 23 16 | 15 8 | 7 0 |
0 | X | X | X | bit0:loopback |
1 | X | X | tx_length | |
2 | X | X | X | X |
3 | X | X | X | X |
4 | X | [17:16] fifo state | tx bytes | |
5 | total tx bytes | |||
6 | X | X | tx bytes | |
7 | total rx bytes |
X: 사용하지 않음.
2.6 IP 통합 BD 설계
이제 zynq와 DMA 인터페이스 DMA Stream FIFO등과 앞에서 생성한 IP를 통합해서 zynq core를 이용하여 자체 Loopback을 통한 UART 송수신 회로를 검증하기 위해 IP를 통합해본다.
zynq와 DMA인터페이스 DMA Stream FIFO를 추가하는 것은 앞장에서 설명한 것과 동일하다.
UART IP를 추가하기 위해서는 설계 도구 왼편의 메뉴에서 IP Catalog를 누르면 설계 창에 IP Catalog가 보이는데 이 윈도우에 커서를 놓고 오른쪽 버튼을 클릭하여 Add Repository를 수행하고 UART IP가 저장되어 있는 디렉토리를 추가하면 IP 목록에 저장한 UART IP가 보이고 이를 더블클릭하면 설계 화면에 IP가 추가되는 것을 볼 수 있다.
여기서 UART Buad Rate를 10M로하기 위해 10MHz의 클럭을 추가하여야 하는데 이것은 CPU에서 생성되는 클럭을 하나 추가하여 10MHz로 설정하여 주면 된다.
또한 DMA Stream FIFO의 읽기 클럭과 쓰기 클럭이 다르기 때문에 DMA Stream FIFO를 더블클릭하여 다음과 같이 설정을 변경한다.
그리고 FIFO의 상태 Flag를 출력하도록 Flag tab에서 Enable almost empty를 Yes로 변경한다.
최종 설계된 블록도는 다음과 같다. 그림에서 노란색으로 표시된 선은 10MHz 클럭이다. UART 블록에서 M00_AXIS_ACLK과 S00_AXIS_ACLK은 사용하지 않지만 이 신호를 연결하지 않으면 합성과정에서 오류가 발생한다. 이제 설계도구 왼편에 있는 Run Synthesis를 클릭하여 합성을 수행한다.
합성이 완료되면 Open Synthesis Design을 수행하고 Open Synthesis Design-> Schematic을 수행하여 아래 그림과 같이 Schematic을 설계 화면에 나오게 하면 아래에 입출력 핀을 설정할 수 있는 창이 보인다.
이 윈도우에서 Scalar ports라고 되어 있는 2개의 핀에 대한 규격을 LVCMOS18 혹은 LVCMOS33으로 지정하고 uartrx 핀을 Pull up으로 설정한 뒤 오른쪽 버튼을 클릭하여 Autoplace I/O Port를 수행한다. IO pin이 할당되면 Fixed에 체크를 하고 Cntr-S를 눌러 IO constraint File을 저장한다.
이제 bitstream을 생성하고 bitstream이 생성되면 SDK로 시험하기 위해 Export Hardware를 수행한다.
2.7 시험
SDK Launch하여 새로운 Application을 생성하고 1장에서 시험한 프로그램을 이용하여 시험을 수행한다.
UART 블록의 레지스터를 읽기 위한 다음의 function을 추가한다.
void show_reg()
{
int i;
for(i=0;i<8;i++){
xil_printf("reg[%d] = %x ", i, REGADDR[i]);
}
xil_printf("\r\n");
}
main 함수에 초기화 부분에 다음을 추가한다.
REGADDR = (u32 *)XPAR_AXI_UARTDMA_0_S00_AXI_BASEADDR;
if(REGADDR[0] == 0x80000001) xil_printf("reg0 = %02x\r\n", REGADDR[0]);
else REGADDR[0] = 0x80000001;
show_reg();
REGADDR는 UART 블록의 Base Address를 지정하기 위한 것으로 bsp의 include에서 xparameter,h 파일을 열어 확인하여야 한다.
reg0에 내부 loopback을 지정하기 위해 0x01을 써준다.
또한 DMA로 데이터를 전송하기 전에 전송되는 Byte수를 알려주기 위해 reg1에 전송 데이터 수를 써주고 DMA 데이터 전송을 수행한다.
REGADDR[1] = (u32)TX_PKT_LEN;
Status = XAxiDma_SimpleTransfer(&AxiDma,(UINTPTR) TxBufferPtr,
TX_PKT_LEN, XAXIDMA_DMA_TO_DEVICE);
Rx Interrupt Handler에서는 DMA로 수신한 뒤 수신 데이터 크기를 RX_PKT_LEN이라는 글로변수로 알려주기 위해 reg6의 값을 RX_PKT_LEN에 다음과 같이 넣어 준다.
Status = XAxiDma_SimpleTransfer(&AxiDma,(UINTPTR) RxBufferPtr,
MAX_PKT_LEN, XAXIDMA_DEVICE_TO_DMA);
//if (Status != XST_SUCCESS) {
// return XST_FAILURE;
//}
RxDone = 1;
RX_PKT_LEN = REGADDR[6];
1장에서와 같이 FPGA를 프로그램밍하고 시험 프로그램을 빌드하여 Run을 수행하면 다음과 같은 결과를 얻을 수 있다. 데이터를 20부터 시작하는 20바이트의 데이터를 전송하였고, 20바이트의 제이터를 잘 수신하여 오류가 없이 전송 되었음을 확인할 수 있다. 데이터의 크기와 내용을 변경하며 반복 시험을 수행하여 우리가 설계한 UART 블록이 잘 동작함을 확인하였다. 시험해 본 결과로는 1~4000바이트 까지 잘 동작하는 것을 확인할 수 있었다.
댓글
댓글 쓰기