代码管在XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
最近在操作一个ESP32C3的开发板 才知道什么叫做憨牛充栋 还有什么叫做捉襟见肘
ESP32C3大抵不是为了Micropython设计的 或许使用IDF 它的内存足够一般应用了
但是如果先塞进去一个Micropython 情况就不一样了 一个原本具备数百k大小内存的MCU 到最后 我必须1k 1k地抠字节 才能勉强启动基本支撑框架 包括蓝牙 无线 OLED显示 网页服务等等 我曾经提了个bug给micropython 只申请2个20k内存 然后激活蓝牙 系统就崩溃了
再加上Micropython本身又内存碎片的问题 动态申请不同大小的内存不可避免 现在不崩溃 以后也得崩溃 除非留出足够的内存余量 这就得在螺蛳壳里做道场了
于是我遇到了一个问题 就是汉字字库 光是用于二分搜索法的汉字索引 就有20k大小 在IDF中 基本不用考虑 嵌入固件中就是 但是这里 就是一个不能接受的内存大户
因为Micropython需要将用到的外部资源 都加载到RAM中 我寻找网上的解决方案 目前没有一个可用的方法 能够直接将资源放在flash中使用 除非自己编译固件 如果是那样 我还不如直接用IDF 毕竟它更容易从github递归克隆 而且 编译Micropython也需要递归克隆 我感觉就跟偷运两只大象一样困难
使用文件来访问是一种方法 但是对于随机读取数个字 作为块设备的文件 其实不合适的 也不是不能 但是效率可能会令人难以接受
但是花了一天时间 总算找到了将资源放到flash 直接使用的方法
首先 先得从flash上划出地盘来 这就需要在载入Micropython 没有运行前就进行 就是修改分区表 划分出单独一个分区 这个需要在首次运行前进行的原因 是因为Micropython会在首次运行时寻找第一个数据区进行格式化
原本的分区表 给数据划分了2M空间 我从中拿出1M用来存放静态资源 就变成
data, nvs, 0x9000, 0x6000, nvs,
data, phy, 0xf000, 0x1000, phy_init,
app, factory, 0x10000, 0x1f0000, factory,
data, fat, 0x200000, 0x100000, vfs,
data, undefined, 0x300000, 0x100000, bin,
网上的分区表工具太繁琐 我自己写了一个 将以上CSV文件转换成可用的可载入的二进制码
import sys
from hashlib import md5
from struct import pack
if len(sys.argv)<3:
print (f'''\
Create binary parition table from .csv file.
Usage:
python {sys.argv[0]} <.csv filename> <.bin filename>
''')
exit(1)
types={
'app':0,
'data':1
}
subtypes={
0:{
'factory':0,
'test':0x20,
},
1:{
'ota':0,
'phy':1,
'nvs':2,
'coredump':3,
'nvs_keys':4,
'efuse':5,
'undefined':6,
'esphttpd':0x80,
'fat':0x81,
'spiffs':0x82,
},
}
flags={
'encrypted':0,
}
bin=bytearray()
with open(sys.argv[1], 'r') as f:
while line:=f.readline():
line=line.strip('\r\n \t')
if not line or line.startswith('#'):
continue
csv=line.strip('\r\n \t').split(',')
bin.extend(b'\xAA\x50')
bin.extend(pack('B', type:=types[csv[0].strip(' \t').lower()]))
bin.extend(pack('B', subtypes[type][csv[1].strip(' \t').lower()]))
bin.extend(pack('<L', eval(csv[2].strip(' \t'))))
bin.extend(pack('<L', eval(csv[3].strip(' \t'))))
bin.extend(pack('16s', csv[4].strip(' \t').encode('utf-8')))
flag=0
if len(csv)>5:
for t in csv[5].split(':'):
if t:=t.strip(' \t'):
flag=flag+(1<<flags[t])
bin.extend(pack('<L', flag))
bin.extend(b'\xEB\xEB'+b'\0'*14+md5(bin).digest())
bin.extend(b'\xFF'*(0xc00-len(bin)))
with open(sys.argv[2], 'wb') as f:
f.write(bin)
然后用"XXXXXXXXXX -p <端口> write_flash 0x8000 <生成的二进制码分区表文件>" 上载即可
下面就是关键了 怎么样像使用内存一样直接访问flash呢 Micropython有machine.mem32等访问内存的方法 但是对ESP32是不能用的 因为ESP32的内存管理有点复杂 它的内存映射和STM32不一样 不是固定的 MCU上的内存地址要转换成FLASH上的地址 有一个叫内存管理单元(MMU)的东西进行管理 我观察了Micropython的大部分代码 都完全没有涉及这块 显然全由bootloader根据app分区运行需要 给它进行内存映射 就这 还不是线性的
解决方法就是 将尚未映射的物理地址 全部都映射上去 幸好ESP的内存映射不像计算机的CPU那么复杂 否则我就可以直接放弃了 还幸好MMU映射表 可以用machine.mem32访问 下面这个模块就是使用非线性映射方式 可以直接访问flash的每个32 16 8位数 ... 嗯 Micropython应用涉及到内存管理核心 我估计也找不到其他的了
需要注意 这个代码只适合ESP32C3 对于其他ESP32 参数需要调整 目前我还没试其他的
# module flash.py
import sys
import machine
from array import array
from struct import unpack
if 'esp32c3' in sys.implementation._machine.lower():
import esp
import esp32
page=4096
size=esp.flash_size()
from esp import flash_read as readinto
from esp import flash_write as write
from esp import flash_erase as erase
# make all flash MMU mapped, for ESP32C3, there are 128 entries, each enter means 64k,
# and virtual address is from 0x3c000000, MMU table is from 0x600C5000
_mmu=array('L', (0,)*128)
free=array('L')
for i in range(128):
t=machine.mem32[0x600C5000+(i<<2)]
if t&0x100:
free.append(i)
elif not t&~0x7F:
_mmu[t]=0x3c000000+(i<<16)
t=0
for i in range(size>>16):
if not _mmu[i]:
if t>=len(free):
raise MemoryError('No available MMU entry')
machine.mem32[0x600C5000+(free[t]<<2)]=i
_mmu[i]=0x3c000000+(free[t]<<16)
t=t+1
def vaddr(faddr):
return _mmu[faddr>>16]+(faddr&0xffff)
# find a partition which has a label 'bin'
if t:=esp32.Partition.find(type=esp32.Partition.TYPE_DATA, label='bin'):
t=t[0].info()
bin_addr=t[2]
bin_size=t[3]
else:
bin_addr=0
bin_size=0
del t, i, free
else:
raise NotImplementedError('Not a supported MCU')
def mem32(pos):
return machine.mem32[vaddr(pos)]
def mem16(pos):
return machine.mem16[vaddr(pos)]
def mem8(pos):
return machine.mem8[vaddr(pos)]
class Bin:
def __init__(self, addr, size):
self.addr=addr
self.size=size
def mem32(self, pos):
return machine.mem32[vaddr(pos+self.addr)]
def mem16(self, pos):
return machine.mem16[vaddr(pos+self.addr)]
def mem8(self, pos):
return machine.mem8[vaddr(pos+self.addr)]
def readinto(self, offset, barray):
readinto(offset+self.addr, barray)
# encoded label must be no longer than 8 bytes
def findsub(self, label):
idx=0
b=bytearray(16)
label=label.encode('utf-8')
while idx+16<=self.size:
readinto(self.addr+idx, b)
idx=idx+16
addr, size, name=unpack('<LL8s', b)
if addr==0xFFFFFFFF or addr==0:
break
if name.strip(b' \0\xFF')==label:
return Bin(self.addr+addr, size)
return None
bin=Bin(bin_addr, bin_size) if bin_size else None
这里我还做了一个Bin类 将分区中的数据 模仿出一个可以分目录的类文件系统 但是名字只有8字节 而且可以随机访问任何32 16 8位数 另外 将现有资源文件打包的脚本也写了
import sys
from struct import pack
if len(sys.argv)<2:
print (f'''\
Pack files into a binaray data for using raw ESP32 partition by my flash.py module.
This script will use the file name as data label, which must not be longer than 8 bytes.
Usage:
python {sys.argv[0]} <packed filename> [files to be packed] ...
''')
exit(1)
addr=16*(len(sys.argv)-1)
bin=bytearray(b'\xFF'*addr)
pos=0
for fname in sys.argv[2:]:
with open(fname, 'rb') as f:
b=f.read()
bin.extend(b)
t=len(b)
bin[pos:pos+16]=pack('<LL8s', addr, t, fname.encode('utf-8'))
pos=pos+16
addr=addr+t
# align to 16 bytes
t=(16-(addr%16))%16
bin.extend(b'\xFF'*t)
addr=addr+t
with open(sys.argv[1], 'wb') as f:
f.write(bin)
使用的步骤是 先将最分枝的文件打包 然后层层往上汇聚打包 (其实我根本就不分第二层了 太累) 然后使用"XXXXXXXXXX -p <端口号> 0x300000 <打成最终的包>"来上载到开始地址为3M的第二数据分区
然后使用之前提供的Bin类就可以直接访问上面的字节了 一个代码例子如
class FontBin(Font):
def __init__(self, b):
self.cnt=b.mem32(4)
super().__init__(b.mem32(8), b.mem32(12))
self.bin=b
...
_font_eng5X8=FontBin(flash.bin.findsub('eng5X8'))
这大概是目前唯一在Micropython中直接访问flash字节的方法 虽然Micropython本身有flash读取块的方法 但是远不如做好内存映射 然后直接读取内存地址来得高效