Add support for 64 pixel high SSD1306 displays

Changes to the velocity scaling logic were also made; mt32lcd now
normalizes the MT-32 part velocity to a floating point number between 0
and 1. The LCD driver can then translate this into a bar pixel height as
required.
This commit is contained in:
Dale Whinham committed 2020-11-08 18:24:32 +00:00
1 parent c6289b9ed1
commit e7a8a70fe1
10 files changed
+139 -82

No files matched your search

+3
View File
@@ -10,10 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Ability to invert the display orientation for SSD1306 (new configuration file option).
- Support for 64 pixel high SSD1306 OLED displays - many thanks to @ctrl_alt_rees for donating a screen!
### Fixed
- SSD1306 text alignment was off by one pixel.
- SSD1306 part level meters' top pixels would flicker with long sustained notes.
- Part level meters would suddenly snap to a lower level if a quiet note followed a loud note instead of falling off.
- Possibility of overflowing text in HD44780 Print() function.
## [0.6.2] - 2020-10-18
+4 -4
View File
@@ -228,9 +228,7 @@ As with all I2C devices, you must know the LCD's I2C address in order for it to
The SSD1306 controller is found in mini 128x32 and 128x64 OLED displays, which are well-known for their use in FlashFloppy/Gotek devices. They can be found for very little money on eBay and AliExpress.
Currently, only the 32-pixel high version is supported, although if someone were to send me a 64-pixel high model, I'd be glad to test it and add support!
These displays usually have an I2C address of `0x3c`.
Both 32 and 64 pixel high variants are supported. These displays usually have an I2C address of `0x3c`.
### Compatibility
@@ -240,6 +238,7 @@ The following displays and configurations have been confirmed as working by our
|----------------|-----------------|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
| BuyDisplay.com | [2002-1 Series] | `type = hd44780_i2c`, `width = 20`, `height = 2`, `i2c_lcd_address = 27` | Very bright and inexpensive 20x2 LCD. Tested by @dwhinham. |
| Generic | [128x32 OLED] | `type = ssd1306_i2c`, `width = 128`, `height = 32`, `i2c_lcd_address = 3c` | Extremely cheap yet nice and bright mini OLED. Widely available on AliExpress and eBay. Tested by @dwhinham. |
| Generic | [128x64 OLED] | `type = ssd1306_i2c`, `width = 128`, `height = 64`, `i2c_lcd_address = 3c` | Similar to the 32 pixel high OLED, just with more rows. Tested by @dwhinham. |
| Raystar | [REC002004B] | `type = hd44780_4bit`, `width = 20`, `height = 4` | High-contrast 20x4 OLED display. Tested by @dwhinham. |
## 🧠 ROM support
@@ -323,7 +322,8 @@ This project, just like [Munt], has no affiliation with Roland Corporation. Use
- The [inih] project for a nice, lightweight config file parser.
[1]: http://www.arvydas.co.uk/2013/07/cheap-usb-midi-cable-some-self-assembly-may-be-required/
[128x32 OLED]: https://www.aliexpress.com/item/32661842518.html?spm=a2g0s.9042311.0.0.27424c4dSo7J9L
[128x32 OLED]: https://www.aliexpress.com/item/32661842518.html
[128x64 OLED]: https://www.aliexpress.com/item/32233334632.html
[2]: https://karusisemus.wordpress.com/2017/01/02/cheap-usb-midi-cable-how-to-modify-it/
[3]: http://www.fm-alive.com/Pages/DXMidi.aspx
[2002-1 Series]: https://www.buydisplay.com/character-lcd-display-module/20x2-character
+5 -5
View File
@@ -52,20 +52,20 @@ protected:
// 20 characters plus null terminator
static constexpr size_t TextBufferLength = 20 + 1;
// MIDI velocity range [1-127] to bar graph height range [0-16] scale factor
static constexpr float VelocityScale = 16.f / (127.f - 1.f);
static constexpr unsigned MessageDisplayTimeMillis = 200;
static constexpr unsigned TimbreDisplayTimeMillis = 1200;
static constexpr float BarFalloff = 1.0f / 16.0f;
static constexpr float PeakFalloff = 1.0f / 64.0f;
TState m_State;
unsigned m_nStateTime;
// MT-32 state
char m_TextBuffer[TextBufferLength];
u8 m_nPreviousMasterVolume;
u8 m_PartLevels[9];
u8 m_PeakLevels[9];
float m_PartLevels[9];
float m_PeakLevels[9];
u8 m_PeakTimes[9];
};
+1 -1
View File
@@ -53,7 +53,7 @@ private:
void ClearPixel(u8 nX, u8 nY);
void DrawChar(char chChar, u8 nCursorX, u8 nCursorY, bool bInverted = false, bool bDoubleWidth = false);
void DrawPartLevels(u8 nRow, bool bDrawPeaks = true);
void DrawPartLevels(u8 nFirstRow, bool bDrawPeaks = true);
CI2CMaster* m_pI2CMaster;
u8 m_nAddress;
+15 -1
View File
@@ -35,13 +35,27 @@
namespace Utility
{
// Templated function for clamping a value between a minimum and a maximum{
// Templated function for clamping a value between a minimum and a maximum
template <class T>
static inline T Clamp(T value, T min, T max)
{
return (value < min) ? min : (value > max) ? max : value;
}
// Templated function for taking the minimum of two values
template <class T>
static inline T Min(T lhs, T rhs)
{
return lhs < rhs ? lhs : rhs;
}
// Templated function for taking the maximum of two values
template <class T>
static inline T Max(T lhs, T rhs)
{
return lhs > rhs ? lhs : rhs;
}
// Return number of elements in an array
template<class T, size_t N>
constexpr size_t ArraySize(const T(&)[N]) { return N; }
+1 -1
View File
@@ -168,7 +168,7 @@ width = 128
#
# Same characters vs. pixels considerations as for width.
#
# Values: 2-32 (2*)
# Values: 2-64 (2*)
height = 32
# Set address (hexadecimal) of I2C LCD.
+6 -4
View File
@@ -224,7 +224,8 @@ void CHD44780Base::DrawPartLevelsSingle(u8 nRow)
for (u8 i = 0; i < 9; ++i)
{
lineBuf[i * 2] = BarChars[m_PartLevels[i] / 2];
const u8 charIndex = static_cast<u8>(m_PartLevels[i] * 8);
lineBuf[i * 2] = BarChars[charIndex];
lineBuf[i * 2 + 1] = ' ';
}
@@ -240,15 +241,16 @@ void CHD44780Base::DrawPartLevelsDouble(u8 nFirstRow)
for (u8 i = 0; i < 9; ++i)
{
if (m_PartLevels[i] > 8)
const u8 partLevel = static_cast<u8>(m_PartLevels[i] * 16);
if (partLevel > 8)
{
line1Buf[i * 2] = BarChars[m_PartLevels[i] - 8];
line1Buf[i * 2] = BarChars[partLevel - 8];
line2Buf[i * 2] = BarChars[8];
}
else
{
line1Buf[i * 2] = BarChars[0];
line2Buf[i * 2] = BarChars[m_PartLevels[i]];
line2Buf[i * 2] = BarChars[partLevel];
}
line1Buf[i * 2 + 1] = line2Buf[i * 2 + 1] = ' ';
+14 -11
View File
@@ -24,6 +24,7 @@
#include "lcd/mt32lcd.h"
#include "mt32synth.h"
#include "utility.h"
CMT32LCD::CMT32LCD()
: m_State(TState::DisplayingPartStates),
@@ -112,10 +113,17 @@ void CMT32LCD::UpdatePartLevels(const CMT32SynthBase& Synth)
u32 partStates = Synth.GetPartStates();
for (u8 i = 0; i < 9; ++i)
{
// If part active
if ((partStates >> i) & 1)
m_PartLevels[i] = floor(VelocityScale * Synth.GetVelocityForPart(i)) + 0.5f;
else if (m_PartLevels[i] > 0)
--m_PartLevels[i];
{
// MIDI velocity range [0-127] to normalized range [0-1]
float partLevel = Synth.GetVelocityForPart(i) / 127.0f;
if (partLevel >= m_PartLevels[i])
m_PartLevels[i] = partLevel;
}
else
m_PartLevels[i] = Utility::Max(m_PartLevels[i] - BarFalloff, 0.0f);
}
}
@@ -123,19 +131,14 @@ void CMT32LCD::UpdatePeakLevels()
{
for (u8 i = 0; i < 9; ++i)
{
if (m_PartLevels[i] > m_PeakLevels[i])
if (m_PartLevels[i] >= m_PeakLevels[i])
{
m_PeakLevels[i] = m_PartLevels[i];
m_PeakTimes[i] = 100;
}
else if (m_PartLevels[i] > 0)
--m_PartLevels[i];
if (m_PeakTimes[i] == 0 && m_PeakLevels[i] > 0)
{
--m_PeakLevels[i];
m_PeakTimes[i] = 3;
}
if (m_PeakTimes[i] == 0 && m_PeakLevels[i] > 0.0f)
m_PeakLevels[i] = Utility::Max(m_PeakLevels[i] - PeakFalloff, 0.0f);
else
--m_PeakTimes[i];
}
+88 -53
View File
@@ -86,6 +86,12 @@ namespace
constexpr auto FontSingle = Font<Utility::ArraySize(Font6x8), decltype(SingleColumn)>(Font6x8, SingleColumn);
constexpr auto FontDouble = Font<Utility::ArraySize(Font6x8), decltype(DoubleColumn)>(Font6x8, DoubleColumn);
// Drawing constants
constexpr u8 Width = 128;
constexpr u8 BarWidth = 12;
constexpr u8 BarSpacing = 2;
constexpr u8 BarOffset = 2;
CSSD1306::CSSD1306(CI2CMaster *pI2CMaster, u8 nAddress, u8 nHeight, TLCDRotation Rotation)
: CMT32LCD(),
m_pI2CMaster(pI2CMaster),
@@ -105,48 +111,51 @@ bool CSSD1306::Initialize()
if (!(m_nHeight == 32 || m_nHeight == 64))
return false;
const u8 segRemap = m_Rotation == TLCDRotation::Inverted ? 0xA0 : 0xA1;
const u8 comScanDir = m_Rotation == TLCDRotation::Inverted ? 0xC0 : 0xC8;
const u8 pageAddrRange = m_nHeight == 32 ? 0x03 : 0x07;
const u8 segRemap = m_Rotation == TLCDRotation::Inverted ? 0xA0 : 0xA1;
const u8 comScanDir = m_Rotation == TLCDRotation::Inverted ? 0xC0 : 0xC8;
const u8 multiplexRatio = static_cast<u8>(m_nHeight - 1);
const u8 comPins = m_nHeight == 32 ? 0x02 : 0x12;
const u8 initSequence[] =
{
0xAE, // Screen off
0x81, // Set contrast
0x7F, // 00-FF, default to half
0xAE, // Screen off
0x81, // Set contrast
0x7F, // 00-FF, default to half
0xA6, // Normal display
0xA6, // Normal display
0x20, // Set memory addressing mode
0x0, // 00 = horizontal
0x21, // Set column start and end address
0x20, // Set memory addressing mode
0x0, // 00 = horizontal
0x21, // Set column start and end address
0x00,
0x7F,
0x22, // Set page address range
0x22, // Set page address range
0x00,
0x03,
pageAddrRange,
segRemap, // Set segment remap
0xA8, // Set multiplex ratio
0x1F, // Screen height - 1 (31)
segRemap, // Set segment remap
0xA8, // Set multiplex ratio
multiplexRatio, // Screen height - 1
comScanDir, // Set COM output scan direction
0xD3, // Set display offset
0x00, // None
0xDA, // Set com pins hardware configuration
0x02, // Alternate COM config and disable COM left/right
comScanDir, // Set COM output scan direction
0xD3, // Set display offset
0x00, // None
0xDA, // Set com pins hardware configuration
comPins, // Alternate COM config and disable COM left/right
0xD5, // Set display oscillator
0x80, // Default value
0xD9, // Set precharge period
0x22, // Default value
0xDB, // Set VCOMH deselected level
0x20, // Default value
0xD5, // Set display oscillator
0x80, // Default value
0xD9, // Set precharge period
0x22, // Default value
0xDB, // Set VCOMH deselected level
0x20, // Default value
0x8D, // Set charge pump
0x14, // VCC generated by internal DC/DC circuit
0x8D, // Set charge pump
0x14, // VCC generated by internal DC/DC circuit
0xA4, // Resume to RAM content display
0xAF // Set display on
0xA4, // Resume to RAM content display
0xAF // Set display on
};
u8 buffer[] = {0x80, 0};
@@ -187,6 +196,7 @@ void CSSD1306::ClearPixel(u8 nX, u8 nY)
void CSSD1306::DrawChar(char chChar, u8 nCursorX, u8 nCursorY, bool bInverted, bool bDoubleWidth)
{
size_t rowOffset = nCursorY * Width * 2;
size_t columnOffset = nCursorX * (bDoubleWidth ? 12 : 6) + 5;
// FIXME: Won't be needed when the full font is implemented in font6x8.h
@@ -208,47 +218,71 @@ void CSSD1306::DrawChar(char chChar, u8 nCursorX, u8 nCursorY, bool bInverted, b
size_t offset = rowOffset + columnOffset + (bDoubleWidth ? i * 2 : i);
m_Framebuffer[offset] = fontColumn & 0xFF;
m_Framebuffer[offset + 128] = (fontColumn >> 8) & 0xFF;
m_Framebuffer[offset + Width] = (fontColumn >> 8) & 0xFF;
if (bDoubleWidth)
{
m_Framebuffer[offset + 1] = m_Framebuffer[offset];
m_Framebuffer[offset + 128 + 1] = m_Framebuffer[offset + 128];
m_Framebuffer[offset + Width + 1] = m_Framebuffer[offset + Width];
}
}
}
void CSSD1306::DrawPartLevels(u8 nRow, bool bDrawPeaks)
void CSSD1306::DrawPartLevels(u8 nFirstRow, bool bDrawPeaks)
{
size_t rowOffset = nRow * 128;
const size_t firstPageOffset = nFirstRow * Width;
const u8 totalPages = m_nHeight / 8 - 2;
const u8 barHeight = m_nHeight - 8 * 2;
// For each part
for (u8 i = 0; i < 9; ++i)
{
// Bar graphs
u8 topVal, bottomVal;
if (m_PartLevels[i] > 8)
{
topVal = 0xFF << (8 - (m_PartLevels[i] - 8));
bottomVal = 0xFF;
}
else
{
topVal = 0x00;
bottomVal = 0xFF << (8 - (m_PartLevels[i]));
}
u8 pageValues[totalPages];
const u8 partLevelPixels = m_PartLevels[i] * barHeight;
const u8 peakLevelPixels = m_PeakLevels[i] * barHeight;
const u8 fullPages = partLevelPixels / 8;
const u8 remainder = partLevelPixels % 8;
for (u8 j = 0; j < fullPages; ++j)
pageValues[j] = 0xFF;
for (u8 j = fullPages; j < totalPages; ++j)
pageValues[j] = 0;
if (remainder)
pageValues[fullPages] = 0xFF << (8 - remainder);
// Peak meters
if (bDrawPeaks)
if (bDrawPeaks && peakLevelPixels)
{
if (m_PeakLevels[i] > 8)
topVal |= 1 << (8 - (m_PeakLevels[i] - 8));
const u8 peakPage = peakLevelPixels / 8;
const u8 remainder = peakLevelPixels % 8;
if (remainder)
pageValues[peakPage] |= 1 << 8 - remainder;
else
bottomVal |= 1 << (8 - (m_PeakLevels[i]));
pageValues[peakPage - 1] |= 1;
}
for (u8 j = 0; j < 12; ++j)
// For each bar column
for (u8 j = 0; j < BarWidth; ++j)
{
m_Framebuffer[rowOffset + i * 14 + j + 3] = topVal;
m_Framebuffer[rowOffset + i * 14 + j + 128 + 3] = bottomVal;
// For each bar row
for (u8 k = 0; k < totalPages; ++k)
{
size_t offset = firstPageOffset;
// Start BarOffset pixels from the left
offset += BarOffset;
// Start from bottom-most page
offset += (totalPages - 1) * Width - k * Width;
// i'th bar + j'th bar column
offset += i * (BarWidth + BarSpacing) + j;
// +1 to skip 0x40 byte
m_Framebuffer[offset + 1] = pageValues[k];
}
}
}
}
@@ -285,6 +319,7 @@ void CSSD1306::Update(const CMT32SynthBase& Synth)
UpdatePeakLevels();
DrawPartLevels(0);
Print(m_TextBuffer, 0, 1, true);
u8 statusRow = m_nHeight == 32 ? 1 : 3;
Print(m_TextBuffer, 0, statusRow, true);
WriteFramebuffer();
}
+2 -2
View File
@@ -195,8 +195,8 @@ u8 CMT32SynthBase::GetVelocityForPart(u8 nPart) const
u8 velocities[MT32Emu::DEFAULT_MAX_PARTIALS];
u32 playingNotes = m_pSynth->getPlayingNotes(nPart, keys, velocities);
u8 maxVelocity = velocities[0];
for (u32 i = 1; i < playingNotes; ++i)
u8 maxVelocity = 0;
for (u32 i = 0; i < playingNotes; ++i)
if (velocities[i] > maxVelocity)
maxVelocity = velocities[i];