├── .gitignore
├── .images
├── Adafruit MCP9808 High Accuracy I2C Temperature Sensor.fzpz
├── esp32-s3-devkit-c-breadboard.fzz
├── esp32-s3-devkit-c-breadboard_bb.png
├── esp32-s3-devkit-c-hardware-connections.png
├── esp32-s3-devkit-c.png
├── mcp9808-resolution-register.png
├── mcp9808-temperature-register.png
├── screen-blink-build.png
├── screen-esp32-serial.png
├── screen-flash-esp32.png
├── screen-temperature-output.png
├── zephyr-config-file.jpg
├── zephyr-dts-generated-macros.jpg
├── zephyr-dts.jpg
├── zephyr-kconfig-search.jpg
├── zephyr-mcp9808-module-info.jpg
└── zephyr-mcp9808-module.jpg
├── .vscode
└── settings.json
├── Dockerfile.espressif
├── README.md
├── scripts
├── espressif
│ └── west.yml
└── zephyr.code-workspace
└── workspace
├── .vscode
├── c_cpp_properties.json
└── settings.json
├── apps
└── blink
│ ├── CMakeLists.txt
│ ├── boards
│ └── esp32s3_devkitc.overlay
│ ├── prj.conf
│ └── src
│ └── main.c
└── modules
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | **/build/
2 | **/.venv/
3 | **/venv/
4 | workspace/zephyr/
--------------------------------------------------------------------------------
/.images/Adafruit MCP9808 High Accuracy I2C Temperature Sensor.fzpz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/Adafruit MCP9808 High Accuracy I2C Temperature Sensor.fzpz
--------------------------------------------------------------------------------
/.images/esp32-s3-devkit-c-breadboard.fzz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/esp32-s3-devkit-c-breadboard.fzz
--------------------------------------------------------------------------------
/.images/esp32-s3-devkit-c-breadboard_bb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/esp32-s3-devkit-c-breadboard_bb.png
--------------------------------------------------------------------------------
/.images/esp32-s3-devkit-c-hardware-connections.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/esp32-s3-devkit-c-hardware-connections.png
--------------------------------------------------------------------------------
/.images/esp32-s3-devkit-c.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/esp32-s3-devkit-c.png
--------------------------------------------------------------------------------
/.images/mcp9808-resolution-register.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/mcp9808-resolution-register.png
--------------------------------------------------------------------------------
/.images/mcp9808-temperature-register.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/mcp9808-temperature-register.png
--------------------------------------------------------------------------------
/.images/screen-blink-build.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/screen-blink-build.png
--------------------------------------------------------------------------------
/.images/screen-esp32-serial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/screen-esp32-serial.png
--------------------------------------------------------------------------------
/.images/screen-flash-esp32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/screen-flash-esp32.png
--------------------------------------------------------------------------------
/.images/screen-temperature-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/screen-temperature-output.png
--------------------------------------------------------------------------------
/.images/zephyr-config-file.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/zephyr-config-file.jpg
--------------------------------------------------------------------------------
/.images/zephyr-dts-generated-macros.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/zephyr-dts-generated-macros.jpg
--------------------------------------------------------------------------------
/.images/zephyr-dts.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/zephyr-dts.jpg
--------------------------------------------------------------------------------
/.images/zephyr-kconfig-search.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/zephyr-kconfig-search.jpg
--------------------------------------------------------------------------------
/.images/zephyr-mcp9808-module-info.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/zephyr-mcp9808-module-info.jpg
--------------------------------------------------------------------------------
/.images/zephyr-mcp9808-module.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnHymel/workshop-zephyr-device-driver/4b0cffe470b3753c17deddd3f66f28dd1bb872b3/.images/zephyr-mcp9808-module.jpg
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "i2c.h": "c",
4 | "device.h": "c",
5 | "devicetree.h": "c",
6 | "byteorder.h": "c",
7 | "errno.h": "c"
8 | }
9 | }
--------------------------------------------------------------------------------
/Dockerfile.espressif:
--------------------------------------------------------------------------------
1 | # Zephyr development image for Espressif targets (e.g. ESP32)
2 |
3 | # Settings
4 | ARG DEBIAN_VERSION=stable-20241016-slim
5 | # Zephyr 3.7.0 is not working with ESP32S3, see: https://github.com/zephyrproject-rtos/zephyr/issues/71397
6 | #ARG ZEPHYR_RTOS_VERSION=3.7.0
7 | ARG ZEPHYR_RTOS_COMMIT=26603cefaf41298c417f2eee834ed40d9ab35d3a
8 | ARG ZEPHYR_SDK_VERSION=0.16.8
9 | ARG VS_CODE_SERVER_VERSION=4.93.1
10 | ARG VS_CODE_SERVER_PORT=8800
11 | ARG VS_CODE_EXT_CPPTOOLS_VERSION=1.22.10
12 | ARG VS_CODE_EXT_CMAKETOOLS_VERSION=1.19.52
13 | ARG VS_CODE_EXT_NRF_DEVICETREE_VERSION=2024.9.26
14 | ARG TOOLCHAIN_LIST="-t xtensa-espressif_esp32_zephyr-elf -t xtensa-espressif_esp32s2_zephyr-elf -t xtensa-espressif_esp32s3_zephyr-elf"
15 | ARG WGET_ARGS="-q --show-progress --progress=bar:force:noscroll"
16 | ARG VIRTUAL_ENV=/opt/venv
17 |
18 | #-------------------------------------------------------------------------------
19 | # Base Image and Dependencies
20 |
21 | # Use Debian as the base image
22 | FROM debian:${DEBIAN_VERSION}
23 |
24 | # Redeclare arguments after FROM
25 | ARG ZEPHYR_RTOS_VERSION
26 | ARG ZEPHYR_RTOS_COMMIT
27 | ARG ZEPHYR_SDK_VERSION
28 | ARG VS_CODE_SERVER_VERSION
29 | ARG VS_CODE_SERVER_PORT
30 | ARG VS_CODE_EXT_CPPTOOLS_VERSION
31 | ARG VS_CODE_EXT_CMAKETOOLS_VERSION
32 | ARG VS_CODE_EXT_NRF_DEVICETREE_VERSION
33 | ARG TOOLCHAIN_LIST
34 | ARG WGET_ARGS
35 | ARG VIRTUAL_ENV
36 | ARG TARGETARCH
37 |
38 | # Set default shell during Docker image build to bash
39 | SHELL ["/bin/bash", "-c"]
40 |
41 | # Check if the target architecture is either x86_64 (amd64) or arm64 (aarch64)
42 | RUN if [ "$TARGETARCH" = "amd64" ] || [ "$TARGETARCH" = "arm64" ]; then \
43 | echo "Architecture $TARGETARCH is supported."; \
44 | else \
45 | echo "Unsupported architecture: $TARGETARCH"; \
46 | exit 1; \
47 | fi
48 |
49 | # Set non-interactive frontend for apt-get to skip any user confirmations
50 | ENV DEBIAN_FRONTEND=noninteractive
51 |
52 | # Install base packages
53 | RUN apt-get -y update && \
54 | apt-get install --no-install-recommends -y \
55 | ca-certificates \
56 | file \
57 | locales \
58 | git \
59 | build-essential \
60 | cmake \
61 | ninja-build gperf \
62 | device-tree-compiler \
63 | wget \
64 | curl \
65 | python3 \
66 | python3-pip \
67 | python3-venv \
68 | xz-utils \
69 | dos2unix \
70 | vim \
71 | nano \
72 | mc
73 |
74 | # Set up a Python virtual environment
75 | ENV VIRTUAL_ENV=${VIRTUAL_ENV}
76 | RUN python3 -m venv ${VIRTUAL_ENV}
77 | ENV PATH="${VIRTUAL_ENV}/bin:$PATH"
78 |
79 | # Install west
80 | RUN python3 -m pip install --no-cache-dir west
81 |
82 | # Clean up stale packages
83 | RUN apt-get clean -y && \
84 | apt-get autoremove --purge -y && \
85 | rm -rf /var/lib/apt/lists/*
86 |
87 | # Set up directories
88 | RUN mkdir -p /workspace/ && \
89 | mkdir -p /opt/toolchains
90 |
91 | #-------------------------------------------------------------------------------
92 | # Zephyr RTOS
93 |
94 | # Set Zephyr environment variables
95 | ENV ZEPHYR_RTOS_VERSION=${ZEPHYR_RTOS_VERSION}
96 |
97 | # Install Zephyr
98 | RUN cd /opt/toolchains && \
99 | git clone https://github.com/zephyrproject-rtos/zephyr.git && \
100 | cd zephyr && \
101 | git checkout ${ZEPHYR_RTOS_COMMIT} && \
102 | python3 -m pip install -r scripts/requirements-base.txt
103 |
104 | # Override the west manifest to only install necessary modules
105 | COPY scripts/espressif/west.yml /opt/toolchains/zephyr/west.yml
106 |
107 | # Instantiate west workspace and install tools
108 | RUN cd /opt/toolchains && \
109 | west init -l zephyr && \
110 | west update --narrow -o=--depth=1
111 |
112 | # Install module-specific blobs
113 | RUN cd /opt/toolchains && \
114 | west blobs fetch hal_espressif
115 |
116 | #-------------------------------------------------------------------------------
117 | # Zephyr SDK
118 |
119 | # Set environment variables
120 | ENV ZEPHYR_SDK_VERSION=${ZEPHYR_SDK_VERSION}
121 |
122 | # Install minimal Zephyr SDK
123 | RUN cd /opt/toolchains && \
124 | wget ${WGET_ARGS} https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${ZEPHYR_SDK_VERSION}/zephyr-sdk-${ZEPHYR_SDK_VERSION}_linux-${HOSTTYPE}_minimal.tar.xz && \
125 | tar xf zephyr-sdk-${ZEPHYR_SDK_VERSION}_linux-${HOSTTYPE}_minimal.tar.xz && \
126 | rm zephyr-sdk-${ZEPHYR_SDK_VERSION}_linux-${HOSTTYPE}_minimal.tar.xz
127 |
128 | # Install Zephyr SDK for the specified toolchains
129 | RUN cd /opt/toolchains/zephyr-sdk-${ZEPHYR_SDK_VERSION} && \
130 | bash setup.sh -c ${TOOLCHAIN_LIST}
131 |
132 | # Install host tools
133 | RUN cd /opt/toolchains/zephyr-sdk-${ZEPHYR_SDK_VERSION} && \
134 | wget ${WGET_ARGS} https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${ZEPHYR_SDK_VERSION}/hosttools_linux-${HOSTTYPE}.tar.xz && \
135 | tar xf hosttools_linux-${HOSTTYPE}.tar.xz && \
136 | rm hosttools_linux-${HOSTTYPE}.tar.xz && \
137 | bash zephyr-sdk-${HOSTTYPE}-hosttools-standalone-*.sh -y -d .
138 |
139 | #-------------------------------------------------------------------------------
140 | # VS Code Server
141 |
142 | # Set VS Code Server environment variables
143 | ENV VS_CODE_SERVER_VERSION=${VS_CODE_SERVER_VERSION}
144 | ENV VS_CODE_SERVER_PORT=${VS_CODE_SERVER_PORT}
145 |
146 | # Install VS Code Server
147 | RUN cd /tmp && \
148 | wget ${WGET_ARGS} https://code-server.dev/install.sh && \
149 | chmod +x install.sh && \
150 | bash install.sh --version ${VS_CODE_SERVER_VERSION}
151 |
152 | # Download CMake and C/C++ extensions (code-server extension manager does not work well)
153 | RUN cd /tmp && \
154 | if [ "$TARGETARCH" = "amd64" ]; then \
155 | wget ${WGET_ARGS} https://github.com/microsoft/vscode-cpptools/releases/download/v${VS_CODE_EXT_CPPTOOLS_VERSION}/cpptools-linux-x64.vsix -O cpptools.vsix; \
156 | elif [ "$TARGETARCH" = "arm64" ]; then \
157 | wget ${WGET_ARGS} https://github.com/microsoft/vscode-cpptools/releases/download/v${VS_CODE_EXT_CPPTOOLS_VERSION}/cpptools-linux-arm64.vsix -O cpptools.vsix; \
158 | else \
159 | echo "Unsupported architecture"; \
160 | exit 1; \
161 | fi && \
162 | wget ${WGET_ARGS} https://github.com/microsoft/vscode-cmake-tools/releases/download/v${VS_CODE_EXT_CMAKETOOLS_VERSION}/cmake-tools.vsix -O cmake-tools.vsix
163 |
164 | # Install CMake and C/C++ extensions
165 | RUN cd /tmp && \
166 | code-server --install-extension cpptools.vsix && \
167 | code-server --install-extension cmake-tools.vsix
168 |
169 | # Install the nRF DeviceTree extension (not working yet)
170 | # RUN code-server --install-extension nordic-semiconductor.nrf-devicetree@${VS_CODE_EXT_NRF_DEVICETREE_VERSION}
171 |
172 | # Clean up
173 | RUN cd /tmp && \
174 | rm install.sh && \
175 | rm cpptools.vsix && \
176 | rm cmake-tools.vsix
177 |
178 | # Copy workspace configuration
179 | COPY scripts/zephyr.code-workspace /zephyr.code-workspace
180 |
181 | #-------------------------------------------------------------------------------
182 | # Optional Settings
183 |
184 | # Initialise system locale (required by menuconfig)
185 | RUN sed -i '/^#.*en_US.UTF-8/s/^#//' /etc/locale.gen && \
186 | locale-gen en_US.UTF-8 && \
187 | update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
188 |
189 | # Use the "dark" theme for Midnight Commander
190 | ENV MC_SKIN=dark
191 |
192 | #-------------------------------------------------------------------------------
193 | # Entrypoint
194 |
195 | # Activate the Python and Zephyr environments for shell sessions
196 | RUN echo "source ${VIRTUAL_ENV}/bin/activate" >> /root/.bashrc && \
197 | echo "source /opt/toolchains/zephyr/zephyr-env.sh" >> /root/.bashrc
198 |
199 | # Entrypoint: Start VS Code Server with the Zephyr workspace
200 | CMD code-server --auth none --bind-addr 0.0.0.0:${VS_CODE_SERVER_PORT} /zephyr.code-workspace
201 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Workshop: Zephyr Device Driver
2 |
3 | Welcome to the Zephyr Device Driver workshop! By the end of this workshop, you should have a broad overview of how a device driver is written, connected to the Devicetree, and then used in a simple example application.
4 |
5 | > **Note**: This tutorial includes actions and a lot of discussion about the code. Actions will be marked with the ✅ icon. Feel free to skip the descriptions and come back to them later for in-depth explanations.
6 |
7 | Writing a device driver involves multiple tools, languages, and configuration file syntaxes. If this is your first time working with Kconfig and Devicetree, know that it will likely be overwhelming at first. This workshop will provide you with a barebones example and then point you to various resources should you wish to dive deeper into these concepts.
8 |
9 | 📺 Check out the full [Introduction to Zephyr](https://github.com/ShawnHymel/introduction-to-zephyr) video series and repository to dive deeper into these concepts!
10 |
11 | > This workshop was tested with Zephyr RTOS v3.7.0.
12 |
13 | > **Cheat mode**: if you want to skip to the solution, go [here](https://github.com/ShawnHymel/workshop-zephyr-device-driver/tree/solution). The device driver is in *workspace/modules/mcp9808/* and the example application is in *workspace/apps/read_temp/*.
14 |
15 | ## Table of Contents
16 |
17 | - [Prerequisites](#prerequisites)
18 | - [Hardware Setup](#hardware-setup)
19 | - [Toolchain Installation](#toolchain-installation)
20 | - [VS Code Server Setup (Recommended)](#vs-code-server-recommended)
21 | - [Interactive Container](#interactive-container)
22 | - [Build and Flash the Blink Demo](#build-and-flash-the-blink-demo)
23 | - [Driver Directory Structure](#driver-directory-structure)
24 | - [Driver Header](#driver-header)
25 | - [Driver Source Code](#driver-source-code)
26 | - [Compatible Driver Definition](#compatible-driver-definition)
27 | - [Logging](#logging)
28 | - [Functions](#functions)
29 | - [API Assignment](#api-assignment)
30 | - [Devicetree Expansion Macro](#devicetree-expansion-macro)
31 | - [CMake Includes](#cmake-includes)
32 | - [Kconfig Settings](#kconfig-settings)
33 | - [Bindings File](#bindings-file)
34 | - [Zephyr Module](#zephyr-module)
35 | - [Demo Application](#demo-application)
36 | - [CMakeLists.txt](#cmakeliststxt)
37 | - [prj.conf](#prjconf)
38 | - [boards/esp32s3_devkitc.overlay](#boardsesp32s3_devkitcoverlay)
39 | - [src/main.c](#srcmainc)
40 | - [Build and Flash](#build-and-flash)
41 | - [Going Further](#going-further)
42 | - [Challenge](#challenge)
43 | - [Troubleshooting](#troubleshooting)
44 | - [License](#license)
45 |
46 | ## Prerequisites
47 |
48 | You should have an understanding of the following:
49 |
50 | * C programming langauge
51 | * Embedded development (e.g. what a GPIO pin is)
52 | * I2C communication
53 |
54 | I recommend the following guides to refresh your knowledge:
55 |
56 | * [C structs](https://www.programiz.com/c-programming/c-structures)
57 | * [C pointers](https://www.programiz.com/c-programming/c-pointers)
58 | * [C macros](https://www.programiz.com/c-programming/c-preprocessor-macros)
59 | * [GPIO example with Zephyr](https://michaelangerer.dev/zephyr/2021/12/21/zephyr-basics-gpio.html)
60 | * [Overview of I2C](https://learn.sparkfun.com/tutorials/i2c/all)
61 |
62 | The following concepts are optional but will help with your understanding of the hands-on portions of the workshop:
63 |
64 | * [Docker](https://www.digikey.com/en/maker/projects/getting-started-with-docker/aa0d4c708c274ffd975f3b427e5c0ce6)
65 | * [CMake](https://www.internalpointers.com/post/modern-cmake-beginner-introduction)
66 | * [Kconfig](https://docs.kernel.org/kbuild/kconfig-language.html)
67 | * [Devicetree](https://docs.nordicsemi.com/bundle/ncs-1.9.2-dev1/page/zephyr/guides/dts/intro.html)
68 | * [YAML](https://circleci.com/blog/what-is-yaml-a-beginner-s-guide/)
69 |
70 | ## Hardware Setup
71 |
72 | You will need the following hardware components:
73 |
74 | * [ESP32-S3-DevKit-C-1](https://www.digikey.com/en/products/detail/espressif-systems/ESP32-S3-DEVKITC-1-N32R8V/15970965)
75 | * [MCP9808 Temperature Sensor Board](https://www.digikey.com/en/products/detail/adafruit-industries-llc/1782/4990781)
76 | * [LED](https://www.digikey.com/en/products/detail/w%C3%BCrth-elektronik/151051RS11000/4490012)
77 | * [220 Ω Resistor](https://www.digikey.com/en/products/detail/yageo/CFR-25JB-52-220R/1295?s=N4IgTCBcDa5gDARQEIGkC0A5AIiAugL5A)
78 | * [Jumper Wires](https://www.digikey.com/en/products/detail/adafruit-industries-llc/1957/6827090)
79 | * [Solderless Breadboard](https://www.digikey.com/en/products/detail/dfrobot/FIT0096/7597069)
80 | * [USB A to Micro B Cable](https://www.digikey.com/en/products/detail/cvilux-usa/DH-20M50055/13175849)
81 |
82 | ✅ Connect the components as follows, and connect the ESP32 dev kit to your computer.
83 |
84 | 
85 |
86 | ## Toolchain Installation
87 |
88 | ✅ To start, download this repository somewhere on your computer (using `git` or direct download + unzip).
89 |
90 | [The Zephyr Project](https://zephyrproject.org/) is not like other IDEs or toolchains: it relies a wide collection of tools with a strong focus on Linux as the host operating system. Windows and macOS are both officially supported, but installing the toolchains on them can be burdensome.
91 |
92 | To create a unified experience for this workshop, this tutorial demonstrates everything using the [pre-made VS Code Docker image](https://github.com/ShawnHymel/vscode-env-zephyr).
93 |
94 | > **Note**: In all cases, you should mount the *workspace/* directory from this repository into the container (which we do with the `-v` argument). That gives us a place to modify/save code so that you can take it with you after the workshop.
95 |
96 | > **Warning**: I recommend deleting the container after using it (hence the `--rm` argument) to keep everything clean. Any changes in the container outside of the */workspace/* directory (which we mount from the host) **will be deleted**!
97 |
98 | (Optional) If you do not want to use Docker, you are welcome to install Zephyr manually on your host operating system by [following these instructions](https://docs.zephyrproject.org/latest/develop/getting_started/index.html). Be warned: the installation locations might affect the flow of this workshop, and you will likely spend some time correcting paths. Zephyr, by default, wants you to install all the RTOS source code and SDK toolchains inside your single, large project. This is an extremely bloated way to develop one-off, smaller projects. As a result, we'll be using something similar to their [T3: Forest topology directory structure](https://docs.zephyrproject.org/latest/develop/west/workspaces.html#t3-forest-topology).
99 |
100 | > **Note**: the instructions below were verified with Python 3.12 running on the host system. If one of the *pip install* steps fails, try installing exactly Python 3.12 and running the command again with `python3.12 -m pip install ...`
101 |
102 | ✅ Before you start, install the following programs on your computer:
103 |
104 | * (Windows) [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install)
105 | * [Docker Desktop](https://www.docker.com/products/docker-desktop/)
106 | * [Python](https://www.python.org/downloads/)
107 |
108 | ✅ Open a terminal, navigate to this directory, and install the following dependencies:
109 |
110 | Linux/macOS:
111 |
112 | ```sh
113 | cd workshop-zephyr-device-driver/
114 | python -m venv venv
115 | source venv/bin/activate
116 | python -m pip install pyserial==3.5 esptool==4.8.1
117 | ```
118 |
119 | Windows:
120 |
121 | ```bat
122 | cd workshop-zephyr-device-driver/
123 | python -m venv venv
124 | venv\Scripts\activate
125 | python -m pip install pyserial==3.5 esptool==4.8.1
126 | ```
127 |
128 | ✅ Load the Docker image using one of the following two options.
129 |
130 | **Option 1**: Build the Docker image (this will take some time):
131 |
132 | ```sh
133 | docker build -t env-zephyr-espressif -f Dockerfile.espressif .
134 | ```
135 |
136 | **Option 2**: Load a pre-made Docker image
137 |
138 | If this is an in-person workshop, I should have USB flash drives with the necessary installers and pre-made images, as conference WiFi connections can sometimes be spotty. Copy the Docker image for your architecture (`*-amd64.tar` for x86_64 processors or `*-arm64.tar` for ARM64 processors like the Mac M1, M2, etc.) to your computer (e.g. *Downloads/* directory). Run the following command to load the Docker image (where `` is either `amd64` or `arm64`):
139 |
140 | ```sh
141 | cd Downloads/
142 | docker load -i env-zephyr-espressif-.tar
143 | ```
144 |
145 | The Docker image includes all of the necessary Zephyr RTOS and SDK elements, the toolchain for building ESP32 applications, and a VS Code Server instance. As a result, you have a few options for interacting with the image: VS Code Server or Interactive Container. Choose one from below:
146 |
147 | ### VS Code Server (Recommended)
148 |
149 | > **Note**: The rest of this tutorial assumes you will be using this method.
150 |
151 | ✅ Open a terminal (or command prompt), navigate to this directory and run the Docker image:
152 |
153 | Linux/macOS:
154 |
155 | ```sh
156 | docker run --rm -it -p 8800:8800 -v "$(pwd)"/workspace:/workspace -w /workspace env-zephyr-espressif
157 | ```
158 |
159 | Windows (PowerShell):
160 |
161 | ```bat
162 | docker run --rm -it -p 8800:8800 -v "${PWD}\workspace:/workspace" -w /workspace env-zephyr-espressif
163 | ```
164 |
165 | Leave that terminal window open, as it will act as our server. Open a browser on your host OS (verified working on Chrome) and navigate to [localhost:8800](http://localhost:8800/). It should connect to the container's server, and you should be presented with a VS Code instance.
166 |
167 | ### Interactive Container
168 |
169 | If you don't want to use the VS Code server, your other option is to run an interactive container and edit your *workspace/* files locally (using your favorite editor) or in the container (using e.g. vim, nano, mcedit). To do that, override the entrypoint for the image:
170 |
171 | Linux/macOS:
172 |
173 | ```sh
174 | cd workshop-zephyr-device-driver/
175 | docker run --rm -it -v "$(pwd)"/workspace:/workspace -w /workspace --entrypoint /bin/bash env-zephyr-espressif
176 | ```
177 |
178 | Windows:
179 |
180 | ```bat
181 | cd workshop-zephyr-device-driver/
182 | docker run --rm -it -v "%cd%\workspace":/workspace -w /workspace --entrypoint /bin/bash env-zephyr-espressif
183 | ```
184 |
185 | You should be presented with a root shell prompt (`#`) in your terminal. Remember that the */workspace/* directory is shared between your host and container, so any changes to files there are reflected on both systems. You can edit files locally (with e.g. your own local VS Code) or in the container (e.g. `mcedit /workspace/apps/blink/src/main.c`).
186 |
187 | ## Build and Flash the Blink Demo
188 |
189 | Before we start driver development, let's make sure we can build the basic blinky demo.
190 |
191 | > **Important!** Take note of the two directories in your VS Code instance:
192 | > * ***/workspace*** is the shared directory between your host and container.
193 | > * ***/opt/toolchains/zephyr*** is the Zephyr RTOS source code. It is for reference only and should not be modified!
194 |
195 | ✅ Open a terminal in the VS Code client and build the project. Note that I'm using the [ESP32-S3-DevKitC](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/hw-reference/esp32s3/user-guide-devkitc-1.html) as my target board. Feel free to change it to one of the [other ESP32 dev boards](https://docs.zephyrproject.org/latest/boards/index.html#vendor=espressif).
196 |
197 | In the VS Code editor in the Docker container:
198 |
199 | ```
200 | cd apps/blink
201 | west build -p always -b esp32s3_devkitc/esp32s3/procpu -- -DDTC_OVERLAY_FILE=boards/esp32s3_devkitc.overlay
202 | ```
203 |
204 | With some luck, the *blink* sample should build. Pay attention to any errors you see.
205 |
206 | 
207 |
208 | The binary files will be in *workspace/apps/blink/build/zephyr*, which you can flash using [esptool](https://docs.espressif.com/projects/esptool/en/latest/esp32/).
209 |
210 | ✅ Connect a USB cable from your computer to the **UART** port on the ESP32-S3-DevKitC. In a new terminal on your **host computer**, activate the Python virtual environment (Linux/macOS: `source venv/bin/activate`, Windows: `venv\Scripts\activate`) if not done so already.
211 |
212 | ✅ Flash the binary to your board. For some ESP32 boards, you need to put it into bootloader by holding the *BOOTSEL* button and pressing the *RESET* button (or cycling power). Change `` to the serial port for your ESP32 board (e.g. `/dev/ttyS0` for Linux, `/dev/tty.usbserial-1420` for macOS, `COM7` for Windows). You might also need to install a serial port driver, depending on the particular board.
213 |
214 | > **Important!** make sure to execute flashing and serial monitor programs from your **host OS** (not from within the Docker container)
215 |
216 |
217 | ```sh
218 | python -m esptool --port "" --chip auto --baud 921600 --before default_reset --after hard_reset write_flash -u --flash_mode keep --flash_freq 40m --flash_size detect 0x0 workspace/apps/blink/build/zephyr/zephyr.bin
219 | ```
220 |
221 | > **Important**: If you are using Linux and get a `Permission denied` or `Port doesn't exist` error when flashing, you likely need to add your user to the *dialout* group with the following command: `sudo usermod -a -G dialout $USER`. Log out and log back in (or restart). You should then be able to call the *esptool* command again to flash the firmware.
222 |
223 | 
224 |
225 | ✅ Open a serial port for debugging (change `` to the serial port for your ESP32 board):
226 |
227 | ```sh
228 | python -m serial.tools.miniterm "" 115200 --raw
229 | ```
230 |
231 | You should see the LED state printed to the console. Exit with *ctrl+]* (or *cmd+]* for macOS).
232 |
233 | 
234 |
235 | ## Driver Directory Structure
236 |
237 | Drivers in Zephyr require a very particular directory structure, as the C compiler, CMake, Kconfig, and the Devicetree Compiler (DTC) browse through folders recursively looking for their respective source files. By default, Zephyr wants you to develop drivers and board Devicetree source (DTS) files "in-tree," which means inside the Zephyr RTOS source code repository (located at */opt/toolchains/zephyr* for this workshop).
238 |
239 | For large projects, this might make sense: you fork the main Zephyr repository and make the changes you need in the actual source code. You would then version control your fork of the Zephyr source code (e.g. with [west](https://docs.zephyrproject.org/latest/develop/west/index.html)). For learning purposes, this is a pain.
240 |
241 | We are going to develop our device driver "out-of-tree," which means it will be placed in a separate folder that we later tell CMake to find when building the project. This will help keep the application and driver folders free of clutter so you can see what's going on.
242 |
243 | ✅ To start, create the following directory and file structure in the */workspace/modules/* directory. You can do this with the VS Code instance or using `mkdir` and `touch` in the terminal. Folders end with `/` and files do not. We're just creating a skeleton right now: the files can be empty.
244 |
245 | ```
246 | modules/
247 | ├── README.md
248 | └── mcp9808/
249 | ├── CMakeLists.txt
250 | ├── Kconfig
251 | ├── drivers/
252 | │ ├── CMakeLists.txt
253 | │ ├── Kconfig
254 | │ └── mcp9808/
255 | │ ├── CMakeLists.txt
256 | │ ├── Kconfig
257 | │ ├── mcp9808.c
258 | │ └── mcp9808.h
259 | ├── dts/
260 | │ └── bindings/
261 | │ └── sensor/
262 | │ └── microchip,mcp9808.yaml
263 | └── zephyr/
264 | └── module.yaml
265 | ```
266 |
267 | Some notes about the files and folder structure:
268 |
269 | * We're keeping things separate: application code goes in *apps/* and driver code goes in *modules/*.
270 | * The *CMakeLists.txt* files tell the CMake build system where to find the code for our driver.
271 | * The *Kconfig* files create an entry in the Kconfig system that allows us to enable, disable, and configure our software components (i.e. the driver code).
272 | * The *mcp9808.c* and *mcp9808.h* files hold our actual device driver code.
273 | * The *microchip,mcp9808.yaml* file is our *Devicetree bindings* file. It is the glue that helps connect Devicetree settings (in DTS syntax) to our code (in C). It uses the [YAML syntax](https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html) and can end in *\*.yaml* or *\*.yml*.
274 | * The *dts/bindings/sensor/* naming and structure matters. During the build process, Zephyr looks for bindings files (\*.yaml) recursively in *dts/bindings/* folders in its *modules*.
275 | * Speaking of modules, the *zephyr/module.yml* file formally declares this directory (*modules/mcp9808/*) as a Zephyr [module](https://docs.zephyrproject.org/latest/develop/modules.html) so it knows where to find the source, Kconfig, and bindings files. Once again, the folder and file name are important here: Zephyr looks for this particular file in this particular directory. It also uses the YAML syntax and must be named *module.yaml* or *module.yml*.
276 |
277 | > **Note**: The device driver used in this example is based on the existing *zephyr/drivers/sensor/jedec/jc42* driver, with the bindings file found in *zephyr\dts\bindings\sensor\jedec,jc-42.4-temp.yaml*. We drop interrupt/trigger support and pare it down to just the essentials to demonstrate how to create an *out-of-tree* driver module.
278 |
279 | ## Driver Header
280 |
281 | After setting up the directory structure, we're going to write our actual driver. Let's start with our header file.
282 |
283 | ✅ Copy the following to ***modules/mcp9808/drivers/mcp9808/mcp9808.h***:
284 |
285 | ```c
286 | #ifndef ZEPHYR_DRIVERS_SENSOR_MICROCHIP_MCP9808_H_
287 | #define ZEPHYR_DRIVERS_SENSOR_MICROCHIP_MCP9808_H_
288 |
289 | #include
290 |
291 | // MCP9808 registers
292 | #define MCP9808_REG_CONFIG 0x01
293 | #define MCP9808_REG_TEMP_AMB 0x05
294 | #define MCP9808_REG_RESOLUTION 0x08
295 |
296 | // Ambient temperature register information
297 | #define MCP9808_TEMP_SCALE_CEL 16
298 | #define MCP9808_TEMP_SIGN_BIT BIT(12)
299 | #define MCP9808_TEMP_ABS_MASK 0x0FFF
300 |
301 | // Sensor data
302 | struct mcp9808_data {
303 | uint16_t reg_val;
304 | };
305 |
306 | // Configuration data
307 | struct mcp9808_config {
308 | struct i2c_dt_spec i2c;
309 | uint8_t resolution;
310 | };
311 |
312 | #endif /* ZEPHYR_DRIVERS_SENSOR_MICROCHIP_MCP9808_H_ */
313 | ```
314 |
315 | There is not much here! We define some constant values and a couple of structs. The weirdest thing is the lack of public-facing functions. In most C/C++ projects, we define our API in the header file that we then `#include` in our application (or other source files). In our driver, we are going to rely on the Devicetree to define our public-facing API. We'll see how to do that in the .c file. For now, know that almost all driver functions should be private (defined as `static`) in the source (.c) file(s).
316 |
317 | We define our I2C device register addresses, as given by the [MCP9808 datasheet](https://ww1.microchip.com/downloads/en/DeviceDoc/MCP9808-0.5C-Maximum-Accuracy-Digital-Temperature-Sensor-Data-Sheet-DS20005095B.pdf). The MCP9808 has a number of extra functions that we will ignore for this workshop--namely the ability to set thresholds and toggle an interrupt trigger pin. We want to focus on the simple actions of setting the *RESOLUTION* register at boot time and then reading from the *AMBIENT TEMPERATURE* register at regular intervals.
318 |
319 | The RESOLUTION register (address 0x08) uses just 2 bits to set the resolution. We'll create an *enum* in the Devicetree that allows users to set the resolution to one of the four available values. Our driver code will read the value from the Devicetree configuration and set the desired resolution in the register during boot initialization.
320 |
321 | 
322 |
323 | The AMBIENT TEMPERATURE register (address 0x05) stores temperature data in 12-bit format with an extra bit used for the sign. We'll ignore bits 13-15, as they're used for setting thresholds. This is a 16-bit register, so we'll read from the register and convert the 12-bit temperature value (plus sign bit) to a usable value in our code.
324 |
325 | 
326 |
327 | ## Driver Source Code
328 |
329 | ✅ Copy the following code into ***modules/mcp9808/drivers/mcp9808/mcp9808.c***:
330 |
331 | ```c
332 | // Ties to the 'compatible = "microchip,mcp9808"' node in the Devicetree
333 | #define DT_DRV_COMPAT microchip_mcp9808
334 |
335 | #include
336 | #include
337 | #include
338 | #include
339 |
340 | #include "mcp9808.h"
341 |
342 | // Enable logging at a given level
343 | LOG_MODULE_REGISTER(MCP9808, CONFIG_SENSOR_LOG_LEVEL);
344 |
345 | //------------------------------------------------------------------------------
346 | // Forward declarations
347 |
348 | static int mcp9808_reg_read(const struct device *dev,
349 | uint8_t reg,
350 | uint16_t *val);
351 | static int mcp9808_reg_write_8bit(const struct device *dev,
352 | uint8_t reg,
353 | uint8_t val);
354 | static int mcp9808_init(const struct device *dev);
355 | static int mcp9808_sample_fetch(const struct device *dev,
356 | enum sensor_channel chan);
357 | static int mcp9808_channel_get(const struct device *dev,
358 | enum sensor_channel chan,
359 | struct sensor_value *val);
360 |
361 | //------------------------------------------------------------------------------
362 | // Private functions
363 |
364 | // Read from a register (at address reg) on the device
365 | static int mcp9808_reg_read(const struct device *dev,
366 | uint8_t reg,
367 | uint16_t *val)
368 | {
369 | const struct mcp9808_config *cfg = dev->config;
370 |
371 | // Write the register address first then read from the I2C bus
372 | int ret = i2c_write_read_dt(&cfg->i2c, ®, sizeof(reg), val, sizeof(*val));
373 | if (ret == 0) {
374 | *val = sys_be16_to_cpu(*val);
375 | }
376 |
377 | return ret;
378 | }
379 |
380 | // Write to a register (at address reg) on the device
381 | static int mcp9808_reg_write_8bit(const struct device *dev,
382 | uint8_t reg,
383 | uint8_t val)
384 | {
385 | const struct mcp9808_config *cfg = dev->config;
386 |
387 | // Construct 2-bute message (address, value)
388 | uint8_t buf[2] = {
389 | reg,
390 | val,
391 | };
392 |
393 | // Perform write operation
394 | return i2c_write_dt(&cfg->i2c, buf, sizeof(buf));
395 | }
396 |
397 | // Initialize the MCP9808 (performed by kernel at boot)
398 | static int mcp9808_init(const struct device *dev)
399 | {
400 | const struct mcp9808_config *cfg = dev->config;
401 | int ret = 0;
402 |
403 | // Print to console
404 | LOG_DBG("Initializing");
405 |
406 | // Check the bus is ready and there is a software handle to the device
407 | if (!device_is_ready(cfg->i2c.bus)) {
408 | LOG_ERR("Bus device is not ready");
409 | return -ENODEV;
410 | }
411 |
412 | // Set temperature resolution (make sure we can write to the device)
413 | ret = mcp9808_reg_write_8bit(dev, MCP9808_REG_RESOLUTION, cfg->resolution);
414 | LOG_DBG("Setting resolution to index %d", cfg->resolution);
415 | if (ret) {
416 | LOG_ERR("Could not set the resolution of mcp9808 module");
417 | return ret;
418 | }
419 |
420 | return ret;
421 | }
422 |
423 | //------------------------------------------------------------------------------
424 | // Public functions (API)
425 |
426 | // Read temperature value from the device and store it in the device data struct
427 | // Call this before calling mcp9808_channel_get()
428 | static int mcp9808_sample_fetch(const struct device *dev,
429 | enum sensor_channel chan)
430 | {
431 | struct mcp9808_data *data = dev->data;
432 |
433 | // Check if the channel is supported
434 | if ((chan != SENSOR_CHAN_ALL) && (chan != SENSOR_CHAN_AMBIENT_TEMP)) {
435 | LOG_ERR("Unsupported channel: %d", chan);
436 | return -ENOTSUP;
437 | }
438 |
439 | // Perform the I2C read, store the data in the device data struct
440 | return mcp9808_reg_read(dev, MCP9808_REG_TEMP_AMB, &data->reg_val);
441 | }
442 |
443 | // Get the temperature value stored in the device data struct
444 | // Make sure to call mcp9808_sample_fetch() to update the device data
445 | static int mcp9808_channel_get(const struct device *dev,
446 | enum sensor_channel chan,
447 | struct sensor_value *val)
448 | {
449 | const struct mcp9808_data *data = dev->data;
450 |
451 | // Convert the 12-bit two's complement to a signed integer value
452 | int temp = data->reg_val & MCP9808_TEMP_ABS_MASK;
453 | if (data->reg_val & MCP9808_TEMP_SIGN_BIT) {
454 | temp = -(1U + (temp ^ MCP9808_TEMP_ABS_MASK));
455 | }
456 |
457 | // Check if the channel is supported
458 | if (chan != SENSOR_CHAN_AMBIENT_TEMP) {
459 | LOG_ERR("Unsupported channel: %d", chan);
460 | return -ENOTSUP;
461 | }
462 |
463 | // Store the value as integer (val1) and millionths (val2)
464 | val->val1 = temp / MCP9808_TEMP_SCALE_CEL;
465 | temp -= val->val1 * MCP9808_TEMP_SCALE_CEL;
466 | val->val2 = (temp * 1000000) / MCP9808_TEMP_SCALE_CEL;
467 |
468 | return 0;
469 | }
470 |
471 | //------------------------------------------------------------------------------
472 | // Devicetree handling - This is the magic that connects this driver source code
473 | // to the Devicetree so that you can use it in your application!
474 |
475 | // Define the public API functions for the driver
476 | static const struct sensor_driver_api mcp9808_api_funcs = {
477 | .sample_fetch = mcp9808_sample_fetch,
478 | .channel_get = mcp9808_channel_get,
479 | };
480 |
481 | // Expansion macro to define the driver instances
482 | // If inst is set to "42" by the Devicetree compiler, this macro creates code
483 | // with the unique id of "42" for the structs, e.g. mcp9808_data_42.
484 | #define MCP9808_DEFINE(inst) \
485 | \
486 | /* Create an instance of the data struct */ \
487 | static struct mcp9808_data mcp9808_data_##inst; \
488 | \
489 | /* Create an instance of the config struct and populate with DT values */ \
490 | static const struct mcp9808_config mcp9808_config_##inst = { \
491 | .i2c = I2C_DT_SPEC_INST_GET(inst), \
492 | .resolution = DT_INST_PROP(inst, resolution), \
493 | }; \
494 | \
495 | /* Create a "device" instance from a Devicetree node identifier and */ \
496 | /* registers the init function to run during boot. */ \
497 | SENSOR_DEVICE_DT_INST_DEFINE(inst, \
498 | mcp9808_init, \
499 | NULL, \
500 | &mcp9808_data_##inst, \
501 | &mcp9808_config_##inst, \
502 | POST_KERNEL, \
503 | CONFIG_SENSOR_INIT_PRIORITY, \
504 | &mcp9808_api_funcs); \
505 |
506 | // The Devicetree build process calls this to create an instance of structs for
507 | // each device (MCP9808) defined in the Devicetree Source (DTS)
508 | DT_INST_FOREACH_STATUS_OKAY(MCP9808_DEFINE)
509 | ```
510 |
511 | There is a lot going on here, so we'll discuss the code in the following subsections.
512 |
513 | > **Note**: These discussions are quite lengthy. Feel free to skip them if you just want to get a working driver.
514 |
515 | ### Compatible Driver Definition
516 |
517 | ```c
518 | // Ties to the 'compatible = "microchip,mcp9808"' node in the Devicetree
519 | #define DT_DRV_COMPAT microchip_mcp9808
520 | ```
521 |
522 | We define the *compatible* name as `microchip_mcp9808` using the `DT_DRV_COMPAT` macro, which is a special macro provided by Zephyr. Because I2C is a bus architecture, we could have multiple MCP9808 devices attached to the same bus (each with its own bus address). To handle this (without relying on object-oriented programming e.g. C++), we must create *instances* of our driver config, data, and API functions that exist separately of each other when we compile our code. Zephyr gives us a series of macros to help us create these instances.
523 |
524 | You can create non-instance driver code, but we'll rely on [Zephyr's instance-based driver macros](https://docs.zephyrproject.org/latest/build/dts/howtos.html#option-1-create-devices-using-instance-numbers) to help us handle multiple I2C devices on the same bus. To use them, you must define `DT_DRV_COMPAT` before calling the instance expansion macros (found at the bottom of the code). During the build process, Zephyr looks for *compatible* symbols in the Devicetree source (DTS) files that match the *compatible* symbols in both the *bindings* files (which we'll explore later) and in the driver source code.
525 |
526 | Note that *compatible* is almost always given as `vender,device` as a standard style in Devicetree lingo. As C macro symbols do not work with commas, the comma is converted to an underscore `_` when the build system is looking for matching source code files. In other words, if you use `compatible = "microchip,mcp9808";` in your Devicetree source, you must use `microchip_mcp9808` to define the matching driver source code macro.
527 |
528 | ### Logging
529 |
530 | ```c
531 | // Enable logging at a given level
532 | LOG_MODULE_REGISTER(MCP9808, CONFIG_SENSOR_LOG_LEVEL);
533 | ```
534 |
535 | The `LOG_MODULE_REGISTER` macro enables logging (for debugging and error identification) at the module level (remember, this driver is considered a *module*). We can then use macros like `LOG_DBG` and `LOG_ERR` to print messages to the console:
536 |
537 | ```c
538 | LOG_ERR // Print ERROR level message to the log
539 | LOG_WRN // Print WARNING level message to the log
540 | LOG_INF // Print INFO level message to the log
541 | LOG_DBG // Print DEBUG level message to the log
542 | ```
543 |
544 | We can use the Kconfig system to set the log level (0-4) for our program:
545 |
546 | 0. None
547 | 1. Error
548 | 2. Warning
549 | 3. Info
550 | 4. Debug
551 |
552 | Note that messages at and *below* the configured log level will be printed. For example, setting the log level to 2 will print all `LOG_WRN` and `LOG_ERR` messages, but it will not print `LOG_INF` and `LOG_DBG` messages.
553 |
554 | ### Functions
555 |
556 | ```c
557 | static int mcp9808_reg_read(const struct device *dev,
558 | uint8_t reg,
559 | uint16_t *val);
560 | ...
561 | ```
562 |
563 | Next, we declare our functions. Note the use gratuitous use of `static` here; we want to keep these functions *private*, which means that they can only be seen by the compiler inside this file. To illustrate how a Zephyr driver *API* works, I divided the functions into *private* and *public* sections, but notice that they are still all declared as `static`.
564 |
565 | From there, we define our functions. The register reading and writing should be familiar if you've worked with I2C devices before. If not, I recommend reading [this introduction to I2C](https://learn.sparkfun.com/tutorials/i2c/all). In these functions, we call Zephyr's I2C API to handle the low-level functions for us.
566 |
567 | > **Note**: Zephyr's I2C API offers a great layer of abstraction! We are not bound to using a specific chip or board hardware abstraction layer (HAL). Rather, Zephyr links to the necessary chip-specific HAL at build time when we specify the board (e.g. `west build -b `). As a result, we can write truly device-agnostic driver code!
568 |
569 | To learn about the available I2C functions, you can either navigate to the I2C header file (*zephyr/include/zephyr/drivers/i2c.h*) or view the [API docs here](https://docs.zephyrproject.org/apidoc/latest/group__i2c__interface.html).
570 |
571 | The interesting part is that we are creating *instance-specific* functions without the use of object-oriented programming. To do that, the I2C functions expect a `i2c_dt_spec` struct as their first parameter, which holds the bus (e.g. `i2c0` or `i2c1`) and device address (e.g. `0x18` for the MCP9808). These are values that get populated from the Devicetree source, rather than from your application code. For example:
572 |
573 | ```c
574 | int ret = i2c_write_read_dt(&cfg->i2c, ®, sizeof(reg), val, sizeof(*val));
575 | ```
576 |
577 | Here, we perform the common pattern of writing a register address (`reg`) out to the device (at I2C address `i2c->addr`) and then reading the value from the register, which is saved to the `val` variable.
578 |
579 | A common pattern in Zephyr is to save output values in parameters and use the return value to send a *return code* back to the caller. These codes are defined by [POSIX.1-2017](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/) and can be found in [here in the Zephyr docs](https://docs.zephyrproject.org/apidoc/latest/group__system__errno.html).
580 |
581 | The `init` function will be registered to the kernel's boot process, and is (normally) called once prior to the application's `main()` being called.
582 |
583 | The *public functions* adhere to the [Zephyr Sensor API template](https://docs.zephyrproject.org/apidoc/latest/structsensor__driver__api.html), which can be found in *zephyr/include/zephyr/drivers/sensor.h*. We will assign these functions to a *sensor_driver_api* struct to enforce the use of this template. While we could define our own structs, the use of such templates provide common patterns for application developers. In other words, all Zephyr *sensors* will work in similar ways.
584 |
585 | In particular, we will define the following public API functions:
586 |
587 | * `sample_fetch` - The sensor takes a reading for the given channel (e.g. temperature, humidity, acceleration X, acceleration Y, etc.) and stores it in the `device` struct's `data` field.
588 | * `channel_get` - Get the value of the desired channel that is currently stored in the `device->data` field.
589 |
590 | ### API Assignment
591 |
592 | ```c
593 | // Define the public API functions for the driver
594 | static const struct sensor_driver_api mcp9808_api_funcs = {
595 | .sample_fetch = mcp9808_sample_fetch,
596 | .channel_get = mcp9808_channel_get,
597 | };
598 | ```
599 |
600 | After we've declared (and/or defined) our public-facing functions, we assign them to the various fields in the `sensor_driver_api` struct. In a minute, we'll use this API struct to create a *device* instance that can be called from our application code.
601 |
602 | ### Devicetree Expansion Macro
603 |
604 | ```c
605 | #define MCP9808_DEFINE(inst) \
606 | \
607 | /* Create an instance of the data struct */ \
608 | static struct mcp9808_data mcp9808_data_##inst; \
609 | \
610 | /* Create an instance of the config struct and populate with DT values */ \
611 | static const struct mcp9808_config mcp9808_config_##inst = { \
612 | .i2c = I2C_DT_SPEC_INST_GET(inst), \
613 | .resolution = DT_INST_PROP(inst, resolution), \
614 | }; \
615 | \
616 | /* Create a "device" instance from a Devicetree node identifier and */ \
617 | /* registers the init function to run during boot. */ \
618 | SENSOR_DEVICE_DT_INST_DEFINE(inst, \
619 | mcp9808_init, \
620 | NULL, \
621 | &mcp9808_data_##inst, \
622 | &mcp9808_config_##inst, \
623 | POST_KERNEL, \
624 | CONFIG_SENSOR_INIT_PRIORITY, \
625 | &mcp9808_api_funcs);
626 | ```
627 |
628 | This is the magic that connects our code to the Devicetree. So long as the `DT_DRV_COMPAT` macro has been defined prior, the Zephyr build system generates instance-based C code. It relies heavily on [macro token concatenation](https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html), also known as *token pasting*.
629 |
630 | The Zephyr build system automatically assigns *instance* numbers to Devicetree nodes when there are multiple instances of that node under a parent node (e.g. multiple I2C devices under an I2C bus node). We will see this in action later.
631 |
632 | During this build process, the preprocessor takes that instance number and generates C code, defining the data, functions, etc. for that instance. For example, if we define an MCP9808 device in the Devicetree, the build system will assign it an instance number (e.g. 42). The preprocessor will then generate the code from this macro as follows:
633 |
634 | ```c
635 | /* Create an instance of the data struct */
636 | static struct mcp9808_data mcp9808_data_42;
637 |
638 | /* Create an instance of the config struct and populate with DT values */
639 | static const struct mcp9808_config mcp9808_config_42 = {
640 | .i2c = I2C_DT_SPEC_INST_GET(42),
641 | .resolution = DT_INST_PROP(42, resolution),
642 | };
643 |
644 | /* Create a "device" instance from a Devicetree node identifier and */
645 | /* registers the init function to run during boot. */
646 | SENSOR_DEVICE_DT_INST_DEFINE(42,
647 | mcp9808_init,
648 | NULL,
649 | &mcp9808_data_42,
650 | &mcp9808_config_42,
651 | POST_KERNEL,
652 | CONFIG_SENSOR_INIT_PRIORITY,
653 | &mcp9808_api_funcs);
654 | ```
655 |
656 | This macro is fed into the `FOREACH` macro as an argument:
657 |
658 | ```c
659 | DT_INST_FOREACH_STATUS_OKAY(MCP9808_DEFINE)
660 | ```
661 |
662 | This `FOREACH` macro tells the preprocessor to generate structs/functions for each instance found in the Devicetree (as long as its `status` property is set to `"okay"`).
663 |
664 | ## CMake Includes
665 |
666 | We need to tell the build system where to find the source files. As Zephyr relies on *CMake*, that involves creating a *CMakeLists.txt* file in each of the folders leading up to the actual *.h* and *.c* files.
667 |
668 | > **Note**: Zephyr defines a number of extra macros and functions that extend/customize CMake. You can find them in the [zephyr/cmake/modules/extensions.cmake](https://github.com/zephyrproject-rtos/zephyr/blob/main/cmake/modules/extensions.cmake) file.
669 |
670 | ✅ Copy the following to ***modules/mcp9808/drivers/mcp9808/CMakeLists.txt***:
671 |
672 | ```cmake
673 | # Declares the current directory as a Zephyr library
674 | # If no name is given, the name is derived from the directory name
675 | zephyr_library()
676 |
677 | # List the source code files for the library
678 | zephyr_library_sources(mcp9808.c)
679 | ```
680 |
681 | ✅ Copy the following to ***modules/mcp9808/drivers/CMakeLists.txt***:
682 |
683 | ```cmake
684 | # Custom Zephyr function that imports the mcp9808/ subdirectory if the Kconfig
685 | # option MCP9808 is defined
686 | add_subdirectory_ifdef(CONFIG_MCP9808 mcp9808)
687 | ```
688 |
689 | ✅ Copy the following to ***modules/mcp9808/CMakeLists.txt***:
690 |
691 | ```cmake
692 | # Include the required subdirectories
693 | add_subdirectory(drivers)
694 |
695 | # Add subdirectories to the compiler's include search path (.h files)
696 | zephyr_include_directories(drivers)
697 | ```
698 |
699 | ## Kconfig Settings
700 |
701 | [Kconfig](https://docs.zephyrproject.org/latest/build/kconfig/index.html) is a system for configuring various software components in Zephyr and is based on the Kconfig system used for Linux. Most notably, it is used to enable/disable components (like our MCP9808 driver) as well as set various parameters.
702 |
703 | We will define an "MCP9808" option in Kconfig that can be enabled to include the driver source code. This kind of module system helps keep the final application binary relatively small.
704 |
705 | ✅ Copy the following to ***modules/mcp9808/drivers/mcp9808/Kconfig***:
706 |
707 | ```conf
708 | # Create a new option in menuconfig
709 | config MCP9808
710 | bool "MCP9808 Temperature Sensor"
711 | default n # Set the driver to be disabled by default
712 | depends on I2C # Make it dependent on I2C
713 | help
714 | Enable driver for the MCP9808 temperature sensor. This driver
715 | depends on the I2C subsystem being enabled.
716 | ```
717 |
718 | This creates a new option in our Kconfig system with the symbol *MCP9808*. The second line defines this as a boolean (with =y or =n as the only options). It also sets the default to `n` (disabled) and makes it depend on the I2C system being enabled.
719 |
720 | ✅ Copy the following to ***modules/mcp9808/drivers/Kconfig***:
721 |
722 | ```conf
723 | rsource "mcp9808/Kconfig"
724 | ```
725 |
726 | Like with CMake, we need to tell Kconfig where to find the relevant Kconfig files. This one says to look in the *relative* source (`rsource`) of the *mcp9808/* subdirectory for a file named *Kconfig*.
727 |
728 | ✅ Copy the following to ***modules/mcp9808/Kconfig***:
729 |
730 | ```conf
731 | rsource "drivers/Kconfig"
732 | ```
733 |
734 | This says to look in the *drivers/* subdirectory for a relevant *Kconfig* file.
735 |
736 | Together, these files help the Zephyr build system find new (or modifications to) Kconfig options relevant to our driver.
737 |
738 | ## Bindings File
739 |
740 | In order to connect the Devicetree settings to the driver source code so you can use it in your application, you need to create a *bindings* file. These files use the [YAML syntax](https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html) and create a specification for the various Devicetree properties that define the device.
741 |
742 | ✅ Copy the following to ***modules/mcp9808/dts/bindings/sensor/microchip,mcp9808.yaml***:
743 |
744 | ```yaml
745 | # Description of the device
746 | description: Microchip MCP9808 temperature sensor
747 |
748 | # Compatibility string that matches the one in the Devicetree source and
749 | # DT_DRV_COMPAT macro in the driver source code
750 | compatible: "microchip,mcp9808"
751 |
752 | # Includes common definitions from other bindings files
753 | # - sensor-device.yaml - common sensor properties
754 | # - i2c-device.yaml - common I2C communication properties
755 | include: [sensor-device.yaml, i2c-device.yaml]
756 |
757 | # Defines specific Devicetree properties of the MCP9808 sensor
758 | properties:
759 | resolution:
760 | type: int
761 | default: 3
762 | description: Sensor resolution. Default is 0.0625°C (3).
763 | enum:
764 | - 0 # 0.5°C
765 | - 1 # 0.25°C
766 | - 2 # 0.125°C
767 | - 3 # 0.0625°C
768 | ```
769 |
770 | The bindings file must define a `compatible` field. The Zephyr build system looks for this string when parsing the Devicetree source files to know what bindings (i.e. interface) to use for a given node and its associated properties. Note that the filename of the bindings file does not matter; it is this field that matters!
771 |
772 | We also include other bindings files here to enumerate common properties found in the *sensor* and *i2c-device* bindings.
773 |
774 | In this example, we define a custom *property* (`resolution`) in addition to the ones given by the *sensor-device* and *i2c-device* bindings. It's best to think about these bindings files as defining an interface for *nodes* in the Devicetree: it specifies the required properties, their types, possible defaults, and (optionally) acceptable values.
775 |
776 | Even though we specify these properties in YAML syntax, we will define Devicetree nodes in Devicetree source (DTS) syntax. You can read more about writing DTS files in [this helpful Zephyr guide](https://docs.zephyrproject.org/latest/build/dts/intro-syntax-structure.html).
777 |
778 | ## Zephyr Module
779 |
780 | We've created the required files that define the necessary device driver, at least according to what CMake, Kconfig, the Devicetree, and the compiler want. The final piece is telling Zephyr that it should treat all of these files and folders as a *module*. If we don't do this, Zephyr will fail to build and link to our driver code. Creating a Zephyr module is relatively straightforward: we just need a *zephyr/module.yaml* file in the top directory of our directory structure.
781 |
782 | ✅ Copy the following to ***modules/mcp9808/zephyr/module.yaml***:
783 |
784 | ```yaml
785 | name: mcp9808
786 | build:
787 | cmake: .
788 | kconfig: Kconfig
789 | settings:
790 | dts_root: .
791 | ```
792 |
793 | The first key-value pair specifies the `name` of the module: *mcp9808*. We then specify how the module should be built with the `build` section:
794 |
795 | * `cmake` - tells the Zephyr build system where to look for CMake files (relative to the module's root directory)
796 | * `kconfig` - path to the *Kconfig* file in the module's root directory
797 | * `settings` - additional settings that Zephyr should know about
798 | * `dts_root` - Zephyr will look for a *dts/* directory at this location and search in there for additional Devicetree source (.dts) and bindings (.yaml, .yml) files
799 |
800 | You can read more about [Zephyr modules here](https://docs.zephyrproject.org/latest/develop/modules.html).
801 |
802 | ## Demo Application
803 |
804 | With our module constructed, we're ready to write a simple application to test our device driver!
805 |
806 | ✅ Create the following directory and file structure for our *read_temp* application in the */workspace/apps/* directory (*blink/* shown for reference):
807 |
808 | ```
809 | apps/
810 | ├── blink/
811 | │ └── ...
812 | └── read_temp/
813 | ├── CMakeLists.txt
814 | ├── prj.conf
815 | ├── boards/
816 | │ └── esp32s3_devkitc.overlay
817 | └── src/
818 | └── main.c
819 | ```
820 |
821 | Let's fill out these files and discuss their contents.
822 |
823 | ### CMakeLists.txt
824 |
825 | ✅ Copy the following to ***apps/read_temp/CMakeLists.txt***:
826 |
827 | ```cmake
828 | # Minimum CMake version
829 | cmake_minimum_required(VERSION 3.20.0)
830 |
831 | # Add additional modules
832 | set(ZEPHYR_EXTRA_MODULES "${CMAKE_SOURCE_DIR}/../../modules/mcp9808")
833 |
834 | # Locate the Zephyr RTOS source
835 | find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
836 |
837 | # Name the project
838 | project(read_temp)
839 |
840 | # Locate the source code for the application
841 | target_sources(app PRIVATE src/main.c)
842 | ```
843 |
844 | We first define the minimum required version of CMake. We derive this from other *CMakeLists.txt* files found in the Zephyr RTOS source code.
845 |
846 | Next, we tell Zephyr where to find our custom *mcp9808* module by setting the `ZEPHYR_EXTRA_MODULES` variable to the module's path. Zephyr will look at this variable during the build process.
847 |
848 | From there, we include the Zephyr RTOS source directory as a CMake package. If we configured Zephyr correctly, the `ZEPHYR_BASE` environment variable should be set in our OS. If not, we could use the path to the Zephyr directory instead.
849 |
850 | Finally, we name the project and tell CMake where to find our source code files.
851 |
852 | I recommend the following resources to learn more about CMake:
853 |
854 | * [CMake's official tutorials](https://cmake.org/cmake/help/latest/guide/tutorial/index.html)
855 | * [Zephyr Build System](https://docs.zephyrproject.org/latest/build/cmake/index.html)
856 |
857 | ### prj.conf
858 |
859 | ✅ Copy the following to ***apps/read_temp/prj.conf***:
860 |
861 | ```conf
862 | CONFIG_GPIO=y
863 | CONFIG_SENSOR=y
864 | CONFIG_I2C=y
865 | CONFIG_MCP9808=y
866 | CONFIG_LOG=y
867 | CONFIG_LOG_DEFAULT_LEVEL=4
868 | ```
869 |
870 | As we saw earlier, Zephyr relies on the Kconfig system (borrowed from the [Linux Kconfig system](https://www.kernel.org/doc/html/v6.9/kbuild/kconfig-language.html)) to enable and disable various software components. Defining options, known as *symbols*, involves setting the symbol name (prefixed with `CONFIG_`) to one of the following value types:
871 |
872 | * *Boolean* - `y` for enabled and `n` for disabled
873 | * *Tristate* - `y`, `n`, or `m` for *loadable modules* (not used much in Zephyr, as modules are only included at build time)
874 | * *Integer* - Integer value (like our `LOG_DEFAULT_LEVEL`)
875 | * *Hex* - Similar to an integer but given in hexadecimal format (prefixed with `0x`)
876 | * *String* - Free-form text input to specify a file path, device name, etc.
877 |
878 | Knowing which options to set can be tricky. Zephyr comes with [two interactive Kconfig interfaces](https://docs.zephyrproject.org/latest/build/kconfig/menuconfig.html) to help. *menuconfig* is the classic, command-line style interface while *guiconfig* is meant for graphical interfaces. As we are not running a full, windowed OS on our container, let's try *menuconfig*.
879 |
880 | > **Note**: You do not need to perform the following actions. We just want to demonstrate how you might find Kconfig options. We already set them for our project in *prj.conf*.
881 |
882 | In a terminal on your VS Code instance (inside the Docker container), navigate to the *apps/read_temp/* directory and build the project with the `-t menuconfig` option:
883 |
884 | ```sh
885 | # west build -b esp32s3_devkitc/esp32s3/procpu -t menuconfig
886 | ```
887 |
888 | Note that we need to specify a board (since it has not been set elsewhere) so that Zephyr knows how to set the default options. Since we are building our application for the *esp32s3_devkitc/esp32s3/procpu*, you can find the default configuration settings in *zephyr/boards/espressif/esp32s3_devkitc/*. In that directory, you'll find that *Kconfig.esp32s3_devkitc* specifies the *system-on-chip* (SOC) module found on the board (`ESP32S3_WROOM_N8`) and which processor we are targeting (`PROCPU` or `APPCPU`). The *esp32s3_devkitc_procpu_defconfig* file then adds or modifies the default values set by the SOC's Kconfig files.
889 |
890 | We also do not need any application code to run *menuconfig*--we just need a CMakeLists.txt file so that the build system knows where to find Zephyr and our custom module.
891 |
892 | You should be presented with a CLI-style graphical interface. Start a search by typing `/` and then enter `MCP9808`. You should see several options related to our MCP9808 driver.
893 |
894 | 
895 |
896 | * *DT_HAS_MICROCHIP_MCP9808* - automatically generated by Zephyr to indicate if the device's module is found on the project's Devicetree. As we have not created our *.overlay* file yet for this project to enable the device, this is (as expected) set to `n`.
897 | * *MCP9808* - the actual driver module we created earlier. Make sure this is set to `y` (to match what we set in *prj.conf*)
898 | * *ZEPHYR_MCP9808_MODULE* - Zephyr generates this symbol based on our module name (defined in */modules/mcp9808/zephyr/module.yaml*). It should be enabled (`y`) by default, and we can't change it manually.
899 | * *menu "mcp9808 (/workspace/apps/read_temp/../../modules/mcp9808)"* - String containing "mcp9808" from our module's description
900 |
901 | Use the arrow keys to highlight *MCP9808* and press enter. You should see our custom module entry. You can enable and disable it by pressing the spacebar (`[ ]` for disabled, `[*]` for enabled).
902 |
903 | 
904 |
905 | Type `?` to view the module info. Here, you can see all the Kconfig options and descriptions we wrote in our *modules/mcp9808/drivers/mcp9808/Kconfig* file!
906 |
907 | 
908 |
909 | Notice that our MCP9808 module depends on the I2C module. If you were to disable the I2C module, you would not be able to enable our module (you can't right now, as too many things depend on I2C).
910 |
911 | If you make any settings in menuconfig, you can save by pressing `s`. Note that menuconfig does not save changes in either the base Zephyr project (*/opt/toolchains/zephyr/*) or our *prj.conf* file. Rather, it saves them in */build/zephyr/.config*.
912 |
913 | Open *apps/read_temp/build/zephyr/.config*. You should see ALL of the Kconfig settings--everything pulled in from default Zephyr, our SOC, board, and *prj.conf*. Tracking down the location of each of these options can be tricky, as this file is compiled from dozens of various config files scattered throughout the Zephyr RTOS code, custom modules, and application settings. This is why we use a tool like *menuconfig*--it makes searching for and setting these options much easier.
914 |
915 | 
916 |
917 | The important thing to note here is that this file sits in our *build/* directory. This is fine if you don't often make changes to your application's build options, but as we've been building with the `-p always` option, this directory gets deleted/repopulated every time. That's not good: any changes we make with menuconfig will be lost when we build!
918 |
919 | There are a number of ways to save this .config file (usually by copying it out to your project folder). My preference is to find which options differ from the default board configuration and set them manually in *prj.conf*. Zephyr looks for a file with this name when building a project and updates the Kconfig options accordingly.
920 |
921 | You can read more about [Zephyr's Kconfig settings here](https://docs.zephyrproject.org/latest/build/kconfig/setting.html).
922 |
923 | ### boards/esp32s3_devkitc.overlay
924 |
925 | Even though we enabled the software portion of our module, we still need to set up the Devicetree node so that our driver code knows how to communicate with the physical device. That means defining an I2C address, assigning SDA and SCL pins, and associating the device with a particular I2C bus.
926 |
927 | ✅ Copy the following to ***apps/read_temp/boards/esp32s3_devkitc.overlay***:
928 |
929 | ```dts
930 | // Create an alias for our MCP9808 device
931 | / {
932 | aliases {
933 | my-mcp9808 = &mcp9808_18_i2c0;
934 | };
935 | };
936 |
937 | // Add custom pins to the node labeled "pinctrl"
938 | &pinctrl {
939 |
940 | // Configure custom pin settings for I2C bus 0
941 | i2c0_custom_pins: i2c0_custom_pins {
942 |
943 | // Custom group name
944 | group1 {
945 | pinmux = , ; // SDA on GPIO9, SCL on GPIO10
946 | bias-pull-up; // Enable pull-up resistors for both pins
947 | drive-open-drain; // Required for I2C
948 | output-high; // Start with lines high (inactive state)
949 | };
950 | };
951 | };
952 |
953 | // Enable I2C0 and add MCP9808 sensor
954 | &i2c0 {
955 | pinctrl-0 = <&i2c0_custom_pins>; // Use the custom pin configuration
956 | status = "okay"; // Enable I2C0 interface
957 |
958 | // Label: name of our device node
959 | mcp9808_18_i2c0: mcp9808@18 {
960 | compatible = "microchip,mcp9808"; // Specify device bindings/driver
961 | reg = <0x18>; // I2C address of the MCP9808
962 | status = "okay"; // Enable the MCP9808 sensor
963 | resolution = <3>; // Set the resolution
964 | };
965 | };
966 | ```
967 |
968 | Zephyr uses the [Devicetree](https://docs.zephyrproject.org/latest/build/dts/index.html) to configure hardware (and some software) components when building a project. An *overlay* file is a supplementary source file that is used to modify or extend the existing Devicetree source (DTS) files, which are pulled in from various SOC, board, and Zephyr locations (much like the Kconfig files). This is where we set pins and configure our custom MCP9808 module.
969 |
970 | > **Note**: Devicetree syntax can be overwhelming at first. I recommend taking a look at [this guide](https://docs.zephyrproject.org/latest/build/dts/intro-syntax-structure.html) to learn more.
971 |
972 | Zephyr defines [aliases and chosen nodes](https://zephyr-docs.listenai.com/guides/dts/intro.html#aliases-and-chosen-nodes) in the root node (`/`) that make addressing other subnodes easier. We create an alias to our device node label so that we can easily access that node in our application code. We define the `mcp9808_18_i2c0` label later in the overlay file when we create our node. Note that *alias* names can only contain the characters `[0-9a-z-]`, which means no underscores! C does not like dashes in names, so when we access this alias from code, we'll need to replace the dash `-` with an underscore `_`.
973 |
974 | We then assign pins to a pin control group that we plan to use for our I2C bus. The `&` symbol says to access the node with the given label (e.g. `pinctrl`). This node is defined elsewhere for our board, and we just want to modify it.
975 |
976 | We create a node (with the syntax `label: node_name`) to group our I2C pins. In there, we define a custom group to configure our pins. We use the pin control names defined in *zephyr/include/zephyr/dt-bindings/pinctrl/esp32s3-pinctrl.h*. This is how the ESP32 assigns functions to pins in Zephyr (e.g. using pins for I2C or UART rather than basic GPIO).
977 |
978 | The `&i2c0` is the label for our I2C bus 0 node on the Devicetree. The actual path is `/soc/i2c@60013000`, which denotes an I2C controller at memory address 0x60013000. This node is set by the SOC configuration. The label `i2c0` is easier to use and makes our code more portable to other processors.
979 |
980 | In this node, we overwrite the `pinctrl-0` property with our new custom pin configuration and then enable the I2C0 controller by setting `status = "okay";` (it is set to `"disabled"` by default). This is how we enable an I2C bus on the ESP32 with Zephyr: set the pins and set the status to `"okay"`.
981 |
982 | We then create a subnode with the name `mcp9808@18`. The `@18` is a *unit address*--it is optional, but it can help readers understand how a device is addressed on a bus. In this node, we define several *properties*:
983 |
984 | * `compatible` - This required string tells Zephyr where to look for the related bindings and driver files
985 | * `reg` - Required address of the device on the bus (our MCP9808's default I2C bus address is 0x18)
986 | * `status` - Required to enable (`"okay"`) or disable (`"disabled"`) the device
987 | * `resolution` - The custom property we defined in our driver to set the temperature resolution. This value is ultimately passed to the instanced code of our driver, which is then sent to the RESOLUTION register in the *init* function at boot time.
988 |
989 | Even though we do not have any application code, you can actually build your project:
990 |
991 | ```sh
992 | west build -p always -b esp32s3_devkitc/esp32s3/procpu -- -DDTC_OVERLAY_FILE=boards/esp32s3_devkitc.overlay
993 | ```
994 |
995 | Note that we pass our overlay file to the build system by setting the `DTC_OVERLAY_FILE` variable. West uses the parameter `--` is used to pass subsequent arguments to the underlying CMake system. So, `DTC_OVERLAY_FILE` is used by CMake rather than the overarching *west* system.
996 |
997 | Zephyr takes our overlay file and combines it with all of the other Devicetree source (.dts) and Devicetree source include (.dtsi) files it found for our SOC and board. It produces a combined DTS file at *apps/read_temp/build/zephyr/zephyr.dts*. If you open that file and search for "mcp9808," you should be able to find the custom node we created. Notice that it is a subnode of `i2c0`, as it is considered to be attached to that bus.
998 |
999 | 
1000 |
1001 | During the build process, Zephyr creates C macros for all of these device nodes and properties. You can find them defined in *apps/read_temp/build/zephyr/include/generated/zephyr/devicetree_generated.h*. Search for "mcp9808" and you'll find the 100+ macros that Zephyr created.
1002 |
1003 | 
1004 |
1005 | The macro naming scheme is based on navigating the Devicetree:
1006 |
1007 | * `DT` - Devicetree
1008 | * `N` - node
1009 | * `S` - slash
1010 | * `P` - property
1011 |
1012 | From this, we can figure out the generated macro for our *resolution* property, which is found at `/soc/i2c@60013000/mcp9808@18{resolution=3}`. This would become `DT_N_S_soc_S_i2c_60013000_S_mcp9808_18_P_resolution` (`@` is not allowed in macro symbols, so it becomes an underscore `_`). Sure enough, if you search for that macro, you'll find it set to `3`.
1013 |
1014 | The other important macro is the `_ORD` macro (highlighted in the image) for our node. This is the *instance* number that Zephyr assigned to our node. Remember our macro magic in the driver code that expands based on each instance of the node? This is that *instance* number. For example, our data struct in the device driver would expand from `mcp9808_data_##inst` to `mcp9808_data_90`.
1015 |
1016 | While this seems like a lot of configuration, it helps create portable device drivers and application code. By making just a few changes to your Devicetree file (and maybe Kconfig), you can port your code to another processor! This is why you'll often see board-specific overlay files in the *boards/* folder of many projects. You can configure one application to build for several different boards.
1017 |
1018 | Zephyr has an [entire series of articles dedicated to the Devicetree](https://docs.zephyrproject.org/latest/build/dts/intro.html), which I highly recommend checking out.
1019 |
1020 | ### src/main.c
1021 |
1022 | Finally, after all that configuration, we get to write our application code!
1023 |
1024 | ✅ Copy the following to ***apps/read_temp/src/main.c***:
1025 |
1026 | ```c
1027 | #include
1028 | #include
1029 | #include
1030 | #include
1031 |
1032 | int main(void)
1033 | {
1034 | const struct device *const mcp = DEVICE_DT_GET(DT_ALIAS(my_mcp9808));
1035 | int ret;
1036 |
1037 | // Check if the MCP9808 is found
1038 | if (mcp == NULL) {
1039 | printf("MCP9808 not found.\n");
1040 | return 0;
1041 | }
1042 |
1043 | // Check if the MCP9808 has been initialized (init function called)
1044 | if (!device_is_ready(mcp)) {
1045 | printf("Device %s is not ready.\n", mcp->name);
1046 | return 0;
1047 | }
1048 |
1049 | // Loop
1050 | while (1)
1051 | {
1052 | struct sensor_value tmp;
1053 |
1054 | // Fetch the temperature value from the sensor into the device's data structure
1055 | ret = sensor_sample_fetch(mcp);
1056 | if (ret != 0) {
1057 | printf("Sample fetch error: %d\n", ret);
1058 | return 0;
1059 | }
1060 |
1061 | // Copy the temperature value from the device's data structure into the tmp struct
1062 | ret = sensor_channel_get(mcp, SENSOR_CHAN_AMBIENT_TEMP, &tmp);
1063 | if (ret != 0) {
1064 | printf("Channel get error: %d\n", ret);
1065 | return 0;
1066 | }
1067 |
1068 | // Print the temperature value
1069 | printf("Temperature: %d.%06d\n", tmp.val1, tmp.val2);
1070 |
1071 | // Sleep for 1 second
1072 | k_sleep(K_SECONDS(1));
1073 | }
1074 |
1075 | return 0;
1076 | }
1077 | ```
1078 |
1079 | The code has been commented to help you understand what is happening. But, I'll call out a few important sections as it relates to our driver.
1080 |
1081 | We get a pointer to the instance of our *mcp9808* driver by using the `DEVICE_DT_GET()` macro. We pass it the value of `DT_ALIAS(my_mcp9808)`, which expands to our Devicetree node given by the alias `my-mcp9808`. Remember that Devicetree aliases cannot contain underscores, and C does not like dashes in macro names. So, Zephyr automatically provides this conversion for us. Also note that we are passing in a macro symbol (`my_mcp9808`) rather than a string (or other C native data type). Zephyr's macro magic uses this symbol to find the associated Devicetree node.
1082 |
1083 | This instance is returned as a *device* struct, which has certain properties defined by Zephyr. You can find this definition in *zephyr/include/zephyr/device.h*. Remember that our MCP9808 driver is a *sensor*, which is a type of *device*. Each of these classifications (sensor, device) provides certain definitions and interface requirements.
1084 |
1085 | Whenever we want to use this device, we call the related functions (whether they are *device* functions, *sensor* functions, or *mcp9808* functions) and pass in a pointer to our `mcp` device struct.
1086 |
1087 | > **Side note**: if this were C++, you'd call the member functions associated with that object. All this instancing macro magic is how Zephyr gets around object-oriented programming.
1088 |
1089 | For our setup code, we first make sure that our MCP9808 instance was found by the `DEVICE_DT_GET()` macro by checking to see if the pointer is `NULL`. We then make sure that `mcp9808_init()` was automatically called during the boot process by checking if `device_is_ready()`.
1090 |
1091 | If you recall from our device driver code, we only have two publically available functions: `sample_fetch` and `channel_get`. OK, we also have the functions, constants, and macros conferred to us by the [Device API](https://docs.zephyrproject.org/apidoc/latest/group__device__model.html) and [Sensor API](https://docs.zephyrproject.org/apidoc/latest/group__sensor__interface.html).
1092 |
1093 | > **Side note**: if you compared this to object-oriented programming, *mcp9808* would be a subclass of *sensor*, which would be a subclass of *device*.
1094 |
1095 | The *sensor* interface declares `sensor_sample_fetch()` and `sensor_channel_get()`. In our driver code, we assigned `mcp9808_sample_fetch()` and `mcp9808_channel_get()` to these functions respectively when we defined `struct sensor_driver_api mcp9808_api_funcs`. So, for example, when we call `sensor_sample_fetch()` in our application code, it actually calls `mcp9808_sample_fetch()` in our driver code.
1096 |
1097 | The basic pattern for working with sensors is to first *fetch* the sample, which causes the sensor to take a reading before storing the result in an internal variable (`dev->data`). You then *get* the channel information, which returns the data stored in `dev->data` for the specified channel (only *ambient tamperature* in our case).
1098 |
1099 | Finally, we print the value and wait 1 second before repeating.
1100 |
1101 | ## Build and Flash
1102 |
1103 | ✅ In the VS Code client, go to the *read_temp* application directory and build the project:
1104 |
1105 | ```
1106 | cd apps/read_temp
1107 | west build -p always -b esp32s3_devkitc/esp32s3/procpu -- -DDTC_OVERLAY_FILE=boards/esp32s3_devkitc.overlay
1108 | ```
1109 |
1110 | Pay attention to any errors you see.
1111 |
1112 | ✅ Connect the ESP32 board to your computer (use the **UART** port on the ESP32). In a new terminal on your **host computer**, activate the Python virtual environment (Linux/macOS: `source venv/bin/activate`, Windows: `venv\Scripts\activate`) if not done so already.
1113 |
1114 | ✅ Flash the binary to your board.
1115 |
1116 | > **Important!** make sure to execute flashing and serial monitor programs from your **host OS** (not from within the Docker container)
1117 |
1118 |
1119 | ```sh
1120 | python -m esptool --port "" --chip auto --baud 921600 --before default_reset --after hard_reset write_flash -u --flash_mode keep --flash_freq 40m --flash_size detect 0x0 workspace/apps/read_temp/build/zephyr/zephyr.bin
1121 | ```
1122 |
1123 | ✅ Open a serial port for debugging (change `` to the serial port for your ESP32 board):
1124 |
1125 | ```sh
1126 | python -m serial.tools.miniterm "" 115200 --raw
1127 | ```
1128 |
1129 | You should see the debugging information (` MCP9808: mcp9808_init: Initializing`) printed once during boot followed by temperature data every second.
1130 |
1131 | 
1132 |
1133 | ## Challenge
1134 |
1135 | If you'd like to challenge yourself, try combining the *blink* and *read_temp* examples to create a simple threshold. If the temperature rises above some value (e.g. 27°C), have the LED turn on. Otherwise, turn the LED off. You will need to modify the Devicetree *overlay* file as well as make changes to the *src/main.c* code.
1136 |
1137 | ## Going Further
1138 |
1139 | This tutorial provided a wide overview (with example code) on how to create a device driver in Zephyr. There is a LOT going on, and it can be overwhelming. As a result, I highly recommend starting with existing drivers found in the Zephyr source code and diving deeper into the device driver model. Here are some additional resources I recommend to help you grapple device drivers:
1140 |
1141 | * [Zephyr Official Documentation: Device Driver Model](https://docs.zephyrproject.org/latest/kernel/drivers/index.html)
1142 | * [Zephyr Official Documentation: Build System (CMake)](https://docs.zephyrproject.org/latest/build/cmake/index.html)
1143 | * [Zephyr Official Documentation: Devicetree Bindings](https://docs.zephyrproject.org/latest/build/dts/bindings.html)
1144 | * [Martin Lampacher's Memfault Series on the Zephyr Devicetree](https://interrupt.memfault.com/authors/lampacher/)
1145 | * [Video: Mastering Zephyr Driver Development by Gerard Marull Paretas](https://www.youtube.com/watch?v=o-f2qCd2AXo)
1146 |
1147 | While we created an *out-of-tree* module, you could submit your device driver code to the main Zephyr RTOS repository. You'll want to follow all of the [contribution guidelines](https://docs.zephyrproject.org/latest/contribute/guidelines.html), including adding any necessary samples, tests, and documentation.
1148 |
1149 | ## Troubleshooting
1150 |
1151 | * If you see an error like `could not find build.ninja` during the build process, try deleting the *build/* folder and rebuilding the project. The *build/* folder sometimes gets corrupted if the build process is interrupted or stops partway through.
1152 | * If you get a `Permission denied` or `Port doesn't exist` error when flashing from a Linux host, you likely need to add your user to the *dialout* group with the following command: `sudo usermod -a -G dialout $USER`. Log out and log back in (or restart).
1153 |
1154 | ## License
1155 |
1156 | This tutorial (README.md) is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.en).
1157 |
1158 | All software in this repository, unless otherwise noted, is licensed under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).
1159 |
--------------------------------------------------------------------------------
/scripts/espressif/west.yml:
--------------------------------------------------------------------------------
1 | manifest:
2 | defaults:
3 | remote: upstream
4 |
5 | remotes:
6 | - name: upstream
7 | url-base: https://github.com/zephyrproject-rtos
8 |
9 | group-filter: [-optional]
10 |
11 | #
12 | # Please add items below based on alphabetical order
13 | projects:
14 | - name: acpica
15 | revision: 8d24867bc9c9d81c81eeac59391cda59333affd4
16 | path: modules/lib/acpica
17 | - name: edtt
18 | revision: b9ca3c7030518f07b7937dacf970d37a47865a76
19 | path: tools/edtt
20 | groups:
21 | - tools
22 | - name: fatfs
23 | revision: 427159bf95ea49b7680facffaa29ad506b42709b
24 | path: modules/fs/fatfs
25 | groups:
26 | - fs
27 | - name: hal_espressif
28 | revision: 61a002ad757f567cdef92014b483e6f325c41afc
29 | path: modules/hal/espressif
30 | west-commands: west/west-commands.yml
31 | groups:
32 | - hal
33 | - name: hal_xtensa
34 | revision: baa56aa3e119b5aae43d16f9b2d2c8112e052871
35 | path: modules/hal/xtensa
36 | groups:
37 | - hal
38 | - name: hostap
39 | path: modules/lib/hostap
40 | revision: 7c32520564908e1220976b6c185dec296b6d4a80
41 | - name: liblc3
42 | revision: 1a5938ebaca4f13fe79ce074f5dee079783aa29f
43 | path: modules/lib/liblc3
44 | - name: littlefs
45 | path: modules/fs/littlefs
46 | groups:
47 | - fs
48 | revision: 009bcff0ed4853a53df8256039fa815bda6854dd
49 | - name: loramac-node
50 | revision: fb00b383072518c918e2258b0916c996f2d4eebe
51 | path: modules/lib/loramac-node
52 | - name: lvgl
53 | revision: 2b498e6f36d6b82ae1da12c8b7742e318624ecf5
54 | path: modules/lib/gui/lvgl
55 | - name: mbedtls
56 | revision: eb55f4734585dfd8cd3da6d4b01a6e372f073ee1
57 | path: modules/crypto/mbedtls
58 | groups:
59 | - crypto
60 | - name: mcuboot
61 | revision: b9d69dd2a2d6df32da6608d549138288bb7d7aa5
62 | path: bootloader/mcuboot
63 | groups:
64 | - bootloader
65 | - name: mipi-sys-t
66 | path: modules/debug/mipi-sys-t
67 | groups:
68 | - debug
69 | revision: 71ace1f5caa03e56c8740a09863e685efb4b2360
70 | - name: net-tools
71 | revision: 93acc8bac4661e74e695eb1aea94c7c5262db2e2
72 | path: tools/net-tools
73 | groups:
74 | - tools
75 | - name: openthread
76 | revision: 2aeb8b833ba760ec29d5f340dd1ce7bcb61c5d56
77 | path: modules/lib/openthread
78 | - name: percepio
79 | path: modules/debug/percepio
80 | revision: b68d17993109b9bee6b45dc8c9794e7b7bce236d
81 | groups:
82 | - debug
83 | - name: picolibc
84 | path: modules/lib/picolibc
85 | revision: d492d5fa7c96918e37653f303028346bb0dd51a2
86 | - name: segger
87 | revision: b011c45b585e097d95d9cf93edf4f2e01588d3cd
88 | path: modules/debug/segger
89 | groups:
90 | - debug
91 | - name: tinycrypt
92 | revision: 1012a3ebee18c15ede5efc8332ee2fc37817670f
93 | path: modules/crypto/tinycrypt
94 | groups:
95 | - crypto
96 | - name: uoscore-uedhoc
97 | revision: 84ef879a46d7bfd9a423fbfb502b04289861f9ea
98 | path: modules/lib/uoscore-uedhoc
99 | - name: zcbor
100 | revision: 47f34dd7f5284e8750b5a715dee7f77c6c5bdc3f
101 | path: modules/lib/zcbor
102 |
103 | self:
104 | path: zephyr
105 | west-commands: scripts/west-commands.yml
106 | import: submanifests
--------------------------------------------------------------------------------
/scripts/zephyr.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "name": "/workspace",
5 | "path": "/workspace"
6 | },
7 | {
8 | "name": "/opt/toolchains/zephyr",
9 | "path": "/opt/toolchains/zephyr"
10 | }
11 | ],
12 | "settings": {
13 | "terminal.integrated.shell.linux": "/bin/bash",
14 | "workbench.colorTheme": "Visual Studio Dark - C++"
15 | },
16 | "extensions": {
17 | "recommendations": [
18 | "ms-vscode.cpptools",
19 | "ms-vscode.cmake-tools"
20 |
21 | ]
22 | }
23 | }
--------------------------------------------------------------------------------
/workspace/.vscode/c_cpp_properties.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "name": "Linux",
5 | "includePath": [
6 | "/workspace/**",
7 | "/opt/toolchains/zephyr/arch/**",
8 | "/opt/toolchains/zephyr/boards/**",
9 | "/opt/toolchains/zephyr/drivers/**",
10 | "/opt/toolchains/zephyr/dts/**",
11 | "/opt/toolchains/zephyr/include/**",
12 | "/opt/toolchains/zephyr/kernel/**",
13 | "/opt/toolchains/zephyr/lib/**",
14 | "/opt/toolchains/zephyr/modules/**",
15 | "/opt/toolchains/zephyr/subsys/**"
16 | ],
17 | "browse": {
18 | "path": [
19 | "/workspace",
20 | "/opt/toolchains/zephyr"
21 | ],
22 | "limitSymbolsToIncludedHeaders": true,
23 | "databaseFilename": ""
24 | },
25 | "defines": [],
26 | "compilerPath": "/usr/bin/gcc",
27 | "cStandard": "c11",
28 | "cppStandard": "c++11",
29 | "intelliSenseMode": "linux-gcc-x64"
30 | }
31 | ],
32 | "version": 4
33 | }
--------------------------------------------------------------------------------
/workspace/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cmake.ignoreCMakeListsMissing": true
3 | }
--------------------------------------------------------------------------------
/workspace/apps/blink/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # Minimum CMake version
2 | cmake_minimum_required(VERSION 3.22.0)
3 |
4 | # Locate the Zephyr RTOS source
5 | find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
6 |
7 | # Name the project
8 | project(blink)
9 |
10 | # Locate the source code for the application
11 | target_sources(app PRIVATE src/main.c)
12 |
--------------------------------------------------------------------------------
/workspace/apps/blink/boards/esp32s3_devkitc.overlay:
--------------------------------------------------------------------------------
1 | / {
2 | aliases {
3 | led0 = &myled0;
4 | };
5 |
6 | leds {
7 | compatible = "gpio-leds";
8 | myled0: led_0 {
9 | gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
10 | };
11 | };
12 | };
--------------------------------------------------------------------------------
/workspace/apps/blink/prj.conf:
--------------------------------------------------------------------------------
1 | CONFIG_GPIO=y
--------------------------------------------------------------------------------
/workspace/apps/blink/src/main.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | /* 1000 msec = 1 sec */
6 | #define SLEEP_TIME_MS 1000
7 |
8 | /* The devicetree node identifier for the "led0" alias. */
9 | #define LED0_NODE DT_ALIAS(led0)
10 |
11 | /*
12 | * A build error on this line means your board is unsupported.
13 | * See the sample documentation for information on how to fix this.
14 | */
15 | static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
16 |
17 | int main(void)
18 | {
19 | int ret;
20 | bool led_state = true;
21 |
22 | if (!gpio_is_ready_dt(&led)) {
23 | return 0;
24 | }
25 |
26 | ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
27 | if (ret < 0) {
28 | return 0;
29 | }
30 |
31 | while (1) {
32 | ret = gpio_pin_toggle_dt(&led);
33 | if (ret < 0) {
34 | return 0;
35 | }
36 |
37 | printk("LED state: %s\n", led_state ? "ON" : "OFF");
38 |
39 | led_state = !led_state;
40 | k_msleep(SLEEP_TIME_MS);
41 | }
42 | return 0;
43 | }
44 |
--------------------------------------------------------------------------------
/workspace/modules/README.md:
--------------------------------------------------------------------------------
1 | Place your out-of-tree or custom modules here. You can reference them in your application's *CMakeLists.txt* file with the following line:
2 |
3 | ```cmake
4 | set(ZEPHYR_EXTRA_MODULES "${CMAKE_SOURCE_DIR}/../../modules/")
5 | ```
--------------------------------------------------------------------------------