이 문서는 파일에 접근하는 NDS 홈브루를 정상적으로 실행하기위해 DLDI 패치를 적용하는 방법을 설명한 문서이다.
1.DLDI란?
DLDI는 Dynamically Linked Device Interface의 약자로써, NDS 홈브루 개발에 사용되는 libfat 라이브러리에서 사용한다. libfat는 기기에 포함된 MicroSD 또는 SD와 같은 메모리 카드 영역에 접근하여 파일을 읽고 쓰는 기능을 하는 라이브러리이다. 메모리 카드에 데이터를 읽고 쓰는 NDS 홈브루라면 반드시 사용하고 있는 라이브러리이다.
하지만 각 기기마다 메모리 카드에 접근하는 방식에 차이가 있기 때문에, 자신의 기기에서 정상적으로 실행하기 위해서는 DLDI에 패치를 가해야 한다.
자신의 기기에 맞는 파일을 다운로드하여 위에서 다운 받은 "dlditool32.exe" 파일이 있는 폴더에 넣자. 만약 파일이 정상적으로 다운로드 되지 않는다면 dlditool-win32-gui.zip에서 다운로드 하자. DLDI 패치 파일과 패치 프로그램이 포함된 압축 파일이다.
<DLDI 패치 파일>
4.홈브루 패치 실행
4.1 초기 화면 및 기기 선택
이제 "dlditool32.exe"을 실행하면 아래와 같은 화면이 표시될 것이다.
<초기 화면>
최상단의 붉은색 콤보박스는 홈브루를 동작시킬 기기를 선택한다. 만약 자신의 기기가 나타나지 않는다면 위로 돌아가서 다시 DLDI 패치 파일을 다운로드해서 "dlditool32.exe" 파일과 같은 폴더에 넣어두자.
4.2 NDS 홈브루 파일 선택
이제 실제 패치할 홈브루를 선택해야 한다. 홈브루 선택은 우측 중앙 쯤에 있는 "..." 버튼으로 선택하면 된다. "..." 버튼을 누르면 아래와 같은 파일 선택 창이 나타날 것이다. NDS 파일을 찾아서 넣어두자.
<NDS 홈브루 파일 선택>
4.3 패치 적용
파일을 선택했으면 제일 아래 쪽에 "Patch" 버튼을 누르면 된다.
<패치 적용>
패치가 성공하면 아래와 같은 메시지가 표시될 것이다. 이제 홈브루를 기기에 복사해서 실행하면 정상적으로 실행되는 것을 확인할 수 있다.
<패치 성공>
5.마치면서...
이상으로 NDS 홈브루에 DLDI 패치를 적용하는 방법을 알아보았다. 이제 NDS의 기능을 확장해주는 홈브루의 세계로 빠져보자. ^^)/~
많은 분들이 닌텐도 DS용 한글 입력 메모장을 쓰고 계시는데, 주의사항이 있어서 알려드립니다.
한글 입력 메모장은 기존 버전과 호환성을 위해 한 페이지에 쓸 수 있는 글 량이 제한되어 있습니다. 스크롤이 없는 이전 버전은 쓸 수 있는 최대 길이가 한 페이지를 꽉 채우는 정도 였습니다.
따라서 한글로 가득 채운다면 220자 정도, 영문으로 가득 채운다면 440자 정도가 최대이니 적당히 채우시고(?) 다음 페이지로 이동하시기 바랍니다. ㅠㅠ
나중에 시간이 나면 업데이트할 때 이를 표시해주는 기능을 넣을 예정이니 불편하시더라도 주의해서 쓰시기 바랍니다.
드디어 닌텐도 DS(NDS)용 한글 입력 메모장 프로그램 v1.2가 나왔습니다. 많은 분들이 건의하셨던 커서 이동 기능과 메모 스크롤 기능이 추가되었습니다. 이제 메모를 수정하기위해 전체를 수정하지 않아도 됩니다. ^^)/~
데이터 파일은 기존의 1.1 버전과 호환되므로 NDS 파일만 교체하시면 정상적으로 수행됩니다. ^^
다만 에디트 박스 컨트롤이 완전하지가 않아서 커서 이동 시에 한글 처리가 미흡합니다. ^^;;; 그 때문에 한글을 삭제하실 때 주의하실 점이 있습니다.
1. 한글을 지우실 때 커서를 한글 뒤쪽에 위치시킨다음 백스페이스를 2번 눌러주셔야합니다. 만약 한번만 누르시면 글자가 깨어집니다. 깨어졌을 경우 한번 더 백스페이스를 눌러주시면 정상적으로 표시됩니다.
2. 커서가 한글의 가운데에 위치했을 때 백스페이스를 누르시면 역시 글자가 깨어집니다. 물론 이때 커서를 오른쪽으로 한칸 이동한 다음 다시 백스페이스를 누르면 깨끗하게 지워집니다.
한글이 원래 2자리를 차지하기때문에 발생하는 문제인데, 다음 버전에 수정할 계획입니다. 급히 릴리즈하느라 소프트웨어 리셋 부분 또한 완전히 처리하지 못했는데, 다음 버전에 같이 수정하여 릴리즈하겠습니다. ^^;;;
1. 커서 표시 및 이동 기능 : 내용을 수정하려면 백스페이스를 이용해서 데이터를 모두 지웠던 불편함 때문에 추가. 윈도우의 에디트박스와 같이 커서를 표시하고 커서 위치를 통해 추가/수정/삭제하도록 변경함. 커서 이동은 왼쪽 방향키로 가능 2. 메모장 스크롤 기능 : 메모를 최대 한 페이지까지 밖에 입력하지 못했던 불편함 때문에 추가. 커서를 이용해서 스크롤 가능하도록 변경함
1. 소프트웨어 리셋 기능 : "START" 버튼을 클릭하여 펌웨어 시작화면으로 돌아가는 기능 추가. 파워를 껏다가 켜야했던 기존의 불편함을 줄임 2. TXT 추가 기능 : 메모 페이지에서 "A"버튼을 누르면 루트 디렉토리에 DATA + 페이지번호 +.txt 형식으로 txt 파일을 생성. PC에서 데이터 파일 공유 가능
3. 메모 페이지 확장 : 메모장의 페이지를 40 페이지를 확장. 기존의 20 페이지용 데이터 파일의 경우, 안전한 사용을 위해 모든 데이터를 백업해 놓고 루트 디렉토리에 있는 NOTEDATA.DAT 파일을 삭제하길 권장
1. 완성형 한글/영문/숫자 입력 기능 : 완성형 한글 오토마타를 이용하여 한글 조합 및 출력 가능. 실제 키보드와 거의 동일하게 자판을 배열하고 구성함. Shift 키와 Delete 키를 지원하고 Space바 좌측의 모드 변환 키를 통해서 키보드의 타입 변경 가능. 2. 메모 입력 기능 : 1줄당 최대 영문 42자 or 한글 21자 입력 가능. 최대 10줄까지 입력가능. 3. 메모 페이지 이동 기능 : 메모 페이지를 최대 20장까지 지원. 키보드 상단의 "<<" or ">>" 버튼으로 메모 페이지 이동가능. 최상단에 현재 페이지 번호 표시. 4. 메모 내용 저장 기능 : 페이지 이동 시 or ""저장" 버튼 클릭 시 메모를 저장하는 기능. NDS 재시작 후에도 이전 메모 기록 확인 가능
다른 소스는 다 올려놨으면서 이 소스는 왜 안올려 놓은건지... 하는 수 없이 윈도우 환경에서 구현하기로 했습니다. NDS를 하다말고 갑자기 왠 윈도우냐 하시는 분이 계실지 모르겠습니다. ^^;;;
NDS용 메모장 홈브루는 제가 틈틈이 만든 NDS 윈도우 라이브러리(http://kkamagui.tistory.com/37)를 기반으로 작성되었기 때문에, 윈도우 환경(MFC)과 매우 유사하기 때문에 가능한 것이지요. 개발을 편하게 진행하기 위해서 필요한 부분만 최대한 비슷하게 구현했습니다.
현재 커서와 데이터 출력, 스크롤 부분까지 처리가 끝났고 이제 남은 것은 커서가 있는 부분을 위주로 추가/수정/삭제하는 부분만 추가하면 되는데... NDS의 한정된 메모리를 사용해야 하기 때문에, 어떻게 하는 것이 좋을지 상당히 고민스럽습니다. 일단은 좀 더 생각해봐야겠습니다. ^^;;;;
아래는 실제 구현된 에디트 박스의 모습입니다. 글자 사이에 있는 빨간 선이 커서를 나타내고 제일 바깥쪽에 있는 붉은 색 선은 NDS의 LCD 영역을 나타냅니다.
아래에 첨부된 압축파일을 풀어서 실행하면 간단한 테스트 해볼 수 있으니, 심심하시면 한번... ^^)/~ 키보드의 화살표 키로 커서를 이동할 수 있으며, 그에 따른 커서 위치 처리와 스크롤에 관한 처리를 테스트 할 수 있습니다. ^^
대박 연휴를 맞아 집에 내려오게됬습니다. 집에서 인터넷으로 열차시간을 확인한 뒤, 역으로 왔더니 고새 매진이더군요. ㅡ_ㅡa... 어쩔 수 없어 한시간 뒤 기차를 예매하고 서점으로 갔습니다. 역에 갈때마다 한번씩 들리는 버릇이 생겼거든요. ^^;;;;
제일 먼저 마이크로소프트웨어 5월호를 들었습니다. 하는 일이 응용 프로그램과 거리가 멀다보니, 위쪽(?) 소식이 많이 궁금합니다(좋은 소식 덧글로 많이 전해주세요 ㅜ_ㅜ...). 마소 기사 목록을 죽 훓어보다보니 블로그 관리를 위한 툴에 대한 기사가 있던데, 갑자기 뭔가 번득하더군요. @0@)/~
블로그에 수시로 들러서 글쓰고 답글을 달지만, "블로그"에서 그 일을 해야한다는 고정관념 같은게 있었습니다. ㅡ_ㅡa. 그런데 저 기사 제목을 보고 나니 당장 툴을 만들어야겠다는 생각이 들더군요. 일일이 덧글 목록을 살펴서 답글을 달다보면 보지 못하고 넘어가는 덧글이 종종있거든요. ^^;;;;(혹시 제가 덧글에 답글을 달지 않았다면, 이 자리를 빌어 죄송하다는 말씀을 드립니다. (__) 일부러 안단게 아니니 용서해주세요 ㅜ_ㅜ)
연휴 기간에 덧글을 검색하는 테스트 프로그램부터 간단히 만들어볼까 생각중입니다. Python으로 만들면 간단히 할 수 있을 듯도 한데, 원체 오래되서 기억이 가물가물하네요. ^^;;; 이번에는 OpenAPI를 사용해서 덧글 리스트를 뽑아봐야겠습니다. ^^)/~
그리고 이전에 만들었던 NDS용 한글 메모장(http://kkamagui.tistory.com/410)을 PSP에 포팅할까 생각중인데, 그 이전에 한글 메모장부터 손볼 생각입니다. 메모장의 가장 큰 문제였던 커서 이동이 안되는 부분을 방향키를 이용해서 이동할 수 있도록 할까 합니다. 물론 커서도 추가해야겠지요. ^^)/~
연휴에 푹 쉬나 했더니 이번 연휴도 바쁘겠군요. ^^)/~ 역시 천성이 개발자인듯 합니다. ㅎㅎ 그럼 다들 좋은밤 되시길~ ^^
얼마전에 제 블로그에 태용님께서 조그만 선물을 해주시겠다는 댓글을 남기셨습니다. 재미있는 장난감이라고 하시길래, 게임 시디나 괜찮은 셈플 어플리케이션이겠거니 생각하고 있었는데.... NDS용 닥터를... 그것도 무려 3종류나 기부해 주셨습니다. 태용님께서 홈브루 개발자를 지원하는 프로젝트를 진행하시는데, 제가 거기에 포함된 모양입니다. ㅜ_ㅜ... 이런 영광이.... ㅜ_ㅜ
아래는 기부해주신 닥터의 사진입니다. DSTT, Acekard2, Acekard R.P.G 3종류입니다.
제가 쓴 글이 다른 분에게 도움이 되고, 그걸로 인해 제가 다시 도움을 받게 될 줄은 몰랐습니다. 왠지 얼떨떨한 기분이군요. ^^;;;; 뭐랄까... 마치 외국에서만 가능할꺼라고 생각했던 일이 저한테 닥치니... 기분이 굉장히 좋습니다. 안그래도 가지고 있는 구형 NDS에 오래된 M3 SD가 달려있어서 동생이 고생하곤했는데, 이제는 그럴 일이 없겠군요. ^^)/~
태용님께서 기부해주신 닥터는 더 열심히 개발하고, 홈브루 릴리즈할 때 테스트를 열심히 하라는 뜻인 것 같습니다(맞는가요? ㅎㅎ ^^;;;;). 열심히 하겠습니다. >ㅁ<)/~ 그리고 나중에 시간나면 제가 갖고있는 닥터와 비교해서 리뷰도 하나 쓰겠습니다.
마지막으로 얼굴도 모르는 저에게 이런 멋진 선물을 주신 태용님께 다시 한번 깊은 감사를 드립니다. ^ㅡ^)/~
1. 소프트웨어 리셋 기능 : "START" 버튼을 클릭하여 펌웨어 시작화면으로 돌아가는 기능 추가. 파워를 껏다가 켜야했던 기존의 불편함을 줄임 2. TXT 추가 기능 : 메모 페이지에서 "A"버튼을 누르면 루트 디렉토리에 DATA + 페이지번호 +.txt 형식으로 txt 파일을 생성. PC에서 데이터 파일 공유 가능 3. 메모 페이지 확장 : 메모장의 페이지를 40 페이지를 확장. 기존의 20 페이지용 데이터 파일의 경우, 안전한 사용을 위해 모든 데이터를 백업해 놓고 루트 디렉토리에 있는 NOTEDATA.DAT 파일을 삭제하길 권장
아래는 실행 화면을 캡쳐한 것입니다.
추가 요구사항이나 기타 건의사항은 http://kkamagui.tistory.com으로 부탁드립니다. ^^
그럼 좋은 하루 되세요 >ㅁ<)/~
틈틈히 만든 닌텐도 DS(이하 NDS)용 한글 입력기와 간단한 메모장 프로그램을 공개합니다. 생각보다 일이 많아서 릴리즈가 조금 늦어졌습니다. NDS용 홈브루(Homebrew)는 많이 있습니다만, 한글 입력이 가능한 홈브루가 없어서 만들어야겠다고 생각만하다가 이제서야 만들었네요. ^^;;;
프로그램은 아래와 같이 최상단의 페이지 번호와 그 아래의 메모입력 화면, 마지막으로 한글/영문/숫자 키보드로 이루어져있습니다. NDS의 듀얼 스크린의 장점이 여기서 나오더군요. ^^
이 프로그램이 제공하는 기능은 아래와 같습니다.
1. 완성형 한글/영문/숫자 입력 기능 : 완성형 한글 오토마타를 이용하여 한글 조합 및 출력 가능. 실제 키보드와 거의 동일하게 자판을 배열하고 구성함. Shift 키와 Delete 키를 지원하고 Space바 좌측의 모드 변환 키를 통해서 키보드의 타입 변경 가능. 2. 메모 입력 기능 : 1줄당 최대 영문 42자 or 한글 21자 입력 가능. 최대 10줄까지 입력가능. 3. 메모 페이지 이동 기능 : 메모 페이지를 최대 20장까지 지원. 키보드 상단의 "<<" or ">>" 버튼으로 메모 페이지 이동가능. 최상단에 현재 페이지 번호 표시. 4. 메모 내용 저장 기능 : 페이지 이동 시 or ""저장" 버튼 클릭 시 메모를 저장하는 기능. NDS 재시작 후에도 이전 메모 기록 확인 가능
프로그램의 기능은 굉장히 간단한데, 글로 쓸려니 길어집니다. ^^ 한번 써보시면 더 쉽게 이해하실 수 있을 겁니다. 아래는 실제 사용하는 동영상입니다.
약 이틀간의 작업끝에 한글 오토마타의 수정을 끝내고 아래와 같은 메모장 프로토타입을 만들었습니다. 이제 상단에 기능 버튼(오늘 일자 표시 및 삭제 기능)만 추가하면 릴리즈해도 괜찮겠군요. 시연 동영상이라도 하나 찍어서 올려야겠습니다. 물론 실행파일도 같이요 ^^)/~
메모장 프로토 타입
제가 만든 홈브루가 굉장히 많은데, 이번 것 만큼 손이 많이가는 것도 처음인것 같네요. 여튼 진행상태로 보면 오늘 내일 안에 메모장 프로토타입을 완성할 수 있을 것 같습니다. 파일로 저장하고 여러 메모를 관리할 수 있도록 하면 간단한 Todo List 메모용으로 사용해도 되겠네요 ^^;;;
여유가 좀 된다면 메모 검색화면이나 기타 기능도 넣으면 좋겠는데, 일단 고민을 좀 해봐야겠습니다. 일단은 1차 릴리즈에서 버그가 없는 것이 더 중요하니까요 ^^)/~~
아이팟 터치(iPod Touch)의 한글 지원 소식에 감명받아서 새벽에 잠깐 짬을내서 Nintendo DS( NDS )용 한글 입력기를 만들어봤습니다. ^^;;; 아직은 테스트 상태라서 많이 불안정한데, 일단 동작은 하니까 스크린 샷을 올려봅니다. ^^
한글 입력 테스트
아이팟 터치의 키보드가 너무 예뻐서 그대로 올려봤는데, NDS에 올리니 약간 포스가 떨어지는군요. ^^;;;
오늘 테스트 덕분에 한글 출력에 한가지 문제를 발견했습니다. 제가 NDS에 올린 폰트는 완성형이었는데, 윈도우에서는 확장 완성형 폰트를 사용했습니다. @0@)/!!! 왠지 '틍'과 같은 단어를 입력할때 깨어진다 싶었더니... 확장 완성형에서 지원하는 문자라서 그랬습니다.
확장 완성형 폰트를 만들어 넣는 것은 큰 문제가 안되지만.... 폰트 데이터가 너무 커지면 용량의 한계때문에 문제가 발생할 소지가 있어서 상당히 조심스럽습니다. 이 문제는 좀더 고민해본 다음 결정해야겠습니다. ^^;;;
벌써 해가뜨네요... ㅜ_ㅜ... 오늘도 일찍 일어나기는 힘들 것 같습니다. ㅜ_ㅜ...
=== 2007/11/26 추가 ===
너무 많은 분들이 이 글을 보러 오셔서, 오늘까지 진행상황을 더 추가했습니다. ^^ 폰트를 더 추가할까 하다가 NDS의 용량이 그리 크지 않아서 포기하고 한글을 조합하는 오토마타를 조금 손봐서 2360자용 한글 입력기를 만들었습니다.
현재 한글/영어 대소문자/숫자 및 특수문자 모두 입력 가능하며 특수문자는 자판 숫자 및 배열 때문에 빠진 것이 좀 있습니다. 추후 작업을 조금 더 진행하여 특수문자를 다 추가해야 할 예정입니다. (혹시 아이팟 가지고 계신분 있으시면 특수문자쪽 키보드 자판 배열 좀 알려주세요 ^^;;;)
완성형 한글 입력 및 영어, 숫자 입력을 캡쳐한 화면입니다. 다음에 작업이 완료되면 새글을 통해 릴리즈하겠습니다. ^^
티스토리에 글을 쓰다가 문득 위에 "동영상" 이라는 버튼이 보였습니다. 이게 뭘까? 한참을 고민하다가 설마하는 생각에 제가 가지고 있는 동영상을 올려보니 블로그에 표시되는 것이 아니겠습니까? @0@ 이글루스에서는 상상도 할 수 없던 것이 티스토리로 옮기니까 가능해 지는군요.
생각난 김에 예전에 만들어 뒀던 닌텐도 DS( NDS )용 작은 커널(Kernel) 시연 동영상을 올립니다. ^^ 커널은 OS(Operating System)의 핵심적인 부분으로 주변기기 제어에 필요한 잡다한 코드들을 제외한 핵심이라고 보시면 되는데요, 어차피 닌텐도 DS( NDS )는 그렇게 주변기기가 많지 않고 마이크 말고는 다 지원하고 있으니 작은 OS라고 해도 괜찮을 것 같습니다.
아래에서 시연 동영상을 보실 수 있습니다. 시작 화면이 시커멓게 나와서 조금 안타깝군요. ㅡ_ㅜ... 운영체제답게 멀티 태스킹을 지원하고 사운드 및 키패드를 이용해서 게임(??)을 Play 할 수 있습니다. 정겨운 버블버블 음악이 게임 내내 흘러나오고 @가 $를 다 먹으면 게임이 끝납니다. ^^;;;
왜 터치스크린의 값이 이토록 튀는 것일까? 아래와 같은 문제점때문이 아닐까 추측하고 이것을 해결하려 노력했다.
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 대신 이용해야 겠다.
즉 소스에서 a = bell_bin[ 3 ]와 같이 사용할 수 있도록 헤더를 구성해 주는 것이다.
3.makefile 확인 및 수정 방법
이제 편리한 사용을 위해 프로젝트의 makefile을 손볼 차례다. makefile을 조금만 수정하면 특정 폴더에 bin 파일을 넣는 것 만으로도 롬파일에 데이터를 포함하고 또 사용할 수 있다.
보통 데이터는 data 폴더를 사용하므로 bin 파일도 data 폴더에 넣는 걸로 하자.
#---------------------------------------------------------------------------------
# BUILD is the directory where object files & intermediate files will be placed
# SOURCES is a list of directories containing source code
# INCLUDES is a list of directories containing extra header files
# DATA is a list of directories containing binary files
# all directories are relative to this makefile
#---------------------------------------------------------------------------------
BUILD := build
SOURCES := source MyLibrary
INCLUDES := include build DATA := data
...... 생략 ......
#---------------------------------------------------------------------------------
# you need a rule like this for each extension you use as binary data
#--------------------------------------------------------------------------------- %.bin.o : %.bin #--------------------------------------------------------------------------------- @echo $(notdir $<)
@$(bin2o)
위에서 조금 의아한 부분이 INCLUDES에 있는 build 일 것이다. 왜 build 폴더를 Include에 넣느냐 하면, bin2o에 의해 생성된 .o 파일과 .h 파일이 build 폴더에서 생성되기 때문이다. 일단 한번 돌려보면 알 수 있다.
4.Wave or MP3 파일을 Raw 파일로 바꾸는 방법
Wave나 MP3 파일을 NDS에서 출력할 수 있는 Sound 파일로 바꾸려면 일단 Raw Data로 변경한 후, libnds의 PlaySound() 옵션에 적절하게 파라메터를 넘겨서 Play 해주면 된다.
TransferRegion 이라는 구조체를 정의해서 사용하는데, 터치 스크린의 좌표 및 RTC, 그리고 Sound 데이터 모두가 위의 구조체에 들어있다. 그리고 구조체가 존재하는 영역은 0x027FF000으로 이 영역은 메인 메모리 영역이다. 일반적으로는 사용할 수 없고 Debug 모드일때 8Mbyte로 메모리가 확장되는데 그때 사용할 수 있는 영역이다. NDS 홈브루 실행 시에 디버그 모드로 진입하는지는 확실치 않으나 저 영역은 디버그 모드가 아니라도 접근할 수 있는 영역이 아닐까 조심스럽게 추측해 본다.
2.마치며...
위의 구조체를 조금 바꾸면 우리가 원하는 데이터를 끼워 넣는 것도 가능하다. 혹시 추가로 기능이 필요하면 넣어서 사용하도록 하자.
NDS 개발 툴킷인 devkitPro를 보면 C 코드 같은 경우 THUMB 코드를 사용하도록 되어있다. 하지만 인터럽트가 발생하면 ARM 모드로 전환되고, 기타 ARM 코드로 생성된 라이브러리나 어셈블리로 짜여진 ARM 코드 등등 같은 것을 같이 호출하여 사용하기위해 interwork 옵션으로 중간에 proxy 함수(베니어 함수-veneer)를 사용하여 이를 처리한다.
Proxy 코드는 어떤식으로 생기는 것일까? NDS 커널에 간단한 테스트를 추가하여 이를 확인해 보기로 했다. NDS에서 C 코드는 기본적으로 Thumb 모드로 컴파일 된다. 하지만 어셈블리어 코드는 ARM 모드로 컴파일한다. 이 두 언어의 코드를 함께 빌드한 후, 역어셈블리(Disassembly)해보면 어떤 식으로 호출되는지 알 수 있다.
ARM 코드를 바로 호출하는게 중간에 THUMB 모드에서 ARM 모드로 전환하는 코드를 통해 호출하고 있는 것을 볼 수 있다. BX 명령은 오퍼런드(Operand)의 0bit 값을 CPSR의 THUMB 모드 비트로 설정해 주는 역할을 하는 명령어이다. 즉 오퍼런드의 0bit가 1이면 Thumb 모드가 되고 0이면 ARM 모드가 된다. 명령어에 대한 자세한 내용은 참고. ARM 어셈블리(Assembly)를 참고하도록 하자.
이제 main 함수부터 순차적으로 따라가보자. 맨 위에 <main>에서 bl 2014e40을 실행하면 __Test3_from_thumb() 함수가 호출되고, bx pc를 호출하면 PC는 항상 현재 실행하는 명령 + ( 2 * 명령어 크기 )의 위치에 있으므로(즉 짝수 이므로), bx pc를 하는 순간 ARM 모드로 전환된다. 그리고 PC는 현재 실행 중인 주소(2015e40) + 4byte( 2 * Thumb 모드 명령어 크기(2) ) 다음에 있는 b 2001d40을 실행하게 된다. 이렇게 하여 무사히 Thumb -> ARM으로 호출되었다.
그런데 뭔가 좀 이상한 느낌이 들지 않는가? BL 명령으로 __Test3_from_thumb 을 호출하고 나면 BX PC 명령에 의해 코드가 4Byte 또는 2Byte로 정렬되어있기 때문에 ARM 모드로 전환된다. 이러한 상태에서 ARM 코드를 정상적으로 실행한 뒤에 BX LR 명령으로 원래 루틴으로 돌아가는데... 정상적으로 다시 실행이 되려면 lr에 들어있는 0번째 bit가 1로 설정되어있어야 한다는 말이 된다.
실제로 그럴까? ARM 문서에 보면 그냥 LR에 들어가는 값은 현재 코드 위치 + 4라고만 되어있었다. 이 설명 만으로는 BX LR 을 수행했을 때 THUMB 모드로 정상적으로 돌아갈 수 없다. 그래서 테스트 코드에서 LR값을 받아 찍어보았다.
결과는 원래 리턴할 위치에 1값이 더 더해져 있었다. 원래 LR값은 0x20012D6에 4가 더해져서 0x20012DA가 되어야 한다. 그러나 실제 들어있는 값은 0x20012DB가 들어있었다. 이것은 결국 THUMB 모드의 BL 같은 경우 LR에 PC + 4 + 1의 값을 넣는다는 것을 의미했다. @0@)/~!!! 뭐 여튼 알아냈으니 다행... ARM모드의 경우는 BL 시에 LR의 값은 PC + 8이다.
아래는 ARM Architecture Manual에서 찾은 THUMB 모드에서 BL, BLX에 대한 내용이다.
2.2 타이머 함수 디스어셈블리(ARM->THUMB)
그럼 이번에는 반대의 경우를 보자. 인터럽트 핸들러 함수인 타이머 함수 핸들러 같은 경우 ARM 코드로 되어있다. 하지만 스케줄러를 호출하는 함수 같은 경우는 C로 된 함수를 호출하여 그 안에서 처리한다. 이 부분에 대한 코드는 아래와 같다.
2001466: b004 add sp, #16
2001468: bd10 pop {r4, pc}
위의 코드에서 보면 역시나 BX IP에서 IP 레지스터가 가리키는 값이 0x20013FD로 isrTimerInC의 주소 + 1로 설정되어있음을 알 수 있다. isrTimerInC 함수에서 lr 레지스터를 꺼내어 PC에 넣는 순간 역시 BX와 마찬가지로 레지스터의 0 bit가 CPSR의 Thumb 비트에 설정된다.
3.마치며...
ARM 코드와 THUMB 코드를 서로 호출할 때 어떠한 일이 일어나는지 확인해 보았다. 모드가 여러종류가 존재하다보니 이러한 복잡한 부분이 생기는 것 같은데... 한번 봐두는 것도 나쁘지 않은 것 같다. THUMB 모드의 명령어와 ARM 모드의 명령어의 동작이 약간씩 다르므로 익혀두는 것도 나쁘지 않은 것 같다.
NDS의 실제 속도는 얼마나 될까? 클럭으로 따지자면 둘다 100MHz가 안되니 그리 빠르다고는 할 수 없다. 하지만 이 수치만 가지고는 뭔가 와닿지가 않는다.
우리가 쉽게 느낄 수 있는 것? 화면 그리는데 몇 ms 걸리고, 얼마의 크기의 메모리를 복사하는데 몇 ms가 걸리고 하는 말이 더 와닿지 않는가? 지금부터 프로파일러를 사용해서 NDS의 속도에 대한 몇가지를 테스트 해보자. 프로파일러에 대한 내용은 22 타이머(Timer)를 이용한 프로파일러(Profiler) 만들기 에서 찾을 수 있다.
1.메모리 복사 속도
메모리 속도 테스트는 RGB555 포맷을 사용하는 프레임 버퍼(Frame Buffer)모드의 화면 전체를 복사하는 것을 테스트 했다. 참고로 스크린의 크기는 256 * 192 이므로 총 메모리양은 256 * 192 * 2 = 98304 Byte이다.
여기서 사용된 코드는 모두 GCC의 최적화 옵션 중 최고인 -O2 옵션으로 컴파일 되고 링크되었다.
The DPG file specification is simple. All DPG files contain a 36 byte header, followed by audio, and then a standard mpeg-1 video stream. The audio format can differ; older DPG files used a special WAV format audio, but newer versions of moonshell have phased that out in favor of MP2 audio.
MoonShell can be very particular about the types of MPEG video it will decode correctly. Videos made with ffmpeg do not work very well; MEncoder seems to be a better option. The FPS and bitrate rely on personal preference, but the DS does not have much processing power - lower bitrates and framerates seem to work better. Finally, since the DS screen is so small, rescaling to 256 by 192 is the most space-efficient.
Here is an example command using MEncoder (this should be on one line):
ARM의 어셈블리 명령어들은 뒤에 post fix가 붙는데, 그 post fix의 의미는 아래와 같다. 일반적으로 아무것도 붙지 않으면 AL이라고 가정한다. Branch 명령인 B와 같은 경우 그냥 사용하면 BAL이 되고 BEQ와 같은 조합으로 CPSR에 있는 Condition flag를 이용하여 조건 분기와 같은 역할을 할 수 있는 것이다.
1.2 Addressing Mode
1.3 Shifter operand Mode
12. <Rm>, =XXX => XXX의 Address를 Rm에 넣는다.
1.4 Post-Fix
S : 계산 후 CPSR에 SPSR을 넣어 복구
! : 계산 결과를 Base Register에 적용
^ : PC가 포함된 경우는 연산 후 CPSR에 SPSR을 넣어 복구 or SYSTEM/USER 모드가 아닌 경우이고 PC가 없는 경우는 SYSTEM/USER 모드 레지스터에 값을 로드/스토어
2.ARM/THUMB 명령어 리스트
2.1 필수 참고 사항
<Rd> : 목적지 레지스터를 의미, 일반적으로 결과값을 저장하거나 전송할 레지스터의 값
<post fix : 'S'>
<Rd>에 PC가 있을 경우 계산 후 CPSR에 SPSR의 값을 대입. SPSR이 존재하지 않는 모드(SYSTEM/USER) 모드일 경우는 동작을 보장할 수 없음
<Rd> PC가 없을 경우 계산 결과 플래그를 CPSR에 업데이트
<post fix : '!'> : 계산 후 Base Register의 값을 갱신. Multiple Load/Store Instruction 시에 유용히 쓰임
<shifter_operand> : #1와 같이 값 자체로도 쓰일 수 있으며 LSL #2와 같이 배럴 쉬프터(Shifter)를 사용해서 할 수도 있다.
BL, BLX : Branch with Link, Branch with Link Exchange
BL <target_addr>
BLX <target_addr>
THUMB 모드의 BL은 LR 레지스터에 0bit를 1로 셋팅하여 BX LR과 같은 명령 사용 시 THUMB 모드로 다시 돌아올 수 있도록 하고 있다.
3.ARM C Calling Convention
ARM의 경우 레지스터가 많기 때문에 Intel과는 달리 4개의 파라메터까지는 Register 0 ~ Register 3에 저장하게 된다. R0 같은 경우는 일반적으로 리턴 값으로 사용되기도 하며, Register 4 ~ Register 8은 Callee에서 사용하는 레지스터이다.
Caller는 R0 ~ R3 까지를 저장하고 복원할 책임을 가지며 Callee는 R4 ~ R8 까지를 저장하고 복원하는 책임을 가진다.
<APCS(Arm Procedure Call Standard)에서 각 레지스터의 역할>
ARM에서 Branch를 하면 LR 레지스터에 Return Address가 저장되므로, 다시 다른 함수를 Call 하게 되면 LR 레지스터를 저장하고 다시 복구해야 한다.
예전에 한번 StrongARM(ARM7)을 이용해서 간단한 OS를 만들어 본적이 있었다. 물론 아주 오래전 일이라 기억이 잘 안나지만 ARM 메뉴얼과 어셈블리어를 가지고 시름하여 결국 태스트 스위칭 기능까지 구현할 수 있었다.
시간이 흘러... 이제 NDS에 커널을 올려볼까 하고 있는데... 다시 어셈블리어를 보고 프로세서에 대해서 볼려니 아주 눈물이 난다. ㅜ_ㅜ.... 그래도 일단 봐야지... 어찌하겠는가... 크윽.. ㅜ_ㅜ
간단하게 프로그래밍하기위한 기능적인 부분만 보자.
1.ARM Architecture 소개
ARM은 RISC 구조의 CPU로써 매우 간단한 명령에들로 구성되어있다. ARM이 하버드 아키텍쳐를 사용했으며 어쩌구 저쩌구 하는 내용은 일단 생략하고... 나의 입장에서 보는 ARM의 구조는 그냥 단순한 블랙박스인데... 레지스터가 굉장히 많고 여러가지 모드를 가진 32bit CPU라는 정도이다. ARM에 대한 자세한 내용은 첨부에 올린 ARM Architecture Manual을 참고하도록 하자.
1.1 ARM Register
1.1.1 Register Overview And General Register
ARM은 여러모드를 가지고 있는데, 한 모드에서는 32bit R0 ~ R15까지 16개의 범용 레지스터 및 CPSR/SPSR의 상태 레지스터를 가진다.
몇개의 레지스터는 특수한 이름과 의미를 가지고 있는데 아래와 같다.
R13 : Stack의 Top을 가리키는 sp 레지스터
R14 : 함수 호출 시 리턴 주소를 저장하는 용도의 Link Register(lr) 레지스터
R15 : 경우는 현재 수행중인 명령 위치를 가리키는 pc 레지스터
몇개의 레지스터는 모드에 따라서 각 모드 전용의 레지스터를 가지는데, 일단 아래를 보자.
<ARM의 Register들>
위에서 보면 좌측 하단의 작은 삼각형이 있는 레지스터들이 있다. 이 레지스터들이 해당 모드의 전용 레지스터로써 R13(sp)와 R14(lr)의 경우는 거의 모든 모드에서 개별 모드의 레지스터를 사용한 다는 것을 알 수 있다. 그리고 User 모드와 System 모드는 레지스터를 그대로 공유한다. 일단 이 정도만 알아두자.
PC에 대해서 잠깐 언급할 것이 있는데, 첨부에 포함된 ARM Architecture Manual에 의하면 ARM 명령어 모드 일때 PC는 현재 명령 주소 + 8의 위치를 가리키고 THUMB 모드 일때는 현재 명령 주소 + 4의 위치를 가리킨다고 되어있다. 이 부분은 PC를 Base로 주소 연산을 할때는 상당이 주의해야할 부분이므로 잘 알아두록 하자. 단 STR 명령에 의해서 값이 레지스터->메모리로 저장될 때 이때는 PC + 8 이 될 수 도 있고 PC + 12가 될 수 도 있는데... 일단 8이라고 알아두자.
1.1.2 PSR(Program Status Registers)
PSR(Program Status Register) 레지스터는 현재 프로그램이 실행되는 모드를 나타내는 레지스터로써 현재 상태를 나타내는 CPSR과 이전의 상태를 나타내는 SPSR 레지스터가 존재한다. SPSR의 경우는 User 모드나 System 모드에서 실행중인 프로그램이 특정 Exception 모드로 변경되었을 때, User/System 모드의 CPSR을 저장하는 용도로 사용된다.
<PSR 레지스터의 구조>
ARM Architecture Manual을 보면 각 플래그에 대해서 아래와 같이 설명해 놓았다.
<PSR 레지스터 설명>
PSR의 아래 5bit는 Mode를 바꾸는데 사용된다. 직접 값을 CPSR이나 SPSR에 넣어서 모드를 바꾸는 것 또한 가능하므로 특정 모드로 전환할 수 있다. THUMB 모드의 경우 T Bit가 1로 설정된다는 정도만 알아두자.
2.ARM CPU Mode
ARM은 총 7개의 Processor Mode를 가지고 있다.
<ARM Processor Mode>
앞서 살펴봤듯이 User 모드를 제외한 모든 모드는 특권(Priviledge) 모드이고 User/System을 제외한 다른 모드는 Exception 모드이다.
재미있는 사용법을 가진 모드를 몇가지 살펴보면 아래와 같다.
FIQ : ISR 중에 특별히 빨리 처리가 되어야 하는 인터럽트 처리 모드
IRQ : 일반적인 인터럽트 처리 모드
Undefined : 명령이나 코프로세서가 존재하지 않아서 발생한 Exception 모드. 여기서 특수한 처리를 해주면 명령을 Emulation 하는 것도 가능
3.ARM Exception Vector And Handling
3.1 Exception Vector
Exception이 발생하면 당연히 Exception에 대한 처리를 해줘야 하는데 Exception에 따른 처리가 들어있는 곳이 바로 Exception Vector이다.
Exceptoin Vector의 경우 0x000000000 의 주소에 위치하거나 설정에 따라서 0xFFFF0000 위치에 위치할 수 있다.
<ARM Exception Vector Table>
위에서 보면 0x14가 빠져있는데 이것은 Reserved 된 Vector 이기 때문에 사용되지 않는다. Exception Vector의 경우 단순히 4Byte의 크기를 가지므로 해당 Exception을 처리하는 루틴으로 jmp하는 명령이 일반적으로 들어있다.
3.2 Exception Handling
Exception이 발생하면 어떤 일이 생길까? 단순히 모드만 변화되는 것일까? 실제로 Exception이 발생했을 때 일어나는 일은 아래와 같다.
// R14(lr) 레지스터에는 Exception 처리를 끝내고 돌아갈 주소가 저장된다.
R14_<exception_mode> = return link
SPSR_<exception_mode> = CPSR
CPSR[4:0] = excpetion mode number
// ARM 모드로 강제 전환
CPSR[ 5 ] = 0
// 만약 reset 이나 FIQ 같은 경우이면 FIQ를 Disable 시킨다.
if <exception_mode> == Reset or FIQ then
CPSR[ 6 ] = 1
// 인터럽트는 무조건 발생 불가
CPSR[ 7 ] = 1
// PC에 exception vector의 주소가 입력 됨으로써 해당 주소의 코드가 실행되게 된다.
PC = exception vector address
...... Exception 처리 ......
// 처리가 완전히 끝난 후 되돌아 간다.
CPSR = SPSR
PC = R14_<exception_mode>
위의 굵은 부분이 처리 흐름 부분인데, 인터럽트를 불가하고 Exception을 처리한다음 다시 복구하는 것을 알 수 있다. CPSR의 값을 SPSR의 값으로 복원하고 PC를 변경하는 것은 이것을 자동으로 해주는 명령어를 통해 할 수 있는데 접미사로 "S'가 붙은 MOVS와 LDMS와 같은 명령으로 가능하다.
Exception 처리에 대한 자세한 내용은 ARM Manual의 A2-14에 잘 나와있으므로 참고하자.
3.3 Exception Priority
Exception이라도 우선 순위가 다른 데, 아래같은 순서를 가진다.
1 (Highest) : Reset
2 : Data Abort
3 : FIQ
4 : IRQ
5 : Prefetch Abort
6 (Lowest) : Undefined instruction SWI
4.Memory Mapped I/O
Memory Mapped I/O는 Port I/O 방식과 달리 메모리 주소에 IO와 통신하는 라인을 연결하여 Memory에 값을 쓰면 버스를 통해 컨트롤러에게 전달되는 방식이다. 따라서 메모리 주소를 공유해서 사용하므로 프로그래밍 시에 접근이 용이하다는 장점이 있으나 실제 사용가능한 메모리 공간이 줄어든다는 단점도 있다.
Port I/O 같은 경우는 Intel CPU가 대표적으로써 I/O를 위한 별도의 주소공간이 존재하고 I/O를 위해 별도의 명령( IN/OUT )을 통해 데이터를 주고 받는 방식이다.
== 여기 나중에 내용 더 체우기 ==
4.마치면서...
ARM에서 프로그래밍을 하기위한 배경지식을 쌓기위해 ARM CPU에 대해서 간략하게 알아보았다. 이제 본격적으로 프로그래밍을 한번 해보자. @0@)/~
위의 링크에서 dswifi-src-XXX.XXX 로 표시되어있는 source가 포함된 버전을 받는다. 라이브러리를 받지 않고 굳이 소스를 받는 이유는 현재 사용중인 데브킷 프로의 버전과 라이브러리가 맞지 않을 수도 있고 기타 다른 라이브러리(PALib 등등)를 사용하고 있다면 데브킷 프로의 라이브러리들이 교체되었을 가능성도 있으므로 이런 일련의 문제를 한방에 해결하기위해 소스를 컴파일 하는 것이다.
소스를 다운받아 적당한 폴더에 압축을 풀고 해당 디렉토리로 이동하여 콘솔창에서 make를 입력하면 라이브러리를 빌드할 수 있다.
빌드가 끝나면 아래와 같은 화면이 나온다.
<빌드완료>
이제 빌드가 완료되었으므로 dswifi의 include와 lib 폴더를 devkitpro가 설치된 libnds 폴더 아래에 복사한다. 같은 이름의 파일들이 있다고 덮어쓸지를 물어보는 데, 전부 다 덮어쓰도록 하자. 이상으로 소스 컴파일 및 설치가 끝났다.
2.라이브러리 사용
dswifi를 사용하기위해서는 ARM9과 ARM7 양쪽 코어에서 작업을 해줘야 한다. 조금 까다로운 절차가 필요한데, 다행이 예제 셈플을 제공하고 있다. 예제 셈플은 http://www.akkit.org/dswifi/ 에서 받을 수 있다(라이브러리를 만든 저작자 같다). 비록 0.3 버전의 테스트 소스이지만 정상적으로 동작한다. 물론 손을 좀 봐야하는 것은 당연한 이야기!!!
홈브루 개발자라면 한번쯤은 자신이 개발한 홈브루도 소프트 리셋(전원 버튼을 누르지 않고 재부팅 시키는 방법)에 대해서 고민해 봤을 것이다. 문쉘(MoonShell)을 보면 플러그인 기능으로 Reset.mse를 통해 소프트 리셋(Soft Reset) 기능을 지원한다. 하지만 사실 문쉘 소스에 있는 Reset.mse는 모든 카드를 지원하지 않는다. 아무래도 제작자가 다 테스트 해보기엔 무리였을지도...
그런데 얼마전에 3 in 1 Expansion Pack 을 쓰면서 우연히 Rudolph(루돌프인가.. ㅡ_ㅡa..)라는 분이 이 문제를 해결했다는 것을 알았다. 이분 덕택에 문쉘 소프트 리셋 기능이 잘 동작하게 된듯....
이제 본격적으로 한번 분석해 보자.
1.ARM9 코드 분석
//---------------------------------------------------------------------------------
int main(void) {
//---------------------------------------------------------------------------------
MSEINFO_Readed=MSE_GetMSEINFO(&MSEINFO);
(*(vu32*)0x027FFE18) = r4->dirEntSector*512+r4->dirEntOffset*32; 코드에서 실제로 카드 내에 FAT 영역의 주소를 ARM7에 넘겨줘서 ARM7이 카드 명령을 통해 데이터를 읽게 만드는 부분이 중요한 부분인것 같다. ARM7에서도 카드에 접근해서 데이터를 읽을 수 있음을 보여주는 예이다.
이부분에 대한 자세한 내용은 아래의 1.1 참고 부분을 참조하자.
아래는 위에서 호출하는 _boot_VRAM_clear() 함수와 ret_menu9_R4() 함수이다.
1.1 참고 (*(vu32*)0x027FFE18) = r4->dirEntSector*512+r4->dirEntOffset*32 코드 분석
2007/07/26 테스트 결과 r4->dirEntSector 값은 실제 _DS_MENU.DAT가 존재하는 Directory Entry의 물리 섹터 번호를 의미하고 r4->dirEntOffset*32 값은 Directory Entry 내의 Offset을 의미했다. 위의 코드를 아래와 같이 고쳐서 테스트 해본 결과 0x2E0 섹터의 2번째 Offset에 _DS_MENU.DAT 파일이 존재하는 것으로 나왔는데, 실제 확인 결과 그러했다.
이것으로 보아 R4에는 디렉토리 엔트리 정보를 이용해서 실제 파일을 읽어들일 수 있음을 알 수 있었다.
또한 rebootlib 소스를 분석하면서 저 _DS_MENU.DAT 파일이 특수한 형태로 암호화된 nds 파일이라는 것을 알 수 있었고, ARM7 소스에서 왜 카드 명령을 통해 다시 읽어오는가도 짐작할 수 있었다. 리버싱을 통해 R4에게 명령을 내리고 R4 카드가 디코딩을 하면 그것을 다시 메모리에 복사하면 굳이 압축 해제 알고리즘을 몰라도 처리 가능하기 때문이었다.
2.ARM7 코드 분석
아래는 ARM7에서 사용하는 함수 메인이다.
__attribute__((noinline)) static void main_Proc_Reset(ERESET RESET)
{
switch(RESET){
case RESET_NULL: return; break;
case RESET_VRAM: {
REG_IME = IME_DISABLE; // Disable interrupts
REG_IF = REG_IF; // Acknowledge interrupt
*((vu32*)0x027FFE34) = (u32)0x06000000; // Bootloader start address for VRAM
swiSoftReset(); // Jump to boot loader
} break;
case RESET_GBAMP: boot_GBAMP(); break;
case RESET_GBAROM: boot_GBAROM(); break;
case RESET_MENU_DSLink: LinkReset_ARM7(); break;
case RESET_MENU_MPCF: break;
case RESET_MENU_M3CF: break;
case RESET_MENU_M3SD: break;
case RESET_MENU_SCCF: break;
case RESET_MENU_SCSD: break;
case RESET_MENU_EZSD: break;
//====== R4TF was added. case RESET_MENU_R4TF: ret_menu7_R4(); break; //====== EZ5S was added.
case RESET_MENU_EZ5S: ret_menu7_EZ5(); break;
//====== by Rudolph (2007/05/25)
}
#define CARD_CR1_ENABLE 0x80 // in byte 1, i.e. 0x8000
#define CARD_CR1_IRQ 0x40 // in byte 1, i.e. 0x4000
// CARD_CR2 register:
#define CARD_ACTIVATE (1<<31) // when writing, get the ball rolling
// 1<<30
#define CARD_nRESET (1<<29) // value on the /reset pin (1 = high out, not a reset state, 0 = low out = in reset)
#define CARD_28 (1<<28) // when writing
#define CARD_27 (1<<27) // when writing
#define CARD_26 (1<<26)
#define CARD_22 (1<<22)
#define CARD_19 (1<<19)
#define CARD_ENCRYPTED (1<<14) // when writing, this command should be encrypted
#define CARD_13 (1<<13) // when writing
#define CARD_4 (1<<4) // when writing
// 3 bits in b10..b8 indicate something
// read bits
#define CARD_BUSY (1<<31) // when reading, still expecting incomming data?
#define CARD_DATA_READY (1<<23) // when reading, CARD_DATA_RD or CARD_DATA has another word of data and is good to go
CARD에 직접 명령을 내려서 이것을 처리할려면 R4에 대해서 자세히 안다는 전제가 필요한데... 정말 대단한 사람들이 아닐 수 없다. 이걸 다 어떻게 분석한거지... ㅡ_ㅡ;;;
구버전의 GBA FS는 File ID에 FILE_STRUCTURE 포인터를 그대로 넘기도록 되어있어서 File ID를 이용하면 FAT 관련 정보를 모두 얻어낼 수 있었다. 하지만 libfat로 업그레이드 되면서 POSIX 표준 함수(fopen, fwrite, fread, fclose 등)을 지원하게 되었고 구조가 조금 달라지게 되었다. 물론 FILE* 값을 이용해서 fileno() 함수로 File ID를 얻어올 수 있지만 테스트 결과 GBA FS 처럼 FILE_STRUCTURE로 캐스팅해서 값을 정상적으로 얻을 수 없었다(물론 잠결에 테스트 했기때문에, 좀더 테스트를 진행해 봐야 한다.. ㅡ_ㅡa..)
다행이도 libfat의 Partition 관련 변수가 export 되어있으므로 그 변수를 이용해서 FAT 관련 기본정보를 얻을 수 있고, diropen() 과 같은 함수를 사용하면 Directry 구조체를 얻을 수 있는데 이것을 이용하면 Directory Entry에 대한 정보를 얻을 수 있으므로 GBA FS 수준의 정보를 얻을 수 있다.
3.1 dir.h 분석
/* Directory iterator for mantaining state between dir* calls */
typedef struct {
int device; void *dirStruct; } DIR_ITER;
DIR_ITER* diropen (const char *path);
int dirreset (DIR_ITER *dirState);
int dirnext (DIR_ITER *dirState, char *filename, struct stat *filestat);
int dirclose (DIR_ITER *dirState);
위의 코드를 보면 diropen() 함수는 DIR_ITER* 를 리턴한다. 구조체의 dirStruct 필드가 심상치 않은데, 예상대로 아래에서 보듯 DIR_STATE_STRUCT 구조체를 얻을 수 있다.
3.2 fatdir.c
int _FAT_dirnext_r (struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { DIR_STATE_STRUCT* state = (DIR_STATE_STRUCT*) (dirState->dirStruct);
// Make sure we are still using this entry
if (!state->inUse) {
r->_errno = EBADF;
return -1;
}
typedef struct { const IO_INTERFACE* disc; CACHE* cache;
// Info about the partition
bool readOnly; // If this is set, then do not try writing to the disc FS_TYPE filesysType; u32 totalSize; u32 rootDirStart; <== 실제 Root Directory가 시작되는 물리 섹터 번호. 이것으로 접근하면 바로 Root Directory를 찾을 수 있다. u32 rootDirCluster;
u32 numberOfSectors;
u32 dataStart;
u32 bytesPerSector;
u32 sectorsPerCluster;
u32 bytesPerCluster; FAT fat; // Values that may change after construction
u32 cwdCluster; // Current working directory cluser
u32 openFileCount;
} PARTITION;
FAT에 관련된 거의 대부분의 정보는 PARTITION 구조체에서 얻을 수 있다. IO_INTERFACE 구조체를 이용하면 현재 사용중인 Device의 타입을 알 수 있다.
extern PARTITION* _FAT_partitions[];
위와 같이 사용하면 실제 libfat에 있는 파티션 정보를 얻어올 수 있고, 인덱스로 PI_DEFAULT( 0 ), PI_SLOT_1( 1 ), PI_SLOT_2( 2 ), PI_CUSTOM( 3 )과 같은 값을 넘겨주면 해당 Partition 정보에 접근할 수 있다. 보통 Default로 설정하여 libfat를 사용하므로 PI_DEFAULT or 0을 넣으면 Partition 정보를 얻을 수 있다.
프로그램을 만들다 보면 이상하게 느린 경우가 있다. 대부분이 알고리즘을 잘못 선택했거나, 아니면 쓸데없는 루프를 돈다거나, 극히 드문 경우지만 버퍼 오버플로우가 나서 다른 함수를 실컷 수행하다가 운좋게 다시 돌아오는 경우도 있다(실제로 겪어봤다... ㅡ_ㅡ;;;)
이런 경우 GetTickCount()나 혹은 다른 카운팅 함수를 이용해서 실행 시간을 측정해서 원인을 분석하는게 일반적이다. 시간을 측정하기 위해서는 어느정도 만족할만한 시간 분해능을 가진 타이머가 필요한데, 마침 NDS에서는 4개의 Timer를 가지고 있으므로 이것을 이용하여 프로파일러를 만들어보자.
1.타이머(Timer) 설정
타이머는 3번 타이머를 사용한다고 했다. 그럼 이제 남은건 분해능 설정인데, 1/1000초 정도면 괜찮을 것 같다. 아래는 Timer를 1/1000초로 설정한는 소스이다.
여러군데 값을 저장하여 테스트 할 수 있으므로 어느정도의 개수를 가지도록 해야 할텐데... 일단 지금은 10개로 해놨다.
3.구현
프로파일러를 구현하는 방법은 의외로 간단하다. 프로파일링을 시작하는 부분에서 현재 Timer의 값을 저장하고, 후에 결과를 출력할 부분에서 현재 값과 저장한 값의 차이를 리턴하여 걸린 시간을 리턴하면 된다. 시간을 계속 갱신하여 인터벌을 계산하고 싶으면 업데이트를 수행하면 된다(윈도우에서 간단하게나마 GetTickCount()를 이용해서 테스트 해본 사람은 금방 알 것이다.)
아래는 Profiler.h의 내용이다.
#ifndef __PROFILER_H__
#define __PROFILER_H__
#define MAX_PROFILERCOUNT 10
#define DWORD unsigned int
class CProfiler
{
protected:
static DWORD ms_vdwTime[ MAX_PROFILERCOUNT ];
ARM9만 사용해도 어느정도 기능을 갖춘 홈브루를 만들 수 있다. 왠만한 컨트롤은 ARM9에서 할 수 있으며, ARM7에서만 가능한 기능도 IPC를 통해서 libnds가 어느정도 구현해 놓았기 때문이다.
하지만 여기서 안주할 수 없다. ARM7에 특별한 기능을 추가하기 위해서는 당연히 ARM7 작성하는 것이 필수이다. 그럼 어떻게 ARM7 코드를 작성하여 ARM9 코드와 같이 NDS 파일을 만들 수 있는지 알아보자.
1.Root의 makefile
\devkitPro\examples\nds\templates 폴더에 가면 Combine 폴더가 있다. Combine 폴더는 ARM7과 ARM9의 elf 파일을 생성하고 그것을 하나로 뭉쳐 nds 파일을 생성하도록 make 파일이 구성되어있다. 일단 Root 폴더에 있는 makefile을 보자.
#---------------------------------------------------------------------------------
.SUFFIXES:
#---------------------------------------------------------------------------------
ifeq ($(strip $(DEVKITARM)),)
$(error "Please set DEVKITARM in your environment. export DEVKITARM=<path to>devkitARM)
endif
templete 폴더에 있는 makefile은 arm7과 arm9이 거의 비슷하지만 약간 다르다. 아래는 ARM7의 INCLUDES 값이다.
INCLUDES := include build
arm9 및 arm7에 build 폴더를 추가하자. bin2s.exe 파일을 통해 binary 데이터를 롬파일에 추가하면 build 폴더에 .h 파일이 생성된다. 따라서 편리하게 쓰기위해서 build 폴더를 include에 추가하자.
자작한 Custom Library를 위한 폴더를 추가해서 Custom 폴더에 자주쓰고 거의 변하지 않는 파일들을 넣어서 나중에 다시 사용할 수 있도록 하자. arm7/arm9 makefile의 SOURCES 부분에 MyLibrary 폴더를 추가하고 arm7/arm9 각각의 폴더에 MyLibrary 폴더를 추가한다. 앞으로 이 폴더에는 자체 제작한 라이브러리 파일을 넣을 것이다.
DATA 부분에는 롬파일에 메모리 배열형태로 추가할 데이터가 있는 폴더를 넣는다. data 폴더로 설정하고 .bin 파일의 이름으로 파일을 생성하면 bin2s.exe 실행파일에 의해 .o 파일이 생성되어 같이 링크되게 된다.
SOURCES := source MyLibrary
DATA := data
이 프로젝트를 빌드하면 .arm9 및 .arm7 파일이 생성되고 .nds 파일이 만들어 진다.
4.실행 결과
생성된 .nds 파일을 실행하면 터치 스크린의 좌표를 찍어주는 아래와 같은 화면이 보인다.
<실행화면>
5.마치며...
지금까지 ARM7 및 ARM9 모두를 사용하는 Templete 파일을 이용해서 나만의 Custom Project를 생성하는 방법을 알아보았다. 이로써 우리는 ARM7 및 ARM9 코드를 모두 작성할 수 있게 되었다. 폴더가 접혔을 때 백그라운드 처리 또는 끄기, 리부팅과 같은 ARM7에서만 접근 가능한 작업들을 할 수 있게 된 것이다.