element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • About Us
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
FPGA
  • Technologies
  • More
FPGA
Blog 7 Ways to Leave Your Spartan-6 project: FPGA polarimeter part 2
  • Blog
  • Forum
  • Documents
  • Quiz
  • Events
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join FPGA to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: jure
  • Date Created: 24 Jul 2022 4:49 PM Date Created
  • Views 1690 views
  • Likes 7 likes
  • Comments 2 comments
  • python
  • 7 Ways to Leave Your Spartan-6
  • xilinx
  • fpga
  • vivado
  • digilent
  • dsp
  • arty s7
  • spartan-7
  • verilog
  • Spartan_Migration
Related
Recommended

7 Ways to Leave Your Spartan-6 project: FPGA polarimeter part 2

jure
jure
24 Jul 2022
7 Ways to Leave Your Spartan-6 project: FPGA polarimeter part 2

This is a continuation of my previous post. If you haven't seen it yet, I would strongly recommend taking a look, since we'll be continuing from where we left off. Everything mentioned and used in the project is located in my GitHub repo.

Previously I was able to make a design, which would collect 2048 samples across a full rotation of a quarter wave plate (QWP). Samples are read as 16bit numbers. If we were to constantly send arrays of 2048 readings, we'd have to send 32kb of data per cycle. Current hardware can run at ~24cycles/s which means 768kbps, which might not seem like much, but with a 100MHz system clock, our UART can achieve only 230400 baud rate, which is insufficient. To utilize the full performance we can process data on the FPGA, and then send it over UART to the host PC.

DSP Introduction

To do this, we'll have to implement a digital signal processing (DSP) module. Typically DSPs are used to implement different filters, etc. For this project, we'll take a look at a different approach. But before that, we'll have to know a little bit about the signal that we're working with. By using Mueller calculus, we can calculate the intensity of light transmitted through a rotating QWP and a linear polarizer. Well assume that the angle of QWP is ωt+δ. We can also assume that the angle of the polarizer is θ. Intensity of the light can then be written as:

image

Here different S represent the Stokes parameters, which is what we want to measure. We can see that our signal will carry 5 components, a DC component along with 2 AC components at double the rotation frequency and 2 AC components at quadruple frequency. Therefore we should create a DSP that will measure/read these 5 components. By doing this, we can directly obtain the values of expressions written in the square brackets.

DSP Implementation

There are multiple approaches, for example, we could do a fast Fourier transform. A more efficient approach is just to get the 5 components directly. This can be done similarly to how it is done in spectrum analyzers or lock-in amplifiers. We can simply take our signal, mix it with reference frequency and integrate it. Mixing signals simply means multiplying them together. So to do this in FPGA we should sample the signal using an ADC, then multiply it with a correct value of sine/cosine for the current position. After that, the product should be added together with previous products. The flow of this is roughly shown in the following diagram.

image

As we can see, we'll need to provide the values of sine/cosine. There are multiple approaches. For example, we could create a Block RAM, configure it as ROM and then fill it with data. Luckily Xilinx already provides the tools, that do this automatically. Using the DDS compiler IP, we can configure it to SIN/COS LUT. This way a ROM populated with values of sine/cosine will be created automatically. To interface it, we can use a simple AXI Stream to set the phase data (the argument of sine/cosine) and then read it on the output AXI Stream. Setting the phase is extremely simple as we just write the 32bit phase input and set the tvalid signal high. Data is output from the LUT in the same way.

Since we're working with a quadrature encoder with 512 counts per revolution, we'll get 2048 (11bits wide) positions in a single revolution. This way we'll need to configure the double frequency LUT to 10bit depth and the quadruple frequency LUT to 9bit depth. Next is the selection of LUT width (output resolution). This can be determined such that the smallest difference between two consecutive entries in the LUT are greater than 1. For a 10bit deep LUT, the width should be 16bits or wider.

imageFinally, we'll need some logic, that will move data between the ADC, LUTs, multiply adders, internal hold register and outputs. The operation is relatively simple. When the encoder count changes to the desired position, we'll update the input of the LUTs. When LUTs and ADC have data ready, we'll write their output values to 4 multiply adders (MADD) to A and B inputs. We'll also write the ADC data directly to the single Adder IP. MADDs need 3 cycles to perform the multiplication and addition. When they are done, we'll write their outputs to 5 hold registers and C inputs on the MADDs. Similarly, the output of the adder is written back to the B input.  Finally, we increment the desired position by 1. If we are in the last position we write data from all 5 hold registers to outputs, after which we reset the value of hold registers to 0. Additionally, the DSP controller outputs a data-ready signal to the interrupt input of the MicroBlaze.

`timescale 1ns / 1ps


module DSP_controller(
    //General signals
    input               fpga_clk_i,
    input               nres_i,
    
    //ADC signals
    input               adc_data_ready_i,
    input       [15:0]  adc_data_i,
    
    //Encoder signal
    input       [10:0]  encoder_cnt_i,
    
    //Multiply adder p signals
    input       [47:0]  madd_sin1f_p,
    input       [47:0]  madd_cos1f_p,
    input       [47:0]  madd_sin2f_p,
    input       [47:0]  madd_cos2f_p,
    
    //Multiply adder c signals
    output reg  [47:0]  madd_sin1f_c,
    output reg  [47:0]  madd_cos1f_c,
    output reg  [47:0]  madd_sin2f_c,
    output reg  [47:0]  madd_cos2f_c,

    //Multiply adder b adc_data 
    output reg  [15:0]  madd_sin1f_b,
    output reg  [15:0]  madd_cos1f_b,
    output reg  [15:0]  madd_sin2f_b,
    output reg  [15:0]  madd_cos2f_b,
    
    //Adder s signal
    input       [31:0]  add_s,
    
    //Adder b signal
    output reg  [31:0]  add_b,
    
    //Adder and Multiply adder a signal
    output reg  [15:0]  adc_data,
    
    //Output registers
    output reg  [31:0]  res_0f,
    output reg  [47:0]  res_sin1f,
    output reg  [47:0]  res_cos1f,
    output reg  [47:0]  res_sin2f,
    output reg  [47:0]  res_cos2f,
    
    //Double frequency (10bit wide) sin/cos lookup table phase data AXI stream output
    output      [15:0]  m0_axis_phase_tdata,
    output              m0_axis_phase_tvalid,

    //Double frequency (10bit wide) sin/cos lookup table AXI stream data input
    input       [31:0]  s0_axis_data_tdata,
    input               s0_axis_data_tvalid,
    
    //Quad frequency (9bit wide) sin/cos lookup table phase data AXI stream
    output      [15:0]  m1_axis_phase_tdata,
    output              m1_axis_phase_tvalid,
    
    //Quad frequency (9bit wide) sin/cos lookup table AXI stream data input
    input       [31:0]  s1_axis_data_tdata,
    input               s1_axis_data_tvalid,
    
    //Data ready output
    output reg          data_ready_o

);
    reg                 cnt_lsb_del;    //single cycle delayed lsb of encoder_cnt_i
    reg         [10:0]  cpos;   //Current position
    reg         [9:0]  cphase;  //Current phase
    reg                 idle;   //Idle flag
    reg                 m_axis_tvalid;  //Tvalid flag used for AXI stream outputs

    reg                 madd_wait_state;    //Wait state flag for multiply adder
    reg         [1:0]   madd_wait_cnt;  //Wait state duration count for multiply adder



    assign m0_axis_phase_tdata = {6'b0, cphase}; //Set phase input of the 1xfreq DDS (10 bit wide input -1024 different values- as it should go through array twice per period)
    assign m1_axis_phase_tdata = {6'b0, cphase}; //Set phase input of the 2xfreq DDS (9 bit wide input as it should go through array twice per period) 

    //Signal that valid data is present on AXI stream outputs
    assign m0_axis_phase_tvalid = m_axis_tvalid;
    assign m1_axis_phase_tvalid = m_axis_tvalid;
    
    //INITIALS
    initial cpos = 0;
    initial idle = 1;
    initial m_axis_tvalid = 0;
    initial madd_wait_state = 0;
    initial madd_wait_cnt = 0;
    
    
    always @(posedge fpga_clk_i) cnt_lsb_del <= encoder_cnt_i[0]; //Get a delayed LSB of encoder count to compare on change for triggering.

    always @(posedge fpga_clk_i)
    begin
        if(~nres_i) //CORE RESET
            begin
                cpos <= 0;
                idle <= 1;
                m_axis_tvalid <= 0;
                madd_wait_state <= 0;
                madd_wait_cnt <= 0;

                madd_sin1f_c <= 0;
                madd_cos1f_c <= 0;

                madd_sin2f_c <= 0;
                madd_cos2f_c <= 0;

                add_b <= 0;
            end
        else
            begin
                if(idle) //Check whether we are idle or not
                    begin
                        if(cnt_lsb_del != encoder_cnt_i[0]) //Check whether the encoder count changed?
                        begin
                            if(cpos == encoder_cnt_i) //Check whether we are in desired position
                            begin
                                cpos    <= cpos + 1;    //Advance for 1 count
                                idle    <= 0;   //Core is not idle anymore
                                cphase  <= cpos[9:0];   //Write phase data to AXI streams
                                m_axis_tvalid <= 1; //Set both tvalid flag to high on both master axis to signal the correct phase on the input of the DDS cores.
                                data_ready_o <= 0;  //Output data is not valid anymore

                            end

                            if(cpos == 0)   //Reset the summation registers and output the generated data -- start of a new cycle
                            begin
                                //Write calculated data to result registers
                                res_sin1f <= madd_sin1f_c;
                                res_cos1f <= madd_cos1f_c;
                                res_sin2f <= madd_sin2f_c;
                                res_cos2f <= madd_cos2f_c;
                                res_0f <= add_b;
                                //Reset the "cary" registers
                                madd_sin1f_c <= 0;
                                madd_cos1f_c <= 0;

                                madd_sin2f_c <= 0;
                                madd_cos2f_c <= 0;

                                add_b <= 0;
                                
                                //Data should be ready now
                                data_ready_o <= 1;
                            end
                        end
                    end
                else //If we're not idle
                    begin

                        if(&{adc_data_ready_i, s0_axis_data_tvalid, s1_axis_data_tvalid, ~madd_wait_state}) //If DDSs and the ADC have data ready
                        begin
                            m_axis_tvalid <= 0; //We got the data from DDSs, so we can reset the tvalid flag

                            madd_sin1f_b <= s0_axis_data_tdata[31:16];  //Output of DDS is 32 bit, higher 16bits represent the value of sine
                            madd_cos1f_b <= s0_axis_data_tdata[15:0];   //Output of DDS is 32 bit, lower 16bits represent the value of cosine

                            madd_sin2f_b <= s1_axis_data_tdata[31:16];  //Output of DDS is 32 bit, higher 16bits represent the value of sine
                            madd_cos2f_b <= s1_axis_data_tdata[15:0];   //Output of DDS is 32 bit, lower 16bits represent the value of cosine

                            adc_data <= adc_data_i; //Write data from adc to Adder/Multiply adder inputs
                            madd_wait_state <= 1;   //Start waiting for multiply adders to finish (3 cycles)
                            madd_wait_cnt <= 2'b11;


                        end

                        if(madd_wait_state)
                        begin
                            if(madd_wait_cnt == 0)  //Multiply adders are done
                                begin
                                    //Move the results from Adder Multiply adders outputs back to their C inputs (B for adder)
                                    madd_sin1f_c <= madd_sin1f_p;
                                    madd_cos1f_c <= madd_cos1f_p;

                                    madd_sin2f_c <= madd_sin2f_p;
                                    madd_cos2f_c <= madd_cos2f_p;

                                    add_b <= add_s;
                                    
                                    //Wait state is over
                                    madd_wait_state <= 0;
                                    
                                    //Switch back to idle -- wait for new encoder increment
                                    idle <= 1;
                                end
                            else
                                begin
                                    madd_wait_cnt <= madd_wait_cnt - 1;
                                end
                        end



                    end


               
            end

    end


endmodule

To summarize I have used 2 DDS compilers configured as SIN/COS LUT to generate the reference sine/cosine signals. These then get multiplied using 4 Multiply adders with the signal from the ADC. Additionally, there is an adder which sums up all ADC readings for a single revolution. Finally, there is a custom RTL code which moves data between inputs and outputs. The full DSP "core" can be seen in the following image.

image

FPGA

To read the data using Microblaze, I have modified the Axi-Lite slave registers such that they now allow 5 outputs, which are connected to the AXI network. Since we now have the DSP to process the ADC data, we can remove the BRAM used in a previous blog post and all of its peripherals. Finally, we can add in the DSP core, which requires the reset and clock signals from the FPGA along with the encoder count and the ADC data output along with ADC data ready output. The DSP core has 5 output registers which hold the results of 5 components in the measured signal. The 4 frequency components are 48bits wide but only the 32 most significant bits can be read using the AXI-Lite slave. This is done because we needed to "divide" the signal by 16bits (65535) since the outputs of the LUTs were 16bits wide (e.g. 65535*sin(x)). Additionally, there is a data-ready signal which is connected to the interrupt on the MicroBlaze.

Everything else is mostly the same as in the previous post.

image

Microblaze firmware

The firmware running on the Microblaze is extremely simple. First, it configures the motor controller, UART and the interrupts. After that, it simply writes data from 5 32bit wide registers to UART.

//INCLUDES
#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "xintc.h"
#include "xil_exception.h"
#include "xil_io.h"
#include "motor_controller.h"
#include "xiic.h"

//I2C DEFINES
#define IIC_DEVICE_ID	   	XPAR_IIC_0_DEVICE_ID
#define IIC_BASE_ADDRESS	XPAR_IIC_0_BASEADDR
#define IIC_MOTOR_ADDRESS	0x29

//DSP DEFINES
#define DSP_CTRL_ADDR XPAR_AXI_DSP_REGISTER_0_BASEADDR

//INTERRUPT DEFINES
#define INTC_DEVICE_ID		XPAR_INTC_0_DEVICE_ID
#define DSP_INTERRUPT_CHANNEL 1


//FUNCTIONS
void WriteFromDSP(void *CallbackRef);




int main() {
	init_platform();	//Initialize the platform (UART)
	microblaze_enable_interrupts();	//Enable the interrupts

	xil_printf("I: Initializing!\n");
	motor_default_settings(IIC_BASE_ADDRESS, IIC_MOTOR_ADDRESS);	//Configure the motor controller to known usable settings
	motor_configure(IIC_BASE_ADDRESS, IIC_MOTOR_ADDRESS);			//Configure the motor to I2C operation, set number of poles and max RPM
	motor_start_rpm(IIC_BASE_ADDRESS, IIC_MOTOR_ADDRESS, 5);		//Set motor start RPM
	motor_set_speed(IIC_BASE_ADDRESS, IIC_MOTOR_ADDRESS, 400);		//Configure motor setpoint speed (not in rpm e.g. with curren motor setpoint of 400 results in 1440 RPM)
	xil_printf("I: Motor Started!\n");
	xil_printf("I: Configuring Interrupts!\n");


	cleanup_platform();
	//Configure the interrupt channel. This will lead to interrupt getting triggered every time that fresh data is available.
	XIntc_RegisterHandler(XPAR_INTC_0_BASEADDR, DSP_INTERRUPT_CHANNEL, (XInterruptHandler)WriteFromDSP, (void *)1);
	XIntc_MasterEnable(XPAR_INTC_0_BASEADDR);
	XIntc_EnableIntr(XPAR_INTC_0_BASEADDR, 0x3);


}

void WriteFromDSP(void *CallbackRef)
{
	/* Print data that is read from DSP registers. Format is A;B;C;D;E
	 * A corresponds to  the constant component of the signal
	 * B corresponds to the sin(2f) component of the signal
	 * C corresponds to the cos(2f) component of the signal
	 * D corresponds to the sin(4f) component of the signal
	 * E corresponds to the cos(4f) component of the signal
	 */
	xil_printf("%i;%d;%d;%d;%d\r\n", Xil_In32(DSP_CTRL_ADDR),
					Xil_In32(DSP_CTRL_ADDR+4),
					Xil_In32(DSP_CTRL_ADDR+8),
					Xil_In32(DSP_CTRL_ADDR+12),
					Xil_In32(DSP_CTRL_ADDR+16));

}

Using the polarimeter and some measurements.

To test everything I have assembled a simple mount that holds a diode laser with optics, followed by a Linear polarizer in a rotation mount. Finally, the polarimeter sensor measures the light transmitted by the polarizer. 

image

To process data, I have written a program in Python which reads data from FPGA and then calculates the Stokes Parameters. The program is threaded such that data reads are done separately from the UI updates. This should prevent any overflowing of the serial buffers.

Data from the FPGA is plotted on the left side. Stokes parameters are plotted in the middle. Lastly, I am plotting the polarization ellipse on the top right. In the bottom right, a Poincare sphere is plotted. To use the program a horizontal reference polarization has to be set. This is done by shining a "reference" linear polarized light into the sensor and running the calibration.  During the calibration, 100 samples will be collected, which will be averaged and used to compute the values of θ and δ mentioned above.

In the image below we can see a measurement of linearly polarized light. You can also see the history while I was playing with the polarizer.

image

I have noticed, that If I was spinning the polarizer the S0 (or the intensity Stokes parameter) was changing. This is because the laser diode I used was emitting polarized light. And now that I have the tools, I could measure it.image

We can see, that the emitted light is elliptical. I also tried getting measurements of different light sources I had on hand. Also, keep in mind that the optical components inside the sensor used are meant for 850nm wavelength.

{gallery}Random measurements

image

Measured polarization of incandescent light. Small and noisy values S1, S2, and S3 are can be interpreted as unpolarized light.

image

Measured polarization of a phone flashlight. Light is unpolarized.

image

Measured polarization of a red laser pointer. Polarization is linear.

A note on result interpretation

Currently, the software draws a point on a fixed-size sphere and an ellipse with a fixed-size major axis. In the images above I added two examples of unpolarized light, incandescent and LED light. To know whether light is polarized or unpolarized can be determined using a "polarization" factor:

image

Using this we can evaluate values of p for measurements above:

Measurement value of p
Linear polarizer 0.400
Test laser 0.053
Incandescent light 0.012
LED 0.002
Red laser pointer 0.019

As can be seen, the LED is the least polarized, followed by the incandescent. We can see that the initial laser (OPV330, VCSEL technology) used for testing is much more polarized than the red laser pointer, which is probably due to the difference between laser cavities.

Conclusion.

Hopefully, this was as interesting to read as it was to put together. When info about the 7 Ways to Leave Your Spartan-6 was posted, I did not know what to make. Luckily I got a recommendation to build a polarimeter from my mentor. I found the project to be a nice learning experience since I have never worked with any kind of DSP. Furthermore, I truly hope that this was able to showcase some use cases of digital signal analysis. Even though a polarimeter is a niche tool it uses some methods such that a similar FPGA design could be used to make a lock-in amplifier or even a simple spectrum analyzer.

As mentioned in my GitHub repo, this project will also be used as a part of my Master's thesis. Hence I plan to do further work. Currently, it is in the last stages of a "proof of concept". Soon I will try to get it tested in a lab. There are also some important changes incoming, such as an FPGA shield PCB to hold all modules and power supplies. PCB on the sensor with some sort of connector, probably with a custom diode amplifier design. The current Python program is mostly just for testing along with the fact that python is not the best for making standalone software, etc. The ability to control the polarimeter from the host needs to be implemented. I also want to add the ability for the device to be used as an SPI slave, such that it can be used with e.g. a display module or a Raspberry Pi. I am also thinking about porting to a Zynq, which should be relatively simple since we're using a 7-series FPGA (It would be much more difficult if I have used Spartan-6...).

If there are any questions or comments please let me know. 

 

 

  • Sign in to reply

Top Comments

  • saadtiwana_int
    saadtiwana_int over 3 years ago +1
    Excellent project. And a bit different too! Thanks for sharing!
  • jure
    jure over 3 years ago in reply to saadtiwana_int

    Thanks. I'm glad you liked it.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • saadtiwana_int
    saadtiwana_int over 3 years ago

    Excellent project. And a bit different too! Thanks for sharing!

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • More
    • Cancel
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube