/***********************************************************
TACOMETRO Autor: LPT 2022
************************************************************
+++++ NEEDED HARDWARE +++++
- 1x Chip: Attiny85
- 1x Display: LCD OLED 0,91'' 128x32 (SSD1306)
- 2x PushButton: 2 pins -> https://mauser.pt/catalog/product_info.php?cPath=324_1401_1629&products_id=010-1120
- 1x Sensor : Any sensor with +Vcc voltage output OR use a relay.
Display - Software.
1st Interaction: Selection of number of target to complete 1 rotation
- Set the number of targets in one turn.
- When the motor completes a full turn, will calculate the RPM's and update oled.
OLED:
****************** x = number of target --> to increment with button
*Numero de Alvos:* A = decrements left to accept and junt to the 2nd Interaction
* x -A *
******************
2nd Interaction:
***** Selecção de MuLtiplicador *****
- This options allow to "simulate" more rpm than the motor is capable:
- Exemple: At full speed the motor only does 60 rpm.
- Input Sensor with X targets will Read 60rpm.
- External Sensor with 1 target will See 1x60rpm
- External Sensor with N targets will See Nx60rpm
OLED:
****************** x = number multiplications --> to increment with button
* MULTIPLICADOR * A = decrements left to accept and junt to the 3nd Interaction
* x -A *
******************
3rd Interaction:
***** MAIN SCREEN *****
OLED:
*********************** x = counted targets
*Alvos: x/y T:zzzzz * y = total targets per 1 rotation
* * z = time per 1 rotation [ms]
* RPM : BBB.B * B = Calculated RPM
***********************
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Improve at future:
- Maybe use the "Attiny13_oled_demo_tiny" lib to see RPM bigger.
- RPM in bigger, - it is realy need to have in one screen this information: RPM + n/targets + time ?
-> n/targets is usefull to setup the sensor.
-> Easy solved with button to tongle beatheen: RPM -> TIME -> n/targets + multipliction -> rpm
- use a simple lib for SSD1306. <Tiny4kOLED.h> is great but is blooted for this simple needs.
***********************************************************/
#undef F_CPU
#define F_CPU 8000000UL
#include <avr/interrupt.h>
#include <TinyWireM.h>
#include <Tiny4kOLED.h>
/******************************/
/*PROGRAM VARIABLES AND VALUES*/
/******************************/
#define time_delay_to_accept 100 //ms // set time to use on "wait_ms(xxx)" on "static uint8_t incremet(uint8_t Value_to_increment)"
#define n_time_delay_to_accept 15 // number of times to count "wait_ms(xxxx);" time on "static uint8_t incremet(uint8_t Value_to_increment)"
#define erro 1.016 //1.022 <- Bons resultados com 1.022 //1.0274; // More info at end of code: secction ERROR
volatile uint32_t timer=0; // interrupts counter
volatile uint32_t time_save=0; // save "timer" value = 1 period time
volatile uint32_t time_calc=0; // time TO calculate rpms, incremented with "time_save"
volatile bool period_complete = false; // is a new period ? if YES calculate in main function
volatile uint8_t n_targets_1_rotation=0; // How many targets one rpm as ?
volatile uint8_t targets=0; // count targets in one rotation
volatile float rpm=0; // store rpm calculation
volatile uint8_t multiplicador=1; // number to multiply with rpm: Tacometer sees one Target, sensor see 2 targuets, sensor see 2x ActualRpm
/******************************/
/* GPIO */
/******************************/
#define PB4_input() ( DDRB &= ~(1 << DDB4) ) // INPUT for SENSOR
#define PB4_pullup() ( PORTB |= (1 << PB4) )
#define PB3_input() ( DDRB &= ~(1 << DDB3) ) // INPUT for BUTTON
#define PB3_read() ((PINB & (1 << PB3)) == 8) // por = high -> PB3_read() = true if( PB3_read() )
#define PB4_read() ((PINB & (1 << PB4)) == 16)
/******************************/
/* INTERRUPT TIMER */
/******************************/
// INTERRUPT SERVICE ROUTINE
ISR (TIMER0_COMPA_vect){ //Interrupt vector for Timer0
// depends of F_CPU , TCCR0B_pre_scaler and OCR0A_value
timer++;
}
void timer0_comp_match_Setup(){
TCCR0A=0x00; //Normal mode
TCCR0B=0x00;
TCCR0B |= (1<<CS01)|(1<<CS00) ; // pres-scaler=64
TCCR0A |=(1<<WGM01); // Toggle mode and compare match mode
OCR0A = 125 ; // OCR0A – Output Compare Register A // 8MHz + pres-scaler=64 (0.008ms) + OCR0A_value=125 -> 125*0.008 = 1ms
// 11.9.4 TCNT0 – Timer/Counter Register
TCNT0 = 0;
sei(); // enabling global interrupt
// 11.9.7 TIMSK – Timer/Counter Interrupt Mask Register
TIMSK|=(1<<OCIE0A); // Bit 4 – OCIE0A: Timer/Counter0 Output Compare Match A Interrupt Enable
}
/******************************/
/* INTERRUPT PIN CHANGE */
/******************************/
ISR (PCINT0_vect){ // Interrupt service routine for Pin Change Interrupt Request 0 -PIN is set on function : pin_change_interrupt_setup()
if(PB4_read() && timer > 5){ // timer > x - try to use without relay
time_save=timer; // save timer value
timer=0; // clean timer
TCNT0=0; // clean TCNT0
period_complete=true; // when true we have Toff+Ton = 1 period
}
}
void pin_change_interrupt_setup() {
//cli(); // Disable interrupts during setup
// 9.3.2 GIMSK – General Interrupt Mask Register
GIMSK|= (1<<PCIE); // pin change interrupt is enabled.
// 9.3.4 PCMSK – Pin Change Mask Register
//PCMSK|= (1<<PCINT1); // PCINT1=PB1
//PCMSK|= (1<<PCINT2); // PCINT2=PB2 NAO SEI SE dá para usar em simultaneo com o INT
//PCMSK|= (1<<PCINT3); // PCINT1=PB3
PCMSK|= (1<<PCINT4); // PCINT1=PB4
//PCMSK|= (1<<PCINT5); // PCINT1=PB5
//sei(); //enabling global interrupt
}
/******************************/
/* PROGRAM FUNCTIONS */
/******************************/
// same as _delay_ms() from "delay.h" . dont know if is precise, in this use doens't matter -> just saved Flash
static void wait_ms(uint16_t time_to_wait){
/* // aux_timer Is not really needed on this function.
timer=0;
uint32_t aux_timer=0;
TCNT0 = 0;
while (time_to_wait > aux_timer) {
aux_timer=timer;
}
*/
//In his function, the varable "timer" (incremented in ISR() ) is set to ZERO and this fucntion set it to ZERO.
// the ISR continues to increment it and when the while function does tthe delay: do until
timer = 0; // set interrupts counter to zero
TCNT0 = 0; // clean TCNT0
while (time_to_wait > timer) { } // executa enquanto "time_to_wait" for maior que "timer"
}
// increvent++ uint8_t variable and return it after some "n_time_delay_to_accept".
static uint8_t incremet(uint8_t Value_to_increment){
bool block=false; // User most relise botton and press it again to increment. Acts like a flipflop
uint8_t aux=0;
uint8_t btt=0;
while(aux <= n_time_delay_to_accept){ // Executa enquanto "aux" for menor ou igual que "n_time_delay_to_accept"
btt= (PB3_read()) ? true : false; // tenary: (PB3_read()) == true : ; this value need to be stored because 1 button action is used 2 times.
if(block==false && btt==true){
Value_to_increment++;
block=true;
aux=0;
}
//oled.setCursor(60, 2);
oled.setCursor(10, 2);
oled.print(Value_to_increment);
oled.setCursor(90, 2);
oled.print(aux-n_time_delay_to_accept); // remaning time exit from while function.
oled.print(" ");
if(btt==0) block=false; // reset "flipflop"
aux++;
wait_ms(time_delay_to_accept);
}
return Value_to_increment;
}
/******************************/
/* OLED SETUP + SCREEN */
/******************************/
//#define txt_n_targets "Numero de Alvos: " // max 17 characters Number of Targets
#define txt_n_targets "Numb. of Targets:" // max 17 characters Number of Targets
//#define txt_n_plus_rpm "Multiplicador" // max 17 characters Number of Targets
#define txt_n_plus_rpm "Multiply RPM : " // max 17 characters Number of Targets
static void oled_setup(){
oled.begin(); // Send the initialization sequence to the oled. This leaves the display turned off
oled.clear(); // Clear the memory before turning on the display
oled.on(); // Turn on the display
//oled.switchRenderFrame(); // Switch the half of RAM that we are writing to, to be the half that is non currently displayed
oled.setFont(FONT6X8); // 4 lines of 21 characters only fills 126x32 : 6x8 font are 6 pixels wide and 8 pixels tall
}
static void wellcome(){
oled.clear();
oled.setFont(FONT8X16); // 4 lines of 21 characters only fills 126x32 : 6x8 font are 6 pixels wide and 8 pixels tall
oled.setCursor(0, 0);
oled.print("Tacometro");
oled.setFont(FONT6X8); // 4 lines of 21 characters only fills 126x32 : 6x8 font are 6 pixels wide and 8 pixels tall
oled.setCursor(90, 3);
wait_ms(1000);
oled.print("L");
wait_ms(100);
oled.print("P");
wait_ms(100);
oled.print("T");
wait_ms(200);
oled.clear();
}
static void main_Screen(){
/****** Main Screen *****/
oled.clear();
oled.setFont(FONT6X8);
oled.setCursor(0, 0);
oled.print("Alvos: ");
oled.setCursor(40, 0);
oled.print("0");
oled.setCursor(54, 0);
oled.print("/");
oled.print(n_targets_1_rotation);
oled.setFont(FONT8X16);
oled.setCursor(25, 2);
oled.print("RPM :");
oled.setCursor(70, 2);
oled.print("xxx");
oled.setFont(FONT6X8);
}
/******************************/
/* MAIN */
/******************************/
int main () {
PB4_input();
//PB4_pullup();
PB3_input();
timer0_comp_match_Setup(); // wait_ms uses this timer
oled_setup();
wellcome();
oled.setFont(FONT8X16); // 4 lines of 21 characters only fills 126x32 : 6x8 font are 6 pixels wide and 8 pixels tall
/***** Selecção de n_targets per 1 rotation ***** More info at TOP: 1st Interaction: */
oled.setCursor(0, 0);
//oled.print("Numero de Alvos: ");
oled.print(txt_n_targets);
n_targets_1_rotation = incremet(1);
oled.clear();
wait_ms(200);
/***** Selecção de MuLtiplicador ***** More info at TOP: 2nd Interaction: */
oled.setCursor(0, 0);
//oled.print("Multiplicador");
oled.print(txt_n_plus_rpm);
oled.setCursor(30, 2);
oled.print("x RPM");
multiplicador = incremet(1);
main_Screen(); /***** More info at TOP: 3rd Interaction: */
pin_change_interrupt_setup();
while(true){
if(PB3_read()){
timer=0;
time_calc=0;
targets=0;
oled.setFont(FONT8X16);
oled.setCursor(70, 2);
oled.print("xxxxxx");
oled.setFont(FONT6X8);
}
if(timer >= 64999) timer=0;// set timer to zero before his overflow
if(period_complete){ // prevent doing the same account over and over again
targets++;
time_calc+=time_save;
oled.setCursor(40, 0);
oled.print(targets);
if(targets<=9)oled.print(" ");
oled.setCursor(80, 0);
oled.print("T:");
oled.print(time_calc); // calculo até 1 rpm são 5 digitos
if(time_calc < 999)oled.print(" ");
if(time_calc < 9999)oled.print(" ");
if(n_targets_1_rotation <= targets){
//rpm= (erro*60000)/(time_calc*OCR0A_value_ms);
rpm= (erro*60000)/(time_calc);
oled.setFont(FONT8X16);
oled.setCursor(70, 2);
oled.print(multiplicador * rpm,1);
oled.print(" ");
oled.setFont(FONT6X8);
time_calc=0;
targets=0;
}
period_complete=false; // clean completed period
}
}
return 1;
}
/***********************************************************
* CALCULATIONS
*
TIME:
#define F_CPU=8 Mhz - 1000000 Hz=1Mhz // frequency do CPU
T= 1/f= 0,000000125 segundos -> 0,125 us
Pré-Scale set to = 64
New Clock frequency with Pré-Scale = F_CPU/Pre-Scale = 1000000/64= 125000 Hz → 125,00 Khz
New Clock PERIOD with Pré-Scale = 1/f = 1/125,00 Khz= 0,000008 -> Segundos = 8 us ( sx 10^-6) = 0,008 ms
0,008 ms This is the time period that each count takes to increment in TCNT0 register
N setps to: 1 ms '= OCR0A = 1/0.008= 125,00 steps
ERROR:
- This value depends of number of code executations:
o programa precis de pelo menos 1 turn completo á velocide constante para poder tirar um bom valor
Lido - esperado - esperado/lido - lido/esperado
14,6 - 15 - 1,0274 - 0,973
29,2 - 30 - 1,0274 - 0,973
58,4 - 60 - 1,0274 - 0,973
116,8 - 120 - 1,0274 - 0,973
Mediana = 1,0274 - 0,973
O erro entre a leitura e o esperado é constante
*/
/*
SERVO + PWM
//https://circuitdigest.com/microcontroller-projects/programming-attiny13-with-arduino-uno-control-a-servo-motor
*/