Skip to content

Commit

Permalink
Common Power Row Groups
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
michaelkamprath authored Jun 6, 2019
1 parent ce32581 commit 554a0e4
Show file tree
Hide file tree
Showing 35 changed files with 778 additions and 90 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@
*.exe
*.out
*.app

# Project Specific

artwork/
test/

12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.


## [Unreleased]
## [1.2.0] - 2018-12-24

### Fixed
- Correct the code for the 4x4 matrix example

### Changed
- 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.

### Added
- 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.
- Support for sending a `blank` single to the shift registers on demand.

## [1.1.1] - 2018-12-24

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

[Unreleased]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.1.1...HEAD
[Unreleased]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/michaelkamprath/ShiftRegisterLEDMatrixLib/compare/v1.0.0...v1.0.1
37 changes: 32 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This library provides a generalized API to create and drive an image on LED matr
This driver uses SPI to transfer bits to the shift registers and uses one timer interrupt.

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

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

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

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.
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.
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.
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.
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).
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.
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).

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

An `RGBImage` can be initialized with an array of `RGBColorType` values sized to be the image's rows\*columns.
### Matrix Driver
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.
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.

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

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`.

#### Common Power Row Groups
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.

To illustrate a common power row group works, consider this following 8 row by 4 columns matrix:

![Example 8 row by 4 column matrix](extras/common-power-row-groups.png)

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:

![Example 8 row by 4 column matrix](extras/common-power-row-groups-with-transistors.png)

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:

![Example 8 row by 4 column matrix](extras/common-power-row-groups-bit-order.png)

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:

![Example 8 row by 4 column matrix](extras/common-power-row-groups-rearranged.png)

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:

![Example 8 row by 4 column matrix](extras/common-power-row-groups-rearranged-with-column-bits.png)

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.


File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
#include <LEDMatrix.h>
#include <RGBLEDMatrix.h>
#include <RGBImage.h>
#include <Glyph.h>
#include <RGBAnimation.h>
#include <RGBAnimationSequence.h>
#include <TimerAction.h>

//
// This is an toroidal array implementation of Conway's gqme of life.
//
// Colors indicate:
// green = cell is newly born
// blue = cell is alive
// red = cell is dying
// black = cell is dead
//
// A random set of cells are born on the first generration, then the game commences
// according to the standard rules.
//

class CellUniverse : public TimerAction {
public:

typedef byte LifeState;

const static LifeState ALIVE = 1;
const static LifeState BORN = 2;
const static LifeState DYING = 3;
const static LifeState DEAD = 0;
const static LifeState GAME_OVER = 4;

private:


RGBLEDMatrix& _leds;

LifeState* _cells;
LifeState* _nextCells;

bool _isReseting;

RGBColorType getColorForLifeState( LifeState state ) const;


protected:
virtual void action();

public:

CellUniverse(
RGBLEDMatrix& matrix,
unsigned long updateMicros
);

void setCellStatus(unsigned int row, unsigned int column, LifeState cellStatus);
LifeState getCellStatus(int row, int column) const;
void createRandomState();

bool isAlive(int row, int column) const;
int countAliveNeighbors(int row, int column) const;

void drawToScreen();
};

CellUniverse::CellUniverse(
RGBLEDMatrix& matrix,
unsigned long updateMicros
) : TimerAction(updateMicros),
_leds(matrix),
_cells(new LifeState[matrix.rows()*matrix.columns()]),
_nextCells(new LifeState[matrix.rows()*matrix.columns()]),
_isReseting(false)
{
memset(_cells,DEAD,matrix.rows()*matrix.columns()*sizeof(LifeState));
memset(_nextCells,DEAD,matrix.rows()*matrix.columns()*sizeof(LifeState));
}

void CellUniverse::setCellStatus(unsigned int row, unsigned int column, LifeState cellStatus) {
if (row < 0 || row >= _leds.rows() || column < 0 || column >= _leds.columns()) {
return;
}

unsigned int idx = row*_leds.columns() + column;

_cells[idx] = cellStatus;
}

CellUniverse::LifeState CellUniverse::getCellStatus(int row, int column) const {
// this causes the matrix to be a toroidal array
unsigned int r = row < 0 ? row + _leds.rows() : ( row >= _leds.rows() ? row - _leds.rows() : row );
unsigned int c = column < 0 ? column + _leds.columns() : ( column >= _leds.columns() ? column - _leds.columns() : column );

// double check just to be sure
if (r >= _leds.rows() || c >= _leds.columns()) {
return CellUniverse::DEAD;
}

unsigned int idx = r*_leds.columns() + c;

return _cells[idx];
}

bool CellUniverse::isAlive(int row, int column) const {
return (this->getCellStatus(row, column) == CellUniverse::ALIVE || this->getCellStatus(row, column) == CellUniverse::BORN);
}

int CellUniverse::countAliveNeighbors(int row, int column) const {
int aliveCount = 0;

for (int x = column - 1; x <= column+1; x++) {
for (int y = row - 1; y <= row + 1; y++ ) {
if (this->isAlive(y, x) && !(x == column && y == row)) {
aliveCount++;
}
}
}

return aliveCount;
}

void CellUniverse::action() {
if (_isReseting) {
delay(5000);
this->createRandomState();
_isReseting = false;
return;
}

_leds.startDrawing();
bool isSame = true;

for (unsigned int x = 0; x < _leds.columns(); x++) {
for (unsigned int y = 0; y < _leds.rows(); y++ ) {
LifeState newState = DEAD;
LifeState currentState = this->getCellStatus(y, x);
int count = this->countAliveNeighbors(y, x);

switch (currentState) {
case BORN:
case ALIVE:
if ( count < 2 || count > 3 ) {
newState = DYING;
}
else {
newState = ALIVE;
}
break;
case DYING:
case DEAD:
default:
if (count == 3) {
newState = BORN;
}
break;
}
if (currentState != newState) {
isSame = false;
}

unsigned int idx = y*_leds.columns() + x;
_nextCells[idx] = newState;

RGBColorType cellColor = this->getColorForLifeState(newState);
_leds.image().pixel(y, x) = cellColor;
}
}
_leds.stopDrawing();

// determine if life needs to start over
if (isSame) {
for (unsigned int x = 0; x < _leds.columns(); x++ ) {
for (unsigned int y = 0; y < _leds.rows(); y++ ) {
if (this->getCellStatus(y, x) == DEAD) {
this->setCellStatus(y, x, GAME_OVER);
}
}
}
this->drawToScreen();
_isReseting = true;
}
else {
memcpy(_cells, _nextCells, _leds.rows()*_leds.columns()*sizeof(LifeState));
}

}

void CellUniverse::createRandomState() {
unsigned int numTotalCells = _leds.rows()*_leds.columns();

for (unsigned int x = 0; x < _leds.columns(); x++ ) {
for (unsigned int y = 0; y < _leds.rows(); y++ ) {
this->setCellStatus(y, x, CellUniverse::DEAD);
}
}

unsigned int countStartingCells = random(0.25*numTotalCells, 0.75*numTotalCells);

for (unsigned int i = 0; i < countStartingCells; i++ ) {
int randomRow = random(0,_leds.rows());
int randomColumn = random(0,_leds.columns());

this->setCellStatus(randomRow, randomColumn, CellUniverse::BORN);
}

this->drawToScreen();
}

void CellUniverse::drawToScreen() {
_leds.startDrawing();
for (unsigned int x = 0; x < _leds.columns(); x++) {
for (unsigned int y = 0; y < _leds.rows(); y++ ) {
LifeState currentState = this->getCellStatus(y, x);
RGBColorType cellColor = this->getColorForLifeState(currentState);
_leds.image().pixel(y, x) = cellColor;
}
}
_leds.stopDrawing();
}

RGBColorType CellUniverse::getColorForLifeState( LifeState state ) const {
RGBColorType cellColor = BLACK_COLOR;
switch (state) {
case BORN:
cellColor = GREEN_COLOR;
break;
case ALIVE:
cellColor = BLUE_COLOR;
break;
case DYING:
cellColor = RED_COLOR;
break;
case GAME_OVER:
cellColor = BLACK_COLOR;
break;
case DEAD:
default:
cellColor = BLACK_COLOR;
break;
}

return cellColor;
}

//
// PROGRAM BEGINS
//

RGBLEDMatrix leds(16,16, RGBLEDMatrix::RGB_GROUPS_CPRG8, HIGH, LOW);

CellUniverse uni(leds, 500000);

void setup() {
leds.setup();
// create starting life positions
// first, pick a rando fraction between 0.25 and 0.75 of cells.
#ifdef RANDOM_REG32
randomSeed(RANDOM_REG32);
#else
randomSeed(analogRead(0));
#endif
int numTotalCells = leds.rows()*leds.columns();
int countStartingCells = random(0.25*numTotalCells, 0.75*numTotalCells);

for (int i = 0; i < countStartingCells; i++ ) {
int randomRow = random(0,leds.rows());
int randomColumn = random(0,leds.columns());

uni.setCellStatus(randomRow, randomColumn, CellUniverse::BORN);
}

uni.drawToScreen();
leds.startScanning();
}

void loop() {
uni.loop();
leds.loop();
}
Loading

0 comments on commit 554a0e4

Please sign in to comment.