File Coverage

blib/lib/Device/Chip/MAX7219Panel.pm
Criterion Covered Total %
statement 134 138 97.1
branch 15 18 83.3
condition n/a
subroutine 23 25 92.0
pod 14 17 82.3
total 186 198 93.9


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