1. 项目简介

信息时代的校园, 离不开信息化的管理, 数字化"校园一卡通"建设是校园信息化建设的重要组成部分, 是为信息化校园提供信息采集的基础工程也是获取学校信息化服务的主要方式之一。

校园一卡通将只能 IC 卡的强大功能与计算机网络的数字化理念融入校园, 将学校各个系统连为一体, 动态掌握每一持卡人情况, 极大提高学校的管理水平和服务质量。

本文介绍通过STM32 微控制器+RFID RC522设计的一个校园一卡通消费充值机的项目,可以模拟实现充值、消费、修改密码、挂失、登录、查询.......等操作。

硬件介绍:

MCU:STM32F103ZE6

刷卡模块: RFID-RC522

LCD屏: 正点原子的3.5寸LCD屏+触摸屏

完整项目下载地址:

视频演示地址:

运行效果:

image-20211221153812817

image-20211221153844993

image-20211221154004356

2. 项目实现

2.1 RFID-RC522模块

RFID-RC522模块直接淘宝购买的现成模块,模块实物图如下:

RC522是NXP公司设计的13.56MHz非接触式读写卡芯片,可以读写IC卡,具备低电压、低成本、体积小的特点,本身支持SPI接口通信,任何单片机都与通信,SPI时序模拟也非常简单。

现在地铁卡、校园卡、公交卡都是属于M1(S50)卡,M1卡内部有16个扇区,每个扇区分为4个块,每个块的容量是16个字节,每个扇区里的最后一个块是存放密码,每次对块里的数据读写都需要验证IC卡的密码,只有具备写权限才可以对块进行读写,密码验证通过之后可以直接利用修改密码、读写扇区等等,读取卡号是不需要验证密码的。

关于IC卡的详细介绍请看这里:

本项目里STM32与RCC522通信使用的SPI是模拟时序,可以很方便的移植到其他的单片机。

SPI模拟时序代如下:

/*
函数功能:移植接口--SPI时序读写一个字节
函数参数:data:要写入的数据
返 回 值:读到的数据
*/
u8 RC522_SPI_ReadWriteOneByte(u8 tx_data)
{			  	 
  u8 rx_data=0;				 
  u8 i;
  for(i=0;i<8;i++)
	{
		RC522_SCLK=0;  
		if(tx_data&0x80){RC522_OUTPUT=1;}
		else {RC522_OUTPUT=0;}
		tx_data<<=1;	
		RC522_SCLK=1;
		rx_data<<=1;
		if(RC522_INPUT)rx_data|=0x01;
	}
	return rx_data; 
}


/*
函数功能:初始化RC522的IO口	 
*/
void RC522_IO_Init(void)
{
	RCC->APB2ENR |= 0x01 << 0;
	AFIO->MAPR |= 0x01 << 26;

	RCC->APB2ENR |= 0x01 << 2;     //PA时钟使能 
	
//#define RC522_CS	PAout(10)
//#define RC522_SCLK	PAout(13)
//#define RC522_OUTPUT	PAout(14)
//#define RC522_INPUT	PAin(15)
//#define RC522_RST	PAout(0)

	GPIOA->CRL &= 0xFFFFFFF0;
	GPIOA->CRL |= 0x00000003;

	GPIOA->CRH &= 0x000FF0FF;
	GPIOA->CRH |= 0x43330300;

	RC522_CS = 1;
	RC522_SCLK = 1;
}

下面列出RC522模块的设计代码:

分别封装了寻卡、防冲撞,选卡,写数据、读数据,验证密码等函数接口。

/*
功    能: 寻卡
参数说明: req_code[IN]:寻卡方式
                0x52   = 寻感应区内所有符合14443A标准的卡
                0x26   = 寻未进入休眠状态的卡
          			pTagType[OUT]:卡片类型代码
                0x4400 = Mifare_UltraLight
                0x0400 = Mifare_One(S50)
                0x0200 = Mifare_One(S70)
                0x0800 = Mifare_Pro(X)
                0x4403 = Mifare_DESFire
返 回 值: 成功返回MI_OK
*/
char RC522_PcdRequest(u8 req_code,u8 *pTagType)
{
	char status;  
	u8 unLen;
	u8 ucComMF522Buf[MAXRLEN];  	   // MAXRLEN  18

	RC522_ClearBitMask(Status2Reg,0x08);	//清RC522寄存器位,/接收数据命令
	RC522_WriteRawRC(BitFramingReg,0x07); //写RC522寄存器
	RC522_SetBitMask(TxControlReg,0x03);  //置RC522寄存器位
 
	ucComMF522Buf[0]=req_code; 	    //寻卡方式
	
	status=RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,1,ucComMF522Buf,&unLen); //通过RC522和ISO14443卡通讯
	
	if((status==MI_OK)&&(unLen==0x10)){    
		*pTagType=ucComMF522Buf[0];
		*(pTagType+1)=ucComMF522Buf[1];
	}
	else{
		status = MI_ERR;
	}  
	return status;
}


/*
功    能: 防冲撞
参数说明: pSnr[OUT]:卡片序列号,4字节
返    回: 成功返回MI_OK
*/
char RC522_PcdAnticoll(u8 *pSnr)
{
	char status;
	u8 i,snr_check=0;
	u8 unLen;
	u8 ucComMF522Buf[MAXRLEN]; 

	RC522_ClearBitMask(Status2Reg,0x08);	//清RC522寄存器位 
	RC522_WriteRawRC(BitFramingReg,0x00);	//写
	RC522_ClearBitMask(CollReg,0x80);	//清

	ucComMF522Buf[0]=PICC_ANTICOLL1;	//PICC_ANTICOLL1 = 0x93
	ucComMF522Buf[1]=0x20;

	//0x0c,通过RC522和ISO14443卡通讯
	//PCD_TRANSCEIVE =发送并接收数据
	//2:写入卡里的数据字节长度
	//ucComMF522Buf:存放数据的地址
	//unLen:从卡里读出的数据长度
	status=RC522_PcdComMF522(PCD_TRANSCEIVE, ucComMF522Buf, 2, ucComMF522Buf, &unLen);

	if(status==MI_OK){
		for(i=0;i<4;i++){   
			*(pSnr+i)=ucComMF522Buf[i];  //把读到的卡号赋值给pSnr
			snr_check^=ucComMF522Buf[i];
		}
		if(snr_check!=ucComMF522Buf[i]){
			status = MI_ERR;
		}
	}   
	RC522_SetBitMask(CollReg,0x80);
	return status;
}


/*
功    能:选定卡片
参数说明:pSnr[IN]:卡片序列号,4字节
返    回:成功返回MI_OK
*/
char RC522_PcdSelect(u8 *pSnr)
{
	char status;
	u8 i;
	u8 unLen;
	u8 ucComMF522Buf[MAXRLEN]; 

	ucComMF522Buf[0]=PICC_ANTICOLL1;
	ucComMF522Buf[1]=0x70;
	ucComMF522Buf[6]=0;

	for(i=0;i<4;i++){
		ucComMF522Buf[i+2]=*(pSnr+i);
		ucComMF522Buf[6]^=*(pSnr+i);
	}
		
	RC522_CalulateCRC(ucComMF522Buf,7,&ucComMF522Buf[7]); //用MF522计算CRC16函数,校验数据
	RC522_ClearBitMask(Status2Reg,0x08);	                //清RC522寄存器位
	status=RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,9,ucComMF522Buf,&unLen);
	if((status==MI_OK)&&(unLen==0x18))
		status=MI_OK;
	else
		status=MI_ERR;
	
	return status;
}


/*
功    能:验证卡片密码
参数说明:auth_mode[IN]: 密码验证模式
                 0x60 = 验证A密钥
                 0x61 = 验证B密钥 
          addr[IN]:块地址
          pKey[IN]:扇区密码
          pSnr[IN]:卡片序列号,4字节
返    回:成功返回MI_OK
*/               
char RC522_PcdAuthState(u8 auth_mode,u8 addr,u8 *pKey,u8 *pSnr)
{
	char status;
	u8 unLen;
	u8 ucComMF522Buf[MAXRLEN];  //MAXRLEN  18(数组的大小)
	  
	  //验证模式+块地址+扇区密码+卡序列号   
	ucComMF522Buf[0]=auth_mode;		
	ucComMF522Buf[1]=addr;				
	memcpy(&ucComMF522Buf[2],pKey,6); //拷贝,复制
	memcpy(&ucComMF522Buf[8],pSnr,4); 
	 
	status=RC522_PcdComMF522(PCD_AUTHENT,ucComMF522Buf,12,ucComMF522Buf,&unLen);
	if((status!= MI_OK)||(!(RC522_ReadRawRC(Status2Reg)&0x08)))
		status = MI_ERR;
	return status;
}


/*
功    能:读取M1卡一块数据
参数说明: 
	  addr:块地址
          p   :读出的块数据,16字节
返    回:成功返回MI_OK
*/ 
char RC522_PcdRead(u8 addr,u8 *p)
{
	char status;
	u8 unLen;
	u8 i,ucComMF522Buf[MAXRLEN]; //18

	ucComMF522Buf[0]=PICC_READ;
	ucComMF522Buf[1]=addr;
	RC522_CalulateCRC(ucComMF522Buf,2,&ucComMF522Buf[2]);
	status=RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,4,ucComMF522Buf,&unLen);//通过RC522和ISO14443卡通讯
	if((status==MI_OK&&(unLen==0x90))){
		for(i=0;i<16;i++){
			*(p +i)=ucComMF522Buf[i];
		}
	}
	else{   
		status=MI_ERR;
	}
	return status;
}


/*
功    能:写数据到M1卡指定块
参数说明:addr:块地址
          p   :向块写入的数据,16字节
返    回:成功返回MI_OK
*/                  
char RC522_PcdWrite(u8 addr,u8 *p)
{
	char status;
	u8 unLen;
	u8 i,ucComMF522Buf[MAXRLEN]; 

	ucComMF522Buf[0]=PICC_WRITE;// 0xA0 //写块
	ucComMF522Buf[1]=addr;      //块地址
	RC522_CalulateCRC(ucComMF522Buf,2,&ucComMF522Buf[2]);

	status=RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,4,ucComMF522Buf,&unLen);

	if((status!= MI_OK)||(unLen != 4)||((ucComMF522Buf[0]&0x0F)!=0x0A)){
		status = MI_ERR;
	}

	if(status==MI_OK){
		for(i=0;i<16;i++){//向FIFO写16Byte数据
			ucComMF522Buf[i]=*(p +i);   
		}
		RC522_CalulateCRC(ucComMF522Buf,16,&ucComMF522Buf[16]);
		status = RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,18,ucComMF522Buf,&unLen);
		if((status != MI_OK)||(unLen != 4)||((ucComMF522Buf[0]&0x0F)!=0x0A)){   
			status = MI_ERR;   
		}
	}
	return status;
}

2.2 LCD屏

LCD使用的是正点原子3.5寸屏,驱动芯片是NT35310,支持8080时序,本身STM32大容量芯片具备FSMC接口的,可以直接使用FSMC接口操作LCD屏完成操作,这里考虑到程序的移植性,因为小容量,中容量的比如STM32F103C8T6就没有FSMC接口,为了方便程序可以移植到这些开发板正常运行,当前项目采用的是模拟8080时序方式,直接使用GPIO口模拟时序操作LCD屏;虽然刷屏效率比FSMC慢不少,但是本项目的界面也不需要很高的刷新率,没有图频繁的切换效果,所以整体效果还是不错的。

模拟时序代码如下: 如果要移植到其他单片机上,只需要修改GPIO口即可。

void lcd_write_cmd(u8 reg)
{
	LCD_CS = 0;	//拉低片选脚,选中 LCD
	LCD_RS = 0;	//拉低数据/命令控制线,选择要操作命令

	LCD_RD = 1;	//禁止读
	LCD_WR = 0;	//拉低 WR,准备写操作
	
	//数据总线输出命令, 把要发送的命令放到数据总线上
	GPIOB->ODR = (u16)reg;
	
	LCD_WR = 1;	//拉高 WR 写使能
	LCD_CS = 1;	//拉高片选,结束操作
}

void lcd_write_data(u16 data)
{
	LCD_CS = 0;	//拉低片选脚,选中 LCD
	LCD_RS = 1;	//拉高数据/命令控制线,选择要操作数据

	LCD_RD = 1;	//禁止读
	LCD_WR = 0;	//拉低 WR,准备写操作
	
	//数据总线输出数据, 把要发送的数据放到数据总线上
	GPIOB->ODR = data;
	
	LCD_WR = 1;	//拉高 WR 写使能
	LCD_CS = 1;	//拉高片选,结束操作
}

void lcd_set_cursor(u16 x, u16 y)
{
	lcd_write_cmd(SET_X_ADDR);
	lcd_write_data(x>>8);
	lcd_write_data(x&0xff);

	lcd_write_cmd(SET_Y_ADDR);
	lcd_write_data(y>>8);
	lcd_write_data(y&0xff);
}

void lcd_write_reg(u16 cmd, u16 parameter)
{
	lcd_write_cmd(cmd);
	lcd_write_data(parameter);
}

void lcd_draw_dot(u16 x, u16 y, u16 color)
{
	lcd_set_cursor(x, y);
	lcd_write_cmd(WRITE_MEMORY_START);
	lcd_write_data(color);	// [15:0] --> [R4-R0:G5-G0:B4-B0]
}

void lcd_show_screen(const u8 * image, u32 size, u16 x, u16 y)
{
	u32 i = 0;

	lcd_set_cursor(x, y);	//设置光标位置
	lcd_write_cmd(WRITE_MEMORY_START);	 //开始写入GRAM

	while( i < size ){
		lcd_write_data( *image<<8 | *(image+1) ); 
		image += 2;
		++i;
	}
}

//画矩形	  
//(x1,y1),(x2,y2):矩形的对角坐标
void lcd_draw_rectblock(u16 y1, u16 y2, u16 color)
{
	u16 i;
	for( ; y1<=y2; ++y1){
		lcd_set_cursor(0,y1);		  //设置光标位置
		lcd_write_cmd(WRITE_MEMORY_START);		  //开始写入GRAM
		for(i=0; i<320; ++i){
			lcd_write_data( color );    //写数据
		}
	}
}

void lcd_show_image(const u8 * image, u16 width, u16 high, u16 x, u16 y)
{	
	u32 i,j;

	for(i=0; i<high; ++i){
		lcd_set_cursor(x,y);		  //设置光标位置
		lcd_write_cmd(WRITE_MEMORY_START);		  //开始写入GRAM
		for(j=0; j<width; ++j){
			lcd_write_data( *image<<8 | *(image+1) );    //写数据
			image += 2;
		}
		++y;
	}
}

void lcd_show_char(u8 * data, u16 x, u16 y, u8 width, u8 high,u16 c,u16 b)
{
	u32 i,j;
	const u16 addr = y; //保留纵坐标的位置
	u8 font;
	for(i=0; i<width*high/8; ++i){
		font = data[i]; //取出一个点阵码
		for(j=0; j<8; ++j){
			if(font&0x80){  //需要画字体颜色
				lcd_draw_dot(x, y++, c);
			}
			else{   //需要画背景色
				lcd_draw_dot(x, y++, b);
			}
			font <<= 1; //依次向左边移动一位
		}
		if(y-addr == high) //表示需要换行
		{
				++x;  
				y = addr; //纵坐标归位
		}
	}
}

void lcd_show_font(const u8 * font, u16 x, u16 y, u16 width, u16 high, u16 f_color, u16 b_color)
{
	const u16 addr = x;
	u16 i, j;
	u8 tmp;

	for(i=0; i<width*high/8; ++i){
		tmp = font[i];
		for(j=0; j<8; ++j){
			if(tmp&0x80)
				lcd_draw_dot(x++, y, f_color);
			else
				lcd_draw_dot(x++, y, b_color);
			tmp <<= 1;
		}
		if(x-addr == width){
			++y;
			x = addr;
		}
	}
}

void lcd_draw_line(u16 x1, u16 y1, u16 x2, u16 y2)
{
	u16 t; 
	int xerr=0,yerr=0,delta_x,delta_y,distance; 
	int incx,incy,uRow,uCol; 
	delta_x=x2-x1; //计算坐标增量 
	delta_y=y2-y1; 
	uRow=x1; 
	uCol=y1; 
	if(delta_x>0)incx=1; //设置单步方向 
	else if(delta_x==0)incx=0;//垂直线 
	else {incx=-1;delta_x=-delta_x;} 
	if(delta_y>0)incy=1; 
	else if(delta_y==0)incy=0;//水平线 
	else{incy=-1;delta_y=-delta_y;} 
	if( delta_x>delta_y)distance=delta_x; //选取基本增量坐标轴 
	else distance=delta_y; 
	for(t=0;t<=distance+1;t++ )//画线输出 
	{  
		lcd_draw_dot(uRow,uCol, WHITE);//画点 
		xerr+=delta_x ; 
		yerr+=delta_y ; 
		if(xerr>distance) 
		{ 
			xerr-=distance; 
			uRow+=incx; 
		} 
		if(yerr>distance) 
		{ 
			yerr-=distance; 
			uCol+=incy; 
		} 
	} 
}

void lcd_draw_circle(u16 x,u16 y,u8 r, u16 color)
{
	int a, b, di;

	a = 0;
	b = r;	  
	di = 3 - (r << 1);	//判断下个点位置的标志

	while(a<=b){
		lcd_draw_dot(x+a,y-b, color);	//5
 		lcd_draw_dot(x+b,y-a, color);	//0           
		lcd_draw_dot(x+b,y+a, color);	//4               
		lcd_draw_dot(x+a,y+b, color);	//6 
		lcd_draw_dot(x-a,y+b, color);	//1       
 		lcd_draw_dot(x-b,y+a, color);             
		lcd_draw_dot(x-a,y-b, color);	//2             
  		lcd_draw_dot(x-b,y-a, color);	//7     	         
		++a;
		//使用Bresenham算法画圆     
		if(di < 0)
			di += 4*a + 6;	  
		else{
			di+=10+4*(a-b);   
			--b;
		}						    
	}
}

void lcd_clear(u16 color)
{
	u32 index;      
	u32 point;
	point = 480*320; 		//得到总点数
	lcd_set_cursor(0x00,0x00);	//设置光标位置 
	lcd_write_cmd(WRITE_MEMORY_START);  	//开始写入GRAM	  	  
	for(index=0; index<point; index++)
		lcd_write_data(color);	
}

3.3 触摸屏

触摸屏是LCD屏本身自带的,触摸芯片是XPT2046,是一个12位的ADC芯片,通信协议是SPI时序。

项目里采用模拟SPI时序进行与触摸屏芯片通信,因为XPT2046本身是ADC芯片,所以在屏幕上按下后读取出来的数据是模拟数据值---物理坐标值,我们还需要将它转为屏幕坐标与LCD屏的像素点对应起来,这样使用起来才比较方便。

XPT2046核心代码如下:

#include "stm32f10x.h"
#include "xpt2046.h"
#include "delay.h"

/*
#define T_SCK	PAout(12)
#define T_MI	PAin(6)
#define T_MO	PAout(11)
#define T_PEN	PAin(7)
#define T_CS	PAout(8)
*/
void xpt2046_init(void)
{
	RCC->APB2ENR |= 0x01 << 2;	// ENABLE port a clock

	GPIOA->CRL &= 0x00FFFFFF;	// 浮空输入
	GPIOA->CRL |= 0x44000000;	// 推挽输出

	GPIOA->CRH &= 0xFFF00FF0;
	GPIOA->CRH |= 0x00033003;

	T_CS = 1;
	T_SCK = 0;
}

/*
cmd format 1: 10010000	0x90	Y-POSITION Measure
cmd format 2: 11010000	0xd0	X-POSITION Measure
*/
u16 xpt2046_read(u8 cmd)
{
	T_SCK = 0;
	T_MO = 0;
	T_CS = 0;
	
	for(u8 i=0; i<8; ++i){
		T_SCK = 0;
		if( cmd & 0x80 )
			T_MO = 1;
		else
			T_MO = 0;
		cmd <<= 1;
		T_SCK = 1;
	}
	// 15时钟周期转换
	T_SCK = 0;
	T_MO = 0;
	T_SCK = 1;
	u16 data = 0;
	for(u8 i=0; i<12; ++i){
		T_SCK = 0;
		data <<= 1;
		T_SCK = 1;
		if( T_MI )
			data |= 0x01;
	}
	T_CS = 1;

	return (data);
}

u8 xpt2046_position(TOUCH * xpt2046_pos)
{
	if( !T_PEN ){
		u8 i, j;
		u16 tmp;
		u16 x[16], y[16];
		
		for(i=0; i<16; ++i){
			x[i] = xpt2046_read(XPOS);
			y[i] = xpt2046_read(YPOS);
		}
		for(i=0; i<16; ++i){
			for(j=0; j<16-i; ++j){
				if(x[j]>x[j+1]){
					tmp = x[j];
					x[j] = x[j+1];
					x[j+1] = tmp;
				}
				if(y[j]>y[j+1]){
					tmp = y[j];
					y[j] = y[j+1];
					y[j+1] = tmp;
				}
			}
		}

		u32 sum_x, sum_y;

		sum_x = sum_y =0;
		for(i=3; i<13; i++){
			sum_x += x[i];
			sum_y += y[i];
		}
		xpt2046_pos->x = sum_x / 10;
		xpt2046_pos->y = sum_y / 10;

		return 0;
	}
	else
		return 1;
}

u8 touch_position(TOUCH * touch_pos)
{
	TOUCH xpt2046_pos;

	if( !xpt2046_position(&xpt2046_pos) ){
		touch_pos->x = 320 - (xpt2046_pos.x - 300) / 11.25;
		touch_pos->y = 480 - (xpt2046_pos.y - 200) / 7.7;
		return 0;
	}
	else{
		touch_pos->x = 0xffff;
		touch_pos->y = 0xffff;
		return 1;
	}
}

3.4 工程项目效果