Stage 3: Write the Embedded Software

Our last posts about specifying the requirements for our design and designing the hardware to meet those requirements have laid the groundwork for everything we need to know and enables us to do the next stage in our design process: write some code for our microcontroller to program our FPGA.

A Haze of Code

The endless list of modern programming languages…

Writing code for computers of any size has evolved hugely over time. From the humble beginnings of punch cards to the modern high level languages like Java and C++, there has been some extraordinary progress in how we instruct computers to perform the tasks we want.

In the modern ecosystem of processing there are different languages for different levels of integration.

Assembler and C code are the usual choice for many embedded level programmers and C++, Java and Python are a significant few for application level programmers.

In this project we’re at the embedded level, so we get to choose between assembler and C. There are some libraries that exist for microcontrollers in C++ but they’re by no means ubiquitous in the industry as of yet.

Choosing between C and assembler is a choice made entirely upon two main aspects: the technical complexity of the task and the code latency/size requirements.

Assembler lends itself well to doing algorithmically simple tasks which are size and speed constrained. That’s because when you’re writing assembly, you’re manually telling the processor which specific opcode you’re using from the processors instruction set one instruction at a time.

C on the other hand lends itself well to the converse. C is much more adapted to doing complex algorithms in timing-relaxed memory-abundant environments.

When you write code in C, the instructions you specify can end up being one or more instructions of assembler in order to complete the task. C also had an added benefit in that it usually has supported libraries for the microcontroller you’re using making the implementation a lot easier for the most used functions. It’s for that reason that we’re going to write the program in C.

Setting up the toolchain

In this design we’re using a Microchip PIC32 microcontroller which has a tool-chain available for download called MPLAB-X.

This toolchain from microchip provides the ability to create projects and manage integration through their MPLAB-X integrated development environment. It does not however include the compiler libraries required in order to compile your source code into a programming file. In order to get your compiler, you have to download the XC compiler intended for your architecture. In this specific instance, we’re using a 32 bit processor, so we select the XC32 compiler.

This is what you need to get started with the PIC and program it “Bare Metal”. However, most manufacturers have preexisting code which can be used in order to make many jobs easier: Middleware.

Middleware is a fairly new phenomenon as microcontrollers become more powerful, complex and cheaper. It enables programmers to interact with an easier to interpret layer of code, removing the difficulty of driving complex peripherals and managing access to these from multiple different processes.

This is also a complexity/latency/size problem, similar to choosing the language it’s written in. For Microchip PIC products this middleware is called MPLAB Harmony. Because we can be fairly liberal with space and we want the processor to perform one specific function at first, we can use this library to do what we need.

This is all the software we need for the computer in order to be able to make our program for our microcontroller. The output of this process is a hex file which you can upload into your design.

Getting this file into the chip however requires a special bit of kit in order to do so- a programming tool.

Microchip has some really good programmers that work just out of the box, namely the PICKIT and the ICD tools. The latest forms of these programmers have had someone from the marketing department have a say in their design which is a shame, but the PICKIT3 is one of the PICKIT family which it preceded. We have a couple of these programmers handy so we’re just going to use one of those.

So that’s the complete toolchain setup. Now we can actually get onto setting up our project and developing/debugging code.

Writing the code

Writing embedded software for a project is not just a technical exercise. If it was then the code we write would likely not be re-usable and would have no structural sense to it.

Planning the software architecture in line with requirements and also building in the ability to re-use modular code blocks is always a necessary activity when doing any design for a product. This means that not only is it easier for others to interpret what’s going on, but also others can pick and choose to use different bits of code from your design in the future to make their job quicker and less risky.

Because of the size and scope of this project, the need to fully architect a piece of software is not as necessary. If this this project was part of a product however, it would be 100% necessary to do so regardless of it’s size.

Just to practically set out what we want to achieve with the code is always a good starting point when writing software. For our example there is a simple two step process that we want this processor to perform in our first iteration of software.

Firstly, we want to send the programming file stored on the microcontroller over SPI to the FPGA. This step includes checking and setting some states of GPIO in the process to ensure that the FPGA is ready to receive the data over the SPI port in the first place. Then, once this programming is complete, we want to flash an LED to show that the programming function has been completed successfully. This is a really simple implementation for our first revision of the software and should get the core of what we want to achieve done.

There are many other things that the processor can do for us but we’re interested in just getting the FPGA up and running, so we’ll do just that. The MCU also needs to be configured by the middleware to provide a clock out to the FPGA as well which we can configure in MPLAB Harmony.

Setting up the project

In order to set up the project we need to go through the steps of setting up an MPLAB Harmony project in the IDE. This is a fairly simple process of setting the name for a project and selectring which processor you want to use. When you have finished the initial setup steps you end up with an MPLAB Harmony screen that has a few tabs.

These are the options- Clock Diagram, Pin Diagram and Pin Settings tabs.

The first port of call is to enable an SPI driver in the middleware so that can just use library calls in our application later in order to write to the SPI. There is a large array of options to select from that will be useful depending on the application. The options needed for this project are shown below.

MPLAB Harmony Middleware Option Selection

Next is to configure the clock tree.

We want to ensure that the middleware configures our clocks so that we get the maximum clock speed out of the MCU and also feed out an adequate clock speed to the FPGA. The maximum clock speed of the MCU is 48MHz, so we can use the auto-calculate function built into the middleware to help us achieve that.

The SYSPLL configures itself to divide the internal RC oscillator (8MHz) to divide by 2, multiply by 24 and then divide by 2 again in order to achieve this. This then also feeds our peripheral bus clock which then in turn is divided by 2 to provide our REFCLK at 24MHz.

There are many more clocking options available on the chip but this solution doesn’t require anything fancy or low power/low speed so what we have configured is more than adequate.

A snapshot of the clock tree tool is shown below.

MPLAB Harmony Clock Tree Tool

The last thing to do, is to set up the project to assign the pins to the different peripheral functions. This a fairly easy process of just filling in a table with all the details of the pin allocations you have made. While ensuring you specify to the IDE what has a special function, what is an input, what is an output and what it’s name is.

We can use the schematic from the previous article in order to fill out the information we need. This allows the middleware to generate functions for the GPIO as well as the SPI interface. Although bare metal GPIO toggling can sometimes be as easy, it is at least another bit of coding taken off our hands.

Once this is all completed and filled in, we can now ask the tool to generate our code. After clicking the generate button, you’re left with a project file tree and files ready to be edited shown below.

Project ready for editing

Adding the functions you need

We asked the middleware in the previous section to generate a file called app.c for us. Unlike bare metal programming where you can do big chunks of work in a main.c file, the middleware gives you individual app files in order to build up your project.

All the initialization of hardware is done for us, and we know it has been initialized because the app.c file would not run until this initialization has been completed.

In a bit of a jump, the major part of the app.c modifications is done below.

/******************************************************************************
  Function:
    void APP_Tasks ( void )

  Remarks:
    See prototype in app.h.
 */

void APP_Tasks ( void )
{

    /* Check the application's current state. */
    switch ( appData.state )
    {
        /* Application's initial state. */
        case APP_STATE_INIT:
        {
            bool appInitialized = true;
            PROG_BOff();
            PIC_LEDOff();
            
            if (appInitialized)
            {
                SPI_DRIVER_REFERENCE_HANDLE = DRV_SPI_Open ( DRV_SPI_INDEX_0, DRV_IO_INTENT_WRITE );
                appData.state = APP_STATE_SERVICE_TASKS;
            }
            break;
        }

        case APP_STATE_SERVICE_TASKS:
        {
            switch (FPGA_Programming_Status){
                case WRITE_BITSTREAM:{
                    PROG_BOff();
                    delay_us(3);
                    PROG_BOn();
                    while(!(INIT_BStateGet()));
                    delay_us(3);
                    FPGA_BITSTREAM_HANDLE = DRV_SPI_BufferAddWrite2 ( SPI_DRIVER_REFERENCE_HANDLE, fpga_bitstream, FPGA_BITSTREAM_SIZE_BYTES, NULL, NULL, NULL);
                    while(!(DRV_SPI_BUFFER_EVENT_COMPLETE & DRV_SPI_BufferStatus(FPGA_BITSTREAM_HANDLE))){    
                        DRV_SPI_Tasks(sysObj.spiObjectIdx0);
                    }
                    FPGA_Programming_Status = WRITE_BITSTREAM_COMPLETE;
                    break;
                }
                case WRITE_BITSTREAM_COMPLETE:{
                    PIC_LEDOn();
                    delay_us(1000000);
                    PIC_LEDOff();
                    delay_us(1000000);
                    break;
                }
                default:{
                    FPGA_Programming_Status = WRITE_BITSTREAM_COMPLETE;
                    PIC_LEDOff();
                    PROG_BOn();
                    break;
                }
            }
            break;
        }
        default:
        {
            /* TODO: Handle error in application's state machine. */
            break;
        }
    }
}

The major section that we are interested in begins at line 167. The switch()statement is letting our application run through a state machine where it writes the bitstream first and then flashes the LED once writing the bitstream has been completed. If for any reason the switch statement goes into an unknown state, then it will go to the state where it just flashes an LED. From line 169 onward we are following the process dictated in Chapter 8 of the Xilinx Spartan-3 Generation Configuration User Guide in order to program the FPGA. In summary, we need to:

  1. Ensure PROG_B is low
  2. Wait 3µS
  3. Ensure PROG_B is high
  4. Wait for INIT_B to go high
  5. Wait for another 3µS
  6. Ask the middleware to write our bitstream over SPI to the FPGA
  7. Wait for the middleware to say it has been done
  8. Go into the flashing LED state to indicate the process is completed.

Asking the middleware to write the bitstream requires using the API provided by the middleware. The function DRV_SPI_BufferAddWrite2()function adds an action for the middleware to take the information passed to it and write out when it is serviced.

To tell the middleware what exactly it is you want to write out, you must pass it a reference to the handler that was used to initialize the SPI driver in the first place (which I won’t explain here),  a pointer to where the bitstream lives so it knows where to get it’s data from and the size of the transmission in bytes. This is exactly what we are doing on line 174.

This is a fairly brief overview of the code presented above which is now in a state algorithmically to program our FPGA. The only other thing left to do is to store our FPGA bitstream in the code.

We want the FPGA bitstream to live in the code but we also want it to live in the flash. Program variables usually get initialized in the MCU’s RAM and so we need to explicitly state that we want this specific static constant to live in the flash. This is done by using the __attribute__ (space(prog))directive when declaring the variable, which will tell the compiler that you can only put this in program space i.e flash. Our variable containing the bitstream is declared in the format of const uint8_t fpga_data[FPGA_BITSTREAM_SIZE_BYTES] __attribute__ ((space(prog), aligned(32), keep)) = {/*FPGA BITSTREAM HERE*/}.
This constant has global scope and also lives in it’s own C file. This is done for two reasons. The first being that the array size is huge and takes up thousands of lines in a single file. If this constant was part of the app.c file it would make it huge. The second reason for choosing this way means that we can write a quick python script to interpret the output of the FPGA design tools and construct a C file for us that represents that data.

The script can also integrate it into the project space for us so the process of updating the FPGA is now as simple as make the bitstream, run the python script to turn it into a C file and then compile the MCU. This helps massively with quick prototyping of FPGA builds.

That concludes the writing of the code we need! Now we just need to compile an example project (an FPGA setting an LED on) and upload this to the board so we can test it.

Uploading and running the design

Uploading the design is done using the 6 pin header and the PICKIT mentioned before.

The PICKIT connects via USB and can be programmed straight from the IDE by just clicking the program button. Conversley, there is also the IPE provided by Microchip which is a standalone production level software tool used to program PIC microcontrollers on the factory floor where people done have the IDE installed.

MPLAB IPE

In order to program a microcontroller, all that is required is that the power to the device being programmed is on, clicking the connect button, selecting your hex file to program into the MCU and clicking program. Upon clicking the connect button the IPE will tell you if it was successful or not in detecting the microcontroller which is really handy.

Once this is done, the program should automatically release the microcontroller from reset and work! After programming the MCU, the board kicked into life shwon below.

So great news! It looks like the MCU is working and also programming the FPGA! So to sum up what’s been achieved we:

  • Decided on what language to use
  • Decided on a loose software architecture
  • Created the project with the right middleware setup we needed
  • Added our specific modifications so that we get the functionality we want
  • Programmed the MCU with our design and verified that it was successful.

In a normal design there are many more stages and much more intricate software control practices needed to create a great electronic design. Have a look at we do in order to meet the needs of our customers in the field of embedded software design or feel free to contact us for more information.

In the next and final article we’ll explore what it takes to program FPGA’s.

Read the next article in the series here