/
Ssd13xx.cs
648 lines (579 loc) · 24.2 KB
/
Ssd13xx.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
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
// 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.Device.Gpio;
using System.Device.I2c;
using System.Threading;
using Iot.Device.Ssd13xx.Commands;
using Iot.Device.Ssd13xx.Commands.Ssd1306Commands;
namespace Iot.Device.Ssd13xx
{
/// <summary>
/// Represents base class for SSD13xx OLED displays
/// </summary>
public abstract class Ssd13xx : IDisposable
{
// Multiply of screen resolution plus single command byte.
private byte[] _genericBuffer;
private byte[] _pageData;
/// <summary>
/// Underlying I2C device
/// </summary>
protected I2cDevice _i2cDevice;
/// <summary>
/// Screen Resolution Width in Pixels
/// </summary>
public int Width { get; set; }
/// <summary>
/// Screen Resolution Height in Pixels
/// </summary>
public int Height { get; set; }
/// <summary>
/// Screen data pages.
/// </summary>
public byte Pages { get; set; }
/// <summary>
/// Font to use.
/// </summary>
public IFont Font { get; set; }
private GpioController _gpioController;
private int _resetPin;
private bool _shouldDispose;
private bool _disposed = false;
/// <summary>
/// Constructs instance of Ssd13xx
/// </summary>
/// <param name="i2cDevice">I2C device used to communicate with the device</param>
/// <param name="resetPin">Reset pin (some displays might be wired to share the microcontroller's
/// reset pin).</param>
/// <param name="resolution">Screen resolution to use for device init.</param>
/// <param name="gpio">Gpio Controller.</param>
/// <param name="shouldDispose">True to dispose the GpioController.</param>
public Ssd13xx(
I2cDevice i2cDevice,
DisplayResolution resolution = DisplayResolution.OLED128x64,
int resetPin = -1,
GpioController gpio = null,
bool shouldDispose = true)
{
_resetPin = resetPin;
if (resetPin >= 0)
{
_gpioController = gpio ?? new GpioController();
this.Reset();
}
_shouldDispose = shouldDispose || (gpio == null);
_i2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice));
switch (resolution)
{
case DisplayResolution.OLED128x64:
Width = 128;
Height = 64;
_i2cDevice.Write(_oled128x64Init);
break;
case DisplayResolution.OLED128x32:
Width = 128;
Height = 32;
_i2cDevice.Write(_oled128x32Init);
break;
case DisplayResolution.OLED96x16:
Width = 96;
Height = 16;
_i2cDevice.Write(_oled96x16Init);
break;
}
Pages = (byte)(Height / 8);
_genericBuffer = new byte[Pages * Width + 4];//adding 4 bytes make it SSH1106 IC OLED compatible
_pageData = new byte[Width + 1];
}
/// <summary>
/// Verifies value is within a specific range.
/// </summary>
/// <param name="value">Value to check.</param>
/// <param name="start">Starting value of range.</param>
/// <param name="end">Ending value of range.</param>
/// <returns>Determines if value is within range.</returns>
internal static bool InRange(uint value, uint start, uint end)
{
return (value - start) <= (end - start);
}
/// <summary>
/// Send a command to the display controller.
/// </summary>
/// <param name="command">The command to send to the display controller.</param>
public abstract void SendCommand(ISharedCommand command);
/// <summary>
/// Send data to the display controller.
/// </summary>
/// <param name="data">The data to send to the display controller.</param>
public virtual void SendData(SpanByte data)
{
if (data.IsEmpty)
{
throw new ArgumentNullException(nameof(data));
}
SpanByte writeBuffer = SliceGenericBuffer(data.Length + 1);
writeBuffer[0] = 0x40; // Control byte.
data.CopyTo(writeBuffer.Slice(1));
_i2cDevice.Write(writeBuffer);
}
/// <summary>
/// Acquires span of specific length pointing to the command buffer.
/// If length of the command buffer is too small it will be reallocated.
/// </summary>
/// <param name="length">Requested length</param>
/// <returns>Span of bytes pointing to the command buffer</returns>
protected SpanByte SliceGenericBuffer(int length) => SliceGenericBuffer(0, length);
/// <summary>
/// Acquires span of specific length at specific position in command buffer.
/// If length of the command buffer is too small it will be reallocated.
/// </summary>
/// <param name="start">Start index of the requested span</param>
/// <param name="length">Requested length</param>
/// <returns>Span of bytes pointing to the command buffer</returns>
protected SpanByte SliceGenericBuffer(int start, int length)
{
if (_genericBuffer.Length < length)
{
var newBuffer = new byte[_genericBuffer.Length * 2];
_genericBuffer.CopyTo(newBuffer, 0);
_genericBuffer = newBuffer;
}
return _genericBuffer;
}
/// <summary>
/// Copies buffer content directly to display buffer.
/// Y and height must be byte aligned because buffer will
/// be copied without any logical operations on existing content.
/// </summary>
/// <param name="x">The x coordinate on the screen.</param>
/// <param name="y">The y coordinate on the screen.</param>
/// <param name="width">Width of buffer content in pixels.</param>
/// <param name="height">Height of buffer content in pixels.</param>
/// <param name="buffer">Data to copy. Buffer size must be equal to height * width / 8.</param>
public void DrawDirectAligned(int x, int y, int width, int height, byte[] buffer)
{
if ((y % 8) != 0)
{
throw new ArgumentException("Y must be aligned to byte boundary.");
}
if ((height % 8) != 0)
{
throw new ArgumentException("Height must be aligned to byte boundary.");
}
var dataHeightInBytes = height / 8;
if ((dataHeightInBytes * width) != buffer.Length)
{
throw new ArgumentException("Width and height do not match the bitmap size.");
}
var genericBufferIdx = y / 8 * Width + x;
var srcIdx = 0;
for (int i = 0; i < dataHeightInBytes; i++)
{
Array.Copy(buffer, srcIdx, _genericBuffer, genericBufferIdx, width);
srcIdx += width;
genericBufferIdx += Width;
}
}
/// <summary>
/// Clears portion of display via writing 0x00 directly to display buffer.
/// Y and height must be byte aligned because bytes will
/// be written without any logical operations on existing content.
/// </summary>
/// <param name="x">The x coordinate on the screen.</param>
/// <param name="y">The y coordinate on the screen.</param>
/// <param name="width">Width of area in pixels.</param>
/// <param name="height">Height of area in pixels.</param>
public void ClearDirectAligned(int x, int y, int width, int height)
{
if ((y % 8) != 0)
{
throw new ArgumentException("Y must be aligned to byte boundary.");
}
if ((height % 8) != 0)
{
throw new ArgumentException("Height must be aligned to byte boundary.");
}
var dataHeightInBytes = height / 8;
var genericBufferIdx = y / 8 * Width + x;
for (int i = 0; i < dataHeightInBytes; i++)
{
Array.Clear(_genericBuffer, genericBufferIdx, width);
genericBufferIdx += Width;
}
}
/// <summary>
/// Draws a pixel on the screen.
/// </summary>
/// <param name="x">The x coordinate on the screen.</param>
/// <param name="y">The y coordinate on the screen.</param>
/// <param name="inverted">Indicates if color to be used turn the pixel on, or leave off.</param>
public void DrawPixel(int x, int y, bool inverted = true)
{
if ((x >= Width) || (y >= Height))
{
return;
}
// x specifies the column
int idx = x + (y / 8) * Width;
if (inverted)
{
_genericBuffer[idx] |= (byte)(1 << (y & 7));
}
else
{
_genericBuffer[idx] &= (byte)~(1 << (y & 7));
}
}
/// <summary>
/// Draws a horizontal line.
/// </summary>
/// <param name="x0">x coordinate starting of the line.</param>
/// <param name="y0">y coordinate starting of line.</param>
/// <param name="length">Line length.</param>
/// <param name="inverted">Turn the pixel on (true) or off (false).</param>
public void DrawHorizontalLine(int x0, int y0, int length, bool inverted = true)
{
for (var x = x0; (x - x0) < length; x++)
{
DrawPixel(x, y0, inverted);
}
}
/// <summary>
/// Draws a vertical line.
/// </summary>
/// <param name="x0">x coordinate starting of the line.</param>
/// <param name="y0">y coordinate starting of line.</param>
/// <param name="length">Line length.</param>
/// <param name="inverted">Turn the pixel on (true) or off (false).</param>
public void DrawVerticalLine(int x0, int y0, int length, bool inverted = true)
{
for (var y = y0; (y - y0) < length; y++)
{
DrawPixel(x0, y, inverted);
}
}
/// <summary>
/// Draws a rectangle that is solid/filled.
/// </summary>
/// <param name="x0">x coordinate starting of the top left.</param>
/// <param name="y0">y coordinate starting of the top left.</param>
/// <param name="width">Width of rectabgle in pixels.</param>
/// <param name="height">Height of rectangle in pixels</param>
/// <param name="inverted">Turn the pixel on (true) or off (false).</param>
public void DrawFilledRectangle(int x0, int y0, int width, int height, bool inverted = true)
{
for (int i = 0; i <= height; i++)
{
DrawHorizontalLine(x0, y0 + i, width, inverted);
}
}
/// <summary>
/// Displays the 1 bit bit map.
/// </summary>
/// <param name="x">The x coordinate on the screen.</param>
/// <param name="y">The y coordinate on the screen.</param>
/// <param name="width">Width in bytes.</param>
/// <param name="height">Height in bytes.</param>
/// <param name="bitmap">Bitmap to display.</param>
/// <param name="size">Drawing size, normal = 1, larger use 2,3 etc.</param>
public void DrawBitmap(int x, int y, int width, int height, byte[] bitmap, byte size = 1)
{
if ((width * height) != bitmap.Length)
{
throw new ArgumentException("Width and height do not match the bitmap size.");
}
byte mask = 0x01;
byte b;
for (var yO = 0; yO < height; yO++)
{
for (var xA = 0; xA < width; xA++)
{
b = bitmap[(yO * width) + xA];
for (var pixel = 0; pixel < 8; pixel++)
{
if (size == 1)
{
DrawPixel(x + (8 * xA) + pixel, y + yO, (b & mask) > 0);
}
else
{
DrawFilledRectangle((x + (8 * xA) + pixel) * size, (y / size + yO) * size, size, size, (b & mask) > 0);
}
mask <<= 1;
}
mask = 0x01;//reset each time to support SSH1106 OLEDs
}
}
}
/// <summary>
/// Writes a text message on the screen with font in use.
/// </summary>
/// <param name="x">The x pixel-coordinate on the screen.</param>
/// <param name="y">The y pixel-coordinate on the screen.</param>
/// <param name="str">Text string to display.</param>
/// <param name="size">Text size, normal = 1, larger use 2,3, 4 etc.</param>
/// <param name="center">Indicates if text should be centered if possible.</param>
/// <seealso cref="Write"/>
public void DrawString(int x, int y, string str, byte size = 1, bool center = false)
{
if (center && str != null)
{
int padSize = (Width / size / Font.Width - str.Length) / 2;
if (padSize > 0)
str = str.PadLeft(str.Length + padSize);
}
byte[] bitMap = (Font.Width > 8 ? this.GetDoubleTextBytes(str) : this.GetTextBytes(str));
this.DrawBitmap(x, y, bitMap.Length / Font.Height, Font.Height, bitMap, size);
}
/// <summary>
/// Writes a text message on the screen with font in use.
/// </summary>
/// <param name="x">The x text-coordinate on the screen.</param>
/// <param name="y">The y text-coordinate on the screen.</param>
/// <param name="str">Text string to display.</param>
/// <param name="size">Text size, normal = 1, larger use 2,3, 4 etc.</param>
/// <param name="center">Indicates if text should be centered if possible.</param>
/// <seealso cref="DrawString"/>
public void Write(int x, int y, string str, byte size = 1, bool center = false)
{
DrawString(x * Font.Width, y * Font.Height, str, size, center);
}
/// <summary>
/// Get the bytes to be drawn on the screen for text, from the font
/// </summary>
/// <param name="text">Strint to be shown on the screen.</param>
/// <returns>The bytes to be drawn using current font.</returns>
byte[] GetTextBytes(string text)
{
byte[] bitMap;
if (Font.Width == 8)
{
bitMap = new byte[text.Length * Font.Height * Font.Width / 8];
for (int i = 0; i < text.Length; i++)
{
var characterMap = Font[text[i]];
for (int segment = 0; segment < Font.Height; segment++)
{
bitMap[i + (segment * text.Length)] = (segment < characterMap.Length ? characterMap[segment] : byte.MinValue);
}
}
}
else
{
throw new Exception("Font width must be 8");
}
return bitMap;
}
/// <summary>
/// Get the bytes to be drawn on the screen for double-byte text, from the font
/// e.g. 功夫, カンフー, 쿵후
/// </summary>
/// <param name="text">Strint to be shown on the screen.</param>
/// <returns>The bytes to be drawn using current font.</returns>
byte[] GetDoubleTextBytes(string text)
{
byte[] bitMap;
if (Font.Width == 16)
{
var charCount = (text.Length * 2);
bitMap = new byte[charCount * Font.Height * Font.Width / 16];
byte[] characterMap = null;
for (int i = 0; i < charCount; i++)
{
var even = (i == 0 || i % 2 == 0);
if (even)
{
characterMap = Font[text[i / 2]];
}
var list = new System.Collections.ArrayList();
for (int idx = (even ? 0 : 1); idx < characterMap.Length; idx += 2)
{
list.Add(characterMap[idx]);
}
for (int segment = 0; segment < Font.Height; segment++)
{
bitMap[i + (segment * charCount)] = (segment < list.Count ? (byte)list[segment] : byte.MinValue);
}
}
}
else
{
throw new Exception("Double-byte characters font width must be 16");
}
return bitMap;
}
/// <summary>
/// Displays the information on the screen using page mode.
/// </summary>
public void Display()
{
for (byte i = 0; i < Pages; i++)
{
_pageCmd[1] = (byte)(PageAddress.Page0 + i); // page number
_i2cDevice.Write(_pageCmd);
_pageData[0] = 0x40; // is data
Array.Copy(_genericBuffer, i * Width, _pageData, 1, Width);
_i2cDevice.Write(_pageData);
}
}
/// <summary>
/// Clears the screen.
/// </summary>
public void ClearScreen()
{
Array.Clear(_genericBuffer, 0, _genericBuffer.Length);
//Display();
}
/// <summary>
/// Sequence of bytes that should be sent to a 128x64 OLED display to setup the device.
/// First byte is the command byte 0x00.
/// </summary>
private readonly byte[] _oled128x64Init =
{
0x00, // is command
0xae, // turn display off
0xd5, 0x80, // set display clock divide ratio/oscillator, set ratio = 0x80
0xa8, 0x3f, // set multiplex ratio 0x00-0x3f
0xd3, 0x00, // set display offset 0x00-0x3f, no offset = 0x00
0x40 | 0x0, // set display start line 0x40-0x7F
0x8d, 0x14, // set charge pump, enable = 0x14 disable = 0x10
0x20, 0x00, // 0x20 set memory address mode, 0x0 = horizontal addressing mode
0xa0 | 0x1, // set segment re-map
0xc8, // set com output scan direction
0xda, 0x12, // set COM pins HW configuration
0x81, 0xcf, // set contrast control for BANK0, extVcc = 0x9F, internal = 0xcf
0xd9, 0xf1, // set pre-charge period to 0xf1, if extVcc then set to 0x22
0xdb, // set VCOMH deselect level
0x40, // set display start line
0xa4, // set display ON
0xa6, // set normal display
0xaf // turn display on 0xaf
};
/// <summary>
/// Sequence of bytes that should be sent to a 128x32 OLED display to setup the device.
/// First byte is the command byte 0x00.
/// </summary>
private readonly byte[] _oled128x32Init =
{
0x00, // is command
0xae, // turn display off
0xd5, 0x80, // set display clock divide ratio/oscillator, set ratio = 0x80
0xa8, 0x1f, // set multiplex ratio 0x00-0x1f
0xd3, 0x00, // set display offset 0x00-0x3f, no offset = 0x00
0x40 | 0x0, // set display start line 0x40-0x7F
0x8d, 0x14, // set charge pump, enable = 0x14 disable = 0x10
0x20, 0x00, // 0x20 set memory address mode, 0x0 = horizontal addressing mode
0xa0 | 0x1, // set segment re-map
0xc8, // set com output scan direction
0xda, 0x02, // set COM pins HW configuration
0x81, 0x8f, // set contrast control for BANK0, extVcc = 0x9F, internal = 0xcf
0xd9, 0xf1, // set pre-charge period to 0xf1, if extVcc then set to 0x22
0xdb, // set VCOMH deselect level
0x40, // set display start line
0xa4, // set display ON
0xa6, // set normal display
0xaf // turn display on 0xaf
};
/// <summary>
/// Sequence of bytes that should be sent to a 96x16 OLED display to setup the device.
/// First byte is the command byte 0x00.
/// </summary>
private readonly byte[] _oled96x16Init =
{
0x00, // is command
0xae, // turn display off
0xd5, 0x80, // set display clock divide ratio/oscillator, set ratio = 0x80
0xa8, 0x1f, // set multiplex ratio 0x00-0x1f
0xd3, 0x00, // set display offset 0x00-0x3f, no offset = 0x00
0x40 | 0x0, // set display start line 0x40-0x7F
0x8d, 0x14, // set charge pump, enable = 0x14 disable = 0x10
0x20, 0x00, // 0x20 set memory address mode, 0x0 = horizontal addressing mode
0xa0 | 0x1, // set segment re-map
0xc8, // set com output scan direction
0xda, 0x02, // set COM pins HW configuration
0x81, 0xaf, // set contrast control for BANK0, extVcc = 0x9F, internal = 0xcf
0xd9, 0xf1, // set pre-charge period to 0xf1, if extVcc then set to 0x22
0xdb, // set VCOMH deselect level
0x40, // set display start line
0xa4, // set display ON
0xa6, // set normal display
0xaf // turn display on 0xaf
};
/// <summary>
/// Resolution specifier.
/// </summary>
public enum DisplayResolution
{
/// <summary>
/// Option for 128x64 OLED
/// </summary>
OLED128x64,
/// <summary>
/// Option for 128x32 OLED
/// </summary>
OLED128x32,
/// <summary>
/// Option for 96x16 OLED
/// </summary>
OLED96x16
}
/// <summary>
/// Page mode output command bytes.
/// </summary>
private byte[] _pageCmd = new byte[]
{
0x00, // is command
0xB0, // page address (B0-B7)
0x00, // lower columns address =0
0x10, // upper columns address =0
};
/// <summary>
/// Reset display controller.
/// </summary>
private void Reset()
{
GpioPin rstPin = _gpioController.OpenPin(_resetPin, PinMode.Output);
rstPin.Write(PinValue.High);
Thread.Sleep(1); // VDD goes high at start, pause for 1 ms
rstPin.Write(PinValue.Low); // Bring reset low
Thread.Sleep(10); // Wait 10 ms
rstPin.Write(PinValue.High); // Bring out of reset
Thread.Sleep(1);
}
/// <summary>
/// Cleanup resources.
/// </summary>
/// <param name="disposing">Should dispose managed resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
if (_resetPin >= 0)
{
_gpioController.ClosePin(_resetPin);
if (_shouldDispose)
{
_gpioController.Dispose();
_gpioController = null;
}
}
_i2cDevice?.Dispose();
_i2cDevice = null!;
_disposed = true;
}
}
/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}