Nokia QVGA TFT LCD for the Arduino Mega. Graphics Library (part 2 of 2)

In part 1 of this two part series I presented the hardware design and build for the Nokia 6300 TFT that shows how we can connect it directly to the external memory interface of the Arduino Mega and that by doing so we achieve the fastest possible interface between the TFT and the Arduino MCU.

Now the driver software has been updated to support the 2.4″ Nokia N82 LCD that I have reverse engineered. Everything that you can do on the 6300 screen, you can now also do on the N82.

A TFT with the fastest transfer times possible on an Arduino deserves a software library to do it justice, and that’s what I hope to provide here. The software library provides the following functionality:

  • 24 bit bitmap graphics with LZ compression.
  • JPEG decoding support for images in flash or received over the serial port.
  • Fixed-width and proportional fonts. Four supplied in the library and an almost limitless amount available online.
  • Rectangles, filled, gradient filled and outlined.
  • Ellipses, filled and outlined.
  • The fastest available line drawing algorithm.
  • Single point plotting.
  • Character terminal with hardware scrolling.
  • PWM backlight driver.
  • Low power (sleep) mode.

The library is implemented as a set of C++ templates. This design decision was taken to maximise the performance of the library as much as possible by isolating the choices that you make at design-time, such as the colour depth and orientation of the LCD and making those compile-time template parameters. This means that entire swathes of code simply do not get compiled making your program as fast and as small as it can be.

To take a contrived example, a traditional library might have a function coded as follows:

uint16_t getWidth() {
  if(_orientation==LANDSCAPE)
    return 320;
  else
    return 240;
}

Note that the code for the design-time decision regarding the LCD orientation is there at runtime even though it’s completely redundant. Contrast this with the template library which might have two function specialisations that look like this:

template<>
uint16_t Nokia6300::getWidth<LANDSCAPE> {
  return 320;
}

template<>
uint16_t Nokia6300::getWidth<PORTRAIT> {
  return 240;
}

When the code is compiled the template specialisation for the orientation that you are not using simply never happens and the optimiser is presented with a trivial function that returns a constant. This is certain to be inlined by the optimiser meaning that the entire function call never happens at all! This design pattern is prevalent throughout my entire library.

Anyway, you can forget the theory and there’s no need to be nervous about the sometimes obscure syntax that comes with templates because I take care of all that and expose only a very small number of simple types that you interact with.

Let’s walk through the library learning by example. Hopefully these examples provide cut-and-paste code that you can use in your own programs.

All of these examples are based around the popular and easy to use Arduino prototyping IDE. Advanced users will find that the library works just as well outside the IDE. My own development environment consists of avr-gcc 4.7.0, Eclipse Indigo and avr-libc 1.8.0 so compatibility with the most advanced toolchain to date is assured.

Library Installation

This library requires that you are using at least version 1.0 of the Arduino IDE.

Download the library zip file from my downloads page and unzip it into your Arduino libraries directory. For me, that directory is C:\Program Files (x86)\arduino-1.0.1\libraries. Adjust that pathname to reflect where you installed the Arduino IDE on your computer.

When you’re finished you should have a new xmemtft directory in the Arduino libraries directory. All of the demos presented below can be accessed under the Arduino File -> Examples -> xmemtft menu.

A very basic sketch

The following sketch shows the bare minimum that you need to do in order to initialise the LCD ready for work.

#include "Nokia6300.h"

using namespace lcd;

typedef Nokia6300_Portrait_16M TftPanel;
//typedef Nokia6300_Landscape_16M TftPanel;
//typedef Nokia6300_Portrait_262K TftPanel;
//typedef Nokia6300_Landscape_262K TftPanel;

TftPanel *tft;

void setup() {
  tft=new TftPanel;
}


void loop() {
  // use the panel here
}

All you need to do is uncomment the typedef line that corresponds to how you want to use the TFT in your project. Select a colour depth and orientation and you’re good to go. The class constructor for the panel object will take care of powering up the device and sending the initialisation sequence.

The entire library is contained within a namespace called lcd. For code-clarity all these examples will import lcd into the global namespace with a using namespace lcd statement. Advanced users will know the reasons why they don’t want to do this in a real project.

Typical use case with backlight

Normally you’re going to want a backlight and you’ll want to clear down the TFT before you use it. Here’s how.

#include "Nokia6300.h"

using namespace lcd;

typedef Nokia6300_Portrait_16M TftPanel;

TftPanel *tft;
DefaultBacklight *backlight;

void setup() {

  // create a backlight controller and use it
  // to switch the backlight off
  
  backlight=new DefaultBacklight;
  backlight->setPercentage(0);

  // reset and initialise the panel

  tft=new TftPanel;

  // clear to black

  tft->setBackground(ColourNames::BLACK);
  tft->clearScreen();

  // fade up the backlight to 100%
  // in 4ms steps (400ms total)

  backlight->fadeTo(100,4);
}


void loop() {
  // use the panel here
}

In the above example we show how to initialise the TFT and switch on the backlight. When the TFT is being initialised it contains random data. This is ugly for the user to see so we begin by ensuring that the backlight is off which makes the data on the panel invisible. We then initialise the TFT, clear down the display to a solid colour (black in this case) and then fade up the backlight smoothly.

  backlight=new DefaultBacklight;

This is the line that creates the backlight and initialises it to zero brightness (off). The brightness is expressed as a percentage in the range 0..100. You must connect PWM pin #2 to the EN pin on the board. You can change both the PWM pin number and the default starting brightness percentage by changing how you declare and construct the Backlight class. See the source code for details.

Note the use of the ColourNames namespace. Take a look at ColourNames.h to see the full set of available names. The names correspond to the X11 colours.

Of course you can specify your own colour by supplying the hex value in the form 0xRRGGBB.

Drawing rectangles

Rectangles can be drawn as an outline in the foreground colour or filled with either the background or foreground colour. Here’s an example that shows all of that.

To keep the code samples concise and focused on the example I’m going to show only the loop() function. You can take the above example code that demonstrates the backlight initialisation and just paste in the code for loop().

void loop() {
  
  Rectangle rc;
  
  // draw a red border around the display
  
  rc.X=rc.Y=0;
  rc.Width=tft->getXmax();
  rc.Height=tft->getYmax();

  tft->setForeground(ColourNames::RED);
  tft->drawRectangle(rc);

  // fill a rectangle in the center with blue
  
  rc.X=20;
  rc.Y=20;
  rc.Width=tft->getXmax()-40;
  rc.Height=tft->getYmax()-40;

  tft->setForeground(ColourNames::BLUE);
  tft->fillRectangle(rc);
  
  // erase a central rectangle from the blue one
  
  rc.X+=40;
  rc.Y+=40;
  rc.Width-=80;
  rc.Height-=80;
  
  tft->setBackground(ColourNames::BLACK);
  tft->clearRectangle(rc);
  
  // the end
  
  for(;;);
}

Unfortunately TFTs photograph and film very badly indeed. I’ll try my best but the images and videos on this page are not representative of the actual picture quality that your eye perceives. Bands and pattern effects seen in the photographs are not present on the actual screen and the colours are a lot more vivid – the rectangle that appears cyan in the picture below is actually a deep solid blue.


The rectangle demo

See how easy it is to draw on to the display? The graphics library remembers two colours, a foreground and a background colour. Most operations will use the foreground colour but some, such as clearRectangle will use the background colour.

The Rectangle object is used to define the co-ordinates of the rectangle on the display. For clarity I’ve shown explicit initialisation of the members but there are constructors that allow you to initialise the members in various different ways.

The whole library is const-correct so you should have no problem anonymously constructing the structure for one-off use like this:

tft->clearRectangle(Rectangle(x,y,w,h));

Gradient filled rectangles

The graphics library contains a linear gradient fill algorithm of my own design. This can be used to fill a rectangle with a gradient calculated from a starting and ending colour and a horizontal or vertical direction.

Here’s some example code that illustrates the function in action.

void loop() {
  doGradientFills(true);
  doGradientFills(false);
}

void doGradientFills(bool horz) {

  Rectangle rc;
  uint16_t i;
  static uint32_t colours[7]={
    ColourNames::RED,
    ColourNames::GREEN,
    ColourNames::BLUE,
    ColourNames::CYAN,
    ColourNames::MAGENTA,
    ColourNames::YELLOW,
    ColourNames::WHITE,
  };

  rc.Width=tft->getXmax()+1;
  rc.Height=(tft->getYmax()+1)/2;

  for(i=0;i<sizeof(colours)/sizeof(colours[0]);i++) {

    rc.X=0;
    rc.Y=0;

    tft->gradientFillRectangle(rc,
                               horz ? HORIZONTAL : VERTICAL,
                               ColourNames::BLACK,
                               colours[i]);

    rc.Y=rc.Height;

    tft->gradientFillRectangle(rc,
                               horz ? HORIZONTAL : VERTICAL,
                               colours[i],
                               ColourNames::BLACK);

    delay(2000);
  }
}

It’s unfortunate that I have to pass the direction as a boolean to the doGradientFills function but there is a bug in the Arduino IDE that prevents an enum value being passed to a function in your sketch.

Ideally I would have liked to have provided the gradient fill functions as template specialisations for horizontal and vertical but due to a limitation in the C++ standard around specialisation of template members of template classes I can’t do it without some ugly hacking.

There is an API change from version 1.0.1 onwards. gradientFillRectangle() now takes start and end colours as parameters instead of using the foreground and background colours.

You can watch a demo video of the gradient fill in action. As you can see it performs very well.

Drawing lines

Drawing lines follows much the same pattern as drawing rectangles. Here’s some sample code.

void loop() {

  Point p1,p2;
  int i;
  TftPanel::TColour randomColour;
 
  for(i=0;i<1000;i++) {

    p1.X=rand() % tft->getXmax();
    p1.Y=rand() % tft->getYmax();
    p2.X=rand() % tft->getXmax();
    p2.Y=rand() % tft->getYmax();

    randomColour=(((uint32_t)rand()<<16) | rand()) & 0xffffff;

    tft->setForeground(randomColour);
    tft->drawLine(p1,p2);
  }
  
  tft->clearScreen();
}

This example shows a loop drawing 1000 random coloured lines at random positions on the display.

Perhaps of interest here is the type name used for holding colour variables. TftPanel::TColour. It actually resolves down to uint32_t which you can use instead if you like. The type name is for portability if and when I use this library to control other TFTs.

The algorithm used is the Extremely fast line drawing algorithm from here. Please respect the author’s copyright:

Freely useable in non-commercial applications as long as credits to Po-Han Lin and link to http://www.edepot.com is provided in source code and can been seen in compiled executable. Commercial applications please inquire about licensing the algorithms.

You can watch a video of the line drawing demo in action. The performance is very good.

Ellipses, filled and outlined

An ellipse is defined by the position of the center of the shape and the two X and Y radii. A circle is just a special case of an ellipse so this is the method you should use to draw or fill circles.

void loop() {
  
  Size s;
  Point p;

  // draw an outline of ellipse around the edge
  
  p.X=(tft->getXmax()+1)/2;
  p.Y=(tft->getYmax()+1)/2;
  s.Width=((tft->getXmax()+1)/2)-1;
  s.Height=((tft->getYmax()+1)/2)-1;
  
  tft->setForeground(ColourNames::GOLDENROD);
  tft->drawEllipse(p,s);
  
  // fill a circle in the center
  
  tft->setForeground(ColourNames::INDIANRED1);
  tft->fillEllipse(p,Size(100,100));
  
  // finished
  
  for(;;);
}


A photograph of the above demo output

Drawing text

The graphics library supports fixed-width and proportional bitmap fonts (the former being merely a special case of the latter). TrueType Fonts can be downloaded from the bitmap fonts section of DaFont and converted to a C++ header file that you can include in your code.

The Windows font conversion utility is included in the zip file that you downloaded.

Let’s start with an example that shows a text string being repeatedly displayed at various positions on the screen. I’ll show the whole sketch this time because there are additions to the includes and to the initialisation.

#include "Nokia6300.h"
#include "Font_volter_goldfish_9.h"
//#include "Font_apple.h"    // fixed width
//#include "Font_kyrou9_regular_8.h"
//#include "Font_kyrou9_bold_8.h"
//#include "Font_tama_ss01.h"

using namespace lcd;

typedef Nokia6300_Portrait_16M TftPanel;

TftPanel *tft;
DefaultBacklight *backlight;
Font *font;

void setup() {

  // create a backlight controller and use it
  // to switch the backlight off
  
  backlight=new DefaultBacklight;
  backlight->setPercentage(0);

  // reset and initialise the panel

  tft=new TftPanel;

  // clear to black

  tft->setBackground(ColourNames::BLACK);
  tft->clearScreen();

  // fade up the backlight to 100%
  // in 4ms steps (400ms total)

  backlight->fadeTo(100,4);
  
  // create the font to use later

  font=new Font_VOLTER__28GOLDFISH_299;
  // font=new Font_APPLE8;
  // font=new Font_KYROU_9_REGULAR8;
  // font=new Font_KYROU_9_REGULAR_BOLD8;
  // font=new Font_TAMA_SS0117;
  
  // select the font so we can use the
  // streaming operators
  
  *tft << *font;
}


void loop() {
	
  int i;
  const char *str="The quick brown fox";
  Size size;
  Point p;
  TftPanel::TColour randomColour;
  
  size=tft->measureString(*font,str);

  for(i=0;i<3000;i++) {
    p.X=rand() % (tft->getXmax()-size.Width);
    p.Y=rand() % (tft->getYmax()-size.Height);

    randomColour=(((uint32_t)rand() << 16) | rand()) & 0xffffff;

    tft->setForeground(randomColour);

    *tft << p << str;
  }
}

We need to add an include for each font header file that you want to use. Uncomment one of the #include lines and put it at the top of your sketch with the other #include directives.

In the setup() function we construct the font class that we’re going to use. The obscure looking name of the class is auto-generated by the font conversion utility and can be found by looking in the header file.

  font=new Font_VOLTER__28GOLDFISH_299;
  // font=new Font_APPLE8;
  // font=new Font_KYROU_9_REGULAR8;
  // font=new Font_KYROU_9_REGULAR_BOLD8;
  // font=new Font_TAMA_SS0117;

After constructing the Font object it’s ready to use.

This demonstration uses the stream operator << to control text output. These operators are much more convenient than calling the writeString and measureString member functions directly because you can chain together multiple operations in one line.

The stream operators can take a font object to change the font that will be used next, a point object to change where the next output will be and of course a string, character, integer or floating point number to write. The current output location is automatically updated in the X direction so you can write out multiple strings and have them tacked on end-to-end.

The default precision for floating point numbers (double’s) is 5 decimal places. If you want to override that you can do so like this example where I show PI to 3 decimal places:

*tft << DoublePrecision(3.14159,3);

There is a double-to-string conversion in the arduino standard libraries but this version is 17% faster at the expense of 40 bytes of SRAM for an internal lookup table. For the curious, the implementation is a port of this open source library.

Here’s a video that shows the demo code in action.

The font conversion utility

There are hundreds of free bitmap fonts available online that you can browse and then convert for use in your sketch. I use the DaFont website.

Only the pixel/bitmap fonts are compatible. The vector fonts require a sophisticated anti-aliasing engine to make them look as great as they do, and typically they include special algorithms that help them to maintain their looks at small font sizes.

Browse to the Arduino library installation directory and navigate to the xmemtft/utility/fontconv sub-directory. Run FontConv.exe.




Font conversion utility (click for larger)

Follow these instructions to convert and save a font.

  1. Find a font you like and obtain the .ttf file for it. Save this to somewhere permanent on your system because the font conversion utility saves a reference to the pathname so it can read the font back later on.
  2. Click Browse for font file… and load it. If the font contains a great many characters then there may be a delay while it is processed.
  3. Select the characters you want to include when you save it. Don’t include characters you don’t use – in an embedded system that’s just a waste of memory. I have included Select all 7-bit and Select all alphanumeric buttons to help you select the most common characters.
  4. Set the correct font size. Most bitmap fonts look hopeless at any size other than the one they were designed for.
  5. If the characters are not centered in their boxes or the boxes are not wide enough then adjust the Extra Lines and Offset parameters until it looks right.
  6. If the characters are tight up against the edges of their boxes (like in the screenshot above) then add on a pixel or two of Character spacing so that the characters will appear spaced apart on screen. There is no preview for this option.
  7. Select the Arduino target button and click Save. The adjustments that you have made are saved in an XML file and the font source code in a header file. For example, enter MyFont as the name to save and you will get MyFont.h and MyFont.xml on disk.
  8. You can now include the font header file in your project.

If, in the future you need to make adjustments to the saved font then use the Load option and browse for the XML file that you saved in the above instructions.

Fonts are saved as a packed bitstream with no padding or wastage at all so flash memory usage is kept to a minimum.

Bitmap graphics

The graphics library supports bitmap graphics compiled into SRAM or flash, compressed or uncompressed. A utility is provided to convert almost any popular format to an internal format that you can include directly into your project.

The bitmap conversion utility

Bitmaps have to be converted from the efficient file storage format such as PNG, JPEG etc. to an internal format for optimised for transferring quickly from memory to the display. A Windows command-line utility is provided for this purpose, called bm2rgbi.exe. You can find it in the xmemtft/utility/bm2rgbi sub-directory of your Arduino libraries folder. The syntax is as follows:

bm2rgbi <input-file> <output-file> mc2pa8201 [16|262]

An example would be:

bm2rgbi picture.png picture.bin mc2pa8201 16

This would convert the file picture.png to a new file picture.bin with 16M colours ready for including in your program.

Uncompressed bitmaps

Uncompressed bitmaps are good for small icons and are the fastest to display. Even simple animation is possible with these and I will demonstrate that in this example code. The drawback is the amount of flash memory required to store the bitmap.

Let’s take a look at a complete example that shows a bitmap being animated by being bounced around the screen.

#include "Nokia6300.h"

using namespace lcd;

// set up in Landscape mode 

typedef Nokia6300_Landscape_16M TftPanel;

TftPanel *tft;
DefaultBacklight *backlight;

// reference to the bitmap pixels and size in flash

extern const uint32_t GlobePixels;
extern const uint32_t GlobePixelsSize;

// some program control variables

int8_t xdir,ydir;
Bitmap bm;
Size panelSize;
Point bmPos;

void setup() {

  // create a backlight controller and use it
  // to switch the backlight off
  
  backlight=new DefaultBacklight;
  backlight->setPercentage(0);

  // reset and initialise the panel

  tft=new TftPanel;

  // clear to black

  tft->setBackground(ColourNames::BLACK);
  tft->clearScreen();

  // fade up the backlight to 100%
  // in 4ms steps (400ms total)

  backlight->fadeTo(100,4);

  // set up the bitmap descriptor

  bm.Dimensions.Width=66;
  bm.Dimensions.Height=66;
  bm.DataSize=GET_FAR_ADDRESS(GlobePixelsSize);
  bm.Pixels=GET_FAR_ADDRESS(GlobePixels);

  // store the panel width and height

  panelSize.Width=tft->getWidth();
  panelSize.Height=tft->getHeight();

  // starting co-ordinates and directions

  bmPos.X=(panelSize.Width/2)-33;
  bmPos.Y=(panelSize.Height/2)-33;
  xdir=ydir=1;
}

void loop() {

  // draw the bitmap
  
  tft->drawUncompressedBitmap(bmPos,bm);

  // move it

  bmPos.X+=xdir;
  bmPos.Y+=ydir;

  // if the edge is hit, bounce it back

  if(bmPos.X==panelSize.Width-66)
    xdir=-1;
  else if(bmPos.X==0)
    xdir=1;

  if(bmPos.Y==panelSize.Height-66)
    ydir=-1;
  else if(bmPos.Y==0)
    ydir=1;
}

You’ve seen most of this before. Let’s take a look at the important points related to bitmaps.

extern const uint32_t GlobePixels;
extern const uint32_t GlobePixelsSize;

The pixels are stored in flash and we need to know where they are and how much memory they occupy. These two extern references will resolve to those values. We’ll see later how we get the pixels compiled into your program.

bm.Dimensions.Width=66;
bm.Dimensions.Height=66;
bm.DataSize=GET_FAR_ADDRESS(GlobePixelsSize);
bm.Pixels=GET_FAR_ADDRESS(GlobePixels);

Before we can display a bitmap we need to set up a structure that tells the library about it. The structure needs to receive the pixel width and height and the external references that we declared before. The GET_FAR_ADDRESS macro is a very useful hack that allows us to reference 24-bit addresses in flash. It’s required because AVR pointers are normally 16-bits wide and without this we would never be able to reference data above 64K in flash… kind of pointless when we’ve got 128K or 256K to play with.

tft->drawUncompressedBitmap(bmPos,bm);

This is where we draw the bitmap. The method takes a Point structure telling it where to display the bitmap and a reference to the bitmap structure itself that we prepared above.

Now we need to explain how we include that .bin converted bitmap in your compiled program. There are a couple of ways to do this and I’ve selected a method that means you don’t have to hardcode the start address and it should work with any size of bitmap limited only by your flash memory size, and it doesn’t matter if it ends up above the 64K memory boundary.

void _asmStub() {

  __asm volatile(
    ".global GlobePixels            nt"
    ".global GlobePixelsSize        nt"
  
    "GlobePixels:                   nt"
    ".incbin "globe64x64.bin"     nt"
    "GlobePixelsSize=.-GlobePixels  nt"

    ".balign 2                      nt"
  );
}

In the Arduino IDE I include this hack in a separate tab. What it’s doing is using the GNU assembler .incbin directive to include arbitrary binary into your program. Two symbols are declared (GlobePixels and GlobePixelsSize) and made global so that the C++ code can see them. Wrapping it all in a function that will never be called (_asmStub) ensures that it ends up in flash with your code. Don’t ever call this function because the MCU will try to execute your binary picture as program code!

Here’s a video showing the demo code in action:

Compressed Bitmaps

Large bitmaps cost memory. An uncompressed QVGA bitmap would occupy 320 x 240 x 3 = 230400 bytes. Far too much for our little MCU. Thankfully we support compression of bitmaps using the tiny liblzg codec. The level of compression is almost identical to PNG – that’s very good indeed and easily suitable for full-screen bitmaps.

There is a cost to using the LZG compression. The authors claim that the algorithm does not require any memory during decompression but this is only true if it’s set up to decompress to RAM because the algorithm needs random access to a preceding ‘window’ of bytes from the decompressed stream, the size of which is determined by the level of compression that was selected.

I have taken steps to limit the size of the compression window to 2Kb and implemented it as a ring buffer in the decompressor. Therefore the algorithm requires 2Kb of stack space to run which is de-allocated when it’s completed.

To get a compressed bitmap we need to add the -c flag to the bm2rgbi conversion utility, for example:

bm2rgbi picture.png picture.bin mc2pa8201 16 -c

In this next example we will really showcase how our little Arduino can play with the big boys by simulating a media player interface complete with a live 16 channel ‘graphic equaliser’ that displays the amplitudes of the frequency ranges in an input signal calculated in real-time by the Fast Fourier Transform (FFT).


The compressed bitmap that we will use for a background

#include "Nokia6300.h"

using namespace lcd;
 
// the bitmaps we'll use

extern const uint32_t mediaPlayerOrigin;
extern const uint32_t mediaPlayerSize;
extern const uint32_t screenStripOrigin;
extern const uint32_t screenStripSize;

// set up in Landscape mode 
 
typedef Nokia6300_Landscape_16M TftPanel;
 
TftPanel *tft;
DefaultBacklight *backlight;
Bitmap bmBack;

// the FFT funciton

extern int fix_fft(char fr[],char fi[],int m,int inverse);

// constants used here

enum {
  CHANNELS=16
};


void setup() {
  
  Bitmap bm;

  // create a backlight manager and switch off the backlight
  // so the user doesn't see the random data that can appear
  // during initialisation

  // create a backlight controller and use it
  // to switch the backlight off
 
  backlight=new DefaultBacklight;
  backlight->setPercentage(0);
 
  // reset and initialise the panel
 
  tft=new TftPanel;
 
  // clear to black
 
  tft->setBackground(ColourNames::WHITE);
  tft->clearScreen();
 
  // the background gradient

  bmBack.Dimensions.Width=10;
  bmBack.Dimensions.Height=78;
  bmBack.Pixels=GET_FAR_ADDRESS(screenStripOrigin);
  bmBack.DataSize=GET_FAR_ADDRESS(screenStripSize);

  // the bar colour
  
  tft->setForeground(0x38808a);

 // load the media player background

  bm.Dimensions.Width=316;
  bm.Dimensions.Height=200;
  bm.Pixels=GET_FAR_ADDRESS(mediaPlayerOrigin);
  bm.DataSize=GET_FAR_ADDRESS(mediaPlayerSize);
  
  tft->drawCompressedBitmap(Point(2,20),bm);

  // fade up the backlight to 100%
  // in 4ms steps (400ms total)

  backlight->fadeTo(100,4);
}


void loop() {

  uint8_t channels1[CHANNELS],channels2[CHANNELS];

  memset(channels2,0,sizeof(channels2));

  for(;;) {

    getSamples(channels1);
    plotSamples(channels1,channels2);

    getSamples(channels2);
    plotSamples(channels2,channels1);
  }
}


void getSamples(uint8_t *channels) {

  int i,val;
  uint32_t now,last;
  char data[128],im[128];
  uint16_t newvalue;

  // capture 128 samples

  last=0;
  i=0;
  
  while(i<128) {

  // get a sample (at best) each millisecond

    if((now=millis())!=last) {

      val=analogRead(0);
      
      data[i]=(val/4)-128;
      im[i]=0;

      last=now;
      i++;
    }
  }

  // do the fft

  fix_fft(data,im,7,0);

  // compute the amplitude
  
  for(i=0;i<64;i++)
    data[i]=sqrt(data[i]*data[i]+im[i]*im[i]);

  // average down to 16 channels and scale the amplitude

  for(i=0;i<64;i+=4) {

    newvalue=((uint16_t)data[i]+
              (uint16_t)data[i+1]+
              (uint16_t)data[i+2]+
              (uint16_t)data[i+3])/4;

    channels[i/4]=min(78,newvalue*4);
  }
}


void plotSamples(uint8_t *channels,uint8_t *previous) {

  int16_t i;
  Rectangle rc;
  Point bmPoint(26,58);

  rc.X=26;
  rc.Width=10;

  for(i=0;i<CHANNELS;i++) {
    if(channels[i]!=previous[i]) {
      if(channels[i]<previous[i]) {

        // replace the gradient background behind the bar
        // because we need to show a shorter bar
        
        tft->drawUncompressedBitmap(bmPoint,bmBack);

        // the new bar will be this tall

        rc.Height=channels[i];
      }
      else {
        
        // the new bar is taller, we fill up from the
        // previous to the new height
        
        rc.Y=136-channels[i];
        rc.Height=channels[i]-previous[i];
      }
      
      // draw the new bar
      
      rc.Y=136-channels[i];
      tft->fillRectangle(rc);
    }

    rc.X+=11;
    bmPoint.X+=11;
  }
}

This code depends on the Modified 8-bit FFT in C code posted on the old Arduino Forum.

Most of the example code is dedicated to the program logic. I’ve highlighted the lines where we use bitmaps. A compressed bitmap is used for the media player interface because without compression it would be about 190Kb. With LZG compression it’s a mere 32Kb.

As before, the raw bitmaps are included into the program code using some inline assembler like this example. Adjust the filenames of the .bin files to match your system.


void _asmStub() {
 
  __asm volatile(

    ".global mediaPlayerOrigin             nt"
    ".global mediaPlayerSize               nt"
  
    ".global screenStripOrigin             nt"
    ".global screenStripSize               nt"
  
    "mediaPlayerOrigin:                    nt"
    ".incbin "MediaPlayer.bin"           nt"
    "mediaPlayerSize=.-mediaPlayerOrigin   nt"
  
    "screenStripOrigin:                    nt"
    ".incbin "ScreenStrip.bin"           nt"
    "screenStripSize=.-screenStripOrigin   nt"
    
    ".balign 2 nt"
  );
}

Here’s a video showing the demo code in action

JPEG images

I’m pleased to announce that as from version 2.1.0 of the driver I now support JPEG images courtesy of the excellent picojpeg library.

The author of picojpeg has gone to great lengths to limit SRAM memory use but it will still cost you about 2.5Kb to call the JPEG decoder. The original version of the library would take that 2.5Kb as global variables which meant that you pay the memory cost for the lifetime of your program, even when you’re not actually decoding a JPEG.

I have modified picojpeg so that all but about 200 bytes of that memory comes off the stack and is only consumed whilst a JPEG is being decoded. Another key limitation is that progressive JPEGs are not supported.

Displaying JPEG images from flash

We can display JPEG images that are compiled into flash memory. My sample images take up about 15Kb each at full-screen (240×320) and 70% quality. Here’s an abbreviated sample that shows the technique:

#include "NokiaN82.h"

using namespace lcd;

// Graphics compiled in to flash

extern const uint32_t Test0Pixels,Test0PixelsSize;

void loop() {

  // show the demo with no fade out/in between frames

  showJpeg(
    GET_FAR_ADDRESS(Test0Pixels),
    GET_FAR_ADDRESS(Test0PixelsSize)
  );
}

void showJpeg(uint32_t pixelData,uint32_t pixelDataSize) {

  // draw the image

  JpegFlashDataSource ds(pixelData,pixelDataSize);
  tft->drawJpeg(Point(0,0),ds);
}

drawJpeg is the key function. It takes a point location on the screen of where to draw the jpeg and a reference to the data-source object that tells the drawing function where the jpeg data is, and how much of it there is.

As in all the previous bitmap examples the pixel data is included directly into flash using a little assembly language file. You can see the entire demo code in the driver zip file.

Here’s a video that shows the JPEG decoder in action:


Displaying JPEG images over the serial port

I have included driver support for displaying JPEG images streamed over the serial port from a connected computer. This frees your application from the memory limitations of the Arduino Mega and opens up the possibility of creating applications such as digital photo frames to showcase your portfolio to unsuspecting family and friends.

Serial support is a little more involved than flash support because we have to marshal the data between the two ends of the wire, ensuring that both ends stay in sync with each other. Here’s a sample application in its entirety:

#include "NokiaN82.h"
#include "Font_volter_goldfish_9.h"

using namespace lcd;

// We'll be working in portrait mode, 262K

typedef NokiaN82_Portrait_262K LcdAccess;
LcdAccess *tft;
DefaultBacklight *backlight;
Font *font;


void setup() {

  // 1Mb/s serial rate
  
  Serial.begin(1000000);

  // create a backlight manager and switch off the backlight
  // so the user doesn't see the random data that can appear
  // during initialisation

  backlight=new DefaultBacklight;
  backlight->setPercentage(0);

  // create and initialise the panel and font

  tft=new LcdAccess;
  font=new Font_VOLTER__28GOLDFISH_299;

  // clear to black

  tft->setBackground(ColourNames::BLACK);
  tft->setForeground(ColourNames::WHITE);
  tft->clearScreen();

  // fade up the backlight to 100% in 4ms steps
  // (400ms total) now that we are in a good state

  backlight->fadeTo(100,4);

  // select the font used througout

  *tft << *font;
}


void loop() {

  int32_t jpegSize;

  // show a prompt and wait for the file size to arrive

  tft->clearScreen();
  *tft << Point(0,tft->getYmax()-font->getHeight())
       << "Awaiting jpeg file size";

  jpegSize=readJpegSize();

  // show a new prompt and receive the jpeg data

  tft->clearScreen();
  *tft << Point(0,tft->getYmax()-font->getHeight()) 
       << "Receiving " << jpegSize << " bytes";

  // 63 is the size of each data chunk that we receive
  // before sending an ack back to the sender. The
  // size should be less than the receive ring buffer
  // capacity (64 on the mega, 16 on the standard)

  JpegSerialDataSource ds(Serial,jpegSize,63);
  tft->drawJpeg(Point(0,0),ds);

  // wait 5 seconds and then go around and read
  // another file

  delay(5000);
}


uint32_t readJpegSize() {

  uint32_t size;

  // read directly on to the 4-byte size. that implies
  // that the data must be sent little-endian (LSB first)

  while(Serial.readBytes(
    reinterpret_cast<char *>(&size),4)!=4
  );

  return size;
}

The key points are that you first initialise the serial link with the desired board rate, then you must initialise a JpegSerialDataSource with the serial object, the size of the data that you are going to transmit, and the size of each chunk of data to receive before sending back a ‘you may continue’ flow-control byte to the sender.

This last number, the chunk size, is important. The Arduino Mega library can, by default, receive 63 bytes before its internal buffer overflows. Therefore the ideal chunk size is 63, and that’s what we’ll use.

How you get the jpeg data size is up to you. In my demo I choose to transmit the size as a 32-bit number ahead of the data itself and you can see in readJpegSize that I just receive those bytes directly on to a uint32_t and return it.

Now for the other end. I created a small C# command line utility to do the work of sending the jpeg over the serial port. In my demo code I use the Arduino’s built in USB connection, the same one you use to flash your programs.

using System;
using System.IO.Ports;
using System.IO;

namespace SendJpeg {

  /*
   * main program class. example usage:
   *   sendjpeg.exe mytest.jpg com5 1000000 63
   */
  
  class Program {
    static void Main(string[] args) {

      try {
        if (args.Length != 4) {
          Console.WriteLine("usage: sendjpeg <jpegfile> <com-port> <baud-rate> <chunk-size>");
          return;
        }

        // open the port

        Console.WriteLine("Opening port "+args[2]);
        SerialPort serialPort = new SerialPort(args[1],int.Parse(args[2]),Parity.None,8,StopBits.One);
        serialPort.Handshake = Handshake.None;
        serialPort.Open(); 

        // get the size of the file to send

        FileInfo fi=new FileInfo(args[0]);
        long size=fi.Length;
        Console.WriteLine("Writing file size ("+size+") bytes");

        // send the file size, LSB first

        byte[] buffer=new byte[4];

        buffer[3]=(byte)((size >> 24) & 0xff);
        buffer[2]=(byte)((size >> 16) & 0xff);
        buffer[1]=(byte)((size >> 8) & 0xff);
        buffer[0]=(byte)(size & 0xff);

        serialPort.Write(buffer,0,4);

        // get ready to send the file

        buffer=new byte[1000];
        long remaining=size;
        int count,value;
        int chunkAvailable,chunkSize=int.Parse(args[3]);

        Console.WriteLine("Writing file data");

        using(BufferedStream bs=new BufferedStream(new FileStream(args[0],FileMode.Open,FileAccess.Read))) {
        
          chunkAvailable=chunkSize;

          while(remaining>0) {

            // read either 1000 or what's left

            count=(int)(remaining<1000 ? remaining : 1000);
            bs.Read(buffer,0,count);

            for(int i=0;i<count;i++) {

              // send a byte

              serialPort.Write(buffer,i,1);

              // if we have just sent the last of a chunk, wait for the ack

              if(--chunkAvailable==0) {
                value=serialPort.ReadByte();
                if(value!=0xaa)
                  throw new Exception("Unexpected chunk acknowledgement");

                // new chunk on its way

                chunkAvailable=chunkSize;
              }
            }

            remaining-=count;
            Console.Write(".");
          }
        }
        Console.WriteLine("\nDone.");
      }
      catch(Exception ex) {
        Console.WriteLine(ex.ToString());
      }
    }
  }
}

Example usage for this tool is:

SendJpeg.exe test.jpg com5 1000000 63

You can find the Visual Studio source code in the JpegSerial example directory, and the compiled executables are in the utility directory.

The serial port support is not limited to actual serial ports. It should work with any peripheral that has a driver that derives from the Arduino Stream class.

Let’s see a video of the serial jpeg link in action. Naturally this mode of operation is not as fast as when the data is compiled into flash because the little Arduino has to devote some time to receiving data over the wire.


Hardware Scrolling

The Nokia 6300 supports hardware scrolling. Well, they call it hardware scrolling but no pixel moving takes place. It’s actually simulated scrolling by offsetting the rows by a user-defined amount.

For example, a scroll amount of 1 means that row zero is actually output one row down and so on until you get to the last row that is wrapped around and will appear at the top where row zero usually is.

Hardware scrolling is officially supported in portrait mode but the current batch of screens that I’ve tested also support it in landscape mode where the scroll direction comes out as horizontal.

Here’s a demo that shows the hardware scrolling feature in action:

#include "Nokia6300.h"
#include "Font_volter_goldfish_9.h"

using namespace lcd;

typedef Nokia6300_Portrait_16M TftPanel;

TftPanel *tft;
DefaultBacklight *backlight;
Font *font;
int scrollPos,scrollDisp;


void setup() {

  int numRows,i;
  Point p;
  
  // create a backlight controller and use it
  // to switch the backlight off
  
  backlight=new DefaultBacklight;
  backlight->setPercentage(0);

  // reset and initialise the panel

  tft=new TftPanel;

  // create the font

  font=new Font_VOLTER__28GOLDFISH_299;

  // set scroll parameters
  
  tft->setScrollArea(0,320);
  tft->setForeground(ColourNames::WHITE);

  scrollPos=0;
  scrollDisp=1;

  // clear to black

  tft->setBackground(ColourNames::BLACK);
  tft->clearScreen();

  // fade up the backlight to 100%
  // in 4ms steps (400ms total)

  backlight->fadeTo(100,4);

  // print the text

  numRows=((tft->getYmax()+1)/font->getHeight())/3;
  p.X=0;

  for(i=0;i<numRows;i++) {
    p.Y=(numRows+i)*font->getHeight();
    tft->writeString(p,*font,"Test text row");
  }
}

void loop() {

  tft->setScrollPosition(scrollPos);
  scrollPos+=scrollDisp;
  
  if(scrollPos==100)
    scrollDisp=-1;
  else if(scrollPos==0)
    scrollDisp=1;
    
  delay(5);
}

Sleep mode

The TFT supports a sleep mode in which the display and much of the controller circuitry is powered down, reducing the current usage to a few microamps. The backlight should also be switched off in sleep mode since it’s the biggest current drain.

In sleep mode the content of the on-board graphics RAM is preserved so that when you wake up you don’t have to refresh the display.

Here’s a demo of the sleep mode in action:

#include "Nokia6300.h"

using namespace lcd;

typedef Nokia6300_Portrait_16M TftPanel;

TftPanel *tft;
DefaultBacklight *backlight;

void setup() {

  Rectangle rc;

  // create a backlight controller and use it
  // to switch the backlight off
  
  backlight=new DefaultBacklight;
  backlight->setPercentage(0);

  // reset and initialise the panel

  tft=new TftPanel;

  // clear to black

  tft->setBackground(ColourNames::BLACK);
  tft->clearScreen();

  // fade up the backlight to 100%
  // in 4ms steps (400ms total)

  backlight->fadeTo(100,4);
  
  // fill a rectangle in the center with green
  
  rc.X=20;
  rc.Y=20;
  rc.Width=tft->getXmax()-40;
  rc.Height=tft->getYmax()-40;

  tft->setForeground(ColourNames::RED);
  tft->fillRectangle(rc);
}

void loop() {
  
  // wait 3 seconds

  delay(3000);
  
  // fade out the backlight and go to sleep
  
  backlight->fadeTo(0,4);
  tft->sleep();

  // wait 2 seconds
  
  delay(2000);

  // wake up and fade back in

  tft->wake();
  backlight->fadeTo(100,4);  
}

Using the TFT as a character terminal

I have provided a high level class that builds on the primitives provided by the graphics library to implement a character terminal that you can use to output strings to.

The terminal class takes care of maintaining a cursor position and implements hardware scrolling when used in portrait mode. The ‘<<' operator is overloaded for output in a manner that imitates the familiar way it's used with C++ streams.

Here’s a demo that illustrates the terminal class being used.

#include "Nokia6300.h"
#include "Font_apple.h"

using namespace lcd;

typedef Nokia6300_Portrait_16M TftPanel;
typedef Nokia6300_Terminal_Portrait_16M TerminalAccess;

TftPanel *tft;
DefaultBacklight *backlight;
TerminalAccess *terminal;
Font *font;

// demo text

static const char  __attribute__((progmem)) demoText[] PROGMEM="
Lorem ipsum dolor sit amet, consectetur adipiscing elit.nn
Mauris malesuada ornare mi, id semper eros congue nec.nn
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean nec arcu ac lorem pulvinar pretium. Etiam at ultricies est.nn
Nunc nisl justo, ullamcorper vitae laoreet sit amet, tristique id est. Nulla imperdiet, massa et tincidunt ultricies, quam magna blandit nulla, vel aliquam tellus ipsum nec erat.nn
Suspendisse dignissim consectetur iaculis. Morbi vel felis quis nibh placerat porttitor eu dignissim mauris. Nunc posuere tincidunt felis elementum molestie.nn
Nunc nulla sem, imperdiet nec ullamcorper at, feugiat eu metus. Nunc congue congue lectus, sed accumsan metus hendrerit in.nn
Nulla non vestibulum leo. Nam sodales dignissim libero non ultrices.nn
Maecenas eget justo nunc. Aliquam erat volutpat. Ut pulvinar, massa id adipiscing blandit, ligula purus rhoncus ante, sed scelerisque tortor magna gravida libero.nn
Curabitur eget neque nec ante porttitor ornare. Morbi congue fermentum pellentesque. Suspendisse nisi tellus, suscipit sed congue ac, accumsan ac quam.nn
Nullam ullamcorper purus vitae diam vestibulum ultrices. Nullam vel libero ut justo imperdiet lobortis. Nullam sed lorem vitae nulla mattis faucibus.nn
Sed a turpis non turpis ullamcorper hendrerit. Nulla et magna ac nunc tristique fermentum eget in ante.";

void setup() {

  // create a backlight controller and use it
  // to switch the backlight off
  
  backlight=new DefaultBacklight;
  backlight->setPercentage(0);

  // reset and initialise the panel

  tft=new TftPanel;

  // clear to black

  tft->setBackground(ColourNames::BLACK);
  tft->clearScreen();

  // create a terminal implementation
  // the font must be fixed width

  font=new Font_APPLE8;
  terminal=new TerminalAccess(tft,font);

  tft->setForeground(ColourNames::WHITE);
  tft->setBackground(ColourNames::BLACK);
  
  // fade up the backlight to 100%
  // in 4ms steps (400ms total)

  backlight->fadeTo(100,4);
}


void loop() {

  const char *ptr;
  int i;
  char c;

  terminal->clearScreen();

  // demo the terminal as a progress indicator

  *terminal << "Loading assets...n";

  for(i=0;i<=100;i++) {

    terminal->clearLine();
    *terminal << i << '%';

    delay(50);
  }

  // demo the terminal as an output device

  terminal->writeString("nn");

  ptr=demoText;
  for(c=pgm_read_byte(ptr++);c;c=pgm_read_byte(ptr++)) {

    *terminal << c;
    delay(rand() % 60);
  }
}

You can watch a video that shows the character terminal in action.

Let’s take a look at the terminal-specific code from the above example.

typedef Nokia6300_Terminal_Portrait_16M TerminalAccess;
TerminalAccess *terminal;

terminal=new TerminalAccess(tft,font);

Here is where we declare the implementation of the terminal that we will use. It must match the orientation and colour depth of the TFT class. The font that you use must be fixed width.

terminal->clearScreen();

// demo the terminal as a progress indicator

*terminal << "Loading assets...n";

for(i=0;i<=100;i++) {

  terminal->clearLine();
  *terminal << i << '%';

  delay(50);
}

Here the demo simulates a percentage counter from 0 to 100. This is intended to show how the clearLine() method can be used to repeatedly show a changing status line.

for(c=pgm_read_byte(ptr++);c;c=pgm_read_byte(ptr++)) {

  *terminal << c;
  delay(rand() % 60);
}

This part of the demo ‘types’ out some paragraphs from the lorem ipsum text that will be familiar to designers the world over. When the text hits the bottom of the screen it will be hardware-scrolled to make room for the next line.

Big graphics library demo

All of the above techniques are demonstrated together in the GraphicsLibraryDemo example available in the Arduino IDE.

Here’s a video of the big demo in action.

Temporarily sold out! Some more boards are supposedly on the way to me so if you’re interested then please use my contact page to let me know and I’ll drop you a no-obligation email when they’re ready.

Update: 2 July 2012

I have noted that not all boards obtained on ebay are the same. To my surprise there are slight differences in the behaviour, nothing radical but enough for me to justify a new release of the software driver to deal with this ‘type B’ model. I will refer to the original model as ‘type A’.

The differences found so far are:

  • Type B will not initialise using my 16M driver. They require the 262K driver and then they will look and behave the same as before.
  • The hardware scrolling offset is reversed. Type ‘A’ boards scroll up one line with an offset of 319. These type ‘B’ boards will scroll up one line with an offset of 1.
  • Type ‘B’ boards support the faster 64K driver. Type ‘A’ boards do not. The raw fill rate for the 64K colour mode is 1.32 megapixels/second. It is 1.06 megapixels/second for the 262K and 16M modes on both boards.
  • These type ‘B’ boards seem to have a brighter backlight and could probably run optimally on a 90% PWM duty cycle.

There will be a software update in the next few days (now released, version 1.1.0) to support the difference in hardware scrolling behaviour.

  • Nicely done Andy.

    I am using similar techniques (templates for compile-time constants, strongly typed enums) in our own AVR runtime library which you may be interested in using, and hapefully contributing to 🙂 It is also built Eclipse/GCC 4.7. http://www.makehackvoid.com/project/MHVLib

    I've spent the last couple of months refactoring as I have learnt more about C++, and have seen signifcant flash/memory & performance gains by using templates to pass constants at compile time instead of runtime. Note that this is all in the head of the GIT repository at the moment. The code is currently being reviewed & tested before we push out the new release and freeze the API.

    You may also be interested in MHV AVR Tools, a GCC 4.7 toolchain I maintain, with support for Linux, Windows & OSX. It also includes SimAVR & AVR GDB for testing & debugging (my intent is to write automated regression & unit tests for MHVLib). The upcoming "sneak preview" version (which I am building at the moment) uses GCC & Binutils snapshots, which should hopefully have link time optimisation working on AVR. http://www.makehackvoid.com/project/mhvavrtools

    • Your library looks very nice and I certainly agree 100% with the reasons and philosophy of why you're doing it. I hope to take a closer look when time allows.

  • Really thanks for this two parts of great information 😉

  • Shane Johnson

    Hi Andy

    After many hours of searching the internet I finally found your site and I was very impressed – thanks for all your hard work.

    Is there any reason why the libraries you have developed for the Nokia screen would not work with other TFT screens?? I understand that some changes will need to be made to the sketches. I am particularly interested in your work with images and fonts.

    Cheers

    • Hi Shane,

      Yes the graphics library would work with other TFTs. The library code that handles the algorithms are separate from the driver code that manages the interface with the TFT. The one assumption made by the library is that the TFT supports windowed access, i.e. you can set a rectangle window in the GRAM and operate only on that window. The library provides compile-time typedefs that bind together the driver and graphics-library templates into one efficient class with a simple interface.

      To use the fast XMEM interface in the Mega the TFT would need to support an 8-bit transfer mode. Most do since it's one of the standard MIPI modes but often you find that shipped TFT breakout boards are hardwired to a different mode. If it did not have 8-bit access then the interface would need to be GPIO which works but has suboptimal performance.

  • Keith

    Hi Andy,
    Just a quick simple question for you…. I'm displaying rectangles and text on one of your N95 screens and I'm struggling a little with flicker on the redraw. I'm using clearRectangle to clear the previous draw and then drawing the new, I've limited it a little by wrapping in a if loop to only redraw if values change but it's still a little obvious. Any ideas or am I asking too much from the little LCD!
    Keep up the good work, its very impressive!
    Cheers

    • Hi Keith. The way to get around this problem is to try avoid double-drawing at all (i.e. erase, fill). The technique involves just drawing the new rectangle at the new position and then, based on your knowledge of the previous position, erasing only the part that needs to be removed. The weather demo does this with the temperature display numbers.

      This is a generic problem with graphics programming where you write directly to the display memory. Either you have to apply the above technique or you have to synchronize your drawing with the display refresh position so that you get it all over and done with before the refresh cycle reaches the part of graphics memory that you're updating. Unfortunately we don't get low-level access to the refresh position so we can't try that one.

  • Banjak

    great work…
    i just want to ask a question. can i use this library to program a pic or dspic uc from microchip using mplab software to control nokia 6300 lcd,
    if not guide me to a tutorial or some link to download and to do that.
    thank you very much.

    • Hi, this library is for the Arduino only. I am sure that the LCD could be driven from a PIC but I have no experience with those MCUs so I cannot help.

  • Banjak

    thanks for the fast reply, i can understand that you are not experience with pic MCUs, but i found this project: http://www.ivica-novakovic.from.hr/MotorCycle%20C
    and i download the project. the problem is, the organization of files and functions. if you have some time for me to to tel me use those files to drew a simple string or rectangle " with initialization ". if you don't have time or programming issues it's ok.
    sorry if i confuse you, thanks.

  • Carlo

    hello Andy,

    This tutorial looks great. I've download the zipfile to open it in arduino IDE. Only the namespace in all the sketchs were not recognized.
    Could you help me?

    Thanks!!

  • carlo

    Hello Andy,

    This site looks great!! I've download the zipfile with the sketches. Only the using namespace are don't recognized.
    Do you know how this is? I've the new arduino IDE.

    Thanks!!

    • All the examples should compile out of the box when selected from the Arduino IDE examples menu. Please tell me more about your platform: Arduino IDE version, OS, your steps to opening the example and the error message you see.

  • Edward Blum

    I can’t get any of your Graphics libraries to compile.

    as soon as GET_FAR_ADDRESS is called the compiler complains of undefined reference:
    ccIrS1JI.ltrans0.o:(.text.startup+0x1024): undefined reference to `GlobePixelsSize’

    • Edward Blum

      I’ve fixed it, the compiler now wants it all inside a .s file with quotes, new line and tab characters removed e.g

      .global LogoPixels
      .global LogoPixelsSize

      LogoPixels:.incbin “src/bitmaps/logo120x120.bin”
      LogoPixelsSize=.-LogoPixels
      .balign 2