这是一个相对“古老”但经典的计算机硬件编程主题,对于理解计算机底层I/O操作非常有帮助,在现代计算机上,直接操作物理并口已经变得非常困难,因为它主要存在于服务器、工控设备或通过PCI/ISA扩展卡添加的台式机上,其原理是通用的,并且可以很好地应用于现代的GPIO(通用输入输出)编程。

并口基础知识
我们需要了解并口的基本工作原理,标准的并口(如SPP标准并口)有25个引脚,但我们主要关注数据端口、状态端口和控制端口。
- 数据寄存器:用于向外部设备发送8位数据(D0-D7),这是最主要的数据输出通道。
- 状态寄存器:用于读取外部设备的状态信息,例如打印机是否“在线”、是否“缺纸”、是否“应答”等,这是一个只读寄存器。
- 控制寄存器:用于控制并口的一些输出信号,例如初始化打印机、选择打印机、发送数据选通信号等,这是一个只写寄存器。
在x86架构的PC中,这些寄存器被映射到特定的I/O端口地址上:
| 寄存器 | 功能 | I/O地址 (默认) | 读写属性 |
|---|---|---|---|
| 数据寄存器 | 写入8位数据 / 读取8位数据 | 0x378 |
读写 |
| 状态寄存器 | 读取5位状态信息 | 0x379 |
只读 |
| 控制寄存器 | 写入4位控制信号 | 0x37A |
只写 |
重要提示:这些地址可能会根据计算机的BIOS/CMOS设置和硬件配置而改变,第二个并口(LPT2)的基地址通常是 0x278,在编程前,最好确认你的系统上并口的实际地址。
在C语言中访问I/O端口
标准的C语言(如C89/C99)本身不提供直接访问硬件I/O端口的函数,这需要依赖于特定操作系统的API或编译器提供的特殊扩展。

1 在DOS环境下(最简单)
在DOS时代,访问I/O端口非常直接,可以使用inportb和outportb函数,这些函数通常在<conio.h>或<dos.h>头文件中定义。
#include <stdio.h>
#include <dos.h> // 或 <conio.h>
#define LPT_DATA_PORT 0x378
#define LPT_STATUS_PORT 0x379
#define LPT_CONTROL_PORT 0x37A
// 向指定端口写入一个字节
void outportb(unsigned short port, unsigned char value) {
__asm__ __volatile__ ("outb %0, %1" : : "a" (value), "Nd" (port));
}
// 从指定端口读取一个字节
unsigned char inportb(unsigned short port) {
unsigned char ret;
__asm__ __volatile__ ("inb %1, %0" : "=a" (ret) : "Nd" (port));
return ret;
}
int main() {
// 示例1:向数据端口写入数据 0x55 (二进制 01010101)
outportb(LPT_DATA_PORT, 0x55);
printf("Wrote 0x55 to data port.\n");
// 示例2:从状态端口读取状态
unsigned char status = inportb(LPT_STATUS_PORT);
printf("Read status port: 0x%X\n", status);
// 状态寄存器的位定义 (bit 7-5, bit 4, bit 3, bit 2, bit 1, bit 0)
// 7: Busy (反向), 6: ACK, 5: PE (Paper End), 4: Select In, 3: Error
// 检查打印机是否准备好 (Busy位为0)
if (!(status & 0x80)) {
printf("Printer is ready (not busy).\n");
} else {
printf("Printer is busy.\n");
}
return 0;
}
编译:在DOS环境下使用Turbo C或Watcom C等编译器编译即可,在Linux下使用GCC编译时,需要加上 -O2 优化选项,并可能需要root权限。
2 在Linux环境下
在Linux中,普通用户程序不能直接访问I/O端口,必须使用iopl()或ioperm()系统调用来获取I/O端口的访问权限,并且通常需要root权限。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // for iopl()
#include <sys/io.h> // for inb(), outb(), iopl()
#define LPT_DATA_PORT 0x378
#define LPT_STATUS_PORT 0x379
#define LPT_CONTROL_PORT 0x37A
int main() {
// 请求I/O端口操作权限 (需要root)
// ioperm() 可以只申请特定端口的权限
if (ioperm(LPT_DATA_PORT, 3, 1)) { // 申请从LPT_DATA_PORT开始的3个端口的权限
perror("ioperm");
exit(1);
}
// 或者使用 iopl(3) 获取所有0-0x3FF端口的权限,权限更高
// if (iopl(3)) {
// perror("iopl");
// exit(1);
// }
printf("Successfully obtained I/O port access.\n");
// 示例:向数据端口写入数据
outb(0xAA, LPT_DATA_PORT); // outb(value, port)
printf("Wrote 0xAA to data port.\n");
// 示例:从状态端口读取数据
unsigned char status = inb(LPT_STATUS_PORT); // inb(port)
printf("Read status port: 0x%X\n", status);
// 释放权限 (好习惯)
ioperm(LPT_DATA_PORT, 3, 0);
return 0;
}
编译与运行:

# 编译 (需要链接libc) gcc -o parallel_test parallel_test.c # 运行 (需要root权限) sudo ./parallel_test
<sys/io.h>提供了inb(), outb(), inw(), outw()等函数,分别用于读写8位和16位数据。
3 在Windows环境下
在Windows中,直接访问硬件I/O端口同样受到严格限制,标准应用程序不能这样做,有几种方法可以实现:
- 使用内核模式驱动:创建一个WDM或WDF驱动程序,这是最稳定、最强大的方法,但也是最复杂的方法。
- 使用第三方库:一些库封装了底层驱动调用,简化了操作。
inpout32.dll或inpoutx64.dll。 - 使用
__inpoutintrinsic函数:如果使用Visual Studio编译器,它提供了一些编译器内部函数来访问I/O端口,但这通常也需要驱动支持或特定权限。
使用 inpout32.dll 的示例 (最常见)
你需要从网上下载 inpout32.dll 和 inpout32.lib (或 inpoutx64.dll / inpoutx64.lib for 64-bit),并将它们放在你的项目目录或系统目录中。
#include <stdio.h>
#include <windows.h>
// 从inpout32.lib导入函数
_declspec(dllimport) void __stdcall Out32(short PortAddr, short Data);
_declspec(dllimport) short __stdcall Inp32(short PortAddr);
#define LPT_DATA_PORT 0x378
#define LPT_STATUS_PORT 0x379
#define LPT_CONTROL_PORT 0x37A
int main() {
printf("Writing 0x55 to data port...\n");
Out32(LPT_DATA_PORT, 0x55);
printf("Reading from status port...\n");
short status = Inp32(LPT_STATUS_PORT);
printf("Status: 0x%X\n", status);
return 0;
}
编译与运行:
- 将
inpout32.lib添加到你的项目中。 - 将
inpout32.dll放在与生成的.exe文件相同的目录下。 - 直接运行即可(通常不需要管理员权限,但这取决于系统安全设置)。
实际应用示例:闪烁LED灯
假设我们将一个LED灯的正极通过一个限流电阻(如330Ω)连接到并口的D0引脚(数据寄存器的bit 0),负极接地。
目标:让LED灯闪烁。
// 这个示例适用于DOS或Linux (使用ioperm)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // for sleep()
#include <sys/io.h> // for inb, outb
#define LPT_DATA_PORT 0x378
int main() {
if (ioperm(LPT_DATA_PORT, 1, 1)) {
perror("ioperm");
exit(1);
}
printf("LED blinking started. Press Ctrl+C to stop.\n");
unsigned char data;
while (1) {
// 点亮LED (D0 = 1)
data = 0x01; // 二进制 00000001
outb(data, LPT_DATA_PORT);
sleep(1); // 延时1秒
// 熄灭LED (D0 = 0)
data = 0x00; // 二进制 00000000
outb(data, LPT_DATA_PORT);
sleep(1); // 延时1秒
}
// 理论上不会执行到这里
ioperm(LPT_DATA_PORT, 1, 0);
return 0;
}
现代替代方案:GPIO
在现代计算机和嵌入式系统中(如树莓派),并口已被GPIO(通用输入/输出)所取代。
- GPIO 的引脚功能是可配置的,可以作为输入或输出,并且通常支持更复杂的协议(如I2C, SPI)。
- 操作GPIO通常是通过读写
/sys文件系统下的虚拟文件,或者使用特定的库(如wiringPifor Raspberry Pi,libgpiodfor Linux)。
GPIO的优势:
- 标准:是现代硬件的标准配置。
- 灵活:引脚功能可配置。
- 安全:通过用户空间文件系统访问,比直接I/O端口访问更安全、更受支持。
- 功能丰富:支持中断、PWM等高级功能。
| 特性 | 并口 | GPIO |
|---|---|---|
| 时代 | 较为古老,主要用于工业和打印机 | 现代标准,用于所有嵌入式和单板计算机 |
| 访问方式 | 直接读写特定I/O端口地址 | 通过文件系统或专用库访问 |
| 灵活性 | 功能固定(数据、状态、控制) | 引脚功能可配置(输入/输出/其他协议) |
| 平台支持 | x86 PC,需要特殊权限/驱动 | 树莓派、BeagleBone、Intel Edison等 |
| 安全性 | 风险较高,容易导致系统不稳定 | 相对安全,由内核管理 |
对于学习C语言底层编程,操作并口是一个极好的起点,但对于任何新的项目,强烈建议使用现代硬件的GPIO接口。
