Configure Emacs and lsp for ESP32 development

ESP32 uses either the RISC-V or Xtensa chips. I have the ESP32-DevKitC-VE which uses the Xtensa toolchain, so I will use that as the example here.

Setting up clangd

Install clangd from your package manager of choice, and you should be able to find it in $PATH. lsp-mode can also download and install a copy under $HOME/.emacs.d/cache/ but I generally prefer the system package manager.

clangd requires a compiler driver with appropriate flags. Instead of specifying all headers and flags manually, it can use a compiler command database which can be generated with cmake. To do this with idf.py, simply add CMAKE_EXPORT_COMPILE_COMMANDS=1 to your environment variables:

$ CMAKE_EXPORT_COMPILE_COMMANDS=1 idf.py build

compile_commands.json is expected to be in any of the parent directories of the file you're editing. I put it in my project root directory as a symbolic link, as the one generated by cmake above is put under build/. In project root, run:

$ ln -s build/compile_commands.json compile_commands.json

As Xtensa uses gcc where some options may differ from clang, I have added a .clangd file under project root which is checked into vcs with the appropriate options.

CompileFlags:
  Remove: [-mlongcalls, -fstrict-volatile-bitfields, -fno-tree-switch-conversion]

The list of options was taken from my first run of lsp where flycheck was complaining of unknown options.

Setting up lsp-mode

To setup Emacs to use lsp-mode and its repertoire of helpers including company-mode and flycheck-mode, we need to install lsp-mode. I use use-package here.

To have lsp-mode use the system clangd with appropriate configurations, I have added lsp-clients-clangd-executable and lsp-clients-clangd-args.

Since I have setup ESP32 toolchain outside of default path, clangd will not use it as a query driver to find system headers. Without the --query-driver argument, clangd will first find the appropriate compiler from the compile command database, but will refuse it as it's not in default path. This is a security measure to prevent running arbitrary executables and the user is expected to "whitelist" the compiler driver to be used by clangd. The driver can be any compiler compatible with gcc or clangd. In this case, Xtensa uses the gcc compiler.

Here's the Emacs lisp snippet to set up both lsp-mode and lsp-ui (optional).

 (use-package lsp-mode
  :commands (lsp lsp-deferred)
  :init
  ; to get lsp-mode going with xtensa
  (setq lsp-clients-clangd-executable "clangd")
  (setq lsp-clients-clangd-args '("--query-driver=/**/bin/xtensa-esp32-elf-*" "--background-index" "--header-insertion=iwyu" "-j=4" ))
  :hook
  (c-mode . lsp)
  (lsp-mode . lsp-enable-which-key-integration))

(use-package lsp-ui
  :commands lsp-ui-mode) 

Setup projectile for running

You can also use projectile's lifecycle commands to build and run the project with idf.py using a per-directory variable file. To do this, add a file named .dir-locals.el to project root with the following contents.

((nil . ((projectile-project-compilation-cmd . "idf.py build")
         (projectile-project-run-cmd . "idf.py flash monitor"))))

And that's pretty much it. To debug any issues that may arise, add "--log=verbose" to lsp-client-clangd-args and have a look at clangd buffers in Emacs.

This setup can theoretically be adapted for use with any cross-compiler toolchain using either clang or gcc as its compiler.