// ===================================================================================
// Project: TinyTacho - RPM-Meter using ATtiny13A and I²C OLED
// Version: v1.0
// Year: 2020
// Author: Stefan Wagner
// Github: https://github.com/wagiminator
// EasyEDA: https://easyeda.com/wagiminator
// License: http://creativecommons.org/licenses/by-sa/3.0/
// ===================================================================================
//
// Description:
// ------------
// This code implements a simple tachometer (RPM counter). An IR photo
// diode is connected to the positive input of ATtiny's internal
// analog comparator, a variable resistor for calibration is connected to
// the negative input. An interrupt is triggered on every falling edge of
// the comparator output which saves the current value of timer/counter 0
// and restarts the timer. The 8-bit timer is expanded to a 16-bit one by
// using the timer overflow interrupt. The saved timer/counter value
// contains the timer counts per revolution. The RPM is calculated by
// utilizing the following equation:
//
// RPM = 60 * F_CPU / prescaler / counter
// = 60 * 1200000 / 64 / counter
// = 1125000 / counter
//
// The calculated RPM value is displayed on an I²C OLED display.
// The I²C protocol implementation is based on a crude bitbanging method.
// It was specifically designed for the limited resources of ATtiny10 and
// ATtiny13, but should work with some other AVRs as well.
// The functions for the OLED are adapted to the SSD1306 128x32 OLED module,
// but they can easily be modified to be used for other modules. In order to
// save resources, only the basic functionalities which are needed for this
// application are implemented.
// For a detailed information on the working principle of the I²C OLED
// implementation visit https://github.com/wagiminator/attiny13-tinyoleddemo
//
// References:
// -----------
// This project was inspired by Great Scott's DIY tachometer:
// https://www.instructables.com/DIY-Tachometer-RPM-Meter/
//
// Wiring:
// -------
// +-----------------------------+
// ---|SDA +--------------------+ |
// ---|SCL | SSD1306 OLED | |
// ---|VCC | 128x36 | |
// ---|GND +--------------------+ |
// +-----------------------------+
//
// +-\/-+
// --- RST ADC0 PB5 1|° |8 Vcc
// SCL OLED ------- ADC3 PB3 2| |7 PB2 ADC1 --------
// SDA OLED ------- ADC2 PB4 3| |6 PB1 AIN1 OC0B --- Calib Poti
// GND 4| |5 PB0 AIN0 OC0A --- IR Photo Diode
// +----+
//
// Compilation Settings:
// ---------------------
// Controller: ATtiny13A
// Core: MicroCore (https://github.com/MCUdude/MicroCore)
// Clockspeed: 1.2 MHz internal
// BOD: BOD disabled
// Timing: Micros disabled
//
// Leave the rest on default settings. Don't forget to "Burn bootloader"!
// No Arduino core functions or libraries are used. Use the makefile if
// you want to compile without Arduino IDE.
//
// Note: The internal oscillator may need to be calibrated for the device
// to function properly.
//
// Fuse settings: -U lfuse:w:0x2a:m -U hfuse:w:0xff:m
// ===================================================================================
// Libraries and Definitions
// ===================================================================================
// Oscillator calibration value (uncomment and set if necessary)
//#define OSCCAL_VAL 0x66
// Libraries
#include <avr/io.h> // for GPIO
#include <avr/interrupt.h> // for interrupts
#include <avr/pgmspace.h> // to store data in programm memory
#include <avr/delay.h>
// Pin definitions
#define I2C_SCL PB3 // I2C serial clock pin
#define I2C_SDA PB4 // I2C serial data pin
// ===================================================================================
// I2C Implementation
// ===================================================================================
// I2C macros
#define I2C_SDA_HIGH() DDRB &= ~(1<<I2C_SDA) // release SDA -> pulled HIGH by resistor
#define I2C_SDA_LOW() DDRB |= (1<<I2C_SDA) // SDA as output -> pulled LOW by MCU
#define I2C_SCL_HIGH() DDRB &= ~(1<<I2C_SCL) // release SCL -> pulled HIGH by resistor
#define I2C_SCL_LOW() DDRB |= (1<<I2C_SCL) // SCL as output -> pulled LOW by MCU
// I2C init function
void I2C_init(void) {
DDRB &= ~((1<<I2C_SDA)|(1<<I2C_SCL)); // pins as input (HIGH-Z) -> lines released
PORTB &= ~((1<<I2C_SDA)|(1<<I2C_SCL)); // should be LOW when as ouput
}
// I2C transmit one data byte to the slave, ignore ACK bit, no clock stretching allowed
void I2C_write(uint8_t data) {
for(uint8_t i = 8; i; i--, data<<=1) { // transmit 8 bits, MSB first
I2C_SDA_LOW(); // SDA LOW for now (saves some flash this way)
if(data & 0x80) I2C_SDA_HIGH(); // SDA HIGH if bit is 1
I2C_SCL_HIGH(); // clock HIGH -> slave reads the bit
I2C_SCL_LOW(); // clock LOW again
}
I2C_SDA_HIGH(); // release SDA for ACK bit of slave
I2C_SCL_HIGH(); // 9th clock pulse is for the ACK bit
I2C_SCL_LOW(); // but ACK bit is ignored
}
// I2C start transmission
void I2C_start(uint8_t addr) {
I2C_SDA_LOW(); // start condition: SDA goes LOW first
I2C_SCL_LOW(); // start condition: SCL goes LOW second
I2C_write(addr); // send slave address
}
// I2C stop transmission
void I2C_stop(void) {
I2C_SDA_LOW(); // prepare SDA for LOW to HIGH transition
I2C_SCL_HIGH(); // stop condition: SCL goes HIGH first
I2C_SDA_HIGH(); // stop condition: SDA goes HIGH second
}
// ===================================================================================
// OLED Implementation
// ===================================================================================
// OLED definitions
#define OLED_ADDR 0x78 // OLED write address
#define OLED_CMD_MODE 0x00 // set command mode
#define OLED_DAT_MODE 0x40 // set data mode
#define OLED_INIT_LEN 15 // length of OLED init command array
// OLED init settings
const uint8_t OLED_INIT_CMD[] PROGMEM = {
0xA8, 0x1F, // set multiplex for 128x32
0x22, 0x00, 0x03, // set min and max page
0x20, 0x01, // set vertical memory addressing mode
0xDA, 0x02, // set COM pins hardware configuration to sequential
0x8D, 0x14, // enable charge pump
0xAF, // switch on OLED
0x00, 0x10, 0xB0 // set cursor at home position
};
// OLED simple reduced 3x8 font
const uint8_t OLED_FONT[] PROGMEM = {
// C1 , C2 , C3 começa de baixo para cima
// primeira linha de baixo é 0
0x00, 0x00, 0x00, // clear 0
0x7F, 0x7F, 0x7F, // blank 1
0x00, 0x00, 0x00, // clear 2
0x7F, 0x41, 0x7F, // 0 3
0x00, 0x00, 0x7F, // 1 4
0x79, 0x49, 0x4F, // 2 5
0x41, 0x49, 0x7F, // 3 6
0x0F, 0x08, 0x7E, // 4 7
0x4F, 0x49, 0x79, // 5 8
0x7F, 0x49, 0x79, // 6 9
0x03, 0x01, 0x7F, // 7 10
0x7F, 0x49, 0x7F, // 8 11
0x4F, 0x49, 0x7F, // 9 12
0x7F, 0x09, 0x7F, // A 13
0x7F, 0x48, 0x78, // b 14
0x7F, 0x41, 0x63, // C 15
0x78, 0x48, 0x7F, // d 16
0x7F, 0x49, 0x41, // E 17
0x7F, 0x09, 0x01, // F 18
0x00, 0x60, 0x00, // . 19
0x00, 0x36, 0x00, // : 20
0x08, 0x08, 0x08, // - 21
0x00, 0x00, 0x00, // 22
0x7F, 0x40, 0x60, // L 23
0x7F, 0x20, 0x7F, // W 24
0x4F, 0x49, 0x79, // S=5 25
0x00, 0x7F, 0x00, // I 26
0b01111111, 0b01001001, 0b00110110, //B 27
0b01111111, 0b01100010, 0b00011100, //D 28
0b01111111, 0b00001100, 0b01111111, //H 29
0b01010111, 0b01110000, 0b00010000, //G 30
0b01010001, 0b01111111, 0b01010001, //I 31
0b01100000, 0b01000000, 0b01111111, //J 31
};
// OLED global variables
uint8_t buffer[8] = {12, 0, 0, 0, 0, 0, 12, 12}; // screen buffer
uint8_t slow[8] = {12, 12, 5, 10, 0, 11, 12, 12}; // "SLOW"
uint16_t divider[5] = {10000, 1000, 100, 10, 1}; // for BCD conversion
uint8_t test[8] = {23, 22, 21, 20, 24, 11, 12, 12}; // "SLOW"
uint8_t clear[8] = {0, 0, 0, 0, 0, 0, 0, 0};
uint8_t black[8] = {1, 1, 1, 1, 1, 1, 1, 1};
uint8_t ABCDEFGH[8] = {13, 27, 15, 28, 17, 18, 30, 29};
uint8_t IJKLMNOP[8] = {31 27, 15, 28, 17, 18, 30, 29};
// OLED init function
void OLED_init(void) {
I2C_init(); // initialize I2C first
I2C_start(OLED_ADDR); // start transmission to OLED
I2C_write(OLED_CMD_MODE); // set command mode
for(uint8_t i = 0; i < OLED_INIT_LEN; i++) I2C_write(pgm_read_byte(&OLED_INIT_CMD[i])); // send the command bytes
I2C_stop(); // stop transmission
}
// OLED stretch a part of a byte
uint8_t OLED_stretch(uint8_t b) {
b = ((b & 2) << 3) | (b & 1); // split 2 LSB into the nibbles
b |= b << 1; // double the bits
b |= b << 2; // double them again = 4 times
return b; // return the value
}
// OLED print a big digit by stretching the character
void OLED_printD(uint8_t ch) {
uint8_t i, j, k, b; // loop variables
uint8_t sb[4]; // stretched character bytes
ch += ch << 1; // calculate position of character in font array
for(i=8; i; i--) I2C_write(0x00); // print spacing between characters
for(i=3; i; i--) { // font has 3 bytes per character
b = pgm_read_byte(&OLED_FONT[ch++]); // read character byte
for(j=0; j<4; j++, b >>= 2) sb[j] = OLED_stretch(b); // stretch 4 times
j=4; if(i==2) j=6; // calculate x-stretch value
while(j--) { // write several times (x-direction)
for(k=0; k<4; k++) I2C_write(sb[k]);// the 4 stretched bytes (y-direction)
}
}
}
// OLED print buffer
void OLED_printB(uint8_t *buffer) {
I2C_start(OLED_ADDR); // start transmission to OLED
I2C_write(OLED_DAT_MODE); // set data mode
for(uint8_t i=0; i<8; i++) OLED_printD(buffer[i]); // print buffer
I2C_stop(); // stop transmission
}
// OLED print 16 bit value (BCD conversion by substraction method)
void OLED_printW(uint16_t value) {
for(uint8_t digit = 0; digit < 5; digit++) { // 5 digits
uint8_t digitval = 0; // start with digit value 0
while(value >= divider[digit]) { // if current divider fits into the value
digitval++; // increase digit value
value -= divider[digit]; // decrease value by divider
}
buffer[digit + 1] = digitval; // set the digit in the screen buffer
}
OLED_printB(buffer); // print screen buffer on the OLED
}
// ===================================================================================
// Main Function
// ===================================================================================
// Main function
int main(void) {
OLED_init(); // initialize the OLED
// Loop
while(1) { // loop until forever
//OLED_printW(123); // print RPM value on the OLED
OLED_printB(ABCDEFGH); // else print "SLOW" on the OLED
_delay_ms(5000);
OLED_printB(IJKLMNOP); // else print "SLOW" on the OLED
_delay_ms(5000);
}
}