File Coverage

blib/lib/Device/Chip/MAX7219Panel.pm
Criterion Covered Total %
statement 137 141 97.1
branch 15 18 83.3
condition n/a
subroutine 24 26 92.3
pod 14 17 82.3
total 190 202 94.0


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2022-2023 -- leonerd@leonerd.org.uk
5              
6 4     4   894678 use v5.26;
  4         40  
7 4     4   27 use warnings;
  4         7  
  4         170  
8 4     4   21 use Object::Pad 0.800 ':experimental(adjust_params)';
  4         31  
  4         181  
9              
10             package Device::Chip::MAX7219Panel 0.09;
11             class Device::Chip::MAX7219Panel
12             :isa(Device::Chip);
13              
14 4     4   1439 use Carp;
  4         9  
  4         227  
15 4     4   23 use Future::AsyncAwait;
  4         19  
  4         33  
16              
17 4     4   277 use constant PROTOCOL => 'SPI';
  4         6  
  4         970  
18              
19             =head1 NAME
20              
21             C - chip driver for a panel of F modules
22              
23             =head1 SYNOPSIS
24              
25             use Device::Chip::MAX7219Panel;
26             use Future::AsyncAwait;
27              
28             my $chip = Device::Chip::MAX7219Panel->new;
29             await $chip->mount( Device::Chip::Adapter::...->new );
30              
31             await $chip->init;
32             await $chip->intensity( 2 );
33              
34             $panel->clear;
35             $panel->draw_hline( 0, $panel->columns-1, 2 );
36             $panel->draw_vline( 12, 0, $panel->rows-1 );
37              
38             await $panel->refresh;
39              
40             =head1 DESCRIPTION
41              
42             This L subclass provides specific communication to an LED panel
43             comprised of multiple F F or similar chips,
44             attached to a computer via an SPI adapter. It maintains a virtual frame-buffer
45             to drive the display in terms of drawing commands.
46              
47             This module expects to find a chain of F or similar chips connected
48             together in a daisy-chain fashion; sharing the C and C signals, and
49             with each chip's C feeding into the C of the next. The chips should be
50             connected in rows, with the first at top left corner, then across the top row,
51             with each subsequent row below it in the same manner. For example, when using
52             8 chips arranged in a 32x16 geometry, the chips would be in the order below:
53              
54             1 2 3 4
55             5 6 7 8
56              
57             The overall API shape of this module is similar to that of
58             L, supporting the same set of drawing methods. A future
59             version of both of these modules may extend the concept into providing access
60             via an interface helper instance, if some standard API shape is defined for
61             driving these kinds of 1bpp pixel displays.
62              
63             =cut
64              
65             method SPI_options
66 4     4 0 1396 {
67             return (
68 4         25 mode => 0,
69             max_bitrate => 1E6,
70             );
71             }
72              
73             use constant {
74 4         13549 REG_NONE => 0x00,
75             REG_DIGIT => 0x01, # .. to 8
76             REG_DECODE => 0x09,
77             REG_INTENSITY => 0x0A,
78             REG_LIMIT => 0x0B,
79             REG_SHUTDOWN => 0x0C,
80             REG_DTEST => 0x0F,
81 4     4   29 };
  4         7  
82              
83             field $_nchips;
84              
85 5     5 1 240 field $_rows :reader;
  5         31  
86 5     5 1 24 field $_columns :reader;
  5         25  
87              
88             =head1 CONSTRUCTOR
89              
90             =head2 new
91              
92             $panel = Device::Chip::MAX7219Panel->new( %opts );
93              
94             Returns a new C instance.
95              
96             The following additional options may be passed:
97              
98             =over 4
99              
100             =item geom => STRING
101              
102             The overall geometry of the display panel, as two integers giving column and
103             row count expressed as a string C. If not specified, will
104             default to the common size for cheaply available module panels, of C<32x8>.
105              
106             Both the column and row count must be a multiple of 8.
107              
108             =item xflip => BOOL
109              
110             =item yflip => BOOL
111              
112             If true, the order of columns or rows respectively will be reversed. In
113             particular, if both are true, this inverts the orientation of the display, if
114             it is mounted upside-down.
115              
116             =back
117              
118             =cut
119              
120             ADJUST :params (
121             :$geom = "32x8",
122             ) {
123             ( $_columns, $_rows ) = $geom =~ m/^(\d+)x(\d+)/ or
124             croak "Unable to parse rows/columns from geometry argument $geom";
125              
126             ( $_columns % 8 ) == 0 or croak "Expected columns to be a multiple of 8";
127             ( $_rows % 8 ) == 0 or croak "Expected rows to be a multiple of 8";
128              
129             $_nchips = ( $_rows / 8 ) * ( $_columns / 8 );
130             }
131              
132 0     0 1 0 field $_xflip :param :reader :writer = 0;
133 0     1 0 0 field $_yflip :param :reader :writer = 0;
  1     0 1 1886  
  1         6  
  0         0  
134              
135 0     1 0 0 =head1 METHODS
  1         1731  
  1         4  
136              
137             The following methods documented in an C expression return L
138             instances.
139              
140             =cut
141              
142             =head2 rows
143              
144             =head2 columns
145              
146             $n = $panel->rows;
147              
148             $n = $panel->columns;
149              
150             Simple accessors that return the number of rows or columns present on the
151             combined physical display panel.
152              
153             =cut
154              
155 17         27 async method _all_writereg ( $reg, $value )
  17         28  
  17         31  
  17         27  
156 17         43 {
157 17         49 await $self->protocol->write( join "", ( chr( $reg ) . chr( $value ) ) x $_nchips );
158 17     17   30 }
159              
160             =head2 init
161              
162             await $panel->init();
163              
164             Initialises the settings across every chip and ready to be used by this
165             module. This method should be called once on startup.
166              
167             =cut
168              
169 3         6 async method init ()
  3         5  
170 3         9 {
171 3         11 await $self->_all_writereg( REG_LIMIT, 7 );
172 3         18643 await $self->_all_writereg( REG_DECODE, 0 );
173 3     3 1 404 }
174              
175             =head2 intensity
176              
177             await $panel->intensity( $value );
178              
179             Sets the intensity register across every chip. C<$value> must be between 0 and
180             15, with higher values giving a more intense output.
181              
182             =cut
183              
184 1         4 async method intensity ( $value )
  1         2  
  1         2  
185 1         3 {
186 1         4 await $self->_all_writereg( REG_INTENSITY, $value );
187 1     1 1 5965 }
188              
189             =head2 shutdown
190              
191             await $panel->shutdown( $off = 1 );
192              
193             Sets the shutdown register across every chip. C<$off> defaults to true, to
194             turn the panel off. If defined but false (such as C<0>), the panel will be
195             switched on.
196              
197             =cut
198              
199 1         3 async method shutdown ( $off = 1 )
  1         3  
  1         2  
200 1         4 {
201 1         4 await $self->_all_writereg( REG_SHUTDOWN, !$off );
202 1     1 1 4628 }
203              
204             =head2 displaytest
205              
206             await $panel->displaytest( $on );
207              
208             Sets the display test register across every chip, overriding the output
209             control and turning on every LED if set to a true value, or restoring normal
210             operation if set to false.
211              
212             =cut
213              
214 1         3 async method displaytest ( $on )
  1         2  
  1         3  
215 1         4 {
216 1         4 await $self->_all_writereg( REG_DTEST, $on );
217 1     1 1 4642 }
218              
219 16         24 async method _write_raw ( $d, $data )
  16         25  
  16         29  
  16         22  
220 16         54 {
221             await $self->protocol->write( join "",
222 16         45 map { chr( REG_DIGIT+$d ) . substr( $data, $_, 1 ) } 0 .. ($_nchips-1)
223             );
224 16     16   28 }
225              
226             ### Display buffer and drawing methods
227              
228             =head1 DRAWING METHODS
229              
230             The following methods operate on an internal framebuffer stored by the
231             instance. The user must invoke the L method to update the actual
232             panel chips after calling them.
233              
234             =cut
235              
236             field @_display;
237             field $_is_display_dirty;
238              
239             =head2 clear
240              
241             $panel->clear;
242              
243             Resets the stored framebuffer to blank.
244              
245             =cut
246              
247 5         8 method clear ()
  5         9  
248 5     5 1 11429 {
249             @_display = (
250 5         15 map { [ ( 0 ) x $_columns ] } 1 .. $_rows
  48         166  
251             );
252 5         16 $_is_display_dirty = 1;
253             }
254              
255             =head2 draw_pixel
256              
257             $panel->draw_pixel( $x, $y, $val = 1 )
258              
259             Draw the given pixel. If the third argument is calse, the pixel will be
260             cleared instead of set.
261              
262             =cut
263              
264 3         9 method draw_pixel ( $x, $y, $val = 1 )
  3         5  
  3         5  
  3         6  
  3         4  
265 3     3 1 509 {
266 3         7 $_display[$y][$x] = $val;
267 3         9 $_is_display_dirty = 1;
268             }
269              
270             =head2 draw_hline
271              
272             $panel->draw_hline( $x1, $x2, $y, $val = 1 )
273              
274             Draw a horizontal line in the given I<$y> row, between the columns I<$x1> and
275             I<$x2> (inclusive). If the fourth argument is false, the pixels will be
276             cleared instead of set.
277              
278             =cut
279              
280 3         9 method draw_hline ( $x1, $x2, $y, $val = 1 )
  3         6  
  3         5  
  3         5  
  3         7  
  3         4  
281 3     3 1 16493 {
282 3         33 $_display[$y][$_] = $val for $x1 .. $x2;
283 3         9 $_is_display_dirty = 1;
284             }
285              
286             =head2 draw_vline
287              
288             $panel->draw_vline( $x, $y1, $y2, $val = 1 )
289              
290             Draw a vertical line in the given I<$x> column, between the rows I<$y1> and
291             I<$y2> (inclusive). If the fourth argument is false, the pixels will be
292             cleared instead of set.
293              
294             =cut
295              
296 3         7 method draw_vline ( $x, $y1, $y2, $val = 1 )
  3         5  
  3         6  
  3         5  
  3         6  
  3         4  
297 3     3 1 1962 {
298 3         16 $_display[$_][$x] = $val for $y1 .. $y2;
299 3         9 $_is_display_dirty = 1;
300             }
301              
302             =head2 draw_blit
303              
304             $panel->draw_blit( $x, $y, @lines );
305              
306             Draws a bitmap pattern by copying the data given in lines, starting at the
307             given position.
308              
309             Each value in I<@lines> should be a string giving a horizontal line of bitmap
310             data, each character corresponding to a single pixel of the display. Pixels
311             corresponding to spaces will be left alone, a hyphen will be cleared, and any
312             other character (for example a C<#>) will be set.
313              
314             For example, to draw a rightward-pointing arrow:
315              
316             $panel->draw_blit( 6, 1,
317             " # ",
318             " ## ",
319             "######",
320             "######",
321             " ## ",
322             " # " );
323              
324             =cut
325              
326 1         3 method draw_blit ( $x0, $y, @lines )
  1         2  
  1         3  
  1         4  
  1         2  
327 1     1 1 1669 {
328 1         5 for( ; @lines; $y++ ) {
329 6         34 my @pixels = split m//, shift @lines;
330 6 50       14 @pixels or next;
331              
332 6         8 my $x = $x0;
333 6         13 for( ; @pixels; $x++ ) {
334 36         53 my $p = shift @pixels;
335              
336 36 50       77 $p eq " " ? next :
    100          
337             $p eq "-" ? ( $_display[$y][$x] = 0 ) :
338             ( $_display[$y][$x] = 1 );
339              
340 20         40 $_is_display_dirty = 1;
341             }
342             }
343             }
344              
345             =head2 refresh
346              
347             await $panel->refresh;
348              
349             Sends the framebuffer to the panel chips.
350              
351             =cut
352              
353 9         11 async method refresh ()
  9         15  
354 9         24 {
355 9 50       26 return unless $_is_display_dirty;
356              
357 9         24 await $self->_all_writereg( REG_SHUTDOWN, 0 );
358              
359 9         14026 my @digits;
360              
361             # Write rows in reverse order, so the data appears in the right order if $_rows > 8
362 9         33 foreach my $row ( reverse 0 .. $_rows-1 ) {
363 80         113 my $data = "";
364 80         106 my $v = 0;
365 80         136 foreach my $col ( 0 .. $_columns-1 ) {
366 2816 100       4003 if( !$_xflip ) {
367 2304         2589 $v >>= 1;
368 2304 100       3575 $v |= 0x80 if $_display[$row][$col];
369             }
370             else {
371 512         637 $v <<= 1;
372 512 100       881 $v |= 1 if $_display[$row][$col];
373             }
374              
375 2816 100       4853 $data .= chr( $v ), $v = 0 if $col % 8 == 7;
376             }
377              
378             # Data should be written final chip first in normal circumstances
379 80 100       168 $data = reverse $data unless $_xflip;
380              
381 80         159 $digits[$row % 8] .= $data;
382             }
383              
384 9         22 foreach my $digit ( 0 .. 7 ) {
385 72         23148 await $self->_write_raw( $_yflip ? $digit : ( 7 - $digit ), $digits[$digit] );
386             }
387              
388 9         3231 await $self->_all_writereg( REG_SHUTDOWN, 1 );
389 9         5716 $_is_display_dirty = 0;
390 9     9 1 36 }
391              
392             =head1 AUTHOR
393              
394             Paul Evans
395              
396             =cut
397              
398             0x55AA;