/****************************************************************************** SparkFun_Alphanumeric_Display.cpp SparkFun Alphanumeric Display Library Source File Priyanka Makin @ SparkFun Electronics Original Creation Date: February 25, 2020 https://github.com/sparkfun/SparkFun_Alphanumeric_Display_Arduino_Library Updated May 2, 2020 by Gaston Williams to add defineChar function Pickup a board here: https://sparkle.sparkfun.com/sparkle/storefront_products/16391 This file implements all functions of the HT16K33 class. Functions here range from printing to one or more Alphanumeric Displays, changing the display settings, and writing/ reading the RAM of the HT16K33. The Holtek HT16K33 seems to be susceptible to address changes intra-sketch. The ADR pins are muxed with the ROW and COM drivers so as semgents are turned on/off that affect the ADR1/ADR0 pins the address has been seen to change. The best way around this is to do a isConnected check before updateRAM() is sent to the driver IC. Development environment specifics: IDE: Arduino 1.8.9 Hardware Platform: Arduino Uno Alphanumeric Display Breakout Version: 1.0.0 This code is beerware; if you see me (or any other SparkFun employee) at the local, and you've found our code helpful, please buy us a round! Distributed as-is; no warranty is given. ******************************************************************************/ #include /*--------------------------- Character Map ----------------------------------*/ #define SFE_ALPHANUM_UNKNOWN_CHAR 95 //This is the lookup table of segments for various characters //For AVR architecture, use PROGMEM #if defined(ARDUINO_ARCH_AVR) || defined(ARDUINO_ARCH_MEGAAVR) #include static const uint16_t PROGMEM alphanumeric_segs[96]{ #else static const uint16_t alphanumeric_segs[96]{ #endif // nmlkjihgfedcba 0b00000000000000, // ' ' (space) 0b00001000001000, // '!' 0b00001000000010, // '"' 0b01001101001110, // '#' 0b01001101101101, // '$' 0b10010000100100, // '%' 0b00110011011001, // '&' 0b00001000000000, // ''' 0b00000000111001, // '(' 0b00000000001111, // ')' 0b11111010000000, // '*' 0b01001101000000, // '+' 0b10000000000000, // ',' 0b00000101000000, // '-' 0b00000000000000, // '.' 0b10010000000000, // '/' 0b00000000111111, // '0' 0b00010000000110, // '1' 0b00000101011011, // '2' 0b00000101001111, // '3' 0b00000101100110, // '4' 0b00000101101101, // '5' 0b00000101111101, // '6' 0b01010000000001, // '7' 0b00000101111111, // '8' 0b00000101100111, // '9' 0b00000000000000, // ':' 0b10001000000000, // ';' 0b00110000000000, // '<' 0b00000101001000, // '=' 0b01000010000000, // '>' 0b01000100000011, // '?' 0b00001100111011, // '@' 0b00000101110111, // 'A' 0b01001100001111, // 'B' 0b00000000111001, // 'C' 0b01001000001111, // 'D' 0b00000101111001, // 'E' 0b00000101110001, // 'F' 0b00000100111101, // 'G' 0b00000101110110, // 'H' 0b01001000001001, // 'I' 0b00000000011110, // 'J' 0b00110001110000, // 'K' 0b00000000111000, // 'L' 0b00010010110110, // 'M' 0b00100010110110, // 'N' 0b00000000111111, // 'O' 0b00000101110011, // 'P' 0b00100000111111, // 'Q' 0b00100101110011, // 'R' 0b00000110001101, // 'S' 0b01001000000001, // 'T' 0b00000000111110, // 'U' 0b10010000110000, // 'V' 0b10100000110110, // 'W' 0b10110010000000, // 'X' 0b01010010000000, // 'Y' 0b10010000001001, // 'Z' 0b00000000111001, // '[' 0b00100010000000, // '\' 0b00000000001111, // ']' 0b10100000000000, // '^' 0b00000000001000, // '_' 0b00000010000000, // '`' 0b00000101011111, // 'a' 0b00100001111000, // 'b' 0b00000101011000, // 'c' 0b10000100001110, // 'd' 0b00000001111001, // 'e' 0b00000001110001, // 'f' 0b00000110001111, // 'g' 0b00000101110100, // 'h' 0b01000000000000, // 'i' 0b00000000001110, // 'j' 0b01111000000000, // 'k' 0b01001000000000, // 'l' 0b01000101010100, // 'm' 0b00100001010000, // 'n' 0b00000101011100, // 'o' 0b00010001110001, // 'p' 0b00100101100011, // 'q' 0b00000001010000, // 'r' 0b00000110001101, // 's' 0b00000001111000, // 't' 0b00000000011100, // 'u' 0b10000000010000, // 'v' 0b10100000010100, // 'w' 0b10110010000000, // 'x' 0b00001100001110, // 'y' 0b10010000001001, // 'z' 0b10000011001001, // '{' 0b01001000000000, // '|' 0b00110100001001, // '}' 0b00000101010010, // '~' 0b11111111111111, // Unknown character (DEL or RUBOUT) }; /*--------------------------- Device Status----------------------------------*/ bool HT16K33::begin(uint8_t addressDisplayOne, uint8_t addressDisplayTwo, uint8_t addressDisplayThree, uint8_t addressDisplayFour, TwoWire &wirePort) { _deviceAddressDisplayOne = addressDisplayOne; // grab the address of the alphanumeric _deviceAddressDisplayTwo = addressDisplayTwo; _deviceAddressDisplayThree = addressDisplayThree; _deviceAddressDisplayFour = addressDisplayFour; if (_deviceAddressDisplayFour != DEFAULT_NOTHING_ATTACHED) numberOfDisplays = 4; else if (_deviceAddressDisplayThree != DEFAULT_NOTHING_ATTACHED) numberOfDisplays = 3; else if (_deviceAddressDisplayTwo != DEFAULT_NOTHING_ATTACHED) numberOfDisplays = 2; else numberOfDisplays = 1; //TODO: malloc more displayRAM _i2cPort = &wirePort; // Remember the user's setting for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (isConnected(i) == false) { return false; } // if (checkDeviceID(i) == false) // { // Serial.println(i); // Serial.println("Hello, I've failed checkDeviceID()"); // return false; // } delay(100); } if (initialize() == false) { return false; } if (clear() == false) // Clear all displays { return false; } displayContent[4 * 4] = '\0'; // Terminate the array because we are doing direct prints return true; } // Check that the display is responding on the I2C bus // The Holtek IC sometimes fails to respond. This attempts multiple times before giving up. bool HT16K33::isConnected(uint8_t displayNumber) { uint8_t triesBeforeGiveup = 5; for (uint8_t x = 0; x < triesBeforeGiveup; x++) { _i2cPort->beginTransmission(lookUpDisplayAddress(displayNumber)); if (_i2cPort->endTransmission() == 0) { return true; } delay(100); } return false; } // Run through initialization procedure for each display connected on the bus bool HT16K33::initialize() { // Turn on system clock of all displays if (enableSystemClock() == false) { return false; } // Set brightness of all displays to full brightness if (setBrightness(15) == false) { return false; } // Turn blinking off for all displays if (setBlinkRate(ALPHA_BLINK_RATE_NOBLINK) == false) { return false; } // Turn on all displays if (displayOn() == false) { return false; } return true; } // Turn on the system oscillator for all displays on the I2C bus bool HT16K33::enableSystemClock() { bool status = true; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (enableSystemClockSingle(i) == false) status = false; } return status; } // Turn off the system oscillator for all displays on the bus bool HT16K33::disableSystemClock() { bool status = true; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (disableSystemClockSingle(i) == false) status = false; } return status; } // Turn on the system oscillator for normal operation mode bool HT16K33::enableSystemClockSingle(uint8_t displayNumber) { uint8_t dataToWrite = ALPHA_CMD_SYSTEM_SETUP | 1; // Enable system clock bool status = writeRAM(lookUpDisplayAddress(displayNumber), dataToWrite); delay(1); // Allow display to start return (status); } // Turn off the system oscillator for standby mode bool HT16K33::disableSystemClockSingle(uint8_t displayNumber) { uint8_t dataToWrite = ALPHA_CMD_SYSTEM_SETUP | 0; // Standby mode return (writeRAM(lookUpDisplayAddress(displayNumber), dataToWrite)); } // This function connects the display number to its coressponding address uint8_t HT16K33::lookUpDisplayAddress(uint8_t displayNumber) { switch (displayNumber) { case 1: return _deviceAddressDisplayOne; break; case 2: return _deviceAddressDisplayTwo; break; case 3: return _deviceAddressDisplayThree; break; case 4: return _deviceAddressDisplayFour; break; } return 0; // We shouldn't get here } /*-------------------------- Display configuration functions ---------------------------*/ // Turn off all segments of all displays connected to bus bool HT16K33::clear() { // Clear the displayRAM array for (uint8_t i = 0; i < 16 * numberOfDisplays; i++) displayRAM[i] = 0; digitPosition = 0; return (updateDisplay()); } // This function sets the brightness of all displays on the bus. // Duty cycle valid between 0 (1/16 brightness) and 15 (full brightness) bool HT16K33::setBrightness(uint8_t duty) { bool status = true; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (setBrightnessSingle(i, duty) == false) status = false; } return status; } // Set the brightness of a single display // Duty cycle valid between 0 (1/16 brightness) and 15 (full brightness) bool HT16K33::setBrightnessSingle(uint8_t displayNumber, uint8_t duty) { if (duty > 15) // Error check duty = 15; uint8_t dataToWrite = ALPHA_CMD_DIMMING_SETUP | duty; return (writeRAM(lookUpDisplayAddress(displayNumber), dataToWrite)); } // Set the blink rate of all displays on the bus // Parameter "rate" in Hz // Valid options for "rate" are defined by datasheet: 2.0, 1.0, or 0.5 Hz // Any other input to this function will result in steady alphanumeric display bool HT16K33::setBlinkRate(float rate) { bool status = true; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (setBlinkRateSingle(i, rate) == false) status = false; } return status; } // Set the blink rate of a single display on the bus // Parameter "rate" is in Hz // Valid options for "rate" are defined by datasheet: 2.0, 1.0, or 0.5 Hz // Any other input to this function will result in steady alphanumeric display bool HT16K33::setBlinkRateSingle(uint8_t displayNumber, float rate) { if (rate == 2.0) { blinkRate = ALPHA_BLINK_RATE_2HZ; } else if (rate == 1.0) { blinkRate = ALPHA_BLINK_RATE_1HZ; } else if (rate == 0.5) { blinkRate = ALPHA_BLINK_RATE_0_5HZ; } //default to no blink else { blinkRate = ALPHA_BLINK_RATE_NOBLINK; } uint8_t dataToWrite = ALPHA_CMD_DISPLAY_SETUP | (blinkRate << 1) | displayOnOff; return (writeRAM(lookUpDisplayAddress(displayNumber), dataToWrite)); } // Turn a single alphanumeric display on bool HT16K33::displayOnSingle(uint8_t displayNumber) { return setDisplayOnOff(displayNumber, true); } // Turn a single alphanumeric display off bool HT16K33::displayOffSingle(uint8_t displayNumber) { return setDisplayOnOff(displayNumber, false); } // Set or clear the display on/off bit of a given display number bool HT16K33::setDisplayOnOff(uint8_t displayNumber, bool turnOnDisplay) { if (turnOnDisplay) { displayOnOff = ALPHA_DISPLAY_ON; } else { displayOnOff = ALPHA_DISPLAY_OFF; } uint8_t dataToWrite = ALPHA_CMD_DISPLAY_SETUP | (blinkRate << 1) | displayOnOff; return (writeRAM(lookUpDisplayAddress(displayNumber), dataToWrite)); } // Turn on all displays on the I2C bus bool HT16K33::displayOn() { bool status = true; displayOnOff = ALPHA_DISPLAY_ON; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (displayOnSingle(i) == false) status = false; } return status; } // Turn off all displays on the I2C bus bool HT16K33::displayOff() { bool status = true; displayOnOff = ALPHA_DISPLAY_OFF; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (displayOffSingle(i) == false) status = false; } return status; } // Turn the decimal point on for a single display bool HT16K33::decimalOnSingle(uint8_t displayNumber, bool updateNow) { return setDecimalOnOff(displayNumber, true, updateNow); } // Turn the decimal point off for a single display bool HT16K33::decimalOffSingle(uint8_t displayNumber, bool updateNow) { return setDecimalOnOff(displayNumber, false, updateNow); } // Set or clear the decimal on/off bit bool HT16K33::setDecimalOnOff(uint8_t displayNumber, bool turnOnDecimal, bool updateNow) { uint8_t adr = 0x03; uint8_t dat; if (turnOnDecimal == true) { decimalOnOff = ALPHA_DECIMAL_ON; dat = 0x01; } else { decimalOnOff = ALPHA_DECIMAL_OFF; dat = 0x00; } displayRAM[adr + (displayNumber - 1) * 16] = displayRAM[adr + (displayNumber - 1) * 16] | dat; if(updateNow) { return updateDisplay(); } else { return true; } } // Turn the decimal on for all displays on bus bool HT16K33::decimalOn() { bool status = true; decimalOnOff = ALPHA_DECIMAL_ON; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (decimalOnSingle(i) == false) status = false; } return status; } // Turn the decimal point off for all displays on bus bool HT16K33::decimalOff() { bool status = true; decimalOnOff = ALPHA_DECIMAL_OFF; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (decimalOffSingle(i) == false) status = false; } return status; } // Turn the colon on for a single display bool HT16K33::colonOnSingle(uint8_t displayNumber, bool updateNow) { return setColonOnOff(displayNumber, true, updateNow); } // Turn the colon off for a single display bool HT16K33::colonOffSingle(uint8_t displayNumber, bool updateNow) { return setColonOnOff(displayNumber, false, updateNow); } // Set or clear the colon on/off bit bool HT16K33::setColonOnOff(uint8_t displayNumber, bool turnOnColon, bool updateNow) { uint8_t adr = 0x01; uint8_t dat; if (turnOnColon == true) { colonOnOff = ALPHA_COLON_ON; dat = 0x01; } else { colonOnOff = ALPHA_COLON_OFF; dat = 0x00; } displayRAM[adr + (displayNumber - 1) * 16] = displayRAM[adr + (displayNumber - 1) * 16] | dat; if(updateNow) { return updateDisplay(); } else { return true; } } // Turn the colon on for all displays on the bus bool HT16K33::colonOn() { bool status = true; colonOnOff = ALPHA_COLON_ON; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (colonOnSingle(i) == false) status = false; } return status; } // Turn the colon off for all displays on the bus bool HT16K33::colonOff() { bool status = true; colonOnOff = ALPHA_COLON_OFF; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (colonOffSingle(i) == false) status = false; } return status; } /*---------------------------- Light up functions ---------------------------------*/ // Given a segment and a digit, set the matching bit within the RAM of the Holtek RAM set void HT16K33::illuminateSegment(uint8_t segment, uint8_t digit) { uint8_t com; uint8_t row; com = segment - 'A'; // Convert the segment letter back to a number if (com > 6) com -= 7; if (segment == 'I') com = 0; if (segment == 'H') com = 1; row = digit % 4; // Convert digit (1 to 16) back to a relative position on a given digit on display if (segment > 'G') row += 4; uint8_t offset = digit / 4 * 16; uint8_t adr = com * 2 + offset; // Determine the address if (row > 7) adr++; // Determine the data bit if (row > 7) row -= 8; uint8_t dat = 1 << row; displayRAM[adr] = displayRAM[adr] | dat; } // Given a binary set of segments and a digit, store this data into the RAM array void HT16K33::illuminateChar(uint16_t segmentsToTurnOn, uint8_t digit) { for (uint8_t i = 0; i < 14; i++) // There are 14 segments on this display { if ((segmentsToTurnOn >> i) & 0b1) illuminateSegment('A' + i, digit); // Convert the segment number to a letter } } // Print a character, for a given digit, on display void HT16K33::printChar(uint8_t displayChar, uint8_t digit) { // moved alphanumeric_segs array to PROGMEM uint16_t characterPosition = 65532; // space if (displayChar == ' ') characterPosition = 0; // Printable Symbols -- Between first character ! and last character ~ else if (displayChar >= '!' && displayChar <= '~') { characterPosition = displayChar - '!' + 1; } uint8_t dispNum = digitPosition / 4; // Take care of special characters by turning correct segment on if (characterPosition == 14) // '.' decimalOnSingle(dispNum+1, false); if (characterPosition == 26) // ':' colonOnSingle(dispNum+1, false); if (characterPosition == 65532) // unknown character characterPosition = SFE_ALPHANUM_UNKNOWN_CHAR; uint16_t segmentsToTurnOn = getSegmentsToTurnOn(characterPosition); illuminateChar(segmentsToTurnOn, digit); } // Update the list to define a new segments display for a particular character bool HT16K33::defineChar(uint8_t displayChar, uint16_t segmentsToTurnOn) { bool result = false; // Check to see if character is within range of displayable ASCII characters if (displayChar >= '!' && displayChar <= '~') { // Get the index of character in table and update its 14-bit segment value uint16_t characterPosition = displayChar - '!' + 1; // Create a new character definition struct CharDef * pNewCharDef = (CharDef *)calloc(1, sizeof(CharDef)); // Set the position to the table index pNewCharDef -> position = characterPosition; // Mask the segment value to 14 bits only pNewCharDef -> segments = segmentsToTurnOn & 0x3FFF; // New definition always goes at the end of the list pNewCharDef -> next = NULL; // If list is empty set it to the new item if (pCharDefList == NULL) { pCharDefList = pNewCharDef; } else { // Otherwise go to the end of the list and add it there struct CharDef * pTail = pCharDefList; while(pTail->next != NULL) { pTail = pTail->next; } pTail->next = pNewCharDef; } // We added the definition so we're all good result = true; } return result; } // Get the character map from the definition list or default table uint16_t HT16K33::getSegmentsToTurnOn(uint8_t charPos) { uint16_t segments = 0; // pointer to a defined character in list struct CharDef * pDefChar = pCharDefList; // Search the chacters list for a match while(pDefChar && (pDefChar->position != charPos)) { pDefChar = pDefChar -> next; } // If we found a match return that value if (pDefChar != NULL) { segments = pDefChar -> segments; } // Otherwise get the value from the table else { segments = pgm_read_word_near(alphanumeric_segs + charPos); } return segments; } /* * Write a byte to the display. * Required for overloading the Print function. */ size_t HT16K33::write(uint8_t b) { // If user wants to print '.' or ':', don't increment the digitPosition! if (b == '.' || b == ':') printChar(b, 0); else { printChar(b, digitPosition++); digitPosition %= (numberOfDisplays * 4); // Convert displays to number of digits } return (updateDisplay()); // Send RAM buffer over I2C bus } /* * Write a character buffer to the display. * Required for overloading the Print function. */ size_t HT16K33::write(const uint8_t *buffer, size_t size) { size_t n = size; uint8_t buff; // Clear the displayRAM array for (uint8_t i = 0; i < 16 * numberOfDisplays; i++) displayRAM[i] = 0; digitPosition = 0; while (size--) { buff = *buffer++; // For special characters like '.' or ':', do not increment the digitPosition if (buff == '.') printChar('.', 0); else if (buff == ':') printChar(':', 0); else { printChar(buff, digitPosition); displayContent[digitPosition] = buff; // Record to internal array digitPosition++; digitPosition %= (numberOfDisplays * 4); } } updateDisplay(); // Send RAM buffer over I2C bus return n; } /* * Write a string to the display. * Required for overloading the Print function. */ size_t HT16K33::write(const char *str) { if (str == NULL) return 0; return write((const uint8_t *)str, strlen(str)); } // Push the contents of displayRAM out to the various displays in 16 byte chunks bool HT16K33::updateDisplay() { bool status = true; for (uint8_t i = 1; i <= numberOfDisplays; i++) { if (writeRAM(lookUpDisplayAddress(i), 0, (uint8_t *)(displayRAM + ((i-1) * 16)), 16) == false) { //Serial.print("updateDisplay fail at display 0x"); //Serial.println(lookUpDisplayAddress(i), HEX); status = false; } } return status; } // Shift the display content to the right one digit bool HT16K33::shiftRight(uint8_t shiftAmt) { for (uint8_t x = (4 * numberOfDisplays) - shiftAmt; x >= shiftAmt; x--) { displayContent[x] = displayContent[x - shiftAmt]; } // Clear the leading characters for (uint8_t x = 0; x < shiftAmt; x++) { if (x + shiftAmt > (4 * numberOfDisplays)) break; // Error check displayContent[0 + x] = ' '; } return (print(displayContent)); } // Shift the display content to the left one digit bool HT16K33::shiftLeft(uint8_t shiftAmt) { for (int x = 0; x < 4 * numberOfDisplays; x++) { if (x + shiftAmt > (4 * numberOfDisplays)) break; // Error check displayContent[x] = displayContent[x + shiftAmt]; } // Clear the trailing characters for (int x = 0; x < shiftAmt; x++) { if (4 * numberOfDisplays - 1 - x < 0) break; //Error check displayContent[4 * numberOfDisplays - 1 - x] = ' '; } return (print(displayContent)); } /*----------------------- Internal I2C Abstraction -----------------------------*/ bool HT16K33::readRAM(uint8_t address, uint8_t reg, uint8_t *buff, uint8_t buffSize) { uint8_t displayNum = 1; if (address == _deviceAddressDisplayTwo) displayNum = 2; else if (address == _deviceAddressDisplayThree) displayNum = 3; else if (address == _deviceAddressDisplayFour) displayNum = 4; isConnected(displayNum); // Wait until display is ready _i2cPort->beginTransmission(address); _i2cPort->write(reg); _i2cPort->endTransmission(false); if (_i2cPort->requestFrom(address, buffSize) > 0) { for (uint8_t i = 0; i < buffSize; i++) buff[i] = _i2cPort->read(); return true; } return false; } // Write the contents of the RAM array out to the Holtek IC bool HT16K33::writeRAM(uint8_t address, uint8_t reg, uint8_t *buff, uint8_t buffSize) { uint8_t displayNum = 1; if (address == _deviceAddressDisplayTwo) displayNum = 2; else if (address == _deviceAddressDisplayThree) displayNum = 3; else if (address == _deviceAddressDisplayFour) displayNum = 4; isConnected(displayNum); //Wait until display is ready _i2cPort->beginTransmission(address); _i2cPort->write(reg); for (uint8_t i = 0; i < buffSize; i++) _i2cPort->write(buff[i]); if (_i2cPort->endTransmission() == 0) return true; return false; } // Write a single byte to the display. This is often a command byte. // The address of the data to write is contained in the first four bits of dataToWrite bool HT16K33::writeRAM(uint8_t address, uint8_t dataToWrite) { uint8_t temp = 0; return (writeRAM(address, dataToWrite, (uint8_t *)&temp, 0)); }