/* --- SSD1306 OLED Driver (simplified, reuse from Lesson 4) --- */
#define SSD1306_ADDR 0x78
static I2C_HandleTypeDef hi2c1;
static SPI_HandleTypeDef hspi1;
/* 5x7 font subset for hex digits and common characters */
static const uint8_t Font5x7[][5] = {
{0x3E,0x51,0x49,0x45,0x3E}, /* 0 */
{0x00,0x42,0x7F,0x40,0x00}, /* 1 */
{0x42,0x61,0x51,0x49,0x46}, /* 2 */
{0x21,0x41,0x45,0x4B,0x31}, /* 3 */
{0x18,0x14,0x12,0x7F,0x10}, /* 4 */
{0x27,0x45,0x45,0x45,0x39}, /* 5 */
{0x3C,0x4A,0x49,0x49,0x30}, /* 6 */
{0x01,0x71,0x09,0x05,0x03}, /* 7 */
{0x36,0x49,0x49,0x49,0x36}, /* 8 */
{0x06,0x49,0x49,0x29,0x1E}, /* 9 */
{0x7C,0x12,0x11,0x12,0x7C}, /* A */
{0x7F,0x49,0x49,0x49,0x36}, /* B */
{0x3E,0x41,0x41,0x41,0x22}, /* C */
{0x7F,0x41,0x41,0x22,0x1C}, /* D */
{0x7F,0x49,0x49,0x49,0x41}, /* E */
{0x7F,0x09,0x09,0x09,0x01}, /* F */
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 >= '0' && c <= '9') idx = c - '0';
else if (c >= 'A' && c <= 'F') idx = c - 'A' + 10;
else if (c >= 'a' && c <= 'f') idx = c - 'a' + 10;
for (uint8_t col = 0; col < 5; col++) {
uint8_t line = Font5x7[idx][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)
if (*str == ' ') { x += 6; str++; continue; }
if (*str == ':') { x += 6; str++; continue; }
SSD1306_DrawChar(x, y, *str);
/* For messages using a minimal ASCII font approach, we draw raw text.
For hex UID display, SSD1306_DrawString handles 0-9, A-F. */
/* --- 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) {
void SysTick_Handler(void)
Comments