Introduction: FPGA Solar Panel Optimizer

Photovoltaics: Maximum Irradiance Detection

Photovoltaic cells convert light to electricity using silicon or some other semiconductor material by absorbing photons with enough energy to knock electrons free so they can flow as a current to produce power. In order to maximize the amount of energy available the light rays must strike the cell as close to 90 degrees as possible. Since the Earth rotates around the Sun, which is our main source of light and energy, many photovoltaic systems pivot in order to follow the Sun’s apparent motion in the sky. This maximizes the amount of irradiance striking the panel. Our project mimics this process by using a servo that rotates a single solar cell due to voltage fluctuations from a light source. It is the goal of our device to find the maximum irradiance due to this light source by scanning 180 degrees from an initial position at 45 degrees to the XY plane. As the cell is rotating incoming voltages will be processed by an analog to digital converter and then passed through a comparator that compares the magnitude of a new state (new voltage) to a present state (previous voltage) which is stored in a register. If the new voltage is greater than the previous voltage, the previous value will be discarded and the new, greater value will be stored. A counter will be used as a timer to keep track of where the maximum voltage occurred so that at the end of the 180 degree sweep, the servo will reverse direction and stop at the location of the maximum voltage. At this point, another servo attached on top of the rotating servo will then pivot the cell to a horizontal position from the initial 45 degree angle, that is, parallel to the XY plane, and in the same manner will store maximum values of voltages. When the cell reaches the horizontal, it will then re-pivot to the maximum voltage location. This location will be the maximum irradiance due to our light source.

Step 1: Materials List

Here are the components we used for the project.

1.) 2 x Parallax Bidirectional Continuous Rotation Servo
2.) RadioShack Solar Panel: 9V VDC
3.) 3D printed frame
4.) Basys 3 FPGA
5.) Breadboard, 3 x 100 Ohm resistors, jumper cables

Theoretically, this project can be replicated using any FPGA board that has sufficient inputs and outputs. We used VHDL as our hardware description language, however it could be replicated just as easily in Verilog. The biggest issue that would come up in using a different board would be having to configure the analog to digital converter differently. Our Basys 3 has a built in ADC, but other boards may not have this capability. If your board does not have an on-board ADC, you can always buy one and connect it up.

Here is the github link for the VHDL code. The formatting was messed up in the Instructable so feel free to download it and follow along:

Step 2: Understanding the Design

In order to complete this project and fully understand what you are going to make, you must first understand the black box diagram. This is basically a summary of all the modules that make up the system on the FPGA. We will now go more in depth into how each module works, what it does within the circuit, and how to design the module. If you ever get confused while reading the description of individual modules, come back to this picture and look at how the module you are confused about is connected up. This may help clarify the design.

Step 3: Finite State Machine

The first module in the design is the finite state machine(FSM). This module has five different states which are used to control how and when the system reacts. Out of the five states, one state is manual control in which the servos can be controlled by the on-board buttons. The other four states occur sequentially when the center button on the board is pressed. These states are called the calibration states. When calibrating, the horizontal servo goes into the horizontal sweep state, sweeping 180 degrees. As it’s scanning the voltages, it looks for the horizontal max voltage. Once it goes through its full motion, the FSM transitions into the horizontal max state where it controls the servo to return the location of the max voltage received. Next, the FSM goes into the vertical sweep state where it controls the vertical servo to scan up and down 45 degrees for the new max voltage. Finally, the FSM goes into the vertical max state once it finds the max voltage and returns to that point, just like the horizontal servo. After calibration is complete, the servos enter back into the manual state.

Here is the entity description of our FSM in VHDL:

entity FSM is
Port ( BTN_L, BTN_R, BTN_U, BTN_D, BTN_C : in STD_LOGIC; --- input buttons
CNT_L, CNT_RU, CNT_D : in STD_LOGIC; --- servo enable signals from counter
CLK : in STD_LOGIC; --- clock signal
HS, VS, MC : out STD_LOGIC; --- counter enable signals
SERVO_L, SERVO_R, SERVO_U, SERVO_D : out STD_LOGIC; --- servo enable signals
CNT_RST : out STD_LOGIC); --- reset signal for one of the counters
end FSM;

The FSM is the brains of the system. It controls all the signals which enable other components to react to input accordingly. The VHDL architecture description consists of two parts. The first is a synchronous process which changes the present state (PS) of the state machine to the next state (NS). The second is the combinational process which takes all the inputs and decides the state machines next state. Together, these processes make up the FSM.

Here is some code from our architecture description. It contains the synchronous process and the first two states from our combinational process:

type state is (man, hor_sweep, hor_max, vert_sweep, vert_max); --- defining our states
signal PS : state;
signal NS : state;

sync_proc: process(CLK, NS) --- synchronous process
if rising_edge(CLK) then
PS <= NS;
end if;
end process sync_proc;

comb_proc: process(PS, BTN_L, BTN_R, BTN_U, BTN_D, BTN_C, CNT_L, CNT_RU, CNT_D)
SERVO_L <= '0'; SERVO_R <= '0';SERVO_U <= '0';SERVO_D <= '0';
case PS is
when man =>
if (BTN_C = '1') then
NS <= hor_sweep; --- go into the first calibration state if the center button is pressed
CNT_RST <= '0';
HS <= '1'; VS <= '0'; MC <= '0'; --- tell the Horizontal counter to start counting up
if (BTN_L = '1') then SERVO_L <= '1'; SERVO_R <= '0'; --- from these you can see how we
else SERVO_L <= '0'; --- managed the servos in manual state
end if;

if (BTN_R = '1') then SERVO_R <= '1'; SERVO_L <= '0';
else SERVO_R <= '0';
end if;
if (BTN_U = '1') then SERVO_U <= '1'; SERVO_D <= '0';
else SERVO_U <= '0';
end if;
if (BTN_D = '1') then SERVO_D <= '1'; SERVO_U <= '0';
else SERVO_D <= '0';
end if;
NS <= man; --- stay in manual state
CNT_RST <= '1'; --- keep the max counter reset
HS <= '0'; VS <= '0'; MC <= '0'; --- do not enable any of the counters
end if;
when hor_sweep =>
SERVO_U <= '0'; SERVO_D <= '0';
CNT_RST <= '0';
if (CNT_L = '1') then
SERVO_L <= '1'; SERVO_R <= '0';
HS <= '1'; VS <= '0'; MC <= '0';
NS <= hor_sweep;
SERVO_L <= '0'; SERVO_R <= '0';
HS <= '0'; VS <= '0'; MC <= '1';
NS <= hor_max;
end if;

The other three states are maximizing the horizontal voltage, sweeping the vertical servo, and then maximizing the vertical voltage. Once it is done calibrating, it goes back to the manual state. In terms of variables, HS, VS, and MC are all enable signals which go to counters. CNT_L, CNT_RU, and CNT_D are signals which come from the counters and tell the FSM drive the corresponding servo. All the buttons are mapped to input into the FSM. These control the initial move into the calibration state as well as moving the servos in the manual state.

Step 4: Analog to Digital Converter (ADC)

If you do not have a Basys 3 board, this setup will be different for you.

The ADC takes an analog signal (voltage) and converts it into a 12-bit binary number. This is how we read the maximum voltage from the solar panel. To get the ADC module to work, you need to instantiate it through Vivado’s IP Catalog. This will bring up a GUI in which you select different settings such as what pins you want the ADC to read and what mode you want it to operate in. Once you have selected all the proper settings, Vivado will synthesize an ADC module for you. You then create your own ADC module in which you instantiate Vivado's ADC module and setup a port map for it.

For the Basys 3, the analog to digital converter can only read from channels 6, 7, 14, and 15 so we had to choose these in the channel selection tab of the Vivado ADC instantiation wizard. If you look look at the schematic of Bank 35 which contains input signals, you can see that there is a series of 8 signals with the form XA#_P/N. These go to the ADC and receive their signal from the bottom left Basys 3 header. Therefore, you need to look in the IO Bank and see which pins correspond to which channel. In our case, we used the J3 and K3 pins which correspond to channel 6 on the ADC.

Since the board’s ADC can’t take in more than one volt, we had to create a voltage divider. This will be described further in the Instructable.

Here is a component description of our ADC in VHDL:

entity adc is
Port ( V_in : in STD_LOGIC;
V_out : in STD_LOGIC;
clk : in STD_LOGIC;
do_out : out STD_LOGIC_VECTOR (15 downto 0));
end adc;

As you can see, the ADC takes in the two voltages, a clock signal, and do_out. do_out is the data read from a register in the ADC which contains the voltage on channel 6. If you use a different channel, you will need to update which address do_out is sent from. For channel 6, the address for the voltage is in x16. The rest of the adc file is a port map of the module Vivado synthesized from the IP Catalog.

signal ADC_addr : STD_LOGIC_VECTOR (6 downto 0);
signal ADC_enable : STD_LOGIC;

ADC_addr <= "001" & x"6"; --- the address input is only 7 bits long

ADC : xadc_wiz_0
port map (daddr_in => ADC_addr,
den_in => ADC_enable,
di_in => x"0000",
dwe_in => '0',
do_out => do_out,
drdy_out => d_rdy,
dclk_in => clk,
reset_in => '0',
vauxp6 => V_in,
vauxn6 => V_out,
busy_out => open,
channel_out => open,
eoc_out => ADC_enable,
eos_out => open,
alarm_out => open,
vp_in => '0',
vn_in => '0');

Many of the ports from the automatically synthesized module are not used.

XADC documentation

Step 5: Register

The register is the module used to store the current max voltage. This module is created behaviorally so that you may use various amounts of bits without having to change much VHDL code. The value stored in the register is sent to another module called the Comparator where it compares the stored max value with all of the raw values that come from the ADC.

Here is an entity description of our register:

entity FF_Array is
Port ( CLK : in STD_LOGIC;
A : in STD_LOGIC_VECTOR(9 downto 0);
LV : out STD_LOGIC_VECTOR(9 downto 0):= "0000000000" );
end FF_Array;

As you can see, it takes in a clock signal, and enable signal, and a value A. When the enable signal is high, the value LV is set to A. If it is low, LV keeps its value.

Here is the behavioral description of the register:

signal inter : STD_LOGIC_VECTOR (9 downto 0) := "0000000000";
comp: process(CLK, A, EN)
if rising_edge(CLK) then
if EN <= '1' then
LV <= A;
inter <= A;
LV <= inter;
end if;
end if;
end process comp;

We only store 10 out of the 12 bits that come from the ADC. We get rid of the two least significant bits because they do not signify a difference in voltage enough to warrant a change. At that small level, those bits can likely be changed by noise on the analog side of the ADC, thus we chose to ignore them.

Step 6: Comparator

The comparator takes in two values of the same bit size and sends the greater value to the register. The two values taken in are from the register and the ADC. The ADC is constantly sending new values to the comparator to be compared against the max value from the register. If the value from the ADC is greater than the value in the register, the comparator outputs a high signal which causes the register to store the value that was in the ADC. This is how we find the max voltage while sweeping in the horizontal and vertical directions

Here is the entity description of our comparator:

entity voltage_comparator is
Port ( PV : in STD_LOGIC_VECTOR (9 downto 0);
LV : in STD_LOGIC_VECTOR (9 downto 0);
GT : out STD_LOGIC);
end voltage_comparator;

The comparator is a very simple module if defined behaviorally. Here is the implementation of it:

comp : process(PV, LV)
if PV > LV then
GT <= '1';
else GT <= '0';
end if;
end process comp;

Step 7: Servo Driver

The two servo driver modules(one for each servo) control the speed and direction of the servos which rotate the frame and solar panel. These modules take in an enable signal from the FSM and then send a PWM (pulse-width-modulation) signal to their respective servo which determines the direction and speed that the servos rotate. Depending on the value of the PWM signal, you can make the servo go left or right with a slow or fast speed.

Our servo driver is actually defined structurally. This means that, like the ADC, we have given it its function by importing other modules and connecting them together in a way where we receive the desired outcome. In this case, the two modules we connect together to create the servo driver are a clock divider and a PWM controller.

Here is our entity description for our servo driver, clock divider, and PWM controller:

entity servo_driver is
Port ( CLK : in STD_LOGIC;
end servo_driver;

component pwm_control is
Port ( CLK : in STD_LOGIC;
DIR : in STD_LOGIC_VECTOR (1 downto 0);
end component;

component clk_div2 is
Port ( CLK : in STD_LOGIC
end component;

The interface for the servo driver is very simple. BTN_0 and BTN_1 are enable signals which tell the servo which way to turn. In the servo_driver architecture, BTN_0 and BTN_1 are put through some logic to get a value for DIR which is fed into pwm_control. pwm_control then creates a pwm signal according to the direction we want the servo to travel. In the case of our servos, a square wave with a width of 1.5 milliseconds with a low period of 20 ms if stopped. Anything wave above 1.5 ms will cause the servo to start to move counterclockwise. Anything below 1.5 will cause the servo to move clockwise. In our case, we wanted the servos to move fairly slowly so we had a square wave of 1.52 ms for the ccw movement and a square wave of 1.48 ms for the cw movement. As for our clock divider, we wanted our servo driver to receive a clock signal that had a period of one microsecond. The Basys board has a default clock speed of 100 Mhz, so we divided the clock by 100 in the clock divider in order to achieve a frequency of 1 Mhz (1/1 Mhz is 1 microsecond).

Here is part of the implementation of the pwm_controler:

constant time_high_stopped : INTEGER := (1500); --- 1500 microseconds = 1.5 ms
constant time_low : INTEGER := (20000);
variable th_cntr : INTEGER range 0 to 2047 := 0;
variable tl_cntr : INTEGER range 0 to 32767 := 0;

if EN = '1' then
if rising_edge(CLK) then ---stopping the servo
if DIR = "00" then
if tl_cntr <= time_low then
tl_cntr := tl_cntr + 1;
SERVO <= '0';
elsif th_cntr <= time_high_stopped then
th_cntr := th_cntr + 1;
SERVO <= '1';
tl_cntr := 0;
th_cntr := 0;
SERVO <= '0';
end if;

As you can see, we compare a counter variable to a maximum and change to the next case accordingly. With the size of the variables and the 1 Mhz divided clock speed, we get a perfect square wave with a 1.5 ms high and 20 ms low.

Step 8: Counters

A counter is register that increments or decrements on a periodic basis. We use three counters in our system. The first two are very similar. They are the horizontal counter and vertical counter. In our system, we have a predefined value that they count up to. When they reach the value that has been specified, they send a control signal to the FSM which then deactivates the current counter and activates the next one. The counter that is activated each time after the horizontal counter and vertical counter finish incrementing and reach their value is called the max counter.

The max counter module is the counter that increments throughout the whole time the system is calibrating. The counter increments by one every time it goes through the rising edge of the clock signal. Whenever a new max value is detected by the comparator, it sends a reset signal to the max counter. When this occurs, the current count that the max counter has restarts back to zero and continues to increment until the horizontal or vertical counter reaches the end of its sweep cycle. Once the horizontal/vertical counter finishes incrementing, its control signal then causes the max counter to start to decrement. While the max counter is decrementing, it is sending a control signal to the FSM which moves the servo back towards the direction of the max voltage. When the max counter has finished decrementing, the servo will have moved the solar panel back to the place where the last max voltage was.

Here is an entity description of our Horizontal and Max counters:

entity horiz_counter is
Port ( CLK : in STD_LOGIC;
end horiz_counter;

entity max_counter is
Port ( CLK : in STD_LOGIC;
end max_counter;

The horizontal counter and vertical counter are essentially the same, however they increment up to different values because it takes longer to do the horizontal sweep than the vertical sweep. They also do not have any reset signals as they will always increment up to their specified value and then reset.

The max counter is a little bit more complex. It has two different reset signals. One comes from the comparator so that the counter can be reset when the max voltage is found. The other comes form the FSM and is only active when the system is in manual mode because we do not want it to count when we are not trying to find the maximum voltage.

Here is a behavioral description for the max counter:

variable currcount : STD_LOGIC_VECTOR(12 downto 0):= "0000000000000";

if RESET = '1' or FSM_RST = '1' then
currcount := "0000000000000";
CNT_RU <= '0';
elsif Rising_Edge(CLK) then
if MC = '0' then
currcount := currcount + 1;
CNT_RU <= '0';
elsif MC = '1' then
currcount := currcount - 1;
if currcount = "000000000000" then
CNT_RU <= '0';
CNT_RU <= '1';
end if;
end if;
end if;

Step 9: Seven Segment Decoder

The seven-segment decoder module displays to the user what voltages the solar panel is reading. It takes in the value from the ADC as an input and and outputs two signals which power the sever segment display on the board. The first signal is a 4-bit number which is an active low enable that is used to determine which of the four zones of the display need to be turned on in order to display the value. The other signal is an 8-bit number which is also an active low enable. It is used to activate the segments on the individual zone.

The way the board is designed makes it so that only one zone can be enabled at once. This makes it so that you need to rapidly change which zone is being enabled. If you do this at the right speed, it will seem like the displays are all on simultaneously when really they are just turning on and off so fast that it is imperceivable.

Here is the entity description for the seven segment decoder.

entity sseg_dec is
Port ( ALU_VAL : in std_logic_vector(9 downto 0);
SIGN : in std_logic;
VALID : in std_logic;
CLK : in std_logic;
DISP_EN : out std_logic_vector(3 downto 0);
SEGMENTS : out std_logic_vector(7 downto 0));
end sseg_dec;

Like the servo driver, the seven segment decoder is also defined structurally with two other components. The first component is the workhorse of the module. It is the binary to decimal converter. This takes the 10-bit ALU_VAL and outputs 4 signals which correspond to the binary values of the decimal numbers that make up ALU_VAL. For example, if ALU_VAL = 110001011 = 395, then the binary to decimal converter would output 0000, 0011, 1001, and 0101. These correspond to 0, 3, 9, and 5 respectively. Because ALU_VAL is a 10-bit number, the maximum value it can represent is 1023. The second component is a clock divider which is used to multiplex the display at a rate that makes it seem like there is more than one segment on.

This module was originally created by Prof Bryan Mealy. It originally displayed up to an 8-bit number (0-255), however we modified it so that it could display up to a 10-bit number (0-1023).

Step 10: Wiring

There are three main component in this project which are connected with jumper wires.

Before you start connecting everything up, it would be best to assemble the voltage divider. Because the ADC
can only take a maximum voltage of 1 volt, we need to step down the voltage from the solar panel yet keep the voltage going into the board directly related to the solar panel voltage. In our case, we found that the solar panel produced about 3 volts max from our phone flashlights while we were indoors. Therefore, we needed the voltage going into the board to be 1 volt when the solar panel voltage was 3 volts. Using the voltage divide equation

Vdiv = Vsrc*(R1)/(R1+R2)

we found that a 200 Ohm resistor in series with a 100 Ohm resistor would give us the right voltage at the node between the resistors. In the equation, Vdiv is the voltage drop across the R1 resistor and Vsrc is the voltage of the voltage source. Plugging in our values we get


which simplifies to


So we we need this ratio between the resistances. Because the solar panel produced so little current, we are able to go with small resistor values. We actually used two 100 Ohm resistors in series to create the 200 Ohm one and then put another 100 Ohm in series with those in order to achieve the right voltage division.

If you use a different solar panel or use the solar panel in higher voltage situations, you will need to use different value resistors in order to keep the voltage at a safe level for the board.

Now that you have the voltage divider figured out, you need to connect it to the ADC. The first wire will go from the node that connects the second and third 100 Ohm resistors to the bottom left port on the Basys 3 board. Depending on which channel you chose for the ADC, you will have to put it in different pins. However, it will be a pin on the top side of the header as all the top pins take the positive voltages. The second wire will come from the ground rail in which the negative side of the solar panel is plugged in. This wire will go directly below the positive wire in the jumper on the Basys 3. This is made clear in the picture above.

Now you need to connect the servos to the board. The board has 3.3 V and GND pins on the left hand side of the jumpers. These will go into the power and ground wires of the servos. The servos also have a third wire that receives the PWM signal which can come from any pin on the header. We chose to output the PWM signal on the pins directly next to the 3.3V and GND pins on the upper left side header.

Step 11: 3D Printing

The frame of our system was created with a 3D printer. We modeled three separate components in Fusion 360, exported them as .stl files for printing, printed them, and then assembled them together with some screws. The arc of the lever which pivots the top support frame does not follow the same arc as the servo arms, so we had to prop up the servo a couple inches from the bed of the middle frame with a little half inch block of wood which we glued to the top side of the middle frame. Feel free to design your own frame which improves upon our design!

Step 12: Finishing Up

We hope our Instructable has given you enough insight into our design so that you can learn from it, replicate it, and improve upon it! We do not have a premium account and so the formatting for the VHDL code is pretty messed up. I will link the github project account to the Instructable so that everyone can download the code and view it in a code editor.

We would like to thank Prof. Danowitz for an awesome Digital Design class and Prof. Mealy for letting us expand up his seven segment decoder design.

Here are some resources where we gathered information and schematics from.

XADC documentation

Servo Documentation

Here is the github link for the VHDL code: