Skip to content

Commit 554a0e4

Browse files
Common Power Row Groups
Added support for matrix electrical designs that power multiple rows simultaneously. This effectively implements a scan rate and enables larger matrices to be made with risking visible blinking due to a large number of rows that need to be scanned through.
1 parent ce32581 commit 554a0e4

35 files changed

+778
-90
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,9 @@
2727
*.exe
2828
*.out
2929
*.app
30+
31+
# Project Specific
32+
33+
artwork/
34+
test/
35+

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
66

77

88
## [Unreleased]
9+
## [1.2.0] - 2018-12-24
10+
911
### Fixed
1012
- Correct the code for the 4x4 matrix example
1113

14+
### Changed
15+
- Removed ESP8266 and 8 bit AVR microcontrollers for list officially supported for 12-bit color. This was done primarily due to realistic assessment of their computational speed.
16+
17+
### Added
18+
- Support for the "Common Power Row Groups" matrix layout outs. This layout is intended for matrices with a large number of rows, too large to effectively doe a single pass scan. With this layout, rows are grouped together into row groups and corresponding rows between each group are powered together through the same switching transistor. For example, in a 16 row matrix that is split up into groups of 8, rows 1 and 9 are powered together, and so on. As a result of row groups each having a row powered simultaneously, the columns are independently controlled in each row group. For now, the only layout supported with this scheme are ones with arow groups of size 8 and columns within each group use the RGB grouping bit layout.
19+
- Support for sending a `blank` single to the shift registers on demand.
20+
1221
## [1.1.1] - 2018-12-24
1322

1423
### Changed
@@ -44,7 +53,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
4453
## 1.0.0 - 2017-12-24
4554
Initial release
4655

47-
[Unreleased]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.1.1...HEAD
56+
[Unreleased]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.2.0...HEAD
57+
[1.2.0]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.1.1...v1.2.0
4858
[1.1.1]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.1.0...v1.1.1
4959
[1.1.0]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.0.1...v1.1.0
5060
[1.0.1]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.0.0...v1.0.1

README.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This library provides a generalized API to create and drive an image on LED matr
55
This driver uses SPI to transfer bits to the shift registers and uses one timer interrupt.
66

77
Find at more about this library and hardware that it is designed for at:
8-
[www.kamprath.net/led-matrix/](http://www.kamprath.net/led-matrix/)
8+
[www.kamprath.net/hacks/led-matrix/](https://kamprath.net/hacks/led-matrix/)
99

1010
# Design and Usage
1111
## Hardware Design
@@ -41,10 +41,10 @@ In this common anode set up, the rows would be "on" when the proper 74HC595 pin
4141

4242
Other similar designs can be used with this library. Common variations would be:
4343

44-
1. Using a DM13A sink driver to drive the cathode columns. It is not recommended to us a DM13A to drive the rows for common cathode RGB LEDs due to high current needs to drive the multiple LEDs in a single row. Using DM13A chips for the columns are nice because you can forgo the current limiting resistor for each column and the DM13A does the job of limiting the current.
44+
1. Using a DM13A sink driver to drive the cathode columns. It is not recommended to use a DM13A to drive the rows for common cathode RGB LEDs due to high current needs to drive the multiple LEDs in a single row. Using DM13A chips for the columns is nice because you can forgo the current limiting resistor for each column and the DM13A does the job of limiting the current.
4545
2. Using common cathode RGB LEDs. In this case NPN transistors would be used to sink the current for a row, and columns are sourced with the current of the high state on a 74HC595 pin.
46-
3. When using acommon anode RGB LEDs, you could use a source driver, such as a UDN2981, to drive a row. This would be turned on with a `high` state on the row's shift register pin.
47-
4. Rather than ordering the column biots as alternating through R, G, and B colors, each color can be grouped together. This is convenient when using manufactored LED matrix modules that group the pins by colors rather than by columns. See [Bit Layouts](#bit-layouts).
46+
3. When using a common anode RGB LEDs, you could use a source driver, such as a UDN2981, to drive a row. This would be turned on with a `high` state on the row's shift register pin.
47+
4. Rather than ordering the column bits as alternating through R, G, and B colors, each color can be grouped together. This is convenient when using manufactured LED matrix modules that group the pins by colors rather than by columns. See [Bit Layouts](#bit-layouts).
4848

4949
## Library Architecture
5050
This library has three general facets: image handling, matrix driver, and animation management.
@@ -88,7 +88,7 @@ Bits 0 4
8888

8989
An `RGBImage` can be initialized with an array of `RGBColorType` values sized to be the image's rows\*columns.
9090
### Matrix Driver
91-
The matrix driver is an object that manages rendering an image on an LED matrix. It does this using a double buffer approach. The first buffer is the image that is desired to be rendered on the LED matrix. The second buffer is the bit sequences that needs to be sent to the LED matrix's shift registers to render the image. The matrix drive object uses SPI to send the bits to the shift register. Since the rows on the matrix are multiplexed when rendering, the matrix driver object will use a system clock interrupt to ensure the multiplexing is consistently timed.
91+
The matrix driver is an object that manages rendering an image on an LED matrix. It does this using a double buffer approach. The first buffer is the image that is desired to be rendered on the LED matrix. The second buffer is the bit sequences that needs to be sent to the LED matrix's shift registers to render the image. The matrix driver object uses SPI to send the bits to the shift register. Since the rows on the matrix are multiplexed when rendering, the matrix driver object will use a system clock interrupt to ensure the multiplexing is consistently timed.
9292

9393
When constructing a matrix driver, you need to tell it a few details:
9494
* The matrix's size in rows and columns
@@ -182,3 +182,30 @@ The second supported bit layout groups all colors together in column order, then
182182
![Default Bit Layout for RGB LED Matrix](extras/rgb-led-matrix-bit-layout-color-groups.png)
183183

184184
When constructing the the `RGBLEDMatrix` object, the third argument is optional and it take a `RGBLEDBitLayout` enum value indicating which bit layout you are using. This argument defaults to `INDIVIDUAL_LEDS`, which is the first layout described above. The other potential value is `RGB_GROUPS`.
185+
186+
#### Common Power Row Groups
187+
As matrices get large, it becomes more difficult to successfully scan through all the rows within the time needed to create gray scales and not create perceptible blinking. One way to resolve this problem is to design the large matrix with common power row groups. Common power rows groups are a circuit design mechanism for implementing [scan rates](https://www.sparkfun.com/sparkx/blog/2650) in the LED matrix. What common power row groups does is effectively deconstruct a large LED matrix into smaller sub-matrices that all share the same set of rows, and thus the same set of row power control bits and switching transistors. The advantage of this design is that scan rate is implemented in hardware and as a result simplifies that hardware by reducing the row power transistors that are needed. The challenge with this approach is transforming the matrix image's logical layout (e.g., 16x16) to its actual circuit layout (e.g. 32x8). This transformation is handled in software by this library. Another challenge is that the row power transition would need to handle higher levels of current than matrices that don't use row groups. Be sure to select the row transistor appropriately.
188+
189+
To illustrate a common power row group works, consider this following 8 row by 4 columns matrix:
190+
191+
![Example 8 row by 4 column matrix](extras/common-power-row-groups.png)
192+
193+
Using an example scan rate of 1:4, that is, every 4th row is powered at any given time, the 8 rows of the matrix can be groups into two 4-row groups. Then, from a circuit perspective, the corresponding rows in each row group would be powered by the same transistor (BJT or MOSFET), like this:
194+
195+
![Example 8 row by 4 column matrix](extras/common-power-row-groups-with-transistors.png)
196+
197+
Note that by setting up the circuit this way, the columns in each row group are now independent columns. So effectively, the example 4-column matrix has an actual circuit with 8 columns. When wiring up the shift registers for these row groups, the columns of the bottom most row group should be the most significant bits of the shift register layout. So the bit ordering of our example 4 column by 8 row matrix with common power row groups using a scan rate of 1:4 would look something like this:
198+
199+
![Example 8 row by 4 column matrix](extras/common-power-row-groups-bit-order.png)
200+
201+
Note that left most column of Group B is the most significant bit, and the first row's control bit is the least significant bit. With this sort of bit ordering, the common power row groups can be virtually rearrange as follows to find the equivalently wired horizontally laid out matrix:
202+
203+
![Example 8 row by 4 column matrix](extras/common-power-row-groups-rearranged.png)
204+
205+
In this way, a 4 column by 8 row matrix with common power row groups using a scan rate of 1:4 is electrically no different from an 8 column by 4 row matrix that does not use common power row groups. However, there is one important consideration here. The column bit ordering within a row group block can be any of the bit orderings that matrices without row groups could use with one nuance: the bit ordering within a row group block is independent from the other row group blocks. If, for example, matrix uses a `RGB_GROUPS` column bit ordering, the columns of one row group block would be sequenced separately from the other blocks. To illustrate this, this is how the column control bits would be ordered if our example matrix was using `RGB_GROUPS` for the column bit ordering:
206+
207+
![Example 8 row by 4 column matrix](extras/common-power-row-groups-rearranged-with-column-bits.png)
208+
209+
When using this library in conjunction with a matrix that is set up to use common power row groups, you declare your matrix size according to how the image is laid out out. In this example's case, that would be the 8 row by 4 column arrangement. This library will take care of rearranging the bits to the equivalent horizontally laid out matrix.
210+
211+
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
#include <LEDMatrix.h>
2+
#include <RGBLEDMatrix.h>
3+
#include <RGBImage.h>
4+
#include <Glyph.h>
5+
#include <RGBAnimation.h>
6+
#include <RGBAnimationSequence.h>
7+
#include <TimerAction.h>
8+
9+
//
10+
// This is an toroidal array implementation of Conway's gqme of life.
11+
//
12+
// Colors indicate:
13+
// green = cell is newly born
14+
// blue = cell is alive
15+
// red = cell is dying
16+
// black = cell is dead
17+
//
18+
// A random set of cells are born on the first generration, then the game commences
19+
// according to the standard rules.
20+
//
21+
22+
class CellUniverse : public TimerAction {
23+
public:
24+
25+
typedef byte LifeState;
26+
27+
const static LifeState ALIVE = 1;
28+
const static LifeState BORN = 2;
29+
const static LifeState DYING = 3;
30+
const static LifeState DEAD = 0;
31+
const static LifeState GAME_OVER = 4;
32+
33+
private:
34+
35+
36+
RGBLEDMatrix& _leds;
37+
38+
LifeState* _cells;
39+
LifeState* _nextCells;
40+
41+
bool _isReseting;
42+
43+
RGBColorType getColorForLifeState( LifeState state ) const;
44+
45+
46+
protected:
47+
virtual void action();
48+
49+
public:
50+
51+
CellUniverse(
52+
RGBLEDMatrix& matrix,
53+
unsigned long updateMicros
54+
);
55+
56+
void setCellStatus(unsigned int row, unsigned int column, LifeState cellStatus);
57+
LifeState getCellStatus(int row, int column) const;
58+
void createRandomState();
59+
60+
bool isAlive(int row, int column) const;
61+
int countAliveNeighbors(int row, int column) const;
62+
63+
void drawToScreen();
64+
};
65+
66+
CellUniverse::CellUniverse(
67+
RGBLEDMatrix& matrix,
68+
unsigned long updateMicros
69+
) : TimerAction(updateMicros),
70+
_leds(matrix),
71+
_cells(new LifeState[matrix.rows()*matrix.columns()]),
72+
_nextCells(new LifeState[matrix.rows()*matrix.columns()]),
73+
_isReseting(false)
74+
{
75+
memset(_cells,DEAD,matrix.rows()*matrix.columns()*sizeof(LifeState));
76+
memset(_nextCells,DEAD,matrix.rows()*matrix.columns()*sizeof(LifeState));
77+
}
78+
79+
void CellUniverse::setCellStatus(unsigned int row, unsigned int column, LifeState cellStatus) {
80+
if (row < 0 || row >= _leds.rows() || column < 0 || column >= _leds.columns()) {
81+
return;
82+
}
83+
84+
unsigned int idx = row*_leds.columns() + column;
85+
86+
_cells[idx] = cellStatus;
87+
}
88+
89+
CellUniverse::LifeState CellUniverse::getCellStatus(int row, int column) const {
90+
// this causes the matrix to be a toroidal array
91+
unsigned int r = row < 0 ? row + _leds.rows() : ( row >= _leds.rows() ? row - _leds.rows() : row );
92+
unsigned int c = column < 0 ? column + _leds.columns() : ( column >= _leds.columns() ? column - _leds.columns() : column );
93+
94+
// double check just to be sure
95+
if (r >= _leds.rows() || c >= _leds.columns()) {
96+
return CellUniverse::DEAD;
97+
}
98+
99+
unsigned int idx = r*_leds.columns() + c;
100+
101+
return _cells[idx];
102+
}
103+
104+
bool CellUniverse::isAlive(int row, int column) const {
105+
return (this->getCellStatus(row, column) == CellUniverse::ALIVE || this->getCellStatus(row, column) == CellUniverse::BORN);
106+
}
107+
108+
int CellUniverse::countAliveNeighbors(int row, int column) const {
109+
int aliveCount = 0;
110+
111+
for (int x = column - 1; x <= column+1; x++) {
112+
for (int y = row - 1; y <= row + 1; y++ ) {
113+
if (this->isAlive(y, x) && !(x == column && y == row)) {
114+
aliveCount++;
115+
}
116+
}
117+
}
118+
119+
return aliveCount;
120+
}
121+
122+
void CellUniverse::action() {
123+
if (_isReseting) {
124+
delay(5000);
125+
this->createRandomState();
126+
_isReseting = false;
127+
return;
128+
}
129+
130+
_leds.startDrawing();
131+
bool isSame = true;
132+
133+
for (unsigned int x = 0; x < _leds.columns(); x++) {
134+
for (unsigned int y = 0; y < _leds.rows(); y++ ) {
135+
LifeState newState = DEAD;
136+
LifeState currentState = this->getCellStatus(y, x);
137+
int count = this->countAliveNeighbors(y, x);
138+
139+
switch (currentState) {
140+
case BORN:
141+
case ALIVE:
142+
if ( count < 2 || count > 3 ) {
143+
newState = DYING;
144+
}
145+
else {
146+
newState = ALIVE;
147+
}
148+
break;
149+
case DYING:
150+
case DEAD:
151+
default:
152+
if (count == 3) {
153+
newState = BORN;
154+
}
155+
break;
156+
}
157+
if (currentState != newState) {
158+
isSame = false;
159+
}
160+
161+
unsigned int idx = y*_leds.columns() + x;
162+
_nextCells[idx] = newState;
163+
164+
RGBColorType cellColor = this->getColorForLifeState(newState);
165+
_leds.image().pixel(y, x) = cellColor;
166+
}
167+
}
168+
_leds.stopDrawing();
169+
170+
// determine if life needs to start over
171+
if (isSame) {
172+
for (unsigned int x = 0; x < _leds.columns(); x++ ) {
173+
for (unsigned int y = 0; y < _leds.rows(); y++ ) {
174+
if (this->getCellStatus(y, x) == DEAD) {
175+
this->setCellStatus(y, x, GAME_OVER);
176+
}
177+
}
178+
}
179+
this->drawToScreen();
180+
_isReseting = true;
181+
}
182+
else {
183+
memcpy(_cells, _nextCells, _leds.rows()*_leds.columns()*sizeof(LifeState));
184+
}
185+
186+
}
187+
188+
void CellUniverse::createRandomState() {
189+
unsigned int numTotalCells = _leds.rows()*_leds.columns();
190+
191+
for (unsigned int x = 0; x < _leds.columns(); x++ ) {
192+
for (unsigned int y = 0; y < _leds.rows(); y++ ) {
193+
this->setCellStatus(y, x, CellUniverse::DEAD);
194+
}
195+
}
196+
197+
unsigned int countStartingCells = random(0.25*numTotalCells, 0.75*numTotalCells);
198+
199+
for (unsigned int i = 0; i < countStartingCells; i++ ) {
200+
int randomRow = random(0,_leds.rows());
201+
int randomColumn = random(0,_leds.columns());
202+
203+
this->setCellStatus(randomRow, randomColumn, CellUniverse::BORN);
204+
}
205+
206+
this->drawToScreen();
207+
}
208+
209+
void CellUniverse::drawToScreen() {
210+
_leds.startDrawing();
211+
for (unsigned int x = 0; x < _leds.columns(); x++) {
212+
for (unsigned int y = 0; y < _leds.rows(); y++ ) {
213+
LifeState currentState = this->getCellStatus(y, x);
214+
RGBColorType cellColor = this->getColorForLifeState(currentState);
215+
_leds.image().pixel(y, x) = cellColor;
216+
}
217+
}
218+
_leds.stopDrawing();
219+
}
220+
221+
RGBColorType CellUniverse::getColorForLifeState( LifeState state ) const {
222+
RGBColorType cellColor = BLACK_COLOR;
223+
switch (state) {
224+
case BORN:
225+
cellColor = GREEN_COLOR;
226+
break;
227+
case ALIVE:
228+
cellColor = BLUE_COLOR;
229+
break;
230+
case DYING:
231+
cellColor = RED_COLOR;
232+
break;
233+
case GAME_OVER:
234+
cellColor = BLACK_COLOR;
235+
break;
236+
case DEAD:
237+
default:
238+
cellColor = BLACK_COLOR;
239+
break;
240+
}
241+
242+
return cellColor;
243+
}
244+
245+
//
246+
// PROGRAM BEGINS
247+
//
248+
249+
RGBLEDMatrix leds(16,16, RGBLEDMatrix::RGB_GROUPS_CPRG8, HIGH, LOW);
250+
251+
CellUniverse uni(leds, 500000);
252+
253+
void setup() {
254+
leds.setup();
255+
// create starting life positions
256+
// first, pick a rando fraction between 0.25 and 0.75 of cells.
257+
#ifdef RANDOM_REG32
258+
randomSeed(RANDOM_REG32);
259+
#else
260+
randomSeed(analogRead(0));
261+
#endif
262+
int numTotalCells = leds.rows()*leds.columns();
263+
int countStartingCells = random(0.25*numTotalCells, 0.75*numTotalCells);
264+
265+
for (int i = 0; i < countStartingCells; i++ ) {
266+
int randomRow = random(0,leds.rows());
267+
int randomColumn = random(0,leds.columns());
268+
269+
uni.setCellStatus(randomRow, randomColumn, CellUniverse::BORN);
270+
}
271+
272+
uni.drawToScreen();
273+
leds.startScanning();
274+
}
275+
276+
void loop() {
277+
uni.loop();
278+
leds.loop();
279+
}

0 commit comments

Comments
 (0)