0 - Introduction
A lot of sensors, screens and even flash memory use SPI as the way to communicate between them and the micro controller. With so many options of what to connect, it would be a mistake to not implement SPI in your baremetal project. In today’s article, we will use what you learned in the previous articles (how to use timers and how to use usb serial) and, with two arduinos (you can buy them here), we will send messages between them.
Because we need to use two micro controllers, this project cannot be tested on Wokwi as we usually do in this type of articles, use this diagram as a guide to wire your real arduinos:
On the Arduino that will act as the master, wire D2 to 5V and then wire D10 to D10 on the slave, D11 to D11, D12 to D12 and D13 to D13.
If you only want the code go to the bottom of the article and you will find all files in there.
1 - Defines and the Ring Buffer
Start by including the ‘avr/io’ header and add defines for each SPI pin, I also added a define for the ‘DDR’ and the ‘PORT’ for clarity in the code.
In this article we will re-use the ring buffer made in the USB Serial article, only changing the name f the functions, so that there is no conflicts with the other one. If you don’t want to have repeated code, you can easely create a separate file for the ring buffer.
#ifndef SPI_H
#define SPI_H
#include <avr/io.h> // used for registers and uintX_t
#ifndef SPI_CS
// D10 on nano
#define SPI_CS PB2
#endif
// DDR
#define SPI_DDR DDRB
// Port
#define SPI_PORT PORTB
// D11 on nano
#define SPI_MOSI PB3
// D12 on nano
#define SPI_MISO PB4
// D13 on nano
#define SPI_SCK PB5
// You can overwrite this on main
#ifndef SPI_BUFFER_SIZE
#define SPI_BUFFER_SIZE 128
#endif
// Ring buffer - struct with an array, head and tail
typedef volatile struct spi_ringbuf
{
// Data array
uint8_t buffer[SPI_BUFFER_SIZE];
// Read location
uint8_t head;
// Write location
uint8_t tail;
} spi_ringbuf_t;
// Add data to ring buffer, returns false (0) on full
uint8_t spi_ringbuf_push(spi_ringbuf_t* buf, uint8_t v);
// Tries to remove data from ring buffer, returns false (0) on empty
uint8_t spi_ringbuf_trypop(spi_ringbuf_t* buf, uint8_t* out);
// Returspi_s the ammount of data in the buffer
uint8_t spi_ringbuf_length(spi_ringbuf_t* buf);
// SPI functions go here
#endif
In the ‘spi.h’ file, include ‘avr/io’, ‘avr/interrupt’ and the spi header files. The ring buffer functions are the same as the ones on the USB Serial article:
#include <avr/io.h>
#include <avr/interrupt.h>
#include "spi.h"
// Ring buffer
// ========== ========== ========== ========== ========== ==========
// Add data to ring buffer, returns false (0) on full
uint8_t spi_ringbuf_push(spi_ringbuf_t* buf, uint8_t v)
{
uint8_t n = buf->head + 1; // next pos
if (n >= SPI_BUFFER_SIZE) // loop to start
n = 0;
if (n == buf->tail) // buf full?
return 0;
buf->buffer[buf->head] = v; // write new data
buf->head = n; // write new pos
return 1;
}
// Tries to remove data from ring buffer, returns false (0) on empty
uint8_t spi_ringbuf_trypop(spi_ringbuf_t* buf, uint8_t* out)
{
if (buf->head == buf->tail) // buf empty?
return 0;
uint8_t n = buf->tail + 1; // next pos
if (n >= SPI_BUFFER_SIZE) // loop to start
n = 0;
*out = buf->buffer[buf->tail]; // write new data
buf->tail = n; // write new pos
return 1;
}
// Returns the ammount of data in the buffer
uint8_t spi_ringbuf_length(spi_ringbuf_t* buf)
{
if (buf->head == buf->tail) // empty buf
return 0;
else if (buf->tail < buf->head) // tail behind head
return (SPI_BUFFER_SIZE - buf->head + buf->tail);
else return buf->tail - buf->head; // tail after head
}
// SPI code goes here
2 - Coding SPI
Let’s start by initializing the SPI module. There are two key to the following function, one for the master and one for the slave. Keep in mind that both will use the ring buffers, so both need to intiialize the buffers head and tails to zero.
On the master’s code, you set the ‘Chip Select (CS)’, ‘Master Out Slave In (MOSI)’ and ‘Serial ClocK (SCK)’ pins to output in the ‘SPI_DDR’, then in the ‘SPCR’ register, set the ‘Spi Enable (SPE) bit, the ‘Spi Interrupt enable (SPIE)’, ‘Master (MSTR)’ and, finally, you set ‘SPR1’ and ‘SPR0’, that way the clock uses a prescaler of 128 (on my setup with a breadboard and 10cm old dupont wires, 64 gives me a lot of noise, probably the wires fault).
On the slave’s code, you set the ‘Master In Slave Out (MISO)’ pin to output and in the ‘SPCR’ register, you enable the Spi and it’s interruptions (‘SPE’ and ‘SPIE’).
For sending and receiving messages it is also needed a way to store the chars to send and received, for that you can create two buffers, one for sending and one for receiving.
// SPI
// ========== ========== ========== ========== ========== ==========
spi_ringbuf_t rxBuf;
spi_ringbuf_t txBuf;
// Initializes the spi's registers and buffers (Don't forget to call sei)
void spi_init(uint8_t master)
{
// Init buffers
rxBuf.head = 0; rxBuf.tail = 0;
txBuf.head = 0; txBuf.tail = 0;
if (master)
{
// set pins as output and input
SPI_DDR = (1 << SPI_CS) | (1 << SPI_MOSI) | (1 << SPI_SCK);
// enable spi, interrupts, master, clock to 128
SPCR = (1 << SPE) | (1 << SPIE) | (1 << MSTR) | (1 << SPR1)| (1 << SPR0);
}
else
{
// set pins as output and input
SPI_DDR = (1 << SPI_MISO);
// enable spi and interrupts
SPCR = (1 << SPE) | (1 << SPIE);
}
spi_cs_off; // Set CS pin to high
}
For reading data that was received, you will be able to use the ‘spi_read’ function. That function is very simple, simply tries to get data from the buffer and, if none is present, a zero is returned:
uint8_t spi_read()
{
uint8_t c;
if (spi_ringbuf_trypop(&rxBuf, &c))
return c;
return 0;
}
The next logical step is to make functions for sending data. For that, there will be 3 functions, one that sends a char, one that sends a string, and one that sends a byte array, all of them, simply add the char, one by one, to the send buffer:
void spi_sendc(uint8_t v)
{
spi_ringbuf_push(&txBuf, v);
}
void spi_sendn(uint8_t* str, uint8_t n)
{
for (int i = 0; i < n; i++)
spi_ringbuf_push(&txBuf, str[i]);
}
void spi_send(uint8_t* str)
{
while (*str != '\0') // while not '\0'
spi_ringbuf_push(&txBuf, *str++);
}
Finally, the last thing that is needed is an ‘ISR’ to handle the SPI’s interrupts. In it, you start by reading the ‘SPDR’ register and, if non-zero, store it in the read buffer, after that, the send buffer is checked and, if empty, a zero is sent, if non-empty, the value is sent:
// ISR
// ========== ========== ========== ========== ========== ==========
// Interrupt on receive
ISR(SPI_STC_vect)
{
uint8_t tosend, toread;
toread = SPDR;
if (toread)
spi_ringbuf_push(&rxBuf, toread);
if (spi_ringbuf_trypop(&txBuf, &tosend))
SPDR = tosend;
else SPDR = 0;
}
Now with the ‘spi.c’ file ready, let’s finish the ‘spi.h’ file, most of it is only function definitions, but the two defines require a little extra attention. The ‘spi_cs_on’ define, sets the CS pin to low, enabling communications between the master and slave. For the interrupts (and communication) to start, a zero is sent. The other define, ‘spi_cs_off’, simply sets the CS pin to high and ends communication:
// Initializes the spi's registers and buffers (Don't forget to call sei)
void spi_init(uint8_t master);
#define spi_cs_on { SPDR = 0; SPI_PORT &= ~(1 << SPI_CS); SPDR = 0; }
#define spi_cs_off { SPI_PORT |= (1 << SPI_CS); }
uint8_t spi_read();
void spi_sendc(uint8_t v);
void spi_sendn(uint8_t* str, uint8_t n);
void spi_send(uint8_t* str);
3 - Main file
Includes and defines, the ones needed will be ‘avr/io’, ‘avr/interrupt’, ‘util/delay’, ‘spi.h’ and ‘uart.h’, as for defines, you will need to set ‘F_CPU’ if not already set:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "spi.h"
#include "uart.h"
#ifndef F_CPU
#define F_CPU 16000000UL
#endif
On this example, a timer will be used to send a number via SPI every 500ms, for that you can initialize the timer with prescaler 64 and ‘OCRnA’ set to 250, that gives us an interrupt every ms. Then, in the ‘ISR’, increment a local counter named ‘t0_millist’ and, when 500 is reached, increment the value of the char to send (‘spi_tosend’) and set the flag ‘spi_new’ which makes the char be sent:
// Stores how much ms since the arduino is on
volatile uint16_t t0_millis = 0;
volatile uint8_t spi_tosend = 0, spi_new = 0;
// Initialize and start the timer 0
void init_timer0()
{
TCCR0A = 0; // normal operating mode
// enable CTC
TCCR0A |= (1 << WGM01);
// set prescaler to 64 (page 117)
TCCR0B |= (1 << CS01) | (1 << CS00);
// enable output compare A
TIMSK0 |= (1 << OCIE0A);
// 1 interrupt per ms
// used the formula given at
// https://www.tmvtech.com/baremetal-arduino-timers#calc
OCR0A = 250;
}
// Interrupt service routine (ISR) for Timer0
ISR(TIMER0_COMPA_vect)
{
t0_millis++;
if (t0_millis > 500)
{
t0_millis = 0;
spi_tosend++;
if (spi_tosend > 9)
spi_tosend = 0;
spi_new = 1;
}
}
Now, on the main function, start by setting the ‘D2’ pin as input, it will be used to tell the arduino if it is the master or not, then initialize the timer and uart, after that, check the value of ‘D2’, if it is set, the arduino will be initialized as master and a local variable named ‘is_master’ is also set to true, then initilize the spi.
After initializing everything, enable global interrupts (‘sei’), a message is logged telling you if the arduino will be a master or a slave and then, after a small delay, the CS pin is set to low and the infinite loop starts:
uint8_t is_master = 0;
int main()
{
// Set D2 as input
DDRD &= ~(1 << PD2);
// init uart
uart_init(115200, 1);
// init timer
init_timer0();
// init as master
is_master = (PIND & (1 << PD2));
spi_init(is_master);
// enable global interrupts
sei();
uart_send(is_master ? "I am the Master!!\n": "I am a Slave!!\n");
_delay_ms(1000);
// enable spi slave
spi_cs_on;
// loop forever
while (1)
{
// LOOP CODE GOES HERE
}
return 0;
}
Inside the loop, you read the messages on the receive buffer and, if non zero, log it to the uart then, if you are the master and a new character is to be sent, send it via spi and set the ‘spi_new’ flag to zero (to not send repeated chars), if you are the slave, simply resend the char you received, if non zero:
uint8_t read = spi_read();
if (read != 0)
uart_sendc(read);
if (is_master && spi_new)
{
spi_sendc(spi_tosend + (uint8_t)'0');
spi_new = 0;
}
else if (!is_master)
{
if (read != 0)
spi_sendc(read);
}
And that’s everything coded! Now to compile and upload it, you can go to this article where we made a Makefile that will be getting updated as needed.
If you simply want to copy and paste everything, you can see all files below.
Thanks for reading and stay tuned for more tech insights and tutorials. Until next time and keep exploring the world of tech!
Below are the spi and main source files, you can get the uart source files at the bottom of this article, just place them in the same folder as main and run the makefile.
main.c
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "spi.h"
#include "uart.h"
#ifndef F_CPU
#define F_CPU 16000000UL
#endif
// Stores how much ms since the arduino is on
volatile uint16_t t0_millis = 0;
volatile uint8_t spi_tosend = 0, spi_new = 0;
// Initialize and start the timer 0
void init_timer0()
{
TCCR0A = 0; // normal operating mode
// enable CTC
TCCR0A |= (1 << WGM01);
// set prescaler to 64 (page 117)
TCCR0B |= (1 << CS01) | (1 << CS00);
// enable output compare A
TIMSK0 |= (1 << OCIE0A);
// 1 interrupt per ms
// used the formula given at
// https://www.tmvtech.com/baremetal-arduino-timers#calc
OCR0A = 250;
}
// Interrupt service routine (ISR) for Timer0
ISR(TIMER0_COMPA_vect)
{
t0_millis++;
if (t0_millis > 500)
{
t0_millis = 0;
spi_tosend++;
if (spi_tosend > 9)
spi_tosend = 0;
spi_new = 1;
}
}
int main()
{
// Set D2 as input
DDRD &= ~(1 << PD2);
// init uart
uart_init(115200, 1);
// init timer
init_timer0();
// send hello
if (PIND & (1 << PD2))
{
spi_init(1);
uart_send("I am the Master!!\n");
}
else
{
spi_init(0);
uart_send("I am a Slave!!");
}
// enable global interrupts
sei();
uart_sendc('\n');
_delay_ms(1000);
// enable spi slave
spi_cs_on;
// loop forever
while (1)
{
uint8_t read = spi_read();
if (read != 0)
uart_sendc(read);
if (PIND & (1 << PD2) && spi_new)
{
spi_sendc(spi_tosend + (uint8_t)'0');
spi_new = 0;
}
else if (PIND & (1 << PD2) == 0)
{
if (read != 0)
spi_sendc(read);
}
}
return 0;
}
spi.h
#ifndef SPI_H
#define SPI_H
#include <avr/io.h> // used for registers and uintX_t
#ifndef SPI_CS
// D10 on nano
#define SPI_CS PB2
#endif
// DDR
#define SPI_DDR DDRB
// Port
#define SPI_PORT PORTB
// D11 on nano
#define SPI_MOSI PB3
// D12 on nano
#define SPI_MISO PB4
// D13 on nano
#define SPI_SCK PB5
// You can overwrite this on main
#ifndef SPI_BUFFER_SIZE
#define SPI_BUFFER_SIZE 128
#endif
// Ring buffer - struct with an array, head and tail
typedef volatile struct spi_ringbuf
{
// Data array
uint8_t buffer[SPI_BUFFER_SIZE];
// Read location
uint8_t head;
// Write location
uint8_t tail;
} spi_ringbuf_t;
// Add data to ring buffer, returns false (0) on full
uint8_t spi_ringbuf_push(spi_ringbuf_t* buf, uint8_t v);
// Tries to remove data from ring buffer, returns false (0) on empty
uint8_t spi_ringbuf_trypop(spi_ringbuf_t* buf, uint8_t* out);
// Returspi_s the ammount of data in the buffer
uint8_t spi_ringbuf_length(spi_ringbuf_t* buf);
// Initializes the spi's registers and buffers (Don't forget to call sei)
void spi_init(uint8_t master);
#define spi_cs_on { SPDR = 0; SPI_PORT &= ~(1 << SPI_CS); SPDR = 0; }
#define spi_cs_off { SPI_PORT |= (1 << SPI_CS); }
uint8_t spi_read();
void spi_sendc(uint8_t v);
void spi_sendn(uint8_t* str, uint8_t n);
void spi_send(uint8_t* str);
#endif
spi.c
#include <avr/io.h>
#include <avr/interrupt.h>
#include "spi.h"
// Ring buffer
// ========== ========== ========== ========== ========== ==========
// Add data to ring buffer, returns false (0) on full
uint8_t spi_ringbuf_push(spi_ringbuf_t* buf, uint8_t v)
{
uint8_t n = buf->head + 1; // next pos
if (n >= SPI_BUFFER_SIZE) // loop to start
n = 0;
if (n == buf->tail) // buf full?
return 0;
buf->buffer[buf->head] = v; // write new data
buf->head = n; // write new pos
return 1;
}
// Tries to remove data from ring buffer, returns false (0) on empty
uint8_t spi_ringbuf_trypop(spi_ringbuf_t* buf, uint8_t* out)
{
if (buf->head == buf->tail) // buf empty?
return 0;
uint8_t n = buf->tail + 1; // next pos
if (n >= SPI_BUFFER_SIZE) // loop to start
n = 0;
*out = buf->buffer[buf->tail]; // write new data
buf->tail = n; // write new pos
return 1;
}
// Returns the ammount of data in the buffer
uint8_t spi_ringbuf_length(spi_ringbuf_t* buf)
{
if (buf->head == buf->tail) // empty buf
return 0;
else if (buf->tail < buf->head) // tail behind head
return (SPI_BUFFER_SIZE - buf->head + buf->tail);
else return buf->tail - buf->head; // tail after head
}
// SPI
// ========== ========== ========== ========== ========== ==========
spi_ringbuf_t rxBuf;
spi_ringbuf_t txBuf;
// Initializes the spi's registers and buffers (Don't forget to call sei)
void spi_init(uint8_t master)
{
// Init buffers
rxBuf.head = 0; rxBuf.tail = 0;
txBuf.head = 0; txBuf.tail = 0;
if (master)
{
// set pins as output and input
SPI_DDR = (1 << SPI_CS) | (1 << SPI_MOSI) | (1 << SPI_SCK);
// enable spi, interrupts, master, clock to 128
SPCR = (1 << SPE) | (1 << SPIE) | (1 << MSTR) | (1 << SPR1)| (1 << SPR0);
}
else
{
// set pins as output and input
SPI_DDR = (1 << SPI_MISO);
// enable spi and interrupts
SPCR = (1 << SPE) | (1 << SPIE);
}
spi_cs_off;
}
uint8_t spi_read()
{
uint8_t c;
if (spi_ringbuf_trypop(&rxBuf, &c))
return c;
return 0;
}
void spi_sendc(uint8_t v)
{
spi_ringbuf_push(&txBuf, v);
}
void spi_sendn(uint8_t* str, uint8_t n)
{
for (int i = 0; i < n; i++)
spi_ringbuf_push(&txBuf, str[i]);
}
void spi_send(uint8_t* str)
{
while (*str != '\0') // while not '\0'
spi_ringbuf_push(&txBuf, *str++);
}
// ISR
// ========== ========== ========== ========== ========== ==========
// Interrupt on receive
ISR(SPI_STC_vect)
{
uint8_t tosend, toread;
toread = SPDR;
if (toread)
spi_ringbuf_push(&rxBuf, toread);
if (spi_ringbuf_trypop(&txBuf, &tosend))
SPDR = tosend;
else SPDR = 0;
}
uart.h
#ifndef UART_H
#define UART_H
#include <avr/io.h>
#ifndef UART_BUFFER_SIZE
#define UART_BUFFER_SIZE 128
#endif
// Ring buffer - struct with an array, head and tail
typedef volatile struct uart_ringbuf
{
uint8_t buffer[UART_BUFFER_SIZE];
uint8_t head;
uint8_t tail;
} uart_ringbuf_t;
// Add data to ring buffer, returns false (0) on full
uint8_t uart_ringbuf_push(uart_ringbuf_t* buf, uint8_t v);
// Tries to remove data from ring buffer, returns false (0) on empty
uint8_t uart_ringbuf_trypop(uart_ringbuf_t* buf, uint8_t* out);
// Returns the ammount of data in the buffer
uint8_t uart_ringbuf_length(uart_ringbuf_t* buf);
// initializes that uart registers for sending and receiving
void uart_init(uint32_t baudrate, uint8_t doublespeed);
// Read char from buf, returns 0 if empty
uint8_t uart_read();
// Add string to buffer and start sending if not started already
void uart_send(char* str);
// Add string to buffer and start sending if not started already
void uart_sendn(char* str, uint8_t n);
// Add char to buffer and start sending if not started already
void uart_sendc(char c);
#endif
uart.c
#include "uart.h"
#include <avr/io.h>
#include <avr/interrupt.h>
// Ring buffer
// ========== ========== ========== ========== ========== ==========
// Add data to ring buffer, returns false (0) on full
uint8_t uart_ringbuf_push(uart_ringbuf_t* buf, uint8_t v)
{
uint8_t n = buf->head + 1; // next pos
if (n >= UART_BUFFER_SIZE) // loop to start
n = 0;
if (n == buf->tail) // buf full?
return 0;
buf->buffer[buf->head] = v; // write new data
buf->head = n; // write new pos
return 1;
}
// Tries to remove data from ring buffer, returns false (0) on empty
uint8_t uart_ringbuf_trypop(uart_ringbuf_t* buf, uint8_t* out)
{
if (buf->head == buf->tail) // buf empty?
return 0;
uint8_t n = buf->tail + 1; // next pos
if (n >= UART_BUFFER_SIZE) // loop to start
n = 0;
*out = buf->buffer[buf->tail]; // write new data
buf->tail = n; // write new pos
return 1;
}
// Returns the ammount of data in the buffer
uint8_t uart_ringbuf_length(uart_ringbuf_t* buf)
{
if (buf->head == buf->tail) // empty buf
return 0;
else if (buf->tail < buf->head) // tail behind head
return (UART_BUFFER_SIZE - buf->head + buf->tail);
else return buf->tail - buf->head; // tail after head
}
// Uart
// ========== ========== ========== ========== ========== ==========
// Receive buffer
uart_ringbuf_t rxBuf;
// Send buffer
uart_ringbuf_t txBuf;
// Send status
volatile uint8_t txStatus = 0;
// If txBuf has chars start sending them
void uart_internal_sendchar()
{
// check if buffer has data
uint8_t tosend;
if (uart_ringbuf_trypop(&txBuf, &tosend))
{
txStatus = 1; // sending data
UDR0 = tosend; // add char to send buffer
}
// no data left to send
else txStatus = 0;
}
// Returns the value for UBRRn for the chosen baudrate and U2Xn (double speed)
uint16_t uart_ubrrn_from_baud(uint32_t baudrate, uint8_t u2xn)
{
if (u2xn)
{
switch (baudrate)
{
case 2400: return 832;
case 4800: return 416;
case 9600: return 207;
case 14400: return 138;
case 19200: return 103;
case 28800: return 68;
case 38400: return 51;
case 57600: return 34;
case 76800: return 25;
case 115200: return 16;
case 230400: return 8;
case 250000: return 7;
case 500000: return 3;
case 1000000: return 1;
default: return 16; // 115200
}
}
switch (baudrate)
{
case 2400: return 416;
case 4800: return 207;
case 9600: return 103;
case 14400: return 68;
case 19200: return 51;
case 28800: return 34;
case 38400: return 25;
case 57600: return 16;
case 76800: return 12;
case 115200: return 8;
case 230400: return 3;
case 250000: return 3;
case 500000: return 1;
case 1000000: return 0;
default: return 8; // 115200
}
}
// initializes that uart registers for sending and receiving
void uart_init(uint32_t baudrate, uint8_t doublespeed)
{
// init buffers
rxBuf.head = 0; rxBuf.tail = 0;
txBuf.head = 0; txBuf.tail = 0;
// set double speed mode
if (doublespeed)
UCSR0A |= (1 << U2X0);
// Enable transmiter and receiver
UCSR0B |= (1 << TXEN0) | (1 << RXEN0);
// Enable interrupts
UCSR0B |= (1 << TXCIE0) | (1 << RXCIE0);
// Set baudrate
UBRR0 = uart_ubrrn_from_baud(baudrate, doublespeed);
}
// Read char from buf, returns 0 if empty
uint8_t uart_read()
{
uint8_t c;
if (uart_ringbuf_trypop(&rxBuf, &c))
return c;
return 0;
}
// Add string to buffer and start sending if not started already
void uart_send(char* str)
{
while (*str != '\0') // while not '\0'
uart_ringbuf_push(&txBuf, *str++);
if (!txStatus) // if not sending, start sending
uart_internal_sendchar();
}
// Add string to buffer and start sending if not started already
void uart_sendn(char* str, uint8_t n)
{
for (int i = 0; i < n; i++) // send n ammount of chars
uart_ringbuf_push(&txBuf, str[i]);
if (!txStatus) // if not sending, start sending
uart_internal_sendchar();
}
// Add char to buffer and start sending if not started already
void uart_sendc(char c)
{
// send a single char
uart_ringbuf_push(&txBuf, c);
if (!txStatus) // if not sending, start sending
uart_internal_sendchar();
}
// ISR
// ========== ========== ========== ========== ========== ==========
// Interrupt on receive
ISR(USART_RX_vect)
{
uart_ringbuf_push(&rxBuf, UDR0);
}
// Interrupt on send
ISR(USART_TX_vect)
{
uart_internal_sendchar();
}
And that’s everything for this article, thanks again for reading and stay tuned for more tech insights and tutorials. Until next time and keep exploring the world of tech!