Archive for May, 2009

Enumerando dispositivos

1 de May de 2009

Dentre minhas tarefas atuais, estava a de ler as amostras de um giroscópio utilizando uma porta serial e gerar um arquivo com elas. Este arquivo seria lido por um driver semelhante ao demonstrado no post anterior a este. No datasheet do giroscópio há uma descrição do protocolo, sem falar do exemplo feito em Visual Basic que existe no site do fabricante, que demonstra a aceleração angular em cada eixo bem como as acelerações lineares deles como mostra abaixo.


Toda a interface de configuração e obtenção dos dados do giroscópio é realizada através da serial. Mesmo sem uma aplicação dedicada pode-se utilizar um programa qualquer, como o HyperTerminal, para ter acesso aos menus oferecidos pelo dispositivo. Utilizando uma porta serial, qualquer kit de microcontrolador que não tenha display e nem mesmo um teclado pode oferecer uma interface bem amigável.

Escrever protocolos seriais não é algo que me assuste. Duro é ter que lembrar como programar uma aplicação User-Mode que tenha janelas, botões e outros controles, mas nada que um pouco de MSDN não resolva. Programadores de drivers normalmente testam tudo que podem utilizando aplicações console, então desenvolvi um pequeno software de terminal para lidar diretamente com o dispositivo. O fonte dele segue abaixo e fica de brinde para quem precisar brincar com portas seriais qualquer dia.

/****
***     main
**
**      E lá vamos nós...
*/
int _tmain(int argc, _TCHAR* argv[])
{
    DWORD           dwBytes,
                    dwError = ERROR_SUCCESS;
    HANDLE          hCom;
    ULONG           i;
    UCHAR           ucByteIn[512], ucByteOut;
    DCB             dcb;
    COMMTIMEOUTS    CommTimeouts;
 
    //-f--> Aqui abrimos aporta serial desejada
    hCom = CreateFile(L"COM3",
                      GENERIC_READ | GENERIC_WRITE,
                      0,
                      NULL,
                      OPEN_EXISTING,
                      0,
                      NULL);
 
    //-f--> Verifica se obtivemos sucesso
    if (hCom == INVALID_HANDLE_VALUE) 
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf("CreateFile failed with error %d.\n",
                dwError);
        return dwError;
    }
 
    //-f--> Existem 7 kilos de configurações da porta serial.
    //      Ao invés de configirar cada uma delas, vamos apenas
    //      obter as configurações padrão do sistema e modificar
    //      apenas as que nos interessa.
    if (!GetCommState(hCom, &dcb))
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf ("GetCommState failed with error %d.\n",
                dwError);
        CloseHandle(hCom);
        return dwError;
    }
 
    //-f--> Aqui modificamos as configurações para setar as
    //      configurações exigidas pelo giroscópio.
    //      57600, 8 N 1 (sem controle de fluxo)
    dcb.DCBlength = sizeof(DCB);
    dcb.BaudRate = CBR_57600;
    dcb.ByteSize = 8;
    dcb.Parity = NOPARITY;
    dcb.StopBits = ONESTOPBIT;
    dcb.fDtrControl = DTR_CONTROL_DISABLE;
    dcb.fRtsControl = RTS_CONTROL_DISABLE;
 
    //-f--> Aqui aplicamos as configurações que modificamos
    if (!SetCommState(hCom, &dcb))
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf ("SetCommState failed with error %d.\n",
                dwError);
        CloseHandle(hCom);
        return dwError;
    }
 
    //-f--> Vou configurar os timeouts de forma que uma
    //      leitura seja completada depois de 100ms mesmo
    //      nenhum caracter seja recebido pela aplicação.
    //      Isso evita da aplicação ficar travada em ReadFile
    //      até que um byte seja recebido pela porta serial
    CommTimeouts.ReadIntervalTimeout = 0;
    CommTimeouts.ReadTotalTimeoutMultiplier = 0;
    CommTimeouts.ReadTotalTimeoutConstant = 100;
    CommTimeouts.WriteTotalTimeoutConstant = 0;
    CommTimeouts.WriteTotalTimeoutMultiplier = 0;
 
    //-f--> Aplica as configurações de timeouts
    if (!SetCommTimeouts(hCom, &CommTimeouts))
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf ("SetCommTimeouts failed with error %d.\n",
                dwError);
        CloseHandle(hCom);
        return dwError;
    }
 
    //-f--> Aqui começa o loop infinito que vai executar
    //      para todo o sempre até o fim dos dias.
    //      Na verdade, se você teclar [ESC] ele termina.
    while(1)
    {
        //-f--> Verifica se há um byte a ser recebido pela
        //      aplicação no buffer de teclado.
        if (_kbhit())
        {
            //-f--> Obtém a tecla
            ucByteOut = _getch();
 
            //-f--> Verifica se a tecla recebida é [ESC]
            if (ucByteOut == 27)
                break;
 
            //-f--> Envia o byte recebido para a porta serial
            if (!WriteFile(hCom,
                           &ucByteOut,
                           1,
                           &dwBytes,
                           NULL))
            {
                //--> Oops!
                dwError = GetLastError();
                break;
            }
        }
 
        //-f--> Verifica se algum byte foi recebido pela
        //      porta serial. Observe que neste loop não
        //      existe o típico Sleep() para não levar a CPU
        //      a 100%. Esta espera é realizada dentro da
        //      chamada à ReadFile(). A função espera por um
        //      byte por até 100ms, conforme configurado nos
        //      timeouts.
        if (!ReadFile(hCom,
                      ucByteIn,
                      sizeof(ucByteIn),
                      &dwBytes,
                      NULL))
        {
            //-f--> Oops!
            dwError = GetLastError();
            break;
        }
 
        //-f--> Imprime na tela a sequência de caracteres recebida
        for (i=0; i
        {
            if (ucByteIn[i] == 0x0d)
                puts("");
            else
                printf("%c", ucByteIn[i]);
        }
    }
 
    //-f--> Bom se chegamos neste ponto é porque chegamos
    //      ao fim dos dias ou a tecle ESC foi pressionada.
    //      Vamos fechar a porta serial e sair correndo.
    CloseHandle(hCom);
    return dwError;
}

Abrir uma porta serial é a parte mais simples desta história, o problema é decidir qual porta abrir. Como qualquer programa decente, deveria haver um combo list com as portas seriais disponíveis no computador, onde o usuário escolheria uma e pronto. A partir daí é só obter a porta selecionada pelo usuário e montar uma chamada para a rotina CreateFile() como ilustrado no cógido acima.

Tudo bem, arrastei o controle para a janela que eu estava programando e agora é só preenchê-lo. Então pensei: "Deve haver alguma função do tipo EnumerateCommPorts() na API", mas não foi o que a página da referência me mostrava. - Como assim não tem? O Google deve saber algo a respeito. - Acabei descobrindo que esta é uma dúvida bem comum por aí. Uns resolvem este problema fazendo um loop que tenta abrir as portas seriais em sequência (COM1, COM2,... ), outros utilizam a função QueryDosDevice() e filtram os Symbolic Links que iniciam com "COM(n)", mas o método que vou mostrar aqui é capaz de enumerar qualquer tipo de interface utilizando a SetupAPI.

Setup quem?

A SetuAPI é uma parte do Plug-And-Play que fornece serviços às aplicações User-Mode. O Plug-And-Play tem como um dos seus objetivos, unificar a configuração, uso e a enumeração de dispositivos e serviços semelhantes. Desta forma todos os fabricantes de placas que oferecem serviços de porta serial podem ter seus dispositivos configurados de uma única maneira. Dispositivos que oferecem serviços de porta serial devem implementar uma interface pré-definida, ou seja, devem se mostrar dispostos a receber IOCTLs e responder a eles de maneira prevista na documentação.

O driver que desejar criar devices que implementem a interface de porta serial deverá declarar isso através da chamada à rotina IoRegisterDeviceInterface(). Aqui um device é associado à uma classe de interface de dispositivo, a qual é identificada por um GUID. Existem diversas classes de interfaces de dispositivos pré-definidas no sistema, como listado aqui, e a classe de portas seriais é uma delas. A partir daí, seu device será enumerado por rotinas do Plug-and-Play como provedor de uma determinada interface.

Então tá. O que temos que fazer é utilizar estas funções de enumeração de interfaces para descobrir quais os dispositivos que implementam a interface de porta serial. A SetupAPI vai nos ajudar com essa tarefa. Mas antes de darmos uma olhada no fonte, vamos resolver uma coisinha. Tenho visto diferentes maneiras de usar GUID_DEVINTERFACE_COMPORT, uns incluem o header de Kernel-Mode Ntddser.h, outros definem o GUID na unha, mas qual seria o jeito correto?

Definindo o GUID de interface

Tudo começa com a chamada à rotina SetupDiGetClassDevs() que vai reunir informações sobre o grupo de dispositivos que correspondem aos critérios de busca adotados nos parâmetros. Vamos querer dispositivos que implementem a interface identificada por GUID_DEVINTERFACE_COMPORT, mas se simplesmente fizermos a chamada como mostra abaixo...

hDevInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT,
                                  NULL,
                                  NULL,
                                  DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);

...obteremos o erro exibido em seguida.

1>z:\sources\samples\enumserialport.obj : error LNK2001: unresolved external
 symbol _GUID_DEVINTERFACE_COMPORT

Isso ocorre porque GUID_DEVINTERFACE_COMPORT está declarado em WinIoCtl.h. Lembre-se que este header é indiretamente incluído por Windows.h quando o símbolo WIN32_LEAN_AND_MEAN não é definido, como já comentei neste outro post. Mas mesmo incluindo este header ainda temos o mesmo problema. Vamos olhar isso um pouco mais de perto.

No header WinIoCtl.h temos:

DEFINE_GUID(GUID_DEVINTERFACE_COMPORT, 0x86e0d1e0L, 0x8089, 0x11d0,
            0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73);

Mas o que é DEFINE_GUID afinal?

Em GuidDef.h temos:

#ifdef INITGUID
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
        EXTERN_C const GUID DECLSPEC_SELECTANY name \
                = { l, w1, w2, { b1, b2,  b3,  b4,  b5,  b6,  b7,  b8 } }
#else
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
    EXTERN_C const GUID FAR name
#endif // INITGUID

Então agora nos vem o sonoro "Aaaah táaa!". GUID_DEVINTERFACE_COMPORT é uma constante que é definida quando o símbolo INITGUID é definido, caso contrário, esta constante é apenas declarada. O símbolo INITGUID é definido no header InitGuid.h. Assim, quando você quiser utilizar os GUIDs declarados com DEFINE_GUID, você terá que em um dos seus módulos incluir o header Initguid.h antes de Windows.h. Como nosso exemplo só tem um módulo, então fica fácil. Como de costume, todo código fonte está contido num exemplo disponível para download.

//-f--> Vamos ter que colocar estes includes na ordem certa
//      para que o GUID que identifica a interface
//      GUID_DEVINTERFACE_COMPORT seja definido.
#include 
#include 
#include 

Enumerando interfaces

/****
***     EnumSerialInterfaces
**
**      Rotina que enumera dispositivos que
**      implementam a interface de porta serial
*/
 
DWORD EnumSerialInterfaces(void)
{
    CHAR                        szFriendlyName[100];
    HDEVINFO                    hDevInfoSet = NULL;
    SP_DEVICE_INTERFACE_DATA    DevInterfaceData;
    SP_DEVINFO_DATA             DevInfoData;
    DWORD                       dwReturn,
                                dwInterfaceIndex = 0;
    try
    {
        //-f--> Reunindo informações sobre dispositivos que implementam
        //      a interface desejada que estejam presentes no
        //      momento em que esta rotina é chamada.
        hDevInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT,
                                          NULL,
                                          NULL,
                                          DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
 
        if (hDevInfoSet == INVALID_HANDLE_VALUE)
            throw GetLastError();
 
        DevInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
 
        //-f--> Agora enumera cada uma das interfaces
        while (SetupDiEnumDeviceInterfaces(hDevInfoSet,
                                           0,
                                           &GUID_DEVINTERFACE_COMPORT,
                                           dwInterfaceIndex++,
                                           &DevInterfaceData))
        {
            DevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
 
            //-f--> Para cada uma das interfaces, obtemos o
            //      device que a implementa.
            if (SetupDiGetDeviceInterfaceDetail(hDevInfoSet,
                                                &DevInterfaceData,
                                                NULL,
                                                0,
                                                NULL,
                                                &DevInfoData))
                throw GetLastError();
 
            //-f--> Agora apenas obtemos o nome camarada do dispositivo
            if (!SetupDiGetDeviceRegistryProperty(hDevInfoSet,
                                                  &DevInfoData,
                                                  SPDRP_FRIENDLYNAME,
                                                  NULL,
                                                  (PBYTE)szFriendlyName,
                                                  sizeof(szFriendlyName),
                                                  NULL))
                throw GetLastError();
 
            //-f--> Printf neles...
            printf("%d) %s\n",
                   dwInterfaceIndex,
                   szFriendlyName);
        }
    }
    catch(DWORD dwError)
    {
        //-f--> Oops!
 
        printf("Error %d on trying enumerate device interfaces.\n",
               dwError);
 
        dwReturn = dwError;
    }
 
    //-f--> Libera as informações obtidas
    if (hDevInfoSet)
        SetupDiDestroyDeviceInfoList(hDevInfoSet);
 
    return dwReturn;
}

Com essa implementação, teremos a seguinte saída.


Legal, mas não era bem isso...

Muito bem. As portas foram enumeradas, mas como eu passaria uma string dessas para a função CreateFile()? Terei que ficar interpretando essa string para pegar a parte "COM1" que está entre parênteses?

Na verdade existem meios de você obter o Symbolic Link dos dispositivos também utilizando funções da SetupAPI, mas eu imagino que o que você queria é o mesmo que eu quero. Preencher um Combo Box com o nome simples das portas seriais, como qualquer programa normal.

O port name, que é a string "COMx" que estamos procurando, está escrito como um valor de registro na chave do dispotivo. Todo driver de porta serial tem que ter esse valor conforme mostra esta página. Para obter a chave de registro referente ao dispositivo, utilizaremos uma outra rotina da SetupAPI. O fonte abaixo vai enumerar as portas seriais da maneira que estamos querendo.

/****
***     EnumSerialPorts
**
**      Rotina que enumera dispositivos que
**      implementam a interface de porta serial e
**      imprime um nome que não é camarada mas que
**      ainda servem para alguma coisa.
*/
 
DWORD EnumSerialPorts(void)
{
    CHAR                        szPortName[10];
    HDEVINFO                    hDevInfoSet = NULL;
    SP_DEVICE_INTERFACE_DATA    DevInterfaceData;
    SP_DEVINFO_DATA             DevInfoData;
    DWORD                       dwReturn,
                                dwSize,
                                dwInterfaceIndex = 0;
    HKEY                        hKey;
 
    try
    {
        //-f--> Reunindo informações sobre dispositivos que implementam
        //      a interface desejada que estejam presentes no
        //      momento em que esta rotina é chamada.
 
        hDevInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT,
                                          NULL,
                                          NULL,
                                          DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
 
        if (hDevInfoSet == INVALID_HANDLE_VALUE)
            throw GetLastError();
 
        DevInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
 
        //-f--> Agora enumera cada uma das interfaces
        while (SetupDiEnumDeviceInterfaces(hDevInfoSet,
                                           0,
                                           &GUID_DEVINTERFACE_COMPORT,
                                           dwInterfaceIndex++,
                                           &DevInterfaceData))
        {
            DevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
 
            //-f--> Para cada uma das interfaces, obtemos o
            //      device que a implementa.
            if (SetupDiGetDeviceInterfaceDetail(hDevInfoSet,
                                                &DevInterfaceData,
                                                NULL,
                                                0,
                                                NULL,
                                                &DevInfoData))
                throw GetLastError();
 
            //-f--> Aqui obtemos a chave de registro do
            //      device que implementa a interface.
            hKey = SetupDiOpenDevRegKey(hDevInfoSet,
                                        &DevInfoData,
                                        DICS_FLAG_GLOBAL,
                                        0,
                                        DIREG_DEV,
                                        KEY_QUERY_VALUE);
 
            if (hKey == INVALID_HANDLE_VALUE)
                throw GetLastError();
 
            //-f--> Aqui obtemos o valor PortName do Registry
            dwSize = sizeof(szPortName);
            dwReturn = RegQueryValueEx(hKey,
                                       "PortName",
                                       NULL,
                                       NULL,
                                       (LPBYTE)szPortName,
                                       &dwSize);
            RegCloseKey(hKey);
 
            if (dwReturn != ERROR_SUCCESS)
                throw dwReturn;
 
            //-f--> Printf neles...
            printf("%d) %s\n",
                   dwInterfaceIndex,
                   szPortName);
        }
    }
    catch(DWORD dwError)
    {
        //-f--> Oops!
        printf("Error %d on trying enumerate device interfaces.\n",
               dwError);
 
        dwReturn = dwError;
    }
 
    //-f--> Libera as informações obtidas
    if (hDevInfoSet)
        SetupDiDestroyDeviceInfoList(hDevInfoSet);
 
    return dwReturn;
}

Agora sim...


Colocar isso num combo box já é assunto para um outro blog. Até que para um desenvolvedor de drivers, a janela abaixo não está tão ruim assim.


Até mais! 😉

EnumSerialPort.zip