Digistump Forums
The Digispark => Digispark Projects => Topic started by: postuma on January 17, 2016, 05:47:43 pm
-
I decided I needed a better bike computer for those winter months - I'd like to know what kind of power I'm cranking, and calculate calories burned based on more than speed or time on bike. Every trainer is different, and provides a different degree of resistance at different speeds ..
Fortunately, I found a power formula for my trainer on the Kurt Kinetic web site, though it seems that they've since taken these down - I guess they have newer products to sell. And while I know these numbers are only approximate and not nearly as accurate as a proper power meter, they're enough for me.
The code is a little unusual in places. For example, I could not use standard string handling functions because of the memory overhead involved, both in terms of total available and dynamic memory. Other compromises have been made. But it works.
//new IRQ version as polling code missed many wheel rotations even w 10 microsecond delay
//
//this code uses a custom truncated version of GLCDFONT.C to conserve memory on the Digispark Pro (max 14,844 bytes of program memory)
//display is in S.I. units, and maxes at 99.9 km/h. If I ever go faster than that on my trainer, I will update the code accordingly
//
//hardware: Arduino 1.77" TFT LCD Display 160x128, two reed switches (coulda used Hall effect sensors, and I might at a later date),
//two 10K pull-down resistors
#include <TFT.h> //LCD TFT screen library
#include <SPI.h>
#include <TinyPinChange.h> //pin management for interrupts
#define pedal_sensor 2 //digital pin 2 for pedal sensor
#define wheel_sensor 3 //pin 3 for wheel sensor
uint8_t virtPort0;
uint8_t virtPort1;
#define cs 9 //pin definitions for TFT screen
#define dc 6
#define rst 7
TFT TFTscreen = TFT(cs, dc, rst);
//define bike, trainer, and personal parameters ** commented out here because of memory overflow problems
//but ideally will define here so they can be adjusted for other bikes, trainers, and athletes
//should not be a problem when porting this code to the Oak
//uint16_t wheelCirc = 1934; //circumference of wheel with tire fully inflated, in mm
//float trainerVar1 = 0.02336; //calibration variables for trainer (from Kurt's Kinetic PC, formula converted to metric)
//float trainerVar2 = 3.082E-7;
//float trainerVar3 = 3.071E-9;
//byte GME = 23; //estimated Gross Metabolic Efficiency, in percent
//initialize sensors, timers
uint32_t i;
char a1[6] = { 0,0,0,0,0,0 }; //two character arrays for displaying to TFT screen; string handling i.e. dtostrf or String()
char a2[6] = { 1,1,1,1,1,0 }; //methods not used, as add > 2kb and > 3.5 kb to compiled code, respectively
uint16_t delayInterval = 300; //timing delay between code loops (with execution time of about 200 msec/loop - 2 updates/second
boolean zeroMainDisplayFlag = 0;
volatile uint16_t pedalCount = 0; //pedal counters; rolls over at 65,535 pedal revolutions - at a cadence of 60/min, after 18 hrs
volatile uint32_t pedalTime = 0; //time pedal sensor was activated, from millis()
uint16_t lastPedalCount = 0;
uint32_t lastPedalTime = 0;
uint16_t pedalLoopTime = 0; //time between pedal sensor activations; if > 10,000 msec, stop recording as wheel is paused
byte pedalLoops = 0; //number of pedal revolutions since last time through main loop
volatile uint32_t wheelCount = 0; //wheel counters
volatile uint32_t wheelTime = 0;
uint32_t lastWheelCount = 0;
uint32_t lastWheelTime = 0;
uint16_t wheelLoopTime = 2000; //first loop through wheel sensor code, before any activity, ensures BikeTime clock does not increment
byte wheelLoops = 0;
uint32_t codeLoopTime = 0; //time elapsed since last loop through code
uint32_t lastCodeLoopTime = 0; //stores prev time through loop
uint32_t bikeTime = 0; //actual cycling time
byte currTime[] = { 0,0,0 }; //currTime used to change minutes, hours displayed only when changed
uint32_t distance = 0;
uint16_t distanceInc = 0;
byte prevKm = 0;
uint16_t spd = 0;
uint16_t spdMean;
uint16_t spdMax = 0;
uint16_t cad;
uint16_t cadMean;
uint16_t cadMax = 0;
uint16_t power = 0;
uint16_t powerMean = 0;
uint32_t powerSum = 0;
uint16_t powerMax = 0;
float work = 0.0;
uint16_t prevWork = 0;
////* SETUP - use Interrupts to trigger pedal and wheel sensor code when magnetic switches activated on digital pins 2 and 3
void setup() {
TinyPinChange_Init();
virtPort0 = TinyPinChange_RegisterIsr(pedal_sensor, incPedal);
virtPort1 = TinyPinChange_RegisterIsr(wheel_sensor, incWheel);
TinyPinChange_EnablePin(pedal_sensor);
TinyPinChange_EnablePin(wheel_sensor);
// float workConvVar = 41.84 * GME; //conversion variable for caloric calculations; restore on system with higher dynamic memory
TFTscreen.begin(); //initialize LCD TFT display screen
TFTscreen.background(0,0,0); //clear the screen with a black background
TFTscreen.stroke(255,255,255); //characters white
TFTscreen.setTextSize(2);
TFTscreen.text("0",0,0);
TFTscreen.text(":",9,0);
TFTscreen.text("00",18,0);
TFTscreen.text(":",39,0);
TFTscreen.text("00",48,0);
TFTscreen.text(" 0",83,0);
TFTscreen.text("000",111,0);
TFTscreen.text(".",32,26);
TFTscreen.text("k",62,26);
TFTscreen.text("/",71,26);
TFTscreen.text("h",79,26);
TFTscreen.text("cad",55,52);
TFTscreen.text("watt",55,78);
TFTscreen.text("calories",55,104);
zeroMainDisplay();
TFTscreen.text("0",36,70);
TFTscreen.text("0",36,96);
TFTscreen.setTextSize(1);
TFTscreen.text(".",105,7);
TFTscreen.text("km",149,7);
TFTscreen.text("0.00 MEAN",107,24);
TFTscreen.text("0.00 MAX",107,34);
TFTscreen.text("0.0 MEAN",113,50);
TFTscreen.text("0.0 MAX",113,60);
TFTscreen.text("0 MEAN",125,76);
TFTscreen.text("0 MAX",125,86);
TFTscreen.stroke(150, 150, 255); //blue
TFTscreen.text("1 SETUP 2 STOP 3 SUMMARY",0,121); //not yet implemented. Waiting for the extra code space on the Oak
}
void loop() {
i = millis();
codeLoopTime = i - lastCodeLoopTime;
lastCodeLoopTime = i;
////* pedal code. Cadence updates if the wheel has turned in the last second i.e. bike is still in motion
pedalLoops = pedalCount - lastPedalCount;
if (pedalLoops > 0) { //check for activity on pedal sensor
pedalLoopTime = pedalTime - lastPedalTime; //check time since last loop through pedal sensor code
pedalLoopTime = (pedalLoopTime < 10000) ? pedalLoopTime : 10000; //and set to 10 sec if more than 10 sec has passed - assume wheel has stopped rotation
lastPedalCount = pedalCount; //update counter with current pedal count
lastPedalTime = pedalTime; //and store current time
cad = (float)600000 / pedalLoopTime; //1000 msec/sec * 60 sec/min * 10 for decimal point (cadence in revs/minute with one decimal, for Max)
TFTscreen.setTextSize(3); //update cadence display
i = cad / 10; //remove decimal point
// i = codeLoopTime; //DEBUG: display time taken by code loops instead of cadence
a1[2] = (i % 10)+'0';
i /= 10;
if (!i) {
a1[1] = ' ';
a1[0] = ' ';
}
else {
a1[1] = (i % 10)+'0';
i /= 10;
a1[0] = (i) ? (i%10)+'0' : ' ';
}
a1[3] = 0;
wrtVal(0,44);
}
TFTscreen.setTextSize(1);
i = (float)pedalCount * 600000 / bikeTime; //revs / bikeTime in millisecs * 60000 msec/min
if (cadMean != i) {
cadMean = i;
a1[4] = (i % 10)+'0';
a1[3] = '.';
i /= 10;
a1[2] = (i % 10)+'0';
i /= 10;
if (!i) {
a1[1] = ' ';
a1[0] = ' ';
}
else {
a1[1] = (i % 10)+'0';
i /= 10;
a1[0] = (i) ? (i%10)+'0' : ' ';
}
wrtVal(101,50);
}
if (cad > cadMax) {
i = cad;
cadMax = i;
a1[4] = (i % 10)+'0';
a1[3] = '.';
i /= 10;
a1[2] = (i % 10)+'0';
i /= 10;
if (!i) {
a1[1] = ' ';
a1[0] = ' ';
}
else {
a1[1] = (i % 10)+'0';
i /= 10;
a1[0] = (i) ? (i%10)+'0' : ' ';
}
wrtVal(101,60);
}
////* wheel rotation handling. Bike timer increments if wheel has turned in the last second. Update those elements depending on wheel rotation i.e. distance, speed, power, calories
wheelLoops = wheelCount - lastWheelCount;
if (wheelLoops > 0) {
wheelLoopTime = wheelTime - lastWheelTime; //check time since last loop through wheel sensor code
wheelLoopTime = (wheelLoopTime < 2000) ? wheelLoopTime : 2000; //and set to 2 sec if more than 2 sec has passed - assume wheel has stopped rotation
lastWheelTime = wheelTime;
lastWheelCount = wheelCount;
// distanceInc = wheelCirc * wheelLoops; //restore trainer variables if/when porting code to processor with more dynamic memory
distanceInc = 1934 * wheelLoops;
distance += distanceInc;
bikeTime += codeLoopTime;
updateTime();
updateMainDisplay();
zeroMainDisplayFlag = 1;
}
else if (wheelLoopTime < 2000) { //if wheel spinning has not been detected for under 2 sec, presumed to be spinning slowly
wheelLoopTime += codeLoopTime; //increment wheelLoopTime by codeLoopTime
bikeTime += codeLoopTime;
updateTime();
updateMainDisplay();
}
else if (zeroMainDisplayFlag == 1) {
zeroMainDisplay(); //if no wheel rotation more than 1 second since last sensed, zero main display - specifically speed, cadence, and power
}
delay(delayInterval);
}
////* bike time display
void updateTime() {
//update time elapsed, but only if value for hours, minutes, or seconds has changed - fewer computations, less screen flicker
TFTscreen.setTextSize(2);
i = bikeTime/1000; //convert to seconds
i = i % 60;
if ((i > currTime[2]) || ((i == 0) && (currTime[2] == 59))) {
a1[0] = (i/10)+'0';
a1[1] = (i%10)+'0';
a1[2] = 0;
wrtVal(48,0);
currTime[2] = (currTime[2] == 59) ? 0 : i;
}
i = bikeTime/60000; //calculate minutes
i = i % 60;
if ((i > currTime[1]) || ((i == 0) && (currTime[1] == 59))) {
a1[0] = (i/10)+'0';
a1[1] = (i%10)+'0';
a1[2] = 0;
wrtVal(18,0);
currTime[1] = (currTime[1] == 59) ? 0 : i;
}
i = bikeTime/3600000; //hours
if (i > currTime[0]) {
a1[0] = i+'0';
a1[1] = 0;
wrtVal(0,0);
currTime[0] = i;
}
}
////* update Main Display: speed, power, calories
void updateMainDisplay() {
TFTscreen.setTextSize(2);
//distance
i = distance /1000;
a1[2] = (i % 10)+'0'; //this code structure uses fewer bytes than for loop
i = i/10;
a1[1] = (i %10)+'0';
i = i/10;
a1[0] = (i %10)+'0';
a1[3] = 0;
wrtVal(111,0);
i = i/10;
if (prevKm != i) {
prevKm = i;
a1[1] = (i %10)+'0';
i = i/10;
a1[0] = (i) ? i+'0' : ' ';
a1[2] = 0;
wrtVal(83,0);
}
//speed, power - update if speed has changed
i = distanceInc * 360.0 / wheelLoopTime; //distanceInc in mm / 10,000 for speed in decametres * 3,600,000 for msec/hr
if (spd != i) {
spd = i;
TFTscreen.setTextSize(3);
i /= 10; //convert to hectometres/hr
a1[0] = (i % 10)+'0';
a1[1] = 0;
wrtVal(43,18);
i /= 10;
a1[1] = (i % 10)+'0';
i /= 10;
a1[0] = (i) ? i+'0' : ' ';
a1[2] = 0;
wrtVal(0,18);
//power calcs - watts. Using straight multiplication since Pow() compiles larger and slower
// i = trainerVar1*spd + trainerVar2*spd*spd + trainerVar3*spd*spd*spd; //restore trainer variables in initializing code if porting program to a processor with more dynamic memory
i = 0.02336*spd + 3.082E-7*spd*spd + 3.071E-9*spd*spd*spd;
power = i;
TFTscreen.setTextSize(3);
a1[2] = (i % 10)+'0';
i /= 10;
if (!i) {
a1[1] = ' ';
a1[0] = ' ';
}
else {
a1[1] = (i % 10)+'0';
i /= 10;
a1[0] = (i) ? (i%10)+'0' : ' ';
}
a1[3] = 0;
wrtVal(0,70);
TFTscreen.setTextSize(1);
if (power > powerMax) {
i = power;
powerMax = i;
a1[2] = (i % 10)+'0';
i /= 10;
if (!i) {
a1[1] = ' ';
a1[0] = ' ';
}
else {
a1[1] = (i % 10)+'0';
i /= 10;
a1[0] = (i) ? (i%10)+'0' : ' ';
}
a1[3] = 0;
wrtVal(113,86);
}
}
//mean and maximum speed
TFTscreen.setTextSize(1);
i = distance * 360.0 / bikeTime;
if (spdMean != i) {
spdMean = i; //mean speed in decametres/hr
a1[4] = (i % 10)+'0';
i /= 10;
a1[3] = (i % 10)+'0';
a1[2] = '.';
i /= 10;
a1[1] = (i % 10)+'0';
i /= 10;
a1[0] = (i) ? (i%10)+'0' : ' ';
wrtVal(101,24);
}
if (spd > spdMax) {
i = spd;
spdMax = i; //update SpdMax to new value
a1[4] = (i % 10)+'0';
i /= 10;
a1[3] = (i % 10)+'0';
a1[2] = '.';
i /= 10;
a1[1] = (i % 10)+'0';
i /= 10;
a1[0] = (i) ? (i%10)+'0' : ' ';
wrtVal(101,34);
}
//mean power
powerSum += (float)power * codeLoopTime; //calculate power consumed in the time elapsed, in Joules
i = powerSum / bikeTime; //and calc average watts over time on bike
if (i != powerMean) {
powerMean = i;
a1[2] = (i % 10)+'0';
i /= 10;
if (!i) {
a1[1] = ' ';
a1[0] = ' ';
}
else {
a1[1] = (i % 10)+'0';
i /= 10;
a1[0] = (i) ? (i%10)+'0' : ' ';
}
a1[3] = 0;
wrtVal(113,76);
}
//calorie count: cal = work * codeLoopTime /1000 (in seconds) / 4.184 cal/joule / (GME/100) for GME percent
// work += (float)power * codeLoopTime / workConvVar;
work += (float)power * codeLoopTime / 962.3;
i = work / 1000; //convert kCal ("calories")
if (i != prevWork) {
prevWork = i;
TFTscreen.setTextSize(3);
a1[2] = (i % 10)+'0';
i /= 10;
if (!i) {
a1[1] = ' ';
a1[0] = ' ';
}
else {
a1[1] = (i % 10)+'0';
i /= 10;
a1[0] = (i) ? (i%10)+'0' : ' ';
}
a1[3] = 0;
wrtVal(0,96);
}
}
////* Interrupt Service Routines for pedal and wheel sensors
void incPedal() {
if(TinyPinChange_RisingEdge(virtPort0, pedal_sensor)) {
uint32_t j = millis();
if (j - pedalTime > 100) { //if at least 10 msec have passed since ISR last triggered, i.e. past possible switch bounce
pedalTime = j; //store current pedalTime
pedalCount++; //and increment pedal counter
}
}
}
void incWheel() {
if(TinyPinChange_RisingEdge(virtPort1, wheel_sensor)) {
uint32_t j = millis();
if (j - wheelTime > 10) { //debounce
wheelTime = j; //etc.
wheelCount++;
}
}
}
////* zero the main display: speed, cadence, watts
void zeroMainDisplay() {
TFTscreen.setTextSize(3);
zeroMainDisplayFlag = 0;
a1[0] = ' '; //zero speed
a1[1] = '0';
a1[2] = 0;
wrtVal(0,18);
a1[0] = '0';
a1[1] = 0;
wrtVal(43,18);
a1[0] = ' '; //and zero cadence and watts
a1[1] = ' ';
a1[2] = '0';
a1[3] = 0;
wrtVal(0,44);
wrtVal(0,70);
}
////* blank out previous characters by writing an extended ASCII code 219 character (block, or █) in black over the previous. To conserve program space,
// I have truncated GLCDFONT.C at 'z', or character 123, and remapped ASCII character 219 to character code 1
void wrtVal(byte x, byte y) { //code works to print up to 5 spaces truncated by 0
byte v=0;
while (a1[v]) {
a2[v] = 1; //print full character-sized rectangles (char code 1 in custom font)
v++;
}
a2[v] = 0;
TFTscreen.stroke(0,0,0); //in black, to blank out the previous character
TFTscreen.text(a2,x,y);
TFTscreen.stroke(255,255,255); //then print new text in white
TFTscreen.text(a1,x,y);
}
-
Have updated the code above - fixed a few bugs, mostly related to wheel rotations being missed while code was executing, by using ISRs to capture wheel and pedal rotations. And then the bugs that came from implementing the new code ...
I've also updated the math for power calculations using formula derived from the www.powercurvesensor.com/cycling-trainer-power-curves/ site, as these are more accurate for my trainer than the values from Kurt Kinetic.
Happily, my trainer computer comes within 0.02% of values derived from a known-good bike computer!
-
and below, the truncated/modified GLCDFONT.C. Numbers and letters are in their original positions - for ease of coding. Truncating the font after 'z' saves 660 bytes compiled. Additionally, I have remapped extended ASCII code 219 character (block, or █) to position 1 in the font. This allows me to blank out any previous character in a given position, using the same code used to write new text, saving even more on compile size.
#ifndef ARDUINO_ARCH_SAM
#include <avr/io.h>
#endif
#include <avr/pgmspace.h>
#ifndef FONT5X7_H
#define FONT5X7_H
// standard ascii 5x7 font
static const unsigned char font[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x3E, 0x6B, 0x4F, 0x6B, 0x3E,
0x1C, 0x3E, 0x7C, 0x3E, 0x1C,
0x18, 0x3C, 0x7E, 0x3C, 0x18,
0x1C, 0x57, 0x7D, 0x57, 0x1C,
0x1C, 0x5E, 0x7F, 0x5E, 0x1C,
0x00, 0x18, 0x3C, 0x18, 0x00,
0xFF, 0xE7, 0xC3, 0xE7, 0xFF,
0x00, 0x18, 0x24, 0x18, 0x00,
0xFF, 0xE7, 0xDB, 0xE7, 0xFF,
0x30, 0x48, 0x3A, 0x06, 0x0E,
0x26, 0x29, 0x79, 0x29, 0x26,
0x40, 0x7F, 0x05, 0x05, 0x07,
0x40, 0x7F, 0x05, 0x25, 0x3F,
0x5A, 0x3C, 0xE7, 0x3C, 0x5A,
0x7F, 0x3E, 0x1C, 0x1C, 0x08,
0x08, 0x1C, 0x1C, 0x3E, 0x7F,
0x14, 0x22, 0x7F, 0x22, 0x14,
0x5F, 0x5F, 0x00, 0x5F, 0x5F,
0x06, 0x09, 0x7F, 0x01, 0x7F,
0x00, 0x66, 0x89, 0x95, 0x6A,
0x60, 0x60, 0x60, 0x60, 0x60,
0x94, 0xA2, 0xFF, 0xA2, 0x94,
0x08, 0x04, 0x7E, 0x04, 0x08,
0x10, 0x20, 0x7E, 0x20, 0x10,
0x08, 0x08, 0x2A, 0x1C, 0x08,
0x08, 0x1C, 0x2A, 0x08, 0x08,
0x1E, 0x10, 0x10, 0x10, 0x10,
0x0C, 0x1E, 0x0C, 0x1E, 0x0C,
0x30, 0x38, 0x3E, 0x38, 0x30,
0x06, 0x0E, 0x3E, 0x0E, 0x06,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x5F, 0x00, 0x00,
0x00, 0x07, 0x00, 0x07, 0x00,
0x14, 0x7F, 0x14, 0x7F, 0x14,
0x24, 0x2A, 0x7F, 0x2A, 0x12,
0x23, 0x13, 0x08, 0x64, 0x62,
0x36, 0x49, 0x56, 0x20, 0x50,
0x00, 0x08, 0x07, 0x03, 0x00,
0x00, 0x1C, 0x22, 0x41, 0x00,
0x00, 0x41, 0x22, 0x1C, 0x00,
0x2A, 0x1C, 0x7F, 0x1C, 0x2A,
0x08, 0x08, 0x3E, 0x08, 0x08,
0x00, 0x80, 0x70, 0x30, 0x00,
0x08, 0x08, 0x08, 0x08, 0x08,
0x00, 0x00, 0x60, 0x60, 0x00,
0x20, 0x10, 0x08, 0x04, 0x02,
0x3E, 0x51, 0x49, 0x45, 0x3E,
0x00, 0x42, 0x7F, 0x40, 0x00,
0x72, 0x49, 0x49, 0x49, 0x46,
0x21, 0x41, 0x49, 0x4D, 0x33,
0x18, 0x14, 0x12, 0x7F, 0x10,
0x27, 0x45, 0x45, 0x45, 0x39,
0x3C, 0x4A, 0x49, 0x49, 0x31,
0x41, 0x21, 0x11, 0x09, 0x07,
0x36, 0x49, 0x49, 0x49, 0x36,
0x46, 0x49, 0x49, 0x29, 0x1E,
0x00, 0x00, 0x14, 0x00, 0x00,
0x00, 0x40, 0x34, 0x00, 0x00,
0x00, 0x08, 0x14, 0x22, 0x41,
0x14, 0x14, 0x14, 0x14, 0x14,
0x00, 0x41, 0x22, 0x14, 0x08,
0x02, 0x01, 0x59, 0x09, 0x06,
0x3E, 0x41, 0x5D, 0x59, 0x4E,
0x7C, 0x12, 0x11, 0x12, 0x7C,
0x7F, 0x49, 0x49, 0x49, 0x36,
0x3E, 0x41, 0x41, 0x41, 0x22,
0x7F, 0x41, 0x41, 0x41, 0x3E,
0x7F, 0x49, 0x49, 0x49, 0x41,
0x7F, 0x09, 0x09, 0x09, 0x01,
0x3E, 0x41, 0x41, 0x51, 0x73,
0x7F, 0x08, 0x08, 0x08, 0x7F,
0x00, 0x41, 0x7F, 0x41, 0x00,
0x20, 0x40, 0x41, 0x3F, 0x01,
0x7F, 0x08, 0x14, 0x22, 0x41,
0x7F, 0x40, 0x40, 0x40, 0x40,
0x7F, 0x02, 0x1C, 0x02, 0x7F,
0x7F, 0x04, 0x08, 0x10, 0x7F,
0x3E, 0x41, 0x41, 0x41, 0x3E,
0x7F, 0x09, 0x09, 0x09, 0x06,
0x3E, 0x41, 0x51, 0x21, 0x5E,
0x7F, 0x09, 0x19, 0x29, 0x46,
0x26, 0x49, 0x49, 0x49, 0x32,
0x03, 0x01, 0x7F, 0x01, 0x03,
0x3F, 0x40, 0x40, 0x40, 0x3F,
0x1F, 0x20, 0x40, 0x20, 0x1F,
0x3F, 0x40, 0x38, 0x40, 0x3F,
0x63, 0x14, 0x08, 0x14, 0x63,
0x03, 0x04, 0x78, 0x04, 0x03,
0x61, 0x59, 0x49, 0x4D, 0x43,
0x00, 0x7F, 0x41, 0x41, 0x41,
0x02, 0x04, 0x08, 0x10, 0x20,
0x00, 0x41, 0x41, 0x41, 0x7F,
0x04, 0x02, 0x01, 0x02, 0x04,
0x40, 0x40, 0x40, 0x40, 0x40,
0x00, 0x03, 0x07, 0x08, 0x00,
0x20, 0x54, 0x54, 0x78, 0x40,
0x7F, 0x28, 0x44, 0x44, 0x38,
0x38, 0x44, 0x44, 0x44, 0x28,
0x38, 0x44, 0x44, 0x28, 0x7F,
0x38, 0x54, 0x54, 0x54, 0x18,
0x00, 0x08, 0x7E, 0x09, 0x02,
0x18, 0xA4, 0xA4, 0x9C, 0x78,
0x7F, 0x08, 0x04, 0x04, 0x78,
0x00, 0x44, 0x7D, 0x40, 0x00,
0x20, 0x40, 0x40, 0x3D, 0x00,
0x7F, 0x10, 0x28, 0x44, 0x00,
0x00, 0x41, 0x7F, 0x40, 0x00,
0x7C, 0x04, 0x78, 0x04, 0x78,
0x7C, 0x08, 0x04, 0x04, 0x78,
0x38, 0x44, 0x44, 0x44, 0x38,
0xFC, 0x18, 0x24, 0x24, 0x18,
0x18, 0x24, 0x24, 0x18, 0xFC,
0x7C, 0x08, 0x04, 0x04, 0x08,
0x48, 0x54, 0x54, 0x54, 0x24,
0x04, 0x04, 0x3F, 0x44, 0x24,
0x3C, 0x40, 0x40, 0x20, 0x7C,
0x1C, 0x20, 0x40, 0x20, 0x1C,
0x3C, 0x40, 0x30, 0x40, 0x3C,
0x44, 0x28, 0x10, 0x28, 0x44,
0x4C, 0x90, 0x90, 0x90, 0x7C,
0x44, 0x64, 0x54, 0x4C, 0x44,
};
#endif