지난 번 위모트(Wii Remote)를 이용해서 멀티 터치 화이트 보드를 만드는 동영상( http://kkamagui.tistory.com/266 )을 만드신 분께서 새로운 동영상을 찍으셨습니다. 상당한 충격이었는데, 이분이 이번에는 헤드 트래킹(Head Tracking)을 이용해서 VR 디스플레이를 만드셨군요. 대단합니다. ㅜ_ㅜ=b
이 분의 펜클럽이라도 만들어야겠습니다. 아주 그냥 신선한 아이디어가 흘러 넘치시는군요. ㅜ_ㅜ 게임 같은 것에 응용하면 상당히 괜찮겠네요. 저도 나중에 한번 시도해 봐야겠습니다. ^^;;;
흑흑... 연수때문에 미리 쓰고 간다고해서, 아주 죽는 줄 알았습니다. 그래도 거의 3주만에 3회를 냅다 쓰고 가는군요. ^^;;;; 글이 서툰 저이지만, 후배들과 선배들의 도움으로 그나마 빨리 끝낼 수 있었습니다. 이제 휴가는 거의 한주 밖에 남지 않았는데... 남은 휴가를 알뜰히 써야 겠습니다.
물론 옷도 좀 사고 말이지요 ;) 오늘은 위모트가 와서 하루종일 위모트와 놀았네요. ^^ 이런 재미난 장난감들이 너무 많이 널려있어서 신납니다. 나중에 여유가 되면 PSP도 하나 질러서 천천히 분해(?)하며 놀아야겠군요. 괴물머신이라 불리우는 PSP의 저력을 한번 느껴봐야~ ㅎㅎ
시간이 너무 빨리가서 불만입니다. ^^;;;; 시간이 조금만 더 있으면 좋을텐데 말이지요. ㅜ_ㅜ...
왜 터치스크린의 값이 이토록 튀는 것일까? 아래와 같은 문제점때문이 아닐까 추측하고 이것을 해결하려 노력했다.
X의 좌표값을 읽고 Y의 좌표값을 읽었을 때 SPI를 통해서 값을 읽고 이것을 몇변 반복하는 동안 시간이 걸린다.
터치스크린으로 부터 값을 읽는 동안 언제든지 사용자의 동작에 따라 펜이 Release 될 수 있다.
ARM7에서 값을 쓰고 ARM9에서 값을 읽을 때 동기화의 문제
이제 위의 문제에 대해서 하나하나 살펴보도록 하자.
1.X/Y 좌표 값을 읽는 타이밍(Timing) 문제
터치스크린쪽 스펙을 보면 터치스크린의 값이 유효하지 않을 수 있으니, 여러번 읽어서 사용하라고 되어있다. 실제 코드도 그것을 증명하듯 libnds도 그렇고 문쉘의 소스도 여러번 읽게 되어있다. 실제로 유효하지 않은 값인지를 판단하는 부분은 양쪽이 다 다르지만...
여러번 읽어서 유효한 값을 찾는 것도 중요하지만, 여기에는 큰 문제가 있다. 일단 SPI를 통해서 읽기 때문에 메모리에서 바로 읽는 것 보다는 더 큰 시간이 걸리고(스펙을 보라... 클럭이 1~3MHz 정도다.. ㅡ_ㅡ;;;) X의 좌표를 여러번 읽게 되므로 Y의 좌표를 읽을 때 쯤에는 상당한 시간이 지나있다. 반복하는 회수에 따라서 차이가 있긴 하겠지만 많이 읽으면 읽을 수록 갭은 커친다.
문쉘의 경우에는 5번을 반복하되 읽은 값이 처음 읽은 값과 비교하여 오차범위가 30 내외이면 유효한 값이라 판단하고 그 값을 사용하게 된다. 5번 반복하는 것도 좋지만 어차피 유효한 값을 체크하는 것이라면 3번 정도도 충분하고 오차범위도 30은 큰편이라 생각되어 10으로 줄였다.
물론 오차범위를 줄이면 유효한 값이라 판단되는 범위가 줄어들어서 펜이 Release가 되었다고 인식될 확률이 높지만, 펜이 자주 릴리즈되는 문제는 타이머를 사용하면 해결 할 수 있기 때문에 될 수 있으면 최대한 유효한 정보를 얻는데 초첨을 두었다.
2.펜 릴리즈(Pen Release) 문제
값을 반복해서 읽다 보면 도중에 펜이 릴리즈되어 값이 유효하지 않는 경우가 생긴다. 이때 값은 문서에 보면 X 축의 유효하지 않은 값은 0으로 송신한다. 하지만 Y의 경우는 0이 아니라 0xFFF로 온다. Y 값 같은 경우 0xFFF가 수신되면 0으로 변경하여 값을 넣게 하고 응용 프로그램에서 터치스크린의 Touch.X, Touch.Y의 값이 둘다 0이 아닐때만 사용하도록 하여 정확도를 높였다.
3. ARM9과 ARM7의 동기화(Syncronization) 문제
사실 이 부분은 가상 IPC 구조체에 Busy 플래그를 1로 설정하는 것으로 처리하고 굳이 신경쓰지 않았다. 그 이유는 ARM7에서 터치스크린에 값을 읽는 시간은 디스플레이의 V Count를 이용하여 80번째 스캔라인에서 읽도록 했기 때문이다. ARM9의 경우 가상 IPC에서 값을 읽는 시간은 VBlank 때이므로 시간적 차이가 좀 있다. 따라서 크게 신경쓰지 않았다. 추후 더 자주 읽어야하거나 어떤 문제가 발생하면 좀 더 동기화에 세밀한 신경을 써야 할지도 모르겠다.
4.구현
아래는 실제 코드인 _touch.c 파일의 내부이다. 첨부 파일로도 추가해 놓았다.
#include "_touch.h"
#include <nds.h>
#include <nds/jtypes.h>
#include <nds/system.h>
#include <nds/arm7/touch.h>
#include <nds/registers_alt.h>
#include <stdlib.h>
// 글로벌 변수들
static bool gs_bTouchInit = false;
static long gs_lXScale, gs_lYScale;
static long gs_lXOffset, gs_lYOffset;
/**
문쉘에서 사용하는 touchRead 함수
libnds 함수랑 크게 차이 없음
*/
__attribute__((noinline)) static uint16 _touchRead(uint32 command) {
uint16 result;
SerialWaitBusy();
// Write the command and wait for it to complete
REG_SPICNT = SPI_ENABLE | SPI_BAUD_2MHz | SPI_DEVICE_TOUCH | SPI_CONTINUOUS; //0x0A01;
REG_SPIDATA = command;
SerialWaitBusy();
// Write the second command and clock in part of the data
REG_SPIDATA = 0;
SerialWaitBusy();
result = REG_SPIDATA;
// Clock in the rest of the data (last transfer)
REG_SPICNT = SPI_ENABLE | 0x201;
REG_SPIDATA = 0;
SerialWaitBusy();
// Return the result
return ((result & 0x7F) << 5) | (REG_SPIDATA >> 3);
}
/**
온도를 읽는 함수
libnds와 크게 차이 없음
*/
uint32 _touchReadTemperature(int * t1, int * t2) {
*t1 = _touchRead(TSC_MEASURE_TEMP1);
*t2 = _touchRead(TSC_MEASURE_TEMP2);
return 8490 * (*t2 - *t1) - 273*4096;
}
/**
TOUCH의 값을 읽어서 유효성을 체크하여 값을 리턴하는 함수
*/
__attribute__((noinline)) static s32 readTouchValue(int measure, int retry, int range)
{
int i;
s32 this_value=0, this_range;
s32 last_value = _touchRead(measure | 1);
for ( i=0; i < retry; i++)
{
this_value = _touchRead(measure | 1);
this_range = abs(last_value - this_value);
if (this_range <= range) break;
}
if ( i >= retry ) this_value = 0;
return this_value;
}
/**
터치 스크린 사용에 관련된 변수값을 설정하고 초기화 하는 함수
*/
void _touchReadXY_AutoDetect(void)
{
gs_lXScale = ((PersonalData->calX2px - PersonalData->calX1px) << 19) / ((PersonalData->calX2) - (PersonalData->calX1));
gs_lYScale = ((PersonalData->calY2px - PersonalData->calY1px) << 19) / ((PersonalData->calY2) - (PersonalData->calY1));
gs_lXOffset = ((PersonalData->calX1 + PersonalData->calX2) * gs_lXScale - ((PersonalData->calX1px + PersonalData->calX2px) << 19) ) / 2;
gs_lYOffset = ((PersonalData->calY1 + PersonalData->calY2) * gs_lYScale - ((PersonalData->calY1px + PersonalData->calY2px) << 19) ) / 2;
gs_bTouchInit = true;
}
/**
외부에서 사용하는 함수
실제 Pixel 좌표와 Raw 좌표를 리턴
*/
touchPosition _touchReadXY()
{
touchPosition touchPos;
if(gs_bTouchInit==false)
{
REG_IME=0;
while(1);
}
// 매크로를 사용하지 않고 그냥 hex 값으로 사용함
touchPos.x = readTouchValue(0xD4 | 1, 2, 10);
touchPos.y = readTouchValue(0x94 | 1, 2, 10);
touchPos.z1 = readTouchValue(0xB4 | 1, 2, 30);
touchPos.z2 = readTouchValue(0xC4 | 1, 2, 30);
// x의 값은 0이면 Release이지만 y는 0xfff이면 Release다. 따라서 처리 로직 추가
if( touchPos.y == 0xFFF )
{
touchPos.y = 0;
}
s16 px = ( touchPos.x * gs_lXScale - gs_lXOffset + gs_lXScale/2 ) >>19;
s16 py = ( touchPos.y * gs_lYScale - gs_lYOffset + gs_lYScale/2 ) >>19;
if ( px < 0) px = 0;
if ( py < 0) py = 0;
if ( px > (SCREEN_WIDTH -1)) px = SCREEN_WIDTH -1;
if ( py > (SCREEN_HEIGHT -1)) py = SCREEN_HEIGHT -1;
touchPos.px = px;
touchPos.py = py;
return touchPos;
}
5.마치며...
실제 위의 코드를 적용하여 새로운 버전의 KKAMAGUI Notepad를 구현하였는데, 튀는 문제가 없어졌다. 앞으로 저 소스를 libfat 대신 이용해야 겠다.