En este tutorial te daré los pasos para que construyas un sensor de distancia a través del Time-Of-Flight VL53L1X de STMicroelectronics y de un microcontrolador PIC18F13K50 funcionando como USB HID. La lectura de datos del lado de la PC se hará a través de Python.
Material necesario
- PIC18F13K50 - Documentación en Microchip
- Sensor de distancia VL53L1X - Documentación en STMicroelectronics
- Programador de PICs (yo todavía uso el PICKit3) - PICKit 5 de Microchip
- Analizador lógico de 8 canales económico para depurar la comunicación I2C
- MPLAB X como ambiente de desarrollo para el PIC - MPLAB® X IDE | Microchip Technology
- PyCharm como ambiente de desarrollo para Python en la PC - PyCharm: The only Python IDE you need
Proyecto completo en Gitlab
Todo lo necesario para llevar a cabo este pequeño proyecto está contenido en este proyecto en Gitlab:
Santiago Villafuerte Rmz. / USB_Distance_VL53L1X · GitLab
- Código fuente del PIC18F13K50
- EXTRA: Código fuente del MSP430G2231
- Esquemático y PCB diseñados en Kicad
- Software en Python para ploteo en tiempo real de la distancia vía USB
Las versiones que utilicé para los diversos softwares de desarrollo son como sigue aunque puedes usar software más reciente.
MPLABX-v6.20-windows-installer.exe
kicad-10.0.4-x86_64.exe
mla_v2018_11_26_windows_installer.exe
xc8-v4.00-full-install-windows-x64-installer.exe
Microchip.PIC18F-K_DFP.1.16.308.atpack
Proyecto origen de Microchip
Para poder generar el código fuente de mi proyecto usé como base el ejemplo de Microchip siguiente:
Este proyecto es fácil de comprender y de migrar hacia algún otro proyecto específico como el mío. Debo mencionar que el proyecto tiene algunos problemas con el compilador XC8 reciente de Microchip ya que declara unos #define que anulan al atributo __interrupt() de las funciones de interrupción del código. Esto hace que éstas NO sean linkeadas en el binario final. Las correcciones a hacer fueron como sigue:
- I2C-Temperature-Data-Logger/Firmware/USB/include/usb_ch9.h at main · MicrochipTech/I2C-Temperature-Data-Logger · GitHub Debes eliminar el #define __attribute__(a).
- I2C-Temperature-Data-Logger/Firmware/USB/include/usb_device.h at main · MicrochipTech/I2C-Temperature-Data-Logger · GitHub Debes eliminar el mismo define.
Afortunadamente, la gente de soporte de Microchip me apoyó a encontrar este defecto en el código y después de ello mi código funcionó correctamente.
Objetivo
Obtener las mediciones de distancia de un sensor VL53L1X a través del microcontrolador PIC18F13K50 usando el puerto I2C y enviar los datos obtenidos a una PC a través de la conexión USB del PIC en modo HID (Human Interface Device).
Conexiones
El sensor VL53L1X es un chip que mediante emisión láser (seguro para la vista) mide la distancia hacia algún objeto colocado frente a él. Estas distancias tienen una resolución de 1mm. El sensor tiene muchas funciones, entre ellas el obtener mediciones de hasta 4m. Para nuestro ejemplo, usaremos el sensor para distancias de hasta 1.3m con un tiempo de muestreo de 100ms (10Hz). Entre menor el rango máximo de distancia se desee, el tiempo de muestreo puede reducirse. Otras configuraciones son posibles, pero para mi caso de uso no las vi necesarias. Asegúrate de leer la API en lenguaje C de STMicroelectronics por si tienes más dudas.
STSW-IMG007 | Product - STMicroelectronics

Este sensor puede encontrarse ya soldado a una PCB con headers estándar de 2.54mm. La PCB comúnmente ya trae un regulador de voltaje que permite que el VL53L1X se comunique con microcontroladores de 3.3V o 5V. Debes revisar la documentación de la PCB que compres para evitar problemas de voltaje o dañar tu sensor de distancia.
La PCB cuenta con 6 pines:
- VIN - Voltaje de alimentación
- GND - Tierra
- SCL - Terminal de reloj de I2C
- SDA - Terminal de datos de I2C
- GPI01 - Terminal de salida del sensor mediante la cual informa al microcontrolador que una medición ya está lista
- XSHUT - Terminal de entrada del sensor mediante la cual el microcontrolador puede poner en reset al sensor con 0V
Este sensor es bastante configurable, incluso contiene registros de configuración para modificar el campo de visión del sensor (FOV). La configuración del sensor no es trivial ya que hay que cargar una lista de datos cuando éste enciende y sin ellos el sensor no funciona adecuadamente.
El microcontrolador que echará a andar el sensor es un PIC18F13K50. Lo elegí por su bajo precio y porque tiene prestaciones suficientes para mi aplicación básica: 8kB de ROM, 512B de RAM, puerto I2C maestro con capacidad de funcionar a 100kHz y puerto USB en modo device capaz de funcionar en Full Speed (12Mbps). Debo mencionar que depurar el microcontrolador NO es posible con el PICkit 3 que tengo por 2 razones: una es que los pines debug (ICSP_PGD e ICSP_PGC) están multiplexados con los pines USB (D+ y D-) y la otra es que se requiere un header de depuración para mi PICkit 3 que no conozco. Entonces, la mayoría de pasos para depurar los hice a través de un analizador lógico de 24MHz económico para observar la comunicación I2C entre el PIC y el sensor. Considera esto en tu diseño ya que te puede hacer pasar malos ratos. 😇

El microcontrolador se conectará con el sensor a través de los siguientes pines:
- PIC RB4 - VL SDA
- PIC RB5 - VL GPI01
- PIC RB6 - VL SCL
- PIC RB7 - VL XSHUT
La PCB que acompaña a mi VL53L1X ya cuenta con resistencias pull-up en todos sus pines digitales por lo que no fue necesario que activara las pull-up de mi PIC. Si tu PCB no trae pull-ups (si mides cuando están desconectadas y no ves un voltaje cercano a VIN) entonces considera modificar los registros WPUB del PIC para activarlas.
Recuerda no colocar cables muy largos entre el sensor y tu PIC ya que I2C está pensado para comunicación corta entre chips.
La conexión USB del PIC hacia la PC se hace de manera trivial usando los pines RA0 y RA1. IMPORTANTE: si vas a programar tu PIC, debes desconectar el cable USB para evitar problemas de voltaje con la PC.
En RC3 conecté un LED que nos va a indicar si la PC enumeró correctamente la conexión USB con nuestro PIC. Este LED lo activamos en el evento EVENT_CONFIGURED de los callbacks que la librería USB de Microchip pone a nuestra disposición. EVENT_CONFIGURED significa que la enumeración con la PC fue exitosa.
Había planeado utilizar SW1 como un selector de dirección de cada sensor de distancia USB conectado a la PC. Esto es para cuando se conecta más de un PIC a la PC y se pueda decidir qué PIC leer desde el software que esté leyendo las distancias. Al estar desarrollando el código mejor decidí que esto se puede hacer en firmware. Si modificamos el archivo usb_descriptors.c, específicamente la variable sd001, ahí podemos elegir un número de serie que el firmware compilado del PIC tendrá. Coloqué "0001" y cuando Python desee comunicarse con mi PIC 0001, la librería deberá buscar ese número de serie para encontrarlo. Es por eso que ya no es necesario soldar SW1.
Conecta el resto de componentes como el esquemático mostrado arriba.
Código fuente del PIC
El código fuente del PIC está organizado de la siguiente manera.
system.c
Este archivo contiene la configuración de los registros de operación del PIC18F13K50 para que opere con un cristal externo de 12MHz y eleve su frecuencia de operación a 48MHz con PLL 4x (12MIPS para la frecuencia de CPU). Este archivo también contiene la función de interrupción que se encargará de atender todos los eventos del puerto USB.
Todos los eventos de USB se atienden por interrupción. Hacerlo por polling complica el código porque las funciones de I2C que empleamos son bloqueantes y si desatendemos las peticiones USB de la PC por mucho tiempo, nos desenumera la conexión USB.
usb_descriptors.c
Este archivo contiene las estructuras USB con las que el PIC se presenta en el handshake con la PC para que ella lo controle como un dispositivo HID. Aquí se declara el Vendor ID (0x04D8 de Microchip) y el Product Id (0x0ABC para nuestro ejemplo). En este archivo también se define el número de serie único con el que flashearemos al PIC para que la PC pueda hablar con diversos PICs conectados a ella midiendo distancias.
Para lograr esto, se debe modificar el string sd001 de "0001" a cualquier otro número deseado. Cuando Python busca al PIC, lo hará con el VID, PID y número de serie deseados.
Dejé 8 versiones del firmware del PIC con los números 0001 al 0008 ya disponibles en este directorio:
firmware/PIC18/reflash_images · main · Santiago Villafuerte Rmz. / USB_Distance_VL53L1X · GitLab
usb_config.h
Aquí se definen:
- Tamaño de los endpoints HID (buffers de comunicación entre PC y PIC), los cuales asigné en 8 bytes cada uno
- Modo de operación de la librería USB de Microchip (por interrupción)
- Habilitación de pull-ups internas del PIC para USB (determinan si el PIC se presenta como Low Speed o Full Speed)
- Modo de operación del PIC (Full Speed a 12Mbps)
- Y otros detalles de la enumeración USB
i2c_init.c e i2c_lowlevel.c
Estos archivos echan a andar el módulo I2C del PIC en modo maestro para comunicarnos con el sensor de distancia. Incluí la función sendI2CCommand() para poder entablar envíos y recepciones de manera sencilla. Esta función maneja automáticamente los pulsos de start, stop y ack/nack necesarios en el protocolo I2C.
HardwareProfile.h
Aquí se define el pinout del PIC a través de defines. También definimos la dirección I2C del sensor VL53L1X (0x52 o 0x29, dependiendo de cómo leas el dato electrónicamente). Aquí también se define la velocidad del reloj del PIC.
main.c
Aquí incluyo el código que comunica al PIC con el sensor de distancia. main() comienza configurando el hardware, levantando el periférico USB y configurando el pinout específico de mi PIC en UserSystemInit(). La terminal XSHUT se pone en 0V para reinicializar al sensor de distancia. GPI01 se pone como entrada aunque no utilicé este pin ya que para determinar si el sensor ya terminó una lectura, lo hago a través de comandos I2C.
La velocidad de I2C se configura a 100kHz poniendo 119 en el registro SSPADD del PIC. Teniendo FOSC = 48MHz, la frecuencia de operación de I2C es de 100kHz.

La inicialización del sensor Vl53L1X contempla varios pasos:
- Poner en 0V el pin XSHUT y esperar un tiempo (yo exageré con 1s)
- Poner en 5V el pin XSHUT y esperar un tiempo (también exageré con 1s)
- Cargar todos los valores default de registros del sensor desde la dirección 0x2D hasta la 0x8D
- Detener el ranging que el sensor pudiera estar llevando a cabo
- Pedir el boot state del sensor para ver si está listo para trabajar; si no está listo hay que esperarlo
- Opcional: pedir la versión del sensor; en mi caso me contesta 0xEA 0xCC 0x10
- Configurar el modo corto de distancia (short 1.3m) mandando los valores necesarios para 6 registros; si necesitas otros valores, puedes revisar la API del sensor
- Configurar el periodo de muestreo del sensor en 100ms ajustando una calibración; cabe señalar que mi PIC NO tiene hardware para punto flotante, por lo que empleé un truco para usar operaciones de enteros. Si usaba punto flotante, el consumo de ROM incrementaba en 12%

- Por último se limpia cualquier interrupción del sensor que pudiera estar activa
La comunicación de I2C deberá verse como sigue:

Primer dato a escribir en sensor (0x29 o 0x52, dependiendo de si lees desde el primer bit o no) en registro 0x2D es 0x00 y el sensor envía ACK al recibirlo.

El comando 0x30 (GPIO_HV_MUX__CTRL) cambiando la polaridad de la interrupción. GPI01 cambia de flanco.

Comando 0x31 (GPIO__TIO_HV_STATUS) preguntando si la lectura ya está lista. Sensor contesta 0x02 (bit 0 en 0) diciendo que no. El PIC espera un rato y vuelve a preguntar.

PIC pregunta de nuevo si la lectura está lista y el sensor contesta con 0x03 (bit 0 en 1) diciendo que sí.

El PIC lee con el comando 0x89 (VL53L1_RESULT__RANGE_STATUS) que indica si la lectura del sensor es válida o no (una respuesta de 0x09 dice que sí, de acuerdo a la tabla status_rtn[]). El lector regresa errores cuando la distancia a leer es mayor a su rango y es crítico leer el resultado de la medición con 0x89 antes de ir a leer la distancia. Luego se lee con 0x96 (VL53L1_RESULT__FINAL_CROSSTALK_CORRECTED_RANGE_MM_SD0) la distancia. El sensor nos la entrega en 2 bytes (uint16_t) en big endian siendo ésta una medición directa en mm. Al terminar limpiamos la interrupción con 0x86 (SYSTEM__INTERRUPT_CLEAR) y reiniciamos la espera del dato.
Si sí hubo una lectura correcta, ponemos la distancia en el endpoint de HID y la envíamos a la PC con HIDTxPacket(). Si la lectura fue incorrecta, no se envían datos a la PC.
El consumo final de recursos de mi PIC es como sigue:

Python leyendo al PIC18F13K50
Desarrollar el script de Python es muy sencillo (Gemini me ayudó). Se hace uso de la librería HID API de Python que funciona para Windows, Linux o Mac indistintamente. Se abre un dispositivo HID con el VID 0x04D8, un PID 0x0ABC y el número serial que se haya flasheado en el PIC (0001 es el default). Basta con leer los datos del micro y reacomodar el endianness de los bytes para obtener la distancia.

Empleé la librería pyqtgraph para el ploteo en tiempo real de los datos.

Gracias por leer este tutorial. 😎