目录
前言
块设备是一类可以整块读取和写入的设备,对应的物理设备可以是SD卡和U盘之类,在块设备的基础之上可以使用文件系统来管理文件。
接下来介绍如何在MicroPython上使用块设备。
准备工作
- 硬件:
(1). 一块刷有MicroPython固件的开发板,可以是stm32开发板,也可以是ESP32,如果用ESP8266可能会遇到内存不够以及爆栈的问题,需要修改源码自行编译固件,这里不推荐使用;
(2). 一片W25QXX芯片,带有转接板,连接好之后如下图:
- 软件:
可以使用任意串口终端软件,这里推荐Thonny,集编辑器、终端、烧录器于一身,同时支持文件上传 。在这里下载> thonny
一、MicroPython是什么?
MicroPython是一个开源项目,它在资源受限的微控制器上实现了精简的Python3语法。
简单来说就是可以在单片机上运行Python脚本。
这是官网> MicroPython官网
二、使用块设备
1.测试官方示例
官方文档给出了一个简单的RAM块设备模型,接下来的移植将以它为基础
(官方文档,文件系统部分> Working with filesystems)
代码如下:
class RAMBlockDev:
def __init__(self, block_size, num_blocks):
self.block_size = block_size
self.data = bytearray(block_size * num_blocks)
def readblocks(self, block_num, buf):
for i in range(len(buf)):
buf[i] = self.data[block_num * self.block_size + i]
def writeblocks(self, block_num, buf):
for i in range(len(buf)):
self.data[block_num * self.block_size + i] = buf[i]
def ioctl(self, op, arg):
if op == 4: # get number of blocks
return len(self.data) // self.block_size
if op == 5: # get block size
return self.block_size
定义了一个 RAMBlockDev 类,提供三个基本方法,分别是readblocks,writeblocks,ioctl
只要实现了这三个方法就可以通过uos.VsFat格式化以及挂载块设备。下面测试一下官方的例子:
import os
bdev = RAMBlockDev(512, 50)
os.VfsFat.mkfs(bdev)
os.mount(bdev, '/ramdisk')
这样在Thonny中查看文件:
执行完上面的代码之后在Thonny中刷新一下可以看到多出了一个ramdisk目录,说明挂载成功了。
挂载成功后,接下来试着读写文件
f = open("/ramdisk/test.txt","w")
f.write("hello")
f.close()
可以看见确实写入了。
官方给的示例是基于内部RAM的块设备,掉电之后文件就会消失,为了解决这个问题我们需要使用非易失性储存设备,比如flash芯片。
2.移植W25QXX芯片驱动
W25QXX芯片是经典的SPI接口flash储存器,本次使用的芯片是W25Q32,有4M字节的空间可用。在网上可以找到很多的C语言驱动程序,现在将其移植到MicroPython平台
完整代码如下
文件名:w25qxx.py
# 移植自: 正点原子W25QXX驱动代码
from machine import SPI, Pin
from micropython import const
import gc
import time
__TYPE = dict(
W25Q80 = 0XEF13,
W25Q16 = 0XEF14,
W25Q32 = 0XEF15,
W25Q64 = 0XEF16,
W25Q128 = 0XEF17,
)
MEM_SIZE = {
0XEF13 : 1000000,
0XEF14 : 2000000,
0XEF15 : 4000000,
0XEF16 : 8000000,
0XEF17 : 16000000,
}
# 指令表
W25X_WriteEnable = const(0x06)
W25X_WriteDisable = const(0x04)
W25X_ReadStatusReg = const(0x05)
W25X_WriteStatusReg = const(0x01)
W25X_ReadData = const(0x03)
W25X_FastReadData = const(0x0B)
W25X_FastReadDual = const(0x3B)
W25X_PageProgram = const(0x02)
W25X_BlockErase = const(0xD8)
W25X_SectorErase = const(0x20)
W25X_ChipErase = const(0xC7)
W25X_PowerDown = const(0xB9)
W25X_ReleasePowerDown = const(0xAB)
W25X_DeviceID = const(0xAB)
W25X_ManufactDeviceID = const(0x90)
W25X_JedecDeviceID = const(0x9F)
class __W25QXX:
def __init__(self, SPI, CS_PIN):
self.__SPI = SPI
self.__CS_PIN = CS_PIN
self.read(0,1)
self.TYPE = self.__readID()
# self.TYPE = 0XEF15
self.__MEM = MEM_SIZE[self.TYPE]
if self.TYPE in list(__TYPE.values()):
print("W25QXX type is " + list(__TYPE.keys())[list(__TYPE.values()).index(self.TYPE)])
else: print("unknown device")
# 写一个字节
def __writeByte(self, b):
self.__SPI.write(bytearray([b]))
# 读取状态寄存器
def __readSR(self):
self.__CS_PIN.off()
self.__SPI.write(bytearray([W25X_ReadStatusReg]))
ret = list(self.__SPI.read(1))
self.__CS_PIN.on()
return ret[0]
# 写状态寄存器
def __writeSR(self, sr):
self.__CS_PIN.off()
self.__SPI.write(bytearray([W25X_WriteStatusReg]))
self.__SPI.write(bytearray([(sr)]))
self.__CS_PIN.on()
# W25QXX写使能
# 将WEL置位
def __writeEnable(self):
self.__CS_PIN.off()
self.__SPI.write(bytearray([W25X_WriteEnable]))
self.__CS_PIN.on()
# W25QXX写禁止
# 将WEL清零
def __writeDisable(self):
self.__CS_PIN.off()
self.__SPI.write(bytearray([W25X_WriteDisable]))
self.__CS_PIN.on()
# 读取芯片ID
def __readID(self):
self.__CS_PIN.off()
self.__SPI.write(bytearray([0x90,0,0,0]))
tmp = list(self.__SPI.read(2))
return (tmp[0]<<8)+ tmp[1]
# 读取SPI FLASH
# 在指定地址开始读取指定长度的数据
# pBuffer:数据存储区
# ReadAddr:开始读取的地址(24bit)
# NumByteToRead:要读取的字节数(最大65535)
def read(self, addr, num):
self.__CS_PIN.off()
self.__SPI.write(bytearray([W25X_ReadData, (addr>>16)&0xff, (addr>>8)&0xff,addr&0xff]))
ret = self.__SPI.read(num)
self.__CS_PIN.on()
return ret
# SPI在一页(0~65535)内写入少于256个字节的数据
# 在指定地址开始写入最大256字节的数据
# pBuffer:数据存储区
# WriteAddr:开始写入的地址(24bit)
# NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
def __writePage(self, buff, addr, num):
self.__writeEnable()
self.__CS_PIN.off()
self.__SPI.write(bytearray([W25X_PageProgram, (addr>>16)&0xff, (addr>>8)&0xff, addr&0xff]))
self.__SPI.write(buff[:num])
self.__CS_PIN.on()
self.__waitBusy()
# 无检验写SPI FLASH
# 必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
# 具有自动换页功能
# 在指定地址开始写入指定长度的数据,但是要确保地址不越界!
# pBuffer:数据存储区
# WriteAddr:开始写入的地址(24bit)
# NumByteToWrite:要写入的字节数(最大65535)
# CHECK OK
# 等待空闲
def __writeNoCheck(self, buff, addr, num):
pageremain = 256- addr%256
if(num <= pageremain): pageremain = num
while True:
self.__writePage(buff, addr, pageremain)
if(num == pageremain): break
else:
buff = buff[pageremain:]
gc.collect()
addr += pageremain
num -= pageremain
if num > 256: pageremain = 256
else: pageremain = num
# 写SPI FLASH
# 在指定地址开始写入指定长度的数据
# 该函数带擦除操作!
# pBuffer:数据存储区
# WriteAddr:开始写入的地址(24bit)
# NumByteToWrite:要写入的字节数(最大65535)
def write(self, buff, addr, num):
buff_tmp = None
secpos = addr // 4096
secoff = addr % 4096
secremain = 4096 - secoff
if num <= secremain: secremain = num
while True:
# print("...")
buff_tmp = bytearray(self.read(secpos * 4096, 4096))
if sum(map(lambda x : x^0xff, buff_tmp[secoff:])): # 需要擦除
# if True: # 需要擦除
self.__eraseSector(secpos)
for i in range(0, secremain):
buff_tmp[i + secoff] = buff[i]
self.__writeNoCheck(buff_tmp, secpos * 4096, 4096)
else: self.__writeNoCheck(buff, secpos * 4096, 4096)
if num == secremain: break # 写入结束
else:
secpos += 1
secoff = 0
buff = buff[secremain:]
addr += secremain
if num > 4096: secremain = 4096
else: secremain = num
gc.collect()
# 擦除整个芯片
# 等待时间超长...
def eraseChip(self):
self.__writeEnable()
self.__waitBusy()
self.__CS_PIN.off()
self.__SPI.write(bytearray([W25X_ChipErase]))
self.__CS_PIN.on()
self.__waitBusy()
# 擦除一个扇区
# Dst_Addr:扇区地址 根据实际容量设置
# 擦除一个扇区的最少时间:150ms
def __eraseSector(self, addr):
# print("erase sector : {0:}".format(addr))
addr *= 4096
self.__writeEnable()
self.__waitBusy()
self.__CS_PIN.off()
self.__SPI.write(bytearray([W25X_SectorErase, (addr>>16)&0xff, (addr>>8)&0xff, addr&0xff]))
self.__CS_PIN.on()
self.__waitBusy()
# 等待空闲
def __waitBusy(self):
while True:
if self.__readSR()&0x01 == 0x01:
time.sleep_ms(5)
else: break
# block device 接口
class W25QXX_BlockDev(__W25QXX):
def __init__(self, SPI, CS_PIN):
self.block_size = 4096
super().__init__(SPI, CS_PIN)
super().read(0,1)
# self.data = bytearray(block_size * num_blocks)
def readblocks(self, block_num, buf):
buf_tmp = self.read(block_num * self.block_size, len(buf))
for i in range(len(buf)):
buf[i] = buf_tmp[i]
def writeblocks(self, block_num, buff):
self.write(buff, block_num * self.block_size, len(buff))
def ioctl(self, op, arg):
if op == 4: # get number of blocks
return self.__MEM // self.block_size
if op == 5: # get block size
return self.block_size
将该文件上传到开发板中,然后使用如下代码挂载:
from machine import Pin, SPI
import w25qxx
import uos
spi = SPI(baudrate=10000000, polarity=1, phase=1, sck=Pin(21), mosi=Pin(4), miso=Pin(22))
cs = Pin(23, Pin.OUT)
flash = w25qxx.W25QXX_BlockDev(SPI = spi, CS_PIN = cs)
uos.VfsFat.mkfs(flash)
uos.mount(flash, '/flash')
!!!(注意这里的SCK连接21引脚,MOSI连接4引脚,MISO连接22引脚,CS片选连接23引脚,实际使用时请根据自己的硬件连接情况修改上述代码)
然后就可以愉快的使用外部flash芯片放置文件了^o^y
总结
将flash芯片的驱动移植之后就可以很方便的在其他支持MicroPython的开发板上使用了,在没有SD卡插槽的情况下还是很管用的
2021-01-26