Micropython@ESP32 将静态资源直接放在flash使用的方法
m24h2024/04/16软件综合 IP:上海
Abstract
Mircropython
Esp32
Flash

代码管在XXXXXXXXXXXXXXXXXX/m24h/esp32flash_mpy

最近在操作一个ESP32C3的开发板 才知道什么叫做憨牛充栋 还有什么叫做捉襟见肘

ESP32C3大抵不是为了Micropython设计的 或许使用IDF 它的内存足够一般应用了

t.png

但是如果先塞进去一个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读取块的方法 但是远不如做好内存映射 然后直接读取内存地址来得高效

[修改于 7个月7天前 - 2024/04/18 09:26:18]

+3  科创币    warmonkey    2024/04/16 真能折腾啊,效果不错
来自:计算机科学 / 软件综合
9
6
已屏蔽 原因:{{ notice.reason }}已屏蔽
{{notice.noticeContent}}
~~空空如也
m24h 作者
7个月7天前 IP:上海
931348
引用rb-sama发表于2楼的内容
一位ESP32的Arduino和IDF用户路过我想说的是SPIFFS用mkspiffs工具很容易把一...

访问block设备的文件 和随机访问大数据是两回事. IDF和Micropython又是两回事.

你考虑一个有个文件 比RAM还大 没有编在固件里 没有给编译器RO段信息 即使在IDF里面 运行时才想去随机访问它的某个32位数 也是不可能的 除了用文件打开 不停seek

但是做了内存映射 就可以直接使用指针 地址之类 直接访问了 效率不一样

引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
m24h作者
7个月3天前 IP:上海
931460
引用rb-sama发表于4楼的内容
对于挂载在外部flash的文件,其实从底层角度上看,是通过SPI或者QSPI的形式对数据进行读取的也...

你这样岂不是更复杂 而且操作flash的SPI 可能会和ROM里面程序 甚至和MCU的功能进行冲突 造成不可预见的麻烦

Micropython的访问 仅仅是调用IDF提供的接口 而指针地址访问 全在虚拟地址上

虽然我想说许多 但是最终还是说 你想简单了 不然实现看看 给个代码或者伪码 或者哪个IDF或者Arduino库函数或者功能 能让你直接访问Flash


引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
m24h作者
7个月3天前 修改于 7个月3天前 IP:上海
931470
引用rb-sama发表于6楼的内容
无意冒犯,不要上火,技术讨论,还是回到技术上把🤣任何编程工具都有其存在的价值,Micropytho...

你压根就没有理解 我做的东西 具体实现了什么能力

首先 无论是esp_flash_xx()还是esp_partition_xx micropython都早搬过去了 如果够用 我又何必操作MMU 这些都是块操作 而不是直接地址访问 

pgmspace.h的功能 与micropython的machine.memXX() 一样 都是访问虚地址 而不是真实地址 对于AVR或者STM32是可用的 对于esp32是不行的 你连esp32的flash 在什么指针地址都不知道 更不可能知道它是不连续的 是分段操作的

你既然在做调取esp32主flash数据块什么的 你不妨考虑一下 你能否单独读取flash的某个字节  高效地 就好像用指针一样访问 而不是为了某个字节  就调用一次什么函数读取一大块 (或者最多把读取字节数设为1 但是这样就算高效吗) 如果你发现你做不到 你才明白要学什么

正因为我知道你不知道 我又怎么可能上火  或者觉得冒犯呢 看了你的东西 我只是感到白期待了 毕竟我也想知道 有什么我不知道的库函数能够不需要自己操作MMU表 但遗憾的是 看来乐鑫并未提供出来

如果你想深入点esp32  其实这个文章的知识点是很少有人提起 更应该是没有具体其他实现案例的 连说明文档都很难找到的 你根本就不会在官方公布的东西里找到细节 包括MMU表的地址等等 (我也是翻IDF源代码才找到一些痕迹) 可惜了 明珠暗投

深入 请深入 不要停留在肤浅的随便一观上


引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
m24h作者
7个月2天前 修改于 7个月2天前 IP:上海
931484
引用rb-sama发表于8楼的内容
一大早起来,看见您又把几个非常有侵略性的词汇赫然放在在您的回复中“你压根就没理解”“你连xx都不知道...

没办法 我说话直 而且敏感点在知识上 不在戏剧用语上 也不在外交上 所以 懒得多说 你爱咋地咋地


引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论

想参与大家的讨论?现在就 登录 或者 注册

所属专业
上级专业
同级专业
m24h
进士 学者 机友
文章
52
回复
893
学术分
1
2020/01/22注册,10时32分前活动

个人开源项目: XXXXXXXXXXXXXX

主体类型:个人
所属领域:无
认证方式:手机号
IP归属地:上海
文件下载
加载中...
{{errorInfo}}
{{downloadWarning}}
你在 {{downloadTime}} 下载过当前文件。
文件名称:{{resource.defaultFile.name}}
下载次数:{{resource.hits}}
上传用户:{{uploader.username}}
所需积分:{{costScores}},{{holdScores}}下载当前附件免费{{description}}
积分不足,去充值
文件已丢失

当前账号的附件下载数量限制如下:
时段 个数
{{f.startingTime}}点 - {{f.endTime}}点 {{f.fileCount}}
视频暂不能访问,请登录试试
仅供内部学术交流或培训使用,请先保存到本地。本内容不代表科创观点,未经原作者同意,请勿转载。
音频暂不能访问,请登录试试
支持的图片格式:jpg, jpeg, png
插入公式
评论控制
加载中...
文号:{{pid}}
投诉或举报
加载中...
{{tip}}
请选择违规类型:
{{reason.type}}

空空如也

加载中...
详情
详情
推送到专栏从专栏移除
设为匿名取消匿名
查看作者
回复
只看作者
加入收藏取消收藏
收藏
取消收藏
折叠回复
置顶取消置顶
评学术分
鼓励
设为精选取消精选
管理提醒
编辑
通过审核
评论控制
退修或删除
历史版本
违规记录
投诉或举报
加入黑名单移除黑名单
查看IP
{{format('YYYY/MM/DD HH:mm:ss', toc)}}