Ghi log là một phần rất quan trọng trong việc lập trình. Trong giai đoạn phát triển, việc ghi log giúp chúng ta rất nhiều trong việc truy vết để debug.
Ở bài viết này, chúng ta sẽ cùng trao đổi về một số cách thức có thể sử dụng để ghi log khi phát triển firmware cho ESP32. Hi vọng sau bài biết này, bạn sẽ không chỉ còn biết đến mỗi Serial.println()
thần thánh nữa.
Ghi log ra đâu?
Mặc dù chuyên ngành đào tạo của mình là Kỹ sư nhúng nhưng mình lại có duyên với Web development hơn. Do đó, mình tiếp cận đến với IoT với background của một web developer hơn là từ một kỹ sư nhúng. Nếu bạn đã từng lập trình ứng dụng web, mobile hoặc desktop sau đó nhảy sang lập trình nhúng như mình, thì chắc hẳn bạn sẽ làm quen với rất nhiều điều mới mẻ, trong đó có Serial.println()
đầu tiên.
Khi phát triển các ứng dụng web, mình có rất nhiều sự lựa chọn để ghi log: ở máy local thì thường sử dụng console, ở môi trường production thì thường ghi file, syslog, aggregator APIs,...
Tuy nhiên, khi đến với việc phát triển firmware, gần như lựa chọn duy nhất mà mình có là: serial. Một số cách như: ghi file vào SPIFFS, ghi ra JTAG,... thì có vẻ hơi cao cấp với mình. Bạn có thể tìm hiểu thêm các cách nâng cao mình đề cập nếu muốn.
OK. Vậy giờ chúng ta nói đến việc ghi log thẳng ra Serial
Có thể bạn sẽ quen với đoạn code kiểu kiểu như sau đây:
void setup() {
Serial.begin(115200); // Setup serial object and define transmission speed
Serial.println("Starting setup().");
Serial.printf("Hello, %s!\n", "Paul");
Serial.printf_P(PSTR("Time: %d\n"), millis()); // using PROGMEM
Serial.println(F("Setup completed")); // using PROGMEM
}
Khi ứng dụng của bạn ngày càng phức tạp, số lượng Serial.print
bạn sử dụng trực tiếp sẽ càng ngày càng nhiều.
Vấn đề với cách tiếp cận này là bạn chỉ có thể có một level ghi log duy nhất mà thôi: ghi tất cả log ra hoặc không ghi ra gì cả. Trừ phi bạn đưa một và #ifdef
vào, viết lại thành kiểu có thể tái sử dụng như mình đã từng viết, kiểu như sau:
// Uncomment logging level that you need
// #define MODULE_LOGGING_HELPER_DEBUG_FLAG 1
// #define MODULE_LOGGING_HELPER_WARNING_FLAG 1
template <typename T>
void log_debug(T message)
{
#ifdef MODULE_LOGGING_HELPER_DEBUG_FLAG
Serial.println(message);
#endif
}
template <typename T>
void log_warning(T message)
{
#ifdef MODULE_LOGGING_HELPER_WARNING_FLAG
Serial.println(message);
#endif
}
Tuy nhiên, vấn đề lớn hơn với cách tiếp cận này là, tất cả đoạn code debug / logging của bạn sẽ đi thẳng vào file binary khi build production firmware. Điều này khiến cho performance giảm, bộ nhớ cần sử dụng tăng lên, mà với các phần cứng IoT, bộ nhớ là rất quý giá. Bạn có thể cải thiện chút đỉnh bằng cách sử dụng PROGMEM nhưng vấn đề là, chỉ chút đỉnh thôi.
Vậy giờ làm sao?
Có thể bạn sẽ tự hỏi giống mình, logging - một phần quan trọng và quen thuộc như vậy ở trong việc phát triển phần mềm, không lẽ không được ESP-IDF lưu tâm tới hay sao?
Ở phần này, mình sẽ trình bày 2 giải pháp:
- Tính năng logging cơ bản được Espressif ESP-IDF cung cấp
- Tính năng logging mà ESP32 Arduino Core build dựa trên cái mà ESP-IDF cung cấp
Tính năng logging cơ bản được Espressif ESP-IDF cung cấp
Tin mừng là ESP-IDF đã thêm một thư viện logging vào trong system level API. Thư viện này support 5 mức độ log level như sau:
- ESP_LOGE – error (lowest)
- ESP_LOGW – warning
- ESP_LOGI – info
- ESP_LOGD – debug
- ESP_LOGV – verbose (highest)
Các level này có thể được set ở bước biên dịch (compile time) sử dụng config CONFIG_LOG_DEFAULT_LEVEL
. Thêm vào đó, level này còn có thể được thay đổi khi chương trình đang chạy (runtime) sử dụng hàm [esp_log_level_set()](docs.espressif.com/projects/esp-idf/en/lat…)
. Để gom nhóm các log lại cho việc theo dõi được thuận tiện (theo class, component hoặc feature), mỗi macro bên trên cần phải được cung cấp một tag ở parameter đầu tiên. Ví dụ:
static const char* TAG = "MyModule";
ESP_LOGW(TAG, "Baud rate error %.1f%%. Requested: %d baud, actual: %d baud", error * 100, baud_req, baud_real);
Tính năng logging mà ESP32 Arduino Core cung cấp
ESP32 Arduino Core build các hàm logging dựa trên thư viện mà ESP-IDF cung cấp (esp32-hal-log.h). Đây là lí do mà developer đưa ra cho câu hỏi: Tại sao mà họ cần cải thiện solution được đưa ra bởi Espressif:
I’m not particularly fond of IDF’s logging facility, because it’s controlled both by compile flags and also allow some runtime changes (but not all). That in effect means that most debug statements are actually compiled, regardless if you want to see them or not and at the same time switching serials is also not possible (all debug goes to UART0 or whatever the build flag is).
Tôi không đặc biệt thích cách ghi log của IDF, vì nó được kiểm soát cả bằng các flag khi biên dịch cũng như cho phép thay đổi khi chương trình đang chạy. Điều đó có nghĩa là hầu hết các câu lệnh debug thực sự được biên dịch vào firmware, bất kể bạn có muốn xem log hay không, thêm vào đó việc chuyển đổi giữa các Serial cũng không thể thực hiện được (tất cả các câu lệnh debug được chuyển đến UART0 bất kể debug flag là gì).
ESP32 Arduino cũng cung cấp 5 log levels nhưng các macros này ở dạng chữ thường:
- log_e – error (lowest)
- log_w – warning
- log_i – info
- log_d – debug
- log_v – verbose (highest)
Bạn có thể tự suy luận được 5 macros này tương ứng với 5 macros được cung cấp bởi ESP-IDF như thế nào rồi đúng không?
Có một lưu ý ở đây là, macros của ESP32 Arduino không bắt bạn phải thêm tag vào parameter đầu tiên như ESP-IDF. Do đó, bạn phải tự thêm bằng tay các tag bạn muốn vào mỗi message. Điều này dẫn đến một vấn đề: thiếu sự nhất quán và thống nhất, có thể có sai sót. (Mà bất cứ thứ gì mà bạn phải handle bằng cơm thì đều có thể dính phải các vấn đề ấy: tính nhất quán, và dễ có sai sót). Ví dụ, bạn có thể quên bỏ các tag vào message, dẫn đến khó theo dõi và gom nhóm sau này,...
Với ESP32 Arduino, log level được set ở bước biên dịch (compile time) thông qua cờ CORE_DEBUG_LEVEL=5
hoặc build_flags = -DCORE_DEBUG_LEVEL=5
nếu bạn sử dụng PlatformIO.
Kết luận
Hi vọng sau bài viết này, bạn sẽ không chỉ biết mỗi Serial.println()
để logging trên ESP32 nữa. Nếu bạn đang sử dụng trực tiếp ESP-IDF, bạn nên sử dụng các macros ESP_LOGx
. Nếu bạn sử dụng ESP32 Arduino, bạn có thể sử dụng các macros log_x
cho đơn giản.
Hẹn gặp lại bạn ở các bài viết tiếp theo.