跳转至

SPI

一、简介

SPI 是串行外设接口(Serial Peripheral Interface)的缩写。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,SDI(数据输入),SDO(数据输出),SCLK(时钟),CS(片选),就可以完成一个主设备和一个或多个从设备之间的通信,事实上 3 根也可以(单向传输时)。在嵌入式系统中,主要应用于 EEPROM,FLASH,实时时钟,AD 转换器,数字信号处理器和数字信号解码器之间的通信,在全双工模式下,传输速率可达上 110000-100000000 Mbps 的水平。

Air724 模块对外提供的硬件 SPI 接口数量有两路,即 SPI1、SPI2,其他 SPI 通道以复用封装作为 LCD 或 MMC 功能管脚使用。

二、演示功能概述

本教程教你如何使用开发板的 SPI 总线读写 FLASH。

三、准备硬件环境

3.1 开发板准备

使用 EVB_Air724 开发板,如下图所示:

淘宝购买链接:Air724UG-NFM 开发板淘宝购买链接

此开发板的详细使用说明参考:Air724UG 产品手册 中的《EVB_Air724UG_AXX 开发板使用说明》,写这篇文章时最新版本的使用说明为:《EVB_Air724UG_A14 开发板使用说明》;开发板使用过程中遇到任何问题,可以直接参考这份使用说明文档。

api:https://ask.openluat.com/wiki/21?wiki_page_id=2249

3.2 数据通信线

USB 数据线一根(micro USB)。

3.3 PC 电脑

WIN7 以及以上版本的 WINDOWS 系统。

3.4 SPI FLASH 模块

25Q64JVSIQ 注意电平是 3.3V

购买链接:https://item.taobao.com/item.htm?abbucket=3&id=742785947394&ns=1&priceTId=213e38aa17287069797641982e1679&spm=a21n57.1.item.2.8b37523cxn7dzT&utparam=%7B%22aplus_abtest%22%3A%22ec019368ed63617475ac7a8adcb51908%22%7D&xxc=taobaoSearch

3.5 电平转换模块

双向电平转换模块(3.3V、1.8V)

购买链接:https://item.taobao.com/item.htm?abbucket=3&id=580885726137&ns=1&priceTId=215040d217298301977777928e74a7&spm=a21n57.1.item.2.5571523cPCNFhy&utparam=%7B%22aplus_abtest%22%3A%220f9d584f58b94489848e30d47ca94e38%22%7D&xxc=taobaoSearch

3.6 组装硬件环境

USB 数据线插入 USB 口,另一端与电脑相连,拨码开关全部拨到 ON,串口切换开关选择 UART1,USB 供电的 4V 对应开关拨至 ON 档,如下图所示。

由于 SPI FLASH 模块只支持 3.3v 电平,开发板 SPI 接口只支持 1.8V 电平,所以 SPI FLASH 模块和开发板之间需要用电平转换模块将相互的 IO 电平转换,具体连接图如下图所示。

开发板与电平转换模块连接:

名称
开发板
电平转换模块
CS
sSPI1_CS/GPIO_10
A5
CLK
SPI1_CLK/GPIO_9
A1
DI
SPI1_DIN/GPIO_12
A3
DO
SPI1_DOUT/GPIO_11
A7
GND
GND
GND
VCC
3.3V
VB

SPI FLASH 与电平转换模块连接:

名称
电平转换模块
SPI FLASH
CS
B5
CS
CLK
B1
CLK
DI
B3
DI
DO
B7
DO
GND
GND
GND

四、准备软件环境

4.1 下载调试工具

使用说明参考:Luatools 下载和详细使用

4.2 源码及固件

4.2.1 底层 core 下载

下载底层固件,并解压

链接:https://docs.openluat.com/air724ug/luatos/firmware/

如下图所示,红框的是我们要使用到的

4.2.2 本教程使用的 demo 见附件:

https://gitee.com/openLuat/LuatOS-Air724UG/tree/master/script_LuaTask/demo/spiFlash

4.3 下载固件和脚本到开发板中

  • 打开 Luatools,开发板上电开机,如开机成功 Luatools 会打印如下信息。

  • 点击项目管理测试选项。

  • 进入管理界面,如下图所示。

  • 点击选择文件,选择底层固件,我的文件放在 D:\luatOS\Air724 路径中

  • 点击增加脚本或资源文件,选择之前下载的程序源码,如下图所示。

  • 点击下载底层和脚本,下载完成如下图所示。

五、代码示例介绍

5.1 API 说明

本文用到的接口函数不做详细介绍,可通过点击右侧链接查看具体介绍:spi API

5.2 test_SPIFlash.lua 代码

test_SPIFlash 程序流程。

--- 验证spi flash驱动接口 目前该驱动兼容w25q32 bh25q32
require"spiFlash"
require "pm"
pm.wake("testSpiFlash")

local flashlist = {
    [0xEF15] = 'w25q32',
    [0xEF16] = 'w25q64',
    [0xEF17] = 'w25q128',
    [0x6815] = 'bh25q32',
}

sys.taskInit(function()
    local spi_flash = spiFlash.setup(spi.SPI_1)
    while true do
        local manufactureID, deviceID = spi_flash:readFlashID()
        print('spi flash id', manufactureID, deviceID)
        local flashName = (manufactureID and deviceID) and flashlist[manufactureID*256 + deviceID]
        log.info('testSPIFlash', 'flash name', flashName or 'unknown')
        print('spi flash erase 4K', spi_flash:erase4K(0x1000))
        print('spi flash write', spi_flash:write(0x1000, '123456'))
        print('spi flash read', spi_flash:read(0x1000, 6)) -- '123456'
        sys.wait(1000)
    end
end)

5.3 main.lua 代码

本代码为主程序脚本,系统启动后首先会对 4G 网络进行配置,然后加载 test_SPIFlash 测试模块。

PROJECT = "TEST_SPI_FLASH"
VERSION = "2.0.0"

--加载日志功能模块,并且设置日志输出等级
--如果关闭调用log模块接口输出的日志,等级设置为log.LOG_SILENT即可
require "log"
LOG_LEVEL = log.LOGLEVEL_TRACE

require "sys"

require "net"
--每1分钟查询一次GSM信号强度
--每1分钟查询一次基站信息
net.startQueryAll(60000, 60000)

--此处关闭RNDIS网卡功能
--否则,模块通过USB连接电脑后,会在电脑的网络适配器中枚举一个RNDIS网卡,电脑默认使用此网卡上网,导致模块使用的sim卡流量流失
--如果项目中需要打开此功能,把ril.request("AT+RNDISCALL=0,1")修改为ril.request("AT+RNDISCALL=1,1")即可
--注意:core固件:V0030以及之后的版本、V3028以及之后的版本,才以稳定地支持此功能
ril.request("AT+RNDISCALL=0,1")

require "test_SPIFlashRandomRW"

--启动系统框架
sys.init(0, 0)
sys.run()

5.4 spiFlash.lua代码

spiFlash.lua 为 W25Q32/BH25Q32 驱动代码。

--- 模块功能:W25Q32/BH25Q32驱动代码
-- @module SPIFlash
-- @author openLuat
-- @license MIT
-- @copyright openLuat
-- @release 2018.8.26

module(..., package.seeall)

local READ_ID = 0x90
local WRITE_ENABLE = 0x06
local READ_STATUS = 0x05
local FLASH_READ = 0x03
local ERASE_SECTOR_4K = 0x20
local BLOCK_ERASE_64K = 0xD8
local ERASE_CHIP = 0x60
local PAGE_PROGRAM = 0x02

local gpio_set = pio.pin.setval
local mt = { __index = {} }

local function address24bit(addr)
    return pack.pack('>I', addr):sub(2, 4)
end

--- 创建spi flash驱动实例
-- @param id spi id
-- @param timeout 等待flash busy状态解除的最长超时时间,默认60000ms
-- @return 返回spi flash驱动实例
-- @usage w25q32 = spiFlash.setup(spi.SPI_1, pio.P0_10)
function setup(id, timeout)

    if not spi.setup(id, 0, 0, 8, 13000000, 1) then
        log.error('BH25Q32.setup', 'spi setup failed')
    end

    local o = {
        id = id,
        timeout = timeout or 60000, -- ms
        pagesize = 256,
    }

    setmetatable(o, mt)
    return o
end

--- 释放spi flash驱动实例
-- @param
-- @return
-- @usage w25q32:close()
function mt.__index:close()
    spi.close(self.id)
end

--- 读取flash id
-- @return manufactureID, deviceID 返回两个参数 厂商id,设备id
function mt.__index:readFlashID()
    log.info("send",pack.pack('bAA', READ_ID, address24bit(0), '\255\255'):toHex())
    local r = spi.send_recv(self.id, pack.pack('bAA', READ_ID, address24bit(0), '\255\255'))
    if r then
        return string.byte(r, 5, 6)
    else
        return false
    end
end

--- 读取flash状态寄存器 STATUS S0-S7
-- @return 返回flash状态寄存器bit0-bit7的值
function mt.__index:readStatus()
    local r = spi.send_recv(self.id, string.char(READ_STATUS, 0xff))
    return r and string.byte(r, 2)
end

--- 查询flash是否处于busy状态
-- @return true - 繁忙 false - 空闲
function mt.__index:busy()
    return self:readStatus() % 2 == 1
end

--- 等待flash空闲
-- @param timeout 超时时间 单位ms
--
function mt.__index:waitNotBusy(timeout)
    local status
    local step = 50
    local count = 0
    timeout = timeout or self.timeout
    while true do
        if not self:busy() then break end
        sys.wait(step)
        count = count + 1
        if count > timeout / step then
            log.error('BH25Q32.waitNotBusy', 'timeout')
            return false
        end
    end
    return true
end

--- 向spi flash 传输数据
-- @param send_data 要发送的数据
-- @param read_size 可选参数,需要读取的数据长度,如果是写指令,不填参数或者填0
-- @return nil - 失败 true - 写指令发送成功 string - 返回读取到的数据
function mt.__index:transfer(send_data, read_size)
    if not self:waitNotBusy() then return end

    if type(send_data) == 'number' then send_data = string.char(send_data) end
    if not read_size then read_size = 0 end

    if read_size == 0 then
        spi.send_recv(self.id, string.char(WRITE_ENABLE))
    end

    local r = spi.send_recv(self.id, send_data .. string.rep('\255', read_size))

    if read_size > 0 then
        return string.sub(r, send_data:len() + 1, -1)
    end

    return true
end

--- 读取spi flash 指定地址的数据
-- @param addr flash地址
-- @param len 长度
-- @return nil - 读取失败 string - 读取到的数据
-- @usage w25q32 = spiFlash.setup(spi.SPI_1, pio.P0_10); w25q32:read(0x1000, 6);
function mt.__index:read(addr, len)
    return self:transfer(string.char(FLASH_READ) .. address24bit(addr), len)
end

--- 按擦除sector(4K)的方式擦除指定地址的数据
-- @param addr 擦除的起始地址,会自动做4K对齐
-- @return true - 成功 false - 失败
function mt.__index:erase4K(addr)
    return self:transfer(string.char(ERASE_SECTOR_4K) .. address24bit(addr - addr % 0x1000)) and self:waitNotBusy()
end

--- 按擦除block 64K的方式擦除指定地址的数据
-- @param addr 擦除的起始地址,会自动做64K对齐
-- @return true - 成功 false - 失败
function mt.__index:erase64K(addr)
    return self:transfer(string.char(BLOCK_ERASE_64K) .. address24bit(addr - addr % 0x10000)) and self:waitNotBusy()
end

--- 擦除整个flash芯片的数据
-- @param 无
-- @return true - 成功 false - 失败
function mt.__index:eraseChip()
    return self:transfer(ERASE_CHIP) and self:waitNotBusy()
end

--- 向spi flash指定地址写入数据
-- @param address 写入的地址
-- @param data 数据
-- @return number - 成功写入的数据长度
function mt.__index:write(address, data)
    local ending_address = address + data:len()
    local wrote_len = 0
    local bytes_to_write = 0
    while address < ending_address do
        bytes_to_write = self.pagesize - (address % self.pagesize)
        if wrote_len + bytes_to_write > data:len() then bytes_to_write = data:len() - wrote_len end
        if not self:transfer(pack.pack('bAA', PAGE_PROGRAM, address24bit(address), data:sub(wrote_len + 1, wrote_len + bytes_to_write))) then
            break
        end
        address = address + bytes_to_write
        wrote_len = wrote_len + bytes_to_write
    end
    return wrote_len
end

六、开机调试

6.1 开发板开机

连接好硬件,启动 Luatools 软件,并下载固件后,系统运行信息将显示在界面中。红框中为开发板连接到 PC 机后正常打印的信息,如下图所示。

6.2 SPI FLASH 调试

读出 SPI FLASH ID, 并显示 SPI FLASH 的型号,同时进行读写测试。

七、常见问题

7.1 为什么 SPI 通信中会出现一些乱码或数据丢失

答:SPI 通信的接口电平不匹配。可用示波器观测 SPI 接口在通信过程中的电平状态和电平质量,确认用户所使用的通信设备中接口电平是否与 Air724 模块 SPI1 接口电平一致,Air724 开发板上的 Air724 模块的 SPI1 接口电平均为 1.8V,如不匹配,请确保一致。不能过高或过低,过低有可能会导致电平电压不能被识别,过高有可能会损坏 SPI 通信接口,另外,建议使用一些高速器件作为电平转换器件,可提高接口电平的信号质量。

7.2 标准 spi 可以驱动的目标芯片电压是多少?

1.8V,这个电压是写死的,无法设置,如果目标芯片工作电压不在此范围内需要用电平转换芯片。

7.3 SPI外置FLASH有demo吗 最大支持多少?

qspi外挂flash参考:demo/qspi 目前有测试过最大的是 16MByte 的 qspi,超过16M不支持,是硬件限制的(QSPI地址只有24位)。

标准spi外挂flash参考:demo/spiFlash,对flash容量没限制,需要自己实现驱动。