- binary_tx_handler를 dr_binary_tx_safe로 전체 교체 (APP_ERROR_CHECK 제거) - data_tx_handler APP_ERROR_CHECK → DBG_PRINTF 교체 - memset/memcpy 하드코딩 크기를 define 상수로 교체 (버퍼 오버런 수정) - SERIAL_NO_LENGTH, HW_NO_LENGTH, PASSKEY_LENGTH를 main.h로 통합 - 미사용 HW 드라이버/EEPROM 코드 삭제, TWI를 i2c_manager.c로 통합 - EEPROM → FDS 전환, 코드 리뷰 현황 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
534 lines
15 KiB
C
534 lines
15 KiB
C
/*******************************************************************************
|
|
* @file dr_w25q32.c
|
|
* @brief W25Q32 SPI Flash Driver for nRF52840
|
|
* Software SPI (bit-bang) implementation
|
|
* @date 2026-03-12
|
|
*
|
|
* @details Bit-bang SPI driver for W25Q32 NOR Flash.
|
|
* SPI Mode 0 (CPOL=0, CPHA=0):
|
|
* - SCLK idle LOW
|
|
* - Data sampled on rising edge
|
|
* - Data shifted out on falling edge
|
|
*
|
|
* W25Q32 SPI timing:
|
|
* CS ──┐ ┌──
|
|
* └────────────────────────────────────┘
|
|
* CLK ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
|
|
* ─────┘ └──┘ └──┘ └── ... ┘ └──┘ └────
|
|
* DI <CMD7><CMD6><CMD5>...<D1 ><D0 >
|
|
* DO <Z ><Z ><Z >...<D1 ><D0 >
|
|
******************************************************************************/
|
|
|
|
#include "dr_w25q32.h"
|
|
#include "nrf_gpio.h"
|
|
#include "nrf_delay.h"
|
|
#include "debug_print.h"
|
|
#include <string.h>
|
|
|
|
/*==============================================================================
|
|
* PRIVATE DEFINES
|
|
*============================================================================*/
|
|
|
|
#define DR_PIN_NUM(pin) ((pin) & 0x1F)
|
|
#define DR_PIN_PORT(pin) (((pin) >> 5) & 0x01)
|
|
|
|
/* Pin masks for direct register access */
|
|
#define FLASH_CS_MASK (1UL << DR_PIN_NUM(DR_FLASH_PIN_CS))
|
|
#define FLASH_SCLK_MASK (1UL << DR_PIN_NUM(DR_FLASH_PIN_SCLK))
|
|
#define FLASH_MISO_MASK (1UL << DR_PIN_NUM(DR_FLASH_PIN_MISO))
|
|
#define FLASH_MOSI_MASK (1UL << DR_PIN_NUM(DR_FLASH_PIN_MOSI))
|
|
|
|
/* Port registers - all pins on P0 */
|
|
#define FLASH_PORT NRF_P0 // NRF_P0: 포트 레지스터에 접근하는 베이스 포인터(포트 전체 제어 X)
|
|
|
|
/* Timeout for busy wait (ms) */
|
|
#define BUSY_TIMEOUT_PAGE_PROGRAM 10
|
|
#define BUSY_TIMEOUT_SECTOR_ERASE 500
|
|
#define BUSY_TIMEOUT_BLOCK_ERASE 2000
|
|
#define BUSY_TIMEOUT_CHIP_ERASE 60000
|
|
|
|
/* W25Q32 JEDEC ID expected values */
|
|
#define W25Q32_MANUFACTURER_ID 0xEF
|
|
#define W25Q32_MEMORY_TYPE 0x40
|
|
#define W25Q32_CAPACITY 0x16
|
|
|
|
/*==============================================================================
|
|
* PRIVATE VARIABLES
|
|
*============================================================================*/
|
|
|
|
static bool m_initialized = false;
|
|
|
|
/*==============================================================================
|
|
* LOW-LEVEL SPI BIT-BANG
|
|
*
|
|
* W25Q32 uses SPI Mode 0:
|
|
* - CPOL=0: Clock idle LOW
|
|
* - CPHA=0: Data sampled on rising edge, shifted on falling edge
|
|
*============================================================================*/
|
|
|
|
static inline void flash_cs_low(void)
|
|
{
|
|
FLASH_PORT->OUTCLR = FLASH_CS_MASK;
|
|
}
|
|
|
|
static inline void flash_cs_high(void)
|
|
{
|
|
FLASH_PORT->OUTSET = FLASH_CS_MASK;
|
|
}
|
|
|
|
static inline void flash_sclk_low(void)
|
|
{
|
|
FLASH_PORT->OUTCLR = FLASH_SCLK_MASK;
|
|
}
|
|
|
|
static inline void flash_sclk_high(void)
|
|
{
|
|
FLASH_PORT->OUTSET = FLASH_SCLK_MASK;
|
|
}
|
|
|
|
static inline void flash_mosi_high(void)
|
|
{
|
|
FLASH_PORT->OUTSET = FLASH_MOSI_MASK;
|
|
}
|
|
|
|
static inline void flash_mosi_low(void)
|
|
{
|
|
FLASH_PORT->OUTCLR = FLASH_MOSI_MASK;
|
|
}
|
|
|
|
static inline uint32_t flash_read_miso(void)
|
|
{
|
|
return (FLASH_PORT->IN & FLASH_MISO_MASK) ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Send one byte via SPI (MSB first)
|
|
*/
|
|
static void spi_send_byte(uint8_t byte)
|
|
{
|
|
for (int i = 7; i >= 0; i--)
|
|
{
|
|
/* Set MOSI */
|
|
if (byte & (1 << i))
|
|
flash_mosi_high();
|
|
else
|
|
flash_mosi_low();
|
|
|
|
/* Rising edge - data sampled by slave */
|
|
flash_sclk_high();
|
|
__NOP();
|
|
__NOP();
|
|
|
|
/* Falling edge */
|
|
flash_sclk_low();
|
|
__NOP();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Receive one byte via SPI (MSB first)
|
|
* @note Sends 0xFF (MOSI high) while reading
|
|
*/
|
|
static uint8_t spi_recv_byte(void)
|
|
{
|
|
uint8_t byte = 0;
|
|
|
|
flash_mosi_high(); /* Keep MOSI high during read */
|
|
|
|
for (int i = 7; i >= 0; i--)
|
|
{
|
|
/* Rising edge - sample MISO */
|
|
flash_sclk_high();
|
|
__NOP();
|
|
__NOP();
|
|
|
|
if (flash_read_miso())
|
|
byte |= (1 << i);
|
|
|
|
/* Falling edge */
|
|
flash_sclk_low();
|
|
__NOP();
|
|
}
|
|
|
|
return byte;
|
|
}
|
|
|
|
/**
|
|
* @brief Initialize GPIO pins
|
|
*/
|
|
static void flash_gpio_init(void)
|
|
{
|
|
/* CS: Output, HIGH (deselected) */
|
|
nrf_gpio_cfg_output(DR_FLASH_PIN_CS);
|
|
nrf_gpio_pin_set(DR_FLASH_PIN_CS);
|
|
|
|
/* SCLK: Output, LOW (idle for Mode 0) */
|
|
nrf_gpio_cfg_output(DR_FLASH_PIN_SCLK);
|
|
nrf_gpio_pin_clear(DR_FLASH_PIN_SCLK);
|
|
|
|
/* MOSI: Output, HIGH */
|
|
nrf_gpio_cfg_output(DR_FLASH_PIN_MOSI);
|
|
nrf_gpio_pin_set(DR_FLASH_PIN_MOSI);
|
|
|
|
/* MISO: Input, no pull */
|
|
nrf_gpio_cfg_input(DR_FLASH_PIN_MISO, NRF_GPIO_PIN_NOPULL);
|
|
}
|
|
|
|
/**
|
|
* @brief Send Write Enable command (0x06)
|
|
*/
|
|
static void flash_write_enable(void)
|
|
{
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_WRITE_ENABLE);
|
|
flash_cs_high();
|
|
__NOP(); __NOP(); __NOP(); __NOP();
|
|
}
|
|
|
|
/*==============================================================================
|
|
* PUBLIC FUNCTIONS - INITIALIZATION
|
|
*============================================================================*/
|
|
|
|
dr_flash_err_t dr_w25q32_init(void)
|
|
{
|
|
DBG_PRINTF("[FLASH] Init\n");
|
|
flash_gpio_init();
|
|
m_initialized = true;
|
|
DBG_PRINTF("[FLASH] Init OK\n");
|
|
return DR_FLASH_OK;
|
|
}
|
|
|
|
void dr_w25q32_uninit(void)
|
|
{
|
|
if (!m_initialized) return;
|
|
|
|
DBG_PRINTF("[FLASH] Uninit\n");
|
|
|
|
/* CS high (deselect) */
|
|
nrf_gpio_pin_set(DR_FLASH_PIN_CS);
|
|
|
|
/* Release pins to default */
|
|
nrf_gpio_cfg_default(DR_FLASH_PIN_CS);
|
|
|
|
/* Don't release shared pins (SCLK, MISO, MOSI) -
|
|
they may be in use by ADC121S051 */
|
|
|
|
m_initialized = false;
|
|
}
|
|
|
|
bool dr_w25q32_is_initialized(void)
|
|
{
|
|
return m_initialized;
|
|
}
|
|
|
|
/*==============================================================================
|
|
* PUBLIC FUNCTIONS - IDENTIFICATION
|
|
*============================================================================*/
|
|
|
|
dr_flash_err_t dr_w25q32_read_jedec_id(dr_flash_jedec_t *jedec) //JEDEC ID 읽기 (0xEF, 0x40, 0x16)
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
if (!jedec) return DR_FLASH_ERR_INVALID_PARAM;
|
|
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_READ_JEDEC_ID);
|
|
jedec->manufacturer_id = spi_recv_byte();
|
|
jedec->memory_type = spi_recv_byte();
|
|
jedec->capacity = spi_recv_byte();
|
|
flash_cs_high();
|
|
|
|
DBG_PRINTF("[FLASH] JEDEC ID: 0x%02X 0x%02X 0x%02X\n",
|
|
jedec->manufacturer_id, jedec->memory_type, jedec->capacity);
|
|
|
|
return DR_FLASH_OK;
|
|
}
|
|
|
|
dr_flash_err_t dr_w25q32_read_uid(uint8_t *uid) // 8바이트 고유 ID 읽기
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
if (!uid) return DR_FLASH_ERR_INVALID_PARAM;
|
|
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_READ_UID);
|
|
|
|
/* 4 dummy bytes */
|
|
spi_send_byte(0x00);
|
|
spi_send_byte(0x00);
|
|
spi_send_byte(0x00);
|
|
spi_send_byte(0x00);
|
|
|
|
/* 8-byte unique ID */
|
|
for (int i = 0; i < DR_FLASH_UID_LENGTH; i++)
|
|
{
|
|
uid[i] = spi_recv_byte();
|
|
}
|
|
flash_cs_high();
|
|
|
|
DBG_PRINTF("[FLASH] UID: %02X%02X%02X%02X%02X%02X%02X%02X\n",
|
|
uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6], uid[7]);
|
|
|
|
return DR_FLASH_OK;
|
|
}
|
|
|
|
/*==============================================================================
|
|
* PUBLIC FUNCTIONS - STATUS
|
|
*============================================================================*/
|
|
|
|
dr_flash_err_t dr_w25q32_read_status(uint8_t *status)
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
if (!status) return DR_FLASH_ERR_INVALID_PARAM;
|
|
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_READ_STATUS1);
|
|
*status = spi_recv_byte();
|
|
flash_cs_high();
|
|
|
|
return DR_FLASH_OK;
|
|
}
|
|
|
|
bool dr_w25q32_is_busy(void)
|
|
{
|
|
uint8_t status = 0;
|
|
dr_w25q32_read_status(&status);
|
|
return (status & DR_FLASH_SR1_BUSY) ? true : false;
|
|
}
|
|
|
|
dr_flash_err_t dr_w25q32_wait_busy(uint32_t timeout_ms)
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
|
|
while (timeout_ms > 0)
|
|
{
|
|
if (!dr_w25q32_is_busy())
|
|
return DR_FLASH_OK;
|
|
|
|
nrf_delay_ms(1);
|
|
timeout_ms--;
|
|
}
|
|
|
|
return DR_FLASH_ERR_TIMEOUT;
|
|
}
|
|
|
|
/*==============================================================================
|
|
* PUBLIC FUNCTIONS - READ
|
|
*============================================================================*/
|
|
|
|
dr_flash_err_t dr_w25q32_read(uint32_t addr, uint8_t *buf, uint32_t len) // 데이터 읽기 (주소, 길이)
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
if (!buf || len == 0) return DR_FLASH_ERR_INVALID_PARAM;
|
|
if (addr + len > DR_FLASH_TOTAL_SIZE) return DR_FLASH_ERR_INVALID_PARAM;
|
|
|
|
DBG_PRINTF("[FLASH] Read addr=0x%06X len=%d\n", addr, len);
|
|
|
|
flash_cs_low();
|
|
|
|
/* Command + 24-bit address */
|
|
spi_send_byte(DR_FLASH_CMD_READ_DATA);
|
|
spi_send_byte((addr >> 16) & 0xFF);
|
|
spi_send_byte((addr >> 8) & 0xFF);
|
|
spi_send_byte((addr ) & 0xFF);
|
|
|
|
/* Read data */
|
|
for (uint32_t i = 0; i < len; i++)
|
|
{
|
|
buf[i] = spi_recv_byte();
|
|
}
|
|
|
|
flash_cs_high();
|
|
|
|
return DR_FLASH_OK;
|
|
}
|
|
|
|
/*==============================================================================
|
|
* PUBLIC FUNCTIONS - WRITE
|
|
*============================================================================*/
|
|
|
|
dr_flash_err_t dr_w25q32_write(uint32_t addr, const uint8_t *data, uint32_t len) // 페이지 쓰기 (최대 256B)
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
if (!data || len == 0 || len > DR_FLASH_PAGE_SIZE)
|
|
return DR_FLASH_ERR_INVALID_PARAM;
|
|
if (addr + len > DR_FLASH_TOTAL_SIZE)
|
|
return DR_FLASH_ERR_INVALID_PARAM;
|
|
|
|
/* Check page boundary crossing */
|
|
uint32_t page_offset = addr % DR_FLASH_PAGE_SIZE;
|
|
if (page_offset + len > DR_FLASH_PAGE_SIZE)
|
|
return DR_FLASH_ERR_INVALID_PARAM;
|
|
|
|
DBG_PRINTF("[FLASH] Write addr=0x%06X len=%d\n", addr, len);
|
|
|
|
/* Write Enable */
|
|
flash_write_enable();
|
|
|
|
/* Page Program */
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_PAGE_PROGRAM);
|
|
spi_send_byte((addr >> 16) & 0xFF);
|
|
spi_send_byte((addr >> 8) & 0xFF);
|
|
spi_send_byte((addr ) & 0xFF);
|
|
|
|
for (uint32_t i = 0; i < len; i++)
|
|
{
|
|
spi_send_byte(data[i]);
|
|
}
|
|
|
|
flash_cs_high();
|
|
|
|
/* Wait for programming to complete */
|
|
dr_flash_err_t err = dr_w25q32_wait_busy(BUSY_TIMEOUT_PAGE_PROGRAM);
|
|
if (err != DR_FLASH_OK)
|
|
DBG_PRINTF("[FLASH] Write TIMEOUT!\n");
|
|
return err;
|
|
}
|
|
|
|
/*==============================================================================
|
|
* PUBLIC FUNCTIONS - ERASE
|
|
*============================================================================*/
|
|
|
|
dr_flash_err_t dr_w25q32_erase_sector(uint32_t addr) // 4KB 섹터 삭제
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
if (addr >= DR_FLASH_TOTAL_SIZE) return DR_FLASH_ERR_INVALID_PARAM;
|
|
|
|
DBG_PRINTF("[FLASH] Erase sector addr=0x%06X\n", addr);
|
|
|
|
flash_write_enable();
|
|
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_SECTOR_ERASE);
|
|
spi_send_byte((addr >> 16) & 0xFF);
|
|
spi_send_byte((addr >> 8) & 0xFF);
|
|
spi_send_byte((addr ) & 0xFF);
|
|
flash_cs_high();
|
|
|
|
dr_flash_err_t err = dr_w25q32_wait_busy(BUSY_TIMEOUT_SECTOR_ERASE);
|
|
if (err != DR_FLASH_OK)
|
|
DBG_PRINTF("[FLASH] Erase sector TIMEOUT!\n");
|
|
return err;
|
|
}
|
|
|
|
dr_flash_err_t dr_w25q32_erase_block_32k(uint32_t addr) // 블록 삭제
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
if (addr >= DR_FLASH_TOTAL_SIZE) return DR_FLASH_ERR_INVALID_PARAM;
|
|
|
|
DBG_PRINTF("[FLASH] Erase block 32K addr=0x%06X\n", addr);
|
|
|
|
flash_write_enable();
|
|
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_BLOCK_ERASE_32K);
|
|
spi_send_byte((addr >> 16) & 0xFF);
|
|
spi_send_byte((addr >> 8) & 0xFF);
|
|
spi_send_byte((addr ) & 0xFF);
|
|
flash_cs_high();
|
|
|
|
dr_flash_err_t err = dr_w25q32_wait_busy(BUSY_TIMEOUT_BLOCK_ERASE);
|
|
if (err != DR_FLASH_OK)
|
|
DBG_PRINTF("[FLASH] Erase block 32K TIMEOUT!\n");
|
|
return err;
|
|
}
|
|
|
|
dr_flash_err_t dr_w25q32_erase_block_64k(uint32_t addr) // 블록 삭제
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
if (addr >= DR_FLASH_TOTAL_SIZE) return DR_FLASH_ERR_INVALID_PARAM;
|
|
|
|
DBG_PRINTF("[FLASH] Erase block 64K addr=0x%06X\n", addr);
|
|
|
|
flash_write_enable();
|
|
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_BLOCK_ERASE_64K);
|
|
spi_send_byte((addr >> 16) & 0xFF);
|
|
spi_send_byte((addr >> 8) & 0xFF);
|
|
spi_send_byte((addr ) & 0xFF);
|
|
flash_cs_high();
|
|
|
|
dr_flash_err_t err = dr_w25q32_wait_busy(BUSY_TIMEOUT_BLOCK_ERASE);
|
|
if (err != DR_FLASH_OK)
|
|
DBG_PRINTF("[FLASH] Erase block 64K TIMEOUT!\n");
|
|
return err;
|
|
}
|
|
|
|
dr_flash_err_t dr_w25q32_chip_erase(void) // 전체 삭제
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
|
|
DBG_PRINTF("[FLASH] Chip erase...\n");
|
|
|
|
flash_write_enable();
|
|
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_CHIP_ERASE);
|
|
flash_cs_high();
|
|
|
|
dr_flash_err_t err = dr_w25q32_wait_busy(BUSY_TIMEOUT_CHIP_ERASE);
|
|
if (err == DR_FLASH_OK)
|
|
DBG_PRINTF("[FLASH] Chip erase OK\n");
|
|
else
|
|
DBG_PRINTF("[FLASH] Chip erase TIMEOUT!\n");
|
|
return err;
|
|
}
|
|
|
|
/*==============================================================================
|
|
* PUBLIC FUNCTIONS - POWER MANAGEMENT
|
|
*============================================================================*/
|
|
|
|
dr_flash_err_t dr_w25q32_deep_powerdown(void) // 저전력 모드 (~1uA)
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
|
|
DBG_PRINTF("[FLASH] Deep power-down\n");
|
|
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_POWER_DOWN);
|
|
flash_cs_high();
|
|
|
|
/* tDP: CS high to deep power-down = 3us max */
|
|
nrf_delay_us(5);
|
|
|
|
return DR_FLASH_OK;
|
|
}
|
|
|
|
dr_flash_err_t dr_w25q32_wakeup(void) // 저전력 모드 해제(깨우기)
|
|
{
|
|
if (!m_initialized) return DR_FLASH_ERR_NOT_INIT;
|
|
|
|
DBG_PRINTF("[FLASH] Wakeup\n");
|
|
|
|
flash_cs_low();
|
|
spi_send_byte(DR_FLASH_CMD_RELEASE_PD);
|
|
flash_cs_high();
|
|
|
|
/* tRES1: CS high to standby = 3us max */
|
|
nrf_delay_us(5);
|
|
|
|
return DR_FLASH_OK;
|
|
}
|
|
|
|
/*==============================================================================
|
|
* PUBLIC FUNCTIONS - DEBUG / TEST
|
|
*============================================================================*/
|
|
|
|
bool dr_w25q32_test(void) // JEDEC ID로 통신 테스트
|
|
{
|
|
dr_flash_jedec_t jedec;
|
|
|
|
if (dr_w25q32_read_jedec_id(&jedec) != DR_FLASH_OK)
|
|
{
|
|
DBG_PRINTF("[FLASH] Test FAIL - read error\n");
|
|
return false;
|
|
}
|
|
|
|
bool pass = (jedec.manufacturer_id == W25Q32_MANUFACTURER_ID &&
|
|
jedec.memory_type == W25Q32_MEMORY_TYPE &&
|
|
jedec.capacity == W25Q32_CAPACITY);
|
|
|
|
DBG_PRINTF("[FLASH] Test %s\n", pass ? "PASS" : "FAIL");
|
|
return pass;
|
|
}
|