Satellite 2202

Last updated: 23 Sept, 2023

I’ve been playing with idea of making tiny free-formed circuit sculptures in the shape of a satellite for a while now. I build a few LED blinker before, but this time I wanted to build something that did more than just blink an LED. I recently came across a small low power LCD module with a built-in driver and controller that you talk to over I2C. Most LCD modules out there are bare LCDs without the driver or controller, and the ones that have built-in drivers are too big. This particular one seems to be a perfect odd duck. So I decided to build a “satellite” around it using an ATtiny85 microcontroller and SHT31 sensor. The four solar cells connected in parallel charge a small super capacitor. The MCU periodically wakes up, takes a sensor measurement, displays the reading, and then goes to sleep to conserve power.

(*some of the product links include Amazon affiliate links)

Parts list

Schematic diagram

The schematic is simple. The LCD and the sensor are connected over I2C. No pull up resistors are required since the sensor board already has them built-in, but if yours doesn’t, then you’ll have to connect them separately. The four solar cells are connected in parallel and they charge the supercapacitor via a reverse polarity blocking diode. You can use any small diode with low forward voltage drop.

I would recommend first assembling the circuit on a breadboard. For programming the ATtiny85, I used the Pololu’s AVR programmer. You can also use an Arduino UNO to act as a programmer instead.

Construction

(I failed to take photos during the build process with my DSLR, so here are some crappy photos from my phone instead.)

I used a small proto PCB to align the joints. Once I soldered the sensor, I removed the proto PCB and replaced it with the LCD module.

 

The device can also directly power up from the solar cells alone in direct sunlight.

Firmware

The code requires a couple of device libraries. An SHT31 sensor library from Adafruit and a CDM4101 library for the LCD. Ensure that you have them installed via the Arduino IDE.

//
// Project link: https://www.bhoite.com/sculptures/satellite-2202/
//
#include "Adafruit_SHT31.h"
//#include <TinyWireM.h>
//#include <USI_TWI_Master.h>
#include <avr/sleep.h> //Needed for sleep_mode
#include <avr/wdt.h> //Needed to enable/disable watch dog timer
//Following is the header file for the LCD module bought on ebay
//Seller link https://www.ebay.com/itm/284843842249
//Information on the LCD http://www.hexpertsystems.com/LCD/
#include <cdm4101.h>
int ctr=1000;
CDM4101 LCD;
char data[25];
int temperature = 0;
bool displayToggle = 0;
uint8_t loopCnt = 0;
Adafruit_SHT31 sht31 = Adafruit_SHT31();
void setup()
{
pinMode(3, INPUT_PULLUP);
pinMode(4, INPUT_PULLUP);
pinMode(5, INPUT_PULLUP);
pinMode(1, INPUT_PULLUP);
ADCSRA &= ~(1<<ADEN); //Disable ADC, saves ~230uA
// put your setup code here, to run once:
//TinyWireM.begin();
Wire.begin();
sht31.begin(0x44);
LCD.Init();
LCD.DispStr("sat6");
delay(1000);
set_sleep_mode(SLEEP_MODE_PWR_DOWN); //Power down everything, wake up from WDT
sleep_enable();
}
//This runs each time the watch dog wakes us up from sleep
ISR(WDT_vect) {
//watchdog_counter++;
displayToggle=!displayToggle;
}
void loop()
{
setup_watchdog(9); //Setup watchdog to go off after 8sec
sleep_mode(); //Go to sleep! Wake up 8 sec later
int temperature = (sht31.readTemperature()*1.8)+32;
//int temperature = sht31.readTemperature();
int humidity = sht31.readHumidity();
if (displayToggle)
{
//insure that the temperature is in a valid range
if ((temperature <110) && (temperature > -20))
{
sprintf(data,"%doF",temperature);
LCD.DispStr(data);
}
}
else
{
//insure that the humidity is in a valid range
if ((humidity <101) && (humidity > 0))
{
sprintf(data,"%drH",humidity);
LCD.DispStr(data);
}
}
}
//Sets the watchdog timer to wake us up, but not reset
//0=16ms, 1=32ms, 2=64ms, 3=128ms, 4=250ms, 5=500ms
//6=1sec, 7=2sec, 8=4sec, 9=8sec
//From: http://interface.khm.de/index.php/lab/experiments/sleep_watchdog_battery/
void setup_watchdog(int timerPrescaler) {
if (timerPrescaler > 9 ) timerPrescaler = 9; //Limit incoming amount to legal settings
byte bb = timerPrescaler & 7;
if (timerPrescaler > 7) bb |= (1<<5); //Set the special 5th bit if necessary
//This order of commands is important and cannot be combined
MCUSR &= ~(1<<WDRF); //Clear the watch dog reset
WDTCR |= (1<<WDCE) | (1<<WDE); //Set WD_change enable, set WD enable
WDTCR = bb; //Set new watchdog timeout value
WDTCR |= _BV(WDIE); //Set the interrupt enable, this will keep unit from resetting after each int
}
//----------------------------------------------------------------------------
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "Arduino.h"
#include "Wire.h"
#include "cdm4101.h"
//----------------------------------------------------------------------------
#define I2C_ADDR (0x38)
#define CMD_MODE_SET 0xCD
#define CMD_LOAD_DP 0x80
#define CMD_DEVICE_SEL 0xE0
#define CMD_BANK_SEL 0xF8
#define CMD_NOBLINK 0x70
#define CMD_BLINK 0x71
#define DASH 10
#define UNDERSCORE 11
#define SPACE 12
#define ASTERISK 39
#define ALPHA_START 13
#define HOLD_TIME 8
//----------------------------------------------------------------------------
static byte const Segs[] =
{
0x6F, // 0x30 0
0x03, // 0x31 1
0x5D, // 0x32 2
0x57, // 0x33 3
0x33, // 0x34 4
0x76, // 0x35 5
0x7E, // 0x36 6
0x43, // 0x37 7
0x7F, // 0x38 8
0x77, // 0x39 9
0x10, // -
0x04, // _
0x00, // space
0x7B, // A
0x3E, // b
0x6C, // C
0x1F, // d
0x7C, // E
0x78, // F
0x77, // G
0x3B, // H
0x02, // i
0x0F, // J
0x38, // K - can't do
0x2C, // L
0x5A, // M - can't do
0x1A, // n
0x6F, // O
0x79, // P
0x75, // Q - can't do
0x18, // r
0x76, // S
0x3C, // t
0x0E, // u
0x2F, // V - can't do
0x4E, // W - can't do
0x2B, // X - can't do
0x37, // y
0x5D, // Z
0x71, // ° - use * to represent it in your string
};
//----------------------------------------------------------------------------
void CDM4101::Update(void)
{
byte data[5]; // bytes to send display segments
if(Ctr)
{
Ctr--;
return;
}
Wire.beginTransmission(I2C_ADDR);
Wire.write(CMD_MODE_SET);
Wire.write(CMD_LOAD_DP);
Wire.write(CMD_DEVICE_SEL);
Wire.write(CMD_BANK_SEL);
if(Blink) Wire.write(CMD_BLINK);
else Wire.write(CMD_NOBLINK);
#if 1
data[0] = (Digits[0] >> 4); // || LCD_BAR
data[1] = (Digits[0] << 4) | (Digits[1] >> 3);
data[2] = (Digits[1] << 5) | (Digits[2] >> 2);
data[3] = (Digits[2] << 6) | (Digits[3] >> 1);
data[4] = (Digits[3] << 7);
#else
Wire.write(0x70);
Wire.write(0x3B);
Wire.write(0xB5);
Wire.write(0xD9);
Wire.write(0x80);
#endif
for(int i=0;i<5;i++) Wire.write(data[i]);
Wire.endTransmission();
}
//----------------------------------------------------------------------------
void CDM4101::Init(void)
{
Blink = 0;
Digits[0] = Segs[SPACE];
Digits[1] = Segs[SPACE];
Digits[2] = Segs[SPACE];
Digits[3] = Segs[SPACE];
Wire.beginTransmission(I2C_ADDR);
Wire.write(CMD_MODE_SET);
Wire.write(CMD_LOAD_DP);
Wire.write(CMD_DEVICE_SEL);
Wire.write(CMD_BANK_SEL);
Wire.write(CMD_NOBLINK);
Wire.write(0x05);
Wire.write(0xD5);
Wire.write(0x9B);
Wire.write(0xFF);
Wire.write(0x00);
Wire.endTransmission();
Ctr = 0;
}
//----------------------------------------------------------------------------
void CDM4101::Command(byte cmd)
{
switch(cmd)
{
case CDM4101_BLINK_OFF :
Blink = 0;
Update();
break;
case CDM4101_BLINK_ON :
Blink = 1;
Update();
break;
case CDM4101_CLEAR :
Digits[0] = Segs[SPACE];
Digits[1] = Segs[SPACE];
Digits[2] = Segs[SPACE];
Digits[3] = Segs[SPACE];
Update();
break;
default :
break;
}
}
//----------------------------------------------------------------------------
char CDM4101::ConvertChar(char c)
{
if((c >= 'a') && (c <= 'z')) c = c - 'a' + ALPHA_START;
else
if((c >= 'A') && (c <= 'Z')) c = c - 'A' + ALPHA_START;
else
if((c >= '0') && (c <= '9')) c = c - '0';
else
if(c == '-') c = DASH;
else
if(c == '_') c = UNDERSCORE;
else
if(c == '*') c = ASTERISK;
else c = SPACE;
return c;
}
//----------------------------------------------------------------------------
void CDM4101::DispChar(byte index, char c)
{
Digits[(int)index] = Segs[(int)(ConvertChar(c))];
Update();
}
//----------------------------------------------------------------------------
void CDM4101::DispStr(char *s)
{
byte i,c;
for(i=0;i<CDM4101_NUM_DIGITS;i++) Digits[i] = Segs[SPACE];
i = 0;
while((i < 4) && s[i])
{
c = Segs[(int)(ConvertChar(s[i]))];
Digits[i] = c;
i++;
}
Update();
}
//----------------------------------------------------------------------------
void CDM4101::DispStrTimed(char *s)
{
DispStr(s);
Ctr = HOLD_TIME;
}
//----------------------------------------------------------------------------
// display a decimal value, right justified.
void CDM4101::DispDec(short n)
{
byte i;
char str[5];
if(n < -999L) n = -999L;
if(n > 9999L) n = 9999L;
for(i=0;i<5;i++) str[i] = 0;
itoa(n, str, 10);
while(str[3] == 0)
{
for(i=3;i>0;i--) str[i] = str[i-1];
str[0] = ' ';
}
DispStr(str);
}
//----------------------------------------------------------------------------
#ifndef _CDM4101_H_
#define _CDM4101_H_
#include <inttypes.h>
//----------------------------------------------------------------------------
#define CDM4101_NUM_DIGITS 4
//----------------------------------------------------------------------------
// LCD commands.
#define CDM4101_BLINK_OFF 6
#define CDM4101_BLINK_ON 7
#define CDM4101_CLEAR 8
//----------------------------------------------------------------------------
class CDM4101
{
public:
void Init(void);
void Command(byte cmd);
void DispChar(byte index, char c);
void DispStr(char *s);
void DispStrTimed(char *s);
void DispDec(short n);
private:
void Update(void);
char ConvertChar(char c);
byte Digits[CDM4101_NUM_DIGITS];
byte Blink;
byte Ctr;
};
//----------------------------------------------------------------------------
#endif // _CDM4101_H_
view raw cdm4101.h hosted with ❤ by GitHub