How-To's Tips & Tricks

使用 CLion 进行 Arduino 开发:从业余爱好到专业项目 – 第二部分

Read this post in other languages:

在本系列的第一篇博文中,我们使用 Arduino 开发板构建了一个简单的原型项目。 第二阶段包括构造一个功能齐全的设备,该设备可以通过气压图显示当前的温度和压强,并且可以使用一块电池运行几个星期。 源代码可以在 GitHub 上找到,读者可以按照这篇博文中的内容逐步操作。

硬件

我们需要一个实时时钟 (RTC),一个用于屏幕的串行外设接口 (SPI),一个用于天气传感器的 I2C 接口,以及至少 16KB 的 RAM 和 64KB 的 ROM,所有这些都最好具有尽可能低的功耗。 我将使用基于 STM32L432 MCU 的 ST Nucleo-L432KC 开发板,因为我手头正好有一块,但符合要求的开发板还有很多,并且该项目可以围绕任何 STM32L4 系列微控制器 (MCU) 进行构建。

以下是开发板及其 MCU 中包含的内容:

  • 一个 72 MHz 的 ARM Cortex-M4 内核。
  • 64 KB RAM。
  • 256 KB 闪存。
  • 硬件 SPI 和 I2C 接口。
  • 用来监测电池电量的模数转换器 (ADC)。
  • 一个可由用户控制的 LED。
  • 一个附带 32.768 kHz 石英振荡器的片上 RTC。
  • 板载 ST-LINK/V2 调试器/编程器,允许我们将代码刷写到 MCU 上,并直接在芯片上进行调试。
  • 一个连接到 USB-UART 桥的硬件串行端口 (UART),可用于诊断输出。
  • 外加许多我们这个项目用不到的其他模组。

我们将使用与第一部分相同的传感器和屏幕:

  • 一个 Bosch BMP085 气压传感器。 我们将使用一个 GY-65 模块,但 BMP180 (GY-68) 也可以完成这项工作。
  • 一块 Waveshare 4.2 英寸电子纸屏幕。

传感器和屏幕均可由 5V (Arduino) 或 3.3V (STM32) 的电压供电。

可以使用一节单芯锂聚合物电池或锂离子电池为该设备供电。 我将使用来自动作相机的 1000 mAh 电池。 任何电池都可以,例如,旧手机电池或 18650 电池。 如果使用 18650 电池,请确保电池带有内置保护电路。 如果短路,无保护的锂电池可能会引发火灾或爆炸。
我们使用最简单的充电器,一个基于 TC4056A 芯片的模块。 它们在 eBay 以及大多数电子产品和爱好者商店都很容易买到。 我用的是下面这个:

开发板

它自带充放电保护电路,非常实用。

Nucleo 开发板改造

由于我的目标是一个电池供电的应用,我需要断开所有额外消耗电池电量的模组。 这包括电源 LED 和 ST-LINK 接口。 根据文档,如果移除焊桥 SB9 和 SB14,板载 MCU 可以通过 3V3 引脚供电。 我还将移除 SB16 和 SB18,因为它们只提供与 Arduino 的引脚到引脚的兼容性,但它们阻止了两个引脚的使用。 下面是来自文档的开发板布局和焊桥图。

开发板布局

现在,板载电源已从 MCU 断开,我们需要提供一个替代电源。 我使用的是 BMP085 模块附带的 LDO。 我将额外的一根导线焊接到传感器芯片的 3.3V 引脚上,并用热熔胶将电线固定在开发板上,以防止焊接点上的压力。

引脚

完整电路组装

完成上述改装后,我终于可以将电路组装在一起了:

电路

该设备可以通过两个 USB 端口中的任何一个来供电和充电:一个在 Nucleo 开发板上,另一个在充电器上。不过,调试和刷写只能通过 Nucleo USB 完成。 还请注意,当我们完成构建后,可以移除 IN+ <-> 5V 导线,我们不再需要为 ST-LINK 接口供电。

我将整套装置安装在了 IKEA 平板电脑支架上:

构造

固件

之后的所有阶段均与源代码相关,我在每个阶段之后都进行一次 git 提交,以便您一步一步跟踪每次更改。 值得一提的是,所有 git 操作都直接在 CLion 中完成。

Git 集成

配置外设

在此阶段,我们来编写设备的实际固件。 我使用的是 STM32CubeMX、适用于 ARM 的 GCC 工具链,以及 CLion。 首先,我们在 STM32CubeMX 中创建一个项目,选择 Nucleo-L432KC 开发板,然后配置所有外设:

外设

  • I2C1 – 标准模式,100 kHz 速度,7 位地址
  • USART2 – 115200 bps,8 位,无奇偶校验
  • ADC1 – 启用软件触发的 Vrefint 通道
  • SPI1 – 仅传输主模式,Motorola 格式,8 位最大速度
  • 用于显示屏 PA0 的额外 GPIO 作为输入;PA1、PA3 和 PA4 作为 GPIO 输出
  • RTC 必须由启用了内部唤醒的 LSE(即石英振荡器)进行时钟同步。

请注意,必须在 Project Manager(项目管理器)标签页上将工具链设置为 STM32CubeIDE

STM32CubeIDE

在完成所有这些设置之后,我选择项目文件夹,并通过点击 Generate Code(生成代码)来生成项目框架。

编写代码

首先,必须为嵌入式开发配置 IDE。 有关如何执行此操作的详细信息,请查看 CLion 的网络帮助

接下来,启动 CLion 并打开项目。 CLion 会为项目创建一个 CMake 结构,然后我们就可以编写代码了。 该项目现在可编译,但无法按预期工作。 首先,我需要编写错误报告例程,并从错误处理程序调用它们。 为此,我可以使用 CLion 提供的所有片上调试功能。 这意味着单步执行、启动和停止固件,监视变量,甚至外设寄存器和位。 在下面的动画中,关注 GPIOB ORD 寄存器的第 3 位:

在 CLion 中调试

我还使用以下函数将 stdout 重定向到 UART2 串行端口:

__unused int _write(__unused int handle, char *data, int size )
{
    HAL_UART_Transmit(&huart2, (uint8_t*)data, size, size);
    return size;
}

请注意,该项目包含许多自动生成的文件,我只能更改伪注释 / USER CODE BEGIN whatever // USER CODE END whatever / 之间的文件。 否则,在 STM32CubeMX 中重新生成项目时,我们可能会丢失所有更改。

传统上,大多数嵌入式源代码是用 C 而非 C++ 编写的。 包括 ST 在内的芯片供应商过去也用 C 语言提供启动代码和硬件访问库。如今,C++ 已经有了很多改进,提升了防错性和可读性,而我更喜欢使用 C++17。 遗憾的是,STM32CubeMX 生成的是 C 代码,而我无论如何都需要调用 C 库函数。 使用 C++ 和供应商代码库的唯一方式是创建一个或多个单独的文件(在我的示例中是 cppMain.cpp),并在其中编写大部分代码。 有一个声明为 external C void cppMain() 的函数,当执行所有自动生成的初始化时,会从全局 main() 函数中对其进行调用。 请注意,C++ 标准通过 CMakeLists.txt 中的 set(CMAKE_CXX_STANDARD 17) 子句进行设置。提交

现在,让压强传感器工作起来。 我从第一部分中使用的 Adafruit 库中借用了相同的代码,我将对其进行修改以使用 STM32 的 I2C 接口。 测量结果会被打印到标准输出,这意味着它是通过连接到我 PC 上的虚拟 COM 端口的 UART 端口发送的。 我使用 PuTTY 实用工具来监视端口并检查结果。 提交

由于设备由电池供电,固件应该密切关注电池电压,并在电池电量不足时发出警告。 我们有一个可以实现此功能的片上 ADC。 为了尽可能降低功耗,MCU 必须在所有屏幕更新任务完成后立即进入休眠模式,而 RTC 必须帮助我们唤醒设备。

单芯锂聚合物电池通常提供的电压范围是 3.0-4.2 V,我们的电源电路会将其限制在最大约 3.3 V。 没有分压装置,因此我们无法测量高于 3.3 V 的电池电压,但当电池电压降至 3.3 V 以下后,3.3 V 线路也会降至更低的电压,此时我们可以进行测量。 固件必须测量内部参考电压相对于电源电压的电压,然后使用存储在 MCU 的非易失性存储器中的预编程校准值计算实际电压。 有一个宏 __LL_ADC_CALC_VREFANALOG_VOLTAGE() 可以实现此功能。
提交

接下来的三个步骤是关于如何让显示屏正常工作的。 我已经将我在第一部分中使用的用于 4.2 英寸电子纸显示屏的 WaveShare 库移植到了 STM32 平台。 这个库是用经典 C++ 编写的,但我更喜欢较新的标准,至少从 C++17 开始。 CLion 会不断执行代码检查,并使用黄色警告标记可疑代码。 这能够显著减少拼写错误和小错误的数量。 但与此同时,现有的代码通常会引发许多警告,既有合理警告,也有误报。 我总会检查这些警告,因为其中一些会指出真正的错误或性能问题。 一些特性和关键字,例如 maybe_unusednoexceptnoreturn __unused,有助于减少误报,保持代码整洁并提高防错性。 我添加了 Doctor Jekyll NF 字体。比起库中提供的字体,我个人更喜欢这个字体。 该字体已使用此字体生成器脚本从 TrueType 格式转换为 C 文件形式的位图 。
提交

现在,我可以更进一步,输出真实数据,但目前没有图形。 此外,我还可以自己绘制符号作为位图字体。 这些符号包括 Battery low(电量低)、Battery empty(电量耗尽),以及显示气压升降的四个象形图。 我的绘画能力远远低于我的编码能力,请见谅。 我将 3.3V 导线上的 Battery low(电量低)和 Battery empty(电量耗尽)分别定义为 3.0 V 和 2.9 V。
提交

下一步是将历史压强数据绘制成图表。 我在两次测量之间使用名为“shutdown”(关机)的 MCU 低功耗模式。 在此模式中,RAM 内容会丢失,我无法在其中存储数据。 幸运的是,在 MCU 中有一个名为“备份域”的特殊块。 它是三十二个 32 位寄存器,在关机模式下与 RTC 一起继续运行,此时,其他一切都被关闭。 二十四个寄存器用于将历史数据保存为四十八个 16 位值,每小时一个值,共保存两天。 RTC 会不时唤醒 MCU 以更新历史记录和屏幕。
提交

功耗

现在,我们有了一个可以正常工作的设备。 它能够显示天气数据并绘制气压图。 我们来检查一下我的电池充一次电能用多久。 我制作了一个同样基于 STM32 MCU 的简单功率表。

功率

我将在另一篇博文中描述制作功率表的过程。 这虽然不是最精确的功率表,但应该能给出合理的功耗指示。

功率详细信息

如图所示,42 分钟内测得的平均功耗约为 25 mA。 这意味着,一块 1000 mAh 电池的续航不到两天,这肯定是不够的。 我们来进行一些优化。

更改 MCU 配置以优化功耗

我们可以将 MCU 时钟从 72 MHz 降低到 24 MHz,将 CPU 内核连接到 MSI 生成器,并关闭额外的时钟单元(HSI、HSI48、PLL 和 LSI)。 对于此应用来说,24 MHz 足够了。 这可以通过 Clock Configuration(时钟配置)标签页上的 STM32CubeMX 完成,只需点几下鼠标和编写三行代码。
屏幕更新需要几个数据数组,每个数组大小为 15 KB。 该库在轮询模式下执行此操作,就像对 8 位 AVR 所进行的操作。 所有的 STM32 MCU 都支持直接内存访问,这使得 SPI 数据传输更快,并在传输过程中让 CPU 内核进入休眠模式,从而降低功耗。 这需要在 STM32CubeMX 中点击几下,然后更改显示器库中的几行代码。

更改固件以优化功耗

MCU 会花费大量时间等待屏幕更新、传感器初始化和其他具有时间延迟的操作。 我们可以通过重写标准 HAL_Delay 函数(请参阅 main.c 文件)对此进行优化。唯一的更改是延迟循环中的 __WFI(等待中断)调用。 它会使 MCU 在定时器中断请求之间进入休眠模式。
最后一项,也是最重要的一项更改,是使用关机模式。 在此模式中,除备份域之外的大部分 MCU 都会关闭,功耗也会大幅下降。 当 MCU 从该模式唤醒后,它将重置并从头开始执行固件。 因此,我们在代码中失去了全局无限循环,因为在关机之后,执行永远不会继续。 我在代码中保留了一个空循环,告诉编译器执行不会超出 HAL_PWREx_EnterSHUTDOWNMode() 调用。 最后,我将 RTC 唤醒周期设置为每小时一次。 提交

我们来看看这对我们的功耗有何影响。

功耗优化

这是 24 小时后的数据:在两次刷新之间的关机模式下为 0.03 mA,平均功耗为 0.07 mA。 这为我们提供了超过一年的电池寿命,即使考虑到电池自放电并允许一些合理的电流测量误差。 这比我们之前的两天时间长了不少!

结论

在实施这个项目的过程中,我用一个简单的基于 Arduino 的原型构建了一个功能强大的设备。 当然,这个设备并不完美,代码还可以继续优化,还可以使用更好的硬件。 在此过程中,我使用了许多现代功能,如自动代码生成、版本控制系统、片上调试和 CLion 的智能编辑器。 当然,没有这些现代功能该项目也能完成,但开发过程将需要更长的时间。 现在,我要说“回头见”了。我要拿出工具箱,为我的气象站做一个外壳,并把它挂到墙上。

本博文英文原作者:

image description

Discover more