#include "stm32f103xb.h"
#include "stm32f103_hal.h"
#include "lcd.h"
#include <stdio.h>
// ===============================================================
// Variables globales (Añadir a las existentes)
// ===============================================================
volatile uint16_t last_capture_val = 0; // Guardará el valor anterior del Timer 1
volatile uint32_t t_hueco_us = 0; // Tiempo que tarda en pasar 1 solo hueco
volatile uint32_t sys_ticks = 0;
volatile uint32_t timeout_counter = 0;
volatile uint32_t last_capture_ms = 0;
volatile uint32_t t_motor_us = 0; // Periodo del motor en microsegundos
volatile uint32_t rpm = 0; // Revoluciones por minuto
volatile uint32_t temp_c = 0; // Temperatura en °C
volatile uint32_t delta_t_us = 0; // Retardo de inyección calculado
// ===============================================================
// Funciones Base y SysTick
// ===============================================================
void delay_ms(uint16_t ms) {
uint32_t start = sys_ticks;
while ((sys_ticks - start) < ms);
}
void delay_us(uint16_t us) {
volatile uint32_t i = (us * 8) / 5;
while(i--) { __asm__("nop"); }
}
void SysTick_Handler(void) {
sys_ticks++;
timeout_counter++;
// Si pasan más de 3000 ms (3 segundos) sin pulsos, el motor está apagado
if (timeout_counter > 3000) {
rpm = 0;
t_motor_us = 0;
delta_t_us = 0;
}
}
// ===============================================================
// ISR 1: Señal de 0° (Sensor Hall en PB1)
// ===============================================================
void EXTI1_IRQHandler(void) {
if (EXTI->PR & EXTI_PR_PR1) {
EXTI->PR = EXTI_PR_PR1;
// ¡Ya no necesitamos apagar TIM4 aquí! OPM lo hizo automáticamente.
if (t_motor_us >= 400) {
uint32_t delay_90_us = t_motor_us / 4;
uint32_t arr_val = delay_90_us / 100;
if (arr_val > 0) {
TIM3->ARR = arr_val;
TIM3->CNT = 0;
TIM3->CR1 |= TIM_CR1_CEN;
}
}
}
}
// ===============================================================
// ISR 2: Encoder Óptico (Con Filtro Automotriz Anti-Aliasing)
// ===============================================================
void EXTI9_5_IRQHandler(void) {
static uint8_t lag_counter = 0;
if (EXTI->PR & EXTI_PR_PR8) {
EXTI->PR = EXTI_PR_PR8; // Limpiar la bandera
uint16_t current_capture = TIM1->CNT; // Leer el cronómetro
// 1. Medir el tiempo "crudo" del hueco
uint32_t raw_hueco_us;
if (current_capture >= last_capture_val) {
raw_hueco_us = current_capture - last_capture_val;
} else {
raw_hueco_us = (0xFFFF - last_capture_val) + current_capture + 1;
}
last_capture_val = current_capture;
// 2. Filtro de Rechazo (Anti-Lag de Wokwi)
// Si el tiempo leído es razonable (< 60ms para evitar desbordes)
if (raw_hueco_us > 0 && raw_hueco_us < 60000) {
// Si el motor estaba apagado, o el tiempo es similar al anterior,
// o si el simulador lleva mucho rato dándonos lecturas largas (el usuario bajó el slider)
if (t_hueco_us == 0 || raw_hueco_us < (t_hueco_us + (t_hueco_us / 2)) || lag_counter > 5) {
// Aceptamos la lectura y la suavizamos un poco (Promedio Móvil)
t_hueco_us = (t_hueco_us * 3 + raw_hueco_us) / 4;
lag_counter = 0; // Reseteamos el contador de anomalías
} else {
// Si el tiempo es ABSURDAMENTE grande comparado con el anterior,
// significa que Wokwi se comió un pulso. ¡Lo ignoramos!
lag_counter++;
}
// 3. Cálculos matemáticos finales con la señal filtrada
t_motor_us = t_hueco_us * 180;
if (t_motor_us > 0) {
rpm = 60000000 / t_motor_us;
timeout_counter = 0; // El motor está girando
}
}
}
}
// ===============================================================
// ISR: TIM3 - Alcanzamos el TDC (90°), Disparar retardo Delta T
// ===============================================================
void TIM3_IRQHandler(void) {
if (TIM3->SR & TIM_SR_UIF) {
TIM3->SR &= ~TIM_SR_UIF;
// --- ¡EL SECRETO! Apagar el TIM3 para que no se repita ---
TIM3->CR1 &= ~TIM_CR1_CEN;
// Arrancamos TIM2 para el retardo de inyección
if (delta_t_us > 0) {
TIM2->ARR = delta_t_us / 100; // Pasamos us a ticks de 100us
TIM2->CNT = 0;
TIM2->CR1 |= TIM_CR1_CEN;
}
}
}
// ===============================================================
// ISR: TIM2 - Fin de Delta T, Disparar Inyector (TIM4)
// ===============================================================
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF;
// --- ¡EL SECRETO! Apagar el TIM2 para que no siga disparando ---
TIM2->CR1 &= ~TIM_CR1_CEN;
// Solo damos la orden de "Fuego". El TIM4 en One-Shot (OPM)
// emitirá su único pulso de 20% a 100kHz y se detendrá solo.
TIM4->CR1 |= TIM_CR1_CEN;
}
}
// ===============================================================
// Inicialización de TIM4 (PB6 - PWM One-Shot a 100 kHz, 20% DC)
// ===============================================================
void TIM4_Init(void) {
RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
// Configurar PB6 como Salida Alterna
GPIOB->CRL &= ~(0xF << 24);
GPIOB->CRL |= (0xB << 24);
// Frecuencia y Duty Cycle
TIM4->PSC = 0;
TIM4->ARR = 719; // 72MHz / 720 = 100 kHz
TIM4->CCR1 = 144; // Duty Cycle del 20% (el 20% de 720)
// Configurar Canal 1 en Modo PWM 1
TIM4->CCMR1 &= ~TIM_CCMR1_CC1S;
TIM4->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1;
TIM4->CCMR1 |= TIM_CCMR1_OC1PE;
// --- LA INSTRUCCIÓN DEL PROFESOR: MODO ONE-SHOT ---
TIM4->CR1 |= TIM_CR1_OPM; // Activar One-Pulse Mode
// Habilitar la salida
TIM4->CCER |= TIM_CCER_CC1E;
TIM4->EGR |= TIM_EGR_UG;
TIM4->CNT = 0; // Garantiza que inicie apagado
}
// ===============================================================
// Configuración Básica del ADC1
// ===============================================================
void ADC1_Init(void) {
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN | RCC_APB2ENR_IOPBEN;
// Configurar PB0 como entrada analógica (Canal 8)
GPIOB->CRL &= ~(0xF << 0);
ADC1->CR2 |= ADC_CR2_ADON;
delay_ms(1);
ADC1->CR2 |= ADC_CR2_CAL;
while (ADC1->CR2 & ADC_CR2_CAL);
ADC1->SQR3 = 8;
}
uint16_t ADC1_Read(void) {
ADC1->CR2 |= ADC_CR2_ADON;
while (!(ADC1->SR & ADC_SR_EOC));
return ADC1->DR;
}
// ===============================================================
// Función Principal
// ===============================================================
int main(void) {
// 1. Reloj del Systick (1ms exacto usando la HAL de tu profesor)
systick_config(999, SYSTICK_CLKSRC_AHB_DIV_8);
// 2. Habilitar relojes de periféricos (¡Añadimos IOPAEN y TIM1EN aquí!)
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPDEN | RCC_APB2ENR_TIM1EN;
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN | RCC_APB1ENR_TIM3EN | RCC_APB1ENR_TIM4EN;
// 3. Inicializar Pantalla LCD
LCD_Init();
LCD_SetCursor(0, 0);
LCD_Print("Arrancando ECU..");
delay_ms(1000);
LCD_Send(0x01, 0);
// ==========================================================
// 4. CONFIGURACIÓN DE LAS 2 SEÑALES DEL MOTOR (PB1 y PA8)
// ==========================================================
// A) PB1: Señal LENTA de 0° (EXTI1)
GPIOB->CRL &= ~(0xF << 4);
GPIOB->CRL |= (0x4 << 4);
AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI1;
AFIO->EXTICR[0] |= AFIO_EXTICR1_EXTI1_PB;
EXTI->IMR |= EXTI_IMR_MR1;
EXTI->RTSR |= EXTI_RTSR_TR1;
nvic_enable_irq(EXTI1_IRQn);
// B) PA8: Señal RÁPIDA del Encoder de 180 huecos (EXTI8)
GPIOA->CRH &= ~(0xF << 0);
GPIOA->CRH |= (0x8 << 0); // Entrada con Pull-Up (Mejor para Wokwi)
GPIOA->ODR |= (1 << 8);
AFIO->EXTICR[2] &= ~AFIO_EXTICR3_EXTI8;
AFIO->EXTICR[2] |= AFIO_EXTICR3_EXTI8_PA;
EXTI->IMR |= EXTI_IMR_MR8;
EXTI->RTSR |= EXTI_RTSR_TR8; // Flanco de subida
nvic_enable_irq(EXTI9_5_IRQn); // Habilitar la interrupción que atiende del EXTI5 al EXTI9
// C) TIM1: Solo como cronómetro libre para medir el tiempo de PA8
TIM1->PSC = 8 - 1; // Reloj interno a 8MHz, nos da ticks exactos de 1 microsegundo
TIM1->ARR = 0xFFFF; // Que cuente hasta el máximo libremente
TIM1->CR1 |= TIM_CR1_CEN; // Arrancar el cronómetro
// ==========================================================
// 5. CONFIGURACIÓN DE TIMERS DE INYECCIÓN (TDC y Dt)
// ==========================================================
// TIM3: Retardo TDC de 90° (Tick adaptado a 100us)
timer_init(TIM3, TIMER_MODE_UP, 0xFFFF, 800-1);
TIM3->CR1 |= TIM_CR1_OPM; // One-Pulse Mode (Se detiene al terminar)
timer_set_interrupt(TIM3, TIMER_IRQ_UPDATE, IRQ_ENABLE);
nvic_enable_irq(TIM3_IRQn);
// TIM2: Retardo de inyección Dt (Tick adaptado a 100us)
timer_init(TIM2, TIMER_MODE_UP, 0xFFFF, 800-1);
TIM2->CR1 |= TIM_CR1_OPM;
timer_set_interrupt(TIM2, TIMER_IRQ_UPDATE, IRQ_ENABLE);
nvic_enable_irq(TIM2_IRQn);
// TIM4: Pulso de Inyección (100kHz, 20% DC)
// Se mantiene súper rápido (Tick a velocidad máxima)
timer_init(TIM4, TIMER_MODE_UP, 79, 0);
TIM4->CR1 |= TIM_CR1_OPM;
timer_set_oc_channel(TIM4, TIMER_CHANNEL_1, TIMER_OC_PWM1, TIMER_OC_POLARITY_HIGH, 16, true);
// Configurar PB6 como salida (Ahí es donde el hardware saca el TIM4_CH1)
GPIOB->CRL &= ~(0xF << 24);
GPIOB->CRL |= (0xB << 24);
// ==========================================================
// 6. INICIALIZAR ADC Y ARRANCAR EL SISTEMA
// ==========================================================
ADC1_Init();
nvic_enable(); // Libera las interrupciones globales
// ==============================================================
// BUCLE PRINCIPAL (Cálculos matemáticos y LCD)
// ==============================================================
uint32_t last_lcd_update = 0;
char buffer[20];
while (1) {
// Actualizar datos cada 500ms
if (sys_ticks - last_lcd_update >= 500) {
last_lcd_update = sys_ticks;
uint16_t adc_val = ADC1_Read();
temp_c = (adc_val * 500) / 4095;
// Cálculo del retardo usando la fórmula exacta del examen
// Cálculo del retardo usando la fórmula original del examen
if (t_motor_us > 0) {
// 1. Calculamos los límites base (T/4 y T/2)
uint32_t t_cuarto = t_motor_us / 4;
uint32_t t_mitad = t_motor_us / 2;
// 2. Diferencia de temperatura (85°C es el ideal)
int32_t temp_diff = 85 - (int32_t)temp_c;
// 3. Fórmula exacta (FORMA ORIGINAL): dt = (T/4) * (1 + 200 * (85 - T))
int32_t dt_calc = t_cuarto * (1 + (200 * temp_diff));
// 4. Candados de seguridad corregidos contra el truco de C
if (dt_calc < (int32_t)t_cuarto) {
delta_t_us = t_cuarto;
} else if (dt_calc > (int32_t)t_mitad) {
delta_t_us = t_mitad;
} else {
delta_t_us = (uint32_t)dt_calc;
}
} else {
delta_t_us = 0;
}
// Mostrar métricas en la LCD
sprintf(buffer, "R:%lu T:%luC ", rpm, temp_c);
LCD_SetCursor(0, 0);
LCD_Print(buffer);
sprintf(buffer, "Dt:%lu us ", delta_t_us);
LCD_SetCursor(0, 1);
LCD_Print(buffer);
}
}
}