试玩微控制器开发:ESP32 初体验

以前在项目中有过几次需要涉及物联网的东东,但当时对微控制器开发这块完全不了解,以至于做方案时信心不足。
最新终于闲下来了,想试试这方面的开发,于是买了两块乐鑫 ESP32-C3 开发板玩

microcontroller

连接设备

  • 运行 ls /dev/cu.* 查看电脑上当前连接的串口设备。
  • 先按乐鑫官方文档安装好驱动,然后将开发板(我的是 ESP32-C3)连接至电脑。
  • 再次运行 ls /dev/cu.* ,多出来的那个设备就是开发板了。

然后就可以使用串口终端连接至开发板,可以使用 idf.py monitor 也可以使用第三方工具。 比如 MacOS 下可以使用 COMTool 这个工具连接进行连接。

初体验

安装乐鑫提供的开发框架 IDF , 注意选择支持自己设备的版本。

将 IDF 的代码拉到本地后,切换到自己设备对应的版本,运行 ./install.sh 脚本安装,然后运行 . ./export.sh 在当前终端会话中导出相关环境变量,然后就可以使用 idf.py 这个工具了。

IDF 有 VS Code 扩展,安装并配置扩展后很多操作就不用在终端里进行了。

从 IDF 仓库示例目录中拉个 hello world 示例过来。

编译代码:idf.py build
列出可编译的目标设备:idf.py --list-targets
设置编译到的目标设备: idf.py set-target esp32c3 (官方文档没提到这条,导致烧录到设备时报错,搜了好久没找到答案)
烧录到指定设备:idf.py -p /dev/cu.usbserial-210 -b 115200 flash (可选参数 -b 115200 用于指定波特率)

烧录完成后设备会自动复位设备,此时我们可使用 COMTool 或者 idf.py -p /dev/cu.usbserial-210 monitor 查看设备的输出信息。

Arduino 支持

Arduino 为非专业嵌入式开发人员提供了一个更简单的开发环境,并且极其繁荣的生态系统,有大量第三方包可用(特别是各式各样的驱动),要在 Arduino 中进行 ESP32 芯片的开发,需要先添加第三方芯片管理器: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json 然后就在切换开发板界面中找到对应的型号了。

搞个温度传感器测个温

我使用的是 DS18B20 温度传感器,商家提供的资料是不支持 ESC32-C3 的,主要是因为资料中使用的 OneWire 这个通信库不支持,幸运的是找到了 OneWireNg 这个开源替代库(支持的板子型号较多,目前提交比较活跃),并且该库与 OneWire 完全兼容。

#include <OneWireNg.h>
#include <OneWireNg_BitBang.h>
#include <OneWire.h>
#include <OneWireNg_Config.h>
#include <OneWireNg_CurrentPlatform.h>

int pin = 6;

//Temperature chip i/o
OneWire ds(pin);

void setup(void) {
  Serial.begin(115200);
}

void loop(void) {
  float temperature = getTemp();
  Serial.print("当前温度:");
  Serial.println(temperature);

  delay(1500);
}

float getTemp() {
  byte data[12];
  byte addr[8];

  if ( !ds.search(addr)) {
    //no more sensors on chain, reset search
    ds.reset_search();
    return -1000;
  }

  if ( OneWire::crc8( addr, 7) != addr[7]) {
    Serial.println("CRC is not valid!");
    return -1000;
  }

  if ( addr[0] != 0x10 && addr[0] != 0x28) {
    Serial.print("Device is not recognized");
    return -1000;
  }

  ds.reset();
  ds.select(addr);
  ds.write(0x44, 1); // start conversion, with parasite power on at the end

  byte present = ds.reset();
  ds.select(addr);
  ds.write(0xBE); // Read Scratchpad


  for (int i = 0; i < 9; i++) { // we need 9 bytes
    data[i] = ds.read();
  }

  ds.reset_search();

  byte MSB = data[1];
  byte LSB = data[0];

  float tempRead = ((MSB << 8) | LSB); //using two's compliment
  float TemperatureSum = tempRead / 16;

  return TemperatureSum;
}

开发方式对比:Arduino、IDF、TinyGo、MicroPython、NodeMcu

目前我尝试着使用 Arduino、IDF、TinyGo 分别进行了一些类似功能的开发。

  • Arduino: Arduino 无疑是最简单方便的,其繁荣的生态几乎可以在上面找到大部分驱动,遇到不懂的东西也很好查资料。
  • ESP-IDF 乐鑫官方提供的ESP-IDF 开发框架功能最为强大,对底层细节控制更多,当然这也会导致开发效率比 Arduino 明显慢很多。
  • TinyGo:从开发体验上说,我个人更喜欢 TinyGo,兼顾了开发效率与运行效率(虽然它有一个GC),但 TinyGo 目前的成熟度还不够,对 ESP32 的支持不完整,生态也比较差,要是乐鑫能加大 TinyGo 对 ESP32 的支持就好了(比如公司员工参与项目开发)。
    同时
  • MicroPython: ESP32 也支持使用 MicroPython 进行开发,但我没尝试,也许对于完全不在意性能的 Py 玩家来说还是有些吸引力的。
  • NodeMcu:乐鑫还搞了这么一套基于 Lua 的开发框架,这货看起来就像 MicroPython 一样,我感觉搞这东东的目的是不是只是为了“自主控制权”。

比如发起一个 http 请求并获取响应结果为例,对比以下 Arduino 和 IDF 两种不同实现方式:

Arduino:

#include <HTTPClient.h>

void httpGet(char* url ) {
  HTTPClient http;
  http.begin(url);
  int httpCode = http.GET();
  if (httpCode == 200) {
    Serial.print("http.GET() done,httpCode:");
    Serial.println(httpCode);
    String payload = http.getString();
    Serial.println(payload);
  } else {
    Serial.println("Error on HTTP request");
  }
  http.end();
}

IDF:

#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_crt_bundle.h"

#include "esp_http_client.h"

static const char *HTTP_TAG = "HTTP_CLIENT";

// 定义一个用于处理 http 事件的函数
esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
    static char *output_buffer; // Buffer to store response of http request from event handler
    static int output_len;      // Stores number of bytes read
    switch (evt->event_id) // 根据不同事件进行处理
    {
    case HTTP_EVENT_ERROR:
        ESP_LOGD(HTTP_TAG, "HTTP_EVENT_ERROR");
        break;
    case HTTP_EVENT_ON_CONNECTED:
        ESP_LOGD(HTTP_TAG, "HTTP_EVENT_ON_CONNECTED");
        break;
    case HTTP_EVENT_HEADER_SENT:
        ESP_LOGD(HTTP_TAG, "HTTP_EVENT_HEADER_SENT");
        break;
    case HTTP_EVENT_ON_HEADER:
        ESP_LOGD(HTTP_TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
        break;
    case HTTP_EVENT_ON_DATA:
        ESP_LOGD(HTTP_TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
        break;
    // ……
    default:
        ESP_LOGD(HTTP_TAG, "OTHER ERROR. (HTTP_EVENT_REDIRECT?)");
        esp_http_client_set_header(evt->client, "From", "user@example.com");
        esp_http_client_set_header(evt->client, "Accept", "text/html");
        break;
    }
    return ESP_OK;
}

// http_get 主函数
static int http_get(char *path, char *query, char *ret_content)
{
    esp_http_client_config_t config = {
        .host = "example.com",
        .path = path,
        .query = query,
        .event_handler = _http_event_handler,
        .user_data = ret_content,
        .disable_auto_redirect = true,
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);

    // GET
    esp_err_t err = esp_http_client_perform(client);
    if (err == ESP_OK)
    {
        printf("hhtp ok.\n");
        ESP_LOGI(HTTP_TAG, "HTTP GET Status = %d, content_length = %d",
                 esp_http_client_get_status_code(client),
                 esp_http_client_get_content_length(client));
    }
    else
    {
        printf("hhtp fail.\n");
        ESP_LOGE(HTTP_TAG, "HTTP GET request failed: %s", esp_err_to_name(err));
    }
    ESP_LOG_BUFFER_HEX(HTTP_TAG, ret_content, strlen(ret_content));

    esp_http_client_cleanup(client);
    return 0;
}