-
Notifications
You must be signed in to change notification settings - Fork 587
/
Hd44780.cs
439 lines (388 loc) · 18.7 KB
/
Hd44780.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Buffers;
using System.Drawing;
namespace Iot.Device.CharacterLcd
{
/// <summary>
/// Supports LCD character displays compatible with the HD44780 LCD controller/driver.
/// Also supports serial interface adapters such as the MCP23008.
/// </summary>
/// <remarks>
/// The Hitatchi HD44780 was released in 1987 and set the standard for LCD controllers. Hitatchi does not make this chipset anymore, but
/// most character LCD drivers are intended to be fully compatible with this chipset. Some examples: Sunplus SPLC780D, Sitronix ST7066U,
/// Samsung KS0066U, Aiptek AIP31066, and many more.
///
/// Some compatible chips extend the HD44780 with addtional pins and features. They are still fully compatible. The ST7036 is one example.
///
/// This implementation was drawn from numerous datasheets and libraries such as Adafruit_Python_CharLCD.
/// </remarks>
public class Hd44780 : ICharacterLcd, IDisposable
{
/// <summary>
/// Command which can be used to clear the display
/// </summary>
protected const byte ClearDisplayCommand = 0b_0001;
/// <summary>
/// Command which can be used to return (cursor) home
/// </summary>
protected const byte ReturnHomeCommand = 0b_0010;
/// <summary>
/// Command which can be used to set CG RAM address
/// </summary>
protected const byte SetCGRamAddressCommand = 0b_0100_0000;
/// <summary>
/// Command which can be used to set DD RAM address
/// </summary>
protected const byte SetDDRamAddressCommand = 0b_1000_0000;
internal DisplayFunction _displayFunction = DisplayFunction.Command;
internal DisplayControl _displayControl = DisplayControl.Command;
internal DisplayEntryMode _displayMode = DisplayEntryMode.Command;
/// <summary>
/// Offsets of the rows
/// </summary>
protected readonly byte[] _rowOffsets;
/// <summary>
/// LCD interface used by the device
/// </summary>
protected readonly LcdInterface _lcdInterface;
/// <summary>
/// Initializes a new HD44780 LCD controller.
/// </summary>
/// <param name="size">The logical size of the LCD.</param>
/// <param name="lcdInterface">The interface to use with the LCD.</param>
public Hd44780(Size size, LcdInterface lcdInterface)
{
Size = size;
_lcdInterface = lcdInterface;
AutoShift = false;
if (_lcdInterface.EightBitMode)
{
_displayFunction |= DisplayFunction.EightBit;
}
Initialize(size.Height);
_rowOffsets = InitializeRowOffsets(size.Height);
}
/// <summary>
/// Logical size, in characters, of the LCD.
/// </summary>
public Size Size { get; }
/// <summary>
/// Returns the number of custom characters for this display.
/// A custom character is one that can be user-defined and assigned to a slot using <see cref="CreateCustomCharacter(int,System.ReadOnlySpan{byte})"/>
/// </summary>
public virtual int NumberOfCustomCharactersSupported => 8;
/// <summary>
/// Initializes the display by setting the specified columns and lines.
/// </summary>
private void Initialize(int rows)
{
// While the chip supports 5x10 pixel characters for one line displays they
// don't seem to be generally available. Supporting 5x10 would require extra
// support for CreateCustomCharacter
if (GetTwoLineMode(rows))
{
_displayFunction |= DisplayFunction.TwoLine;
}
_displayControl |= DisplayControl.DisplayOn;
_displayMode |= DisplayEntryMode.Increment;
// Function must be set first to ensure that we always have the basic
// instruction set selected. (See PCF2119x datasheet Function_set note
// for one documented example of where this is necessary.)
SendCommandAndWait((byte)_displayFunction);
SendCommandAndWait((byte)_displayControl);
SendCommandAndWait((byte)_displayMode);
SendCommandAndWait(ClearDisplayCommand);
}
/// <summary>
/// Enable/disable the backlight. (Will always return false if no backlight pin was provided.)
/// </summary>
public virtual bool BacklightOn
{
get => _lcdInterface.BacklightOn;
set => _lcdInterface.BacklightOn = value;
}
/// <summary>
/// Sends byte to the device
/// </summary>
/// <param name="value">Byte to be sent to the device</param>
protected void SendData(byte value) => _lcdInterface.SendData(value);
/// <summary>
/// Sends command to the device
/// </summary>
/// <param name="command">Byte representing the command to be sent</param>
protected void SendCommand(byte command) => _lcdInterface.SendCommand(command);
/// <summary>
/// The initialization sequence and some other complex commands should be sent with delays, or the display may
/// behave unexpectedly. It may show random, blinking characters
/// or display text very faintly only.
/// </summary>
/// <param name="command">The command to send</param>
protected void SendCommandAndWait(byte command) => _lcdInterface.SendCommandAndWait(command);
/// <summary>
/// Sends data to the device
/// </summary>
/// <param name="values">Data to be send to the device</param>
protected void SendData(ReadOnlySpan<byte> values) => _lcdInterface.SendData(values);
/// <summary>
/// Sends data to the device
/// </summary>
/// <param name="values">Data to be send to the device</param>
protected void SendData(ReadOnlySpan<char> values) => _lcdInterface.SendData(values);
/// <summary>
/// Send commands to the device
/// </summary>
/// <param name="commands">Each byte represents command being sent to the device</param>
protected void SendCommands(ReadOnlySpan<byte> commands) => _lcdInterface.SendCommands(commands);
/// <summary>
/// Determines if the device should use two line mode
/// </summary>
/// <param name="rows">Number of rows on the device</param>
/// <returns>True if device should use two line mode</returns>
protected virtual bool GetTwoLineMode(int rows) => rows > 1;
/// <summary>
/// Initializes row offsets
/// </summary>
/// <param name="rows">Rows to be initialized</param>
/// <returns>Array of offsets</returns>
// In one-line mode DDRAM addresses go from 0 - 79 [0x00 - 0x4F]
//
// In two-line mode DDRAM addresses are laid out as follows:
//
// First row: 0 - 39 [0x00 - 0x27]
// Second row: 64 - 103 [0x40 - 0x67]
//
// (The address gap presumably is to allow all second row addresses to be
// identifiable with one bit? Not sure what the value of that is.)
//
// The chipset doesn't natively support more than two rows. For tested
// four row displays the two rows are split as follows:
//
// First row: 0 - 19 [0x00 - 0x13]
// Second row: 64 - 83 [0x40 - 0x53]
// Third row: 20 - 39 [0x14 - 0x27] (Continues first row)
// Fourth row: 84 - 103 [0x54 - 0x67] (Continues second row)
protected virtual byte[] InitializeRowOffsets(int rows) => rows switch
{
1 => new byte[1],
2 => new byte[] { 0, 64 },
4 => new byte[] { 0, 64, 20, 84 },
// We don't support other rows, users can derive for odd cases.
// (Three row LCDs exist, but aren't common.)
_ => throw new ArgumentOutOfRangeException(nameof(rows)),
};
/// <summary>
/// Wait for the device to not be busy.
/// </summary>
/// <param name="microseconds">Time to wait if checking busy state isn't possible/practical.</param>
protected void WaitForNotBusy(int microseconds) => _lcdInterface.WaitForNotBusy(microseconds);
/// <summary>
/// Clears the LCD, returning the cursor to home and unshifting if shifted.
/// Will also set to Increment.
/// </summary>
public void Clear()
{
SendCommand(ClearDisplayCommand);
// The HD44780 spec doesn't call out how long this takes. Home is documented as
// taking 1.52ms, and as this does more work (sets all memory to the space character)
// we do a longer wait. On the PCF2119x it is described as taking 165 clock cycles which
// would be 660μs on the "typical" clock.
WaitForNotBusy(2000);
}
/// <summary>
/// Moves the cursor to the first line and first column, unshifting if shifted.
/// </summary>
public void Home()
{
SendCommand(ReturnHomeCommand);
// The return home command is documented as taking 1.52ms with the standard 270KHz clock.
// SendCommand already waits for 37μs,
WaitForNotBusy(1520);
}
/// <summary>
/// Moves the cursor to an explicit column and row position.
/// </summary>
/// <param name="left">The column position from left to right starting with 0.</param>
/// <param name="top">The row position from the top starting with 0.</param>
public void SetCursorPosition(int left, int top)
{
int rows = _rowOffsets.Length;
if (top < 0 || top >= rows)
{
throw new ArgumentOutOfRangeException(nameof(top));
}
// Throw if we're given a negative left value or the calculated address would be
// larger than the max "good" address. Addressing is covered in detail in
// InitializeRowOffsets above.
int newAddress = left + _rowOffsets[top];
if (left < 0 || (rows == 1 && newAddress >= 80) || (rows > 1 && newAddress >= 104))
{
throw new ArgumentOutOfRangeException(nameof(left));
}
SendCommand((byte)(SetDDRamAddressCommand | newAddress));
}
/// <summary>
/// Enable/disable the display.
/// </summary>
public bool DisplayOn
{
get => (_displayControl & DisplayControl.DisplayOn) > 0;
set => SendCommandAndWait((byte)(value ? _displayControl |= DisplayControl.DisplayOn
: _displayControl &= ~DisplayControl.DisplayOn));
}
/// <summary>
/// Enable/disable the underline cursor.
/// </summary>
public bool UnderlineCursorVisible
{
get => (_displayControl & DisplayControl.CursorOn) > 0;
set => SendCommandAndWait((byte)(value ? _displayControl |= DisplayControl.CursorOn
: _displayControl &= ~DisplayControl.CursorOn));
}
/// <summary>
/// Enable/disable the blinking cursor.
/// </summary>
public bool BlinkingCursorVisible
{
get => (_displayControl & DisplayControl.BlinkOn) > 0;
set => SendCommandAndWait((byte)(value ? _displayControl |= DisplayControl.BlinkOn
: _displayControl &= ~DisplayControl.BlinkOn));
}
/// <summary>
/// When enabled the display will shift rather than the cursor.
/// </summary>
public bool AutoShift
{
get => (_displayMode & DisplayEntryMode.DisplayShift) > 0;
set => SendCommandAndWait((byte)(value ? _displayMode |= DisplayEntryMode.DisplayShift
: _displayMode &= ~DisplayEntryMode.DisplayShift));
}
/// <summary>
/// Gets/sets whether the cursor location increments (true) or decrements (false).
/// </summary>
public bool Increment
{
get => (_displayMode & DisplayEntryMode.Increment) > 0;
set => SendCommandAndWait((byte)(value ? _displayMode |= DisplayEntryMode.Increment
: _displayMode &= ~DisplayEntryMode.Increment));
}
/// <summary>
/// Move the display left one position.
/// </summary>
public void ShiftDisplayLeft() => SendCommand((byte)(DisplayShift.Command | DisplayShift.Display));
/// <summary>
/// Move the display right one position.
/// </summary>
public void ShiftDisplayRight() => SendCommand((byte)(DisplayShift.Command | DisplayShift.Display | DisplayShift.Right));
/// <summary>
/// Move the cursor left one position.
/// </summary>
public void ShiftCursorLeft() => SendCommand((byte)(DisplayShift.Command | DisplayShift.Display));
/// <summary>
/// Move the cursor right one position.
/// </summary>
public void ShiftCursorRight() => SendCommand((byte)(DisplayShift.Command | DisplayShift.Display | DisplayShift.Right));
/// <summary>
/// Fill one of the 8 CGRAM locations (character codes 0 - 7) with custom characters.
/// </summary>
/// <remarks>
/// The custom characters also occupy character codes 8 - 15.
///
/// You can find help designing characters at https://www.quinapalus.com/hd44780udg.html.
///
/// The datasheet description for custom characters is very difficult to follow. Here is
/// a rehash of the technical details that is hopefully easier:
///
/// Only 6 bits of addresses are available for character ram. That makes for 64 bytes of
/// available character data. 8 bytes of data are used for each character, which is where
/// the 8 total custom characters comes from (64/8).
///
/// Each byte corresponds to a character line. Characters are only 5 bits wide so only
/// bits 0-4 are used for display. Whatever is in bits 5-7 is just ignored. Store bits
/// there if it makes you happy, but it won't impact the display. '1' is on, '0' is off.
///
/// In the built-in characters the 8th byte is usually empty as this is where the underline
/// cursor will be if enabled. You can put data there if you like, which gives you the full
/// 5x8 character. The underline cursor just turns on the entire bottom row.
///
/// 5x10 mode is effectively useless as displays aren't available that utilize it. In 5x10
/// mode *16* bytes of data are used for each character. That leaves room for only *4*
/// custom characters. The first character is addressable from code 0, 1, 8, and 9. The
/// second is 2, 3, 10, 11 and so on...
///
/// In this mode *11* bytes of data are actually used for the character data, which
/// effectively gives you a 5x11 character, although typically the last line is blank to
/// leave room for the underline cursor. Why the modes are referred to as 5x8 and 5x10 as
/// opposed to 5x7 and 5x10 or 5x8 and 5x11 is a mystery. In an early pre-release data
/// book 5x7 and 5x10 is used (Advance Copy #AP4 from July 1985). Perhaps it was a
/// marketing change?
///
/// As only 11 bytes are used in 5x10 mode, but 16 bytes are reserved, the last 5 bytes
/// are useless. The datasheet helpfully suggests that you can store your own data there.
/// The same would be true for bits 5-7 of lines that matter for both 5x8 and 5x10.
/// </remarks>
/// <param name="location">Should be between 0 and 7</param>
/// <param name="characterMap">Provide an array of 8 bytes containing the pattern</param>
public void CreateCustomCharacter(int location, ReadOnlySpan<byte> characterMap)
{
if (location >= NumberOfCustomCharactersSupported)
{
throw new ArgumentOutOfRangeException(nameof(location));
}
if (characterMap.Length != 8)
{
throw new ArgumentException("The character map must be exactly 8 bytes long", nameof(characterMap));
}
// The character address is set in bits 3-5 of the command byte
SendCommand((byte)(SetCGRamAddressCommand | (location << 3)));
SendData(characterMap);
}
/// <summary>
/// Fill one of the 8 CGRAM locations (character codes 0 - 7) with custom characters.
/// See <see cref="CreateCustomCharacter(int,System.ReadOnlySpan{byte})"/> for details.
/// </summary>
/// <param name="location">Should be between 0 and 7</param>
/// <param name="characterMap">Provide an array of 8 bytes containing the pattern</param>
public void CreateCustomCharacter(int location, byte[] characterMap)
{
CreateCustomCharacter(location, new ReadOnlySpan<byte>(characterMap));
}
/// <summary>
/// Write text to display.
/// </summary>
/// <param name="text">Text to be displayed.</param>
/// <remarks>
/// There are only 256 characters available. There are chip variants
/// with different character sets. Characters from space ' ' (32) to
/// '}' are usually the same with the exception of '\', which is a
/// yen symbol on some chips '¥'.
/// </remarks>
public void Write(string text)
{
Span<byte> buffer = stackalloc byte[text.Length];
for (int i = 0; i < text.Length; ++i)
{
buffer[i] = (byte)text[i];
}
SendData(buffer);
}
/// <summary>
/// Write a raw byte stream to the display.
/// Used if character translation already took place
/// </summary>
/// <param name="text">Text to print</param>
public void Write(ReadOnlySpan<char> text) => SendData(text);
/// <summary>
/// Write a raw byte stream to the display.
/// Used if character translation already took place
/// </summary>
/// <param name="text">Text to print</param>
public void Write(char[] text) => SendData(text);
/// <summary>
/// Releases unmanaged resources used by Hd44780
/// and optionally release managed resources
/// </summary>
public virtual void Dispose() => _lcdInterface?.Dispose();
}
}