I have been thinking about this for a while and finally decided to see what it would take. After a couple of days I got it working. I would not recommend it unless you are desperate. Driving the max7219 via the pcf8574 is about 1/60 the speed of using digitalWrite() directly. It takes about 325 ms to write a 4 character display. Here is the code if you want to try it.
Create a folder in your sketchbook (wherever it might be) and add the following code.
===== this set in a file called LedI2C.h ======
/******************************************
*
* LedI2C - A class to implement a class for pcf8574 driving a max7219
*
*
************************************************/
#ifndef LED_I2C_h
#define LED_I2C_h
#include <stdint.h>
#include "Arduino.h"
#include <pcf8574.h>
// define MAX7219 registers
const uint8_t REG_NO_OP = 0x00;
const uint8_t REG_GRP_BASE = 0x01; // top row of the matrix
const uint8_t REG_CURSOR = 0x08; // use bottom row for cursor
const uint8_t REG_DECODE_MODE = 0x09;
const uint8_t REG_INTENSITY = 0x0A;
const uint8_t REG_SCAN_LIMIT = 0x0B;
const uint8_t REG_SHUTDOWN = 0x0C;
const uint8_t REG_TEST = 0x0F;
enum DisplayType {
MATRIX_T,
SEGMENT_T
};
struct Command {
uint8_t opCode;
uint8_t data;
};
class LedI2C {
public:
LedI2C(){}; // constructor
void begin(PCF8574* expand,uint8_t clkPin,uint8_t dataPin,uint8_t loadPin,uint8_t numDevices,DisplayType dt=MATRIX_T);
void setChar(uint8_t device,uint8_t character ); // Char to display
void setCursor(uint8_t device,bool state=true ); // Char to set cursor
void set7Seg(uint8_t digit,uint8_t value,bool dp=false ); // 7 segment digit to disolay
void clearDisplay();
void setIntensity(uint8_t intensity); // 0 - 15 low-high
void shutDown(bool mode); // true - shutdown, false - normal operation
void ledTest(bool mode); // true - turn on all leds, false - normal operation
uint8_t getDeviceCount(); // return max device count
private:
void setDecode(); // set decode mode on
void setScanAll();
/* Send out commands to the devices */
void spiTransfer(uint8_t numCommands);
/* Send out a single command to a device */
void spiTransferDevice(uint8_t addr, uint8_t opcode, uint8_t data);
/* Send out a command to all the devices */
void spiTransferAll( uint8_t opcode, uint8_t data);
// object data
uint8_t clk_Pin;
uint8_t data_Pin;
uint8_t load_Pin;
uint8_t num_Devices;
DisplayType dspType;
Command spidata[8]; // The array for shifting the data to the devices, max 8 devices
PCF8574* ex; // reference to pcf8574 that we created at the specified address
/*******************************************
*
* Minimize chances for user to screw up - if you think
* you need these think again. If you are sure, write the constructors
* rather than relying on the default. Also write the destructor.
*
* If left to its own devices the compiler will generate default
* copy and assignment constructors which do a bit wise copy of
* the object. The default destructor simply releases the storage.
* This may not be correct depending on the complexity of the object.
*
* In the case of limited resource processors like 8 bit Arduinos
* this is usually a waste of time and storage. I discourage the easily accidental
* use of these constructors by causing an error.
*
* Search on "C++ big three rule" for more information
*
* **********************************************/
LedI2C (const LedI2C& a); // disallow copy constructor
LedI2C & operator=(const LedI2C& a); // disallow assignment operator
}; // end LedI2C class definition
#endif
============ end LedI2c.h ======
=========== this set in a file called LedI2C.cpp ====
#include <Streaming.h> // for debug
/******************************************
*
* LedI2C - A class to implement the MAX7219 using a pcf8574 interface
*
* The data sheet is here - you may want to follow along as you look at the code
* https://datasheets.maximintegrated.com/en/ds/MAX7219-MAX7221.pdf
*
*
* Note:
* In the comments in begin() and spiTransfer() you can see the
* changes if you want to use digitalWrite() directly to
* the pins.
**********************************************/
// using program memeory will conserve ram - important on
// 8 bit AVR processors, on esp processors not a big deal
#define USE_PGM_MEM
#include "LedI2C.h"
#ifdef USE_PGM_MEM
const static uint8_t charTable [96*7] PROGMEM = {
#else
const static uint8_t charTable [96*7] = {
#endif
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 20 32
0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, // 21 33 !
0x28, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, // 22 34 "
0x28, 0x7C, 0x28, 0x28, 0x7C, 0x28, 0x00, // 23 35 #
0x38, 0x54, 0x30, 0x18, 0x54, 0x38, 0x00, // 24 36 $
0x44, 0x4C, 0x18, 0x30, 0x64, 0x44, 0x00, // 25 37 %
0x20, 0x50, 0x20, 0x54, 0x48, 0x34, 0x00, // 26 38 &
0x08, 0x10, 0x20, 0x00, 0x00, 0x00, 0x00, // 27 39 '
0x08, 0x10, 0x10, 0x10, 0x10, 0x08, 0x00, // 28 40 (
0x20, 0x10, 0x10, 0x10, 0x10, 0x20, 0x00, // 29 41 )
0x44, 0x28, 0x7C, 0x28, 0x44, 0x00, 0x00, // 2A 42 *
0x10, 0x10, 0x7C, 0x10, 0x10, 0x00, 0x00, // 2B 43 +
0x00, 0x00, 0x00, 0x00, 0x30, 0x10, 0x20, // 2C 44 ,
0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, // 2D 45 -
0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, // 2E 46 .
0x04, 0x08, 0x10, 0x20, 0x40, 0x00, 0x00, // 2F 47 /
0x38, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, // 30 48 0
0x10, 0x30, 0x10, 0x10, 0x10, 0x38, 0x00, // 31 49 1
0x38, 0x44, 0x08, 0x10, 0x20, 0x7C, 0x00, // 32 50 2
0x38, 0x44, 0x18, 0x04, 0x44, 0x38, 0x00, // 33 51 3
0x08, 0x18, 0x28, 0x48, 0x7C, 0x08, 0x00, // 34 52 4
0x78, 0x40, 0x78, 0x04, 0x44, 0x38, 0x00, // 35 53 5
0x38, 0x40, 0x78, 0x44, 0x44, 0x38, 0x00, // 36 54 6
0x7C, 0x04, 0x08, 0x10, 0x20, 0x20, 0x00, // 37 55 7
0x38, 0x44, 0x38, 0x44, 0x44, 0x38, 0x00, // 38 56 8
0x38, 0x44, 0x44, 0x3C, 0x04, 0x78, 0x00, // 39 57 9
0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, // 3A 58 :
0x00, 0x30, 0x30, 0x00, 0x30, 0x10, 0x20, // 3B 59 ;
0x00, 0x10, 0x20, 0x40, 0x20, 0x10, 0x00, // 3C 60 <
0x00, 0x00, 0x7C, 0x00, 0x7C, 0x00, 0x00, // 3D 61 =
0x00, 0x10, 0x08, 0x04, 0x08, 0x10, 0x00, // 3E 62 >
0x38, 0x44, 0x08, 0x10, 0x00, 0x10, 0x00, // 3F 63 ?
0x38, 0x44, 0x54, 0x58, 0x40, 0x3C, 0x00, // 40 64 @
0x38, 0x44, 0x44, 0x7C, 0x44, 0x44, 0x00, // 41 65 A
0x78, 0x44, 0x78, 0x44, 0x44, 0x78, 0x00, // 42 66 B
0x38, 0x44, 0x40, 0x40, 0x44, 0x38, 0x00, // 43 67 C
0x78, 0x44, 0x44, 0x44, 0x44, 0x78, 0x00, // 44 68 D
0x7C, 0x40, 0x78, 0x40, 0x40, 0x7C, 0x00, // 45 69 E
0x7C, 0x40, 0x78, 0x40, 0x40, 0x40, 0x00, // 46 70 F
0x38, 0x44, 0x40, 0x4C, 0x44, 0x38, 0x00, // 47 71 G
0x44, 0x44, 0x7C, 0x44, 0x44, 0x44, 0x00, // 48 72 H
0x38, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, // 49 73 I
0x04, 0x04, 0x04, 0x04, 0x44, 0x38, 0x00, // 4A 74 J
0x44, 0x48, 0x50, 0x70, 0x48, 0x44, 0x00, // 4B 75 K
0x40, 0x40, 0x40, 0x40, 0x40, 0x7C, 0x00, // 4C 76 L
0x44, 0x6C, 0x54, 0x44, 0x44, 0x44, 0x00, // 4D 77 M
0x44, 0x64, 0x54, 0x54, 0x4C, 0x44, 0x00, // 4E 78 N
0x7C, 0x44, 0x44, 0x44, 0x44, 0x7C, 0x00, // 4F 79 O
0x78, 0x44, 0x44, 0x78, 0x40, 0x40, 0x00, // 50 80 P
0x38, 0x44, 0x44, 0x54, 0x4C, 0x3C, 0x00, // 51 81 Q
0x78, 0x44, 0x44, 0x78, 0x48, 0x44, 0x00, // 52 82 R
0x38, 0x44, 0x30, 0x08, 0x44, 0x38, 0x00, // 53 83 S
0x7C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, // 54 84 T
0x44, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, // 55 85 U
0x44, 0x44, 0x44, 0x44, 0x28, 0x10, 0x00, // 56 86 V
0x44, 0x44, 0x44, 0x54, 0x54, 0x28, 0x00, // 57 87 W
0x44, 0x28, 0x10, 0x10, 0x28, 0x44, 0x00, // 58 88 X
0x44, 0x44, 0x28, 0x10, 0x10, 0x10, 0x00, // 59 89 Y
0x7C, 0x08, 0x10, 0x20, 0x40, 0x7C, 0x00, // 5A 90 Z
0x38, 0x20, 0x20, 0x20, 0x20, 0x38, 0x00, // 5B 91 [
0x00, 0x40, 0x20, 0x10, 0x08, 0x04, 0x00, // 5C 92 '\'
0x38, 0x08, 0x08, 0x08, 0x08, 0x38, 0x00, // 5D 93 ]
0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, // 5E 94 ^
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, // 5F 95 _
0x20, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, // 60 96 `
0x00, 0x00, 0x38, 0x48, 0x48, 0x3C, 0x00, // 61 97 a
0x20, 0x20, 0x38, 0x24, 0x24, 0x38, 0x00, // 62 98 b
0x00, 0x00, 0x1C, 0x20, 0x20, 0x1C, 0x00, // 63 99 c
0x04, 0x04, 0x1C, 0x24, 0x24, 0x1C, 0x00, // 64 100 d
0x00, 0x00, 0x1C, 0x28, 0x30, 0x1C, 0x00, // 65 101 e
0x0C, 0x10, 0x38, 0x10, 0x10, 0x10, 0x00, // 66 102 f
0x00, 0x00, 0x1C, 0x24, 0x1C, 0x04, 0x38, // 67 103 g
0x20, 0x20, 0x38, 0x24, 0x24, 0x24, 0x00, // 68 104 h
0x10, 0x00, 0x30, 0x10, 0x10, 0x38, 0x00, // 69 105 i
0x08, 0x00, 0x08, 0x08, 0x08, 0x48, 0x30, // 6A 106 j
0x20, 0x20, 0x24, 0x38, 0x28, 0x24, 0x00, // 6B 107 k
0x30, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, // 6C 108 l
0x00, 0x00, 0x78, 0x54, 0x54, 0x54, 0x00, // 6D 109 m
0x00, 0x00, 0x38, 0x24, 0x24, 0x24, 0x00, // 6E 110 n
0x00, 0x00, 0x18, 0x24, 0x24, 0x18, 0x00, // 6F 111 o
0x00, 0x00, 0x38, 0x24, 0x38, 0x20, 0x20, // 70 112 p
0x00, 0x00, 0x1C, 0x24, 0x1C, 0x04, 0x04, // 71 113 q
0x00, 0x00, 0x28, 0x34, 0x20, 0x20, 0x00, // 72 114 r
0x00, 0x00, 0x1C, 0x30, 0x0C, 0x38, 0x00, // 73 115 s
0x10, 0x10, 0x38, 0x10, 0x10, 0x0C, 0x00, // 74 116 t
0x00, 0x00, 0x24, 0x24, 0x24, 0x1C, 0x00, // 75 117 u
0x00, 0x00, 0x44, 0x28, 0x28, 0x10, 0x00, // 76 118 v
0x00, 0x00, 0x44, 0x54, 0x54, 0x28, 0x00, // 77 119 w
0x00, 0x00, 0x24, 0x18, 0x18, 0x24, 0x00, // 78 120 x
0x00, 0x00, 0x24, 0x24, 0x1C, 0x04, 0x38, // 79 121 y
0x00, 0x00, 0x3C, 0x08, 0x10, 0x3C, 0x00, // 7A 122 z
0x0C, 0x10, 0x10, 0x20, 0x10, 0x10, 0x0C, // 7B 123 {
0x10, 0x10, 0x10, 0x00, 0x10, 0x10, 0x10, // 7C 124 |
0x60, 0x10, 0x10, 0x08, 0x10, 0x10, 0x60, // 7D 125 }
0x00, 0x20, 0x54, 0x08, 0x00, 0x00, 0x00, // 7E 126 ~
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // 7F 127
};
// implementation
void LedI2C::begin(PCF8574* expand,uint8_t clkPin,uint8_t dataPin,uint8_t loadPin,
uint8_t numDevices,DisplayType dt){
clk_Pin = clkPin;
data_Pin = dataPin;
load_Pin = loadPin;
dspType = dt;
// pcf8574 doesn't have pin modes
/*
pinMode(clk_Pin,OUTPUT);
pinMode(data_Pin,OUTPUT);
pinMode(load_Pin,OUTPUT);
*/
num_Devices = numDevices;
ex = expand;
if (dspType == SEGMENT_T) {
setDecode(); // set digit decode for 7 seg device
}
setScanAll(); // use all groups
setIntensity(7); // set to half intensity
clearDisplay(); // what is says :-)
shutDown(false); // start display
// Serial << "end begin " << " num_Devices " << num_Devices <<endl;
}
void LedI2C::clearDisplay(){
// set all groups/segment off
uint8_t i;
for(i = 0; i<8;i++) {
// The blank code for for matrix display is 0
if (dspType==MATRIX_T) { // clear all rows on all devices
spiTransferAll(i+REG_GRP_BASE,(0));
} else {
// The blank code for 7 seg decode is 0x0F
// we only need to clear the individual digits
spidata[0].opCode = i+REG_GRP_BASE;
spidata[0].data = 0x0f;
spiTransfer(1); // transfer 1 command
}
}
}
void LedI2C::setIntensity(uint8_t intensity) {
spiTransferAll(REG_INTENSITY,intensity);
}
void LedI2C::shutDown(bool mode){
spiTransferAll(REG_SHUTDOWN, (mode?0:1));
}
void LedI2C::ledTest(bool mode){
spiTransferAll(REG_TEST, (mode?0:1));
}
uint8_t LedI2C::getDeviceCount(){
return num_Devices;
}
void LedI2C::setChar(uint8_t device,uint8_t character ){ // Char to display
uint8_t rowVal,i;
uint16_t charStart;
if (dspType == SEGMENT_T) {return;}; // invalid call for 7 segment type device
// the character table starts at blank, so we subtact the blank value
// to get the table entry for the character. Each character has a 7 byte
// entry for the 7 rows in the display
charStart = (character - ' ')*7; // offset to start of character data in table
for (i=0;i<7;i++){
#ifdef USE_PGM_MEM
rowVal=pgm_read_byte_near(charTable+charStart + i);
#else
rowVal= charTable[charStart + i];
#endif
spiTransferDevice(device,REG_GRP_BASE+i,rowVal);
}
}
void LedI2C::setCursor(uint8_t device,bool state ){ // where to set curcor
if (state){
spiTransferDevice(device,REG_CURSOR,0xff);
}else {
spiTransferDevice(device,REG_CURSOR,0);
}
}
void LedI2C::set7Seg(uint8_t digit,uint8_t value, bool dp ){ // number to display
if (dspType == MATRIX_T) return; // invalid call for matrix device
spidata[0].opCode = REG_GRP_BASE+digit;
if (dp) {
spidata[0].data = value|0x80; // turn on decimal point
}else {
spidata[0].data = value;
}
spiTransfer(1); // transfer 1 command
}
void LedI2C::setDecode(){
spidata[0].opCode = REG_DECODE_MODE;
spidata[0].data = 0xff; // decode all digits
spiTransfer(1); // transfer 1 command
}
void LedI2C::setScanAll(){
spiTransferAll(REG_SCAN_LIMIT, 0xff);
}
void LedI2C::spiTransferAll(uint8_t opcode, uint8_t data) {
// Send same command to all devices
for(int i=0;i<num_Devices;i++){
//put our device data into the array
spidata[i].opCode=opcode;
spidata[i].data=data;
}
spiTransfer(num_Devices);
}
void LedI2C::spiTransferDevice(uint8_t addr, uint8_t opcode, uint8_t data) {
// send command to specified device
//Create an array with the data to shift out
// Set no-ops for all devices
for(int i=0;i<num_Devices;i++){
spidata[i].opCode=REG_NO_OP;
spidata[i].data=0;
}
//put our device data into the array
spidata[addr].opCode=opcode;
spidata[addr].data=data;
spiTransfer(num_Devices);
}
void LedI2C::spiTransfer(uint8_t numCommands) {
// transfer data to device(s)
//enable the load line
digitalWrite(*ex,load_Pin,LOW);
/* digitalWrite(load_Pin,LOW); */
//Now shift out the data
for(int i= 0 ;i<numCommands;i++) {
uint8_t val;
uint8_t j;
uint8_t mask = 0x80;
val = spidata[i].opCode;
// send op code msb first
for (j = 8; j > 0; j--) {
// the ? operator is shorthand for if .. then ..else
digitalWrite(*ex, data_Pin, ( val & mask) ? HIGH:LOW); // get data bit
digitalWrite(*ex,clk_Pin, HIGH); // toggle
digitalWrite(*ex,clk_Pin, LOW); // clock
/*
digitalWrite(data_Pin, ( val & mask) ? HIGH:LOW); // get data bit
digitalWrite(clk_Pin, HIGH); // toggle
digitalWrite(clk_Pin, LOW); // clock
*/
mask >>= 1; // next msb
}
mask = 0x80;
val = spidata[i].data;
// send data msb first
for (j = 8; j > 0; j--) {
// the ? operator is shorthand for if .. then ..else
digitalWrite(*ex, data_Pin, ( val & mask) ? HIGH:LOW); // get data bit
digitalWrite(*ex,clk_Pin, HIGH); // toggle
digitalWrite(*ex,clk_Pin, LOW); // clock
/*
digitalWrite(data_Pin, ( val & mask) ? HIGH:LOW); // get data bit
digitalWrite(clk_Pin, HIGH); // toggle
digitalWrite(clk_Pin, LOW); // clock
*/
mask >>= 1; // next msb
}
}
//latch the data onto the display
digitalWrite(*ex,load_Pin,HIGH);
/* digitalWrite(load_Pin,HIGH); */
}
// end implementation
========== end LedI2C.cpp ===========
======== finally this test case as [folder name].ino =======
/*
* Demo for useing a pcf8574 as the interface to a max7219
*
* oldcurmudgeon Nov 11,2023
*
*/
#include <Streaming.h>
#include <pcf8574.h>
PCF8574 ex(0x3F);
#include "LedI2C.h"
#include <Wire.h>
const uint8_t clkPin = 4;
const uint8_t dataPin = 5;
const uint8_t matrixLoad = 6;
const uint8_t segmentLoad = 7;
LedI2C lcMatrix;
LedI2C lcSegment;
void setup() {
uint8_t i;
uint32_t startMs,endMs,totalTime;
Serial.begin(9600);
Serial.println(" in setup");
// Uncomment the following line for ESP-01
Wire.begin(0,2); // Set I2C for esp01 gpio0=data,gpio2=clock
// the begin sets up the dosplay, on return the state is
// scan all,intensity = 7,display clear,display on
// format of the begin is
// begin(PCF8574* expander,uint8_t clkPin,uint8_t dataPin,uint8_t loadPin,uint8_t numDevices,DisplayType dt=MATRIX_T)
// set up for 7 segment display
lcSegment.begin(&ex,clkPin,dataPin,segmentLoad,1,SEGMENT_T);
// set up for matrix display
lcMatrix.begin(&ex,clkPin,dataPin,matrixLoad,4); // matrix type is the default
// Run 7 seg display
for (i = 0;i<8;i++){
if (i !=4){
lcSegment.set7Seg(i,i);
} else {
lcSegment.set7Seg(i,i,true); // set decimal point on digit 4
}
}
delay(2000);
for (i = 8;i<16;i++){
lcSegment.set7Seg(i-8,i);
}
delay(2000);
// run matrix display
lcMatrix.setChar(0,'C');
lcMatrix.setChar(1,'s');
lcMatrix.setChar(2,'r');
lcMatrix.setCursor(3); // on is the default
delay(2000);
lcMatrix.setCursor(3,false); // set cursor off
delay(2000);
startMs = millis();
for (i=0;i<96;i+=4) { // display characters starting at blank, 4 at a time
lcMatrix.setChar(0,i+' '); //
lcMatrix.setChar(1,i+' '+1); //
lcMatrix.setChar(2,i+' '+2); //
lcMatrix.setChar(3,i+' '+3); //
yield(); // for esp8266 processors - pet the dog
}
endMs = millis();
totalTime = endMs -startMs;
Serial << " total ms = "<< totalTime << " ms/char= "<< totalTime/96 <<endl;
}
void loop() { }
========== end test case =========
Open the sketch in the Arduino IDE and set up the hardware to give it a try.
The test case is set up for an esp-01 driving a pcf8574 via a level shifter. Connected to the pcf8574 are a max7219 driving 8 7 segment displays and a set of 4 max7219 8x8 dot matrix displays.