Arduino Getting Started With Mikroduino-V2
Arduino Getting Started With Mikroduino-V2
Introduction
Arduino is an extremely popular single board computer that can be used to make a vast vari-
ety of intelligent devices. With this book you will learn various parts of the Arduino boards,
its philosophy and the entire ecosystem that surrounds the microcontroller based project de-
velopment. This includes planning electronics, hardware, resources, programming and de-
bugging. All these are addressed in this book in different parts where need arises. This book
focuses on programming and getting the most out of Arduino through programming. It is
not intended to teach electronics and the hardware details, however a little description of
hardware and basic electronics is given where it is related. I believe that only with a solid
understanding of ‘Basics’ and ‘Fundamentals’ one cannot innovate. Innovation is nothing
but a continuation of the existing knowledge.
To get most out of this book, I would recommend not only to write the programs yourself,
but also think of new ways to achieve the same or even better results. There is only one
thing that is most important in this journey, that is ‘Passion’ to achieve. No matter how
many times your code fails, keep on trying, keep on asking yourself and your peers.
Algorithm
When writing an essay or even a paragraph on some topic, you must have realized that
different people would write it in different way, all using same or different sets of words,
but end result is same. They convey the same message, some have achieved this with few
words, some with more words, some have considered even minor details others have
skipped those, and all that difference highlights the quality of essay.
An algorithm is similarly the route you plan in computer programming, to solve a particular
problem. The choice of algorithm is sometimes a personal matter, some programmers use
already described algorithms, while others tend to develop their own. In a real world
situation it’s a combination of both. Where applicable you use a previously described
algorithm or algorithms and add your own logic to it to achieve a working solution.
Now that we know the programming is an art and there is always chances to improve your
skills. Lets come back to our original question, “Why Using Arduino ?” There are many
reasons to start programming electronic devices using Arduino. First we need to clarify
what is Arduino actually, the board, like Arduino UNO or The programming language?
Here is an example C++ code where a number of functions with easy to remember textual
names are called in a particular order to get the job done. These individual functions, actual-
ly contain many lines of code that is generated in background when we compile the pro-
gram. However from an application developer perspective all we need to know is what ac-
tion a particular function will perform, when it will be called in a program.
Learning Arduino programming is the beginning of fascinating journey into the rapidly
evolving field of embedded systems. I accepts you as a newbie and makes the things simple
and easy to achieve. When you achieve the results by simple commands, it gives you a sat-
isfaction of achievement and motivates to dive more deep. The concepts of programming
are same weather its Arduino, or a data server computer, only difference is the scale of re-
sources available. Indeed I believe programming a device with limited resources is more
challenging than a robust system with tons of resources.
The knowledge, experience and attitude to solve a problem that you gain while program-
ming Arduino, will form the basis to accept the other advanced microcontrollers with ease.
This is just like if you learn driving a small car, the same concepts apply to driving a bus or
a truck.
This book is structured as a self-paced learning platform. You may be a student, hobbyist or
a teacher you will find this book quite helpful in exploring this wonderful world. Each chap-
ter builds upon the knowledge that you have gained from the previous chapters. Some theo-
ry like electronics, basic math's and related algorithms etc. are explained wherever their
need arises. There are dedicated chapters where we use ready-made modules and publicly
available libraries that will bring those modules to life in a matter of minutes. I would how-
ever emphasize on you to experiment with it and explore the native structure yourselves.
The lessons in this book have been classroom tested. Students have created projects of their
own designs based on what they've learned with earlier versions of these lessons. hey have
made model helicopters and airplanes, elaborate rolling robots, musical instruments, light
panels, keyboards to drive synthesizers, "laser" tag games, hover boards, Segway-like vehi-
cles, and more.
Whether you are exploring this book for yourself or to teach others, I hope you find the con-
tent engaging and useful. I invite you to share your thoughts, suggestions, and cool projects
of your own.
CHAPTER 2
The Arduino family has many development boards all sharing the same basic philosophy of
easy to use and easy to program. The Arduino IDE and the modified C++ programming lan-
guage that it implements, abstract many of the underlying hardware details. The boards pro-
vide necessary circuitry for the microcontroller, like power supply, access to its IO lines and
various hardware modules, and programming interface. Apart from that the bare board does
not do anything useful. In order to get it used in a project you need a supporting external
circuit that should contain the required electronics, components and a plethora of devices.
The external circuit is usually built rapidly (rapid prototyping) using breadboard. The circuit
is first built on breadboard and then its controlling connections are made with the Arduino
board. In this way a complete functional project is ready. Now the Arduino board is pro-
grammed and the project comes to life.
The above picture shows a typical project with jumper wires connecting Arduino board to
the breadboard. This arrangement is good, and rapid when the external circuit is not very
complex and requires few components. However it becomes really tedious and complicated
when the required number of electronic components grow and their interconnections be-
come complex. This entails many jumper connections on the breadboard as well as from
breadboard to Arduino board.
Sometimes this complexity is natural as the required project is complex, but many a times
its unnecessary as well as repetitive. By repetitive I mean that there are some components
that are very frequently required in most projects, like displays, LEDs or buttons etc. when
building these circuits of breadboard they tend to add to the complexity of breadboard wir-
ing as well as they need to be made every time in new projects. A Simple solution for such a
problem is to readymade modules that one can build for his own personal lab. You can
make many other similar modules like for LCD display, seven segment displays and so on.
These modules are usually soldered on perfboard and are quite stable and useful. Soldering
and making permanent electrical connections, reduces the clutter of wires.
A major problem that I have observed with breadboard projects, is the loose connections of
wires with the breadboard contacts. Moreover the disorganized connections makes it really
difficult to trace any fault if the project is not behaving as
per expectation. When using such modules the number of
connections are dramatically reduced.
Many commercial companies have started producing
modules for this purpose. Sometimes the external circuitry
required is based upon special purpose chips, like Blue-
tooth communication, wireless communication or interfac-
ing with TFT displays etc. The circuits of these devices
are so complex and delicate that they are beyond the reach
of a student or hobbyist to make on breadboard. These
modules offer the same level of convenience and flexibil-
MPU6050 3-Axis Gyroscope Module
ity as of homemade modules.
Jumper Wires
When connecting Arduino to the breadboard or other modules jumper wires are used. The
ends of these wires contain terminals that should fit the
connector and establish a secure connection. By secure
connection I mean the connection should be fitting snug-
gly, without any loose connections, and they should be
secure that accidental movements of hand or otherwise
should not dislodge the connection.
Since Arduino boards specially Arduino UNO and Mega
have ‘female’ type headers for connections and the bread-
board also has ‘female’ type sockets for connections, male
-male jumper wires are frequently used. In my personal
experience there are various ‘Qualities’ of wires available
in market. Some have very short and thin connectors, that Male-Male Jumper Wires
don’t fit well in the Arduino sockets as well as breadboard
sockets.
Consequently most commercial modules provide male
type headers on their module, so that you need a female-
type jumper connector to connect to them. This connec-
tion in my personal experience is more secure both in
terms of secure connection and accidental dislodgment.
Since Arduino board has female header so we use these
jumper wires with male connector on one side and female
connector on other side. Use of male connections on Ar-
duino still makes these connections a less attractive option. But still at least connections
with module are secure.
Arduino Nano
Although Arduino UNO is the flagship board of Arduino family, another variant and com-
pact sized board from Arduino family is ‘Arduino
Nano’. It has the same microcontroller as on Arduino
UNO, and has similar resources like power supply
and regulator etc. but on a small scale factor. Also the
connections are usually as male header so that it can
be treated just lie a 40 pin integrated circuit.
This board can be connected to modules using female
-female jumper wires, or can even be plugged in di-
rectly on breadboard. In that case further connections
will still need male-male jumper wires. The header pins of Arduino nano are usually quite
thick and strong and usually make good contact with the breadboard.
Additionally the board has a number of headers to connect various commercially available
modules directly to the board. These headers have pin arrangements that are similar to the
pin arrangements on module, making it simple to connect. Moreover some modules that use
I2C communication or SPI communication need to be connected to the specified pins of Ar-
duino only, so there is not question of choice of IO line for them, the headers connect these
pins directly to the module pins. There are only one set of I2C pins on Arduino board, while
the literature says you can connect a number of devices together on the same lines. In case
you want to connect more than one I2C compliant devices you need expansion boards, or
connectors like breadboard. This board offers the I2C and SPI lines not only on the IO head-
er pins, but also dedicated headers for external devices. Indeed the board also has headers
for I2C LCD, I2C OLED and I2C EEPROM, I2CDS1307 RTC etc. Lets dive in a little bit
to explore the various parts of this board.
Power Supply
All projects when running need a reliable power supply. Most projects do not need very
heavy power supply so they can get the supply from Arduino boards. This board can get the
power supply directly from the Arduino Nano, which gets is directly from the USB port and
in most cases from your computer. The supply current depends upon your computer’s USB
port. Usually these ports can supply up to a maximum of 500mA current. The Arduino
Nano board provides the same 5V to the microcontroller as well as to its distribution pins
for use by modules.
There is also a 3.3V regulator on Arduino nano (LP2985-33 or similar) that can provide on-
ly 50mA of current. Though 3.3V is not used by Arduino Nano board, yet there are some
modules out there that run on 3.3V so it becomes handy to have this power supply to use
those modules readily.
MikroDuino Nano board gives an external source of power supply as well. So you can get
the supply from USB connector of Arduino Nano, or when not connected to your PC from
this external source. The external source has a barrel connection for DC power adapter. Alt-
hough you can give up to 30V DC, but we recommend giving 9-12V DC supply. There is a
reverse polarity protection diode that prevents powering the board if the polarity of power
supply is reversed. The board has a robust 5V linear voltage regulator (LM7805) that can
power up to 1.5A of current. The board has its own 3.3V regulator (LM1117) that can sup-
ply up to 1A of 3.3V current.
The 5V power supply is routed to the 5V rail of Arduino nano, as well as to headers for var-
ious modules. The board has 3 headers for supplying the board power to external modules.
Header labelled DC9-12V gets direct power from the barrel jack. So whatever you provide
appears here. DC 5V and DC3.3V headers provide 5V and 3.3V regulated power for exter-
nal modules if required.
Arduino IO Lines
Arduino nano has 22 general purpose IO lines, also called GPIO lines. These lines are
grouped as “digital” and “analog”. Digital lines are numbered from D0 to D11 and analog
lines as A0 to A7. Some dig-
ital lines have PWM mod-
ules attached to them, so
these can be used to produce
PWM signals. While some
lines have internal connec-
tions to I2C module and SPI module. MikroDuino board gives access to
these lines as two headers, one exposing digital pins D0 to D7 and other D8
to D11. PWM pins are D3, D5, D6, D9, D10, D11.
Analog pins are provided on a separate header and labelled as A0 to A7.
The analog pins can also be used as digital, in that case their numbers con-
tinue from D11 onwards, like A0 can be used as D12 and so on. A6 and A7
are however purely analog input and these cannot be used for digital data.
The analog pins are connected to internal Analog to digital converter mod-
ule. These pins therefore can read analog voltage as input. These pins can
not produce analog output.
Configuration Jumpers
As previously said the board has number of commonly used compo-
nents, like LEDs, Buttons, potentiometer etc. Instead of hard wiring
these devices to the GPIO pins, we have provided jumper headers.
On one side of jumper header are the device pins (Like LEDs) and on
the other are digital GPIO lines as selected by us. Placing a jumper
tab on the header will connect the corresponding device pin to the
labelled GPIO pin. And disconnecting the tab will free the pin from
this device. Additionally is your project requires the device to be con-
nected to a different IO line other than the one, we have selected, just remove the tab, and
using a jumper wire connect the device pin to GPIO pin of your choice.
This arrangement makes the board highly configurable, you can connect a device pin to a
preselected GPIO, or to a GPIO of your choice or even disconnect it, if you want that pin to
be used in some other module or simply don’t want this device in your project.
Potentiometer (A7)
Potentiometer is just a variable resistor, its two ends are connected
to 5V and GND supply the center tap is available for connecting to
Arduino. Its jumper is also located on Jumper-1 and by default con-
nects to A7 pin of Arduino nano. Potentiometers are useful in test-
ing the analog input to simulate a sensor, or just as an analog
source.
In this chapter we will dive a little deep to understand the Arduino nano board, and the
AVR microcontroller that it has. Though using Arduino IDE all this knowledge is not re-
quired, because the beauty of Arduino IDE is in abstracting all this detail. I however believe
that one should always know the fundamentals and atomic level structure of the things. This
helps better understanding, and ability to modify the code or even libraries if you want.
ARDUINO Nano Board
The Arduino nano board Is a small form factor duplicate of the flagship Arduino UNO
board. Arduino nano has the same microcontroller as on Arduino UNO. ATMega328P mi-
crocontroller is used in Arduino nano.
The board has all the necessary circuitry to support the functionality of ATMega328P mi-
crocontroller. The board has a USB to serial conversion chip that allows communication
with PC using USB port. Most of the Arduino nano boards contain CH340G chip, its driver
is usually automatically detected by windows. The board has one user programmable LED
which is connected to D13 pin of the MCU. In addition there are LEDs for serial communi-
cation, labelled as Tx and Rx, and there is one LED for power. There is a reset button on the
board. The board gets power supply from USB port, which is always 5V. However it can
also be powered through Vin pin which goes to 5V regulator chip and then the 5V rail is
powered up. The 5V regulator is small scale, and can give only upto 500mA of current. The
board itself needs very little power, however the 5V rail is available through jumper header
for expansion. The limitation of this 5V regulator current should be kept in mind. The 5V
supply also passes through a 3.3V regulator, which is also available in header for expansion
modules.
All GPIO Lines marked as digital and Analog are available as header on both sides of the
board. The Arduino nano has 14 digital lines, numbered from D0 to D13 and 8 Analog lines
numbered from A0 to A7. Pins A0 to A5 can also be used as digital using digital labels,
D14 to D19. Pin A6 and A7 are however purely analog inputs.
The picture above shows the pinout map of Arduino nano. Pins labelled in pink are digital
pins, and are used in Arduino programming language to access the digital pins. Pins shown
in orange are for Analog to digital conversion, and are numbered from A0 to A7. Notice the
analog pins A0 to A5 are also digital and can be addressed as D14 to D19. However A6 and
A7 are only analog.
There are three communication modules in ATMega328 microcontroller. These are USART
(Green Pins), SPI (yellow pins) and I2C (sky Blue pins). Each of these modules have their
own specific pins that are hard wired to certain pins of Arduino nano board. When using
these modules the external circuitry must be connected to “these” pins only.
Digital pins numbered D3,D5,D6, D9,D10 and D11 can be provided with six independently
controlled 8 bits Pulse Width Modulation (PWM) Signals. The pins are indicated by a wave
like symbol on them.
Pina labelled in violet are the same as digital or analog, but these names like PD0, PD1,
PD2 etc. are used by the native compiler that is provided by the manufacturer. The datasheet
of microcontroller also name these pins as these labels, indeed they actually group them as 8
bit Ports, like PORTD, PORTB etc. each having its bits named as PD0, PD1 and so on.
When programming the Arduino board using Microchip Studio, and native C++ program-
ming you access these pins by these names. Arduino has simplified the process to access
these individually named pins, by replacing or mapping an internal array thus allowing us to
access these pins just by using a linear scale of integer numbers, from 0 to all the way to 19.
This Arduino scheme of mapping the in- Although We label digital pins as D0, D1 etc in this
dividual Ports and their bits, to a linear text, they re represented by the integer number
scale has dramatically simplified the ap- only in program. So D13 will be accessed as 13
plication development. This ease of appli- only in program.
cation development however comes at
some ‘Cost’. Arduino generates an extra code that is run every time you access the pin by
Arduino numbering system. This translation takes some CPU time, and thereby affects the
speed of performance. Most of our applications are not that much time critical, so this small
reduction in speed is acceptable in view of the convenience that it provides.
The underlying compiler running in Arduino is same as provided by Microchip in its Micro-
chip Studio. Therefore you are free to use the native C++ naming convention and bit access-
ing while in Arduino IDE. I will show you these methods in appropriate sections.
Pins labelled in White are special function bits of certain modules, specially timer modules,
that are used to control the PWM output. The analog pins are also shown in white, these pin
names are used by the compiler when accessing these pins as analog inputs.
Programmer Pins
There are a group of six pins, on a separate edge of the board. All these pins are also availa-
ble from the Arduino standard side header, but they have been
grouped together to allow a hardware programmer. This device
is a separate hardware, not provided as part of Arduino, and
there are actually a number of devices available from different
vendors. These devices put the microcontroller into program-
ming mode and allow transfer of compiled program from PC
into the program memory of microcontroller. In addition to
programming these devices also provide access to inside regis-
ters directly at run time allowing low level debugging of the
application. The same devices also gives you access to the
“Fuse Bits” which are special protected area of the microcontroller and configures various
aspects of the microcontroller, like protecting your code from being copied, or configuring a
different speed of crystal etc.
Arduino Programming
Another great thing Arduino developers have done, is that they have written a piece of code
called “Bootloader”, this code is already present in every Arduino board. This code occu-
pies a small part of program memory, and whenever the microcontroller is reset, either by a
signal on reset pin, or power up, this code starts execution. It will monitor the Serial port
(USART) for a new compiled user program, Arduino IDE sends its compiled program, over
the USB using USART commands. If the bootloader gets a new program, it erases the pre-
vious user program in microcontroller, and writes the new program in its place. When new
user program is in place, it hand overs the control to the user program. In case of reset or
power up, if there is no new user program coming, the bootloader simply starts execution of
the existing user program. All this process is automated by Arduino IDE, and when you
press the program button, the IDE compiles the user program, sends a reset signal to the Ar-
duino board and transfers the new program to be executed.
This is fairly good for most of the times, but this bootloader does not allow you to access
the fuse bits, and special function registers at run time, thus limiting the debugging and de-
vice settings.
So if you get a new chip from market (ATMega328) it cannot be programmed via Arduino
IDE, unless the bootloader is first transferred into it using standard programmer like USB-
ASP or you may program it using USB-ASP only as you like.
What is a Microcontroller
While programming the Arduino using Arduino IDE does not require you to understand the
underlying architecture and hardware of the microcontroller itself, yet I believe knowledge
is power and the more you know about your hardware the more control and command you
have over it. This is just like driving a car, most of us only require to know our dashboard,
and important paddles that car manufacturer has provided us to communicate with the un-
derlying hardware of the car. You don’t need to know how the fuel jets work, when you
press the accelerator, and how the gears shift when you change the gear lever and so on and
so forth. The knowledge of underlying hardware becomes important when troubleshooting.
Same thing goes about microcontrollers. If you know what you are driving and playing with
you understand better what's happening and what limitations it has.
A microcontroller is a mini computer with a handful of modules useful in communication
with other electronic components. All packed together in one integrated circuit with pins for
connecting the microcontroller with other electronic devices on board. The microcontroller
is able to read data from external environment, or other specialized chips. Process that data
according to a set of requirements as determined by a program, and then produce an output
to affect the controlling device.
A microcontroller control system usually consists of sensing the environment, getting user
input, displaying output for user information, and then producing an appropriate control sig-
nal to control the application.
ATMEGA328P Introduction
This is the right time to know the features, powers and limitations of your microcontroller.
Always make it a habit to go through the manufacturer documentation to get the true picture
of your hardware. When we deal with third party resources like Arduino, or other communi-
ty acquired libraries, they may or may not harvest the full potential of your hardware, and
force you to play within their own boundaries.
ATMega328 is an 8 bit microcontroller manufactured by Microchip corporation. It belongs
to AVR series of family. This family was originally designed and produced by Atmel mi-
crocontrollers company, later it has been acquired by microchip. The AVR series of micro-
controllers are based upon advanced RISC architecture. RISC stands for “Reduced Instruc-
tion Set Computing”. The instruction set has been reduced to essential instructions, resulting
in reduced CPU complexity and circuitry. This enables reduced power consumption and im-
proved performance. The ATMega328 microcontroller can run at a maximum clock fre-
quency of 16MHz, though we can make it run using slower speed crystals, or even using an
internal 8MHz oscillator. Most of the instructions of AVR series microcontrollers can com-
plete their execution within 1 clock cycle. This means if the CPU is running at 16MHz, it is
executing 16 million instructions per second.
High clock frequency delivers better computational performance, but also makes the appli-
cation power hungry. Higher is the frequency higher is the power consumption. So when
designing applications that will actually run on battery this factor becomes really important.
So you choose the frequency according to system requirements. High speed is not always
the best choice. The Arduino system however is designed to run only at 16MHz, limiting
your choice of power consumption.
There are three different types of memories inside the microcontroller. It has 32KB of flash
memory dedicated to keep the application software. Usually it is called ‘Program memory’.
This is nonvolatile memory, which means the contents of memory remain preserved even
when power is turned off. Second type of memory is 2KB of SRAM. SRAM is volatile
memory, and keeps the contents alive till the power is on. On reset and power off this
memory gets cleared. The speed of reading and writing data to this memory is however
quite fast and is mainly used to keep the runtime variables, and objects. Third type of
memory is 1KB EEPROM. Technically its similar to flash memory, but separate from it. It
is also nonvolatile and is used to record the program settings and some configuration varia-
bles, that are generated during program execution. It has 23 general purpose I/O pins, sup-
ports six PWM channels, eight 10 bit ADC channels and multiple communication interfaces
such as USART, SPI and I2C. The microcontroller has a wide operating voltages from
1.8V to 5.5V. With nano power consumption modes and deep sleep mode it is suitable for
battery powered applications. It has 3 programmable timers 2 8-bit and 1 16-bit.
Feature Specification
Architecture 8-bit AVR RISC
Package SMD (TQFP-32, QFN-32)
Operating Voltage 1.8V – 5.5V
Up to 20 MHz (16 MHz typical in Ar-
Clock Speed
duino)
Flash Memory 32 KB
SRAM 2 KB
EEPROM 1 KB
GPIO Pins 23 (Digital + Analog)
Digital I/O 14 (including 6 PWM channels)
PWM Channels 6 (D3, D5, D6, D9, D10, D11)
Analog Inputs 8 (10-bit ADC, A0-A7)
Communication USART, SPI, I2C (TWI)
Timers 3 (Two 8-bit, One 16-bit)
External Interrupts 2 (INT0, INT1)
Power Consumption Low-power modes available
Bootloader Support Yes (Used in Arduino boards)
ATmega328P USART Module and Its Features
The ATmega328P microcontroller includes a Universal Synchronous and Asynchronous
Receiver-Transmitter (USART) module, which enables efficient serial communication.
This module is a crucial interface for exchanging data between the microcontroller and oth-
er devices such as computers, sensors, or other microcontrollers. The USART module in
ATmega328P supports full-duplex communication, meaning it can send and receive data
simultaneously. It operates with a baud rate generator, allowing flexible speed selection
for communication. The module is integrated with hardware buffer registers to ensure
smooth and efficient data transfer without software intervention.
Key features of the ATmega328P USART module include:
• Asynchronous and Synchronous communication support
• Full-duplex operation (separate TX and RX lines)
• Configurable baud rate generator
• Frame format selection (5, 6, 7, 8, or 9 data bits with 1 or 2 stop bits)
• Parity bit support (None, Even, or Odd)
• Double-speed mode (U2X bit) for faster communication
• Multi-processor communication mode
• Hardware-based data transmission and reception buffer
• Interrupt-based communication for efficiency
• Modes of Operation
The ATmega328P USART module operates in the following modes:
1. Asynchronous Normal Mode
• The most commonly used mode, where data transmission and reception occur
without an external clock signal.
• A dedicated baud rate generator determines the speed of communication.
• It supports data frames of 5 to 9 bits with selectable parity and stop bits.
2. Asynchronous Double-Speed Mode (U2X = 1)
• Reduces the number of clock cycles required per bit, effectively doubling the
baud rate.
• Provides faster communication while maintaining the same clock frequency.
3. Synchronous Mode
• Uses an external clock signal for communication, making it more reliable in tim-
ing-critical applications.
• Requires a synchronized transmitter and receiver to operate correctly.
• Less commonly used in general applications but useful in certain embedded sys-
tems.
4. Master SPI Mode
• The USART can be configured as an SPI Master to communicate with SPI-
compatible devices.
• This mode is enabled by setting the UMSEL bits to SPI mode and configuring
the USART clock.
By default, 8-bit data frames with no parity and 1 stop bit (8N1 format) are used in Arduino.
The Double Speed Mode (U2X = 1) is also enabled in the Arduino core to allow higher baud
rates with improved accuracy. However, synchronous communication and multi-processor
modes are not typically used in the Arduino framework.
In most applications, Arduino acts as the SPI master and communicates with peripherals like
SD cards, displays (e.g., OLED, TFT, MAX7219 LED matrix), shift registers, and external
EEPROMs. The SPI library provides functions like SPI.begin(), SPI.transfer(), and
SPI.setClockDivider() to configure and send data over SPI.
Since SPI is much faster than I2C and UART, it is preferred for high-speed data transfer
applications like real-time sensor data acquisition and display interfaces. However, un-
like I2C, SPI does not support multiple masters, making it best suited for single-master,
multi-slave configurations.
Summary
In general ATMega328 microcontroller is quite powerful and highly configurable to fit re-
quirements of a number of projects. Taping its best powers however requires programming
at chip level and understanding its architecture. Arduino IDE has done a great job at simpli-
fying most tasks, but also does not allow user to harness all the powers of the microcontrol-
ler. This has been highlighted only to emphasize the importance of knowledge of the under-
lying architecture, and not relying on easy to use libraries and tools. Always be prepared to
accept the challenges and dig deeper.
CHAPTER 4
In order to write programs for Arduino board, or more specifically ATMega328 you need a
chain of tools, or software. These tools include an editor, where you can write abd edit the
program, a compiler that will compile your program into microcontroller understandable
code, a Linker that would incorporate already developed libraries into the executable code
and produce final application. Then you need a programmer software that would take the
compiled executable file and transfer it to the microcontroller over some communication
system, like USB or serial port.
All these tools are practically separate software. Arduino has combined them together in
one, and called it a ‘Development Environment’. That is why the Arduino software that you
install on your PC is called Arduino IDE (Integrated Development Environment). You can
download Arduino IDE free from their official website. The installation is simple and
straightforward process so I will not go into the details of downloading and installing the
IDE. When you install the Arduino IDE it automatically installs the GNU C++ compiler and
AVRDude which is the programmer that communicates with Arduino board and transfers
the program.
I will not go into the details of this application here, but mention here that many profession-
al developers consider it a better alternative to the Arduino IDE. The interface is however
little more technical and you need to configure the things. I would recommend it only for
experienced users, for beginners standard Arduino IDE is fine.
Other Development Options
Arduino also provides an online version of its IDE, using it you can make and compile the
programs online, without need to install anything on your computer. This option is good
where you are working in a shared environment.
Arduino GPIO
Beginning with GPIO, in microcontroller world is like the ‘hello world’ in any other pro-
gramming language. GPIOs are the most basic form of microcontroller devices. As the
name suggests these are simple digital pins that can take logical data (High or Low) from
external environment or send logical data to external environment. In its most simple form
we can set a GPIO pin as HIGH or LOW. When the pin is set to HIGH (binary 1) the pin
gets 5V (or VCC) supply on it and is ready to source a small current to the output as well.
The Arduino pins can source a maximum of 25mA current, which is enough to light an
LED, or to turn a transistor ON. Similarly when a GPIO pin is set to LOW (binary 0) the
pin gets connected internally to ground or 0V. The pin then effectively can sink a current of
up to 25mA. The Arduino nano has digital pins labelled as D0 to D13. the ‘D’ is used only
to indicate pure digital pins. These pins can perform other functions like serial communica-
tion etc., but essentially they can assume only a HIGH or LOW state and all serial commu-
nication will take place through same two states. The pins A0 to A5 are commonly used for
acquiring analog inputs, but they can also be used as digital pins, labelled as D14 to D19. In
your Arduino code you can access the digital pins by referring them as a numeric number.
The number can be a constant, or a variable.
Each of these 19 GPIO pins can be individually programmed, and used without affecting
each other’s state. Before using a pin, we have to set its ‘Mode’. The mode of pin can be
‘OUTPUT’ or ‘INPUT’. When set to OUTPUT the program can set the state of pin as
HIGH or LOW, while when set to INPUT the program can read the externally applied sig-
nal on the pin. Usually in any given project we know before programming which of the pins
would be OUTPUT and which ones will be INPUT. So these settings are set only once dur-
ing the program startup. Nevertheless if required the mode of pin can be changed from OUT
to INPUT and vice versa during program execution, as in case when the same pin is used to
send and receive data.
When a pin is set for OUTPUT keep it in mind that it can source or sink only up to 25mA of
current. Therefore you cannot drive a heavy load like a motor, or a relay directly from this
pin. Similarly when the pin is acting as INPUT the externally applied voltage must not ex-
ceed the pin’s rated voltage. For
Arduino Nano, all its pins are 5V Some boards like ESP32, or STM32 etc. are powered by
tolerant. So any external signal 3.3V and most of their pins are not 5V tolerant. However
should not be more than that. Ap- some of the pins can accept 5V signals. Always check
plication of higher voltage, like datasheet of your microcontroller while designing the
12V from some external device
hardware.
will damage the microcontroller
pin.
PULL-UP Resistor
When a pin is configured as INPUT, it should always be reading a HIGH or LOW signal. In
no circumstances it should be left open. If left open it will read noise from external environ-
ment and program may malfunction. If the pin is connect-
ed to an external signaling device that ensures a HIGH or
LOW state weather its communicating or not, there is no
need of a PULL-UP resistor. In other situations, like in
push buttons, the pin is left open when button is not
pressed. In this situation a pull-up resistor is must.
Here in this picture R1 is a pull-up resistor. The value of
R1 is not critical, but a general rule is to use 10K of exter-
nal resistor. In this configuration when button is ‘Not’ be-
ing pressed the microcontroller will read the pin as ‘HIGH’, and when the button is pressed,
it will read the pin as ‘LOW’
Just like Pull-up you can also use a pull-down resistor. Arduino has no objection, but a gen-
eral usage is to use a pull-up resistor.
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(500);
digitalWrite(LED_BUILTIN, LOW);
delay(500);
}
Once you have selected the board and port, you can now compile and program the board.
Notice the two buttons with a check mark and arrow on the upper left corner of screen. The
check mark only compiles the program, and reports if there are any errors. The Arrow but-
ton, is compile and program or upload the code.
The results of compilation and uploading are shown in the Output window below. If every-
thing is OK, the program starts executing on the board and an Built-in LED on Arduino
board starts flashing.
Congratulations, you have just completed the complete cycle of writing a program and up-
loading to your board.
Now lets have a look at the code. Since this book is mainly about coding the Arduino, we
shall concentrate more on that. Unlike the standard C/C++ program (if you already have
experience on it), the Arduino code (Also called sketch in Arduino community) does not
contain the main() function. Instead the Arduino IDE by default makes two functions, the
setup() and loop().
The void setup() function is where the program first starts execution. This part of code is
runs every time the microcontroller starts execution. Once the code in this part is finished
executing, the control is handed over to the loop() function. As the name suggests the loop
function, keeps on executing the code line by line, endlessly.
Usually we write the various configuration commands like setting up the modes of pins, or
initializing some variables etc. in the setup() function.
pinMode(LED_BUILTIN, OUTPUT);
The pinMode() function is used to set the mode of a particular pin to OUTPUT, INPUT or
INPUT_PULLUP. The First argulent of the function is the pin number and second is the
mode. The constant LED_BUILTIN is already defined in the Arduino as 13 which is the
digital pin (D13) of the Arduino to which an onboard LED is connected. If you want some
other pin to set its mode, just use its number here. Remember write only number not the ‘D’
part. So if you want the led connected on D6 to blink with this code, just replace
LED_BUILTIN with 6.
digitalWrite(LED_BUILTIN, HIGH);
digitalWrite() is the function that actually turns the port pin HIGH or LOW. Again it has
two arguments, the first one is the pin number, that you want to affect and second is the
state you want. HIGH or LOW. Notice the words high and low are written in upper case.
This is the C++ style of writing the constants. HIGH and LOW are constants defined by Ar-
duino compiler as logic words 1 and 0. you can write 1 instead of HIGH and 0 instead of
LOW. However writing HIGH and LOW makes the code more easy to read and understand-
able to you and others if you share the code.
delay(500);
Delay() function takes only one argument, and it’s a number. This number indicates time in
milliseconds. Thus 500 means 500mS of delay. The program actually halts here for half a
second. During this time the previous state of pins remains unchanged. After a delay, or
waiting for half a second the program proceeds to next statement, which is again a digital-
Write() and that turns the LED off. Again a wait of 500mS and the loop function, starts over
again. This cycle keeps on repeating endlessly so the LED turn ON for half a second and
then turns OFF for half a second.
Assignments:
2. Change the Blink Cycle to 1/10 second ON and 1/2 second OFF (=> 100mS and 500mS)
3. Connect an LED to Digital Pin 6 (D6) and repeat the above examples
void setup() {
pinMode(6, OUTPUT);
pinMode(7, OUTPUT);
pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
pinMode(11, OUTPUT);
pinMode(12, OUTPUT);
pinMode(13, OUTPUT);
}
The above technique is simplest and most flexible, however it needs more typing and will
generate more code as similar lines of code are being executed repeatedly. Since our output
pins are from 6 to 13 in a simple ascending order sequence, we can make use of a variable
as counter and implement it in a loop.
void setup() {
for(int i=6;i<14;i++){
pinMode(i, OUTPUT);
}
}
For loop is a common method in C to implement a counter type loop. Here a local variable i
has been declared as type int (integer) and its initial value has been set to 6. the pinMode
functions accepts this variable as first argument, thus first time when loop begins the value
is 6, and pin 6 gets its mode. The counter is then incremented by 1 (i++) and the pinMode()
function is called again, this time I has a value of 7 now, so pin 7 gets its mode set. The loop
continues up to 13, when the value of i is incremented to 14 the loop terminates, thus setting
pinMode of all the pins from 6 to 13.
Lets write a simple program that should turn all these LEDs from D6 to D13 one by one
ON, and then OFF in the same sequence.
void loop() {
// Turning LEds ON one by One
for(int a=6;a<14;a++){
digitalWrite(a, HIGH);
delay(500);
}
// Turning LEds OFF one by One
for(int a=6;a<14;a++){
digitalWrite(a, LOW);
delay(500);
}
}
In the above example we have omitted the setup() function, I hope you understand that in
setup() function all pins from 6 to 13 should be set for OUTPUT. In the loop() section, there
are two for loops, the first loop runs from 6 to 13 and during each cycle of loop sets the cor-
responding pin HIGH, after that another loop runs that turns the corresponding LEDs OFF
and the cycle repeats endlessly.
In this second program, we will write code to make a chasing LED, in which one LED turns
ON, stays ON for a while and then turns OFF. Same steps are repeated for all LEDs in se-
quence.
void loop() {
// One LED Chase
for (int i = 6; i <= 13; i++) {
digitalWrite(i, HIGH);
delay(200);
digitalWrite(i, LOW);
}
}
In our next example we will modify the above code to make a ping-pong effect that is LED
chases forward and backwards continuously.
Notice in second loop, we are counting back, from 12 to 6, and decreasing the counter varia-
ble using i-- statement.
Binary 0b11001000
Decimal 200
Hexadecimal 0xC8
Internally every number is stored in binary format, however in program, we can use any for-
mat we like. The prefix 0b is written before the binary number (in program) to indicate that
the digits that follow are binary representation. Prefix 0x is used before the hexadecimal
numbers. Decimal numbers do not need any prefix.
Reading individual bit from a byte or variable
Some times we need to extract the individual bit setting in a variable. Say for example in a
byte sized variable, that has 8 bits, we want to know the value of 5th bit weather it is set to 1
or 0.
Unfortunately C language does not have any direct command to extract this information.
What we do is do a bitwise manipulation, or bitwise mathematics to extract this infor-
mation.
Bitwise And Operation
Bitwise operations are used to manipulate individual bits in a stored variable (or register). A
detailed discussion is beyond this text, and you may refer to various sources on internet to
read more about them and their uses. Here we will outline only the basic functionality as
required in this context.
Bitwise Shift Operations.
There are two bitwise shift operations, called shift right (>>) and shift left (<<). In shift
right operation all bits in the variable are shifted 1 position to the right, the right most bit
(bit 0) is dropped out. In Shift Left operation opposite happens and all bits are shifted to left
by 1 bit and most significant bit is dropped off.
As an example if we have a Binary number: n=0b10011001 a shift right operation would be
( n >> 1) the result will be a new number 0b01001100 we can shift any number of bits, like
(n>>4) will shift 4 bits not just 1. When we shift the bits, the right most bit is dropped an a
new bit is entered in most significant bit, and its value is 0.
Testing a Bit if its 1 or 0
To test a bit in a variable if its set to 1 or 0, we use a bitwise AND operation. First we shift
the required bit to 0 bit position and then do an AND operation with a number with 0th bit
set to 1.
Letes have a byte containing 0b10011001 number and we want to test if bit no 3 is 1 or 0.
n=0b10011001;
(n>>3) & 1
The second statement will shift the bits to right by 3 positions, making sure that bit no 3 is
at position 0. Now we AND it with 1 (0b00000001) so is the n>>3 has 0th bit set to 1 this
operation will return 1 otherwise 0.
Using this understanding now lets write a program that would show the bit pattern in a byte
on the 8 LEDs we have. Essentially we will have a byte sized variable and put some value
in it. Then test each of its bits from 0 to 7th if its 1 or 0 and then set the corresponding LED
ON or OFF.
Since our LEDs are beginning from pin 6 we will show the 0th bit on pin 6 and 1st bit on
pin 7 and so on.
// LED Binary
void loop() {
int count = 210;
for (int i = 0; i < 8; i++) {
digitalWrite(i + 6, (count >> i) & 1);
}
}
The test variable is count that has a value 210. Although the variable int is of two bytes (int
in C is 2 byte) the value 210 is within scope of single byte, so it will be stored in lower byte
of the count variable. Alternately you could take unsigned char as variable type, that will
ensure an 8 bits variable.
digitalWrite(i + 6, (count >> i) & 1);
This line is the key. (count >> i) shifts the ith bit to 0 position, then its ANDed with 1 and
the result is either 1 or 0 that is placed as second argument of digitalWrite function (1 is
HIGH and 0 is LOW).
Assignment:
1. Convert the above program to make a Binary counter that should display the entire range from 0 to 255
on LEDs with a delay of 500mS for each number.
Random Number
This example will introduce you to a random number generator function in Arduino. The
random() function lakes two arguments, minimum number and maximum number and it
will generate a random number in between these two limits, We will make an example to
generate a random number between 6 and 13 (as our Led pins are from 6 to 13) and then
light the LED corresponding to the number generated. The random number generator gener-
void setup() {
for (int i = 6; i < 14; i++) {
pinMode(i, OUTPUT);
digitalWrite(i, LOW);
}
}
// LED Random
void loop() {
int led = random(6, 14);
digitalWrite(led, HIGH);
delay(1000);
digitalWrite(led, LOW);
}
ates a number 1 less than maximum number. That’s why in program we have used random
(6,14).
Assignment:
1. Write a Program to simulate a rolling Dice. When program is turned on it just sets one led from LED0 to
LED5 indicating the dice as its rolled.
Scope of Variables
Strictly speaking this is a topic of C/C++ language, but I feel its important to discuss vari-
ous programming language features and tips in between the microcontroller related topics.
C programming language uses { … } these curly brackets to group various statements. Like
the statements that will be part of a function, or part of loop etc. the variables are declared
when the variable name is preceded by its data type like int, char, float etc. whenever a vari-
able is declared within the control structure, that is within the body of a loop, or within the
{…} brackets, it remains active and accessible only within this part. As soon as program is
done with this part of code, the variable is cleared from memory and the space occupied in
SRAM is freed to be used by other variables. This has two major effects, first you can use
the same variable name, in multiple parts of the program, without one affecting other. Sec-
ondly the SRAM is used in an optimistic way.
A variable that is declared outside any of these structures, specially outside the setup() or
loop functions, is declared as global or public. This variable remains active throughout the
life of program, and is accessible from any part of the program. Usually we use global vari-
ables to assign various setup data that we want to be accessible and modifiable across vari-
ous parts of the program. Beware of using too many global variables, because they keep the
SRAM reserved and thus limit the resources available for rest of the program.
Knight Rider Effect (Chasing LEDs)
Earlier in this chapter we have made an example, where a single LED scrolls forward and
backward in a loop to demonstrate the effect of swinging pendulum. In 1980’s there was a
popular TV serial ‘Knight Rider’ this was a fiction series where an autonomous hi-tech self
driving intelligent car was the central point of show. This car had a series of lights in its
front that used to scroll left to right and back. Two Lights were On at one time and when the
next light turns on then previous one turns off. Here is an example for this.
Now if you have LEDs connected to different pins in different order, just change the num-
bers in array, and the code remains same.
void setup() {
pinMode(BUTTON_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
delay(500); // Allow time to release button
}
}
This program will read the button attached to pin 2 of Arduino and toggle the state of LED
attached to digital pin 6. The MikroDuino board has this default setting so just enable the
jumpers of LED0 and SW1.
Macros in C
Macros are special compile time identifiers that are commonly used to represent constant
types of data. Like in this case BUTTON is a macro and it maps to number 2. When compil-
ing the program the compiler first scans the entire program to see if you have used BUT-
TON word anywhere, if used it will be replaced (in temporary file) with number 2. then the
program will compile. This way it is convenient to change the constant value at one place
and the entire code will adapt itself. Secondly since change takes place at compile time, it
does not consume SRAM. It is traditional to name the macro names in upper case to differ-
entiate them from variables.
So the program defines two macros, one for BUTTO_PIN and other for LED_PIN.
bool ledState = false;
This statement defines a global variable, of type Boolean. It can take only two states, like 1
or 0. Arduino defines these as true and false. This makes program more readable.
digitalRead(BUTTON_PIN)
This function reads the current state of digitalpin 2 (as defined by macro BUTTON_PIN) if
button is pressed this pin will read LOW (Gnd). The result is compared in an if() structure
and if button press is detected, the state of Boolean variable is reversed (!ledState) the !
(Not) operation sets true to false and false to true. The ledState is the used to set the status
of LED_PIN.
Button Debouncing
Buttons are mechanical devices, when a mechanical contact is made, its not instantaneous,
rather there are a series of very short contacts, loss of contacts, like thing, before finally set-
ting for a good contact. You may call it a noise, or we can think of it as a micro ‘spark’ type
thing that happens when a contact is made and current flows through it.
Since microcontroller is reading the pin for a very short period of time, and if its in a rapid
loop like thing, it may read the button press and release many times, whereas in fact the in-
tention of user is to press the button only once. This may produce unwanted effects in the
application. To counteract this, rapid bouncing of button various techniques are applied.
Some of these techniques involve additional hardware, while others use a software approach
to counteract this. In this text we discuss both, and then its up to you which one to choose.
Most people however choose software techniques, to reduce additional hardware.
Fix it in Hardware
The basic ide of hardware fix, it to implement a low pass filter. That should absorb the re-
dundant noise and let the signal pass through in a clean state. A simple solution is to use an
RC Filter. I will not go into the details of design, and
calculations, but only mention that using a pull-up resis-
tor and a capacitor can solve the problem to a large ex-
tent. Since most microcontrollers have internal pull-up
resistor, you can use it and simply use one capacitor.
We need to know the time duration during which the
oscillations take place, and we want to smooth out that
transition time. Usually this noise period lasts only 1mS
in most buttons. Here in this circuit C1 and RP1 are
forming the network filter. You choose these values, so
that the product R*C (R in Ohms and C in Farads) gives
you the time constant for which this filter would be ac-
tive. For a 10K resistor and 0.1uF capacitor this turns out to be 0.001S of 1mS. The resitor
R1 essentially forms a voltage divider when the button is pressed. You may eliminate it, or
if want to use it, its value must be significantly lower than pull up resistor, to ensure the
voltage produced on button press is close to 0V threshold of the microcontroller. If you
eliminate the 1K resistor, button press will discharge the capacitor rapidly and may produce
unwanted high frequency noise. While the filter smooths things out fairly well, it doesn’t
guarantee that you don’t get unlucky bounces that wiggle around just as the voltage on the
capacitor is reaching 2.5 V. If you pick the product of RC large enough, you’ll get away
with it most of the time.
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) { // Button Pressed
delay(50); // Simple debounce delay
digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // Toggle LED
}
}
time.
2. The above code is most simple form, it just reads the button, and on very first encounter
that it has ben pressed it weights for 50ms (for the bouncing noise to finish) and then as-
sumes that button has settled in its final position. There are two problems with this ap-
proach, one it is blocking in nature. The delay function does not let other parts of program
like sensors, or displays to be updated, second it does not detect when button is lifted. A
continuous press will keep the LED toggle multiple times.
3. One simple addition to it can be a code, that should ensure the program to proceed when
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) { // Button Pressed
delay(50); // Simple debounce delay
if (digitalRead(BUTTON_PIN) == LOW) { // Confirm press
digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // Toggle LED
}
while (digitalRead(BUTTON_PIN) == LOW); // Wait for button re-
lease
}
}
4 How It Works
1. Read the current button state from the input pin.
2. Compare it with the previous state to detect a change.
3. If the state changes from HIGH to LOW, introduce a short delay (debounce time).
4. After the delay, check the button state again—if it remains LOW, register a valid
press.
5. Store the new state as the last known state to prevent multiple detections.
#define BUTTON_PIN 2
#define LED_PIN 13
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
bool buttonState = digitalRead(BUTTON_PIN);
This method does not have a blocking loop to wait for button release and toggle the LED
many times. However it still has a small 50ms delay to overcome the debounce period.
Using millis() for non-blocking Debounce
In embedded systems, blocking code (such as using delay()) halts execution for a set peri-
od, making the system unresponsive to other inputs or tasks. If we use delay(50), the Ar-
duino will ignore everything else during that time, which is inefficient in a real-time sys-
tem. To solve this, we use non-blocking code with millis(), which allows the microcontroller to
continue running other tasks while keeping track of time for debounce processing.
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
bool reading = digitalRead(BUTTON_PIN); // Read button state
The non blocking code runs without blocking other commands, and it can update the dis-
plays, and read other sensors while monitoring if the button has been pressed. The technique
is slightly more complicated but it is scalable and can be used in many situations.
#define BUTTON_PIN 2
#define LED_PIN 13
void handleButtonPress() {
unsigned long currentTime = millis();
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
void loop() {
if (buttonPressed) {
buttonPressed = false; // Reset flag
digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // Toggle LED
}
}
Assignment:
1. Write a program to simulate a dice of 1 to 6 which should select an LED from1 to 6 at random on press
of a button.
CHAPTER 6
Communication is one of the most talked topic in embedded systems. Almost every micro-
controller application has to have a way to communicate with other devices, that are either
part of the same hardware or are separate devices all together. The smallest unit of data is a
bit, but microcontrollers and computer systems, group them as a byte and treat byte as a
unit. Thus all internal working of a microcontroller is byte based. A byte consists of 8 bits,
and therefore when communicating with another device we need to exchange 8 bits of data.
In simple terms this would need 8 digital IO lines between sender and receiver. An addition-
al clock line is required to synchronize data communication. The transmission of data will
be almost instantaneous as entire byte is transferred in a single clock pulse. The problem
with this method however is that it would require a large number of PCB tracks from one
chip to another, and if more than one chips are involved the routing will become even more
complex. The complexity of routing makes PCB size larger, as well as chances of introduc-
tion of external noise in any one track may affect the reliability of data exchange. This be-
comes more important when it comes to communication beyond the same hardware, to an
external device, like PC, or another control system. The cable will contain a large number of
wires, each likely to catch the environmental noise and distort the signal. A single misread-
ing of 1 or 0 will distort the message. The alternate is to keep the 8 bit version inside the
microcontroller and transfer the data using a single sire, in a series of bits, one by one.
This will also require a clock line to synchronize data between transmitter and receiver.
Now we have to take care of only two lines, its easier to route and even shield when trans-
mitting over longer distance.
There are many protocols as to how the data will be encoded, over the lines. One of the
most commonly used is USART or its simplified version UART. The USART protocol re-
quires a clock line for synchronization, whereas the UART does not require a synchronizing
clock signal. Instead it depends upon the commonly agreed speed of data transmission be-
tween sender and receiver system.
Baud Rate
Baud Rate defines the speed of communication (bits per second). Common baud rates are:
9600, 19200, 38400, 57600, 115200. Note the generation of baud rate depends upon the per-
formance of your microcontroller, its timers, and the clock speed. Not all baud rates are
generated exactly as intended. This produces some error. Both sender and receiver must use
the same baud rate for communication.
Data Bits
This defines the number of bits that will form one byte. In early days when only text was
transmitted 7 bits of data were used. Now however data may consist of binary information,
so an entire 8 bits are used.
Stop Bits
Stop bits indicate the end of transmission. Common standards are 1, 1.5 and 2 stop bits (1
stop bit is common).
Parity Bit
Parity bit is used to detect error during communication. Option are none, Even parity or Odd
parity. The parity bit represents the number of 1s in data.
Flow Control
This prevents data loss by managing flow of data between sender and receiver. It nan be
None (most commonly used), Software (XON/XOFF) or Hardware (RTS/CTS or DTR/
DSR)
Synchronization
This setting can be asynchronous or synchronous. In synchronous setting, an additional
clock signal is required to synchronize. In asynchronous the communication reliability de-
pends upon baud rate and data frame. We use asynchronous most of the times.
Serial Communication in Arduino
Arduino offers two types of serial communication. One uses the dedicated hardware module
of the Arduino, and other use software techniques to implement it. Hardware module UART
is better and used most of the times. The communication takes place through dedicated Tx
and Rx pins. Arduino IDE offers a built-in object called ‘Serial’. You don’t need to define
this object.
Setting baud rate in Arduino is all that is required to start communication.
Set using Serial.begin(baudRate);
Example: Serial.begin(9600); (sets baud rate to 9600 bps).
Data Bits, Parity and Stop Bits
These are set optionally as a second parameter of the Serial.begin() function.
Configured using Serial.begin(baudRate, configuration);
Default setting: 8 data bits, no parity, 1 stop bit (8N1).
After you initialize the Serial object, make sure the receiver is also configured to use the
same settings.
Sending Data
Receiving Data
While receiving you first check if there is a data present in the receive buffer or not. Since it
Serial.print("Hello"); // Sends "Hello" as ASCII characters
Serial.println(123); // Sends "123" followed by a newline
Serial.write(65); // Sends binary value 65 (ASCII 'A')
is not known when the sender will send data, in its absence the receive buffer is empty. The
if (Serial.available()) { // Check if data is received
char data = Serial.read(); // Read a single byte
}
UART buffer in ATMega328 is 64 bytes. There are separate buffers for Transmitter and Re-
ceiver parts. You can change this setting in ‘hardwareSerial.h’ file in Arduino installation if
required.
This function returns the number of bytes present in the buffer. When you read one byte,
that byte is removed from buffer.
If we want to ensure that all data in Tx buffer is sent and its clear, before sending further
data:
Serial.flush(); // Waits for outgoing data to be transmitted
Arduino does not support hardware flow control, and software flow control must be imple-
mented if required.
What if we need two Serial Ports.
The ATmega328 has only one hardware serial module, and therefore only one set of Tx and
Rx pins. In case we need another port in Arduino, we can use software library to implement
an additional serial communication port. The software library can use any digital GPIO
lines to act as TX or Rx pins. The software serial communication however generates more
code, and consumes more program memory.
The above code shows how to implement the software Serial library. We have used pin 10
and 11 for Rx and Tx, you can use any two digital pins.
#include <SoftwareSerial.h>
SoftwareSerial mySerial(10, 11); // RX, TX on pins 10 and 11
mySerial.begin(9600);
mySerial.println("Hello from SoftwareSerial");
Example Programs to Demonstrate Serial Communication
Basic Serial Communication (Hello World)
This program sends the message “Hello Arduino!” to the serial monitor every second. Each
message is on a new line.
To view this data you must turn on your serial monitor. This is built-in available within Ar-
void setup() {
Serial.begin(9600); // Start serial communication at 9600 baud
}
void loop() {
Serial.println("Hello, Arduino!"); // Send data to Serial Monitor
delay(1000); // Wait for 1 second
}
Hello, Arduino!
Hello, Arduino!
...
Serial Monitor
Now lets write a program, that reads data from serial device and also writes data back to the
serial device.
void loop() {
if (Serial.available()) { // Check if data is available
char receivedChar = Serial.read(); // Read the received character
Serial.print("You sent: ");
Serial.println(receivedChar); // Echo back the received character
}
}
We have omitted the setup() where you will initialize the Serial port using Serial.begin()
command. Now open the Serial monitor and type any character and press enter. The Ar-
duino will respond by sending the same character back.
Assignment:
1. Change the above program to return back the upper case letter. Like if you type ‘h’ it should return ‘H’
void loop() {
if (Serial.available()) {
int num = Serial.parseInt(); // Read an integer
Serial.print("Double of ");
Serial.print(num);
Serial.print(" is ");
Serial.println(num * 2);
}
}
nately Serial object, gives us a function parseInt() that reads the numeric value from serial
device and converts it into an integer value.
1. Read a value from 2 to 16 from Serial monitor and return a times table (1 to 10) for the given Number.
void setup() {
Serial.begin(9600); // Initialize Serial Communication
Serial.println("Welcome to the Number Guessing Game!");
Serial.println("Type 'start' to begin.");
}
void loop() {
if (Serial.available()) {
String input = Serial.readString(); // Read user input
input.trim(); // Remove any newline characters
if (input == "start") {
startGame();
}
else if (gameActive) {
int guess = input.toInt(); // Convert input to integer
void setup() {
Serial.begin(9600); // Initialize Serial Communication
Serial.println("Think of a number between 1 and 100.");
Serial.println("Type 'start' when you're ready.");
}
void loop() {
if (Serial.available()) {
String input = Serial.readString(); // Read user input
input.trim(); // Remove extra spaces and newline characters
if (input == "start") {
startGame();
}
else if (gameActive) {
if (input == "H") {
high = guess - 1;
makeGuess();
}
else if (input == "L") {
low = guess + 1;
makeGuess();
}
else if (input == "C") {
Serial.println("?? Yay! I guessed your number! ??");
Serial.println("Type 'start' to play again.");
gameActive = false;
}
else {
Serial.println("Invalid input! Reply with 'H' for too high, 'L'
for too low, or 'C' for correct.");
}
}
else {
Serial.println("Type 'start' to begin the game.");
}
}
}
void setup() {
Serial.begin(9600); // Initialize Serial Communication
Serial.println("Simple Calculator");
Serial.println("Enter an expression (e.g., 5 + 3) and press Enter:");
}
void loop() {
if (Serial.available()) {
String input = Serial.readString(); // Read user input
input.trim(); // Remove spaces and newlines
two integers, and the operand from expression. Then undergo mathematical evaluation and
give the result.
The key function here is sscanf() that takes the input string as an argument, and number of
parameters to be separated as arguments. The & before arguments indicates the address of
variable, so that function can write values directly into the variable.
Industrial Communication protocols for UART
So far we have covered the basics of Serial communication and then we have seen how to
implement it in Arduino. Now lets dive a little deeper to see how all this can be applied in
industrial or even in a practical environment.
TTL Communication
So far we are communicating with the standard digital logic levels that is 5V and 0V as log-
ic 1 and logic 0 respectively. Some other microcontrollers will use 3.3V as logic 1. this is
alright for communication within the same hardware or for short distances. When the dis-
tance increases, the ling wires tend to increase the resistance resulting in loss of signal
strength as well as tend to act like ‘antennas’ and pick up the noise from surroundings. The
result is poor communication and unreliable transmission.
adds further noise immunity. 120 ohms terminal resistors are needed to reduce signal re-
flections.
MAX485 is an easily available chip that does the job. You can give it TTL logic and it will
convert the data to RS485 standards. You use one module for half duplex, and two modules
for full-duplex communication.
CHAPTER 7
We live in an analog world, but process this information in digital domain. The analog data
is abundant in nature. Almost all physical properties of matter and event quantum mechan-
ics are actually analog values. Though there are certain matters that can not be quantified as
analog, but we know they do exist. One of these is fragrance, we can appreciate fragrance
but cannot quantify it, similarly taste, beauty, softness etc these are all physical qualities we
come across and use in our everyday life but we cannot quantify them, or its not easy to
quantify them reliably.
There are so many ways the analog data exists in nature, like if we talk about light only, it
has intensity, color, frequency, wavelength, save shape or speed and may be many other pa-
rameters that describe a given sample of light. Our electrical systems, unfortunately have a
handful ways of detecting or quantifying these properties.
Simplest is voltage, if we can somehow convert an analog property into equivalent or repre-
sentative voltage, we can read it in our electrical systems. Second is current, provided volt-
age remains constant, current flow can be used as a parameter to assess some physical prop-
erty. Others are change in resistance or capacitance of a substance that can be influenced by
the physical parameter. The change in resistance or capacitance can be translated into two
measurable parameters, voltage or oscillating frequency.
Analog Voltage
Analog voltage is a continuous variable, that contains unlimited number of values. Even if
we have an absolute upper and lower limits, the physical property may translate into volt-
age, that can have unlimited values. Unfortunately our digital measuring systems have a
limit, depending upon the width of variables we choose. By width I mean number of bits.
This is expressed as the resolution of digital system to acquire analog data. Second parame-
ter is how fast the system can acquire the analog data, that is the time slice, before the ana-
log volts change. This is termed as sampling rate.
The analog data or voltage can have rapid fluctuations, and a slower acquisition speed is
likely to miss the signal value in between the samples. So there are practically two issues
with digital conversion of analog data. First limited quantification, and secondly chances of
missing a data in between the samples. Moreover the actual data or voltage can also have
negative voltage, that is difficult to measure by the regular microcontroller electronics.
Arduino ADC
The Arduino microcontroller ATMega328P has an analog to digital conversion module that
uses a simpler but effective technique of acquiring analog data. The Arduino nano has 8
channels to acquire data, but they all are
internally connected to one ADC module.
So you can select the channel to read, but
only one channel can be read at any given
time.
Internally a small capacitor is charged
with the incoming voltage, Which is fed
to the input of a comparator. The other
side of comparator is connected to a digi-
tal to analog converter (DAC), like an
R2R resistor network. Now the microcontroller tries to approximate the voltage of capacitor
by setting different values in the DAC, until the two voltages become equal and comparator
output becomes 0.
The Arduino has 10 bits of DAC so the input sample can have a 210 number of discreet val-
ues. This turns out to be 1024. The input sample is therefore broken down into 1024 possi-
ble levels, that this module can measure. So if we are measuring a maximum of 5V the pos-
sibility of input voltages can be 0-5V this 5V spectrum will be divided in 1024 steps (0-
1023). So that when the voltage is 0V the output is digital number 0. when it is 5V the out-
put is 1023 (10 bit digital number). This gives a resolution of 5/1024 = 0.00488V or
4.88mV per step. So if voltage is 4.88mV the digital output will be number 1. with this res-
olution we cannot measure the voltage of 2mV for example. So all measurements will be in
steps of 4.88mV.
To improve upon this ADC uses another technique, in which the range is reduced, but num-
ber of bits remain same. So if we know that our input signal will never be more than 3.3V
we use this as a standard and instruct the ADC to consider 3.3V as highest (instead of 5V)
and distribute the 1024 steps from 0-3.3V. This will effectively decrease the voltage step
size. Thus 3.3/1024 = 0.0032 or 3.2mV. This will however unable to differentiate in voltag-
es above 3.3V. This instruction of ceiling voltages is called ‘Reference Voltage’. Usually
Arduino uses the supply voltage (5V) as a reference by default and therefore we tend to
have a measuring steps of 4.88mV. Other possible references are an internal calibrated 1.1V
reverence. Using this as reference will give a maximum input volts of 1.1V but give steps of
1.1/1024 = 0.00107 or 1.07mV steps. There is a special pin labelled as Aref on Arduino. We
can configure the ATMega328 to use the voltage applied on this pin as reference for the up-
per limit. Usually we apply an external 2.5V to this pin. This gives steps of
2.5/1024=0.00244 or 2.44mV steps.
In any case the Arduino pins cannot have a negative voltage, and no more than 5V on sig-
nal. More than 5V will damage the microcontroller pin.
tion. A practical method of acquiring weak signals in reliable way is to use a pre-
conditioning circuit or amplifier. An operational amplifier can be used to amplify the weak
signals. You can find many different amplifier designs to acquire small analog signals into
Arduino.
Pre-Conditioning Large Signals
Arduino pins can take only positive voltages as input, and the applied voltage must not be
greater than 5V. Most industrial sensors and equipment use 12V signals, these must be
scaled down to safe levels before applying to the Arduino pins. Moreover the industrial
noise may include negative voltage spikes that can damage the pins. Although the manufac-
turers of ATMega328 and other microcontrollers have clipping diodes present on the pins,
that tend to protect the pins from negative spikes, but these diodes are of limited capacity.
Use schottky diodes like BAT54 or 1N5819 in the above configuration. When a negative
voltage spike comes it shunts the spike to GND. The schottkey diodes have low forward
voltage (~0.3V) so they start conducting early. Also a 1K to
10K resistor in series protects against rush of current through
these diodes.
If your signal is above 5V it is essential to scale it down to
bring it into safe 5V range. Use a simple voltage divider to scale
down the input voltage.
Choose resistor values in K Ohms, so that the signal current is
low, and voltages of signal do not drop erroneously due to cur-
rent sinking.
I prefer to reduce the input voltage by 3 times or more depend-
ing upon the input voltage. Then calculate the scale by dividing
the input with output, so if the scale 2.4 for example I will mul-
tiply the sensed voltage with 2.4 to get the original voltage in
program.
I choose R2=4.7K and R1=10K this gives a scale down of 3.1 times. Always combine this
with Schottky diode protection to protect against ESD spikes. Though voltage divider gives
a good cushion, as 12V will be scaled down to 3.8V, still an accidental 48V signal will give
higher voltages. In case such a possibility exist, add a 5V Zener diode in parallel to the pin.
Sampling Rate
Analog to digital conversion takes some time, this conversion time depends upon the under-
lying technology used for this conversion. Arduino uses ‘Successive Approximation’ tech-
nique. In this technique the sample voltage or signal is applied to the input of a comparator
(sometimes using a charging capacitor) and then using a DAC, various voltages are applied
to the other input of comparator, till both inputs become equal. The digital value that finaliz-
es the output is reported to the MCU.
The ADC conversion depends upon the ADC clock, which is derived from system clock.
The maximum ADC clock is 200KHz for accurate reading, usual ADC clock is 125KHz. A
single ADC conversion takes 13 ADC Clock cycles. This translates to approximately 104
uS per conversion at 125KHz ADC clock resulting in a sample rate of approximately
9.6KHz.
Pre-scaler
Pre-scaler is a technique used to reduce the clock frequency. The Arduino ADC needs a
clock frequency between 50KHz to 200KHz for accurate conversion. Since clock is derived
from system clock of 16MHz, it needs to be divided many times to achieve this level of
scale down.
The default prescaler of Arduino is 128 that results in ADC clock frequency of 125KHz.
Lower the frequency better is adc accuracy. Increasing frequency increases speed but reduc-
es accuracy.
You can change the ADC pre-scaler or other settings by direct register manipulation. Set-
ting appropriate bits in ADCSRA register will set the different pre-scaler.
How to implement ADC in Arduino
As already explained, Arduino IDE by default sets the most commonly used and acceptable
settings. So all you need is to read the appropriate channel. Arduino UNO has 6 ADC chan-
nels, named from A0 to A5 while Arduino Nano has 8 channels, A0 to A7. Since ADC re-
turns a 10 bit value, the variable to hold the digital value must be able to hold 10 bit num-
ber. Since ‘int’ datatype is two bytes long, and it has 16 bits, it can easily store the 10 bits
value.
int sensorValue = analogRead(A0); // Reads the analog voltage at pin A0
The analogRead() function does the magic. It accepts an argument which is channel num-
ber. A0 is internally defined as number 14. You can use number 14, but preferred way for
Arduino is to use the internally defined pin numbers as A0, A1 etc. This function reads the
analog pin A0 for analog signal and converts it to equivalent integer value. 0V=0 and 5V =
1023 similarly 2.5V = 512 and so on. The conversion takes 104 uS and the function waits
for the conversion to finish.
Setting Analog Reference Volts
By default the analog system of Arduino uses the supply voltage (5V or 3.3V whatever it is)
as the reference volts. This voltage is used to map the digital output of all 10 bits of ADC
output set to 1. This means 5V will return 1023 value.
Reference can also be set to internal calibrated 1.1V or external whatever volts we apply on
AREF pin/
analogReference(INTERNAL); // Use internal 1.1V reference
void loop() {
int sensorValue = analogRead(POT_PIN); // Read the analog input (0-1023)
Serial.println(sensorValue); // Print the value to Serial Monitor
delay(500); // Delay for readability
}
void loop() {
int sensorValue = analogRead(POT_PIN); // Read analog input
float voltage = sensorValue * (5.0 / 1023.0); // Convert to voltage
Serial.print("ADC Value:" );
Serial.print(sensorValue);
Serial.print(" Voltage: ");
Serial.print(voltage);
Serial.println(" V");
delay(500);
}
// LDR Demo
#define LDR_PIN A6
#define LED_PIN 6
void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}
void loop() {
int lightValue = analogRead(LDR_PIN);
Serial.println(lightValue);
delay(500);
}
void setup() {
Serial.begin(9600);
}
void loop() {
int sensorValue = analogRead(TEMP_SENSOR);
float temperature = sensorValue * (5.0 / 1023.0) * 100; // Convert to °C
Serial.print("Temperature: ");
Serial.print(temperature);
Serial.println(" °C");
delay(1000);
}
Assignments:
1. Using 8 LEDs, make a bar-graph to correspond the ambient light. When its total dark all 8 LEDs should be
ON and when full bright All LEDs Off.
2. Make a bar-graph using 8 LEDs to indicate the ambient temperature. All lights on when upper limit of set
temperature is reached, and one LED when lower limit is reached.
3. Make a temperature alarm system, get the upper limit of set temperature from USART (Range between
30-50) and monitor temperature using LM35. On reaching set limit, turn an LED On (indicating Alarm)
when temp lowers, it should turn Off
void setup() {
for (int i = 0; i < NUM_LEDS; i++) {
pinMode(ledPins[i], OUTPUT); // Set LED pins as output
}
}
void loop() {
int potValue = analogRead(POT_PIN); // Read potentiometer value (0-1023)
int ledLevel = map(potValue, 0, 1023, 0, NUM_LEDS); // Map to LED range (0-
8)
When connecting to Arduino, we can charge the capacitor through a known resistor 10K,
first discharge the capacitor by setting the pin LOW, and then making it HIGH. This will
start charging the capacitor. Start monitoring and counting time. Charge the capacitor volts
till it reaches 63% of the supply volts. For Arduino 5V supply its 3.15V or analogread value
645. When this value is reached timer count is stopped and t is calculated. If time is meas-
ured in Seconds capacitance is given in Farads, if measured in microSeconds capacitance is
given as uF.
void setup() {
Serial.begin(9600);
}
void loop() {
// Discharge capacitor
pinMode(chargePin, OUTPUT);
digitalWrite(chargePin, LOW);
delay(100);
// Prepare to measure
pinMode(chargePin, INPUT); // Hi-Z state
delay(10);
// Start charging
pinMode(chargePin, OUTPUT);
digitalWrite(chargePin, HIGH);
delay(2000);
}
CHAPTER 8
Pulse width modulation or PWM, is a waveform with two levels of pulses, On and Off.
One complete wave cycle starts from 0 to 1, stays 1 for some time, and then goes low and
remains low for some time. And then the cycle repeats. The total time it take for one cycle
is the wavelength. PWM relates the ratio between On time and OFF time during a cycle.
Irrespective of the total time of cycle. Thus if the On and Off times are equal it is termed as
50% duty cycle. It is customary to define a PWM signal as the percentage of On time. The
Off time is automatically assumed to be 100-duty cycle.
Although as I have already said we can encode information on this, using a defined protocol
to describe what our data is. But in general PWM technique is used to produce Analog Sig-
nals or to transfer a varying power to a device using digital signals.
Just imagine if the duty cycle is 100%, it means the pin, or output is continuously ON, this
means that the target device is getting full power. However if we turn the pin ON for half
the time, and keep it OFF half the time, the target device is getting intermittent power, and
averaging it over time, we can say it is getting 50%of the total power.
This technique is used to control the speed of motors, brightness of LEDs or even produce
various analog waveforms. The analog waveforms cannot be described as truly analog, but
you can say they resemble analog signals.
Due to its widespread use in electronics and projects, almost every microcontroller is
equipped with hardware modules to continuously produce PWM signals. Although these
signals can be produced in a simple loop, yet the streaming of PWM signal will be affected
when microcontroller is busy in some other loop or task. The internal timers in microcon-
trollers can run autonomously in background and can be used to produce stream of PWM
signals.
PWM in Arduino
Theoretically PWM can be produced on any digital GPIO line, but since internal timers are
used to produce PWM, specific GPIO pins are dedicated to produce PWM signals. On Ar-
duino boards a ~ sign is printed along with pin number to indicate the PWM capability of
the pin. The Arduino nano has 6 independently controllable pins that can be used to produce
PWM signals. These are D3, D5, D6, D9, D10 and D11. The analogWrite(pin,
value) function is used to generate PWM signals, where the value can range from 0
(0% duty cycle) to 255 (100% duty cycle). PWM on Arduino typically runs at a frequency
of around 490 Hz or 980 Hz, depending on the pin.
The table above shows various frequencies at which PWM waves are generated on Arduino
pins. Since PWM generation depends upon timers, any other functions that influence timer
settings, are likely to influence the PWM behavior.
// PWM DEMO
// Pin connected to LED
const int ledPin = 9; // Use any PWM pin (e.g., 3, 5, 6, 9, 10, 11 on UNO)
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
// Increase brightness
for (int brightness = 0; brightness <= 255; brightness++) {
analogWrite(ledPin, brightness);
delay(10);
}
// Decrease brightness
for (int brightness = 255; brightness >= 0; brightness--) {
analogWrite(ledPin, brightness);
delay(10);
}
}
Since PWM is being controlled by timers, once you set a duty cycle, it tends to remain in
effect, no matter what else the program is doing.
Assignment:
void setup() {
pinMode(fanPin, OUTPUT);
}
void loop() {
// Ramp up
for (int speed = 0; speed <= 255; speed++) {
analogWrite(fanPin, speed);
delay(10);
}
// Ramp down
for (int speed = 255; speed >= 0; speed--) {
analogWrite(fanPin, speed);
delay(10);
}
delay(1000);
}
void setup() {
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
}
void loop() {
analogWrite(redPin, 255); // Red full
analogWrite(greenPin, 0);
analogWrite(bluePin, 0);
delay(1000);
analogWrite(redPin, 0);
analogWrite(greenPin, 255); // Green full
analogWrite(bluePin, 0);
delay(1000);
analogWrite(redPin, 0);
analogWrite(greenPin, 0);
analogWrite(bluePin, 255); // Blue full
delay(1000);
}
Now lets have the fun part, of controlling multiple LEDs in RGB LED, to mix colors.
//PWM RGB LEDs
const int redPin = 9;
const int greenPin = 10;
const int bluePin = 11;
void setup() {
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
}
void loop() {
// Fade red up, others off
for (int i = 0; i <= 255; i++) {
analogWrite(redPin, i);
analogWrite(greenPin, 0);
analogWrite(bluePin, 0);
delay(10);
}
// All off
analogWrite(redPin, 0);
analogWrite(greenPin, 0);
analogWrite(bluePin, 0);
delay(1000);
}
Assignment:
Here is a version to show different colors using a push button. The program also demon-
strates use of a function.
int mode = 0;
bool lastButtonState = HIGH;
void setup() {
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
}
void loop() {
bool buttonState = digitalRead(buttonPin);
switch (mode) {
case 0: setColor(255, 0, 0); break; // Red
case 1: setColor(0, 255, 0); break; // Green
case 2: setColor(0, 0, 255); break; // Blue
case 3: setColor(255, 255, 0); break; // Yellow
case 4: setColor(0, 255, 255); break; // Cyan
case 5: setColor(255, 0, 255); break; // Magenta
case 6: setColor(255, 255, 255); break; // White
}
}
A low pass filter essentially consists of a resistor and a capacitor. The values of resistor and
capacitor are important in selecting the frequency that needs to be passed and filter out other
frequencies. The cutoff frequency is calculated using this formula.
The carrier frequency of PWM signal is 980Hz so the signal that we are going to produce on
it must be below this frequency. Lets suppose we want a signal to be 100-200Hz to be mod-
ulated on this PWM frequency.
Using a resistor of 1K and a capacitor of 1uF we can easily produce a wave on PWM. You
will need an oscilloscope to see the signal.
Let's create an Arduino program that generates a sawtooth wave using PWM output and
your RC low-pass filter (designed for ~980 Hz PWM). The RC filter will smooth the PWM
signal into an analog-style sawtooth wave. A sawtooth wave gradually increases in ampli-
tude, reaching maximum and then abruptly drops to zero and start over.
Using PWM we will modulate this as gradually increasing the PWM duty cycle from 0 to
255 and then dropping back to 0 and repeat. The slope of wave will be determined by delay
in between the changing duty cycles.
// Sawtooth Wave useing PWM
const int pwmPin = 5; // PWM-capable pin (pins 5 or 6 have ~980Hz PWM)
const int maxValue = 255; // 8-bit PWM range
void setup() {
pinMode(pwmPin, OUTPUT);
}
void loop() {
for (int value = 0; value <= maxValue; value++) {
analogWrite(pwmPin, value); // Set PWM duty cycle
delayMicroseconds(200); // Controls the frequency of the wave
}
}
This trace of oscilloscope has been takes from D5 pin output this shows the PWM signal.
Notice how the duty cycle is changing from left to right. With narrow pulses to start with
and gradually increasing the pulse width.
This trace shows the output of the filter. Notice How the voltage is gradually increasing and
then dropping. Though PWM signal is digital, with filter we can convert it to something like
analog. This is not truly analog as it contains the switching noise. With better filters and im-
proving frequency a better wave can be produced.
Waveform shape: Linear rising voltage from 0V to ~5V
Frequency control: Delay × 256 steps = one full cycle Example: delayMicroseconds
(200) → ~50 Hz wave (200 µs × 256 ≈ 51.2 ms per cycle → ~19.5 Hz)
Try reducing the delay for a faster wave (e.g., 100 µs for ~40 Hz).
Similarly you can produce a variety of signals, like Ramp Wave, Sine Wave, Triangular
wave etc.
I2C Communication
Inter Integrated Circuit Communication or I2C is a very popular protocol for multiple de-
vice communication within a system. It was developed by Philips Semiconductor (now
NXP) in the 1980s. It is designed for short distance, low speed communication between
multiple devices on the same circuit board. This communication protocol is particularly use-
ful in embedded systems and microcontroller projects. This protocol uses two digital lines,
named as SDA and SCL, SDA line is bidirectional and used to send and receive data while
SCL is clock line and used to synchronize communication. The SCL line is controlled by
the master device, all other devices are called slave devices that follow the master com-
mands. The standard practice is to have one master device and one or more slave devices,
but it is possible to have more than one masters, called multi-master system. In any case on-
ly one master is communicating at one time.
Each slave device has a unique 7-bit address that master can use to communicate selective-
ly. Some systems now allow 10-bit addressing too, but this 7-Bit addressing is fairly stand-
ard. The commercially produced integrated circuits and devices have their unique addresses
assigned by the I2C Bus Committee run by NXP semiconductors. In large scale for com-
mercial applications conflict of address by some devices can cause serious issues.
The address of a particular I2C device is hard coded in the chip, however some manufactur-
ers hard code a few bits of the address and allow few other bits to be set by the user to allow
flexibility and add more devices to the circuit. The same bus is shared by all devices, the
entire bus needs to have a single set of Pull-Up resistors.
Various communication speed modes are available, the standard is 100 Kbps others are 400
Kbps, 1 Mbps and 3.4 Mbps. (bits per second). The master device initiates communication
by sending a start condition, followed by the address of the target slave and a read/write bit.
Data is then exchanged in 8-bit frames, each followed by an acknowledgment (ACK) bit.
Communication ends with a stop condition sent by the master.
First you need to include the <Wire.h> library. Notice the ‘W’ is capital. You can also in-
clude it by selecting wire library from library manager in Arduino IDE. Rest of the com-
mands are self explanatory. The Wire object is automatically created. Many other libraries
use this library in background, so they may require you to include this library in the project.
The wire library uses interrupts to manage I2C events in the background. It uses the internal
32 Bytes storage registers for I2C data.
When all I2C devices are connected you can access them using their address. The address is
provided by the manufacturer. However since address is a 7 bit number, we can write a
scanning program to try to ping all the numbers, and see which ones respond.
This I2C scanning program is very common among the Arduino community. Sometimes it
is essential because the manufacturers, that are not registered with NXP semiconductors,
tend to assign their own addresses which may not be the ones mentioned in datasheets.
//I2C Address Scanner
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(9600);
while (!Serial); // Leonardo: wait for Serial Monitor
Serial.println("\nI2C Scanner");
}
void loop() {
int nDevices = 0;
Serial.println("Scanning...");
if (error == 0) {
Serial.print("I2C device found at address 0x");
if (address < 16) {
Serial.print("0");
}
Serial.print(address, HEX);
Serial.println(" !");
++nDevices;
} else if (error == 4) {
Serial.print("Unknown error at address 0x");
if (address < 16) {
Serial.print("0");
}
Serial.println(address, HEX);
}
}
if (nDevices == 0) {
Serial.println("No I2C devices found\n");
} else {
Serial.println("done\n");
}
delay(5000); // Wait 5 seconds for next scan
}
CHAPTER 10
Displays are an important part of embedded systems. They tend to provide a convenient in-
terface for the embedded system to communicate with the user. Just like buttons and various
other input devices that are used to get user input, displays are used to
give outputs in text or graphical form to the user. Apart from LEDs
which are the simplest way of giving system status to the user, seven
segment, LED display, LED matrix displays and many other devices
that are based upon LEDs are used as output devices.
Liquid crystal displays come in various forms, some are general pur-
pose while others are customized for a particular device. Liquid crys-
tal displays are in market since long, as they are not energy hungry
like LEDs, and they are easy to manufacture. They have been frequently used in wrist
watches, calculators or fuel stations. One of the important features of this type of display is
that it has a very good contrast ratio, and readability is very good in daylight. In dim light or
room applications you need an illumination system, either a side LED light or a backlight.
In the absence of this LED the native liquid crystals require a very few current and therefore
are battery friendly.
If you look at the physics of these displays, the liquid crystals spin under the influence of
applied voltage and therefore conduct or block the light to pass through, in other words they
change the polarization of light resulting in black display segments. These crystals cannot
be held in this position for a long time by constantly applying the volts, as in case of LEDs.
Rather they need an alternating current signal at a frequency of about 100 Hz to keep them
spinning and display the segment. A special display driver is therefore needed to keep these
segments oscillating with an alternating current. Some manufacturers make these drivers
along with the display, while others just sell
the bare display with connections, and let the
electronics engineer to work around the dis-
play.
Character LCD
In the display above the characters or seg-
ments are arranged as digits, or fixed sym-
bols. However in order to make a general pur-
pose display a character LCD matrix was developed. This matrix contains LCD dots about
5x8 in each character. The entire display contains several such characters. Various charac-
ters and graphics can therefore be displayed on these matrix characters. Many manufactur-
ers used to make these displays, and they tend to have their own driver circuitry. Since
drawing characters, and managing the alternating current signals to each dot (or pixel) in
display is a huge task.
Hitachi company introduced an LCD controller HD44780, it was easy to program, and use.
Displays using this controller became widely used in industry and among electronic equip-
ment manufacturers. Today HD44780 controller based character LCDs are available in gen-
eral market as standalone products that can be used in any project. These LCDs are being
manufactured by many different manufacturers, but they all comply to the same standard.
They may differ in the quality of display but interfacing is same.
These displays typically have 16 pins for power, contrast, control , data and LED. The dis-
play can be driven from 3.3V to 5V. VE is the contrast pin, and a potentiometer is usually
attached to this pin to adjust the contrast, Register select pin is used to select the two regis-
ters, either a command register or data register. Read/Write pin is used to select the opera-
tion, if we want to write to the LCD or read data from it. Most of the times we do not need
to read from LCD, so this pin is permanently configured as ‘write’ by connecting it to GND.
Enable pin is used to push the data into the LCD. D0 to D7 are 8 bits of data. The LCD can
be configured to use either all of these 8 bits, or only 4 bits. In 4 bit mode only pins D4-D7
are used to send a byte of data in two chunks. The LED backlight can be hard wired to pow-
er or through a transistor and controlled via microcontroller pin. When using this display in
4 bit mode, which is the most commonly used mode to save the microcontroller digital IO
lines, You need 4 lines for data, and two for control register like RS and E. So at least 6 dig-
ital lines are required to use the character LCD with a microcontroller.
When using the character LCD in this way, there is a built-in Arduino library to control it.
void setup() {
// set up the LCD's number of columns and rows:
lcd.begin(16, 2);
// Print a message to the LCD.
lcd.print("hello, world!");
}
void loop() {
// set the cursor to column 0, line 1
// (note: line 1 is the second row, since counting begins with 0):
lcd.setCursor(0, 1);
// print the number of seconds since reset:
lcd.print(millis() / 1000);
}
Using Arduino Libraries
Although libraries are a common concept in C, they just need to be included by the header
file, and call the public functions. Arduino libraries are little different. These are actually
classes of objects, and therefore to use the library you have to include its class, by including
the .h header file. After that you have to instantiate an object of type, library, this object will
contain all the code of the library. The object oriented nature of Arduino libraries, exposes
only the public functions and variables to the user (programmer) and hides all other private
functions and internal variables. Moreover the library allows you to create more than one
objects of the type, and access them completely independently.
#include <LiquidCrystal.h>
lcd.begin(cols, rows)
This sets the dimensions of the LCD. Most commonly used sizes are 16 columns by 2 rows (16x2) or 20x4. It
must be called inside the setup() function.
Usage: lcd.begin(16, 2);
lcd.print(data)
Prints characters, strings, or numbers to the LCD screen starting from the current cursor location. It works
like Serial.print().
Usage: lcd.print("Hello, World!");
lcd.write(byte)
Writes a raw byte to the display. It is mainly used to write custom characters that were previously defined.
Usage: lcd.write(0);
lcd.setCursor(col, row)
Sets the position of the cursor on the screen. Both column and row numbers start from 0.
Usage: lcd.setCursor(0, 1);
lcd.clear()
Clears all content on the LCD and moves the cursor to the top-left corner (0,0).
Usage: lcd.clear();
lcd.home()
Moves the cursor to the top-left corner without clearing the display content.
Usage: lcd.home();
lcd.display()
Turns the display back on if it was previously turned off with noDisplay(). Content remains unaffected.
Usage: lcd.display();
lcd.noDisplay()
Turns the display off without erasing its contents. Useful for power saving or blinking effects.
Usage: lcd.noDisplay();
lcd.cursor()
Displays an underscore-style cursor at the current position.
Usage: lcd.cursor();
lcd.noCursor()
Hides the underscore-style cursor from the screen.
Usage: lcd.noCursor();
lcd.blink()
Enables a blinking block cursor on the screen at the current location.
Usage: lcd.blink();
lcd.noBlink()
Disables the blinking cursor.
Usage: lcd.noBlink();
lcd.scrollDisplayLeft()
Scrolls all display text one position to the left.
Usage: lcd.scrollDisplayLeft();
lcd.scrollDisplayRight()
Scrolls all display text one position to the right.
Usage: lcd.scrollDisplayRight();
lcd.leftToRight()
Sets text direction from left to right, which is the default behavior.
Usage: lcd.leftToRight();
lcd.rightToLeft()
Reverses text direction to right to left, useful for some languages or effects.
Usage: lcd.rightToLeft();
lcd.autoscroll()
Enables automatic scrolling of the display when new characters are printed.
Usage: lcd.autoscroll();
lcd.noAutoscroll()
Disables automatic scrolling, returning to the default behavior.
Usage: lcd.noAutoscroll();
lcd.createChar(num, data[])
Defines a custom character to be stored in one of the 8 available CGRAM slots (0–7). Each
character is defined by 8 bytes.
Usage: lcd.createChar(0, heart);
Arduino Demo: All LiquidCrystal Commands .
// Liquid Crystal Library Demonstration
#include <LiquidCrystal.h>
void setup() {
lcd.begin(16, 2); // Set up the LCD's number of columns and rows
lcd.createChar(0, heart); // Create custom character at location 0
}
void loop() {
demoPrint();
demoCursor();
demoBlink();
demoScrollLeft();
demoScrollRight();
demoTextDirection();
demoAutoscroll();
demoDisplayToggle();
demoCustomChar();
demoFinish();
}
HD44780 liquid crystal displays come in various sizes. Like 16x2 in which there are two
rows of display characters and 16 characters per row. Similarly there are 16x4 or even 20x4
LCD displays. All have the same interface, same commands and same connections.
Since there are three address inputs that can be either HIGH or LOW, there are eight possi-
ble combinations (2^3 = 8) of addresses.
The default address of this board therefore is 0x27. When you short the A0 to A3 jumpers,
you bring that particular bit LOW. You can set the I2C addresses like this:
If your Module has NXP manufactured PCF8574 chip the base address is 0x3F. The three
A0 to A2 bits are located at the lower three bit positions of the address byte.
The display can have following addresses:
void setup() {
lcd.init();
lcd.clear();
lcd.backlight(); // Make sure backlight is on
void loop() {
}
First you include the installed library, and initialize the LCD object.
#include <LiquidCrystal_I2C.h>
These are the various commands that LiquidCrystal_I2C Library exposes. You can use
them, just by calling them in your code.
Assignments:
1. Make a Counter using two buttons to increment or decrement its value and display on I2C LCD
3. Make a Password entry system. Using two buttons get user input of 4 digits, when 4th digit is entered
compare it to previously stored number and turn an LED On.
4. Reaction Time Game: Turn an LED ON, and as the LED turns ON the user is supposed to press the button.
Display the time in milliseconds between LED ON and Button press.
5. Make a two user reaction time game, where the user who presses the button first wins
CHAPTER 11
In general an LED is made up of silicon with PN junction. An organic LED in contrast are
certain organic compounds that emit light on electrical stimula-
tion (just like the light insect). Such organic material are widely
found in nature, like many marine animals have light emitting
properties.
OLED (Organic Light Emitting Diode) displays have become
increasingly popular in hobby electronics due to their vibrant col-
ors, excellent contrast, and low power consumption. Unlike tradi-
tional LCDs that require a backlight, OLED displays emit light
directly from each pixel. This results in deep blacks, sharp imag-
es, and high visibility even in low-light environments. These displays consume very little
power and are therefore useful in battery operated projects. A number of interface control-
lers are available, but commonly an SPI and I2C are used. In this chapter we will explore
the I2C based OLED displays that are easily available from
hobby electronics stores. The I2C OLED displays are com-
monly available as graphic displays, where we can control
individual pixel to turn On or Off. The come in resolution
128x64 pixel resolution. The physical size however can be
0.96” or 1.3” in diameter. The number of pixels are same
however. These displays have only one color for the pixels,
either blue or white. Some have different zones, like upper zone has amber color and lower
zone has white color. These displays are driven by an I2C compliant controller SSD1306
that is integrated on the module. An even smaller version is also available, that has a resolu-
tion of 128x32 pixels having the same driving interface.
Connecting OLED Display
The connection header has 4 pins, two for power and two for I2C bus. You can power it
from 3.3V or 5V. The SDA and SCL lines are connected to the I2C specific pins of Ar-
duino. The MikroDuino Nano board has a header for I2C LCD, you can connect the OLED
display to the same connector, or there is also a separate I2C header, it can be connected
there.
Interfacing I2C OLED Displays with Arduino
Tired of using character LCD displays in your Arduino projects over and over? Well! They
are, in fact, a thing of the past. Enter the fantastic OLED (Organic Light-Emitting Diode)
displays! They’re extremely light, almost paper-thin, theoretically flexible, and produce a
brighter, crisper image. These displays come in two types of interfaces, an I2C and SPI. The
SPI is in general faster than I2C, but requires little more IO lines. The I2C on the other hand
takes only two lines, and these two lines can be shared across other i2C devices.
The SSD1306 controller operates at 1.65V to 3.3V, while the OLED panel itself requires a
7V to 15V supply voltage. All of these various power requirements are fulfilled by inter-
nal charge pump circuitry on the module. This makes it possible to connect the display to an
Arduino or any other 5V logic microcontroller without requiring a logic level converter.
OLED Memory Map
Although knowledge of internal structure of the controller is not necessary for interfacing it,
yet I believe the more you know, better you are equipped. Each pixel on display is con-
trolled by a bit in the memory of controller. The SSD1306 has 1KB of graphics display
memory or GDDRAM. The memory is arranged in 8 pages (0-7) and each page has 128 col-
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
// Clear buffer
display.clearDisplay();
// Print message
display.println("Hello, World!");
// Show on screen
display.display();
}
void loop() {
// Nothing to repeat
}
Library Inclusions
So the first task is to include the appropriate libraries. Not only the Adafruit libraries that
are meant for deriving the display, but also graphics library that they have made to draw
text, fonts, and other graphics. Since the display has to use I2C communication, the Adafruit
libraries depend upon the Arduino supplied ‘wire.h’ library.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
This function returns 0 or 1, depending upon if it could initialize the display or not. It can be
used to give warning about the status of display device.
These functions define how your text appears. setTextSize() sets the font size (1 = normal, 2
= double size, etc.). setTextColor() sets the text color—usually SSD1306_WHITE. The
OLED is monochrome, so there are no colors other than white and black. setCursor(x, y)
sets the position where text will begin (top-left corner is 0,0).
println() works just like in the Serial Monitor—it prints text and moves the cursor to the
next line. The display.display() command is essential—it sends all buffered graphics to the
screen. Without this, nothing will show up.
Larger Fonts
You can always increase the font size, using .setTextSize() function. Size 1 is normal, 2 and
3 are larger. The size and font are defined in the Adafruit GFX library.
Scrolling Text
The SSD1306 controller has built-in commands to scroll the display left, right, or diagonal-
ly. The Adafruit library includes simple functions for that, such as:
startscrollright(0x00, 0x0F)
startscrollleft(0x00, 0x0F)
stopscroll()
This is great for marquee-style displays or when your message is longer than the screen
width.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
display.clearDisplay();
// Scroll demo
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Scrolling right...");
display.display();
display.startscrollright(0x00, 0x0F);
delay(3000);
display.stopscroll();
void loop() {
// Nothing in loop
}
Text Animation
You can create custom animations by manually updating the text position in a loop and us-
ing clearDisplay() and setCursor() to simulate movement. It's simple and flexi-
ble for any text motion, like bouncing text.
Assignments:
1. Make a button counter on OLED display to show the number of times a button is pressed
3. Make a Button counter, one button increases the count and other decreases it. Long press resets the
counter. Limit the count between 0 and 20
4. Make a stopwatch,
StopWatch
Lets write a program to implement a simple stopwatch, it uses millis() function to get the
system clock in milliseconds, and uses two buttons S3 and S4)
//Simple stopwatch
//S3 to start and stop
//S4 to reset
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define BUTTON_START 3
#define BUTTON_RESET 4
void setup() {
pinMode(BUTTON_START, INPUT_PULLUP);
pinMode(BUTTON_RESET, INPUT_PULLUP);
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
updateDisplay();
}
void loop() {
// Handle start/stop button
bool currentStartState = digitalRead(BUTTON_START);
if (currentStartState == LOW && lastStartState == HIGH) {
running = !running;
if (running) {
startTime = millis() - elapsedTime;
} else {
elapsedTime = millis() - startTime;
}
delay(200); // Debounce delay
}
lastStartState = currentStartState;
updateDisplay();
delay(50); // Smooth refresh rate
}
void updateDisplay() {
unsigned long centi = (elapsedTime / 10) % 100;
unsigned long seconds = (elapsedTime / 1000) % 60;
unsigned long minutes = (elapsedTime / 60000);
display.clearDisplay();
display.setCursor(10, 20);
display.print(timeString);
In this program we have use a command called sprintf(), this is equivalent of C command,
printf that formats the print data. Sprint(), formats the print data into a string and then we
print that string on display.
#define BUTTON_DOWN 3
#define BUTTON_SELECT 4
void setup() {
pinMode(BUTTON_DOWN, INPUT_PULLUP);
pinMode(BUTTON_SELECT, INPUT_PULLUP);
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.clearDisplay();
display.display();
showMenu();
}
void loop() {
bool downState = digitalRead(BUTTON_DOWN);
bool selectState = digitalRead(BUTTON_SELECT);
lastDownState = downState;
lastSelectState = selectState;
}
void showMenu() {
display.clearDisplay();
display.setTextSize(1);
for (int i = 0; i < menuLength; i++) {
if (i == currentSelection) {
display.setCursor(0, i * 12);
display.print("> ");
} else {
display.setCursor(0, i * 12);
display.print(" ");
}
display.print(menuItems[i]);
}
display.display();
}
Function Description
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
display.clearDisplay();
// Rectangle
display.drawRect(10, 10, 50, 30, SSD1306_WHITE);
// Circle
display.drawCircle(90, 20, 15, SSD1306_WHITE);
// Line
display.drawLine(0, 0, 127, 63, SSD1306_WHITE);
display.display();
}
void loop() {
// No animation here
}
Bouncing Ball
Here is a program that demonstrates an animation of a ball bouncing when it comes in con-
tact with display edge.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// Display dimensions
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // Reset pin not used
#define OLED_ADDR 0x3C // I2C address
// Ball properties
int ballX = 10;
int ballY = 10;
int ballRadius = 4;
int dx = 2;
int dy = 1;
void setup() {
// Initialize the display
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Don't proceed, loop forever
}
display.clearDisplay();
display.display();
}
void loop() {
// Clear previous frame
display.clearDisplay();
// Update position
ballX += dx;
ballY += dy;
This program creates a small ball that moves around on the OLED screen and bounces off
the edges.
2. Snake Representation
The snake's body is stored in two arrays:
int snakeX[MAX_LENGTH];
int snakeY[MAX_LENGTH];
Each index of these arrays represents a segment of the snake (head, body, tail).
3. Movement
• A direction (dx, dy) is used to move the snake.
• On each loop, the snake moves by shifting its body positions: each segment takes the
position of the one in front of it.
• The head moves forward based on the current direction.
// Button pins
#define UP_BUTTON 2
#define LEFT_BUTTON 3
#define RIGHT_BUTTON 4
#define DOWN_BUTTON 5
// Snake variables
#define BLOCK_SIZE 4
#define MAX_LENGTH 64
int snakeX[MAX_LENGTH];
int snakeY[MAX_LENGTH];
int length = 3;
int dx = BLOCK_SIZE;
int dy = 0;
void spawnFood() {
foodX = (random(SCREEN_WIDTH / BLOCK_SIZE)) * BLOCK_SIZE;
foodY = (random(SCREEN_HEIGHT / BLOCK_SIZE)) * BLOCK_SIZE;
}
void setup() {
pinMode(UP_BUTTON, INPUT_PULLUP);
pinMode(LEFT_BUTTON, INPUT_PULLUP);
pinMode(RIGHT_BUTTON, INPUT_PULLUP);
pinMode(DOWN_BUTTON, INPUT_PULLUP);
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
display.clearDisplay();
display.display();
randomSeed(analogRead(0));
spawnFood();
}
void loop() {
if (gameOver) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(30, 30);
display.print("Game Over!");
display.display();
delay(2000);
return;
}
// Handle input
if (digitalRead(UP_BUTTON) == LOW && dy == 0) { dx = 0; dy = -BLOCK_SIZE; }
if (digitalRead(DOWN_BUTTON) == LOW && dy == 0) { dx = 0; dy = BLOCK_SIZE; }
if (digitalRead(LEFT_BUTTON) == LOW && dx == 0) { dx = -BLOCK_SIZE; dy = 0; }
if (digitalRead(RIGHT_BUTTON) == LOW && dx == 0) { dx = BLOCK_SIZE; dy = 0; }
// Move snake
for (int i = length - 1; i > 0; i--) {
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
snakeX[0] += dx;
snakeY[0] += dy;
// Draw everything
display.clearDisplay();
// Draw snake
for (int i = 0; i < length; i++) {
display.fillRect(snakeX[i], snakeY[i], BLOCK_SIZE, BLOCK_SIZE,
SSD1306_WHITE);
}
// Draw food
display.fillRect(foodX, foodY, BLOCK_SIZE, BLOCK_SIZE, SSD1306_WHITE);
display.display();
delay(150); // Control speed
}
2. Player Movement
Two push buttons (connected to digital pins 3 and 4) are used to move the player block left
and right. Button states are checked using digitalRead() inside the loop() function,
and the player's position is updated accordingly.
3. Obstacle Logic
An obstacle (block) starts from a random x position at the top of the screen and moves
downward at a constant speed. When it reaches the bottom or collides with the player, the
game either resets the obstacle or ends the game.
4. Collision Detection
Collision is checked by comparing the coordinates of the falling block and the player
block. If their rectangular areas overlap, a collision is detected and the game ends.
// Button pins
#define LEFT_BUTTON 3
#define RIGHT_BUTTON 4
// Player variables
#define PLAYER_WIDTH 10
#define PLAYER_HEIGHT 4
int playerX = SCREEN_WIDTH / 2 - PLAYER_WIDTH / 2;
const int playerY = SCREEN_HEIGHT - PLAYER_HEIGHT;
// Obstacle variables
int obstacleX;
int obstacleY = 0;
const int obstacleSize = 8;
int obstacleSpeed = 2;
void spawnObstacle() {
obstacleX = random(0, SCREEN_WIDTH - obstacleSize);
obstacleY = 0;
}
void setup() {
Clears and redraws the display
pinMode(LEFT_BUTTON, INPUT_PULLUP);
pinMode(RIGHT_BUTTON, INPUT_PULLUP);
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
display.clearDisplay();
display.display();
randomSeed(analogRead(0));
spawnObstacle();
}
void loop() {
if (gameOver) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(30, 30);
display.print("Game Over!");
display.display();
delay(3000);
// Restart game
playerX = SCREEN_WIDTH / 2 - PLAYER_WIDTH / 2;
gameOver = false;
spawnObstacle();
}
// Handle input
if (digitalRead(LEFT_BUTTON) == LOW && playerX > 0) {
playerX -= 3;
}
if (digitalRead(RIGHT_BUTTON) == LOW && playerX < SCREEN_WIDTH - PLAYER_WIDTH)
{
playerX += 3;
}
// Move obstacle
obstacleY += obstacleSpeed;
// Check collision
if (obstacleY + obstacleSize >= playerY &&
obstacleX + obstacleSize > playerX &&
obstacleX < playerX + PLAYER_WIDTH) {
gameOver = true;
}
// Draw everything
display.clearDisplay();
// Draw player
display.fillRect(playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT,
SSD1306_WHITE);
// Draw obstacle
display.fillRect(obstacleX, obstacleY, obstacleSize, obstacleSize,
SSD1306_WHITE);
display.display();
delay(50); // controls game speed
}
This is repeated at regular intervals (using delay(50)) to control the game speed.
2. Button Configuration
We use 4 buttons connected to digital pins:
• D2 for Up
• D3 for Left
• D4 for Right
• D5 for Down
Players move the cursor with these buttons and perform a "long press" on any button to
place their mark in the current cell.
char board[3][3];
int cursorX = 0;
int cursorY = 0;
5. Placing a Move
If a long press is detected and the cell is empty, the current player’s symbol (X or O) is
placed using markCell().
6. Switching Players
After a valid move, the code switches between players:
8. Detecting a Draw
If all 9 cells are filled without a winner, the isDraw() function returns true and displays a
draw message.
9. Resetting the Game
After a win or draw, the screen shows the result for 3 seconds, then the game is automatical-
ly reset.
// Button pins
#define UP_BUTTON 2
#define LEFT_BUTTON 3
#define RIGHT_BUTTON 4
#define DOWN_BUTTON 5
// Game state
char board[3][3];
int cursorX = 0;
int cursorY = 0;
char currentPlayer = 'X';
bool gameOver = false;
// Button timing
unsigned long lastPressTime = 0;
void drawGrid() {
display.clearDisplay();
// Draw Xs and Os
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
int cellX = x * 42 + 10;
int cellY = y * 21 + 5;
if (board[y][x] == 'X') {
display.drawLine(cellX, cellY, cellX + 20, cellY + 15, SSD1306_WHITE);
display.drawLine(cellX + 20, cellY, cellX, cellY + 15, SSD1306_WHITE);
} else if (board[y][x] == 'O') {
display.drawCircle(cellX + 10, cellY + 8, 8, SSD1306_WHITE);
}
}
}
// Draw cursor
int cursorPixelX = cursorX * 42;
int cursorPixelY = cursorY * 21;
display.drawRect(cursorPixelX, cursorPixelY, 42, 21, SSD1306_WHITE);
display.display();
}
char checkWinner() {
for (int i = 0; i < 3; i++) {
// Check rows and columns
if (board[i][0] != ' ' &&
board[i][0] == board[i][1] &&
board[i][1] == board[i][2])
return board[i][0];
// Diagonals
if (board[0][0] != ' ' &&
board[0][0] == board[1][1] &&
board[1][1] == board[2][2])
return board[0][0];
bool isDraw() {
for (int y = 0; y < 3; y++)
for (int x = 0; x < 3; x++)
if (board[y][x] == ' ') return false;
return true;
}
void resetGame() {
for (int y = 0; y < 3; y++)
for (int x = 0; x < 3; x++)
board[y][x] = ' ';
cursorX = 0;
cursorY = 0;
currentPlayer = 'X';
gameOver = false;
drawGrid();
}
void setup() {
pinMode(UP_BUTTON, INPUT_PULLUP);
pinMode(LEFT_BUTTON, INPUT_PULLUP);
pinMode(RIGHT_BUTTON, INPUT_PULLUP);
pinMode(DOWN_BUTTON, INPUT_PULLUP);
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
display.clearDisplay();
display.display();
resetGame();
}
void loop() {
if (gameOver) {
delay(1000);
resetGame();
return;
}
if (isButtonPressed(LEFT_BUTTON)) {
if (cursorX > 0) cursorX--;
delay(200);
}
if (isButtonPressed(RIGHT_BUTTON)) {
if (cursorX < 2) cursorX++;
delay(200);
}
if (isButtonPressed(UP_BUTTON)) {
if (cursorY > 0) cursorY--;
delay(200);
}
if (isButtonPressed(DOWN_BUTTON)) {
if (cursorY < 2) cursorY++;
delay(200);
}
drawGrid();
}
CHAPTER 12
Real-time clocks (RTCs) are essential components in many embedded systems and micro-
controller-based projects that require accurate timekeeping. Unlike the internal timers of
microcontrollers, which lose track of time when the device is powered off, RTC modules
maintain precise time and date information using a dedicated oscillator and a small backup
battery. This makes them ideal for applications such as data logging, alarms, event schedul-
ing, access control systems, and time-stamped displays.
In the hobbyist market, several RTC modules are available, each offering different features
and communication protocols. Popular RTC chips include the DS1307, DS3231, and
PCF85263, among others. The DS1307 is one of the most commonly used RTC modules
due to its affordability, I2C communication interface, and com-
patibility with a wide range of development platforms including
the Arduino. The DS1307 maintains time in seconds, minutes,
hours, day, date, month, and year format and can operate in either
12-hour or 24-hour modes. It features an onboard 32.768 kHz
crystal oscillator and a backup battery socket, allowing the mod-
ule to keep track of time even when the main power is removed.
In this chapter, we will explore how to interface the DS1307 with
the Arduino Nano, display the current time and date on an I2C
OLED screen, and use push buttons connected to digital pins D2,
D3, D4, and D5 to interact with the system. As usual we will use our MikroDuino Nano as a
platform to connect DS1307 module, as well as OLED display for these projects. You can
use your own setup for this.
The DS1307 Module also contains, an I2C EEPROM chip, which is separate from DS1307
and can be used separately for EEPROM functionality. Additionally this module allows you
to solder a digital temperature sensor DS18B20 that can also be individually accessed
through its header pins.
The DS1307 uses an external 32kHz crystal to keep track of time. This
crystal is what helps the chip measure each passing second. However,
there’s a small issue: the crystal’s frequency can change slightly when
the temperature around it changes.
Even though the change is very tiny, over time, it can cause the clock
to slowly become less accurate. In fact, the time can drift by about
five minutes each month.
This might sound like a big problem, but for most basic projects, it’s
not a big deal. The DS1307 is still a reliable and popular real-time
clock for many everyday uses where super precise timing isn’t critical.
The DS1307 chip includes a battery backup feature that helps keep the clock running even if
the main power gets disconnected. On the back of the module, there’s a battery holder de-
signed to fit a 20mm 3V lithium coin cell battery. Inside the chip, there’s a built-in smart
power-sensing circuit that constantly checks whether the main power is on. If this circuit
detects that the main power has been lost, it automatically switches to the backup battery.
In addition to timekeeping, the DS1307 includes 56 bytes of battery-backed SRAM, which
can be used to store small amounts of user data that must be preserved across power cycles.
Thanks to its low power consumption, easy interfacing through I2C, and wide availability as
ready-to-use modules, the DS1307 remains a favorite choice for hobbyists and educators
when building clocks, alarms, timers, and time-stamped data logging systems.
Connecting to Arduino
DS1307 uses I2C interface, and can share the same I2C lines (SDA and SCL) with other
I2C devices. The DS1307 has an I2C address of 0x68.
The module has two sets of pins, a 5 pin header, that
has power SDA and SCL pins, and pin for DS. The
DS pin has nothing to do with DS1307, it’s the output
of DS18B20 temperature sensor. This digital tempera-
ture sensor, has footprints on the PCB, but not sol-
dered. It will be a good exercise to get a DS18B20 and
solder it on this module, this way we have another de-
vice to play and learn with.
The second set of connections is 7 pin header, that has an additional output of SQ and BAT.
The SQ is an optional output of DS1307 to give out square wave outputs at regular inter-
vals, that can be used in projects for various timing related events. BAT is the output of bat-
tery and can be used in applications to monitor the battery voltage etc.
MikroDuino Nano board has a dedicated header with 5 pins to plug in this
module. Since the module connects directly to SDA and SCL lines of I2C
there is no need to set jumper tabs, or to reposition these connections to
any other pins. The DS pin (DS18B20) however can be connected to any
digital line, and therefore has a tab jumper option. We will consider this
in appropriate chapter.
So to experiment with this module, you need to connect this module to
the dedicated header and optionally (rather better) to have a display, like I2C OLED or I2C
LCD to display the DS1307 output.
void setup() {
Serial.begin(9600); // Start serial communication for debugging
if (!rtc.begin()) { // Initialize the RTC
Serial.println("Couldn't find RTC");
while (1); // Stop the program if RTC is not found
}
data from the chip. The program initializes the chip and stats the timing function. Display
void loop() {
DateTime now = rtc.now(); // Read current time and date from RTC
Serial.print(now.year());
Serial.print('/');
Serial.print(now.month());
Serial.print('/');
Serial.print(now.day());
Serial.print(" ");
Serial.print(now.hour());
Serial.print(':');
Serial.print(now.minute());
Serial.print(':');
Serial.print(now.second());
Serial.println();
#include <Wire.h>
This library is needed for I2C communication between Arduino and the DS1307.
#include <RTClib.h>
This is the main library that provides easy functions to interact with the DS1307.
RTC_DS1307 rtc;
This line creates an object rtc that represents our DS1307 device.
rtc.begin();
Initializes communication with the RTC module. It must be called in the setup()
function. If it fails (for example, if the module is not connected), the program stops.
rtc.isrunning();
Checks if the RTC is actively keeping time. If the backup battery is missing or the
clock is not yet set, it will report that the RTC is not running.
rtc.adjust(DateTime(F(__DATE__), F(__TIME__))) (Optional)
This function sets the RTC time and date to the moment the sketch was compiled.
It’s used if you need to set the clock initially.
rtc.now();
Reads the current time and date from the RTC and returns it as a DateTime object.
DateTime object
This object (now in the example) provides methods like .year(), .month(),
.day(), .hour(), .minute(), and .second() to access the individual com-
ponents of time.
The above code sets the date and time of the RTC as system generated date and time when
the program is compiled. The (__DATE__) and (__TIME__) are the compiler generated
global constants that contain the date and time of you PC when the program was compiled.
You can use the global constants not only to set the date and time of the RTC, but also in
many other places where compiled date and time are needed. This can include version con-
trol, or even conditional compiling. The letter F before (__DATE__) and time macros indi-
cate that this information is stored in the program memory and RAM is not used for these.
This is done because we have more program memory available than the RAM that is used
for runtime variables.
if (!rtc.isrunning()) {
Serial.println("RTC is NOT running!");
// Manually set the date and time: (Year, Month, Day, Hour, Minute, Second)
rtc.adjust(DateTime(2025, 4, 26, 15, 30, 0));
// This sets the RTC to 26th April 2025, 3:30:00 PM
}
In case you want to set the date and time of RTC to some custom values, you do so using
this code.
void setup() {
Wire.begin();
rtc.begin();
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
if (!rtc.isrunning()) {
display.clearDisplay();
display.setCursor(0, 0);
display.println("RTC not running!");
display.display();
Notice how we have included both libraries and their dependencies in the code, also defined
the constants for each library as we have used them before.
void loop() {
DateTime now = rtc.now(); // Read current time
display.clearDisplay();
display.setCursor(0, 10);
display.setTextSize(2); // Make text bigger for time display
display.print(now.hour());
display.print(':');
if (now.minute() < 10) display.print('0'); // Leading zero for minute
display.print(now.minute());
display.print(':');
if (now.second() < 10) display.print('0'); // Leading zero for second
display.print(now.second());
void setup() {
Wire.begin();
rtc.begin();
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
while (1);
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
pinMode(BTN_SELECT, INPUT_PULLUP);
pinMode(BTN_INC, INPUT_PULLUP);
pinMode(BTN_DEC, INPUT_PULLUP);
pinMode(BTN_SAVE, INPUT_PULLUP);
if (!rtc.isrunning()) {
rtc.adjust(DateTime(__DATE__, __TIME__)); // Auto set time if RTC stopped
}
}
void loop() {
if (digitalRead(BTN_SELECT) == LOW) {
delay(200); // Debounce
if (!settingMode) {
DateTime now = rtc.now();
setHour = now.hour();
setMinute = now.minute();
setDay = now.day();
setMonth = now.month();
setYear = now.year();
settingMode = true;
selectedField = 0;
} else {
selectedField = (selectedField + 1) % 5; // Move to next field
}
}
if (settingMode) {
adjustDateTime();
showSetTimeScreen();
} else {
showNormalClock();
}
void adjustDateTime() {
if (digitalRead(BTN_INC) == LOW) {
delay(200);
switch (selectedField) {
case 0: setHour = (setHour + 1) % 24; break;
case 1: setMinute = (setMinute + 1) % 60; break;
case 2: setDay = (setDay % 31) + 1; break;
case 3: setMonth = (setMonth % 12) + 1; break;
case 4: setYear++; break;
}
}
if (digitalRead(BTN_DEC) == LOW) {
delay(200);
switch (selectedField) {
case 0: setHour = (setHour + 23) % 24; break;
case 1: setMinute = (setMinute + 59) % 60; break;
case 2: setDay = (setDay == 1) ? 31 : (setDay - 1); break;
case 3: setMonth = (setMonth == 1) ? 12 : (setMonth - 1); break;
case 4: setYear--; break;
}
}
}
void showNormalClock() {
DateTime now = rtc.now();
display.clearDisplay();
display.setTextSize(3);
display.setCursor(10, 10);
if (showColon) display.print(':');
else display.print(' ');
display.setTextSize(1);
display.setCursor(20, 50);
if (now.day() < 10) display.print('0');
display.print(now.day());
display.print('/');
if (now.month() < 10) display.print('0');
display.print(now.month());
display.print('/');
display.print(now.year());
display.display();
showColon = !showColon;
delay(1000);
}
void showSetTimeScreen() {
display.clearDisplay();
display.setTextSize(2);
display.setCursor(0, 0);
display.print((selectedField == 0) ? ">" : " ");
if (setHour < 10) display.print('0');
display.print(setHour);
display.print(":");
display.setTextSize(1);
display.setCursor(0, 30);
display.print((selectedField == 2) ? ">" : " ");
if (setDay < 10) display.print('0');
display.print(setDay);
display.print("/");
display.print((selectedField == 3) ? ">" : " ");
if (setMonth < 10) display.print('0');
display.print(setMonth);
display.print("/");
display.setCursor(0, 55);
display.setTextSize(1);
display.println("Press Save to confirm");
display.display();
}
To set the time press button S1, the clock will enter the set mode, and an arrow will appear
indicating the currently selected field. Use S2 and S3 to increment or decrement value, press
S1 to move to next field. Press S4 to save and exit setup mode.
CHAPTER 13
What is SPI
SPI stands for Serial Peripheral Interface. It’s a method for two or more electronic devic-
es to quickly and reliably exchange data using a few dedicated wires.
Imagine you’re at a fast-food counter:
• You (the customer) place an order.
• The cashier (the server) listens to you and passes the message to the kitchen.
• The kitchen prepares your meal and hands it back to the cashier, who gives it to you.
In SPI communication:
• The Arduino acts like the Master (the customer).
• The connected device (like a sensor or display) acts like the Slave (the cashier and
kitchen).
• The Master always starts the communication and controls when and how data is ex-
changed.
Even with the arrival of more modern LED drivers and matrix controllers, the MAX7219
continues to be widely used because of its simplicity, reliability, and the huge number of
libraries and ready-made modules available for it. It remains an excellent starting point for
beginners who are learning about serial communication and LED matrix programming.
In short, the MAX7219 is not just a chip — it is a bridge between simple electronics and
dazzling digital displays, helping generations of engineers, students, and hobbyists bring
their ideas to life.
Now that we understand the basics, lets start diving in, and start exporing the MAX7219
LED matrix display using Arduino and SPI protocol.
The MAX7219 LED Matrix Module
1. LedControl Library
• Simple and lightweight
• Good for small projects (1–4 modules)
• Uses software SPI, not hardware optimized
• Limited features (no font/text/scrolling support)
In this book we will use MD_MAX72XX library along with Parola library to get animations
and effects. This has been chosen because of advanced functionalities it offers and because
the student will be able to make professional level devices using this library.
Installation of the libraries is simple, just search for MD_MAX72XX in the library manager
and select the library by “Majic Designs” also search for and install MD_Parola library by
the same group “Majic Designs”.
Parola-Compatible Modules
• Designed to be fully compatible with the Parola display library from Majic Designs.
• May follow a different internal layout but offer better alignment with the MD_MAX72XX and
MD_Parola libraries.
Check the Orientation: Upload a basic example using a known configuration (like FC-16).
If characters appear flipped, mirrored, or scrambled, the layout in code needs to be adjust-
ed.
Use the MD_MAX72XX Hardware Test Sketch: The MD_MAX72XX library includes an exam-
ple sketch named MD_MAX72XX_HW_Mapper. This sketch helps you test various hardware types
visually to find the correct setting.
Try different #define HARDWARE_TYPE options like FC16_HW, PAROLA_HW, or GENERIC_HW.
Observe IC Position: If the MAX7219 chips are located behind each matrix or in line on
the side, that gives clues about the design and cascading direction.
#include <MD_MAX72xx.h>
#include <SPI.h>
void setup() {
mx.begin();
mx.clear();
}
void loop() {
mx.setPoint(3, 4, true); // row 3, col 4
delay(1000);
mx.setPoint(3, 4, false);
delay(1000);
}
Here is a summary of the commands used to control the MAX7219 in the above code:
Explanation of Commands
#include <MD_MAX72xx.h>
This includes the main library that allows communication and control of MAX7219-based
LED matrix displays. It abstracts the SPI interface and provides easy-to-use functions for
graphics, text, and animation.
#include <SPI.h>
This includes the Arduino SPI library. Even though MD_MAX72xx internally uses SPI, this
include ensures proper linkage and support for SPI communication.#define HARD-
WARE_TYPE MD_MAX72XX::FC16_HW
This defines the type of LED matrix hardware being used. FC16_HW is the most common con-
figuration for MAX7219 8x8 modules. Other types include GENERIC_HW and PAROLA_HW. Choos-
ing the correct hardware type is essential for correctly orienting the characters and ensuring
proper addressing of the LEDs.
#define MAX_DEVICES 1
Defines how many 8x8 LED matrix modules are connected in series (daisy-chained). Here,
only one module is used.
#define CS_PIN 10
Defines the Chip Select pin (also called LOAD or SS) used to select the MAX7219 device
on the SPI bus. Digital pin 10 is often the default SPI CS pin on most Arduino boards like
the Uno.
MD_MAX72XX matrix = MD_MAX72XX(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
This line creates an instance of the MD_MAX72XX class called matrix. It is initialized with:
• The hardware type (FC16_HW)
• The CS pin (10)
• The number of devices (1)
This object handles all interaction with the matrix display.
matrix.begin();
Initializes the display. This sets up SPI communication and prepares the MAX7219 to re-
ceive commands and data.
matrix.clear();
Clears the entire display, turning off all LEDs. This ensures the screen starts in a blank state
before anything is shown.
matrix.setIntensity(5);
Sets the brightness level of the LEDs. The value ranges from 0 (dim) to 15 (bright). A mid-
level brightness like 5 is comfortable for most indoor uses.
void setup() {
mx.begin();
mx.clear();
}
void loop() {
mx.clear();
for (uint8_t i = 0; i < 8; i++) {
mx.setPoint(i, i, true); // draw diagonal
delay(100);
}
delay(1000);
}
The above code simply plots a line diagonally on the display. It increments the row and col-
umn, sequentially to light up the LEDs.
void setup() {
mx.begin();
mx.clear();
mx.control(MD_MAX72XX::INTENSITY, 5);
}
uint8_t smiley[8] = {
B00111100,
B01000010,
B10100101,
B10000001,
B10100101,
B10011001,
B01000010,
B00111100
};
void loop() {
mx.clear();
for (uint8_t row = 0; row < 8; row++) {
mx.setRow(0, row, smiley[row]);
}
delay(2000);
}
mx.control(MD_MAX72XX::INTENSITY, 5);
Use this statement to control the intensity of display. The value 5 is the intensity level and it
can be from 0 to 15.
void setup() {
mx.begin();
mx.clear();
mx.control(MD_MAX72XX::INTENSITY, 5);
}
uint8_t frames[2][8] = {
{
B00011000,
B00111100,
B01111110,
B11111111,
B11111111,
B01111110,
B00111100,
B00011000
},
{
B00000000,
B00011000,
B00111100,
B01111110,
B00111100,
B00011000,
B00000000,
B00000000
}
};
void loop() {
for (uint8_t f = 0; f < 2; f++) {
for (uint8_t row = 0; row < 8; row++) {
mx.setRow(0, row, frames[f][row]);
}
delay(500);
}
}
Displaying Text with MD_MAX72XX Library
The MD_MAX72XX library provides powerful functions to render characters and text on 8x8 LED
matrix modules driven by the MAX7219 chip. It supports ASCII characters from a built-in
5x7 font and offers precise control over character placement, alignment, and animation.
void setup() {
matrix.begin();
matrix.control(MD_MAX72XX::INTENSITY, 5);
matrix.clear();
matrix.setChar(6 , 'A');
}
void loop() {
// Do nothing, static display
}
matrix.setChar(6 , 'A');
This function displays a character on the display. The number 6 is the column number from
where to start drawing the character.
Scrolling Text
The space on display is always limited, while the length of message display may be long.
Even when you have multiple modules connected, you may find the display area has a lim-
ited window. In such situations we can scroll the text on display. Scrolling can be in any
direction, but for English it is usually from Right to Left. If using a display for an elevator,
it is more useful to animate the floor number appearing as vertical scroll.
In this code we have made use of MD_Parola library instead of the standard
MD_MAX72XX library. The MD_Parola library depends upon the MD_MAX72XX library
to provide the basic commands to control and display text on the display. The MD_Parola
extends this library to give features like animations, display strings of text and scrolling text
etc.
display.displayText("WELCOME", PA_CENTER, 100, 0, PA_SCROLL_LEFT,
PA_SCROLL_LEFT);
Here display object is of type MD_Parola and its displayText function does all the magic.
//Scrolling text
// Use cascaded 4 modules
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
void setup() {
display.begin();
display.setIntensity(5);
display.displayClear();
display.displayText("WELCOME", PA_CENTER, 100, 0, PA_SCROLL_LEFT,
PA_SCROLL_LEFT);
display.displayAnimate();
}
void loop() {
if (display.displayAnimate()) {
display.displayReset();
}
}
In order to display and animate the text, we need two part statements. First we prepare the
text that is to be displayed, and then we issue the animate function to actually affect the dis-
play.
display.displayText("WELCOME", PA_CENTER, 100, 0, PA_SCROLL_LEFT,
PA_SCROLL_LEFT);
This line sets up the text to be scrolled on the matrix.
"WELCOME": The actual message to be displayed.
PA_CENTER: The alignment of the text (could also be PA_LEFT or PA_RIGHT).
100: Scroll speed in milliseconds between animation frames. Lower means faster.
0: Pause time (in milliseconds) after one scroll cycle completes.
PA_SCROLL_LEFT: Entry animation — how the text appears.
PA_SCROLL_LEFT: Exit animation — how the text leaves.
This setup prepares the message, speed, alignment, and effect, but doesn't actually scroll it
yet — that's the job of the next function.
display.displayAnimate();
This function runs the animation defined in displayText(...). It returns true when the ani-
mation has completed.You must call this function repeatedly in loop() to advance the ani-
mation frame by frame.
if (display.displayAnimate()) { display.displayReset(); }
This construct checks whether the current scroll cycle has completed. If it has:
display.displayReset(); starts the same animation again from the beginning.
This loop gives the effect of continuous scrolling — it keeps resetting after each pass.
Elevator Counter
You must have encountered counters in elevators, that scroll up or down when the elevator
reaches a floor. Here is an animation for this.
//Elevator Counter
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
int currentFloor = 0;
int direction = 1; // 1 = up, -1 = down
char floorText[4]; // Enough to hold "5" or even "G", "10" etc.
void setup() {
display.begin();
display.setIntensity(5);
display.displayClear();
void loop() {
if (display.displayAnimate()) {
currentFloor += direction;
if (currentFloor > 5) {
currentFloor = 5;
direction = -1;
} else if (currentFloor < 0) {
currentFloor = 0;
direction = 1;
}
// Select direction of scroll
textEffect_t scrollEffect = (direction == 1) ? PA_SCROLL_UP :
PA_SCROLL_DOWN;
The heart of this code is again displaytext() function in the MD_Parola library. The text to
display, is obtained by converting the floor number into a text by using sprintf() the %d is
the formatting for conversion.
Assignments:
2. Modify the elevator code to respond to two buttons, one for up and one for down (range 0 to 5)
3. Using LM35 temperature sensor display room temperature on display, update every 5 seconds
4. Using LDR sense the ambient light, and adjust the display brightness
Digital Dice
Lets make a program to randomly select a number between 1 to 6 and display the number as
dots (real dice like) on the display.
// Digital dice
// Use D2 Button to scroll
#include <MD_MAX72xx.h>
#include <SPI.h>
int currentDice = 1;
bool lastButtonState = HIGH;
byte dicePatterns[6][8] = {
// Dice 1
{
B00000000,
B00000000,
B00000000,
B00010000,
B00010000,
B00000000,
B00000000,
B00000000
},
// Dice 2
{
B00000000,
B00000000,
B00000010,
B00000000,
B00000000,
B01000000,
B00000000,
B00000000
},
// Dice 3
{
B00000000,
B00000010,
B00000000,
B00010000,
B00000000,
B01000000,
B00000000,
B00000000
},
// Dice 4
{
B00000000,
B01000010,
B00000000,
B00000000,
B00000000,
B01000010,
B00000000,
B00000000
},
// Dice 5
{
B00000000,
B01000010,
B00000000,
B00010000,
B00000000,
B01000010,
B00000000,
B00000000
},
// Dice 6
{
B00000000,
B01000010,
B00000000,
B01000010,
B00000000,
B01000010,
B00000000,
B00000000
}
};
void setup() {
mx.begin();
pinMode(BUTTON_PIN, INPUT_PULLUP); // use internal pull-up
displayDice(currentDice);
}
void loop() {
bool buttonState = digitalRead(BUTTON_PIN);
lastButtonState = buttonState;
}
void rollDice() {
// Simulate roll animation
for (int i = 0; i < 10; i++) {
int r = random(1, 7);
displayDice(r);
delay(100);
}
void setup() {
Serial.begin(9600);
display.begin();
display.setIntensity(5); // Brightness: 0–15
display.displayClear();
display.displayScroll("Send Text via Serial", PA_LEFT, PA_SCROLL_LEFT, 50);
display.displayAnimate();
}
void loop() {
if (Serial.available()) {
inputText = Serial.readStringUntil('\n');
inputText.trim(); // Remove trailing newline or spaces
if (inputText.length() > 0) {
display.displayClear();
display.displayScroll(inputText.c_str(), PA_LEFT, PA_SCROLL_LEFT, 50);
}
}
if (display.displayAnimate()) {
// Automatically restart animation
display.displayReset();
}
}
Required Library
MD_MAX72XX by MajicDesigns (No need for MD_Parola if you're only working with graphics
only)
PONG Game
//Pong game
// use buttons D3 and D4
#include <MD_MAX72xx.h>
#include <SPI.h>
// Button pins
const int buttonLeft = 4;
const int buttonRight = 3;
// Paddle position
int paddleX = 3;
void setup() {
mx.begin();
pinMode(buttonLeft, INPUT_PULLUP);
pinMode(buttonRight, INPUT_PULLUP);
mx.clear();
mx.update();
}
void loop() {
mx.clear();
// Read buttons
if (digitalRead(buttonLeft) == LOW && paddleX > 0) {
paddleX--;
}
if (digitalRead(buttonRight) == LOW && paddleX < 6) {
paddleX++;
}
// Move ball
ballX += ballDirX;
ballY += ballDirY;
// Draw ball
mx.setPoint(ballY, ballX, true);
mx.update();
delay(150); // adjust for speed
}
CHAPTER 14
As you advance in the world of embedded systems and Arduino development, you'll inevi-
tably encounter the need to understand your project's movement, orientation, or balance.
Whether you're building a robotic arm, a self-balancing vehicle, or a wearable fitness track-
er, motion sensing becomes a crucial part of the design. In this chapter, we will explore how
to interface and use the MPU6050, a popular 6-axis motion tracking device, with an Ar-
duino.
We’ll utilize I2C communication, which allows efficient data exchange using only two
pins, making it ideal for projects where pin availability is limited. In addition to reading sen-
sor data, we'll also explore ways to visualize the output using peripherals like the
MAX7219-based LED matrix display, the SSD1306 OLED display, and user buttons
for interaction and mode selection.
Key Features:
Gyroscope Range: ±250, ±500, ±1000, ±2000 °/s
Accelerometer Range: ±2g, ±4g, ±8g, ±16g
16-bit ADCs for each channel
Built-in DMP for computing motion processing algorithms
Auxiliary I2C Bus to connect additional sensors like magnetometers
Power Supply: 3.3V to 5V (depending on the breakout module)
Pin Description
GND Ground
Adafruit_MPU6050 mpu;
void setup() {
Serial.begin(115200);
while (!Serial)
delay(10); // Wait for Serial Monitor
// Initialize MPU6050
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip");
while (1) {
delay(10);
}
}
Serial.println("MPU6050 connected!");
void loop() {
sensors_event_t accel, gyro, temp;
// Print acceleration
Serial.print("Accel (m/s^2): ");
Serial.print(accel.acceleration.x); Serial.print(", ");
Serial.print(accel.acceleration.y); Serial.print(", ");
Serial.print(accel.acceleration.z);
// Print gyroscope
Serial.print(" | Gyro (rad/s): ");
Serial.print(gyro.gyro.x); Serial.print(", ");
Serial.print(gyro.gyro.y); Serial.print(", ");
Serial.print(gyro.gyro.z);
// Print temperature
Serial.print(" | Temp (°C): ");
Serial.println(temp.temperature);
Units Matter: Unlike older libraries, this one provides data in meaningful units (no scaling
needed).
Filter Bandwidth: Lower bandwidth filters out high-frequency noise but may respond
slower to quick changes.
Acceleration in m/s²: Earth's gravity (~9.8 m/s²) will appear when stationary depending on
orientation.
Temperature sensor is internal and may not represent ambient room temperature accurate-
ly.
mpu.begin()
Initializes the MPU6050 over I2C. Returns false if the
sensor is not found.
mpu.setAccelerometerRange(...)
Optional: Sets the sensitivity range of the accelerome-
ter.
getEvent(...)
Reads the latest sensor values into the accel, gyro,
and temp structures.
Serial.print(...)
Displays sensor readings on the Serial Monitor for
testing and calibration.
now move on to displaying MPU6050 sensor data on a 128x64 I2C OLED display
(SSD1306). This is a powerful way for you to see real-time motion data visually on a hard-
ware screen without needing a serial connection. Both of these devices use i2C communica-
tion, so the same bus lines will be used for both. The MikroDuino board however has sepa-
rate headers for their connections.
We will use the Adafruit SSD1306 library along with the Adafruit MPU6050 library.
void setup() {
Serial.begin(115200);
// Initialize OLED
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
while (true);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// Initialize MPU6050
if (!mpu.begin()) {
display.clearDisplay();
display.setCursor(0, 0);
display.println("MPU6050 not found");
display.display();
while (true);
}
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
display.clearDisplay();
display.setCursor(0, 0);
display.println("MPU6050 Ready!");
display.display();
delay(1000);
}
void loop() {
sensors_event_t accel, gyro, temp;
mpu.getEvent(&accel, &gyro, &temp);
// Display acceleration
display.println("Accel (m/s^2):");
display.print("X: "); display.println(accel.acceleration.x, 1);
display.print("Y: "); display.println(accel.acceleration.y, 1);
display.print("Z: "); display.println(accel.acceleration.z, 1);
// Display temperature
display.print("Temp: "); display.print(temp.temperature, 1);
display.println(" C");
The above example simply shows the MPU6050 data as text on the display. Now lets make
it little bit graphical.
void setup() {
Serial.begin(115200);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
while (true);
}
if (!mpu.begin()) {
display.clearDisplay();
display.setCursor(0, 0);
display.println("MPU6050 not found");
display.display();
while (true);
}
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
display.clearDisplay();
display.display();
delay(1000);
}
void loop() {
sensors_event_t a, g, t;
mpu.getEvent(&a, &g, &t);
display.clearDisplay();
// Draw X bar
drawAxisBar(0, "X", a.acceleration.x);
// Draw Y bar
drawAxisBar(22, "Y", a.acceleration.y);
// Draw Z bar
drawAxisBar(44, "Z", a.acceleration.z);
display.display();
delay(200);
}
display.setCursor(0, y);
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.print(label);
display.print(":");
In the world of embedded systems and DIY electronics, the ability to measure environmen-
tal conditions like temperature and humidity is essential for a wide range of applications—
from smart homes and greenhouses to weather monitoring and personal health devices.
Among the most popular sensors used by students and hobbyists for this purpose are the
DHT11 and DHT22 temperature and humidity sensors.
These sensors are inexpensive, readily available, and easy to use with Arduino boards such
as the Arduino Nano. Despite their simplicity, they offer a great opportunity to understand
digital sensors, sensor data communication protocols, and real-time environmental monitor-
ing.
In this chapter, we will explore:
• The theoretical background and working principle of DHT11 and DHT22 sen-
sors.
• The key differences between DHT11 and DHT22, and when to choose one over
the other.
• How to interface these sensors with an Arduino Nano.
• How to display the sensor readings on a 0.96" I2C OLED display, making your
project more interactive and user-friendly.
• Step-by-step coding examples and hands-on projects to solidify your understand-
ing.
By the end of this chapter, you will be able to build your own Arduino-based weather sta-
tion and gain a deeper appreciation of how digital sensors integrate with microcontrollers to
create intelligent systems.
Theoretical Background
The DHT11 and DHT22 are digital sensors used for measuring temperature and relative
humidity. These sensors are based on capacitive humidity sensing and a thermistor for tem-
perature measurement, combined with a basic digital signal processing unit that outputs data
in a digital format, making them easy to interface with microcontrollers like the Arduino
Nano.
Working Principle
Humidity Sensing: Both sensors use a moisture-holding substrate (a polymer capacitor)
whose capacitance changes with the relative humidity of the surrounding air. This change is
measured and converted into a humidity reading.
Temperature Sensing: They include a NTC thermistor, whose resistance varies with tem-
perature. An internal ADC (Analog-to-Digital Converter) digitizes this signal.
The sensor packages these readings and sends them to the microcontroller via a single-wire
digital communication protocol.
Two modules are available in market,
DHT11 and DHT22. Both of these are simi-
lar in interfacing but differ in accuracy and
range of measurements. Generally speaking
for ordinary usage DHT11 is sufficient, so
we will be using this in our chapter.
Data Communication
Both sensors use a proprietary single-wire
protocol. Communication involves a specif-
ic timing sequence where the microcontroller requests data, and the sensor responds with 40
bits: 16 bits for humidity, 16 bits for temperature, and 8 bits for checksum. Since the com-
munication is timing-sensitive, it's recommended to use existing libraries that handle the
low-level protocol. You can use any digital GPIO line to connect this module to Arduino.
MikroDuino board has a dedicated header for this, and has a tabbed jumper to connect the
output pin to A1. When a tab is connected to this jumper the output pin get connected to pin
A1 of Arduino nano. However you want to connect to some
other pin, for your project, just remove the tab and connect
the pin labelled DHT to any other GPIO pin of your choice
using a jumper wire.
Additionally we will use the I2C SSD1306 OLED display to
display the readings from this sensor. We have already in-
stalled the library for SSD1306 display, install the Adafruit
DHT11 Library. It supports both DHT11 and DHT22. This
library is widely used, well documented and regularly updat-
ed. It handles the sensor’s timing specific single wire protocol internally and therefore user
is free from bothering about low level signals.
Below is a progressive series of example codes demonstrating how to use the DHT11/
DHT22 sensor and an I2C OLED display with an Arduino Nano. These examples not on-
ly teach basic functionality but also introduce important programming concepts and tech-
niques.
void setup() {
Serial.begin(9600);
dht.begin();
}
void loop() {
float humidity = dht.readHumidity();
float temperature =
dht.readTemperature();
Serial.print("Temp: ");
Serial.print(temperature);
Serial.print(" °C | Humidity: ");
Serial.print(humidity);
Serial.println(" %");
#define DHTPIN A1
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
void setup() {
Serial.begin(9600);
dht.begin();
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 allocation failed");
for (;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
}
void loop() {
float h = dht.readHumidity();
float t = dht.readTemperature();
if (isnan(h) || isnan(t)) {
Serial.println("Sensor error");
return;
}
display.clearDisplay();
display.setCursor(0, 0);
display.print("Temp: ");
display.print(t);
display.println(" C");
display.print("Hum: ");
display.print(h);
display.println(" %");
display.display();
delay(2000);
}
//DHT11 data logger store data in EEPROM Display on terminal when Button D2 is
pressed
#include <EEPROM.h>
#include <DHT.h>
#define DHTPIN A1
#define DHTTYPE DHT11
#define BUTTON_PIN 2
// Each entry will take 4 bytes: 2 for temperature, 2 for humidity (float values
stored as int * 100)
const int RECORD_SIZE = 4;
const int MAX_RECORDS = 100; // EEPROM on Nano has 1024 bytes. 100 records = 400
bytes max.
int recordIndex = 0;
void setup() {
Serial.begin(9600);
dht.begin();
pinMode(BUTTON_PIN, INPUT);
Serial.println("EEPROM Data Logger Ready");
}
void loop() {
static unsigned long lastLogTime = 0;
recordIndex++;
} else {
Serial.println("Sensor error, skipping logging.");
}
lastLogTime = millis();
}
EEPROM.put(addr, t);
EEPROM.put(addr + 2, h);
}
temp = t / 100.0;
hum = h / 100.0;
}
Notes:
EEPROM has limited write cycles (around 100,000 per cell). Avoid writing too often in real
-world applications. If needed, you can extend this to use external EEPROM or SD card for
longer-term or frequent logging.
CHAPTER 16
IR LED
An IR LED is a diode that emits infrared light when current
flows through it. It is used in remote controls to send out bursts
of modulated light representing data.
IR Sensor
Just like an IR LED there is an IR sensor, this is also a diode like
device that is sensitive to IR light usually in the same range of
wavelength as is emitted by the IR LED. Together the IR LED
and sensor are used to detect the obstacles, and detect lines for
line following robots. When the IR sensor receives the IR light it
lowers its resistance.
IR Remote Receiver
The IR Remote control, has the same IR LED as mentioned above,
but it modulates the signal on a square wave of 38KHz. This helps
isolate the communication from surrounding IR noise.
An IR receiver module (such as TSOP382, VS1838B, or HS0038)
is designed to detect IR signals modulated at a specific frequency—typically 38kHz. It de-
modulates this signal and provides a clean digital output to the microcontroller.
NEC Protocol
• Widely used in consumer electronics.
• Uses a 38kHz carrier.
• Data is 32 bits:
• 8-bit Address
• 8-bit Logical Inverse of Address
• 8-bit Command
• 8-bit Logical Inverse of Command
• Uses pulse distance encoding.
void loop() {
if (receiver.decode(&results)) {
Serial.println(results.value, HEX);
receiver.resume();
}
}
When a button is pressed the code is sent, when the button is pressed for long time, after the
initial code, FFFFFFFF is sent indicating that last sent code is being repeated.
You create an instance of the receiver object and pass on the pin module is connected as pa-
rameter. Then there is a complex variable type (actually a structure) that holds the results of
decoded data. Decode_results is the data type defined by the library to hold the results of
decoding. receiver.enableIRIn();
This command enables the receiver co start monitoring the IO pin.
receiver.decode(&results)
This function is passed the address of results variable, and it will return the data if any.
The results structure variable will then contain all the required data. fter receiving every
command we have to reset the receiver by giving resume command.
receiver.resume();
Transmitting IR Signals
With the use of an IR LED and current-limiting resistor, you can transmit signals from Ar-
duino using:
irsend.sendNEC(code, bits);
Sends an NEC protocol signal.
irsend.sendSony(code, bits);
Sends a Sony protocol signal.
Other available functions:
sendRC5()
sendRC6()
sendPanasonic()
sendJVC()
sendSamsung()
#include <IRremote.h>
// Replace these with your remote's button codes (use Serial Monitor to find
them)
#define RED_BUTTON 0xFF6897
#define GREEN_BUTTON 0xFF9867
#define BLUE_BUTTON 0xFFB04F
IRrecv irrecv(RECV_PIN);
decode_results results;
void setup() {
Serial.begin(9600);
irrecv.enableIRIn(); // Start the receiver
pinMode(redLED, OUTPUT);
pinMode(greenLED, OUTPUT);
pinMode(blueLED, OUTPUT);
}
void loop() {
if (irrecv.decode(&results)) {
Serial.print("Received: 0x");
Serial.println(results.value, HEX);
switch (results.value) {
case RED_BUTTON:
redState = !redState;
digitalWrite(redLED, redState);
break;
case GREEN_BUTTON:
greenState = !greenState;
digitalWrite(greenLED, greenState);
break;
case BLUE_BUTTON:
blueState = !blueState;
digitalWrite(blueLED, blueState);
break;
case 0xFFFFFFFF: // Repeat code
// Optionally ignore or repeat last command
break;
}
Assignments:
1. Make an LED Dimmer to increase or decrease the brightness using a remote control
2. Make a Remote control copier. Read the values of a remote control, and then make a transmitter to
transmit those values.
CHAPTER 17
One of the reasons for choosing microcontrollers for application development is to reduce
the external hardware dependence and get the equivalent functionality using software.
Though this is a great idea, this not only reduces the size
of product but also cost. Driving seven segment LED
displays, has always been traditionally done through mi-
crocontrollers using digital IO lines and multiplexing
more digits to display the numbers. This technique
though good, and applicable in many situations, but has
few drawbacks. The LEDs are current driven devices,
when they are scanned for multiplexing the average
amount of power delivered is reduced and therefore dig-
its become dull to display. Secondly if scanning is inter-
rupted as for reading some sensor etc, the display tends to flicker. Special integrated circuits
that can drive and scan the LEDs are developed to help these matters. We have seen previ-
ously that MAX7219 is one of them. Today we are going to explore yet another display
driver IC, TM1637 and its elder brother TM1638.
The TM1637 and TM1638 are versatile driver ICs widely used in Arduino-based projects
for controlling LED displays and interfacing with input devices. These chips are popular in
the maker community due to their simplicity, efficient communication protocols, and ease
of integration into a wide range of microcontroller-based applications. Both ICs are pro-
duced by Titan Micro Electronics and are designed specifically to handle multiple 7-
segment LED displays and other components like LEDs and push buttons with minimal
wiring.
The TM1637 is primarily known for its role in controlling 4-digit 7-segment displays. It
uses a two-wire serial communication interface, similar in nature to I²C but with a custom
protocol, which makes it extremely convenient for use with microcontrollers that have lim-
ited I/O capabilities. This IC handles the multiplexing and current limiting internally, which
offloads much of the work from the microcontroller and simplifies the hardware design.
With only two pins required for communication—usually referred to as CLK (clock) and
DIO (data input/output)—the TM1637 allows efficient control of display modules while
freeing up other microcontroller pins for additional tasks. It is often used in digital clocks,
timers, and temperature displays.
On the other hand, the TM1638 is a more powerful and feature-rich driver IC, typically
used in modules that combine multiple 7-segment displays, a row of LEDs, and push but-
tons in a compact layout. It communicates over a 3-wire serial interface and is capable of
controlling up to 8 digits of 7-segment displays, 8 individual LEDs, and up to 24 key inputs.
This makes it ideal for applications where both output display and user input are needed in a
single module, such as in control panels, counters, or digital dashboards. Like the TM1637,
the TM1638 offloads the complexity of multiplexing and scanning, allowing for smooth and
flicker-free display updates and reliable button detection.
Both TM1637 and TM1638 play an important role in making LED display and input control
simple and efficient in Arduino projects. They allow developers to build visually interactive
systems with minimal hardware complexity and reduced I/O pin usage, which is especially
valuable when working with microcontrollers that have limited resources. Their widespread
availability and well-documented Arduino libraries make them an essential topic for anyone
learning or teaching embedded system programming using Arduino.
TM1637
Interfacing the TM1637 with an Arduino is straightforward and requires only two GPIO
pins from the microcontroller. These are typically connected to the CLK (clock) and DIO
(data input/output) pins of the TM1637-based display module. Unlike standard I²C or SPI
protocols, the TM1637 uses a custom 2-wire serial communication protocol, which is
synchronous but does not follow I²C addressing or handshaking conventions. This custom
protocol consists of start and stop conditions, followed by a sequence of commands and data
bytes, all managed through software-controlled timing (bit-banging). Due to its simplicity,
many ready-made Arduino libraries—such as TM1637Display.h—exist to abstract the low-
level communication, allowing users to display numbers, characters, or scrolling text with
minimal code.
The communication begins when the Arduino sends a start condition by pulling both CLK
and DIO low, followed by shifting out command or data bytes bit by bit while toggling the
clock line. After data transmission, a stop condition is sent by setting the clock high while
the data line is also released to high. Though the TM1637 is not a fully open-drain bus like
I²C, it expects pull-up resistors on both CLK and DIO lines—typically 10kΩ—to ensure
reliable signal levels. Some modules already include these resistors onboard.
From an electrical perspective, the TM1637 operates at 5V DC, making it directly compati-
ble with standard Arduino boards such as the Uno, Nano, or Mega. It can source a limited
amount of current to drive common cathode 7-segment displays, with each segment capa-
ble of sinking up to 40mA, though typical usage is much lower. To protect the IC and en-
sure consistent brightness, internal current-limiting resistors are often included in the dis-
play module itself. If not, external resistors should be added.
The TM1637 is generally used with 4-digit 7-segment displays, where it performs auto-
matic multiplexing to reduce power consumption and simplify circuit design. This means it
turns on each digit sequentially in rapid succession, giving the illusion of a continuous dis-
play to the human eye. Due to current limitations and multiplexing speed, the TM1637 is
best suited for small- to medium-sized displays—typically 0.36" to 0.56" digit height.
Larger displays with higher current demands may not be suitable without additional driver
circuitry.
Driving up to 6 Digits
Although many ready-made modules use the TM1637 with a 4-digit display, the IC itself is
capable of driving up to 6 digits. It features:
• 6 segment output pins (SEG1–SEG6)
• 8 grid (digit select) pins (GRID1–GRID6 and others)
Each digit typically uses 8 segments (7 segments plus decimal point), and the TM1637 han-
dles the multiplexing of up to 6 digits internally. So, if you're designing your own display
module or using a larger one that exposes more connections, the TM1637 can indeed be
configured to handle a 6-digit 7-segment display. However, this comes at the cost of more
current draw and may require careful attention to brightness and power considerations.
Modules with 6-digit displays are less common, partly due to size and power constraints,
but are entirely possible if you're working with the IC directly or custom hardware.
Connect DIO to pin 11 and CLK to 12. You can choose any two GPIO pins. Install the
TM1637 library by Avishay Orpaz , and you are ready to go.
The example below displays a value of 1234 all commands of the library are self explanato-
ry. First you create an object of the TM1637Display type, you set the brighness of display
#include <TM1637Display.h>
void setup() {
// Set display brightness (0 to 7)
display.setBrightness(5); // Medium brightness
// Display a number
int value = 1234;
display.showNumberDec(value); // Display 1234 in decimal
}
void loop() {
// Nothing to do here
}
#include <TM1637Display.h>
void setup() {
display.setBrightness(7); // Max brightness
void loop() {
// Show counting number
display.showNumberDec(counter);
// Wait 1 second
delay(1000);
counter++;
delay(2000);
display.clear();
counter = 0; // Reset counter
}
}
#define CLK 3
#define DIO 2
This sets up communication between the Arduino and the TM1637 using digital pins 3
(CLK) and 2 (DIO). The library handles the underlying communication protocol.
Leading Zeros
display.showNumberDec(i, true);
The second argument true forces the display of leading zeros, so "42" becomes "0042".
#include <TM1637Display.h>
#define CLK 12
#define DIO 11
void setup() {
display.setBrightness(7); // Max brightness
display.setSegments(HELP); // Display HELP
}
void loop() {
// Nothing in loop, static display
}
TM1638 Module
TM1638 is the big brother of TM1637 and can handle more digits, and scan inputs. It has a
built-in constant current source for deriving the LEDs.
This module contains a TM1638 chip, 8 digits of seven segment display, 8 individual LEDs
and 8 push buttons. The module uses a custom three wire protocol to communicate with mi-
crocontroller. This combination makes it an ideal component for Arduino-based control
panels, debug consoles, educational kits, and DIY embedded systems, offering a simple
and effective way to interact with the user through both visual output and physical input.
TM1638 uses three pins to communicate, a CLK, DIO and STB (strobe or latch). The DIO
is bidirectional, it can send data to the chip as well as read from the chip.
Key Features
• Segment and LED control: Each of the 8 digits has 8 bits (7 segments + decimal
point), and the 8 LEDs are individually addressable.
• Button scan matrix: All 8 buttons are scanned and their states can be read with a single
command.
• Integrated current control: TM1638 includes constant current drivers, removing the
need for external resistors.
• Simplified microcontroller interface: Only 3 pins are required, making it suitable for
microcontrollers with limited I/O.
How It Works
The TM1638 handles all display and input logic internally. The Arduino sends data serially
to:
Update the LED segments and individual LEDs.
Query the state of buttons.
This offloads much of the work from the microcontroller and allows the Arduino to interact
with the module using a few simple commands, without the need to continuously scan but-
tons or multiplex displays manually.
Data transfer is handled through:
STB (Strobe): Latches commands and data.
CLK (Clock): Clocks bits in/out.
DIO (Data I/O): Bidirectional data line for both sending and receiving data.
The TM1638 supports multiple addressing modes and automatic address increment,
which means you can send a stream of data to fill all displays or LEDs in sequence, or target
specific segments or LEDs as needed.
Let’s proceed with the first demo program to interface the TM1638 module with an Ar-
duino. This basic example will:
Initialize the TM1638 module
Display a number on the 7-segment display
Turn on LEDs one by one
Read button inputs and show their states
We’ll use the popular and easy-to-use TM1638 library by rjbatista, which simplifies all
interactions. This library is not available through Arduino library manager, instead you can
download the .zip file from here:
https://github.com/rjbatista/tm1638-library
Then in Arduino library manager, use .zip library installer to install this library.
Connect the STB pin to Arduino pin 7, DIO to 8 and CLK to 9. You can use any digital
pins.
#include <TM1638.h>
#define DIO 8
#define CLK 9
#define STB 7
void setup() {
// Show a welcome message on the segments
module.setDisplayToString("HELLO");
delay(2000);
// Display a number
module.setDisplayToDecNumber(12345678, 0, false);
void loop() {
// Read buttons and reflect their state on LEDs
byte buttons = module.getButtons();
Explanation
module.setDisplayToString("HELLO");
Displays the string HELLO on the segments. Only characters supported by 7-segment encoding will appear
correctly (E, L, H, etc.).
module.setDisplayToDecNumber(12345678, 0, false);
Displays the number 12345678. The second parameter is the dots (decimal points), which we are skipping for
now.
module.setLED(TM1638_COLOR_RED, i);
Lights up red LEDs one after another on positions 0 to 7.
module.getButtons();
Reads the 8 push buttons. Each bit of the returned byte represents one button (bit 0 = button
0, bit 1 = button 1, etc.).
module.setLED(...); in loop()
Updates each LED depending on whether its corresponding button is pressed.
module.setDisplayToDecNumber(buttons, 0, false);
Displays the raw binary value of the pressed buttons as a decimal number.
Let’s now explore how to display custom text and scroll messages on the TM1638 mod-
ule, and also understand how to format text when dealing with the limitations of 7-segment
displays.