Liên Lạc Với Các Thiết Bị Thí Nghiệm Qua Các Cổng Máy Tính
Trong Hệ Điều Hành Linux

Võ Quang Nhân  (Lược Dịch và Diễn Giải)       ngày 25 tháng 5 năm 2004
 
 

Các bạn thân mến,

Đôi khi các bạn muốn dùng computer để "đọc dữ liệu và ra lệnh" lên một số thiết bị ngoại vi cho các đồ án khoa học cuả mình (như là các thí nghiêm điều khiển robot, thí nghiệm về vật lí hay ngay cả việc nối thẳng với các thiết bị đo đạc  cho các nghiên cứu về hoá, sinh, y học,.... )  nhưng lại "mắc kẹt" không biết phải bắt đầu từ đâu. 

Sau đây là một bài viết khá rõ về viêc soạn một chương trình dùng ngôn ngữ C hướng dẫn các bạn những bước đi chập chững. Nếu như bạn không muốn tìm hiểu sâu vào chi tiết thì có thể chép lại mã nguồn ví dụ từ bài này đem ra và điều chỉnh lại theo ý muốn và nó có thể dùng để liên lạc qua cổng song song (parallel port) -- nhưng dẫu sao thì đọc hết các chi tiết sẽ giúp bạn giải thích rõ hơn lí do phải dùng lệnh này hay lệnh nọ cho nó ... phiền. Hì hì. Nhớ là đọc cách dịch ra tệp mệnh lệnh mà chúng tôi viết thêm với ví dụ. Còn như như bạn có đủ vốn Anh ngữ thì nên đọc thêm nguyên văn mà chúng tôi đính kèm ở cuối bài. Trong bài dịch này, chúng tôi đã ráng viết lại thêm thắt các chi tiết cho dễ hiểu hơn 1 tí. Lời khuyên chung cuả ngươì dịch là: "bạn nên đọc kĩ chi tiết vì nó có thể giúp bạn giảm thiểu tối đa các trở ngại do sai sót hay bất cẩn

Kiến thức cần thiết mà bạn phải có là hiểu chút ít về ngôn ngữ C và nếu bạn biết thêm về Linux thì càng tốt. Hơn nưã để công việc có hiệu quả thì các bạn nên có 1 oscilloscope hay ít nhất 1 volt kế đủ nhạy cũng như hãy cài thời gian nghỉ lâu hơn và có thể dùng 1 loop các lệnh xuất nhập để có thể đo đạc kiểm nghiệm các chân I/O trên các connector (đầu cắm) dể hơn

Trong mọi tình huống nếu bạn muốn biết nhiều chi tiết hơn về 1 hàm thì có thể gõ vào máy mệnh lệnh "man <tên_hàm>"

Baì viết dưạ theo dàn ý chung cuả Riku Saikkonen  tưạ đề "Linux Port Programming mini-HOWTO" và được người dịch ghép thêm các nguồn mở khác để diển giải cho rõ ý.

1. Dẫn nhập và bản quyền:

Hồ sơ này mô tả việc thảo chương trên các cổng xuất nhập và cách tạo nên 1 thời gian nghĩ (tương đương với lệnh delay hay sleep trong một số ngôn ngữ)  cho chương trình trong chế độ xử dụng cho hệ thống Intel 0x86

Vì đây là hồ sơ Linux (mở) nên bạn có thể xử dụng nó miễn phí. Các tác giả giữ bản quyền

 Đọc bản quyền

2. Cách dùng cổng các xuất nhập (I/O ports) trong C:

2.1 Phương pháp thông thường:

Các thủ tục để dùng cổng I/O là các được tạo sẵn bởi Linux. Do vậy bạn chỉ việc khai báo lệnh "#include <asm/io.h>" trong chương trình là đủ (không cần phải khai báo thêm bất kì tệp thư viện nào hết!)

Vì sự giới hạn cuả trình dịch gcc nên bạn phải mở chế độ  ưu khi chuyển dịch các mã nguồn (nghiã là dùng lệnh "gcc -O1 ..." hay cao hơn --O2, O3,..). Thay vì phải dịch kiểu như vậy, bạn cũng có thể thêm một khai báo "#define extern static" ngay đằng trước cuả câu khai báo "#include <asm/io.h>" (dĩ nhiên là đừng quên câu lệnh "#undef extern" sau đó)

Sự cho phép:

Trước khi có bất kì mệnh lệnh nào tới I/O thì bắt buộc bạn phải xin phép. Trong Linux thì dể xin phép "đụng tới" phần cứng chúng ta dùng hàm ioperm(điạ_chỉ, "độ_rộng_cổng","mở_tắt" ). Lệnh này đòi hỏi thêm khai báo "#include <unistd.h>" ở phần khia báo cuả chương trình.  Thí dụ như câu lệnh "ioperm(0x300,5,1)"  sẽ cho phép mở 5 cổng có điạ chỉ từ 0x300 cho đến 0x304. còn câu lệnh "ioperm(0x300,5,0)"  sẽ đóng các cổng đó lại.  Để biết thêm chi tiết về hàm ioperm naỳ thì trong LInux hãy gõ lệnh "man ioperm".

Hàm ioperm chỉ cho phép sử dụng các điạ chỉ từ 0x000 đến 0x3ff cho các cổng có địa chỉ lớn hơn bạn có thể dùng hàm "iopl" Dùng cấp độ "iopl(3)" sẽ cho phép bạn làm việc với tất cả các cổng nhưng hày cẩn thận vì nếu bạn dùng không đúng điạ chỉ, bạn có thể làm cho máy bị hư hỏng. Gõ câu lệnh "man iopl 2" đê biết thêm chi tiết sử dụng hàm này

Cả hai hàm ioperm()iopl() đều đòi hỏi bạn phải có quyền ưu tiên "ROOT" để xài (như vậy nếu bạn chỉ là user thì không đuợc trừ khi bạn đổi quyền sang root (Trong Linux, user "root" thì tương đương vớ user "admin" trong Windows)

Xử dụng các cổng:

Để đọc (nhập) 1 byte (8 bit) từ  một cổng, hãy dùng lệnh "inb(điạ_chỉ_cổng)".

Để xuất (gửi ra) 1 byte dùng lệnh "outb(giá_tri_gửi_đi, điạ_chỉ_cổng)". 

 Để đọc 1 word (16 bit) từ hai cổng có điạ chỉ là x và x+1 thì gọi "inw(x)"

 và để xuất 1 word đến hai cổng có diạ chỉ lần lượt là x và x+1 thì xài "outw(giá_trị_gửi_đi, x)".

Nếu bạn không biết rõ tính năng cuả cổng thì tốt nhất là chỉ dùng hàm outb()inb(). Cũng nên nhớ rằng để mỗi lệnh được thi hành phải có ít nhất vài micro giây.

Các mệnh lệnh khác như là  inb_p(),outb_p(),inw_p(), và outw_p() vận hành giống hệt như các mệnh lệnh ở trên nhưng sẽ chậm hơn chừng 1 mili-giây.

Do đó, bạn có thể thêm vào khai báo
"
#define REALLY_SLOW_IO" trước khai báo "#include <asm/io.h>".

Hơn nưã bạn còn cần dùng thêm lệnh ioperm() cho cổng 0x80 vì các mệnh lệnh này cần có cổng 0x80 để hoạt động.

2.2 Phương pháp khác: dùng /dev/port

Ta cũng có thể dùng hàm open mà mở tệp /dev/port (đây là một "character device", số major là 1, và minor là 4) cho việc đọc vào hay viết ra (lưu ý nên tránh dùng hàm stdio f*() cho việc này!). Hãy dùng lseek(pos) để chuyển đến vị trí đúng cuả cổng (nghiã là vị trí pos= 0 -> cổng 0x00, vị trí pos=1 -> cổng 0x01, nói chung vi trí pos chính là điạ chỉ cuả cổng mà bạn cần làm việc). Sau đó, từ vị trí này, dùng các hàm read() hay write() để xuất/nhập byte (hoặc là word) lên vị trí cổng đó

Vì dùng hàm read()write() nên phưong pháp này có lẽ hơi chậm hơn cách đầu tiên, nhưng có điều tiện lợi là bạn sẽ không phải dịch với chế độ tối ưu (nghĩa là không cần thêm -O1 khi dùng gcc) và cũng không cần dùng hàm ioperm() để xin phép. Cái không tốt cuả việc này là nó có thể xử dụng phần cứng trục tiếp và do đó có thể gây tổn thương cho hệ thống. Bạn phải rất cẩn thận khi dùng

3. IRQs và DMA access

Nếu muốn "đụng" đến Interrupt (IRQ) và DMA  thì cần phải biết viết kernel driver.  Bạn có thể tìm thấy nhiiều tài liệu trên Internet chỉ cách viết kernel driver băng cách dùng http://google.com và gõ từ khoá chẳng hạn như: "Linux device driver Howto". Hay là thu hẹp hơn thị truờng tìm kiếm bằng từ khoá "Linux kernel Hacker Guide" dể có các hướng dẫn và các mã ví dụ

4. Định Thời với sự chính xác cao:

4.1 Trì Hoãn: đôi khi trong khi "nói chuyện" qua các cổng chúng ta cần áp dụng các trì hoản để đợi cho các thiết bị phần cứng có đủ thì giờ "nhận lệnh, thì hành lệnh và trả lời"

Bạn sẽ không có một sự bảo đảm nào về tính chính xác khi chi phối thời gian cho chương trình bởi vì bản chất tự nhiên cuả môi trường multitasking trong Linux.  Mỗi process (quá trình xử lí) có thể bị trể từ 10 mili giây cho đến vài giây (trên 1 hệ thống tải nhiều).  Tuy vậy, thường đối với hầu hết các ứng dụng trên I/O ports diều này không mấy quan trọng. Để giảm sự sai sót này bạn có thể dùng lệnh nice (hày gõ "man nice 2" dể biết thêm chi tiết)

Còn nếu muốn chính xác hơn nưã, bạn có thể dùng chế độ "real time" (cho Linux kernel  2.x trở lên). Hãy dùng lệnh "man sched_setscheduler" để xem thêm chi tiết

Lệnh sleep() và usleep()

Để ngừng chương trình không chạy trong hàng giây thì tốt nhất là dùng hàm sleep(thời_lựơng_giây). Để ngưng chạy trong ít nhất 10 mili giây thì hãy dùng usleep(Thời_lượng_micro_giây)

Lệnh nanosleep()

Với kernel 2.x cho phép ngưng chạy trong 1 thời gian rất ngắn (vài micor giây hay hơn). Sau đây là ví dụ mà chúng tôi đã dùng để chạy hàm nanosleep():

// the program "asleep.c" is only to put itself in sleep mode for 0.05 of a second
#include <errno.h>
#include <time.h>
#include <math.h>
#include <stdio.h>

int asleep(double sleep_time){

struct timespec tv;
tv.tv_sec = (time_t) sleep_time;

tv.tv_nsec = (long) ((sleep_time - tv.tv_sec) * 1e+9);
while (1)
{
 

int rval = nanosleep(&tv, &tv);
if (rval) return 0;
else if (errno == EINTR) continue;
else return rval;

}
return 0;

}

main (int argc, char **argv){

double sleep = 0.05;
asleep(sleep);
return 0;

}

Dùng lệnh gcc  -o asleep asleep.c dể dịch tệp "asleep.c" thành lệnh "asleep".  Bạn có thể thay vì gán cho biến "double sleep" giá trị thì có thể đọc thẳng từ commandline

Trì hoản dùng I/O port:

 Một cách khác để trì hoản vài micro giây là dùng cổng xuất nhập. Nhập hay xuất một byte bất kì từ cổ 0x80 thì sẽ làm máy ngưng gần như 1 đúng micro giây tuỳ theo vận tốc máy và kiểu CPU.  Làm như thế nhiều lần để trì hoản vài micro giây. Sự xuất ra cổng 0x80 sẽ không gây tổn hại cho bấtt kì  máy thông thường nào

đúng ra, các lệnh xuất nhập trên hầu hết các cổng có điạ chỉ 0 đến 0x3ff thì chiếm hầu như dúng 1 micro giây, chẳng hạn khi đang dùng parallel port trực tiếp thì chỉ cần thêm vào inb() các cổng để trì hoản

Trì hoản với các mệnh lệnh Assembler:

Nếu như bạn biết rõ kiểu và vận tốc của máy bạn có thể mã "chờ" trong 1 thời gian cực ngắn bằng các lệnh cuả Assembly. Bạn nên có 1 cuốn sách xuất bản cuả hãng Intel (hay AMD) viết về con CPU mà bạn đang có (cũng có thể tìm ra các tin tức này qua google nhưng tôi không dám chắc) để biết rằng mỗi câu lệnh trong Assembly sẽ lấy bao nhiêu chư kì cuả máy và tuỳ theo vận tốc máy mà tính ra thời gian trì hoãn tạo nên bởi lệnh đó. Tuy nhiên cùng cần chú ý các con chip thế hệ mới (lai với kiểu RISC như Pentinum chẳng hạn) thì trong 1 chu ki máy có thể làm nhiều hơn 1 lệnh và do đó sự "thực thi" 1 mệnh lệnh trong Assembly lại càng rút ngắn hơn.

4.2 Đo đạc thời gian:

Nếu đo thời gian với độ chính xác là giây thì có thể dùng hàm time(). Để có sự chính xác nhiều hơn thì dùng hàm gettimeofday() đúng đến micro giây (dĩ nhiên ở đây cũng bị sai số do chế độ multitasking gây ra!)

Nếu muốn cái process cuả bạn nhận tín hiệu sau một thời lượng, hày dùng setitimer() hay dùng alarm().

5. Các Ngôn ngữ thảo chương khác:

Những mô tả trên tập trung vào ngôn ngữ C. Bạn có thể áp dụng trực tiếp nó cho C++. Trong Assembler, bạn phải gọi ioperm() hoặc iopl() như là trong C, nhưng sau đó bạn có thể dùng các lệnh đọc/viết lên các cổng xuất nhập

Trong các ngôn ngữ khác, trừ khi bạn có thể dùng inline assembler hay mã C vào trong chương trình hoặc xử dụng các lệnh hệ thống (system calls) nhắc đến ở trên. Cách dễ nhất là viết ra một mã nguồn đơn giản trong C trong đó bạn cung cấp các hàm xuất nhập với các cổng va link nó vào phần còn lại cuả chương trình. Hoặc là dùng /dev/port như mô tả ở trên

6. Một số cổng hữu dụng:

6.1 Cổng song song: cổng parallel có điạ chỉ cơ sở (BASE)  là 0x3bc cho cổng /dev/lp0, 0x378 cho cổng /dev/lp1, và 0x278 cho cổng /dev/lp2

Hình 1: các dữ liệu phần cứng cuả cổng song song

Điạ chỉ cơ sở BASE+0 điều khiển các tín hiệu dữ liệu cuả cổng này (D0 đến D7 cho các bit (giá trị) 0 tới 7). Một cách tương ứng 0= trạng thái thấp (0 V), và 1= trạng thái cao (5 V). một lệnh "viết" trên cổng sẽ cài dữ liệu lên các chân này. một lệnh "đọc" trên cổng sẽ cho ra dữ liệu mà lần lệnh "viết" cuối cùng đã xuất ra (bằng kiểu chuẩn hay kiểu mở rộng), hoặc cho ra dữ liệu trong các chân từ một thiết bị khác trong kiểu "đọc" mở rộng -- chúng ta sẽ dùng rất nhiều kiểu đọc/viết này để liên lạc máy tính với các thiết bị bên ngoài.

Điạ chỉ cơ sở BASE+1 thì chỉ dùng cho đọc trạng thái cuả các tín hiệu nhập vào như sau:

  • Bits 0 và 1 thì được để dành không xài

  • bit 2 là trạng thái cuả IRQ không phải cuả chân

  • bit 3 chỉ lỗi

  • bit 4 SLCT  -- cổng select

  • bit 5 PE

  • bit 6 ACK

  • bit 7 BUSY

Ngoại trừ bit 7 BUSY, tất cả các bit khác đều chọn 1= điện thế cao

Điạ chỉ cơ sở BASE+2 thì dùng cho việc "xuất" (viết ra cổng) và điều khiển những tín hiệu trạng thái sau đây:

·        Bit 0 -STROBE (0= cao)

·        Bit 1 -AUTO_FD_XT (0=cao)

·        Bit 2 INIT (1=cao)

·        Bit 3 -SLCT_IN (0=cao)

·        Bit 4 Cho phép IRQ cuả cổng (mà xảy ra trong trạng thái  nảy  chuyển trạng thái thấp tới cao cuả ACK) khi cài giá trị là 1.

·        Bit 5 điều khiển chìều (xuất hay nhập) cuả kiểu mở rộng (0 = viết, 1 = đọc), và hoàn toàn chỉ dùng cho xuất.

·        Bits 6 7 chưa xài tới.

Cấu hình các chân ra  ( 25-chân giống cái  đầu cắm D-shell ) (i=nhập, o=xuất):

1io -STROBE, 2io D0, 3io D1, 4io D2, 5io D3, 6io D4, 7io D5, 8io D6,

9io D7, 10i ACK, 11i -BUSY, 12i PE, 13i SLCT, 14o -AUTO_FD_XT,

15i ERROR, 16o INIT, 17o -SLCT_IN, 18-25 Ground

Theo chi tiết kĩ thuật thiết kế cuả IBM thì trên các chân 1, 14, 16, và 17 có thể dùng thế 5 V qua diện trở 4.7 K Ohm. Hày đọc thêm trang  http://www.hut.fi/Misc/Electronics/circuits/lptpower.html để biết rõ là cổng song song có thể như là nguồn cung cấp điện năng tối đa là bao nhiêu cho mồi chân. Để tiện dùng, xin bạn xem qua bảng trích sau đây:

Bảng 1: Dòng điện tối đa có thể chạy qua các chân:
 

Normal 

UM82C11-C

EEE 1284 level II

Data output (>2.4V)

2.6 mA

2 mA

14 mA

Data line sink (<0.4V)

24 mA

24 mA

14 mA

Control output (>2.4 V)

0.5 mA*

1.5 mA

??

Signal lines (short circuit)

1 mA

??

??

Control line sink  (<0.4V)

7 mA

7 mA

14 mA

Lưu ý quan trọng: Thật sự cẩn thận với việc nối đất (mass). Tôi (nguyên tác giả) đã làm hỏng nhiều  cổng song song vì đã nối chúng trong khi computer đang hoạt động. (có nghiã là hày tắt máy trước khi nối kết cách mạch điện -- điều này không chỉ đúng cho cổng song song mà còn cho nhiều cổng thông thường khác-- người dịch) Thật sẽ là không hay ho nếu cái cổng này lại được "hàn chết" với bo mẹ cuả máy. Bạn có thể kiếm ra 1 cổng song song thứ nhì (hay các cổng khác)  bằng cách mua 1 một "multi I/O card" rẻ tiền. chỉ cần tắt tất cả các cổng song song mà bạn không dùng tới (bằng cài đặt BIOS -- người dịch) và cài các điạ chỉ cuả các cổng song song cho cái card mới mua. Và bạn cùng chẳng cần lo về các IRQ cho cổng này vì bạn sẽ không sử dụng đến

6.2 Cổng Joystick

Các điạ chỉ cuả cổng Joystick (game) là 0x200-0x207.  Nếu bạn muốn điều khiển các cổng joystick bạn nên dùng cái driver cung cấp bởi Linux kernel

Chân ra (một đầu cắm giống cái gồm 15 chân) cuả cổng:

  • chân 1, 8, 9, 15: +5 V

  • chân 4, 5, 12: dây đất

  • chân 2, 7, 10, 14: nhập dữ liệu số BA1, BA2, BB1, BB2 một cách tương ứng

  • chân 3, 6, 11, 13: nhập dữ liệu tương tự AX, AY, BX, BY một cách tưong ứng

Các chân +5V thường được nối thẳng lên nguồn cấp điện cuả bo mẹ, chúng có thể dùng như nguồn cấp điện tuỳ theo bo mẹ, nguồn cấp điện va cổng chơi

Pin No.

Signal Name
Regular Port

Signal Name
Midi Enabled Port

1

+ 5 Vdc

+ 5 Vdc

2

Joystick A, Button 1

Joystick A, Button 1

3

Joystick A, X Axis

Joystick A, X Axis

4

Ground

Ground

5

Ground

Ground

6

Joystick A, Y Axis

Joystick A, Y Axis

7

Joystick A, Button 2

Joystick A, Button 2

8

+ 5 Vdc

+ 5 Vdc

9

+ 5 Vdc

+ 5 Vdc

10

Joystick B, Button 1

Joystick B, Button 1

11

Joystick B, X Axis

Joystick B, X Axis

12

Ground

MIDI RxD

13

Joystick B, Y Axis

Joystick B, Y Axis

14

Joystick B, Button 2

Joystick B, Button 2

15

+ 5 Vdc

MIDI TxD

Những cổng nhập số (digital inputs) dùng cho các nút cuả hai joystick (joystick A và B mồi cái có 2 nút) mà bạn có thể nối vào cổng. Chúng thường là các cổng nhập TTL,  và bạn có thể đọc trạng thái cuả chúng trực tiếp từ các cổng trạng thái (xem sau đây). Một joystick thực trả về trạng thái thấp (0 V) khi nút được nhấn và trạng thái cao trong trưòng hợp còn lại (điện thế 5 V từ năng lượng cuả các chân khi tải qua 1 điện trở 1 K Ohm)

Các cổng nhập tuơng tự thực ra chúng đo điện trở. Cổng joystick có 4 "one-shot multivibrator" (tạm dịch đơn phát đa giao động kế) -- đó là con chíp 558 nối với 4 ngỏ nhập. Trong mỗi ngõ nhập, có một điện trở 2,2K nối giưã chân vào và chân ra cuả multivibrator

 Joystick
   Pot in  >---+
  (stick n)    |
               \
       R1      /        ____________
       2.2K    \       |            |
               /       |            |>----------> data bit to ISA bus
               |       | Monostable |out
               +-------|   Multi-   |
               |       |  vibrator  |
       C1     ---      |            |<----------< write to I/O port strobe
       22 nF  ---      |____________|trigger
               |
               |
              ---
 

Để đọc tín hiệu từ ngõ vào tương tự cuả joystick, bạn phải khởi động multivibrator (với lệnh viết ra cổng -- xem dưới đây); sau đó "kiểm tra" các trạng thái cuả 4 cái trục (bằng sự lập lại cuả các lệnh đọc) cho tới khi trạng thái dọc bị rơi từ cao sang thấp, đo thời gian cuả chu kì ở mức cao. Cách đếm này lấy rất nhiều thời gian cuả CPU và kết quả cho người dùng Linux thông thường sẽ không chính xác lắm. Nếu biết rằng tín hiệu sẽ lấy 1 thời gian dài (hàng chục mili giây) trước khi hạ thấp xuống thì hày dùng lệnh usleep() trước khi kiểm tra các trạng thái để dành thêm thì giờ cho CPU làm việc khác.

Điạ chỉ duy nhất cho xuất nhập là 0x201. mọi lẹnh viết sang cổng naỳ sẽ khởi dộng multivibrator. Mỗi lệnh đọc từ cổng này sẽ trả về trạng thái cuả tín hiệu nhập

·        Bit 0: AX (trạng thái (1=cao) cuả multivibrator xuất)

·        Bit 1: AY (trạng thái (1=cao) cuả multivibrator xuất)

·        Bit 2: BX (trạng thái (1=cao) cuả multivibrator xuất)

·        Bit 3: BY (trạng thái (1=cao) cuả multivibrator xuất)

·        Bit 4: BA1 (nhập kiểu số, 1=cao)

·        Bit 5: BA2 (nhập kiểu số, 1=cao)

·        Bit 6: BB1 (nhập kiểu số, 1=cao)

·        Bit 7: BB2 (nhập kiểu số, 1=cao)

Hình2: sơ đồ ví dụ đơn giản nối cổng joystick ra ngoài

6.3 Cổng Nối tiếp:

Giao diện cổng serial (RS232): Chân ra và Tín Hiệu

9
chân #

25
chân#

Viết tắt

Tên đầy đủ

Hướng

Ý nghiã

3
2
TxD

xuất dữ liệu

—»

Gửi  bytes ra ngoài  PC

2
3
RxD

nhập dữ liệu

«—

nhận bytes vào PC

7
4
RTS

Request To Send

—»

RTS/CTS dòng điều khiển

8
5
CTS

Clear To Send

«—

RTS/CTS dòng điều khiển

6
6
DSR

Data Set Ready

«—

sẳn sàng để liên lạc

4
20
DTR

Data Terminal Ready

—»

sẳn sàng để liên lạc

1
8
DCD

Data Carrier Detect

«—

Modem đã nối với thiết bị khác

9
22
RI

Ring Indicator

«—

đường dây Telephone đang reo

5
7
SG

Signal Ground

   

Chú ý: DCD đôi khi được viết là CD

Cổng serial có rất nhiều dạng đầu cắm khác nhau bạn nên vào trang http://www.easysw.com/~mike/serial/serial.html để đọc thêm chi tiết về việc dùng cổng nối tiếp trong thảo chương.  Hãy dùng lệnh "man termios" và nếu có thể đọc thêm mã nguồn cuả bộ lái để xem chi tiết về việc cài đặt và xuất nhập dữ liêu với cổng serial trong /usr/src/linux/drivers/char/serial.c

Các ví dụ code sau đây giả sử rằng bạn là "root" hay bạn đã xin phép dùng cổng này (xem lại về sự cho phép ). Trước tiên, bạn sẽ phải "mở" cổng rồi mới có thể xuất nhập dữ liệu và sau khi dùng thì "đóng" chúng lại

mở một cổng serial.

    #include <stdio.h>   /* Standard input/output definitions */
    #include <string.h>  /* String function definitions */
    #include <unistd.h>  /* UNIX standard function definitions */
    #include <fcntl.h>   /* File control definitions */
    #include <errno.h>   /* Error number definitions */
    #include <termios.h> /* POSIX terminal control definitions */

    /*
     * 'open_port()' - Open serial port 1.
     *
     * Returns the file descriptor on success or -1 on error.
     */

    int
    open_port(void)
    {
      int fd; /* File descriptor for the port */


      fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY);
      if (fd == -1)
      {
       /*
	* Could not open the port.
	*/

	perror("open_port: Unable to open /dev/ttyS0 - ");
      }
      else
	fcntl(fd, F_SETFL, 0);

      return (fd);
    }

Các hệ thống khác nhau sẽ có thể phải được ghi đúng thiết bị (phần cứng cuả cổng nối tiếp) và trong Linux cổng nối tiếp đầu tiên cuả máy sẽ có tên là /dev/ttys0 các cổng kế tiếp sẽ có tên /dev/ttys2 , /dev/ttys3,..... Tất cả các phần còn lại cuả nguồn không đổi (ở đây dùng cổng đầu tiên ttys0)

Đoạn mã để xuất dữ liệu::

    n = write(fd, "ATZ\r", 4);
    if (n < 0)
      fputs("write() of 4 bytes failed!\n", stderr);
Hàm write sẽ trả về giá trị -1 nếu có lỗi.

Nhập dữ liệu: hãy dùng lệnh read như bình thường. Tuy nhiên, sẽ có vài lưu ý như sau:

Lệnh read sẽ trả về tất cả các character mà nó có trong bộ đệm nhập(serial input buffer). Nếu không có dữ liệu nào trong đó thì nó sẽ đợi cho đến khi có 1 character xuất hiện hay nếu quá thì hạn sẽ trả về lỗi. Để khắc phục, hàm read có thể nhập dữ liệu về lập tức bằng cách sau đây:

fcntl(fd, F_SETFL, FNDELAY);

hằng số FNDELAY làm cho hàm read trả về 0 ngay lập tức nếu không có dữ liệu trong bộ đệm. Để trả lại trạng thái cũ (bình thường) cuả cổng hãy dùng lệnh:

fcntl(fd, F_SETFL, 0);

Đóng cổng: dùng lệnh

:    close(fd);

7. Các Hướng Dẫn:

Nếu muốn có những thiết bị xuất nhập tốt kiểu tương tự, bạn có thể nối dây từ các con chip ADC và/hoặc là DAC với cổng parallel. Bạn có thể dùng đầu nối cuả cổng joystick (các chân 1,8, và 9) hay là ngay cả 1 đầu dây cắm dư ra trong máy (thường dùng để nối với các ổ cứng hay các ổ CD ROM) để làm nguồn cấp điện 5V

Với các thiết bị kiểu tương tự chính xác, cách nối không đúng sẽ tạo nên nhiều lỗi trong xuất và nhập dữ liệu. Trong trường hợp này bạn nên dùng thêm các optocouplers (thiết bị bao gồm các LED va photodiode nhằm cô lập diện trở giưã ngỏ ra và ngỏ vào). Hãy lấy nguồn cấp điện cuả computer (qua các đầu nối chưa xài) để cấp điện cho các optocoupler

Nếu bạn muốn làm các mạch in thì đã có một ứng dụng miễn phí gọi là Pcb trên Xwindow cuả Linux. Nếu Linux trên máy cuả bạn chưa có thi có thể tải về ứng dụng này qua điạ chỉ http://sunsite.unc.edu/pub/Linux/apps/circuits  Các tệp nên tải về sẽ có tiếp đầu ngữ là pcb-*

8. Hỏi Đáp dò tìm các "Pan"

H1: Tôi bị lỗi "segmentation fault" khi dùng các cổng

Đ1: Bạn có th không phải là "root" khi chạy máy, hoặc là lệnh ioperm() bị bác vì lí do nào đó. HÀy xem giá trị trả về cuả hàm ioperm(). Nếu dùng các lệnh inb_p(), outb_p(),... hày nhớ gọi thêm ioperm để xin phép xài cổng 0x80 nưã.

 

H2: Không tìm ra các hàm in*() hay out*()gcc báo lỗi "undefined references"

Đ2: Bạn đã quên dùng thêm tham số (-O) khi dùng gcc hoặc đã thiếu dòng #include <asm/io.h> ở đầu  chương trình

 

H3: out*() chẳng có làm gì hết hay là nó có nhng kết quả "kì cục"

Đ3: Kiểm lại các thứ tự cuả tham số; nó nên là outb(giá_trị, điạ_chỉ_cổng) đừng dùng hàm outportb(điạ_chỉ, giá_trị) như trong nhiều chương trình DOS

 

9. Thí dụ mã nguồn

Đây là một mã nguồn để dùng với cổng song song đơn giản. Hãy chỉnh lại theo đồ án riêng cuả bạn mà xài. Khi dich có thể dùng lệnh "gcc -O1 example.c -o example" . Tệp mệnh lệnh ra sẽ mang tên "example"

 

/*

 * example.c: very simple example of port I/O

 *

 * This code does nothing useful, just a port write, a pause,

 * and a port read. Compile with `gcc -O2 -o example example.c',

 * and run as root with `./example'.

 */

 

#include <stdio.h>

#include <unistd.h>

#include <asm/io.h>

 

#define BASEPORT 0x378 /* lp1 */

 

int main()

{

  /* Get access to the ports */

  if (ioperm(BASEPORT, 3, 1)) {perror("ioperm"); exit(1);}

 

  /* Set the data signals (D0-7) of the port to all low (0) */

  outb(0, BASEPORT);

 

  /* Sleep for a while (100 ms) */

  usleep(100000);

 

  /* Read from the status port (BASE+1) and display the result */

  printf("status: %d\n", inb(BASEPORT + 1));

 

  /* We don't need the ports anymore */

  if (ioperm(BASEPORT, 3, 0)) {perror("ioperm"); exit(1);}

 

  exit(0);

}

 

/* end of example.c */

 

 

Tài kiệu tham khảo: Đa phần các thông tin trong bài được lấy từ các nguồn sau đây:

http://www.beyondlogic.org/epp/epp.htm

http://www.hut.fi/Misc/Electronics/circuits/lptpower.html

http://www.ctips.com/game.html

http://www.epanorama.net/documents/joystick/index.html

http://www.easysw.com/~mike/serial/serial.html


1. Introduction

This HOWTO document describes programming hardware I/O ports and waiting for small periods of time in user-mode Linux programs running on the Intel x86 architecture. This document is a descendant of the very small IO-Port mini-HOWTO by the same author.

This document is Copyright 1995-2000 Riku Saikkonen. See the Linux HOWTO http://sunsite.unc.edu/pub/Linux/docs/HOWTO/COPYRIGHT

  for details.

If you have corrections or something to add, feel free to e-mail me (Riku.Saikkonen@hut.fi)...

2. Using I/O ports in C programs

2.1 The normal method

Routines for accessing I/O ports are in /usr/include/asm/io.h (or linux/include/asm-i386/io.h in the kernel source distribution). The routines there are inline macros, so it is enough to #include <asm/io.h>; you do not need any additional libraries.

Because of a limitation in gcc (present in all versions I know of, including egcs), you have to compile any source code that uses these routines with optimisation turned on (gcc -O1 or higher), or alternatively use #define extern static before you #include <asm/io.h> (remember to #undef externafterwards).

For debugging, you can use gcc -g -O (at least with modern versions of gcc), though optimisation can sometimes make the debugger behave a bit strangely. If this bothers you, put the routines that use I/O port access in a separate source file and compile only that with optimisation turned on.

Permissions

Before you access any ports, you must give your program permission to do so. This is done by calling the ioperm() function (declared in unistd.h, and defined in the kernel) somewhere near the start of your program (before any I/O port accesses). The syntax is ioperm(from, num, turn_on), where from is the first port number to give access to, and num the number of consecutive ports to give access to. For example, ioperm(0x300, 5, 1) would give access to ports 0x300 through 0x304 (a total of 5 ports). The last argument is a Boolean value specifying whether to give access to the program to the ports (true (1)) or to remove access (false (0)). You can call ioperm() multiple times to enable multiple non-consecutive ports. See the ioperm(2) manual page for details on the syntax.

The ioperm() call requires your program to have root privileges; thus you need to either run it as the root user, or make it setuid root. You can drop the root privileges after you have called ioperm() to enable the ports you want to use. You are not required to explicitly drop your port access privileges with ioperm(..., 0) at the end of your program; this is done automatically as the process exits.

A setuid() to a non-root user does not disable the port access granted by ioperm(), but a fork() does (the child process does not get access, but the parent retains it).

ioperm() can only give access to ports 0x000 through 0x3ff; for higher ports, you need to use iopl() (which gives you access to all ports at once). Use the level argument 3 (i.e., iopl(3)) to give your program access to all I/O ports (so be careful --- accessing the wrong ports can do all sorts of nasty things to your computer). Again, you need root privileges to call iopl(). See the iopl(2) manual page for details.

Accessing the ports

To input a byte (8 bits) from a port, call inb(port), it returns the byte it got. To output a byte, call outb(value, port) (please note the order of the parameters). To input a word (16 bits) from ports x and x+1 (one byte from each to form the word, using the assembler instruction inw), call inw(x). To output a word to the two ports, use outw(value, x). If you're unsure of which port instructions (byte or word) to use, you probably want inb() and outb() --- most devices are designed for bytewise port access. Note that all port access instructions take at least about a microsecond to execute.

The inb_p(), outb_p(), inw_p(), and outw_p() macros work otherwise identically to the ones above, but they do an additional short (about one microsecond) delay after the port access; you can make the delay about four microseconds with #define REALLY_SLOW_IO before you #include <asm/io.h>. These macros normally (unless you #define SLOW_IO_BY_JUMPING, which is probably less accurate) use a port output to port 0x80 for their delay, so you need to give access to port 0x80 with ioperm() first (outputs to port 0x80 should not affect any part of the system). For more versatile methods of delaying, read on.

There are manual pages for ioperm(2), iopl(2), and the above macros in reasonably recent releases of the Linux manual page collection.

2.2 An alternate method: /dev/port

Another way to access I/O ports is to open() /dev/port (a character device, major number 1, minor 4) for reading and/or writing (the stdio f*() functions have internal buffering, so avoid them). Then lseek() to the appropriate byte in the file (file position 0 = port 0x00, file position 1 = port 0x01, and so on), and read() or write() a byte or word from or to it.

Naturally, for this to work your program needs read/write access to /dev/port. This method is probably slower than the normal method above, but does not need compiler optimisation nor ioperm(). It doesn't need root access either, if you give a non-root user or group access to /dev/port --- but this is a very bad thing to do in terms of system security, since it is possible to hurt the system, perhaps even gain root access, by using /dev/port to access hard disks, network cards, etc. directly.

You cannot use select(2) or poll(2) to read /dev/port, because the hardware does not have a facility for notifying the CPU when a value in an input port changes.

3. Interrupts (IRQs) and DMA access

You cannot use IRQs or DMA directly from a user-mode process. You need to write a kernel driver; see The Linux Kernel Hacker's Guide <http://www.redhat.com:8080/HyperNews/get/khg.html> for details and the kernel source code for examples.

You can disable interrupts from within a user-mode program, though it can be dangerous (even kernel drivers do it for as short a time as possible). After calling iopl(3), you can disable interrupts simply by calling asm("cli");, and re-enable them with asm("sti");.

4. High-resolution timing

4.1 Delays

First of all, I should say that you cannot guarantee user-mode processes to have exact control of timing because of the multi-tasking nature of Linux. Your process might be scheduled out at any time for anything from about 10 milliseconds to a few seconds (on a system with very high load). However, for most applications using I/O ports, this does not really matter. To minimise this, you may want to nice your process to a high-priority value (see the nice(2) manual page) or use real-time scheduling (see below).

If you want more precise timing than normal user-mode processes give you, there are some provisions for user-mode `real time' support. Linux 2.x kernels have soft real time support; see the manual page for sched_setscheduler(2) for details. There is a special kernel that supports hard real time; see <http://luz.cs.nmt.edu/~rtlinux/> for more information on this.

Sleeping: sleep() and usleep()

Now, let me start with the easier timing calls. For delays of multiple seconds, your best bet is probably to use sleep(). For delays of at least tens of milliseconds (about 10 ms seems to be the minimum delay), usleep() should work. These functions give the CPU to other processes (``sleep''), so CPU time isn't wasted. See the manual pages sleep(3) and usleep(3) for details.

For delays of under about 50 milliseconds (depending on the speed of your processor and machine, and the system load), giving up the CPU takes too much time, because the Linux scheduler (for the x86 architecture) usually takes at least about 10-30 milliseconds before it returns control to your process. Due to this, in small delays, usleep(3) usually delays somewhat more than the amount that you specify in the parameters, and at least about 10 ms.

nanosleep()

In the 2.0.x series of Linux kernels, there is a new system call, nanosleep() (see the nanosleep(2) manual page), that allows you to sleep or delay for short times (a few microseconds or more).

For delays <= 2 ms, if (and only if) your process is set to soft real time scheduling (using sched_setscheduler()), nanosleep() uses a busy loop; otherwise it sleeps, just like usleep().

The busy loop uses udelay() (an internal kernel function used by many kernel drivers), and the length of the loop is calculated using the BogoMips value (the speed of this kind of busy loop is one of the things that BogoMips measures accurately). See /usr/include/asm/delay.h) for details on how it works.

Delaying with port I/O

Another way of delaying small numbers of microseconds is port I/O. Inputting or outputting any byte from/to port 0x80 (see above for how to do it) should wait for almost exactly 1 microsecond independent of your processor type and speed. You can do this multiple times to wait a few microseconds. The port output should have no harmful side effects on any standard machine (and some kernel drivers use it). This is how {in|out}[bw]_p() normally do the delay (see asm/io.h).

Actually, a port I/O instruction on most ports in the 0-0x3ff range takes almost exactly 1 microsecond, so if you're, for example, using the parallel port directly, just do additional inb()s from that port to delay.

Delaying with assembler instructions

If you know the processor type and clock speed of the machine the program will be running on, you can hard-code shorter delays by running certain assembler instructions (but remember, your process might be scheduled out at any time, so the delays might well be longer every now and then). For the table below, the internal processor speed determines the number of clock cycles taken; e.g., for a 50 MHz processor (e.g. 486DX-50 or 486DX2-50), one clock cycle takes 1/50000000 seconds (=200 nanoseconds).

Instruction   i386 clock cycles   i486 clock cycles

xchg %bx,%bx          3                   3

nop                   3                   1

or %ax,%ax            2                   1

mov %ax,%ax           2                   1

add %ax,0             2                   1

Clock cycles for Pentiums should be the same as for i486, except that on Pentium Pro/II, add %ax, 0 may take only 1/2 clock cycles. It can sometimes be paired with another instruction (because of out-of-order execution, this need not even be the very next instruction in the instruction stream).

The instructions nop and xchg in the table should have no side effects. The rest may modify the flags register, but this shouldn't matter since gcc should detect it. xchg %bx, %bx is a safe choice for a delay instruction.

To use these, call asm("instruction") in your program. The syntax of the instructions is as in the table above; if you want multiple instructions in a single asm() statement, separate them with semicolons. For example, asm("nop ; nop ; nop ; nop") executes four nop instructions, delaying for four clock cycles on i486 or Pentium processors (or 12 clock cycles on an i386).

asm() is translated into inline assembler code by gcc, so there is no function call overhead.

Shorter delays than one clock cycle are impossible in the Intel x86 architecture.

rdtsc for Pentiums

For Pentiums, you can get the number of clock cycles elapsed since the last reboot with the following C code (which executes the CPU instrution named RDTSC):

 

   extern __inline__ unsigned long long int rdtsc()

   {

     unsigned long long int x;

     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));

     return x;

   }

 

You can poll this value in a busy loop to delay for as many clock cycles as you want.

4.2 Measuring time

For times accurate to one second, it is probably easiest to use time(). For more accurate times, gettimeofday() is accurate to about a microsecond (but see above about scheduling). For Pentiums, the rdtsc code fragment above is accurate to one clock cycle.

If you want your process to get a signal after some amount of time, use setitimer() or alarm(). See the manual pages of the functions for details.

5. Other programming languages

The description above concentrates on the C programming language. It should apply directly to C++ and Objective C. In assembler, you have to call ioperm() or iopl() as in C, but after that you can use the I/O port read/write instructions directly.

In other languages, unless you can insert inline assembler or C code into the program or use the system calls mentioned above, it is probably easiest to write a simple C source file with functions for the I/O port accesses or delays that you need, and compile and link it in with the rest of your program. Or use /dev/port as described above.

6. Some useful ports

Here is some programming information for common ports that can be directly used for general-purpose TTL (or CMOS) logic I/O.

If you want to use these or other common ports for their intended purpose (e.g., to control a normal printer or modem), you should most likely use existing drivers (which are usually included in the kernel) instead of programming the ports directly as this HOWTO describes. This section is intended for those people who want to connect LCD displays, stepper motors, or other custom electronics to a PC's standard ports.

If you want to control a mass-market device like a scanner (that has been on the market for a while), look for an existing Linux driver for it. The Hardware-HOWTO <http://sunsite.unc.edu/pub/Linux/docs/HOWTO/Hardware-HOWTO> is a good place to start.

<http://www.hut.fi/Misc/Electronics/> is a good source for more information on connecting devices to computers (and on electronics in general).

6.1 The parallel port

The parallel port's base address (called ``BASE'' below) is 0x3bc for /dev/lp0, 0x378 for /dev/lp1, and 0x278 for /dev/lp2. If you only want to control something that acts like a normal printer, see the Printing-HOWTO <http://sunsite.unc.edu/pub/Linux/docs/HOWTO/Printing-HOWTO>.

In addition to the standard output-only mode described below, there is an `extended' bidirectional mode in most parallel ports. For information on this and the newer ECP/EPP modes (and the IEEE 1284 standard in general), see <http://www.fapo.com/> and <http://www.senet.com.au/~cpeacock/parallel.htm>. Remember that since you cannot use IRQs or DMA in a user-mode program, you will probably have to write a kernel driver to use ECP/EPP; I think someone is writing such a driver, but I don't know the details.

The port BASE+0 (Data port) controls the data signals of the port (D0 to D7 for bits 0 to 7, respectively; states: 0 = low (0 V), 1 = high (5 V)). A write to this port latches the data on the pins. A read returns the data last written in standard or extended write mode, or the data in the pins from another device in extended read mode.

The port BASE+1 (Status port) is read-only, and returns the state of the following input signals:

·        Bits 0 and 1 are reserved.

·        Bit 2 IRQ status (not a pin, I don't know how this works)

·        Bit 3 ERROR (1=high)

·        Bit 4 SLCT (1=high)

·        Bit 5 PE (1=high)

·        Bit 6 ACK (1=high)

·        Bit 7 -BUSY (0=high)

The port BASE+2 (Control port) is write-only (a read returns the data last written), and controls the following status signals:

·        Bit 0 -STROBE (0=high)

·        Bit 1 -AUTO_FD_XT (0=high)

·        Bit 2 INIT (1=high)

·        Bit 3 -SLCT_IN (0=high)

·        Bit 4 enables the parallel port IRQ (which occurs on the low-to-high transition of ACK) when set to 1.

·        Bit 5 controls the extended mode direction (0 = write, 1 = read), and is completely write-only (a read returns nothing useful for this bit).

·        Bits 6 and 7 are reserved.

Pinout (a 25-pin female D-shell connector on the port) (i=input, o=output):

1io -STROBE, 2io D0, 3io D1, 4io D2, 5io D3, 6io D4, 7io D5, 8io D6,

9io D7, 10i ACK, 11i -BUSY, 12i PE, 13i SLCT, 14o -AUTO_FD_XT,

15i ERROR, 16o INIT, 17o -SLCT_IN, 18-25 Ground

The IBM specifications say that pins 1, 14, 16, and 17 (the control outputs) have open collector drivers pulled to 5 V through 4.7 kiloohm resistors (sink 20 mA, source 0.55 mA, high-level output 5.0 V minus pullup). The rest of the pins sink 24 mA, source 15 mA, and their high-level output is min. 2.4 V. The low state for both is max. 0.5 V. Non-IBM parallel ports probably deviate from this standard. For more information on this, see <http://www.hut.fi/Misc/Electronics/circuits/lptpower.html>.

Finally, a warning: Be careful with grounding. I've broken several parallel ports by connecting to them while the computer is turned on. It might be a good thing to use a parallel port not integrated on the motherboard for things like this. (You can usually get a second parallel port for your machine with a cheap standard `multi-I/O' card; just disable the ports that you don't need, and set the parallel port I/O address on the card to a free address. You don't need to care about the parallel port IRQ if you don't use it.)

6.2 The game (joystick) port

The game port is located at port addresses 0x200-0x207. If you want to control normal joysticks, you're probably better off using the drivers distributed with the Linux kernel.

Pinout (a 15-pin female D-shell connector on the port):

·        1,8,9,15: +5 V (power)

·        4,5,12: Ground

·        2,7,10,14: Digital inputs BA1, BA2, BB1, and BB2, respectively

·        3,6,11,13: ``Analog'' inputs AX, AY, BX, and BY, respectively

The +5 V pins seem to often be connected directly to the power lines in the motherboard, so they may be able to source quite a lot of power, depending on the motherboard, power supply and game port.

The digital inputs are used for the buttons of the two joysticks (joystick A and joystick B, with two buttons each) that you can connect to the port. They should be normal TTL-level inputs, and you can read their status directly from the status port (see below). A real joystick returns a low (0 V) status when the button is pressed and a high (the 5 V from the power pins through an 1 Kohm resistor) status otherwise.

The so-called analog inputs actually measure resistance. The game port has a quad one-shot multivibrator (a 558 chip) connected to the four inputs. In each input, there is a 2.2 Kohm resistor between the input pin and the multivibrator output, and a 0.01 uF timing capacitor between the multivibrator output and the ground. A real joystick has a potentiometer for each axis (X and Y), wired between +5 V and the appropriate input pin (AX or AY for joystick A, or BX or BY for joystick B).

The multivibrator, when activated, sets its output lines high (5 V) and waits for each timing capacitor to reach 3.3 V before lowering the respective output line. Thus the high period duration of the multivibrator is proportional to the resistance of the potentiometer in the joystick (i.e., the position of the joystick in the appropriate axis), as follows:

R = (t - 24.2) / 0.011,

where R is the resistance (ohms) of the potentiometer and t the high period duration (microseconds).

Thus, to read the analog inputs, you first activate the multivibrator (with a port write; see below), then poll the state of the four axes (with repeated port reads) until they drop from high to low state, measuring their high period duration. This polling uses quite a lot of CPU time, and on a non-realtime multitasking system like (normal user-mode) Linux, the result is not very accurate because you cannot poll the port constantly (unless you use a kernel-level driver and disable interrupts while polling, but this wastes even more CPU time). If you know that the signal is going to take a long time (tens of ms) to go down, you can call usleep() before polling to give CPU time to other processes.

The only I/O port you need to access is port 0x201 (the other ports either behave identically or do nothing). Any write to this port (it doesn't matter what you write) activates the multivibrator. A read from this port returns the state of the input signals:

·        Bit 0: AX (status (1=high) of the multivibrator output)

·        Bit 1: AY (status (1=high) of the multivibrator output)

·        Bit 2: BX (status (1=high) of the multivibrator output)

·        Bit 3: BY (status (1=high) of the multivibrator output)

·        Bit 4: BA1 (digital input, 1=high)

·        Bit 5: BA2 (digital input, 1=high)

·        Bit 6: BB1 (digital input, 1=high)

·        Bit 7: BB2 (digital input, 1=high)

6.3 The serial port

If the device you're talking to supports something resembling RS-232, you should be able to use the serial port to talk to it. The Linux serial driver should be enough for almost all applications (you shouldn't have to program the serial port directly, and you'd probably have to write a kernel driver to do it); it is quite versatile, so using non-standard bps rates and so on shouldn't be a problem.

See the termios(3) manual page, the serial driver source code (linux/drivers/char/serial.c), and <http://www.easysw.com/~mike/serial/> for more information on programming serial ports on Unix systems.

 

7. Hints

If you want good analog I/O, you can wire up ADC and/or DAC chips to the parallel port (hint: for power, use the game port connector or a spare disk drive power connector wired to outside the computer case, unless you have a low-power device and can use the parallel port itself for power, or use an external power supply), or buy an AD/DA card (most of the older/slower ones are controlled by I/O ports). Or, if you're satisfied with 1 or 2 channels, inaccuracy, and (probably) bad zeroing, a cheap sound card supported by the Linux sound driver should do (and it's quite fast).

With accurate analog devices, improper grounding may generate errors in the analog inputs or outputs. If you experience something like this, you could try electrically isolating your device from the computer with optocouplers (on all signals between the computer and your device). Try to get power for the optocouplers from the computer (spare signals on the port may give enough power) to achieve better isolation.

If you're looking for printed circuit board design software for Linux, there is a free X11 application called Pcb that should do a nice job, at least if you aren't doing anything very complex. It is included in many Linux distributions, and available in <ftp://sunsite.unc.edu/pub/Linux/apps/circuits/> (filename pcb-*).

8. Troubleshooting

Q1.

I get segmentation faults when accessing ports.

A1.

Either your program does not have root privileges, or the ioperm() call failed for some other reason. Check the return value of ioperm(). Also, check that you're actually accessing the ports that you enabled with ioperm() (see Q3). If you're using the delaying macros (inb_p(), outb_p(), and so on), remember to call ioperm() to get access to port 0x80 too.

Q2.

I can't find the in*(), out*() functions defined anywhere, and gcc complains about undefined references.

A2.

You did not compile with optimisation turned on (-O), and thus gcc could not resolve the macros in asm/io.h. Or you did not #include <asm/io.h> at all.

Q3.

out*() doesn't do anything, or does something weird.

A3.

Check the order of the parameters; it should be outb(value, port), not outportb(port, value) as is common in MS-DOS.

Q4.

I want to control a standard RS-232 device/parallel printer/joystick...

A4.

You're probably better off using existing drivers (in the Linux kernel or an X server or somewhere else) to do it. The drivers are usually quite versatile, so even slightly non-standard devices usually work with them. See the information on standard ports above for pointers to documentation for them.

9. Example code

Here's a piece of simple example code for I/O port access:

 

/*

 * example.c: very simple example of port I/O

 *

 * This code does nothing useful, just a port write, a pause,

 * and a port read. Compile with `gcc -O2 -o example example.c',

 * and run as root with `./example'.

 */

 

#include <stdio.h>

#include <unistd.h>

#include <asm/io.h>

 

#define BASEPORT 0x378 /* lp1 */

 

int main()

{

  /* Get access to the ports */

  if (ioperm(BASEPORT, 3, 1)) {perror("ioperm"); exit(1);}

 

  /* Set the data signals (D0-7) of the port to all low (0) */

  outb(0, BASEPORT);

 

  /* Sleep for a while (100 ms) */

  usleep(100000);

 

  /* Read from the status port (BASE+1) and display the result */

  printf("status: %d\n", inb(BASEPORT + 1));

 

  /* We don't need the ports anymore */

  if (ioperm(BASEPORT, 3, 0)) {perror("ioperm"); exit(1);}

 

  exit(0);

}

 

/* end of example.c */

-----------------------------

How do I find the I/O address of a COM port?

 

 

 Look in the four words beginning at 0040:0000 for COM1 through COM4.

 (The DEBUG command "D 40:0 L8" will do this. Remember that words are

 stored and displayed low byte first, so a word value of 03F8 will be

 displayed as F8 03.) If the value is zero, that COM port is not

 installed (or you've got an old BIOS; see <Q:06.01> [How do I set my

 machine up to use COM3 and COM4?]). If the value is nonzero, it is the

 I/O address of the transmit/receive register for the COM port.

 

 Each COM port occupies eight consecutive I/O addresses (though many

 chips use only the first seven).

 

 Here's some C code to find the I/O address:

 

   unsigned ptSel(unsigned comport)

   {

     unsigned io_addr;

 

     if (comport >= 1  &&  comport <= 4)

     {

       unsigned far *com_addr = (unsigned far *)0x00400000UL;

       io_addr = com_addr[comport-1];

     }

     else

       io_addr = 0;

 

     return io_addr;

   }

 

 You might also want to explore Port Finder, downloadable as:

 <<http://www.simtel.net/pub/pd/47138.html>>

 

 I haven't tried it myself, but a posted article reviewed it very

 favorably and said it also lets you swap ports around.