1059 lines
36 KiB
Text
1059 lines
36 KiB
Text
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2021-01-13T18:54:58.722373Z",
|
|
"start_time": "2021-01-13T18:54:57.178438Z"
|
|
}
|
|
},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Populating the interactive namespace from numpy and matplotlib\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%pylab inline"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Notebook magic"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 4,
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2021-01-13T18:57:41.555892Z",
|
|
"start_time": "2021-01-13T18:57:41.551121Z"
|
|
}
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
|
|
"from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
|
|
"from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
|
|
"import subprocess\n",
|
|
"import os"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 5,
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2021-01-13T18:57:42.313231Z",
|
|
"start_time": "2021-01-13T18:57:42.288402Z"
|
|
}
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"@magics_class\n",
|
|
"class PyboardMagic(Magics):\n",
|
|
" @cell_magic\n",
|
|
" @magic_arguments()\n",
|
|
" @argument('-skip')\n",
|
|
" @argument('-unix')\n",
|
|
" @argument('-pyboard')\n",
|
|
" @argument('-file')\n",
|
|
" @argument('-data')\n",
|
|
" @argument('-time')\n",
|
|
" @argument('-memory')\n",
|
|
" def micropython(self, line='', cell=None):\n",
|
|
" args = parse_argstring(self.micropython, line)\n",
|
|
" if args.skip: # doesn't care about the cell's content\n",
|
|
" print('skipped execution')\n",
|
|
" return None # do not parse the rest\n",
|
|
" if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
|
|
" with open('/dev/shm/micropython.py', 'w') as fout:\n",
|
|
" fout.write(cell)\n",
|
|
" proc = subprocess.Popen([\"../micropython/ports/unix/build-2/micropython-2\", \"/dev/shm/micropython.py\"], \n",
|
|
" stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
|
|
" print(proc.stdout.read().decode(\"utf-8\"))\n",
|
|
" print(proc.stderr.read().decode(\"utf-8\"))\n",
|
|
" return None\n",
|
|
" if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
|
|
" spaces = \" \"\n",
|
|
" try:\n",
|
|
" with open(args.file, 'w') as fout:\n",
|
|
" fout.write(cell.replace('\\t', spaces))\n",
|
|
" printf('written cell to {}'.format(args.file))\n",
|
|
" except:\n",
|
|
" print('Failed to write to disc!')\n",
|
|
" return None # do not parse the rest\n",
|
|
" if args.data: # can be used to load data from the pyboard directly into kernel space\n",
|
|
" message = pyb.exec(cell)\n",
|
|
" if len(message) == 0:\n",
|
|
" print('pyboard >>>')\n",
|
|
" else:\n",
|
|
" print(message.decode('utf-8'))\n",
|
|
" # register new variable in user namespace\n",
|
|
" self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
|
|
" \n",
|
|
" if args.time: # measures the time of executions\n",
|
|
" pyb.exec('import utime')\n",
|
|
" message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
|
|
" \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
|
|
" print(message.decode('utf-8'))\n",
|
|
" \n",
|
|
" if args.memory: # prints out memory information \n",
|
|
" message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
|
|
" print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
|
|
" message = pyb.exec(cell)\n",
|
|
" print(\">>> \", message.decode('utf-8'))\n",
|
|
" message = pyb.exec('print(mem_info())')\n",
|
|
" print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
|
|
"\n",
|
|
" if args.pyboard:\n",
|
|
" message = pyb.exec(cell)\n",
|
|
" print(message.decode('utf-8'))\n",
|
|
"\n",
|
|
"ip = get_ipython()\n",
|
|
"ip.register_magics(PyboardMagic)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## pyboard"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 57,
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2020-05-07T07:35:35.126401Z",
|
|
"start_time": "2020-05-07T07:35:35.105824Z"
|
|
}
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"import pyboard\n",
|
|
"pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
|
|
"pyb.enter_raw_repl()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 9,
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2020-05-19T19:11:18.145548Z",
|
|
"start_time": "2020-05-19T19:11:18.137468Z"
|
|
}
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"pyb.exit_raw_repl()\n",
|
|
"pyb.close()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2020-05-07T07:35:38.725924Z",
|
|
"start_time": "2020-05-07T07:35:38.645488Z"
|
|
}
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"%%micropython -pyboard 1\n",
|
|
"\n",
|
|
"import utime\n",
|
|
"import ulab as np\n",
|
|
"\n",
|
|
"def timeit(n=1000):\n",
|
|
" def wrapper(f, *args, **kwargs):\n",
|
|
" func_name = str(f).split(' ')[1]\n",
|
|
" def new_func(*args, **kwargs):\n",
|
|
" run_times = np.zeros(n, dtype=np.uint16)\n",
|
|
" for i in range(n):\n",
|
|
" t = utime.ticks_us()\n",
|
|
" result = f(*args, **kwargs)\n",
|
|
" run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
|
|
" print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
|
|
" print('\\tbest: %d us'%np.min(run_times))\n",
|
|
" print('\\tworst: %d us'%np.max(run_times))\n",
|
|
" print('\\taverage: %d us'%np.mean(run_times))\n",
|
|
" print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
|
|
" return result\n",
|
|
" return new_func\n",
|
|
" return wrapper\n",
|
|
"\n",
|
|
"def timeit(f, *args, **kwargs):\n",
|
|
" func_name = str(f).split(' ')[1]\n",
|
|
" def new_func(*args, **kwargs):\n",
|
|
" t = utime.ticks_us()\n",
|
|
" result = f(*args, **kwargs)\n",
|
|
" print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
|
|
" return result\n",
|
|
" return new_func"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"__END_OF_DEFS__"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# PID module\n",
|
|
"\n",
|
|
"`ulab` provides a module with a generic and fast implementation of the proportional-integral-derivative ([PID](https://en.wikipedia.org/wiki/Proportional%E2%80%93integral%E2%80%93derivative_controller)) controller. While it is relatively easy to write a PID controller in `python`, the problem with that approach is execution speed: even in thermal applications, where the time scales are not particularly short, running more than one controller simultanously might be difficult, but if the goal is e.g. to mechanically control a self-balancing robot, the task will prove outright impossible. The reason for this slowness of the code execution is the number of floating point operations, where the type-agnostic nature of `python` requires constant type conversion.\n",
|
|
"\n",
|
|
"`ulab`'s `PID` module implements the controller in C, and for very time-critical applications, it allows the user to take data directly from the measurement\n",
|
|
"device (most probably an analogue-to-digital converter), if it supports the buffer protocol. Likewise, data can be output directly to the physical device (in most cases a digital-to-analogue converter), if it can take data from a buffer. This direct access to the device data eliminates the need for data conversion in `python`, and reduces the number of objects that must be passed to the controller loop. In fact, in most cases the controller can be run without moving any data whatsoever, simply by calling a method of the `PID` instance without arguments."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Background\n",
|
|
"\n",
|
|
"In general, running a controller loop requires the following steps:\n",
|
|
"\n",
|
|
"1. Define desired set point\n",
|
|
"1. Measure the current value of the quantity that is to be controlled\n",
|
|
"1. Calculate the error, the difference between set point and current value\n",
|
|
"1. Based on the error's value, calculate the output; this is where the control loop itself is defined\n",
|
|
"1. Place the calculated output on a physical output device\n",
|
|
"1. Repeat indefinitely\n",
|
|
"\n",
|
|
"While the description above seems innocuous, steps 2-5 might require considerable resources, when implemented in an interpreted language."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Measuring the value of a physical quantity\n",
|
|
"\n",
|
|
"The quantity that a physical device can measure is not necessarily the quantity that we are interested in, which means that the measured quantity has to undergo some conversion steps before it can be used in the controller loop. \n",
|
|
"\n",
|
|
"Take the example of temperature measurements: while there are digital thermometers that will output the temperature directly, in most cases, the temperature is only inferred from a voltage. In the simplest case, a thermistor will form half of a voltage divider connected to a reference voltage $U_{ref}$, and the potential $U_{meas}$ is measured at mid-point. It is obvious that even if the thermistors temperature-resistance relationship $R_{th}(T)$ were to be linear, the measured potential will not be (if the thermistor is in the lower half of the divider, $R$ and $R_{th}$ will have to be swapped in the expression below, but that does not alter the fact that $U_{meas}$ is non-linear in $R_{th}$):\n",
|
|
"\n",
|
|
"\\begin{equation}\n",
|
|
"U_{meas} = U_{ref}\\frac{R}{R + R_{th}(T)}\n",
|
|
"\\end{equation}\n",
|
|
"\n",
|
|
"Most thermistors will have a highly non-linear characteristic curve, thus, converting the potential values to temperature becomes non-trivial, and in the most general case, the conversion must be calculated from the expression above, by inverting $R_{th}(T)$. We can note, however, that any well-behaved function can be expanded in a Taylor series around a point, where the series converges, hence the temperature can be expressed as \n",
|
|
"\n",
|
|
"\\begin{equation}\n",
|
|
"T = f(U_{meas}) = \n",
|
|
"f(U_0) + \n",
|
|
"\\left. \\frac{df}{dU} \\right|_{U_0} (U_{meas} - U_0) + \n",
|
|
"\\left. \\frac{1}{2}\\frac{d^2f}{dU^2} \\right|_{U_0} (U_{meas} - U_0)^2 + \n",
|
|
"\\left. \\frac{1}{6}\\frac{d^3f}{dU^3} \\right|_{U_0} (U_{meas} - U_0)^3 + ...\n",
|
|
"\\end{equation}\n",
|
|
"where the differentials are evaluated at $U_0$.\n",
|
|
"\n",
|
|
"\n",
|
|
"The coefficients of the expansion are determined by the parameters of the physical system in question, as well as $U_0$. Also note that higher-order expansions are inherently more accurate, but take more time to calculate. Usually, a second-order expansion, or even a linear one is enough, if the expected deviations from $U_0$ are not too large. However, for obvious reasons, the series representation of $f(U_{meas})$ must be monotonic in the domain of interest.\n",
|
|
"\n",
|
|
"As pointed out, what is actually of interest to us is not the measured values as a function of the environmental value, but the inverse function: we want to know what the environmental value was, if we happen to measure a potential. In the most general case, finding the Taylor series expansion of the inverse function is highly non-trivial. Ben Coleman gives a very good exposition of the [subject](https://randorithms.com/2021/08/31/Taylor-Series-Inverse.html). But it might not be required to go down that path: given a small number of values of the $T(U_{meas})$ relationship, we might get away with a polynomial fit, and use that in lieu of the Taylor series. This must be done in terms of $(U_{meas} - U_0)$, however."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Converting the digital control values to physical control signals\n",
|
|
"\n",
|
|
"The comments on converting measured values to physically relevant quantities apply, when we want to convert calculated values to physially relevant control signals, i.e., when at the end of the control loop, we want to use the calculated signal to influence the physical drive mechanism: the calculated digital value will be converted to a current, potential etc. by means of a digital-to-analogue converter (DAC), and some analogue circuitry that takes the analogue output potential of the DAC as input, and generates a current, another potential as output, or turns a valve. The relationship between the input potential and the physical control quantity is not necessarily the identity, or even linear, thus, in the general case, we have to express the conversion function as \n",
|
|
"\n",
|
|
"\\begin{equation}\n",
|
|
"I_{control} = g(U_{control}) = \n",
|
|
"g(U^*) + \n",
|
|
"\\left. \\frac{dg}{dU} \\right|_{U^*} (U_{control} - U^*) + \n",
|
|
"\\left. \\frac{1}{2}\\frac{d^2g}{dU^2} \\right|_{U^*} (U_{control} - U^*)^2 + \n",
|
|
"\\left. \\frac{1}{6}\\frac{d^3g}{dU^3} \\right|_{U^*} (U_{control} - U^*)^3 + ...\n",
|
|
"\\end{equation}\n",
|
|
"where the control output is expanded around the potential $U^*$. \n",
|
|
"\n",
|
|
"The coefficients of the expansion are determined by the parameters of the physical system in question, as well as $U^*$. Also note that higher-order expansions are inherently more accurate, but take more time to calculate. Usually, a second-order expansion, or even a linear one is enough, if the expected deviations from $U^*$ are not too large. Again, we do not necessarily have to deal with the Taylor series, and it might suffice to use a polynomial fit to some pre-calculated values. If that is the case, the expansion must be in terms of $(U_{control} - U^*)$."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Input-output buffers\n",
|
|
"\n",
|
|
"Conversion between analogue and digital values works not with floating point numbers, which are most probably the results of the controller algorithm, but with integers that are transmitted to/from the converter. A typical transmission sequence will, in general, contain not only the data, but some auxiliary bits, like status, channel number, gain, etc., as shown below. The input/output string $S$ consists of $n + N + m$ bits, where the *D*s are the data bits with the digital values, while the *A*s are the auxiliary bits that are important for the functioning of the converter, but carry no numerical information. The data relevant to the numerical control algorithm are embedded in a string, whose auxiliary bits are fixed, and do not change from transmission to transmission.\n",
|
|
"\n",
|
|
"\\begin{equation}\n",
|
|
"S = \\overbrace{A_n A_{n - 1} ... A_1 A_0}^{\\mathrm{auxiliary\\ bits}}\n",
|
|
"\\overbrace{D_N D_{N - 1} ... D_1 D_0}^{\\mathrm{data\\ bits}} \n",
|
|
"\\overbrace{A_m A_{m - 1} ... A_1 A_0}^{\\mathrm{auxiliary\\ bits}}\n",
|
|
"\\end{equation}\n",
|
|
"\n",
|
|
"In order to make data exchange between the control algorithm and the peripheral devices transparent and seamless, the `PID` module implements input/output buffers, which not only place the relevant bits at the correct positions in pre-defined strings automatically, but also take care of the conversion of data bits to floating point numbers and vica versa.\n",
|
|
"\n",
|
|
"Since we are not interested in the sequences $A_n A_{n - 1} ... A_1 A_0$, and $A_m A_{m - 1} ... A_1 A_0$, the data can be recovered by shifting its bits to the right by so many places that $D_0$ reaches a byte boundary, and then masking the result by $2^N - 1$ (this is equivalent to $N$ 1s). In the code, $n$ will be referred to as the `offset`, while $N$ as the `bitdepth`. The exact values should be taken from the data sheets of the ADC or DAC that are used as the interface devices."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Input-output data converters\n",
|
|
"\n",
|
|
"We have seen above how the numbers in the control algorithm can be interfaced with the outside world. To facilitate the convension, `ulab`'s `PID` module defines input-output data buffers, which hold an input/output buffer as described in the previous section, and the coefficients of the Taylor series expansions, i.e., all information required to turn a string of bits into a floating point number that is the true represenation of the value that we are interested in."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Methods \n",
|
|
"\n",
|
|
"The module implements a class with the following methods\n",
|
|
"\n",
|
|
"1. [reset](#reset)\n",
|
|
"1. [step](#step)\n",
|
|
"1. [float_step](#float_step)\n",
|
|
"\n",
|
|
"Parameters and some internal values can be accessed via the following attributes of an instance:\n",
|
|
"\n",
|
|
"1. [P](#P)\n",
|
|
"1. [I](#I)\n",
|
|
"1. [D](#D)\n",
|
|
"1. [setpoint](#setpoint)\n",
|
|
"1. [value](#value)\n",
|
|
"1. [input/output](#buffers)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Instantiating the PID object\n",
|
|
"\n",
|
|
"The controller can be instantiated by importing the `PID` module, and calling the constructor without arguments. At this point, the object will hold all values required for the operation of the PID loop, and some of these can be displayed by simply printing the controller object: "
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 17,
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2021-01-13T19:06:54.640444Z",
|
|
"start_time": "2021-01-13T19:06:54.623467Z"
|
|
}
|
|
},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"PID object at 0x7f94172053a0\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"print(pid)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### reset\n",
|
|
"\n",
|
|
"This method takes no arguments, and resets the controller, i.e., zeroes the time, the number of steps, and the integral part."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 45,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"initial state of controller\n",
|
|
"value: 0.0, steps: 0\n",
|
|
"\n",
|
|
"stepping the controller 10 times\n",
|
|
"value: 0.3, out: 1.178690476190476, steps: 10\n",
|
|
"\n",
|
|
"resetting controller\n",
|
|
"value: 0.0, out: 0.0, steps: 0\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"pid.P = 1.0\n",
|
|
"pid.I = 0.1\n",
|
|
"pid.setpoint = 0.0\n",
|
|
"\n",
|
|
"print('initial state of controller')\n",
|
|
"print(f'value: {pid.value}, steps: {pid.steps}')\n",
|
|
"\n",
|
|
"print()\n",
|
|
"print('stepping the controller 10 times')\n",
|
|
"for i in range(10):\n",
|
|
" pid.float_step(3.0 / (i + 1))\n",
|
|
"\n",
|
|
"print(f'value: {pid.value}, out: {pid.out}, steps: {pid.steps}')\n",
|
|
"\n",
|
|
"print()\n",
|
|
"print('resetting controller')\n",
|
|
"pid.reset()\n",
|
|
"print(f'value: {pid.value}, out: {pid.out}, steps: {pid.steps}')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### step\n",
|
|
"\n",
|
|
"This method takes no arguments, and steps the controller, i.e., takes and converts the data in the inpput buffer, calculates the three terms of the controller's output, and places the data on the output buffer. At the same time, the `value`, `steps`, and `integral` attributes of the controller are updated. Time differences are measured in units of ms; keep this in mind, when adjusting the values of `P`, `I`, and `D`! "
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 58,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"None\n",
|
|
"value: 0.0, out: 0.0\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"\n",
|
|
"# initialise the input/output buffers with default values\n",
|
|
"pid.input.init()\n",
|
|
"pid.output.init()\n",
|
|
"\n",
|
|
"pid.P = 1.0\n",
|
|
"pid.D = 1.0\n",
|
|
"\n",
|
|
"pid.step()\n",
|
|
"print(f'value: {pid.value}, out: {pid.out}')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### float_step\n",
|
|
"\n",
|
|
"The method takes one, or two arguments, and returns the computed control value. The first argument is the current value of the signal to be controlled, while the time can be supplied in the optional second argument. If that is missing, then a time increment of 1.0 is taken as default. As with [step](#step), all internal variables are updated. Note that since the time difference can be supplied by the user, the units are not fixed, i.e., the values of `P`, `I`, and `D` can arbitrarily chosen."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 55,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"P: 1.0, I: 0.0, D: 1.0\n",
|
|
"2.0\n",
|
|
"0.75\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"pid.P = 1.0\n",
|
|
"pid.D = 1.0\n",
|
|
"print(f'P: {pid.P}, I: {pid.I}, D: {pid.D}')\n",
|
|
"\n",
|
|
"print(pid.float_step(1.0))\n",
|
|
"print(pid.float_step(2.0, 0.2))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### P\n",
|
|
"\n",
|
|
"This attribute *gets* or *sets* the constant in front of the term proportinal to the error. In the first case, it returns a floating point number, in the second case, a floating point number can be assigned to it."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 40,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"P: 0.0\n",
|
|
"P: 13.0\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"print(f'P: {pid.P}')\n",
|
|
"pid.P = 13.0\n",
|
|
"print(f'P: {pid.P}')\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### I\n",
|
|
"\n",
|
|
"This attribute *gets* or *sets* the constant in front of the term proportinal to the integral of the error. In the first case, it returns a floating point number, in the second case, a floating point number can be assigned to it."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 43,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"I: 0.0\n",
|
|
"I: 1.0\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"print(f'I: {pid.I}')\n",
|
|
"pid.I = 1.0\n",
|
|
"print(f'I: {pid.I}')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### D\n",
|
|
"\n",
|
|
"This attribute *gets* or *sets* the constant in front of the term proportinal to the derivative of the error. In the first case, it returns a floating point number, in the second case, a floating point number can be assigned to it."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 44,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"D: 0.0\n",
|
|
"D: 0.5\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"print(f'D: {pid.D}')\n",
|
|
"pid.D = 0.5\n",
|
|
"print(f'D: {pid.D}')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Buffers\n",
|
|
"\n",
|
|
"As mentioned above, the controller interfaces with external devices through its input/output buffers. Internally, these hold the parameters of the Taylor expansion, and everything required to turn the bits of the binary stream of the ADC to floating point numbers, and to turn the floating point numbers into the binary stream consumed by the DAC.\n",
|
|
"\n",
|
|
"The `PID` instance' `input` and `output` attributes return a `buffer` object that has the method\n",
|
|
"\n",
|
|
"1. [evaluate](#evaluate)\n",
|
|
"1. [init](#init)\n",
|
|
"\n",
|
|
"and the following attributes:\n",
|
|
"\n",
|
|
"1. [bitdepth](#bitdepth)\n",
|
|
"1. [bytes](#bytes)\n",
|
|
"1. [coeffs](#coeffs)\n",
|
|
"1. [mask](#mask)\n",
|
|
"1. [offset](#offset)\n",
|
|
"1. [order](#order)\n",
|
|
"1. [x0](#x0)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### evaluate\n",
|
|
"\n",
|
|
"This method evaluates the series expansion at the argument `x - x0`, and returns a floating point number with the computed value."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 54,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"coefficients: (1.0, 0.0), x0: 0.0\n",
|
|
"value at x = 10.0: 10.0\n",
|
|
"\n",
|
|
"coefficients: (1.0, 2.0, 3.0), x0: 1.0\n",
|
|
"value at x = 10.0: 102.0\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"pid.input.init()\n",
|
|
"# y = x, x0 = 0.0, default\n",
|
|
"print(f'coefficients: {pid.input.coeffs}, x0: {pid.input.x0}')\n",
|
|
"print(f'value at x = 10.0: {pid.input.evaluate(10.0)}')\n",
|
|
"\n",
|
|
"print()\n",
|
|
"pid.input.x0 = 1.0\n",
|
|
"# y = (x - x0) ** 2 + 2 * (x - x0) + 3.0\n",
|
|
"pid.input.coeffs = (1, 2, 3)\n",
|
|
"print(f'coefficients: {pid.input.coeffs}, x0: {pid.input.x0}')\n",
|
|
"# x - x0 = 9 -> y = 81 + 2 * 9 + 3\n",
|
|
"print(f'value at x = 10.0: {pid.input.evaluate(10.0)}')\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### init\n",
|
|
"\n",
|
|
"The method initialises the input's ADC or the output's DAC buffer with default values, i.e., it sets [x0](#x0) to 0.0, and [coeffs](#coeffs) to the identity, i.e., to the array `[1.0, 0.0]`."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 26,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"x0: 0.0, order: 0, coefficients: ()\n",
|
|
"x0: 1.0, order: 2, coefficients: (1.0, 0.0)\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"print(f'x0: {pid.input.x0}, order: {pid.input.order}, coefficients: {pid.input.coeffs}')\n",
|
|
"pid.input.init()\n",
|
|
"print(f'x0: {pid.input.x0}, order: {pid.input.order}, coefficients: {pid.input.coeffs}')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### bitdepth\n",
|
|
"\n",
|
|
"This attribute *gets* or *sets* the bit depth of the input's ADC or the output's DAC. Note that setting the value of the attribute does not communicate with the ADC or DAC, it simply changes how the bits in the input and output buffers are interpreted. If the attribute is used as a getter, it returns the currently active bitdepth setting for that buffer. If an unsigned integer is assigned to the attribute (i.e., when it is a *setter*) the number of bits taken by the data in the buffer will be set. "
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 8,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"initial bit depth: 0\n",
|
|
"new bit depth: 12\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"print('initial bit depth: ', pid.input.bitdepth)\n",
|
|
"pid.input.bitdepth = 12\n",
|
|
"print('new bit depth: ', pid.input.bitdepth)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### bytes\n",
|
|
"\n",
|
|
"This attribute *gets* or *sets* the byte array of the input's ADC or the output's DAC. Note that setting the value of the attribute does not communicate with the ADC or DAC, it simply changes the pointer to the memory segment. If the attribute is used as a getter, it returns the currently active byte array setting for that buffer. If a byte array is assigned to the attribute (i.e., when it is a *setter*) that byte array will be set to the corresponding internal variable."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 9,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"bytearray: bytearray(b'\\x01\\x02\\x03\\x04')\n",
|
|
"bytearray in PID object: bytearray(b'\\x01\\x02\\x03\\x04')\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"buffer = bytearray([1, 2, 3, 4])\n",
|
|
"print('bytearray: ', buffer)\n",
|
|
"pid.input.bytes = buffer\n",
|
|
"print('bytearray in PID object: ', pid.input.bytes)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### coeffs\n",
|
|
"\n",
|
|
"This attribute *gets* or *sets* the coefficients of the Taylor series expansion. Without assignment, the attribute returns a tuple of floating point numbers in descending order of the powers. The attribute can be used as a *setter*, when a generic `micropython` interable is assigned to it. In addition to converting the items in the iterable to floating point numbers, the setter also assigns the internal variable [order](#order)."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 10,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"coefficients: (1.0, 2.0, 3.0)\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"pid.input.coeffs = [1, 2, 3]\n",
|
|
"print('coefficients: ', pid.input.coeffs)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### mask\n",
|
|
"\n",
|
|
"This attribute only *gets* the computed internal variable `mask`, which is updated, whenever the [bitdepth](#bitdepth) is set."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 13,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"bit depth: 12\n",
|
|
"mask: 4095\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"pid.input.bitdepth = 12\n",
|
|
"print('bit depth: ', pid.input.bitdepth)\n",
|
|
"print('mask: ', pid.input.mask)\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### offset\n",
|
|
"\n",
|
|
"This attribute *gets* or *sets* the offset of the input's ADC or the output's DAC stream. Note that setting the value of the attribute does not communicate with the ADC or DAC, it simply changes the pointer to the memory segment. f the attribute is used as a getter, it returns the currently active offset setting for that buffer. If an unsigned integer is assigned to the attribute (i.e., when it is a *setter*) the buffer's internal offset variable will be set."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 14,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"offset: 0\n",
|
|
"new offset: 12\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"print('offset: ', pid.input.offset)\n",
|
|
"pid.input.offset = 12\n",
|
|
"print('new offset: ', pid.input.offset)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### order\n",
|
|
"\n",
|
|
"This attribute only *gets* the computed internal variable `order`, which is inferred from the length of the Taylor series expansion's setting in [coeffs](#coeffs)."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 15,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"coefficients: (1.0, 2.0, 3.0)\n",
|
|
"expansion order: 3\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"pid.input.coeffs = [1, 2, 3]\n",
|
|
"print('coefficients: ', pid.input.coeffs)\n",
|
|
"print('expansion order: ', pid.input.order)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### x0\n",
|
|
"\n",
|
|
"This attribute *gets* or *sets* the point around which the ADC's input, or the DAC's output is expanded. If the attribute is used as a getter, it returns the current value, while if a floating point number is assigned to it (i.e., when it is a *setter*) expansion point will be set."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 16,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"x0: 0.0\n",
|
|
"new x0: 123.0\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%%micropython -unix 1\n",
|
|
"\n",
|
|
"from ulab import PID\n",
|
|
"\n",
|
|
"pid = PID.PID()\n",
|
|
"print('x0: ', pid.input.x0)\n",
|
|
"pid.input.x0 = 123.0\n",
|
|
"print('new x0: ', pid.input.x0)"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"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.9.13"
|
|
},
|
|
"toc": {
|
|
"base_numbering": 1,
|
|
"nav_menu": {},
|
|
"number_sections": true,
|
|
"sideBar": true,
|
|
"skip_h1_title": false,
|
|
"title_cell": "Table of Contents",
|
|
"title_sidebar": "Contents",
|
|
"toc_cell": false,
|
|
"toc_position": {
|
|
"height": "calc(100% - 180px)",
|
|
"left": "10px",
|
|
"top": "150px",
|
|
"width": "382.797px"
|
|
},
|
|
"toc_section_display": true,
|
|
"toc_window_display": true
|
|
},
|
|
"varInspector": {
|
|
"cols": {
|
|
"lenName": 16,
|
|
"lenType": 16,
|
|
"lenVar": 40
|
|
},
|
|
"kernels_config": {
|
|
"python": {
|
|
"delete_cmd_postfix": "",
|
|
"delete_cmd_prefix": "del ",
|
|
"library": "var_list.py",
|
|
"varRefreshCmd": "print(var_dic_list())"
|
|
},
|
|
"r": {
|
|
"delete_cmd_postfix": ") ",
|
|
"delete_cmd_prefix": "rm(",
|
|
"library": "var_list.r",
|
|
"varRefreshCmd": "cat(var_dic_list()) "
|
|
}
|
|
},
|
|
"types_to_exclude": [
|
|
"module",
|
|
"function",
|
|
"builtin_function_or_method",
|
|
"instance",
|
|
"_Feature"
|
|
],
|
|
"window_display": false
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4
|
|
}
|