{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 18. Using a liquid crystal display\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "While using a computer to control and display results from a device is very powerful, it does result in the device being tethered to a computer in order to function. You may wish to have a stand-alone device. For such applications, portable **liquid crystal displays** (LCDs) are quite useful." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The LDC\n", "\n", "The LCD we have in lab can display 32 characters. Each character is represented in a block of pixels that is eight pixels tall and five pixels wide. The LCD display is based on the [Hitachi HD44780 chip](https://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller), which is very popular for small, low-cost, text-based LCDs. Fortunately, they are so popular that Arduino has built-in libraries for driving them, and we will put those to use. \n", "\n", "Here are photos of the front and back of your LCD.\n", "\n", "
\n", " \n", "![LCD photo](lcd_photo.jpg)\n", " \n", "
\n", "\n", "On the front (top photo) are 16 pins. In looking at the pinout on the [Wikipedia page](https://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller), you can see that many pins need to be connected to digital pins on the Arduino Uno in order to operate the LCD. Conveniently, though, the LCD we have contains a \"backpack\", shown on the bottom of the two photos above. The backpack enables powering the LCD and connecting it to Aruino using I2C, which uses just two pins (SDA and SCL). Also on the backpack is a potentiometer, which you can tune using a small Phillips head screwdriver. The potentiometer adjusts the brightness of the display.\n", "\n", "So, fortunately, we can use I2C to update the display of the LCD and we do not need to use many pins to do so." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Running multiple devices on the I2C bus\n", "\n", "You can use I2C to control multiple peripheral devices. You simply need to supply the address of each. From the last lesson, you currently have a DAC set up for control over I2C. To demonstrate that we can control multiple devices, we will the MCP4725 DAC device connected. We will also connet the LCD. We will work through the rest of this lesson as a follow-along exercise." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Follow-along exercise 13: Using an LCD display\n", "\n", "Set up the circuit below. You will add a potentiometer and the LCD to your setup from the last lesson. You may be able to leave your DAC setup as is, but you might need to move things around to keep wires tidy.\n", "\n", "
\n", " \n", "![DAC with LCD](13-dac_with_lcd_potentiometer_schem.svg)\n", " \n", "
\n", "\n", "Our goal with this circuit is to have the flashing LED with the DAC running just as it did before. Separately we have a potentiometer that will adjust an input voltage to one of the analog input pins. The LCD will display the voltage, both in text and graphically. The reason we have these two essentially independent circuits at once is to demonstrate how to drive multiple devices using I2C." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Characters for the LCD\n", "\n", "The LCD can display a set of characters" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Driving the LCD\n", "\n", "The address of the of the LCD is `0x27`. (If you are trying to find the addresses of I2C devices you have wired up, [this sketch](https://playground.arduino.cc/Main/I2cScanner/) is really useful!) We use the `LiquidCrystal_I2C` library to drive the LCD, so we need to be sure to `#include ` in your sketch. It is also a good idea to `#include ` so that you can set your I2C speed to fast mode using `Wire.setClock(400000)`, which will enable I2C communication at 400k baud.\n", "\n", "With that library included, you can instantiate an instance for communicating with the LCD using\n", "\n", "```arduino\n", "LiquidCrystal_I2C lcd = LiquidCrystal_I2C(LCD_ADDR, nCols, nRows);\n", "```\n", "\n", "Here, `LCD_ADDR` is `0x27`, the address of the LCD, and `nCols` and `nRows` are the number of columns and rows of blocks of pixels, respectively, in the LCD. For the ones we have, there are 16 columns and 2 rows. Once the `lcd` instance is in place, you need to initialize and backlight the LCD by calling\n", "\n", "```arduino\n", "lcd.init();\n", "lcd.backlight();\n", "```\n", "\n", "This should be done in the `setup()` function.\n", "\n", "When using the LCD, you need to set the cursor position before writing a character. This is done using the `lcd.setCursor()` function. The first argument is the column position (starting at zero), and the second argument is the row position (also starting at zero). To write \"Hello, world.\" on the LCD on the first line starting to the left, you can do the following.\n", "\n", "```arduino\n", "lcd.setCursor(0, 0);\n", "lcd.print(\"Hello, world.\");\n", "```\n", "The `lcd.print()` function is useful for printing strings. You can also define your own characters and write them using `lcd.write()`. The format of your own characters is a byte array of `nCols` 5-bit binary numbers. For example, to make an [LA logo](https://www.lamag.com/citythinkblog/meet-guy-made-la-fingers-thing/), you could define the character like this:\n", "\n", "```arduino\n", "const byte LAlogo[8] = {\n", " B10000,\n", " B10000,\n", " B10010,\n", " B10101,\n", " B11111,\n", " B00101,\n", " B00101,\n", " B00101\n", "};\n", "```\n", "\n", "To make this a character that the LCD can display, you need to call\n", "\n", "```arduino\n", "lcd.creatChar(0, LAlogo);\n", "```\n", "\n", "The first argument is an integer that will stand for the character in calls to `lcd.write()`. You can specify up to eight characters, and they need to be numbered zero through seven.\n", "\n", "To write this character a few places over on the second row, you would do:\n", "\n", "```arduino\n", "lcd.setCursor(3, 1);\n", "lcd.write(0);\n", "```\n", "\n", "We will make custom characters to represent a graphical display of voltage." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### The sketch\n", "\n", "Now that we have a basic idea of how to use the LCD, let's look at the sketch, which you can go ahead and upload.\n", "\n", "```arduino\n", "#include \n", "#include \n", "#include \n", "\n", "// This is the I2C Address of the MCP4725, by default (A0 pulled to GND).\n", "// For devices with A0 pulled HIGH, use 0x61\n", "#define MCP4725_ADDR 0x62\n", "\n", "// LCD address\n", "#define LCD_ADDR 0x27\n", "\n", "// Geometry of LCD\n", "const int nRows = 2;\n", "const int nCols = 16;\n", "\n", "// Custom characters for LCD bars\n", "const byte zeroBlock[8] = {B00000, B00000, B00000, B00000, B00000, B00000, B00000, B00000};\n", "const byte twentyBlock[8] = {B10000, B10000, B10000, B10000, B10000, B10000, B10000, B10000};\n", "const byte fortyBlock[8] = {B11000, B11000, B11000, B11000, B11000, B11000, B11000, B11000};\n", "const byte sixtyBlock[8] = {B11100, B11100, B11100, B11100, B11100, B11100, B11100, B11100};\n", "const byte eightyBlock[8] = {B11110, B11110, B11110, B11110, B11110, B11110, B11110, B11110};\n", "const byte fullBlock[8] = {B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111};\n", "\n", "// Frequency of oscillating signal\n", "const int freq = 1;\n", "\n", "// Delays for updating\n", "const unsigned long sampleDelay = 20;\n", "unsigned long lastSampleTime = 0;\n", "const unsigned long reportDelay = 100;\n", "unsigned long lastReportTime = 0;\n", "\n", "// Sensor pin connected to potentiometer\n", "const int sensorPin = A0;\n", "\n", "// Instantiate the convenient classses for DAC and LCD\n", "Adafruit_MCP4725 dac;\n", "LiquidCrystal_I2C lcd = LiquidCrystal_I2C(LCD_ADDR, nCols, nRows);\n", "\n", "void writeGraphicalVoltage(int x, int minX, int maxX) {\n", " /*\n", " * Graphical display of voltage on second row of LCD\n", " */\n", " float frac = ((float) (x - minX)) / (maxX - minX);\n", " int nBars = (int) (nCols * 5 * frac);\n", " int n5BarBlocks = nBars / 5;\n", " int fracBlock = nBars % 5;\n", "\n", " // Write the parts that are full blocks\n", " for (int i = 0; i < n5BarBlocks; i++) {\n", " lcd.setCursor(i, 1);\n", " lcd.write(5);\n", " }\n", "\n", " // Write fractional block\n", " if (n5BarBlocks < nCols) {\n", " lcd.setCursor(n5BarBlocks, 1);\n", " lcd.write(fracBlock);\n", " }\n", "\n", " // Write blank blocks\n", " for (int i = n5BarBlocks + 1; i < nCols; i++) {\n", " lcd.setCursor(i, 1);\n", " lcd.write(0);\n", " }\n", "}\n", "\n", "\n", "void setup() {\n", " // Set I2C to be fast mode\n", " Wire.setClock(400000);\n", " \n", " dac.begin(MCP4725_ADDR);\n", "\n", " // Initialize the LCD\n", " lcd.init();\n", " lcd.backlight();\n", "\n", " // Add the special characters for blocks\n", " lcd.createChar(0, zeroBlock);\n", " lcd.createChar(1, twentyBlock);\n", " lcd.createChar(2, fortyBlock);\n", " lcd.createChar(3, sixtyBlock);\n", " lcd.createChar(4, eightyBlock);\n", " lcd.createChar(5, fullBlock);\n", "\n", " // Label message on LCD\n", " lcd.setCursor(0, 0);\n", " lcd.print(\"Voltage: \");\n", "}\n", "\n", "\n", "void loop() {\n", " unsigned long currTime = millis();\n", "\n", " if (currTime - lastSampleTime > sampleDelay) {\n", " // Sinusoidal signal to DAC\n", " uint16_t x = (uint16_t)(4095 * (1 + sin(2 * PI * freq * millis() / 1000.0)) / 2.0);\n", "\n", " dac.setVoltage(x, false);\n", "\n", " lastSampleTime = currTime;\n", "\n", " // Report voltage to LCE\n", " if (currTime - lastReportTime > reportDelay) {\n", " // Read in and convert voltage\n", " int sensorValue = analogRead(sensorPin);\n", " float voltage = sensorValue / 1023.0 * 5.0;\n", "\n", " // Write the voltage on the first row (text display)\n", " lcd.setCursor(9, 0);\n", " lcd.print(String(voltage, 2) + \" V\");\n", "\n", " // Display the voltage graphically\n", " writeGraphicalVoltage(sensorValue, 0, 1023);\n", " \n", " lastReportTime = currTime;\n", " }\n", " \n", " }\n", "\n", "}\n", "```\n", "\n", "The sketch should be self-explanatory since we have covered how to communicate with the LCD. Importantly the `writeGRaphicalVoltage()` function plots the voltage graphically on the second line of the LCD.\n", "\n", "Note also that we have two devices connected to the same I2C bus and it is trivial to control them independently; we just have to set their addresses.\n", "\n", "You can play with the potentiometer and watch the LCD change!\n", "\n", "
" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.8" } }, "nbformat": 4, "nbformat_minor": 4 }