When I wrote Parallel Port Tester, I discovered that detecting parallel ports and their I/O addresses in Windows is a pain in the butt. I have received several inquiries asking about how I did it, so it’s obvious that others agree with me. Hopefully, this post will help everyone learn how to automatically detect their parallel port addresses. I certainly don’t claim to be an expert, and there may be better ways to do it, but this method seems to work for me.

Don’t use Win32_ParallelPort

One of the strategies I’ve seen recommended is to do a WMI query on the Win32_ParallelPort class. This is originally how I started implementing Parallel Port Tester, but I quickly discovered that it sucks! Sometimes it gets the I/O address for the wrong port, and sometimes it doesn’t even find your parallel port at all. It seems to depend on the version of Windows and the chipset manufacturer. In particular, I found that it would not detect a parallel port implemented by a Moschip chipset in Windows XP. It also was returning incorrect I/O addresses on a user’s system running Vista. Anyway, I’d highly recommend that you avoid this class. I could not get it to work correctly. Maybe I was doing something wrong…but I’m pretty sure it just sucks. It sucks so bad that I disabled this method of discovering parallel ports in version 1.0.0.1 because of a report that it was returning erroneous information.

Instead, find objects belonging to the Ports class manually

For more reliable results, what I do is search for all devices that are ports (which also include COM ports). The GUID for this class is {4D36E978-E325-11CE-BFC1-08002BE10318}. I only pay attention to returned ports that are LPT ports. On each found LPT port, I request all of the port resources to determine the I/O addresses.

How do you do that? Here’s some sample C# code using .NET. Note that I’m only showing relevant bits and pieces, so you will have to fit them into a class and function as appropriate.

using System.Management;

private static readonly ushort SPPIOLength = 8;
private static readonly ushort ECPIOLength = 4;

SelectQuery portsQuery = new SelectQuery("Win32_PnPEntity",
    "ClassGUID=\"{4D36E978-E325-11CE-BFC1-08002BE10318}\"");
ManagementObjectSearcher portSearcher = new ManagementObjectSearcher(portsQuery);
foreach (ManagementObject port in portSearcher.Get())
{
    // Grab its name. This will be in the form "Blah Blah Parallel Port (LPT1)".
    // We're going to get ugly now...
    string name = (string)port.Properties["Name"].Value;

    // Skip ports that are not parallel ports. This is UGLY...I'd much rather
    // do this a cleaner way, but it seems that different parallel ports do
    // things differently.
    if (!name.Contains("(LPT")) { continue; }

    // and extract the parallel port name by looking for everything inside
    // the parentheses. I don't know how else to do it...
    int beginLoc = name.IndexOf("(LPT") + 1;
    int endLoc = name.IndexOf(')', beginLoc);
    name = name.Substring(beginLoc, endLoc - beginLoc);

    // Grab the device ID so we can find all the associated port resources.
    string deviceID = (string)port.Properties["DeviceID"].Value;
    // Replace single backslashes with double backslashes to be suitable
    // to use in a query. Note that I also had to escape it myself in
    // code...so \\ represents a single backslash.
    // TODO: Any other escaping necessary?
    deviceID = deviceID.Replace("\\", "\\\\");

    // Now search for I/O ranges of this port.
    ObjectQuery resourceQuery =
        new ObjectQuery("ASSOCIATORS OF {Win32_PnPEntity.DeviceID=\"" +
        deviceID + "\"} WHERE ResultClass = Win32_PortResource");
    ManagementObjectSearcher resourceSearcher =
        new ManagementObjectSearcher(resourceQuery);

    // Find the SPP and ECP base addresses
    ushort SPPBase = 0xFFFF;
    ushort ECPBase = 0xFFFF;
    foreach (ManagementObject resource in resourceSearcher.Get())
    {
        // Grab starting & ending addresses
        ushort startAddress =
            (ushort)(UInt64)resource.Properties["StartingAddress"].Value;
        ushort endAddress =
            (ushort)(UInt64)resource.Properties["EndingAddress"].Value;

        ushort rangeLen = (ushort)(endAddress - startAddress + 1);
        // If we haven't yet found the SPP base address, and this range
        // is big enough, use it as the SPP base address.
        if ((SPPBase == 0xFFFF) && (rangeLen >= SPPIOLength))
        {
            SPPBase = startAddress;
        }
        // If we haven't yet found the ECP base address, and this range
        // is big enough, use it as the ECP base address
        else if ((ECPBase == 0xFFFF) && (rangeLen >= ECPIOLength))
        {
            ECPBase = startAddress;
        }
    }
    // Although I didn't include the code here, if I fail to detect an
    // SPP address with the rules above, I grab the first returned
    // address and use it as the SPP base address. Then I set the ECP
    // address to 0xFFFF (which I treat as meaning "unable to find")

    // TODO: If SPPBase != 0xFFFF, add it to the list of discovered
    // ports -- the name of the port is stored in the variable "name"
}

I hope this code is helpful to someone out there! From this point, you can use something like Inpout32.dll to access the port registers directly.

I’m not a huge fan of the hack where I extract the “LPT#” name from the long name by searching for the “(LPT” string, but I couldn’t find a better way to do it. Suggestions would be appreciated! There also may be a better way to use parameters to do the ASSOCIATORS query instead of ugly escaping and string concatenation, but it worked and that was good enough for me on this particular project. I couldn’t find an easy way to set it up for use with parameters or else I would have done it that way.

Trackback

8 comments

  1. Hi Doug,

    Thanks a lot for sharing this with us.

    I used Win32_ParallelPort to retrieve the PnpDeviceId values and searched the corresponding string into Win32_PNPAllocatedResource.

    It seems to work for me in Win7 but after reading your post, it might fail on some systems.

    Can you tell me how you extracted the addresses from Win23_ParallelPort?

    Anyway great job!

  2. Hey Arc,

    Glad to help! Here’s a description of the old, buggy method I was using previously. I did a SelectQuery on Win32_ParallelPort. Then for each result, I grabbed its DeviceID and did this query:

    “ASSOCIATORS OF {Win32_ParallelPort.DeviceID='” + deviceID + “‘} WHERE ResultClass = Win32_PortResource”

    It failed to find one of the ports on my test setup here with an ExpressCard parallel port on my laptop running XP. I also received a report that Parallel Port Tester was getting the resource addresses wrong from someone running Vista with both an internal parallel port and a SIIG parallel port card. It wasn’t detecting the internal port, and for the SIIG card it was returning the I/O addresses of the internal port. I wrote a diagnostic program which confirmed that Win32_ParallelPort was returning the wrong info to me. The technique described in this post obtained the correct addresses. At that point I decided never to trust Win32_ParallelPort again 🙂

  3. Hi Doug, thanks for your answer. I will use your method, if you dont mind, as it works great in my environment.

    Best regards, keep up with the good work.

    Arc.

  4. Hi Doug !

    Thank you for sharing this. I am currently using your code. I think it is great !

    Werner

  5. Hi Doug,
    Thank you for your script. It worked for me.

    I noticed that “)” of line 50 would not be necessary. It worked after I deleted it.

    Thank you anyway.

    Katsuya

  6. Thank you for the correction, Katsuya. Oops! Fixed in the post.

  7. Hello Doug and gang. I was just wondering if anybody has a working version of this in C++ or VB Express. My knowledge of C# is about what I can figure out the differences at the time and I have tried the online translators but they seem to generate as many errors as code. Tried SharpeDevelop and couldn’t work out how to draw a form so I went to C# Express for a simple form with a button1 and a listbox1 but trying this code gives me the error list that follows it.

    It seems to be these two lines and probably a scope problem but I have moved them all over the place and just generated more errors that I don’t fully understand any more than this lot.

    SelectQuery portsQuery = new SelectQuery(“Win32_PnPEntity”,
    “ClassGUID=\”{4D36E978-E325-11CE-BFC1-08002BE10318}\””);
    ManagementObjectSearcher portSearcher = new ManagementObjectSearcher(portsQuery);

    Am I missing a using directive or an assembly reference?

    —CODE—
    using System.Management;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;

    namespace Enumerator
    {

    public partial class Form1 : Form
    {
    private static readonly ushort SPPIOLength = 8;
    private static readonly ushort ECPIOLength = 4;

    public Form1()
    {
    InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
    SelectQuery portsQuery = new SelectQuery(“Win32_PnPEntity”,
    “ClassGUID=\”{4D36E978-E325-11CE-BFC1-08002BE10318}\””);
    ManagementObjectSearcher portSearcher = new ManagementObjectSearcher(portsQuery);

    foreach (ManagementObject port in portSearcher.Get())
    {
    // Grab its name. This will be in the form “Blah Blah Parallel Port (LPT1)”.
    // We’re going to get ugly now…
    string name = (string)port.Properties[“Name”].Value;

    // Skip ports that are not parallel ports. This is UGLY…I’d much rather
    // do this a cleaner way, but it seems that different parallel ports do
    // things differently.
    if (!name.Contains(“(LPT”)) { continue; }

    // and extract the parallel port name by looking for everything inside
    // the parentheses. I don’t know how else to do it…
    int beginLoc = name.IndexOf(“(LPT”) + 1;
    int endLoc = name.IndexOf(‘)’, beginLoc);
    name = name.Substring(beginLoc, endLoc – beginLoc);

    // Grab the device ID so we can find all the associated port resources.
    string deviceID = (string)port.Properties[“DeviceID”].Value;
    // Replace single backslashes with double backslashes to be suitable
    // to use in a query. Note that I also had to escape it myself in
    // code…so \\ represents a single backslash.
    // TODO: Any other escaping necessary?
    deviceID = deviceID.Replace(“\\”, “\\\\”);

    // Now search for I/O ranges of this port.
    ObjectQuery resourceQuery =
    new ObjectQuery(“ASSOCIATORS OF {Win32_PnPEntity.DeviceID=\”” +
    deviceID + “\”} WHERE ResultClass = Win32_PortResource”);
    ManagementObjectSearcher resourceSearcher =
    new ManagementObjectSearcher(resourceQuery);

    // Find the SPP and ECP base addresses
    ushort SPPBase = 0xFFFF;
    ushort ECPBase = 0xFFFF;
    foreach (ManagementObject resource in resourceSearcher.Get())
    {
    // Grab starting & ending addresses
    ushort startAddress =
    (ushort)(UInt64)resource.Properties[“StartingAddress”].Value;
    ushort endAddress =
    (ushort)(UInt64)resource.Properties[“EndingAddress”].Value;

    ushort rangeLen = (ushort)(endAddress – startAddress + 1);
    // If we haven’t yet found the SPP base address, and this range
    // is big enough, use it as the SPP base address.
    if ((SPPBase == 0xFFFF) && (rangeLen >= SPPIOLength))
    {
    SPPBase = startAddress;
    }
    // If we haven’t yet found the ECP base address, and this range
    // is big enough, use it as the ECP base address
    else if ((ECPBase == 0xFFFF) && (rangeLen >= ECPIOLength))
    {
    ECPBase = startAddress;
    }
    }
    // Although I didn’t include the code here, if I fail to detect an
    // SPP address with the rules above, I grab the first returned
    // address and use it as the SPP base address. Then I set the ECP
    // address to 0xFFFF (which I treat as meaning “unable to find”)

    // TODO: If SPPBase != 0xFFFF, add it to the list of discovered
    // ports — the name of the port is stored in the variable “name”
    }
    }
    }
    }
    —\CODE—

    Error 1 The type or namespace name ‘SelectQuery’ could not be found (are you missing a using directive or an assembly reference?) C:\Users\Admin\AppData\Local\Temporary Projects\Enumerator\Form1.cs 28 13 Enumerator
    Error 2 The type or namespace name ‘SelectQuery’ could not be found (are you missing a using directive or an assembly reference?) C:\Users\Admin\AppData\Local\Temporary Projects\Enumerator\Form1.cs 28 42 Enumerator
    Error 3 The type or namespace name ‘ManagementObjectSearcher’ could not be found (are you missing a using directive or an assembly reference?) C:\Users\Admin\AppData\Local\Temporary Projects\Enumerator\Form1.cs 30 13 Enumerator
    Error 4 The type or namespace name ‘ManagementObjectSearcher’ could not be found (are you missing a using directive or an assembly reference?) C:\Users\Admin\AppData\Local\Temporary Projects\Enumerator\Form1.cs 30 57 Enumerator
    Warning 5 ‘2008’ is not a valid warning number Enumerator

  8. Hello all
    For any future travellers finding this place in their search, firstly congratulate Doug for getting it so close to the top of the list. Now here is the Visual Basic version. The code here has been tested in VS2010 Express as a console project but I do have it working with minimal modification in my actual Windows application. It should just be a drop in cut and paste to have a look at it working although any of the worthwhile documentation is probably left over from Dougs work. Just remember that for some reason you do need to add your references to your project in the IDE as well as import them in your code.

    ie. Project->Add Reference->.NET tab->System
    Project->Add Reference->.NET tab->System.Management

    I have also added a bit of non essential code just to show what other information might be obtained and how by this method.

    And here is the code

    Imports System
    Imports System.Management

    Module Module1
    Public Sub Main(ByVal args() As String)
    Dim SPPIOLength As System.UInt16 = 8
    Dim ECPIOLength As System.UInt16 = 4
    Dim s As SelectQuery = New SelectQuery(“Win32_PnPEntity”, “ClassGUID=””{4D36E978-E325-11CE-BFC1-08002BE10318}”””) ‘ True, “__CLASS = ‘Win32_Service'”
    Dim searcher As ManagementObjectSearcher = New ManagementObjectSearcher(s)
    For Each port As ManagementObject In searcher.Get ‘ Service’s changed to port’s
    ‘ show the class
    Dim name As String = CType(port.Properties(“Name”).Value, String)
    If name.Contains(“(LPT”) Then
    Dim beginLoc As Integer = (name.IndexOf(“(LPT”) + 1)
    Dim endLoc As Integer = name.IndexOf(Microsoft.VisualBasic.ChrW(41), beginLoc)
    name = name.Substring(beginLoc, (endLoc – beginLoc))
    Dim deviceID As String = CType(port.Properties(“DeviceID”).Value, String)
    deviceID = deviceID.Replace(“\”, “\\”)

    Dim Qry As String = “ASSOCIATORS OF {Win32_PnPEntity.DeviceID=””” + deviceID + “””} WHERE ResultClass = Win32_PortResource”
    Dim resourceQuery As ObjectQuery = New ObjectQuery(Qry)
    Dim resourceSearcher As ManagementObjectSearcher = New ManagementObjectSearcher(resourceQuery)

    Dim SPPBase As System.UInt16 = 65535
    Dim ECPBase As System.UInt16 = 65535

    For Each resource As ManagementObject In resourceSearcher.Get ‘ Grab starting & ending addresses
    Dim startAddress As System.UInt16 = CType(CType(resource.Properties(“StartingAddress”).Value, UInt64), System.UInt16)
    Dim endAddress As System.UInt16 = CType(CType(resource.Properties(“EndingAddress”).Value, UInt64), System.UInt16)
    Dim rangeLen As System.UInt16 = CType(((endAddress – startAddress) + 1), System.UInt16)
    ‘ If we haven’t yet found the SPP base address, and this range
    ‘ is big enough, use it as the SPP base address.
    If ((SPPBase = 65535) _
    AndAlso (rangeLen >= SPPIOLength)) Then
    SPPBase = startAddress
    Debug.Print(“”)
    Debug.Print(“”)
    Debug.Print(“SPP”)
    Debug.Print(“Name: ” + name + ” DeviceID: ” + deviceID) ‘ port.ToString)
    Debug.Print(“Start = &H” + Hex(startAddress))
    Debug.Print(“End = &H” + Hex(endAddress))
    Debug.Print(“Range &H= ” + Hex(rangeLen))
    End If

    ‘ If we haven’t yet found the ECP base address, and this range
    ‘ is big enough, use it as the ECP base address
    If ((ECPBase = 65535) _
    AndAlso (rangeLen >= ECPIOLength)) Then
    ECPBase = startAddress
    Debug.Print(“”)
    Debug.Print(“”)
    Debug.Print(“ECP”)
    Debug.Print(“Name: ” + name + ” DeviceID: ” + deviceID) ‘ port.ToString)
    Debug.Print(“Start = &H” + Hex(startAddress))
    Debug.Print(“End = &H” + Hex(endAddress))
    Debug.Print(“Range &H= ” + Hex(rangeLen))
    End If

    ‘ Console.WriteLine(port.ToString)
    Next

    Debug.Print(“Availability: = ” + CType(port.Properties(“Availability”).Value, String))
    Debug.Print(“Caption: = ” + CType(port.Properties(“Caption”).Value, String))
    Debug.Print(“ClassGuid: = ” + CType(port.Properties(“ClassGuid”).Value, String))
    ‘Debug.Print(“CompatibleID[]: = ” + CType(port.Properties(“CompatibleID[]”).Value, String))
    Debug.Print(“ConfigManagerErrorCode: = ” + CType(port.Properties(“ConfigManagerErrorCode”).Value, String))
    Debug.Print(“ConfigManagerUserConfig: = ” + CType(port.Properties(“ConfigManagerUserConfig”).Value, String))
    Debug.Print(“CreationClassName: = ” + CType(port.Properties(“CreationClassName”).Value, String))
    Debug.Print(“Description: = ” + CType(port.Properties(“Description”).Value, String))
    Debug.Print(“DeviceID: = ” + CType(port.Properties(“DeviceID”).Value, String))
    Debug.Print(“ErrorCleared: = ” + CType(port.Properties(“ErrorCleared”).Value, String))
    Debug.Print(“ErrorDescription: = ” + CType(port.Properties(“ErrorDescription”).Value, String))
    ‘Debug.Print(“HardwareID[]: = ” + CType(port.Properties(“HardwareID[]”).Value, String))
    Debug.Print(“InstallDate: = ” + CType(port.Properties(“InstallDate”).Value, String))
    Debug.Print(“LastErrorCode: = ” + CType(port.Properties(“LastErrorCode”).Value, String))
    Debug.Print(“Manufacturer: = ” + CType(port.Properties(“Manufacturer”).Value, String))
    Debug.Print(“Name: = ” + CType(port.Properties(“Name”).Value, String))
    ‘Debug.Print(“PNPClass: = ” + CType(port.Properties(“PNPClass”).Value, String))
    Debug.Print(“PNPDeviceID: = ” + CType(port.Properties(“PNPDeviceID”).Value, String))
    ‘Debug.Print(“PowerManagementCapabilities[]: = ” + CType(port.Properties(“PowerManagementCapabilities[]”).Value, String))
    Debug.Print(“PowerManagementSupported: = ” + CType(port.Properties(“PowerManagementSupported”).Value, String))
    ‘Debug.Print(“Present: = ” + CType(port.Properties(“Present”).Value, String))
    Debug.Print(“Service: = ” + CType(port.Properties(“Service”).Value, String))
    Debug.Print(“Status: = ” + CType(port.Properties(“Status”).Value, String))
    Debug.Print(“StatusInfo: = ” + CType(port.Properties(“StatusInfo”).Value, String))
    Debug.Print(“SystemCreationClassName: = ” + CType(port.Properties(“SystemCreationClassName”).Value, String))
    Debug.Print(“SystemName: = ” + CType(port.Properties(“SystemName”).Value, String))

    End If
    Next
    End Sub
    End Module

Add your comment now