EMBEDDED TECHNOLOGY'" SERIES
CD-ROM
INCLUDED
CONTAINING
•
C code
■
examples
FREE
C90
Compiln
'
Programming 16-bit
Microcontrollers in C
Learning to Fly the PIC 24
Lucio Di Jasio
Newnes
Programming 16-Bit PIC Microcontrollers in C
Programming 16-Bit PIC Microcontrollers in C
Learning to Fly the PIC24
By
Lucio Di Jasio
ELSEVIER
AMSTERDAM • BOSTON • HEIDELBERG • LONDON
NEW YORK • OXFORD • PARIS • SAN DIEGO
SAN FRANCISCO • SINGAPORE • SYDNEY • TOKYO
Newnes is an imprint of Elsevier
Newnes
Newnes is an imprint of Elsevier
30 Corporate Drive, Suite 400, Burlington, MA 01803, USA
Linacre House, Jordan Hill, Oxford OX2 8DP, UK
Copyright © 2007, Elsevier Inc. All rights reserved.
No part of this publication may be reproduced, stored in a retrieval system, or
transmitted in any form or by any means, electronic, mechanical, photocopying,
recording, or otherwise, without the prior written permission of the publisher.
Permissions may be sought directly from Elsevier's Science & Technology Rights
Department in Oxford, UK: phone: (+44) 1865 843830, fax: (+44) 1865 853333,
e-mail: permissions@elsevier.com.uk. You may also complete your request online via
the Elsevier homepage (http://www.elsevier.com), by selecting "Customer Support" and
then "Obtaining Permissions."
f£Z^ Recognizing the importance of preserving what has been written,
^V Elsevier prints its books on acid-free paper whenever possible.
Library of Congress Cataloging-in-Publication Data
(Application submitted.)
British Library Cataloguing-in-Publication Data
A catalogue record for this book is available from the British Library.
ISBN-13: 978-0-7506-8292-3
ISBN-10: 0-7506-8292-2
For information on all Newnes publications
visit our Web site at www.books.elsevier.com
07 08 09 10 10 9 8 7 6 5 4 3 2 1
Printed in the United States of America
Working together to grow
libraries in developing countries
www.elsevier.com www.bookaid.org www.sabre.org
ELSEVIER ESS^^S Sabre Foundation
Dedication
To Sara
Contents
Preface xv
Introduction xvii
Who should read this book? xvii
Structure of the book xviii
What this book is not xix
Checklists xix
PARTI
CHAPTER 1: THE FIRST FLIGHT. 3
Flight plan 3
Pref light checklist 4
The flight 4
Compiling and linking 6
Building the first project 7
PORT initialization 9
Retesting PORTA 10
Testing PORTB 10
Post-flight briefing 12
Notes for assembly experts 13
Notes for PIC MCU experts 14
Notes for C experts 14
Tips and tricks 14
Exercises 15
Books 15
Links 15
CHAPTER 2: A LOOP IN THE PATTERN 17
Flight plan 17
Pref light checklist 17
The flight 18
An animated simulation 21
Using the Logic Analyzer 24
Post-flight briefing 26
Notes for assembly experts 27
VII
Contents
Notes for PIC microcontroller experts 27
Notes for C experts 27
Tips and tricks 27
Exercises 28
Books 28
Links 28
CHAPTER 3: MORE PATTERN WORK, MORE LOOPS 29
Flight plan 29
Pref light checklist 30
The flight 30
Do Loops 30
Variable declarations 31
for loops 31
More loop examples 32
Arrays 33
A new demo 34
Testing with the Logic Analyzer 36
Using the Explorerl6 demonstration board 37
Post-flight briefing 37
Notes for assembly experts 37
Notes for PIC microcontroller experts 38
Notes for C experts 38
Tips and tricks 38
Exercises 40
Books 40
Links 40
CHAPTER 4: NUMB3RS 41
Flight plan 41
Pref light checklist 42
The flight 42
On optimization (or lack thereof) 43
Testing 44
Going long 44
Note on the multiplication of long integers 45
Long long multiplication 45
Floating point 46
Notes for C experts 46
Measuring performance 47
Post-flight briefing 49
Notes for assembly experts 50
Notes for PIC microcontroller experts 51
Tips and tricks 51
VII
Contents
Math libraries 51
Complex datatypes 51
Exercises 52
Books 52
Links 52
CHAPTER 5: INTERRUPTS 53
Flight plan 53
Pref light checklist 53
The flight 54
Nesting of interrupts 57
Traps 57
A template and an example for Timerl interrupt 58
A real example with Timerl 59
Testing the Timerl interrupt 61
The secondary oscillator 63
The real-time clock calendar (RTCC) 64
Managing multiple interrupts 64
Post-flight briefing 65
Notes for C experts 65
Notes for assembly experts 65
Notes for PIC microcontroller experts 66
Tips and tricks 66
Exercises 68
Books 68
Links 68
CHAPTER 6: TAKING A LOOK UNDER THE HOOD 69
Flight plan 69
Pref light checklist 69
The flight 70
Memory space allocation 71
Program space visibility 72
Investigating memory allocation 73
Looking at the MAP 77
Pointers 79
The heap 80
MPLAB C30 Memory Models 81
Post-flight brief ing 81
Notes for C experts 81
Notes for assembly experts 82
Notes for PIC microcontroller experts 82
Tips and tricks 82
Exercises 83
IX
Contents
Books 83
Links 83
PART II - FLYING "SOLO"
CHAPTER 7: COMMUNICATION 89
Flight plan 89
Pref light checklist 89
The flight 90
Synchronous serial interfaces 90
Asynchronous serial interfaces 91
Parallel interfaces 92
Synchronous communication using the SPI modules 93
Testing the Read Status Register command 95
Writing to the EEPROM 98
Reading the memory contents 99
A nonvolatile storage library 99
Testing the new NVM library 102
Post-flight brief ing 104
Notes for C experts 104
Notes for the experts 104
Notes for PIC microcontroller experts 705
Tips and tricks 705
Exercises 707
Books 707
Links 707
CHAPTER 8: ASYNCHRONOUS COMMUNICATION 709
Flight plan 709
Pref light checklist 709
The flight 770
UART configuration 777
Sending and receiving data 113
Testing the serial communication routines 1 14
Building a simple console library 116
Testing a VT 100 terminal 118
Using the serial port as a debugging tool 120
The matrix 120
Post-flight brief ing 722
Notes for C experts 725
Notes for PIC microcontroller experts 724
Tips and tricks 724
About the ICD2 and UARTs on ICE 724
Exercises 725
Contents
Books 125
Links 125
CHAPTER 9: GLASS BLISS 127
Flight plan 128
Pre-f light checklist 128
The flight 128
HD44780 controller compatibility 129
The Parallel Master Port 131
Configuring the PMP for LCD module control 132
A small library of functions to access an LCD display 133
Advanced LCD control 136
Post-flight briefing 138
Notes for C experts 138
Tips and tricks 139
Exercises 140
Books 140
Links 740
CHAPTER 10: IT'S AN ANALOG WORLD 141
Flight plan 141
Pref light checklist 142
The flight 142
The first conversion 144
Automatic sampling timing 145
Developing a demo 146
Developing a game 147
Measuring temperature 149
The breath-alizer game 153
Post-flight briefing 154
Notes for C experts 754
Tips and tricks 755
Exercises 755
Books 755
Links 755
PART III - CROSS-COUNTRY FLYING
CHAPTER 11: CAPTURING INPUTS 767
Flight plan 767
The flight 762
The PS/2 communication protocol 163
Interfacing a PIC24 to the PS/2 163
Input Capture 163
Testing the Input Capture method using Stimulus Scripts 168
XI
Contents
Testing the PS/2 receive routines 172
The simulation 174
The Simulator Profile 175
Another method - Change Notification 176
Evaluating cost 181
A third method - I/O polling 181
Testing the I/O polling method 186
Cost and efficiency of the solution 188
Completing the interface: adding a FIFO buffer 190
Completing the interface: performing key codes decoding 194
Post-flight briefing 197
Tips and tricks 198
Stalling transmissions from the keyboard - Open-Drain Output Control 198
Exercises 199
Books 199
Links 199
CHAPTER 12: THE DARK SCREEN. 201
Flight plan 201
The flight 202
Generating the composite video signal 204
Using the Output Compare modules 208
Memory allocation 211
Image serialization 211
Building the video module 214
Testing the video generator 218
Measuring performance 220
The dark screen 221
A test pattern 222
Plotting 223
A starry night 224
Line drawing 226
Bresenham algorithm 228
Plotting math functions 230
Two-dimensional function visualization 232
Fractals 236
Text 242
Testing the TextOnGPage module 246
Developing a text page video 247
Testing the text page performance 256
Post-flight brief ing 260
Tips and tricks 260
Exercises 261
Books 261
Links 261
XII
Contents
CHAPTER 13: MASS STORAGE 263
Flight plan 263
The flight 264
The SD/MMC card physical interface 264
Interfacing to the Explorerl6 board 265
Starting a new project 266
Selecting the SPI mode of operation 267
Sending commands in SPI mode 267
Completing the SD/MMC card initialization 270
Reading data from an SD/MMC card 271
Writing data to an SD/MMC card 274
Using the SD/MMC interface module 276
Post-flight briefing 280
Tips and tricks 280
Exercises 281
Books 281
Links 281
CHAPTER 14: FILE I/O 283
Flight plan 283
The flight 284
Sectors and Clusters 284
The File Allocation Table (FAT) 285
The Root Directory 286
The treasure hunt 288
Opening a file 296
Reading data from a file 305
Closing a file 308
Creating the fileio module 308
Testing fopenM() and freadM() 311
Writing data to a file 313
Closing a file, second take 317
Accessory functions 319
Testing the complete fileio module 323
Code Size 326
Post-flight briefing 326
Tips and tricks 327
Exercises 327
Books 328
Links 328
CHAPTER 1 5: VOLARE 329
Flight plan 330
The flight 330
XII
Contents
Using the PIC24 OC modules in PWM mode 332
Testing the PWM as a D/A converter 334
Producing analog waveforms 335
Reproducing voice messages 338
A media player 339
The WAVE file format 339
The play () function 341
The low level audio routines 347
Testing the WAVE file player 350
Optimizing the file I/O 353
LED Profiling 353
Looking under the hood for more 356
Post-flight briefing 360
Tips and tricks 360
Exercises 360
Books 361
Links 361
About the Author 363
Index 365
XIV
Preface
Writing this book turned out to be much more work than I had expected and, believe me, I was already
expecting a lot. This project would never have been possible if I did not have 1 10% support and
understanding from my wife, Sara. Special thanks also go to Steve Bowling, a friend, a pilot and an
expert on Microchip 16-bit architecture, for reviewing the technical content of this book and providing
many helpful suggestions for the demonstration projects and hardware experiments. Many thanks go
to Eric Lawson for constantly encouraging me to write and for all the time he spent fixing my eternally
long-running sentences and my bad use of punctuation. I owe big thanks also to Thang Nguyen, who
was first to launch the idea of the book; Joe Drzewiecky and Vince Sheard for patiently listening to my
frequent laments and, always working hard on making MPLAB® IDE a better tool; Calum Wilkie and
Guy McCarthy for quickly addressing all my questions and offering so much help and insight into the
inner workings of the MPLAB C30 compiler and libraries. I would also like to extend my gratitude
to all my friends and colleagues at Microchip Technology and the many embedded-control engineers
I have been honored to work with over the years. You have so profoundly influenced my work and
shaped my experience in the fantastic world of embedded control.
xv
Introduction
The story goes that I badly wanted to write a book about one of the greatest passions in my life: flying!
I wanted to write a book that would convince other engineers like me to take the challenge and live the
dream — learn to fly and become private pilots. However, I knew the few hours of actual flying experi-
ence I had did not qualify me as a credible expert on the art of flying. So when I had an opportunity to
write a book about Microchip's new 16-bit PIC24 microcontrollers, I just could not resist the tempta-
tion to join the two things, programming and flying, in one project. After all, learning to fly means
following a well-structured process — a journey that allows you to acquire new capabilities and push
beyond your limits. It gradually takes you through a number of both theoretical and practical subjects,
and culminates with the delivery of the private pilot license. The pilot license, though, is really just the
beginning of a whole new adventure — a license to learn, as they say. This compares very well to the
process of learning new programming skills, or learning to take advantage of the capabilities of a new
microcontroller architecture.
Throughout the book, I will make brief parallels between the two worlds and in the references for each
chapter I will add, here and there, some suggestions for reading about flying. I hope I will stimulate
your curiosity and, if you happen to have this dream inside you, I will give you that last final push to
help make it happen.
Who should read this book?
This is the part where I am supposed to tell you that you will have a wonderful experience reading this
book, that you will have a lot of fun experimenting with the software and hardware projects, and, that
you will learn about programming a shiny new 16-bit RISC processor in C, practically from scratch.
But, in all honesty, I cannot! This is only partially true. I do hope you will have a lot of fun reading this
book and the experiments are... "playful," and you should enjoy them. However, you will need some
preparation and hard work in order to be able to digest the material I am presenting at a pace that will
accelerate rapidly through the first few chapters.
This book is meant for programmers having a basic to intermediate level of experience, but not for
"absolute" beginners. Don't expect me to start with the basics of binary numbers, hexadecimal notation
or the fundamentals of programming. However, we will briefly review the basics of C programming
as it relates to applications for the latest generation of general-purpose 16-bit microcontrollers, before
moving on to more challenging projects. My assumption is that you, the reader, belong to one of four
categories:
Embedded-control programmer: experienced in assembly-language microcontroller program-
ming, but with only a basic understanding of the C language.
PIC® microcontroller expert: having a basic understanding of the C language.
xvii
Introduction
Student or professional: with some knowledge of C (or C++) programming for PCs.
Other SLF (superior life forms): I know programmers don't like to be classified that easily, so
I created this special category just for you!
Depending on your level and type of experience, you should be able to find something of interest in
every chapter. I worked hard to make sure that every one of them contained both C programming tech-
niques and new hardware-peripheral details. Should you already be familiar with both, feel free to skip
to the experts section at the end of the chapter, or consider the additional exercises, book references
and links for further research/reading.
These are some of the things you will learn:
The structure of an embedded-control C program: loops, loops and more loops
Basic timing and I/O operations
Basic embedded-control multitasking in C, using the PIC24 interrupts
New PIC24 peripherals, in no specific order:
Input Capture
Output Compare
Change Notification
Parallel Master Port
— Asynchronous Serial Communication
Synchronous Serial Communication
Analog-to-Digital Conversion
How to control LCD displays
How to generate video signals
How to generate audio signals
How to access mass-storage media
How to share files on a mass-storage device with a PC
Structure of the book
Similar to a flying course, the book is composed of three parts. The first part contains five small chap-
ters of increasing levels of complexity. In each chapter, we will review one basic hardware peripheral
of the PIC24FJ128GA010 microcontroller and one aspect of the C language, using the MPLAB C30
compiler (Student Version included on the CD-ROM). In each chapter, we will develop at least one
demonstration project. Initially, such projects will require exclusive use of the MPLAB SIM software
simulator (included on the CD-ROM), and no actual hardware will be necessary, although an Explorer
16 demonstration board might be used.
XVIII
Introduction
In the second part of the book, containing five more chapters, an Explorer 16 demonstration board (or
third-party equivalent) will become more critical, as some of the peripherals used will require real
hardware to be properly tested.
The third part of the book contains five larger chapters. Each one of them builds on the lessons learned
in multiple previous chapters, while adding new peripherals to develop projects of greater complexity.
The projects in the third part of the book require the use of the Explorer 16 demonstration board and
basic prototyping-skills, too (yes, you might need to use a soldering iron). If you don't want to or you
don't have access to basic hardware-prototyping tools, an ad hoc expansion board containing all the
circuitry and components necessary to complete all the demonstration projects will be made available
on the companion Web site: http://www.flyingthepic24.com.
All the source code developed in each chapter is also available for immediate use on the companion
CD-ROM.
What this book is not
This book is not a replacement for the PIC24 datasheet, reference manual and programmer's manual
published by Microchip Technology. It is also not a replacement for the MPLAB C30 compiler user's
guide, and all the libraries and related software tools offered by Microchip. Copies are available on the
companion CD-ROM, but I expect you to download the most recent versions of all those documents
and tools from Microchip's Web site {http://www.microchip.com). Familiarize yourself with them
and keep them handy. I will often refer to them throughout the book, and I might present small block
diagrams and other excerpts here and there as necessary. However, my narration cannot replace the in-
formation presented in the official manuals. Should you notice a conflict between my narration and the
official documentation, ALWAYS refer to the latter. Please do send me an email if a conflict arises. I
will appreciate your help and I will publish any corrections and useful hints I receive on the companion
Web site: http://www.flyingthepic24.com.
This book is also not a primer on the C language. Although a review of the language is given through-
out the first few chapters, the reader will find in the references several suggestions on more complete
introductory courses and books on the subject.
Checklists
Pilots, both professional and not, use checklists to perform every single procedure before and during
a flight. This is not because the procedures are too long to be memorized or because pilots suffer from
more memory problems than others. They use checklists because it is proven that the human memory
can fail and that it tends to do so more often when stress is involved. Pilots can perhaps afford fewer
mistakes than other proffessionals, and they value safety above their pride.
There is nothing really dangerous that you as a programmer can do or forget to do while developing
code for the PIC24. Nonetheless, I have prepared a number of simple checklists to help you perform
the most common programming and debugging tasks. Hopefully, they will help you in the early stages,
when learning to use the new PIC24 toolset — even later if you are, like most of us, alternating between
several projects and development environments from different vendors.
XIX
PART
I
CHAPTER
1
The first flight
In This Chapter
Compiling and linking
Building the first project
PORT initialization
► Retesting PORTA
Testing PORTB
The first flight for every student pilot is typically a blur — a sequence of brief but very intense sensa-
tions, including:
The rush of the first take-off, which is performed by the instructor.
The white-knuckled, sweaty grip on the yoke while trying to keep the plane flying straight for
a couple of minutes, after the instructor gives the standard "anybody that can drive a car can
do this" speech.
Acute motion sickness, as the instructor returns for the landing and performs a sickness-in-
ducing maneuver, called the "side slip." where it looks like the runway is coming through the
side window.
For those who are new to the world of embedded programming, this first chapter will be no different.
Flight plan
Every flight should have a purpose, and preparing a flight plan is the best way to start.
This is going to be our first project with the PIC24 16-bit microcontroller and, for some of you, the first
project with the MPLAB® IDE Integrated Development Environment and the MPLAB C30 language
suite. Even if you have never heard of the C language before, you might have heard of the famous
"Hello World!" programming example. If not, let me tell you about it.
Since the very first book on the C language, written by Kernighan and Ritchie several decades ago, ev-
ery decent C-language book has featured an example program containing a single statement to display
the words "Hello World" on the computer screen. Hundreds, if not thousands, of books have respected
this tradition, and I don't want this book to be the exception. However, it will have to be just a little
different. Let's be realistic — we are talking about programming microcontrollers because we want to
design embedded-control applications. While the availability of a monitor screen is a perfectly safe
assumption for any personal computer or workstation, this is definitely not the case in the embedded-
Chapter 1
control world. For our first embedded application, we better stick to a more basic type of output — a
digital I/O pin. In a later and more advanced chapter, we will be able to interface to an LCD display
and/or a terminal connected to a serial port. But by then we will have better things to do than writing
"Hello World!"
Preflight checklist
Each flight is preceded by a preflight inspection — simply a walk around the airplane in which we
check that, among many other things, gas is in the tank and the wings are still attached to the fuselage.
So, let's verify we have all the necessary pieces of equipment ready and installed (from the attached
CD-ROM and/or the latest version available for download from Microchip's web site at http://www.
microchip, com/mplab) :
MPLAB IDE, free Integrated Development Environment
• MPLAB SIM, software simulator
• MPLAB C30, C compiler (free Student Version).
Then, let's follow the "New Project Set-up" checklist to create a new project with the MPLAB IDE:
1. Select "Project— > Project Wizard" to activate the new project wizard, which will guide us auto-
matically through the following steps...
2. Select the PIC24FJ 1 28G AO 1 device, and click Next.
3. Select the MPLAB C30 Compiler Suite and click Next.
4. Create a new folder and name it "Hello"; name the project "Hello Embedded World" and click
Next.
5. Simply click Next to the following dialog box — there is no need to copy any source files from
any previous projects or directories.
6. Click on Finish to complete the Wizard set-up.
For this first time, let's continue with the following additional steps:
7. Open a new editor window.
8. Type the following three comment lines:
//
// Hello Embedded World!
//
9. Select "File-^Save As", to save the file as: "Hello . c " .
10. Select "Project— > Save" to save the project.
The flight
It is time to start writing some code. I can see your trepidation, especially if you have never written any
C code for an embedded-control application before. Our first line of code is going to be:
#include <p24f j 128ga010 .h>
The first flight
This is not yet a proper C statement, but more of a pseudo-instruction for the preprocessor telling the
compiler to read the content of a device-specific file before proceeding any further. The content of the
device-specific \h" file chosen is nothing more than a long list of the names (and sizes) of all the
internal special-function registers (SFRs) of the chosen PIC24 model. If the include file is accurate,
those names reflect exactly those being used in the device datasheet. If any doubt, just open the file
and take a look — it is a simple text file you can open with the MPLAB editor. Here is a segment of the
p2 4f jl2 8ga0l0 .h file where the program counter and a few other special-function registers (SFRs)
are defined:
extern volatile unsigned int PCL attribute (( sfr ));
extern volatile unsigned char PCH attribute (( sfr ) ) ;
extern volatile unsigned char TBLPAG attribute (( sfr )
extern volatile unsigned char PSVPAG attribute (( sfr )
extern volatile unsigned int RCOUNT attribute (( sfr )
extern volatile unsigned int SR attribute (( sfr ));
Going back to our "Hello . c" source file, let's add a couple more lines that will introduce you to the
main ( ) function:
main ( )
{
}
What we have now is already a complete, although still empty and pretty useless, C-language program.
In between those two curly brackets is where we will soon put the first few instructions of our embed-
ded-control application.
Independently of this function position in the file, whether in the first lines on top or the last few lines
in a million-line file, the main ( ) function is the place where the microcontroller (program counter) will
go first at power-up or after each subsequent reset.
One caveat — before entering the main ( ) function, the microcontroller will execute a short initializa-
tion code segment automatically inserted by the linker. This is known as the cO code. The cO code will
perform basic housekeeping chores, including the initialization of the microcontroller stack, among
other things.
We said our mission was to turn on one or more I/O pins: say PORTA, pins RAO-7. In assembly, we
would have used a pair of mov instructions to transfer a literal value to the output port. In C it is much
easier — we can write an "assignment statement" as in the following example:
ttinclude <p24f j 128ga010 .h>
main ( )
{
PORTA = Oxff;
}
Chapter 1
First, notice how each individual statement in C is terminated with a semicolon. Then notice how it
resembles a mathematical equation. . .it is not!
An assignment statement has a right side, which is computed first. A resulting value is obtained (in this
case it was simply a literal constant) and it is then transferred to the left side, which acts as a receiv-
ing container. In this case it was a special-function 16-bit register of the microcontroller (the name of
which was predefined in the .h file).
Note: In C language, by prefixing the literal value with Ox, we indicate the use of the hexadecimal
radix. Otherwise the compiler assumes the default decimal radix. Alternatively, the Ob prefix can
be used for binary literal values, while for historical reasons a single (zero) prefix is used for the
octal notation. (Does anybody use octal anymore?)
Compiling and linking
Now that we have completed the main ( ) and only function of our first C program, how do we trans-
form the source into a binary executable?
Using the MPLAB Integrated Development Environment (IDE), it is very easy! It's a matter of a single
click of your mouse. This operation is called a Project Build. The sequence of events is fairly long and
complex, but it is composed mainly of two steps:
Compiling: The C compiler is invoked and an object code file ( . o) is generated. This file is
not yet a complete executable. While most of the code generation is complete, all the address-
es of functions and variables are still undefined. In fact, this is also called a relocatable code
object. If there are multiple source files, this step is repeated for each one of them.
Linking: The linker is invoked and a proper position in the memory space is found for each
function and each variable. Also any number of precompiler object code files and standard
library functions may be added at this time as required. Among the several output files pro-
duced by the linker is the actual binary executable file (.hex).
All this is performed in a very rapid sequence as soon as you select the option "Build All" from the
Project menu.
Should you prefer a command-line interface, you will be pleased to learn that there are alternative
methods to invoke the compiler and linker and achieve the same results without using the MPAB IDE,
although you will have to refer to the MPLAB C compiler User Guide for instructions. In the remain-
der of this book, we will stick to the MPLAB IDE interface and we will make use of the appropriate
checklists to make it even easier.
In order for MPLAB to know which file(s) need to be compiled, we will need to add their names
(Hello . c in this case) to the project Source Files List.
In order for the linker to assign the correct addresses to each variable and function, we will need to pro-
vide MPLAB with the name of a device-specific "linker script" file ( . gld). Just like the include ( . h)
file tells the compiler about the names (and sizes) of device-specific, special-function registers (SFRs),
the linker scripts ( . gld) file informs the linker about their predefined positions in memory (according
to the device datasheet) as well as provides essential memory space information such as: total amount
of Flash memory available, total amount of RAM memory available, and their address ranges.
The linker script file is a simple text file and it can be opened and inspected using the MPLAB editor.
The first flight
Here is a segment of the p24f j I2 8ga0l0 . gld file where the addresses of the program counter and a
few other special-function registers are defined:
PCL
= 0x2E
PCL
= 0x2 E
PCH
= 0x3
PCH
= 0x3
TBLPAG
= 0x32
TBLPAG
= 0x32
PSVPAG
= 0x34
PSVPAG
= 0x34
RCOUNT
= 0x3 6
RCOUNT
= 0x3 6
SR
= 0x42
SR
= 0x42
Building the first project
Let's review the last few steps required to complete our first demo project:
1. Add the current source file to the "Project Source Files" list.
There are three possible checklists to choose from, corresponding to three different methods
to achieve the same result. This first time we will:
a) Open the Project window, if not already open, selecting "View— > Project".
b) With the cursor on the editor window, right click to activate the editor pop-up menu.
c) Select "Add to project".
2. Add the PIC24 "linker script" file to the Project.
Following the appropriate checklist "Add linker script to project":
a) Right click on the linker scripts list in the project window.
b) Select "Add file," browse and select the "p24f j I28ga0l0 . gld" file found in the
support/gld subdirectory of MPLAB.
Your Project window should now look similar to Figure 1-1.
3. Select the "Project— >Build" function and watch the C30 compiler, followed by the linker,
work and generate the executable code as well as a few, hopefully reassuring, messages in the
MPLAB IDE Build window.
Note: The "Project Build" checklist contains several additional steps that will help you in future
and more complex examples. (See Figure 1-2.)
4. Select "Debugger— > Select Tool— >MPLAB SIM" to select and activate the simulator as our
main debugging tool for this lesson. Note: the "MPLAB SIM debugger set-up" checklist will
help you properly configure the simulator.
If all is well, before trying to run the code, let's also open a Watch window and select and add the
porta special-function register to it (type or select porta in the SFR combo box, and then click on the
"Add SFR" button). (See Figure 1-3.)
Chapter 1
Q Hello Embedded World.mcp
B Source Files
Hello.c
Header Fifes
Object Files
Library Files
Linker Scripts
p24FJ128GA010.gld
Other Files
Figure 1-1. MPLAB IDE Project window set up for the "Hello Embedded World" project.
Version Control Find in F3es MPLAB SIM
Executing: "C:\Program Files\Microchip\MPLAB C30\bin\pic30-gcc.exe" -mcpu=24FJ1 28GA01 -c
Moke: The target "C:\work\C3Q\1 Hello\Hello Embedded World.cof is out of date-
Executing: "C:\Program Files\Microchip\MPLAB C30\bin\pic30-gcc.exe" -WT C:\work\C30\1 Helto\
Executing; "C:\Program Files\Microchip\MPLAB ASM30 Suite\bin\pic30-bin2hex exe w "Hello Embe
Loaded C:\work\C30\1 Hello\Hello Embedded World.cof.
BUILD SUCCEEDED
li
Figure 1-2. MPLAB IDE Output window, Build tab after successfully building a project.
Address
Symbol Name
Value
| Watch
Watch 1 Watch 2
Watch 3
Watch
Figure 1-3. MPLAB IDE Watch window.
Add Filter-in
Add Filter-out Trace
Remove Rite
Rerrn- Iter Traces
Close
Set 8real<point
Breakpoints ►
Run to Cursor
Set PC at Cursor
Figure 1-4. MPLAB IDE Editor context menu (right click).
8
The first flight
5. Hit the simulator Reset button L3y (or select "Debugger— >Reset") and observe the contents
of porta. It should be cleared at reset. Then, place the cursor on the line containing the port
assignment, inside the main function, and select the "Run to Cursor" option on the right-click
menu.
This will let you skip all the C-compiler initialization code (cO) and get right to the beginning of our
code.
TO. {*}
6. Now single-step, (use the Step-Over " or Step-In functions) to execute the one and
only statement in our first program and observe how the content of porta changes in the
Watch window. Or, notice how nothing happens: surprise!
PORT initialization
It is time to hit the books, specifically the PIC24FJ128GA datasheet (Chapter 9, for the I/O ports de-
tail). PORTA is a pretty busy, 16-pin wide, port.
Peripheral Module
Peripheral Input Data
Output Multiplexers
r — — n
Peripheral Module Enable
Peripheral Output Enable
Peripheral Output Data
L________J
PIO Module
ReadTRIS
Data Bus
WRTRIS
WR LAT +
WR Port
Read LAT
D Q
TRIS Latch
D Q
►CK - *
Data Latch
Read Port
I/O
1
i
[Output Enable
Output Data
0,
_ _ J
M
I/O Pin
Input Data
Figure 1 -5. Diagram of a typical PIC24 I/O port.
Looking at the pin-out diagrams on the datasheet, we can tell there are many peripheral modules being
multiplexed on top of each pin. We can also determine what the default direction is for all I/O pins
at reset: they are configured as inputs, which is a standard for all PIC® microcontrollers. The trisa
special-function register controls the direction of each pin on PORTA. Hence, we need to add one more
assignment to our program, to change the direction of all the pins of PORTA to output, if we want to
see their status change:
#include <p24f j 128ga010 .h>
Chapter 1
main ( )
{
TRISA
PORTA
}
0;
Oxff ;
// all PORTA pins output
Retesting PORTA
1 . Rebuild the project now.
2. Set the cursor on the trisa assignment.
3. Execute a "Run to Cursor" command to skip all the compiler initialization just as we did
before.
4. Execute a couple of single steps and. ..we have it!
Address
Symbol Name
Value
02C2
PORTA
QxOQFF
Figure 1-6. MPLAB IDE Watch window detail; PORTA content has changed!
If all went well, you should see the content of porta change to OxOOFF, highlighted in the Watch win-
dow in red. Hello, World!
Our first choice of PORTA was dictated partially by the alphabetical order and partially by the fact that,
on the popular Explorer 16 demonstration boards, PORTA pins RAO through RA7 are conveniently
connected to 8 LEDs. So if you try to execute this example code on the actual demo board, you will
have the satisfaction of seeing all the LEDs turn on, nice and bright.
Testing PORTB
To complete our lesson, we will now explore the use of one more I/O port, PORTB.
It is simple to edit the program and replace the two PORTA control register assignments with trisb
and portb . Rebuild the project and follow the same steps we did in the previous exercise and. . .you'll
get a new surprise. The same code that worked for PORTA does not work for PORTB !
Don't panic! I did it on purpose. I wanted you to experience a little PIC24 migration pain. It will help
you learn and grow stronger.
It is time to go back to the datasheet, and study in more detail the PIC24 pin-out diagrams. There are
two fundamental differences between the 8-bit PIC microcontroller architecture and the new PIC24
architecture:
Most of PORTB pins are multiplexed with the analog inputs of the analog-to-digital converter
(ADC) peripheral. The 8-bit architecture reserved porta pins primarily for this purpose — the
roles of the two ports have been swapped!
10
The first flight
With the PIC24, if a peripheral module input/output signal is multiplexed on an I/O pin, as
soon as the module is enabled, it takes complete control of the I/O pin — independently of the
direction (trisx) control register content. In the 8-bit architectures, it was up to the user to
assign the correct direction to each pin, even when a peripheral module required its use.
By default, pins multiplexed with "analog" inputs are disconnected from their "digitaF'input ports. This
is exactly what was happening in the last example. All PORTB pins in the PIC24FJ128GA010 are, by
default at power-up, assigned an analog input function; therefore, reading PORTB returns all Os. No-
tice, though, that the output latch of PORTB has been correctly set although we cannot see it through
the portb register. To verify it, check the contents of the latb register instead.
To reconnect PORTB inputs to the digital inputs, we have to act on the analog-to-digital conversion
(ADC) module inputs. From the datasheet, we learn that the special-function register adIpcfg controls
the analog/digital assignment of each pin.
Upper Byte
R/W-0
■
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
PCFG15
PCFG14
PCFG13
PCFG12
PCFG11
PCFG10
PCFG9
PCFG8
bit 15
bit 8
Lower Byte
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
PCFG7
PCFG6
PCFG5
PCFG4
PCFG3
PCFG2
PCFG1
PCFGO
bit 7
bitO
bit 15-0 PCFG15:PCFG0: Analog Input Pin Configuration Control bits
1 = Pin for corresponding analog channel is configured in Digital mode; I/O port read enabled
= Pin configured in Analog mode; I/O port read disabled, A/D samples pin voltage
Figure 1-7. adipcfg: ADC port configuration register
Assigning a 1 to each bit in the adipcgf special-function register will accomplish the task. Our new
and complete program example is now:
#include <p24f j 128ga010 .h>
main ( )
(
TRISB = 0;
ADIPCFG = Oxffff;
PORTB = Oxff;
}
This time, compiling and single-stepping through it will give us the desired results.
// all PORTB pins output
// all PORTB pins digital
11
Chapter 1
« MPlAB IDE v7.22 - [MPLAB IDE Editor]
Z2 E** Ed* $ew P/o(eet Debuwer Programmer look Configure Window Hefc
D g£ U 16 a«
d'^BQidi $ M
Irfn
C> II E>0 m Ij* ri*
Checksum: N/A
Ei Hello Embedded World.mc p
B Source Fles
Heto.c
Header Ftes
Object Files
LfcraryFies
B Lr*er Scripts
p24FJI28GA0l0.oU
Other Ftes
Address
STKtooi Net
p24FJ 1 288A01 CPS. h Kefa v1 .c p24f ji 28GA01 0PS.gkt I
*\
liL
Step Over
//
// Hello Errtoedded World
//
// nay first PIC24 program with the KPLAB C30 compiler
//
n include <p2« jl28ga0l0.h>
main
TRISB 0: // all PORTB ft3 output
AD1PCFG ■ Oxffff; // all PORTB as digital
PORTS - Oxif;
J
wiinFa wl MPlAB SIM I
Executing: "C:\pTogrom Files\Microchip\MPLAB C30\bin\pic3tr-gcc.exe" -mcpu-24FJl 28GA0T0 -c-^J
Executing: "C:\Progrom Rles\Microchip\MPLA8 C30\tJin\pic30-gcc.exe" -WL"C\worK\C30M Hello\
Executing: "C:\program Files\Microchip\MPLAB ASM30 Suile\bin\pic30-bin?hexexe" "Hello Embi
Loaded C:\work\e30\1 Hello\Hello Embedded Worid.eof.
BUILD SUCCEEDED
ii
MPUBSM
AddSml lAOCSUFQ -rj AddS^nootl lZsP"
"3
Address
5yn*>ol Nome
Value
02CA
02 ca
02 Cfi
LATB
PORTB
TRISB
QxOOTF
OxOOFF
OXOOOO
Watch 1 Watch 2 Watch 3 Watch 4
.
PIC24FJ129GA0Jpc:0x28e oabsablPO DCnovZC
Hn 14, Colt INS WR
Figure 1 -8. Hello Embedded World Project.
Post-flight briefing
After each flight, there should be a brief review. Sitting on a comfortable chair in front of a cool glass
of water, it's time to reflect with the instructor on what we have learned from this first experience.
Writing a C program for a PIC24 microcontroller can be very simple, or at least no more complicated
than the assembly-language equivalent. Two or three instructions, depending on which port we plan to
use, can give us direct control over the most basic tool available to the microcontroller for communica-
tion with the rest of the world: the I/O pins.
Also, there is nothing the C30 compiler can do to read our mind. Just like in assembly, we are respon-
sible for setting the correct direction of the I/O pins. And we are still required to study the datasheet
and learn about the small differences between the 8-bit PIC microcontrollers we might be familiar with
and the new 16-bit breed.
As high-level as the C programming language is touted to be, writing code for an embedded-control
device still requires us to be intimately familiar with the finest details of the hardware we use.
12
The first flight
Notes for assembly experts
If you have difficulties blindly accepting the validity of the code generated by the MPLAB C30 com-
piler, you might find comfort in knowing that, at any given point in time, you can decide to switch to
the "Disassembly Listing" view. You can quickly inspect the code generated by the compiler, as each C
source line is shown in a comment that precedes the segment of code it generated.
Disassembly Listing
6:
7:
8:
9:
10:
00280
11:
00282
00284
12:
00286
00288
13:
0028A
0028C
14:
00201
00290
FAOOOO
EB0000
831600
F.B8000
881960
200FFO
881610
FA8QQQ
060000
f include -=p24f j!2Sga010 h
main < )
{
Ink #0x0
TRISA = 0;
clr.w 0x0000
moY.w 0x0000, 0x02c0
AD1PCFG = Oxffff;
setm.it 0x0000
tov.w 0x0000, 0x032c
PORTA = Ox£f;
i»v.w fOxff, 0x0000
inov,w 0xO00Q,0xO2c2
3
// all PORTA as output
// all PORTA as digital
ulnk
return
iJJ
A
Figure 1-9. Disassembly Listing Window.
You can even single-step through the code and do all the debugging from this view, although I strongly
encourage you not to do so (or limit the exercise to a few exploratory sessions as we progress through
the first chapters of this book). Satisfy your curiosity, but gradually learn to trust the compiler. Even-
tually, use of the C language will give a boost to your productivity and increase the readability and
maintainability of your code.
As a final exercise, I encourage you to open the Memory Usage Gauge window — select "View^
Memory Usage Gauge".
Memory Usage Gauge
Jnj
PE.:-n,-in u*mci,
Tofcl: t!0»
Data Uwnory
T#toi:8i9:
Figure 1-10. MPLAB IDE Memory Usage Gauge window.
13
Chapter 1
Don't be alarmed! Even though we wrote only three lines of code in our first example and the amount
of program memory used appears to already be up to 300+ bytes, this is not an indication of any inher-
ent inefficiency of the C language. There is a minimum block of code that is always generated (for
our convenience) by the C30 compiler. This is the initialization code (c0 ) that we mentioned briefly
before. We will get to it, in more detail, in the following chapters as we discuss variables initialization,
memory allocation and interrupts.
Notes for PIC MCU experts
Those of you who are familiar with the PIC 16 and PIC 18 architecture will find it interesting that most
PIC24 control registers, including the I/O ports, are now 16 bits wide. Looking at the PIC24 datasheet,
note also how most peripherals have names that look very similar, if not identical, to the 8-bit peripher-
als you are already familiar with. You will feel at home in no time!
Notes for C experts
Certainly we could have used the print f function from the standard C libraries. In fact the librar-
ies are readily available with the MPLAB C30 compiler. But we are targeting embedded-control
applications and we are not writing code for multigigabyte workstations. Get used to manipulating
low-level hardware peripherals inside the PIC24 microcontrollers. A single call to a library function,
like print f , could have added several kilobytes of code to your executable. Don't assume a serial port
and a terminal or a text display will always be available to you. Instead, develop a sensibility for the
"weight" of each function and library you use in light of the limited resources available in the embed-
ded-design world.
Tips and tricks
The PIC24FJ family of microcontrollers is based on a 3V CMOS process with a 2.0V to 3.6V operat-
ing range. As a consequence, a 3V power supply (Vdd) must be used and this limits the output voltage
of each I/O pin when producing a logic high output. However, interfacing to 5 V legacy devices and
applications is really simple:
To drive a 5V output, use the odcx control registers (odca for PORTA, odcb for PORTB and
so on...) to set individual output pins in open-drain mode and connect external pull-up resis-
tors to a 5V power supply.
Digital input pins instead are already capable of tolerating up to 5 V. They can be connected
directly to 5V input signals.
Be careful with I/O pins that are multiplexed with analog inputs though — they cannot tolerate voltages
above Vdd.
14
The first flight
Exercises
If you have the Explorer 16 board:
1. Use the ICD2 Debugging Checklist to help you prepare the project for debugging.
2. To test the PORTA example, connect the Explorer 16 board and check the visual output on
LEDO-7.
3. To test the PORTB example, connect a voltmeter (or DMM) to pin RBO and watch the needle
move as you single-step through the code.
Books
Kernighan, B. and Ritchie, D., "The C Programming Language," Prentice-Hall, Englewood
Cliffs, NJ.
When you read or hear a programmer talk about the "K&R," they mean this book!
Also known as "the white book," the C language has evolved since the first edition of this
book was published in 1978! The second edition (1988) includes the more recent ANSI C
standard definitions of the language, which is closer to the standard the MPLAB C30 compiler
adheres to (ANSI90).
"Private Pilot Manual," Jeppesen Sanderson, Inc., Englewood, CO.
This is "the" reference book for every student pilot. Highly recommended, even if you are just
curious about aviation.
Links
http://en. wikibooks. org/wiki/C_Prog ramming
This is a Wiki-book on C programming. It's convenient if you don't mind doing all your
reading online. Hint: look for the chapter called "A taste of C" to find the omnipresent "Hello
World!" exercise.
15
CHAPTER
2
A loop in the pattern
In This Chapter
while loops
An animated simulation
Using the Logic Analyzer
The "pattern" is a standardized rectangular circuit, where each pilot flies in a loop. Every airport has
a pattern of given (published) altitude and position for each runway. Its purpose is to organize traffic
around the airport and its working is not too dissimilar to how a roundabout works. All airplanes are
supposed to circle in a given direction consistent with the prevailing wind at the moment. They all
fly at the same altitude, so it is easier to visually keep track of each other's position. They all talk on
the radio on the same frequencies, communicating with a tower if there is one, or among one another
with the smaller airports. As a student pilot, you will spend quite some time, especially in the first few
lessons, flying in the pattern with your instructor to practice continuous sequences of landings im-
mediately followed by take-offs (touch-and-goes), refining your newly acquired skills. As a student of
embedded programming, you will have a loop of your own to learn — the main loop.
Flight plan
Embedded-control programs need a framework, similar to the pilots' pattern, so that the flow of code
can be managed. In this lesson, we will review the basics of the loops syntax in C and we'll also take
the opportunity to introduce a new peripheral module: the 16-bit Timer 1. Two new MPLAB® SIM
features will be used for the first time: the 'Animate" mode and the "Logic Analyzer."
Pref light checklist
For this second lesson, we will need the same basic software components installed (from the attached
CD-ROM and/or the latest versions, available for download from Microchip's website) and used be-
fore, including:
MPLAB IDE, Integrated Development Environment
• MPLAB SIM, software simulator
• MPLAB C30 compiler (Student Version)
17
Chapter 2
We will also reuse the "New Project Set-up" checklist to create a new project with the MPLAB IDE:
1. Select "Project^Project Wizard", to start creating a new project.
2. Select the PIC24FJ 1 28G AO 1 device, and click Next.
3. Select the MPLAB C30 compiler suite and click Next.
4. Create a new folder and name it "Loop." name the project "A Loop in the Pattern," and click
Next.
5. There is no need to copy any source files from the previous lessons; click Next once more.
6. Click Finish to complete the Wizard set-up.
This will be followed by the "Adding Linker Script file" checklist, to add the linker script
"p24f j I2 8ga0l0 . gld" to the project. It can typically be found in the MPLAB IDE installation direc-
tory "C: /Program Files/Microchip/", within the subdirectory "MPLAB C30/support/gld/".
After completing the "Create New File and Add to Project" checklist:
7. Open a new editor window.
8. Type the main program header:
//
// A loop in the pattern
//
9. Select "Project— >AddNewFiletoProject", to save the file as: "loop .c" and have it automati-
cally added to the project source files list.
10. Save the project.
The flight
One of the key questions that might have come to mind after working through the previous lesson is
"What happens when all the code in the main ( ) function has been executed?" Well, nothing really hap-
pens, or at least nothing that you would not expect. The device will reset, and the entire program will
execute again... and again.
In fact, the compiler puts a special software reset instruction right after the end of the main ( ) function
code, just to make sure. In embedded control we want the application to run continuously, from the
moment the power switch has been flipped on until the moment it is turned off. So, letting the program
run through entirely, reset and execute again might seem like a convenient way to arrange the applica-
tion so that it keeps repeating as long as there is "juice." This option might work in a few limited cases,
but what you will soon discover is that, running in this "loop." you develop a "limp." Reaching the end
of the program and executing a reset takes the microcontroller back to the very beginning to execute all
the initialization code, including the c0 code segment briefly mentioned in the previous lesson. So, as
short as the initialization part might be, it will make the loop very unbalanced. Going through all the
special-function register and global- variables initializations each time is probably not necessary and it
will certainly slow down the application. A better option is to design an application "main loop." Let's
review the most basic loop-coding options in C first.
18
A Loop in the pattern
while loops
In C there are at least three ways to code a loop; here is the first — the while loop:
while ( x)
{
// your code here. . .
}
Anything you put between those two curly brackets ({ }) will be repeated for as long as the logic ex-
pression in parenthesis ( x) returns a true value. But what is a logic expression in C?
First of all, in C there is no distinction between logic expressions and arithmetic expressions. In C, the
Boolean logic TRUE and FALSE values are represented just as integer numbers with a simple rule:
FALSE is represented by the integer 0.
TRUE is represented by any integer except 0.
So l is true, but so are 13 and -2 7 8. In order to evaluate logic expressions, a number of logic operators
are defined, such as:
the logic OR operator,
ScSc the logic AND operator,
! the logic NOT operator.
These operators consider their operands as logical (Boolean) values using the rule mentioned above,
and they return a logical value. Here are some trivial examples:
(when a = 17 and b = l, or in other words they are both true)
( a | | b ) is true,
( a ScSc b) is true
( ! a ) is false
There are, then, a number of operators that compare numbers (integers of any kind and floating-point
values, too) and return logic values. They are:
== the "equal-to" operator; notice it is composed of two equal signs to distinguish it
from the "assignment" operator we used in the previous lesson,
! = the "NOT-equal to" operator,
> the "greater-than" operator,
>= the "greater-or-equal to" operator,
< the "less-than" operator,
<= the "less-than-or-equal to" operator.
Here are some examples:
assuming a = 10
a > l ) is true
-a >= 0) is false
19
Chapter 2
( a == 17) is false
( a ! = 3 ) is true
Back to the while loop, we said that as long as the expression in parentheses produces a true logic
value (that is any integer value but 0), the program execution will continue around the loop. When the
expression produces a false logic value, the loop will terminate and the execution will continue from
the first instruction after the closing curly bracket.
Note that the expression is evaluated first, before the curly bracket content is executed (if ever), and is
then reevaluated each time.
Here are a few curious loop examples to consider:
While ( 0)
{
// your code here. . .
}
A constant false condition means that the loop will never be executed. This is not very useful. In fact I
believe we have a good candidate for the "world's most useless code" contest!
Here is another example:
while ( 1)
{
// your code here. . .
}
A constant true condition means that the loop will execute forever. This is useful, and is in fact what
we will use for our main program loops from now on. For the sake of readability, a few purists among
you will consider using a more elegant approach, defining a couple of constants:
# define TRUE 1
# define FALSE
and using them consistently in their code, as in:
While ( TRUE)
{
/ / your code here...
}
It is time to add a few new lines of code to the "loop . c" source file now, and put the while loop to
good use.
#include <p24f j 128ga010 .h>
20
A Loop in the pattern
main ( )
{
// init the control registers
TRISA = OxffOO;// PORTA pin 0..7 as output
// application main loop
while ( 1)
{
PORTA = Oxff; // turn pin 0-7 on
PORTA =0; // turn all pin off
}
}
The structure of this example program is essentially the structure of every embedded-control program
written in C. There will always be two main parts:
The initialization, which includes both the device peripherals initialization and variables ini-
tialization, executed only once at the beginning.
The main loop, which contains all the control functions that define the application behavior,
and is executed continuously.
An animated simulation
Use the Project Build checklist to compile and link the "loop . c" program. Also use the "MPLAB SIM
simulator set-up" checklist to prepare the software simulator.
To test the code in this example with the simulator, I recommend you use the "Animate" mode (De-
bugger— > Animate). In this mode, the simulator executes one C program line at a time, pausing for Vi
second after each one to give us the time to observe the immediate results. If you add the porta spe-
cial-function register to the Watch window, you should be able to see its value alternating rhythmically
between Oxff and 0x00.
The speed of execution in Animate mode can be controlled with the "Debug— > Settings" dialog box,
selecting the "Animation/Real Time Updates" tab, and modifying the "Animation Step Time" param-
eter, which by default is set to 500 ms. As you can imagine, the "Animate" mode can be a valuable and
entertaining debugging tool, but it gives you quite a distorted idea of what the actual program execution
timing will be. In practice, if our example code was to be executed on a real hardware target, say an
Explorer 16 demonstration board (where the PIC24 is running at 32 MHz), the LEDs connected to the
PORTA output pins would blink too fast for our eyes to notice. In fact, each LED would be turned on
and off several million times each second.
To slow things down to a point where the LEDs would blink nicely just a couple of times per second,
I propose we use a timer, so that in the process we learn to use one of the key peripherals integrated in
all PIC24 microcontrollers. For this example, we will choose the first timer, Timer 1, of the five timers
available inside the PIC24FJ128GA010. This is one of the most flexible and simple peripheral mod-
ules. All we need to do is take a quick look at the PIC24 datasheet, check the block diagram and the
details of the Timerl control registers, and find the ideal initialization values.
21
Chapter 2
TCKPS1:TCKPS0
SOSCO/
TICK
SOSCI
SOSCEN
Gate
Sync
Tcy
TGATE
M
SetTIIF
-i
t
Q D
Q^CK
1x
01
00
TON
i
Prescaler
1,8,64,256
TGATE
TCS
Reset
Equal
TMR1
Comparator
Sync
TSYNC
PR1
Figure 2-1. 16-bit Timer 1 Module block diagram.
We quickly learn that there are three special-function registers that control most of the Timer 1 func-
tions. They are:
• TMRl, which contains the 16-bit counter value.
T1CON, which controls activation and the operating mode of the timer.
• PRl, which can be used to produce a periodic reset of the timer
(not required here).
We can clear the tmri register to start counting from zero.
TMRl = 0;
Then we can initialize ticon so that the timer will operate in a simple configuration where:
Timer 1 is activated: ton = l
The main MCU clock serves as the source (Fosc/2): tcs =
The prescaler is set to the maximum value (1 :256): tckps = 11
The input gating and synchronization functions are not required, since we use the MCU inter-
nal clock directly as the timer clock: tgate = o , tsync = o
We do not worry about the behavior in IDLE mode: tsidl = (default )
22
A Loop in the pattern
Upper Byte:
R/W-0
U-0
R/W-0
U-0
U-0
U-0
U-0
U-0
TON
—
TSIDL
—
—
—
—
—
bit 15
bit 8
Lower Byte
U-0
■
R/W-0
R/W-0
R/W-0
U-0
R/W-0
R/W-0
U-0
—
TGATE
TCKPS1
TCKPSO
—
TSYNC
TCS
—
bit 7
bitO
Figure 2-2. T1CON: Timer 1 control register.
Once we assemble all the bits in a single 16-bit value to assign to ticon, we get:
T1CON = OblOOOOOOOOOllOOOO;
or, in a more compact hexadecimal notation:
TICON = 0x8030;
Once we are done initializing the timer, we enter a loop where we wait for tmri to reach
the desired value set by the constant delay.
while ( TMRI < DELAY)
(
// wait
}
Assuming a 32-MHz clock will be used, we need to assign quite a large value to DELAY so as to
obtain a delay of about a quarter of a second. In fact the following formula dictates the total delay time
produced by the tmri loop:
Tdelay = (2/Fosc) * 256 * DELAY
With Tdelay = 256 ms and resolving for delay, we obtain the value 16,000:
# define DELAY 16000
By putting two such delay loops in front of each porta assignment inside the main loop, we get our
latest and best code example:
#include <p24f j 128ga010 .h>
# define DELAY
16000
main ( )
{
// init the control registers
TRISA = OxffOO; // PORTA pin 0..7 as output
TICON = 0x8030; // TMRI on, prescaler 1:256 Tclk/2
// main application loop
while ( 1)
{
23
Chapter 2
I / 1. turn pin 0-7 on and wait for X A second
PORTA = Oxff;
TMR1 =0; // restart the count
while ( TMR1 < DELAY)
{
// just wait
}
// 2. turn all pin off and wait for X A second
PORTA = 0x0 0;
TMR1 =0; // restart the count
while ( TMR1 < DELAY)
{
// just wait
}
} // main loop
} // main
Note: When programming in C, the number of opening and closing curly brackets tends to in-
crease rapidly as your code grows. After a very short while, even if you stick religiously to the best
indentation rules, it can become difficult to remember which closing curly brackets belong to which
opening curly brackets. By putting little reminders (comments) on the closing brackets, I try to
make it easier and more readable.
It is time now to build the complete project and verify that it is working. If you have an Explorer 16
demonstration board available, you may try to run the code right away. The LEDs should flash at a
comfortably slow pace, with a frequency of about two flashes per second.
If you try to run the same code with the MPLAB SIM simulator, though, you will discover that things
are now way too slow. I don't know how fast your PC is, but on mine MPLAB-SIM cannot get any-
where close to the execution speed of a true 32-MHz PIC24 microcontroller.
If you use the Animate mode, things get even worse. As we saw before, the animation adds a further
delay of about half a second between the execution of each individual line of code. So, for pure debug-
ging purposes, on the simulator feel free to change the delay constant to a much smaller value (16, for
example).
Using the Logic Analyzer
To complete this lesson and make things more entertaining, after building the project, I suggest we play
with a new simulation tool: the MPLAB logic analyzer.
The logic analyzer gives you a graphical and extremely effective view of the recorded values for any
number of the device output pins, but it requires a little care in the initial set-up.
Before anything else, you should make sure that the Tracing function of the simulator is turned on.
24
A Loop in the pattern
l.
2.
3.
Select the "Debug^Settings" dialog box and then choose the Osc/Trace tab.
In the Tracing options section, check the Trace All box.
Now you can open the Analyzer window, from the "View— ^Simulator" Logic Analyzer menu.
Logic Analyzer
Trigger Position
Start*? Center C EndT
Tnggei PL =
Now Clear |
Time Bas e Mode
|Cyc *|
5 mple
Channels
+:*! ®M dM NHlalal
"i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — i — r
2910000.0 292O000.0 2930000.0 2940000.0 2950000.0 2960000.0
Figure 2-3. Logic Analyzer window.
4. Then click on the channel button, to bring up the channel-selection dialog box.
Configure Channels
Available Signals S elected S ignal(s)
lAII ^J Configure Bus(s]
xl
Figure 2-4. Channel Selection Dialog Box.
25
Chapter 2
5. From here, you can select the device output pins you would like to visualize.
In our case, select RAO and click "Add =>".
6. Click on OK to close the channel-selection dialog box.
Note: For future reference, all the steps above are listed in the "Logic Analyzer Set-up" checklist.
. The Logic Analyzer window
Run the code for a short while and then hit the Halt button
should display a neat square- wave plot, as in Figure 2-5.
AV N PI All 1I)F vV.-jn
Fie Ed* «e w Project D ebugger Programmer Tools Configure Window Help
■mm
Nti? D q? q ® $
E> II N> 1H IF !
Checksum: N/A
. _|A loop in the pattemmcp
E3 L_l Source files
^\ lOOp.C
□ Header Ffc*
_| object files
LJ library Fie
H J l»*er Scripts
Q p24f JI28GAQ10.g!d
Cj Other Ftl«
JjJ
_J F*es ''7 Symbols
MPWH lOt Ed- i
-1". x
Loopc
I
...
PORTA - 0:
THIU - 0;
while | TMJtl
t
1
I // main loop
// main
MLAY.
1
J
K
±T
^J
logic ArtAlytvr
Jfl| si
" Titggar Posfon
SlartG" Dertarr End^
TiiogeiPC"
1
J
TirrteB*
« Mode
|Sir<*s
How |
Ceo- |
l<*d
Channels ]
+P& ^ H*l« %OMQj
r
RAO
l t l i i r r r ■ i 3 i
24150000 2420000.0
i i 1 i i r r f !
2425000.0 24300000
i i i l r
2435000.0
r r r i
24400000
i i 1 l l
2445000.0
i i
V«MonCortw3t| Fwj in Files | MPLA8SIMJ
Executing. 'C.\Progrem Frle*\M'Crochip\MPLAB C3O\bin\prc30-gct exe" -mcpu-24FJl 28GA^J
Executing: "CAPragrem Files\M«crochip\MPLAB C3O\bin\prc30-gcc.exe" -Wl"C:\work\C3Cl\2
Executing: 'C.\Progrom Ft1esVMicmchip\MPLAB C30\bin\p.c30-bin2hexexe' "A loop in the p
Loaded C:\woik\C30\? Loop\A loop in the paltem.cof.
BUILD SUCCEEDED
MFvABStM
PIC2+FJ323GA01O pc:QxZa4 ;oab sab IPO fk N OV J «
A-diSFR| [AD1CHS -| Add Symbd| |_SP
H
Address
Symbol Kc
Valtie
02 02 PORTA
02 CO TRISA
0100 TKftl
0x0000
QxOOQT
Figure 2-5. Logic Analyzer window showing square-wave plot.
Post-flight briefing
In this brief lesson, we learned about the way the MPLAB C30 compiler deals with program termina-
tion. For the first time, we gave our little project a bit of structure — separating the main ( ) function in
an initialization section and an infinite loop. To do so, we learned about the while loop statements and
we took the opportunity to touch briefly on the subject of logical expressions evaluation. We closed the
lesson with a final example, where we used a timer module for the first time and we played with the
Logic Analyzer window to plot the RAO pin output.
We will return to all these elements, so don't worry if you have more doubts now than when we
started — this is all part of the learning experience.
26
A Loop in the pattern
Notes for assembly experts
Logic expressions in C can be tricky for the assembly programmer who is used to dealing with binary
operators of identical names (AND, OR, NOT. . .). C has a set of binary operators too, but I purposely
avoided showing them in this lesson to avoid mixing things up. Binary logic operators take pairs of bits
from each operand and compute the result according to the defined truth table. Logic operators, on the
other hand, look at each operand (independently of the number of bits used) as a single Boolean value.
See the following examples of byte-sized operands:
11110101 11110101 (TRUE)
binary OR 00001000 logical OR 00001000 (TRUE)
gives 11111101 gives 00000001 (TRUE)
Notes for PIC microcontroller experts
I am sure you noticed: TimerO has disappeared! The good news is: you are not going to miss it! In
fact, the remaining five timers of a PIC24 are so loaded with features that there is no functionality of
TimerO that you are going to feel nostalgic about. All of the special-function registers that control the
timers have names similar to the ones used on PIC 16 and PIC 18 microcontrollers, and are pretty much
identical in structure. Still, keep an eye on the datasheet; the designers managed to cram in several new
features, including:
All timers are now 16 bits wide.
Each timer has a 16-bit period register.
A new 32-bit mode timer-pairing mechanism is available for Timer2/3 and Timer4/5.
A new external clock gating feature has been added on Timer 1.
Notes for C experts
If you are used to programming in C on a personal computer or workstation, you expect that, upon
termination of the main ( ) function, control would be returned to the operating system. While several
real-time operating systems (RTOSs) are available for the PIC24, a large number of applications don't
need and won't use one. This is certainly true for all the simple examples in this book. By default, the
C30 compiler assumes there is no operating system to return control to, and does the safest possible
thing — it resets.
Tips and tricks
Some embedded applications are designed to run their main loop for months or years in a row without
ever being turned off or receiving a reset command. But the control registers of a microcontroller are
simple RAM memory cells. The probability that a power- supply fluctuation (undetected by the brown-
out reset circuit), an electromagnetic pulse emitted by some noisy equipment in the proximity, or even
a cosmic ray could alter their contents is a small but finite number. Given enough time, depending on
the application, you will see it happen. When you design applications that have to operate reliably on
such huge time scales, you should start seriously considering the need to provide a periodic "refresh"
of the most important control registers of the essential peripherals used by the application.
27
Chapter 2
Group the sequence of initialization instructions in one or more functions. Call the functions once at
power-up, before entering the main loop, but also make sure that inside the main loop the initialization
functions are called when no other critical task is pending and every control register is reinitialized
periodically.
Exercises
1 . Output a counter on the PORTA pins instead of the alternating on and off patterns.
2. Use a rotating pattern instead of alternating on and off.
Books
• Ullman, L. and Liyanage, M. (2005)
C Programming
Peachpit Press, Berkeley, CA.
This is a fast-reading and modern book, with a simple step-by-step introduction to the C pro-
gramming language.
• Adams, N. (2003)
The Flyers, in Search of Wilbur and Orville Wright
Three Rivers Press, New York, NY
A trip back in time to the first powered flight in history, just 120 feet on the sands of Kitty
Hawk.
Links
http://en. wikipedia. org/wiki/Control_flow#Loops
A wide perspective on programming languages and the problems related to coding and taming
loops.
http://en. wikipedia. org/wiki/Spaghetti_code
Your code gets out of control when you cannot fly the pattern.
28
CHAPTER
3
More pattern work, more loops
In This Chapter
do loops
► A new demo
Variable declarations
Testing with the Logic Analyzer
for loops
Using the Explorer16 demonstration
More loop examples
board
► Arrays
In aviation, a proper "loop" is an "aerobatic" maneuver performed only by pilots that have received
advanced training, using airplanes that are specially equipped for the task. You could take this as either
a disappointment or a reassurance, but you can be certain that, when preparing for the private-pilot
license, you will not be asked to perform any such trick. There will be plenty of other challenges,
though, as you will be asked to perform and repeat to perfection a variety of "turns" including: turns
around a point, S turns, steep turns and standard rate turns. In all these exercises, you will discover how
difficult it can be — while navigating in a three-dimensional environment — to control only one of the
dimensions at a time. When circling around a reference point on the ground, you will initially struggle
to maintain a constant altitude and a constant speed. A little bit of wind will add to the challenge of
maintaining a constant distance from the reference point, and performing a nice and smooth circle.
Practice will make you perfect.
In C-language programming, there are several types of loops, too. Learning which one to choose, and
when and why, will take a little practice, but will make you a better embedded-control programmer.
Flight plan
In the previous lesson, we learned there is a loop at the core of every embedded-control application. In
this lesson, we will continue exploring a variety of other techniques available to the C programmer to
perform loops. Along the way, we will take the opportunity to briefly review integer variables declara-
tions, and increment and decrement operators, quickly touching the arrays declaration and use subject.
As in any good flight lesson, the theory is immediately followed by the practice, and we will conclude
the lesson with a, hopefully entertaining, exercise that will make use of all the concepts and tools
acquired during the lesson.
29
Chapter 3
Preflight checklist
In this lesson we will continue using the MPLAB® SIM software simulator, but once more an Ex-
plorer^ demonstration board could add to the entertainment. In preparation for the new demonstration
project, you can use the "New Project Set-up" checklist to create a new project called "More Loops"
and create a new source file to be called "More . c".
The flight
In a while loop, a block of code enclosed by two curly brackets is executed if, and for as long as, a
logic expression returns a Boolean true value (not zero). The logic expression is evaluated before the
loop, which means that if the expression returns false right from the beginning, the code inside the loop
might never be executed.
do Loops
If you need a type of loop that gets executed at least once, but only subsequent repetitions are depen-
dent on a logic expression, then you have to look at a different type of loop.
Let me introduce you to the do loop syntax:
do {
// your code here...
} while ( x) ;
Don't be confused by the fact that the do loop syntax is using the while keyword again to close the
loop — the behavior of the two loop types is very different.
In a do loop, the code (if any) found between the curly brackets is always executed first, and only then
is the logic expression evaluated. Of course, if all we want is an infinite loop for our main ( ) function,
then it makes no difference if we choose the do or the while. . .
main ( )
{
// initialization code
// main application loop
do {
} while ( 1)
} // main
Looking for curious cases, we might analyze the behavior of the following loop:
do{
// your code segment here...
} while ( 0) ;
You will realize that the code segment inside the loop is going to be executed once and, no matter what,
only once. In other words, the loop syntax around the code is, in this case, a total waste of your typing
efforts and another good candidate for the "most useless piece of code in the world" contest.
30
More pattern work, more loops
Let's now look at a more useful example, where we use a while loop to repeatedly execute a piece of
code for a predefined and exact number of times. First of all, we need a variable to perform the count.
In other words, we need to allocate one or more RAM memory locations to store a counter value.
Note: In the previous two lessons we have been able to skip almost entirely the subject of vari-
able declarations, as we relied exclusively on the use of what are in fact predefined variables: the
special-function registers of the PIC24.
Variable declarations
We can declare an integer variable with the following syntax:
int c ;
Since we used the keyword int to declare c as a 16-bit (signed) integer, the MPLAB C30 compiler
will make arrangements for two bytes of memory to be used. Later, the linker will determine where
those two bytes will be allocated in the physical RAM memory of the selected PIC24 model. As
defined, the variable c will allow us to count from a negative minimum value -32,768 to a maximum
positive value of +32,767. If we need a larger integer numerical range, we can use the long (signed )
integer type as in:
long c;
The MPLAB C30 compiler will use 32 bits (four bytes) for the variable.
If we are looking for a smaller counter, and we can accept a range of values from -128 to +127, we can
use the char integer type instead:
char c ;
In this case the MPLAB C30 compiler will use 8 bits (a single byte).
All three types can be further modified by the unsigned attribute as in:
unsigned char c ; // ranges from 0..255
unsigned int i ; // ranges from 0.. 65, 53 5
unsigned long 1; // ranges from 0.-4,294,967,295
There are then variable types defined for use in floating-point arithmetic:
float f ; // defines a 32 bit precision floating point
long double d; // defines a 64 bit precision floating point variable
for loops
We can now return to our counter example. All we need is a simple integer variable to be used as index/
counter, capable of covering the range from to 5; therefore a char integer type will do:
char i ; // declare i as an 8-bit integer with sign
i = ; // init the index/counter
while ( i<5)
31
Chapter 3
II insert your code here. . .
// it will be executed for i= 0, 1, 2, 3, 4
i = i+1; // increment
}
Whether counting up or down, this is something you are going to do a lot in your everyday program-
ming life.
In C language, there is a third type of loop that has been designed specifically to make coding this
common case easy. It is called the for loop, and this is how you would have used it in the previous
example:
for ( i=0; i<5; i=i+l)
{
// insert your code here. . .
// it will be executed for i=0, 1, 2, 3, 4
}
You will agree that the for loop syntax is compact, and it is certainly easier to write. It is also easier to
read and debug later. The three expressions separated by semicolons and enclosed in the brackets fol-
lowing the for keyword are exactly the same three expressions we used in the prior example:
initialize the index.
check for termination, using a logic expression.
advance the index/counter... in this case incrementing it.
You can think of the for loop as an abbreviated syntax of the while loop. In fact, the logic expression
is evaluated first and, if false from the beginning, the code inside the loop's curly brackets may never
be executed.
Perhaps this is also a good time to review another convenient shortcut available in C. There
is a special notation reserved for the increment and decrement operations that uses the operators:
++ to increment, as in i++ ; is equivalent to i = i+1;
to decrement, as in i--; is equivalent to i = i-1;
There will be much more to say on the subject in later chapters, but this will suffice for now.
More loop examples
Let's see some more examples of the use of the for loop and the increment/decrement operators.
First, a count from to 4:
for ( i=0; i<5; i++)
{
// insert your code here. . .
// it will be executed for i= 0, 1, 2, 3, 4
)
32
More pattern work, more loops
Then, a countdown from 4 to 0:
for ( i=4; i>=0; i--)
{
// insert your code here...
// it will be executed for i= 4, 3, 2, 1,
}
Can we use the for loop to code an (infinite) main program loop?
Sure we can — here is an example:
main ( )
{
// 0. initialization code
// 1. the main application loop
for ( ; 1; )
{
}
} // main
If you like it, feel free to use this form. As for me, from now on I will stick to the while syntax (it is
just an old habit).
Arrays
Before starting to code our next project, we need to review one last C-language feature: array variable
types. An array is just a contiguous block of memory containing a given number of identical elements
of the same type. Once the array is defined, each element can be accessed via the array name and an
index. Declaring an array is as simple as declaring a single variable — just add the desired number of
elements in square brackets after the variable name:
char c[10] ; // declares c as an array of 10 x 8-bit integers
int i[10] ; // declares i as an array of 10 x 16-bit integers
long 1[10] ; // declares 1 as an array of 10 x 32-bit integers
The same squared-brackets notation is used to refer to the content or assign a value to each element of
an array as in:
a = c[0] ; // copy the value of the 1st element of c into a
c[l] = 123; // assign the value 123 to the second element of c
i[2] = 12345; // assign the value 12,345 to the third element of i
1[3] = 123* i[4] ; // compute 123 x the value of the fifth element of i
Note: In C language, the elements of an array of size N have indexes 0, 1, 2. . .(N-l). It is when
manipulating arrays that the for type of loop shows all its merits.
33
Chapter 3
Let's see an example where we declare an array of 10 integers and we initialize each element of the
array to a constant value of 1 :
int a[10]; // declare array of 10 integers: a[0], a[l], a [2]...
a[9]
int i; // the loop index
for ( i=0; i<10; i++)
{
a[ i] = 1;
)
A new demo
The best way to conclude this lesson would be to take all the elements of the C language we have
reviewed so far and put them to use in our next project. This project will consist of making a row of
LEDs, connected to PORTA (as they happen to be connected on the Explorer 16 demo board), flash in a
rapid sequence so that when moving the board left and right rhythmically they will display a short text
message.
How about "Hello World!" or, perhaps more modestly, "HELLO"?
Here is the code:
#include <p24f j 128ga010 .h>
// 1. define timing constant
#define SHORT_DELAY 100
#define LONG_DELAY 800
// 2. declare and initialize an array with the message bitmap
char bi tmap [30]
= {
Obllllllll,
//
H
ObOOOOlOOO,
ObOOOOlOOO,
Obllllllll,
ObOOOOOOOO,
ObOOOOOOOO,
Obllllllll,
//
E
OblOOOlOOl,
OblOOOlOOl,
OblOOOOOOl,
ObOOOOOOOO,
ObOOOOOOOO,
Obllllllll,
//
L
OblOOOOOOO,
OblOOOOOOO,
OblOOOOOOO,
ObOOOOOOOO,
ObOOOOOOOO,
34
More pattern work, more loops
Obllllllll, // L
OblOOOOOOO,
OblOOOOOOO,
OblOOOOOOO,
ObOOOOOOOO,
ObOOOOOOOO,
ObOllllllO, //
OblOOOOOOl,
OblOOOOOOl,
ObOllllllO,
ObOOOOOOOO,
ObOOOOOOOO
};
// 3. the main program
main ( )
{
// 3.1 variable declarations
int i ; // i will serve as the index
// 3.2 initialization
TRISA = OxffOO; // PORTA pins connected to LEDs are outputs
T1CON = 0x8030; // TMRl on, prescale 1:256 Tclk/2
// 3.3 the main loop
while ( 1)
{
// 3.3.1 display loop, hand moving to the right
for( i=0; i<30; i++)
{ // update the LEDs
PORTA = bitmap[i];
// short pause
TMRl = 0;
while ( TMRl < SHORT_DELAY)
{
}
} // for i
// 3.3.2 long pause, hand moving back to the left
PORTA =0; // turn LEDs off
// long pause
TMRl = 0;
while ( TMRl < LONG_DELAY)
{
}
} // main loop
} // main
35
Chapter 3
In section 1, we define a couple of timing constants, so that we can control the flashing sequence speed
for execution and debugging.
In section 2, we declare and initialize an 8-bit integer array of 30 elements, each containing an LED
configuration in the sequence.
Hint: using a highlighter you can mark the "Is" on the page to see the message emerge.
Section 3 contains the main program, with the variable declarations (3.1) at the top, followed by the
microcontroller initialization (3.2) and eventually the main loop (3.3).
The main (while) loop, in turn, is divided in two sections:
1.1.1 Contains the actual LED flash sequence, all 30 steps, that is to be played when the
board is swept from left to right. A for loop is used for accessing each element of the
array, in order. A while loop is used to wait on Timer 1 for the proper sequence timing.
1.1.2 Contains a pause for the sweep back, implemented using a while loop with a longer
delay on Timer 1.
Testing with the Logic Analyzer
To test the program, we will initially use the MPLAB SIM software simulator and the Logic Analyzer
window.
1. Build the project (using the appropriate check list).
2. Open the Logic Analyzer window.
3. Click on the Channel button to add, in order, all the I/O pins from RAO to RA7 connected to
the row of LEDs.
The "MPLAB SIM Set-up" and "Logic Analyzer Set-up" checklists will help you make sure that you
don't forget any detail.
Trigger Position —
Start** Centeff Endr
finger PC
Now CJea
TimeSare
[Cyc~~z]
Mode
\SuTQh
Owrtefc
FPj&I eJ®J nM ga|Hlrta.
RAO -
•
•
RA1 -
RA2 -
RA3 -
•••i
RA4 -
•
•
•
RA5 -
RA6 -
RA7-
•
i — i-
-i — i — i —
T — 1 — " —
r-r
— T —
- > 1
—
r i
— i — i — i
—
i — i — i—
i —
i — i — i — i—
1 —
— r
■
T 1 1
— T-
-i — i — r
0.0
ISTjOQJ
35000.0
Figure 3-1 . Snapshot of the Logic Analyzer window after the first sweep.
36
More pattern work, more loops
Then, I suggest you go back to the editor window and set the cursor on the first instruction of the 3.3.2
section and select the "Run to Cursor" option from the right click (context) menu. This will let the pro-
gram execute the entire portion containing the message output (3.3.1) and stop before the long delay.
As soon as the simulation halts on the cursor line, you can switch to the Logic Analyzer window and
verify the output waveforms. They should look like the figure below:
To help you visualize the output, I added a few dots to represent the LEDs being turned on during the
first few steps of the sequence. If you train your eye to see an LED on wherever the corresponding pin
is at the logic high level, you should be able to read the desired message.
Using the Explorer16 demonstration board
If you have an actual Explorer 16 demonstration board available, the fun can be doubled.
1. Use the "MPLAB ICD2 Set-up" checklist to enable the in-circuit debugger.
2. Use the "MPLAB ICD2 Device Configuration" to verify the device configuration bits proper
setting for use on the Explorer 16 demonstration board.
3. Use the "MPLAB ICD2 Programming" checklist to program the PIC24 in circuit.
If successful, and if you dim the light a bit in the room, you should be able to see the message flashing
as you "shake" the board. The experience is going to be far from perfect though. With the Simulator
and the Logic Analyzer window, we can choose which part of the sequence we want to visualize with
precision and "freeze" it on the screen. On the demonstration board, you might find it quite challenging
to synchronize the board's movement with the LED sequence.
Consider adjusting the timing constants to your optimal speed. After some experimentation, I found the
values 100 and 800 ideal, respectively, for the short and long delays, but your preferences might differ.
Post-flight briefing
In this lesson we reviewed the declaration of a few basic variable types, including integers and float-
ing point of different sizes. Array declarations and their initialization were also used to create an LED
display sequence and a for loop was used to play it back.
Notes for assembly experts
If you were wondering whether the increment and decrement operators were going to be translated by
the C30 compiler with the inc and dec assembly instructions, you were mostly right. I am saying
"mostly" and not "always" because the ++ and — operators are actually much smarter than that. If the
variable they are applied to is an integer, as in the trivial examples above, this is certainly the case. But
if they are applied to a pointer (which is a variable type that contains a memory address) they actually
increase the address by the exact number of bytes required to represent the quantity pointed to. For
example, a pointer to 16-bit integers will increment its address by 2, a pointer to a 32-bit long integer
will increment its address by 4, and so on. To satisfy your curiosity, switch to the disassembly view and
see how the MPLAB C30 chooses the best assembly code, depending on the situation.
Loops in C can be confusing: should you test the condition at the beginning or the end? Should you
use the for type or not? The fact is, in some situations the algorithm you are coding will dictate which
one to use, but in many situations you will have a degree of freedom and more than one type might do.
Choose the one that makes your code more readable, and if it really doesn't matter, as in the main loop,
just choose the one you like and be consistent.
37
Chapter 3
Notes for PIC microcontroller experts
Depending on the target microcontroller architecture, and ultimately the arithmetic and logic unit
(ALU), operating on bytes versus operating on word quantities can make a big difference in terms of
code compactness and efficiency. While in the PIC 16 and PIC 18 8-bit architectures there is a strong
incentive to use byte-sized integers wherever possible, in the PIC24, 16-bit architecture word-sized in-
tegers can be manipulated with the same efficiency. The only limiting factor preventing us from always
using 16-bit integers with the MPLAB C30 compiler is the consideration of the relative preciousness of
the internal resources of the microcontroller, and in this case the RAM memory.
Notes for C experts
Even if PIC24 microcontrollers have a relatively large RAM memory array, embedded-control applica-
tions will always have to contend with the reality of cost and size limitations. If you learned to program
in C on a PC or a workstation, you probably never considered using anything smaller than an int as a
loop index. Well, this is the time to think again. Shaving one byte at a time off the requirements of your
application might, in some cases, mean the ability to select a smaller model of PIC24 microcontroller,
saving fractions of a dollar that, when multiplied by thousands or millions of units (depending on your
production run rates), can mean real money added to the bottom line. In other words, if you learn to
keep the size of your variables to the strict minimum necessary, you will become a better embedded-
control designer and ultimately... this is what engineering is all about.
Tips and tricks
This is the third lesson and, I am sure you will have noticed, for the third time I have been instructing
you to start the simulation by setting a cursor on the first line of code and executing a Run To Cursor
command (or setting a breakpoint) instead of more simply starting to single-step through the code.
Why bother? Why can't we just start in Animation mode, for example, right after completing the proj-
ect build?
As I briefly mentioned more than once, it is because of the CO initialization code. Let me add, it's also
because of MPLAB 's obsessive desire to shield you from the low-level details. In fact, MPLAB won't
even show the cursor (the big green arrow) if you try to single-step through it — quite a disconcerting
experience. It will not let you see any trace of the CO code even if you use the Disassembly window.
But the CO code is starting to do interesting things for you, and you might be getting curious. For exam-
ple, in this last exercise we declared an array called bitmap [ ] and we asked for it to be initialized with
a specific series of values. The array, being a data structure, resides in RAM during execution, so the
compiler has to instruct the CO initialization code to copy the contents of the array from a table in Flash
memory immediately after the program start.
The only way to take a look at the CO inner workings is to open the Program Memory window
("View— > Program Memory"), select the Symbolic mode (using the buttons at the bottom of the win-
dow), and patiently inspect the assembly code. A few labels here and there will offer a little support.
The first line of the program memory window will correspond to the reset vector of the PIC24 and will
always contain a jump to the proper beginning of the program.
0000 goto _reset
You will have to scroll through several pages of what, you will learn shortly, is the interrupt vectors
38
More pattern work, more loops
table. Eventually, you will find the _reset label. There, in a short sequence, you will recognize three
essential pieces of code:
the stack pointer (wl5) initialization
_reset mov.w #0x81e,wl5
a call to a subroutine for the variable (RAM) initialization
rcall _data_init
the call to the main ( ) function
call main
a software reset instruction upon program termination
reset
I hope this satisfies your curiosity for now. If during a future debugging session you are not able to find
the cursor, chances are you will be able to find it in here. Something might have caused the processor
to reset (a bug, an external event?) and you might be stepping through the very heart of the CO initial-
ization code. Check out the many emergency checklists created to help you recover and find your way
safely home.
39
Chapter 3
Exercises
1. Improve the display /hand synchronization, waiting for a button to be pressed before the hand
sweep is started.
2. Add a switch to sense the sweep movement reversal and play the LED sequence backward on
the back sweep.
Books
Rony, P., Larsen D. and Titus J., 1976
The 8080A Bugbook, Microcomputer Interfacing and Programming
Howard W. Sams & Co., Inc., Indianapolis, IN
This is the book that introduced me to the world of microprocessors and changed my life
forever. No high-level language programming here, just the basics of assembly programming
and hardware interfacing. (Too bad this book is already considered museum material; see link
below.)
• Shulman, S. (2003)
Unlocking the Sky, Glenn Hammond Curtis and the Race to Invent the Airplane
Harper Collins, New York, NY
A beautiful recount of the "struggle to innovate" in the early days of aviation.
Links
http://www.bugbookcomputermuseum.com/BugBook-Titles.html
A link to the "Bugbooks museum" — 30 years since the introduction of the Intel 8080 micro-
processor and it is like centuries have already passed.
40
CHAPTER
4
NUMB3RS
In This Chapter
On optimization (or lack
thereof)
Testing
► Going long
► Note on the multiplication
of long integers
► Long Long Multiplication
► Floatingpoint
The human sense of equilibrium is based on a device (the labyrinth or vestibular apparatus) located
inside the ear that gives us feedback on gravity and motion. But, unlike that of birds, ours was just
not designed for flight. It can be easily tricked by a little centrifugal acceleration and, in the absence
of visual clues (in fog, clouds or during a night flight), it can have us flying happily into a tightening
spiral... into the ground. To overcome our shortcomings, we have to rely on instruments to tell us how
fast we are flying, in which direction and, perhaps most importantly, which way is up. Practically, this
means that so much information that directly reaches the brain of a bird from its senses will arrive at
our brain only in the form of numbers.
A good portion of the time spent by a student pilot on an airplane, after the first few flights, is spent
learning the "right" numbers for his airplane — like the best climb speed, the best glide speed, the
take-off (rotation) speed, the approach speeds and so on. Most of the time, these numbers are avail-
able inside the Pilot Operating Handbook (POH), the airplane datasheet, and, for convenience, on the
related checklists. Each pilot tries his best to follow them religiously so that his flying performance
gains consistency as he improves the command of the machine. However, even the most experienced
aerobatic pilots, and certainly all the airline pilots who fly thousands of hours every year, will tell you
how flying can be extremely spontaneous, if you know all your numbers exactly!
Similarly in embedded control, we need to know well the numeric types, their relative performance,
and the costs and benefits of each one.
Flight plan
In this lesson we will review all the numerical data types offered by the MPLAB® C30 compiler. We
will learn how much memory the compiler allocates for the numerical variables and we will investi-
gate the relative efficiency of the routines used to perform arithmetic operations by using the MPLAB
SIM Stopwatch as a basic profiling tool. This experience will help you choose the "right" numbers for
your embedded-control application, understanding when and how to balance performance and memory
resources, real-time constraints and complexity.
41
Chapter 4
Preflight checklist
This entire lesson will be performed exclusively with software tools including the MPLAB IDE,
MPLAB C30 compiler and the MPLAB SIM simulator.
Use the "New Project Set-up" checklist to create a new project called "Numbers" and a new source file
called "numbers . c" .
The flight
To review all the data types available, I recommend you take a look at the MPLAB C30 User Guide.
You can start in Chapter 5, where you can find a first list of the supported integer types.
Type
Bits
Min
Max
char, signed char
8
-128
127
unsigned char
8
255
short, signed short
16
-32768
32767
unsigned short
16
65535
int, signed int
16
-32768
32767
unsigned int
16
65535
long, signed long
32
_2 31
2 31-1
unsigned long
32
2 32 -1
long long**, signed long long**
64
_2 63
2 63 -1
unsigned long long**
64
2 64.-,
ANSI-89 extension
Table 4-1 . Integer data types.
As you can see in Table 4-1, there are 10 different integer types as specified in the ANSI C standard
including: char, int, short, long, and long long, both in the signed (default) and unsigned
variant. The table shows the number of bits allocated specifically by the MPLAB C30 compiler for
each type, and, for your convenience, spells out the minimum and maximum value that can be repre-
sented.
It is expected that, when the type is signed, one bit must be dedicated to the sign itself and the result-
ing numerical range is therefore halved. It is also interesting to note how the C30 compiler treats int
and short as synonyms by allocating 16 bits for both of them. Both 8- and 16-bit quantities can be
processed efficiently by the PIC24 arithmetic and logic unit (ALU), so that most of the arithmetic
operations can be coded by the compiler using few and efficient instructions. The long integers are
treated as 32-bit quantities, using four bytes, while the long long type (specified by the ANSI C ex-
tensions in 1989) requires eight bytes. Operations on long integers are performed by the compiler using
short sequences of instructions inserted inline. So, there is a small performance penalty to pay for using
long integers, and a proportionally larger penalty to pay for long long integers, that must be taken
into account.
42
NUMB3RS
Let's see a first integer example; we'll start by typing the following code:
unsigned int i,j,k;
main ( )
{
i = 0x1234; // assign an initial value to i
j = 0x5678; // assign an initial value to j
k = i * j ; // perform the product and store the result in k
}
After building the project (Project— > Build All or Ctrl + F10), we can open the Disassembly window
("View— ^Disassembly Listing") and take a look at the code generated by the compiler. Even without
knowing the PIC24 instruction set in detail, we can recognize the two assignments. They are performed
by loading the literal values to register w0 first and from there to the memory locations reserved for the
variable i , and later for variable j .
i = 1234;
204D20
mov.
,w #0x4d2, 0x0000
884290
mov.
,w 0x0000, 0x0852
j :
= 5678;
2162E0
mov.
,w #0xl62e, 0x0000
8842A0
mov.
,w 0x0000, 0x0854
k =
= i * j;
804291
mov.
,w 0x0852, 0x0002
8042A0
mov.
,w 0x0854, 0x0000
B98800
mul .
,ss 0x0002,0x0000,0x0000
8842B0
mov.
,w 0x0000, 0x0856
// move literal value to W0
// move data from W0 to i
// move literal value to W0
// move data from W0 to j
II move data from i to Wl
// move data from j to W0
// move result to k
The multiplication is performed by transferring the values from the locations reserved for the two in-
teger variables i and j back to registers w0 and wl , and then performing a single mul instruction. The
result, available in w0 , is stored back into the locations reserved for k. Pretty straightforward.
On optimization (or lack thereof)
You will notice how the overall program, as compiled, is somewhat redundant. The value of j , for
example, is still available in register w0 when it is reloaded again — just before the multiplication. Can't
the compiler see that this operation is unnecessary?
In fact, the compiler does not see things this clearly — its role is to create "safe" code, avoiding (at least
initially) any assumption and using standard sequences of instructions. Later on, if the proper optimiza-
tion options are enabled, a second pass (or more) is performed to remove the redundant code. During
the development and debugging phases of a project, though, it is always good practice to disable all
optimizations as they might modify the structure of the code being analyzed and render single-step-
ping and breakpoint placement problematic. In the rest of this book, we will consistently avoid making
use of any compiler optimization option; we will verify that the required levels of performance are
43
Chapter 4
obtained regardless. As a consequence, you will be able to execute all the examples presented in this
and the following chapters using the C30 Compiler Student Edition, which is free and available on the
companion CD-ROM.
Testing
To test the code, we can choose to work with the simulator from the Disassembly Listing window
itself, single-stepping on each assembly instruction. Or we can choose to work from the C source in the
editor window, single-stepping through each C language statement. In both cases, we can:
1 . Set the cursor on the first line containing the initialization of the first variable, and perform a
Run To Cursor command to let the program initialize and stop the execution just before the
first instruction we want to observe.
2. Open the Watch window ("View^ Watch") and select wregO in the SFR selection box, then
click on the "Add SFR" button.
3. Repeat the operation for wregi.
4. Select "i " in the symbol selection box, and click on the 'Add Symbol" button.
5. Repeat the operation for j and k.
6. Use the "Step Over" function to execute the next few program lines, observing the effects on
the registers and variables in the Watch window. As we noted before, when the value of a vari-
able in the Watch window changes, it is conveniently highlighted in red.
If you need to repeat the test, perform a Reset ("Debugger— >Reset—> Processor Reset") and again place
the cursor on the first line of code to analyze, followed by a new Run To Cursor command.
Going long
At this point, modifying only the first line of code, we can change the entire program to perform opera-
tions on long integer variables.
unsigned long i,j,k;
mam
{
i = 0x1234; // assign an initial value to i
j = 0x5678; // assign an initial value to j
k = i * j; // perform the product and store the result in k
}
Rebuilding the project and switching again to the Disassembly Listing window (if you had the editor
window maximized and you did not close the Disassembly Listing window, you could use the Ctrl +
Tab command to quickly alternate between the editor and the Disassembly Listing), we can see how
the newly generated code is considerably longer than the previous version. While the initializations are
still straightforward, the multiplication is now performed using several more instructions.
44
NUMB3RS
k = i * j;
8042C1 mov.w 0x0858,0x0002
8042E0 mov.w 0x085c , 0x0000
B80A00 mul.uu 0x0002,0x0000,0x0008
8042C1 mov.w 0x0858,0x0002
8042F0 mov.w 0x085e, 0x0000
B98800 mul.ss 0x0002,0x0000,0x0000
780105 mov.w 0x000a, 0x0004
410100 add.w 0x0004,0x0000,0x0004
8042E1 mov.w 0x085c , 0x0002
8042D0 mov.w 0x085a, 0x0000
B98800 mul.ss 0x0002,0x0000,0x0000
410100 add.w 0x0004,0x0000,0x0004
780282 mov.w 0x0004 , 0x000a
884304 mov.w 0x0008,0x0860
884315 mov.w 0x000a, 0x0862
The PIC24 arithmetic and logic unit can only process 16 bits at a time, so the 32-bit multiplication is
actually performed as a sequence of 16-bit multiplications and additions. The sequence used by the
compiler is generated with pretty much the same technique that we learned to use in elementary school,
only performed on a word at a time rather than a digit at a time.
Note on the multiplication of long integers
In practice, to perform a 32-bit multiplication using 16-bit instructions, there should be four mul-
tiplications and two additions, but you will note how the compiler has actually inserted only three
multiplication instructions. What is going on here?
The fact is that multiplying two long integers (32 bits each) will produce a 64-bit wide result. But in
the example above, we have specified that the result will be stored in yet another long variable, there-
fore limiting the result to a maximum of 32 bits. Doing so, we have clearly left the door open for the
possibility (not so remote) of an overflow, but we have also given the compiler the permission to ignore
the most significant bits of the result. Knowing those bits are not going to be missed, the compiler has
eliminated completely the fourth multiplication step — in a way, already optimizing the code.
Long long multiplication
Changing the variables declarations to the long long integer type (64-bit) is just as simple:
unsigned long long i,j,k;
main ( )
{
i = 0x1234; // assign an initial value to i
j = 0x5678; // assign an initial value to j
k = i * j ; // perform the product and store the result in k
}
45
Chapter 4
Recompiling and inspecting the new code in the Disassembly Listing window reveals that this time the
compiler has chosen a different approach. Instead of a longer sequence inserted inline, there are now
only a few instructions to transfer the data into predefined registers and there is a call to a subroutine.
The subroutine will appear in the disassembly listing, after all the main function code. This subrou-
tine is clearly separated and identified by a comment line that indicates it is part of a library, a module
called "muldi3 . c". The source for this routine is actually available as part of the complete documen-
tation of the C30 compiler and can be found in the subdirectory "src/libm/src/" under the same
directory tree where the C30 compiler has been installed on your hard disk.
By selecting a subroutine in this case, the compiler has clearly made a compromise. Calling the
subroutine means adding a few extra instructions and using extra space on the stack. On the other
hand, fewer instructions will be added each time a new multiplication (among long long integers) is
required in the program; therefore code space will be preserved.
Floating point
Beyond integer data types, the C30 compiler offers support for a few more data types that can capture
fractional values — the floating-point data types. There are three types to choose from, corresponding to
two levels of resolution: float, double and long double.
Notice how the MPLAB C30 compiler, by default, allocates the same number of bits for both the float
and the double types, using the single precision floating-point format defined in the IEEE754 standard.
Only the long double data type is treated as a true double-precision IEEE754 floating-point type.
Type
Bits
EMin
EMax
NMin
N Max
float
32
-126
127
2- 126
2 128
double*
32
-126
127
2- 126
2128
long double
64
-1022
1023
;:.--C:2
21024
£ = Exponent
N = Normalized (approximate)
* double is equivalent to long double if -fno- short -double is used
Table 4-2. Floating points data types.
Notes for C experts
It is my belief that these floating-point settings were intentionally used by the MPLAB C30 design-
ers to simplify and make more efficient the porting of complex math algorithms to embedded-control
target applications. Most of the algorithms and libraries available in literature are designed for the
performance and resources of personal computers and workstations, and make use of double-precision
floating-point arithmetic whenever possible to maximize accuracy. Most often in embedded control,
we are willing to compromise some of that accuracy for the level of performance necessary to achieve
real-time response.
If needed, this behavior can be changed either locally, by turning doubles into long doubles in selected
cases, or globally, by using special compiler options (open the "Project— > Build Options— > Project"
dialog box, check the Use alternate Setting check box and add "-f no-short-double" to the edit box
underneath).
46
NUMB3RS
Since the PIC24 doesn't have a hardware floating point unit (FPU), all operations on floating-point
types must be coded by the compiler using floating-point arithmetic libraries whose size and com-
plexity is considerably larger/higher than any of the integer libraries. You should expect a major
performance penalty if you choose to use these data types, but, again, if the problem calls for fractional
quantities to be taken into account, the C30 compiler certainly makes dealing with them easy.
Let's modify our previous example to use floating-point variables:
float
i/
j,k
i
main
{
i
=
12.
34
J
=
56.
78
k
=
i *
J
// assign an initial value to i
// assign an initial value to j
// perform the product and store the result in k
}
After recompiling and inspecting the Disassembly Listing window, you will notice that the compiler
has immediately chosen to use a subroutine instead of inline code.
Changing the program again to use double-precision floating-point type, long double, produces very
similar results. Only the initial assignments seem to be affected, and all we can see is a subroutine call.
The C compiler makes using any data type so easy that we might be tempted to always use the largest
integer or floating-point type available, just to stay on the safe side and avoid the risk of overflows and
underflows. On the contrary, choosing the right data type for each application can be critical in embed-
ded control to balance performance and optimize the use of resources. In order to make an informed
decision, we need to know more about the level of performance we can expect when choosing the vari-
ous precision data types.
Measuring performance
Let's use what we have learned so far about simulation tools to measure the actual relative performance
of the arithmetic libraries (integer and floating-point) used by the C30 compiler. We can start using the
software simulator's (MPLAB SIM) built-in Stopwatch tool, with the following code:
//
//
Numbers
//
int
i 1 , i 2 , 13;
long
11, 12, 13;
long
long 111, 112,
113;
float
fl,f2, f3;
long
double dl, d2 ,
d3;
main
47
Chapter 4
il = 1234; // testing integers (16-bit)
i2 = 5678;
i3= il * i2; // 1. int multiplication
11 = 1234; // testing long integers (32-bit)
12 = 5678;
13= 11 * 12; // 2. long multiplication
111 = 1234; // testing long long integers (64-bit)
112 = 5678;
113= 111 * 112; // 3.
fl = 12.34; // testing single precision (32-bit) floating point
f2 = 56.78;
f3= fl * f 2 ; // 4. single precision multiplication
dl = 12.34; // testing double precision (64-bit) floating point
d2 = 56.78;
d3= dl * d2 ; // 5. double precision multiplication
}
After compiling and linking the project, we can set the cursor on the line containing the first integer
multiplication (/ / l . ) in the editor window and perform a Run To Cursor, to position the program
counter for our test. Open the Stopwatch window ("Debugger— ^Stopwatch") and position the window
according to your preferences (personally I like it docked to the bottom of the screen so that it does not
overlap with the editor window and it is always visible and accessible).
Zero the Stopwatch timer and execute a Step-Over command ("Debug— >StepOver", or press F8). As
the Simulator completes updating the Stopwatch window, you can manually record the execution time
required to perform the integer operation. The time is provided by the simulator in the form of a cycle
count along with an indication in milliseconds derived by the cycle count multiplied by the simulated
clock frequency, a parameter specified in the Debugger Settings ("Debugger— ^Settings— >Osc/Trace"
tab).
Proceed by setting the cursor over the next multiplication (/ / 2.), and execute a new Run To Cursor
command or simply continue Stepping until you reach it. Again zero the Stopwatch, execute a Step-
Over and record the second time. Continue until all five types have been tested.
Multiplication Test
Cycle
Count
Performance relative to
int
long
Float
Integer
4
1
-
-
Long Integer
15
3.75
1
-
Long-Long Integer
99
24.75
6.6
-
Single Precision f.p.
121
30
8
1
Double Precision f.p.
317
79
21
2.6
Table 4-3. Relative Performance Test Results Using MPLAB C30 rev. 1.30
(all optimizations disabled).
48
NUMB3RS
In Table 4-3, 1 have recorded the results (cycle counts) in the first column and then added more col-
umns to show the relative performance ratios, obtained by dividing the cycle count of each row by the
cycle count recorded for the reference type. Don't be alarmed if you happen to record different values;
several factors can affect the measure. Future versions of the compiler could possibly use more ef-
ficient libraries, and/or optimization features could be introduced or enabled at the time of testing.
Keep in mind that this type of test lacks any of the rigorousness required by a true performance bench-
mark. What we are looking for here is just a basic understanding of the impact on the performance we
can expect from choosing to perform our calculations with one data type versus another. We are look-
ing for the big picture — relative orders of magnitude. For that purpose, the table we just obtained can
already give us some interesting indications.
As expected, 16-bit operations appear to be the fastest. Long-integer (32-bit) multiplications are about
four times slower, while long-long-integer (64-bit) multiplications are one order of magnitude slower.
Again, it was expected that single precision floating-point operations would require more effort than
integer operations. Multiplying a 32-bit integer is only about four times slower than multiplying a
16-bit integer. However, multiplying 32-bit floating-point numbers is more than 30 times slower than
multiplying 16-bit integers. That means it is eight times slower than the corresponding 32-bit inte-
ger multiplication, or about an order of magnitude. Going to double-precision floating-point (64-bit)
though, only doubles the number of cycles. This tells us that, apparently, the double-precision floating-
point libraries used by the compiler are more efficient than the corresponding 64-bit integer libraries.
So, when should we use floating point and when should we use integer arithmetic?
Beyond the obvious, from the little we have learned so far we can perhaps extract the following rules:
1. Use integers every time you can (i.e., when fractions are not required, or the algorithm can be
rewritten for integer arithmetic).
2. Use the smallest integer type that will not produce an overflow or underflow.
3. If you have to use a floating-point type (fractions are required), expect an order-of-magnitude
reduction in the performance of the compiled program.
4. Double-precision floating-point (long double) seems to only reduce the performance further
by a factor of two.
Keep in mind also that floating-point types offer the largest value ranges, but also are always intro-
ducing approximations. As a consequence, floating-point types are not recommended for financial
calculations. Use long or long long integers instead, and perform all operations in cents (instead of
dollars and fractions).
Post-flight briefing
In this lesson, we have learned not only what data types are available and how much memory is al-
located to them, but also how they affect the resulting compiled program — code size and the execution
speed. We used the MPLAB SIM simulator Stopwatch function to measure the number of instruction
cycles (and therefore time) required for the execution of a series of code segments. Some of the infor-
mation gathered will, hopefully, be useful to guide our actions in the future when balancing our needs
for precision and performance in embedded-control applications.
49
Chapter 4
Notes for assembly experts
The brave few assembly experts that have attempted to deal with floating-point numbers in their ap-
plications tend to be extremely pleased and forever thankful for the great simplification achieved by the
use of the C compiler. Single- or double-precision arithmetic becomes just as easy to code as integer
arithmetic has always been.
When using integer numbers, though, there is sometimes a sense of loss of control, as the compiler
hides the details of the implementation and some operations might become obscure or much less intui-
tive/readable. Here are some examples of conversion and byte-manipulation operations that can induce
some anxiety:
1. Converting an integer type into a smaller/larger one.
2. Extracting or setting the most or least significant byte of a 16-bit data type.
3. Extracting or setting one bit out of an integer variable.
The C language offers convenient mechanisms for covering all such cases via implicit type conversions
as in:
int i; // 16-bit
long 1; // 3 2 -bit
1 = i ; // the value of i is transferred into the two LSB of 1
// the two MSB of 1 are cleared
Explicit conversions might be required (called "type casting") in some cases where the compiler would
otherwise assume an error, as in:
int i; // 16-bit
long 1; // 3 2 -bit
i = (int) 1; // (int) is a type cast that results in the two MSB of 1
// to be discarded as 1 is treated as a 16-bit value
Bit fields are used to cover the conversion to and from integer types that are smaller than one byte.
Bit fields are treated by the MPLAB C30 compiler with great efficiency and will result in the use of
bit-manipulation instructions whenever possible. The PIC24 library files contain numerous examples of
definitions of bit fields for the manipulation of all the control bits in the peripheral and the core special-
function registers.
Here is an example extracted from the include file used in our project, where the Timer 1 control regis-
ter ticon is defined and each individual control bit is exposed in a structure defined as TicoNbits:
extern unsigned int TICON;
extern union {
struct {
unsigned : 1 ;
unsigned TCS:1;
unsigned TSYNC : 1 ;
unsigned : 1 ;
unsigned TCKPS0:1;
unsigned TCKPS1:1;
50
NUMB3RS
unsigned TGATE : 1 ;
unsigned : 6 ;
unsigned TSIDL:1;
unsigned : 1 ;
unsigned T0N:1;
};
struct {
unsigned : 4 ;
unsigned TCKPS : 2 ;
};
} TlCONbits;
Notes for PIC microcontroller experts
The PIC microcontroller user, familiar with the 8-bit PIC microcontrollers and their respective com-
pilers, will notice a considerable improvement in the performance, both with integer arithmetic and
floating-point arithmetic. The 16-bit ALU available in the PIC24 architecture is clearly providing a
great advantage by manipulating twice the number of bits per cycle, but the performance improvement
is further accentuated by the availability of up to eight working registers, which make the coding of
critical arithmetic routines and numerical algorithms more efficient.
Tips and tricks
Math libraries
The MPLAB C30 compiler supports several standard ANSI C libraries including:
"limits .h", which contains many useful macros defining implementation-dependent limits,
such as, for example, the number of bits composing a char type (char_bit) or the largest
integer value (int_max).
"float .h" , which contains similar implementation-dependent limits for floating-point data
types, such as, for example, the largest exponent for a single-precision floating- point variable
(flt_max_exp).
"math . h", which contains trigonometric functions, rounding functions, logarithms and
exponentials.
Complex data types
The MPLAB C30 compiler supports complex data types, as an extension of both integer and floating-
point types. Here is an example declaration for a single-precision floating-point type:
complex float z ;
Notice the use of a double underscore before and after the keyword complex .
The variable z so defined has now a real and an imaginary part that can be individually addressed using
the syntax: real z and imag z, respectively.
Similarly, the next declaration produces a complex variable of 16-bit integer type:
complex int x;
51
Chapter 4
Complex constants are easily created adding the suffix "i" or "j" as in the following examples:
x = 2 + 3 j ;
z = 2. Of + 3. Of j;
All standard arithmetic operations (+, -, *, /) are performed correctly on complex data types; addition-
ally, the "~" operator produces the complex conjugate.
Complex types could be pretty handy in some types of applications, making the code more readable
and helping avoid trivial errors. Unfortunately, as of this writing, the MPLAB IDE support of complex
variables during debugging is only partial, giving access only to the "real" part through the Watch
window and the mouse-over function.
Exercises
1. Write a program that uses Timer2 as a stopwatch for real-time performance measurements. If
the width of Timer 2 is not sufficient:
use the prescaler (and lose some of the lsb), or
use Timer2 and Timer3 joined in the new 32-bit timer mode.
2. Test the relative performance of the division for the various data types.
3. Test the performance of the trigonometric functions relative to standard arithmetic operations.
4. Test the relative performance of the multiplication for complex data types.
Books
• Gahlinger, P. M. (2000)
The Cockpit, a Flight of Escape and Discovery
Sagebrush Press, Salt Lake City, UT
An interesting journey around the world, following the author in search of... his soul.
Every instrument in the cockpit triggers a memory and starts a new chapter.
Links
http://en. wikipedia. org/wiki/Taylor_series
If you are curious how the C compiler can approximate some of the functions in the math
library.
52
CHAPTER
5
Interrupts
In This Chapter
Nesting of interrupts
►
Testing the Timerl interrupt
Traps
►
The secondary oscillator
A template and an example
►
The Real-time Clock Calendar (RTCC)
for Timerl interrupt
►
Managing multiple interrupts
A real example with Timerl
Every pilot is taught to keep his eyes constantly scanning the horizon, looking for visual clues about
position and direction of flight and looking for other airplanes. But, he also needs to check the airplane
instruments momentarily to verify his speed and altitude, and to keep an eye on the map. Now and
then, there might be the need to focus longer on one of the inputs, and it is essential to learn how some
instruments need a more frequent check than others, depending on the phase of flight and a number of
other conditions. In other words, pilots need to learn to multitask, assigning the correct priority to each
instrument and optimizing the use of time so as to stay always ahead of the machine.
For reasons of efficiency, size, and ultimately cost, in the embedded-control world the smallest applica-
tions, which happen to be implemented in the highest volumes, most often cannot afford the "luxury"
of a multitasking operating system and use the interrupt mechanisms instead to "divide their attention"
on the many tasks at hand.
Flight plan
In this lesson we will see how the MPLAB® C30 compiler allows us to easily manage the interrupt
mechanisms offered by the PIC24 microcontroller architecture. After a brief review of some of the C
language extensions and some practical considerations, we will present a short example of how to use
the secondary (low-frequency) oscillator to maintain a real-time clock.
Pref light checklist
This entire lesson will be performed exclusively with software tools, including the MPLAB IDE,
MPLAB C30 compiler and the MPLAB SIM simulator.
Use the "New Project Set-up" checklist to create a new project called "Interrupts" and a new source file
similarly called "interrupts . c".
53
Chapter 5
The flight
An interrupt is an internal or external event that requires quick attention from the CPU. The PIC24
architecture provides a rich interrupt system that can manage as many as 1 18 distinct sources of inter-
rupts. Each interrupt source can have a unique piece of code, called the Interrupt Service Routine
(ISR) directly associated via a pointer, also called a "vector," to provide the required response action.
Interrupts can be completely asynchronous with the execution flow of the main program. They can be
triggered at any point in time and in an unpredictable order. Responding quickly to interrupts is essen-
tial to allow prompt reaction to the trigger event and a fast return to the main program execution flow.
Therefore, the goal is to minimize the interrupt latency, defined as the time between the triggering event
and the execution of the first instruction of the Interrupt Service Routine (ISR). In the PIC24 architec-
ture, the latency is not only very short but it is also fixed for each given interrupt source — only three
instruction cycles for internal events and four instruction cycles for external events. This is a highly
desirable quality that makes the PIC24 interrupt management superior to most other architectures.
The MPLAB C30 compiler helps manage the complexity of the interrupt system by providing a few
language extensions. The PIC24 keeps all interrupt vectors in one large Interrupt Vector Table (IVT)
and the MPLAB C30 compiler can automatically associate interrupt vectors with "special" user-de-
fined C functions as long as a few limitations are kept in consideration, such as:
They are not supposed to return any value (use type void).
No parameter can be passed to the function (use parameter void) .
They cannot be called directly by other functions.
Ideally, they should not call any other function.
The first three limitations should be obvious given the nature of the interrupt mechanism — since it is
triggered by an external event, there cannot be parameters or a return value because there is no proper
function call in the first place. The last limitation is more of a recommendation to keep in mind for ef-
ficiency considerations.
The following example illustrates the syntax that could be used to associate a function to the Timer 1
interrupt vector:
void attribute ( ( interrupt) ) _T1 Interrupt ( void)
{
// interrupt service routine code here...
} // _InterruptVector
The function name _ti interrupt was not an arbitrary choice, but is actually the predefined identifier
for the Timer 1 interrupt as found in the Interrupt Vectors Table of the PIC24, (defined in the datasheet)
and as coded in the linker script, the " . gld" file loaded for the current project.
The attribute ( ( ) ) mechanism is used by the C30 compiler in this and many other circum-
stances as a way to specify special features such as a C language extension. Personally, I find this
syntax too lengthy and hard to read. I recommend the use of a couple of macros that can be found in
each PIC24 include (" . h") files and that greatly improve the code readability. In the following exam-
ple, the _isr macro is used to the same effect as the previous code snippet:
54
Interrupts
void _ISR _T1 Interrupt (void)
{
// interrupt service routine code here...
} // _InterruptVector
From Tables 5- la and 5- lb, taken from the PIC24FJ128GA010 family datasheet, you can see
which events can be used to trigger an interrupt. Among the external sources available for the
PIC24FJ128GA010, there are:
5 x External pins with level trigger detection
22 x External pins connected to the Change Notification module
5 x Input Capture modules
5 x Output Compare modules
2 x Serial port interfaces (UARTs)
4 x Synchronous serial interfaces (SPI and PC™)
Parallel Master Port
Among the internal sources we count:
5 x 16-bit Timers
1 x Analog-to-Digital Converter
1 x Analog Comparators module
1 x Real-time Clock and Calendar
1 x CRC generator
Many of these sources in their turn can generate several different interrupts. For example, a serial-port
interface peripheral (UART) can generate three type of interrupts:
When new data has been received and is available in the receive buffer for processing.
When data in the transmit buffer has been sent and the buffer is empty, ready and available, to
transmit more.
When an error condition has been generated and action might be required to
re-establish communication.
Each interrupt source also has five associated control bits, allocated in various special-function regis-
ters (see Table 5-1):
The Interrupt Enable bit (typically represented with a suffix -ie):
- When cleared, the specific trigger event is prevented from generating interrupts.
- When set, it allows the interrupt to be processed.
55
Chapter 5
Interrupt Source
Vector
Number
IVT Address
AIVT
Address
Interrupt Bit Locations
Flag
Enable
Priority
ADC1 Conversion Done
13
00002Eh
00012Eh
IFS0<13>
IEC0<13>
IPC3<6:4>
Comparator Event
18
000038h
000138h
IFS1<2>
IEC1<2>
IPC4<10:8>
CRC Generator
67
00009Ah
0001 9Ah
IFS4<3>
IEC4<3>
IPC16<14:12>
External Interrupt
00001 4h
000114h
IFS0<0>
IEC0<0>
IPC0<2:0>
External Interrupt 1
20
00003Ch
0001 3Ch
IFS1<4>
IEC1<4>
IPC5<2:0>
External Interrupt 2
29
00004Eh
00014Eh
IFS1<13>
IEC1<13>
IPC7<6:4>
External Interrupt 3
53
00007Eh
00017Eh
IFS3<5>
IEC3<5>
IPC13<6:4>
External Interrupt 4
54
000080h
0001 80h
IFS3<6>
IEC3<6>
IPC13<10:8>
I2C1 Master Event
17
000036h
0001 36h
IFS1<1>
IEC1<1>
IPC4<6:4>
I2C1 Slave Event
16
000034h
000034h
IFS1<0>
IEC1<0>
IPC4<2:0>
I2C2 Master Event
50
000078h
000178h
IFS3<2>
IEC3<2>
IPC12<10:8>
I2C2 Slave Event
49
000076h
000176h
IFS3<1>
IEC3<1>
IPC12<6:4>
Input Capture 1
1
000016h
000116h
IFS0<1>
IEC0<1>
IPC0<6:4>
Input Capture 2
5
00001 Eh
00011Eh
IFS0<5>
IEC0<5>
IPC1<6:4>
Input Capture 3
37
00005Eh
00015Eh
IFS2<5>
IEC2<5>
IPC9<6:4>
Input Capture 4
38
000060h
000160h
IFS2<6>
IEC2<6>
IPC9<10:8>
Input Capture 5
39
000062h
000162h
IFS2<7>
IEC2<7>
IPC9<14:12>
Input Change Notification
19
00003Ah
0001 3Ah
IFS1<3>
IEC1<3>
IPC4<14:12>
Output Compare 1
2
00001 8h
000118h
IFS0<2>
IEC0<2>
IPC0<10:8>
Output Compare 2
6
000020h
0001 20h
IFS0<6>
IEC0<6>
IPC1<10:8>
Output Compare 3
25
000046h
0001 46h
IFS1<9>
IEC1<9>
IPC6<6:4>
Output Compare 4
26
000048h
0001 48h
IFS1<10>
IEC1<10>
IPC6<10:8>
Output Compare 5
41
000066h
0001 66h
IFS2<9>
IEC2<9>
IPC10<6:4>
Parallel Master Port
45
00006Eh
00016Eh
IFS2<13>
IEC2<13>
IPC11<6:4>
Real-Time Clock/Calendar
62
000090h
0001 90h
IFS3<14>
IEC3<13>
IPC15<10:8>
SPI1 Error
9
000026h
000126h
IFS0<9>
IEC0<9>
IPC2<6:4>
SPI1 Event
10
000028h
000128h
IFS0<10>
IEC0<10>
IPC2<10:8>
SPI2 Error
32
000054h
0001 54h
IFS2<0>
IEC0<0>
IPC8<2:0>
SPI2 Event
33
000056h
0001 56h
IFS2<1>
IEC2<1>
IPC8<6:4>
Timerl
3
00001Ah
00011Ah
IFS0<3>
IEC0<3>
IPC0<14:12>
Timer2
7
000022h
000122h
IFS0<7>
IEC0<7>
IPC1<14:12>
Timer3
8
000024h
000124h
IFS0<8>
IEC0<8>
IPC2<2:0>
Timer4
27
00004Ah
0001 4Ah
IFS1<11>
IEC1<11>
IPC6<14:12>
Timer5
28
00004Ch
00014Ch
IFS1<12>
IEC1<12>
IPC7<2:0>
UART1 Error
65
000096h
0001 96h
IFS4<1>
IEC4<1>
IPC16<6:4>
UART1 Receiver
11
00002Ah
0001 2Ah
IFS0<11>
IEC0<11>
IPC2<14:12>
UART1 Transmitter
12
00002Ch
00012Ch
IFS0<12>
IEC0<12>
IPC3<2:0>
UART2 Error
66
000098h
0001 98h
IFS4<2>
IEC4<2>
IPC16<10:8>
UART2 Receiver
30
000050h
000150h
IFS1<14>
IEC1<14>
IPC7<10:8>
UART2 Transmitter
31
000052h
0001 52h
IFS1<15>
IEC1<15>
IPC7<14:12>
Table 5-1. Interrupt Vectors as implemented in the PIC24FJ128OA010 family.
At power on, all interrupt sources are disabled by default.
The Interrupt Flag (typically represented with a suffix -if). This single bit of data is set
each time the specific trigger event is activated, independently by the status of the enable bit.
Notice how, once set, it must be cleared (manually) by the user. In other words, it must be
cleared before exiting the interrupt service routine, or the same interrupt service routine will
be immediately called again.
56
Interrupts
The priority level (typically represented with a suffix -ip). Interrupts can have up to 7 levels
of priority. Should two interrupt events occur at the same time, the highest priority event will
be served first. Three bits encode the priority level of each interrupt source. At any given point
in time, the PIC24 execution priority level value is kept in the SR register in three bits referred
to as I PL . . IPL2 . Interrupts with a priority level lower than the current value of I PL
will be ignored. At power on, all interrupt sources are assigned a default level of four and the
processor priority is initially set at level zero.
Within an assigned priority level there is also a relative (default) priority among the various sources in
the fixed order of appearance in the IVT table.
Nesting of interrupts
Interrupts can be nested, so that a lower-priority interrupt service routine can be interrupted by a
higher-priority routine. This behavior can be controlled by the nstdis bit in the intconi register of
the PIC24.
When the nstdis bit is set, as soon as an interrupt is received the priority level of the processor (ipl)
is set to the highest level (7) independently of the specific interrupt level assigned to the event. This
prevents new interrupts from being serviced until the present one is completed. In other words, when
the nstdis bit is set, the priority level of each interrupt is used only to resolve conflicts, should mul-
tiple interrupts occur simultaneously, and all interrupts are serviced sequentially.
Traps
Eight additional vectors occupy the first locations on top of the IVT table. They are used to capture
special error conditions such as a failure of the selected CPU oscillator, an incorrect address (word
access to odd address), stack underflow, or a divide by zero (math error).
Vector Number
IVT Address
Trap Source
000004h
Reserved
1
000006h
Oscillator Failure
2
000008h
Address Error
3
OOOOOAh
Stack Error
4
OOOOOCh
Math Error
5
OOOOOEh
Reserved
6
00001 Oh
Reserved
7
00001 2h
Reserved
Table 5-2. TRAP vector details.
Since these types of errors have generally fatal consequences for a running application, they have been
assigned fixed priority levels above the seven basic levels available to all other interrupts. This also
means that they cannot be inadvertently masked (or delayed by the nstdis mechanism) and it provides
an extra level of security for the application. The MPLAB C30 compiler associates all trap vectors with
a single default routine that will produce a processor reset. You can change such behavior using the
same technique illustrated for all generic interrupt service routines.
57
Chapter 5
A template and an example for Timerl interrupt
This all might seem extremely complicated, but we will quickly see that, by following some simple
guidelines, we can put it to use in no time. Let's create a template, which we will reuse in future practi-
cal examples that demonstrate the use of the Timerl peripheral module as the interrupt source. We will
start by writing the interrupt service routine function:
// 1. Timerl interrupt service routine
void _ISR _TlInterrupt ( void)
{
// insert your code here
// . . .
// remember to clear the interrupt flag before exit
_T1IF = 0;
} //Tllnterrupt
We used the _isr macro just like before and made sure to declare the function type and parameters as
void. Remembering to clear the interrupt flag (_tiif) before exiting the function is extremely impor-
tant, as we have seen. In general, the application code should be very concise. The goal of any interrupt
service routine is to perform a simple task quickly and efficiently in rapid response to an event. As a
general rule, I would say that if you should find yourself writing more than a page of code (or con-
templating calling other functions) you should most probably stop and reconsider the goals and the
structure of your application. Lengthy calculations have a place in the main function and specifically in
the main loop, not inside an interrupt service routine where time is at premium.
Let's complete the template with a few lines of code that we will add to the main function:
main ( )
{
// 2. initializations
_TlIP = 4; // set Timerl priority, (4 is the default value)
TMR1 =0; // clear the timer
PRl = period-1; // set the period register
// 2.1 configure Timerl module clock source and sync setting
T1CON = 0x8000; // check T1CON register options
// 2.2 init the Timerl Interrupt control bits
_T1IF =0; // clear the interrupt flag, before
_T1IE = 1 ; // enable the Tl interrupt source
// 2.3 init the processor priority level
_IP =0; // is the default value
// 3. the main loop
while ( 1)
{
58
Interrupts
II your main code here. . .
// main loop
} // main
In 2, we assign a priority level to the Timer 1 interrupt source, although this might not be strictly neces-
sary, as we know that all interrupt sources are assigned a default level-four priority at power on. We
also clear the timer and assign a value to its period register.
In 2.1, we complete the configuration of the timer module, by turning the timer on with the chosen
settings.
In 2.2, we clear the interrupt flag just before enabling the interrupt source.
The interrupt-trigger event for the timer module is defined as the instant the timer value reaches the
value assigned to the period register. In that instant, the interrupt flag is set and the timer is reset to
begin a new cycle. If the interrupt-enable bit is set as well, and the priority level is higher than the pro-
cessor current priority (_ip), the interrupt service function is immediately called.
In 2.3, we initialize the processor priority level although, once more, this is not strictly necessary as the
processor priority is initialized to zero by default at power on.
In 3.0, we will insert the main loop code. If everything goes as planned, the main loop will execute
continuously, interrupted periodically by a brief call to the interrupt service routine.
A real example with Timerl
By adding only a couple of lines of code, we can turn this template into a more practical example
where Timerl is used to maintain a real-time clock, with tenths of a second, seconds and minutes. As a
simple visual feedback we can use the lower 8 bits of PORTA as a binary display showing the seconds
running. Here is what we need to add:
Before 1., add the declaration of a few new integer variables that will act as the seconds and
minutes counters:
int dSec = ;
int Sec = ;
int Min = ;
In 1.2, have the interrupt service routine increment the counter:
dSec++;
A few additional lines of code will be added to take care of the carry-over into seconds and
minutes.
In 2, set the period register for Timerl to a value that (assuming a 32-MHz clock) will give us
a tenth of a second period between interrupts.
PRl = 25000-1; // 25,000 * 64 * 1 cycle (62.5ns) = 0.1 s
• Set PORTA lsb as output:
TRISA = OxffOO;
In 2.1, set the Timerl prescaler to 1:64 to help achieve the desired period.
TlCON = 0x802 0;
59
Chapter 5
In 3., add code inside the main loop to continuously refresh the content of porta (lsb) with
the current value of the milliseconds counter.
PORTA = Sec;
The new project is ready to build:
#include <p24f j 128ga010 .h>
int dSec = ;
int Sec = ;
int Min = ;
// 1. Timerl interrupt service routine
void _ISR _TlInterrupt ( void)
{
// 1.1 your code here
dSec++; // increment the tens of a second counter
if ( dSec > 9) // 10 tens in a second
{
dSec = ;
Sec++; // increment the minute counter
if ( Sec > 59) // 6 seconds make a minute
{
Sec = 0;
// 1.2 increment the minute counter
Min++;
if ( Min > 59)// 59 minutes in an hour
Min = 0;
} // minutes
} // seconds
// 1.3 clear the interrupt flag
_T1IF = 0;
} //Tllnterrupt
main ( )
{
// 2. init Timer 1, TlON, 1:1 prescaler, internal clock source
_T1IP = 4; // this is the default value anyway
TMR1 =0; // clear the timer
PR1 = 25000-1;// set the period register
TRISA = OxffOO; // set PORTA lsb as output
60
Interrupts
II 2.1 configure Timerl module
T1C0N = 0x8020; // enabled, prescaler 1:64, internal clock
// 2.2 init the Timer 1 Interrupt, clear the flag, enable the source
_T1IF = 0;
_T1IE = 1;
// 2.3 init the processor priority level
_IP =0; // this is the default value anyway
// 3 . main loop
while ( 1)
{
// your main code here
PORTA = Sec;
} // main loop
} // main
Testing the Timerl interrupt
1 . Open the Watch window (dock it to your favorite spot).
2. Add the following variables:
- dSec, select from the Symbol pulldown box, then click on Add
- tmri , select from the SFR pulldown box, then click on Add
- sr, select from the SFR pulldown box, then click on Add
3. Open the Simulator Stopwatch window ("Debugger— > Stop Watch").
4. Set a breakpoint on the first instruction of the interrupt response routine after 1.1.
5. Set the cursor on the line and from the right click menu select: Set Breakpoint, or simply
double click. By setting the breakpoint here, we will be able to observe whether the interrupt
is actually being triggered.
6. Execute a Run ("Debugger— >Run" or F9). The simulation should stop quickly, with the
program counter cursor (the green arrow) pointing right at the breakpoint inside the interrupt
service routine.
So we did stop inside the interrupt service routine! This means that the trigger event was activated; that
is, the Timerl reached a count of 24,999 (remember though that the Timerl count starts with 0, there-
fore 25,000 counts have been performed) which, multiplied by the prescaler value, means that 25,000 x
64 or exactly 1.6 million cycles, have elapsed.
The Stopwatch window will confirm that the total number of cycles executed so far is, in fact, slightly
higher than 1 .6 million. The Stopwatch count includes the time required by the initialization part of our
61
Chapter 5
program too. At the PIC24's execution rate (16 million instructions per second or 62.5 ns per cycle)
this all happened in a tenth of a second!
From the Watch window, we can now observe the current value of processor priority level (ip). Since
we are inside an interrupt service routine that was configured to operate at level four, we should be able
to verify that bits 3, 4 and 5 of the status register (sr) contain exactly this value. For convenience, the
MPLAB IDE shows the completely decoded contents of the status register in a small box, as part of the
status bar located at the bottom of the main window.
- MPlAfl IDF vMO - MPI AB IDF Editor
f<k E* View Protect DebuQper Programmer Toots Configure W*xJow He<p
□ of y
>' _J Interruptsjmcp
B -J Source Files
3 lotenupts.c
Q Header files
_JCfc|ectfHK
U library FJes
LJ linker Scripts
£p24fJ]2SGA0l0,gld
_J other He*
Has *£ Symbctt
AdJSFR H OUGHS ^j [A.AiS,wbolj |_SP ~]
Address | Symbol ..
Value
0042 SR
0100 T1R1
0602 sec
OKpiai
oxsHT
OxSOOQ
Wetcht W«*^iJ W**2| W*«ch4|
Buid [v«oorv Control! FndnFtejl MPLAB SIM '
Inlefrupts.c
d c$ y s» # es i> ii w> th ip et- b&
Checksum: N/A
3
tf include :p24£]12egaO10.h:
lnt dSsc - 0;
int Sec ■ 0;
itit Bin - 0;
// 1. Timerl interrupt service routine
void _ISR JTlInterrupt raid)
(
// 1.1 your code here
dSee++; // inceement the tens of a second, counter
If dSec > 9) // 10 tens in a second
<
dSec - 0;
Sec++: // increment the seconds counter
it ( Sec > 59) it 60 seconds Make a minute
1
l
Sec - 0;
Hin++; // increment the minute counter
If ( Mm > 59)// 59 minutes in an hour
Hin - 0;
I // minutes
i ft seconds
ill
S
Moke: The targel 'C:\wariQC30\5 InlemipisVrHemjpis.cof is oul o1 date.
Executing: 'CW^rogram Ries\Microehip\MPlA8 C^0\bin\p«c30-gcc.exe*-Vr1. B C\woik\C30\S
Execulirtg: X^Program File6\Microchtp\MPLAB C30\bin\pk30-bin2hex«ice" "Intemipts.Gof
Loaded C:\work\C30\5 lnlefrupls\lnleiTup1s cof
BUILD SUCCEEDED; Wed Aug 09 21 :04. 44 2006
v!
Stopwatch
Total Simisted
| Syreh| lmlrucl»n CydM|~ 1G0006G [~
1600060
2HO | Tiw (msec*) | (^ 1QQQ0375Q |
100003750
Ptoceisa Frequency I MHi J
32C00CCO
MPLAB 9M
^1C24F3128GA010 pcOtfM |oab sAiB>4jbc rr ov z C
|Ui 17, Coi 1 (INS |VVR
Figure 5-1 . Screenshot showing the processor status after Timerl interrupt.
In Figure 5-1,1 have circled the IP indication in the status bar (showing IP4 to indicate interrupt
priority level four) as well as the sr register content and the Stopwatch actual value (in milliseconds).
Single stepping from the current position (using either the StepOver or the Stepln commands), we can
monitor the execution of the next few instructions inside the interrupt service routine. Upon its comple-
tion, we can observe how the priority level returns back to the initial value — look for the IPO indication
in the status bar and the sr register bits 5, 6 and 7 to be cleared.
7. After executing another Run command, we should find ourselves again with the program
counter (represented graphically by the green arrow) pointing inside the interrupt service rou-
tine. This time, you will notice how exactly 1.6 million cycles have been added to the previous
count.
62
Interrupts
8. Add the Sec and Min variables to the Watch window.
9. Execute the Run command a few more times to verify that, after 10 iterations, the seconds
counter is incremented.
To test the minutes increment, you might want to remove the current breakpoint and place a new one a
few lines below — otherwise you will have to execute the Run command exactly 600 times !
10. Place the new breakpoint on the Min++ statement in 1.2.
11. Execute Run once and observe that the seconds counter has already been cleared.
12. Execute the StepOver command once and the minute counter will be incremented.
The interrupt routine has been executed 600 times, in total, at precise intervals of one tenth of a second.
Meanwhile, the code present in the main loop has been executed continuously to use the vast majority
of the grand total of 960 million cycles. In all honesty, our demo program did not make much use of all
those cycles — wasting them all in a continuous update of the PORTA content. In a real application, we
could have performed a lot of work, all the while maintaining a precise real-time clock count.
The secondary oscillator
There is another feature of the PIC24 Timer 1 module (common to all previous generations of 8-bit
PIC® microcontrollers) that we could have used to obtain a real-time clock. In fact, there is a low-
frequency oscillator (known as the secondary oscillator) that can be used to feed just the Timer 1
module instead of the high-frequency main clock. Since it is designed for low-frequency operation
(typically it is used in conjunction with an inexpensive 32,768-Hz crystal), it requires very little power
to operate. And since it is independent from the main clock circuit, it can be maintained in operation
when the main clock is disabled and the processor enters one of the many possible low-power modes.
In fact, the secondary oscillator is an essential part for many of those low-power modes. In some
cases it is used to replace the main clock, while in others it remains active only to feed the Timer 1 or a
selected group of peripherals.
To convert our previous example for use with the secondary oscillator, we will need to perform only a
few minor modifications, such as:
change the interrupt routine to count only seconds and minutes (the much slower clock rate
does not require the extra step for the tenth of a second).
// 1. Timerl interrupt service routine
void _ISR _TlInterrupt ( void)
{
// 1.1 clear the interrupt flag
_T1IF = 0;
// 1.2 your code here
Sec++; // increment the seconds counter
if ( Sec > 59) // 60 seconds make a minute
{
Sec = 0;
Min++; // increment the minute counter
63
Chapter 5
if ( Min > 59)// 59 minutes in an hour
Min = 0;
} // minutes
} //Tllnterrupt
in 2, change the period register to generate one interrupt every 32,768 cycles.
PRl = 32768-1;// set the period register
in 2.1, change the Timer 1 configuration word (the prescaler is not required anymore).
TlCON = 0x8002; // enabled, prescaler 1:1, use secondary oscillator
Unfortunately, you will not be able to immediately test this new configuration with the simulator, since
the secondary oscillator input is not automatically simulated.
In a later lesson, we will learn how a new set of tools will help us to generate a stimulus file that could
be used to provide a convenient emulation of a 32-kHz crystal connected to the TICK and SOSCI pins
ofthePIC24.
The real-time clock calendar (RTCC)
Building on the previous two examples, we could evolve the real-time clock implementations to in-
clude the complete functionality of a calendar, adding the count of days, day of the week, months and
years. These few new lines of code would be executed only once a day, once a month or once a year,
and therefore would produce no decrease in the performance of the overall application whatsoever.
Although it would be somewhat entertaining to develop such code, considering lapsed years and work-
ing out all the details, the PIC24FJ128GA010 already has a complete Real-time Clock and Calendar
(RTCC) module built in and ready for use. How convenient! Not only does it feed from the same
low-power secondary oscillator, but it comes with all the bells and whistles, including a built-in Alarm
function that can generate interrupts. In other words, once the module is initialized, it is possible to ac-
tivate the RTCC alarm and wait for an interrupt to be generated, for example, on the exact month, day,
hour, minute and second desired once a year (or if set on February 29th, even once every four years!).
This is what the interrupt service routine would look like:
// 1. RTCC interrupt service routine
void _ISR _RTCCInterrupt ( void)
{
// 1.1 clear the interrupt flag
_RTCIF = 0;
// 1.2 your code here, will be executed only once a year
// that is once every 365 x 24 x 60 x 60 x 16,000,000 MCU cycles
// that is once every 504,576,000,000,000 MCU cycles
} // RTCCInterrupt
Managing multiple interrupts
It is typical of an embedded-control application to require several interrupt sources to be serviced. For
example, a serial communication port might require periodic attention at the same time that a PWM
module is active and requires periodic updates to control an analog output. Multiple timer modules
64
Interrupts
might be used simultaneously to produce pulsed outputs, while multiple inputs could be sampled by
the analog-to-digital converter and their values would need to be buffered. There is almost no limit to
the number of things that can be done with 118 interrupt sources available. At the same time, there is
no limit to the complexity of the bugs that can be generated, thanks to the same sophisticated mecha-
nisms, if a little discipline and some common sense are not applied.
Here are some of the rules to keep in mind:
1. Keep it short and simple. Make sure the interrupt routines are the shortest/fastest possible, and
under no circumstances should you attempt to perform any processing of the incoming data.
Limit the activity to buffering, transferring and flagging.
2. Use the priority levels to determine which event deserves to be serviced first, in case two
events are triggered simultaneously.
3. But consider very carefully whether you want to face the additional complexity and headaches
that result from enabling the use of nested interrupt calls. After all, if the interrupt service rou-
tines are short and efficient, the extra latency introduced by waiting for the current interrupt
to be completed before a new one is serviced is going to be extremely small. If you determine
that you don't need it that bad, make sure the nstdis control bit is set to prevent nesting:
_NSTDIS = 1; // disable interrupt nesting (default)
Post-flight briefing
In this lesson, we have seen how an interrupt service routine can be simple to code, thanks to the
language extensions built into the C30 compiler and the powerful interrupt-control mechanisms offered
by the PIC24 architecture. Interrupts can be an extremely efficient tool in the hands of the embed-
ded-control programmer, to manage multiple tasks while maintaining precious timing and resources
constraints. At the same time, they can be an extremely powerful source of trouble. In the PIC24 refer-
ence manual and the MPLAB C30 User Guide, you will find more useful information than we could
possibly cram into one single lesson. Finally, in this lesson we took the opportunity to learn more about
the uses of Timer 1 and the secondary oscillator, and we got a glimpse of the features of the new Real-
Time Clock and Calendar (RTCC) module.
Notes for C experts
The interrupt vector table (IVT) is an essential part of the CO code segment for the PIC24. Actually two
copies of it are required to be present in the first 256 locations of the program memory — one is used
during normal program execution and the second (or Alternate IVT) during debugging. These tables
account for most of the size of the CO code in all the examples we have been developing in these first
five lessons. Subtract 256 words (or 768 bytes) from the file size of each example to obtain the "net"
code size.
Notes for assembly experts
The _isrfast macro can be used to declare a function as an interrupt service routine, and to further
specify that it will use an additional and convenient feature of the PIC24 architecture: a set of four
shadow registers. By allowing the processor to automatically save the content of the first four work-
ing registers (wo-w3-i.e., the most frequently used ones) and most of the content of the sr register in
special reserved locations, without requiring the use of the stack, the shadow registers provide the fast-
65
Chapter 5
est possible interrupt response time. Naturally, since there is only one set of such registers, their use is
limited to applications where only one interrupt will be served at any given time. This does not limit us
to use only one interrupt in the entire application, but rather to use _isrfast only in applications that
have all interrupts with the same priority level or, if multiple levels are in use, reserve the _isrfast
options only for the interrupt service routines with the highest level of priority.
Notes for PIC microcontroller experts
Notice that on the PIC24 architecture there is no single control bit that disables all interrupts, but there
is an instruction (disi) that can disable interrupts for a limited number of cycles. If there are portions
of code that require all interrupts to be temporarily disabled, you can use the following inline assembly
command:
asm volatile ( "disi #0x3FFF"); // disable temporarily all interrupts
// your code here
// . . .
DISICNT =0; // re-enable all interrupts
Tips and tricks
According to the PIC24 datasheet, to activate the secondary low-power oscillator you need to set the
soscen bit in the osccon register. But before you rush to type the code in the last example and try to
execute it on a real target board, notice that the osccon register, containing vital controls for the MCU
affecting the choice of the main active oscillator and its speed, is protected by a locking mechanism.
As a safety measure, you will have to perform a special unlock sequence first or your command will be
ignored. Here is an example, using inline assembly:
// OSCCON unlock sequence, setting SOSCEN
asm volatile
asm volatile
asm volatile
asm volatile
asm volatile
asm volatile
asm volatile
"mov #0SCC0N,W1")
"mov.b #0x46, W2")
"mov.b #0x57, W3")
"mov.b #0x02, W0")
"mov.b W2, [Wl] ") ;
"mov.b W3, [Wl] ") ;
"mov.b W0, [Wl] ") ;
// SOSCEN = 1
A similar combination lock mechanism has been put in place to protect the key RTCC register rcfg-
cal. A special bit must be set (rtcwren) to allow writing to the register, but this bit requires its own
special unlock sequence to be executed first. Here is an example using, once more, inline assembly
code:
// RCFGCAL unlock sequence, setting RTCWREN
asm volatile ( "disi #5");
asm volatile ( "mov #0x55, w7") ;
asm volatile ( "mov w7,_NVMKEY") ;
asm volatile ( "mov #0xAA, w8") ;
asm volatile ( "mov w8,_NVMKEY") ;
66
Interrupts
asm volatile ( "bset
asm volatile ( "nop"
asm volatile ( "nop"
_RCFGCAL, #13"
// RTCWREN = 1;
After these two steps, which initialize the RTCC, setting the date and time is trivial:
_RTCEN =0; // disable the module
// example set 12/01/2006 WED 12:01:30
_RTCPTR = 3;
RTCVAL = 0x2 006
RTCVAL = 0x110
RTCVAL = 0x0312
RTCVAL = 0x013
// start the loading sequence
// YEAR
// MONTH- 1/ DAY- 1
// WEEKDAY /HOURS
// MINUTES /SECONDS
// optional calibration
//_CAL = 0x00;
// enable and lock
_RTCEN = 1; // enable the module
_RTCWREN =0; // lock settings
Setting the alarm does not require any special unlock combination. Here is an example that will help
you remember my birthday:
// disable alarm
_ALRMEN = 0;
// set the ALARM for a specific day of the year (my birthday
_ALRMPTR = 2; II start the sequence
ALRMVAL = 0x1124
ALRMVAL = 0x0006
ALRMVAL = 0x0000
// MONTH- 1/ DAY- 1
// WEEKDAY /HOUR
// MINUTES /SECONDS
// set the repeat counter
_ARPT =0; // once
_CHIME = 1; // indefinitely
// set the alarm mask
_AMASK = OblOOl; // once a year
_ALRMEN = 1;
RTCIF = 0;
_RTCIE = 1;
// enable alarm
// clear interrupt flag
// enable interrupt
67
Chapter 5
Exercises
Write interrupt-based routines for the following applications:
1. Serial port software emulation.
2. Remote-control radio receiver.
3. NTSC video output (Hint: in a few chapters, you'll find the solution).
Books
• Curtis, K. E. (2006)
Embedded Multitasking
Newnes, Burlington, MA
Keith knows multitasking and what it takes to create small and efficient embedded-control
applications.
• Brown, G. (2003)
Flying Carpet, The Soul of an Airplane
Iowa State Press, Ames, 10
Greg has many fun episodes from the real life of a general aviation pilot that uses his plane for
recreation as well as family utility.
Links
http://www. aopa. org
This is the web site of the Aircraft Owners and Pilots Association. Feel free to browse through
the web site and access the many magazines and free services offered by the association. You
will find a lot of useful and interesting information in here.
68
CHAPTER
6
Taking a look under the hood
In This Chapter
Memory space allocation
►
Looking at the MAP
Program space visibility
►
Pointers
Investigating memory
►
The heap
allocation
►
MPLAB® C30 memory models
Whether you are trying to get a driver's license or a pilot license, sooner or later you have to start look-
ing under the hood, or the cowling for pilots. You don't have to understand how each part of the engine
works, nor how to fix it — mechanics will be happy to do that for you. But a basic understanding of
what is going on will help you be a better driver/pilot. If you understand the machine, you can control
it better — it's that simple. You can diagnose little problems, and you can do a little maintenance.
Working with a compiler is not that dissimilar; sooner or later you have to start looking under the hood
if you want to get the best performance out of it. Since the very first lesson, we have been peeking
inside the engine compartment, but this time we will delve into a little bit more detail.
Flight plan
In this lesson we will review the basics of string declaration as an excuse to introduce the memory-
allocation techniques used by the MPLAB C30 compiler. The RISC architecture of the PIC24 poses
some interesting challenges and offers innovative solutions. We will use several tools, including the
Disassembly Listing window, the Program Memory window and the Map file to investigate how the
MPLAB C30 compiler and linker operate in combination to generate the most compact and efficient
code.
Pref light checklist
This lesson will be performed exclusively with software tools including the MPLAB IDE, MPLAB
C30 compiler and the MPLAB SIM simulator.
Use the "New Project Set-up" checklist to create a new project called "Strings" and a new source file
similarly called "strings . c".
69
Chapter 6
The flight
Strings are treated in C language as simple ASCII character arrays. Every character composing a string
is assumed to be stored sequentially in memory in consecutive 8-bit elements of the array. After the last
character of the string an additional byte containing a value of zero (represented in a character notation
with '\0') is added as a termination flag.
Notice though, that this is just a convention that applies to the standard C string manipulation library
"string. h". It would be entirely possible, for example, to define a new library and store strings in
arrays where the first element is used to record the length of the string — in fact, Pascal programmers
would be very familiar with this method. Also, if you are developing "international" applications —
i.e., applications that communicate using languages that require large character sets (like Chinese,
Japanese, Korean) — you might want to consider using Unicode, a technology that allocates multiple
bytes per character, in place of plain ASCII. The MPLAB C30 library "stdlib.h" provides basic
support for the translation from/to multibyte strings according to the ANSI90 standard.
Let's get started by reviewing the declaration of a variable containing a single character:
char c ;
As we have seen from the previous lessons, this is how we declare an 8-bit integer (character), that is
treated as a signed value (-128. . . + 127) by default.
We can declare and initialize it with a numerical value:
char c = 0x41;
Or, we can declare and initialize it with an ASCII value:
char c = ' a ' ;
Note the use of the single quotes for ASCII character constants. The result is the same, and to the C
compiler there is absolutely no distinction between the two declarations — characters ARE numbers.
We can now declare and initialize a string as an array of 8-bit integers (characters):
char s [ 5 ] = { ' H ' , ■ E ' , ' L ' , ' L ' , ' ' } ;
In this example, we initialized the array using the standard notation for numerical arrays. But, we could
have also used a far more convenient notation (a shortcut) specifically created for string initializations:
char s [5] = "HELLO" ;
To further simplify things, and save you from having to count the number of characters composing the
string (thus preventing human errors), you can use the following notation:
char s [] = "HELLO" ;
The MPLAB C30 compiler will automatically determine the number of characters required to store
the string, while automatically adding a termination character (zero) that will be useful to the string
manipulation routines later to correctly identify the string length. So, the example above is, in truth,
equivalent to the following declaration:
char s [ 6 ] = { ' H ' , ' E ■ , ' L ' , ' L ' , 'O 1 , ' \ ' };
70
Taking a look under the hood
Assigning a value to a char (8-bit integer) variable and performing arithmetic on it is no different than
performing the same operation on any integer type:
char c ; / / declare c as an 8-bit signed integer
c = 'a'; // assign to it the value corresponding to 'a' in the ASCII table
C++; // increment it. . . it will represent the ASCII character 'b' now
The same operations can be performed on any element of an array of characters (string), but there is
no simple shortcut, similar to the one used above for the initialization that can assign a new value to an
entire string:
char s[15] ; // declare s as a string of 15 characters
s = "Hello!"; // Error! This does not work!
Including the "string . h" file at the top of your source file, you'll gain access to numerous useful
functions that will allow you to:
copy the content of a string onto another:
strcpy( s, "HELLO"); // s : "HELLO"
append (or concatenate) two strings:
strcat( s, " WORLD");// s : "HELLO WORLD"
determine the length of a string:
i = strlen( s) ; // i : 11
and many more.
Memory space allocation
Just as with numerical initializations, every time a string variable is declared and initialized as in:
char s[] = "Flying with the PIC24";
three things happen:
1. the MPLAB C30 linker reserves a contiguous set of memory locations in RAM (data space)
to contain the variable: 22 bytes in the example above. This space is part of the ndata (near)
data section.
2. the MPLAB C30 linker stores the initialization value in a 22-byte long table (in program
memory). This space is part of the init code section.
3. the MPLAB C30 compiler creates a small routine that will be called before the main program
(part of the CO code we mentioned in previous chapters) to copy the values from code to data
memory, therefore initializing the variable.
In other words, the string "Flying with the PIC24" ends up using twice the space you would expect, as
a copy of it is stored in Flash program memory and space is reserved for it in RAM memory, too. Ad-
ditionally, you must consider the initialization code and the time spent in the actual copying process. If
the string is not supposed to be manipulated during the program, but is only used "as is," transmitted to
a serial port or sent to a display, then there is no need to waste precious resources. Declaring the string
as a "constant" will save RAM space and initialization code/time:
71
Chapter 6
const char s[] = "Flying with the PIC2 4";
Now, the MPLAB C30 linker will only allocate space in program memory, in the const code section,
where the string will be accessible via the Program Space Visibility window — an advanced feature of
the PIC24 architecture that we will review shortly.
The string will be treated by the compiler as a direct pointer into program memory and, as a conse-
quence, there will be no need to waste RAM space.
In the previous examples of this lesson, we saw other strings implicitly defined as constants:
strcpy( s, "HELLO") ;
The string "HELLO" was implicitly defined as of const char type, and similarly assigned to the
const section in program memory to be accessible via the Program Space Visibility window.
Note that, if the same constant string is used multiple times throughout the program, the MPLAB C30
compiler will automatically store only one copy in the const section to optimize memory use, even if
all optimization features of the compiler have been turned off.
Program space visibility
The PIC24 architecture is somewhat different from most other 16-bit microcontroller architectures you
might be familiar with. It was designed for maximum efficiency according to the Harvard model, as op-
posed to the more common Von Neumann model. The big difference between the two is that there are two
completely separated and independent buses available, one for access to the Program Memory (Flash) and
one for access to the Data Memory (RAM). The net result is a doubled bandwidth; while the data bus is in
use during the execution of one instruction, the program memory bus is available to fetch the next instruc-
tion code and initiate the decoding. In traditional Von Neumann architectures, the two activities must be
interleaved instead, with a consequent performance penalty. The drawback of this architectural choice is
that access to constants and data stored in program memory requires special considerations.
The PIC24 architecture offers two methods to read data from program memory: using special table
access instructions (tblrd) and through a second mechanism, called the Program Space Visibility or
PSV This is a window of up to 32K bytes of program memory accessible from the data memory bus.
In other words the PSV is a bridge between the program memory bus and the data memory bus.
Program Memory Space (Flash)
24-bit wide
Data Memory Space (RAM)
16-bit wide
< ►
0x0000
PSVPAG
OxFFFFFF
16-bit wide
< ►
32 Kbyte block
OxFFFF
Figure 6-1. PIC24FJ128GA010 Program Space Visibility window.
72
Taking a look under the hood
Notice that the PIC24 uses a 24-bit wide program memory bus but operates only on a 16-bit wide data
bus. The mismatch between the two buses makes the PSV "bridge" a little more interesting. In practice
the PSV connects only the lower 16 bits of the program memory bus to the data memory bus. The upper
portion (8 bits) of each program memory word is not accessible with the PSV window. On the contrary,
when using the table-access instructions, all parts of the program memory word become accessible, but
at the cost of having to differentiate the manipulation of data in RAM (using direct addressing) from the
manipulation of data in program memory (using the special table-access instructions).
The PIC24 programmer can therefore choose between a more convenient but relatively memory-inef-
ficient method for transferring data between the two buses such as the PSV, or a more memory-efficient
but less-transparent solution offered by the table-access instructions.
The designers of the MPLAB C30 compiler considered the trade-offs and chose to use both mecha-
nisms, although to solve different problems at different times:
the PSV is used to manage constant arrays (numeric and strings) so that a single type of
pointer (to the data memory bus) can be used uniformly for constants and variables.
the table-access mechanism is used to perform the variable initializations (limited to the CO
segment) for maximum compactness and efficiency.
Investigating memory allocation
We will start investigating these issues with the MPLAB SIM simulator and the following short snippet
of code:
/*
** Strings
*/
#include <p24f j 128ga010 .h>
#include <string.h>
// 1. variable declarations
const char a[] = "Learn to fly with the PIC24";
char b [ 1 ] =
// 2. main program
main ( )
{
strcpy( b, "MPLAB C30"); // assign new content to b
} //main
Now, follow these steps:
1. Build the project using the Project Build checklist.
2. Add the Watch window (and dock it to the preferred position).
3. Select the two variables "a" and "b" from the symbol selection box and click "Add Symbol"
to add them to the Watch window.
73
Chapter 6
Watch
AddSFR
ADCBUFu -
AddSymbdl
*l
Address
Symbo
Value
00296
0800
L+j a
S b
"Learn to fly with the PIC24"
Watch 1 Watch 2
Watch 3
Watch 4
Figure 6-2. Adding arrays to the Watch window.
A little "+" symbol enclosed in a box will identify these variables as arrays and will allow you to ex-
pand the view to identify each individual element.
Watch
Add SFR
*I
ADCBUFO *
AddSymbdl
Address
002 9 6
00296
00297
00298
00299
0029A
0029B
0029C
0029D
0029E
0029F
002 AO
002A1
Symbo, ,
- a
Value
"Learn to fly with the PIC24
[0]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10]
[11]
| Watch 1 Watch 2 1 WatchI
L
e
a
r
p
%
Q
i
1
y
Watch 4
Figure 6-3. Expanding an array in the Watch window.
By default MPLAB shows each element of the array as an ASCII character, but you can change the
display to reflect your personal preferences:
4. Select one element of the array with the left button of your mouse.
5. Right click to show the Watch window menu.
6. Select "Properties" (the last item in the menu).
You will be presented with the Watch window Properties dialog box.
74
Taking a look under the hood
Figure 6-4. Watch window properties dialog box.
From this dialog box you can change the format used to display the content of the selected array ele-
ment, but you can also observe the "Memory" field (grayed) that tells you where the selected variable
is allocated: data or code space.
If you select the Properties dialog box for the constant string "a", you will notice that the memory
space is indicated as "Program", confirming that the constant string is using only the minimum amount
of space required in the Flash program memory of the PIC24 and will be accessed through the PS V so
that no RAM needs to be assigned to it.
On the contrary, the Properties dialog box will reveal how the string "b" is allocated in a File register,
or in other words RAM memory.
Continuing our investigation, notice how the string "a" appears to be already initialized, as the Watch
window shows it's ready to use, right after the project build.
The string "b", on the contrary, appears to be still empty, and uninitialized. Only when we set the
cursor on the first line of code inside the main routine and we execute a Run To Cursor command, the
string "b" is initialized with the proper value.
75
Chapter 6
Watch
AddSFR
ADCBUFO _*J | Add Symbol |
*l
SP
Address
Syiribo. . .
Value
00296 B a
0800 ® b
"Learn to fly with the PIC24"
"Initialized"
Watch 1 ''v'/atch 2
Watch 3
Watch 4
_
Figure 6-5. Array "b" initialized.
As we have seen, "b" is allocated in RAM space, and the CO segment of code must be executed first for
the variable to be initialized and "ready for use."
Just a warning — the Watch window aligns all strings to the right, so if there is a long string (like
"a" in our example) and the window is too narrow, you might not be able to see the content of other
variables containing shorter strings. Undock the Watch window if necessary, and resize it to be able
to see the entire Value column.
Once more we can make use of the Disassembly Listing window to observe the code produced by the
compiler:
— c
l
2
3
4
5
6
7
9
10
11
12
13
14
15
0028A
16:
0028C
0028E
\work\C30\6 StringsXStrings . c
/*
** Strings
*/
FA0000
282B21
208000
#include <p24f jl28ga010 .h>
#include <string.h>
// 1. variable declarations
const char a[] = "Learn to fly with the PIC24";
char b[100] = "Initialized";
// 2. main program
main ( )
{
Ink #0x0
strcpy( b, "MPLAB C30");
mov.w #0x82b2, 0x0002
mov.w #0x800,0x0000
// assign new content to b
76
Taking a look under the hood
00290
07FFF7
rcall 0x0
17:
18:
} // main
00292
FA8 00
ulnk
00294
060000
return
c : \pic3 0-build\build_2 060131\src\standardc\sxl\strcpy . c
00280 780100
00282 784931
00284 E00432
00286 3AFFFD
00288 060000
We can see the main i
bottom of the listing.
mov.w 0x0000,0x0004
mov.b [0x0002++] , [0x0004]
cpO.b [0x0004++]
bra nz, 0x000282
return
function and the strcpy ( ) library function full disassembly appended at the
Notice how compact the code is produced for the strcpy ( ) routine, barely five instructions. You will
also appreciate how this is the only routine attached. Although the "string. h" library contains dozens
of functions, and the include file "string . h" contains the declarations for all of them, the linker is
wisely appending only the functions that are actually being used.
What the Disassembly Listing window does not show, though, is the initialization code CO. As men-
tioned in a previous chapter, in order to see it, you will have to rely on the Program Memory window
(I recommend you select the Symbolic view tab at the bottom). There the most curious and patient
readers will discover how the initialization of the string "b" is performed using the Table Read (tblrd)
instructions to extract the data from the program memory (Flash) and to store the values in the allo-
cated space in data memory (RAM).
Looking at the MAP
Another tool we have at our disposal to help us understand how strings (and in general any array vari-
able) are initialized and allocated in memory is the "map file". This text file, produced by the MPLAB
C30 linker, can be easily inspected with the MPLAB editor and is designed specifically to help you
understand and resolve memory allocation issues.
To find this file, look for it in the main project directory where all the project source files are. Select
"File— >Open" and then browse until you reach the project directory. By default the MPLAB editor will
list all the \c" files, but you can change the File Type field to " .map".
All Source Files r.c;".h; M asm;".as;"Jnc; M s;".bas ▼ |
Alf Source Files (^c; H .h^asm^as^inc;*.sj".bas; H .s<
Assembly Source Files {*.a$m;*.a$;*.inc;*.$)
C Souice Files fjsf. h)
Basic Source Files t H .bas;".inc)
SCL Source Files (" .scl)
Linker Files (Mnie.lki^gld)
List Files fJst
Figure 6-6. Selecting the ".map" file type.
77
Chapter 6
Map files tend to be pretty long and verbose but, by learning to inspect only a few critical sections, you
will be able to find a lot of useful data. The Program Memory Usage summary, for example, is found
among the very first few lines:
Program Memory Usage
section
address
. reset
.ivt
0x4
.aivt
0x104
. text
0x2 00
. const
0x2 9 6
. dinit
0x2bc
. isr
0x308
length (PC units
length (bytes) (dec
0x4
0x6
(6)
Oxfc
0x17a
(378)
Oxfc
0x17a
(378)
0x96
Oxel
(225)
0x2 6
0x3 9
(57)
0x4c
0x72
(114)
0x2
0x3
(3)
Total program memory used (bytes
0x489 (1161) <1
This is a list of small sections of code assembled by the MPLAB C30 linker in a specific order (dic-
tated by the . gld linker script file) and position.
Most section names are pretty intuitive, other are... historical:
the . reset section is where the reset vector is placed.
the .ivt is the Interrupt Vector Table, discussed in the previous chapter.
the .aivt is the Alternate Interrupt Vector Table.
the .text section is where all the code generated by the MPLAB C30 compiler from your
source files will be placed (the name of this section has been used since the original imple-
mentation of the very first C compiler).
the .const section is where the constants (integers and strings) will be placed for access via
the PSV
the .dinit section is where the variables initialization data (used by the CO code) will be
found.
the .isr is where the Interrupt Service Routine (in this case a default one) will be found.
It's in the .const section that the "a" constant string, as well as the "MPLAB C30" (implicit) constant
string, are stored for access via the PSV window.
You can confirm this by inspecting the Program Memory window at the address 0x296.
Note the two-by-two character grouping; remember how the PSV allows us to use only 16 bits of each
24-bit program memory word.
00290
00298
002A0
002 AS
002B0
002B8
Q7FFF7 FA8000 060000 00654C . .
007261 00206E 006F74 006620 ar
00796C 007720 007469 002068 ly
006874 002065 004950 003243 th
000034 00504D 00414C 002042 4.
003343 000030 000800 000064 C3
. . . . .
. .
.Le. .
. . n .
. to
. f . .
. . w.
. it
.h . .
. . e .
. PI
.C2 . .
. .HP.
. LA
.B . .
. .0. .
.d. . .
78
Taking a look under the hood
In .dinit is where the "b" variable initialization string, will be found. It is prepared for access via the
table instructions, so it uses each and every one of the 24 bits available in each program memory word.
Note the character grouping in three by three:
002C0 000002 696E49 616974 7A696C . . . . Ini . tia.liz.
002C8 006465 000000 000000 000000 ed
002D0 000000 000000 000000 000000
The next part of the map file we might want to inspect is the Data Memory Usage (RAM) summary:
Data Memory Usage
section address alignment gaps total length (dec)
.ndata 0x800 0x64 (100)
Total data memory used (bytes) : 0x64 (100) 1%
In our simple example, it contains only one section: .ndata, and in it, only one variable "b" for which
100 bytes are reserved starting at the address 0x800, the first location available in the PIC24 RAM.
Pointers
Pointers are variables used to refer indirectly (point to) other variables or part of their contents. Point-
ers and strings go hand in hand in C programming, as they are in general a powerful mechanism to
work on any array data type. So powerful, in fact, that they are also one of the most dangerous tools in
the programmer's hands and a source of a disproportionately large share of programming bugs. Some
programming languages, like Java, have gone to the extreme of banning completely the use of pointers
in an effort to make the language more robust and verifiable.
The MPLAB C30 compiler takes advantage of the PIC24 16-bit architecture to manage with ease large
amounts of data memory (up to 32 kbytes of RAM available in current models). In particular, thanks
to the PSV window, the MPLAB C30 compiler doesn't make any distinction between pointers to data
memory objects and const objects allocated in program memory space. This allows a single set of
standard functions to manipulate variables and/or generic memory blocks as needed from both spaces.
The following classic program example will compare the use of pointers versus indexing to perform
sequential access to an array of integers:
int *pi; // define a pointer to an integer
int i; // index/ counter
int a [10]; // the array of integers
// 1. sequential access using array indexing
for( i=0; i<10; i++)
a [ i ] = i ;
// 2. sequential access using a pointer
pi = a;
for( i=0; i<10; i++)
{
79
Chapter 6
*pi = i;
}
In 1. we performed a simple for loop and at each round in the loop we used "i" as an index in the ar-
ray. To perform the assignment, the compiler will have to take the value of "i", multiply it by the size
of the array element in bytes ( 2), and add the resulting offset to the initial address of the array "a" .
In 2. we initialized a pointer to point to the initial address of the array "a". At each round in the loop
we simply used the pointer (*) to perform the assignment, and then we just incremented the pointer.
Comparing the two cases, we see how, by using the pointer, we can save at least one multiplication step
for each round in the loop. If inside the loop the array element is used more times, the performance
improvement is going to be proportionally greater.
Pointers syntax can become very "concise" in C, allowing for some effective code to be written, but
also opening the door to more bugs.
At a minimum, you should become familiar with the most common contractions. The previous snippet
of code is more often reduced to the following:
// 2. sequential access to array using pointers
f or ( i=0, p=a; i<10; i++)
*pi++ = i;
Also note that an empty pointer — that is, a pointer without a target — is assigned a special value null,
which is implementation specific and defined in "stddef .h".
The heap
One of the advantages offered by the use of pointers is the ability to manipulate objects that are defined
dynamically (at run time) in memory. The "heap" is the area of data memory reserved for such use, and
a set of functions, part of the standard C library "stdlib.h", provides the tools to allocate and free the
memory blocks. They include at a minimum the fundamental functions:
void *malloc (size_t size);
takes a block of memory of requested size from the heap, and returns a pointer to it.
void free (void *ptr) ;
returns the block of memory pointed to by ptr to the heap.
The MPLAB C30 linker places the heap in the RAM memory space left unused above all project
global variables and the reserved stack space. Although the amount of memory left unused is known
to the linker and listed in the map file of each project, you will have to explicitly instruct the linker to
reserve an exact amount for use by the heap.
Use the "Project^BuildOptions^Project" menu command to open the Build Options dialog box,
select the MPLAB Link30 tab, and define the heap size in bytes.
As a general rule, allocate the largest amount of memory possible, as this will allow the malloc ( )
function to make the most efficient use of the memory available. After all, if it is not assigned to the
heap it will remain unused.
80
Taking a look under the hood
MPLAB C30 memory models
The PIC24 architecture allows for a very efficient (compact) instruction encoding for all operations
performed on data memory within the first 8 kbytes of addressing space. This is referred to as the
"near" memory area and in the case of the PIC24FJ128GA010 it corresponds to the group of SFRs
(first 2 kbytes) and the following 6 kbytes of general-purpose RAM. Only the top 2 kbytes of RAM are
actually outside the near space.
Access to memory beyond the 8-kbyte limit requires the use of indirect addressing methods (pointers)
and could be less efficient if not properly planned. The stack (and with it all the local variables used
by C functions) and the heap (used for dynamic memory allocation) are naturally accessed via point-
ers and are correspondingly ideal candidates to be placed in the upper RAM space. This is exactly
what the linker will attempt to do by default. It will also try to place all the global variables defined in
a project in the near memory space for maximum efficiency. If a variable cannot be placed within the
near memory space, it has to be "manually" declared with a "far" attribute, so that the compiler will
generate the appropriate access code. This behavior is referred to as the Small Data Memory Model
as opposed to the Large Memory model, where each variable is assumed to be far unless the "near"
attribute is specified.
In practice, while using the PIC24FJ128GA010, you will use almost uniquely the default Small Mem-
ory model and in rare occasions you will find it necessary to identify a variable with the "far" attribute.
In lesson number 12, we will observe one such case, where a very large array that would otherwise not
fit in the near memory space will have to be declared as "far". As a consequence, not only will the
compiler generate the correct addressing instructions, but the linker will also push it to an upper area of
RAM, giving priority to the other global variables and allowing them to be accessed in the near space.
Since access to elements of an array (explicitly via pointers or by indexing) is performed via indirect
addressing anyway, there will be no performance or code size penalty.
A similar concept applies to the program memory space. In fact, within each compiled module, func-
tions are called by making use of a more compact addressing scheme that relies on a maximum range
of 32 kbytes. Program memory models (small and large) define the default behavior of the compiler/
linker with regards to the addressing of functions within or outside such 32-kbyte range.
Post-flight briefing
In the C language, strings are defined as simple arrays of characters, but the C language standard had
no concept of different memory regions (RAM vs. Flash) nor of the particular mechanisms required to
cross the bridge between different buses in a Harvard architecture. The programmer using the MPLAB
C30 compiler needs a basic understanding of the trade-offs of the various mechanisms available and
the allocation strategies adopted to make the most of the precious resources (RAM especially) avail-
able to embedded-control applications.
Notes for C experts
The const attribute is normally used in the C language, together with most other variable types, only
to assist the compiler in catching common parameter usage errors. When a parameter is passed to a
function as a const or a variable is declared as a const, the compiler can in fact help flag any follow-
ing attempt to modify it. The MPLAB C30 use of the PS V extends this semantic in a very natural way,
allowing for a more efficient implementation, as we have seen.
81
Chapter 6
Notes for assembly experts
The "string. h" library contains many useful block manipulation functions that can be useful, via
the use of pointers, to perform operations on any type of data array, not just strings, like memcpy ( ) ,
memcmp ( ) , memset ( ) and memmove ( ) .
The "ctype .h" library contains instead functions that help discriminate individual characters accord-
ing to their position in the ASCII table, to discriminate lower case from upper case, and/or convert
between the two.
Notes for PIC microcontroller experts
Since the PIC24 program memory is implemented using Flash technology, programmable with a single
supply voltage even at run time, during code execution, it is possible to design boot-loaders — that
is, applications that automatically update part or all of their own code. It is also possible to utilize a
section of the Flash program memory as a nonvolatile memory storage area, within some pretty basic
limitations. To write to the Flash program memory, though, you will need to utilize the table-access
methods and exercise extreme caution. The PSV window is a read-only device and, as we have seen
before, it gives access only to 16 of the 24 bits of each program memory location.
Also, notice that the memory can only be written in complete rows of 64 words each and must be first
erased in blocks of 8 rows (512 words) each. This can make frequent updates impractical if single
words or small data structures in general are being managed.
Tips and tricks
String manipulation can be fun in C once you realize how to make the zero termination character work
for you efficiently. Take, for example, the mycpy ( ) function below:
void mycpy ( char *dest, char * src)
{
while ( *dest++ = *src++) ;
}
This is quite a dangerous piece of code, as there is no limit to how many characters could be copied,
there is no check whether the dest pointer is pointing to a buffer that is large enough, and you can
imagine what would happen should the src string be missing the termination character. It would be
very easy for this code to continue beyond the allocated variable spaces and to corrupt the entire con-
tents of the data RAM, including the all precious SFRs.
At a minimum, you should try to verify that pointers passed to your functions have been initialized before
use. Compare them with the null value (declared in "stdlib . h" and/or "stddef . h") to catch the error.
Add a limit to the number of bytes to be copied; it is reasonable to assume that you will know the
size of the strings/arrays used by your program, and if you don't, use the sizeof ( ) operator. A better
implementation of mycpy ( ) could be the following:
void mycpy ( char *dest, char *src, int max)
{
if ((dest != NULL) && ( src != NULL))
while (( max-- > 0) && ( *src) )
*dest++ = *src++;
}
82
Taking a look under the hood
Exercises
Develop new string manipulation functions to perform the following operations:
1. Search for a string in an array of strings, sequential.
2. Implement a Binary search.
3. Develop a simple Hash Table management library.
Books
• Wirth,N. (1976)
Algorithms + Data Structures = Programs
Prentice-Hall, Englewood Cliffs, NJ
With unparalleled simplicity, Wirth (the father of the Pascal programming language) takes you
from the basics of programming all the way up to writing your own compiler.
Links
http://en.wikipedia.org/wiki/Pointers#Support_in_various^rogramming_languages
Learn more about pointers and see how they are managed in various programming languages.
83
PART
IT
Flying "Solo "
Part II - Flying "Solo"
Congratulations! You have endured the first few lessons and gained the necessary confidence to
perform your first flight without an instructor sitting next to you. You are going to fly solo! As a conse-
quence, in the next group of lessons more is going to be expected of you.
In the second part of this book, we will continue reviewing one by one the fundamental peripherals that
allow a PIC24 to interface with the outside world. Since the complexity of the examples will grow a
little bit, having an actual demonstration board at hand is recommended so that a practical demonstra-
tion can be performed. I will refer often to the standard Microchip Explorer 16 demonstration board,
but any third-party tool that offers similar features or allows for a small prototyping area can be used
just as effectively.
87
CHAPTER
7
Comm unica tion
In This Chapter
Synchronous serial interfaces
►
Testing the Read Status Register
Asynchronous serial interfaces
command
► Parallel interfaces
►
Writing to the EEPROM
Synchronous communication
►
Reading the memory contents
using the SPI modules
►
A nonvolatile storage library
►
Testing the new NVM library
On some of the major airlines, sometimes they make an additional channel available — the "cockpit
channel," where you can listen to the actual conversation over the radio between the pilots and the
traffic controllers. When you listen to it the first few times, it seems impossible to believe that there is
actually any intelligent conversation going on. It all sounds like a continuous sequence of seemingly
random numbers and unrecognizable acronyms. But, as you listen further and become familiar with
some of the terms used in aviation, it starts to make sense. A precise protocol is followed by both pilots
and controllers, selected radio frequencies are used as the media, and there is a whole language that
must be learned and practiced to communicate from the cockpit of any airplane.
In embedded control, communication is equally a matter of understanding the protocols as well as the
characteristics of the physical media available. In embedded-control programming, learning to choose
the right communication interface can be as important as knowing how to use it.
Flight plan
In this lesson we will review a couple of communication peripherals available in all the general-purpose
devices in the new PIC24 family. In particular, we will explore the asynchronous serial communication
interfaces UART1 and UART2, and the synchronous serial communication interfaces SPI1 and SPI2,
comparing their relative strengths and limitations for use in embedded-control applications.
Preflight checklist
In addition to the usual software tools, including the MPLAB® IDE, MPLAB C30 compiler and the
MPLAB SIM simulator, this lesson will require the use of the Explorer 16 demonstration board and the
MPLAB ICD2 In Circuit Debugger.
89
Chapter 7
Use the "New Project Set-up" checklist to create a new project called "SPI" and a new source file simi-
larly called ™ spi2 . c " .
The flight
The PIC24FJ128GA010 offers seven communication peripherals that are designed to assist in all com-
mon embedded-control applications. As many as six of them are "serial" communication peripherals,
as they transmit and receive a single bit of information at a time; they are:
2 x the universal asynchronous receiver and transmitters (UARTs)
2 x the SPI synchronous serial interfaces
2 x the PC™ synchronous serial interfaces
The main difference between a synchronous interface (like the SPI or PC) and an asynchronous one
(like the UART) is in the way the timing information is passed from transmitter to receiver. Synchro-
nous communication peripherals need a physical line (a wire) to be dedicated to the clock signal,
providing synchronization between the two devices. The device(s) that originates the clock signal is
typically referred to as the Master and the device(s) that synchronizes with it is called the Slave(s).
Synchronous serial interfaces
The PC interface, for example, uses two wires (and therefore two pins of the microcontroller), one for
the clock (referred to as SCL) and one (bidirectional) for the data (SDA).
PIC24
l 2 C interface
(Master)
Clock (SCL)
l 2 C Peripheral
(Slave)
Data (SDA)
Figure 7-1 . 1 2 C interface block diagram.
The SPI interface instead separates the data line in two, one for the input (SDI) and one for the output
(SDO), requiring one extra wire but allowing simultaneous (faster) data transfer in both directions.
PIC24
SPI interface
SCK
SDO
SDI
Clock
SPI Peripheral
SCK
SDI
SDO
Data
Figure 7-2. SPI interface block diagram.
90
Communication
In order to connect multiple devices to the same serial communication interfaces (bus configuration),
the PC interface requires a 10-bit address to be sent over the data line before any actual data is trans-
ferred. This slows down the communication but allows the same two wires (SCL and SDA) to be used
for as many as (theoretically) 1,000 devices. Also, the PC interface allows for multiple devices to act as
masters and share the bus using a simple arbitration protocol.
The SPI interface, on the other side, requires an additional physical line, the slave select (SS) to be
connected to each device. In practice, this means that, using an SPI bus, as the number of devices con-
nected grows, the number of I/O pins required on the PIC24 grows proportionally with them.
PIC24
SPI
SPI
SPI interface
Peripheral
(Slave #1)
Peripheral
(Slave #2)
SCK
SDO
SDI
CS1
CS2
CSN
SCK
SDO
SDI SS
SCK
SDO
SDI SS
fr "
fr "
1 >
Figure 7-3. SPI bus block diagram.
Sharing an SPI bus among multiple masters is theoretically possible but practically very rare. The main
advantages of the SPI interface are truly its simplicity and the speed that can be one order of magnitude
higher than that of the fastest PC bus (even without taking into consideration the details of the proto-
col-specific overhead).
Asynchronous serial interfaces
In asynchronous communication interfaces, there is no clock line, while typically two data lines are
used: TX and RX, respectively, for input and output (optionally two more lines may be used to provide
hardware handshake). The synchronization between transmitter and receiver is obtained by extract-
ing timing information from the data stream itself. Start and stop bits are added to the data and precise
formatting (with a fixed baud rate) allow reliable data transfer.
PIC24
UART interface
TX
RX
RTS
CTS
Asynchronous
Peripheral
RX
TX
CTS
RTS
Data
Optional Handshake
Figure 7-4. Asynchronous serial interface block diagram.
91
Chapter 7
Several asynchronous serial interface standards dictate the use of special transceivers to improve the
noise immunity, extending the physical distance up to several thousand feet.
Each serial communication interface has its advantages and disadvantages. Table 7-1 tries to summa-
rize the most important ones as well as the most common applications:
Synchronous
Asynchronous
Peripheral
SPI
PC
UART
Max Bit Rate
10 Mbit/s
1 Mbit/s
500 kbit/s
Max Bus Size
Limited by no. of pins
128 devices
Point-to-point (RS232)
256 devices (RS485)
Number of Pins
3 + nxCS
2
2
Pros
Simple, low cost, high
speed
Small pin count, allows
multiple masters
Longer distance, improved
noise immunity (requires
transceivers)
Cons
Single master, short
distance
Slowest, short distance
Requires accurate clock
frequency
Typical
Application
Direct connection to
ASICs and other
peripherals on same PCB
Bus connection with
peripherals on same
PCB
Interface with terminals,
personal computers and
other data acquisition
systems
Examples
Serial EEPROMs (25CXXX
series),
MCP320X A/D converter,
ENC28J60 Ethernet
controller,
MCP251X CAN controller...
Serial EEPROMs
(24CXXX series),
MCP98XX tempera-
ture sensors, MCP322x
A/D converters...
RS232, RS422, RS485,
LIN bus, MCP2550 IrDA
interface...
Table 7-1 . A comparison of synchronous and asynchronous serial communication peripherals.
Parallel interfaces
The Parallel Master Port (PMP) completes the list of basic communication interfaces of the PIC24.
The PMP has the ability to transfer up to 8 bits of information at a time while providing several address
lines, so as to interface directly to most LCD display modules commercially available (alphanumeric
and graphic modules with integrated controller) as well as Compact Flash memory cards (or CF-I/O
devices), printer ports and an almost infinite number of other basic 8-bit parallel devices available on
the market and featuring the standard control signals: -CS, -RD, -WR.
In the rest of this lesson we will begin focusing specifically on the use of one synchronous serial
interface, the SPI. In the following chapters we will cover also the asynchronous serial interfaces and
separately the PMP.
92
Communication
Synchronous communication using the SPI modules
The SPI interface is perhaps the simplest of all the interfaces available, although the PIC24 implemen-
tation is particularly rich in options and interesting features.
1:1 to 1:8
Secondary
Prescaler
1:1/4/16/64
Primary
Prescaler
I
i
t
FCY
SPIxCON1<1:0>
SPIxCON1<4:2>
Enable
Master Clock
Transfer
8-Level FIFO Buffer
(Enhanced Modes)
I
SPIxBUF (i:
Read SPIxBUF
Write SPIxBUF
16
/_
^ Internal Data Bus
Note 1: In Standard modes, data is transferred directly between SPIxSR and SPIxBUF.
Figure 7-5. The SPI module block diagram.
The SPI interface is essentially composed of an 8-bit shift register: bits are simultaneously shifted in
(MSB first) from the SDI line and shifted out from the SDO line in sync with the clock on pin SCK.
If the device is configured as a bus Master, the clock is generated internally (derived from the periph-
eral clock after a cascade of two prescalers for maximum flexibility) and output on the SCK pin. If the
device is a bus Slave, the clock is received from the SCK pin.
As with all other peripherals we will encounter, the essential configuration options are controlled by a
special function register, spixCONI in this case, and additional advanced options are offered in spixCON2
Upper Byte:
U-0
U-0
U-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
—
—
—
DISSCK
DISSDO
MODE16
SMP
CKE
bit 15
bit 8
Lower Byte:
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
SSEN
CKP
MSTEN
SPRE2
SPRE1
SPREO
PPRE1
PPREO
bit 7
bitO
Figure 7-6. The SPIxCONI control register.
93
Chapter 7
To demonstrate the basic functionality of the SPI peripheral, we will use the Explorer 16 demo board
where the PIC24 SPI2 module is connected to a 25LC256 EEPROM device, often referred to as a
Serial EEPROM (SEE or sometimes just E2 — pronounced, e-squared). This is a small and inexpensive
device that contains 256 kbits, or 32 kbytes, of nonvolatile high-endurance memory.
In order to prepare the SPI2 module for communication with the serial memory device, we will need to
fine tune the peripheral module configuration.
The SEE responds to a short list of 8-bit (modi 6 = 0) commands that according to the device datasheet
must be supplied via the SPI interface with the following setting:
clock IDLE level is low, clock ACTIVE is high (ckp=o)
serial output changes on transition from ACTIVE to IDLE (cke=1)
The PIC24 will act as a bus Master (msten=i) and will produce the clock signal SCK, deriving it from
the internal clock after prescaling (in this case we will use the default prescalers values 1:64 and 1:8 for
a total of 1:512).
The chosen configuration value can be defined as a constant that will later be assigned to the SPI2CON1
register:
#define SPI_MASTER 0x012 // select 8-bit master mode, CKE=1, CKP=0
To enable the peripheral, we will access the spi2stat register where, similarly to most other PIC24
peripherals, bit 15 is the main enable control bit; another constant is defined for readability:
#define SPI_ENABLE 0x8000 // enable SPI port, clear status
Pin 12 of PORTD is connected to the memory chip select (CS), active low pin, so we will add two
more definitions to the program, once more, to make it more readable:
#define CSEE _RD12 // select line for Serial EEPROM
#define TCSEE _TRISD12 // tris control for CSEE pin
We can now write the peripheral initialization part of our demonstration program:
// 1. init the PIC24 SPI peripheral
TCSEE =0; // make SSEE pin output
CSEE = 1; // de-select the Serial EEPROM (low power standby)
SPI2CON1 = SPI_MASTER; // select mode
SPI2STAT = SPI_ENABLE; // enable the peripheral
We can now write a small function that will be used to transfer data to and from the serial EEPROM
device:
// send one byte of data and receive one back at the same time
int writeSPl2 ( int data)
{
SPI2BUF = data; // write to buffer for TX
while ( ! SPl2STATbits . SPIRBF) ; // wait for transfer to complete
return SPI2BUF; // read the received value
}//writeSP!2
94
Communication
The writeSPi2 isa truly bidirectional transfer function. It immediately writes a character to the
transmit buffer and then enters a loop to wait for the receive flag to be set to indicate that the transmis-
sion was completed as well as that data was received back from the device. The data received is then
returned as the value of the function.
When communicating with the memory device, however, there are situations when a command is sent
to the memory, but there is no immediate response. There are also cases when data is read from the
memory device but no further data commands need to be sent by the PIC24. In the first case (write
command), the return value of the function can be simply ignored. In the second case (read command),
a dummy value can be sent to the memory while shifting in data from the device.
The 25LC256 datasheet contains accurate depictions of all seven possible command sequences that
can be used to read or write data to and from the memory device. A small table of constants can help
encode all such commands:
// 2 5LC2 5 6 Serial EEPROM commands
#define SEE_WRSR 1
# define SEE_WRITE 2
# define SEE_READ 3
# define SEE_WDI 4
#define SEE_STAT 5
#define SEE WEN 6
// write status register
// write command
// read command
// write disable
// read status register
// write enable
We can now write a small test program to verify that the communication with the device is properly
established. For example, using the Read Status Register command we can interrogate the memory
device and verify that the SPI peripheral is properly configured.
Testing the Read Status Register command
After sending the appropriate command (see_stat), we will need to add an additional call to the
writeSPi2() function with a dummy piece of data to capture the response from the memory device.
CS
SCK
01 23456789 10 11
f
21
22 23 24 25 26 27 28 29 30 31
SI
SO
instruction
16-bit address-
o/ ~l T^SJ^AJ^f^ CE)CDGD(
high-impedance
data out
7X6X5X4X3X2X1X0-
Figure 7-7. The complete Read Status Register command timing sequence.
Sending any command to the SEE requires at a minimum the following steps:
Activate the memory, taking the CS pin low.
Shift out the 8-bit instruction.
95
Chapter 7
Add one or more additional steps here, depending on the specific command.
Deactivate the memory (taking the CS pin high) to complete the command, after which the
memory will go back to a low-power consumption stand-by mode.
In practice, the following code is required to perform the complete Read Status Register operation:
// Check the Serial EEPROM status
CSEE =0; // select the Serial EEPROM
writeSPl2 ( SEE_STAT) ; // send a READ STATUS COMMAND, ignore immediate data
i = writeSPl2 ( 0) ; // send dummy, read data
CSEE = 1; // deselect to complete the command
The complete project listing should look like:
/*
** SPI2 demo
*/
#include <p24f j 128ga010 .h>
// I/O definitions
#define CSEE _RD12
#define TCSEE TRISD12
// select line for Serial EEPROM
// tris control for CSEE pin
// peripheral configurations
#define SPI_MASTER 0x012
#define SPI ENABLE 0x8000
// select 8-bit master mode, CKE=1, CKP=0
// enable SPI port, clear status
// 2 5LC2 5 6 Serial EEPROM commands
#define SEE_WRSR 1
#define SEE_WRITE 2
# define SEE_READ 3
# define SEE_WDI 4
#define SEE_STAT 5
#define SEE WEN 6
// write status register
// write command
// read command
// write disable
// read status register
// write enable
// send one byte of data and receive one back at the same time
int writeSPl2 ( int data)
{
SPI2BUF = data; // write to buffer for TX
while ( ! SPl2STATbits . SPIRBF) ; // wait for transfer to complete
return SPI2BUF; // read the received value
}//writeSP!2
96
Communication
main ( )
{
int i ;
// 1. init the SPI peripheral
TCSEE = 0;
CSEE = 1;
SPI2CON1 = SPI_MASTER;
SPI 2 STAT = SPI_ENABLE;
// make SSEE pin output
// de-select the Serial EEPROM
// select mode
// enable the peripheral
// 2. Check the Serial EEPROM status
CSEE = 0;
writeSPl2 ( SEE_STAT) ;
i = writeSPl2 ( 0) ;
CSEE = 1;
// select the Serial EEPROM
// send a READ STATUS COMMAND
// send dummy, read data
// terminate command <-set brkpt here
} // main
Follow the "MPLAB ICD2 Debugger Set-up" checklist to enable the In Circuit Debugger and prepare
the project configuration. Then follow the "Project Build" checklist to compile and link the demo code.
1. After connecting the ICD2 to the Explorer 16 demo board, program the PIC24 selecting the
"Debugger— > Program" option. By default MPLAB will choose the smallest range of memory
required to transfer the project code into the device, so that programming time will be mini-
mized. After a few seconds, the PIC24 should be programmed, verified and ready to execute.
2. Add the Watch window to the project.
3. Select "i" in the symbol selection box, and then click on the "Add Symbol" button.
4. Set the cursor on the last line of code in the main loop and set a breakpoint (double-click).
Then start the execution by selecting the "Debugger— >Run" command.
5. When the execution terminates, the contents of the 25LC256 memory Status Register should
have been transferred to the variable "i", visible in the Watch window.
Unfortunately, you will be disappointed to learn that the default status of the 25LC256 memory (at
power on) is represented by a 0x00 value, since BP1..BP0 are off to indicate no block protection, the
write enable latch WEL is disabled, and no Write In Progress WIP flag should be active.
7
6
5
4
3
2
1
W/R
-
-
-
W/R
W/R
R
R
WPEN
X
X
X
BP1
BP0
WEL
WIP
W/R = writable/readable. R = read-only.
Table 7-2. The 25LC256 Serial EEPROM Status Register.
Not a very telling result for our little test program. So, to spice things up a little we could start by set-
ting the Write Enable Latch before interrogating the Status Register — it would be great to see bit 1 set.
97
Chapter 7
To set the Write Enable Latch we will insert the following code before section 2, which we will
promptly renumber to 2.2:
// 2.1 send a Write Enable command
CSEE =0; // select the Serial EEPROM
writeSPl2 ( SEE_WEN) ; // send command, ignore immediate data
CSEE = 1; // deselect to complete the command
1. Rebuild the project.
2. Reprogram the device.
3. Set a breakpoint to the last line of code in the main program, and
4. Run (or Run to Cursor).
If everything went well, you will see the variable "i" in the Watch window turn red and show a value of 2.
Now these are the great satisfactions that you can get only by developing code for a 16-bit embedded
controller!
More seriously, now that the Write Enable latch has been set, we can add a write command and start
"modifying" the contents of the EEPROM device. We can write a single byte at a time, or we can write
a long string, up to a maximum of 64 bytes, all in a single sequence/command called Page Write. Read
more on the datasheet about address restrictions that apply to this mode of operation, though.
Writing to the EEPROM
After sending the write command, two bytes of address (addr_msb, addr_lsb) must be supplied
before the actual data is shifted out. The following code exemplifies the correct write sequence:
// send a Write command
CSEE =0; // select the Serial EEPROM
writeSPl2 ( SEE_WRITE) ; // send command, ignore immediate data
writeSPl2 ( ADDR_MSB) ; // send MSB of memory address
writeSPl2 ( ADDR_LSB) ; // send LSB of memory address
writeSPl2 ( data); // send the actual data to be written
// send more data here to perform a page write
CSEE = 1; // start actual EEPROM write cycle
Notice how the actual EEPROM write cycle initiates only after the CS line is brought high again. Also,
it will be necessary to wait for a time (Twc), specified in the memory device datasheet, for the cycle to
complete before a new command can be issued. There are two methods to make sure that the memory
is allowed the right amount of time to complete the write command. The simplest one consists of
inserting a fixed delay after the write sequence. The length of such a delay should be longer than the
maximum cycle time specified in the memory device datasheet (Twc max = 5 ms).
A better method consists of checking the Status Register contents before issuing any further read/write
command, waiting for the Write In Progress (WIP) flag to be cleared (this will also coincide with the
Write Enable bit being reset). By doing so, we will be waiting only the exact minimum amount of time
required by the memory device in the current operating conditions.
98
Communication
Reading the memory contents
Reading back the memory contents is even simpler; here is a snippet of code that will perform the
necessary sequence:
// send a Write command
CSEE = 0;
writeSPl2 ( SEE_READ)
writeSPl2 ( ADDR_MSB)
writeSPl2 ( ADDR_LSB)
data = writeSP!2 ( 0)
// select the Serial EEPROM
// send command, ignore immediate data
// send MSB of memory address
// send LSB of memory address
// send dummy, read data
// read more data here sequentially incrementing the address
CSEE = 1; // terminate the read sequence, return to low power
The read sequence can be indefinitely extended by reading sequentially the entire memory contents if nec-
essary, and upon reaching the last memory address (0x7FFF), rolling over and starting from 0x0000 again.
A nonvolatile storage library
We can now assemble a small library of functions dedicated to access the 25LC256 serial EEPROM.
The library will hide all the details of the implementation, such as the SPI port used, specific sequences
and timing details. It will expose instead only two basic commands to read and write integer data types
to a generic (black box) nonvolatile storage device.
Let's create a new project using the Project Wizard and the usual checklist. An appropriate name could
be "NVM". After creating a new source file "nvm.c" we can copy most of the definitions we prepared
in the SPI2 project:
/*
** NVM Access Library
*/
#include <p24f j 128ga010 .h>
# include "NVM.h"
// I/O definitions for PIC24 + Explorerl6 demo board
#define CSEE _RD12 // select line for Serial EEPROM
#define TCSEE _TRISD12 // tris control for CSEE pin
// peripheral configurations
#define SPI_MASTER 0x012 2
#define SPI ENABLE 0x8000
// select 8-bit master mode, CKE=1, CKP=0
// enable SPI port, clear status
// 2 5LC2 5 6 Serial EEPROM commands
#define SEE_WRSR 1
# define SEE_WRITE 2
# define SEE_READ 3
# define SEE_WDI 4
#define SEE_STAT 5
#define SEE WEN 6
// write status register
// write command
// read command
// write disable
// read status register
// write enable
99
Chapter 7
From the same project you can extract: the initialization code, the SPI2 write function and the status
register read command. Each one will become a separate function:
void InitNVM(void)
{
// init the SPI peripheral
TCSEE =0; // make SSEE pin output
CSEE = 1; // de-select the Serial EEPROM
SPI2CON1 = SPI_MASTER; // select mode
SPI2STAT = SPI_ENABLE; // enable the peripheral
}//InitNVM
int writeSPl2 ( int data)
{// send one byte of data and receive one back at the same time
SPI2BUF = data; // write to buffer for TX
while ( ! SPl2STATbits . SPIRBF) ; // wait for transfer to complete
return SPI2BUF; // read the received value
}//WriteSP!2
int ReadSR( void)
{// Check the Serial EEPROM status register
int i ;
CSEE =0; // select the Serial EEPROM
WriteSPl2 ( SEE_STAT) ; // send a READ STATUS COMMAND
i = WriteSPl2 ( 0); // send/receive
CSEE = 1; // deselect to terminate command
return i ;
} //ReadSR
To create a function that reads an integer value from the nonvolatile memory, first we verify that any
previous command (write) has been correctly terminated by reading the status register. A sequential
read of two bytes is used to assemble an integer value:
int iReadNVM( int address)
{ // read a 16-bit value starting at an even address
int lsb, msb;
// wait until any work in progress is completed
while ( ReadSR () & 0x3); // check the two lsb WEN and WIP
// perform a 16-bit read sequence (two byte sequential read)
CSEE =0; // select the Serial EEPROM
WriteSPI2 ( SEE_READ) ; // read command
WriteSP!2 ( address>>8); // address MSB first
100
Communication
WriteSPl2 ( address & Oxfe) ; // address LSB (word aligned)
msb = WriteSPI2 ( 0); // send dummy, read msb
lsb = WriteSPl2 ( 0); // send dummy, read lsb
CSEE = 1;
return ( (msb<<8)+ lsb);
}//iReadNVM
Finally, the write enable function can be created extracting the short segment of code used to access the
Write Enable latch from our previous project and adding a page write sequence:
void WriteEnable ( void)
{ // send a Write Enable command
CSEE = 0;
WriteSPl2 ( SEE_WEN) ;
CSEE = 1;
} //WriteEnable
// select the Serial EEPROM
// write enable command
// deselect to complete the command
void iWriteNVM( int address, int data)
{ // write a 16-bit value starting at an even address
int lsb, msb;
// wait until any work in progress is completed
while ( ReadSRO & 0x3); // check the two lsb WEN and WIP
// Set the Write Enable Latch
WriteEnable ( ) ;
// perform a 16-bit write sequence
CSEE = 0;
WriteSPl2 ( SEE_WRITE) ;
WriteSPl2( address>>8);
WriteSPl2( address & Oxfe);
WriteSPl2( data >>8);
WriteSPl2 ( data & Oxff ) ;
CSEE = 1;
}//iWriteNVM
(2 byte page write)
// select the Serial EEPROM
// write command
// address MSB first
// address LSB (word aligned)
// send msb
// send lsb
More functions could be added at this point to access long and long long types, for example, but for
our purposes this will suffice.
Note that the "page write" operation (see the 25LC256 memory datasheet for details) requires the
address to be aligned on a power of two boundary (in this case, just an even address will do). The
requirement must be extended to the read function for consistency.
Save the code in file "nvm.c" file and add it to the project using one of the three methods shown in the
checklists. You can either use the editor right-click menu and select "Add to Project" or right-click on
the project window on the "Source Files" branch and choose "Add Files", and then select the "nvm. c"
file from the current project directory.
101
Chapter 7
To make a few selected functions from this module accessible to other applications, create a new file,
"NVM.h", and insert the following declarations:
/*
** NVM storage library
** encapsulates 25LC256 Serial EEPROM
** as a NVM storage device for PIC24 + Explorerl6 applications
*/
// initialize access to memory device
void InitNVM (void) ;
// 16-bit integer read and write functions
// NOTE: address must be an even value between 0x0000 and 0x7ffe
// (see page write restrictions on the device datasheet)
int iReadNVM ( int address) ;
void iWriteNVM( int address, int data);
This will expose only the initialization function and the integer read/write functions, hiding all other
details of the implementation.
Add the "NVM.h" file to the project by right clicking in the project windows on the Header Files icon
and selecting it from the current project directory.
Testing the new NVM library
To test the functionality of the library we can now create a test application containing a few lines of
code that repeatedly read the contents of a memory location (at address 0x1234), increment its value,
and write it back to the memory:
/*
** NVM Library test
*/
#include <p24f j 128ga010 .h>
# include "NVM.h"
main ( )
{
int data;
// initialize the SPI2 port and CS to access the 25LC256
InitNVM () ;
102
Communication
II main loop
while ( 1)
{
// read current content of memory location
data = iReadNVM( 0x1234);
// increment current value
Nop ( ) ; // <-set brkpt here
data++;
// write back the new value
iWriteNVM( 0x1234, data) ;
// address ++;
} // main loop
} //main
Save this file as "NVMtest . c" and add it to the current project too.
Invoking the Build All command, you will observe the MPLAB C30 compiler working sequentially on
the two source files ( . c) and later the linker to combine the object codes to produce an output execut-
able (.hex).
We are planning on using the ICD2 as the debugging tool of choice to test this code, as the MPLAB
SIM does not have the capability to accurately emulate the SPI ports. Make sure not only that it is
selected in the Debugger menu, but also that in "Project-^Settings", and specifically in the MPLAB
C30 linker tab, the "Link for ICD2" option is selected. (See Figure 7-8.)
This setting is required when operating with the ICD2 debugger in order to reserve a few RAM
locations (at the end of the memory space) for the ICD2 itself and avoid conflicts with the memory
allocated by our application.
If the Build All command is completed successfully, the code is ready to be programmed on the device.
1. Adding data to the Watch window and setting a breakpoint on the line immediately following
the read command will allow us to test the proper operation of the NVM library.
2. Hit the Run command and watch the program stop after the first read.
Note the value of data and then hit Run again. It should increment continuously and, even when reset-
ting the program or disconnecting the board completely from the power supply to reconnect it later, we
will observe that the contents of location 0x1234 will be preserved and successively incremented.
Careful — if the main program loop is left running indefinitely without any breakpoint, the test pro-
gram will quickly turn into a test of the Serial EEPROM endurance. In fact the loop will continue to
reprogram location 0x1234 at a rate that will be mostly dependent on the actual Twc of the device. In a
best-case scenario (maximum Twc = 5 ms) this will mean 200 updates every second. Or, in other terms,
the theoretical endurance limit of the EEPROM (1,000,000 cycles) will be reached in 5,000 seconds, or
slightly less than one hour and a half of continuous operation.
103
Chapter 7
Build Options For Protect "NVMmcp"
JJXJ
General | ASM3O/C30 Suite | MPLAB ASM30 1 MPUB C30 MPLAB LJNK30
Categoiie* | General
Genetete Command Line
Heap size:
M in Stack Sizec
byte* f Afcw overtapped sections
Mes |? LiikfotlCD2
- Symbol DefirAons^
Add...
Remove
Remove All
Restote Defaults
TARGETBASE .map" ■q'WARGETBASE .flTARGETSUFFM"
Use AJteinate Settings
r~
OK
]
Cancel
Figure 7-8. "Project-^ Bui Id Options^MPLAB LINK30" Tab.
Post-flight briefing
In this lesson we have seen briefly how to use the SPI peripheral module, in its simplest configuration,
to gain access to a 25LC256 Serial EEPROM memory, one of the most common types of nonvolatile
memory peripherals used in embedded-control applications. The small library module developed will
hopefully be useful to you in future applications to provide "mass" storage (32 kbytes).
Notes for C experts
The C programmer used to developing code for large workstations and personal computers will be
tempted to develop the library further to include the most flexible and comprehensive set of functions.
My word of advice is to resist, hold your breath and count to ten, especially before you start adding
any new parameter to the library functions. In the embedded-control world, passing more parameters
means using up more stack space, spending more time copying data to and from the stack and, in gen-
eral, producing a larger output code. Keep the libraries simple and therefore easy to test and maintain.
This does not mean that proper object-oriented programming practices should not be followed. On the
contrary, the example above can be considered an example of object encapsulation, as all the details of
the SPI interface and Serial EEPROM internal workings can be completely hidden from the user, who
is provided with a simple interface to a generic storage device.
Notes for the experts
In developing the code examples above, we have ignored any access-speed consideration and simply
configured the SPI module for the slowest possible operation. The PIC24 SPI peripheral module oper-
ates off the peripheral clock system, which can be ticking as fast as 16 MHz in the current production
models. Few peripherals can operate at such speeds at 3V. Specifically, the 25LC256 series Serial
104
Communication
EEPROMs operate with a maximum clock rate of 5 MHz when the power supply is in the 2.5 V to 4.5V
range. This means that the fastest SPI port configuration compatible with the memory device can be
obtained with a primary prescaler configured for a 4: 1 ratio and a secondary prescaler configured for
1:1 operation (16 MHz/4 = 4 MHz). A sequential read command could therefore provide a maximum
throughput of 4 Mbit per second or 512 kbytes per second. At such a rate the CPU would still be able
to execute 32 instructions between each new byte of data received — not enough to perform complex
calculations, but most probably sufficient for simple data transfer tasks.
Notes for PIC microcontroller experts
In addition to the SPI options available on most PIC microcontroller SPI interfaces (offered by the SSP
and MSSP modules), such as:
selectable clock polarity
selectable clock edge
master or slave mode operation
the PIC24 SPI interface module adds several new capabilities, including:
16-bit transfer mode
data input sampling phase selection
framed transmission mode
frame sync pulse control (polarity and edge selectable)
Enhanced Mode (8 deep transmit and receive FIFOs).
In particular, the 16-bit transfer mode could be used during sequential read and/or page write op-
erations to improve the efficiency and increase the number of cycles available to the CPU between
accesses to the SPI buffers (doubling it). But it is the Enhanced Mode, with eight-levels deep FIFOs,
that can truly free up a considerable amount of CPU time. Up to eight words of data (16 bytes) can be
written or retrieved from the SPI buffers in short bursts, leaving much more time to the CPU to process
the data in between the successive bursts.
Tips and tricks
If you store important data in an external nonvolatile memory, you might want to put some additional
safety measures in place (both hardware and software). From a hardware perspective make sure that:
Adequate power-supply decoupling (a capacitor) is provided close to the memory device.
A pull-up resistor (10k ohm) is provided on the Chip Select line, to avoid floating during the
microcontroller power up and reset.
An additional pull-down resistor (10k ohm) can be provided on the SCK clock line to avoid
clocking of the peripheral during boundary scan and other board testing procedures.
Verify clean and fast power-up and down slope are provided to the microcontroller to guar-
antee reliable power on reset operation. If necessary, add an external voltage supervisor (see
MCP809 devices, for example).
105
Chapter 7
A number of software methods can then be employed to prevent even the most remote possibility that a
program bug or the proverbial cosmic ray might trigger the write routine. Here are some suggestions:
Avoid reading and especially updating the NVM content right after power up. Allow a few
milliseconds for the power supply to stabilize (application dependent).
Add a software write-enable flag, and demand that the calling application set the flag before
calling the write routine, possibly after verifying some essential application- specific entry
condition.
Add a stack-level counter; each function in the stack of calls implemented by the library
should increment the counter upon entry and decrement it on exit. The write routine should
refuse to perform if the counter is not at the expected level.
Some users refuse to use the NVM memory locations corresponding to the first address
(0x0000) and/or the last address (Oxffff), believing they could be statistically more likely to be
subject to corruption.
More seriously, store two copies of each essential piece of data, performing two separate calls
to the write routine. If each copy contains even a simple checksum, it will be easy, when read-
ing it back, to discard the corrupted one and recover.
106
Communication
Exercises
1. Develop (circular) buffered versions of the read and write functions.
2. Enable the new SPI 16-bit mode to accelerate basic read and write operation.
3. Several functions in the library are performing locking loops that could reduce the overall
application performance. By utilizing the SPI port interrupts implement a non blocking ver-
sion of the library.
Books
• Eady, F. (2004)
Networking and Internetworking with Microcontrollers
Newnes, Burlington, MA
An entertaining introduction to serial communication in embedded control.
• Buck, R. (1997)
Flight of Passage: A Memoir
Hyperion, New York, NY
A grand adventure, in which two teenagers fly coast to coast in an aviation ritual of passage.
Links
http://www. microchip, com/stellent/idcplg ?IdcService = SS_GET_PAGE&nodeId=
1406&dDocNarne=en010003
Use the link above or search on Microchip's web site for a free tool called "Total Endurance
Software." It will help you estimate the endurance to expect from a given NVM device in your
actual application conditions. It will give you an indication of the total number of e/w cycles
or the number of expected years of your application life before a certain target failure rate is
reached.
107
CHAPTER
8
Asynchronous communication
In This Chapter
UART configuration
► Testing a VT100 terminal
Sending and receiving data
► Using the serial port as a
Testing the serial
debugging tool
communication routines
The matrix
► Building a simple
console library
If you have any experience with radio communication, whether it be with a walkie-talkie or a proper
CB radio, you know how different it is from talking on a cell phone. For one, it's a half-duplex system,
meaning you cannot talk if somebody else is already talking. You have to listen patiently, wait for your
turn and then speak up, trying to be as concise as possible to give others the possibility of joining the
conversation too. A simple verbal handshake system is used to prevent conflicts and misunderstanding.
This is exactly how it works in aviation. There is a precise protocol, a set of rules that dictates who
should talk at any given point in time, what they should say and how. There are specific roles — such as
the air traffic controllers, the pilots, the flight stations and the towers — and they all share the media in a
coordinated and efficient way.
This works well as an introduction to the many asynchronous serial protocols. Some are full-duplex,
other are just half-duplex, some are multipoint, others are point-to-point, but they all require coordina-
tion and adherence to basic rules (standards) that make communication possible and allow for efficient
use of the media.
Flight plan
In this lesson we will review the PIC24 asynchronous serial communication interface modules, UART1
and UART2. We will develop a basic console library that will be handy in future projects for interface
and debugging purposes.
Pref light checklist
In addition to the usual software tools, including the MPLAB® IDE, MPLAB C30 compiler and the
MPLAB SIM simulator, this lesson will require the use of the Explorer 16 demonstration board, the
MPLAB ICD2 In-Circuit Debugger and a PC with an RS232 serial port (or a serial to USB adapter).
109
Chapter 8
You will also need a terminal emulation program, if you are using Microsoft® Windows® operating
system; the HyperTerminal application will suffice. ("Start^Programs^Accessories-^Communica-
tion— >Hy perTerminal") .
The flight
The UART interface is perhaps the oldest interface used in the embedded-control world. Some of its
features were dictated by the need for compatibility with the first mechanical teletypewriters; this
means that at least some of its technology has century-old roots.
On the other hand, nowadays finding an asynchronous serial port on a new computer (and especially
on a laptop) is becoming a challenge. The serial port has been declared a "legacy interface" and, for
several years now, strong pressure has been placed on computer manufacturers to replace it with the
USB interface. Despite the decline in their popularity, and the clearly superior performance and char-
acteristics of the USB interface, asynchronous serial interfaces are strenuously resisting in the world of
embedded applications because of their great simplicity and extremely low cost of implementation.
Four main classes of asynchronous serial applications are still being used:
1. RS232 point-to-point connection: often simply referred to as "the serial port," is used by ter-
minals, modems and personal computers, using + 12V/- 12V transceivers.
2. RS485 (EIA-485) multipoint serial connection: used in industrial applications, it uses a 9-bit
word and special half-duplex transceivers.
3. LIN bus: a low-cost, low- voltage bus designed for noncritical automotive applications. It
requires a UART capable of baud rate auto-detection.
4. Infrared wireless communication: requires a 38-40-kHz signal modulation and optical
transceivers.
The PIC24's UART modules can support all four major application classes and packs a few more
interesting features too.
Baud Rate Generator
lrDA Q
X BCLKx
Hardware Flow Control
1X1 UxRTS
|X UxCTS
UARTx Receiver
X uxRx
UARTx Transmitter
X uxTx
Figure 8-1 . Simplified UART modules block diagram.
110
Asynchronous communication
To demonstrate the basic functionality of a UART peripheral, we will use the Explorer 16 demo board
where the UART2 module is connected to an RS232 transceiver device and to a standard 9 poles D
female connector. This can be connected to any PC serial port or, in absence of the "legacy interface"
as mentioned above, to an RS232-to-USB converter device. In both cases, the Microsoft Windows
HyperTerminal program will be able to exchange data with the Explorer 16 board with a basic configu-
ration setting.
The first step is the definition of the transmission parameters. The options include:
baud rate
number of data bits
parity bit, if present
number of stop bits
handshake protocol.
For our demo we will choose the fast and convenient configuration: "1 15200, 8, N, 1, CTS/RTS",
that is:
115,200 baud
8 data bits
No parity
1 stop bit
hardware handshake using the CTS and RTS lines.
UART configuration
Use the "New Project Set-up" checklist to create a new project called "Serial" and a new source file
similarly called "serial . c". We will start by adding a few useful I/O definitions to help us control the
hardware handshake lines:
/*
** Asynchronous Serial Communication
** UART2 RS232 asynchronous communication demonstration code
*/
#include <p24f j 128ga010 .h>
// I/O definitions for the Explorerl6
#define CTS _RF12 // Clear To Send, input, HW handshake
#define RTS _RF13 // Request To Send, output, HW handshake
#define TRTSTRISFbits .TRISF13 // Tris control for RTS pin
The hardware handshake is especially necessary when communicating with a Windows terminal
application, since Windows is a multitasking operating system and its applications can sometimes
experience long delays that would otherwise cause significant loss of data. We will use one I/O pin as
111
Chapter 8
an input (RF12 on the Explorer 16 board) to sense when the terminal is ready to receive a new character
(Clear To Send), and one I/O pin as an output (RF13 on the Explorerl6 board) to advise the terminal
when our application is ready to receive a character (Request To Send).
To set the baud rate, we get to play with the Baud Rate Generator (breg2), a 16-bit counter that feeds
on the peripheral clock circuit. From the device datasheet, we learn that in the normal mode of opera-
tion (bregh = 0) it operates off a 1:16 divider, versus a high-speed mode (BREGH =1) where its
clock operates off a 1 :4 divider. A simple formula, published on the datasheet, allows us to calculate
the ideal setting for our configuration:
BREG2 = (Fosc / 8 / baudrate) -1 ; for BREGH=1
In our case this translates to the following expression:
BREG2 = (Fosc / 8 / 115,200) -1 = 33.7 where Fosc = 32MHz.
To decide how to best round out the result (we need a 16-bit integer after all) we will use the reverse
formula to calculate the actual baud rate and determine the percentage error:
Error = ((Fosc/ 8 / (BREG2 + 1)) - baudrate) / baudrate %
Rounding up to a value of 34, we obtain an actual baud rate of 1 14,285 Bd with an error of just 0.7%,
well within acceptable tolerance. With a value of 33, we obtain 1 17,647 baud or a 2.1% error, outside
the acceptable tolerance range (± 2%) for a standard RS232 port.
We therefore define the constant brate as :
# define BRATE
34
// 115200 Bd (BREGH=1
Two more constants will help us define the initialization values for the UART2 main control registers,
called u2mode and u2sta.
Upper Byte:
R/W-0
U-0
R/W-0
R/W-0
R/W-0
U-0
R/W-0 (1)
R/W-0 (1)
UARTEN
—
USIDL
IREN
RTSMD
—
UEN1
UEN0
bit 15
bit 8
Lower Byte
R/W-0 HC
R/W-0
R/W-0 HC
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
WAKE
LPBACK
ABAUD
RXINV
BRGH
PDSEL1
PDSEL0
STSEL
bit 7
bitO
Figure 8-2. The UxMODE control registers.
The initialization value for u2mode will include the bregh bit, the number of stop bits and the parity bit
settings.
#define U ENABLE 0x80 08
// enable UART, BREGH=1, 1 stop, no parity
The initialization for U2STA will enable the transmitter and clear the error flags:
# define U TX
0x0400
// enable transmission, clear all flags
112
Asynchronous communication
Upper Byte:
R/W-0 R/W-0
R/W-0
U-0
R/W-0 HC
R/W-0
R-0
R-1
UTXISEL1 UTXINV (1 )
UTXISELO
—
UTXBRK
UTXEN
UTXBF
TRMT
bit 15
bit 8
Lower Byte:
R/W-0 R/W-0
R/W-0
R-1
R-0
R-0
R/C-0
R-0
URXISEL1
URXISELO
ADDEN
RIDLE
PERR
FERR
OERR
URXDA
bit 7
bitO
Figure 8-3. The UxSTA control registers.
We will create a new function, by using the constants defined above, to initialize the control register of
the UART2, the baud-rate generator and the I/O pins used for the handshake:
void initU2 ( void)
{
U2BRG = BRATE; // initialize the baud rate generator
U2MODE = U_ENABLE; // initialize the UART module
U2STA = U_TX; // enable the Transmitter
TRTS =0; // make RTS an output pin
RTS = 1; // set RTS default status (not ready)
} // initU2
Sending and receiving data
Sending a character to the serial port is a three-step procedure:
1 . Make sure that the terminal (PC running Windows HyperTerminal) is ready. Check the Clear
to Send (CTS) line. CTS is an active low signal — that is, while it is high, we better wait pa-
tiently.
2. Make sure that the UART is not still busy sending some previous data. PIC24 UARTs have a
four-level deep FIFO buffer, so all we need to do is wait until at least the top level frees up, or
in other words, we need to check for the transmit buffer full flag utxbf to be clear.
3. Finally, transfer the new character to the UART transmit buffer (FIFO).
All of this can be nicely packaged in one short function:
int putU2 ( int c)
{
while ( CTS) ;
while ( U2 STAbits. UTXBF ) ;
U2TXREG = c;
return c ;
// wait for !CTS, clear to send
// wait while Tx buffer full
} // putU2
To receive a character from the serial port, we follow a very similar sequence:
1. Alert the terminal that we are ready to receive by asserting the RTS signal (active low).
2. Patiently wait for something to arrive in the receive buffer, checking the urxda flag.
113
Chapter 8
3. Fetch the character from the receive buffer (FIFO).
Again, all of these steps can be nicely packaged in one last function:
char getU2 ( void)
{
RTS =0; // assert Request To Send ! RTS
while ( !U2STAbits .URXDA) ; // wait for a new character to arrive
return U2RXREG; // read the character from the receive buffer
RTS = 1;
}// getU2
Testing the serial communication routines
To test our serial port control routines, we can now write a small program that will initialize the serial
port, send a prompt, and let us type on the terminal keyboard while echoing each character back to the
terminal screen:
main ( )
{
char c ;
// 1. init the UART2 serial port
initU2 () ;
// 2 . prompt
putU2 ( N > ' ) ;
// 3 . main loop
while ( 1)
{
// 3.1 wait for a character
c = getU2 ( ) ;
// 3.2 echo the character
putU2 ( c ) ;
} // main loop
}// main
Follow these steps:
1. Build the project first, and then follow the standard checklist to activate the ICD2 Debugger
and to program the Explorer 16.
2. Connect the serial cable to the PC (directly or via a serial-to-USB converter) and configure
HyperTerminal for the same communication parameters: 1 15200, n, 8, 1, RTS/CTS on the
available COM port.
114
Asynchronous communication
3. Click on HyperTerminal Connect button to start the terminal emulation.
4. Select "Run" from the Debugger menu to execute the demonstration program. Note: I recom-
mend, for now, you do not attempt to single-step, use breakpoints, or RunToCursor when
using the UART! See the "Tips and Tricks" section at the end of the chapter for a detailed
explanation.
Note also that, if HyperTerminal is already set to provide an echo for each character sent, you will see
double... literally! To disable this functionality, first hit the "Disconnect" button on HyperTerminal.
Then select "File^Properties" and in the properties dialog box select the "Settings Pane Tab." This will
be a good opportunity to set a couple more options that will come handy in the rest of the lesson.
Conned To Settings
Function, arrow, and Ctrl keys act as
(* Terminal keys C Windows keys
Backspace key sends —
P Qrl+H r Del r QrkH, Space, Ort+H
Emulation:
I H Terminal Setup...
Telnet terminal ID: VT100
Backscrol buffer lines: |500
Play sound when connecting or disconnecting
3
Input Translation.
ASCII Setup.
OK
Cancel
Figure 8-4. HyperTerminal Properties dialog box, Setting Pane.
1 . Select the VT100 terminal emulation mode so that a number of commands (activated by
special "escape" strings) will become available and will give us more control of the cursor
position on the terminal screen.
2. Select ASCII Setup to complete the configuration. In particular, make sure that the "Echo
typed characters locally" function is NOT checked (this will immediately improve your...
vision). (See Figure 8-5.)
3. Also check the "Append line feeds to incoming line ends" option. This will make sure that ev-
ery time an ASCII carriage return ( x \r ' ) character is received, an additional line feed ( v \n ' )
character is inserted automatically.
115
Chapter 8
ASCII Setup
ASCII Sending
Send line ends with line feeds
f~ Echo typed characters locally
Line delay: [O milliseconds.
Character delay: [0 miliseconds.
-LJxJ
ASCII Receiving
Force incoming data to 7-bit ASCI!
W Wrap fines that exceed terminal width
OK
Cancel
Figure 8-5. ASCII Setup dialog box.
Building a simple console library
To transform our demo project into a proper terminal console library that could become handy in future
projects, we need only a couple more functions that will complete the puzzle: a function to print an
entire (zero terminated) string and a function to input a full text line. Printing a string is, as you can
imagine, the simple part:
int putsU2 ( char *s]
{
while ( *s)
putU2 ( *s++)
} // putsU2
// loop until *s == x \0', end of string
// send the character and point to the next one
It is just a loop that keeps calling the putu2 function to send, one after the other, each character in the
string to the serial port.
Reading a text string from the terminal (console) into a string buffer can be equally simple, but we have
to make sure that the size of the buffer is not exceeded (should the user type a really long string) and
we have to convert the carriage-return character at the end of the line into a proper v \ ' character for
the string termination.
char *getsnU2 ( char *s, int len)
{
char *p = s ; // copy the buffer pointer
do{
*s = getU2 ( ) ; // wait for a new character
if ( * s =='\r')
break;
s + +;
len-- ;
} while ( len>l ) ;
// end of line, end loop
// increment buffer pointer
// until buffer full
116
Asynchronous communication
*s = x \0'; // null terminate the string
return p; // return buffer pointer
} // getsnU2
In practice the function, as presented, would prove very hard to use. There is no echo of what is being
typed and the user has no room for errors. Make only the smallest typo and the entire line must be
retyped. If you are like me, you do make a lot of typos. . .all of the time. . . , and the most battered key on
your keyboard is the backspace key. A better version of the getsnU2 function must include character
echo and at least provisions for the backspace key to perform basic editing. It really takes only a couple
more lines of code. The echo is quickly added after each character is received. The backspace character
(identified by the ASCII code 0x8) is decoded to move the buffer pointer one character backward (as
long as we are not at the beginning of the line already). We must also output a specific sequence of
characters to visually remove the previous character from the terminal screen:
char *getsnU2 ( char *s, int len)
{
char *p = s; // copy the buffer pointer
int cc = 0; // character count
do{
*s = getU2 ( ) ; // wait for a new character
putU2 ( *s) ; // echo character
if (( *s==BACKSPACE)&&( s>p) )
{
putU2 ( % *)# // overwrite the last character
putU2 ( BACKSPACE) ;
len++;
s--; // back the pointer
continue;
}
if ( *s=='\n') // line feed, ignore it
continue;
if ( *s=='\r') // end of line, end loop
break;
s++; // increment buffer pointer
len--;
} while ( len>l ); // until buffer full
*s = v \0'; // null terminate the string
return p; // return buffer pointer
} // getsnU2
117
Chapter 8
Put all the functions in a separate file that we will call "conU2 . c". Then create a small header file
"conU2 .h", to decide which functions (prototypes) and which constants to publish and make visible to
the outside world:
/*
* * C0NU2 . h
** console I/O library for Explorerl6 board
*/
// I/O definitions for the Explorerl6
#define CTS _RF12 // Clear To Send, input, HW handshake
#define RTS _RF13 // Request To Send, output, HW handshake
#define BACKSPACE 0x8 // ASCII backspace character code
// init the serial port (UART2, 115200@32MHz , 8, N, 1, CTS/RTS )
void initU2 ( void) ;
// send a character to the serial port
int putU2 ( int c) ;
// wait for a new character to arrive to the serial port
char getU2 ( void) ;
// send a null terminated string to the serial port
int putsU2 ( char *s);
// receive a null terminated string in a buffer of len char
char * getsnU2 ( char *s, int n) ;
Testing a VT100 terminal
Since we have enabled the VT100 terminal-emulation mode (see HyperTerminal settings above), we
now have a few commands available to better control the terminal screen and cursor position, such as:
• clrscr, to clear the terminal screen.
home, to move the cursor to the home position in the upper left corner of the screen.
These commands are performed by sending so called "escape sequences" (defined in the ECMA-48
standard, (also ISO/IEC 6429 and ANSI X3.64), also referred to as ANSI escape codes. They all start
with the characters ESC (ASCII Ox lb) and the character '[' (left-squared bracket):
// useful macros for VT100 terminal emulation
#denne clrscr () putsU2 ( "\xlb[2J")
#define home ( ) putsU2 ( "\xlb [1 , 1H" )
In order to test the console library, we can now write up a small program that will:
1 . Initialize the serial port.
2. Clear the terminal screen.
118
Asynchronous communication
3. Send a welcome message/banner.
4. Send a prompt character.
5. Read a full line of text.
6. Print the text on a new line.
Save the following code in a new file that we will call "CONU2test . c":
/*
** CONU2 Test
** UART2 RS2 32 asynchronous communication demonstration code
*/
#include <p24f j 128ga010 .h>
#include "conU2.h"
#define BUF_SIZE 12 8
main ( )
{
char s [BUF_SIZE] ;
// 1. init the console serial port
initU2 () ;
// 2 . text prompt
clrscr ( ) ;
home ( ) ;
putsU2 ( "Learn to fly with the PIC24!");
/ / 3 . main loop
while ( 1)
{
putU2 (">"); // prompt
// 3.1 read a full line of text
getsnU2 ( s, BUF_SIZE) ;
// 3.2 send a string to the serial port
putsU2 ( s) ;
// 3.3 send a carriage return
putU2 p\r' ) ;
} // main loop
} / / main
119
Chapter 8
Follow these steps:
1. Create a new project, using the "New Project" checklist, and add all three files: "conU2 .h",
"conU2 . c" and "conU2test . c" to the project and build all.
2. Use the ICD2 checklist to connect the ICD2 debugger and program the Explorer 16 board.
3. Test the editing capabilities of the new console library you just completed.
Using the serial port as a debugging tool
Once you have a small library of functions to send and receive data to a console through the serial port,
you have a new and powerful debugging tool available. You can strategically position calls to print
functions to present the content of critical variables and other diagnostic information on the terminal.
You can easily format the output so as to be in the most convenient format for you to read. You can add
input functions to set parameters that can better help test your code or you can use the input function to
simply pause the execution and give you time to read the diagnostic output when required. This is one
of the oldest debugging tools, effectively used since the first computer was ever invented.
The matrix
To finish this lesson on a more entertaining note, let's develop a new demo project that we will call the
"matrix . c". The intent is to test the speed of the serial port and the PC terminal emulation by send-
ing large quantities of text to the terminal and clocking its performance. The only problem is that we
don't have (yet) access to a large storage device from which to extract some meaningful content to send
to the terminal. So the next best option is that of "generating" some content using a pseudo-random
number generator. The "stdlib . h" library offers in fact a convenient rand ( ) function that returns
a positive integer between and MAX_RAND (a constant defined in the "limits .h" file that in the
MPLAB C30 implementation can be verified to be equal to 32,767).
Using the "remainder of operator we can reduce its output to any smaller integer range and produce
only a subset of printable character values from the ASCII set. The following statement, for example,
will produce only characters in the range from 33 to 127.
putU2 ( 33 + (rand()%94) ) ;
To generate a more appealing and entertaining output, especially if you happened to watch the movie
The Matrix, we will present the (random) content by columns instead of rows. We will use the pseudo-
random number generator to change the content and the "length" of each column as we continuously
refresh the screen.
/*
** The Matrix
* *
*/
#include <p24f j 128ga010 .h>
# inc lude " CONU2 . h "
#include <stdlib.h>
#define COL 40
# define ROW 2 3
120
Asynchronous communication
# define DELAY 3 000
main ( )
{
int v[40]; // vector containing length of each string
int i,j,k;
// 1. initializations
T1CON = 0x8030; // TMRl on, prescale 256, Tcy/2
initU2(); // initialize the console (115200, 8, N, 1, CTS/RTS
clrscr ( ) ; // clear the terminal (VT100 emulation)
getU2 ( ) ; // wait for one character to randomize the sequence
srand( TMRl) ;
// 2. init each column length
for( j =0; j<COL; j++)
v[j] = rand()%ROW;
// 3 . main loop
while ( 1)
{
home ( ) ;
// 3.1 refresh the screen with random columns
for( i=0; i<ROW; i++)
{
// refresh one row at a time
for( j=0; j<COL; j++)
{
// print a random character down to each column length
if ( i < v[j] )
putU2 ( 33 + (rand()%94) ) ;
else
putu2 r l ) ;
putU2 ( ' M ;
} // for j
per ( ) ;
} // for i
// 3.2 randomly increase or reduce each column length
for( j=0; j<COL; j++)
{
switch ( rand()%3)
{
121
Chapter 8
case 0: // increase length
v[j]++;
if (v[j]>ROW)
v[j]=ROW;
break;
case 1: // decrease length
v[j] — ;
if (v[j]<l)
v[j]=l;
break;
default:// unchanged
break;
} // switch
} // for
} // main loop
} // main
Forget the performance — watching this code run is fun. It is too fast anyway — in fact, you will have to
add a small delay loop (inside the for loop in 3.1) to make it more pleasant on the eye:
// 3.1.1 delay to slow down the screen update
TMR1 =0;
while ( TMR1<DELAY) ;
Note: remember to take the blue pill the next time!
Post-flight briefing
In this lesson we have developed a small console I/O library while reviewing the basic functionality
of the UART module for operation as an RS232 serial port. We connected the Explorer 16 board to a
VT100 (emulated) terminal (Windows HyperTerminal). We will take advantage of this library in the
next few lessons to provide us with a new debugging tool and possibly as a user interface for more
advanced flights/projects.
122
Asynchronous communication
Notes for C experts
I am sure at this point you are wondering about the possibility of using the more advanced library func-
tions defined in the "stdio .h" library (such as print f ) to direct the output to the UART2 peripheral.
This is in fact possible by simply replacing one of the essential library functions: "write . c":
/*
** write. c
** replaces stdio lib write function
* *
*/
#include <p24f j 128ga010 .h>
#include <stdio.h>
#include "conu2.h"
int write (int handle, void *buffer, unsigned int len)
{
int i, *p;
const char *pf;
switch (handle)
{
case 0: // stdin
case 1: // stdout
case 2 : // stderr
for (i = len; i; --i)
putU2 ( * (char*) buffer) ;
break;
default :
break;
} // switch
return (len) ;
} // write
Save this code in a file called "write . c" in your project directory and add it to the list of source files
for the project.
From this moment on, the linker will perform the connection and any call to one of the "stdio . h"
library functions producing output on one of the standard streams (stdin, stdout, stderr) will be
redirected to the UART2.
Notice that you will still be responsible for the proper UART initialization and the "conu2 . c" file will
have to be included in the project sources as well.
123
Chapter 8
Notes for PIC microcontroller experts
Sooner or later, every embedded-control designer will have to come to terms with the USB bus. If, for
now, a small "dongle" (converting the serial port to a USB port) can be a reasonable solution, sooner or
later you are going to find opportunities and designs that will actually benefit from the superior perfor-
mance and compatibility of the USB bus. Several 8-bit PIC microcontroller models already incorporate
a USB Serial Interface Engine (SIE) as a standard communication interface. Microchip offers a free
USB software stack with drivers and ready-to-use solutions for the most common classes of applica-
tions. One of them, known as the Communication Device Class (or CDC), makes the USB connection
look completely transparent to the PC application so that even HyperTerminal cannot tell the differ-
ence. Most importantly, you will not need to write and/or install any special Windows drivers. When
writing the application in C, you won't even notice the difference, if not for the absence of a need to
specify any communication parameter. In USB there is no baud rate to set, no parity to calculate, no
port number to select (wrong), while the communication speed is so much higher...
Tips and tricks
About the ICD2 and UARTs on ICE
As we mentioned during one of the early exercises presented in this lesson, single-stepping through a
routine that enables and uses the UART to transmit and receive data from the HyperTerminal program
is a bad idea. You will be frustrated, seeing the HyperTerminal program misbehave and/or simply lock
up and ignore any data sent to it without any apparent reason. In order to understand the problems, you
need to know more about how the MPLAB ICD2 in-circuit debugger operates. After executing each
instruction, when in single-step mode or, upon encountering a breakpoint, the ICD2 debugger not only
stops the CPU execution, but also "freezes" all the peripherals. It freezes them as in dead-cold-ice all
of a sudden — not a single clock pulse is transmitted through their digital veins. When this happens to a
UART peripheral that is busy in the middle of a transmission, the output serial line (TX) is also frozen
in the current state. If a bit was being shifted out in that precise instant, and specifically if it was a 1,
the TX line will be held in the "break" state (low) indeterminately.
The HyperTerminal program, on the other side, would sense this permanent "break" condition and
interpret it as a line error. It will assume the connection is lost and it will disconnect. Since HyperTer-
minal is a pretty "basic" program, it will not bother letting you know what is going on. . .it will not send
a beep, not an error message, nothing — it will just lock up!
If you are aware of the potential problem, this is not a big deal. When you restart your program with
the ICD2, you will have just to remember to hit the HyperTerminal Disconnect button first and then the
Connect button again. All operations will resume normally.
124
Asynchronous communication
Exercises
1 . Write a console library with buffered I/O (using interrupts) to minimize the impact on pro-
gram execution (and debugging).
Books
• Eady, F. (2005)
Implementing 802.11 with Microcontrollers:
Wireless Networking for Embedded Systems Designers
Newnes, Burlington, MA
Fred brings his humor and experience in embedded programming to make even wireless
networking easy.
• Axelson,J. (1999)
USB Complete, 3 rd ed.
Lakeview Research, Madison, WI
Jan's book has reached the third edition already. She has added more material at every step
and still managed to keep things very simple.
Links
httpi/fen. wikipedia. org/wiki/ANSI_escape_code
This is a link to the complete table of ANSI escape codes as implemented by the VT100 Hy-
perTerminal emulation.
125
CHAPTER
9
Glass bliss
In This Chapter
HD44780 controller
► A small library of functions
compatibility
to access an LCD display
The Parallel Master Port
► Advanced LCD control
Configuring the PMP for LCD
module control
In the old days, big round instruments that looked like steam gauges populated the cockpit of every
airplane, from the smallest single-engine Cessna to the ultrasonic Concord. Being so ubiquitous, the
six principal instruments, placed always in the same order, had gained the affectionate nickname of the
six-pack. But the next time you get on a commercial plane, peek into the cabin if you can. Sure, there
are still plenty of knobs and switches, but right in front of the pilots you will notice there has been a big
change. There is a large and flat piece of glass (or two). And "glass" is what the pilots call this revolu-
tion, although there is much more silicon behind it than most of them would suspect. It is the digital
revolution of the cockpit, and it has happened only in the last few years.
Numerous powerful microprocessors work hard behind that glass to cram as much information as
possible into a very simple, intuitive and possibly pleasing interface. Global positioning system (GPS)
technology has been the driving force behind this innovation, and every airplane manufacturer today
offers several advanced glass cockpit options for new models. Some are even speculating that the re-
cent increase in sales of new airplanes, and the stimulus to the entire industry that has followed, might
be attributed to the excitement generated by the new "glass cockpit."
Unfortunately, these are not exactly the type of airplanes that you, as a student pilot, would be flying
for the first few lessons. It might take a little while for modern new airplanes to hit the schools fleet,
but it is just a matter of time now — glass bliss is on the horizon.
The embedded world also makes copious use of glass, with LCD displays. Let's explore the basics of
LCD interfaces...
127
Chapter 9
Flight plan
In this lesson, we will learn how to interface with a small and inexpensive LCD display module. This
project will be a good excuse for us to learn and use the Parallel Master Port (PMP), a new and flexible
parallel interface available on the PIC24 microcontrollers.
Pre-flight checklist
In addition to the usual software tools, including the MPLAB® IDE, MPLAB C30 compiler and the
MPLAB SIM simulator, this lesson will require only the use of the Explorer 16 demonstration board
and the MPLAB ICD2 In-Circuit Debugger.
The flight
The Explorerl6 board can accommodate three different types of dot-matrix, alphanumeric LCD display
modules and one type of graphic LCD display module. By default, it comes with a simple "2-rows by
16-character" display, a 3V alphanumeric LCD module (Tianma TM162JCAWG1) compatible with the
industry-standard HD44780 controllers. These LCD modules are complete display systems composed
of the LCD glass, column and row multiplexing drivers, power-supply circuitry and an intelligent con-
troller, all assembled together into the so-called Chip On Glass (COG) technology. Thanks to this high
level of integration, the circuitry required to control the dot-matrix display is greatly simplified. Instead
of the hundreds of pins required by the column-and-row drivers to directly control each pixel, we can
interface to the module with a simple 8-bit parallel bus using just eleven I/Os.
+3.3V
R44-
10K-
LCD1
R43;
1.3K
(RD4/PMWR)
(RD5/PMRD~)
4-3>3V
(RB15/PMAQ) i
Iviil — a
7 /
REO/PMDO)
B /
RE1/PMD1)
9 /
RE2/PMD2)
10 /
RE3/PMD3)
u <
RE4/PMD4_1 )
RE5/PMD5_1)
13 /
RE6/PMD<3_1 )
14 /
RE7/PMD7_1 >
Figure 9-1 . Default alphanumeric LCD Module connections.
On alphanumeric modules in particular, we can directly place ASCII character codes into the LCD
module controller RAM buffer (DDR AM). The output image is produced by an integrated character
generator (a table) using a 5 x 7 grid of pixels to represent each character. The table typically contains
an extended ASCII character set, in the sense that it has been somewhat merged with a small subset of
Japanese Kanji characters as well some symbols of common use. While the character generator table
is mostly implemented in the display controller ROM, various display models offer the possibility to
extend the character set by modifying/creating new characters (up to 8 on some models) accessing a
second small internal RAM buffer (CGRAM).
128
Glass bliss
Char, code
o 000000111111
001111001111
I 110011110011
jjj — 010101010101
xxxxQOOO
xxxxOOO!
xxxxOOlO
xxxxOO 1 1
xxxxO 1 00
xxxxO 101
xxxx0110
xxxxO 1 1 1
xxxxlOOO
xxxx1001
xxxx1010
xxxxlOI 1
xxxxl 100
xxxxl 101
xxxxl 1 10
XXXX 1111
Figure 9-2. Character Generator table used by HD44780 compatible LCD display controllers.
HD44780 controller compatibility
As mentioned above, the 2x16 LCD module used in the Explorer 16 board is one among a vast selec-
tion of LCD display modules available on the market in configurations ranging from one to four lines
of 8, 16, 20, 32 and up to 40 characters each, that are compatible with the original HD44780 chipset,
today considered an industry standard.
The HD44780 compatibility means that the integrated controller contains just two registers separately
addressable, one for ASCII data and one for commands, and the following standard set of commands
can be used to set up and control the display:
3
p
■■,
P
-
•?
— -
w
P 1
T
1
H
Q
a
*i
□
7
+
u
a
q
"
2
B
R
b
r
r
-1
■■■
;■-'
(^ :
e
#
3
c
s
G
s
J
T
f
£
M
$4D
T d t
s
I
h
F
M
fi
k
5
E
U
e
i.j
•
^T
t
2
rT
u
&
b
F
u
t
'..-'
3
11
-
3
P
E
?
7
13
H
III
9
h
w
X
7
■1'
7
7
q
—
X
■::
S
;■
yi
V
i
y
-ti
■7
-'
lb
u
■¥
■
a
J
z
j
z
S
1
A
V
i
=f
+
K
[
k
(
*
V
t
D
X
."F
7
<
L
¥
1
1
P
i- 1
Z>
9
t
R
—
=
n
]
n
>
ZL
7.
-"■-.
!-■
i
-
•
>
H
--'■■
n
*
3
1Z
T.
••■-
n
,--
:■
—
o
*-
"J
y
7"
n
■
Instruction
Code
Description
Execution
time
RS
R/W
DB7
DB6
DB5
DB4
DB3
DB2
DB1
DBO
Clear display
1
Clears display and returns cursor
to the home position (address 0).
1.64ms
Cursor home
1
*
Returns cursor to home position
(address 0). Also returns display
being shifted to the original posi-
tion. DDRAM contents remains
unchanged.
1.64ms
Entry mode set
1
l/D
S
Sets cursor move direction (l/D),
specifies to shift the display (S).
These operations are performed
during data read/write.
40us
Display On/Off
control
1
D
C
B
Sets On/Off of all display (D), cur-
sor On/Off (C) and blink of cursor
position character (B).
40us
Cursor/display
shift
1
s/c
R/L
*
*
Sets cursor-move or display-shift
(S/C), shift direction (R/L). DDRAM
contents remains unchanged.
40us
129
Chapter 9
Instruction
Code
Description
Execution
time
RS
R/W
DB7
DB6
DB5
DB4
DB3
DB2
DB1
DBO
Function set
1
DL
N
F
*
*
Sets interface data length (DL),
number of display line (N) and
character font(F).
40us
Set CGRAM
address
1
CGRAM address
Sets the CGRAM address.
CGRAM data is sent and received
after this setting.
40us
Set DDRAM
address
1
DDRAM address
Sets the DDRAM address. DDRAM
data is sent and received after this
setting.
40us
Read busy-flag
and address
counter
1
BF
CGRAM /DDRAM address
Reads Busy-flag (BF) indicating
internal operation is being
performed and reads CGRAM
or DDRAM address counter
contents (depending on previous
instruction).
Ous
Write to
CGRAM or
DDRAM
1
write data
Writes data to CGRAM or
DDRAM.
40us
Read from
CGRAM or
DDRAM
1
1
read data
Reads data from CGRAM or
DDRAM.
40us
Table 9-1. The HD44780 instruction set.
Bit name
Setting / Status
l/D
= Decrement cursor position
1 = Increment cursor position
S
= No display shift
1 = Display shift
D
= Display off
1 = Display on
C
= Cursor off
1 = Cursor on
B
= Cursor blink off
1 = Cursor blink on
S/C
= Move cursor
1 = Shift display
R/L
= Shift left
1 = Shift right
DL
= 4-bit interface
1 = 8-bit interface
N
= 1/8 or 1/11 Duty(1 line)
1 = 1/16 Duty (2 lines)
F
= 5x7 dots
1 = 5x10 dots
BF
= Can accept instruction
1 = Internal operation in progress
Table 9-2. HD44780 command bits.
130
Glass bliss
Thanks to this commonality, any code we will develop to drive the LCD on the Explorer 16 board
will be immediately available for use with any of the other HD447 80-compatible alphanumeric LCD
display modules.
The Parallel Master Port
The simplicity of the 8-bit bus shared by all these display modules is remarkable. Beside the eight
bidirectional data lines (that could be reduced to just four for further I/O savings by enabling a special
"nibble" mode), there is:
An Enable strobe line (E).
• A Read/Write selection line (R/W) .
An address line (RS) for the register selection.
It would be simple enough to control the 1 1 I/Os by manually controlling (bit-banging) the individual
PORTE and PORTD pins to implement each bus sequence, but we will take this opportunity instead to
explore the capabilities of a new peripheral introduced with the PIC24 architecture: the Parallel Master
Port (PMP). The designers of the PIC24 family have created this new addressable parallel port to auto-
mate and accelerate access to a large number of external parallel devices of common use ranging from
analog-to-digital converters, RAM buffers, ISA bus compatible interfaces, LCD display modules and
even hard disks and CompactFlash® cards.
You can think of the PMP as a sort of flexible I/O bus added to the PIC24 architecture that does not
interfere with (or slow down) the operation of the 24-bit wide program memory bus, nor the 16-bit data
memory bus. The PMP offers:
8- or 16-bit bidirectional data path.
Up to 64k of addressing space (16 address lines).
Six additional strobe/control lines including:
- Enable
- Address Latch
- Read
- Write
- and two Chip Select lines.
The PMP can also be configured to operate in slave mode, to attach as an addressable peripheral to a
larger microprocessor/microcontroller system.
131
Chapter 9
Both bus-read and bus- write sequences are fully programmable so that not only the polarity and choice
of control signals can be configured to match the target bus, but also the timing can be finely tuned to
adapt to the speed of the peripherals we interface to.
Configuring the PMP for LCD module control
As in all other PIC24 peripherals, there is a set of control registers dedicated to the PMP configuration.
The first one is pmcon, and you will recognize the familiar sequence of control bits common to all the
module xxCON registers.
Upper Byte
R/W-0
U-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
R/W-0
PMPEN
—
PSIDL
ADRMUX1
ADRMUXO
PTBEEN
PTWREN
PTRDEN
bit 15
bit 8
Lower Byte:
R/W-0
R/W-0
R/W-0 {1)
R/W-0 (1)
R/W-0 (1)
R/W-0
R/W-0
R/W-0
CSF1
CSFO
ALP
CS2P
CS1P
BEP
WRSP
RDSP
bit 7
bitO
Figure 9-3. PMCON control register.
But the list of control registers that we will need to initialize is a bit longer this time and includes also:
pmmode, pmaddr, pmstat, pmpen and possibly padcfgi. They are packed with powerful options and
they all require your careful consideration. Instead of proceeding through the lengthy review of each and
every one of them, I will list only the key choices required specifically by the LCD module interface:
PMP enabled
Fully demultiplexed interface (separate data and address lines will be used)
Enable strobe signal enabled (RD4)
Read signal enabled (RD5)
Enable strobe active high
Read active high, Write active low
Master mode with Read and Write signals on the same pin (RD5)
8-bit bus interface (using PORTE pins)
Only one address bit is required, so we will choose the minimum configuration including
PMA0(RB15) andPMAl
Also, considering that the typical LCD module is an extremely slow device, we better select the most
generous timing, adding the maximum number of wait states allowed at each phase of a read or write
sequence:
4 x Tcy wait data set-up before read/write
15 x Tcy wait between R/W and Enable
132
Glass bliss
4 x Tcy wait data set-up after Enable.
A small library of functions to access an LCD display
Create a new project using the "New Project" checklist and a new source file.
We will start writing the LCD initialization routine first. It is natural to start with the initialization of
the PMP port key control registers:
void LCDinit ( void)
{
// PMP initialization
PMCON = 0x8 3 BF
PMMODE = 0x3 FF
PMPEN = 0x0001
// Enable the PMP, long waits
// Master Mode 1
// PMA0 enabled
After these steps we are able to communicate with the LCD module for the first time and we can follow
a standard LCD initialization sequence recommended by the manufacturer. The initialization sequence
must be timed precisely; see the HD44780 instruction set for the details. It cannot be initiated before at
least 30 ms have been granted to the LCD module to proceed with its own internal initialization (power-
on reset) sequence. For simplicity and safety, we will hard code a delay in the LCD module initialization
function and we will use Timer 1 to obtain simple but precise timing loops for all subsequent steps:
// init TMR1
T1CON = 0x8030; // Fosc/2, prescaled 1:256, 16us/tick
// wait for >30ms
TMR1 = 0; while ( TMR1<2000); // 2000 x 16us = 32ms
For our convenience, we will also define a couple of constants that will help us, hopefully, make the
following code more readable:
#define LCDDATA 1 / / RS = 1 ; access data register
#define LCDCMD / / RS = ; access command register
#define PMDATA PMDIN1 // PMP data buffer
To send each command to the LCD module, we will select the command register (setting the address
PMA0 = RS = 0) first. Then we will start a PMP write sequence by depositing the desired command
byte in the PMP data output buffer:
PMADDR = LCDCMD; // select the command register (ADDR = 0)
PMDATA = ObOOlllOOO; // function set: 8-bit interface, 2 lines, 5x7
The PMP will perform the complete bus write sequence as listed below:
1. The address will be published on the PMP address bus (PMA0).
2. The content of pmdata will be published on the PMP data bus (PMD0-PMD7).
3. After 4 x Tcy the R/W signal will be asserted low (RD5).
4. After 15 x Tcy the Enable strobe will be asserted high (RD4).
5. After 4 x Tcy the Enable strobe will lowered and pmdata removed from the bus.
133
Chapter 9
Notice how this sequence is quite long as it extends for more than 20 x Tcy or more than 1.25 us after
the PIC24 has initiated it. In other words the PMP will still be busy executing part of this sequence
while the PIC24 will have already executed another 20 instructions or more. Since we are going to wait
for a considerably longer amount of time anyway (>40 us) to allow the LCD module to execute the
command, we will not worry about the time required by the PMP to complete the command at this time:
TMRl = 0; while ( TMR1<3); // 3 x 16us = 48us
We will then proceed similarly with the remaining steps of the LCD module initialization sequence:
PMDATA = ObOOOOllOO; // display ON, cursor off, blink off
TMRl = 0; while ( TMR1<3); // 3 x 16us = 48us
PMDATA = ObOOOOOOOl; // clear display
TMRl = 0; while ( TMR1<100); // 10 x 16us = 1.6ms
PMDATA = ObOOOOOllO; // increment cursor, no shift
TMRl = 0; while ( TMR1<100); // 100 x 16us = 1.6ms
After the LCD module initialization, things will get a little easier and the timing loops will not be nec-
essary anymore, as we will be able to use the LCD module Read Busy Flag command. This will tell us
if the integrated LCD module controller has completed the last command given and is ready to receive
and process a new one. In order to read the LCD status register containing the Busy Flag, we will need
to instruct the PMP to execute a bus read sequence. This is a two-step process: we initiate the read
sequence by reading (and discarding) the contents of the PMP data buffer a first time. When the PMP
sequence is completed, the data buffer will contain the actual value read from the bus, and we will
read its contents from the PMP data buffer again. But how can we tell when the PMP read sequence is
complete? Simple — we can check the PMP Busy flag in the PMSTAT control register.
In summary, to check the LCD module Busy flag we will need to check the PMP Busy flag first, issue
a read command, wait for the PMP Busy flag again, and finally we will gain access to the actual LCD
module status-register contents, including the LCD Busy flag.
By passing the register address as a parameter to the read function, we will obtain a more generic func-
tion that will be able to read the LCD status register or the data register as in the following code:
char LCDread ( int addr)
{
int dummy;
while ( PMMODEbits .BUSY) ; // wait for PMP to complete previous commands
PMADDR = addr; // select the command address
dummy = PMDATA; // initiate a read cycle, dummy read
while ( PMMODEbits .BUSY) ; // wait for PMP to complete the sequence
return ( PMDATA); // read the status register
} // LCDread
The LCD module status register contains two pieces of information: the LCD Busy flag and the LCD
RAM pointer current value. We can use two simple macros to split the two pieces: LCDbusy ( ) and
LCDaddr ( ) , and a third one to access the data register: getLCD ( ) :
134
Glass bliss
#define LCDbusy ( ) LCDread ( LCDCMD) & 0x80
#define LCDaddr ( ) LCDread ( LCDCMD) & 0x7F
#denne getLCD ( ) LCDread ( LCDDATA)
Using the LCDbusy ( ) function, we can create a function to write data or commands to the LCD module:
void LCDwrite ( int addr, char c)
{
while ( LCDbusy ( ) ) ;
while ( PMMODEbits.BUSY) ; // wait for PMP to be available
PMADDR = addr;
PMDATA = c;
} // LCDwrite
A few additional macros will help complete the library:
putLCD ( ) will send ASCII data to the LCD module:
#define putLCD ( d) LCDwrite ( LCDDATA, (d) )
• LCDcmd ( ) will send generic commands to the LCD module:
#define LCDcmd ( c) LCDwrite( LCDCMD, (c))
LCDhome ( ) will reposition the cursor on the first character of the first row:
#define LCDhome ( ) LCDwrite ( LCDCMD, 2)
• LCDclr ( ) will clear the entire contents of the display:
#define LCDclr ( ) LCDwrite ( LCDCMD, 1)
And finally, for our convenience, we might want to add putsLCD ( ) , a function that will send an entire
null terminated string to the display module:
void putsLCD ( char *s)
{
while ( *s)
putLCD ( *s++) ;
} //putsLCD
Let's put all of this to work adding a short main function:
main ( void)
{
// initializations
LCDinit () ;
// put a title on the first line
putsLCD( "Flying the PIC24");
// main loop, empty for now
while ( 1)
{
}
} // main
135
Chapter 9
If all went well after building the project and programming the Explorer 16 board with the ICD2
debugger (using the usual checklists), you will now have the great satisfaction of seeing the title string
published on the first row of the LCD display.
Advanced LCD control
If you felt that all of the preceding was not too complex, and certainly not rewarding enough, here I
have some more interesting stuff, and a new challenge for you to consider.
When introducing the HD447 80-compatible alphanumeric LCD modules, we mentioned how the dis-
play content was generated by the module controller by using a table, the character generator, located
in ROM. But we also mentioned the possibility of extending the character set using an additional RAM
buffer (known as the CGRAM). Writing to the CGRAM it is possible to create new 5x7 character pat-
terns to create new symbols and possibly small graphic elements.
How about adding a small airplane to the character set of the Explorer 16 LCD module display?
We will need a function to set the LCD module RAM buffer pointer to the beginning of the CGRAM
area using the "Set CGRAM Address" command or, better, a macro that uses the LCDwrite ( ) function:
#define LCDsetG( a) LCDwrite ( LCDCMD, (a & 0x3F)
0x40
To generate two new 5x7 character patterns, one for the nose of the plane and one for the tail, we will
use the putLCD ( ) function. Each byte of data will contribute 5 bits (lsb) to define one row of the pat-
tern. After the last row of each character is defined, an extra byte of data (8 th ) will be inserted to align
for the next character block.
// generate two new characters
LCDsetG(O) ;
putLCD (
ObOOOlO)
putLCD (
ObOOOlO)
putLCD (
ObOOllO)
putLCD (
Oblllll)
putLCD (
ObOOllO)
putLCD (
ObOOOlO)
putLCD (
ObOOOlO)
putLCD (
) ; / /
alignment
putLCD (
ObOOOOO)
putLCD (
ObOOlOO)
putLCD (
ObOllOO)
putLCD (
OblllOO)
putLCD (
ObOOOOO)
putLCD (
ObOOOOO)
putLCD (
ObOOOOO)
putLCD (
0) ;
'/
alignment
The two new symbols will now be accessible with the codes and 1 respectively of the character-
generator table.
136
Glass bliss
To reposition the buffer pointer back to the data RAM buffer (DDRAM), use the following macro:
#define LCDsetC ( a) LCDwrite ( LCDCMD, (a & 0x7F) | 0x8 0)
Notice that while the first line of the display corresponds to addresses from to Oxf of the DDRAM
buffer, the second line is always found at addresses from 0x40 to 0x4f independently of the display size
— the number of characters that compose each line of the actual display.
Also a simple delay mechanism (based once more on Timer 1) will be necessary to make sure that our
airplane flies on time and stays visible. LCD displays are slow, and if the display is updated too fast the
image tends to disappear like a ghost:
#define TFLY 9000 // 9000 x 16us = 144ms
#define DELAY ( ) TMR1 = 0; while ( TMR1<TFLY)
It is time to devise a simple algorithm to make the little airplane fly in the main loop. Here it is:
// main loop
while ( 1)
{
// the entire plane appears at the right margin
LCDsetC (0x40+14) ;
putLCD( 0); putLCD( 1);
DELAY ( ) ;
// fly fly fly (right to left)
for( i=13; i>=0; i--)
{
LCDsetC (0x40+i) ; // set the cursor to the next position
putLCD(O); putLCD(l); // new airplane
putLCD ( x M ; // erase the previous tail
DELAY ( ) ;
}
// the tip disappears off the left margin, only the tail is visible
LCDsetC (0x40) ;
putLCD ( 1); putLCD p v );
DELAY ( ) ;
// erase the tail
LCDsetC ( 0x4 ) ; // point to the left margin of the 2 nd line
putLCD ( ' N ) ;
// and draw just the tip appearing from the right
LCDsetC ( 0x40+15 ) ; // point to the right margin of the 2 nd line
putLCD ( 0) ;
DELAY ( ) ;
} // repeat the main loop
Have fun flying the PIC24!
137
Chapter 9
Post-flight briefing
In this lesson we learned how to use the Parallel Master Port to drive an LCD display module. Actually,
we have just started scratching the surface. Also, since the LCD display module is a relatively slow
peripheral, it might seem that there has been little or no significant advantage in using the PMP instead
of a traditional bit-banged I/O solution. In reality, even when accessing such simple and slow peripher-
als the use of the PMP can provide two important benefits:
the timing, sequence and multiplexing of the control signals is always guaranteed to match the
configuration parameters, eliminating the risk of dangerous bus collisions and/or unreliable
operation as a consequence of coding errors and/or unexpected execution and timing condi-
tions (interrupts, bugs, ...).
the MCU is completely free from tending to the peripheral bus, allowing simultaneous execu-
tion of any number of higher priority tasks.
Notes for C experts
As we did in the previous lesson, when using the asynchronous serial interfaces, it is possible to
replace the low-level I/O routines defined in the "stdio .h" library, and in particular "write . c", to
redirect the output to the LCD display. We can actually extend the previous example by providing
redirection to the UART2 for the standard streams (stdin, stdout and stderr) and adding a fourth
stream for the LCD as in the following example code:
/*
** write. c
** replaces stdio lib write function
*/
#include <p24f j 128ga010 .h>
#include <stdio.h>
#include "conU2.h"
# include "LCD.h"
int write (int handle, void *buffer, unsigned int len
{
int
i.
*P;
cons
t
char
*pf;
switch
(handle)
1
case
//
stdin
case
1
//
stdout
case
2
//
stderr
for
(i = len
i; -
-i)
putU2 ( *
(char* ) buff er) ;
138
Glass bliss
break;
case LCD: // additional stream
for (i = len; i; --i)
putLCD( * (char*) buffer) ;
break;
default :
break;
} // switch
return (len) ;
} //write
In an alternate scheme, you might want to redirect the "stdout" stream to the LCD display as the main
output of the application, and the "stderr" stream to the serial port for debugging purposes.
Also, it is likely that at this point, you will want to modify the putLCD ( ) function to interpret special
characters like x \n ' , to advance to the next line, or even to introduce a partial decoding for a few ANSI
escape codes so in order to be able to position the cursor and clear the screen (using the macros defined
in this lesson) just as on a terminal console.
Tips and tricks
Since the LCD display is a slow peripheral, waiting for its commands to be completed in tight (locking)
loops as in the examples provided in this lesson could constitute an unacceptable waste of MCU cycles
in some applications. A better scheme would require caching LCD commands in a FIFO buffer and
using an interrupt mechanism to periodically schedule their execution. In other words, interrupts would
be used to perform multitasking of a slow process in the background of the application execution.
An example of such a mechanism is provided in the "LCD .c" example code provided with the
Explorer 16 demonstration board.
139
Chapter 9
Exercises
1. Enhance the putLCD ( ) function to correctly interpret the following characters:
v \n ' : advance to the next line.
v \r ' : reposition cursor to the beginning of current line.
v \t ' : advance to a fixed tabulation position.
2. Enhance the putLCD ( ) function to interpret the following ANSI escape codes:
v \xlb [ 2 j ' : clear entire screen.
v \xlb [ l , ih ". home cursor.
v \xlb[n,mH' : position the cursor at row x n', column y m f .
Books
Bentham, J.
TCP/IP Lean, Web Servers for Embedded Systems
CMP Books, Lawrence, Kansas
This book will take you one level of complexity higher, showing you how the TCP/IP protocols,
the foundation of the Internet, can be easily implemented in a "few" lines of C code. Jeremy
knows how to keep things "lean," as is necessary in every embedded-control application.
Links
http://www. microchip, com/stellent/idcplg ?IdcService = SS_GET_PAGE&nodeId=
1824&appnote=en011993
This is a link to Microchip Application Note 833, a free TCP/IP stack for all PICmicros.
http://www. microchip, com/stellent/idcplg ?IdcService = SS_GET_PAGE&nodeId=
1824&appnote=en012108
Application Note 870 describes a Simple Network Management Protocol for Microchip TCP/
IP stack-based applications.
140
CHAPTER
10
It's an analog world
In This Chapter
The first conversion
Automatic sampling timing
Developing a demo
► Developing a game
► Measuring temperature
► A breath-alizer game
There are certain things that, no matter how many times you practice, never seem to come out the same
way twice. Landings are a good example. Even the most experienced airline captains will occasionally
have a bad day and screw it up. I'm sure you must have noticed it, when they "bounce" a landing. What
is wrong with landings? Why are they so difficult?
The fact is that, no matter how hard you try, the conditions affecting a landing are never really exactly
the same. The wind speed and direction change continuously, the performance of the engine changes,
and even the wings change with the slightest change in the air temperature. Additionally, the pilot
reflexes (and alertness) change. It all combines to create an infinite number of unpredictable conditions
that make for an infinite number of possible ways to get it wrong.
We live in an analog world. All the input variables, temperature, wind speed and direction are analog.
All of our sensory system inputs (sight, sounds, pressure) are analog. The output, such as the move-
ment of the pilot's muscles to control the plane, is analog. With time, we learn to interpret (or should
I say convert) all the analog inputs from the world around us and make the best decisions we can.
Practice can make us perfect, almost!
In embedded control, the information from the analog world must first be converted to digital. The
analog-to-digital converter module is one of the key interfaces to the "real" world.
Flight plan
The PIC24 family was designed with embedded-control applications in mind and therefore is ideally
prepared to deal with the analog nature of the world. A fast analog-to-digital converter (ADC), capable
of 500,000 conversions per second, is available on all models with an input multiplexer that allows you
to monitor a number of analog inputs quickly and with high resolution. In this lesson we will learn how
to use the 10-bit ADC module available on the PIC24FJ128GA010 family to perform two simple mea-
surements on the Explorer 16 board: reading a voltage input from a potentiometer first and a voltage
input from a temperature sensor later.
141
Chapter 10
Preflight checklist
In addition to the usual software tools, including the MPLAB® IDE, MPLAB C30 compiler and the
MPLAB SIM simulator, this lesson will require the use of the Explorer 16 demonstration board and the
MPLAB ICD2 In-Circuit Debugger.
The flight
The first step in using the analog-to-digital converter, as with any other peripheral modules inside the
PIC24, is to familiarize yourself with the module building blocks and the key control registers. Yes, this
means reading the datasheet once more, and even the Explorer 16 User Guide to look at the schematics.
We can start by looking at the ADC module block diagram:
r -, Internal Data Bus
Comparator
Input MUX Control
Pin Config. Control
Conversion Control
Figure 10-1 . ADC module block diagram
142
It's an analog world
This is a pretty sophisticated structure that offers many interesting capabilities:
Up to 16 input pins can be used to receive the analog inputs.
Two input multiplexers can be used to select different input analog channels and different
reference sources each.
The output of the 10-bit converter can be formatted for integer or fixed-point arithmetic, and
signed or unsigned 16-bit output.
The control logic allows for many possible automated conversion sequences so as to synchro-
nize the process to the activity of other related modules and inputs
The conversion output is stored in a 16-bit wide, 16- words deep buffer that can be configured
for sequential scanning or simple FIFO buffering.
All these capabilities require a number of control registers to be properly configured and I understand
how, especially at the beginning, the number of options available and decisions to take could make you
a bit dizzy. So we will start by taking the shortest and simplest approach with the simplest example ap-
plication: reading the position of the R6 potentiometer on the Explorerl6 board.
■ RB
1QK
R12 AAA 470
WV^ — (RB5/AN5)
:12
NL
Figure 10-2. Detail of the Explorer 16 demonstration board, R6 potentiometer.
The 10 kohm potentiometer is connected directly to the power-supply rails so that its output can span
the entire range of values from 3.3V to the ground reference. It is connected to the RB5 pin that cor-
responds to the analog input AN5 of the ADC input multiplexer.
After creating a new project using the appropriate checklist, we can create a new source file "pot .c"
including the usual header file and adding the definition of a couple of useful constants. The first one,
pot, defines the input channel assigned to the potentiometer and the second one, ainputs, is a mask
that will help us define which inputs should be treated as analog and which ones as digital:
/*
** It's an analog world
** Converting the analog signal from a potentiometer
*/
#include <p24f j 128ga010 .h>
#define POT 5 // 10k potentiometer connected to AN5 input
#define AINPUTS Oxffef // Analog inputs for Explorerl6 POT and TSENS
143
Chapter 10
The actual initialization of all the ADC control registers can be best performed by a short function,
initADC ( ) , that will produce the desired initial configuration:
adipcfg will be passed the mask selecting the analog input channels: Os will mark the analog
inputs, and Is will configure the respective pins as digital inputs.
adiconi will set the conversion to start automatically, triggered by the completion of the
auto-timed sampling phase; also, the output will be formatted for a simple unsigned, right-
aligned (integer) value.
adicssl will be cleared, as no scanning function will be used (only one input).
AD1CON2 will select the use of MUXA and will connect the ADC reference inputs to the ana-
log input rails AVdd and AVss pins.
AD1CON3 will select the conversion clock source and divider.
Finally, setting adon, the entire ADC peripheral will be activated and ready for use.
void initADC ( int amask)
{
ADIPCFG = amask; // select analog input pins
ADICONI =
ADICSSL =
AD1CON2 =
// manual conversion sequence control
// no scanning required
// use MUXA, AVss and AVdd are used as Vref+/-
AD1CON3 = 0xlF02; // Tad = 2 x Tcy = 125ns >75ns
ADlCONlbits.ADON = 1; // turn on the ADC
} //initADC
By passing amask as a parameter to the initialization routine, we make it flexible enough to accept mul-
tiple input channels in future applications.
The first conversion
The actual analog-to-digital conversion is a two-step process. First we need to take a sample of the
input voltage signal, and then we can disconnect the input and perform the actual conversion of the
sampled voltage to a numerical value. The two distinct phases are controlled by two separate control
bits in the adiconi register: samp and done. The timing of the two phases is important to provide the
necessary accuracy of the measurement:
During the sampling phase the external signal is connected to an internal capacitor that needs
to be charged up to the input voltage. Enough time must be provided for the capacitor to track
the input voltage and this time is mainly proportional to the impedance of the input signal
source (in our case known to be less than 5 kohm) as well as the internal capacitor value. In
general, the longer the sampling time, the better the result, compatibly with the input signal
frequency (not an issue in our case).
The conversion phase timing depends on the selected ADC clock source. This is typically
derived by the main CPU clock signal via a divider or alternatively by an independent RC
oscillator. The RC option, although appealing for its simplicity, is a good choice when a
conversion needs to be performed during a sleep (low-power mode) phase, when the CPU
144
It's an analog world
clock is turned off. The oscillator clock divider is a better option in more general cases, since
it provides synchronous operation with the CPU and therefore a better rejection of the noise
internally produced by it. The conversion clock should be the fastest possible, compatibly
with the specifications of the ADC module (in our case Tad is required to be longer than 75 ns,
requiring a minimum clock divider by two).
Here is a basic conversion routine:
int readADC ( int ch)
{
AD1CHS = ch; // 1. select analog input channel
ADlCONlbitS.SAMP = 1;
// 2. start sampling
TMR1 = 0;
while (TMR1< 100) ;
// 3. wait for sampling time
// 6.25 us
ADlCONlbits.DONE = 1;
// 4. start the conversion
while (! ADlCONlbits.DONE ) ; // 5. wait for the conversion to complete
return ADC1BUF0;
// 6. read the conversion result
} // readADC
Automatic sampling timing
As you can see, using this basic method, we have been responsible for providing the exact timing of
the sampling phase, dedicating a timer to this task and performing two waiting loops. But on the PIC24
there is a new option that allows for a more automatic process. The sampling phase can be self timed,
provided the input source impedance is small enough to require a maximum sampling time of 32 x Tad
(32 x 120 ns = 3.8 us in our case). This can be achieved by setting the ssrc bits in the adiconi regis-
ter to the Obi 11 configuration, so as to enable an automatic start of the conversion upon termination of
a self-timed sampling period. The period itself is selected by the AD1CON3 register sam bits. Here is a
new and improved example that uses the self-timed sampling and conversion trigger:
void initADC ( int amask)
{
AD1PCFG = amask;
ADICONI = 0x0 0E0;
AD1CSSL = 0;
AD1CON2 = 0;
AD1CON3 = 0xlF02;
ADlCONlbits.ADON = 1
} //initADC
// select analog input pins
// automatic conversion start after sampling
// no scanning required
// use MUXA, AVss and AVdd are used as Vref+/-
// Tsamp = 32 x Tad; Tad=12 5ns
// turn on the ADC
Notice how making the conversion-start be triggered automatically by the completion of the self-timed
sampling phase gives us two advantages:
145
Chapter 10
Proper timing of the sampling phase is guaranteed without requiring us to use any timed delay
loop and/or other resource.
One command (start of the sample phase) suffices to complete the entire sampling and conver-
sion sequence.
With the ADC so configured, starting a conversion and reading the output is a trivial matter:
• adichs selects the input channel for MUXA.
Setting the samp bit in adiconi starts the timed sampling phase, which will be immediately
followed by the conversion.
The done bit will be set in the adiconi register as soon as the entire sequence is completed
and a result is ready.
Reading the adcibufo register will immediately return the desired conversion result.
int readADC ( int ch)
{
ADICHS = ch; // 1. select analog input channel
ADlCONlbits.SAMP = 1; // 2. start sampling
while ( ! ADlCONlbits .DONE) ; // 3. wait for the conversion to complete
return ADCIBUFO; // 4. read the conversion result
} // readADC
Developing a demo
All that remains to do at this point is to figure out an entertaining way to put the converted value to use
on the Explorer 16 demo board. The LEDs connected to PORTA are an obvious choice, but instead of
simply providing a binary output, publishing the eight most significant bits of the 10-bit result, why not
jazz things up a little and provide a visual feedback more reminiscent of the analog nature of our input?
We could turn on one LED at a time, using it as an index on a mechanical dial. Here is the main routine
we will use to test our analog-to-digital functions:
main ( )
{
int a ;
// initializations
initADC( AINPUTS) ; // initialize the ADC for the Explorerl6 analog inputs
TRISA = OxffOO; // select the PORTA pins as outputs to drive the LEDs
// main loop
while ( 1)
{
146
It's an analog world
a = readADC ( POT); // select the POT input and convert
// reduce the 10-bit result to a 3 bit value (0. .7)
// (divide by 128 or shift right 7 times
a >>= 7 ;
// turn on only the corresponding LED
// -> leftmost LED. . . . 7-> rightmost LED
PORTA = (0x80 >> a) ;
} // main loop
} // main
After the call to the initialization routine (to which we provide a mask that defines bit 5 as analog
input), we initialize the trisa register to make the pins connected to the LED bar digital outputs.
Then, in the main loop we perform the conversion on AN5 and we reformat the output to fit our special
display requirements. As configured, the 10-bit conversion output will be returned as a right-aligned
integer in a range of values between and 1024. By dividing that value by 128 (or in other words shift-
ing it right seven times) we can reduce the range to a to 7 value. The final output, though, requires
one more transformation to produce the eight desired LED configurations. Note that the LED cor-
responding to the MSB is located to the left of the bar, and to maintain the correspondence between
the potentiometer movement clockwise and the index LED moving to the right we need to start with a
Obiooooooo pattern and shift it right as required.
Build the project and, following the usual ICD2 debugging checklist, program the Explorer 16 board. If
all goes well, you will be able to play with the potentiometer, moving it from side to side while observ-
ing the LED index moving left and right correspondingly.
Developing a game
OK, I will admit it, the previous example was not too exciting. After all, we have been using a
16-MIPS capable 16-bit machine to perform an analog-to-digital conversion roughly 200,000 times
a second (32 Tad sampling +12 Tad conversion, where Tad =125 ns, you do the math) only to dis-
card all but three bits of the result and watch a single LED light up. How about making it a bit more
challenging and playful instead? How about developing a little "Whac-A-Mole" 1 game, just a mono-
dimensional version?
Let's turn on a second LED (the mole), controlled by the PIC24 and distinguishable from the user-con-
trolled LED (the mallet) because it's somewhat dimmer. By moving the mallet (bright LED), rotating
the potentiometer until you reach the mole (dim LED), you will get to "whack it"! A new mole, in a
different random position will immediately appear and the game will continue.
The pseudo-random number generator function rand ( ) (defined in "stdlib . h") will be helpful here,
as all (computer) games need a certain degree of unpredictability. We will use it to determine where to
place each new mole.
1 Whac-a-Mole is a trademark of Bob's Space Racers Inc.
147
Chapter 10
Save the source file from the first project with a new name "LEDgame . c" and create an entire new proj
ect. Then update the main ( ) function to include just a few more lines of code:
main ( )
{
int a , r , c ;
// 1. initializations
initADC( AINPUTS) ; // initialize the ADC for the Explorerl6 analog inputs
TRISA = OxffOO; // select the PORTA pins as outputs to drive the LEDs
// 2. use the first reading to randomize the number generator
srand( readADC ( POT) ) ;
r = 0x80;
c = 0;
/ / 3 . main loop
while ( 1)
{
a = readADC ( POT); // select the POT input and convert
// 3.1 reduce the 10-bit result to a 3 bit value (0..7)
// (divide by 128 or shift right 7 times
a >>= 7 ;
// 3.2 turn on only the corresponding LED
// -> leftmost LED.... 7-> rigtmost LED
a = (0x80 >> a) ;
// 3.3 as soon as the cursor hits the random dot, generate a new one
while (a == r )
r = 0x8 >> (rand() & 0x7);
// 3.4 display the user (bright) LED and food (dim) LED
if ( (c & Oxf ) == 0)
PORTA = a + r; // add food LED only 1/16 of the times (dim)
else
PORTA = a; // always display the user LED (bright)
// 3.5 loop counter
C + +;
} // main loop
} // main
148
It's an analog world
In 1 , we perform the usual initialization of the analog-to-digital converter module and the
PORTA I/Os connected to the bar of LEDs.
In 2, we read the potentiometer value for the first time and we use its position as the SEED
value for the random-number generator. This makes the game experience truly unique each
time, provided the potentiometer is not always found in the leftmost or rightmost position.
That would provide a SEED value of or 1023, respectively, every time and therefore would
make the game quite repetitive as the pseudo-random sequence would proceed through the
same steps at any game restart.
In 3, the main loop begins, as in the previous example, reading an integer 10-bit value and
reducing it to the three most significant bits. (3.1).
In 3.2, the conversion into an LED position "a" is performed just as before, but it is in 3.3 that
things get interesting. If the position of the user LED represented by "a" is overlapping the
"mole" LED position "r", a new random position is immediately calculated. The operation
needs to be repeated as a while loop because, each time a new random value for "r" is cal-
culated, there is a chance (exactly 1/8 if our pseudo-random generator is a good one) that the
new value could be the same. In other words we could be creating a new "mole" right under
the mallet. And that would not be very challenging or sporting. Don't you agree?
Steps 3.4 and 3.5 are all about displaying and differentiating the two LEDs. To show both
LEDs on the display bar, we could simply "add" the two binary patterns "a" and "r" but it
would be very hard for the player to tell which is which. To represent the "mole" LED with
a dimmer light, we can alternate cycles of the main loop where we present both LEDs and
cycles where only the "mallet" LED is visible. Since the main loop is executed hundreds of
thousands of time per second, our eye will perceive the "mole" LED as dimmer, proportion-
ally to the number of cycles it is missing. For example, if we add the "mole" LED only once
every 16 cycles, its apparent brightness will be only 1/16 that of the "mallet" LED.
The counter w c", constantly incremented in 3.5, helps us to implement this mechanism.
In 3.4 we look only at the 4 lsb of the counter (0. . . 15) and we add the "mole" LED to the
display only when their value is ObOOOO. For the remaining 15 loops, only the "mallet" LED
will be added to the display.
Build the project and download it to the Explorer 16 board. You have to admit that it's much more
entertaining now !
Measuring temperature
Moving on to more serious things, there is a temperature-sensor mounted on the Explorer 16 board and
it happens to be a Microchip TCI 047 A integrated temperature-sensing device with a nice linear volt-
age output. This device is very small, as it is offered in a SOT-23 (three-pin, surface-mount) package.
The power consumption is limited to 35 uA (typ.) while the power supply can cover the entire range
from 2.5V to 5.5V. The output voltage is independent from the power supply and is an extremely linear
function of the temperature (typically within 0.5 degree C) with a slope of exactly 10 mV/C. The offset
is adjusted to provide an absolute temperature indication according to the formula seen in Figure 10-3.
149
Chapter 10
1.75
1.7
1.5
o
>
1.3
1.1
H
o
>
0.9
0.7
0.5
0.3
0.1
Vout = (10 mV/°C) (Temperature °C) + 500 mV
-40 -30 -20 -10 10 20 30 40 50 60 70 80 90 100 110 120 125
Temperature (°C)
Figure 10-3. TC1047 Output Voltage vs. Temperature characteristics.
We can apply our newly acquired abilities to convert the voltage output to digital information using,
once more, the analog-to-digital converter of the PIC24. The temperature sensor is directly connected
to the AN4 analog input channel as per the Explorer 16 board schematic.
4-3.3V
R24 A A A 100
WV 1 ^ L |RB4/AN4)
Figure 10-4. Detail of the Explorer 16 demonstration board, TC1047A temperature sensor.
We can reuse the ADC functions developed for the previous exercise and put them in a new project
called "TSense" and save the previous source file as "Tsense . c".
Let's start modifying the code to include a new constant definition: tsens for the ADC input channel
assigned to the temperature sensor:
/*
** It's an analog world
** Converting the analog signal from a TC1047 Temperature Sensor
*/
#include <p24f j 128ga010 .h>
150
It's an analog world
#define POT 5 // 10k potentiometer connected to AN5 input
#define TSENS 4 // TC1047 Temperature sensor with voltage output
#define AINPUTS Oxffcf // Analog inputs for Explorerl6 POT and TSENS
// initialize the ADC for single conversion, select Analog input pins
void initADC ( int amask)
{
ADlPCFG = amask; // select analog input pins
AD1CON1 = OxOOEO; // auto convert after end of sampling
AD1CSSL =0; //no scanning required
AD1CON3 = 0xlF02; // max sample time = 3lTad, Tad = 2 x Tcy = 125ns >75ns
AD1CON2 =0; //use MUXA, AVss and AVdd are used as Vref+/-
ADlCONlbits.ADON = 1; // turn on the ADC
} //initADC
int readADC ( int ch)
{
AD1CHS = ch; // select analog input channel
ADlCONlbits . SAMP = 1; // start sampling, auto-conversion will follow
while (! ADlCONlbits .DONE) ; // wait to complete the conversion
return ADC1BUF0; // read the conversion result
} // readADC
As you can see, nothing else needed to change with regard to the ADC configuration or activation of
the conversion sequence. Presenting the result on the LED bar might be a little tricky though. Tempera-
ture sensors provide a certain level of noise and, to give a more stable reading, it is common to perform
a little filtering. Taking groups of 16 samples and performing an average will give us a cleaner value to
work with:
a = 0;
for ( j= 16; j >0; j — )
a += readADC ( TSENS); // add up 16 successive temperature readings
i = a >> 4; // divide the result by 16 to obtain the average
But how could we display the result using only the bar of LEDs?
We could pick the most significant bits of the conversion result and publish them in binary or BCD, but
once more it would not be fun. How about providing instead a relative temperature indication using a
similar (single LED) index moving along the LED bar?
We will sample the initial temperature value just before the main loop and use it as the offset for the
center bar position. In the main loop we will update the dot position, moving it to the right as the
sensed temperature increases or to the left as the sensed temperature decreases. Here is the complete
code for the new temperature- sensing example:
main ( )
{
int a , i , j ;
151
Chapter 10
II 1. initializations
initADC( AINPUTS); // initialize the ADC for the Explorerl6 analog inputs
TRISA = OxffOO; // select the PORTA pins as outputs to drive the LEDs
T1CON = 0x8030; // TMRl on, prescale 1:256 Tclk/2
// 2. sample initial temp value
a = 0;
for ( j= 16; j >0; j--)
a += readADC ( TSENS); // read the temperature
i = a >> 4;
// this will give the central bar reference
/ / 3 . main loop
while ( 1)
{
// 3.1 read a new (averaged) temperature value
a = 0;
for ( j= 16; j >0; j--)
{
TMRl = 0;
while ( TMRl < 3900); // 3900 x 256 x Tcy ~= lsec
a += readADC ( TSENS); // read the temperature
}
a >>= 4; // averaged over 16 readings
// 3.2 compare with the initial reading and move the bar 1 pos . per C
a = 3 + (a - i) ;
// 3.3 keep the result in the value range 0..7, keep the bar visible
if ( a > 7)
a = 7;
if ( a < 0)
a = 0;
// 3.4 turn on the corresponding LED
PORTA = ( 0x80 >> a) ;
} // main loop
} // main
In 3.2, we determine the difference between the initial reading "i" and the new averaged read-
ing "a". The result is centered, so that a central LED is lit up when the difference is zero.
In 3.3 the result is checked against the boundaries of the available display. Once the difference
becomes negative and more than three bits wide, the display must simply indicate the leftmost
position. When the difference is positive and more than four bits wide, the rightmost LED
must be activated.
152
It's an analog world
In 3.4 we publish this result as in the previous example.
To complete the exercise and give you a more aesthetically pleasing experience, I recommend that you
also introduce an additional delay loop (for convenience inserted inside the 3.1 averaging loop). This
will slow things down quite a bit, reducing the update rate of the display (and eventually the entire
main loop cycle) to a period of about one second. A faster update rate would produce only an annoying
flicker when the temperature readings are too close to the intermediate values between two contiguous
dot positions.
Build the project with the usual checklists and download it to the Explorer 16 board.
After identifying the temperature sensor on the board (hint: it is close to the lower left corner of the
PIC24 processor module and it looks like a surface-mount transistor), run the program and observe
how small temperature variations, obtained by touching or blowing hot/cold air on the sensor, move the
cursor around.
The breath-alizer game
To have a bit more fun with the temperature sensor, we can now merge the last two exercises into one
new game. We'll call it the "Breath-alizer" game. The idea is to whack the "mole" (dim) LED by con-
trolling the "mallet" using the temperature sensor. Heat the sensor up with some hot air to move it to
the right, blow cold air on it to move it to the left. Have fun!
main ( )
{
int a , i , j , k , r ;
// 1. initializations
initADC( AINPUTS) ; // initialize the ADC for the Explorerl6 analog inputs
TRISA = OxffOO; // select the PORTA pins as outputs to drive the LEDs
T1CON = 0x8030; // TMRl on, prescale 1:256 Tclk/2
// 2. use the first reading to randomize the number generator
srand( readADC ( TSENS));
// generate the first random position
r = 0x80 >> (rand() & 0x7);
k = 0;
// 3. compute the average value for the initial reference
a = 0;
for ( j= 16; j >0; j--)
a += readADC ( TSENS); // read the temperature
i = a > > 4 ;
// 5. main loop
while ( 1)
{
// 5.1 take the average value over 1 second
a = 0;
for ( j= 16; j >0; j--)
153
Chapter 10
TMRl = 0;
while ( TMRl < 3900) // 16 x 3900 x 256 x Tcy ~= lsec
{ // display the user LED and dim random LED
if ( (TMRl 8c Oxf ) == 0)
PORTA = k + r;
else
PORTA = k ;
}
a += readADC ( TSENS) ; // read the temperature
}
a >>= 4; // averaged over 16 readings
// 5.2 compare with the initial reading and move the bar 1 pos . per C
a = 3 + (a - i) ;
// keep the result in the value range 0. .7, keep the bar visible
if ( a > 7)
a = 7;
if ( a < 0)
a = 0;
// update the user LED
k = ( 0x80 >> a) ;
// 5.3 as soon as the user hits the random LED, generate a new position
while (k == r )
r = 0x80 >> (rand() & 0x7);
} // main loop
} // main
Post-flight briefing
In this lesson with have just started scratching the surface and exploring the possibilities offered by the
analog-to-digital converter module of the PIC24. We have used one simple configuration of the many
possible and only a few of the advanced features available. We have tested our newly acquired capabili-
ties with two types of analog inputs available on the Explorer 16 board, and hopefully we had some fun
in the process.
Notes for C experts
Even if the PIC24 has a fast divide instruction, there is no reason to waste any processor cycles. In
embedded control, "every" processor cycle is precious. If the divisor is a power of two, the integer
division can be best performed as a simple shift right by an appropriate number of positions with a
computational cost that is at least an order of magnitude smaller than a regular division. If the divider
154
It's an analog world
is not a power of two, consider changing it if the application allows. In our last example, we could have
opted for averaging 10 temperature samples, or 15 as well as 20, but we chose 16 because this made the
division a simple matter of shifting the sum by 4 bits to the right (in a single cycle PIC24 instruction).
Tips and tricks
If the sampling time required is longer than the maximum available option (32 x Tad) you can try to
extend Tad first, or a better option is to swap things around and enable the automatic sampling start
(at the end of the conversion). This way the sampling circuit is always open, charging, whenever the
conversion is not occurring. Manually clearing the samp bit will trigger the actual conversion start.
Further, having Timer3 periodically clearing the samp control bit for you (one of the options for the
ssrc bits in adiconi), and enabling the ADC end of conversion interrupt will provide the widest
choice of sampling periods possible for the least amount of MCU overhead possible. No waiting loops,
only a periodic interrupt when the results are available and ready to be fetched.
Exercises
1. Use the ADC FIFO buffer to collect conversion results; set up Timer 3 for automatic conver-
sion and the interrupt mechanism so that a call is performed only once the buffer is full and
temperature values are ready to be averaged.
Books
• Baker, B.
A Baker's Dozen: Real Analog Solutions for Digital Designers
Newnes, Burlington, MA
For proper care and feeding of an analog-to-digital converter, look no further than Bonnie's
cookbook.
Links
http://www. microchip, com/stellent/idcplg ?IdcService = SS_GET_PAGE&nodeId=2 1 02 ¶m
=en021419&pageld= 79&pageld= 79
Temperature sensors are available in many flavors and with a choice of interface options, including
direct PC™ or SPI digital output.
155
PART
III
Cross -Country
Flying
Part III - Cross-country flying
Congratulations ! You have endured a few more lessons and gained the ability to complete more com-
plex flights. You are now going to enter the third and last part of your training where you'll practice
cross-country flying. No more pattern work around the airport, no more landings and take-offs, or
maneuvers in the practice area — you will finally get to go somewhere !
In the third part of this book, we will start developing new projects that will require you to master sev-
eral peripheral modules at once. Since the examples will become a bit more complex, not only having
an actual demonstration board (the Explorer 16) at hand is recommended, but also having the ability to
perform small modifications and utilize the prototyping area to add new functionality to the demonstra-
tion board will be necessary. Simple schematics and component part numbers will be offered in the
following chapters as required. On the companion web site "FlyingthePIC24.com" (and/or "Program-
mingthePIC24.com") you will find additional expansion boards and prototyping options to help you
enjoy even the most advanced projects.
159
CHAPTER
11
Capturing inputs
In This Chapter
►
The PS/2 communications protocol
Another method - Change Notification
►
Interfacing a PIC24 to the PS/2
►
Evaluating cost
►
Input capture
►
A Third method - I/O Polling
►
Testing the Input Capture
►
Testing the I/O poling method
method using Stimulus Scripts
►
Cost and efficiency of the solution
►
Testing the PS/2 receive routines
►
Completing the interface:
►
The simulation
adding a FIFO buffer
►
The simulator profile
►
Completing the interface:
performing key codes decoding
As we were saying in a previous chapter, advanced electronics is rapidly gaining space in the cockpits
of all but the smallest airplanes. While the glass (LCD) displays are supplanting the old steam gauges,
GPS satellite receivers are plotting the airplane position in real time on colorful maps depicting terrain
elevations and, with additional equipment, up-to-the-minute satellite weather information too. Pilots
can enter an entire flight plan in the navigation system and then follow their path on the moving map,
just like in a video game. The interaction with these new instruments, though, is becoming the next
big challenge. Just as with computer applications, each instrument is controlled by a different menu
system and a set of knobs and buttons to allow the pilot to provide the inputs quickly and, hopefully,
intuitively. However, the limited space in the cockpit has so far imposed serious limitations on the type
and number of such input devices, which for the most part — at least in the first generations — have been
mimicking the knobs and buttons of the primitive VHF radios.
If you have a GPS navigation system in your car and you have tried to dial in a street address in a
foreign city (say "Bahnhofstrasse, 17, Munich) by twisting and turning that little knob while driving on
a highway .. .well, you know exactly the type of challenge I am talking about. Keyboards are the logi-
cal next level of input interface for several advanced avionics (aviation electronics) systems. They are
already common in business jet cockpits, but they are starting to make their appearance in the smaller
general aviation airplanes too. How about a keyboard in your next car?
Flight plan
With the advent of the USB bus, computers have finally been freed from a number of "legacy" in-
terfaces that had been in use for decades since the introduction of the first IBM PC. The PS/2 mouse
and keyboard interface is one of them. The result of this transition is that a large number of the "old"
161
Chapter 1 1
keyboards are now flooding the surplus market and even new PS/2 keyboards are selling for very low
prices. This creates the opportunity to give our future PIC24 projects a powerful input capability. It will
also give us the motivation to investigate a few alternative interface methods and their trade-offs. We'll
implement software state machines, refresh our experience using interrupts and possibly learn to use
new peripherals.
The flight
The physical PS/2 port uses a 5 -pin DIN or a 6-pin mini-DIN connector. The first was common on the
original IBM PC-XT and AT series but has not been in use for a while now. The smaller 6-pin version
has been more common in recent years. Once the different pin-outs are taken into consideration, the
two are electrically identical.
Male
Female
(Plug)
(a)
(Socket)
(b)
5-pin DIN (AT/XT):
1 - Clock
2 - Data
3-NC
4 - Ground
5 - Vcc (+5V)
Male
Female
6-pin Mini-DIN (PS/2):
1 - Data
5|6\
• ■ • \
• I
1 2 /
/6|5\
(4 3)
\ 2 1 /
\o 0/
2-NC
3 - Ground
(Plug)
(Socket)
4 - Vcc (+5V)
(c)
(d)
5 - Clock
6-NC
Figure
11-
■1a-d. PS/2 Physical Interface.
The host must provide a 5V power supply. The current consumption will vary with the keyboard model
and year, but you can expect values between 50 and 100 mA (the original specifications used to call for
up to 275 mA max).
The data and clock lines are both open-collector with pull-up resistors (1-10 kohm) so as to allow for
two-way communication. In the normal mode of operation, it is the keyboard that drives both lines in
order to send data to the personal computer. When it is necessary, though, the computer can take con-
trol to configure the keyboard and to change the status LEDs ("Caps" and "Num Lock").
162
Capturing inputs
The PS/2 communication protocol
At idle, both the data and clock lines are held high by the pull-ups (located inside the keyboard). In this
condition the keyboard is enabled and can start sending data as soon as a key has been pressed. If the
host holds the clock line low for more than 100 us, any further keyboard transmissions are suspended.
If the host holds the data line low and then releases the clock line, this is interpreted as a request to
send a command.
CLOCK
DATA
vx)C0300000^r
BITO BIT 2 BIT 4 BIT6 PARITY
START BIT1 BIT 3 BIT 5 BIT 7
Figure 7 1-2. Keyboard to Host communication waveform.
The protocol is a curious mix of synchronous and asynchronous communication protocols that we
have seen in previous chapters. It is synchronous since a clock line is provided, but it is similar to an
asynchronous protocol since a start, a stop and a parity bit are used to bracket the actual 8-bit data
packet. Unfortunately, the baud rate used is not a standard value and can change from unit to unit, over
time, with temperature and the phase of the moon. In fact, typical values range from 10 to 16 kbit per
second. Data changes during the clock-high state. Data is valid when the clock line is low. Whether
data is flowing from the host to the keyboard or vice versa, it is the keyboard that always generates the
clock signal.
Note: The USB bus reverses the roles as it makes each peripheral a synchronous slave of the host.
This simplifies things enormously for a non-real-time, nonpreemptive multitasking operating sys-
tem like Windows®. The serial port and the parallel port were similarly asynchronous interfaces,
and — probably for the same reason — both became legacy with the introduction of the USB bus
specification.
Interfacing a PIC24 to the PS/2
The unique peculiarities of the protocol make interfacing to a PS/2 keyboard an interesting challenge,
as neither the PIC24 SPI interface nor the UART interface can be used. In fact, the SPI interface does
not accept 1 1-bit words (8-bit or 16-bit words are the only options), while the PIC24 UART would
require the periodic transmission of special break characters to make use of the powerful auto baud-
rate detection capabilities. Also notice that the PS/2 protocol is based on 5V-level signals. This requires
care in choosing which pins can be directly connected to the PIC24. In fact, only the 5V-tolerant digital
input pins can be used, which excludes the I/O pins that are multiplexed with the analog-to-digital
converter.
Input Capture
The first idea that comes to mind is to implement in software a PS/2 serial interface peripheral using
the Input Capture mechanism.
163
Chapter 1 1
ICx pin
Prescaler
Counter
(1,4, 16)
f
Edge Detection Logic
and
Clock Synchronizer
^
ICM<2:0>(ICxCON<2:0>)
Mode Select
J
ICOV, ICBNE(ICxCON<4:3>)
From 16-bit Timers
TMRy TMRx
ICI<1:0>
ICxCON
V
System Bus
Set Flag ICxIF
(in IFSn Register)
16
16
\l 07 ICTMR
' (ICxCON<7>)
Note: An 'x' in a signal, register or bit name denotes the number of the capture channel.
Figure 1 1-3. Input Capture module block diagram.
Five Input Capture modules are available on the PIC24FJ128GA010, connected respectively to the
IC1-IC5 pins multiplexed on PORTD pins 8, 9, 10, 1 1 and 12.
Each Input Capture module is controlled by a single corresponding control register ICxCON and works
in combination with one of two timers, either Timer2 or Timer3.
One of several possible events can trigger the input capture:
rising edge
falling edge
rising and falling edge
4 th rising edge
16 th rising edge.
The current value of the selected timer is recorded and stored in a FIFO buffer to be retrieved by read-
ing the corresponding icxbuf register. In addition to the capture event, an interrupt can be generated
after a programmable number of events (each time, every second, every third or every fourth).
To put the Input Capture peripheral to use and receive the data stream from a PS/2 keyboard, we can
connect the IC 1 input to the clock line and configure the peripheral to generate an interrupt on each and
every falling edge of the clock.
164
Capturing inputs
Falling Edge
Input Capture Event
Clock Line
Data Line
Figure 1 1-4. PS/2 Interface Bit Timing and the Input Capture trigger event.
After creating a new project, and following our usual template, we can start adding the following ini-
tialization code:
#define PS2DATA _RG12
#define PS2CLOCK RD8
// any available 5V tolerant input
// use the IC1 module input pin
void initKBD( void)
{
// clear the flag
KBDReady = 0;
_TRISD8 = 1;
_TRISG12 = 1;
IC1CON = 0x0002;
_IC1IF = 0;
_IC1IE = 1;
// make IC1 = RD8 pin an input (clock)
// make the RG12 pin an input (data)
// use TMR3 , int every capture, falling edge
// clear the interrupt flag
// enable the IC1 interrupt
} // void initKBD
We will also need to create an interrupt service routine for the IC 1 interrupt vector. This routine will
have to operate as a state machine and perform the following steps in sequence:
1. Verify the presence of a start bit (data line low).
2. Shift in 8 bits of data and compute a parity.
3. Verify a valid parity bit.
4. Verify the presence of a stop bit (data line high).
If any of these checks fails, the state machine must reset and return to the start condition. When a
valid byte of data is received, we will store it in a buffer — think of it as a mailbox — and a flag will be
raised so that the main program or any other "consumer" routine will know a valid key code has been
received and is ready to be retrieved. To fetch the code, it will suffice to copy it from the mailbox first
and then to clear the flag.
165
Chapter 1 1
Data = high
Bitcount < 8
Figure 1 1-5. The PS/2 receive state machine diagram.
The state machine requires only four states and a counter, and all the transitions are summarized in
Table 11-1:
State
Conditions
Effect
Start
Data = low
Init Bitcount,
In it Parity,
Transition to Bit state
Bit
Bitcount < 8
Shift in key code, LSB first (shift right),
Update Parity
Increment Bitcount
Bitcount = 8
Transition to Parity state
Parity
Parity = even
Error. Transition back to Start
Parity = odd
Transition to Stop
Stop
Data = low
Error. Transition back to Start
Data = high
Save the key code in buffer,
Set flag,
Transition to Start
Table 11-1. PS/2 receive state machine transitions table.
Theoretically we should consider this an 1 1 -state machine, counting each time the Bit state is entered
with a different Bitcount value as a distinct state. But the four-state model works best for an efficient
C-language implementation. Let's define a few constants and variables that we will use to maintain the
state machine:
166
Capturing inputs
I / definition of the keyboard PS/2 state machine
#define PS2 START
#define PS2BIT 1
#define PS2 PARITY 2
#define PS2ST0P 3
// PS2 KBD state machine and buffer
int PS2State;
unsigned char KBDBuf; // temporary buffer
int KCount, KParity, ; // bitcount and parity
// key code flag and mailbox
volatile int KBDReady;
volatile unsigned char KBDCode;
Note: The keyword volatile is used as a modifier in a variable declaration to alert the compiler
that the content of the variable could change unpredictably as a consequence of an interrupt or
other hardware mechanism. We use it here to prevent the compiler from applying any optimiza-
tion technique (loop extraction, procedure abstraction..) whenever these two variables are used.
Admittedly, we could have omitted the detail in this code example (after all, all optimizations are
supposed to be turned off during debugging), only to find ourselves with a big headache in the
future, when using this code in a more complex project and trying to squeeze it to get the highest
possible performance. KBDReady and KBDcode are the only two variables used in both the interrupt
service routine and the main interface code.
The interrupt service routine for the input capture IC1 module can finally be implemented using a
simple switch statement that performs the entire state machine.
void _ISR _IClInterrupt ( void)
{ // input capture interrupt service routine
switch( PS2State) {
default :
case PS2 START:
if ( ! PS2DAT)
{
KCount =8; // init bit counter
KParity =0; // init parity check
PS2State = PS2BIT;
}
break;
167
Chapter 1 1
case PS2BIT:
KBDBuf »=1;
if ( PS2DAT)
KBDBuf += 0x8 0;
KParity A = KBDBuf;
if ( --KCount == 0)
PS2State = PS2PARITY;
break;
// shift in data bit
// update parity
// if all bit read, move on
case PS2PARITY:
if ( PS2DAT)
KParity A = 0x80;
if ( KParity & 0x80)
PS2State = PS2STOP;
else
PS2State = PS2START;
break;
// if parity is odd, continue
case PS2STOP:
if ( PS2DAT)
{
KBDCode = KBDBuf;
KBDReady = 1;
}
PS2State = PS2START;
break;
// verify stop bit
// save the key code in mail box
// set flag, key code available
} // switch state machine
// clear interrupt flag
_IC1IF = 0;
} // IC1 Interrupt
Testing the Input Capture method using Stimulus Scripts
The small perforated prototyping area can be used to attach a PS/2 mini-DIN connector to the
Explorer 16 demonstration board, the only alternative being the development of a custom daughter
board (PICTail™) for the expansion connectors. Before committing to designing such a board though,
we would like to make sure that the chosen pin-out and code is going to work. The MPLAB® SIM
software simulator is going to be, once more, our tool of choice.
While in previous chapters we have used the software simulator in conjunction with the Watch window,
the Stopwatch, and the Logic Analyzer to verify that our programs were generating the proper tim-
ings and outputs, this time we will need to simulate inputs as well. To this end MPLAB SIM offers a
considerable number of options and resources, so many in fact that the system might seem a bit intimi-
dating at first. First of all, the simulator offers two types of input stimuli: asynchronous ones, typically
triggered manually by the user, and synchronous ones, triggered automatically by the simulator after a
168
Capturing inputs
scripted amount of time (expressed in processor cycles or seconds). The script files (.SCL) containing
the descriptions of the synchronous stimuli (that can be quite complex) can be prepared using a conve-
nient tool, called the SCL Generator. You can invoke the SCL Generator by selecting "SCLGenerator^
New Workbook" from the Debugger menu. In order to prepare the simplest type of stimulus script, one
that assigns values to specific input pins (and entire registers) at given points in time, you can select the
first tab in the Generator window: "Pin/Register Actions".
After selecting the unit of measurement of choice, microseconds in our case, click on the first row of the
table that occupies most of the dialog box window space (where it says "Click here to Add Signals"). This
will allow you to add columns to the table. Add one column for every pin for which you want to simulate
inputs. In our example, that would be RG12 for the PS2 Data line and IC1 for the Input Capture pin that
we want connected to the PS2 Clock line. At this point we can start typing in the stimulus timing table.
To simulate a generic PS/2 keyboard transmission, we will need to produce a 10-kHz clock signal for 1 1
cycles as represented in the PS/2 keyboard waveform in Figure 1 1-4. This requires an event to be inserted
in the timing table every 50 us. As an example, Table 11-2 illustrates the trigger events I recommend you
add to the SCL Generator timing table to simulate the transmission of key code 0x79.
SCL Workbook - C:\„.\IC PS2 simulation.sbs
^jnjxl
Pin / Register Actions | Advanced Pin / Register ] Clock Stimulus | Register Injection | Register Trace
Time Units | us JJ
l~" Repeat after f
(decimal I
Time
RG12
IC1
Click here to Add Signals
a
(dec)
(bin}
[bin]
1
1
100
1
1
150
200
1
1
250
1
300
350
1
1
400
1
450
500
1
1
zl
EKT»
1
n
Generate SCL From Workbook
Delete Row
Save Workbook
Help
Figure 1 1-6. The SCL Generator window.
169
Chapter 1 1
Time (us)
RG12
IC1
Comment
1
1
Idle state, both lines are pulled up
100
1
1
150
First falling edge, Start bit (0)
200
1
1
250
1
Bit 0, key code LSb (1)
300
1
350
Bit 1 (0)
400
1
450
Bit 2 (0)
500
1
550
Bit 3(1)
600
1
650
Bit 4(1)
700
1
750
Bit 5(1)
800
1
850
Bit 6 ( 1)
900
1
950
Bit 7, key code MSb (0)
1000
1
1050
Parity bit (0)
1100
1
1
1150
1
Stop bit (1 )
1200
1
1
Idle
Table 1 1-2. SCL Generator timing example for basic PS/2 simulation.
Once the timing table is filled, you can save the current content for future use with the "Save Work-
book" button. The file generated will be an ASCII file with the . sbs extension. In theory, you could
edit this file manually with the MPLAB IDE editor or any basic ASCII editor, but you are strongly
discouraged from doing so. The format is more rigid than meets the eye and you might end up trash-
ing it. If you were wondering why the term "Workbook" is used for what looks like a simple table, you
are invited to explore the other panes (accessible clicking on the tabs at the top of the dialog box) of
the SCL Generator. You will see that what we are using in this example is just one of the many stimuli
generation methods available, representing a minuscule portion of the capabilities of the SCL Genera-
tor. A Workbook file can contain a number of different types of stimuli produced by any (or several) of
those panes.
170
Capturing inputs
A segment of the SCL Generator Workbook file is shown here:
## SCL Builder Setup File: Do not edit!!
## VERSION: 3.22.00.00
## FORMAT: vl.4 0.0
## DEVICE: PIC24FJ128GA010
## PINREGACTIONS
us
No Repeat
RG12
IC1
1
1
100
1
1
150
200
1
1
At this point an actual stimulus script file can be generated from the timing table we just defined.
Stimulus script files have a . scl extension and are, once more, simple ASCII text files. The script files
contain the real commands and information that the MPLAB SIM simulator will use to simulate the
actual input signals. A segment of the stimulus file is shown here:
//
// .../IC PS2 simulation. scl
// Generated by SCL Generator ver . 3.22.00.00
// DATE TIME
//
configuration for "pic24f j 128ga010" is
end configuration;
testbench for "pic24f j 128ga010 " is
begin
171
Chapter 1 1
process is
begin
wait for us;
report "Stimulus actions after us";
RG12 <= v 1 ' ;
IC1 <= ! 1';
wait;
end process;
process is
begin
wait for 100 us;
report "Stimulus actions after 100 us";
RG12 <= l l' ;
IC1 <= 'l';
wait;
end process;
You might notice a certain resemblance between the notation used in the SCL file and some hardware
description languages (VHDL). Perhaps it is not just a coincidence!
The structured format adopted is, in fact, designed to allow great flexibility in describing the stimuli as
well as a fast simulation execution.
Testing the PS/2 receive routines
Before we get to use the stimulus file generated, we have to complete the project with a few final
touches. Let's package the PS/2 receive routines as a module that we might want to call "PS2IC . c".
Remember to include the file in the project (right-click in the editor window, and "Add to Project").
Let's also prepare an include file to publish the accessible function: initKBD ( ) , the flag KBDReady and
the buffer for the received key code KBDCode:
/*
** PS2lC.h
** PS/2 keyboard input library using input capture
*/
extern volatile int KBDReady;
extern volatile unsigned char KBDCode;
void initKBD ( void);
172
Capturing inputs
Note that there is no reason to publish any other detail of the inner workings of the PS2 receiver
implementation. This will give us freedom to later try a few different methods without changing the
interface. Save this file as "PS2IC .h" and include it in the project.
Let's also create a new file "PS2iCTest . c" that will contain the main routine and will use the PS2IC
module to test its functionality:
/*
** PS2 KBD Test
* *
*/
#include <p24f j 128ga010 .h>
#include "PS2lC.h"
main
{
TRISA = OxffOO;
initKBDO ;
// call the initialization routine
// wait for the flag
// fetch the key code and publish on PORTA
// clear the flag
while ( 1)
{
if ( KBDReady)
{
PORTA = KBDCode;
KBDReady = 0;
}
} // main loop
} //main
This will initialize PORTA LSB for output (on the Explorer 16 connected to the LEDs), and will call
the PS/2 keyboard initialization routine that, in its turn, will initialize the chosen input pins, the state
machine, and the interrupts on input capture.
The main loop will wait for the interrupt routine to raise the flag (key code available), will fetch the key
code and publish it on the LEDs, and finally will clear the flag, ready to receive a new character.
Now remember to add the file to the project and "Build All."
173
Chapter 1 1
The simulation
Instead of launching the simulation immediately, proceed to the Debugger menu once more and select
the "Stimulus Controller" submenu.
Select Tool
i -•
Clear Memory
►
Run
F9
Animate
Step Into
F7
Step Over
F8
Step
Reset
►
Breakpoints.,,
F2
Stopwatch
Stimulus Controller ►
Mew Scenario
SCL Generator
►
Open Scenario
Profile
►
Save Scenario
Refresh PM
Close Scenario
Settings,..
Figure 1 1-7. Stimulus Controller submenu.
Select "New Scenario" and you will see a new dialog box appear on the screen. This is the Stimulus Con-
troller and, although it looks deceptively similar to the SCL Generator dialog box, don't let it fool you!
Stimulus Controller - (j\..*\ICPS2.stc
Stimulus (SCL] Fie
c:\wotkVe30\ic\ie ps2 simulation, scl
jch Detach Import/Merge
Asynchronous Stimulus
Fire Pin/SFR I Action
Width Units Comments / Message
-
ie rem
Save Scenario
E:- :i r
Help
Figure 1 1-8. The Stimulus Controller window.
The Stimulus Controller allows you to attach to the project the synchronous stimulus scripts you gener-
ated with the SCL Generator, and add to them "asynchronous stimuli" triggered by the "Fire" buttons
that you see in the Stimulus Controller table.
174
Capturing inputs
Select the "Attach" button and select the . scl file we generated before.
You could now save this "scenario" for later use, but in our case, since we will be dealing with just this
one . scl file and no further asynchronous stimulus will be created, there is really no point.
Note: You must keep the Stimulus Controller window open (in the background). Resist the tempta-
tion to hit the Exit button, as that would close the scenario and leave us without stimuli.
Finally! Hit the Reset button (or select "Debugger— > Reset") and watch for the first stimulus to arrive
as the microsecond trigger is fired. Remember, both lines RG12 and IC1 are supposed to be set high
according to our simulation timetable. A message will be confirming this in the Output window.
Figure 1 1-9. The output window (MPLAB SIM pane) showing that a stimulus action has been triggered.
It is your choice now to proceed by single-stepping or animating through the program to verify its cor-
rect execution. My suggestion is that you start by placing a breakpoint inside the main loop, just past
the instruction copying the KBDCode on to porta. Open the Watch window and add porta from the
SFR list, and then RUN.
After a few seconds, the execution should terminate at the breakpoint and the content of porta should
reflect the data we sent through the simulated PS/2 lines: 0x79!
The Simulator Profile
If you were curious about how fast the simulation of a PIC24 could run on your computer, there is an
interesting feature available to you in the MPLAB SIM Debugger menu, the Profile. Select the Profile
submenu ("Debugger— > Profile") and click on "Reset Profile" first. (See Figure 11-10.)
This will clear the Simulator Profile counters and timers. Then remove all breakpoints and let the simu-
lator run ("Debugger— >Run") for a few seconds. Halt the simulation and go back to the "Debugger— >
Profile" submenu. This time, select "Display Profile". (See Figure 11-11.)
A relatively long report will be available in the output window (MPLAB SIM pane), listing how many
times each instruction was used by the processor during the simulation and at the very bottom offer-
ing an assessment of the absolute simulation speed. In my case, that turned out to be a respectable 2.7
MIPS, meaning the software simulation (on my laptop) ran at about one-sixth the actual processor
speed. Not bad at all!
175
Chapter 1 1
Select Tool
Clear Memory
Run
Animate
Step Into
Step Over
Breakpoints... F2
Stopwatch
Stimulus Controller ►
SCI Generator ►
Settings..,
Figure 11-10. The Simulator Profile submenu.
Build Version Control Find in Files MPLAB SIM MPLAB ICO
£V
^
Simulation Execution time on this computer: 3.735 seconds (1 01 39792 instructions. 271 5 MIPS).
Execution cycles: 1 01 39890
Instruction stalls: 1 7
ZJ
Figure 11-11 . Simulator Profile output.
Another method - Change Notification
While the Input Capture technique worked all right, there are other options that we might be curious to
explore in order to interface efficiently with a PS/2 keyboard. In particular, there is another interesting
peripheral available in the PIC24 that could offer an alternative method to implement a PS/2 interface:
the Change Notification (CN) module. There are as many as 22 I/O pins connected to this module and
this can give us some freedom in choosing the ideal input pins for the PS/2 interface, while making
sure they don't conflict with other functions required in our project or already in use on the Explorer 16
board. There are only four control registers associated with the CN module. The cneni and cnen2 reg-
isters contain the interrupt-enable control bits for each of the CN input pins. Setting any of these bits
enables a CN interrupt for the corresponding pins. Note that only one interrupt vector is available for
the entire CN module, therefore it will be the responsibility of the interrupt service routine to determine
which one of the enabled inputs has actually changed.
File Name
Addr
Bit 15
Bit 14
Bit 13
Bit 12
Bit 11
Bit 10
Bit 9
Bit 8
Bit 7
Bit 6
Bit 5
Bit 4
Bit 3
Bit 2
Bit 1
BitO
All
Resets
CNEN1
0060
CN15IE
CN14IE
CN13IE
CN12IE
CN11IE
CN10IE
CN9IE
CN8IE
CN7IE
CN6IE
CN5IE
CN4IE
CN3IE
CN2IE
CN1IE
CN0IE
0000
CNEN2
0062
CN21IE
CN20IE
CN19IE
CN18IE
CN17IE
CN16IE
0000
CNPU1
0068
CN15PUE
CN14PUE
CN13PUE
CN12PUE
CN11PUE
CN10PUE
CN9PUE
CN8PUE
CN7PUE
CN6PUE
CN5PUE
CN4PUE
CN3PUE
CN2PUE
CN1PUE
CN0PUE
0000
CNPU2
006A
CN21PUE
CN20PUE
CN19PUE
CN18PUE
CN17PUE
CN16PUE
0000
Legend: — = unimplemented, read as '0'. Reset values are shown in hexadecimal.
Table 1 1-3. The CN control register table.
176
Capturing inputs
Each CN pin also has a weak pull-up connected to it. The pull-up acts as a current source that is con-
nected to the pin and eliminates the need for external resistors when push-button or keypad devices are
connected. The pull-ups are enabled separately using the cnpui and cnpu2 registers, which contain the
control bits for each of the CN pins. Setting any of the control bits enables the weak pull-ups for the
corresponding pins.
In practice, all we need to support the PS/2 interface is only one of the CN inputs connected to the PS 2
clock line. The PIC24 weak pull-up will not be necessary in this case, as it is already provided by the
keyboard.
There are 22 pins to choose from, and we will find a CN input that is not shared with the analog-to-
digital converter (remember we need a 5V-tolerant input) and is not overlapping with some other
peripheral used on the Explorer 16 board. This takes a little studying between the device datasheet and
the Explorer 16 user guide. But once the input pin is chosen, say CN1 1 (multiplexed with PORTG pin
9, the SS line of the SPI2 module and the PMP module Address line 2), a new initialization routine can
be written in just a few lines:
#DEFINE PS2CLOCK _RG9
#define PS2DAT RG12
// CN11 input pin
// any available 5V tolerant input
void initKBD( void)
{ // PS/2 keyboard
CNEN1 = 0x0800;
_CNIF = 0;
_CNIE = 1;
} // initKBD
// enable CNll input change notification
// clear the interrupt flag
// enable the interrupt on change notification
As per the interrupt service routine, we can use exactly the same state machine used in the previous
example, adding only a couple of lines of code to make sure that we are looking at a falling edge of the
clock line.
Change Notifications
Clock Line
Data Line
Figure 11-13. PS/2 Interface Bit Timing, Change Notification events detail.
177
Chapter 1 1
In fact, when using the Input Capture module, we could choose to receive an interrupt only on the
desired clock edge, while the Change Notification module will generate an interrupt both on falling
and rising edges. A simple check of the status of the Clock line immediately after entering the interrupt
service routine will help us tell the two edges apart:
void _ISR _CNInterrupt ( void)
{ // change notification interrupt service routine
// make sure it was a falling edge
if ( PS2CLK == 0)
{
// PS/2 receiving state machine
switch ( PS2State) {
default :
case PS2 START:
if ( ! PS2DAT)
{
KCount = 8;
KParity = 0;
PS2State = PS2BIT;
}
break;
// init bit counter
// init parity check
case PS2BIT:
KBDBuf »=1;
if ( PS2DAT)
KBDBuf += 0x8 0;
KParity A = KBDBuf;
if ( --KCount == 0)
PS2State = PS2PARITY;
break;
// shift in data bit
// update parity
// if all bit read, move on
case PS2PARITY:
if ( PS2DAT)
KParity A = 0x80;
if ( KParity & 0x80)
PS2State = PS2STOP;
else
PS2State = PS2START;
break;
// if parity is odd, continue
178
Capturing inputs
case PS2ST0P:
KBDBuf »=1; // shift in data bit
if ( PS2DAT)
KBDBuf += 0x8 0;
KParity A = KBDBuf; // update parity
if ( --KCount ==0) //if all bit read, move on
PS2State = PS2PARITY;
break;
} // switch state machine
} // if falling edge
// clear interrupt flag
_CNIF = 0;
} // CN Interrupt
Add the constants and variables declarations already used in the previous example:
#include <p24f j 128ga010 .h>
# include "PS2CN.h"
#define PS2DAT _RG12 // PS2 Data input pin
#define PS2CLK _RG9 // PS2 Clock input pin
// definition of the keyboard PS/2 state machine
#define PS2 START
#define PS2BIT 1
#define PS2 PARITY 2
#define PS2STOP 3
// PS2 KBD state machine and buffer
int PS2State;
unsigned char KBDBuf;
int KCount, KParity;
// mailbox
volatile int KBDReady;
volatile unsigned char KBDCode;
Package it all together in a file that we will call w PS2CN . c " .
The include file "PS2CN.h" is going to be almost identical to the previous example since we are going
to offer the same interface:
179
Chapter 1 1
/*
* *
** PS2CN.h
* *
** PS/2 keyboard input module using Change Notification
*/
extern volatile int KBDReady;
extern volatile unsigned char KBDCode;
void initKBD( void);
Create a new project called "PS2CN" and add both the . c and the . h files to the project.
Finally, create a main module to test this new technique. One more time, it is going to be almost identi-
cal to the previous project:
/*
** PS2 KBD Test
*/
#include <p24f j 128ga010 .h>
# include "PS2CN.h"
main ( )
{
TRISA = OxffOO;
initKBDO ;
// call the initialization routine
// wait for the flag
// fetch the key code and publish on PORTA
// clear the flag
while ( 1)
{
if ( KBDReady)
{
PORTA = KBDCode;
KBDReady = 0;
}
} // main loop
} //main
Save the project and then build the project ("Project^BuildAll") to compile and link all the modules.
To test the change notification technique, we will once more use MPLAB SIM stimulus-generation
capabilities and we will repeat most of the steps performed in the previous project. Starting with the
SCL Generator ("Debugger— >SCLGenerator"), we will create a new Workbook. Inside the generator
window, create two columns; one will be for the same PS2 Data line connected to RG12, but the other
one this time will be for the PS2 Clock line connected to the CN1 1 Change Notification module input.
180
Capturing inputs
Add the same sequence of times and events to the table as used in the previous example, replacing the
IC1 input column with the CN11 column. Save the Workbook as "PS2CN. sbs" and then hit the "Gen-
erate SCL" file to produce the output stimulus script file: "PS2CN. scl". Finally, activate the Stimulus
Controller ("Debugger— >StimulusController") and create a new Scenario. From the Stimulus Control-
ler window, click on the Attach button and select the "PS2CN. scl" file to activate the input simulation.
Save the Scenario if you want, but don't close the Controller window (you can minimize it though).
We are ready now to execute the code and test (in simulation) the proper functioning of the new PS/2
interface. Open the Watch window and add porta. Then set a breakpoint inside the main loop, right
after the key code is copied into the porta register. Finally, perform a reset ("Debugger— >Reset") and
verify that the first event is triggered (setting both PS/2 input lines high at time us). Execute the code
("Debugger— >RUN") and, if all goes well, you will see the processor stop at the breakpoint after less
than a second and you will see the contents of porta updated to reflect the key code 0x79. Success!
Evaluating cost
Changing from the input capture to the Change Notification method was almost too easy. The two
peripherals are extremely potent and, although designed for different purposes, when applied to the
task at hand they performed almost identically. In the embedded world, though, you should constantly
ask yourself if you could solve the problem with fewer resources, even when apparently, as in this case,
there seems to be abundance. Let's evaluate the real cost of each solution by counting the resources
used and their relative scarcity. When using the input capture, we are in fact using one of five IC mod-
ules available in the PIC24FJ128GA010 model. This peripheral is designed to operate in conjunction
with a timer (Timer2 or Timer3), although we are not using the timing information in our application,
but only the interrupt mechanism associated with the input edge trigger. When using the change notifi-
cation, we are using only one of 22 possible inputs, but we are also taking control of the sole interrupt
vector available to this peripheral. In other words, should we need any other input pin to be controlled
by the Change Notification peripheral, we will have to share the interrupt vector, adding latency and
complexity to the solution. I would call this a tie.
A third method - I/O polling
There is one more method that we could explore to interface to a PS/2 keyboard. It is the most basic
one and it implies the use of a timer, set for a periodic interrupt, and its inputs can be any (5V tolerant)
I/O pin of the microcontroller. In a way, this method is the most flexible from a configuration and lay-
out point of view. It is also the most generic, as any microcontroller model, even the smallest and most
inexpensive, will offer at least one timer module suitable for our purpose. The theory of operation is
pretty simple. At regular intervals, an interrupt will be generated, set by the value of the period register
associated with the chosen timer.
181
Chapter 1 1
Periodic sampling
Clock Line
Data Line
Figure 11-14. PS/2 Interface Bit Timing, I/O Polling sampling points.
We will use Timer4 this time, since we've never used it before. Hence, pr4 will be the period register.
The interrupt service routine (t4 interrupt) will sample the status of the PS/2 Clock line and it will
determine whether a falling edge has occurred on the PS/2 Clock line over the previous period. When a
falling edge is detected, the Data line status will be considered to have received the key code. In order
to determine how frequently we should perform the sampling, and therefore identify the optimal value
of the PR4 register, we should look at the shortest amount of time allowed between two edges on the
PS/2 clock line. This is determined by the maximum bit-rate specified for the PS/2 interface, which,
according to the documentation in our possession, corresponds to about 16 kbit/s. At that rate, the
clock signal can be represented by a square wave with approximately 50% duty cycle, and a period of
approximately 62.5 us. In other words, the clock line will stay low for little more than 30 us each time
a data bit is presented on the Data line, and will stay high for approximately the same amount of time,
during which the next bit will be shifted out. By setting pr4 to a value that will make the interrupt
period shorter than 30 us (say 25 us), we can guarantee that the clock line will be sampled at least once
between two consecutive edges. The keyboard transmission bit rate, though, could be as slow as
10 kbit/s, giving a maximum distance between edges of about 50 us. In that case, we would be sam-
pling the clock and data lines twice and possibly up to three times between each clock edge. In other
words, we will have to build a new state machine to detect the actual occurrence of a falling edge and
to properly keep track of the PS/2 clock signal.
Clock =
Clock = 1
Clock = 1
Clock = 0, Falling Edge
Figure 11-15. Clock-polling state machine graph.
182
Capturing inputs
The state machine requires only two states, and all the transitions can be summarized in Table 1 1-4.
State
Conditions
Effect
StateO
Clock =
Remain in StateO
Clock = 1
Rising Edge, Transition to State 1
State 1
Clock = 1
Remain in State 1
Clock =
Falling edge detected
Execute the Data state machine
Transition to StateO
Table 1 1-4. Clock-polling state machine transitions table.
When a falling edge is detected, we can still use the same state machine developed in the previous
projects to read the data line. It is important to notice that in this case the value of the data line is not
guaranteed to be sampled right after the actual falling edge of the clock line has occurred, but could
be considerably delayed. To avoid the possibility of reading the data line outside the valid period, it is
imperative to sample simultaneously both the clock and the data line. By definition (PS/2 specifica-
tions), if the clock line is low, the data can be considered valid. In practice the requirement translates
into the necessity to assign both the data and clock inputs to pins of the same port. In our example we
will choose to use RG12 (again) for the clock line, and RG15 for the data line. In this way, copying
portg contents into a temporary variable, as soon as we enter the interrupt service routine, will give us
an atomic action and perfect simultaneity in sampling the two lines. Here is the simplest implementa-
tion of the Clock state machine illustrated in Figure 11-15:
#define PS2DAT _RG12
#define PS2CLK _RG15
#define CLKMASK 0x80
#define DATMASK 0x1000
// PS2 Data input pin
// PS2 Clock input pin
// mask to detect the clock line
// mask to detect the data line
unsigned char KBDBuf;
int KState;
// mailbox
volatile int KBDReady;
volatile unsigned char KBDCode;
void _ISR _T4lnterrupt ( void)
{
int PS2IN;
// sample the inputs, clock and data, at the same time
PS 2 IN = PORTG;
183
Chapter 1 1
II Keyboard clock state machine
if ( KState)
{ // previous time clock was high, Statel
if ( ! (PS2IN Sc CLKMASK) ) // PS2CLK =
{// falling edge detected
KState =0; // transition to StateO
<<<. . . Insert Data state machine here!>>>
} // falling edge
else
{ // clock still high, remain in Statel
} // clock still high
} // State 1
else
{ // State
if ( PS2IN Sc CLKMASK) // PS2CLK = 1
{ // rising edge detected
KState = 1; // transition to Statel
} // rising edge
else
{ // clocl still low, remain in StateO
} // clock still low
} // State
// clear the interrupt flag
_T4IF = 0;
} // T4 Interrupt
Thanks to the periodic nature of the polling mechanism we just developed, we can add a new feature
to the PS2 interface to make it more robust with minimal effort. First of all we can add a counter of the
idle loops of both states of the clock state machine. This way we will be able to create a timeout, so
as to be able to detect and correct error conditions should the PS/2 keyboard be disconnected during a
transmission or should the receive routine lose synchronization for any reason.
The new transition table is quickly updated to include the timeout counter Ktimer.
184
Capturing inputs
State
Conditions
Effect
Remain in StateO
StateO
Clock =
Decrement Ktimer
If Ktimer = 0, error
Reset the data state machine
Clock = 1
Rising Edge, Transition to State 1
Remain in State 1
Clock = 1
Decrement Ktimer
If Ktimer = 0, error
State 1
Reset the data state machine
Falling edge detected
Clock =
Execute the Data state machine
Transition to StateO
Restart Ktimer
Table 1 1-5. Clock-polling (with timeout) state machine transition table.
The new transition table adds only a few instructions to our interrupt service routine.
void _ISR _T4lnterrupt ( void)
{
int PS2IN;
// sample the inputs, clock and data, at the same time
PS2IN = PORTG;
// Keyboard clock state machine
if ( KState)
{ // previous time clock was high, Statel
if ( ! (PS2IN & CLKMASK) ) // PS2CLK =
{// falling edge detected
KState =0; // transition to StateO
KTimer = KMAX; // restart the counter
<<<... Insert Data state machine here!>>>
} // falling edge
else
{ // clock still high, remain in Statel
KTimer-- ;
if ( KTimer ==0) // timeout!
PS2State = PS2START; // reset the data state machine
185
Chapter 1 1
} // clock still high
} // State 1
else
{ // State
if ( PS2IN & CLKMASK)
{ // rising edge detected
KState = 1;
// PS2CLK = 1
// transition to Statel
} // rising edge
else
{ // clocl still low, remain in StateO
KTimer--;
if ( KTimer ==0) // timeout!
PS2State = PS2START; // reset the data state machine
} // clock still low
} // State
// clear the interrupt flag
_T4IF = 0;
} // T4 Interrupt
Testing the I/O polling method
Let's now insert the Data state machine from the previous projects, modified to operate on the value
sampled in PS2IN at the interrupt service routine entry:
switch ( PS2State) {
default :
case PS2START:
if ( ! (PS2IN & DATMASK) )
{
KCount = 8 ;
KParity = 0;
PS2State = PS2BIT;
}
break;
// init bit counter
// init parity check
case PS2BIT:
KBDBuf »=1;
if ( PS2IN & DATMASK)
KBDBuf += 0x80;
KParity A = KBDBuf;
if ( --KCount == 0)
PS2State = PS2PARITY;
break;
// shift in data bit
//PS2DAT
// calculate parity
// if all bit read, move on
186
Capturing inputs
case PS2PARITY:
if ( PS2IN 8c DATMASK)
KParity A = 0x80;
if ( KParity & 0x80)
PS2State = PS2STOP;
else
PS2State = PS2START;
break;
// if parity is odd, continue
case PS2STOP:
if ( PS2IN & DATMASK)
{
KBDCode = KBDBuf;
KBDReady = 1;
}
PS2State = PS2START;
break;
// verify stop bit
// write in the buffer
// set flag
} // switch
Let's complete this third module with a proper initialization routine.
void initKBD( void)
{
// init I/Os
_TRISG15 = 1; // make RG15 an input pin, PS/2 Clock
_TRISG12 = 1; // make RG12 an input pin, PS/2 Data
// clear the flag
KBDReady = 0;
PR4 = 25 * 16; // 25 us, set the period register
T4CON = 0x8000; // T4 on, prescaler 1:1
_T4IF = 0; // clear interrupt flag
_T4IE = 1; // enable interrupt
} // init KBD
This is straightforward.
Let's save it all in a module we can call "PS2T4 . c". Let's create a new include file too:
/*
** PS2T4.h
* *
** PS/2 keyboard input library using T4 polling
*/
187
Chapter 1 1
extern volatile int KBDReady;
extern volatile unsigned char KBDCode;
void initKBD( void);
It is practically identical to all previous modules' include files, and the main module will not be much
different either:
/*
** PS2 KBD Test
* *
* *
*/
#include <p24f j 128ga010 .h>
#include "PS2T4.h"
main
{
TRISA = OxffOO;
initKBDO ;
// call the initialization routine
while ( 1)
{
if ( KBDReady) // wait for the flag
{
PORTA = KBDCode; // fetch the key code and publish on PORTA
KBDReady =0; // clear the flag
}
} // main loop
} //main
Create a new project "PS2T4" and add all three files to it. Build all and follow the same series of steps
used in the previous two examples to generate a stimulus script file "PS2T4 . scl". Remember that,
this time, the stimulus for the clock line must be provided on the RG15 pin. Open a new scenario with
the stimulus controller and attach the stimulus script file to start the simulation (remember to leave the
Stimulus Controller window open in the background). Open the Watch window and add porta. Finally,
set a breakpoint to the line after the assignment to porta and execute. If all goes well, even this time
you should be able to see porta updated in the Watch window and showing a new value of 0x79.
Success again!
Cost and efficiency of the solution
Comparing the cost of this solution to the previous two, we realize that the I/O polling approach is the
one that gives us the most freedom in choosing the input pins and uses only one resource, a timer, and
one interrupt vector. The periodic interrupt can also be seamlessly shared with other tasks to form a
188
Capturing inputs
common time base if they all can be reduced to multiples of the polling period. The timeout feature is
an extra bonus; in order to implement it in the previous techniques, we would have had to use a sepa-
rate timer and another interrupt service routine in addition to the Input Capture or Change Notification
modules and interrupts. Looking at the code efficiency, the Input Capture and the Change Notifica-
tion methods appear to have an advantage, as an interrupt is generated only when an edge is detected.
Actually, as we have seen, the input capture is the best method from this point of view, as we can select
precisely the one type of edge we are interested in — that is, the falling edge of the clock line. The
I/O polling method appears to require the longest interrupt routine, but the number of lines does not
reflect the actual weight of the interrupt service routine. In fact, of the two nested state machines that
comprise the I/O polling interrupt service routine, only a few instructions are executed at every call,
resulting in a very short execution time and minimal overhead.
To verify the actual software overhead imposed by the interrupt service routines, we can perform one
simple test on each one of the three implementations of the PS/2 interface. I will use the last one as an
example. We can allocate one of the I/O pins (one of the LED outputs would be a logical choice) to
help us visualize when the microcontroller is inside an interrupt service routine. We can set the pin on
entry and reset it right before exit:
void _ISR _T4lnterrupt ( void)
{
_RA0 = 1; // flag up, inside the ISR
<<< Interrupt service routine here >>
_RA0 = 0; // flag down, back to the main
}
Using the MPLAB SIM simulator Logic Analyzer view, we can visualize it on our computer screen.
Follow the Logic Analyzer checklist so you will remember to enable the Trace buffer, and set the cor-
rect simulation speed. Select the RAO channel and rebuild the project. To test the first two methods you
will need to activate the Stimulus Controller again to simulate the inputs; without them, there are going
to be no interrupts at all.
To test the polling routine, you don't need stimuli. The timer interrupt keeps coming anyway and we
are particularly interested in seeing how much time is wasted by the continuous polling when no key-
board input is provided.
Let MPLAB SIM execute for a few seconds; then stop the simulation and switch back to the Logic
Analyzer window. You will have to zoom in quite a bit to get an accurate picture.
189
Chapter 1 1
Trigger Position-
Start^ Centeir EnoT
Trigger PC
NOW Hear
Time Bas e Mode
Cyc ^| I Simple
Channels
FFj&l §J®J fnf^ %Ih1#|[&
RAO
~~ l r r
23834000
i 1 1
23834100.0
i 1 r
23834200.0
2333430M
t ! 1 r
23834400.0
Figure 11-16. Logic Analyzer view, measuring the I/O polling period.
i*
Activate the cursors I p|1 and drag them to measure the number of cycles between two consecutive ris-
ing edges of RAO, marking the entry in the interrupt service routine. Since we selected a 25-jas period,
you should read 400 cycles between calls (25 us * 16 cycles/us @32 MHz). Measuring the number
of cycles between a rising edge and a falling edge of RAO will tell us, with good approximation, how
much time we are spending inside the interrupt service routine; 16 cycles is what I found. The ratio
between the two quantities will give us an indication of the computing power absorbed by the PS/2
interface. In our case, that turns out to be just 2.5%.
Completing the interface: adding a FIFO buffer
Independently from the solution you will choose out of the three we explored so far, there are a few
more details we need to take care of before we can claim to have completed the interface to the PS/2
keyboard. First of all, we need to add a FIFO buffering mechanism between the PS/2 interface routines
and the "consumer" or the main application. So far, in fact, we have provided only a simple mailbox
mechanism that can store only the last key code received. If you investigate further how the PS/2 key-
board protocol works, you will discover that when a single key is pressed and released, a minimum of
three (and a maximum of five) key codes are sent to the host. If you consider shift, control and Alt-key
combinations, things get a little more complicated and you realize immediately that the single-byte
mailbox is not going to be sufficient. My suggestion, in fact, is to add at least a 16-byte FIFO buffer.
The input to the buffer can be easily integrated with the receiver interrupt service routines so that, when
a new key code is received, it is immediately inserted in the FIFO. The buffer can be declared as an ar-
ray of characters and two pointers will keep track of the head and tail of the buffer in a circular scheme.
190
Capturing inputs
<
KCB[16]
[0]
[1]
[15]
nz >
di
i
i
D
D
empty
filled
KBR KBW
Figure 11-17. Circular buffer FIFO.
I / circular buffer
unsigned char KCB [ KB_SIZE] ;
// head and tail or write and read pointers
volatile int KBR, KBW;
By following a few simple rules we can keep track of the buffer content:
the write pointer kbw (or head), marks the first empty location that will receive the next key
code.
the read pointer kbr (or tail), marks the first filled location.
when the buffer is empty, kbr and kbw are pointing at the same location.
when the buffer is full, kbw points to the location before kbr.
after reading or writing a character to/from the buffer, the corresponding pointer is incremented.
upon reaching the end of the array, each pointer will wrap around to the first element of the
array
Insert the following snippet of code into the initialization routine:
// init the circular buffer pointers
KBR = 0;
KBW = 0;
Then update the interrupt routine state machine STOP state:
case PS2STOP:
if ( PS2IN & DATMASK) // verify stop bit
{
KCB[ KBW] = KBDBuf; // write in the buffer
if ( (KBW+1) %KB_SIZE != KBR) // check if buffer full
KBW++; // else increment buffer
KBW %= KB_SIZE; // wrap around
}
PS2State = PS2START;
break;
Notice the use of the "%" operator to give us the remainder of the division by the buffer size. This al-
lows us to keep the pointers wrapping around the circular buffer.
191
Chapter 1 1
A few considerations are required for fetching key codes from the FIFO buffer. In particular, if we
choose the Input Capture or the Change Notification methods, we will need to make a new function
available ( getKeyCode ( ) ) to replace the mailbox/flag mechanism. The function will return FALSE if
there are no key codes available in the buffer and TRUE if there is at least one key code in the buffer,
and the code is returned via a pointer:
int getKeyCode ( char *c)
{
if ( KBR == KBW) // buffer empty
return FALSE;
// buffer contains at least one key code
*c = KCB [ KBR+ + ] ; // extract the first key code
KBR %= KB_SIZE; // wrap around the pointer
return TRUE;
} // getKeyCode
Notice that the extraction routine modifies only the read pointer; therefore, it is safe to perform this
operation when the interrupts are enabled. Should an interrupt occur during the extraction, there are
two possible scenarios:
the buffer was empty: a new key code will be added, but the getKeyCode routine will "notice"
the available character only at the next call.
the buffer was not empty: the interrupt routine will add a new character to the buffer tail, if
there is enough room.
In both cases, there are no particular concerns of conflicts or dangerous consequences.
If we choose the polling technique, there is one more option we might want to explore. In fact, since
the timer interrupt is constantly active, we can use it to perform one more task for us. The idea is
to maintain the simple mailbox-and-flag mechanism for delivering key codes as the interface to the
receive routine, and have the interrupt constantly checking the mailbox, ready to replenish it with the
content from the FIFO. This way we can confine the entire FIFO management to the interrupt service
routine, making the buffering completely transparent and maintaining the simplicity of the mailbox de-
livery interface. The new and complete interrupt service routine for the polling I/O mechanism follows:
void _ISR _T4lnterrupt ( void)
{
int PS2IN;
// check if buffer available
if ( IKBDReady && ( KBR!=KBW))
{
KBDCode = KCB [ KBR++] ;
KBR %= KB_SIZE;
KBDReady = 1; // signal character available
}
// sample the inputs clock and data at the same time
PS 2 IN = PORTG;
192
Capturing inputs
II Keyboard state machine
if ( KState)
{ // previous time clock was high KState 1
if ( ! (PS2IN & CLKMASK) ) // PS2CLK =
{ // falling edge detected,
KState =0; // transition to StateO
KTimer = KMAX; // restart the counter
switch ( PS2State) {
default :
case PS2START:
if
{
! (PS2IN & DATMASK)
KCount = 8;
KParity = 0;
PS2State = PS2BIT;
}
// init bit counter
// init parity check
break;
case PS2BIT:
KBDBuf »=1;
if ( PS2IN & DATMASK)
KBDBuf += 0x80;
KParity A = KBDBuf;
if ( --KCount == 0)
PS2State = PS2PARITY;
break;
// shift in data bit
//PS2DAT
// calculate parity
// if all bit read, move on
case PS2PARITY:
if ( PS2IN & DATMASK)
KParity A = 0x80;
if ( KParity & 0x80)
PS2State = PS2STOP;
else
PS2State = PS2START;
break;
// if parity is odd, continue
case PS2STOP:
if ( PS2IN & DATMASK)
{
KCB[ KBW] = KBDBuf;
if ( (KBW+1) %KB_SIZE
KBW++;
KBW %= KB_SIZE;
}
PS2State = PS2START;
break;
// verify stop bit
// write in the buffer
KBR) // check if buffer full
// else increment buffer
/ / wrap around
193
Chapter 1 1
} // switch
} // falling edge
else
{ // clock still high, remain in Statel
KTimer--;
if ( KTimer ==0)
PS2State = PS2START;
} // clock still high
} // Kstate 1
else
{ // Kstate
if ( PS2IN 8c CLKMASK) // PS2CLK = 1
{ // rising edge, transition to Statel
KState = 1;
} // rising edge
else
{ // clocl still low, remain in StateO
KTimer--;
if ( KTimer == 0)
PS2State = PS2START;
} // clock still low
} // Kstate
// clear the interrupt flag
_T4IF = 0;
} // T4 Interrupt
Completing the interface: performing key codes decoding
So far we have been talking exclusively about key codes and you might have assumed that they match
the ASCII codes for each key. For example, if you press the "A" key on the keyboard, you would
expect the corresponding ASCII code (0x41) to be sent. Unfortunately, this is not the case. For his-
torical reasons, even the newest USB keyboards are still bound to use "scan codes" where each key
is assigned a numerical value that is related to the original implementation of the keyboard-scanning
firmware (which used an 8048 microcontroller) for the first IBM PC keyboard circa 1980. The fact that
the translation from key codes to a specific character set happens at a higher level (performed by Win-
dows keyboard drivers) is actually a good thing, since it provides a generic mechanism to support many
different international keyboard layouts. Keep in mind also that, for historical reasons, there are at least
three different and partially compatible "scan code sets." Fortunately, by default, all keyboards support
scan code set #2, which is the one we will focus on in the following section.
Each time a key is pressed (any key, including a shift or control key), the scan code associated to it is sent
to the host. This is called the "make code." But also, as soon as the same key is released, a new sequence
of scan codes is sent to the host. This is called the "break code." The break code is typically composed of
the same scan code but prefixed with the code "OxFO". Some keys can have a two-byte-long make code
(typically the Ctrl, Alt and arrows) and consequently the break code is three bytes long.
194
Capturing inputs
Key
Make Code
Break Code
"A"
1C
F0,1C
"5"
2E
F0,2E
"F10"
09
F0,09
Right Arrow
E0,74
E0, F0, 74
Right "Ctrl"
E0, 14
E0, F0, 14
Table 1 1-6. Example of make and break codes used in Scan Code Set 2 (default).
In order to process this information and translate the scan codes intro proper ASCII, we will need a
table that will help us map the basic scan codes for a basic U.S. English keyboard layout.
// PS2 keyboard codes (standard set #2)
const char keyCodes [12 8] ={
o,
F9
o,
F5,
F3, Fl,
F2,
F12,
//oo
o,
F10
F8,
F6,
F4, TAB,
"■
1
0,
//08
o,
L_SHFT
, 0,L_
_CTRL, 'q' ,
1
1
o,
//10
0,
x z' ,
v s' ,
a
, % W ,
2
1
o,
//18
o,
x c'
x x' ,
v d' ,
e
, x 4',
3
1
o,
//20
o,
\ \
% V ,
v f ,
t
, x r' ,
5
1
o,
//28
o,
v n'
l b'.
*h' ,
g
, % y' ,
6
1
o,
//30
o,
% m' ,
% j' ,
u
8
1
o,
//38
o,
\ /
i
x k',
x i' ,
o
, '0' ,
9
1
o,
//40
o,
\ i
V,
*1' ,
/
i X P',
-
1
o,
//48
o,
'V ',
0,
[
0,
o,
//50
CAPS , R_
SHFT
ENTER,
% ] ',
(
), 0x5c,
0,
o,
//58
o,
0,
o,
(
), 0, I
3KSP
o,
//60
o,
*1'
0,
x 4 '
7
o,
0,
o,
//68
o,
\ /
'2' ,
v 5' ,
6
, x 8' , I
]SC,
NUM,
//70
Fll,
' + '
'3' ,
\ _ i
*
(
),
//78
};
Notice that the array has been declared as const so that it will be allocated in program memory space
to save more precious RAM space.
It will also be convenient to have a similar table available for the shift function of each key.
const char keySCodes [12 8] = {
o,
F9
o,
F5
F3,
Fl,
o,
F10
F8,
F6
F4,
TAB,
o,
L_SHFT
, o
L_CTRL, 'Q'
o,
X Z' ,
V S'
X A' ,
% W ,
o,
% c
X X' ,
X D'
X E' ,
*$',
o,
\ \
X V ,
X F'
'T' ,
X R' ,
o,
V N'
X B' ,
X H'
X G' ,
X Y' ,
o,
X M' ,
x J'
X U' ,
x &' ,
o,
v < '
X K' ,
*I'
x O' ,
M ',
o,
v > '
\ 9 ,
i
X L'
\ . i
X P' ,
o,
' \ " ' ,
% {\
x + ' ,
F2
F12,
//oo
0,
//08
0,
//10
o,
//18
o,
//20
o,
//28
o,
//30
o,
//38
o,
//40
o,
//48
o,
//50
195
Chapter 1 1
, ENTER,
'}
, o
\ i
0, 0,
//58
0,
BKSP, 0,
//60
0,
! 4'
s l'
0, 0,
//68
, x 2',
v 5'
*6'
*8'
ESC, NUM,
//70
, x 3',
\ _ i
\ * /
,9/
0,
//IS
CAPS , R_SHFT
0,
0, '1'
0, '.'
Fll, » + '
};
For all the ASCII characters, the translation is straightforward, but we will have to assign special values to
the function, shift and control keys. Only a few of them will find a corresponding code in the ASCII set:
// special function characters
# define TAB 0x9
#define BKSP 0x8
#define ENTER Oxd
#define ESC 0x1b
For all the others, we will have to create our own conventions, or, until we have a use for them, we
might just ignore them and assign them a common code (0):
# define
L_SHFT
0x12
# define
R_SHFT
0x12
# define
CAPS
0x58
# define
L_CTRL
0x0
# define
NUM
0x0
# define
Fl
0x0
# define
F2
0x0
# define
F3
0x0
# define
F4
0x0
# define
F5
0x0
# define
F6
0x0
# define
F7
0x0
# define
F8
0x0
# define
F9
0x0
# define
F10
0x0
# define
Fll
0x0
# define
F12
0x0
The following routine getc ( ) performs the basic translations for the most common keys and it takes
care of the shift status as well as the CAPS key toggling:
int CapsFlag=0;
char getC ( void)
{
unsigned char c ;
while ( 1)
{
while ( IKBDReady) ; // wait for a key to be pressed
// check if it is a break code
196
Capturing inputs
while (KBDCode == OxfO)
{ // consume the break code
KBDReady = 0;
// wait for a new key code
while ( ! KBDReady) ;
// check if the shift button is released
if ( KBDCode == L_SHFT)
CapsFlag = 0;
// and discard it
KBDReady = 0;
// wait for the next key
while ( ! KBDReady) ;
}
// check for special keys
if ( KBDCode == L_SHFT)
{
CapsFlag = 1;
KBDReady = 0;
}
else if ( KBDCode == CAPS)
{
CapsFlag = ! CapsFlag;
KBDReady = 0;
}
else // translate into an ASCII code
{
if ( CapsFlag)
c = keySCodes[KBDCode%128] ;
else
c = keyCodes [KBDCode%128] ;
break;
}
}
// consume the current character
KBDReady = 0;
return ( c) ;
} // getC
Post-flight briefing
In this lesson we have learned how to interface to a PS/2 computer keyboard, exploring three alterna-
tive methods. This gave us the perfect opportunity to exercise two new peripheral modules: the Input
Capture and the Change Notification modules. We also discussed methods to implement a FIFO buffer
and polished our interrupt management skills. Throughout the entire lesson, our focus has been con-
stantly on balancing the use of resources and the performance offered by each solution.
197
Chapter 1 1
Tips and tricks
Stalling transmissions from the keyboard - Open-Drain Output Control
Each PS/2 keyboard has an internal FIFO buffer 16 key codes deep. This allows the keyboard to ac-
cumulate the user input even when the host is not ready to receive. The host, as we mentioned at the
very beginning of this chapter, has the option to stall the communication by pulling the clock line low
at any given point in time (for at least 100 us) and can hold it low for the desired period of time. When
the clock line is released, the keyboard will resume transmissions. It will retransmit the last key code, if
it had been interrupted, and will offload its FIFO buffer.
To exercise our right to stall the keyboard transmissions as a host, we have to control the clock line
with an output using an open drain driver. Fortunately, this is easy with the PIC24, thanks to its con-
figurable I/O port modules. In fact, each I/O port (PORTx) has an associated control register (odcx)
that can individually configure each pin output driver to operate in open-drain mode.
Note: This feature is extremely useful in general to interface PIC24 outputs to any 5V device.
In our example, turning the PS/2 clock line into an open-drain output would require only a few lines of code:
_ODG13 = 1; // configure the PORTG pin 13 output driver in open-drain
_LATG13 = 1; // initially let the output in pull up
_TRISG13 =0; // enable the output driver
Note that, as usual for all PIC® microcontrollers, even if a pin is configured as an output, its current
status can still be read as an input. So there is no reason to switch continuously between input and
output when we alternate stalling and receiving characters from the keyboard.
198
Capturing inputs
Exercises
1 . Add a function to send commands to the keyboard to control the status LEDs and set the key
repeat rate.
2. Replace the "stdio . h" library input function read ( ) to redirect the keyboard input from the
stdin stream.
3. Add support for a PS/2 mouse interface.
Books
• Anderson F. (2003)
Flying the Mountains
McGraw-Hill, New York, NY
Flying the mountains requires extra caution and preparation. This could be the next challenge
after you have completed your private pilot license.
Links
http://www. computer-engineering, org/
This is an excellent web site where you will find a lot of useful documentation on the PS/2
keyboard and mouse interface.
199
CHAPTER
12
The Dark Screen
In This Chapter
►
Generating the composite
►
Plotting
video signal
►
A starry night
►
Using the Output Compare
►
Line drawing
modules
►
Bresenham algorithm
►
Memory allocation
►
Plotting math functions
►
Image serialization
►
Two-dimensional function visualization
►
Building the video module
►
Fractals
►
Testing the video generator
►
Text
►
Measuring performance
►
Testing the TextOnGPage module
►
The dark screen
►
Developing a Text Page Video
►
A Test Pattern
►
Testing the text page performance
I have always liked driving the car at night. Generally there is less traffic, the air is always cooler and,
unless I am really tired, the lights of the vehicles in the other direction never really bother me much.
But when my instructor proposed a first cross-country flight at night, I got a little worried. The idea of
staring at a windshield filled with pitch black void... was a little frightening, I have to admit. However,
the actual experience a week later converted me forever. Sure, night flying is a bit more serious stuff
than the usual around-the-pattern practice. There is more careful planning involved, but it is just so
rewarding. Flying over an uninhabited area fills the screen with so many stars that a city boy like me
has hardly ever seen — it feels like flying a starship to another solar system. Flying over or near a large
city transforms the grey and uniform spread of concrete of alternating parking lots and housing devel-
opments into a wonderful show of lights — it's like Christmas as far as the eye can see. Turns out, the
screen is never really dark. It's a big show and it is on every night.
Flight plan
In this lesson we will consider techniques to interface to a TV screen or, for that matter, to any dis-
play that can accept a standard composite video signal. It will be a good excuse to use new features of
several peripheral modules of the PIC24 and review new programming techniques. Our first project
objective will be to get a nice dark screen (a well-synchronized video frame), but we will soon fill it up
with several entertaining graphical applications.
201
Chapter 12
The flight
There are many different formats and standards today in use in the world of video, but perhaps the
oldest and most common one is the so-called "composite" video format. This is what was originally
used by the very first TV sets to appear in the consumer market, and today it represents the minimum
common denominator of every video display, whether a modern high-definition flat-screen TV of the
latest generation, a DVD player, or a VHS tape recorder. All video devices are based on the same basic
concept: the image is "painted" one line at a time, starting from the top left corner of the screen and
moving horizontally to the right edge, then quickly jumping back to the left edge at a lower position
and painting a second line, and so on and on, in a zig-zag motion, until the entire screen has been
scanned. Then the process repeats and the entire image is refreshed fast enough for our eyes to be
tricked into believing that the entire image is present at the same time, and if there is motion, it is fluid
and continuous.
Frame
Line N
Figure 12-1. Video image scanning.
In different parts of the world, slightly incompatible systems have been developed over the years, but
the basic mechanism remains the same. What changes eventually is the number of lines composing the
image, the refreshing frequency, and the way the color information is encoded.
US
Europe, Asia
France and others
Standard
NTSC
PAL
SECAM
Frames per second
29.97*
25
25
Number of lines
525
625
625
* NTSC used to be 30 frames per second, but the introduction of the new color standard changed it to 29.97, to accommodate
for a specific frequency used by the "color subcarrier" crystal oscillator.
Table 12-1. International video standard examples.
Table 12-1 illustrates three of the most commonly used video standards adopted in the US, Europe and
Asia. All those standards encode the "luminance" information (that is, the underlying black-and-white
image) together with synchronization information in a similarly defined composite signal.
The name "composite" is used to describe the fact that three different pieces of information are
combined into one video signal: the actual luminance signal and both horizontal and vertical synchro-
nization information.
202
The Dark Screen
WHITE 100 -
P
LU
CC
BLACK- 10
7.5 -"
°7
BLACKER /x
THAN BLACK
VIDEO
t
G3.5.US
L BLANKING J
10.9JJS
CO
3 f4.7X)S^ h-2.5+|
FRONT
PORCH
-40
T
-40
_L
SYNC TIP
Figure 12-2. NTSC composite signal, horizontal line detail.
The horizontal line signal is in fact composed of:
1 . the horizontal synchronization pulse, used by the display to identify the beginning of each
line.
2. the so-called back porch, that creates a dark frame around the image.
3. the actual line luminosity signal; the higher the voltage, the more luminous the point.
4. the so-called front porch, producing the right edge of the image.
The color information is transmitted separately, modulated on a high frequency subcarrier. The three
main standards differ significantly in the way they encode the color information but, for our purposes,
it will be easy to ignore the problem altogether to obtain a simple black-and-white display output.
All these standard systems utilize a technique called "interlacing" to provide a (relatively) high-
resolution output while requiring a reduced bandwidth. In practice, only half the number of lines is
transmitted and painted on the screen in each frame. Alternate frames present only the odd or the
even lines composing the picture so that the entire image content is effectively updated only at half
the refresh rate (25 Hz and 30 Hz, respectively for PAL and NTSC). This is effective for typical TV
broadcasting but can produce an annoying flicker when text and especially horizontal lines are dis-
played, as is often the case in computer monitor applications. For this reason all modern computer
displays do not use "interlaced" but progressive scanning. Most modern TV sets, and especially those
using LCD and plasma technologies, perform a deinterlacing of the received broadcast image. In our
project we will avoid "interlacing" as well, sacrificing half of the image resolution in favor of a more
stable and readable display output. In other words, we will transmit frames of 262 lines (for NTSC)
at the double rate of 60 frames per second. Readers that have easier access to PAL or SECAM TV
sets/monitors will find it relatively easy to modify the project for 312-line resolution with a refresh rate
of 50 frames per second.
203
Chapter 12
A complete video frame signal is represented in Figure 12-3
261 262
nnnnnnjujuujnnni
21 22
B 10 18:19; 20; -^ . ftj ^. 1
riririHHj^nA
< Pre equalizing 4 Vertical synch ^ Post-equalizing ^ Image first line
pulses pulse pulses
Frame start
Figure 12-3. A complete video frame signal.
Notice how, out of the total number of lines composing each frame, three line periods are filled by
prolonged synchronization pulses to provide the vertical synchronization information, identifying the
beginning of each new frame. They are preceded and followed by groups of three additional lines,
referred to as the pre- and post-equalization lines.
Generating the composite video signal
If we limit the scope of the project to generating a simple black-and-white image (no gray shades, no
color) and a noninterlaced image as well, we can simplify the hardware and software requirements of
our project considerably. In particular, the hardware interface can be reduced to just three resistors of
appropriate value connected to two digital I/O pins. One of the I/O pins will generate the synchroniza-
tion pulses and the other I/O pin will produce the actual luminance signal.
R2 L20
Video
Sync
RCA Video Conn.
~ GMD
Figure 12-4. Simple hardware interface for NTSC video output.
204
The Dark Screen
The values of the three resistors must be selected so that the relative amplitudes of the luminance and
synchronization signals are close to the standard NTSC specifications, the signal total amplitude is
close to IV peak to peak, and the output impedance of the circuit is approximately 75 ohms. With the
standard resistor values shown in the previous picture, we can satisfy such requirements and generate
the three basic signal levels required to produce a black-and-white image:
Signal Feature
Sync
Video
Sync pulse
Black level
1
White level
1
1
Table 12-2. Generating Luminance and Synchronization pulses.
Frame
White
Black
Sync
Line x
-63.5 ps
1V
Figure 12-5. Simplified NTSC composite signal.
Since we are not going to utilize the interlacing feature, we can also simplify the pre-equalization,
vertical synchronization and post-equalization pulses by producing a single horizontal synchronization
pulse per each period, as illustrated in Figure 12-6.
261 262
inr-u
10
■ Pre e<|u.ilizifHj
pulses
Frame stall
LJ-JiriririMf'
Vertical synch 4
pulse
Posi equalizing ^
pulses
Image first line
Figure 12-6. Simplified NTSC video frame (noninterlaced).
The problem of generating a complete video output signal can now be reduced to (once more) a simple
state machine that can be driven by a fixed period time base produced by a single timer interrupt. The
state machine will be quite trivial, as each state will be associated to one type of line composing the
frame, and it will repeat for a fixed number of times before transitioning to the next state.
205
Chapter 12
repeat
PREEQ_N
times
repeat
VRES
times
repeat
VSYNC_N
times
repeat
POSTEQJM
times
Figure 12-7. Video state machine graph.
A simple table will help describe the transitions from each state:
State
Repeat
Transition to
Pre-equal
PREEQ_N times
Vertical Sync
Vertical Sync
3 times
Post-equal
Post-equal
POSTEQ_N times
Image line
Image line
VRES times
Pre-equal
Table 12-3. Video state machine transitions table.
While the number of vertical synchronization lines is fixed and prescribed by the NTSC video stan-
dard, the number of lines effectively comprising the image inside each frame is up to us to define
(within limits, of course). Although in theory we could use all of the lines available to display the
largest possible amount of data on the screen, we will have to consider some practical limitations, in
particular the RAM memory available to store the video image inside the PIC24FJ128GA010 micro-
controller. These limitations will dictate a specific number (vres) of lines to be used for the image
while all the remaining (up to the NTSC standard line count) will be left blank.
In practice, if v_ntsc is the total number of lines composing a standard NTSC video frame and vres is
the desired vertical resolution, we will determine a value for preeq_n and posteq_n as follows:
#define V_NTSC 2 62
# define VSYNC N 3
// total number of lines composing a frame
// V sync lines
// count the number of remaining black lines top+bottom
#define VBLANK_N (V_NTSC -VRES - VSYNC_N)
#define PREEQ_N VBLANK_N /2
# define POSTEQ_N VBLANK_N - PREEQ_N
// pre equalization + bottom blank lines
// post equalization + top blank lines
If we choose Timer3 to generate the time base, we can initialize its period register PR3 to produce an
interrupt with the prescribed period and create an interrupt service routine where we will place the state
machine. Here is a skeleton of the interrupt service routine on which we will grow the complete video
generator logic.
206
The Dark Screen
II next state table
int VS[4] = { SV_SYNC, SV_POSTEQ, SV_LINE, SV_PREEQ} ;
// next counter table
int VC[4] = { VSYNC_N, POSTEQ_N, VRES, PREEQ_N} ;
void _ISRFAST _T3 Interrupt ( void)
{
// Start a Sync pulse
SYNC = 0;
// decrement the vertical counter
VCount--;
// vertical state machine
switch ( VState) {
case SV_PREEQ:
// horizontal sync pulse
break;
case SV_SYNC:
// vertical sync pulse
break;
case SV_POSTEQ:
// horizontal sync pulse
break;
default:
case SV_LINE:
} //switch
// advance the state machine
if ( VCount == 0)
{
VCount = VC[ VState];
VState = VS[ VState];
}
// clear the interrupt flag
_T3IF = 0;
} // T3 Interrupt
207
Chapter 12
Once inside the interrupt service routine, we can immediately lower the Sync output pin to start gener-
ating the horizontal sync pulse, but we need a different mechanism to provide us with the right timing
(approx 4.5 us) to complete the pulse (rising edge) and produce the rest of the horizontal line wave-
form. There are of course several options we can explore:
1 . use a short delay loop using a counter.
2. use a second timer, and associated interrupt service routine.
3. use the output compare modules and the associated interrupt service routines.
The first solution is probably the simplest to code, but has the clear disadvantage of wasting a large
number of processor cycles (4.5 us x 16 cycles per microsecond = 72 cycles), which repeated each
horizontal line period (63.5 us or about 1018 cycles) would add up to as much as 7% of the total pro-
cessing power available.
The second solution is clearly more efficient, and by now we have ample experience in using timer
interrupts and their interrupt service routines to execute small state machines.
The third solution involves the use of a new peripheral we have not yet explored in the previous chap-
ters and which deserves a little more attention.
Using the Output Compare modules
The PIC24FJ128GA010 microcontroller has five Output Compare peripheral modules that can be
used for a variety of applications including: single pulse generation, continuous pulse generation, and
pulse width modulation (PWM). Each module can be associated to one of two 16-bit timers (Timer2 or
Timer3) and has one output pin that can be configured to toggle and produce rising or falling edges if
necessary. Most importantly each module has an associated and independent interrupt vector.
Set Flag bit
OCxlF< 1 )
OCxRS< 1
57
OCxR (1 -
Comparator
OCTSEL
Output - S Q
Logic _ R
*
OCM2:OCM0
Mode Select
m^HEi ^
Output Enable
2\ OCFA or OCFB (2)
16>1 4 16
l\
TMR register inputs
from time bases
(see Note 3).
Period match signals
from time bases
(see Note 3).
Note 1 : Where 'x' is shown, reference is made to the registers associated with the respective output compare channels 1 through 8.
2: OCFA pin controls OC1 -OC4 channels. OCFB pin controls OC5-OC8 channels.
3: Each output compare channel can use one of two selectable time bases. Refer to the device data sheet for the time
bases associated with the module.
Figure 12-8. Output compare module block diagram.
208
The Dark Screen
When used in single pulse mode specifically, the ocxr register can be used to determine the instant
(relative to the value of the selected timer) when the interrupt event will be triggered and, if desired, an
output pin will be set/reset or toggled as required.
Upper Byte
U-0
U-0
R/W-0
U-0
U-0
U-0
U-0
U-0
—
—
OCSIDL
—
—
—
—
—
bit 15
bit 8
Lower Byte
U-0
•
U-0
U-0
R-OHC
R/W-0
R/W-0
R/W-0
R/W-0
—
—
—
OCFLT
OCTSEL
OCM2
OCM1
OCMO
bit 7
bitO
Figure 12-9. The Output Compare control register OCxCON.
The OCxCON register is the only configuration register required to control each of the output compare
modules.
In our application the output compare mechanism can be quite useful as there are two precise instants
where we need to take action: the end of the horizontal synchronization pulse, when generating a
pre/post-equalization or a vertical synchronization line, and the end of the back porch, where the actual
image begins.
OC3 Interrupt (vertical sync line)
OC3 Interrupt (pre/post-equalization line)
Timer3 Interrupt
^
Timer3 period (PR3+1)
Figure 12-10. Interrupt sequence for a synchronization line.
We will choose to use one of the Output Compare modules (OC3 will be our choice) to help us identify
precisely the end of the synchronization pulse. We will not need to use the associated output pin (RD2),
but rather in the corresponding interrupt service routine we will raise the Sync signal.
void _ISRFAST _OC3 Interrupt ( void)
{
SYNC = 1; // bring the output up to the black level
_0C3IF = 0; // clear the interrupt flag
} // OC3 Interrupt
The OC3CON control register will be set so as to activate the output compare module in the single pulse
mode (ocm=001) and to use Timer3 as the reference time base (octsel=i).
209
Chapter 12
We will also initialize the 0C3R register with the selected timing value depending on the type of line
(state of the state machine) as follows:
// vertical state machine
switch ( VState) {
case SV_PREEQ:
// horizontal sync pulse
OC3R = HSYNC_T;
OC3CON = 0x00 09; // single event mode
break;
case SV_SYNC:
// vertical sync pulse
OC3R = H_NTSC - HSYNC_T;
OC3CON = 0x00 09; // single event mode
break;
case SV_POSTEQ:
// horizontal sync pulse
OC3R = HSYNC_T;
OC3CON = 0x00 09; // single event mode
When generating a video line, we will use a second Output Compare module (OC4) to mark the end of
the back porch and the corresponding interrupt service routine will be used to initiate the streaming of
the actual image line.
(
DC3 Interrupt
OC4 Interrupt
Timer3 Interrupt
Timer3 period (PR3+1)
w
Figure 12-11. Interrupt sequence for a video line.
case SV_LINE:
// activate OC3 for the end of the horizontal sync pulse
OC3R = HSYNC_T;
OC3CON = 0x0009; // single event
// activate OC4 for the end of the back porch
OC4R = HSYNC_T + BPORCH_T;
OC4CON = 0x0009; // single event
break;
210
The Dark Screen
Memory allocation
So far we have been working on the generation of the synchronization signals composing the NTSC
video waveform controlled by only one of two I/Os of the simple hardware interface. The second I/O
will be used once we are generating one of the lines containing the actual image. Toggling the Video
I/O, we can alternate segments of the line that will be painted in white (1) or black (0). Since the NTSC
standard specifies a maximum luminance signal bandwidth of 4.2 MHz, and the space between front
and back porch is 52 us wide, it follows that the maximum number of alternate segments (cycles) of
black and white we can display is 218, (52 x 4.2) or in other words, our maximum theoretical hori-
zontal resolution is 436 pixels per line (assuming the screen is completely used from side to side). The
maximum vertical resolution is given by the total number of lines making up each NTSC standard
frame minus the minimum number of equalization and vertical synchronization lines that gives 253. If
we were to generate the largest possible image, it would be composed of an array of 253 x 436 pixels,
or 1 10,308 pixels. Further, if one bit was used to represent each pixel that would require us to allocate
an array of 13.5 kbytes, way too large to fit in the 8 kbytes available within the PIC24FJ128GA010
RAM. In practice, while it is nice to be able to generate a high-resolution output, we need to make sure
that the image will fit in the available RAM memory and possibly leave enough space for an actual ap-
plication to run comfortably, allowing for adequate room for the stack and application variables. While
there is an almost infinite number of possible combinations of the horizontal and vertical resolution
values that will give an acceptable memory size, there are two considerations that we will use to pick
the perfect numbers: making the horizontal resolution a multiple of 16 will make the math involved in
determining the position of a pixel in the memory map easier, assuming we use an array of integers.
Also, making the two resolution values in an approximate ratio of 4:3 will avoid image geometrical
distortions (in other words, circles drawn on the screen will look like circles rather then ovals).
Choosing a horizontal resolution of 256 pixels (hres) and a vertical resolution of 192 lines (vres) we
obtain an image memory requirement of 6,144 bytes (256 x 192/8), leaving as much as 2,048 bytes for
stack and application variables.
Using the C30 compiler, we can easily allocate a single array of integers (grouping 16 pixels at a time
in each word) to contain the entire image memory map. But we need to make sure that the entire con-
tents of the array are addressable and this is not possible if we declare it simply as a near variable (the
default when using the small memory model). Near variables must be found within the first 8 kbytes of
the data addressing space but this space also includes the special function registers area and the PSV
area. The best way to avoid an allocation error message is to explicitly declare the video memory map
with a far attribute:
#define _FAR attribute (( far))
int _FAR VMap[VRES * (HRES/ 16) ];
This ensures that access to the array elements is performed via pointers, something we would have
done anyway both when reading and writing to the array.
Image serialization
If each image line is represented in memory in the VMap array by a row of 16 integers, we will need to
serially output each bit (pixel) in a timely fashion in the short amount of time (52 us) between the back
and the front porch part of the composite video waveform.
211
Chapter 12
In other words, we will need to set or reset the chosen Video output pin with a new pixel value each
200 ns or better. This would translate into about three machine cycles between pixels, way too fast for
a simple shift loop even if we plan on coding it directly in assembly. Worse, even assuming we man-
aged to squeeze the loop in so tight, we would end up using an enormous percentage of the processing
power for the video generation, leaving very few processor cycles for the main application (<18% in
the best case). Fortunately, there is one peripheral of the PIC24 that can help us efficiently serialize the
image data. It's the SPI synchronous serial communication module.
In a previous chapter we used the SPI2 port to communicate with a serial EEPROM memory. In that
chapter we noted how the SPI module is in fact composed of a simple shift register that can be clocked
by an external clock signal (when in slave mode) or by an internal clock (when in master mode). In
our new project we can use the SPI1 module as a master, connecting only the SDO (serial data output)
directly to the Video pin of the hardware interface, leaving the SDI (data input) unused and the SCK
(clock output) and SS (slave select) pins disabled. Among the many new and advanced features of the
PIC24 SPI module, two fit our video application particularly well: the ability to operate in 16-bit mode
and a powerful 8-level-deep FIFO buffer. Operating in 16-bit mode, we can practically double the
transfer speed of data between the image memory map and the SPI module. Enabling the 8-level-deep
FIFO buffer we can load up to 128 pixels (8 words x 16 bits) at a time in the SPI buffer and quickly
return from the interrupt service routine, only to return 25 us later for a second final load, maximizing
the efficiency of the video generator by requiring only two short bursts of activity for each image line.
We can now write the interrupt service routine for the second Output Compare module, configured by
the state machine to be activated right after the back porch to produce the actual image line output:
void _ISRFAST _OC4lnterrupt ( void)
{
// load SPI FIFO with 8 x 16-bit words = 128 pixels
SPI1BUF = *VPtr++
SPI1BUF = *VPtr++
SPI1BUF = *VPtr++
SPI1BUF = *VPtr++
SPI1BUF = *VPtr++
SPI1BUF = *VPtr++
SPI1BUF = *VPtr++
SPI1BUF = *VPtr++
if ( --HCount > 0)
{ // activate again in time for the next SPI load
OC4R += ( PIX_T * 7 * 16);
OC4CON = 0x0009; // single event
}
// clear the interrupt flag
_0C4IF = 0;
} // OC4lnterrupt
Notice how the interrupt service routine reconfigures the OC4 module for a second burst (the second
half of the image line) after loading the first 128 pixel data in the SPI buffer.
212
The Dark Screen
Now that we have identified all the pieces of the puzzle, we can write the complete initialization rou-
tine for all the modules required by the video generator:
void initVideo ( void)
{
// set the priority levels
_T3IP = 4; // this is the default value anyway
_0C3IP = 4;
_0C4IP = 4;
TMR3 =0; // clear the timer
PR3 = H_NTSC; // set the period register to NTSC line
// 2.1 configure Timer3 modules
T3CON = 0x8000; // enabled, prescaler 1:1, internal clock
// 2.2 init Timer3 /OC3 /OC4 Interrupts, clear the flag
_0C3IF = 0; _0C3IE = 1;
_0C4IF = 0; _0C4IE = 1;
_T3IF = 0; _T3IE = 1 ;
// 2.3 init the processor priority level
_IP =0; // this is the default value anyway
// init the SPIl
if ( PIX_T == 2)
SPI1CON1 = 0x043B; // Master, 16 bit, disable SCK/SS, prescale 1:3
else
SPI1CON1 = 0x0437; // Master, 16 bit, disable SCK/SS, prescale 1:2
SPI1CON2 = 0x0001; // Enhanced mode, 8 x FIFO
SPI1STAT = 0x8000; // enable SPI port
// init PORTF for the Sync
_TRISG0 =0; // output the SYNC pin
// init the vertical sync state machine
VState = SV_PREEQ;
VCount = PREEQ_N;
} // initVideo
Notice how the parameter pix_t can be used to select different SPI clock prescaling values so as to
adapt to different horizontal resolution requirements. Setting PIX_T = 3 will provide the least image
distortion by giving each pixel 3 clock cycles for a total of 187.5 ns, very close to the 200-ns value
previously calculated for the 256-pixel horizontal resolution.
213
Chapter 12
Building the video module
We can now complete the coding of the entire video state machine, adding all the definitions and pin
assignments necessary:
/*
** NTSC Video using T3 and Output Compare interrupts
*/
#include <p24f j 128ga010 .h>
# include "Graphic.h"
// I/O definitions
#define SYNC _LATG0 // output
#define SDO _RF8 // SPIl SDO
// timing definitions for NTSC video vertical state machine
#define V_NTSC 2 62 // total number of lines composing a frame
#define VSYNC_N 3 // V sync lines
// count the number of remaining black lines top+bottom
#define VBLANK_N (V_NTSC -VRES - VSYNC_N)
#define PREEQ_N VBLANK_N /2 // pre equalization + bottom blank lines
#define P0STEQ_N VBLANK_N - PREEQ_N // post equalization + top blank lines
// definition of the vertical sync state machine
# define SV_PREEQ
#define SV_SYNC 1
#define SV_POSTEQ 2
#define SV_LINE 3
// timing definitions for NTSC video horizontal state machine
#define H_NTSC 1018 // total number of Tcy in a line (63.5us)
#define HSYNC_T 90 // Tcy in a horizontal sync pulse
#define BPORCH_T 90 // Tcy in a back porch
#define PIX_T 3 // Tcy in each pixel, valid values are only 2 or 3
#define _FAR attribute (( far))
int _FAR VMap[VRES * (HRES/16)];
volatile int *VPtr;
volatile int HCount, VCount, VState, HState;
214
The Dark Screen
II next state table
int VS[4] = { SV_SYNC, SV_POSTEQ, SV_LINE, SV_PREEQ} ;
// next counter table
int VC[4] = { VSYNC_N, POSTEQ_N, VRES, PREEQ_N} ;
void _ISRFAST _T3 Interrupt ( void)
{
// Start a Sync pulse
SYNC = 0;
// decrement the vertical counter
VCount-- ;
// vertical state machine
switch ( VState) {
case SV_PREEQ:
// horizontal sync pulse
OC3R = HSYNC_T;
OC3CON = 0x0009; // single event
break;
case SV_SYNC:
// vertical sync pulse
OC3R = H_NTSC - HSYNC_T;
OC3CON = 0x0009; // single event
break;
case SV_POSTEQ:
// horizontal sync pulse
OC3R = HSYNC_T;
OC3CON = 0x0009; // single event
// on the last posteq prepare for the new frame
if ( VCount == 0)
{
VPtr = VMap;
}
break;
default :
case SV_LINE:
// horizontal sync pulse
OC3R = HSYNC_T;
OC3CON = 0x0009; // single event
// activate OC4 for the SPI loading
OC4R = HSYNC_T + BPORCH_T;
215
Chapter 12
0C4C0N = 0x0009; // single event
HCount = HRES/128; // loads 8x16 bits at a time
break;
} //switch
// advance the state machine
if ( VCount == 0)
{
VCount = VC[ VState];
VState = VS[ VState];
}
// clear the interrupt flag
_T3IF = 0;
} // T3 Interrupt
To make it a complete library module we will need to add the interrupt service routines presented for
the Output Compare modules OC3 and OC4 illustrated in the previous sections of this chapter, as well
as a couple of additional accessory functions that will follow:
void clearScreen( void)
{
int i , j ;
int *v;
v = ( int * ) &VMap [ ] ;
// clear the screen
for ( i=0; i < (VRES* ( HRES/16)); i++)
*v++ = 0;
} //clearScreen
void haltVideo ( void)
{
T3CONbits .TON =0; // turn off the vertical state machine
} //haltVideo
void synchV( void)
{
while ( VCount != 1) ;
} / / synchV
216
The Dark Screen
In particular, clear Screen will be useful to initialize the image memory map, the VMap array, while
haltvideo will be useful to suspend the video generation should an important task/application require
100% of the PIC24 processing power.
The synchv function can be used to synchronize a task to the video generator; this function will return
only when the video generator has started "painting" the last line of the screen. This can be useful for
graphic applications to minimize flicker and/or provide more fluid scrolling and motion.
Save all of these functions in a file called "graphic . c" and add this file to a new project called "video".
Then create a new file and add the following definitions:
/*
* *
NTSC Video
* *
Graphic
library
* *
*/
# define
VRES
192
# define
HRES
256
// desired vertical resolution
// desired horizontal resolution (pixel)
void initVideo ( void) ;
void haltvideo ( void) ;
void clearScreen( void);
void synchV ( void);
extern int VMap [HRES/ 16 *VRES] ;
Save this file as "graphic .h" and add it to the same project.
Notice how the horizontal resolution and vertical resolution values are the only two parameters ex-
posed. Within reasonable limits (due to timing constraints and the many considerations expressed in the
previous sections), they can be changed to adapt to specific application needs; the state machine and all
other mechanisms of the video generator module will adapt their timing as a consequence.
217
Chapter 12
Testing the video generator
In order to test the video generator module we have just completed, we need only the MPLAB® SIM
simulator tool and possibly a few lines of code for a main program:
//
// Graphic Test.c
//
// testing the basic graphic module
//
#include <p24f j 128ga010 .h>
#include ". . /graphic/graphic . h"
main ( )
{
// initializations
TRISA = 0xff80; // set PORTA lsb as output for debugging
clearScreen ( ) ; // init the video map
initVideo ( ) ; // start the video state machine
// main loop
while ( 1)
{
} // main loop
} // main
Save the project and use the build project checklist to build the entire project.
Open the logic analyzer window and use the logic analyzer checklist to add the RGO pin (Sync) and
the SDOl output (Video) to the analyzer channels. At this point you could run the simulator for a few
seconds and, after pressing the halt button, switch to the logic analyzer output window to observe the
results. The trace memory of the simulator is of a limited capacity and can visuallize only small subset
of an entire video frame. In other words, it is very likely that you will be confronted with a relatively
uninteresting display containing a regular series of sync pulses and a flat video output. Unfortunately,
the simulator does not simulate the output of the SPI port, so for that we'll have to wait until we run
the application on real hardware. As per the Sync line, there is one interesting time we would like to
observe — that is when we generate the vertical synchronization signal with a sequence of three long
horizontal sync pulses at the beginning of each frame. By setting a breakpoint on the first line of the
OC4 interrupt service routine (called for the first time at the beginning of the first image line), you can
make sure that the simulation will stop relatively close to the beginning of a new frame.
218
The Dark Screen
SD01
RGO
Figure 12-12. Screen capture of the logic analyzer window, vertical sync pulses.
If you are patient you can count the number of lines (one per sync pulse) following the three vertical
sync (long) pulses and verify that they are in fact 33 (that is (262-192-3) / 2). Also you can zoom in
the central portion to verify the proper timing of the sync pulses in the pre/post and vertical sync lines.
Using the cursors, you can verify the number of cycles composing a horizontal line period and the
width of the horizontal sync pulse. Keep in mind that the logic analyzer window approximates the
reading to the nearest screen pixel, so the accuracy of your reading will depend on the magnification
(improving as you zoom in) and the resolution of your PC screen. Naturally if what you need is to de-
termine a time interval with absolute precision, the most direct method is to use the stopwatch function
of the MPLAB SIM software simulator together with the appropriate breakpoint settings.
SD01
RGO
1 01 9
- 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 r
83400.0 83600.0 83800.0 84000.0 84200.0 84400.0 84600.0
Figure 12-13. Zoomed view of a single pre-equalization line.
219
Chapter 12
Measuring performance
Since the video generator module uses three different sources of interrupt and a state machine with four
states, it might be interesting to get an idea of the actual processor overhead involved, possibly utilizing
the logic analyzer to illustrate the percentage of time the processor spends inside the various interrupt
service routines.
To this end, we will need to make a few simple modifications to all three of the interrupt service rou-
tines. We will use a pin of PORTA (RAO) as a flag that will be set to indicate when we are executing
inside an interrupt service routine and cleared when we are executing the main loop:
void _ISRFAST _T3lnterrupt ( void)
{
_RA0=1;
_RA0=0;
} // T3 Interrupt
void _ISRFAST _0C3 Interrupt ( void)
{
_RA0=1;
_RA0=0;
} // 0C3 Interrupt
void _ISRFAST _OC4lnterrupt ( void)
{
_RA0=1;
_RA0=0;
} // OC4lnterrupt
RGO
RAO
73 ISR 0C3 ISR
OC4 ISR
0C4 ISR
t 1 1 1 1 1 r
2855000.0
t 1 1 r
2855200.0
t 1 1 r
2855400.0
t 1 r
2855600.0
t 1 r
2855800.0
Figure 12-14. Screen capture of the logic analyzer output, measuring performance.
220
The Dark Screen
After recompiling and adding RAO to the channels captured by the logic analyzer tool, we can zoom
into a single horizontal line period (select an image line).
Using the cursors, we can measure the approximate duration of each interrupt service routine and, add-
ing up the values for the worst possible case (an image line where all four interrupts are invoked), we
obtain a value of 200 cycles out of a line period of 1018 cycles, representing an overhead of less than
20% of the processor time, a remarkably good result.
The dark screen
Playing with the simulator and the logic analyzer tool can be entertaining for a little while, but I am
sure at this point you will feel an itch for the real thing! You can test the video interface on a real TV
screen (or any other device capable of receiving an NTSC composite video signal) connected with the
simple three-resistor interface to an actual PIC24 device. If you have an Explorer 16 board, this is the
time to take out the soldering iron and connect the three resistors to the small prototyping area in the
top right corner of the demo board and out to a standard RCA video jack. Alternatively, if you feel your
electronic hobbyist skills are up to the task, you could even develop a small PCB for a daughterboard
that would fit in the expansion connectors of the Explorer 16.
Check on the companion web site "www.flyingthepic24.com" for the availability of expansion
boards that will allow you to follow all the advanced projects of the third part of the book.
Whatever your choice, though, the experience will be breathtaking.
Or not! In fact, if you wire all the connections just right when you power up the Explorer 16 board,
what you are going to be staring at is just a blank (or I should better say black) screen. Sure, this is an
achievement; in fact this already means that a lot of things are working right, as both the horizontal and
vertical synchronization signals are being decoded correctly by the TV set and a nice, uniform black
background is being displayed.
Figure 12-15. The dark screen.
221
Chapter 12
A test pattern
To spice things up, we should start filling that video array with something worth looking at, possibly
something simple that can give us an immediate feedback on the proper functioning of the video
generator.
Let's create a new test program as follows:
//
// Graphic Test2.c
//
// testing the basic graphic module
//
#include <p24f j 128ga010 .h>
#include ". . /graphic/graphic .h"
main ( )
{
int x , y ;
// fill the video memory map with a pattern
for( y=0; y<VRES; y++)
for (x=0; x<HRES/16; x++)
VMap[y*16 + x] = y;
initVideo ( ) ; // start the video state machine
// main loop
while ( 1)
(
} // main loop
} // main
Instead of calling the clearScreen function, this time we use two nested for loops to initialize the VMap
array. The external (y) loop counts the vertical lines, the internal (x) loop moves horizontally, filling the
16 words (each containing 16 bits) with the same value: the line count. In other words, on the first line
each 16-bit word will be assigned the value 0, on the second line each word will be assigned the value
1, and so on until the last line (192) where each word will be assigned the value 191 (OxBF in hexa-
decimal).
222
The Dark Screen
If you build the new project and test the video output, you should be able to see the following pattern:
ill ill u m i n I If
1 1 1 i liii 1 1 1 it 1 1 1
9l-flLsl-9 r ^H I ~^M _J^H ~^l __^B _^H
Figure 12-16. A screen capture of the video output generated with the test pattern.
In its simplicity, there is a lot we can learn from observing the test pattern. First of all, we notice that
each word is visually represented on the screen in binary with the most significant bit presented on the
left. This is a consequence of the order used by the SPI module to shift out bits: that is in fact msb first.
Secondly, we can verify that the last row contains the expected pattern: OxOObf, so we know that all
rows of the memory map are being displayed. Finally, we can appreciate the detail of the image. Dif-
ferent output devices (TV sets, projectors, LCD panels,...) will be able to lock the image more or less
effectively and/or will be able to present a sharper image depending on the actual display resolution
and their input stage bandwidth. In general, you should be able to appreciate how the PIC24 can gener-
ate effectively straight vertical lines. This is not a trivial achievement. In fact, for each pixel to align
properly row after row in a straight vertical line, there must be an absolute jitterless (deterministic)
response to the timer interrupts, a notable characteristic of all PIC® microcontroller architectures.
This does not mean that on the largest screens you will not be able to notice small imperfections here
and there, as small echoes and possibly minor visual artifacts in the output image. Realistically the
simple three-resistor interface can only take us so far.
Ultimately, the entire composite video-signal interface could be blamed for a lower quality output. As
you might know, S -Video, VGA and most other video interfaces keep luminance and synchronization
signals separate to provide a more stable and clean picture.
Plotting
Now that we are reassured about the proper function of the graphic display module, we can start focus-
ing more on generating the actual images onto the memory map. The first natural step is to develop a
function that allows us to light up one pixel at a precise coordinate (x, y) on the screen. The first thing
to do is derive the line number from the y coordinate. If the x and y coordinates are based on the tradi-
tional Cartesian plane representation, with the origin located in the bottom left corner of the screen, we
need to invert the address before accessing the memory map, so that the first row in the memory map
223
Chapter 12
corresponds to the y maximum coordinate vres-1 or 189, while the last row in the memory map cor-
responds to the y coordinate 0. Also, since our memory map is organized in rows of 16 words, we will
need to multiply the resulting line number by 16 to obtain the address of the first word on the given
line. This can be obtained with the following expression: VMap [ ( vres-1 -y) *16] .
Pixels are grouped in 16-bit words, so to resolve the x coordinate we first need to identify the word that
will contain the desired pixel. A simple division by 16 will give us the word offset on the line. Adding
the offset to the line address as calculated above will provide us with the complete word address inside
the memory map:
VMap[ (VRES-1 -y) *16 + (x/16)]
In order to optimize the address calculation we can make use of shift operations to perform the multi-
plication and division as follows:
VMap[ (VRES-1 -y) << 4 + (x»4)]
To identify the bit position inside the word corresponding to the required pixel, we can use the remain-
der of the division of x by 16, or more efficiently we can mask out the lower 4 bits of the x coordinate.
Since we want to turn the pixel on, we will need to perform a binary OR operation with an appropriate
mask that has a single bit set in the corresponding pixel position. Remembering that the display puts
the msb of each word to the left (the SPI module shifts bits msb first) we can build the mask with the
following expression:
( 0x8000 » ( x & Oxf ) )
Putting it all together, we obtain the core plot function:
VMap[ ( (VRES-l-y)«4) + (x»4)] |= (0x8000 >> (x & Oxf));
As a final touch we can add "clipping," that is a simple safety check, just to make sure that the coordi-
nates we are given are in fact valid and within the current screen map limits:
void Plot ( unsigned x, unsigned y)
{
if ((x<HRES) ScSc (y<VRES) )
VMap[ ( (VRES-l-y)«4) + (x»4)] |= (0x8000 >> (x & Oxf));
} // plot
By defining the x and y parameters as unsigned integers we guarantee that negative values will be
discarded too, as they will be considered large integers outside the screen resolution.
A starry night
To test the newly developed plot function, we will create a new project. We will include the "graphic . c "
and "graphic .h" files but we will also use the pseudo-random number generator functions avail-
able in the standard C library "stdlib.h". By using the pseudo-random number generator to produce
224
The Dark Screen
random x and y coordinates for a thousand points, we will both test the plot function and, in a way, the
random generator itself with the following simple code:
//
// Graphic Test3.c
//
// testing the basic graphic module
// plotting random points
//
#include <p24f j 128ga010 .h>
# include ". . /graphic /graphic .h"
#include <stdlib.h>
void plot ( unsigned x, unsigned y)
{
if ((x<HRES) && (y<VRES) )
VMap[ ( (VRES-l-y)«4) + (x»4)] |= (0x8000 >> (x & Oxf));
} // plot
main ( ;
(
int i ;
// initializations
clearScreen ( ) ; // init the video map
initVideo ( ) ; // start the video state machine
srand(13); // initialize the pseudo random number generator
for( i=0; i<1000; i++)
{
plot( rand()%HRES, rand ( ) %VRES) ;
}
// main loop
while ( 1)
{
} // main loop
} // main
225
Chapter 12
The output on your video display should look like a nice starry night, as in the screen shot captured in
Figure 12-17.
Figure 12-17. Screen capture, plotting a starry night.
A starry night it is, but not a realistic one you will notice, as there is no recognizable trace of any in-
creased density of stars around a belt — in other words, there is no Milky Way! This is a good thing! This
is a simple proof that our pseudo-random number generator is in fact doing the job it is supposed to.
We can now add the plot function to the "graphic . c" module. Remember to also add the prototype to
the "graphic .h" function so that in the following exercises we will be able to use it.
void plot ( unsigned, unsigned);
Line drawing
The next obvious step is drawing lines, or, I should better say, line segments. Granted, horizontal and
vertical line segments are not a problem; a simple for loop can take care of it, but drawing oblique
lines is a completely different thing. We could start with the basic formula for the line between two
points that you will remember from the old school days:
y = yO + (yl-yO) / (xl-xO) * ( x- xO )
where (xO , yO) and (xl , yl) are, respectively, the coordinates of two generic points that belong to the line.
This formula gives us, for any given value of x, a corresponding y coordinate, so we might be tempted
to use it in a loop for each discrete value of x between the starting and ending point of the line, as in the
following example:
//
// Line Testl.c
//
// testing the basic line drawing function
//
#include <p24f j 128ga010 .h>
# include ". . /graphic /graphic . h"
226
The Dark Screen
main ( )
{
int x;
float xO
= 10, yO = 20, xl = 200, yl = 150, x2 = 20, y2 = 150;
// initializations
clearScreen ( ) ; // init the video map
initVideo ( ) ; // start the video state machine
// draw an oblique line (x0,y0) - (xl,yl)
f or ( x=x0 ; x<xl ; x++)
plot( x, y0+(yl-y0) / (xl-xO) * (x-x0));
// draw a second (steeper) line (x0,y0) - ( x2 , y2
f or ( x=x0 ; x<x2 ; x++)
plot( x, y0+(y2-y0) / (x2-x0) * (x-x0));
// main loop
while ( 1)
{
} // main loop
} // main
The output produced is an acceptably continuous segment only for the first (shallower) line where the
horizontal distance (xl - xO) is greater than the vertical distance (yl - yO). In the second, much steeper,
line the dots appear disconnected and we are clearly unhappy with the result. Also we had to perform
floating-point arithmetic, a computationally expensive proposition compared to integer arithmetic, as
we have seen in previous chapters.
Figure 12-18. Screen capture, drawing oblique lines.
227
Chapter 12
Bresenham algorithm
Back in 1962, when working at IBM in the San Jose development lab, Jack E. Bresenham developed a
line-drawing algorithm that uses exclusively integer arithmetic and is today considered the foundation
of any computer graphic program. Its approach is based on three optimization "tricks":
1. Reduction of the drawing direction to a single case (left to right).
2. Reduction of the line steepness to the single case where the horizontal distance is the greatest.
3. Multiplying both sides of the equation by the horizontal distance (del tax) to obtain only
integer quantities.
The resulting line-drawing code is compact and extremely efficient; here is an adaptation for our video
module:
#define abs ( a) (((a)> 0) ? (a) : -(a))
void line( int xO , int yO , int xl, int yl)
{
int steep, t ;
int deltax, deltay, error;
int x, y;
int ystep;
steep = ( abs (yl - yO) > abs (xl - xO ) ) ;
if ( steep )
{ / / swap x and y
t = xO; xO = yO; yO = t;
t = xl ; xl = yl ; yl = t ;
}
if (xO > xl)
{ / / swap ends
t = xO ; xO = xl ; xl = t ;
t = yO; yO = yl; yl = t;
)
deltax = xl - xO ;
deltay = abs (yl - yO);
error = ;
y = yO;
if (yO < yl) ystep = 1; else ystep = -1;
for (x = xO ; x < xl; x++)
{
if ( steep) plot(y,x); else plot(x,y);
error += deltay;
if ( (error«l) >= deltax)
{
228
The Dark Screen
y += ystep;
error -= deltax;
} // if
} // for
} // line
We can add this function to the video module "graphic . c" and a prototype to the include file
"graphic . h".
To test the efficiency of the Bresenham algorithm, we can create a new small project and once more
include the pseudo-random number generator by including the "stdlib.h" library. The following
example code will first draw a frame around the screen and then it will exercise the line-drawing
routine producing a hundred lines from randomly generated coordinates. The main loop also contains a
check for the S3 button (the leftmost button on the bottom of the Explorer 16 demo board) to be pressed
before the screen is cleared again and a new set of random lines is drawn on the screen.
//
// Bresenham. c
//
// Bresenham algorithm example
//
#include <p24f j 128ga010 .h>
#include <stdlib.h>
#include ". . /graphic /graphic .h"
main ( )
{
int i ;
// initializations
initVideo ( ) ; // start the state machines
srand ( 12 ) ;
// main loop
while ( 1)
{
clearScreen ( ) ;
line( 0, 0, 0, VRES-1) ;
line( 0, VRES-1, HRES-1, VRES-1);
line( HRES-1, VRES-1, HRES-1, 0);
line( 0, 0, HRES-1, 0) ;
for( i = 0; i<100; i++)
line( rand()%HRES, rand()%VRES, rand()%HRES, rand ( ) %VRES ) ;
// waiting for a button to be pressed
while ( 1)
229
Chapter 12
i
if ( !_RD6
break;
} // wait
} // main loop
} // main
Figure 12-19. Screen capture, Bresenham line-drawing algorithm test.
You will be impressed by the speed of the line-drawing algorithm; even when increasing the number of
lines drawn to batches of one thousand, the performance of the PIC24 will be apparent.
Plotting math functions
With the completed graphic module we can now start exploring some interesting applications that can
take full advantage of its visualization capabilities. One classical application could be plotting a graph
based on data logged from a sensor or, more simply for our demonstration purposes, calculated on the
fly from a given math function.
For example, let's assume the function is a sinusoid with a twist as in the following:
y (x) = x * sin( x)
Let's also assume we want to plot its graph for values of x between and 8 * PI.
With minor manipulations we can scale the function to fit our screen, remapping the input range from
to 200 and the output range to the +151-15 values range.
230
The Dark Screen
The following program example will plot the function after tracing the x and y axes:
/*
** Plotting a ID function graph
*/
#include <p24f j 128ga010 .h>
# include <math.h>
# include ". . /graphic /graphic .h"
#define XO 10
#define Y0 (VRES/2)
#denne PI 3.141592654f
main ( void)
{
int x, y;
float xf, yf;
// initializations
clearScreen ( ) ;
initVideo ( ) ;
// draw the x and y axes crossing in (X0,Y0)
line( XO, 10, XO , VRES-10); // y axes
line( XO-5, Y0 , HRES-10, Y0 ) ; // x axes
// plot the graph of the function for
for( x=0; x<2 00; x++)
{
xf = (8 * PI / 200) * (float) x;
yf = 75.0 / ( 8 * PI) * xf * sin( xf);
plot ( x+XO, yf+YO) ;
}
// main loop
while ( 1) ;
} // main
Should the points on the graph become too sparse, we have the option of using the line-drawing algo-
rithm to connect each point to the previous.
231
Chapter 12
Figure 12-20. Screen capture, a sinusoidal function graph.
Two-dimensional function visualization
Plotting two-dimensional function graphs could be more interesting and perhaps entertaining. This
adds the thrill of managing the perspective distortion and the challenge of connecting the calculated
points to form a visually pleasing grid.
The simplest method to squeeze the third axis into a two-dimensional image is to utilize what is com-
monly known as an isometric projection, a method that requires minimal computational resources
while providing a small visual distortion. The following formulas applied to the x, y and z coordinates
of a point in a three-dimensional space produce the px and py coordinates of the projection on a two-
dimensional space (our video screen).
px = x + yll;
py = z + yll\
X
Figure 12-21. Isometric projection.
In order to plot the three-dimensional graph of a given function: z =f(x,y) we proceed on a grid of
points equally spaced in the x and y plane using two nested for loops. For each point we compute the
232
The Dark Screen
function to obtain the z coordinate and we apply the isometric projection to obtain a (px.py) coordinate
pair. Then we connect the newly calculated point with a segment to the previous point on the same row
(previous column). A second segment needs to be drawn to connect the point to the previously com-
puted point in the same column and the previous row.
previous row
newly calculated point
(px, py)
previous point
(prev.x, prev.y)
X
Figure 12-22. Drawing a grid to enhance a two-dimensional graph visualization.
While it is trivial to keep track of the coordinates of the previously computed point on the same row,
recording the coordinates of the points on each previous row might require significant memory space.
If, for example, we are using a grid of 20 x 20 points, we would need to store the coordinates of up to
400 points. Requiring two integers each, that would add up to 800 words or 1,600 bytes of precious
RAM memory. In reality, as should be evident from the picture above, all we really need is the coor-
dinates of the points on the "edge" of the grid as painted so far. Therefore, with a little care, we can
reduce the memory requirement to just 20 coordinate pairs by maintaining a small rolling buffer.
The example code below visualizes the graph of the function:
z(x 9 y) = 1/ sqrt( x 2 + y 2 ) * cos ( sqrt( x 2 + y 2 )
for values of x and y in the range -3 * PI to +3 * PI
/*
** Plotting a 2D function graph
* *
**
*/
#include <p24f j 128ga010 .h>
#include <math.h>
#include ". . /graphic/graphic .h"
#define X0
10
# define Y0
10
# define PI
3.141592654f
ttdefine NODES
20
# define SIDE
10
233
Chapter 12
typedef struct {
int x;
int y;
} point;
point edge [NODES], prev;
main ( void)
{
int i, j, x, y, z ;
float xf, yf , zf, sf;
int px, py;
// initializations
clearScreen ( ) ;
initVideo ( ) ;
// draw the x, y and z axes crossing in (X0,Y0)
line( XO, 10, X0 , VRES-50); // z axis
line( XO-5, Y0 , HRES-10, Y0 ) ; // x axis
line( XO-2, YO-2, XO+120, YO+120); // y axis
// init the array of previous egde points
for( j = 0; j<NODES; j++)
{
edge[j].x = X0+ j*side/2;
edge[j].y = Y0+ j*SlDE/2;
}
// plot the graph of the function for
for( i=0; i<NODES; i++)
{
// transform the x coordinate range to 0..200 offset 100
x = i * SIDE;
xf = (6 * PI / 200) * (float) (x-100) ;
prev.y = Y0 ;
prev.x = X0 + x;
for ( j=0; j<NODES; j++)
{
// transform the y coordinate range to 0. .200 offset 100
y = j * SIDE;
yf = (6 * PI / 200) * (float) (y-100) ;
// compute the function
sf = sqrt ( xf * xf + yf * yf) ;
zf = 1/ (1+ sf ) * cos( sf ) ;
234
The Dark Screen
II scale the output
z = zf * 75;
// apply isometric perspective and offset
px = XO + x + y/2;
py = YO + z + y/2;
// plot the point
plot ( px, py) ;
// draw connecting lines to visualize the grid
line( px, py, prev.x, prev.y) ; // connect to prev point on same x
line( px, py, edge[j].x, edge[j].y);
// update the previous points
prev . x = px ;
prev.y = py;
edge [ j ] .x = px;
edge [ j ] . y = py ;
} // for j
} // for i
// main loop
while ( 1 ) ;
} // main
After building the project and connecting to a display, you will notice how quickly the PIC24 will
produce the output graph, although significant floating-point math is required as the function is applied
sequentially to 400 points and as much as 800 line segments are drawn on the video memory.
Figure 12-23. Screen capture, graph of a two-dimensional function.
235
Chapter 12
Fractals
"Fractals" is a term coined for the first time by Benoit Mandelbrot, a mathematician (and fellow
researcher at the IBM Pacific Northwest Labs) back in 1975, to denote a large set of mathematical
objects which presented an interesting property: that of appearing self-similar at all scales of magnifi-
cation as if constructed recursively with an infinite level of detail. There are many examples of fractals
in nature, although their self- similarity property is typically extended over a finite scale range. Exam-
ples include clouds, snow flakes, mountains, river networks and even the blood vessels in our body.
Because it lends itself to impressive computer visualizations, the most popular example of a mathemat-
ical fractal object is perhaps the Mandelbrot set. It's defined as a subset of the complex plane where
the quadratic function z 2 + c is iterated. By exclusion, points (c) of the complex plane for which the
iteration does not "diverge" are considered to be part of the set. Since it is easy to prove that once the
modulus of z is greater than 2, the iteration is bound to diverge (hence the given point is not part of the
set) we can proceed by elimination. The problem is that as long as the modulus of z remains smaller
than 2, we have no way of telling when to stop the iteration and declare the point part of the set. So
typically computer algorithms that depict the Mandelbrot set use an approximation, by setting an arbi-
trary maximum number of iterations past which a point is simply assumed to be part of the set.
Here is an example of how the inner iteration can be coded in C:
// initialization
x = xO ;
y = yO;
k = 0;
// core iteration
do {
x2 = x*x;
y2 = y*y;
y = 2*x*y + yO ;
x = x2 - y2 + xO ;
k++;
} while ( (x2 + y2 < 4) && ( k < MAXIT) ) ;
// check if the point belongs to the Mandelbrot set
if ( k == MAXIT) plot( xO, yO);
where xO and yO are the coordinates in the complex space of the point c.
We can repeat this iteration for each point of a squared subset of the complex plane so as to obtain
an image of the entire Mandelbrot set. From the literature we learn that the entire set is included in a
disc of radius 2 around the origin, so we can develop a first program that will scan the complex plane
in a grid of 192 x 192 points (to use the maximum screen resolution as defined by our video module)
overlapping such a disc:
/*
* *
** Mandelbrot Set graphic demo
* *
*/
236
The Dark Screen
#include <p24f j 128ga010 .h>
# include ". . /graphic /graphic . h"
#define SIZE VRES
# define MAXIT 64
void mandelbrot ( float xxO , float yyO , float w)
(
float x, y, d, xO , yO , x2 , y2 ;
int i, j , k;
// calculate increments
d = w/SIZE;
// repeat on each screen pixel
yO = yyO;
for (i=0; i<SIZE; i++)
{
xO = xxO ;
for (j=0; j<SIZE; j++)
{
// initialization
x = xO ;
y = yO;
k = 0;
// core iteration
do {
x2 = x*x;
y2 = y*y;
y = 2*x*y + yO ;
x = x2 - y2 + xO ;
k++;
} while ( (x2 + y2 < 4) && ( k < MAXIT));
// check if the point belongs to the Mandelbrot set
if ( k == MAXIT) plot( j, i);
// compute next point xO
xO += d;
} // for j
// compute next yO
yO += d;
} // for i
} // mandelbrot
237
Chapter 12
main (
{
float x, y, w;
// initializations
initVideo ( ) ; // start the state machines
// intial coordinates lower left corner of the grid
x = -2.0;
Y = -2.0;
// initial grid side
w = 4.0;
while ( 1)
(
clearScreen ( ) ; // clear the screen
mandelbrot ( x, y, w) ;
while (1) ;
} // main loop
} // main
With the maximum number of iterations set to 64, the PIC24 will produce the complete image below,
the so-called Mandelbrot cardioid, in approximately 30 seconds.
Figure 12-24. Screen capture, Mandelbrot set.
238
The Dark Screen
I will confess that since I bought my first personal computer as a kid (actually "home computer" was
the term used back then — it was a Sinclair ZX Spectrum), I have been playing with fractal programs.
So I have a vivid memory of the long hours I used to spend staring at the computer screen waiting for
the old trusty Z80 processor (running at the whopping speed of 3.5 MHz) to paint this same image. A
few years later, my first IBM PC, an XT clone (running on a 8088 processor at a not much higher clock
speed of 4 MHz) was not faring much better, and although the screen resolution of my monochrome
Hercules graphic card was higher, I would still launch programs in the evening to watch the results
the following morning, after what amounted sometimes to up to eight hours of processing. Clearly the
amount of computation required to paint a fractal image varies enormously with the chosen area and
the number of maximum iterations allowed, but the first time I ran this program I could not help being
amazed by how rapidly the PIC24 painted the cardioid before my eyes.
But the real fun has just begun. The most interesting parts of the Mandelbrot set are at the fringes,
where we can increase the magnification and zoom in to discover an infinitely complex world of de-
tails. By visualizing not just the points that belong to the set, but also the ones at its edges that diverge,
and assigning a color that depends on how fast they did in fact diverge, we can further improve the re-
sulting image. Since we have only a monochrome display, we will simply use alternate bands of black
and white assigned to each point according to the number of iterations it took before it either reached
the maximum modulus or the maximum number of iterations. Simply enough, this means we will have
to modify just one line of code from our previous example:
// check if the point belongs to the Mandelbrot set
if ( k & 1) plot( j, i) ;
Also, since the best way to play with Mandelbrot set images is to explore them by selecting new
areas and zooming in on the details, we can transform the main program loop by adding a simple user
interface, by means of the four buttons of the Explorer 16 board. We can imagine splitting the image
into four quadrants. A button will correspond to each quadrant, and by pressing it, we will zoom in,
doubling the resolution and halving the grid dimension (w).
1
c
2
4
r
Figure 12-25. Splitting the screen into four quadrants.
239
Chapter 12
main ( )
{
float x, y, w;
// initializations
initVideo ( ) ; // start the state machines
// intial coordinates lower left corner of the grid
x = -2.0;
Y = -2.0;
// initial grid size
W = 4.0;
while ( 1)
{
clearScreen ( ) ;
mandelbrot ( x, y, w) ;
// clear the screen
/ / draw new image
// wait for a button to be pressed
while (1)
{ // wait for a key pressed
if ( !_RD6)
{ // first quadrant
w/= 2 ;
y += w;
break;
}
if ( !_RD7)
{ // second quadrant
w/= 2;
y += w;
x += w;
break;
}
if ( !_RA7)
{ // third quadrant
w/= 2;
x += w;
break;
}
if ( !_RD13)
{ // fourth quadrant
w/= 2;
break;
}
} // wait for a key
} // main loop
} // main
240
The Dark Screen
Here is a little selection of interesting areas you will be able to explore with a little patience:
Figure. 12 -2 6a
(+0.25 +/ 0.5), w = 0.25
Figure 12 -2 6c
(-1.28125 +/ 0.3125), w = 0.3125
Figure. 12-26b
(+0.37500 -; 0.57813), w = 0.01563
Figure. 12-266
(+0.34375 +/' 0.56250), w = 0.03125
Figure. 12-26e
(+0.34375 +/ 0.56250), w = 0.03125
241
Chapter 12
Text
So far we have been focusing heavily on graphical visualizations, but on more than one occasion you
might have felt the desire to augment the information presented on the screen with some text. Writing
text on the video memory is no different from plotting points or drawing lines, and in fact it can be
achieved with a variety of methods, including using the plotting and line-drawing functions we have
already developed. But for greater performance and in order to require the smallest possible amount of
code, the easiest way to get text on our graphic display is by developing and using an 8 x 8 font array.
Each character can be drawn in an 8 x 8 pixel box; one byte will encode each row, and 8 bytes will
encode the entire character. We can then assemble the 96 base alphabetical, numerical and punctuation
characters in the order and position in which they are presented in the ASCII character set in a single
array and save it as an include file.
1
1
1
1
1
1
1
1
1
1
1
1
Figure 12-27. The letter A as represented in an 8 x 8 font.
To save space we don't need to create the first 32 codes defined in the ASCII set, which correspond
mostly to commands and legacy special synchronization codes used by teletypewriters and modems of
the old times:
//
// 8 x 8 Font definition
//
#define F_OFFS 0x2
#define F SIZE 0x60
// initial offset
// only the first 64 characters defined so far
const char Font8x8[]
// 20 - SPACE
ObOOOOOOOO,
ObOOOOOOOO,
ObOOOOOOOO,
ObOOOOOOOO,
ObOOOOOOOO,
ObOOOOOOOO,
ObOOOOOOOO,
= (
242
The Dark Screen
ObOOOOOOOO,
// 1 - !
ObOOOllOOO,
ObOOOllOOO,
ObOOOllOOO,
ObOOOllOOO,
ObOOOllOOO,
ObOOOOOOOO,
ObOOOllOOO,
ObOOOOOOOO,
Notice that the Font 8x8 [ ] array is defined with the attribute const, as its contents are supposed to
remain unchanged during the execution of the program and it is best allocated in the program memory
space (Flash memory of the PIC24) to save precious RAM memory space.
A complete listing of the H font .h" file would waste several pages, so we will omit it here, but you will
be able to find it on the companion CD-ROM.
Of course, as it is a matter of personal taste, you are welcome to modify the Font 8x8 [ ] array contents
to fit your preferences.
Printing a character on the screen then is a matter of copying one byte at a time from the font ar-
ray to the desired screen position. In the simplest case, characters can be aligned to the words that
compose the VMap (video memory) array defined by the graphics module. In this way, the character
positions would be limited to 32 characters per line (256/8) and a maximum of 24 rows of text could
be displayed (192/8). A more advanced solution would call for absolute freedom in positioning each
character at any given pixel coordinate. This would require a type of manipulation often referred to as
BitBLT (an acronym that stands for Bit BLock Transfer) that is common in computer graphics, particu-
larly in video game design. In the following, though, we will stick to the simpler approach, looking for
the solution that requires the smallest amount of resources to get the job done.
Let's create a new project that we will call "TextOnGPage" and a new source file "TextOnGPage . c"
that will contain all the functions required to print text on the graphic video page. Then, let's define two
integer variables for maintaining the cursor position:
int ex, cy;
We can now write a simple function that prints one ASCII character at a time on the screen at the cur-
rent cursor position as follows:
void putcV( int a)
{
int i, *p;
const char *pf;
// 1. check if char in range
a -= F_OFFS;
if ( a < 0) a = 0;
if ( a >= F_SIZE) a = F_SIZE-1;
243
Chapter 12
II 2. check page boundaries
if ( ex >= HRES/8) // wrap around x
{
ex = ;
cy++;
}
if ( cy >= VRES/8) // wrap around y
cy = 0;
// 3. set pointer to word in the video map
p = &VMap[ cy * 8 * HRES/16 + cx/2];
// set pointer to first row of the character in the font array
pf = &Font8x8 [ a << 3] ;
// 4. copy one by one each line of the character on the screen
for ( i=0; i<8; i++)
{
if ( ex Sc 1)
{
*p & = OxffOO;
*p |= *pf++;
}
else
{
*p & = Oxff;
*p |= (*pf++)<<8;
}
// point to next row
p += HRES/16;
} // for
// increment cursor position
cx++;
} // putcV
In the very first few lines of the function (1.) we verify that the character passed to the function is part
of the subset of the ASCII character set currently defined in our font. If not, we change it into either the
first character defined or the last one. An alternative strategy, available to the reader, would have been
to ignore the character altogether and exit the routine immediately in such a case.
The second part of the function (2.) deals with positioning the cursor (cy , cy), making sure that if we
reach the right edge of the screen we wrap around onto the next line as a typewriter would. A similar
action is taken when we reach the bottom right extreme of the screen by wrapping around to the top of
the screen. The alternative here would have been to implement a scrolling feature that would move the
entire contents of the screen up by one line to make room for a whole new line of text.
244
The Dark Screen
In the third part (3.) a pointer to the screen memory map is computed based on the cursor coordinates,
and a pointer into the font array is computed based on the ASCII character code. Finally (4.) a loop
takes care of copying, line by line, the font image into the video array. Since the video array (VMap)
is organized in words (and the MSB is displayed first) a little attention must be paid in transferring
each byte to the proper position within each 16-bit word. If the cursor position is even, the MSB of the
selected word is replaced by the font data. If the cursor position is odd, the LSB of the selected word is
replaced by the font data. At each step in the loop, the pointer inside the video map (p) is incremented
by 16 words (hres/16) to point to the same position on the following line, while the pointer inside the
font array (pf) is incremented by one to obtain the next byte composing the character image.
For our convenience we can now create a function that will print an entire NULL terminated ASCII
string on the screen:
void putsV( unsigned char *s)
{
while (*s)
putcV( *s++) ;
} // putsV
Remember also to include all the necessary files to compile this module:
#include <p24f j 128ga010 .h>
#include ".. /font /font .h"
# include ". . /graphic /graphic . h"
Finally, let's create a new include file to export the newly defined functions and to add a couple of use-
ful macros:
/*
** Text on Graphic Page
*/
extern int ex, cy;
void putcV( int a) ;
void putsV( unsigned char *s);
#define Home ( ) { cx=0; cy=0;}
#define Clrscr ( ) { clearScreen ( ) ; Home ( ) ; }
#define AT ( x, y) { ex = (x) ; cy = (y) ; }
Home ( ) will simply position the cursor on the upper left corner of the screen.
clrscr ( ) will clear the screen first by invoking the function defined in the graphic module.
at ( ) will position the cursor as required for the next putcv and/or putsv command.
245
Chapter 12
Notice how, differently from the graphic coordinate system, the text cursor coordinate system is de-
fined with the origin located in the home position on the upper left corner of the screen and increasing
vertical coordinates are referring to lines further down the page.
Testing the TextOnGPage module
In order to quickly test the effectiveness of the new text module, we can now create a small program
that, after printing a small banner on the first line of the screen will print out each character defined in
the 8x8 font:
/*
** Text Page Test
* *
*/
#include <p24f j 128ga010 .h>
# include *. . /graphic /graphic . h"
#include ". . /textg/TextOnGPage .h
main ( void)
{
int i ;
// initializations
initVideo ( ) ; // start the state machines
Clrscr ( ) ;
AT ( , ) ;
putsV( "FLYING THE PIC24!");
AT ( , 2 ) ;
for( i=32; i<128; i++)
putcV( i) ;
while ( 1 ) ;
} // main
Save this file as "TextOnGTest .c" and add it to the project. Make sure all the other required modules
are added to the project too, including: "graphic, c", "graphic, h", "font.h", "textongpage.c"
and "textongpage .h". Finally, build the project and run.
246
The Dark Screen
Figure 12-28. Screen capture, text on the graphic page.
Developing a text page video
Using the newly developed "TextOnGPage . c" module, we have now acquired the capability to display
text and graphics on the video screen. The system in its entirety requires 6,080 bytes of RAM for the
video map, a significant portion of the total amount of RAM available inside the PIC24fjl28ga010, but
only a minuscule portion of the program memory available.
Memory Usage Gauge
Program Memory
Total: 44030
Data Memory
Total: 8192
Figure 12-29. Memory usage gauges for the TextOnGTest project.
If our application was going to need the video output only to display text, this would have been an
extremely inefficient solution. In fact, using an 8x8 font, we can only display 32 characters per line
and a maximum of 24 lines, for a grand total of 768 characters. In other words, if our application uses
the video as a pure text display, we are wasting as much as 5,244 bytes of precious RAM. In the early
computer days (including the first IBM PC) this was a serious (economical) problem that demanded a
custom hardware solution. All early personal computer systems had in fact a "text page," that is a video
mode where the display could visualize ONLY text, with the advantage of reducing considerably the
RAM requirements (to a fraction of those of a graphic page) while also increasing considerably the
screen manipulation performance. In a text page, character ASCII codes are stored directly in the video
memory and they are converted on the fly to the graphical font representation by a hardware device
(known as the font generator) intimately connected to the video scanning and timing logic. In this way,
the amount of memory required to maintain a page of 768 characters (as in our previous project) would
have been only and exactly 768 bytes; that is approximately only 10% of the memory required by our
graphic-display solution.
247
Chapter 12
This sounds to me like an interesting new challenge. In the next project we will develop a more RAM-
efficient video solution targeting pure text display applications. This will force us to go back to the
initial definition of the state machine at the heart of the graphic video module. In fact, we can keep
most of its structure intact and proceed to optimize only a few critical areas. All the elements that com-
pose the horizontal and vertical synchronization signals will remain unchanged. Also the construction
of horizontal lines remains untouched up to the point where we start sending data to the SPI1 module
to serialize. Where in the graphic display we take each word of the memory map as is and we push it
on to the SPI buffer, in a text-page video application we will need to operate on a byte at a time and in-
terpose a conversion step. The Font 8x8 [ ] array will act as a look-up table that will be used to convert
the ASCII code on the fly from the text page (now VMap will be defined as a byte array) into an image
that will be sent to the SPI buffer for serialization. In generic terms we can express this translation with
the following expression:
lookup = Font8x8 [ *VPtr * 8 + RCount];
where vptr is the pointer to the current character inside the text page array, and RCount is a counter
from to 7 that keeps track of each video line forming one row of text (there are 8 video lines for each
row of text).
In practice, things are a little more complicated. Since the SPI module must be fed with 16 bits of data
at a time, we need to assemble two characters in one word after performing two look-ups one after the
other:
lookupl = Font8x8 [ *VPtr++ * 8 + RCount];
lookup2 = Font8x8[ *VPtr++ * 8 + RCount];
SPI1BUF = ( 256 * lookupl + lookup2);
Repeat this for 8 times to fill the entire SPI buffer.
Now this is a lot of work to perform in the few microseconds available to the OC4 interrupt service
routine. Even if we were to enable the highest level of optimization of the compiler (and in this book
we actually chose to never enable any optimization), the possibility that it would fit in the time avail-
able (less than 25 us) is pretty slim. There are simply too many multiplications and additions to
perform when working the look-up table. Fortunately, this is something we can change. In fact we
can rearrange the way the Font array is built. While it is convenient to initialize the array filling in all
8 rows of each character and proceeding sequentially, in order to simplify the look-up expression it
would be best if the array were organized the other way around. In other words, we should fill the array
starting with the first byte of each character in the font, followed by each second byte of each character
and so on. We could rewrite the expressions above with the new rearranged font RFont as follows:
lookupl = RFont [ (RCount * F_SIZE) + *VPtr++ ];
lookup2 = RFont [ (RCount * F_SIZE) + *VPtr++ ];
SPI1BUF = ( 256 * lookupl + lookup2);
The great advantage lies in the fact now (RCount*F_siZE) is a constant offset and we can even obtain
a pointer inside the font that already takes care of such offset with the following expression:
FPtr = &RFont[ RCount * F_SIZE] ;
This can be precalculated (inside the Timer3 interrupt service routine) at the beginning of each line for
a significant saving.
248
The Dark Screen
The new look-up expressions are now simplified to:
lookupl = FPtr [ *VPtr++ ] ;
lookup2 = FPtr[ *VPtr++ ] ;
SPI1BUF = ( lookupl << 8 + lookup2);
Now we at least have a chance that the look-up expression could fit in the few microseconds available,
but we are not satisfied yet. Every nanosecond counts in a routine as critical and frequently invoked as
the OC4 interrupt service routine is. The ultimate optimization trick is in fact constituted by the selec-
tive manual coding in assembly of the few most critical steps. If we assume that the font pointer (FPtr)
has been placed in the W2 working register and the video memory pointer (vptr) has been placed in
the wi working register, we can code the entire look-up sequence with just three powerful assembly
instructions:
mov.b [wl++] , wO // wO = *VPtr++ (8 bit)
ze wO , wO // extend wO to a 16 bit integer
mov.b [w2+w0], w3 // w3 = FPtr [ wO ] = FPtr [ *VPtr++] = lookupl
Repeating the same instructions for lookup2 is trivial; combining the two values in one word requires
only a shift:
si w3, #8, w3 // shift W3 8 bits to the left (*256)
and later on an addition:
add wO , w3 / wO // add ( lookupl*256 ) and lookup2
We can put it all together in a single macro that we will call decode ( ) :
#define DECODE ( sfr) \
asm volatile
asm volatile
asm volatile
asm volatile
asm volatile
asm volatile
asm volatile
asm volatile
asm volatile
asm volatile
"mov.b [wl++] , wO" ) ; \
"ze wO, wO") ; \
"mov . b [ w2 +w0 ] , w3 " ) ; \
"si w3, #8,w3" ) ; \
"mov.b [wl++] , wO" ) ; \
"ze wO, wO") ; \
"mov.b [w2+w0], wO " ) ; \
"ze wO, wO") ; \
"add wO, w3, wO") ; \
"mov wO, %0" : "=U"((sfr))
The volatile attribute is used here to make sure that the compiler will not change the order and position
of the inline assembly code should the optimizer be turned on in the future. Also, the last line might
seem a bit cryptic. In fact, we are using an advanced feature of the inline assembly syntax offered by
the C30 compiler that allows us to mix in C variable names, passed as parameters to the asm ( ) func-
tion. The special notation :"=U" indicates that a data operand in brackets is being passed as an output
data recipient.
249
Chapter 12
We can now modify the OC4 interrupt routine to make full use of our highly optimized font table lookup:
void _ISRFAST _OC4 Interrupt ( void)
{
// prepare pointers
volatile asm ( "mov %0, w2 " : : "U" (FPtr) ) ; // w2 = FPtr
volatile asm ( "mov %0, wl" : : "U" (VPtr) ) ; // wl = VPtr
// inline text to font translation * 8 words
DECODE ( SPI1BUF)
DECODE ( SPI1BUF)
DECODE ( SPI1BUF)
DECODE ( SPI1BUF)
DECODE ( SPI1BUF)
DECODE ( SPI1BUF)
DECODE ( SPI1BUF)
DECODE ( SPI1BUF)
asm ( "mov wl , %0" : W =U" (VPtr)); // update VPtr
if ( --HCount > 0)
{ // activate again in time for the next SPI load
0C4R += ( PIX_T * 8 * 16);
OC4CON = 0x0009; // single event
}
// clear the interrupt flag
_0C4IF = 0;
} // OC4lnterrupt
As we said before, the modifications to the Timer3 interrupt service routine are minor, as only a couple
of pointers need to be prepared for the text lines to be properly sequenced and for the font offset to be
precalculated:
void _ISRFAST _T3lnterrupt ( void)
{
// Start a Sync pulse
SYNC = 0;
// decrement the vertical counter
VCount-- ;
// vertical state machine
switch ( VState) {
case SV_PREEQ:
// horizontal sync pulse
OC3R = HSYNC_T;
OC3CON = 0x0009; // single event
break;
250
The Dark Screen
case SV_SYNC:
// vertical sync pulse
0C3R = H_NTSC - HSYNC_T;
0C3C0N = 0x0009; // single event
break;
case SV_POSTEQ:
// horizontal sync pulse
OC3R = HSYNC_T;
OC3CON = 0x0009; // single event
//on the last posteq prepare for the new frame
if ( VCount == 0)
{
LPtr = VMap;
RCount = 0;
}
break;
default:
case SV_LINE:
// horizontal sync pulse
OC3R = HSYNCJT;
OC3CON = 0x0009; // single event
// activate OC4 for the SPI loading
OC4R = HSYNC_T + BPORCH_T;
OC4CON = 0x0009; // single event
HCount =3; // reload counter
// prepare the font pointer
FPtr = &RFont[ RCount * F_SIZE] ;
// prepare the line pointer
VPtr = LPtr;
// Advance the RCount
if ( ++RCount == 8)
{
RCount = ;
LPtr += COLS;
}
} //switch
// advance the state machine
if ( VCount == 0)
{
VCount = VC[ VState] ;
251
Chapter 12
VState = VS[ VState];
}
// clear the interrupt flag
_T3IF = 0;
} // T3 Interrupt
The video initialization routine will now require one more step, as the font array needs to be rearranged
as discussed above:
// prepare a reversed font table
for (i=0; i<8; i++)
{
p = Font8x8 + i;
for (j=0; j<F_SIZE; j++)
{
* r ++ = *p;
p+ = 8;
} // for j
} // for i
While for simplicity we implement this as a second array allocated in RAM where we copy things in
the new order, the ultimate solution is to rearrange the "font .h" file definition, so that the Font8x8
array is already defined in the new and optimal order, there is no RAM waste, and no processing time
is used during the video initialization to perform the translation.
Back when we were working on the graphical interface, we found that a 256 x 192 pixel screen was
an acceptable compromise between screen resolution and memory usage as it would leave 2 kbytes of
RAM available for the application to use. Now the balance is considerably changed; with a 24 lines by
32 column display, only 768 bytes are used by the video module and we can in fact afford to expand
the resolution a bit. The horizontal resolution is the one most in need of an upgrade. Most video termi-
nals use a 25 x 80 format while the average printed document has no less than 60 characters per line.
While we could afford the RAM (25 rows x 80 columns = 2,000 characters), this time it is the NTSC
video specifications that are going to dictate the ultimate limit. As we observed at the very beginning of
this chapter, the maximum signal bandwidth for an NTSC video composite signal is fixed at 4.2 MHz,
while the portion of the waveform producing the visible line image is 52 us wide. This determines a
maximum theoretical horizontal resolution of 436 pixels that, in the case of an 8x8 font, would imply
a maximum number of 54 columns. In practice we would do better to choose a smaller value and, to
make the best use of the SPI FIFO mechanism that we have been using with success so far, we had
better choose a number that is a multiple of 16. While in the graphic module we used two successive
blocks of 128 pixels each to fill the SPI FIFO buffers, for the text page module we can now add a third
block, bringing the total horizontal resolution up to 48 characters. Note how this will require the SPI
clock prescaler to be switched to the higher frequency mode (PIX_T= 2).
For the vertical resolution we have considerable freedom, since the NTSC standard specifies 262 lines
of which theoretically up to 253 could be used for the actual image. There is no difficulty in making 25
rows of text (adding up to 200 lines) fit.
252
The Dark Screen
Overall our text-page module will produce a 25 row by 48 column display, using a total of just 1,200
bytes. This will represent a considerable improvement in readability with respect to the text on graphic
page approach, with a significant reduction in the RAM memory usage as well.
This is the new set of constants and definitions that completes the new "Text" video module:
/*
** TextPage.c
* *
** Text Page video module
* *
*/
#include <p24f j 128ga010 .h>
# include " . . /Text /Text Page . h"
#include ".. /font /font .h"
// I/O definitions
#define SYNC _LATG0 // output
#define SDO _RF8 // SPIl SDO
// calculates the NTSC video parameters for the vertical state machine
#define V_NTSC 2 62 // total number of lines composing a frame
#define VRES (ROWS*8) // desired vertical resolution (<242)
#define VSYNC_N 3 // V sync lines
// count the number of remaining black lines top+bottom
#define VBLANK_N (V_NTSC -VRES - VSYNC_N)
#define PREEQ_N VBLANK_N /2 // pre equalization + bottom blank
#define POSTEQ_N VBLANK_N - PREEQ_N // post equalization + top blank lines
// definition of the vertical sync state machine
# define SV_PREEQ
* define SV_SYNC 1
#define SV_POSTEQ 2
#define SV_LINE 3
// calculates the NTSC video parameters for the horizontal state machine
#define H_NTSC 1018 // total number of Tcy in a line (63.5us)
#define HRES (COLS*8) // desired horizontal resolution (divisible by 16)
#define HSYNC_T 72 // Tcy in a horizontal sync pulse (4.7us)
#define BPORCH_T 9 // Tcy in a back porch (4.7us)
#define PIX_T 2 // Tcy in each pixel
#define LINE_T HRES * PIX_T // Tcy in each horizontal image line
253
Chapter 12
1 1 Text Page array
unsigned char VMap [ COLS * ROWS];
unsigned char *VPtr, *LPtr;
// reordered Font
unsigned char RFont [F_SIZE*8] ;
unsigned char *FPtr;
volatile int HCount, VCount, RCount, VState, HState;
// next state table
int VS[4] = { SV_SYNC, SV_POSTEQ, SV_LINE, SV_PREEQ} ;
// next counter table
int VC[4] = { VSYNC_N, P0STEQ_N, VRES , PREEQ_N} ;
The same routines we developed for the TextOnGPage project can now be added directly to this project.
void haltVideo ( )
{
T3C0Nbits .TON =0; // turn off the vertical state machine
} //haltVideo
void initScreen( void)
{
int i , j ;
char *v;
v = VMap;
// clear the screen
for ( i=0; i < (ROWS); i++)
for ( j=0; j < (COLS) ; j+
*v+ + = 0;
} //initScreen
int ex, cy;
void putcV( int a)
{
// check if char in font range
a -= F_0FFS;
if ( a < 0) a = 0;
if ( a >= F_SIZE) a = F_SIZE-1;
254
The Dark Screen
II check page boundaries
if ( ex >= COLS) // wrap around x
{
ex = ;
cy++;
}
cy %= ROWS; // wrap around y
// find first row in the video map
VMap[ cy * COLS + ex] = a;
// increment cursor position
cx++;
} // putcV
void putsV( unsigned char *s)
{
while (*s)
putcV( *s++) ;
} // putsV
void per ( void)
(
ex = ;
cy++;
cy %= ROWS;
} // per
We can save the new project file as "TextPage .c" and create a new include file "TextPage .h" as
well.
/*
** TextPage.h
* *
** Text Page Video Module
*/
#define ROWS 2 5 // rows of text
#define COLS 48 // columns of text
/ / Text Page array
extern unsigned char VMap [ COLS * ROWS] ;
255
Chapter 12
II initializes the video output
void initVideo ( void) ;
// stops the video output
void haltVideo ( ) ;
// clears the video map
void initScreen( void);
// cursor
extern int ex, cy;
void putV( int a) ;
void putsV( unsigned char *s);
void per ( void) ;
#define home ( ) { cx=0; cy=0;}
#define clrscr ( ) { initScreen ( ) ; home ( ) ; }
#define AT ( x, y) { ex = (x) ; cy = (y) ; }
Testing the text page performance
In order to test the new text page video module, we could try to modify an example seen in a previous
chapter: the Matrix demo. Back then we were using the asynchronous serial communication module
(UART1) to communicate with a VT100 computer terminal (or more likely a PC running the Hyper-
Terminal program configured for emulation of the historical DEC terminals VT100 protocol). Now we
can replace the putcu routine calls used to send a character to the serial port with putcv calls directed
at our video interface.
Let's create a new project called "Matrix2" and let's add all the necessary modules to it including:
the rand . c, rand . h, textpage . c, textpage . h and finally a new main module that we will call
"matrix2.c" or "the-matrix-reloaded. c" if you prefer.
/*
** The Matrix Reloaded
* *
*/
#include <p24f j 128ga010 .h>
#include ".. /random/ rand. h"
#include " . . /Text/TextPage .h"
#define COL 40
# define ROW 2 4
# define DELAY 12 000
256
The Dark Screen
#define per ( ) {ex = 0; cy++;}
main ( )
{
int v[40] ; // vector containing lengh of each string
int i,j,k;
// 1. initializations
T1CON = 0x8030; // TMRl on, prescale 256, Tcy/2
initVideo ( ) ;
clrscr ( ) ; // clear the screen
randomize ( 12); // start the random number sequence
// 2. init each column lenght
for( j =0; j<COL; j++)
v[j] = rand()%ROW;
/ / 3 . main loop
while ( 1)
{
home ( ) ;
// 3.1 refresh the screen with random columns
for( i=0; i<ROW; i++)
{
// refresh one row at a time
for( j=0; j<COL; j++)
{
// print a random character down to each column lenght
if ( i < v[j])
putcV( V A' + (rand()%32) ) ;
else
putcV( v l ) ;
} // for j
per ( ) ;
} // for i
// 3.1.1 delay to slow down the screen update
TMRl =0;
while ( TMR1<DELAY) ;
// 3.2 randomly increase or reduce each column lenght
for( j=0; j<COL; j++)
257
Chapter 12
{
switch ( rand()%3)
{
case 0: // increase length
v[j]++;
if (v[j]>ROW)
v[j]=ROW;
break;
case 1: // decrease length
v[j]— ;
if (v[j]<l)
v[j]=l;
break;
default:// unchanged
break;
} // switch
} // for
} // main loop
} // main
After saving and building the project, run it on the Explorer 16 connected to your video device of
choice. You will notice how much faster the screen updates, as the program now has direct access to the
video memory and there is no serial connection limiting the information transfers (even as fast as the
1 15,200-baud connection was in our previous demo project). Also, because now every character placed
in the video memory can be retrieved and manipulated in place, new tricks are possible to make the
video resemble more closely the movie characteristic and somewhat alien scrolling effect.
Besides the visual impression, though, we are now interested in measuring the actual processor
overhead imposed by the new video routines that perform the on-the-fly font translation. For this mea-
surement, the MPLAB SIM software simulator is again our tool of choice. As we did in the previous
chapters, we can use one of the PORTA pins (RAO) to signal when we are executing code inside one of
the three interrupt service routines:
void _ISRFAST _T3lnterrupt ( void)
{
_RA0=1;
_RA0=0;
} // T3 Interrupt
258
The Dark Screen
void _ISRFAST _OC3 Interrupt ( void)
{
_RA0=1;
_RA0=0;
} // 0C3 Interrupt
void _ISRFAST _OC4lnterrupt ( void)
(
_RA0=1;
_RA0=0;
} // 0C4lnterrupt
Remember to add the initialization of the TRISA register inside the initvideo ( ) function or the main
program to enable the RAO pin output. Then, add both the RGO pin (responsible for producing the
synchronization pulse) and the RAO pin to the Logic Analyzer window channels.
Rebuild the project and run it for a short while, just enough to get the first few image lines that repre-
sent the worst-case scenario, where the most work is produced by the interrupt service routines.
RGO
0C4_
SR
0C4JSQ
-
T3JSR
0C3JSR
0C4J
SR
RAO
Figure 12-30. Logic analyzer window, measuring the text-page video module overhead.
Using the cursor feature, we can now measure the number of cycles required by each of the four inter-
rupt service routines executed during each of the horizontal line periods. While only the Stop Watch
tool can give us an exact cycle count, the logic analyzer window can give us a good approximation
with a lot less work. My measurements indicate that 384 cycles are spent inside the interrupt service
routines of the video module at each 1018 cycles period; that amounts to approximately 38% of the
processor available computing power. This is almost double the overhead incurred by the graphic
video module routines, but the 20% difference is a price we gladly pay for the great reduction in RAM
memory requirements and the increased resolution we gain for all those applications that require a pure
text output.
259
Chapter 12
Post-flight briefing
In this lesson we have explored the possibility of producing a video output using a minimal hardware
interface made up of only three resistors. We learned to use four peripheral modules together to build
the complex mechanism required to produce a properly formatted NTSC composite video signal.
A 16-bit timer was used to generate the fundamental horizontal synchronization period. Two output
compare modules provided intermediate timing references, and finally the SPI module was used in
enhanced mode to serialize the video data using the new 8-level-deep by 16-bit FIFO. After develop-
ing basic graphic functions to plot individual pixels first and efficiently draw lines, we explored some
of the possibilities offered by the availability of a graphic video output, including unidimensional and
two-dimensional functions graphing. After briefly exploring the world of fractals, we changed gears to
look at the problem of displaying text. First we developed routines to add text to the graphic page, and
later we developed a new video module specifically optimized for text display only.
Tips and tricks
The final touch, to complete our brief excursion into the world of graphics, would be to add some anima-
tion to our video output libraries. To make the motion fluid and avoid an annoying continuous flicker of
the image on the screen, we would need to adopt a technique known as "double buffering." This requires
us to have two image buffers in use at any point in time. One is the "active" buffer and its contents are
shown on the screen while the other "hidden" buffer is being drawn. When the second buffer drawing is
complete, the two are swapped. The first buffer, not visible anymore, is cleared and the drawing process
starts again. The only limitation with implementing this technique in our case is represented by the
RAM memory size. To make two image buffers fit in the 8 kbytes of memory of the PIC24fj 128ga010,
while leaving some space for variables and stack, we will need to reduce the image resolution. A pair of
image buffers of 160 x 160, for example, would fit as each would require only 3,200 bytes:
int _FAR VlMap[VRES * (HRES/16)];
int _FAR V2Map[VRES * (HRES/16)];
The only other changes required to the project would be:
1 . Replace direct references to the VMap [ ] array with references to pointers
2. Make the interrupt-driven state machine that refreshes the screen use a pointer to the active
buffer:
int *VA;
3. Make the plotting and drawing functions use a pointer to the hidden buffer:
int *VH;
The swap between the two buffers can then be performed swapping only two pointers:
void swapV ( void)
{
int * V;
while ( VCount != 1) ; // wait until the end of the frame
V = VA; VA = VH; VH = V; //at the next VSynch it will swap the screen
} / / swapV
Notice that care must be taken not to perform the swap in the middle of a frame, but synchronized with
the end of a frame and the beginning of the next.
260
The Dark Screen
Exercises
1 . Replace the "wri te . c " function to redirect the B s tdio . h" library function output to the
text/graphic screen.
2. Add the PS/2 keyboard input support to provide a complete console.
Books
• R. Koster, 2004
A Theory of Fun for Game Design
Paraglyph Press
Links
You must take game design seriously. Or maybe not?
http://en. wikipedia. org/wiki/Zx_spectrum
The Sinclair ZX Spectrum was one of the first personal computers (home computers as they
used to be called) launched in the early 1980s. Its graphic capabilities were very similar to
those of the graphic libraries we developed in this project. Although it used several custom
logic devices to provide the video output, its processing power was less than a quarter that
of the PIC24. Still, the limited ability to produce color (only 16 colors with a resolution of
a block of 8 x 8 pixels) enticed many programmers to create thousands of challenging and
creative video games.
261
CHAPTER
13
Mass storage
In This Chapter
The SD™/MMC card physical
►
Sending commands in SPI mode
interface
►
Completing the SD/MMC card
Interfacing to the Explorer 16
initialization
board
►
Reading data from an SD/MMC card
Starting a new project
►
Writing data to an SD/MMC card
Selecting the SPI mode of
►
Using the SD/MMC interface module
operation
The relationship between weight (mass) and performance of an airplane is generally well understood
by most pilots and nonpilots too. Try to put too much weight on those wings and the takeoff is going to
be longer — much longer, or actually so long that there is not enough runway to continue and there is no
takeoff at all. Ouch!
The more common problem seems to be in understanding how much all that stuff that you (or your
significant other) want to bring along actually weighs. Packing the airplane for a trip with friends or
family is just like packing your backpack for an excursion in the outdoors. The fact that everything
seemed to fit in does not mean you will be able to lift it. As a pilot you won't be allowed to guess at
it; you will have to compile a weight and balance sheet and, if necessary, use a scale to determine the
exact numbers and decide what to sacrifice: some of the load or maybe some of the fuel. One thing that
I can strongly discourage you from doing, though, is to ask your significant other to step on the scale.
Flight plan
In many embedded-control applications you might find a need for a larger nonvolatile data storage
space, well beyond the capabilities of the common serial EEPROM devices we interfaced to in previ-
ous chapters and certainly larger than the Flash program memory available inside the microcontroller
itself. You might be looking for orders of magnitude more, hundreds of megabytes and possibly
gigabytes. If you own a digital camera, an MP3 player or even a cell phone, you have probably become
familiar with the storage requirements of consumer multimedia applications and with the available
mass-storage technologies. Hard disk drives have become smaller and less power thirsty, but also
a multitude of solid-state solutions (based once more on Flash technologies like CompactFlash®,
SmartMedia™, Secure Digital (SD), Memory Stick® and others) have flooded the market. Due to the
volumes absorbed by the consumer market, the price range has been reduced to a point where it is pos-
sible, if not convenient, to integrate these devices into embedded-control applications.
263
Chapter 13
In this lesson we will learn how to interface one of the most common and inexpensive mass-storage
device types to a PIC24 microcontroller using the smallest amount of processor resources.
The flight
Each one of the many competing mass-storage technologies has its strengths and weaknesses, as each
one was designed for a somewhat different target application. We will choose the mass-storage media
according to the following criteria:
Wide availability of the memory and required connectors.
Small pin count required by the physical interface (serial).
Large memory capacity.
Open specifications available.
Ease of implementation.
Low cost of the memory and the required connectors.
The Secure Digital (SD) standard compares favorably in all those aspect as it is today one of the most
commonly adopted mass-storage media for digital cameras and many other multimedia consumer ap-
plications. The SD card specifications represent an evolution of a previous technology known as Multi
Media Card, or MMC, with which they are still partially (forward) compatible both electrically and
mechanically. The Secure Digital Card Association (SDCA) owns and controls the technical specifica-
tion standards for the SD memory card and they require that all companies who plan to actively engage
in the design, development, manufacture or sale of products that utilize the SD specifications must
become members of the association. As of this writing, a general SDCA membership will cost you
$2,000 in annual fees. The Multi Media Card Association (MMC A) on the other side does not require
implementers to necessarily become members, but makes copies of the MMC specifications available
for sale starting at $500. So both technologies are far from free, nor open. Fortunately, there is a subset
of the SD specifications that has been released to the public by the SDCA in the form of a "simpli-
fied physical specification." This information is all we need to develop a basic understanding of the
SD/MMC memory technology and get started designing a PIC24 mass-storage interface.
The SD/MMC card physical interface
SD cards require only nine electrical contacts, and an SD/MMC-compatible connector, which can be
purchased on most online catalogs for less than a couple of dollars, requires only a couple of pins more
to account for insertion detection and write-pro tection switch sensing. There are two main modes of
communication available: the first one (known as the SD bus) is original to the SD/MMC standard and
it requires a nibble (4-bit) wide bus interface; the second mode is serial and is based on the popular
SPI bus standard. It is this second mode that makes the SD/MMC mass-storage devices particularly
appealing for all embedded-control applications, as most microcontrollers will either have a hardware
SPI interface available or will be able to easily emulate one (bit-banging) with a reduced number of
I/Os. Finally, the physical specifications of the SD/MMC cards indicate an operating voltage range of
2.0V to 3.6V that is ideally suited for all applications with modern microcontrollers implemented in
advanced CMOS processes, as is the case with the PIC24 family.
264
Mass storage
SD
MMC
8. DAT1
7. DATO/DO
6. Vss2
5. CLK
4. Vcc
3. Vss1
2. CMD/DI
1 . DAT3/CS
9. DAT2
7. DATO/DO
6. Vss2
5. CLK
4. Vcc
3. Vss1
2. CMD/DI
1 . DAT3/CS
Figure 13-1 . SD card and MMC card connectors pin-out.
Interfacing to the Explorer16 board
Unfortunately, although the number of electrical connections required for the SPI interface is very low,
all SD/MMC card connectors available on the market are designed for surface-mount applications only,
which makes it almost impossible to use the prototyping area of the Explorer 16 demonstration board.
To facilitate this lesson and the following lessons that will make use of mass-storage devices, complete
schematics and PCB layout information for an expansion board have been published on the companion
web site http://www.flyingthePIC24.com. The expansion board also has interfaces that will be used in
the following chapters of the book.
Since in the previous chapter we have used the first SPI peripheral module to produce a video output and
the application does not allow for sharing of the resource, we will share instead the second SPI module
(SPI2) between the SD card interface and the EEPROM interface using separate Chip Select signals for
the two. In addition to the usual SCK, SDI and SDO pins, we will provide pull-ups for the unused pins
(reserved for the 4-bit wide SD bus interface) of the SD/MMC connector and for two more pins that will
be dedicated to the Card Detect and Write Protect signals.
*3.3V
SD Card connector
~ GND
Figure 13-2. SD/MMC card interface to Explorer 16 demo board
265
# define
SDWD
_RG1
# define
SDCD
_RF1
# define
SDCS
RFO
Chapter 13
Starting a new project
After creating a new project (using the usual checklist) we will start by creating the basic initialization
routines for all the necessary I/Os and configuring the SPI2 module:
/*
** SD card interface
* *
*/
#include <p24f j 128ga010 .h> // pin out definitions
// Write Protect input
// Card Detect input
// Card Select output
void initSD( void)
// initializes the I/Os and peripheral modules (SPI2)
{
SDCS = 1; // default Card not-selected (high)
_TRISF0 =0; // make only Card select an output pin
// init the spi module for a slow (safe) clock speed first
SPI2CON1 = 0x013c; // CKE=1, SMP=0, CKP=0, prescale 1:64
SPI2STAT = 0x8000; // enable the SPI2 peripheral
} // initSD
In particular, in the SPI2CON1 register we need to configure the SPI module to operate in master mode
with the proper clock polarity, clock edge, input sampling point and initial clock frequency. The clock
output (SCK) must be enabled and set low when idle. The sampling point for the SDI input must be
centered. The frequency is controlled by means of two prescalers (primary and secondary) that divide
the main processor cycle clock (Tcy) to generate the SPI clock signal. After power up and until the SD
card is properly initialized, we will have to reduce the clock speed to a safe setting (below 400 kHz);
therefore we will use the primary prescaler setting 1:64 to obtain a 250-kHz clock signal. This is just
a temporary arrangement; after sending only the first few commands, we will be able to speed up the
communication considerably.
Notice how only the RFO pin, controlling the Card Select signal, needs to be manually configured
as an output pin, while RG6 and RG8 (corresponding to the pins SCK2 and SD02) are automati-
cally configured as outputs when we enable the SPI2 peripheral.
266
Mass storage
Selecting the SPI mode of operation
When an SD/MMC card is inserted in the connector and powered up, it is in the default mode of
communication: the SD bus mode. In order to inform the card that we intend to communicate using
the alternative SPI mode, all we need to do is to select the card (sending the SDCS pin low) and start
sending the first RESET command. We can rest assured that, once in the SPI mode, the card will not
be able to change back to the SD bus mode unless the power supply is cycled. This means, though, that
if the card is removed from the slot without our knowledge and then reinserted, we will have to make
sure that the initialization routine or at least the reset command is repeated in order to get back to the
SPI mode. We can detect the card presence at any time by checking the status of the RF1 pin connected
to the CD line.
Sending commands in SPI mode
In SPI mode, commands are sent to an SD/MMC card as packets of six bytes, and all responses from
the SD card are provided with multiple-byte data blocks of variable length. So all we need to commu-
nicate with the memory card is the usual basic SPI routine to send and receive (the two operations are
really the same as we have seen in the previous chapter) a byte at a time:
// send one byte of data and receive one back at the same time
unsigned char writeSPI ( unsigned char b)
{
SPI2BUF = b; // write to buffer for TX
while ( ! SPl2STATbits . SPIRBF) ; // wait for transfer to complete
return SPI2BUF; // read the received value
}// writeSPI
For improved code readability and convenience, we will also define two more macros that will mask
the same writeSPI ( ) function as a pure readspi ( ) , or just as a clock output function clockspi ( ) .
Both macros will send a dummy byte of data (OxFF):
#define readSPI ( ) writeSPI ( OxFF)
#define clockSPI ( ) writeSPI ( OxFF)
To send a command we will start selecting the card (SDCS low) and sending a packet composed of
three parts through the SPI port.
BYTE 1
BYTE 2
BYTE 3
BYTE 4
BYTE 5
BYTE 6
76543210
76543210
76543210
76543210
76543210
76543210
COMMAND
ADDRESS
CRC
Figure 13-3. SPI-mode SD/MMC Card command format.
The first part is a single byte containing a command index. The following definitions will cover all the
commands we will be using for this project:
// SD card commands
#define RESET // a.k.a. GO_IDLE (CMD0)
#define INIT 1 //a.k.a SEND_OP_COND (CMDl)
#define READ_SINGLE 17 // read a block of data
#define WRITE_SINGLE 2 4 // write a block of data
267
Chapter 13
The command index is followed by a 32-bit memory address. It is an unsigned long integer value
that must be sent MSB first. For convenience we will define a new type to represent such large address
fields, which we will call lba, borrowing from a term used in other mass-storage applications to repre-
sent a very large address to a generic block of data:
typedef unsigned long LBA; // logic block address, 32 bit wide
Finally, the command packet is completed by a one-byte CRC. This Cyclic Redundancy Check (CRC)
feature is always used in SD bus mode to make sure that every command and every block of data
transmitted on the bus is free from errors. But as soon as we switch to the SPI mode (after sending
the reset command) the CRC protection will be automatically disabled, as the card will assume that
a direct and reliable connection to the host, the PIC24 in our case, is available. By taking advantage
of this default behavior, we can considerably simplify our code replacing the CRC calculation with a
precomputed value. This will be the CRC code of the reset command, and it will be ignored for all
the subsequent commands for which the CRC field will be a "don't care." Here is the first part of the
sendSDCmdO function:
int sendSDCmd( unsigned char c, LBA a)
// sends a 6 byte command block to the card and leaves SDCS active
{
int i , r ;
// enable SD card
SDCS = 0;
// send a comand packet (6 bytes)
writeSPI ( c 0x40) ; // send command + frame bit
writeSPI ( (unsigned char) a>>24) ; // msb of the address
writeSPI ( a>>16) ;
writeSPI ( a>>8) ;
writeSPI ( a) ; // lsb
// NOTE only CMDO-RESET requires an actual CRC (once in SPI mode CRC is disabled)
writeSPI ( 0x95); // send CRC of RESET, for all other cmds it's a don't care
After sending all six bytes to the card, we wait in a loop for a response byte (we will in fact keep send-
ing dummy data continuously clocking the SPI port). The response will be OxFF (basically the SDI
line will be kept high) until the card is ready to provide us with a proper response code. The specifica-
tions indicate that up to 64 clock pulses (eight bytes) might be necessary before a proper response is
received. Should we exceed this limit we would have to assume a major malfunctioning of the card and
abort communication.
268
Mass storage
// now wait for a response up to 8 bytes delay
i = 9;
do {
r = readSPI ( ) ; // check if ready
if ( r != OxFF) break;
} while ( --i > 0) ;
return ( r) ;
/* return response
FF - timeout, no answer
- command accepted
01 - command received, card in idle state (after RESET)
other errors
*
/
} // sendSDCmd
If we receive a response code, each bit if set will provide us with an indication of a possible problem:
bit = Idle state
bit 1 = Erase Reset
bit 2 = Illegal command
bit 3 = Communication CRC error
bit 4 = Erase sequence error
bit 5 = Address error
bit 6 = Parameter error
bit 7 = Always
Notice that on return the sendSDCmd ( ) function leaves the SD card still selected (SDCS low) so that
commands such as Block Write and Block Read, that require additional data to be sent or received
from the card, will be able to proceed. In all other commands that do not require additional data trans-
fers, we will have to remember to deselect the card (set SDCS high) immediately after the function
call. Furthermore, since we want to share the SPI2 port with other peripherals (for example, the Serial
EEPROM mounted on the Explorer 16 board) we need to make sure that the SD/MMC card receives a
few more clock cycles (8 will suffice) immediately after the rising edge of the chip select line (SDCS).
According to the SD/MMC specifications this will allow the card to complete a few important house-
keeping chores, including the proper release of the SDO line, essential to allow other devices on the
same bus to communicate properly.
Another pair of macros will help us perform this consistently:
#define disableSDO SDCS = 1; clockSPI ( )
#define enableSDO SDCS =
269
Chapter 13
Completing the SD/MMC card initialization
Before the card can be effectively used for mass-storage applications, there is a well-defined sequence
of commands that needs to be completed. This sequence is defined in the original MMC card specifica-
tions and has been modified only slightly by the SD card specifications. Since we are not planning on
using any of the advanced features specific to the SD card standard, we will use the basic sequence as
defined for MMC cards for maximum compatibility. There are five parts of this sequence:
1. the card is inserted in the connector and powered up.
2. the CS line is initially kept high (card not selected).
3. more than 74 clock pulses must be provided before the card becomes capable of receiving
commands.
4. the card must then be selected and a reset (cmdo ) command provided: the card should re-
spond entering the Idle state and (activating the SPI mode).
5. an init (cmdi ) command is provided and will be repeated until the card exits the Idle state.
INITcmd
RESET cmd
Figure 13-4. SD Card initialization sequence
The following segment of the function initMedia ( ) will perform these initial five steps:
int initMedia ( void)
{
int i , r ;
// 1. while the card is not selected
SDCS = 1;
// 2. send 80 clock cycles to start up
for ( i=0; i<10; i++)
clockSPI () ;
270
Mass storage
II 3. then select the card
SDCS = 0;
// 4. send a reset command to enter SPI mode
r = sendSDCmd( RESET, 0); SDCS = 1;
if ( r != 1)
return 0x84;
// 5. send repeatedly INIT
i = 10000; // allow for up to 0.3s before timeout
do {
r = sendSDCmd( INIT, 0); SDCS = 1;
if ( r) break;
} while ( --i > 0) ;
if (( i==0) | | ( r!=l))
return 0x85; // timed out
The initialization command can require quite some time, depending on the size and type of memory
card, normally measured in several tenths of a second. Since we are operating at 250 kb/s, each byte
sent will require 32 us. Accounting for six bytes for every command retry and using a timeout count of
10,000 will provide us with a generous timeout limit of approximately two seconds.
It is only upon successful completion of this sequence that we will be allowed to finally switch gears
and increase the clock speed to the highest possible value supported by our hardware. With minimal
experimentation you will find that an Explorer 16 board, with a properly designed daughter board
providing the SD/MMC connector, can easily sustain a clock rate as high as 8 MHz. This value can be
obtained by reconfiguring the SPI primary prescaler for a 1:1 ratio and using the secondary prescaler
for a 1:2 ratio. We can now complete the initMedia ( ) function with the last segment:
// 6. increase speed
SPI2STAT =0; // disable momentarily the SPI2 module
SPI2CON1 = 0x013b; // change prescaler to 1:2
SPI2STAT = 0x8000; // re-enable the SPI2 module
return ;
} // init media
Reading data from an SD/MMC card
SD/MMC cards are solid-state devices containing typically large arrays of Flash memory, so we would
expect to be able to read and write any amount of data (within the card capacity limits) at any desired
address. In reality, compatibility considerations with many previous (legacy) mass-storage technolo-
gies have imposed a number of constraints on how we can access the memory. In fact all operations
are defined in blocks of a fixed size that by default is 512 bytes. It is not a coincidence that 512 bytes
is the exact standard size of a data "sector" of a typical personal computer hard disk. Although this can
be changed with an appropriate command, we will maintain the default setting so as to take advantage
of this compatibility. We will develop a set of routines that will allow us in the following chapter to
implement a complete file system compatible with the most common PC operating systems. This way
271
Chapter 13
we will be able to access files written on the card by a personal computer and, vice versa a personal
computer will be able to access files written by our applications.
The read_s ingle (CMD17 ) is all we need to initiate a transfer of a single "sector" from a given
address in memory. The command takes as an argument a 32-bit "byte" address though, so to avoid
confusion, in the following sections we will uniformly use only lbas or block addresses and we will
obtain an actual byte address by multiplying the lba value by 512 just before passing the parameter to
the read_s ingle command.
We can use the sendSDCmd ( ) function developed above to initiate the read sequence (it will select the
card and leave it selected), and after checking the returned response code for errors (there should be
none), we will wait for the memory card to send a specific token: data_start. This uniquely identi-
fies the beginning of the block of data. Again here, as during the initialization phases, it is important to
impose a timeout, although we can be generous. Since only the readSPi ( ) function is called repeat-
edly (sending/receiving only one byte at a time) while waiting for the data token, a timeout counter of
10,000 will provide an effective time limit of approximately 0.32 seconds (extremely generous).
Once the token is identified, we can confidently read in a rapid sequence all 512 bytes composing the
requested block of data. They will be followed by a 16-bit CRC value that we should read, although we
will have no use for it.
It is only at this point that we will deselect the memory card and terminate the entire read command
sequence.
DATA OUT
(SDO) —
COMMAND
DATA IN
(SDI)
DATA BLOCK
CRC
RESPONSE START TOKEN 16-bit CRC
Figure 13-5. Data transfer during a READ_SINGLE command.
272
Mass storage
The routine readSECTOR ( ) performs the entire sequence in a few lines of code.
// SD card responses
# define DATA START OxFE
int readSECTOR ( LBA a, char *p)
//a LBA requested
// p pointer to data buffer
// returns TRUE if successful
{
int r , i ;
READ_LED = 1;
r = sendSDCmd( READ_S INGLE, ( a « 9) ) ;
if ( r == 0) // check if command was accepted
{
// wait for a response
i = 10000;
do{
r = readSPI ( ) ;
if ( r == DATA_START) break;
}while( --i>0) ;
// if it did not timeout, read a 512 byte sector of data
if ( i)
{
for( i=0; i<512; i++)
*p++ = readSPI ( ) ;
// ignore CRC
readSPI ( ) ;
readSPI ( ) ;
} // data arrived
} // command accepted
// remember to disable the card
disableSD( ) ;
READ_LED = 0;
return ( r == DATA_START) ; // return TRUE if successful
} // readSECTOR
273
Chapter 13
To provide a visual indication of activity on the memory card similar to that used on hard drives and flop-
py disk drives, we have assigned one of the LEDs available on the Explorer 16 board as the "read" LED,
hoping this will help prevent a user from removing the card while in use. The LED is turned on before
each read command and turned off at the end. Other strategies are possible, however. For example, similar
to the common practice on USB Flash drives, an LED could be turned on as soon as the card is initial-
ized, regardless of whether an actual command is performed on it at any given point in time. Only calling
a deinitialization routine would turn the LED off and indicate to the user that the card can be removed.
DATA OUT
(SDO)
DATA IN
(SDI)
COMMAND
DATA BLOCK
START TOKEN
CRC
16-bit CRC
RESPONSE
DATA ACCEPT
WRITE COMPLETE
Figure 13-6. Data transfer during a WRITE_5INGLE command.
Writing data to an SD/MMC card
Based on the same consideration we made for the read functions, we will develop a write function that
will be similarly constrained to operate on "Sectors" — that is, blocks of 512 bytes of data. The write
sequence will use, as you would expect, the write_s ingle command, but this time the data transfer
will be in the opposite direction. Once we make sure that the command is accepted, we will immediate-
ly start sending the DATA_START token and right after it all 512 bytes of data, followed by two more
bytes for the 16-bit CRC (any dummy value will do). At this point we will pause and check that a new
token: DATA_ ACCEPT is sent by the card. It will confirm that the entire block of data has been re-
ceived and the write operation has started. While the card is busy writing it will keep the SDO line low.
Waiting for the completion of the write command will require a new loop where we will wait for the
SDO line to return high. Once more, a timeout must be imposed to limit the amount of time allowed to
the card to complete the operation. Since all SD/MMC memories are based on Flash memory technol-
ogy, we can expect the time typically required for a write operation to be considerably longer than that
required for a read operation. A timeout value of 10,000 would provide us again with a 0.3-s limit that
is more than sufficient to accommodate even the slowest memory card on the market.
#define DATA ACCEPT
0x05
int writeSECTOR ( LBA a, char *p)
//a LBA of sector requested
// p pointer to sector buffer
// returns TRUE if successful
{
unsigned r, i;
WRITE_LED = 1;
r = sendSDCmd( WRITE_SINGLE, ( a << 9) ) ;
if ( r == 0) // check if command was accepted
274
Mass storage
{
writeSPI ( DATA_START) ;
for( i=0; i<512; i++)
writeSPI ( *p++) ;
// send dummy CRC
clockSPI () ;
clockSPI () ;
// check if data accepted
if ( (r = readSPK) & Oxf) == DATA_ACCEPT)
{
for( i=10000; i>0; i--)
{// wait for end of write operation
if ( r = readSPI () )
break;
}
} // accepted
else
r = FAIL;
} // command accepted
// to disable the card and return
disableSD( ) ;
WRITE_LED = 0;
return ( r) ; // return TRUE if successful
} // writeSECTOR
Similarly to the read routine, a second LED has been assigned to indicate when a write operation is be-
ing performed and potentially alert the user. If the card is removed during the write sequence, data will
most likely be lost.
Save the source we developed so far in a file called "sdmmc . c".
Then add a couple of functions for detecting the card presence and the position of the write protect
switch:
int detectSD( void)
{
return ( !SDCD) ;
} // detect SD
int detectWP( void)
{
return ( !SDWP) ;
} // detect WP
275
Chapter 13
Notice that the WP switch is just providing an indication; it is not connected to a hardware mecha-
nism that would prevent an actual write operation from being performed on the card. It is your
responsibility to decide where and when to check for the presence of the WP and to respect it.
Finally, let's create a new include file called "sdmmc .h" to provide the prototypes and basic definitions
used in the SD/MMC interface module.
/*
** SD/MMC low level card interface
# *
*/
# define TRUE 1
# define FALSE
# define FAIL
// 10 definitions
# define READ_LED _RA1
#define WRITE LED RA2
typedef unsigned long LBA; // logic block address, 32 bit wide
void initSD( void);
int initMedia( void);
int detectSD( void);
int detectWP ( void);
int readSECTOR ( LBA, char *);
int writeSECTOR ( LBA, char *);
Using the SD/MMC interface module
Whether you believe it or not, the six minuscule routines we just developed are all we need to gain
access to the seemingly unlimited amount of nonvolatile memory offered by the SD/MMC memory
cards. For example, a 512 MB card would provide us with approximately 1,000,000 (yes, that is one
million) individually addressable memory blocks (sectors) each 512 bytes large. Note that, as of this
writing, SD/MMC cards of this capacity are normally offered for retail in the U.S. for less than $20!
Let's develop a small test program to demonstrate the use of the SD/MMC module. The idea is to
simulate a somewhat typical application that is required to save some large amount of data on the
SD/MMC memory card. A fixed number of blocks of data will be written in a predetermined range of
addresses and then read back to verify the successful completion of the process.
Let's open a new source file and start by adding the usual header and processor- specific include file,
followed by the sdmmc . h include file.
276
Mass storage
/*
** SDMMC read/write Test
* *
*/
#include <p24f j 128ga010 .h>
# include " SDMMC. h"
Then, let's define two byte arrays each the size of a default SD/MMC memory block that is 512 bytes.
#define B_SIZE 512 // sector /data block size
char data [ B_SIZE] ;
char buffer [ B_SIZE] ;
The test program will fill the first with a specific and easy-to-recognize pattern, and will repeatedly
write its contents onto the memory card. The chosen address range will be defined by two constants:
#define START_ADDRESS 10000 // start block address
#define N_BLOCKS 10 00 // number of blocks
The LEDs on PORTA of the Explorer 16 demonstration board will provide us with visual feedback
about the correct execution of the program and/or any error encountered.
The first few lines of the main program can now be written to initialize the I/Os required by the SD/
MMC module and the PORTA pins connected to the row of LEDs.
main ( void)
{
LBA addr;
int i, r;
// I/O initializations
TRISA = OxffOO; // initialize PORTA LEDs output pins
initSDO; // initialize all I/Os required for the SD/MMC module
// fill the buffer with "data"
for( i=0; i<B_SIZE; i++)
data [ i ] = i ;
The next code segment will have to check for the presence of the SD card in the slot/connector. We will
wait in a loop for the card detection switch if necessary, and we will provide an additional delay for the
contacts to properly debounce.
// wait for card to be inserted
while ( ! detectSD ( ) ) ; // assumes SDCD pin is by default an input
Delayms ( 100) ; // wait for card contacts debounce and power up
We will be generous with the debouncing delay as we want to make sure that the card connection is
stable before we start firing "write" commands that could otherwise potentially corrupt other data
present on the card. A 100-ms delay is a reasonable delay to use and the Delayms ( ) function can be
quickly implemented using any of the PIC24 timers or even the RTCC module. Here is an example that
277
Chapter 13
uses the Timer 1 timer module and assumes a processor clock of 32 MHz as is the case on the Explor-
er^ board.
void Delayms ( unsigned t)
{
T1C0N = 0x8000; // enable tmrl, Tcy, 1:1
while (t--)
{
TMR1 = 0;
while (TMR1<16000) ;
}
} // Delayms
Keeping the debouncing delay function separate from the detectSD ( ) function and the SD/MMC
module in general is important, as this will allow different applications to pick and choose the best tim-
ing strategy and optimize the resources allocation.
Once we are sure that the card is present, we can proceed with its initialization calling the initMedia ( )
function.
// initialize the memory card (returns if successful)
r = initMedia();
if ( r) // could not initialize the card
{
PORTA = r; // show error code on LEDs
while ( 1); // halt here
}
The function returns an integer value, which is zero for a successful completion of the initialization
sequence, or a specific error code otherwise. In our test program, in case of an initialization error we
will simply publish the error code on the LEDs and halt the execution, entering an infinite loop. The
codes 0x84 and 0x85 will indicate that the initMedia ( ) function steps 4 or 5 have failed, respectively,
corresponding to an incorrect execution of the card reset command and card init commands (failure
or timeout), respectively.
If all goes well, we will be able to proceed with the actual data-writing phase.
else
{
// fill N_BLOCK blocks/SECTOR with the contents of data buffer
addr = START_ADDRESS;
for( i=0; i<N_BLOCKS; i++)
if ( !writeSECTOR( addr+i, data))
{ // writing failed
PORTA = 0x0 f;
while( 1); // halt here
}
The simple for loop performs repeatedly the writeSECTOR ( ) function over the address range from
block 10,000 to block 10,999, copying over and over the same data block and verifying at each step
that the write command is performed successfully. In case any of the block write commands returns an
278
Mass storage
error, a unique code (OxOf) will be presented on the LEDs and the execution will be halted. In practice
this will be equivalent to writing a file of 512,000 bytes.
// verify the contents of each block/SECTOR written
addr = START_ADDRESS;
for( i=0; i<N_BLOCKS; i++)
{ // read back one block at a time
if ( ! readSECTOR ( addr+i, buffer))
{ // reading failed
PORTA = OxfO;
while ( 1); // halt here
}
// verify each block content
if ( ! memcmp ( data, buffer, B_SIZE) )
{ // mismatch
PORTA = Oxff;
while ( 1); // halt here
}
} // for each block
Next, we will start a new loop, to read back each data block into the second buffer, and we will
compare its contents with the original pattern still available in the first buffer. If the readSECTOR ( )
function should fail we will present an error code (OxfO) on the LEDs display and terminate the test.
Otherwise, a standard C library function memcmp ( ) will help us perform a fast comparison of the buffer
contents, returning an integer value that is zero if the two buffers are identical as we hope, not zero
otherwise. Once more a new unique error indication (0x55) will be provided if the comparison should
fail. To gain access to the memcmp ( ) function that belongs to the standard C string library, we will add
a new include file to our list:
#include <string.h>
We can now complete the main program with a final indication of successful execution, lighting up all
LEDs on PORTA.
} // else media initialized
// indicate successful execution
PORTA = OxFF;
// main loop
while ( 1 ) ;
} // main
If you have added all the required source files: "sdmmc .h", "sdmmc . c" and "sdmmctest .c" to the
project, you can now use the standard checklist to build the project and program it on the Explorer 16
demonstration board. You will need a daughterboard with the SD/MMC connections, as described at
the beginning of the lesson, to actually perform the test. But the effort of building one (or the expense
of purchasing one) will be more than compensated for by the joy of seeing the PIC24 perform the test
flawlessly in a fraction of a second. The amount of code required was also impressively small.
279
Chapter 13
Figure 13-7. MPLAB® Memory Gauges window.
All together, the test program and the SD/MMC access module have used up only 803 words (2409
bytes) of the processor Flash program memory; that is less than 2% of the total memory available. As
in all previous lessons, this result was obtained with all compiler optimization options turned off.
Post-flight briefing
In my personal opinion no other mass storage technology is cheaper or easier than this. After all, we
can use only a handful of pull-up resistors, a cheap connector, and just a few I/O pins to expand enor-
mously the storage capabilities of our applications. In terms of PIC24 resources required, only the SPI
peripheral module has been used and even that could be shared with other applications.
The simplicity of the approach has its obvious limitations though. Data can be written only in blocks of
fixed size and its position inside the memory array is completely application specific. In other words,
there is no way to share data with a personal computer or other device capable of accessing SD/MMC
memory cards unless a "custom" application is developed. Worse, if an attempt is made to use a card
already used by a PC, PC data will likely be corrupted and the entire card might require complete re-
formatting. In the next lesson we will address these issues by developing a complete file system library.
Tips and tricks
The choice of operating on the default block size of 512 bytes was dictated mostly by historical rea-
sons. By making the low-level access routines in this lesson conform with the standard size adopted by
most other mass storage media devices (including hard drives), we made developing the next layer (the
file system) easier. But if we were looking for maximum performance, this could have been the wrong
choice. In fact, if we were looking for faster write performance, typically the bottleneck of every Flash
memory media, we would be better looking at much larger data blocks. Flash memory offers typically
very fast access to data (reading) but is relatively slow when it comes to writing. Writing requires two
steps: first a large block of data (often referred to as a page) must be erased; then the actual writing can
be performed on smaller blocks. The larger the memory array, the larger, proportionally, the erase page
size will be. For example, on a 512 MB memory card, the erase page can easily exceed 2 kbytes. While
these details are typically hidden from the user, as the main controller inside the card takes care of the
erase/write sequencing and buffering, this can have an impact on the overall performance of the appli-
cation. In fact, if we assume a specific SD card has a 2 kbytes page, writing any amount of data (<2k)
would require the internal card controller to perform the following steps:
Read the contents of an entire 2 kbyte block in an internal buffer.
Erase it, and wait for the erase-time.
280
Mass storage
Replace a portion of the buffer content with the new data.
Write back the entire 2-kbytes block, and wait for the write-time.
By performing write operations only on blocks of 512 bytes each, to write 2 kbytes of data, our library
would have to ask the SD card controller to perform the entire sequence four times, while it could be
done in just one sequence by changing the data block length or using a multiple-block write command.
While this approach could theoretically increase the writing speed by 400% in the example above,
consider the option carefully as the price to pay could be quite high. In fact, consider the following
drawbacks:
The actual memory page size might not be known or guaranteed by the manufacturer, al-
though betting on increasing densities of Flash media (and therefore increasing page size) is
pretty safe.
The size of the RAM buffer to be allocated inside the PIC24 application is increased and this
is a precious resource in any embedded application.
The higher software layers (that we will explore in the next lesson) might be more difficult to
integrate if the data block size varies.
The larger the buffer, the larger the data loss if the card is removed before the buffer is
flushed.
Exercises
1. Experiment with various data block sizes to identify where your SD card provides the best
write performance. This will give you an indirect indication of the actual page size of the
Flash memory device used by the card manufacturer.
2. Experiment with multiple-block write commands by changing the block length to verify
how the internal buffering is performed by the SD card controller and if the two methods are
equivalent.
Books
J. Axelson, 2006
USB Mass Storage: Designing and Programming Devices and Embedded Hosts
Lakeview Research, WI
This book continues the excellent series on USB by Jan Axelson. While low-level interfacing
directly to a SD/MMC card was easy, as you have seen in this chapter, creating a proper USB
interface to a mass storage device is a project of a much higher order of complexity.
Links
http://www. mmca. org/home
The official web site of the MultiMedia Card Association (MMCA).
http://wxvw. sdcard. org/
The official web site of the Secure Digital Card Association SDCA.
281
Chapter 13
http://www.sdcard.org/sdio/Simplified%20SDIO%20Card%20Specification.pdf
The simplified SDIO card specifications. With SDIO, the SD interface is no longer used only
for mass storage, but is also a viable interface for a number of advanced peripherals and giz-
mos, such as GPS receivers, digital cameras and more.
282
CHAPTER
14
File I/O
In This Chapter
Sectors and Clusters
The File Allocation Table (FAT)
The root directory
The treasure hunt
► Opening a file
Reading data from a file
Closing a file
►
►
►
►
►
►
►
Creating a fileio module
Testing fopenMQ and freadMQ
Writing data to a file
Closing a file, second take
Accessory functions
Testing the complete fileio module
Code size
Every flight during the training should have a precise purpose assigned by the instructor or inspired by
the course syllabus used by the school. In each and every lesson, we stated our purpose in a section we
called the flight plan, but in aviation an actual flight plan is a different thing. It is a very detailed list
containing the times, altitudes, headings, fuel-consumption figures, and so forth for all the segments
(legs) composing the flight. For cross-country flights this is an essential tool that will help the pilot stay
ahead of the game and be constantly aware of his position and his options in case of emergency. Offi-
cially filing the flight plan, calling a Flight Service Station (FSS) and dictating the plan on the phone to
a controller, or submitting it via the internet, gives additional advantages. Once the FSS (and ultimately
the FAA) knows where, when and along which route you are going, they can keep an eye on you, so to
speak. They can track you on their radar (a service called flight following) and, as a minimum, if you
are flying too low for them to follow you, they can check that you actually reached your destination at
the estimated arrival time or within a reasonable period. If they don't hear from you or if there is no re-
cord of your arrival at the destination airport, they will immediately start a search operation. Especially
in extreme climates, over mountainous terrain and uninhabited areas, this prompt reaction could be
crucial to your life. When it comes to filing flight plans, most pilots have mixed feelings. It feels a bit
like when you were a teenager and had to let mom know where you were going to spend the evening;
you hate having to do it, although you understand that it is for your own good. Sharing information
with mom, I mean the FAA, requires a little effort, but it brings great benefits.
In embedded control, sharing files (information) with a PC can be of great benefit, but you have to
know the rules — that is, you need to know how PC file systems work.
Flight plan
In the previous lesson we developed a basic interface module (both software and hardware) to gain
access to an SD™/MMC card and support applications that require large amounts of data storage. A
283
Chapter 14
similar interface could be built for several other types of mass-storage media, but in this lesson we
will rather focus on the algorithms and data structures required to properly share information on the
mass-storage device with the most common PC operating systems (DOS, Windows®, and some Linux
distributions). In other words, we will develop a module for access to a standard file system known
commonly as FAT 16. The first FAT file system was created by Bill Gates and Marc McDonald in 1977
for managing disks in Microsoft Disk BASIC. It used techniques that had been available in file systems
many years prior and it continued to evolve in numerous versions over the last few decades to accom-
modate ever larger capacity mass-storage devices and new features. Among the many versions still in
use today, the FAT 12, FAT 16 and FAT32 are the most common ones. FAT 16 and FAT32, in particular,
are recognized by practically every PC operating system currently in use and the choice between the
two is mostly dictated by efficiency considerations and the capacity of the media. Ultimately, for most
Flash mass-storage devices of common use in consumer multimedia applications, FAT 16 is the file
system of choice.
The flight
The name FAT is an acronym that stands for file allocation table, which is also the name of one of the
most important data structures used in this file system. After all, a file system is just a method for stor-
ing and organizing computer files and the data they contain, to make it easy to find and access them.
Unfortunately, as often is the case in the history of personal computing, standards and technologies are
the fruit of constant evolutionary progress rather than original creation. For this reason, many of the
details of the FAT file system that we will reveal in the following sections can only be explained in the
context of a struggle to continue and maintain compatibility with an enormous mass of legacy tech-
nologies and software over many years.
Sectors and Clusters
Still, the basic ideas at the root of a FAT file system are quite simple. As we have seen in the previ-
ous lesson, most mass-storage devices follow a "tradition" derived from the hard-disk technology of
managing memory space in blocks of a fixed size of 512 bytes commonly referred to as "sectors." In a
FAT file system, a small number of these sectors are reserved and used as a sort of general index: the
file allocation table. The remaining (majority) of the sectors are available for proper data storage, but
instead of being handled individually, small groups of contiguous sectors are handled jointly to form
new, larger entities known as "clusters." Clusters can be as small as one single sector, or can commonly
be formed by as many as 64 sectors. It is the use of each cluster and its position that is tracked inside
the file allocation table. Therefore, clusters are the true smallest unit of memory allocation in a FAT file
system.
The simplified diagram shown in Figure 14-1 illustrates a hypothetical example of a FAT file system
formatted for 1022 clusters, each composed of 16 sectors. (Notice that the data area starts with clus-
ter number 2.) In this example each cluster would contain 8 kB of data and the total storage capacity
would be about 8 MB.
Note that, the larger clusters are, the fewer are required to manage the entire memory space and the
smaller the allocation table required, hence the higher efficiency of the file system. On the contrary, if
many small files are to be written, the larger the cluster size, the more space will be wasted. It is typi-
cally the responsibility of the operating system, when formatting a storage device for use with a FAT
file system, to decide the ideal cluster size to be used for an optimal balance.
284
File I/O
Sector
Reserved
Cluster 4:Sector
FAT -S
Cluster 4: Sector 1
Reserved
r
Cluster 2
Cluster 3
Data
space
<
ilusters)
Cluster4: Sector 15
Cluster 4
i
i
i
\
Cluster 1022
Cluster 1023
Figure 14-1 . Simplified example of a FAT file system layout.
The File Allocation Table (FAT)
In the FAT 16 file system, the file allocation table itself contains one 16-bit integer value for each and
every cluster. If the cluster is to be considered empty and available, the corresponding position in the
table will contain the value 0x0000. If a cluster is in use and it contains an entire file of data, its cor-
responding position in the table will contain the value OxFFFF. If a file is larger than the size of a single
cluster, a chain of clusters is formed. In the FAT each cluster position in order will contain the number
of the following cluster in the chain. The last cluster in the chain will have in the corresponding table
position the value OxFFFF. Additionally, certain unique values are used to mark reserved clusters
(0x0001) and bad clusters (0xFFF7). The fact that 0x0000 and 0x0001 have been assigned special
meanings is the fundamental reason for the convention of starting the data area with cluster number 2.
In the FAT, correspondingly, the first two 16-bit integers are reserved.
In Figure 14-2 you can see an example of the content of a FAT for the system presented in our previous
example. Clusters and 1 are reserved. Cluster 2 appears to contain some data, meaning that some or
all of the (16) sectors forming the cluster have been filled with data from a file whose size must have
been less than 8 kB.
Cluster 3 appears to be the first cluster in a chain of three that also includes cluster 4 and 5. All of clus-
ter 3 and 4 sectors and some or all of cluster 5 sectors must have been filled with data from a file whose
size (we can only assume so far) was more than 16 kB but less than 24 kB. All the following clusters
appear to be empty and available.
Notice that the size of a FAT itself is dictated by the total number of clusters multiplied by two (two
bytes per cluster) and can spread over multiple sectors. In our previous example a FAT of 1024 clusters
would have required 2048 bytes, or 4 sectors of 512 bytes each. Also, since the file allocation table
is perhaps the most critical structure in the entire FAT file system, multiple copies (typically two) are
maintained and allocated one after the other before the beginning of the data space.
285
Chapter 14
Cluster 0x0000
Cluster 0x0001
Cluster 0x0002
Cluster 0x0003
Cluster 0x0004
Cluster 0x0005
Cluster 0x0006
Cluster 0x1023
OxFFFF
0x0004
0x0005
OxFFFF
0x0000
0x0000
r Reserved
In use, single cluster
In use, pointing to next cluster
in chain
In use, pointing to next cluster
in chain
In use, last cluster in chain
Empty and available
Figure 14-2. Cluster chains in a File Allocation Table.
The Root Directory
The role of the FAT is to keep track of how and where data is allocated. It does not contain any infor-
mation about the nature of the file to which the data belongs. For that purpose there is another structure
called the root directory, whose sole purpose is to store file names, sizes, dates, times and a number of
other attributes. In a FAT 16 file system the root directory (or simply the root from now on) is allocated
in a fixed amount of space and a fixed position right between the FAT (second copy) and the first data
cluster.
Sector
Reserved
/
FAT1
and J
FAT2
Cluster 4:Sector
Cluster 4: Sector 1
Root Directory
r
Cluster 2
V
Cluster 3
Data
space
clusters)
Cluster4: Sector 15
Cluster 4
Cluster 1022
Cluster 1023
Figure 14-3. Example of a FAT file system layout.
286
File I/O
Since both position and size (number of sectors) are fixed, the maximum number of files (or directory
entries) in the root directory is limited and determined when formatting the media. Each sector al-
located to the root will allow for 16 file entries to be documented, where each entry will require a block
of 32 bytes as represented in Figure 14-4.
offset:
offset: 8
offset: 1 1
offset: 22
offset: 24
offset: 26
offset: 28
File Name
Extension
Attributes
Reserved
Time
Date
First Cluster
File Size
8 ASCII characters
3 ASCII characters
1 byte
1 word (16 bit)
1 word (16 bit)
1 word (16 bit)
1 long word (32 bit)
Figure 14-4. Basic Root Directory Entry structure.
The Name and Extension fields are the most obvious, if you are familiar with the older Microsoft oper-
ating systems using the 8:3 conventions (the two fields need only to be padded with spaces and the dot
can be discarded).
The Attributes field is composed of a group of flags with the meanings shown in Table 14-1:
Bit
Mask
Description
0x01
Read Only
1
0x02
Hidden
2
0x04
System
3
0x08
Volume Label
4
0x10
Subdirectory
5
0x20
Archive
Table 14-1 . File attributes in a directory entry.
The Time and Date fields (Tables 14-2 and 14-3) refer to the last time the file was modified and must
be encoded in a special format to compress all the information in just two 16-bit words.
287
Chapter 14
Bits
Description
15-11
Hours (0-23)
10-5
Minutes (0-59)
4-0
Seconds/2 (0-29)
Table 14-2. Time encoding in a directory entry field.
Bits
Description
15-9
Year(0 = 1980, 127 = 2107)
8-5
Month (1 = January, 12 = December)
4-0
Day (1-31)
Table 14-3. Date encoding in a directory entry field.
Notice how the date field encoding does not allow for the code 0x0000 to be interpreted as a valid date,
helping provide clues to the file system when the field is not used or corrupted.
The First Cluster field provides the fundamental link to the FAT. The 16-bit word it contains is nothing
but the number of the cluster (could be the only or the first in a chain) containing the file data.
Finally, the Size field, contains in a long integer (32-bit) the size in bytes of the file data.
Looking at the first character of the file name in a directory entry, we can also tell if the entry is cur-
rently in use, in which case an actual ASCII-printable character is present, or if the entry is empty, in
which case the first byte is a zero and we can also assume that the list of files is terminated as the file
system proceeds sequentially using all entries in order. There is a third possibility: when a file is re-
moved from the directory the first character is simply replaced by a special code (0xE5). This indicates
that the contents of the entry are no longer valid, and the entry can be reused for a new file at the next
opportunity. However, when browsing through the list, searching for a file, we should continue as more
active entries might follow.
The treasure hunt
There is much more to say to fully document the structure of a FAT 16 file system, but if you have fol-
lowed the introduction so far, you should now have a reasonable understanding of its core mechanisms
and should be ready to dive in for more detail, as we will soon start writing some code.
So far we have maintained a certain level of simplification by ignoring some fundamental questions such as:
Where do we learn about a storage device capacity?
How can we tell where the FAT is located?
How can we tell how many sectors are in each cluster?
How can we tell where the data space starts?
The answers to all those questions will be found soon by following a sequence of steps that somewhat
resemble a child's treasure hunt. We will start using the "sdmmc . c" module functions developed in the
previous lesson to initialize the I/Os with the initSD ( ) function first and check for the presence of the
card in the slot.
288
File I/O
II 0. init the I/Os
initSDO ;
// 1. check if the card is in the slot
if ( !detectSD() )
{
FError = FE_NOT_PRESENT;
return NULL;
}
We will proceed then to initialize the storage device with the initMedia ( ) function.
// 2. initialize the card
if ( initMedia () )
{
FError = FE_CANNOT_INIT;
return NULL;
}
We will also use the standard C libraries (stdlib.h) to allocate dynamically two data structures:
// 3. allocate space for a MEDIA structure
D = (MEDIA *) malloc( sizeof ( MEDIA));
if ( D == NULL) // report an error
{
FError = FE_MALLOC_FAILED;
return NULL;
}
// 4. allocate space for a temp sector buffer
buffer = (unsigned char *) malloc ( 512);
if ( buffer == NULL) // report an error
{
FError = FE_MALLOC_FAILED;
free ( D) ;
return NULL;
}
The first one, that will be fully revealed later, is a structure that we will call media and will be the place
where we will collect the answer to all the questions above (perhaps a more appropriate name would
have been treasure).
The second structure buffer is simply a 512-bytes array that will be used to retrieve sectors of data
during the hunt.
Notice that to allow the malloc ( ) function to successfully allocate memory, you will have to remem-
ber to reserve some RAM space for the Heap. Hint: follow the "Project Build" checklist to learn how to
reach and modify the linker settings of your project.
289
Chapter 14
Mostly for historical reasons, the first sector (address 0) of each mass storage device will contain what
is commonly known as a Master Boot Record (MBR).
Here is how we invoke the readSECTOR ( ) function for the first time to access the Master Boot Record:
// 5. get the Master Boot Record
if ( ! readSECTOR ( 0, buffer))
{
FError = FE_CANNOT_READ_MBR;
f ree ( D) ; f ree ( buffer);
return NULL;
}
A signature, consisting of a specific word value (0x55AA) present in the last word of the MBR sector,
will confirm that we have indeed read the correct data.
#define FO_SIGN OxlFE // MBR signature location (55,AA)
// 6. check if the MBR sector is valid
// verify the signature word
if (( buffer [ FO_SIGN] != 0x55) ||
( buffer [ FO_SIGN +1] != OxAA) )
{
FError = FE_INVALID_MBR;
f ree ( D) ; f ree ( buffer);
return NULL;
}
Once upon a time, this record used to contain actual code to be executed by a PC upon power-up. No
personal computer does this anymore, though, and certainly there is no use for that 8086 code for our
PIC24 applications. Most of the time, you will find the Master Boot Record sector to be empty, mostly
filled with zeros, except for one fixed position starting at offset OxIbe. This is where we will find what
is called a Partition Table, a table (with only four entries containing 16 bytes each), which has no use
on a relatively small memory card like our SD/MMC, but that is kept for compatibility reasons and is
identical to the hard-disk partition tables you might have used on your PC. (See Figure 14-5.)
In our applications it is safe to assume that the entire card will have been formatted in a single partition
and that this will be the first and only entry (16-byte block) in the table. Of those 16 bytes we will need
only a few to deduce the partition size (should include the entire card), the starting sector, and most
importantly the type of file system contained. A couple of macros will help us assemble the data from
the buffer into words and long words:
#define ReadW( a, f) * (unsigned *) (a+f )
#define ReadL ( a, f) * (unsigned long *) (a+f)
290
File I/O
Offset
1
2
3
4
5
6
7
8
9
A
E
c
D
E
F
Access ▼
00000000
00000010
00000020
00000030
00000040
00000050
00000060
00000070
00000080
00000090
OOOOOOAO
OOOOOOBO
OOOOOOCO
OOOOOODO
OOOOOOEO
OOOOOOFO
00000100
00000110
00000120
00000130
00000140
OOOOOISO
00000160
00000170
00000180
00000190
OOOOOIAO
OOOOOIBO
OOOOOICO
OOOOOIDO
OOOOOIEO
OOOOOIFO
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
35
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
06
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
08
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
D8
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
IJIJ
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
CI
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
Fl
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
IJIJ
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
OF
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
C9
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
OE
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
55
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
03
00
00
00
AA
5. . .0J&. . . .£
us
Figure 14-5. Hex dump of an MBR sector.
Also the following definitions will point us to the right offset in the MBR.
#define FO_FIRST_P OxlBE // offset of first partition table
#define FO_FIRST_TYPE 0xlC2 // offset of first partition type
#define FO_FIRST_SECT 0xlC6 // first sector of first partition offset
#define FO_FIRST_SIZE OxlCA // number of sectors in partition
// 7. read the number of sectors in partition
psize = ReadL( buffer, FO_FIRST_SIZE) ;
291
Chapter 14
II 8. check if the partition type is acceptable
i = buffer [ FO_FIRST_TYPE] ;
switch ( i)
{
case 0x04
case 0x06
case OxOE
// valid FAT16 options
break;
default :
FError = FE_PARTITION_TYPE;
f ree ( D) ; f ree ( buffer);
return NULL;
} // switch
For historical reasons, there are several codes that correspond to a FAT 16 file system that we will be
able to correctly decode, including 0x04, 0x06 and OxOE.
Next, we will need to extract the long word (32-bit) value found at offset fo_first_sect (0xlC6), in
the first partition table entry, to proceed in the treasure hunt.
// 9. get the first partition first sector -> Boot Record
firsts = ReadL( buffer, FO_FIRST_SECT) ;
It contains the address of the next sector that we need to read from the device.
// 10. get the sector loaded (boot record)
if ( !readSECTOR( firsts, buffer))
{
f ree ( D) ; f ree ( buffer);
return NULL;
}
It has a signature, similar to the Master Boot Record, located in the last word of the sector, and we need
to verify it before proceeding.
// 11. check if the boot record is valid
// verify the signature word
if (( buffer [ FO_SIGN] != 0x55) ||
( buffer [ FO_SIGN +1] 1= OxAA) )
{
FError = FE_INVALID_BR;
f ree ( D) ; f ree ( buffer);
return NULL;
}
It is called the (First Partition) Boot Record, and once more it is supposed to contain actual executable
code that is of no value to us. (See Figure 14-6.)
292
File I/O
Offset
1
2
3
4
5
6
7
8
9
A
B
c
D
E
F
Access ▼
0001E200
EB
00
90
20
20
20
20
20
20
20
20
00
02
20
01
00
e i
0001E210
02
00
02
00
00
F8
77
00
3F
00
10
00
Fl
00
00
00
..... 0W .? ... n .
0001E220
OF
C9
0E
00
80
00
29
13
18
FD
EO
20
20
20
20
20
.£.
1 ) ■ ya
0001E230
20
20
20
20
20
20
46
41
54
31
36
20
20
20
00
00
FAT16
0001E240
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
■ * ■ « 1 • i f • i
0001E250
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
- A - - + * -
* .
0001E260
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
• •••»••
QQ01E270
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
. ,
0001E280
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0001E290
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0001E2AO
00
00
00
00
00
00
00
LIU
00
00
00
00
00
00
00
00
0001E2B0
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0001E2C0
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
«....«.
0001E2D0
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
■ »**»••
0001E2E0
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0001E2F0
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
.,..,*.
0001E300
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0001E310
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
.*,..♦.
Q001E320
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
QQ01E330
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0001E340
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0001E350
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0001E360
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
..... r .
0001E370
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0001E380
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
■ •*■»«■
0001E390
00
00
00
00
GO
00
00
00
00
00
00
00
00
00
00
00
■ »««»*•
0001E3A0
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
• 1 .... .
0001E3B0
00
00
00
00
00
00
00
00
i] o
00
00
00
00
00
00
00
0001E3C0
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
■ »«••«•
0001E3D0
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0001E3EO
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
■ •«■•.*■
0001E3F0
00
00
00
00
00
00
00
00
00
00
00
00
00
00
55
AA
. .u*
Figure 14-6. Hex dump of a Boot Record.
Fortunately, in the same record at fixed and known positions, there are some more of the answers we
were looking for and other elements that will help us calculate the rest and complete the map of the
entire FAT 16 file system. These are the key offsets in the Boot Record buffer:
// Partition Boot Record key fields offsets
# define BR_SXC Oxd
# define BR_RES Oxe
#define BR_FAT_SIZE 0x16
#define BR_FAT_CPY 0x10
#define BR MAX ROOT 0x11
// (byte) number of secotrs per cluster
// (word) number of reserved sectors for the boot record
// (word) FAT size in number of sectors
// (byte) number of FAT copies
// (odd word) max number of entries in root dir
And with the following code we can calculate the size of a cluster:
// 12. determine the size of a cluster
D->sxc = buffer [ BR_SXC] ;
// this will also act as flag that the media is mounted
293
Chapter 14
determine the position of the FAT, its size and the number of copies:
// 13. determine fat, root and data LBAs
// FAT = first sector in partition (boot record) + reserved records
D->fat = firsts + ReadW ( buffer, BR_RES) ;
D->fatsize = ReadW ( buffer, BR_FAT_SIZE) ;
D->fatcopy = buffer [ BR_FAT_CPY] ;
and find the position of the Root Directory too:
// 14. ROOT = FAT + (sectors per FAT * copies of FAT)
D->root = D->fat + ( D->fatsize * D->fatcopy) ;
Careful now, as we get ready to grab the last few pieces of gold — watch out for a trap!
// 15. MAX ROOT is the maximum number of entries in the root directory
D->maxroot = ReadW ( buffer, BR_MAX_ROOT) ;
Can you see it? No? OK, here is a hint. Look at the value of the br_max_root offset as defined a few
lines above. You will notice that this is an odd address (Oxll). This is all it takes for the Readw ( )
macro, which attempts to use it as a word address, to throw a processor trap and reset the PIC24!
We need a special macro (perhaps less efficient) but one that can assemble a word one byte at a time
without falling into the trap !
// these is the safe versions of ReadW to be used on odd address fields
#define ReadOddW( a, f) (*(a+f) + ( *(a+f+l) « 8))
// 15. MAX ROOT is the maximum number of entries in the root directory
D->maxroot = ReadOddW( buffer, BR_MAX_R00T) ;
The last two pieces of information are easy to grab now. With them, we learn where the data area (di-
vided in clusters) begins and how many clusters are available to our application:
// 16. DATA = ROOT + (MAXIMUM ROOT *32 / 512)
D->data = D->root + ( D->maxroot >> 4) ; // assuming maxroot % 16 == 0! ! !
// 17. max clusters in this partition = (tot sectors - sys sectors ) /sxc
D->maxcls = (psize - (D->data - firsts) ) / D->sxc;
It took us as many as 17 careful steps to get to the treasure, but now we have all the information needed
to fully figure out the layout of the FAT 16 file system present on the SD/MMC memory card (or for
that reason almost any other mass-storage media). The treasure, then, is nothing more than another
map, a map we will need next to find the files on the mass-storage device. (See Figure 14-7.)
294
File I/O
I can now reveal to you the definition of the entire media structure that we allocated on the heap at the
very beginning and we have been so patiently filling. Here is where we will keep the treasure.
typedef struct {
LBA fat ;
LBA root;
LBA data;
unsigned maxroot;
unsigned maxcls;
unsigned fatsize;
unsigned char fatcopy;
unsigned char sxc;
} MEDIA;
// lba of FAT
// lba of root directory
// lba of the data area
// max number of entries in root dir
// max number of clusters in partition
// number of sectors
// number of FAT copies
// number of sectors per cluster
We can now assemble all the steps into one essential function that we can call mount
ity to a function available in the Unix family of operating systems.
for its similar-
Sector 0= MBR
maxcls
Boot Record ►
fat — p>
fatsize i
fatcopy ►
root ►
data ►
Root Directory
Cluster 2
Cluster 3
Cluster 4
Cluster maxcls
p
Cluster 4:Sector
Cluster 4: Sector 1
Cluster 4: Sector sxc
sxc
Figure 14-7. We found the treasure — the FAT 16 complete layout.
For a mass-storage device to be used in Unix, the file system present on the device must be "mounted,"
or in other words attached as a new branch of the main (system) file system. Windows users might
not be familiar with the concept as they don't have the option to choose if, when, or where the new
device file system will be mounted. All new mass-storage devices are automatically and uncondition-
ally mounted by Windows at power-up, or after insertion for removable media, at the very root of the
Windows file system by assigning them a single letter identifier ("C:", "D:", "E:", etc.).
295
Chapter 14
, ,
// mount initializes a MEDIA structure for FILEIO access
//
MEDIA * mount ( void)
{
LBA psize; // number of sectors in partition
LBA firsts; // LBA of first sector inside the first partition
int i ;
unsigned char *buffer;
. . . insert here all 17 steps of our treasure hunt
// 18. free up the temporary buffer
free ( buffer) ;
return D;
} / / mount
We will also define a global pointer to a media structure (d) to be used to hold the pointer returned by
the mount ( ) function. It will serve as the starting point for the entire file system. Initially, we will as-
sume that only one storage device is available at any given point in time (one connector/slot, one card).
// global definitions
MEDIA *D;
We will also define an unmount ( ) function that will have the sole duty of releasing the space allocated
for the media structure.
//
// unmount releases the space allocated for the MEDIA structure
//
void unmount ( void)
{
free( D) ;
D = NULL;
} / / unmount
Opening a file
Now that we have figured out the map of the storage device, we can start pursuing our original objec-
tive: accessing individual files. In practice, what we will develop in the following parts of this lesson is
a set of high-level functions similar to those found in most operating systems for file manipulation. We
will need a function to find a file location on the storage device, one for sequentially reading the data
from the file and possibly one more to write data and create new files.
In a logical order, we will start developing what we will call the f openM ( ) function. Its role will be
that of finding all possible information regarding a file (if present) and gathering it in a new structure
that we will call mfile. The name of this structure was chosen so as to avoid conflicts with similar
structures and functions defined inside the standard C library "stdio.h".
296
typedef struct {
MEDIA * mda;
unsigned char * buffer;
unsigned cluster;
unsigned eels;
unsigned sec;
unsigned pos;
unsigned top;
long seek;
long size;
unsigned time;
unsigned date;
char name [11] ;
char chk;
unsigned entry;
char mode ;
} MFILE;
File I/O
1/ MEDIA structure
// sector buffer
// first cluster
// current cluster in file
// sector in current cluster
// position in current sector
// number of data bytes in the buffer
// position in the file
// file size
// last update time
// last update date
// file name
// MFILE structure checksum = ~( entry + name[0
// entry position in cur directory
/ / mode x r ' , v w'
I know at first sight it looks like a lot — it is more than 40 bytes large — but as you will see in the rest of
the lesson, we will end up needing all of them. You will have to trust me for now.
Mimicking standard C library implementations (common to many operating systems) the fopenM( )
function will receive two (ASCII) string parameters: the file name and a "mode" string, containing "r"
or "w", that will indicate if the file is supposed to be opened for reading or writing.
MFILE *fopenM( const char ^filename, const char *mode)
{
char c ;
int i , r ;
unsigned char *b;
MFILE *fp;
MEDIA *mda=D;
// newly allocated buffer
// pointer to newly allocated MFILE structure
// pointer to MEDIA structure
To optimize memory usage an mfile structure is allocated only when necessary; it is in fact one of the
first tasks of the f openM ( ) function, and a pointer to the data structure is its return value. In case f o-
penM ( ) should fail, a NULL pointer will act as an error report.
Of course, a prerequisite for opening a file is to have the storage device file system mapped out, and
that is supposed to have already been performed by the mount ( ) function. A pointer to a media struc-
ture must have already been deposited in the global d pointer.
// 1. check if a storage device is mounted
if ( D == NULL) // unmounted
{
FError = FE_MEDIA_NOT_MNTD;
return NULL;
}
297
Chapter 14
Since all activity with the storage device must be performed in blocks of 512 bytes, we will need that
much space to be allocated for it to act as a read/write buffer.
// 2. allocate a buffer for the file
b = (unsigned char* )malloc ( 512);
if ( b == NULL)
{
FError = FE_MALLOC_FAILED;
return NULL;
}
Only if that amount of memory is available can we proceed and allocate some more memory for the
mfile structure proper.
// 3. allocate a MFILE structure on the heap
fp = (MFILE *) malloc( sizeof( MFILE));
if ( fp == NULL) // report an error
{
FError = FE_MALLOC_FAILED;
free ( b) ;
return NULL;
}
The buffer pointer and the media pointers can now be recorded inside the mfile data structure.
// 4. set pointers to the MEDIA structure and buffer
fp->mda = D;
fp->buffer = b;
The file name parameter must be extracted; each character must be translated to upper case (using the
standard C library functions defined in "ctype .h") and padded, if necessary, with spaces to an eight-
character length.
// 5. format the filename into name
for( i=0; i<8; i++)
{
c = toupper ( *filename++) ; // read a char and convert to upper case
if (( c == *.') ( c == N \0'))// extension or short name noextension
break;
else
fp->name[i] = c ;
} // for
// if short fill the rest up to 8 with spaces
while ( i<8) fp->name [i++] = * x ;
298
File I/O
Similarly, after discarding the dot, an extension of up to three characters must be formatted and
padded.
// 6. if there is an extension
if ( c != »\0' )
{
for( i=8; i<ll; i++)
{
c = to-upper ( *fLlename++) ; // read char, convert to upper case
if ( c == * . ')
c = toupper ( *filename++) ;
if ( c == *\0') // short extension
break;
else
fp->name[i] = c;
} // for
// if short fill the rest up to 3 with spaces
while ( i<ll) fp->name [i++] = * v ;
} // if
While most C libraries provide extensive support for multiple "modes" of access to files, like dis-
tinguishing between text and binary files and offering an "append" option, we will accept (at least
initially) a subset consisting of two basic options only: "r" or "w".
// 7. copy the file mode character (r, w)
if ( ( *mode == x r' ) \ | ( *mode == x w' ) )
fp->mode = *mode;
else
{
FError = FE_INVALID_MODE;
goto ExitOpen;
}
With the file name properly formatted, we can now start searching the root directory of the storage
device for an entry of the same name.
// 8. Search for the file in current directory
if ( ( r = findDIR( fp) ) == FAIL)
{
FError = FE_FIND_ERROR;
goto ExitOpen;
}
Let's leave the details of the search out for now and let's trust the new findDiR ( ) function to return
to us one of three possible values: fail, not_found and eventually found. A possible failure must
always be taken into account. After all, before we consider the possibility of major fatal failures of
the storage device, there is always the possibility that the user simply removed the card from its slot
without our knowledge. If that is the case, as in all prior error cases, we have no business continuing
299
Chapter 14
in the process. We better immediately release the memory allocated thus far and return with a NULL
pointer after leaving an error code in the dedicated "mail box" FError, just like we did during the
mount process.
But if the search for the file is completed without errors (whether it was found or not), we can continue
initializing the mfile structure.
// 9. init all counters to the beginning of the file
fp->seek =0; // first byte in file
fp->sec =0; // first sector in the cluster
fp->pos =0; // first byte in sector /cluster
The counter seek will be used to keep track of our position inside the file as we will access its contents
sequentially. Its value will be a long integer (unsigned long) between and the size of the entire file
expressed in bytes. The sec field will keep track of which sector (inside the current cluster) we are cur-
rently operating on. Its value will be an integer between and sxc-1, the number of sectors composing
each data cluster. Finally, pos will keep track of which byte (inside the current buffer) we are going to
access next. Its value will be an integer between and 511.
'&'
// 10. depending on the mode (read or write)
if ( fp->mode == *r')
{
At this point, different things need to be done depending on whether an existing file needs to be opened
for reading or a new file needs to be created for writing. Initially we will complete all the necessary
steps for the f openM ( ) function when invoked in the read ("r") mode, in which case the file better be
found.
// 10.1 % r' open for reading
if ( r == NOT_FOUND)
{
FError = FE_FILE_NOT_FOUND;
goto ExitOpen;
}
If it was indeed found, the findDiR ( ) function will have filled for us a couple more fields of the mfile
structure, including:
entry : indicating the position in the root directory where the file was found
cluster: indicating the number of the first data cluster used to store the file data as retrieved
from the directory entry
• size: indicating the number of bytes composing the entire file
• time and date of creation
the file attributes
300
File I/O
The first cluster number will become our current cluster: eels.
else
{ // found
// 10.2 set the current cluster pointer on the first file cluster
fp->ccls = fp->cluster;
We now have all the information required to identify the first sector of data into the buffer. The func-
tion readDATA ( ) (that we will describe in detail shortly) will perform the simple calculation required
to convert the eels and sec values into an absolute sector number inside the data area and will use the
low-level readSECTOR ( ) function to retrieve the data from the storage device.
// 10.3 read a sector of data from the file
if ( ! readDATA ( fp) )
{
goto ExitOpen;
}
Notice that the file length is not constrained to be a multiple of a sector size. So it is perfectly possible
that only a part of the data retrieved in the buffer belongs to the actual file. The mfile structure field
top will help us keep track of where the actual file data ends and possibly padding begins.
// 10.4 determine how much data is really inside the buffer
if ( fp->size-fp->seek < 512)
fp->top = fp->size - fp->seek;
else
fp->top = 512;
} // found
} // 'r'
As this is all we really need to complete the f openM ( ) function (when opening a file for reading), we
could return now with the precious pointer to the mfile structure. As an additional safety measure,
though, to help flag possible future mistakes related to the use and reuse of pointers, we will compute
a simple checksum field that will result correctly only if the entire open function was completed
successfully.
// 12. compute the MFILE structure checksum
fp->chk = ~( fp->entry + fp->name [0 ] ) ;
return fp;
Note: Shortly we will be inserting some more code before this point, so don't worry for now about
the numbering sequence.
301
Chapter 14
In case any of the previous steps failed, we will exit the function, returning a null pointer after having
released both the memory allocated for the sector buffer and the mfile structure.
// 13. Exit with error
ExitOpen:
f ree ( fp->buffer) ;
free( fp) ;
return NULL;
} // fopenM
In a top-down fashion, we can now complete the two accessory functions used during the development
of fopenM ( ) , Starting with readDATA ( ) :
unsigned readDATA ( MFILE *fp)
{
LBA 1;
// calculate lba of cluster/sector
1 = fp->mda->data + (LBA) (fp->ccls-2) * fp->mda->sxc + fp->sec;
return( readSECTOR ( 1, fp->buf fer) )
} / / readDATA
Notice how we need data and sxc from the media structure to compute the correct sector number.
Very simple!
Similarly, we can create a function to read from the root directory a block of data containing a given
entry.
unsigned readDIR( MFILE *fp, unsigned e)
// loads current entry sector in file buffer
// returns FAIL/TRUE
{
LBA 1;
// load the root sector containing the DIR entry "e"
1 = fp->mda->root + (e >> 4);
return ( readSECTOR( 1, fp->buf fer) ) ;
} // readDIR
We know that each directory entry is 32 bytes long; therefore each sector will contain 16 entries.
302
File I/O
The findDiR ( ) function can now be quickly coded as a short sequence of steps enclosed in a search
loop through all the available entries in the root directory.
unsigned findDIR( MFILE *fp)
// fp file structure
// return f ound/not_f ound/f ail
(
unsigned eCount; // current entry counter
unsigned e; // current entry offset in buffer
int i, a, c, d;
MEDIA *mda = fp->mda;
// 1. start from the first entry
eCount = ;
// load the first sector of root
if ( !readDIR( fp, eCount))
return FAIL;
We start loading the first root sector, containing the first 16 entries, in the buffer. For each entry we
compute its offset inside the buffer:
// 2. loop until you reach the end or find the file
while ( 1)
{
// 2.0 determine the offset in current buffer
e = (eCount & Oxf) * DIR_ESIZE;
and we inspect the first character of the entry file name:
// 2.1 read the first char of the file name
a = fp->buffer[ e + DIR_NAME] ;
If its value is 0, indicating an empty entry and the end of the list, we can immediately exit reporting the
file name was not found.
// 2.2 terminate if it is empty (end of the list)
if ( a == DIR_EMPTY)
{
return NOT_FOUND;
} // empty entry
The other possibility is that the entry was marked as deleted, in which case we will skip it.
// 2.3 skip erased entries if looking for a match
if ( a != DIR_DEL)
{
303
Chapter 14
Otherwise, it's a valid healthy entry, and we should check the attributes to determine if it corresponds
to a proper file or any other type of object. The possibilities include: subdirectories, volume labels and
long file names. None of them is of our concern, as we will choose to keep things simple and we will
steer clear of the most advanced and sometimes patented features of the more recent versions of the
FAT file system standard.
// 2.3.1 if not VOLume or DIR compare the names
a = fp->buffer[ e + DIR_ATTRIB] ;
if ( ! ( (a & ATT_DIR) | | ( a & ATT_VOL) ) )
{
We will then compare the file names character by character, looking for a complete match.
// compare file name and extension
for (i=DIR_NAME; i<DIR_ATTRIB; i++)
{
if ( ( fp->buffer[ e + i] ) != ( fp->name [i] ) )
break; // difference found
}
Only if every character matches, we will extract the essential pieces of information from the entry and
we will copy them into the mfile structure, returning a found code.
if ( i == DIR_ATTRIB)
{
// entry found, fill the mfile structure
fp->entry = eCount; // store entry index
fp->time = ReadW( fp->buffer, e + DIR_TIME)
fp->date = ReadW( fp->buffer, e + DIR_DATE)
fp->size = ReadL ( fp->buffer, e + DIR_SIZE)
fp->cluster = ReadL ( fp->buffer, e + DIR_CLST) ;
return FOUND;
}
} // not a dir nor a vol
} // not deleted
Should the file name and extension differ, we will simply continue our search with the next entry,
remembering to load the next sector from the root directory after each group of 16 entries.
// 2.4 get the next entry
eCount++ ;
if ( eCount & Oxf == 0)
{ // load a new sector from the Dir
if ( !readDIR( fp, eCount))
return FAIL;
}
304
File I/O
We know the maximum number of entries in the root directory (maxroot) and we need to terminate
our search if we reach the end of the directory without a match indicating not_found.
// 2.5. exit the loop if reached the end or error
if ( eCount >= mda->maxroot )
return NOT_FOUND; // last entry reached
}// while
} // findDIR
Reading data from a file
Finally, this is the moment we have waited for so long. The file system is mounted, a file is found and
opened for reading, and it is time to develop the f readM ( ) function, to read freely blocks of data from it.
unsigned f readM ( void * dest, unsigned size, MFILE *fp)
// fp pointer to MFILE structure
// dest pointer to destination buffer
// count number of bytes to transfer
// returns number of bytes actually transferred
{
MEDIA * mda = fp->mda; // media structure
unsigned count=size; // counts the number of bytes to be transferred
unsigned len;
The name, number and sequence of parameters passed to this function are again supposed to mimic
closely those of similarly named functions available in the standard C libraries.
A destination buffer is supplied where the data read from the file will be copied, and a number of bytes
is requested while passing the usual pointer to an open mfile structure.
The f readM ( ) function will do its best to read as many of the bytes requested as possible from the
file, and will return an unsigned integer value to report how many it effectively managed to get. In our
simple implementation, if the number returned is not identical to what was requested by the calling ap-
plication, we will have to assume that something major has happened. The end of file has been reached,
most probably, but we will not make a distinction if, instead, another type of failure has occurred — for
example, the card has been removed during the process.
As usual, we will not trust the pointer passed in the argument and we will check instead if it is pointing
to a valid mfile structure by recalculating and comparing the simple checksum performed by the open
function when successfully opening a file.
// 1. check if fp points to a valid open file structure
if (( fp->entry + fp->name[0] != ~fp->chk ) ( fp->mode != x r'))
{ // checksum fails or not open in read mode
FError = FE_INVALID_FILE;
return size-count;
}
305
Chapter 14
Only then we will enter a loop to start transferring the data from the sector data buffer.
// 2. loop to transfer the data
while ( count>0)
{
Inside the loop, the first condition to check will be our current position, with regard to the total file size.
// 2.1 check if EOF reached
if ( fp->seek >= fp->size)
{
FError = FE_EOF; // reached the end
break;
}
Notice that this error will be generated only if the application calling the f readM ( ) function will
ignore the previous symptom: if the last f readM ( ) call returned with a number of data bytes inferior to
what was requested, or if the calling application has requested the exact number of bytes available in
the file with the previous calls.
Otherwise we will verify if the current buffer of data has already been used up completely.
// 2.2 load a new sector if necessary
if (fp->pos == fp->top)
{
If necessary, we will reset our buffer pointers and attempt to load the next sector from the file:
fp->pos = 0;
fp->sec++ ;
If we had already used up all the sectors in the current cluster, this might force us to step into the next
cluster by peeking inside the FAT and following the chain of clusters:
// 2.2.1 get a new cluster if necessary
if ( fp->sec == mda->sxc)
(
fp->sec = 0;
if ( !nextFAT( fp, 1) )
{
break;
}
306
File I/O
In either case, we load the new sector of data in the buffer, paying attention to verify the possibility that
it might be the last one of the file and it might be only partially filled:
// 2.2.2 load a sector of data
if ( !readDATA( fp) )
{
break;
}
// 2.2.3 determine how much data is really inside the buffer
if ( fp->size-fp->seek < 512)
fp->top = fp->size - fp->seek;
else
fp->top = 512;
} // load new sector
Now that we know we have data in the buffer, ready to be transferred, we can determine how much of
it we can transfer in a single chunk:
// 2.3 copy as many bytes as possible in a single chunk
// take as much as fits in the current sector
if ( fp->pos+count < fp->top)
len = count; // fits all in current sector
else
len = fp->top - fp->pos; // take a first chunk, there is more
memcpy ( dest, fp->buffer + fp->pos, len);
Using the memcpy ( ) function from the standard C libraries (string . h) to move a block of data from
the file buffer to the destination buffer, we get the best performance, as these routines are optimized for
speed of execution. The pointers and counters can be updated and the loop can be repeated until all the
data requested has been transferred.
// 2.4 update all counters and pointers
count-= len; // compute what is left
dest += len; // advance destination pointer
fp->pos += len; // advance the pointer in current sector
fp->seek += len; // advance the seek pointer
} // while count
Finally, we can exit the function and return the number of actual bytes transferred in the loop:
// 3. return number of bytes actually transferred
return size-count;
} // freadM
307
Chapter 14
Closing a file
Since we can only open a file for reading (with the f openM ( ) function as defined so far), there is not
much work to perform upon closing the file. We can consider invalidating the checksum created by
f openM ( ) but we must remember to free all the memory allocated for the mfile structure and the
sector buffer.
unsigned fcloseM( MFILE *fp)
{
// 1. invalidate the file structure
fp->chk = fp->entry + fp->name [ ] ; // set checksum invalid!
// 2. free up the buffer and the MFILE struct
f ree ( fp->buffer) ;
free( fp) ;
} // fcloseM
Creating the fileio module
We can create a small library module by saving all the functions written so far in a file called
"fileio . c " . We will need to add the usual header with a few include files:
/*
** FILE I/O interface
* *
** module: fileio . c
• *
*/
// standard C libraries used
#include <stdlib.h> // NULL, malloc, free...
#include <ctype.h> // toupper...
#include <string.h> // memcpy. . .
#include "sdmmc.h" // sd/mmc card interface
#include "fileio. h" // file I/O routines
And of course, we will need to create the "fileio . h" file too, with all the definitions and prototypes
that we wish to publish for future applications to use.
/*
** FILE I/O interface
** FAT16 support
* *
** module: fileio. h
*/
308
extern char FError; // mailbox for error reporting
File I/O
II FILEIO
ERROR CODES
# define
FE_
_IDE_ERROR
1
# define
FE_
_NOT_PRESENT
2
# define
FE_
_PARTITION_TYPE
3
# define
FE_
_INVALID_MBR
4
# define
FE_
_INVALID_BR
5
# define
FE_
_MEDIA_NOT_MNTD
6
# define
FE_
_FILE_NOT_FOUND
7
# define
FE_
_INVALID_FILE
8
# define
FE_
_FAT_EOF
9
# define
FE_
_EOF
10
# define
FE_
_INVALID_CLUSTER
11
# define
FE_
_DIR_FULL
12
# define
FE_
_MEDIA_FULL
13
# define
FE_
_FILE_OVERWRITE
14
# define
FE_
_CANNOT_INIT
15
# define
FE_
_CANNOT_READ_MBR
16
# define
FE_
_MALLOC_FAILED
17
# define
FE_
_INVALID_MODE
18
# define
FE_
_FIND ERROR
19
// IDE command execution error
// CARD not present
// WRONG partition type, not FAT12
// MBR sector invalid signature
// Boot Record invalid signature
// Media not mounted
// File not found in open for read
// File not open
// Fat attempt to read beyond EOF
// Reached the end of file
// Invalid cluster value > maxcls
// All root dir entry are taken
// All clusters in partition are taken
//A file with same name exists already
// Cannot init the CARD
// Cannot read the MBR
// Malloc could not allocate the MFILE struct
// Mode was not r.w.
// Failure during FILE search
typedef struct {
LBA fat;
LBA root;
LBA data;
unsigned maxroot;
unsigned maxcls;
unsigned fatsize;
unsigned char fatcopy;
unsigned char sxc;
} MEDIA;
// lba of FAT
// lba of root directory
// lba of the data area
// max number of entries in root dir
// max number of clusters in partition
// number of sectors
// number of copies
// number of sectors per cluster (!=0 flags media mounted)
typedef struct {
MEDIA * mda;
unsigned char * buffer;
unsigned cluster;
unsigned eels;
unsigned sec;
unsigned pos;
unsigned top;
long seek;
long size;
unsigned time;
// media structure pointer
// sector buffer
// first cluster
// current cluster in file
// sector in current cluster
// position in current sector
// number of data bytes in the buffer
// position in the file
// file size
// last update time
309
Chapter 14
unsigned date;
char name [11] ;
char chk;
unsigned entry;
char mode ;
} MFILE;
// last update date
// file name
/ / checksum = ~ ( entry + name [ ] )
// entry position in cur directory
// mode v r' , v w' , v a'
// file attributes
# define
ATT_RO
1
# define
ATT_HIDE
2
# define
ATT_SYS
4
# define
ATT_VOL
8
# define
ATT_DIR
0x10
# define
ATT_ARC
0x2
# define
ATT_LFN
OxOf
# define
FOUND
2
# define
NOT FOUND
1
// attribute read only
// attribute hidden
// w system file
// " volume label
// " sub-directory
// " (to) archive
// mask for Long File Name records
// directory entry match
// directory entry not found
// macros to extract words and longs from a byte array
// watch out, a processor trap will be generated if the address is not word
aligned
#define ReadW( a, f) * (unsigned *)(a+f)
#define ReadL ( a, f) * (unsigned long *) (a+f)
// this is a "safe" version of ReadW to be used on odd address fields
#define ReadOddW( a, f) (*(a+f) + ( * (a+f +1) << 8))
// prototypes
unsigned nextFAT ( MFILE * fp, unsigned n) ;
unsigned newFAT ( MFILE * fp) ;
unsigned readDIR( MFILE *fp, unsigned entry);
unsigned findDIR( MFILE *fp) ;
unsigned newDIR ( MFILE *fp) ;
MEDIA * mount ( void);
void unmount ( void) ;
MFILE * fopenM
unsigned freadM
unsigned fwriteM
unsigned fcloseM
( const char *name, const char *mode) ;
( void * dest, unsigned count, MFILE *) ;
( void * src, unsigned count, MFILE *) ;
( MFILE *fp) ;
310
File I/O
Don't worry now if we have not yet fleshed out all the functions; we will continue working on them as
we proceed through the rest of the lesson.
Testing fopenMO and f read MO
It might seem like a long time since we built a project for the last time. To verify the code that we have
developed so far, we had to reach a critical mass, a minimal core of routines without which no applica-
tion could have worked. Now that we have this core functionality, we can develop for the first time a
small test program to read a file from an SD/MMC card that was formatted with the FAT 16 file system.
The idea is to copy a text file (any text file would work) onto the SD/MMC card from your PC, and
then have the PIC24, with the new "fileio . c" module, read the file and send its content to the serial
port back to the PC (running Hyper terminal or any other terminal or printer available with an RS232
serial port).
This is the main module that you will save as "ReadTest .c".
/*
* *
* *
*/
ReadTest . c
#include <p24f j 128ga010 .h>
# include "SDMMC.h"
#include "nleio.h"
#include ".. /delay/delay .h"
#include * . . /3 comm/conu2.h"
#define B_SIZE 10
char data[ B_SIZE] ;
main ( void)
{
MFILE *fs;
unsigned i, r;
//initializations
initU2 ( ) ;
//115,200 baud 8,n,l
putsU2 ( "init") ;
while ( IdetectSD ( ) ) ; // assumes SDCD pin is by default an input
Delayms ( 100) ; // wait for card to power up
putsU2 ( "media detected" ) ;
311
Chapter 14
if ( mount ( ) )
{
putsU2 ( "mount") ;
if ( fs = fopenM( "name.txt", "r'))
{
putsU2 ( "file opened" ) ;
do{
r = freadM( data, B_SIZE, fs) ;
f or ( i=0; i<r; i++)
putU2 ( data[i] ) ;
} while ( r==B_SIZE) ;
fcloseM( fs) ;
putsU2("file closed");
}
else
putsU2 ( "could not open file");
unmount ( ) ;
putsU2 ( "media unmounted" ) ;
}
// main loop
while ( 1 ) ;
} // main
We will use the serial communication module "conu2 . c" developed in one of the early lessons and a
delay module that will provide a delayms ( ) function similar, to the one we used to test the "sdmmc .c"
module in the previous lesson. The sequence of operation is also similar, only this time instead of call-
ing the initMedia ( ) function and then reading and writing directly to sectors of the SD/MMC card,
we will call the mount ( ) function to access the FAT 16 file system on the card. We will open the data
file using its "proper" name, and we will read data from it in blocks of arbitrary length (b_size) and
we will send its contents to the serial port of the Explorer 16 board.
Once we have exhausted the content of the entire file, we will close the file, deallocating all the
memory used.
After creating a new project we will need to add all the necessary modules to the project window
including:
"sdmmc . c"
"fileio.c"
"conu2 . c"
• "delay. c"
• "readtest.c"
and all the corresponding include files ( .h).
312
File I/O
Remember to follow the checklists for a new project and for the ICD2 debugger, so that you will
remember to set the ICD2 option for the linker; in the same configuration dialog box, remember to add
some space for the heap so that we will be able to allocate memory dynamically for the file system
structures and buffers (even if 580 bytes should suffice, give the heap ample room to maneuver).
After building the project and programming the executable on the Explorer 16 board, we are ready to
run the test.
If all goes well, you will be able to see the contents of the text file scrolling on the screen of your ter-
minal of choice, probably too fast for you to read it, except for the last part.
Notice that you can recompile the project and run the test with different sizes for the data buffer from 1
byte to as large as the memory of the PIC24 will allow. The f readM ( ) function will take care of read-
ing as many sectors of data required to fulfill your request as long as there is data in the file.
Writing data to a file
We are far from finished. The "fileio .c" module is not complete until we include the ability to create
new files. This requires us to create an fwriteM ( ) function but also to complete a piece of the f o-
penM ( ) function. So far, in fact, we had f openM ( ) return with an error code when a file could not be
found in the root directory or the mode was not "r". But this is exactly what we want when we open a
new file for writing. When we check for the mode parameter value, we now need to add a new op-
tion. This time, it is when the file is not_found during the first scan of the directory that we want to
proceed.
else // 11. open for 'write'
{
if ( r == NOT_FOUND)
{
A new file needs a new cluster to be allocated to contain its data. The function newFAT ( ) will be used
to search in the FAT for an available spot, a cluster that is still marked (with 0x0000) as available. This
search could fail and the function could return an error that, among other things, could indicate that the
storage device is full and all data clusters are taken. Should the search be successful, though, we will
take note of the new cluster position and update the mfile structure, making it the first cluster of our
new file.
// 11.1 allocate a first cluster to it
fp->ccls = 0; // indicate brand new file
if ( newFAT( fp) == FAIL)
{ // must be media full
FError = FE_MEDIA_FULL;
goto ExitOpen;
}
fp->cluster = fp->ccls;
Next, we need to find an available entry space in the directory for the new file. This will require a sec-
ond pass through the root directory, this time looking for the first entry that is either marked as deleted
(code 0xE5) or for the end of the list where an empty entry is found (marked with the code 0x00).
313
Chapter 14
II 11.2 create a new entry
// search again, for an empty entry this time
if ( (r = newDIR( fp) ) == FAIL) // report any error
{
FError = FE_IDE_ERROR;
goto ExitOpen;
}
The function newDiR ( ) will take care of finding an available entry and, similarly to the findDiR ( )
function used before, will return one of three possible codes:
fail, indicating a major problem occurred (or the card was removed)
• not_found, indicating the root directory must be full
• found, indicating an available entry has been identified
// 11.3 new entry not found
if ( r == NOT_FOUND)
{
FError = FE_DIR_FULL;
goto ExitOpen;
}
In both the first two cases we have to report an error and we cannot continue. But if an entry is found,
we have plenty of work to do to initialize it.
After calculating the offset of the entry in the current buffer, we will start filling some of its fields with
data from the mfile structure. The file size will be first:
else // 11.4 new entry identified fp->entry filled
{
// 11.4.1 init file size
fp->size = ;
// 11.4.2 determine offset in DIR sector
e = (fp->entry & Oxf) * DIR_ESIZE; // 16 entry per sector
// 11.4.3 set initial file size to
fp->buffer[ e + DIR_SIZE] =
fp->buffer[ e + DIR_SIZE+1]=
fp->buffer[ e + DIR_SIZE+2]=
fp->buffer[ e + DIR_SIZE+3]=
The time and date fields could be derived from the RTCC module registers or any other timekeeping
mechanism available to the application, but a default value will be supplied here only for demonstra-
tion purposes.
fp->date = 0x34FE; // July 30th, 2006
fp->buffer[ e + DIR_DATE] = fp->date;
314
File I/O
fp->buffer[ e + DIR_DATE+1]= fp->date>>8 ;
fp->buffer[ e + DIR_TIME] = fp->time;
fp->buffer[ e + DIR_TIME+1]= fp->time>>8 ;
The file's first cluster number, the file name and the attributes (defaults) will complete the directory
entry:
// 11.4.5 set first cluster
fp->buffer[ e + DIR_CLST] = fp->cluster ;
fp->buffer[ e + DIR_CLST+1] = ( fp->cluster>>8 ) ;
// 11.4.6 set name
for ( i = 0; i<DIR_ATTRIB; i++)
fp->buffer[ e + i] = fp->name[i];
// 11.4.7 set attrib
fp->buffer[ e + DIR_ATTRIB] = ATT_ARC;
// 11.4.8 update the directory sector;
if ( !writeDIR( fp, fp->entry) )
{
FError = FE_IDE_ERROR;
goto ExitOpen;
}
} // new entry
} // not found
Back to the results of our first search through the root directory — in case a file with the same name was
indeed found, we will need to report an error.
else // file exist already, report error
{
FError = FE_FILE_OVERWRITE;
goto ExitOpen
}
Alternatively, we would have had to delete the current entry first, release all the clusters used and then
start from the beginning. After all, reporting the problem as an error is an easier way out for now.
So much for the changes required to the f openM ( ) function. We can now start writing the proper new
fwriteM ( ) function, once more modeled after a similarly named standard C library function.
unsigned fwriteM ( void *src, unsigned count, MFILE * fp)
// src points to source data (buffer)
// count number of bytes to write
// returns number of bytes actually written
{
MEDIA *mda = fp->mda;
unsigned len, size = count;
315
Chapter 14
II 1. check if file is open
if ( fp->entry + fp->name[0] != ~fp->chk )
{ // checksum fails
FError = FE_INVALID_FILE;
return FAIL;
}
The parameters passed to the function are identical to those used in the f readM ( ) function and the first
test we will perform on the integrity of the mfile structure, passed as a parameter, is the same as well.
It will help us determine if we can trust the contents of the mfile structure, having been successfully
prepared for us by a call to f openM ( ) .
The core of the function will be a loop as well:
// 2. loop writing count bytes
while ( count>0)
{
Our intention is to transfer as many bytes of data as possible at a time, using the fast memcpy ( ) func-
tion from the " string. h" libraries.
// 2.1 copy as many bytes at a time as possible
if ( fp->pos+count < 512)
len = count;
else
len = 512- fp->pos ;
memcpy ( fp->buffer+ fp->pos, src, len);
There are numerous pointers and counters that we need to update to keep track of our position as we
add data to the buffer and we increase the size of the file.
// 2.2 update all pointers and counters
fp->pos+=len; // advance buffer position
fp->seek+=len; // count the added bytes
count-=len; // update the counter
src+=len; // advance the source pointer
// 2.3 update the file size too
if (fp->seek > fp->size)
fp->size = fp->seek;
Once the buffer is full, we need to transfer the data to the media in a sector of the currently allocated
cluster.
// 2.4 if buffer full, write current buffer to current sector
if (fp->pos == 512)
{
// 2.4.1 write buffer full of data
if ( !writeDATA( fp) )
return FAIL;
316
File I/O
Notice that an error at this point would be rather fatal. We will return the code fail, whose value is 0,
therefore indicating that not a single byte has been transferred; in fact all the data written to the storage
device this far is now lost.
If all proceeds correctly, though, we can now increment the sector pointers and, if we have exhausted
all the sectors in the current cluster, we need to consider the need to allocate a new one, calling new-
fat ( ) once more.
// 2.4.2 advance to next sector in cluster
fp->pos = 0;
fp->sec++;
// 2.4.3 get a new cluster if necessary
if ( fp->sec == mda->sxc)
{
fp->sec = 0;
if ( newFAT( fp)== FAIL)
return FAIL;
}
} // store sector
} // while count
Shortly, when developing newFAT ( ) , we will have to make sure that the function accurately maintains
the chaining of the clusters in the FAT as they are added to a file.
// 3. number of bytes actually written
return size-count;
} // fwriteM
The function is now complete and we can report the number of bytes written upon exit from the loop.
Closing a file, second take
While closing a file opened for reading was a mere formality and a matter of releasing some memory
from the heap, when we close a file that has been opened for writing, there is a considerable amount of
housekeeping work that needs to be performed.
A new and improved f closeM ( ) function is needed and it will start with a check of the mode field.
unsigned f closeM ( MFILE *fp)
{
unsigned e, r; // offset of directory entry in current buffer
r = FAIL;
// 1. check if it was open for write
if ( fp->mode == x w' )
{
317
Chapter 14
In fact, when we close a file, there might still be some data in the buffer that needs to be written to the
storage device, although it does not fill an entire sector.
// 1.1 if the current buffer contains data, flush it
if ( fp->pos >0)
{
if ( !writeDATA( fp) )
goto ExitClose;
}
Once more, any error at this point is a rather fatal event and will mean that all the file data is lost since
the fcloseM( ) function will not properly complete.
The proper root directory sector must be retrieved and an offset for the directory entry must be calcu-
lated inside the buffer.
// 1.2 finally update the dir entry,
// 1.2.1 retrive the dir sector
if ( !readDIR( fp, fp->entry) )
goto ExitClose;
// 1.2.2 determine position in DIR sector
e = (fp->entry & Oxf) * DIR_ESIZE; // 16 entry per sector
Next, we need to update the file entry in the root directory with the actual file size (it had been initially
set to zero).
// 1.2.3 update file size
fp->buffer[ e + DIR_SIZE] = fp->size;
fp->buffer[ e + DIR_SIZE+1]= fp->size>>8;
fp->buffer[ e + DIR_SIZE+2]= fp->size>>16 ;
fp->buffer[ e + DIR_SIZE+3]= fp->size>>24 ;
Finally, the entire root directory sector containing the entry is written back to the media.
// 1.2.4 update the directory sector;
if ( !writeDIR( fp, fp->entry) )
goto ExitClose;
} // write
If all went well, we will complete the f closeM ( ) function invalidating the checksum field to prevent
accidental reuses of this mfile structure and deallocating the memory used by it and its buffer.
// 2. exit with success
r = TRUE;
ExitClose:
// 3. invalidate the file structure
fp->chk = fp->entry + fp->name [ ] ; // set checksum wrong!
318
File I/O
II 4. free up the buffer and the MFILE struct
f ree ( fp->buffer) ;
free( fp) ;
return ( r) ;
} // fcloseM
Accessory functions
In completing f openM ( ) , f closeM ( ) and creating the new fwriteM ( ) function, we have used a num-
ber of lower-level functions to perform important repetitive tasks.
We will start with newDiR ( ) , used to find an available spot in the root directory to create a new file.
The similarity with findDiR ( ) is obvious, yet the task performed is very different.
unsigned newDIR( MFILE *fp)
// fp file structure
// return found/ fail, fp->entry filled
{
unsigned eCount; // current entry counter
unsigned e; // current entry offset in buffer
int a ;
MEDIA *mda = fp->mda;
// 1. start from the first entry
eCount = ;
// load the first sector of root
if ( !readDIR( fp, eCount))
return FAIL;
// 2. loop until you reach the end or find the file
while ( 1)
{
// 2.0 determine the offset in current buffer
e = (eCount&Oxf) * DIR_ESIZE;
// 2.1 read the first char of the file name
a = fp->buffer[ e + DIR_NAME] ;
// 2.2 terminate if it is empty (end of the list) or deleted
if ( ( a == DIR_EMPTY) | | ( a == DIR_DEL) )
{
fp->entry = eCount;
return FOUND;
} // empty or deleted entry found
319
Chapter 14
II 2.3 get the next entry
eCount++;
if ( (eCount & Oxf) == 0)
{ // load a new sector from the root
if ( !readDIR( fp, eCount))
return FAIL;
}
// 2.4 exit the loop if reached the end or error
if ( eCount > mda->maxroot )
return NOT_FOUND; // last entry reached
}// while
return FAIL;
} // newDIR
The function newFAT ( ) was used to find an available cluster to allocate for a new block of data/new
file.
unsigned newFAT ( MFILE * fp)
// fp file structure
// fp->ccls ==0 if first cluster to be allocated
// !=0 if additional cluster
// return TRUE/FAIL
// fp->ccls new cluster number
{
unsigned i, c = fp->ccls;
// sequentially scan through the FAT looking for an empty cluster
do {
C++; // check next cluster in FAT
// check if reached last cluster in FAT, re-start from top
if ( c >= fp->mda->maxcls)
c = 0;
// check if full circle done, media full
if ( c == fp->ccls)
{
FError = FE_MEDIA_FULL;
return FAIL;
}
// look at its value
i = readFAT( fp, c) ;
320
File I/O
} while ( i!=0); // scanning for an empty cluster
// mark the cluster as taken, and last in chain
writeFAT( fp, c, FAT_EOF) ;
// if not first cluster, link current cluster to the new one
if ( fp->ccls >0)
writeFAT( fp, fp->ccls, c ) ;
// update the MFILE structure
fp->ccls = c;
return TRUE;
} // allocate new cluster
When allocating a new cluster beyond the first one, newFAT ( ) keeps linking the clusters in a chain
and it marks every cluster as properly used. For its working, the function uses two accessory functions
readFATO and writeFAT ( ) in turn.
unsigned readFAT ( MFILE *fp, unsigned eels)
// fp MFILE structure
// eels current cluster
// return next cluster value,
// Oxffff if failed or last
{
unsigned p, c;
LBA 1;
// get address of current cluster in fat
p = eels;
// cluster = Oxabcd
// packed as : 1 2 3
/ / word p 1 | 2 3 | 4 5 | 6 7 |..
// cd ab | cd ab | cd ab | cd ab |
// load the fat sector containing the cluster
1 = fp->mda->fat + (p >> 8 ); // 256 clusters per sector
if ( ! readSECTOR ( 1, fp->buf fer) )
return Oxffff; // failed
// get the next cluster value
c = ReadOddW( fp->buffer, ( (p & 0xFF)<<1));
return c;
} / / readFAT
321
Chapter 14
The writeFAT ( ) function updates the contents of the FAT and keeps all its copies current.
unsigned writeFAT ( MFILE *fp, unsigned els, unsigned v)
// fp MFILE structure
// els current cluster
/ / v next value
// return TRUE if successful, or FAIL
{
unsigned p;
LBA 1;
// get address of current cluster in fat
p = els * 2 ; // always even
// cluster = Oxabcd
// packed as : 1 2 3
// word p 1 | 2 3 | 4 5 | 6 7 |..
// cd ab | cd ab | cd ab | cd ab |
// load the fat sector containing the cluster
1 = fp->mda->fat + (p >> 9 ) ;
p &= Oxlfe;
if ( !readSECTOR( 1, fp->buf fer) )
return FAIL;
// get the next cluster value
fp->buffer[ p] = v; // lsb
fp->buffer[ p+1] = (v>>8);// msb
// update all FAT copies
for ( i=0; i<fp->mda->f atcopy; i++, 1 += fp->mda->f atsize)
if ( !writeSECTOR( 1, fp->buf fer) )
return FAIL;
return TRUE;
} // writeFAT
Finally, writeDATA ( ) was used both by fwriteM ( ) and f closeM ( ) to write actual sectors of data to
the storage device, computing the sector address based on the current cluster number.
unsigned writeDATA ( MFILE *fp)
{
LBA 1;
// calculate lba of cluster/sector
1 = fp->mda->data + (LBA) (fp->ccls-2) * fp->mda->sxc + fp->sec;
return ( writeSECTOR( 1, fp->buf fer) ) ;
} // writeDATA
322
File I/O
Testing the complete fileio module
It is time to test the functionality of the entire module we just completed. As in the previous test, we
will use the serial communication module "conu2 . c" developed in one of the early lessons and the
same delay module that will provide a delayms ( ) function. This time, after mounting the file system,
we will open a source file (that could be any file), and we will copy its contents into a new "destina-
tion" file that we will create on the spot. Here is the code we will use for the "writetest . c" main
file.
/*
** WriteTest.c
*/
#include <p24f j 128ga010 .h>
# include "SDMMC.h"
#include "fileio. h"
#include " . . /delay/delay . h"
#include "../8 cornm/conu2.h"
#define B_SIZE 1024
char data [B_SIZE] ;
int main ( void)
{
MFILE *fs, *fd;
unsigned r;
//initializations
initU2(); //115,200 baud 8,n,l
putsU2 ( "init") ;
while ( ! detectSD ( ) ) ; // assumes SDCD pin is by default an input
Delayms ( 100) ; // wait for card to power up
if ( mount ( ) )
{
putsU2 ("mount") ;
if ( (fs = fopenM( "source.txt", »r')))
{
putsU2 ( "source file opened for reading");
if ( (fd = fopenM( "dest3.txt", "w" ) ) )
{
323
Chapter 14
putsU2 ( "destination file opened for writing");
do{
r = freadM( data, B_SIZE, fs) ;
r = fwriteM( data, r, fd) ;
putU2 ( v . ' ) ;
} while ( r==B_SIZE) ;
fcloseM( fd) ;
putsU2 ( "destination file closed");
}
else
putsU2 ( "could not open the destination file");
fcloseM( f s) ;
putsU2 ( "source file closed");
}
else
putsU2 ( "could not open the source file");
unmount ( ) ;
putsU2 ( "unmount" ) ;
}
else
putsU2 ( "mount failed" ) ;
// main loop
while ( 1) ;
} // main
Make sure you replace the source file name with the actual name of the file you copied on the card for
the experiment.
After creating a new project (let's call it "WriteTest" this time), we will need to add all the necessary
modules to the project window, including:
"sdmmc . c"
"fileio.c"
"conu2 . c"
"delay. c"
"writetest . c"
and all the corresponding include files (.h).
324
File I/O
Once more, remember to follow the checklists for a new project and for the ICD2 debugger, but this
time remember to add some more space for the heap so that we will be able to allocate dynamically at
least two buffers and two mfile structures.
Note: Once enough space is left for the global variables and the stack, there is no reason to with-
hold any memory from the heap. Allocate as large a heap as possible to allow malloc ( ) and
free ( ) to make optimal use of all the memory available.
After building the project and programming the executable on the Explorer 16 board, we are ready to
run the test. If all goes well, after a fraction of a second (the actual time will depend on the size of the
source file chosen) you will be able to see on the screen of your terminal the following messages:
init
mount
source file opened for reading
destination file opened for writing
destination file closed
source file closed
unmount
The number of dots will be proportional to the size of the file, and since we chose the buffer size to be 1024
for this demo, each dot will correspond exactly to one kilobyte of data transferred. At this point, if you trans-
fer the SD/MMC card back to your PC, you should be able to verify that a new file has been created.
^Baek * • ^Search Folders [???] »
Address -^ G:\
3
Date Modified
Ur*s
»
File and Folder Tasks
PjJ Make a new folder
> \ Publish this folder to the
Web
fc2 Share this Folder
El DE5T.TXT
fr-.'l-lP'IE.TXl
131KB Text Document
131KB Text Document
7/20/2002 12:00 AM
7/17/20061; 12 PM
Other Places
tj My Computer
*_j My Documents
%j My Network Places
Details
Removable Disk (G;)
Removable Disk
Fie System: FAT
2 objects
261 KB
j My Computer
A
Figure 14-8. Windows Explorer Screen capture.
325
Chapter 14
Its size and content are identical to the source file, while the date and time reflect the values we set in
the fcloseMO function.
Notice that if you try to run the test program a second time, it is bound to fail now.
init
mount
source file opened for reading
could not open the destination file
source file closed
unmount
This is because, as discussed during the development of the f openM ( ) function, we chose to report an
error when trying to open a file for writing and on the storage device finding a file (dest . txt) already
there with the same name.
Notice that you can recompile the project and run the test with different sizes for the data buffer, from
1 byte to as large as the memory of the PIC24 will allow. Both the f readM ( ) and fwriteM ( ) functions
will take care of reading and writing as many sectors of data as required to fulfill your request. The
time required to complete the operation will change though.
Code Size
The size of the code produced by the "WriteTest" project is considerably larger than the simple
"sdmrac . c" module we tested in the previous lesson.
Memory Usage Gauge
"I
Program Memory
Total: 44030
Data Memory
Totat8192
Figure 14-9. The Memory Usage Gauge.
Still, with all optimization options turned off, the code will add up to just 8,442 bytes (2814 words *
3). This represents only 6% of the total program memory space available on the PIC24FJ128GA010. 1
consider this a very small price to pay for a lot of functionality !
Post-flight briefing
In this lesson we have learned the basics of the FAT 16 file system and we have developed a small inter-
face module that allows a PIC24 to read and write data files to and from a generic mass-storage device.
By using the "sdmmc . c" module, developed in the previous lesson for the low-level interface, we have
created a basic file I/O interface for SD/MMC memory cards.
Now you can share data between the PIC24 and most any other computer system that is capable of
accessing SD/MMC cards, from PDAs to laptops and desktop PCs, from DOS, Windows and Linux
machines to Apple computers running OS-X.
326
File I/O
Tips and tricks
A frequent question I get from embedded-control engineers is: "How can I interface to a "Thumb
drive" (sometimes referred to as a USB stick), a USB mass-storage device, to share/transport data
between my application and a PC?"
The short answer I have is simple: "Don't, if you can help it!"
The longer answer is: "Use an SD card instead!" and here is why. As you have seen in this lesson and
the previous one, reading and writing to an SD card is really simple and requires very little code and
only an SPI port (possibly shared, too).
The USB interface, on the other hand, has all the appeal and appearance of simplicity from the user
perspective, but reading and writing to a USB thumb drive can be deceptively complex and expensive
for a modest embedded-control application. First of all, the simplicity of the SPI interface must be re-
placed by the relatively greater complexity of the USB bus interface. What is required, then, is not just
the standard USB peripheral kind of interface, but the Host USB protocol in its full glory. There is a
considerable hardware price to pay, in terms of dedicated USB transceivers and a large Serial Interface
Engine (SIE) module required. There is an even larger cost in terms of the code and RAM memory
required to support it all. This can be estimated to be several orders of magnitude larger than the basic
SD/MMC card solution we have examined here.
Exercises
1. Consider adding the following functionality:
- Subdirectories management.
- Erasing files.
- Long file-name support.
2. Use the RTCC to provide the current time and date information
3. Consider caching (and/or using a separate buffer) for the current FAT record content to im-
prove read/write performance
4. Consider the modifications required to perform buffering of larger blocks and/or entire
clusters and performing multiblock read/write operations to optimize the SD card low-level
performance. Consider pros and cons.
327
Chapter 14
Books
• Buck, B. (2002)
North Star Over My Shoulder
Simon & Shuster, New York, NY
The story of aviation through the experiences of a lifetime as a pilot.
Links
http://www. tldp. org/LDP/tlk/tlk-title. html
"The Linux Kernel" by David A. Rusling, an online book that describes the inner workings of
Linux and its file system.
http://en. wikipedia. org/wiki/File_Allocation_Table
Once more, an excellent page of the wikipedia that describes the history and many ramifica-
tions of the FAT technology.
http://en. wikipedia. org/wiki/List_of_Jile_sy stems
An attempt to list and classify all major computer file systems in use.
328
CHAPTER
15
Volare
In This Chapter
Using the PIC24 OC modules
►
The WAVE file format
in PWM mode
►
The playO function
Testing the PWM as a D/A
►
The lower level audio routines
converter
►
Testing the WAVE file player
Producing analog waveforms
►
Optimizing the file I/O
Reproducing voice messages
►
LED Profiling
► A Media Player
►
Looking under the hood for more
The last flight, the check-ride with the FAA examiner, is a time of great tension and a little fear. It is
a flight meant to summarize all the phases of flight, where you are asked to put all the knowledge you
gained during the training into practice. Don't worry — it will be easy because you are at the peak of
your preparation and it will be over so fast that you won't have time to realize it.
Just as in a final check-ride, this last lesson will use many of the building blocks developed in the
previous lessons and will put them to practical use to develop a new and entertaining demo project: a
media player.
Congratulations, you are a pilot now. It is time to celebrate and sing!
NEE BLU DIPINTO DI BLU
Italy 1958 / Domenico Modugno
Written by Franco Migliacci & Domenico Modugno
Penso che un sogno cost non ritorni mai piu:
Mi dipingevo le mani e lafaccia di blu
Poi d'improvviso venivo dal vento rapito
E incominciavo a volare nel cielo infinite)
Volare, oh... cant are, oh...
The lyrics are in Italian. The title translates to "In The Blue (Sky), Painted in Blue" ("Volare": to fly).
Modugno cowrote it with Franco Migliacci after Modugno described a man's dream of flying through
the air with his face and hands painted in blue.
329
Chapter 15
Flight plan
In this lesson we will explore the possibility of producing audio signals using, once more, the Output
Compare modules of the PIC24. When in the Pulse Width Modulation (PWM) mode in combination
with more or less sophisticated low-pass filters, the Output Compare modules can be used effectively
as digital-to-analog converters to produce an analog output signal. If we manage to modulate the ana-
log signal with frequencies that fall into the range that is recognized by the human ear, approximately
between 20 Hz and 20 kHz, we get sound!
The flight
The way a pulse width modulation signal works is pretty simple. A pulse is produced at regular inter-
vals (7) typically provided by a timer and its period register. The pulse width (T on ) though is not fixed,
but it is programmable and it can vary between and 100% of the timer period. The ratio between the
pulse width (T on ) and the signal period (7) is called the duty cycle.
50% duty cycle
T on /T=1/2
10% duty cycle
T on /T=1/10
Figure 15-1 . Example of PWM signals of different duty cycle.
There are two extreme cases possible for the duty cycle: 0% and 100%. The first one corresponds to a
signal that is always off. The second one is the case when the output signal is always on. The number
of possible cases in between, typically a relatively small finite number expressed as a logarithm in base 2,
is commonly referred to as the resolution of the PWM. If, for example, there are 256 possible pulse
widths, we will say that we have a PWM signal with an 8-bit resolution.
If you could feed an ideal PWM signal with fixed duty cycle to a spectrum analyzer to study its compo-
sition, you would discover that it contains three parts:
a DC component, with an amplitude directly proportional to the duty cycle.
a sinusoid at the fundamental frequency (f= l/T).
followed by an infinite number of harmonics whose frequency is a multiple of the fundamen-
tal (2f, 3/, 4/ f 5/, 6/...).
330
Volare
Amplitude a
DC component
Fundamental
T
Harmonics
r
^>
f = 1/T
2f
3f
low pass filter
Figure 15-2. Frequency spectrum of a PWM signal.
Frequency
Therefore, if we could attach an "ideal" low-pass filter to the output of a PWM signal generator to
remove all frequencies from the fundamental and up, we could obtain a clean DC analog signal whose
amplitude would be directly proportional to the duty cycle.
Of course such an ideal filter does not exist, but we can use more or less sophisticated approxima-
tions of it to remove as much of the unwanted frequency components as needed. This filter could be
as simple as a single passive R/C circuit (first-order low-pass filter) or could require several (N) active
stages (2 x N-order low pass).
50% duty cycle
T on /T=1/2
Analog out = 0.5
T
on
T
10% duty cycle
T on /T=1/10
Analog out = 0.1
T
on
T
Figure 15-3. Analog output of PWM and ideal low pass filter circuit.
If we aim at producing an audio signal and we choose the PWM frequency wisely, we can take advan-
tage of the natural limitation of the human ear that will act as an additional filter ignoring any signal
whose frequency is outside the 20 Hz to 20 kHz range. In addition to that, most of the audio ampli-
fiers we might want to feed the output signal into will also include a similar type of filter in their input
stages. In other words, if we make sure that the PWM signal operates on a frequency at or above 20
kHz, both phenomenon will contribute to help our cause and will allow us to use a simpler and more
inexpensive filter circuit.
331
Chapter 15
Also intuitively enough, since we can only change the duty cycle once every PWM period (T), the
higher the frequency of the PWM, the faster we will be able to change the output analog signal, and
therefore the higher will be the frequency of the audio signal we will be able to generate.
In practical terms, this means that the highest audio signal a PWM can produce is only half of the
PWM frequency. So, for example, a 20-kHz PWM circuit will be able to reproduce only audio signals
up to 10 kHz, while to cover the entire audible frequency spectrum we need a base period of at least
40 kHz. It is not a coincidence that, for example, music CDs are digitally encoded at the rate of 44,100
samples per second.
Using the PIC24 OC modules in PWM mode
In a previous lesson we have already used the PIC24 Output Compare modules to produce precise
timing intervals (to produce a video output). This time we will use the OC modules in PWM mode, to
generate a continuous stream of pulses with the desired duty cycle.
Upper Byte:
U-0
U-0
R/W-0
U-0
U-0
U-0
U-0
U-0
—
—
ICSIDL
—
—
—
—
—
bit 15
bit 8
Lower Byte:
R/W-0
R/W-0
R/W-0
R-0, HC
R-0, HC
R/W-0
R/W-0
R/W-0
ICTMR
ICI1
ICIO
ICOV
ICBNE
ICM2
ICM1
ICMO
bit 7
bitO
Figure 15-4. The Output Compare module main control register OCxCON.
All we need to do to initialize the OC module to generate a PWM signal is set the three ocm bits in the
OCxCON control register to the 0x1 10 configuration. A second PWM mode is available (0x1 11), but we
have no use for the fault input pins (OCFA/OCFB), commonly required by a different set of applica-
tions as a protection mechanism (motor control/power conversion). Next we need to select the timer on
which to base the PWM period. The choice is limited to Timer2 or Timer3, and for now it will make
no difference to us. It is how we will configure the chosen timer that will make all the difference. (See
Figure 15-5.)
Keeping in mind that we want to be able to produce at least a 40 kHz PWM period, and assuming a
peripheral clock of 16 MHz as is the case when using the Explorer 16 board, we can calculate the opti-
mal values for the prescaler and the period register prx. With a prescaler set to a 1:1 ratio, we obtain a
400-cycle period, generating an exact 40-kHz signal. This value also dictates the resolution of the duty
cycle for the Output Compare module. Since we will have 400 possible values of the duty cycle, we
can claim a resolution between 8 and 9 bits, as we have more than 256 (2 8 ) steps but less than 512 (2 9 ).
Reducing the frequency to 20 kHz would give us one bit more of resolution (between 9 and 10), but
would also mean that we would be limiting the output frequency range to a maximum of 10 kHz, prob-
ably a small but noticeable difference to the human ear. Once the chosen timer is configured and just
before writing to the ocxcon register, we will need to set, for the first time, the value of the first duty
cycle writing to the register ocxR, and the register ocxrs. When in PWM mode, the two registers will
work in a master/slave configuration. Once the PWM module is started (writing the mode bits in the
ocxcon register), we will be able to change the duty cycle by writing only to the ocxrs register. The
ocxR register will be updated, copying a new value from ocxrs, only and precisely at the beginning of
332
Volare
each new period so as to avoid glitches and to leave us with an entire period (T) of time to prepare the
next duty cycle value:
Set Flag bit
OCxlF (1 >
OCxRS (1)
OCxR< 1 '
^
Comparator
/91V
OCTSEL /0
Output
Logic
VI
OCM2:OCM0
Mode Select
S Q
R
Output Enable
ocx< 1 =
OCFA or OCFB <2)
B
16>
\ 1 «
TMR register inputs
from time bases
(see Note 3).
Period match signals
from time bases
(see Note 3).
Note 1 : Where 'x' is shown, reference is made to the registers associated with the respective output compare channels 1 through 8.
2: OCFA pin controls OC1 -OC4 channels. OCFB pin controls OC5-OC8 channels.
3: Each output compare channel can use one of two selectable time bases. Refer to the device data sheet for the time
bases associated with the module.
Figure 1 5-5. Output Compare module block diagram.
Here is an example of a simple initialization routine for the OC1 module:
void initAudio ( void)
{
// init TMR3 to provide the timebase
T3C0N = 0x8000; // enable TMR3 , prescale 1:1, internal clock
PR3 = 400-1; // set the period for the given bitrate
_T3IF = 0; // clear interrupt flag
_T3IE = 1; // enable TMR3 interrupt
// init PWM
// set the initial duty cycles (master and slave)
OC1R = OC1RS = 200; // init at 50%
// activate the PWM module
OC1CON = OxOOOE;
} // initAudio
Notice that we have also taken the opportunity to enable the Timer3 interrupt so that we are alerted
each time a new period starts, and we can decide how and if to update the next duty cycle value writing
tO OC1RS.
333
Chapter 15
Testing the PWM as a D/A converter
To start experimenting on the Explorer 16 we will need to add just a couple of discrete components to
the prototyping area. A 1-kohm resistor and a 100 nF capacitor will produce the simplest low-pass filter
(first order with a 1.5-kHz cut-off frequency). We can connect the two in series and wire them to the
output pin of the OC 1 module found on pin of PORTD as represented in the schematic below.
Rl
Ik
RD0 QC1 > WW
<Test
CI
100nF
- GND
Figure 15-6. Using a PWM signal to produce an analog output.
A couple of more lines of code will complete our short test project:
void _ISRFAST _T3I
nter
rupt (
void)
{
// clear inter
rupt
flag
and
exi
t
_T3IF = 0;
} // T3 Interrupt
main ( void)
{
init Audio ( ) ;
// main loop
while ( 1 ) ;
} / / main
Add the usual header and include file, and save the code in a new file called TestDA. c. You can then
create a quick test project that will contain this single file, build it and use the ICD2 debugger to pro-
gram the Explorer 16 board.
After connecting a meter, or an oscilloscope probe if available, to the test point, run the program and
verify the average (DC) output level.
The needle of the meter (or the trace of the scope) will swing to a voltage level of approximately 1.5V
that is 50% of the regular voltage output of a digital I/O pin on the Explorer 16 board. This is consistent
with the value of the duty cycle set by the initialization routine to 200 (for a period of 400 cycles). If you
have an oscilloscope, you can also point the probe directly at the other end of the Rl resistor (directly to
the output pin of the OC1 module) and verify that a square wave of the exact frequency of 40 kHz is pres-
ent with a duty cycle of 50%.
334
Volare
You can now change the initialization routine to experiment with other values between and 399 to
verify the response of the circuit and the proportionality of the output signal to the changing values of
the duty cycle with analog values between and 3V.
Producing analog waveforms
With help from the OC1 module, we have just crossed the boundary between the digital world made
of ones and zeros and the analog world in which we have been able to generate a multitude of values
between and 3V.
We can now play with the duty cycle, changing it from period to period to produce waveforms of any
sort and shape. Let's start by modifying the project a little bit, adding some code to the interrupt rou-
tine that so far has been left empty:
OC1RS = (count < 20) 400 : 0;
count++;
if ( count >= 40)
count = ;
You will need to declare count as a global integer and remember to initialize it to 0.
Save and rebuild the project to test the new code on the Explorer 16 board.
Every 20 PWM periods, the filter output will alternate between the value 3V (100%) and the value 0V
(0%) producing a square wave visible on the oscilloscope at the frequency of 1 KHz (40 kHz/40).
A more interesting waveform could be generated by the following algorithm:
OC1RS = count*10;
count ++;
if ( count >= 40)
count = ;
This will produce a triangular waveform (sawtooth) of approximately 3 V peak amplitude, with a
gradual ramp of the duty cycle from to 100% in 40 steps (2.5% each), followed by an abrupt fall back
to where it will repeat. This signal will repeat with a frequency of 1 kHz as well.
Neither of the two examples will qualify as a "nice" sound if you try to feed them to an audio amplifier,
although they will both have a recognizable (fundamental) high-pitched tone, at about 1 kHz. Lots of
harmonics will be present and audible in the audio spectrum and will give the sound an unpleasant buzz.
To generate a single clean tone, what we need is a pure sinusoid. The interrupt service routine below
would serve the purpose, generating a perfect sinusoid at the frequency of 400 Hz (in musical terms,
that would be close to an A).
void _ISRFAST _T3 Interrupt ( void)
{// compute the new sample for the next cycle
OC1RS = 200+ ( 200* sin(count *0.0628));
count ++ ;
if ( count >= 40)
count = ;
// clear interrupt flag and exit
_T3IF = 0;
} // T3 Interrupt
335
Chapter 15
Unfortunately, as fast as the PIC24 and the math libraries of the C30 compiler are, there is no chance
that we can use the sin() function, perform the multiplications and additions required, and obtain a new
duty-cycle value at the required rate of 400 Hz. The Timer3 interrupt hits every 25 us, too short a time
for such a complex floating-point calculation, so the interrupt service routine would end up "skipping"
interrupts and producing a sinusoidal output that is only half the required frequency (one octave lower).
Still, if you listen to it by feeding the signal to an audio amplifier, you will be immediately able to ap-
preciate the greatly improved clarity of the sound.
For higher frequencies we will need to pretabulate the sinusoid values so as to perform the smallest
number of calculations possible (preferably working on integers only) at run time. Here is an example
that uses a table (stored in the Flash program memory of the PIC24) containing precomputed values. I
obtained the table by using a spreadsheet program in which I used the following formula:
= offset + INT( amplitude * SIN( ROW * 6.28/ PERIOD))
For a period of 100 samples (400 Hz), offset and amplitude of 200, 1 obtained:
= 200 + INT( 200*SIN(A1 *6. 28/100))
I filled the first column (A) of the spreadsheet with a counter and I copied the formula over the first 100
rows of the second column (B), formatting the output for zero decimal digits.
Then, I cut and pasted the entire column in the source code, adding commas at the end of each line to
comply with the C syntax.
void _ISRFAST _T3 Interrupt ( void)
{
// load the new samples for the next cycle
OC1RS = Table [ count] ;
count++;
if ( count >= 40)
count = ;
// clear interrupt flag and exit
_T3IF = 0;
} // T3 Interrupt
const int Table [100] = {
200,
212,
225,
237,
249,
149,
161,
174,
186,
199
};
336
Volare
This time, you will easily be able to produce the 400-Hz output frequency desired, and there will be
plenty of time between the Timer3 interrupt calls to perform other tasks as well.
A
C D E F H I J K L
1
1255175
200
212
?
1
I
3
~?
25.05401
225
4
3
37.45749
237
4UU -
350 -
300 -
250 -
200 -
150 -
100 -
50 ■
n -
/\
5
4
49.7133
249
6
5
61.7731
261
7
6
73.58937
85.11551
273
8
7
285
296
307
q
8
96.30607
107.1169
10
9
/
11
10
117.5055
127.4308
136.8537
145.737
317
327
u
11
/
13
12
336
14
13
345
354
361
368
375
380
385
390
393
396
15
16
17
14 154.0458
15 161.7472
1C iCQQinQ
18
1?
175,2091
180.9166
185.9107
190.1719
\
19
20
21
18
19
\ /
20
22
23
24
25
26
27
21 193.6833
22
23
196.4311
198.4045
199.5957
199.9999
199.6157
398
1QQ
24
399
399
u
25
26
28
29
30
31
32
33
34
2?
198.4444
196.4908
193.7625
190.2703
186.0279
398
396
393
390
28
29
30
31
336
32 181.0521
33 175.3625
381
375
35
34 168.9815
368
Figure 15-7. Spreedsheet to compute the 400-Hz sinusoid table.
337
Chapter 15
Reproducing voice messages
Once we learn how to produce sound, there is no stopping us. There are infinite applications in embed-
ded control where we can put these capabilities to use. Any "human" interface can be greatly enhanced
by using sound to provide feedback, to capture the attention of the user with alerts and error messages
or, if properly done, to simply enhance the user experience. But we don't have to limit ourselves to
simple tones or basic melodies. We can, in fact, reproduce any kind of sound, as long as we have a
description of the waveforms required. Just like the table used for the sinusoid in the previous example,
we could use a larger table to contain the unmistakable sound produced by a particular instrument
or even a complete vocal message. The only limit becomes the room available in the Flash program
memory of the PIC24 to store the data tables next to the application code.
If, in particular, we look at the possibility of storing voice messages, knowing that the energy of the hu-
man voice is mostly concentrated in the frequency range between 400 Hz and 4 kHz, we can considerably
reduce our output frequency requirements and limit the PWM playback to the rate of only 8,000 samples
per second. Notice that we should still maintain a high PWM frequency to keep the PWM signal harmon-
ics outside the audio frequency range and the low-pass filter simple and inexpensive. It is only the rate
at which we change the PWM duty cycle and read new data from the table that will have to be reduced,
once every five interrupts in this case (40,000/8,000 = 5). With 8,000 samples per second, we would
theoretically be able to play back as much as 16 seconds of voice messages stored inside the controller
Flash memory. That is already a lot of talking for a single-chip solution. To increase the capacity further,
potentially doubling it, we could start looking at simple compression techniques used for voice applica-
tions such as ADPCM, for example. ADPCM stands for Adaptive Differential Pulse Coded Modulation,
and it is based on the assumption that the difference between two consecutive samples is smaller than the
absolute value of each sample and can therefore be encoded using a smaller number of bits. The actual
number of bits used is then optimized; it changes dynamically so as to avoid signal distortion while pro-
viding a desired compression ratio — hence the use of the term "adaptive."
338
Volare
A media player
In the rest of this lesson, we will focus on a much more ambitious project, putting to use all the librar-
ies and capabilities we have acquired in the last several lessons. We will attempt to create a basic
multimedia application capable of playing stereo music files from an SD™/MMC memory card. The
format of choice will be the uncompressed WAVE format that is compatible with almost any audio ap-
plication and is the default destination format when extracting files from a music CD.
We will start by creating a brand new project using the usual checklists. We will immediately add the
SD/MMC low-level interface and the file I/O library for access to a FAT 16 file system to the project
source files list.
After opening a file for reading, though, this time we will need to be able to understand the specific
format used to encode the data it contains.
The WAVE file format
Files with the . wav extension, encoded in the WAVE format, are among the simplest and best docu-
mented, but they still require some careful study. The WAVE format is a variant of the RIFF file format,
a standard across multiple operating systems, which uses a particular technique to store multiple pieces
of information/data by dividing them into "chunks." A chunk is nothing more than a block of data pre-
ceded by a header containing two 32-bit elements: the chunk ID and the chunk size.
Offset
Size
Value
Description
0x00
4
ASCII
Chunk ID
0x04
4
Size
Chunk size (size of the content)
0x08
size
Data content
0x08+size
1
0x00
Optional padding
Table 15-1. Format of a data "chunk."
Note also that the chunk total size must be a multiple of two so that all the data in a RIFF file ends up
being nicely word aligned. If the data block size is not a multiple of two, an extra byte of padding is
added to the chunk.
A chunk with the "RIFF" ID is always found at the beginning of a . wav file and its data block begins
with a 4-byte "type" field. This type field must contain the string "WAVE". Chunks can be nested like
Russian dolls, but there can also be multiple subchunks inside a given type of chunk.
The Table 15-2 illustrates a " .wav file RIFF chunk structure:
Offset
Size
Value
Description
0x00
4
"RIFF"
This is the RIFF chunk ID
0x04
4
Size
(size of the data block+4)
0x08
4
"WAVE"
Type ID
0x10
Size
Data block (subchunks)
Table 15-2. "RIFF" chunk of type "WAVE".
339
Chapter 15
The data block in its turn contains a "fmt" chunk followed by a "data" chunk. As is often the case, one
image is worth a thousands words.
Chunk ID "RIFF"
Chunk Data Size
Chunk type "WAVE"
Chunk ID "fmt"
Chunk Data Size
Sample Info
Chunk ID "data"
Chunk Data Size
Audio Samples
Figure 15-8. Basic WAVE file layout.
The "fmt" chunk contains a defined sequence of parameters that fully describes the stream of samples
that follows in the "data" chunk, as represented in Table 15.3.
Offset
Size
Description
Value
0x00
4
Chunk ID
"fmt "
0x04
4
Chunk Data Size
1 6 + extra format bytes
0x08
2
Compression code
Unsigned int
0x0a
2
Number of channels
Unsigned int
0x0c
4
Sample rate
Unsigned long
0x10
4
Average bytes per second
Unsigned long
0x14
2
Block align
Unsigned int
0x16
2
Significant bits per sample
Unsigned int (>1)
0x18
2
Extra format bytes
Unsigned int
Table 15-3. The "fmt" chunk content.
In between the "fmt" and "data" chunks there could be other chunks containing additional information
about the file, so we might have to scan the chunk IDs and skip through the list until we find the chunk
we are looking for.
340
Volare
The play () function
Let's create a new software module that will take care of opening a given " . wav file and, after capturing
and decoding the information in the "fmt" chunk, will initialize an audio output module similar to, if not
more sophisticated than, the one we developed in the first part of the lesson. We will call it "wave . c " .
/*
** Wave.C
** Wave File Player
** Uses 2x8 bit PWM channels
* *
*/
#include <stdlib.h>
#include ".. /Audio /Audio PWM.h"
# include " . . /sdmmc/sdmmc . h"
#include " . . /sdmmc/fileio .h"
// chunk ID definitions
#define RIFF_DWORD 0x46464952UL
#define WAVE_DWORD 0x45 5 64 15 7UL
#define DATA_DWORD 0x6174 61 64UL
# define FMT DWORD 0x2 074 6d66UL
typedef struct {
// data chunk
unsigned long dlength;
char data [4] ;
// actual data size
// "data"
/ / format chunk
unsigned bitpsample; //
unsigned bpsample;
unsigned long bps;
unsigned long srate;
unsigned channels;
unsigned subtype;
unsigned long flength;
char fmt [4] ;
// bytes per sample (4 = 16 bit stereo)
// bytes per second
// sample rate in Hz
// number of channels (1= mono, 2 = stereo
// always 01
// size of encapsulated block (16)
// "fmt_"
char type [ 4 ] ;
unsigned long t length;
char riff [4] ;
} WAVE;
// file type name "WAVE"
// size of encapsulated block
// envelope "RIFF"
The wave structure will be useful to collect all the "fmt" parameters in one place and the chunk ID
macros will help us recognize the different unique IDs, treating them as long integers and allowing us a
quick and efficient comparison.
341
Chapter 15
Next, let's start coding the play ( ) function. It needs just one parameter: the file name.
unsigned play( const char *name)
{
int i ;
WAVE wav ;
MFILE *f;
unsigned wi;
unsigned long lc, r, d;
int skip, size, stereo, fix, pos;
// 1. open the file
if ( (f = fopenM( name, "r')) == NULL)
{ // failed to open
return FALSE;
}
After trying to open the file and reporting the error if unable, we will immediately start looking inside
the data buffer for the RIFF chunk ID and the WAVE type ID, as a signature, that will confirm to us we
have the right kind of file:
// 2. verify it is a RIFF formatted file
if ( ReadL( f->buffer, 0) 1= RIFF_DWORD)
{
f close ( f ) ;
return FALSE;
}
!= WAVE DWORD
// 3. look for the WAVE type
if ( (d = ReadL( f->buffer, 8)
{
f close ( f ) ;
return FALSE;
}
If successful, we should verify that the "fmt" chunk is the first in line inside the data block. Then we
will harvest all the information needed to process the data block for the playback:
// 4. look for the chunk containing the wave format data
if ( ReadL( f->buffer, 12) != FMT_DWORD)
return FALSE;
wav. channels
wav.bitpsample
wav. srate
wav.bps
wav.bpsample
= ReadW( f->buffer, 22
= ReadW( f->buffer, 34
= ReadL( f->buffer, 24
= ReadL( f->buffer, 28
= ReadW( f->buffer, 32
342
Volare
Next, we start looking for the "data" chunk, inspecting the chunk ID fields of the next block of data
after the end of the "fmt" chunk, and skipping the entire block if not matching:
// 5. search for the data chunk
wi = 20 + ReadW( f->buffer, 16);
while ( wi < 512)
{
if (ReadL( f->buffer, wi ) == DATA_DWORD)
break;
wi += 8 + ReadW( f->buffer, wi+4) ;
}
if ( wi >= 512) // could not find a data chunk in current sector
{
f close ( f ) ;
return FALSE;
}
If, in the process, we exhaust the content of the currently loaded buffer of data, we know we have a
problem. Typical WAV files produced by extracting data from a music CD will have just the "data"
chunk immediately following the "fmt" chunk. Other applications (MIDI interfaces, for example) can
generate "WAV" files with more complex structures including multiple "data" chunks, "playlists."
"cues." "labels", etc. but we aim at playing back only plain- vanilla type "WAV" files.
Once found, the size of the "data" chunk will tell us the real number of samples contained in the file:
// 5.1 find the data size (actual wave content)
wav.dlength = ReadL ( f->buffer, wi+4);
The playback sample rate must now be taken into consideration to determine if we can "play" that fast.
It could in fact exceed our capabilities, and we might have to skip every other sample to reduce the
data rate. We will consider 48k samples/sec our limit so we will be able to read the data fast enough to
maintain at least an 8-bit resolution.
// 6. compute the period and adjust the bit rate
r = wav.bps / wav.bpsample; // r = samples per second
skip = wav.bpsample; // skip factor to reduce bandwith (stereo)
while ( r > 48000)
{
r >>= 1; //as you divide sample rate by two
skip <<= 1; // multiply skip by two
}
Higher rates will be treated by gradually dividing the rate by a factor of two and doubling the skip.
We can then compute the required PWM period value (to be used to set the prx register). A problem
could occur if the required period exceeds the available bits in the register (16), resulting in a period
value greater than 65,536.
343
Chapter 15
II 6.1 check if the sample rate is compatible with the TMR3 prescaler 1:1
d = (16000000L/r) -1;
if ( d > ( 65536L)) // max TMR3 period value (16 bit)
{
f close ( f ) ;
return FALSE;
}
During the playback we will keep track of the number of samples extracted from the file to determine
when we have reached the end of the file. The long integer variable (lc) will keep track of it.
// 7. start loading the buffers
// determine the number of bytes composing the wav data chunk
lc = wav.dlength;
Notice that so far we have not used the f readM ( ) function; we have been (cheating) peeking inside the
file buffer knowing that f openM ( ) had it already loaded.
MFILEf*
ABuffer[ 1-CurBuf] ABuffer[ CurBuf]
"fmt"
chunk
"data"
chunk
play()
function
Timer3 ISR
OCx
PWM modules
AEmptyFlag
CurBuf
Figure 15-9.
To make the playback smooth, we will use a double-buffering scheme, so that as the audio interrupt
routines are fetching data from one buffer, we will take our time in refilling the other buffer with new
data from the file. The array ABuf f er [ ] is in fact defined as two blocks of b_size bytes each. b_size
is chosen to be a multiple of 512, so that the calls to the f readM ( ) function will be able to transfer en-
tire sectors of data at a time for maximum efficiency. We will have to verify that the time required for
f readM ( ) to fill one buffer will be shorter than the time required to play back (consume) all the data in
the second buffer by the PWM interrupt service function.
When starting the double-buffering scheme, we will fill both buffers to get a head start:
// 8. pre-load both buffers
r = fread( ABuffer[0], B_SIZE*2, f ) ;
AEmptyFlag = FALSE;
lc-= B SIZE*2
//we assume here that lc>=B_SIZE*2 ! ! !
The assumption here is that the . wav file will contain at least enough data to fill the two buffers, but if
you plan on using very short files containing less than a few kbytes of data, you might want to modify
this code. Check the number of bytes returned by f readM ( ) and add the correct padding at the end of
the buffer(s).
344
Volare
At this point we are ready to initialize the audio playback "machine," which will be simply our T3 int-
errupt ( ) function modified to accommodate two channels for stereo playback. We will also add
the ability to skip samples, to reduce the sample rate if necessary, and the ability to deal with 16-bit
samples (signed) as well as 8-bit samples (unsigned). All this information will be passed to the audio
module initAudio ( ) routine as a short list of parameters:
// 9. start playing, enable the interrupt
initAudio ( wav.srate, skip, size, stereo, fix, pos);
As the timer interrupt is activated, the service routine starts immediately consuming data from the first
buffer and, as soon as its whole content is exhausted, it will set the flag AEmptyFlag to let us know
that new data needs to be retrieved from the WAV file and the second buffer will be selected as the
active one. Therefore, to maintain the playback flowing smoothly, we will sit in a tight loop, constantly
checking for the AEmptyFlag, ready to perform the refill, counting the bytes we read from the file until
we use them all up.
// 10. keep feeding the buffers in the playing loop
while (lc >=B_SIZE)
{
if ( AEmptyFlag)
{
r = fread( ABuf f er [1-CurBuf ] , B_SIZE, f);
AEmptyFlag = FALSE;
lC-= B_SIZE;
}
} // while wav data available
Actually, we stop a little sooner, when the data left in the file is no longer sufficient to fill another entire
buffer load. In that case, unless the data block size was an exact multiple of the buffer size and there is
no new data to read, the last piece is loaded and needs to be padded to fill completely what will be the
last buffer to play back:
// 11. flush the buffers with the data tail
if( lc>0)
{
// load the last sector
r = fread( ABuf f er [1-CurBuf ] , lc, f);
last = ABuffer [1-CurBuf] [r-1] ;
while(( r<B_SIZE) && (last>0))
ABuffer [1-CurBuf] [r++] = last;
// wait for current buffer to be emptied
AEmptyFlag = 0;
while ( ! AEmptyFlag) ;
}
345
Chapter 15
We wait then for the completion of the playback of the very last buffer and we immediately terminate
the audio playback:
// 12. finish the last buffer
AEmptyFlag = ;
while ( ! AEmptyFlag) ;
// 13. stop playback
ha It Audio ( ) ;
Closing the file, we release the memory allocated and we return to the calling application:
// 14. close the file
fclose ( f ) ;
// 15. return with success
return TRUE;
} // play
To complete this module, we need to create a small include file "wave . h" to publish the prototype of
the play() function:
/*
** Wave.H
** Wave File Player
** Uses 2x8 bit PWM channels
* *
*/
unsigned play( const char *name) ;
346
Volare
The low level audio routines
The play ( ) function we have just completed relied heavily on a lower-level audio module to perform
the actual Timer and OC peripherals initialization, as well as to perform the actual periodic update of
the PWM duty cycle. We will call this module "audiopwm. c" and it will be mostly based on the code
developed in the beginning of this chapter, extended to manage two channels for stereo playback and a
number of additional options. The OC1 and OC2 modules will be used simultaneously to produce the
left and right channels. The timer interrupt routine will be the real core of the playback functionality. A
pointer BPtr will keep track of our position inside each buffer, as we will be using up the data to feed
the PWM modules with new samples at every period.
void _ISRFAST _T3 Interrupt ( void)
{
// 1. load the new samples for the next cycle
OC1RS = 30+(*BPtr A Fix);
if ( Stereo)
OC2RS =30 + (*(BPtr + Size) A Fix);
else // mono
OC2RS = OC1RS;
The pointer is advanced by a number of bytes that depends both on the size of the samples (16 or 8 bits
each) as well as the need to skip samples to reduce the sample rate when the play ( ) function deter-
mines it is necessary:
// 2. skip samples to reduce the bitrate
BPtr += Skip;
As soon as a buffer-load of data is used up, we need to swap the active buffer:
// 3. check if buffer emptied
if ( --BCount == 0)
{
// 3.1 swap buffers
CurBuf = 1- CurBuf;
BPtr = ABuffer[ CurBuf];
Reset the samples counter and set a flag to alert the play ( ) routine we need a new buffer to be pre-
pared before we run out of data again:
// 3.2 restart counter
BCount = B_SIZE/Size;
// 3.3 flag a new buffer needs to be filled
AEmptyFlag = 1;
}
Only then can we exit after clearing the interrupt flag:
// 4. clear interrupt flag and exit
_T3IF = 0;
} // T3 Interrupt
347
Chapter 15
The initialization routine is equally straightforward if you recall the one we created at the beginning
of the chapter, except more parameters are passed from the calling application and copied into the
module's own (private) variables:
void initAudio ( long bitrate, int skip, int size, int stereo, int fix, int pos)
{
// 1. init pointers
CurBuf =0; // start with bufferO active first
BPtr = ABuffer[ CurBuf] +pos;
BCount = (B_SIZE-pos) /size; // number of samples to be played
AEmptyFlag = ;
Skip = skip;
Fix = fix;
Stereo = stereo;
Size = size;
One buffer is selected as the "current" in-use buffer and all the pointers and counters are initialized.
Then the timer is initialized and its interrupt mechanism:
// 2. init the timebase
T3CON = 0x8000; // enable TMR3 , prescale 1:1, internal clock
PR3 = FCY / bitrate; // set the period for the given bitrate
Offset = PR3/2;
_T3IF = 0; // clear interrupt flag
_T3IE = 1; // enable TMR3 interrupt
The duty cycles are initialized next to an initial offset that will be half the value of the period, to pro-
vide an even 50% initial output level.
// 3. set the initial duty cycles
OC1R = OC1RS = Offset; // left
OC2R = OC2RS = Offset; // right
Finally, the Output Compare modules are fired up:
// 4. activate the PWM modules
OC1CON = 0x0 00E; // CHI and CH2 in PWM mode, TMR3 based
OC2CON = 0x0 00E;
} // initAudio
348
Volare
The function haltAudio ( ) called at the end of the playback will definitely be the simplest. Its only
task is to disable Timer3 and therefore freeze the Output Compare modules and with them the entire
interrupt mechanism:
void haltAudio ( void)
(
T3IE = 0;
} // halt audio
// disable TMR3 interrupt
To complete the module you will need the usual header, include files and the definitions of the global
variables allocated, which will include the audio buffers.
/*
** Audio PWM demo
*/
#include <p24f j 128ga010 .h>
# include " Audi o PWM. h"
#define FAR attribute
far
// global definitions
unsigned Offset;
char _FAR ABuffer[ 2][ B_SIZE] ;
int CurBuf;
volatile int AEmptyFlag;
// 50% duty cycle value
// double data buffer
// index of buffer in use
// flag a buffer needs to be filled
// internal variables
int Stereo;
int Fix;
int Skip;
int Size;
// flag stereo play back
// sign fix for 16-bit samples
// skip factor to reduce sample/rate
// sample size (8 or 16-bit)
// local definitions
unsigned char *BPtr;
int BCount;
// pointer inside active buffer
Notice that, just as we did in previous lessons, when allocating large buffers for video applications we
can use the far attribute to allocate memory beyond the PIC24 near addressing space.
349
Chapter 15
The include file "audiopwm.h" will publish all the necessary definitions and prototypes for the "Wave .c"
module and other applications to make use of the services provided by the Audio PWM module.
/*
** AudioPWM.h
*/
#define FCY
# define TCYxUS
#define B SIZE
16000000L
16
2048
// instruction cycle frequency
// number of Tcycles in a microsecond
// audio buffer size
extern char ABuffer[ 2] [ B_SIZE] ; // double data buffer
extern int CurBuf; // index of buffer in use
extern volatile int AEmptyFlag; // flag a buffer needs to be filled
void initAudio ( long bitrate, int skip, int size, int stereo, int fix, int pos) ;
void haltAudio ( void) ;
Testing the WAVE file player
Now that the low-level audio module and the playback module have been completed, it is time to put it
all together and start testing by playing some music.
Let's create a new project called "WaveTest" and let's immediately add all the necessary modules and
their include files to the project. They are:
"sdmmc . c"
"fileio.c"
"audiopwm. c"
"wave . c"
"sdmmc .h"
"fileio.h"
"audiopwm. h"
"wave.h"
350
Volare
Then, let's add a new main module "wave test ,c", which will contain just a few lines of code. It will
invoke the play ( ) function indicating the name of a single file that we will have copied onto the SD/
MMC card (trackoo .wav).
/*
** WaveTest
*/
#include <p24f j 128ga010 .h>
# include "SDMMC.h"
#include "fileio.h"
#include ".. /Audio /Audio PWM.h"
# include " . . /Wave /Wave .h"
main ( void)
(
TRISA = OxffOO;
if ( ! mount () )
PORTA = FError + 0x80;
else
{
if ( play( "TRACK00.WAV"))
PORTA = 0;
else
PORTA = OxFF;
} // mounted
while ( 1)
{
} // main loop
} //main
The PORTA row of LEDs will serve as our display to report errors, should the mount ( ) operation fail
or should the file not be found on the storage device.
Build the project and program the code on the Explorer 16 board using the appropriate checklists. Don't
forget to reserve some room for the Heap, as the fileio . c module uses it to allocate buffers and data
structures.
351
Chapter 15
To proceed gradually, I would recommend that you test the program with WAV files of increasingly
high sample rates and sizes. For example, you should run the first test with a WAV file using 8-bit
samples, mono, at 8k samples/second. Then proceed, gradually increasing the complexity of the format
and the speed of playback, possibly aiming to reach with a last test the full capabilities of our applica-
tion with a 16-bit per sample, stereo, 44,100 samples/second file. The reason for this gradual increase is
that we will need to verify that the performance of our "fileio.c" module is up to the task. In fact, as
the sample rate, number of channels and size of the samples increase, so does the bandwidth required
from the file system. We can quickly calculate the performance levels required by a few combinations
of these parameters.
File
Sample
size
Channels
Sample-rate
Byte-rate
Reload
period (ms)
Voice mono
1
8,000
8,000
64.0
Voice stereo
2
8,000
16,000
32.0
Audio 8-bit mono
1
22,050
22,050
23.2
Audio 8-bit stereo
2
22,050
44,100
11.6
Audio 8-bit high
bitrate mono
1
44,100
44,100
11.6
Audio 8-bit high
bitrate stereo
2
44,100
88,200
5.8
Audio 16-bit mono
2
1
44,100
88,200
5.8
Audio 16-bit stereo
2
2
44,100
176,400
2.9
The table shows the byte-rate required by each file format — that is, the number of bytes that get con-
sumed by the playback function for every second (sample size x channels x sample rate). In particular,
the last column shows how often a new buffer full of data will be required to be replenished (512/byte-
rate); that gives us the time available for the play ( ) routine to read the next sector of data from the
WAV file.
Notice that since the PIC24 PWMs can only produce less than 9 bits of resolution when operating at
the 44,100-Hz frequency, the audio PWM module has been designed to use only the MSB of a 16-bit
sample. Therefore, don't expect any increase in the quality of the audio output once you attempt to play
back a WAV file in one of the last two formats. All you obtain at that point is a waste of the space on
the SD/MMC memory card. If you want to maximize the use of the storage space available, make sure
that when copying a file onto the card, you reduce the sample size to 8 bits. You will be able to pack a
double number of music files on the card for the same output audio resolution.
If you start experimenting gradually, as I suggested, moving down the table, you should find that
beyond a certain point (beyond the audio 8-bit high-bitrate mono, probably) things just won't play out
right. The playback will skip, repeat and hiccup and it just won't sound right. What is happening is that
the f readM ( ) function has reached its limit and is not capable of keeping up with the audio playback
demands. The time it takes, on average, to load a new buffer of data is longer than the time it takes to
352
Volare
consume one; after a short while, the play ( ) routine starts falling behind and the audio playback func-
tion starts repeating a buffer or playing back buffers that are not completely filled yet.
Optimizing the file I/O
When we wrote the file I/O library, and even before, when we wrote the low-level functions to access
the SD/MMC card, we have focused mainly on getting things done. We have never really tried to assess
the level of performance provided. Perhaps now we have the right motivation to look into it. Through-
out the rest of the book we have resisted using any of the optimization features of the compiler, so that
every example could be tested using simply the free MPLAB® C compiler Student Version. We want
to maintain this commitment. Perhaps there is some room to improve the performance using just a little
ingenuity.
The first thing to do is to discover where the PIC24 is spending the most time when reading data from
the card. Inspecting the f readM ( ) function, you will notice that there are only two calls to lower-level
subroutines. One is a readDATA ( ) function call used to load a new sector from the current cluster and
the other is a next fat ( ) function call used to identify the next cluster, once every sector of the cur-
rent cluster is exhausted. Eventually both functions will call in their turn the readSECTOR ( ) function
to actually retrieve a block of 5 12 bytes of data. Lastly, a call to the standard C function memcpy ( )
is performed to transfer a data block to the calling application buffer. So the ultimate performance of
f readM ( ) will depend on the performance of readSECTOR ( ) and memcpy ( ) .
LED Profiling
To determine which one of the two has the largest responsibility is a relatively easy job, if you happen
to have an oscilloscope at hand. In fact, if you remember, we designed readSECTOR ( ) to use one of the
LEDs on PORTA to signal when a read operation is being performed on the SD/MMC card. If we point
the oscilloscope on the anode of the corresponding LED during the playback loop, we should be able
to see a periodic pulse whose length is indicating to us the exact amount of time the PIC24 is spending
inside the readSECTOR ( ) function while transferring data. The pause in between the pulses is other-
wise proportional to the time spent inside the memcpy ( ) function and eventually most of the rest of the
f readM ( ) function call stack. In one single glance, you will immediately realize where the problem lies.
READ LED
readSECTORQ
freadQ
Figure 15-10. Pointing the oscilloscope on the READ_LED pin.
There is no doubt that, it is the readSECTOR ( ) function that needs our full attention, since it uses up
the largest part of a period that is more than 10 ms long!
353
Chapter 15
int readSECTOR( LBA a, char *p)
//a LBA of sector requested
// p pointer to sector buffer
// returns TRUE if successful
{
int r, tout;
READ_LED = 1;
r = sendSDCmd( READ_S INGLE, ( a « 9));
if ( r == 0) // check if command was accepted
{
// wait for a response
tout = 10000;
do{
r = readSPI ( ) ;
if ( r == DATA_START) break;
}while( --tout>0) ;
// if it did not timeout, read a 512 byte sector of data
if ( tout)
{
for( i=0; i<512; i++)
*p+ + = readSPI ( ) ;
// ignore CRC
readSPI ( ) ;
readSPI ( ) ;
} // data arrived
} // command accepted
// remember to disable the card
disableSD( ) ;
READ_LED = 0;
return ( r == DATA_START) ; // return TRUE if successful
} // readSECTOR
If you look at the function listing now, you will notice that there are only three possible areas where the
PIC24 could be spending so much time:
1. The sendSDCmd () function.
2. The loop where we wait for the data_start token from the card (perhaps it is just a slow
SD/MMC card?).
3. The loop where we read, one by one, all 5 12 bytes from the card.
354
Volare
To discriminate among the three we can simply change the point where we turn on the READ_LED
and where we turn it off so as to bracket one of three spots. If you recompile the project and run the
test a couple of times, you will notice that when bracketing the sendSDCmd ( ) function the pulse on-
time is reduced to a barely readable blip.
READ_LED = 1;
r = sendSDCmd ( READ_S INGLE, ( a << 9 ) ) ;
READ_LED = 0;
This means the card is very fast to respond to the command and time must be spent elsewhere.
If you bracket the loop waiting for the DATA_START token, you will get a very similar result:
READ_LED = 1;
// wait for a response
tout = 10000;
do{
r = readSPI ( ) ;
if ( r == DATA_START) break;
}while( --tout>0) ;
READ_LED = 0;
It is the third loop, apparently so innocuous but repeated 512 times, that seems to be taking all the
cycles the PIC24 has to spare.
READ_LED = 1;
for( i=0; i<512; i++)
*p++ = readSPI ( ) ;
READ_LED = 0;
Here is where we will have to concentrate all our optimization efforts.
The first idea that comes to mind is to try to remove the function call to the readSPI() function and
replace it directly with the few lines of code required inline:
READ_LED = 1;
for( i=0; i<512; i++)
{
SPI2BUF = OxFF; // write to buffer for TX
while ( ! (SPl2STATbits .SPIRBF) ) ; // wait for transfer complete
*p++ = SPI2BUF; // read the received value
}
READ_LED = 0;
If you patiently rebuild the project and measure the new pulse length, you should already see an im-
provement, but it is not going to make enough of a difference.
355
Chapter 15
Looking under the hood for more
The next natural step for us is to take a look at how the compiler is treating those few lines of code,
peeking at the specific segment in the disassembly listing window:
139: for( i=0; i<512; i++)
011A4 EB0000 clr.w 0x0000
011A6 980750 mov.w 0x0000, [0x001c+10]
011A8 9000DE mov.w [0x0010+10 3,0x0002
011AA 201FF0 mov.w #0xlff , 0x0000
011AC 508F80 sub.w 0x0002 , 0x0000 ,[ OxOOle]
011AE 3C0013 bra gts, 0x0011d6
011CE 90005E mov.w [0x0010+10 3,0x0000
011D0 E80000 inc.w 0x0000,0x0000
011D2 980750 mov.w 0x0000, [0x001c+10]
011D4 37FFE9 bra 0x0011a8
142: {
144: SPI2BUF = OxFF;
011B0 200FF0 mov.w #0xff, 0x0000
011B2 881340 mov.w 0x0000,0x0268
146: while ( ! SPl2STATbits . SPIRBF) ;
011B4 BFC260 mov.b 0x0260,0x0000
011B6 FB8000 ze.b 0x0000,0x0000
011B8 600061 and. w 0x0000, #1, 0x0000
011BA E00000 cpO.w 0x0000
011BC 32FFFB bra z, 0x0011b4
147: *p+ + = SPI2BUF;
011BE 4701E4 add. w OxOOle , #4 , 0x0006
011C0 780093 mov.w [ 0x0006 ], 0x0002
011C2 801340 mov.w 0x0268,0x0000
011C4 784100 mov.b 0x0000,0x0004
011C6 780001 mov.w 0x0002,0x0000
011C8 784802 mov.b 0x0004, [0x0000]
011CA E80081 inc.w 0x0002,0x0002
011CC 780981 mov.w 0x0002, [0x0006]
148: }
. . . <<for loop closing code here>>
011D6
More than 25 instructions are used to perform what seemed a straightforward for loop. Naturally, we
should look at reducing the complexity of the innermost loop, the while loop where we wait for the
SPI peripheral to complete the transfer. While most of that loop seems straightforward, there is a sign-
extension (ze.b) instruction inside it that might appear redundant. It makes us wonder if it is not just a
byproduct of bit field arithmetic used by the compiler to check the SPi2STATbits . spirbf flag.
356
Volare
Reformulating the code using direct masking of the contents of the spi2stat register improves the
situation and confirms the suspicion:
for( i=0; i<512; i++)
{
SPI2BUF = OxFF; // write to buffer for TX
while ( ! (SPI2STAT & 1)); // wait for transfer complete
*p++ = SPI2BUF; // read the received value
}
The code produced is now just one instruction shorter, but keep in mind that that instruction is repeated
at least twice for each of the 512 loops.
139: for( i=0; i<512; i++)
011A4 EB0000 clr.w 0x0000
011A6 980750 mov.w 0x0000, [0x001c+10]
011A8 9000DE mov.w [0x001c+10 ], 0x0002
011AA 201FF0 mov.w #0xlff , 0x0000
011AC 508F80 sub.w 0x0002 , 0x0000 ,[ OxOOle]
011AE 3C0012 bra gts, 0x0011d4
011CC 90005E mov.w [0x001c+10 ], 0x0000
011CE E80000 inc.w 0x0000,0x0000
011D0 980750 mov.w 0x0000, [0x001c+10]
011D2 37FFEA bra 0x0011a8
142: {
144: SPI2BUF = OxFF;
011B0 200FF0 mov.w #0xff, 0x0000
011B2 881340 mov.w 0x0000,0x0268
145:
while (
! (SPI2STAT&1) ) ;
011B4
801300
mov.w 0x0260,0x0000
011B6
600061
and.w 0x0000, #1, 0x0000
011B8
E00000
cpO.w 0x0000
011BA
32FFFC
bra z, 0x0011b4
146: *p++ = SPI2BUF;
011BC 4701E4 add. w OxOOle, #4, 0x0006
011BE 780093 mov.w [ 0x0006 ], 0x0002
011C0 801340 mov.w 0x0268,0x0000
011C2 784100 mov.b 0x0000,0x0004
011C4 780001 mov.w 0x0002,0x0000
011C6 784802 mov.b 0x0004, [0x0000]
011C8 E80081 inc.w 0x0002,0x0002
011CA 780981 mov.w 0x0002, [0x0006]
147: }
. . . <<for loop closing code here>>
011D4
357
Chapter 15
The next trick up our sleeve is to try to reduce the shuffling of data to and from the software stack by
assigning specific registers to hold variables of frequent use. One such candidate is the variable i used
as an index in the for loop, and the other one is the pointer p.
The C30 compiler will let us assign a variable to a register with the following syntax:
register unsigned i asm( "w5");
but the result is not guaranteed unless the specific register is available. Typically the compiler uses the
first four registers W0...W3 as a scratch pad and won't let us have one of those all for ourselves. Also, the
register cannot be a parameter of the function, as unfortunately is the case for p, as this might impact
the register allocation scheme of the calling functions. We can quickly work around such limitations by
copying the contents of p into a new pointer that we will call q, as in the following code:
register unsigned i asm( "w5");
register char * q asm("w6");
q = P;
for( i=0; i<512; i++)
{
SPI2BUF = OxFF;
while( ! (SPI2STAT&1) ) ; // wait for transfer to complete
*q++ = SPI2BUF; // read the received value
}
This time, recompiling the code, we can observe a considerable reduction in the outer loop size and a
simplification in the for loop encoding:
for( i=0; i<512; i++)
clr.w 0x000a
mov.w #0xlff, 0x0000
sub.w 0x000a, 0x0000, [OxOOle]
bra gtu, 0x0011c8
inc.w 0x000a, 0x000a
bra 0x0011a8
{
SPI2BUF = OxFF;
mov.w #0xff , 0x0000
mov.w 0x0000,0x0268
while( ! (SPI2STAT&1) ) ;
mov.w 0x0260,0x0000
and.w 0x0000, #1, 0x0000
cpO.w 0x0000
bra z, 0x0011b2
*q++ = SPI2BUF;
mov.w 0x0268,0x0000
mov.b 0x0000,0x0002
mov.w 0x000c, 0x0000
mov.b 0x0002, [0x0000]
inc.w 0x000c, 0x000c
. . <<for loop closing code here>>
011C8
358
139:
011A6
EB0280
011A8
201FF0
011AA
528F80
011AC
3E000D
011C4
E80285
011C6
37FFF0
142:
144:
011AE
200FF0
011B0
881340
145:
011B2
801300
011B4
600061
011B6
E00000
011B8
32FFFC
146:
011BA
801340
011BC
784080
011BE
780006
011C0
784801
011C2
E80306
Volare
We are down to 17 instructions. The last step will consist of trying to use a different type of loop to
count the 512 bytes of data. This time we will use a simple do loop and we will count backward:
register unsigned i asm( "w5");
register char * q asra("w6") ;
q = p;
i = 512;
do {
SPI2BUF = OxFF;
while ( ! (SPI2STAT&1) ) ; // wait for transfer to complete
*q++ = SPI2BUF; // read the received value
} while ( — i>0) ;
This gives us the best results so far — only 15 instructions all included!
mov.w #0x200, 0x000a
do{
SPI2BUF = OxFF;
mov.w #0xff , 0x0000
mov.w 0x0000,0x0268
while ( ! (SPI2STAT&1) ) ;
mov.w 0x0260,0x0000
and.w 0x0000, #1, 0x0000
cpO.w 0x0000
bra z, OxOOllac
*q++ = SPI2BUF;
mov.w 0x0268,0x0000
mov.b 0x0000,0x0002
mov.w 0x000c, 0x0000
mov.b 0x0002, [0x0000]
inc.w 0x000c, 0x000c
} while (--i>0) ;
dec.w 0x000a, 0x000a
cpO.w 0x000a
bra nz, 0x0011a8
It is time to reprogram the Explorer 16 board with the new code and check once more with the scope to
see how long it takes now for the readSECTOR ( ) function to complete reading a 5 12-kbyte sector of data.
You will be pleasantly surprised to verify that we have now managed to reduce the time required to less
than 1.5 ms. This will be enough to let us play back even the most demanding WAV file and then some.
011A6
202005
141:
144:
011A8
200FF0
011AA
881340
145:
011AC
801300
011AE
600061
011B0
E00000
011B2
32FFFC
146:
011B4
801340
011B6
784080
011B8
780006
011BA
784801
011BC
E80306
148:
011BE
E90285
011C0
E00005
011C2
3AFFF2
011C4
359
Chapter 15
Post-flight briefing
This final lesson was perhaps the ideal conclusion for our learning experience, as we mixed the most
advanced software and hardware capabilities in a project that covered both the digital and the analog
domain. We started using the Output Compare peripherals to produce analog signals in the audio spec-
trum of frequencies. We used this new capability together with the "fileio . c" module, developed in the
previous lesson, to play back uncompressed music files (WAV file format) from a mass-storage device
(SD/MMC card). The basic media player application obtained represents only a new starting point.
There is no limit to the possible expansions of this project and if I have managed to excite your curiosity
and imagination, there is no limit to what you can do with the PIC24 and the MPLAB C30 compiler.
Tips and tricks
The beginning and the end of the playback are two critical moments for the PWM modules. At rest, the
output filter capacitor is discharged and the output voltage is OV. But as soon as the playback begins, a
50% duty cycle will force it to ramp very quickly to approximately a 1.5V level, producing a loud and
unpleasant click. The opposite might happen at the end should we turn off the PWM modules instead
of just disabling the interrupts as we did in the demo project. The phenomenon is not dissimilar to what
happens to analog amplifier circuits at power-on and -off. A simple work- around consists of adding just
a couple of lines of code. Before the timer interrupt is enabled and the playback machine starts, add a
small (timed) loop to gradually increase the output's duty cycle from zero all the way up to the value of
the first sample taken from the playback buffer.
Exercises
1 . Investigate the decoding techniques for ADPCM signals for use with voice messages (see ap-
plication note AN643).
2. Search for all the "WAV" files on the card, and build a "playlist."
3. Implement a "shuffle" mode using the pseudo-random number generator and gradually emp-
tying the playlist.
4. Experiment with basic digital filtering techniques to remove undesired frequencies, boost oth-
ers or simply distort sounds and voices.
360
Volare
Books
• Mandrioli, D. & Ghezzi, C. (1987)
Theoretical Foundations of Computer Science
John Wiley & Sons, New York, NY
Not an easy read, but if you are curious about the deep mathematical, theoretical foundations
of computer science. . .
• Leroy Cook, (1990)
101 Things to Do With Your Private License
TAB books, a division of McGraw-Hill, Inc
Links
http://en. wikipedia. org/wiki/RIFF
The RIFF file format explained.
http://en. wikipedia. org/wiki/WAV
The WAVE file format explained.
http://ccrma.stanford. edu/courses/422/projects/WaveFormat/
Another excellent description of the WAVE file format.
361
About the Author
Lucio Di Jasio received his MSEE (Summa cum Laude) from the University of Trieste, Italy in 1990,
presenting a thesis on the "Simulation of digital logic circuits using the Occam model of parallelism."
After graduating, he worked as a software/hardware designer on projects as diverse as Parallel C
digital-image processing in industrial-automation applications, Unix C/4GL programming in Super-
visory Control And Data Acquisition (SCADA) applications, and encryption for security systems in
automotive applications.
He joined Microchip Technology in 1995 as a Field Application Engineer covering the South of
Europe. In 2000, he moved to Chandler, AZ, and specialized in the KEELOQ® secure-data product
line, publishing several application notes.
In 2002, Lucio moved into a Product Marketing position, supporting the definition and launch of the
new High Pin Count and High Density families of PIC microcontrollers. Since 2005, he has been in
charge of the Application Segment Group, a cross-divisional team of engineers that develops and pro-
motes Microchip's solutions across a wide range of application segments, including: utility metering,
intelligent power conversion, motor control and lighting applications.
Lucio earned his private pilot license in 2002, and an instrument rating in 2005. He has accumulated
350 hours of experience in various single engine airplanes. Lucio owns a Cessna 172 (N75816), which
he tries to fly as frequently as possible to escape the heat of the Arizona summer.
363
Index
Numbers
101 Things to Do With Your Private
License, 361
4th rising edge, 164
8080A Bugbook, Microcomputer
Interfacing and Programming, 40
8- or 16-bit bidirectional data path,
131
8-bit bus interface (using PORTE
pins), 132
16-bit transfer mode, 105
16-bit timers, 55
16th rising edge, 164
ABuffer, 344-345,347-350
Accessory, xiii, 110,216,283,302,
321
Functions, 319
Accounting, 271
Adams, N. 28
Adaptive Differential Pulse Coded
Modulation, 338
ADC, 10-11, 141-146,148,
150-153, 155
ADC FIFO, 155
ADC1BUF0, 145-146, 151
Adding Linker Script, 18
AddNewFiletoProject, 18
Address Latch, 131
ADON, 144-145,151
ADPCM,338, 360
Advanced LCD control, xi, 127, 136
AEmptyFlag, 344-350
Aircraft Owners, 68
Airplane, 4, 40-41, 53, 68,89, 127,
136-137, 161,263
Alarm, 64, 67
Alert, 113, 167, 275,347
Algorithms, 46, 51,236,284
Algorithms + Data Structures =
Programs, 83
Alt-key, 190,194
Alternate Interrupt Vector Table, 78
Alternate IVT, 65
ALU, 38, 42, 51
Analog, xi, xiv, 10-11, 14, 55,
64,141,143-148, 150-155,
329-332,334-335,360
Analog Comparators module, 55
Analog-to-digital conversion
(ADC),xviii, 11, 144,147
Analog-to-Digital Converter,
(ADC), 10, 14,55,65,141-142,
149-150, 154-155,163, 177
Analyzer, vii-viii, 17, 24-26, 29,
36-37, 168,189-190,218-221,
259,330
Anderson, R, 199
Animation, 21,24, 38, 260
Animation/Real Time Updates, 21
Animation Step Time, 21
Another method - Change Notifica-
tion, 176
ANSI, 15,42, 51, 118,125,
139-140
ANSIC, 15,42,51
ANSI X3., 118
ANSI90, 15,70
ANSI90, 15,70
Apple, 326
Application Note, 140,360
ARC, 310, 315
Archive, 287,310
Arithmetic and logic unit (ALU), 38,
42
Arrays, 33-34, 36-38, 70-71,
74-77,79-83,190-191,195,
211,217,222,234,242-245,
248, 252, 254-255, 260, 280,
289,310,344
Arrays Before, 33
ASCII,
ASCII Setup, 115-116
ASCII-printable, 288
ASICs, 92
Asynchronous serial interfaces, 91
365
Index
Asynchronous Serial Communica-
tion, xviii, 89, 92, 109, 111, 256
Audio PWM, 341,347, 349-352
audiopwm.c, 347, 350
audiopwm.h, 349-350
Automatic sampling timing, 145
AVdd, 144-145, 151
AVss, 144-145, 151
Axelsonjan, 125,281
B
Baker, Bonnie, 155
Baker's Dozen: Real Analog Solu-
tions for Digital Designers, 155
Basic Root Directory Entry, 287
Basic WAVE, 340
Baud Rate Generator, 112-113
BCD, 151
BenthamJ., 140
BF, 130
Bill Gates, 284
Binary, xvii, 6, 27, 59,83, 146, 149,
151,223-224,299
Bits,
Bit BLock Transfer, 243
BitBLT, 243
Bitcount, 166-167
BKSP, 195-196
Block Read, 267, 269
Block Write, 267, 269, 278
Boolean, 19,27,30
Bowling, Steve, xv
BPtr, 347
Break Code, 194-197
Breath-alizer, xi, 141, 153
Bresenham, Jack E., 228
Bresenham algorithm, 228
Brown, G., 68
Buck, B., 107,328
BUF, 119
Building a simple console library,
116
Building the first project, 7
Building the video module, 214
Busy-flag (BF), 130, 134
Byte-rate, 352
C
c,
C Learning, iii
C programming, xvii-xviii, 12, 15,
24, 27-28, 79, 363
C-compiler, 9
C Programming Language, 12, 15,
28
C30 Compiler Student Edition, 44
CAL, 67
Calendar, ix, 53, 55, 64-65
CapsFlag, 196-197
Card,
Card Detect, 265-267
Card Select, 266-267, 271-272
Cartesian, 223
CB, 109
CD, 267, 321-322,332, 339,343
CD-ROM, xviii-xix, 4, 17,44, 243
CDC, 124
Cessna, 127,363
CF-I,92
CGRAM Address, 130, 136
Change Notification (CN), xii, xviii,
55,161,176-178, 180-181,
189,192,197
Channels, 25,36, 89, 143-144,
146,150-151,189 218,221,
259, 340-342, 345-347, 352
Channel Selection Dialog Box., 25
CHAR_BIT, 51
Character Generator, 128-129, 136
Checklists, vii, xix, 6-7, 36, 39, 41,
101,136,153,313,325,339,
351
Chip On Glass (COG), 128
Chip Select (CS), 94, 105,131,265,
269
Chunks, 339-340, 343
Chunk Data Size, 340
Chunk ID, 339-343
Chunk Size, 339-340
Circular, 107, 190-191
Clear To Send, 111-113
Clock-polling, 182-183,185
Closing a file, 308
Closing a file, second take, 3 1 7
Cluster, 284-286, 288, 293, 295,
297, 300-302, 304, 306, 309,
313,315-317,320-322,353
CMD,
CMDO, 267, 270
CMDO, 267, 270
CMDO-RESET, 268
CMD1,267, 270
CMD17, 272
CMOS, 14,264
CN, 176-177, 179
CN Interrupt, 176, 179
CN1 1, 177, 180-181
CN11 Change Notification, 180
CNEN1, 176-177
CNEN2, 176
CNIE, 177
CNIF, 177,179
CNInterrupt, 178
CNPU1J77
366
Index
CNPU2, 177
Cockpit, 52, 89, 127,161
Cockpit, a Flight of Escape and
Discovery, The, 52
Code Size, xiii, 49, 65,81,283,326
Cook, Leroy, 361
Communication, x-xi, xviii, 1 2, 55,
64,89-95,107, 109-111,114,
119,124,162-163, 198,212,
256,264,266-269,312,323
Communication CRC, 269
Communication Device Class, 124
CompactFlash, 92, 131,263
Compiling and linking, 6
Completing the interface: adding a
FIFO buffer, 190
Completing the interface: perform-
ing key codes decoding, 194
Completing the SD/MMC card
initialization 270
Complex Data Types, ix, 51-52
Compression, 338, 340
Concord, 127
Conditions, 53, 57,98, 107, 138,
141,166,183-185
Configuring the PMP for LCD mod-
ule control, 132
Cost and efficiency of the solution,
188
Cons, 92,327
conu2.c, 118-120, 123,312,
323-324
conu2.h, 118-120, 123, 138,311,
323
CONU2 Test, 119
CONU2test.c, 119-120
Cost,xii,38, 53, 73,92, 110,154,
161,181,188,264,327
CPU, 54, 57, 105, 124,144-145
CPY, 293-294
CRC, 55, 268-269, 272-275, 354
CRC generator, 55
CRC of RESET, 268
CS,92, 94-96, 98, 102, 270
CSEE, 94,96-101
Creating the fileio module, 308
Ctrl, 43-44, 194-196
CTS/RTS, 111,114, 118, 121
CurBuf, 347-350
Cursor, 7,9-10,37-39,44,48,61,
75,97-98,115, 118,129-130,
134-135, 137,139-140, 148,
153,243-246,255-256,259
Customer Support, iv
Cycle Count, 48-49, 259
Cyclic Redundancy Check (CRC),
268
D/A,xiv,31,92, 293,303, 329,334
Dark Screen, xii, 201, 221
Data length (DL), 130
Data memory, 71-73, 77, 79-81,
99,103,131
Usage, 79
Data structures, 82-83, 284, 289,
351
DB,
DB0, 129-130
DB1, 129-130
DB2, 129-130
DB3, 129-130
DB4, 129-130
DB5, 129-130
DB6, 129-130
DB7, 129-130
DC, 330-331, 334
Deactivate, 96
Debug, 21,25,32,48
DEC, 37, 78-79, 256, 359
Decrement, 29, 32, 37, 106, 130,
185,207,215,250
Decrement Ktimer, 185
Define PS2CLOCK, 165, 177
delay.c,312, 324
Delayms, 277-278, 311-312,323
DEST,82,305, 307,310, 326
DESTTXT, 326
Developing a demo, 146
Developing a game, 147
Developing a text page video, 247
Development Environment MPLAB
SIM, 4, 17
Di Jasio, Lucio, iii, 363
Digital designers, 155
Disassembly Listing, 13, 43-44,
46-47, 69, 76-77, 356
Disassembly Listing Window,
13,44,46-47,69,76-77,356
Disconnect, 115, 124, 144
Display on, 129-130, 134, 149
Display profile, 175
DL, 130
DMM, 15
Do Loops, 30
DOS, 284, 326
Double-precision, 46-47, 48, 49-50
Drzewiecky, Joe, xv
DVD, 202
Eady, Fred, 107,125
Echo, 114-115, 117
EEPROM,x, 89,94-104, 212, 263,
367
Index
265, 269
Embedded,
Hosts, 281
Hosts Lakeview Research, 281
Multitasking, 68
Systems, 125, 140
Systems CMP Books, 140
Systems Designers, 125
Embedded-control, xv, xvii-xviii,
3-5, 12,14, 17,21,29,38,
41,46,49,53,64-65,68,81,
89-90, 104,110, 124,140-141,
263-264, 327
Embedded Multitasking, 68
EOF, 306, 309,321
Erase, 137, 269, 280
Erase Reset, 269
Erasing, 327
Error, 50, 55, 57, 71,82, 112,124,
166,184-185,211,228-229,
269, 277-279, 289, 297-300,
302,305-306,309,313-315,
317-318,320,326,338,342
ESC, 118,195-196
Evaluating cost, 181
Example of PWM, 330
Execute, 5, 9-10, 18, 20,31,37,
44,48,59,61,63,66,75,97,
105,115,134, 181,183, 185,
188-189,208
Execute Run, 10,61,63,75
Execution, 20-21,24, 36, 38,
44, 48-49, 54,
57,62,65,72,82,97,120,
124-125,
129-130,138-139,172,
175,189,243,
277-279, 307, 309
Exercises,
Consider, 327
Develop, 83, 107
Experiment, 281
Exit, 58, 106,175, 189,244,
302-303,305,307,317-318,
320, 334-336, 347
ExitClose, 318
EXP, 51
Experiment, 281,324, 335, 360
Explorer16,
Explored 6 LCD, 136
Explored 6 POT, 143, 151
Explorer 16 POT, 143, 151
Explorerl 6 User Guide, 142, 177
Extension, 51,54, 170-171,287,
298-299, 304, 339
External, 14, 27,39, 54-55, 105,
131,144,177,212,222
External pins connected to the
Change Notification
module, 55
External pins with level trigger
detection, 55
Extracting, 50, 91, 101,339,343
F
F keys,
F1, 47-48, 195-196
F2, 47-48, 195-196
F3, 47-48, 195-196
F4, 195-196
F5, 195-196
F6, 195-196
F7, 196
F8, 48, 195-196
F9, 61,195-196
F11, 195-196
F12, 195-196
FAA, 283, 329
Failure, 57, 107,278,299,305,309
FCY, 348, 350
FError, 289-290, 292, 297-300,
305-306,309,313-316,320,
351
Fetch, 72, 114, 165,173, 180,188
FF, 269
FIFO, xii, 105, 113-114, 139,
143,155,161, 164,190-192,
197-198,212-213,252,260
File Allocation Table (FAT), xiii,
283-286, 285, 328
File Type, 77, 290, 341
FILEIO,xiii, 283, 296, 308-309,
311-313,323-324,341,
350-352, 360
fileio.c, 308, 311-313, 324,
350-352, 360
FILEIO error codes, 309
fileio.h, 308, 311,323,341,
350-351
First Cluster, 285-286, 288, 297,
300-301,309,313,315,
320-321
First Flight, vii, 3, 28, 87, 142,201
First Partition, 291—292, 296
Fix, 69, 342, 345, 347-350
Flight of Escape, 52
Flight of Passage: A Memoir, 1 07
Flight Service Station (FSS), 283
Float, 31, 46-48, 51, 227, 231, 234,
237-238, 240
float.h, 51
Floating point, 46
Floating point unit (FPU), 47
Floating-point data types, 46, 51
368
Index
FIT, 51
FLT_MAX_EXP, 51
Fly, iii,xvii, 17, 28,41,73, 76, 87,
107,119,137,230,247-248,
329,363
Flyers, 28
Flying Carpet, 68
Flying the Mountains, 199
Flying the PIC24,xix, 135, 137,159,
221,246,265
FlyingthePIC24.com, xix, 159,
221,265
FMT, 340-343
Font, 130, 242-254, 258
Font8x8, 242-244, 248, 252
for loops, 31
FPtr, 248-251,251,254
FPU, 47
Fractals, xii, 201, 236, 260
Frames, 202-203
FSS, 283
Functions, xii, xiv, 5-7, 9, 1 1, 14,
18,24,26-27,30,39,44,46,
49, 52, 54, 58-59, 64-65,
77,80-82,93-95, 100-102,
106,113-117,120, 123,130,
133-136, 138-140,144, 147-
149,172,192, 195-196,199,
201,211,217,219,222-226,
229-236, 243-245, 249, 259,
261,267-272,274,277-279,
288-290, 295-297, 299-303,
305-308,312-323,326,329,
336,341-342,344-347,349,
351-355,358-359
Future, 7, 26, 39, 49, 58, 104,109,
116,144,162, 167,170,249,
301,308
Gahlinger, P. M., 52
Generate SCL, 181
Generating Luminance, 205
Generating the composite video
signal, 204
Generator, xii, 55, 112-113, 120,
128-129, 136,147-149, 153,
169-171, 174,180,201,206,
212-213,217-218,220,222,
224-226,229,247,331,360
Ghezzi, C, 361
Glass bliss, xi, 127
Global positioning system (GPS),
127
Going long, 44
GPS, 127,161,282
Graphic, 92, 128, 136,214,
217-218,222-226,228-231,
233, 236-237, 239, 242-243,
245-248,252-253,259-261
Page, 243, 245, 247, 253, 260
Test, 218
Test2, 222
Test3,225
H
Halt, 26, 175,218,278-279,349
Hash Table, 83
HCount, 214, 216, 251,254
HD44780 controller compatibility,
129
HD44780-compatible, 131,136
Header Files, 102 # 349
Heap, ix, 69,80-81,289, 295, 298,
313,317,325,351
Heat, 153, 363
Hello.c, 4-6
Hello Embedded World, 4, 8, 12
Hello World! exercise, 15
Hex, 6, 103, 291,293
HIDE, 99,310
Host, 162-163, 190, 194,198,268,
327
Host USB, 327
HState, 214, 254
HW, 111,118
Hyperion, 107
HyperTerminal,
Connect, 115
Disconnect, 124
Properties, 1 1 5
/
l 2 C synchronous serial interfaces, 90
l/D, 129-130, 237
l/Os, 128, 131,149, 187,211,264,
266, 277, 288-289
IBM, 161-162, 194,228,236,239,
247
IBM Pacific Northwest Labs, 236
IBM PC, 161,194, 239, 247
IBM PC-XT, 162
IC, 171,181
ICPS2, 171
IC1 Interrupt, 165, 168
ICMC5, 164
IC1CON, 165
IC1IE, 165
I C 1 1F, 165, 168
IC1 Interrupt, 167
ICD2 Debugger, 97, 103,109,
114, 120,124, 128,136,142,
313,325,334
ICD2 Debugging Checklist, 15,
147
369
Index
ICE,x, 124
ICxBUF ; 164
ICxCON, 164
ID, 339-343
IdcService, 107,140, 155
IDE, xv, 3-4, 6-8, 10, 13, 17-18,
42,52-53,62,69,89, 109,128,
142,170,309,314-315
IDE Integrated Development
Environment, 3, 6, 17
IDLE, 22,94, 163,170,184,
266-267, 269-270
IDs, 340-341
IE, 55
IEC, 118
IEEE754,46
Illegal, 269
Image serialization, 211
Implementing 802.11 with Micro-
controllers: Wireless Networking
for Embedded Systems Designers,
125
In Circuit Debugger, 89, 97
In The Blue, 329
Increment Bitcount, 166
Infrared, 110
Init Bitcount, 166
Init Parity, 166-167, 178,186, 193
Initialize, 22, 32, 34, 36, 44, 59,
67,70,102, 113-114,118, 121,
132,146-148,151-153, 173,
206,210,217,222,225,248,
277-278,288-289,314,332,
335,341,345
Initialized, 38, 59,64, 70-71,
75-77, 80, 82, 266, 274, 279,
348
InitNVM, 100, 102
Input Capture modules, 55
Input Capture, 163
Insert Data, 184-186
Inspecting, 46-47, 78, 343, 353
Instruction, 18, 20, 37, 39, 43-44,
49,54,61,66,72,81,95, 124,
129-130, 133,154-155, 175,
350,356-357
Instruction Code Description
Execution, 129-130
Integer, 19-20, 29, 31,36-37,
42-51,59,70-71,79,99-100,
102,112,120, 143-144,147,
149,154,227-228,243,249,
268, 278-279, 285, 288, 300,
305,335,344
Integrated Development Environ-
ment, 3-4, 6, 1 7
Intel, 40
Interface Bit Timing, 165, 177, 182
Interfacing a PIC24 to the PS/2, 163
Interfacing to the Explored 6 board,
265
Internet, 140,283
Internetworking, 107
Interrupt,
Interrupt Enable, 55, 67, 177,
187,333,345,348
Interrupt Flag, 56, 58-60,
63-64,67, 165,168, 177,
179, 184,186-187,194,207,
209,212,216,250,252,333-
336, 347-348
Interrupt Service Routine (ISR),
54,189
Interrupt Vector Table (IVT), 54,
65,78
InterruptVector, 54-55
Invalid, 290, 292, 299, 305,
308-309,316
Invent, 40
Investigate, 41,69, 162, 190,360
Investigating Memory Allocation,
ix, 69, 73
IO, 68, 276
IP, 57-59,61-62, 140, 213
IPO, 62
ISA, 131
ISO, 118
Isometric, 232-233, 235
ISR, 54-55, 58,60,63-64, 78, 167,
178,183,185, 189,192
IVT, 54, 57, 65, 78
J
Java, 79
K
K&R, 15, 153-154
KBD, 167,173, 179-180, 187-188
KBR, 191-193
KCB, 191-193
Kernighan, 3, 15
Keyboard, xii, 114,117,161-164,
167,169,172-173, 176-177,
179-182, 184-185,187, 189-
190,193-195,197-199,261
Kitty Hawk, 28
Koster, R., 261
Kstate, 183-186,193-194
Ktimer, 184-186, 193-194
Landings, 17,141, 159
Large Memory, 38, 81,264
Larsen, D., 40
370
Index
LATB, 1 1
Latch, 11, 97-98, 101, 131
Lawrence, 140
Lawson, Eric, xv
LBAs, 272, 294
LCD,
LCD Busy, 134
LCD.c, 139
LCD.h, 138
LCD Module, xi, 127-129,
132-136,138
LCD RAM, 134,136
LCDaddr, 134-135
LCDbusy, 134-135
LCDclr, 135
LCDcmd, 133, 135-137
LCDDATA, 133, 135
LCDhome, 135
LCDinit, 133,135
LCDread, 134-135
LCDsetC, 137
LCDsetG, 136
LCDsetG, 136
LDP, 328
LED,
LED Profiling, xiv, 329, 353
LEDO, 15
LEDgame, 148
LEDgame.c, 148
LFN, 310
limits.h, 51, 120
LIN, 92, 110
Line drawing, 226
Line Testl, 226
Linux, 284, 326, 328
Linux Kernel, 328
Liyanage, M., 28
Logarithms and exponentials, 51
Logic,
Logic Analyzer Set-up, 26, 36
Logic NOT operator, 19
Logic OR operator, 19
LONG,
LONG_DELAY, 34-35, 37
Long File Name, 310
Long Integer, 31,37, 44-45,
48, 268, 288, 300, 344
Long Long Multiplication, viii, 41,
45-46, 48
Long-integer, 49
Long-Long Integer, 48
Looking at the MAP, 77
Looking under the hood for more,
356
Loop, vii-viii, 17-21,23-24, 26-38,
58-61,63,80,95,97, 103,114,
116-117, 119,121-122, 135,
137,146-149,151-154, 167,
173,175,180-181, 188,208,
212,218,220,222,225-227,
229-231,235,238-240,245,
257-258, 268, 274, 277-279,
303,305-307,312,316-317,
319-320,324,334,345,351,
353-360
Loop in the pattern, vii, 1 7-1 8
Low level audio routines, 347
LPtr, 251,254
LSB, 50, 52, 59-60, 98-101,136,
149,166,170, 173,218,245,
268,322
M
Make Code, 177,194-195
Malloc, 80, 289, 298, 308-309, 325
Managing multiple interrupts, 64
Mandelbrot, Benoit, 236
Mandelbrot Set, 236-239
Mandrioli, D., 361
MAP, ix, 53, 69, 77-80, 161,195,
211-212,217-218,222-225,
227, 244-245, 247-248,
255-256, 293-294, 296
Mask, 67, 143-144, 147,183,224,
267,287,310
Master, xi, xviii, 55, 90, 92-94,
96-97,99-100,105, 127-128,
131-133, 138,159,212-213,
266, 290, 292, 332-333
Master boot record (MBR), 290, 292
Master Mode, 94, 96-97, 99-100,
132-133,212,266
Math, ix,xii, 46, 51-52, 57, 147,
201,211,230-231,233,235,
336
math. h, 51,231,233
Matrix, x, 109, 120,256
Matrix Reloaded, 256
Matrix2, 256
Max Bit Rate, 92
Max Bus Size, 92
MBR, 290-291,309
McCarthy, Guy, xv
McDonald, Marc, 284
MCP,
MCP251XCAN, 92
MCP2550 IrDA, 92
MCP320XA,92
MCP322xA, 92
MCP809, 105
MCP98XX, 92
MCU,vii, 14,22,64,66, 138-139,
155
Measuring performance, 220
371
Index
Measuring temperature, 149
Media, xiv, xviii, 89, 109,264,
271,279-281,284,287,289,
293-298, 302-303, 305,
309-313,315-316,318-320,
329,339,360
Media Player, xiv, 329, 339, 360
Memoir, 107
Hyperion, 107
Memory,
Memory Allocation, 73
Memory Space Allocation, ix, 69,
71
Memory Stick, 263
Memory Usage Gauge, 13, 326
MFILE, 296-298, 300-305,
308-311,313-323,325,342
Mi, 329
Microchip, xv, xvii, xix, 4, 17-18, 87,
107,124,140, 149,155,363
Microchip Application Note, 140
Microchip Explored 6, 87
Microchip TC1047A, 149
Microchip TCP, 140
Microchip Technology, xv, xix, 363
Microcomputer Interfacing, 40
Microcontrollers, i, iii, xvii, 3, 9, 1 2,
14,21,27,38,51,63,107, 125,
128,198,264,363
Microsoft, 110-111,284,287
Disk BASIC, 284
Windows HyperTerminal, 1 1 1
Windows, 110-111
MIDI, 343
Migliacci, Franco, 329
Milky Way, 226
MIPS, 175
IC Card, xiii, 263-265, 267,
269-271,274,276,281,283,
294,308,311-312,325,327,
339,351-354,360
MMC-compatible, 264
MMCA, 264, 281
MNTD, 297, 309
MOD16,94
Modugno, Domenico, 329
More.c, 30
More loop examples, 32
Move, 15,43, 117-118, 129-130,
152-154, 168,178-179, 186,
193,244,307
Moving, xvii, 34-35, 147, 149,
151,161,202,352
MP3, 263
MPLAB, ix, xv, xviii-xix, 3-8, 10,
13-15, 17-18,21,24,26,
30-31,36-38,41-42,46-54,
57,62,65,69-74,76-81,89,
97,103-104,109, 120,124,
128,142,168, 170-171,175,
180,189,218-219,258,280,
353,360
MPLAB IDE, xv, 3-4, 6-8, 10,
13,17-18,42,
52-53,62,69,89,109, 128,
142,170
MPLAB IDE Integrated Develop-
ment Environment,
3,6,17
MPLAB C, 4, 6,31,353
MPLAB C30 Compiler Suite, 4, 18
MPLAB C30 Memory Models, ix,
69,81
MPLAB C30 User Guide, 42, 65
MPLAB ICD2, 37, 89,97, 109,
124,128,142
MPLAB ICD2 Debugger Set-up, 97
MPLAB ICD2 Device Configuration,
37
MPLAB ICD2 In Circuit Debugger,
89
MPLAB ICD2 In-Circuit Debugger,
109, 124,128, 142
MPLAB ICD2 Programming, 37
MPLAB ICD2 Set-up, 37
MPLAB IDE Build, 7
MPLAB IDE Editor, 8, 170
MPLAB IDE Memory Usage
Gauge, 13
MPLAB IDE Output, 8
MPLAB IDE Project, 8
MPLAB IDE Watch, 8, 10
MPLAB Integrated Development
Environment, 4, 6, 17
MPLAB Link30, 80, 104
MPLAB Memory Gauges, 280
MPLAB SIM, xviii, 4, 7, 17, 21,
24,30,36,41-42,47,49,
53,69,73,89,103,109, 128,
142, 168,171, 175,180,189,
218-219,258
MPLAB SIM Debugger, 7,
175
MPLAB SIM Set-up, 36
MPLAB-SIM, 24
MSb, 50,93, 98-101,147, 170,
223-224,245,268,322,352
MSSP, 105
Multi Media Card Association
(MMCA), 264, 281
Multiplication Test, 48
Multiplying, 45, 49, 228, 272
N
372
Index
N-order,331
Nesting, ix, 53, 57, 65
Nesting of interrupts, 57
Networking, 125
Networking and Internetworking
with Microcontrollers, 107
New PIC24, xvii-xix, 10,89
New Project, xiii, 4, 18,30,42,53,
60,69,90,99,111, 120,133,
143,148,150, 165,180, 188,
212,217,223-224,229,243,
255-256,263,266,312-313,
324-325,339,350
New Scenario, 174, 181, 188
New Workbook, 169, 180
Nguyen, Thang, xv
Nonvolatile Storage, x, 82, 89, 99,
263
Nonvolatile storage library, 99
Nop, 67, 103
North Star Over My Shoulder, 328
Note on the multiplication of long
integers, 45
NOT-equal, 19
NSTDIS, 57, 65
NTSC, 68, 202-206, 210-211,
213-215,217,221,251-253,
260
NTSC-VRES, 206, 214, 253
NTSC Video, 68, 204-206, 211,
214,217,221,252-253,260
NUM, 162,195-196
Num Lock, 162
NUMB3RS, viii, 41
Number of Pins, 92
NVM,x,89, 99, 101-103, 106-107
NVM Access Library, 99
NVM.h,99, 102
NVM Library, x, 89, 99, 102-103
NVMKEY, 66
NVMtest, 103
NVMtest.c, 103
OC, xiv, 329, 332, 347
OCFA, 332
OCFA/OCFB, 332
OCFB, 332
OC1,
0C1C0N, 333,348
OC1R, 333,348
OC1RS, 333, 335-336, 347-348
OC2, 347
OC2CON, 348
OC2R, 348
OC2RS, 347-348
OC3, 209-210, 213, 216
OC3IE, 213
OC3IF, 209, 213
OC3IP, 213
OC4 Interrupts, 213
OC4IE, 213
OC4IF, 212-213, 250
OC4lnterrupt Notice, 212
OC4lnterrupt Remember, 259
OC4IP, 213
OC4R210, 212, 215, 250-251
OCM, 209, 332
OFFS, 242-243, 254
Offset, 80, 149, 151,224,234-235,
242, 248, 250, 290-292, 294,
303,314,317-319,336,
339-340, 348-349
OP, 267
Open-Drain Output Control, xii, 198
Opening a file, 296
Operations, xviii, 32, 41-42, 44, 47,
49-50,52,71,81-83,105,124,
129-130,224,267,271,281,
327
Optimizing, xiv, 45, 53, 329, 353
Optimization (or lack thereof), 43
Optimizing the file I/O, 353, xiv
OS-X, 326
Osc, 25, 48
Output,
Output Compare, xii, xviii, 55,
201,208-210,212,214,216,
260, 330, 332-333, 348-349,
360
Output Compare modules, 55
Package, 149,172, 179
Packing, 263
Page Write, 98, 101-102, 105
PAL, 202-203
Parallel master port (PMP), xi, xviii,
55,92,127-128, 131,138
Partition, 290-296, 309
Partition Boot Record, 292-294
Partition Table, 290-292
Pascal, 70, 83
PCB, 92, 221,265
PCB Interface, 92
PCH, 5, 7
PCL, 5, 7
PDAs, 326
Performance, viii, xii, 41-43, 46-49,
51-52,64,69,72,80-81,107,
110,120,122, 124,141, 167,
197,201,220,230,242,247,
256,263,280-281,307,327,
352-353
373
Index
Physical Interface, xiii, 162,
263-264
PI, 79-80, 230-231,233-234
PIC,
PICMCU,vii, 14
PIC16, 14,27,38
PIC18, 14,27,38
PIC24,
PIC24 application, 281
PIC24 1, 9
PIC24 0C,xiv,329,332
PIC24 Output Compare, 332
PIC24 PWMs, 352
PIC24 PWMs, 352
PIC24 RAM, 79
PIC24 SPI2, 94
PIC24Timer1,63
PIC24UART, 110,163
PIC24UARTS, 113
PIC24FJ, 14
PIC24FJ128GA,9
PIC24fj128ga010,xviii,4, 11,18,
21,55-56,64,
72,81,90, 141,164, 171,181,
206,208, 211,
247, 260, 326
PIC24FJ128GA010 Program
Space Visibility, 72
PIC24FJ128GA010RAM, 211
PICmicros, 140
PICTail, 168
Pilots, xvii, xix, 17,29,41,53,
68-69,89,109, 127,161,263,
283
Pilot Operating Handbook (POH),
41
Pilots Association, 68
Private Pilot Manual, 15
Pin/Register Actions, 169
Pixels, 128,211-212,224,252,
260-261
PlayO function, 341
Plotting, xii, 161,201,223,
225-226, 230-233, 242, 260
Plotting math functions, 230
PMP,xi, 92, 127-128, 131-135,
138,177
PMPBusy, 134
POH, 41
Poi, 329
Point-to-point, 92, 109-110
Pointers, ix, 69, 79-83, 190-191,
211,250,260,298,301,
306-307,316-317,348
PORT,
PORT initialization, 9
PORTA,
PORTA = KBDCode, 173, 180,
188
PORTA = Sec, 60-61
PORTA I, 35, 149
PORTA l/Os, 149
PORTA LEDs, 35, 277, 279, 353
PORTA LSB, 59-60, 173,218
PORTB,vii,3, 10-11,14-15
PORTD, 94, 131, 164,334
PORTE, 131-132
PORTF, 213
PORTG, 177, 183,185, 192,198
PORTx, 198
Post-equal, 206
POT, 143,147-148, 151
Practice, 17, 21,29,43,45, 73, 81,
91,96,117, 141,159, 177,183,
201,203,206,211,248,252,
274, 279, 296, 329
Pre-equal, 206
Pre-flight, xi, 128
Print, 116,119-121,243,245-246,
257
Printing, 116,243,246
Private Pilot Manual, 15
Processor Reset, 39, 44, 57
Producing, xiv, 14, 104, 123,
203, 205, 229, 252, 259-260,
329-331,335-336,360
Producing analog waveforms, 335
Profiling, xiv, 41,329, 353
Program Files, 18
Program Memory, 14, 38, 65, 69,
71-73,75,77-79,81-82, 131,
195,243,247,263,280,326,
336,338
Program Memory Usage, 78
Program Space Visibility, ix, 69, 72
Programming, i, iii, xvii-xix, 3, 1 2,
15,17,24,27-29,32,37,40,
79,83,89,97,104, 125,136,
201,281,313,325,363
Devices, 281
ProgrammingthePIC24, 159
ProgrammingthePIC24.com, 159
Programs, 17,83, 110, 168,239
Project Source Files, 6-7, 18, 77,
339
Project Wizard, 4, 18,99
PS2, 167, 169,171, 173,177,
179-180, 183-184,188, 195
PS2 Clock, 169, 177,179-180,
183
PS2 Data, 169,179-180,183
PS2KBD, 167, 173, 179-180, 188
PS/2 communication protocol, 163
PSV, 72,81
374
Index
PSVPAG, 5, 7
Pulse width modulation (PWM),
208, 330
PWM,xiv, 64, 208,329-335, 338,
341,343-344,346-352,360
R/L, 129-130
R/W, 129-133, 297, 299, 309-310
RAM,
RAM buffer, 128,136-137,281
RAM-efficient, 248
RAND, 120-121,147-148,
153-154,225,229,256-258
RC, 144
RCA, 221
RCount, 251
RD,92
RD12,94,96, 99
RD13, 240
RD2, 209
RD4, 132-133
RD5, 132-133
RD6, 230, 240
RD7, 240
RD8, 165
Read Busy Flag, 134
Readonly, 192,287,310
Read Status Register, x, 89, 95-96,
99-100
Readers, 77, 203
ReadOddW, 294,310,321
Reading data from a file, 305
Reading data from an SD/MMC
card, 271
Reading the memory contents, 99
Reads, 100, 130
Reads Busy-flag, 130
ReadSR, 100-101
Readiest, 311-312
ReadW, 290, 294, 304,310,
342-343
Read/Write selection line (R/W), 131
Real analog solutions, 155
Real example with Timerl , ix, 53, 59
Real Time,
Updates, 21
Real-time clock, ix, 53, 55, 59,
63-65
Real-time Clock Calendar (RTCC),
ix, 53, 55, 64
Rebuild, 10,98, 189,259,335,355
Rebuilding, 44
Recompiling, 46-47, 221,358
Reformulating, 357
Register Actions, 169
Relative Performance Test Results
Using MPLABC30, 48
Remote-control, 68
Reproducing voice messages, 338
Reprogram, 98, 103,359
Request To Send, 111-112,114,
118,163
RES, 293-294
Reset,
Reset Profile, 175
Restart Ktimer, 185, 193
Retesting PORTA, vii, 3, 10
Returns, 3, 11, 19,30,62,80, 120,
129-130,273-274,278,302,
305,315,354
Right Arrow, 61, 195
RISC, xvii, 69
Rising Edge, 164,183-186, 190,
194,208,269
Ritchie, 3, 15
RO,310
ROM, 128,136
Rony, P., etal., 40
Root Directory, xiii, 283, 286-287,
294-295, 299-300, 302-305,
309,313-315,318-319
Rounding, 51, 112
Rounding functions, 51
RS, 129-131, 133
RS232, 92, 109-112, 119, 122,
311
RS232-to-USB, 111
RS422, 92
RS485, 92, 110
RTCC, ix, 53,64-67, 277, 314,327
RTCCInterrupt, 64
RTOSs, 27
RTS, 111, 113-114,118, 121
Run To Cursor, 9-1 0, 37-38, 44, 48,
75,98
Rusling, David A., 328
RX, 91
S/C, 129-130,150, 187
S-Video, 223
SAMP, 144-146, 151, 155
Sample, 144, 146,151-152,
182-183, 185,192,335,338,
340-341,343-345,347,349,
352,360
Sample-rate, 352
Sams, Howard W., 40
Sanderson, Jeppesen, 15
Save,
Save As, 4, 242,311
Save Workbook, 170,181
SBS, 170, 181
375
Index
Scan Code Set, 194-195
Scenario, 103, 174-175, 181,188,
259
SCK, 93
SCK/SS, 213
SCK2, 266
SCL,
SCL Builder Setup File, 171
SCL Generator Window, 169
SCL Generator Workbook, 171
SCLGenerator, 169, 180
Screenshot, 62
SD,xiii, 263-271, 273-283, 290,
294,308,311-312,325-327,
339,351-354,360
SD/MMC,xiii, 263-265, 267,
269-271,274,
276-281,283,290,294,
308,311-312,
325-327,339,351-354,360
SD Card, xiii, 263-271,
273-274, 277,
280-281,283,308,311-312,
325,327,351,353-354,360
SDA, 90-91
SDI, 90,93, 212, 265-266, 268
SD/MMC card physical interface, xiii,
263-264
sdmmc.c, 275, 279, 288,312,
324,326,350
sdmmc.h, 276-277, 279, 308,
311,323,341,350-351
SDO,90, 93, 212, 214, 253, 265,
269, 274
SECAM, 202-203
TV, 203
Secondary oscillator, ix, 53, 63-65
SECT, 291-292
Sectors, xiii, 274, 276, 283-285,
287-289,291,293-296,300,
306,309,312-313,317,322,
326, 344
Sectors and Clusters, 284
Secure Digital (SD), 263-264, 281
Secure Digital Card Association
(SDCA), 264, 281
SEED, 149
Sending and receiving data, 113
Sending commands in SPI mode,
267
Selecting the SPI mode of operation,
267
Serial,
Serial Data Output (SDO), 212
Serial EEPROM, 94-104, 212,
263, 269
Serial EEPROM SPI2CON1 =
SPLMASTER, 97, 100
Serial EEPROM Status Register,
97,100
Serial EEPROMs, 92, 104
Serial port interfaces (UARTs), 55
Serial Interface Engine (SIE), 124,
327
Set,
Set Breakpoint, 61,97-98, 181,
188
SetCGRAM, 130, 136
Set CGRAM Address, 130,136
SetDDRAM, 130
Set PORTA, 59-60, 181, 188,218
Sets, 70, 129-130, 194,202-
203,223
Sets On, 129-130
Settings, 21,25,46,48, 59, 67,
103,112,115, 118,219,289
Setting Pane, 115
Setting PIX, 213
SFR, 5, 7,44,61,81-82, 175, 249
Sharing, 91,265, 283
Sheard, Vince, xv
SIE, 124,327
Signal Feature, 205
SIM,xviii,4, 7, 17,21,24,30,36,
41-42,47,49,53,69,73,89,
103,109,128, 142,168, 171,
175,180,189,218-219,258
SIM Stopwatch, 41,47, 49
Simple Network Management
Protocol, 140
Simplified,
Simplified NTSC, 205
Simplified UART, 110
Simulator, xii, xviii, 4, 7, 9, 17,
21,24-25,30,36-37,42,44,
47-49,53,61,64,69,73,89,
109,128,142, 161,168, 171,
175-176, 189,218-219,221,
258
Simulation, 174
Simulator Profile, xii, 161,
175-176
Simulator Stopwatch, 49, 61
SIN, 230-231,335-336
Sinclair ZX Spectrum, 239, 261
Single Precision, 46, 48-49
Sky, 40, 329
Slave, 90-91,93, 105, 131,163,
212,332-333
Slave select (SS), 91, 212
SLF, xviii
Small Data Memory Model, 81
Small library of functions to access
an LCD display, 133
376
Index
Small Memory, 81,211,290
SmartMedia, 263
SMP, 266
Source Files, 4, 6-7, 18,77-78,
101,103,123,279,339
Source Files List, 6-7, 18,339
SPI,x,xiii, 55,89-97,99-100,
103-105, 107,155, 163,
212-213,215,218,223-224,
248, 250-252, 260, 263-268,
270-271,280,327,356
SPLENABLE, 94, 96-97, 99-100,
213
SPI FIFO, 212, 252
SPI-mode SD, 267
SPI-mode SD/MMC Card, 267
SPI synchronous serial interfaces,
90
SR, 5, 7, 57,61-62,65
SS, 43, 45, 91, 107,140, 155,177,
212-213
SSEE,94,97, 100
SSP, 105
SSRC, 145,155
Standard NTSC PAL SECAM Frames,
202
Start Time, 4
Starting a new project, 266
StateO, 183-186, 193-194
KTimer, 185,193
KTimer = KMAX, 185,193
KTimer, 185
Status Register (SR), x, 62, 89,
95-100, 134
Step-Over, 9, 44, 48, 62-63
Stepln, 62
Stepping, 39, 48, 62
Stereo, 339,341-343,345,
347-350, 352
Stimulus, xi, 64, 127, 161, 168-169,
171-172, 174-175,181,
188-189
Stimulus Scripts, xi, 161, 168, 174
StimulusController, 181
StopWatch, 41, 47-49, 52,61-62,
168,219,259
String, 69-73, 75-79, 82-83, 98,
116-119, 121,135-136,245,
257,279,297,307-308,316,
339
Student, xviii, 3-4, 15, 17,41,44,
127,353
Student Version, xviii, 4, 1 7, 353
Subdirectories, 304, 327
Subdirectory, 7, 18,46,287
Subtract, 65
Success, 181,188,252,318,346
Superior life forms (SLF), xviii
Support, iv, xv, 7, 18, 38, 46, 52,
70,83,110, 177,194, 199,261,
283, 299, 308, 327
SXC, 293-295,302,306, 309,317,
322
Symbol, 44, 61,73-74, 97
Synchronization, 22, 40, 90-91,
184,202-206,209,211,218,
221,223,242,248,259-260
Synchronous serial communica-
tion, xviii, 89, 212
Synchronous serial interfaces (SPI
and I2C), 55
SYS, 294, 310
System, iv, 27, 53-54, 104,
109-111, 127,131, 141,161,
163,168,201,246-247,271,
280, 284-288, 290, 292-297,
304-305,310-313,323,326,
328,339,35
T
T1,58
T3, 214, 334-336,347
T4, 184, 186-187,194
Table Read, 77
Taste of C, 1 5
Tclk, 23,35, 152-153
TCP/IP Lean, Web Servers for
Embedded Systems, 140
TCS, 22, 50
Tcy, 121, 132-134, 144,151-152,
154,214,253,257,266,278
Tcycles, 350
Tcy wait data set-up after enable,
132-133
Tcy wait data set-up before read/
write, 132
Tcy wait between R/W and En-
able, 132
TCYxUS,350
Tdelay, 23
Temperature, xi, 92, 141, 149-155,
163
Template and an example forTimerl
interrupt, 58
Test Pattern, xii, 201, 222-223
TestDA, 334
TestDA.c, 334
Testing, 44
Testing a VT1 00 terminal, 118
Testing fopenMO and freadMO,
311
Testing PORTB, 10
Testing the complete fileio
module, 323
377
Index
Testing the I/O polling method,
186
Testing the Input Capture method
using Stimulus Scripts, 168
Testing the new NVM library, 102
Testing the PS/2 receive routines,
172
Testing the PWM as a D/A con-
verter, 334
Testing the Read Status Register
command, 95
Testing the serial communication
routines, 114
Testing the text page performance,
256
Testing the TextOnG Page module,
246
Testing theTimerl interrupt, 61
Testing the video generator, 218
Testing the WAVE file player, 350
Testing with the Logic Analyzer,
36
Text,
Text Page, xii, 201, 246-248,
252-256
Text Page Test, 246
Text Page Video, xii, 201,247,
253, 255-256
Text Page Video Module, 253,
255-256
TextPage.c, 253, 255-256
TextPage.h, 253, 255-256
TextOnGPage, xii, 201,243,
246-247, 254
TextOnGPage.c,
243, 246-247
TextOnGTest,
246-247
TextOnGTest.c, 246
Theoretical Foundations of
Computer Science, 361
Theory of Fun for Game Design, 261
Third method - I/O polling, 181
Time (us), xii, 21, 170,352
Time and date of creation, 300
TimerO, 27
Timer! ,59
TimeM Interrupt, ix, 53-54,
58-63
Timerl Module, 22, 58, 61, 63,
278
Timer3, 336
Timer4, 27, 182
Timers, 21,27, 55, 164,175,208,
277
Titus J., 40
Toggling, 196,211
TON, 22, 51,216, 254,330
Total Endurance Software, 107
Transmitter, 90-91, 112-113
Traps, ix, 53, 57, 294,310
Tricks, vii-xiv, 14, 27, 38, 51, 66, 82,
105,115,124, 139,155, 198,
228,258,260,280,327,360
Trigonometric functions, 51-52
Tris, 94, 96, 99, 1 1 1
TV, 201-203, 221,223
Twc,98, 103
Two-dimensional, xii, 201, 232-233,
235, 260
Two-dimensional function
visualization, xii, 201, 232
TX, 91,94, 96, 100, 112-113,124,
267,355,357
Type ID, 339, 342
Typical WAV, 343
U
Undock, 76
Unicode, 70
Universal asynchronous receiver and
transmitters (UARTs), 90
UART1,89, 109,256
UART1,89, 109,256
UART2RS232, 111,119
Unlocking the Sky, Glenn Hammond
Curtis and the Race to Invent the
Airplane, 40
Unix, 295, 363
Update Parity, 166, 168,178-179
USB, 109-110, 124-125,161,163,
194,274,327
Flash, 274
Serial Interface Engine, 124
USB Complete, 3rd ed., 125
USB Mass Storage: Designing and
Programming Devices and Em-
bedded Hosts, 281
User Guide, xix, 6,42, 65, 142, 177
Using the,
Explored 6 demonstration board,
viii, 29,37
Logic Analyzer, vii, 1 7, 24, 29
Output Compare modules, xii,
201,208
PIC24 OC modules in PWM mode,
xiv, 329, 332
SD/MMC interface module, xiii,
263, 276
Serial port as a debugging tool, x,
109,120
UT, 52
V
Variable Declarations, viii, 29, 31,
378
Index
35-36, 73, 76
Verify a valid parity bit, 165
Verify the presence of a start bit
(data line low), 165
Verify the presence of a stop bit
(data line high), 165
Vertical Sync, 206-207, 210,
213-215,219,251,253
VGA, 223
VH, 260
VHDL, 172
VHF, 161
VHS, 202
Video I/O, 211
Voice, xiv, 329,338,352, 360
VOL, 304, 310
Volare, xiv, 329
VOLume, 287, 304,310
Volume Label, 287,310
Von Neumann, 72
VRES, 206-207, 211,214-217,
222,224-225,229,231,237,
244, 253-254, 260
VT100 HyperTerminal, 125
W
wave.c, 341,350
Wave File Player, xiv, 329, 341, 346,
350
WAVE file format, 339
wave.h, 346,350-351
WaveFormat, 361
WaveTest, 350-351
WDI, 95-96, 99
Web, iv,xix,4, 68, 107, 140,159,
199,221,265,281
Webservers, 140
WED, 67
WEL, 97
Whac-A-Mole, 147
Wl, 125, 281,342-343
Wiki-book, 15
Wilkie, Calum,xv
Windows Explorer Screen, 325
Windows HyperTerminal, 111, 113,
122
WIP, 97-98, 100-101
Wireless Networking, 125
Wirth, N. (1976), 83
Workbook, 169-171, 180-181
WP, 275-276
WP Notice, 275
Wright, Orville, 28
Write,
Data to a file, 313
Enable, 95-99, 101,107
Enable Latch, 97-98, 101
Writing data to an SD/MMC card,
274
Write In Progress (WIP),
97-98
Writing to the EEPROM, 98
Write Protect, 265-266, 275
WriteEnable, 101
Writes, 95, 130
WriteTest,
323-324, 326
writetest.c, 323-324
Writing, x, xiii, xv, 4, 12, 14, 52,
58,66,83,89,98, 124,133,
136,191,211,242,263-264,
274,276,278-281,283,288,
297,300,312-313,315-317,
324-327, 332-333
WRONG, 124, 141,280,309,318
WRSR, 95-96, 99
X
X0-2, 234
X0-5, 231,234
XT, 239
Z
Z80, 239
Zero, 6, 22, 30, 48, 57, 59, 70, 82,
116,152,278-279,288,318,
336, 360
Zoomed, 219
379