一、系统实现的主要功能
a、显示系统初始化界面、功能菜单界面以及实时时间界面,后二者可以随时切换;
b、具有4种模式,分别为刷卡解锁、IC卡信息管理、密码解锁、修改密码,并且有LED灯进行提示;
c、成功解锁时,串口打印卡号以及当前时间,如使用密码开锁模式,则不显示卡号信息;
d、三次解锁失败时,则会触发报警器报警。
二、系统设计方案
1、系统框架
图 1 系统总体框架
2、相关模块
(1)读卡器模块
MFRC522如下图,采用SPI通信总线协议。具体参照RC522文章详述。
图 2 MFRC522
(2)键盘模块
打板的朋友可以自己画4*4键盘矩阵,不打板的可以可以直接买下边现成模块。
图 3 4*4键盘模块
(3)显示模块
0.96寸的OLED显示屏分为两种,一种是七脚的,采取的是IIC通信协议,另一种是四脚的,采取的是SPI通信协议。本次设计采取的是采用四针管脚0.96寸OLED显示屏。详细见往期OLED介绍文章。
图 4 7针的OLED显示模块和4针的OLED显示模块
(4)模拟门锁模块
采用SG90舵机,利用PWM波控制舵机转动模拟门锁开关。可以参考呼吸灯设计那篇文章。
图 5 舵机元件图
(5)报警模块
采用低电平触发的有源蜂鸣器报警电路,因为单片机电流过小,无法驱动蜂鸣器正常工作。这里采用PNP驱动电路,也可以采用NPN,具体可以自行选择设计。值得注意的一个点,这里采用的是5V正电压,在这种情况下,若单片机直接接上USB,会导致PNP一直导通;若单片机用Jlink供电,PNP则不会一直导通,可以正常工作。这里建议画板的时候把5V改用3.3V的管脚接上。
另外,若读者选择无源蜂鸣器,则需要输出特定频率的PWM波才能使得蜂鸣器正常发出声音。
图 6 蜂鸣器驱动电路原理图
(6)存储模块
采用单片机内部的flash模拟EEPROM,实现密码掉电不丢失的效果,也可以存储其他数据,但是前提是不能影响到内部代码。相关代码大小以及其他数据量可以通过下图查看。也可以通过外接一个EEPROM模块(W25Q128),具体操作看往期W25Q128相关文章。
图 7 工程大小相关数据
(7)时钟模块
采用STM32F103C8T6最小系统板内部的RTC模块,实现获取实时时间的功能。
三、相关代码
1、spi.c与rc522.c
(1)rc522.c的基本代码
#include "stm32f1xx_hal.h"
#include "./BSP/RC522/rc522.h"
#include "stdio.h"
#include "./SYSTEM/usart/usart.h"
#include <string.h>
#include "./BSP/SERVO/servo.h"
#include "./BSP/KEY/key.h"
#include "./BSP/OLED/OLED.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/RTC/rtc.h"extern SPI_HandleTypeDef hspi2;/**************************************************************************************
* 函数名称:MFRC_Init
* 功能描述:MFRC初始化
* 入口参数:无
* 出口参数:无
* 返 回 值:无
* 说 明:MFRC的SPI接口速率为0~10Mbps
***************************************************************************************/
void MFRC_Init(void)
{RS522_NSS(1);RS522_RST(1);
}/**************************************************************************************
* 函数名称: SPI_RW_Byte
* 功能描述: 模拟SPI读写一个字节
* 入口参数: -byte:要发送的数据
* 出口参数: -byte:接收到的数据
***************************************************************************************/
static uint8_t ret; //这些函数是HAL与标准库不同的地方【读写函数】
uint8_t SPI2_RW_Byte(uint8_t byte)
{HAL_SPI_TransmitReceive(&hspi2, &byte, &ret, 1, 10);//把byte 写入,并读出一个值,把它存入retreturn ret;//入口是byte 的地址,读取时用的也是ret地址,一次只写入一个值10
}/**************************************************************************************
* 函数名称:MFRC_WriteReg
* 功能描述:写一个寄存器
* 入口参数:-addr:待写的寄存器地址
* -data:待写的寄存器数据
* 出口参数:无
* 返 回 值:无
* 说 明:无
***************************************************************************************/
void MFRC_WriteReg(uint8_t addr, uint8_t data)
{uint8_t AddrByte;AddrByte = (addr << 1 ) & 0x7E; //求出地址字节RS522_NSS(0); //NSS拉低SPI2_RW_Byte(AddrByte); //写地址字节SPI2_RW_Byte(data); //写数据RS522_NSS(1); //NSS拉高
}/**************************************************************************************
* 函数名称:MFRC_ReadReg
* 功能描述:读一个寄存器
* 入口参数:-addr:待读的寄存器地址
* 出口参数:无
* 返 回 值:-data:读到寄存器的数据
* 说 明:无
***************************************************************************************/
uint8_t MFRC_ReadReg(uint8_t addr)
{uint8_t AddrByte, data;AddrByte = ((addr << 1 ) & 0x7E ) | 0x80; //求出地址字节RS522_NSS(0); //NSS拉低SPI2_RW_Byte(AddrByte); //写地址字节data = SPI2_RW_Byte(0x00); //读数据RS522_NSS(1); //NSS拉高return data;
}/**************************************************************************************
* 函数名称:MFRC_SetBitMask
* 功能描述:设置寄存器的位
* 入口参数:-addr:待设置的寄存器地址
* -mask:待设置寄存器的位(可同时设置多个bit)
* 出口参数:无
* 返 回 值:无
* 说 明:无
***************************************************************************************/
void MFRC_SetBitMask(uint8_t addr, uint8_t mask)
{uint8_t temp;temp = MFRC_ReadReg(addr); //先读回寄存器的值MFRC_WriteReg(addr, temp | mask); //处理过的数据再写入寄存器
}/**************************************************************************************
* 函数名称:MFRC_ClrBitMask
* 功能描述:清除寄存器的位
* 入口参数:-addr:待清除的寄存器地址
* -mask:待清除寄存器的位(可同时清除多个bit)
* 出口参数:无
* 返 回 值:无
* 说 明:无
***************************************************************************************/
void MFRC_ClrBitMask(uint8_t addr, uint8_t mask)
{uint8_t temp;temp = MFRC_ReadReg(addr); //先读回寄存器的值MFRC_WriteReg(addr, temp & ~mask); //处理过的数据再写入寄存器
}/**************************************************************************************
* 函数名称:MFRC_CalulateCRC
* 功能描述:用MFRC计算CRC结果
* 入口参数:-pInData:带进行CRC计算的数据
* -len:带进行CRC计算的数据长度
* -pOutData:CRC计算结果
* 出口参数:-pOutData:CRC计算结果
* 返 回 值:无
* 说 明:无
***************************************************************************************/
void MFRC_CalulateCRC(uint8_t *pInData, uint8_t len, uint8_t *pOutData)
{//0xc1 1 2 pInData[2]uint8_t temp;uint32_t i;MFRC_ClrBitMask(MFRC_DivIrqReg, 0x04); //使能CRC中断MFRC_WriteReg(MFRC_CommandReg, MFRC_IDLE); //取消当前命令的执行MFRC_SetBitMask(MFRC_FIFOLevelReg, 0x80); //清除FIFO及其标志位for(i = 0; i < len; i++) //将待CRC计算的数据写入FIFO{MFRC_WriteReg(MFRC_FIFODataReg, *(pInData + i));}MFRC_WriteReg(MFRC_CommandReg, MFRC_CALCCRC); //执行CRC计算i = 100000;do{temp = MFRC_ReadReg(MFRC_DivIrqReg); //读取DivIrqReg寄存器的值i--;}while((i != 0) && !(temp & 0x04)); //等待CRC计算完成pOutData[0] = MFRC_ReadReg(MFRC_CRCResultRegL); //读取CRC计算结果pOutData[1] = MFRC_ReadReg(MFRC_CRCResultRegM);
}/**************************************************************************************
* 函数名称:MFRC_CmdFrame
* 功能描述:MFRC522和ISO14443A卡通讯的命令帧函数
* 入口参数:-cmd:MFRC522命令字
* -pIndata:MFRC522发送给MF1卡的数据的缓冲区首地址
* -InLenByte:发送数据的字节长度
* -pOutdata:用于接收MF1卡片返回数据的缓冲区首地址
* -pOutLenBit:MF1卡返回数据的位长度
* 出口参数:-pOutdata:用于接收MF1卡片返回数据的缓冲区首地址
* -pOutLenBit:用于MF1卡返回数据位长度的首地址
* 返 回 值:-status:错误代码(MFRC_OK、MFRC_NOTAGERR、MFRC_ERR)
* 说 明:无
***************************************************************************************/
char MFRC_CmdFrame(uint8_t cmd, uint8_t *pInData, uint8_t InLenByte, uint8_t *pOutData, uint16_t *pOutLenBit)
{uint8_t lastBits;uint8_t n;uint32_t i;char status = MFRC_ERR;uint8_t irqEn = 0x00;uint8_t waitFor = 0x00;/*根据命令设置标志位*/switch(cmd){case MFRC_AUTHENT: //Mifare认证irqEn = 0x12;waitFor = 0x10; //idleIRq中断标志break;case MFRC_TRANSCEIVE: //发送并接收数据irqEn = 0x77;waitFor = 0x30; //RxIRq和idleIRq中断标志break;}/*发送命令帧前准备*/MFRC_WriteReg(MFRC_ComIEnReg, irqEn | 0x80); //开中断MFRC_ClrBitMask(MFRC_ComIrqReg, 0x80); //清除中断标志位SET1MFRC_WriteReg(MFRC_CommandReg, MFRC_IDLE); //取消当前命令的执行MFRC_SetBitMask(MFRC_FIFOLevelReg, 0x80); //清除FIFO缓冲区及其标志位/*发送命令帧*/for(i = 0; i < InLenByte; i++) //写入命令参数{MFRC_WriteReg(MFRC_FIFODataReg, pInData[i]);}MFRC_WriteReg(MFRC_CommandReg, cmd); //执行命令if(cmd == MFRC_TRANSCEIVE){MFRC_SetBitMask(MFRC_BitFramingReg, 0x80); //启动发送}i = 300000; //根据时钟频率调整,操作M1卡最大等待时间25msdo{n = MFRC_ReadReg(MFRC_ComIrqReg);i--;}while((i != 0) && !(n & 0x01) && !(n & waitFor)); //等待命令完成MFRC_ClrBitMask(MFRC_BitFramingReg, 0x80); //停止发送/*处理接收的数据*/if(i != 0){if(!(MFRC_ReadReg(MFRC_ErrorReg) & 0x1B)){status = MFRC_OK;if(n & irqEn & 0x01){status = MFRC_NOTAGERR;}if(cmd == MFRC_TRANSCEIVE){n = MFRC_ReadReg(MFRC_FIFOLevelReg);lastBits = MFRC_ReadReg(MFRC_ControlReg) & 0x07;if (lastBits){*pOutLenBit = (n - 1) * 8 + lastBits;}else{*pOutLenBit = n * 8;}if(n == 0){n = 1;}if(n > MFRC_MAXRLEN){n = MFRC_MAXRLEN;}for(i = 0; i < n; i++){pOutData[i] = MFRC_ReadReg(MFRC_FIFODataReg);}}}else{status = MFRC_ERR;}}MFRC_SetBitMask(MFRC_ControlReg, 0x80); //停止定时器运行MFRC_WriteReg(MFRC_CommandReg, MFRC_IDLE); //取消当前命令的执行return status;
}/**************************************************************************************
* 函数名称:PCD_Reset
* 功能描述:PCD复位
* 入口参数:无
* 出口参数:无
* 返 回 值:无
* 说 明:无
***************************************************************************************/
void PCD_Reset(void)
{/*硬复位*/RS522_RST(1);//用到复位引脚osDelay(2);RS522_RST(0);osDelay(2);RS522_RST(1);osDelay(2);/*软复位*/MFRC_WriteReg(MFRC_CommandReg, MFRC_RESETPHASE);osDelay(2);/*复位后的初始化配置*/MFRC_WriteReg(MFRC_ModeReg, 0x3D); //CRC初始值0x6363MFRC_WriteReg(MFRC_TReloadRegL, 30); //定时器重装值MFRC_WriteReg(MFRC_TReloadRegH, 0);MFRC_WriteReg(MFRC_TModeReg, 0x8D); //定时器设置MFRC_WriteReg(MFRC_TPrescalerReg, 0x3E); //定时器预分频值MFRC_WriteReg(MFRC_TxAutoReg, 0x40); //100%ASKPCD_AntennaOff(); //关天线osDelay(2);PCD_AntennaOn(); //开天线}/**************************************************************************************
* 函数名称:PCD_AntennaOn
* 功能描述:开启天线,使能PCD发送能量载波信号
* 入口参数:无
* 出口参数:无
* 返 回 值:无
* 说 明:每次开启或关闭天线之间应至少有1ms的间隔
***************************************************************************************/
void PCD_AntennaOn(void)
{uint8_t temp;temp = MFRC_ReadReg(MFRC_TxControlReg);if (!(temp & 0x03)){MFRC_SetBitMask(MFRC_TxControlReg, 0x03);}
}/**************************************************************************************
* 函数名称:PCD_AntennaOff
* 功能描述:关闭天线,失能PCD发送能量载波信号
* 入口参数:无
* 出口参数:无
* 返 回 值:无
* 说 明:每次开启或关闭天线之间应至少有1ms的间隔
***************************************************************************************/
void PCD_AntennaOff(void)
{MFRC_ClrBitMask(MFRC_TxControlReg, 0x03);
}/***************************************************************************************
* 函数名称:PCD_Init
* 功能描述:读写器初始化
* 入口参数:无
* 出口参数:无
* 返 回 值:无
* 说 明:无
***************************************************************************************/
void PCD_Init(void)
{MFRC_Init(); //MFRC管脚配置PCD_Reset(); //PCD复位 并初始化配置PCD_AntennaOff(); //关闭天线PCD_AntennaOn(); //开启天线
}/***************************************************************************************
* 函数名称:PCD_Request
* 功能描述:寻卡
* 入口参数: -RequestMode:讯卡方式
* PICC_REQIDL:寻天线区内未进入休眠状态
* PICC_REQALL:寻天线区内全部卡
* -pCardType:用于保存卡片类型
* 出口参数:-pCardType:卡片类型
* 0x4400:Mifare_UltraLight
* 0x0400:Mifare_One(S50)
* 0x0200:Mifare_One(S70)
* 0x0800:Mifare_Pro(X)
* 0x4403:Mifare_DESFire
* 返 回 值:-status:错误代码(PCD_OK、PCD_NOTAGERR、PCD_ERR)
* 说 明:无
***************************************************************************************/
char PCD_Request(uint8_t RequestMode, uint8_t *pCardType)
{int status;uint16_t unLen;uint8_t CmdFrameBuf[MFRC_MAXRLEN];MFRC_ClrBitMask(MFRC_Status2Reg, 0x08);//关内部温度传感器MFRC_WriteReg(MFRC_BitFramingReg, 0x07); //存储模式,发送模式,是否启动发送等MFRC_SetBitMask(MFRC_TxControlReg, 0x03);//配置调制信号13.56MHZCmdFrameBuf[0] = RequestMode;status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 1, CmdFrameBuf, &unLen);if((status == PCD_OK) && (unLen == 0x10)){*pCardType = CmdFrameBuf[0];*(pCardType + 1) = CmdFrameBuf[1];}return status;
}/***************************************************************************************
* 函数名称:PCD_Anticoll
* 功能描述:防冲突,获取卡号
* 入口参数:-preadUidr:用于保存卡片序列号,4字节
* 出口参数:-preadUidr:卡片序列号,4字节
* 返 回 值:-status:错误代码(PCD_OK、PCD_NOTAGERR、PCD_ERR)
* 说 明:无
***************************************************************************************/
char PCD_Anticoll(uint8_t *preadUidr)
{char status;uint8_t i, readUidr_check = 0;uint16_t unLen;uint8_t CmdFrameBuf[MFRC_MAXRLEN];MFRC_ClrBitMask(MFRC_Status2Reg, 0x08);MFRC_WriteReg(MFRC_BitFramingReg, 0x00);MFRC_ClrBitMask(MFRC_CollReg, 0x80);CmdFrameBuf[0] = PICC_ANTICOLL1;CmdFrameBuf[1] = 0x20;status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 2, CmdFrameBuf, &unLen);if(status == PCD_OK){for(i = 0; i < 4; i++){*(preadUidr + i) = CmdFrameBuf[i];readUidr_check ^= CmdFrameBuf[i];}if(readUidr_check != CmdFrameBuf[i]){status = PCD_ERR;}}MFRC_SetBitMask(MFRC_CollReg, 0x80);return status;
}/***************************************************************************************
* 函数名称:PCD_Select
* 功能描述:选卡
* 入口参数:-preadUidr:卡片序列号,4字节
* 出口参数:无
* 返 回 值:-status:错误代码(PCD_OK、PCD_NOTAGERR、PCD_ERR)
* 说 明:无
***************************************************************************************/
char PCD_Select(uint8_t *preadUidr)
{char status;uint8_t i;uint16_t unLen;uint8_t CmdFrameBuf[MFRC_MAXRLEN];CmdFrameBuf[0] = PICC_ANTICOLL1;CmdFrameBuf[1] = 0x70;CmdFrameBuf[6] = 0;for(i = 0; i < 4; i++){CmdFrameBuf[i + 2] = *(preadUidr + i);CmdFrameBuf[6] ^= *(preadUidr + i);}MFRC_CalulateCRC(CmdFrameBuf, 7, &CmdFrameBuf[7]);MFRC_ClrBitMask(MFRC_Status2Reg, 0x08);status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 9, CmdFrameBuf, &unLen);if((status == PCD_OK) && (unLen == 0x18)){status = PCD_OK;}else{status = PCD_ERR;}return status;
}/***************************************************************************************
* 函数名称:PCD_AuthState
* 功能描述:验证卡片密码
* 入口参数:-AuthMode:验证模式
* PICC_AUTHENT1A:验证A密码
* PICC_AUTHENT1B:验证B密码
* -BlockAddr:块地址(0~63)
* -pKey:密码
* -preadUidr:卡片序列号,4字节
* 出口参数:无
* 返 回 值:-status:错误代码(PCD_OK、PCD_NOTAGERR、PCD_ERR)
* 说 明:验证密码时,以扇区为单位,BlockAddr参数可以是同一个扇区的任意块
***************************************************************************************/
char PCD_AuthState(uint8_t AuthMode, uint8_t BlockAddr, uint8_t *pKey, uint8_t *preadUidr)
{char status;uint16_t unLen;uint8_t i, CmdFrameBuf[MFRC_MAXRLEN];CmdFrameBuf[0] = AuthMode;CmdFrameBuf[1] = BlockAddr;for(i = 0; i < 6; i++){CmdFrameBuf[i + 2] = *(pKey + i);}for(i = 0; i < 4; i++){CmdFrameBuf[i + 8] = *(preadUidr + i);}status = MFRC_CmdFrame(MFRC_AUTHENT, CmdFrameBuf, 12, CmdFrameBuf, &unLen);if((status != PCD_OK) || (!(MFRC_ReadReg(MFRC_Status2Reg) & 0x08))){status = PCD_ERR;}return status;
}/***************************************************************************************
* 函数名称:PCD_WriteBlock
* 功能描述:读MF1卡数据块
* 入口参数:-BlockAddr:块地址
* -pData: 用于保存待写入的数据,16字节
* 出口参数:无
* 返 回 值:-status:错误代码(PCD_OK、PCD_NOTAGERR、PCD_ERR)
* 说 明:无
***************************************************************************************/
char PCD_WriteBlock(uint8_t BlockAddr, uint8_t *pData)
{char status;uint16_t unLen;uint8_t i, CmdFrameBuf[MFRC_MAXRLEN];CmdFrameBuf[0] = PICC_WRITE;CmdFrameBuf[1] = BlockAddr;MFRC_CalulateCRC(CmdFrameBuf, 2, &CmdFrameBuf[2]);status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 4, CmdFrameBuf, &unLen);if((status != PCD_OK) || (unLen != 4) || ((CmdFrameBuf[0] & 0x0F) != 0x0A)){status = PCD_ERR;}if(status == PCD_OK){for(i = 0; i < 16; i++){CmdFrameBuf[i] = *(pData + i);}MFRC_CalulateCRC(CmdFrameBuf, 16, &CmdFrameBuf[16]);status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 18, CmdFrameBuf, &unLen);if((status != PCD_OK) || (unLen != 4) || ((CmdFrameBuf[0] & 0x0F) != 0x0A)){status = PCD_ERR;}}return status;
}/***************************************************************************************
* 函数名称:PCD_ReadBlock
* 功能描述:读MF1卡数据块
* 入口参数:-BlockAddr:块地址
* -pData: 用于保存读出的数据,16字节
* 出口参数:-pData: 用于保存读出的数据,16字节
* 返 回 值:-status:错误代码(PCD_OK、PCD_NOTAGERR、PCD_ERR)
* 说 明:无
***************************************************************************************/
char PCD_ReadBlock(uint8_t BlockAddr, uint8_t *pData)
{char status;uint16_t unLen;uint8_t i, CmdFrameBuf[MFRC_MAXRLEN];CmdFrameBuf[0] = PICC_READ;CmdFrameBuf[1] = BlockAddr;MFRC_CalulateCRC(CmdFrameBuf, 2, &CmdFrameBuf[2]);status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 4, CmdFrameBuf, &unLen);if((status == PCD_OK) && (unLen == 0x90)){for(i = 0; i < 16; i++){*(pData + i) = CmdFrameBuf[i];}}else{status = PCD_ERR;}return status;
}/***************************************************************************************
* 函数名称:PCD_Value
* 功能描述:对MF1卡数据块增减值操作
* 入口参数:
* -BlockAddr:块地址
* -pValue:四字节增值的值,低位在前
* -mode:数值块操作模式
* PICC_INCREMENT:增值
* PICC_DECREMENT:减值
* 出口参数:无
* 返 回 值:-status:错误代码(PCD_OK、PCD_NOTAGERR、PCD_ERR)
* 说 明:无
***************************************************************************************/
char PCD_Value(uint8_t mode, uint8_t BlockAddr, uint8_t *pValue)
{//0XC1 1 Increment[4]={0x03, 0x01, 0x01, 0x01};char status;uint16_t unLen;uint8_t i, CmdFrameBuf[MFRC_MAXRLEN];CmdFrameBuf[0] = mode;CmdFrameBuf[1] = BlockAddr;MFRC_CalulateCRC(CmdFrameBuf, 2, &CmdFrameBuf[2]);status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 4, CmdFrameBuf, &unLen);if((status != PCD_OK) || (unLen != 4) || ((CmdFrameBuf[0] & 0x0F) != 0x0A)){status = PCD_ERR;}if(status == PCD_OK){for(i = 0; i < 16; i++){CmdFrameBuf[i] = *(pValue + i);}MFRC_CalulateCRC(CmdFrameBuf, 4, &CmdFrameBuf[4]);unLen = 0;status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 6, CmdFrameBuf, &unLen);if(status != PCD_ERR){status = PCD_OK;}}if(status == PCD_OK){CmdFrameBuf[0] = PICC_TRANSFER;CmdFrameBuf[1] = BlockAddr;MFRC_CalulateCRC(CmdFrameBuf, 2, &CmdFrameBuf[2]);status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 4, CmdFrameBuf, &unLen);if((status != PCD_OK) || (unLen != 4) || ((CmdFrameBuf[0] & 0x0F) != 0x0A)){status = PCD_ERR;}}return status;
}/***************************************************************************************
* 函数名称:PCD_BakValue
* 功能描述:备份钱包(块转存)
* 入口参数:-sourceBlockAddr:源块地址
* -goalBlockAddr :目标块地址
* 出口参数:无
* 返 回 值:-status:错误代码(PCD_OK、PCD_NOTAGERR、PCD_ERR)
* 说 明:只能在同一个扇区内转存
***************************************************************************************/
char PCD_BakValue(uint8_t sourceBlockAddr, uint8_t goalBlockAddr)
{char status;uint16_t unLen;uint8_t CmdFrameBuf[MFRC_MAXRLEN];CmdFrameBuf[0] = PICC_RESTORE;CmdFrameBuf[1] = sourceBlockAddr;MFRC_CalulateCRC(CmdFrameBuf, 2, &CmdFrameBuf[2]);status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 4, CmdFrameBuf, &unLen);if((status != PCD_OK) || (unLen != 4) || ((CmdFrameBuf[0] & 0x0F) != 0x0A)){status = PCD_ERR;}if(status == PCD_OK){CmdFrameBuf[0] = 0;CmdFrameBuf[1] = 0;CmdFrameBuf[2] = 0;CmdFrameBuf[3] = 0;MFRC_CalulateCRC(CmdFrameBuf, 4, &CmdFrameBuf[4]);status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 6, CmdFrameBuf, &unLen);if(status != PCD_ERR){status = PCD_OK;}}if(status != PCD_OK){return PCD_ERR;}CmdFrameBuf[0] = PICC_TRANSFER;CmdFrameBuf[1] = goalBlockAddr;MFRC_CalulateCRC(CmdFrameBuf, 2, &CmdFrameBuf[2]);status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 4, CmdFrameBuf, &unLen);if((status != PCD_OK) || (unLen != 4) || ((CmdFrameBuf[0] & 0x0F) != 0x0A)){status = PCD_ERR;}return status;
}/***************************************************************************************
* 函数名称:PCD_Halt
* 功能描述:命令卡片进入休眠状态
* 入口参数:无
* 出口参数:无
* 返 回 值:-status:错误代码(PCD_OK、PCD_NOTAGERR、PCD_ERR)
* 说 明:无
***************************************************************************************/
char PCD_Halt(void)
{char status;uint16_t unLen;uint8_t CmdFrameBuf[MFRC_MAXRLEN];CmdFrameBuf[0] = PICC_HALT;CmdFrameBuf[1] = 0;MFRC_CalulateCRC(CmdFrameBuf, 2, &CmdFrameBuf[2]);status = MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 4, CmdFrameBuf, &unLen);return status;
}
(2)rc522.c的核心代码
代码思路,在第一扇区第一块中写入特定的数字,表示录入卡号权限;在第一扇区第一块中删除掉相关数字,表示删除卡号权限。读取卡号时,就通过读取在第一扇区第一块中是否有特定的数字即可确认是否有权限。具体相见代码,IC_RW_control(uint8_t RW) 中,RW为1时表示读,RW为0时表示写。
uint8_t readUid[5]; //卡号
uint8_t CT[3]; //卡类型
/* 利用读写来管理卡号权限 */
uint8_t addr = 0x01*4 + 0x01; // 总共16个扇区。一个扇区4个块,从0开始算,表示第一扇区第一块
uint8_t DATA[16];
uint8_t AddCode[16] = {2,2,3,4,5,6,7,8}; //存放数据
uint8_t ClearCode[16] = {0,0,0,0,0,0,0,0}; //存放数据
uint8_t CardFlag = 1;
uint16_t Cardtemp;
uint8_t CardErrorNum; //刷卡错误次数void IC_RW_control(uint8_t RW) //1为R,0为W,利用读来开锁,写来进行权限管理
{uint8_t i;status = PCD_Request(0x52, CT); //找到卡返回0if(!status) //寻卡成功{status = PCD_ERR;status = PCD_Anticoll(readUid); //防冲撞}if(!status) //防冲撞成功{status = PCD_ERR;//printf("卡的类型为:%x%x%x\r\n",CT[0],CT[1],CT[2]); /* 读取卡的类型 *///printf("卡号:%x-%x-%x-%x\r\n",readUid[0],readUid[1],readUid[2],readUid[3]);OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Card ID:");OLED_ShowHexNum(2,1,readUid[0],2);OLED_ShowString(2,3,"-"); OLED_ShowHexNum(2,4,readUid[1],2);OLED_ShowString(2,6,"-"); OLED_ShowHexNum(2,7,readUid[2],2);OLED_ShowString(2,9,"-"); OLED_ShowHexNum(2,10,readUid[3],2);HAL_Delay(1000);status=PCD_Select(readUid); /* 选卡 */}if(!status) //选卡成功{status = PCD_ERR;// 验证A密钥 块地址 密码 SN status = PCD_AuthState(PICC_AUTHENT1A, addr, KEY, readUid);if(status == PCD_OK)//验证A成功{//printf("A密钥验证成功\r\n");HAL_Delay(1000);}else{//printf("A密钥验证失败\r\n");HAL_Delay(1000);}// 验证B密钥 块地址 密码 SN status = PCD_AuthState(PICC_AUTHENT1B, addr, KEY, readUid);if(status == PCD_OK)//验证B成功{//printf("B密钥验证成功\r\n");}else{//printf("B密钥验证失败\r\n"); }HAL_Delay(1000);} if(RW == 1 && status == PCD_OK) //读卡操作{status = PCD_ERR;status = PCD_ReadBlock(addr, DATA); if(status == PCD_OK)//读卡成功{ printf("1扇区1块DATA:");for(i = 0; i < 16; i++){printf("%02x", DATA[i]);}printf("\r\n");for(i = 0; i < 16; i++){if(DATA[i] != AddCode[i]){CardFlag = 0; //卡号无权限错误标志位}}if(!CardFlag) //卡号无权限{CardFlag = 1;OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"ID ERROR");CardErrorNum++;OLED_ShowString(2,1,"Error Num:");OLED_ShowNum(2,11,CardErrorNum,1);if(CardErrorNum == 3){HAL_Delay(2000);OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Buzzer~");beep_handle();CardErrorNum = 0;HAL_Delay(2000);}}else if(i == 16 && CardFlag){OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"ID RIGHT");OLED_ShowString(2,1,"Open the door~");servohandle();rtc_get_time();printf("卡号:%x-%x-%x-%x\r\n",readUid[0],readUid[1],readUid[2],readUid[3]);sprintf((char *)CTbuf, "Time:%04d-%02d-%02d %02d:%02d:%02d", calendar.year, calendar.month, calendar.date, calendar.hour, calendar.min, calendar.sec);printf("人员出入时间:%04d-%02d-%02d %02d:%02d:%02d\r\n", calendar.year, calendar.month, calendar.date, calendar.hour, calendar.min, calendar.sec);}}else{OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"NO CARD");}}else if(RW == 0 && status == PCD_OK) //写卡操作{modeflag = 0;status = PCD_ERR;while(!modeflag){Cardtemp = key_scan();if(modeflag == 1){status = PCD_WriteBlock(addr, ClearCode);OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Delete the card."); }else if(modeflag == 2){status = PCD_WriteBlock(addr, AddCode);OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Add the card."); }}}HAL_Delay(2000);OLED_MENU_2nd_Display();}
(3)SPI.c
#include "./BSP/SPI/spi.h"
#include "main.h"/* USER CODE BEGIN 0 *//* USER CODE END 0 */SPI_HandleTypeDef hspi2;/* SPI2 init function */
void MX_SPI2_Init(void)
{hspi2.Instance = SPI2;hspi2.Init.Mode = SPI_MODE_MASTER;hspi2.Init.Direction = SPI_DIRECTION_2LINES;hspi2.Init.DataSize = SPI_DATASIZE_8BIT;hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;hspi2.Init.CLKPhase = SPI_PHASE_1EDGE;hspi2.Init.NSS = SPI_NSS_SOFT;hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32;hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;hspi2.Init.TIMode = SPI_TIMODE_DISABLE;hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;hspi2.Init.CRCPolynomial = 10;if (HAL_SPI_Init(&hspi2) != HAL_OK){Error_Handler();}}void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{GPIO_InitTypeDef GPIO_InitStruct = {0};if(spiHandle->Instance==SPI2){/* USER CODE BEGIN SPI2_MspInit 0 *//* USER CODE END SPI2_MspInit 0 *//* SPI2 clock enable */__HAL_RCC_SPI2_CLK_ENABLE();SPI2_GPIO_CLK_ENABLE();/**SPI2 GPIO Configuration PB13 ------> SPI2_SCKPB14 ------> SPI2_MISOPB15 ------> SPI2_MOSI */GPIO_InitStruct.Pin = SPI2_SCK_GPIO_PIN | SPI2_MOSI_GPIO_PIN;GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(SPI2_GPIO_PORT, &GPIO_InitStruct);GPIO_InitStruct.Pin = SPI2_MISO_GPIO_PIN;GPIO_InitStruct.Mode = GPIO_MODE_INPUT;GPIO_InitStruct.Pull = GPIO_NOPULL;HAL_GPIO_Init(SPI2_GPIO_PORT, &GPIO_InitStruct);GPIO_InitTypeDef GPIO_InitStruct = {0};/*PB12为片选引脚(CS/NSS/SDA)PB4为RC522的RST引脚*//*Configure GPIO pins : PB12 PB4 */GPIO_InitStruct.Pin = SPI2_SDA_GPIO_PIN | RC522_RST_GPIO_PIN;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(SPI2_GPIO_PORT, &GPIO_InitStruct);/*Configure GPIO pin Output Level */HAL_GPIO_WritePin(SPI2_GPIO_PORT, SPI2_SDA_GPIO_PIN | RC522_RST_GPIO_PIN, GPIO_PIN_SET); /* 必须要拉高 */}
}
2、key.c
下面为扫描代码和模式选择代码。
/*ABCD的ASCII码(16进制)分别为41、42、43、44*的ASCII码为2a#的ASCII码为23
*/
uint8_t key_scan(void)
{uint16_t key_val=0;uint16_t temp=0;/*************Scan 1st Line************************/GPIOA->ODR=0X00;GPIOA->ODR=0XF7;if((GPIOA->IDR&0XF0)!=0XF0){delay_ms(30);if((GPIOA->IDR & 0XF0)!=0XF0){delay_ms(30);temp=(GPIOA->IDR&0XF7);switch(temp){case 0xE7:{ key_val=1; flag = 1; break;}case 0xD7:{ key_val=2; flag = 1; break;}case 0xB7:{ key_val=3; flag = 1; break;}case 0x77:{ key_val='A'; break;}}}}/*************Scan 2st Line************************/GPIOA->ODR=0X00;GPIOA->ODR=0XFB;if((GPIOA->IDR&0XF0)!=0XF0){delay_ms(30);if((GPIOA->IDR & 0XF0)!=0XF0){delay_ms(30);temp=(GPIOA->IDR&0XFB);switch(temp){case 0xEB:{ key_val=4; flag = 1; break;}case 0xDB:{ key_val=5; flag = 1; break;}case 0xBB:{ key_val=6; flag = 1; break;}case 0x7B:{ key_val='B'; break;}}}}/*************Scan 3st Line************************/GPIOA->ODR=0X00;GPIOA->ODR=0XFD;if((GPIOA->IDR&0XF0)!=0XF0){delay_ms(30);if((GPIOA->IDR & 0XF0)!=0XF0){delay_ms(30);temp=(GPIOA->IDR&0XFD);switch(temp){case 0xED:{ key_val=7; flag = 1; break;}case 0xDD:{ key_val=8; flag = 1; break;}case 0xBD:{ key_val=9; flag = 1; break;}case 0x7D:{ key_val='C';modeflag = 3; break;}}}}/*************Scan 4st Line************************/GPIOA->ODR=0X00;GPIOA->ODR=0XFE;if((GPIOA->IDR&0XF0)!=0XF0){delay_ms(30);if((GPIOA->IDR & 0XF0)!=0XF0){delay_ms(30);temp=(GPIOA->IDR&0XFE);switch(temp){case 0xEE:{ key_val='*'; modeflag = 1; break;}case 0xDE:{ key_val=0; flag = 1; break;}case 0xBE:{ key_val='#'; modeflag = 2; break;}case 0x7E:{ key_val='D'; modeflag = 4; break;}}}}return key_val;}
uint16_t mode = 0;void select_mode(void)
{modeflag = 0;mode = key_scan();switch(mode){case 0x41:{OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"A-Card Unlock");IC_RW_control(1);break; //A:刷卡解锁}case 0x42:{OLED_Clear(); //OLED清屏OLED_ShowString(2,1,"B-Card Managing"); IC_RW_control(0);break; //B:卡片权限管理}case 0x43:{OLED_Clear(); //OLED清屏OLED_ShowString(3,1,"C-Code Unlock"); keyin_and_judge();break; //C:密码解锁}case 0x44:{OLED_Clear(); //OLED清屏OLED_ShowString(4,1,"D-Code Changing"); Chandge_passwd();break; //D:修改密码}case 0x2a:OLED_MENU_1st_Display();break;//*:确认/第一菜单显示实时时间case 0x23:OLED_MENU_2nd_Display();break;//#:删除/第二菜单显示功能界面}}
键入密码和修改密码的代码如下。值得注意的是,修改密码时,利用stmflash_write(FLASH_SAVE_ADDR, (uint16_t *)passwd, 4)函数,可以把已修改的密码保存到flash中去。stmflash_read(FLASH_SAVE_ADDR, (uint16_t *)passwd_save, 4)函数是读取掉电前保存在flash中的密码,可以实现设置密码掉电不丢失的效果。另外,读写卡代码于rc522.c中已给出。
uint16_t passwd[4]; //键入的密码缓冲数组
uint16_t passwd_save[4]; //flash存储值缓冲数组,即已修改的密码
uint16_t keyvalue; //按键按下返回值
uint16_t temp; //暂存值
uint8_t count = 0; //密码位数
uint8_t error; //错误标志/* key_and_judge函数使用 */
uint8_t KeyErrorNum; //输入密码错误次数
uint8_t KTbuf[40]; //存储当前时间信息
uint16_t overtime; //超时跳回主页设置void key_in(void)
{flag = 0; //必须要有 temp = key_scan();if(flag){keyvalue = key_scan();if(temp == keyvalue) //可以消抖{passwd[count] = keyvalue;OLED_ShowNum(2,count + 1,passwd[count],1);if(modeflag == 3) //仅密码解锁模式时,对比已存密码{if(passwd[count] != passwd_save[count]){error = 1;}}count++;flag = 0;}}
}void keyin_and_judge(void)
{//VISION TWOstmflash_read(FLASH_SAVE_ADDR, (uint16_t *)passwd_save, 4); //保存掉电前的密码,实现设置密码掉电不丢失的效果temp = key_scan();OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Input passwd:");while(count < 4){key_in(); }HAL_Delay(2000);if(count == 4){if(error){OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Passwd Error!");KeyErrorNum++;OLED_ShowString(2,1,"Error Num:");OLED_ShowNum(2,11,KeyErrorNum,1);count = 0;error = 0;if(KeyErrorNum == 3){HAL_Delay(2000);OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Buzzer~");beep_handle();KeyErrorNum = 0;HAL_Delay(2000);}}else{OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Passwd Right!");OLED_ShowString(2,1,"Open the door~");servohandle();rtc_get_time();sprintf((char *)KTbuf, "Time:%04d-%02d-%02d %02d:%02d:%02d", calendar.year, calendar.month, calendar.date, calendar.hour, calendar.min, calendar.sec);printf("人员出入时间:%04d-%02d-%02d %02d:%02d:%02d\r\n", calendar.year, calendar.month, calendar.date, calendar.hour, calendar.min, calendar.sec);count = 0;}} modeflag = 0;HAL_Delay(2000);OLED_MENU_2nd_Display();}void Chandge_passwd(void)
{temp = key_scan();if(modeflag == 4){keyvalue = key_scan();if(temp == keyvalue) //可以消抖{if(keyvalue == 'D'){OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Input new passwd");flag = 0;while(count < 4){key_in();}count = 0;stmflash_write(FLASH_SAVE_ADDR, (uint16_t *)passwd, 4);stmflash_read(FLASH_SAVE_ADDR, (uint16_t *)passwd_save, 4);OLED_ShowString(3,1,"Successful~");HAL_Delay(2000);OLED_MENU_2nd_Display();}}modeflag = 0;}
}
3、OLED.c
/*************笔记****************1、接线:B6->GNDB7->VCCB8->SCLB9->SDL***********************************/#include "./BSP/OLED/OLED.h"
#include "./BSP/OLED/OLED_Font.h"
#include "./SYSTEM/sys/sys.h"
#include "./BSP/RTC/rtc.h"
#include "./BSP/KEY/key.h"void OLED_I2C_Init(void)
{/* 1、时钟使能 */OLED_GPIO_CLK_ENABLE();/* 2、引脚初始化 */GPIO_InitTypeDef g_init_struct;g_init_struct.Pin = OLED_SCL_GPIO_PIN;g_init_struct.Mode = GPIO_MODE_OUTPUT_PP;g_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;g_init_struct.Pull = GPIO_PULLUP;HAL_GPIO_Init(OLED_GPIO_PORT, &g_init_struct);g_init_struct.Pin = OLED_SDL_GPIO_PIN;HAL_GPIO_Init(OLED_GPIO_PORT, &g_init_struct);OLED_W_SCL(1);OLED_W_SDA(1);
}/*** @brief I2C开始* @param 无* @retval 无*/
void OLED_I2C_Start(void)
{OLED_W_SDA(1);OLED_W_SCL(1);OLED_W_SDA(0);OLED_W_SCL(0);
}/*** @brief I2C停止* @param 无* @retval 无*/
void OLED_I2C_Stop(void)
{OLED_W_SDA(0);OLED_W_SCL(1);OLED_W_SDA(1);
}/*** @brief I2C发送一个字节* @param Byte 要发送的一个字节* @retval 无*/
void OLED_I2C_SendByte(uint8_t Byte)
{uint8_t i;for (i = 0; i < 8; i++){OLED_W_SDA(Byte & (0x80 >> i));OLED_W_SCL(1);OLED_W_SCL(0);}OLED_W_SCL(1); //额外的一个时钟,不处理应答信号OLED_W_SCL(0);
}/*** @brief OLED写命令* @param Command 要写入的命令* @retval 无*/
void OLED_WriteCommand(uint8_t Command)
{OLED_I2C_Start();OLED_I2C_SendByte(0x78); //从机地址OLED_I2C_SendByte(0x00); //写命令OLED_I2C_SendByte(Command); OLED_I2C_Stop();
}/*** @brief OLED写数据* @param Data 要写入的数据* @retval 无*/
void OLED_WriteData(uint8_t Data)
{OLED_I2C_Start();OLED_I2C_SendByte(0x78); //从机地址OLED_I2C_SendByte(0x40); //写数据OLED_I2C_SendByte(Data);OLED_I2C_Stop();
}/*** @brief OLED设置光标位置* @param Y 以左上角为原点,向下方向的坐标,范围:0~7* @param X 以左上角为原点,向右方向的坐标,范围:0~127* @retval 无*/
void OLED_SetCursor(uint8_t Y, uint8_t X)
{OLED_WriteCommand(0xB0 | Y); //设置Y位置OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //设置X位置高4位OLED_WriteCommand(0x00 | (X & 0x0F)); //设置X位置低4位
}/*** @brief OLED清屏* @param 无* @retval 无*/
void OLED_Clear(void)
{ uint8_t i, j;for (j = 0; j < 8; j++){OLED_SetCursor(j, 0);for(i = 0; i < 128; i++){OLED_WriteData(0x00);}}
}/*** @brief OLED显示一个字符* @param Line 行位置,范围:1~4* @param Column 列位置,范围:1~16* @param Char 要显示的一个字符,范围:ASCII可见字符* @retval 无*/
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{ uint8_t i;OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //设置光标位置在上半部分for (i = 0; i < 8; i++){OLED_WriteData(OLED_F8x16[Char - ' '][i]); //显示上半部分内容}OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //设置光标位置在下半部分for (i = 0; i < 8; i++){OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //显示下半部分内容}
}/*** @brief OLED显示字符串* @param Line 起始行位置,范围:1~4* @param Column 起始列位置,范围:1~16* @param String 要显示的字符串,范围:ASCII可见字符* @retval 无*/
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{uint8_t i;for (i = 0; String[i] != '\0'; i++){OLED_ShowChar(Line, Column + i, String[i]);}
}/*** @brief OLED次方函数* @retval 返回值等于X的Y次方*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{uint32_t Result = 1;while (Y--){Result *= X;}return Result;
}/*** @brief OLED显示数字(十进制,正数)* @param Line 起始行位置,范围:1~4* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~4294967295* @param Length 要显示数字的长度,范围:1~10* @retval 无*/
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{uint8_t i;for (i = 0; i < Length; i++) {OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');}
}/*** @brief OLED显示数字(十进制,带符号数)* @param Line 起始行位置,范围:1~4* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:-2147483648~2147483647* @param Length 要显示数字的长度,范围:1~10* @retval 无*/
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{uint8_t i;uint32_t Number1;if (Number >= 0){OLED_ShowChar(Line, Column, '+');Number1 = Number;}else{OLED_ShowChar(Line, Column, '-');Number1 = -Number;}for (i = 0; i < Length; i++) {OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');}
}/*** @brief OLED显示数字(十六进制,正数)* @param Line 起始行位置,范围:1~4* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~0xFFFFFFFF* @param Length 要显示数字的长度,范围:1~8* @retval 无*/
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{uint8_t i, SingleNumber;for (i = 0; i < Length; i++) {SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;if (SingleNumber < 10){OLED_ShowChar(Line, Column + i, SingleNumber + '0');}else{OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');}}
}/*** @brief OLED显示数字(二进制,正数)* @param Line 起始行位置,范围:1~4* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~1111 1111 1111 1111* @param Length 要显示数字的长度,范围:1~16* @retval 无*/
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{uint8_t i;for (i = 0; i < Length; i++) {OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');}
}/*** @brief OLED初始化* @param 无* @retval 无*/
void OLED_Init(void)
{uint32_t i, j;for (i = 0; i < 1000; i++) //上电延时{for (j = 0; j < 1000; j++);}OLED_I2C_Init(); //端口初始化OLED_WriteCommand(0xAE); //关闭显示OLED_WriteCommand(0xD5); //设置显示时钟分频比/振荡器频率OLED_WriteCommand(0x80);OLED_WriteCommand(0xA8); //设置多路复用率OLED_WriteCommand(0x3F);OLED_WriteCommand(0xD3); //设置显示偏移OLED_WriteCommand(0x00);OLED_WriteCommand(0x40); //设置显示开始行OLED_WriteCommand(0xA1); //设置左右方向,0xA1正常 0xA0左右反置OLED_WriteCommand(0xC8); //设置上下方向,0xC8正常 0xC0上下反置OLED_WriteCommand(0xDA); //设置COM引脚硬件配置OLED_WriteCommand(0x12);OLED_WriteCommand(0x81); //设置对比度控制OLED_WriteCommand(0xCF);OLED_WriteCommand(0xD9); //设置预充电周期OLED_WriteCommand(0xF1);OLED_WriteCommand(0xDB); //设置VCOMH取消选择级别OLED_WriteCommand(0x30);OLED_WriteCommand(0xA4); //设置整个显示打开/关闭OLED_WriteCommand(0xA6); //设置正常/倒转显示OLED_WriteCommand(0x8D); //设置充电泵OLED_WriteCommand(0x14);OLED_WriteCommand(0xAF); //开启显示OLED_Clear(); //OLED清屏
}/* 像素点为64*128,64/16=4,128/8=16 */
void OLED_MENU_Display(void)
{OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Door Lock System");OLED_ShowString(2,7,"MENU");OLED_ShowString(3,6,"INITING");HAL_Delay(2500);OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"A-Card Unlock");OLED_ShowString(2,1,"B-Card Managing"); OLED_ShowString(3,1,"C-Code Unlock");OLED_ShowString(4,1,"D-Code Changing");
}uint8_t tbuf[40];void OLED_MENU_1st_Display(void)
{OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"Door Lock System");OLED_ShowString(2,4,"REAL TIME");rtc_get_time();sprintf((char *)tbuf, "Time:%04d-%02d-%02d %02d:%02d:%02d", calendar.year, calendar.month, calendar.date, calendar.hour, calendar.min, calendar.sec);OLED_ShowString(3,1,"Date:");OLED_ShowNum(3,6,calendar.year,4);OLED_ShowString(3,10,"-"); OLED_ShowNum(3,11,calendar.month,2);OLED_ShowString(3,13,"-"); OLED_ShowNum(3,14,calendar.date,2);OLED_ShowString(4,1,"Time:");OLED_ShowNum(4,6,calendar.hour,2);OLED_ShowString(4,8,"-"); OLED_ShowNum(4,9,calendar.min,2);OLED_ShowString(4,11,"-"); OLED_ShowNum(4,12,calendar.sec,2);HAL_Delay(400);}void OLED_MENU_2nd_Display(void)
{OLED_Clear(); //OLED清屏OLED_ShowString(1,1,"A-Card Unlock");OLED_ShowString(2,1,"B-Card Managing"); OLED_ShowString(3,1,"C-Code Unlock");OLED_ShowString(4,1,"D-Code Changing");
}
4、gtim.c与servo.c
(1)gtim.c
/*************笔记****************B5配置为TIM3通道2输出PWM波***********************************/#include "./BSP/TIMER/gtim.h"TIM_HandleTypeDef g_timx_pwm_chy_handle;/* 通用定时器PWM输出初始化函数 */
void gtim_timx_pwm_chy_init(uint16_t psc,uint16_t arr)
{TIM_OC_InitTypeDef timx_oc_pwm_chy;g_timx_pwm_chy_handle.Instance = TIM3;g_timx_pwm_chy_handle.Init.Prescaler = psc;g_timx_pwm_chy_handle.Init.Period = arr;g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 向上计数模式 */HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle);timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1; /* 模式选择PWM1 */timx_oc_pwm_chy.Pulse = 80; /* 设置比较值CCRx,此值用来确定占空比 *//* 这里默认设置比较值为自动重装载值的一半,即占空比为50% */timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_LOW; /* 设定CCxP的值,低电平有效 */HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle,&timx_oc_pwm_chy, TIM_CHANNEL_2); /* 配置TIM3通道2 */HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, TIM_CHANNEL_2); /* 开启对应PWM通道 */}/* 定时器输出PWM MSP初始化函数 */
/* 1、使能TIM时钟;2、使能IO时钟及引脚复用. */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM3){GPIO_InitTypeDef g_init_struct;__HAL_RCC_GPIOB_CLK_ENABLE();__HAL_RCC_TIM3_CLK_ENABLE();g_init_struct.Pin = GPIO_PIN_5;g_init_struct.Mode = GPIO_MODE_AF_PP; /* 推挽复用功能 */g_init_struct.Pull = GPIO_PULLUP; /* 上拉 */g_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */HAL_GPIO_Init(GPIOB, &g_init_struct);__HAL_RCC_AFIO_CLK_ENABLE(); /* 复用IO时钟使能 */__HAL_AFIO_REMAP_TIM3_PARTIAL(); /* 部分复用使能 */}
}
(2)servo.c
驱动舵机时,需要将舵机VCC接到单片机5V输出引脚上,并且需要将单片机接上USB线,才能有足够大的电流去驱动舵机,单接JLINK无法驱动。
#include "./SYSTEM/sys/sys.h"
#include "./BSP/TIMER/gtim.h"
#include "./BSP/SERVO/servo.h"
#include "./BSP/RC522/rc522.h"uint32_t ledpwmval = 80;/* 舵机转动九十度后归位,模拟门锁开关,数值根据自己情况来设定。B5输出PWM波驱动舵机转动由于舵机稳定性不高,有时候会出现不转动或者转动角度没有达到要求以及没有及时归位的现象
*/void servohandle(void)
{HAL_Delay(500);while(ledpwmval < 185){ledpwmval += 5;HAL_Delay(10);/* 修改比较值控制占空比 */__HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, TIM_CHANNEL_2, ledpwmval);}HAL_Delay(1000);while(ledpwmval > 80){ledpwmval -= 5;HAL_Delay(10);/* 修改比较值控制占空比 */__HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, TIM_CHANNEL_2, ledpwmval);}HAL_Delay(1000);}
5、beep.c
B1引脚(BEEP)作为蜂鸣器的引脚,低电平时蜂鸣器工作。需要外接驱动电路,单片机电流较小不足以驱动蜂鸣器工作。
#include "./SYSTEM/sys/sys.h"
#include "./BSP/BEEP/beep.h"void beep_init(void)
{ GPIO_InitTypeDef g_init_struct;BEEP_GPIO_CLK_ENABLE();g_init_struct.Pin = BEEP_GPIO_PIN;g_init_struct.Mode = GPIO_MODE_OUTPUT_PP;g_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;g_init_struct.Pull = GPIO_PULLUP;HAL_GPIO_Init(BEEP_GPIO_PORT, &g_init_struct);BEEP(1);
}void beep_handle(void)
{BEEP(0);HAL_Delay(1000);BEEP(1);}
6、stmflash.c
其中,其他文件中主要调用下面这两个接口函数,实现对flash的读写。值得注意的是,设置FLASH 保存地址(必须为偶数,且其值要大于本代码所占用FLASH的大小 + 0X08000000)。
void stmflash_read(uint32_t raddr, uint16_t *pbuf, uint16_t length); /* 从指定地址开始读出指定长度的数据 */
void stmflash_write(uint32_t waddr, uint16_t *pbuf, uint16_t length); /* 在FLASH 指定位置, 写入指定长度的数据(自动擦除) */
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/STMFLASH/stmflash.h"/*** @brief 从指定地址读取一个半字 (16位数据)* @param faddr : 读取地址 (此地址必须为2的倍数!!)* @retval 读取到的数据 (16位)*/
uint16_t stmflash_read_halfword(uint32_t faddr)
{return *(volatile uint16_t *)faddr;
}/*** @brief 从指定地址开始读出指定长度的数据* @param raddr : 起始地址* @param pbuf : 数据指针* @param length: 要读取的半字(16位)数,即2个字节的整数倍* @retval 无*/
void stmflash_read(uint32_t raddr, uint16_t *pbuf, uint16_t length)
{uint16_t i;for (i = 0; i < length; i++){pbuf[i] = stmflash_read_halfword(raddr); /* 读取2个字节 */raddr += 2; /* 偏移2个字节 */}
}/*** @brief 不检查的写入这个函数的假设已经把原来的扇区擦除过再写入* @param waddr : 起始地址 (此地址必须为2的倍数!!,否则写入出错!)* @param pbuf : 数据指针* @param length : 要写入的 半字(16位)数* @retval 无*/
void stmflash_write_nocheck(uint32_t waddr, uint16_t *pbuf, uint16_t length)
{uint16_t i;for (i = 0; i < length; i++){HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, waddr, pbuf[i]);waddr += 2; /* 指向下一个半字 */}
}/*** @brief 在FLASH 指定位置, 写入指定长度的数据(自动擦除)* @note 该函数往 STM32 内部 FLASH 指定位置写入指定长度的数据* 该函数会先检测要写入的扇区是否是空(全0XFFFF)的?, 如果* 不是, 则先擦除, 如果是, 则直接往扇区里面写入数据.* 数据长度不足扇区时,自动被回擦除前的数据* @param waddr : 起始地址 (此地址必须为2的倍数!!,否则写入出错!)* @param pbuf : 数据指针* @param length : 要写入的 半字(16位)数* @retval 无*/
uint16_t g_flashbuf[STM32_SECTOR_SIZE / 2]; /* 最多是2K字节 */
void stmflash_write(uint32_t waddr, uint16_t *pbuf, uint16_t length)
{uint32_t secpos; /* 扇区地址 */uint16_t secoff; /* 扇区内偏移地址(16位字计算) */uint16_t secremain; /* 扇区内剩余地址(16位字计算) */uint16_t i;uint32_t offaddr; /* 去掉0X08000000后的地址 */FLASH_EraseInitTypeDef flash_eraseop;uint32_t erase_addr; /* 擦除错误,这个值为发生错误的扇区地址 */if (waddr < STM32_FLASH_BASE || (waddr >= (STM32_FLASH_BASE + 1024 * STM32_FLASH_SIZE))){return; /* 非法地址 */}HAL_FLASH_Unlock(); /* FLASH解锁 */offaddr = waddr - STM32_FLASH_BASE; /* 实际偏移地址. */secpos = offaddr / STM32_SECTOR_SIZE; /* 扇区地址 0~127 for STM32F103RBT6 */secoff = (offaddr % STM32_SECTOR_SIZE) / 2; /* 在扇区内的偏移(2个字节为基本单位.) */secremain = STM32_SECTOR_SIZE / 2 - secoff; /* 扇区剩余空间大小 */if (length <= secremain){secremain = length; /* 不大于该扇区范围 */}while (1){stmflash_read(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE, g_flashbuf, STM32_SECTOR_SIZE / 2); /* 读出整个扇区的内容 */for (i = 0; i < secremain; i++) /* 校验数据 */{if (g_flashbuf[secoff + i] != 0XFFFF){break; /* 需要擦除 */}}if (i < secremain) /* 需要擦除 */{ flash_eraseop.TypeErase = FLASH_TYPEERASE_PAGES; /* 选择面擦除 */flash_eraseop.Banks = FLASH_BANK_1;flash_eraseop.NbPages = 1;flash_eraseop.PageAddress = secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE; /* 要擦除的扇区 */HAL_FLASHEx_Erase( &flash_eraseop, &erase_addr);for (i = 0; i < secremain; i++) /* 复制 */{g_flashbuf[i + secoff] = pbuf[i];}stmflash_write_nocheck(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE, g_flashbuf, STM32_SECTOR_SIZE / 2); /* 写入整个扇区 */}else{stmflash_write_nocheck(waddr, pbuf, secremain); /* 写已经擦除了的,直接写入扇区剩余区间. */}if (length == secremain){break; /* 写入结束了 */}else /* 写入未结束 */{secpos++; /* 扇区地址增1 */secoff = 0; /* 偏移位置为0 */pbuf += secremain; /* 指针偏移 */waddr += secremain * 2; /* 写地址偏移(16位数据地址,需要*2) */length -= secremain; /* 字节(16位)数递减 */if (length > (STM32_SECTOR_SIZE / 2)){secremain = STM32_SECTOR_SIZE / 2; /* 下一个扇区还是写不完 */}else{secremain = length; /* 下一个扇区可以写完了 */}}}HAL_FLASH_Lock(); /* 上锁 */
}
7、rtc.c
#include "./SYSTEM/sys/sys.h"
#include "./BSP/RTC/rtc.h"
#include "./BSP/LED/led.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"RTC_HandleTypeDef g_rtc_handle; /* RTC控制句柄 */
_calendar_obj calendar; /* 时间结构体 *//*** @brief RTC写入后备区域SRAM* @param bkrx : 后备区寄存器编号,范围:0~41对应 RTC_BKP_DR1~RTC_BKP_DR42* @param data : 要写入的数据,16位长度* @retval 无*/
void rtc_write_bkr(uint32_t bkrx, uint16_t data)
{HAL_PWR_EnableBkUpAccess(); /* 取消备份区写保护 */HAL_RTCEx_BKUPWrite(&g_rtc_handle, bkrx + 1, data);
}/*** @brief RTC读取后备区域SRAM* @param bkrx : 后备区寄存器编号,范围:0~41对应 RTC_BKP_DR1~RTC_BKP_DR42* @retval 读取到的值*/
uint16_t rtc_read_bkr(uint32_t bkrx)
{uint32_t temp = 0;temp = HAL_RTCEx_BKUPRead(&g_rtc_handle, bkrx + 1);return (uint16_t)temp; /* 返回读取到的值 */
}/*** @brief RTC初始化* @note* 默认尝试使用LSE,当LSE启动失败后,切换为LSI.* 通过BKP寄存器0的值,可以判断RTC使用的是LSE/LSI:* 当BKP0==0X5050时,使用的是LSE* 当BKP0==0X5051时,使用的是LSI* 注意:切换LSI/LSE将导致时间/日期丢失,切换后需重新设置.** @param 无* @retval 0,成功* 1,进入初始化模式失败*/
uint8_t rtc_init(void)
{/* 检查是不是第一次配置时钟 */uint16_t bkpflag = 0;__HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟 */__HAL_RCC_BKP_CLK_ENABLE(); /* 使能备份时钟 */HAL_PWR_EnableBkUpAccess(); /* 取消备份区写保护 */bkpflag = rtc_read_bkr(0); /* 读取BKP0的值 */g_rtc_handle.Instance = RTC;g_rtc_handle.Init.AsynchPrediv = 32767; /* 时钟周期设置,理论值:32767, 这里也可以用 RTC_AUTO_1_SECOND */g_rtc_handle.Init.OutPut = RTC_OUTPUTSOURCE_NONE;RCC->BDCR |= 1 << 16; /* 备份区域软复位 */RCC->BDCR &= ~(1 << 16); /* 备份区域软复位结束 */if (HAL_RTC_Init(&g_rtc_handle) != HAL_OK){return 1;}if ((bkpflag != 0X5050) && (bkpflag != 0x5051)) /* 之前未初始化过,重新配置 */{rtc_set_time(2023, 4, 2, 20, 25, 35); /* 设置时间 */}__HAL_RTC_ALARM_ENABLE_IT(&g_rtc_handle, RTC_IT_SEC); /* 允许秒中断 */HAL_NVIC_SetPriority(RTC_IRQn, 0x2, 0); /* 优先级设置 */HAL_NVIC_EnableIRQ(RTC_IRQn); /* 使能RTC中断通道 */rtc_get_time(); /* 更新时间 */return 0;
}/*** @brief RTC初始化* @note* RTC底层驱动,时钟配置,此函数会被HAL_RTC_Init()调用* @param hrtc:RTC句柄* @retval 无*/
void HAL_RTC_MspInit(RTC_HandleTypeDef *hrtc)
{uint16_t retry = 200;__HAL_RCC_RTC_ENABLE(); /* RTC时钟使能 */RCC_OscInitTypeDef rcc_oscinitstruct;RCC_PeriphCLKInitTypeDef rcc_periphclkinitstruct;/* 使用寄存器的方式去检测LSE是否可以正常工作 */RCC->BDCR |= 1 << 0; /* 开启外部低速振荡器LSE */while (retry && ((RCC->BDCR & 0X02) == 0)) /* 等待LSE准备好 */{retry--;delay_ms(5);}if (retry == 0) /* LSE起振失败 使用LSI */{rcc_oscinitstruct.OscillatorType = RCC_OSCILLATORTYPE_LSI; /* 选择要配置的振荡器 */rcc_oscinitstruct.LSEState = RCC_LSI_ON; /* LSI状态:开启 */rcc_oscinitstruct.PLL.PLLState = RCC_PLL_NONE; /* PLL无配置 */HAL_RCC_OscConfig(&rcc_oscinitstruct); /* 配置设置的rcc_oscinitstruct */rcc_periphclkinitstruct.PeriphClockSelection = RCC_PERIPHCLK_RTC; /* 选择要配置的外设 RTC */rcc_periphclkinitstruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; /* RTC时钟源选择 LSI */HAL_RCCEx_PeriphCLKConfig(&rcc_periphclkinitstruct); /* 配置设置的rcc_periphClkInitStruct */rtc_write_bkr(0, 0X5051);}else{rcc_oscinitstruct.OscillatorType = RCC_OSCILLATORTYPE_LSE ; /* 选择要配置的振荡器 */rcc_oscinitstruct.LSEState = RCC_LSE_ON; /* LSE状态:开启 */rcc_oscinitstruct.PLL.PLLState = RCC_PLL_NONE; /* PLL不配置 */HAL_RCC_OscConfig(&rcc_oscinitstruct); /* 配置设置的rcc_oscinitstruct */rcc_periphclkinitstruct.PeriphClockSelection = RCC_PERIPHCLK_RTC; /* 选择要配置外设 RTC */rcc_periphclkinitstruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; /* RTC时钟源选择LSE */HAL_RCCEx_PeriphCLKConfig(&rcc_periphclkinitstruct); /* 配置设置的rcc_periphclkinitstruct */rtc_write_bkr(0, 0X5050);}
}/*** @brief RTC时钟中断* @note 秒钟中断服务函数,顺带处理闹钟标志* 根据RTC_CRL寄存器的 SECF 和 ALRF 位区分是哪个中断* @param 无* @retval 无*/
void RTC_IRQHandler(void)
{if (__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_SEC) != RESET) /* 秒中断 */{rtc_get_time(); /* 更新时间 */__HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_SEC); /* 清除秒中断 *///printf("sec:%d\r\n", calendar.sec); /* 打印秒钟 */}/* 顺带处理闹钟标志 */if (__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_ALRAF) != RESET) /* 闹钟标志 */{__HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_ALRAF); /* 清除闹钟标志 */printf("Alarm Time:%d-%d-%d %d:%d:%d\n", calendar.year, calendar.month, calendar.date, calendar.hour, calendar.min, calendar.sec);}__HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_OW); /* 清除溢出中断标志 */while (!__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_RTOFF)); /* 等待RTC寄存器操作完成, 即等待RTOFF == 1 */
}/*** @brief 判断年份是否是闰年* @note 月份天数表:* 月份 1 2 3 4 5 6 7 8 9 10 11 12* 闰年 31 29 31 30 31 30 31 31 30 31 30 31* 非闰年 31 28 31 30 31 30 31 31 30 31 30 31* @param year : 年份* @retval 0, 非闰年; 1, 是闰年;*/
static uint8_t rtc_is_leap_year(uint16_t year)
{/* 闰年规则: 四年闰百年不闰,四百年又闰 */if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)){return 1;}else{return 0;}
}/*** @brief 设置时间, 包括年月日时分秒* @note 以1970年1月1日为基准, 往后累加时间* 合法年份范围为: 1970 ~ 2105年HAL默认为年份起点为2000年* @param syear : 年份* @param smon : 月份* @param sday : 日期* @param hour : 小时* @param min : 分钟* @param sec : 秒钟* @retval 0, 成功; 1, 失败;*/
uint8_t rtc_set_time(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{uint32_t seccount = 0;seccount = rtc_date2sec(syear, smon, sday, hour, min, sec); /* 将年月日时分秒转换成总秒钟数 */__HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟 */__HAL_RCC_BKP_CLK_ENABLE(); /* 使能备份域时钟 */HAL_PWR_EnableBkUpAccess(); /* 取消备份域写保护 *//* 上面三步是必须的! */RTC->CRL |= 1 << 4; /* 允许配置 */RTC->CNTL = seccount & 0xffff;RTC->CNTH = seccount >> 16;RTC->CRL &= ~(1 << 4); /* 配置更新 */while (!__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_RTOFF)); /* 等待RTC寄存器操作完成, 即等待RTOFF == 1 */return 0;
}/*** @brief 设置闹钟, 具体到年月日时分秒* @note 以1970年1月1日为基准, 往后累加时间* 合法年份范围为: 1970 ~ 2105年* @param syear : 年份* @param smon : 月份* @param sday : 日期* @param hour : 小时* @param min : 分钟* @param sec : 秒钟* @retval 0, 成功; 1, 失败;*/
uint8_t rtc_set_alarm(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{uint32_t seccount = 0;seccount = rtc_date2sec(syear, smon, sday, hour, min, sec); /* 将年月日时分秒转换成总秒钟数 */__HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟 */__HAL_RCC_BKP_CLK_ENABLE(); /* 使能备份域时钟 */HAL_PWR_EnableBkUpAccess(); /* 取消备份域写保护 *//* 上面三步是必须的! */RTC->CRL |= 1 << 4; /* 允许配置 */RTC->ALRL = seccount & 0xffff;RTC->ALRH = seccount >> 16;RTC->CRL &= ~(1 << 4); /* 配置更新 */while (!__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_RTOFF)); /* 等待RTC寄存器操作完成, 即等待RTOFF == 1 */return 0;
}/*** @brief 得到当前的时间* @note 该函数不直接返回时间, 时间数据保存在calendar结构体里面* @param 无* @retval 无*/
void rtc_get_time(void)
{static uint16_t daycnt = 0;uint32_t seccount = 0;uint32_t temp = 0;uint16_t temp1 = 0;const uint8_t month_table[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; /* 平年的月份日期表 */seccount = RTC->CNTH; /* 得到计数器中的值(秒钟数) */seccount <<= 16;seccount += RTC->CNTL;temp = seccount / 86400; /* 得到天数(秒钟数对应的) */if (daycnt != temp) /* 超过一天了 */{daycnt = temp;temp1 = 1970; /* 从1970年开始 */while (temp >= 365){if (rtc_is_leap_year(temp1)) /* 是闰年 */{if (temp >= 366){temp -= 366; /* 闰年的秒钟数 */}else{break;}}else{temp -= 365; /* 平年 */}temp1++;}calendar.year = temp1; /* 得到年份 */temp1 = 0;while (temp >= 28) /* 超过了一个月 */{if (rtc_is_leap_year(calendar.year) && temp1 == 1) /* 当年是不是闰年/2月份 */{if (temp >= 29){temp -= 29; /* 闰年的秒钟数 */}else{break;}}else{if (temp >= month_table[temp1]){temp -= month_table[temp1]; /* 平年 */}else{break;}}temp1++;}calendar.month = temp1 + 1; /* 得到月份 */calendar.date = temp + 1; /* 得到日期 */}temp = seccount % 86400; /* 得到秒钟数 */calendar.hour = temp / 3600; /* 小时 */calendar.min = (temp % 3600) / 60; /* 分钟 */calendar.sec = (temp % 3600) % 60; /* 秒钟 */calendar.week = rtc_get_week(calendar.year, calendar.month, calendar.date); /* 获取星期 */
}/*** @brief 将年月日时分秒转换成秒钟数* @note 输入公历日期得到星期(起始时间为: 公元0年3月1日开始, 输入往后的任何日期, 都可以获取正确的星期)* 使用 基姆拉尔森计算公式 计算, 原理说明见此贴:* https://www.cnblogs.com/fengbohello/p/3264300.html* @param syear : 年份* @param smon : 月份* @param sday : 日期* @retval 0, 星期天; 1 ~ 6: 星期一 ~ 星期六*/
uint8_t rtc_get_week(uint16_t year, uint8_t month, uint8_t day)
{uint8_t week = 0;if (month < 3){month += 12;--year;}week = (day + 1 + 2 * month + 3 * (month + 1) / 5 + year + (year >> 2) - year / 100 + year / 400) % 7;return week;
}/*** @brief 将年月日时分秒转换成秒钟数* @note 以1970年1月1日为基准, 1970年1月1日, 0时0分0秒, 表示第0秒钟* 最大表示到2105年, 因为uint32_t最大表示136年的秒钟数(不包括闰年)!* 本代码参考只linux mktime函数, 原理说明见此贴:* http://www.openedv.com/thread-63389-1-1.html* @param syear : 年份* @param smon : 月份* @param sday : 日期* @param hour : 小时* @param min : 分钟* @param sec : 秒钟* @retval 转换后的秒钟数*/
static long rtc_date2sec(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{uint32_t Y, M, D, X, T;signed char monx = smon; /* 将月份转换成带符号的值, 方便后面运算 */if (0 >= (monx -= 2)) /* 1..12 -> 11,12,1..10 */{monx += 12; /* Puts Feb last since it has leap day */syear -= 1;}Y = (syear - 1) * 365 + syear / 4 - syear / 100 + syear / 400; /* 公元元年1到现在的闰年数 */M = 367 * monx / 12 - 30 + 59;D = sday - 1;X = Y + M + D - 719162; /* 减去公元元年到1970年的天数 */T = ((X * 24 + hour) * 60 + min) * 60 + sec; /* 总秒钟数 */return T;
}
四、设计总结
图 8 最终打板图
本次智能门锁设计的难度不高,基本上就是对于各种模块的学习了解之后,拼凑而成的一个作品。在这个过程中,模块化编程是很重要的编程思想,作为小白,还是应该多注意学习一下这种编程方法。原理图以及PCB在这里就不放上来了,有需要可以关注咨询,不免费。建议小白可以跟我一样自己学着画一下,不难,基本都是模块。
另外,在RC522的MF1卡权限管理主要是依靠对卡内扇区的读写,如果不希望对卡内扇区内容进行修改,可以直接创造数组对卡号进行存储,采用链表的方式去存储,这里不做赘述。
该系统也有一些值得改进的方面,一是,本设计主要思想是一直循环查询是否有按键按下,会造成资源的浪费。首先,可通过设计4*4键盘矩阵且一端接地,并使用中断对按键进行检测,就不会对资源造成过多的浪费。再者,也可结合RTOS对该系统进行设计,会提高内存以及资源的利用。二是,可以利用蓝牙、Wi-Fi等技术对智能门锁进行远距离通信。
五、验证视频
智能门锁系统功能验证视频
赶在自己论文查重完马上就发出来了。如果对你有帮助的话,欢迎点赞评论关注,有需要PCB、原理图的可以联系咨询,有什么问题可以在下面评论讨论。祝大家生活愉快,谢谢各位~