今天咱们要聊的内容非常简单,所以先扯点别的。上一篇水文中,老周没能将 TinyUSB 的源码编译进 Arduino 中,心有两百万个不甘,于是清明节的时候再试了一次,居然成功了,已经在 esp32 开发板上验证过,可行!官方的文档中说只在 Ubuntu、树莓派和 mac 中编译通过,win 平台未测。老周已测试,在 Windows 11 上成功编译。
下面说说如何把 TinyUSB 编译到 Arduino 框架中。
1、创建空的 idf 项目。
2、在 VS Code 中选择好芯片型号,如 esp32 s3。使用内置 USB JTag 模式。
3、到 GitHub - espressif/esp32-arduino-lib-builder 下载 esp32-arduino-lib-builder 到你喜欢的目录下,备用。
4、到 GitHub - hathach/tinyusb: An open source cross-platform USB stack for embedded system 下载 tinyUSB,放到你喜欢的目录下备用。
5、回到 VS Code,在 main 目录下,如果 main.c 不是C++文件,将其重命名为 main.cpp。
6、打开 main 目录下的 CMakeLists.txt 文件,把源文件名也改为 main.cpp。
idf_component_register(SRCS "main.cpp"
INCLUDE_DIRS ".")
idf 插件在你重命名文件时会自动修改 CMakeLists.txt 文件中的源文件,但不敢保证每次都能自动识别,所以还是手动检查一下。
7、点击底部状态栏中"设置乐鑫设备目标"按钮,选好开发板,如 esp32c6、esp32p4 等(要根据你用的板子来选),烧录方式优先使用 USB Jtag。
8、设备目标设置成功后,点击状态栏上的"SDK 编辑器"按钮,打开设置页,找到 FreeRTOS 相关配置项。打 tick 频率改为 1000 Hz,最后保存并关闭页面。这个你可以看看老周上上篇水文,Arduino 框架在配置的时候会验证这个频率,如低于 1k Hz 就会报错。

9、添加对 Arduino 框架的依赖。这次为了让大家方便,老周使用 ESP Component Registry 在线安装。打开 https://components.espressif.com/,搜索"arduino-esp32",找到后在右边会给出 idf.py 命令行,直接复制它。

10、回到 VS Code,点击状态栏上的"打开 ESP-IDF 终端"按钮,然后粘贴执行刚才的命令。
11、在 ESP-IDF 终端中执行 idf.py reconfigure 命令,坐和等待,就会自动下载依赖的组件了。总共要下20多个包,约几百兆。因为不是从 github 上下载的,所以不用担心挂掉。下载的组件存放在项目目录下的 managed_components 子目录中。

12、在项目根目录下(是项目的根目录,不是 main 目录)创建一个目录,名为 components。
13、打开前面下载的 esp32-arduino-lib-builder 代码包,一般是个压缩包。把压缩包中 components/arduino_tinyusb 整个目录复制到项目下的 components 目录下。

14、找到前面下载的 tinyusb 源码包,把 tinyusb 整个目录复制到项目中的 components/arduino_tinyusb 目录下。

如果你不确定复制时有没有放对位置,可以打开 arduino_tinyusb 组件下的 CMakeLists.txt 文件,核对一下源代码文件的路径。
set(srcs
# espressif:
"${COMPONENT_DIR}/src/dcd_dwc2.c"
# tusb:
#"${COMPONENT_DIR}/tinyusb/src/portable/synopsys/dwc2/dcd_dwc2.c"
"${COMPONENT_DIR}/tinyusb/src/portable/synopsys/dwc2/dwc2_common.c"
"${COMPONENT_DIR}/tinyusb/src/class/cdc/cdc_device.c"
......
"${COMPONENT_DIR}/tinyusb/src/device/usbd.c"
"${COMPONENT_DIR}/tinyusb/src/tusb.c")
15、注意组件管理器自动下载的 arduino 框架的目录名(也是组件名)不叫 arduino-esp32,而是叫 espressif__arduino-esp32,即"<发布者>__<组件名>",中间有两个下画线。因此,咱们要修改项目目录 /components/arduino_tinyusb 下面的 CMakeLists.txt 文件。找到设置 priv_requires 变量的语句。
set(priv_requires arduino main)
把其中的 arduino 改为 espressif__arduino-esp32,保存并关闭文件。
set(priv_requires espressif__arduino-esp32 main)
可以看到,这个地方出现了反向依赖,即 main + arduino 需要 arduino_tinyusb,而 arduino_tinyusb 又依赖 arduino 和 main。为了避免循环依赖而出错,你不需要改动项目 main 和 arduino 组件的依赖设置。毕竟是官方写好的,咱们还是得相信他们,不乱改的话,不会报错。
16、再次打开SDK配置编辑器页面,如无意外,此时能看到 Arduino 和 TinyUSB 相关的配置了。确保 Enable TinyUSB driver 被选上(默认是选中的)。

17、假设要模拟 USB 键盘,需要向下滚动到 Human Interface driver 选项,并选中 Enable USB Human Interface (HID) TinyUSB driver。还可以在 HID Device String 处为设备写上自定义描述文本。

顺便开启 Autostart Arduino setup and loop on boot 选项,这样更简洁。

点击保存并关闭配置页面。
所有工作准备就绪,现在咱们回到 main.cpp 文件,用以下代码做测试。
#include <stdio.h>
#include "Arduino.h"
#include "USB.h"
#include "USBHIDKeyboard.h"
// 实例化键盘对象
USBHIDKeyboard keyboard;
// 初始化设置
void setup()
{
// 记得初始化USB相关的功能,否则 HID 不能用
USB.begin();
// 初始化键盘
keyboard.begin();
}
// 循环函数
void loop()
{
// LEFT_GUI 就是左边的 Win 键
keyboard.write(KEY_LEFT_GUI);
// 每隔3秒发送一次按键
delay(3000);
}
这时候 USBHIDKeyboard 类就能用了。这个都封装好的,用起来 666 的。记得初始化时,USB.begin 方法也要调用,它可以初始化 USB 相关的基础类型。调用 write 方法即可发送按键。这里的 GUI 键其实就是 Win 键。上述代码的功能就是每三秒发一次Windows按键。
尝试编译,并烧录进板子。有的板子烧录后要手动按一下复位键。然后你看看"开始"菜单弹不弹,如果弹出来,说明模拟键盘起作用了。
以上均是F话,下面开始本文主题。Arduino 有着强大的生态和开源库,要是把这些库都型到 idf 项目中,那开发效率是指数级提高的。不仅保证了开发效率,也兼顾了运行效率(毕竟代码都是C++写的,性能不用担心),这才是咱们高端码农的追求。
只要在 Arduino IDE 中能用的库都可以移植到 idf 中,而且移植起来不难,99.999% 的情况下你不需要改源代码。只要咱们明白这两大框架的区别就好办:
1、Arduino 处理库代码时是扫描 src 目录(包括头文件,代码文件)来组织代码(并且是递归扫描)。即,一个库,你只要把头文件和源码文件都放进 src 目录,Arduino IDE 就会扫描到,并自动添加到编译列表中。如果你的库中有 examples 目录,可以把示例都放里面,示例就会显示在 Arduino IDE 的示例菜单中。
2、idf 是用 CMake 来组织代码的,而且以组件为单位。每个组件目录下必须放一个 CMakeLists.txt 文件,并注册组件的代码。
综合两大框架的区别,咱们只要把 Arduino 库放到 idf 搜索组件的路径上,并用 CMake 文件注册为组件,就可以在 idf 项目中使用了。idf 项目默认从以下路径查找组件:
1、优先找项目根目录下的 components 目录;
2、如果找不到要找的,看看你有没有设置 EXTRA_COMPONENT_DIRS 变量,如果有,就往该变量所列出的路径中找;
3、如果还找不到,再到项目中 managed_components 目录中找。这里的组件会由组件管理器自动下载。当然,如果组件管理器被禁用,那就不会到这里来找了;
4、到 idf 框架的 components 目录下查找。
说人话就是:只要你移植的 Arduino 库放到以上任意路径下都能被找到。如果你移植的库只在这个项目中用一次,那就放在项目下的 components 目录吧。如果你觉得这些库会经常用(别的项目也用),那可以放到其他路径下,然后通过 EXTRA_COMPONENT_DIRS 变量配置。一般推荐上述两个位置,managed_components 和 idf框架内的 components 最好不动,免得搞出别的故障。
光说理论还是太抽象,下面老周做一个实际演示。这次老周用的是 M5Stack 出的一款新玩具------M5StamPLC,对,它就是一个 PLC,含 8 路数字信号输入 + 4 路继电器。最大的遗憾是这厮不支持模拟信号输入。其主控是 ESP32 S3,在 P4 产量之前,估计很多新开发板都会用 S3,毕竟这个型号比较强大。当然你得注意,P4 是没有无线功能的,主打多媒体应用和低功耗、多引脚。
M5 官方给的是 Arduino 库(其实也有 idf 出厂示例),严重封装好的,开盒就用。所以咱们把它移植到 idf 项目中爽一下。
首先,咱们需要下载以下三个压缩包:
1、M5StamPLC:https://github.com/m5stack/M5StamPLC/archive/refs/tags/1.0.0.zip;
2、M5Unified:https://github.com/m5stack/M5Unified/archive/refs/tags/0.2.5.zip;
3、M5GFX:https://github.com/m5stack/M5GFX/archive/refs/tags/0.2.6.zip。
M5Unified 和 M5GFX 在 ESP Component Registry 上也有,可以用 idf.py add-dependency 下载。只是那里还没更新到最新版本,所以,老周选用 github 上的版本。把三个压缩包分别解压,放到项目的 components 目录下。然后可以将目录精简一下,文档和示例可以删除。



M5StamPLC 组件设有 CMake 文件,咱们要手动加上。在 M5StamPLC 目录下新建 CMakeLists.txt 文件。
# 代码文件
set(srcs
src/M5StamPLC.cpp
src/utils/aw9523/aw9523.cpp
src/utils/ina226/ina226.cpp
src/utils/lm75b/lm75b.cpp
src/utils/modbus_params/modbus_params.c
src/utils/pi4ioe5v6408/pi4ioe5v6408.cpp
src/utils/rx8130/rx8130.cpp
)
# 头文件目录
set(inc_dirs
src
)
# 注册组件
idf_component_register(
SRCS "${srcs}"
INCLUDE_DIRS "${inc_dirs}"
REQUIRES M5Unified espressif__arduino-esp32
)
M5StamPLC 依赖 M5Unified,而 M5Unified 依赖 MCM5GFX,同时,M5StamPLC 依赖 arduino 框架。因此,组件依赖列表中只需要指定 M5Unified 和 espressif__arduino-esp32 即可。保存 CMakeLists.txt 文件后,在项目根目录执行一次 idf.py reconfigure 命令可验证配置是否正确。
顺便说一下 M5 这几个库的功能:
1、M5GFX:图形绘制基础库,包括各种显示屏的驱动芯片的封装。从 LovyanGFX 库扩展而来。据说,LovyanGFX 源自 tft_espi 库。
2、M5Unified:一个综合库,支持 M5Stack 自家各种产品,初始化后可直接调用。
3、M5StamPLC:专为该 PLC 产品封装的库,可直接访问如 ina226(测量电压/电流)等内置芯片。
下面咱们简单试一下,以下代码会轮流打开和关闭 4 路继电器。
#include <stdio.h>
#include "Arduino.h"
#include "M5StamPLC.h"
// 初始化设置
void setup()
{
// 初始化各个外设
M5StamPLC.begin();
}
static uint8_t ch;
// 循环函数
void loop()
{
// 先打开各个继电器
for(ch = 0; ch < 4; ch++)
{
M5StamPLC.writePlcRelay(ch, true);
// 停一会儿
delay(900);
}
delay(3000);
// 再关闭各个继电器
for(ch = 0; ch < 4; ch++)
{
M5StamPLC.writePlcRelay(ch, false);
delay(900);
}
delay(8000);
}
如果你想一次性关闭或打开所有继电器,也可以调用 writePlcAllRelay 方法。
==============================================================================================
接下来又是老周随便聊时间。今天咱们聊聊,学习 ESP32 物联网开发买哪些开发板靠谱。
如果你确实不太愿意花钱去买,可以买那些几十块钱的,就是最常见的那些。反正能用就行。如果你想做些可以实际使用的东西,则老周觉得有两类可以选:
一、直接走生产力路线,即买工控级别的板子。
二、比较有名的几个品牌,如 M5Stack、LILYGO、微雪。哦,对,不能少了乐鑫官方的。
目前国内做得最好的就这三个了(不包括乐鑫自己),除此之外,亚博也可以,还有一个叫幻尔的也行。不过,由于幻尔给老周留下的印象不太好,对它有一点点偏见。曾经因为他们的技术支持瞎扯,买前问他们说好某板子支持XXX的,结果到手后发现根本不支持;去找他们算帐,他们说搞错型号了。无理由退货可以,但要先给他们五星好评。这弄得老周有点想变成迪迦去抽他两顿。
当然,众所周知,那三家的产品价格都偏高。所以,你得自己做决定,觉得可玩性高的可以买,觉得是电子废物的就不必买。
微雪电子的东西应该主要还是供国内的,比其他两个稍便宜些。LILYGO 是最贵的,M5 次之。这两货主要的市场还是国外,这使得其定价是按美元来算的,所以就显得比较贵。M5 的产品在日本很火,LILYGO 好像在老美表现不错。一开始老周还以为 M5Stack 是日本公司,确实他们很多文档,甚至书籍都是日文的。就连 github 上的许多代码注释也用日语。至于微雪,老周不清楚有没有出口产品。
可能有大伙伴会提到合宙。实话说,合宙的板子老周没选上,一开始除了便宜还没发现有别的特点,现在他们都涨价了。这几家里面,老周比较喜欢 M5 的,集成性好,小巧精致,尽管品控不是十分完美(中奖了可以退换货)。LILYGO 的外壳总感觉有些廉价,而且边沿毛剌多,没有经过打磨。乐鑫官方的成品也是按美元算的,所以不用问,也是偏贵的。不过呢,乐鑫的板子做工确实比那些便宜货好,人家用的外设、芯片也不错。比如,USB 转串口,人家就不会用 CH34XX 这种不稳定又慢的芯片。正点原子应该学学人家,原子哥老喜欢用 CH34XX,太掉档次了,毕竟原子的东西也是价格偏高的。
老周一直想不明白,为什么国内做得精致的开发板,主力市场都在国外,难道国内不重视嵌入式开发?你别说,国内还真TM不重视,不仅不重视嵌入式,也不重视物联网。提到了物联网,想起一次和同事聊天时,他们说嵌入式和物联网到底是不是两个东西?老周是 coder 派,不是学院派,过度装逼的理论不会说,老周个人理解,物联网的基础是嵌入式和通信技术,但思维上不是传统的嵌入式,而是应用式思维。就像做应用软件、生产力工具那样的思维,而不是局限在时序、协议、IO层面。
老周一开始是做 ERP 和 Web 的,即做软件出道的。后来为了能多接点私活干,就自己啃硬件方面的。说真的,很多实际项目里,你只会软件开发的话真的不好做,许多场景都要涉及硬件。简单的如串口收发、什么 485 转无线透传的;复杂点的要操作机械臂什么的,老周接触过两类机械臂控制,一类是 modbus,另一类是直接写寄存器。原理好像也差不多,直接写寄存器的麻烦些,主要是老周的汇编学得很烂。
传统的嵌入式开发思维确实有缺点的,很多生产设备的开发者就是能用就行,其他不管,有不少还是单向通信的。你只管给设备发命令,但设备不回复你,车间里就算设备爆炸了你也不知道。物联网则是把应用软件的开发思维融入到嵌入式中,这一点老周是很支持的。这也是老周一直不喜欢 Stm32 的开发模式的原因。只接触过 51,没写过 Linux C 语言的人都会觉得 esp idf 的 API 很怪。老周反而觉得 idf 的 API 好看呢,可能是老周玩过树莓派,而且软硬件都搞的原因。玩树莓派学 Linux 确实很不错,比你为学 Linux 而学更有效。
有大伙伴会说,大草莓没有国内的各种山寨派便宜。是的,不过山寨派现在也涨价了。最主要人家大草莓的资源好,生态好。山寨派这两年可能在文档上有了进步,早期的话是很无语的。某个更离谱的,找客服要文档还要开会员(发生在 2023 年,某开发板)。这使我对山寨派的印象很不好。真纳闷,国内为什么那么喜欢开会员?真是被某些大厂毒害得太深。