tttapa / Control-Surface

Arduino library for creating MIDI controllers and other MIDI devices.
GNU General Public License v3.0
1.19k stars 134 forks source link

8 SSD1306 0.66 64X48 OLEDS #164

Open henkmeid opened 4 years ago

henkmeid commented 4 years ago

Hello,

I ran into this project and i might be suitable for my needs. I have basically 2 questions:

  1. Is it possible to use a smaller size SPI 7pin OLED? I have 64x48.
  2. Is it possible to use 8 OLEDS by using extra digital pins for the CS lines?

Great project!

tttapa commented 4 years ago

Is it possible to use a smaller size SPI 7pin OLED? I have 64x48.

Yes, this shouldn't be an issue. The OLED examples all use the Adafruit_SSD1306 library, so if that library supports your display, it should work. You can simply change the resolution and the pin configuration. I've successfully used it with both I²C and SPI variants of the SSD1306.

Is it possible to use 8 OLEDS by using extra digital pins for the CS lines?

Yes, but there's a caveat: the Adafruit_SSD1306 library allocates a separate buffer for each display you add. This requires huge amounts of RAM if you have many displays.
The Control Surface library keeps all displays and display elements in order, so it only draws to one display buffer at a time, which means that you can reuse one buffer for all displays.
The standard Adafruit_SSD1306 doesn't support this, but you can use my fork, which does support it: https://github.com/adafruit/Adafruit_SSD1306/pull/149

henkmeid commented 4 years ago

Cool!

Yes, but there's a caveat: the Adafruit_SSD1306 library allocates a separate buffer for each display you add. This requires huge amounts of RAM if you have many displays.

I would like to keep the code as standard as possible. Would board with larger RAM, such as an Arduino Mega, work?

tttapa commented 4 years ago

It depends on what else you have going on in your sketch. You need 48×64×8 bits of RAM for the display buffers, which is 3 KiB. With the improved SSD1306 library, you only need 384 bytes.

I wouldn't recommend an Arduino Mega for MIDI controllers, you can find more info here: https://tttapa.github.io/Control-Surface-doc/Doxygen/d8/d4a/md_pages_MIDI-over-USB.html

henkmeid commented 4 years ago

It depends on what else you have going on in your sketch.

Basically all i want to see are tracknames, maybe the VU. Pan, Solo, Mute, etc i have already on my existing control surface.

tttapa commented 4 years ago

You can try compiling the code for an Arduino Mega, add 3 KiB, and make sure you have at least a couple of hundreds of bytes left for the stack.

If you just want some displays, I don't think an Arduino Mega is the best board choice, especially if you want MIDI over USB.
Personally, I'd probably use an Arduino Micro/Leonardo with the improved SSD1306 library, or a Teensy LC (has 8K of RAM and native USB support).

henkmeid commented 4 years ago

I think I will go with a Teensy.

Out or curiosity, a teensy 3.2 supports 4 usb-to-midi devices. Would I be able to make this 4x with one controler. So 32 display over 4 mackie contollers?

I will take it step by step though, first I want to get this working with 1 midi device and 8 screens. But I have 40 faders on my current control surface.

henkmeid commented 4 years ago

Or would a better approach be a seperate controller per 8 oleds and rename the controlers?

tttapa commented 4 years ago

Teensy 3.2 even supports 16 USB MIDI cables. You can map them as Mackie Control Extenders in your DAW, for instance (keep in mind that your DAW may limit the number of extenders).

The Control Surface library supports up to 16 different USB MIDI cables as well. You can use the third optional cable field of the MIDIAddress class.

Or would a better approach be a seperate controller per 8 oleds and rename the controlers?

I don't think that has any advantage over using a single controller, it's probably more complicated.

henkmeid commented 4 years ago

Wow, you are fast in your replies!

I don't think that has any advantage over using a single controller, it's probably more complicated.

The reason i'm asking this, is the choice of controller, if i would use a single controller, i would need more digital pins of all the cs lines. Do you think a teensy 3.5 or 3.6 would be good enough to handle 40 screens?

tttapa commented 4 years ago

40 screens really is a lot, and it would probably require a lot of bandwidth to refresh all of them. For static things like track names, this is not an issue, but for VU meters it could be.

You can use shift registers or demultiplexers to handle the cs lines, this would probably require just a small tweak to the SSD1306 library, and it might be worth the $10 you save by using a Teensy 3.2 over a Teensy 3.6.
Even the Teensy 3.5/6 only have 40 breadboard-accessible IO in total.

henkmeid commented 4 years ago

Alright, I will start with an Teensy 3.2 and 8 OLEDs and keep you up to date.

henkmeid commented 4 years ago

Update. I have now got 2 screens hooked up according to the example. First, only the second screen worked with a shared reset pin. But when i uses 2 seperate reset pins it works. Is this normal behaviour? Or should this be working with 1 shared reset pin? Or could I be using wrong capacitor type and values? Does it need to be ceramic or elco?

Also i cannot get tracknames and time to show on the screens. VU, Pan, Rec, Mute and Solo works. I am using Cubase 9.5 pro. Any suggestions?

henkmeid commented 4 years ago

Just tried with Reaper and tracknames now showing. Any thoughts on how to get this working with cubase?

henkmeid commented 4 years ago

I got 4 Oleds working now. Although its with 4 seperate reset pin. When I try one reset pin and connect them all together only the last screen works, the rest remains black. Its a waste of digital IO pins as i am looking to connect 8.

tttapa commented 4 years ago

If you use an RC circuit for the reset pin you don't have to connect it to an IO pin.

If you do want to use an IO pin for the reset line, you can share a single IO pin for all displays, but you have to pass reset = false to the Adafruit_SSD1306::begin method for all displays except for the first one:

https://github.com/adafruit/Adafruit_SSD1306/blob/66cba544151eef5f39a6d9689a773bb2f04fbd5e/Adafruit_SSD1306.cpp#L432-L439

I've never used Cubase, so I'm afraid I can't help you with that. If Cubase fully supports the MCU protocol, it should work, but if Cubase doesn't send the track names, there's no way to display them on the Arduino. You can't really "request" them from the DAW, communication is mostly one way.

You could try the Mackie-Control-Universal-Reverse-Engineering.ino to see what Cubase is sending, or you could try a MIDI monitor on your computer.

If you figure out how Cubase sends the track names, I can tell you how to receive them with the library, but I can't test it myself, because I don't have Cubase.

The text is most likely sent as a SysEx message, you can look for 20 in the data (0x20 is the ASCII code for a space, most text fields will contain some spaces).

henkmeid commented 4 years ago

That’s some very useful information! I will give it a try. I assume Cubase is fully MCU compatible. But I will use the monitor to see what type of messages it is sending.

henkmeid commented 4 years ago

If you use an RC circuit for the reset pin you don't have to connect it to an IO pin.

Works! Bit dodgy though, probably need to change the capacitor with a different value. Sometimes one of the screens doesn’t light up, but resetting everything again does the trick.

henkmeid commented 4 years ago

You could try the Mackie-Control-Universal-Reverse-Engineering.ino to see what Cubase is sending, or you could try a MIDI monitor on your computer.

By running this code, when I set channnel 1 to trackname "Henk", i get the following result:

SysEx: f0 00 00 66 14 12 39 20 48 65 6e 6b f7 on cable 0

tttapa commented 4 years ago

The format is correct. The problem lies in how the message is displayed by the MCU::LCDDisplay class: On the original Mackie Control Universal, you had one large LCD display, so you could also display messages across the different channel strips. If you split up the channels like on an OLED display, you need a way do determine if track names are displayed, or if the display is used for one long message across all channels.
The Control Surface library currently checks the following:

https://github.com/tttapa/Control-Surface/blob/ac372918071bd18f243ff77dd180a30d5a105fd2/src/Display/MCU/LCDDisplay.hpp#L78-L102

This worked for Tracktion and Reaper, but apparently, other DAWs don't always write spaces between channels.

For now, you can just remove the check here: https://github.com/tttapa/Control-Surface/blob/ac372918071bd18f243ff77dd180a30d5a105fd2/src/Display/MCU/LCDDisplay.hpp#L58-L60

Long-term, I don't know a single solution that would work on all DAWs, if you have any ideas, feel free to let me know!

henkmeid commented 4 years ago

Thanks! That makes sense. I noticed a difference with Cubase and Reaper with the output on the monitor, Reaper sends out 3 extra spaces.

So I commented out the suggested piece of code and now info is showing up on my screens, but not the correct one yet. Cubase sends out 2 lines per channel, so now i'm seeing at the first screen "Pan", on the 3rd "Left". How can i get the second line to show with MCU::LCDDisplay?

Example on how Cubase outputs on Mackie Control.

[]https://dt7v1i9vyp3mf.cloudfront.net/styles/header/s3/imagelibrary/m/mackiecontrol1-RiCgQ9ziij_q4fn2FGQdMTCCjm6WkUsh.jpg

tttapa commented 4 years ago

There's a constructor that takes an extra line argument:

https://github.com/tttapa/Control-Surface/blob/ac372918071bd18f243ff77dd180a30d5a105fd2/src/Display/MCU/LCDDisplay.hpp#L38-L55

If you pass 0, it'll display the first line, if you pass 1 it'll display the second line.

henkmeid commented 4 years ago

Succes!! Will post findings tomorrow.

IMG_7268

henkmeid commented 4 years ago

I'm still having some issues with some of the oled not starting every time, its random which ones (can be multiple) they are. Does each OLED needs its own reset RC circuit (capacitor, resistor), or can the share one circuit with all 8 of them?

Alternatively:

If you do want to use an IO pin for the reset line, you can share a single IO pin for all displays, but you have to pass reset = false to the Adafruit_SSD1306::begin method for all displays except for the first one: https://github.com/adafruit/Adafruit_SSD1306/blob/66cba544151eef5f39a6d9689a773bb2f04fbd5e/Adafruit_SSD1306.cpp#L432-L439

How do I achieve this? Is there a place in my sketch where I can put this per display or do I have to amend something in another file?

tttapa commented 4 years ago

I'm still having some issues with some of the oled not starting every time, its random which ones (can be multiple) they are. Does each OLED needs its own reset RC circuit (capacitor, resistor), or can the share one circuit with all 8 of them?

Does this happen when powering/plugging in the Arduino?
You could try increasing the capacitance or lowering the resistance.
Sharing them shouldn't be an issue.

How do I achieve this? Is there a place in my sketch where I can put this per display or do I have to amend something in another file?

You could do something like this (in your sketch):

// Implement the display interface, specifically, the begin and drawBackground
// methods.
class MySSD1306_DisplayInterface : public SSD1306_DisplayInterface {
 public:
  MySSD1306_DisplayInterface(Adafruit_SSD1306 &display)
    : SSD1306_DisplayInterface(display) {}

  void begin() override {
    // Initialize the Adafruit_SSD1306 display
    if (!disp.begin(SSD1306_SWITCHCAPVCC, 0, first))
      FATAL_ERROR(F("SSD1306 allocation failed."), 0x1306);

    first = false;

    // If you override the begin method, remember to call the super class method
    SSD1306_DisplayInterface::begin();
  }

  void drawBackground() override { disp.drawLine(1, 8, 126, 8, WHITE); }

 private:
  static bool first;
};
bool MySSD1306_DisplayInterface::first = true;
henkmeid commented 4 years ago

Does this happen when powering/plugging in the Arduino?

Both on power-up and reset.

You could do something like this (in your sketch):

Thanks, that increased the stability a little bit. Though sometimes I have to reset the controller a few times to light them up all at the same time. Could it be that signals are a bit noisy and interfered due to my setup (lots of wires and breadboard)? Or could I have a faulty oled among them which causes the whole lot to have this behaviour?

henkmeid commented 4 years ago

I have designed the following PCB:

Oled_PCB

Any thoughts?

tttapa commented 4 years ago

lowering the resistance

Sorry, that should be increasing as well, of course. You want to increase the RC time.

Thanks, that increased the stability a little bit. Though sometimes I have to reset the controller a few times to light them up all at the same time. Could it be that signals are a bit noisy and interfered due to my setup (lots of wires and breadboard)? Or could I have a faulty oled among them which causes the whole lot to have this behaviour?

Is this using a GPIO pin for the reset line? Did you pass the right pins to the constructors for your displays? When using this approach, you have to leave out the capacitor and the resistor.

You could also try adding some delay at the start of your setup, to make sure the displays are out of reset when you start sending data to them.

I've never tried more than two displays, so I've never had this problem. You might have more luck asking this question on the Arduino forum, for example?

The PCB looks nice, though!

henkmeid commented 4 years ago

Is this using a GPIO pin for the reset line? When using this approach, you have to leave out the capacitor and the resistor.

Yes, while using the GPIO pin, and i have removed the capacitor and resistor. I think such a lot of jumperwires on a breadboard are not the best connections, also when i touch them i see some strange jumping pixels on some of the screens. I think i'll order some PCB's online and see if that makes any difference.

Did you pass the right pins to the constructors for your displays?

henkmeid commented 4 years ago

@tttapa are you from the Netherlands?

tttapa commented 4 years ago

I'm from Belgium.
Are you from the Netherlands by any chance?

henkmeid commented 4 years ago

Are you from the Netherlands by any chance?

Yes I am! 😀

henkmeid commented 4 years ago

I have ordered the PCB's from JLCPCB, this can take a couple a days.

In the meantime: Is it possible to invert a display display.invertDisplay(true); when a track is in Record Arm / Ready and false when not? So not all displays, but just the one(s) which are in Record Arm / Ready.

Ps. let me know if this I should make a seperate issue for this.

tttapa commented 4 years ago

Sure, you could use a NoteRange<8> to listen for MIDI input for the MCU::REC_RDY_1 address. You could use the generic version to attach your own callback.

I'll see if I can post some example code later today.

henkmeid commented 4 years ago

I'll see if I can post some example code later today.

Thanks that would be nice!

tttapa commented 4 years ago

I think something like this should work, but I don't have the hardware to test it right now

#include <Adafruit_SSD1306.h>
#include <Control_Surface.h>

Adafruit_SSD1306 displays[8] = {
  /* ... */
};

struct InvertDisplayCallback {
  Adafruit_SSD1306 *displays;

  template <class Input> 
  void begin(const Input &) {}

  template <class Input>
  void update(const Input &input, uint8_t index) {
    displays[index].invertDisplay(input.getValue(index) >= 0x40);
  }

  template <class Input>
  void updateAll(const Input &input) {
    for (uint8_t i = 0; i < input.length(); ++i)
      update(input, i);
  }
};

GenericNoteRange<8, InvertDisplayCallback> invert_disp_arm = {
  MCU::REC_RDY_1, // first address in the range
  {displays},     // InvertDisplayCallback constructor
};
henkmeid commented 4 years ago

@tttapa Thanks man! I really appreciate your help! My PCB's will arrive this week and then I will give it a try.

henkmeid commented 4 years ago

Hey, PCB's are in! It looks awesome, see below for photo's and what I'm intending it to use for.

Unfortunately, I'm still having issues resetting 8 displays at once, both with a single I/O pin or a reset RC circuit. I've tested it out with other displays, both 128x64 and 64x48, but no luck. The only way I get it working properly is by using a seperate I/O pin for reset for each display, this works like a charm without any glitches. So I will go that route, because of the power limitations of the teensy I cannot get the amount displays I want anyway, so I have enough I/O pins to use.

Right now i'm using the folowing (RESET, CS):

Display 1: 0, 1 Display 2: 2, 3 Display 3: 4, 5 Display 4: 6, 8 Display 5: 9, 10 Display 6: 11, 14 Display 7: 15, 16 Display 8: 18, 19

So I will start designing a new PCB with this current pin configuration, but now I will make one for all 8 displays and also put the teensy directly on the PCB, so I don't have to solder any wires. I will have nice compact package and the only thing connecting will be the USB port.

And then I have to figure out how to get multiply teensies working at the same time, I have found out how to change the device name, so I guess this won't be much of an issue.

I will post my new PCB layout once it's done. In the mean time, any suggestions are welcome!

IMG_7309

IMG_7306

IMG_7307

IMG_7308

tttapa commented 4 years ago

The PCB looks very nice!

I'm afraid I can't really comment on the reset issues you're having. I've only ever used a maximum of two displays at a time.
Maybe you could ask a question about it on the Arduino forum?

In theory, it shouldn't matter how you reset the displays, you just pull down the reset pin for a long enough time before starting to write data to it. Whether you do this using a single IO pin, or with one pin per display shouldn't matter.
But as is often the case with electronics, theory and practice don't really seem to match here ...

henkmeid commented 4 years ago

So here is my updated PCB. It fits 8 0.66 64x48 OLED's on the front and a teensy 3.2 on the back. I have also broken out the remaining I/O pins for possible future use as well as GND and 3.3V.

8xOLED_PCB_TEENSY32_BREAKOUT

I will check all traces and routes once more before ordering, so fingers crossed and wait again for 10 days 😂

henkmeid commented 4 years ago

I discovered a tiny issue on my test bench:

IMG_7352

As you see, screen 3 only shows the line because of disp.drawLine(1, 8, 126, 8, WHITE); in the code, so initialises good, but when i write the tracknumbers and pan info to all screen is skips the 3rd screen. Any ideas?

Ps. the other screens work fine, its just screen refreshing while making a photo.

tttapa commented 4 years ago

What code are you using?

henkmeid commented 4 years ago

It's basically the example code for 2 displays, but added the extra displays.

henkmeid commented 4 years ago

Found it! I use pin 5 as CS line for the 3rd screen, but it was also assigned to a push button here: IncrementSelector<2> bankselector = {bank, 5}; Changed that to a different pin which was not in use and it worked! 😅

henkmeid commented 3 years ago

@tttapa Update:

image

image

image

tttapa commented 3 years ago

That looks really nice, thanks for sharing! I love the aluminium aesthetics.

Do you happen to have a link to where you got those OLED displays?

You might want to keep an eye out for the next release (or try out the new-input branch), because the new version has some significant improvements in the MIDI input and display code, it redraws only the displays that contain an element whose value changed. (There are some minor breaking changes mentioned in the readme if you want to convert your code.)

henkmeid commented 3 years ago

That looks really nice, thanks for sharing! I love the aluminium aesthetics.

Thanks! It's 2mm aluminum sheet cut with a CNC machine.

Do you happen to have a link to where you got those OLED displays?

I got them from aliexpress, just search for 7pin SPI 0.66" OLED. This is the one i've bought: https://nl.aliexpress.com/item/32835034812.html?spm=a2g0s.9042311.0.0.1b994c4d7rqU5t

You might want to keep an eye out for the next release (or try out the new-input branch)

I'm still working with the branch which i qot in april, so i need to catch up anyway. The scanlines are not visible with the eye, only when trying to make a photo or video. But 'm looking forward to the next release.

2 quick questions I have.

  1. Is it possible to send static text to individual screens?
  2. And is it possible to process and alter the text displayed from the MCU? For example if text = "A" then "B" else "A"
henkmeid commented 3 years ago

I got them from aliexpress, just search for 7pin SPI 0.66" OLED. This is the one i've bought: https://nl.aliexpress.com/item/32835034812.html?spm=a2g0s.9042311.0.0.1b994c4d7rqU5t

Bear in mind that these are 64x48 pixel displays. I couldn't get them working by configuring them as 64x48 in the code, lots of static and artifacts. So i configured them as 128x64 and only use the visible space, so top left pixel would be 32, 16.

For example the v-pot on screen 1: {display_1, vpot[0], {32, 16}, 14, 12, WHITE}

tttapa commented 3 years ago

Is it possible to send static text to individual screens?

Yes, but not in the main loop. There are two ways to do this:

  1. Add an identifier to your display interface and add some logic to the drawBackground() method to write different text to each display, depending on the identifier.
  2. Create a class that inherits from DisplayElement and draws the custom text in the draw() method. Each DisplayElement is associated with a specific display, so you can have different display elements with different content on different displays.

Here's an example that demonstrates both approaches:

#include <Control_Surface.h> // Include the Control Surface library
// Include the display interface you'd like to use
#include <Display/DisplayInterfaces/DisplayInterfaceSSD1306.hpp>

USBDebugMIDI_Interface midi; // Control Surface always needs at least one MIDI interface

// ----------------------------- Display setup ------------------------------ //

constexpr uint8_t SCREEN_WIDTH = 128;
constexpr uint8_t SCREEN_HEIGHT = 64;

constexpr int8_t OLED_DC = 17;    // Data/Command pin of the display
constexpr int8_t OLED_reset = -1; // Use the external RC circuit for reset
constexpr int8_t OLED_CS_L = 10;  // Chip Select pin of the left display
constexpr int8_t OLED_CS_R = 18;  // Chip Select pin of the right display

constexpr uint32_t SPI_Frequency = SPI_MAX_SPEED;

// Instantiate the displays
Adafruit_SSD1306 ssd1306Display_L {
  SCREEN_WIDTH, SCREEN_HEIGHT, &SPI,          OLED_DC,
  OLED_reset,   OLED_CS_L,     SPI_Frequency,
};
Adafruit_SSD1306 ssd1306Display_R {
  SCREEN_WIDTH, SCREEN_HEIGHT, &SPI,          OLED_DC,
  OLED_reset,   OLED_CS_R,     SPI_Frequency,
};

// --------------------------- Display interface ---------------------------- //

// Implement the display interface, specifically, the begin and drawBackground
// methods.
class MySSD1306_DisplayInterface : public SSD1306_DisplayInterface {
  public:
    MySSD1306_DisplayInterface(Adafruit_SSD1306 &display, uint8_t id)
      : SSD1306_DisplayInterface(display), id(id) {}

    void begin() override {
      // Initialize the Adafruit_SSD1306 display
      if (!disp.begin())
        FATAL_ERROR(F("SSD1306 initialization failed."), 0x1306);

      // If you override the begin method, remember to call the super class method
      SSD1306_DisplayInterface::begin();
    }

    void drawBackground() override {
      setCursor(32, 32);
      setTextColor(WHITE);
      setTextSize(1);
      print("Display ");
      print(id);
    }

    uint8_t id;
} display_L {ssd1306Display_L, 1}, 
  display_R {ssd1306Display_R, 2};

// ------------------------- Custom display element ------------------------- //

class CustomDisplayElement : public DisplayElement {
  public:
    CustomDisplayElement(DisplayInterface &display, const char *text)
      : DisplayElement(display), text(text) {}
  private:
    void draw() override {
      display.setCursor(32, 40);
      display.setTextColor(WHITE);
      display.setTextSize(1);
      display.print(text);
      drawn = true;
    };
    bool getDirty() const override {
      return !drawn;
    }
    bool drawn = false;
    const char *text;
};

CustomDisplayElement custom_L {display_L, "Custom left"};
CustomDisplayElement custom_R {display_R, "Custom right"};

// ------------------------------ Setup & Loop ------------------------------ //

void setup() {
  Control_Surface.begin();
}

void loop() {
  Control_Surface.loop();
}

This code is for the new-input branch. I don't really have time to test it on the master branch, but the approach should be very similar. The main difference will be that new-input has a DisplayElement::getDirty() method to signal when it should be re-drawn to the display, while master does not.

On the new-input branch, when you call Control_Surface.loop(), it checks the results of getDirty() for all display elements, and if one of them returns true, it clears the display of that element, draws the background of the display (using the DisplayInterface::drawBackground() method), and then re-draws all display elements on that display (even the ones that weren't dirty, and in the order they are defined in in your code). Displays where none of the display elements are dirty are not re-drawn (neither are displays without any display elements).

On the master branch and in previous releases, there is no way to tell which elements have to be re-drawn, so they are just updated continuously, even if nothing changed.

tttapa commented 3 years ago

And is it possible to process and alter the text displayed from the MCU? For example if text = "A" then "B" else "A"

Yes, basically you just copy the LCDDisplay class and add your own logic to the draw() method. It's a display element, just like the CustomDisplayElement class above.

To get the text that the DAW sent, you can use the MCU::LCD::getText() method. It returns a pointer to an array of 112 characters. The first 56 characters are the first line, characters 57-112 are the second line. There are 7 characters per track (except the rightmost track, which only has 6, IIRC, the 7th character of that track is not visible on the original MCU, but you can still read the character, the memory is there).

Edit: just verified it, from the Logic Control manual:

There are 7 displayed characters per channel, with the exception of channel 8, which displays only the first 6 characters. Internally however, the LCD stores 2 x 56 characters.

It might all be a bit abstract, if you get stuck, feel free to post your code here and I'll have a look at it.

henkmeid commented 3 years ago

Man, that's awesome!

When this whole covid thing is over and we are allowed to travel again, we should meet up and i'll buy you a beer! 🍺