搞过嵌入式的同学们都知道,当我们开始一个新的平台后,我们首先想到的是系统层面的东西,这就体现了嵌入式开发中系统工程师的作用了,BTW,Android平台除外,Google已经伺候的太好了。
ESP32实际上相对简单,一些例如Bringup的流程就省了。下面我就把我的思考过程写下来。
一、Build系统
ESP32的Build系统比较简单,留给开发者需要修改的也比较少。
一个项目典型的目录结构是:
project
--------main
--------components
Build大概的原理是,将esp-idf的components和本项目的components拷贝到一起,代码模块收集完成后,和main下面的代码一起编译成Bin文件,其实就是静态链接到一起了。
二、分区设计
分区设计需要在项目初始阶段考虑好,否则量产或者试产后更改代价比较大,甚至可能很难升级。因为只升级APP分区可以通过OTA等方式在线升级,整个的烧写至少需要串口连接。
ESP32文档中有对分区的类型的详细描述:
https://esp-idf.readthedocs.io/en/latest/api-guides/partition-tables.html
分区类型及子类型:
类型
|
子类型 |
app
|
factory
|
|
ota_0 |
|
。。。。
|
|
ota_15 |
|
test
|
data
|
ota
|
|
phy
|
|
nvs
|
创建分区表的时候,第一个要注意ESP32的默认分区及地址,其中nvs和phy_init是必须有的,如果支持ota升级,otadata分区也是必须有的,factory分区其实就是app分区,地址是从10000开始的。
# Espressif ESP32 Partition Table
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x6000
otadata, data, ota, 0xd000, 0x2000
phy_init, data, phy, 0xf000, 0x1000
要分析清楚项目的需求,比方说有没有需要可写的配置参数分区,保存自有数据的分区,例如音频文件等,还有没有一些临时的文件分区等。
我们项目使用的一个典型分区表如下:
1 # Name, Type, SubType, Offset, Size
2 # Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
3 nvs, data, nvs, 0x9000, 0x4000
4 otadata, data, ota, 0xd000, 0x2000
5 phy_init, data, phy, 0xf000, 0x1000
6 ota_0, 0, ota_0, 0x10000, 1728K
7 ota_1, 0, ota_1, , 1728K
8 config, data, nvs, , 64K
9 audio, data, fat, , 448K
10 coredump, data, coredump,, 64K
从分区表上看,我们支持OTA升级,ota_0和ota_1是双备份分区,我们需要一个保存配置的cofig分区,还有一个保存音频文件的audio分区,最后的coredump分区是为了debug使用,当系统crash后,会把crash的栈dump到此分区。
三、Bootloader及系统启动流程
在做分区设计的时候,就需要研究Bootloader的功能,因为要做OTA,所以要保证OTA的绝对可靠,不允许某些情况下升级失败后,系统变砖的情况,
可以看到我们有ota_0和ota_1双备份分区,假设我们在使用ota_0分区的时候开始升级新版本,一个健壮的升级逻辑需要保证 :
1、升级是写到ota_1分区,如果升级出错,再启动应该进入到ota_0分区,则不会有问题;
2、升级是写到ota_1分区, 如果升级写bin文件成功,假设bin文件本身有问题,升级完成启动进入ota_1分区,启动失败,此时必须能够重新启动进入ota_0分区。
bootloader需要保证以上逻辑才能保证OTA的绝对可靠,那ESP32是否支持呢?否则我们需要自己实现以上逻辑。
那我们就看看bootloader的启动流程吧,来验证我们的想法。
bootloader启动的逻辑在esp-idf/components/bootloader/subproject/main/bootloader_start.c中
主要函数有bootloader_main(), get_selected_boot_partition(), load_boot_image() 等函数,ESP32的bootloader的启动逻辑如下:
1、get_selected_boot_partition() 函数
从
otadata
分区和分区表决策出当前的分区
Index
:
2、load_boot_image()以上一步返回的分区Index为起点,如果此分区可以Boot,则启动此分区,如果不能启动则以以下的顺序轮番尝试各个分区,直到找到一个可以启动的分区:
首先尝试返回Index的分区;
其次依次递减ota分区的index,尝试启动这些分区;
直到递减到factory分区;
然后从初始Index+1分区开始,一直到ota index最高的分区;
从上面的启动顺序来看,ESP32已经想的比较周到了,我们上面的分区设计在OTA的情况也是比较健壮的了。So, Continue!
四、OTA升级
ota的升级的API在如下的文档链接中有描述:
https://esp-idf.readthedocs.io/en/latest/api-reference/system/ota.html?highlight=ota
OTA本身的话基本上逻辑比较简单,找一个合适的时机检查版本,如果有新版本就下载写到一个新的分区。
如果需要灰度升级、版本维护那就是服务端的逻辑了,有兴趣的也可以探讨一下。我们也在服务端实现了一个非常简单的管理逻辑,灰度升级、版本状态管理、黑白名单、升级及下载统计等。
另外一个需要解决的问题是,ESP32的设备通常交互比较简单,不像手机那样可以清楚的知道正在下载,正在升级,当前版本等;如何知道每个设备使用的那个版本、升级状态如何,这个需要做一些简单的设计。
五、Debug及调试方案设计
开发阶段的调试可以通过串口或者JTAG来进行,那么试产或者量产后,设备可能没有串口了,有严重问题怎么办呢,在系统设计阶段也要稍作考虑。
其实大部分的Bug可以通过测试复现来解决。往往是一些Crash的问题都是偶现的,我们加了一个coredump分区,一些crash的问题可以通过dump这个分区来后续收集,不过这个是可选的。
六、IOT系统整体架构考虑
基于ESP32的IOT设备,肯定都是要联网交互的,一个典型的IOT设备会有一个手机移动端的APP和之匹配,那么这个交互如何设计呢?
一般来讲有两个思路:1、基于局域网或者点对点直接通讯的;2、通过云端来通讯;如果有运营和远程控制的需求,一般会选择第二个方案。
具体的协议来讲也大致有两个方向:1、采用物联网大热的MQTT协议;2、自定义协议,底层一般基于HTTP/HTTPS.