4
\$\begingroup\$

I am trying to make a custom tv remote using an ATmeta328p microcontroller. It contains 28 buttons which are wired in a matrix configuration, resulting in 5 outputs (columns) and 6 inputs (rows). Before ordering a PCB, I want to make sure electrical the circuit which I came up with is going to work, so I have made a similar circuit with only 4 buttons (2 rows and 2 columns) on a piece of perf board to which I can test with an Arduino Uno.

Now I have written the code for it, but the program gives an incorrect correct output when do not I serial print a message at some line which I have used for debugging. If I do not print at a specific line, two buttons on the same row will sometimes not be detected correctly when I press them simultaneously. I have tried to put a delay on the line instead of serial print, but this does not help.

I have put my code below. The message which I need to print for the program to work correctly is on line 35 (Serial.print("\ntest");)

#include <stdint.h>
#define NR_OF_COLS 2
#define NR_OF_ROWS 2

int main() {
  init();
  Serial.begin(9600);
  for (uint8_t i = 0; i < NR_OF_COLS; i++) {
    //make pins 8 - 8+i low (cols)
    PORTB &= ~(1 << i);
    //make pins 8 - 8+i outputs (cols)
    DDRB |= (1 << i);
  }
  for (uint8_t i = 0; i < NR_OF_ROWS; i++) {
    //make pins A0 - A(0+i) = digital inputs (rows)
    DDRC &= ~(1 << i);
  }
  //make pin 2 input (interrupt)
  DDRD &= ~0x4; 

  while(1) {
    //if interrupt pin became low
    if (!(PIND & 4)) {
      uint8_t btnArray[NR_OF_COLS] = {0};
      uint8_t rows = 0;
      
      //the row pins need some time to change after the interrupt has been triggered
      delay(1);
      
      for (uint8_t i = 0; i < NR_OF_ROWS; i++) {
        //read rows, store inverted bits (rows are active low)
        rows |= (PINC & (1 << i)) ^ (1 << i);
      }
      
      Serial.print("\ntest");
      
      for (uint8_t i = 0; i < NR_OF_COLS; i++) {
        //set col pin i
        PORTB |= 1 << i; 
        //the row pins need some time to change based on thew new value of the col pins
        delay(1);        
        
        uint8_t newRows = 0;
        for (uint8_t j = 0; j < NR_OF_ROWS; j++) {
          //read rows again
          newRows |= (PINC & (1 << j)) ^ (1 << j);
        }
        
        for(uint8_t j = 0; j < NR_OF_ROWS; j++) {
          //check if row pins have changed based on thew new values of the col pins
          if (rows & (1 << j) && !(newRows & (1 << j))) {
            //store row values in col i
            btnArray[i] |= (1 << j);
          }
        }

        //clear col pin i
        PORTB &= ~(1 << i); 
      }

      //print debug information
      Serial.print("\nButtons:\n");
      for (int i = 0; i < NR_OF_ROWS; i++) {
        for (int j = 0; j < NR_OF_COLS; j++) {
          Serial.print((btnArray[j] & (1 << i)) != 0);
          Serial.print(' ');
        }
        Serial.print('\n');
      }
      delay(1000);
    }
  }
}

When a button is pressed, the corresponding row becomes low because all column pins are set to low (acting as a GND). If this happens, the row state is stored, and then every column pin will be set to high one by one. If then a specific column pin makes a specific row pin become high again, I should know the row/column position of the button which was pressed.

When I press for example the two buttons on row 0 simultaneously with the printing message in between, I see this on the serial monitor:

test
Buttons:
0 0 
1 1 

And when I remove the message, I get this on the serial monitor:

Buttons:
0 0 
0 0 

I am not sure if this problem arises because of the hardware or the software, so I have also put the schematic of the circuit below:

schematic

simulate this circuit – Schematic created using CircuitLab

Can someone see what I'm doing wrong?

\$\endgroup\$
4
  • 1
    \$\begingroup\$ a serial print of "test\n" @9600 bps would take about 5ms if that function call is blocking, perhaps replace the print with a delay(5) to make sure your GPIO circuit isnt magically dependant on the serial port? if that succeeds I'd guess that switch bounce could be causing you troubles, though then your output would be more random and not always 0. Also you mention interrupt a few times, but I see no interrupt service routine - is there one? if there is and you're sharing global variables between the ISR and the main routine be sure to declare them as volatile <type> varname; \$\endgroup\$
    – Bryan
    May 13, 2022 at 22:16
  • \$\begingroup\$ I have tried to replace the serial print with a delay, but it did not help. I have tried delay(1) and delay(1000). I do not expect delay(5) will give a different result, but I can try it. The output without the serial print is indeed a bit more random. It is not always 0, but I only showed 0 in this example. There is no interrupt service routine yet, but I'm planning to implement one later when I'm sure this is working correctly. I have called it interrupt because it is the pin which will trigger the isr. \$\endgroup\$
    – Roy Meijer
    May 13, 2022 at 22:45
  • 2
    \$\begingroup\$ replacing Arduino main() removes Arduino initialization. it is possible delay() doesn't work without it. implement Arduino setup() and loop() and let core do main() \$\endgroup\$
    – Juraj
    May 14, 2022 at 5:18
  • 1
    \$\begingroup\$ I have already called init() at the start of the main so everything should work correctly. I am sure delay works, because the delay(1000) at the end does stop the program for 1 second. \$\endgroup\$
    – Roy Meijer
    May 15, 2022 at 14:13

1 Answer 1

2
\$\begingroup\$

If you press both buttons in a row at the same time, your scanning method by switching the COL outputs creates a kind of soft short circuit. While one COL is high and the other becomes low at ROW0 you have VCC/2 (+ a little from R3 and R5). It's at random what the MCU would read there (no, not random, but hard to predict).

Try to set PORTB to all bits low and scan with the DDRB to switch between output(active low) and input(inactive open drain). This way you avoid ambigous analog levels on the ROW lines and wasted 5mA cross current.

By the way, you don't need R3 and R4 if you activate the internal pullup's of the ROW input port (PORTC-bits high).

Reduce R5 to 10k or lower or you may receive false int low on EMI spikes.

A good solution is using a timer interrupt, read the rows there and and change the active COL for the next int cycle. If you have done all COLs, you can calculate the pressed buttons and set a (volatile) bool flag to inform your main loop. In this polling mode you don't need the int input and you don't waste cpu time on delay(). However, pressing more than one button in such a matrix may produce additional buttons looking pressed, it's the price you pay using a matrix. Just discard results with more than one button pressed.

Some years ago I did the same with an atTiny24 using sleep mode to run it from a coin cell. Well, this is Pascal code with assembly sections and the comments are German, but you may extract some useful ideas out of it.

//-------------------------------------------------------------------------
//      IR-Fernbedienung
//-------------------------------------------------------------------------
//      Copyright (c) 2015 E42 Elektronik
//      Version 1, 25.07.15.JH
//-------------------------------------------------------------------------
//      MPU: ATTiny24
//      Compiler: ELab, Version 4
//-------------------------------------------------------------------------
//      Kommentare:
//      - Der Prozessor arbeitet mit internem Takt von 8MHz und
//        ClkDiv8-Option per Fuses. Daher effektiv 1MHz Systemtakt beim
//        Booten.
//-------------------------------------------------------------------------
//      Aenderungen:
//-------------------------------------------------------------------------

program IRControl;

{$TYPEDCONST OFF}
{$NOSHADOW}

Device = Tiny24, VCC=3;

Define_Fuses
   // Konfiguration fuer E-Lab Programmer
   OverRide_Fuses;
   Supply = 3.0, 30;  // 3.@30mA
   LockBits0 = [LOCKBIT1,LOCKBIT2];
   FuseBits0 = [CKSEL3,CKSEL2,CKSEL0,SUT0,CKDIV8];
   FuseBits1 = [BODLEVEL1];  //2.7V BOD
   ProgMode  = SPI;
   ProgFuses = True;
   ProgLock  = True;
   ProgFlash = True;

Import;              // Keine Erweiterungsmodule

From System Import ; // Keine weiteren Laufzeitbibliotheken

Define
   ProcClock  = 1000000;       {Hertz}
   StackSize  = $001C, iData;  // Schachtelungstiefe von Aufrufen
   FrameSize  = $0018, iData;  // Platz fuer lokale Variablen

///////////////////////////////////////////////////////////////////////////
IMPLEMENTATION
///////////////////////////////////////////////////////////////////////////

const
   IR_TX_BIT       =  0;
   IRBurstLenght   = 11;

   cMinButtonNo    =  1;
   cMaxButtonNo    = 21;
   
   cBurdaIdent     = $F1;
   cKeyF1          = $49;  // Button 4
   cKeyF2          = $48;  // Button 5
   cKeyF3          = $56;  // Button 6
   cKeyReset       = $16;  // Button 21
   cKeyPreflushInc = $42;  // Button 13
   cKeyPreflushDec = $41;  // Button 15
   cKeyFlushInc    = $45;  // Button 16
   cKeyFlushDec    = $44;  // Button 18
   cKeyRangeInc    = $58;  // Button 7
   cKeyRangeDec    = $18;  // Button 9
   cKeyFanInc      = $59;
   cKeyFanDec      = $4D;
   cKeyFlush       = $1D;  // Button 16
   cKeyFlushStop   = $1E;  // Button 17
   cKeyNone        = $00;

   cKeyTable : array [1..21] of byte =
      (cKeyNone, cKeyNone, cKeyNone,
       cKeyF1, cKeyF2, cKeyF3,
       cKeyRangeInc, cKeyNone, cKeyRangeDec,
       cKeyPreflushInc, cKeyNone, cKeyPreflushDec,
       cKeyFlushInc, cKeyNone, cKeyFlushDec,
       cKeyFlush, cKeyFlushStop, cKeyNone,
       cKeyNone, cKeyNone, cKeyReset);

(*
   Port-Layout

   PA0: 13    N/A (URef)
   PA1: 12    Matrix pin 3
   PA2: 11    Matrix pin 4
   PA3: 10    Matrix pin 5
   PA4: 09    Matrix pin 6
   PA5: 08    Matrix pin 7
   PA6: 07    Matrix pin 8
   PA7: 06    Matrix pin 11

   PB0: 02    out, IR-LED
   PB1: 03    out, Visible LED
   PB2: 05    Matrix pin 12
*)

(*
 
   Button Matrix nach Tiny24-Pins
    |   A1 A2 A3 A4 A5 A6 A7 B2
----------------------------------
 A1 |   .  /  /  R- T- S- /  RS
 A2 |   .  .  /  R+ T+ S+ /  /
 A3 |   .  .  .  /  /  /  /  /
 A4 |   9  7  8  .  /  /  F1 /
 A5 |   12 10 11 .  .  /  F2 W+
 A6 |   15 13 14 .  .  .  F3 W-
 A7 |   3  2  1  4  5  6  .  /
 B2 |   21 19 20 18 16 17 .  .
*)


{$PDATA}
var
   Q_IRTransmit  [@PortB, 0] : bit;  // PB0: IR-TX
   E_IRTransmit  [@DDRB,  0] : bit;
   Q_DiagLed     [@PortB, 1] : bit;  // PB1: visible LED
   E_DiagLed     [@DDRB,  1] : bit;
//   Q_DiagLed     [@PortA, 0] : bit;  // PB1: visible LED
//   E_DiagLed     [@DDRA,  0] : bit;
   Q_Row0        [@PortB, 2] : bit;  // PB2: Matrix Row 0
   E_Row0        [@DDRB,  2] : bit;

{$IDATA}
var
   LastButton    : byte;
   BurstClocks   : byte;
   ClockCounter  : word;
   TxCode        : byte;

procedure Troedel;
begin
end;

function ReadRow (BitMask: byte): byte;
var
   A: byte;
begin
   DDRA  := BitMask;
   PortA := not BitMask;
   Troedel;
   Troedel;
   A := PinA;
   DDRA  := $00;
   PortA := $FF;
   Return (A);
end;

function GetActiveButton: byte;
var
   A: byte;
begin
   Q_Row0 := True;
   E_Row0 := False;
   DDRA  := $00;
   PortA := $FF;
   
   Q_Row0 := False;  // row B2 to 0
   E_Row0 := True;
   Troedel;
   Troedel;
   A := PinA;
   Q_Row0 := True;
   E_Row0 := False;
   if (A and $02) = 0 then Return(21); endif;
   if (A and $04) = 0 then Return(19); endif;
   if (A and $08) = 0 then Return(20); endif;
   if (A and $10) = 0 then Return(18); endif;
   if (A and $20) = 0 then Return(16); endif;
   if (A and $40) = 0 then Return(17); endif;

   A := ReadRow ($80); // row A7 to 0
   if (A and $02) = 0 then Return(3); endif;
   if (A and $04) = 0 then Return(2); endif;
   if (A and $08) = 0 then Return(1); endif;
   if (A and $10) = 0 then Return(4); endif;
   if (A and $20) = 0 then Return(5); endif;
   if (A and $40) = 0 then Return(6); endif;

   A := ReadRow ($40); // row A6 to 0
   if (A and $02) = 0 then Return(15); endif;
   if (A and $04) = 0 then Return(13); endif;
   if (A and $08) = 0 then Return(14); endif;

   A := ReadRow ($20); // row A5 to 0
   if (A and $02) = 0 then Return(12); endif;
   if (A and $04) = 0 then Return(10); endif;
   if (A and $08) = 0 then Return(11); endif;

   A := ReadRow ($10); // row A4 to 0
   if (A and $02) = 0 then Return(9); endif;
   if (A and $04) = 0 then Return(7); endif;
   if (A and $08) = 0 then Return(8); endif;
   Return (0);
end;

// IR-Led Tastverhaeltnis An zu Aus
procedure TroedelOn;
begin
asm;
        PUSH    _ACCA
        LDI     _ACCA, 18
ton_loop:
        DEC     _ACCA
        BRNE    ton_loop
        NOP
        POP     _ACCA
endasm;
end;

procedure TroedelOff;
begin
asm;
        PUSH    _ACCA
        LDI     _ACCA, 40
toff_loop:
        DEC     _ACCA
        BRNE    toff_loop
        POP     _ACCA
endasm;
end;

// 38kHz Burst
procedure OnPeriod;
begin
asm;
        LDS     _ACCCLO, IRControl.ClockCounter
        LDS     _ACCCHI, IRControl.ClockCounter+1
        LDS     _ACCB, IRControl.BurstClocks
        CLR     _ACCA
        ADD     _ACCCLO, _ACCB
        ADC     _ACCCHI, _ACCA
        STS     IRControl.ClockCounter, _ACCCLO
        STS     IRControl.ClockCounter+1, _ACCCHI
irb_loop1_on:
        LDI     _ACCA, IRControl.IRBurstLenght
irb_loop2_on:
        CBI     PortB, IRControl.IR_TX_BIT
        RCALL   IRControl.TroedelOn
        SBI     PortB, IRControl.IR_TX_BIT
        RCALL   IRControl.TroedelOff
        DEC     _ACCA
        BRNE    irb_loop2_on
        DEC     _ACCB
        BRNE    irb_loop1_on
endasm;
end;

procedure OffPeriod;
begin
asm;
        LDS     _ACCCLO, IRControl.ClockCounter
        LDS     _ACCCHI, IRControl.ClockCounter+1
        LDS     _ACCB, IRControl.BurstClocks
        CLR     _ACCA
        ADD     _ACCCLO, _ACCB
        ADC     _ACCCHI, _ACCA
        STS     IRControl.ClockCounter, _ACCCLO
        STS     IRControl.ClockCounter+1, _ACCCHI
irb_loop1_off:
        LDI     _ACCA, IRControl.IRBurstLenght
irb_loop2_off:
        SBI     PortB, IRControl.IR_TX_BIT
        RCALL   IRControl.TroedelOn
        SBI     PortB, IRControl.IR_TX_BIT
        RCALL   IRControl.TroedelOff
        DEC     _ACCA
        BRNE    irb_loop2_off
        DEC     _ACCB
        BRNE    irb_loop1_off
endasm;
end;

procedure AddByte;
begin
asm;
        LDI     _ACCALO, 8    ; 8 bits
        LDS     _ACCAHI, IRControl.TxCode
ab_loop:
        LDI     _ACCA, 2      ; on time
        STS     IRControl.BurstClocks, _ACCA
        RCALL   IRControl.OnPeriod
        ROR     _ACCAHI
        BRCS    ab_odd
        LDI     _ACCA, 2      ; short pause for zero
        RJMP    ab_put
ab_odd:
        LDI     _ACCA, 6      ; long pause for one
ab_put:
        STS     IRControl.BurstClocks, _ACCA
        RCALL   IRControl.OffPeriod
        DEC     _ACCALO
        BRNE    ab_loop
endasm;
end;

procedure SendButtonCode (KeyCode: byte; IsRepeated: boolean);
begin
   Q_IRTransmit := True;
   E_IRTransmit := True;
   ClockCounter := 0;
   
   DisableInts;
   // SynPulse
   BurstClocks := 31;
   OnPeriod;
   if IsRepeated then
      BurstClocks := 8;
      OffPeriod;
      BurstClocks := 2;
      OnPeriod;
      BurstClocks := 12;
      OffPeriod;
   else
      BurstClocks := 16;
      OffPeriod;
   endif;
   // Data
   TxCode := cBurdaIdent;
   AddByte;
   TxCode := cBurdaIdent xor $FF;
   AddByte;
   TxCode := KeyCode;
   AddByte;
   TxCode := KeyCode xor $FF;
   AddByte;
   // EOT
   BurstClocks := 2;
   OnPeriod;
   
   Q_IRTransmit := True;
   E_IRTransmit := False;
   EnableInts;
   
   // Gap
   while ClockCounter < 372 do
      BurstClocks := 1;
      OffPeriod;
   endwhile;
end;

//--------------------------------------------------------------------

// hier passiert nichts, ausser dass die Hauptschleife danach unten weiterlaeuft
interrupt WatchDogOvr;
begin
end;

procedure InitPorts;
begin
   PRR := $0F;      // Timer1 off, Timer0 off, SIO off, ADC off
   LastButton := 0;
   Q_DiagLed := True;
   E_DiagLed := True;
end;

// der Watchdog ist das primaere Zeitmesssystem, alle 64ms weckt er den
// Prozessor auf um eine neue Messung vorzunehmen und zu bewerten.
// clear WDIF und WDE, enable WDIE,
// C4 = 0.25s int rate
// C3 = 0.125s int rate
// C2 = 64ms int rate
procedure InitWDogInt;
begin
asm;
        CLI
        WDR
        IN      _ACCA, WDTCR
        ORI     _ACCA, 018h
        OUT     WDTCR, _ACCA
        LDI     _ACCA, 0C2h
        OUT     WDTCR, _ACCA
        SEI
endasm;
end;

procedure EnterIdleSpeed;
begin
   CLKPR    := $80;    // enable clock divider change
//   CLKPR    := $04;    // set System Clock [4=500kHz, 5=250kHz]
   CLKPR    := $00;    // set fast System Clock [0=8MHz]
end;

// Takt auf 8MHz hochschalten
procedure EnterFullSpeed;
begin
   CLKPR    := $80;    // enable clock divider change
   CLKPR    := $00;    // set fast System Clock [0=8MHz]
end;

// laeuft mit langsamem Takt
procedure IdleHandler;
var
   B: byte;
   KeyCode: byte;
begin
   Q_DiagLed := False;
   repeat
      B := GetActiveButton;
      Keycode := cKeyNone;
      if (B >= cMinButtonNo) and (B <= cMaxButtonNo) then
         KeyCode := cKeyTable[B];
         if Keycode <> cKeyNone then
            EnterFullSpeed;
            SendButtonCode (KeyCode, LastButton = B);
            EnterIdleSpeed;
         endif;
      endif;
      LastButton := B;
   until (B = 0) or (Keycode = cKeyNone);
   Q_DiagLed := True;

   // disable BOD-detect during sleep
   MCUCR := $84;
   MCUCR := $80;
   // nun CPU abschalten, bis sie vom Hund geweckt wird..
   MCUCR := $30;       // power down enabled
asm;
        SLEEP
endasm;
end;

begin
   InitPorts;          // Definition Eingaenge/Ausgaene
   EnterIdleSpeed;     // Auf niedrigen CPU-Stromverbrauch trimmen
   MDelay (100);       // Warten bis Versorgungsspannung stabil ist
   InitWDogInt;        // Wachhund als Zeitbasis einrichten
   EnableInts;         // Zeitbasis erlauben
   loop
      IdleHandler;
   endloop;
end IRControl.


```
\$\endgroup\$
1
  • \$\begingroup\$ Thank you, this is very helpful \$\endgroup\$
    – Roy Meijer
    May 17, 2022 at 20:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.