I was right in my previous post where I catalogued some trials in trying to get an LCD 1602 display (HD44780 compatible) to work with Windows IoT Core and a Raspberry PI 2.
In the end, what it needed was solder.
I’d played around with code for hours while pretending that my dodgy connections to the LCD panel were ‘good enough’ but, in the end, my suspicions were right and making nice, shiny, soldered connections addressed the problem straight away. Stupid of me to waste time thinking that I could get away with it!
Here’s my handiwork – I was quite pleased given that I reckon it’s over 30 years since I soldered anything to anything;
I wired up the LCD panel in exactly the same way as this article;
But I didn’t use the code from that article. That wasn’t because it’s bad code or anything like that. It’s because I’d already started writing my own code prior to finding that article and also as part of trying to get things to work which (in the end) turned out to be down to me not soldering the connections.
There are 12 pins that you end up wiring on the LCD panel;
- VSS – ground
- VDD – 5V
- V0 – voltage for the backlight. I wired this in to a 10K potentiometer so I could adjust the backlight
- RS – shift register
- RW – read/write which I wired to ground because I’m always writing
- E – enable bit which you toggle to get the device to read instructions/data
- Data registers D4,5,6,7
- A – 5V
- K – ground
Because I’ve only wired up 4 of the 8 data lines (D4…D7 missing out D0…3) I use the 4-bit addressing mode that’s well explained in the datasheet. Connecting 4 more wires felt like overkill to me.
In my setup, I’ve wired RS to GPIO pin 5, E to pin 6 and then Data pins D4,5,6,7 to GPIO pins 23,24,25,26 respectively as you’ll spot in the code listing in a moment.
Writing a Library to Control the Panel
I wrote a little library to control the panel and made sure that it referenced both the UWP and also the IoT Extensions SDK.
The first class I needed was something to represent how the RS/RW/E/Data4,5,6,7 pins have been wried up and so I wrote;
namespace LcdControl { using System.Linq; public class LcdPinMapping { public int RSPin { get; set; } public int EnablePin { get; set; } public int Data4Pin { get; set; } public int Data5Pin { get; set; } public int Data6Pin { get; set; } public int Data7Pin { get; set; } public int[] AllPins { get { return ( (new int[] { this.RSPin, this.EnablePin }).Concat(this.DataPinsHighToLow).ToArray()); } } public int[] DataPinsHighToLow { get { return (new int[] { this.Data7Pin, this.Data6Pin, this.Data5Pin, this.Data4Pin }); } } } }
It’s pretty simple but it did the job. With that in place, I wrote a little class that would take one of these mappings and then use the extension SDK to open up the GpioPin objects for those pins and keep them around. I also made that class responsible for writing bytes/nibbles of data to the LCD which involves writing the high 4 bits first followed by the low 4 bits. It also involves signaling the E pin from High to Low to tell the panel there’s something waiting for it. That class looks like;
namespace LcdControl { using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Threading.Tasks; using Windows.Devices.Gpio; class LcdPinManager { public LcdPinManager(LcdPinMapping mapping) { this.mapping = mapping; this.pinMap = new Dictionary<int, GpioPin>(); } public void OpenPins() { foreach (var pin in this.mapping.AllPins) { var gpioPin = GpioController.GetDefault().OpenPin(pin); this.pinMap[pin] = gpioPin; gpioPin.SetDriveMode(GpioPinDriveMode.Output); } } public async Task WriteByteAsync(byte bits, bool command) { Debug.WriteLine($"Writing byte {bits}"); await this.WriteNibbleAsync((byte)(bits >> 4), command); await this.WriteNibbleAsync((byte)(bits & 0x0F), command); } public async Task WriteNibbleAsync(byte content, bool isCommand) { #if DEBUG StringBuilder debugString = new StringBuilder(); debugString.AppendFormat($"Writing Nibble {content}:"); #endif this.Enable(true); this.pinMap[this.mapping.RSPin].Write( isCommand ? GpioPinValue.Low : GpioPinValue.High); debugString.AppendFormat("RS({0}):", isCommand ? 0 : 1); for (int i = 0; i < this.mapping.DataPinsHighToLow.Length; i++) { bool high = ((content & (1 << (3 - i))) != 0); GpioPin gpioPin = this.pinMap[this.mapping.DataPinsHighToLow[i]]; gpioPin.Write(high ? GpioPinValue.High : GpioPinValue.Low); #if DEBUG debugString.AppendFormat("{0}", high ? 1 : 0); #endif } Debug.WriteLine(debugString.ToString()); this.Enable(false); await Task.Delay(enableDelay); } void Enable(bool high) { this.pinMap[this.mapping.EnablePin].Write(high ? GpioPinValue.High : GpioPinValue.Low); } static readonly TimeSpan enableDelay = TimeSpan.FromMilliseconds(20); LcdPinMapping mapping; Dictionary<int, GpioPin> pinMap; } }
I should say that I think the 20ms delay here is overkill but it worked for me and I can spare 20ms here and there so I left it as is. If you read the datasheet then you’ll see that you can adjust these times down a lot for certain instructions.
On top of this class, I wrapped up a number of command bytes into enums as per;
namespace LcdControl { enum Commands: byte { ClearDisplay = 0x1, CursorHome = 0x2, EntryModeSet = 0x4, DisplayControl = 0x8, CursorDisplayShift = 0x10, FunctionSet = 0x20, SetDDRAMAddress = 0x80 } public enum RightLeftOptions : byte { Left = 0x0, Right = 0x4 } public enum MoveDirections : byte { Decrement = 0x0, Increment = 0x2 } // I haven't implemented this one because I think it means changing my DDRAM // addressing. enum EntryModeShiftOptions : byte { NoShift = 0x0, Shift = 0x1 } public enum DisplayOptions : byte { Off = 0, On = 0x4 } public enum CursorOptions : byte { Hidden = 0x0, Shown = 0x2 } public enum BlinkOptions : byte { NoBlink = 0x0, Blink = 0x1 } public enum DataLengthOptions : byte { FourBit = 0x0, EightBit = 0x10 } public enum NumberLinesOptions : byte { OneLine = 0x0, TwoLines = 0x8 } enum FontOptions : byte { FiveByEight = 0x0, FiveByTen = 0x4 } }
And then I wrote an LcdManager class to use these pieces to try and write data to the panel;
namespace LcdControl { using System; using System.Text; using System.Threading.Tasks; /// <summary> /// Simple class to support a 16x02 LCD panel over GPIO. Note, this class gives /// the promise of supporting 4-bit and 8-bit comms but, in fact, I only wrote /// the 4-bit part because I couldn't be fussed connecting 8 wires from my /// PI to the device. Sorry! /// /// </summary> public class LcdManager { LcdPinManager pinManager; public LcdManager(LcdPinMapping mapping, DataLengthOptions dataLength) { if (dataLength != DataLengthOptions.FourBit) { throw new ArgumentException("Sorry, I didn't write the 8 bit mode part of this"); } this.pinManager = new LcdPinManager(mapping); this.pinManager.OpenPins(); } /// <summary> /// Initialises the device. If you're wondering where these slightly /// wacky bit sequences come from, I took them from /// http://web.alfredstate.edu/weimandn/lcd/lcd_initialization/lcd_initialization_index.html /// </summary> /// <returns></returns> public async Task InitialiseAsync() { // Formal initialisation starts here. for (int i = 0; i < 3; i++) { // We write bit pattern 0011 three times to the device. // this is as per the referenced doc. The lower 4 bits // are irrelevant here. await this.pinManager.WriteNibbleAsync(0x3, true); } // We then send down 0010 which should switch the LCD // panel into 4-bit mode. await this.pinManager.WriteNibbleAsync(0x2, true); // Should be 0x28. await this.FunctionSetAsync( DataLengthOptions.FourBit, NumberLinesOptions.TwoLines, FontOptions.FiveByEight); // Should be 0x8. NB: this is part of initialisation, it's not // how I actually want the display (turned off). await this.DisplayControlAsync( DisplayOptions.Off, CursorOptions.Hidden, BlinkOptions.NoBlink); // 0x1. await this.ClearDisplayAsync(); // 0x6. Entry Mode Set. await this.EntryModeSetAsync(MoveDirections.Increment); // 0xF. Now, set the display on, the cursor visible and // the cursor blinking. await this.DisplayControlAsync(DisplayOptions.On, CursorOptions.Shown, BlinkOptions.Blink); } public Task ClearDisplayAsync() { return (this.pinManager.WriteByteAsync((byte)Commands.ClearDisplay, true)); } public Task ReturnHomeAsync() { return (this.pinManager.WriteByteAsync((byte)Commands.CursorHome, true)); } public Task EntryModeSetAsync(MoveDirections direction) { byte command = (byte)Commands.EntryModeSet; command |= (byte)direction; command |= (byte)EntryModeShiftOptions.NoShift; return (this.pinManager.WriteByteAsync(command, true)); } public Task DisplayControlAsync(DisplayOptions onOff, CursorOptions cursorOnOff, BlinkOptions blinkOnOff) { byte command = (byte)Commands.DisplayControl; command |= (byte)onOff; command |= (byte)cursorOnOff; command |= (byte)blinkOnOff; return (this.pinManager.WriteByteAsync(command, true)); } public async Task MoveToPosition(uint x, uint y) { if ((x >= SCREEN_WIDTH) || (y >= SCREEN_HEIGHT)) { throw new ArgumentException("Invalid x,y position passed"); } await this.SetDDRAMAddress( (byte)((y == 0 ? LINE_ONE : LINE_TWO) + (byte)x)); } public async Task WriteCharAtCoordAsync(uint x, uint y, char output) { await this.MoveToPosition(x, y); await this.WriteCharAsync(output); } public async Task WriteCharAsync(char output) { var bytes = UTF8Encoding.UTF8.GetBytes(new char[] { output }); if ((bytes.Length > 1) || (bytes[0] < ASCII_SPACE) || (bytes[0] > ASCII_TILDE)) { throw new ArgumentException("Character doesn't look like an ASCII code"); } await this.pinManager.WriteByteAsync(bytes[0], false); } public async Task WriteStringAsync(string output) { foreach (var character in output) { await this.WriteCharAsync(character); } } public async Task WriteStringLeftToRightAtCoordAsync(uint x, uint y, string output) { if ((x + output.Length) > SCREEN_WIDTH) { throw new ArgumentException("String is too long for the screen"); } uint xCoord = x; foreach (var character in output) { await this.WriteCharAtCoordAsync(xCoord, y, character); xCoord++; } } /// <summary> /// Not public as I don't think you can control this once you've gone /// through the initialisation point. /// </summary> /// <param name="dataLength"></param> /// <param name="numberLines"></param> /// <param name="font"></param> /// <returns></returns> Task FunctionSetAsync( DataLengthOptions dataLength, NumberLinesOptions numberLines, FontOptions font) { byte command = (byte)Commands.FunctionSet; command |= (byte)dataLength; command |= (byte)numberLines; command |= (byte)font; return (this.pinManager.WriteByteAsync(command, true)); } Task SetDDRAMAddress(byte address) { byte command = (byte)Commands.SetDDRAMAddress; command |= address; return (this.pinManager.WriteByteAsync(command, true)); } static readonly int SCREEN_WIDTH = 16; static readonly int SCREEN_HEIGHT = 2; static readonly byte ASCII_SPACE = 0x20; static readonly byte ASCII_TILDE = 0x7E; static readonly byte LINE_ONE = 0x00; static readonly byte LINE_TWO = 0x40; } }
With that in place, I was ready to try things out a little.
Building a ‘Test UI’
I wrote a little test UI to try out the main functions of that LcdManager and so I knocked up a little XAML;
<Page x:Class="LabProject.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:LabProject" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Page.Resources> <Style TargetType="Button"> <Setter Property="Margin" Value="5"/> </Style> <Style TargetType="TextBlock"> <Setter Property="Margin" Value="5"/> </Style> <Style TargetType="StackPanel"> <Setter Property="Margin" Value="5"/> </Style> <Style TargetType="TextBox"> <Setter Property="Margin" Value="5"/> </Style> <Style TargetType="ToggleSwitch"> <Setter Property="Margin" Value="5"/> </Style> </Page.Resources> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <ToggleSwitch IsOn="{Binding DisplayOn,Mode=TwoWay}">Display On</ToggleSwitch> <ToggleSwitch IsOn="{Binding CursorOn,Mode=TwoWay}">Cursor On</ToggleSwitch> <ToggleSwitch IsOn="{Binding CursorBlink,Mode=TwoWay}">Cursor Blink</ToggleSwitch> <ToggleSwitch IsOn="{Binding MoveDirection,Mode=TwoWay}" OnContent="Increment" OffContent="Decrement"/> <Button Content="Clear" Click="OnClear"/> <Button Content="Return Home" Click="OnReturnHome"/> <StackPanel Orientation="Horizontal"> <TextBlock Text="Move to (x,y)"/> <TextBox Text="{Binding XCoord,Mode=TwoWay}"/> <TextBox Text="{Binding YCoord,Mode=TwoWay}"/> <Button Click="MoveToPosition" Content="Move"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text="Write Character at Current"/> <TextBox MaxLength="1" Text="{Binding CharToWrite, Mode=TwoWay}"/> <Button Click="WriteCharAtCurrent" Content="Write"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text="Write String at Current"/> <TextBox MaxLength="16" Width="240" Text="{Binding StringToWrite, Mode=TwoWay}"/> <Button Click="WriteStringAtCurrent" Content="Write"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text="Write Character at (x,y)"/> <TextBox Text="{Binding XCoord,Mode=TwoWay}"/> <TextBox Text="{Binding YCoord,Mode=TwoWay}"/> <TextBox MaxLength="1" Text="{Binding CharToWrite, Mode=TwoWay}"/> <Button Click="WriteCharAtCoord" Content="Write"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text="Write String Left To Right at (x,y)"/> <TextBox Text="{Binding XStringCoord,Mode=TwoWay}"/> <TextBox Text="{Binding YStringCoord,Mode=TwoWay}"/> <TextBox MaxLength="16" Width="240" Text="{Binding StringToWrite, Mode=TwoWay}"/> <Button Click="WriteStringAtCoord" Content="Write"/> </StackPanel> </StackPanel> </Grid> </Page>
With some code behind it;
namespace LabProject { using LcdControl; using System.ComponentModel; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; public sealed partial class MainPage : Page, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public MainPage() { this.InitializeComponent(); this.Loaded += OnLoaded; } public bool MoveDirection { get { return (this.moveDirection == MoveDirections.Increment); } set { this.moveDirection = value ? MoveDirections.Increment : MoveDirections.Decrement; this.displayManager.EntryModeSetAsync(this.moveDirection); this.RaisePropertyChanged("MoveDirection"); } } public uint XCoord { get { return (this.xCharCoord); } set { this.xCharCoord = value; this.RaisePropertyChanged("XCoord"); } } public uint YCoord { get { return (this.yCharCoord); } set { this.yCharCoord = value; this.RaisePropertyChanged("YCoord"); } } public string CharToWrite { get { return (this.charToWrite); } set { this.charToWrite = value; this.RaisePropertyChanged("CharToWrite"); } } public uint XStringCoord { get { return (this.xStringCoord); } set { this.xStringCoord = value; this.RaisePropertyChanged("XStringCoord"); } } public uint YStringCoord { get { return (this.yStringCoord); } set { this.yStringCoord = value; this.RaisePropertyChanged("YStringCoord"); } } public string StringToWrite { get { return (this.stringToWrite); } set { this.stringToWrite = value; this.RaisePropertyChanged("StringToWrite"); } } public bool CursorBlink { get { return (this.blinkOnOff == BlinkOptions.Blink); } set { this.blinkOnOff = value ? BlinkOptions.Blink : BlinkOptions.NoBlink; this.displayManager.DisplayControlAsync(this.displayOnOff, this.cursorOnOff, this.blinkOnOff); this.RaisePropertyChanged("CursorBlink"); } } public bool DisplayOn { get { return (this.displayOnOff == DisplayOptions.On); } set { this.displayOnOff = value ? DisplayOptions.On : DisplayOptions.Off; this.displayManager.DisplayControlAsync(this.displayOnOff, this.cursorOnOff, this.blinkOnOff); this.RaisePropertyChanged("DisplayOn"); } } public bool CursorOn { get { return (this.cursorOnOff == CursorOptions.Shown); } set { this.cursorOnOff = value ? CursorOptions.Shown : CursorOptions.Hidden; this.displayManager.DisplayControlAsync(this.displayOnOff, this.cursorOnOff, this.blinkOnOff); this.RaisePropertyChanged("CursorOn"); } } async void OnLoaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) { LcdPinMapping mapping = new LcdPinMapping() { RSPin = 5, EnablePin = 6, Data4Pin = 23, Data5Pin = 24, Data6Pin = 25, Data7Pin = 26 }; this.displayManager = new LcdManager(mapping, DataLengthOptions.FourBit); await this.displayManager.InitialiseAsync(); this.blinkOnOff = BlinkOptions.Blink; this.displayOnOff = DisplayOptions.On; this.cursorOnOff = CursorOptions.Shown; this.moveDirection = MoveDirections.Increment; this.DataContext = this; } void RaisePropertyChanged(string propertyName) { var handlers = this.PropertyChanged; if (handlers != null) { handlers(this, new PropertyChangedEventArgs(propertyName)); } } void OnClear(object sender, RoutedEventArgs e) { this.displayManager.ClearDisplayAsync(); } void OnReturnHome(object sender, RoutedEventArgs e) { this.displayManager.ReturnHomeAsync(); } void WriteCharAtCoord(object sender, RoutedEventArgs e) { this.displayManager.WriteCharAtCoordAsync(this.xCharCoord, this.yCharCoord, this.charToWrite[0]); } void WriteStringAtCoord(object sender, RoutedEventArgs e) { this.displayManager.WriteStringLeftToRightAtCoordAsync(this.xStringCoord, this.yStringCoord, this.stringToWrite); } void WriteStringAtCurrent(object sender, RoutedEventArgs e) { this.displayManager.WriteStringAsync(this.stringToWrite); } void WriteCharAtCurrent(object sender, RoutedEventArgs e) { this.displayManager.WriteCharAsync(this.charToWrite[0]); } void MoveToPosition(object sender, RoutedEventArgs e) { this.displayManager.MoveToPosition(this.xCharCoord, this.yCharCoord); } DisplayOptions displayOnOff; CursorOptions cursorOnOff; BlinkOptions blinkOnOff; MoveDirections moveDirection; LcdManager displayManager; uint xCharCoord; uint yCharCoord; string charToWrite; uint xStringCoord; uint yStringCoord; string stringToWrite; } }
And that gives me one of the ugliest forms I’ve ever seen! But I could drive it on the PI via mouse and keyboard (I have a monitor plugged into my PI over HDMI);
But, sure enough, that let me test out this code as I’ll try and illustrate on the little (phone captured) video below where I wrapped a viewbox around the UI to make it a little bigger;
and that all seems to work reasonably well. The code for all of that is here;
I then thought that it might be nice to wire this up such that a press of a switch did something on the display and so I wrote a little ‘SwitchManager’ class;
namespace LabProject { using System; using Windows.Devices.Gpio; class SwitchManager { public event EventHandler SwitchPressed; public SwitchManager(uint gpioPin) { this.pin = GpioController.GetDefault().OpenPin((int)gpioPin); this.pin.ValueChanged += OnPinValueChanged; } void OnPinValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args) { var handlers = this.SwitchPressed; var value = sender.Read(); if ((value == GpioPinValue.High) && (handlers != null)) { handlers(this, EventArgs.Empty); } } GpioPin pin; } }
and connected a little switch on my breadboard to GPIO pin 12 and then changed my UI to be blank and wrote this little code behind;
namespace LabProject { using LcdControl; using System.ComponentModel; using System.Threading.Tasks; using Windows.UI.Xaml.Controls; public sealed partial class MainPage : Page, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public MainPage() { this.InitializeComponent(); this.Loaded += OnLoaded; } async void OnLoaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) { LcdPinMapping mapping = new LcdPinMapping() { RSPin = 5, EnablePin = 6, Data4Pin = 23, Data5Pin = 24, Data6Pin = 25, Data7Pin = 26 }; this.displayManager = new LcdManager(mapping, DataLengthOptions.FourBit); await this.displayManager.InitialiseAsync(); this.switchManager = new SwitchManager(12); this.switchManager.SwitchPressed += OnSwitched; this.messageOne = true; await this.ChangeDisplayAsync(); } async Task ChangeDisplayAsync() { await this.displayManager.ClearDisplayAsync(); if (this.messageOne) { await this.displayManager.WriteStringLeftToRightAtCoordAsync(0, 0, "Press the switch"); await this.displayManager.WriteStringLeftToRightAtCoordAsync(0, 1, "2 change display"); } else { await this.displayManager.WriteStringLeftToRightAtCoordAsync(0, 0, "this is the"); await this.displayManager.WriteStringLeftToRightAtCoordAsync(0, 1, "other display"); } this.messageOne = !this.messageOne; } void OnSwitched(object sender, System.EventArgs e) { this.ChangeDisplayAsync(); } bool messageOne; LcdManager displayManager; SwitchManager switchManager; } }
And that gives me a little button that changes the display and could (e.g.) be the start of a simple menu/control system. The short video below shows that working;