SPI
一、简介
SPI 是串行外设接口(Serial Peripheral Interface)的缩写。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,SDI(数据输入),SDO(数据输出),SCLK(时钟),CS(片选),就可以完成一个主设备和一个或多个从设备之间的通信,事实上 3 根也可以(单向传输时)。在嵌入式系统中,主要应用于 EEPROM,FLASH,实时时钟,AD 转换器,数字信号处理器和数字信号解码器之间的通信,在全双工模式下,传输速率可达上 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://doc.openluat.com/wiki/21?wiki_page_id=2068
3.2 数据通信线
USB 数据线一根(micro USB)。
3.3 PC 电脑
WIN7 以及以上版本的 WINDOWS 系统。
3.4 SPI FLASH 模块
25Q64JVSIQ 注意电平是 3.3V
3.5 电平转换模块
双向电平转换模块(3.3V、1.8V)
3.6 组装硬件环境
USB 数据线插入 USB 口,另一端与电脑相连,拨码开关全部拨到 ON,串口切换开关选择 UART1,USB 供电的 4V 对应开关拨至 ON 档,SIM 卡放到 SIM 卡槽中锁紧,如下图所示。
由于 SPI FLASH 模块只支持 3.3v 电平,开发板 SPI 接口只支持 1.8V 电平,所以 SPI FLASH 模块和开发板之间需要用电平转换模块将相互的 IO 电平转换,具体连接图如下图所示。
开发板与电平转换模块连接:
名称 | 开发板 | 电平转换模块 |
---|---|---|
CS | /SPI1_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 说明
5.1.1 spi 开启接口
spi.setup(id,chpa,cpol,dataBits,clock,duplex)
参数 | 类型 | 释义 | 取值 |
---|---|---|---|
id | number | SPI 的 ID spi.SPI_1 表示 SPI1,Air720U 系列只有 SPI1 作为普通 SPI 接口使用,固定传 spi.SPI_1 即可通道选择 | 0:普通 spi 1:lcdspi |
chpa | number | 0:第一个 clk 的跳变沿传输数据,1:第二个 clk 跳变沿传输数据 | 0/1 |
cpol | number | clock 管脚的默认状态,0 表示低电平,1 表示高电平 | 0/1 |
dataBits | number | 数据位 | 8 |
clock | number | spi 时钟频率,number 数值 | 110000-100000000 |
duplex | number | 只支持全双工 | 1 |
返回值
返回值 | 类型 | 释义 | 取值 |
---|---|---|---|
result | number | 失败 | 0 |
result | number | 成功 | 1 |
5.1.2 spi 写数据
spi.send(id,data[,cscontrol])
参数 | 类型 | 释义 | 取值 |
---|---|---|---|
id | number | SPI 的 ID spi.SPI_1 表示 SPI1,Air720U 系列只有 SPI1 作为普通 SPI 接口使用,固定传 spi.SPI_1 即可通道选择 | 0:普通 spi 1:lcdspi |
data | string | string 类型,要发送的数据 | |
cscontrol | number | cs 是否自动控制,可选参数默认 0,1 代表 CS 随数据自动变化,0 代表 cs 先拉低数据结束再恢复 | 0/1 |
返回值
返回值 | 类型 | 释义 | 取值 |
---|---|---|---|
result | number | 写数据的长度 |
5.1.3 读数据
spi.recv(id,length[,cscontrol])
参数 | 类型 | 释义 | 取值 |
---|---|---|---|
id | number | SPI 的 ID spi.SPI_1 表示 SPI1,Air720U 系列只有 SPI1 作为普通 SPI 接口使用,固定传 spi.SPI_1 即可通道选择 | 0:普通 spi 1:lcdspi |
length | number | 要读取的数据的长度 | |
cscontrol | number | cs 是否自动控制,可选参数默认 0,1 代表 CS 随数据自动变化,0 代表 cs 先拉低数据结束再恢复 | 0/1 |
返回值
返回值 | 类型 | 释义 | 取值 |
---|---|---|---|
result | string | 读取的数据内容 |
5.1.4 读写数据
spi.send_recv(id,data[,cscontrol])
参数 | 类型 | 释义 | 取值 |
---|---|---|---|
id | number | SPI 的 ID spi.SPI_1 表示 SPI1,Air720U 系列只有 SPI1 作为普通 SPI 接口使用,固定传 spi.SPI_1 即可通道选择 | 0:普通 spi 1:lcdspi |
data | string | 要发送的数据 | |
cscontrol | number | cs 是否自动控制,可选参数默认 0,1 代表 CS 随数据自动变化,0 代表 cs 先拉低数据结束再恢复 | 0/1 |
返回值
返回值 | 类型 | 释义 | 取值 |
---|---|---|---|
result | string | 读取的数据内容 |
5.1.5 关闭 SPI
spi.close(id)
参数 | 类型 | 释义 | 取值 |
---|---|---|---|
id | number | SPI 的 ID,spi.SPI_1 表示 SPI1,Air720U 系列只有 SPI1 作为普通 SPI 接口使用,固定传 spi.SPI_1 即可 | 0:普通 spi 1:lcdspi 复用为普通 spi |
返回值
返回值 | 类型 | 释义 | 取值 |
---|---|---|---|
result | number | 失败 | 0 |
result | number | 成功 | 1 |
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 测试模块。
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,这个电压是写死的,无法设置,如果目标芯片工作电压不在此范围内需要用电平转换芯片。
给读者的话
本篇文章由
杨超
开发;本篇文章描述的内容,如果有错误、细节缺失、细节不清晰或者其他任何问题,总之就是无法解决您遇到的问题;
请登录合宙技术交流论坛,点击文档找错赢奖金-Air724UG-LuatOS-软件指南-硬件驱动-SPI;
用截图标注+文字描述的方式跟帖回复,记录清楚您发现的问题;
我们会迅速核实并且修改文档;
同时也会为您累计找错积分,您还可能赢取月度找错奖金!