Low Level Programming of the Raspberry Pi in C

One of the things that disappointed me when I first got my Raspberry Pi was the fact that everybody was doing very high level programming with it. I wanted to program it like I used to do with microcontrollers, but it seemed like this was not as easy as I thought it would be. I believe however, that for embedded applications, you should be very cautious about dependencies, thus try to use as few libraries as possible. This article will show you how to program the Raspberry Pi in C code in a low level way. Tackling this can be a challenge, so let's get started!

Note that this tutorial will make use of the BCM2835 ARM Peripherals Manual. I will also refer to this as "datasheet", because it sound natural to me.

Getting Started

If you have any experience with low level programming of microcontrollers, then you know that everything is done by writing to the registers in the memory of the device. If you, for example, want to set a pin high, then you write a "1" to the register that corresponds to that certain pin. That means that, if we want to program our Raspberry Pi, we need to gain access to the memory of the BCM2835 first. This is by far the most complex part of programming the Raspberry Pi, so don't be discouraged!

First, we need a header file which defines some macro's.

RPI.h

#include <stdio.h>
 
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
 
#include <unistd.h>
 
#define BCM2708_PERI_BASE       0x20000000
#define GPIO_BASE               (BCM2708_PERI_BASE + 0x200000)	// GPIO controller 
 
#define BLOCK_SIZE 		(4*1024)
 
// IO Acces
struct bcm2835_peripheral {
    unsigned long addr_p;
    int mem_fd;
    void *map;
    volatile unsigned int *addr;
};
 
struct bcm2835_peripheral gpio = {GPIO_BASE};
 
extern struct bcm2835_peripheral gpio;  // They have to be found somewhere, but can't be in the header

So, what is this? If we look at the Broadcom BCM2835 ARM Peripherals manual at page 6, we read:

Physical addresses range from 0x20000000 to 0x20FFFFFF for peripherals. The bus addresses for peripherals are set up to map onto the peripheral bus address range starting at 0x7E000000. Thus a peripheral advertised here at bus address 0x7Ennnnnn is available at physical address 0x20nnnnnn.
So the BCM2708_PERI_BASE macro contains the physical adress value at which the peripheral registers start. This is the adress we will need to use in our program. The virtual address value is 0x7E000000, and it are these virtual adresses that will be found in the datasheet.

There are a lot of different peripherals available on the BCM2835 (Timers, USB, GPIO, I2C, ...), and they will be defined by an offset to this virtual adress! For example, if we are interested in the GPIO peripheral (like in the example code above), we can find in the manual at page 90 that the virtual address is 0x7E200000. This means that the offset to the physical adress will be 0x200000 which explains the GPIO_BASE.

for every peripheral we define a struct of the type bcm2835_peripheral, which will contain the information about the location of the registers. Then we intialize it with the map_peripheral() function from the c file below. It is not so important to understand how this works in detail, as you can just copy paste the code. If you do want to know this, search for "accessing /dev/mem" on the internet. To release the peripheral, we can use unmap_peripheral()

RPI.c

#include "RPI.h"
 
struct bcm2835_peripheral gpio = {GPIO_BASE};
 
// Exposes the physical address defined in the passed structure using mmap on /dev/mem
int map_peripheral(struct bcm2835_peripheral *p)
{
   // Open /dev/mem
   if ((p->mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) {
      printf("Failed to open /dev/mem, try checking permissions.\n");
      return -1;
   }
 
   p->map = mmap(
      NULL,
      BLOCK_SIZE,
      PROT_READ|PROT_WRITE,
      MAP_SHARED,
      p->mem_fd,      // File descriptor to physical memory virtual file '/dev/mem'
      p->addr_p       // Address in physical map that we want this memory block to expose
   );
 
   if (p->map == MAP_FAILED) {
        perror("mmap");
        return -1;
   }
 
   p->addr = (volatile unsigned int *)p->map;
 
   return 0;
}
 
void unmap_peripheral(struct bcm2835_peripheral *p) {
 
    munmap(p->map, BLOCK_SIZE);
    close(p->mem_fd);
}

Now we have code to access the memory of the BCM2835, we can start to use the peripherals to do some badass projects! If you didn't fully grasp the above code, this is not a problem! Everything will start making sense after you see that code in action.

GPIO

GPIO stands for General Purpose In/Out, so this peripheral will allow us to set a pin either as in- or output and read or write (to) it. We can add the macro's below to our header file (RPI.h) to support this. I will explain how they work underneath.

// GPIO setup macros. Always use INP_GPIO(x) before using OUT_GPIO(x)
#define INP_GPIO(g)   *(gpio.addr + ((g)/10)) &= ~(7<<(((g)%10)*3))
#define OUT_GPIO(g)   *(gpio.addr + ((g)/10)) |=  (1<<(((g)%10)*3))
#define SET_GPIO_ALT(g,a) *(gpio.addr + (((g)/10))) |= (((a)<=3?(a) + 4:(a)==4?3:2)<<(((g)%10)*3))
 
#define GPIO_SET  *(gpio.addr + 7)  // sets   bits which are 1 ignores bits which are 0
#define GPIO_CLR  *(gpio.addr + 10) // clears bits which are 1 ignores bits which are 0
 
#define GPIO_READ(g)  *(gpio.addr + 13) &= (1<<(g))

At first sight, those macro's look terrifying! But actually they are quite easy to understand, so don't be scared!

gpio is a struct of the type bcm2835_peripheral that was initialized with map_peripheral() at the adress GPIO_BASE. This is what we discussed above. To be able to use this code, this need to be done first!

To understand properly how the macro's work, we need to take a look at the Broadcom BCM2835 ARM Peripherals manual at chapter 6 (page 89 etc.). We will go over all the macro's in the following text.

#define INP_GPIO(g) *(gpio.addr + ((g)/10)) &= ~(7<<(((g)%10)*3))

This macro sets pin "g" as an input. How?

In the datasheet at page 91 we find that the GPFSEL registers are organised per 10 pins. So one 32-bit register contains the setup bits for 10 pins. *gpio.addr + ((g))/10 is the register address that contains the GPFSEL bits of the pin "g" (and 9 other pins we are not interested in). Remember that the result of a division on integers in C does not contain the part behind the komma. There are three GPFSEL bits per pin (000: input, 001: output). The location of these three bits inside the GPFSEL register is given by ((g)%10)*3 (three times the remainder, remember the modulo % operator).

So to set pin "g" as an input, we need to set these bits to "000". This is done with a AND-operation between the GPSEL register and a string of which everything except those bits are 1. This string is created by bitshifting 7, which is binary 111, over ((g)%10)*3 places, and taking the inverse of that.

A little example: Say that we want to use gpio4 as an input.

  • The GPSEL address containing the function select bits for gpio4 is *gpio.addr + 4/10=*gpio.addr + 0
  • Inside this 32-bit register, the bits concerning gpio4 start from bit (4%10)*3=12
  • If we bitshift binary 111 over 12 positions we get: 00000000000000000111000000000000
  • We have to make those three bits zero in the GPFSEL register, so we have to invert that 32-bit string and perform an AND operation with the GPSEL register. (X = unknown)
  •      1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1   
         X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X   (GPFSEL old)
    AND  ---------------------------------------------------------------
         X X X X X X X X X X X X X X X X X 0 0 0 X X X X X X X X X X X X   (GPFSEL new)      
    

#define OUT_GPIO(g)   *(gpio.addr + ((g)/10)) |=  (1<<(((g)%10)*3))

To set pin "g" as an output, we have to set its setup bits to 001. This is done in the same way as before, but now we bitshift a 1, and use a OR operation on the normal (not inverted) string.

This is also the reason that the comment says to "always use INP_GPIO(x) before using OUT_GPIO(x)". This way you are sure that the other 2 bits are 0, and justifies the use of a OR operation here. If you don't do that, you are not sure those bits will be zero and you might have given the pin "g" a different setup.

#define SET_GPIO_ALT(g,a) *(gpio.addr + (((g)/10))) |= (((a)<=3?(a) + 4:(a)==4?3:2)<<(((g)%10)*3))

As you can see in the picture above, input and output are not the only functionalities available for the pins. There are 6 "alternate function" possibilities for each pin. It are these alternate functions that you will want to use if you need to use other peripherals than the gpio, like I2C and SPI. The alternate function possibilities are on page 102 in section 6.2 of the manual. With SET_GPIO_ALT(g,a) you can set pin g to function 0,1,... In the part about I2C below, we will use this function to set GPIO0 and GPIO1 to SDA and SCL for use with I2C.

#define GPIO_SET  *(gpio.addr + 7)

In the datasheet on page 90, we seet that the GPSET register is located 7 32-bit registers further than the gpio base register. There is a small mistake on this page of the datasheet, as the first line is printed twice.

gpio.addr is defined as a pointer to an unsigned int, which is 32-bit on the Raspberry Pi. When you add an integer to a pointer, it will know that we are working with 32-bit values, so we don't need to multiply this with 4 (7*4 = 0x1C) to obtain the address from the datasheet.

So to use the macro GPIO_SET to set e.g. gpio 4 pin we can set the 4'th bit of this register to 1 using a bitshift:

GPIO_SET = 1 << 4;
It's important to note that writing a "0" to the GPIO_SET register will not reset that pin. You have to write a "1" to the GPIO_CLR register to reset a pin. This is very useful, as this means that we can use the above bitshift operation to write to all pins, without taking the risk of accidentally resetting a pin we forgot that was on. Awesome.

The gpio clear is ofcourse similar.

#define GPIO_READ(g)  *(gpio.addr + 13) &= (1<<(g))

To read a pin we simply perform a bitwise AND on the GPLEV register and the pin we are interested in (g). The result is the value of that pin.

Example - Blink a LED

Hopefully the above analysis helped you understanding how to use the GPIO functionality of the Raspberry Pi. It really is nothing more than writing 1's and 0's to the correct registers ... As a little example, here is the code to blink a led. Note that pin 7 corresponds to gpio 4 if you look a the pinout. To run this, make sure you added the GPIO macro's to your header file!

Don't try this at home (without a resistor in series with the led)!

main.c

#include "RPI.h"
 
int main(
{
  if(map_peripheral(&gpio) == -1) 
  {
    printf("Failed to map the physical GPIO registers into the virtual memory space.\n");
    return -1;
  }
 
  // Define pin 7 as output
  INP_GPIO(4);
  OUT_GPIO(4);
 
  while(1)
  {
    // Toggle pin 7 (blink a led!)
    GPIO_SET = 1 << 4;
    sleep(1);
 
    GPIO_CLR = 1 << 4;
    sleep(1);
  }
 
  return 0; 
}

I2C

The peripheral we will need to use the I2C functionality of the Raspberry Pi is the BSC (Broadcom Serial Controller). Information about this can be found in de datasheet chapter 3 (p. 28 and further). There are two BSC's: BSC0 and BSC1. We will use BSC0. First we map all the registers by adding the following macro's to our RPI.h file.

#define BSC0_BASE     (BCM2708_PERI_BASE + 0x205000)  // I2C controller 
 
extern struct bcm2835_peripheral bsc0;	
 
// I2C macros
#define BSC0_C          *(bsc0.addr + 0x00)
#define BSC0_S          *(bsc0.addr + 0x01)
#define BSC0_DLEN     *(bsc0.addr + 0x02)
#define BSC0_A          *(bsc0.addr + 0x03)
#define BSC0_FIFO     *(bsc0.addr + 0x04)
 
#define BSC_C_I2CEN     (1 << 15)
#define BSC_C_INTR      (1 << 10)
#define BSC_C_INTT      (1 << 9)
#define BSC_C_INTD      (1 << 8)
#define BSC_C_ST        (1 << 7)
#define BSC_C_CLEAR     (1 << 4)
#define BSC_C_READ      1
 
#define START_READ      BSC_C_I2CEN|BSC_C_ST|BSC_C_CLEAR|BSC_C_READ
#define START_WRITE     BSC_C_I2CEN|BSC_C_ST
 
#define BSC_S_CLKT  (1 << 9)
#define BSC_S_ERR     (1 << 8)
#define BSC_S_RXF     (1 << 7)
#define BSC_S_TXE     (1 << 6)
#define BSC_S_RXD     (1 << 5)
#define BSC_S_TXD     (1 << 4)
#define BSC_S_RXR     (1 << 3)
#define BSC_S_TXW     (1 << 2)
#define BSC_S_DONE    (1 << 1)
#define BSC_S_TA      1
 
#define CLEAR_STATUS    BSC_S_CLKT|BSC_S_ERR|BSC_S_DONE
 
// I2C Function Prototypes
void i2c_init();
void wait_i2c_done();

Not much special here, except maybe for the START_READ, START_WRITE and CLEAR_STATUS. Of course, these are just the combinations (OR) of bits in the status register that need to be set for that action to occur. It's just easier to do it like that than to set all the bits seperately every time.

Then we need to add the following functions to our RPI.C file.

struct bcm2835_peripheral bsc0 = {BSC0_BASE};
 
// Initialize I2C
void i2c_init()
{
    INP_GPIO(0);
    SET_GPIO_ALT(0, 0);
    INP_GPIO(1);
    SET_GPIO_ALT(1, 0);
}  
 
// Function to wait for the I2C transaction to complete
void wait_i2c_done() {
 
        int timeout = 50;
        while((!((BSC0_S) & BSC_S_DONE)) && --timeout) {
            usleep(1000);
        }
        if(timeout == 0)
            printf("Error: wait_i2c_done() timeout.\n");
}

Notice how we access the memory of the BSC0 peripheral with a struct of the type bcm2835_peripheral called bsc0. We initalize it so that the address points to BSC0_BASE.

The classic I2C delay is implemented with wait_i2c_done(). It constantly polls the "DONE" bit of the BSC0 S register (status). This bit becomes 1 when the transfer is completed. See datasheet p. 32.

Now we need to make sure that the GPIO pins are set up properly for I2C. For this we need one pin to behave as SDA, and one pin to behave as SCL. If we look at the pinout again, we see that GPIO0 and GPIO1 are the proper pins for this. They have to be enabled first though. For this we use the i2c_init() function. Inside you will find the SET_ALT_GPIO() macro used to set these pins to alternate functionality 0. The reason we set these to alternate functionality 0 is because the datasheet shows the following table at page 102:

Ok, so after initializing the I2C we are very close to start sending data over the I2C bus! To send or write some data, we need to take the following steps:

  • Write the I2C slave address of the device you want to communicate with (you can find this in the datasheet) to the BSC0_A adress (slave adress register).
  • Write the amount/quantity of bytes you want to send/receive to the BSC0_DLEN register (Data Length).
  • If you want to write to the slave, write the bytes you want to write in the BSC0_FIFO. Remember: FIFO = First In First Out!
  • Clear the status register: BSC0_S = CLEAR_STATUS
  • Start write or start read by writing to the BSC0 C register (Control): BSC0_C = START_WRITE or BSC0_C = START_READ

Are you totally confused? Me too! Let's take a look at some examples!

Example 1 - Real Time Clock (RTC)

In this example we will read a Real Time Clock (RTC) with a Raspberry Pi using I2C. The RTC we are using is the PCF8563, a very classic device. Normally you would want to be able to write the RTC as well for initializing, but I want to start with a simple example.

The data from the PCF8563 starts at register 2 (seconds) and the 6 sequential adresses contain minutes, hours, ... So in total there are 7 registers we need to read, starting from adress 2.

Reading data from the RTC is described in the PCF8563 datasheet and should be performed like this:

The first step, writing the slave address to the bus, will be done automatically for us if we write this address to the BSC0_A register. So we have to do this before starting communication. After this we need to write the register address of the RTC we want to start reading from to the bus. In our case, this is "2". After that, it will start sending the data, so we have to start reading data from the bus.

The code example below nicely illustrates how this is done on the Raspberry Pi.

#include "RPI.h"
#include <stdio.h>
#include <time.h>
#include <fcntl.h>
 
struct tm t;
 
/* Functions to do conversions between BCD and decimal */
unsigned int bcd_to_decimal(unsigned int bcd) {
    return ((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f);
}
unsigned int decimal_to_bcd(unsigned int d) {
    return ((d / 10) << 4) + (d % 10);
}
 
int main(int argc, char *argv[]) {
 
    /* Gain access to raspberry pi gpio and i2c peripherals */
    if(map_peripheral(&gpio) == -1) {
        printf("Failed to map the physical GPIO registers into the virtual memory space.\n");
        return -1;
    }
    if(map_peripheral(&bsc0) == -1) {
        printf("Failed to map the physical BSC0 (I2C) registers into the virtual memory space.\n");
        return -1;
    }
 
    /* BSC0 is on GPIO 0 & 1 */
    i2c_init();
 
    /* I2C Device Address 0x51 (See Datasheet) */
    BSC0_A = 0x51;
 
    /* Write operation to restart the PCF8563 register at index 2 ('secs' field) */
    BSC0_DLEN = 1;            // one byte
    BSC0_FIFO = 2;            // value 2
    BSC0_S = CLEAR_STATUS;    // Reset status bits (see #define)
    BSC0_C = START_WRITE;     // Start Write (see #define)
 
    wait_i2c_done();
 
    /* Start Read of RTC chip's time */
    BSC0_DLEN = 7;
    BSC0_S = CLEAR_STATUS;  // Reset status bits (see #define)
    BSC0_C = START_READ;    // Start Read after clearing FIFO (see #define)
 
    wait_i2c_done();
 
    /* Store values in struct */
    t.tm_sec = bcd_to_decimal(BSC0_FIFO & 0x7f);
    t.tm_min = bcd_to_decimal(BSC0_FIFO & 0x7f);
    t.tm_hour = bcd_to_decimal(BSC0_FIFO & 0x3f);
    t.tm_mday = bcd_to_decimal(BSC0_FIFO & 0x3f);
    t.tm_wday = bcd_to_decimal(BSC0_FIFO & 0x07);
    t.tm_mon = bcd_to_decimal(BSC0_FIFO & 0x1f) - 1; // 1-12 --> 0-11
    t.tm_year = bcd_to_decimal(BSC0_FIFO) + 100;
 
    printf("%02d:%02d:%02d %02d/%02d/%02d (UTC on PCF8563)\n",
        t.tm_hour,t.tm_min,t.tm_sec,
        t.tm_mday,t.tm_mon + 1,t.tm_year - 100);
 
    /* Unmap the peripheral */
    unmap_peripheral(&gpio);
    unmap_peripheral(&bsc0);decimal_to_bcd
}

Example 2 - IMU (MPU6050)

For reading the MPU6050 IMU I will just provide the source code and hope that you understand what to do with it after the previous examination. If there still is something that is not clear, please contact me, and I will add more explanation here.

MPU6050.h

#ifndef _INC_MPU6050_H
#define _INC_MPU6050_H
 
#include "RPI.h"
#include <stdio.h>
 
#define MPU6050_ADDR 0b01101001 // 7 bit adres 
#define CONFIG 0x1A
#define ACCEL_CONFIG 0x1C
#define GYRO_CONFIG 0x1B
#define PWR_MGMT_1 0x6B
 
void MPU6050_Read(short * accData, short * gyrData);
void MPU6050_Init(void);
void MPU6050_SetRegister(unsigned char regAddr, unsigned char regValue);
 
#endif

MPU6050.c

#include "MPU6050.h"
 
void MPU6050_Init(void)
{
//  MPU6050_SetRegister(PWR_MGMT_1, 0x80);  // Device Reset
    MPU6050_SetRegister(PWR_MGMT_1, 0x00);  // Clear sleep bit
    MPU6050_SetRegister(CONFIG, 0x00);  
    MPU6050_SetRegister(GYRO_CONFIG, 0x08);
    MPU6050_SetRegister(ACCEL_CONFIG, 0x08);
}
 
void MPU6050_SetRegister(unsigned char regAddr, unsigned char regValue)
{
    // See datasheet (PS) page 36: Single Byte Write Sequence
 
    // Master:   S  AD+W       RA       DATA       P
    // Slave :            ACK      ACK        ACK
 
    BSC0_A = MPU6050_ADDR;
 
    BSC0_DLEN = 2;
    BSC0_FIFO = (unsigned char)regAddr;
    BSC0_FIFO = (unsigned char)regValue;
 
    BSC0_S = CLEAR_STATUS;  // Reset status bits (see #define)
    BSC0_C = START_WRITE;     // Start Write (see #define)
 
    wait_i2c_done();
}
 
void MPU6050_Read(short * accData, short * gyrData)
{
    // See datasheet (PS) page 37: Burst Byte Read Sequence
 
    // Master:   S  AD+W       RA       S  AD+R           ACK        NACK  P
    // Slave :            ACK      ACK          ACK DATA       DATA
 
    BSC0_DLEN = 1;        // one byte
    BSC0_FIFO = 0x3B;     // value of first register
    BSC0_S = CLEAR_STATUS;  // Reset status bits (see #define)
    BSC0_C = START_WRITE;     // Start Write (see #define)
 
    wait_i2c_done();
 
    BSC0_DLEN = 14;
 
    BSC0_S = CLEAR_STATUS;  // Reset status bits (see #define)
    BSC0_C = START_READ;      // Start Read after clearing FIFO (see #define)
 
    wait_i2c_done();
 
    short tmp;
 
    int i = 0;  
    for(i; i < 3; i++)    // Accelerometer
    {
  tmp = BSC0_FIFO << 8; 
  tmp += BSC0_FIFO;
  accData[i] = tmp; 
    }
 
    tmp = BSC0_FIFO << 8;   // Temperature
    tmp += BSC0_FIFO; 
 
    i = 0;  
    for(i; i < 3; i++)    // Gyroscope
    {
  tmp = BSC0_FIFO << 8;
  tmp += BSC0_FIFO;
  gyrData[i] = tmp; 
    }
}

GitHub Repository

All this code can also be found in my GitHub repository: https://github.com/Pieter-Jan/PJ_RPI. You will need CMake (sudo apt-get install cmake) and git (sudo apt-get install git) to use it.

To use it, create a folder on the pi where you want to put the files in and extract the git repository in there:

git clone git://github.com/Pieter-Jan/PJ_RPI.git 

Then you have to install the library, so cd into the PJ_RPI directory and:

mkdir Build 
cd Build 
cmake ../ 
make 
sudo make install

Now the library is installed and you can build the examples that are in the "Examples" folder using CMake.

If you want to build the MPU6050 example you will also need ncurses for the visualization (sudo apt-get install ncurses-dev).

DIY Raspberry Pi Breakout Cable

Suprisingly enough, a lot of people have asked me how I made the breakout / expansion cable for my Raspberry Pi. It's so easy! So if you don't want to wait on some online order, and want to get started as soon as possible doing epic stuff, you can follow the simple instructions below. A cable like this can be extremely useful for connecting the Raspberry Pi to a breadboard and doing some prototyping!

The following things are needed:

  • 2x13 flat cable
  • 2 times 1x13 male header with 2.54 mm (1 inch) spacing
  • Electrical tape with a nice color
It's also necessary that you know how to solder and have the right equipment for this.

The first step is cut the flat cable nicely in two pieces. If you have a flat cable from an old computer that is 2x13, you can definitely use this! If you don't have one (my case), there are a lot of stores where you can find it. We will continue working with one half of the cable as shown in the figure below.

The second step is to split the cable in 2 pieces, so that you have 2 parts that each contain 13 wires. Then you need to seperate the individual wires a little bit (not too far!) with a knife. Now you should be able to strip them, and solder them to the 1x13 header. You will have to do this for both parts of the cable. Note that it might be useful to make the wires in the middle a little bit shorter than those at the ends, because the header is a lot wider than the half flat cable. The result should look like shown in the picture below. Make sure that you don't make short circuits! If you are in doubt, double check with a multimeter.

Because the above result does not look good, and having unstripped wires is a bit dangerous, I decided to tape this in with some electrical tape. Be careful not to apply too much pressure to the wires so that the pins of the header don't bend!

You should end up with a nice looking cable ready for action!

Reading a IMU Without Kalman: The Complementary Filter

These days, IMU's (Intertial Measurement Units) are used everywhere. They are e.g. the sensors that are responsible for keeping track of the oriëntation of your mobile phone. This can be very useful for automatic screen tilting etc. The reason I am interested in this sensor is because I want to use it to stabilize my quadrocopter. If you are here for another reason, this is not a problem as this tutorial will apply for everyone.

When looking for the best way to make use of a IMU-sensor, thus combine the accelerometer and gyroscope data, a lot of people get fooled into using the very powerful but complex Kalman filter. However the Kalman filter is great, there are 2 big problems with it that make it hard to use:

  • Very complex to understand.
  • Very hard, if not impossible, to implement on certain hardware (8-bit microcontroller etc.)
In this tutorial I will present a solution for both of these problems with another type of filter: the complementary filter. It's extremely easy to understand, and even easier to implement.

Why do I need a filter?

Most IMU's have 6 DOF (Degrees Of Freedom). This means that there are 3 accelerometers, and 3 gyrosocopes inside the unit. If you remember anything from a robotics class you might have taken, you might be fooled into thinking that the IMU will be able to measure the precise position and orientation of the object it is attached to. This because they have told you that an object in free space has 6DOF. So if we can measure them all, we know everything, right? Well ... not really. The sensor data is not good enough to be used in this way.

We will use both the accelerometer and gyroscope data for the same purpose: obtaining the angular position of the object. The gyroscope can do this by integrating the angular velocity over time, as was explained in a previous article. To obtain the angular position with the accelerometer, we are going to determine the position of the gravity vector (g-force) which is always visible on the accelerometer. This can easily be done by using an atan2 function. In both these cases, there is a big problem, which makes the data very hard to use without filter.

The problem with accelerometers

As an accelerometer measures all forces that are working on the object, it will also see a lot more than just the gravity vector. Every small force working on the object will disturb our measurement completely. If we are working on an actuated system (like the quadrocopter), then the forces that drive the system will be visible on the sensor as well. The accelerometer data is reliable only on the long term, so a "low pass" filter has to be used.

The problem with gyroscopes

In one of the previous articles I explained how to obtain the angular position by use of a gyroscope. We saw that it was very easy to obtain an accurate measurement that was not susceptible to external forces. The less good news was that, because of the integration over time, the measurement has the tendency to drift, not returning to zero when the system went back to its original position. The gyroscope data is reliable only on the short term, as it starts to drift on the long term.

The complementary filter

The complementary filter gives us a "best of both worlds" kind of deal. On the short term, we use the data from the gyroscope, because it is very precise and not susceptible to external forces. On the long term, we use the data from the accelerometer, as it does not drift. In it's most simple form, the filter looks as follows:

The gyroscope data is integrated every timestep with the current angle value. After this it is combined with the low-pass data from the accelerometer (already processed with atan2). The constants (0.98 and 0.02) have to add up to 1 but can of course be changed to tune the filter properly.

I implemented this filter on a Raspberry Pi using a MPU6050 IMU. I will not discuss how to read data from the MPU6050 in this article (contact me if you want the source code). The implementation of the filter is shown in the code snippet below. As you can see it is very easy in comparison to Kalman.

The function "ComplementaryFilter" has to be used in a infinite loop. Every iteration the pitch and roll angle values are updated with the new gyroscope values by means of integration over time. The filter then checks if the magnitude of the force seen by the accelerometer has a reasonable value that could be the real g-force vector. If the value is too small or too big, we know for sure that it is a disturbance we don't need to take into account. Afterwards, it will update the pitch and roll angles with the accelerometer data by taking 98% of the current value, and adding 2% of the angle calculated by the accelerometer. This will ensure that the measurement won't drift, but that it will be very accurate on the short term.

It should be noted that this code snippet is only an example, and should not be copy pasted as it will probably not work like that without using the exact same settings as me.

#define ACCELEROMETER_SENSITIVITY 8192.0
#define GYROSCOPE_SENSITIVITY 65.536
 
#define M_PI 3.14159265359	    
 
#define dt 0.01							// 10 ms sample rate!    
 
void ComplementaryFilter(short accData[3], short gyrData[3], float *pitch, float *roll)
{
    float pitchAcc, rollAcc;               
 
    // Integrate the gyroscope data -> int(angularSpeed) = angle
    *pitch += ((float)gyrData[0] / GYROSCOPE_SENSITIVITY) * dt; // Angle around the X-axis
    *roll -= ((float)gyrData[1] / GYROSCOPE_SENSITIVITY) * dt;    // Angle around the Y-axis
 
    // Compensate for drift with accelerometer data if !bullshit
    // Sensitivity = -2 to 2 G at 16Bit -> 2G = 32768 && 0.5G = 8192
    int forceMagnitudeApprox = abs(accData[0]) + abs(accData[1]) + abs(accData[2]);
    if (forceMagnitudeApprox > 8192 && forceMagnitudeApprox < 32768)
    {
	// Turning around the X axis results in a vector on the Y-axis
        pitchAcc = atan2f((float)accData[1], (float)accData[2]) * 180 / M_PI;
        *pitch = *pitch * 0.98 + pitchAcc * 0.02;
 
	// Turning around the Y axis results in a vector on the X-axis
        rollAcc = atan2f((float)accData[0], (float)accData[2]) * 180 / M_PI;
        *roll = *roll * 0.98 + rollAcc * 0.02;
    }
} 

If we use the sweetness of having a real computer (Raspberry Pi) collecting our data, we can easily create a graph using GNUPlot. It's easily seen that the filter (red) follows the gyroscope (blue) for fast changes, but keeps following the mean value of the accelerometer (green) for slower changes, thus not feeling the noisy accelerometer data and not drifting away eiter.

The filter is very easy and light to implement making it perfect for embedded systems. It should be noted that for stabilization systems (like the quadrocopter), the angle will never be very big. The atan2 function can then be approximated by using a small angle approximation. In this way, this filter could easily fit on an 8 bit system.

Hope this article was of any help.

Replacing the Raspberry Pi's SD Card Socket

Not so long ago I dropped my Raspberry Pi. It fell on the SD card, which resulted in breaking the slot. After a long search on the internet it was clear that a lot of people had the exact same problem, but all of them solved it by gluing the slot back together in some way or another. Because this solution was not satisfactory for me, I replaced my slot with a new one. In this article I will describe how to do that.

Choosing an appropriate replacement slot

First we need to know how the SD Card slot of the Raspberry Pi is built up. This is shown in the figure below. (Sorry it is hand drawn and everything, but I really didn't have the time to make proper figures on the computer)


As you can see it is a 13-pin slot, using 2 pins for both for the "Write Protect" and "Card Detect". This is a problem, as most of the available slots we can find at electronic parts vendors have either 9 pins (no "Card Detect" and "Write Protect") or 11 pins, using just one pin for each function.

I chose to use a 11-pin slot that I found on ebay. Most of the 11 pin slots have a similar layout. It's very cheap and shown in the picture below.

The layout of the slot is given in the picture below:

Because of the bad positioning of the "Write Protect" and "Card Detect" pins, we will not be able to attatch these pins correctly to the Raspberry Pi. We can solder the "Card Detect" pins on the pi together, so they make a short circuit and the Pi will work. I don't think this is a big problem. And correct me if I'm wrong, but I might have read somewhere that the "Wire Protect" is not even used.

Desoldering the remainders of the old slot & soldering the new slot in place

First get rid of the old slot by desoldering all the broken parts!

Then just solder on the new slot and leave the pins "Card Detect" and "Write Protect" from the SD connector floating (or cut them off if you want to be safe). The other pins should nicely align with the solder pads on the Raspberry Pi. Use the figures (poorly drawn sketches) above for reference and proper alignment. Make sure to short circuit the "Card Detect" solder pads on the Raspberry Pi. For mechanical strength, I added a wire as tightly as possible to the slot and the solder pads of the Raspberry Pi. It is shown in the picture below. I strongly recommend doing this, as it will increase the lifetime of the slot A LOT.


Enjoying the enhanced Pi

Now you should be done. When you plug in your SD Card and power on the Pi, everything should work as it did before. Enjoy your frankenberry pi!


It even still fits in a pibow!

Reading Nintendo 64 controller with PIC microcontroller

I have a few old N64 controllers lying around and figured that it would be pretty cool to use them to control other things. In this article I will describe in detail every step I took to achieve this. I've used a PIC microcontroller, but it should not be too hard to port the code to any other architecture.

Connector

The N64 controller has three connections. From right to left with the round side up: Ground, Data and VCC (3.3V). Just plug some cables in the holes and you're ready to go.

Hardware

The circuit I created for reading out the controller is shown in the schematic below. It is based around a PIC18F2550. I chose a pic from the 18F range, because I am interested in the USB capabilities, as I might try to use the N64 controller with a computer some day (future article maybe?). I did however limit my oscillator frequency to 20 MHz, so that everything should work on a 16F pic as well if you don't need USB.

I've connected three LED's to RB5, RB4 and RB3. They will provide some visual feedback so we can see if everything works properly. A 3.3V voltage regulator provides the voltage for the board. The maximum voltage the N64 controller can handle is 3.6V, so don't exceed that. If you use 5V you will risk frying your controller. The N64 controller is connected to the board with a simple screw connector. Here the supply voltage is given, and the data pin is connected to the RB7 pin of the PIC18F2550.

I've also connected a header (JP1) for the PICKit2. This allows me to program the PIC without having to take it out of the circuit (ICSP). At the moment it also provides the supply voltage to the board because I am to lazy to find a good power supply. Make sure you set the PICKit to 3.3V instead of the default 5V for the reason I mentioned earlier.

The image below shows the fully built circuit built on perfboard. The rectangular thing to the right of the microcontroller is a wireless RF transmitter. I will talk about this in a next article, but it is of no importance to us now.

Interface

Now we're going to take a quick look at the communication with the Nintendo 64 controller. The interface looks a little bit like one wire, but is still different. It's a serial interface, which means that all the signals discussed here, will take place on one pin: the DATA (middle) pin of the connector (white wire in the pictures above).

Signal Shapes

The signal shapes used by the N64 controller for one and zero are shown below. As you can see these signals are very fast with a period of 4 uS and a resolution of 1 us! On a PIC microcontroller, one instruction cycle equals 4 oscillator cycles, which means that with a 20 MHz oscillator, 1 uS takes only 5 instruction cycles! To generate these signals, we will need a low-level programming language like assembly, as C just won't cut it. I will give as much information as needed, so if this is your first experience with assembly, you will be able to follow easily. A good reference for the PIC instruction set can be found here: PIC Instructions.

Zero
One
movlw 0x00
movwf PORTB
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
movlw 0x80
movwf PORTB
nop
nop
nop
5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
movlw 0x00
movwf PORTB
nop
nop
nop
movlw 0x80
movwf PORTB
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
15
1
2
3
4
5
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Below both signal shapes I have included a very simple program to generate the signal in asm (of course there are other possibilities e.g. with bsf and bcf). There are only three different instructions used:

  • movlw: place the value that you indicate in the W register.
  • movwf: place the value that is currently in the W register, in the register that you indicate.
  • nop: do nothing.
It's easy to see that a combo of movlw with movwf can set a pin by modifying the PORTB register. As our N64 controller is connected to pin RB7, we set this pin high by sending the binary signal 0b10000000 to PORTB, which is 0x80 in hexadecimals. Ofcouse sending 0x00 makes the pin 0.

All three instructions we used take 1 instruction cycle (check the PIC 18F instruction set here). This means that it is very easy to count every step. As I said previously: 1 uS takes 5 instruction cycles. That means that, to generate a zero, we have to set pin RB7 low, wait for 15 instructions, and then set it high for 5 instructions. I have counted and indicated the instructions that the pin is high with a bold font, and the instructions that it is low with a gray font. You have to think about these code blocks as if a lot of them are cascaded after each other, so that the first instruction of every code block, is actually the last instruction of the previous block. This will make the timing exact.

Of course this code will only work with an oscillator frequency of 20 MHz on a PIC microcontroller. If you have another oscillator you can easily calculate how many instruction cycles you need for 1 uS though. If you are using another architecture (AVR, ARM, ...) you have to figure out how many oscillator cycles one instruction takes first (it is usually less than 4, which makes it easier to use C on those architectures).

Controller Data

The N64 controller data consists of a 32 bit (4 bytes), which gives you the status (on/off) of all buttons and the joystick position (2 bytes) and one stop bit. The array is built up like this:

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16-23
24-31
32
A
B
Z
Start
Up
Down
Left
Right
/
/
L
R
C-Up
C-Down
C-Left
C-Right
X-Axis
Y-Axis
Stop bit (1)
e.g. if the Z button is pressed, the 3th element of this array will read one etc.

The position of the joystick can be determined from the two bytes given by "X-Axis" and "Y-Axis".

Polling Signal

To receive the data array from the controller, a polling signal has to be sent first. This signal is: 0b000000011 (9 bits). After this signal is sent, the controller will respond with the 33 bit array (4 bytes + stop bit). This is shown in the picture below. The B-button is pushed so the second bit of the response is 1.

The assembly code used to generate the polling signal is shown below. Instead of copy pasting 7 zero blocks and 2 one blocks from above (which would work perfectly). I used the decfsz (decrement file, skip if zero) instruction to create loops. This instruction will decrement the register you indicate, and skip the next instruction if this register becomes zero. It takes two instruction cycles if it skips, else one! The registers d1, d2 and d3 can be defined in the beginning of the program using a cblock. After the decfsz instruction there is a goto instruction, which will move the program counter to the instruction after the word that you have indicated. It should be clear to see how a combo of these two instructions create a loop.

All instructions use one instruction cycle (4 oscillator cycles), except for: goto, which uses 2 instruction cycles. Of course decfsz also takes two instruction cycles when it skips, as mentioned earlier. Its not so hard to distinguish the two code blocks we saw earlier, only this time I tried to make them a little shorter by replacing some of the nop's with loops (zeroloop, oneloop). If you take the time to count the instruction cycles carefully for each loop (which I strongly recommend), you will see that there would be no difference if we would copy paste 7 zero's and 2 one's from the previous code.

bcf TRISB, 7            ; make RB7 output
    
; 0b000000011
movlw 0x07              ; number of zeros that need to be sent
movwf d2

movlw 0x02              ; number of ones that need to be sent
movwf d3

zero
    movlw 0x00
    movwf PORTB
    movlw 0x03
    movwf d1
    zeroloop            ; 3 instruction cycles * 3 = 9
        decfsz d1, f
        goto zeroloop
    nop
    nop
    movlw 0x80
    movwf PORTB
	decfsz d2
    goto zero

one
    movlw 0x00
    movwf PORTB
    nop
    nop
	nop
    movlw 0x80
    movwf PORTB
    movlw 0x02
    movwf d1
    oneloop             ; 4 instruction cycles * 2 = 8
        nop
        decfsz d1, f
        goto oneloop
    decfsz d3
goto one

note: the pin RB7 is set to function as an output pin by setting the TRISB register (duh!).

In the image below you can see a picture of an oscilloscope measuring the polling signal generated with the code above. You can clearly see the signal 0b00000011.

This is a close up of the 7th (0) and 8th (1) bit of the polling signal. The timebase is set to 1 uS. As you can see, the signal is extremely correct.

If you now connect the N64 controller, you should see that it responds immediately after the polling signal was sent. I have shown this in the pictures below. In the second picture the B button is pressed, so you can see that the second bit of the response is a 1. Once you attach the controller, the signal wil deform because of its impedance. This is normal and not really a problem for us.


Reading The Controller Data

Now we succeeded in triggering the Nintendo 64 controller so that it sends its data, we have to find a way to read & store this data properly. The easiest way to do this, is to wait until the middle of the signal (approx 2 uS) and then see if it is one or zero with the btfss (bit file, skip if set) instruction. This instruction will check the bit you indicate (e.g. "PORTB, 7" -> 7th bit of the PORTB register) and then skip the next instruction if this bit is one. It will take one instruction cycle, except if it skips when it will take 2 instruction cycles (it compensates automatically for the skipped instruction - nice!). Of course, we have to switch the RB7 pin to an intput now by setting it to 1 in the TRISB register.

Once we know if the bit was a 0 or a 1, we can store this somewhere in the RAM by using indirect addressing. The INDF0 register contains the value of the register pointed to by FSR0L, so if we increment FSR0L, we go to the next adress etc.

We can now read and store all values, so we will do this 33 times as there are 33 bits in the N64 array (see higher).

movlw 0x22              ; 0x21 = 33 bits
movwf d4            	; contains the number of buttons

movlw 0x20              ; set up the array adress for indirect adressing
movwf FSR0L
    
bsf TRISB, 7            ; RB7 as input 
    
readLoop                ; read all 33 bits and store them on locations 0x20 etc.
    nop                 ; Wait
    nop
    nop
    nop
    nop
    nop
    nop
    movlw 0xFF          ; if bit is one
    btfss PORTB, 7      ; read the bit (9th instruction)
    movlw 0x00          ; if bit is zero
    movwf INDF0         ; Wait & store the value of the bit in the array
    incf FSR0L          ; Go to next adres in the array
    nop
    nop			
    nop
    nop
    nop
    decfsz d4           ; end loop if all 33 bits are read
goto readLoop

note 1: the syntax for indirect addressing is a little bit different on 16F architectures. They use FSR and INDF.

note 2: this is definitely not the best way to read out a serial signal. A better way would be to re-sync the signal after every bit, by searching for a rising edge and then falling edge. This way you know for sure that you are at the beginning of the pulse form. I have tried to do this, but it seemed impossible with the slow pic (4 oscillator cycles / instruction cycle). You might succeed with this approach on an AVR though (1 oscillator cycle / instruction). Please let me know if you find a cleaner way! (This works perfectly though ...)

The Full Assembly (ASM) program

The full assembly program is given below. It blinks the leds RB3, RB4 and RB5 respectively when the buttons A, B & Z are pressed. I made use of the mpasm (v5.46) compiler which comes with the MPLAB X IDE. It's probably pretty easy to figure out what everything does if you look up the instructions on the webpage I referred to earlier, so I won't go into detail here.

#include <P18F2550.INC>

CONFIG WDT = OFF        ; disable watchdog timer
CONFIG FOSC = HS        ; Oscillator crystal of 20MHz!
CONFIG LVP = OFF        ; Low-Voltage programming disabled
CONFIG DEBUG = OFF
CONFIG CP0 = OFF
CONFIG CP1 = OFF
CONFIG MCLRE = OFF

org 0

init
    cblock
        d1
        d2              
        d3           
        d4              
    endc

    movlw 0x00
    movwf LATB
    movwf TRISB
    movwf PORTB

    movlw 0x80              ; initalise the line = HIGH
    movwf PORTB

Main
    ; Part 1 - Read the N64 controller and store the status values

    movlw 0x22              ; 0x21 = 33 bits
    movwf d4                ; contains the number of buttons

    movlw 0x20              ; set up the array adress
    movwf FSR0L

    bcf TRISB, 7            ; make RB7 output
    call poll               ; Send the polling signal (0b000000011)

    bsf TRISB, 7            ; RB7 as input  (is also last instruction of poll)

    readLoop                ; read all 33 bits and store them on locations 0x20 etc.

        nop                 ; Wait
        nop
        nop
        nop
        nop
        nop
        nop

        movlw 0xFF          ; if bit is one
        btfss PORTB, 7      ; read the bit (9th instruction)
        movlw 0x00          ; if bit is zero

        movwf INDF0         ; Wait & store the value of the bit in the array
        incf FSR0L          ; Go to next adres in the array

        nop
        nop
        nop
        nop
        nop

        decfsz d4           ; end loop if all 33 bits are read
    goto readLoop

    ; Blink some leds

    movlw 0x20              ; read A button (0x20) from array
    movwf FSR0L
    movlw 0x20              ; led RB5
    btfsc INDF0, 0
    iorwf LATB, PORTB

    incf FSR0L              ; read B button (0x21) from array
    movlw 0x10              ; led RB4
    btfsc INDF0, 0
    iorwf LATB, PORTB

    incf FSR0L              ; read Z button (0x22) from array
    movlw 0x08              ; led RB3
    btfsc INDF0, 0
    iorwf LATB, PORTB

    call delay
goto Main

poll                        ; 0b000000011
    movlw 0x07              ; number of zeros that need to be sent
    movwf d2

    movlw 0x02              ; number of ones that need to be sent
    movwf d3

    zero
        movlw 0x00
        movwf PORTB
        movlw 0x03
        movwf d1
        zeroloop            ; 9 instruction cycles
            decfsz d1
            goto zeroloop
        nop
        nop
        movlw 0x80
        movwf PORTB
        decfsz d2
        goto zero

    one
        movlw 0x00
        movwf PORTB
        nop
        nop
        nop
        movlw 0x80
        movwf PORTB
        movlw 0x02
        movwf d1
        oneloop             ; 8 instruction cycles
            nop
            decfsz d1
            goto oneloop
        decfsz d3
        goto one
return


delay   movlw   0x5f        ; +- 1500 uS = 1.5 ms
        movwf   d1
outer   movlw   0x60
        movwf   d2
inner   nop
        nop
        decfsz  d2
        goto    inner       ; Inner loop = 5 cycles = 1uS
        decfsz  d1
        goto    outer       ; outer loop = inner + 4 cycles
return

end

Video

Here is a small video of the result. I'm sorry that I didn't do anything cooler with it than blinking some leds, but that's for when I have some more time!

Getting the angular position from gyroscope data

In this article I will explain how I succeeded in finding the angular position (angle) of one axis of a quadrocopter by integrating gyroscope data. For this article I will use the gyrosocope from the Hobbyking HK401B module I hacked in a previous article. Because I wanted a fast prototyping method I used an arduino, but the principle is the same on any other device. I will include the code for a PIC microcontroller as well.

A little math

Before we take a look at the program we will go through the basic mathematics involved here, as they will make understanding the program a lot easier.

The gyroscope gives us the rate of change of the angular position over time (angular velocity) with a unit of [deg./s]. This means that we get the derivative of the angular position over time.

However rate feedback is extremely useful in control engineering, it is usually used in combination with position feedback. To obtain the angular position, we can simply integrate the angular velocity. So, assuming that at t=0 theta=0, we can find the angular position at any given moment t with the following equation:

The third part in this equation shows the approximation we make when using digital systems. Because we can't take a perfectly continuous integral, we have to take the sum of a finite number of samples taken at a constant interval Ts. Ts is called the sampling period. Of course this approximation will introduce errors. When gyrosocope data changes faster than the sampling frequency, we will not detect it, and the integral approximation will be incorrect. This error is called drift, as it increases in time. It results in the sensor reading not returning to 0 at the rest position. For this, it is important that we choose a good sampling period. The sampling frequency recommended for mechanical systems lies between 100 and 200Hz. I will use 100Hz (sampling period of 10ms) as this equals the cycle time of my quadrocopter PWM program.

Choosing a sampling frequency of 100Hz is a good choice for an "ideal case" mechanical system. It has to be understood though that disturbance inputs like shocks & vibrations because of the motors can have a much higher frequency and will not be measured properly. This is mostly the case for disturbances that work directly on the sensor, and it is a big problem of this integration method. Disturbances like wind and external forces that work on the system (quadrocopter) will be slowed down by the intertia of this system so that they will be measured correctly and can be compensated for.

The gyroscope sensor

This sensor was already discussed briefly in a previous article. It is an analog sensor, which means that it will output an analog voltage that will be a measure for the angular velocity. It is also a one axis gyroscope, which means that there is only one output. The axis that this sensor can measure falls together with the axis of the wires. It is shown in the picture below.


When the angular velocity is 0 deg./s, the sensor will output a voltage that is close to half of the supply voltage. A negative velocity will decrease this voltage while a positive velocity will increase this voltage. In the previous article I put a reference to a datasheet, but it doesn't seem to be correct. After 50 mails to Hobbyking, I gave up finding the datasheet, and just found out my values by trial/error. This what I came up with at a supply voltage of 5V:
  • Offset (output at angular velocity = 0): 2.65V
  • Sensitivity (scale factor): 107.42 mV/deg./s
The value for the offset can be measured with a multimeter. To find the value for the sensitivity, I wrote the arduino program that is shown below, and used the Serial monitor to check the correctness. E.g. hold the quadrocopter at 90 deg. and see if the measurement is correct or not. Trial/error! The weird number "107.42" was a result of the conversion from digital to analog.

The arduino program

Let's take a closer look at the arduino program now. If you are using the gyroscope from the HK401B like me, you can connect it by putting 5V between the black and red wires of the gyro (or whatever colors you soldered to the supply pins) and by connecting the white (signal) cable to the A0 analog input. If you are using another analog sensor, it should be easy enough to figure out how to connect it! The whole program works with 8 bit values. This means that the analog offset value of 2.65 results in a digital value of 136:

The same principle would apply to the sensitivity if it was given in the datasheet. I found this value by trial/error so I knew it was 5.5 digitally.

However the arduino has a 10-bit ADC, I only use the 8 most significant bits. The last two bits of the ADC are usually not used because they contain a lot of noise and it is a lot more efficiënt to work with 8-bit values on an 8-bit processor ;).

It should be very easy now to see how I converted the approximated integral equation we saw earlier into an extremely simple program. The program runs at 100Hz. Every iteration we read the sensor, and convert its value into an 8-bit digital number (gyroValue). After this, the offset is removed and the gyroValue is being converted to [deg. /s] by multiplying it with the sensitvity. Finally we multiply it with the sampling period (*0.01 or /100) and add it to the angle, which is the sum (approximated integral) of all previous values.

It is very important to take a look at the datatypes you are using. I recommend using a float for the angle and the rate. The reason for this is that if you make more approximations by rounding off to another datatype, the drift would grow (extremely) fast. The only way this integral approximation will work, is by doing it very precise.

unsigned char gyroValue = 0;      
unsigned char offset = 136;
float angle = 0;
float gyroRate;
 
void setup() {
  Serial.begin(9600); 
}
 
void loop() {
  gyroValue = analogRead(A0) >> 2;   // Work with 8 most significant bits!
 
  gyroRate = (gyroValue - offset) * 5.5; // Sensitivity has been found trial/error
  angle += gyroRate / 100.0;
 
  Serial.print(angle);
  Serial.print("\n");
 
  delay(10);                     
}

The PIC program

To demonstrate how easy it is to port this code on another platform, I included my implementation for the PIC16F690. A 20MHz crystal was used, and the gyro is connected the RA0-pin. I make use of the HI-TECH C Lite compiler.

#include <pic.h>
#include <pic16f690.h>
 
__CONFIG(FOSC_HS & WDTE_OFF & PWRTE_OFF & MCLRE_OFF & CP_OFF);
 
#define _XTAL_FREQ 20000000
 
/* Function prototypes */
void ADC(void);
void main(void);
 
unsigned char gyroValue;
unsigned char gyroOffset = 136;
float gyroRate;
float angle = 0;
 
/* Main program */
void main(void)
{
    // Init
    TRISA = 0b00000001;     // RA0 input (Analog)
 
    ADCON0 = 0b00000001;    // AD conversion init RA0 = input
    ADCON1 = 0b00000000; 
 
    while(1)
    {
        // Read Gyro
        ADC();
        gyroValue = ADRESH;
 
        gyroRate = (gyroValue - gyroOffset) * 5.5;
        angle += gyroRate / 100.0;
 
        __delay_us(10000);	// 10 ms 
    }
}
 
void ADC(void)
{
    GO_DONE = 1;
    while(GO_DONE);
}

Results

Here is a little video of the program in action. It is very clear to see the problem of drift introduced by approximating the integral by a sum. I also gave the sensor a few ticks with my finger to show that the drift is much stronger as a result of external forces that work directly on the sensor (e.g. motor vibrations, shocks, etc.).

UPDATE: To solve the drift error, we can combine the gyroscope data with accelerometer data using a Complementary Filter. This is described here.

Setting up the Raspberry Pi for programming from Mac OS X

In this article I will explain how I set up my Raspberry Pi to be programmable from my macbook pro. I didn't want to use an external monitor & keyboard, nor did I want to connect my Pi to the router of my home network as this setup would not be very mobile. To achieve this, I made use of SSH and the mac's ability to share its internet with other computers. I will not use any GUI to do the programming, as I find that running a GUI on a small embedded computer like this is overkill. Instead I will do all the programming from the command line. A small example of how to do this is included here as well. This article was mainly written as a future reference for myself, but I hope it can be useful for others as well.

Step 1: Prepare the SD-card

First of all, you will need an SD-card with a bootable linux image. I recommend using the "Raspbian Wheezy" image, which is an optimized version of debian. It is downloadable from http://www.raspberrypi.org/downloads/. Instructions for putting the image on the card properly can be found on the eLinux website.

Preparing SD card

If this has succeeded, you can boot up your Raspberry Pi! For the initial boot we will need an external monitor & keyboard, so we can set everything up properly.

Step 2: Basic configurations

The easiest way to do the basic setup is by making use of the raspi-config tool, it should start up automatically on your first boot.


If it doesn't start automatically, you can also launch it with:
sudo raspi-config
I would recommend starting off by setting configure_keyboard and change_timezone. As we won't use any GUI for our programming, it is a good idea to set up memory_split so that the ARM gets 240MB of RAM and the VideoCore only 16MB as this will speed up things a lot. If you have a large SD-card (> 4G) you might want to consider using expand_rootfs to enable all this space. The downside of this is that backups of your SD image will be significantly larger.

The most important thing we had to do here, was to enable SSH. The more recent versions of raspbian have this enabled by default though.

If you have used configure_keyboard, you should run:

sudo setupcon
to avoid a massive delay in the boot process.

Step 3: Find out the Raspberry Pi's IP adres

At this point, we need our Pi connected to the internet. If you have a router close to your working environment, plugging it in there would be fine. If you don't however, it is very easy to use the "internet sharing" option on a mac to provide internet to your Raspberry Pi. On your mac, go to: "System Preferences > Sharing".


As you can see, I chose to share my macbook's wifi through the ethernet port. If I plug in my Raspberry there, it will have internet.

Now go back to the Raspberry Pi's monitor & keyboard. To be able to connect to the Pi through SSH, we need to know the IP adres it is using. To find it out, we enter the following command:
ifconfig
You should see something like "inet addr: 192.168.2.2".

Step 4: Connect to the Raspberry Pi from your mac

Now we know everything we need to know and everything is set up properly, we can disconnect the monitor & keyboard from our Pi (forever). To connect establish the SSH connection, go to the mac terminal and type (you should of course change the IP with the one you found earlier):

ssh -lpi 192.168.2.2

If it is the first time you connect to this device, you will probably see something like "The authenticity of the host can't be ... blabla". You can answer "yes" to this.

You will be prompted for the password now. If you did not change this in step 2, and you are using the recommended raspbian image, the password is "raspberry". Enter this, and you are now logged in to the Pi! You should see something like this:

Step 5: Setup a basic programming environment

You don't need much to programming from the command line in linux, but you do need a good text editor. I use vim. If this is the first time you work with vim, I recommend taking 10 minutes time to check out a good tutorial. To install vim on your raspberry pi, simply type:

sudo apt-get install vim

Because I think syntax highlighting and line numbering are essential programming tools, I want to enable these by default. You can do this by editing (or creating) the .vimrc file in your user directory. We'll do this using vim:

vim ~/.vimrc
Then add the following two lines:
syntax on
set number

Now it's time to test our tools! If you write a very simple "Hello World!"-program with vim, it should look like this:

If you name the file "main.cpp", you can compile it with:

g++ main.cpp -o HelloWorld
If you then run it with:
./HelloWorld
you can sit back, and enjoy the most simple program in the world!

If you're interested in doing some more advanced programming of your Raspberry Pi, check out my article on Low Level Programming of the Raspberry Pi.

Universal/Jamming Gripper Prototype

I made my own version of the Universal/Jamming Gripper created by John Ahmend.

You can do this too in 5 minutes! The recourses you need are:

  • Plastic syringe (ask your pharmacist, he/she will look weird but give it to you if you don't look like a drug addict)
  • Balloon (a small one)
  • Ground coffee
  • Duct tape
  • Piece of cloth/woven tissue (should act as a filter to let the air pass but not the coffee so keep this in mind)
  • Superglue

To assemble it you have to go through the following steps:

  • Step 1 - Cutt off the tip of the plastic syringe as cleanly as possible.
  • Step 2 - Cover the hole that is now in the syringe with the piece of hankerchief and glue it reasonably tight with the superglue. Let it dry.
  • Step 3 - Fill the balloon as good as you can with the coffee. This is a difficult step! It might help if you fill the balloon with air a few times so that it stretches.
  • Step 4 - Make sure the syringe is closed so that it does not contain any air. Then pull the neck from the balloon over the syringe, and make sure you don't trap any air. If you do trap air, try to get it out as much as possible as it will drastically decrease the performance of your gripper.
  • Step 5 - Tape the balloon and the syringe together as tightly as possible using the duct tape.

Here is the gripper in action!


The cool thing about this gripper is that you don't need a pump or anything pneumatic. So if you could find a way to professionally make this, and actuate the seringe with a solenoïd, you can turn this into a very compact and fast device which has all the actuators on board. This would make it fully electrically controllable.

I quickly sketched up an idea I had for this in which I added some extra "fingers" which would make the universal gripper even more universal ;). The first part of the finger would be driven with a small motor and a worm wheel to block it when the motor is not on. The second part part of the finger would be driven by small linear actuators. I never really built this, it's just an idea.

Hacking the Hobbyking HK401B gyroscope module

For the quadrocopter project we are doing, we bought a hobbyking HK401B gyro module. This module includes a one axis analog gyroscope and a full microcontroller circuit, that already processes the gyroscope data. It sends out the "corrections" the motors should make and can be connected directly to the typical RC-setup.

Because we are not interested in processed data, we decided to break open the module, and desolder the gyroscope. Here are some pictures of the proces:

As you can see on the pictures, the HK401B consists of three parts. The gyro part is the one that has initially no wires attached to it (in the pictures above I already added wires though). Once you desolder this part, it is sufficient to add the supply voltage (black and red wires: 5V - check the picture to see which is which) and a third wire for the signal (white). The axis of rotation that the gyro can "sense" is in the same direction of the wires. The following video shows the signal that can be expected:

This datasheet seems like it might be the right one.

Four (4) independent PWM-signals generated with one PIC

Every self-respecting engineer has at least one moment in his or her life when it seems like the right thing to do is build flying stuff. In the case of me and a few friends, this means that we are going to do an attempt at creating our own quadrocopter. We quickly started looking around the internet, and placed an order at http://www.hobbyking.com/ (I will describe the contents of this order in a later post once it has shipped to my doorstep).

Because the shipping would take a few days, and another property of the self-respecting engineer is that he does not have any form of patience when it comes to building stuff. We decided that we would already start designing the control circuitry, which we will try to do from scratch. A first step in this is finding a way to control the speed of the motors.

The Signal

Quadrocopters are built with brushless DC motors. A common mistake is to assume that these are conventional DC-motors, and can be driven as such, which is far from true!! Actually, a brushless DC motor is a permanent magnet AC motor with a block waveform. I won't go into detail, but this is important stuff! Because of this AC-block waveform we can't just drive this motor with a normal H-bridge, other circuitry is required. It would be very possible to build this ourselves, but the challenge is pretty low and because this is part of the power-circuit, the risk of introducing power-loss is too high. This makes that we use what the industry has provided for us: the ESC (Electronic Speed Controller). We can just hook up our motor and our battery to the ESC. If this is driven with a specific PWM signal, the speed of the motor will vary. This is what the PWM signal looks like:

First send a 1 ms pulse, then send the second pulse that controls the speed (max. speed = 1ms, min. speed = 0ms) and then wait until the total period is about 10ms. The waiting time can be longer than 10 ms because it would be hard to do this with RC otherwise. It is only the pulse length that is important. From this information, we know that we have to generate a PWM-signal of 100Hz (period 10ms) with a duty cycle that is variable between 10% and 20%. We will have to be able to do this 4 times independently, because a quadrocopter has four motors that have to be able to operate at different speeds (duh!).

Prototype

As the title said, we created this PWM-controller using a PIC microcontroller. The one we chose was a very small one: a PIC16F684 with 128 bytes of RAM and 2048 words of flash. Because we don't have any RC-setup yet, the inputs that control the motor speed are 4 potentiometers that are read in with an analog multiplexer (74HCT4051). This saves us a pin on the MCU and makes it a little easier to program the ADC unit properly. Also when we want to add accelerometer or gyro modules later with analog signals, we can easily do this without losing pins (except for one extra adres pin ofcouse!). The full schematic looks like this:

Here's a picture of the prototype:

The four LED's are at the output pins (where the ESC's will be attached to) and give some visual feedback during prototyping (the intensity of a LED changes when the duty cycle is modified, duh!).

We could have chosen a PIC that has 4 independent PWM channels like the PIC18F4x31. Why didn't we? Because we believe that this is one of the simplest parts of making a quadrocopter and it shouldn't have any impact on the choice of your hardware. It's also a nice exercise in efficiency to work with low resources. If we want to cram the full quadrocopter control-software in a PIC one day, we might want to start thinking efficient from day one. The code I provide here is not 100% optimal, but I did my best (let me know if you have any improvements!).

Program Structure

The hardest thing about writing the code was finding a way of outputting the 4 PWM signals with varying duty cycles at the same time . It is possible that motor 1 had to go faster than motor 2 at first (motor 1's pulse is longer than motor 2's pulse), but when the direction ischanged, motor 2 has to go faster than motor 1. This made it a little more awkward to generate the 4 PWM's at the same time. A little creativity though, easily solved this problem by sorting the pulses from short to long so that they could be turned off sequentially. Because there are 3 obvious parts to the signal shown above, I decided to cut my code into three big pieces. This is how the program was organized:

Part 1: 0 - 1 ms

  • Set all output pins to high.
  • Read the 4 potentiometers and convert them to digital values using the internal ADC of the PIC. The values are stored in the ADCValues table.
  • Sort the ADCValues table from low to high and keep track of which output corresponds to which value (whichMotor).
  • Wait until the first millisecond has passed

Part 2: 1 - 2 ms

  • Wait until first output pin has to be turned off, and then turn it off.
  • Do this for the other 3 pins as well. - Wait until the second millisecond has passed.

Part 3: 2 - 8 ms

  • Wait until 8 ms have passed.

The C program

I make extensive use of the internal timers (timer 1) of the microcontroller because this gives a lot of control over the actual time passed, and you can do other stuff in the meantime instead of just waiting like with a delay function. The time that passes for a value of the TMR1-register can be easily calculated with: time = 1/OscValue * 4 * Prescaler * (65536 - TMR1). A very useful tool for this is the PIC timer calculator. As you can see in the code underneath, the remaining program has to be put in the two while loops. I indicated this with "// Do Something" in the comment.

I make use of the MPLABX IDE and the Hi-Tech C Lite compiler.

#include <pic.h>
#include <pic16f690.h>
#include <math.h>
 
__CONFIG(FOSC_HS & WDTE_OFF & PWRTE_OFF & MCLRE_OFF & CP_OFF);
 
#define _XTAL_FREQ 20000000
 
/* Function prototypes */
void ADC(void);
void main(void);
 
/* Global variables */
unsigned char ADCValues[4];
unsigned char whichMotor[4];
unsigned char help;
 
static int accData[3], gyrData[3]; // AccX ; AccY ; AccZ ; GyrX; GyrY; GyrZ
//SPid pitchPID, rollPID;
static int pitch = 0, roll = 0;
 
/* Main program */
void main(void)
{
    unsigned char i, j;
    unsigned char PORTAValue = 0b00000000, PORTCValue = 0b00000000;
 
    // Init
    TRISA = 0b00000001;     // RA0 input (Analog), RA1 & RA2 output (MUX adres)
    TRISC = 0b00000000;     // Port C output (PWM Signals)
 
    ADCON0 = 0b00000001;    // AD conversie init
    ADCON1 = 0b00000000;
 
    PEIE = 1;               // Peripheral interrupt enable
    GIE = 1;                // Global interrupt enable
    TMR1IE = 1;             // Timer 1 interrupt enable
    T0IE = 1;
 
    TMR1IF = 0;
 
    T1CON = 0b00000000;     // Timer 1 prescale 1:1
 
    // Infinite loop
    while(1)
    {
        // ------------------------------------------------------------------ //
        // -------------------------- MS 0 to MS 1 -------------------------- //
        // ------------------------------------------------------------------ //
 
        // Set all the pins high to start the PWM
        PORTCValue = 0b00001111;
        // Keep track of current state of the ports in a register to walk around
        // the RMW-problem
        PORTC = PORTCValue;
 
        // Start Timer 1 ms (time = 1/OscValue * 4 * Prescaler * (65536 - TMR1))
        TMR1 = 60536;   // 1 ms with prescaler 1:1 and freq 20MHZ
        TMR1ON = 1;
 
        // Get ADC value of all input POT'S
        i=0;
        for(i; i < 4; i++)
        {
            // Set MUX adres on RA1 and RA2
            // without affecting current state of PORTA (READ MODIFY WRITE)
            PORTAValue &= 0b11111001;
            PORTAValue |= (i << 1);
            PORTA = PORTAValue;
 
            __delay_us(1); // Delay here to wait until MUX value is on the pin
            ADC();
            (ADRESH <= 250)? ADCValues[i] = ADRESH : ADCValues[i] = 250;
        }
 
        // Initialise the whichMotor table
        i = 0;
        for(i; i<4; i++)
            whichMotor[i] = i;
 
        // Sort the ADC values from low to high
        // (this is the order in which they will be turned off)
        i=0;
        for(i; i<4; i++)
        {
            j = i+1;
            for(j; j < 4; j++)
            {
                if(ADCValues[j] < ADCValues[i])
                {
                    help = ADCValues[i];
                    ADCValues[i]=ADCValues[j];
                    ADCValues[j]=help;
 
                    help = whichMotor[i];   // Keep track of output pins
                    whichMotor[i] = whichMotor[j];
                    whichMotor[j] = help;
                }
            }
        }
 
        // Calculate the values that have to be loaded in the timer
        unsigned int delayValues[5];
        i = 0;
        delayValues[0] = 65535 - ADCValues[0]*20;
        for(i; i<3; i++)
        {
            // time = 1/OscValue * 4 * Prescaler * (65536 - TMR1)
            // ADC value is scaled to 1/4 of 1000 (value * 4)
            delayValues[i+1] = 65535 - (ADCValues[i+1] - ADCValues[i])*20;
        }
        delayValues[4] = 60535 + ADCValues[3]*20; // remaining time to 1 ms
 
        // Wait until the first ms is passed
        while(!TMR1IF)  // Wait until the timer overflows
        {
            // DO something
        }
        TMR1IF = 0;     // Must be cleared in the software
        TMR1ON = 0;
 
        // ------------------------------------------------------------------ //
        // -------------------------- MS 2 to MS 3 -------------------------- //
        // ------------------------------------------------------------------ //
 
        // Turn off the outputs in the right order: ~ is binary invert
        // Wait until first output has to be turned off
        // whichMotor holds the index of the current output pin
        // (bitshift inverted 1 this many places to turn that output off)
        i = 0;
        for (i; i<4; i++)
        {
            TMR1 = delayValues[i];
            TMR1ON = 1;
            PORTCValue &= ~(0b00000001 << whichMotor[i]);
            while(!TMR1IF);
            TMR1ON = 0;
            TMR1IF = 0;
            PORTC = PORTCValue;
        }
 
         TMR1 = delayValues[4];
         TMR1ON = 1;
         while(!TMR1IF);
         TMR1ON = 0;
         TMR1IF = 0;
 
        // ------------------------------------------------------------------ //
        // -------------------------- MS 3 to MS 10 ------------------------- //
        // ------------------------------------------------------------------ //
 
        // Start Timer 8 ms
        TMR1 = 25536;   // 8 ms with prescaler 1:1 and freq 20MHZ
        TMR1ON = 1;
        while(!TMR1IF)
        {
            // Do shit - read sensor or whatever
        }
        TMR1IF = 0;
        TMR1ON = 0;
    }
}
 
void ADC(void)
{
    GO_DONE = 1;
    while(GO_DONE);
}

Results

It has to be said that this code is not perfect yet. Because stopping & starting the timers and writing to PORTC all happens outside the timer, a small error is made here. Of course this could be compensated for, but this would make the code more complex, which is not necessairy.

Here are some pictures and a video to show the working circuit.



Here is a video of our PWM-controller driving two motors (I was too lazy to screw on all four motors because I will have to take them off soon anyway ...):

NARF Keypoint Test

NARF is a method for interest point detection developed by the PCL-community. However the algorithm is different, it is mostly used as a license free variant of the SIFT-algorithm. These kind of algorithms have proven to be very effective for object recognition, which is an important reason I found it necessary to check them out. A second reason is that I still haven't succeeded in getting good results with the ICP-algorithm for point cloud alignment (registration). I hope that, when using ICP on two sets of keypoints, the algorithm will be faster and less prone to accumulative errors.

To implement the NARF-detector I took a quick look at the tutorial on the PCL-website. I used the code as a starting point to make my own cleaner version of it. It is given in the code block underneath. Note that this is part of a class called "Model3D". The code basically creates a range image, and then simply detects the keypoints on that image. Afterwards the found points are transformed back to 3D, and added to the original cloud in green (for visualization purposes!).

void Model3D::CreateRangeImage()
{
   pcl::PointCloud<PointType>& pointCloud = *this->cloud;
 
   Eigen::Affine3f sceneSensorPose = Eigen::Affine3f(Eigen::Translation3f(pointCloud.sensor_origin_[0], pointCloud.sensor_origin_[1], pointCloud.sensor_origin_[2]))*Eigen::Affine3f (pointCloud.sensor_orientation_);
 
   this->rangeImage->createFromPointCloud (pointCloud, pcl::deg2rad (0.5f), pcl::deg2rad (360.0f), pcl::deg2rad (180.0f), sceneSensorPose, pcl::RangeImage::CAMERA_FRAME, 0.0, 0.0f, 1);
 
   this->rangeImage->setUnseenToMaxRange();
}
 
void Model3D::DetectNarfKeypoints()
{
   this->CreateRangeImage();
 
       /* Extract NARF keypoints */
   pcl::RangeImageBorderExtractor rangeImageBorderExtractor;
   pcl::NarfKeypoint narfKeypointDetector;
   narfKeypointDetector.setRangeImageBorderExtractor (&rangeImageBorderExtractor);
   narfKeypointDetector.setRangeImage (this->rangeImage.get());
   narfKeypointDetector.getParameters().support_size = 0.2f;
   narfKeypointDetector.setRadiusSearch(0.01);
   //narfKeypointDetector.setSearchMethod(tree);
 
   pcl::PointCloud<int> keypointIndices;
   narfKeypointDetector.compute (keypointIndices);
 
   cout << "Found "<<keypointIndices.points.size ()<<" key points.\n";
 
   /* Put the points in a cloud */
   this->keyPoints->points.resize(keypointIndices.points.size());
   for (size_t i=0; i<keypointIndices.points.size(); ++i)
   {
       this->keyPoints->points[i].getVector3fMap () = this->rangeImage->points[keypointIndices.points[i]].getVector3fMap();
       this->keyPoints->points[i].r = 0;
       this->keyPoints->points[i].g = 255;
       this->keyPoints->points[i].b = 0;
       //this->keyPoints->points[i].size
   }
 
   *this->cloud += *this->keyPoints;
 
   /* Extract NARF descriptors for interest points */
   std::vector<int> keypointIndices2;
   keypointIndices2.resize (keypointIndices.points.size ());
   for (unsigned int i=0; i<keypointIndices.size (); ++i) // This step is necessary to get the right vector type
       keypointIndices2[i]=keypointIndices.points[i];
   pcl::NarfDescriptor narfDescriptor (this->rangeImage.get(), &keypointIndices2);
   narfDescriptor.getParameters().support_size = 0.2f;
   narfDescriptor.getParameters().rotation_invariant = true;
   pcl::PointCloud<pcl::Narf36> narfDescriptors;
   narfDescriptor.compute (narfDescriptors);
   cout << "Extracted "<<narfDescriptors.size ()<<" descriptors for "
                         <<keypointIndices.points.size ()<< " keypoints.\n";
}

The following video demonstrates the software I have created for my master thesis. In the end there is a quick demo of the NARF-feature using the above code. Because the points where not very visible I added an image underneath, which shows them better because I set the point-size of the visualizer to a bigger value.


Custom ROI (Region Of Interest) with OpenCV

Today, I was in need of a custom ROI (a parallellogram). In OpenCV, the way to do this is to use a mask. This can be created by puting a filled white shape on a black one-channel image (CV_U8C1 format). After this the CopyTo-method can be used to subtract the ROI from the image. The code I used can be seen in the code block below. The vector "ROI_Vertices" contains the vertices of the parallogram I want to use as the ROI, so you will have to define this first. I first fit a polygon around it (ROI_Poly) because OpenCV likes this better, and then fill it with white.

/* ROI by creating mask for the parallelogram */
Mat mask = cvCreateMat(480, 640, CV_8UC1);
// Create black image with the same size as the original
for(int i=0; i<mask.cols; i++)
   for(int j=0; j<mask.rows; j++)
       mask.at<uchar>(Point(i,j)) = 0;
 
// Create Polygon from vertices
vector<Point> ROI_Poly;
approxPolyDP(ROI_Vertices, ROI_Poly, 1.0, true);
 
// Fill polygon white
fillConvexPoly(mask, &ROI_Poly[0], ROI_Poly.size(), 255, 8, 0);                 
 
// Create new image for result storage
Mat imageDest = cvCreateMat(480, 640, CV_8UC3);
 
// Cut out ROI and store it in imageDest
image->copyTo(imageDest, mask);    

The mask looks like this:

The subtracted part is shown here:

Bachelor Assignment

The assignment was to build a solar tracker as efficiently as possible. We decided to build one that is controlled by a microcontroller-circuit. A Real Time Clock was used to calculate the position of the sun with a time-based algorithm. This is the circuit we designed:

Here are some pictures of the fully soldered circuit:



I also built a prototype in wood to see if it works well. The solar tracker only moves a few times a day. Here is a video taken when it did:




Subscribe to Front page feed