#include <xc.h>
#include <stdint.h>
/* CONFIG1 */
#pragma config FEXTOSC = OFF // no external oscillator
#pragma config RSTOSC = HFINTOSC_32MHz // Fosc = 32 MHz internal (max for this chip)
#pragma config CLKOUTEN = OFF // OSC2 is a normal I/O pin
/* CONFIG2 */
#pragma config WDTE = OFF // watchdog off
#pragma config MCLRE = EXTMCLR // MCLR pin enabled
#pragma config LVP = ON // low-voltage programming (on-board debugger)
#define _XTAL_FREQ 32000000UL // must match Fosc -> used by __delay_us()
/* ------------------------------------------------------------------ wiring */
#define LED_ON() (LATCbits.LATC2 = 0) /* active low */
#define LED_OFF() (LATCbits.LATC2 = 1)
#define BTN_IS_DOWN() (PORTCbits.RC3 == 0) /* active low */
/* ---------------------------------------------------------------- tunables */
/*
* The tick body (read port, debounce, bump counter, call/return) is roughly
* 25 instructions. At 32 MHz an instruction is 125 ns, so that is only about
* 3 us out of every millisecond -- hence this small trim.
*
* Getting it slightly wrong does not affect your score: the demo beat and your
* taps are counted with the same tick and the score is a ratio, so the error
* cancels exactly. It only shifts the printed BPM numbers. If a metronome says
* the beat runs fast, raise this; if slow, lower it.
*/
#define TICK_TRIM_US 3u
#define DEBOUNCE_MS 8u /* stable samples needed to accept a change */
/*
* Each round picks a random LENGTH as well as a random tempo. The board plays
* `beats` blinks and you tap back the same number -- so the demo itself tells
* you how many taps to give, and the game stays playable with no terminal.
*
* Fewer beats means fewer intervals, and fewer intervals make the average
* noisier: at 3 beats the score rests on just 2 intervals, so one twitchy tap
* hurts much more. That is the difficulty knob -- short rounds are harder.
*/
#define MIN_TAPS 3u
#define MAX_TAPS 6u
#define BLINK_MS 60u /* LED on-time for one demo beat */
#define ACK_MS 35u /* LED on-time acknowledging your tap */
#define LEAD_IN_MS 800u /* pause after start before beat 1 */
#define GAP_MS 700u /* pause between the demo and your turn */
#define FIRST_TAP_TIMEOUT 4000u /* how long we wait for your first tap */
#define TAP_TIMEOUT 2500u /* ...and for each tap after that */
#define TEMPO_ZERO_ERR_X10 200u /* 20.0% tempo error scores 0 */
#define STEADY_ZERO_DEV_X10 200u /* 20.0% wobble scores 0 */
/* Tempos the game can pick, as milliseconds per beat: 60..160 BPM. */
static const uint16_t TEMPO_MS[] = {
1000, 857, 750, 667, 600, 545, 500, 462, 429, 400, 375
};
#define TEMPO_COUNT (sizeof TEMPO_MS / sizeof TEMPO_MS[0])
/* ------------------------------------------------------- clock + button I/O */
static uint16_t g_ms; /* free-running millisecond counter (wraps at 65.5 s) */
static uint8_t btn_level; /* debounced state: 1 = pressed */
static uint8_t btn_count; /* consecutive samples disagreeing with btn_level */
static uint8_t btn_edge; /* latched "a press happened", cleared by took_press() */
/**
* @brief Advance the game clock by one millisecond and sample the button.
*
* Everything that waits goes through this function, so the clock keeps running
* and no press is ever missed, whatever the game is doing.
*/
static void ms_tick(void)
{
uint8_t raw = BTN_IS_DOWN() ? 1u : 0u;
if (raw != btn_level)
{
if (++btn_count >= DEBOUNCE_MS) /* stable long enough -> believe it */
{
btn_level = raw;
btn_count = 0;
if (raw)
btn_edge = 1; /* rising edge = a fresh press */
}
}
else
{
btn_count = 0; /* noise: restart the count */
}
g_ms++;
__delay_us(1000u - TICK_TRIM_US);
}
/** @brief Consume a latched press. Returns 1 if there was one. */
static uint8_t took_press(void)
{
if (btn_edge) { btn_edge = 0; return 1u; }
return 0u;
}
static void wait_ms(uint16_t n)
{
while (n--)
ms_tick();
}
/** @brief Wait up to n ms, stopping early on a press. Returns 1 if pressed. */
static uint8_t wait_ms_or_press(uint16_t n)
{
while (n--)
{
ms_tick();
if (btn_edge)
return 1u;
}
return 0u;
}
/** @brief Wait for a press. Returns 0 if it never came within timeout_ms. */
static uint8_t wait_press(uint16_t timeout_ms)
{
while (timeout_ms--)
{
if (took_press())
return 1u;
ms_tick();
}
return 0u;
}
static void blink(uint16_t on_ms, uint16_t off_ms)
{
LED_ON(); wait_ms(on_ms);
LED_OFF(); wait_ms(off_ms);
}
static void ack_flash(void)
{
LED_ON(); wait_ms(ACK_MS);
LED_OFF();
}
/* -------------------------------------------------------------- EUSART1 (TX) */
/**
* @brief EUSART1: 9600 baud, 8-N-1, transmit only, out on pin RC4.
*
* Baud, from datasheet Table 31-2 with SYNC=0, BRG16=1, BRGH=1:
* baud = Fosc / (4 * (n + 1))
* At 32 MHz, n = 832 gives 9604 baud -- 0.04% off, comfortably inside tolerance.
*
* NOTE: n depends on Fosc. Changing the clock silently breaks the serial output
* unless n is recomputed.
*/
static void uart_init(void)
{
/* TX pin: RC4 digital output, then route EUSART1 TX onto it. */
ANSELCbits.ANSELC4 = 0; /* digital, not analog */
TRISCbits.TRISC4 = 0; /* output driver enabled (required for PPS out) */
/* PPS output code 0x13 = TX1/CK1 (datasheet Table 18-2). PPSLOCKED is clear
at reset on this device, so the write lands; lock it afterwards so stray
writes cannot re-route the pin. */
RC4PPS = 0x13;
PPSLOCK = 0x55; /* required sequence */
PPSLOCK = 0xAA;
PPSLOCKbits.PPSLOCKED = 1;
SP1BRGL = 0x40; /* n = 832 = 0x0340 */
SP1BRGH = 0x03;
BAUD1CON = 0x08; /* BRG16 = 1 (16-bit baud generator) */
TX1STA = 0x24; /* TXEN = 1, BRGH = 1, SYNC = 0 (asynchronous) */
RC1STA = 0x80; /* SPEN = 1: serial port enabled */
}
/**
* @brief Send one byte. Gives up after a short spin rather than blocking.
*
* A dead or mis-clocked UART must never be able to freeze the game -- the LED is
* the primary display and has to keep working with no terminal attached at all.
*/
static void uart_write(char c)
{
uint16_t guard = 20000u;
while (!PIR4bits.TX1IF && --guard)
;
TX1REG = (uint8_t)c;
}
static void print(const char *s)
{
while (*s)
uart_write(*s++);
}
static void print_u16(uint16_t v)
{
char buf[6];
uint8_t i = 0;
if (v == 0u) { uart_write('0'); return; }
while (v) { buf[i++] = (char)('0' + (v % 10u)); v /= 10u; }
while (i) uart_write(buf[--i]);
}
/** @brief Print right-aligned in a field of `width`, so columns line up. */
static void print_u16_w(uint16_t v, uint8_t width)
{
uint16_t t = v;
uint8_t digits = 1;
while (t >= 10u) { t /= 10u; digits++; }
while (digits < width) { uart_write(' '); digits++; }
print_u16(v);
}
/** @brief Print a value held in tenths: 84 -> "8.4". */
static void print_tenths(uint16_t v)
{
print_u16((uint16_t)(v / 10u));
uart_write('.');
uart_write((char)('0' + (v % 10u)));
}
static void print_bar(uint8_t score)
{
uint8_t i, filled = (uint8_t)(score / 10u); /* 0..10 */
uart_write('[');
for (i = 0; i < 10u; i++)
uart_write(i < filled ? '#' : '.');
uart_write(']');
}
static void nl(void)
{
print("\r\n");
}
/* ------------------------------------------------------------ tempo picking */
/* 16-bit xorshift: tiny, and plenty random for choosing a tempo. */
static uint16_t rnd_state = 0xACE1u;
static uint16_t rnd_next(void)
{
rnd_state ^= (uint16_t)(rnd_state << 7);
rnd_state ^= (uint16_t)(rnd_state >> 9);
rnd_state ^= (uint16_t)(rnd_state << 8);
return rnd_state;
}
/**
* @brief Stir in some entropy -- we use human reaction time, which is the only
* genuinely unpredictable thing a bare microcontroller has to hand.
*/
static void rnd_mix(uint16_t entropy)
{
rnd_state ^= entropy;
if (rnd_state == 0u)
rnd_state = 0xACE1u; /* xorshift must never sit at zero */
(void)rnd_next();
}
/* ------------------------------------------------------------------ scoring */
/** @brief Turn an error (in 0.1% units) into 0..100, reaching 0 at `zero_at`. */
static uint8_t score_from_err(uint16_t err_x10, uint16_t zero_at)
{
if (err_x10 >= zero_at)
return 0u;
return (uint8_t)(100u - (((uint32_t)err_x10 * 100u) / zero_at));
}
static const char *grade_text(uint8_t s)
{
if (s >= 90u) return "PERFECT";
if (s >= 75u) return "GREAT";
if (s >= 60u) return "GOOD";
if (s >= 40u) return "OK";
return "OFF BEAT";
}
/**
* @brief Blink the grade, so the game is playable with no terminal attached.
* 5 fast blinks = PERFECT ... 1 long blink = OFF BEAT.
*/
static void celebrate(uint8_t score, uint8_t new_best)
{
uint8_t i, n;
if (score >= 90u) n = 5u;
else if (score >= 75u) n = 4u;
else if (score >= 60u) n = 3u;
else if (score >= 40u) n = 2u;
else n = 0u; /* 0 means "one long sad blink" */
if (n == 0u)
{
blink(700u, 300u);
}
else
{
for (i = 0; i < n; i++)
blink(70u, 110u);
}
if (new_best) /* a quick flutter = new personal best */
{
wait_ms(250u);
for (i = 0; i < 6u; i++)
blink(30u, 50u);
}
}
/* --------------------------------------------------------------------- setup */
static void board_init(void)
{
/* LED0 on RC2: digital output, start dark. */
ANSELCbits.ANSELC2 = 0;
TRISCbits.TRISC2 = 0;
LED_OFF();
/* SW0 on RC3: digital input with the internal pull-up. The schematic notes
no pull-ups are fitted on the board, so without WPU the pin floats.
ANSELC3 must be cleared too or the PORT read always returns 0. */
ANSELCbits.ANSELC3 = 0;
TRISCbits.TRISC3 = 1;
WPUCbits.WPUC3 = 1;
}
/**
* @brief Three unmistakable blinks at power-up, before anything else runs.
*
* Deliberately uses NOTHING but __delay_ms(): no millisecond tick, no button,
* no serial. So if these three blinks appear, the chip is programmed, running,
* and driving RC2 correctly, and the game can be trusted to at least start.
*
* This is the "flash a slow blink first" rule from the breathing-LED lesson,
* generalised: never let a peripheral hold up the first sign of life. Their
* SPEED also reports the clock -- three blinks inside about a second means Fosc
* is the intended 32 MHz.
*/
static void proof_of_life(void)
{
uint8_t i;
for (i = 0; i < 3u; i++)
{
LED_ON(); __delay_ms(120);
LED_OFF(); __delay_ms(180);
}
__delay_ms(400);
}
/**
* @brief Attract mode: heartbeat the LED until SW0 is pressed.
*
* Stays SILENT on the serial line while waiting -- the LED heartbeat is the
* "I'm alive" indicator, and the log only records things that actually happen.
* Consequence to remember: opening a terminal does not reset the board, so a
* freshly-opened terminal shows nothing until you press SW0. That is normal.
*/
static void attract_until_press(void)
{
(void)took_press(); /* ignore a stale press */
for (;;)
{
LED_ON();
if (wait_ms_or_press(120u)) break; /* a clear double-thump, not a blip */
LED_OFF();
if (wait_ms_or_press(180u)) break;
LED_ON();
if (wait_ms_or_press(120u)) break;
LED_OFF();
if (wait_ms_or_press(900u)) break;
}
LED_OFF();
(void)took_press();
}
/* ---------------------------------------------------------------------- game */
void main(void)
{
uint16_t round_no = 0;
uint8_t best = 0;
board_init();
proof_of_life(); /* LED first: prove we are alive before any peripheral */
uart_init();
nl();
print("=== GUESS THE TEMPO ===\r\n");
print("The LED taps out a beat -- tap SW0 back at the\r\n");
print("same speed. Only the spacing between your taps\r\n");
print("is scored, not when you start.\r\n");
/* Stating the range here also fingerprints the build: seeing this line
proves the variable-length firmware is the one actually running. */
print("Rounds are ");
print_u16(MIN_TAPS);
print("-");
print_u16(MAX_TAPS);
print(" beats -- count the blinks, tap back the same number.\r\n");
for (;;)
{
uint16_t iv[MAX_TAPS - 1u]; /* your tap-to-tap intervals, ms */
uint16_t target, avg, err_x10, dev_x10;
uint16_t sum = 0, worst = 0;
uint16_t t_prev = 0, t_now;
uint8_t beats; /* this round's length, MIN_TAPS..MAX_TAPS */
uint8_t i, tempo_score, steady_score, total;
uint8_t timed_out = 0;
nl();
print("Waiting -- press SW0.\r\n");
attract_until_press();
rnd_mix(g_ms); /* entropy: when exactly you pressed */
round_no++;
target = TEMPO_MS[rnd_next() % TEMPO_COUNT];
beats = (uint8_t)(MIN_TAPS + (rnd_next() % (MAX_TAPS - MIN_TAPS + 1u)));
print("START round ");
print_u16(round_no);
print(", ");
print_u16(beats);
print(" beats at a secret tempo -- count them\r\n");
wait_ms(LEAD_IN_MS);
for (i = 0; i < beats; i++)
blink(BLINK_MS, (uint16_t)(target - BLINK_MS));
/* Cue: two quick flashes = your turn. */
wait_ms(GAP_MS);
blink(30u, 120u);
blink(30u, 120u);
print("MY TURN tap SW0 ");
print_u16(beats);
print(" times\r\n");
print(" ");
(void)took_press(); /* drop anything tapped during the demo */
if (!wait_press(FIRST_TAP_TIMEOUT))
{
timed_out = 1;
}
else
{
t_prev = g_ms; /* first tap only starts the stopwatch */
uart_write('.'); /* live feedback: one dot per tap */
ack_flash();
for (i = 0; i < beats - 1u; i++)
{
if (!wait_press(TAP_TIMEOUT)) { timed_out = 1; break; }
t_now = g_ms;
iv[i] = (uint16_t)(t_now - t_prev); /* wrap-safe subtraction */
t_prev = t_now;
uart_write('.');
ack_flash();
}
}
nl();
if (timed_out)
{
print("STOP no tap in time -- round cancelled\r\n");
blink(600u, 400u); /* one long blink = round cancelled */
continue;
}
print("STOP ");
print_u16((uint16_t)(beats - 1u));
print(" intervals captured\r\n");
/* --- how fast were you, on average? --- */
for (i = 0; i < beats - 1u; i++)
sum += iv[i];
avg = (uint16_t)(sum / (beats - 1u));
/* --- and how steady? worst single interval vs your own average --- */
for (i = 0; i < beats - 1u; i++)
{
uint16_t d = (iv[i] > avg) ? (uint16_t)(iv[i] - avg)
: (uint16_t)(avg - iv[i]);
if (d > worst)
worst = d;
}
err_x10 = (uint16_t)(((uint32_t)((avg > target) ? (avg - target)
: (target - avg))
* 1000UL) / target);
dev_x10 = (uint16_t)(((uint32_t)worst * 1000UL) / avg);
tempo_score = score_from_err(err_x10, TEMPO_ZERO_ERR_X10);
steady_score = score_from_err(dev_x10, STEADY_ZERO_DEV_X10);
total = (uint8_t)(((uint16_t)tempo_score * 3u + steady_score) / 4u);
/* --- the scorecard --- */
print("GRADE ");
print_u16_w(total, 3u);
print("/100 ");
print(grade_text(total));
nl();
print(" target ");
print_u16_w((uint16_t)(60000UL / target), 3u);
print(" BPM (");
print_u16(target);
print(" ms)\r\n");
print(" you ");
print_u16_w((uint16_t)(60000UL / avg), 3u);
print(" BPM (");
print_u16(avg);
print(" ms) ");
print_tenths(err_x10);
print("% ");
if (avg < target) print("FASTER\r\n");
else if (avg > target) print("SLOWER\r\n");
else print("SPOT ON\r\n");
print(" taps ");
for (i = 0; i < beats - 1u; i++)
{
uart_write(' ');
print_u16(iv[i]);
}
print(" ms\r\n");
print(" tempo ");
print_u16_w(tempo_score, 3u);
print("/100 ");
print_bar(tempo_score);
nl();
print(" steady ");
print_u16_w(steady_score, 3u);
print("/100 ");
print_bar(steady_score);
nl();
if (total > best)
{
best = total;
print(" best ");
print_u16_w(best, 3u);
print("/100 NEW BEST!\r\n");
celebrate(total, 1u);
}
else
{
print(" best ");
print_u16_w(best, 3u);
print("/100\r\n");
celebrate(total, 0u);
}
}
}
Comments