FriendlyWire.com


beginner-friendly electronics tutorials and projects


Discover the joy of understanding electronics!

How to make a scrolling text display

March 3, 2020 project updated March 20, 2020

Some time ago we learned how to use shift registers with microcontrollers, and in this project we will use them together with the PIC16F1455 microcontroller to build a scrolling text display. In this article we will go through all necessary steps so that you can easily build your own! Let's go :)

Here are the features of our 7-segment scrolling text display:

  • Eight letter display, but custom expandability.
  • Adjustable scrolling speed.
  • It can show as many pre-programmed messages as you want, with a total length of around 6000 characters.
  • Use the PICkit3 to flash your custom message onto the PIC16F1455 or to customize the font.

Let me know on social media if you built this project, or if you feel that something is missing from this tutorial. I look forward to hearing from you!

What do you need?

Here is a picture of all the components that are needed for the scrolling text display (and there is a detailed list in the resources box of this article as well):

  • This project is centered around the PIC16F1455 microcontroller, which will store the texts that are displayed on the 7-segment displays. It will also interface with the shift registers, the pushbutton, and the potentiometer.
  • As shift registers we will use the CD4094 registers, but in principle you can use any shift register you want.
  • The 7-segment displays are common cathode displays, but if you only have common anode displays you can use them as well but will have to tweak the code a little bit, more on that below.
  • I decided to use 170-pin breadboards for this project because I wanted the entire circuit to fit into a small housing, and other breadboards with power rails on either side (or even just on one side) were too wide. You can of course choose the breadboards you like best for this project.
  • Other than that, there is only a switch to turn the display on or off, a 4.5V battery compartment (which you could also choose to be 3×AA for a longer battery lifetime, but I couldn't due to space constraints), a couple of 220Ω resistors for the 7-segment displays, and a pushbutton as well as a 4.7kω potentiometer (that could also be 5kω or 10kΩ).
  • Important: You will also need the PICkit3 to program and flash the PIC16F1455 microcontroller, together with a 6-pole pin header, more on that below.

And that's about it! Let's talk about the schematic next!

The schematic

Before we look at the full schematic will all eight displays, let us focus on the essentials:

Alright, it looks like a lot, but as always, let's break it down into smaller pieces that are easy to understand :)

  • The PIC16F1455 microcontroller is the brain of the circuit. It connects to the pushbutton S2 and to the potentiometer R9, and will check during operation if the button is pressed and what value the potentiometer is set to. The switch S1 can be toggled to cut the power to the entire circuit.
  • Now what are those funny symbols IC1P, IC2P, and IC8P on the left side? Those are the power connections to the shift register ICs IC1, IC2, and IC8. It just means that for each shift register that we use we have to connect its pin 16 to +4.5V and its pin 7 to ground. It's a bit confusing, but you can read more about that kind of symbol in our article on how to read schematics.
  • The symbol JP1 is a six-pole pin header. We have used it many times before, because we need to connect the PICkit3 to the PIC16F1455 microcontroller to flash the .hex file onto the controller, because without a .hex file the controller does not know what to do. But other than in all previous projects we will include this pin header permanently in our circuit because this way it will be super easy to change the text that our scrolling display will show. This technique is also called in-circuit serial programming and is often abbreviated as ICSP. It is just terribly convenient that we don't have to remove the PIC16F1455 from the circuit to update the code.
  • As an aside, you might wonder: why is this the first time we are using ICSP? I did not want to include it in the previous projects because those never really needed us to change the source code after assembling the test circuit. Program it once, and it runs forever as an electronic dice or a binary clock or an electronic candle. But with a scrolling text display we might want to change the text some time down the road :)
  • Alright, so all that JP1 does is provide convenient connections to both VDD and ground as well as to the three programming pins MCLR (master clear), PGD (programming data), and PGC (programming clock). The sixth pin can be left unconnected.
  • Now we can focus on the shift registers IC1, IC2, and IC8. Let us first only look at IC1. Here is the relevant part of the schematic that only involves IC1:

  • Each CD4094 register has eight outputs Q1-Q8, and we use them to drive the seven segments (and the decimal point) of each 7-segment display. The assignment of where Q1 goes (here to segment 'e') is completely random and can be adjusted in the software that we will discuss below.
  • The pin OE (Output Enable) is permanently tied to VDD (which is +4.5V in this case) because we want all 7-segment displays to be on during normal operation. If you connect OE to ground instead, all outputs Q1-Q8 will be low.
  • CLK (clock) and STR (strobe) are the main controls of each register, and they are all connected in parallel. This means that for all shift registers in this circuit we will connect their CLK pins together, and then we will connect all their STR pins together, so that we only need two wires to set the CLK and STR pins of all registers. This is very important.
  • The D (data) pin is where a registers is fed its serial data, that can either be 1 or 0, and this is where it gets interesting. The first register (meaning the one on the right, where the scrolling text will first appear) receives the data directly from the PIC16F1455 microcontroller. The data “goes in” on the right of the display.
  • Let us imagine we write one bit to the shift register. Then, when you pulse the clock pin eight times (read more in our shift register tutorial), that bit that we started with would be lost because the register is only eight bits wide. But each register has the QS* output, where that bit will appear, and if we want to keep it we can just connect the QS* output of the first register to the clock input of the next pin. This is called cascading shift registers because the data is not lost but passed along into the next shift register :)
  • And I hope you can imagine that you can repeat that process many many times. In principle, you can cascade millions of shift registers, but in this project we will only cascade eight of them to keep it manageable.

And that's the main idea!

For completeness, here is the full schematic of the project as I will build it in this article:

All what happens is that there are now the additional ICs IC3 through IC7 as well as their 7-segment displays LED3 through LED7 that arise from cascading many shift registers. You can imagine that it is quite easy to add even more displays: all that you need to do is connect the previous QS' pin to the following DATA pin, and connect all CLOCK and STROBE pins together :)

If you add too many displays, however, one of two things will happen to you:

  • The 7-segment displays will draw too much current, so make sure that your power supply can handle it.
  • The display will take very long to clear, because for clearing the display you need to send a zero everywhere (more on that below), and not just pop in the next letter (which always takes the same amount of time, irrespective of how many displays you are using).

Alright, now we can in principle build the circuit following our schematic above. But before we do that I want to talk about the source code that tells our microcontroller how to send out the data to the shift registers so that some text appears.

The source code

All microcontroller have to be programmed, otherwise they are just empty chips who don't know what to do. We will program our PIC16F1455 in the programming language C and use the freely available XC8 compiler which works great with MicroChip's also freely available integrated development environment MPLAB X IDE.

A compiler converts the human-readable C code (OK, kind of human-readable) into machine code that is tailored for the specific controller you are using, here it's the PIC16F1455. This file is called a .hex file, and we need to transfer this file onto the controller so that the controller knows what to do. For this we need a so-called programmer, and I will use the PICkit3 which only costs around $25 and is a great investment if you want to work with PIC microcontrollers. You can learn more about microcontroller basics in our introductory tutorial.

Alright, let's have a look at the code. We will go through the most relevant parts first, and then go through the rest chronologically. You can find the full source code in the appendix or download the source code file (called main.c) in the resources box, along with its readily compiled .hex file. But since this scrolling text display contains text, you might want to compile your own .hex file that contains your own text :)

Speaking of which, these lines here tell the controller your lines:

// define your texts
// (can be as many as you want, total length is around 6000 characters)
char *myTexts[] = {"GREETINGS, NICE PEOPLE OF THE INTERNET!    ",
                   "HELLO THERE.  GENERAL KENOBI, YOU ARE A BOLD ONE!    ",
                   "WHAT IS THE ANSWER TO THE ULTIMATE QUESTION OF LIFE, THE UNIVERSE, AND EVERYTHING?     ",
                   "42.     ",
                   "THANK YOU SO MUCH FOR WATCHING, AND I WILL SEE YOU NEXT TIME!            "};

You can change them to anything you like, and even add more lines (or less). For example, this here is also possible:

// define your texts
// (can be as many as you want, total length is around 6000 characters)
char *myTexts[] = {"text 1    ",
                   "text 2    ",
                   "text 3     ",
                   "text 4.............    ",
                   "text 5   ",
                   "this getting old yet?    ",
                   "yawn!     "};

You get the idea. The blank spaces at the end are necessary so that there is some blank space after the text has scrolled through and before it begins again.

There is a whole lot of space on a PIC16F1455. To get some idea, you can program eight entire paragraphs of Lorem Ipsum into the PIC16F1455 (download the .hex file lorem-ipsum.hex that does just that):

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus molestie nunc non blandit massa enim nec dui nunc. Fames ac turpis egestas sed tempus urna. Libero enim sed faucibus turpis in. Elementum integer enim neque volutpat ac tincidunt vitae. Imperdiet nulla malesuada pellentesque elit eget gravida cum sociis. Viverra adipiscing at in tellus integer feugiat scelerisque. Ut enim blandit volutpat maecenas volutpat. Dolor morbi non arcu risus quis varius quam quisque id. Urna nec tincidunt praesent semper feugiat nibh. Egestas sed sed risus pretium. Tincidunt tortor aliquam nulla facilisi cras fermentum odio eu feugiat. Dignissim enim sit amet venenatis urna cursus eget nunc. Pulvinar mattis nunc sed blandit libero volutpat sed. Magna fringilla urna porttitor rhoncus dolor purus non enim praesent. Adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus urna. Nulla facilisi morbi tempus iaculis urna id volutpat lacus. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Tortor id aliquet lectus proin nibh nisl condimentum id. Semper auctor neque vitae tempus. Quis risus sed vulputate odio ut enim. Nulla facilisi morbi tempus iaculis. Tincidunt nunc pulvinar sapien et ligula ullamcorper. Vitae suscipit tellus mauris a diam maecenas sed enim. Porttitor leo a diam sollicitudin tempor id. Et malesuada fames ac turpis egestas maecenas. At elementum eu facilisis sed odio morbi quis commodo. Egestas dui id ornare arcu odio ut sem nulla pharetra. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque pulvinar pellentesque. Eget nunc scelerisque viverra mauris in aliquam sem. Eget aliquet nibh praesent tristique magna. Consequat interdum varius sit amet mattis vulputate enim nulla aliquet. Massa vitae tortor condimentum lacinia quis. Dui accumsan sit amet nulla facilisi morbi tempus. Ut tortor pretium viverra suspendisse potenti. Integer malesuada nunc vel risus commodo viverra maecenas accumsan lacus. Non quam lacus suspendisse faucibus interdum posuere lorem ipsum. Sagittis eu volutpat odio facilisis mauris. Condimentum vitae sapien pellentesque habitant morbi tristique. Vitae suscipit tellus mauris a diam maecenas sed enim ut. At lectus urna duis convallis. Magna sit amet purus gravida quis. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Condimentum mattis pellentesque id nibh tortor id aliquet lectus proin. In nisl nisi scelerisque eu ultrices vitae auctor eu. Orci sagittis eu volutpat odio facilisis mauris. Et odio pellentesque diam volutpat commodo sed egestas egestas fringilla. Sed elementum tempus egestas sed sed risus. Sit amet venenatis urna cursus eget. Enim ut sem viverra aliquet eget sit. Donec adipiscing tristique risus nec feugiat in fermentum posuere. Neque convallis a cras semper auctor neque vitae tempus quam. Quis ipsum suspendisse ultrices gravida dictum fusce ut placerat. Arcu ac tortor dignissim convallis aenean et tortor at risus. Eget dolor morbi non arcu risus quis varius quam. Consectetur adipiscing elit ut aliquam purus sit amet luctus. Purus in mollis nunc sed id. Posuere morbi leo urna molestie at. Consectetur libero id faucibus nisl tincidunt eget nullam non nisi. Varius duis at consectetur lorem. Porttitor leo a diam sollicitudin tempor. Purus in mollis nunc sed id semper risus in. Elit eget gravida cum sociis natoque penatibus. Mauris cursus mattis molestie a iaculis at erat pellentesque. Varius vel pharetra vel turpis nunc eget. Neque egestas congue quisque egestas diam in. Diam donec adipiscing tristique risus nec. Viverra vitae congue eu consequat ac felis donec et odio. Ornare arcu dui vivamus arcu felis. Viverra maecenas accumsan lacus vel facilisis volutpat est velit egestas. Semper feugiat nibh sed pulvinar proin gravida. Et tortor consequat id porta nibh venenatis cras sed. Elementum integer enim neque volutpat ac. Risus at ultrices mi tempus. Varius vel pharetra vel turpis nunc eget. In metus vulputate eu scelerisque felis imperdiet. Pellentesque sit amet porttitor eget. Ut venenatis tellus in metus vulputate eu. In aliquam sem fringilla ut morbi. Habitant morbi tristique senectus et netus et malesuada. Massa vitae tortor condimentum lacinia quis vel eros donec. Tellus integer feugiat scelerisque varius morbi enim nunc faucibus a. Libero justo laoreet sit amet cursus sit amet dictum. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus nullam. Interdum consectetur libero id faucibus. Duis at tellus at urna. Ullamcorper sit amet risus nullam eget felis eget. Arcu felis bibendum ut tristique et egestas quis ipsum. Rhoncus dolor purus non enim. Viverra orci sagittis eu volutpat odio facilisis mauris sit amet. Ut consequat semper viverra nam libero justo laoreet sit. A scelerisque purus semper eget. Fringilla ut morbi tincidunt augue interdum velit euismod in pellentesque. Pellentesque diam volutpat commodo sed. Velit euismod in pellentesque massa placerat duis ultricies lacus sed. Aliquet sagittis id consectetur purus. Rhoncus mattis rhoncus urna neque viverra justo. Netus et malesuada fames ac turpis egestas. Mauris ultrices eros in cursus turpis massa tincidunt dui. Facilisi morbi tempus iaculis urna id volutpat lacus laoreet non. Pharetra diam sit amet nisl suscipit adipiscing bibendum est. Nascetur ridiculus mus mauris vitae ultricies leo integer malesuada nunc. Et netus et malesuada fames ac turpis egestas sed. Congue quisque egestas diam in arcu cursus. Faucibus in ornare quam viverra. Donec massa sapien faucibus et molestie. Cursus turpis massa tincidunt dui ut. Semper auctor neque vitae tempus quam pellentesque nec nam aliquam. Ut morbi tincidunt augue interdum. Netus et malesuada fames ac. Vitae tempus quam pellentesque nec nam. Enim nulla aliquet porttitor lacus luctus accumsan tortor posuere ac. Arcu risus quis varius quam. Leo a diam sollicitudin tempor id eu nisl nunc. Id faucibus nisl tincidunt eget nullam non nisi. Feugiat vivamus at augue eget arcu dictum. Posuere morbi leo urna molestie at elementum eu. Congue mauris rhoncus aenean vel elit. Eu sem integer vitae justo eget. Orci eu lobortis elementum nibh. A diam sollicitudin tempor id eu nisl. Condimentum mattis pellentesque id nibh. Vel elit scelerisque mauris pellentesque. Volutpat maecenas volutpat blandit aliquam etiam erat velit. Pharetra magna ac placerat vestibulum lectus mauris ultrices. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus. Ut venenatis tellus in metus vulputate eu scelerisque felis imperdiet. In hendrerit gravida rutrum quisque non tellus orci ac. Mattis pellentesque id nibh tortor id aliquet lectus. Et netus et malesuada fames. Est ultricies integer quis auctor elit sed. Pellentesque sit amet porttitor eget. Nunc eget lorem dolor sed viverra ipsum nunc. Pellentesque elit eget gravida cum sociis natoque penatibus et magnis.

Or, if mathematics is your thing, you can let the scrolling text display show the the first 6000 digits of π (pi6000.hex is the .hex file for that):

3.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493038196442881097566593344612847564823378678316527120190914564856692346034861045432664821339360726024914127372458700660631558817488152092096282925409171536436789259036001133053054882046652138414695194151160943305727036575959195309218611738193261179310511854807446237996274956735188575272489122793818301194912983367336244065664308602139494639522473719070217986094370277053921717629317675238467481846766940513200056812714526356082778577134275778960917363717872146844090122495343014654958537105079227968925892354201995611212902196086403441815981362977477130996051870721134999999837297804995105973173281609631859502445945534690830264252230825334468503526193118817101000313783875288658753320838142061717766914730359825349042875546873115956286388235378759375195778185778053217122680661300192787661119590921642019893809525720106548586327886593615338182796823030195203530185296899577362259941389124972177528347913151557485724245415069595082953311686172785588907509838175463746493931925506040092770167113900984882401285836160356370766010471018194295559619894676783744944825537977472684710404753464620804668425906949129331367702898915210475216205696602405803815019351125338243003558764024749647326391419927260426992279678235478163600934172164121992458631503028618297455570674983850549458858692699569092721079750930295532116534498720275596023648066549911988183479775356636980742654252786255181841757467289097777279380008164706001614524919217321721477235014144197356854816136115735255213347574184946843852332390739414333454776241686251898356948556209921922218427255025425688767179049460165346680498862723279178608578438382796797668145410095388378636095068006422512520511739298489608412848862694560424196528502221066118630674427862203919494504712371378696095636437191728746776465757396241389086583264599581339047802759009946576407895126946839835259570982582262052248940772671947826848260147699090264013639443745530506820349625245174939965143142980919065925093722169646151570985838741059788595977297549893016175392846813826868386894277415599185592524595395943104997252468084598727364469584865383673622262609912460805124388439045124413654976278079771569143599770012961608944169486855584840635342207222582848864815845602850601684273945226746767889525213852254995466672782398645659611635488623057745649803559363456817432411251507606947945109659609402522887971089314566913686722874894056010150330861792868092087476091782493858900971490967598526136554978189312978482168299894872265880485756401427047755513237964145152374623436454285844479526586782105114135473573952311342716610213596953623144295248493718711014576540359027993440374200731057853906219838744780847848968332144571386875194350643021845319104848100537061468067491927819119793995206141966342875444064374512371819217999839101591956181467514269123974894090718649423196156794520809514655022523160388193014209376213785595663893778708303906979207734672218256259966150142150306803844773454920260541466592520149744285073251866600213243408819071048633173464965145390579626856100550810665879699816357473638405257145910289706414011097120628043903975951567715770042033786993600723055876317635942187312514712053292819182618612586732157919841484882916447060957527069572209175671167229109816909152801735067127485832228718352093539657251210835791513698820914442100675103346711031412671113699086585163983150197016515116851714376576183515565088490998985998238734552833163550764791853589322618548963213293308985706420467525907091548141654985946163718027098199430992448895757128289059232332609729971208443357326548938239119325974636673058360414281388303203824903758985243744170291327656180937734440307074692112019130203303801976211011004492932151608424448596376698389522868478312355265821314495768572624334418930396864262434107732269780280731891544110104468232527162010526522721116603966655730925471105578537634668206531098965269186205647693125705863566201855810072936065987648611791045334885034611365768675324944166803962657978771855608455296541266540853061434443185867697514566140680070023787765913440171274947042056223053899456131407112700040785473326993908145466464588079727082668306343285878569830523580893306575740679545716377525420211495576158140025012622859413021647155097925923099079654737612551765675135751782966645477917450112996148903046399471329621073404375189573596145890193897131117904297828564750320319869151402870808599048010941214722131794764777262241425485454033215718530614228813758504306332175182979866223717215916077166925474873898665494945011465406284336639379003976926567214638530673609657120918076383271664162748888007869256029022847210403172118608204190004229661711963779213375751149595015660496318629472654736425230817703675159067350235072835405670403867435136222247715891504953098444893330963408780769325993978054193414473774418426312986080998886874132604721569516239658645730216315981931951673538129741677294786724229246543668009806769282382806899640048243540370141631496589794092432378969070697794223625082216889573837986230015937764716512289357860158816175578297352334460428151262720373431465319777741603199066554187639792933441952154134189948544473456738316249934191318148092777710386387734317720754565453220777092120190516609628049092636019759882816133231666365286193266863360627356763035447762803504507772355471058595487027908143562401451718062464362679456127531813407833033625423278394497538243720583531147711992606381334677687969597030983391307710987040859133746414428227726346594704745878477872019277152807317679077071572134447306057007334924369311383504931631284042512192565179806941135280131470130478164378851852909285452011658393419656213491434159562586586557055269049652098580338507224264829397285847831630577775606888764462482468579260395352773480304802900587607582510474709164396136267604492562742042083208566119062545433721315359584506877245

Okay, so there is enough space on there to put in whatever text you want. The microcontroller converts each digit into a 7-segment character. Here is the character set:


  • 0

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • A

  • B

  • c

  • C

  • d

  • E

  • F

  • G

  • h

  • H

  • i

  • I

  • j

  • J

  • K

  • l

  • L

  • M

  • n

  • N

  • o

  • O

  • P

  • Q

  • R

  • S

  • T

  • u

  • U

  • v

  • V

  • w

  • x

  • y

  • Y

  • Z

  • .

  • ,

  • ;

  • !

  • ?

  • -

  • _

  • '

  • "

  • =

  • °

  • (

  • )

It's not perfect, especially the letters K, M, W, and X are problematic, and the letters u and v, and some others, are not always unique. I hope that from the context it becomes clear what is meant. Here are two examples:

If you don't like these fonts, you can simply adjust the function convertCharacterToPattern that starts in line 192. Let's have a look:

// This function converts an ASCII character into its corresponding
// 7-segment symbol. You can modify this function to create your own
// custom 7-segment font.
unsigned char convertCharacterToPattern (char ASCII) {
    
    // what is the character?
    switch (ASCII) {
        
        // numbers         abcdefg.
        case '0': return 0b11111100;
        case '1': return 0b01100000;
        case '2': return 0b11011010;
        case '3': return 0b11110010;
        case '4': return 0b01100110;
        case '5': return 0b10110110;
        case '6': return 0b10111110;
        case '7': return 0b11100000;
        case '8': return 0b11111110;
        case '9': return 0b11110110;
        
        // letters         abcdefg.
        case 'a': return 0b11101110;
        case 'A': return 0b11101110;
        case 'b': return 0b00111110;
        case 'B': return 0b00111110;
        case 'c': return 0b00011010;

...and so on. Basically it assigns a bit pattern for each possible letter in one giant switch statement.

Okay, now we know how you can create your own text and where you can change the characters if you want to. Next, you might wonder where we tell the controller how to connect the segments a-g and the decimal point to the eight outputs Q1-Q8 of the shift register. This is done here:

// how are your segments connected to the shift register?
// map        a  b  c  d  e  f  g  .
char map[] = {7, 8, 3, 2, 1, 6, 5, 4};

These lines mean that segment a is connected to Q7, segment b to Q8, segment c to Q3, and so on. The decimal point is connected to Q4. If you, for some reason, want to use different connections then you can adjust this single line here and don't have to change the segment patterns in the convertCharacterToPattern function, which would be a lot of work. I hope this makes your life simpler as well :)

For the rest of this section we will go through the code chronologically and explain every line as necessary.

// declare functions
void clearDisplay (int digits);
unsigned char convertCharacterToPattern (char ASCII);
void sendValue (unsigned char value);

// define locations of shift register controls
#define STROBE RC5
#define DATA RC4
#define CLOCK RC3
#define NEXT !RA5

// variables
int text_index = 0, char_index = 0, adc_value, TIME_BUFFER, NEXT_BUFFER;
  • In lines 44-47 we define custom functions that will make our life simpler down the road. I will talk more about them when we get there.
  • Lines 49-53 define where we connect the the strobe, data, and clock lines to the microcontroller, as well as where we connect the NEXT button that allows us to switch to the next text message. If you want to change the location of those pins you will need to change these lines here, but you will also need to change some stuff later, so keep that in mind :)
  • Lines 55-56 initialize the main variables that our software will use. We define them outside the main function because we want these variables to be globally accessible, meaning also inside the functions we defined in lines 44-47 above.
// main function
void main (void) {
    
    // set internal oscillator to 4MHz
    IRCF0 = 1;
    IRCF1 = 0;
    IRCF2 = 1;
    IRCF3 = 1;
    
    // weak pull-up for button
    TRISA5 = 1;
    WPUA5 = 1;
    nWPUEN = 0;
    
    // ADC settings
    
	// ADC sampling frequency per bit is F_osc/16
	ADCS0 = 1;
	ADCS1 = 0;
    ADCS2 = 1;
    
    // result alignment
    ADFM = 0;
    
    // RC2 as analog input
	TRISC2 = 1;
    ANSC2 = 1;
    
    // select channel AN6 (which corresponds to RC2)
    CHS0 = 0;
    CHS1 = 1;
    CHS2 = 1;
    CHS3 = 0;
    CHS4 = 0;
    
    // turn the ADC on
	ADON = 1;
  • We now enter the main function at line 58.
  • Lines 61-65 set the internal oscillator to 4MHz, which is convenient because it saves us external components.
  • Lines 67-70 enable a weak internal pull-up resistor on pin RA5. This is also convenient because it means that we save that external component as well.
  • In lines 72-94 we configure the 10-bit ADC module so that we can read out the potentiometer, which is connected to pin RC2 (analog channel 6, AN6). Make sure to change this according to the PIC16F1455 datasheet (section 16.3, page 155) if you want to connect your potentiometer to a different ADC input channel. We already talked about this in great detail in the ADC tutorial, so I won't go into more detail here :)
	// set outputs and disable analog features on pin RC3
    TRISC5 = 0;
    TRISC4 = 0;
    TRISC3 = 0;
    ANSC3 = 0;
    
    // TIMER0 settings
    
	// internal clock, no prescaler, interrupt on overflow
	TMR0CS = 0;
	PSA = 1;
	TMR0IE = 1;

	// enable global interrupts
	GIE = 1;
    
    //  begin of main program
    
    // clear all eight registers
    clearDisplay(8);
  • Then, in lines 96-100 we set the pins RC3, RC4, and RC5 as output pins, and we have to disable the analog features on pin RC3 (because otherwise it would not work correctly as a purely digital output). You have to make sure to change these lines as well if you want to change the location of the clock, data, and strobe pins.
  • Lines 102-110 set up the TIMER0 module, which we will use to control the speed of our animation. You can learn more about the TIMER0 in the programming a 1Hz clock signal article.
  • Then the main program begins, and I included line 155 to send out a bunch of zeros to the registers to make sure that whatever else was stored in the registers before is replaced by zeros, so that all LED segments are turned off. If you decide to use 12 displays instead, replace the 8 by a 12, and so on.

We have almost made it! We arrived at the main loop, which is executed over and over again, until the PIC16F1455 is turned off. Here you can see it:

    // main loop
    while (1) {
        
        // get potentiometer position
        GO = 1; while (GO);
        adc_value = ADRESH;
        
        // has the "NEXT" button been pressed?
        if ((NEXT) && (NEXT_BUFFER == 0)) {
            
            // remove old text
            clearDisplay(8);
            
            // reset everything to display new text
            TIME_BUFFER = 0;
            char_index = 0;
            
            // advance to the next line of text, and go back
            // to the first one if we have arrived at the end
            text_index++;
            if (text_index >= sizeof(myTexts)/sizeof(myTexts[0])) {
                text_index = 0;
            }
            
            // reset debounce variable
            NEXT_BUFFER = 50;
            
        }
        
    }
  • In lines 120-122 we read out the eight most significant bits of the 1--bit analog to digital converter, which will be a number between 0 and 255. It will be zero if the potentiometer is turned all the way, say, left (so that the voltage on theADC input pin AN6 is zero), and it will be 255 if the potentiometer is turned all the other way so that the voltage on the ADC input pin coincides with VDD (which is 4.5V in our case). The result is then stored in the variable adc_result for convenience. You can learn more about that in the ADC tutorial or in the electronic candle project.
  • The rest of the main loop handles whatever happens when the NEXT button is pressed:
  • Line 128 removes the old text that is still visible at this point.
  • Lines 130-132 reset the current character that is being piped out to the shift registers (because we start at the beginning of a new message). It also resets the time base variable TIME_BUFFER, more on that later.
  • In lines 134-139 we advance to the next text message. If we have arrived at the end, the program will start over and start at the first line of text that we specified earlier in line 32.
  • And last, in line 142, the buffer variable NEXT_BUFFER is set to 50 in order to de-bounce the pushbutton. Learn more about that idea in the pushbutton article :)

Okay, but where in all of that have we sent our text to the shift registers? That is a great question! We haven't! We will do that now in the special interrupt service routine (sometimes abbreviated as isr). Remember lines 102-110 where we set up TIMER0? In these lines we also told the controller to call an interrupt whenever the timer flows over, which in our case happens around 3900 times per second. You can read more about it in the 1Hz article.

Enough chit chat, here is the isr in all its glory:

// the interrupt service routine
void __interrupt () isr (void) {   

    // this is called abound 3900 times per second
	if (TMR0IF) {
        
        // increase the time buffer by 1
        TIME_BUFFER++;
        
        // have we waited long enough for the text to
        // advance to the next letter?
        if (TIME_BUFFER > 4*adc_value + 100) {
            
            // reset the timing buffer variable
            TIME_BUFFER = 0;
            
            // pipe out the next text character to the shift registers
            sendValue(convertCharacterToPattern(myTexts[text_index][char_index]));
            STROBE = 1;
            STROBE = 0;
            
            // increase the character index,
            // and reset to 0 if we have reached the end
            char_index++;
            if (myTexts[text_index][char_index] == 0) {
                char_index = 0;
            }
            
        }
        
        // debounce the "NEXT" button
        if ((!NEXT) && (NEXT_BUFFER > 0)) {
            NEXT_BUFFER -= 1;
        }

		// reset interrupt flag
		TMR0IF = 0;

	}
    
}

Relax, it is not as bad as it looks :) One step at a time!

  • First, in line 156 we make sure that it was indeed the TIMER0 overflow that caused the PIC16F1455 to enter the interrupt routine. In line 188 we clear that interrupt bit, because otherwise the PIC16F1455 would keep calling this function over and over again, non-stop.
  • Lines 182-185 debounce the NEXT button, and this is exactly what we have done before as well, nothing new :)
  • Line 159 is the first interesting line: the variable TIME_BUFFER is increased by 1, and that happens roughly 3900 times per second. This is nice because it allows us to have a precise timing, since one tick in the TIME_BASE variable corresponds to roughly 250μs.
  • Line 163 makes use of that! If a certain time has passed (anything between 25 milliseconds to around 280ms), the next character is displayed. Here the position of the potentiometer comes into play. If you think the animation is too fast you can change these values and see what happens :)
  • Line 169-171 are at the absolute heart of the scrolling text display. They send out the current character. Because all shift registers are connected in series (from right to left), the characters already in display will be moved on digit to the left. In lines 170 and 171 the strobe line is pulsed to make the change in the shift register data visible.
  • sendValue is a custom function that does the sending of the data for us, more on that in a second! The object myTexts[text_index][char_index] contains all our text information. The first bracket with the variable text_index labels the current line of text that is being displayed (we can define as many as we want), and the second bracket with the variable char_index labels the current character of the current line of text that is being sent out to the registers.
  • That was a bit dense, I know, but it gets better now. In lines 173-178 we prepare the next character to be sent out to the registers. If we have reached the end of the current line of text (code in line 176) we return back to the first character.

Okay, a lot of stuff. But we are almost done! The last step is the sendValue function that takes an eight bit pattern and sends it out to the registers. Here it is:

// This function that an eight-bit value to
// the CD4094 shift registers
void sendValue (unsigned char value) {

    // auxiliary index variable
	int n = 0;
    
    // loop over all eight bits
	for (n = 0; n < 8; n++) {
        
        // Map the n-th bit to its corresponding
        // position in the shift register. This is
        // controlled by the array map[].
        DATA = (value >> (map[n]-1)) & 1;
		
        // pulse the clock pin
        CLOCK = 1;
		CLOCK = 0;
        
	}

}
  • This function loops over all eight bits of the current character that is to be sent out to the shift registers.
  • The function does not go through the bits from 0 to 7, but uses the array map[]. In line 42 we defined this array to tell the microcontroller where the different segments are connected to the shift register. We have to subtract one because the entries of map[] run from 1 to 8 and not from 0 to 7.

And that's all the code! Why did I write all of this here? Is it too much? Probably. But this contains everything I have thought about when I wrote this code, and I want you to see that it is not magic but can be understood quite easily. It is just a lot of different ideas that come together :)

Building the display board

Okay, if you have read all this: you deserve a break! And if you have skipped the previous stuff: that's fine too, you can get back to that later. If you just take the .hex file from the resources box you will also have a fully functioning scrolling text display :)

The building of the circuit is split into two parts: first, we need to connect the shift register circuit. And then we can connect everything to the PIC16F1455. Especially that second part depends a lot on your personal choice of housing, which is why I won't go into much detail on that part, and I will rather focus on how you can build the shift register circuit. Trust me: once you have built this, the remaining wires to and from the controller will be a piece of cake :)

There are three ways of how you can build this circuit:

  • Method 1: Straightforward way that takes up more space than necessary, but it is easy for beginners.
  • Method 2: Slightly advanced way that uses less space but requires soldering.
  • Method 3: Use Dupont wires to connect the 7-segment LED displays to the shift registers. This method is very flexible, but you will have a lot of wires flying around :)

You can decide what works for you. It is the same circuit, but just a different way of building it. I will first illustrate the simpler way to show you how it can be done. Then, for the rest of this tutorial, I will focus on the slightly more advanced way of construction because the result will look a bit better.

A standard 7-segment display has this pinout here:

Together with the CD4094 pinout that we selected in line 42 of the source code we have to connect it like so:

That is certainly doable, and in method one we connect everything like this:

  • Step 1

    Place the 170-pin breadboard in front of you :)

  • Step 2

    Insert the CD4094 register and make sure the notch (between pins 1 and 16) points to the left.

  • Step 3

    Insert the 7-segment display.

  • Step 4

    Insert the 220Ω resistor between the ground pin of the CD4094 (pin 8) and the center pin of the 7-segment display.

  • Step 5

    Connect pin 14 of the CD4094 with segment g of the 7-segment display.

  • Step 6

    Connect pin 13 of the CD4094 with segment f of the 7-segment display.

  • Step 7

    Connect pin 12 of the CD4094 with segment a of the 7-segment display.

  • Step 8

    Connect pin 11 of the CD4094 with segment b of the 7-segment display.

  • Step 9

    Connect pin 4 of the CD4094 with segment e of the 7-segment display.

  • Step 10

    Connect pin 5 of the CD4094 with segment d of the 7-segment display.

  • Step 11

    Connect pin 6 of the CD4094 with segment c of the 7-segment display.

  • Step 12

    Connect pin 7 of the CD4094 with the decimal point of the 7-segment display.

And that's it! (The other pins of the CD4094 have to be connected as well, of course, we will talk about that later.) But you can imagine that this way we will need one breadboard for each digit, and the digits will also not be really close together. For this reason I recommend method 2, but there is a special preparation we have to make first. You see, if we place the 7-segment display on top of the IC we can have connections like this here:

And this is very convenient, because the register IC and the 7-segment display are one the same grid. We just need to extend the pins of the 7-segment display by soldering AWG 24/0.6mm wires to it, and then we can bend the pins into place. This is how it looks like:

I hope this makes the idea clear. You also have to bend the common cathode pin to the side and connect a 220Ω resistor that then plugs into pin 8 of the CD4094 (which is the ground connection). If you don't want to do this (for example, if you don't have a soldering iron) then don't worry. There is a third option, which is to use Dupont-style wires:

In my build it made sense for me to modify the 7-segment displays, and for that reason I will focus on that method.

With all 7-segment displays prepared and modified like this, we can assemble the circuit!

  • Step 1

    Place four 170-pin breadboards in front of you and connect them together :)

  • Step 2

    Insert eight CD4094 shift register ICs, and make sure their notches point to the left.

    The shift registers are labeled from right to left, just like in the schematic above. The one on the right is register 1, then comes register 2, and so on, until register number 8 on the very left.

  • Step 3

    Connect the QS* outputs of the registers to the clock input of the cascaded shift registers.

    In detail: connect pin 10 (QS*) of IC1 to pin 2 (clock) of IC2, pin 10 (QS*) of IC2 to pin 2 (clock) of IC3, pin 10 (QS*) of IC3 to pin 2 (clock) of IC4, pin 10 (QS*) of IC4 to pin 2 (clock) of IC5, pin 10 (QS*) of IC5 to pin 2 (clock) of IC6, pin 10 (QS*) of IC6 to pin 2 (clock) of IC7, and pin 10 (QS*) of IC7 to pin 2 (clock) of IC8.

  • Step 4

    Connect all VDD pins (pin 16) together. In this layout there are no power rails, so you have to connect all pins manually. If you use AWG 24/0.6mm wire, then two wires fit into one connection hole on the breadboard, and that makes the construction more compact.

  • Step 5

    Connect each OE pin (output enable), pin 15, to VDD. This makes sure that each register's output is turned on.

  • Step 6

    Plug in the 7-segment displays over each shift register. Make sure that the resistor connects to pin 8 of each IC (that is where ground is located), and make sure that the pins of the 7-segment displays align properly with the outputs Q1-Q8.

    Important: Follow the special preparation of the 7-segment display as we described it above. If you don't want to solder, you can also use Dupont wires to connect the 7-segment display to the shift register.

    For completeness, the connections are like this: segment a is connected to output Q7 (pin 12), segment b is connected to output Q8 (pin 11), segment c is connected to output Q3 (pin 6), segment d is connected to output Q2 (pin 5), segment e is connected to output Q1 (pin 4), segment f is connected to output Q6 (pin 13), segment g is connected to output Q5 (pin 14), the decimal point is connected to output Q4 (pin 7).

  • Step 7

    Connect all strobe inputs (pin 1) together.

  • Step 8

    Connect all clock inputs (pin 3) together.

  • Step 9

    Last, connect all ground pins (pin 7) to form the ground rail. The way we plugged in the 7-segment displays in Step 6 the resistors should also be connected to the ground rail automatically when you do this.

Nice! But we are not done yet, we still need to do the following things:

  • Connect the battery to switch S1.
  • Connect the pushbutton S2 to the PIC16F1455.
  • Connect the potentiometer to the PIC16F1455.
  • Connect the ICSP interface (JP1) to PIC16F1455.
  • Connect the shift register wires (data, clock, and strobe) to the PIC16F1455.

But for now let's focus on putting what we have so far into the enclosure. Then we will connect everything else as we just described.

The housing

Every project is different, and I always like to browse dollar stores for nice possible enclosures. For this project I decided to use a simple hinged pencil box like this one:

As you can see, the 7-segment circuits fit in perfectly:

Here you can see the pencil box from the side. It is a bit asymmetrical:

I decided to remove the lid, and place it on the back of the box to cover parts of the circuits. More on that later! Here you can see the PIC15F1455 breadboard, the battery compartment, the potentiometer, pushbutton, and the power switch all laid out. It fits nicely!

Then I simply cut into the very thin and soft wood with an X-Acto knife to make some space for the breadboard and the battery compartment.

Then I drilled some holes (switch: 6mm, pushbutton and potentiometer: 7mm) to mount the switch, pushbutton, and potentiometer.

Let's hope it all fits!

The six-pole pin header for the ICSP interface needs a small slot as well (about 3mm × 15mm):

I ended up drilling a bunch of 2mm holes close to each other, and then used a file to widen the slot until everything fit well.

I also used copious amounts of hot glue to act as a strain relief for the wires.

I also drilled a hole into the back of the housing so that we can connect VDD, ground, data, clock, and strobe to the 7-segment display board. I used this color-coding: VDD=red, ground=black, data=yellow, clock=green, strobe=blue.

And now we have to connect these five wires. This is very simple: the data cable (yellow) goes to pin 2 of the rightmost shift register. All other wires are even more easily connected because in the assembly steps 4, 7, 8, and 9 we already connected VDD, strobe, clock, and GND all in parallel. So you can connect these wires anywhere you want to these. For that reason I also used the same color-coding.

Of course the other end still pokes out of the back:

Let's connect these wires to the microcontroller!

  • Step 10

    Place the PIC16F1455 in the breadboard, and make sure the notch of the PIC16F1455 points up.

  • Step 11

    Connect strobe (the blue wire) to pin 5 (RC5) of the PIC16F1455.

  • Step 12

    Connect clock (the green wire) to pin 6 (RC4) of the PIC16F1455.

  • Step 13

    Connect data (the yellow wire) to pin 7 (RC3) of the PIC16F1455.

  • Step 14

    Connect VDD (the red wire) to pin 1.

  • Step 15

    Connect ground (the black wire) to pin 14.

  • Step 16

    The pushbutton is connected between pin 2 (RA5) and ground. I drew these connections in red in the picture.

  • Step 17

    The potentiometer's center pin (green wire) is connected to pin 8 (RC2) of the PIC16F1455. The remaining two wires are connected to VDD and ground.

    It does not matter which of the remaining wires you connect to VDD and which one to ground, it will just switch the direction in which you have to turn the potentiometer to increase the scrolling animation speed.

  • Step 18

    Now you can connect the battery compartment. Pin 1 (VDD) goes to the switch S1, the other wire of the switch S1 goes to the positive terminal of the battery pack. The negative terminal of the battery pack is permanently connected to pin 14.

Now we can close the back panel and glue it in place:

  • Step 19

    Last, wire up the six-pole the ICSP connector. Master clear (MCLR, yellow) goes to pin 4, VDD (red) goes to pin 1, GND (ground, black) goes to pin 14, PGD (programming data, orange) goes to pin 10, and PGC (programming clock, green) goes to pin 9.

    Important: The connections on the pin header that plugs into the PICkit3 are not in the same sequence as arranged in the top of this picture. More on that below.

And that was the last wires! Here is how they connect to the pin header:

Now we are done, but I decided to add a red filter to increase the readability of the display. As it turns out, a cheap plastic file folder had the perfect amount of translucence!

I carefully cut out a piece with the X-Acto knife:

Remember the screws and hinges from earlier?

I decided to use four of these screws to hold the filter in place. In my opinion this is better than gluing the filter in place, because if I ever need to get to the electronics: I can :)

Yeah, that fits nicely!

Last, I added two rubber feet to the front so that the box stands at a small angle. This increases readability :)

And that's it! Now you can enjoy your scrolling text display!

Program custom texts

But wait! We have not yet programmed the controller! In order to flash the .hex file onto the controller (read more about it in the source code part of this article) we need to connect the PICkit3 to our scrolling text display. But because of the ICSP interface this is super easy:

I drew a small triangle next to the ICSP connector that marks the master clear pin. When plugging in the PICkit3, make sure that this triangle aligns with the white triangle printed on the PICkit3:

Now you can create a new project in the MPLAB X IDE for the PIC16F1455, load the main.c source code, and enter the text that you like. You can add or remove lines, and in total there can be around 6000 characters.

// define your texts
// (can be as many as you want, total length is around 6000 characters)
char *myTexts[] = {"GREETINGS, NICE PEOPLE OF THE INTERNET!    ",
                   "HELLO THERE.  GENERAL KENOBI, YOU ARE A BOLD ONE!    ",
                   "WHAT IS THE ANSWER TO THE ULTIMATE QUESTION OF LIFE, THE UNIVERSE, AND EVERYTHING?     ",
                   "42.     ",
                   "THANK YOU SO MUCH FOR WATCHING, AND I WILL SEE YOU NEXT TIME!            "};

After you are happy with your text messages, compile the project and then use MPLAB IPE to flash the .hex file onto the PIC16F1455. You can read all about how to do that in our introductory article on how to flash microcontrollers.

Alternatively, if you don't want to compile your own .hex file: I included the two files lorem-ipsum.hex and pi6000.hex which display a few thousand characters of the Lorem Ipsum blind text or the first 6000 digits of the letter π, because why not.

Important: The scrolling text display needs to be turned on when you want to program the new text onto it, so make sure that is the case :)

YouTube video

I covered this entire tutorial in a dedicated YouTube video:

Final thoughts

And here is the finished display:

I think a filter is definitely a good idea:

If you have made it this far: thanks so much for reading! This has been a fun project for me, and it took a bit longer than I originally anticipated. The housing is a bit too small for all the wires, so if you want to build it yourself I might recommend a slightly larger enclosure.

Other than that, I believe this is a fascinating and rewarding project for anybody who wants to learn about microcontrollers. Please let me know if you found this article helpful, and what I can improve or explain better.

Thank you very much for your interest, and I will see you around!

Appendix: The full source code

Here you can find the full source code. The code (as well as the .hex file) are also available for download in the resources box.

/*
 * File:   main.c
 * Author: boos
 *
 * Created on February 21, 2020, 7:49 PM
 */

// CONFIG1
#pragma config FOSC = INTOSC    // Oscillator Selection Bits (INTOSC oscillator: I/O function on CLKIN pin)
#pragma config WDTE = OFF       // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable (PWRT disabled)
#pragma config MCLRE = OFF      // MCLR Pin Function Select (MCLR/VPP pin function is digital input)
#pragma config CP = OFF         // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config BOREN = OFF      // Brown-out Reset Enable (Brown-out Reset disabled)
#pragma config CLKOUTEN = OFF   // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF       // Internal/External Switchover Mode (Internal/External Switchover Mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)

// CONFIG2
#pragma config WRT = OFF        // Flash Memory Self-Write Protection (Write protection off)
#pragma config CPUDIV = NOCLKDIV// CPU System Clock Selection Bit (NO CPU system divide)
#pragma config USBLSCLK = 48MHz // USB Low SPeed Clock Selection bit (System clock expects 48 MHz, FS/LS USB CLKENs divide-by is set to 8.)
#pragma config PLLMULT = 3x     // PLL Multipler Selection Bit (3x Output Frequency Selected)
#pragma config PLLEN = ENABLED  // PLL Enable Bit (3x or 4x PLL Enabled)
#pragma config STVREN = ON      // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will cause a Reset)
#pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LPBOR = OFF      // Low-Power Brown Out Reset (Low-Power BOR is disabled)
#pragma config LVP = OFF        // Low-Voltage Programming Enable (Low-voltage programming disabled)

#include <xc.h>

// define your texts
// (can be as many as you want, total length is around 6000 characters)
char *myTexts[] = {"GREETINGS, NICE PEOPLE OF THE INTERNET!    ",
                   "HELLO THERE.  GENERAL KENOBI, YOU ARE A BOLD ONE!    ",
                   "WHAT IS THE ANSWER TO THE ULTIMATE QUESTION OF LIFE, THE UNIVERSE, AND EVERYTHING?     ",
                   "42.     ",
                   "THANK YOU SO MUCH FOR WATCHING, AND I WILL SEE YOU NEXT TIME!            "};

// how are your segments connected to the shift register?
// map        a  b  c  d  e  f  g  .
char map[] = {7, 8, 3, 2, 1, 6, 5, 4};

// declare functions
void clearDisplay (int digits);
unsigned char convertCharacterToPattern (char ASCII);
void sendValue (unsigned char value);

// define locations of shift register controls
#define STROBE RC5
#define DATA RC4
#define CLOCK RC3
#define NEXT !RA5

// variables
int text_index = 0, char_index = 0, adc_value, TIME_BUFFER, NEXT_BUFFER;

// main function
void main (void) {
    
    // set internal oscillator to 4MHz
    IRCF0 = 1;
    IRCF1 = 0;
    IRCF2 = 1;
    IRCF3 = 1;
    
    // weak pull-up for button
    TRISA5 = 1;
    WPUA5 = 1;
    nWPUEN = 0;
    
    // ADC settings
    
	// ADC sampling frequency per bit is F_osc/16
	ADCS0 = 1;
	ADCS1 = 0;
    ADCS2 = 1;
    
    // result alignment
    ADFM = 0;
    
    // RC2 as analog input
	TRISC2 = 1;
    ANSC2 = 1;
    
    // select channel AN6 (which corresponds to RC2)
    CHS0 = 0;
    CHS1 = 1;
    CHS2 = 1;
    CHS3 = 0;
    CHS4 = 0;
    
    // turn the ADC on
	ADON = 1;
    
    // set outputs and disable analog features on pin RC3
    TRISC5 = 0;
    TRISC4 = 0;
    TRISC3 = 0;
    ANSC3 = 0;
    
    // TIMER0 settings
    
	// internal clock, no prescaler, interrupt on overflow
	TMR0CS = 0;
	PSA = 1;
	TMR0IE = 1;

	// enable global interrupts
	GIE = 1;
    
    //  begin of main program
    
    // clear all eight registers
    clearDisplay(8);
    
    // main loop
    while (1) {
        
        // get potentiometer position
        GO = 1; while (GO);
        adc_value = ADRESH;
        
        // has the "NEXT" button been pressed?
        if ((NEXT) && (NEXT_BUFFER == 0)) {
            
            // remove old text
            clearDisplay(8);
            
            // reset everything to display new text
            TIME_BUFFER = 0;
            char_index = 0;
            
            // advance to the next line of text, and go back
            // to the first one if we have arrived at the end
            text_index++;
            if (text_index >= sizeof(myTexts)/sizeof(myTexts[0])) {
                text_index = 0;
            }
            
            // reset debounce variable
            NEXT_BUFFER = 50;
            
        }
        
    }
    
    return;
    
}

// the interrupt service routine
void __interrupt () isr (void) {   

    // this is called abound 3900 times per second
	if (TMR0IF) {
        
        // increase the time buffer by 1
        TIME_BUFFER++;
        
        // have we waited long enough for the text to
        // advance to the next letter?
        if (TIME_BUFFER > 4*adc_value + 100) {
            
            // reset the timing buffer variable
            TIME_BUFFER = 0;
            
            // pipe out the next text character to the shift registers
            sendValue(convertCharacterToPattern(myTexts[text_index][char_index]));
            STROBE = 1;
            STROBE = 0;
            
            // increase the character index,
            // and reset to 0 if we have reached the end
            char_index++;
            if (myTexts[text_index][char_index] == 0) {
                char_index = 0;
            }
            
        }
        
        // debounce the "NEXT" button
        if ((!NEXT) && (NEXT_BUFFER > 0)) {
            NEXT_BUFFER -= 1;
        }

		// reset interrupt flag
		TMR0IF = 0;

	}
    
}

// This function converts an ASCII character into its corresponding
// 7-segment symbol. You can modify this function to create your own
// custom 7-segment font.
unsigned char convertCharacterToPattern (char ASCII) {
    
    // what is the character?
    switch (ASCII) {
        
        // numbers         abcdefg.
        case '0': return 0b11111100;
        case '1': return 0b01100000;
        case '2': return 0b11011010;
        case '3': return 0b11110010;
        case '4': return 0b01100110;
        case '5': return 0b10110110;
        case '6': return 0b10111110;
        case '7': return 0b11100000;
        case '8': return 0b11111110;
        case '9': return 0b11110110;
        
        // letters         abcdefg.
        case 'a': return 0b11101110;
        case 'A': return 0b11101110;
        case 'b': return 0b00111110;
        case 'B': return 0b00111110;
        case 'c': return 0b00011010;
        case 'C': return 0b10011100;
        case 'd': return 0b01111010;
        case 'D': return 0b01111010;
        case 'e': return 0b10011110;
        case 'E': return 0b10011110;
        case 'f': return 0b10001110;
        case 'F': return 0b10001110;
        case 'g': return 0b10111100;
        case 'G': return 0b10111100;
        case 'h': return 0b00101110;
        case 'H': return 0b01101110;
        case 'i': return 0b00100000;
        case 'I': return 0b01100000;
        case 'j': return 0b01111000;
        case 'J': return 0b11111000;
        case 'k': return 0b10101110;
        case 'K': return 0b10101110;
        case 'l': return 0b00111100;
        case 'L': return 0b00011100;
        case 'm': return 0b10101000;
        case 'M': return 0b10101000;
        case 'n': return 0b00101010;
        case 'N': return 0b11101100;      
        case 'o': return 0b00111010;
        case 'O': return 0b11111100;
        case 'p': return 0b11001110;
        case 'P': return 0b11001110;
        case 'q': return 0b11100110;
        case 'Q': return 0b11100110;
        case 'r': return 0b00001010;
        case 'R': return 0b00001010;
        case 's': return 0b10110110;
        case 'S': return 0b10110110;
        case 't': return 0b00011110;
        case 'T': return 0b00011110;
        case 'u': return 0b00111000;
        case 'U': return 0b01111100;
        case 'v': return 0b00111000;
        case 'V': return 0b01111100;
        case 'w': return 0b01010100;
        case 'W': return 0b01010100;
        case 'x': return 0b01101110;
        case 'X': return 0b01101110;
        case 'y': return 0b01100110;
        case 'Y': return 0b01110110;
        case 'z': return 0b11011010;
        case 'Z': return 0b11011010;
        
        // symbols          abcdefg.
        case '=':  return 0b00010010;
        case '-':  return 0b00000010;
        case '_':  return 0b00010000;
        case 248:  return 0b11000110; // degree sign
        case '\'': return 0b01000000;
        case '"':  return 0b01000100;
        case '(':  return 0b10011100;
        case '[':  return 0b10011100;
        case '{':  return 0b10011100;
        case ')':  return 0b11110000;
        case ']':  return 0b11110000;
        case '}':  return 0b11110000;
        case '.':  return 0b00000001;
        case ',':  return 0b00100000;
        case ';':  return 0b00100001;
        case '!':  return 0b01000001;
        case '?':  return 0b11001011;
        case ' ':  return 0b00000000;
        
        // default symbol is an empty space
        default: return 0b00000000;
        
    }
    
}

// This function that an eight-bit value to
// the CD4094 shift registers
void sendValue (unsigned char value) {

    // auxiliary index variable
	int n = 0;
    
    // loop over all eight bits
	for (n = 0; n < 8; n++) {
        
        // Map the n-th bit to its corresponding
        // position in the shift register. This is
        // controlled by the array map[].
        DATA = (value >> (map[n]-1)) & 1;
		
        // pulse the clock pin
        CLOCK = 1;
		CLOCK = 0;
        
	}

}

// This function clears the display by sending out zeros.
void clearDisplay (int digits) {
   
    // send zeros
    DATA = 0;
    
    // send eight zeros for each digit
    for (int tmp=0; tmp<8*digits; tmp++) {
        CLOCK = 1;
        CLOCK = 0;
    }
    
    // update the registers
    STROBE = 1;
    STROBE = 0;
    
}

About FriendlyWire

Beginner-friendly electronics tutorials and projects. Discover the joy of electronics! Keep reading.

Components Needed

5*×170-pin breadboard (link)
1×3×AAA 4.5V battery compartment (link)
3×AAA 1.5V battery
1×PIC16F1455 microcontroller (link)
8*×common cathode 7-segment LED display (kit)
8*×220Ω resistor (standard 1/4W) (link, kit)
8*×CD4094 shift register (link)
1×switch (kit)
1×pushbutton (kit)
1×4.7kΩ potentiometer (link, kit)
1×jumper wire (or AWG 24/0.6mm single-stranded wire) (link 1, link 2)

*In this article we build a scrolling text display with eight digits. But nobody stops you from building one with only four digits, or one with 256 digits :)

Click on the items to learn more.

Optional Components

1×potentiometer knob
1×wooden pencil box
1×red filter
2×rubber feet

Tools Needed

1×PICkit3 (link, read more here)
1×6-pole pin header (link)
1×pliers and side cutters
1×drill with 3mm, 6mm, and 7mm drill bits

Let's build a community

How did you get interested in electronics? What do you want to learn? Connect and share your story!

Tag Cloud

  • scrolling text display
  • PIC16F1455
  • CD4094
  • shift register cascade
  • PICkit3
  • In-Circuit Serial Programming
  • ICSP
  • step-by-step
  • beginner-friendly
  • project