Hardware Access/zh CN

From Free Pascal wiki
Jump to navigationJump to search

Deutsch (de) English (en) español (es) français (fr) magyar (hu) 日本語 (ja) 한국어 (ko) polski (pl) português (pt) русский (ru) slovenčina (sk) 中文(中国大陆)‎ (zh_CN)

概览

本页是关于在Lazarus上访问硬件设备的教程的开始。这些设备包括,但不限于:ISA,PCI,USB,并行端口,串行端口。

RTL或LCL没有实现统一的多平台硬件设备的访问。因此本教程将基本覆盖不同平台上的硬件访问方法。在不同环境下可以使用条件编译来编译代码,像这样:

 uses
  Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, ExtCtrls,
 {$IFDEF WIN32}
   Windows;
 {$ENDIF}
 {$IFDEF Unix}
   ports;
 {$ENDIF}

此时还不知道Mac OS X/x86是否会允许HW访问。它可能不允许,虽然那种情形下我假设像io.dll的驱动器将会及时出现。

并行和串行的比较

ISA卡,PCI卡和并行端口使用“并行”协议与计算机进行通信。串行端口和USB设备使用“串行”协议。因为处理器和编程语言都通过并行方式处理数据,在软件端比较容易实现对这类协议的访问。例如,访问一个整型变量时,可以仅用一个命令就能访问它的值。然而,使用串行协议,一次仅能知道一位(bit),需要将所有片断(piece)放在一起才能理解数据。

串行通信比较难于直接实现,如果使用预制(pre-made)组件,可能会稍微容易些。在硬件端也比较困难,因此许多设备使用专门的集成电路或甚至微控制器来实现。

现在来做一个硬件访问协议的简要比较:

速度 硬件实现难度
串行端口 非常慢 (< E5 bit/s) 中等
并行端口 慢 (~ E6 bit/s) 容易
ISA卡 中等 (~ E7 bit/s) 中等
USB 中等 (~ E7 bit/s) 困难
PCI卡 非常快 (> E9 bit/s) 非常难

并行通信

在Windows操作系统中使用inpout32.dll

在9x系列和NT系列,Windows有不同的方法来访问硬件设备。在9x系列(95,98,Me),程序可以直接访问硬件,正如在DOS上一样。然而,NT系列(Windows NT和XP)不允许这种方式。在该架构上,所有与硬件端口的通信必须通过一个设备驱动器。这是一种安全机制,但是为一个小项目开发一个驱动器可能会花费太多的时间和金钱。

幸运地是有一个库解决了这个问题。如果检测到Windows NT,它解压HWInterface.sys内核设备驱动器并安装。如果检测到Windows 9x,它简单地使用汇编操作码访问硬件。

但是如何使用这个库呢?非常简单!它仅有2个函数:Inp32和Out32,它们的使用是比较直观的。

我们将动态加载该库,因此让我们首先定义这2个函数:

 type
   TInp32 = function(Address: SmallInt): SmallInt; stdcall;
   TOut32 = procedure(Address: SmallInt; Data: SmallInt); stdcall;
  • Address代表期望访问的端口地址
  • Out32发送数据到指定地址的端口
  • Inp32从指定地址的端口返回一字节

现在可以加载该库了。这可能在一个类似程序主form的OnCreate方法的地方来实现:

 type
   TMyForm = class(TForm)
   .........
   private
     { private declarations }
     Inpout32: THandle;
     Inp32: TInp32;
     Out32: TOut32;
   .........
 implementation
   .........
 procedure TMyForm.FormCreate(Sender: TObject);
 begin
 {$IFDEF WIN32}
   Inpout32 := LoadLibrary('inpout32.dll');
   if (Inpout32 <> 0) then
   begin
     // needs overtyping, plain Delphi's @Inp32 = GetProc... leads to compile errors
     Inp32 := TInp32(GetProcAddress(Inpout32, 'Inp32'));
     if (@Inp32 = nil) then Caption := 'Error';
     Out32 := TOut32(GetProcAddress(Inpout32, 'Out32'));
     if (@Out32 = nil) then Caption := 'Error';
   end
   else Caption := 'Error';
 {$ENDIF}
 end;

如果在OnCreate加载了该库,那么不要忘记在OnDestroy卸载它:

 procedure TMyForm.FormDestroy(Sender: TObject);
 begin
 {$IFDEF WIN32}
   FreeLibrary(Inpout32);
 {$ENDIF}
 end;

这里是一个如何使用Inp32函数的简单例子:

 {$IFDEF WIN32}
   myLabel.Caption := IntToStr(Inp32($0220));
 {$ENDIF}

该代码在Windows XP上使用Lazarus 0.9.10,在端口$0220的一个自定义ISA卡上测试过。当然为了让该代码运行,你需要有一个有使用条款的Windows。为了部署,你仅需要把“inpout32.dll”包含在应用的相同目录下。

这是该库的主页:www.logix4u.net/inpout32.htm *查看讨论*

在Windows 9x上使用汇编

在Windows 9x上也可以使用汇编代码。假设希望把$CC发送到$320端口。下面代码可以实现:

 {$ASMMODE ATT}
 ...
    asm
        movl $0x320, %edx
        movb $0xCC, %al
        outb %al, %dx
    end ['EAX','EDX'];

在Windows上的疑难解答

在Windows上使用不支持即插即用并行硬件的一个可能问题来源是,Windows可能将该硬件使用的端口分配给了另外的设备。你可以在下面的URL里找到关于如何告诉Windows不要将你设备的地址分配给即插即用设备的指示:

http://support.microsoft.com/kb/135168

在Linux上使用ioperm访问端口

在Linux上访问硬件的最好方法通过设备驱动器,但是考虑到创建一个驱动器任务的复杂性,有时候一个快速的方法是非常有用的。

为了使用Linux下的“ports”单元,程序必须以root身份运行,并且必须调用IOPerm设置端口访问的合适权限。你可以在这里找到关于“ports”单元的文档。

要做的第一件事是链接(g)libc和调用IOPerm。一个链接整个(g)libc的单元存在于free pascal,但是当应用直接使用时该单元出现了问题,并且静态链接整个(g)libc库不是一个非常好的主意,因为在不同版本间它以不兼容的方式改变。然而,像ioperm类的函数不大可能改变。

 {$IFDEF Linux}
 function ioperm(from: Cardinal; num: Cardinal; turn_on: Integer): Integer; cdecl; external 'libc';
 {$ENDIF}
  • from 代表访问的第一个端口.
  • num 是访问的端口数,因此ioperm($220, 8, 1)将为程序访问$220——$227之间(含)的所有端口。

在链接IOPerm后,可以使用port[<Address>]访问端口。

 {$IFDEF Linux}
   i := ioperm($220, 8, 1);
   port[$220] := $00;
   myLabel.Caption := 'ioperm: ' + IntToStr(i);
   i := Integer(port[$220]);
   myOtherLabel.Caption := 'response: ' + IntToStr(i);
 {$ENDIF}

该代码在Mandriva Linux 2005和Damn Small Linux 1.5上使用Lazarus 0.9.10,在端口$0220的一个自定义ISA卡上测试过。

通用UNIX硬件访问

{$IFDEF Unix}
Uses Clib;   // retrieve libc library name.
{$ENDIF}

{$IFDEF Unix}
function ioperm(from: Cardinal; num: Cardinal; turn_on: Integer): Integer; cdecl; external clib;
{$ENDIF}


注意 FPC在unit x86里为ioperm提供了一个叫做“fpioperm”的抽象,也定义了fpIOPL和输出/输入函数。这些函数当前是为Linux/x86和FreeBSD/x86实现的。

不建议链接到的libc,除非绝对必要的,因为可能的部署和移植功能。 像上面那样(通过为别处有效的函数声明特设导入)手工链接libc也是不建议的(例如,如果标准C库不叫作libc,那么上面的libc导入行将不必要(unnecessarily)地失败,比如BeOS或非标准C符号扩展(symbol mangling)平台上的libroot)。

注意 2 不建议在除了Kylix兼容性的任何环境下使用unit libc。参见libc unit

串行通信

使用Synaser库开发一个串行通信软件是非常容易的。当与Synaser文档一起使用时,例子应该很好理解。最主要的部分是TBlockSerial.Config设置速度(位/秒),数据位,奇偶校验位,停止位和握手协议,如果有的话。下面代码在一个连接到COM 1的串行鼠标上测试过。

program comm;

{$apptype console}

uses
  Classes, SysUtils, Synaser;

var
  ser: TBlockSerial;
begin
  ser:=TBlockSerial.Create;
  try
    ser.Connect('COM1');
    ser.config(1200, 7, 'N', SB1, False, False);
    while True do
      Write(IntToHex(ser.RecvByte(10000), 2), ' ');
  finally
    ser.free;
  end;
end.

下面的代码例子是上面例子的一个替代版本。上面的例子看起来在主要概念上有严重错误,准确地是“while true do...”部分。在测试系统(Asus A6T Laptop with Digitus USB to RS232 Adapter, Ubuntu 8.04.1)上,该部分导致了下面错误:每个会话只能成功运行一次应用,当再次起动应用时,应用不能连接到串行端口。因此,每次用户试图重启应用时,需要重启(计算机),这是一个非常恼人的bug。

原因不难理解:应用处于while true do循环,更准确地说是无限循环。没有退出条件,因此关闭应用的唯一办法是关闭终端或按CTRL-C。但是如果通过种办法退出应用,释放串行端口的重要部分“ser.free”从来不被调用。德语Lazarus论坛的下面贴子里描述了这个问题http://www.lazarusforum.de/viewtopic.php?f=10&t=2082

为了让每个用户明白,而不是按CTRL-C,在主要应用周围有一些代码。也许有人担心,为什么/dev/ttyUSB0被用作com端口:这是由于在测试系统上使用的USB串行适配器。如果有内置串行端口,请使用“Com0”——像上面例子代码中声明。

program serialtest;

{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  {$ENDIF}{$ENDIF}
  Classes,SysUtils,Synaser,Crt
  { you can add units after this };

  var l:boolean;

  function check_affirmation():boolean;
  var k:string;
  begin
       Writeln('To quit the application please do NOT use CTRL-C! Instead, please press any key to quit the application! '+
       'Please confirm this notification before the application continues! '+
       '[0]=Quit, [1]=Confirm, please continue! ');
       Writeln('Your decision: ');
       Read(k);
       if StrtoInt(k) = 1 then
       begin
            check_affirmation:=true;
            Writeln('OK, application continues ...');
       end
       else
       begin
            check_affirmation:=false;
            Writeln('Abort');
       end
  end;

  procedure RS232_connect;
  var
     ser: TBlockSerial;
  begin
       ser:=TBlockSerial.Create;
       try
          ser.Connect('/dev/ttyUSB0'); //ComPort
          Sleep(1000);
          ser.config(1200, 7, 'N', SB1, False, False);
          Write('Device: ' + ser.Device + '   Status: ' + ser.LastErrorDesc +' '+
          Inttostr(ser.LastError));
          Sleep(1000);
          repeat
                Write(IntToHex(ser.RecvByte(10000), 2), ' ');
          until keypressed; //Important!!!
       finally
              Writeln('Serial Port will be freed...');
              ser.free;
              Writeln('Serial Port was freed successfully!');
       end;
  end;

  begin
     l:=check_affirmation();
     if l=true then
     RS232_connect()
     else
     Writeln('Program quit! ');
  end.

另外,外部链接节有UNIX和Windows串行端口教程。


值得一提的是linux下TBlockSerial.LinuxLock参数的功能。当设置默认为True时,连接将试图在/var/lock下创建一个锁文件(例如,“LCK..ttyUSB0”),如果所请求的端口存在一个锁就会失败。如果没有调用释放,锁文件将一直存在。设置LinuxLock为False将使Synaser忽略端口锁定。

替代Synaser:

基于Synaser的可视组件5dpo

另外一个非常简单的fpc单元现在是freepascal(至少版本2.2.2)的部分了,只要将Serial放到你的Uses清单里,然而除了Serial.pp源文件外还没有其它文档。

USB

libusb

驱动开发向来是内核开发中工作量最多的一块,随着USB设备的普及,大量的USB设备的驱动开发也成为驱动开发者手头上做的最多的事情。本文主要介绍Linux平台下基于libusb的驱动开发,希望能够给从事Linux驱动开发的朋友带来些帮助,更希望能够给其他平台上的无驱设计带来些帮助。文章是我在工作中使用libusb的一些总结,难免有错误,如有不当的地方,还请指正。

Linux 平台上的usb驱动开发,主要有内核驱动的开发和基于libusb的无驱设计。

对于内核驱动的大部分设备,诸如带usb接口的hid设备,linux本身已经自带了相关的驱动,我们只要操作设备文件便可以完成对设备大部分的操作,而另外一些设备,诸如自己设计的硬件产品,这些驱动就需要我们驱动工程师开发出相关的驱动了。内核驱动有它的优点,然而内核驱动在某些情况下会遇到如下的一些问题:

1 当使用我们产品的客户有2.4内核的平台,同时也有2.6内核的平台,我们要设计的驱动是要兼容两个平台的,就连makefile 我们都要写两个。

2 当我们要把linux移植到嵌入平台上,你会发现原先linux自 带的驱动移过去还挺大的,我的内核当然是越小越好拉,这样有必要么。这还不是最郁闷的地方,如果嵌入平台是客户的,客户要购买你的产品,你突然发现客户设 备里的系统和你的环境不一样,它没有你要的驱动了,你的程序运行不了,你会先想:“没关系,我写个内核驱动加载一下不就行了“。却发现客户连insmod加载模块的工具都没移植,那时你就看看老天,说声我怎么那么倒霉啊,客户可不想你动他花了n时间移植的内核哦

3 花了些功夫写了个新产品的驱动,挺有成就感啊,代码质量也是相当的有水准啊。正当你沉醉在你的代码中时,客服不断的邮件来了,“客户需要2.6.5内核的驱动,config文件我已经发你了” “客户需要双核的 2.6.18-smp 的驱动” “客户的平台是自己定制的是2.6.12-xxx “ 你恨不得把驱动的源代码给客户,这样省得编译了。你的一部分工作时间编译内核,定制驱动

有问题产生必然会有想办法解决问题的人, libusb的出现给我们带来了某些方便,即节约了我们的时间,也降低了公司的成本。 所以在一些情况下,就可以考虑使用libusb的无驱设计了。

下面我们就来详细讨论一下libusb, 并以写一个hid设备的驱动来讲解如何运用libusb,至于文章中涉及的usb协议的知识,限于篇幅,就不详细讲解了,相关的可自行查看usb相关协议。

一 libusb 介绍

libusb 设计了一系列的外部API 为应用程序所调用,通过这些API应用程序可以操作硬件,从libusb的源代码可以看出,这些API 调用了内核的底层接口,和kernel driver中所用到的函数所实现的功能差不多,只是libusb更加接近USB 规范。使得libusb的使用也比开发内核驱动相对容易的多。 Libusb 的编译安装请查看Readme,这里不做详解

二 libusb 的外部接口

2.1 初始化设备接口

这些接口也可以称为核心函数,它们主要用来初始化并寻找相关设备。

usb_init

函数定义: void usb_init(void);

从函数名称可以看出这个函数是用来初始化相关数据的,这个函数大家只要记住必须调用就行了,而且是一开始就要调用的.

usb_find_busses

函数定义: int usb_find_busses(void);

寻找系统上的usb总线,任何usb设备都通过usb总线和计算机总线通信。进而和其他设备通信。此函数返回总线数。

usb_find_devices

函数定义: int usb_find_devices(void);

寻找总线上的usb设备,这个函数必要在调用usb_find_busses()后使用。以上的三个函数都是一开始就要用到的,此函数返回设备数量。

usb_get_busses

函数定义: struct usb_bus *usb_get_busses(void);

这个函数返回总线的列表,在高一些的版本中已经用不到了,这在下面的实例中会有讲解

2.2 操作设备接口

usb_open

函数定义: usb_dev_handle *usb_open(struct *usb_device dev);

打开要使用的设备,在对硬件进行操作前必须要调用usb_open 来打开设备,这里大家看到有两个结构体 usb_dev_handle 和 usb_device 是我们在开发中经常碰到的,有必要把它们的 结构看一看。在libusb 中的usb.h和usbi.h中有定义。

这里我们不妨理解为返回的 usb_dev_handle 指针是指向设备的句柄,而行参里输入就是需要打开的设备。

usb_close

函数定义: int usb_close(usb_dev_handle *dev);

与usb_open相对应,关闭设备,是必须调用的, 返回0成功,<0 失败。

usb_set_configuration

函数定义: int usb_set_configuration(usb_dev_handle *dev, int configuration);

设置当前设备使用的configuration,参数configuration 是你要使用的configurtation descriptoes中的bConfigurationValue, 返回0成功,<0失败( 一个设备可能包含多个 configuration,比如同时支持高速和低速的设备就有对应的两个configuration,详细可查看usb标准)

usb_set_altinterface

函数定义: int usb_set_altinterface(usb_dev_handle *dev, int alternate);

和名字的意思一样,此函数设置当前设备配置的interface descriptor,参数alternate是指interface descriptor中的bAlternateSetting。返回0成功,<0失败

usb_resetep

函数定义: int usb_resetep(usb_dev_handle *dev, unsigned int ep);

复位指定的endpoint,参数ep 是指bEndpointAddress,。这个函数不经常用,被下面介绍的usb_clear_halt函数所替代。

usb_clear_halt

函数定义: int usb_clear_halt (usb_dev_handle *dev, unsigned int ep);

复位指定的endpoint,参数ep 是指bEndpointAddress。这个函数用来替代usb_resetep

usb_reset

函数定义: int usb_reset(usb_dev_handle *dev);

这个函数现在基本不怎么用,不过这里我也讲一下,和名字所起的意思一样,这个函数reset设备,因为重启设备后还是要重新打开设备,所以用usb_close就已经可以满足要求了。

usb_claim_interface

函数定义: int usb_claim_interface(usb_dev_handle *dev, int interface);

注册与操作系统通信的接口,这个函数必须被调用,因为只有注册接口,才能做相应的操作。

Interface 指 bInterfaceNumber. (下面介绍的usb_release_interface 与之相对应,也是必须调用的函数)

usb_release_interface

函数定义: int usb_release_interface(usb_dev_handle *dev, int interface);

注销被usb_claim_interface函数调用后的接口,释放资源,和usb_claim_interface对应使用。

2.3 控制传输接口

usb_control_msg

函数定义:int usb_control_msg(usb_dev_handle *dev, int requesttype, int request, int value, int index, char *bytes, int size, int timeout);

从默认的管道发送和接受控制数据

usb_get_string

函数定义: int usb_get_string(usb_dev_handle *dev, int index, int langid, char *buf, size_t buflen);

usb_get_string_simple

函数定义: int usb_get_string_simple(usb_dev_handle *dev, int index, char *buf, size_t buflen);

usb_get_descriptor

函数定义: int usb_get_descriptor(usb_dev_handle *dev, unsigned char type, unsigned char index, void *buf, int size);

usb_get_descriptor_by_endpoint

函数定义: int usb_get_descriptor_by_endpoint(usb_dev_handle *dev, int ep, unsigned char type, unsigned char index, void *buf, int size);


2.4 批传输接口

usb_bulk_write

函数定义: int usb_bulk_write(usb_dev_handle *dev, int ep, char *bytes, int size, int timeout);

usb_interrupt_read

函数定义: int usb_interrupt_read(usb_dev_handle *dev, int ep, char *bytes, int size, int timeout);

2.5 中断传输接口

usb_bulk_write

函数定义: int usb_bulk_write(usb_dev_handle *dev, int ep, char *bytes, int size, int timeout);

usb_interrupt_read

函数定义: int usb_interrupt_read(usb_dev_handle *dev, int ep, char *bytes, int size, int timeout);

A cross platform possibility for Linux, BSDs and Mac OS X is libusb.

Headers are listed in http://www.freepascal.org/contrib/db.php3?category=Miscellaneous:

文件名 作者 版本 日期 链接 备注
libusb.pp Uwe Zimmermann 0.1.12 2006-06-29 http://www.sciencetronics.com/download/fpc_libusb.tgz
libusb.pas Johann Glaser 2012-09-23 https://github.com/hansiglaser/pas-libusb includes OOP wrapper, see branch "libusb-1.0" for libusb 1.0
fpcusb Joe Jared 0.11-14 2006-02-02 http://relays.osirusoft.com/fpcusb.tgz download link broken
libusb.pp Marko Medic 1.0 2010-12-14 http://www.lazarus.freepascal.org/index.php/topic,11435.0.html

FTDI

If you use one of the chips from FTDI, you can use their pascal headers for their dll interface to the chips.

外部链接

通信协议速度比较:

  1. http://en.wikipedia.org/wiki/Serial_port#Speed
  2. http://www.lvr.com/jansfaq.htm - Jan Axelson's Parallel Port FAQ
  3. http://en.wikipedia.org/wiki/USB#Transfer_Speed
  4. http://en.wikipedia.org/wiki/PCI#Conventional_PCI_bus_specifications

串行通信链接:

  1. On UNIX: [1]
  2. On Windows: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnfiles/html/msdn_serial.asp
  3. Synaser component: http://synapse.ararat.cz/
  4. Comport Delphi package: http://sourceforge.net/projects/comport/

ISA数字示波器——一个硬件访问例子的全部源代码包含在:

[2]