目录
前言
本篇文章记录利用RTthread的SPI设备来驱动SPI flash,熟悉RTthread的SPI设备。另外对SPI flash(NM25Q/W25Q)不做介绍,仅仅是按照手册上的时序编写代码。NM25Q和W25Q基本上是一样的,只不过是设备厂商ID不一样。本次用的是一颗NM25Q128芯片做实践。
1 将NM25Q挂载到SPI总线上
/*先定义一个SPI设备对象*/
struct rt_spi_device * spi_dev_nm25q;
int rt_hw_NM25Q_Init(void)
{
rt_uint8_t dummy = 0xFF;
rt_hw_spi_device_attach("spi2", "spi20", GPIOB, GPIO_PIN_12);
spi_dev_nm25q = (struct rt_spi_device *)rt_device_find("spi20");
if (!spi_dev_nm25q)
{
rt_kprintf("spi sample run failed! can't find spi20 device!\n" );
}
else
{ /* configure SPI*/
struct rt_spi_configuration cfg;
cfg.data_width = 8;
cfg.mode = RT_SPI_MASTER | RT_SPI_MODE_3 | RT_SPI_MSB;
cfg.max_hz = 35 * 1000 *1000; //用的主控是stm32F103,所以最大频率不超过35MHz
rt_spi_configure(spi_dev_nm25q, &cfg);
rt_spi_send(spi_dev_nm25q,&dummy,1);//开启传输
}
return 0;
}
INIT_DEVICE_EXPORT(rt_hw_NM25Q_Init);
这样就实现了将NM25Q挂载到SPI2上,设备名为SPI20,将这函数添加到自动初始化序列实现初始化,可以在命令行上输入list_device看到设备注册成功。
2 读取NM25Q的设备ID
时序图如下:
从上图看到CS片选拉低,先发送命令,在发送24位地址0x000000,接着读取Manufacturer/Device ID, 最后再把片选拉高。
在RTthread中的SPI设备函数中符合这种时序的有函数:
struct rt_spi_message *rt_spi_transfer_message(struct rt_spi_device *device,struct rt_spi_message *message) ;
读取设备ID的函数最终编写如下:
void Read_ManufacturerID(int argc, char *argv[])
{
rt_uint8_t cmd = 0x90;
rt_uint8_t id[5];
struct rt_spi_message msg1, msg2;
msg1.cs_take = 1;
msg1.cs_release = 0;
msg1.length = 1;
msg1.next = &msg2;
msg1.recv_buf = RT_NULL;
msg1.send_buf = &cmd;
msg2.cs_take = 0;
msg2.cs_release = 1;
msg2.length = 5;
msg2.next = RT_NULL;
msg2.recv_buf = id;
msg2.send_buf = RT_NULL;
rt_spi_transfer_message(spi_dev_nm25q, &msg1);
rt_kprintf("NM25Q128 ID: 0x%x%x.\r\n", id[4],id[3]);
}
MSH_CMD_EXPORT(Read_ManufacturerID, NM25Q128 ID);
这里先将该函数导出到MSH命令列表中,主要是先观察与NM25Q通信是否正常先,在命令行中输入Read_ManufacturerID看看该函数运行结果。
读出来的ID正常,说明SPI通信正常,接着就是写写剩余的时序。
3 读取NM25Q状态寄存器1
读取状态寄存器1/2/3的时序如下:
先拉低片选,发送命令,再读取SR(1个字节)。在RTthread的SPI设备函数接口中,函数
rt_inline rt_uint8_t rt_spi_sendrecv8(struct rt_spi_device *device, rt_uint8_t data)
满足时序要求。为什么不单独使用函数
rt_inline rt_size_t rt_spi_recv(struct rt_spi_device *device, void *recv_buf, rt_size_t length)
和rt_inline rt_size_t rt_spi_send(struct rt_spi_device *device, const void *send_buf, rt_size_t length)
呢?因为这两个函数都是拉低片选单方向传输length个字节就把片选给拉高了,不满足。下面是代码实现:
/**
* @brief: 读取NM25Q128的状态
* @param: void
* @return
*/
static rt_uint8_t NM25Q128_ReadSR(void)
{
rt_uint8_t SR;
SR = rt_spi_sendrecv8(spi_dev_nm25q, NM25X_ReadStatusReg);
return SR;
}
4 写NM25Q状态寄存器1
时序:
时序对应代码:
/**
* @brief: 写NM25Q状态寄存器
* @param:rt_uint8_t sr,寄存器内部的数据
* @return: void
*/
static void NM25Q128_Write_SR(rt_uint8_t sr)
{
rt_uint8_t cmd = NM25X_WriteStatusReg;
struct rt_spi_message msg1, msg2;
msg1.cs_take = 1;
msg1.cs_release = 0;
msg1.length = 1;
msg1.next = &msg2;
msg1.recv_buf = RT_NULL;
msg1.send_buf = &cmd;
msg2.cs_take = 0;
msg2.cs_release = 1;
msg2.length = 1;
msg2.next = RT_NULL;
msg2.recv_buf = RT_NULL;
msg2.send_buf = &sr;
rt_spi_transfer_message(spi_dev_nm25q, &msg1);
}
5 使能NM25Q的写
时序:
时序对应代码:
/**
* @brief: NM25Q写使能
* @param: void
* @return: void
*/
static void NM25QXX_Write_Enable(void)
{
rt_uint8_t cmd = NM25X_WriteEnable;
rt_spi_send(spi_dev_nm25q, &cmd, 1);
}
6 失能NM25Q的写
时序:
时序对应代码:
/**
* @brief: NM25Q写失能
* @param:void
* @return: void
*/
static void NM25QXX_Write_Disable(void)
{
rt_uint8_t cmd = NM25X_WriteDisable;
rt_spi_send(spi_dev_nm25q, &cmd, 1);
}
7 用户读取NM25Q内存数据
时序:
先发送读命令03H,接着是24位的地址,高字节优先,然后就能开始读数据了,时序对应代码:
/**
* @brief: 读取NM25Q,在指定地址开始读取指定长度的数据
* @param: rt_uint8_t *pBuffer:数据存储区
* rt_uint32_t ReadAddr:开始读取的地址(24bit)
* rt_uint16_t NumByteToRead:要读取的字节数(最大65535)
* @return: void
*/
void NM25QXX_Read(rt_uint8_t* pBuffer, rt_uint32_t ReadAddr, rt_uint16_t NumByteToRead)
{
rt_uint8_t cmd = NM25X_ReadData;
rt_uint8_t addr[3];
addr[0] = (rt_uint8_t)(ReadAddr>>16);
addr[1] = (rt_uint8_t)(ReadAddr>>8);
addr[2] = (rt_uint8_t)ReadAddr;
struct rt_spi_message msg1, msg2, msg3;
msg1.cs_take = 1;
msg1.cs_release = 0;
msg1.length = 1;
msg1.next = &msg2;
msg1.recv_buf = RT_NULL;
msg1.send_buf = &cmd;
msg2.cs_take = 0;
msg2.cs_release = 0;
msg2.length = 3;
msg2.next = &msg3;
msg2.recv_buf = RT_NULL;
msg2.send_buf = &addr[0];
msg3.cs_take = 0;
msg3.cs_release = 1;
msg3.length = NumByteToRead;
msg3.next = RT_NULL;
msg3.recv_buf = pBuffer;
msg3.send_buf = RT_NULL;
rt_spi_transfer_message(spi_dev_nm25q, &msg1);
}
8 等待NM25Q空闲
在给NM25Q发送命令之前需要确保NM25Q是空闲的状态,所以写个等待空闲的函数。
/**
* @brief: 等待空闲
* @param: void
* @return: void
*/
void NM25QXX_Wait_Busy(void)
{
while((NM25Q128_ReadSR()&0x01)==0x01); // 等待BUSY位清空
}
9 NM25Q掉电模式
时序:
tDP最大3us,这里程序直接给1ms。
/**
* @brief: NM25Q进入掉电模式
* @param: void
* @return:void
*/
void NM25QXX_PowerDown(void)
{
rt_uint8_t cmd = NM25X_PowerDown;
rt_spi_send(spi_dev_nm25q, &cmd, 1);
rt_thread_delay(1);
}
10 唤醒NM25Q
时序:
/**
* @brief: 唤醒NM25Q
* @param: void
* @return: void
*/
void NM25QXX_WAKEUP(void)
{
rt_uint8_t cmd = NM25X_ReleasePowerDown;
rt_spi_send(spi_dev_nm25q, &cmd, 1);
rt_thread_delay(1);
}
11 擦除NM25Q整片内存
时序:
A Write Enable Instruction must be executed before the device will accept the Chip erase instruction.所以代码写成:
/**
* @brief: NM25Q擦除整片
* @param: void
* @return: void
*/
void NM25QXX_Erase_Chip(void)
{
rt_uint8_t cmd = NM25X_ChipErase;
NM25QXX_Write_Enable(); //SET WEL
NM25QXX_Wait_Busy();
rt_spi_send(spi_dev_nm25q, &cmd ,1);
NM25QXX_Wait_Busy(); //等待芯片擦除结束
}
12 擦除NM25Q扇区
时序:
时序对应代码如下:
/**
* @brief: 擦除一个扇区
* @param: rt_uint32_t Dst_Addr,扇区地址,单位:1扇区
* @return: void
*/
void NM25QXX_Erase_Sector(rt_uint32_t Dst_Addr)
{
rt_uint8_t cmd = NM25X_SectorErase;
rt_uint8_t addr[3];
struct rt_spi_message msg1, msg2;
Dst_Addr*=4096;
addr[0] = (rt_uint8_t)(Dst_Addr >> 16);
addr[1] = (rt_uint8_t)(Dst_Addr >> 8);
addr[2] = (rt_uint8_t)(Dst_Addr);
msg1.cs_take = 1;
msg1.cs_release = 0;
msg1.length = 1;
msg1.next = &msg2;
msg1.recv_buf = RT_NULL;
msg1.send_buf = &cmd;
msg2.cs_take = 0;
msg2.cs_release = 1;
msg2.length = 3;
msg2.next = RT_NULL;
msg2.recv_buf = RT_NULL;
msg2.send_buf = &addr[0];
NM25QXX_Write_Enable(); //SET WEL
NM25QXX_Wait_Busy();
rt_spi_transfer_message(spi_dev_nm25q, &msg1);
NM25QXX_Wait_Busy(); //等待擦除完成
}
13 写NM25Q一页数据(页编程)
时序:
对应代码如下:
/**
* @brief: NM25Q写一页数据
* @param: rt_uint8_t *pBuffer:数据源
* rt_uint32_t WriteAddr:写的起始地址
* rt_uint16_t NumByteToWrite: 写入字节个数
* @return:void
*/
void NM25QXX_Write_Page(rt_uint8_t* pBuffer,rt_uint32_t WriteAddr,rt_uint16_t NumByteToWrite)
{
rt_uint8_t cmd = NM25X_PageProgram;
struct rt_spi_message msg1, msg2, msg3;
rt_uint8_t addr[3];
addr[0] = (rt_uint8_t)(WriteAddr>>16);
addr[1] = (rt_uint8_t)(WriteAddr>>8);
addr[2] = (rt_uint8_t)(WriteAddr);
NM25QXX_Write_Enable(); //SET WEL
msg1.cs_take = 1;
msg1.cs_release = 0;
msg1.length = 1;
msg1.next = &msg2;
msg1.recv_buf = RT_NULL;
msg1.send_buf = &cmd;
msg2.cs_take = 0;
msg2.cs_release = 0;
msg2.length = 3;
msg2.next = &msg3;
msg2.recv_buf = RT_NULL;
msg2.send_buf = &addr[0];
msg3.cs_take = 0;
msg3.cs_release = 1;
msg3.length = NumByteToWrite;
msg3.next = RT_NULL;
msg3.recv_buf = RT_NULL;
msg3.send_buf = pBuffer;
rt_spi_transfer_message(spi_dev_nm25q, &msg1);
NM25QXX_Wait_Busy(); //等待写入结束
}
14 无检查写NM25Q
至此,可以利用上面的函数大片大片的写NM25Q了。代码如下:
/**
* @brief: NM25Q写,必须确保写入之前内存是0xFF,具有自动换页功能,在指定地址开始写入指定长度的数据,但是要确保地址不越界!
* @param: rt_uint8_t *pBuffer:写入数据
* rt_uint32_t WriteAddr: 写入地址
* rt_uint16_t NumByteToWrite: 写入字节个数
* @return: void
*/
void NM25QXX_Write_NoCheck(rt_uint8_t* pBuffer, rt_uint32_t WriteAddr, rt_uint16_t NumByteToWrite)
{
rt_uint16_t pageremain;
pageremain=256-WriteAddr%256; //单页剩余的字节数
if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节
while(1)
{
NM25QXX_Write_Page(pBuffer,WriteAddr,pageremain);
if(NumByteToWrite==pageremain)break;//写入结束了
else //NumByteToWrite>pageremain
{
pBuffer+=pageremain;
WriteAddr+=pageremain;
NumByteToWrite-=pageremain; //减去已经写入了的字节数
if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节
else pageremain=NumByteToWrite; //不够256个字节了
}
}
}
15 用户接口-写NM25Q
代码如下:
//写SPI FLASH
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
void NM25QXX_Write(rt_uint8_t* pBuffer, rt_uint32_t WriteAddr, rt_uint16_t NumByteToWrite)
{
rt_uint32_t secpos;
rt_uint16_t secoff;
rt_uint16_t secremain;
rt_uint16_t i;
rt_uint8_t NM25QXX_BUF[4096];
secpos = WriteAddr / 4096;//扇区地址
secoff = WriteAddr % 4096;//在扇区内的偏移
secremain = 4096 - secoff;//扇区剩余空间大小
if(NumByteToWrite <= secremain)
{
secremain = NumByteToWrite;//不大于4096个字节
}
while(1)
{
NM25QXX_Read(NM25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容
for(i=0; i<secremain; i++)//校验数据
{
if(NM25QXX_BUF[secoff+i] != 0XFF)
break;//需要擦除
}
if(i < secremain)//需要擦除
{
NM25QXX_Erase_Sector(secpos); //擦除这个扇区
for(i=0; i<secremain; i++) //将待写入的数据复制到后续
{
NM25QXX_BUF[i+secoff] = pBuffer[i];
}
NM25QXX_Write_NoCheck(NM25QXX_BUF, secpos*4096, 4096);//写入整个扇区
}
else
{
NM25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//不用擦除,直接写入扇区剩余区间.
}
if(NumByteToWrite==secremain)
{
break;//写入结束了
}
else//写入未结束
{
secpos++;//扇区地址增1
secoff = 0;//偏移位置为0
pBuffer += secremain; //指针偏移
WriteAddr += secremain; //写地址偏移
NumByteToWrite -= secremain; //字节数递减
if(NumByteToWrite > 4096)
{
secremain=4096;//下一个扇区还是写不完
}
else
{
secremain=NumByteToWrite; //下一个扇区可以写完了
}
}
}
}