Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sharpmem #724

Open
wants to merge 14 commits into
base: dev
Choose a base branch
from
Open

Add sharpmem #724

wants to merge 14 commits into from

Conversation

rdnt
Copy link

@rdnt rdnt commented Nov 30, 2024

This PR adds the Sharp Memody Display SPI driver.

The implementation has been tested with nice!view (Sharp LS011B7DH03) on a nice!nano v2

[...] there are 240 clocks for each line, but only 230 horizontal pixels. This means there are 10 dummy "pixels" for each line. The datasheet says that they don't matter, but that they're recommended to be zeroes (source).

The spec says that there are seven clocks of mode select (3 meaningful bits, then 4 bits of "low data") followed by nine bits of line address. This makes sense; there are 303 lines on this display, which is more than would fit in 8 bits of address (source).

Inspired by github.com/funkycode/tinygo-corne

@rdnt
Copy link
Author

rdnt commented Nov 30, 2024

Marking as draft as I would like to perform some additional manual testing.

@rdnt
Copy link
Author

rdnt commented Nov 30, 2024

In case it helps someone else, here is an example where the display, if wired correctly, will do some back-and-forth between white and black, and then scan right and down. (nice!nano + nice!view)

Note: a refined version of the script is further down in the comments.

package main

import (
	"image/color"
	"machine"
	"time"

	"tinygo.org/x/drivers/sharpmem"
)

var display *sharpmem.Device

func initDisplay() error {
	machine.P0_06.Configure(machine.PinConfig{Mode: machine.PinOutput})

	err := machine.SPI0.Configure(machine.SPIConfig{
		Frequency: 2000000,
		SCK:       machine.P0_20,
		SDO:       machine.P0_17,
		SDI:       machine.P0_25,
		Mode:      0,
		LSBFirst:  true,
	})
	if err != nil {
		println("spi configure", err)
		return err
	}

	d := sharpmem.NewSPI(machine.SPI0, machine.P0_06)
	d.Configure(sharpmem.Config{
		Width:  160,
		Height: 68,
	})

	display = &d

	return nil
}

func main() {
	err := initDisplay()
	if err != nil {
		println(err.Error())
		return
	}

	it := 0

	for {
		time.Sleep(500 * time.Millisecond)

		switch it {
		case 0, 2, 4:
			for x := range int16(160) {
				for y := range int16(68) {
					display.SetPixel(x, y, color.RGBA{R: 0, G: 0, B: 0, A: 255})
				}
			}

			err = display.Display()
			if err != nil {
				println(err.Error())
			}

			println("all pixels black")

		case 1:
			err := display.Clear()
			if err != nil {
				println(err.Error())
			}
			println("all pixels white (display.Clear)")

		case 3:
			display.ClearBuffer()
			err = display.Display()
			if err != nil {
				println(err.Error())
			}
			println("all pixels white (display.ClearBuffer + display.Display)")

		case 5:
			for x := range int16(160) {
				for y := range int16(68) {
					display.SetPixel(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 255})
				}
			}

			err = display.Display()
			if err != nil {
				println(err.Error())
			}
			println("all pixels white (display.SetPixel + display.Display)")

		case 6:
			println("scan right")
			for x := range int16(160) {
				for y := range int16(68) {
					display.SetPixel(x, y, color.RGBA{R: 0, G: 0, B: 0, A: 255})
				}
				err = display.Display()
				if err != nil {
					println(err.Error())
				}
			}
			err := display.Clear()
			if err != nil {
				println(err.Error())
			}
			println("all pixels white (display.Clear)")

		case 7:
			println("scan down")
			for y := range int16(68) {
				for x := range int16(160) {
					display.SetPixel(x, y, color.RGBA{R: 0, G: 0, B: 0, A: 255})
				}
				err = display.Display()
				if err != nil {
					println(err.Error())
				}
				// scan down is really fast because of the line-diffing
				time.Sleep(16 * time.Millisecond)
			}
			err := display.Clear()
			if err != nil {
				println(err.Error())
			}
			println("all pixels white (display.Clear)")

		}

		it = (it + 1) % 8
	}
}
24eff85fef15fcb9.mp4

@rdnt
Copy link
Author

rdnt commented Nov 30, 2024

I'd say the PR is ready for review. Let me know if anything needs to be revised.

@rdnt rdnt marked this pull request as ready for review November 30, 2024 21:56
@rdnt
Copy link
Author

rdnt commented Dec 1, 2024

Found an out of bounds exception, something to do with the line calculations during Display() on 68x68 (and 240x240 which is one of the existing SKUs) resolution. Looking into it, will update the PR status accordingly.

@rdnt rdnt marked this pull request as draft December 1, 2024 12:20
@rdnt
Copy link
Author

rdnt commented Dec 13, 2024

I did some refactoring and studied all the datasheets, and I refactored the driver to allow for all of the supported devices to (theoretically) work. Some observations I made that led to the refactor:

  • Some devices have larger than 8-bit resolutions (and some smaller). When transferring the current line to be updated, some bits (1 or 2, for 9-bit or 10-bit heights respectively) actually leak into the first byte.
    • For the case with the 9-bit height (LS018B7DH02), the datasheet mentions 3 bits for mode, then 4 low bits, and then 9 bits for resolution. The first byte is depicted as MMMLLLLR, (Mode, Low, Resolution), but I am assuming the grouping of the bits is in the wrong order, as the mode bits are lower when we prepare the bytes for transfer (e.g. 0x01 or 0x04) on other implementations. So the actual order has to be RLLLLMMM.
    • The bits for line number (and the actual bits for the pixels that follow) are correctly represented.
image - image - image - A specific device (LS018B7DH02 -- same as before) requires the 4 padding bits to be low, while other devices simply mention those bits as 'dummy'. Another device ([LS032B7DD02](https://www.sharpsde.com/fileadmin/products/Displays/Specs/LS032B7DD02_01Nov23_Spec_LD-2023X13.pdf)) that requires 10 bits for the line number mentions the 3 padding bits as 'dummy'. They remain low in the implementation to accommodate all devices. - For devices the height of which is not divisible by 16, the 'data bits' for the line number itself are padded to the nearest 2nd-byte.

Here, I have derived a chart with the (theoretically) supported SKUs, their resolutions, the clocks required to transfer a line of pixels (the bits of the pixels + padding), the padding itself explicitly, and lastly the bits required for the address:

// SKU             width    height    clocks    padding    address
// LS010B7DH04     128      128       128       0          8
// LS011B7DH03     160      68        160       0          8
// LS012B7DD01     184      38        184+8     8          6
// LS013B7DH03     128      128       128       0          8
// LS013B7DH05     144      168       144       0          8
// LS018B7DH02     230      303       230+10    10         9
// LS027B7DH01     400      240       400       0          8
// LS027B7DH01A    400      240       400       0          8
// LS032B7DD02     336      536       336       0          10
// LS044Q7DH01     320      240       320       0          8

TL;DR: Lots of complexity, but the implementation tries to transfer the bytes in a way that should be compatible with all SKUs. With the only exception being the shifting of the high bits of the address based on max address size.

For completeness, I am unable to test in any other device except for LS011B7DH03. I can test the other implementations for out of bounds exceptions and panics, but ofc the display renders gibberish if I try to render with a different resolution.
If you'd like me to remove the SKUs that haven't been tested, I can of course do that. Or I can mention that the driver has only been tested with LS011B7DH03 perhaps? Anyway.

@deadprogram
Copy link
Member

Thanks for all the work on this @rdnt

For completeness, I am unable to test in any other device except for LS011B7DH03. I can test the other implementations for out of bounds exceptions and panics, but ofc the display renders gibberish if I try to render with a different resolution.

I think testing for out of bounds exceptions and panics on displays you do not have sounds like a good idea.

If you'd like me to remove the SKUs that haven't been tested, I can of course do that. Or I can mention that the driver has only been tested with LS011B7DH03 perhaps?

I think leaving them in while documenting that only the LS011B7DH03 has been tested should be sufficient. Hopefully people with the other display variants can test them out and let us know of any anomalies.

@rdnt
Copy link
Author

rdnt commented Dec 13, 2024

I added a (speculative) bug fix regarding the address padding (and the assumed intricacies of it), and some tests for it, and did some cleanup.

@deadprogram I added the doc comment you mentioned on the constructor.


Marking as ready-for-review again. 🙏🏻
Let me know if something is amiss.

Oh, and here is a refined script for others to test the implementation with:

package main

import (
	"image/color"
	"machine"
	"time"

	"tinygo.org/x/drivers/sharpmem"
)

func initSPI() error {
	machine.P0_06.Configure(machine.PinConfig{Mode: machine.PinOutput})

	err := machine.SPI0.Configure(machine.SPIConfig{
		Frequency: 2000000,
		SCK:       machine.P0_20,
		SDO:       machine.P0_17,
		SDI:       machine.P0_25,
		Mode:      0,
		LSBFirst:  true,
	})
	if err != nil {
		println("spi configure", err)
		return err
	}

	return nil
}

func main() {
	time.Sleep(time.Second)

	configs := []sharpmem.Config{
		//{Width: 128, Height: 128}, // LS010B7DH04, LS013B7DH03
		{Width: 160, Height: 68}, // LS011B7DH03
		//{Width: 184, Height: 38},  // LS012B7DD01
		//{Width: 144, Height: 168}, // LS013B7DH05
		//{Width: 230, Height: 303}, // LS018B7DH02
		//{Width: 400, Height: 240}, // LS027B7DH01, LS027B7DH01A
		//{Width: 336, Height: 536}, //LS032B7DD02
		//{Width: 320, Height: 240}, //LS044Q7DH01
	}

	// test with optimizations being disabled
	cfgLen := len(configs)
	for i := 0; i < cfgLen; i++ {
		configs = append(configs, sharpmem.Config{
			Width:                configs[i].Width,
			Height:               configs[i].Height,
			DisableOptimizations: true,
		})
	}

	err := initSPI()
	if err != nil {
		println(err.Error())
		return
	}

	display := sharpmem.New(machine.SPI0, machine.P0_06)

	println("initialized")

	for {
		for _, cfg := range configs {
			print("=== Testing resolution: ", cfg.Width, "x", cfg.Height, " (optimizations: ", !cfg.DisableOptimizations, ")", "\n")

			display.Configure(cfg)

			for it := range 10 {
				time.Sleep(500 * time.Millisecond)

				switch it {
				case 0, 2, 4:
					for x := range cfg.Width {
						for y := range cfg.Height {
							display.SetPixel(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 255})
						}
					}

					err = display.Display()
					if err != nil {
						println(err.Error())
					}

					println("all pixels black")

				case 1:
					err := display.Clear()
					if err != nil {
						println(err.Error())
					}
					println("all pixels white (display.Clear)")

				case 3:
					display.ClearBuffer()
					err = display.Display()
					if err != nil {
						println(err.Error())
					}
					println("all pixels white (display.ClearBuffer + display.Display)")

				case 5:
					for x := range cfg.Width {
						for y := range cfg.Height {
							display.SetPixel(x, y, color.RGBA{R: 0, G: 0, B: 0, A: 255})
						}
					}

					err = display.Display()
					if err != nil {
						println(err.Error())
					}
					println("all pixels white (display.SetPixel + display.Display)")

				case 6:
					println("scan right")
					for x := range cfg.Width {
						for y := range cfg.Height {
							display.SetPixel(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 255})
						}
						err = display.Display()
						if err != nil {
							println(err.Error())
						}
					}

					err = display.Clear()
					if err != nil {
						println(err.Error())
					}
					println("all pixels white (display.Clear)")

				case 7:
					println("scan down")
					for y := range cfg.Height {
						for x := range cfg.Width {
							display.SetPixel(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 255})
						}
						err = display.Display()
						if err != nil {
							println(err.Error())
						}

						// scan down is really fast because of the line-diffing (when enabled)
						//time.Sleep(16 * time.Millisecond)
					}
					err = display.Clear()
					if err != nil {
						println(err.Error())
					}
					println("all pixels white (display.Clear)")

				case 8:
					display.ClearBuffer()

					for x := range cfg.Width / 2 {
						for y := range cfg.Height {
							display.SetPixel(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 255})
						}
					}

					for range 10 {
						err = display.Display()
						if err != nil {
							println(err.Error())
						}

						time.Sleep(100 * time.Millisecond)
					}

					display.ClearBuffer()

					for x := range cfg.Width / 2 {
						for y := range cfg.Height {
							display.SetPixel(x+cfg.Width/2, y, color.RGBA{R: 255, G: 255, B: 255, A: 255})
						}
					}

					for range 10 {
						err = display.Display()
						if err != nil {
							println(err.Error())
						}

						time.Sleep(100 * time.Millisecond)
					}

				}
			}
		}
	}
}

@rdnt rdnt marked this pull request as ready for review December 13, 2024 17:29
@deadprogram
Copy link
Member

@rdnt looking at the sample code, makes me think some predefined config values would be useful.Something like this?

ConfigLS010B7DH04 = Config{Width: 128, Height: 128}
ConfigLS013B7DH03 = ConfigLS010B7DH04
ConfigLS011B7DH03 = Config{Width: 160, Height: 68}
...

@rdnt
Copy link
Author

rdnt commented Dec 18, 2024

@deadprogram excellent suggestion! Added.

@deadprogram
Copy link
Member

@rdnt I think the olny thing missing now is to add an example, and then add building that example to the smoketests here https://github.com/tinygo-org/drivers/blob/dev/smoketest.sh

Thanks!

@rdnt
Copy link
Author

rdnt commented Dec 19, 2024

@deadprogram Added!

What I am not so sure about is the output filetype (hex vs uf2), could you take a look if it's okay as it is?
(Using uf2 because it is a simple drag & drop operation for the actual build to be flashed to the nicenano)

I will do some additional testing with all possible devices for the example itself just to be sure it doesn't cause a panic of some sort and let you know.
(No issues found, considering resolved)

@rdnt
Copy link
Author

rdnt commented Dec 19, 2024

I noticed on the repo's README that some of the pins for the examples are specified using global variables for easy tweaking by new users. I'll update the example to do the same and maybe add a wiring diagram for the pins.

@rdnt
Copy link
Author

rdnt commented Dec 19, 2024

I noticed on the repo's README that some of the pins for the examples are specified using global variables for easy tweaking by new users. I'll update the example to do the same and maybe add a wiring diagram for the pins.

Done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants