这是一块两年前的小板子,因为 ESP32 足够便宜而且有方便的 wifi 功能,而且这一款甚至带有 CAN 收发器,所以购入。在 ESP 中 CAN 接口叫做 TWAI ,官方代码库(早期也叫 CAN,现在改掉了)、资料等都找不到 CAN 相关字样,应该是没交版权费,功能上是一模一样的。
两年前使用 Arduino IDE 做了一点点简单的功能-网络桥接啥的,然后因为 Arduino IDE 或者 ESP-IDF 过于简单或者过于复杂而劝退。当然也是没有什么实际需求懒得继续研究。
之前花 200 大洋咨询别人一些问题的时候,别人就力荐我全面使用 PlatformIO ,当然我完全没当回事。现在重新把玩这个玩具,发现 PlatformIO 环境虽然不符合我的开发美学,但确实有其独到之处,足够简单足够应用。虽然各种编译过程各种环境都藏着不让人看,但当我们把所有知识类的开发经验抛开,只专注于实现某种应用,这玩意可太棒了。
下面是我这两周玩出的成果。
[ 懒得录改天再说 ]
PlatformIO 是一个集成了编译链、python环境、包管理器(他自有的)、下载算法、调试器等等所有开发所需的东西的一个平台。
因为大而全,所以基于 PlatformIO 的开发项目是相对封闭的,所有的指令或者其他操作都应该在 PlatformIO CLI 中进行,在 vscode 的蚂蚁头 PlatformIO 图标下,可以在 QUICK ACCESS->Misellaneous-> PlatformIO Core CLI 打开 PlatformIO CLI 终端,在其中打指令例如 pio --help
、which python
才是调用集成环境中的东西。
而关于在此平台下载的 Libraries,也不要想着改库,照着包的 Examples 用就好了,否则在平台更新时可能会自动将你的修改给还原导致丢失代码。必须要增改功能的包自行放在项目的 lib 文件夹中,平台会自动监测和添加编译指令。
另外都使用这样的平台了,就别想着 JTAG 单步调试了,我进行了很多尝试在这方面耗费大量的时间,但总是有这样那样的问题,另外芯片的引脚资源也不够用,完全不能支持额外的四个引脚拿来调试。
PlatformIO 首先需要有 vscode ,然后在 vscode 中左下角齿轮新建一个空的配置文件,建议初建是全空,甚至不要有主题插件。
在插件市场找到 PlatformIO IDE
下载即可。
安装完成后左侧会出现蚂蚁头,这就是整个 PlatformIO IDE
环境了。
点开蚂蚁头,可以在 QUICK ACCESS->PIO Home->Platforms 找到所有支持的板子平台。
其中比较常用的就是 Espressif32、Espressif8266,其他的芯片关键词都可以搜出来,只有这两个 esp32、esp8266 居然搜不到,当然一共也没几个,手动看一圈就好了。点开需要的平台,Install 就好了。
在某个 Platform 第一次下载时,他会提示你下载许多他依赖的小工具,不要担心,这些都会集合下载在他自有的文件夹中,不会影响我们自己的文件结构。如果下载过慢或者失败(顺利的话一分钟以内就完成了),需要挂 VPN。
点开蚂蚁头,可以在 QUICK ACCESS->PIO Home->Project & Configuration 看到此平台下的所有项目,新环境是没有的,点击 Create New Project 新建一个。
Name 随便起,Board 搜索开发板或芯片开头几个字母大部分都可以搜到,Framework 推荐直接选 arduino 就好。
没有下载的 Platform 会自动下载。
创建后大概是这样的结构
我们实际的 main.cpp 文件在 src 文件夹下,暂时不理会。
lib 文件夹用于存放自己引入非平台管理的库,假如你想要改一个库的源码,应该将这个库放在lib 文件夹中,平台会自动处理源码和头文件路径的。
打开 platformio.ini 并进行一些调整。
build_flags
是额外添加的编译标志,这里表示将 src 文件夹也加入 include path
,便于我们在 src 文件夹也可以创建头文件。
build_type
是不必介绍了吧,改为发布版可以节省一些资源供我们使用,毕竟 arduino 天生就会浪费很多资源。
首先再整理一下项目,找到 src/main.cpp 清理为如下的样子。仅保留基本的 setup 和 loop 函数,arduino 中 setup 函数会先运行一次,随后 loop 函数不断运行。
在 src 下新建两个文件 McuInit.cpp 、SysTask.cpp,并且整理为如下内容。
McuInit
用于一些应用模块的初始化配置,例如示例的串口模块,在 arduino 中实际上没有外设概念,直接操作 MCU 的外设功能可能会引起冲突;
SysTask
用于一些可能有时序相关的工作,例如每 100ms 扫描一次 wifi 这种,在 loop 函数中无法精确把握时序;
而原有的 loop
函数则用于时序无关以尽可能快的速度运行的工作,例如刷新 UI。
lib 文件夹塞进所有用到的外部库,平台会自动添加其中的源文件和头文件路径。
点开蚂蚁头,可以在 QUICK ACCESS->PIO Home->Libraries 找到平台推荐的一些库,点开想要使用的库,"Add to Project"可以加入项目,随后直接按照库的 Examples 使用就好,这个平台自带的库管理器添加的库,源码不太好寻,也不用在意。
本项目用到的库。
库本身的库管理器-
lib 文件夹引入的库
Note
由于路由器、网络环境的限制,这些 MCU 无法直接暴露自己的公网IP(或者很麻烦和无法随处手持),建议往服务器转发方向研究,比如
MQTT
服务器中转。
搭项目框架 3~4 天,研究 JTAG 调试 2~3 天,代码填充 2~3 天,这玩意弄起来还是很快的。
在 PlatformIO CLI 中可以使用这些指令进行一些操作。
检查芯片整体设置,例如 JTAG 功能是否开启。这个指令来自 espressif 工具链。
烧录固件,例如将 your.bin 烧录到 0x1000 处。这个指令来自 espressif 工具链。
检查所有已安装的库和包。这个指令来自 PlatformIO 平台。
至于其他指令,类似 pio --help
、esptool --help
这样加上 --help
都可以层层往下查。
一些额外的参考资料
ESPCoredump with Arduino Framework – Hpsaturn --- ESPCoredump with Arduino Framework – Hpsaturn
些许困难
屏幕驱动与陀螺仪驱动都需要 I2C 接口,而这些在库封装中没有提及如何兼容(当然使用的前提是芯片有两个 I2C 外设)。I2C 外设在 ESP 代码库中叫做 Wire.h,宗旨是其中一个底层需要调用 Wire
对象,而另一个调用 Wire1
。
屏幕图形库使用 u8g2,修改到 128*80 分辨率已经不易,再修改使用对象太麻烦。而 MPU6050 则提供了这样的绑定接口。在定义 MPU6050 对象时使用这样的语句 MPU6050 accelgyro(MPU6050_DEFAULT_ADDRESS, &Wire1);
其中传参1是依据构造函数的默认,传参2提供绑定的 Wire1
指针。
ESP32 通常的开发方式都不支持像通常的 C/C++ 编译器那样指定精细完备的链接器脚本,而需要使用他自己格式的一个 csv 文件作为分区表
,一个示例类似如下。其中 Offset 列可以不填,那么会自动计算紧密的偏移量。
对于 Type 列,数值 0 与 app 等效,相当于 *(.text) 程序代码段。
在 PlatformIO 平台中可以通过在 platformio.ini 增加 board_build.partitions = partitions.csv
字段进行指定。否则使用平台默认。
详情可参考官方文档(虽然也是藏着掖着没什么营养) 分区表 - ESP32 - — ESP-IDF 编程指南 v5.5 文档
Name 中的 ffat 用于支持 FFat.h 库的应用,也就是将一部分 flash 作为内置文件系统,可以存储一些 test.txt、log,txt 这种格式化的信息。
一些单文件示例,用于展示常用API
需要分区表包含 ffat name ,示例的分区表如下
name
├── .pio
├── .vscode
├── include
├── lib
└── src
└── main.cpp
└── test
└── platformio.ini
[env:pico32]
platform = espressif32
board = pico32
framework = arduino
; 增加以下两句
build_flags = -I src
build_type = release
#include <Arduino.h>
void setup( )
{
}
void loop( )
{
}
#include <Arduino.h>
extern void McuInit(void);
extern void SysTask(void *parameter);
//
void setup( )
{
McuInit( );
xTaskCreate(SysTask, /*任务函数*/
"SysTask", /*带任务名称的字符串*/
8192, /*堆栈大小,单位为字节*/
NULL, /*作为任务输入传递的参数*/
1, /*任务的优先级*/
NULL); /*任务句柄*/
}
void loop( )
{
}
espefuse -p COM11 summary
esptool -p COM11 write_flash 0x1000 your_firmware.bin
pio pkg list
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
# bootloader.bin,, 0x1000, 32K
# partition table, 0x8000, 4K
nvs, data, nvs, 0x9000, 20K,
otadata, data, ota, 0xe000, 8K,
ota_0, 0, ota_0, 0x10000, 1408K,
ota_1, 0, ota_1, , 1408K,
uf2, app, factory, , 256K,
ffat, data, fat, , 924K,
coredump, data, coredump, ,36k,
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 20K
otadata, data, ota, 0xE000, 8K
ota_0, app, ota_0, 0x10000, 1024K
ota_1, app, ota_1, 0x110000,1024K
ffat, data, fat, 0x210000,1624K
coredump, data, coredump,0x3A8000, 64K
#include "FFat.h"
void listDir(fs::FS &fs, const char *dirname, uint8_t levels)
{
Serial.printf("Listing directory: %s\r\n", dirname);
File root = fs.open(dirname);
if(!root)
{
Serial.println("- failed to open directory");
return;
}
if(!root.isDirectory( ))
{
Serial.println(" - not a directory");
return;
}
File file = root.openNextFile( );
while(file)
{
if(file.isDirectory( ))
{
Serial.print(" DIR : ");
Serial.println(file.name( ));
if(levels)
{
listDir(fs, file.path( ), levels - 1);
}
}
else
{
Serial.print(" FILE: ");
Serial.print(file.name( ));
Serial.print("\tSIZE: ");
Serial.println(file.size( ));
}
file = root.openNextFile( );
}
}
void setup( )
{
Serial.begin(115200);
// 格式化FFat分区
if(!FFat.begin(true))
{ // true参数表示格式化
Serial.println("An Error has occurred while mounting FFat");
return;
}
// 创建和写入文件
File file = FFat.open("/test.txt", FILE_WRITE);
if(!file)
{
Serial.println("Failed to open file for writing");
return;
}
file.println("Hello, ESP32 with FFat!");
file.close( );
// 读取文件
file = FFat.open("/test.txt");
if(!file)
{
Serial.println("Failed to open file for reading");
return;
}
while(file.available( ))
{
Serial.write(file.read( ));
}
file.close( );
// 调试串口打印出现在的文件结构
listDir(FFat, "/", 0);
}
void loop( )
{
// 不需要在loop中做任何事情
}