Control de LED RGB a través de microcontroladores Cypress UDB PSoC



Introduccion


Hace tiempo que quería aprender la técnica de programar bloques UDB en los controladores Cypress PSoC, pero de alguna manera no me llegaron las manos. Y así, surgió un problema en el que esto podría hacerse. Al comprender los materiales de la red, me di cuenta de que las recomendaciones prácticas para trabajar con UDB se limitan a diversas variaciones de contadores y PWM. Por alguna razón, todos los autores hacen sus variaciones de estos dos ejemplos canónicos, por lo que la descripción de algo más puede ser interesante para los lectores.

Entonces Hubo un problema para administrar dinámicamente una larga línea de LED RGB WS2812B. Se conocen enfoques clásicos de este asunto. Puede tomar el Arduino trivial, pero allí la salida se realiza mediante programación, por lo que mientras se emiten los datos, todo lo demás está inactivo, de lo contrario los diagramas de temporización fallarán. Puede tomar STM32 y enviar datos a través de DMA a PWM o de DMA a SPI. Se conocen técnicas. Incluso, en un momento, personalmente controlé una línea de dieciséis diodos a través de SPI. Pero la sobrecarga es genial. Un bit de datos en los LED ocupa 8 bits en la memoria para el caso de PWM y de 3 a 4 bits (dependiendo de la frialdad de PLL en el controlador) para SPI. Si bien hay pocos LED, esto no da miedo, pero si hay, por ejemplo, un par de cientos, entonces 200 * 24 = 4800 bits = 600 bytes de datos útiles deben almacenarse físicamente en un búfer con una capacidad de más de 4 kilobytes para la opción PWM o más de 2 kilobytes para SPI- opciones. Para la indicación dinámica de buffers, debe haber varios, y STM32F103 tiene RAM para todo sobre todo, 20 kilobytes. No es que nos hayamos topado con una tarea irrealizable, pero una razón para verificar si esto se puede implementar en el PSoC sin tener que gastar RAM adicional es bastante importante.

Referencias teóricas


Primero, descubramos qué tipo de bestia es esa UDB y cómo funcionan con ella. Maravillosas películas de instrucciones del fabricante del controlador ayudarán en esto.

Debería comenzar a mirar desde aquí , y luego, al final de cada video, habrá un enlace a la próxima serie. Paso a paso, obtendrá conocimientos básicos y considerará el ejemplo canónico "contador". Bueno, y un sistema de control de semáforo.

Casi lo mismo, pero cortado en pequeños pedazos, puedes verlo aquí . Mi video no se reprodujo, pero se puede descargar y ver localmente. Entre otras cosas, también hay un ejemplo canónico de la implementación de PWM.

Soluciones terminadas


Para no reinventar la rueda (y viceversa, para aprender la metodología de la experiencia de otra persona), rebusqué en la red en busca de soluciones listas para controlar los LED RGB. La solución más popular es StripLightLib.cylib. Pero desde hace muchos años tiene planes para agregar el soporte Agregar DMA. Pero quiero probar una solución que no dependa del procesador central. Quiero comenzar el proceso y olvidarlo, centrándome en preparar el siguiente marco.

La solución que se adapta a mis deseos se encontró en https://github.com/PolyVinalDistillate/PSoC_DMA_NeoPixel .

Todo se implementa allí en UDB (pero los LED son solo una excusa, el objetivo es aprender UDB). Hay soporte para DMA. Y el proyecto allí está claramente bellamente organizado.

Problemas de la solución elegida como base


Cómo se organiza el "firmware" en el proyecto PSoC_DMA_NeoPixel, quienes lo deseen pueden verlo después de leer el artículo. Esto arreglará el material. Por ahora, solo diré que primero simplifiqué la lógica del firmware original sin reducir los recursos consumidos (pero se ha vuelto más fácil de entender). Luego comenzó a experimentar con la sustitución de la lógica del autómata, que prometía una ganancia en recursos, pero se encontró con un problema grave. Y así decidió: ¡no se elimina! Y las vagas dudas comenzaron a atormentarme: ¿tenía el autor inglés el mismo problema? Su demo parpadea muy bien con LED. Pero, ¿qué sucede si reemplazamos el hermoso relleno con "todas las unidades" y controlamos la salida no con nuestros ojos, sino con un osciloscopio?
Entonces, de la forma más cruda posible (incluso podría decir "brutalmente") formamos los datos:

memset (pPixelArray,0xff,sizeof(pPixelArray)); //Call NeoPixel update function (non blocking) to trigger DMA pixel update NP_Update(); 

Y aquí vemos esa imagen en un osciloscopio:



El primer bit tiene un ancho diferente al resto. Pedí enviar todas las unidades, pero no todas se van. Entre ellos cero! Cambiar el escaneo:



El ancho es diferente para cada octavo bit.

En general, este ejemplo como solución independiente no es adecuado, sino como fuente de inspiración, simplemente perfecto. En primer lugar, su inoperabilidad no es visible con el ojo (los LED aún están brillantes, el ojo no ve que brillan a la mitad), pero el código está bien estructurado, es bueno tomarlo como base. En segundo lugar, este ejemplo proporciona espacio para encontrar formas de simplificar, y en tercer lugar, te hace pensar cómo solucionar el defecto. ¡Lo mismo es entender el material! Entonces, una vez más, después de leer el artículo, recomiendo intentar analizar el ejemplo original, dándome cuenta de cómo funciona.

Parte práctica


Ahora comenzamos a practicar. Estamos probando los aspectos principales del desarrollo de firmware para UDB. Considere la relación y las técnicas básicas. Para hacer esto, abra mi versión del proyecto . El bloque izquierdo almacena información sobre archivos de trabajo. Por defecto, la pestaña Fuente está abierta. La fuente principal del proyecto es el archivo main.c. En realidad, no hay otros archivos de trabajo en el grupo Archivos de origen .



El grupo Fuente Generada contiene funciones de biblioteca. Es mejor no editarlos. Después de cada cambio del "firmware" de UDB, este grupo se regenerará. Entonces, ¿dónde está la descripción del código para UDB en este idilio? Para verlo, debe cambiar a la pestaña Componentes :



El autor del proyecto original hizo un conjunto de componentes de dos niveles. En el nivel superior se encuentra el circuito NeoPixel_v1_2.cysch . Esto se puede ver desde el esquema principal:



El componente es el siguiente:



El soporte de software para este esquema se discutirá más adelante. Mientras tanto, descubra que es una unidad DMA normal y un cierto símbolo NeoPixDrv_v1 . Este misterioso bloque se describe anteriormente en el árbol, que se desprende de la siguiente información sobre herramientas:



UDB "Firmware"


Abra ese componente (archivo con la extensión .cyudb ). El dibujo abierto es simplemente enorme. Comenzamos a entender qué es qué.



A diferencia del autor del proyecto original, considero la transmisión de cada bit de datos en forma de tres partes iguales (en el tiempo):

  1. Parte inicial (siempre 1)
  2. Parte de datos
  3. Detener parte (siempre 0)

Con este enfoque, no se requiere una gran cantidad de contadores (en el original había hasta tres piezas, que consumían una gran cantidad de recursos). La duración de todas las partes es la misma y se puede configurar con un registro. Por lo tanto, el gráfico de transición del firmware contiene los siguientes estados:

Estado inactivo . La máquina permanece en ella hasta que lleguen nuevos datos a FIFO.



De los videos de capacitación, no estaba del todo claro para mí cómo el estado de la máquina está relacionado con ALU. Los autores usan la comunicación como algo natural, pero yo, como principiante, no pude verla de inmediato. Echemos un vistazo rápido en detalle. La figura anterior muestra que el estado inactivo está codificado con el valor 1'b0. 3'b000 será más correcto, pero el editor rehacerá todo de todos modos. Las entradas del bloque Datapath se describen así:



Si hace doble clic en ellos, aparecerá una versión más detallada:



Esto significa que el bit cero de la dirección de la instrucción ALU corresponde al bit cero de la variable que establece el estado de la máquina. El primero es el primero, el segundo es el segundo. Si lo desea, cualquier variable e incluso expresiones pueden coincidir con los bits de dirección de la instrucción ALU (en la versión original, el segundo bit de la dirección de la instrucción ALU coincidía con una expresión, además, no se usa explícitamente en la versión actual, pero es muy obvio como un ejemplo que lleva el cerebro, entonces puede echar un vistazo).

Entonces Con la configuración actual de las entradas, que es el código de estado binario de la máquina, se utiliza dicha instrucción ALU. Cuando estamos en estado inactivo con el código 000, se utiliza la instrucción nula. Aquí esta:



Ya sé por esta entrada que este es un NOP banal. Pero puede hacer doble clic en él y leer la versión completa:



Los NOP están inscritos en todas partes. Los registros no se rellenan con nada.

¡Ahora descubramos qué tipo de bandera misteriosa ! NoData , obligando a la máquina a dejar el estado inactivo . Esta es la salida del bloque Datapath . En total, se pueden describir hasta seis salidas. Es solo que Datapath puede producir muchos más indicadores, pero no hay suficientes recursos de rastreo para todos, por lo que debemos elegir qué seis (o menos) realmente necesitamos. Aquí está la lista en la figura:



Si hace doble clic en él, se revelarán los detalles:



Aquí está la lista completa de banderas que se pueden mostrar:



Después de haber seleccionado la bandera requerida, debe darle un nombre. De ahora en adelante, el sistema tiene una bandera. Como puede ver, el indicador NoData es el nombre del estado del bloque F0 de la cadena (vacío) . Es decir, una señal de que no hay datos en el búfer de entrada. Ah ! NoData , respectivamente, su inversión. Señal de disponibilidad de datos. Tan pronto como los datos ingresen al FIFO (mediante programación o usando DMA), la bandera se borrará (y se activará su inversión), y en el siguiente ciclo de reloj, el autómata saldrá del estado inactivo y entrará en el estado GetData .



Como puede ver, el autómata saldrá de este estado incondicionalmente después de haber estado en él exactamente un ciclo de reloj. No se indican acciones en el gráfico de transición para este estado. Pero siempre debe mirar lo que hará ALU. El código de estado es 1'b1, es decir, 3'b001. Nos fijamos en la dirección correspondiente en ALU:



Hay algo Al no tener experiencia en leer lo que está escrito aquí, ábralo haciendo doble clic en la celda correspondiente:



De ello se deduce que la ALU en sí todavía no realiza ninguna acción. Pero el contenido de FIFO0, es decir, los datos que provienen del programa o del bloque DMA, se colocarán en el registro A0. Mirando hacia el futuro, diré que A0 se usa como un registro de desplazamiento, desde el cual el byte saldrá en forma de serie. El registro A1 colocará el valor del registro D1. En general, todos los registros D generalmente se completan con software antes de que el hardware comience a estar activo. Luego, al examinar la API, veremos que la cantidad de tics de reloj se coloca en este registro, que establece la duración del tercer bit. Entonces En A0, el valor desplazado cayó, y en A1, la duración de la parte inicial del bit. Y en el próximo latido, la máquina pasará al estado Constant1 .



Como el nombre del estado implica, aquí se genera la constante 1. Veamos la documentación del LED. Así es como se debe transferir la unidad:



Y aquí está: cero:



Líneas rojas que agregué. Si suponemos que las duraciones de los tercios son iguales, entonces se cumplen los requisitos para la duración de los pulsos (que figuran en la misma documentación). Es decir, cualquier impulso consiste en una unidad de inicio, un bit de datos y una parada cero. En realidad, la unidad de arranque se transmite cuando la máquina está en estado Constante1 .

En este estado, la máquina bloquea la unidad en su disparador interno. El nombre del desencadenante es CurrentBit . En el proyecto original, generalmente era un disparador que establece el estado del autómata auxiliar. Decidí que esa máquina solo confundiría a todos, así que comencé un disparador. No se describe en ninguna parte. Pero si ingresa las propiedades de estado, el siguiente registro es visible en la tabla:



Y debajo del estado en el gráfico hay dicho texto:



No se alarme por el símbolo igual. Estas son las características del editor. En el código Verilog resultante (generado automáticamente por el mismo sistema) habrá una flecha:

 Constant1 : begin CurrentBit <= (1); if (( CycleTimeout ) == 1'b1) begin MainState <= Setup1 ; end end 

El valor retenido en este disparador es la salida de todo nuestro bloque:



Es decir, cuando la máquina entró en el estado de Constant1 , la salida del bloque que estamos desarrollando obtendrá uno. Ahora veamos cómo se programa la ALU para la dirección 3'b010:



Revelamos este elemento:



La unidad 1 se resta del registro A1. El valor de salida de ALU cae en el registro A1. Arriba, consideramos que A1 es un contador de reloj utilizado para establecer la duración del pulso de salida. Déjame recordarte que arrancó desde D1 en el último paso.
¿Cuál es la condición para salir de un estado? CycleTimeOut . Se describe entre las salidas de la siguiente manera:



Entonces, unimos la lógica. En el estado anterior, el contenido del registro D1 previamente completado por el programa cayó en el registro A1. En este paso, la máquina traduce el activador CurrentBit a uno, y en ALU, el registro A1 disminuye en cada ciclo de reloj. Cuando A1 se convierte en cero, la bandera se elevará automáticamente, a lo que el autor le dio el nombre CycleTimeout , como resultado de lo cual la máquina cambiará al estado Setup1 .

El estado Setup1 prepara datos para transmitir el pulso útil.



Observamos la instrucción ALU en 3'b011. Lo abriré de inmediato:



Parece que ALU no tiene acciones. Operación NOP. Y la salida de ALU no llega a ninguna parte. Pero esto no es así. Una acción extremadamente importante es el cambio de datos en ALU. El hecho es que el bit de transporte entre las salidas está conectado a nuestra cadena ShiftOut :



Y como resultado de esta operación de cambio, el valor desplazado en sí mismo no llegará a ninguna parte, pero la cadena ShiftOut tomará el valor del bit más significativo del registro A0. Es decir, los datos que deben transmitirse. Bajo el estado del gráfico, se puede ver que este valor, que dejó la ALU en la cadena ShiftOut , se enganchará en el activador CurrentBit . Permítanme mostrar el dibujo nuevamente para no rebobinar el artículo:



Comienza la transmisión de la segunda parte del bit: el valor inmediato es 0 o 1.

Volvemos a las instrucciones para ALU. Además de lo que ya se ha dicho, está claro que el contenido del registro D1 se colocará nuevamente en el registro A1 para medir nuevamente la duración del segundo tercio del pulso.

El estado de DataStage es muy similar al estado de Constant1 . El autómata simplemente resta uno de A1 y entra al siguiente estado cuando llega a cero. Déjame incluso mostrarlo así:



y así:



Luego viene el estado de Setup2 , cuya esencia ya conocemos.



En este estado, el activador CurrentBit se restablece a cero (ya que se transmitirá el tercer tercio del pulso, la parte de parada, y siempre es cero). ALU carga el contenido de D1 en A1. Incluso puede verlo en una breve nota con su ojo entrenado:



El estado de Constant0 es completamente idéntico a los estados de Constant1 y DataStage . Resta la unidad de A1. Cuando el valor llegue a cero, salga al estado ShiftData :





El estado de ShiftData es más complejo. En las instrucciones correspondientes para ALU, se realizan las siguientes acciones:



El registro A0 se desplaza 1 bit y los resultados se vuelven a colocar en A0. En A1, los contenidos de D1 se vuelven a poner para comenzar a medir el inicio tercero para el siguiente bit de datos.

Es mejor considerar las flechas de salida teniendo en cuenta las prioridades, para lo cual hacemos doble clic en el estado ShiftData .



Si no se transmite el último bit (sobre cómo se forma este indicador, un poco más bajo), entonces transferimos uno para el siguiente bit del byte actual.

Si se transmite el último bit y no hay datos en FIFO, pasamos al estado inactivo.

Finalmente, si se transmite el último bit, pero hay datos en el FIFO, pasamos a la selección y transmisión del siguiente byte.

Ahora sobre el contador de bits. Solo hay dos baterías en ALU: A0 y A1. Ya están ocupados por el registro de desplazamiento y el contador de retraso, respectivamente. Por lo tanto, un contador de bits se usa externamente.



Haga doble clic en él:



El valor en el arranque es seis. Se carga utilizando el indicador LoadCounter descrito en la sección variable:



Es decir, cuando se toma el siguiente byte de datos, esta constante se carga en el camino.

Cuando la máquina entra en el estado ShiftData , el contador disminuye el valor. Cuando llega a cero, la salida TerminalCount está conectada, conectada al circuito de nuestra semilla FinalBit . Es este circuito el que establece si la máquina transferirá el siguiente bit del byte actual o transferirá un nuevo byte (bueno, o esperará un nuevo paquete de datos).

En realidad, todo es de la lógica. Cómo se genera la señal SpaceForData , que establece el estado de la salida Hungry (informando a la unidad DMA que es posible transmitir los siguientes datos), los lectores están invitados a realizar un seguimiento de forma independiente.

Soporte de software


El autor del proyecto original eligió hacer soporte de software para todo el sistema en el bloque que describe la solución integrada. Déjame recordarte, estamos hablando de este bloque:



Desde este nivel, hay control sobre la unidad de biblioteca DMA y todas las partes incluidas en la parte UDB. Para implementar la API, el autor del original agregó el encabezado y los archivos de programa:



El formato del cuerpo de estos archivos te pone triste. Toda la culpa es el amor de los desarrolladores de PSoC Designer por los "puros". De ahí las terribles macros y nombres de kilómetros. La organización de clases en C ++ sería útil aquí. Al menos lo comprobamos al implementar nuestro RTOS MAX: resultó hermoso y conveniente. Pero aquí puedes discutir mucho, pero tendrás que usar lo que hemos defraudado desde arriba. Solo mostraré brevemente cómo se ve la función API que contiene estas macros:

 volatile void* `$INSTANCE_NAME`_Start(unsigned int nNumberOfNeopixels, void* pBuffer, double fSpeedMHz) { //work out cycles required at specified clock speed... `$INSTANCE_NAME`_g_pFrameBuffer = NULL; if((0.3/(1.0/(fSpeedMHz))) > 255) return NULL; unsigned char fCyclesOn = (unsigned char)(0.35/(1.0/(fSpeedMHz))); `$INSTANCE_NAME`_g_nFrameBufferSize = nNumberOfNeopixels*3; //Configure for 19.2 MHz operation `$INSTANCE_NAME`_Neo_BITCNT_Start(); //Counts bits in a byte //Sets bitrate frequency in number of clocks. Must be larger than largest of above two counter periods CY_SET_REG8(`$INSTANCE_NAME`_Neo_DPTH_D1_PTR, fCyclesOn+1); //Setup a DMA channel `$INSTANCE_NAME`_g_nDMA_Chan = `$INSTANCE_NAME`_DMA_DmaInitialize(`$INSTANCE_NAME`_DMA_BYTES_PER_BURST, `$INSTANCE_NAME`_DMA_REQUEST_PER_BURST, HI16(`$INSTANCE_NAME`_DMA_SRC_BASE), HI16(`$INSTANCE_NAME`_DMA_DST_BASE)); if(pBuffer == NULL) ... 

Estas reglas del juego tienen que ser aceptadas. Ahora ya sabe de dónde inspirarse cuando desarrolla sus funciones (es mejor hacerlo en el proyecto original). Y prefiero hablar sobre los detalles, tomando la opción ya procesada por el generador.

Después de generar el código (descrito a continuación), este archivo se almacenará aquí:



Y la vista ya será perfectamente legible. Hay dos funciones hasta ahora. El primero inicializa el sistema, el segundo inicia la transferencia de datos desde el búfer a la línea de LED.

La inicialización afecta a todas las partes del sistema. Hay una inicialización del contador de siete bits, que forma parte del sistema UDB:

  NP_Neo_BITCNT_Start(); //Counts bits in a byte 

Hay un cálculo constante que debe cargarse en el registro D1 (recuerdo que establece la duración de cada uno de los terceros bits):

 unsigned char fCyclesOn = (unsigned char)(0.35/(1.0/(fSpeedMHz))); CY_SET_REG8(NP_Neo_DPTH_D1_PTR, fCyclesOn+1); 

Configurar un bloque DMA ocupa la mayor parte de esta función. El búfer se usa como fuente y el FIFO0 del bloque UDB se usa como receptor (NP_Neo_DPTH_F0_PTR en el registro de kilómetros). El autor tenía una parte de esta configuración en la función de transferencia de datos. Pero, en mi opinión, hacer todos los cálculos por el bien de cada transmisión es demasiado derrochador. Especialmente cuando consideras que una de las acciones dentro de la función se ve muy, muy voluminosa.

 //work out cycles required at specified clock speed... NP_g_pFrameBuffer = NULL; NP_g_nFrameBufferSize = nNumberOfNeopixels*3; //Setup a DMA channel NP_g_nDMA_Chan = NP_DMA_DmaInitialize(NP_DMA_BYTES_PER_BURST, NP_DMA_REQUEST_PER_BURST, HI16(NP_DMA_SRC_BASE), HI16(NP_DMA_DST_BASE)); ... NP_g_nDMA_TD = CyDmaTdAllocate(); CyDmaTdSetConfiguration(NP_g_nDMA_TD, NP_g_nFrameBufferSize, CY_DMA_DISABLE_TD, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT); CyDmaTdSetAddress(NP_g_nDMA_TD, LO16((uint32)NP_g_pFrameBuffer), LO16((uint32)NP_Neo_DPTH_F0_PTR)); CyDmaChSetInitialTd(NP_g_nDMA_Chan, NP_g_nDMA_TD); 

La segunda función en el contexto de la primera es la parte superior del laconismo. Es solo que el primero se llama en la etapa de inicialización, cuando los requisitos de rendimiento son bastante gratuitos. Durante la operación, es mejor no desperdiciar los ciclos del procesador en nada superfluo:

 void NP_Update() { if(NP_g_pFrameBuffer) { CyDmaChEnable(NP_g_nDMA_Chan, 1); } } 

Claramente, no hay suficiente funcionalidad para trabajar con múltiples buffers (para proporcionar doble buffer), pero en general, una discusión sobre la funcionalidad de la API está más allá del alcance del artículo. Ahora lo principal es mostrar cómo agregar soporte de software al firmware desarrollado. Ahora sabemos cómo hacerlo.

Generacion de proyectos


Entonces, toda la parte del firmware está lista, se agrega la API, ¿qué hacer a continuación? Seleccione el elemento del menú Generar-> Generar aplicación .



Si todo va bien, puede abrir la pestaña Resultados y ver el archivo con la extensión rpt .



Muestra cuántos recursos del sistema se destinaron a la implementación del firmware.





Cuando comparo los resultados con los que estaban en el proyecto original, mi alma se calienta.

Ahora vaya a la pestaña Fuente y comience a trabajar con la parte de software. Pero esto ya es trivial y no requiere explicaciones especiales.



Conclusión


Espero que de este ejemplo, los lectores hayan aprendido algo nuevo e interesante sobre el trabajo práctico con bloques UDB. Traté de concentrarme en una tarea específica (control LED), así como en la metodología de diseño, ya que tenía que comprender algunos aspectos que eran obvios para los especialistas. Traté de marcarlos mientras los recuerdos de la búsqueda son frescos. En cuanto al problema resuelto, los diagramas de tiempo para mí resultaron no ser tan ideales como el autor del desarrollo original, pero encajan perfectamente en las tolerancias definidas en la documentación para los LED, y los recursos del sistema fueron significativamente menores.

De hecho, esto es solo una parte de la información no estándar encontrada. En particular, de la mayoría de los materiales puede parecer que UDB funciona bien solo con datos en serie, pero esto no es así. Nota de aplicación encontrada, que muestra brevemente cómo puede conducir y datos paralelos. Podríamos considerar ejemplos específicos basados ​​en esta información (aunque es imposible eclipsar el FX2LP, otro controlador de Cypress: PSoC tiene una velocidad de bus USB más baja).

Mi cabeza está dando vueltas a ideas sobre cómo resolver el problema de "flashear" una impresora 3D, lo que me ha atormentado durante mucho tiempo. Allí, las interrupciones que sirven a los motores paso a paso devoran solo un porcentaje increíble de tiempo de CPU. En general, hablé mucho sobre interrupciones y tiempo de procesador en un artículo sobre RTOS MAX . Se estima que para el mantenimiento de motores paso a paso es posible llevar todas las cabañas temporales completamente a la UDB, dejando al procesador una tarea puramente computacional sin temor a que no tenga tiempo para hacerlo en un intervalo de tiempo dedicado.

Pero estas cosas solo pueden razonarse si el tema es interesante.

Source: https://habr.com/ru/post/es429882/


All Articles