RT_threadのSPI设备驱动W25Q/NM25Q128

  • Post author:
  • Post category:其他





前言

本篇文章记录利用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;      //下一个扇区可以写完了
            }
        }
    }
}



版权声明:本文为qq_40993639原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。