{"id":13802353,"url":"https://github.com/peterhinch/micropython-fourier","last_synced_at":"2025-06-10T08:35:26.444Z","repository":{"id":30297729,"uuid":"33849405","full_name":"peterhinch/micropython-fourier","owner":"peterhinch","description":"Fast Fourier transform in MicroPython's inline ARM assembler.","archived":false,"fork":false,"pushed_at":"2024-08-17T16:51:37.000Z","size":145,"stargazers_count":85,"open_issues_count":0,"forks_count":13,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-04-05T09:01:54.457Z","etag":null,"topics":["assembler","dft","embedded","micropython"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"databricks/spark-deep-learning","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/peterhinch.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-04-13T05:23:39.000Z","updated_at":"2025-04-02T16:11:27.000Z","dependencies_parsed_at":"2024-08-17T18:06:16.900Z","dependency_job_id":null,"html_url":"https://github.com/peterhinch/micropython-fourier","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterhinch%2Fmicropython-fourier","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterhinch%2Fmicropython-fourier/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterhinch%2Fmicropython-fourier/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterhinch%2Fmicropython-fourier/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/peterhinch","download_url":"https://codeload.github.com/peterhinch/micropython-fourier/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterhinch%2Fmicropython-fourier/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259039156,"owners_count":22796767,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["assembler","dft","embedded","micropython"],"created_at":"2024-08-04T00:01:42.576Z","updated_at":"2025-06-10T08:35:26.398Z","avatar_url":"https://github.com/peterhinch.png","language":"Python","funding_links":[],"categories":["Libraries"],"sub_categories":["Mathematics"],"readme":"Single precision FFT written in ARM assembler\n=============================================\n\nV0.52 6th Oct 2019  \nAuthor: Peter Hinch  \nRequires: ARM platform with FPU supporting Arm Thumb V7 assembler. (e.g.\nPyboard 1.x, Pyboard D, Pico 2). Any firmware version dated 2018 or later.  \n\n# Contents\n\n 1. [Overview](./README.md#1-overview)  \n  1.1 [The Pico 2](./README.md#11-the-pico-2)  \n 2. [Design](./README.md#2-design)  \n  2.1 [Future development](./README.md#21-future-development)  \n 3. [Getting Started](./README.md#3-getting-started)  \n 4. [The DFT class](./README.md#4-the-dft-class)  \n  4.1 [Conversion types](./README.md#41-conversion-types)  \n  4.2 [The populate function](./README.md#42-the-populate-function)  \n  4.3 [The window function](./README.md#43-the-window-function)  \n  4.4 [FORWARD transform](./README.md#44-forward-transform)  \n  4.5 [REVERSE transform](./README.md#45-reverse-transform)  \n  4.6 [POLAR transform](./README.md#46-polar-transform)  \n  4.7 [DB transform](./README.md#47-db-transform)  \n 5. [The DFTADC class](./README.md#5-the-dftadc-class)  \n 6. [Implementation](./README.md#6-implementation)  \n 7. [Note for beginners](./README.md#7-note-for-beginners)  \n 8. [Performance](./README.md#8-performance)  \n 9. [Whimsical observations](./README.md#9-whimsical-observations)  \n\n# 1. Overview\n\nThe `DFT` class is intended to perform a fast discrete fourier transform on an\narray of data typically received from a sensor. Apart from initialisation most\nof the code is written in ARM assembler for speed. It uses the floating point\ncoprocessor and does not allocate heap storage: it can therefore be called from\na MicroPython interrupt handler. See [section 8](./README.md#8-performance) for\nbenchmark results on the various Pyboard options.\n\nThe DFT is performed using the Cooley-Tukey algorithm. This requires arrays of\nlength 2**N where N is an integer. The \"twiddle factors\" are precomputed in\nPython. The algorithm performs the conversion in place to minimise RAM usage\n(i.e. the results appear in the same arrays as the source data).\n\nInverse transforms and fast Cartesian to polar conversion are supported, as is\nthe use of window functions. There is an option to convert polar magnitudes to\ndB.\n\nThe principal purpose of this library is for processing signals acquired from a\nPyboard ADC. Features are targeted at typical engineering applications. It can\nbe used with arbitrary data in other applications, but such users may also want\nto consider [ulab](https://github.com/v923z/micropython-ulab.git) which is a\n\"micro\" version of NumPy implemented as a C module.\n\n## 1.1 The Pico 2\n\nThe following files are currently Pyboard-specific because they use the `pyb`\nmodule:\n * dftclass.py\n * dftadc.py\n * dftadc_tests.py\n\nThe first can be changed to replace `pyb` with `machine`: this enables synthetic\ndata tests and demos to run. For real applications using the ADC, adaptation for\nlow data rates should be easy. It may be possible to achieve fast sampling using\nthe PIO.\n\n# 2. Design\n\nThis code obsoletes my integer based converter which was written before the\ninline assembler supported floating point instructions: the ARM FPU is so fast\nthat integer code offers no speed advantage. The use of floating point avoids\nproblems with scaling and loss of precision which become apparent when integers\nare used for transforms with more than 256 bins.\n\n`adc.read_timed()` may be used for data acquisition. It blocks until completion\nbut is designed to work up to about a 750KHz sample rate.\n\nConversion from Cartesian to polar is performed in assembler using an\napproximation to the `math.atan2()` function. Its accuracy is of the order of\n+-0.085 degrees.\n\nCalculations use single precision floating point on all platforms.\n\n## 2.1 Future development\n\nModern compilers mean that the traditional performance benefit of assembler is\nnonexistent unless the assembler code is hand crafted and optimised to an\nextreme level. This is because the compiler exploits details of the internal\ndesign of the CPU in ways which are difficult for the programmer to achieve.\n\nThe benefit of the inline assembler (compared to C modules) is that the code\nmay be run on a standard firmware build. When dynamically loaded C modules\narrive this will no longer apply. The drawback of assembler is that it is not\nportable.\n\nI may rewrite this library as a dynamically loadable C module.\n\n# 3. Getting Started\n\nThe first step is to determine how to populate the real data array. If you are\nusing the Pyboard and intend to use an onboard ADC, one option is to use the\nADC's `read_timed()` method. This can acquire data at high speed but has the\ndrawack of blocking for the duration of the read. Its use is supported by the\n`DFTADC` class:\n\n```python\nfrom dftclass import DFTADC, POLAR\nmydft = DFTADC(128, \"X7\")\n # Acquire data for 0.1 secs and convert\nmydft.run(POLAR, 0.1)  # values are in mydft.re and mydft.im\n```\n\nWhere the data is to be acquired by other means you will need to instantiate a\n`DFT` object and provide a function to populate its `re` array. For synthetic\ndata this is straightforward. Data from sensors is usually in the form of\nintegers which will need to be converted to floats. While this is trivial in\nPython, if speed is critical the `window.icopy` function can copy and convert\nan integer array to one of floats (see the `DFTADC` class). The test programs\n`dftadc.py`, `dftadc_tests.py` and `dfttest.py` provide examples, the latter\nshowing the use of synthetic data.\n\nFile | Purpose |\n-----|-------- |\ndftadc.py   | Demo program using the DAC to generate analog data passed to the ADC. |\ndftadc_tests.py | Further ADC demos showing window function etc. |\ndfttest.py  | Demo with synthetic data. |\ndft.py      | The fft implementation. |\ndftclass.py | Python interface. Requires `polar.py`, `window.py`, `dft.py`. |\nwindow.py   | Assembler code to initialise an array and to multiply two 1D arrays. |\npolar.py    | Cartesian to polar conversion. Includes fast atan2 approximation. |\nctrlmap.ods | Describes structure of the control array. |\nalgorithms.py | Pure Python DFT used as basis for asm code. |\ndftbench.py | Benchmark times a 1024-point forward transform. |\n\nTest programs `dftadc.py` and `dfttest.py` provide means of demonstrating the\ncode with ADC and synthetic data respectively. `dftadc_tests.py` also\nillustrates the use of a window function. For ease of reading the test programs\nprint phase angles in degrees.\n\nTest programs require `dft.py`, `dftclass.py`, `polar.py`, and `window.py`.\nNote that `dft.py` cannot be frozen as bytecode because of its use of assembler.\n\n###### [Top](./README.md#contents)\n\n# 4. The DFT class\n\nThis is the interface to the conversion. The constructor takes the following\narguments:  \n 1. `length` Mandatory. Integer. The conversion length. Must be an integer\n power of 2.\n 2. `popfunc=None` An optional function to populate the real array.\n 3. `winfunc=None` An optional window function.\n\nMethod:  \n * `run` Mandatory arg: `conversion`. Specifies the conversion type. See below.\n Returns the time in μs taken by the raw conversion.\n\nProperties:  \n * `scale` Integer. Read/write. The consructor initialises this with the\n default scaling factor `1/length`. This may be modified prior to executing\n `run()`.\n * `length` Integer. Read only. The transform length.\n\nUser-accessible bound variables:  \n * `re` Real data array. Elements are of type `float`.\n * `im` Imaginary data array. Elements are of type `float`.\n * `dboffset` Float. Offset for dB conversion. Default 0. See section 4.7.\n\n## 4.1 Conversion types\n\nThese constants in `dftclass.py` are passed to `DFT.run()` and define the\nconversion to be performed. The following are the options, described in detail\nbelow:\n\nOption | Result |\n-------|------- |\nFORWARD | Normal forward transform. See 4.4 below. |\nREVERSE | Perform a reverse transform. See 4.5 below. |\nPOLAR | Forward transform with results as polar coordinates. See 4.6. |\nDB | As per POLAR but magnitude is converted to dB. See 4.7. |\n\n## 4.2 The populate function\n\nThis optional function is called each time `run` is executed. Its purpose is to\npopulate the `re` data array, possibly by accessing hardware. It receives the\n`DFT` instance as its arg. Any return value is ignored. Any windowing is\napplied after it returns.\n\n## 4.3 The window function\n\nA discussion of the purpose of window functions is outside the scope of this\ndocument. See:  \n[Mathematical background](https://en.wikipedia.org/wiki/Window_function)  \n[Engineer's guide](http://www.bores.com/courses/advanced/windows/files/windows.pdf)\n\nThis optional function takes two arguments:\n * `x` Point number (0 \u003c= number \u003c length).\n * `length` Transform length.\n\nIt should return the window function value for the specified point. Commonly\nthis is in range 0-1.0. A typical window function is the Hanning (Hann) function:\n\n```python\ndef hanning(x, length):\n    return 0.5 - 0.5*math.cos(2*math.pi*x/(length-1))\n```\n\nThis has a -6dB coherent gain which may be offset by multiplying by 2 to\npreserve signal amplitude:\n\n```python\ndef hanning(x, length):\n    return 1 - math.cos(2*math.pi*x/(length-1))\n```\n\n## 4.4 FORWARD transform\n\nForward transforms assume real data: you only need to populate the real array.\nThe imaginary array is zeroed by `DFT.run()` before a conversion is performed.\nBy default values are scaled by the transform length to produce mathematically\ncorrect values. The `forward()` function in `dfttest.py` provides an example of\nthis.\n\nThe result comprises complex data in the DFT object's `re` and `im` arrays.\n\n## 4.5 REVERSE transform\n\nThese accept complex data in the DFT object's `re` and `im` arrays. If you use\na `populate()` function it must initialise both arrays. The `trev()` function\nin `dfttest.py` provides an example of this.\n\nThe conversion result comprises complex data in the DFT object's `re` and `im`\narrays.\n\n## 4.6 POLAR transform\n\nThis is a forward transform with results converted to polar coordinates.\n\nOn completion the magnitude is in the DFT object's `re` array and the phase is\nin `im`. Phase is in radians with the same conventions as `math.atan2()`. The\n`test()` function in `dfttest.py` provides an example of this.\n\nFor performance only the first half of `re` and `im` arrays are converted. The\ncomplex conjugates are ignored.\n\n## 4.7 DB transform\n\nThis is a forward transform with results converted to polar coordinates. The\nmagnitude is converted to dB. The 0dB level defaults to 1VRMS. Magnitudes are\nscaled by subtracting the `dboffset` bound variable. Magnitudes \u003c= 0.0 are\nreturned as -80dB. The `dbtest()` function in `dfttest.py` provides an example\nof this.\n\nOn completion the magnitude is in the DFT object's `re` array and the phase is\nin `im`. Phase is in radians in a form compatible with `math.atan2()`.\n\nFor performance only the first half of `re` and `im` arrays are converted. The\ncomplex conjugates are ignored.\n\nAs noted above the 0dB reference voltage is determined by the bound variable\n`DFTADC.dboffset`. An explanation of the calculation of its value may be found\nin comments in `dftclass.py`. The value may be changed prior to performing a DB\ntransform to change the reference voltage.\n\n###### [Top](./README.md#contents)\n\n# 5 The DFTADC class\n\nThis supports input from a Pyboard ADC using `pyb.Timer.read_timed`. Its base\nclass is `DFT`.\n\nCostructor. This takes the following args:\n 1. `length` Mandatory. Integer defining transform length.\n 2. `adcpin` Mandatory. This may take an ADC instance or an object capable of\n defining one e.g. `'X7'` or `pyb.Pin.board.X19`.\n 3. `winfunc=None` Window function. See section 4.3.\n 4. `timer=6` Can take a `pyb.Timer` instance or a timer no. Defines the timer\n used for data acquisition.\n\nThe constructor sets the `dboffset` bound variable so that the scaling is such\nthat 0dB corresponds to a 1V RMS sinewave applied to the Pyboard ADC (with\nsuitable DC bias). This only affects `DB` conversions.\n\nMethod.  \n * `run` Mandatory args: `conversion`, `duration`.\n Returns the time in μs taken by the conversion from the time of completion of\n data acquisition to the completion of conversion.\n\n`conversion` must be one of the forward conversion types defined in\n[section 4.1](./README.md#41-conversion-types).  \n`duration` Integer or float. Acquisition duration in seconds.\n\n`run` will block for the duration.\n\nIn the case of `DB` conversions scaling may be modified by altering the\n`dboffset` bound variable.\n\n# 6. Implementation\n\nThe DFT constructor creates and initialises three member float arrays, `re`,\n`im`, and `cmplx` and an integer array `ctrl`. The first two store the real\nand imaginary parts of the input and output data: for a 256 bin transform\neach will use 1KB of RAM. The `ctrl` and `cmplx` arrays are small (total size\nof the order of 120 bytes, size of the latter varies slightly with transform\nlength) and contains data used by the transform itself, including a one-off\ncalculation of the roots of unity (twiddle factors). There is no need to\naccess this data, but for those wishing to understand the code the structure of\nthe `ctrl` and `cmplx` arrays is documented in `ctrlmap.ods`.\n\nThe constructor is pure Python as it is assumed that the speed of\ninitialisation is not critical. The `run()` member function which performs the\ntransform uses assembler for iterative routines in an attempt to optimise\nperformance. The one exception is dB conversion of the result which is in\nPython.\n\n###### [Top](./README.md#contents)\n\n# 7. Note for beginners\n\nThis README does assume some familiarity with sampling theory and the DFT. It\nis worth noting that, in any sampled data system, precautions need to be taken\nto prevent a phenomenon known as aliasing. If you read the ADC at 1mS\nintervals, the maximum frequency which can be extracted from the set of samples\nis 500Hz. If signals above this frequency are present in the input analog\nsignal, these will incorrectly appear as signals below this frequency. This is\na fundamental property of all sampled data systems and you need to ensure that\nsuch signals are removed. Typically this is performed by a combination of\nanalog and digital filtering.\n\nAssume you read the ADC over one second and do a 1024 point DFT. Samples will\nbe acquired at a rate of 1.024KHz. However as described above the maximum\nfrequency which can be acquired at that rate is 512Hz. The output data from the\nconversion occupies 1024 frequency \"bins\". bin[0] contains the DC component.\nbin[1] contains the 1Hz component up to bin[511] with 511Hz.\n\nBins from 1023 downwards contain complex conjugates of the lower bins, so\nbin[1023] contains 1Hz conjugate, bin[1022] 2Hz and so on. Frequencies are\nrepresented as two contra-rotating phasors with the same magnitude but opposite\nphase (complex conjugates) which add to produce a real voltage.\n\nAs such these higher bins contain no information and can be ignored: simply\ndouble the absolute value of the lower order bins to retrieve the voltage.\n\n# 8. Performance\n\nThe script `dftbench.py` times a 1024 point forward transform in a way that\nmimics a typical application. In such an application the `DFTADC` would be\ninstantiated at the start. Data would be acquired repeatedly from an ADC at an\napplication dependent rate. Critical timing is from the end of data acquisition\nto the availability of transform data. This is the interval that `dftbench`\nmeasures. Results were:\n\nBoard | Time (ms) |\n|-----|-----------|\n| Pyboard 1.x | 12.9 |\n| Pyboard D SF2W |  3.6 |\n| Pyboard D SF6W |  3.6 |\n| Pico 2 |  6.97 |\n\n# 9. Whimsical observations\n\nAt one time a 1024 point DFT was widely used as a computer benchmark. As such\nthey were implemented in highly optimised assembler. I can't make this claim:\nmy code could be significantly improved. It does it in 12.9mS on a Pyboard. It\ncosts £28.\n\nOne of the first supercomputers, a Cray 1, took 9mS. It cost a king's ransom.\n\nMy own introduction to DFT involved punching cards, handing them in to the\ncomputer operator, and retrieving a listing (often with only an error code)\nthe following day...\n\n###### [Top](./README.md#contents)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterhinch%2Fmicropython-fourier","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpeterhinch%2Fmicropython-fourier","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterhinch%2Fmicropython-fourier/lists"}