About Me Weekly Assignments Final Project
Kevin J Jijo
Week 11

11. Networking and Communication

Group Assignment

Results can be found on the group assignment page of our lab.

Individual Assignment

Output Week Board

This is the Output Week board. It has a display that shows five track options connected to a servo motor and a stepper motor.


Communication Week Plan

Many of the goals I set during Output Devices Week could not be completed, so I carried those objectives forward into Communication Week.

The goals of this week are as follows:

My initial plan was to redesign the Output Week board to accommodate an additional nRF module that would receive instructions from an nRF-based remote. However, I decided against redesigning the existing board. Instead, I designed a separate nRF module using another ATtiny1624, which communicates with the main output board using RX/TX serial lines.

After completing that, I designed the nRF remote module. The remote allows the user to select which track to play and pause playback simply by lifting the needle. The remote therefore includes:


nRF24L01

The nRF24L01 operates in the 2.4 GHz ISM band and supports data rates from 250 kbps up to 2 Mbps. Under open-space conditions and lower data rates, communication ranges can reach approximately 100 meters.

The module supports 125 RF channels, allowing multiple wireless systems to operate simultaneously within the same area. Each channel supports up to six logical addresses, enabling communication with multiple devices at once.

Communication with microcontrollers such as Arduino or ATtiny devices is performed using the SPI protocol.


Pinout Configuration

The nRF24L01 module has eight pins:

GND
Ground connection.

VCC
Power supply input (1.9 V – 3.6 V only). Connecting directly to 5 V will damage the module.

CE (Chip Enable)
Active HIGH pin used to switch between transmit (TX) and receive (RX) modes.

CSN (Chip Select Not)
Active LOW pin used to enable SPI communication.

SCK (Serial Clock)
SPI clock signal generated by the microcontroller.

MOSI (Master Out Slave In)
SPI data input from the microcontroller.

MISO (Master In Slave Out)
SPI data output from the module.

IRQ (Interrupt)
Active LOW interrupt output used to notify transmission or reception events.


How It Works

SPI Communication

The microcontroller operates as the SPI master while the nRF24L01 acts as the slave device. Radio configuration and payload transfer are handled through SPI commands.

Addressing

Each module uses a unique 5-byte address. Receivers listen only for matching addresses, allowing multiple devices to coexist within the same wireless network.

Operation Modes

Auto-Acknowledgment

After successful reception, the module can automatically send an acknowledgment packet. If no acknowledgment is received, the transmitter automatically retries transmission, improving reliability without additional firmware complexity.

The operating voltage range of the nRF24L01 is 1.9 V to 3.6 V, with 3.3 V being typical. Although most communication pins tolerate 5 V logic levels, the power pin must never be connected directly to 5 V.

Power supply noise is a common cause of unstable communication. RF circuits are sensitive to ripple and transient noise, which may cause packet loss or initialization failures. Therefore, a decoupling capacitor should always be placed close to the module’s power pins.

A Low-Dropout (LDO) regulator, such as the AMS1117-3.3, is commonly used to provide a clean 3.3 V rail from a 5 V system supply dedicated to the nRF module.

The AZ1117I linear regulator is available in fixed output voltages including 1.2 V, 1.5 V, 1.8 V, 2.5 V, 3.3 V, and 5.0 V, along with an adjustable version.

Fixed versions integrate adjustment resistors internally, simplifying design. The adjustable version allows output voltage configuration using two external resistors forming a voltage divider.

Adjustable Output Setup

https://nerdralph.blogspot.com/2014/01/nrf24l01-control-with-3-attiny85-pins.html


The schematic design of the nRF module for the output board

The IRQ pin was left unconnected, but it can be attached to any available microcontroller pin if interrupt functionality is required.


The PCB Design


The schematic design of the nRF remote


PCB design of the nRF remote

Both designs were exported using Gerber2PNG to generate Gerber files for milling. Multiple zero-ohm resistors were used to maintain a single-sided PCB layout, although a double-sided board would provide a more reliable design.

The milled board:

All required components:

All components soldered:

The final powered and connected circuit:


Coding the NRF Module and NRF Remote

First, I verified that both boards could communicate with the computer’s serial monitor to confirm correct operation.

Code used to test UART:


        void setup() {

          Serial.begin(115200);

          delay(1000);

          Serial.println("ATtiny1624 UART Test Started");
        }

        void loop() {

          Serial.println("Hello from ATtiny1624");
          delay(1000);

          if (Serial.available()) {
            char c = Serial.read();

            Serial.print("Received: ");
            Serial.println(c);
          }
        }
        

After confirming UART communication on both boards, one board was configured as a transmitter and the other as a receiver to test nRF communication. The transmitter continuously sent messages while the receiver listened and displayed received data on the serial monitor.

Transmitter Code


        #include <SPI.h>
        #include <RF24.h>

        RF24 radio(PIN_PA3, PIN_PA7);

        const byte address[6] = "00001";

        void setup() {

          Serial.begin(115200);
          delay(1000);

          if (!radio.begin()) {
            Serial.println("NRF not found");
            while (1);
          }

          radio.openWritingPipe(address);
          radio.setPALevel(RF24_PA_LOW);
          radio.stopListening();

          Serial.println("Transmitter Ready");
        }

        void loop() {

          const char text[] = "Hello";

          bool ok = radio.write(&text, sizeof(text));

          if (ok)
            Serial.println("Sent");
          else
            Serial.println("Failed");

          delay(1000);
        }
        

Receiver Code


        #include <SPI.h>
        #include <RF24.h>

        RF24 radio(PIN_PA3, PIN_PA7);

        const byte address[6] = "00001";

        void setup() {

          Serial.begin(115200);
          delay(1000);

          if (!radio.begin()) {
            Serial.println("NRF not found");
            while (1);
          }

          radio.openReadingPipe(0, address);
          radio.setPALevel(RF24_PA_LOW);
          radio.startListening();

          Serial.println("Receiver Ready");
        }

        void loop() {

          if (radio.available()) {

            char text[32] = "";

            radio.read(&text, sizeof(text));

            Serial.print("Received: ");
            Serial.println(text);
          }
        }
        

Successful testing confirmed both UART and nRF communication were working. UART is used between the output device board and the nRF module, while nRF communication links the module and remote.

Prompt used to generate these codes: Give me simple code for an NRF working on SPI to an ATtiny1624 for a transmitter and receiver. I have both; I will upload to both and read.


Final NRF Module Code


        #include <SPI.h>
        #include <RF24.h>

        #define CE_PIN   PIN_PA5
        #define CSN_PIN  PIN_PA4

        RF24 radio(CE_PIN, CSN_PIN);
        const byte address[6] = "00001";

        #define MSG_UP      1
        #define MSG_DOWN    2
        #define MSG_SELECT  3
        #define MSG_PAUSE   4

        void setup() {
          Serial.begin(9600);

          radio.begin();
          radio.openReadingPipe(0, address);
          radio.setPALevel(RF24_PA_LOW);
          radio.startListening();
        }

        void loop() {
          if (radio.available()) {
            uint8_t msg;
            radio.read(&msg, sizeof(msg));

            Serial.write(msg);
          }
        }
        

Final NRF Remote Code


        #include <SPI.h>
        #include <RF24.h>

        #define CE_PIN   PIN_PA5
        #define CSN_PIN  PIN_PA4

        RF24 radio(CE_PIN, CSN_PIN);
        const byte address[6] = "00001";

        #define BTN_UP      PIN_PA7
        #define BTN_DOWN    PIN_PB0
        #define BTN_SELECT  PIN_PA6
        #define BTN_PAUSE   PIN_PB1

        #define MSG_UP      1
        #define MSG_DOWN    2
        #define MSG_SELECT  3
        #define MSG_PAUSE   4

        void setup() {
          pinMode(BTN_UP,     INPUT_PULLUP);
          pinMode(BTN_DOWN,   INPUT_PULLUP);
          pinMode(BTN_SELECT, INPUT_PULLUP);
          pinMode(BTN_PAUSE,  INPUT_PULLUP);

          radio.begin();
          radio.openWritingPipe(address);
          radio.setPALevel(RF24_PA_LOW);
          radio.stopListening();
        }

        void loop() {
          if (digitalRead(BTN_UP) == LOW) {
            sendMessage(MSG_UP);
            waitRelease(BTN_UP);
          }
          if (digitalRead(BTN_DOWN) == LOW) {
            sendMessage(MSG_DOWN);
            waitRelease(BTN_DOWN);
          }
          if (digitalRead(BTN_SELECT) == LOW) {
            sendMessage(MSG_SELECT);
            waitRelease(BTN_SELECT);
          }
          if (digitalRead(BTN_PAUSE) == LOW) {
            sendMessage(MSG_PAUSE);
            waitRelease(BTN_PAUSE);
          }
        }

        void sendMessage(uint8_t msg) {
          delay(20);
          radio.write(&msg, sizeof(msg));
        }

        void waitRelease(uint8_t pin) {
          while (digitalRead(pin) == LOW);
          delay(20);
        }
        

Modified Output Devices Board Code (NRF Enabled)


          #include 
          #include 
          #include 
          #include 

          #define SCREEN_WIDTH 128
          #define SCREEN_HEIGHT 64
          #define OLED_RESET    -1

          Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

          // Rotary encoder & button
          #define ROTARY_A    PIN_PA2
          #define ROTARY_B    PIN_PA1
          #define ROTARY_SW   PIN_PB4

          // Stepper pins
          #define IN1 PIN_PC0
          #define IN2 PIN_PC1
          #define IN3 PIN_PC2
          #define IN4 PIN_PC3

          // Servo pin
          #define SERVO_PIN   PIN_PA3

          // Remote Controls

          #define MSG_UP      1
          #define MSG_DOWN    2
          #define MSG_SELECT  3
          #define MSG_PAUSE   4

          bool paused = false;

          Servo myServo;

          int selectedTrack  = 1;
          int activeTrack    = 0;   // 0 = none selected yet
          int lastA          = HIGH;
          int lastButton     = HIGH;
          int stepDelay      = 2;

          // 5 equal stepper positions across 200 steps (0 to 200)
          const int trackPositions[6] = {0, 0, 50, 100, 150, 200};
          int currentStepperPos = 0;

          void setup() {
            pinMode(ROTARY_A,  INPUT_PULLUP);
            pinMode(ROTARY_B,  INPUT_PULLUP);
            pinMode(ROTARY_SW, INPUT_PULLUP);
            lastA = digitalRead(ROTARY_A);

            pinMode(IN1, OUTPUT);
            pinMode(IN2, OUTPUT);
            pinMode(IN3, OUTPUT);
            pinMode(IN4, OUTPUT);

            myServo.attach(SERVO_PIN);
            myServo.write(90);        // start at 90 (open/lifted position)

            Wire.begin();
            display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
            updateDisplay();
          }

          void loop() {
            handleEncoder();
            handleButton();
            handleRemote(); 
          }

          // ── Rotary encoder ────────────────────────────────────────
          void handleEncoder() {
            int a = digitalRead(ROTARY_A);
            if (a == LOW && lastA == HIGH) {
              if (digitalRead(ROTARY_B) == LOW) {
                selectedTrack++;
              } else {
                selectedTrack--;
              }
              if (selectedTrack > 5) selectedTrack = 1;
              if (selectedTrack < 1) selectedTrack = 5;
              updateDisplay();
            }
            lastA = a;
            delay(2);
          }

          // ── Button press ──────────────────────────────────────────
          void handleButton() {
            int btn = digitalRead(ROTARY_SW);
            if (btn == LOW && lastButton == HIGH) {
              delay(20);                            // debounce
              if (digitalRead(ROTARY_SW) == LOW) {
                goToTrack(selectedTrack);
              }
            }
            lastButton = btn;
          }

          // REMOTE CONTROL

          void handleRemote() {
            if (Serial.available()) {
              uint8_t msg = Serial.read();

              if (msg == MSG_UP) {
                selectedTrack--;
                if (selectedTrack < 1) selectedTrack = 5;
                updateDisplay();
              }
              else if (msg == MSG_DOWN) {
                selectedTrack++;
                if (selectedTrack > 5) selectedTrack = 1;
                updateDisplay();
              }
              else if (msg == MSG_SELECT) {
                goToTrack(selectedTrack);
              }
              else if (msg == MSG_PAUSE) {
                // pause/resume logic - you can expand this
                paused = !paused;
              }
            }
          }

          // ── Move to selected track ────────────────────────────────
          void goToTrack(int track) {
            // Step 1: lift servo from 0 to 90 (only if already placed)
            if (activeTrack != 0) {
              servoMove(0, 90);
            }

            // Step 2: move stepper to new position
            int targetPos = trackPositions[track];
            if (targetPos > currentStepperPos) {
              int steps = targetPos - currentStepperPos;
              for (int i = 0; i < steps; i++) stepForward();
            } else if (targetPos < currentStepperPos) {
              int steps = currentStepperPos - targetPos;
              for (int i = 0; i < steps; i++) stepBackward();
            }
            currentStepperPos = targetPos;

            // Step 3: lower servo from 90 to 0
            servoMove(90, 0);

            activeTrack = track;
            updateDisplay();
          }

          // ── Servo slow sweep ──────────────────────────────────────
          void servoMove(int from, int to) {
            if (from < to) {
              for (int pos = from; pos <= to; pos++) {
                myServo.write(pos);
                delay(15);
              }
            } else {
              for (int pos = from; pos >= to; pos--) {
                myServo.write(pos);
                delay(15);
              }
            }
          }

          // ── Stepper ───────────────────────────────────────────────
          void stepForward() {
            digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);  
            digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);  delay(stepDelay);
            digitalWrite(IN1, LOW);  digitalWrite(IN2, HIGH); 
            digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);  delay(stepDelay);
            digitalWrite(IN1, LOW);  digitalWrite(IN2, HIGH); 
            digitalWrite(IN3, LOW);  digitalWrite(IN4, HIGH); delay(stepDelay);
            digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);  
            digitalWrite(IN3, LOW);  digitalWrite(IN4, HIGH); delay(stepDelay);
          }

          void stepBackward() {
            digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);  
            digitalWrite(IN3, LOW);  digitalWrite(IN4, HIGH); delay(stepDelay);
            digitalWrite(IN1, LOW);  digitalWrite(IN2, HIGH); 
            digitalWrite(IN3, LOW);  digitalWrite(IN4, HIGH); delay(stepDelay);
            digitalWrite(IN1, LOW);  digitalWrite(IN2, HIGH); 
            digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);  delay(stepDelay);
            digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);  
            digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);  delay(stepDelay);
          }

          // ── Display ───────────────────────────────────────────────
          void updateDisplay() {
            display.clearDisplay();

            display.setTextSize(1);
            display.setTextColor(SSD1306_WHITE);
            display.setCursor(25, 5);
            display.println("Record Tracks");
            display.drawLine(0, 16, 127, 16, SSD1306_WHITE);

            for (int i = 1; i <= 5; i++) {
              int y = 20 + (i - 1) * 9;

              if (i == selectedTrack) {
                display.fillRect(0, y - 1, 128, 10, SSD1306_WHITE);
                display.setTextColor(SSD1306_BLACK);
              } else {
                display.setTextColor(SSD1306_WHITE);
              }

              display.setCursor(10, y);
              display.print("Track ");
              display.print(i);

              // show a marker on the active/loaded track
              if (i == activeTrack) {
                display.setTextColor(i == selectedTrack ? SSD1306_BLACK : SSD1306_WHITE);
                display.setCursor(90, y);
                display.print("<");
              }
            }

            display.display();
          }
        

This is how the output devices are normally controlled:

Now the output board can also be controlled using the nRF remote.

Project Files