[APM32E0] 基于apm32e030的nfc电子锁

[复制链接]
gaoshanmicai 发表于 2025-8-22 23:07 | 显示全部楼层 |阅读模式
[i=s] 本帖最后由 gaoshanmicai 于 2025-8-22 23:09 编辑 [/i]

这个门禁锁系统将使用 PN532 NFC 模块读取卡片信息,APM32E030R 微控制器作为主控,通过继电器来控制电磁锁,蜂鸣器用于提供声音反馈。

硬件构成说明

chuang.jpg

系统工作流程:

PN532 模块检测并读取 NFC 卡片信息
APM32E030R 验证卡片合法性
通过DRV5050的霍尔传感器来检查门锁是否关上
验证通过:蜂鸣器发出提示音,控制继电器来打开门锁
验证失败:蜂鸣器发出错误提示音
通过OLED12864 能显示当前锁的状态,显示是否是授权卡

mermaid-diagram.png

系统工作流程详解

这个门禁锁系统的完整工作流程如下:

a. 系统初始化:

初始化所有外设(蜂鸣器、步进电机、PN532 模块)
系统启动后自动锁门
进入等待卡片状态
卡片检测与验证:
​PN532
模块持续检测 NFC 卡片
当检测到卡片时,读取其 UID 信息
将读取到的 UID 与授权列表比对
验证通过:蜂鸣器发出两声短提示音
验证失败:蜂鸣器发出一声长提示音

b. ​门锁控制
验证通过后,继电器打开锁
门保持打开状态 5 秒,
5 秒后,继电器打开关闭锁
关门后蜂鸣器发出一声短提示音

c. ​OLED显示
OLED显示当前锁的状态,当刷卡检查是否是授权卡
如是切换OLED显示状态。如否状态不变,显示非授权卡

各个模块连接说明与通信

APM32E030R 与 PN532 通过串口 2 连接的具体引脚定义:
APM32E030R -> PN532
PA2 (USART2_TX) -> PN532_RX
PA3 (USART2_RX) -> PN532_TX
3.3V -> VCC
GND -> GND
注意:PN532 模块需要工作在 3.3V 电压下,不要连接 5V,否则可能损坏模块

测试中使用了两个串口,串口1做为一个Debug串口用于调试信息,串口2 连接与PN532 进行通信。串口2 采用DMA接收PN532 传回来的不定长度的数据。

USART2 DMA 初始化如下

#define UART2_DMA_RX_BUFFER_SIZE 0xff
static uint8_t uart2DmaRxBuffer[UART2_DMA_RX_BUFFER_SIZE];
static volatile uint16_t uart2RxDataLength = 0;
static volatile bool uart2HasNewData = false;
// 初始化USART2(PA2=TX, PA3=RX)和DMA1通道5
void USART2_DMA_Init(uint32_t baudrate) {
    GPIO_Config_T gpioConfig;
    USART_Config_T usartConfig;
    DMA_Config_T dmaConfig;

    DMA_Disable(DMA1_CHANNEL_5);
  
    // 使能时钟
    RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_GPIOA);

    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_USART2);
    RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA1);

    /* Connect PXx to USARTx_Tx */
    GPIO_ConfigPinAF(GPIOA, GPIO_PIN_SOURCE_2, GPIO_AF_PIN1);

    /* Connect PXx to USARTx_Rx */
    GPIO_ConfigPinAF(GPIOA,GPIO_PIN_SOURCE_3, GPIO_AF_PIN1);
  
    // 配置USART2引脚
    // PA2: USART2_TX (复用推挽输出)
    gpioConfig.mode = GPIO_MODE_AF;
    gpioConfig.pin = GPIO_PIN_2;
    gpioConfig.speed = GPIO_SPEED_10MHz;
    gpioConfig.outtype = GPIO_OUT_TYPE_PP;
    gpioConfig.pupd = GPIO_PUPD_PU;
    GPIO_Config(GPIOA, &gpioConfig);
  
    // PA3: USART2_RX (浮空输入)
    gpioConfig.pin = GPIO_PIN_3;
    GPIO_Config(GPIOA, &gpioConfig);
  
    // 配置USART2参数
    usartConfig.baudRate = baudrate;
    usartConfig.wordLength = USART_WORD_LEN_8B;
    usartConfig.stopBits = USART_STOP_BIT_1;
    usartConfig.parity = USART_PARITY_NONE;
    usartConfig.hardwareFlowCtrl = USART_FLOW_CTRL_NONE;
    usartConfig.mode = USART_MODE_TX_RX;
    USART_Config(USART2, &usartConfig);
  
    // 配置DMA1通道5(USART2 RX专用通道)
    dmaConfig.bufferSize = UART2_DMA_RX_BUFFER_SIZE;
    dmaConfig.memoryDataSize= DMA_MEMORY_DATASIZE_BYTE;
    dmaConfig.peripheralInc = DMA_PERIPHERAL_INC_DISABLE;
    dmaConfig.memoryInc = DMA_MEMORY_INC_ENABLE;
    dmaConfig.peripheralInc =DMA_MEMORY_INC_DISABLE;
    dmaConfig.circular = DMA_CIRCULAR_DISABLE;  // 正常模式
    dmaConfig.memoryTomemory = DMA_M2M_DISABLE;
    dmaConfig.priority = DMA_PRIORITY_LEVEL_HIGHT;
    dmaConfig.direction = DMA_DIR_PERIPHERAL;
    dmaConfig.memoryAddress = (uint32_t)uart2DmaRxBuffer;
    dmaConfig.peripheralAddress =(uint32_t)&USART2->RXDATA;
    DMA_Config(DMA1_CHANNEL_5, &dmaConfig);
    DMA_ClearIntFlag(DMA1_INT_FLAG_TF5);
    DMA_EnableInterrupt(DMA1_CHANNEL_5, DMA_INT_TFIE);
    NVIC_EnableIRQRequest(DMA1_CH4_5_IRQn, 3);  // DMA1通道5中断
    // 配置中断
    NVIC_EnableIRQRequest(USART2_IRQn, 1);         // USART2中断

    // 使能USART2空闲中断和DMA接收
    USART_EnableInterrupt(USART2, USART_INT_IDLEIE);
    // 启动DMA传输
    DMA_Enable(DMA1_CHANNEL_5);

    // 使能USART2
    USART_Enable(USART2);
}


// 检查是否有新数据
bool USART2_DMA_HasData(void) {
    return uart2HasNewData;
}

// 获取接收数据长度
uint16_t USART2_DMA_GetDataLength(void) {
    return uart2RxDataLength;
}

// 读取接收数据
uint16_t USART2_DMA_ReadData(uint8_t *buffer, uint16_t maxLength) {
    if (buffer == NULL || maxLength == 0 || !uart2HasNewData) {
        return 0;
    }
  
    uint16_t readLength = (maxLength < uart2RxDataLength) ? maxLength : uart2RxDataLength;
  
    // 复制数据到用户缓冲区
    for (uint16_t i = 0; i < readLength; i++) {
        buffer[i] = uart2DmaRxBuffer[i];
    }
  
    // 清除标志
    uart2HasNewData = false;
    return readLength;
}

// 清除接收缓冲区
void USART2_DMA_ClearBuffer(void) {
    DMA_Disable(DMA1_CHANNEL_5);
    uart2RxDataLength = 0;
    uart2HasNewData = false;
    DMA_SetDataNumber(DMA1_CHANNEL_5, UART2_DMA_RX_BUFFER_SIZE);
    DMA_Enable(DMA1_CHANNEL_5);
}

// USART2中断服务函数(处理空闲中断)
void USART2_IRQHandler(void) {
    if (USART_ReadIntFlag(USART2, USART_INT_IDLEIE) &&
        USART_ReadStatusFlag(USART2, USART_FLAG_IDLEF)) {
    
        // 清除空闲中断标志(读SR再读DR)
        uint32_t temp = USART2_ReadByte();
        USART_ClearStatusFlag(USART2,USART_FLAG_IDLEF);
        // 禁用DMA
        DMA_Disable(DMA1_CHANNEL_5);
    
        // 计算接收长度
        uart2RxDataLength = UART2_DMA_RX_BUFFER_SIZE - DMA_ReadDataNumber(DMA1_CHANNEL_5);


        // 设置新数据标志
        if (uart2RxDataLength > 0) {
            uart2HasNewData = true;
        }
    
        // 重启DMA
        DMA_SetDataNumber(DMA1_CHANNEL_5, UART2_DMA_RX_BUFFER_SIZE);
        DMA_Enable(DMA1_CHANNEL_5);
    }
}

// DMA1通道5中断服务函数(处理缓冲区满)
void DMA1_CH4_5_IRQHandler(void) {
    if (DMA_ReadStatusFlag(DMA1_FLAG_TF5)) {
        DMA_ClearStatusFlag(DMA1_FLAG_TF5);
    
        // 禁用DMA
        DMA_Disable(DMA1_CHANNEL_5);
    
        // 缓冲区已满
        uart2RxDataLength = UART2_DMA_RX_BUFFER_SIZE;
        uart2HasNewData = true;
    
        // 重启DMA
        DMA_SetDataNumber(DMA1_CHANNEL_5, UART2_DMA_RX_BUFFER_SIZE);
        DMA_Enable(DMA1_CHANNEL_5);

    }
}

UART 函数初始化部分主要做的工作就是 设置 串口2 的引脚,时钟,DMA的时钟,中断灯信息的配置。
在uart中断处理函数中主要做的是数据的接收以及DMA重启
在DMA 中断处理函数做的是,DMA缓冲区的处理与DMA的重启。

uart1 重定向printf 主要从写了一个函数

通过重写_write 函数,添加stdio头文件就可以方便的使用printf进行调试
具体与PN532的通信过程如下:

readcardProcess.png

1、唤醒模块,主机发送命令
0x55,0x55,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x03,0xFD,0xD4,0x14,0x01,0x17,0x00
模块回应
0x00,0x00,0xFF,0x02,0xFE,0xD5,0x15,0x16,0x00
在唤醒模块的过程中,数据的包头有些特殊,而这个包头是有芯片手册中找到的。
注意,为了简略,下面的命令只包含数据包格式中的TFI\DATA,其他的请读者自行加上

2、扫描卡片并获取到卡片ID,主机发送
invenCard.png
0xd4,0x4A(列出卡片),0x02(数量多2个),0x00(波特率)
Response

tagInfo.png
0xd5,0x4b,0x02两个设备
0x01(一号设备),0x04,0x00,0x08,0x04(长度),id1,id2,id3,id4,
0x02(二号设备),0x04,0x00,0x00,0x04(长度),id1,id2,id3,id4,
!

  1. 我们接收到卡ID数据后,然后与我们的授权卡数据进行比对。
// 授权卡片UID列表 (示例数据)
const uint8_t authorizedUIDs[][8] = {
    {0x12, 0x34, 0x56, 0x78},  // 授权卡片1
    {0xAA, 0xBB, 0xCC, 0xDD},  // 授权卡片2
    {0x01, 0x02, 0x03, 0x04, 0x05}, // 授权卡片3 (5字节UID)
    {0x04,0x0e,0xde,0x24}      // 授权卡4 

};
// 检查卡片是否授权
bool PN532_CheckCardAuthorization(CardInfo_t *card) {
    // 比较卡片UID与授权列表
    for (uint8_t i = 0; i < authorizedCount; i++) {
        if (card->uidLen == authorizedUIDsLen[i]) {
            bool match = true;
            for (uint8_t j = 0; j < card->uidLen; j++) {
                if (card->uid[j] != authorizedUIDs[i][j]) {
                    match = false;
                    break;
                }
            }
            if (match) {
                return true;  // 找到匹配的授权卡片
            }
        }
    }
    return false;  // 未授权卡片
}

如果成功状态机 切换到下一状态

OLED显示

这里采用模拟IIC的硬件对OLED12864进行控制
这里主要是对于SDA,SCL引脚进行配置,然后us的延时控制
SDA,SCL引脚配置与延时代码如下:
延时函数

void DelayUs(uint32_t nus)
{
    for (uint32_t i = 0; i < nus; i++)
    {
        __NOP();__NOP();__NOP();__NOP();
    }
}

// 毫秒级延时
void DelayMs(uint16_t nms) {
    uint32_t i;
    for (i = 0; i < nms; i++) {
        DelayUs(1000);
    }
}
#define OLED_SCLK_Clr() GPIO_ClearBit(OLED_SCL_PORT,OLED_SCL_PIN)

#define OLED_SCLK_Set() GPIO_SetBit(OLED_SCL_PORT,OLED_SCL_PIN)

#define OLED_SDIN_Clr() GPIO_ClearBit(OLED_SDA_PORT,OLED_SDA_PIN)

#define OLED_SDIN_Set() GPIO_SetBit(OLED_SDA_PORT,OLED_SDA_PIN)


void OLED_Init(void) {

    GPIO_Config_T GPIO_InitStructure;
    RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_GPIOC);

    GPIO_InitStructure.pin = OLED_SDA_PIN;
    GPIO_InitStructure.mode =GPIO_MODE_OUT ;//
    GPIO_InitStructure.outtype =GPIO_OUT_TYPE_PP;
    GPIO_InitStructure.speed =GPIO_SPEED_10MHz;//
    GPIO_InitStructure.pupd =GPIO_PUPD_PU;//

    GPIO_Config(OLED_SDA_PORT,&GPIO_InitStructure);

    GPIO_InitStructure.pin = OLED_SCL_PIN;
    GPIO_InitStructure.mode =GPIO_MODE_OUT ;//
    GPIO_InitStructure.outtype =GPIO_OUT_TYPE_PP;
    GPIO_InitStructure.speed =GPIO_SPEED_10MHz;//
    GPIO_InitStructure.pupd =GPIO_PUPD_PU;//

    GPIO_Config(OLED_SCL_PORT,&GPIO_InitStructure);

    GPIO_SetBit(OLED_SDA_PORT,OLED_SDA_PIN|OLED_SCL_PIN);
    OLED_WR_Byte(0xAE,OLED_CMD); //
    OLED_WR_Byte(0x00,OLED_CMD); //
    OLED_WR_Byte(0x10,OLED_CMD); //
    OLED_WR_Byte(0x40,OLED_CMD); //

    OLED_WR_Byte(0x81,OLED_CMD); //
    OLED_WR_Byte(0xCF,OLED_CMD); // 

    OLED_WR_Byte(0xA1,OLED_CMD); //
    OLED_WR_Byte(0xC8,OLED_CMD); //
    OLED_WR_Byte(0xA6,OLED_CMD); //

    OLED_WR_Byte(0xA8,OLED_CMD); //
    OLED_WR_Byte(0x3f,OLED_CMD); //

    OLED_WR_Byte(0xD3,OLED_CMD); //
    OLED_WR_Byte(0x00,OLED_CMD); //

    OLED_WR_Byte(0xd5,OLED_CMD); //
    OLED_WR_Byte(0x80,OLED_CMD); //

    OLED_WR_Byte(0xD9,OLED_CMD); //
    OLED_WR_Byte(0xF1,OLED_CMD); //

    OLED_WR_Byte(0xDA,OLED_CMD); //
    OLED_WR_Byte(0x12,OLED_CMD);

    OLED_WR_Byte(0xDB,OLED_CMD); //
    OLED_WR_Byte(0x40,OLED_CMD); //

    OLED_WR_Byte(0x20,OLED_CMD); //
    OLED_WR_Byte(0x02,OLED_CMD); //

    OLED_WR_Byte(0x8D,OLED_CMD); //
    OLED_WR_Byte(0x14,OLED_CMD); //

    OLED_WR_Byte(0xA4,OLED_CMD); //  
    OLED_WR_Byte(0xA6,OLED_CMD); //

    OLED_Clear();
    OLED_WR_Byte(0xAF,OLED_CMD); //

    OLED_ColorTurn(0); //
    OLED_DisplayTurn(0); //
}
```然后实现I2C的 start,send data ,ack等命令帧
```c
void I2C_Start(void);
void I2C_Stop(void);
void I2C_WaitAck(void);
void Send_Byte(uint8_t dat);

最后基于这些函数,实现oled的一些绘图,显示字符,刷新,清屏等函数

void OLED_ClearPoint(uint8_t x,uint8_t y);
void OLED_ColorTurn(uint8_t i);
void OLED_DisplayTurn(uint8_t i);

void OLED_WR_Byte(uint8_t dat,uint8_t mode);
void OLED_DisPlay_On(void);
void OLED_DisPlay_Off(void);
void OLED_Refresh(void);
void OLED_Clear(void);
void OLED_DrawPoint(uint8_t x,uint8_t y);
void OLED_DrawLine(uint8_t x1,uint8_t y1,uint8_t x2,uint8_t y2);
void OLED_DrawCircle(uint8_t x,uint8_t y,u8 r);
void OLED_ShowChar(uint8_t x,uint8_t y,uint8_t chr,uint8_t size1);
void OLED_ShowString(uint8_t x,u8 y,uint8_t *chr,uint8_t size1);
void OLED_ShowNum(uint8_t x,uint8_t y,uint32_t num,u8 len,uint8_t size1);
void OLED_ShowChinese(uint8_t x,uint8_t y,uint8_t num,uint8_t size1);
void OLED_ShowFloatNum(u8 x,u8 y,float num,u8 size1);
void OLED_ScrollDisplay(uint8_t num,uint8_t space);
void OLED_WR_BP(uint8_t x,u8 y);
void OLED_ShowPicture(uint8_t x0,uint8_t y0,uint8_t x1,uint8_t y1,uint8_t BMP[]);
void OLED_Init(void);

开关门状态检查

开关们状态检查主要是通过DRV5055 这个模拟的霍尔传感器进行检查

drv5050.png

当门关闭时,固定在门框上的磁铁与锁上的DRV5050接近时,这样通过读取与传感器连接的引脚PA0 的状态就能获取当前门是否关上

#define DOOR_SENSOR_PORT    GPIOB
#define DOOR_SENSOR_PIN     GPIO_PIN_2
// 获取门状态(带消抖处理)

DoorState_t DoorSensor_GetState(void) {
    // 读取当前状态
    uint8_t currentState = GPIO_ReadInputBit(DOOR_SENSOR_PORT, DOOR_SENSOR_PIN);
  
    // 延时消抖
    DelayMs(SENSOR_DEBOUNCE_DELAY);
  
    // 再次读取确认
    if (currentState == GPIO_ReadInputBit(DOOR_SENSOR_PORT, DOOR_SENSOR_PIN)) {
        // 根据DRV5050特性:磁铁靠近时输出低电平,远离时输出高电平
        return (currentState == 0) ? DOOR_CLOSED : DOOR_OPEN;
    }
  
    // 状态不稳定,返回当前读取值
    return (currentState == 0) ? DOOR_CLOSED : DOOR_OPEN;
}

// 检查门是否关闭
bool DoorSensor_IsClosed(void) {
    return (DoorSensor_GetState() == DOOR_CLOSED);
}

// 检查门是否打开
bool DoorSensor_IsOpen(void) {
    return (DoorSensor_GetState() == DOOR_OPEN);
}

效果演示

总结

通过使用apm32E030的开发板开发的过程中有以下几点心得:

  1. 国产单片机的sdk开发的越来越好了。通过简单的配置就能实现很复杂的功能
  2. 性能发面不输stm32等国外大厂的ic。功耗方面越来越低。性能方面非常优秀.
    在以后的工作中会多多使用极海的单片机来开发项目。
 楼主| gaoshanmicai 发表于 2025-8-22 23:08 | 显示全部楼层
[i=s] 本帖最后由 gaoshanmicai 于 2025-8-22 23:10 编辑 [/i]

smile

您需要登录后才可以回帖 登录 | 注册

本版积分规则

7

主题

24

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部

7

主题

24

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部