WOLFRAM NOTEBOOK

Deploy bandpass filters using the Wolfram Language Microcontroller Kit

Suba Thomas
Wolfram Research, Inc.

Introduction

In this project I will analyze the responses of a bandpass Butterworth and Chebyshev1 filter deployed to an Arduino Nano from the Wolfram Language.
As these are analog filters, they need to be discretized before deployment. It is interesting to see in real-time how the frequency response of deployed filter closely matches that of the analog version.
The frequency responses create a mental imagery of what the real-time responses of the filters would look like and how they would differ from each other. But I wanted to make it concrete. So I created a visualization juxtaposing the frequency responses of the analog filters and the real-time responses of discretized filters, to see both in real-time as I change the parameters of the input signals.
In the first part, the filters are computed, analyzed, and deployed. In the second part, I acquire and analyze the filtered data to visualize the responses and evaluate the performance of the filters.
All the computations and tasks are done using the Wolfram Language: I use its signal processing capabilities to compute and analyze the filters, the Microcontroller Kit to deploy the filters, the device framework to do the data acquisition, and the notebook interface to visualize the data. If you are new to the Wolfram Language, there is a fast introduction that will quickly get you up to speed.

Compute, analyze, and deploy the filter

I start off by creating a function that will compute a filter with passband frequencies of 2 Hz and 5 Hz, stopband frequencies of 1 Hz and 10 Hz, and an attenuation of -30 dB at the stopband frequencies.
In[]:=
Clear[tfm]tfm[filter_,s_:s]:=filter[{"Bandpass",{1,2,5,10.},{30,1}},s]//TransferFunctionExpand//Chop
Using that I obtain the corresponding Butterworth and Chebyshev type 1 bandpass filters.
In[]:=
{brw,cbs}=tfm/@{ButterworthFilterModel,Chebyshev1FilterModel};
Out[]=
Butterworth filter
159.183
4
s
10000.+9281.85s+8307.64
2
s
+3955.63
3
s
+1620.71
4
s
+395.563
5
s
+83.0764
6
s
+9.28185
7
s
+
8
s
Chebyshev1 filter
-
13.2653
3
s
1000.+296.502s+411.457
2
s
+72.5658
3
s
+41.1457
4
s
+2.96502
5
s
+
6
s
The Bode magnitude plot attests that frequencies between 2 Hz and 5 Hz are passed through, and frequencies outside this range are attenuated with an attenuation of around -35 dB at 1 Hz and 10 Hz.
In[]:=
{bMagPlot,bPhasePlot}=BodePlot[{brw,cbs},{0.5,18},PlotLayout"List",GridLinesAutomatic,ImageSizeMedium,PlotStyle{cB,cC,cI},PlotTheme"Detailed",PlotLegends{"Butterworth","Chebyshev1"}];
In[]:=
bMagPlot
Out[]=
Butterworth
Chebyshev1
The phase plot shows that at around 3 Hz for the Butterworth filter and 2 Hz for the Chebyshev, there is no phase shift. For all other frequencies there are varying amounts of phase shifts.
In[]:=
bPhasePlot
Out[]=
Butterworth
Chebyshev1
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 3 frequency components. One component lies in the bandpass range and the other two lie outside it.
In[]:=
inpC=Sin[0.5t]+4Sin[2.5t]+Sin[10t];
The responses show that the two frequences outside the bandpass are in fact stripped away.
In[]:=
outC=Table[OutputResponse[sys,inpC,{t,0,15}],{sys,{brw,cbs}}];Plot[{outC,inpC},{t,0,15},PlotLegends{"Butterworth","Chebyshev1","Input"},PlotStyle{cB,cC,cI},PlotTheme"Detailed"]
Out[]=
Butterworth
Chebyshev1
Input
I then discretize the filters and put them together into one model.
In[]:=
sp=0.1;
In[]:=
StateSpaceModel[ToDiscreteTimeModel[#,sp]]&/@{brw,cbs};sysD=NonlinearStateSpaceModel[SystemsModelParallelConnect[Sequence@@%,{{1,1}},{}]]/.Times[1.`,v_]v//Chop
Out[]=
x.
1
x.
2
x.
2
x.
3
x.
3
x.
4
x.
4
x.
5
x.
5
x.
6
x.
6
x.
7
x.
7
x.
8
x.
8
u.
1
-0.405815
x.
1
+3.43349
x.
2
-12.9245
x.
3
+28.257
x.
4
-39.2343
x.
5
+35.4225
x.
6
-20.3071
x.
7
+6.75879
x.
8
x.
9
x.
10
x.
10
x.
11
x.
11
x.
12
x.
12
x.
13
x.
13
x.
14
x.
14
u.
1
-0.750703
x.
9
+4.44058
x.
10
-11.2287
x.
11
+15.5191
x.
12
-12.3602
x.
13
+5.37913
x.
14
0.000574178
u.
1
+0.000341168
x.
1
+0.00197143
x.
2
-0.00971768
x.
3
+0.0162245
x.
4
-0.0190824
x.
5
+0.0203388
x.
6
-0.0139566
x.
7
+0.00388075
x.
8
-0.00131301
u.
1
+0.0022987
x.
9
-0.00583054
x.
10
+0.0108043
x.
11
-0.0203768
x.
12
+0.0201682
x.
13
-0.00706288
x.
14
0.1
For good measure, I simulate and verify the response of the discretized model as well.
In[]:=
With[{tmax=15},With[{inpD=Table[inpC,{t,0,tmax,sp}]},ListLinePlot[Join[OutputResponse[sysD,inpD],{inpD}],PlotLegends{"Butterworth","Chebyshev1","Input"},PlotStyle{cB,cC,cI},PlotTheme"Detailed",PlotMarkers{Automatic,Scaled[0.025]}]]]
Out[]=
Butterworth
Chebyshev1
Input
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.
In[]:=
=MicrocontrollerEmbedCode[sysD,<|"Target""ArduinoNano","Inputs""Serial","Outputs"{"Serial","Serial"}|>,"/dev/cu.usbserial-A106PX6Q"]
Out[]=
MicrocontrollerCodeData
Status:
Target: ArduinoNano
(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 DeviceManager on Windows, or by searching for file names of the form /dev/cu.usb* and /dev/ttyUSB* on Mac and Linux systems. )
At this point, I can connect any other serial device to send and receive the data. I will use device framework in Mathematica to do that, as its notebook interface provides a great way to visualize the data in realtime.

Acquire and display the data

To set up the data transfer, I begin by identifying the start, delimiter, and end bytes.
In[]:=
{sB,dB,eB}=Lookup[["Serial"],{"StartByte","DelimiterByte","EndByte"}]
Out[]=
{19,44,17}
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.
In[]:=
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,in1i1++];AppendTo[u2,in2i2++];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.
Now I am ready to actually send and receive the data, and I open a connection to the Arduino.
I then generate the input signals and submit the scheduled tasks to the kernel.
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.
Since the Arduino needs to be up and running to see the panel update dynamically, I am going to include some screenshots of the results.
Finally, before disconnecting the Arduino, I remove the tasks and close the connection to the device.
Wolfram Cloud

You are using a browser not supported by the Wolfram Cloud

Supported browsers include recent versions of Chrome, Edge, Firefox and Safari.


I understand and wish to continue anyway »

You are using a browser not supported by the Wolfram Cloud. Supported browsers include recent versions of Chrome, Edge, Firefox and Safari.