This is only a preview of the September 2021 issue of Practical Electronics. You can view 0 of the 72 pages in the full issue. Articles in this series:
|
PIC n’Mix
Mike Hibbett’s column for PIC project enlightenment and related topics
Part 7: PIC18F Development Board
T
his month, we expand on the
voltage monitoring capability we
developed previously by logging
the data to a file on a standard Micro-SD
card, so we can download the data later to
a PC. As our development board is already
equipped with the necessary hardware,
this month is going to be a software discussion. Bringing file storage capability
into a small, embedded microprocessor
project is a big task, even with the support
of the Microchip Code Composer utility,
so it’s worth taking some time over this.
The hardware interface, shown in Fig.1,
is very simple electronically – it’s just
regular SPI (Serial Peripheral Interface).
There are additional functions available
on card sockets, such as a pin that signals
when a card is inserted and a write-protect signal, but for an application such
as ours, where the card will always be
connected and inside an enclosure, it
is perfectly acceptable and common to
ignore those extras and stick with standard four-wire SPI.
To make life even more interesting we
are going to add one further feature into the
mix this month – including a time stamp
with the sample data as it is written to a
file on the card. For data logging applications this is an essential feature, because
knowing when a change in a sensed value
occurs is often just as important as knowing what that sensed value is.
Tracking time
There are several relatively cheap ICs
available that are specifically designed
PIC n’ Mix PIC18F
Development Board
The PCB for the PIC18F
Development Board is available
from the PE PCB Service – see the
July 2021 section.
www.electronpublishing.com/
product-category/pe-pcb-service/
50
to store and maintain a clock, with communication over I2C or SPI. These ICs are
designed to operate with a small battery
which will keep the clock running when
the system power is turned off. Your PC,
laptop and mobile phone have one of these
devices (in PCs and laptops, the battery is
typically a replaceable coin cell.) If you
are happy to set the time and date when
the system is powered on each time (we
are, in this project) then that cost can be
avoided as we have everything required
in our existing hardware.
The processor contains a number of
timer peripherals which can be configured to increment on every clock cycle
(or on a division of the input clock signal)
and generate an interrupt when a particular count (say, one second’s worth of
clocks) is reached. That interrupt could
call an interrupt routine that updates a
clock timekeeping variable by 1s. The
timer peripheral can be configured to
automatically restart; counting up from
zero and giving you an accurate clock
that is updated automatically without
interfering with your main application.
For a clock to be useful, it must be reasonably accurate. Our processor operates
on a clock signal generated internally,
based on a resistor and capacitor to provide the reference frequency. This clock
signal is calibrated during IC manufacture
to 2% accuracy. That’s perfectly fine for
running code and communicating with
other devices over serial interfaces, but it
would be terrible for maintaining a realtime clock – it could lose 30 minutes each
day. This is why we have a crystal fitted
to our board, which oscillates accurately
at 32.768kHz (but hereafter referred to as
the ‘32kHz’ crystal). The crystal is wired
to two pins that connect internally to the
secondary oscillator peripheral. This is
designed to work with 32kHz crystals,
a type of crystal that can run with very
low power consumption. It’s also such
a low frequency that we can configure
a timer to interrupt once every second,
which will make our interrupt function
nice and simple.
What’s the date?
So, we now know how we will produce
an accurate, one-second timer. Next we
need to decide the format we will use
to store the timestamp. A naive choice
would be in a ‘human-understandable
format’, for example: 02/07/2021 11:25.
That format is easy to read by both
humans and computers but has several
problems. First, is that date in Day/Month
or Month/Day format? Is the time in 12hour or 24-hour format? If 12-hour format,
what is the pm indicator going to be?
You can define these settings in advance
of course, but it is not trivial to update
this information in software – you need
to allow differing number of days in each
month, and for leap years. These problems, however, are nothing compared
to the main issue. What time zone is
the device operating in? Also, daylight
saving time, when the clock moves forward or backward by an hour, depends
on your particular time zone. Or rather,
the time zone your device runs in. This
can quickly become extremely complicated to manage, for what should be a
relatively simple problem.
Thankfully, this problem has been
considered before, and a simple solution
proposed – ‘Unix Time’. This is a simple
32-bit positive integer, which marks the
number of seconds since 1 January 1970,
UTC. By storing and maintaining a Unix
Time variable, we only need to increment
it by one every second, using the very
simple algorithm we discussed earlier.
The interpretation of what that value
means can then be left to whatever application the user is using to display or
process the data. This means, instead of
(V S S )
G N D
V D D
V D D
D A T 2
D I
IR Q /D A T 1
D O
G N D
(V S S )
S C L K
C S
C a r d D e t e ct
Fig.1. Micro-SD card connector pinout.
Practical Electronics | September | 2021
H a rd w a re
M C C
a u to -g e n e ra te d c o d e
C o d e w e w r ite
(perhaps to send data
over a Bluetooth link)
A p p l i ca t i o n co d e
without the application code requiring
any changes.
U A R T
F i l e syt
e m
W i- F i
T im e r
A D C
‘Layering’ is the
d r i ve r
d r i ve r
d r i ve r
d r i ve r
d r i ve r
technique of creating
drivers at different deS D -C a rd
U A R T
grees of abstraction
d r i ve r
d r i ve r
from the underlying
hardware. When we
S P I
draw this as a diad r i ve r
gram, layers that are
closer to the hardware
M C P 2 2 2 1 A
M i cr o - S D
E S P -0 1
O p a m p
are at the bottom, with
U S B U A R T IC
ca r d
m o d u le
b u ffe r
the level of abstraction increasing as we
move up. The final
Fig.2. Software architecture, showing how the program is
made up in layers. Thanks to MCC, we only need to write one application code appearing at the top.
layer of software.
The MCC tool creates drivers for us, so it makes sense to
writing ‘25/06/2021 18:25’ to a data file,
draw out a design for our project based
we would write ‘1624641898’.
on this approach, as shown in Fig.2.
Essentially, we push the problem to
The simplest driver shown here is
the system displaying the datafile conthe Timer. The driver code contains all
tent, and there is a very good chance
the configuration settings and functions
that system would be a PC, connected to
to interact with the timer and gives us
the Internet, and able to make sense of
two very simple functions to use – one
the time zone it inhabits, and adjust the
to start the timer running, and one to
data it is receiving as appropriate to the
handle the periodic interrupt. Neither
time zone settings the user has specified.
of these functions require us to look at
This makes our life much easier.
the datasheet of the processor to underIf you would like to convert a value
stand how to use them.
from Unix Time to local time, there are
A more complex example is how we
online tools to help, for example: www.
access files on the Micro-SD card. The
unixtimestamp.com
file system driver will provide functions
In summary, we will store a timestamp
such as ‘open file’, ‘write to file’ and ‘read
for each data sample recorded, using Unix
from file’. The file system driver knows
Time. Initially, when our device starts
nothing about how those files are stored
up we will set this time to 0, but in the
on the card; it uses the SD-Card driver to
next article we will provide an interface
manage that. Likewise, the SD-Card driver
for setting the true current time value.
does not know how to communicate with
the card via the physical interface; it uses
Software architecture
an SPI driver to do that for us.
As we move forward with the software for
There are several benefits to this
this application, we start to move away
approach. First, most of these (quite
from a simple design to something with
complicated) drivers are created for us
quite a complex organisation. This is a
by the MCC tool. Second, if we were to
good time to talk about how we segregate
change, say, to a USB-based memory stick,
program functions in a way that simplithe file system driver and our application
fies the design of the overall system: by
code would not change – only the lower
breaking the design down into smaller
layer driver would need to be replaced.
chunks. We do this using two concepts:
By the time we finish our application
software drivers, and software layering.
this month you will see the benefits. The
A software driver is a piece of software
application code we write will be simple,
that performs a specific job. It is typically
and easy to read.
a set of variables and functions that proThe most complex of the drivers we will
vide some kind of service. One example
be using is the file system driver, called
is a serial port driver. This contains the
‘FatFs’. This allows us to create, read and
code to configure the UART port and
write to files in a format that can be read
provides a character transmit and charby a computer. FatFs is actually a thirdacter receive function. The application
party library, created and maintained as
can then call these functions to send
a hobby project by an engineer in Japan.
data over the UART port without requirIt’s been a freely available library for over
ing the application code to ‘know’ that
15 years and has been very professionally
a UART is being used. When properly
maintained by the owner over this time.
written, the driver code could be reWith the latest updates made just a few
moved and replaced with another driver
Practical Electronics | September | 2021
months ago, it’s clearly a popular, valuable
and effective driver. Microchip have
integrated it into the MCC tool which
saves us having to ‘integrate’ it ourselves –
so many thanks to both parties for making
our lives easy! If you are interested in
viewing the history of the development,
you can find the author’s website here:
http://elm-chan.org/fsw/ff/00index_e.html
Adding the drivers
Before making any updates to our application code, we need to add the drivers
into our project. We start by opening up
the project files from the previous article.
These can be found on the September
2021 page of the PE website in a single
zip file (named picnmix-sept-beginning.
zip). Unzip the file into a directory of your
choice. The top-level directory within
the zip file is: monitor.X
Now open MPLAB, click on ‘Open
Project...’ and navigate to the directory
containing the monitor.X directory you
just created. You will see it shown with
an IC icon next to it in the file listing,
indicating that MPLAB recognises it
as an MPLAB project. Double click on
monitor.X to open the project.
Next, click on the MCC icon on the
top menu bar, which should bring up the
‘Project Resources’ and ‘Device Resources’
windows on the left-hand side, as shown
in Fig.3. The Project Resources window
shows the features already included in our
Fig.3. MCC Resources windows.
51
add the following line of code just before
the TMR1_StartTimer() call:
TMR1_SetInterruptHandler(timer1_
callback);
Fig.4. TMR1 configuration window.
project (we did that in the previous article), and Device Resources also lists those
additional drivers available for us to add.
Let’s start with the simplest driver – we
want to add a timer, which we will use to
maintain a ‘Unix Time’ clock. Scroll down
the Device Resources list to the ‘Timer’
drop down entry, expand it (by clicking
on the triangle icon) and click the ‘+’ icon
nest to ‘TMR1’. Change the ‘Clock Source’,
‘Timer Period’, ‘Enable Timer Interrupt’
and ‘Callback Function Rate’ as shown
in Fig.4. These changes configure TMR1
to use the secondary oscillator inside
the processor which is connected to the
32kHz crystal. Then click the ‘Generate’
button at the top of the Project Resources window. Notice that we have enabled
interrupt operation – we have chosen to
enable the timer interrupt to allow the
timer to run in the background so that
our main application does not need to
handle looking for an actual timeout event
once a second. We will instead use the
‘callback function’, which is a function
we write that will be called by the timer
code once every second in the interrupt
handler. All of which is managed for us
by MCC. Again, simplifying our interaction with the timer specifics.
The generation process creates a
number of new files and adds these
into our project directory. The interesting ones for us in this case are the two
files tmr1.c and tmr1.h
tmr1.c holds the source code for the
peripheral’s configuration, which we do
not need to look at. tmr1.h contains the
‘API’, the list of functions that we can
make use of in our application. Looking
through this file, we can find the important functions for us for this project.
The first interesting function is
TMR1_Initialize(), which sets the
52
peripheral’s control registers. This is important, but is called for us automatically
by the SYSTEM_Initialize() function
already present in our main.c file. That
function was updated automatically
when we clicked the Generate button.
Next up is TMR1_StartTimer() – we
call that to start the timer running. We
can do this at any time after the call to
SYSTEM_Initialize(), so we copy it
into our main.c directly after that line
of code. While in main.c we have to uncomment this line:
//INTERRUPT_GlobalInterruptEnable();
Doing this allows the timer interrupts to
be actioned by the processor.
F i n a l l y, b a c k i n t m r 1 . h , w e
find an important function:
TMR1_SetInterruptHandler()
It is not immediately obvious, but this
is the function we call to assign our timer
callback function. Let’s head back to main.c
and implement that function. First, we
need a global variable that will be our Unix
Time counter. We create that just above the
main() function. Then we write a simple
callback function – this just increments that
Unix Time variable by one each time it is
called. The code looks like this:
uint32_t unixtime = 0;
void timer1_callback(void)
{
unixtime = unixtime + 1;
}
Note, the name of the function can be
whatever you want.
Finally, TMR1_SetInterruptHandler()
is the function used to assign our callback
function to the timer driver. To do this we
That’s it – if we run the code in debug
mode on our development board, we
should see the Unix Time counter incrementing when we pause the program.
Now we get to the slightly more challenging part, adding support for the
Micro-SD card.
We start by adding the ‘SD Card (SPI)’
driver from the Libraries section of the
Device Resources window. This automatically brings in the SPI1 driver at
the same time. Next, add in the FatFs
driver. Now, we are ready to configure
these drivers. We will start at the lowest
layer, the SPI1 driver.
Click on the SPI1 driver in the Project Resources window; then, in the SPI1
driver details window that appears, we
change the ‘Clock Divider’ from 0 to 64.
This reduces the SPI clock frequency
down to 123kHz. Why? That’s because
Micro-SD cards must, on their initial
configuration following power on, be
run at a very slow speed, presumably for
compatibility reasons. We can increase
the speed of access later. Right now, it’s
not clear how we would do that within
the software, so we will leave it as is
and move on.
In the SD Card (SPI) driver window,
uncheck ‘Enable Card Detect (CD)’ and
‘Enable Write Protect (WP)’, as these are
features, we do not need in our application.
We must now make sure that the correct processor pins are assigned to our
SPI interface. Open the ‘Pin Module’
from the Project Resources window by
double-clicking on the name, and then
select the ‘Pin Manager: Grid View’ in
the lower window. Notice how some pins
have already been allocated for the SPI1
module. Click on the padlock symbols to
select the correct pins as defined by our
circuit. Then, in the ‘Pin Module’ window,
click ‘Start High’ for the SDCard_CS signal
(it’s an active low), and also unclick the
‘Analog’ checkbox associated with RC3.
Your settings should look like Fig.5.
Now we can turn our attention to the
file system driver. Click on the FatFs
label in the Project Resources window,
then in the FatFs window that appears,
click on the ‘Configuration’ tab.
The list that appears is long, so we will
concentrate only on the features that we
will change from their defaults. We start
by leaving the ‘Generate example/demo
files’ checkbox set, as these files may serve
as a useful reference later. Next, we must
tell FatFs what SD-Card driver it should
use to communicate with the physical
card. You could – in theory – have several
Practical Electronics | September | 2021
Fig.5. Mapping pins for the Micro-SD
card interface.
cards attached at one time. We only have
the one driver, listed as ‘SD Card (SPI)’,
so just click the ‘+ Insert Driver’ button
to link that to the FatFs driver.
Now we get to enable or disable a
variety of FatFs functionalities. This
capability has been provided to help us
minimise the amount of code and RAM
used by the driver; it can get quite high
if all features are enabled as it is a very
comprehensive driver. Microchip have
provided default settings that allow the
driver to be used with PIC18F processors, but as we have a fairly large device
on our board, we will enable some extra
functions, just to be able to experiment
more with the files written to the card.
Let’s start by changing ‘Function optimization’ from ‘3’ to ‘0’, enabling more features
in general. Then change ‘String function
optimization’ from ‘0’ to ‘1’, which will
allow us to write text to the file. Click on
the ‘Enable find/search functions’, as these
enable us to perform directory listings.
Practical Electronics | September | 2021
We will leave the remaining configuration settings at their defaults. This means
we can only use simple filenames and are
limited to a single file open at any one
time – a reasonable setup for us, since
having multiple files open at any one
time can consume RAM quickly.
We can now click the ‘Generate’ button
to have MCC create the source files for us.
Performing a code build now should
result in a successful build, but with
dozens of warnings – these are all in the
FatFs code itself (and hence of no concern to us) or simply warnings about code
that has been generated, yet not used.
So, let’s go ahead and use some of those
functions to write to a card!
The functions that we can use to access
the card (referred to as the driver’s ‘API’,
or application programming interface)
are listed in the file ff.h. This is quite a
complicated file with limited comments,
so we look instead to one of the demonstration files provided: fatfs_demo.c
You can find this file by exploring the
list of project files in the ‘Files’ tab in the
left-hand window of MPLAB-X. From
this, we can see that accessing the card
requires the following steps; we call:
f_mount() to initialise access to the card
f_open() to open or create a file on
the card
f_write() one or more times to write
to the file
f_close() to commit any unwritten
data to the card
Note that f_close() is very important.
Data is written to the card in blocks to
allow for efficient data transfer. If you
wrote say, a single byte, that would be
stored in RAM until more data is written. f_close() flushes any unwritten
data to the file, and makes sure that the
file system is updated correctly.
You can see how we integrated these
calls to write our sensor data to a file
named a.txt in Listing 1.
Having typed in this code, we attempted to build the project. Surprisingly, this
resulted in an error message:
ff.c:6197:: error: (1089) recursive
function call to "_putc_bfd"
53
This error is emitted by the compiler,
so let’s take a look at the offending lines
of code:
if (FF_USE_STRFUNC == 2 && c ==
‘\n’) {/* LF -> CRLF conversion */
putc_bfd(pb, ‘\r’);
}
We can see that this block of code should
never be called, since FF_USE_STRFUNC
is set to 1, not 2. We were unable to find
an answer on the Internet as to why this
issue occurs, so we simply commented
out the code. On re-building the project,
we end up with a successful build with
a 30% utilisation of code and memory –
that’s perfectly fine, and plenty of space
remaining to allow us to add extra features.
This kind of issue probably arose because
there are so many different configuration
options for FatFs, we may have selected
an unusual combination. No matter, the
issue appears to have been easy to correct.
Before we test the code, we must prepare a Micro-SD card by formatting it on
a PC. This does two things – it removes
old files from the drive, freeing up space
(make sure you take a copy of any valuables files first!) and it also allows you to
specify the type of file system that will
Listing 1: Our updated application code.
// Mount the SD Card, and do it immediately
fr = f_mount(&fs, "", 1);
// stop here if there was an issue mounting the drive
if (fr != FR_OK) {
printf("*** Failed to mount disk\r\n");
do {} while (1);
}
while (1)
{
fr = f_open(&fil, "a.txt", FA_OPEN_APPEND | FA_READ | FA_WRITE);
if (fr != FR_OK) {
printf("*** Retrying file open\r\n");
fr = f_open(&fil, "a.txt", FA_OPEN_APPEND | FA_READ | FA_WRITE);
if (fr != FR_OK) {
printf("*** Failed to open file\r\n");
do {} while (1);
}
}
ADCC_StartConversion(channel_ANA0);
while(!ADCC_IsConversionDone());
solarPanel_volts_adc = ADCC_GetConversionResult();
solarPanel_volts = (double)solarPanel_volts_adc;
// Convert ADC value to volts, compensate for divide by 2
solarPanel_volts = (solarPanel_volts * 3.3 * 2.0) / 4095.0;
ADCC_StartConversion(channel_ANA5);
while(!ADCC_IsConversionDone());
battery_volts_adc = ADCC_GetConversionResult();
battery_volts = (double)battery_volts_adc;
// Convert ADC value to volts, compensate for divide by 2
battery_volts = (battery_volts * 3.3 * 2.0) / 4095.0;
// Print the values over the serial port
sprintf(str, "Time: %ld: Battery voltage: %1.2f, Solar voltage:
%1.2f\r\n",unixtime,battery_volts, solarPanel_volts);
printf(str);
bytes_written = f_puts(str, &fil);
if ((bytes_written < 45) || (bytes_written > 99)) {
printf("*** Failed to write to file correct number of bytes.\r\n");
do {} while (1);
}
__delay_ms(500);
f_close(&fil);
__delay_ms(500);
}
54
be on the disk. There are typically three
options to choose from – FAT32, NTFS
or exFAT. Ensure you select FAT32 – the
other two are not supported with the configuration of FatFs that we are using. The
file system is a specification for how files
are stored on the card, and how a program accessing the card can locate those
files. Different file systems have different
benefits, but for use with an embedded
system design, FAT32 is perfectly acceptable – you will be hard pressed to find
any limitations (for example, file sizes are
‘limited’ to a maximum of 4GB.)
Eject the card from your PC and insert
it into the development board socket.
Start the code running, allow it to continue for a few minutes, then stop the
code, remove the card and re-insert it
into your PC card reader. You should
find the file a.txt, which when opened
with a text editor such as notepad, should
reveal the written data.
Wrapping up
By integrating the wonderful FatFs library into MCC, Microchip have made
bringing access to huge amounts of cheap
persistent data storage a breeze. MicroSD cards are not only great for storing
data, but also for storing configuration
information, readable data sets (like
graphics or sound) and even firmware
updates. The full source code for this
month’s article – both the starting point
and the final functional code – is available for download from the September
2021 page of the PE website.
We have now reached a stage where (in
theory) the software is functional enough
to perform the task we originally set out
to do – monitor a solar-powered device’s
power supply rails. There are, however,
some enhancements that will not only
make the hardware easier to work with
but will also allow us to demonstrate the
next major piece of functionality available on our development board – Wi-Fi.
Coming up next
In our next article we will look to add
in the Wi-Fi interface so we can read the
data file from the device remotely. We’ll
also add some user interface features, implemented to be accessed over the serial
interface. We will also use the Micro-SD
card to store a text file containing our
Wi-Fi credentials, rather than hard code
them into the program. At that point we
should be done and can move the circuit
from the development board to a small
circuit and put it to practical use.
PIC n’ Mix files
The programming files discussed
in this article are available for
download from the PE website.
Practical Electronics | September | 2021
|