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() và
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()
và 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() và
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 và 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*() và 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.
|