Todo sobre USB-C: Respondiendo a la DP de bajo nivel

La última vez, configuramos el FUSB302 para recibir mensajes USB PD, y recibimos con éxito un mensaje de "anuncio de capacidad" de una fuente de alimentación USB-C. Ahora abrimos la especificación PD, analizamos el mensaje y creamos una respuesta que hace que la fuente de alimentación nos proporcione el voltaje más alto disponible.

¿Cómo se ve el contenido del búfer?

>>> b b'xe0xa1a,x91x01x08,xd1x02x00x13xc1x03x00xdcxb0x04x00xa5@x06x00<!xdcxc0Hxc6xe7xc6x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00' 

Los ceros del final pueden parecer no significativos, y de hecho no lo son con una certeza del 99,99% - dicho esto, no descartes todo el final; uno de los bytes del principio codifica la longitud del mensaje. Leeremos primero esos bytes, y luego leeremos sólo exactamente lo que necesitemos, asegurándonos de que no estamos leyendo dos mensajes e interpretándolos como uno, y de que no estamos descartando ceros que forman parte del mensaje.

Hoy, escribiremos código que analice los mensajes justo después de leerlos del buffer FIFO - sin embargo, mantén este mensaje a mano como referencia, todavía; y si no tienes el hardware, puedes usarlo para probar tu mano en la decodificación de todos modos. Si quieres participar, puedes encontrar el código completo de hoy aquí.

Análisis de cabecera

El primer byte del buffer es 0xe0y en realidad no es parte de un mensaje PD que necesitemos analizar - es un token de "inicio de mensaje", y puedes encontrarlo en la sección "RX tokens" en la hoja de datos del FUSB302 página 29. Si estás buscando allí y no sabes lo que es SOP - para nuestros propósitos, SOP (sin ' o " al final) significa "este paquete es para un dispositivo al final del cable, y no dentro del cable"; estamos, efectivamente, al final de un cable y no dentro de uno. Otros bytes son, sin embargo, una parte significativa de un paquete USB-C, y ahí es donde quieres abrir la especificación PD.

La cabecera se describe en la sección 6.2.1.1 de la especificación PD 3.0 - página 109. Son dos bytes: en nuestro caso, son los xa1a parte de la representación bytearray de Python, 0xa1 0x61 en hexadecimal y 0b10100001 0b1100001 en binario. El primer byte contiene los bits 7-0 y el segundo byte contiene los bits 15-8 - se podría decir que cada parte de un mensaje PD viene al revés. La parte principal que nos interesa son los bits 14-12 - tome el segundo byte, desplácelo 4 a la derecha y enmascárelo con 0b111 para obtener la longitud del mensaje. En nuestro caso, (0x61 >> 4) & 0b111 es igual a 6.

Si la longitud del mensaje es igual a cero, hemos recibido un mensaje de control - éstos se describen en la sección 6.3, en la página 119 de la especificación. En el mensaje de ejemplo, la longitud es 6. No se trata de un número de bytes, sino del número de objetos de datos PD, también conocidos como PDO (Power Data Object). Cada uno de ellos tiene una longitud de cuatro bytes, y en nuestro caso, cada uno de ellos corresponde a un perfil PD. Además, hay un CRC al final del mensaje, que es de cuatro bytes. Afortunadamente, no necesitamos verificar el CRC - el FUSB302 ha verificado el CRC por nosotros; si el CRC no fuera correcto, no pondría el mensaje en el FIFO para que lo leyéramos en primer lugar.

¿Cuántos bytes más tenemos que leer? Ya hemos leído tres bytes, determinando que tenemos que leer seis objetos de datos de cuatro bytes, y luego un CRC de cuatro bytes. En total, este mensaje tiene 31 bytes. Leamos primero los objetos, luego leamos el CRC y descartémoslo. Lo más fácil probablemente sería leer del FIFO cuatro bytes a la vez - yo he leído todo el PDO y luego lo he dividido en mensajes después en mi propia implementación.

Obteniendo los Perfiles de Potencia

pdo_count = 6 pdos = [] for i in range(pdo_count): pdo = i2c.readfrom_mem(0x22, 0x43, 4) pdos.append(pdo) _ = i2c.readfrom_mem(0x22, 0x43, 4) # discarding the CRC 

Ahora, tenemos una lista de perfiles de potencia aún no analizados en pdos - Me referiré a ellos como PDOs por brevedad. Aquí, harías bien escribiendo una función separada para analizar un PDO, aunque sólo sea por razones de legibilidad.

El formato de los mensajes de datos se describe en la sección 6.4 de la especificación, página 129. Lo primero que se comprueba con un PDO es el tipo de datos, bits 30-31, o bits 7-6 del último byte del PDO tal y como lo recibimos. Hay cuatro tipos posibles - fijo (el más popular), batería y suministro variable, y el tipo PDO aumentado. Podemos limitarnos a procesar PDOs fijos por ahora, e ignorar con seguridad los otros tipos.

Si ya empiezas a analizar los PDOs, te darás cuenta de que tenemos cinco PDOs fijos y uno extendido. Debo decir que esto coincide con la marca de la fuente de alimentación USB-C con la que recibí este mensaje. Veamos la tabla 6-9 de los PDO en la página 132; es una tabla muy bonita y tiene todo lo que se puede necesitar. Analicemos el primer PDO.

00101100 10010001 00000001 00001000


La corriente máxima son los bits 0-9 - los dos últimos bits del byte 1, y luego el byte 0 entero. Voltaje es bits 19-10 - cuatro últimos bits del byte 2, y seis primeros bits del byte 1. Si esto es doloroso de leer, consulta este trozo de código que analiza PDOs en Python. Después de obtener los números de voltaje y corriente, multiplica el voltaje por 50 y la corriente por 10, para obtener milivoltios y miliamperios respectivamente.

>>> 0b0100101100 * 10 3000 >>> 0b0001100100 * 50 5000 

Oh, mira eso - tenemos 3000 y 5000, que, como habrás adivinado, significa 5 V a 3 A. La función de análisis de PDO para esta parte se puede encontrar aquí.

Solicitar un perfil de potencia

Ahora, tenemos los PDOs - desde 5 V hasta 20 V. Para pedir a la PSU uno de ellos, tenemos que elaborar un mensaje de solicitud. Y recuerda, para conseguir que la fuente de alimentación nos proporcione un voltaje más alto, tenemos que enviar nuestra respuesta rápidamente, antes de que la fuente de alimentación agote el tiempo de espera. Vamos, entonces, a escribir una función que elabora una respuesta y puede responder automáticamente con ella. Es un mensaje de cuatro bytes, con una cabecera de dos bytes - vamos a hacer una lista de seis ceros, modificarlos en su lugar, y luego enviarlos. Algo rápido y sucio como pdo = [0 for i in range(6)] hará maravillas.

Para empezar, vamos a referirnos a la especificación de la cabecera - ahora realmente tenemos que leer a través de los campos en la cabecera del mensaje y establecer los que necesitamos. De nuevo, sección 6.2.1.1, página 109. Para los bits 15-8 (pdo[1]), sólo tenemos que cambiar el número de objetos de datos. En nuestro caso, es 1 - estamos enviando un mensaje de datos con un único mensaje de petición PDO en su interior. Para los bits 7-0 (pdo[0]), necesitamos establecer la revisión de la especificación (bytes 7-6) a 0b11. También necesitamos establecer el tipo de mensaje de datos en los bytes 4-0: ver la tabla 6-6 en la página 128 para ello; en nuestro caso, es un mensaje de Petición, con el código 0b00010. Ah, y hay un campo "Message ID" que ahora podemos dejar en 0, pero que querrás incrementar para mensajes posteriores. Esto es todo lo que necesitamos de la cabecera - ahora, vamos a elaborar la solicitud real en los cuatro bytes restantes.

Los mensajes de petición se describen en la sección 6.4.2, página 141 - necesitará la tabla 6-21. Para solicitar un PDO, necesitamos conocer su índice - e incrementarlo en 1 antes de enviarlo. Así, 5 V @ 3 A es PDO 1, 9 V @ 3 A es PDO 2, y así sucesivamente. Vayamos a por el PDO de 9 V y pongamos 0b010 en los bits 31-28. La fuente de alimentación USB-C también querrá saber la corriente máxima y media que pensamos consumir. Como estamos experimentando, vamos a pedir algo así como 1 A, poniendo tanto la corriente máxima (bits 9-0) como la corriente de funcionamiento (bits 19-10) en 0b1100100. También te irá bien poner el bit 24 (bit 0 de pdo[5]) para desactivar la suspensión USB - por si acaso.

Ahora, ¡tenemos un mensaje! Sin embargo, no podemos simplemente meterlo en el FIFO. Tenemos que añadir y añadir dos secuencias de bytes que permiten que el FUSB302 sepa lo que pasa, conocidas como secuencias SOP y EOP (Inicio y Fin de Paquete, respectivamente) - consulte la hoja de datos del FUSB302 página 29, de nuevo. La secuencia SOP tiene cinco tokens y esencialmente transmite un preámbulo del mensaje - tres tokens SOP1, un token SOP2, y un token PACKSYM; necesitamos OR el token PACKSYM con la longitud de nuestro mensaje en bytes, seis en nuestro caso, haciéndolo 0x86. La secuencia EOP es JAM_CRC, EOP (token), TXOFF y TXON. No entiendo muy bien por qué estas secuencias exactas, pero me alegro de tener algunas pilas de PD de código abierto de las que copiar este comportamiento. Así que.., 0x12 0x12 0x12 0x13 0x86 antes del paquete, y 0xff 0x14 0xfe 0xa1 después.

Secuencia SOP, paquete, secuencia EOP - ponlos todos en un FIFO, y habremos enviado un mensaje Request. El flujo de trabajo general es sencillo: obtenemos capacidades, analizamos capacidades, elegimos la que nos gusta, creamos un mensaje de solicitud, lo enviamos y obtenemos la tensión. ¿La recompensa? Obtienes el voltaje de tu elección.

Un poco de depuración

Si no nos perdimos nada, sondeo VBUS mostrará que ha extraído con éxito el perfil de 9 V que acordamos probar. Si usted está experimentando cualquier contratiempo, de nuevo, aquí hay código de referencia en Python que puede utilizar, y aquí hay una referencia de transmisión I2C para el Pinecil. ¿Tienes problemas? Aquí hay algunos consejos.

Como es habitual con la depuración, print() te ayudarán bastante, hasta cierto punto. Por un lado, son indispensables, especialmente si eres meticuloso a la hora de convertir los datos a representaciones binarias o hexadecimales, dependiendo de cuál sea la más útil en cada punto de depuración. Por ejemplo, puedes imprimir todo el paquete en hexadecimal, y luego imprimir los PDOs en binario para poder comprobar tu código de análisis.

No hacen falta muchas impresiones en un bucle para retrasar significativamente las comunicaciones

Por otro lado, print() interferirán con los requisitos de tiempo en un grado sorprendente. El envío de datos a través de la consola lleva un montón de tiempo - incluso si se trata de una consola virtual, como es el caso de la UART virtual del RP2040 sobre USB-CDC. He pasado cerca de dos horas depurando este código en un RP2040 y golpeando la ventana de tiempo de espera todo el tiempo, sólo para descubrir que tenía veinte sentencias de impresión, y ellas solas llevaron mi código de "realmente rápido" a "demasiado lento para responder". Después de comentar el print() mi código empezó a funcionar en todas las PSU con las que lo probé, y añadí un montón de lógica de selección de voltaje y corriente sin ningún problema.

Comprobar el contenido del búfer de recepción también es útil. Después de haber enviado su solicitud, compruebe el estado del búfer de recepción - al igual que al final del último artículo. ¿Hay algún dato esperando? Lee el mensaje fuera de él, y comprueba la cabecera - ¿es un mensaje Accept? Consulta la página 119 para ver el código correspondiente. ¿Nada en el buffer después de un mensaje de petición? Es probable que hayas violado los requisitos de tiempo.

Por otro lado, es bastante difícil escribir MicroPython que sea lo suficientemente lento como para violar los requerimientos de tiempo aquí. A medida que haces el script más complejo, puede ser que pases demasiadas cosas entre la recepción de los PDOs y el envío de una respuesta. O, tal vez, ¿obtienes un tipo diferente de mensaje en tu buffer de recepción? Su fuente de alimentación podría estar enviando algún otro mensaje que requiere una respuesta rápida - tal vez, usted está trabajando con el puerto USB-C de un ordenador portátil, y quiere algo más.

9 Voltios Alcanzados - ¿Qué Sigue?

Lo que hemos hecho aquí rivaliza en precio con una placa de activación PD, es mucho más personalizable, probablemente tan barata o más que un IC de activación PD, e innegablemente mucho más genial. Ah, y hemos aprendido a leer y enviar mensajes PD - que puede y le ayudará si alguna vez está interesado en crear algo fuera de lo común con USB-C. Todo lo que necesitas es un chip FUSB302 (unos 50 céntimos cada uno), emparejado con un microcontrolador lo suficientemente dedicado a la tarea de hablar PD - puede que ya tengas un MCU de este tipo en tu proyecto haciendo otra cosa.

El código está en MicroPython; dicho esto, es lo suficientemente pseudocódigo como para que sea fácil portarlo a un lenguaje diferente desde aquí. Si estás ejecutando C++ o C, comprueba la pila IronOS; hay una adaptada a STM32 HAL, una adaptada a Arduino, y hay una pila decente de Microchip. Sólo he visto el primero en la acción; sin embargo, si usted no se siente como MicroPython, yo apostaría uno de ellos será adecuado para usted.

Algo que habrás notado - en ningún momento he tenido que referirme a los espeluznantes diagramas de la máquina de estados USB-C. Hay algunos estados en este código, técnicamente, y las máquinas de estado son lo suficientemente grandes que este código mejoraría con uno si fuera a crecer más complejo; sin embargo, realmente no necesitas uno si todo lo que quieres es 9 V de una fuente de alimentación USB-C. Sin embargo, los diagramas espeluznantes pueden ayudarte a depurar cosas como el tiempo de espera de 500 ms entre anuncio y respuesta; en otras palabras, no tengas miedo.

A partir de aquí, usted puede hacer un montón de cosas USB-C. Puedes convertir tus fuentes de alimentación de jack de barril en USB-C con un poco de circuitería extra, hacer una fuente con perfiles personalizados extravagantes, explorar las capacidades ocultas de los controladores PD, obtener DisplayPort de los puertos USB-C - diablos, si eres pentesting-inclinado, incluso puedes crear gadgets USB-C maliciosos.

Aquí está mi sencillo hack personal - un algoritmo corto que elige el mejor PDO para un valor de resistencia estática teniendo en cuenta los valores máximos de corriente; resolviendo exactamente el escenario en el que una placa de disparo nos falla. Se vincula perfectamente con el código que hemos escrito hasta ahora, y si quieres desarrollar un dispositivo USB-C de alta potencia que haga algo similar, podría ser de tu interés.

Usted puede, y debe acercarse a USB-C de una manera hacker, y este artículo es un gran ejemplo de que usted no necesita toda la complejidad de la norma USB-C PD si quieres hacer cosas útiles con PD - todo lo que necesita a cabo es de diez páginas de ochocientos, y un centenar de líneas de código.

Óscar Soto
Óscar Soto

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *