Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 2 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 2 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
Hand tools and fabrication machines | ||||||
| ||||||
|
Around six weeks ago, fellow Hackster Arnov Sharma published a project called PCB Hotplate Mini Edition. I was blown away that an simple PCB could reach temperatures hot enough to reflow solder paste.
A couple of months before, I had built a SMD hot plate using a standard 200W element (See SMD Reflow Hot Plate). The issue with heating up Surface Mount Devices (SMD) is that if heated or cooled too quickly, potentially damage can occur to the device. Also the flux in the solder needs time to activate and any water vapor that may of seeped in needs time to evaporate. This usually requires a soak period for around 90 seconds at 150 degrees Celsius before raising the temperature to around 220 degrees Celsius to melt the solder paste . Thus manufacturers of these components provide heat profiles which you can sometimes obtain from their respective datasheet.
So I wanted to take Arnov's concept and incorporate heating profiles and show them on a small screen.
The design of the electronics are based around a ATtiny1614 microprocessor and a 0.96in OLED I2C display.
The heating element is actually the spiral track on the top and bottom of the PCB. It can draw up to 4.6 Amps and can reach temperatures around 225 degrees Celsius. It is switched on and off via a AO4406 MOSFET. PWM is used to control the power to the heating element.
This circuit is basically a copy of my SMD Reflow Hot Plate so it supports both a hot plate temperature sensor and a ambient temperature sensor. Ultimately I decided to ignore the ambient temperature sensor and set the cool down temperature to 50 degrees Celsius. The cool down fan connection is also present and it is optional but it can really make a difference in how quickly the plate can cool down. It is a standard 12V case fan. I used a 70mm x 70mm fan.
The unit needs a 12V 5A or higher power brick and is controlled by two buttons:
SELECT: When in STOPPED mode, it will select the heat curve to use. In PAUSED mode, it will abandon the current heat curve.
START: When in RUNNING mode, it will pause the at the current temperature in the heat curve and maintain that temperature until the START button is pressed again. In STOPPED mode, it will start the displayed heat curve from the beginning.
PCBThis is the first project that I have got a PCB commercially made. I used PCBway to make the PCB using the Gerber files attached. Unfortunately I think I must of missed the tValue layer as the final silk screen only showed the tName layer. It just makes it a bit harder to assemble. You can get the values from the image below:
Start by adding the SMD components
Add the 4 pin straight female header for the OLED display
Add the right angle high male header for the UPDI programmer.
Screw on four 10mm female-female M3 metal spaces using M3 screws.
3D print "PHP - TFT Spacers.stl". Temporarily plug in the OLED display. The spacer with the knob on top fits into one of the mounting holes. The other spacer goes next to the 1117-50 regulator. Super glue them to the PCB as shown below.
Before you plug in the OLED display, you should program the ATtiny1614 microprocessor. See the programming section.
Add the two 2 pin right angle low male header for the temperature sensors. If you wish you can leave off the ambient connector because it is redundant in this version.
Add the 3 pin right angle high male header for the fan.
Fix the temperature sensor to the middle of the heating element and hold it in place with some Kapon tape. Connect it to the Temp connector as shown below.
The fan is optional but it is recommended. Any 12V case fan will do. Print two of the "PHP - Fan Stand.stl" STL files. Rotate them 90 degrees in the X or Y axis first. Use a couple of M4 bolts to fix them to your fan.
The fan connector has 3 pins although only two are required. The third pin is just a place to connect the Tachometer wire for the three wire fans.
Software & programmingThe software makes use of the attached thermistor library by Miguel Angel Califa Urquiza.
The software can hold any number of heat curves. A plot is a series of six pairs of temperature and time values. The time is the number of seconds from the start to when that temperature should be reached. A temperature of zero will be considered the end of the heat curve and any other slots ignored.
Originally I had the tables in flash memory but because flash memory is very tight, I moved them to the static RAM (Data space).
If you add to the number of plots, don't forget to increase the NUMBER_OF_PLOTS definition and also append a reference to your new plot in the plots array.
#define TABLES_IN_DATA_SPACE
typedef struct {
int temp; //Temperature to reach
int period; //Seconds to reach temperature
} TARGET;
#define NUMBER_OF_PERIODS 6
#ifdef TABLES_IN_DATA_SPACE
const TARGET plot1[NUMBER_OF_PERIODS] = {{150,90},{150,180},{240,240},{240,260},{0,420},{0,0}};
const TARGET plot2[NUMBER_OF_PERIODS] = {{150,50},{180,140},{240,175},{240,185},{120,250},{0,350}};
const TARGET plot3[NUMBER_OF_PERIODS] = {{150,60},{200,120},{250,160},{250,190},{0,260},{0,0}};
#else
const TARGET plot1[NUMBER_OF_PERIODS] PROGMEM = {{150,90},{150,180},{240,240},{240,260},{0,420},{0,0}};
const TARGET plot2[NUMBER_OF_PERIODS] PROGMEM = {{150,50},{180,140},{240,175},{240,185},{120,250},{0,350}};
const TARGET plot3[NUMBER_OF_PERIODS] PROGMEM = {{150,60},{200,120},{250,160},{250,190},{0,260},{0,0}};
#endif
#define NUMBER_OF_PLOTS 3
const TARGET* plots[NUMBER_OF_PLOTS] = {plot1, plot2, plot3};
The other variable you may need to adjust is the MAX_PID_VALUE. If your hot plate can't reach the desired temperature, you can increase this value. It has a range of 0 to 255 and controls the maximum PWM ON period.
#define MAX_PID_VALUE 180 //Max PID value. You can change this.
Unlike the earlier ATtiny series such as the ATtiny85, the ATtiny1614 uses the RESET pin to program the CPU. To program it you need a UPDI programmer. I made one using a Arduino Nano. You can find complete build instructions at Create Your Own UPDI Programmer. It also contains the instructions for adding the megaTinyCore boards to your IDE.
Once the board has been installed in the IDE, select it from the Tools menu.
Select the ATtiny1614 board in your IDE
Select Board, chip, clock speed, COM port the Arduino Nano is connected and the programmer
The Programmer needs to be set to jtag2updi (megaTinyCore).
Open the sketch and upload it to the ATtiny1614.
ConclusionI'm still blown away by how well this works. Thanks Arnov for a great project 👍.
/**************************************************************************
SMD Hot Plate
2022-09-09 John Bradnam (jbrad2089@gmail.com)
V1: Create program for ATtiny1614
V2: Added PID code from electronoobs
Added Ambient temperature support
V3: Modified for PCB Hot Plate
--------------------------------------------------------------------------
Arduino IDE:
--------------------------------------------------------------------------
BOARD: ATtiny1614/1604/814/804/414/404/214/204
Chip: ATtiny1614
Clock Speed: 20MHz
millis()/micros(): "Enabled (default timer)"
Programmer: jtag2updi (megaTinyCore)
ATTiny1614 Pins mapped to Ardunio Pins
+--------+
VCC + 1 14 + GND
(SS) 0 PA4 + 2 13 + PA3 10 (SCK)
1 PA5 + 3 12 + PA2 9 (MISO)
(DAC) 2 PA6 + 4 11 + PA1 8 (MOSI)
3 PA7 + 5 10 + PA0 11 (UPDI)
(RXD) 4 PB3 + 6 9 + PB0 7 (SCL)
(TXD) 5 PB2 + 7 8 + PB1 6 (SDA)
+--------+
**************************************************************************/
//Comment out next line to disable heater
#define ENABLE_HEATER
//Comment out next line to disable ambient sensor
//#define ENABLE_AMBIENT
#include "SSD1306_I2C.h"
#include <thermistor.h> //http://electronoobs.com/eng_arduino_thermistor.php
#define TEMP_PIN 0 //PA4
#define AMBIENT_PIN 1 //PA5
#define HEAT_PIN 5 //PB2
#define FAN_PIN 10 //PA3
#define SWITCHES 8 //PA1
#define SPEAKER 2 //PA6
enum SWITCH {NONE, SELECT, START};
enum MODE {STOP, RUN, PAUSE};
#define EPSILON 2 //Degrees from required to current before reacting
thermistor therm(TEMP_PIN,0); //PA4 has 3950 Thermistor
thermistor amb(AMBIENT_PIN,0); //PA5 has 3950 Thermistor (optional - Can leave off)
#define STATUS_VALUE_X 106
#define STATUS_VALUE_Y 0
#define TIME_VALUE_X 106
#define TIME_VALUE_Y 10
#define TEMP_VALUE_X 106
#define TEMP_VALUE_Y 20
//Color TFT 160x128
//Mono OLED 128x64
#define GRAPH_X_MIN 16
#define GRAPH_X_DIV 9
#define GRAPH_X_GAP 12
#define GRAPH_X_TEXT 0
#define GRAPH_X_MAX (SSD1306_SCREEN_WIDTH - 1)
#define GRAPH_Y_MIN 53
#define GRAPH_Y_DIV 6
#define GRAPH_Y_GAP 8
#define GRAPH_Y_TEXT (GRAPH_Y_MIN + 3)
#define GRAPH_Y_MAX (SSD1306_SCREEN_HEIGHT - 1)
#define TABLES_IN_DATA_SPACE
typedef struct {
int temp; //Temperature to reach
int period; //Seconds to reach temperature
} TARGET;
#define NUMBER_OF_PERIODS 6
#ifdef TABLES_IN_DATA_SPACE
const TARGET plot1[NUMBER_OF_PERIODS] = {{150,90},{150,180},{240,240},{240,260},{0,420},{0,0}};
const TARGET plot2[NUMBER_OF_PERIODS] = {{150,50},{180,140},{240,175},{240,185},{120,250},{0,350}};
const TARGET plot3[NUMBER_OF_PERIODS] = {{150,60},{200,120},{250,160},{250,190},{0,260},{0,0}};
#else
const TARGET plot1[NUMBER_OF_PERIODS] PROGMEM = {{150,90},{150,180},{240,240},{240,260},{0,420},{0,0}};
const TARGET plot2[NUMBER_OF_PERIODS] PROGMEM = {{150,50},{180,140},{240,175},{240,185},{120,250},{0,350}};
const TARGET plot3[NUMBER_OF_PERIODS] PROGMEM = {{150,60},{200,120},{250,160},{250,190},{0,260},{0,0}};
#endif
#define NUMBER_OF_PLOTS 3
const TARGET* plots[NUMBER_OF_PLOTS] = {plot1, plot2, plot3};
volatile unsigned int minutes; // In minutes
volatile unsigned int seconds; // In seconds
volatile bool updateDisplay; // Force display update
int currentPlot; // Currently selected heating plot
float temperature; // Current temperature
float ambient; // Outside temperature
volatile MODE currentMode; // Current mode
char buf[16]; // Used to format strings
/////////////////////PID VARIABLES///////////////////////
#define PID_REFRESH_RATE 50
#define MIN_PID_VALUE 0
#define MAX_PID_VALUE 255 //Max PID value. You can change this. (128 3A 200C, 180 3.5A 220C, 220 4.2A 222C )
uint32_t pidTimeout = 0; //Used to hold next PID period
float Kp = 4; //Mine was 2 - How fast the system responds (too high cause overshoot)
float Ki = 0.0025; //Mine was 0.0025 - How fast the steady state error is removed
float Kd = 9; //Mine was 9 - How far into the future to predict the rate of change
float PID_Output = 0;
float PID_P, PID_I, PID_D;
float PID_ERROR, PREV_ERROR;
/////////////////////////////////////////////////////////
//---------------------------------------------------
// Hardware setup
void setup(void)
{
pinMode(TEMP_PIN, INPUT);
pinMode(AMBIENT_PIN, INPUT);
pinMode(HEAT_PIN, OUTPUT);
digitalWrite(HEAT_PIN, LOW);
pinMode(FAN_PIN, OUTPUT);
analogWrite(FAN_PIN, 0);
pinMode(SWITCHES, INPUT);
pinMode(SPEAKER, OUTPUT);
digitalWrite(SPEAKER, LOW);
initDisplay(SSD1306_SWITCHCAPVCC);
currentMode = STOP;
minutes = 0;
seconds = 0;
RTCSetup();
//Assume 10degC above current temperature is cold temperature
#ifdef ENABLE_AMBIENT
ambient = amb.analog2temp() + 10;
#else
ambient = 50.0; //Safe temperature
#endif
drawGraphFrame();
updateDisplay = true;
currentPlot = 0;
plotGraph(currentPlot);
}
//---------------------------------------------------
// Primary loop
void loop()
{
if (millis() > pidTimeout)
{
pidTimeout = millis() + PID_REFRESH_RATE;
temperature = therm.analog2temp();
if (currentMode == RUN || currentMode == PAUSE)
{
int s = minutes * 60 + seconds;
int t = timeToTemperature(currentPlot, s);
if (s != 0 && t == 0 && temperature <= ambient)
{
//Finished
digitalWrite(HEAT_PIN, LOW);
digitalWrite(FAN_PIN, LOW);
currentMode = STOP;
tone(SPEAKER, 440);
delay(2000);
noTone(SPEAKER);
}
else if (s != 0 && t == 0)
{
//switch on fan until temp drops below ambient
digitalWrite(FAN_PIN, (temperature >= ambient) ? HIGH : LOW);
}
else
{
//Calculate PID
PID_ERROR = t - temperature;
PID_P = Kp*PID_ERROR;
PID_I = PID_I+(Ki*PID_ERROR);
PID_D = Kd * (PID_ERROR-PREV_ERROR);
PID_Output = max(min(PID_P + PID_I + PID_D, MAX_PID_VALUE), MIN_PID_VALUE);
#ifdef ENABLE_HEATER
analogWrite(HEAT_PIN, PID_Output); //Change the Duty Cycle applied to the SSR
#endif
PREV_ERROR = PID_ERROR;
//switch on fan if not heating
digitalWrite(FAN_PIN, (temperature >= ambient && PID_Output == MIN_PID_VALUE) ? HIGH : LOW);
}
}
else
{
//switch on fan while pasued or stopped if temp more than ambient
digitalWrite(FAN_PIN, (temperature >= ambient) ? HIGH : LOW);
}
}
if (updateDisplay)
{
updateDisplay = false;
useLargeDigits(true);
//Status
MoveTo(STATUS_VALUE_X, STATUS_VALUE_Y);
switch(currentMode)
{
case STOP: strcpy(buf,"STOP"); break;
case RUN: strcpy(buf,"START"); break;
case PAUSE: strcpy(buf,"PAUSE"); break;
}
spadr(buf,9);
PlotText(buf);
//Temperature
temperature = floor(therm.analog2temp() + 0.5);
//temperature = float(timeToTemperature(currentPlot,minutes * 60 + seconds));
MoveTo(TEMP_VALUE_X, TEMP_VALUE_Y);
dtostrf(temperature, 3, 0, buf);
strcat(buf,"*c ");
spadr(buf,7);
PlotText(buf);
if (currentMode != STOP)
{
plotCurrentTemperature(temperature, minutes * 60 + seconds);
}
//Time
MoveTo(TIME_VALUE_X, TIME_VALUE_Y);
sprintf(buf,"%02d:%02d ",minutes,seconds);
spadr(buf,5);
PlotText(buf);
useLargeDigits(false);
refreshDisplay();
}
SWITCH sw = readSwitches(true);
if (currentMode == STOP || currentMode == PAUSE)
{
switch(sw)
{
case SELECT:
if (currentMode == PAUSE)
{
currentMode = STOP; //Stop if paused
minutes = 0;
seconds = 0;
digitalWrite(HEAT_PIN, LOW);
}
else
{
currentPlot++;
if (currentPlot == NUMBER_OF_PLOTS)
{
currentPlot = 0;
}
}
clearDisplay();
drawGraphFrame();
plotGraph(currentPlot);
updateDisplay = true;
break;
case START:
if (currentMode == STOP)
{
minutes = 0;
seconds = 0;
clearDisplay();
drawGraphFrame();
plotGraph(currentPlot);
}
currentMode = RUN;
digitalWrite(FAN_PIN, HIGH); //Run fan
updateDisplay = true;
break;
case NONE:
break;
}
}
else if (sw == START)
{
//Currently running
currentMode = PAUSE;
updateDisplay = true;
}
delay(100);
}
//---------------------------------------------------------------------
// Real-Time Clock Setup
void RTCSetup()
{
// Initialize RTC
while (RTC.STATUS > 0); // Wait until registers synchronized
//Use the internal oscillator
RTC.CLKSEL = RTC_CLKSEL_INT32K_gc; // 32.768kHz Internal Oscillator
RTC.PITINTCTRL = RTC_PI_bm; //Periodic Interrupt: enabled
RTC.PITCTRLA = RTC_PERIOD_CYC32768_gc | RTC_PITEN_bm; //RTC Clock Cycles 32768, resulting in 32.768kHz/32768 = 1Hz and enable
}
//---------------------------------------------------------------------
//RTC interrupt occurs every second
ISR(RTC_PIT_vect)
{
if (currentMode == RUN)
{
if (seconds < 59)
{
seconds++;
}
else
{
seconds = 0;
minutes++;
}
}
updateDisplay = true;
RTC.PITINTFLAGS = RTC_PI_bm; //Clear flag by writing '1'
}
//---------------------------------------------------------------------
//Read current switches state
// - wait - True to wait for button released if pressed
// - Returns NONE, START or SELECT
SWITCH readSwitches(bool wait)
{
SWITCH sw = NONE;
int value = analogRead(SWITCHES);
if (value < 1000)
{
delay(10); //debounce
if (value == analogRead(SWITCHES))
{
sw = (value < 100) ? START : SELECT;
if (wait)
{
//wait for release
while (analogRead(SWITCHES) < 1000)
{
delay(50);
}
}
}
}
return sw;
}
//---------------------------------------------------------------------
// Draw grid and labels on X and Y axis
void drawGraphFrame()
{
//Axis
//drawFastHLine(0, GRAPH_Y_MIN, GRAPH_X_MAX, SSD1306_WHITE);
MoveTo(0, GRAPH_Y_MIN); DrawTo(GRAPH_X_MAX, GRAPH_Y_MIN);
MoveTo(GRAPH_X_MIN, 0); DrawTo(GRAPH_X_MIN, GRAPH_Y_MAX);
for(int x = 0; x < GRAPH_X_DIV; x++)
{
if (x != 0)
{
//Tick mark
MoveTo(GRAPH_X_MIN + (x * GRAPH_X_GAP),GRAPH_Y_MIN); DrawTo(GRAPH_X_MIN + (x * GRAPH_X_GAP),GRAPH_Y_MIN-3);
//Text
if (x == (GRAPH_X_DIV - 1))
{
MoveTo(GRAPH_X_MIN + (x * GRAPH_X_GAP) - 6,GRAPH_Y_TEXT);
PlotText("(min)");
}
else
{
sprintf(buf,"%d",x);
MoveTo(GRAPH_X_MIN + (x * GRAPH_X_GAP) - 2,GRAPH_Y_TEXT);
PlotText(buf);
}
}
}
for(int y = 0; y < GRAPH_Y_DIV; y++)
{
if (y != 0)
{
//Tick mark
MoveTo(GRAPH_X_MIN, GRAPH_Y_MIN - (y * GRAPH_Y_GAP)); DrawTo(GRAPH_X_MIN + 3, GRAPH_Y_MIN - (y * GRAPH_Y_GAP));
//Text
MoveTo(GRAPH_X_TEXT, GRAPH_Y_MIN - (y * GRAPH_Y_GAP) - 4);
if (y > 1)
{
sprintf(buf,"%3d",y*50);
PlotText(buf);
}
}
}
//refreshDisplay();
}
//---------------------------------------------------------------------
//Plot current graph
// plot - Graph to plot
void plotGraph(int plot)
{
const TARGET* p = plots[plot];
int te, se;
int ss = 0;
int ts = 0;
for (int i = 0; i < NUMBER_OF_PERIODS; i++)
{
#ifdef TABLES_IN_DATA_SPACE
te = p->temp;
se = p->period;
#else
te = pgm_read_word(&p->temp);
se = pgm_read_word(&p->period);
#endif
if (te != 0 || se != 0 || i == 0)
{
MoveTo(toGraphX(ss),toGraphY(ts));
DrawTo(toGraphX(se),toGraphY(te));
}
ts = te;
ss = se;
p++;
}
}
//---------------------------------------------------------------------
//Convert a time in seconds to the X position on the graph
// x - time in seconds
// returns X position on graph
int toGraphX(int x)
{
return (GRAPH_X_MIN + (x * GRAPH_X_GAP) / 60);
}
//---------------------------------------------------------------------
//Convert a temperature to the Y position on the graph
// y - temperature in degrees C
// returns Y position on graph
int toGraphY(int y)
{
return (GRAPH_Y_MIN - (y * GRAPH_Y_GAP) / 50);
}
//---------------------------------------------------------------------
//Highlight the current temparture
// t - Current temperature
// s - Current time in seconds
void plotCurrentTemperature(float t, int s)
{
MoveTo(toGraphX(s),toGraphY(round(t)));
DrawCircle(2);
}
//---------------------------------------------------------------------
//Highlight the expected temparture
// plot - Graph being plotted
// s - Current time in seconds
// Returns false when reached end
bool plotExpectedTemperature(int plot, int s)
{
int t = timeToTemperature(plot, s);
MoveTo(toGraphX(s),toGraphY(t));
DrawCircle(2);
return (s == 0 || t != 0);
}
//---------------------------------------------------------------------
//Convert a time in seconds to a temperature
// plot - Graph being plotted
// s - Current time in seconds
// Returns temperature expected
int timeToTemperature(int plot, int s)
{
const TARGET* p = plots[plot];
long te, se;
long ss = 0;
long ts = 0;
for (int i = 0; i < NUMBER_OF_PERIODS; i++)
{
#ifdef TABLES_IN_DATA_SPACE
te = p->temp;
se = p->period;
#else
te = pgm_read_word(&p->temp);
se = pgm_read_word(&p->period);
#endif
if (te == 0 && se == 0)
{
return 0;
}
else if (s <= se)
{
//found target
return ts + (((long)s - ss) * (te - ts)) / (se - ss);
}
ts = te;
ss = se;
p++;
}
return 0;
}
//---------------------------------------------------------------------
//Pad string right with spaces
// - p pointer to start of string
// - l length of final string
void spadr(char* p, int l)
{
int k = strlen(p);
int i = k;
for(; i < l; i++)
{
p[i] = ' ';
}
p[i] = '\0';
}
/*
* SSD1306 OLED display library for ATtiny1614
* based on SSD1306_I2C library
* by John Bradnam
*/
#pragma once
#include <Wire.h>
#include "fontVSx8.h"
#define SSD1306_SCREEN_WIDTH 128 // OLED display width, in pixels
#define SSD1306_SCREEN_HEIGHT 64 // OLED display height, in pixels
#define SSD1306_BLACK 0 ///< Draw 'off' pixels
#define SSD1306_WHITE 1 ///< Draw 'on' pixels
#define SSD1306_INVERSE 2 ///< Invert pixels
#define SSD1306_MEMORYMODE 0x20 ///< See datasheet
#define SSD1306_COLUMNADDR 0x21 ///< See datasheet
#define SSD1306_PAGEADDR 0x22 ///< See datasheet
#define SSD1306_SETCONTRAST 0x81 ///< See datasheet
#define SSD1306_CHARGEPUMP 0x8D ///< See datasheet
#define SSD1306_SEGREMAP 0xA0 ///< See datasheet
#define SSD1306_DISPLAYALLON_RESUME 0xA4 ///< See datasheet
#define SSD1306_DISPLAYALLON 0xA5 ///< Not currently used
#define SSD1306_NORMALDISPLAY 0xA6 ///< See datasheet
#define SSD1306_INVERTDISPLAY 0xA7 ///< See datasheet
#define SSD1306_SETMULTIPLEX 0xA8 ///< See datasheet
#define SSD1306_DISPLAYOFF 0xAE ///< See datasheet
#define SSD1306_DISPLAYON 0xAF ///< See datasheet
#define SSD1306_COMSCANINC 0xC0 ///< Not currently used
#define SSD1306_COMSCANDEC 0xC8 ///< See datasheet
#define SSD1306_SETDISPLAYOFFSET 0xD3 ///< See datasheet
#define SSD1306_SETDISPLAYCLOCKDIV 0xD5 ///< See datasheet
#define SSD1306_SETPRECHARGE 0xD9 ///< See datasheet
#define SSD1306_SETCOMPINS 0xDA ///< See datasheet
#define SSD1306_SETVCOMDETECT 0xDB ///< See datasheet
#define SSD1306_SETLOWCOLUMN 0x00 ///< Not currently used
#define SSD1306_SETHIGHCOLUMN 0x10 ///< Not currently used
#define SSD1306_SETSTARTLINE 0x40 ///< See datasheet
#define SSD1306_DEACTIVATE_SCROLL 0x2E ///< Stop scroll
#define SSD1306_EXTERNALVCC 0x01 ///< External display voltage source
#define SSD1306_SWITCHCAPVCC 0x02 ///< Gen. display voltage from 3.3V
#define SSD1306_I2CADDR 0x3C
// SOME DEFINES AND STATIC VARIABLES USED INTERNALLY -----------------------
#define WIRE_MAX 32 ///< Use common Arduino core default
#define ssd1306_swap(a, b) \
(((a) ^= (b)), ((b) ^= (a)), ((a) ^= (b))) ///< No-temp-var swap operation
#define TRANSACTION_START Wire.setClock(400000UL); //Set before I2C transfer
#define TRANSACTION_END Wire.setClock(100000UL); //Restore after I2C xfer
// VARIABLES ---------------------------------------------------------------
uint8_t screenBuf[SSD1306_SCREEN_WIDTH * ((SSD1306_SCREEN_HEIGHT + 7) / 8)]; ///Buffer data used for display buffer.
int xPos; // Current plot X position
int yPos; // Current plot Y position
bool largeDigits = false;
// FUNCTIONS ---------------------------------------------------------------
void ssd1306_command1(uint8_t c);
void ssd1306_commandList(const uint8_t *c, uint8_t n);
void initDisplay(uint8_t vcs);
void drawPixel(int16_t x, int16_t y, uint16_t color);
void clearDisplay(void);
void drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color);
void drawFastHLineInternal(int16_t x, int16_t y, int16_t w, uint16_t color);
void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color);
void drawFastVLineInternal(int16_t x, int16_t __y, int16_t __h, uint16_t color);
bool getPixel(int16_t x, int16_t y);
void MoveTo(int x, int y);
void DrawTo(int x, int y);
void DrawCircle(int radius);
uint8_t ReverseByte(uint8_t x);
int PlotChar(int c, int x, int y);
void PlotText(String s);
void useLargeDigits(bool use);
void refreshDisplay(void);
// LOW-LEVEL UTILS ---------------------------------------------------------
void ssd1306_command1(uint8_t c)
{
Wire.beginTransmission(SSD1306_I2CADDR);
Wire.write((uint8_t)0x00); // Co = 0, D/C = 0
Wire.write(c);
Wire.endTransmission();
}
void ssd1306_commandList(const uint8_t *c, uint8_t n)
{
Wire.beginTransmission(SSD1306_I2CADDR);
Wire.write((uint8_t)0x00); // Co = 0, D/C = 0
uint16_t bytesOut = 1;
while (n--) {
if (bytesOut >= WIRE_MAX) {
Wire.endTransmission();
Wire.beginTransmission(SSD1306_I2CADDR);
Wire.write((uint8_t)0x00); // Co = 0, D/C = 0
bytesOut = 1;
}
Wire.write(pgm_read_byte(c++));
bytesOut++;
}
Wire.endTransmission();
}
// ALLOCATE & INIT DISPLAY -------------------------------------------------
/*!
@param vcs
VCC selection. Pass SSD1306_SWITCHCAPVCC to generate the display
voltage (step up) from the 3.3V source, or SSD1306_EXTERNALVCC
otherwise. Most situations with Adafruit SSD1306 breakouts will
want SSD1306_SWITCHCAPVCC.
@return true on successful allocation/init, false otherwise.
Well-behaved code should check the return value before
proceeding.
@note MUST call this function before any drawing or updates!
*/
void initDisplay(uint8_t vcs)
{
//Create display buffer and clear
clearDisplay();
Wire.begin();
TRANSACTION_START
// Init sequence
static const uint8_t PROGMEM init1[] = {SSD1306_DISPLAYOFF, // 0xAE
SSD1306_SETDISPLAYCLOCKDIV, // 0xD5
0x80, // the suggested ratio 0x80
SSD1306_SETMULTIPLEX}; // 0xA8
ssd1306_commandList(init1, sizeof(init1));
ssd1306_command1(SSD1306_SCREEN_HEIGHT - 1);
static const uint8_t PROGMEM init2[] = {SSD1306_SETDISPLAYOFFSET, // 0xD3
0x0, // no offset
SSD1306_SETSTARTLINE | 0x0, // 0x40 | line #0
SSD1306_CHARGEPUMP}; // 0x8D
ssd1306_commandList(init2, sizeof(init2));
ssd1306_command1((vcs == SSD1306_EXTERNALVCC) ? 0x10 : 0x14);
static const uint8_t PROGMEM init3[] = {SSD1306_MEMORYMODE, // 0x20
0x0, // 0x0 act like ks0108
SSD1306_SEGREMAP | 0x1, // 0xA0 | 0x1
SSD1306_COMSCANDEC}; // 0C8
ssd1306_commandList(init3, sizeof(init3));
// (SSD1306_SCREEN_WIDTH == 128) && (SSD1306_SCREEN_HEIGHT == 64))
ssd1306_command1(SSD1306_SETCOMPINS); // 0xDA
ssd1306_command1(0x12);
ssd1306_command1(SSD1306_SETCONTRAST); // 0x81
ssd1306_command1((vcs == SSD1306_EXTERNALVCC) ? 0x9F : 0xCF);
ssd1306_command1(SSD1306_SETPRECHARGE); // 0xD9
ssd1306_command1((vcs == SSD1306_EXTERNALVCC) ? 0x22 : 0xF1);
static const uint8_t PROGMEM init5[] = {
SSD1306_SETVCOMDETECT, // 0xDB
0x40,
SSD1306_DISPLAYALLON_RESUME, // 0xA4
SSD1306_NORMALDISPLAY, // 0xA6
SSD1306_DEACTIVATE_SCROLL, // 0x2E
SSD1306_DISPLAYON}; // 0xAF Main screen turn on
ssd1306_commandList(init5, sizeof(init5));
TRANSACTION_END
}
// DRAWING FUNCTIONS -------------------------------------------------------
/*!
@brief Set/clear/invert a single pixel. This is also invoked by the
Adafruit_GFX library in generating many higher-level graphics
primitives.
@param x
Column of display -- 0 at left to (screen width - 1) at right.
@param y
Row of display -- 0 at top to (screen height -1) at bottom.
@param color
Pixel color, one of: SSD1306_BLACK, SSD1306_WHITE or
SSD1306_INVERSE.
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void drawPixel(int16_t x, int16_t y, uint16_t color)
{
if ((x >= 0) && (x < SSD1306_SCREEN_WIDTH) && (y >= 0) && (y < SSD1306_SCREEN_HEIGHT))
{
// Pixel is in-bounds. Rotate coordinates if needed.
/*
switch (getRotation())
{
case 1:
ssd1306_swap(x, y);
x = SSD1306_SCREEN_WIDTH - x - 1;
break;
case 2:
x = SSD1306_SCREEN_WIDTH - x - 1;
y = SSD1306_SCREEN_HEIGHT - y - 1;
break;
case 3:
ssd1306_swap(x, y);
y = SSD1306_SCREEN_HEIGHT - y - 1;
break;
}
*/
switch (color)
{
case SSD1306_WHITE:
screenBuf[x + (y / 8) * SSD1306_SCREEN_WIDTH] |= (1 << (y & 7));
break;
case SSD1306_BLACK:
screenBuf[x + (y / 8) * SSD1306_SCREEN_WIDTH] &= ~(1 << (y & 7));
break;
case SSD1306_INVERSE:
screenBuf[x + (y / 8) * SSD1306_SCREEN_WIDTH] ^= (1 << (y & 7));
break;
}
}
}
/*!
@brief Clear contents of display buffer (set all pixels to off).
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void clearDisplay(void)
{
memset(&screenBuf, 0, SSD1306_SCREEN_WIDTH * ((SSD1306_SCREEN_HEIGHT + 7) / 8));
}
/*!
@brief Draw a horizontal line. This is also invoked by the Adafruit_GFX
library in generating many higher-level graphics primitives.
@param x
Leftmost column -- 0 at left to (screen width - 1) at right.
@param y
Row of display -- 0 at top to (screen height -1) at bottom.
@param w
Width of line, in pixels.
@param color
Line color, one of: SSD1306_BLACK, SSD1306_WHITE or SSD1306_INVERSE.
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color)
{
bool bSwap = false;
/*
switch (rotation)
{
case 1:
// 90 degree rotation, swap x & y for rotation, then invert x
bSwap = true;
ssd1306_swap(x, y);
x = SSD1306_SCREEN_WIDTH - x - 1;
break;
case 2:
// 180 degree rotation, invert x and y, then shift y around for height.
x = SSD1306_SCREEN_WIDTH - x - 1;
y = SSD1306_SCREEN_HEIGHT - y - 1;
x -= (w - 1);
break;
case 3:
// 270 degree rotation, swap x & y for rotation,
// then invert y and adjust y for w (not to become h)
bSwap = true;
ssd1306_swap(x, y);
y = SSD1306_SCREEN_HEIGHT - y - 1;
y -= (w - 1);
break;
}
*/
if (bSwap)
drawFastVLineInternal(x, y, w, color);
else
drawFastHLineInternal(x, y, w, color);
}
/*!
@brief Draw a horizontal line with a width and color. Used by public
methods drawFastHLine,drawFastVLine
@param x
Leftmost column -- 0 at left to (screen width - 1) at right.
@param y
Row of display -- 0 at top to (screen height -1) at bottom.
@param w
Width of line, in pixels.
@param color
Line color, one of: SSD1306_BLACK, SSD1306_WHITE or
SSD1306_INVERSE.
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void drawFastHLineInternal(int16_t x, int16_t y, int16_t w, uint16_t color)
{
if ((y >= 0) && (y < SSD1306_SCREEN_HEIGHT)) { // Y coord in bounds?
if (x < 0) { // Clip left
w += x;
x = 0;
}
if ((x + w) > SSD1306_SCREEN_WIDTH) { // Clip right
w = (SSD1306_SCREEN_WIDTH - x);
}
if (w > 0) { // Proceed only if width is positive
uint8_t *pBuf = &screenBuf[(y / 8) * SSD1306_SCREEN_WIDTH + x], mask = 1 << (y & 7);
switch (color)
{
case SSD1306_WHITE:
while (w--) {
*pBuf++ |= mask;
};
break;
case SSD1306_BLACK:
mask = ~mask;
while (w--) {
*pBuf++ &= mask;
};
break;
case SSD1306_INVERSE:
while (w--) {
*pBuf++ ^= mask;
};
break;
}
}
}
}
/*!
@brief Draw a vertical line. This is also invoked by the Adafruit_GFX
library in generating many higher-level graphics primitives.
@param x
Column of display -- 0 at left to (screen width -1) at right.
@param y
Topmost row -- 0 at top to (screen height - 1) at bottom.
@param h
Height of line, in pixels.
@param color
Line color, one of: SSD1306_BLACK, SSD1306_WHITE or SSD1306_INVERSE.
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color)
{
bool bSwap = false;
/*
switch (rotation)
{
case 1:
// 90 degree rotation, swap x & y for rotation,
// then invert x and adjust x for h (now to become w)
bSwap = true;
ssd1306_swap(x, y);
x = SSD1306_SCREEN_WIDTH - x - 1;
x -= (h - 1);
break;
case 2:
// 180 degree rotation, invert x and y, then shift y around for height.
x = SSD1306_SCREEN_WIDTH - x - 1;
y = SSD1306_SCREEN_HEIGHT - y - 1;
y -= (h - 1);
break;
case 3:
// 270 degree rotation, swap x & y for rotation, then invert y
bSwap = true;
ssd1306_swap(x, y);
y = SSD1306_SCREEN_HEIGHT - y - 1;
break;
}
*/
if (bSwap)
drawFastHLineInternal(x, y, h, color);
else
drawFastVLineInternal(x, y, h, color);
}
/*!
@brief Draw a vertical line with a width and color. Used by public method
drawFastHLine,drawFastVLine
@param x
Leftmost column -- 0 at left to (screen width - 1) at right.
@param __y
Row of display -- 0 at top to (screen height -1) at bottom.
@param __h height of the line in pixels
@param color
Line color, one of: SSD1306_BLACK, SSD1306_WHITE or
SSD1306_INVERSE.
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void drawFastVLineInternal(int16_t x, int16_t __y, int16_t __h, uint16_t color)
{
if ((x >= 0) && (x < SSD1306_SCREEN_WIDTH)) { // X coord in bounds?
if (__y < 0) { // Clip top
__h += __y;
__y = 0;
}
if ((__y + __h) > SSD1306_SCREEN_HEIGHT) { // Clip bottom
__h = (SSD1306_SCREEN_HEIGHT - __y);
}
if (__h > 0) { // Proceed only if height is now positive
// this display doesn't need ints for coordinates,
// use local byte registers for faster juggling
uint8_t y = __y, h = __h;
uint8_t *pBuf = &screenBuf[(y / 8) * SSD1306_SCREEN_WIDTH + x];
// do the first partial byte, if necessary - this requires some masking
uint8_t mod = (y & 7);
if (mod) {
// mask off the high n bits we want to set
mod = 8 - mod;
// note - lookup table results in a nearly 10% performance
// improvement in fill* functions
// uint8_t mask = ~(0xFF >> mod);
static const uint8_t PROGMEM premask[8] = {0x00, 0x80, 0xC0, 0xE0,
0xF0, 0xF8, 0xFC, 0xFE};
uint8_t mask = pgm_read_byte(&premask[mod]);
// adjust the mask if we're not going to reach the end of this byte
if (h < mod)
mask &= (0XFF >> (mod - h));
switch (color)
{
case SSD1306_WHITE:
*pBuf |= mask;
break;
case SSD1306_BLACK:
*pBuf &= ~mask;
break;
case SSD1306_INVERSE:
*pBuf ^= mask;
break;
}
pBuf += SSD1306_SCREEN_WIDTH;
}
if (h >= mod) { // More to go?
h -= mod;
// Write solid bytes while we can - effectively 8 rows at a time
if (h >= 8) {
if (color == SSD1306_INVERSE) {
// separate copy of the code so we don't impact performance of
// black/white write version with an extra comparison per loop
do {
*pBuf ^= 0xFF; // Invert byte
pBuf += SSD1306_SCREEN_WIDTH; // Advance pointer 8 rows
h -= 8; // Subtract 8 rows from height
} while (h >= 8);
} else {
// store a local value to work with
uint8_t val = (color != SSD1306_BLACK) ? 255 : 0;
do {
*pBuf = val; // Set byte
pBuf += SSD1306_SCREEN_WIDTH; // Advance pointer 8 rows
h -= 8; // Subtract 8 rows from height
} while (h >= 8);
}
}
if (h) { // Do the final partial byte, if necessary
mod = h & 7;
// this time we want to mask the low bits of the byte,
// vs the high bits we did above
// uint8_t mask = (1 << mod) - 1;
// note - lookup table results in a nearly 10% performance
// improvement in fill* functions
static const uint8_t PROGMEM postmask[8] = {0x00, 0x01, 0x03, 0x07,
0x0F, 0x1F, 0x3F, 0x7F};
uint8_t mask = pgm_read_byte(&postmask[mod]);
switch (color)
{
case SSD1306_WHITE:
*pBuf |= mask;
break;
case SSD1306_BLACK:
*pBuf &= ~mask;
break;
case SSD1306_INVERSE:
*pBuf ^= mask;
break;
}
}
}
} // endif positive height
} // endif x in bounds
}
/*!
@brief Return color of a single pixel in display buffer.
@param x
Column of display -- 0 at left to (screen width - 1) at right.
@param y
Row of display -- 0 at top to (screen height -1) at bottom.
@return true if pixel is set (usually SSD1306_WHITE, unless display invert
mode is enabled), false if clear (SSD1306_BLACK).
@note Reads from buffer contents; may not reflect current contents of
screen if display() has not been called.
*/
bool getPixel(int16_t x, int16_t y)
{
if ((x >= 0) && (x < SSD1306_SCREEN_WIDTH) && (y >= 0) && (y < SSD1306_SCREEN_HEIGHT))
{
/*
// Pixel is in-bounds. Rotate coordinates if needed.
switch (getRotation())
{
case 1:
ssd1306_swap(x, y);
x = SSD1306_SCREEN_WIDTH - x - 1;
break;
case 2:
x = SSD1306_SCREEN_WIDTH - x - 1;
y = SSD1306_SCREEN_HEIGHT - y - 1;
break;
case 3:
ssd1306_swap(x, y);
y = SSD1306_SCREEN_HEIGHT - y - 1;
break;
}
*/
return (screenBuf[x + (y / 8) * SSD1306_SCREEN_WIDTH] & (1 << (y & 7)));
}
return false; // Pixel out of bounds
}
// Move current plot position to x,y
void MoveTo(int x, int y)
{
xPos = x;
yPos = y;
}
// Draw a line to x,y
void DrawTo(int x, int y)
{
int sx, sy, e2, err;
int dx = abs(x - xPos);
int dy = abs(y - yPos);
if (xPos < x) sx = 1; else sx = -1;
if (yPos < y) sy = 1; else sy = -1;
err = dx - dy;
for (;;) {
drawPixel(xPos, yPos, SSD1306_WHITE);
if (xPos==x && yPos==y) return;
e2 = err<<1;
if (e2 > -dy) { err = err - dy; xPos = xPos + sx; }
if (e2 < dx) { err = err + dx; yPos = yPos + sy; }
}
}
void DrawCircle(int radius)
{
int x1 = xPos, y1 = yPos, dx = 1, dy = 1;
int x = radius - 1, y = 0;
int err = dx - (radius<<1);
while (x >= y) {
drawPixel(x1-x, y1+y, SSD1306_WHITE); drawPixel(x1+x, y1+y, SSD1306_WHITE);
drawPixel(x1-y, y1+x, SSD1306_WHITE); drawPixel(x1+y, y1+x, SSD1306_WHITE);
drawPixel(x1-y, y1-x, SSD1306_WHITE); drawPixel(x1+y, y1-x, SSD1306_WHITE);
drawPixel(x1-x, y1-y, SSD1306_WHITE); drawPixel(x1+x, y1-y, SSD1306_WHITE);
if (err > 0) {
x = x - 1; dx = dx + 2;
err = err - (radius<<1) + dx;
} else {
y = y + 1; err = err + dy;
dy = dy + 2;
}
}
}
uint8_t ReverseByte(uint8_t x)
{
x = ((x >> 1) & 0x55) | ((x << 1) & 0xaa);
x = ((x >> 2) & 0x33) | ((x << 2) & 0xcc);
x = ((x >> 4) & 0x0f) | ((x << 4) & 0xf0);
return x;
}
//Plot character and return width
int PlotChar(int c, int x, int y)
{
if (largeDigits && c >= 0x30 && c <= 0x39)
{
c = c + 0x26; //(0x56 - 0x30) or 'V' - '0' gives offset to big numbers
}
c = c - 32;
int cnt = pgm_read_byte(&ssd1306xled_fontVSx8[c][0]); //Get width of character
for (int8_t i = 0; i < cnt; i++) { // Char bitmap = 5 columns
uint8_t line = pgm_read_byte(&ssd1306xled_fontVSx8[c][i+1]);
for (int8_t j = 0; j < 8; j++, line >>= 1) {
drawPixel(x + i, y + j, (line & 1) ? SSD1306_WHITE : SSD1306_BLACK);
}
}
return cnt;
}
// Plot text starting at the current plot position
void PlotText(String s)
{
int p = 0;
while (1) {
char c = s[p++];
if (c == 0) return;
xPos += PlotChar(c, xPos, yPos); //Add width of character to X position
}
}
void useLargeDigits(bool use)
{
largeDigits = use;
}
// REFRESH DISPLAY ---------------------------------------------------------
/*!
@brief Push data currently in RAM to SSD1306 display.
@return None (void).
@note Drawing operations are not visible until this function is
called. Call after each graphics command, or after a whole set
of graphics commands, as best needed by one's own application.
*/
void refreshDisplay(void)
{
TRANSACTION_START
static const uint8_t PROGMEM dlist1[] = {
SSD1306_PAGEADDR,
0, // Page start address
0xFF, // Page end (not really, but works here)
SSD1306_COLUMNADDR, 0}; // Column start address
ssd1306_commandList(dlist1, sizeof(dlist1));
ssd1306_command1(SSD1306_SCREEN_WIDTH - 1); // Column end address
#if defined(ESP8266)
// ESP8266 needs a periodic yield() call to avoid watchdog reset.
// With the limited size of SSD1306 displays, and the fast bitrate
// being used (1 MHz or more), I think one yield() immediately before
// a screen write and one immediately after should cover it. But if
// not, if this becomes a problem, yields() might be added in the
// 32-byte transfer condition below.
yield();
#endif
uint16_t count = SSD1306_SCREEN_WIDTH * ((SSD1306_SCREEN_HEIGHT + 7) / 8);
uint8_t *ptr = screenBuf;
Wire.beginTransmission(SSD1306_I2CADDR);
Wire.write((uint8_t)0x40);
uint16_t bytesOut = 1;
while (count--) {
if (bytesOut >= WIRE_MAX) {
Wire.endTransmission();
Wire.beginTransmission(SSD1306_I2CADDR);
Wire.write((uint8_t)0x40);
bytesOut = 1;
}
Wire.write(*ptr++);
bytesOut++;
}
Wire.endTransmission();
TRANSACTION_END
#if defined(ESP8266)
yield();
#endif
}
/*
* SSD1306xLED - Drivers for SSD1306 controlled dot matrix OLED/PLED 128x64 displays
*
* @created: 2014-08-12
* @author: Neven Boyanov
*
* Source code available at: https://bitbucket.org/tinusaur/ssd1306xled
*
*/
// ----------------------------------------------------------------------------
#include <avr/pgmspace.h>
// ----------------------------------------------------------------------------
/* Variable ASCII 8 pixel high font */
/* first byte contains width */
const uint8_t ssd1306xled_fontVSx8[96][7] PROGMEM = {
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sp
0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00, // !
0x04, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, // "
0x00, 0x14, 0x7f, 0x14, 0x7f, 0x14, 0x00, // #
0x00, 0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x00, // $
0x00, 0x62, 0x64, 0x08, 0x13, 0x23, 0x00, // %
0x00, 0x36, 0x49, 0x55, 0x22, 0x50, 0x00, // &
0x00, 0x00, 0x05, 0x03, 0x00, 0x00, 0x00, // '
0x03, 0x0E, 0x11, 0x00, 0x00, 0x00, 0x00, // (
0x03, 0x11, 0x0E, 0x00, 0x00, 0x00, 0x00, // )
0x03, 0x02, 0x05, 0x02, 0x00, 0x00, 0x00, // *
0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, // +
0x00, 0x00, 0x00, 0xA0, 0x60, 0x00, 0x00, // ,
0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, // -
0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, // .
0x00, 0x20, 0x10, 0x08, 0x04, 0x02, 0x00, // /
0x05, 0x0E, 0x11, 0x11, 0x0E, 0x00, 0x00, // 0
0x03, 0x02, 0x1F, 0x00, 0x00, 0x00, 0x00, // 1
0x05, 0x12, 0x19, 0x15, 0x12, 0x00, 0x00, // 2
0x05, 0x0A, 0x11, 0x15, 0x0A, 0x00, 0x00, // 3
0x05, 0x0C, 0x0A, 0x1F, 0x80, 0x00, 0x00, // 4
0x04, 0x17, 0x15, 0x0D, 0x00, 0x00, 0x00, // 5
0x05, 0x0C, 0x16, 0x15, 0x08, 0x00, 0x00, // 6
0x04, 0x01, 0x1D, 0x03, 0x00, 0x00, 0x00, // 7
0x05, 0x0A, 0x15, 0x15, 0x0A, 0x00, 0x00, // 8
0x05, 0x02, 0x15, 0x0D, 0x06, 0x00, 0x00, // 9
0x02, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, // :
0x00, 0x00, 0x56, 0x36, 0x00, 0x00, 0x00, // ;
0x00, 0x08, 0x14, 0x22, 0x41, 0x00, 0x00, // <
0x00, 0x14, 0x14, 0x14, 0x14, 0x14, 0x00, // =
0x00, 0x00, 0x41, 0x22, 0x14, 0x08, 0x00, // >
0x00, 0x02, 0x01, 0x51, 0x09, 0x06, 0x00, // ?
0x00, 0x32, 0x49, 0x59, 0x51, 0x3E, 0x00, // @
0x04, 0x1E, 0x05, 0x1E, 0x00, 0x00, 0x00, // A
0x00, 0x7F, 0x49, 0x49, 0x49, 0x36, 0x00, // B
0x05, 0x1C, 0x22, 0x22, 0x14, 0x00, 0x00, // C
0x00, 0x7F, 0x41, 0x41, 0x22, 0x1C, 0x00, // D
0x04, 0x1F, 0x15, 0x15, 0x00, 0x00, 0x00, // E
0x00, 0x7F, 0x09, 0x09, 0x09, 0x01, 0x00, // F
0x00, 0x3E, 0x41, 0x49, 0x49, 0x7A, 0x00, // G
0x00, 0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00, // H
0x00, 0x00, 0x41, 0x7F, 0x41, 0x00, 0x00, // I
0x00, 0x20, 0x40, 0x41, 0x3F, 0x01, 0x00, // J
0x00, 0x7F, 0x08, 0x14, 0x22, 0x41, 0x00, // K
0x00, 0x7F, 0x40, 0x40, 0x40, 0x40, 0x00, // L
0x00, 0x7F, 0x02, 0x0C, 0x02, 0x7F, 0x00, // M
0x00, 0x7F, 0x04, 0x08, 0x10, 0x7F, 0x00, // N
0x04, 0x1F, 0x11, 0x1F, 0x00, 0x00, 0x00, // O
0x04, 0x1F, 0x05, 0x02, 0x00, 0x00, 0x00, // P
0x00, 0x3E, 0x41, 0x51, 0x21, 0x5E, 0x00, // Q
0x04, 0x1F, 0x05, 0x1A, 0x00, 0x00, 0x00, // R
0x04, 0x17, 0x15, 0x1D, 0x00, 0x00, 0x00, // S
0x04, 0x01, 0x1F, 0x01, 0x00, 0x00, 0x00, // T
0x04, 0x1F, 0x10, 0x1F, 0x00, 0x00, 0x00, // U
0x05, 0x1E, 0x21, 0x21, 0x1E, 0x00, 0x00, // V 0
0x03, 0x02, 0x3F, 0x00, 0x00, 0x00, 0x00, // W 1
0x05, 0x22, 0x31, 0x29, 0x26, 0x00, 0x00, // X 2
0x05, 0x12, 0x21, 0x25, 0x1A, 0x00, 0x00, // Y 3
0x05, 0x0C, 0x0A, 0x3F, 0x08, 0x00, 0x00, // Z 4
0x05, 0x17, 0x25, 0x25, 0x19, 0x00, 0x00, // [ 5
0x05, 0x1C, 0x26, 0x25, 0x18, 0x00, 0x00, // 6
0x05, 0x01, 0x39, 0x05, 0x03, 0x00, 0x00, // ] 7
0x05, 0x1A, 0x25, 0x25, 0x1A, 0x00, 0x00, // ^ 8
0x05, 0x06, 0x29, 0x19, 0x0E, 0x00, 0x00, // _ 9
0x00, 0x00, 0x01, 0x02, 0x04, 0x00, 0x00, // '
0x00, 0x20, 0x54, 0x54, 0x54, 0x78, 0x00, // a
0x00, 0x7F, 0x48, 0x44, 0x44, 0x38, 0x00, // b
0x05, 0x1C, 0x22, 0x22, 0x14, 0x00, 0x00, // c
0x00, 0x38, 0x44, 0x44, 0x48, 0x7F, 0x00, // d
0x00, 0x38, 0x54, 0x54, 0x54, 0x18, 0x00, // e
0x00, 0x08, 0x7E, 0x09, 0x01, 0x02, 0x00, // f
0x00, 0x18, 0xA4, 0xA4, 0xA4, 0x7C, 0x00, // g
0x00, 0x7F, 0x08, 0x04, 0x04, 0x78, 0x00, // h
0x02, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, // i
0x00, 0x40, 0x80, 0x84, 0x7D, 0x00, 0x00, // j
0x00, 0x7F, 0x10, 0x28, 0x44, 0x00, 0x00, // k
0x00, 0x00, 0x41, 0x7F, 0x40, 0x00, 0x00, // l
0x06, 0x1E, 0x02, 0x1E, 0x02, 0x1C, 0x00, // m
0x05, 0x1E, 0x02, 0x02, 0x1C, 0x00, 0x00, // n
0x00, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, // o
0x00, 0xFC, 0x24, 0x24, 0x24, 0x18, 0x00, // p
0x00, 0x18, 0x24, 0x24, 0x18, 0xFC, 0x00, // q
0x00, 0x7C, 0x08, 0x04, 0x04, 0x08, 0x00, // r
0x00, 0x48, 0x54, 0x54, 0x54, 0x20, 0x00, // s
0x00, 0x04, 0x3F, 0x44, 0x40, 0x20, 0x00, // t
0x00, 0x3C, 0x40, 0x40, 0x20, 0x7C, 0x00, // u
0x00, 0x1C, 0x20, 0x40, 0x20, 0x1C, 0x00, // v
0x00, 0x3C, 0x40, 0x30, 0x40, 0x3C, 0x00, // w
0x00, 0x44, 0x28, 0x10, 0x28, 0x44, 0x00, // x
0x00, 0x1C, 0xA0, 0xA0, 0xA0, 0x7C, 0x00, // y
0x00, 0x44, 0x64, 0x54, 0x4C, 0x44, 0x00, // z
};
// ----------------------------------------------------------------------------
Comments