Deploying Bandpass Filters Using the Wolfram Language Microcontroller Kit

September 24, 2019 — Suba Thomas, Software Engineer, Algorithms R&D

Real-time filters work like magic. Usually out of sight, they clean data to make it useful for the larger system they are part of, and sometimes even for human consumption. A fascinating thing about these filters is that they don’t have a big-picture perspective. They work wonders with only a small window into the data that is streaming in. On the other hand, if I had a stream of numbers flying across my screen, I would at the very least need to plot it to make sense of the data. These types of filters are very simple as well.

Take a basic lowpass filter based on two weights, 49/50 and 1/50. The filter computes its next output as a weighted sum of the current output and input. Specifically, outputk+1 = 49/50 outputk + 1/50 inputk. So with a few elementary operations, it essentially strips away the high-frequency component on a properly sampled input signal:

 ✕ ```With[{u = Table[Sin[t/10] + Sin[10 t], {t, 0, 150, 1/10.}]}, ListLinePlot[{u, RecurrenceFilter[{{1, -(49/50)}, {1/50}}, u]}, PlotLegends -> {"original signal", "filtered signal"}]]```

In hindsight, this output makes sense. The filter relies only a bit on the current input, and hence misses the high-frequency portion of the signal, effectively getting rid of it. Any slowly changing component has already been captured in the output.

The filter weights need not be conjured up or arrived at by trial and error. They can be obtained in a systematic fashion using the signal processing functions in the Wolfram Language. (And that’s what I will be doing in the next section for a couple of nontrivial filters.) I can also compute the frequency response of the filter, which will tell me how it will shift and smother or amplify a signal. This creates a ballpark of what to expect from the real-time responses of the filter. I can simulate its real-time response to various signals as well. But how does the filter perform in real time? Can I see the shifting and smothering? And how does this compare to what is predicted by the frequency response?

To investigate all this, I will pick some slightly more involved filters—namely a bandpass Butterworth and a Chebyshev 1 filter—and deploy them to an Arduino Nano. To go about this, I need several things. Luckily, they are all now available in the Wolfram Language.

The functionality to analyze and design filters has existed in the Wolfram Language for quite some time now. The next link in the workflow is the C or Arduino filter code that needs to be deployed to the microcontroller. Thanks to the Microcontroller Kit, which was released with Version 12, I can generate and deploy the code directly from the Wolfram Language. It will automatically generate the needed microcontroller code, sparing me the wearisome task of having to write the code and make sure that I have the coefficients correct. And finally, I use the Wolfram Device Framework to transmit and receive the data from the Nano.

In the first part of this blog post, I will compute, analyze and deploy the filters. In the second part, I will acquire and analyze the filtered data to visualize the responses and evaluate the performance of the filters.

Compute, Analyze and Deploy the Filter

I start off by creating a function to compute a bandpass filter that will pass signals with frequencies between 2 Hz and 5 Hz and attenuate signals of 1 Hz and 10 Hz by -30 dB:

 ✕ ```tfm[filter_, s_: s] := filter[{"Bandpass", {1, 2, 5, 10}, {30, 1}}, s] // N // TransferFunctionExpand // Chop```

With this, I obtain the corresponding Butterworth and Chebyshev 1 bandpass filters:

 ✕ `{brw, cbs} = tfm /@ {ButterworthFilterModel, Chebyshev1FilterModel};`
 ✕ ```Grid[{{"Butterworth filter", brw}, {"Chebyshev 1 filter", cbs}}, Background -> {None, {Lighter[#, 0.6] & /@ {cB, cC}}}, Frame -> True, Spacings -> {1, 1}, Alignment -> Left]```

The Bode magnitude plot attests that frequencies between 2 Hz and 5 Hz will be passed through, and frequencies outside this range will be attenuated, with an attenuation of around -35 dB at 1 Hz and 10 Hz:

 ✕ ```{bMagPlot, bPhasePlot} = BodePlot[{brw, cbs}, {0.5, 18}, PlotLayout -> "List", GridLines -> Table[{{1, 2, 5, 10}, Automatic}, {2}], FrameLabel -> {{"frequency (Hz)", "magnitude (dB)"}, {"frequency (Hz)", "phase (deg)"}}, FrameTicks -> Table[{{Automatic, Automatic}, {{1, 2, 5, 10}, Automatic}}, {2}], ImageSize -> Medium, PlotStyle -> {cB, cC, cI}, PlotTheme -> "Detailed", PlotLegends -> {"Butterworth", "Chebyshev 1"}];```
 ✕ `bMagPlot`

The phase plot shows that at around 3 Hz for the Butterworth filter and 2 Hz for the Chebyshev, there will be no phase shift. For all other frequencies, there will be varying amounts of phase shifts:

 ✕ `bPhasePlot`

Later, I will put these frequency responses side by side with the filtered response from the microcontroller to check if they add up.

For now, I will simulate the response of the filters to a signal with three frequency components. The second component lies in the bandpass range, while the other two lie outside it:

 ✕ `inpC = Sin[0.75 t] + Sin[2.5 t] + Sin[10 t];`

The responses have a single frequency component, and the two frequences outside the bandpass have in fact been stripped away:

 ✕ ```outC = Table[OutputResponse[sys, inpC, {t, 0, 15}], {sys, {brw, cbs}}]; Plot[{outC, inpC}, {t, 0, 15}, PlotLegends -> {"Butterworth", "Chebyshev 1", "Input"}, PlotStyle -> {cB, cC, cI}, PlotTheme -> "Detailed"]```

I then discretize the filters and put them together into one model:

 ✕ `sp = 0.1;`
 ✕ ```StateSpaceModel[ToDiscreteTimeModel[#, sp]] & /@ {brw, cbs} ; sysD = NonlinearStateSpaceModel[ SystemsModelParallelConnect[Sequence @@ %, {{1, 1}}, {}]] /. Times[1.`, v_] :> v // Chop```

For good measure, I simulate and verify the response of the discretized model as well:

 ✕ ```With[{tmax = 15}, With[{inpD = Table[inpC, {t, 0, tmax, sp}]}, ListLinePlot[Join[OutputResponse[sysD, inpD], {inpD}], PlotLegends -> {"Butterworth", "Chebyshev 1", "Input"}, PlotStyle -> {cB, cC, cI}, PlotTheme -> "Detailed", PlotMarkers -> {Automatic, Scaled[0.025]}]]]```

And I wrap up this first stage by deploying the filter and setting up the Arduino to send and receive the signals over the serial pins:

 ✕ ```\[ScriptCapitalM] = MicrocontrollerEmbedCode[ sysD, <|"Target" -> "ArduinoNano", "Inputs" -> "Serial", "Outputs" -> {"Serial", "Serial"}|>, "/dev/cu.usbserial-A106PX6Q"]```

The port name /dev/cu.usbserial-A106PX6Q will not work for you. If you are following along, you will have to change it to the correct value. You can figure it out using Device Manager on Windows, or by searching for file names of the form /dev/cu.usb* and /dev/ttyUSB* on Mac and Linux systems, respectively.

At this point, I can connect any other serial device to send and receive the data. I will use the Device Framework in Mathematica to do that, as its notebook interface provides a great way to visualize the data in real time.

Acquire and Display the Data

To set up the data transfer, I begin by identifying the start, delimiter and end bytes:

 ✕ ```{sB, dB, eB} = Lookup[\[ScriptCapitalM]["Serial"], {"StartByte", "DelimiterByte", "EndByte"}]```

Then I create a scheduled task that reads the filtered output signals and sends the input signal over to the Arduino, and runs at exactly the same sampling period as the discretized filters:

 ✕ ```i1 = i2 = 1; yRaw = y1 = y2 = u1 = u2 = {}; task1 = ScheduledTask[ If[DeviceExecute[dev, "SerialReadyQ"], If[i1 > len1, i1 = 1]; If[i2 > len2, i2 = 1]; AppendTo[yRaw, DeviceReadBuffer[dev, "ReadTerminator" -> eB]]; AppendTo[u1, in1[[i1++]]]; AppendTo[u2, in2[[i2++]]]; DeviceWrite[dev, sB]; DeviceWrite[dev, ToString[Last[u1] + Last[u2]]]; DeviceWrite[dev, eB] ] , sp];```

I also create a second and less frequent scheduled task that parses the data and discards the old values:

 ✕ ```task2 = ScheduledTask[ yRaw1 = yRaw; yRaw = Drop[yRaw, Length@yRaw1]; {y1P, y2P} = Transpose[parseData /@ yRaw1]; y1 = Join[y1, y1P]; y2 = Join[y2, y2P]; {u1, u2, y1, y2} = drop /@ {u1, u2, y1, y2}; , 3 sp ];```

Now I am ready to actually send and receive the data, so I open a connection to the Arduino:

 ✕ `dev = DeviceOpen["Serial", "/dev/cu.usbserial-A106PX6Q"]`

I then generate the input signals and submit the scheduled tasks to the kernel:

 ✕ ```signals[w1, g1, w2, g2]; {taskObj1, taskObj2} = SessionSubmit /@ {task1, task2};```

The input signals are of the form g1 Sin[w1 t] and g2 Sin[w1 t]. The utility function signals generates a cyclically padded list of the sampled values for each input. The code for this and other utility functions are available in the downloadable notebook.

At this point, the data is going back and forth between my Mac and the Arduino. To visualize the data and control the input signals, I create a panel. From the panel, I control the frequency and magnitude of the input signals. I plot the input and filtered signals, and also the frequency response of the filters. The frequency response plots have lines showing the expected magnitude and phase of the filtered signals, which I can verify on the signal plots:

 ✕ ```DynamicModule[{ iPart, ioPart, plotOpts = {ImageSize -> Medium, GridLines -> Automatic, Frame -> True, PlotTheme -> "Business"}}, Panel[ Column[{ Grid[{{ Panel[ Column[{ Style["Input signal 1", Bold, cI1], Grid[{{"freq.", Slider[ Dynamic[w1, {None, (w1 = #)& , signals[#, g1, w2, g2]& }], { 0, 7, 0.25}, Appearance -> "Labeled"]}}], Grid[{{"mag.", Slider[ Dynamic[g1, {None, (g1 = #)& , signals[w1, #, w2, g2]& }], { 0, 5, 0.25}, Appearance -> "Labeled"]}}]}]], Panel[ Column[{ Style["Input signal 2", Bold, cI2], Grid[{{"freq.", Slider[ Dynamic[w2, {None, (w2 = #)& , signals[w1, g1, #, g2]& }], { 0, 7, 0.25}, Appearance -> "Labeled"]}}], Grid[{{"mag.", Slider[ Dynamic[g2, {None, (g2 = #)& , signals[w1, g1, w2, #]& }], { 0, 5, 0.25}, Appearance -> "Labeled"]}}]}]]}}], Grid[{{ Dynamic[ ListLinePlot[ Part[{u1, u2, u1 + u2}, iPart], PlotStyle -> Part[{cI1, cI2, cI}, iPart], PlotRange -> {All, (g1 + g2) {1, -1}}, plotOpts], TrackedSymbols :> {u1, u2}, Initialization :> (iPart = {3}; g1 = (g2 = 0); Null)], Dynamic[ ListLinePlot[ Part[ Join[{y1, y2}, {u1 + u2}], ioPart], PlotStyle -> Part[{cB, cC, cI}, ioPart], PlotRange -> {All, (g1 + g2) {1, -1}}, FrameTicks -> {{None, All}, {Automatic, Automatic}}, plotOpts], TrackedSymbols :> {y1, y2, u1, u2}, Initialization :> (ioPart = {1, 2})]}, { Grid[{{ CheckboxBar[ Dynamic[iPart], Thread[Range[3] -> ""], Appearance -> "Vertical"], lI}}], Grid[{{ CheckboxBar[ Dynamic[ioPart], Thread[Range[3] -> ""], Appearance -> "Vertical"], lIO}}]}, { Labeled[ Dynamic[ BodePlot[{brw, cbs}, {0.5, 18}, PlotLayout -> "Magnitude", PlotLegends -> None, GridLines -> Automatic, ImageSize -> Medium, PlotStyle -> {cB, cC}, PlotTheme -> "Detailed", Epilog -> Join[ mLine[w1, cI1], mLine[w2, cI2]]], TrackedSymbols :> {w1, w2}, SaveDefinitions -> True], "Bode magnitude"], Labeled[ Dynamic[ BodePlot[{brw, cbs}, {0.5, 18}, PlotLayout -> "Phase", PlotLegends -> None, GridLines -> Automatic, ImageSize -> Medium, PlotStyle -> {cB, cC}, PlotTheme -> "Detailed", Epilog -> Join[ pLine[w1, cI1], pLine[w2, cI2]]], TrackedSymbols :> {w1, w2}, SaveDefinitions -> True], "Bode phase"]}}, Frame -> {All, {True, None, True}}, FrameStyle -> Lighter[Gray, 0.6]]}, Center]]]```

Since the Arduino needs to be up and running to see the panel update dynamically, I’ll show you some examples of my results here:

Finally, before disconnecting the Arduino, I remove the tasks and close the connection to the device:

 ✕ ```TaskRemove /@ {taskObj1, taskObj2}; DeviceClose[dev]```

It’s interesting to see in real time how the response of the deployed filter closely matches that of the analog version.

I think it’s extremely convenient to have the design and microcontroller code so closely coupled, the reason being that if I looked at just the microcontroller code, the coefficients of the filters are abstruse, especially compared to those of the simple first-order lowpass filter. However, the design specifications from which they were obtained are very concrete; thus, having them in one unified place makes it better suited for further analysis and experimentation.

The entire notebook with the filter design, analysis, code generation and visualization is available for download. Of course, you need an Arduino Nano or a similar microcontroller. And with just a few small tweaks—in some cases just changing the target and port name—you can replicate it on a whole host of other microcontrollers. Happy tinkering!

 Get full access to the latest Wolfram Language functionality for the Microcontroller Kit with Mathematica 12. Buy now

RELATED POSTS