Making a Digital Compass

Combining a compass moduile, 7-segment display and a PIC MCU


This project constructs a digital compass that displays the instantaneous magnetic heading on a 4-digit LCD display. All the really hard work is done in the three modules, each containing a microcontroller, so what we do is connect them using synchronous serial interfaces. The 16F690 MCU communicates with the LCD display through a Serial Peripheral Interface (SPI), and with the compass module through an I2C interface. This project is chiefly about understanding these interfaces and programming the PIC to manage them. We have already considered using the SPI to control a digital potentiometer in which we programmed the MCU to manage it. Here we shall use the Synchronous Serial Port that is built into the 16F690 for the SPI. The MCU will be programmed to emulate an I2C master for communication with the compass module, since the SSP does not include the master function.

In the past, these peripherals would have used parallel interfaces, which would require many extra pins and conductors (at least 20 in this case), though the programming would be simple. Here, we will connect the compass module with only two wires, and the display with three (in addition to the ground and power, of course), shifting all the complexity to the programming.

The Serial Peripheral Interface

The SPI can be considered as two 8-bit shift registers connected as shown in the diagram. With each clock period, a bit is shifted from the master shift register to the slave shift register via the MOSI connection, while simultaneously a bit is shifted from the slave shift register to the master shift register via the MISO connection, starting with the highest-order bit 7. After 8 clock periods, the master and slave shift registers have exchanged their contents, which are disposed of as required. The master device controls the clock signal SCLK and the chip select CS. Usually, CS is pulled low before a transaction to select the slave device and activate it. The MOSI signal is always controlled by the master, and the MISO signal by the slave, so the data lines are unidirectional and the communication is full duplex.

The clock may be low (0) when CS is asserted, when it is said to idle low. It is brought high (1) to complete the first clock period. The second clock period begins when the clock is pulled low, and ends a half-period after it is pulled high. When the clock is low, the data line is free to change. When it goes high, the data is asserted to be valid, until it goes low again. The receiver may sample the data any time while the clock is high, from the middle of the clock period to the end. This is called clock polarity 0, or CPOL = 0.

It is also possible to choose that the clock begin high when CS is asserted. The first clock period begins when the clock goes low, and ends at the end of the next high pulse of the clock. This is CPOL = 1. The data is again valid when the clock is high, and may be changed when the clock is low.

The relation between the clock and the reading and changing of data is called the clock phase. In CPHA = 0, the data is read on the first edge of a clock cycle, and changed on the second, while with CPHA = 1, data is changed on the first edge and read on the second. The situation we described first is CPOL = 0 and CPHA = 0, which is sometimes called mode 0. In the second case, CPOL=1 and CPHA = 1, which is mode 3.

There are two other possibilities, when data is valid when the clock is low and can be changed when it its high, CPOL = 1, CPHA = 1 and CPOL = 0,CPHA = 1. If you are designing both master and slave, it is easy to choose a compatible scheme. However, in general you must choose a master scheme that matches the slave. Since there is no definitive SPI specification, the terms used my have nonstandard meanings. If the slave data sheet says the clock idles low, then the arrangement is probably mode 0, CPOL = 0, CPHA = 0, while if the clock idles high, it is mode 3, CPOL = 1, CPH a = 1.

The PIC SSP uses a different set of configuration bits. The clock polarity is set by the bit CKP in the SSPCON register. This is 0 for idle low, 1 for idle high. The CKE bit in the SSPSTAT register controls the clock edge on which data is read, a 1 for the first edge, 0 for the second. Therefore, with CKP = 0, CKE = 1 will read data on the rising edge of the clock, so we have mode 0. This is the setting we should use with the display. There is another control bit, SMP in SSPSTAT that controls when the input data is sampled. SMP = 0 samples in the middle of the clock period, while SMP = 1 samples at the end. We want the default value of 0 for SMP.

The waveforms for mode 0, CPOL = 0, CPHA = 0, taken from the 16F690 data sheet, are shown in the figure at the left, and this is what we desire for the display. To turn on the SSP, the SSPEN bit in SSPCON must be set. This configures RB6, pin 11, as SCLK (SCK), RC7, pin 9, as MOSI (SDO), RB4, pin 13, as MISO (SDI). All these pins must be configured as outputs in TRISC and TRISB except RB4, which should be an input. Since RB4 is by default an analog input for the A/D converter, it must be made a digital input by clearing its bit in ANSELH. This does not affect the outputs. Now that the SSP is configured and enabled, all that is necessary to send a byte is to write it to the SSPBUF. After a byte is sent, one has also been received so SSPBUF must be read in order to clear the port for another byte. The flag bit BF in SSPSTAT signals when the buffer is full, and can be monitored to determine when the sending is complete, when it is set to 1. Reading SSPBUF clears BF automatically. We really do not use the received byte (it may not even be connected) and simply ignore it. In some applications, however, we may want to read bytes from the slave device, and it that case we have to load dummy bytes in SSPBUF to shift them out.

A routine for sending or receiving a byte is shown at the right. To send a byte, load the byte into W and call the routine. The routine returns with the received byte in W, which we simply ignore here. If a byte is to be received, the routine simply is called and returns with the received byte in W.

The pins PB6, PB7 and PC7 must be configured as outputs, and PB4 as an input, in TRISB and TRISC. Since PB4 is a default analog input, the corresponding bit in ANSELH must be cleared so it will be a digital input. ANSELH is address 11F in bank 2, and might as well be zeroed to avoid problems, though it does not affect digital output. In the routine given, The chip select, CS is RB7. The signal called SS in the SSP data is an input for use in slave SPI mode, not this CS. If we had more than one slave we would use a separate CS for each one. In SSPCON I set SSPEN and cleared CKP, and chose master mode 0001, which gives a clock of fo/16. That is, I wrote 0x21 to SSPCON. SSPSTAT is in bank 1. The SMP bit should be clear, and the CKE bit set, so I wrote 0x40 to it. Remember that we have four choices for CKP and CKE, and this one gives us mode 0 or (0,0).

The I2C Interface

The I2C interface is much more complicated than the SPI, but it has the advantage of a complete and uniform specification. It accommodates multiple masters and multiple slaves on the same two wires. Fortunately, we shall require only a small subset of its capabilities, since we have only one master talking to only one slave. The compass module would be much easier to use with an SPI interface, but it does give us an opportunity to study the I2C interface. It is so complicated that the PIC SSP does not support a hardware I2C interface master, only slave modes.

Only two circuits are required, the clock SCK and the data bus SDA. These are open-collector buses, so anything connected with them should have two states, high impedance and active pull-down. Both circuits are passively pulled up by resistors. Instead of writing to a port to control them, we write a 0 to the port latch, and then manipulate the TRIS bit. A 1 releases the circuit, while a 0 actively pulls it down. This is the reason we can let multiple masters control the clock and data lines, and multiple slaves talk on the data line, so only two wires are required for any number of devices. A slave can even hold the clock line low to "stretch" the clock if it needs more time. We will not need this feature, nor the process of arbitration between more than one master that wishes to use the bus. The data line is bidirectional, so that either the master or the slave can control it. The master will always control the clock.

Normally, data can be changed only when the clock is low, and data is valid when the clock is high. Exceptional cases are the Start and Stop bits, which are transitions of the data while the clock is high. A Start bit is issued by pulling the data line low when the clock is high. It signals to all other masters that this master wishes to transmit, and the bus is now busy. The Stop bit is issued by releasing the data line when the clock is high, and signals that this master is releasing the bus, which is now idle. Of course, this is of no importance for us, since we will have only one master, but it still must be observed. Now the master can shift out an 8-bit address message. The first 7 bits are the slave address, the last bit is 0 for a write to the slave, and 1 for a read from the slave. In a ninth clock period, the master releases the data bus so that the addressed slave can pull it low in an acknowledge bit or ACK. We can ignore the ACK bit, but we must allow for it to exist, since the slave will insist on sending it. On a write, we then follow the address by one or more data bytes and the slave acknowledges each one. On a read, we release the data bus to the slave and shift in one or more data bytes. The master must acknowledge each byte in the ACK bit. The slave will insist on this, and refuse to send anything more if the acknowledge is not received. Finally, the master issues a Stop bit and releases the buses.

The data sheet for the HMC6352 gives an excellent description of the use of the I2C interface with waveforms. The default configuration can be accepted for normal use. First, the address 42 followed by the data byte 41 ("A" command), which causes the device to make a measurement and calculate a heading. After waiting 6 ms, the heading can be read as two data bytes following the address 43 (note that the write bit is set). This 16-bit binary number is tenths of a degree in the heading clockwise from magnetic north.

A routine for sending a byte and accepting the ACK is shown at the left. When the program initializes, bits PC0 and PC1 are cleared, so that when a corresponding bit in TRISC is 0, a low is output, while when it is 1 the pin is high impedance and the bus is pulled high. Since these are default analog inputs, the corresponding bits in ANSEL must be cleared. It is simplest to write 0 to ANSEL, since we will not be using the A/D converter. Note the #defines for SDA and SCL. When we are in bank 0, they will write and read the pins, while in bank 1 they will affect TRISC rather than PORTC.

A delay routine called pause gives roughly a half clock period delay. If it decrements a register, it does not matter which bank is activated, although a different register will be used depending on the bank. I assigned the shift register SRO to address 70, which is the same in any bank (this property is shared by locations 70-7F), though I believe it is only referred to when bank 1 is active.

To send a byte, it is loaded into SRO and the routine is called. The first thing that is done is to set the C flag and ensure that we are in bank 1. The clock is then pulled low, and we shift bit 7 in RPO into C, and C into bit 0. The first time, we shift in a 1, but on successive bits we will shift in 0's. After the ninth shift, SPO will contain 0 and the whole byte has been transmitted. We check for 0 by moving SRO into itself, and if Z = 1, then we accept the ACK. Until then, we set SDA to reflect the bit just shifted into C. This is done by first setting SDA = 1, and if C happens to be clear, setting SDA = 0. Now we wait out the first half of the clock period, set the SCL high, and wait out the second half. Then we set C = 0 and shift out the next bit.

In the ACK bit, we first drop SDA so that the slave can pull it low. We wait out a half period, set the SCL high, and wait out the final half-period. Finally we pull the SCL low to start another clock period (though this is repeated at the beginning of another shout call). When the routine returs, we are sill in bank 1, the clock is low, and SDA is released.

A routine for shifting in a byte and acknowledging it is shown at the right. The shift register SRI can be the same as SRO, since they are never used at the same time. We cannot use the same trick for counting out the bits, so we use a counter register CTR. I used a bank-independent location 73, though it appears to be used only in bank 0. First, the counter is initialized to 8. for each bit, bank 1 is set, SCL is low, and SDA is released so the slave can control it. After a half-period wait, SCL is allowed to go high and the data is stable. We assume it is 1, set bank 0 to read it, and correct our assumption if it is wrong. C is then shifted into SRI and we make sure we wait out a full clock period (which may not be necessary). The counter is decremented, and if it is not zero we go back for the next bit. If it is zero, we drop into the ACK bit. After setting bank 1 again, we pull SCL and SDA low, asserting ACK. Now we wait out a clock cycle, letting SCL go high in the middle. We must keep SDA low for a full cycle, or the slave will not believe it and will not send any successive bytes. The routine returns with SDA low and SCL high.

Note that to issue a STOP bit after shin returns, all we have to do is wait out a while with SCL high, and then release SDA. After shout, we must pull SDA low while SCL is low, raise SCL, wait a bit, then release SDA. To issue a START bit, we first set SDA high while SCL is low, let SCL go high, wait a bit, and pull SDA low. After a pause, we can call shout to send the address byte. SDA may be changed any time SCL is low without consequences, but when SCL is high we issue START and STOP bits. I do not know whether the slave pays any attention to START and STOP, but the specification requires them. The slave does pay attention to the ACK from the master when it sends a byte.

Writing the Program

The program must include the following elements:
1. Initialize ports and SSPCON, SSPSTAT, OSCCON, ANSEL, ANSELH.
2. Delay 1 sec. on power-up to complete initialization of display and compass.
3. In a main loop, send "42" and "41" to the compass to trigger a measurement.
4. Wait at least 6 ms for measurement to be ready.
5. Send "43" to the compass and read two bytes for HH and HL.
6. Convert the binary HH and HL into four decimal digits.
7. Display the digits
8. Wait about 1 sec. and go back to 3.

The best procedure seems to be to first test communication with the display, using the "send" routine already given. Then the binary to decimal conversion should be verified by loading HH and HL with test values. Finally, the I2C routines for communicating with the compass module can be tested.

The program fragment at the left shows a complete I2C transaction to trigger a heading management.

After a delay of at least 6 ms, the heading can be read as two bytes by the program fragment shown at the right.

The magnetic heading in tenths of a degree is read and loaded into HH and HL. The decimal digits are determined by subtracting 1000, 100 and 10 repeatedly in binary. The remainder is the fourth digit. The subtrahend is loaded into SH, SL at each stage. 1000 = 03E8, 100 = 0064, and 10 = 000A. A routine for performing the repeated subtractions is shown at the left. It must be used for each digit, initializing SH and SL appropriately and clearing DIG. It returns when a further subtraction is impossible, with the remainder in HH and HL and with the number of successful subtractions in DIG. Read the description of the SUBWF instruction carefully and note its effect on C.

Delay routines will be required for a delay of a few microseconds, about 6 milliseconds, and for about 1 second. I found reliable operation for a processor clock of 1 MHz, which requires writing 0x40 to OSCCON.

The program should be single-stepped as far as possible, carefully noting that the bank is always properly set for every memory reference. When testing the send routine, set BF = 1 in SSPSTAT or the program will loop forever. The Stopwatch can be used to determine the length of delays. To use it, set breakpoints and use Debug/Run to step between them. View the Stopwatch window and reset the Stopwatch to zero before running, and view it again at the breakpoint to find the elapsed time. Be sure that the processor clock speed is properly set.

Constructing the Compass

The compass can be built on a solderless breadboard (such as Sparkfun PRT-00112). Put jumpers in the power and ground buses at the middle of the board so that they are continuous. Use an LM317 voltage regulator to provide 3.3 or 5.0 V. This can be placed at tne corner of the board with an SPST switch, pilot LED, capacitor and diode for protection against incorrect polarity. A power plug allows the connection of a 9V DC wall transformer (Sparkfun TOL-00298). The voltage is selected by using a wire to ground one point for 3.3 V, removing it for 5.0 V. When looking at the front of the 317, the pins are, from left to right, ADJ, VOUT and VIN. The power supply will provide ample current; even an LM317LZ supplyling only up to 100 mA would probably work here.

Solder an 8-pin header into the display, and a 4-pin header into the compass module, so they can be inserted in the breadboard. Arrange so it is easy to remove and insert the 16F690 for programming. All the devices will function properly on 3.3 V, so arrange that this voltage is supplied. Fortunately, 5 V would not damange them. Pullup resistors are provided for the I2C lines on the compass module breakout board, so no additional components are necessary. An 0.1 μF capacitor should be located near the power pins of the MCU, on the buses where it will be out of the way. On the display, the pins are, in order: CSN (active low chip select), SI (serial input data), RST (reset), SCK (serial clock), SO (serial data out), VCC and GND. On the compass module, the pins are: SCL (serial clock), SDA (serial data), VCC and GND.

If the program is ready, the MCU can be programmed with the PICkit 2 and inserted into the breadboard. When the power is turned on, the display should start with 0000, then show the compass heading, updating it regularly. Check the digital compass against a lensatic compass. At Denver, the 2010 magnetic declination is about 9° east, and the dip is about 67 °, with a total intesity of about 0.53 gauss. The compass module is rather sensitive to departures from horizontal.

In fact, it is better to develop the program incrementally and make sure each addition works. First, the display communication should be checked. Next, the binary to decimal conversion should be tested. When this is done, the I2C link can be tested.


Read about the SPI and I2C interfaces in Wikipedia. Search for "Serial peripheral Interface" and for "I2C". These articles include many further references. The I2C article from is good.

The compass module is a Honeywell HMC6352, available from Sparkfun Electronics as SEN-07915. The 4-digit 7-segment display is also available from Sparkfun as COM-09764. The PIC16F690 is supplied by Digi-Key. Downlioad the data sheets from Sparkfun for the compass module and the display.

Microchip has a slide-show format presentation on using the SSP for the SPI that can be found on the website.

Microchip AN554, "Software Implementation of I2C Bus Master", has more features than we require, but shows how to program a PIC MCU as a bus master.

Return to Electronics Index

Composed by J. B. Calvert
Created 21 July 2010
Last revised