/*
* Snake – ESP32-C6 + 4× MAX7219 (32×8) + analog joystick
* ESP-IDF (C11)
*
* Kopplingar (från diagram.json):
\\ 8x8 LED Dot Matrix with MAX7219 Controller
* DIN → GPIO23 - Data input
* CLK → GPIO18 - Clock input
* CS → GPIO5 - Chip Select
\\ Analog Joystick
* HORZ → GPIO4 (ADC) - Horizontal axis output (analog)
* VERT → GPIO3 (ADC) - Vertical axis output (analog)
* SEL → GPIO2 (joystick-knapp, tryck = reset)
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_random.h"
/* ── Pinnar ─────────────────────────────────────────────────────── */
#define PIN_DIN 23
#define PIN_CLK 18
#define PIN_CS 5
#define PIN_HORZ ADC_CHANNEL_4 /* GPIO4 */
#define PIN_VERT ADC_CHANNEL_3 /* GPIO3 */
#define PIN_SEL 2 /* joystick-knapp */
/* ── Spelplan: 32 kolumner × 8 rader (4 st 8×8-matriser) ─────── */
#define NUM_MATRICES 4
#define COLS (NUM_MATRICES * 8) /* 32 */
#define ROWS 8
#define MAX_LEN (COLS * ROWS)
/* ── MAX7219-register ────────────────────────────────────────────── */
#define REG_NOOP 0x00
#define REG_DIGIT0 0x01
#define REG_DECODEMODE 0x09
#define REG_INTENSITY 0x0A
#define REG_SCANLIMIT 0x0B
#define REG_SHUTDOWN 0x0C
#define REG_DISPLAYTEST 0x0F
/* ── Riktningar ──────────────────────────────────────────────────── */
typedef enum { RIGHT = 0, DOWN, LEFT, UP } Dir;
/* ── Speldata ────────────────────────────────────────────────────── */
static struct { int8_t x, y; } snake[MAX_LEN];
static int snake_len;
static Dir dir;
static struct { int8_t x, y; } food;
static bool game_over;
/* ── Skärmbuffer: en uint32_t per rad, bit = kolumn ─────────────── */
static uint32_t fb[ROWS];
/* ── SPI & ADC handles ───────────────────────────────────────────── */
static spi_device_handle_t spi;
static adc_oneshot_unit_handle_t adc_handle;
/* ═══════════════════════════════════════════════════════════════════
* MAX7219 – SPI
* ═══════════════════════════════════════════════════════════════════ */
/*
* Skicka ett kommando till ALLA 4 matriser (samma reg + data).
* Kedjan: matris4 → matris3 → matris2 → matris1 (vänster = sist ut)
*/
static void max_send_all(uint8_t reg, uint8_t data)
{
uint8_t buf[NUM_MATRICES * 2];
for (int i = 0; i < NUM_MATRICES; i++) {
buf[i * 2] = reg;
buf[i * 2 + 1] = data;
}
spi_transaction_t t = {
.length = NUM_MATRICES * 16,
.tx_buffer = buf,
};
gpio_set_level(PIN_CS, 0);
spi_device_polling_transmit(spi, &t);
gpio_set_level(PIN_CS, 1);
}
/*
* Skicka en hel rad till alla 4 matriser på en gång.
* fb-biten 0 = vänster kolumn (matris 1), bit 31 = höger kolumn (matris 4).
* MAX7219 i kedja: sista byten i buffern hamnar i matris 1.
*/
static void max_send_row(uint8_t row, uint32_t pixels)
{
uint8_t buf[NUM_MATRICES * 2];
for (int m = 0; m < NUM_MATRICES; m++) {
/* matris m hanterar kolumnerna m*8 .. m*8+7 */
uint8_t byte = (pixels >> (m * 8)) & 0xFF;
/* Kedjan är omvänd: matris 0 (vänster) skickas sist */
int idx = (NUM_MATRICES - 1 - m) * 2;
buf[idx] = REG_DIGIT0 + row;
buf[idx + 1] = byte;
}
spi_transaction_t t = {
.length = NUM_MATRICES * 16,
.tx_buffer = buf,
};
gpio_set_level(PIN_CS, 0);
spi_device_polling_transmit(spi, &t);
gpio_set_level(PIN_CS, 1);
}
static void max_init(void)
{
max_send_all(REG_DISPLAYTEST, 0x00);
max_send_all(REG_SCANLIMIT, 0x07);
max_send_all(REG_DECODEMODE, 0x00);
max_send_all(REG_INTENSITY, 0x03);
max_send_all(REG_SHUTDOWN, 0x01);
}
static void max_flush(void)
{
for (int row = 0; row < ROWS; row++) {
max_send_row(row, fb[row]);
}
}
/* ═══════════════════════════════════════════════════════════════════
* SPI-init
* ═══════════════════════════════════════════════════════════════════ */
static void spi_init(void)
{
gpio_reset_pin(PIN_CS);
gpio_set_direction(PIN_CS, GPIO_MODE_OUTPUT);
gpio_set_level(PIN_CS, 1);
spi_bus_config_t buscfg = {
.mosi_io_num = PIN_DIN,
.miso_io_num = -1,
.sclk_io_num = PIN_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
spi_device_interface_config_t devcfg = {
.clock_speed_hz = 1000000,
.mode = 0,
.spics_io_num = -1,
.queue_size = 1,
};
spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
spi_bus_add_device(SPI2_HOST, &devcfg, &spi);
}
/* ═══════════════════════════════════════════════════════════════════
* ADC-init (ESP-IDF oneshot API)
* ═══════════════════════════════════════════════════════════════════ */
static void adc_init_joystick(void)
{
adc_oneshot_unit_init_cfg_t init_cfg = {
.unit_id = ADC_UNIT_1,
};
adc_oneshot_new_unit(&init_cfg, &adc_handle);
adc_oneshot_chan_cfg_t chan_cfg = {
.bitwidth = ADC_BITWIDTH_12,
.atten = ADC_ATTEN_DB_12,
};
adc_oneshot_config_channel(adc_handle, PIN_HORZ, &chan_cfg);
adc_oneshot_config_channel(adc_handle, PIN_VERT, &chan_cfg);
}
/* ── GPIO för joystick-knapp ─────────────────────────────────────── */
static void gpio_init_sel(void)
{
gpio_config_t cfg = {
.pin_bit_mask = (1ULL << PIN_SEL),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&cfg);
}
/* ═══════════════════════════════════════════════════════════════════
* Spellogik
* ═══════════════════════════════════════════════════════════════════ */
static void place_food(void)
{
while (1) {
int8_t fx = esp_random() % COLS;
int8_t fy = esp_random() % ROWS;
bool ok = true;
for (int i = 0; i < snake_len; i++) {
if (snake[i].x == fx && snake[i].y == fy) { ok = false; break; }
}
if (ok) { food.x = fx; food.y = fy; return; }
}
}
static void game_init(void)
{
snake_len = 4;
dir = RIGHT;
game_over = false;
for (int i = 0; i < snake_len; i++) {
snake[i].x = (COLS / 2) - i;
snake[i].y = ROWS / 2;
}
place_food();
}
/* Läs joystick → uppdatera riktning, returnera steghastighet (ms) */
static int read_joystick(void)
{
int hv, vv;
adc_oneshot_read(adc_handle, PIN_HORZ, &hv);
adc_oneshot_read(adc_handle, PIN_VERT, &vv);
int dx = hv - 2048;
int dy = vv - 2048;
if (abs(dx) > abs(dy)) {
if (dx > 600 && dir != LEFT) dir = RIGHT;
if (dx < -600 && dir != RIGHT) dir = LEFT;
} else {
if (dy > 600 && dir != UP) dir = DOWN;
if (dy < -600 && dir != DOWN) dir = UP;
}
/* Analog hastighet: 600–2048 utslag → 350–80 ms */
int deflect = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
if (deflect < 600) return 350;
int speed = 350 - (int)((deflect - 600) * 270 / 1448);
return (speed < 80) ? 80 : speed;
}
static void game_step(void)
{
for (int i = snake_len - 1; i > 0; i--)
snake[i] = snake[i - 1];
switch (dir) {
case RIGHT: snake[0].x++; break;
case LEFT: snake[0].x--; break;
case DOWN: snake[0].y++; break;
case UP: snake[0].y--; break;
}
if (snake[0].x < 0 || snake[0].x >= COLS ||
snake[0].y < 0 || snake[0].y >= ROWS) {
game_over = true; return;
}
for (int i = 1; i < snake_len; i++) {
if (snake[i].x == snake[0].x && snake[i].y == snake[0].y) {
game_over = true; return;
}
}
if (snake[0].x == food.x && snake[0].y == food.y) {
if (snake_len < MAX_LEN) snake_len++;
place_food();
}
}
/* ═══════════════════════════════════════════════════════════════════
* Rendering
* ═══════════════════════════════════════════════════════════════════ */
static void render(void)
{
memset(fb, 0, sizeof(fb));
for (int i = 0; i < snake_len; i++)
fb[snake[i].y] |= (1UL << snake[i].x);
fb[food.y] |= (1UL << food.x);
max_flush();
}
static void anim_game_over(void)
{
for (int i = 0; i < 4; i++) {
memset(fb, 0xFF, sizeof(fb));
max_flush();
vTaskDelay(pdMS_TO_TICKS(250));
memset(fb, 0, sizeof(fb));
max_flush();
vTaskDelay(pdMS_TO_TICKS(250));
}
}
/* ═══════════════════════════════════════════════════════════════════
* app_main
* ═══════════════════════════════════════════════════════════════════ */
void app_main(void)
{
spi_init();
adc_init_joystick();
gpio_init_sel();
max_init();
while (1) {
game_init();
while (!game_over) {
/* Joystick-knapp = omedelbar reset */
if (gpio_get_level(PIN_SEL) == 0) break;
int delay_ms = read_joystick();
game_step();
render();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
anim_game_over();
vTaskDelay(pdMS_TO_TICKS(800));
}
}
Loading
esp32-c6-devkitc-1
esp32-c6-devkitc-1