Friday, August 8, 2014

Reading HMC5983 sensor data asynchronously over SPI

In the current context “asynchronously” mean that reading samples from the magnetometer will take place asynchronously with regards to the main execution thread on one or more interrupt threads. Thus the main thread, which most probably will be our control loop, may go through its calculations completely oblivious to the fact that its operation will be interrupted for a few microseconds to deal with the new data sample arriving from HMC5983. However, when the main thread needs a fresh data sample from the magnetometer, it most probably will be ready for consumption without any unnecessary wait.

If you may recall from the previous post, the HMC_Reset(…) function, invoked as part of initialization, configured HMC5983 for Continuous-Measurement Mode. According to the HMC5983 datasheet: “In continuous-measurement mode, the device continuously performs measurements and places the result in the data register. RDY goes high when new data is placed in all three registers“.  So what is this mysterious RDY that goes high?

From the same datasheet, “RDY - Ready Bit. Set when data is written to all six data registers. Cleared when device initiates a write to the data output registers and after one or more of the data output registers are written to. When RDY bit is cleared it shall remain cleared for >200 μs. DRDY pin can be used as an alternative to the status register for monitoring the device for measurement data.”

So, what is DRDY pin? It is Pin 15 of HMC5983 package: “DRDY – Data Ready Interrupt pin”.  Thus by connecting DRDY pin to one of the available pins on our MCU and associating this pin with the MCU Interrupt controller using Peripheral Pin Select (please see _HMCInitPinMap() function) with proper configuration we may trigger an interrupt and switch control from the main thread to the Interrupt Service Routine (ISR) as soon as magnetometer has a new measurement!

HMC_AsyncStart() function, defined in the HMCSPI_Async.c file, performs this “proper configuration” by simply enabling respective interrupt. It also sets the flag indicating that asynchronous data read from the sensor is enabled (_HMC_Async = 1) and resets to 0 counter of the samples (_HMC_Ready = 0) in asynchronous read buffer. The importance of this counter will become clear shortly. HMC_AsyncStop() function implemented in the same file reverses the process by resetting asynchronous operation flag, discarding samples in the buffer, and disabling interrupt.

Functions HMC_AsyncReadIfReady(…) and HMC_AsyncReadWhenReady(…) provide access to the sensor data accumulated in the asynchronous read buffer. The difference between these functions is that the former returns error code if there is no yet new data in the buffer, while the latter waits until the data becomes available. For the actual data retrieval from the asynchronous buffer both of these functions call internal helper routine _HMCAsyncRead(…). In my code, as you may have noticed, the names of internal functions and variables, which are not exposed to the API caller, are prefixed with “_”.

_HMCAsyncRead(…) function deserves a closer look. This function needs to extract the sample from the asynchronous read buffer and modify (actually, reset to 0) the counter of the samples in this buffer. However, both the buffer and respective counter could also be modified by the interrupt routine – how can we serialize access to these data structures so the main thread (on which _HMCAsyncRead(…) is being executed) and interrupt routine do not step on each other’s toes? Usually Operating Systems provide some primitives to provide thread synchronization – like locks, mutexes, semaphores, etc., but here we work on a “bare metal” – our code IS the Operating System, so we have to implement serialization ourselves.

PIC architecture defines 8 priority levels available for the main thread and user ISRs; typically the main thread runs at the lowest priority of 0, while ISRs are configured to run at higher priorities, which allows them to interrupt main thread. Actually, another way to disable an interrupt routine is to set its priority to 0 so that it will not be able to interrupt the main thread. However the recommended way to implement serialization is to create a so-called “critical section” by temporarily raising priority of the main thread to the level of interrupt routine that we would like to block.

If you recall, HMC_Init(…) function as its first parameter takes the numeric value which defines the priority level (interrupt level) of all the interrupt routines configured in the HMCSPI module. This value is also stored in the static variable _HMC_IL and is made available to all routines in the module. Compiler provided function SET_AND_SAVE_CPU_IPL(current_cpu_ipl, _HMC_IL) stores current priority level in the local variable and sets the current CPU priority to the value stored in _HMC_IL effectively blocking ISRs defined in the HMCSPI module thus creating a “Critical Section”, which protect consistency of the data shared between the main thread and ISRs. Function RESTORE_CPU_IPL(current_cpu_ipl) restores priority to the original level thus terminating the Critical Section.

It is important to maintain the duration of the Critical Section at minimum, so in the Critical section I usually just copy data from shared to some local storage and perform any further data processing outside of the Critical Section.

Now let’s see how the data gets into this asynchronous data buffer that we got to so much trouble to protect. The actual read of the HMC5983 sample data performed by two inter-related interrupt routines - HMC_Interrupt() and SPIInterrupt() implemented in the code file HMCSPI_ISR.c.

If asynchronous processing of the data samples in enabled through the call to HMC_AsyncStart() function, raising edge on the DRDY line will trigger External interrupt and transfer control to HMC_Interrupt() function. First this function checks the availability of the SPI bus (some synchronous operation may be using bus at this time) and, if available, acquires it. This is achieved through the call to inline function _HMC_AcquireBus(). If you recall, the same function is being used by synchronous _HMC_RegRead(…) and _HMC_RegWrite(…) functions, which are usually called on the main thread. To protect against potential collision due to race condition in checking and acquiring bus _HMC_AcquireBus() also implements Critical Section protection.

If SPI bus is busy, HMC_Interrupt() function just return without any further processing – this sample will be missed and reading will restart when the new sample will be made available by the sensor and indicated by raising DRDY line. If the SPI bus was acquired successfully, interrupt function formats the FIFO buffer template, enables SPI interrupt, and pushes formatted FIFO template into the SPI FIFO buffer. This ends processing the DRDY interrupt – the whole processing takes just a few microseconds!

14 microseconds later (transferring 7 bytes over 4 Mbs SPI bus) transmission will be complete and SPI bus will raise SPI interrupt; this interrupt will transfer control to SPIInterrupt() function. This function retrieves bytes from the FIFO read buffer and converts them into the raw sensor measurements for X, Y, and Z axis. Then comes an interesting point – if _HMC_Ready is 0, the newly read sample stored in the asynchronous read buffer _HMC_Sample, otherwise the new sample is added to the previous values in _HMC_Sample buffer. _HMC_Ready is incremented in either case.

The advantage of this approach is that if our control loop runs at lower frequency than sensor’s ODR, the next call to one of the asynchronous read functions will return AVERAGE of the samples accumulated since the previous call. This implements some additional low-pass filtering without much overhead.

Project 13-HMCSPI-Async brings all of this asynchronous functionality together in a small demo program that reads data from the sensor and submits it for logging over the SerialData Logging interface. The output is identical to the one discussed in the previous post and uses the same log file template for parsing.


Now, equipped with the deep understanding of interrupt-driven IO processing we can bravely jump into issues of I2C communication, which will be the topic of the next post.

Wednesday, August 6, 2014

Communicating with HMC5983 Magnetometer over SPI

My new board uses new magnetometer form Honeywell – the HMC5983. This one has serious advantages over its predecessors – automatic temperature compensation, much higher sampling rate up to 220 Hz, and a choice of protocols – I2C (which was present in older sensors) and SPI.

I did a lot of work with I2C in my previous designs and even found a way to talk to multiple devices on the same bus in the interrupt-driven mode, but I found I2C rather slow and too “chatty” and too heavy on the interrupt system as it generates interrupts per each byte transferred. There are several good reviews comparing I2C and SPI protocols and providing some implementation details for each: I²Cvs SPI, SelectingBetween I2C and SPI, Introductionto SPI and many, many more. Anyway, for this project I decided to use SPI to communicate with HMC5983.

SPI is rather simple protocol and, in my implementation, one of the PIC24EP512GP806 SPI modules is dedicated to communication with HMC5983 so I decided against creating a separate SW module to encompass SPI functionality – both SPI bus management routines and functions specific to HMC5983 are integrated in one SW module – HMCSPI available in my code library.

As usually in my SW modules, header HMCSPI_Profile.h provides definitions required to link the code to specific SPI and Interrupt modules of MCU and define the MCU’s pins dedicated to control and communication with HMC5983. Please note that in this board HMC5983 connected to PIC24EP512GP806 using SPI2 module, which has dedicated pins for SDO, SDI, and SCK. If any other available module would have been used (SPI1, SPI3, or SPI4) we would have to link respective pins to the module using PIC’s Peripheral Pin Select feature that we touched upon in the previous post.

HMCSPI_Local.c and HMCSPI_Local.h allocate space and provide definitions for some variables, flags, and structures shared among the routines of the module. Those variables that may be changed both in the main thread code and in interrupt service routines (ISRs) are tagged as “volatile” to provide compiler with the proper hint. Also please note that there are several vectors defined in these files for Hard and Soft Iron correction – for now they are just initialized; how to fill them with the values specific to the sensor and the board we will discuss in the post devoted to HMC5983 calibration. Vector module implementation consists of one header file Vector.h, which defines Vector structure and provides inline functions implementing various vector operations.

Header HMCSPI_Inlines.h provides definitions for several inline functions that assist with some basic HMC5983 data transformations and SPI bus control. These functions are rather trivial and are well documented with the code comments. Header HMCSPI.h provides HMCSPI API definitions required for external programs to use functionality exposed by this module; this is the only header file that need to be included in the programs that use HMCSPI module.

Prior to using HMCSPI module’s functionality it needs to be initialized using the call to HMC_Init(…) function defined in HMCSPI_Init.c file. Except for the Interrupt Level (priority of the interrupts associated with the module), all other parameter are just being passed to HMC_Reset(…) function, which we will discuss shortly, for sensor configuration. Other than that, HMC_Init(…) performs initialization of some data structures, configures and resets interrupt lines, and initializes and configures SPI HW module.

Every line in this function is provided with comments explaining what is being done. However, it is important to mention that SPI module is configured to enable 8-deep FIFO buffer and the SPI interrupt is configured to take place after all the bytes loaded into FIFO are transmitted. All the data communication with HMC5983 implemented in the module is limited to 7 bytes – 1 byte for register address and 6 bytes of sensors data – two bytes for each X, Y, and Z; individual registers reads and writes are even shorter – from two to four bytes. Thus all required I/O operations can be completed with just one load into the FIFO buffer. If I would have to transmit more than 8 bytes, I probably will trigger interrupt when FIFO is half-empty to add more bytes for data transfer. In this case the situation would be quite similar to the one we discussed in relation to SerialData Logging in the previous post. Anyway, while looking at this function it would be handy to keep open SPI FamilyReference document for this MCU.

At the end of initialization HMC_Init(…) calls HMC_Reset(…) function, defined in the HMCSPI_Reset.c code file, to perform initialization of the magnetometer itself. Parameter for this function are documented and explained in the code. It is important to note that this function calculates and stores conversion factor, based upon the Gain parameter, to be used for converting raw sensor data into the actual strength of magnetic field in Ga.

As part of the reset process sensor is put into the Continuous-Measurement Mode, in which the sensor automatically initiates new measurement at the defined Output Data Rate (ODR parameter). More detailed explanation of register settings required for sensor initialization is provided in the HMC5983 datasheet.

Now that initialization is completed, we can start looking at synchronous communication with the HMC5983 sensor. By “synchronous” in this context I mean that all operations are performed on the main thread and all call for read and write are blocking. At the core of the synchronous communication are 3 internal functions (localized within the module and not exposed to the users of HMCSPI module) - _HMC_IO(…), _HMC_RegRead(…), and _HMC_RegWrite(…). All synchronous I/O functions, including both internal and external – exposed to the API users, are implemented in HMCSPI_Sync.c code file.

_HMC_IO(…) function accepts a byte array, loads it into the FIFO buffer, and then waits until transmission is completed. At the end of transmission it reads received data from the FIFO buffer and returns it to the caller. It also indicate for the sensor that operation is ended by de-assering CS line through the call to CS_Stop() macro. Please note that CS line is active-low, so the CS_Stop() macro actually raises CS line to Vdd. Function does not check the length of the data array, so it is responsibility of the caller to make sure that the buffer provided does not exceeds the FIFO depth.

SPI operation is fully duplex, so from the perspective of a low-level I/O both reads and writes are identical, thus we can use single routine, _HMC_IO(…), for both reads and writes. The differentiation factor between reads and writes is actually the flag in the first transmitted byte. Configuring this byte for respective operation is actually the function of the other two helper functions - HMC_RegRead(…) and _HMC_RegWrite(…).

These two functions are almost identical – the only difference is that one sets the flag in the first byte for Read, and the other one – for Write. The other two fields in the first byte are the sensor’s register address from which we want to read data or into which we want to write, and the “auto-increment” flag. If this flag is set, then for a multi-byte operation the first read (or write) will be from the register identified by the Register Address, but for each subsequent byte the register address will be incremented. Considering, for example, that sensor data is located on HMC5983 in six consecutive 1-byte registers starting at the address 0x03, by setting the “auto-increment” flag and providing address 0x03 in the first byte we can read all six bytes in one multi-byte operation.

As parameters these two functions receive the HMC5983 register address and the data buffer from which to write to the sensor or into which to read sensor data. Functions validate parameters to make sure that the requested amount of data does not exceed the sisze of the FIFO buffer, and then format the data into the template of FIFO buffer.

Next these functions attempt to “acquire” control over the SPI bus – the bus might be busy if the asynchronous (on the interrupt thread) operation is taking place; we will look at the details of asynchronous operation shortly. Anyway, if the bus is busy, _HMC_RegRead(…) and _HMC_RegWrite(…) will wait until it is free, acquire it into exclusive use, and pass the formatted FIFO template buffer to _HMC_IO(…) function to perform actual operation.

Based upon these helper functions multiple register read/write functions are implemented – they are trivial in their nature and are just the wrappers around _HMC_RegRead(…) and _HMC_RegWrite(…) functions. HMC_Reset(…) function, which we discussed earlier, uses register Read/Write functions implemented in HMCSPI_Sync.c code file to perform initialization/configuration of the sensor.

Project 12-HMCSPI-sync brings all of this functionality together in a small demo program that reads data from the sensor and submits it for logging over the Serial Data Logging interface. The data logged includes the timestamp (in microseconds) of the sample, X, Y, and Z values of the magnetic field, and sequential number of the sample. To parse this log I use the XMLconfiguration file for my utility discussed in SerialData Logging post. The timestamp value scaled down to seconds by using the FieldWeight=”0.000001” scale factor. Values for magnetic field are rounded to 3 digits after the decimal point.

This is the fragment of the Excel file that the log file is being converted to:
TS
MX
MY
MZ
Count    
5.017436
41.284
-219.266
322.936
1
5.021849
39.450
-223.853
322.936
2
5.026263
40.367
-222.018
324.771
3
5.030672
43.119
-220.183
323.853
4
5.035085
39.450
-220.183
322.936
5
5.039500
39.450
-221.101
322.936
6
5.043911
41.284
-222.018
324.771
7
5.048328
40.367
-222.018
321.101
8
5.052745
36.697
-221.101
323.853
9
5.057160
41.284
-223.853
326.605
10
5.061575
39.450
-222.018
328.440
11
5.065989
38.532
-221.101
325.688
12
5.070406
41.284
-222.018
322.018
13





At highest ODR HMC5983 provides measurements at a rate of about 220 Hz, which is roughly every 4.5 msec. Using synchronous read routines, which we discussed thus far, we would have to wait a few milliseconds whenever we need to read next measurement from the sensor – and this time would be purely wasted eating into the time allotted for the control loop. The answer to this problem – implement asynchronous communication with HMC5983 using PI24EP512GP806 interrupt system, which will be the topic of the next post.

Wednesday, July 30, 2014

Serial Data Logging

When developing and debugging firmware for a MCU, which is performing some quite extensive processing of the data from multiple sensors, it is very important to be able to look at this data as it is being collected and processed. Similarly, when tuning, for example, PID parameters it is critical to capture the RC input, attitude data, and control signals to motors. All of this points to a need to capture data from the board and subsequently transfer it to a PC for analysis.

Now that we identified the need for some form of the data transfer from MCU to a PC, we need to decide on the protocol for this communication. Article SerialProtocols Compared provides an overview of popular serial protocols.
  • RS-232 (in its “modified” form adopted for MCUs) protocol is an obvious choice for implementing communication between MCU and PC the following reasons:
  • Most MCUs (including my PIC) provide some form of a HW support for RS-232 communication in the form of UART modules. Actually, the PIC24EP512GP806, which I use for my board, supports four independent UART modules. 
  •  Most PCs support RS-232 serial protocol either through dedicated or emulated (over USB) serial ports.
  • There is a lot of third-party HW that supports RS-232 communication – FTDI cables, XBee wireless modules, Bluetooth modules, data loggers – just check Sparkfun.com.
  • RS-232 considerably fast – for PIC-to-PIC communication over a short range or PIC-to-PC communication over the FTDI cable it is reasonable to boost the speed to 1 Mbps; for wireless communication, for example – over a pair of XBee modules, speed usually limited to 115.2 Kbps.
  •  One MCU pin is required to implement RS-232 transmitter or two pins if you would like to implement HW Flow Control; I opt for the second choice.
Thus, my decision for this board was to use one of the available four UART modules to implement Serial Data Logger (SDLModule) interface. Project “11-SDL” tests SDL interface. However, prior to jumping into the implementation details, there are two issues that we need to discuss.

UART data stream is just a stream of bytes – serial protocol does not define any structure over this stream. From the logical perspective, we will be passing messages, so on the receiving end we need to know where one message ends and the new one begins so that we may correctly parse the stream and recreate the message even if some of the messages got distorted during transmission. In my implementation, I decided that every message would start with 0x5555 and end with 0xAAAA. For obvious reasons we could not use all 0s or all 1s (appear quite often in the data), but why this specific combination? No particular reason, except, maybe, that in both of these code words each successive bit is the inverse of the previous one (0x55 = 0b01010101 and 0xAA = 0b10101010), which results in a visually appealing and highly visible pattern in the logic analyzer.

Even at 115.2 Kbps transmitting 30-40 bytes takes between 2.5 and 3.5 msec – an eternity from the viewpoint of the processor running at 64 MIPS. During this time processor may execute about 20,000 commands – perform the whole iteration of the control loop! Thus, the implementation of data logging should be asynchronous with regard to the main execution thread with the maximum delegation of the whole transmission to the HW. Luckily, presence of the HW-based UART module together with the Interrupt system allows us easily achieve this goal on PIC.

Before we start discussing the code, a few words about the UART implementation on PICs and the timing errors. PIC supports 2 modes for UART timing – the so-called Standard mode and High-Speed mode. In Standard mode 16 timing pulses required for each bit, while in High-Speed mode only 4. The frequency of the timing pulses depends on the command frequency Fcy (which we set at 64 MHz) divided by the value in the Baud Rate Generator (BRG) register. More details on Baud rate generation and overall UART module operation provided in Microchip document UniversalAsynchronous Receiver Transmitter (UART). The important thing to remember is that with this mechanism most of the typical communication frequencies could not be defined exactly – there will be some error! However, certain amount of timing error is quite acceptable for serial communication – article TimingErrors in Serial Communication provides great background on the issue and defines the upper bound on the timing error acceptable for reliable communication.

Important thing to remember is that High-Speed mode allows for more precise setting for the communication frequency resulting in the lower error. I created an Excel file that calculates BRG values for typical communication speeds together with the respective errors for both UART modes assuming that the Fcy=64MHz.




Advantage of the Standard mode is that due to the larger number of timing pulses (16) the data line is sampled 3 times – on the 7th, 8th, and 9th pulse significantly reducing potential for read error. With just the four timing pulses in the High-Speed mode multiple sampling is impractical. However, multiple sampling important only for the UART Receiver; as here we are implementing UART Transmitter I opted for High-Speed mode, which reduces the timing error.

Now we may start looking at the code of the SDL Module. Header file SDL_Profile.h provides definitions that allow to associate code with one of the four available on the MCU HW UART Modules. Most of the HW modules (UART including) of the PIC MCU are not tied permanently to some MCU pins – when needed for particular design, modules may be associated with respective pins using advanced feature – Peripheral Pin Select. _SDLInitPinMap function, defined in SDL_Profile.h implements exactly that.

Header SDL.h defines thre functions implemented by SDL module – SDLInit, SDLPostIfReady, and SDLPostWhenReady. The former one initializes the module specifying required Baud rate and the interrupt level to be used by the module. SDLPostIfReady initiates data transmission if the SDL module is Ready – that is the previous transmission has completed. SDLPostWhenReadywaits until the SDL module is Ready, and then initiates data transmission. Both functions copy the data to be transmitted into the internal buffer and then load the message header (0x5555) into the UART FIFO buffer, at which point they return to the main thread without waiting for the transmission to complete.

The rest of the data transmission is carried out in the UART interrupt routine, defined in SDL_ISR.c, which is invoked when the FIFO buffer is empty. Interrupt routine moves bytes from the internal buffer into the UART FIFO buffer and immediately returns control. If the internal buffer exhausted, interrupt routine pushes into the FIFO buffer message trailer (0xAAAA) and set the indicator that SDL Module completed transmission and is ready for the next one.

While the actual transmission of the message may take several milliseconds, the UART routine will be active just a few microseconds during this time, leaving the rest of the time for the execution of the main thread.
Project 11-SDL provides the test platform for the SDL Module and may serve as a very trivial sample of usage. In the later projects we will see more examples of the SDL usage.

Finally, a few words about the practical use of SDL module – it is not enough just to send data, we need to have some ways to receive and process it! In my development depending on specific requirements (speed vs. flexibility) I use either an FTDI 3.3 V cable to link UART from my board to the USB port on my computer or a pair of XBees – one connected to the UART on the board and the other – to again the USB port using XBee Explorer USB from Sparkfun.com. To collect detailed telemetry during the flight I use MicroSD data logger (SD-Logger-V2).


To make sense of this log data on a PC I developed two Windows-based programs – LogStreamer to read the log data directly from the USB port and LogConverter to read the log file from the MicroSD card. Both programs store the log as a tab-delimited text file ready for load into Excel. Both programs rely on a XML-based format file, which defines the format of the data in the log. SDLTest.xml is the format file to interpret the log produced by the 11-SDL project. You are welcome to use these programs under the MIT License.

The format template file can be quite elaborate and detailed discussion of these log processing routines might be a topic for a separate blog. However we will be using them quite often while working with the sensors and will be looking at more templates, so their structure and basic usage should become obvious.

This closes the logging chapter and now we may start looking at sensors. The first one will be HMC5983 magnetometer connected over the SPI bus, but that will be the topic of the next post.

Thursday, July 24, 2014

The first project – Initialization and Timing

When I start with the new board, I develop firmware in successive steps with each step represented as an MPLab-X project. The first project that I do is usually the “10-Basic”. The goal of this project is to establish basic MCU initialization, configure oscillator to set the speed of MCU (INIT module), and develop (or migrate from the previous release) timing service (TMR module) and basic “information” service to control on-board LED and/or external LED/buzzer (BLI Module).

The first step is to decide at what speed we would run the MCU. PIC24EP series of MCUs could be configured to run at a speed of up to 70 MHz. This is the instruction cycle speed, which usually designated as Fcy. The first impulse would be to configure it to run at maximum allowed speed – that is at Fcy = 70 MHz. However, Fcy plays critical role in configuring other timing parameters – the frequencies of stand-alone timers as well as frequencies of multiple peripherals including UART, I2C, and SPI speed. These frequencies are defined as fractions of Fcy and these fractions are most often the powers of 2. Taking these fractions of 70 MHz will result in very odd numbers. For example, if we set a divider (usually called “prescaler”) for a timer to, let’s say, 64, the timer update interval will be 1.09375 usec – not an impossible number to work with, but also not an easy one to perform precise time calculations.

Thus, my decision was to reduce the speed of MCU by about 9% to 64 MHz for the sake of simplifying various timing calculations. This also provides some safety margin as the higher the frequency of MCU operation the more stringent are the requirements to the operation temperature. At 64 MHz (or MIPS) PIC24EP can withstand temperatures up to about 115 degrees Celsius, while at 70 MIPS it is limited to 85 degrees.

The board is equipped with the 12 MHz crystal oscillator. However, PIC MCU provides a PLL frequency multiplier and a pre-scaler and post-scaler registers so it is quite easy to convert any crystal frequency to almost any Fcy subject to some limitations for intermediate frequencies. My selections for the values of these parameters are documented in the Init.c file. If you are interested in specifics od oscillator configuration, they are pretty well documented in the Family Reference Manual for respective MCU family available at www.microchip.com.

The board has 4-position DIP switch – the initialization routine (Init.c) configures associated pins and captures their values. As this pins are read and their values set only during initialization, so flipping them over after the board already powered up does not have any effect.

Besides configuring the oscillator and reading switches, Init.c performs some other initialization functions – configures MCU global control registers (“fuses” in PIC terminology), disables all HW modules (like I2C, SPI, etc.), disable all interrupts while enabling interrupt nesting, configures all analog-capable pins as digital. Initialization interface defined in Init.h and switches values are exposed through Switches.h.

It is critical for the flight controller to have access to precise timestamps to be able to calculate derivatives and integrals of the sensors’ values, as well as rather useful to have access to other timing features, such as well-defined delays (“sleeps”) etc. This functionality is provided by the TMR module. Timer module utilizes three HW timers – 16-bit Timer1 for low-precision functions and a pair of even-odd 16-bit timers to create one high-precision timer. The high-precision timer provides timestamps with the resolution of 1 usec, which is sufficient for the flight controller. TMRProfile.h file defines which pair of the available HW timers should be combined into a 32-bit high-precision timer.

Timer1 is configured to generate interrupt every 125 usec; the interrupt routine increments 32-bit counter. This counter of 125 usec intervals, which I call “ticks”, is used to implement “delays” and “alarms”. Let’s say, you have an operation that may take from 3 to 5 msec depending on values of some parameters and other conditions, that you perform in a loop. However, you would like to repeat this loop exactly every 10 msec, so, depending on the duration of calculation in the loop, at the end of the loop you need to implement a variable delay from 5 to 7 msec. This can be easily achieved with the “alarm” – at the beginning of each iteration of the loop you set alarm for 10 msec and then perform the required calculation, at the end of the calculation before ending the iteration you wait for the pre-set alarm – bingo! Each iteration of the loop will take exactly 10 msec!

Another very important feature provided by the Timer module is the callback after a certain period of time. TMR.h provides prototype for the callback function, which should be implemented by the user. TMRCallBackAfter function allows to register user’s callback function with the Timer module and specifies when it should be called. When the required interval passes, the callback function will be invoked on the interrupt routine asynchronously to the execution of the main code. As usual with the interrupt routines, callback function should not perform any lengthy operations as it will block main execution thread for the duration of the callback. Active callback request can be cancelled by calling TMRCallBackDiscard function. My current implementation allows just for one active callback at any given time; however it would be quite easy to change the code to allow multiple active callbacks – just did not see practical use for it thus far. BLI module, which we will discuss shortly, provides an example of the need for the callback functionality.

BLI Module (“blinker”) provides functions to control on-board LED and/or external LED/buzzer. The MCU pin, associated with the LED control, is defined in BLILocal.h – did not see a need to create a separate “profile” header for this simple definitionJ. Blinker module provide functions to directly control LED (and buzzer, if connected) – BLISignalON, BLISignalOFF, and BLISignalFlip.

When the flight controller performs some lengthy operation, like initialization or calibration of sensors, or waiting for “arming”, or waiting for GPS to obtain first fix, it would be nice to inform user that an operation is taking place by blinking the signal LED at certain frequency or in certain pattern. It is possible to achieve this with the functions providing direct control over the LED, but would be extremely cumbersome. This is where Timer callbacks come very handy!

Blinker module may set the LED control and then subscribe for a Timer callback. In the callback function, which will interrupt the main routine performing the lengthy operation for a very short time, Blinker may flip the LED and re-subscribe for the next callback. BLIAsyncStart function defines blinking pattern, subscribes for callback, and immediately returns so that the main thread may initiate some lengthy operation, during which the Blinker will happily blink requested pattern on its own. When the lengthy operation completes, blinking can be stopped by a call to BLIAsyncStop.

This worked fine for me for quite some time until I exhausted my fantasy coming up with distinctive blinking patterns. To address this problem, I added to the Blinker the Morse table and BLIAsyncMorse function, which accepts as a blinking pattern some character string and then blinks it asynchronously. Morse blinking can be terminated by the call to BLIAsyncStop.

These three modules – INIT, Timer, and Blinker – are the first I implement for any new board. Project 10-Basic test the functionality of these three modules and allows to build upon them in the further development.


In the next post we will look at the asynchronous implementation of the serial UART data sender.

Wednesday, July 23, 2014

The board

There were several design requirements that I tried to implement while going from board Version 2 to Version 3, which we are discussing here:
  • Standardize size.
When I was working on Version 2 board, I was not aware about specific size limitations – there was no standardized quad frames available, so I picked some size for the board rather randomly. With the Version 3 I decided to make the board compatible with the broad range of available frames by matching the size and mounting holes of the KK and MultiWii flight controllers.
  • Add more and upgrade existing on-board sensors.
Added MPL3115A barometric altimeter and replaced HMC-5883 magnetometer with the next generation HMC-5983.
  • Increase number of PWM outputs (from 4 to 8)
With the idea to control quad-, hexa-, or octa-copter and/or provide feed for camera orientation control.
  • Provide better vibration rejection.
Insulating mounting like rubber bumpers, vibration tape, etc. are traditional mechanism to eliminate vibration effect on the board. In this design, I am attempting to address vibration (the high-frequency one) by using two MPU-6050 sensors positioned at 45 degrees to each other and +/- 22.5 degrees to the axis of the board. I also expect to have more precise estimate of the vertical acceleration to be able to provide independent estimate for altitude.
  • Provide GPS port.
MOLEX PicoBlade 1.25 pitch 4-pin connector provided on board to connect GPS; connector provides ground, +3.3V, TX, and RX lines.

  • Provide US-100 port.

JST ZH 1.5 pitch 4-pin connector provided on board for US-100 ultrasonic range sensor; connector provides ground, +3.3V, TX, and RX lines.

  • Serial port for RC receiver.

With this board I plan to use either UART-enabled receiver or a satellite receiver as my primary. I did some analysis of this configuration and posted my findings in a blog post at http://diydrones.com.

  • DIP Switch.

4-channel half-pitch SMT switch to control some of the flight controller configuration parameters.

Putting all of these requirements together, I came up with the following board design:

The board is a 4-layer one with internal layers used only for Ground and power; all routing is done in the top and bottom layers. The board provides taps to connect logic analyzer to each of the 2 I2C buses for debugging and testing. Respective interrupt lines (from sensors) are also routed to these taps.

Eagle project files for this board and schematic printout are provided in my repository at Google Code.

Now we can power up the board and perform initial configuration, which will be the topic of the next post.

Saturday, July 19, 2014

Control board and firmware architecture

First thing first, all my boards and all of my code are for Microchip PIC 16-bit microprocessors. Sorry, Atmel AVR and Arduino fans, you would not find much of interest here, except, maybe, something related to sensors or PID controller L

Why PIC? I simply got used to it – I started working on the flight controller idea about 10 years ago and somebody in my flight club recommended PIC, so I started with PIC MCUs, got quite comfortable with them and they are fully capable of addressing all of my needs. If you are interested in some of my previous iterations, check the Project History page.

I definitely do not imply that one architecture is better than the other – if you would like to see a great comparison of Microchip PIC and Atmel AVR MCUs, check the link http://www.eevblog.com/2010/02/22/eevblog-63-microchip-pic-vs-atmel-avr/ and if you have a strong preference and would like to get into arguments, please argue with the author of that post. I am impartial J

Now that we are over the MCU hurdle, let’s talk about the code. Code is developed using Microchip’s MPLab-X IDE and XC16 compiler – both products are freely available from www.microchip.com (the free version of XC16 compiler does not perform optimization of code beyond the basics). I work on Windows platform, so if you are a MAC or LINUX user (MPLab-X and XC16 available for all platforms) and would like to compile my code on your machine you may have to replace all “\” with “/” in all of the #include directives and in directory references in the Project Properties in MPLab-X.

Now, about the code itself – ALL of my communication libraries (UART, I2C, SPI, PWM for motor control, RC Receiver communication, etc.) are interrupt-based! There is no time lost on waiting in my control loop as all the communication with sensors and other peripheral devices happens in parallel with the computations in the Interrupt routines (ICRs). This leaves me a lot of time for calculations, so I use floating-point arithmetic for all calculations without the need to resort to time saving tricks like doing calculations using fixed-point or Q.15 or Q.31 arithmetic with subsequent normalization. I can also freely use trigonometric function and matrix operations without any concern to how much time they use! 

Time savings produced by implementing asynchronous IO are so high that without any special optimization my control loop on the Version 2 board (previous generation using 40 MHz PIC24HJ MCU), which implements self-stabilization, course lock, battery management, etc., runs at 400 Hz. I actually had to insert additional wait into the control loop to bring it down to 100 Hz because my data logger (the “black box” in aviation terminology) could not keep up with the data stream!

So, even if you are not an enthusiast of flight and do not care about the multicopters, you still may find some interesting code snippets in this blog. For example, my I2C library allows for multiple slave devices on the same bus; respective control programs may acquire bus for exclusive use in the asynchronous mode by implementing interrupt callbacks. More about this in the I2C section later.

Another interesting feature of the Version 3 design is that the board uses two MPU-6050 6-DOF (3-axis gyro and 3-axis accelerometer) sensors. Sensors are oriented at 45 degrees to each other – +/- 22.5 degree in reference to the X-axis of the board. Thus, the Roll and Pitch calculations are based upon fusion of four measurements from each sensor; Yaw and vertical acceleration are calculated based upon the average of the measurements from both sensors. Sensors are physically located roughly symmetrically around the center of the board, but quite apart from each other. My expectations are that this design will reduce sensitivity of attitude calculations to vibration, which is a serious issue for multi- and helicopters. However, whether these expectations will materialize or I just wasted a perfectly usable second sensor – we will see when we get there.


Now it is probably the right time to have a look at the board, which we will do in the next post.

Introduction

Hi! Thanks for stopping by.

Your first question probably would be “What is this blog about?” As the name implies, it is about building from scratch quadro- or multi-copter flight control board – the “brain inside the machine” that is responsible for letting it fly.

There is a multitude of flight control boards on the market today – AeroQuad, ArduCopter, DJI, KK, MultiWii - just to name a few. A rather large list of these boards provided at http://robot-kingdom.com/best-flight-controller-for-quadcopter-and-multicopter/, but if you search Google, you probably find many more. So if your interest is in just flying a multicopter or building one and then flying it – you would be much better off just buying either a kit or a RTF set.

This blog goes into the details of building the board and developing firmware for it from scratch! That is from the bare-bone MCU – no external libraries, no black-box code – everything is here and everything is explained. All the code that will be discussed here is available at https://code.google.com/p/custom-pic-quad/ under the Open Source MIT License – look at it or use it at your own discretion WITHOUT ANY WARRANTY; without even the implied warranty of   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

The version of the board and corresponding firmware that will be discussed here is for the Version 3 of the board, which I am working on now. Thus, I will be publishing new articles for this blog as my development effort progresses. Is there a promise that I will conclude this effort and do not leave you, my readers, halfway there? I think so. I have been working on flight control boards since about 2004. While the first couple of revisions (starting with Version 0) were total failure, I finally succeeded with the Version 2 (code and Eagle files for the board itself are at the link above), which happily carries my quad in the air - https://www.youtube.com/watch?v=kEo5DSe7PDg If you are interested, more details are provided on the Project History page.

So why don’t I blog about the working Version 2? Oh, it is boring! Since then I got some new idea, improved code, designed much better board, etc. – so why bother with the old stuff when we can together delve into the new one?

Another reason for this blog is rather selfish – while working on Version 2 I implemented a lot of calibration of the sensors like calibration of magnetometer for Hard and Soft-iron interference (Application notes AN4246 and AN4248 from http://www.freescale.com/) and temperature compensation and calibration of InvenSense IMU MPU-6050. Yes, I have some Excel spreadsheets hanging around from that time, but I did not document all of those efforts properly to assist me in calibrating sensors on my new board. Now I plan to document calibrating sensors on my new board in this blog, so when I get to the next iteration (maybe, Version 4 or 5) I will have all my notes readily documented in this blog! This also applies to sensor fusion, PID tuning, etc.

If you stayed with me up to here, you probably would be interested in what is so special about my project and what is the overall architecture of my board and the corresponding firmware? That will be the topic of the next post.