1.项目涉及的知识点
1.SPI通信在RC522上的刷卡应用 |
2.操作STM32内部FLASH |
3.IIC在OLED上显示数据的应用 |
4.串口编程及其应用 |
2.STM32与各个模块的管脚接线
STM32管脚 | 模块管脚 |
3V3或者5V | 串口3V3或者5V |
PA9 | 串口1的RXD |
PA10 | 串口1的TXD |
GND | 串口GND |
3V3或者5V | 0.96寸OLED屏的3V3或者5V |
PB6 | 0.96寸OLED屏的SDA |
PB7 | 0.96寸OLED屏的SCL |
GND | 0.96寸OLED屏的GND |
3V3 | RC522的3V3 |
PA1 | RC522的RST |
GND | RC522的GND |
PA0 | RC522的MISO |
PC15 | RC522的MOSI |
PC14 | RC522的SCK |
PC13 | RC522的SDA |
3.SPI通信协议介绍
3.1 SPI通信协议
SPI (Serial Peripheral interface,就是串行外围设备接口。是摩托罗拉公司创造的,SPI通信只需要四根线就可以实现全双工通信,就是在同一时间内既可以发数据也可以收数据,并且SPI的传输速率很高,可以达到几十兆Hz,不像IIC,最大也才能到3.4MHz(常用的一般就是100KHz或者400KHz)。
NSS(CS) | 片选信号线 |
SCK | 时钟信号线 |
MOSI | 主设备输出从设备输入线 |
MISO | 主设备输入从设备输出线 |
就这四根线,在大部分嵌入式用的通信总线中,只有SPI是四根线,其他都是两根线(有异议可以在评论区一起讨论)。
SPI通信分硬件SPI和软件模拟SPI,跟IIC一样,都是有两种方式,本次设计用的0.96寸OLED就是用的IIC通信,OLED显示数据代码用的也是那一套,关于IIC的通信协议可以看我之前写的一篇博客,链接:
https://blog.csdn.net/weixin_41011452/article/details/140220717
其实SPI你只需要搞清楚四根线在通信的时候怎么用就行了,我一根一根来说。
首先是NSS(CS)片选信号线,片选信号线主要是为了识别从设备用的,比如你主设备是STM32单片机,你的从设备有两个,一个RC522刷卡模块,一个W25Q128闪存模块。那么你CS就是识别这俩从设备用的,想给哪个从设备传输数据主机就会把哪个从设备的CS引脚拉低,传输完再拉高。
SCK时钟线是为了保持主从设备在传输数据的一致性,时钟信号是由主设备发起的,从设备保持一致的时钟信号。
MOSI信号线是主设备输出数据,同时从设备输入数据。
MISO信号线是从设备输出数据,同时主设备输入数据。
本设计采用的是软件模拟SPI,好处就是写代码的时候不需要复杂的配置,自己写个收发函数就行了,不需要调用STM32的库函数,并且随便选四个GPIO引脚就行,方便移植,但是传输速率受到GPIO引脚的限制,没有硬件SPI的传输速率高;硬件SPI就是你要用STM32具有SPI引脚的GPIO,像STM32C8T6具有SPI1和SPI2。
SPI1CS | PA4 |
SPI1SCK | PA5 |
SPI1MOSI | PA6 |
SPI1MISO | PA7 |
SPI2CS | PB12 |
SPI2SCK | PB13 |
SPI2MOSI | PB14 |
SPI2MISO | PB15 |
这个SPI1和SPI2指的是SPI的工作模式,SPI一共有四种工作模式,在初始化的时候要配置好,这个要看从设备支持哪种模式,主设备(STM32)的SPI初始化代码里就必须要配置成哪种模式,不然是不能通信的,四种通信模式如下表:
SPI0 | (CPOL=0, CPHA=0) | 时钟信号在空闲时为低电平,数据在第一个跳变沿(下降沿)被采样。 |
SPI1 | (CPOL=0, CPHA=1) | 时钟信号在空闲时为低电平,但数据在第二个跳变沿(上升沿)被采样。 |
SPI2 | (CPOL=1, CPHA=0) | 时钟信号在空闲时为高电平,数据在第一个跳变沿(下降沿)被采样。 |
SPI3 | (CPOL=1, CPHA=1) | 时钟信号在空闲时为高电平,数据在第二个跳变沿(上升沿)被采样。 |
至于啥叫跳变沿,就是数据从高电平变成低电平或者数据从低电平变成高电平,只要发生电平变化就是跳变了。从高电平变成低电平称为下降沿,从低电平变成高电平称为上升沿。
我们的是软件模拟SPI,用的是SPI0模式,也就是在时钟(SCK)在发送数据时初始设置为低电平 ,然后在设置数据线(MOSI)后,将时钟(SCK)拉高,并在高电平期间接收数据。传输完毕数据将时钟拉低 ,准备下一位的数据传输。
4 STM32内部FLASH
因为本设计需要把RC522的刷卡信息存储,所以用到了STM32的内部FLASH,这个内部FLASH先把这个讲解一下。FLASH说白了就类似于你电脑的硬盘一样,就算把电源关了里面的数据也不会丢失,只不过内存非常小,STM32C8T6的FLASH内存是64KB,主要存储的是主程序、初始化的一些数据还有就是如果你做OTA功能的话,是通过操作FLASH进行的。至于OTA是啥,这个你们自己百度吧,一般消费电子产品都需要这个功能,联网就能把程序升级了。
4.1 FLASH代码分析
先看看STM32官方手册的截图。
FLASH闪存主要分三部分:主存储块、信息块和闪存存储器寄存器。我们存储数据就是在主存储块进行的,可以看到主存储卡的起始地址就是0x0800 0000,这也是为什么STM32的启动文件为啥要把启动程序的地址放在0x0800 0000这个位置上的原因,下图就是从Keil MDK的设置Debug界面截图的。
从上图中可以看到其实地址就是0x0800 0000,大小是0x00020000,把这个0x00020000换算成十进制数就是131072byte,那么你用这个数字去除以1024(因为1KB=1024byte字节),就会得到128这个数字,也就是这个大小是128KB。
接着看STM32官方手册的那个截图,可以看到主存储块把128KB分成了128页,那么1页的大小就是1KB,也就是1024个字节,所以我们操作一次是以页为单位的。FLASH操作其实就是两个流程,一个是写入数据,一个是擦除数据。
好了,接着贴代码,这个代码也是我移植的,做嵌入式很少有从零开始自己写代码的,有现成的干嘛不用,前提是你移植后要会根据你的电路板去修改,读懂并修改,除非你有能力自己写个很牛的架构,我目前是做不到,多向大佬们学习吧。
写入数据步骤如下:
1.先解锁
2.计算剩余空间大小
3.检查完毕后,写入之前要先读出来擦除的内容
4.擦除要写入的地址里面的内容
5.写完上锁。避免被误操作
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
u32 secpos; //扇区地址
u16 secoff; //扇区内偏移地址(16位字计算)
u16 secremain; //扇区内剩余地址(16位字计算)
u16 i;
u32 offaddr; //去掉0X08000000后的地址
//1.检查是否上锁
if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址
FLASH_Unlock(); //解锁
//2.计算剩余空间大小
offaddr=WriteAddr-STM32_FLASH_BASE; //实际偏移地址.
secpos=offaddr/STM_SECTOR_SIZE; //扇区地址 0~127 for STM32F103RBT6
secoff=(offaddr%STM_SECTOR_SIZE)/2; //在扇区内的偏移(2个字节为基本单位.)
secremain=STM_SECTOR_SIZE/2-secoff; //扇区剩余空间大小
if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围
while(1)
{
//3.检查完毕后,写入之前先读出来要擦除的内容
STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
for(i=0;i<secremain;i++)//校验数据
{
if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除
}
//4.擦除写入的地址内容
if(i<secremain)//需要擦除
{
FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
for(i=0;i<secremain;i++)
{
STMFLASH_BUF[i+secoff]=pBuffer[i];
}
STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区
}
else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间.
if(NumToWrite==secremain)
break;//写入结束了
else//写入未结束
{
secpos++; //扇区地址增1
secoff=0; //偏移位置为0
pBuffer+=secremain; //指针偏移
WriteAddr+=secremain; //写地址偏移
NumToWrite-=secremain; //字节(16位)数递减
if(NumToWrite>(STM_SECTOR_SIZE/2))
secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
else secremain=NumToWrite;//下一个扇区可以写完了
}
};
//5.写完上锁,避免被误操作
FLASH_Lock();//上锁
}
看代码对应的步骤注释,第一步先判断要写入的地址是否是合理区间,也就是是否在0x0800 0000--0x8001 0000,是不是有点懵圈?这个0x8001 0000咋来的,别急,我主打的就是教零基础的人。前面说过STM32C8T6是有64KB的FLASH,那么64KB换算成byte是不是就是:64*1024=65536,再把65536转换成十六进制数就是:0x0001 0000,我们的起始地址是0x8000 0000,那它的64KB之后的地址不就是0x8001 0000了嘛。
所以先检查地址是否越界,如果没有越界,再到解锁FLASH,目的就是防止写入错误地址,把FLASH其他地址的数据给破坏了,尤其是引导程序Bootloader,如果你不小心把这个给写错了,那你这个芯片就废了,开机都开不了。首先STM32_FLASH_BASE这个呢,就是起始地址0x8000 0000,代码里宏定义了。检查要写入的地址是否小于0x8000 0000或者大于0x8001 0000。如果不是,则FLAHS解锁。
第二步就是计算剩余空间大小了,要看看这64KB的大小还有多少可以用的了,就跟停车一样,停车场都满了你非要硬挤,撞人家停车杆是要赔的。这一步是最重要的也是最难理解的,但是不要慌,我通俗易懂的讲。
计算剩余的偏移地址,比如64KB,已经用了10KB+2byte=10242byte,因为10KB=10*1024byte,10242换算成十六进制就是0x2802,那么你写数据就要从0x8000 2802这个地址开始写。因为是16位操作,一次写入2个字节,因为FLASH操作不是以8位操作的,而是以16位操作的,16位就是两个字节,也就是半字,这个在STM32的官方手册里写的,贴图如下。
所以我的偏移地址offaddr就是0x0800 2802 -0x0800 0000 = 0x0000 2802。我们前面说过把这个FLASH分成了128页,其实我们的STM32C8T6只有64页,因为一页就是1KB。页地址secpos就是0x2802/1024=10(这是对其取整),这个STM_SECTOR_SIZE=1024,这个也是宏定义的,就是1KB,所以这个偏移地址除以1KB就是10,也就是你在原来的0KB基础上加上这个10KB,也就是从10KB位置开始的,也就是从第11页开始写的(因为第一页是页0,需要写到页10,中间使用了0-9页,一共10页)。
所以secoff=(offaddr%STM_SECTOR_SIZE)/2; 这句代码就是0x0000 2802这个偏移地址对1024取模,再除以2(这个2就是因为16位操作,除以2才是16位操作,如果不除以2,就是8位操作了),所以secoff=1,因为你用0x0000 2802这个数字去除以STM_SECTOR_SIZE(1024)必然不能整除,是有余数2的,这个余数2再除以2=1才是secoff,代表的就是你要写的地址是第11页第2个16位开始(第一个16位已经被10KB_2byte占用了),也就是地址0x0800 2802这个地址。这句secremain=STM_SECTOR_SIZE/2-secoff; 所以secremain=511。
上面已经把你要写数据在第几页第多少个字节计算出来了,那么你就可以从这开始写你的数据了,因为前面的已经被其他数据用完了,不过在写之前,要先把这之后的地址数据先读一下,看看是不是有被占用的,目的就是为了安全的写入,所以
STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);这句代码就是读出来这一页的全部数据放到缓冲区里,为的就是保护前两个字节已有的数据在擦除后还能从缓冲区放回到FLASH里,secpos前面计算出来是10,secpos*STM_SECTOR_SIZE就是10*1024=10240byte,再加上STM32_FLASH_BASE(0x0800 0000),就变成了0x0800 2800,这是你要读从这个位置开始的数据,STMFLASH_BUF这是你要读的数据内容,,STM_SECTOR_SIZE/2这个是你要读的长度,因为读是要读整个页的,就算第11页的前两个字节被占用了,你也要从这个页的第0个字节开始读,必须整页整页的读,不管这页已经写了多少数据。
这段代码
for(i=0;i<secremain;i++)//校验数据
{
if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除
}
就是你要擦除你打算写入的起始地址0x0800 2802,为啥呢,因为是从secoff开始读的,也就是你之前不是用了10KB+2byte嘛,它就从第2个16位开始擦除(注意这个和读不一样,读是整页整页的读,它才不管你前两个字节是不是已经被使用了),因为FLASH擦除的特点就是擦除之后数据全都是1,没有一个0,为啥是0XFFFF呢,0XFFFF就是两个字节啊,也就是字,16位,FLASH就是半字操作的,懂了吧。如果读出来发现不是全1,那说明这里有数据,就需要擦除了,因为你要用这个地方的地址啊,必须给它擦了你才能用。什么时候跳出这个for循环呢,只要检查到有不是全1的2字节就会跳出去break,执行下面的擦除操作代码。
if(i<secremain)//需要擦除
{
FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
for(i=0;i<secremain;i++)//复制
{
STMFLASH_BUF[i+secoff]=pBuffer[i];
}
STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区
}
else STMFLASH_Write_NoCheck(Wr
擦除操作是按页操作的,所以调用了FLASH_ErasePage这个函数,擦除的范围就是从0x0800 2800开始,然后循环secremain=512次,擦除完了要把缓冲区的内容给写回去,原来有数据的前两个字节还会恢复,后面的1022个字节没有数据就全变成了1。else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);这句就是检查到如果不需要擦除的,就是直接写入剩余的页,不需要擦除操作。
if(NumToWrite==secremain)break;//写入结束了
else//写入未结束
{
secpos++; //扇区地址增1
secoff=0; //偏移位置为0
pBuffer+=secremain; //指针偏移
WriteAddr+=secremain; //写地址偏移
NumToWrite-=secremain; //字节(16位)数递减
if(NumToWrite>(STM_SECTOR_SIZE/2))
secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
else secremain=NumToWrite;//下一个扇区可以写完了
}
如果新写入的数据已经还没有使用完这页就写完了,那么就结束写入了。如果这页都结束了,都写到第1023个字节处了(因为从0字节开始写的,一页最多写到1024个字节处),那么就开始把页码增加secpos++;比如上面是第11页,那么secpos++;就是从第12页开始继续写数据,secoff=0; 就是从第12页的第0个字节开始写,同时写入数据的指针(也就是地址要继续向后偏移),后面的就不解释了,很简单。
整个FLASH操作就是这样,确实很难理解,多读两遍我上面写的,你就懂了,实在不懂可以评论区交流。
4.2 RC522存储卡片信息调用FLASH代码分析
上面说了一堆,就是为了给这个存储卡片信息用的,就像你去公司上班或者你在食堂吃饭刷饭卡,那怎么保证你用的卡和别人的卡有区别呢,就是靠内部的信息来识别的,RC522的卡片就是复旦微电子公司产的配套S50卡片,无非就是两种常见的:大白卡和异形卡。
这个卡片在刷卡后就有了内部信息,比如卡号,姓名等等,这个根据不同的公司去自己更改的,我们做实验啥也不改,就是把它的内部ID信息读取出来,一般的都是8个字节的数据,随便举例一个,比如0x23 55 98 03。RC522就是通过识别每个卡的ID区分不同的卡片的。那单片机每次识别卡片怎么能知道这个卡片就是你这个公司的员工卡片呢,那就是发给员工卡片之前把这个卡的ID信息记录在公司管理机器上,也就是我们的内存上,我们的设计就是存在STM32的内部FLASH上,前面说过STM32C8T6单片机的内部FLASH只有64KB,一张卡的ID号码数据是4byte,所以能存储64*1024/8=16384张卡的信息。
上面的代码就是RC522读取卡片的信息并且存储在STM32内部的FLASH上,id_num就是卡片有几个,最多8个。第80行代码就是把卡的数量写在0x0800 E100上。第81行就是把一张卡的8个字节的ID数据存储在0x0800 E000上,至于为啥是&id_num,(u16*)card_numberbuf,,这就是C语言数组和指针的基础知识了,那你要是看不懂就去自己补补C语言基础。因为一个卡的ID占4个字节,而FLASH操作是两个字节两个字节操作的,所以后面的2就是操作两次,2字节操作一次,不就是操作了4个字节嘛。那第一张卡的ID号存在了0x0800 E000地址处,第二张卡的信息就是在第一个卡的信息4字节后的偏移地址,就是0x0800 E004,这就是92行的代码。
5. RCC结合SPI代码
5.1 SPI初始化
这部分代码就是把SPI四根线还有RST的时钟和管脚配置一下,这个没啥好说的,注意一点就是初始化完成后要把CS片选引脚拉高,因为CS在高电平状态下是不通信的,现在是初始化配置阶段,当然不需要通信。
5.2 数据发送
SPI通信协议是边发边收的,所以是全双工通信。我们用的模式0方式,所以是在第一个边沿的高电平期间发送数据,也就是在SCK高电平期间发数据,因为我们是用的软件模拟SPI,所以需要自己写收发数据的函数,不能调用STM32库函数。下面这个函数就是SPI通用的数据发送,这个也很简单,比如你要发0x1010 0110,SPI是MSB先发的,也就是高位先发,发送数据顺序是1->0->1->0->0->1->1->0。所以91行到112行就是把这个8个位的数据按上面的顺序发出去,我只以前两个位举例说明,先把第一个数据位1准备好,此时i=0,然后第94行就变成了if(1&1<<(7-0)),那么也就是把1左移7位,就是把这个1移到最高位上。然后MOSI现在的数据位就是数据1,然后延时5us,目的就是保证数据能够顺利置1,再把SCK拉高才把数据发出去,此时才是发数据,在SCK没拉高之前那叫准备好数据,随时等待SCK拉高才发,然后第107行判断如果主机接收到了从机发送的数据是1,就把数据1发给主机并且移位,这里需要注意的是这个是从机发过来的数据,并不是主机发出去的,所以就算你第二位发的是0,这行代码就不会执行了。然后把SCK拉低,为第二数据位的传输做准备。
下面这段代码才是通过SPI结合RC522把数据发出去,在SPI发送数据期间CS片选拉低才能通信,通信完成后再拉高就行了。C522 的寄存器地址格式为0xxxxxx0,也就是最高位一般设置为0.最低位是读写位,写是0,读是1。左移一位目的是为了给最低位腾出来读写位,与上0x7E目的也是为了最高位和最低位都是0,因为这些写函数,写完了需要把CS片选拉高置于空闲状态。
5.3 数据接收
数据接收也很简单,就是操作MOSI和MISO的两个引脚,在SCK高电平期间把从设备返回来的数据接收。
同样,也是在CS片选拉低的时候收数据,要一位一位的去把数据存到SPI环形缓冲区,按整字节的去收数据。
5.4 RC522初始化
SPI初始化只是初始化了通信总线,但是RC522有自己的通信步骤,根据RC522的数据手册里的指令去按步骤初始化,主要包括调用SPI的初始化、模块复位、设置一些参数、打开天线等等。
5.5 主函数
主函数就是要把各个子函数调用,包括中断初始化,OLED显示屏初始化,串口初始化,因为OLED如果想显示汉字就需要取字模,这个比较麻烦,我就没做,只在串口里做了汉字的打印信息,通过串口1连接到电脑上位机的串口助手把卡的信息打印出来。同时主函数要调用添加刷卡信息的子函数。
6.实物效果展示
6.1 实物状态和显示屏显示
用杜邦线把OLED屏幕、RC522模块、USB转TTL模块、单片机对应的管脚接好,然后把程序烧录到STM32单片机里,然后复位一下开机,可以看到OLED屏幕上显示:ID Input 字样,就是请输入ID,也就是刷卡。
然后把卡片靠近RC522刷卡区域,此时屏幕就会显示:Inputing,意思就是正在刷卡。
刷卡3秒后,屏幕上会显示:success字样,就表示刷卡成功。
6.2 串口助手信息打印
从串口助手可以看到,上电后就会收到:等待胖虎刷卡(我养的金毛叫胖虎),然后把卡贴近RC522刷卡区域等待3秒,就会收到检测到胖虎正在刷卡,下面的:0x43 0xd4 0x61 0x11 就是这张卡的ID号码。然后就会收到新卡的序列号,核对一下没问题,录入第0张卡,这个是数组的原因,我从0开始计的,懒得改了.
7.总结
这个项目的设计资料可以扩展成门禁卡、图书馆借阅管理、停车场计费、充电站刷卡充电等等需要刷卡的。根据自己的需求移植就行。
需要资料的请加我微信!
标签:STM32,字节,FLASH,STM32C8T6,----,SPI,地址,就是 From: https://blog.csdn.net/weixin_41011452/article/details/140422263