Manual

do

Maker

.

com

Roleta eletrônica com Arduino e FreeRTOS

Roleta eletrônica com Arduino e FreeRTOS

Dado eletrônico ou roleta eletrônica? Bem, não importa muito.

O projeto é simples, mas agradável; o buzzer builtin da placa MaxBlitz ajudou a dar o efeito sonoro de catraca e usando o FreeRTOS pude mostrar uma outra forma de monitorar um pino. Mas usar FreeRTOS no Arduino não é igual utilizar em uma placa própria para isso.

Roleta eletrônica

Meu propósito era mostrar alguns recursos de software diferentes, afinal, fazer um dado eletrônico ou uma roleta eletrônica (como preferir chamar) é uma tarefa simples. Mas pode ser bem interessante.

O código está em implementação, quero adicionar mais recursos, mas com o pouco implementado já pude notar que o FreeRTOS no Arduino não funciona tão "liso" como em um ESP32 ou em um STM32.

Matriz de LEDs 8x8

Utilizar a matriz de LEDs é só um dos recursos pretendidos. Na verdade, quero fazer algo mais utilizando o PCF8574 contido na placa da MaxBlitz e criar um jogo com a Roleta eletrônica. O código para isso já está implementado e (aparentemente) funcional, mas vou escrevendo artigos conforme adicionar recursos.

A matriz de LEDs pode ser adquirida no nosso parceiro Curto Circuito.

Arduino standalone MaxBlitz

Já a placa MaxBlitz é um pouco mais complicada, porque é feita em lotes limitados e acaba muito rápido, mas você pode acompanhar no site do criador. Escrevi sobre esse standalone há poucos artigos antes desse.

Código do projeto

Números da roleta eletrônica

Compus uma matriz bidimensional para guardar os números. Já é algo pouco usual nos projetos makers, então foi uma oportunidade de mostrar o recurso:

uint8_t numbers[8][8] = {{B00000000, B00000000, B01111111, B01000001, B01000001, B01111111, B00000000, B00000000},
                       {B00000000, B00000000, B00000001, B01111111, B01100001, B00100000, B00000000, B00000000},
                       {B00000000, B00110001, B01111001, B01000101, B01100101, B00100011, B00000000, B00000000},
                       {B00000000, B01101110, B01010011, B01010001, B01000001, B01000010, B00000000, B00000000},
                       {B00000000, B00000000, B01111111, B00001000, B00001000, B01111000, B00000000, B00000000},
                       {B00000000, B00000000, B01001110, B01010001, B01010001, B01110001, B00000000, B00000000},
                       {B00000000, B00000110, B01001001, B00101001, B00010001, B00001110, B00000000, B00000000},
                       {B00000000, B01110000, B01001000, B01000100, B01000011, B01000000, B00000000, B00000000}};

Não precisaria ser em binário, mas é uma forma de visualizar como se fosse pontos. Se colocar cada octeto um embaixo do outro, seria como se o número estivesse deitado para a esquerda. Primeiro desenhei no papel e então escrevi o array.

Efeito sonoro

O som é a coisa mais simples do mundo, bastou tocar com a função tone passando o intervalo de tempo como parâmetro.

void plac(int timeToPlac){
    tone(mbz.buzzer_pin,100,timeToPlac);
}

Para guardar os parâmetros da placa, criei uma struct, que deixa a lógica mais clara, fácil de achar onde a variável foi definida e a struct também permite cópias para outros propósitos. Não vai dar pra mostrar a principal utilidade da struct nesse programa, mas ela ficou assim:

struct MaxBlitz{
  int stat : 1;
  uint8_t buzzer_pin  = 3;
  uint8_t button      = 4;
  uint8_t builtin_led = 13; 
  uint8_t pcf_address = 0x20;

  uint8_t pcf_pin[8]  = {1,2,4,8,16,32,64,128};

} mbz;

Função para blink

O blink é o "hello world" da eletrônica digital, certo? Usamos pinModedigitalWrite em Arduino, mas com essa função não é necessário fazer o pinMode. Ela verifica se está configurado como OUTPUT e, se não estiver, faz o ajuste diretamente no registrador do respectivo pino. Se for passado 0 no loop, essa função apenas liga o LED.  Utilizei o LED builtin para saber quando passou pela função setup() e a partir de então a roleta eletrônica já estará pronta para uso.

Olha só que interessante; o registrador dos pinos 0 a 7 é o DDRB e o registrador do pino 8 ao 13 é o DDRD. Então a função verifica qual foi o pino solicitado e verifica se o pino já foi configurado como OUTPUT. Se não foi, o coloca. Poderia ser direto também, mas repare na condicional relacionado ao pino, que verifica se é maior que 7 e seu estado. Depois, faz um digitalWrite comum, mas poderia ser escrito diretamente no respectivo registrador também.

void blinker(int n_times, int x_delay, uint8_t p_pin){
    //check pin mode
    //0-7 port D (0 e 1 são serial, não usar)
    //8-13 port B (bits 6 e 7 são cristal, não usar)
    uint8_t pReg   = p_pin > 7 ? p_pin-8 : p_pin-1;

    if (p_pin > 7 && (DDRB^(1<<pReg))){
        Serial.println(DDRB,BIN);
        DDRB = DDRB|(1<<pReg);
        Serial.println(DDRB,BIN);
        Serial.println("<<<DDRB>>>");
    }
    else if ((DDRD^(1<<pReg))){
        DDRD = DDRD&(1<<pReg);
    }

    uint8_t state = digitalRead(p_pin);
    if ( n_times == 0 && x_delay == 0 ) {
        digitalWrite(p_pin, !state);
        return;
    }

    for (int t=0;t<n_times;t++){
        digitalWrite(p_pin,!digitalRead(p_pin));
        delay(x_delay);
    }
}

Função roulette

A função roleta também tem algo interessante.

Para gerar números randômicos em qualquer plataforma, é necessário uma semente. Se a semente for estática, a sequência randômica será sempre a mesma. Para fazer algo mais dinâmico, podemos ler o valor de um pino analógico que não esteja em uso e aproveitar sua flutuação.

Dentro dessa função também é feito o deslocamento dos bits do PCF8574 para simular uma roleta "mesmo", que é a próxima fase desse projeto. Já está implementado, mas ainda preciso preparar os LEDs. Por fim, os números definidos para a matriz de LEDs 8x8 são chamadas conforme sua posição no loop, com delay incremental, para simular a desaceleração. Na última volta do loop, ele é interrompido no momento em que passar pela posição correspondente ao número randômico gerado.

uint8_t PCF_roulette(bool state){
    uint8_t result = 0;
    int wait    = 100;

    result = analogRead(0);
    randomSeed(analogRead(0));

    result = random(7);
    Serial.println(result);
    result = random(7);
    Serial.println(result);

    for (uint8_t j=0; j<5; j++){
        for (uint8_t i = 0; i<8; i++){
            Wire.beginTransmission(mbz.pcf_address);
            Wire.write((1<<i));
            Wire.endTransmission();

            Serial.println((1<<i));
            
            plac(10);
            delay(wait);
            wait += 10;

            for (int p=0;p<8;p++){
                lc.setRow(0,p,numbers[i][p]);
            }
            if (i == result && j == 4) return result;
        }
    }
}

Tasks

Uma das vantagens em utilizar um sistema operacional de tempo real é a possiblidade de processamento assíncrono. No caso desse programa, não é nada que não pudesse ser resolvido com interrupção, mas para figurar o recurso, criei uma task chamada vTaskRoulette que fica em loop monitorando o botão que aciona a roleta. Uma vez a função disparada, não adianta apertar subsequentemente, porque o processo estará preso no loop da roleta e assim não precisamos nos preocupar em tratar uma interrupção, nem garantir que não haverá reentrância.

oid vTaskRoulette(void * pvParameters){
    
    while (true){
        if (digitalRead(mbz.button) == 0){
            uint8_t val = PCF_roulette(HIGH);
            Serial.print("Resultado: ");
            Serial.println(mbz.pcf_pin[val]);
        }
        vTaskDelay(pdMS_TO_TICKS(100));
        //delay(100);
    }
}

Depois em setup(), simplesmente criamos a tarefa:

xTaskCreate(vTaskRoulette, (const portCHAR*)"vTaskRoulette", 128, NULL, 2, NULL);

Um dos macetes aqui é que em alguns lugares não dá pra usar os ticks invés de delay e vice-versa. Vou deixar isso pra explicar em outro momento, por enquanto vou deixar o código completo para quem tiver interesse em reproduzir o brinquedo. Como utilizei o VisualStudio Code para programar, foi necessário fazer o include de Arduino.h, mas na IDE do Arduino esse include não é necessário:

#include <Arduino.h>
#include <Arduino_FreeRTOS.h>
#include <Wire.h>
#include "LedControl.h"

//LedControl lc = LedControl(12,11,10,1);
LedControl lc = LedControl(12,11,10,1);

unsigned long delaytime=250;
/*
 pin 12 is connected to the DataIn 
 pin 11 is connected to the CLK 
 pin 10 is connected to LOAD 
 We have only a single MAX72XX.
 */

struct MaxBlitz{
  int stat : 1;
  uint8_t buzzer_pin  = 3;
  uint8_t button      = 4;
  uint8_t builtin_led = 13; 
  uint8_t pcf_address = 0x20;

  uint8_t pcf_pin[8]  = {1,2,4,8,16,32,64,128};

} mbz;

uint8_t numbers[8][8] = {{B00000000, B00000000, B01111111, B01000001, B01000001, B01111111, B00000000, B00000000},
                       {B00000000, B00000000, B00000001, B01111111, B01100001, B00100000, B00000000, B00000000},
                       {B00000000, B00110001, B01111001, B01000101, B01100101, B00100011, B00000000, B00000000},
                       {B00000000, B01101110, B01010011, B01010001, B01000001, B01000010, B00000000, B00000000},
                       {B00000000, B00000000, B01111111, B00001000, B00001000, B01111000, B00000000, B00000000},
                       {B00000000, B00000000, B01001110, B01010001, B01010001, B01110001, B00000000, B00000000},
                       {B00000000, B00000110, B01001001, B00101001, B00010001, B00001110, B00000000, B00000000},
                       {B00000000, B01110000, B01001000, B01000100, B01000011, B01000000, B00000000, B00000000}};



//som breve para parecer uma catraca da roleta
void plac(int timeToPlaca);
//executa um blink por N vezes com intervalo X no pino P. Se n e x = 0, só inverte o estado do pino P.
void blinker(int n_times, int x_delay, uint8_t p_pin);

//Tarefa que monitora o botão
void vTaskButton(void *pvParameters);

//Tarefa pra tocar o buzzer
void vTaskSirene(void *pvParameters);

void vTaskRoulette(void * pvParameters);

/* Faz um loop tipo "blink" nos pinos escolhidos.
times      - número de vezes que deve fazer o loop
pinsToLoop - Soma dos pinos a usar. Ex: mbz.pcf_pin[0] + mbz.pcf_pin[1] + mbz.pcf_pin[4], que equivale a 1+2+8, ou 11.
PCF_loop_state(3, 255); - loop em todos os pinos, 3 vezes.
PCF_loop_state(3, mbz.pcf_pin[0]+mbz.pcf_pin[7]); - loop nos pinos 0 e 7, por 3 vezes
*/
void PCF_loop_state(int times, uint8_t pinsToLoop);

/* Inverte o estado de um pino do PCF . Ex:
PCF_toggle_pin(7); - muda o estado do pino 7
*/
void PCF_toggle_pin(uint8_t pcf_pin);

/* Coloca os pino especificados em HIGH ou LOW, não importa se já está. Ex:
PCF_set_pins_state(mbz.pcf_pin[0] + mbz.pcf_pin[7], HIGH); - coloca o bit 0 e o 7 em HIGH
*/
void PCF_set_pins_state(uint8_t pcf_pins, bool state);

/* Pega o estado do pino e retorna. Se estiver em LOW, retorna 0,
   se estiver em HIGH, retorna o valor do pino. Ex:
   uint8_t stat = PCF_get_pin_state(mbz.pcf_pin[7]); - retorna 128 se estiver em HIGH
   uint8_t stat = PCF_get_pin_state(mbz.pcf_pin[0]); - retorna 1 se estiver em HIGH
 */
uint8_t PCF_get_pin_state(uint8_t pcf_pin);

/* Faz um sorteio randômico de um dos pinos do PCF8574 e o coloca no estado do parâmetro */
uint8_t PCF_roulette(bool state);

void PCF_loop_state(int times, uint8_t pinsToLoop){

}

void PCF_toggle_pin(uint8_t pcf_pin){

}

void PCF_set_pins_state(uint8_t pcf_pins, bool state){

}

uint8_t PCF_get_pin_state(uint8_t pcf_pin){

}

void blinker(int n_times, int x_delay, uint8_t p_pin){
    //check pin mode
    //0-7 port D (0 e 1 são serial, não usar)
    //8-13 port B (bits 6 e 7 são cristal, não usar)
    uint8_t pReg   = p_pin > 7 ? p_pin-8 : p_pin-1;

    if (p_pin > 7 && (DDRB^(1<<pReg))){
        Serial.println(DDRB,BIN);
        DDRB = DDRB|(1<<pReg);
        Serial.println(DDRB,BIN);
        Serial.println("<<<DDRB>>>");
    }
    else if ((DDRD^(1<<pReg))){
        DDRD = DDRD&(1<<pReg);
    }

    uint8_t state = digitalRead(p_pin);
    if ( n_times == 0 && x_delay == 0 ) {
        digitalWrite(p_pin, !state);
        return;
    }

    for (int t=0;t<n_times;t++){
        digitalWrite(p_pin,!digitalRead(p_pin));
        delay(x_delay);
    }
}

uint8_t PCF_roulette(bool state){
    uint8_t result = 0;
    int wait    = 100;

    result = analogRead(0);
    randomSeed(analogRead(0));

    result = random(7);
    Serial.println(result);
    result = random(7);
    Serial.println(result);

    for (uint8_t j=0; j<5; j++){
        for (uint8_t i = 0; i<8; i++){
            Wire.beginTransmission(mbz.pcf_address);
            Wire.write((1<<i));
            Wire.endTransmission();

            Serial.println((1<<i));
            
            plac(10);
            delay(wait);
            wait += 10;

            for (int p=0;p<8;p++){
                lc.setRow(0,p,numbers[i][p]);
            }
            if (i == result && j == 4) return result;
        }
    }
}

void vTaskSirene(void *pvParameters){
    float sinVal  = 0;
    int toneVal   = 0;

    for (uint8_t i=0; i<5;i++){
        for (uint8_t x=0;x<180;x++){
            //converte graus em radianos
            sinVal = (sin(x*(3.1412/180)));
            //agora gera uma frequencia
            toneVal = (int(sinVal*100))-100;
            //toca o valor no buzzer
            tone(mbz.buzzer_pin,toneVal,1000);
            //atraso de 2ms e gera novo tom
            vTaskDelay(pdMS_TO_TICKS(2));
        }
    }
    noTone(mbz.buzzer_pin);
    vTaskDelete(NULL);
}

void plac(int timeToPlaca){
    tone(mbz.buzzer_pin,100,timeToPlaca);
}

void vTaskRoulette(void * pvParameters){
    
    while (true){
        if (digitalRead(mbz.button) == 0){
            uint8_t val = PCF_roulette(HIGH);
            Serial.print("Resultado: ");
            Serial.println(mbz.pcf_pin[val]);
        }
        vTaskDelay(pdMS_TO_TICKS(100));
        //delay(100);
    }
}

void setup() {
    Serial.begin(9600);
    pinMode(mbz.buzzer_pin,OUTPUT);
    pinMode(mbz.button,INPUT_PULLUP);

    lc.shutdown(0,false);
    lc.setIntensity(0,8);
    lc.clearDisplay(0);

    Wire.begin();
    delay(2000);
    //xTaskCreate(vTaskSirene, (const portCHAR*)"vTaskSirene", 128, NULL, 1, NULL);
    xTaskCreate(vTaskRoulette, (const portCHAR*)"vTaskRoulette", 128, NULL, 2, NULL);

    //blinker(10,80,13);
    //delay(2000);
    blinker(0,0,13);
}

void loop() {
    Serial.print(".");
    delay(1000);
  
}

Vídeo

Deixei uma demonstração no instagram e em nossa página no facebook, dê uma conferida na roleta eletrônica funcionando, minha filha viciou na brincadeira, e olhe que agora é só um número randômico!

 

Curso "Raspberry para hobistas e profissionais"

Continuo na divulgação do curso  que produzi, tendo 3 horas de conteúdo e mostrando desde a instalação do sistema em Windows e Linux à manipulação do GPIO em Shell Script, C e Python, além de interagir com dispositivos I2C, proteção do sistema de arquivos e como fazer um backup reduzido do sistema, entre outros.

Use o cupom de desconto DOBITAOBYTE e adquira o curso por R$21,00. Assim você apoia o blog e motiva outros cursos com Raspberry e outras plataformas, valeu?

 

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.  

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.