소개
이 페이지는 Xillybus' programming guide for Linux를 기반으로 합니다. 아래 항목에 대한 보다 포괄적인 보기는 이 가이드를 참조하는 것이 좋습니다. Microsoft Windows에 대한 유사한 가이드 도 있습니다.
아직 "Hello, World" 테스트를 수행하지 않은 경우 먼저 수행하는 것이 좋습니다.
Xillybus IP core 와의 통신은 host의 device files를 통해 이루어집니다. 이러한 device files는 일반 파일처럼 액세스됩니다. 그러나 일반 파일과 달리 device file은 디스크에 저장된 data를 나타내지 않습니다. 대신 device file 에 대한 읽기 및 쓰기는 I/O 작업으로 이어집니다.
따라서 거의 모든 프로그래밍 언어로 Xillybus의 device files 에 액세스할 수 있습니다. 이 목적으로 Linux command-line utilities를 사용하는 것도 가능합니다. 따라서 이 주제를 논의할 필요가 있는 이유가 무엇인지 궁금할 수 있습니다. 파일을 올바르게 읽고 쓰는 방법을 알고 있다면 Xillybus로 작업하는 방법을 알고 있는 것입니다.
실제로 API에 대한 자세한 지식 없이도 일반 파일에 액세스하는 프로그램을 작성할 수 있습니다. 그러나 이것은 I/O로 작업하기에는 충분하지 않습니다. 하드웨어와의 상호 작용은 일반 파일에서는 거의 발생하지 않는 상황을 만듭니다. 프로그래머로서 API의 도움으로 이러한 상황을 처리하는 방법을 알아야 합니다.
따라서 이 페이지의 대부분은 일반 파일 액세스와 관련된 항목에 전념합니다. device files 와의 차이점은 API를 사용할 때 실수에 대해 덜 관대하다는 것입니다.
API 에 대한 이해 부족은 주로 두 가지 종류의 혼란을 야기할 수 있습니다.
- I/O operation 의 data 양은 예상과 다를 수 있습니다(일반적으로 예상보다 적음).
- FPGA 와의 통신이 예상보다 늦게 발생합니다.
프로그래밍 언어 및 운영 체제
Linux를 사용하면 파일에 액세스할 수 있는 모든 도구 또는 프로그래밍 언어가 Xillybus에서 작동합니다.
Windows를 사용하면 C, C++, C#, Python, Perl 및 Cygwin와 함께 제공되는 모든 프로그래밍 언어에 문제가 없습니다. 그러나 일부 도구(예: MATLAB)는 Xillybus의 device files에서 작동하지 않을 수 있습니다. 이러한 도구는 device file이 일반 파일이 아님을 감지하고 이를 오류로 간주합니다. 일반적으로 low-level I/O용으로 의도된 extensions을 사용하는 이 상황에 대한 해결 방법이 있습니다.
아래 논의는 C 언어를 기반으로 하지만 주제는 모든 프로그래밍 언어와 관련이 있습니다.
예시 code
Xillybus웹사이트에서 다운로드할 수 있는 코딩 예제가 있습니다. 이 예제는 C로 작성되었으며 저수준 API를 올바르게 사용하는 방법을 보여줍니다.
예제를 다운로드하려면 demo bundle을 다운로드한 웹 페이지(예: Xillybus용 페이지 또는 XillyUSB용 페이지 )로 이동하십시오.
Linux를 사용 중이라면 Linux driver를 다운로드하세요. 예제 code는 동일한 .tar.gz 파일에 포함되어 있습니다.
Windows를 사용하는 경우 Xillybus package for Windows를 다운로드하십시오.
어느 쪽이든, 예시 code는 demoapps/ subdirectory내부에 있습니다. 이 code에서 각 I/O operation 에서 읽거나 쓰는 data 의 양은 작은 buffer가 할당되기 때문에 작습니다(128 bytes). 이것의 목적은 단지 code를 더 단순하게 만드는 것입니다. 실제 애플리케이션에서는 더 큰 buffer가 권장됩니다(32 kBytes가 보통 좋은 선택입니다).
Linux 와 Windows 의 차이점은 작지만 중요합니다. 특히:
- functions 의 이름은 약간 다릅니다. _open() 대 open() 등
- Windows의 경우: 파일을 열 때 _O_BINARY를 사용해야 합니다.
그러나 code 예시의 원리는 정확히 동일합니다.
Buffered file I/O
프로그래밍 언어는 일반적으로 파일 액세스를 위해 두 개의 별도 APIs를 제공합니다. 하나의 상위 수준 API및 하나의 하위 수준 API. 높은 수준의 API는 작업하기가 더 쉽기 때문에 더 일반적으로 사용됩니다.
예를 들어 C 언어에서 상위 수준의 API는 fopen(), fread(), fwrite(), fprintf(), fclose() 등으로 구성됩니다. 하위 수준의 API는 open(), read(), write(), close() 등으로 구성됩니다. 이 두 APIs 의 차이점은 다음과 같습니다. functions의 이름에 약간의 차이가 있을 뿐입니다. 높은 수준의 API는 user-space RAM buffers를 제공합니다. 이러한 buffers는 C run-time library에 의해 구현됩니다. kernel에서 driver 로 제어되는 DMA buffers와 혼동하지 마십시오.
가장 중요한 차이점은 fwrite()의 동작입니다. 이 function 에 대한 call 의 결과는 data가 user-space buffer에 저장될 수 있습니다. FPGA 로의 전송은 나중까지 지연될 수 있습니다. 실제로 data는 파일이 닫힐 때까지 fwrite()의 buffer 에 무기한 남아 있을 수 있습니다. data가 파일에 작성되었지만 아무 일도 일어나지 않았기 때문에 이것은 Xillybus의 버그처럼 보일 수 있습니다.
따라서 하위 수준(non-buffering) API를 사용하는 것이 좋습니다. 대부분의 프로그래밍 언어가 높은 수준의 API사용을 촉진하더라도 이는 일반적으로 가능합니다. 도구 또는 프로그래밍 언어가 하위 수준 API를 지원하지 않는 경우 대신 사용할 수 있는 API를 사용하십시오. 이 경우 I/O가 발생하는 시점을 제어할 수 없다는 점을 기억하는 것이 중요합니다. 그럼에도 불구하고 이것은 예를 들어 data acquisition와 같은 많은 응용 프로그램에서 충분합니다.
다시 한 번 말하지만 buffered I/O를 Xillybus의 buffers와 혼동해서는 안 됩니다. 가장 중요한 것은 Xillybus의 buffers때문에 data가 무한정 멈추지 않는다는 것입니다. 이에 대해서는 아래에서 zero-length write()와 관련하여 자세히 설명합니다.
device file에서 읽기: 기본 사항
device file 에서 읽기 위한 예제 코드는 streamread.c입니다. 이 program은 demoapps/ directory에서 찾을 수 있습니다. 그러나 아래에서 보여드릴 code는 다른 프로그램에서 나온 것입니다. memread.c (같은 directory에서). 이 program은 다른 목적을 위해 만들어졌지만 allread()라는 이름의 function을 포함하고 있습니다. 이 function로 몇 가지 주제를 설명하는 것이 더 편리합니다.
이 command로 파일이 열렸다고 가정해 보겠습니다.
int fd, len;
char *buf;
fd = open("/dev/xillybus_read_32", O_RDONLY);
이제 파일에서 @len bytes를 buffer로 읽어들이려고 합니다. 그러나 부분적인 결과는 허용되지 않아야 합니다. 우리는 항상 필요한 양의 data를 읽는 function을 원합니다.
이것은 allread()가 다음과 같이 사용될 때 하는 일입니다.
allread(fd, buf, len);
이 function은 다음과 같이 정의됩니다.
void allread(int fd, unsigned char *buf, int len) {
int received = 0;
int rc;
while (received < len) {
rc = read(fd, buf + received, len - received);
if ((rc < 0) && (errno == EINTR))
continue;
if (rc < 0) {
perror("allread() failed to read");
exit(1);
}
if (rc == 0) {
fprintf(stderr, "Reached read EOF\n");
exit(1);
}
received += rc;
}
}
이 function은 항상 요청된 bytes수를 읽는다는 점에 유의하세요. 이것이 가능하지 않으면 function은 program을 종료시킵니다. 이것은 일반적인 사용 시나리오에는 너무 과격할 수 있습니다. allread()는 저수준 API로 파일에 액세스하는 방법에 대한 간단한 데모로 취급해야 합니다.
이 function에 대해 설명하겠습니다. 첫 번째 흥미로운 부분은 다음과 같습니다.
rc = read(fd, buf + received, len - received);
@received는 첫 번째 loop동안 0과 같습니다. 따라서 이 행은 다음과 동일합니다.
rc = read(fd, buf, len);
read()는 file descriptor (@fd)에서 @len bytes를 읽고 data를 buffer (@buf)에 저장하려고 시도합니다.
Xillybus device file의 경우: 요청한 양의 data (@len bytes)를 사용할 수 없는 경우 read()는 최대 10 ms 까지 기다립니다. 이 짧은 시간이 지난 후 function은 필요한 것보다 적은 data (하지만 적어도 하나의 byte)를 가지고 돌아옵니다. data가 전혀 없는 경우 read()는 FPGA 에서 data가 도착할 때까지 무기한 기다립니다(하지만 예외가 있으며 이에 대한 자세한 내용은 아래 참조). 이 동작은 Xillybus의 driver 에만 해당합니다(하지만 표준 API와 호환됩니다).
read()가 무언가를 읽을 수 있는 경우 @rc는 읽은 bytes 의 수와 같습니다. 즉, @rc는 양수이고 loop 의 모든 if-statements는 건너뜁니다. 따라서 이것은 다음에 발생합니다.
received += rc;
따라서 @received 에는 항상 지금까지 읽은 bytes 의 총 개수가 포함됩니다. @rc가 @len (요청된 bytes 의 수)보다 작은 것은 완전히 합법적이고 정상입니다.
while-loop은 @received (읽은 bytes 의 총 수)가 @len에 도달할 때까지 계속됩니다. 이것은 while statement에 반영됩니다.
while (received < len) { ... }
따라서 필요한 경우 더 많은 data를 읽습니다.
rc = read(fd, buf + received, len - received);
이번에는 buffer 의 시작점을 @received로 이동합니다. 요청된 bytes 의 수도 같은 수로 줄어듭니다. 이러한 조정은 이것이 data를 읽으려는 반복적인 시도라는 것을 반영할 뿐입니다.
read()가 아무것도 읽지 않을 때
지금까지 read()가 데이터를 읽을 수 있을 때 무슨 일이 일어나는지에 집중했습니다. 그러나 read()가 아무것도 읽지 못하는 세 가지 상황이 있습니다. 이러한 각 상황은 자체 if-statement에 의해 처리됩니다.
POSIX signals
CTRL-C를 눌러 program을 중지하면 운영 체제가 POSIX signal을 process로 보냅니다. 이것이 program을 종료시키는 메커니즘입니다. "kill" command를 같은 목적으로 사용할 때도 같은 일이 발생합니다. 하지만 대부분의 상황에서 무시해야 할 다른 유형의 signals 도 많이 있습니다.
그렇다면 process가 read()에 대한 함수 호출 중간에 signal을 받으면 어떻게 될까요? Linux의 규칙에 따르면 read()는 즉시 주 program 에 제어권을 반환해야 합니다. read()가 이 일이 일어나기 전에 data를 읽을 수 있었다면 특별한 일은 일어나지 않습니다. @rc는 bytes의 번호를 포함하며 signal이 수신되었다는 표시는 없습니다.
하지만 새로운 data가 도착하지 않으면 @rc는 음수가 되고 @errno는 EINTR와 같아집니다. 이 상황을 처리하는 표준적인 방법은 code에서 보여지는 것과 같습니다. 아무 일도 없었던 것처럼 행동하고 다시 시도하십시오.
if ((rc < 0) && (errno == EINTR))
continue;
이것은 signal이 무시된다는 의미는 아닙니다. 예를 들어, signal 의 이유가 사용자가 CTRL-C를 눌렀기 때문이라면 program은 평소처럼 종료됩니다. 이를 처리하는 또 다른 메커니즘이 있습니다. 이 if-statement 의 목적은 극적인 일을 일으키지 않으려는 signals를 처리하는 것입니다. continue-statement는 process가 이런 종류의 signal을 수신하더라도 이상한 일이 일어나지 않도록 보장합니다.
예를 들어, process가 CTRL-Z로 중지되면 이 if-statement는 실행이 재개될 때 program이 계속 실행되도록 하는 데 필요합니다. 또한 인간의 개입 없이 도착할 수 있는 다른 signals가 여러 개 있습니다.
진짜 오류
당연히 data를 읽으려고 시도하는 동안 무언가 잘못될 수 있습니다. 이런 경우 @rc는 음수가 되고 @errno의 값은 EINTR가 아닌 다른 값이 됩니다. 예제 code는 이 오류를 보고하고 program을 종료합니다.
if (rc < 0) {
perror("allread() failed to read");
exit(1);
}
EOF
파일 끝에 도달했기 때문에 read()가 data를 제공할 수 없는 경우 이 function은 값 0을 반환합니다. 이것은 일반 파일의 경우에도 마찬가지입니다. 그러나 Xillybus 에는 data stream이 종료되었음을 선언하는 기능도 있습니다. 동작은 동일합니다.
관련 code는 다음과 같습니다.
if (rc == 0) {
fprintf(stderr, "Reached read EOF\n");
exit(1);
}
이것도 오류로 처리되어 program이 종료됩니다. 요청된 data (@len bytes)의 양을 읽기 전에 EOF 에 도달했음을 의미합니다. 이 if-statement는 @received가 @len보다 작은 경우에만 도달할 수 있습니다.
allread() 의 아이디어는 항상 필요한 양의 data를 읽는다는 것입니다. 이것이 가능하지 않으면 이 function은 컴퓨터 program을 멈춥니다.
다른 프로그래밍 언어와의 관련성
위의 code 예제는 C로 작성되었지만, 프로그래밍 언어와 상관없이 관련성 있는 몇 가지 중요한 점을 보여줍니다.
- read()는 요청한 것보다 적은 data 로 반환될 수 있습니다. 이는 buffered I/O 에서도 발생할 수 있습니다(예: fread() 사용). 그러나 buffered I/O에서는 오류가 있거나 EOF 에 도달한 경우에만 이 문제가 발생합니다. 대조적으로, 이것은 read()에서 정상이며 특별한 일이 발생했음을 나타내지 않습니다.
- program은 POSIX signals를 제대로 처리해야 합니다.
- read()는 모든 data를 읽고 end-of-file (EOF)에 도달한 후 값 0을 반환합니다.
device file에 쓰기
파일에 쓰기 위한 저수준 API는 파일에서 읽는 것과 거의 동일합니다. 이를 입증하기 위해 allwrite()라는 이름의 function은 streamwrite.c에서 찾을 수 있습니다.
void allwrite(int fd, unsigned char *buf, int len) {
int sent = 0;
int rc;
while (sent < len) {
rc = write(fd, buf + sent, len - sent);
if ((rc < 0) && (errno == EINTR))
continue;
if (rc < 0) {
perror("allwrite() failed to write");
exit(1);
}
if (rc == 0) {
fprintf(stderr, "Reached write EOF (?!)\n");
exit(1);
}
sent += rc;
}
}
위에 표시된 allread() 와 비교하십시오. 차이점은 세 가지뿐입니다.
- read()대신 write() 사용. 그러나이 functions는 정확히 같은 방식으로 사용됩니다.
- variable @received 의 이름이 @sent로 변경되었습니다. 그러나 차이점은 variable의 이름뿐입니다. 이 variable 의 의미와 사용법은 정확히 동일합니다.
- 텍스트 출력은 다음과 같이 조정됩니다. 이전에 "read"라고 표시된 곳에 "write"라고 표시되어 있습니다.
따라서 원칙적으로 쓰기와 읽기 사이에는 차이가 없습니다.
파일에 쓸 때 EOF 에는 의미가 없기 때문에 @rc는 0이 되어서는 안 됩니다. POSIX standard에 따르면 @rc는 write()가 0인 bytes를 쓰도록 요청된 경우에만 0일 수 있습니다. 하지만 이 while loop에서는 이런 일이 절대 일어나지 않습니다.
요약하면 allwrite()는 항상 요청된 bytes 의 수를 기록합니다. 유일한 대안은 process를 종료하는 것입니다. 즉, device file이 다음과 같은 방식으로 열렸다고 가정합니다.
int fd, len;
char *buf;
fd = open("/dev/xillybus_write_32", O_WRONLY);
@buf 에서 @len bytes 쓰기는 다음과 같이 수행됩니다.
allwrite(fd, buf, len);
allread() 에 대해 위에서 언급한 모든 내용은 allwrite() 에도 적용됩니다. 여기에는 다른 프로그래밍 언어와의 관련성이 포함됩니다.
Zero-length write
bytes가 0인 write() 에 함수 호출을 할 수 있습니다. 표준 API는 특정 경우에 어떤 일이 일어날지 말하지 않습니다. 그러나 분명히 이것은 data가 작성되지 않음을 의미합니다.
이러한 종류의 함수 호출은 Xillybus device file에 대해 특별한 의미가 있습니다. bytes를 0으로 쓰는 것은 flush를 요청하는 것을 의미합니다. 이것의 의미를 이해하기 위해 먼저 data가 device file에 기록될 때 어떤 일이 발생하는지 살펴보겠습니다.
device file이 asynchronous stream라고 가정해 봅시다. 이 용어는 다른 페이지 에 간략하게 설명되어 있으며 자세한 내용은 설명서 에 나와 있습니다.
data가 device file ( write() 포함)에 기록되면 Xillybus driver는 이 data를 RAM buffer에 저장합니다. 이 data 의 일부 또는 전부가 즉시 FPGA 로 전송될 수 있습니다. 하지만 일반적으로 data 의 일부는 buffer에 남아 있을 수 있으며 함수 호출을 수행한 program은 그래도 계속됩니다. 이 메커니즘의 목적은 성능을 개선하는 것이며, 특히 write()에 대한 함수 호출이 많은 경우 더욱 그렇습니다.
그렇다면 driver의 RAM buffer 에 있는 data는 언제 FPGA로 전송됩니까? 네 가지 가능한 상황이 있습니다.
- RAM buffer가 가득 찼습니다.
- device file이 닫힙니다.
- 10 ms 의 시간이 경과했습니다(automatic flush).
- zero-length write()가 수행됩니다.
따라서 data는 driver의 buffer 에 너무 오래 붙어 있지 않습니다. 이는 data가 항상 10 ms기간 내에 FPGA 로 전송되기 때문입니다. 그러나 일부 응용 프로그램에서는 이러한 지연도 허용되지 않습니다. 이 경우 zero-length write()를 사용하여 나머지 data를 즉시 보내도록 요청할 수 있습니다.
다음은 C 언어에서 이를 수행하는 방법입니다.
write(fd, NULL, 0);
address 에서 buffer는 NULL입니다. 쓸 bytes 의 수가 0이므로 괜찮습니다. 그러나 이 함수 호출은 요청의 성공을 보장하지 않습니다. 성공할 가능성이 매우 높지만 올바른 방법은 다음과 같습니다.
while (1) {
rc = write(fd, NULL, 0);
if ((rc < 0) && (errno == EINTR))
continue; // Interrupted. Try again.
if (rc < 0) {
perror("flushing failed");
break;
}
break; // Flush successful
}
이 모든 것은 asynchronous stream에 대해 언급되었습니다. device file이 synchronous stream인 경우 data는 write()에 대한 함수 호출의 결과로 항상 즉시 FPGA 로 전송됩니다. 그 외에도 write()는 data가 FPGA 에 도달할 때까지 기다렸다가 돌아옵니다( zero-length write()는 그렇게 하지 않음).
따라서 zero-length write()는 asynchronous streams와만 관련이 있습니다. 이 기능은 FPGA와의 통신 속도를 저하시키므로 필요한 경우가 아니면 사용해서는 안 됩니다.
요약
이미 언급했듯이 위에 작성된 거의 모든 내용은 모든 파일에 액세스하는 데 적합합니다. 몇 가지 주제만 Xillybus에만 적용되었습니다.
FPGA와 통신하는 동안 일관된 동작을 보장하기 위해 이러한 지침을 따르는 것이 중요합니다. 이러한 주제를 고려하지 않고 작성된 Programs는 가끔 오류가 발생할 가능성이 높습니다. 이러한 오류는 종종 FPGA 또는 driver의 문제인 것처럼 보입니다. 따라서 적절한 프로그래밍 기술을 사용하면 많은 혼란과 불필요한 노력을 절약할 수 있습니다.