SPI buffer size limited to 65535 bytes

Hello,

I was trying to implement some graphic drivers and I’m facing an issue with SPI.Write() not allowing more than 65535 bytes at once, although its size parameter is an integer.

Trying to send more than UInt16.MaxValue throws an “Invalid operation exception” :

#### Exception System.InvalidOperationException - CLR_E_INVALID_OPERATION (1) ####
#### Message: 
#### GHIElectronics.TinyCLR.Devices.Spi.Provider.SpiControllerApiWrapper::WriteRead [IP: 0000] ####
#### GHIElectronics.TinyCLR.Devices.Spi.SpiDevice::WriteRead [IP: 001d] ####
#### GHIElectronics.TinyCLR.Devices.Spi.SpiDevice::Write [IP: 000b] ####
#### MBN.Modules.ILI9341.ILI9341Controller::DrawBuffer [IP: 0053] ####
#### ILI9341.Program::Graphics_OnFlushEvent [IP: 0008] ####
#### System.Drawing.Graphics::Flush [IP: 002d] ####
#### ILI9341.Program::Main [IP: 000b] ####

Exception levée : ‘System.InvalidOperationException’ dans GHIElectronics.TinyCLR.Devices.Spi.dll

I will raise an issue on the repo about this.

Strange. It might be sure to internal use of dma/fifo…

If this is a bug, we will fix but otherwise sending multiple spi transactions will do the trick and won’t have any significant performance implication as this is only happening over every 64k bytes

Unfortunately, it does have significant performance implications :frowning:

Consider the following code :

public void DrawBuffer(byte[] buffer)
        {
            SendCommand(ILI9341CommandId.RAMWR);
            control.Write(GpioPinValue.High);
            
            BitConverter.SwapEndianness(buffer, 2);
            spi.Write(buffer, 0, 1024 * 64 - 1);
            //spi.Write(buffer, 1024 * 64, 1024 * 64 - 1);
            //spi.Write(buffer, 1024 * 64 * 2, 22530);
        } 

This gives 42 fps.
Adding the second spi.Write() gives 22 fps
And finally, adding the third call gives 19 fps

SPI frequency is set to 24MHz (max value for the bus).

The more spi data you send the more time it takes. This is not related to the issue.

Breaking your buffer into three transactions should not impact FPS. If it did, it should be very very minimal. What am I missing?

Time to transfer the managed buffer(s) to the native side ?

The call only passes a pointer to the data.

We will be looking into this anyway

Ok. Thank you.

But this will mean that I would not have much more than 20 fps. That will give flickering or visible page refresh.
Not sure I keep on working on this, then. I understand that managed drivers will always be slower that native ones, but here the framerate is really to low, to me.

How many bytes do it need to send for one frame?

Max 320x240x2 = 153 600 bytes

so based on this need buffer of 256 kb max at once write

Have you connected a scope to see what is taking time? Maybe it is but converter, maybe something else.

Bit converter only eats 1 fps, it’s really fast. That was the (unexpected) good news.

256KB would indeed be more than enough. But I don’t know how it’s handled at the MCU level, so I’m not sure if it’s possible.
And no, I did not connect a scope. I don’t want to debug SPI :stuck_out_tongue:

ok lazy man, we will do it for you Large SPI Displays speed · Issue #777 · ghi-electronics/TinyCLR-Libraries · GitHub

1 Like

Is this the display I need to test your code? Where is your code?

This one looks like to expose only parallel, no SPI?

those board is SPI
also it include xpt2040 mcu
for touch screen part

The display does but I do not think the SPI pins are exposed on the PCB.

for that have a lot of pins
for display
for touch,
for backlight …
and sd card

Hello Gus,

Here is a link for a SPI version.
My displays have no touch chip but it does not really matter. As you write in your issue, it’s all about write-only. So, no read involved.
By the way, the initial issue was more about the 64K limit, not SPI speed. But if both can be improved, I take !

And about my lazyness, well… you’re not that far from the truth :wink:

Anyway, thank you for taking care of this request !

Edit : here the code. It’s still work in progress but the main functions are fine.

using System;
using System.Diagnostics;
using System.Threading;
using GHIElectronics.TinyCLR.Devices.Gpio;
using GHIElectronics.TinyCLR.Devices.Spi;

namespace MBN.Modules.ILI9341
{
    public enum ILI9341CommandId : Byte
    {
        SWRESET = 0x01, // Software Reset
        SLPOUT = 0x11, // Sleep Out
        INVOFF = 0x20, // Display Inversion Off
        INVON = 0x21, // display Inversion On
        GAMMASET = 0x26, // Digital Gamma Control
        DISPOFF = 0x28, // Display Off
        DISPON = 0x29, // Display On
        CASET = 0x2A, // Column Address Set
        PASET = 0x2B, // Page Address Set
        RAMWR = 0x2C, // Memory Write
        MADCTL = 0x36, // Memory Access control
        PIXFMT = 0x3A, // Pixel Color Format
        FRMCTR1 = 0xB1, // Frame Control 1 for normal dispay
        DFUNCTR = 0xB6, // Display Function Control
        EMSET = 0xB7, // Entry Mode Set
        MADCTL_MY = 0x80,
        MADCTL_MX = 0x40,
        MADCTL_MV = 0x20,
        MADCTL_BGR = 0x08,
        MADCTL_RGB = 0x00
    }

    public class ILI9341Controller
    {
        private readonly byte[] buffer1 = new byte[1];
        private readonly byte[] buffer4 = new byte[4];
        private byte[] buffer;
        private readonly SpiDevice spi;
        private readonly GpioPin control;
        private readonly GpioPin reset;

        private int bpp = 16;
        private bool rowColumnSwapped;

        public int Width
        {
            get; private set;
        }
        public int Height
        {
            get; private set;
        }

        public int Orientation
        {
            get; private set;
        }

        public int MaxWidth => rowColumnSwapped ? 320 : 240;
        public int MaxHeight => rowColumnSwapped ? 240 : 320;

        public ILI9341Controller(Hardware.Socket socket)
        {
            spi = SpiController.FromName(socket.SpiBus).GetDevice(new SpiConnectionSettings()
            {
                ChipSelectType = SpiChipSelectType.Gpio,
                ChipSelectLine = GpioController.GetDefault().OpenPin(socket.Cs),
                Mode = SpiMode.Mode0,
                ClockFrequency = SpiController.FromName(socket.SpiBus).MaxClockFrequency
            });

            Debug.WriteLine($"SPI freq = {spi.ConnectionSettings.ClockFrequency}");

            var backlight = GpioController.GetDefault().OpenPin(socket.PwmPin);
            backlight.SetDriveMode(GpioPinDriveMode.Output);
            backlight.Write(GpioPinValue.High);


            control = GpioController.GetDefault().OpenPin(socket.AnPin);
            control.SetDriveMode(GpioPinDriveMode.Output);

            reset = GpioController.GetDefault().OpenPin(socket.Rst);
            reset.SetDriveMode(GpioPinDriveMode.Output);

            Width = 240;
            Height = 320;

            Reset();
            Initialize();
            SetActiveWindow(0, 0, MaxWidth, MaxHeight);

            Enable();
        }

        private void Reset()
        {
            reset.Write(GpioPinValue.Low);
            Thread.Sleep(50);

            reset.Write(GpioPinValue.High);
            Thread.Sleep(200);
        }

        private void Initialize()
        {
            SendCommand(ILI9341CommandId.SWRESET); // Software Reset
            Thread.Sleep(10);

            SendCommand(ILI9341CommandId.DISPOFF); // Display Off

            SendCommand(ILI9341CommandId.MADCTL); // Memory Access Control

            SendData(0x08 | 0x40); // Set initially to Portrait Orientation.

            SendCommand(ILI9341CommandId.PIXFMT);
            SendData(0x55); //16-bits per pixel

            SendCommand(ILI9341CommandId.FRMCTR1); // Frame Control 1
            SendData(0x00);
            SendData(0x1B);

            SendCommand(ILI9341CommandId.GAMMASET); // Set Digital Gamma Control
            SendData(0x01); // Use Gamma Set 1

            SendCommand(ILI9341CommandId.CASET); // Width of the screen 239 (0 - 239) or 240 pixels wide.
            SendData(0x00);
            SendData(0x00);
            SendData(0x00);
            SendData(0xEF);

            SendCommand(ILI9341CommandId.PASET); // Width of the screen 319 (0 - 319) or 320 pixels high.
            SendData(0x00);
            SendData(0x00);
            SendData(0x01);
            SendData(0x3F);

            SendCommand(ILI9341CommandId.EMSET); // Entry Mode Set
            SendData(0x07); // Low voltage detect - disable, Normal On

            SendCommand(ILI9341CommandId.DFUNCTR); // Display function Control
            SendData(0x0A);
            SendData(0x82);
            SendData(0x27);
            SendData(0x00);

            SendCommand(ILI9341CommandId.SLPOUT); // Sleep Out
            Thread.Sleep(120);

            SendCommand(ILI9341CommandId.DISPON); // Display On
            Thread.Sleep(100);
        }

        public void Dispose()
        {
            spi.Dispose();
            control.Dispose();
            reset?.Dispose();
        }

        public void Enable() => SendCommand(ILI9341CommandId.DISPON);
        public void Disable() => SendCommand(ILI9341CommandId.DISPOFF);

        private void SendCommand(ILI9341CommandId command)
        {
            buffer1[0] = (byte)command;
            control.Write(GpioPinValue.Low);
            spi.Write(buffer1);
        }

        private void SendData(byte data)
        {
            buffer1[0] = data;
            control.Write(GpioPinValue.High);
            spi.Write(buffer1);
        }

        private void SendData(byte[] data)
        {
            control.Write(GpioPinValue.High);
            spi.Write(data);
        }


        public void SetActiveWindow(int x, int y, int width, int height)
        {
            var x_end = x + width - 1;
            var y_end = y + height - 1;

            SendCommand(ILI9341CommandId.CASET);
            SendData((Byte)(x >> 8));
            SendData((Byte)x);
            SendData((Byte)(x_end >> 8));
            SendData((Byte)x_end);

            SendCommand(ILI9341CommandId.PASET);
            SendData((Byte)(y >> 8));
            SendData((Byte)y);
            SendData((Byte)(y_end >> 8));
            SendData((Byte)y_end);

            SendCommand(ILI9341CommandId.RAMWR);

            Width = width;
            Height = height;
        }


        public void DrawBuffer(byte[] buffer)
        {
            SendCommand(ILI9341CommandId.RAMWR);
            control.Write(GpioPinValue.High);
            
            try
            {
                BitConverter.SwapEndianness(buffer, 2);
                spi.Write(buffer, 0, 1024 * 64 - 1);
                spi.Write(buffer, 1024 * 64-1, 1024 * 64 - 1);
                spi.Write(buffer, 1024 * 64 * 2 -2, 22530);
            }
            catch
            {
                Debug.WriteLine("Exception SPI");
            }
        }

        public void SetOrientation(int orientationDegrees)
        {
            switch (orientationDegrees)
            {
                case 0:
                    Width = 240;
                    Height = 320;
                    Orientation = 0;
                    SetActiveWindow(0, 0, 240, 320);
                    SendCommand(ILI9341CommandId.MADCTL);
                    SendData(0x48);
                    break;
                case 90:
                    Width = 320;
                    Height = 240;
                    Orientation = 90;
                    SetActiveWindow(0, 0, 320, 240);
                    SendCommand(ILI9341CommandId.MADCTL);
                    SendData(0xE8);
                    break;
                case 180:
                    Width = 240;
                    Height = 320;
                    Orientation = 180;
                    SetActiveWindow(0, 0, 240, 320);
                    SendCommand(ILI9341CommandId.MADCTL);
                    SendData(0x88);
                    break;
                case 270:
                    Width = 320;
                    Height = 240;
                    Orientation = 270;
                    SetActiveWindow(0, 0, 320, 240);
                    SendCommand(ILI9341CommandId.MADCTL);
                    SendData(0x28);
                    break;
                default:
                    break;
            }
        }
    }
}

And here is a test code :

using GHIElectronics.TinyCLR.Devices.Gpio;
using GHIElectronics.TinyCLR.Devices.Spi;
using MBN;
using MBN.Modules.ILI9341;
using System;
using System.Drawing;
using System.Collections;
using System.Text;
using System.Threading;
using GHIElectronics.TinyCLR.Native;
using System.Diagnostics;

namespace ILI9341
{
    class Program
    {
        private static ILI9341Controller ILI9341;
        private const int SCREEN_WIDTH = 240;
        private const int SCREEN_HEIGHT = 320;

        private static int framerate;
        private static Object LockFramerate = new object();

        static void Main()
        {
            Info();
            new Thread(DisplayFramerate).Start();

            TestILI9341(Hardware.SocketTwo);

            Thread.Sleep(Timeout.Infinite);
        }

        private static void Info()
        {
            Debug.WriteLine($"Device name : {DeviceInformation.DeviceName}");
            Debug.WriteLine($"+-----------+------------+------------+");
            Debug.WriteLine($"| Memory    | Used       | Free       |");
            Debug.WriteLine($"+-----------+------------+------------+");
            Debug.WriteLine($"| Managed   | {Memory.ManagedMemory.UsedBytes,10:N0} | {Memory.ManagedMemory.FreeBytes,10:N0} |");
            Debug.WriteLine($"| Unmanaged | {Memory.UnmanagedMemory.UsedBytes,10:N0} | {Memory.UnmanagedMemory.FreeBytes,10:N0} |");
            Debug.WriteLine($"+-----------+------------+------------+\r\n");
        }


        private static void TestILI9341(Hardware.Socket socket)
        {
            ILI9341 = new ILI9341Controller(socket);

            ILI9341.SetOrientation(180);

            // Create flush event
            Graphics.OnFlushEvent += Graphics_OnFlushEvent;

            // Create bitmap buffer
            var screen = Graphics.FromImage(new Bitmap(SCREEN_WIDTH, SCREEN_HEIGHT));

            //var image = Properties.Resources.GetBitmap(Properties.Resources.BitmapResources.smallJpegBackground);

            var font = Properties.Resources.GetFont(Properties.Resources.FontResources.droid_reg24);

            screen.Clear();

            screen.FillEllipse(new SolidBrush(Color.FromArgb(255, 255, 0, 0)), 0, 0, 80, 64);
            screen.FillEllipse(new SolidBrush(Color.FromArgb(255, 0, 0, 255)), 80, 0, 80, 64);
            screen.FillEllipse(new SolidBrush(Color.FromArgb(128, 0, 255, 0)), 40, 0, 80, 64);

            //screen.DrawImage(image, 56, 50);

            screen.DrawRectangle(new Pen(Color.Yellow), 10, 80, 40, 25);
            screen.DrawEllipse(new Pen(Color.Purple), 60, 80, 40, 25);
            screen.FillRectangle(new SolidBrush(Color.Teal), 110, 80, 40, 25);

            screen.DrawLine(new Pen(Color.White), 10, 127, 150, 127);

            screen.DrawLine(new Pen(Color.White), 120, 1, 120, 319);
            screen.SetPixel(80, 92, 0xFF0000);

            screen.DrawString("Hello world!", font, new SolidBrush(Color.Yellow), 50, 110);
            screen.DrawString("Hello world!", font, new SolidBrush(Color.Yellow), 10, 180);

            //while (true)
            {
                screen.Flush();
            }
        }

        private static void Graphics_OnFlushEvent(IntPtr hdc, byte[] data)
        {
            ILI9341.DrawBuffer(data);
            framerate++;
        }

        private static void DisplayFramerate()
        {
            while (true)
            {
                //Debug.WriteLine($"Framerate : {framerate}");
                framerate = 0;
                Thread.Sleep(1000);
            }
        }
    }
}

You will have to remove the comments on the while (true) statement (before the screen.Flush()) and the one to display FPS in the DisplayFramerate() method.

Just ordered the display… you owe me some croissants :fr:

2 Likes