Phytonizando el proyecto PicUSB

El proyecto PicUSB lo encontré cuando buscaba una forma de comunicar a un PIC con una PC por medio del estándar USB directamente y no por medio de la emulación CDC (PDF) que crea un puerto RS232 virtual.

Proyecto original

Los archivos del proyecto los bajé hace mucho tiempo  de aquí pero el driver usado es una versión vieja que no soporta windows 7 (debido a las limitaciones propias del driver y la librería de microchip). Este ejemplo está basado en otro proyecto que sí funciona en Windows Vista en la dirección http://www.todopic.com.ar/foros/index.php?topic=2260.0 está la nueva versión y esta hecha por “J1M”.
Lo que hice fue lo mismo que hizo el autor del post gu1llermo en Visual C pero en python: tomar de la API de microchip mpusbapi.dll versión 1.0.0.0 las funciones para comunicarse con el PIC. Las funciones se pueden tomar del código fuente proporcionado por gu1llermo o desde el archivo funciones_dll.txt.
Si se tiene instalado mingw instalado en Windows, se pueden obtener los nombres de las funciones con:
objdump -p mpusbapi.dll
El proyecto original utiliza una aplicación llamada PicUSB que se muestra a continuación:

Al presionar el botón Nro. de dispositivos busca los dispositivos conectados que tengan el valor PID = 0925 y VID = 1231, si encuentra alguno entonces se mostrará un 1 al lado, si no, mostrará un 0. En la primer caja de texto se escribe un valor que representa a un comando, si el número escrito es 1 el LED conectado al PIC se enciende, si es un 2 el LED  se apaga, el comando enviado aparece al lado. Las cajas de texto restantes contienen “parámetros”, que son solo números que al ser enviados con el botón Transmite2 regresa su valor multiplicado por 2.

El circuito original es el siguiente:


Pero este circuito tiene algunos problemas: no siempre funciona y a veces deja de funcionar completamente.

Proyecto pythonizado

Primero necesitamos comprender el código fuente:



Se declaran los fuses y la velocidad del reloj. Cambie el reloj interno por un cristal de 12 mhz, así que los fuses quedan de la siguiente manera:

#fuses HSPLL,NOWDT,NOPROTECT,NOLVP,NODEBUG,USBDIV,PLL3,CPUDIV1,VREGEN,NOPBADEN

los valores de configuración del oscilador para que funcione el USB se ven en la tabla  “TABLE 2-3: OSCILLATOR CONFIGURATION OPTIONS FOR USB OPERATION” en la hoja de datos.


Estos valores son:

HSPLL, PLL3, CPUDIV1

y dan a partir de un cristal de 12 mHz una velocidad de reloj de 48 mHz. Con la palabra clave USBDIV indicamos que utilizaremos el PLL del uC, si no lo ponemos, entonces se usaría el cristal externo como fuente para el USB.

Después definimos los parámetros de la comunicación USB:

     No es un dispositivo HID.
     Habilitamos el endpoint 1 de transmisión.
     Habilitamos el endpoint 1 de recepción.
     El numero de bytes que serán enviados es de 5.
     El numero de bytes que serán recibidos es de 5.
     La transferencia de salida es tipo BULK.
    La transferencia de entrada es tipo BULK.

esto traducido a código es:
#define USB_HID_DEVICE     FALSE             //deshabilitamos el uso de las directivas HID
#define USB_EP1_TX_ENABLE  USB_ENABLE_BULK   //turn on EP1(EndPoint1) for IN bulk/interrupt transfers
#define USB_EP1_RX_ENABLE  USB_ENABLE_BULK   //turn on EP1(EndPoint1) for OUT bulk/interrupt transfers
#define USB_EP1_TX_SIZE    5                 //size to allocate for the tx endpoint 1 buffer
#define USB_EP1_RX_SIZE    5                 //size to allocate for the rx endpoint 1 buffer

Definimos los alias para las funciones de encendido y apagado, así como el PIN del LED, y le asignamos a cada byte de las variables (5 bytes por variable), un significado:
#define LED       PIN_D2
#define LED_ON    output_high
#define LED_OFF   output_low

#define comando   recibe[0]
#define param1    recibe[1]
#define param2    recibe[2]
#define param3    recibe[3]
#define param4    recibe[4]
#define eco       envia[0]// Hace un eco del comando recibido
#define result1   envia[1]
#define result2   envia[2]
#define result3   envia[3]
#define result4   envia[4]

Ahora seguimos con las funciones USB del uC
usb_init();                      //inicializamos el USB

   usb_task();                      //habilita periferico usb e interrupciones
   usb_wait_for_enumeration();      //esperamos hasta que el PicUSB sea configurado por el host
Tenemos que tener en cuenta que la instrucción usb_init() se utiliza cuando el uC esta alimentado por el bus, y esta ligada también a la instruccion usb_task() que es la que habilita las interrupciones. Como en este ejemplo el uC se alimenta por el bus y no hace otras operaciones mientras espera datos por el puerto USB, se puede dejar usb_task() fuera del bucle principal, si el circuito se alimentara de una fuente externa, entonces se tiene que estar llamando usb_task() para comprobar si el uC esta conectado al PC y se usa la instrucción usb_init_cs(), para que no espere datos por el puerto USB y pueda realizar otras tareas.Por último esperamos que la PC reconozca al PIC.

Ya que el PIC esta conectado y enumerado podemos iniciar el bucle principal. Con la instrucción usb_kbhit(endpoint) verificamos si el endpoint de SALIDA tiene datos del host.
usb_get_packet(1, recibe, 5);
con la instrucción anterior recogemos los datos del endpoint 1, en la variable recibe, y deben ser 5 bytes recibidos en total.

Por último almacenamos el comando recibido en la variable eco que será enviado a la PC junto con los valores multiplicados por dos. Si el comando recibido es un 1 (0x01), entonces se enciende el LED, si el comando recibido es un 2 (0x02) entonces el LED se apaga.
eco = comando;  //Regresa el comando recibido
            result1 = param1*2;
            result2 = param2*2;
            result3 = param3*2;
            result4 = param4*2;
            
            switch (comando)
            {
               case 1:// Enciende LED
               LED_ON(LED);
               break;
               
               case 2:// Apaga LED
               LED_OFF(LED);
               break;  
            }
        usb_put_packet(1, envia, 5, USB_DTS_TOGGLE); 

con la instruccion usb_put_packet(endpoint, var, 5, param) se envían los datos de la variable var que debe ser de 5 bytes de tamaño por el endpoint.

Se debe tener muy en cuenta en que parte del código se pone la instrucción para enviar datos por el USB, por que si tarda demasiado en ejecutarse o el host no espera lo suficiente, entonces la comunicación no se realizará.

Para la configuración de los descriptores USB del PIC, se toma el archivo de ejemplo usb_desc_scope.h y se modifica el PID y el VID, además de los descriptores String
0x25,0x09,           //vendor id (0x0925)
0x31,0x12,           //product id, (0x1231)

En los descriptores String no solo se cambia el texto sino el tamaño del texto (length of string index) como se ve a continuación:
char const USB_STRING_DESC[]={
   //string 0
         4, //length of string index
         USB_DESC_STRING_TYPE, //descriptor type 0x03 (STRING)
         0x09,0x04,   //Microsoft Defined for US-English
   //string 1 --> la compañia del producto ???
         8, //length of string index
         USB_DESC_STRING_TYPE, //descriptor type 0x03 (STRING)
         'J',0,
         '1',0,
         'M',0,
   //string 2 --> nombre del dispositivo
         22, //length of string index
         USB_DESC_STRING_TYPE, //descriptor type 0x03 (STRING)
         'J',0,
         '1',0,
         'M',0,
         ' ',0,
         'P',0,
         'i',0,
         'c',0,
         'U',0,
         'S',0,
         'B',0
};

Se cambió el circuito original añadiéndole un capacitor de 470 uF al pin VUSB y un cristal de 12 mHz.
Aplicación en python
Primero hice una aplicación en Python en modo texto para probar la ctypes y la API de Microchip:

cada número se lee desde la línea de comandos y debe ser un número de un solo dígito, este número es un tipo String, que se convierte a su valor ASCII y se le resta 0x30 de manera que para los primeros 5 números quedaría así:

Número ASCII ASCII - 0x30
1 0x31 0x01
2 0x32 0x02
3 0x33 0x03
4 0x34 0x04
5 0x35 0x05

La variable cadena es tipo String y contiene todos los bytes que han sido ingresados y convertidos con a un String con chr.

Se crean las variables necesarias para la transmisión:
SendData = ctypes.create_string_buffer(cadena) 
SentDataLength = (ctypes.c_ulong*1)() 
ctypes.cast(SentDataLength, ctypes.POINTER(ctypes.c_ulong)) 
SendDelay = 10 
SendLength = 5
SendData contiene la cadena que será mandada por el puerto USB. Esta variable se crea con la instrucción create_string_buffer() por que en la función original de la API de Microchip no se envía la cadena directamente, sino que es un puntero a una cadena. Este es el equivalente a crear un puntero a una cadena.
SendDataLength es un tipo long que contiene el numero de caracteres que han sido enviados. Como en la función original, se pasa como parámetro un puntero, se convierte con la función cast().
SendDelay es el numero de milisegundos que esperará el host antes de cortar la transmisión.
SendLength es el número de bytes que serán enviados.

Después se crean las variables necesarias para la recepción:
ReceiveData = ctypes.create_string_buffer(5) 
ReceiveLength = (ctypes.c_ulong*1)() 
ctypes.cast(ReceiveLength, ctypes.POINTER(ctypes.c_ulong)) 
ReceiveDelay = 10 
ExpectedReceiveLength = 5
ReceiveData es la variable que contendrá a los datos recibidos del PIC.
ReceiveLength es la variable que contendrá el número de datos recibidos.
ReceiveDelay es el número de milisegundos que el host esperará para recibir los datos.
ExpectedReceiveLength es el numero de bytes que el host espera recibir.

Antes de iniciar la transmisión y recepción de datos se debe de cargar la API con la instrucción CDLL().
mcpUsbApi = ctypes.CDLL("mpusbapi.dll") #con WinDLL marca error 
se obtiene el numero de dispositivos, se abre el pipe de SALIDA y el pipe de ENTRADA
devcount = mcpUsbApi._MPUSBGetDeviceCount(vid_pid_norm) 
myOutPipe = mcpUsbApi._MPUSBOpen(selection, vid_pid_norm, out_pipe, 0, 0) 
myInPipe = mcpUsbApi._MPUSBOpen(selection, vid_pid_norm, in_pipe, 1, 0)
con:
_MPUSBOpen(nro de dispositivo,vid&pid, numero de endpoint, tipo de endpoint, parámetros)
el tipo de endpoint puede ser de entrada (1) o de salida (0).
Por último se transmiten los datos leídos de la línea de comando, y se leen los datos recibidos del PIC, para después imprimirse:
mcpUsbApi._MPUSBWrite(myOutPipe,SendData, SendLength, SentDataLength, SendDelay)
#time.sleep(1) ## si se tarda mucho en ejecutar la sentencia usb_put_packet
mcpUsbApi._MPUSBRead(myInPipe, ReceiveData, ExpectedReceiveLength, ReceiveLength, ReceiveDelay)
mcpUsbApi._MPUSBClose(myOutPipe)
mcpUsbApi._MPUSBClose(myInPipe) 

print 
print "Version de la API: " + str(hex(version)[2:])
print "Numero de dispositivos: " + str(devcount)
print "Comando recibido: " + str(ord(ReceiveData[0]))
print "Param1 resultado: " + str(ord(ReceiveData[1]))
print "Param2 resultado: " + str(ord(ReceiveData[2]))
print "Param3 resultado: " + str(ord(ReceiveData[3]))
print "Param4 resultado: " + str(ord(ReceiveData[4]))
Aplicación gráfica
Creamos en QtDesigner la interfaz:
Modificamos las propiedade inputMask = 9  y maxLength = 1 de las cajas de texto para que solo acepten un número.
Creamos un archivo en python desde donde serán llamadas las funciones de la API de Microchip:
import ctypes

vid_pid_norm = "vid_0925&pid_1231"
out_pipe = "\\MCHP_EP1"
in_pipe = "\\MCHP_EP1"
selection = 0
global mcpUsbApi
global myInPipe
global myOutPipe

def loadAPI():
    global mcpUsbApi
    mcpUsbApi = ctypes.CDLL("mpusbapi.dll") #con WinDLL marca error
    #loadAPI

def dispositivos():
    global mcpUsbApi
    devcount = mcpUsbApi._MPUSBGetDeviceCount(vid_pid_norm)
    return devcount
    #dispositivos

def openPipes():
    global mcpUsbApi
    global myInPipe
    global myOutPipe
    
    myOutPipe = mcpUsbApi._MPUSBOpen(selection, vid_pid_norm, out_pipe, 0, 0)
    myInPipe = mcpUsbApi._MPUSBOpen(selection, vid_pid_norm, in_pipe, 1, 0)
    #openPipes

def closePipes():
    global mcpUsbApi
    global myInPipe
    global myOutPipe
    
    mcpUsbApi._MPUSBClose(myOutPipe)
    mcpUsbApi._MPUSBClose(myInPipe)
    #closePipes
    
def sendPacket(SendData):
    global mcpUsbApi
    global myInPipe
    global myOutPipe
    
    SendDelay = 10
    SendLength = 5 #es un valor fijo por que en el pic es fijo
    SentDataLength = (ctypes.c_ulong*1)()
    ctypes.cast(SentDataLength, ctypes.POINTER(ctypes.c_ulong)) 
    mcpUsbApi._MPUSBWrite(myOutPipe,SendData, SendLength, SentDataLength, SendDelay)
    return SentDataLength[0]
    #sendPacket

def receivePacket():
    global mcpUsbApi
    global myInPipe
    global myOutPipe
    ReceiveData = ctypes.create_string_buffer(5)
    ExpectedReceiveLength = 5 #es un valor fijo por que en el pic es fijo
    ReceiveDelay = 10
    ReceiveLength = (ctypes.c_ulong*1)()
    ctypes.cast(ReceiveLength, ctypes.POINTER(ctypes.c_ulong)) 
    mcpUsbApi._MPUSBRead(myInPipe, ReceiveData, ExpectedReceiveLength, ReceiveLength, ReceiveDelay)
    recibido = ReceiveData[0] + str(ReceiveData[1]) + str(ReceiveData[2]) + \
        str(ReceiveData[3])  + str(ReceiveData[4])    
    return recibido
    #ReceivePacket

Y al final el programa principal:
import sys
import PPicUSBAPI
from PyQt4 import QtCore, QtGui
from Ui_PPicUSBgui import Ui_mainWindow


class PPicUSB(QtGui.QMainWindow):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.ui = Ui_mainWindow()
        self.ui.setupUi(self)
        
        #connect
        self.ui.btnDispositivos.clicked.connect(self.btnDispositivosClicked)  
        self.ui.btnTransmite.clicked.connect(self.btnTransmiteClicked)
        PPicUSBAPI.loadAPI()
    #__init__
    
    def btnDispositivosClicked(self):
        nDisp = PPicUSBAPI.dispositivos()
        self.ui.lblDispositivos.setText(str(nDisp))
    #btnDispositivosClicked

    def btnTransmiteClicked(self):
        comando = str(self.ui.txtComando.text())
        param1 = str(self.ui.txtParam1.text()) 
        param2 = str(self.ui.txtParam2.text()) 
        param3 = str(self.ui.txtParam3.text()) 
        param4 = str(self.ui.txtParam4.text()) 
        
        comando = ord(comando) - 0x30
        param1 = ord(param1) - 0x30
        param2 = ord(param2) - 0x30
        param3 = ord(param3) - 0x30
        param4 = ord(param4) - 0x30
        cadena = chr(comando) + chr(param1) + chr(param2) + chr(param3) + chr(param4)
        
        PPicUSBAPI.openPipes()
        PPicUSBAPI.sendPacket(cadena)
        recibido  = PPicUSBAPI.receivePacket()
        PPicUSBAPI.closePipes()
        
        comandoRec = ord(recibido[0])
        paramRec1 = ord(recibido[1])
        paramRec2 = ord(recibido[2])
        paramRec3 = ord(recibido[3])
        paramRec4 = ord(recibido[4])
        self.ui.lblComando.setText(str(comandoRec))
        self.ui.lblResult1.setText(str(paramRec1))
        self.ui.lblResult2.setText(str(paramRec2))
        self.ui.lblResult3.setText(str(paramRec3))
        self.ui.lblResult4.setText(str(paramRec4))
    #btnTransmiteClicked

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    myapp = PPicUSB()
    myapp.show()
    sys.exit(app.exec_())

El resultado final se muestra a continuación:
 
Con esto ya podemos usar python con el PIC por medio del USB.
Compatibilidad con otros sistemas operativos
El ejemplo anterior no funciona en Windows Vista ni en Windows 7. Para que funcione debemos instalar el nuevo driver y utilizar la librería mpusbapi.dll version 6.0. Esa se encuentra dentro del directorio USB\Tools\MCHPUSB Custom Driver despues de instalar las Microchip application libraries.

Descargar ejemplo

2 comentarios:

Democrito de Abdera dijo...

Hola amigo,

Antes de nada felicitarte por los estupendo trabajos que publicas. Están concienzudamente currados.

Me interesa "pitonizar" el tema USB en modo bulk transfer. Hay un parte de la página en la que creo que debería salir un código y al menos a mi no me sale. Es en la que controlas el tema en modo texto, donde dices:

<<
Primero hice una aplicación en Python en modo texto para probar la ctypes y la API de Microchip:
Mostrar/ocultar
>>

Cuando hago clic a ese botón en concreto "Mostrar/Ocultar" no me sale nada; los demás sí funcionan. ¿Hay alguna manera de poder ver ese código?

Muchas gracias!

Democrito de Abdera dijo...

Al final he conseguido hacerlo funcionar y lo hace perfecto. Muchas gracias por esta publicación, muy necesaria para mi y que casi no hay información en Inet, y tu artículo es el único en español, que además es el mejor artículo que he leído en cualquier idioma.

Quiero añadir varias cosas y evitar quebraderos de cabeza para quien desee hacer un proyecto parecido:

1.) En versiones Python 3.x no funciona. Tuve que instalar una versión anterior, en mi caso la 2.7.6

2.) Como la DLL (mpusbapi.dll) está diseñada para 32 bits, no puede funcionar en Python de 64 bits. Es decir, que cuando instales Python ha de ser de 32 bits y una versión anterior a las 3.x (por ejemplo la que uso, que es la ver: 2.7.6).

3.) La pregunta que hice arriba (primer post después del artículo) donde preguntaba por el código fuente porque no me salía lo resolví mirando el código fuente de la página, pude de esa forma extraer el programa y que pongo aquí. Es la opción en modo texto, primer programa:

import ctypes
#import time

out_pipe = "\\MCHP_EP1"
in_pipe = "\\MCHP_EP1"
vid_pid_norm = "vid_0925&pid_1231"
selection = 0 #numero de instancia. Por si hay mas dispositivos con igual pid&vid

##comandos extra para mantener la compatibilidad con el firmware
comando = ord(raw_input("Comando: ")) - 0x30
param1 = ord(raw_input("Param1: ")) - 0x30
param2 = ord(raw_input("Param2: ")) - 0x30
param3 = ord(raw_input("Param3: ")) - 0x30
param4 = ord(raw_input("Param4: ")) - 0x30

cadena = chr(comando) + chr(param1) + chr(param2) + chr(param3) + chr(param4)

SendData = ctypes.create_string_buffer(cadena)
SentDataLength = (ctypes.c_ulong*1)()
ctypes.cast(SentDataLength, ctypes.POINTER(ctypes.c_ulong))
SendDelay = 10
SendLength = 5

ReceiveData = ctypes.create_string_buffer(5)
ReceiveLength = (ctypes.c_ulong*1)()
ctypes.cast(ReceiveLength, ctypes.POINTER(ctypes.c_ulong))
ReceiveDelay = 10
ExpectedReceiveLength = 5

mcpUsbApi = ctypes.CDLL("mpusbapi.dll") #con WinDLL marca error
version = mcpUsbApi._MPUSBGetDLLVersion()
devcount = mcpUsbApi._MPUSBGetDeviceCount(vid_pid_norm)
myOutPipe = mcpUsbApi._MPUSBOpen(selection, vid_pid_norm, out_pipe, 0, 0)
myInPipe = mcpUsbApi._MPUSBOpen(selection, vid_pid_norm, in_pipe, 1, 0)

mcpUsbApi._MPUSBWrite(myOutPipe,SendData, SendLength, SentDataLength, SendDelay)
#time.sleep(1) ## si se tarda mucho en ejecutar la sentencia usb_put_packet
mcpUsbApi._MPUSBRead(myInPipe, ReceiveData, ExpectedReceiveLength, ReceiveLength, ReceiveDelay)
mcpUsbApi._MPUSBClose(myOutPipe)
mcpUsbApi._MPUSBClose(myInPipe)

print
print "Version de la API: " + str(hex(version)[2:])
print "Numero de dispositivos: " + str(devcount)
print "Comando recibido: " + str(ord(ReceiveData[0]))
print "Param1 resultado: " + str(ord(ReceiveData[1]))
print "Param2 resultado: " + str(ord(ReceiveData[2]))
print "Param3 resultado: " + str(ord(ReceiveData[3]))
print "Param4 resultado: " + str(ord(ReceiveData[4]))

Publicar un comentario