pull up resistor:

since current is 0, voltage will be exactly what is at vcc.
pin outputs:

basic logic is to do matrix scanning:

since 11 is output, those leds are grounded by default.
since 12 is input, no current is flowing through it (therefore leds) are off.
code:
void setup() {
pinMode(12, OUTPUT);
pinMode(11, INPUT);
pinMode(14, OUTPUT);
pinMode(15, OUTPUT);
}
void loop() {
digitalWrite(14, HIGH);
digitalWrite(15, HIGH);
pinMode(12, OUTPUT);
pinMode(11, INPUT);
delay(100);
digitalWrite(15, HIGH);
digitalWrite(14, LOW);
pinMode(12, INPUT);
pinMode(11, OUTPUT);
delay(100);
}
therefore, enabling a cathode allows you access to that layer’s leds:

more complex:
int cathodes[2] = { 12, 11 };
int anodes[4] = { 14, 15, 10, 9 };
int del = 100;
void setup() {
for (int i = 0; i < 4; i++) {
pinMode(anodes[i], OUTPUT);
}
}
void loop() {
// digitalWrite(anodes[0], HIGH);
// digitalWrite(anodes[1], HIGH);
// digitalWrite(anodes[2], HIGH);
// pinMode(cathodes[0], OUTPUT);
// pinMode(cathodes[1], INPUT);
// delay(500);
// reset();
// digitalWrite(anodes[2], HIGH);
// pinMode(cathodes[1], OUTPUT);
// pinMode(cathodes[0], INPUT);
// delay(500);
digitalWrite(anodes[0], HIGH);
pinMode(cathodes[0], INPUT);
pinMode(cathodes[1], OUTPUT);
delay(del);
reset();
digitalWrite(anodes[1], HIGH);
digitalWrite(anodes[3], HIGH);
pinMode(cathodes[1], INPUT);
pinMode(cathodes[0], OUTPUT);
delay(del);
reset();
digitalWrite(anodes[2], HIGH);
pinMode(cathodes[0], INPUT);
pinMode(cathodes[1], OUTPUT);
delay(del);
reset();
digitalWrite(anodes[2], HIGH);
digitalWrite(anodes[0], HIGH);
pinMode(cathodes[1], INPUT);
pinMode(cathodes[0], OUTPUT);
delay (del);
reset();
}
void light_up_top(int arr){
Serial.println(arr);
}
//helper to reset:
void reset() {
for (int i = 0; i < 4; i++) {
digitalWrite(anodes[i], LOW);
}
}const int cathodes[] = { 12, 11 };
const int anodes[] = { 14, 15, 10, 9 };
const int num_cathodes = sizeof(cathodes) / sizeof(cathodes[0]);
const int num_anodes = sizeof(anodes) / sizeof(anodes[0]);
// cathode row indices
const int top = 0;
const int bottom = 1;
int del = 100;
void setup() {
for (int i = 0; i < num_anodes; i++) {
pinMode(anodes[i], OUTPUT);
}
reset();
}
void loop() {
int top_pins[] = { anodes[0] };
light_up(top_pins, 1, top);
int bottom_pins[] = { anodes[2] };
light_up(bottom_pins, 1, bottom);
}
// helpers:
// light any number of anodes against one cathode row
void light_up(const int pins[], int count, int row) {
select_row(row);
for (int i = 0; i < count; i++) {
digitalWrite(pins[i], HIGH);
}
delay(del);
reset();
}
// active cathode sinks current (output low), the rest float (input)
void select_row(int row) {
for (int i = 0; i < num_cathodes; i++) {
if (i == row) {
pinMode(cathodes[i], OUTPUT);
digitalWrite(cathodes[i], LOW);
} else {
pinMode(cathodes[i], INPUT);
}
}
}
// all anodes low, all cathodes floating
void reset() {
for (int i = 0; i < num_anodes; i++) {
digitalWrite(anodes[i], LOW);
}
for (int i = 0; i < num_cathodes; i++) {
pinMode(cathodes[i], INPUT);
}
}
abstraction:
send data for whole matrix (2x2x2) in a byte (8 bits):

define binary as:
0b01000111
with prefix 0b.

for larger arrays, use transistors.
keyboard inputs work like so:
try holding down y, u, and then h at the same time. h won’t work because of the way the matrix is designed. wasd will work.

solution is to add a diode:

charlieplexing:

using shift registers and multiplexors, you can pass data for a matrix.