-
Notifications
You must be signed in to change notification settings - Fork 139
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
Proof of concept for configurable OLED display support (#166) #326
Proof of concept for configurable OLED display support (#166) #326
Conversation
Just glancing over everything quickly. This looks great. It'll take me a few days to get to a full review, but here are just a few things I've noticed briefly looking it over:
Overall, this looks pretty solid already. I'll be able to test with the existing SPI display pretty quickly, but I'll have to see about getting one hooked up on I2C. So if you've already validated that, that'd be good to know as well. Thanks a ton for the contribution! |
Thanks for the feedback, I'll have a look at the points you mention later this week. The implementation in its current form has been tested on three (simultaneous) SSD1306 OLEDs - I2C_1, I2C_4 and SPI: Everything works fine and the refresh rates are good, except a 2px strip of junk on the right of the 128x64 display... I have no idea what is causing that, I'm looking into it. |
Oh that's excellent weird about the noise in the last two columns of pixels on that bigger display. If that's the one you have connected with SPI I'll be able to verify that pretty quickly (that looks identical to the displays I have here). Looking forward to your thoughts on the other point. No rush, I probably won't have time to do any testing/verification on my end until Monday or so anyway. |
@recursinging Good job, this looks very good to me! Your approach is pretty much exactly what I had in mind. My time budget didn't allow me to tackle such a large task lately, but it seems like I can cross this off my todo list because you did an amazing job here! Thanks for pushing this forward, this is great to see! |
I tried it out - it was pretty much plug and play. I'm using the SPI transport with a SSD1306 0.96" display. I'll post a video in slack later. Good job! |
@TheSlowGrowth - glad to hear this PR worked well for you. Out of curiosity, is your 0.96" display also 124x68? In your video, I didn't see the same 2px strip of noise on the right side that I've been getting on mine. It might just be a hardware issue on my end. So I made some changes based on suggestions from @stephenhensley. The result ist relatively straightforward, even if a little verbose. Usage examples now look like this for SPI:
...and for I2C:
I've decided against a general In terms of code ergonomics - a point where libDaisy really (really) excels in comparison to Arduino/Teensy land, and which I'd like to maintain - I'm on the fence about this design, as it exposes a lot of abstractions. Not everyone feels at home in angle-bracket land as I do. I'd definitely be open to suggestions to make this a bit more beginner friendly. @stephenhensley do you use a specific formatting tool, or definition for VSCode? Also, are there any specifics about code documentation you'd like me to take into account? |
@recursinging The init is definitely straightforward. Looks nice. I agree it is a bit verbose, and I agree that a lot of people aren't very comfortable with templates. What we could do is typedef a few standard configurations and their transports. That doesn't necessarily simplify much, but it would make it look less confusing to those coming from C or other languages. In terms of simplification I don't have a ton of ideas, but if we're not using a separate buffer type, and there isn't any inherent configuration to be done on the My reasoning behind wanting to provide the buffer externally was so that it could be located in the uncacheable SRAM if/when we want to add DMA transactions to the transport classes. The tricky thing with libdaisy in general is being able to provide the high level functions that might be familiar, or even comfortable for beginners/newcomers, while still exposing a mid level hierarchy that can be used for maximum flexibility. Right now with the breakout boards, I think we accomplish that quite well. It's just once you're working on a custom board, or the seed on a breadboard, I can see how things can become daunting pretty quickly. I'm always open to ideas to making everything more accessible. As for formatting, we use clang-format to do code styling. There is a You can also verify that it will pass CI by using the following helper script: It'll return any issues, or nothing if there are no problems. |
Thanks for the feedback, I'll check out the formatting tools. Pardon my ignorance, but why can the |
No ignorance to pardon, we really don't have any high-level documentation that covers the details of the internal memory use yet. libDaisy enables the Data Cache within the processor to benefit from the added performance it gives. However, any memory section that is cached does not play well with the DMA without adding cache-maintenance functions (clean/invalidate). To simplify this, we have a chunk of memory in the D2 SRAM dedicated to buffers that need access to the DMA in the linker, and configured as non-cache memory by the MPU. The current way to declare memory in this way is using the gcc attribute for memory sections. Since a class is a contiguous block of memory, individual members cannot be declared in a different section of memory. So one could have the global object that contains the OledDisplay, or the OledDisplay itself declared in that memory section, but then all of the non-buffer data would lose any cache benefits as well. So having the buffer be a separate entity (whether a simple array, or a custom type like in the But again, that does make the init process a bit more complicated (similar to how the led driver is). |
Well now I understand. Thanks for the clear explanation. It's important to make sure access to DMA memory is available for this and other future display drivers. I'll mull it over and make some changes when I get a chance. |
Sounds good. I'll think it over as well. This seems like it's going to become a very common thing for device drivers for every peripheral. |
I have a couple of points and suggestions for a simplification, especially for new users without template-experience. Right now, I see three main difficulties for a new user:
Here's an idea how to resolve this: The main goal for me is to have the driver as a member of the display class - and the transport as a member of the driver class. This resolves issue 1 (lifetime management) and 3 (init order). To do this, give the display driver class a class DisplayDriver
{
public:
// this struct is expected here - must be implemented in every DisplayDriver
struct Config
{
// ...
}
// the init functions is expected to have this exact definition:
// note: I'm not passing a const reference here to make clear that the Config struct
// is just a temporary plain-old-data type and doesn't need to be held alive.
void Init(Config config);
}; Then the display class could look like this: template<typename DisplayDriverType>
class OledDisplay
{
public:
void Init(DisplayDriverType::Config driverConfig)
{
dirver_.Init(config);
}
private:
DisplayDriverType driver_;
}; Note that the Display OWNS the driver and driver-dependent configuration is entirely realised via the Config struct. If the driver needs no specific transport abstraction, we're done already. Otherwise THE SAME idea can be used to configure the transport for the driver: class DisplayTransport
{
public:
// this struct is expected here - must be implemented in every DisplayTransport
struct Config
{
// ...
}
// the init functions is expected to have this exact definition:
// note: I'm not passing a const reference here to make clear that the Config struct
// is just a temporary plain-old-data type and doesn't need to be held alive.
void Init(Config config);
};
template<typename TransportType>
class DisplayDriver
{
public:
// this struct is expected here - must be implemented in every DisplayDriver
struct Config
{
// The transport config is placed in the driver Config
TransportType::Config transportConfig;
// ...
}
void Init(Config config)
{
transport_.Init(config.transportConfig);
// ...
}
private:
TransportType transport_;
}; Now we can typedef-away all the complexity for a display like this (using the Daisy Field here as an example, not sure if that's actually correct but you get the idea). using FieldOledDisplayTransportType = daisy::SSD130x4WireSpiTransport;
using FieldOledDisplayDriverType = daisy::SSD130xDriver<128, 64, FieldOledDisplayTransportType>;
using FieldOledDisplay = daisy::OledDisplay<FieldOledDisplayDriverType>; Then all the user has to do is this: FieldOledDisplay display;
void main()
{
FieldOledDisplay::config displayCfg;
displayCfg.transportCfg.pin_config.dc = { DSY_GPIOC, 12 };
displayCfg.transportCfg.pin_config.reset = { DSY_GPIOB, 4 };
display.Init(displayCfg);
} Now the complexity is hidden nicely and the ordering and lifetime problems are gone. |
@TheSlowGrowth ouu. This is well thought out, and definitely provides the user-level simplicity that I think we're aiming for. Definitely a little bit of rework to get us there, but I like it. What do you think @recursinging? Also, if we need more hands on this specific feature, I can create an interim branch for us to work off. |
@stephenhensley - Agreed, the suggestions from @TheSlowGrowth reduced the user facing complexity (and potential foot-guns) considerably. I'd say it's about as intuitive as it can be, considering the abstractions built it, and code-completion of an IDE quickly aids in finding you way there. I've pushed the changes suggested, but I haven't addressed the DMA issue yet. After looking over I think at this point it would be best to switch to an interim branch, as I'm not sure how much help I can be anymore. I do plan on someday trying to port a TFT driver (ILI9341 or ST7735) to this architecture, just to see how it fits. Unfortunately, the Kindergarten just closed again here, so I don't have a lot of spare time at the moment. I can gladly verify changes against my breadboard setup in the future. |
@recursinging This looks great! I like that you type-def'd various typical configurations. I think this is probably the simplest solution we could possibly have while still supporting various transports and display types. I REALLY like that the transport is separated from the actual driver like this. I'll give it a try later today. Assuming that it works, IMO, this could be merged as-is. |
I agree, but the core problem is that all member variables of a class have to be in a continuous memory region. There IS a possible solution to this, but I'm not sure how it would work performance-wise: You can request the D-cache to manually flush / reload a region of memory. We could have the transmit buffers as members of the class and then flush them to memory right before we start the DMA transmission. Given that we would write to those buffers a lot, it might even improve performance compared to the existing solution: If the buffers sit in the D-cache-disabled section of memory, then each access to the buffer is potentially slowed down compared to accessing some memory where the cache is enabled. This is probably not a problem for the led driver but it could be a problem for the display, where the buffer is much larger and drawing operations could cause a lot of accesses to the buffer. |
I had the same though after I saw Alternatively, I was wondering if it might be possible to have template peripheral handle classes which allocate their own transmit and receive buffers sized by the template type(s). The entire peripheral handle class instance could be placed in |
Let's discuss this in an issue and keep the display driver PR free of noise. #335 |
This is fantastic. I also like the typedefs for typical display types. I agree that it can't really get a whole lot simpler. I agree that pending some testing/verification this is good to merge as-is, and we can handle the concerns with DMA separately (since SPI still doesn't have DMA transfers set up yet anyway). Really great work and thanks a ton for the contribution! I haven't had a chance to run this on my field or patch, but if you both have tested it I'll be okay with merging it as well. That said, I might be able to find sometime this afternoon to do a quick test on the field or something. |
Actually, I was able to flash up one of the examples (Quad Mixer) and drop it into the patch and field to make sure the display lights up, and doesn't have any noise, etc. So I think I'm signing off here. If you guys have tried it and haven't found any other issues, or changes we should make, I think it's ready to merge. |
Alright. I'm taking the rocket-ship, and otherwise silence as a sign that neither of you have found any other issues. Merging! Thanks again, @recursinging for the excellent work, and @TheSlowGrowth for the feedback and testing. You both rock! |
Hi,
this is a suggestion for adding support for configurable OLED displays to libDaisy.
In advance, I'm a Java dev by profession, and not at all versed in the best practices of C++, so please take this merely as a best attempt.
My primary motivation to get configurable support for OLED displays is my kxmx_bluemchen Eurorack module, which makes use of a 64x32 SSD1306 OLED via I2C. This obviously doesn't work without modifying libDaisy, so I thought I'd try my hand at a contribution.
As suggested by @TheSlowGrowth in #166, I've used templates to provide configurable specialization to the
OledDisplay
Class. After experimenting with this idea, I determined that an additional abstraction might be useful, as the drawing functions of theOledDisplay
class, are not really specific to the SSD1306/SSD1309. Since there are a other monochrome OLED driver chips, such as the SH1106, I found it prudent to provide both a driver and a transport abstraction. The result is a more complicated, but more flexible initialization process. For example the Daisy Patch:...I have a tendency to over-engineer things, which might be the case here, but I find the flexibility appealing. In the case of the kxmx_bluemchen, the initialization routine now looks like this:
There is one aspect of this design with which I am not satisfied, and that is the display buffer. Because the template does not communicate any dimension information, there is no way (that I am aware of) to statically allocate a display buffer sized to the display dimensions. In this PR, I've simply allocated the max possible (128x64) space to the buffer, which is partially unused if the display is smaller, as is the case for my kxmx_bluemchen module. Perhaps there is another way to realize this?
The pattern used here also lends itself for future implementations of specialized drawing classes like
ColorOledDisplay<Driver<Transport>>
,GrayscaleOledDisplay<Driver<Transport>>
LcdTftDisplay<Driver<Transport>>
,LcdGraphicDisplay<Driver<Transport>>
orLcdCharacterDisplay<Driver<Transport>>
.Please advise if this idea has legs.
Sam