打印

百为STM32开发板教程之九——SPI FLASH

[复制链接]
7830|20
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
xi_liang|  楼主 | 2013-7-23 21:38 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
百为STM32开发板教程之九——SPI FLASH

实验目的:把字库写到SPI FLASH中,然后在程序里利用SPI FLASH中的字库进行显示

主要内容:
一、了解SPI通信协议
二、了解STM32的SPI接口及编程
三、了解SPI FLASH的工作原理及读写方法
四、了解如何把字库写到SPI FLASH及把字库读出显示

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

一、SPI通信协议
SPI总线规定了4个逻辑信号:
(1)MOSI – SPI总线主机输出/ 从机输入(Master Output/Slave Input)
(2)MISO – SPI总线主机输入/ 从机输出(Master Input/Slave Output)
(3)SCLK – 时钟信号,由主设备产生
(4)CS – 从设备使能信号,由主设备控制(Chip select)

其中SCLK的极性和相位是可设置的:


CPOL表示时钟极性,即空闲状态时的电平,低电平(0)或高电平(1)
CPOH表示数据采样的时钟沿,第一个沿(0)或第二个沿(1)

CPOL和CPOH组成了SPI的四种工作模式:(0)  CPOL = 0 CPOH = 0  (1)  CPOL = 0 CPOH = 1  (2)  CPOL = 1, CPOH = 0  (3)  CPOL = 1, CPOH = 1


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

二、STM32的SPI接口及编程

STM32F103ZET6有3个SPI,分别为SPI1、SPI2和SPI3。
其中SP2和I2S2,SPI3和I2S3共用管脚,采用SPI功能还是I2S功能是由SPI_I2S_CFGR寄存器的第11位I2SMOD来决定的。

SPI_I2S配置寄存器(SPI_I2S_CFGR)

其中I2SMODE位为1时表示选择I2S模式,为0时表示选择SPI模式。

在我们百为STM32开发板上,是采用STM32的SPI1连接SPI FLASH的

IO对应关系为:
STM32                                                   M25P80
PB2(CS)----------------------------------  S(片选信号)
PA5(SPI1_SCK)-------------------------  C(串行时钟)
PA6(SPI1_MISO)------------------------  Q(串行数据输出)
PA7(SPI1_MOSI)------------------------  D(串行数据输入)

程序中要SPI,首先要调用下面的函数进行初始化
void SPI_FLASH_Init(void)
{
  SPI_InitTypeDef  SPI_InitStructure;
  GPIO_InitTypeDef GPIO_InitStructure;
  /* 打开SPI1和GPIO时钟 */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIO_CS, ENABLE);
  /* 配置SPI1管脚: SCK, MISO and MOSI */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
  /* 配置SPI FLASH 片选IO */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_CS;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_Init(GPIO_CS, &GPIO_InitStructure);
  /* 初始化SPI FLASH片选为高电平 */
  SPI_FLASH_CS_HIGH();
  /* SPI1参数配置 */
  SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI为全双工通信
  SPI_InitStructure.SPI_Mode = SPI_Mode_Master;  //设置为SPI主模式
  SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;  //设置为8位数据格式
  SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;  //设置为SPI工作模式4,即CPOL = 1,
  SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //CPOH = 1
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;  //设置NSS为软件管理
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;  //设置SPI波特率为fpclk/4
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;  //设置数据高位在先
  SPI_InitStructure.SPI_CRCPolynomial = 7;
  SPI_Init(SPI1, &SPI_InitStructure);
  /* 使能SPI1  */
  SPI_Cmd(SPI1, ENABLE);
}

三、了解SPI FLASH的工作原理及读写方法
1、简介
M25P80是一个8Mbit(1Mbitx8)的串行FLASH,具有高级的写保护机制,可以通过SPI兼容的总线进行读写。
M25P80一共有16个扇区,每个扇区有256个页,每个页有256 byte。
通过页写入指令,可以一次写入1-256 byte数据。
通过扇区擦除指令,可以以扇区为单位进行擦除。

2、M25P80的SPI工作模式
M25P80支持两种SPI模式:(0)  CPOL = 0 CPOH = 0   (3)  CPOL = 1, CPOH = 1


3、操作指令
(1)写使能指令(0x06)
部分指令如页写入指令,扇区擦除指令,批量擦除指令操作之前都需要发送写使能指令。
写使能指令时序:


(2)页写入指令(0x02)
页写入需要两个指令,一个是写使能指令(1个字节),另一个是页写入指令(4个字节以上)。


(3)扇区擦除指令(0xD8)
在页写入之前,我们必须擦除相应的扇区。扇区擦除也需要两个指令,一个是写使能指令(1个字节),另一个是扇区擦除指令(4个字节)。
扇区擦除指令时序:


(4)批量擦除指令(0xC7)
批量擦除是指擦除整个芯片内容。批量擦除需要两个指令,一个是写使能指令(1个字节),另一个是批量擦除指令(1个字节)
批量擦除指令时序:


(5)读ID指令(0x9F)
发送读ID指令时数据输出序列:

读ID指令时序:


(6)读数据指令(0x03)
写入读数据指令(1个字节)和地址(3个字节),可以读回1个字节以上的数据。
读数据指令时序:


(7)读状态指令(0x05)
状态寄存器格式:

读状态寄存器指令时序:


读SPI FLASH ID程序:
u32 SPI_FLASH_ReadID(void)
{
  u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
  /* 设置片选为低电平,选中SPI FLASH*/
  SPI_FLASH_CS_LOW();
  /* 发送读ID指令0x9F */
  SPI_FLASH_SendByte(0x9F);
  /* 从SPI FLASH读出第一个字节(0x20) */
  Temp0 = SPI_FLASH_SendByte(Dummy_Byte);
  /* 从SPI FLASH读出第二个字节(0x20) */
  Temp1 = SPI_FLASH_SendByte(Dummy_Byte);
  /* 从SPI FLASH读出第三个字节(0x14) */
  Temp2 = SPI_FLASH_SendByte(Dummy_Byte);
  /* 设置片选为高电平,取消选择SPI FLASH */
  SPI_FLASH_CS_HIGH();
  Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
  return Temp;
}

SPI FLASH扇区擦除函数:
void SPI_FLASH_SectorErase(u32 SectorAddr)
{
  /* 发送写使能指令 */
  SPI_FLASH_WriteEnable();
  /* 扇区擦除 */
  /* 设置片选为低电平,选中SPI FLASH */
  SPI_FLASH_CS_LOW();
  /* 发送扇区擦除指令 */
  SPI_FLASH_SendByte(SE);
  /* 发送扇区地址a23~a16 */
  SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
  /* 发送扇区地址a15~a8 */
  SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
  /* 发送扇区地址a7~a0 */
  SPI_FLASH_SendByte(SectorAddr & 0xFF);
  /* 设置片选为高电平,取消选择SPI FLASH */
  SPI_FLASH_CS_HIGH();
  /* 等待写操作完成 */
  SPI_FLASH_WaitForWriteEnd();
}

写入数据到SPI FLASH,SPI FLASH只提供了页写入指令对SPI FLASH进行写操作,一次最多能写256 Byte数据,所以如果超过256 Byte,我们要多次调用页写入指令。
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
  u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
  Addr = WriteAddr % SPI_FLASH_PageSize;  //检测地址WriteAddr是否是页(256 Byte)对齐的
  count = SPI_FLASH_PageSize - Addr;  //Addr 是最后不足一页的地址偏移,count是补齐一页的地址长度
  NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;  //计算出总页数
  NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;  //计算出最后不足一页的数据长度
  if (Addr == 0) /* 判断地址WriteAddr是否是页对齐的*/
  {
    if (NumOfPage == 0) /* 要写入的字节数NumByteToWrite 小于 页大小SPI_FLASH_PageSize,则只调用一次页写入指令即可,否则要调用多次*/
    {
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
    }
    else /* 要写入的字节数NumByteToWrite 小于 页大小SPI_FLASH_PageSize */
    {
      while (NumOfPage--) //写入NumOfPage个页
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr +=  SPI_FLASH_PageSize;  //地址指向下一页
        pBuffer += SPI_FLASH_PageSize; //pBuffer指针指向要写入下一页的数据
      }
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle); //写入最后不足一页的字节数
    }
  }
  else /* 地址WriteAddr不是页对齐的  */
  {
    if (NumOfPage == 0) /* 要写入的字节数NumByteToWrite 小于页大小SPI_FLASH_PageSize */
    {
      if (NumOfSingle > count) /* 要写入的数据超出了当前地址WriteAddr到页尾的距离,(NumByteToWrite + WriteAddr) > SPI_FLASH_PageSize */
      {
        temp = NumOfSingle - count;  //超出部分写到下一页,temp表示写到下一页的字节数
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);  //写当前页
        WriteAddr +=  count;  //地址指向下一页
        pBuffer += count; //pBuffer指针指向要写入下一页的数据
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);  //写下一页
      }
      else
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);  //要写入的数据没有超出当前地址WriteAddr到页尾的距离,直接写入当前页
      }
    }
    else /* 要写入的字节数NumByteToWrite 大于页大小SPI_FLASH_PageSize */
    {
      NumByteToWrite -= count;  //NumByteToWrite -count表示补齐当前页地址长度后,剩余的数据长度
      NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;  //剩余数据长度所占的页数
      NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; //剩余数据长度最后不足一页的数据长度
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);  //补齐当前页
      WriteAddr +=  count;  //地址指向下一页
      pBuffer += count; //pBuffer指针指向要写入下一页的数据
      while (NumOfPage--) //写入NumOfPage个页
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr +=  SPI_FLASH_PageSize;  //地址指向下一页
        pBuffer += SPI_FLASH_PageSize; //pBuffer指针指向要写入下一页的数据
      }
      if (NumOfSingle != 0)
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle); //写入最后不足一页的字节数
      }
    }
  }
}

SPI FLASH页写入函数:
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
  /* 发送写使能指令 */
  SPI_FLASH_WriteEnable();
  /* 片选设为低电平,选中SPI FLASH */
  SPI_FLASH_CS_LOW();
  /* 发送页写入指令 */
  SPI_FLASH_SendByte(WRITE);
  /* 发送地址a23~a16 */
  SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
  /* 发送地址a15~a8 */
  SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
  /* 发送地址a7~a0 */
  SPI_FLASH_SendByte(WriteAddr & 0xFF);
  /* 循环写入NumByteToWrite个字节数据 */
  while (NumByteToWrite--)
  {
    /* 发送一字节数据 */
    SPI_FLASH_SendByte(*pBuffer);
    /* pBuffer指向下一个要写入的数据 */
    pBuffer++;
  }
  /* 片选设为高电平,取消选中SPI FLASH*/
  SPI_FLASH_CS_HIGH();
  /* 等待写操作完成 */
  SPI_FLASH_WaitForWriteEnd();
}

从SPI FLASH读取数据,发送读指令后,可以读取无限个数据:
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
  /* 片选设为低电平,选中SPI FLASH */
  SPI_FLASH_CS_LOW();
  /* 发送读数据指令 */
  SPI_FLASH_SendByte(READ);
  /* 发送地址a23~a16 */
  SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
  /* 发送地址a15~a8 */
  SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
  /* 发送地址a7~a0 */
  SPI_FLASH_SendByte(ReadAddr & 0xFF);
  while (NumByteToRead--) /* 读取NumByteToRead个字节数据 */
  {
    /* 从SPI FLASH读一字节数据*/
    *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
    /* pBuffer指针指向读出数据被存放到的下一个位置 */
    pBuffer++;
  }
  /* 设置片选为高电平,取消选择SPI FLASH */
  SPI_FLASH_CS_HIGH();
}


四、了解如何把字库写到SPI FLASH及把字库读出显示
把hzk16.c文件里的字库数据写到SPI FLASH里,首先要擦除要写入的扇区
相关代码:
/* main.c */
sectorCnt = sizeof(GB2312_16x16_Table)/SPI_FLASH_SectorSize;  //计算字库数据需要写入的扇区数
if(sizeof(GB2312_16x16_Table)%SPI_FLASH_SectorSize != 0)
  sectorCnt++;   //如果不满一个扇区,擦除一个扇区
for(i = 0; i < sectorCnt; i++ )  //擦除sectorCnt个扇区
{
  SPI_FLASH_SectorErase(i*SPI_FLASH_SectorSize);  //擦除一个扇区
}

SPI_FLASH_BufferWrite((u8*)GB2312_16x16_Table, 0, sizeof(GB2312_16x16_Table)); //把字库数据写入到SPI FLASH,从地址0开始写。

/* stm3210e_eval_lcd.c */
void LCD_DisplayChineseChar(uint16_t Line, uint16_t Column, uint8_t *Char_GB)
{
uint32_t offset;
uint16_t byteOfDots;
uint8_t pData[24*24];  
byteOfDots = LCD_Currentfonts->Height*(LCD_Currentfonts->Width/8);
offset = 94 * (*Char_GB - 0xA1) * byteOfDots + (*(Char_GB+1) - 0xA1) * byteOfDots;  //计算当前要显示的字符的点阵数据在SPI FLASH中的偏移地址
SPI_FLASH_BufferRead(pData, offset, byteOfDots);  //从SPI FLASH中读取字库点阵数据,存放在pData数组中
//  LCD_DrawChineseChar(Line, Column, &LCD_Currentfonts->table[offset]);
LCD_DrawChineseChar(Line, Column, (uint16_t*)pData);  //调用函数把点阵数据画到LCD上
}
沙发
三好学生| | 2013-10-12 20:34 | 只看该作者
讲得很详细,顶一个!!!

使用特权

评论回复
板凳
xi_liang|  楼主 | 2013-10-13 22:34 | 只看该作者
三好学生 发表于 2013-10-12 20:34
讲得很详细,顶一个!!!

谢谢帮顶:victory:

使用特权

评论回复
地板
nelsonfung| | 2013-11-27 16:21 | 只看该作者
当字库数据很大的时候应该就不能用这个方法了吧?

使用特权

评论回复
5
xi_liang|  楼主 | 2013-11-27 18:25 | 只看该作者
nelsonfung 发表于 2013-11-27 16:21
当字库数据很大的时候应该就不能用这个方法了吧?

那是,字库大的时候可以考虑从SD里读出来,写到SPI FLASH里面去。
如果是大批量生产一般是用编程器烧好程序,再去贴片的吧

使用特权

评论回复
6
sinadz| | 2013-11-27 18:30 | 只看该作者
很不错的开发教程

使用特权

评论回复
7
pkat| | 2013-11-27 18:42 | 只看该作者
很好的一些学习教程

使用特权

评论回复
8
秋天落叶| | 2013-11-27 18:49 | 只看该作者
很详细的教程

使用特权

评论回复
9
Jason_Ding| | 2013-11-28 08:48 | 只看该作者
很详细的很不错的教程!

使用特权

评论回复
10
jany.wei| | 2014-2-26 18:02 | 只看该作者
很详细,谢谢楼主。

使用特权

评论回复
11
sinadz| | 2014-2-26 18:12 | 只看该作者
很实用的教程

使用特权

评论回复
12
秋天落叶| | 2014-2-26 18:25 | 只看该作者
很给力的开发板教程

使用特权

评论回复
13
xsgy123| | 2014-2-26 18:32 | 只看该作者
教程还是挺实用的

使用特权

评论回复
14
kenmy| | 2014-4-17 16:12 | 只看该作者
楼主真是有心的人,写的很详细,以后我做完项目也来总结,大赞

使用特权

评论回复
15
xi_liang|  楼主 | 2014-5-10 00:05 | 只看该作者
xsgy123 发表于 2014-2-26 18:32
教程还是挺实用的

多谢赞赏,花了不少心机的:lol:handshake

使用特权

评论回复
16
xi_liang|  楼主 | 2014-5-10 00:06 | 只看该作者
kenmy 发表于 2014-4-17 16:12
楼主真是有心的人,写的很详细,以后我做完项目也来总结,大赞

看来这个用的人还是比较多的。多谢夸奖,多谢支持

使用特权

评论回复
17
一般首席| | 2014-12-1 21:26 | 只看该作者
低功耗

使用特权

评论回复
18
kgs0716| | 2015-3-4 15:14 | 只看该作者
感觉比原子写的SPI教程通俗易懂很多,棒

使用特权

评论回复
19
quray1985| | 2015-4-6 19:39 | 只看该作者
楼主讲的很详细,不错,学习了

使用特权

评论回复
20
小浣熊| | 2015-4-6 21:28 | 只看该作者
嗯,确实说得好~!百为看来不错。。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:http://baiweijishu.taobao.com/ 百为STM32开发板 兼容官方STM3210E-EVAL开发板 WM-G-MR-09 WIFI开发板

41

主题

285

帖子

10

粉丝