Publicidade:

sexta-feira, 19 de agosto de 2016

Arduino - Tetris com display LCD 20x4 e Joystick

Em junho postei duas implementações do jogo Tetris (veja aqui), uma com display de leds e outra com display LCD 16x2. Em nenhuma o tamanho original do jogo foi utilizado (20 linhas e 10 colunas), por limitações dos componentes. Mas a ideia não era reproduzir o jogo em seu formato 100% original e sim, criar algo mais por diversão e pra servir de exemplo pra quem gosta de programar.

Mas essa semana chegou um display LCD 20x4 e um módulo I2C para o display que comprei há algum tempo e pensei que poderia criar uma versão do tetris pra ele também. Basicamente o que era preciso, era apenas substituir a biblioteca LCD pela biblioteca LCD I2C.

Através da própria IDE do Arduino (versão 1.6.10) procurei e instalei a biblioteca para display LCD I2C,  depois fiz uma cópia da sketch que tinha usado no jogo com o display 16x2 e fiz algumas pequenas alterações no código aumentando a quantidade de linhas e colunas do jogo. O Resultado foi que ficou extremamente lenta a atualização dos dados no display LCD. Fiquei um pouco frustrado, achei que não iria dar certo utilizando o display 20x4. Mas resolvi dar uma olhada na biblioteca I2C, pra ver como era a comunicação entre o Arduino e o display.

A biblioteca do display I2C utiliza a biblioteca Wire para o envio dos dados para o display e analisando melhor o código da biblioteca, de fato havia algumas coisas que poderiam ser melhoradas para que ela ficasse mais rápida. Pois a biblioteca, para cada caractere enviado para o display, inicia uma transmissão, envia o dado e finaliza a transmissão, isso significa que se 20 caracteres forem enviados para o display, serão iniciadas e finalizadas 20 transmissões, algo que não seria necessário, pois a biblioteca Wire conta com um buffer que pode armazenar vários dados, e depois de finalizado, os dados podem ser enviados todos dentro de uma mesma transmissão, economizando assim, chamadas desnecessárias. Para mais de 90% das aplicações que utilizam um display LCD com certeza isso não é nenhum problema, mas no caso do jogo, as atualizações da tela são executadas a uma taxa bem alta, faz muita diferença.

Então eu tinha duas opções, criar uma nova biblioteca, baseada na biblioteca I2C ou, na própria sketch criar uma classe que herdasse da classe original, e nela fizesse as alterações necessárias. Fiquei com a segunda opção, pois não queria criar e manter uma biblioteca a parte. Porém, foi necessário fazer algumas alterações na biblioteca original para que eu pudesse sobrescrever o método que fazia o envio de dados (tornar o método virtual) e também precisei alterar a visibilidade de alguns atributos da classe, para que as mesmas ao invés de serem private passassem a ser protected. Essas alterações foram necessárias apenas no Header (arquivo .h) da biblioteca. Com isso, na própria sketch criei uma nova classe herdada da classe original, e fiz os devidos tratamentos.

Nessa nova classe foram implementados dois métodos, startWrite() e sendAll(). Com isso, agora, antes de enviar os dados para o display eu chamo startWrite e começo a enviar os dados para o display, que na verdade são mantidos no buffer, e somente após eu chamar sendAll, os dados são de fatos enviados para o display. (Na verdade, se durante o envio o buffer ficar cheio, a classe envia os dados para o display).

Depois de tudo pronto, fiz os testes. Melhorou bastante, mas ainda não estava satisfatório o resultado, então resolvi analisar o que eu poderia alterar no código do próprio jogo, e vi que eu fiz, para que a cada caractere enviado para o display estava sendo posicionado onde deveria ser escrito (através de setCursor(...) ). Porém se os dados são enviados dentro de uma mesma linha, não há a necessidade de chamar setCursor para cada escrita, pois o próprio display, ao receber o comando de escrita de um caractere, já posiciona o cursor na próxima posição. Com isso eliminei, várias chamadas desnecessárias.

Testei novamente e o resultado agora foi bem mais satisfatório, sendo os dados atualizados numa velocidade aceitável.

Outra alteração, foi que agora implementei o jogo com um joystick, onde antes eu utilizava um rotary encoder.

Vídeo



Código-fonte

/*
Fabiano A. Arndt - 2016
www.youtube.com/user/fabianoallex
www.facebook.com/dicasarduino
fabianoallex@gmail.com
*/
 
#include <LiquidCrystal_I2C.h>

/*************************************************************************************************************
*******************************CLASSE DERIVADA DE LiquidCrystal_I2C*******************************************
**************************************************************************************************************
*essa classe foi criada para ser mais rápida na hora de enviar os dados para o display através da biblioteca 
*wire, utilizando o buffer, requerendo assim, menos chamadas a biblioteca wire. a biblioteca original, a cada
*chamada para write estava iniciando uma transmissão, enviando o dado e em seguida finalizando a transmissão, 
*mesmo que várias informações estivessem sendo enviadas sequencialmente. sendo desnecessario o envio de 
*cada informação dentro uma transmissao separada. as alterações foram justamente pra utilizar o buffer 
*que a biblioteca wire disponibiliza.
*
*foram incluídos os métodos startWrite e sendAll. responsáveis pelo inicio e fim da transmissao. se durante o 
*envio dos dados o buffer ficar totalmente cheio, o fim da transmissao é automaticamente chamado e em seguida
*reiniciada outra transmissao. 
*
*caso esses métodos não forem chamados durante o uso da classe, os dados são enviados
*a cada escrita dentro de uma transmissao pra cada escrita, sem utilizar o buffer, como é exatamente o 
*comportamento da biblioteca original.
*
*foi necessário fazer algumas alterações na biblioteca original, no arquivo header (LiquidCrystal_I2C.h).
*os métodos e membros abaixo, passaram a ser protected ao invés de private:
*protected:
  uint8_t _Addr;                         //alterado fabiano. passou a ser protected para ser visto pelas classes derivadas
  uint8_t _backlightval;                 //alterado fabiano. passou a ser protected para ser visto pelas classes derivadas
  void pulseEnable(uint8_t);             //alterado fabiano. passou a ser protected para ser visto pelas classes derivadas 

 a função abaixo passou a ser virtual para que pudesse ser reescrita

 virtual void expanderWrite(uint8_t);      //alterado fabiano. definido como método virtual, para poder ser redefinido
**************************************************************************************************************/
class LiquidCrystal_I2C_ : public LiquidCrystal_I2C {
  private:
    uint8_t _startWrite;
    uint8_t _countByte;
    void write4bits(uint8_t value) { pulseEnable(value); }
    void expanderWrite(uint8_t _data){
      if (_startWrite == 0) {   Wire.beginTransmission(_Addr);  } else { 
        _countByte += sizeof(int); 
        if (_countByte > 64 ) {                    //32 é o tamanho do buffer definido em BUFFER_LENGTH ............ fiz o teste com 64 bytes e funcionou, com valor maior ocorreu erros, caso der algum erro de envio, voltar para o valor BUFFER_LENGHT no lugar do 64
          sendAll();
          startWrite();
          _countByte += sizeof(int);
        }
      }
      Wire.write((int)(_data) | _backlightval);
      if (_startWrite == 0) {   Wire.endTransmission();   }
    }
  public:
    LiquidCrystal_I2C_(uint8_t lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows) : LiquidCrystal_I2C(lcd_Addr, lcd_cols, lcd_rows){
      _startWrite = 0;
      _countByte = 0;
    }
    inline size_t write(uint8_t value) {
      uint8_t highnib = value&0xf0;
      uint8_t lownib  = (value<<4)&0xf0;
      write4bits((highnib)|Rs);
      write4bits((lownib)|Rs); 
      return 1;
    }
    void startWrite() {
      _countByte = 0;
      _startWrite = 1;
      Wire.beginTransmission(_Addr); 
    }
    void sendAll(){
      _startWrite = 0;
      Wire.endTransmission();
    }
};
/*************************************************************************************************************
*******************************FIM CLASSE DERIVADA DE LiquidCrystal_I2C***************************************
**************************************************************************************************************/

/*************************************************************************************************************
************************************CLASSE JOYSTICK***********************************************************
*************************************************************************************************************/
class Joystick {
  private:
    boolean _isTop    : 1;
    boolean _isBottom : 1;
    boolean _isLeft   : 1;
    boolean _isRight  : 1;
    unsigned int _x   : 10;
    unsigned int _y   : 10;
    byte _pinX        : 8;
    byte _pinY        : 8;
    byte _pinZ        : 8;
  public:
    Joystick(int pinX, int pinY, int pinZ){
      _pinX = pinX;
      _pinY = pinY;
      _pinZ = pinZ;
      pinMode(pinX, INPUT);
      pinMode(pinY, INPUT);
      pinMode(pinZ, INPUT_PULLUP);
    }
    update() {
      _x = analogRead(_pinX);
      _y = analogRead(_pinY);
      _isTop    = (_x < 200);
      _isBottom = (_x > 800);
      _isRight  = (_y < 200);
      _isLeft   = (_y > 800);
    }
    boolean isTop()      { return _isTop; }
    boolean isBottom()   { return _isBottom; }
    boolean isLeft()     { return _isLeft; }
    boolean isRight()    { return _isRight; }
    unsigned int readX() { return _x; }
    unsigned int readY() { return _y; }
    byte buttonRead()    { return digitalRead(_pinZ); }
};
/*************************************************************************************************************
************************************FIM CLASSE CLASSE JOYSTICK************************************************
*************************************************************************************************************/
   
/*************************************************************************************************************
*******************************BIT ARRAY *********************************************************************
**************************************************************************************************************
mais informações aqui: http://fabianoallex.blogspot.com.br/2015/09/arduino-array-de-bits.html
**************************************************************************************************************/
class BitArray2D {
  private:
    unsigned int _rows;
    unsigned int _columns;
    unsigned int _cols_array; //pra cada 8 colunas, 1 byte é usado 
    byte**       _bits;
  public:
    BitArray2D(unsigned int rows, unsigned int columns){
      _rows       = rows;
      _columns    = columns;
      _cols_array = columns/8 + (_columns%8 ? 1 : 0) + 1; //divide por 8 o número de colunas
      _bits = (byte **)malloc(_rows * sizeof(byte *));
      for(int i=0;i<_rows;i++){ _bits[i] = (byte *)malloc(  _cols_array  *  sizeof(byte)); } //cria varios arrays
      clear();
    }
    unsigned int rows(){ return _rows; }
    unsigned int columns(){ return _columns; }
    void clear() { 
      for(int i=0;i<_rows;i++){      
        for(int j=0; j<_cols_array;j++) { _bits[i][j] = B00000000; }       
      }   
    }
    void write(unsigned int row, unsigned int column, int value){
      byte b = _bits[row][ column/8 + (column%8 ? 1 : 0) ];
      unsigned int bit = column%8;    
      if (value) { b |= (1 << bit); } else { b &= ~(1 << bit);  }
      _bits[row][ column/8 + (column%8 ? 1 : 0) ] = b;
    }
    void write(byte value){
      for(int i=0;i<_rows;i++){      
        for(int j=0; j<_cols_array;j++) {      
          _bits[i][j] = value ? B11111111 : B00000000;     
        }       
      }  
    }
    int read(unsigned int row, unsigned int column){
      byte b = _bits[row][ column/8 + (column%8 ? 1 : 0) ];
      unsigned int bit = column%8;    
      return (b & (1 << bit)) != 0;
    }
    void toggle(unsigned int row, unsigned int column){ write(row, column, !read(row, column)); }
    void toggle(){ for(int i=0;i<_rows;i++){      for(int j=0; j<_columns;j++) {      toggle(i,j);   }   }   }
};
/*************************************************************************************************************
*******************************FIM BIT ARRAY *****************************************************************
**************************************************************************************************************/
      
/*************************************************************************************************************
************************************CLASSE UNIQUE RANDOM******************************************************
**************************************************************************************************************
mais informações aqui: http://fabianoallex.blogspot.com.br/2015/09/arduino-numeros-aleatorio-repetidos-e.html
*************************************************************************************************************/
class UniqueRandom{
  private:
    int _index;
    int _min;
    int _max;
    int _size;
    int* _list;
    void _init(int min, int max) {
      _list = 0; 
      if (min < max) { _min = min; _max = max; } else { _min = max; _max = min; }
      _size = _max - _min; 
      _index = 0;
    }    
  public:
    UniqueRandom(int max)           { _init(0,   max); randomize(); } //construtor com 1 parametro
    UniqueRandom(int min, int max)  { _init(min, max); randomize(); } //construtor com 2 parametros
    void randomize() {
      _index = 0;
      if (_list == 0) { _list = (int*) malloc(size() * sizeof(int)); }  
      for (int i=0; i<size(); i++) {   _list[i] = _min+i;  }   //preenche a lista do menor ao maior valor
      //embaralha a lista
      for (int i=0; i<size(); i++) {  
        int r = random(0, size());     //sorteia uma posição qualquer
        int aux = _list[i];               
        _list[i] = _list[r];
        _list[r] = aux;
      }
    }
    int next() {                                  //retorna o proximo numero da lista
      int n = _list[_index++];
      if (_index >= size() ) { _index = 0;} //após recuper o ultimo numero, recomeça na posicao 0
      return n;
    }
    void first() { _index = 0; }
    boolean eof() { return size() == _index+1; }
    int size() { return _size; }
    int getIndex() {  return _index; }
    ~UniqueRandom(){ free ( _list ); }  //destrutor
};
/*************************************************************************************************************
************************************FIM CLASSE UNIQUE RANDOM**************************************************
*************************************************************************************************************/
     
/*************************************************************************************************************
*******************************CLASSES TETRIS GAME************************************************************
**************************************************************************************************************/
const int TETRIS_TIME_INIT = 700; //tempo entre deslocamento das peças
const int TETRIS_TIME_INC  = 20;  //incremento da velocidade
enum Direction        { DIR_STOP, DIR_TOP, DIR_LEFT, DIR_BOTTOM, DIR_RIGHT};
enum TetrisGameStatus { TETRIS_GAME_ON, TETRIS_GAME_OVER };
enum TetrisBlockTypes { TETRIS_I, TETRIS_J, TETRIS_L, TETRIS_O, TETRIS_S, TETRIS_Z, TETRIS_T, TETRIS_COUNT_BTYPES};
struct Position       { int row, column; };
struct Block          { byte row0:4; byte row1:4; byte row2:4; byte row3:4; };  //4 bit cada linha
//blocos e suas rotacoes
Block block_i[] = { {B1111, B0000, B0000, B0000}, {B0001, B0001, B0001, B0001} };
Block block_j[] = { {B0000, B0010, B0010, B0011}, {B0000, B0000, B0001, B0111}, {B0000, B0110, B0010, B0010}, {B0000, B0000, B0111, B0100} };
Block block_l[] = { {B0000, B0010, B0010, B0110}, {B0000, B0000, B0100, B0111}, {B0000, B0011, B0010, B0010}, {B0000, B0000, B0111, B0001} };
Block block_o[] = { {B0000, B0000, B0110, B0110} };
Block block_s[] = { {B0000, B0001, B0011, B0010}, {B0000, B0000, B0110, B0011} };
Block block_z[] = { {B0000, B0000, B0110, B1100}, {B0000, B1000, B1100, B0100} };
Block block_t[] = { {B0000, B0000, B0010, B0111}, {B0000, B0010, B0011, B0010}, {B0000, B0000, B0111, B0010}, {B0000, B0001, B0011, B0001} };
class TetrisBlock{
  private:
    Block _block;       //bloco que vai descendo. player
    Position _position; //posição do bloco na tela. pode assumir posição negativa
    TetrisBlockTypes _blockType;
    byte _blockTypeRotation;
  public:
    TetrisBlock(){
      _blockType =  (TetrisBlockTypes) random(0, TETRIS_COUNT_BTYPES);
    }
    Position getPosition()  { return _position; }
    byte getRow(int row) { 
      if (row == 0 ) return _block.row0;
      if (row == 1 ) return _block.row1;
      if (row == 2 ) return _block.row2;
      if (row == 3 ) return _block.row3; 
      return 0;
    }
    byte getColumn(int col) {
      byte b = 0;
      for (int i=0;i<4;i++){ bitWrite(b, 3-i, bitRead(getRow(i), 3-col) );  }
      return b;
    }
    int read(int row, int column){
      byte b = getRow(row);
      unsigned int bit = column%8;    
      return (b & (1 << bit)) != 0;
    }
    void left()   { _position.column--;  }
    void right()  { _position.column++;  }
    void bottom() { _position.row++;     }
    void top()    { _position.row--;     }
    TetrisBlockTypes getBlockType() { return _blockType; }
    void generateBlock(byte col, TetrisBlockTypes blockType = TETRIS_COUNT_BTYPES) {
      _position.row      = 0; 
      _position.column   = col; 
      _blockType         = (blockType == TETRIS_COUNT_BTYPES) ? (TetrisBlockTypes) random(0, TETRIS_COUNT_BTYPES) : blockType;
      _blockTypeRotation = 0;
      if ( _blockType == TETRIS_I ){ _block = block_i[_blockTypeRotation]; }
      if ( _blockType == TETRIS_J ){ _block = block_j[_blockTypeRotation]; }
      if ( _blockType == TETRIS_L ){ _block = block_l[_blockTypeRotation]; }
      if ( _blockType == TETRIS_O ){ _block = block_o[_blockTypeRotation]; }
      if ( _blockType == TETRIS_S ){ _block = block_s[_blockTypeRotation]; }
      if ( _blockType == TETRIS_Z ){ _block = block_z[_blockTypeRotation]; }
      if ( _blockType == TETRIS_T ){ _block = block_t[_blockTypeRotation]; }
      if (!getRow(0))   { _position.row--;
        if (!getRow(1))   { _position.row--;          
          if (!getRow(2))   { _position.row--; } } }
    }
    void rotate(int direction = 1){
      int maxPos = 0;
      if ( _blockType == TETRIS_I ){ maxPos = sizeof(block_i)/sizeof(Block); }
      if ( _blockType == TETRIS_J ){ maxPos = sizeof(block_j)/sizeof(Block); }
      if ( _blockType == TETRIS_L ){ maxPos = sizeof(block_l)/sizeof(Block); }
      if ( _blockType == TETRIS_O ){ maxPos = sizeof(block_o)/sizeof(Block); }
      if ( _blockType == TETRIS_S ){ maxPos = sizeof(block_s)/sizeof(Block); }
      if ( _blockType == TETRIS_Z ){ maxPos = sizeof(block_z)/sizeof(Block); }
      if ( _blockType == TETRIS_T ){ maxPos = sizeof(block_t)/sizeof(Block); }
      (direction==1) ? _blockTypeRotation++ : _blockTypeRotation--;
      if (_blockTypeRotation == 255)    { _blockTypeRotation = maxPos-1; }
      if (_blockTypeRotation >= maxPos) { _blockTypeRotation = 0; }
      if ( _blockType == TETRIS_I ){ _block = block_i[_blockTypeRotation]; }
      if ( _blockType == TETRIS_J ){ _block = block_j[_blockTypeRotation]; }
      if ( _blockType == TETRIS_L ){ _block = block_l[_blockTypeRotation]; }
      if ( _blockType == TETRIS_O ){ _block = block_o[_blockTypeRotation]; }
      if ( _blockType == TETRIS_S ){ _block = block_s[_blockTypeRotation]; }
      if ( _blockType == TETRIS_Z ){ _block = block_z[_blockTypeRotation]; }
      if ( _blockType == TETRIS_T ){ _block = block_t[_blockTypeRotation]; }
    }
};
    
class TetrisGame {
  private:
    BitArray2D * _display;   //display com todos os objetos da tela. recebido como parâmetro
    BitArray2D * _recipient; //recepiente onde os blocos vão se acumulando. criado internamente pela classe
    TetrisBlock * _block;    //bloco controlado
    TetrisBlock * _blockNext;    //bloco que fica na parte superior, indicando o próximo bloco a ser controlado
    Direction _direction;
    TetrisGameStatus _tetrisGameStatus;
    unsigned long _last_millis;
    unsigned long _last_millis_player;
    int _time;
    int _score;
    byte _fast : 1;          //acelerar a descida
    UniqueRandom * _ur;      //utilizado no game over
    void _gameOver(){ 
      _tetrisGameStatus = TETRIS_GAME_OVER; 
      _direction   = DIR_STOP;
      _time = 20;
    }
    void _inc_speed(){
      _score++;
      _time -= TETRIS_TIME_INC;
    }
    void _start(){
      _recipient->clear();
      _last_millis = 0;
      _score = 0;
      _time = TETRIS_TIME_INIT;
      _fast = 0;
      _direction = DIR_STOP;
      _tetrisGameStatus = TETRIS_GAME_ON; 
      _blockNext->generateBlock( _display->columns() / 2 - 1);
      _newBlock();
    }
    void _newBlock() {
      _block->generateBlock( _display->columns() / 2 - 1, _blockNext->getBlockType() );
      _blockNext->generateBlock( _display->columns() / 2 - 1 );
      if (_testCollision()){ _gameOver(); }
    }
    void _updateDisplay() {
      if (_tetrisGameStatus == TETRIS_GAME_ON) { _display->clear();  }
      boolean indPrintNext = false;
      for (int r=0; r<4; r++) {
        for (int c=0; c<4; c++) { 
          if ( _block->read(r, c) ) { 
            _display->write(_block->getPosition().row+r, _block->getPosition().column+c,  HIGH ); 
            if (_block->getPosition().row > 2 ) { indPrintNext = true; }
          }
        }
      }
      if (indPrintNext){              //só imprime o próximo bloco, quando o bloco controlado já tiver descido um pouco
        for (int r=0; r<4; r++) {
          for (int c=0; c<4; c++) { 
            if ( _blockNext->read(r, c) ) { 
              _display->write(_blockNext->getPosition().row+r, _blockNext->getPosition().column+c,  HIGH ); 
            }
          }
        }
      }
      for (int row=0; row<_display->rows(); row++) {
        for (int col=0; col<_display->columns(); col++) {
          boolean val = _recipient->read(row, col);
          if (val) { _display->write(row, col,  val ); }
        }
      }
    }
    boolean _testCollision(){
      int col = _block->getPosition().column;  //coluna do bloco em relacao ao recepiente. pode ser negativa
      int row = _block->getPosition().row;     //linha do bloco em relacao ao recepiente. pode ser negativa
      for (byte c=0; c<4; c++){
        for (byte r=0; r<4; r++){
          if (! _block->read(r, c) )            { continue;   }  
          if ( row+r < 0 )                      { return true;}  
          if ( row+r >= _display->rows() )      { return true;}
          if ( col+c < 0 )                      { return true;}
          if ( col+c >= _display->columns() )   { return true;}
          if ( _recipient->read(row+r, col+c) ) { return true;}
        }
      }
      return false;
    }
    boolean _canLeft(){
      _block->left();
      boolean r = ! _testCollision();
      _block->right();
      return r;
    }
    boolean _canRight(){
      _block->right();
      boolean r = ! _testCollision();
      _block->left();
      return r;
    }
    boolean _canBottom(){
      _block->bottom();
      boolean r = ! _testCollision();
      _block->top();
      return r;
    }
    boolean _canRotate(){
      _block->rotate();
      boolean r = ! _testCollision();
      _block->rotate(-1);
      return r;
    }
    void _blockToRecipient(){
      for (byte c=0; c<4; c++){
        for (byte r=0; r<4; r++){
          if ( _block->read(r, c) ){ _recipient->write(_block->getPosition().row+r, _block->getPosition().column+c, HIGH); }
        }
      }
      _verifyCompleteRow();
      _newBlock();
    }
    void _verifyCompleteRow(){
      for (int row=0; row<_recipient->rows(); row++) {
        boolean complete = true;
        for (int col=0; col<_recipient->columns(); col++) {
          if (! _recipient->read(row, col) ) { complete = false; break; }
        }
        if (complete){ 
          _incScore(); 
          //move as linhas pra baixo
          for (int row2 = row; row2>=0; row2--) {
            for (int col2=0; col2<_recipient->columns(); col2++){
              _recipient->write(row2, col2,  (row2>0) ? _recipient->read(row2-1, col2) : 0 );
            }
          }
        }
      }
    }
    void _incScore(){
      _score++;
      _time -= TETRIS_TIME_INC;
    }
    void _runGameOver(){
      int r   = _ur->next();
      int row = (r / _display->columns());
      int col = (r % _display->columns());
      _display->write(row, col, HIGH );
      if ( _ur->getIndex() == (_ur->size()-1) ) {
        _ur->randomize();
        _ur->first();
        _start(); 
      }
    }
    void _runPlayer() {
      if (_direction == DIR_LEFT  && _canLeft()  ) { _block->left();  }
      if (_direction == DIR_RIGHT && _canRight() ) { _block->right(); }
      _direction = DIR_STOP;
    }
    void _runBlock() {
      _canBottom() ? _block->bottom() : _blockToRecipient() ;
      _direction = DIR_STOP; 
    }
  public:
    TetrisGame(BitArray2D * display){ 
      _display   = display;
      _recipient = new BitArray2D( _display->rows(), _display->columns() );
      _ur        = new UniqueRandom( _display->rows() * _display->columns() );
      _block     = new TetrisBlock();
      _blockNext = new TetrisBlock();
      _start();
    }
    void fast()   { _fast = 1; }  //acelerar o deslocamento
    void noFast() { _fast = 0; }  //descida do bloco na velocidade normal
    void left()   { _direction = DIR_LEFT;   }
    void right()  { _direction = DIR_RIGHT;  }
    void bottom() { _direction = DIR_BOTTOM; }
    void rotate() { if (_canRotate()) { _block->rotate(); } }
    int getScore(){ return _score; }
    Position getPosition() { return _block->getPosition(); }
    int update(){
      int r = false;
      if (millis() - _last_millis > ( (_fast==1) ? 50 : _time ) ) { 
        r = true;
        _last_millis = millis();  
        if (_tetrisGameStatus == TETRIS_GAME_ON)   { _runBlock(); } 
        if (_tetrisGameStatus == TETRIS_GAME_OVER) { _runGameOver(); }
      }
      if (millis() - _last_millis_player > 70 ) {  //atualiza os movimentos do player a cada 70ms
        r = true;
        _last_millis_player = millis();  
        if (_tetrisGameStatus == TETRIS_GAME_ON)   { _runPlayer(); } 
      }
      if (r) { _updateDisplay(); }
      return r; //r-->indica se houve mudança no display
    }
};
/*************************************************************************************************************
*******************************FIM CLASSES TETRIS GAME********************************************************
**************************************************************************************************************/
    
/*************************************************************************************************************
*******************************DISPLAY************************************************************************
**************************************************************************************************************/
    
/*
rotation indica qual parte do display estará para cima
    
         TOP
L  . . . . . . . . R
E  . . . . . . . . I
F  . . . . . . . . G
T  . . . . . . . . H
   . . . . . . . . T
   . . . . . . . .
   . . . . . . . .
   . . . . . . . .   
       BOTTOM
        
*/
enum Rotation {TOP, LEFT, BOTTOM, RIGHT};
    
struct Display {
  int index;
  Position position; 
  Rotation rotation;
};
    
/*************************************************************************************************************
*******************************FIM DISPLAY********************************************************************
**************************************************************************************************************/

const int LINHAS  = 20;
const int COLUNAS = 10;

/*************************************************************************************************************
*******************************CLASSE GAME LCD****************************************************************
*classe auxiliar para imprimir o jogo no display lcd e criar os caracteres customizáveis
**************************************************************************************************************/
/*
  0        1        2        3        4        5        6
 ***                        ***      ***               ***                 
 ***                        ***      ***               ***          
                                                                
 ***      ***               ***               ***             
 ***      ***               ***               ***             
                                                                  
 ***      ***      ***               ***                        
 ***      ***      ***               ***                            
*/
byte c0[8] = {B11111,  B11111,  B00000,  B11111,  B11111,  B00000,  B11111,  B11111 };
byte c1[8] = {B00000,  B00000,  B00000,  B11111,  B11111,  B00000,  B11111,  B11111 };
byte c2[8] = {B00000,  B00000,  B00000,  B00000,  B00000,  B00000,  B11111,  B11111 };
byte c3[8] = {B11111,  B11111,  B00000,  B11111,  B11111,  B00000,  B00000,  B00000 };
byte c4[8] = {B11111,  B11111,  B00000,  B00000,  B00000,  B00000,  B11111,  B11111 };
byte c5[8] = {B00000,  B00000,  B00000,  B11111,  B11111,  B00000,  B00000,  B00000 };
byte c6[8] = {B11111,  B11111,  B00000,  B00000,  B00000,  B00000,  B00000,  B00000 };
class GameLCD {
  private:
    byte _col;
    byte _row;
    LiquidCrystal_I2C_ * _lcd;  //ponteiro para um objeto lcd
  public:
    void createChars() { 
      _lcd->createChar(0, c0);  
      _lcd->createChar(1, c1);  
      _lcd->createChar(2, c2); 
      _lcd->createChar(3, c3);  
      _lcd->createChar(4, c4);
      _lcd->createChar(5, c5);
      _lcd->createChar(6, c6);
    }
    GameLCD(LiquidCrystal_I2C_ * lcd) {  _lcd = lcd;  }
    void write(byte col, byte row, byte val){
      if ( ! (_row == row && _col == (col-1) && col < (LINHAS-1) ) ) {  _lcd->setCursor(col, row); }  //só seta o cursor, caso a posicao não seja a que está na sequencia
      if (val == B000) { _lcd->print(" ");  }
      if (val == B111) { _lcd->write((byte)0);  }
      if (val == B011) { _lcd->write((byte)1);  }
      if (val == B001) { _lcd->write((byte)2);  }
      if (val == B110) { _lcd->write((byte)3);  }
      if (val == B101) { _lcd->write((byte)4);  }
      if (val == B010) { _lcd->write((byte)5);  }
      if (val == B100) { _lcd->write((byte)6);  }
      _col = col;
      _row = row;
    }
    void startWrite(){ _lcd->startWrite(); }
    void sendAll()   { _lcd->sendAll();    }
};
/*************************************************************************************************************
*******************************FIM CLASSE GAME LCD***********************************************************
**************************************************************************************************************/
    
/*************************************************************************************************************
*******************************DECLARACAO DOS OBJETOS*********************************************************
**************************************************************************************************************/
LiquidCrystal_I2C_ lcd(0x3F,20,4);  // set the LCD address to 0x3F for a 20 chars and 4 line display
GameLCD gameLcd(&lcd);
BitArray2D ba(LINHAS, COLUNAS); //8 linhas e 16 colunas, usada para armazenar o estado do display
TetrisGame tetris(&ba);
Joystick joystick(A0, A1, 4);
/*************************************************************************************************************
*******************************FIM DECLARACAO DOS OBJETOS*****************************************************
**************************************************************************************************************/
 
void update_display() {
  gameLcd.startWrite();
  int cols_lcd = (ba.columns() / 3 + (ba.columns()%3 ? 1 : 0));
  for(int col_lcd=0; col_lcd<cols_lcd; col_lcd++ ) {
    for (int lin=ba.rows()-1; lin>=0; lin--) {
      byte val = 0;
      for(byte i=0;i<3;i++) {
        byte leitura = (col_lcd*3+i >= ba.columns()) ? HIGH : ba.read(lin, col_lcd*3+i);  //se for a coluna maior que o tamnho do display, retorna como "parede"
        if (i == 0){ val = 0; }
        val = val << 1; 
        val = val | leitura;
        if (i == 2) { gameLcd.write(LINHAS-lin-1, col_lcd, val) ; }
      }
    }
  }
  gameLcd.sendAll();
}
 
void setup() { 
  lcd.init();
  lcd.backlight();
  gameLcd.createChars();
  randomSeed(analogRead(A2));
//  Serial.begin(9600);
}
         
void loop() {
  joystick.update();

  static byte lastLeft = 0;
  static unsigned long m_left = 0;
  byte left = joystick.isLeft();
  if( left && !lastLeft ) {    //borda de descida
    tetris.left();
    delay(70);
    m_left = millis();
  } else if (left) {
    if (millis() - m_left > 100) {
      tetris.left();
      m_left = millis();
    }
  }
  lastLeft = left;

  static byte lastRight = 0;
  static unsigned long m_right = 0;
  byte right = joystick.isRight();
  if( right && !lastRight ) {    //borda de descida
    tetris.right();
    delay(70);
    m_left = millis();
  } else if (right) {
    if (millis() - m_right > 100) {
      tetris.right();
      m_right = millis();
    }
  }
  lastRight = right;

  if ( tetris.update() ) {  update_display();  }  
        
  static byte lastb = HIGH; //leitura anterior do botão
  static unsigned long m_pressed = 0;  //millis na borda de subida do botão
    
  int b = joystick.buttonRead();
  if( !b && lastb ) {    //borda de subida
    m_pressed = millis();
    delay(70);
  }
  if( b && !lastb ) {    //borda de descida
    if (millis() - m_pressed < 350) {
      tetris.rotate();
      m_pressed = millis();
    }
  } 
  if( joystick.isBottom() ){
    tetris.fast(); 
  } else {
    tetris.noFast(); 
  }
  lastb = b;
} 





LiquidCristal_I2C.h - Alterações da biblioteca original


//YWROBOT
#ifndef LiquidCrystal_I2C_h
#define LiquidCrystal_I2C_h

#include <inttypes.h>
#include "Print.h" 
#include <Wire.h>

// commands
#define LCD_CLEARDISPLAY 0x01
#define LCD_RETURNHOME 0x02
#define LCD_ENTRYMODESET 0x04
#define LCD_DISPLAYCONTROL 0x08
#define LCD_CURSORSHIFT 0x10
#define LCD_FUNCTIONSET 0x20
#define LCD_SETCGRAMADDR 0x40
#define LCD_SETDDRAMADDR 0x80

// flags for display entry mode
#define LCD_ENTRYRIGHT 0x00
#define LCD_ENTRYLEFT 0x02
#define LCD_ENTRYSHIFTINCREMENT 0x01
#define LCD_ENTRYSHIFTDECREMENT 0x00

// flags for display on/off control
#define LCD_DISPLAYON 0x04
#define LCD_DISPLAYOFF 0x00
#define LCD_CURSORON 0x02
#define LCD_CURSOROFF 0x00
#define LCD_BLINKON 0x01
#define LCD_BLINKOFF 0x00

// flags for display/cursor shift
#define LCD_DISPLAYMOVE 0x08
#define LCD_CURSORMOVE 0x00
#define LCD_MOVERIGHT 0x04
#define LCD_MOVELEFT 0x00

// flags for function set
#define LCD_8BITMODE 0x10
#define LCD_4BITMODE 0x00
#define LCD_2LINE 0x08
#define LCD_1LINE 0x00
#define LCD_5x10DOTS 0x04
#define LCD_5x8DOTS 0x00

// flags for backlight control
#define LCD_BACKLIGHT 0x08
#define LCD_NOBACKLIGHT 0x00

#define En B00000100  // Enable bit
#define Rw B00000010  // Read/Write bit
#define Rs B00000001  // Register select bit

class LiquidCrystal_I2C : public Print {
public:
  LiquidCrystal_I2C(uint8_t lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows);
  void begin(uint8_t cols, uint8_t rows, uint8_t charsize = LCD_5x8DOTS );
  void clear();
  void home();
  void noDisplay();
  void display();
  void noBlink();
  void blink();
  void noCursor();
  void cursor();
  void scrollDisplayLeft();
  void scrollDisplayRight();
  void printLeft();
  void printRight();
  void leftToRight();
  void rightToLeft();
  void shiftIncrement();
  void shiftDecrement();
  void noBacklight();
  void backlight();
  void autoscroll();
  void noAutoscroll(); 
  void createChar(uint8_t, uint8_t[]);
  void setCursor(uint8_t, uint8_t); 
#if defined(ARDUINO) && ARDUINO >= 100
  virtual size_t write(uint8_t);
#else
  virtual void write(uint8_t);
#endif
  void command(uint8_t);
  void init();

////compatibility API function aliases
void blink_on();      // alias for blink()
void blink_off();            // alias for noBlink()
void cursor_on();             // alias for cursor()
void cursor_off();           // alias for noCursor()
void setBacklight(uint8_t new_val);    // alias for backlight() and nobacklight()
void load_custom_character(uint8_t char_num, uint8_t *rows); // alias for createChar()
void printstr(const char[]);

////Unsupported API functions (not implemented in this library)
uint8_t status();
void setContrast(uint8_t new_val);
uint8_t keypad();
void setDelay(int,int);
void on();
void off();
uint8_t init_bargraph(uint8_t graphtype);
void draw_horizontal_graph(uint8_t row, uint8_t column, uint8_t len,  uint8_t pixel_col_end);
void draw_vertical_graph(uint8_t row, uint8_t column, uint8_t len,  uint8_t pixel_col_end);
  

private:
  void init_priv();
  void send(uint8_t, uint8_t);
  void write4bits(uint8_t);
  virtual void expanderWrite(uint8_t);      //alterado fabiano. definido como método virtual, para poder ser redefinido
  uint8_t _displayfunction;
  uint8_t _displaycontrol;
  uint8_t _displaymode;
  uint8_t _numlines;
  uint8_t _cols;
  uint8_t _rows;
protected:
  uint8_t _Addr;                         //alterado fabiano. passou a ser protected para ser visto pelas classes derivadas
  uint8_t _backlightval;                 //alterado fabiano. passou a ser protected para ser visto pelas classes derivadas
  void pulseEnable(uint8_t);             //alterado fabiano. passou a ser protected para ser visto pelas classes derivadas 
};

#endif





terça-feira, 16 de agosto de 2016

Arduino - Data e hora

Nativamente, o Arduino não possui nenhum recurso que permita nos dar a data e hora atuais, apesar de possuir recursos para a contagem de tempo, como a função millis e a função micros, onde a primeira retorna a quantidade de milissegundos que se passaram desde que a placa foi ligada ou resetada, enquanto a segunda a quantidade de microssegundos. Mas essa informação por si só não é capaz de nos dizer se hoje é terça ou quinta, se é abril ou setembro ou se estamos em 1970 ou em 2016.

Para obtermos data e horas atuais no Arduino precisamos contar com algum recurso externo, como por exemplo, um módulo RTC ou através de um servidor de data e hora na internet.

Existem diversas formas de conseguir obter a data e hora atual, a forma mais viável vai depender do projeto em si. Nesse artigo, vamos ver sobre algumas das estratégias que podemos utilizar.


  • Módulo RTC
  • Servidor de Data e Hora
  • Entrada através da Serial
  • Entrada Manual
  • Data e Hora de compilação


RTC

Contar com o um módulo RTC é uma boa saída para obtermos data e hora em nossas aplicações, há alguns modelos disponíveis no mercado, os quais contam com uma bateria de 3V, de modo que, mesmo sem energia na placa, o próprio módulo consegue se manter.

Servidor de Data e Hora

Caso a aplicação conte com algum meio de acesso a internet, ou mesmo acesso a uma rede interna, uma boa saída é acessar um servidor NTP. Nesse artigo falo um pouco como funciona esse recurso:
http://fabianoallex.blogspot.com.br/2015/08/arduino-ntp-acessando-servidor-de-hora.html

É preciso ficar atento ao fato de que o servidor de data e hora pode não retornar a hora no mesmo fuso-horário de onde a aplicação estiver rodando, ficando a cargo do programador fazer os ajustes necessários, além disso, alguns servidores também não retornam a data no horário de verão.

Serial

Um meio simples e eficiente de sincronizarmos a data e hora do Arduino é através da comunicação serial, caso o arduino esteja conectado a outros arduinos ou computadores que possam fornecer a data e hora. Esse tipo de solução pode ser utilizado em aplicações distribuídas, onde um Arduino tem acesso a data e hora, e consegue repassar essa informação através de uma comunicação serial. Por exemplo, se existe um Arduino com um módulo RTC o qual se conecta com outros Arduinos, não tem porque cada um dos Arduinos possuirem um módulo. Basta que o Arduino com o módulo RTC repasse a data e hora atual para os demais.

Manual

Caso não seja possível para uma aplicação contar com nenhum outro meio de obter a data e hora, ainda existe a possibilidade do próprio usuário da aplicação informar a data e hora atual. A partir do momento que o Arduino sabe qual é a data e hora atual, ele é capaz de se manter atualizado enquanto estiver energizado ou não for resetado. Obviamente não é o meio mais confiável de se obter data e hora, mas pode ser a única solução dependendo da aplicação. O único inconveniente, é que será necessário ter uma interface onde o usuário possa dar a entrada manual. Se durante a execução, existir o risco da aplicação ser reiniciada, uma saída paliativa para conseguir recuperar a data e hora, seria gravar a cada tantos segundos a data atual na eeprom (ou outro meio disponível), e caso reiniciar a aplicação, a mesma iniciaria com a última data gravada. O inconveniente deste método, é que cada vez que a aplicação reiniciar, a precisão da hora será perdida. E caso o meio utilizado pra gravar a informação for a eeprom, é preciso ficar atento ao limite de vezes que uma eeprom pode ser gravada, antes de acabar sua vida útil.

Data e Hora de compilação

Outra opção, que é mais aconselhável para ser usada durante a programação, é utilizar a data e hora de compilação do arquivo executável. Ela facilita o programador ter uma forma de testar seu código, sem ter necessariamente que contar com algum recurso externo. Essa forma de obtenção de data e hora só não é viável para se utilizar em produção, pois sempre que a aplicação for resetada, a data inicial será a mesma, então se uma aplicação foi compilada ha dois meses, ao reiniciar a aplicação, a data será a data de dois meses atrás.

Outros

Ainda há outros modos de se obter a data e hora, como por exemplo, através de um módulo de GPS ou GSM, caso a aplicação utilize-os.


No exemplo abaixo mostro um exemplo que sincroniza a data e hora com um módulo RTC, com um servidor NTP, com a entrada serial e com a data e hora de compilação do executável.

Código-fonte:

Inicialmente foi criado uma struct chamada Datetime, para que possamos ter os campos da data separados, o que facilita nosso entendimento. E também foi definido o tipo time_t, que na verdade é um long, de 4 bytes. Esse tipo na verdade é o chamado Unix Time, que nada mais é que a quantidade de segundos que se passaram desde o dia 01/01/1970. Ter a hora armazenada em um tipo de dado único facilita diversas operações com datas, como soma e subtração, por isso, a definição e utilização do tipo de dados time_t.

Como há dois tipos de dados, Datetime (que é uma struct) e time_t (que é um long), sendo que ambos representam a mesma coisa, foram criadas duas funções que converte a data de um tipo para o outro, sendo elas:

unixTimeToDatetime e datetimeToUnixTime

Com essas duas funções, fica fácil alternar entre um e outro tipo de dados. As Demais classes e funções utilizadas para tratar as datas se baseiam nesses tipos de dados definidos e nessas duas funções de conversões.

Para controlar a data atual do Arduino, foi criada uma classe chamada ArduinoDatetime. Essa classe é responsável por nos informar a data e hora atual do Arduino. Mas para isso, antes precisamos informar a data inicial. Isso é feito através do método Sync. Depois disso podemos obter a data e hora atuais através das funções getDatetime(), que retorna a data atual no formato Datetime, e através na função now() que retorna a data e hora atual no formato time_t.

Essa classe armazena duas informações: o millis no momento da sincronização e a data e hora daquele momento. Para a classe retornar a data atual, basta que classe calcule quantos segundos se passaram desde a última sincronização e somar esse valor à data e hora do momento de sincronização.

A princípio, é necessário apenas uma única sincronização para que o Arduino possa nos dar a data e hora atual, mas é aconselhável, dentro de um certo período de tempo, refazer a sincronização. Isso porque o clock do Arduino pode ter algum atraso durante longos períodos de tempo.

Veja que a classe em si não é baseada em nenhum meio específico de obtenção da data e hora, ela apenas depende de uma prévia sincronização de data e hora, sem se preocupar com a origem da data. Mais abaixo no código, temos as diferentes funções que nos dão data e hora com diferentes origens: vindo do módulo DS3231, via NTP, via Serial e ainda da data e hora da compilação. Esses são apenas alguns exemplos. É perfeitamente possível que outras funções sejam incluídas.

No loop, o código fica alternando a sincronização da data com as diversas funções que criamos que podem nos retornar a data inicial. Obviamente que em uma aplicação não será necessário ter mais de uma. O que foi feito aqui é apenas para demonstrar as possibilidades.



#include "Wire.h"
#include <LiquidCrystal_I2C.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include "Dns.h"


LiquidCrystal_I2C lcd(0x3F,20,4);  // set the LCD address to 0x27 for a 16 chars and 2 line display

struct Datetime {                 //sizeof(Datetime) ==>  5 bytes --- sem bitfields teria 8 bytes
  byte second       : 6;          //0..63
  byte minute       : 6;          //0..63
  byte hour         : 5;          //0..31
  byte dayOfWeek    : 3;          //0..7
  byte dayOfMonth   : 5;          //0..31
  byte month        : 4;          //0..15
  unsigned int year : 11;         //0..2047
};

typedef unsigned long time_t;          //variavel de 32 bits. para maior precisão (datas acima do ano de 2038. utilizar a linha abaixo.)
//typedef unsigned long long time_t;   //variavel de 64 bits


/****************************************************************************************
************************************ETHERNET CONFIG/FUNCTIONS****************************
*****************************************************************************************
se for rodar numa rede com firewall, verifique se os ip utilizado está liberado.
Servidores da NTP.br
a.st1.ntp.br 200.160.7.186 e 2001:12ff:0:7::186
b.st1.ntp.br 201.49.148.135
c.st1.ntp.br 200.186.125.195
d.st1.ntp.br 200.20.186.76
a.ntp.br 200.160.0.8 e 2001:12ff::8
b.ntp.br 200.189.40.8
c.ntp.br 200.192.232.8
****************************************************************************************/
byte mac[] = {  0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD};
unsigned int localPort = 8888;           // local port to listen for UDP packets
IPAddress timeServer(200, 192, 232, 8);  // time.nist.gov NTP server (fallback) - segunda tentativa caso a primeira de erro
const int NTP_PACKET_SIZE = 48;         // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE];   // buffer to hold incoming and outgoing packets 
 
//host para a primeira tentativa
const char* host = "a.st1.ntp.br";  // servidor da NTP.br - ver lista acima para todos os servidores da NTP.br
//const char* host = "192.168.200.254";  // servidor interno 01 - caso tenha um servidor de hora interno, pode ser configurado o nome ou ip na variavel host
 
EthernetUDP Udp;
DNSClient Dns;
IPAddress rem_add;
 
unsigned long sendNTPpacket(IPAddress& address) {           // send an NTP request to the time server at the given address 
  memset(packetBuffer, 0, NTP_PACKET_SIZE);                 // set all bytes in the buffer to 0
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode      // Initialize values needed to form NTP request (see URL above for details on the packets)
  packetBuffer[1] = 0;            // Stratum, or type of clock
  packetBuffer[2] = 6;            // Polling Interval
  packetBuffer[3] = 0xEC;         // Peer Clock Precision
  packetBuffer[12] = 49;          // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;
  Udp.beginPacket(address, 123); //NTP requests are to port 123                           // all NTP fields have been given values, now you can send a packet requesting a timestamp: 
  Udp.write(packetBuffer,NTP_PACKET_SIZE);
  Udp.endPacket(); 
}
/****************************************************************************************
************************************FIM ETHERNET CONFIG/FUNCTIONS************************
****************************************************************************************/

/****************************************************************************************
****************************DATA/HORA*****************************************************
****************************************************************************************/
#define LEAP_YEAR(_year) ((_year%4)==0)
static byte monthDays[] = {31, 28, 31, 30 , 31, 30, 31, 31, 30, 31, 30, 31};
#define DS3231_I2C_ADDRESS 0x68
enum WeekDay {WD_SUNDAY=0, WD_MONDAY, WD_TUESDAY, WD_WEDNESDAYM, WD_THURSDAY, WD_FRIDEY, WD_SATURDAY};
 
Datetime unixTimeToDatetime(time_t timep) {
  Datetime dt = {0,0,0,0,0,0,0};
  unsigned long long epoch = timep;
  byte year, month, monthLength;
  unsigned long days;
  dt.second  =  epoch % 60;
  epoch  /= 60; // now it is minutes
  dt.minute  =  epoch % 60;
  epoch  /= 60; // now it is hours
  dt.hour =  epoch % 24;
  epoch  /= 24; // now it is days
  dt.dayOfWeek =  (epoch+4) % 7;
  year = 70;  
  days = 0;
  while((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= epoch) { year++; }
  dt.year = year; // is returned as years from 1900
  days  -= LEAP_YEAR(year) ? 366 : 365;
  epoch -= days; // now it is days in this year, starting at 0
  for (month=0; month<12; month++) {
    monthLength = ( (month==1) && LEAP_YEAR(year) ) ? 29 : monthDays[month];  // month==1 -> february
    if (epoch >= monthLength) { epoch -= monthLength; } else { break; }
  }
  dt.month = month+1;  // jan is month 0
  dt.dayOfMonth = epoch+1;  // day of month
  dt.year += 1900;
  return dt;
}

time_t datetimeToUnixTime(Datetime dt) {                                                                           // note year argument is full four digit year (or digits since 2000), i.e.1975, (year 8 is 2008)
  time_t seconds;
  if(dt.year < 69) { dt.year += 2000; }                                                                                   // seconds from 1970 till 1 jan 00:00:00 this year
  seconds = (dt.year-1970)*(60*60*24L*365);
  for (int i=1970; i<dt.year;  i++)   { if (LEAP_YEAR(i)) { seconds += 60*60*24L; } }                                       // add extra days for leap years
  for (int i=0;    i<dt.month-1; i++) { seconds += (i==1 && LEAP_YEAR(dt.year)) ? 60*60*24L*29 : 60*60*24L*monthDays[i]; }  // add days for this year
  seconds += (dt.dayOfMonth-1)*3600*24L   +   dt.hour*3600L    +    dt.minute*60L    +    dt.second;
  return seconds; 
}

class DS3231{
  private:
    static byte decToBcd(byte val) { return( (val/10*16) + (val%10) ); }  // Convert normal decimal numbers to binary coded decimal
    static byte bcdToDec(byte val) { return( (val/16*10) + (val%16) ); }  // Convert binary coded decimal to normal decimal numbers
  public:
    static void sync(time_t t)    { sync(unixTimeToDatetime(t)); }
    static void sync(Datetime dt) {
      Wire.beginTransmission(DS3231_I2C_ADDRESS);
      Wire.write(0);                       // set next input to start at the seconds register
      Wire.write(decToBcd(dt.second));     // set seconds
      Wire.write(decToBcd(dt.minute));     // set minutes
      Wire.write(decToBcd(dt.hour));       // set hours
      Wire.write(decToBcd(dt.dayOfWeek+1));// set day of week (1=Sunday, 7=Saturday)
      Wire.write(decToBcd(dt.dayOfMonth)); // set date (1 to 31)
      Wire.write(decToBcd(dt.month));      // set month
      Wire.write(decToBcd( (byte)(dt.year-2000) ));  // set year (0 to 99)
      //Wire.write(2000-decToBcd(dt.year));  // set year (0 to 99)  --> substituido pela linha acima corrigido 07/06/201
      Wire.endTransmission();
    }
    static Datetime getDatetime() {
      Datetime dt;
      Wire.beginTransmission(DS3231_I2C_ADDRESS);
      Wire.write(0);                               // set DS3231 register pointer to 00h
      Wire.endTransmission();
      Wire.requestFrom(DS3231_I2C_ADDRESS, 7); 
      dt.second     = bcdToDec(Wire.read() & 0x7f);  // request seven bytes of data from DS3231 starting from register 00h
      dt.minute     = bcdToDec(Wire.read());
      dt.hour       = bcdToDec(Wire.read() & 0x3f);
      dt.dayOfWeek  = bcdToDec(Wire.read())-1;
      dt.dayOfMonth = bcdToDec(Wire.read());
      dt.month      = bcdToDec(Wire.read());
      dt.year       = bcdToDec(Wire.read())+2000;
      return dt;
    }
    static time_t now(){ return datetimeToUnixTime(getDatetime()); }
};

class ArduinoDatetime {
  private:
    static unsigned long _mRef;  //millis de referencia na hora do sincronismo
    static time_t        _time;  //data e hora no momento do último sincronismo
  public:
    static void sync(Datetime dt) { sync(datetimeToUnixTime(dt)); }
    static void sync(time_t t)    { _mRef = millis(); _time = t; }
    static time_t now()           { return _time + (millis() - _mRef)/1000; }
    static Datetime getDatetime() { return unixTimeToDatetime(now()) ;}
};
unsigned long ArduinoDatetime::_mRef = 0;
time_t ArduinoDatetime::_time = 0;

//retorna a data e hora que o programa foi compilado
Datetime compiledDatetime() {
  Datetime dt;
  char s_month[5];
  int day, hour, minute, second, year;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
  sscanf(__DATE__, "%s %d %d", s_month, &day, &year);
  sscanf(__TIME__, "%2d %*c %2d %*c %2d", &hour, &minute, &second);
  dt.second = second;
  dt.minute = minute;
  dt.hour = hour;
  dt.dayOfMonth = day;
  dt.month = (strstr(month_names, s_month) - month_names) / 3 + 1;
  dt.year = year;
  return dt;
}

//retorna a data e hora de um servidor ntp (sem fuso-horário)
Datetime NTPDatetime(){
  if(Dns.getHostByName(host, rem_add) == 1 ){
    Serial.println("DNS resolve...");  
    Serial.print(host);
    Serial.print(" = ");
    Serial.println(rem_add);
    sendNTPpacket(rem_add);
  } else {
    Serial.println("DNS fail...");
    Serial.print("time.nist.gov = ");
    Serial.println(timeServer); // caso a primeira tentativa não retorne um host válido
    sendNTPpacket(timeServer);  // send an NTP packet to a time server
  }
  delay(1000); //aguarda um segundo, para receber os dados enviados.
  if ( Udp.parsePacket() ) {  
    Udp.read(packetBuffer, NTP_PACKET_SIZE);                              // We've received a packet, read the data from it. read the packet into the buffer
    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);    // the timestamp starts at byte 40 of the received packet and is four bytes or two words, long. First, esxtract the two words:
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);  
    unsigned long secsSince1900 = highWord << 16 | lowWord;               // combine the four bytes (two words) into a long integer this is NTP time (seconds since Jan 1 1900):
    Serial.print("Segundos desde 1 de Jan. de 1900 = " );
    Serial.println(secsSince1900);     
    const unsigned long seventyYears = 2208988800UL;      // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
    unsigned long epoch = secsSince1900 - seventyYears;  //desconta 70 anos
    return unixTimeToDatetime(epoch);
  }
  return unixTimeToDatetime(0);  //01/01/1970 00:00:00
}

//---------------------funcoes para sincronia da data e hora da Classe ArduinoDatetime
void syncCompiledDatetime(){
  lcd.setCursor(0, 0);
  lcd.clear();
  lcd.print("Sinc pela compilacao");
  ArduinoDatetime::sync(compiledDatetime());
}

void syncDS3231(){
  lcd.setCursor(0, 0);
  lcd.clear();
  lcd.print("Sinc. pelo rtc");
  ArduinoDatetime::sync(DS3231::getDatetime());
}

void syncNTP(){
  lcd.setCursor(0, 0);
  lcd.clear();
  lcd.print("Sinc via NTP");
  ArduinoDatetime::sync(NTPDatetime());
}

void syncSerial() {
  //formato    |DATETIME|DIA|MES|ANO|HORA|MINUTO|SEGUNDO|
  while (Serial.available() ) {
    String str = Serial.readStringUntil('|');
    if (str == F("DATETIME")) {

      lcd.setCursor(0, 0);
      lcd.clear();
      lcd.print("Sinc. pela serial");
      
      Datetime dt;
      dt.dayOfMonth = Serial.parseInt();
                      Serial.readStringUntil('|');
      dt.month      = Serial.parseInt();
                      Serial.readStringUntil('|');
      dt.year       = Serial.parseInt();
                      Serial.readStringUntil('|');
      dt.hour       = Serial.parseInt();
                      Serial.readStringUntil('|');
      dt.minute     = Serial.parseInt();
                      Serial.readStringUntil('|');
      dt.second     = Serial.parseInt();
                      Serial.readStringUntil('|');

      ArduinoDatetime::sync(dt);
    }
  }
}

String zero(int a){ if(a>=10) {return (String)a+"";} else { return "0"+(String)a;} }

/****************************************************************************************
****************************FIM DATA/HORA************************************************
****************************************************************************************/

void setup() {
  Serial.begin(9600);
  Wire.begin();
  lcd.init();                      // initialize the lcd 
  lcd.backlight();


  pinMode(4, OUTPUT);      //ANTES DE INICIAR O ETHERNET, DESABILITA O SDCARD
  digitalWrite(4, HIGH);
  if (Ethernet.begin(mac) == 0) { Serial.println("Failed to configure Ethernet using DHCP"); }
  Udp.begin(localPort);
  Dns.begin(Ethernet.dnsServerIP() );

  
  syncNTP();
}

void loop() {

  syncSerial();   //aguarda a entrada de dados via serial pra configurar a data.
  
  Datetime dt = ArduinoDatetime::getDatetime();

  lcd.setCursor(0, 1);
  lcd.print(zero(dt.dayOfMonth));
  lcd.print("/");
  lcd.print(zero(dt.month));
  lcd.print("/");
  lcd.print(dt.year);
  lcd.print(" ");
  lcd.print(zero(dt.hour));
  lcd.print(":");
  lcd.print(zero(dt.minute));
  lcd.print(":");
  lcd.print(zero(dt.second));
  delay(999);

  static unsigned long m = 0;
  static int cont = 0;
  if (millis() - m > 10000){
    m = millis();

    if (cont == 0){ syncDS3231(); }
    if (cont == 1){ syncCompiledDatetime(); }
    if (cont == 2){ syncNTP(); }
    cont++;
    if (cont == 3){ cont = 0; }
  }
}


Vídeo



quinta-feira, 16 de junho de 2016

Arduino - Tetris game - com rotary encoder

Mais um jogo pra Arduino, dessa vez o famoso Tetris!!

Essa é uma implementação para fins didáticos, voltada para programadores iniciantes (ou não).

Veja os demais vídeos de jogos para Arduino no canal:

Wall Defender - Versão display 8x8: https://youtu.be/LBVgnoU8u38
Wall Defender - Versão display LCD: https://youtu.be/7VWK050VXa4
Vídeo do jogo Snake: https://youtu.be/In1RD3-msJ8
Vídeo do jogo Race: https://youtu.be/lVmGilUGi8A

Inscreva-se no Canal: https://www.youtube.com/user/fabianoallex?sub_confirmation=1
Curta a página do facebook: https://www.facebook.com/dicasarduino

Vídeo:




Código-fonte:


/*
Fabiano A. Arndt - 2016
www.youtube.com/user/fabianoallex
www.facebook.com/dicasarduino
fabianoallex@gmail.com
*/
 
/*************************************************************************************************************
*******************************BIT ARRAY *********************************************************************
**************************************************************************************************************
mais informações aqui: http://fabianoallex.blogspot.com.br/2015/09/arduino-array-de-bits.html
**************************************************************************************************************/
class BitArray2D {
  private:
    unsigned int _rows;
    unsigned int _columns;
    unsigned int _cols_array; //pra cada 8 colunas, 1 byte é usado 
    byte**       _bits;
  public:
    BitArray2D(unsigned int rows, unsigned int columns){
      _rows       = rows;
      _columns    = columns;
      _cols_array = columns/8 + (_columns%8 ? 1 : 0) + 1; //divide por 8 o número de colunas
      _bits = (byte **)malloc(_rows * sizeof(byte *));
      for(int i=0;i<_rows;i++){ _bits[i] = (byte *)malloc(  _cols_array  *  sizeof(byte)); } //cria varios arrays
      clear();
    }
    unsigned int rows(){ return _rows; }
    unsigned int columns(){ return _columns; }
    void clear() { 
      for(int i=0;i<_rows;i++){      
        for(int j=0; j<_cols_array;j++) { _bits[i][j] = B00000000; }       
      }   
    }
    void write(unsigned int row, unsigned int column, int value){
      byte b = _bits[row][ column/8 + (column%8 ? 1 : 0) ];
      unsigned int bit = column%8;    
      if (value) { b |= (1 << bit); } else { b &= ~(1 << bit);  }
      _bits[row][ column/8 + (column%8 ? 1 : 0) ] = b;
    }
    void write(byte value){
      for(int i=0;i<_rows;i++){      
        for(int j=0; j<_cols_array;j++) {      
          _bits[i][j] = value ? B11111111 : B00000000;     
        }       
      }  
    }
    int read(unsigned int row, unsigned int column){
      byte b = _bits[row][ column/8 + (column%8 ? 1 : 0) ];
      unsigned int bit = column%8;    
      return (b & (1 << bit)) != 0;
    }
    void toggle(unsigned int row, unsigned int column){ write(row, column, !read(row, column)); }
    void toggle(){ for(int i=0;i<_rows;i++){      for(int j=0; j<_columns;j++) {      toggle(i,j);   }   }   }
};
/*************************************************************************************************************
*******************************FIM BIT ARRAY *****************************************************************
**************************************************************************************************************/
    
/*************************************************************************************************************
************************************CLASSE UNIQUE RANDOM******************************************************
**************************************************************************************************************
mais informações aqui: http://fabianoallex.blogspot.com.br/2015/09/arduino-numeros-aleatorio-repetidos-e.html
*************************************************************************************************************/
class UniqueRandom{
  private:
    int _index;
    int _min;
    int _max;
    int _size;
    int* _list;
    void _init(int min, int max) {
      _list = 0; 
      if (min < max) { _min = min; _max = max; } else { _min = max; _max = min; }
      _size = _max - _min; 
      _index = 0;
    }    
  public:
    UniqueRandom(int max)           { _init(0,   max); randomize(); } //construtor com 1 parametro
    UniqueRandom(int min, int max)  { _init(min, max); randomize(); } //construtor com 2 parametros
    void randomize() {
      _index = 0;
      if (_list == 0) { _list = (int*) malloc(size() * sizeof(int)); }  
      for (int i=0; i<size(); i++) {   _list[i] = _min+i;  }   //preenche a lista do menor ao maior valor
      //embaralha a lista
      for (int i=0; i<size(); i++) {  
        int r = random(0, size());     //sorteia uma posição qualquer
        int aux = _list[i];               
        _list[i] = _list[r];
        _list[r] = aux;
      }
    }
    int next() {                                  //retorna o proximo numero da lista
      int n = _list[_index++];
      if (_index >= size() ) { _index = 0;} //após recuper o ultimo numero, recomeça na posicao 0
      return n;
    }
    void first() { _index = 0; }
    boolean eof() { return size() == _index+1; }
    int size() { return _size; }
    int getIndex() {  return _index; }
    ~UniqueRandom(){ free ( _list ); }  //destrutor
};
/*************************************************************************************************************
************************************FIM CLASSE UNIQUE RANDOM**************************************************
*************************************************************************************************************/
    
/*************************************************************************************************************
*******************************LEDCONTROL ALTERADA************************************************************
mais informações: http://fabianoallex.blogspot.com.br/2015/09/arduino-alteracoes-na-biblioteca.html
**************************************************************************************************************/
//the opcodes for the MAX7221 and MAX7219
#define OP_DECODEMODE  9
#define OP_INTENSITY   10
#define OP_SCANLIMIT   11
#define OP_SHUTDOWN    12
#define OP_DISPLAYTEST 15
class LedControl {
  private :
    byte spidata[16];
    byte * status;
    int SPI_MOSI;
    int SPI_CLK;
    int SPI_CS;
    int maxDevices;
    int _auto_send;
    void spiTransfer(int addr, volatile byte opcode, volatile byte data) {
      int offset   = addr*2;
      int maxbytes = maxDevices*2;
      for(int i=0;i<maxbytes;i++)  { spidata[i]=(byte)0; }
      spidata[offset+1] = opcode;
      spidata[offset]   = data;
      digitalWrite(SPI_CS,LOW);
      for(int i=maxbytes;i>0;i--) { shiftOut(SPI_MOSI,SPI_CLK,MSBFIRST,spidata[i-1]); }
      digitalWrite(SPI_CS,HIGH);
    }
  public:
    LedControl(int dataPin, int clkPin, int csPin, int numDevices) {
      _auto_send  = true;
      SPI_MOSI    = dataPin;
      SPI_CLK     = clkPin;
      SPI_CS      = csPin;
      maxDevices  = numDevices;
      pinMode(SPI_MOSI, OUTPUT);
      pinMode(SPI_CLK,  OUTPUT);
      pinMode(SPI_CS,   OUTPUT);
      digitalWrite(SPI_CS, HIGH);
      status      = new byte[maxDevices * 8]; //instancia o array de acordo com a quantia de displays usados
      for(int i=0;i<maxDevices * 8 ;i++) { status[i]=0x00; }
      for(int i=0;i<maxDevices;i++) {
        spiTransfer(i, OP_DISPLAYTEST,0);
        setScanLimit(i, 7);               //scanlimit is set to max on startup
        spiTransfer(i, OP_DECODEMODE,0);  //decode is done in source
        clearDisplay(i);
        shutdown(i,true);                 //we go into shutdown-mode on startup
      }
    }
    void startWrite() {  _auto_send = false;  };
    void send() {
      for (int j=0; j<maxDevices; j++) {
        int offset = j*8;
        for(int i=0;i<8;i++) { spiTransfer(j, i+1, status[offset+i]); }
      }
      _auto_send = true;
    }
    int getDeviceCount(){ return maxDevices; }
    void shutdown(int addr, bool b){
      if(addr<0 || addr>=maxDevices) return;
      spiTransfer(addr, OP_SHUTDOWN, b ? 0 : 1);
    }
    void setScanLimit(int addr, int limit){
      if(addr<0 || addr>=maxDevices) return;
      if(limit>=0 && limit<8) spiTransfer(addr, OP_SCANLIMIT,limit);
    }
    void setIntensity(int addr, int intensity) {
      if(addr<0 || addr>=maxDevices)   {  return;                                    }
      if(intensity>=0 && intensity<16) {  spiTransfer(addr, OP_INTENSITY, intensity); }
    }
    void clearDisplay(int addr){
      if(addr<0 || addr>=maxDevices) return;
      int offset = addr*8;
      for(int i=0;i<8;i++) {
        status[offset+i] = 0;
        if (_auto_send) { spiTransfer(addr, i+1, status[offset+i]); }
      }
    }
    void setLed(int addr, int row, int column, boolean state) {
      if(addr<0 || addr>=maxDevices)             { return; }
      if(row<0 || row>7 || column<0 || column>7) { return; }
      int offset = addr*8;
      byte val = B10000000 >> column;
      if(state) { status[offset+row] = status[offset+row] | val; } else { val=~val; status[offset+row] = status[offset+row]&val; }
      if (_auto_send) { spiTransfer(addr, row+1, status[offset+row]); }
    }
    void setRow(int addr, int row, byte value) {
      if(addr<0 || addr>=maxDevices) return;
      if(row<0 || row>7) return;
      int offset = addr*8;
      status[offset+row] = value;
      if (_auto_send) { spiTransfer(addr, row+1, status[offset+row]); }
    }
    void setColumn(int addr, int col, byte value) {
      if(addr<0 || addr>=maxDevices) return;
      if(col<0 || col>7)             return;
      byte val;
      for(int row=0; row<8; row++) {
        val=value >> (7-row);
        val=val & 0x01;
        setLed(addr,row,col,val);
      }
    }
};
/*************************************************************************************************************
*******************************FIM LEDCONTROL ALTERADA********************************************************
**************************************************************************************************************/
    
/*************************************************************************************************************
************************************CLASSE ROTARY ENCODER*****************************************************
mais informações: http://fabianoallex.blogspot.com.br/2016/05/arduino-rotary-encoder.html
*************************************************************************************************************/
#define ROTARY_NO_BUTTON     255
        
struct RotaryEncoderLimits{
  int min;
  int max;
};
        
class RotaryEncoder {
  private:
    byte _pin_clk;
    byte _pin_dt;
    byte _pin_sw;
    volatile byte _num_results;
    volatile int _result;
    volatile int * _results;
    byte _index_result;
    RotaryEncoderLimits * _limits;
    byte _a : 1;
    byte _b : 1;
  public:
    RotaryEncoder(byte pin_clk, byte pin_dt, byte pin_sw = ROTARY_NO_BUTTON, byte num_results=1, RotaryEncoderLimits * limits=0){   //parametro do botao opcional
      _pin_clk = pin_clk;
      _pin_dt = pin_dt;
      _pin_sw = pin_sw;
      pinMode(_pin_clk, INPUT);
      pinMode(_pin_dt, INPUT);
      if (_pin_sw != ROTARY_NO_BUTTON){ 
        pinMode(_pin_sw, INPUT); 
        digitalWrite(_pin_sw, HIGH);
      }
      if (num_results == 0) { num_results = 1; }
      _num_results = num_results;
      _results = new int[_num_results];
      for (int i; i<_num_results; i++){ _results[i] = (limits) ? limits[i].min : 0; }
      _index_result = 0;
      _limits = limits;
      _a = false;
      _b = false;
    }
    byte getIndex() { return _index_result; }
    void next()     { _index_result++; if (_index_result >= _num_results) { _index_result = 0; } } 
    void update_a() {
      _result = 0;
      delay (1);
      if( digitalRead(_pin_clk) != _a ) { 
        _a = !_a;
        if ( _a && !_b ) { _result = -1; }
      }
      if (_results[_index_result]+_result >= _limits[_index_result].min && 
          _results[_index_result]+_result <= _limits[_index_result].max ) {
        _results[_index_result] += _result;
      }
    }
    void update_b() {
      _result = 0;
      delay (1);
      if( digitalRead(_pin_dt) != _b ) {  
        _b = !_b;
        if ( _b && !_a ) { _result = +1; }
      }
      if (_results[_index_result]+_result >= _limits[_index_result].min && 
          _results[_index_result]+_result <= _limits[_index_result].max ) {
        _results[_index_result] += _result;
      }
    }
    int read(){ return _result; }                                        //retorn -1, 0 ou 1.
    int getValue(int index=-1) {                                         //retorna o valor da variável corrente ou a passada como parametro
      if (index < 0 ){ return _results[_index_result]; }
      return _results[index];
    }
    void setValue(int value, int index=-1){ _results[ (index==-1) ? _index_result : index] = value; }         //caso a variável inicializa em determinado valor diferente de zero, utilizar esse método.
    int buttonRead(){ return (_pin_sw == ROTARY_NO_BUTTON) ? LOW : digitalRead(_pin_sw); }
};
/*************************************************************************************************************
************************************FIM CLASSE ROTARY ENCODER*************************************************
*************************************************************************************************************/
    
/*************************************************************************************************************
*******************************CLASSES TETRIS GAME************************************************************
**************************************************************************************************************/
const int TETRIS_TIME_INIT = 700; //tempo entre deslocamento dos inimigos
const int TETRIS_TIME_INC  = 20;  //incremento da velocidade
enum Direction        { DIR_STOP, DIR_TOP, DIR_LEFT, DIR_BOTTOM, DIR_RIGHT};
enum TetrisGameStatus { TETRIS_GAME_ON, TETRIS_GAME_OVER };
enum TetrisBlockTypes { TETRIS_I, TETRIS_J, TETRIS_L, TETRIS_O, TETRIS_S, TETRIS_Z, TETRIS_T, TETRIS_COUNT_BTYPES};
struct Position       { int row, column; };
struct Block          { byte row0:4; byte row1:4; byte row2:4; byte row3:4; };  //4 bit cada linha
//blocos e suas rotacoes
Block block_i[] = { {B1111, B0000, B0000, B0000}, {B0001, B0001, B0001, B0001} };
Block block_j[] = { {B0000, B0010, B0010, B0011}, {B0000, B0000, B0001, B0111}, {B0000, B0110, B0010, B0010}, {B0000, B0000, B0111, B0100} };
Block block_l[] = { {B0000, B0010, B0010, B0110}, {B0000, B0000, B0100, B0111}, {B0000, B0011, B0010, B0010}, {B0000, B0000, B0111, B0001} };
Block block_o[] = { {B0000, B0000, B0110, B0110} };
Block block_s[] = { {B0000, B0001, B0011, B0010}, {B0000, B0000, B0110, B0011} };
Block block_z[] = { {B0000, B0000, B0110, B1100}, {B0000, B1000, B1100, B0100} };
Block block_t[] = { {B0000, B0000, B0010, B0111}, {B0000, B0010, B0011, B0010}, {B0000, B0000, B0111, B0010}, {B0000, B0001, B0011, B0001} };
class TetrisBlock{
  private:
    Block _block;       //bloco que vai descendo. player
    Position _position; //posição do bloco na tela. pode assumir posição negativa
    TetrisBlockTypes _blockType;
    byte _blockTypeRotation;
  public:
    Position getPosition()  { return _position; }
    byte getRow(int row) { 
      if (row == 0 ) return _block.row0;
      if (row == 1 ) return _block.row1;
      if (row == 2 ) return _block.row2;
      if (row == 3 ) return _block.row3; 
      return 0;
    }
    byte getColumn(int col) {
      byte b = 0;
      for (int i=0;i<4;i++){ bitWrite(b, 3-i, bitRead(getRow(i), 3-col) );  }
      return b;
    }
    int read(int row, int column){
      byte b = getRow(row);
      unsigned int bit = column%8;    
      return (b & (1 << bit)) != 0;
    }
    void left()   { _position.column--;  }
    void right()  { _position.column++;  }
    void bottom() { _position.row++;     }
    void top()    { _position.row--;     }
    void generateBlock(byte col) {
      _position.row = 0; 
      _position.column = col; 
      _blockType =  (TetrisBlockTypes) random(0, TETRIS_COUNT_BTYPES);
      _blockTypeRotation = 0;
      
      if ( _blockType == TETRIS_I ){ _block = block_i[_blockTypeRotation]; }
      if ( _blockType == TETRIS_J ){ _block = block_j[_blockTypeRotation]; }
      if ( _blockType == TETRIS_L ){ _block = block_l[_blockTypeRotation]; }
      if ( _blockType == TETRIS_O ){ _block = block_o[_blockTypeRotation]; }
      if ( _blockType == TETRIS_S ){ _block = block_s[_blockTypeRotation]; }
      if ( _blockType == TETRIS_Z ){ _block = block_z[_blockTypeRotation]; }
      if ( _blockType == TETRIS_T ){ _block = block_t[_blockTypeRotation]; }
      
      if (!getRow(0))   { _position.row--;
        if (!getRow(1))   { _position.row--;          
          if (!getRow(2))   { _position.row--; } } }
    }
    void rotate(int direction = 1){
      int maxPos = 0;
      if ( _blockType == TETRIS_I ){ maxPos = sizeof(block_i)/sizeof(Block); }
      if ( _blockType == TETRIS_J ){ maxPos = sizeof(block_j)/sizeof(Block); }
      if ( _blockType == TETRIS_L ){ maxPos = sizeof(block_l)/sizeof(Block); }
      if ( _blockType == TETRIS_O ){ maxPos = sizeof(block_o)/sizeof(Block); }
      if ( _blockType == TETRIS_S ){ maxPos = sizeof(block_s)/sizeof(Block); }
      if ( _blockType == TETRIS_Z ){ maxPos = sizeof(block_z)/sizeof(Block); }
      if ( _blockType == TETRIS_T ){ maxPos = sizeof(block_t)/sizeof(Block); }
      (direction==1) ? _blockTypeRotation++ : _blockTypeRotation--;
      if (_blockTypeRotation == 255)    { _blockTypeRotation = maxPos-1; }
      if (_blockTypeRotation >= maxPos) { _blockTypeRotation = 0; }
      if ( _blockType == TETRIS_I ){ _block = block_i[_blockTypeRotation]; }
      if ( _blockType == TETRIS_J ){ _block = block_j[_blockTypeRotation]; }
      if ( _blockType == TETRIS_L ){ _block = block_l[_blockTypeRotation]; }
      if ( _blockType == TETRIS_O ){ _block = block_o[_blockTypeRotation]; }
      if ( _blockType == TETRIS_S ){ _block = block_s[_blockTypeRotation]; }
      if ( _blockType == TETRIS_Z ){ _block = block_z[_blockTypeRotation]; }
      if ( _blockType == TETRIS_T ){ _block = block_t[_blockTypeRotation]; }
    }
};
  
class TetrisGame {
  private:
    BitArray2D * _display;   //display com todos os objetos da tela. recebido como parâmetro
    BitArray2D * _recipient; //recepiente onde os blocos vão se acumulando. criado internamente pela classe
    TetrisBlock * _block;
    Direction _direction;
    TetrisGameStatus _tetrisGameStatus;
    unsigned long _last_millis;
    unsigned long _last_millis_player;
    int _time;
    int _score;
    byte _fast : 1;          //acelerar a descida
    UniqueRandom * _ur;      //utilizado no game over
    void _gameOver(){ 
      _tetrisGameStatus = TETRIS_GAME_OVER; 
      _direction   = DIR_STOP;
      _time = 20;
    }
    void _inc_speed(){
      _score++;
      _time -= TETRIS_TIME_INC;
    }
    void _start(){
      _recipient->clear();
      _last_millis = 0;
      _score = 0;
      _time = TETRIS_TIME_INIT;
      _fast = 0;
      _direction = DIR_STOP;
      _tetrisGameStatus = TETRIS_GAME_ON; 
      _newBlock();
    }
    void _newBlock() {
      _block->generateBlock( _display->columns() / 2 - 1 );
      if (_testCollision()){ _gameOver(); }
    }
    void _updateDisplay() {
      if (_tetrisGameStatus == TETRIS_GAME_ON) { _display->clear();  }
      for (int r=0; r<4; r++) {
        for (int c=0; c<4; c++) { 
          if ( _block->read(r, c) ) { 
            _display->write(_block->getPosition().row+r, _block->getPosition().column+c,  HIGH ); 
          }
        }
      }
      for (int row=0; row<_display->rows(); row++) {
        for (int col=0; col<_display->columns(); col++) {
          boolean val = _recipient->read(row, col);
          if (val) { _display->write(row, col,  val ); }
        }
      }
    }
    boolean _testCollision(){
      int col = _block->getPosition().column;  //coluna do bloco em relacao ao recepiente. pode ser negativa
      int row = _block->getPosition().row;     //linha do bloco em relacao ao recepiente. pode ser negativa
      for (byte c=0; c<4; c++){
        for (byte r=0; r<4; r++){
          if (! _block->read(r, c) )            { continue;  }  
          if ( row+r < 0 )                      {return true;}  
          if ( row+r >= _display->rows() )      {return true;}
          if ( col+c < 0 )                      {return true;}
          if ( col+c >= _display->columns() )   {return true;}
          if ( _recipient->read(row+r, col+c) ) {return true;}
        }
      }
      return false;
    }
    boolean _canLeft(){
      _block->left();
      boolean r = ! _testCollision();
      _block->right();
      return r;
    }
    boolean _canRight(){
      _block->right();
      boolean r = ! _testCollision();
      _block->left();
      return r;
    }
    boolean _canBottom(){
      _block->bottom();
      boolean r = ! _testCollision();
      _block->top();
      return r;
    }
    boolean _canRotate(){
      _block->rotate();
      boolean r = ! _testCollision();
      _block->rotate(-1);
      return r;
    }
    void _blockToRecipient(){
      for (byte c=0; c<4; c++){
        for (byte r=0; r<4; r++){
          if ( _block->read(r, c) ){ _recipient->write(_block->getPosition().row+r, _block->getPosition().column+c, HIGH); }
        }
      }
      _verifyCompleteRow();
      _newBlock();
    }
    void _verifyCompleteRow(){
      for (int row=0; row<_recipient->rows(); row++) {
        boolean complete = true;
        for (int col=0; col<_recipient->columns(); col++) {
          if (! _recipient->read(row, col) ) { complete = false; break; }
        }
        if (complete){ 
          _incScore(); 
          //move as linhas pra baixo
          for (int row2 = row; row2>=0; row2--) {
            for (int col2=0; col2<_recipient->columns(); col2++){
              _recipient->write(row2, col2,  (row2>0) ? _recipient->read(row2-1, col2) : 0 );
            }
          }
        }
      }
    }
    void _incScore(){
      _score++;
      _time -= TETRIS_TIME_INC;
    }
    void _runGameOver(){
      int r   = _ur->next();
      int row = (r / _display->columns());
      int col = (r % _display->columns());
      _display->write(row, col, HIGH );
      if ( _ur->getIndex() == (_ur->size()-1) ) {
        _ur->randomize();
        _ur->first();
        _start(); 
      }
    }
    void _runPlayer() {
      if (_direction == DIR_LEFT  && _canLeft()  ) { _block->left();  }
      if (_direction == DIR_RIGHT && _canRight() ) { _block->right(); }
      _direction = DIR_STOP;
    }
    void _runBlock() {
      _canBottom() ? _block->bottom() : _blockToRecipient() ;
      _direction = DIR_STOP; 
    }
  public:
    TetrisGame(BitArray2D * display){ 
      _display   = display;
      _recipient = new BitArray2D( _display->rows(), _display->columns() );
      _ur        = new UniqueRandom( _display->rows() * _display->columns() );
      _block     = new TetrisBlock();
      _start();
    }
    void fast()   { _fast = 1; }  //acelerar o deslocamento
    void noFast() { _fast = 0; }  //descida do bloco na velocidade normal
    void left()   { _direction = DIR_LEFT;   }
    void right()  { _direction = DIR_RIGHT;  }
    void bottom() { _direction = DIR_BOTTOM; }
    void rotate() { if (_canRotate()) { _block->rotate(); } }
    int getScore(){ return _score; }
    Position getPosition() { return _block->getPosition(); }
    int update(){
      int r = false;
      if (millis() - _last_millis > ( (_fast==1) ? 50 : _time ) ) { 
        r = true;
        _last_millis = millis();  
        if (_tetrisGameStatus == TETRIS_GAME_ON)   { _runBlock(); } 
        if (_tetrisGameStatus == TETRIS_GAME_OVER) { _runGameOver(); }
      }
      if (millis() - _last_millis_player > 70 ) {  //atualiza os movimentos do player a cada 70ms
        r = true;
        _last_millis_player = millis();  
        if (_tetrisGameStatus == TETRIS_GAME_ON)   { _runPlayer(); } 
      }
      if (r) { _updateDisplay(); }
      return r; //r-->indica se houve mudança no display
    }
};
/*************************************************************************************************************
*******************************FIM CLASSES TETRIS GAME********************************************************
**************************************************************************************************************/
  
/*************************************************************************************************************
*******************************DISPLAY************************************************************************
**************************************************************************************************************/
  
/*
rotation indica qual parte do display estará para cima
  
         TOP
L  . . . . . . . . R
E  . . . . . . . . I
F  . . . . . . . . G
T  . . . . . . . . H
   . . . . . . . . T
   . . . . . . . .
   . . . . . . . .
   . . . . . . . .   
       BOTTOM
      
*/
enum Rotation {TOP, LEFT, BOTTOM, RIGHT};
  
struct Display {
  int index;
  Position position; 
  Rotation rotation;
};
  
/*************************************************************************************************************
*******************************FIM DISPLAY********************************************************************
**************************************************************************************************************/
  
/*************************************************************************************************************
*******************************DECLARACAO DOS OBJETOS*********************************************************
**************************************************************************************************************/
/*
 tamanho display real
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * * 
*/
    
const int LINHAS  = 16;
const int COLUNAS = 8;
 
Display displays_8x8[] = {
  {1, {0,0}, BOTTOM},
  {0, {8,0}, TOP}
};
     
BitArray2D ba(LINHAS, COLUNAS); //8 linhas e 16 colunas, usada para armazenar o estado do display
TetrisGame tetris(&ba);
    
RotaryEncoderLimits lim[] = { {-1000,1000} };  //limites máximos e mínimos que as variaveis podem atingir
RotaryEncoder       re(A0, A1, 4, 1, lim);  //pino clk, pino dt, pino sw, qtd variaveis, limites
 
LedControl lc=LedControl(10,12,11,2);  //pin 10: DataIn ; pin 12: CLK ;  pin 11: LOAD   --- 2 display 8x8
/*************************************************************************************************************
*******************************FIM DECLARACAO DOS OBJETOS*****************************************************
**************************************************************************************************************/
      
/*************************************************************************************************************
*******************************TRATAMENTO DAS INTERRUPÇÕES****************************************************
**************************************************************************************************************/
//interrupções dos pinos A0 e A1 via Pin Change Interrupt
ISR(PCINT1_vect) {
  volatile static byte lastVal_a0 = LOW;
  volatile static byte lastVal_a1 = LOW;
  byte val_a0 = digitalRead(A0);
  byte val_a1 = digitalRead(A1);
  if (lastVal_a0 != val_a0){ re.update_a(); lastVal_a0 = val_a0; }
  if (lastVal_a1 != val_a1){ re.update_b(); lastVal_a1 = val_a1; }
}
     
void setup_interrupts(){
  //-----PCI - Pin Change Interrupt ----
  pinMode(A0,INPUT);   // set Pin as Input (default)
  digitalWrite(A0,HIGH);  // enable pullup resistor
  pinMode(A1,INPUT);   // set Pin as Input (default)
  digitalWrite(A1,HIGH);  // enable pullup resistor
  cli();
  PCICR |= 0b00000010; // habilita a porta C - Pin Change Interrupts
  PCMSK1 |= 0b00000011; // habilita interrupção da porta c nos pinos: PCINT8 (A0) e PCINT9(A1)
  sei();
}
/*************************************************************************************************************
*******************************FIM TRATAMENTO DAS INTERRUPÇÕES****************************************************
**************************************************************************************************************/
void update_display() {
  lc.startWrite();
  for (int lin=0; lin<ba.rows(); lin++) {
    for (int col=0; col<ba.columns(); col++) {
      for (int i=0; i<sizeof(displays_8x8)/sizeof(Display); i++) {
        int l = lin - displays_8x8[i].position.row;
        int c = col - displays_8x8[i].position.column;
        if (l>=0 && l<=7 && c>=0 && c<=7) {
          if (displays_8x8[i].rotation == BOTTOM) {            c=7-c; l=7-l;   }
          if (displays_8x8[i].rotation == LEFT)   { int aux=c; c=l;   l=7-aux; }
          if (displays_8x8[i].rotation == RIGHT)  { int aux=l; l=c;   c=7-aux; }
          lc.setLed(displays_8x8[i].index, l, c, ba.read(lin, col) );
        }
      }
    }
  }   
  lc.send();
}
  
void setup() { 
  lc.shutdown(0,false);
  lc.setIntensity(0,1);
  lc.clearDisplay(0);  
  lc.shutdown(1,false);
  lc.setIntensity(1,1);
  lc.clearDisplay(1);  
   
  setup_interrupts();
  randomSeed(analogRead(A2));
  re.setValue(0, 0);  //inicializa o rotary em 0
}
       
void loop() {
  static int val_encoder = 0;
  static boolean change = false;
  
  if (re.getValue(0) < val_encoder && !change){ tetris.left();  change = true; }  //sentido anti-horario move para esquerda
  if (re.getValue(0) > val_encoder && !change){ tetris.right(); change = true; }  //sentido horario move para direita
    
  val_encoder = re.getValue(0);
  
  if ( tetris.update() ) { 
    change = false;
    update_display(); 
  }  
      
  //controla o click do botao do enconder
  //clique curto --> rotate
  //clique longo --> fast
  static byte lastb = HIGH; //leitura anterior do botão
  static unsigned long m_pressed = 0;  //millis na borda de subida do botão
  
  int b = re.buttonRead();
  if( !b && lastb ) {    //borda de subida
    m_pressed = millis();
    delay(70);
  }
  if( b && !lastb ) {    //borda de descida
    if (millis() - m_pressed < 350) {
      tetris.rotate();
      m_pressed = millis();
    }
  } 
  if( !b &&  millis()-m_pressed > 350){
    tetris.fast(); 
  } else {
    tetris.noFast(); 
  }
  lastb = b;
} 


Atualização 20/06/2016 - Versão em display LCD 16x2

Como já implementei alguns outros jogos em display lcd 16x2, não poderia deixar de fazer uma versão do tetris também.

Vídeo:



Código-fonte:

/*
Fabiano A. Arndt - 2016
www.youtube.com/user/fabianoallex
www.facebook.com/dicasarduino
fabianoallex@gmail.com
*/

#include <LiquidCrystal.h>
  
/*************************************************************************************************************
*******************************BIT ARRAY *********************************************************************
**************************************************************************************************************
mais informações aqui: http://fabianoallex.blogspot.com.br/2015/09/arduino-array-de-bits.html
**************************************************************************************************************/
class BitArray2D {
  private:
    unsigned int _rows;
    unsigned int _columns;
    unsigned int _cols_array; //pra cada 8 colunas, 1 byte é usado 
    byte**       _bits;
  public:
    BitArray2D(unsigned int rows, unsigned int columns){
      _rows       = rows;
      _columns    = columns;
      _cols_array = columns/8 + (_columns%8 ? 1 : 0) + 1; //divide por 8 o número de colunas
      _bits = (byte **)malloc(_rows * sizeof(byte *));
      for(int i=0;i<_rows;i++){ _bits[i] = (byte *)malloc(  _cols_array  *  sizeof(byte)); } //cria varios arrays
      clear();
    }
    unsigned int rows(){ return _rows; }
    unsigned int columns(){ return _columns; }
    void clear() { 
      for(int i=0;i<_rows;i++){      
        for(int j=0; j<_cols_array;j++) { _bits[i][j] = B00000000; }       
      }   
    }
    void write(unsigned int row, unsigned int column, int value){
      byte b = _bits[row][ column/8 + (column%8 ? 1 : 0) ];
      unsigned int bit = column%8;    
      if (value) { b |= (1 << bit); } else { b &= ~(1 << bit);  }
      _bits[row][ column/8 + (column%8 ? 1 : 0) ] = b;
    }
    void write(byte value){
      for(int i=0;i<_rows;i++){      
        for(int j=0; j<_cols_array;j++) {      
          _bits[i][j] = value ? B11111111 : B00000000;     
        }       
      }  
    }
    int read(unsigned int row, unsigned int column){
      byte b = _bits[row][ column/8 + (column%8 ? 1 : 0) ];
      unsigned int bit = column%8;    
      return (b & (1 << bit)) != 0;
    }
    void toggle(unsigned int row, unsigned int column){ write(row, column, !read(row, column)); }
    void toggle(){ for(int i=0;i<_rows;i++){      for(int j=0; j<_columns;j++) {      toggle(i,j);   }   }   }
};
/*************************************************************************************************************
*******************************FIM BIT ARRAY *****************************************************************
**************************************************************************************************************/
     
/*************************************************************************************************************
************************************CLASSE UNIQUE RANDOM******************************************************
**************************************************************************************************************
mais informações aqui: http://fabianoallex.blogspot.com.br/2015/09/arduino-numeros-aleatorio-repetidos-e.html
*************************************************************************************************************/
class UniqueRandom{
  private:
    int _index;
    int _min;
    int _max;
    int _size;
    int* _list;
    void _init(int min, int max) {
      _list = 0; 
      if (min < max) { _min = min; _max = max; } else { _min = max; _max = min; }
      _size = _max - _min; 
      _index = 0;
    }    
  public:
    UniqueRandom(int max)           { _init(0,   max); randomize(); } //construtor com 1 parametro
    UniqueRandom(int min, int max)  { _init(min, max); randomize(); } //construtor com 2 parametros
    void randomize() {
      _index = 0;
      if (_list == 0) { _list = (int*) malloc(size() * sizeof(int)); }  
      for (int i=0; i<size(); i++) {   _list[i] = _min+i;  }   //preenche a lista do menor ao maior valor
      //embaralha a lista
      for (int i=0; i<size(); i++) {  
        int r = random(0, size());     //sorteia uma posição qualquer
        int aux = _list[i];               
        _list[i] = _list[r];
        _list[r] = aux;
      }
    }
    int next() {                                  //retorna o proximo numero da lista
      int n = _list[_index++];
      if (_index >= size() ) { _index = 0;} //após recuper o ultimo numero, recomeça na posicao 0
      return n;
    }
    void first() { _index = 0; }
    boolean eof() { return size() == _index+1; }
    int size() { return _size; }
    int getIndex() {  return _index; }
    ~UniqueRandom(){ free ( _list ); }  //destrutor
};
/*************************************************************************************************************
************************************FIM CLASSE UNIQUE RANDOM**************************************************
*************************************************************************************************************/
     
/*************************************************************************************************************
************************************CLASSE ROTARY ENCODER*****************************************************
mais informações: http://fabianoallex.blogspot.com.br/2016/05/arduino-rotary-encoder.html
*************************************************************************************************************/
#define ROTARY_NO_BUTTON     255
         
struct RotaryEncoderLimits{
  int min;
  int max;
};
         
class RotaryEncoder {
  private:
    byte _pin_clk;
    byte _pin_dt;
    byte _pin_sw;
    volatile byte _num_results;
    volatile int _result;
    volatile int * _results;
    byte _index_result;
    RotaryEncoderLimits * _limits;
    byte _a : 1;
    byte _b : 1;
  public:
    RotaryEncoder(byte pin_clk, byte pin_dt, byte pin_sw = ROTARY_NO_BUTTON, byte num_results=1, RotaryEncoderLimits * limits=0){   //parametro do botao opcional
      _pin_clk = pin_clk;
      _pin_dt = pin_dt;
      _pin_sw = pin_sw;
      pinMode(_pin_clk, INPUT);
      pinMode(_pin_dt, INPUT);
      if (_pin_sw != ROTARY_NO_BUTTON){ 
        pinMode(_pin_sw, INPUT); 
        digitalWrite(_pin_sw, HIGH);
      }
      if (num_results == 0) { num_results = 1; }
      _num_results = num_results;
      _results = new int[_num_results];
      for (int i; i<_num_results; i++){ _results[i] = (limits) ? limits[i].min : 0; }
      _index_result = 0;
      _limits = limits;
      _a = false;
      _b = false;
    }
    byte getIndex() { return _index_result; }
    void next()     { _index_result++; if (_index_result >= _num_results) { _index_result = 0; } } 
    void update_a() {
      _result = 0;
      delay (1);
      if( digitalRead(_pin_clk) != _a ) { 
        _a = !_a;
        if ( _a && !_b ) { _result = -1; }
      }
      if (_results[_index_result]+_result >= _limits[_index_result].min && 
          _results[_index_result]+_result <= _limits[_index_result].max ) {
        _results[_index_result] += _result;
      }
    }
    void update_b() {
      _result = 0;
      delay (1);
      if( digitalRead(_pin_dt) != _b ) {  
        _b = !_b;
        if ( _b && !_a ) { _result = +1; }
      }
      if (_results[_index_result]+_result >= _limits[_index_result].min && 
          _results[_index_result]+_result <= _limits[_index_result].max ) {
        _results[_index_result] += _result;
      }
    }
    int read(){ return _result; }                                        //retorn -1, 0 ou 1.
    int getValue(int index=-1) {                                         //retorna o valor da variável corrente ou a passada como parametro
      if (index < 0 ){ return _results[_index_result]; }
      return _results[index];
    }
    void setValue(int value, int index=-1){ _results[ (index==-1) ? _index_result : index] = value; }         //caso a variável inicializa em determinado valor diferente de zero, utilizar esse método.
    int buttonRead(){ return (_pin_sw == ROTARY_NO_BUTTON) ? LOW : digitalRead(_pin_sw); }
};
/*************************************************************************************************************
************************************FIM CLASSE ROTARY ENCODER*************************************************
*************************************************************************************************************/
     
/*************************************************************************************************************
*******************************CLASSES TETRIS GAME************************************************************
**************************************************************************************************************/
const int TETRIS_TIME_INIT = 700; //tempo entre deslocamento dos inimigos
const int TETRIS_TIME_INC  = 20;  //incremento da velocidade
enum Direction        { DIR_STOP, DIR_TOP, DIR_LEFT, DIR_BOTTOM, DIR_RIGHT};
enum TetrisGameStatus { TETRIS_GAME_ON, TETRIS_GAME_OVER };
enum TetrisBlockTypes { TETRIS_I, TETRIS_J, TETRIS_L, TETRIS_O, TETRIS_S, TETRIS_Z, TETRIS_T, TETRIS_COUNT_BTYPES};
struct Position       { int row, column; };
struct Block          { byte row0:4; byte row1:4; byte row2:4; byte row3:4; };  //4 bit cada linha
//blocos e suas rotacoes
Block block_i[] = { {B1111, B0000, B0000, B0000}, {B0001, B0001, B0001, B0001} };
Block block_j[] = { {B0000, B0010, B0010, B0011}, {B0000, B0000, B0001, B0111}, {B0000, B0110, B0010, B0010}, {B0000, B0000, B0111, B0100} };
Block block_l[] = { {B0000, B0010, B0010, B0110}, {B0000, B0000, B0100, B0111}, {B0000, B0011, B0010, B0010}, {B0000, B0000, B0111, B0001} };
Block block_o[] = { {B0000, B0000, B0110, B0110} };
Block block_s[] = { {B0000, B0001, B0011, B0010}, {B0000, B0000, B0110, B0011} };
Block block_z[] = { {B0000, B0000, B0110, B1100}, {B0000, B1000, B1100, B0100} };
Block block_t[] = { {B0000, B0000, B0010, B0111}, {B0000, B0010, B0011, B0010}, {B0000, B0000, B0111, B0010}, {B0000, B0001, B0011, B0001} };
class TetrisBlock{
  private:
    Block _block;       //bloco que vai descendo. player
    Position _position; //posição do bloco na tela. pode assumir posição negativa
    TetrisBlockTypes _blockType;
    byte _blockTypeRotation;
  public:
    Position getPosition()  { return _position; }
    byte getRow(int row) { 
      if (row == 0 ) return _block.row0;
      if (row == 1 ) return _block.row1;
      if (row == 2 ) return _block.row2;
      if (row == 3 ) return _block.row3; 
      return 0;
    }
    byte getColumn(int col) {
      byte b = 0;
      for (int i=0;i<4;i++){ bitWrite(b, 3-i, bitRead(getRow(i), 3-col) );  }
      return b;
    }
    int read(int row, int column){
      byte b = getRow(row);
      unsigned int bit = column%8;    
      return (b & (1 << bit)) != 0;
    }
    void left()   { _position.column--;  }
    void right()  { _position.column++;  }
    void bottom() { _position.row++;     }
    void top()    { _position.row--;     }
    void generateBlock(byte col) {
      _position.row = 0; 
      _position.column = col; 
      _blockType =  (TetrisBlockTypes) random(0, TETRIS_COUNT_BTYPES);
      _blockTypeRotation = 0;
       
      if ( _blockType == TETRIS_I ){ _block = block_i[_blockTypeRotation]; }
      if ( _blockType == TETRIS_J ){ _block = block_j[_blockTypeRotation]; }
      if ( _blockType == TETRIS_L ){ _block = block_l[_blockTypeRotation]; }
      if ( _blockType == TETRIS_O ){ _block = block_o[_blockTypeRotation]; }
      if ( _blockType == TETRIS_S ){ _block = block_s[_blockTypeRotation]; }
      if ( _blockType == TETRIS_Z ){ _block = block_z[_blockTypeRotation]; }
      if ( _blockType == TETRIS_T ){ _block = block_t[_blockTypeRotation]; }
       
      if (!getRow(0))   { _position.row--;
        if (!getRow(1))   { _position.row--;          
          if (!getRow(2))   { _position.row--; } } }
    }
    void rotate(int direction = 1){
      int maxPos = 0;
      if ( _blockType == TETRIS_I ){ maxPos = sizeof(block_i)/sizeof(Block); }
      if ( _blockType == TETRIS_J ){ maxPos = sizeof(block_j)/sizeof(Block); }
      if ( _blockType == TETRIS_L ){ maxPos = sizeof(block_l)/sizeof(Block); }
      if ( _blockType == TETRIS_O ){ maxPos = sizeof(block_o)/sizeof(Block); }
      if ( _blockType == TETRIS_S ){ maxPos = sizeof(block_s)/sizeof(Block); }
      if ( _blockType == TETRIS_Z ){ maxPos = sizeof(block_z)/sizeof(Block); }
      if ( _blockType == TETRIS_T ){ maxPos = sizeof(block_t)/sizeof(Block); }
      (direction==1) ? _blockTypeRotation++ : _blockTypeRotation--;
      if (_blockTypeRotation == 255)    { _blockTypeRotation = maxPos-1; }
      if (_blockTypeRotation >= maxPos) { _blockTypeRotation = 0; }
      if ( _blockType == TETRIS_I ){ _block = block_i[_blockTypeRotation]; }
      if ( _blockType == TETRIS_J ){ _block = block_j[_blockTypeRotation]; }
      if ( _blockType == TETRIS_L ){ _block = block_l[_blockTypeRotation]; }
      if ( _blockType == TETRIS_O ){ _block = block_o[_blockTypeRotation]; }
      if ( _blockType == TETRIS_S ){ _block = block_s[_blockTypeRotation]; }
      if ( _blockType == TETRIS_Z ){ _block = block_z[_blockTypeRotation]; }
      if ( _blockType == TETRIS_T ){ _block = block_t[_blockTypeRotation]; }
    }
};
   
class TetrisGame {
  private:
    BitArray2D * _display;   //display com todos os objetos da tela. recebido como parâmetro
    BitArray2D * _recipient; //recepiente onde os blocos vão se acumulando. criado internamente pela classe
    TetrisBlock * _block;
    Direction _direction;
    TetrisGameStatus _tetrisGameStatus;
    unsigned long _last_millis;
    unsigned long _last_millis_player;
    int _time;
    int _score;
    byte _fast : 1;          //acelerar a descida
    UniqueRandom * _ur;      //utilizado no game over
    void _gameOver(){ 
      _tetrisGameStatus = TETRIS_GAME_OVER; 
      _direction   = DIR_STOP;
      _time = 20;
    }
    void _inc_speed(){
      _score++;
      _time -= TETRIS_TIME_INC;
    }
    void _start(){
      _recipient->clear();
      _last_millis = 0;
      _score = 0;
      _time = TETRIS_TIME_INIT;
      _fast = 0;
      _direction = DIR_STOP;
      _tetrisGameStatus = TETRIS_GAME_ON; 
      _newBlock();
    }
    void _newBlock() {
      _block->generateBlock( _display->columns() / 2 - 1 );
      if (_testCollision()){ _gameOver(); }
    }
    void _updateDisplay() {
      if (_tetrisGameStatus == TETRIS_GAME_ON) { _display->clear();  }
      for (int r=0; r<4; r++) {
        for (int c=0; c<4; c++) { 
          if ( _block->read(r, c) ) { 
            _display->write(_block->getPosition().row+r, _block->getPosition().column+c,  HIGH ); 
          }
        }
      }
      for (int row=0; row<_display->rows(); row++) {
        for (int col=0; col<_display->columns(); col++) {
          boolean val = _recipient->read(row, col);
          if (val) { _display->write(row, col,  val ); }
        }
      }
    }
    boolean _testCollision(){
      int col = _block->getPosition().column;  //coluna do bloco em relacao ao recepiente. pode ser negativa
      int row = _block->getPosition().row;     //linha do bloco em relacao ao recepiente. pode ser negativa
      for (byte c=0; c<4; c++){
        for (byte r=0; r<4; r++){
          if (! _block->read(r, c) )            { continue;  }  
          if ( row+r < 0 )                      {return true;}  
          if ( row+r >= _display->rows() )      {return true;}
          if ( col+c < 0 )                      {return true;}
          if ( col+c >= _display->columns() )   {return true;}
          if ( _recipient->read(row+r, col+c) ) {return true;}
        }
      }
      return false;
    }
    boolean _canLeft(){
      _block->left();
      boolean r = ! _testCollision();
      _block->right();
      return r;
    }
    boolean _canRight(){
      _block->right();
      boolean r = ! _testCollision();
      _block->left();
      return r;
    }
    boolean _canBottom(){
      _block->bottom();
      boolean r = ! _testCollision();
      _block->top();
      return r;
    }
    boolean _canRotate(){
      _block->rotate();
      boolean r = ! _testCollision();
      _block->rotate(-1);
      return r;
    }
    void _blockToRecipient(){
      for (byte c=0; c<4; c++){
        for (byte r=0; r<4; r++){
          if ( _block->read(r, c) ){ _recipient->write(_block->getPosition().row+r, _block->getPosition().column+c, HIGH); }
        }
      }
      _verifyCompleteRow();
      _newBlock();
    }
    void _verifyCompleteRow(){
      for (int row=0; row<_recipient->rows(); row++) {
        boolean complete = true;
        for (int col=0; col<_recipient->columns(); col++) {
          if (! _recipient->read(row, col) ) { complete = false; break; }
        }
        if (complete){ 
          _incScore(); 
          //move as linhas pra baixo
          for (int row2 = row; row2>=0; row2--) {
            for (int col2=0; col2<_recipient->columns(); col2++){
              _recipient->write(row2, col2,  (row2>0) ? _recipient->read(row2-1, col2) : 0 );
            }
          }
        }
      }
    }
    void _incScore(){
      _score++;
      _time -= TETRIS_TIME_INC;
    }
    void _runGameOver(){
      int r   = _ur->next();
      int row = (r / _display->columns());
      int col = (r % _display->columns());
      _display->write(row, col, HIGH );
      if ( _ur->getIndex() == (_ur->size()-1) ) {
        _ur->randomize();
        _ur->first();
        _start(); 
      }
    }
    void _runPlayer() {
      if (_direction == DIR_LEFT  && _canLeft()  ) { _block->left();  }
      if (_direction == DIR_RIGHT && _canRight() ) { _block->right(); }
      _direction = DIR_STOP;
    }
    void _runBlock() {
      _canBottom() ? _block->bottom() : _blockToRecipient() ;
      _direction = DIR_STOP; 
    }
  public:
    TetrisGame(BitArray2D * display){ 
      _display   = display;
      _recipient = new BitArray2D( _display->rows(), _display->columns() );
      _ur        = new UniqueRandom( _display->rows() * _display->columns() );
      _block     = new TetrisBlock();
      _start();
    }
    void fast()   { _fast = 1; }  //acelerar o deslocamento
    void noFast() { _fast = 0; }  //descida do bloco na velocidade normal
    void left()   { _direction = DIR_LEFT;   }
    void right()  { _direction = DIR_RIGHT;  }
    void bottom() { _direction = DIR_BOTTOM; }
    void rotate() { if (_canRotate()) { _block->rotate(); } }
    int getScore(){ return _score; }
    Position getPosition() { return _block->getPosition(); }
    int update(){
      int r = false;
      if (millis() - _last_millis > ( (_fast==1) ? 50 : _time ) ) { 
        r = true;
        _last_millis = millis();  
        if (_tetrisGameStatus == TETRIS_GAME_ON)   { _runBlock(); } 
        if (_tetrisGameStatus == TETRIS_GAME_OVER) { _runGameOver(); }
      }
      if (millis() - _last_millis_player > 70 ) {  //atualiza os movimentos do player a cada 70ms
        r = true;
        _last_millis_player = millis();  
        if (_tetrisGameStatus == TETRIS_GAME_ON)   { _runPlayer(); } 
      }
      if (r) { _updateDisplay(); }
      return r; //r-->indica se houve mudança no display
    }
};
/*************************************************************************************************************
*******************************FIM CLASSES TETRIS GAME********************************************************
**************************************************************************************************************/
   
/*************************************************************************************************************
*******************************DISPLAY************************************************************************
**************************************************************************************************************/
   
/*
rotation indica qual parte do display estará para cima
   
         TOP
L  . . . . . . . . R
E  . . . . . . . . I
F  . . . . . . . . G
T  . . . . . . . . H
   . . . . . . . . T
   . . . . . . . .
   . . . . . . . .
   . . . . . . . .   
       BOTTOM
       
*/
enum Rotation {TOP, LEFT, BOTTOM, RIGHT};
   
struct Display {
  int index;
  Position position; 
  Rotation rotation;
};
   
/*************************************************************************************************************
*******************************FIM DISPLAY********************************************************************
**************************************************************************************************************/

/*************************************************************************************************************
*******************************CLASSE GAME LCD****************************************************************
**************************************************************************************************************/
/*
  0        1        2        3        4        5        6
 ***                        ***      ***               ***                 
 ***                        ***      ***               ***          
                                                               
 ***      ***               ***               ***             
 ***      ***               ***               ***             
                                                                 
 ***      ***      ***               ***                        
 ***      ***      ***               ***                            
*/
byte c0[8] = {B11111,  B11111,  B00000,  B11111,  B11111,  B00000,  B11111,  B11111 };
byte c1[8] = {B00000,  B00000,  B00000,  B11111,  B11111,  B00000,  B11111,  B11111 };
byte c2[8] = {B00000,  B00000,  B00000,  B00000,  B00000,  B00000,  B11111,  B11111 };
byte c3[8] = {B11111,  B11111,  B00000,  B11111,  B11111,  B00000,  B00000,  B00000 };
byte c4[8] = {B11111,  B11111,  B00000,  B00000,  B00000,  B00000,  B11111,  B11111 };
byte c5[8] = {B00000,  B00000,  B00000,  B11111,  B11111,  B00000,  B00000,  B00000 };
byte c6[8] = {B11111,  B11111,  B00000,  B00000,  B00000,  B00000,  B00000,  B00000 };
class GameLCD {
  private:
    LiquidCrystal * _lcd;  //ponteiro para um objeto lcd
  public:
    void createChars() { 
      _lcd->createChar(0, c0);  
      _lcd->createChar(1, c1);  
      _lcd->createChar(2, c2); 
      _lcd->createChar(3, c3);  
      _lcd->createChar(4, c4);
      _lcd->createChar(5, c5);
      _lcd->createChar(6, c6);
    }
    GameLCD(LiquidCrystal * lcd) {  _lcd = lcd;  }
    void write(byte col, byte row, byte val){
      _lcd->setCursor(col, row);
      if (val == B000) { _lcd->print(" ");  }
      if (val == B111) { _lcd->write((byte)0);  }
      if (val == B011) { _lcd->write((byte)1);  }
      if (val == B001) { _lcd->write((byte)2);  }
      if (val == B110) { _lcd->write((byte)3);  }
      if (val == B101) { _lcd->write((byte)4);  }
      if (val == B010) { _lcd->write((byte)5);  }
      if (val == B100) { _lcd->write((byte)6);  }
    }
};
/*************************************************************************************************************
*******************************FIM CLASSE GAME LCD***********************************************************
**************************************************************************************************************/
   
/*************************************************************************************************************
*******************************DECLARACAO DOS OBJETOS*********************************************************
**************************************************************************************************************/
/*
 tamanho display real
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * *
 * * * * * * * * * * * * * * * * 
*/
     
const int LINHAS  = 16;
const int COLUNAS = 6;

LiquidCrystal lcd(12, 11, 10, 9, 8, 7);
GameLCD gameLcd(&lcd);
      
BitArray2D ba(LINHAS, COLUNAS); //8 linhas e 16 colunas, usada para armazenar o estado do display
TetrisGame tetris(&ba);
     
RotaryEncoderLimits lim[] = { {-1000,1000} };  //limites máximos e mínimos que as variaveis podem atingir
RotaryEncoder       re(A0, A1, 4, 1, lim);  //pino clk, pino dt, pino sw, qtd variaveis, limites

/*************************************************************************************************************
*******************************FIM DECLARACAO DOS OBJETOS*****************************************************
**************************************************************************************************************/
       
/*************************************************************************************************************
*******************************TRATAMENTO DAS INTERRUPÇÕES****************************************************
**************************************************************************************************************/
//interrupções dos pinos A0 e A1 via Pin Change Interrupt
ISR(PCINT1_vect) {
  volatile static byte lastVal_a0 = LOW;
  volatile static byte lastVal_a1 = LOW;
  byte val_a0 = digitalRead(A0);
  byte val_a1 = digitalRead(A1);
  if (lastVal_a0 != val_a0){ re.update_a(); lastVal_a0 = val_a0; }
  if (lastVal_a1 != val_a1){ re.update_b(); lastVal_a1 = val_a1; }
}
      
void setup_interrupts(){
  //-----PCI - Pin Change Interrupt ----
  pinMode(A0,INPUT);   // set Pin as Input (default)
  digitalWrite(A0,HIGH);  // enable pullup resistor
  pinMode(A1,INPUT);   // set Pin as Input (default)
  digitalWrite(A1,HIGH);  // enable pullup resistor
  cli();
  PCICR |= 0b00000010; // habilita a porta C - Pin Change Interrupts
  PCMSK1 |= 0b00000011; // habilita interrupção da porta c nos pinos: PCINT8 (A0) e PCINT9(A1)
  sei();
}
/*************************************************************************************************************
*******************************FIM TRATAMENTO DAS INTERRUPÇÕES************************************************
**************************************************************************************************************/

void update_display() {
  for (int lin=ba.rows()-1; lin>=0; lin--) {
    byte lin_lcd = 0;
    byte cont = 0;
    byte val = 0;
    
    for (int col=0; col<ba.columns(); col++) {
      if (cont == 0){ val = 0; }
      val = val << 1; 
      val = val | ba.read(lin, col);
      cont++;
      if (cont == 3) {
        gameLcd.write(LINHAS-lin-1, lin_lcd, val) ;
        cont = 0;
        lin_lcd++;
      }
    }
  }
}

   
void setup() { 
  setup_interrupts();
  lcd.begin(16, 2);
  gameLcd.createChars();
  randomSeed(analogRead(A2));
  re.setValue(0, 0);  //inicializa o rotary em 0
}
        
void loop() {
  static int val_encoder = 0;
  static boolean change = false;
   
  if (re.getValue(0) < val_encoder && !change){ tetris.left();  change = true; }  //sentido anti-horario move para esquerda
  if (re.getValue(0) > val_encoder && !change){ tetris.right(); change = true; }  //sentido horario move para direita
     
  val_encoder = re.getValue(0);
   
  if ( tetris.update() ) { 
    change = false;
    update_display(); 
  }  
       
  //controla o click do botao do enconder
  //clique curto --> rotate
  //clique longo --> fast
  static byte lastb = HIGH; //leitura anterior do botão
  static unsigned long m_pressed = 0;  //millis na borda de subida do botão
   
  int b = re.buttonRead();
  if( !b && lastb ) {    //borda de subida
    m_pressed = millis();
    delay(70);
  }
  if( b && !lastb ) {    //borda de descida
    if (millis() - m_pressed < 350) {
      tetris.rotate();
      m_pressed = millis();
    }
  } 
  if( !b &&  millis()-m_pressed > 350){
    tetris.fast(); 
  } else {
    tetris.noFast(); 
  }
  lastb = b;
}