From e02f7dad6b5a717b307f276a4def54c84bfb1544 Mon Sep 17 00:00:00 2001 From: Christos Choutouridis Date: Thu, 7 Oct 2021 12:01:59 +0300 Subject: [PATCH] DEV: CRTP driver for liquid crytal displays --- include/drv/liquid_crystal.h | 534 +++++++++++++++++++++++++++++++++++ include/tbx.h | 1 + 2 files changed, 535 insertions(+) create mode 100644 include/drv/liquid_crystal.h diff --git a/include/drv/liquid_crystal.h b/include/drv/liquid_crystal.h new file mode 100644 index 0000000..f724b9b --- /dev/null +++ b/include/drv/liquid_crystal.h @@ -0,0 +1,534 @@ +/*! + * \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 + */ + 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: + + /*! + * 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': + set_cursor (1, 1); + 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_ */ diff --git a/include/tbx.h b/include/tbx.h index 51bd35f..ffdc4dc 100644 --- a/include/tbx.h +++ b/include/tbx.h @@ -42,6 +42,7 @@ #include #include +#include #include #endif /* TBX_H_ */