Interfacing an LCD12864 (ST7920 controller) to a microcontroller

I am currently planning a new all band transceiver that might be a little larger than the “Microtransceivers” I built this  year. For this project I bought one of those larger graphic display modules that are sold from incountable Chinese vendors:

LCD-12864-ST7920
LCD-12864-ST7920

Unfortunately I did not find any “non-Arduino” modules on the web to meet my requirements. So I decided to write my own code (mainly as an academic exercise 😉 ) in standard C language for the AVR controller family. You will find this source code by the end of this paper.

General aspects to know

What to do first: RTFM! Data sheets for this module are widely available and I strongly recommend reading one of them. The problem is: You can get ones that don’t cover even the minimum you must know to get the thing working. Other are more suitable (Example). Read them before you start! At least once and by skimming. I will only refer to the things that I think are not clear in the data sheets or are controversial between the various versions of the papers.

Hardware: These LCD modules have 2 controllers of the ST7920-type inside. The screen is 128 pixels wide and 64 pixels high. The modules have built-in ROM-based standard character sets like they are familiar from the well-known line oriented modules like the 16×2 ones. But there is also the possibility to drive them in full graphics mode with your own fonts to be used.

The LCD module communicates via 8- or 4-bit parallel mode or a serial “SPI” derivate.

Connectors

The LCD12864 is wired to the rest of the circuit by a 20-pin header strip in 2.54mm  (0.1″) spacing. The pins are

  • VSS (0V or “minus”)
  • VDD (2.7 to 5.5V, “plus”)
  • VO (a contrast set but without function with my module)
  • RS (determining if there is data (pin to VDD i. e. “hi”) or an instruction (pin to VSS i. e. “low”) transferred)
  • RW (“low” when writing data, “high” when reading data from the module)
  • E (the “enabled” pin that goes high when a byte of data is transferred)
  • D0:D7: The parallel data lines
  • PSB (set “low” if you project serial communication or “high” if you project parallel communication (4- or 8 bit bus width  possible))
  • NC: no connection
  • RST: The reset pin, must be pulled to GND for 1 ms or so and then set to high to reset the module when program starts
  • VOUT: A reference voltage but not used in my project
  • BLA: Backlight, connected to +12 V via R=270Ohm
  • BLK: Backlight cathode, connected to GND

If you want to run my software, connect the microcontroller to the LCD module as follows:

  • LCD-Data: PD0..PD7
  • RS: PC0
  • RW: PC1
  • E: PC2
  • RST PC3
  • PSB: GND
  • BLA to +12V via R=270 Ohms
  • BLK to GND

Driving the module by software

In my software I use 8 bit parallel transfer because it is the fastest way to get the data displayed. I use the full PORTD of the microcontroller for this purpose. In general this LCD module is something for the “bigger” controllers if you intend to do something more than just displaying funny messages. 😉

Initializing

Before the first data can be displayed the module must be reset and subsequently initialized:

//Init LCD
void lcd_init(void)
{ 
    //Reset
    PORTC &= ~(8);
    _delay_ms(1);
    PORTC |= 8;
    _delay_ms(40);

    lcd_write(0, 0x30); //Use 8-bit mode parallel
    _delay_ms(1);

    lcd_write(0, 0x0C); //All on Cursor on, Blink on , Display on
    _delay_ms(1);

    lcd_write(0, 0x01); //Perform CLS in text mode to eliminate random chars from screen
    _delay_ms(20);

    lcd_write(0, 0x34); //Switch to extended mode, redefine function set
    _delay_us(100);

    lcd_write(0, 0x36); //Add graphic mode
    _delay_us(100);

    lcd_write(0, 0x12); //Display control and display ON
    _delay_us(100);
}

I found that getting the module ready for work correctly is not easy. I encountered the problem that I still had some random characters of the text mode on my graphics screen after having switched the LCD to graphics mode. To solve this problem I did the initialization procedure in the following manner:

  1. Set 8-Bit parallel mode first,
  2. Switch the module to a standard text module behavior,
  3. Clear the screen,
  4. Switch the module to extended and graphics mode and perform the remaining initialization process.
  5. Clear screen once again and you are “ready for take off”.

After you have switched to the “Extended instruction set” you can access the pixels of the module individually.

How to set and reset pixels in graphics mode

The data sheets I browsed through concerning this aspect were not concise. Following is the correct addressing mode for my module:

GDRAM (Graphics RAM) organisation of LCD12864 (ST7920)
GDRAM (Graphics RAM) organisation of LCD12864 (ST7920)

You can see 16 banks each 16 pixels wide and 32 pixels high. 16 pixels are treated as one integer, consisting of MSB (bit 15:8) and LSB (bit 7:0). They are read from the left side (MSB) to the right end (LSB).

If you want to set a 16 bit section, you must specify the graphics address of the line you want to set. This address is stored in the so-called “GDRAM” (graphics data ram).

X coordinate is the bank starting with no. 0 to no. 15 max. Y coordinate is a number between 0:31 referring to the respective line that shall be set.

Hint: If you are about to access the lower part of the screen, you must use a “bank” numbered >= 8. To prevent the screen from looking “broken” it is necessary to set row back to a value diminished by 0x20 (32 dec.) and the “bank” increased by the value of 0x08 (8 dec.).

if(row & 0x20) //Enter lower part of screen => go to next page bank
{
    row &= ~0x20;
    col |= 8;
}

After having written an address to the module the address counter automatically increases by 1 for the horizontal part of the address. The vertical address remains unchanged. This increment for the X direction can be compensated by defining the specified GDRAM address each time you project to set or reset a 16-bit line of data.

You will totally have to transfer 4 bytes to set one line of 16 pixels:

1. Set vertical address(Y) for GDRAM
2. Set horizontal address(X) for GDRAM
3. Write D15:D8 to GDRAM (first byte)
4. Write D7:D0 to GDRAM (second byte)

(excerpt from data sheet)

Setting GDRAM address

Before you can write data to a GDRAM cell you must state where this data shall be displayed. Therefore you specify the GDRAM (here “DDRAM”) address:

LCD12864_set_CDRAM_adr

DB7 is set to one, so 80H (0x80) has to be added to the address value for each writing.

Here is the software code for this procedure:

//Set address
lcd_write(0, 0x80 + row);  //”0″ means “instruction”
lcd_write(0, 0x80 + col);
lcd_write(1, msb); //”1″ means “data”
lcd_write(1, lsb);

Hint: If you use a font that is only 8 bits wide you will have to cope with the following problem: When writing one character (8 bits wide) you will override the other 8 bits that might already be on the screen because you can only address 16 bits at once. So, before you write a character to the LCD, you have to read the GDRAM for the other half of the character and store this value. Later you assemble both characters (old one and new one) to a complete 16-bit value and write this data back to the LCD. You also have to find out if the new char is the left or the right one of a 16-bit cell.

This works out as:

//Set address
lcd_write(0, 0x80 + row + t1);
lcd_write(0, 0x80 + col);

//Get old values of 2 GDRAM bytes 
v1 = lcd_read(1); //Dummy read required!
v1 = lcd_read(1);
v2 = lcd_read(1);

//Set address
lcd_write(0, 0x80 + row);
lcd_write(0, 0x80 + col);

if(!inv) //Char normal or inverted?
{
    ch = font[ch0 * FONTHEIGHT + t1];
}
else 
{
     ch = ~font[ch0 * FONTHEIGHT + t1];
}

if(odd) //"Odd" or "even" position in 16 bit integer?
{ 
    //Write data on RIGHT side of existing character
    lcd_write(1, v1);
    lcd_write(1, ch);
}
else 
{ 
    //Write data on LEFT side of existing character
    lcd_write(1, ch);
    lcd_write(1, v2);
}

This only prints out one line of the char, please see the full code attached to this article to get the full information for printing the full character to the LCD screen!

How to use my software

I always put all of my code into one C-file because I cannot post ZIP-files with header-files etc. here. Sorry for that!

There are various functions that you can use to write text or data to the screen:

void lcd_putchar(int, int, unsigned char, int);
void lcd_putchar2(int, int, unsigned char, int);
void lcd_putchar3(int, int, unsigned char, int);

  • lcd_putchar() prints a character defined in the font in normal size. If you want to invert the character looking, set the last “int” to 1.
  • lcd_putchar2() produces the same character in double height.
  • lcd_putchar3() set a character in double height and double width.

void lcd_putstring_a(int, int, char*, int, int);
void lcd_putstring_b(int, int, char*, int);

  • lcd_putstring_a() prints a “0”-terminated string to a given position (row, col, string, height (0==normal, 1==double) and inverted.
  • lcd_putstring_b() does the same in double height and double width.

First three parameters are always column, row and data. Following additional information about size, character inverted printing etc. You will find that in the code by the end of this paper.

The most complex function is

  • void lcd_putnumber(int, int, long, int, int, int);

which converts a number to a string and subsequently displays it. Parameters are

void lcd_putnumber(col, row, number, decimal, size, invert);

“Number” can be a long variable or an integer or even char. “Decimal” sets the decimal separator (counted from the right) if wanted, if not set this parameter to “-1”, “Size” is 0 for normal and 1 for double height. “Invert” = 1 for inverted and 0 for normal appearance.

The full software

I apologize for the ugly looking code but the web based “beautifier” and highlighter destroyed my indenting to a max. ! 😦

So, if there are still questions, feel free to mail me: peter(dot)rachow(ät)web(dot)de!

73 de Peter

/*****************************************************************/
/*           LCD12864-ST7920-Demo with ATMega32                  */
/*  ************************************************************ */
/*  Mikrocontroller:  ATMEL AVR ATmega32, 8 MHz                  */
/*                                                               */
/*  Compiler:         GCC (GNU AVR C-Compiler)                   */
/*  Author:           Peter Rachow (DK7IH)                       */
/*  Letzte Aenderung: 2018-12-25                                 */
/*****************************************************************/

// O U T P U T for LCD 

//Connection LCD to uC:
//LCD-Data: PD0..PD7
//RS:       PC0
//RW:       PC1
//E:        PC2
//RST       PC3

#define F_CPU 8000000
#define FONTHEIGHT 8

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/eeprom.h>
#include <util/delay.h>

int main(void);

void lcd_write(char, unsigned char);
char lcd_read(char);
void set_rs(char);
void set_e(char);
void set_rw(char);
int is_lcd_busy(void);
void lcd_init(void);
void lcd_cls(void);
void lcd_putchar(int, int, unsigned char, int);
void lcd_putchar2(int, int, unsigned char, int);
void lcd_putchar3(int, int, unsigned char, int);
void lcd_putstring_a(int, int, char*, int, int);
void lcd_putstring_b(int, int, char*, int);
void lcd_putnumber(int, int, long, int, int, int);

//STRING FUNCTIONS
int int2asc(long, int, char*, int);

//Font for graphics LCD 5x8
unsigned char font[] =
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	// 0x00
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	// 0x01
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	// 0x02
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	// 0x03
0x00,0x04,0x0E,0x1F,0x1F,0x0E,0x04,0x00,	// 0x04
0x04,0x0E,0x0E,0x04,0x1F,0x1F,0x04,0x00,	// 0x05
0x00,0x04,0x0E,0x1F,0x1F,0x04,0x0E,0x00,	// 0x06
0x0E,0x1F,0x15,0x1F,0x11,0x1F,0x0E,0x00,	// 0x07
0x0E,0x11,0x1B,0x11,0x15,0x11,0x0E,0x00,	// 0x08
0x00,0x0A,0x1F,0x1F,0x1F,0x0E,0x04,0x00,	// 0x09
0x0E,0x11,0x1B,0x11,0x15,0x11,0x0E,0x00,	// 0x0A
0x00,0x07,0x03,0x0D,0x12,0x12,0x0C,0x00,	// 0x0B
0x0E,0x11,0x11,0x0E,0x04,0x0E,0x04,0x00,	// 0x0C
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	// 0x0D
0x03,0x0D,0x0B,0x0D,0x0B,0x1B,0x18,0x00,	// 0x0E
0x00,0x15,0x0E,0x1B,0x0E,0x15,0x00,0x00,	// 0x0F
0x08,0x0C,0x0E,0x0F,0x0E,0x0C,0x08,0x00,	// 0x10
0x02,0x06,0x0E,0x1E,0x0E,0x06,0x02,0x00,	// 0x11
0x04,0x0E,0x1F,0x04,0x1F,0x0E,0x04,0x00,	// 0x12
0x0A,0x0A,0x0A,0x0A,0x0A,0x00,0x0A,0x00,	// 0x13
0x0F,0x15,0x15,0x0D,0x05,0x05,0x05,0x00,	// 0x14
0x0E,0x11,0x0C,0x0A,0x06,0x11,0x0E,0x00,	// 0x15
0x00,0x00,0x00,0x00,0x00,0x1E,0x1E,0x00,	// 0x16
0x04,0x0E,0x1F,0x04,0x1F,0x0E,0x04,0x0E,	// 0x17
0x04,0x0E,0x1F,0x04,0x04,0x04,0x04,0x00,	// 0x18
0x04,0x04,0x04,0x04,0x1F,0x0E,0x04,0x00,	// 0x19
0x00,0x04,0x06,0x1F,0x06,0x04,0x00,0x00,	// 0x1A
0x00,0x04,0x0C,0x1F,0x0C,0x04,0x00,0x00,	// 0x1B
0x00,0x00,0x00,0x10,0x10,0x10,0x1F,0x00,	// 0x1C
0x00,0x0A,0x0A,0x1F,0x0A,0x0A,0x00,0x00,	// 0x1D
0x04,0x04,0x0E,0x0E,0x1F,0x1F,0x00,0x00,	// 0x1E
0x1F,0x1F,0x0E,0x0E,0x04,0x04,0x00,0x00,	// 0x1F
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	// 0x20
0x04,0x0E,0x0E,0x04,0x04,0x00,0x04,0x00,	// 0x21
0x1B,0x1B,0x12,0x00,0x00,0x00,0x00,0x00,	// 0x22
0x00,0x0A,0x1F,0x0A,0x0A,0x1F,0x0A,0x00,	// 0x23
0x08,0x0E,0x10,0x0C,0x02,0x1C,0x04,0x00,	// 0x24
0x19,0x19,0x02,0x04,0x08,0x13,0x13,0x00,	// 0x25
0x08,0x14,0x14,0x08,0x15,0x12,0x0D,0x00,	// 0x26
0x0C,0x0C,0x08,0x00,0x00,0x00,0x00,0x00,	// 0x27
0x04,0x08,0x08,0x08,0x08,0x08,0x04,0x00,	// 0x28
0x08,0x04,0x04,0x04,0x04,0x04,0x08,0x00,	// 0x29
0x00,0x0A,0x0E,0x1F,0x0E,0x0A,0x00,0x00,	// 0x2A
0x00,0x04,0x04,0x1F,0x04,0x04,0x00,0x00,	// 0x2B
0x00,0x00,0x00,0x00,0x00,0x0C,0x0C,0x08,	// 0x2C
0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00,	// 0x2D
0x00,0x00,0x00,0x00,0x00,0x0C,0x0C,0x00,	// 0x2E
0x00,0x01,0x02,0x04,0x08,0x10,0x00,0x00,	// 0x2F
0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00,	// 0x30
0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00,	// 0x31
0x0E,0x11,0x01,0x06,0x08,0x10,0x1F,0x00,	// 0x32
0x0E,0x11,0x01,0x0E,0x01,0x11,0x0E,0x00,	// 0x33
0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00,	// 0x34
0x1F,0x10,0x10,0x1E,0x01,0x11,0x0E,0x00,	// 0x35
0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00,	// 0x36
0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00,	// 0x37
0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00,	// 0x38
0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00,	// 0x39
0x00,0x00,0x0C,0x0C,0x00,0x0C,0x0C,0x00,	// 0x3A
0x00,0x00,0x0C,0x0C,0x00,0x0C,0x0C,0x08,	// 0x3B
0x02,0x04,0x08,0x10,0x08,0x04,0x02,0x00,	// 0x3C
0x00,0x00,0x1F,0x00,0x00,0x1F,0x00,0x00,	// 0x3D
0x08,0x04,0x02,0x01,0x02,0x04,0x08,0x00,	// 0x3E
0x0E,0x11,0x01,0x06,0x04,0x00,0x04,0x00,	// 0x3F
0x0E,0x11,0x17,0x15,0x17,0x10,0x0E,0x00,	// 0x40
0x0E,0x11,0x11,0x11,0x1F,0x11,0x11,0x00,	// 0x41
0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E,0x00,	// 0x42
0x0E,0x11,0x10,0x10,0x10,0x11,0x0E,0x00,	// 0x43
0x1E,0x11,0x11,0x11,0x11,0x11,0x1E,0x00,	// 0x44
0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F,0x00,	// 0x45
0x1F,0x10,0x10,0x1E,0x10,0x10,0x10,0x00,	// 0x46
0x0E,0x11,0x10,0x17,0x11,0x11,0x0F,0x00,	// 0x47
0x11,0x11,0x11,0x1F,0x11,0x11,0x11,0x00,	// 0x48
0x0E,0x04,0x04,0x04,0x04,0x04,0x0E,0x00,	// 0x49
0x01,0x01,0x01,0x01,0x11,0x11,0x0E,0x00,	// 0x4A
0x11,0x12,0x14,0x18,0x14,0x12,0x11,0x00,	// 0x4B
0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x00,	// 0x4C
0x11,0x1B,0x15,0x11,0x11,0x11,0x11,0x00,	// 0x4D
0x11,0x19,0x15,0x13,0x11,0x11,0x11,0x00,	// 0x4E
0x0E,0x11,0x11,0x11,0x11,0x11,0x0E,0x00,	// 0x4F
0x1E,0x11,0x11,0x1E,0x10,0x10,0x10,0x00,	// 0x50
0x0E,0x11,0x11,0x11,0x15,0x12,0x0D,0x00,	// 0x51
0x1E,0x11,0x11,0x1E,0x12,0x11,0x11,0x00,	// 0x52
0x0E,0x11,0x10,0x0E,0x01,0x11,0x0E,0x00,	// 0x53
0x1F,0x04,0x04,0x04,0x04,0x04,0x04,0x00,	// 0x54
0x11,0x11,0x11,0x11,0x11,0x11,0x0E,0x00,	// 0x55
0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00,	// 0x56
0x11,0x11,0x15,0x15,0x15,0x15,0x0A,0x00,	// 0x57
0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00,	// 0x58
0x11,0x11,0x11,0x0A,0x04,0x04,0x04,0x00,	// 0x59
0x1E,0x02,0x04,0x08,0x10,0x10,0x1E,0x00,	// 0x5A
0x0E,0x08,0x08,0x08,0x08,0x08,0x0E,0x00,	// 0x5B
0x00,0x10,0x08,0x04,0x02,0x01,0x00,0x00,	// 0x5C
0x0E,0x02,0x02,0x02,0x02,0x02,0x0E,0x00,	// 0x5D
0x04,0x0A,0x11,0x00,0x00,0x00,0x00,0x00,	// 0x5E
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,	// 0x5F
0x0C,0x0C,0x04,0x00,0x00,0x00,0x00,0x00,	// 0x60
0x00,0x00,0x0E,0x01,0x0F,0x11,0x0F,0x00,	// 0x61
0x10,0x10,0x1E,0x11,0x11,0x11,0x1E,0x00,	// 0x62
0x00,0x00,0x0E,0x11,0x10,0x11,0x0E,0x00,	// 0x63
0x01,0x01,0x0F,0x11,0x11,0x11,0x0F,0x00,	// 0x64
0x00,0x00,0x0E,0x11,0x1E,0x10,0x0E,0x00,	// 0x65
0x06,0x08,0x08,0x1E,0x08,0x08,0x08,0x00,	// 0x66
0x00,0x00,0x0F,0x11,0x11,0x0F,0x01,0x0E,	// 0x67
0x10,0x10,0x1C,0x12,0x12,0x12,0x12,0x00,	// 0x68
0x04,0x00,0x04,0x04,0x04,0x04,0x06,0x00,	// 0x69
0x02,0x00,0x06,0x02,0x02,0x02,0x12,0x0C,	// 0x6A
0x10,0x10,0x12,0x14,0x18,0x14,0x12,0x00,	// 0x6B
0x04,0x04,0x04,0x04,0x04,0x04,0x06,0x00,	// 0x6C
0x00,0x00,0x1A,0x15,0x15,0x11,0x11,0x00,	// 0x6D
0x00,0x00,0x1C,0x12,0x12,0x12,0x12,0x00,	// 0x6E
0x00,0x00,0x0E,0x11,0x11,0x11,0x0E,0x00,	// 0x6F
0x00,0x00,0x1E,0x11,0x11,0x11,0x1E,0x10,	// 0x70
0x00,0x00,0x0F,0x11,0x11,0x11,0x0F,0x01,	// 0x71
0x00,0x00,0x16,0x09,0x08,0x08,0x1C,0x00,	// 0x72
0x00,0x00,0x0E,0x10,0x0E,0x01,0x0E,0x00,	// 0x73
0x00,0x08,0x1E,0x08,0x08,0x0A,0x04,0x00,	// 0x74
0x00,0x00,0x12,0x12,0x12,0x16,0x0A,0x00,	// 0x75
0x00,0x00,0x11,0x11,0x11,0x0A,0x04,0x00,	// 0x76
0x00,0x00,0x11,0x11,0x15,0x1F,0x0A,0x00,	// 0x77
0x00,0x00,0x12,0x12,0x0C,0x12,0x12,0x00,	// 0x78
0x00,0x00,0x12,0x12,0x12,0x0E,0x04,0x18,	// 0x79
0x00,0x00,0x1E,0x02,0x0C,0x10,0x1E,0x00,	// 0x7A
0x06,0x08,0x08,0x18,0x08,0x08,0x06,0x00,	// 0x7B
0x04,0x04,0x04,0x00,0x04,0x04,0x04,0x00,	// 0x7C
0x0C,0x02,0x02,0x03,0x02,0x02,0x0C,0x00,	// 0x7D
0x0A,0x14,0x00,0x00,0x00,0x00,0x00,0x00,	// 0x7E
0x04,0x0E,0x1B,0x11,0x11,0x1F,0x00,0x00,	// 0x7F
0x0E,0x11,0x10,0x10,0x11,0x0E,0x04,0x0C,	// 0x80
/* 

//If you operate a microcontroller with more memory space 
//than an ATmega32 you can also use the following 127 characters!

0x12,0x00,0x12,0x12,0x12,0x16,0x0A,0x00,	// 0x81
0x03,0x00,0x0E,0x11,0x1E,0x10,0x0E,0x00,	// 0x82
0x0E,0x00,0x0E,0x01,0x0F,0x11,0x0F,0x00,	// 0x83
0x0A,0x00,0x0E,0x01,0x0F,0x11,0x0F,0x00,	// 0x84
0x0C,0x00,0x0E,0x01,0x0F,0x11,0x0F,0x00,	// 0x85
0x0E,0x0A,0x0E,0x01,0x0F,0x11,0x0F,0x00,	// 0x86
0x00,0x0E,0x11,0x10,0x11,0x0E,0x04,0x0C,	// 0x87
0x0E,0x00,0x0E,0x11,0x1E,0x10,0x0E,0x00,	// 0x88
0x0A,0x00,0x0E,0x11,0x1E,0x10,0x0E,0x00,	// 0x89
0x0C,0x00,0x0E,0x11,0x1E,0x10,0x0E,0x00,	// 0x8A
0x0A,0x00,0x04,0x04,0x04,0x04,0x06,0x00,	// 0x8B
0x0E,0x00,0x04,0x04,0x04,0x04,0x06,0x00,	// 0x8C
0x08,0x00,0x04,0x04,0x04,0x04,0x06,0x00,	// 0x8D
0x0A,0x00,0x04,0x0A,0x11,0x1F,0x11,0x00,	// 0x8E
0x0E,0x0A,0x0E,0x1B,0x11,0x1F,0x11,0x00,	// 0x8F
0x03,0x00,0x1F,0x10,0x1E,0x10,0x1F,0x00,	// 0x90
0x00,0x00,0x1E,0x05,0x1F,0x14,0x0F,0x00,	// 0x91
0x0F,0x14,0x14,0x1F,0x14,0x14,0x17,0x00,	// 0x92
0x0E,0x00,0x0C,0x12,0x12,0x12,0x0C,0x00,	// 0x93
0x0A,0x00,0x0C,0x12,0x12,0x12,0x0C,0x00,	// 0x94
0x18,0x00,0x0C,0x12,0x12,0x12,0x0C,0x00,	// 0x95
0x0E,0x00,0x12,0x12,0x12,0x16,0x0A,0x00,	// 0x96
0x18,0x00,0x12,0x12,0x12,0x16,0x0A,0x00,	// 0x97
0x0A,0x00,0x12,0x12,0x12,0x0E,0x04,0x18,	// 0x98
0x12,0x0C,0x12,0x12,0x12,0x12,0x0C,0x00,	// 0x99
0x0A,0x00,0x12,0x12,0x12,0x12,0x0C,0x00,	// 0x9A
0x00,0x00,0x01,0x0E,0x16,0x1A,0x1C,0x20,	// 0x9B
0x06,0x09,0x08,0x1E,0x08,0x09,0x17,0x00,	// 0x9C
0x0F,0x13,0x15,0x15,0x15,0x19,0x1E,0x00,	// 0x9D
0x00,0x11,0x0A,0x04,0x0A,0x11,0x00,0x00,	// 0x9E
0x02,0x05,0x04,0x0E,0x04,0x04,0x14,0x08,	// 0x9F
0x06,0x00,0x0E,0x01,0x0F,0x11,0x0F,0x00,	// 0xA0
0x06,0x00,0x04,0x04,0x04,0x04,0x06,0x00,	// 0xA1
0x06,0x00,0x0C,0x12,0x12,0x12,0x0C,0x00,	// 0xA2
0x06,0x00,0x12,0x12,0x12,0x16,0x0A,0x00,	// 0xA3
0x0A,0x14,0x00,0x1C,0x12,0x12,0x12,0x00,	// 0xA4
0x0A,0x14,0x00,0x12,0x1A,0x16,0x12,0x00,	// 0xA5
0x0E,0x01,0x0F,0x11,0x0F,0x00,0x0F,0x00,	// 0xA6
0x0C,0x12,0x12,0x12,0x0C,0x00,0x1E,0x00,	// 0xA7
0x04,0x00,0x04,0x0C,0x10,0x11,0x0E,0x00,	// 0xA8
0x1E,0x25,0x2B,0x2D,0x2B,0x21,0x1E,0x00,	// 0xA9
0x00,0x00,0x3F,0x01,0x01,0x00,0x00,0x00,	// 0xAA
0x10,0x12,0x14,0x0E,0x11,0x02,0x07,0x00,	// 0xAB
0x10,0x12,0x14,0x0B,0x15,0x07,0x01,0x00,	// 0xAC
0x04,0x00,0x04,0x04,0x0E,0x0E,0x04,0x00,	// 0xAD
0x00,0x00,0x09,0x12,0x09,0x00,0x00,0x00,	// 0xAE
0x00,0x00,0x12,0x09,0x12,0x00,0x00,0x00,	// 0xAF
0x15,0x00,0x2A,0x00,0x15,0x00,0x2A,0x00,	// 0xB0
0x15,0x2A,0x15,0x2A,0x15,0x2A,0x15,0x2A,	// 0xB1
0x2A,0x3F,0x15,0x3F,0x2A,0x3F,0x15,0x3F,	// 0xB2
0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,	// 0xB3
0x04,0x04,0x04,0x3C,0x04,0x04,0x04,0x04,	// 0xB4
0x06,0x00,0x04,0x0A,0x11,0x1F,0x11,0x00,	// 0xB5
0x0E,0x00,0x04,0x0A,0x11,0x1F,0x11,0x00,	// 0xB6
0x0C,0x00,0x04,0x0A,0x11,0x1F,0x11,0x00,	// 0xB7
0x1E,0x21,0x2D,0x29,0x2D,0x21,0x1E,0x00,	// 0xB8
0x14,0x34,0x04,0x34,0x14,0x14,0x14,0x14,	// 0xB9
0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x14,	// 0xBA
0x00,0x3C,0x04,0x34,0x14,0x14,0x14,0x14,	// 0xBB
0x14,0x34,0x04,0x3C,0x00,0x00,0x00,0x00,	// 0xBC
0x00,0x04,0x0E,0x10,0x10,0x0E,0x04,0x00,	// 0xBD
0x11,0x0A,0x04,0x1F,0x04,0x1F,0x04,0x00,	// 0xBE
0x00,0x00,0x00,0x3C,0x04,0x04,0x04,0x04,	// 0xBF
0x04,0x04,0x04,0x07,0x00,0x00,0x00,0x00,	// 0xC0
0x04,0x04,0x04,0x3F,0x00,0x00,0x00,0x00,	// 0xC1
0x00,0x00,0x00,0x3F,0x04,0x04,0x04,0x04,	// 0xC2
0x04,0x04,0x04,0x07,0x04,0x04,0x04,0x04,	// 0xC3
0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00,	// 0xC4
0x04,0x04,0x04,0x3F,0x04,0x04,0x04,0x04,	// 0xC5
0x05,0x0A,0x0E,0x01,0x0F,0x11,0x0F,0x00,	// 0xC6
0x05,0x0A,0x04,0x0A,0x11,0x1F,0x11,0x00,	// 0xC7
0x14,0x17,0x10,0x1F,0x00,0x00,0x00,0x00,	// 0xC8
0x00,0x1F,0x10,0x17,0x14,0x14,0x14,0x14,	// 0xC9
0x14,0x37,0x00,0x3F,0x00,0x00,0x00,0x00,	// 0xCA
0x00,0x3F,0x00,0x37,0x14,0x14,0x14,0x14,	// 0xCB
0x14,0x17,0x10,0x17,0x14,0x14,0x14,0x14,	// 0xCC
0x00,0x3F,0x00,0x3F,0x00,0x00,0x00,0x00,	// 0xCD
0x14,0x37,0x00,0x37,0x14,0x14,0x14,0x14,	// 0xCE
0x11,0x0E,0x11,0x11,0x11,0x0E,0x11,0x00,	// 0xCF
0x0C,0x10,0x08,0x04,0x0E,0x12,0x0C,0x00,	// 0xD0
0x0E,0x09,0x09,0x1D,0x09,0x09,0x0E,0x00,	// 0xD1
0x0E,0x00,0x1F,0x10,0x1E,0x10,0x1F,0x00,	// 0xD2
0x0A,0x00,0x1F,0x10,0x1E,0x10,0x1F,0x00,	// 0xD3
0x0C,0x00,0x1F,0x10,0x1E,0x10,0x1F,0x00,	// 0xD4
0x04,0x04,0x04,0x00,0x00,0x00,0x00,0x00,	// 0xD5
0x06,0x00,0x0E,0x04,0x04,0x04,0x0E,0x00,	// 0xD6
0x0E,0x00,0x0E,0x04,0x04,0x04,0x0E,0x00,	// 0xD7
0x0A,0x00,0x0E,0x04,0x04,0x04,0x0E,0x00,	// 0xD8
0x04,0x04,0x04,0x3C,0x00,0x00,0x00,0x00,	// 0xD9
0x00,0x00,0x00,0x07,0x04,0x04,0x04,0x04,	// 0xDA
0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,	// 0xDB
0x00,0x00,0x00,0x00,0x3F,0x3F,0x3F,0x3F,	// 0xDC
0x04,0x04,0x04,0x00,0x04,0x04,0x04,0x00,	// 0xDD
0x0C,0x00,0x0E,0x04,0x04,0x04,0x0E,0x00,	// 0xDE
0x3F,0x3F,0x3F,0x3F,0x00,0x00,0x00,0x00,	// 0xDF
0x06,0x0C,0x12,0x12,0x12,0x12,0x0C,0x00,	// 0xE0
0x00,0x1C,0x12,0x1C,0x12,0x12,0x1C,0x10,	// 0xE1
0x0E,0x0C,0x12,0x12,0x12,0x12,0x0C,0x00,	// 0xE2
0x18,0x0C,0x12,0x12,0x12,0x12,0x0C,0x00,	// 0xE3
0x0A,0x14,0x00,0x0C,0x12,0x12,0x0C,0x00,	// 0xE4
0x0A,0x14,0x0C,0x12,0x12,0x12,0x0C,0x00,	// 0xE5
0x00,0x00,0x12,0x12,0x12,0x1C,0x10,0x10,	// 0xE6
0x00,0x18,0x10,0x1C,0x12,0x1C,0x10,0x18,	// 0xE7
0x18,0x10,0x1C,0x12,0x12,0x1C,0x10,0x18,	// 0xE8
0x06,0x00,0x12,0x12,0x12,0x12,0x0C,0x00,	// 0xE9
0x0E,0x00,0x12,0x12,0x12,0x12,0x0C,0x00,	// 0xEA
0x18,0x00,0x12,0x12,0x12,0x12,0x0C,0x00,	// 0xEB
0x06,0x00,0x12,0x12,0x12,0x0E,0x04,0x18,	// 0xEC
0x06,0x00,0x11,0x0A,0x04,0x04,0x04,0x00,	// 0xED
0x00,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,	// 0xEE
0x0C,0x0C,0x08,0x00,0x00,0x00,0x00,0x00,	// 0xEF
0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x00,	// 0xF0
0x00,0x04,0x0E,0x04,0x00,0x0E,0x00,0x00,	// 0xF1
0x00,0x00,0x1F,0x00,0x00,0x1F,0x00,0x00,	// 0xF2
0x30,0x1A,0x34,0x0B,0x15,0x07,0x01,0x00,	// 0xF3
0x0F,0x15,0x15,0x0D,0x05,0x05,0x05,0x00,	// 0xF4
0x0E,0x11,0x0C,0x0A,0x06,0x11,0x0E,0x00,	// 0xF5
0x00,0x04,0x00,0x1F,0x00,0x04,0x00,0x00,	// 0xF6
0x00,0x00,0x00,0x0E,0x06,0x00,0x00,0x00,	// 0xF7
0x0C,0x12,0x12,0x0C,0x00,0x00,0x00,0x00,	// 0xF8
0x00,0x00,0x00,0x0A,0x00,0x00,0x00,0x00,	// 0xF9
0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x00,	// 0xFA
0x08,0x18,0x08,0x08,0x00,0x00,0x00,0x00,	// 0xFB
0x1C,0x08,0x0C,0x18,0x00,0x00,0x00,0x00,	// 0xFC
0x18,0x04,0x08,0x1C,0x00,0x00,0x00,0x00,	// 0xFD
0x00,0x00,0x1E,0x1E,0x1E,0x1E,0x00,0x00,	// 0xFE
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 	// 0xFF
*/
};
	
  /////////////////////////////////////
 // Functions for LCD12864 control  //
/////////////////////////////////////
//Write instruction (code==0) or data (code==1) to LCD
void lcd_write(char lcdmode, unsigned char value)
{
	DDRD = 0xFF;     //Set port for write operation
	
	set_rw(0);	     //Write operation
	set_rs(lcdmode); //0 for instruction, 1 for data
       
	PORTD = value;
    set_e(1);
    _delay_us(10);
	set_e(0);	
		
	set_rs(0);
}    

//Read data from LCD
char lcd_read(char lcdmode)
{
	unsigned char value;
	
	DDRD = 0x00;       //Set port for read operation
	//PORTD = 0xFF;      //Activate pull-up resistors to ensure proper data transmission
    
    set_rw(1);	       //Read operation
	set_rs(lcdmode);   //Get value 0: for busy flag, 1 for other data
	
	set_e(1);          //Read data
    _delay_us(10);       
	value = PIND;
    set_e(0);	
	
	//set_rs(0);
	
	return value;
}    

//Set RW line
void set_rw(char status)  
{
    if(status)
	{
        PORTC |= 2;
	}	
    else
	{
	    PORTC &= ~(2);
	}	
}

//Set RS line
void set_rs(char status) 
{
    if(status)
	{
        PORTC |= 1;
	}	
    else
	{
	    PORTC &= ~(1);
	}	
}

//Set E line
void set_e(char status)  
{
    if(status)
	{
        PORTC |= 4;
	}	
    else
	{
	    PORTC &= ~(4);
	}	
}

//Check for busy flag (BF)
int is_lcd_busy(void)
{
	int v = lcd_read(0);
	_delay_us(10);
	v = lcd_read(0);
			
	if(v & 0x80)
	{
		return -1;
	}
	else
	{
		return 0;
	}	
		
}

//Send one character to LCD (Normal size)
//
void lcd_putchar(int row0, int col0, unsigned char ch0, int inv)
{
	int t1;
	int odd = 0;
	unsigned char v1, v2;
	int col = col0 / 2;
	int row = row0 * FONTHEIGHT;
	unsigned char ch;
		
	if(row & 0x20)  //Enter lower part of screen => go to next page
	{
        row &= ~0x20;
        col |= 8;
    }
        
	if(col0 & 1) //Detect odd coloumn
	{
		odd = 1;
	}
		
	for(t1 = 0; t1 < FONTHEIGHT; t1++)
    {
	    //Set address
        lcd_write(0, 0x80 + row + t1);
        lcd_write(0, 0x80 + col);
             
        //Get old values of 2 GDRAM bytes	
	    v1 = lcd_read(1);                
        v1 = lcd_read(1);
        v2 = lcd_read(1);

        //Set address
        lcd_write(0, 0x80 + row + t1);
        lcd_write(0, 0x80 + col);
        
        if(!inv)
        {
			ch = font[ch0 * FONTHEIGHT + t1];
		}
		else	
        {
			ch = ~font[ch0 * FONTHEIGHT + t1];
		}
		     
        if(odd)
        {     
            //Write data on RIGHT side of existing character
            lcd_write(1, v1);
            lcd_write(1, ch);
        }
        else    
        {   
			//Write data on LEFT side of existing character
            lcd_write(1, ch);
            lcd_write(1, v2);
        }
    }    
}   

//Send one character to LCD (DOUBLE size and normal width)
//
void lcd_putchar2(int row0, int col0, unsigned char ch0, int inv)
{
	int t1, t2;
	int odd = 0;
	unsigned char v1, v2;
	int col = col0 >> 1;
	int row = row0 * FONTHEIGHT;
	unsigned char ch;
		
	if(row & 0x20)  //Enter lower part of screen => go to next page
	{
        row &= ~0x20;
        col |= 8;
    }
        
	if(col0 & 1) //Detect odd coloumn
	{
		odd = 1;
	}
		
	for(t1 = 0; t1 < FONTHEIGHT; t1++)
    {
		if(!inv) //Calculate character position in array and xor invert number if needed
        {
			ch = (font[ch0 * FONTHEIGHT + t1]);
		}
		else	
        {
			ch = (~font[ch0 * FONTHEIGHT + t1]);
		}
		
		for(t2 = 0; t2 < 2; t2++)
		{
	        //Set address
            lcd_write(0, 0x80 + row + t1 * 2 + t2);
            lcd_write(0, 0x80 + col);
             
            //Get old values of 2 GDRAM bytes	
	        v1 = lcd_read(1);                
            v1 = lcd_read(1);
            v2 = lcd_read(1);

            //Set address
            lcd_write(0, 0x80 + row + t1 * 2 + t2);
            lcd_write(0, 0x80 + col);
        		     
            if(odd)
            {     
                //Write data on RIGHT side of existing character
                lcd_write(1, v1);
                lcd_write(1, ch);
            }
            else    
            {   
			    //Write data on LEFT side of existing character
                lcd_write(1, ch);
                lcd_write(1, v2);
            }    
        }
    }    
}   

//Send one character to LCD (DOUBLE size and DOUBLE width)
//
void lcd_putchar3(int row0, int col0, unsigned char ch0, int inv)
{
	int t1, t2;
	unsigned char ch;
	//unsigned int i[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
	unsigned int i[FONTHEIGHT] = {0, 0, 0, 0, 0, 0, 0, 0};
	int col = col0 >> 1;
	int row = row0 * FONTHEIGHT;
	
	if(row & 0x20)  //Enter lower part of screen => go to next page
	{
        row &= ~0x20;
        col |= 8;
    }
        
		
	for(t1 = 0; t1 < FONTHEIGHT; t1++)
    {
		if(!inv) //Calculate character position in array and xor invert number if needed
        {
			ch = (font[ch0 * FONTHEIGHT + t1]);
		}
		else	
        {
			ch = (~font[ch0 * FONTHEIGHT + t1]);
		}
		
		//Double 8 to 16 bits
	    i[t1] = 0;
		for(t2 = 7; t2 > -1; t2--)
		{
			if(ch & (1 << t2))
			{
				i[t1] += (1 << ((t2 << 1) + 1)) + (1 << (t2 << 1)); //Double bit pattern 2 by 1
			}
		}
	}
	
	t2 = 0;
	for(t1 = 0; t1 < FONTHEIGHT; t1++)
	{
		for(t2 = 0; t2 < 2; t2++)
		{
	        //Set address
            lcd_write(0, 0x80 + row + t1 * 2 + t2);
            lcd_write(0, 0x80 + col);
                       
            lcd_write(1, ((i[t1] >> 8) & 0xFF));
            lcd_write(1, i[t1] & 0xFF); 
            //lcd_putnumber(t1, 0, i[t1] , -1, 0, 0);
        }    
    }    
		
}	

//Send string (\0 terminated) to LCD normal or double height
void lcd_putstring_a(int row, int col, char *s, int size, int inv)
{
    unsigned char t1;

    for(t1 = col; *(s); t1++)
	{
		if(!size)
		{
            lcd_putchar(row, t1, *(s++), inv);
        }
        else    
        {
            lcd_putchar2(row, t1, *(s++), inv);
        }
	}	
}

//String in DOUBLE height and DOUBLE width
void lcd_putstring_b(int row, int col, char *s, int inv)
{
    unsigned char t1;

    for(t1 = col; *(s); t1++)
	{
	    lcd_putchar3(row, t1 * 2, *(s++), inv);
	}	
}

//Clear LCD
void lcd_cls(void)
{
	int x, y;
    for(x = 0; x < 16; x++)
    {
		for(y = 0; y < 64; y++)
		{
			//Set address
            lcd_write(0, 0x80 + y);
            lcd_write(0, 0x80 + x);
             
            //Write data
            lcd_write(1, 0);
            lcd_write(1, 0);
        }
    }    
}

//Convert a number to a string and print it
//col, row: Coordinates, Num: int or long to be displayed
//dec: Set position of decimal separator
//
//inv: Set to 1 if inverted charactor is required
void lcd_putnumber(int col, int row, long num, int dec, int lsize, int inv)
{
    char *s = malloc(16);
	if(s != NULL)
	{
	    int2asc(num, dec, s, 16);
	    lcd_putstring_a(col, row, s, lsize, inv);
	    free(s);
	}	
	else
	{
		lcd_putstring_a(col, row, "Error", 0, 0);
	}	
}


//Init LCD
void lcd_init(void)
{            
    //Reset
    PORTC &= ~(8);
    _delay_ms(1);
    PORTC |= 8;
    _delay_ms(40);
    
    lcd_write(0, 0x30);	//Use 8-bit mode parallel
    _delay_ms(1);
         
    lcd_write(0, 0x0C); //All on Cursor on, Blink on , Display on
    _delay_ms(1);
        
    lcd_write(0, 0x01); //Perform CLS in text mode to eliminate random chars from screen
    _delay_ms(20);
    
    lcd_write(0, 0x34); //Switch to extended mode, redefine function set
    _delay_us(100);
    
    lcd_write(0, 0x36); //Add graphic mode
    _delay_us(100);
                   
    lcd_write(0, 0x12); //Display control and display ON
    _delay_us(100);
}

//////////////////////
// STRING FUNCTIONS //
//////////////////////
//INT 2 ASC
int int2asc(long num, int dec, char *buf, int buflen)
{
    int i, c, xp = 0, neg = 0;
    long n, dd = 1E09;

    if(!num)
	{
	    *buf++ = '0';
		*buf = 0;
		return 1;
	}	
		
    if(num < 0)
    {
     	neg = 1;
	    n = num * -1;
    }
    else
    {
	    n = num;
    }

    //Fill buffer with \0
    for(i = 0; i < 12; i++)
    {
	    *(buf + i) = 0;
    }

    c = 9; //Max. number of displayable digits
    while(dd)
    {
	    i = n / dd;
	    n = n - i * dd;
	
	    *(buf + 9 - c + xp) = i + 48;
	    dd /= 10;
	    if(c == dec && dec)
	    {
	        *(buf + 9 - c + ++xp) = '.';
	    }
	    c--;
    }

    //Search for 1st char different from '0'
    i = 0;
    while(*(buf + i) == 48)
    {
	    *(buf + i++) = 32;
    }

    //Add minus-sign if neccessary
    if(neg)
    {
	    *(buf + --i) = '-';
    }

    //Eleminate leading spaces
    c = 0;
    while(*(buf + i))
    {
	    *(buf + c++) = *(buf + i++);
    }
    *(buf + c) = 0;
	
	return c;
}


int main(void)
{
    // Set ports for LCD output and input data
    DDRC = 0x0F; //LCD RS, RW, E and RST at PC0:PC3
	DDRD = 0xFF; //LCD data on PD0:PD7
   	                 		
	//Display init
	_delay_ms(100);
    lcd_init();
	_delay_ms(100);
	
    lcd_cls();
        
    lcd_putstring_a(0, 0, "LCD 12864 ST7920", 0, 0);
    lcd_putstring_a(1, 0, "   DK7IH 2018   ", 0, 1);
    lcd_putstring_a(2, 0, "Graphical Fonts:", 0, 0);
    lcd_putstring_a(3, 0, "8x8px.", 0, 0);
    lcd_putnumber(4, 0, 1234, 1, 0, 0);    
       
	lcd_putstring_a(4, 0, "16x8px.", 1, 0);
	
	lcd_putstring_b(6, 0, "16x16px.", 0);
			    
    for(;;) 
	{
    
    
	}
	return 0;
}
Advertisements

Decoding NMEA data from a GPS module with a microcontroller

I found one of those widely distributed GPS receiver modules in my collection of electronic material. I had bought it some years ago for a school project I did with students to launch a balloon into the stratosphere (Link to text in German). Here a video of the flight phase when maximum altitude was reached:

The GPS module that was used then has still been sleeping in my junk box. When looking through the C code I had written then I found that this could be optimized. I connected the GPS receiver to an AVR ATmega32 microcontroller, added a simple 2-lines-16-characters lcd and started rewriting the software for communicating with the module.

The Hardware

OK, an ATmgea32 is somehow “overkill” for this simple circuit, but I had already installed it on my breadboard. The data lines of the ATmega32 controller that are in use for this project are pointed out here:

ATMega32_GPS-Connections

The GPS module is connected to the controller via the serial RS232 interface (TxD, RxD, PIN 15 and 14 of the micro). Note that RX of the GPS module must be connected to TxD with the controller and vice versa.

The rest of the layout is also very simple.

Microcontroller interface connections for this project

The lcd is driven in 4-bit-mode, thus only four display data lines are needed. Plus the 2 controls RS and E:

ATMega32 => LCD

  • PD4 => LCD PIN D4
  • PD5 => LCD PIN D5
  • PD6 => LCD PIN D6
  • PD7 => LCD PIN D7
  • PC2 => LCD RS
  • PC3 => LCD E

ATMega32 => GPS Module

  • PD0 (RxD) => GPS TX
  • PD1 (TxD) => GPS RX

Decoding GPS data

GPS data from the module comes coded as a so-called “NMEA”-string. “NMEA” stands for the American National Marine Electronics Association . The data is encrypted using a standardized ASCII format that appears in various patterns and is transferred by the GPS module regularly in very short intervals (about 1 second or less). The communication settings for the RS232 interface are fixed and can not be altered:

9600 baud, 8 data bits, 1 stop bit, no parity

The ATMega’s UART (universal asynchronous receiver and transmitter)  must be set with an initialization routine to the required parameters:

//Init UART
void uart_init()
{
    /* 9600 Baud */
    UBRRL = 51; UBRRH = 0;

    /* RX and TX on */
    UCSRB = (1<<RXEN)|(1<<TXEN);

    /* 8 databits, 1 stopbit, No parity */
    UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0);
}

NMEA strings (example)

There are various types of data strings transferred by the module, each serving an individual purpose. They are all coded with a leading identifier marked by a $-sign and an identifier code. We will discuss the absolute basic string that delivers essential GPS information, the so-called “GPRMC”-string, where the letters “RMC” represent “recommended minimum sentence”.

This sequence contains all the necessary data for a basic GPS operation and therefore is ideal for experiments with the GPS module. Example for such a string:

$GPRMC,191410,A,4735.5634,N,00739.3538,E,0.0,0.0,181102,0.4,E*19

The various data fields are separated by a comma, please note that the data fields may have various width so just counting bytes is not reliable if you want to “land” in a specific field. Instead the comma separators must be counted.

What do the separate data fields mean?

The first entry (“191410”) after the “$GPRMC” intro is the UTC time stamp, telling when the recent GPS data set was processed, i. e. the current time in HHMMSS- format. There might be even 10th of seconds then the format is HHMMSSss.

Next is a letter (“A” for correct data, “V” (void) if the receiver was not able to get correct data), that indicates if the data is valid and can be used or not. You can use that to discard the data set when the receiver is not able to detect a valid GPS signal.

Following a number (here “4735.5634”) together with the subsequent letter (N or S) indicates the latitude of the current position. This is a 100fold decimal value formatted  by a decimal separator. Latitude can be in the range from 0 to 90 degrees North or South. Here it is about 47 degrees in the northern hemisphere.

Subsequently the same coding is used for transferring longitude data. Longitude is in the range from 0 to 180 degrees East or West with the Prime Meridian in Greenwich as separator.

Next two values (0.0 and 0.0) indicate ground speed in knots and bearing in degrees.

“181102” is the date stamp (18th Nov 2002).

“0.4” and “E” indicate a magnetic declination (degrees and direction) that might occur.

“19” is the  XOR generated checksum of the string. I found that data transfer is very reliable so I don’t calculate this.

Calculating QTH locator

For a radio amateur it is clear that GPS data must be converted to the Maidenhead system to determine the current QTH locator (JN49AB in my case). Thus I have programmed a short function that can convert decimal data for latitude and longitude to the Maidenhead system:

//Calc QTH locator from decimal value for latitude and
//longitude. o1 and o2 are signifier for 'E or 'W' (longitude) resp.
//'N' or 'S' for latitude
//example for calling: calc_maidenhead(8.002, 'E', 49.1002, 'N', buf);
void calc_maidenhead(double longitude, char o1, double latitude, char o2, char *buffer)
{

    //Longitude
    int deg_lo, lo1, lo2, lo3;
    double deg10_lo;

    //Lattitude
    int deg_la, la1, la2, la3;
    double deg10_la;

    deg_lo = longitude; //Truncate values for longitude and latitude
    deg_la = latitude;

    deg10_lo = (longitude - deg_lo) * 12; //Calculate fraction behind decimal separator
    deg10_la = (latitude - deg_la) * 24;

    //Longitude
    if(o1 == 'E')
    {
        lo1 = 180 + deg_lo;
    }
    else
    {
        lo1 = 180 - deg_lo;
    }
    lo2 = lo1 / 20;
    lo3 = lo1 - lo2 * 20;

    //Lattitude
    if(o2 == 'N')
    {
        la1 = 90 + deg_la;
    }
    else
    {
        la1 = 90 - deg_la;
    }

    la2 = la1 / 10;
    la3 = la1 - la2 * 10;

    *(buffer + 0) = lo2 + 65;
    *(buffer + 1) = la2 + 65;
    *(buffer + 2) = lo3 / 2 + '0';
    *(buffer + 3) = la3 + '0';
    *(buffer + 4) = (int) deg10_lo + 'A';
    *(buffer + 5) = (int) deg10_la + 'A';
}

Parameters handed over to the function are

  • Longitude in decimals (double),
  • ‘E’ or ‘W’ for orientation,
  • Latitude in decimals (double),
  • ‘N’ or ‘S’ for orientation,
  • a 6 byte buffer (+ 1 byte for “\0” termination) referenced by address for the locator data.

The program rolls through the data permanently giving relevant information on the display with changes every second. Due to the fact that I haven’t implemented error handling there might be more or less useles indications when data from the GPS receiver is corrupted or invalid. But mostly the routine works out very fine without any complaint. You can use the ‘A’ or ‘V’ indicator transferred to change the display routine if unvalid data is detected.

By the end of this article you can get the full  C-code for the project. Thanks for reading!

73 de Peter (DK7IH)

/*****************************************************************/
/*                    GPS with ATMega32                          */
/*  ************************************************************ */
/*  Mikrocontroller:  ATMEL AVR ATmega32, 8 MHz                  */
/*                                                               */
/*  Compiler:         GCC (GNU AVR C-Compiler)                   */
/*  Autor:            Peter Rachow  2018                         */
/*  Fuses: -U lfuse:w:0xe4:m -U hfuse:w:0xd9:m                   */
/*****************************************************************/
// This code reads data from a GPS module via RS232 interface and
// decodes the basic NMEA string of $GPRMC type, the so-called
// "RMC" (Recommended Minimum Specific GNSS Data) string.
// Displayed are Date, Time, QTH-Locator (Maidenhead), Lattitude and
// Longitude and Ground speed
/*   PORTS */
// O U T P U T
// LCD
// RS      = PC2
// E       = PC3
// D4...D7 = PD4..PD7

// PD0  TX at GPS module
// PD1  RX at GPS module

#define F_CPU 8000000

#include
#include
#include
#include 

#include
#include
#include
#include
#include 

int main(void);

/***************/
/* LCD-Display */
/***************/
#define LCD_INST 0x00
#define LCD_DATA 0x01

void lcd_write(char, unsigned char);
void lcd_write(char, unsigned char);
void set_rs(char);
void set_e(char);
void lcd_init(void);
void lcd_cls(void);
void lcd_line_cls(int);
void lcd_putchar(int, int, unsigned char);
void lcd_putstring(int, int, char*);
void lcd_putnumber(int, int, long, int);
void lcd_display_test(void);
void setcustomcharacters(void);

/*******************/
/* Stringhandling */
/*******************/
int strlen(char *s);
int instring(char*, char*);
int strcomp(char*, char*);
void int2asc(long, int, char*, int);

/*******************/
/*   U  A  R  T    */
/*******************/
void uart_init(void);
void uart_putc(char);
void uart_send_string(char*);
void init_rx_buffer(void);
char make_crc(int, int);

/**********************/
/* V A R I A B L E S  */
/**********************/
//USART defines &amp; variables
#define RX_BUF_SIZE 64
char rx_buf[RX_BUF_SIZE];
int rx_buf_cnt = 0;

/*****************************/
/* Result string formatting  */
/*****************************/
void get_time(char*, char*);
int get_receiver_status(char*);
void get_latitude(char*, char*);
void get_longitude(char*, char*);
void get_latitude_ns(char*, char*);
void get_longitude_ew(char*, char*);
double get_gps_coordinate_decimal(char*, int, char*);
void get_ground_speed(char*, char*);
void get_ground_speed(char*, char*);
void get_date(char*, char*);
void calc_maidenhead(double, char, double, char, char*);

/**************************************/
/*            L  C  D                 */
/**************************************/
/* Send one byte to LCD */
void lcd_write(char lcdmode, unsigned char value)
{
    int x = 16, t1;

    set_e(0); 

    if(!lcdmode)
	{
        set_rs(0);
	}
    else
	{
        set_rs(1);
	}	

    _delay_ms(1);

    set_e(1);

    /* Hi */
	for(t1 = 0; t1 &lt; 4; t1++)
	{
	    if(value &amp; x)
	    {
	       PORTD |= x;
	    }
        else
	    {
           PORTD &amp;= ~(x);
	    }  

		x *= 2;
	}
	set_e(0);

	x = 16;

	set_e(1);
	/* Lo */
	for(t1 = 0; t1 &lt; 4; t1++)
	{
	    if((value &amp; 0x0F) * 16 &amp; x)
	    {
	       PORTD |= x;
	    }
        else
	    {
           PORTD &amp;= ~(x);
	    }  

		x *= 2;
	}

    set_e(0);
}

/* RS */
void set_rs(char status)
{
    if(status)
	{
        PORTC |= (1 &lt;&lt; PC2);
	}
    else
	{
	    PORTC &amp;= ~(1 &lt;&lt; PC2);
	}
}

/* E */
void set_e(char status)
{
    if(status)
	{
        PORTC |= (1 &lt;&lt; PC3);
	}
    else
	{
	    PORTC &amp;= ~(1 &lt;&lt; PC3);
	}
}

void lcd_putchar(int row, int col, unsigned char ch)
{
    lcd_write(LCD_INST, col + 128 + row * 0x40);
    lcd_write(LCD_DATA, ch);
}

void lcd_putstring(int row, int col, char *s)
{
    unsigned char t1;

    for(t1 = col; *(s); t1++)
	{
        lcd_putchar(row, t1, *(s++));
	}
}

void lcd_putnumber(int y, int x, long number, int dec)
{
	char *buf;

	buf = malloc(32);
	int2asc(number, dec, buf, 31);
	lcd_putstring(y, x, buf);
	free(buf);
}	

void lcd_cls(void)
{
    lcd_write(LCD_INST, 1);
}

void lcd_init(void)
{

    lcd_write(LCD_INST, 40);

    //Matrix 5*7
    lcd_write(LCD_INST, 8);

    /* Display on, Cursor off, Blink off */
    /* Entrymode !cursoincrease + !displayshifted */
    lcd_write(LCD_INST, 12);

	//4-Bit-Mode
    lcd_write(LCD_INST, 2);	

	lcd_cls();
}

void lcd_line_cls(int ln)
{
    int t1;

	for(t1 = 0; t1 &lt; 15; t1++)
	{
	    lcd_putchar(1, t1, 32);
	}
}	

//Define own chars
void setcustomcharacters(void)
{
    int i1;
    unsigned char adr=64;

    unsigned char customchar[]={ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,     // \0
		                         0x04, 0x0A, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00};    //°-sign
    lcd_write(LCD_INST, 0);
    lcd_write(LCD_DATA, 0);

    //Send data to CGRAM in lcd
    for (i1 = 0; i1 &lt; 16; i1++)
    {
        lcd_write(LCD_INST, adr++);
        lcd_write(LCD_DATA, customchar[i1]);
    }
}

/***********************/
//
// STRING-FUNCTIONS
//
/**********************/
//Convert int number to string
void int2asc(long num, int dec, char *buf, int buflen)
{
    int i, c, xp = 0, neg = 0;
    long n, dd = 1E09;

    //Write 0 to buffer in case value == 0
    if(!num)
    {
		buf[0] = &#039;0&#039;;
		buf[1] = 0;
		return;
	}	

    if(num &lt; 0)
    {
     	neg = 1;
	    n = num * -1;
    }
    else
    {
	    n = num;
    }

    //Fill buffer with \0
    for(i = 0; i &lt; 12; i++)
    {
	    *(buf + i) = 0;
    }

    c = 9; //Max. number of displayable digits
    while(dd)
    {
	    i = n / dd;
	    n = n - i * dd;

	    *(buf + 9 - c + xp) = i + 48;
	    dd /= 10;
	    if(c == dec &amp;&amp; dec)
	    {
	        *(buf + 9 - c + ++xp) = &#039;,&#039;;
	    }
	    c--;
    }

    //Search for 1st char different from &#039;0&#039;
    i = 0;
    while(*(buf + i) == 48)
    {
	    *(buf + i++) = 32;
    }

    //Add minus-sign if neccessary
    if(neg)
    {
	    *(buf + --i) = &#039;-&#039;;
    }

    //Eleminate leading spaces
    c = 0;
    while(*(buf + i))
    {
	    *(buf + c++) = *(buf + i++);
    }
    *(buf + c) = 0;
}

//Compare 2 strings
int strcomp(char *s1, char *s2)
{
    int t1;
    for(t1 = 0; t1 &lt; strlen(s2); t1++)
    {
        if(*(s1 + t1) != *(s2 + t1))
	    {
	       return 0;
		}
    }

    return 1;
}

//Get length of string
int strlen(char *s)
{
   int t1 = 0;

   while(*(s + t1++));

   return (t1 - 1);
}

//Find s2 in s1 if present, hand back position in string if yes
int instring(char *s1, char *s2)
{
   int t1, t2, ok;

   for(t1 = 0; *(s1 + t1) ; t1++)
   {
	    ok = 1;
	    for(t2 = 0; t2 &lt; strlen(s2); t2++)
	    {

	        if(*(s1 + t1 + t2) != *(s2 + t2))
	        {
	            ok = 0;
	        }
	    }

		if(ok)
	    {
	        return t1;
	    }
    }

    return 0;
}

//************/
//    UART
//************/
//Init UART
void uart_init()
{
    /* 9600 Baud */
    UBRRL = 51;
    UBRRH = 0;

    /* RX and TX on */
    UCSRB = (1&lt;&lt;RXEN)|(1&lt;&lt;TXEN);    

    /* 8 Datenbits, 1 Stopbit, keine Paritaet */
    UCSRC = (1&lt;&lt;URSEL)|(1&lt;&lt;UCSZ1)|(1&lt;&lt;UCSZ0);
}

//Send 1 char to UART
void uart_putc(char tx_char)
{
    while(!(UCSRA &amp; (1&lt;&lt;UDRE)));

    UDR = tx_char;
}

//Send one string to UART
void uart_send_string(char *s)
{
    int t1 = 0;
	while(*(s + t1))
	{
	    uart_putc(*(s + t1++));
	}
	uart_putc(13);
	uart_putc(10);

}

uint8_t uart_getc(void)
{
    while(!(UCSRA &amp; (1&lt;&lt;RXC)));   // warten bis Zeichen verfuegbar
    return UDR;                   // Zeichen aus UDR an Aufrufer zurueckgeben
}

//Init RX buffer
void init_rx_buffer(void)
{
	int t1;

    for(t1 = 0; t1 &lt; RX_BUF_SIZE - 1; t1++)
    {
		rx_buf[t1] = 0;
	}
	rx_buf_cnt = 0;
}		

/* CRC-calculation */
char make_crc(int buflen, int addchar)
{
    int t1, x = 0;

    for(t1 = 0; t1 &lt; buflen; t1++) /* Puffer bis dato */
	{
        x = x ^ rx_buf[t1];
    }
	x = x ^ addchar;                /* Sendebyte */

    return x;
}

///////////////////// END UART ///////////////

//////////////////////////////
//                          //
// NMEA decode functions    //
//                          //
//////////////////////////////

//Format date string
void get_date(char *buffer2, char *buffer3)
{

    int t1, t2 = 0;
    int p1, p2;

    //Zero buffer3
    for(t1 = 0; t1 &lt; RX_BUF_SIZE; t1++)
    {
		*(buffer3 + t1) = 0;
	}

	//Find 9th datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 9 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p1 = t1;

	//Find end of datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 10 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p2 = t1;

	//Copy relevant prt of string to buffer3
	t2 = 0;
    for(t1 = p1; t1 &lt; p2 - 1; t1++)
    {
		if((t1 - p1) == 2 || (t1 - p1) == 4)
		{
			*(buffer3 + t2++) = &#039;.&#039;;
		}
		*(buffer3 + t2++) = *(buffer2 + t1);
	}
}	

//Format time string
void get_time(char *buffer2, char *buffer3)
{

    int t1, t2 = 0;
    int p1, p2;

    //Zero buffer3
    for(t1 = 0; t1 &lt; RX_BUF_SIZE; t1++)
    {
		*(buffer3 + t1) = 0;
	}

	//Find first datafield
	t1 = 0;
	while(*(buffer2 + t1) != &#039;,&#039; &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		t1++;
	}
	p1 = t1 + 1;

	//Find dot in timestring
	t1 = p1 + 1;
	while(*(buffer2 + t1) != &#039;.&#039; &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		t1++;
	}
	p2 = t1;

	//Copy relevant prt of string to buffer3
    for(t1 = p1; t1 &lt; p2; t1++)
    {
		if(t1 == 9 || t1 == 11)
		{
			*(buffer3 + t2++) = &#039;:&#039;;
		}
		*(buffer3 + t2++) = *(buffer2 + t1);
	}
}	

//Format receiver status string
int get_receiver_status(char *buffer)
{
	int t1, t2 = 0;
    int p1;

	//Find second datafield
	t1 = 0;
	while(t2 != 2 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p1 = t1;

	if(*(buffer + p1) == &#039;A&#039;)
    {
		return 0;
	}
	else
	{
		return 1;
	}
}	

//Format latitude to degrees
void get_latitude(char *buffer2, char *buffer3)
{

    int t1, t2;
    int p1, p2;

    //Zero buffer3
    for(t1 = 0; t1 &lt; RX_BUF_SIZE; t1++)
    {
		*(buffer3 + t1) = 0;
	}

	//Find 3rd datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 3 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p1 = t1;

	//Find end of datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 4 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p2 = t1;

	//No data available
	if(p2 - p1 == 1)
	{
		return;
	}	

	//Copy relevant prt of string to buffer3
	t2 = 0;
    for(t1 = p1; t1 &lt; p2 - 1; t1++)
    {
		if(t1 == p1 + 2)
		{
			*(buffer3 + t2++) = 1; //°
		}	

		*(buffer3 + t2++) = *(buffer2 + t1);
	}
}	

//Format latitude orientation
void get_latitude_ns(char *buffer2, char *buffer3)
{

    int t1, t2;
    int p1, p2;

    //Zero buffer3
    for(t1 = 0; t1 &lt; RX_BUF_SIZE; t1++)
    {
		*(buffer3 + t1) = 0;
	}

	//Find 4th datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 4 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p1 = t1;

	//Find end of datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 5 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p2 = t1;

	//No data available
	if(p2 - p1 == 1)
	{
		return;
	}	

	//Copy relevant prt of string to buffer3
	t2 = 0;
    for(t1 = p1; t1 &lt; p2 - 1; t1++)
    {
		*(buffer3 + t2++) = *(buffer2 + t1);
	}	

}	

//Format longitude
void get_longitude(char *buffer2, char *buffer3)
{

    int t1, t2;
    int p1, p2;

    //Zero buffer3
    for(t1 = 0; t1 &lt; RX_BUF_SIZE; t1++)
    {
		*(buffer3 + t1) = 0;
	}

	//Find 5th datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 5 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p1 = t1;

	//Find end of datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 6 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p2 = t1;

	//No data available
	if(p2 - p1 == 1)
	{
		return;
	}	

	//Copy relevant prt of string to buffer3
	t2 = 0;
    for(t1 = p1; t1 &lt; p2 - 1; t1++)
    {
		if(t1 == p1 + 3)
		{
			*(buffer3 + t2++) = 1; //°
		}	

		*(buffer3 + t2++) = *(buffer2 + t1);
	}	

}	

//Format longitude orientation
void get_longitude_ew(char *buffer2, char *buffer3)
{

    int t1, t2;
    int p1, p2;

    //Zero buffer3
    for(t1 = 0; t1 &lt; RX_BUF_SIZE; t1++)
    {
		*(buffer3 + t1) = 0;
	}

	//Find 5th datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 6 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p1 = t1;

	//Find end of datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 7 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p2 = t1;

	//No data available
	if(p2 - p1 == 1)
	{
		return;
	}	

	//Copy relevant prt of string to buffer3
	t2 = 0;
    for(t1 = p1; t1  lattiude
double get_gps_coordinate_decimal(char *buf, int ctype, char *retbuf)
{
	int t1, t2;
	int sp; //Position of 1st comma of relevant data field
	double rval = 0;
	double x;

	char *l_str = malloc(20);

	if(!ctype)
	{
		sp = 5;  //Longitude
	}
	else
	{
		sp = 3; //Lattitude
	}	

	//Init temporary string
	for(t1 = 0; t1 &lt; 20; t1++)
	{
		*(l_str + t1) = 0;
	}

	//Search start position of value (3rd or 5th &#039;,&#039;)
	t1 = 0; t2 = 0;
	while(buf[t1] != 0 &amp;&amp; t2 != sp)
	{
		if(buf[t1] == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	sp = t1;	

	//Load relevant part of original string to new bufferstring
	t2 = 0;
	for(t1 = sp; *(buf + t1) != &#039;,&#039;; t1++)
	{
		*(l_str + t2++) = buf[t1];
	}

    //Check multiplier
	if(!ctype)
	{
		x = 100;
	}
	else
	{
		x = 10;
	}

	//Convert string to FP number
	for(t1 = 0; l_str[t1] != 0; t1++)
	{
		if(l_str[t1] != &#039;.&#039;)
		{
		    rval += (double)(l_str[t1] - 48) * x;
		    x /= 10;
		}
	}

	free(l_str);

	//Get orientation indicator (N/S or W/E)
	if(!ctype)
	{
		sp = 6; //Longitude
	}
	else
	{
		sp = 4; //Lattitude
	}	

	//Search start position (4th or 6th &#039;,&#039;)
	//where letter is expected
	t1 = 0; t2 = 0;
	while(buf[t1] != 0 &amp;&amp; t2 != sp)
	{
		if(buf[t1] == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	sp = t1;	

	retbuf[0] = buf[sp];

	return rval;
}	

//Ground speed
void get_ground_speed(char *buffer2, char *buffer3)
{

    int t1, t2;
    int p1, p2;

    //Zero buffer3
    for(t1 = 0; t1 &lt; RX_BUF_SIZE; t1++)
    {
		*(buffer3 + t1) = 0;
	}

	//Find 5th datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 7 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p1 = t1;

	//Find end of datafield
	t1 = 0;
	t2 = 0;
	while(t2 != 8 &amp;&amp; t1 &lt; RX_BUF_SIZE)
	{
		if(*(buffer2 + t1) == &#039;,&#039;)
		{
			t2++;
		}
		t1++;
	}
	p2 = t1;

	//No data available
	if(p2 - p1 == 1)
	{
		return;
	}	

	//Copy relevant prt of string to buffer3
	t2 = 0;
    for(t1 = p1; t1 = RX_BUF_SIZE)
        {
			/*
			for(t1 = 0; t1 = 0; t1--)
		    {
			    //Scan for specific identifier of NMEA message RMC Example &quot;$GPRMC&quot;
		        valid = 1;
		        for(t2 = 0; t2 &lt; 5; t2++)
		        {
				    if(rx_buf[t1 + t2] != msg_code[t2])
				    {
					    valid = 0;
				    }
			    }		

		        if(valid) //Infotype OK, data found
		        {
					//Copy rx-buffer to buffer2

					//Init buffer2
					for(t2 = 0; t2 &lt; RX_BUF_SIZE; t2++)
					{
						*(buf2 + t2) = 0;
					}	

					//Copy relevant part of rx-buffer to buffer2
					t3 = 0;
					for(t2 = t1; t2 &lt; rx_buf_cnt &amp;&amp; rx_buf[t2] != &#039;*&#039;; t2++)
			        {
					    *(buf2 + t3) = rx_buf[t2];
					    t3++;
					}   

					lcd_cls();

					//Display information
					lcd_putstring(0, 0, &quot;  GPS Receiver&quot;);
                    lcd_putstring(1, 0, &quot;   DK7IH 2018&quot;);
                    _delay_ms(dispdelay);
					lcd_cls();

					//Message string
					lcd_putstring(0, 2, &quot;Message type&quot;);
					lcd_putstring(1, 4, msg_code);
					_delay_ms(dispdelay);
					lcd_cls();

					//QTH locator (Maidenhead)
					lcd_putstring(0, 2, &quot;QTH Locator&quot;);
					rbuf0 = malloc(4);
					rbuf1 = malloc(4);
					for(t1 = 0; t1 &lt; 4; t1++)
					{
						*(rbuf0 + t1) = 0;
						*(rbuf1 + t1) = 0;
					}	

					lon = get_gps_coordinate_decimal(buf2, 0, rbuf0); //Calc longitude
					lat = get_gps_coordinate_decimal(buf2, 1, rbuf1); //Calc latitude

					rstr = malloc(7);
					for(t1 = 0; t1 <span data-mce-type="bookmark" id="mce_SELREST_start" data-mce-style="overflow:hidden;line-height:0" style="overflow:hidden;line-height:0;"></span>&lt; 7; t1++)
					{
						*(rstr + t1) = 0;
					}
					calc_maidenhead(lon, rbuf0[0], lat, rbuf1[0], rstr);
					//calc_maidenhead(8.3362, &#039;E&#039;, 49.0145, &#039;N&#039;, rstr); //Test for &quot;JN49EA&quot;
					lcd_putstring(1, 4, rstr);
					_delay_ms(dispdelay);
				    free(rbuf0);
   			        free(rbuf1);
   			        free(rstr);
					lcd_cls(); 

					//DATE
					lcd_putstring(0, 6, &quot;Date&quot;);
					get_date(buf2, buf3);
					lcd_putstring(1, 4, buf3);
					_delay_ms(dispdelay);
					lcd_cls();

					//TIME
					lcd_putstring(0, 3, &quot;Time (UTC)&quot;);
					get_time(buf2, buf3);
					lcd_putstring(1, 4, buf3);
					_delay_ms(dispdelay);
					lcd_cls();

					//Receiver status
					lcd_putstring(0, 0, &quot;Receiver status&quot;);
					if(get_receiver_status(buf2) == 0)
					{
						lcd_putstring(1, 7, &quot;OK&quot;);
					}
					else
					{
						lcd_putstring(1, 7, &quot;ERR&quot;);
					}
					_delay_ms(dispdelay);
					lcd_cls();

					//Lattitude
					lcd_putstring(0, 4, &quot;Lattitude&quot;);

					get_latitude(buf2, buf3);
					lcd_putstring(1, 2, buf3);
					get_latitude_ns(buf2, buf3); //N or S
					lcd_putstring(1, 14, buf3);
					_delay_ms(dispdelay);
					lcd_cls();

					//Longitude
					lcd_putstring(0, 4, &quot;Longitude&quot;);
					get_longitude(buf2, buf3);
					lcd_putstring(1, 2, buf3);
					lcd_putstring(0, 4, &quot;Longitude&quot;); //E or W
					get_longitude_ew(buf2, buf3);
					lcd_putstring(1, 15, buf3);
					_delay_ms(dispdelay);
					lcd_cls();

                    //Ground speed
					lcd_putstring(0, 2, &quot;Ground speed&quot;);
					get_ground_speed(buf2, buf3);
					lcd_putstring(1, 5, buf3);
					lcd_putstring(1, 12, &quot;[kn]&quot;);
					_delay_ms(dispdelay);
					lcd_cls();
		        }
		    }
	        rx_buf_cnt = 0;
	    }
	    else
	    {
		    rx_buf[rx_buf_cnt++] = ch;
	    }
    }
	return 0;
}

Interfacing an analog temperature sensor to an AVR microcontroller

For a transceiver I wanted to design a temperature control for the final amplifier transistors. I have some KTY81-210 temperature sensors here in my junk box. So, one of these should be used. The transceiver again holds digital equipment (AD9951 as VFO and AD9834 as LO) driven by an AVR ATMega32 microcontroller. One of the ADC inputs of this device was designated to process the data delivered by the temp sensor.

Some theory in advance: How does a temperature sensor work?

The KTY81-210 is a “PTC” element. This to say that electrical resistance increases with temperature.

PTCs are mostly made from barium titanate (BaTiO3). This material has an interesting feature: Below the so called “Curie temperature” the material holds an NTC characteristic. Above this point the material changes its behavior and increases resistance sharply and resistance from now on increases with temperature.

The T->R function shows a relatively linear approach with some limitations:

kty81-210

For the accuracy in a simple temperature measurement for PA transistors this should be sufficient.

Connecting the PTC to the microcontroller

A simple voltage divider to GND is the best idea to get a voltage depending on the conductivity of the sensor element.

PTC-ADC-connect

One thing should be kept in mind: When current flows through the sensor, the sensor will heat up. If the current is too high this will corrupt your temperature reading. Thus I recommend having Rv > 5 kΩ.

Setting up the ADC (analog digital converter)

AVR microcontrollers have a 10-bit analog digital converter (ADC) operating on the successive approximation principle. The resolution due to the number of bits used is limited to 1024 discrete values (ADC value in the range 0..1023). They can be operated with a reference voltage of either 5V or 2.56V determined by software setup.

Before you can take a measurement the ADC must be set up. This can be done in the function that is used to read an ADC value or at the beginning of the program before enterin the main() loop. I here use the complete setup in the called function (code for ATmega32):

//Read ADC value
int get_adc(int adc_channel)
{
    int adc_val = 0;

    //ADC config and ADC init
    ADCSRA = (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1); //Activate ADC, Prescaler=64

    ADMUX = (1<<REFS0) + adc_channel; //Read ADC channel with Vref=VCC

    _delay_ms(3);

    ADCSRA |= (1<<ADSC);
    while(ADCSRA & (1<<ADSC));
    _delay_ms(3);

    ADCSRA |= (1<<ADSC);
    while(ADCSRA & (1<<ADSC));

    adc_val = ADCL;
    adc_val += ADCH * 256; 

    ADCSRA &= ~(1<<ADEN); //Deactivate ADC

    return adc_val;
}

With this the ADC references an input voltage set to 5V.

Calculating the ADC value

The ADC value depend on the input voltage: 0 represents 0 volts, 1023 represents 5V. Any value in between is calculated by:

Vx / 5V = ADC / 1024

where Vx is the applied voltage to the ADC input channel, ADC is the figure between 0 and 1023 representing the voltage.

So, Vx equals to

Vx = ADC * 5V / 1024

Calculating the temperature from the ADC voltage

The voltage divider equation lets us deduce the value of RPTC:

5V / UADC = (Rv + RPTC) / RPTC.

which equals to

5V / UADC = Rv / RPTC + 1

5V / UADC – 1 = Rv / RPTC

(5V / UADC – 1) / Rv = 1 / RPTC

Rv / (5V / UADC – 1) = RPTC (where UADC != 0)

Once having calculated RPTC we can calculate the temperature. Therefore we assume a linear dependency of the resistor to temperature:

R = T * m + R0.

(m is the slope factor (gradient), T0 is the constant term, in this case the PTC’s resistance at 0°C).

Changing the equation so that T is the result, we get:

(R – R0) / m = T.

R0 from the datasheet is 1630Ω typical. The slope factor (gradient) can also be deduced from the data sheet. It is

m = Δy/Δx.

So we can take two points out of the function (e . g. 0°C and 100°C) and calculate

m = Δy/Δx = 100K / (3392Ω-1630Ω) =100K / 1762Ω = 0.0567K/Ω

That means that the PTC element equals a change in resistance by 1Ω when the temperature changes by 0.0567K.

Packing all this into a C-function for the microcontroller we get:

//Measure temperature of final amplifier
//Sensor = KTY81-210
int get_temp(void)
{
    double temp;
    double rt, rv = 5100, ut;
    double r0 = 1630; //Resistance of temperature sensor at 0°C
    double m = 17.62; //slope of temperature sensor in Ohms/K

    //Calculate voltage from ADC value
    ut = (double)get_adc(4) * 5 / 1024;

    //Calculate thermal resistor value from ADC voltage ut
    rt = rv / (5 / ut - 1);

    //Calculate temperature from rt
    temp = 10 * ((rt - r0) / m);

    return (int)(temp);
}

Rv of the voltage divider in this case is set to 5100Ω. For other values please change value in the definitions section.

get_adc(4) is the function to get an ADC reading (see above!), in this case the voltage divider is connected to ADC4 input.

By the way: The functions returns a 10 fold value because in the reading I add a decimal to get one tenth resolution.

73 de Peter

(DK7IH)

 

 

Programming the AD9834 DDS chip

This is a software project for building a VFO with the 75MHz clocked AD9834 synthesizer chip by Analog Devices. Due to the Nyquyst theoreme with a maximum clock rate of 75 MHz a frequency of 37.5 MHz can be achieved. When overclocking the chip to 100 MHz (which has been succesfully tested in many cases) the maximum output frequency theoretically rises up to 50 MHz. But when coming close to these boundaries signal quality deteriorates severely. So it is recommended not to produce higher output frequencies than 25 MHz (75 MHz clock) or respectively 33 MHz (100MHz clock).

Theoretical outline

The AD9834 is a low power (20mW power consumption when VDD=3.3V) DDS module. It can handle up to 5.5 V as VDD (2.3V min.), so 5V single supply use makes circuitry simple. It comes in a 20 lead TSSOP case, breakout boards are available.

SPI signal structure

Programming a desired frequency into the DDS chip is performed by a 3 line communication, a serial peripheral interface (SPI). These three lines are called

  • SCLCK (the clock signal)
  • FSYNC (the signal that determines the end of the transfer of a single word (16 bits)
  • SDATA (the frequency or control information 16 bit words)

The timing diagram found in AD’s datasheet gives the precise structure of the signal communication:

AD9834 DDS SPI Timing diagram
AD9834 DDS SPI Timing diagram

FSYNC is  high when the first word (16 bits) is going to be transferred. SCLK is also high in this moment. Then FSYNC is set low, 16 bits subsequently are transmitted via the SDATA line. After one bit has been transmitted, SCLK is set low for 10 ns minimum and then goes high again for the next bit. Transfer starts with MSB (D15).

After 16 bits have been transmitted, FSYNC is set high again, showing the DDS chip that the word has been completely transferred. As soon a FSYNC is low again the DDS is ready for the transmission of the next 16 bits.

The DDS chip “language”

Frequency set

The chip has two frequency registers (FREQ0 and FREQ1). These registers contain 28 bits of frequency information. They can be addressed individually and are divided into two 14-bit sections each (MSB and LSB). In addition it is possible to load the LSB independently from the MSB if only a minor frequency change is required.

Frequency registers are selected by the first two MS-bits (DB15 and DB14) of a 16 bit structure sent to the DDS. “01” determines a frequency load for FREQ0, “10” loads the FREQ1 register.

Control transmission

Besides the frequency information some controls must be sent to the DDS chip. A control is also 16 bits wide. A control is initiated by a “00” starting sequence for DB15 and DB14.

Summary

So we can distinguish the purpose of a word by its first two bits:

  • “01” + 14 following bits loads the FREQ0 register,
  • “10” + 14 following bits loads the FREQ1 register,
  • “00” + 14 following bits transfers a control word.

One important control bit under the looking glass

There are many features to control the AD9834 chip. We want to limit this to the absolute basics. The most important bit is DB13. If you set this to “1” the chip is informed that the frequency information for the register to be loaded next will come in two consecutive 16 bit words addressing the respective frequency register with the 14 + 14 bits of frequency information.

So the first step is to transfer the “00” signaling a control code and next the “1” signaling that the user wants to write two 16 bit words for changing the frequency. The rest of the 16 bits of this control can be left “0”. This results in

“0010000000000000” (0x2000)

is the first word to be transmitted.

Frequency calculation and transfer

The frequency data of the waveform the user wants the chip to put out is determined by 28 bits, a so called “frequency word”. The formula is

frequency word = 2^28 / fclk * f

  • frequency word: a floating point number that will later be converted into a long integer containing the frequency information for the chip,
  • fclk: The master clock rate of the clock oscillator connected to the DDS [Hz],
  • f: The frequency the user wants to be generated [Hz].

Example

With a clock rate of 75 MHz a user frequency of 1 MHz would be calculated as a frequency word of

268435456 / 75000000 * 1000000 = 3579139,413333333

By leaving only the integer part of the number we get 3579139 which now is the frequency word that must be transferred to the chip.

Converted to binary this number figures out as

00001101101001110100000011

This is now split into two parts, 14 bits each:

0000110110100 1110100000011

Now we must tell the DDS in which of the two frequency registers we want to store this. Therefore we add the 2-digit-code for the desired frequency register in front of the respective number. In this example the destination is FREQ0, so we add “01”. The result are two words of 16 bits each:

010000110110100 011110100000011

Together with the control that allows us to write the 2 words consecutive into the chip we get a complete sequence of

0010000000000000 011110100000011 010000110110100

because the correct order is CONTROL first, then LSB, and MSB last. In HEX this is 0x2000, 0x3D03, 0x21B4.

Coding

Code examples in C for the AVR family follow. First the declarations, then some defines in advance so that you can adapt the code easily to your layout:

//Declarations SPI for DDS
void spi_start(void);
void spi_send_bit(int);
void spi_stop(void);
void set_frequency2(unsigned long);
// Defines SPI DDS (AD9834)
#define DDS_PORT PORTC 
#define DDS_FSYNC 1   //PC0
#define DDS_SDATA 2   //PC1 
#define DDS_SCLK 4    //PC2
#define DDS2_RESETPIN 3  //PC3

Before a transfer starts we need to send a “start” command to the SPI to set SCLK and FSYNC adequately. This is coded as:

void spi_start(void)
{
    DDS_PORT |= DDS_SCLK;     //SCLK hi
    DDS_PORT &= ~(DDS_FSYNC); //FSYNC lo
}

After a word has been transmitted this mus be shown with the “stop” command to inform the chip that 16 bits have been sent. So we set FSYNC to high:

void spi_stop(void)
{
    DDS_PORT |= DDS_FSYNC; //FSYNC hi
}

With these two functions we can initiate and terminate the transfer of 16 bits of data to the chip.

Next we must learn how to transfer data. This will be done by sending just one bit to the DDS and afterwards switching the clock accurately:

void spi_send_bit(int sbit)
{
    if(sbit)
    {
        DDS_PORT |= DDS_SDATA; //SDATA hi
    }
    else
    {
        DDS_PORT &= ~(DDS_SDATA); //SDATA lo
    }
    DDS_PORT |= DDS_SCLK; //SCLK hi
    DDS_PORT &= ~(DDS_SCLK); //SCLK lo
}

And now for computing and sending the frequency word:

void set_frequency2(unsigned long f)
{
    double fword0;
    long fword1, x;
    int l[] = {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    int m[] = {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, t1;

    fword0 = (double) 3.579139413 * f; // 3.579139413 = 268435456 / 75000000
    fword1 = (long) fword0;

    //Transfer frequency word to byte array
    x = (1 << 13); //2^13
    for(t1 = 2; t1 < 16; t1++)
    {
        if(fword1 & x)
        {
            l[t1] = 1;
        }
        x >>= 1;
    }

    x = (1L << 27); //2^27
    for(t1 = 2; t1 < 16; t1++)
    {
        if(fword1 & x)
        {
            m[t1] = 1;
        }
        x >>= 1;
    }

    //Transfer to DDS
    //Send start command
    spi_start();
    for(t1 = 15; t1 >= 0; t1--)
    {
        spi_send_bit(0x2000 & (1 << t1));
    }
    spi_stop();

    //Transfer frequency word 
    //L-WORD
    spi_start();
    for(t1 = 0; t1 < 16; t1++)
    {
        spi_send_bit(l[t1]);
    }
    spi_stop();

    //M-WORD
    spi_start();
    for(t1 = 0; t1 < 16; t1++)
    {
        spi_send_bit(m[t1]);
    }
    spi_stop();
}

I use a set of 2 arrays as predefined words including the start sequence for FREQ0 and then writing each single bit into the respective array.

To start the DDS correctly a short reset sequence should be placed in your main()-function:

//Reset DDS (AD9834) 
_delay_ms(10); 
DDS_PORT |= (1 << DDS_RESETPIN); //Bit hi
_delay_ms(10); 
DDS_PORT &= ~(1 << DDS_RESETPIN); //Bit lo
_delay_ms(10);

Alternative: Tie the RESET-Pin of the AD9834 permanently to GND. Notable that also a software rest is possible, but I prefer the hardware method.

By set_frequency(Value) you can start using this DDS.

73 de Peter (DK7IH)

Going back in time: “Old school” transceiver for 20 meters with 20 watts of output

"Old school" SSB transceiver for 14MHz by DK7IH (2018)

Hi again! This project directly “beams” you back to the “Good ol’ 80s” when there was no stuff like “DDS, “OLED” or even “SDR” or other modern technology we today use to build our radios.

I designed this transceiver using the “old school” techniques because in a German QRP forum on the internet some hams originated a “Back to the roots”-movement which I thought was a great idea. So I too went back in time 3 decades and constructed a radio like I did it in the eighties at the beginning of my “homebrew career”. That meant: No digital stuff, just a simple VFO but (and that is new) higher rf output power because condx are fairly low on the hf bands currently.

I later presented this radio at an annual German convention of homebrewing hams called the “Black Forest Meeting” named by the place where it is held the beginning of October each year.

To give you an impression, that’s how the radio looks from the outside. Pretty “old school”, isn’t it?

“Old school” SSB transceiver for 14MHz by DK7IH (2018)

The main design objectives were very simple:

  • Compact in size (even without using SMD components),
  • Analog VFO with vernier drive (1:10 gear) and variable capacitor,
  • No digital stuff (=> no digital noise!),
  • RF Output in the range from 15 to 20 watts pep in SSB,
  • Single conversion superhet (9MHz interfrequency)
  • No “save as many components as possible”-design.

First the block diagram giving you the basic structure of this radio::

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – basic outline

The VFO

I decided to use an analog VFO in this project due to three reasons:

  1. It’s really old style,
  2. it is much less prone to produce any unwanted “birdies”, and
  3. phase noise performance usually is better than most of the digital ways to generate a signal.

For the VFO I chose the Hartley design characterized by a tapped coil. This type uses less critical components than a comparable Colpitts circuit thus reducing number of parts (2 caps in this case that are avoided) which might lead to unwanted frequency changes (drift).

How to build a VFO that is really stable

Lots of pages have been written about this topic. This another one. First, be aware of the fact that it is not possible to build a VFO that has the same frequency stability like a modern digital system. This is because these systems are all crystal controlled. But it is possible to achieve a drift of some dozen Hertz within an hour or so which is absolutely sufficient for having even a longer QSO.

The main problem is based on physics, or thermodynamics to say more exactly. All material expands when heated and contracts when ambient temperature decreases. OK, some exceptions exist, water below 4°C is the commonly known example of them.

Avoiding thermal runaway

Heat is the problem in such a circuit. It comes from the interior of the components when current flows through them and from the outside, for example when the transceiver is exposed to sunlight or placed near another source of thermal energy. Also heating of the final rf amplifier stages may contribute to heating the cabinet inside. The electronic parts forming the central strucure of the tuned circuit exert the main influence connected to thermal runaway of the frequency that is generated.

The general approach is: When we can’t avoid physical effects we must choose components that change their values in such a way to compensate the changing of the values of the other parts. That means we have to look carefully on temperature coefficients of the varoius components we intend to build into our VFO.

Choosing the “right” components for your VFO

Choosing advantageous components is crucial for frequency stability. So I did some brief research to find out more about temperature coefficients of coils of various types and available capacitors. Here are some of the outcomes.

Explanation of syntax: If a relation is negative, a minus sign (“-“) is given. In this case the value (C or L) decreases when temperaure increases. A plus sign (“+”) indicates a positive coefficient. When the relation of value change by temperature change is weak (that means no intense changing of the value when heated), there is only one “-“-sign. The more “-“-signs you have, the higher this respective ratio is. The same applies for “+”-signs to indicate a positive relation.

Capacitors:

  • Ceramic capacitors: —
  • Polystyrene capacitor: –
  • NP0 (C0G) capacitor: no measurable effect

Inductors:

  • Air coil on polystyrene coil former: +++
  • Coil wound on T50-6 yellow toroid: +

Based on this short survey, the best combination would be NP0- and Polystyrene caps together with an inductor wound on an T50-6 (yellow) core. Hopefully their temperature behavoiur will compensate more or less and lead to best stability. Hint: On the photos appearing later in this text you will see an air coil wound on a TOKO style coil former that has been used because it does not need so much space.

The VFO circuit

I finally chose the Hartley circuit for my VFO. There it is:

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – VFO

VFO Circuit explanation

Starting from the left you can see the tapped coil (here 60 turns tapped at 10 turns from the bottom end) on a 5.5 mm TOKO style coil former without any core. In parallel there are various capacitors (polystyrene and NP0 mixed) to build up the total capcity. It is common use to spread the total capacity needed to various single capacitors because it has turned out that the effects of temperature change are less significant if you use more (and therefore smaller) single capacitors.

A 100k resistor is used to pull the gate to ground and therefore provides a correct bias at the FET’s gate. The 1N914 diode is a so called “clamp” diode that has been installed to stabilize (and therefore reduce) the rf voltage in order to avoid excessive rf voltage coming to the FET’S gate which would lead to distortion. This diode has a negative side effect, but that an be accepeted for a VFO in the rf bands: It slightly increases phase noise because it works as a regulator. With some the designs you can see this diode in reverse position, don’t worry, the regulating effect takes place either.

To ensure the oscillator to produce radio waves, in-phase feedback between gate and source is generated via the tap you can see with the coil. A tap of about 1/6 of the whole number of windings provides enough feedback voltage to let the oscillator start by its inherent thermal noise and generate clear sine waves afterwards. Putting the tap too close to the “hot end” will cause distortion because the amount of energy coupled back to the gate will be too high. Also instabilites are probable because of excessive drive power to the gate of the FET.

On top of the tuned circuit there is a varactor diode that is used to be controlled by a positive voltage to form a RIT (receiver independend tuning) control circuit. It is very loosely coupled to the tuned circuit to minimze temperature effects and because only 1 or 2 kHz “swing” is needed. The generation of the RIT voltage will be described later in this text.

The main tuning capacitor

An air capacitor is mandatory here! You can either use a ready made one from the surplus market. But to keep it as small as possible I built my own by dismanteling one of old variable capacitors formerly used for homemade AM radios. Use a small drill to remove the rivets, dismantle the capacitor completely and rebuild it again as an air capacitor (get rid of the plastic dielectric interlayers!) by using M2-screws and nuts. Youl will have evenings of endless fun with this game! 😉

Buffering and amplifying the Signal

The second stage with another FET is very loosely coupled to the source of the first FET. This is made to minimize effects of load changes to the frequency. This stage is a so called “source follower” giving a very low impedance signal to the final stage that is responsible for the amplification of the signal to a level of 2 to 3 volts pp which you will need for the rx mixer that has been designed as a dual-gate-MOSFET mixer (see receiver chapter later!).

DC voltage in the VFO

Voltage stabilization is crucial for best performance of this critical part of the radio. Supply voltage changes always lead to frequency changes. So a two-level buffering is common use here. The first (and most critical) stage is buffered twice (10V voltage regulator integrated circuit 78L10 and subsequently by a 6.2V zener diode) whereas the buffer and the amplifier stages are supplied with 10V regulated DC voltage only.

Ambient thermal isolation

To avoid the VFO being affected by interior thermal convection (flow of warm air inside the cabinet) it is recommended to shield the VFO from the rest of the transceiver. I do not recommend using metal sheets as walls here because these form other unwanted capacities that will lead to thermal effects on the generated frequency. Metal also is a good conductor for thermal energy, so you might run counter to your goals. My thermal insulation therefore is made of simple cardboard.

The Local Oscillator (LO)

This oscillator is much more uncritical than the VFO because it is crystal controlled. The purpose of the LO is to supply a carrier signal for the SSB modulator. Due to the fact that there are two sidebands we theoretically can use this LO must be switched to either one of two possible frequencies. In case of an interfrequency of 9MHz (9000kHz) these are: 9001.5 kHz for the first sideband and 8998.5 kHz for the second sideband. Please note that I did not write “USB” or “LSB” because the frequencies forming each sideband might be changed because of the frequency plan of the transceiver where by mixing with the VFO frequency the sideband might be inverted depending on if you add or substract the VFO frequency from the 9MHz-SSB signal.

There are several possibilities to produce these two frequencies:

  1. Using two different oscillators each equipped with a single crystal,
  2. switching two crystals with one osillator,
  3. using a variabale capacitor or a coil to “pull” one crystal to the desiered frequency.

Discussion:

  1. This method means high effort but surely is the most exact one because there are no influences of the unneeded choice to the other crystal currently on duty.
  2. Is the worst idea because the unswitched crystal is highly prone to influence the freqeuncy of the switched one because they are linked to parasetic capacities within the wiring, the switch and so on. Forget this one espacially when using the internal oscillator of a NE602/SA612!
  3. This to my point of view is the best compromise between circuit simplicity and function. You can see this way of sideband switching in my transceiver.

This is my local oscillator:

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – Local Oscillator (LO)

It is a simple Colpitts circuit where in-phase feedback and feedback voltage control are achieved  by a series of two identical capacitors. A simple switch,  a capacitor (90pF max.) and a coil (4 to 8uH max.) that are either connected to the base of the transistor via the 9MHz crystal determine the sidband freqeuncy of the oscillator. Signal is taken out via the collector.

The Receiver

The receiver will be presented step-by-step starting with the front end stage:

The RF Preamp

This stage is connected to the antenna relay. It provides an amount of basic amplification for the antenna signals. But that is not the main purpose. Noise figure improves significantly if you use a stage with low inherent noise. Thus a dual-.gate MOSFET is installed here. This semiconductor is also used to control stage gain because gate 2 of the MOSFET is connected to the AGC chain of the transceiver. About 12 dB gain swing are possible here. Stage gain is about 15dB.

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – RX PREAMP

Note the position of the primary and secondary windings of the input and the output coil. To avoid self-oscillation the output (drain) of the MOSFET is connected to the untuned part of the LC circuit. Coils must be shielded and should be equipped with ferrite heads (in the photo the piece in left bottom corner).

The Receiver’s Mixer

In this stage also a dual-gate MOSFET is used. This type of mixers provides good capabilities to cope with high signal levels without producing unwanted signals (high IMD3), gives some dB of gain and is low-noise also.

One “disadvantage”, if you want to say so, is that it needs a little bit of higher VFO drive (about 2 to 3 volts pp). Gate 2 bias is generated via the voltage drop on the source line. The tuned circuit in drain line is adjusted to the desired interfrequency. See the schematic for the exact winding data and parallel capacitor.

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – RX MIXER

The SSB Filter section

Transmitter and receiver share the same SSB filter in this transceiver. So some sort of switching is recommended even if circuits exist that go without one. I used a high quality relay made by Teledyne that I bought in a 10-piece bundle for low price (1€ each!) via a well-known internet marketplace. Caution: Some SMD-relay I tested prior to building this rig were disastrous concerning signal isolation between terminals. To avoid any disappointment or frustration  I recommend testing a relay before you finally install it.

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – SSB FILTER

All connections to the rest of the circuit must be made with shielded cable. I found an interesting alternative: I sometimes design my own very thin shielded “cable” with brass tubing (1 mm inside diameter) where I put insulated cable inside. The brass tube is connected to GND on the Veroboard. You can not bend these tubes but longer lines can be interrupted for a short piece so that the “bend” can be made by putting two parts of tubing in 90° degree angle for example.

The IF Amplifier

This one might look familiar to you. It is a simple “remake” of the front-end stage. The one remarkable thing is the secondary of the output transformer. This coil has 4 windings (prim. 16 turns). The secondary is center tapped (2 + 2 turns). This is because the product detector (SSB demodulator) has a symmetric input. Very important in this stage is the 100uF capacitor in VDD line. This cap prevents the stage from AF resonating and self-oscillations on the VDD line and makes the receiver much more “quiet”.

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – IF AMP

The SSB demodulator

This stage is probably the most “old-school” part of the whole transceiver. It uses an old CA3028A differential amplifier as mixer circuit:

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – PROD DET

You won’t be able to buy large amounts of this IC anymore. And if you get one, the prices are close to or beyond a rip-off. But there is an alternative. You can build your own “IC”. Watch this page where all the information you need is provided!

Signal input goes to the paired transistors forming the amplifier stages. LO is fed into the line transistor that is used to set the current of the differential amplifier thus providing a switching and therefore superposition of the two signals.

The output circuit is made of an audio transformer formerly used in the final audio amp  of old AM radios (coil resistance is about 300Ω each side). The 2.2nF capacitors eliminate remainders of the rf signals and “ground” the terminals of the AF transformer.

Audio amplifier

This final receiver part consists of two stages: An audio preamplifier with a bipolar transistor and a final amp with a TBA820M integrated circuit.

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – AUDIO AMPS

The two caps 0.22 and 0.1uF determine how the higher frequency components in the audio signal are cut off. The higher the total value the more the higher frequency components of the audio signal will be limited due to the equation XC=1/(2*PI*f*C).

Tr1, which is a universal purpose NPN transistor, provides high gain. Thus a 10k resistor is installed to form a voltage divider with the audio gain potentiometer.

In the final stage I use a TBA820M ic (8-pin DIL version). This one is more linear than the well-known LM386 that you usually can find in this place and it is not so prone to self-oscillation. The cap aside the 100uF in the top left corner of the schematic is not marked, its value is 0.1uF.

Loudspeaker impedance is 8Ω.

The AGC

Automatic gain control makes listening to signals much more comfortable. AGC voltage is audio derived, like in my other transceivers. The circuit also is nearly the same:

“Old school” SSB transceiver for 14MHz by DK7IH (2018) –

Due to the very high gain of the product detector this stage is directly connected to this circuit and not to the AF preamp. A potentiometer is used to set the threshold of the AGC onset.

Next stage is a simple audio preamplifier followed by a “Greinacher Circuit” serving as voltage doubler and providing DC voltage proportional to the audio signal level. A dc amplifier with another NPN transistor lets its collector voltage drop as soon as it is fed with significant dc input voltage. Thus this voltage decreases and so it can be used to control gate 2 of the MOSFETs in the various receiver stages that are equipped with tetrodes.

The S-Meter is connected to the emmitter of the final transistor. If conductivity in the transistor rises, the emmitter becomes more positive and the S-Meter needle is deflected proportionally. The 220Ω potentiometer in the emmitter line must be set in accordance to the respective S-Meter you are using. One shortcoming should be mentioned: If you have a not so sensitive meter then the value of the pot can be set to nearly 100Ω or above. This will prevent the collector from dropping to nearly 0V in case there is a strong signal and hence reduce the maximum dB you can get from the AGC chain.

The Transmitter

The transmitter section is designed for an output level of about 20 watts and uses 4 stages all equipped with bipolar transistors. The last stage is a push-pull stage, the 3 low-power stages are single ended. I prefer push-pull for the last stage (if possible)  because this circuit inherently does not create even harmonics thus simplifying output filtering.

The first parts of the transmitter to be shown here are the microphone amplifier, the SSB-generator and the TX mixer:

“Old school” SSB transceiver for 14MHz by DK7IH (2018) -MIC AMP

The mic amp is simple but provides enough gain and good linearity for using an old-style dynamic microphone. It works in common emmitter mode and has gain of about 15 to 20 dB.

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – SSB GEN

The audio signal amplified by the microphone amplifier is fed into PIN1 (Input 1) of an NE602/SA612 mixer IC which is the simpliest way to generate a DSB signal with a Gilbert cell. LO input is fed to PIN7 and should be in the range of 200 to 300mVpp. Thus a 12pF cap has been installed to limit LO voltage going to input at PIN7.

Carrier suppression is around 45dB when LO offset frequency is correctly set for each of the two sidebands and LO voltage is not much higher than the 300mVpp mentioned before.

The DSB signal produced by this mixer goes on to the SSB filter relay and filter that has been described before. The use of shielded cable is mandatory, too.

The TX Mixer

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – TX MIXER

You won’t be able to recognize many differences if you compare this TX mixer to the DSB generator. In fact, there are none.

The 14MHz Band Pass Filter

Next is the band pass filter that consists of 2 coupled tuned LC circuits for 14MHz. They are also wound on TOKO style coil formers. Data can be found in the schematic underneath.

It is important to also install the ferrite heads that are provided with most of the coil formers and to use the shield “metal cans” that are also standard for these coils. This is to prevent stray coupling of rf energy into the first stage of the power amplifier strip and therefore preventing self-oscillation of the transmitter strip.

For proper adjustment set the transmit frequency to about half of the frequency swing ((i. e. to about 14.200 kHz) and tune for max. output.

“Old school” SSB transceiver for 14MHz by DK7IH (2018) -BPF

If you modulate with a two-tone signal to the mic amp you should see about 500mVpp by the output of the BPF when the chain is fully driven.

The Preamplifier

We start with the low power end of the power transmitter section. A bipolar rf type transistor is the center part of this stage.

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – TX PREAMP

This one is a standard circuit and has been “trimmed” for maximum linearity in order to reduce distortion to a minimum (which is also true for the following stages). You can see the well understood 2 master ways of achieving max. linearity in an amplifier stage:

  • Negative feedback between collector and base (i)
  • Emmitter degeneration (II)

Explanation:

i) The first measure goes along with the 2.7kΩ resistor between collector and base of the transistor. This resistor provides positive dc bias voltage to the base and leads 90° out-of-phase ac voltage to the transistor’s input. This reduces gain and therefore distortion. But due to the fact that the whole transmitter strip has plenty of gain, this loss in gain is not a serious problem.

ii) The 10Ω resistor in the emmitter line is not bypassed by a capacitor. This stabilizes the circuit. When the current through transistor increases the emmitter voltage will rise (according to Ohm’s law) and the voltage between collector and emmitter drops. This reduces voltage difference between base and emmitter and hence also reduces gain.

The coupling to the next stage is done by a capacitor of 0.1uF. This causes some impedance mismatch. But that is as well not a big problem because the gain reduction here helps to prevent the whole transmitter from unwanted oscillations by diminishing overall gain.

The Predriver

This stage is somehow a copy of the stage before but allows more current to flow through the stage. It is also operated in class “A” mode and uses the same methods to maximize linearity like the preamp stage.

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – TX PREDRIVER

You can use a 2N3866 transistor here which is available. But any other rf power transistor for driver stages (2SC1973 etc.) will also do the job well. A heatsink is recommended even if stage current ist not that high. T1 should be a toroid, a “pig-nose” core in this place to my experience is not the best choice. The 10uH RFCs are ready made ones but you can also wind 20 turns of 0.4mm enameled wire to a FT37-43 toroid core.

RF output of this stage could be measured as 100mW into a 50Ω load.

The Main Driver

This stage has an old 2SC2078 CB transistor and is operated in class “AB” mode. An alternative could be a 2N3553 that is available on ebay for example. A heatsink is neccessary for whatever type you use.

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – TX DRIVER

Correctly set bias for “AB” operation is ensured by the 1kΩ resistor from VDD to the bias circuit. The 1kΩ resistor limits the current whereas the diode works as a stabilizing element (thermistor). It must be connected directly to the case of the transistor ensuring good thermal contact. If the temperature of the devices rises the resistance of the diode will decrease. Hence current through the diode increases thus reducing the part of the current that can pass through the base-emmiter line of the 2SC2078. Quiescent current is stabilized and thermal runaway is prevented.

The rf output is uncommonly terminated with a low-pass-filter. This is because I first intended to build the transceiver for an output level of about 4 watts. But then I had the idea that the space still available on the veroboard could be used by another amplifier definitely leaving the QRP power level. So I left the circuit how it first was and just added the final amplifier stage.

Output of this driver stage now ist set to 1 watt into a 50Ω resistor.

The Final RF Amplifier

Now let’s go for the power machine in this transceiver:

“Old school” SSB transceiver for 14MHz by DK7IH (2018) – TX POWER AMP

2 rf power transistors 2SC1969 by Eleflow provide up to 20 watts of rf power. Bias for such a high power stage can not be set by a simple resistor. Here a line transistor (BD137) serves as current control. Diodes D1 and D2 (1N4002 or equ.) follow the same purpose like the single one in the stage described before. They must be mounted with excellent thermal contact to each of the 2 power devices which ensures secured protection against thermal runaway. The transistors also must be connected to a large heatsink. I use Aluminium metal strips (2mm thickness) to connect them to the back wall of the cabinet.

RF is fed into the power transistors via a network of 8.2Ω  resistors and two 22uH rf chokes that seperate the rf line from the dc bias line letting only dc pass. This method makes construction of the input transformer easier. Winding ratio is 4 turns primary, 2 turns secondary. This is because the input impedance of the stage ist fairly low (aorund some ohms).

The output transformer is a homemade “pig-nose” of 6 toroids FT50-43, where 3 toroids are stacked (using 2-component glue) and 2 of these stacks are glued in parallel (see picture at the end of this text for details!). Winding ratio is  1 + 1 (primary center tapped) to 4 on secondary.

Quiescent current of this stage should be set to about 100mA.

A low-pass-filter terminates this stage and is connected to the antenna relay.

In addition you find a section to measure rf power. This is again the so called “Greinacher-Circuit” which doubles the voltage and serves as a charge pump. The dc output of this circuit directly leads to the S-Meter indicating output power of the transmitter.

Performance

First the spectrum of the signal with the transmitter fully driven to 20 watts output power with a two-tone-signal:

IMD3 is about 28dB below signal peak which I think is acceptable.

Amplitude diagram is as follows:

2-Tone-Signal Amplitude
2-Tone-Signal Amplitude

Max. radio frequency voltage is 90.4Vpp which calculates to about 20 watts of rf power (P=(Vpp/(2*SQR(2)))²/50Ω).

Power switch board and RIT voltage

A 12V relay with two pairs of contact sets is the heart of this unit. DC power is lead to TX, RX and permanent supply via the respective power lines.

RIT voltage generation is a little bit more complicated. When the RIT switch is in “OFF” position, RIT voltage always is taken from the fixed voltage divider that is formed of the two 4.7k resistors either when on receive or transmit mode.

If RIT is “ON” then there are two possibilities: When on receive mode, RIT voltage is gained from the 10k lin. potentiometer in the front panel. When on transmit mode RIT again is taken from the fixed voltage divider.

There is also a false polarity protection diode. This can be any silicon type with max. current >= 5 A.

"Old school" SSB transceiver for 14MHz by DK7IH (2018) - TR SWITCH RIT
“Old school” SSB transceiver for 14MHz by DK7IH (2018) – TR SWITCH RIT

Mechanics

The construction is sandwich style made of 2 layers:

“Old school” SSB transceiver for 14MHz by DK7IH (2018) -Inside View – VFO, LO, RX, SSB generator, TX mixer etc.
“Old school” SSB transceiver for 14MHz by DK7IH (2018) -Inside View TX and switchboard

OK, that’s the story. Thanks for joining me on the trip to the past! 😉

73 de Peter.

Shrinking a QRP transceiver to (nearly) the size of a pack of cigarettes

The challenge started some weeks ago, when John, ZL2TCA, commented to this blog

you next challange is to build a rig into a cigerette packet size case.

My problem: I don’t smoke, have never smoked and probably never will. 😉 But I have a new transceiver for 20 meters, that might come close to the dimensions of a pack of “cancer sticks”.

DK7IH pocket sized qrp transceiver 20-4 a
DK7IH pocket sized QRP transceiver 20-4

The transceiver is nearly the same circuit as applied with the “Micro 20-III” but uses a single ended final amplifier instead of a push-pull circuit. I hope to find time the next days to publish an article on this rig featuring full description of the radio. Currently I’m in the IOTA contest and working stations from all over Europe.

73 de Peter

Reviewing and improving the semi-automatic antenna tuner

Foreword

When I built the semi automatic tuner two years ago I did not take into account some possible shortcomings the device could suffer from. The first of these I noticed when I exceeded power levels of about more or less than 50 watts. In some cases there was rf incoupling leading the microcontroller to fail so that the relay setting was invalid for the given combination of frequency and antenna. The next point was that the algorithm to set the capacitor was far from being optimized. And, as I found out, the maximum inductance I had inlcuded was far too high. On the other hand the lowest indcutance was to big to ensure very fine tuning. So this was revised, too by stepping the inductors more carefully.

The consequence then was a complete reconstruction of the tuner trying to avoid the problems from rf stray energy being coupled into the microcontroller and improving the software and hardware.

Abstract

Thsi article describes a microcontroller driven semi-automatic antenna tuner capable of handling power levels up to 150 watts. The device is a low pass filter tuner manually tuned by setting the optimized L/C combination by hand and then storing the values into the EEPROM of the mictrocontroller to recall them later (seperately for each band from 80 to 10 meters including WARC bands). The tuner ist designed to couple long wire antennas (i. e. longer than half a wavelength) in the frequency range from about 3 MHz to 30 MHz. The antennas can be balanced or non-balanced.

The device uses 7 coils wound on iron powder toroids and a variable capacitor controlled by a motor with a reduction drive and a a device that detects the current turning angle of the rotator.

Also integrated you will find a measurement section to give the current standing wave ratio and put out this on the display.

General layout

The tuner consists of two main parts:

  • The microcontroller unit
  • The RF unit containing the tuning coils and the variable capacitor.

Let’s see the schematic first:

DK7IH semi automatic antenna tuner V2 (schematic)
DK7IH semi automatic antenna tuner V2 (Schematic FULL SIZE)

Circuit description

Starting on the left side, you see the microcontroller unit equipped with an 8-bit AVR microcontroller (ATMega16 or similar). The user interface is very simple and made of 6 push buttons. These are connected to GND via switchable individual resistors leading to the ADC3 input of the cntroller. The pull up resistor for ADC3 is activated thus forming a voltage divider and thus an individual ADC value for each push button to be recognized by the software. This is done because it saves control lines and controller ports to a wide extent.

The LCD is a two line 16 characters LCD.

Output ports are connected to ULN2003 driver ICs. These ICs contain a driver capable to handle up to 30V DC including a clamp diode so this IC can drive mtors and relays directly. There are capacitors of 0.1uF connected to the port lines to minimize rf coupling effects.

The RF board is made of 7 coils wound on T68-2 toroids with the inductances given in the schematic:

  • L1: 0.1uH: 4 turns,
  • L2: 0.25uH: 7 turns,
  • L3: 0.5uH: 9 turns,
  • L4: 1uH: 13 turns,
  • L5: 2uH: 19 turns,
  • L6: 4uH: 26 turns,
  • L7: 8uH: 37 turns.

Wire gauge is 0.4mm. The inductors are shortened by a 12V relay each if neccessary. So you can (by binary calculation) set any value from 0.1 uH to close to 16uH.

An output transformer is used to give a balanced out for e. g. doublet antenns. It is 10 turns bifilar on a 2.5 cm toroid ferrite core of No. 43 material. If you use a non-balanced antenna you can leave out this transformer.

The capacitor in my construction is a 200pF max. butterfly capacitor with air as dielectric.  The advantage of a butterfly type is that it needs only 90° angle to turn it from minimum to maximum capacity. The motor (a 5V dc version) is connected via a 240:1 gear drive by TAMIYA. The motor is pulse driven so it can be directly connected to 12 Volts with running danger to damage it.

The drive has two outlets providing one axle at each side of the drive. To one of the axles I connected the capacitor, the other one connects to a potentiometer to report the current swing angle to the microcontroller. This allows precise feedback of the capacitor’s current position which is essential for setting it to the desired value. The value of this variable resistor does not really matter since it is only a simple voltage divider. Anything between 1k and 10k should fit. Make sure that you use a piece that is easy to turn to minimize friction. To connect the axles I used PVC tubing with an inside diameter of 3 mm.

Also included is a measurement section to give the user a current reading of VSWR. The coupler can be anything you should regard as proper. I used a strip power coupler from an old CB radio. But other systems should also work.

To compensate losses when tuning low frequencies I added a DC amplifier based on an operational dual system amplifier (LM358). There is no means to reduce sensitivity. This is done by regulating the input power of the driving transmitter. The software will give you a readout for FWD and REF power in the range from 0 to 999. Tune to maxmimun FWD and minimum REF energy and everything will be alright! 😉

Practical aspects

To minimize RF couping into the microcontroller I seperated the control board from the RF board by putting it on a seperate veroboard with an aluminium shielding underneath:

DK7IH semi automatic antenna tuner V2 (inside view)
DK7IH semi automatic antenna tuner V2 (inside view)

The RF board is mounted to the bottom of the case, here with dismantled controller board. The controller board is sited on top of the package.

DK7IH semi automatic antenna tuner V2 (inside view)
DK7IH semi automatic antenna tuner V2 (inside view)

On the right side there is the variable butterfly capacitor, on the board, centered you can see the motor plus the reduction drive. On the left side of this there is a small potentiometer that forms the turning angele detector. To connect this to the ADC of the controller it is highly recommended to use shielded cable!

To come to an end, here is the deveice positioned under the roof window of my shack directly connected to the ladder line:

DK7IH semi automatic antenna tuner V
DK7IH semi automatic antenna tuner V”

Thanks for reading and enjoy your radio! 73 de Peter (DK7IH)

The software

(Apology for having some comments in German. I have been using this code for centuries! 😉

/*****************************************************************/
/*              Antennatuner with ATMega 16 V2                   */
/*  ************************************************************ */
/*  Microcontroller:  ATMEL AVR ATmega16, 8 MHz                  */
/*                                                               */
/*  Compiler:         GCC (GNU AVR C-Compiler)                   */
/*  Author:           Peter Rachow DK7IH                         */
/*  Last cahnge:      2018-07-27                                 */
/*****************************************************************/

/*    PORTS */
// O U T P U T 
// LCD 
// RS      = PD2
// E       = PD3
// D4...D7 = PD4..PD7

//Coil relays: PC0...PC6
//Motor drive on/off: PD1
//Motor direction relay: PD0
//Extra capacitor 200pF: PC7 (not yet!)

//I N P U T
//ADC0: SWR-Meter 0
//ADC1: SWR-Meter 1
//ADC2:
//ADC3: Keys
//ADC4: Potentiometer for Capacitor position

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/eeprom.h>
#include <util/delay.h>

int main(void);

#define LCD_INST 0x00
#define LCD_DATA 0x01

#define MAXCAP 220
#define CAPDELAY 10
#define MAXBANDS 7

void lcd_write(char, unsigned char);
void set_rs(char);
void set_e(char);
void lcd_init(void);
void lcd_cls(void);
void lcd_line_cls(int);
void lcd_putchar(int, int, unsigned char);
void lcd_putstring(int, int, char*);
int lcd_putnumber(int, int, long, int);
void lcd_display_test(void);

//BAND DISPLAY & MISC
void show_band(int);
void show_meter(int, int, int);
void define_chars(void);

//Coils & Cpas
void set_coils(int);
void set_cap(int);
void rotate_cap(int);
int get_cap(void);

//ADC
int get_adc(int);
int get_keys(void);

//Delay
void wait_ms(int);

//String
int int2str(long, int, char *, int);
int stringlen(char *);


long runsecs = 0;

/**************************************/
/* Funktionen und Prozeduren fuer LCD */
/**************************************/
// LCD 
// RS      = PD2
// E       = PD3
// D4...D7 = PD4..PD7

/* Ein Byte (Befehl bzw. Zeichen) zum Display senden */
void lcd_write(char lcdmode, unsigned char value)
{
    int x = 16, t1;
	
    set_e(0); 

    if(!lcdmode)
	{
        set_rs(0);    //RS=0 => INST
	}	
    else
	{
        set_rs(1);    // RS=1 => DATA
	}	

    wait_ms(4);
	
    //Hi nibble
    set_e(1);
	for(t1 = 0; t1 < 4; t1++)
	{
	    if(value & x)
	    {
	       PORTD |= x;
	    }
        else	
	    {
           PORTD &= ~(x);
	    }  
		
		x <<= 1;
	}	
	set_e(0);
	
	x = 16;

	// Lo nibble
	set_e(1);
	for(t1 = 0; t1 < 4; t1++)
	{
	    if((value & 0x0F) * 16 & x)
	    {
	       PORTD |= x;
	    }
        else	
	    {
           PORTD &= ~(x);
	    }  
		
		x <<= 1;
	}
    set_e(0);

}

//RS
void set_rs(char status) //PD2  
{
    if(status)
	{
        PORTD |= 4;
	}	
    else
	{
	    PORTD &= ~(4);
	}	
}

//E
void set_e(char status)  //PD3
{
    if(status)
	{
        PORTD |= 8;
	}	
    else
	{
	    PORTD &= ~(8);
	}	
}

/* Ein Zeichen (Char) zum Display senden, dieses in */
/* Zeile row und Spalte col positionieren           */
void lcd_putchar(int row, int col, unsigned char ch)
{
    lcd_write(LCD_INST, col + 128 + row * 0x40);
    lcd_write(LCD_DATA, ch);
}


/* Eine Zeichenkette direkt in das LCD schreiben */
/* Parameter: Startposition, Zeile und Pointer   */
void lcd_putstring(int row, int col, char *s)
{
    unsigned char t1;

    for(t1 = col; *(s); t1++)
	{
        lcd_putchar(row, t1, *(s++));
	}	
}


/* Display loeschen */
void lcd_cls(void)
{
    lcd_write(LCD_INST, 1);
}


/* LCD-Display initialisieren */
void lcd_init(void)
{
    /* Grundeinstellungen: 2 Zeilen, 5x7 Matrix, 4 Bit */
    lcd_write(LCD_INST, 40);
    lcd_write(LCD_INST, 40);
    lcd_write(LCD_INST, 40);

    //MAtrix 5*7
    lcd_write(LCD_INST, 8);

    /* Display on, Cursor off, Blink off */
    lcd_write(LCD_INST, 12);

    /* Entrymode !cursoincrease + !displayshifted */
    lcd_write(LCD_INST, 4);
	
	//4-Bit-Mode
    lcd_write(LCD_INST, 2);	
	
	lcd_cls();
}


//Write number with given amount on digits to LCD
int lcd_putnumber(int col, int row, long num, int dec)
{
    char *numstr = malloc(32);
    int l = 0;
    if(numstr != NULL)
    {
        int2str(num, dec, numstr, 16);
        lcd_putstring(col, row, numstr);
        l = stringlen(numstr);
        free(numstr);
        return l;
      
    } 
    return 0;
}


void lcd_line_cls(int ln)
{
    int t1;
	
	for(t1 = 0; t1 < 15; t1++)
	{
	    lcd_putchar(ln, t1, 32);
	}
}	
/*****************************************/
//           STRING FUNCTIONS
/*****************************************/
//INT 2 ASC
int int2str(long num, int dec, char *buf, int buflen)
{
    int i, c, xp = 0, neg = 0;
    long n, dd = 1E09;

    if(!num)
	{
	    *buf++ = '0';
		*buf = 0;
		return 1;
	}	
		
    if(num < 0)
    {
     	neg = 1;
	    n = num * -1;
    }
    else
    {
	    n = num;
    }

    //Fill buffer with \0
    for(i = 0; i < 12; i++)
    {
	    *(buf + i) = 0;
    }

    c = 9; //Max. number of displayable digits
    while(dd)
    {
	    i = n / dd;
	    n = n - i * dd;
	
	    *(buf + 9 - c + xp) = i + 48;
	    dd /= 10;
	    if(c == dec && dec)
	    {
	        *(buf + 9 - c + ++xp) = '.';
	    }
	    c--;
    }

    //Search for 1st char different from '0'
    i = 0;
    while(*(buf + i) == 48)
    {
	    *(buf + i++) = 32;
    }

    //Add minus-sign if neccessary
    if(neg)
    {
	    *(buf + --i) = '-';
    }

    //Eleminate leading spaces
    c = 0;
    while(*(buf + i))
    {
	    *(buf + c++) = *(buf + i++);
    }
    *(buf + c) = 0;
	
	return c;
}

//STRLEN
int stringlen(char *s)
{
   int t1 = 0;

   while(*(s + t1++));

   return (t1 - 1);
}


//BAND DISPLAY
void show_band(int b)
{

    char *band_str[] = {"80m", "40m", "30m", "20m", "17m", "15m", "12m", "10m"};
    lcd_putstring(0, 13, band_str[b]);
}

//Meter (max. value = 25)
void show_meter(int value, int value_old, int pos)
{
	#define MAXBLOCKS 4
    int v1, v2, v3, i1;
	
    //Clear meter (5 chars) if new value > old value
    if(value < value_old)
    {
        for(i1 = 0; i1 < MAXBLOCKS; i1++)
        {
	        lcd_putchar(1, i1 + pos * 6, 32);
        }
	}
			
	v1 = (int) value / MAXBLOCKS; //Full blocks, 5 cols each
    v2 = value - v1 * MAXBLOCKS;  //Rest
	if(v1 > MAXBLOCKS)
	{
		v1 = MAXBLOCKS;
	}
	
	if(value >= value_old)
	{
	    v3 = (int) value_old / MAXBLOCKS; //Full blocks, 5 cols each, already drawn
	}
	else
	{  
		v3 = 0;
	}	    
	
	//Full blocks	
	for(i1 = v3; i1 < v1; i1++)
	{
	    lcd_putchar(1, i1 + pos * 6, 4); 
	}
	
	//Rest
	if(i1 < MAXBLOCKS)
	{
		if(v2)
	    {
	        lcd_putchar(1, i1 + pos * 6, v2 - 1);
	    }
        else
        {
            lcd_putchar(1, i1 + pos * 6, ' ');
        }
	}
}

//PROGRAM CUNSTOM CHARS FOR S-SMETER
void define_chars(void)
{
    int i1;
	unsigned char adr = 64;
						  
	unsigned char b1[] = {0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, //S-Meter blocks 
	                      0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00,
						  0x00, 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x00, 0x00,
						  0x00, 0x00, 0x1E, 0x1E, 0x1E, 0x1E, 0x00, 0x00,
						  0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x00, 0x00,
						  	0x0,0x0,0x4,0xe,0x4,0x0,0x0, 0, //+
						  	0x4,0xe,0x4,0x0,0x4,0xe,0x4,0, //++
						  0x00, 0x01, 0x02, 0x12, 0x0A, 0x04, 0x00, 0x00  //Tick
						  
						  };					  
	//Dummy operation!
	lcd_write(LCD_INST, 0);
	lcd_write(LCD_DATA, 0);

    //Load data into CGRAM
	for(i1 = 0; i1 < 64; i1++)  
	{
	    lcd_write(LCD_INST, adr++);
		lcd_write(LCD_DATA, b1[i1]);
	}
}		


/////////////////////////////////////
//
// C O I L S & C A P S
//
/////////////////////////////////////
void set_coils(int pattern)
{
    int unsigned t1, x = 1, p = pattern;
    
    //Inductance in uH  * 100
    int i0[] = {10, 25, 50, 100, 200, 400, 800}, i1 = 0;
    
    //lcd_putstring(0, 10, "    ");
    //lcd_putnumber(0, 10, p, -1, -1, 'l', 0);
    
    for(t1 = 0; t1 < 7; t1++)
    {
		if(p & x)
        {
            PORTC &= ~(x);      
            i1 += i0[t1];
        }
        else
        {    
            PORTC |= x;
        }
        x <<= 1;
     }       
     lcd_putstring(0, 0, "       ");
     lcd_putstring(0, lcd_putnumber(0, 0, i1, 2), "uH");
}

void set_cap(int cap)
{
               
    if(cap < 0 ||cap > MAXCAP)
    {
		return;
	}
	
	lcd_putstring(0, lcd_putnumber(0, 7, cap, -1) + 7, "pF");
	wait_ms(500);
	/*	
    lcd_putnumber(0, 0, get_adc(4), -1, -1, 'l', 0);
    lcd_putnumber(0, 8, cap, -1, -1, 'l', 0);
    lcd_putnumber(1, 0, (int) adc0, -1, -1, 'l', 0);	
    */       
    
    while(get_cap() > cap)
    {
		rotate_cap(0);
		lcd_putstring(0, 7, "      ");
        lcd_putstring(0, lcd_putnumber(0, 7, get_cap(), -1) + 7, "pF");
        if(get_keys() == 5 || get_keys() == 6) //Quit if band QSY key is pressed
        {
			return;
		}	
    }
    
    while(get_cap() < cap)
    {
		rotate_cap(1);
		lcd_putstring(0, 7, "      ");
        lcd_putstring(0, lcd_putnumber(0, 7, get_cap(), -1) + 7, "pF");
        if(get_keys() == 5 || get_keys() == 6) //Quit if band QSY key is pressed
        {
			return;
		}	
    }
        
    

}

//Measure real cap val from sensor
int get_cap(void)
{
	double val = (double) (get_adc(4) - 384) * MAXCAP / 256;
	
	return (int) val;
}
	
void rotate_cap(int direction)
{
	if(direction)
	{
		PORTD &= ~(1); //RelayOFF      
	}
	else
	{
	    PORTD |= 1;   //Relay ON	
	}	
	
	PORTD |= 2;   //Motor ON
	wait_ms(CAPDELAY);
	PORTD &= ~(2);  //Motor OFF      
	wait_ms(CAPDELAY);
}	


ISR(TIMER1_OVF_vect)   	   // Timer1 Überlauf
{ 
    runsecs++;
    TCNT1 = 57724;	  
} 

//***************************************************
//                      ADC
//***************************************************

int get_adc(int adcmode)
{
    int adc_val = 0;
	
	
	ADMUX = (ADMUX &~(0x1F)) | (adcmode & 0x1F);     // Kanal adcmode aktivieren PA0=TUNE
    wait_ms(3);
	
    ADCSRA |= (1<<ADSC);
	wait_ms(3);
	
	adc_val = ADCL;
    adc_val += ADCH * 256;   
	
	while(ADCSRA & (1<<ADSC));
	
	return adc_val;
}	

//Read keys via ADC0
int get_keys(void)
{

    int key_value[] = {18, 22, 29, 43, 74, 132};  
       	
    int t1;
    int adcval = get_adc(3);
        
    //TEST display of ADC value 
    /*
    lcd_cls();
    lcd_putnumber(0, 0, get_adc(3), -1, -1, 'l', 0);
    return 0;    	
    */
    
    for(t1 = 0; t1 < 6; t1++)
    {
        if(adcval > key_value[t1] - 2 && adcval < key_value[t1] + 2)
        {
			 return t1 + 1;     
        }
    }
    
    return 0;
}

/***************************************************/
/* Wartezeit in Millisekunden bei fck = 8.000 MHz  */
/***************************************************/
void wait_ms(int ms)
{
  int t1, t2;

  for(t1 = 0; t1 < ms; t1++)
    for(t2 = 0; t2 < 137 * 8; t2++)
      asm volatile ("nop" ::);
}

int main()
{
	int t1;
	int key;     //Keystroke
	int adc_val; //Value of ADC
    int i = 65;  //L
    int band = 0;
    int l1 = 0, c1 = 0;
    
    //Standard values for DK7IH antenna
    //L=      - (128), 8uH(64), 4uH(32), 2uH(16), 1uH(8), 0.5uH(4), 0.25uH(2), 0.125uH(1)
    int std_l [] = {65, 33, 12, 8, 6, 4, 3, 2, 1};
    int std_c [] = {172, 41, 75, 110, 88, 55, 33, 17};
    
    //Meter data
    int s0, s1;
    int s0_old = -1, s1_old = -1;
    int s0off, s1off;
    int loopcnt0 = 0;;
        
    int ok = 0;
    long runsecs2 = 0;
	
	/* Set ports */
    /* OUTPUT */
    DDRB = 0x1F; //Relays 1.55 of cap switching
    DDRC = 0x7F; //Relays for coils             
    DDRD = 0xFF; //LCD data on PD4...PD7
                 //LCD RS:PD2, E:PD3
                 //PD0, PD1: Relay 6 and 7 of cap switches
    
    PORTA = 0x08; //Pullup resistor for keys' input   
    
    //Display
    lcd_init();
	wait_ms(500);
	lcd_cls();
	lcd_putstring(0, 0, " DK7IH Antenna");				
	lcd_putstring(1, 0, " Tuner Ver. 2.0");				
	wait_ms(500);
	lcd_cls();
			
	//Watchdog abschalten
	WDTCR = 0;
	WDTCR = 0;
    	
	//ADC initialisieren
    ADMUX = (1<<REFS0);     // Referenz = AVCC
    ADCSRA = (1<<ADPS2) | (1<<ADPS1) | (1<<ADEN); //Frequenzvorteiler 64 u. //ADC einschalten
	ADCSRA |= (1<<ADSC); //Eine Wandlung vornehmen
	while (ADCSRA & (1<<ADSC)); //Eine Wandlung abwarten
    adc_val = ADCL;
    adc_val += ADCH * 256;   //Wert auslesen
	adc_val = 0;
	
	//Timer 1
	TCCR1A = 0;                      // normal mode, keine PWM Ausgänge
    TCCR1B = (1<<CS12) + (1<<CS10) ;   // start Timer mit Systemtakt, Prescaler = /1024
	                                    //Auslösung des Overflow alle Sekunde sec.
    TIMSK = (1<<TOIE1);   // overflow aktivieren.
	TCNT1 = 57724;        //Startwert für Sekundentakt

    //Define custom chars for meter
    //define_chars();

    //Load standard data if eeprom cell empty
    for(t1 = 0; t1 < 8; t1++)
    {
		if(eeprom_read_byte((uint8_t*)(t1 * 2)) == 255)
		{ 
			eeprom_write_byte((uint8_t*)(t1 * 2), std_l[t1]);
			eeprom_write_byte((uint8_t*)(t1 * 2 + 1), std_c[t1]);
		}
	}		
				
		
    //Get recent data           		
   	band = eeprom_read_byte((uint8_t*)32);
   	if(band < 0 || band > MAXBANDS)
	{
	    band = 3;
	}
	show_band(band);
	
	l1 = eeprom_read_byte((uint8_t*)(band * 2));
	if(l1 >= 0 && l1 < 128)
	{
		set_coils(l1);
	}
	else
	{
		set_coils(65);
		lcd_line_cls(1);
		lcd_putstring(0, 0, " -x-uH");
		wait_ms(1000);
	}
	c1 = eeprom_read_byte((uint8_t*)(band * 2) + 1);	
	if(c1 >= 0 && c1 < MAXCAP)
	{
		set_cap(c1);
	}
	else
	{
		set_cap(110);
		lcd_line_cls(1);
		lcd_putstring(0, 0, " -x-pF");
		wait_ms(1000);
	}	
    sei();

    show_band(band);
    
    //Calculate 0-offset of swr meter
    s0off = get_adc(0);
    s1off = get_adc(1);
    
    for(;;) 
	{
		key = get_keys();
		if(i > 0 && key == 2)
		{
			i--;
		    set_coils(i);
		    wait_ms(50);
		}    
		
		if(i < 127 && key == 1)
		{
			i++;
		    set_coils(i);
		    wait_ms(50);
		}    
				
		while(get_cap() > 0 && key == 4) //C(-)
		{
			rotate_cap(0);
		    lcd_putstring(0, 7, "      ");
            lcd_putstring(0, lcd_putnumber(0, 7, get_cap(), -1) + 7, "pF");     
            wait_ms(100);
            key = get_keys();
		}    
		
		while(get_cap() < MAXCAP && key == 3) //C(+)
		{
			rotate_cap(1);
		    lcd_putstring(0, 7, "      ");
            lcd_putstring(0, lcd_putnumber(0, 7, get_cap(), -1) + 7, "pF");     
            wait_ms(100);
            key = get_keys();
		}    

        if(band > 0 && key == 6)         //Band (-)
        {
			band--;
		
			while(!eeprom_is_ready());
		    eeprom_write_byte((uint8_t*)32, band);
										
			lcd_putstring(1, 0, "Recalling.");
				
			l1 = eeprom_read_byte((uint8_t*)(band * 2));
			c1 = eeprom_read_byte((uint8_t*)(band * 2 + 1));

	        if(l1 >= 0 && l1 < 128)
	        {
		        set_coils(l1);
		        i = l1;
	        }
	        else
	        {
		        set_coils(65);
		        lcd_putstring(0, 0, " -x-uH");
		    }    
									
														
			if(c1 >= 0 && c1 <= MAXCAP)
			{
			    set_cap(c1);
			}	
			else
			{
				lcd_putstring(0, 7, "-x-pF");
			}	
				
			show_band(band);
			lcd_putstring(1, 0, "             ");
			while(get_keys());
		}
		
        
        if(key == 5) //Band (+)
		{
		    show_band(band);
			lcd_line_cls(1);
			
		    runsecs2 = runsecs;
			ok = 0;
			lcd_putstring(1, 0, "Waiting....");
			while(get_keys() == 5 && !ok)
			{
			    
			    lcd_putnumber(1, 10, 3 - (runsecs - runsecs2), -1);
				if(runsecs - runsecs2 > 2)
				{
				    ok = 1;
				}	
			}	
			
			if(runsecs > runsecs2 + 1) //Key has been pressed for longer than a second, new values for current band to be set
			{                          //Also store values in EEPROM 
			    while(!eeprom_is_ready());
				eeprom_write_byte((uint8_t*)(band * 2), i); 
				
			    while(!eeprom_is_ready());
				eeprom_write_byte((uint8_t*)(band * 2 + 1), get_cap());
				
				while(!eeprom_is_ready());
				eeprom_write_byte((uint8_t*)32, band);
								
	            lcd_line_cls(1);
				lcd_putstring(1, 0, "Stored.");				
				wait_ms(1000);
				lcd_line_cls(1);
			}
            else	
			{
			    if(band < MAXBANDS) //Change band 1 up
				{
				    band++;
				}
                				
				while(!eeprom_is_ready());
				eeprom_write_byte((uint8_t*)32, band);
				
				show_band(band);
				
				lcd_putstring(1, 0, "Recalling.");
				
			    l1 = eeprom_read_byte((uint8_t*)(band * 2));
				c1 = eeprom_read_byte((uint8_t*)(band * 2 + 1));
								
	            if(l1 >= 0 && l1 < 128)
	            {
		            set_coils(l1);
		            i = l1;
	            }
	            else
	            {
		            set_coils(65);
		            lcd_putstring(0, 0, " -x-uH");
		            wait_ms(1000);
		        }    
									
														
			    if(c1 >= 0 && c1 <= MAXCAP)
			    {
			        set_cap(c1);
			    }	
			    else
			    {
				     lcd_putstring(0, 7, "-x-pF");
			    }	
			    lcd_putstring(1, 0, "             ");
			}	
		}
		
		//Meter check
        if(loopcnt0++ > 10)
        { 
            
            s0 = get_adc(0) - s0off;
            s1 = get_adc(1) - s1off;
            
            if(s0 != s0_old)
            {
				if(s0 > 999)
				{
					s0 = 999;
				}	
				lcd_putstring(1, 0, "FWD:    "); 
				lcd_putnumber(1, 4, s0, -1);
				s0_old = s0;
			}
			
			if(s1 != s1_old)
            {
				if(s1 > 999)
				{
					s1 = 999;
				}
				lcd_putstring(1, 8, "REF:     "); 
				lcd_putnumber(1, 12, s1, -1);
				s1_old = s1;
			}	
            
            loopcnt0 = 0;
        }    
        
	}
	return 0;
}