/* --- SSD1306 OLED Driver (simplified, reuse from Lesson 4) --- */
#define SSD1306_ADDR 0x78
static I2C_HandleTypeDef hi2c1;
static SPI_HandleTypeDef hspi1;
/* 5x7 font covering printable ASCII from space (0x20) through 'Z' (0x5A) */
static const uint8_t Font5x7[][5] = {
{0x00, 0x00, 0x00, 0x00, 0x00}, /* ' ' (space) */
{0x00, 0x00, 0x5F, 0x00, 0x00}, /* '!' */
{0x00, 0x07, 0x00, 0x07, 0x00}, /* '"' */
{0x14, 0x7F, 0x14, 0x7F, 0x14}, /* '#' */
{0x24, 0x2A, 0x7F, 0x2A, 0x12}, /* '$' */
{0x23, 0x13, 0x08, 0x64, 0x62}, /* '%' */
{0x36, 0x49, 0x56, 0x20, 0x50}, /* '&' */
{0x00, 0x08, 0x07, 0x03, 0x00}, /* '\'' */
{0x00, 0x1C, 0x22, 0x41, 0x00}, /* '(' */
{0x00, 0x41, 0x22, 0x1C, 0x00}, /* ')' */
{0x2A, 0x1C, 0x7F, 0x1C, 0x2A}, /* '*' */
{0x08, 0x08, 0x3E, 0x08, 0x08}, /* '+' */
{0x00, 0x80, 0x70, 0x30, 0x00}, /* ',' */
{0x08, 0x08, 0x08, 0x08, 0x08}, /* '-' */
{0x00, 0x00, 0x60, 0x60, 0x00}, /* '.' */
{0x20, 0x10, 0x08, 0x04, 0x02}, /* '/' */
{0x3E, 0x51, 0x49, 0x45, 0x3E}, /* '0' */
{0x00, 0x42, 0x7F, 0x40, 0x00}, /* '1' */
{0x72, 0x49, 0x49, 0x49, 0x46}, /* '2' */
{0x21, 0x41, 0x49, 0x4D, 0x33}, /* '3' */
{0x18, 0x14, 0x12, 0x7F, 0x10}, /* '4' */
{0x27, 0x45, 0x45, 0x45, 0x39}, /* '5' */
{0x3C, 0x4A, 0x49, 0x49, 0x31}, /* '6' */
{0x41, 0x21, 0x11, 0x09, 0x07}, /* '7' */
{0x36, 0x49, 0x49, 0x49, 0x36}, /* '8' */
{0x46, 0x49, 0x49, 0x29, 0x1E}, /* '9' */
{0x00, 0x00, 0x14, 0x00, 0x00}, /* ':' */
{0x00, 0x40, 0x34, 0x00, 0x00}, /* ';' */
{0x00, 0x08, 0x14, 0x22, 0x41}, /* '<' */
{0x14, 0x14, 0x14, 0x14, 0x14}, /* '=' */
{0x00, 0x41, 0x22, 0x14, 0x08}, /* '>' */
{0x02, 0x01, 0x59, 0x09, 0x06}, /* '?' */
{0x3E, 0x41, 0x5D, 0x59, 0x4E}, /* '@' */
{0x7C, 0x12, 0x11, 0x12, 0x7C}, /* 'A' */
{0x7F, 0x49, 0x49, 0x49, 0x36}, /* 'B' */
{0x3E, 0x41, 0x41, 0x41, 0x22}, /* 'C' */
{0x7F, 0x41, 0x41, 0x41, 0x3E}, /* 'D' */
{0x7F, 0x49, 0x49, 0x49, 0x41}, /* 'E' */
{0x7F, 0x09, 0x09, 0x09, 0x01}, /* 'F' */
{0x3E, 0x41, 0x41, 0x51, 0x73}, /* 'G' */
{0x7F, 0x08, 0x08, 0x08, 0x7F}, /* 'H' */
{0x00, 0x41, 0x7F, 0x41, 0x00}, /* 'I' */
{0x20, 0x40, 0x41, 0x3F, 0x01}, /* 'J' */
{0x7F, 0x08, 0x14, 0x22, 0x41}, /* 'K' */
{0x7F, 0x40, 0x40, 0x40, 0x40}, /* 'L' */
{0x7F, 0x02, 0x1C, 0x02, 0x7F}, /* 'M' */
{0x7F, 0x04, 0x08, 0x10, 0x7F}, /* 'N' */
{0x3E, 0x41, 0x41, 0x41, 0x3E}, /* 'O' */
{0x7F, 0x09, 0x09, 0x09, 0x06}, /* 'P' */
{0x3E, 0x41, 0x51, 0x21, 0x5E}, /* 'Q' */
{0x7F, 0x09, 0x19, 0x29, 0x46}, /* 'R' */
{0x26, 0x49, 0x49, 0x49, 0x32}, /* 'S' */
{0x03, 0x01, 0x7F, 0x01, 0x03}, /* 'T' */
{0x3F, 0x40, 0x40, 0x40, 0x3F}, /* 'U' */
{0x1F, 0x20, 0x40, 0x20, 0x1F}, /* 'V' */
{0x3F, 0x40, 0x38, 0x40, 0x3F}, /* 'W' */
{0x63, 0x14, 0x08, 0x14, 0x63}, /* 'X' */
{0x03, 0x04, 0x78, 0x04, 0x03}, /* 'Y' */
{0x61, 0x59, 0x49, 0x4D, 0x43}, /* 'Z' */
static uint8_t oled_buffer[1024]; /* 128x64 / 8 = 1024 bytes */
static void SSD1306_Command(uint8_t cmd)
uint8_t data[2] = {0x00, cmd};
HAL_I2C_Master_Transmit(&hi2c1, SSD1306_ADDR, data, 2, 50);
static void SSD1306_Init(void)
SSD1306_Command(0xAE); /* Display off */
SSD1306_Command(0xD5); SSD1306_Command(0x80); /* Clock divide */
SSD1306_Command(0xA8); SSD1306_Command(0x3F); /* Multiplex 64 */
SSD1306_Command(0xD3); SSD1306_Command(0x00); /* Display offset */
SSD1306_Command(0x40); /* Start line */
SSD1306_Command(0x8D); SSD1306_Command(0x14); /* Charge pump on */
SSD1306_Command(0x20); SSD1306_Command(0x00); /* Horizontal addressing */
SSD1306_Command(0xA1); /* Segment remap */
SSD1306_Command(0xC8); /* COM scan direction */
SSD1306_Command(0xDA); SSD1306_Command(0x12); /* COM pins */
SSD1306_Command(0x81); SSD1306_Command(0xCF); /* Contrast */
SSD1306_Command(0xD9); SSD1306_Command(0xF1); /* Pre-charge */
SSD1306_Command(0xDB); SSD1306_Command(0x40); /* VCOMH deselect */
SSD1306_Command(0xA4); /* Resume from RAM */
SSD1306_Command(0xA6); /* Normal display */
SSD1306_Command(0xAF); /* Display on */
static void SSD1306_Clear(void)
memset(oled_buffer, 0, sizeof(oled_buffer));
static void SSD1306_Update(void)
SSD1306_Command(0x21); SSD1306_Command(0); SSD1306_Command(127);
SSD1306_Command(0x22); SSD1306_Command(0); SSD1306_Command(7);
for (uint16_t i = 0; i < 1024; i += 16) {
memcpy(&buf[1], &oled_buffer[i], 16);
HAL_I2C_Master_Transmit(&hi2c1, SSD1306_ADDR, buf, 17, 50);
static void SSD1306_SetPixel(uint8_t x, uint8_t y, uint8_t on)
if (x >= 128 || y >= 64) return;
if (on) oled_buffer[x + (y / 8) * 128] |= (1 << (y % 8));
else oled_buffer[x + (y / 8) * 128] &= ~(1 << (y % 8));
static void SSD1306_DrawChar(uint8_t x, uint8_t y, char c)
if (c < ' ' || c > 'Z') return;
for (uint8_t col = 0; col < 5; col++) {
uint8_t line = Font5x7[c - ' '][col];
for (uint8_t row = 0; row < 7; row++) {
SSD1306_SetPixel(x + col, y + row, (line >> row) & 1);
static void SSD1306_DrawString(uint8_t x, uint8_t y, const char *str)
SSD1306_DrawChar(x, y, *str);
/* --- Indicator control --- */
static void LED_Green(uint8_t on) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, on ? GPIO_PIN_SET : GPIO_PIN_RESET);
static void LED_Red(uint8_t on) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, on ? GPIO_PIN_SET : GPIO_PIN_RESET);
static void Buzzer(uint8_t on) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, on ? GPIO_PIN_SET : GPIO_PIN_RESET);
static void Beep_Short(void) { Buzzer(1); HAL_Delay(100); Buzzer(0); }
static void Beep_Long(void) { Buzzer(1); HAL_Delay(500); Buzzer(0); }
static void Beep_Double(void){ Beep_Short(); HAL_Delay(50); Beep_Short(); }
/* --- Access control data --- */
static uint8_t master_uid[UID_SIZE];
static uint8_t authorized_uids[MAX_CARDS][UID_SIZE];
static uint8_t num_authorized = 0;
static uint8_t master_registered = 0;
static AccessState_t state = STATE_IDLE;
static uint8_t UID_Match(uint8_t *uid1, uint8_t *uid2)
return (memcmp(uid1, uid2, UID_SIZE) == 0);
static int8_t Find_UID(uint8_t *uid)
for (uint8_t i = 0; i < num_authorized; i++) {
if (UID_Match(uid, authorized_uids[i])) return i;
static uint8_t Add_UID(uint8_t *uid)
if (num_authorized >= MAX_CARDS) return 0;
if (Find_UID(uid) >= 0) return 0; /* Already exists */
memcpy(authorized_uids[num_authorized], uid, UID_SIZE);
static uint8_t Remove_UID(uint8_t *uid)
int8_t idx = Find_UID(uid);
/* Shift remaining entries down */
for (uint8_t i = idx; i < num_authorized - 1; i++) {
memcpy(authorized_uids[i], authorized_uids[i + 1], UID_SIZE);
static void UID_ToHexStr(uint8_t *uid, char *str)
const char hex[] = "0123456789ABCDEF";
for (int i = 0; i < UID_SIZE; i++) {
str[i * 3] = hex[(uid[i] >> 4) & 0x0F];
str[i * 3 + 1] = hex[uid[i] & 0x0F];
str[i * 3 + 2] = (i < UID_SIZE - 1) ? ' ' : '\0';
str[UID_SIZE * 3 - 1] = '\0';
/* --- Display helpers --- */
static void Display_Idle(void)
/* Row 0: title area (we skip fancy text, just show UID area) */
/* Row 3: "SCAN CARD" in hex-drawable chars is limited,
so we draw nothing or a simple indicator */
static void Display_UID(uint8_t *uid, const char *status_hex)
UID_ToHexStr(uid, hex_str);
SSD1306_DrawString(10, 20, hex_str);
SSD1306_DrawString(10, 40, status_hex);
/* --- System clock and peripheral init (generated by CubeMX) --- */
static void SystemClock_Config(void)
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
static void MX_GPIO_Init(void)
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/* PA4: RC522 CS (output, high by default) */
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
/* PB0: RC522 RST, PB1: Buzzer, PB12: Green LED, PB13: Red LED */
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_12 | GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); /* RST high (active) */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET);
static void MX_SPI1_Init(void)
__HAL_RCC_SPI1_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* PA5: SCK, PA7: MOSI (AF push-pull) */
GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* PA6: MISO (input floating) */
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; /* 72/16 = 4.5 MHz */
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
static void MX_I2C1_Init(void)
__HAL_RCC_I2C1_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* PB6: SCL, PB7: SDA (AF open-drain) */
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
hi2c1.Init.ClockSpeed = 400000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
/* --- Main application --- */
uint32_t enroll_timeout = 0;
uint8_t enroll_active = 0;
/* Attempt to detect a card */
if (RC522_Request(PICC_REQIDL, tagType) == MI_OK) {
state = STATE_CARD_DETECTED;
case STATE_CARD_DETECTED:
/* Run anti-collision to get UID */
if (RC522_Anticoll(card_uid) == MI_OK) {
state = STATE_AUTHENTICATING;
case STATE_AUTHENTICATING:
/* First card ever becomes master */
if (!master_registered) {
memcpy(master_uid, card_uid, UID_SIZE);
Display_UID(card_uid, "1"); /* "1" = first card */
/* Check if this is the master card */
if (UID_Match(card_uid, master_uid)) {
enroll_active = !enroll_active;
enroll_timeout = HAL_GetTick() + 10000; /* 10 seconds */
Display_UID(card_uid, "E"); /* E = enroll mode */
Display_UID(card_uid, "0"); /* 0 = enroll off */
/* If in enroll mode, add or remove card */
if (enroll_active && HAL_GetTick() < enroll_timeout) {
if (Find_UID(card_uid) >= 0) {
Display_UID(card_uid, "D"); /* D = deleted */
Display_UID(card_uid, "A"); /* A = added */
Display_UID(card_uid, "F"); /* F = full */
/* Normal access check */
if (Find_UID(card_uid) >= 0) {
state = STATE_ACCESS_GRANTED;
state = STATE_ACCESS_DENIED;
case STATE_ACCESS_GRANTED:
Display_UID(card_uid, "ACC");
case STATE_ACCESS_DENIED:
Display_UID(card_uid, "DEN");
/* Auto-exit enroll mode after timeout */
if (enroll_active && HAL_GetTick() >= enroll_timeout) {
* NOTE: Do not define SysTick_Handler here. CubeMX already generates one
* in stm32f1xx_it.c that calls HAL_IncTick(). If you need to add custom
* tick-based logic, place it inside the existing SysTick_Handler in
* Core/Src/stm32f1xx_it.c rather than defining a duplicate here.
Comments