// Driver for 128x634 graphical LCD with ST7920 controller // D Crocker, Escher Technologies Ltd. // adapted for STM32-Arduino by Matthias Diro #include "lcd7920_STM.h" #include #include // LCD basic instructions. These all take 72us to execute except LcdDisplayClear, which takes 1.6ms const uint8_t LcdDisplayClear = 0x01; const uint8_t LcdHome = 0x02; const uint8_t LcdEntryModeSet = 0x06; // move cursor right and indcement address when writing data const uint8_t LcdDisplayOff = 0x08; const uint8_t LcdDisplayOn = 0x0C; // add 0x02 for cursor on and/or 0x01 for cursor blink on const uint8_t LcdFunctionSetBasicAlpha = 0x20; const uint8_t LcdFunctionSetBasicGraphic = 0x22; const uint8_t LcdFunctionSetExtendedAlpha = 0x24; const uint8_t LcdFunctionSetExtendedGraphic = 0x26; const uint8_t LcdSetDdramAddress = 0x80; // add the address we want to set // LCD extended instructions const uint8_t LcdSetGdramAddress = 0x80; //const unsigned int LcdCommandDelayMicros = 72 - 24; // 72us required, less 24us time to send the command @ 1MHz const unsigned int LcdCommandDelayMicros = 2; const unsigned int LcdDataDelayMicros = 2;// 10; // Delay between sending data bytes const unsigned int LcdDisplayClearDelayMillis = 2; // 1.6ms should be enough const unsigned int numRows = 64; const unsigned int numCols = 128; Lcd7920::Lcd7920(uint8_t cPin, uint8_t dPin, bool spi) : clockPin(cPin), dataPin(dPin), useSpi(spi), currentFont(0), textInverted(false) { } size_t Lcd7920::write(uint8_t ch) { if (gfxMode) { if (currentFont == 0) { return 0; } const uint8_t startChar = pgm_read_byte_near(&(currentFont->startCharacter)); const uint8_t endChar = pgm_read_byte_near(&(currentFont->endCharacter)); if (ch < startChar || ch > endChar) { return 0; } const uint8_t fontWidth = pgm_read_byte_near(&(currentFont->width)); const uint8_t fontHeight = pgm_read_byte_near(&(currentFont->height)); const uint8_t bytesPerColumn = (fontHeight + 7)/8; const uint8_t bytesPerChar = (bytesPerColumn * fontWidth) + 1; PROGMEM const uint8_t *fontPtr = (PROGMEM const uint8_t*)pgm_read_word_near(&(currentFont->ptr)) + (bytesPerChar * (ch - startChar)); uint16_t cmask = (1 << fontHeight) - 1; uint8_t nCols = pgm_read_byte_near(fontPtr++); // Update dirty rectangle coordinates, except for endCol which we do at the end { if (startRow > row) { startRow = row; } if (startCol > column) { startCol = column; } uint8_t nextRow = row + fontHeight; if (nextRow > numRows) { nextRow = numRows; } if (endRow < nextRow) { endRow = nextRow; } } // Decide whether to add a space column first (auto-kerning) // We don't add a space column before a space character. // We add a space column after a space character if we would have added one between the preceding and following characters. if (column != numCols) { uint16_t thisCharColData = pgm_read_word_near(fontPtr) & cmask; // atmega328p is little-endian if (thisCharColData == 0) // for characters with deliberate space row at the start, e.g. decimal point { thisCharColData = pgm_read_word_near(fontPtr + 2) & cmask; } bool wantSpace = ((thisCharColData | (thisCharColData << 1)) & (lastCharColData | (lastCharColData << 1))) != 0; if (wantSpace) { // Add space after character uint8_t mask = 0x80 >> (column & 7); uint8_t *p = image + ((row * (numCols/8)) + (column/8)); for (uint8_t i = 0; i < fontHeight && p < (image + sizeof(image)); ++i) { if (textInverted) { *p |= mask; } else { *p &= ~mask; } p += (numCols/8); } ++column; } } while (nCols != 0 && column < numCols) { uint16_t colData = pgm_read_word_near(fontPtr); fontPtr += bytesPerColumn; if (colData != 0) { lastCharColData = colData & cmask; } uint8_t mask1 = 0x80 >> (column & 7); uint8_t mask2 = ~mask1; uint8_t *p = image + ((row * (numCols/8)) + (column/8)); const uint16_t setPixelVal = (textInverted) ? 0 : 1; for (uint8_t i = 0; i < fontHeight && p < (image + sizeof(image)); ++i) { if ((colData & 1) == setPixelVal) { *p |= mask1; // set pixel } else { *p &= mask2; // clear pixel } colData >>= 1; p += (numCols/8); } --nCols; ++column; } if (column > endCol) { endCol = column; } } else { // Alphanumeric mode if (ch == '\r') { setCursor(0, column); } else if (ch == '\n') { setCursor(0, row + 1); } else { if (column == 16) { setCursor(row + 1, 0); } sendLcdData(ch); ++column; commandDelay(); // so that we are ready to write another character immediately } } return 1; } // Select normal or inverted text (only works in graphics mode) void Lcd7920::textInvert(bool b) { if (b != textInverted) { textInverted = b; lastCharColData = 0xFFFF; // always need space between inverted and non-inverted text } } // NB - if using SPI then the SS pin must be set to be an output before calling this! void Lcd7920::begin(bool gmode) { // Set up the SPI interface for talking to the LCD. We have to set MOSI, SCLK and SS to outputs, then enable SPI. digitalWrite(clockPin, LOW); digitalWrite(dataPin, LOW); pinMode(clockPin, OUTPUT); pinMode(dataPin, OUTPUT); if (useSpi) { delay(1); //dummy // SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0); // enable SPI, master mode, clock low when idle, data sampled on rising edge, clock = f/16 (= 1MHz), send MSB first //SPSR = (1 << SPI2X); // double the speed to 2MHz (optional) } gfxMode = false; sendLcdCommand(LcdFunctionSetBasicAlpha); delay(1); sendLcdCommand(LcdFunctionSetBasicAlpha); commandDelay(); sendLcdCommand(LcdEntryModeSet); commandDelay(); extendedMode = false; clear(); // clear alpha ram if (gmode) { gfxMode = true; clear(); // clear gfx ram } setCursor(0, 0); sendLcdCommand(LcdDisplayOn); commandDelay(); currentFont = 0; textInverted = false; } void Lcd7920::setFont(const PROGMEM LcdFont *newFont) { currentFont = newFont; } void Lcd7920::clear() { if (gfxMode) { memset(image, 0, sizeof(image)); // flag whole image as dirty and update startRow = 0; endRow = numRows; startCol = 0; endCol = numCols; flush(); } else { ensureBasicMode(); sendLcdCommand(LcdDisplayClear); delay(LcdDisplayClearDelayMillis); } setCursor(0, 0); textInverted = false; } // Draw a line using the Bresenham Algorithm (thanks Wikipedia) void Lcd7920::line(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, PixelMode mode) { int dx = (x1 >= x0) ? x1 - x0 : x0 - x1; int dy = (y1 >= y0) ? y1 - y0 : y0 - y1; int sx = (x0 < x1) ? 1 : -1; int sy = (y0 < y1) ? 1 : -1; int err = dx - dy; for (;;) { setPixel(x0, y0, mode); if (x0 == x1 && y0 == y1) break; int e2 = err + err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } void Lcd7920::fastHline(uint8_t x0, uint8_t y0, uint8_t x1, PixelMode mode) { for (byte xx=0;xx< x1;xx++) { setPixel(x0+xx, y0, mode); } } void Lcd7920::fastVline(uint8_t x0, uint8_t y0, uint8_t y1, PixelMode mode) { for (byte yy=0;yy< y1;yy++) { setPixel(x0, y0+yy, mode); } } // draw simple box - x0,y0=start, x1,y1-length in pixel void Lcd7920::box(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, PixelMode mode) { //line( x0, y0, x0+x1, y0, mode); fastHline(x0,y0,x1, mode); fastHline(x0,y0+y1,x1, mode); // line( x0, y0+y1, x0+x1, y0+y1, mode); //line( x0, y0+1, x0, y0+y1-1, mode); fastVline(x0,y0+1,y1-1, mode); fastVline(x0+x1-1,y0+1,y1-1, mode); //line( x0+x1, y0+1, x0+x1, y0+y1-1, mode); //line( x0, y0, x0+x1, y0+y1, mode); } //draw full box void Lcd7920::fillbox(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, PixelMode mode) { for (byte x=0;x= 0) { y--; ddF_y += 2; f += ddF_y; } x++; ddF_x += 2; f += ddF_x; setPixel(x0 + x, y0 + y, mode); setPixel(x0 - x, y0 + y, mode); setPixel(x0 + x, y0 - y, mode); setPixel(x0 - x, y0 - y, mode); setPixel(x0 + y, y0 + x, mode); setPixel(x0 - y, y0 + x, mode); setPixel(x0 + y, y0 - x, mode); setPixel(x0 - y, y0 - x, mode); } } // Draw a bitmap void Lcd7920::bitmap(uint8_t x0, uint8_t y0, uint8_t width, uint8_t height, PROGMEM const uint8_t data[]) { for (uint8_t r = 0; r < height && r + y0 < numRows; ++r) { uint8_t *p = image + (((r + y0) * (numCols/8)) + (x0/8)); uint16_t bitMapOffset = r * (width/8); for (uint8_t c = 0; c < (width/8) && c + (x0/8) < numCols/8; ++c) { *p++ = pgm_read_byte_near(bitMapOffset++); } } if (x0 < startCol) startCol = x0; if (x0 + width > endCol) endCol = x0 + width; if (y0 < startRow) startRow = y0; if (y0 + height > endRow) endRow = y0 + height; } // Flush the dirty part of the image to the lcd void Lcd7920::flush() { if (gfxMode && endCol > startCol && endRow > startRow) { uint8_t startColNum = startCol/16; uint8_t endColNum = (endCol + 15)/16; for (uint8_t r = startRow; r < endRow; ++r) { setGraphicsAddress(r, startColNum); uint8_t *ptr = image + ((16 * r) + (2 * startColNum)); for (uint8_t i = startColNum; i < endColNum; ++i) { sendLcdData(*ptr++); //commandDelay(); // don't seem to need a delay here sendLcdData(*ptr++); //commandDelay(); // don't seem to need as long a delay as this delayMicroseconds(LcdDataDelayMicros); } } startRow = numRows; startCol = numCols; endCol = endRow = 0; } } // Set the cursor position. We can only set alternate columns. The row addressing is rather odd. void Lcd7920::setCursor(uint8_t r, uint8_t c) { if (gfxMode) { row = r % numRows; column = c % numCols; lastCharColData = 0u; // flag that we just set the cursor position, so no space before next character } else { row = r % 4; column = c % 16; ensureBasicMode(); sendLcdCommand(LcdSetDdramAddress + ((row & 1) * 0x10) + (column/2) + (row >> 1) * 8); commandDelay(); } } void Lcd7920::setPixel(uint8_t x, uint8_t y, PixelMode mode) { if (y < numRows && x < numCols) { uint8_t *p = image + ((y * (numCols/8)) + (x/8)); uint8_t mask = 0x80u >> (x%8); switch(mode) { case PixelClear: *p &= ~mask; break; case PixelSet: *p |= mask; break; case PixelFlip: *p ^= mask; break; } // Change the dirty rectangle to account for a pixel being dirty (we assume it was changed) if (startRow > y) { startRow = y; } if (endRow <= y) { endRow = y + 1; } if (startCol > x) { startCol = x; } if (endCol <= x) { endCol = x + 1; } } } bool Lcd7920::readPixel(uint8_t x, uint8_t y) { if (y < numRows && x < numCols) { uint8_t *p = image + ((y * (numCols/8)) + (x/8)); return (*p & (0x80u >> (x%8))) != 0; } return false; } void Lcd7920::setGraphicsAddress(unsigned int r, unsigned int c) { if (gfxMode) { ensureExtendedMode(); sendLcdCommand(LcdSetGdramAddress | (r & 31)); //commandDelay(); // don't seem to need this one sendLcdCommand(LcdSetGdramAddress | c | ((r & 32) >> 2)); commandDelay(); // we definitely need this one } } void Lcd7920::commandDelay() { delayMicroseconds(LcdCommandDelayMicros); } // Send a command to the LCD void Lcd7920::sendLcdCommand(uint8_t command) { sendLcd(0xF8, command); } // Send a data byte to the LCD void Lcd7920::sendLcdData(uint8_t data) { sendLcd(0xFA, data); } // Send a command to the lcd. Data1 is sent as-is, data2 is split into 2 bytes, high nibble first. void Lcd7920::sendLcd(uint8_t data1, uint8_t data2) { if (useSpi) { delay(1); // dummy /* SPDR = data1; while ((SPSR & (1 << SPIF)) == 0) { } SPDR = data2 & 0xF0; while ((SPSR & (1 << SPIF)) == 0) { } SPDR = data2 << 4; while ((SPSR & (1 << SPIF)) == 0) { } */ } else { sendLcdSlow(data1); sendLcdSlow(data2 & 0xF0); sendLcdSlow(data2 << 4); } } void Lcd7920::sendLcdSlow(uint8_t data) { /* #if 1 // Fast shiftOut function volatile uint32_t *sclkPort = portOutputRegister(digitalPinToPort(clockPin)); volatile uint32_t *mosiPort = portOutputRegister(digitalPinToPort(dataPin)); uint8_t sclkMask = digitalPinToBitMask(clockPin); uint8_t mosiMask = digitalPinToBitMask(dataPin); uint8_t oldSREG = SREG; cli(); for (uint8_t i = 0; i < 8; ++i) { if (data & 0x80) { *mosiPort |= mosiMask; } else { *mosiPort &= ~mosiMask; } *sclkPort |= sclkMask; *sclkPort &= ~sclkMask; data <<= 1; } SREG = oldSREG; #else */ // really slow version, like Arduino shiftOut function for (uint8_t i = 0; i < 8; ++i) { digitalWrite(dataPin, (data & 0x80) ? HIGH : LOW); // mosifast((data & 0x80) ? HIGH : LOW); // faster digitalWrite(clockPin, HIGH); // do not change to high speed! digitalWrite(clockPin, LOW); // do not change to high speed pin change! data <<= 1; } /* for (uint8_t i = 0; i < 8; ++i) { mosifast((data & 0x80) ? HIGH : LOW); delayMicroseconds(1); sckfast(HIGH); sckfast(LOW); data <<= 1; } */ //#endif } void Lcd7920::ensureBasicMode() { if (extendedMode) { sendLcdCommand(gfxMode ? LcdFunctionSetBasicGraphic : LcdFunctionSetBasicAlpha); commandDelay(); extendedMode = false; } } void Lcd7920::ensureExtendedMode() { if (!extendedMode) { sendLcdCommand(gfxMode ? LcdFunctionSetExtendedGraphic : LcdFunctionSetExtendedAlpha); commandDelay(); extendedMode = true; } }