C语言如何操作并行端口?

99ANYc3cd6
预计阅读时长 22 分钟
位置: 首页 C语言 正文

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

C语言parallel port
(图片来源网络,侵删)

并口基础知识

我们需要了解并口的基本工作原理,标准的并口(如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或编译器提供的特殊扩展。

C语言parallel port
(图片来源网络,侵删)

1 在DOS环境下(最简单)

在DOS时代,访问I/O端口非常直接,可以使用inportboutportb函数,这些函数通常在<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;
}

编译与运行

C语言parallel port
(图片来源网络,侵删)
# 编译 (需要链接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端口同样受到严格限制,标准应用程序不能这样做,有几种方法可以实现:

  1. 使用内核模式驱动:创建一个WDM或WDF驱动程序,这是最稳定、最强大的方法,但也是最复杂的方法。
  2. 使用第三方库:一些库封装了底层驱动调用,简化了操作。inpout32.dllinpoutx64.dll
  3. 使用__inpout intrinsic函数:如果使用Visual Studio编译器,它提供了一些编译器内部函数来访问I/O端口,但这通常也需要驱动支持或特定权限。

使用 inpout32.dll 的示例 (最常见)

你需要从网上下载 inpout32.dllinpout32.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;
}

编译与运行

  1. inpout32.lib 添加到你的项目中。
  2. inpout32.dll 放在与生成的 .exe 文件相同的目录下。
  3. 直接运行即可(通常不需要管理员权限,但这取决于系统安全设置)。

实际应用示例:闪烁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 文件系统下的虚拟文件,或者使用特定的库(如 wiringPi for Raspberry Pi, libgpiod for Linux)。

GPIO的优势

  • 标准:是现代硬件的标准配置。
  • 灵活:引脚功能可配置。
  • 安全:通过用户空间文件系统访问,比直接I/O端口访问更安全、更受支持。
  • 功能丰富:支持中断、PWM等高级功能。
特性 并口 GPIO
时代 较为古老,主要用于工业和打印机 现代标准,用于所有嵌入式和单板计算机
访问方式 直接读写特定I/O端口地址 通过文件系统或专用库访问
灵活性 功能固定(数据、状态、控制) 引脚功能可配置(输入/输出/其他协议)
平台支持 x86 PC,需要特殊权限/驱动 树莓派、BeagleBone、Intel Edison等
安全性 风险较高,容易导致系统不稳定 相对安全,由内核管理

对于学习C语言底层编程,操作并口是一个极好的起点,但对于任何新的项目,强烈建议使用现代硬件的GPIO接口。

-- 展开阅读全文 --
头像
CentOS下C语言如何连接MySQL?
« 上一篇 12-17
百度 dede swfupload如何配置使用?
下一篇 » 12-17

相关文章

取消
微信二维码
支付宝二维码

目录[+]