Implementation

Atmel AT90USBKey

The Atmel AT90USBKey has built-in ADC, PWM, and USB, all of which are used in this project. Applications for the AT90 board are written in C, compiled in the AVR Studio environment, and loaded into flash memory through USB using the software program Flip. In the remainder of this section, some general notes with respect to programming the microcontroller are described.

Programming Notes

When referring to a register or bit in a program, the precise form must be used. For instance, PORTC6 refers to bit 6 of Port C or OCIE0A refers to bit 1 in the TIMSK0 register.

In order to read the value of a pin on any one of the I/O ports, the data direction register for that pin must be set as input. For example, DDRD &= ~(_BV(1)); configures PORT D1 to be input. Alternatively, DDRD |= (_BV(1)) would set PORT D1 as output.

Another point worth noting is that the microcontroller uses Little Endian when reading and writing to the registers. This means the low byte is processed before the high byte.

System Clock Prescaler

The default clock speed is 1 MHz (s. 6.2.1, p. 40 of the User Manual). However, in this project, the clock speed is set to 8 MHz. This can be done in two ways: (1) setting the frequency in Project Configuration Options in AVR Studio or (2) directly through the programming code. To change the system clock through programming, the first thing to do is disable interrupts so the change is not interrupted. Next, it is necessary to enable the clock prescaler, which is done by setting the CLKPCE bit in the CLKPR register. Next the division factor is set using the CLKPS3..0 bits. For example, for a prescale factor of 8, CLKPS3..0 needs to be set to 0011. After the prescale factor has been selected, CLKPCE must be cleared in order for the change to take effect. Finally, enable interrupts if they are being used.

Timers

The microcontroller has four timers available for use: two 8-bit timers (Timers 0 and 2) and two 16-bit timers (Timers 1 and 3). For this project, Timer 0 is used with the height motor, Timer 1 is used to implement the hardware timer function, Timer 2 is used with the left/right motors, and Timer 3 is used with the sonar. Each timer must be initialized before it can be used. For the initialization code, please see the appropriate section below.

[Top]

UART

The UART driver was written by a previous student of this course, Ron Desmarais. The driver includes functions which are not used in this project. For an explanation of the functions/code not used here, please see the driver code.

Enabling the UART

"The initialization process normally consists of setting the baud rate, setting frame format and enabling the Transmitter or the Receiver depending on the usage." (s.18.4, p.187 of the User Manual). The following uart_init() function was written to initialize the UART:

CLKPR = 0x80;
CLKPR = 0x00;
UCSR1A = (0<<U2X1);
UBRR1L = 51;
UCSR1B = (1<<RXEN1)|(1<<TXEN1)|(1<<RXCIE1)|(1<<TXCIE1)|
  (1<<UDRIE1)|(0<<UCSZ12);
// Initialize the UART to 9600 Bd, tx/rx, 8N1. And set its mode to 0 or 1
UCSR1C = (0<<UMSEL11)|(0<<UMSEL10)|(0<<UPM10)|(0<<USBS1)|
  (1<<UCSZ10)|(1<<UCSZ11)|(0<<UCPOL1);
// Set frame format: 8data, 3stop bit
mode = m;

Printing to a Hyper-Terminal using UART

The following uart_println("...") function was written to print strings to a hyper-terminal using the UART:

va_list arg_list;
va_start (arg_list, str);
char str1[TX_BUFSIZE];
vsprintf(str1,str, arg_list);
va_end (arg_list);
uart_putstringln(str1);
// This formats the string so it acts like the printf function

Note: when opening a hyper-terminal, ensure that the baud rate selected is 9600 and the data flow rate is set to none. Otherwise, this UART driver will not work with the hyper-terminal.

[Top]

Radio

The radio driver was written by Scott Craig and Justin Tanner and modified by Leanne Ross in collaboration with Mantis Cheng.

This radio chip has three modes of operation: configuration, stand by, and active. The configuration mode consists of a 15-byte word being downloaded to the radio. This configuration word consists of the amount of data to be transmitted (the payload), the address of the radio, the channel data is sent over, and the active mode bit.

static uint8_t radio_cfg[CFG_VECT_LEN] =
{
// Configuration data defined in radio.h
DATA2_W,
/* byte 0 */   0 - not used
DATA1_W,
/* byte 1 */   256
ADDR2,
/* bytes 2 to 6 */   0,0,0,0,0 - not used
ADDR1,
/* bytes 7 to 11 */   0,0,0,2-byte address of radio
ADDRW_CRC,
/* byte 12 */   ((16 << 2) | _BV(1) | _BV(0))
MODES,
/* byte 13 */   (_BV(6) | _BV(5) | _BV(3) | _BV(2) | _BV(1) | _BV(0))
RX2_EN=0, CM=1(ShockBurst), RFDR=1(1 Mbps), XOF=011 (16 MHz crystal), RF_PWR=11 (HIGH)
RFCH_RXEN
/* byte 14 */   1 for receive, 0 for transmit
};

The following section of code places the radio in the configuration mode:

CE_LOW();
DATA_DDR |= _BV(DATA_PINNUM); /* Set DATA pin to output */
DATA_LOW();
CLK1_LOW();
delay_125ns(); /* Td = 50 ns */
CS_HIGH();
_delay_us(5); /* Tcs2data = 5 us */

The stand by mode is used to minimize power consumption and maintain short start up times by basically powering down the radio yet maintaining the configuration word. The following code places the radio in stand by mode:

EIMSK &= ~_BV(INT4); /* Disable external interrupt for INT4 */
CS_LOW();
CE_LOW();
CLK1_LOW();
DATA_LOW();

There are two active modes: Direct Mode and Shockburst Mode (see Figure 1 below). We use the Shockburst Mode because the data is clocked at a low data rate using on-chip FIFO and transmitted at a very high rate, providing low power consumption.

Figure 1 - Clocking Data and Sending with Shockburst

The following code places the radio in active mode:

CS_LOW();
delay_125ns(); /* Td = 50 ns */
DATA_LOW();
CLK1_LOW();
CE_HIGH();
_delay_us(5); /* Tce2data = 5 us */

When in the active mode, messages can be sent or received. There are two functions in the radio driver that change the mode: radio_set_transmit() and radio_set_receive(). Both functions follow the same steps:

  1.  First place the radio in configuration mode
  2.  Set the rx_tx bit: 0 for transmit / 1 for receive
  3.  Place the radio in stand by mode
  4.  Set the direction of the DATA pin: output for transmit / input for receive
  5.  Set the external interrupt pin for INT4: 0 for transmit / 1 for receive
  6.  Place the radio in active mode
  7.  One final step for receive mode only, delay 250 µs

Configuring the Radio

The radio_init() function configures the radio. It takes three parameters: channel, address, and rx_enable.

Please see the radio.c file for the complete radio_init() code.

Sending a Message

The radio_send() function is called to transmit a radio packet. This function takes three parameters: destination address, a pointer to the character array that will populate the payload, and the number of bytes the message will occupy in the payload. The number of bytes cannot be greater than 28 bytes (PAYLOAD_BYTES). First and foremost, this function places the radio in transmit mode. It then writes the destination address followed by the desired data (payload) and then clocks it out using Shockburst transmission. Finally, the radio is placed back into the stand by mode. The complete code for the radio_send() function can been found in the radio.c file.

Stucture of Payload

The structure of payload sent is such that the first byte refers to the type of message and the remainder of the payload is the actual data. In order to read the data, both the blimp and the base station must have the same radio structure. The following is the structure used here:

typedef struct _packet
{
/* define radio packet */
uint8_t type;
// type of radio packet
union
{
struct
{
// joystick information
uint8_t X;
uint8_t Y;
uint8_t Z;
} control;
struct
{
// status of blimp
uint8_t currHeight;
uint8_t lastHeight;
} status;
char msg[PAYLOAD_BYTES-sizeof(uint8_t)];
// use for debugging only
} payload;
} packet;

Note: When using the msg part of the payload, the strncpy function must be used to save your string into msg. For instance, strncpy(p.payload.msg, "Blimp Connected", 16);. In this example, p is the radio packet, "..." is the string to be sent, and 16 is the number of characters in the string plus 1 for the null character automatically added at the end.

Receiving a Message

The radio has been connected to work with interrupts, in particular INT4. Therefore, when a radio packet is received, the DR1 pin is pulled high and an interrupt is triggered.

Interrupt Service Routine

This ISR reads the payload from the received packet, saving it into a buffer (radio_buf), and then sets the packet_available flag. Finally, the radio is placed in stand by mode so further packets cannot be received until this one has been processed.

When the packet_available flag is set, a pointer is initialized to point to the radio_buf. For example, q = ((packet*) radio_buf); (q is of type packet*). The next thing is to determine what type of message has been received (q->type). The remainder of the payload is processed depending on the type of message received. Please see the code for how the blimp and base station process packets.

The readme text file has further explanations on using the radio driver.

[Top]

Joystick

The joystick driver was written by students in this class previously. Some of the functions have been modified for ease of use and understanding.

The joystick controller uses Port F, which is a 10-bit resolution analog to digital converter (ADC). The ADC Multiplexer Select (ADMUX) register is used when reading the joystick coordinates (X, Y, and Z) in order to select the channel being sampled/read. After this, the start conversion is enabled on the ADC specific selected channel. This is done by setting Bit 7 (ADEN) and Bit 6 (ADSC) in the ADCSRA register as explained in the User Manual (s.24, p.310).

Enabling the Joystick

The following joystick_init() function is used to initialize the joystick:

/* Set up input pins */
DIDR0 = ~PORTF_ENABLE_MASK; // Disable unused ADC input pins
DDRF &= ~PORTF_ENABLE_MASK; // Set low for input
PORTF &= ~PORTF_ENABLE_MASK; // Write 0 to disable pull-up resistors

Sampling the Joystick

Eleven samples are read and averaged using the following function:

joyCoordinates avgSample()
{
joyCoordinates jC;
int i;
jC.joyX = jC.joyY = jC.joyZ = 0;
for( i = 1; i <= 11; i++)
jC.joyX = ((i - 1)*jC.joyX + single_sample(SAMP0))/i;

for( i = 1; i <= 11; i++)
jC.joyY = ((i - 1)*jC.joyY + single_sample(SAMP1))/i;

for( i = 1; i <= 11; i++)
jC.joyZ = ((i - 1)*jC.joyZ + single_sample(SAMP2))/i;

return jC;
}

Transfer Function

The joystick coordinates X and Y correspond to the speed of the left and right motors respectively. The joystick coordinate Z is used to control the target height of the blimp. Table 1 below is the lookup table used to translate the joystick coordinates X and Y to the motor speed. For now, a very simple 3x3 matrix has been used with the values hard coded based on assumptions made. In later projects, this table could be extended to cover more values for the position of the joystick in the X-Y plane. Each row and column in the table corresponds to the joystick position. For example, row 1 and column 1 corresponds to the top-left-most position of the joystick in the X-Y plane as show in Figure 2 below.

Left Duty=50
Right Duty=25

Left Duty=0
Right Duty=0

Left Duty=25
Right Duty=50

Left Duty=100
Right Duty=0

Left Duty=50
Right Duty=50

Left Duty=0
Right Duty=100

Left Duty=100
Right Duty=75

Left Duty=100
Right Duty=100

Left Duty=75
Right Duty=100

Table 1 - Joystick Coordinate Lookup Table

Figure 2 - Joystick Top-Left-Most Position in the X-Y Plane

The matrix show in Table 1 above is stored as an array. The following code computes the X and Y index in this array:

int Xindex = floor(3*joyX/256);
int Yindex = floor(3*joyY/256);
[Top]

DC Motors

A varying pulse-width modulated (PWM) signal is applied to control the speed of the left/right motors and a PID controller is used to control the speed and direction of the up/down motor.

Timers

Both Timer 0 and Timer 2 are 8-bit general purpose Timer/Counter modules with two independent Output Compare Units and PWM support. They allow accurate program execution timing (event management) and waveform generation. Each timer has the following four modes of operation:

  1. Normal mode
  2. Fast PWM (Pulse Width Modulation) mode
  3. Clear Timer on Compare Match mode
  4. Phase Correct PWM mode

The mode is selected using the WGMn2..0 bits in the TCCRnA and TCCRnB registers where n refers to the number 0 or 2. The Fast PWM mode is the mode used here for both timers.

The Fast PWM mode generates a high frequency PWM waveform, which controls the power frequency sent to the motors and in turn controls the speed. The Fast PWM mode differs from the Phase Correct PWM mode due to its single-slope operation. In Fast PWM mode, the counter counts from BOTTOM to TOP, continuously incrementing until it matches the TOP value when it is reset to start from BOTTOM again. Selecting the Fast PWM mode and initializing the timers is taken care of when the motors are initialized.

Initializing the Motors

The following motor_init() function was written to initialize all three motors:

DDRD |= (1<<PORTD1) | (1<<PORTD7) | (1<<PORTD4) | (1<<PORTD5);
DDRB |= (1<<PORTB4) | (1<<PORTB7);
// Set the direction registers for PWM signal
TCCR0A = (1<<WGM00) | (1<<WGM01) | (1<<COM0A0) | (1<<COM0A1) |
  (1<<COM0B0) | (1<<COM0B1);
TCCR0B = (0<<WGM02) | (1<<CS02) | (0<<CS01) | (0<<CS00);
// Setup Timer/Counter 0 for Fast PWM (TOP=MAX) inverting mode and clock source: prescaler 64
TCCR2A = (1<<WGM20) | (1<<WGM21) | (1<<COM2A0) | (1<<COM2A1) |
  (1<<COM2B0) | (1<<COM2B1);
TCCR2B = (0<<WGM22) | (1<<CS22) | (1<<CS21) | (0<<CS20);
// Setup Timer/Counter 0 for Fast PWM (TOP=MAX) inverting mode and clock source: prescaler 256
setLeftMotorOFF();
setRightMotorOFF();
setHeightMotorOFF();
fillLookupTable();

The fillLookupTable() function simply fills a table that maps the joystick position to the duty cycles.

Enabling the Motors

Turning the motors on/off simply involves setting/clearing the enable pin as below:

PORTD |= (1<<PORTD5); //turn the left motor ON
PORTD &= ~(1<<PORTD5);
 
//turn the left motor OFF
PORTD |= (1<<PORTD4); //turn the right motor ON
PORTD &= ~(1<<PORTD4);
 
//turn the right motor OFF
PORTD |= (1<<PORTD7); //turn the height motor ON
PORTD &= ~(1<<PORTD7); //turn the height motor OFF

Setting the Motor Speed

In the Fast PWM mode, the free TCNTn timer counts up until it reaches the OCRnA/OCRnB value. So by setting the values of these bits, the motor speed can be changed as follows:

OCR2A = (uint16_t) duty; // Set speed of left motor
OCR2B = (uint16_t) duty; // Set speed of right motor
OCR0A = (uint16_t) duty; // Set speed of height motor

Setting the Motor Direction

The direction of the left/right motors are not changed in this project. Even though this section specifically refers to setting the direction for the height motor, the same procedure could be applied to the left/right motors.

The direction of the height motor is changed by setting/clearing the COM0A0 bit in the TCCR0A register for up/down directions.

TCCR0A |= (1<<COM0A0); // Set the UP direction
TCCR0A &= ~(1<<COM0A0); // Set the DOWN direction

When COM0A0 is set to 0, OC0A is cleared on compare match and set at TOP. When COM0A0 is set to 1, OC0A is set on compare match and cleared at TOP. Thus, the input to Pin 2 of the H-Bridge changes, causing the direction of the current to change, which changes the motor direction.

[Top]

Sonar

Using interrupts signalled by the PW pin, allows us to save the time when the pulse is fired (PW pin is high) and the time when the echo is received (PW pin is low). The difference between these two time values provides a measure of the distance to an object (for instance, the floor). This distance is returned in clock cycles. To convert clock cycles to inches, we use the scale factor of 147 µs per inch. The formula reads as follows:

Time Difference / 147 = Distance (in inches)

The process of firing a pulse and receiving an echo can be done every 49 ms. However, the first time the RX pin is held high, the sensor runs a calibration cycle which takes 49 ms. The first reading, therefore, takes 100 ms. All in all, 20 readings can be taken every minute after the initial power-up period (250 ms) has elapsed.

Enabling the Sonar

Timer 3 is used to control the interrupts for the sonar. To completely enable the sonar, we first need to enable PORTC6 as output. PORTC7 is by default set to input. Next, set the clock source prescale value to 8, which sets the clock to operate at 1 MHz, simplifying our calculations and, in turn, simplifying our code. The prescale value is set using Bits 2:0 of TCCR3B (Timer/Counter 3 Control Register). Next, enable the noise canceller of TCCR3B, which filters the input from the Input Capture Pin.

DDRC |= SONAR_PULSE_MASK; // Enable output on Pin 6, port C
TCCR3B &= ~(_BV(CS32) | _BV(CS30)); // Set clock source to prescale by 8
TCCR3B |= _BV(CS31); // Set clock source to prescale by 8
TCCR3B |= _BV(ICNC3); // Enable noise cancelling

The final step to enabling the sonar is to enable interrupts. Interrupts allow us to capture when the echo pulse of the sonar is received. The time it takes to receive the echo pulse is used to calculate the distance the blimp is from the ground.

Enabling Interrupts

In order to use interrupts to capture sonar readings, we need to set the edge the interrupt will detect. SET_RISING_EDGE() sets Bit 6 of the TCCR3B register so the interrupt will be detected on a rising edge. CLEAR_IC_FLAG() clears Bit 5 of TIFR3 (Timer/Counter 3 Interrupt Flag Register). This bit is set when a capture event occurs on the Interrupt Capture Pin. By clearing the bit, an interrupt can be detected again. SET_IC_ENABLE() sets Bit 5 of the TIMSK3 (Timer/Counter 3 Interrupt Mask Register). When this bit is set, the Timer/Counter 3 Input Capture Interrupt is enabled. ENABLE_ECHO() sets Bit 7 of Port C which is where the echo pulse is received. The final step to enabling interrupts is to enable global interrupts. This is done using the sei() function. This function sets the I-Flag in the Status Register. If this flag is not set, interrupts will not be enabled regardless of whether Timer 3 interrupts have been enabled.

SET_RISING_EDGE(); // Set Bit 6 of TCCR3B
CLEAR_IC_FLAG(); // Clear Bit 5 of TIFR3
SET_IC_ENABLE(); // Set Bit 5 of TIMSK3
ENABLE_ECHO(); // Set Bit 7 of Port C - PW pin
sei(); // Enable global interrupts

Following the initilization, a 250 ms delay is necessary the first time the sonar is powered on because the MaxSonar-EZ1 is not ready to accept an RX command until 250 ms has passed.

Triggering the Sonar

In order to trigger the sonar, we first need to set Bit 6 of Port C (ENABLE_PULSE()), which is connected to the RX pin on the sonar. As stated above, setting the RX pin to high will fire the pulse. We have already seen the remaining commands: SET_RISING_EDGE(), CLEAR_IC_FLAG(), and SET_IC_ENABLE(). This is the complete set of commands/steps required to trigger the sonar.

ENABLE_PULSE(); // Set Bit 6 of Port C - RW pin
SET_RISING_EDGE(); // Set Bit 6 of TCCR3B
CLEAR_IC_FLAG(); // Clear Bit 5 of TIFR3
SET_IC_ENABLE(); // Set Bit 5 of TIMSK3

Interrupt Service Routine

Once the sonar is triggered, an interrupt is captured when the rising edge of Port C7 is detected, which is the moment the pulse is finished firing. This input capture sets the Input Capture Flag (Pin ICF3) in TIFR3 and calls the Interrupt Service Routine (ISR). The first step of the ISR is to disable the pulse signal; otherwise, the sonar will continuously fire pulses, which will interfere with reading the pulse we are interested in. The next step of the ISR is to check which edge was detected. If a rising edge is detected, the ISR resets the value of TCNT3 (the free-running counter for Timer 3) to 0 signifying this is when the pulse is first fired. Then, we set the edge to be detected next to a falling edge, which signifies the echo pulse has been received, and finally, we clear the Interrupt Capture flag so the next interrupt can be detected. When a falling edge is detected, the ISR captures the time value of TCNT3 (stored in ICR3 when an interrupt is detected) into a variable called time_falling. This variable is used when reading the sonar. Finally, after the time count has been saved, we clear the Input Capture Flag.


ISR(TIMER3_CAPT_vect)
{

DISABLE_PULSE();
// Read one pulse at a time
if (IS_RISING_EDGE())
{

TCNT3=0;
// Reset timer counter for start of pulse
SET_FALLING_EDGE();

CLEAR_IC_FLAG();

}
else
{

time_falling = ICR3;
// ICR3 = TCNT3 when interrupt detected
SET_RISING_EDGE();

CLEAR_IC_FLAG();

}

}

As soon as the ISR has been executed, command is transferred back to the main block of our program, which will resume where it left off when the interrupt was received. Then, when an interrupt is detected, the ISR will again take command.

Reading the Sonar

To read the sonar, we simply take the value stored in the variable time_falling and divide it by the pulse width to return the value in inches rather than clock ticks. The conversion for pulse width is 147 µs/inch. The converted value then gives us the current height of the blimp.

uint8_t read_distance()
{

return (time_falling/US_PER_INCH);
// Pulse width = 147 us/inch
}
[Top]

Hardware Timer

For this project, we used the AVR Studio libraries © Atmel Inc. The AVR library package provides a subset of the standard C library for Atmel AVR 8-bit RISC microcontrollers. The timer function provided by the AVR Studio library does not provide an exact or precise time delay. A precise time delay at various places in our project was required so we implemented our own hardware timer delay function of 5 ms. In the following paragraphs, the implementation of the time delay function is described.

Timer/Counter 1 on the AT90 board is used. This timer is 16 bit, which means 65536 iterations. Normal mode operation is used for this counter, which is the simplest mode of operation. In this mode, the counter direction is always up. The timer simply overruns when it passes its maximum 16-bit (MAX = 0xFFFF) value and then restarts from the BOTTOM (0x0000). The Output Compare Unit of this timer is used in non-PWM mode to generate an interrupt after every TICK, which can be set by the user (see comments in the source code). TCCR1A and TCCR1B registers are used to set up the timer in this mode (see Initializing Hardware Timer below). The Timer/Counter 1 interrupt mask register, TIMSK1, controls the interrupts generated by the Timer/Counter. When the Timer/Counter 1 Output Compare A Match Interrupt Enable, OCIE1A, is written to one and the I-flag in the Status Register is set (interrupts globally enabled), the Timer/Counter 1 Output Compare A Match interrupt is enabled.

Initializing Hardware Timer

void timerInit()
{

TCCR1A = 0x0;
TCCR1B = 0x1;
// Set Timer/Counter 1 for Normal Mode - no prescale factor
TIMSK1 = (0<<ICIE1) | (0<<OCIE1C) | (0<<OCIE1B) |
  (1<<OCIE1A) | (0<<TOIE1);
// Set Timer 1 Output Compare interrupt, the TICK clock
TIFR1 = (0<<ICF1) | (0<<OCF1C) | (0<<OCF1B) |
  (0<<OCF1A) | (0<<TOV1);
// Clear flag
OCR1A = TCNT1 + TICK_CYCLES;

tick_count = 0;

}

Interrupt Service Routine

The corresponding Interrupt Vector (TIMER1_COMPA_vect defined in the AVR library) is executed when the OCF1A Flag, located in TIFR1, is set. The ISR implemented increments the tick_count and updates the OCR1A register with the TICK_CYCLES value as defined for the next tick.

ISR(TIMER1_COMPA_vect)
{

OCR1A += TICK_CYCLES;
// Prepare for next tick interrupt.
tick_count++;

}

Example using Hardware Timer to delay 50&nbspms

timerInit(); // An example using timer function to delay for 50 ms
int time = currentTime();
for (;;) // Time delay of 50 ms
if ((currentTime() - time) >= 10)
break;



uint16_t currentTime()
{
uint8_t flags;
uint16_t t;
flags = SREG;
Disable_Interrupt();
t = tick_count;
SREG = flags;
return t;

}
// This function returns tick_count variable as the current time
[Top]