Skip to content

Commit

Permalink
Add examples and documentation for ESP32 ADC driver
Browse files Browse the repository at this point in the history
Adds Erlang and Elixir examples for the ESP32 ADC driver, and documentation to
the Programmers Guide.

Signed-off-by: Winford <[email protected]>
  • Loading branch information
UncleGrumpy committed Sep 28, 2024
1 parent 234559d commit da1bf5e
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")`
- Support for Elixir `List.Chars` protocol
- Support for `gen_server:start_monitor/3,4`
- Support for `code:ensure_loaded/1`
- ESP32: add support for `esp_adc` ADC driver, with Erlang and Elixir examples

### Changed

Expand Down
57 changes: 57 additions & 0 deletions doc/src/programmers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,63 @@ Since only one instance of the GPIO driver is allowed, you may also simply use [
ok = gpio:stop().
```
### ESP32 ADC
The [`esp_adc` module](./apidocs/erlang/eavmlib/esp_adc.md) provides the functionality to use the ESP32 family [SAR ADC](https://en.wikipedia.org/wiki/Successive-approximation_ADC) peripheral to measure (analog) voltages from a pin and obtain both raw bit values as well as calibrated voltage values in millivolts.
The module provides two sets of APIs for using the ADC peripheral; there is a set of low level resource based nifs, and a gen_server managed set of convenience functions. The nifs rely on unit and channel handle resources for configuring and taking measurements. The convenience functions use the gen_server to maintain these resources and use pin numbers to interact with the driver. Examples for both APIs can be found the [AtomVM repository atomvm/examples/erlang/esp32](https://github.com/atomvm/AtomVM/tree/main/examples/erlang/esp32) directory. A demonstration of the simple APIs is as follows:
```erlang
...
Pin = 33,
ok = esp_adc:start(Pin, [{bitwidth, bit_12}, {atten, db_2_5}]),
{ok, {Raw, Mv}} = esp_adc:read(Pin, [raw, voltage, {samples, 48}]),
io:format("ADC pin ~p raw value=~p millivolts=~p~n", [Pin, Raw, Mv]),
ok = esp_adc:stop(),
...
```
#### ESP32 ADC configuration options
Some newer ESP32 family devices only use a single fixed bit width, this is typically 12 bits, but some provide 13 bit resolution. The ESP32 classic supports 9 bit up to 12 bit resolutions. The `bitwidth` option `bit_max` will use the highest supported resolution for the device.
The `attenuation` option determines the range of voltage to be measured, the specific voltage range for each setting varies by chip, so as always consult your devices datasheet before connecting an ADC pin to a voltage supply to be measured. The chart below depicts the approximate safe voltage ranges for each attenuation level:
| Attenuation | Min Millivolts | Max Millivolts |
|------------------|----------------|----------------|
| `db_0` | 0-100 | 750-950 |
| `db_2_5` | 0-100 | 1050-1250 |
| `db_6` | 0-150 | 1300-1750 |
| `db_11 \| db_12` | 0-150 | 2450-2500 |
Consult the datasheet of your device for the exact voltage ranges supported by each attenuation level.
```{warning}
The option `db_11` has been superseded by `db_12`. The option `db_11` and will be deprecated in a future release, applications should be updated to use `db_12` (except for builds with ESP-IDF versions prior to v5.2). To Continue to support older IDF version builds, the default will remain `db_11`, which is the maximum tolerated voltage on all builds, as `db_12` supported builds will automatically use `db_12` in place of `db_11`. After `db_11` is deprecated in all builds (with the sunset of ESP-IDF v5.1 support) the default will be changed to `db_12`.
```
```{note}
For a higher degree of accuracy increase the number of sample taken, the default is 64. If highly stable and accurate ADC measurements are required for an application you may need to connect a bypass capacitor (e.g., a 100 nF ceramic capacitor) to the ADC input pad in use, to minimize noise. This chart from the [Espressif ADC Calibration Driver documentation](https://docs.espressif.com/projects/esp-idf/en/v5.3/esp32/api-reference/peripherals/adc_calibration.html) shows the difference between the use of a capacitor and without, as well as with a capacitor and multisampling of 64 samples.
![ADC Noise Comparison](https://docs.espressif.com/projects/esp-idf/en/v5.3/esp32/_images/adc-noise-graph.jpg)
You can clearly see the noisy results without a capacitor. This is mitigated by the use of multisampling but without a decoupling capacitor results will likely still contain some noise.
```
When an ADC channel is configured by the use of `esp_adc:acquire/2,4` or `esp_adc:start/1,2` the driver will select the optimal calibration mechanism supported by the device and channel configuration. If neither the line fitting or curve fitting mechanisms are supported by the device using the provided configuration options an estimated result will be used to provide `voltage` values, based on the [formula suggested by Espressif](https://docs.espressif.com/projects/esp-idf/en/v5.3/esp32/api-reference/peripherals/adc_oneshot.html#read-conversion-result). For chips using the line fitting calibration scheme that do not have the default vref efuse set, a default vref of 1100 mV is used, this is not currently settable.
#### ESP32 ADC read options
The read options take the form of a proplist, if the key `raw` is true (`{raw, true}` or simply appears in the list as the atom `raw`), then the raw value will be returned in the first element of the returned tuple. Otherwise, this element will be the atom `undefined`.
If the key `voltage` is true (or simply appears in the list as an atom), then a calibrated voltage value will be returned in millivolts in the second element of the returned tuple. Otherwise, this element will be the atom `undefined`.
You may specify the number of samples (1 - 100000) to be taken and averaged over using the tuple `{samples, Samples :: 1..100000}`, the default is `64`.
```{warning}
Using a large number of samples can significantly increase the amount of time before a response, up to several seconds.
```
### I2C
The [`i2c` module](./apidocs/erlang/eavmlib/i2c.md) encapsulates functionality associated with the 2-wire Inter-Integrated Circuit (I2C) interface.
Expand Down
45 changes: 45 additions & 0 deletions examples/elixir/esp32/Adc_nifs.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#
# This file is part of AtomVM.
#
# Copyright 2024 Winford <[email protected]>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
#


defmodule ADCnifs do
# suppress warnings when compiling the VM
@compile {:no_warn_undefined, [Esp.ADC]}
@pin 33

def start() do
IO.puts("Testing ADC on pin #{@pin}")
{:ok, unit} = Esp.ADC.init()
{:ok, chan} = Esp.ADC.acquire(@pin, unit, :bit_max, :db_12)
loop(@pin, unit, chan)
end

defp loop(pin, unit, chan) do
case Esp.ADC.sample(chan, unit, [:raw, :voltage, {:samples, 64}]) do
{:ok, {raw, mv}} ->
IO.puts("Pin #{pin} value = #{raw}, millivolts = #{mv}")
error ->
IO.puts("Error taking ADC sample from pin #{pin}: #{error}")
end
Process.sleep(500)
loop(pin, unit, chan)
end

end
40 changes: 40 additions & 0 deletions examples/erlang/esp32/adc_example.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
%% Copyright (c) 2020 dushin.net
%% Copyright (c) 2024 Winford <[email protected]>
%% All rights reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%
% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
%

-module(adc_example).

-export([start/0]).

-define(Pin, 34).

start() ->
io:format("Testing ADC on pin ~p~n", [?Pin]),
ok = esp_adc:start(?Pin),
loop(?Pin).

loop(Pin) ->
case esp_adc:read(Pin) of
{ok, {Raw, MilliVolts}} ->
io:format("Raw: ~p Voltage: ~pmV~n", [Raw, MilliVolts]);
Error ->
io:format("Error taking reading: ~p~n", [Error])
end,
timer:sleep(1000),
loop(Pin).
40 changes: 40 additions & 0 deletions examples/erlang/esp32/adc_nif_example.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
%%
%% Copyright (c) 2024 Winford <[email protected]>
%% All rights reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%
% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
%

-module(adc_nif_example).

-export([start/0]).
-define(Pin, 34).

start() ->
io:format("Testing ADC resource NIFs on pin ~p~n", [?Pin]),
{ok, Unit} = esp_adc:init(),
{ok, Chan} = esp_adc:acquire(?Pin, Unit),
loop(Chan, Unit).

loop(Chan, Unit) ->
case esp_adc:sample(Chan, Unit) of
{ok, {Raw, MilliVolts}} ->
io:format("Raw: ~p Voltage: ~pmV~n", [Raw, MilliVolts]);
Error ->
io:format("Error taking reading: ~p~n", [Error])
end,
timer:sleep(1000),
loop(Chan, Unit).

0 comments on commit da1bf5e

Please sign in to comment.