{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 8. Pulse width modulation\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's say we want to provide a component with a constant voltage of, say, 2.5 volts. It would be convenient if we could just tell the Arduino Uno to deliver analog 2.5 volts out of a specified pin. This process, in which you send a digital number that results in an analog voltage coming out is called [digital-to-analog conversion](https://en.wikipedia.org/wiki/Digital-to-analog_converter), abbreviated as DAC. [Arduino Due](https://store.arduino.cc/usa/due) and [Ardunio Zero](https://store.arduino.cc/usa/arduino-zero) offer 12-bit and 10-bit DAC, respectively, but there are no built-in DAC capabilities in Arduino Uno. The Uno board can only write digital values. That is, it can only send values that are `HIGH` (5 V) and `LOW` (0 V). To deliver voltages out of pins between zero and five volts, it uses **pulse width modulation (PWM)** as a replacement for DAC." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The idea behind PWM\n", "\n", "While we cannot deliver 2.5 volts at any given time using Arduino Uno, we can deliver an *average* of 2.5 volts over time. Think of a light switch. If you flip it on for a second, then off for a second, them back on for a second, etc., over the course of a minute, you would have, on average, a half-dimmed light.\n", "\n", "Now, say we turn digital pin 3 on (`HIGH`, 5 V) for one millisecond. Then we turn it off (`LOW`, 0 V) for one millisecond. We keep doing this, and, on average, we deliver 2.5 V.\n", "\n", "A signal given by PWM is characterized by three parameters.\n", "\n", "1. The amplitude. For Arduino Uno, this is 5 Volts.\n", "2. The frequency. This is how rapidly the voltage oscillates.\n", "3. The duty ratio. This is the fraction of time in one period of the oscillation that the voltage is high.\n", "\n", "To see how these parameters affect the voltage levels over time, you can manipulate the interactive plot below. I fixed the amplitude to 5 V, and you can vary the frequency and duty ratio. I intentionally left the Python/JavaScript code to generate the plot in case you are interested in how to build these things using [Bokeh](http://bokeh.pydata.org/). Soon, we will be building interactive dashboards for controlling devices using Bokeh. (But don't worry, you won't have to hack and JavaScript; I used JavaScript so you could interact with the plot in the static HTML document.)" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "
\n", " \n", " Loading BokehJS ...\n", "
" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "\n", "(function(root) {\n", " function now() {\n", " return new Date();\n", " }\n", "\n", " var force = true;\n", "\n", " if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n", " root._bokeh_onload_callbacks = [];\n", " root._bokeh_is_loading = undefined;\n", " }\n", "\n", " var JS_MIME_TYPE = 'application/javascript';\n", " var HTML_MIME_TYPE = 'text/html';\n", " var EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n", " var CLASS_NAME = 'output_bokeh rendered_html';\n", "\n", " /**\n", " * Render data to the DOM node\n", " */\n", " function render(props, node) {\n", " var script = document.createElement(\"script\");\n", " node.appendChild(script);\n", " }\n", "\n", " /**\n", " * Handle when an output is cleared or removed\n", " */\n", " function handleClearOutput(event, handle) {\n", " var cell = handle.cell;\n", "\n", " var id = cell.output_area._bokeh_element_id;\n", " var server_id = cell.output_area._bokeh_server_id;\n", " // Clean up Bokeh references\n", " if (id != null && id in Bokeh.index) {\n", " Bokeh.index[id].model.document.clear();\n", " delete Bokeh.index[id];\n", " }\n", "\n", " if (server_id !== undefined) {\n", " // Clean up Bokeh references\n", " var cmd = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n", " cell.notebook.kernel.execute(cmd, {\n", " iopub: {\n", " output: function(msg) {\n", " var id = msg.content.text.trim();\n", " if (id in Bokeh.index) {\n", " Bokeh.index[id].model.document.clear();\n", " delete Bokeh.index[id];\n", " }\n", " }\n", " }\n", " });\n", " // Destroy server and session\n", " var cmd = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n", " cell.notebook.kernel.execute(cmd);\n", " }\n", " }\n", "\n", " /**\n", " * Handle when a new output is added\n", " */\n", " function handleAddOutput(event, handle) {\n", " var output_area = handle.output_area;\n", " var output = handle.output;\n", "\n", " // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n", " if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n", " return\n", " }\n", "\n", " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", "\n", " if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n", " toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n", " // store reference to embed id on output_area\n", " output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", " }\n", " if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", " var bk_div = document.createElement(\"div\");\n", " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", " var script_attrs = bk_div.children[0].attributes;\n", " for (var i = 0; i < script_attrs.length; i++) {\n", " toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n", " toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n", " }\n", " // store reference to server id on output_area\n", " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", " }\n", " }\n", "\n", " function register_renderer(events, OutputArea) {\n", "\n", " function append_mime(data, metadata, element) {\n", " // create a DOM node to render to\n", " var toinsert = this.create_output_subarea(\n", " metadata,\n", " CLASS_NAME,\n", " EXEC_MIME_TYPE\n", " );\n", " this.keyboard_manager.register_events(toinsert);\n", " // Render to node\n", " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", " render(props, toinsert[toinsert.length - 1]);\n", " element.append(toinsert);\n", " return toinsert\n", " }\n", "\n", " /* Handle when an output is cleared or removed */\n", " events.on('clear_output.CodeCell', handleClearOutput);\n", " events.on('delete.Cell', handleClearOutput);\n", "\n", " /* Handle when a new output is added */\n", " events.on('output_added.OutputArea', handleAddOutput);\n", "\n", " /**\n", " * Register the mime type and append_mime function with output_area\n", " */\n", " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", " /* Is output safe? */\n", " safe: true,\n", " /* Index of renderer in `output_area.display_order` */\n", " index: 0\n", " });\n", " }\n", "\n", " // register the mime type if in Jupyter Notebook environment and previously unregistered\n", " if (root.Jupyter !== undefined) {\n", " var events = require('base/js/events');\n", " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", "\n", " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", " register_renderer(events, OutputArea);\n", " }\n", " }\n", "\n", " \n", " if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n", " root._bokeh_timeout = Date.now() + 5000;\n", " root._bokeh_failed_load = false;\n", " }\n", "\n", " var NB_LOAD_WARNING = {'data': {'text/html':\n", " \"
\\n\"+\n", " \"

\\n\"+\n", " \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n", " \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n", " \"

\\n\"+\n", " \"\\n\"+\n", " \"\\n\"+\n", " \"from bokeh.resources import INLINE\\n\"+\n", " \"output_notebook(resources=INLINE)\\n\"+\n", " \"\\n\"+\n", " \"
\"}};\n", "\n", " function display_loaded() {\n", " var el = document.getElementById(\"1002\");\n", " if (el != null) {\n", " el.textContent = \"BokehJS is loading...\";\n", " }\n", " if (root.Bokeh !== undefined) {\n", " if (el != null) {\n", " el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n", " }\n", " } else if (Date.now() < root._bokeh_timeout) {\n", " setTimeout(display_loaded, 100)\n", " }\n", " }\n", "\n", "\n", " function run_callbacks() {\n", " try {\n", " root._bokeh_onload_callbacks.forEach(function(callback) {\n", " if (callback != null)\n", " callback();\n", " });\n", " } finally {\n", " delete root._bokeh_onload_callbacks\n", " }\n", " console.debug(\"Bokeh: all callbacks have finished\");\n", " }\n", "\n", " function load_libs(css_urls, js_urls, callback) {\n", " if (css_urls == null) css_urls = [];\n", " if (js_urls == null) js_urls = [];\n", "\n", " root._bokeh_onload_callbacks.push(callback);\n", " if (root._bokeh_is_loading > 0) {\n", " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", " return null;\n", " }\n", " if (js_urls == null || js_urls.length === 0) {\n", " run_callbacks();\n", " return null;\n", " }\n", " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", " root._bokeh_is_loading = css_urls.length + js_urls.length;\n", "\n", " function on_load() {\n", " root._bokeh_is_loading--;\n", " if (root._bokeh_is_loading === 0) {\n", " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", " run_callbacks()\n", " }\n", " }\n", "\n", " function on_error(url) {\n", " console.error(\"failed to load \" + url);\n", " }\n", "\n", " for (let i = 0; i < css_urls.length; i++) {\n", " const url = css_urls[i];\n", " const element = document.createElement(\"link\");\n", " element.onload = on_load;\n", " element.onerror = on_error.bind(null, url);\n", " element.rel = \"stylesheet\";\n", " element.type = \"text/css\";\n", " element.href = url;\n", " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", " document.body.appendChild(element);\n", " }\n", "\n", " const hashes = {\"https://cdn.bokeh.org/bokeh/release/bokeh-2.3.0.min.js\": \"HjagQp6T0/7bxYTAXbLotF1MLAGWmhkY5siA1Gc/pcEgvgRPtMsRn0gQtMwGKiw1\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.3.0.min.js\": \"ZEPPTjL+mdyqgIq+/pl9KTwzji8Kow2NnI3zWY8+sFinWP/SYJ80BnfeJsa45iYj\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.3.0.min.js\": \"exLqv2ACDRIaV7ZK1iL8aGzGYQvKVuT3U2CT7FsQREBxRah6JrkVCoFy0koY1YqV\"};\n", "\n", " for (let i = 0; i < js_urls.length; i++) {\n", " const url = js_urls[i];\n", " const element = document.createElement('script');\n", " element.onload = on_load;\n", " element.onerror = on_error.bind(null, url);\n", " element.async = false;\n", " element.src = url;\n", " if (url in hashes) {\n", " element.crossOrigin = \"anonymous\";\n", " element.integrity = \"sha384-\" + hashes[url];\n", " }\n", " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", " document.head.appendChild(element);\n", " }\n", " };\n", "\n", " function inject_raw_css(css) {\n", " const element = document.createElement(\"style\");\n", " element.appendChild(document.createTextNode(css));\n", " document.body.appendChild(element);\n", " }\n", "\n", " \n", " var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-2.3.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.3.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.3.0.min.js\"];\n", " var css_urls = [];\n", " \n", "\n", " var inline_js = [\n", " function(Bokeh) {\n", " Bokeh.set_log_level(\"info\");\n", " },\n", " function(Bokeh) {\n", " \n", " \n", " }\n", " ];\n", "\n", " function run_inline_js() {\n", " \n", " if (root.Bokeh !== undefined || force === true) {\n", " \n", " for (var i = 0; i < inline_js.length; i++) {\n", " inline_js[i].call(root, root.Bokeh);\n", " }\n", " if (force === true) {\n", " display_loaded();\n", " }} else if (Date.now() < root._bokeh_timeout) {\n", " setTimeout(run_inline_js, 100);\n", " } else if (!root._bokeh_failed_load) {\n", " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", " root._bokeh_failed_load = true;\n", " } else if (force !== true) {\n", " var cell = $(document.getElementById(\"1002\")).parents('.cell').data().cell;\n", " cell.output_area.append_execute_result(NB_LOAD_WARNING)\n", " }\n", "\n", " }\n", "\n", " if (root._bokeh_is_loading === 0) {\n", " console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n", " run_inline_js();\n", " } else {\n", " load_libs(css_urls, js_urls, function() {\n", " console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", " run_inline_js();\n", " });\n", " }\n", "}(window));" ], "application/vnd.bokehjs_load.v0+json": "\n(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\n \n\n \n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n var NB_LOAD_WARNING = {'data': {'text/html':\n \"
\\n\"+\n \"

\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"

\\n\"+\n \"\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded() {\n var el = document.getElementById(\"1002\");\n if (el != null) {\n el.textContent = \"BokehJS is loading...\";\n }\n if (root.Bokeh !== undefined) {\n if (el != null) {\n el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(display_loaded, 100)\n }\n }\n\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n const hashes = {\"https://cdn.bokeh.org/bokeh/release/bokeh-2.3.0.min.js\": \"HjagQp6T0/7bxYTAXbLotF1MLAGWmhkY5siA1Gc/pcEgvgRPtMsRn0gQtMwGKiw1\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.3.0.min.js\": \"ZEPPTjL+mdyqgIq+/pl9KTwzji8Kow2NnI3zWY8+sFinWP/SYJ80BnfeJsa45iYj\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.3.0.min.js\": \"exLqv2ACDRIaV7ZK1iL8aGzGYQvKVuT3U2CT7FsQREBxRah6JrkVCoFy0koY1YqV\"};\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n if (url in hashes) {\n element.crossOrigin = \"anonymous\";\n element.integrity = \"sha384-\" + hashes[url];\n }\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n \n var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-2.3.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.3.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.3.0.min.js\"];\n var css_urls = [];\n \n\n var inline_js = [\n function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\n function(Bokeh) {\n \n \n }\n ];\n\n function run_inline_js() {\n \n if (root.Bokeh !== undefined || force === true) {\n \n for (var i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n if (force === true) {\n display_loaded();\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n var cell = $(document.getElementById(\"1002\")).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));" }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "(function(root) {\n", " function embed_document(root) {\n", " \n", " var docs_json = {\"a414496b-b6a3-4c34-bfc9-df1c9daeb7ca\":{\"defs\":[{\"extends\":null,\"module\":null,\"name\":\"DataModel\",\"overrides\":[],\"properties\":[]}],\"roots\":{\"references\":[{\"attributes\":{\"children\":[{\"id\":\"1004\"},{\"id\":\"1005\"},{\"id\":\"1007\"}]},\"id\":\"1032\",\"type\":\"Column\"},{\"attributes\":{\"active_multi\":null,\"tools\":[{\"id\":\"1024\"}]},\"id\":\"1025\",\"type\":\"Toolbar\"},{\"attributes\":{},\"id\":\"1024\",\"type\":\"SaveTool\"},{\"attributes\":{},\"id\":\"1035\",\"type\":\"AllLabels\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1031\",\"type\":\"CDSView\"},{\"attributes\":{},\"id\":\"1037\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{\"end\":0.99,\"js_property_callbacks\":{\"change:value\":[{\"id\":\"1006\"}]},\"start\":0.01,\"step\":0.01,\"title\":\"duty ratio\",\"value\":0.5},\"id\":\"1004\",\"type\":\"Slider\"},{\"attributes\":{\"end\":1000,\"js_property_callbacks\":{\"change:value\":[{\"id\":\"1006\"}]},\"start\":100,\"title\":\"frequency (Hz)\",\"value\":490},\"id\":\"1005\",\"type\":\"Slider\"},{\"attributes\":{\"end\":0.025},\"id\":\"1008\",\"type\":\"Range1d\"},{\"attributes\":{},\"id\":\"1034\",\"type\":\"Title\"},{\"attributes\":{\"axis_label\":\"time (s)\",\"formatter\":{\"id\":\"1037\"},\"major_label_policy\":{\"id\":\"1035\"},\"ticker\":{\"id\":\"1017\"}},\"id\":\"1016\",\"type\":\"LinearAxis\"},{\"attributes\":{\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1028\"},\"hover_glyph\":null,\"muted_glyph\":null,\"nonselection_glyph\":{\"id\":\"1029\"},\"view\":{\"id\":\"1031\"}},\"id\":\"1030\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"below\":[{\"id\":\"1016\"}],\"center\":[{\"id\":\"1019\"},{\"id\":\"1023\"}],\"frame_height\":150,\"frame_width\":500,\"left\":[{\"id\":\"1020\"}],\"renderers\":[{\"id\":\"1030\"}],\"title\":{\"id\":\"1034\"},\"toolbar\":{\"id\":\"1025\"},\"x_range\":{\"id\":\"1008\"},\"x_scale\":{\"id\":\"1012\"},\"y_range\":{\"id\":\"1010\"},\"y_scale\":{\"id\":\"1014\"}},\"id\":\"1007\",\"subtype\":\"Figure\",\"type\":\"Plot\"},{\"attributes\":{\"line_color\":\"#1f77b4\",\"line_width\":2,\"x\":{\"field\":\"x\"},\"y\":{\"field\":\"y\"}},\"id\":\"1028\",\"type\":\"Line\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"#1f77b4\",\"line_width\":2,\"x\":{\"field\":\"x\"},\"y\":{\"field\":\"y\"}},\"id\":\"1029\",\"type\":\"Line\"},{\"attributes\":{},\"id\":\"1012\",\"type\":\"LinearScale\"},{\"attributes\":{},\"id\":\"1038\",\"type\":\"AllLabels\"},{\"attributes\":{},\"id\":\"1040\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{},\"id\":\"1014\",\"type\":\"LinearScale\"},{\"attributes\":{\"end\":5.25,\"start\":-0.25},\"id\":\"1010\",\"type\":\"Range1d\"},{\"attributes\":{\"args\":{\"duty_ratio_slider\":{\"id\":\"1004\"},\"freq_slider\":{\"id\":\"1005\"},\"source\":{\"id\":\"1003\"}},\"code\":\"\\n// Exract data from sliders\\nlet dutyRatio = duty_ratio_slider.value;\\nlet freq = freq_slider.value;\\nlet tmax = 0.025;\\n\\n// Compute period, duty ratio in units of seconds and number of periods\\nlet T = 1.0 / freq;\\nlet d = dutyRatio * T;\\nlet nPeriods = Math.ceil(tmax / T)\\n\\n// Update the data in the plot\\nlet x = [];\\nlet y = []\\nfor (let i = 0; i < nPeriods; i++) {\\n let iT = i * T;\\n x = x.concat([0 + iT, d + iT, d + iT, T + iT]);\\n y = y.concat([5, 5, 0, 0]);\\n}\\n\\nsource.data['x'] = x;\\nsource.data['y'] = y;\\n\\nsource.change.emit();\\n\"},\"id\":\"1006\",\"type\":\"CustomJS\"},{\"attributes\":{},\"id\":\"1041\",\"type\":\"UnionRenderers\"},{\"attributes\":{},\"id\":\"1017\",\"type\":\"BasicTicker\"},{\"attributes\":{},\"id\":\"1042\",\"type\":\"Selection\"},{\"attributes\":{\"axis\":{\"id\":\"1016\"},\"ticker\":null},\"id\":\"1019\",\"type\":\"Grid\"},{\"attributes\":{\"data\":{\"x\":{\"__ndarray__\":\"AAAAAAAAAADInSXs5rdQP8idJezmt1A/yJ0l7Oa3YD/InSXs5rdgP6xsOGLaE2k/rGw4YtoTaT/InSXs5rdwP8idJezmt3A/OgUvp+DldD86BS+n4OV0P6xsOGLaE3k/rGw4YtoTeT8e1EEd1EF9Px7UQR3UQX0/yJ0l7Oa3gD/InSXs5reAP4FRqsnjzoI/gVGqyePOgj86BS+n4OWEPzoFL6fg5YQ/87izhN38hj/zuLOE3fyGP6xsOGLaE4k/rGw4YtoTiT9lIL0/1yqLP2UgvT/XKos/HtRBHdRBjT8e1EEd1EGNP9eHxvrQWI8/14fG+tBYjz/InSXs5reQP8idJezmt5A/pPfnWmXDkT+k9+daZcORP4FRqsnjzpI/gVGqyePOkj9eq2w4YtqTP16rbDhi2pM/OgUvp+DllD86BS+n4OWUPxZf8RVf8ZU/Fl/xFV/xlT/zuLOE3fyWP/O4s4Td/JY/0BJ281sImD/QEnbzWwiYP6xsOGLaE5k/rGw4YtoTmT+IxvrQWB+aP4jG+tBYH5o/ZSC9P9cqmz8=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[52]},\"y\":[5,5,0,0,5,5,0,0,5,5,0,0,5,5,0,0,5,5,0,0,5,5,0,0,5,5,0,0,5,5,0,0,5,5,0,0,5,5,0,0,5,5,0,0,5,5,0,0,5,5,0,0]},\"selected\":{\"id\":\"1042\"},\"selection_policy\":{\"id\":\"1041\"}},\"id\":\"1003\",\"type\":\"ColumnDataSource\"},{\"attributes\":{\"axis_label\":\"voltage(V)\",\"formatter\":{\"id\":\"1040\"},\"major_label_policy\":{\"id\":\"1038\"},\"ticker\":{\"id\":\"1021\"}},\"id\":\"1020\",\"type\":\"LinearAxis\"},{\"attributes\":{\"axis\":{\"id\":\"1020\"},\"dimension\":1,\"ticker\":null},\"id\":\"1023\",\"type\":\"Grid\"},{\"attributes\":{},\"id\":\"1021\",\"type\":\"BasicTicker\"}],\"root_ids\":[\"1032\"]},\"title\":\"Bokeh Application\",\"version\":\"2.3.0\"}};\n", " var render_items = [{\"docid\":\"a414496b-b6a3-4c34-bfc9-df1c9daeb7ca\",\"root_ids\":[\"1032\"],\"roots\":{\"1032\":\"3c472f9c-2b14-423b-94e2-6b199d806ff7\"}}];\n", " root.Bokeh.embed.embed_items_notebook(docs_json, render_items);\n", "\n", " }\n", " if (root.Bokeh !== undefined) {\n", " embed_document(root);\n", " } else {\n", " var attempts = 0;\n", " var timer = setInterval(function(root) {\n", " if (root.Bokeh !== undefined) {\n", " clearInterval(timer);\n", " embed_document(root);\n", " } else {\n", " attempts++;\n", " if (attempts > 100) {\n", " clearInterval(timer);\n", " console.log(\"Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing\");\n", " }\n", " }\n", " }, 10, root)\n", " }\n", "})(window);" ], "application/vnd.bokehjs_exec.v0+json": "" }, "metadata": { "application/vnd.bokehjs_exec.v0+json": { "id": "1032" } }, "output_type": "display_data" } ], "source": [ "import numpy as np\n", "\n", "import bokeh.plotting\n", "import bokeh.io\n", "\n", "bokeh.io.output_notebook()\n", "\n", "tmax = 0.025\n", "\n", "\n", "def pwm_signal(duty_ratio, freq):\n", " T = 1 / freq\n", " d = duty_ratio * T\n", " n_periods = int(tmax / T) + 1\n", " x = np.concatenate([np.array([0, d, d, T]) + i * T for i in range(n_periods)])\n", " y = np.concatenate([np.array([5, 5, 0, 0])] * n_periods)\n", "\n", " return x, y\n", "\n", "\n", "x, y = pwm_signal(0.5, 490)\n", "source = bokeh.models.ColumnDataSource(dict(x=x, y=y))\n", "\n", "duty_ratio_slider = bokeh.models.Slider(\n", " start=0.01, end=0.99, value=0.5, step=0.01, title=\"duty ratio\"\n", ")\n", "freq_slider = bokeh.models.Slider(\n", " start=100, end=1000, value=490, step=1, title=\"frequency (Hz)\"\n", ")\n", "\n", "js_code = \"\"\"\n", "// Exract data from sliders\n", "let dutyRatio = duty_ratio_slider.value;\n", "let freq = freq_slider.value;\n", "let tmax = 0.025;\n", "\n", "// Compute period, duty ratio in units of seconds and number of periods\n", "let T = 1.0 / freq;\n", "let d = dutyRatio * T;\n", "let nPeriods = Math.ceil(tmax / T)\n", "\n", "// Update the data in the plot\n", "let x = [];\n", "let y = []\n", "for (let i = 0; i < nPeriods; i++) {\n", " let iT = i * T;\n", " x = x.concat([0 + iT, d + iT, d + iT, T + iT]);\n", " y = y.concat([5, 5, 0, 0]);\n", "}\n", "\n", "source.data['x'] = x;\n", "source.data['y'] = y;\n", "\n", "source.change.emit();\n", "\"\"\"\n", "\n", "callback = bokeh.models.CustomJS(\n", " args=dict(\n", " source=source, duty_ratio_slider=duty_ratio_slider, freq_slider=freq_slider\n", " ),\n", " code=js_code,\n", ")\n", "\n", "duty_ratio_slider.js_on_change(\"value\", callback)\n", "freq_slider.js_on_change(\"value\", callback)\n", "\n", "p = bokeh.plotting.figure(\n", " x_axis_label=\"time (s)\",\n", " y_axis_label=\"voltage(V)\",\n", " frame_height=150,\n", " frame_width=500,\n", " x_range=[0, tmax],\n", " y_range=[-0.25, 5.25],\n", " tools=\"save\",\n", ")\n", "\n", "p.line(source=source, x=\"x\", y=\"y\", line_width=2)\n", "\n", "bokeh.io.show(bokeh.layouts.column(duty_ratio_slider, freq_slider, p))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## PWM on the Arduino Uno with analogWrite()\n", "\n", "Given that the Arduino Uno does not have DAC, why does it have a function `analogWrite()`? The perhaps ill-named function actually invokes PWM from a digital pin. The `analogWrite()` function takes two arguments: the pin you are using for PWM and the duty ratio. The duty ratio is not given as a fraction, but rather an `int`. Although this argument must be given as an `int`, its minimum value is zero, corresponding to a duty ratio of zero, and its maximum value is 255, corresponding to a duty ratio of one. So, if you wanted a duty ratio of $d$, you can compute the number you want to pass to `analogWrite()` as $255 d$. Here is an example of telling pin 3 to perform PWM with a duty ratio of approximately 0.5.\n", "\n", "```arduino\n", "analogWrite(3, 127);\n", "```\n", "\n", "Note that not all digital pins can do PWM. This is because PWM requires use of the various timers available on Arduino, and a maximum of six pins can be performing PWM at a time. Those that can do PWM are marked with a tilde next to them on the Arduino Uno. Because they use different timers, they have different frequencies.\n", "\n", "| Pin | Frequency (Hz)|\n", "| ------------- |:-------------:|\n", "| 3 | 490 |\n", "| 5 | 980 |\n", "| 6 | 980 |\n", "| 9 | 490 |\n", "| 10 | 490 |\n", "| 11 | 490 |\n", "\n", "Note that when PWM is invoked on a pin, its use of the time could affect other functions. Specifically, pins 3 and 11 use Timer2, which is also used for `tone()`. When `tone()` is being used, it may interfere with PWM on pins 3 and 11.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Follow-along exercise 4: PWM demo\n", "\n", "To demonstrate how the Arduino Uno does PWM in lieu of an [oscilloscope](https://en.wikipedia.org/wiki/Oscilloscope), you can watch the voltage sent out of a digital pin by reading its output via ADC. The set-up couldn't be simpler. Connect digital pin 3 to analog input A0.\n", "\n", "
\n", " \n", "![PWM demo setup](pwm_demo_schem.svg)\n", " \n", "
\n", "\n", "We will do PWM on pin 3 and then immediately read in the signal on pin A0. We will send the reading along the serial connection as fast as we can (which is 2,000,000 baud).\n", "\n", "```arduino\n", "const int sensorPin = A0;\n", "const int outPin = 3;\n", "\n", "void setup() {\n", " pinMode(outPin, OUTPUT);\n", "\n", " Serial.begin(2000000);\n", " analogWrite(outPin, 127);\n", "}\n", "\n", "void loop() {\n", " Serial.println(analogRead(sensorPin));\n", "}\n", "```\n", "\n", "To visualize the signal, you can either use serial-dashboard or the Serial Plotter of the Arduino IDE. In either case, be sure to set the baud rate to 2,000,000. If you do use serial-dashboard, note that you may fill up RAM as you watch the signal, since serial-dashboard aims to store whatever you record, while the Serial Plotter will dump values not displayed.\n", "\n", "Go ahead and upload this sketch and watch the plot to see the actual voltage signals coming out of pin 3.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Many devices, especially mechanical devices like motors, do not react as quickly as the rate at which the voltage switches on and off during PWM. This means that PWM can be an effective means to throttle power to a device.\n", "\n", "Even when some devices can react as fast as the PWM, when *perception* of the device is more important, the device would be perceived as having throttled voltage. This is the case for LEDs. By flipping the input voltage on and off via PWM, an LED can appear brighter or dimmer, depending on the duty ratio. Let's put that idea to work." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Do-it-yourself exercise 3: Brightness control by PWM\n", "\n", "Connect a red LED to a pin capable of PWM. Don't forget to use a 220 Ω resistor so you do not fry your LED. Write a sketch that gradually brings the brightness of the LED up and down, up and down, up and down, ad infinitum.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Computing environment" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPython 3.8.8\n", "IPython 7.21.0\n", "\n", "numpy 1.19.2\n", "bokeh 2.3.0\n", "jupyterlab 3.0.11\n" ] } ], "source": [ "%load_ext watermark\n", "%watermark -v -p numpy,bokeh,jupyterlab" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.11" } }, "nbformat": 4, "nbformat_minor": 4 }