Learn To Write Code For 8051, Arduino, AVR, dsPIC, PIC, STM32 ARM Microcontroller, etc.
Coding Embedded Controller With C/C++.
Printed Circuit Board (PCB) Project For Electronics Hobbyists.
The Serial Peripheral Interface (SPI) is a high speed serial communication that commonly use three wires, serial clock, serial data and enable (master transmit only). Most of modern micro-controller has this serial communication interface. It is useful for board to board or to device communication due to wiring complexity compare to parallel port data transmission.
SPI is popular among graphical display, Flash memory, I/O expansion chip, SRAM, EEPROM, real time clock chip etc. Its transmission and reception speed could reach up to several Mbits/s. These are some example of SPI devices:
DS3234 Real Time Clock
Nokia 5510 graphical LCD module
MCP23S17 I/O Expansion Chip
SN74HC595N Serial In Parallel Out Shift Registers
ILI9341/ST7785 240x320 TFT Display
ST7735 128x160 TFT Display
W25Q64JVSSIQ Flash Memory Chip
SD MMC Card etc.
W25Q64JVSSIQ
ILI9341/ST7785 240x320 TFT Display
Some earlier MCU doesn't have an SPI module inside for instance the AT89S52 or PIC16F84A. However the programmer could emulate a software SPI by using the bit-banging technique. This method is popular but it yield a lower speed data transmission and reception.
Typically a system operation contains a master MCU that transmit and receive data from its connected SPI slave devices.
Single master to single slave: basic SPI wiring
The pin names of the SPI module in the AVR micro-controller are different from other MCU such as PIC. These pins are:
Abbr.
Name
Description
SS
Slave Select
Active-lowchip selectsignal from master to enable communication with a specific slave device
The master MCU must select any connected SPI slave device via its slave select (SS) pin. One slave device has a unique SS pin. So multiple slave devices have many SS pins.
A typical hardware setup using two shift registers to form an inter-chip circular buffer
The SS pin is active low or active high upon the specific device.
Its clock polarity is commonly positive (low to high). Somes device use a negative (high to low) clock polarity.
However its clock polarity and phase are configured by user software that requires an understanding of technical detail of an MCU (Dedicated SPI Module).
The master MCU can communicates with many different SPI slave devices (Multidrop SPI bus) on a single bus using additional SS pins.
Multidrop SPI bus
If multiple SPI slave devices with the same type connect to a single SPI bus we can use the Daisy chain configuration. For instance the SN74HC595N or SN74HC164 serial in parallel out shift registers.
Daisy chain configuration
It is used for expanding a number of output ports for instance driving a dot matrix display.
Currently there are a lot of newer method of SPI data transmission that is precise and high speed.
ATMega644P Serial Peripheral Interface
The SPI module of the ATMega644P has the following features:
• Full-duplex, Three-wire Synchronous Data Transfer • Master or Slave Operation • LSB First or MSB First Data Transfer •Seven Programmable Bit Rates • End of Transmission Interrupt Flag • Write Collision Flag Protection • Wake-up from Idle Mode • Double Speed (CK/2) Master SPI Mode
Its operation mode and speed are configured in user program via its relevant special function registers.
SPI Block Diagram
Its clock generator is divided form the MCU clock up to CK/2. Data is shifted in and out using its internal 8-bit shift register at each clock cycle of the SPI clock generator. It also generate interrupt flag whenever the shift registers is empty.
SPI Master-slave Interconnection
These are its relevant registers:
SPCR – SPI Control Register
SPCR – SPI Control Register
SPSR – SPI Status Register
SPSR – SPI Status Register
SPDR – SPI Data Register
SPDR – SPI Data Register
The ATMega644P may operate in master transmit only, slave receive only or even full duplex (synchronous data transfer) depending on these registers. For more detail please see the device datasheet.
ATMega644P SPI Interfacing and Programming
We can configure the SPI module of the ATMega644P to operates in a specific mode depends on the worked application.
SPI Master Transmit Mode
An SPI master transmits only is common. There are many SPI slave device that only need data reception such as the SN74HC595N or 74HC164 shift register chips.
SN74HC595N and SN74HC164N Shift Registers
These chips is very popular due to its availability, low cost, ease of control etc. The SN74HC595N is commonly found in output expanding application such as relays driving, LED driving especially the dot matrix display driver.
SN74HC595N Pin Diagram
In this example the ATMega644P operate in master mode to transmit data to the SN74HC595N shifter register chip. The output port of this chip can connects to LED, 7-Segment displays or even a character LCD.
There are sample code of using the SPI transmit in device data sheet in both Assembly and C program.
Sample Code
LED Driving
I have a DIY single chip SN74HC595N LED module that is very easy to make. However we can install them together on a single breadboard.
In previous example the ATMega644P emulate the TWI via software bit-banging that able to communicate to various slave TWI devices on a single bus. In this example the ATMega644P master MCU send display data to the SH1106 128x64 OLED display and read calendar data from the DS1307 RTC. For more information about using the SH1106 128x64 OLED display please visit this post.
ATMega644P Software TWI SH1106 OLED
ATMega644P Software TWI SH1106 OLED
For an introductory example the master MCU send display data to the SH1106 128x64 OLED module.
Source Code "main.c":
/*
* 11-soft_twi_sh1106.c
*
* Created: 2/10/2026 12:03:22 AM
* Author : Admin
*/
#include<avr/io.h>
#include<util/delay.h>
#define F_CPU 16000000UL
intmain(void)
{
/* Replace with your application code */
_delay_ms(1000);
display_init();
display_clear(0);
display_text_8x16(0,0,"SH1106 I2C OLED");
display_text_8x16(0,1,"And ATMega644P");
display_text_8x16(0,2,"Programming With");
display_text_8x16(0,3,"Microchip Studio");
_delay_ms(10000);
display_clear(0x00);
while (1)
{
display_text_8x16(0,0,"Software TWI");
_delay_ms(2500);
uint8_t h=15;
for(uint8_t i=0;i<10;i++) {
display_char_8x16(h,1,'0'+i);
h+=10;
}
_delay_ms(2500);
h=0;
for(uint8_t i=0;i<14;i++) {
display_char_8x16(h,2,'A'+i);
h+=10;
}
_delay_ms(2500);
h=0;
for(uint8_t i=0;i<14;i++) {
display_char_8x16(h,3,'N'+i);
h+=10;
}
_delay_ms(10000);
display_clear(255);
_delay_ms(10000);
display_clear(0);
_delay_ms(1000);
}
}
Schematic:
Schematic
Proteus only has a model for the SSD1306. Simulating this part is very slow.
For any MCU that doesn't have a dedicated TWI module inside the programmer can emulate a TWI using software bit-banging. It's very common and readable for most of students, programmers and electronics hobbyists. They just need to understand about the overall protocol of TWI data transmission and reception. It's very similar to the SPI but it excepts START, REPEATED START, STOP Condition and one additional ACK/NACK bit. Without an MCU the programmer uses the PC parallel port instead.
However using software TWI it's not robust and fast like the TWI module inside an MCU. The software TWI can fit to any MCU with any additional code modifications without understanding the the detail of the dedicated TWI module of an MCU.
For an AVR micro-controller the user need set a correct fuse and a proper software configuration for the internal TWI module to make a correct operation of the TWI data transmission and reception.
Unfortunately my ATMega644P was supplied with a over voltage supply of around 10VDC for almost one day. It caused by a malfunction ASM1117-5.0 low drop out voltage regulator chip. But the MCU still works with some defected parts. Then its dedicated TWI module also defected.
PCF8574AP Software TWI GPIO Port Writing
I decide to learn and write my own software TWI routine to control a TWI OLED display I posses. I worked well. To get started I wrote a simple C routine for writing data to a PCF8574AP I/O expansion chip.
A simple software TWI data sending to the PCF8574AP I/O Expansion
This introductory example is a draft of C routine I first wrote. The master MCU send data to the PCF8574AP via its software TWI pins, SCL and SDA. We can select other pins depends on the program settings. The 8-bit write address of PCF8574AP (A2:A0=0x00) is 0x70 and 0x71 for read address.
C Source File "main.c" :
/*
* 11-soft_twi_pcf8574ap.c
*
* Created: 2/9/2026 10:06:25 PM
* Author : Admin
*/
#include<avr/io.h>
#include<util/delay.h>
#define F_CPU 16000000UL
#define TWI_PORT PORTC
#define TWI_SDA_IN PINC
#define TWI_DIR DDRC
constchar SDA=1;
constchar SCL=0;
voiddelay_counts(unsignedint count){
for(unsignedint i=0;i<count;i++);
}
voidtwi_start(void){
TWI_DIR=(1<<SDA)|(1<<SCL);
TWI_PORT=(1<<SDA)|(1<<SCL);
delay_counts(20);
TWI_PORT&=~(1<<SDA);
delay_counts(10);
TWI_PORT&=~(1<<SCL);
delay_counts(10);
}
voidtwi_stop(void){
TWI_PORT&=~(1<<SCL);
TWI_PORT&=~(1<<SDA);
TWI_PORT|=(1<<SCL);
delay_counts(10);
TWI_PORT|=(1<<SDA);
delay_counts(10);
TWI_PORT=(1<<SDA)|(1<<SCL);
delay_counts(20);
}
/*
void twi_write(char data){
char temp=0;
for (unsigned char i=0;i<8;i++)
{
TWI_PORT&=~(1<<SCL);
temp=data&(1<<7);
if(temp==0) TWI_PORT&=~(1<<SDA);
else TWI_PORT|=(1<<SDA);
delay_counts(10);
TWI_PORT|=(1<<SCL);
data<<=1;
delay_counts(10);
}
TWI_PORT&=~(1<<SDA);
TWI_PORT&=~(1<<SCL);
TWI_DIR&=~(1<<SDA);
while(TWI_PORT&(1<<SDA)==0);
delay_counts(10);
TWI_PORT|=(1<<SCL);
delay_counts(10);
TWI_DIR=(1<<SDA)|(1<<SCL);
}
*/
voidtwi_write(char data){
char temp=0;
TWI_DIR|=(1<<SDA)|(1<<SCL);
for (unsignedchar i=0;i<9;i++)
{
TWI_PORT&=~(1<<SCL);
if(i<8){
temp=data&0x80;
if(temp==0) TWI_PORT&=~(1<<SDA);
else TWI_PORT|=(1<<SDA);
}
else{
TWI_PORT&=~(1<<SDA);
TWI_DIR&=~(1<<SDA);
while(TWI_SDA_IN&(1<<SDA)==0);
}
delay_counts(100);
TWI_PORT|=(1<<SCL);
data<<=1;
delay_counts(100);
}
TWI_DIR|=(1<<SDA)|(1<<SCL);
}
chartwi_read(void){
char temp=0,data=0;
for (unsignedchar i=0;i<8;i++)
{
TWI_PORT&=~(1<<SCL);
temp=data&(1<<7);
temp=TWI_PORT&&(1<<SDA);
if(temp==1) data|=1;
else data|=0;
delay_counts(10);
TWI_PORT|=(1<<SCL);
data<<=1;
delay_counts(10);
}
TWI_PORT&=~(1<<SDA);
TWI_PORT&=~(1<<SCL);
//TWI_DIR&=~(1<<SDA);
while(TWI_PORT&(1<<SDA)==0);
delay_counts(10);
TWI_PORT|=(1<<SCL);
delay_counts(10);
TWI_DIR=(1<<SDA)|(1<<SCL);
}
intmain(void)
{
/* Replace with your application code */
char i=0;
DDRB=0xFF;
twi_start();
twi_write(0x70);
twi_write(1);
twi_stop();
_delay_ms(5000);
twi_start();
twi_write(0x70);
twi_write(0);
twi_stop();
_delay_ms(5000);
while (1)
{
twi_start();
twi_write(0x70);
twi_write(1<<i);
twi_stop();
_delay_ms(500);
i++;
if(i==8) i=0;
/*
twi_start();
twi_write(0x70);
twi_write(0xF0);
twi_stop();
_delay_ms(500);
*/
}
}
I didn't test this firmware in real hardware. But I think it work like the one's in software simulator.
PCF8574AP Software TWI GPIO Port Reading and Writing
Now I added and test a software TWI write routine. The master ATMega644P MCU periodically send data between PORTB and I/O port of the PCF8574AP.
PCF8574AP GPIO Port Reading and Writing
The lower nibble of PORTB is an output port while its higher nibble is an input port. The lower nibble of PCF8574AP is an input port while its higher nibble is an output port. The data is exchanged between these two chip periodically.
Source File "main.c" :
/*
* 11-soft_twi_pcf8574ap_ex.c
*
* Created: 2/12/2026 9:02:47 AM
* Author : Admin
*/
#include<avr/io.h>
#include<util/delay.h>
#define F_CPU 16000000UL
#define TWI_PORT PORTC
#define TWI_SDA_IN PINC
#define TWI_DIR DDRC
constchar SDA=1;
constchar SCL=0;
constchar PULSE=20;
voiddelay_counts(unsignedint count){
for(unsignedint i=0;i<count;i++);
}
voidtwi_start(void){
TWI_DIR|=(1<<SDA)|(1<<SCL);
TWI_PORT|=(1<<SDA)|(1<<SCL);
delay_counts(PULSE);
TWI_PORT&=~(1<<SDA);
delay_counts(PULSE);
TWI_PORT&=~(1<<SCL);
delay_counts(PULSE);
}
voidtwi_stop(void){
TWI_PORT&=~(1<<SCL);
TWI_PORT&=~(1<<SDA);
TWI_PORT|=(1<<SCL);
delay_counts(PULSE);
TWI_PORT|=(1<<SDA);
delay_counts(PULSE);
TWI_PORT|=(1<<SDA)|(1<<SCL);
delay_counts(PULSE);
}
voidtwi_write(char data){
char temp=0;
TWI_DIR|=(1<<SDA)|(1<<SCL);
for (unsignedchar i=0;i<9;i++)
{
TWI_PORT&=~(1<<SCL);
delay_counts(PULSE);
if(i<8){
temp=data&0x80;
if(temp==0) TWI_PORT&=~(1<<SDA);
else TWI_PORT|=(1<<SDA);
}
else{
TWI_PORT&=~(1<<SDA);
TWI_DIR&=~(1<<SDA);
while(TWI_SDA_IN&(1<<SDA)==0);
}
TWI_PORT|=(1<<SCL);
delay_counts(PULSE);
data<<=1;
}
TWI_DIR|=(1<<SDA)|(1<<SCL);
}
chartwi_read(void){
char temp=0,data=0;
TWI_DIR&=~(1<<SDA);
for (unsignedchar i=0;i<9;i++)
{
TWI_PORT&=~(1<<SCL);
delay_counts(PULSE);
if(i<8){
/*
data<<=1;
temp=TWI_SDA_IN&(1<<SDA);
if(temp==(1<<SDA)) data|=1;
else data|=0;
*/
temp=TWI_SDA_IN&(1<<SDA);
temp>>=1;
data|=(temp<<(7-i));
}
else{
while((TWI_SDA_IN&(1<<SDA))==0);
}
TWI_PORT|=(1<<SCL);
delay_counts(PULSE);
}
return data;
}
intmain(void)
{
/* Replace with your application code */
char data;
DDRB=0x0F;
PORTB=0xF0;
twi_start();
twi_write(0x70);
twi_write(0x0F);
twi_stop();
_delay_ms(100);
while (1)
{
twi_start();
twi_write(0x71);
data=twi_read();
twi_stop();
_delay_ms(100);
//Send Data To PORTB and Turn On Pull Up For PINC4:PINC7
I tested the on-board DS13017 RTC and the HD44780 based 16x2 characters LCD. This 8-bit parallel port LCD operates in its 4-bit data transfer mode at PORTC. The software TWI uses PORTC PC0(SCL) and PC1(SDA) in the same port with the LCD. It doesn't cause any problem since I use masking in C program.
Driving an HD4470 based character LCD using the PCF8574T is very easy. A character LCD with a PCF8574T is widely available at very low cost. It uses only four wires connects to a master MCU, GND, VCC, SDA and SCL.
PCF8574T HD44780 Character LCD Controller
We don't need to use its full 8-bit data transfer mode. Sending the 8-bit data or command twice using its 4-bit data transfer mode is suitable for most of applications.
At this point the ATMega644P master MCU read the RTC data from the on-board DS1307 Real Time Clock (RTC). The date and time data will show on an HD44780 character LCD via its driver PCF8574T. The software TWI work fine without reading or writing error.
There are a lot of newer Real Time Clock chip that consumes lower energy, rich of functions and more precise. The DS3231 or DS3232 is a TWI Real Time Clock(RTC) that has some addional functions for example alarm setting and alarm triggering. Its timing data is very precise due to its internal clock and programmable capacitors.
I bought a few Arduino DS3231 RTC AT2432N EEPROM module around 2USD. Its slave address and data/time data are quivalent to the DS1307. So I don't need to write additional firmware for this example. It work the same way as above. I just modify some lines of code.
Schematic:
Schematic
The DS3231 and PCF8574T connected on a single TWI bus.
AVR Experiment Board:
MCP23017 16-bit GPIO Example
The Microchip MCP23017 is a TWI 16-bit GPIO expansion chip. It I/O port and internal structure is well organized. However it's more expensive than the PCF8574. The PCF8574 is more popular and widely available at very low cost.