Sometimes it is necessary to send data between two microprocessors. The SPI bus is a very fast serial data-bus that is normally used to interface with various peripherals like OLED Screens, Radios and various sensors. In today’s very short post, I will show you how to interface the STM32F103C8T6 “Blue Pill” with an Arduino Nano to send bidirectional data via the SPI Interface between the two microprocessors. You will need the following to experiment with this by yourself.
1) An STM32F103C8T6 ” Blue Pill” 2) An Arduino Compatible or Original Uno or Nano 3) Two Breadboards 4) Some Linkup wires (at least 4 male-to-male DuPont wires)
Let us look at the pin configuration on the two boards
PIN NAME
“Blue Pill”
Arduino Nano or Uno
MOSI
PA7
D11
MISO
PA6
D12
SCK
PA5
D13
SS
PA4
D10
Connections for the “Blue Pill” are shown above
Connections for Maker NANO are shown above
You can now type in the code for the Master ( The Blue Pill ) into your Arduino IDE
#include<SPI.h>
// Define Constants and Variables
#define SS PA4
#define led PC13
unsigned char MasterSend;;
unsigned char MasterReceive;
void setup() {
pinMode(led,OUTPUT);
Serial.begin(115200);
// SPI Init
SPI.begin();
SPI.setClockDivider(SPI_CLOCK_DIV16);
digitalWrite(SS,HIGH); // set as master
MasterSend = 0xFF;
}
void loop() {
Serial.print(“Sent to Slave “);
Serial.print(” [0x”);
Serial.println(MasterSend,HEX);
MasterReceive = SPI.transfer(MasterSend);
Serial.print(“Received from Slave “);
Serial.print(” [0x”);
Serial.print(MasterReceive,HEX);
Serial.println(“]”);
digitalWrite(led,!digitalRead(led));
delay(200);
}
And in ANOTHER INSTANCE of the Arduino IDE, the code for the SLAVE (Maker NANO)
//SPI Slave Code for Arduino
//SPI Communication between STM32F103C8 & Arduino
#include<SPI.h>
volatile boolean received;
volatile unsigned char SlaveReceived;
volatile unsigned char SlaveSend;
void setup()
{
Serial.begin(115200);
pinMode(MISO,OUTPUT);
SPCR |= _BV(SPE);
received = false;
SPI.attachInterrupt();
SlaveSend = 0xAA;
}
ISR(SPI_STC_vect)
{
SlaveReceived = SPDR;
received = true;
}
void loop() {
Serial.print(“Received from Master”);
Serial.print(” [0x”);
Serial.print(SlaveReceived,HEX);
Serial.println(“] “);
SPDR = SlaveSend;
Serial.print(“Sent to Master”);
Serial.print(” [0x”);
Serial.print(SlaveSend,HEX);
Serial.println(“]”);
delay(200);
}
Upload the code to the boards, and open the serial monitors on both instances of the Arduino IDE. Set the Baud Rate to 115200 You will see that the Master sends a byte to the Slave, and the Save replies with a byte of it’s own.
Master sends data to Slave, Receives Data Back
Slave received data from Master, and replies with data of its own
This sketch can now very easily be modified to send reading from sensors, or instructions to control other peripherals between the two microprocessors. It is limited only by your imagination, and your ability to code.
The Serial Peripheral Interface is a synchronous serial communication interface for short-distance communication, it is typically used in embedded systems. The interface was developed by Motorola in the mid 1980’s and has become a very popular standard.
It is used with many kinds of sensors, LCD’s and also SD-Cards. SPI operates in a Master-Slave model, with a possibility of multiple slave devices, each selected in turn by a SS (slave select) or CS (chip select) pin that is usually pulled low by the master.
Typical configuration
SPI is a four-wire interface, with the different lines being – MOSI [Master Out Slave In] -MISO [Master In Slave Out] -SCLK [Serial Clock OUT – generated by the master] -SS/CS [Slave Select or Chip Select, sometimes also labelled CE – Chip Enable]
SPI is a FULL DUPLEX interface, where the master initiates the communication frames between the various slave devices. This is usually done by pulling the particular device’s SS/CS pin low. Data is then shifted simultaneously into and out of the devices by means of the MOSI and MISO lines on the bus. The frequency of the serially shifted data is controlled by the SCLK line. This clock signal is generated by the master device.
It is important to note that MOST of the slave devices have a tri-state (HIGH IMPEDANCE) mode on their MISO pins. This electrically disconnects the MISO pin from the bus when the device is not selected via the SS/CS pin.
You should also note the SPI slave devices that do not have a tri-state mode on their MISO pins, should not be used on the same bus as devices that have without using an external tri-state buffer circuit between the non-tristate device and the rest of the devices on the MISO bus.
It is possible to connect multiple SPI slave devices to on Master device if you remember that each slave device will need its own dedicated SS/CS pin on the master. This can however quickly use a lot of IO pins on a microcontroller, thus being one of the disadvantages of SPI versus I2C. SPI is however quite a bit faster than I2C.
Data Transmission
To begin communication, the bus master configures the clock, using a frequency supported by the slave device, typically up to a few MHz. The master then selects the slave device with a logic level 0 on the select line. If a waiting period is required, such as for an analog-to-digital conversion, the master must wait for at least that period of time before issuing clock cycles.
During each SPI clock cycle, full-duplex data transmission occurs. The master sends a bit on the MOSI line and the slave reads it, while the slave sends a bit on the MISO line and the master reads it. This sequence is maintained even when only one-directional data transfer is intended.
Transmissions normally involve two shift registers of some given word-size, such as eight bits, one in the master and one in the slave; they are connected in a virtual ring topology. Data is usually shifted out with the most significant bit first. On the clock edge, both master and slave shift out a bit and output it on the transmission line to the counterpart. On the next clock edge, at each receiver the bit is sampled from the transmission line and set as a new least-significant bit of the shift register. After the register bits have been shifted out and in, the master and slave have exchanged register values. If more data needs to be exchanged, the shift registers are reloaded and the process repeats. Transmission may continue for any number of clock cycles. When complete, the master stops toggling the clock signal, and typically deselects the slave.
Transmissions often consist of eight-bit words. However, other word-sizes are also common, for example, sixteen-bit words for touch-screen controllers or audio codecs, such as the TSC2101 by Texas Instruments, or twelve-bit words for many digital-to-analogue or analogue-to-digital converters.
Every slave on the bus that has not been activated using its chip select line must disregard the input clock and MOSI signals and should not drive MISO (I.E. must have a tri-state output) although some devices need external tri-state buffers to implement this.
Clock polarity and phasing
In addition to setting the clock frequency, the master must also configure the clock polarity and phase with respect to the data. Motorola SPI Block Guide names these two options as CPOL and CPHA (for clock polarity and phase) respectively, a convention most vendors have also adopted.
The timing diagram is shown below. The timing is further described below and applies to both the master and the slave device.
CPOL determines the polarity of the clock. The polarities can be converted with a simple inverter.
CPOL=0 is a clock which idles at 0, and each cycle consists of a pulse of 1. That is, the leading edge is a rising edge, and the trailing edge is a falling edge.
CPOL=1 is a clock which idles at 1, and each cycle consists of a pulse of 0. That is, the leading edge is a falling edge, and the trailing edge is a rising edge.
CPHA determines the timing (i.e. phase) of the data bits relative to the clock pulses. Conversion between these two forms is non-trivial.
For CPHA=0, the “out” side changes the data on the trailing edge of the preceding clock cycle, while the “in” side captures the data on (or shortly after) the leading edge of the clock cycle. The out-side holds the data valid until the trailing edge of the current clock cycle. For the first cycle, the first bit must be on the MOSI line before the leading clock edge.
An alternative way of considering it is to say that a CPHA=0 cycle consists of a half cycle with the clock idle, followed by a half cycle with the clock asserted.
For CPHA=1, the “out” side changes the data on the leading edge of the current clock cycle, while the “in” side captures the data on (or shortly after) the trailing edge of the clock cycle. The out-side holds the data valid until the leading edge of the following clock cycle. For the last cycle, the slave holds the MISO line valid until slave select is de-selected.
An alternative way of considering it is to say that a CPHA=1 cycle consists of a half cycle with the clock asserted, followed by a half cycle with the clock idle.
The MOSI and MISO signals are usually stable (at their reception points) for the half cycle until the next clock transition. SPI master and slave devices may well sample data at different points in that half cycle.
This adds more flexibility to the communication channel between the master and slave.
Mode numbers
The combinations of polarity and phases are often referred to as modes which are commonly numbered according to the following convention, with CPOL as the high order bit and CPHA as the low order bit:
For “Microchip PIC” / “ARM-based” microcontrollers (note that NCPHA is the inversion of CPHA):
SPI mode
Clock polarity (CPOL/CKP)
Clock phase (CPHA)
Clock edge (CKE/NCPHA)
0
0
0
1
1
0
1
0
2
1
0
1
3
1
1
0
For PIC32MX: SPI mode configure CKP, CKE and SMP bits. Set SMP bit and CKP, CKE two bits configured as above table.
Mode
CPOL
CPHA
0
0
0
1
0
1
2
1
0
3
1
1
For other microcontrollers:
Another commonly used notation represents the mode as a (CPOL, CPHA) tuple; e.g., the value ‘(0, 1)’ would indicate CPOL=0 and CPHA=1.
Note that in Full Duplex operation, the Master device could transmit and receive with different modes. For instance, it could transmit in Mode 0 and be receiving in Mode 1 at the same time.
Independent Slave Configuration
In the independent slave configuration, there is an independent chip select line for each slave. This is the way SPI is normally used. The master asserts only one chip select at a time.
Pull-up resistors between the power source and chip select lines are recommended for systems where the master’s chip select pins may default to an undefined state. When separate software routines initialize each chip select and communicate with its slave, pull-up resistors prevent other uninitialized slaves from responding.
Since the MISO pins of the slaves are connected together, they are required to be tri-state pins (high, low or high-impedance), where the high-impedance output must be applied when the slave is not selected. Slave devices not supporting tri-state may be used in independent slave configuration by adding a tri-state buffer chip controlled by the chip select signal. (Since only a single signal line needs to be tri-stated per slave, one typical standard logic chip that contains four tristate buffers with independent gate inputs can be used to interface up to four slave devices to an SPI bus.)
Daisy chain configuration
Some products that implement SPI may be connected in a daisy chain configuration, the first slave output being connected to the second slave input, etc. The SPI port of each slave is designed to send out during the second group of clock pulses an exact copy of the data it received during the first group of clock pulses. The whole chain acts as a communication shift register; daisy chaining is often done with shift registers to provide a bank of inputs or outputs through SPI. Each slave copies input to output in the next clock cycle until the active low SS line goes high. Such a feature only requires a single SS line from the master, rather than a separate SS line for each slave.
Note that not all SPI devices support this. You should thus check your datasheet before using this configuration!
Valid Communications
Some slave devices are designed to ignore any SPI communications in which the number of clock pulses is greater than specified. Others do not care, ignoring extra inputs and continuing to shift the same output bit. It is common for different devices to use SPI communications with different lengths, as, for example, when SPI is used to access the scan chain of a digital IC by issuing a command word of one size (perhaps 32 bits) and then getting a response of a different size (perhaps 153 bits, one for each pin in that scan chain).
Interrupts
SPI devices sometimes use another signal line to send an interrupt signal to a host CPU. Examples include pen-down interrupts from touchscreen sensors, thermal limit alerts from temperature sensors, alarms issued by real-time clock chips, SDIO, and headset jack insertions from the sound codec in a cell phone. Interrupts are not covered by the SPI standard; their usage is neither forbidden nor specified by the standard. In other words, interrupts are outside the scope of the SPI standard and are optionally implemented independently from it.
Bit Banging a SPI Master – Example code
Below is an example of bit-banging the SPI protocol as an SPI master with CPOL=0, CPHA=0, and eight bits per transfer. The example is written in the C programming language. Because this is CPOL=0 the clock must be pulled low before the chip select is activated. The chip select line must be activated, which normally means being toggled low, for the peripheral before the start of the transfer, and then deactivated afterwards. Most peripherals allow or require several transfers while the select line is low; this routine might be called several times before deselecting the chip.
/*
* Simultaneously transmit and receive a byte on the SPI.
*
* Polarity and phase are assumed to be both 0, i.e.:
* - input data is captured on rising edge of SCLK.
* - output data is propagated on falling edge of SCLK.
*
* Returns the received byte.
*/
uint8_t SPI_transfer_byte(uint8_t byte_out)
{
uint8_t byte_in = 0;
uint8_t bit;
for (bit = 0x80; bit; bit >>= 1) {
/* Shift-out a bit to the MOSI line */
write_MOSI((byte_out & bit) ? HIGH : LOW);
/* Delay for at least the peer's setup time */
delay(SPI_SCLK_LOW_TIME);
/* Pull the clock line high */
write_SCLK(HIGH);
/* Shift-in a bit from the MISO line */
if (read_MISO() == HIGH)
byte_in |= bit;
/* Delay for at least the peer's hold time */
delay(SPI_SCLK_HIGH_TIME);
/* Pull the clock line low */
write_SCLK(LOW);
}
return byte_in;
}
This concludes part 1 of my series on SPI. I hope you found it interesting and useful.
Adding a display to any project can instantly increase its visual appeal, as well as make the project easier to control. Displays available to Electronic enthusiasts mostly include some sort of LCD or even TFT display. LCD displays are usually bulky and very limited in their ability to display a lot of information, whereas TFT type displays are still a bit on the expensive side, and not very easy to interface with for the beginner.
Today, I would like to introduce a different type of display, which is available in an I2C as well as SPI version. These displays are very easily readable in almost any light conditions, lightweight, and most importantly, they are extremely cheap. I am talking about the OLED display of course… Many of us may already have one of them in our mobile phones, or even TV screen…
There are two main families of OLED: those based on small molecules and those employing polymers. Adding mobile ions to an OLED creates a light-emitting electrochemical cell (LEC) which has a slightly different mode of operation. An OLED display can be driven with a passive-matrix (PMOLED) or active-matrix (AMOLED) control scheme. In the PMOLED scheme, each row (and line) in the display is controlled sequentially, one by one,[6] whereas AMOLED control uses a thin-film transistor backplane to directly access and switch each individual pixel on or off, allowing for higher resolution and larger display sizes.
An OLED display works without a backlight because it emits visible light. Thus, it can display deep black levels and can be thinner and lighter than a liquid crystal display (LCD). In low ambient light conditions (such as a dark room), an OLED screen can achieve a higher contrast ratio than an LCD, regardless of whether the LCD uses cold cathode fluorescent lamps or an LED backlight. OLED displays are made in the same way as LCDs, but after TFT (for active matrix displays), addressable grid (for passive matrix displays) or ITO segment (for segment displays) formation, the display is coated with hole injection, transport and blocking layers, as well with electroluminescent material after the 2 first layers, after which ITO or metal may be applied again as a cathode and later the entire stack of materials is encapsulated. The TFT layer, addressable grid or ITO segments serve as or are connected to the anode, which may be made of ITO or metal.[7][8] OLEDs can be made flexible and transparent, with transparent displays being used in smartphones with optical fingerprint scanners and flexible displays being used in foldable smartphones.
The full article is available here if you are interested.
Connecting the circuit
This display is once again extremely easy to connect, as it uses the very versatile I2C protocol. (An SPI version is also available).
Connect the following wires to the Arduino / ESP32 +5v (red) to the VCC pin on the display Gnd to Gnd SDA (A4 on Uno) to SDA, and SCL (A5 on Uno) to SCL
The Software Libraries
The 128×32 OLED display that we will be using today, is based on the SSD1306. We will thus be using a library suplied by Adafruit to interface with this chip. There are various other libraries available, but I have found the Adafruit library the most stable.
To load this, start by opening the Arduino IDE, and go to the Sketch->Include Library->Manage Libraries option on the menu
The Library Manager will now open
We need to install two (2) Libraries
– Adafruit GFX ( this is for graphics) – Adafruit SSD1306 ( to control the actual display )
Click on “Close” after installation is completed.
Using the display
We will use one of the standard Adafruit examples to show you the capabilities of the tiny little screen. The example are so straight forward to use, that I find it unnecessary to say anything else about it 🙂
Open the ssd1306_128x32_ic2 Example from the Examples menu in the Arduino IDE and upload it to your Arduino, making sure that you set the dimensions of your screen first (in my case 128×32 )
/**************************************************************************
This is an example for our Monochrome OLEDs based on SSD1306 drivers
Pick one up today in the adafruit shop!
------> http://www.adafruit.com/category/63_98
This example is for a 128x32 pixel display using I2C to communicate
3 pins are required to interface (two I2C and one reset).
Adafruit invests time and resources providing this open
source code, please support Adafruit and open-source
hardware by purchasing products from Adafruit!
Written by Limor Fried/Ladyada for Adafruit Industries,
with contributions from the open source community.
BSD license, check license.txt for more information
All text above, and the splash screen below must be
included in any redistribution.
**************************************************************************/
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET 4 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define NUMFLAKES 10 // Number of snowflakes in the animation example
#define LOGO_HEIGHT 16
#define LOGO_WIDTH 16
static const unsigned char PROGMEM logo_bmp[] =
{ B00000000, B11000000,
B00000001, B11000000,
B00000001, B11000000,
B00000011, B11100000,
B11110011, B11100000,
B11111110, B11111000,
B01111110, B11111111,
B00110011, B10011111,
B00011111, B11111100,
B00001101, B01110000,
B00011011, B10100000,
B00111111, B11100000,
B00111111, B11110000,
B01111100, B11110000,
B01110000, B01110000,
B00000000, B00110000 };
void setup() {
Serial.begin(9600);
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
// Show initial display buffer contents on the screen --
// the library initializes this with an Adafruit splash screen.
display.display();
delay(2000); // Pause for 2 seconds
// Clear the buffer
display.clearDisplay();
// Draw a single pixel in white
display.drawPixel(10, 10, SSD1306_WHITE);
// Show the display buffer on the screen. You MUST call display() after
// drawing commands to make them visible on screen!
display.display();
delay(2000);
// display.display() is NOT necessary after every single drawing command,
// unless that's what you want...rather, you can batch up a bunch of
// drawing operations and then update the screen all at once by calling
// display.display(). These examples demonstrate both approaches...
testdrawline(); // Draw many lines
testdrawrect(); // Draw rectangles (outlines)
testfillrect(); // Draw rectangles (filled)
testdrawcircle(); // Draw circles (outlines)
testfillcircle(); // Draw circles (filled)
testdrawroundrect(); // Draw rounded rectangles (outlines)
testfillroundrect(); // Draw rounded rectangles (filled)
testdrawtriangle(); // Draw triangles (outlines)
testfilltriangle(); // Draw triangles (filled)
testdrawchar(); // Draw characters of the default font
testdrawstyles(); // Draw 'stylized' characters
testscrolltext(); // Draw scrolling text
testdrawbitmap(); // Draw a small bitmap image
// Invert and restore display, pausing in-between
display.invertDisplay(true);
delay(1000);
display.invertDisplay(false);
delay(1000);
testanimate(logo_bmp, LOGO_WIDTH, LOGO_HEIGHT); // Animate bitmaps
}
void loop() {
}
void testdrawline() {
int16_t i;
display.clearDisplay(); // Clear display buffer
for(i=0; i<display.width(); i+=4) {
display.drawLine(0, 0, i, display.height()-1, SSD1306_WHITE);
display.display(); // Update screen with each newly-drawn line
delay(1);
}
for(i=0; i<display.height(); i+=4) {
display.drawLine(0, 0, display.width()-1, i, SSD1306_WHITE);
display.display();
delay(1);
}
delay(250);
display.clearDisplay();
for(i=0; i<display.width(); i+=4) {
display.drawLine(0, display.height()-1, i, 0, SSD1306_WHITE);
display.display();
delay(1);
}
for(i=display.height()-1; i>=0; i-=4) {
display.drawLine(0, display.height()-1, display.width()-1, i, SSD1306_WHITE);
display.display();
delay(1);
}
delay(250);
display.clearDisplay();
for(i=display.width()-1; i>=0; i-=4) {
display.drawLine(display.width()-1, display.height()-1, i, 0, SSD1306_WHITE);
display.display();
delay(1);
}
for(i=display.height()-1; i>=0; i-=4) {
display.drawLine(display.width()-1, display.height()-1, 0, i, SSD1306_WHITE);
display.display();
delay(1);
}
delay(250);
display.clearDisplay();
for(i=0; i<display.height(); i+=4) {
display.drawLine(display.width()-1, 0, 0, i, SSD1306_WHITE);
display.display();
delay(1);
}
for(i=0; i<display.width(); i+=4) {
display.drawLine(display.width()-1, 0, i, display.height()-1, SSD1306_WHITE);
display.display();
delay(1);
}
delay(2000); // Pause for 2 seconds
}
void testdrawrect(void) {
display.clearDisplay();
for(int16_t i=0; i<display.height()/2; i+=2) {
display.drawRect(i, i, display.width()-2*i, display.height()-2*i, SSD1306_WHITE);
display.display(); // Update screen with each newly-drawn rectangle
delay(1);
}
delay(2000);
}
void testfillrect(void) {
display.clearDisplay();
for(int16_t i=0; i<display.height()/2; i+=3) {
// The INVERSE color is used so rectangles alternate white/black
display.fillRect(i, i, display.width()-i*2, display.height()-i*2, SSD1306_INVERSE);
display.display(); // Update screen with each newly-drawn rectangle
delay(1);
}
delay(2000);
}
void testdrawcircle(void) {
display.clearDisplay();
for(int16_t i=0; i<max(display.width(),display.height())/2; i+=2) {
display.drawCircle(display.width()/2, display.height()/2, i, SSD1306_WHITE);
display.display();
delay(1);
}
delay(2000);
}
void testfillcircle(void) {
display.clearDisplay();
for(int16_t i=max(display.width(),display.height())/2; i>0; i-=3) {
// The INVERSE color is used so circles alternate white/black
display.fillCircle(display.width() / 2, display.height() / 2, i, SSD1306_INVERSE);
display.display(); // Update screen with each newly-drawn circle
delay(1);
}
delay(2000);
}
void testdrawroundrect(void) {
display.clearDisplay();
for(int16_t i=0; i<display.height()/2-2; i+=2) {
display.drawRoundRect(i, i, display.width()-2*i, display.height()-2*i,
display.height()/4, SSD1306_WHITE);
display.display();
delay(1);
}
delay(2000);
}
void testfillroundrect(void) {
display.clearDisplay();
for(int16_t i=0; i<display.height()/2-2; i+=2) {
// The INVERSE color is used so round-rects alternate white/black
display.fillRoundRect(i, i, display.width()-2*i, display.height()-2*i,
display.height()/4, SSD1306_INVERSE);
display.display();
delay(1);
}
delay(2000);
}
void testdrawtriangle(void) {
display.clearDisplay();
for(int16_t i=0; i<max(display.width(),display.height())/2; i+=5) {
display.drawTriangle(
display.width()/2 , display.height()/2-i,
display.width()/2-i, display.height()/2+i,
display.width()/2+i, display.height()/2+i, SSD1306_WHITE);
display.display();
delay(1);
}
delay(2000);
}
void testfilltriangle(void) {
display.clearDisplay();
for(int16_t i=max(display.width(),display.height())/2; i>0; i-=5) {
// The INVERSE color is used so triangles alternate white/black
display.fillTriangle(
display.width()/2 , display.height()/2-i,
display.width()/2-i, display.height()/2+i,
display.width()/2+i, display.height()/2+i, SSD1306_INVERSE);
display.display();
delay(1);
}
delay(2000);
}
void testdrawchar(void) {
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(SSD1306_WHITE); // Draw white text
display.setCursor(0, 0); // Start at top-left corner
display.cp437(true); // Use full 256 char 'Code Page 437' font
// Not all the characters will fit on the display. This is normal.
// Library will draw what it can and the rest will be clipped.
for(int16_t i=0; i<256; i++) {
if(i == '\n') display.write(' ');
else display.write(i);
}
display.display();
delay(2000);
}
void testdrawstyles(void) {
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(SSD1306_WHITE); // Draw white text
display.setCursor(0,0); // Start at top-left corner
display.println(F("Hello, world!"));
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Draw 'inverse' text
display.println(3.141592);
display.setTextSize(2); // Draw 2X-scale text
display.setTextColor(SSD1306_WHITE);
display.print(F("0x")); display.println(0xDEADBEEF, HEX);
display.display();
delay(2000);
}
void testscrolltext(void) {
display.clearDisplay();
display.setTextSize(2); // Draw 2X-scale text
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 0);
display.println(F("scroll"));
display.display(); // Show initial text
delay(100);
// Scroll in various directions, pausing in-between:
display.startscrollright(0x00, 0x0F);
delay(2000);
display.stopscroll();
delay(1000);
display.startscrollleft(0x00, 0x0F);
delay(2000);
display.stopscroll();
delay(1000);
display.startscrolldiagright(0x00, 0x07);
delay(2000);
display.startscrolldiagleft(0x00, 0x07);
delay(2000);
display.stopscroll();
delay(1000);
}
void testdrawbitmap(void) {
display.clearDisplay();
display.drawBitmap(
(display.width() - LOGO_WIDTH ) / 2,
(display.height() - LOGO_HEIGHT) / 2,
logo_bmp, LOGO_WIDTH, LOGO_HEIGHT, 1);
display.display();
delay(1000);
}
#define XPOS 0 // Indexes into the 'icons' array in function below
#define YPOS 1
#define DELTAY 2
void testanimate(const uint8_t *bitmap, uint8_t w, uint8_t h) {
int8_t f, icons[NUMFLAKES][3];
// Initialize 'snowflake' positions
for(f=0; f< NUMFLAKES; f++) {
icons[f][XPOS] = random(1 - LOGO_WIDTH, display.width());
icons[f][YPOS] = -LOGO_HEIGHT;
icons[f][DELTAY] = random(1, 6);
Serial.print(F("x: "));
Serial.print(icons[f][XPOS], DEC);
Serial.print(F(" y: "));
Serial.print(icons[f][YPOS], DEC);
Serial.print(F(" dy: "));
Serial.println(icons[f][DELTAY], DEC);
}
for(;;) { // Loop forever...
display.clearDisplay(); // Clear the display buffer
// Draw each snowflake:
for(f=0; f< NUMFLAKES; f++) {
display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SSD1306_WHITE);
}
display.display(); // Show the display buffer on the screen
delay(200); // Pause for 1/10 second
// Then update coordinates of each flake...
for(f=0; f< NUMFLAKES; f++) {
icons[f][YPOS] += icons[f][DELTAY];
// If snowflake is off the bottom of the screen...
if (icons[f][YPOS] >= display.height()) {
// Reinitialize to a random position, just off the top
icons[f][XPOS] = random(1 - LOGO_WIDTH, display.width());
icons[f][YPOS] = -LOGO_HEIGHT;
icons[f][DELTAY] = random(1, 6);
}
}
}
}
I hope that you find this useful and inspiring. Thank you
Today I will continue my series on I2C by showing you how to use multiple devices on the I2C bus. This will be an extremely short post, as it builds on skills that we have already covered.
All of these devices will be controlled from Arduino Uno, using the following libraries
LiquidCrystal_I2C.h to control the LCD screen, Wire.h and PCF8574.h to control the I2C IO extenders and Adafruit_GFX, Adafruit_SSD1306.h and SPI.h to control the SSD1306 128×32 OLED display.
With DuPont wires and breadboards being the reliable things they are, I decided that, after initial testing, I will not show you how to do button inputs on the PCF8574 at this stage. The amount of stray capacitance floating around on the breadboards, and small momentary push-button switches, made for a very impressive but unreliable mess of wires, with no real learning value to it 😉 Maybe some more on that later when I do a decent real-world example using these technologies 🙂
As the total distance between the devices is relatively short, it was not necessary to use pull-up resistors on the I2C bus in my setup. I suspect that that is due to the fact that they may already be included on some of my devices.
The circuit is quite straight forward.
Connect all SDA pins on the I2C devices together serially, and connect that to the Arduino SDA pin ( That is usually A4)
Connect all SCL pins on the I2C devices together serially, and connect that to the Arduino SCL pin ( That is usually A5)
A note: On my Uno clone, there is an additional I2C breakout at the top of the device, near the USB adapter. I chose to use that as well as A4 and A5, as the bus hung itself up when connected to the breadboard. Your mileage may vary on this one 🙂
Connect all 5v (Vcc) lines to 5v on the Arduino, and all Ground (GND) lines to GND on the Arduino.
Now connect 4 LEDs, through a suitable resistor ( 640 ohms up to 1k ohm ) to pin P0 and P1 on both of the PCF8574 IO extenders. Also, connect the other leg of the LED to ground.
I have powered my Uno from an external 5v power supply, as I did not want to pull too much current from the regulator on the actual Uno clone.
That should complete your hardware setup. Double check all your connections, and then load the i2c scanner sketch in the Arduino IDE, you may find it under the examples for the Wire.h library.
Power up the circuit, and upload the sketch to the Uno. Open the Serial Monitor.
You should see 4 I2C devices being detected. Note their addresses. If you dont see 4 devices, check your wiring and addresses. You may have a device with a conflicting address or a bad connection. If you used the breadboard to connect the bus, chances are very good that you will not see all the devices.
Good, if all of that is working, copy paste the following code into a new Arduino IDE window. I will explain the code in the section below:
/*
Multiple devices on the I2C bus
Maker and Iot Ideas, MakerIoT2020
*/
// Include the libraries that we will need
#include <SPI.h> // needed for OLED display.
#include <PCF8574.h> // PCF8574
#include <Wire.h> // Generic I2C library
#include <Adafruit_GFX.h> // for OLED display
#include <Adafruit_SSD1306.h> // for OLED display
#include <LiquidCrystal_I2C.h> // For I2C LCD display
// we need to define the size of the OLED screen
#define OLED_WIDTH 128
#define OLED_HEIGHT 32
// mine does not have an onboard reset pin. If yours do, specify the
// pin that it is connected to on the Arduino here. To use the
// Arduino reset pin, specify -1 as below
#define OLED_RESET -1
// Define the OLED display, width,hight protocol and reset pin
Adafruit_SSD1306 oled(OLED_WIDTH,OLED_HEIGHT, &Wire, OLED_RESET);
// Define the I2C LCD screen address and pin configuration
LiquidCrystal_I2C lcd(0x27,2,1,0,4,5,6,7,3,POSITIVE);
// Define the PCF8574 devices ( you can have up to 8 on a bus )
// but in this case, my LCD uses address 0x27, so I will have a
// conflicting address if I were to use 8 of them together with the
// LCD
PCF8574 Remote_1(0x20);
PCF8574 Remote_2(0x21);
// Note the I2C addresses. You can obtain them from the i2c_scanner
void setup() {
// serial debugging if needed
Serial.begin(115200);
// Start OLED Display Init
if (!oled.begin(SSD1306_SWITCHCAPVCC,0x3C)) { // Init the OLED
Serial.println(F("OLED INIT FAILED"));
for(;;); // Dont proceed ... loop forever
}
oled.display();
delay(2000); // This delay is required to give display time to
// initialise properly
oled.clearDisplay();
oled.setTextSize(0);
oled.setTextColor(SSD1306_WHITE);
oled.setCursor(0,0);
oled.println("TEST SCREEN");
oled.display();
delay(2000);
oled.clearDisplay();
oled.setCursor(1,0);
oled.println("OLED SCREEN ON");
oled.display();
// Start the LCD
lcd.begin(16,2);
// Set the initial state of the pins on the PCF8574 devices
// I found that the PCF8574 library sometimes does funny things
// This is also an example of how to use native i2c to set the
// status of the pins
Wire.begin();
Wire.beginTransmission(0x20); // device 1
Wire.write(0x00); // all ports off
Wire.endTransmission();
Wire.begin();
Wire.beginTransmission(0x21); // device 2
Wire.write(0x00); // all ports off
Wire.endTransmission();
// Set pinModes for PCF8574 devices
// Note that there are two of them
Remote_1.pinMode(P0,OUTPUT);
Remote_1.pinMode(P1,OUTPUT);
Remote_2.pinMode(P0,OUTPUT);
Remote_2.pinMode(P1,OUTPUT);
// Start both IO extenders
Remote_1.begin();
Remote_2.begin();
// and set ports to low on both
// you may find that if you ommit this step, they come up in an
// unstable state.
Remote_1.digitalWrite(P0,LOW);
Remote_1.digitalWrite(P1,LOW);
Remote_2.digitalWrite(P0,LOW);
Remote_2.digitalWrite(P1,LOW);
}
void loop() {
// Draw a character map on the OLED display.
// This function is borrowed from the Adafruit library
testdrawchar();
// Write to the IO extenders
Remote_1.digitalWrite(P0,HIGH);
Remote_1.digitalWrite(P1,LOW);
Remote_2.digitalWrite(P0,HIGH);
Remote_2.digitalWrite(P1,LOW);
// Display their status on the LCD
lcd.setCursor(0,0);
lcd.print(" R1 P0=1 P1=0");
lcd.setCursor(0,1);
lcd.print(" R2 P0=1 P1=0");
delay(500);
// Change status
Remote_1.digitalWrite(P1,HIGH);
Remote_1.digitalWrite(P0,LOW);
Remote_2.digitalWrite(P1,HIGH);
Remote_2.digitalWrite(P0,LOW);
// Update LCD
lcd.setCursor(0,0);
lcd.print(" R1 P0=0 P1=1");
lcd.setCursor(0,1);
lcd.print(" R2 P0=0 P1=1");
delay(500);
// Do some graphics on the OLED display
// Function borrowed from Adafruit
testdrawrect();
oled.clearDisplay();
delay(500);
// repeat indefinitely
}
void testdrawrect(void) {
oled.clearDisplay();
for(int16_t i=0; i<oled.height()/2; i+=2) {
oled.drawRect(i, i, oled.width()-2*i, oled.height()-2*i, SSD1306_WHITE);
oled.display(); // Update screen with each newly-drawn rectangle
delay(1);
}
delay(500);
}
void testdrawchar(void) {
oled.clearDisplay();
oled.setTextSize(1); // Normal 1:1 pixel scale
oled.setTextColor(SSD1306_WHITE); // Draw white text
oled.setCursor(0, 0); // Start at top-left corner
oled.cp437(true); // Use full 256 char 'Code Page 437' font
// Not all the characters will fit on the display. This is normal.
// Library will draw what it can and the rest will be clipped.
for(int16_t i=0; i<256; i++) {
if(i == '\n') oled.write(' ');
else oled.write(i);
}
oled.display();
delay(500);
}
This concludes a quick and dirty show and tell… I hope that it will stimulate questions and ideas for a lot of people.
In our last post, we started looking at the workings of the I2C protocol. In case you missed that, you can read about that here. Today, I will continue with I2C by showing you how to implement the protocol between an Maker Nano (a Arduino Nano Clone) and Kid-Bright32 (a ESP32 based development and Education device).
This project will eventually be turned into an IoT device, with Google Assistant voice control. The Kid-Bright have very limited IO pins, and the Maker Nano has no Network Connectivity unless we use an Ethernet Shield. Enough of that for now, let us start with today’s tutorial, we will get back to this project later…
The Code for the Master
/* I2C Master Code - Kid Bright 32 v 1.3
Can be adapted to Arduino or NodeMCU or STM32
As it uses no special libraries, only the standard Wire.h
that is already included with the Arduino IDE
*/
#include <Wire.h>
#define button1 16 // Button 1
#define button2 14 // Button 2
#define led_blue 17 // Led 1
#define led_red 2 // Led 2
#define led_yellow 15 // Led 3
void setup() {
Wire.begin(); // Start I2C, join bus as a Master
pinMode(button1,INPUT_PULLUP); // Set as input
pinMode(button2,INPUT_PULLUP);
pinMode(led_blue,OUTPUT); // Set as output
pinMode(led_red,OUTPUT);
pinMode(led_yellow,OUTPUT);
digitalWrite(led_blue,HIGH); // LED is Active Low
digitalWrite(led_red,HIGH); // LED is Active Low
digitalWrite(led_yellow,HIGH); // LED is Active Low
Serial.begin(115200); // Start Serial for debugging
}
byte Data = 0; // Variable for sending data to the slave
int SlaveData = 0; // Variable for receiving data from the slave
void loop() {
Wire.beginTransmission(4); // Send data to Slave at address #4
Wire.write(Data); // Transmit one byte ( 8 bits)
Wire.endTransmission(); // End Transmission
Wire.requestFrom(4,1); // Request one (1) byte of data from the //slave at address$
while (Wire.available()) { // If data received
SlaveData = Wire.read(); // Save it into a variable
}
// We will implement a simple latch in software, where a single
// button latches or releases a bit with every press and release.
// This code should ideally include debouncing as well. It was left
// out for clarity.
if (digitalRead(button1) == LOW) { // If button 1 pressed
if (bitRead(Data,0) == HIGH) { // test if bit 0 in variable is set
bitClear(Data,0); // clear it if it is set
} else {
bitSet(Data,0) == HIGH; // set bit to high
}
}
// Do the same for the second button
if (digitalRead(button2) == LOW) {
if (bitRead(Data,1) == HIGH) {
bitClear(Data,1);
} else {
bitSet(Data,1) == HIGH;
}
}
// We will test for a set bit in the transmitted byte, and invert it,
// as the LED's are active LOW
digitalWrite(led_blue,!bitRead(Data,0)); // Toggle Led 1
digitalWrite(led_red,!bitRead(Data,1)); // Toggle Led 2
// Same with the data received from the slave
digitalWrite(led_yellow,!bitRead(SlaveData,0)); // Toggle Led 3
// Print Debug info on serial port
Serial.print("Send to Slave 0xb");
Serial.println(Data,BIN);
Serial.print("Received from Slave 0xb");
Serial.println(SlaveData,BIN);
// Small delay, should change to millis in production code
delay(200);
}
The Master Device. Connections are +5v (red) SCL (brown) SDA (orange) Ground (blue)
Code for the Slave
/*
I2C Slave
Arduino Nano or Compatible, can be used with ESP32 or STM32
as well as no special libraries used, only standard Wire.h
*/
#include <Wire.h>
#define button 2 // A user button
#define led1 3 // Led 1
#define led2 4 // Led 2
byte Data = 0; // Variable for sending data to the Master
void setup()
{
pinMode(button,INPUT_PULLUP); // Set as Input
pinMode(led1,OUTPUT);
pinMode(led2,OUTPUT);
digitalWrite(led1,LOW); // Led is Active High, so switch it off
digitalWrite(led2,LOW);
Wire.begin(4); // Join I2C Bus as device #4
Wire.onReceive(receiveEvent); // Register receive Event
Wire.onRequest(requestEvent); // Register request event
Serial.begin(115200); // start serial debugging
}
void loop() {
// implement a software bit latch, on bit 0 of the Data variable
// the latch is toggled by pressing and releasing the button
// should ideally be debounced as well
if (digitalRead(button) == LOW) {
if (bitRead(Data,0) == HIGH) {
bitClear(Data,0);
} else {
bitSet(Data,0) == HIGH;
}
}
delay(200); // small delay
}
// This event will be triggered when the master requests data
void requestEvent()
{
Wire.write(Data); // Send data to the master
Serial.print("Sending to Master 0xb");
Serial.println(Data,BIN);
}
// This event gets triggered when the master sends data
void receiveEvent(int Quantity)
{
int x = Wire.read(); // read the data ( one byte in this case)
digitalWrite(led1,bitRead(x,0)); // Toggle LED 1 on Bit 0 state
digitalWrite(led2,bitRead(x,1)); // Toggle LED 2 on Bit 1 state
Serial.print("Received from Master 0xb"); // Debugginh
Serial.println(x,BIN);
}
Maker Nano on an IO Shield, Connections are +5v (red) SCL (brown) SDA (orange) Ground (blue)
How does this work.
After uploading the code to the two boards, and connecting the boards to the I2C bus, we may power everything up. Please note that the boards MUST have a common ground. I have also powered both from the same supply. also make sure the SDA goes to SDA, and SCL to SCL… On a short distance like this, pull-up resistors are not required ( your milage may vary )
When we first power it up, is will seem as if nothing happened, but if you press and release one of the switches, the LED’s will light up, and stay lit until you press the switch again.
Some Pictures showing the project in action
What next ?
In further parts of this, we will expand on this device, turning it into an IoT device, by combining many different skills that I have presented in previous tutorials.
In this post, I will tell you all the basics of the I2C protocol. What it is, where it comes from and also how it is configured and setup. We will also look at how data is transferred and received
I2C communication is the short form name for inter-integrated circuit protocol. It is a communication protocol developed by Philips Semiconductors for the transfer of data between a central processor and multiple integrated circuits on the same circuit board by using just two common wires.
Due to its simplicity, it is widely adopted for communication between microcontrollers and sensor arrays, displays, IoT devices, EEPROMs etc.
This is a synchronous serial communication protocol. It means that data bits are transferred one by one at regular intervals of time set by a reference clock line.
The Features of I2C
The I2C protocol has the following important features
Only two common bus lines (wires) are required to control any device/IC on the I2C network.
There is no need for a prior agreement on data transfer rate like in UART communications. The data transfer speed can thus be adjusted whenever it is required.
It has a simple mechanism for validating the transferred data.
It uses a 7-bit addressing system to target a specific device/IC on the I2C bus.
I2C networks are extremely easy to scale. New devices can simply be connected to the two common I2C bus lines.
The Hardware
The physical I2C Bus
The I2C Bus (Interface wires) consists of just two wires and are named Serial Clock Line (SCL) and Serial Data Line (SDA). The data to be transferred is sent through the SDA wire and is synchronized with the clock signal from SCL. All the devices/ICs on the I2C network are connected to the same SCL and SDA lines as shown in the image below:
The physical I2C Bus. All devices are connected to the same 2 wired on the bus, namely SDA and SCL
Both the I2C bus lines (SDA, SCL) are operated as in open-drain driver mode. It means that any device/IC on the I2C network can drive(pull) SDA and SCL low, but they cannot drive them high. So, a pull-up resistor is used on each bus line, to keep them high (at positive voltage) by default.
This is to prevent the bus from shorting, which might happen when one device tries to pull the line high and some other device tries to pull the line low.
The Master and Slave Devices on the I2C Bus
The devices connected to the I2C bus are categorized as either masters or slaves. At any instant of time, only a single master stays active on the I2C bus. It controls the SCL clock line and decides what operation is to be done on the SDA data line.
All the devices that respond to instructions from this master device are slaves. For differentiating between multiple slave devices connected to the same I2C bus, each slave device is physically assigned a permanent 7-bit address.
When a master device wants to transfer data to or from a slave device, it specifies this particular slave device address on the SDA line and then proceeds with the transfer. So effectively communication takes place between the master device and a particular slave device.
All the other slave devices don’t respond unless their address is specified by the master device on the SDA line.
The Master and Slave Devices on the I2C Bus. Note that each Slave device has it’s own address.
The Data Transfer Protocol
The protocol (set of rules) that is followed by the master device and slave devices for the transfer of data between them works as follows:
Data is transferred between the master device and slave devices through the SDA data line, via patterned sequences of 0’s and 1’s (bits). Each sequence of 0’s and 1’s is called a transaction and each data transaction is structured as in the image below:
The structure of an I2C Data transaction
The Start Condition
Whenever a master device/IC decides to start a transaction, it switches the SDA line from a high level to a low level before the SCL line switches from high to low.
Once a start condition is sent by the master device, all the slave devices get active even if they were in sleep mode, and wait for the address bits to see which device should respond.
The I2C Start Condition. Note that SDA Switches LOW before SCL. All slave devices on the bus will now listen for an address bit to decide which device should respond.
The Address Block
The Address block is comprised of 7 bits and are filled with the address of slave device (in binary) to/from which the master device needs to send/receive data. All the slave devices on the I2C bus will compare these address bits with their own address.
The Read/Write Bit
This bit specifies the direction that the data must be transferred in. If the master device/IC needs to send data to a slave device, this bit is set to ‘0’. If the master device/IC needs to receive data from the slave device, it is set to ‘1’.
The ACK/NACK Bit
This is the Acknowledged/Not-Acknowledged bit. If the physical address of any slave device is the same as the address that was broadcasted by the master device, that slave device will set the value of this bit to ‘0’ . If there are no slave device(s) with the broadcasted address, this bit will remain at logic ‘1’ (default). This will tell the master that the data/command has been received and/or acknowledged by a slave device.
The Data Block
The data block is comprised of 8 bits and they are set by the transmitter,wheather this be the master or the slave, depending on wheather a read or a write operation was requested, with the data bits that needs to transfered to the receiver. This block is followed by an ACK/NACK bit that is set to ‘0’ by the receiver if it successfully receives data. Otherwise it stays at logic ‘1’.
This combination of data blocks followed by an ACK/NACK bit is repeated until all the data is completely transferred.
The Stop Condition
After all the required data blocks are transferred through the SDA line, the master device switches the SDA line from low to high before the SCL line switches back from high to low.
The I2C Stop condition. This signals the end of a transaction. Note SDA returns to High BEFORE the SCL line is pulled High.
How does I2C work in practice
When an I2C transaction is initiated by a master device either to send or receive data to/from a slave device, all of the processes mentioned above will happen at least one. Let us look at a typical scenario for each of the different type of scenarios.
Sending Data to a Slave Device
The following sequence of operations will take place when a master device tries to send data to a particular slave device through I2C bus:
The master device sends a start condition
The master device sends the 7 address bits which correspond to the slave device to be targeted
The master device sets the Read/Write bit to ‘0’, which signifies a write
Now two scenarios are possible:
If no slave device matches with the address sent by the master device, the next ACK/NACK bit stays at ‘1’ (default). This signals the master device that the slave device identification is unsuccessful. The master clock will end the current transaction by sending a Stop condition or a new Start condition
If a slave device exists with the same address as the one specified by the master device, the slave device sets the ACK/NACK bit to ‘0’, which signals the master device that a slave device is successfully targeted
If a slave device is successfully targeted, the master device now sends 8 bits of data which is only considered and received by the targeted slave device. This data means nothing to the remaining slave devices
If the data is successfully received by the slave device, it sets the ACK/NACK bit to ‘0’, which signals the master device to continue
The previous two steps are repeated until all the data is transferred
After all the data is sent to the slave device, the master device sends the Stop condition which signals all the slave devices that the current transaction has ended
The image below represents the transaction with the data bits sent on the SDA line and the device that controls each of them:
I2C Master sending data to a slave device
Reading Data from a Slave Device
The sequence of operations remain the same as in previous scenario except for the following:
The master device sets the Read/Write bit to ‘1’ instead of ‘0’ which signals the targeted slave device that the master device is expecting data from it
The 8 bits corresponding to the data block are sent by the slave device and the ACK/NACK bit is set by the master device
Once the required data is received by the master device, it sends a NACK bit. Then the slave device stops sending data and releases the SDA line
If the master device to read data from specific internal location of a slave device, it first sends the location data to the slave device using the steps in previous scenario. It then starts the process of reading data with a repeated start condition.
The below figure represents the overall data bits sent on the SDA line and the device that controls each of them:
Reading data from a Slave device on the I2C bus
The Clock Stretching concept
Let say the master device started a transaction and sent address bits of a particular slave device followed by a Read bit of ‘1’. The specific slave device needs to send an ACK bit, immediately followed by data.
But if the slave device needs some time to fetch and send data to master device, during this gap, the master device will think that the slave device is sending some data.
To prevent this, the slave device holds the SCL clock line low until it is ready to transfer data bits. By doing this, the slave device signals the master device to wait for data bits until the clock line is released
Conclusion
This concludes this tutorial. In a future post, I will show you how to use I2C to transfer data between two micro-controllers.
In my previous post, I showed you how to use AJAX and JavaScript to make a very responsive Web server on Arduino. In Part 2, we will look at making some important modifications.
Arduino has limited storage space – Use an SD Card
As all of us already know, the Arduino, especially the Uno and Nano, has very limited storage space. If we want to create a truly useful IoT Web server device, we need to do something to increase the available storage space on our Arduino Device.
We can however not increase the program memory. What we can do is store our static HTML page, as well as any images and icons, on a SD-CARD. These come in many sizes, but for our example, I will use a 2gb card, not that we will use all of it anyway.
The standard Arduino Ethernet Shield also comes standard with a SD-CARD reader slot already built in.
Arduino Ethernet Shield for Arduino Uno or Mega. Note That SD Card Slot is already built in
This makes our lives a lot easier. You can also buy a stand-alone SPI SD Card module for a few bucks online. This will be needed if you try to make this project using an Arduino Nano.
Preparing the Card for use
You should format your card using your computer, and a suitable adapter. The card should be formatted to the FAT filesystem. NTFS or other filesystems does unfortunately not work as far as I know.
SD Card IO – How to use the SD Card in Arduino
The Arduino IDE already includes an SD Card library. You can also download additional libraries from the internet that allows more specialized control and functionality. The standard library will however be sufficient for our needs.
It is also easy to test if your card is working or not. The code below is from the “CardInfo” example that ships with the Arduino IDE
/*
SD card test
This example shows how use the utility libraries on which the'
SD library is based in order to get info about your SD card.
Very useful for testing a card when you're not sure whether its working or not.
The circuit:
SD card attached to SPI bus as follows:
** MOSI - pin 11 on Arduino Uno/Duemilanove/Diecimila
** MISO - pin 12 on Arduino Uno/Duemilanove/Diecimila
** CLK - pin 13 on Arduino Uno/Duemilanove/Diecimila
** CS - depends on your SD card shield or module.
Pin 4 used here for consistency with other Arduino examples
created 28 Mar 2011
by Limor Fried
modified 9 Apr 2012
by Tom Igoe
*/
// include the SD library:
#include <SPI.h>
#include <SD.h>
// set up variables using the SD utility library functions:
Sd2Card card;
SdVolume volume;
SdFile root;
// change this to match your SD shield or module;
// Arduino Ethernet shield: pin 4
// Adafruit SD shields and modules: pin 10
// Sparkfun SD shield: pin 8
// MKRZero SD: SDCARD_SS_PIN
const int chipSelect = 4;
void setup() {
// Open serial communications and wait for port to open:
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
Serial.print("\nInitializing SD card...");
// we'll use the initialization code from the utility libraries
// since we're just testing if the card is working!
if (!card.init(SPI_HALF_SPEED, chipSelect)) {
Serial.println("initialization failed. Things to check:");
Serial.println("* is a card inserted?");
Serial.println("* is your wiring correct?");
Serial.println("* did you change the chipSelect pin to match your shield or module?");
while (1);
} else {
Serial.println("Wiring is correct and a card is present.");
}
// print the type of card
Serial.println();
Serial.print("Card type: ");
switch (card.type()) {
case SD_CARD_TYPE_SD1:
Serial.println("SD1");
break;
case SD_CARD_TYPE_SD2:
Serial.println("SD2");
break;
case SD_CARD_TYPE_SDHC:
Serial.println("SDHC");
break;
default:
Serial.println("Unknown");
}
// Now we will try to open the 'volume'/'partition' - it should be FAT16 or FAT32
if (!volume.init(card)) {
Serial.println("Could not find FAT16/FAT32 partition.\nMake sure you've formatted the card");
while (1);
}
Serial.print("Clusters: ");
Serial.println(volume.clusterCount());
Serial.print("Blocks x Cluster: ");
Serial.println(volume.blocksPerCluster());
Serial.print("Total Blocks: ");
Serial.println(volume.blocksPerCluster() * volume.clusterCount());
Serial.println();
// print the type and size of the first FAT-type volume
uint32_t volumesize;
Serial.print("Volume type is: FAT");
Serial.println(volume.fatType(), DEC);
volumesize = volume.blocksPerCluster(); // clusters are collections of blocks
volumesize *= volume.clusterCount(); // we'll have a lot of clusters
volumesize /= 2; // SD card blocks are always 512 bytes (2 blocks are 1KB)
Serial.print("Volume size (Kb): ");
Serial.println(volumesize);
Serial.print("Volume size (Mb): ");
volumesize /= 1024;
Serial.println(volumesize);
Serial.print("Volume size (Gb): ");
Serial.println((float)volumesize / 1024.0);
Serial.println("\nFiles found on the card (name, date and size in bytes): ");
root.openRoot(volume);
// list all files in the card with date and size
root.ls(LS_R | LS_DATE | LS_SIZE);
}
void loop(void) {
}
Open this example in your Arduino IDE, and then make sure that the CS pin is set to the correct pin for your Ethernet Shield ( Pin 10 is for Ethernet, Pin 4 is usually for the SD Card).
Both of these devices will be connected to the SPI bus on your Arduino, and the CS pin will determine which device is active, by being pulled LOW.
Insert your formatted card into the slot, power on the Arduino, and upload the sketch to the Arduino. Open the Serial monitor. If all goes well, you should see information about your SD Card ( Size, sectors etc being displayed ). If your card was already formatted to the FAT file system and contained other files, the names of these files will also be displayed.
Create your Web Page
Power down the Arduino, and remove the SD Card. Put it into the relevant adapter and connect it to your computer.
Now, open a plain text editor, notepad on windows, or any other specialized html editor, as long as you feel comfortable with it, and create a simple html file. Feel free to use my example below, and modify it to your liking
<!DOCTYPE html>
<html>
<head>
<title>Arduino SD Card Web Page EXAMPLE - Maker and IOT Ideas</title>
</head>
<body>
<h1>Welcome to your Arduino Based Web Server</h1>
<p>This page is stored on the SD Card connected to your Arduino.</p>
<p>Please do not remove the card while the Arduino is connected to a power source</p>
</body>
</html>
Save this file as index.htm, and remove the card from your computer, making sure that you properly stop it as per the standard procedures for your operating system.
Put it back into the slot on the Arduino Ethernet Shield, open the serial monitor, and apply power to your Arduino. make sure that you see the file, index.htm listed in the output.
Coding your Webserver
Our next step will be to write the code to create our Arduino Web Server. This code will be similar to the code in part 1 of this series, but I recommend that you start fresh, open a new sketch, and copy-paste my code into the IDE. you can always modify it later to suit your needs…
#include <SPI.h>
#include <Ethernet.h>
#include <SD.h>
// MAC address from Ethernet shield sticker under board
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ip(192, 168, 100, 32); // IP address, may need to change depending on network
EthernetServer server(80); // create a server at port 80
File webFile;
void setup()
{
// initialize SD card
Serial.println("Initializing SD card...");
if (!SD.begin(4)) {
Serial.println("ERROR - SD card initialization failed!");
return; // init failed
}
Serial.println("SD card initialized. [OK]");
// check for index.htm file
if (!SD.exists("index.htm")) {
Serial.println("ERROR - Can't find index.htm file!");
return; // can't find index file
}
Serial.println("index.htm file found - Starting Webserver");
Ethernet.begin(mac, ip); // initialize Ethernet device
server.begin(); // start to listen for clients
Serial.begin(9600); // for debugging
}
void loop()
{
EthernetClient client = server.available(); // try to get client
if (client) { // got client?
boolean currentLineIsBlank = true;
while (client.connected()) {
if (client.available()) { // client data available to read
char c = client.read(); // read 1 byte (character) from client
// last line of client request is blank and ends with \n
// respond to client only after last line received
if (c == '\n' && currentLineIsBlank) {
// send a standard http response header
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println("Connection: close");
client.println();
// send web page
webFile = SD.open("index.htm"); // open web page file
if (webFile) {
while(webFile.available()) {
client.write(webFile.read()); // send web page to client
}
webFile.close();
}
break;
}
// every line of text received from the client ends with \r\n
if (c == '\n') {
// last character on line of received text
// starting new line with next character read
currentLineIsBlank = true;
}
else if (c != '\r') {
// a text character was received from client
currentLineIsBlank = false;
}
} // end if (client.available())
} // end while (client.connected())
delay(1); // give the web browser time to receive the data
client.stop(); // close the connection
} // end if (client)
}
Upload the sketch to your Arduino and navigate to the IP Address of the server using your browser. You should see the page displayed as you coded it.
What to do from here
You can now modify your page to include links and even images and CSS styling. You should however remember that the Arduino also does not have a lot of RAM memory. You should thus not add extremely large images or pages. Those will take a long time to display, or may even time-out and not display at all
In the next part of this series, I will show you how to add links, images and CSS to make your page look a bit more visually appealing. We will also integrate the AJAX and JavaScript functionality from the previous part of the series, to allow our server to interact with the inputs and outputs on the Arduino.
There are many ways to use Arduino to create your own IoT device. One of the easiest is to configure your Arduino as a Web Server. This way, you can connect to it from any browser capable device on your home network. It is also quite a bit safer to do it this way, as you don’t have to expose your device to the internet, reducing the security implications of many of the other methods available.
It does, however, have the disadvantage of not being able to connect to your device from outside your home network. ( In a later part of this series, I will show you how to do this relatively safely, but take note that you still won’t have SSH encryption to the device, that is a huge security risk in today’s online world.
What use will this kind of IoT device have?
You can use an Arduino based web server to monitor various devices in your home, as well as control them. Many of us have old Android tablets and other devices lying around, that may be to old to run the newest Android Operating System. Such a Tablet can however be mounted to a wall, to provide a permanent display and control device. You will only be limited by your imagination, as well as your skill with interfacing your devices with electronics and Arduino.
A few examples of this can be – controlling lights – controlling a fan – measuring temperature and light levels using various sensors, and performing actions based on those values
But Arduino Web Servers are slow
The normal Arduino web servers that we have all seen in various projects on the internet are indeed slow and cumbersome. This is because they usually have to refresh and reload the entire page to display every single update of a switch or output. We can however take advantage of technology used on computer web servers, as well as the browser of the end user.
Ajax and Javascript
What is AJAX?
AJAX stands for Asynchronous JavaScript and XML.
AJAX is basically the use of JavaScript functions for getting information from the webserver (Your Arduino). This means that data on a web page can be updated without fetching the whole page each time.
This means that only the relevant part of the web page will be updated, either automatically, or when the end user performs an action, like click on a button, or when an input on the actual Arduino changes state.
What is JavaScript?
JavaScript is a client-side scripting language. This means that the code will run on the web browser. Meaning on the end-user computer or mobile device.
The JavaScript is included in the HTML page that will be served by the Arduino Webserver. When you load the web page hosted by the Arduino, the page and the JavaScript is loaded to your browser. Your browser then runs the JavaScript code (provided that you have not disabled JavaScript in your browser).
What will we need to do this?
You will need the following hardware and software to do this project
– Arduino UNO or Compatible – Arduino Ethernet Shield with NO SD-Card Inserted – Breadboard – 1K resistor – LED – 10k OR 22k Resistor – Hookup Wire (5 pieces) – Push Button
The Circuit
Wire the following circuit on your breadboard
Circuit Diagram for the Arduino AJAX Web Server – Part 1
Connect your Ethernet Shield to the Arduino Uno. Connect the Resistors, LED and push-button Switch as shown. Connect +5v from Arduino to Red Line, Gnd From Arduino to Blue line
Connect Orange Wire from Arduino Pin 2 to a hole BETWEEN the 10k resistor and the switch ( See Diagram). Connect Green Wire from Arduino Pin 3 to a hole above the 1k resistor ( See Diagram)
The Code
Copy the following code into your Arduino IDE, Or download the file below
#include <SPI.h>
#include <Ethernet.h>
// MAC address from Ethernet shield sticker under board
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ip(192, 168, 100, 32); // IP address, may need to change depending on network
EthernetServer server(80); // create a server at port 80
String HTTP_req; // stores the HTTP request
void setup()
{
Ethernet.begin(mac, ip); // initialize Ethernet device
server.begin(); // start to listen for clients
Serial.begin(9600); // for diagnostics
pinMode(2, INPUT_PULLUP); // switch is attached to Arduino pin 2
pinMode(3, OUTPUT);
digitalWrite(3,LOW);
}
void loop()
{
EthernetClient client = server.available(); // try to get client
if (client) { // got client? boolean currentLineIsBlank = true; while (client.connected()) { if (client.available()) { // client data available to read char c = client.read(); // read 1 byte (character) from client HTTP_req += c; // save the HTTP request 1 char at a time // last line of client request is blank and ends with \n // respond to client only after last line received if (c == '\n' && currentLineIsBlank) { // send a standard http response header client.println("HTTP/1.1 200 OK"); client.println("Content-Type: text/html"); client.println("Connection: keep-alive"); client.println(); // AJAX request for switch state if (HTTP_req.indexOf("ajax_switch") > -1) { // read switch state and send appropriate paragraph text GetSwitchState(client); } else { // HTTP request for web page // send web page - contains JavaScript with AJAX calls client.println("<!DOCTYPE html>"); client.println("<html>"); client.println("<head>"); client.println("<title>Arduino Web Page with AJAX - Maker and IoT Ideas</title>"); client.println("<script>"); client.println("function GetSwitchState() {"); client.println("nocache = \"&nocache=\"\ + Math.random() * 1000000;"); client.println("var request = new XMLHttpRequest();"); client.println("request.onreadystatechange = function() {"); client.println("if (this.readyState == 4) {"); client.println("if (this.status == 200) {"); client.println("if (this.responseText != null) {"); client.println("document.getElementById(\"switch_txt\")\.innerHTML =this.responseText;");
client.println("}}}}");
client.println("request.open(\"GET\", \"ajax_switch\" + nocache, true);");
client.println("request.send(null);");
client.println("setTimeout('GetSwitchState()', 1000);");
client.println("}");
client.println("");
client.println("");
client.println("");
client.println("Arduino Web Server with AJAX");
client.println("Switch Status on D2");
client.println( "Switch state: Not requested…");
client.println("");
client.println("");
}
// display received HTTP request on serial port
Serial.print(HTTP_req);
HTTP_req = ""; // finished with request, empty string
break;
}
// every line of text received from the client ends with \r\n
if (c == '\n') {
// last character on line of received text
// starting new line with next character read
currentLineIsBlank = true;
}
else if (c != '\r') {
// a text character was received from client
currentLineIsBlank = false;
}
} // end if (client.available())
} // end while (client.connected())
delay(1); // give the web browser time to receive the data
client.stop(); // close the connection
} // end if (client)
}
// send the state of the switch to the web browser
void GetSwitchState(EthernetClient cl)
{
if (digitalRead(2)) {
cl.println("Switch at D2 is: OFF, LED at D3 is OFF");
digitalWrite(3,LOW);
}
else {
cl.println("Switch at D2 is: ON, LED at D2 is ON");
digitalWrite(3,HIGH);
}
}
Upload the code to your Arduino
Testing the results
Open a web browser and go to the IP Address of the server ( the one you set in your code). If you did everything correctly, you should see a screen similar to this.
The Ajax Web Server shows the button and LED is OFF
Now press the button
The web page should immediately update and tell you that the button is On, and the LED is ON
Ajax Web Server showing Status of Button and LED as ON
The Generated HTML will look like this
<!DOCTYPE html>
<html>
<head>
<title>Arduino Web Page with AJAX - Maker and IoT Ideas</title>
<script>
function GetSwitchState() {
nocache = "&nocache=" + Math.random() * 1000000;
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200) {
if (this.responseText != null) {
document.getElementById("switch_txt").innerHTML = this.responseText;
}}}}
request.open("GET", "ajax_switch" + nocache, true);
request.send(null);
setTimeout('GetSwitchState()', 1000);
}
</script>
</head>
<body onload="GetSwitchState()">
<h1>Arduino Web Server with AJAX</h1>
<h3>Switch Status on D2</h3>
<p id="switch_txt">Switch state: Not requested...</p>
</body>
</html>
Images of the Working Hardware
Please note that I did not use a push button switch in my example. I have just used a piece of hookup wire to connect the pulled-up pin to ground, as it is easier to photograph that way, without my finger being in the way on a button. 🙂
Conclusion
This concludes part 1 of this series. This example can very easily be extended to be more useful, as well as be modified to work on other platforms, like ESP32. In further parts, I will show you how to extend this very simple server into becoming something much more useful. Please visit again to see the rest of this series.