DSP

TMS320C6748 을 활용한 DSP_ 오디오 (LAB5)

알 수 없는 사용자 2019. 10. 21. 01:41
반응형

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

 

 

LAB_5

 

LAB_5에서는 드디어 오디오를 다룰 것이다. DSP를 배운다는 것은 기본적으로 오디오를 다뤄야 하기 때문에 이제야 오디오를 다룬다고 표현할 수도 있을 것 같다.


우리가 쓰는 보드에 코덱이 달려있다. 바로 AC3106I이다. 이 칩의 다이어그램을 살펴보면 다음과 같다.

 이 칩은 24비트 96헤르츠까지 지원한다. 다이어그램을 자세히 보자. 잘 안보이겠지만 입력은 3개까지 넣을 수 있고 그 입력들을 믹싱 할 수 있다는 것을 알 수 있다. (동그라미+가 의미하는 것이 바로 믹싱이다.) 이런 구조에서 만약 입력된 신호 중 하나만 쓰고 싶으면 쓰고 싶은 신호를 제외하고 나머지를 0으로 만들면 될 것이다. ad컨버터는 2개 달려있고 PGA에서는 증폭을 한다. 그리고 각각의 입력에 대해 개별적인 증폭 또한 지원한다는 것을 알 수 있다.

 

다이어그램 오른쪽은 DA컨버터 부분이다. 2개가 들어있고, 헤드폰아웃과 라인아웃을 지원한다. 어떤 아웃풋을 사용할지는 레지스터 설정으로 컨트롤 가능하다.


여담이지만 만약 오디오 코덱 펌웨어를 작성해야 한다면 하루에 10시간씩 작업해서 잘 아는 사람도 3주는 잡아야 할 것이다.

C6748 LCDK SchematicStereo Audio Codec 부분이다.

 

이 스키매틱에서 눈 여겨 봐야 할 것은 디지털 포트 파트이다. 왼쪽 하단의 SDA SCL 두 포트를 가지고 통신하는 I2C가 중요하다. I2C는 비교하자면 LAN 포트와 비슷하고 UART와는 다르다고 할 수 있다. , 11 통신이 아니라 1대 다 통신이라는 것이다. 자기 자신의 주소를 지니고 있는 상태로 보드상에서 칩들간의 통신을 위해 만들어진 하나의 방식이다. 대용량의 데이터를 주고받는 용도라기 보다는 메시지나 작은 명령어를 전달하는 데 주로 쓰인다.

 

그 위에 MCLK, BCLK, WCLK은 각각 머신클럭, 빅클럭, 워드클럭이다. 클럭 밑에 있는 포트 DIN, DOUT은 시리얼통신을 할 때 사용된다. 머신클럭, 빅클럭, 워드클럭, DIN, DOUT 5개의 포트를 데이터 포트라고 한다.

 

SDA, SCL은 컨트롤 포트이다. 이 칩의 레지스터를 설정 할 때 사용하는 것이다. 예전에는 이 컨트롤과 데이터가 하나로 합쳐진 칩들도 있었는데 이러한 방식은 칩 단가는 내려가지만 프로토콜과 프로그램이 너무 복잡하여 져서 요즘에는 분리하여 나오고 있다.

 

 데이터 포트에 대해 조금 더 설명을 해보자. 데이터 통신 중에 UART라는 통신 방법이 있다. 이 방식은 약속 된 주기에 맞춰 데이터를 보내고 받는 형태이다. 라인 하나만 가지고도 데이터를 주고받을 수 있는 쉽고 강력한 방식이지만 한가지 단점이 있다. 그 단점이 바로 양쪽이 정확히 동기화 된 클럭을 가져야 한다는 것이다. 만약 클럭의 오차가 있다면 약속된 주기가 흐트러지면서 데이터 통신이 정상적으로 이루어지지 못할 것이다. 이는 빠른 통신에 있어서 더더욱 크게 작용한다. 위에서 간략하게 설명한 바와 같이 우리 보드는 기본적으로 DINDOUT을 통해 데이터를 주고받는 형식으로 되어있다. UART형태의 보내는 라인과 받는 라인 2개로 구성된 형태이다. 이 때 양쪽의 클럭이 달라서 문제가 발생할 수 있다면 클럭 자체를 보내는 라인을 하나 더 추가하면 해결되지 않을까? 그 역할을 담당하는 것이 바로 빅클럭이다. 빅클럭을 사용하면 보내는 쪽에서는 클럭이 올라갈 때 신호를 보내고, 받는 쪽에서는 떨어질 때 신호를 받는 형태로 정보를 주고받아 속도를 굉장히 올릴 수 있다. 수백Mbpsd에서 Gbps까지도 가능하다.

 

 사실 이런 빅클럭을 이용한 프로토콜 설정만 잘 하여도 매우 유용한 통신이 가능하다. 하지만 한번의 데이터 통신에는 아무런 문제가 없을지 몰라도 데이터 통신이 잦으면 헤더가 필요해진다. 이러한 헤더 부분은 소프트웨어적인 처리가 물론 가능하지만 이를 하드웨어적으로 처리하고자 만든 것이 바로 워드클럭이다. 워드클럭을 통해 데이터를 보낼 때 클럭을 하나 같이 보내주어 데이터의 시작을 알리는 방식으로 작동한다. 이 때 워드클럭을 통해 보내지는 클럭을 프레임 싱크라고 부르기도 한다.

 

 그러면 머신클럭은 뭘까? 그건 일종의 시스템 클럭이다. 보드 자체의 클럭이라고 봐도 무방하다. 이건 당연히 있어야 할 것이다.

 

고속으로 통신하기 위해서는 이 3개의 클럭 라인이 있으면 아주 좋다. 보통 워드클락과 빅클럭은 공용으로 쓰인다. 즉 칩과 칩 사이에 같이 동기화 되는 것이다. 이 때 코덱이 직접 클럭을 만든다면 이를 마스터 디바이스라고 한다. 만약 클럭을 만들지 않고 받아서 사용하면 슬레이브 디바이스라고 한다. 우리가 사용하는 코덱은 마스터와 슬레이브를 둘 다 지원한다. (클럭을 만들 수 있지만 사용하지 않으면 슬레이브가 될 것이다.) 보통 시리얼 디바이스가 마스터를 지원하면 마스터로 사용하기 때문에 우리 코덱은 마스터 디바이스라고 할 수 있다.

 

 오디오 데이터는 기본적으로 32비트로 이루어져 있다. 이를 더 쪼개보면 왼쪽 16비트, 오른쪽 16비트로 되어있다. 우리 칩으로 보낼 수 있는 오디오 데이터의 포맷은 크게 2가지로 나뉜다.


DSP I2S이다.

두 포맷 모두 WCLK가 하나씩 뜰 때 그게 바로 샘플링 주기가 된다. 이 때, I2Sleftright를 아예 구분하여 샘플링 한다는 차이가 있다.


위에서 언급한 데이터 포트들을 포함하여 모든 것들이 바로 MCASP라는 포트에 하드웨어 인터럽트로 연결되어있다. 따라서 우리는 가장 먼저 하드웨어 인터럽트를 설정해야 한다.


기존과 똑같이 app.cfg 파일을 통해 인터럽트를 작성한다. 이 때 event id는 데이터 시트를 통해 알아낼 수 있다.

 

MCASP61번으로 설정되어 있기 때문에 61event id가 되는 것이다.

 하드웨어 인터럽트를 설정했다면 이제 코드를 살펴보면서 설명을 이어가자.

인터럽트 넘버는 임의로 정하면 되는데, 우리는 5번을 사용할 것이다.

일단 I2C를 초기화 해야 한다. I2C_Init( 400 )을 통해 400으로 초기화한 것이다. 그렇게 빠르진 않다. 그 다음엔 코덱을 초기화해야 할 것이다. 이는 일종의 레지스터 설정이다. CodecInit에 샘플링 주파수, 워드랭스, 입력 출력을 인자로 넘겨주어 코덱 초기화를 시킨다. 데이터 포트도 초기화 해 줘야 한다. ConfigMcASP( MCASP_32BIT, MCASP_1SLOT, CFG, RINT, NO_XINT )McASP에게 한 워드가 오면 32bit를 받아야 한다는 사실을 알려주는 것이다. 이는 우리가 할 DSP16비트 스트레오이기 때문이다. 32비트짜리를 여러 채널을 받을 수 있게 되어있는데 우리는 하나만 받도록 하자

 

설정 할 때 첫 번째를 16비트, 두 번째를 2SLOT으로 해도 한번에 32비트씩 받아온다. 무슨 소리인고 하니 ConfigMcASP( MCASP_16BIT, MCASP_2SLOT, CFG, RINT, NO_XINT ) 이와 같이 사용하여도 32비트씩 받아 올 수 있다는 것이다. 다만 차이는 있다. 무슨 차이가 있을까? 그 차이는 바로 슬롯단위로 인터럽트를 발생시킨다는데 있다. 따라서 2SLOT은 인터럽트가 2번 발생한다. 대신 레프트와 라이트의 구별이 가능하다는 장점이 있다.

 

 CFG(config)는 나중에 설명하도록 하겠다.

 

 우리는 리시브 인터럽트를 쓸 것이다. (RINT) A/D를 통해 리시브가 들어오면 그때 인터럽트를 거는 것이다. , 오디오가 들어오면 A/D를 통해 리시브가 생기고 CPU에게 인터럽트 발생 후 트랜스펄 레지스터에 옮겨 적을 것이다. 그 후 다음 신호가 들어올 때 D/A컨버터를 통해 오디오가 나갈 것이다. (XINT trans interrupt이다.)

InitMcASP( RECEIVE, CFG )를 통해 초기화를 하면 기본적인 설정은 끝났다. 이제 인터럽트 함수만 만들면 될 것이다.

 


인터럽트 서비스 루틴 함수이다. 데이터가 들어오면 인터럽트가 발생하여 저 MCASP_ISR함수가 호출 될 것이다. MCASP_ISR함수는 입력을 받아 출력으로 복사하는 역할을 한다. RBUF14는 리시브레지스터의 이름이고 XBUF13은 트랜스펄레지스터의 이름이다. 각각 레지스터는 14번라인과 13번 라인에 물려있다. 레지스터를 지정하여 값을 넣어줌으로써 실시간으로 받는 음악을 출력으로 바로 내보낼 수 있게 된다.

윗부분 구멍은 입력데이터이고, 아래 구멍은 출력이다. 핸드폰 등을 이용해여 음악을 넣어주면

밑의 구멍에 꽂은 이어폰을 통해 음악을 들을 수 있다.

 

 IdleLED함수는 cpu가 오디오 출력을 끝마친 후 남는 시간에 실행되는 함수이다. 현재 Cpu48000분의 1초에 한번씩 작동하고 있다. 따라서 대부분의 시간이 남을 것이기 때문에 IdleLED함수를 대부분 실행 중일 것이다. 이를 통해 노래도 재생 되면서 LED도 깜박일 것이다.

 

 , 이제 뭔가 DSP스러운 최초의 작업을 해보자. 가장 간단한 코드를 통해 스트레오의 left right를 나눠서 한쪽 귀만 안 들리게 하는 것이다. 위의 설명을 잘 읽었다면 간단하다! 16비트 단위로 끊어 한쪽을 0으로 만들면 그 쪽 소리는 들리지 않을 것이다!


이렇게 말이다. 이 코드가 이해가 되지 않는다면 C언어의 비트 시프트와 비트연산을 공부하길 바란다.

 .. 근데 저기 저 왼쪽의 수많은 파일들이 보이는가.. 매우 난잡하다. 실질적으로 쓰이는 파일은 몇 개 없음에도 불구하고 여러 파일들이 함수를 불러오거나 라이브러리처럼 사용하기 위해 쓰이고 있다. 이런 파일들을 깔끔하게 lib폴더에 넣어 저장하자.


대신 lib폴더에 넣었으면 해당 폴더를 path에 추가해줘야 한다.

프로젝트의 설정에 들어가 사진과 같은 경로로 들어 간 뒤 $(PROJECT_ROOT)\lib 을 추가해준다. 이제 기능을 하나씩 추가할 때마다 번호를 붙이거나 알파벳을 달리하여 학습하여보자.


LAB5_A <Delayed Codec Talk-Thr>

코드를 살펴보자. data라는 배열을 만들어 버튼이 눌렸을 때 데이터에 있는 값을 출력으로 내보낸다. 이 때 data의 크기는 1/8초에 해당하는 데이터 분량을 저장할 수 있는 크기이다. 따라서 처음부터 버튼을 누르고 있는다면 처음 1/8초동안은 0이 출력될 것이다. 그리고 나서 1/8초 후에 소리가 나올 것이다. , 1/8초만큼 딜레이 된다고 볼 수 있다. 이때 잘 안느껴지면 1/2로 바꿔보자.

 

단순히 입력을 메모리에 저장하였다가 일정 시간이 지난 후 출력으로 내보내는 이런 방식으로 딜레이를 주려면 메모리가 많이 필요하다. 지금 사용되는 메모리의 크기는 24000이다. 추가로 위의 사진에는 보이지 않지만 버튼을 감지하는 Idle함수도 제작하였다.

 

LAB5_A1


이번에는 버튼을 눌렀을 때 사인파을 생성해보자. y(n)=Asin(2pifn/Fs)

소리를 들어보면 뿌우ㅜ우우우우우우우우우우우우 거릴 것이다. 높낮이가 바뀌면서 말이다. 재밌고 신기하다~

 

LAB5_A2

이번에는 일정 주기마다 소리가 왼쪽과 오른쪽에서 번갈아 가며 나오게 해보자. 타이머를 활용해야 할 것이다. 이제는 익숙해진 app.cfg파일을 통해 Clock을 만들자.

함수를 ProcessClk이라 정한 뒤 인터럽트 파일 단에서 해당 함수를 생성해준다.

좌우 번갈아 가며 소리가 날 것이다.

 

LAB5_B < Single Buffer Processing>

 

더블 버퍼링을 나중에 설명할 것이다. 그 전에 싱글 버퍼를 먼저 살펴보면서 왜 더블 버퍼를 사용하는 지 알아보도록 하자.

지금까지 프로세싱한 것들은 모두 다 샘플 프로세싱이었다. , 샘플by샘플을 하나씩 처리하였었다. (사인은 인터럽트가 걸려서 사인을 생성 한 뒤 넘겼었다.) 입력이 들어옴과 동시에 리시브 인터럽트가 발생하여 해당 데이터를 샘플by샘플로 처리하는 경우도 물론 있지만 대부분의 경우 버퍼 단위로 모아서 데이터를 처리한다. 이러한 처리방식의 가장 큰 이유는 인터럽트 오버헤드이다. 매 샘플마다 인터럽트가 걸리면 1초에 48천번 인터럽트가 걸린다. 인터럽트에 의해 실행되는 함수는 사실 처리해야 하는 일이기 때문에 당연히 수행해야 하지만 인터럽트에 걸렸을 때 생기는 인터럽트오버헤드는 다른 문제이다. 인터럽트가 일종의 택배라면 택배를 받아오기 위해 하던 일을 멈추고 현관으로 나가 택배를 받은 뒤 다시 하던 일을 하러 방으로 돌아가는 과정이 인터럽트오버헤드라고 할 수 있다.

 

두 번째 이유는 무언가를 처리 할 때 통계적으로 처리하기 위해서이다. 데이터를 처리할 때 통계적인 성질을 이용하여 프로세싱 하는 경우가 많은데 이를 위해서는 데이터를 모아야 한다. 통계적인 성질을 이용하기 위해서는 데이터의 크기가 클수록 좋은 것이다.

 

따라서 input 버퍼가 있고 output 버퍼가 있다면 인풋에 데이터를 모으고 모은 다음에 아웃풋에 넣어준다. 그리고 아웃풋 버퍼에서 데이터를 실시간으로 출력한다. 당연하겠지만 이러한 버퍼 프로세싱은 기본적으로 딜레이가 발생한다. 이 과정에서 연산을 하고 싶다면 인풋 버퍼에서 아웃풋 버퍼로 옮기는 과정에서 연산을 하면 될 것이다. 증폭을 한다면 인풋에 다 모은 뒤 곱하기 n을 하여 아웃풋에 옮기는 식으로 말이다.

 

한번 버퍼 단위로 인풋버퍼에 들어온 값을 버튼을 누르면 사인값을 곱해서 아웃풋 버퍼에 넣도록 해보자. 기존에는 DA컴버터가 16비트라 싸인 함수 GenSine에서 short를 썼다. 하지만 이제는 들어온 값에 곱해지는 사인값을 그대로 하면 안될 것이다. 또한 값을 곱할 것이기 떄문에 GenSine()을 통해 나온 값은 -1~+1 사이 값을 가져야 한다. 따라서 기존과는 코드가 조금 달라져야 할 것이다.


왼쪽과 오른쪽의 오디오가 32비트에 묶여 있다는 것을 기억하자. 곱할 때 왼쪽과 오른쪽을 나눠야 한다!! 굉장히 주기가 긴 사인을 넣으면 재미있는 결과물이 나온다. 2헤르츠로 설정해보자.

잘 작동하는가? 아쉽게도 동작하지 않는다. BUFLEN 10으로 바꿔보자. 동작한다! 30에서는 실시간이 안되었던 것이다. 데이터를 많이 모으면 확실히 딜레이가 생긴다. 데이터의 양과 딜레이는 트레이드오프 관계에 있다. 하지만 보통 100~200샘플 정도를 모아서 처리하는데 반해 지금은 20샘플만 되어도 작동이 안되었다. 그래도 DSP라는게 이정도 만으로 실시간이 안 된다?

 

우선 이 설정을 맞춰놓자. 이것만으로도 성능이 많이 좋아질 것이다. sin 대신 sinf를 쓰면 더블프레시젼 연산을 안 해도 되어서 성능을 좀더 올릴 수 있다. 대략 50까지 올라간다.  하지만 근본적인 문제는 다른 곳에 있다. 바로 데이터를 버퍼에 쌓는 동안에는 CPU가 논다는 사실이다. 그러다가 단 한번 인풋 데이터의 텀 즉, 48000분의 1초 사이에 쌓아 둔 버퍼에 대한 계산을 한번에 다 해야 한다. 이게 바로 싱글버퍼의 문제점이다. 주어진 시간은 30/48000 임에도 계산은 1/48000에 하려는 것이다. 이를 해결하기 위해 더블 버퍼를 사용한다. 이 개념은 실시간 처리에서 가장 중요한 개념이다. 잘 알아두도록 하자.

 

더블버퍼는 인풋 버퍼와 아웃 버퍼를 2개씩 만든다! 이를 왔다갔다한다 하여 핑퐁버퍼라고 부른다. in_ping , in_pong 버퍼와 out_ping, out_pong 이렇게 4개의 버퍼를 사용한다. 두 개의 버퍼를 교차로 사용하면서 남는 시간에 이전 버퍼의 연산을 진행한다는 점이 싱글버퍼의 문제점을 해결하는 방식이다. 이렇게 작동하면 버퍼의 크기가 얼마가 되든 상관 없이 연산이 가능해진다.

 

하지만 더블버퍼의 개념만으로는 매번 인터럽트가 걸리는 문제는 해결하지 못한다. 인터럽트가 자주 걸리는 문제는 후에 DMA를 배우고 처리할 것이다.

 

LAB5_C

 

1. ping-pong buffer를 사용할 것이고

2. SWI를 사용할 것이다.


- 인풋 버퍼에 넣고 아웃풋 버퍼의 내용을 내보내는 건 급한 일이다. (-> HWI) 그 외 시간에 하는 연산은 2순위 이다. 2순위의 작업들을 SWI에 넣을 것이다.

 

#include <ti/sysbios/knl/Swi.h> 를 통해 소프트웨어 인터럽트를 사용할 것이다.

 

Swi_post( swi0 ); swi0를 포스팅하는 기능이다. Swi0  무엇일지 감이 잡히지 않는가?

바로 SwiHandle이다. Handle을 통해 SWI를 조절할 수 있다. , 하드웨어 인터럽트는 swi0를 통해 ProcessSWI 를 포스팅 한 뒤 종료한다. 포스팅은 일종의 예약과 같아서 하드웨어 인터럽트가 종료된 후 동작한다.

 

 위 코드들을 통해 더블 버퍼를 활용한 DSP를 구현하였다. 이러한 더블버퍼링은 크기의 제한이 없다. 그저 딜레이만 늘어날 뿐이다. 크기를 아무리 늘려도 계산할 수 있는 시간 또한 같이 증가하므로 버퍼사이즈를 400으로 늘려도 잘 작동한다. 이 구조는 매우 중요하다. 앞으로도 이 구조를 가지고 계속 사용할 것이다.

 

다만 현재 cpu 1초에 48000번 인터럽트가 걸리는 것은 변함이 없다. 그런데 인터럽트는 오버헤드가 크다. 앞서 언급하였듯이 하드웨어 인터럽트는 간단한 일임에도 그 과정이 복잡하다.

 

따라서 DMA를 사용하여 데이터 무브를 활용할 수 있다. DMA는 딱 데이터만 옮길 수 있는 칩이다. 이를 활용하면 엄청나게 효율적인 구조로 동작할 수 있다. 그렇게 되면 cpu는 하는 일이 정말 없을 것이다.

 

LAB5_D


프로젝트가 커지면 하드웨어 인터럽트와 소프트웨어 인터럽트의 양이 많아져 파일을 나누게 된다.

그냥 단순히 두 파일을 나눴을 때 중복하여 사용되는 which_buffer를 글로벌 변수로 선언하여 사용하게 된다. 하지만 글로벌 변수는 가급적이면 안 쓰는 것이 좋다. 따라서 우리는 소프트웨어 인터럽트를 포스팅 할 때 메세지를 보낼 수 있는 사실을 이용할 것이다. 즉 인자를 넘기는 것이다. 그러면 함수명이 조금 달라진다.

 

Swi_or( swi0, which_buffer ); // 포스팅 위치버퍼를 보낸다.

Int32 which_buffer = Swi_getTrigger(); //메세지를 받아온다.

 

, 이전에는 하드웨어 인터럽트가 완전히 종료 된 후 which_buffer의 값이 넘어갔었다. 그래서 반전이 된 후 넘어갔는데 인자로 넘기는 경우 반전되기 전 값이 넘어가기 때문에 기존의 코드를 고쳐야 한다.

아주 작은 차이이지만 실제 프로그램 상에서는 크게 작용할 수 있기 때문에 신경 써야 할 것이다.

반응형