/*! * \file drv/liquid_crystal.h * \brief * A liquid crystal display driver * * \copyright Copyright (C) 2021 Christos Choutouridis * *
License
* The MIT License (MIT) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. *
*/ #ifndef TBX_DRV_LIQUID_CRYSTAL_H_ #define TBX_DRV_LIQUID_CRYSTAL_H_ #include #include #include #include #include namespace tbx { /*! * \class liquid_crystal * \brief * A CRTP driver class for liquid crystal display with parallel interface, * based on Hitachi HD44780 (Samsung KS0066U, or compatible). * * The driver inherits from Print * The Implementation requirements are: * - void bus_impl(data); To set the 4bit/8bit bus * - void rs_pin_impl(state); To set/clear RS pin * - void en_pin_impl(state); To set/clear EN pin * - void power_pin_impl(state); To set/clear PWR pin * - void bl_pin_impl(state); To set/clear BackLight pin * - void delay_usec_impl(usec); To provide delay in usec * * \tparam Impl_t The derived class * \tparam Lines The lines of the LCD * \tparam Columns The coluns of the LCD * \tparam BusSize The LCD bus size (4 or 8 bits) */ template class liquid_crystal : public Print, char>{ friend Print; _CRTP_IMPL(Impl_t); static_assert((BusSize == 4) || (BusSize == 8), "BusSize must be either 4 or 8."); public: constexpr static size_t lines = Lines; constexpr static size_t columns = Columns; /*! * Public enumerator to be used as argument to the init() function. * Selects the font size and thus the number of active lines */ enum class FontSize :uint8_t { dots_5x8, dots_5x10 }; private: // Commands constexpr static uint8_t Cmd_cls = 0x01; constexpr static uint8_t Cmd_RetHome = 0x02; constexpr static uint8_t Cmd_EntryMode = 0x04; constexpr static uint8_t Cmd_DispCtrl = 0x08; constexpr static uint8_t Cmd_Shift = 0x10; constexpr static uint8_t Cmd_FunSet = 0x20; constexpr static uint8_t Cmd_SetGRamAddr= 0x40; constexpr static uint8_t Cmd_SetDRamAddr= 0x80; /* * Entry Mode Set -----> 0 0 0 0 0 1 I/D S * ---------------------------------------------------- * I/D = 1 Increment Curs * 0 Decrement * S = 1 Display shift * 0 Not */ constexpr static uint8_t Entry_Right = 0x00; constexpr static uint8_t Entry_Left = 0x02; constexpr static uint8_t Entry_ShiftInc = 0x01; constexpr static uint8_t Entry_ShiftDec = 0x00; /* * DispOnOffControll --> 0 0 0 0 1 D C B * ------------------------------------------------- * D = Display On * C = Cursor On * B = Blinking On */ constexpr static uint8_t Display_On = 0x04; constexpr static uint8_t Display_Off = 0x00; constexpr static uint8_t Cursor_On = 0x02; constexpr static uint8_t Cursor_Off = 0x00; constexpr static uint8_t Blink_On = 0x01; constexpr static uint8_t Blink_Off = 0x00; /* * Cursor/Display Shift --> 0 0 0 1 S/C R/L x x * --------------------------------------------------- * S/C = 1 Display Shift * 0 Cursor Shift * R/L = 1 Shift Right * 0 Shift left */ constexpr static uint8_t DisMove_Display= 0x08; constexpr static uint8_t DisMove_Cursor = 0x00; constexpr static uint8_t DisMove_Right = 0x04; constexpr static uint8_t DisMove_Left = 0x00; /* * FunctionSet ------> 0 0 1 DL N F x x * --------------------------------------------------- * DL = 1 8bit * 0 4bit * N = 1 2 lines * 0 1 line * F = 1 5x10 dots * 0 5x8 dots */ constexpr static uint8_t FunSet_8bitMode= 0x10; constexpr static uint8_t FunSet_4bitMode= 0x00; constexpr static uint8_t FunSet_2Line = 0x08; constexpr static uint8_t FunSet_1Line = 0x00; constexpr static uint8_t FunSet_5x10dots= 0x04; constexpr static uint8_t FunSet_5x8dots = 0x00; /*! * \brief * Helper class to keep track of the display cursor. As we dont read the cursor * position on the display, in order to implement backspace operation we need to * keep track the cursor position manually. */ struct Cursor { uint8_t inc_x() noexcept { if (++x_ > Columns) x_ =1; return x_; } uint8_t dec_x() noexcept { if (--x_ < 1) x_ =Columns; return x_; } uint8_t inc_y () noexcept { if (++y_ > max_lines) y_ =1; return y_; } uint8_t dec_y () noexcept { if (--y_ < 1) y_ =max_lines; return y_; } uint8_t operator++() noexcept { return inc_x(); } uint8_t operator--() noexcept { return dec_x(); } void set(uint8_t x, uint8_t y) noexcept { x_ = x; y_ = y; } uint8_t get_x() noexcept { return x_; } uint8_t get_y() noexcept { return y_; } void set_lines (uint8_t l) noexcept { max_lines = l; } private: uint8_t x_, y_; uint8_t max_lines; }; private: //! \name Implementation requirements //! @{ void BUS (uint8_t data) { impl().bus_impl(data); } void RS_Pin (bool state) { impl().rs_pin_impl(state); } void EN_Pin (bool state) { impl().en_pin_impl(state); } void PWR_Pin (bool state) { impl().power_pin_impl(state); } void BL_Pin (bool state) { impl().bl_pin_impl(state); } void delay_usec(size_t usec) { impl().delay_usec_impl(usec); } //! @} //! \name Print interface requirements //! @{ size_t write_impl (const char* str, size_t size) { size_t ret =0; while (*str && ret < size) { putchar (*str++); ++ret; } return ret; } size_t write_impl (const char ch) { return (putchar(ch) == ch) ? 1:0; } //! @} protected: //! \name Object lifetime //! @{ liquid_crystal() noexcept = default; //!< Construct from derived only //~liquid_crystal() = default; liquid_crystal(const liquid_crystal&) = delete; //!< No copies liquid_crystal& operator= (const liquid_crystal&) = delete; //!< No copies //! @} private: //! Send enable pulse to display void pulse_enable () { EN_Pin(0); delay_usec (2); // time to settle BUS pin voltages EN_Pin(1); delay_usec (2); // >450 [nsec] EN_Pin(0); delay_usec (50); // > 37 [usec] } //! Writes 4/8bit data to display and pulse the EN pin //! \param data The data to write void write_bits (uint8_t data) { if constexpr (BusSize == 4) BUS (data & 0x0F); else BUS (data); pulse_enable (); } /*! * \brief * Sends commands or character to display by controlling RS pin * \param data The data to send * \param mode Character/command mode (RS pin state) */ void send (uint8_t data, uint8_t mode) { RS_Pin (mode); if constexpr (BusSize == 4) { write_bits (data >> 4); write_bits (data & 0x0F); } else { write_bits (data); } } //! Send a command to display void command (uint8_t c) { send(c, 0); } //! Send a character to display void character (uint8_t c) { send(c, 1); } public: //! \name Public API //! @{ /*! * \brief * Initialize the display. After construction the object is valid but reflects the init state * of display configuration. In order for the display to be functional it needs initialization. * So the user has to call this function. This function requires a settled environment, so usually * its called after main(). * \param mode 4bit or 8bit mode * \param fontSize 5x8 or 5x10 dots font size. */ void init (FontSize fontSize =FontSize::dots_5x8) { disp_mode_ = disp_mode_init_; // Set values to LCD's startup configuration disp_control_ = disp_control_init_; disp_function_=disp_function_init_; // Read user configuration if constexpr (BusSize == 4) // note: keep this runtime, so the disp_function reflects lcd's configuration state disp_function_ &= ~FunSet_8bitMode; else disp_function_ |= FunSet_8bitMode; if (fontSize == FontSize::dots_5x10) { disp_function_ |= FunSet_5x10dots; disp_function_ &= ~FunSet_2Line; cursor_.set_lines(Lines>>1); } else { disp_function_ &= ~FunSet_5x10dots; disp_function_ |= FunSet_2Line; cursor_.set_lines(Lines); } // start with All-zeros BUS (0); EN_Pin(0); RS_Pin(0); BL_Pin(0); delay_usec(100000); if constexpr (BusSize == 4) { // 4bit BUS write_bits (0x03); // 1t try delay_usec(20000); write_bits (0x03); // 2nd try delay_usec(5000); write_bits (0x03); // 3rd try delay_usec(5000); write_bits (0x02); // We set 4 bit interface delay_usec (10000); } else { // 8bit BUS write_bits (Cmd_FunSet | disp_function_); // 1st try delay_usec(20000); write_bits (Cmd_FunSet | disp_function_); // 2nd try delay_usec(5000); write_bits (Cmd_FunSet | disp_function_); // 3rd try delay_usec(5000); } command (Cmd_FunSet | disp_function_); // Finally we set #lines and font size delay_usec(5000); command (Cmd_DispCtrl | Display_Off); // Display off delay_usec(5000); command (Cmd_cls); // Clear screen delay_usec(5000); command (Cmd_EntryMode | disp_mode_); // Entry mode set delay_usec(5000); command (Cmd_RetHome); // Return home delay_usec(10000); display(true); // Finally display On, done. cursor_.set(1, 1); } //! Utility function to enable/disable power to display. This has an effect IFF there is a //! power pin on the board void power (bool en) { PWR_Pin(en); } //! Utility function to enable/disable backlight. This has an effect IFF there is a //! backlight pin on the board void backlight (bool en) { BL_Pin(en); } //! Utility function to send on/off command to display. void display (bool en) { if (en) disp_control_ |= Display_On; else disp_control_ &= ~Display_On; command (Cmd_DispCtrl | disp_control_); delay_usec(100); } //! Utility function to enable/disable display cursor. void cursor (bool en) { if (en) disp_control_ |= Cursor_On; else disp_control_ &= ~Cursor_On; command (Cmd_DispCtrl | disp_control_); delay_usec(100); } //! Utility function to enable/disable cursor blinking. void blink (bool en) { if (en) disp_control_ |= Blink_On; else disp_control_ &= ~Blink_On; command (Cmd_DispCtrl | disp_control_); delay_usec(100); } //! Utility function to enable/disable autoscroll. void autoscroll (bool en) { if (en) disp_mode_ |= Entry_ShiftInc; else disp_mode_ &= ~Entry_ShiftInc; command (Cmd_EntryMode | disp_mode_); delay_usec(100); } /*! * \brief * Tool to set display cursor * \param x The column position (starting with 1) * \param y The line position (starting with 1 at the top of the display) */ void set_cursor (uint8_t x, uint8_t y) { uint8_t offset; switch (y) { default: case 1: offset = 0x0; break; case 2: offset = 0x40; break; case 3: offset = 0x0 + Columns; break; case 4: offset = 0x40 + Columns; break; } command( Cmd_SetDRamAddr | offset | (x-1)); cursor_.set(x, y); } //! Utility function to set left to right entry mode. //! \note //! This is the default void set_left_to_right () { disp_mode_ |= Entry_Left; command (Cmd_EntryMode | disp_mode_); delay_usec(100); } //! Utility function to set right to left entry mode. void set_right_to_left () { disp_mode_ &= ~Entry_Left; command (Cmd_EntryMode | disp_mode_); delay_usec(100); } //! Command to scroll display left one position void scroll_left () { command (Cmd_Shift | DisMove_Display | DisMove_Left); } //! Command to scroll display right one position void scroll_right () { command (Cmd_Shift | DisMove_Display | DisMove_Right); } //! Clears the display and return home void clear() { command (Cmd_cls); cursor_.set(1, 1); delay_usec(2000); } //! return home without clearing the display void home() { command (Cmd_RetHome); cursor_.set(1, 1); delay_usec(2000); } /*! * \brief * Create custom character and store it to LCD. * \param location The location to store the character [0..7] allowed * \param charmap The character map buffer with the font */ void create_char (uint8_t location, uint8_t charmap[]) { location &= 0x7; // we only have 8 locations 0-7 command(Cmd_SetGRamAddr | (location << 3)); for (size_t i=0; i<8; ++i) { character(charmap[i]); } } /*! * \brief * Send an ascii character to liquid crystal display. * \param ch the character to send * \return the character send. * * \note * This is the driver's "putchar()" functionality to glue. * Tailor this function to redirect stdout to display. */ int putchar (int ch) { // LCD Character dispatcher switch (ch) { case 0: // don't send null termination to device break; case '\n': cursor_.inc_y(); set_cursor (1, cursor_.get_y()); break; case '\r': set_cursor (1, cursor_.get_y()); break; case '\v': home (); break; case '\f': clear(); break; case '\b': --cursor_; set_cursor (cursor_.get_x(), cursor_.get_y()); character (' '); --cursor_; set_cursor (cursor_.get_x(), cursor_.get_y()); break; default: character (ch); ++cursor_; break; } //ANSI C (C99) compatible mode return ch; } //! @} private: //! \name Data members //! @{ //! The init entry mode of the display after power up static constexpr uint8_t disp_mode_init_ = Entry_Left | Entry_ShiftDec; //! The init control mode of the display after power up static constexpr uint8_t disp_control_init_ = Display_Off | Cursor_Off | Blink_Off; //! The init function set of the display after power up static constexpr uint8_t disp_function_init_= FunSet_8bitMode | FunSet_1Line | FunSet_5x8dots; Cursor cursor_{}; uint8_t disp_mode_ {disp_mode_init_}; uint8_t disp_control_ {disp_control_init_}; uint8_t disp_function_ {disp_function_init_}; /*! * \note * When the display powers up, it is configured as follows: * 1. Display clear * 2. Function set: 0x10 * DL = 1; 8-bit interface data * N = 0; 1-line display * F = 0; 5x8 dot character font * 3. Display on/off control: 0x00 * D = 0; Display off * C = 0; Cursor off * B = 0; Blinking off * 4. Entry mode set: 0x02 * I/D = 1; Increment by 1 * S = 0; No shift */ //! @} }; } #endif /* TBX_DRV_LIQUID_CRYSTAL_H_ */