Getting Started with a PIC16

Specifically, a PIC16f18326

Image for post
Image for post

We live in a wonderful present, where any hobbist can get any manner of easily accessible demo board for single-digit prices and after reasonable time delays. When I first started fiddling with Arduino and Raspberry Pi I was ecstatic with the feeling of empowerment that followed being able to manifest my ideas in the real hardware world.

Once mastered the feat of programming pre-built educational boards, the next step was working with raw, bare metal microcontrollers —or not really, but it incidentally became my job in the meantime. It’s a fun place, even if it’s barren, deserted, depressing, full of desperation and unsuitable for human life.

Today I’m going to make a quick tutorial on how to program a bare PIC16, specifically the PIC16f18326. No demo boards and no USB cable, just the MCU, a breadboard and a dedicated programmer.

What a wonderful question. Why choosing a PIC and, most importantly, Microchip to work with? At this point you’d expect me to start listing the advantages of the architecture, an highly usable software toolset, the qualities of their microcontroller families; the only problem being I would be lying.

Microchip sells, without any doubt, the worst family of products I’ve ever had the displeasure of working with. Their documentation is cluttered and outdated, their attitude towards the community didn’t evolve in 30 years and their software suite is nothing short of unpresentable.

One day I’ll probably take the time to properly justify my rant; today, let’s focus on why I chose to write a tutorial on 8-bits PICs in spite of their glaring defects. There are mainly three reasons:

  1. I have a lot of experience: I work with PIC everyday (which is why I hate them so passionately) and I learned how to use them from scratch, so I’m confident in my knowledge.
  2. I wanted to make a very bare metal tutorial, meaning we are going to work with the MCU alone; no demo board, no development kits. To do this the device itself must be in a manageable format, which would be Dual In-Line Package (in short, PDIP). There are only two families of microcontrollers that I know of that ship in PDIP: PIC and Atmel. Atmel is overall better but got absorbed by Microchip a few years ago, and I’m betting in a few more they’ll strip away everything good it had, so…
  3. A severe lack of tutorials. Seriously, most of the stuff you find on Google are 10–20 years old. Starting out on PIC programming was a real pain for me, and I want to make it a little easier.
Image for post
Image for post
A PDIP-packaged PIC16

What you will need

For the purpuse of this guide you will need the following components:

  • A PIC16f18326 in PDIP format.
  • A Microchip SNAP programmer.
  • Microchip’s horrible development toolchain.
  • A breadboard.
  • An LED
  • Two resistors: 47 Ohm and 10 kOhm
  • A 10uF ceramic capacitor
  • Some jumper wires

A simple 8-bit cheap microcontroller. Buying them from Microchip’s store costs about 1 Euro each, but they also give them away for free with their samples program. Make sure to get the PDIP packaging.

Image for post
Image for post

The model itself is nothing special, I just happened to have one laying around; as such, a similar device is ok as well: we’re just going to blink an LED so the procedure should be more or less the same. The conditional here is important: Microchip products are not very good at following standards and their registers often vary wildly in name and form. If you already have another MCU give it a try; otherwise get the same model as mine.

One of the main reasons for this tutorial is the new MPLAB SNAP programmer. Up until about a year ago the cheapest option to program a PIC device was the PicKit3, sitting at the abundant price of ~40 Euros — far from the typical budget for an hobbist to get started. The SNAP finally fills that hole by coming as cheap as 13 Euros while keeping most of the functionality. It’s still pricey when compared to low-end options from competitors (I’m thinking about the 2 Euros ST-Link programmers), but an improvement nonetheless. You can grab one from Microchip’s official store.

Being a low-cost alternative it comes as a naked PCB. If you have a 3D printer you can make your own case, maybe using the one I’ve modeled on Thingiverse.

Breadboard and wires are essential for the basic connections and without the LED you won’t see anything happen; as for the resistor and capacitor connected to the programming pins, the latter can be ignored while the former is essential.

Still, if anything goes wrong, the first thing you should do is check if your circuit is correct and complete.

There is only one way to program PIC controllers: the steaming pile of garbage that is Microchip’s IDE. It’s old, cluttered, buggy, slow and ugly. Despite that it is actually fairly straightforward to install and never took me more than 10 minutes to get up and running on a new machine, be it Linux or Windows. You can download the latest version from their website.

You also have to download separately their compiler for 8-bit devices: XC8. Nothing too crazy here, it’s just a required step. At the time of writing the latest version is 2.05.

There is also a new browser-based version identical to the old IDE (if something doesn’t work why change it, right?), but for the life of me I couldn’t run the Browser-USB Java interface layer. Try it out, if successful you can get to code even faster.

From now on I’ll assume you have an installed IDE and compiler.

Schematic and Connections

Image for post
Image for post

Unfortunately, we can’t just plug our MCU and expect it to work; we need to fiddle a little with connections and wires. The MPLAB SNAP has an header with 8 pins, out of which for some reason only 5 are used (the ones connected in the diagram). The white wire in the image corresponds to the side of the header marked with an arrow on the PCB.

Image for post
Image for post
  1. MCLR pin (white wire) is a reset line; it is used by the programmer to reset the device before programming.
  2. Vdd (red wire) is the power line, 3.3V in our scenario.
  3. Vss (black wire) is the ground line.
  4. PGD (blue wire) is the Data transmission line, used to actually send the program.
  5. PGC (yellow wire) is the clock line.

With an inexplicably poor design choice SNAP does not allow to power your device, so we need to provide an external source. 3V are sufficient, in the form of two AA batteries.

The only pieces left on the circuit are the LED+limiting resistor (47 Ohm), which is not strictly necessary but we will use it to see proof of activity, a 10 kOhm pull up resistor on the MCLR line that must be connected and a ceramic capacitor (10 microFarad) on the power source to stabilize it (you can try even if you don’t have it, but some issues might arise).

You can find more details on the connections needed to program it in section 2.0 of the datasheet.

Creating a new project

Once everything is setup and ready, open MPLABX and create a new empty project for a PIC16f18326 (or whatever your PIC model happens to be). Hit File > New Project and fill in the forms.

Image for post
Image for post
Create a standalone project.
Image for post
Image for post
Select the correct device ID (you can’t miss, invalid names are not allowed).
Image for post
Image for post
Here you should select the compiler toolchain; if you have not yet installed it you can do it now, following the highlighted link. If you managed to use MPLAB XPRESS this step is not required.

The empty project should have nothing in it, so we can begin by creating a main.c. You can do this either from the project view (left click on Source Files > New > main.c) or by creating a new file and adding it to the project (left click on Source Files > Add existing item). Annoyingly, a main function alone is not enough to run anything (sometimes it might be, but let’s play it safe); you first need to configure your device.

MPLABX manages everything by creating automated makefiles, so you don’t have to worry about linker scripts and memory configuration. If nothing else, it makes it easier to get started.

Register configuration

There are certain system registers that are written by the program in flash memory (the read-only part of memory were firmware is stored), so their configuration is static and cannot by changed during runtime. Unlike pretty much everyone else Microchip did not go with the default linker script approach to change specific sections of program memory; instead, they use #pragma directives.

#pragma is a special C preprocessor directive used to turn on and off custom features in the compiler, and the XC8 compiler defines the config feature that allows to change static registers values.

There are 20–30 system registers to be configured. Hunting their names and function in the datasheet would be a huge pain, but luckily we can generate the boilerplate code through a decent interface integrated in MPLABX.

Go into Window > Target Memory Views > Configuration Bits; in the bottom of the IDE a new widget with a list of registers should appear. Right of the bat you can make sure that FWDTEN is set to OFF. This enables the Watchdog Timer, a special device that resets our MCU if no activity is detected. Don’t care about it now. Beside that, we have to take care of the internal oscillator.

The oscillator is the heart of our MCU. It is the device that creates a steady clock signal used to execute instructions; in PIC architecture every instruction typically takes 4 clock cycles to execute. This means that, for example, with a clock frequency of 32 MHz (32 million clock pulses per second) your MCU will run at 8 million instructions per second, with every instruction taking 125 nanoseconds.

All the PIC I’ve used so far can take advantage of an internal oscillator module, and ours is no exception. To understand how to properly configure it however we need to delve into the datasheet, sections 5.2 and 7.0.

My device has several oscillator options, controlled with the RSTOSC register. By default, this register contains the value for an external oscillator; we need to change it and use the more reasonably available internal one. In the register configuration view, select HFINT1 , the internal oscillator running at 1 MHz.

Image for post
Image for post
The configuration window.

After that just click on “Generate Source Code to Output”: a list of #pragma directives will be printed so you can copy them and paste on top of your main.c file.

Note: I’ve tried leaving default values for the configuration bits; sometimes it works, sometimes it doesn’t. According to the datasheet the default oscillator should be an external one that I have not connected: yet my setup still worked, running at 32 MHz (so possibly using the internal one). You can’t trust Microchip documentation, so make sure to initialize all values.

The Code

Finally we can start actually doing something. Fill in the main function with some meaningless instructions, just so we can confirm everything works.

// Here you should have all the #pragma directives from the 
// register configuration
// ...
int main() {
int x = 0;
while(1) {
x++;
x--;
x++;
}
return 0;
}

This obviously does nothing, but it’s enough to test the debugger. Add a breakpoint by clicking on the line number for the x-- instruction. Connect the MPLAB SNAP programmer to your PC, power the circuit and hit the “Debug” button.

Image for post
Image for post
The debug button.

Now the IDE should work its magic, compile and flash the program. You might need to select the SNAP tool in a popup window. Once everything is done you should see the program reaching your breakpoint; then you can try the debugger by advancing step by step in the loop.

When programming a PIC for debugging the firmware will only run if the programmer is connected. After checking that everything works, you should use the Programming button, highlighted in the image, to flash permanently.

Image for post
Image for post

If anything went wrong, head to the troubleshooting section at the bottom of the article.

Let there be Light

The debugger works. Cool. Let’s try to use the LED.

In my circuit the light is connected to the pin on the bottom left corner of the PIC. There is a 47 Ohm limiting resistor to make sure it doesn’t burn, but if you’re feeling lucky (or don’t mind breaking an LED) you can connect it directly.

Image for post
Image for post
The device pinout.

GPIO ports are named after alphabet letters on a PIC. You can read all about it in section 12.0 of the datasheet, but I’d advise you against it because it’s mostly electrical jargon.

Suffice to say that there are 3 classes of registers when working with PIC GPIOs: TRIS, PORT and LATCH.

  • TRIS registers select the GPIO direction (input or output).
  • PORT registers return the level perceived on the pin if it’s configured as an input.
  • LATCH registers (shortened to LAT) are used to set the level for an output pin.

Unlike smarter architectures PIC peripherals are not memory mapped; instead, a specific registers are defined as keywords in the compiler for every one of them.

You can include the <xc.h> header to benefit from a set of register definitions. The pin I’m using in my circuit is RC3, so to select it as an output I have to write 0 in the TRISCbits.TRISC3 definition. A 1 corresponds to an input pin instead. Then, I can drive the line level by writing to the LATCbits.LATC3 register:

// Register configuration
// ...
#include <xc.h>int main() {
TRISCbits.TRISC3 = 0; // set C3 as output
LATCbits.LATC3 = 1;
while(1)
;
return 0;
}

Using this example, your LED should light up!

Delay

Turning the LED on is nice, but we still don’t have a sign of sizeable activity. Blinking it is a start, but how do we tell how much time has passed?

Well, we configured our oscillator to run at 1 MHz and each instruction takes 4 clock ticks, so we could use a busy cycle to wait some arbitrary amount of time; for example, counting up to 10000 sums up to about 1 second.

There is however a built-in function to wait for a more-or-less precise type period: __delay_ms() . It is included in xc.h, but to use it you must first define a special macro, _XTAL_FREQ , short for “crystal frequency”.

Inside xc.h the crystal frequency define is used to build up a precise busy waiting cycle based on the specified clock speed.

#define __delay_ms(x) 
_delay((unsigned long)((x)*(_XTAL_FREQ/4000.0)))
// _delay is a native PIC function

If we #define _XTAL_FREQ 1000000 before including xc.h we will be able to use __delay_ms(1000) to wait for 1 second. Note that _XTAL_FREQ must contain the actual frequency of your oscillator (1 MHz for us), otherwise the timing will be off.

The following program will blink the LED once per second:

// Register configuration
// ...
#define _XTAL_FREQ 1000000
#include <xc.h>
int main() {
TRISCbits.TRISC3 = 0; // Set C3 as output
LATCbits.LATC3 = 1;
while(1) {
__delay_ms(1000);
LATCbits.LATC3 = ~LATCbits.LATC3;
}
return 0;
}

The LATCH register can be read as well to check the line level, so we use it in combination with the ~ operator to reverse, or toggle, the current output.

Interrupts

When working with low-level MCUs, interrupts are paramount to manage multiple tasks. Maybe you want your device to constantly flash a light to signal it is alive even while doing other work. To achieve this, we can activate a timer interrupt and have it fire periodically.

Every PIC can have multiple timers; for now we’re just going to use the first usable one (would be TMR2) described at section 28.0 of the datasheet.

A timer is nothing but a register that is increased every time a condition is met (e.g. a clock tick) and that can fire an interrupt on rollover (reaches its maximum value and turns back to 0) or when a certain number is reached.

Timers are somewhat of a complex peripheral, although they really shouldn’t be. This is where every device disagrees on register number and disposition: some let you choose the clock source, they can have a prescaler, a postscaler, comparators, enables, different modes,… I’m going to explain how to use this one, but this knowledge is not easily portable. To fire a TMR2 induced interrupt we have to use no less than 8 registers.

Luckily, the clock source for TMR2 is fixed to the system oscillator divided by 4, so 250 kHz because we’re running at 1 MHz. This means that the timer counter (which is 8-bits, so reaching up to 255 before rolling over) is increased by one every 4 microseconds and resets every 255*4 = 510 microseconds; it’s quite fast, not a very usable time slice.

We can slow it down by setting up a prescaler and a postscaler: the latter is a divider applied to the source frequency before the timer, while the former is applied after. In the end, both contribute to slow down the count.

The maximum settings we can have for TMR2 is a 1:16 division for the postscaler and 1:64 for the prescaler; together they tone down the 250 kHz clock frequency to 244 Hz, just below the max value for the 8-bit counter, 255. The timer counter will then roll over a little more than once per second.

We can then set up a comparator register to 244–1 (count starts at 0) to have the interrupt fire exactly once per second. Those are the registers we will be using:

  • T2OUTPS : the postscaler register
  • T2CKPS: the prescaler register.
  • PR2: the comparator register.
  • TMR2ON: enable register for TMR2.
  • TMR2IE: enable register for TMR2 interrupt.
  • PEIE: enable register for peripheral interrupts.
  • GIE: enable register for all interrupts.
  • TMR2IF: flag register for TMR2 interrupt.

The prescaler and postscaler registers have specific codes for the preset values (found in the datasheet); PR2 contains the count we want to reach; the various enable registers must be set to 1 for the peripheral to be active, and the flag register is set whenever an interrupt is pending and must be cleared after it was handled, otherwise new interrupts will not be launched.

In the end, this is what we should have:

#define _XTAL_FREQ 1000000
#include <xc.h>
void __interrupt () my_isr(void)
{
if (PIR1bits.TMR2IF) {
LATCbits.LATC3 = ~LATCbits.LATC3;
PIR1bits.TMR2IF = 0;
}
}
int main() {
TRISCbits.TRISC3 = 0;
LATCbits.LATC3 = 1;

T2CONbits.T2OUTPS = 0b1111; // Timer2 postscaler 1:16
T2CONbits.T2CKPS = 0b11; // Timer2 prescaler 1:64
PR2 = 244-1; // Set comparator

T2CONbits.TMR2ON = 1; // Timer2 enable
PIE1bits.TMR2IE = 1; // Timer2 interrupt enable
PIR1bits.TMR2IF = 0; // Clear timer2 interrupt flag

INTCONbits.PEIE = 1; // Peripheral interrupt enable
INTCONbits.GIE = 1; // Global interrupt enable


while(1) {
__delay_ms(1000);
}

return 0;
}

8-bits PIC devices have a single interrupt routine for every peripheral, defined by the __interrupt() directive. This is nice, makes everything easier for the developer to set up. Our main function does nothing now, but the LED still flashes thanks to the recurring interrupt. The comparator register needs to be set only once and it will keep firing at the right time.

Note that the resulting frequency is not actually 244 Hz, but 244.140625 Hz. The comparator and counter register are digital integer values, so we can’t capture the decimal places; the resulting time will be off by ~0.06% . TMR2 cannot be more precise than that, but we could have it run multiple times in a second and increment a variable to know when we are done. For this example, I’ve kept it simple.

Troubleshooting

There are hundreds of things that can go wrong in this tutorial. The whole software toolchain is barely functional, so if you notice ghost breakpoints, buttons not working the first time, IDE crashes and freezes don’t worry, it’s not your fault.

Also, error messages are pretty generic and unhelpful. If you don’t power up the breadboard before programming, for example, you will be met with the following error:

Image for post
Image for post

Which is stupid, because the programmer has power lines and should be able to detect voltage.

For every other connection problem the IDE returns the same misleading error message:

Image for post
Image for post

In this case, double check your circuit has everything in order. Sometimes you will need to close the IDE and reopen it (I know, I’m sorry, this is literally the only way). I had to after I purposefully disconnected a line to reproduce those error messages because they wouldn’t go away even when the circuit was restored.

If your problem is in the compilation process, regular C programming rules should apply; XC8 handles error messages very poorly (what a surprise, uh?), so if you’re in a pinch just download my code and see if it works.

Conclusion

That was a simple example on how to get started with a PIC16 device. Hope I’ve made your life a little easier with it! From an hardware standpoint, these are really good and accessible. The software suite should be scrapped entirely or opened up for the community to replace it, but Microchip seems still stuck in the ’80 with their mentality, and I’m afraid I’ll move to ARM long before they realize their mistake.

Computer Science Master from Alma Mater Studiorum, Bologna; interested in a wide range of topics, from functional programming to embedded systems.

Get the Medium app