널모뎀을 통한 직렬 통신 프로그램
2004년 5월에 작성된 문서입니다.
━┯━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│목차│
└──┘
0x00. 잡담
0x01. 기초지식
0x02. 프로그램 분석
0x03. 마치며
≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
━┯━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│0x00. 잡담│
└─────┘
학교 과제 중 널모뎀을 이용한 파일전송을 하는 과제가 있었는데..
재미가 있어서 정리도 할 겸, 소스코드와 기타 관련 지식들을 함께 적어봅니다.
컴퓨터가 두대 있으신 분은 직접 시리얼 포트를 이용해서 테스트 해보세요.
글이 전반적으로 반말투인데 이해바랍니다.
≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
━┯━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│0x01. 기초지식│
└───────┘
먼저 프로그램을 구현하기 전에 몇가지 알아두어야 할 기초지식을 알아보고
본격적으로 소스를 구현해보도록 하자.
데이터 전송에는 두가지 방법이 있다.
하나는 병렬 전송으로써 한번에 n 개의 비트씩 전송하는 방식이고,
다른 하나는 직렬 전송으로써 한번에 한 비트씩 전송하는 방식이다.
우리는 이용할 전송방식은 질렬 전송이 되겠다.
이 직렬 전송에는 다시 두가지 방법으로 나뉜다.
한 문자 단위로 동기화하여 전송하는 비동기식과 데이터를 블록 단위로 동기화하는 동기식이 있다.
우리가 이용할 방법은 비동기식 전송형식이다.
좀 더 자세하게 알아보면 비동기식 전송형식은
시작비트(1비트) + 데이터비트(5~7비트) + 패리티비트(1비트) + 정지비트(1~2비트)
로 구성되어 있으며 시작비트는 말 그대로 데이터 전송의 시작을 나타내며 2진수 0의 값을 갖는다.
데이터비트는 한 문자에 해당하는 코드값이고, 패리티비트는 데이터가 정상적으로 전송했는지 오류를 검사하기
위한 비트이다. 정지비트는 한 문자 전송의 끝을 나타내며, 2진수 1의 값을 갖는다.
그렇다면 널모뎀이란 무엇인가?
두 대의 컴퓨터를 모뎀없이 케이블로 직접 연결하는 방식을 말한다.
여기서 쓰이는 케이블은 RS-232이다. RS-232에 대한 자세한 내용은 terms.co.kr에서 확인하기 바란다.^^;
추가적으로 숙지해야할 내용은 인터럽트와 레지스터이다.
인터럽트란 현재 진행중인 작업을 중단하고 강제적으로 다른 작업을 수행하도록 하는 기능으로써
이 프로그램에선 스프트웨어 인터럽트를 이용할 것이다.
레지스터는 임시 기억장소로써 프로세서의 산술 연산과 논리 연산 수행에 사용되며
우리가 알아야 할 레지스터는 범용 레지스터이다.
레지스터 기능
AX(Accumulation Register) 전송하거나 연산을 수행할 코드나 데이터를 지정
BX(Base Register) 특정 블록의 시작 주소를 지정
CX(Counter Register) 루프 연산 등에서 반복할 횟수를 지정
DX(Data Register) 전송하거나 연산을 수행할 데이터의 시작 주소를 지정
SP(Stack Pointer) 스택의 최신 데이터가 저장되어 있는 번지를 지정
BP(Base Pointer) 스택에 있는 데이터의 번지를 간접적으로 지정
SI(Source Index) 번지의 간접 지정에 사용(주소를 옮길 데이터가 있는 곳의 주소)
DI(Destination Index) 번지의 간접 지정에 사용(주소가 옮겨질 데이터가 있는 곳 주소)
IP(Instruction Pointer) 프로그램에서 다음에 실행할 곳의 번지를 지시
더 많은 기초지식(?)들이 필요하나, 프로그램을 분석해 나가는 과정에서 추가적으로
설명하도록 하겠다.
≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
━┯━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━
│0x02. 프로그램 분석 │
└──────────┘
■ 레지스터 구조체의 정의
union REGS {
struct WORDREGS x;
struct BYTEREGS h;
};
struct WORDREGS {
unsigned int ax;
unsigned int bx;
unsigned int cx;
unsigned int dx;
};
struct BYTEREGS (
unsigned char al;
unsigned char ah;
unsigned char bl;
unsigned char bh;
. . . 중 략 . . .
};
위에서 설명한 레지스터 AX의 경우 상위 8비트와 하위 8비트로 나누어 사용이 가능하다. AX -> AH, AL
프로그램 구현도중 AX로 사용해야할 경우가 있고, 나눠서 사용해야할 경우가 있다.
유니온을 사용하여 구조체를 정의했기 때문에 메모리가 공유되어 ax로 사용하든 ah과 al로 사용하든 문제가 없다.
그 때를 위해서 구조체를 WORDREGS와 BYTEREGS로 정의해 놨다.
■ int86 함수
#include <dos.h>
int int86(intno, inregs, outregs)
int intno;
union REGS *inregs, *outregs;
80x86 프로세서에서 인터럽트를 발생시키는 기본적인 C함수로써 intno는 인터럽트 번호이고,
inregs는 인터럽터 전의 레지스터값이고, outregs는 인터럽트 후의 레지스터 값을 나타낸다.
int86()의 리턴값을 ax 레지스터의 값이다.
여기서 인터럽트 번호와 레지스터 값에 대해 알아보자.
직렬통신을 위해서 0x14번 인터럽트를 사용하는데 서비스 번호에 따른 기능은 다음과 같다.
0번 서비스는 직렬 포트 초기화
1번 서비스는 직렬 포트로 한 문자 송신
2번 서비스는 직렬 포트에서 한 문자 수신
3번 서비스는 직렬 포트의 상태 검사
DX 레지스터는 직렬포트 지정에 사용하고
AH 레지스터는 서비스 번호 지정에
AL 레지스터는 사용하는 서비스에 따라 용도를 달리할 것이다.
■ 직렬 포트의 초기화 (int 14h, 0번 서비스)
void port_init(int port, unsigned char code)
{
union REGS r;
r.x.dx=port;
r.h.ah=0;
r.h.al=code;
int86(0x14, &r, &r);
}
정의한 union을 선언한 후 r.x.dx에는 프트번호를,
r.h.ah에는 0번 서비스 즉, 직렬 포트 초기화를 하고,
r.h.al에는 code를 넣고 int86()함수를 호출한다.
여기서 AL레지스터는 초기화 설정 비트를 말한다.
초기화 설정 비트는 어떻게 설정하는 것인지 알아보자.
비트 의미 상태
1-0 데이터 비트 길이 10: 7비트, 11: 8비트
2 정지 비트 길이 0: 1비트, 1: 2비트
4-3 패리티 비트 정의 11: 짝수, 01: 홀수, 00: 노패리티
7-5 보드율(전송율) 000: 110bps, 001: 150bps
010: 300 bps, 011: 600 bps
100: 1200 bps, 101: 2400 bps
110: 4800 bps, 111: 9600 bps
예: 9600bps이고, 짝수 패리티에 1개의 정지비트, 8개의 데이터 비트로 설정할 경우 -> 111 11 0 11
이런 식으로 AL레지스터에 값을 넣어주면 된다.
이 프로그램에선 231초기화 비트를 사용한다. 2진수로 바꿔서 확인해 보면 11100111이 된다.
■ 한 문자 송신 (int 14h, 1번 서비스)
void sport(int port, char c)
{
union REGS r;
r.x.dx=port;
r.h.al=c;
r.h.ah=1;
int86(0x14, &r, &r);
if(r.h.ah & 128) {
printf("send error detected in serial port");
exit(1);
}
}
DX레지스터에 직렬 포트를 지정하고, AH레지스터에 서비스번호 1번을 할당한다.
1번 서비스는 직렬 포트로 한 문자 송신한다는 것을 의미한다.
AL레지스터엔 전송할 바이트를 지정한다.
AH레지스터를 이진수 10000000(128)을 비트의 and 연산을 해서 거짓일 경우 즉, 1번서비스가 아닐 경우
에러메시지를 출력해 준다.
■ 포트의 상태 검사 (int 14h, 3번 서비스)
unsigned int check_stat(int port)
{
union REGS r;
r.x.dx=port;
r.h.ah=3;
int86(0x14, &r, &r);
return(r.x.ax);
}
DX레지스터에 직렬 포트를 지정하고, AH레지스터에 서비스번호 3번을 할당한다.
서비스 번호 3번은 직렬 포트의 상태 검사이다.
인터럽터 후 전송선의 상태를 AH레지스터에 모뎀의 상태를 AL레지스터에 반환한다.
리턴값은 하나만 지정할 수 있기 때문에 ah+al => ax로 처리한 것이다.
이제 전송된 AH레지스터와 AL레지스터의 바이트에 따른 상태를 알아보자.
□ 전송선의 상태(AH레지스터) 바이트
설정 되었을 때의 의미 비트
데이터가 준비 되었음(data ready) 0
오버런 에러(overrun error) 1
패리티 에러 2
틀 에러(framing error) 3
브레이크 검출 에러 4
Transfer holding register empty 5
Transfer shift register empty 6
타임-아웃 에러 7
□ 모뎀의 상태(AL 레지스터) 바이트
설정 되었을 때의 의미 비트
Clear-To-Send 에서의 변화 0
Data-Set-Ready 에서의 변화 1
Trailing-edge ring detector 2
전송선 신호의 변화 3
Clear-To-Send 4
Data-Set-Ready 5
링 지시자 6
Line signal이 감지됨 7
■ 한 문자 수신 (int 14h, 2번 서비스)
unsigned char rport(int port)
{
union REGS r;
while(!(check_stat(PORT) & 256))
if(kbhit()) {
getch();
exit(1);
}
r.x.dx=port;
r.h.ah=2;
int86(0x14, &r, &r);
if(r.h.ah & 128)
printf("read error detected in serial port");
return(r.h.al);
}
수신을 위해 프로그램이 키보드를 클릭하기 전까지 kbhit()함수를 이용해서 체크한다.
만약 수신용 프로그램을 종료하길 원하면 아무키보드나 클릭하면 된다.
DX레지스터에 직렬포트를 지정하고 AH레지스터에 서비스번호 2번을 할당하고,
수신한 바이트는 AL레지스터에 저장하여 반환한다.
■ 파일 보내기 send_file() 함수
① 전송할 파일 open(read mode)
② 파일 이름을 한 바이트씩 전송: send_file_name() 함수
③ 파일의 크기 검사: filesize() 함수
④ 파일의 크기 전송
⑤ 파일의 실제 내용을 한 문자씩 전송
void send_file(char *fname)
{
FILE *fp;
char ch;
union {
char c[2];
unsigned int count;
} cnt;
if((fp = fopen(fname, "rb")) == NULL) {
printf("cannot open input file\n");
exit(1);
}
send_file_name(fname);
wait(PORT);
cnt.count=filesize(fp);
sport(PORT, cnt.c[0]);
wait(PORT);
sport(PORT, cnt.c[1]);
do {
ch= getc(fp);
if(ferror(fp)) {
printf("error reading input file");
break;
}
if(!feof(fp)) {
wait(PORT);
sport(PORT, ch);
}
} while(!feof(fp));
wait(PORT);
fclose(fp);
}
실질적으로 파일을 보내는 부분이다.
먼저 파일을 바이너리 read모드로 연 후 send_file_name()함수를 호출한다.
이 부분은 파일의 이름을 전송하는 부분이다.
그 후 wait()함수를 호출하는데 이는 전송보내고자 하는 것이 제대로 호출이 되었는지 여부를 '.'이
되돌아옴을 확인해서 알 수 있다.
그런 후 파일크기를 cnt.count로 받아서 그 값을 cnt.c[0]과 cnt.c[1]로 sport()함수를 통해서 전송해 준다.
변수선언부를 보면 유니온을 정의하고 있다. 유니온은 메모리를 공유해서 사용하기 때문에
이런식의 편법을 사용한 것이다.
그리고 실절적으로 파일을 전송하는 부분인 do while문이 나온다.파일의 끝일 때까지 getc()함수를 이용해서 전송을 한다.
전송을 마친 후 마쳤다고 wait()함수를 호출한다.
void send_file_name(char *f)
{
printf("Transmitter waiting...\n");
do {
sport(PORT,'?');
} while(!kbhit() && !(check_stat(PORT) & 256));
if(kbhit()) {
getch();
exit(1);
}
wait(PORT);
printf("sending %s\n\n",f);
while(*f) {
sport(PORT, *f++);
wait(PORT);
}
sport(PORT,'\0');
}
이 부분은 파일의 이름을 전송하는 부분이다.
void wait(int port)
{
if(rport(port) !='.') {
printf("communication error\n");
exit(1);
}
}
이 부분은 전송측에서 전송을 한 후 수신측에서 그 응답을 '.'이 오면 정상적으로
송신이 되었음을 확인하기 위한 함수이다.
unsigned int filesize(FILE *fp)
{
unsigned long int i;
i=0;
do {
getc(fp);
i++;
} while(!feof(fp));
rewind(fp);
return(i-1);
}
이 부분은 파일이름을 전송 한 후 전송하고자 하는 파일의 크기를
알려주기 위한 함수이다. do while()을 이용해서 파일의 크기를 확인한다.
■ 파일 받기 rec_file() 함수
① 파일 이름 수신: get_file_name() 함수
② 저장할 파일 open(write mode)
③ 파일의 크기(바이트 수) 수신
④ 파일의 실제 내용을 한 문자씩 수신
void rec_file(void)
{
FILE *fp;
char ch;
char fname[14];
union {
char c[2];
unsigned int count;
} cnt;
get_file_name(fname);
printf("receiving file %s\n", fname);
remove(fname);
if((fp=fopen(fname, "wb")) == NULL) {
printf("cannot open output file\n");
exit(1);
}
sport(PORT, '.');
cnt.c[0]=rport(PORT);
sport(PORT, '.');
cnt.c[1]=rport(PORT);
sport(PORT, '.');
for(; cnt.count; cnt.count--) {
ch=rport(PORT);
putc(ch, fp);
if(ferror(fp)) {
printf("error writing file");
exit(1);
}
sport(PORT,'.');
}
fclose(fp);
}
get_file_name() 함수를 이용해서 파일 이름을 전송받는다.
그 이름으로 파일을 바이너리 write모드로 연 후 send_file()함수에서 보낸 값들을
정상적으로 수신했다는 의미로 sport()함수를 이용해서 '.'을 송신해 준다.
그리고 for()을 이용해서 실질적인 파일의 내용도 수신받는다.
void get_file_name(char *f)
{
printf("receiver waiting...\n");
while(rport(PORT) !='?');
sport(PORT,'.');
while((*f=rport(PORT)) !=NULL) {
if(*f !='?') {
f++;
sport(PORT,'.');
}
}
}
파일의 이름을 전송받는 부분이다. 송신측은 수신측에 전송을 하기 위한 준비과정으로
'?'를 전송하는데 그 부분이 아닌 파일이름에 해당된다면 하나씩 파일이름을 전송받고 '.'을 받았다는
표시로 다시 전송해 준다.
■ 전송 프로그램
전송측: C:\> trans s 파일이름
수신측: C:\> trans r
main(int argc, char* argv[])
{
if(argc < 2) {
printf("Usage: trans s filename OR trans r\n");
exit(1);
}
printf("File transfer program in operation. To abort,\n");
printf("press any key.\n\n");
port_init(PORT, 231);
if(tolower(*argv[1]) == 's') send_file(argv[2]);
else rec_file();
}
이 프로그램은 아귀먼트를 달리하여 수신용과 송신용으로 쓸 수 있다.
먼저 수신용을 r모드로 실행한 후, 송신용을 s모드로 파일이름을 명시하여 호출하면
수신용은 rec_file()함수를 실행할 것이고, 송신용은 send_file()함수를 실행할 것이다.
≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
━┯━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│0x03. 마치며│
└──────┘
도스에서 프로그램을 돌렸기 때문에 스샷을 하지 못했습니다.
하지만 학교에서도 충분히 테스트 할 수 있으리라 생각됩니다.
전체 코드도 첨부하겠습니다.
≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
