The aim of this project is to create a pwm module that supports the AXI lite interface to control the speed of a fan. We will later create the kernel driver to interface with the hardware. Later on we can create a user level application to manage the fan operation.
Hardware setup:It is very important to choose the right hardware configuration. We will have the parts:
- The SoC baseboard with the PL bank pin (in my case W12).
- 12V DC fan.
Additionally we will need an NPN transistor or an N-channel mosfet that will act as a switch and a zener diode that will act as a flyback diode to protect the transistor when the fan is switched off.
In my case I will be using the following circuitry that is already available on my baseboard.
The 20kHz signal generator in the circuit above represents the pwm_out signal that will be connected to the PL bank of the SoC. This PL pin will generate a 20kHz signal, meaning 50us periode. It is important to have a pwm frequency higher than the audible range that is 20kHz, otherwise we will be hearing some annoying squeaking.
The 3.3V supply here is optional and it pulls up the gate of the mosfet if the pwm_out signal is not driving it low. This ensures that the gate is off if the pwm_out signal is not present.
Next lets realise the signal generator on the FPGA using verilog.
FPGA part:Now we need to realise the pwm_out signal on the PL pin. This is very simple, we will need a register that will hold the duty cycle of the pwm signal and a counter that will keep incrementing with each positive edge of the input clock until reaching the defined duty cycle value, when the counter reaches the duty cycle then we will set the pwm_out high. This will keep repeating.
`timescale 1ns / 1ps
module pwm_fan_control (
input wire clk,
input wire rst,
input wire [7:0] duty_cycle, // the duty cycle determines the value
output reg pwm_output
);
reg [7:0] counter = 0; // 8-bit counter for fan speed control
always @(posedge clk or posedge rst) begin
if (rst) begin
counter <= 0;
pwm_output <= 0;
end else begin
if (counter >= 255)
counter <= 0;
else
counter <= counter + 1;
pwm_output <= (counter < duty_cycle);
end
end
endmodule
In order to have a pwm frequency that is above the audiable range, we need to properly choose the counter and the input frequency. The input clk I will be using is 25MHz, which means 40ns periode.
Basically we need to determine how many pwm periods we can fit into the input clock's periode, and this is exactly how we define the counter limit.As defined above the counter limit I choose is 8 bit width which means a maximum of 256. According to the equation above. We can solve for the pwm frequency which is approx. 97kHz.
97kHz is above the audible frequency and therfore the fan should not produce any noise or squeaking.
Next we can create an AXI lite interface in Vivado and use one of the registers to hold the value of the duty cycle. The duty cycle is also of the width 8bits meaning 256 different speed levels for the fan.
If the duty cycle is 0 then the fan will be off, if it is set to 255 then the fan will operate at full speed. So reg0 of the AXI lite interface will be used to hold the value of the duty cycle. The complete IP-Core including all the sources are availabe under my git-repo. Refer to the previous blog in order to know more about creating IP-Cores in Vivado.
Once the IP-Core is created we can now move on to create the system.
This is how the system will look like, we will have the the pwm_controller connected to the M_AXI_HPM0_LPD also the pl_clk0, which supplies the pwm_controller is set to 25MHz. The pwm_output is then made external and defined then sat to the PL pin. Now we can synthesize, implement and generate the bitstream. Then we export the hardware (xsa) including the bitstream in order to import it into our petalinux project.
Petalinux project:Usually it makes sense to test the FPGA code directly under Vivado as well as testing the complete system under a baremetal/standalone environment like Vitis. This I have done already so I will skip this part and jump directly to petalinux
Next we will create a petalinux project and import the generated xsa into the project. For the installation of petalinux you can refer to my git-repo.
In order to learn more about petalinux and the underlaying yocto project, I will be creating a custom layer that basically includes the driver and the user level application and link it with petalinux.
Lets us start with the kernel driver, the kernel driver will not be much different than the one we created to control the PL leds here. The difference is that we need to set the reg0 with duty cycle and remember that the duty cycle is 8bit wide. If we set the duty cycle to 255 then the fan will operate at the full speed because we will have a duty cycle of 100%. So in the driver we can create some default configurations that the user can choose from. For example FULL_SPEED or QUIET_MODE, or FAN_OFF.
The kernel driver structure will remain mostly the same as the one for the PL-leds, we will do the modifications to the ioctl function that will provide the "APIs " to the user application.
static long pwm_fan_control_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
struct pwm_fan_controller_local *lp = filep->private_data;
unsigned int tempVal;
switch (cmd)
{
case IOCTL_PWM_SPEED_FULLSPEED:
iowrite32( 0xff, lp->base_addr );
break;
case IOCTL_PWM_SPEED_MIDDLE:
iowrite32( 0x96, lp->base_addr );
break;
case IOCTL_PWM_SPEED_QUIET:
iowrite32( 0x0F, lp->base_addr );
break;
case IOCTL_PWM_SPEED_OFF:
iowrite32( 0x00, lp->base_addr );
break;
case IOCTL_PWM_SPEED_CUSTOM:
if ((arg > 255) || (arg < 0 ))
{
printk("Unable to set the given value, please choose between 0 and 255.\n");
}
else
{
printk("Setting the duty cycle register to %d\n", arg);
iowrite32(arg, lp->base_addr);
}
break;
}
return 0;
}
Notice here that we always write to the base_addr, this is because this is where reg0 is located, if we want to write to reg1 then we will have to plus 4 bytes. So basically the offset of reg0 is zero unlike the other unused registers.
From the code provided above, we can see that we will be offering the user different options, the user can use IOCTL_PWM_SPEED_FULLSPEED that will write 0xff (decimal 255) to the duty cycle register or IOCTL_PWM_SPEED_MIDDLE that will set the duty cycle to 0x96(decimal 150) where the duty cycle will be almost ~50%, then we have a QUIET option and fan completely switched off.
Another option that the user can choose is providing the duty cycle by himself and that is the IOCTL_PWM_SPEED_CUSTOM. There is a check to make sure that the value being set is within our duty cycle range. The complete kernel driver is found here among the bitbake recipe (.bb) and the Makefile to build the target.
PS: Setting a larger value will not cause a big issue since the reg0 is 32 bits wide but the duty cycle is only 8 bits, this will cause the upper bits to drop when assigned to the 8bits wide duty cycle. So to keep things correct we will only allow values between 0 and 255.
Next we will need to create the user level application and call it "fanspeed-cli", this time the user level application will utilized as a command line interface (cli). The user should be able to do something like fanspeed-cli --speed-config=FULL, or also set a custom speed as a percentage like fanspeed for example the user can do something like fanspeed-cli --speed=50, which will set the speed to 50%.
To achieve this we will be using a struct called option that will allow us to parse command line options. We will have two command line options as mentioned above as well as the help option to explain both of them, one will set the fan speed as percentage and the other one will set the a default fan mode. A very simple application the will use the driver and open a file handle to the device and then check the paramter provided by the user and according to it, it will use the needed ioctl. The cli application can be found here.
Next we need to add our application fanspeed-cli to the path so it can be available always, for that we will need to place it under the /usr/bin. Also I would like my board to switch on the fan automatically when booting and switching it off when powering down. To do all of this we need to extend the.bb that will compile our application and package it under /usr/bin as well as copying a oneshot systemd service-unit that will execute when powering up and powering down the board. When the system is starting we will execute the cli with the speed config MIDDLE and when powering it off the system, we will execute it with OFF.
SUMMARY = "bitbake file to manage the cli application and the corresponding systemd service"
DESCRIPTION = "compiles the cpp cli application and deploy the corresponding systemd"
LICENSE = "GPLv2"
SRC_URI = "file://pwm-fan-controller-userapp.cpp \
file://pwm-fan-controller.h \
file://fan-speed-controller.service"
inherit systemd
SYSTEMD_SERVICE:${PN} = "fan-speed-controller.service"
SYSTEMD_AUTO_ENABLE:${PN} = "enable"
do_compile() {
${CXX} ${CXXFLAGS} ${LDFLAGS} -o fanspeed-cli ${WORKDIR}/pwm-fan-controller-userapp.cpp
}
do_install() {
install -d ${D}${bindir}
install -m 0755 fanspeed-cli ${D}${bindir}/fanspeed-cli
install -d ${D}${systemd_system_unitdir}
install -m 0644 ${WORKDIR}/fan-speed-controller.service ${D}${systemd_system_unitdir}/fan-speed-controller.service
}
FILES:${PN} += "${bindir}/fanspeed-cli"
FILES:${PN} += "${systemd_system_unitdir}/fan-speed-controller.service"
The .bb above will do the stuff mentioned previously. An important note is that in recent versions of yocto (i guess Honister yocto 3.4 with kernel 5.15 ) the FILES_${PN} was altered by FILES:${PN} not changing it will not generate an error but will not make things work.
The last thing that needs to be done is that we need to tell the petalinux project, where the custom layer is for that we need to add its path to the end of this file:
nano PathToPetalinuxProject/build/conf/bblayers.conf
Next we can add the components names under the following:
nano PathToPetalinuxProjectproject-spec/meta-user/conf/user-rootfsconfig
The components under custom layer are two, the user level app and the driver so it should look something like this:
CONFIG_fanspeed-cli
CONFIG_fanspeed-pwm-driver
The last thing is to enable them and this can be done either by calling the menuconfig by:
petalinux-config -c rootfs
And enable it from there or by directly editing the file:
nano project-spec/configs/rootfs_config
And adding them:
CONFIG_fanspeed-cli=y
CONFIG_fanspeed-pwm-driver=y
Once all these changes are made we can now proceed to build the whole petalinux project and then generate the images either for the SD card or initramfs or flash, how ever you set up your board.
petalinux-build
petalinux-package --boot --fsbl --fpga --pmufw --u-boot --force
All the changes above can be observed on the board when its running in front of me, will see if I can upload a video in the future to better demonstrate the whole project :)
So this is is pretty much for this project, now we know how to control a DC fan that is connected to the PL, all the way from the lower layer up to the highest user level.
Comments
Please log in or sign up to comment.