Arduin e LED, ou como atualizar o designer infantil



Meu filho firmemente "fisgou" o construtor magnético Magformers . Depois de examinar uma série de Fixiks em que o mesmo construtor foi apresentado, a criança perguntou: "Pai, por que os fixics têm detalhes que brilham, mas não temos?".

Descobriu-se que realmente existe um "Magformers Neon LED Set", onde, além dos componentes comuns, também há um elemento com um LED. Como nessa época já tínhamos reunido uma caixa inteira de ímãs de todas as formas e tamanhos possíveis (quanto a mim, os formadores de magias chineses não são nada inferiores ao original), de alguma forma eu não queria comprar outro conjunto apenas por uma lâmpada. Além disso, este conjunto custa significativamente mais do que um similar sem luz de fundo.

Tendo calculado que há apenas alguns dólares nos componentes, a maioria dos quais eu já tinha, decidi coletar minha morgulka. Sim, e com efeitos que o original não teve.

Sob o gato, você encontrará a opção de um pisca-pisca no ATTiny85 e o painel de LED nos LEDs WS8212. Vou falar sobre circuitos, como eu alimentei tudo isso com a bateria, bem como com os problemas não óbvios que resolvi ao longo do caminho. Também falarei em detalhes sobre o componente de software do projeto.

Primeiros passos


Pareceu-me que o brilho de um LED comum (mesmo RGB) é chato e banal. Mas sentir algo como WS8212 parecia interessante. No ebee, foram oferecidos LEDs e matrizes individuais de até 16x16 em tamanho. Tendo comprado vários módulos diferentes, decidi por uma matriz 4x4. Existem muitos LEDs para permitir vários efeitos visuais, enquanto o módulo é comparável em tamanho à janela do bloco quadrado do designer.



Para controlar a matriz de LEDs, basta apenas um pino do microcontrolador, de modo que até o arduino nano parece um busto (além disso, ele não se encaixa no gabinete). Mas o clone do digispark no controlador ATTiny85 acabou sendo perfeito - ele não possui muita memória e pinos, mas é suficiente para o pisca-pisca de LED. O módulo se integra perfeitamente ao IDE do Arduino e possui um carregador de inicialização USB a bordo, portanto, a programação deste módulo é muito simples e confortável. Há muito tempo que queria experimentar.

Começou com o esquema mais simples.



Nesta forma, foi possível depurar rapidamente todos os algoritmos de brilho / piscada (sobre eles abaixo). Mas um brinquedo movido a fio não é o caso - você precisa pensar na energia da bateria. Além disso, para não falir com as baterias digitais (que, além disso, não cabem no envelope), foi decidido o uso de lítio. E como há uma bateria de lítio, você precisa pensar em como carregá-la. Nas caixas, acabamos de encontrar um controlador de carregamento "popular" no chip TP4056 que foi comprado na ocasião.

Mas não funcionou imediatamente. O circuito do módulo Digispark ATTiny85 não é muito projetado para isso - existe energia USB, mas a energia é fornecida diretamente ao microcontrolador (através do barramento +5) ou da entrada VIN, mas a energia passa pelo estabilizador linear 7805. Opção quando o módulo de carregamento de lítio inserido no espaço entre o conector USB e o microcontrolador não é fornecido. Eu tive que modificar um pouco o circuito e remover os detalhes extras.



Portanto, agora a energia USB é fornecida ao pino VIN e depois é direcionada à entrada do carregador. A saída do carregador (na verdade, a bateria está conectada diretamente) volta para a placa através do pé de 5V. E, embora de fato haja de 3 a 4,2V (tensão da bateria), isso é bastante normal - a faixa de tensão operacional do microcontrolador é de 1,8-5,5V. E até o módulo LED funciona normalmente de 2,7V, embora abaixo de 3,2V o LED azul esteja um pouco ausente e as cores “flutuem” um pouco em amarelo.

Para economizar energia, o LED D2 sempre ativo também desapareceu. O esquema geral agora se parece com isso



Seria possível alimentar o circuito através do conector USB no carregador, mas a capacidade de fazer o upload do firmware através do conector USB na placa controladora seria perdida. Seria possível deixar dois conectores USB para vários fins - um para carregar e outro para firmware, mas isso está de alguma forma errado.

Comprei uma bateria de tamanho 6x25x35 no ebay, mas ela estava com defeito ou a matei com um curto-circuito ou uma grande corrente de carga (por padrão, a corrente de carga é definida como 1A e você precisa soldar um resistor para reduzir a corrente). De qualquer forma, quando a carga foi conectada, mesmo a 10 mA, a voltagem da bateria caiu para 1V. No momento do teste, troquei para uma bateria LiPo meio morta de um pequeno quadrocóptero. Um pouco mais tarde, pedi a bateria de outro vendedor e ela ficou boa.

Em princípio, seria possível parar com isso, soldar os fios de conexão e empurrar tudo suavemente para algum tipo de alojamento, mas decidi medir o consumo do circuito. E então eu chorei. Bem, em condições de trabalho (quando as lâmpadas brilham completamente), essa coisa consome até 130mA; portanto, em repouso, o consumo é superior a 25mA! I.e. esse pisca-pisca consome minha bateria de 600mAh em menos de um dia!

Verificou-se que cerca de 10 mA consomem LEDs. Mesmo se eles não acenderem, um microcontrolador ainda funciona em cada um deles e aguarda um comando. I.e. Você precisa criar um circuito de desligamento para os LEDs.

Os 15 mA restantes são consumidos pelo microcontrolador. Sim, pode ser colocado na cama e, de acordo com a ficha técnica, o consumo será medido por microamperes, mas, na verdade, não foi possível obter menos de 1 mA. Desliguei o ADC e traduzi os pinos em entrada. Parece que em algum lugar do circuito há algum tipo de vazamento, mas meu conhecimento modesto de eletrônica não é suficiente para encontrá-lo e compreendê-lo.

Nós complicamos o esquema


Lembrei-me de que comprei um chip PT1502 para um teste. Este chip é um controlador de carga de bateria de lítio completo com uma fonte de alimentação com várias entradas de controle. A única dificuldade é que o microcircuito é fornecido em uma embalagem QFN20 de 4x4 mm e requer algumas correias. Soldar isso em casa é difícil, mas possível. A taxa é difícil para um LUT regular e deve ser solicitada aos chineses. Mas não temos medo de dificuldades, certo?

Em várias caixas, o esquema pode ser descrito da seguinte maneira.



No estado desligado, a energia não é fornecida ao controlador e aos LEDs. O dispositivo possui um botão 'Power' que liga o pisca-pisca (ele também alterna os modos). O LED brilha, digamos, um minuto e, se não houver atividade do usuário (ninguém pressiona um botão), o dispositivo desliga. I.e. Ele não apenas dorme, mas desliga a energia pelo sinal Power Hold. E desliga tudo de uma vez - tanto o microcontrolador quanto os LEDs. A funcionalidade de ligar e desligar é implementada dentro do chip PT1502

Tudo o que resta é desenhar um diagrama de circuito e fazer uma placa de circuito. O circuito é, na maioria das vezes, associado à folha de dados do PT1502 e ao módulo Digispark ATTiny85. O microcircuito do controlador de potência PT1502 é funcionalmente dividido em várias partes, portanto, é dividido em blocos no circuito.



Na verdade, este é um controlador de carga de bateria de lítio com seu próprio chicote. LED1 indica o estado da carga, a carga está ligada. O resistor R6 define a corrente de carga para 470mA. Desde que eu tenho uma bateria de 600mAh, em princípio, você pode aumentar a corrente e colocar um resistor em 780-800 Ohms até 600mA. No entanto, não tenho certeza da qualidade especial da minha bateria - é melhor carregar mais devagar, mas ela durará mais.

Considere um plano de energia



O botão SW1 inicia todo o sistema - o chip PT1502 acorda sozinho e, em seguida, inicia todas as fontes de energia (das quais possui 3). Quando a energia é instalada, o microcircuito inicia o microcontrolador liberando o sinal RESET. Para facilitar a depuração, também adicionei um botão Redefinir separado.

O sinal HOLD é usado para desligar todo o sistema. Quando o microcontrolador é iniciado, ele deve definir a unidade nesta linha. Quando é hora de terminar, o microcontrolador define zero na linha HOLD e o chip de potência PT1502 interrompe todas as fontes de energia.

Ainda seria possível rastrear a carga da bateria fraca usando a saída BAT_LOW, mas neste artigo eu a classifiquei - você não precisa salvar nenhum dado e nada explodirá se você não perceber uma bateria descarregada a tempo. Morre assim morre. Mas por precaução, o conselho forneceu contato para esse negócio.

Vamos voltar ao botão SW1 por um segundo. Decidi não fazer 2 botões separados para ligar e controlar. Portanto, o mesmo botão também é conectado ao ATTiny85 e, durante a operação, alterna os modos de piscar. Os valores do divisor R7-R8 são selecionados para não queimar a porta do microcontrolador PB2. Para todas as faixas de tensão da bateria (3,3 - 4,2V), a tensão será fornecida ao pé do controlador dentro dos limites especificados da folha de dados (0,7 * VCC - VCC + 0,5V)

Considere uma fonte de energia



Este é um conversor DC-DC pulsado. A tensão de saída é definida pelos resistores R10-R11 e, de acordo com a fórmula da folha de dados, é definida como 3,3V. Tudo o resto é simples.

Para o bem, uma fonte de energia tão complicada não é realmente necessária - seria possível alimentar o microcontrolador em geral diretamente da bateria. Só que essa fonte já está implementada no chip PT1502 e pode ser ligada / desligada quando necessário - por que não usá-la?



O chip também possui 2 estabilizadores lineares, mas não os utilizarei. Infelizmente, como se viu, ainda é necessário fornecer a tensão de entrada para essa fonte; caso contrário, o microcircuito acha que a energia ainda não é estável o suficiente e não inicia o microcontrolador (esse conhecimento me foi dado por uma semana soldando a placa de teste) - eu não conseguia entender por que ela não funciona. )

Vamos para a parte lógica.



O cabo USB é dobrado da placa Digispark inalterado. Isso é necessário para coordenar a tensão do USB (que executa 3,3V) e os sinais do microcontrolador (que no original é alimentado por 5V). Como no meu caso o microcontrolador também é alimentado por 3,3V, o circuito pode ser simplificado, mas, por precaução, eu me divorciei do circuito original na placa.



Não há nada de interessante na ligação do microcontrolador.

O toque final é o conector



Na verdade, eu adquiri uma placa de depuração para mim no ATTiny85 com suporte a USB e um controlador de energia com uma bateria de lítio. Portanto, não me limitei apenas a emitir a linha para o LED. Em vez disso, trouxe todas as linhas do microcontrolador para o pente - ao mesmo tempo, é conveniente conectar-se ao programador.

E deixe quase todas as linhas atadas rigidamente a uma certa funcionalidade (PB1 - Linha de espera, PB2 - botão liga / desliga, PB3 / PB4 - USB, PB5 - Reset) no futuro, será possível ignorar alguns limites. Por exemplo, não solde o cabo USB e solte as linhas PB3 / PB4. Ou, por exemplo, recusar uma redefinição e liberar PB5. Enquanto isso, apenas o PB0 permanece livre - e conecte nosso LED a ele.

Passamos para o quadro. Dadas as limitações no tamanho da placa em 40x40mm, o número de componentes e a caixa QFN20 do chip PT1502, eu nem sequer considerei a fabricação da placa em casa. Portanto, comecei imediatamente a criar a placa de duas camadas mais compacta. Foi o que eu consegui



Para facilitar o uso, no verso, assinei todas as funções de saída possíveis (recebi a idéia do quadro do Digispark)



Eu pedi o conselho no JLCPCB . Para ser sincero, não estou muito satisfeito com a qualidade - se você solda o chip muitas vezes, a máscara perto dos pequenos contatos do PT1502 fica um pouco nublada. Bem, pequenas inscrições flutuaram um pouco. No entanto, se tudo for soldado pela primeira vez, então as normas.

Para soldar QFN20, você precisa de um ferro de soldar, tudo o mais pode ser soldado com um certo ferro de solda com uma certa habilidade. É assim que a placa soldada se parece



Habitação


É hora de seguir para o casco. Imprimi-o em uma impressora 3D. Design sem frescuras - caixa e botão. Ganchos especiais são fornecidos na caixa para instalar o vaga-lume no módulo quadrado padrão do designer.



A placa principal e a bateria ficam no gabinete.





O painel de LEDs é montado na tampa, que por sua vez é parafusada na caixa principal com parafusos

No começo, pensei em parafusar o painel de LED na tampa com parafusos, mas no final apenas o colei em uma fita dupla face. Acabou assim



Nesta forma, o dispositivo já pode ser usado, mas ainda parece feio - não há difusor suficiente.

Tentei fazer a primeira versão do difusor usando a tecnologia de encolhimento de garrafas PET com um secador de cabelo para construção (espiado nos modelos de aeronaves).

Então, primeiro você precisa de um espaço em branco. Fiz de gesso, que coloquei em um formulário que imprimi em uma impressora 3D. Na primeira versão, o formulário era uma peça e eu nunca fui capaz de puxar o disco fundido. Portanto, eu tive que fazer um formulário de duas peças.



A ideia do método é a seguinte. Você coloca uma garrafa de iogurte em um espaço em branco e a senta com um secador de cabelo. Aqui estão apenas re-portando pedaços de 20 contêineres diferentes sob leite diferente. Eu nunca consegui colocar essa coisa muito bem, sem dobras e bolhas. Aparentemente, você precisa cercar algum tipo de instalação a vácuo e assentar a folha de plástico. Em geral, acabou sendo muito difícil para esse tipo de embarcação.

Depois de resmungar pelos esquilos, encontrei um par de metros da sonda de plástico Verbatim PET Transparent. Decidi experimentar o difusor apenas para imprimir. E, embora na entrada da impressora, o plástico pareça cristalino, a parte real é opaca. Provavelmente isso se deve à estrutura interna, pois as camadas não preenchem completamente o volume, mas se sobrepõem a lacunas e lacunas. Além disso, se você tentar processar a peça com uma lixa para obter uma superfície mais lisa, obteremos ainda mais mate. No entanto, é exatamente isso que eu precisava.

Eu estava com preguiça de me preocupar com o suporte do difusor, então o adicionei à cola quente. Portanto, meu design agora é condicionalmente dobrável. Eu poderia estar confuso com a invenção de algum tipo de trava, mas já fiquei sem a sonda de plástico transparente. Então deixe derreter quente.





Firmware


Para piscas de LED, você não precisa mergulhar particularmente na periferia do microcontrolador - apenas algumas funções para trabalhar com o GPIO são suficientes. Mas como o módulo está encaixado na plataforma Arduino, por que não tirar proveito disso?

Primeiro, algumas definições e 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 

Isso determina o número de pixels na minha matriz, números de pinos e o brilho máximo dos LEDs (durante a depuração, era conveniente configurá-lo em 50 para que não cegasse meus olhos)

Os LEDs na minha matriz são organizados de uma maneira não óbvia - um zigue-zague. Portanto, para efeitos diferentes, tive 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 os LEDs, não reinventei a roda e peguei uma biblioteca pronta para trabalhar com os LEDs WS8211 . A interface da biblioteca é ligeiramente caiada de branco. Algumas funções auxiliares (por exemplo, a conversão de HSV para RGB) também ficaram presas lá.

Primeiro, a placa e a biblioteca WS8211 precisam ser inicializadas.

 // 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); } 

Antes de tudo, é necessário definir o sinal POWER HOLD como unidade - este será um sinal para o chip PT1502 que o microcontrolador acabou e está funcionando corretamente. O microcircuito, por sua vez, fornecerá regularmente eletricidade ao microcontrolador e LEDs, desde que o sinal HOLD esteja definido como unidade.

Em seguida, as pernas para controlar o LED na saída e os botões na entrada são configurados. Depois disso, você pode inicializar a biblioteca WS8211.

Como este é um dispositivo bastante autônomo, não se pode permitir que o microcontrolador fique em um estado incompreensível e consuma a bateria inteira. Para fazer isso, inicio o timer do watchdog por 2 segundos. O cronômetro será reiniciado no loop principal do programa.

Agora você precisa definir algumas funções auxiliares. A biblioteca WS8211 armazena um buffer com os valores de cores de cada LED. Trabalhar diretamente com o buffer não é muito conveniente, porque escrevi uma função simples para gravar valores RGB em um 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; } 

Mas na maioria dos casos, no modelo de cores RGB, contar cores não é muito conveniente ou mesmo impossível. Por exemplo, ao desenhar qualquer tipo de arco-íris, é mais conveniente trabalhar com o modelo de cores HSV . A cor de cada pixel é definida pelo valor do tom e brilho da cor. A saturação é omitida por simplicidade (o máximo é usado). Os valores de matiz são reduzidos para um intervalo de 0 a 255 (em vez do padrão de 0 a 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) ); } 

A função é retirada da biblioteca Ai_WS8211 e ligeiramente arquivada. Na versão original dessa função da biblioteca, havia alguns bugs, devido aos quais a cor do arco-íris era mostrada com empurrões.

Vamos seguir para a implementação de vários efeitos. Cada função é chamada do loop principal para desenhar um "quadro". Como cada efeito opera com parâmetros diferentes entre as chamadas, eles são armazenados em variáveis ​​estáticas.

Esse é o efeito mais simples - todos os LEDs são preenchidos com uma cor, que muda 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); } 

O próximo efeito é mais interessante - exibe um arco-íris ao longo do contorno da matriz e as cores no arco-íris mudam gradualmente em um 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); } 

E esse efeito preenche toda a matriz com uma cor aleatória, que primeiro acende suavemente e depois também sai 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); } 

O próximo grupo de efeitos desenha diferentes faróis piscantes. Assim, por exemplo, uma criança gosta de construir um trator com ímãs e um pisca-pisca laranja será muito útil lá.

 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); } 

Tecnicamente, o efeito parece um ponto brilhante que se move ao longo da matriz. Mas, para ficar bonito, os LEDs vizinhos desaparecem gradualmente à medida que você se afasta do ponto principal. Portanto, eu precisava de uma função que calcula esse mesmo brilho.

 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 é uma certa posição condicional do ponto luminoso de brilho, mapeado para um intervalo de loopback de 0 a 255. ledPos é a posição do LED (exibido na mesma faixa) cujo brilho você precisa calcular. Se a diferença de posição for maior que delta, o LED não acenderá e, quanto mais próximo da posição, mais brilhante brilha.

Ou, por exemplo, uma luz piscante vermelho-azul da polícia

 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 falando de carros, o semáforo aqui não é um problema para implementar.

Estas são funções que incluem vários sinais de trânsito em várias posições.

 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(); } 

É hora de revivê-lo. O semáforo opera de acordo com um programa especial definido em uma espécie de bytecode. A placa descreve o modo e a hora em que esse modo deve ser ativado.

 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 }; 

Na verdade, a função que processa tudo

 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); } 

Ao atingir o intervalo de tempo especificado, o próximo modo de semáforo é ativado e a contagem regressiva inicia novamente.

O último efeito sobre o qual minha imaginação foi suficiente são asteriscos. 5 LEDs aleatórios acendem com brilho aleatório e depois desligam sem problemas. Se uma estrela se apagar, outra em um local aleatório acenderá.

 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); } 

Em algum lugar aqui um inseto maligno apareceu. Às vezes, as estrelas se iluminam bruscamente ou vice-versa desaparecem abruptamente. Mas, para ser sincero, eu estava com preguiça de descobrir - parece bem normal.

É hora de pensar em economizar bateria. Eu já dei os valores de consumo dessa coisa toda. Se você não pensar em desligar a energia, os LEDs consumirão a bateria em algumas horas. Essa função é responsável por desligar a energia após 90 segundos de inatividade. Inicialmente, eram 60 segundos, mas com um jogo real isso não era suficiente, e 2 minutos eram de alguma forma longos.

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

Na verdade, o desligamento ocorre da seguinte maneira.

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

Se o usuário pressionar os botões, o temporizador é redefinido. Após o tempo definido, a função define o sinal HOLD para zero, que é um comando PT1502 para desligar a energia. O Watchdog, a propósito, também precisa ser interrompido; caso contrário, após 2 segundos, ele despertará o sistema e ligará a energia novamente.

Finalmente, o loop principal que inicia tudo

 // 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(); } 

Pressionar o botão alterna os modos e redefine o timer de desligamento automático. Dependendo do modo atual, uma das funções de efeito da lista Modos é iniciada. Em cada ciclo, o watchdog também é redefinido.

Se uma criança, por exemplo, estava brincando com um carro da polícia e depois de 1,5 minutos a luz de emergência se apagasse, provavelmente depois de um segundo turno, o filho vai querer continuar brincando. Para fazer isso, o modo selecionado é salvo na EEPROM (o número de célula 10 é selecionado no bulldozer).

Aqui está um vídeo que mostra como tudo funciona.


Carregador de inicialização


Quase tudo está pronto. Mas há mais uma coisa que precisa ser arquivada - um gerenciador de inicialização. O fato é que o carregador de inicialização padrão não é adequado para nós.

Em primeiro lugar, quando você liga a alimentação, ela espera por 6 segundos - talvez o firmware comece a derramar nele. Somente após esse controle ser transferido para o firmware principal. Isso é conveniente no estágio de desenvolvimento, mas será irritante no dispositivo acabado.

E segundo, o carregador de inicialização padrão não sabe nada sobre o chip PT1502, o que seria bom para dar um sinal de espera. Sem esse sinal, o microcircuito pensa que o microcontrolador não foi iniciado ou, pelo contrário, deseja desligar. E, nesse caso, depois de alguns milissegundos, o PT1502 cortará a energia de todo o circuito.

O benefício de corrigir os dois problemas não é difícil. O digispark ATTiny85 usa o gerenciador de inicialização de micronúcleos . Este gerenciador de inicialização é fácil o suficiente para arquivar para nossas necessidades. É necessário apenas corrigir as definições correspondentes no arquivo de configuração.

Primeiro, copiei a configuração padrão de firmware \ configuration \ t85_default para meu próprio diretório e já fiz todas as alterações nele. Portanto, será fácil reverter para o gerenciador de inicialização original.

No arquivo bootloaderconfig.h, há uma maneira de inserir o carregador de inicialização. Pelo que é oferecido imediatamente, nada nos convém, mas a opção mais próxima é ENTRY_JUMPER. Nesta opção, o gerenciador de inicialização é acessado apenas se um determinado nível aparecer em um pino específico (o jumper está fechado na placa).

 #define ENTRYMODE ENTRY_JUMPER 

Não temos um jumper, mas há um botão no pé do PB2. Deixe o carregador de inicialização entrar se o botão for pressionado por 5-7 segundos quando a energia for ligada. Mas se pressionado e liberado, a transição para o firmware principal ocorre imediatamente.

Precisamos definir três funções - inicialização, desinicialização e realmente verificar se é hora de entrar no gerenciador de inicialização. No original, todos são simples e implementados com macros. Apenas os 2 primeiros serão 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 o pino do botão (JUMPER_PIN) na entrada e desativa o suspensor nela. Já temos um pull-up no quadro e no chão, e quando você pressiona um botão no pino, pelo contrário, haverá um. Ao mesmo tempo, você pode configurar imediatamente o sinal HOLD para emitir e definir a unidade para ele ...

Para obter uma explicação da aritmética de bits, por exemplo, acesse aqui , e uma compreensão dos registros de configuração GPIO nos controladores AVR pode ser obtida, por exemplo, a partir daqui .

A função bootLoaderExit () está vazia porque a configuração exposta é bastante adequada para a transição subsequente para o firmware principal

A função bootLoaderStartCondition (), responsável por inserir o carregador de inicialização no formato macro, não se encaixa e, portanto, tornou-se uma função 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 

A função dentro de alguns segundos (de fato, cerca de 6-7) verifica o status do botão. Se o botão foi liberado anteriormente, não precisamos entrar no gerenciador de inicialização. Paciente e persistente são permitidos no gerenciador de inicialização.

Como se viu, o arquivo bootloaderconfig.h está envolvido na compilação dos arquivos do assembler e o código nesse arquivo causa erros. Eu tive que colocar a função no bloco #ifndef __ASSEMBLER__

Outro parâmetro que eu ajustei diz ao gerenciador de inicialização o que fazer se não estiver conectado ao USB - saia após um segundo. O fato é que, durante o arrombamento, o filho frequentemente pressionava o botão e acidentalmente entrava no gerenciador de inicialização. Não sei como é milagroso, mas se o carregador de inicialização não viu a conexão USB, ele pode substituir acidentalmente algumas páginas de memória. Portanto, se não houver conexão, simplesmente sairemos para o 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 

Nós compilamos ... e obtemos um erro de que o código não se encaixa no espaço do carregador de inicialização alocado para ele. Como a memória flash no controlador é muito pequena, o gerenciador de inicialização é compactado ao máximo para deixar mais espaço para o programa principal. Mas isso é facilmente corrigido no arquivo Makefile.inc, seguindo as instruções.

 # 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 

Depois, reduzi o endereço inicial do carregador de inicialização para uma página (64 bytes), aumentando assim o espaço do carregador de inicialização.

Caso contrário, compilar e fazer upload do gerenciador de inicialização usando o programador USBAsp não foi um problema.

Conclusão


Era uma maneira muito interessante de um protótipo em uma placa de ensaio para um dispositivo acabado. Parece um pisca-pisca comum de uma aula de arduino, mas, de fato, no processo de trabalho, tive que resolver um monte de problemas interessantes - aqui estão a luta com o consumo, a escolha da base do elemento e o design do caso, e trazendo à mente o firmware com o gerenciador de inicialização. Espero sinceramente que minha experiência seja útil para alguém.

Poderia ter sido mais fácil? Claro que você pode. Eu acho que tudo poderia ser feito com um transistor. Infelizmente , li este artigo depois de soldar o quadro. Eu veria o artigo anteriormente - eu faria tudo no mesmo TP4056 popular - é mais fácil soldá-lo. De qualquer forma, o conversor DC-DC, que está dentro do PT1502 neste dispositivo, definitivamente, não é necessário. No entanto, um estudo prático do microcircuito PT1502 é útil para mim em meu outro projeto, bem como a capacidade de soldar microcircuitos no pacote QFN20.

Finalmente, aqui estão os links para o meu projeto:

Código de firmware
Circuito e placa
Modelo de caixa e difusor
Modelos STL prontos para impressão

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


All Articles