Arduin y LED, o cómo actualizar el diseñador infantil



Mi hijo "enganchó" firmemente al constructor magnético Magformers . Una vez que examinó una serie de Fixiks donde aparecía el mismo constructor, el niño preguntó: "Papá, ¿por qué los detalles tienen detalles que brillan, pero nosotros no?".

Resultó que realmente hay un "conjunto de LED de neón Magformers", donde además de los bloques de construcción habituales, también hay un elemento con un LED. Como en este momento ya habíamos reunido una caja completa de imanes de todas las formas y tamaños posibles (en cuanto a mí, los formadores chinos no son inferiores al original en absoluto), de alguna manera no quería comprar otro juego solo por el hecho de una bombilla. Además, este conjunto cuesta significativamente más que uno similar sin retroiluminación.

Habiendo estimado que solo hay un par de dólares en los componentes, la mayoría de los cuales ya tenía, decidí recoger mi morgulka. Sí, y con efectos que el original no tuvo.

Debajo del gato encontrará la opción de una luz intermitente en el ATTiny85 y el panel LED en los LED WS8212. Hablaré sobre los circuitos, cómo alimente todo esto desde la batería, así como los problemas obvios que acumulé en el camino. También hablaré en detalle sobre el componente de software del proyecto.

Primeros pasos


Me pareció que el brillo de un LED normal (incluso RGB) es aburrido y banal. Pero sentir algo como WS8212 parecía interesante. En ebee, se ofrecieron tanto LED individuales como matrices de hasta 16x16 de tamaño. Habiendo comprado varios módulos diferentes, me decidí por una matriz 4x4. Hay muchos LED en él para disfrutar de varios efectos visuales, mientras que el módulo es comparable en tamaño a la ventana del bloque cuadrado del diseñador.



Para controlar la matriz de LED, solo un pin del microcontrolador es suficiente, por lo que incluso el arduino nano parece un busto (además, no cabe en la carcasa). Pero el clon digispark en el controlador ATTiny85 resultó ser perfecto: no tiene mucha memoria y pines, pero es más que suficiente para el parpadeo del LED. El módulo se integra perfectamente con Arduino IDE y tiene un cargador de arranque USB incorporado, por lo que programar este módulo es muy simple y cómodo. Hace mucho que quería probarlo.

Comenzó con el esquema más simple.



De esta forma, fue posible depurar rápidamente todos los algoritmos de brillo / parpadeo (sobre ellos a continuación). Pero un juguete con cable no es el caso: debe pensar en la energía de la batería. Además, para no quebrar con las pilas de los dedos (que, además, no caben en el sobre), se decidió utilizar litio. Y dado que hay una batería de litio, debe pensar en cómo cargarla. En los contenedores, acabamos de encontrar un controlador de carga "popular" en el chip TP4056 que se compró en la ocasión.

Pero no funcionó de inmediato. El circuito del módulo Digispark ATTiny85 no está muy diseñado para esto: hay alimentación USB, pero luego la alimentación se suministra directamente al microcontrolador (a través del bus +5) o desde la entrada VIN, pero luego la alimentación pasa por el estabilizador lineal 7805. Opción cuando el módulo de carga de litio insertado en el espacio entre el conector USB y el microcontrolador no se proporciona. Tuve que modificar un poco el circuito y eliminar los detalles adicionales.



Entonces, ahora se suministra alimentación USB al pin VIN y luego se dirige a la entrada del cargador. La salida del cargador (de hecho, la batería está conectada directamente) vuelve a la placa a través del pie de 5V. Y aunque, de hecho, habrá de 3 a 4.2V (voltaje de la batería), esto es bastante normal: el rango de voltaje de funcionamiento del microcontrolador es 1.8-5.5V. E incluso el módulo LED funciona normalmente desde 2.7V, aunque por debajo de 3.2V el LED azul es un poco escaso y los colores "flotan" un poco en amarillo.

Para ahorrar energía, el LED D2 siempre encendido también desapareció. El esquema general ahora se ve así



Sería posible alimentar el circuito a través del conector USB en el cargador, pero luego se perdería la capacidad de cargar firmware a través del conector USB en la placa del controlador. Sería posible dejar dos conectores USB para diversos fines: uno para cargar, el otro para firmware, pero esto es de alguna manera incorrecto.

Compré una batería de tamaño 6x25x35 en eBay, pero resultó ser defectuosa o la apagué con un cortocircuito o una corriente de carga grande (de manera predeterminada, la corriente de carga está configurada en 1A y necesita soldar una resistencia para reducir la corriente). En cualquier caso, cuando se conectó la carga, incluso a 10 mA, el voltaje de la batería cayó a 1V. En el momento de la prueba, cambié a una batería LiPo medio muerta desde un pequeño quadrocopter. Un poco más tarde pedí la batería a otro vendedor y resultó ser buena.

En principio, sería posible detenerse en esto, soldar los cables de conexión y empujar suavemente todo dentro de algún tipo de carcasa, pero decidí medir el consumo del circuito. Y luego lloré. Bueno, en condiciones de funcionamiento (cuando las bombillas brillan por completo), esta cosa consume hasta 130 mA, por lo que, en reposo, el consumo es de más de 25 mA. Es decir ¡Esta luz intermitente consume mi batería de 600 mAh en menos de un día!

Resultó que unos 10 mA consumen LED. Incluso si no se encienden, un microcontrolador todavía funciona en cada uno de ellos y espera un comando. Es decir Debe crear un circuito de apagado para los LED.

Los restantes 15 mA son consumidos por el microcontrolador. Sí, puede acostarse y, según la hoja de datos, el consumo se medirá mediante microamperios, pero de hecho no fue posible obtener menos de 1 mA. Apagué el ADC y traduje los pines en la entrada. Parece que en algún lugar del circuito hay algún tipo de fuga, pero mi modesto conocimiento de la electrónica no es suficiente para encontrarlo y comprenderlo.

Complicamos el esquema


Entonces recordé que compré un chip PT1502 para una prueba. Este chip es un controlador de carga de batería de litio completo con una fuente de alimentación con varias entradas de control. La única dificultad es que el microcircuito viene en un paquete QFN20 de 4x4 mm y requiere algunas correas. Soldar esto en casa es difícil, pero posible. La tarifa es difícil para un LUT regular y debe solicitarse a los chinos. Pero no tenemos miedo a las dificultades, ¿verdad?

En varios cuadros, el esquema se puede describir de la siguiente manera.



En el estado apagado, no se suministra energía al controlador ni a los LED. El dispositivo tiene un botón de 'Encendido' que enciende la luz intermitente (también cambia de modo). El LED brilla, digamos, un minuto, y si no hay actividad del usuario (nadie presiona un botón), el dispositivo se apaga. Es decir No solo se va a dormir, sino que se apaga mediante la señal Power Hold. Y apaga todo a la vez, tanto el microcontrolador como los LED. La funcionalidad de encendido y apagado se implementa dentro del chip PT1502

Todo lo que queda es dibujar un diagrama de circuito y hacer una placa de circuito. La circuitería está, en su mayor parte, unida a la hoja de datos PT1502, así como al módulo Digispark ATTiny85. El microcircuito del controlador de potencia PT1502 se divide funcionalmente en varias partes, por lo tanto, se divide en bloques en el circuito.



Esto, de hecho, es un controlador de carga de batería de litio con su propio arnés. El LED1 indica el estado de carga - encendido, luego la carga está encendida. La resistencia R6 establece la corriente de carga a 470 mA. Como tengo una batería de 600 mAh, en principio, puede aumentar la corriente y poner una resistencia en 780-800 ohmios hasta 600 mA. Sin embargo, no estoy seguro acerca de la calidad especial de mi batería: es mejor cargarla más lentamente, pero durará más.

Considere un plan de energía



El botón SW1 inicia todo el sistema: el chip PT1502 se activa solo y luego inicia todas las fuentes de alimentación (de las cuales tiene 3). Cuando se instala la alimentación, el microcircuito iniciará el microcontrolador liberando la señal RESET. Para facilitar la depuración, también agregué un botón Restablecer por separado.

La señal HOLD se usa para apagar todo el sistema. Cuando se inicia el microcontrolador, debe configurar la unidad en esta línea. Cuando llega el momento de redondear, el microcontrolador establece cero en la línea HOLD y el chip de alimentación PT1502 detendrá todas las fuentes de alimentación.

Sería posible seguir la carga de la batería baja usando la salida BAT_LOW, pero en este artículo lo califiqué: no necesita guardar ningún dato y nada explotará si no nota una batería agotada a tiempo. Muere, así muere. Pero por si acaso, la junta proporcionó contacto para este negocio.

Volvamos al botón SW1 por un segundo. Decidí no hacer 2 botones separados para encender y controlar. Por lo tanto, el mismo botón también está conectado al ATTiny85 y durante la operación cambia los modos de parpadeo. Los valores del divisor R7-R8 se seleccionan para no quemar el puerto del microcontrolador PB2. Para todos los rangos de voltaje de la batería (3.3 - 4.2V), el voltaje se suministrará al pie del controlador dentro de los límites especificados de la hoja de datos (0.7 * VCC - VCC + 0.5V)

Considera una fuente de energía



Este es un convertidor DC-DC pulsado. El voltaje de salida se establece mediante las resistencias R10-R11 y, de acuerdo con la fórmula de la hoja de datos, se establece en 3.3V. Todo lo demás es un simple flejado.

Para bien, una fuente de alimentación tan engañosa no es realmente necesaria: sería posible alimentar el microcontrolador en general directamente desde la batería. Es solo que esta fuente ya está implementada en el chip PT1502 y puede encenderse / apagarse cuando la necesitemos, ¿por qué no usarla?



El chip también tiene 2 estabilizadores lineales, pero no los usaré. Desafortunadamente, resultó que todavía es necesario suministrar el voltaje de entrada a esta fuente, de lo contrario, el microcircuito piensa que la energía aún no es lo suficientemente estable y no inicia el microcontrolador (este conocimiento me fue dado por una semana de soldar el tablero de prueba de un lado a otro; no podía entender por qué no funciona )

Pasemos a la parte lógica.



El cable USB se traslapa de la placa Digispark sin cambios. Esto es necesario para coordinar el voltaje USB (que funciona con 3.3V) y las señales del microcontrolador (que en el original está alimentado por 5V). Como en mi caso el microcontrolador también funciona con 3.3V, el circuito podría simplificarse, pero por si acaso, me divorcié del circuito original en el tablero.



No hay nada interesante en la unión del microcontrolador.

El toque final es el conector



De hecho, obtuve una placa de depuración para mí en ATTiny85 con soporte USB y un controlador de energía con una batería de litio. Por lo tanto, no me limité a simplemente enviar la línea al LED. En cambio, traje todas las líneas del microcontrolador al peine; al mismo tiempo, es conveniente conectarse al programador.

Y deje que casi todas las líneas estén rígidamente vinculadas a una determinada funcionalidad (PB1 - Línea de espera, PB2 - botón de encendido, PB3 / PB4 - USB, PB5 - Restablecer) en el futuro será posible eludir algunos límites. Por ejemplo, no suelde el cable USB y libere las líneas PB3 / PB4. O, por ejemplo, rechazar un reinicio y liberar PB5. Mientras tanto, solo PB0 permanece libre, y conecta nuestro LED a él.

Pasamos al tablero. Dadas las limitaciones en el tamaño de la placa en 40x40 mm, la cantidad de componentes y la carcasa QFN20 del chip PT1502, ni siquiera consideré la fabricación de la placa en casa. Por lo tanto, inmediatamente comencé a criar el tablero de dos capas más compacto. Eso es lo que tengo



Para facilitar su uso, en el reverso firmé todas las funciones de salida posibles (se me ocurrió la idea de la placa Digispark)



Ordené el tablero en JLCPCB . Para ser honesto, no estoy muy satisfecho con la calidad: si suelda el chip muchas veces, la máscara cerca de los contactos pequeños del PT1502 está un poco nublada. Bueno, pequeñas inscripciones flotaban un poco. Sin embargo, si todo se suelda la primera vez, entonces las normas.

Para soldar QFN20 necesita un soldador, todo lo demás se puede soldar con un cierto soldador con cierta habilidad. Así es como se ve la placa soldada



Vivienda


Es hora de pasar al casco. Lo imprimí en una impresora 3D. Diseño sin adornos: caja y botón. Se proporcionan ganchos especiales en la caja para instalar la luciérnaga en el módulo cuadrado estándar del diseñador.



La placa principal y la batería viven en el estuche.





El panel LED está montado en la cubierta, que a su vez se atornilla a la caja principal con tornillos.

Al principio pensé en atornillar el panel LED a la cubierta con tornillos, pero al final simplemente lo pegué en una cinta de doble cara. Resultó así



De esta forma, el dispositivo ya se puede usar, pero todavía se ve feo: no hay suficiente difusor.

Traté de hacer la primera versión del difusor utilizando la tecnología de contracción de botellas de PET con un secador de construcción (se asomó a los modelos de aviones).

Entonces, primero necesitas un espacio en blanco. Lo hice con yeso, que vertí en un formulario que imprimí en una impresora 3D. En la primera versión, el formulario era de una sola pieza y nunca pude sacar el disco fundido. Por lo tanto, tuve que hacer una forma de dos piezas.



La idea del método es la siguiente. Pones una botella de yogurt para bebés en un espacio en blanco y la sientas con un secador de cabello. Estos son solo reportajes de 20 recipientes diferentes de debajo de leche diferente. Nunca logré poner esto bien, sin pliegues ni burbujas. Aparentemente necesita cercar algún tipo de instalación de vacío y colocar una lámina de plástico. En general, resultó ser demasiado difícil para tal oficio.

Después de refunfuñar entre los topos, encontré un par de metros de sonda de plástico transparente PET Verbatim. Decidí probar el difusor solo para imprimir. Y aunque en la entrada de la impresora, el plástico parece cristalino, la parte real es aburrida. Esto probablemente se deba a la estructura interna, ya que Las capas no llenan el volumen por completo, sino que se superponen con huecos y huecos. Además, si intenta procesar la pieza con papel de lija para obtener una superficie más lisa, obtenemos aún más esteras. Sin embargo, esto es exactamente lo que necesitaba.

Era demasiado vago para molestarme con el soporte para el difusor, así que lo agregué al pegamento caliente. Entonces mi diseño ahora es condicionalmente plegable. Podría confundirme con la invención de algún tipo de pestillos, pero ya me he quedado sin sonda de plástico transparente. Así que deja que se derrita.





Firmware


Para las luces intermitentes LED, no necesita sumergirse particularmente en la periferia del microcontrolador: solo un par de funciones para trabajar con GPIO son suficientes. Pero dado que el módulo está acoplado a la plataforma Arduino, ¿por qué no aprovechar esto?

Primero, algunas definiciones y constantes

// Number of total LEDs on the board. Mine has 4x4 LEDs #define NUM_HW_PIXELS 16 // Pin number where LED data pin is attached #define DATA_PIN 0 // Pin number where mode switch button is attached #define BUTTON_PIN 2 // Power Enabled pin #define POWER_EN_PIN 1 // Max brightness (dimming the light for debugging) #define MAX_VAL 255 

Esto determina el número de píxeles en mi matriz, los números de pin y el brillo máximo de los LED (durante la depuración, fue conveniente establecerlo en 50 para que no me ciegue los ojos)

Los LED en mi matriz están dispuestos de una manera bastante obvia: un zigzag. Por lo tanto, para diferentes efectos, tuve que renumerar.

 // LED indexes for different patterns uint8_t circleLEDIndexes[] = {0, 1, 2, 3, 4, 11, 12, 13, 14, 15, 8, 7}; uint8_t beaconLEDIndexes[] = {6, 5, 10, 9}; uint8_t policeLEDIndexes[] = {7, 6, 10, 11, 4, 5, 9, 8}; 

Para controlar los LED, no reinventé la rueda y tomé una biblioteca lista para trabajar con los LED WS8211 . La interfaz de la biblioteca está ligeramente encalada. Algunas funciones auxiliares (por ejemplo, convertir HSV a RGB) también se atascaron allí.

Primero, la placa y la biblioteca WS8211 deben inicializarse.

 // Driver Ai_WS2811 ws2811; void setup() { // Set up power pinMode(POWER_EN_PIN, OUTPUT); digitalWrite(POWER_EN_PIN, HIGH); // initialize LED data pin pinMode(LED_PIN, OUTPUT); // Initialize button pin pinMode(BUTTON_PIN, INPUT); // Initialize WS8211 library static CRGB ledsBuf[NUM_HW_PIXELS]; ws2811.init(DATA_PIN, NUM_HW_PIXELS, ledsBuf); // Set the watchdog timer to 2 sec wdt_enable(WDTO_2S); } 

En primer lugar, debe configurar la señal POWER HOLD en la unidad: esta será una señal para el chip PT1502 de que el microcontrolador se ha enrollado y funciona correctamente. El microcircuito, a su vez, suministrará electricidad regularmente al microcontrolador y a los LED siempre que la señal de RETENCIÓN esté establecida en la unidad.

A continuación, se configuran las patas para controlar el LED en la salida y los botones en la entrada. Después de eso, puede inicializar la biblioteca WS8211.

Dado que este es un dispositivo bastante autónomo, es imposible permitir que el microcontrolador se pegue en un estado incomprensible y consuma toda la batería. Para hacer esto, inicio el temporizador de vigilancia durante 2 segundos. El temporizador se reiniciará en el bucle principal del programa.

Ahora necesita definir un par de funciones auxiliares. La biblioteca WS8211 almacena un búfer con los valores de color de cada LED. Trabajar con el búfer directamente no es muy conveniente, porque escribí una función simple para escribir valores RGB en un LED específico

 void setRgb(uint8_t led_idx, uint8_t r, uint8_t g, uint8_t b) { CRGB * leds = ws2811.getRGBData(); leds[led_idx].r = r; leds[led_idx].g = g; leds[led_idx].b = b; } 

Pero en la mayoría de los casos, en el modelo de color RGB, contar colores no es muy conveniente, o incluso imposible. Por ejemplo, al dibujar cualquier tipo de arco iris, es más conveniente trabajar con el modelo de color HSV . El color de cada píxel se establece por el valor del tono de color y el brillo. La saturación se omite por simplicidad (se usa el máximo). Los valores de matiz se reducen a un rango de 0-255 (en lugar del estándar 0-359).

 /** * HVS to RGB conversion (simplified to the range 0-255) **/ void setHue(uint8_t led_idx, int hue, int brightness) { //this is the algorithm to convert from RGB to HSV double r = 0; double g = 0; double b = 0; double hf = hue/42.6; // Not /60 as range is _not_ 0-360 int i=(int)floor(hue/42.6); double f = hue/42.6 - i; double qv = 1 - f; double tv = f; switch (i) { case 0: r = 1; g = tv; break; case 1: r = qv; g = 1; break; case 2: g = 1; b = tv; break; case 3: g = qv; b = 1; break; case 4: r = tv; b = 1; break; case 5: r = 1; b = qv; break; } brightness = constrain(brightness, 0, MAX_VAL); setRgb(led_idx, constrain(brightness*r, 0, MAX_VAL), constrain(brightness*g, 0, MAX_VAL), constrain(brightness*b, 0, MAX_VAL) ); } 

La función se toma de la biblioteca Ai_WS8211 y se archiva ligeramente. En la versión original de esta función de la biblioteca había un par de errores debido a que el color en los arcoíris se mostraba con sacudidas.

Pasemos a la implementación de varios efectos. Cada función se llama desde el bucle principal para dibujar un "marco". Dado que cada efecto opera con diferentes parámetros entre llamadas, se almacenan en variables estáticas.

Este es el efecto más simple: todos los LED están llenos de un color, que cambia suavemente.

 void rainbow() { static uint8_t hue = 0; hue++; for (int led = 0; led < NUM_HW_PIXELS; led++) setHue(led, hue, MAX_VAL); ws2811.sendLedData(); delay(80); } 

El siguiente efecto es más interesante: muestra un arco iris a lo largo del contorno de la matriz y los colores del arco iris cambian gradualmente en un círculo.

 void slidingRainbow() { static uint8_t pos = 0; pos++; for (int led = 0; led < ARRAY_SIZE(circleLEDIndexes); led++) { int hue = (pos + led*256/ARRAY_SIZE(circleLEDIndexes)) % 256; setHue(circleLEDIndexes[led], hue, MAX_VAL); } ws2811.sendLedData(); delay(10); } 

Y este efecto llena toda la matriz con un color aleatorio, que primero se ilumina suavemente y luego también se apaga suavemente.

 void randomColorsFadeInOut() { static uint8_t color = 0; static bool goesUp = false; static uint8_t curLevel = 0; if(curLevel == 0 && !goesUp) { color = rand() % 256; goesUp = true; } if(curLevel == MAX_VAL && goesUp) { goesUp = false; } for(int led = 0; led < NUM_HW_PIXELS; led++) setHue(led, color, curLevel); if(goesUp) curLevel++; else curLevel--; ws2811.sendLedData(); delay(10); } 

El siguiente grupo de efectos dibuja diferentes balizas intermitentes. Entonces, por ejemplo, a un niño le gusta construir una excavadora con imanes y una luz intermitente naranja será muy útil allí.

 void orangeBeacon() { const int ORANGE_HUE = 17; static uint8_t pos = 0; pos+=3; for (int led = 0; led < ARRAY_SIZE(circleLEDIndexes); led++) { int brightness = brightnessByPos(pos, led*255/ARRAY_SIZE(circleLEDIndexes), 70); setHue(circleLEDIndexes[led], ORANGE_HUE, brightness); } ws2811.sendLedData(); delay(1); } 

Técnicamente, el efecto se ve como un punto brillante que se mueve a lo largo de la matriz. Pero para que se vea hermoso, los LED vecinos se desvanecen gradualmente a medida que se aleja del punto principal. Por lo tanto, necesitaba una función que calcule este mismo brillo.

 int brightnessByPos(int pos, int ledPos, int delta) { int diff = abs(pos - ledPos); if(diff > 127) diff = abs(256-diff); int brightness = MAX_VAL - constrain(MAX_VAL*diff/delta, 0, MAX_VAL); return brightness; } 

Pos es una cierta posición condicional del punto luminoso de brillo, asignada a un rango de loopback de 0-255. ledPos es la posición del LED (que se muestra en el mismo rango) cuyo brillo necesita calcular. Si la diferencia de posición es mayor que delta, entonces el LED no se enciende, y cuanto más cerca de la posición, más brillante se ilumina.

O, por ejemplo, una luz intermitente roja y azul de la policía

 void policeBeacon() { const int RED_HUE = 0; const int BLUE_HUE = 170; static uint8_t pos = 0; pos += 2; for (int led = 0; led < ARRAY_SIZE(policeLEDIndexes); led++) { int ledPos = led*255/ARRAY_SIZE(policeLEDIndexes); int brightness = brightnessByPos(pos, ledPos, 50); setHue(policeLEDIndexes[led], RED_HUE, brightness); if(brightness == 0) { brightness = brightnessByPos((pos+100) % 256, ledPos, 50); setHue(policeLEDIndexes[led], BLUE_HUE, brightness); } } ws2811.sendLedData(); delay(1); } 

Como estamos hablando de automóviles, entonces el semáforo aquí no es un problema para implementar.

Estas son funciones que incluyen varias señales de tráfico en varias posiciones.

 void clearPixels() { for(int i=0; i<NUM_HW_PIXELS; i++) { setRgb(i, 0, 0, 0); } } void redTrafficLights() { for(int i=0; i<4; i++) setRgb(i, MAX_VAL, 0, 0); ws2811.sendLedData(); } void yellowTrafficLights() { for(int i=4; i<8; i++) setRgb(i, MAX_VAL, MAX_VAL, 0); ws2811.sendLedData(); } void greenTrafficLights() { for(int i=8; i<16; i++) setRgb(i, 0, MAX_VAL, 0); ws2811.sendLedData(); } 

Es hora de revivirlo. El semáforo opera de acuerdo con un programa especial definido en una especie de bytecode. La placa describe el modo y el tiempo durante el cual este modo debe estar activado.

 enum TRAFFIC_LIGHTS { NONE, RED, YELLOW, GREEN }; struct trafficLightState { uint8_t state; uint16_t duration; }; const trafficLightState trafficLightStates[] = { {NONE, 1}, // clear yellow {RED, 7000}, // red {YELLOW, 2000}, // red + yellow {NONE, 1}, // clear red+yellow {GREEN, 7000}, // green {NONE, 300}, // Blinking green {GREEN, 300}, // Blinking green {NONE, 300}, // Blinking green {GREEN, 300}, // Blinking green {NONE, 300}, // Blinking green {GREEN, 300}, // Blinking green {NONE, 1}, // clear green {YELLOW, 2000}, // yellow }; 

En realidad, la función que lo procesa todo

 void trafficLights() { static uint8_t curStateIdx = 0; static unsigned long curStateTimeStamp = 0; // Switch to a new state when time comes if(millis() - curStateTimeStamp > (unsigned long)trafficLightStates[curStateIdx].duration) { curStateIdx++; curStateIdx %= ARRAY_SIZE(trafficLightStates); curStateTimeStamp = millis(); } switch(trafficLightStates[curStateIdx].state) { case NONE: clearPixels(); ws2811.sendLedData(); break; case RED: redTrafficLights(); break; case YELLOW: yellowTrafficLights(); break; case GREEN: greenTrafficLights(); break; default: break; } // Just waiting delay(10); } 

Al alcanzar el intervalo de tiempo especificado, se activa el siguiente modo de semáforo y la cuenta regresiva comienza nuevamente.

El último efecto sobre el cual fue suficiente mi imaginación son los asteriscos. 5 LED aleatorios se iluminan con un brillo aleatorio y luego se apagan suavemente. Si una estrella se apaga, se encenderá otra en un lugar aleatorio.

 void stars() { const uint8_t numleds = 5; static uint8_t ledIndexes[numleds] = {0}; static uint8_t curVal[numleds] = {0}; static uint8_t maxVal[numleds] = {0}; for(int i=0; i<numleds; i++) { if(ledIndexes[i] == 0) { uint8_t led = rand() % (NUM_HW_PIXELS+1); CRGB * leds = ws2811.getRGBData(); if(leds[led].r == 0) { ledIndexes[i] = led; maxVal[i] = rand() % (MAX_VAL-1) + 1; curVal[i] = 0; } } else { uint8_t led = ledIndexes[i]; if(curVal[i] < maxVal[i]) curVal[i]++; else if(curVal[i] == maxVal[i]) maxVal[i] = 0; else if(curVal[i] == 0 || --curVal[i] == 0) ledIndexes[i] = 0; setRgb(led-1, curVal[i], curVal[i], curVal[i]); } } ws2811.sendLedData(); delay(80); } 

En algún lugar aquí apareció un bicho malvado. A veces las estrellas se iluminan bruscamente, o viceversa, se apagan abruptamente. Pero para ser honesto, era demasiado vago como para entenderlo, parece bastante normal.

Es hora de pensar en ahorrar batería. Ya he dado los valores de consumo de todo esto. Si no piensa en apagar la alimentación, los LED consumirán la batería en un par de horas. Esta función es responsable de apagar la alimentación después de 90 segundos de inactividad. Inicialmente, fueron 60 segundos, pero con un juego real esto no fue suficiente, y 2 minutos fueron de alguna manera largos.

 void shutdownOnTimeOut(bool resetTimer = false) { static unsigned long periodStartTime = 0; if(periodStartTime == 0 || resetTimer) { periodStartTime = millis(); return; } if(millis() - periodStartTime >= 90000UL) { periodStartTime = 0; shutDown(); } } 

En realidad, el apagado ocurre de la siguiente manera.

 void shutDown() { clearPixels(); ws2811.sendLedData(); wdt_disable(); digitalWrite(POWER_EN_PIN, LOW); // No power after this point while(true) ; } 

Si el usuario presiona los botones, el temporizador se reinicia. Una vez transcurrido el tiempo establecido, la función establece la señal HOLD en cero, que es un comando PT1502 para apagar la alimentación. Watchdog, por cierto, también debe detenerse, de lo contrario, después de 2 segundos, activará el sistema y volverá a encender.

Finalmente, el ciclo principal que lo inicia todo

 // List of pointers to functions that serve different modes void (*Modes[])() = { rainbow, slidingRainbow, orangeBeacon, policeBeacon, trafficLights, stars, randomColorsFadeInOut }; void loop() { static uint8_t mode = eeprom_read_byte( (uint8_t*) 10 ); static bool waitingForBtnUp = false; static long btnPressTimeStamp; // Button switches mode if(digitalRead(BUTTON_PIN) == HIGH && !waitingForBtnUp) { delay(20); if(digitalRead(BUTTON_PIN) == HIGH) { mode++; mode %= ARRAY_SIZE(Modes); // num modes clearPixels(); ws2811.sendLedData(); delay(1); eeprom_write_byte( (uint8_t*) 10, mode ); waitingForBtnUp = true; btnPressTimeStamp = millis(); shutdownOnTimeOut(true); } } // Shut down on long press over 5s if(digitalRead(BUTTON_PIN) == HIGH && waitingForBtnUp && millis() - btnPressTimeStamp > 5000) shutDown(); // Detect button release if(digitalRead(BUTTON_PIN) == LOW && waitingForBtnUp) waitingForBtnUp = false; // display LEDs according to current mode Modes[mode](); // pong shutdown timer shutdownOnTimeOut(); // Yes, we still alive wdt_reset(); } 

Al presionar el botón se cambian los modos y se restablece el temporizador de apagado automático. Dependiendo del modo actual, se inicia una de las funciones de efectos de la lista Modos. En cada ciclo, el perro guardián también se reinicia.

Si un niño, por ejemplo, jugaba con un automóvil policial y después de 1,5 minutos se apagaba la luz de emergencia, lo más probable es que después de un segundo turno el hijo quiera seguir jugando el automóvil policial. Para hacer esto, el modo seleccionado se guarda en EEPROM (el número de celda 10 se selecciona del bulldozer).

Aquí hay un video que muestra cómo funciona todo.


Cargador de arranque


Casi todo está listo. Pero hay una cosa más que debe archivarse: un gestor de arranque. El hecho es que el gestor de arranque estándar no nos conviene.

En primer lugar, cuando enciende la alimentación, espera hasta 6 segundos; tal vez el firmware comenzará a fluir. Solo después de que este control se transfiere al firmware principal. Esto es conveniente en la etapa de desarrollo, pero será molesto en el dispositivo terminado.

Y en segundo lugar, el gestor de arranque estándar no sabe nada sobre el chip PT1502, lo que sería bueno dar una señal de RETENCIÓN. Sin esta señal, el microcircuito piensa que el microcontrolador no se inició o, por el contrario, quiere apagarse. Y si es así, luego de unos pocos milisegundos, el PT1502 cortará la alimentación de todo el circuito.

El beneficio de solucionar ambos problemas no es difícil. El digispark ATTiny85 utiliza el gestor de arranque de micronúcleos . Este gestor de arranque es lo suficientemente fácil de archivar para nuestras necesidades. Solo es necesario corregir las definiciones correspondientes en el archivo de configuración.

En primer lugar, copié la configuración estándar de firmware \ configuración \ t85_default en mi propio directorio y ya hice todos los cambios en él. Por lo tanto, en caso de que sea fácil volver al cargador de arranque original.

En el archivo bootloaderconfig.h, hay una opción de cómo ingresar al gestor de arranque. De lo que se ofrece de fábrica, nada nos conviene, pero la opción más cercana es ENTRY_JUMPER. En esta opción, se accede al gestor de arranque solo si aparece un cierto nivel en un pin específico (el puente está cerrado en el tablero).

 #define ENTRYMODE ENTRY_JUMPER 

No tenemos un puente, pero hay un botón en el pie de PB2. Deje que el gestor de arranque entre si el botón se mantiene presionado durante 5-7 segundos cuando se enciende la alimentación. Pero si se presiona y suelta, la transición al firmware principal se produce de inmediato.

Necesitamos definir 3 funciones: inicialización, desinicialización y, de hecho, verificar si es hora de ingresar al gestor de arranque. En el original, todos son simples e implementados con macros. Solo los primeros 2 serán simples

 #define HOLD_PIN PB1 #define JUMPER_PIN PB2 #define JUMPER_PORT PORTB #define JUMPER_DDR DDRB #define JUMPER_INP PINB #define bootLoaderInit() {JUMPER_DDR &= ~_BV(JUMPER_PIN); JUMPER_DDR |= _BV(HOLD_PIN); JUMPER_PORT &= ~_BV(JUMPER_PIN); JUMPER_PORT |= _BV(HOLD_PIN); _delay_ms(1);} #define bootLoaderExit() {;} 

bootLoaderInit () configura el pin del botón (JUMPER_PIN) en la entrada y apaga el suspensor. Ya tenemos un pull-up en el tablero y en el suelo, y cuando presionas un botón en el pin, por el contrario, habrá uno. Al mismo tiempo, puede configurar inmediatamente la señal HOLD para que salga y configurar la unidad para que ...

Para obtener una explicación de la aritmética de bits, por ejemplo, vaya aquí , y se puede obtener una comprensión de los registros de configuración GPIO en los controladores AVR, por ejemplo, desde aquí .

La función bootLoaderExit () está vacía porque la configuración expuesta es bastante adecuada para la transición posterior al firmware principal

La función bootLoaderStartCondition (), que es responsable de ingresar el gestor de arranque en el formato macro, no se ha adaptado y, por lo tanto, se ha convertido en una función completa

 #ifndef __ASSEMBLER__ // Bootloader condition is to hold the button for 5 seconds inline unsigned char bootLoaderStartCondition() { long int i; for(i=0; i<10000000; i++) if( !(JUMPER_INP & _BV(JUMPER_PIN))) return 0; return 1; } #endif 

La función dentro de unos segundos (de hecho, aproximadamente 6-7) verifica el estado del botón. Si el botón se soltó antes, entonces no necesitamos ingresar al gestor de arranque. Paciente y persistente están permitidos en el gestor de arranque.

Al final resultó que, el archivo bootloaderconfig.h está involucrado en la compilación de archivos ensambladores y el código en este archivo causa errores. Tuve que poner la función en el bloque #ifndef __ASSEMBLER__

Otro parámetro que modifiqué le dice al gestor de arranque qué hacer si no está conectado a USB: salga después de un segundo. El hecho es que durante el robo, el hijo a menudo presionó el botón y accidentalmente entró en el gestor de arranque. No sé qué milagroso, pero si el gestor de arranque no vio la conexión USB, podría sobrescribir accidentalmente algunas páginas de memoria. Por lo tanto, si no hay conexión, simplemente saldremos al programa principal.

 /* * Define bootloader timeout value. * * The bootloader will only time out if a user program was loaded. * * AUTO_EXIT_NO_USB_MS The bootloader will exit after this delay if no USB is connected. * Set to 0 to disable * Adds ~6 bytes. * (This will wait for an USB SE0 reset from the host) * * All values are approx. in milliseconds */ #define AUTO_EXIT_NO_USB_MS 1000 

Compilamos ... y recibimos un error de que el código no cabe en el espacio del cargador de arranque asignado a él. Dado que la memoria flash en el controlador es muy pequeña, el gestor de arranque se exprime al máximo para dejar más espacio para el programa principal. Pero esto se soluciona fácilmente en el archivo Makefile.inc siguiendo las instrucciones

 # hexadecimal address for bootloader section to begin. To calculate the best value: # - make clean; make main.hex; ### output will list data: 2124 (or something like that) # - for the size of your device (8kb = 1024 * 8 = 8192) subtract above value 2124... = 6068 # - How many pages in is that? 6068 / 64 (tiny85 page size in bytes) = 94.8125 # - round that down to 94 - our new bootloader address is 94 * 64 = 6016, in hex = 1780 BOOTLOADER_ADDRESS = 1940 

Luego, reduje la dirección de inicio del cargador de arranque a una página (64 bytes), aumentando así el espacio del cargador de arranque.

De lo contrario, compilar y cargar el gestor de arranque utilizando el programador USBAsp no fue un problema.

Conclusión


Fue una forma muy interesante de un prototipo en una placa de prueba a un dispositivo terminado. Parece un intermitente ordinario de una lección de arduino, pero de hecho, en el proceso de trabajo, tuve que resolver un montón de problemas interesantes: aquí están la lucha con el consumo, la elección de la base del elemento y el diseño del caso, y recordar el firmware con el gestor de arranque. Espero sinceramente que mi experiencia sea útil para alguien.

¿Pudo haber sido más fácil? Por supuesto que puedes. Creo que todo se podría hacer con un transistor. Desafortunadamente , leí este artículo después de soldar la placa. Vería el artículo antes, haría todo en el mismo TP4056 popular, es más fácil soldarlo. De todos modos, el convertidor DC-DC, que está dentro del PT1502 en este dispositivo, para bien, no es necesario. Sin embargo, un estudio práctico del microcircuito PT1502 me es útil para mi otro proyecto, así como la capacidad de soldar microcircuitos en el paquete QFN20.

Finalmente, aquí están los enlaces a mi proyecto:

Código de firmware
Circuito y placa
Modelo de carcasa y difusor
Modelos STL listos para imprimir

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


All Articles