DSP

TMS320C6748 을 활용한 DSP_EDMA&McASP (LAB7)

알 수 없는 사용자 2019. 11. 27. 13:42
반응형

<이 내용은 강원대학교 전자공학과 실시간 신호처리 과목에서 진행된 내용을 기반으로 작성되었습니다.>

 

LAB7

 

 LAB7에서는 EDMA를 활용하여 McASP를 다룰 것이다. 이를 위해 EDMA_McASP라는 파일을 추가하였다. 기존의 구조를 떠올리면 데이터를 받는 리시브 이벤트와 데이터를 주는 트랜스 이벤트가 있었다. 이러한 이벤트를 어디서 찾아와 설정을 했는지 살짝 설명하며 LAB7을 시작하겠다.

 


TMS320C6748 Datasheet를 보면 여러 이벤트들의 넘버가 적혀있는 것을 볼 수 있다. 해당 데이터시트를 참고하면 McASP0 Receive가 이벤트 0번이고, Transmit1번임을 알 수 있다. 따라서 EDMA_McASP.h 파일에 다음과 같이 정의해놓은 것이다.

이처럼 임베디드에서는 define 하나하나까지도 정해져 있는 규칙을 따라 선언된다는 것을 유념하자.

 


해당 파일을 조금 설명하도록 하겠다. 우리는 EDMA를 사용하여 실시간으로 음악파일을 넘기는 일을 할 것이다. 그러기 위해서는 외부에서 들어오는 파일들을 받아와야 할 것이다.

ConfigEDMAForR 함수는 EDMA명령서를 가지고 있는 함수이다. 이제는 익숙해져서 해당 명령서가 어떤 명령을 지니고 있는지 쉽게 파악할 수 있을 것이다. 인자로 지니고 있는 REVT는 자기자신에게 링크를 거는 역할을 한다. 이를 통해 명령 수행이 다 끝나면 자기의 명령서를 다시 복사해올 수 있다. TCINT는 명령 수행이 끝나고 나서 팬딩 레지스터를 체크하는 용도이다.

 

CSL_FMKT(EDMA3CC_OPT_SYNCDIM, ASYNC)는 이벤트 한번에 엘리먼트 하나씩 움직여야 할 것이므로 A싱크를 사용한다는 것을 나타내고 있다. 음악파일은 지속적으로 들어올 것이고 들어오는 족족 옮겨줘야 하므로 이벤트 한번에 엘리먼트 하나씩 움직여야 한다는 사실은 쉽게 생각할 수 있다.

(Uint32)(&mcaspRegs->RBUF14) 음악파일은 RBUF14로 들어온다. 따라서 EDMA명령서의 소스는 RBUF14가 되어야 할 것이다.

 

또한 해당 명령서에는 소스 B인덱스가 존재하지 않는다. , 소스 B인덱스는 0이다. 계속 하나씩 들어오는 형식의 데이터이기 때문에 당연한 사실이다.

 

ConfigEDMAForX함수는 데스티니 즉, 목적지가 XBUF13으로 정해졌다는 사실을 유념하자.

 

main파일에서 BIOS_start()가 되면 CPU는 버프가 다 찰 때까지 인터럽트도 안 걸리고 할 일도 없을 것이다. IDLE 루틴만 실행될 것이다. 그러다가 핑퐁 메모리가 다 차면 인터럽트 팬딩 레지스터가 SET 되고, (인터럽트 인에이블 레지스터는 0번만 인에이블 시켰다. , 아웃버퍼는 다 되어도 인터럽트는 발생되지 않는다. 다만 팬딩 레지스터를 통해 확인 할 수 있다.) 인터럽트 서비스 루틴으로 들어갈 것이다. 그 뒤 실행되는 것은 EDMA3_CC_ISR()이다. 이 함수는 event id8을 가진 인터럽트 함수이다.

 


해당 함수는 하드웨어 인터럽트에 구현해 두었다. 이 코드에서 생각해야 할 것은 A싱크 모드에서는 B카운트 값이 하나씩 움직일 때마다 줄어든다는 사실이다. 따라서 우리는 한 프레임이 다 움직였는지 여부를 이것으로 판단할 수 있다. B카운트 값은 DMA트랜스퍼가 동작할 때마다 줄어들어 최종적으로 0값을 가지고 있을 것이다.

, 이제 CPU 입장에서는 인터럽트가 발생할 때마다 데스티와 소스를 새로 지정해주고 카운트를 재설정해주기만 하면 데이터의 이동은 EDMA가 모두 해결해주는 편한 상황이 되었다. 기존의 매번 CPU가 처리하던 형태를 탈피한 것이다.

 

LAB7_A

물론 이전에 비하면 CPU가 하는 일은 매우 줄어들었다. 하지만 매번 데스티와 소스를 새로 지정해 주어야 한다는 사실은 뭔가 반복된 작업이다. 이 작업을 줄일 수 있지 않을까?


방법은 간단하다! 각각 버퍼에 대한 총 4개의 명령어(명령서)를 미리 작성해 두는 것이다. , in_ping, in_pong, out_ping, out_pong 명령어를 각각 써두는 것이다. 그리고 인핑과 인퐁을 서로 링크를 걸어두면 될 것이다. 이를 위해서 EDMA_McASP.h 파일에 다음과 같이 define 해 두었다.

 

그리고 main에 있는 SetupEDMA3를 통해 명령서를 작성하였다.

주석을 보면 이해하기 쉬울 것이다. RAMSET의 안 쓰는 부분들 중 32~35번을 명령서의 저장공간으로 활용하였다. 또한 서로가 서로에게 링크되어있는 형식으로 작성하여 지속적으로 바뀔 수 있도록 코드를 작성하였다. 이를 통해 CPU는 기존과 같이 매번 데스티와 소스의 위치를 알려주지 않아도 될 것이다.

 

 

LAB7_B     


기존의 코드를 보면 버튼이 눌렸을 때 sine값을 곱하는 연산을 하도록 만들었었다. 이는 SWI파일에 정의되어 있다.

 

그런데 이런 방식의 연산은 다음과 같은 순서로 진행된다. 우선 모든 샘플에 대해 각각을 leftright로 분리한다. 그 후 분리된 값들에 대해 sine값을 곱하는 연산을 거친다. 최종적으로 연산이 끝난 각각의 값을 다시 합친다. 이러한 과정은 간단하지만 모든 샘플에 적용되어야 하기 때문에 연산량을 극심하게 증가시킨다. 따라서 leftright를 분리시켜 저장하는 방식으로 전환하여 연산량을 줄여보도록 하자.


우선 각각의 버퍼를 선언해줘야 한다.

 

그리고 난 뒤, main문 안에 있는 ConfigMcASP( MCASP_16BIT, MCASP_2SLOT, CFG, NO_RINT, NO_XINT ); 이 부분을 수정해 주어야 한다. 이전에 설명한 것을 기억하고 있다면 해당 코드를 통해 32비트의 데이터를 어떻게 받아올 것인지 설정할 수 있다는 사실을 알 것이다. 기존에는 32비트의 데이터를 통째로 받아왔었다. 왜냐하면 데이터를 받아올 때마다 인터럽트가 발생하는 구조의 코드였고, 16비트씩 나눠서 받아오면 인터럽트가 2배로 증가하기 때문에 이를 방지하기 위해 그런 구조로 만들었던 것이다. 하지만 이제는 인터럽트가 아닌 이벤트이며 CPU가 처리하는 것이 아닌 EDMA가 처리한다. 따라서 이를 16비트씩 2SLOT으로 받아온다면 각각 leftright에 대해 16비트 데이터가 들어올 때마다 이벤트를 발생시켜 줄 것이다.

 

이제 생각해야 할 것은 DMA설정이다.

 

소스는 RBUF이고, 데스티는 inpong[0]이며 한번에 움직이는 데이터의 크기가 2바이트이므로 A카운트는 2이다. 그리고 그 다음은 inpong[1]로 가면 될 것이다. 이것은 B인덱스를 가지고 설정하면 된다. 2 쌍을 한 프레임으로 설정 하면 left, right를 옮길 수 있다. C인덱스를 조정해서 다시 inpong[0]의 다음 위치에 넣어주면 될 것이다. C카운트는 총 개수로 만들면 될 것이다.

 


이걸 코드화 시키자.

 

ELEMENT_SIZE 2가 될 것이다. 참고로 BCNTRLD A싱크 사용시 B카운트가 0으로 줄어드는걸 매번 바꾸어 줄 수 있다. 이 값을 통해 한 프레임을 옮기고 난 뒤에 B카운트 값을 자동으로 설정해준다.

 

LAB7_C


LAB7_C는 코드를 조금 더 세련되게 만들어보려 한다. 기존 코드를 캡슐화 시키는 것이 목표이다. 사실 C++은 이러한 캡슐화를 직접적으로 지원해주지만 C에서는 이러한 구현을 개발자가 직접 해줘야 한다는 귀찮은 점이 있다. 캡슐화는 간단하다. 기존의 핑퐁버퍼를 하나의 struct 구조로 묶는 것이다.

 

Define.h 파일에서 이와 같이 RT_buf를 선언해준다. 단순히 기존에 있던 버퍼들을 이름을 바꿔 struct안에 넣을 것이므로 기존의 코드를 struct에 참조하는 형식으로 고쳐주면 될 것이다.

 

LAB7_D

 

이제 종착지에 도착하였다. 코드는 거의 완벽해졌다. 다만 한가지 눈에 거슬리는 것이 하나 있다. 바로 which_buffer이다. 어떤 버퍼를 사용 중인지 알기 위해 우리는 which_buffer라는 변수를 전역변수로 사용하여 체크하고 있었다. 이런 전역변수의 사용은 항상 줄이는 것이 좋기 때문에 이를 타개할 방법을 소개하고자 한다.

 

소프트웨어 인터럽트 입장에서는 어떤 버퍼를 쓰는지 알아야 하기 때문에 이 변수가 필요하다. 그런데 이걸 없애기 위해 우리는 큐를 사용할 것이다. 큐는 FIFO이다. 그림처럼 해보면? 핑과 퐁이 지속적으로 바뀌게 된다.


꺼내서 처리하고 다시 큐에 넣어주면 된다.

큐를 사용하기 위해서는 큐를 활성화시켜줘야 한다.

 

 

 

RT_Que라는 큐 핸들을 만들어주자. 이제 준비는 끝났다.


먼저  큐를 선언해 줘야 할 것이다. 큐에는 항상 Queue_Elem이라는 것이 있어야 한다. 이에 대해 자세한 것은 스스로 찾아보자. 중요한 것은 rt_buf를 포인터로 썼다는 것이다. RT_buf rt_buf; 그냥 이렇게 선언하면 될 것 같다고 생각할 수도 있으나 이 버퍼 하나는 무려 1600바이트이다. 하나를 꺼내면 1600바이트가 이동한다는 것은 매우 비효율적이다. 따라서 static으로 버퍼를 만들어 둔 뒤, 포인터를 만들어 큐에 넣어준 것이다.


main문에서 해당 큐를 선언하고, SWI에 구현해 둔 InitQue()가 실행된다.

 

이를 통해 큐가 초기화 되고, 바이오스가 start된 이후 SWI에 큐를 사용할 때 다음 사진과 같이 사용하게 된다.

 

코드가 깔끔해 졌고, 계산과정도 쉬워졌다. 이로써 DSP를 위한 기본적인 코드의 세팅이 완료되었다.

반응형