Main Site Documentation

Running multiple display simultaniously


#1

I will be adding this to the book later but for now…

Adding multiple displays to a NETMF system is very much possible, including to a .NET Gadgeteer mainboard. The driver itself may need minor changes but this is why it is all open source.

For example, we have a FEZ Hydra connected to the 3.5" touch display using TFT bus, sockets RGB. Then we also added the small OLED display on SPI bus, socket S. The large display is controlled the usual way but the OLED display will need very minor change. All I am going to do is take the source driver in the system and not add the display to the designer. This way I can modify the driver easily right into my project. Next, use the display this way


// Init
OledDisplay oled;
oled = new OledDisplay(3);
Bitmap LCD = new Bitmap(128, 128); // the image to paint on the display

// Render
int i = 0;
 
Font font = Resources.GetFont(Resources.FontResources.big);
 
while (true)
{
    LCD.Clear();
    LCD.DrawText(i.ToString(), font, GT.Color.Red, 32, 12);
    i++;
    oled.Flush(LCD);
}

Here is the driver.


using System;
using Microsoft.SPOT;
using System.Threading;

using GT = Gadgeteer;
using GTM = Gadgeteer.Modules;
using GTI = Gadgeteer.Interfaces;

namespace Gadgeteer.Modules.Seeed
{
    /// <summary>
    /// A 128x128 OLED Display module by Seeed Studio for Microsoft .NET Gadgeteer
    /// </summary>
    public class OledDisplay : GTM.Module
    {
        // Note: A constructor summary is auto-generated by the doc builder.
        /// <summary></summary>
        /// <param name="socketNumber">The socket that this module is plugged in to.</param>
        public OledDisplay(int socketNumber)
           // : base(WPFRenderOptions.Intercept)
        {
            // This finds the Socket instance from the user-specified socket number.  
            // This will generate user-friendly error messages if the socket is invalid.
            // If there is more than one socket on this module, then instead of "null" for the last parameter, 
            // put text that identifies the socket to the user (e.g. "S" if there is a socket type S)
            Socket socket = Socket.GetSocket(socketNumber, true, this, null);

            _rst = new GTI.DigitalOutput(socket, Socket.Pin.Four, true, null);
            _dc = new GTI.DigitalOutput(socket, Socket.Pin.Three, true, null);

            // Create an LCD Configuration and set LCDControllerEnabled to false
            // to tell the mainboard that the LCD controller is not used and save on clock cycles

            GTI.SPI.Configuration spiConfig = new GTI.SPI.Configuration(false, 0, 0, false, true, 1000);
            _spi = new GTI.SPI(socket, spiConfig, GTI.SPI.Sharing.Shared, socket, Socket.Pin.Six, null);
            _spi.Write(new byte[] { 0x00 });

            InitializeDisplay();
        }

        private GT.Interfaces.SPI _spi;
        private GT.Interfaces.DigitalOutput _rst;
        private GT.Interfaces.DigitalOutput _dc;

        public uint Height
        {
            get { return 128; }

        }

        public uint Width
        {
            get { return 128; }
        }

        private void Reset()
        {
            _rst.Write(false);
            Thread.Sleep(100);
            _rst.Write(true);
            Thread.Sleep(50);
        }


        private byte[] _ba = new byte[1];

        /// <summary>
        /// Gets or sets a value that indicates whether the thread should sleep after a send operation.
        /// </summary>
        private bool _sleepAfterSend = false;

        /// <summary>
        /// Gets or sets the amount of time to sleep (if <see cref="_sleepAfterSend"/> is <b>true</b>)
        /// after a send operation.
        /// </summary>
        private int _sleeptime = 1;

        /// <summary>
        /// Sends a command to the display device.
        /// </summary>
        /// <param name="command">The command to send.</param>
        /// <param name="data">The command data.</param>
        private void Send(byte command, byte[] data)
        {
            _ba[0] = (byte)command;
            _dc.Write(false);
            _spi.Write(_ba);
            if (data != null && data.Length > 0)
            {
                _dc.Write(true);
                _spi.Write(data);
            }
            if (_sleepAfterSend) Thread.Sleep(_sleeptime);
        }

        private byte[] vram;

        /// <summary>
        /// Initializes the display.
        /// </summary>
        protected void InitializeDisplay()
        {
            vram = new byte[Width * Height * 2];

            _sleepAfterSend = true;

            SendParams(Cmd.SetCommandLock1, 0x12);
            SendParams(Cmd.SetCommandLock1, 0xB1);
            SendParams(Cmd.SetSleepModeON0);
            SendParams(Cmd.FrontClockDividerAndOscillatorFrequency1, 0xF1); // DIVSET = 1, Oscillator frequency = 15
            SendParams(Cmd.SetMuxRatio1, 0x7F);
            SendParams(Cmd.SetDisplayOffset1, 0x00);
            SendParams(Cmd.SetDisplayStartLine1, 0x00);
            SendParams(Cmd.SetRemapAndColorDepth1, 0x74); // 65K (16 bit) color
            SendParams(Cmd.SetGPIO1, 0x00);
            SendParams(Cmd.FunctionSelection1, 0x01); // internal VDD
            SendParams(Cmd.SetSegmentLowVoltage3, 0xA0, 0xB5, 0x55); // External VSL

            SendParams(Cmd.SetContrastCurrentForColourABC3, 0xC8, 0x80, 0xC8);
            SendParams(Cmd.MasterContrastCurrentControl1, 0x0F); // halve output currents 

            SendParams(Cmd.UseBuiltinLinearLUT0);

            SendParams(Cmd.SetResetAndPrechargePeriod1, 0x32); // minimum periods allowed
            SendParams((Cmd)0xB2, 0xA4, 0x00, 0x00); // "Enhance Driving Scheme Capability" - undocumented setting?
            SendParams(Cmd.SetPrechargeVoltage1, 0x17);
            SendParams(Cmd.SetSecondPrechargePeriod1, 0x01); // minimum period allowed
            SendParams(Cmd.SetVCOMHVoltage1, 0x05);
            SendParams(Cmd.SetDisplayModeNORMAL0);
            SendParams(Cmd.WriteRAMCommand0, vram);
            SendParams(Cmd.SetSleepModeOFF0);

            _sleepAfterSend = false;
        }

        private void SendParams(Cmd cmd, params byte[] args)
        {
            Send((byte)cmd, args);
        }

        private void Send(Cmd cmd, byte[] args)
        {
            Send((byte)cmd, args);
        }

        // from SSD1351 datasheet
        private enum Cmd
        {
            SetColumnAddress2 = 0x15,
            SetRowAddress2 = 0x75,
            WriteRAMCommand0 = 0x5C,
            ReadRAMCommand0 = 0x5D,
            SetRemapAndColorDepth1 = 0xA0,
            SetDisplayStartLine1 = 0xA1,
            SetDisplayOffset1 = 0xA2,
            SetDisplayModeOFF0 = 0xA4,
            SetDisplayModeALLON0 = 0xA5,
            SetDisplayModeNORMAL0 = 0xA6,
            SetDisplayModeINVERSE0 = 0xA7,
            FunctionSelection1 = 0xAB,
            SetSleepModeON0 = 0xAE,
            SetSleepModeOFF0 = 0xAF,
            SetResetAndPrechargePeriod1 = 0xB1,
            FrontClockDividerAndOscillatorFrequency1 = 0xB3,
            SetSegmentLowVoltage3 = 0xB4,
            SetGPIO1 = 0xB5,
            SetSecondPrechargePeriod1 = 0xB6,
            LookUpTableForGrayScalePulseWidth63 = 0xB8,
            UseBuiltinLinearLUT0 = 0xB9,
            SetPrechargeVoltage1 = 0xBB,
            SetVCOMHVoltage1 = 0xBE,
            SetContrastCurrentForColourABC3 = 0xC1,
            MasterContrastCurrentControl1 = 0xC7,
            SetMuxRatio1 = 0xCA,
            SetCommandLock1 = 0xFD
        }

        public void Flush(Bitmap bitmap)
        {
            try
            {
                byte[] bitmapbytes = bitmap.GetBitmap();
                int bitmapSize = vram.Length * 2;
                byte[] output = new byte[bitmapSize/2];
               
                // do conversion here
                //GTM.Module.Mainboard.NativeBitmapConverter(bitmapbytes, vram, Mainboard.BPP.BPP16_BGR_BE);

                int x=0;
                for (int i = 0; i < bitmapSize; i += 4)
                {
                    byte R = bitmapbytes[i];
                    byte G = bitmapbytes[i + 1];
                    byte B = bitmapbytes[i + 2];

                    output[x] = (byte)((R & 0xE0)| (G>>5));
                    output[x + 1] = (byte)(B >> 3);
                    x += 2;
                }

                Send(Cmd.WriteRAMCommand0, output);
            }
            catch
            {
                //ErrorPrint("Painting error");
            }
        }
    }
}

Here is a video as well

Important note: there is a bug in hydra on SPI bus so this will not work for you. I will delete this note once the next SDK is out and bug is fixed.


#2

@ Gus

Nice! Can’t wait for the updated SDK so I can get OLED working on my Hydra…


#3

I also see an exception when initializing the OLED display on Socket 4. It’s thrown when Gadgeteer’s SPI.cs calls into NETMF to initialize the SPI configuration:


if (!si.IsInitialised)
{
     si.SpotSPI = new Microsoft.SPOT.Hardware.SPI(this.spiConfig);
     si.IsInitialised = true;
}

On Socket 3, the modified OLED driver produces screen output but the text crawls across the screen. This might be the SPI issue that Gus is referring to :slight_smile:


#4

Yes Kerry that is correct, we found the bug and a fix come shortly. We have requested a feature addition to the core then we will make another release.


#5

Would the same concept work with 2 TFTs? I am actually looking to run 3 TFT’s


#6

TFT in that demo uses R,G,B, T sockets. Oled is SPI (S,socket).There is only one set of R,G,B,T.


#7

Will ComputeTextInRect work correctly in that new release (hint, hint), as I’m having a difficult time getting back a correct x value after a wrap (ie where the x component is for the wrapped line).

Thanks