One of the things I’ve wanted to do since I was a teenager in the 80’s is to build a homebrew 8-bit microcomputer. If you scan through YouTube you’ll quickly find that I’m not the only one. In fact many people have done this and done it very well. The question I keep coming up against though is, if I’m creating something from scratch, what would I consider “from scratch” to actually mean?

Many of the implementations you see online for example, will put together a 6502 or Z80 chipset with either a serial interface or a simple video interface and keyboard and plug in a Microsoft BASIC ROM. For me though, I’ve always known that I want to try to write my own version of BASIC. In some instances I’ve found people who have built microprocessors (or parts of microprocessors) from logic gates. This makes the “from scratch’ question even harder to answer. For example, do you allow yourself to use RAM, multiplexer, ALU components etc and create you’re own CPU and memory? Will you only allow yourself to use gates? Good luck if you choose the latter!

Similarly there’s the question of what tools you allow yourself to use. If I want to create a BASIC ROM, is it ‘cheating’ for me to compile the code on my MacBook Pro and test it using an emulator? In the end it comes down to practicality as there’s only so much someone can do on their own or within a small group of hobbyists and makers.

It was while pondering some of these questions that I came across Jeri Ellsworth’s YouTube channel which adds a whole new dimension to the question of what “from scratch” actually means!

While these are not new videos, if you haven’t seen them already I recommend you grab a coffee and spend 20 minutes giving them a watch.

It’s a lot of fun writing code on your Discovery board with its myriad of peripherals to explore and exposed headers for further experimentation. Nothing, however, beats the satisfaction to be had from creating and coding your own board and there comes a time when every red blooded geek must strike out on their own!

I’ve recently been presented with an opportunity to create a new board that required a lot more grunt than the PIC based boards I normally deal with. We needed a much faster processor, an FPU and some DSP capability. As a result I opted to use the STM32F405RG as the MCU. The next thing I wanted to do was to mash up a prototype but I needed a quick and easy way to program it. I remembered that the Discovery board has an ST-LINK section with an SWD interface broken out onto headers so I thought I’d give it a go.

STM32F4 Discovery ST-LINK Jumpers

Setting up your Discovery to use it as a programmer is a very straightforward affair. The ST-LINK section is the top third of the board and can be easily isolated by removing the two CN3 jumpers shown above.

You’re also clearly going to need to expose the SWD interface on your board so you have something to connect the ST-LINK to. The schematic below shows how I did it.

jtag

A couple of things to note about the circuit above. I’ve pulled BOOT0 low which means that I’m programming flash memory. If you want to program other areas of memory then you’ll want to be able to change the state of this and the BOOT1 pin which is multiplexed with PB2 on the STM32F40x parts. Details can be found in section 2.4 of the reference manual.

The other thing I did was to expose the additional JTAG interface pins also in order to give me several programming and debugging options. Finally, I connected the NRST pin on the ST-LINK to the NJRST pin on my STM32. That may be wrong, you might be better off connecting it to the NRST pin on the STM32. Perhaps someone out there can let me know in the comments below.

The SWD interface is connected as follows:

ST-LINK to STM32F405 Connections

I chose to expose the interface as a row of 0.1” pitch header and connected the two boards together with female to female patch leads.

The next step is to try and get some code running on the device. I’m using Atollic TrueSTUDIO Lite for this but I’m sure the process is pretty much the same for other IDEs too.

Project Creation Steps

To get up a program up and running on the new board you need to create a new project that targets the STM32F405 or whichever STM32 part you’re using. This will generate a new, compilable and runnable project. If you run the program up now in the debugger you should be able to step through the code, look at variable values and memory content. This shows that everything is working fine.

The final step is to sort out your clocks. TrueSTUDIO will create a default system_stm32f4xx.c file containing a clock configuration for you but unless you happen to be using a 25MHz crystal you’ll want to change it for one targeting your own board. Mine, for example, uses a 4MHz external clock.

STM32 Clock Configuration Tool

STM32 Clock Configuration Tool

The easiest way to do this (as far as I’m aware at least) is to use the clock configuration tool provided by STMicroelectronics for the purpose. You can download the tool and the accompanying notes from their website. I used the simple wizard mode, followed the instructions very carefully and where I wasn’t sure of a value, I looked it up in the original file created by TrueSTUDIO. Once the new system_stm32f4xx.c is generated, it needs to replace the one in your project. Simply re-build the project, make sure you can still launch and debug it and you’re up and running.

So I’ve been running with this setup for a couple of weeks now with very few problems. I did get a bit of a scare when I saw smoke pouring out of the Discovery’s USB port once but I think that was a pretty freak incident as it hasn’t happened since. I’ve had the odd unexplained debugger disconnection but given the amount of myopic prodding I tend to do with scope probes that could be more about what I’m doing than the what the debugger is doing. All in all I think it provides quite a good option to anyone wishing to experiment with creating their own STM32 boards without forking out for new kit.

I’ve recently developed what some might consider to be an unhealthy interest in FPGAs and Verilog. The stuff you can can do with these things is just awesome. As a result of this I went onto YouTube to look for inspiration and information and I came across a series of video lectures by Cornell University Senior Lecturer Bruce Land. Just check out the stack machine and compiler lecture. I would seriously recommend these videos to anyone else out there who might be struggling with an FPGA obsession.

Bruce Land, honestly, you absolutely rock!

Rich and Jon

I mentioned in a post a while back that I was planning to leave my previous job to try to put something new together. Well a whole lot has happened since then and so I reckon it’s probably about time for a bit of an update.

I eventually left Siemens at the end of September 2013 after an eight month notice period (blame a lack of self-confidence!) and, along with fellow geek Richard Hackett, we formed Polyhedrus Electronics Ltd in order to grab an opportunity that came our way to work in the world of motor sport.

The opportunity seemed like a pretty straightforward one: to come up with a sensor that can make contactless linear measurements over a variety of lengths. We were pointed at a bunch of existing products in the marketplace that were close to what was required and off we went.

Within a few months Rich and I had a working prototype that came close to meeting the spec. It didn’t quite have the linear range we needed and the output, an analogue voltage signal, didn’t quite have the range we were after but when it was demonstrated at the Performance Racing Industry show in Indianapolis towards the end of 2013 it caused quite a stir and it wasn’t long before orders came in and we were suddenly faced with the prospect of turning our prototype into a full on production unit.

This is pretty much where we are now. The first few production units are out there and are being tested by an F1 team who are hoping to use them in the 2014 season. We’ve also produced a capacitive fuel level sensor PCB and have a few other exciting projects on the go which I’m sure we’ll be discussing over the coming months.

So the million dollar question then, was it worth it?

Well the answer at the moment would have to be, “Hell yeah!”

I wouldn’t say that we’ve got it completely nailed yet. Unlike Rich who has worked at home for many years, I’m still getting used to that aspect of the work (hardest thing is to stop!) There are still a few niggles with the boards that we’re working through and timescales in motor racing are crazy so we’ve had to make that adjustment too. Trying to get a production process and supply chain going at short notice is a huge challenge and we’re still debugging that. Our feet haven’t touched the ground in the last 6 months!

Despite all of this though I think it’s fair to say that this has been an exhilarating experience and we’re loving it. Rich and I decided very early on that the core value of our new business would be Freedom. Freedom to travel (we both enjoy that), freedom to work the way we want to work and freedom to explore new and interesting technology and ideas. The business has a bit of a hippy vibe to it that I love and we’ll need to work hard to make sure that the daily pressures of keeping our business going don’t detract from that.

For anyone out there who is thinking of doing something like this themselves I’d say go for it! Make sure you are doing something you love and seek out the work of people such as Seth Godin, Marianne Cantwell and others who can provide concrete help and advice.

Meanwhile if there’s anyone out there with a really cool idea and needs a couple of talented geeks to make it happen, feel free to drop me a line and we’d be more than happy to discuss it with you.

We’ve been too busy to get ourselves a website yet but we have just put together a small Facebook page if you fancy finding out more about what we’re up to. The link is https://www.facebook.com/polyhedruselectronics.

LM60 circuit

Measuring the temperature of a PCB is quite a common thing to have to do in embedded systems and I have been working on a board that needs to do just that. There are dozens of ways to achieve this but for this project we chose to use the LM60 from Texas Instruments. The data sheet for this part can be found at http://www.ti.com/lit/ds/snis119d/snis119d.pdf. Note that for this project I’m using a PIC18 MCU and the CCS C compiler. If you’re using another development environment you’ll need to adapt the source code accordingly.

The LM60 isn’t exactly the most accurate temperature sensor in the world, accuracy in the data sheet is quoted at +-3.0-4.0% but in this application we were looking to provide PCB temperature over a CAN interface so that this can be monitored and correlated to any PCB failure that might occur in the field. The accuracy of the LM60 is good enough in this particular instance.

What this part does have going for it though is that it is small, it comes in SOT-23 as well as TO-92 packages, and it has a linear voltage output with respect to temperature which makes it pretty straightforward to convert to an absolute value in degrees Celsius using an ADC in a microcontroller. The LM60 is capable of measuring temperatures from -40 to +125 degrees and there is an automotive spec version, the LM60-Q1 which is fortunate as that was exactly what I needed.

The output voltage of the LM60 ranges from 174mV at -40ºC to 1205mV at 125ºC regardless of input voltage which can range from 2.7 to 10V. The output voltage with respect to temperature is calculated using the equation:

Vo = (+6.25mV/ºC x TºC) + 424mV

Translating that into the general form for a linear equation y = mx + c we can see that the two coefficients are:

Gradient m is 6.25
Offset c is 424

The next step is to figure out what values these voltages will be converted to by the ADC. The PIC18 I used in this project has 12 bit ADCs and the circuit runs at 3.6V. By dividing the ADC range (0-4095) by the voltage range (0-3600mV) we find that each 1mV at the ADC results in a value of 1.14… or it would if the value wasn’t an integer.

With that in mind we can convert the Vo to ºC equation into an ADC value to ºC equation by multiplying each of the coefficients by 1.14. This results in the new equation:

ADCin = (7.11 x TºC) + 482.3

Finally we need to move the equation around a bit as we’re not trying to calculate the ADCin value, we’re going to be getting that from the MCU. What we’re interested in is converting from the ADCin value to a temperature. Re-arranging the equation gives us:

TºC = (ADCin – 482.3) / 7.11

There’s just one other trick used in this driver and that is to avoid the use of floating point maths by multiplying up the values by a fixed amount and returning a result in milli-Celsius. Floating point maths, especially on an 8-bit PIC18, is an incredibly slow thing and best avoided where at all possible. To get around this I used a fairly unsophisticated technique of multiplying the input value and c-coefficient by 1,000,000 and the m-coefficient which will be the divisor in the equation by 1,000. By having the dividend an order of magnitude higher than the divisor and outputting in 1000ths of a degree, we can perform integer division without losing too much precision.

The final code for the driver is listed below. There are plenty of ways in which it could be extended and improved. For example, the code configures the ADC to use a VREF of VDD which is circuit dependent and affects the coefficients used in the equation. There are of course other options. You could for example fix VREF at a specific voltage if your PIC supports that. For example, the PIC18 I was using supports a fixed voltage reference (FVR) of 2.048V which would make better use of the 12-bit ADC and make it VDD independent (as long as VDD > 2.048V obviously!) Alternatively you could pass the value of VDD into the initialisation method and calculate the coefficients at that point.

lm60_driver.h

/*
 * File:   lm60_driver.h
 * Author: Jon Masters
 *
 * Created on 22 January 2014, 11:11
 *
 * Driver code for lm60 temperature sensing using a 10 or 12-bit ADC channel.
 * Assumes that the lm60 chip is being driven at VDD. All temperatures are
 * returned as degrees "milli" Celsius to avoid FP maths.
 *
 * PREREQUISITES:
 * ==============
 *
 * Code must include a #use delay.
 * Code should include either of the following:
 *
 * #device ADC=10
 * #device ADC=12
 *
 * If #device ADC=10 is used, include the following pre-processor definition:
 *
 * #define __LM60_10BIT_ADC
 *
 * Before using any ADC, CCS requires you to call setup_adc(). This driver
 * library will call this as part of the initialisation method. If you don't
 * want it to do that then include the following pre-processor directive:
 *
 * #define __LM60_INIT_NOSETUP
 *
 * You are then responsible for ensuring the call to setup_adc() is made.
 *
 * USAGE:
 * ======
 *
 * Start by calling the lm60_init method passing in the analog channel information
 * corresponding to the channel that the LM60 is connected to. Note that the method will,
 * by default initialise the ADC to use the internal clock. To override this
 * behaviour use __LM60_INIT_NOSETUP and call setup_adc() yourself.
 *
 * Once this is done you can call lm60_read_temp_mC() to read the temperature in
 * thousandths of a degree Celsius.
 */

#ifndef LM60_DRIVER_H
#define     LM60_DRIVER_H

#ifdef     __cplusplus
extern "C" {
#endif

/* Constants used to convert voltages to temperatures. */
#define lm60_y_intercept 482300000
#define lm60_y_grad 7110

/*
 * Holds the index of the ADC channel that the LM60 Vout is connected to.
 */
int8 lm60_adc_channel;

/*
 * Initialises the driver library.
 *
 * ARGS:
 *  sANx - the analog port that the LM60 Vout is connected to. Use the constants
 *          defined in the PIC header e.g. sAN0
 *  channel - the index of the analog channel that matches the sANx argument e.g.
 *          for sAN0, this will be the index 0, sAN25 will be 25.
 *
 * RETURNS:
 *  Nothing.
 */
void lm60_init(unsigned int16 sANx, int8 channel);

/*
 * Returns a temperature reading from the LM60 in milli-Celsius i.e. in 1/1000's
 * of a Celsius. E.g. 25C is returned as 25000mC. The means that FP maths isn't
 * required which should improve performance.
 *
 * ARGS:
 *  NONE.
 *
 * RETURNS:
 *  Temperature in mC.
 */
signed int32 lm60_read_temp_mC();

#ifdef     __cplusplus
}
#endif

#endif     /* LM60_DRIVER_H */

lm60_driver.c

/*
 * File:   lm60_driver.c
 * Author: Jon Masters
 *
 * Created on 22 January 2014, 11:05
 *
 * See header for pre-reqs, usage and function documentation.
 */

#include "lm60_driver.h"

void lm60_init(unsigned int16 sANx, int8 channel)
{
   
#ifndef __LM60_INIT_NOSETUP
    setup_adc(ADC_CLOCK_INTERNAL | ADC_TAD_MUL_16);
#endif //__LM60_INIT_NOSETUP

    setup_adc_ports(sANx, VSS_VDD);
    lm60_adc_channel = channel;
}

signed int32 lm60_read_temp_mC()
{
    set_adc_channel(lm60_adc_channel);
    delay_us(10);
    unsigned int16 r = read_adc();

#ifdef __LM60_10BIT_ADC
    r = r << 2;
#endif

    unsigned int32 adc_val = r;
    adc_val *= 1000000;

    // calculate result in milli-Celsius
    signed int32 rv = adc_val - lm60_y_intercept;
    rv /= lm60_y_grad;
    return rv;
}

As always I welcome your feedback. Please feel free to post comments / questions below, especially if I’ve made some horrendous gaff! Otherwise, I hope this has been in some way useful and I’ll see you next time.

PWM LEDS Breadboard

PWM LEDS Breadboard

In last few posts I looked at ways to chase a few LEDs and dim them using the PWM functionality of the PIC16F887 which is mounted on the PICkit2 demo board. I decided that I wanted to go a bit further with this though and to combine the two things together and see if I could get some smoother chase action going by using PWM to pulse the LEDs on and off rather than switch them on and off.

I had to go ditch the demo board to do this though as the LEDs on the demo board aren’t really connected to suitable inputs, unless of course you implement PWM in code using GPIO which is possible. I happen to have a PIC18F26K22 knocking about in a 28 pin DIP package, some breadboard and a bunch of other bits and bobs so I decided to build my own demo board for this project.

The first thing I did was to build a basic circuit that would allow me to power, program and run the PIC. I then checked the PIC data sheet to see where the CCP (capture, compare, PWM) pins are and added in a few LEDs along with current limiting resistors. Finally I dug out a 10K variable resistor and wired that up to one of the analog inputs of the PIC. The circuit diagram is shown below.

PWM LEDs Schematic

PWM LEDs Schematic

As the PIC18F26K22 is not supported by the PICkit 2 I also had to get hold of a PICkit 3 programmer. It’s time I just bought the programmer on its own rather than buy one with a demo board. After all, I’m creating my own board now right?

Next I had to figure out exactly what it was that I wanted the thing to do. I decided that I’d keep the chasing LEDs but use PWM to dim the brightness using a set of brightness values derived from a sine wave. Each of the three LEDs would start from a different point in the sine wave so that the first is at 0 degrees, the second at 45 degrees and the last one at 90 degrees. I decided to implement this using a look up table of byte values. The variable resistor, I decided, would be used to control speed.

Having moved to a new PIC, I had to generate a new set of config values. Once again I used the tool built into the MPLAB IDE to do this. I set it to use the internal oscillator and I switched LVP off as requested by the PICKit 3 programmer. Generating the code yielded the following which I added into the main.c code file.

// PIC18F26K22 Configuration Bit Settings

// CONFIG1H
#pragma config FOSC = INTIO67   // Oscillator Selection bits (Internal oscillator block)
#pragma config PLLCFG = OFF     // 4X PLL Enable (Oscillator used directly)
#pragma config PRICLKEN = ON    // Primary clock enable bit (Primary clock enabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable bit (Fail-Safe Clock Monitor disabled)
#pragma config IESO = OFF       // Internal/External Oscillator Switchover bit (Oscillator Switchover mode disabled)

// CONFIG2L
#pragma config PWRTEN = OFF     // Power-up Timer Enable bit (Power up timer disabled)
#pragma config BOREN = SBORDIS  // Brown-out Reset Enable bits (Brown-out Reset enabled in hardware only (SBOREN is disabled))
#pragma config BORV = 190       // Brown Out Reset Voltage bits (VBOR set to 1.90 V nominal)

// CONFIG2H
#pragma config WDTEN = ON       // Watchdog Timer Enable bits (WDT is always enabled. SWDTEN bit has no effect)
#pragma config WDTPS = 32768    // Watchdog Timer Postscale Select bits (1:32768)

// CONFIG3H
#pragma config CCP2MX = PORTC1  // CCP2 MUX bit (CCP2 input/output is multiplexed with RC1)
#pragma config PBADEN = ON      // PORTB A/D Enable bit (PORTB pins are configured as analog input channels on Reset)
#pragma config CCP3MX = PORTB5  // P3A/CCP3 Mux bit (P3A/CCP3 input/output is multiplexed with RB5)
#pragma config HFOFST = ON      // HFINTOSC Fast Start-up (HFINTOSC output and ready status are not delayed by the oscillator stable status)
#pragma config T3CMX = PORTC0   // Timer3 Clock input mux bit (T3CKI is on RC0)
#pragma config P2BMX = PORTB5   // ECCP2 B output mux bit (P2B is on RB5)
#pragma config MCLRE = EXTMCLR  // MCLR Pin Enable bit (MCLR pin enabled, RE3 input pin disabled)

// CONFIG4L
#pragma config STVREN = ON      // Stack Full/Underflow Reset Enable bit (Stack full/underflow will cause Reset)
#pragma config LVP = OFF        // Single-Supply ICSP Enable bit (Single-Supply ICSP disabled)
#pragma config XINST = OFF      // Extended Instruction Set Enable bit (Instruction set extension and Indexed Addressing mode disabled (Legacy mode))

// CONFIG5L
#pragma config CP0 = OFF        // Code Protection Block 0 (Block 0 (000800-003FFFh) not code-protected)
#pragma config CP1 = OFF        // Code Protection Block 1 (Block 1 (004000-007FFFh) not code-protected)
#pragma config CP2 = OFF        // Code Protection Block 2 (Block 2 (008000-00BFFFh) not code-protected)
#pragma config CP3 = OFF        // Code Protection Block 3 (Block 3 (00C000-00FFFFh) not code-protected)

// CONFIG5H
#pragma config CPB = OFF        // Boot Block Code Protection bit (Boot block (000000-0007FFh) not code-protected)
#pragma config CPD = OFF        // Data EEPROM Code Protection bit (Data EEPROM not code-protected)

// CONFIG6L
#pragma config WRT0 = OFF       // Write Protection Block 0 (Block 0 (000800-003FFFh) not write-protected)
#pragma config WRT1 = OFF       // Write Protection Block 1 (Block 1 (004000-007FFFh) not write-protected)
#pragma config WRT2 = OFF       // Write Protection Block 2 (Block 2 (008000-00BFFFh) not write-protected)
#pragma config WRT3 = OFF       // Write Protection Block 3 (Block 3 (00C000-00FFFFh) not write-protected)

// CONFIG6H
#pragma config WRTC = OFF       // Configuration Register Write Protection bit (Configuration registers (300000-3000FFh) not write-protected)
#pragma config WRTB = OFF       // Boot Block Write Protection bit (Boot Block (000000-0007FFh) not write-protected)
#pragma config WRTD = OFF       // Data EEPROM Write Protection bit (Data EEPROM not write-protected)

// CONFIG7L
#pragma config EBTR0 = OFF      // Table Read Protection Block 0 (Block 0 (000800-003FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR1 = OFF      // Table Read Protection Block 1 (Block 1 (004000-007FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR2 = OFF      // Table Read Protection Block 2 (Block 2 (008000-00BFFFh) not protected from table reads executed in other blocks)
#pragma config EBTR3 = OFF      // Table Read Protection Block 3 (Block 3 (00C000-00FFFFh) not protected from table reads executed in other blocks)

// CONFIG7H
#pragma config EBTRB = OFF      // Boot Block Table Read Protection bit (Boot Block (000000-0007FFh) not protected from table reads executed in other blocks)

I also set the clock frequency by setting the IRCF bits in the OSCCON register. This is the first statement in my main() function.

/*
 * The main function.
 */
void main()
{
    // set up the internal oscillator
    OSCCONbits.IRCF = 0b111;  // 16MHz

    while(1)
    {
    }
}

Knowing I was going to need a delay function later on, I defined _XTAL_FREQ at 16MHz and created a millisecond delay function based on the __delay_ms macro.

#define _XTAL_FREQ 16000000

/*
 * Implements a delay of a number of milliseconds.
 */
void delay(int ms)
{
    while(ms-- > 0)
    {
        __delay_ms(1);
    }
}

The approach I took to generating the sine table of brightness values was to calculate the values using a spreadsheet. I arbitrarily decided on 100 values and created a list of angles in 3.6 degree steps. In the column next to it I generate raw sine values for each angle which I multiplied by 128 and added 127. This gave me a range of values between 0 and 255 and I transferred those into an array of values in the program.

/*
 * Defines a list of values that are derived from a sine wave that
 * can be used to smoothly pulse the PWM duty cycle and the LEDs.
 */
const char brightness_values[] = {
    128, 136, 144, 152, 160, 168, 175, 182, 190, 197,
    203, 210, 216, 221, 227, 232, 236, 240, 244, 247,
    250, 252, 254, 255, 255, 255, 255, 255, 254, 252,
    250, 247, 244, 240, 236, 232, 227, 221, 216, 210,
    203, 197, 190, 182, 175, 168, 160, 152, 144, 136,
    128, 120, 112, 104,  96,  88,  81,  74,  66,  59,
     53,  46,  40,  35,  29,  24,  20,  16,  12,   9,
      6,   4,   2,   1,   0,   0,   0,   1,   2,   4,
      6,   9,  12,  16,  20,  24,  29,  35,  40,  46,
     53,  59,  66,  74,  81,  88,  96, 104, 112, 120
};

#define BRIGHTNESS_VALUES_COUNT 100

These values are going to act as my duty cycles to control the LED brightness.

Once I had done that I added in a method to initialise the PIC PWM on the three CCP modules I wanted to use to drive the LEDs. The configuration on the PIC18 is unsurprisingly slightly different to that of the PIC16 I was using before so, having used the previous trick of turning the steps in the data sheet into comments, I went back to the data sheet and worked through each step to make sure everything was properly configured.

I wanted to make sure my resolution was still 8 bits to match my brightness data so having selected a TMRx divider of 8 and a PRx value of 64, I added a new sheet to my ‘useful calculations’ spreadsheet to do the math and check it.

PWM Calculator

PWM Calculator

It turns out that the resolution is 8 bits with those settings and I have a period frequency of 7.7KHz which is great. The final code for the initPWM() method is below.

/*
 * Initialises PWM on the three PWM generators.
 */
void initPWM()
{
    // 1. Disable the CCPx pin output driver by setting the associated TRIS bit.
    TRISC = 0b00000110; // RC1 and RC2
    TRISBbits.RB5 = 1;

    // 2. Select the 8-bit TimerX resource, (Timer2, Timer4 or Timer6) to be
    //    used for PWM generation by setting the CxTSEL bits in the
    //    CCPTMRSx register.
    CCPTMRS0bits.C1TSEL = 0; // user timer 2

    // 3. Load the PRx register for the selected TimerX with the PWM period value.
    PR2 = 0x40; // decimal 64 period

    // 4. Configure the CCP module for the PWM mode by loading the CCPxCON
    //    register with the appropriate values.
    CCP1CONbits.CCP1M = 0b1100;
    CCP2CONbits.CCP2M = 0b1100;
    CCP3CONbits.CCP3M = 0b1100;

    // 5. Load the CCPRxL register and the DCxB bits of the CCPxCON register,
    //    with the PWM duty cycle value.
    CCPR1L = 128 >> 2;
    CCPR2L = 128 >> 2;
    CCPR3L = 128 >> 2;

    CCP1CONbits.DC1B = 128 & 0b00000011; // 128 is 50% duty cycle
    CCP2CONbits.DC2B = 128 & 0b00000011; // 128 is 50% duty cycle
    CCP3CONbits.DC3B = 128 & 0b00000011; // 128 is 50% duty cycle

    // 6. Configure and start the 8-bit TimerX resource:
    //    - Clear the TMRxIF interrupt flag bit of the
    //      PIR2 or PIR4 register. See Note 1 below.
    PIR1bits.TMR2IF = 0;

    //    - Configure the TxCKPS bits of the TxCON
    //      register with the Timer prescale value.
    T2CONbits.T2CKPS = 0b11; // prescale 8

    //    - Enable the Timer by setting the TMRxON
    //      bit of the TxCON register.
    T2CONbits.TMR2ON = 1;

    // 7. Enable PWM output pin:
    //    - Wait until the Timer overflows and the TMRxIF bit of the PIR2 or PIR4
    //      register is set. See Note 1 below.
    while(PIR1bits.TMR2IF == 0){}

    //    - Enable the CCPx pin output driver by clearing the associated TRIS bit.
    TRISC = 0;
    TRISBbits.RB5 = 0;
}

This method can then be called from the main() function prior to the while loop to initialise PWM across the three outputs. I also added a separate method to allow the duty cycles for each output to be set individually.

/*
 * Sets the PWM values for each of the three PWM controllers.
 */
void setPWM(char value1, char value2, char value3)
{
    CCPR1L = value1 >> 2;
    CCP1CONbits.DC1B = value1 & 0b00000011;

    CCPR2L = value2 >> 2;
    CCP2CONbits.DC2B = value2 & 0b00000011;

    CCPR3L = value3 >> 2;
    CCP3CONbits.DC3B = value3 & 0b00000011;
}

All this method does is replace the PWM duty cycle value in each of the CCPRxL and CCPxCON registers. The bit shifting and masking ensures that the LSBs end up in the DCxB bits of the CCPxCON register while the MSBs are written to the CCPRxL registers.

The next task was to add in the code to configure the ADC to read the data from the variable resistor. Again, I took the initialisation procedure from the data sheet and turned it into a set of comments that I could then take my time filling in. There are a couple of things that needed some thought. Firstly the ADC clock needed to be divided by 16 to bring it into the range of speeds that the ADC can cope with. This is all detailed in the data sheet in table 17-1 so there’s no mysterious voodoo in any of that. The other thing that needs to be calculated is the acquisition delay length. The ADC uses an sample and hold circuit based on an RC network and the acquisition time is the amount of time it’ll take to charge the capacitor before performing the conversion. Obviously you need to give it enough time to do that job and the way to do that is to set a period in TAD units in the ACQT bits of the ADCON2 register. Equation 17-1 is provided in the data sheet to help calculate the minimum TAD required. Again, I popped it into my ever growing spreadsheet of useful calculations and selected 12 as my value. This leaves plenty of time for signal capture in this application.

/*
 * Initialises the variable resistor ADC configuration.
 */
void initVR()
{
    // 1. Configure Port:
    //  - Disable pin output driver(See TRIS register)
    TRISAbits.RA0 = 1;

    //  - Configure pin as analog
    ANSELAbits.ANSA0 = 1;

    // 2. Configure the ADC module:
    //  - Select ADC conversion clock
    ADCON2bits.ADCS = 0b101; // Fosc/16

    //  - Configure voltage reference
    ADCON1bits.PVCFG = 0; // +ref is VDD
    ADCON1bits.NVCFG = 0; // -ref is VSS

    //  - Select ADC input channel
    ADCON0bits.CHS = 0; // connect AN0 to the ADC

    //  - Select result format
    ADCON2bits.ADFM = 0; // left justified

    //  - Select acquisition delay
    ADCON2bits.ACQT = 0b101; // 12 TAD

    //  - Turn on ADC module
    ADCON0bits.ADON = 1;
}

As with the previous programs I set the result alignment to be left aligned so that I could return a single byte value that worked across the whole of the potentiometer range. There’s plenty of resolution there for something this simple. Reference voltages again are VDD and VSS (GND) as that matches the potentiometer circuit and the analog input is configured to be taken from AN0 which is multiplexed with RA0.

I also added in a function to read and return the potentiometer value. This simply starts a conversion and waits for it to complete by set the GO bit in ADCON0 and waiting for it to clear. We could use interrupts for this which would be a neater solution but this will do for now. Finally the result is returned.

/*
 * Reads the value of the variable resistor into a single byte.
 */
char readVR()
{
    // start a conversion
    ADCON0bits.GO = 1;

    // wait for it to complete
    while(ADCON0bits.GO == 1){}

    // return the top 8 bits of the value
    return ADRESH;
}

Almost there now, the last thing to do was to update the main function to call of these methods and light up the LEDs.

/*
 * The main function.
 */
void main()
{
    // set up the internal oscillator
    OSCCONbits.IRCF = 0b111;  // 16MHz

    // init the PWM
    initPWM();

    // init the VR
    initVR();

    // brightness counter
    char val1 = 0;
    char val2 = 0;
    char val3 = 0;

    while(1)
    {
        // create a set of values at 45 degrees to each other in the table
        val1 = (val1 + 1) % 100;
        val2 = (val1 + 33) % 100;
        val3 = (val1 + 66) % 100;

        // set the PWM values
        setPWM(brightness_values[val1],
               brightness_values[val2],
               brightness_values[val3]);

        // create a delay proportional to the VR value
        delay((readVR() / 10) + 2);
    }
}

It’s all very straightforward. I created three values to hold individual indexes into the brightness values for each LED. They start at different points in the cycle and increment each time through the loop. The modulus operator resets the index back to the start of the look up table if they wrap around. Meanwhile the delay is set by the potentiometer reading from the AD conversion so that the speed of the chasing can be set. The delay value is manipulated a bit because I wanted to limit it from 2ms to roughly 30ms. Those values were determined through experimentation to see what felt about right.

The complete main.c code is included below.

#include <htc.h>
#include <pic18f26k22.h>


// PIC18F26K22 Configuration Bit Settings

// CONFIG1H
#pragma config FOSC = INTIO67   // Oscillator Selection bits (Internal oscillator block)
#pragma config PLLCFG = OFF     // 4X PLL Enable (Oscillator used directly)
#pragma config PRICLKEN = ON    // Primary clock enable bit (Primary clock enabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable bit (Fail-Safe Clock Monitor disabled)
#pragma config IESO = OFF       // Internal/External Oscillator Switchover bit (Oscillator Switchover mode disabled)

// CONFIG2L
#pragma config PWRTEN = OFF     // Power-up Timer Enable bit (Power up timer disabled)
#pragma config BOREN = SBORDIS  // Brown-out Reset Enable bits (Brown-out Reset enabled in hardware only (SBOREN is disabled))
#pragma config BORV = 190       // Brown Out Reset Voltage bits (VBOR set to 1.90 V nominal)

// CONFIG2H
#pragma config WDTEN = ON       // Watchdog Timer Enable bits (WDT is always enabled. SWDTEN bit has no effect)
#pragma config WDTPS = 32768    // Watchdog Timer Postscale Select bits (1:32768)

// CONFIG3H
#pragma config CCP2MX = PORTC1  // CCP2 MUX bit (CCP2 input/output is multiplexed with RC1)
#pragma config PBADEN = ON      // PORTB A/D Enable bit (PORTB pins are configured as analog input channels on Reset)
#pragma config CCP3MX = PORTB5  // P3A/CCP3 Mux bit (P3A/CCP3 input/output is multiplexed with RB5)
#pragma config HFOFST = ON      // HFINTOSC Fast Start-up (HFINTOSC output and ready status are not delayed by the oscillator stable status)
#pragma config T3CMX = PORTC0   // Timer3 Clock input mux bit (T3CKI is on RC0)
#pragma config P2BMX = PORTB5   // ECCP2 B output mux bit (P2B is on RB5)
#pragma config MCLRE = EXTMCLR  // MCLR Pin Enable bit (MCLR pin enabled, RE3 input pin disabled)

// CONFIG4L
#pragma config STVREN = ON      // Stack Full/Underflow Reset Enable bit (Stack full/underflow will cause Reset)
#pragma config LVP = OFF        // Single-Supply ICSP Enable bit (Single-Supply ICSP disabled)
#pragma config XINST = OFF      // Extended Instruction Set Enable bit (Instruction set extension and Indexed Addressing mode disabled (Legacy mode))

// CONFIG5L
#pragma config CP0 = OFF        // Code Protection Block 0 (Block 0 (000800-003FFFh) not code-protected)
#pragma config CP1 = OFF        // Code Protection Block 1 (Block 1 (004000-007FFFh) not code-protected)
#pragma config CP2 = OFF        // Code Protection Block 2 (Block 2 (008000-00BFFFh) not code-protected)
#pragma config CP3 = OFF        // Code Protection Block 3 (Block 3 (00C000-00FFFFh) not code-protected)

// CONFIG5H
#pragma config CPB = OFF        // Boot Block Code Protection bit (Boot block (000000-0007FFh) not code-protected)
#pragma config CPD = OFF        // Data EEPROM Code Protection bit (Data EEPROM not code-protected)

// CONFIG6L
#pragma config WRT0 = OFF       // Write Protection Block 0 (Block 0 (000800-003FFFh) not write-protected)
#pragma config WRT1 = OFF       // Write Protection Block 1 (Block 1 (004000-007FFFh) not write-protected)
#pragma config WRT2 = OFF       // Write Protection Block 2 (Block 2 (008000-00BFFFh) not write-protected)
#pragma config WRT3 = OFF       // Write Protection Block 3 (Block 3 (00C000-00FFFFh) not write-protected)

// CONFIG6H
#pragma config WRTC = OFF       // Configuration Register Write Protection bit (Configuration registers (300000-3000FFh) not write-protected)
#pragma config WRTB = OFF       // Boot Block Write Protection bit (Boot Block (000000-0007FFh) not write-protected)
#pragma config WRTD = OFF       // Data EEPROM Write Protection bit (Data EEPROM not write-protected)

// CONFIG7L
#pragma config EBTR0 = OFF      // Table Read Protection Block 0 (Block 0 (000800-003FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR1 = OFF      // Table Read Protection Block 1 (Block 1 (004000-007FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR2 = OFF      // Table Read Protection Block 2 (Block 2 (008000-00BFFFh) not protected from table reads executed in other blocks)
#pragma config EBTR3 = OFF      // Table Read Protection Block 3 (Block 3 (00C000-00FFFFh) not protected from table reads executed in other blocks)

// CONFIG7H
#pragma config EBTRB = OFF      // Boot Block Table Read Protection bit (Boot Block (000000-0007FFh) not protected from table reads executed in other blocks)

#define _XTAL_FREQ 16000000

/*
 * Defines a list of values that are derived from a sine wave that
 * can be used to smoothly pulse the PWM duty cycle and the LEDs.
 */
const char brightness_values[] = {
    128, 136, 144, 152, 160, 168, 175, 182, 190, 197,
    203, 210, 216, 221, 227, 232, 236, 240, 244, 247,
    250, 252, 254, 255, 255, 255, 255, 255, 254, 252,
    250, 247, 244, 240, 236, 232, 227, 221, 216, 210,
    203, 197, 190, 182, 175, 168, 160, 152, 144, 136,
    128, 120, 112, 104,  96,  88,  81,  74,  66,  59,
     53,  46,  40,  35,  29,  24,  20,  16,  12,   9,
      6,   4,   2,   1,   0,   0,   0,   1,   2,   4,
      6,   9,  12,  16,  20,  24,  29,  35,  40,  46,
     53,  59,  66,  74,  81,  88,  96, 104, 112, 120
};

#define BRIGHTNESS_VALUES_COUNT 100

/*
 * Implements a delay of a number of milliseconds.
 */
void delay(int ms)
{
    while(ms-- > 0)
    {
        __delay_ms(1);
    }
}

/*
 * Initialises the variable resistor ADC configuration.
 */
void initVR()
{
    // 1. Configure Port:
    //  - Disable pin output driver(See TRIS register)
    TRISAbits.RA0 = 1;

    //  - Configure pin as analog
    ANSELAbits.ANSA0 = 1;

    // 2. Configure the ADC module:
    //  - Select ADC conversion clock
    ADCON2bits.ADCS = 0b101; // Fosc/16

    //  - Configure voltage reference
    ADCON1bits.PVCFG = 0; // +ref is VDD
    ADCON1bits.NVCFG = 0; // -ref is VSS

    //  - Select ADC input channel
    ADCON0bits.CHS = 0; // connect AN0 to the ADC

    //  - Select result format
    ADCON2bits.ADFM = 0; // left justified

    //  - Select acquisition delay
    ADCON2bits.ACQT = 0b101; // 12 TAD

    //  - Turn on ADC module
    ADCON0bits.ADON = 1;
}

/*
 * Reads the value of the variable resistor into a single byte.
 */
char readVR()
{
    // start a conversion
    ADCON0bits.GO = 1;

    // wait for it to complete
    while(ADCON0bits.GO == 1)
    {
        delay(1);
    }

    // return the top 8 bits of the value
    return ADRESH;
}

/*
 * Initialises PWM on the three PWM generators.
 */
void initPWM()
{
    // 1. Disable the CCPx pin output driver by setting the associated TRIS bit.
    TRISC = 0b00000110; // RC1 and RC2
    TRISBbits.RB5 = 1;

    // 2. Select the 8-bit TimerX resource, (Timer2, Timer4 or Timer6) to be
    //    used for PWM generation by setting the CxTSEL bits in the
    //    CCPTMRSx register.
    CCPTMRS0bits.C1TSEL = 0; // user timer 2

    // 3. Load the PRx register for the selected TimerX with the PWM period value.
    PR2 = 0x40; // decimal 64 period

    // 4. Configure the CCP module for the PWM mode by loading the CCPxCON
    //    register with the appropriate values.
    CCP1CONbits.CCP1M = 0b1100;
    CCP2CONbits.CCP2M = 0b1100;
    CCP3CONbits.CCP3M = 0b1100;

    // 5. Load the CCPRxL register and the DCxB bits of the CCPxCON register,
    //    with the PWM duty cycle value.
    CCPR1L = 128 >> 2;
    CCPR2L = 128 >> 2;
    CCPR3L = 128 >> 2;

    CCP1CONbits.DC1B = 128 & 0b00000011; // 128 is 50% duty cycle
    CCP2CONbits.DC2B = 128 & 0b00000011; // 128 is 50% duty cycle
    CCP3CONbits.DC3B = 128 & 0b00000011; // 128 is 50% duty cycle

    // 6. Configure and start the 8-bit TimerX resource:
    //    - Clear the TMRxIF interrupt flag bit of the
    //      PIR2 or PIR4 register. See Note 1 below.
    PIR1bits.TMR2IF = 0;

    //    - Configure the TxCKPS bits of the TxCON
    //      register with the Timer prescale value.
    T2CONbits.T2CKPS = 0b11; // prescale 8

    //    - Enable the Timer by setting the TMRxON
    //      bit of the TxCON register.
    T2CONbits.TMR2ON = 1;

    // 7. Enable PWM output pin:
    //    - Wait until the Timer overflows and the TMRxIF bit of the PIR2 or PIR4
    //      register is set. See Note 1 below.
    while(PIR1bits.TMR2IF == 0){}

    //    - Enable the CCPx pin output driver by clearing the associated TRIS bit.
    TRISC = 0;
    TRISBbits.RB5 = 0;
}

/*
 * Sets the PWM values for each of the three PWM controllers.
 */
void setPWM(char value1, char value2, char value3)
{
    CCPR1L = value1 >> 2;
    CCP1CONbits.DC1B = value1 & 0b00000011;

    CCPR2L = value2 >> 2;
    CCP2CONbits.DC2B = value2 & 0b00000011;

    CCPR3L = value3 >> 2;
    CCP3CONbits.DC3B = value3 & 0b00000011;
}

/*
 * The main function.
 */
void main()
{
    // set up the internal oscillator
    OSCCONbits.IRCF = 0b111;  // 16MHz

    // init the PWM
    initPWM();

    // init the VR
    initVR();

    // brightness counter
    char val1 = 0;
    char val2 = 0;
    char val3 = 0;

    while(1)
    {
        // create a set of values at 45- to each other in the table
        val1 = (val1 + 1) % 100;
        val2 = (val1 + 33) % 100;
        val3 = (val1 + 66) % 100;

        // set the PWM values
        setPWM(brightness_values[val1],
               brightness_values[val2],
               brightness_values[val3]);

        // create a delay proportional to the VR value
        delay((readVR() / 10) + 2);
    }
}

So there it is, sinusoidally pulsed, PWM implemented, variable speed LED chasing. At some point I might look at driving a string of LEDs this way. Perhaps it could be a project for later in the year. In the meantime though I’m going to continue to explore some of the other features of the PIC18F26K22 and see what fun can be had.

Once again, please feel free to contact me with any comments or suggestions. I’m always happy to hear from you!