Introduction Hardware Connections Device Tree Configuration Rebuilding Image Requirements Kernel Device Tree Application Driver Main Init Driver Write Read Building and Run the SPI Application Driver Conclusion Introduction This document describes how to interface and use Xtrinsic FXLS8471Q digital accelerometer with IMX6Q processor. For this purpose, UDOO Quad board is used with NXP Linux image for IMX6QSABRE-SD board with Kernel 3.14.56 (not with Udoobuntu) to simulate working with a custom board, the process to customize and build such image for UDOO Quad board is described here: Building Linux Image with QT5 for UDOO Quad On the sensor side FRDM-FXS-MULTI(-B) sensor expansion board which features many of the Xtrinsic sensors introduced in 2013 including the FXSL8471Q is used. Hardware Connections The SPI signals from FRDM-FXS-MULTI are routed to SPI1 module of UDOO Quad. FXLS8471Q provides an INT pin which is indicated on image below, however on this implementation polling method is used. Please note that Chip Select is not controlled automatically by SPI module, therefore this pin is configured as GPIO. Besides these signals, reset and power source pins were also connected. The following figure shows the pins used in FRDM-FXS-MULTI which are connected with UDOO Quad board. The pins used on the UDOO Quad side are shown in the images below. You can find UDOO Quad pinout diagram here Index of /download/files/pinout. Device Tree Configuration As mentioned at the beginning of this document, a NXP Linux image for IMX6Q-SABRESD is used. In order to customize this image to be used with the UDOO Quad board it is necessary to build a .dtb that matches with it. This task was accomplished obtaining dts and dtsi source files listed below from Kernel Linux Repository for UDOO at UDOOboard (UDOOboard) · GitHub. imx6qdl-udoo.dtsi imx6qdl-udoo-externalpins.dtsi imx6q-udoo-hdmi.dts These files were copied into the IMX6Q-SABRESD build source folder and a imx6q-udoo.dtb was generated. This process is described on Building Linux Image with QT5 for UDOO Quad The following snippets show how the nodes involved on the SPI configuration were set for UDOO Quad board. Please note that each device tree should match your custom board. In imx6qdl-udoo-externalpins.dtsi verify that ecspi1 node matches with the one shown below. &ecspi1 {
fsl,spi-num-chipselects = <1>;
cs-gpios = <&gpio5 17 0>;
pinctrl-0 = <&pinctrl_ecspi1>;
pinctrl-names = "default";
status = "disabled";
spidev0: spi@0 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "spidev";
reg = <0>;
spi-max-frequency = <2000000>;
};
}; As you can see on snippet above, ecspi1 node uses pinctrl_ecspi1 as the pin configuration node which is defined in the same file and it looks as follows. pinctrl_ecspi1: ecspi1grp {
fsl,pins = <
MX6QDL_PAD_DISP0_DAT22__ECSPI1_MISO 0x100b1
MX6QDL_PAD_DISP0_DAT20__ECSPI1_SCLK 0x100b1
MX6QDL_PAD_DISP0_DAT21__ECSPI1_MOSI 0x100b1
MX6QDL_PAD_DISP0_DAT23__GPIO5_IO17 0x80000000
>;
}; However, as you may know each pin on i.MX devices has up to 8 potential functions, and on the other side, one function can be available in different pins. For example, ECSPI1_MISO can be mapped to 4 different pins and each pin can have different functions. From all the available functions in a pin one is chosen to be the pad (pin) name. In the image below DISP0_DATA22 was chosen to be the pad name. Now, back to pinctrl_ecspi1 node, the macros used here are defined in imx6q-pinfuc.h, as you can see DISP0_DAT20 - DISP0_DAT23 are the pads used with the ECSPI signals. It is necessary to check it that there are no other configurations for DISP0_DAT20 - DISP0_DAT23 and if they are it is necessary to comment out them or delete them. In this case there were other configurations for these pins in imx6qdl-udoo-externalpins.dtsi and they were commented out. &iomuxc {
imx6q-udoo {
// External Pinout GPIOs
external_hog: hoggrp-2 {
fsl,pins = <
MX6QDL_PAD_CSI0_DAT11__GPIO5_IO29 0x80000000 // {{external-gpio-0}}
MX6QDL_PAD_CSI0_DAT10__GPIO5_IO28 0x80000000 // {{external-gpio-1}}
MX6QDL_PAD_SD1_CLK__GPIO1_IO20 0x80000000 // {{external-gpio-2}}
MX6QDL_PAD_SD1_DAT0__GPIO1_IO16 0x80000000 // {{external-gpio-3}}
MX6QDL_PAD_SD1_DAT1__GPIO1_IO17 0x80000000 // {{external-gpio-4}}
MX6QDL_PAD_SD1_CMD__GPIO1_IO18 0x80000000 // {{external-gpio-5}}
MX6QDL_PAD_SD4_DAT1__GPIO2_IO09 0x80000000 // {{external-gpio-6}}
MX6QDL_PAD_SD4_DAT2__GPIO2_IO10 0x80000000 // {{external-gpio-7}}
MX6QDL_PAD_SD1_DAT3__GPIO1_IO21 0x80000000 // {{external-gpio-8}}
MX6QDL_PAD_SD1_DAT2__GPIO1_IO19 0x80000000 // {{external-gpio-9}}
MX6QDL_PAD_GPIO_1__GPIO1_IO01 0x80000000 // {{external-gpio-10}}
MX6QDL_PAD_GPIO_9__GPIO1_IO09 0x80000000 // {{external-gpio-11}}
MX6QDL_PAD_GPIO_3__GPIO1_IO03 0x80000000 // {{external-gpio-12}}
MX6QDL_PAD_SD4_DAT0__GPIO2_IO08 0x80000000 // {{external-gpio-13}}
MX6QDL_PAD_CSI0_DAT4__GPIO5_IO22 0x80000000 // {{external-gpio-14}}
MX6QDL_PAD_CSI0_DAT16__GPIO6_IO02 0x80000000 // {{external-gpio-15}}
MX6QDL_PAD_CSI0_DAT14__GPIO6_IO00 0x80000000 // {{external-gpio-16}}
MX6QDL_PAD_CSI0_DAT15__GPIO6_IO01 0x80000000 // {{external-gpio-17}}
MX6QDL_PAD_CSI0_DAT12__GPIO5_IO30 0x80000000 // {{external-gpio-18}}
MX6QDL_PAD_CSI0_DAT13__GPIO5_IO31 0x80000000 // {{external-gpio-19}}
MX6QDL_PAD_EIM_D28__GPIO3_IO28 0x80000000 // {{external-gpio-20}}
MX6QDL_PAD_EIM_D21__GPIO3_IO21 0x80000000 // {{external-gpio-21}}
MX6QDL_PAD_DISP0_DAT6__GPIO4_IO27 0x80000000 // {{external-gpio-22}}
MX6QDL_PAD_DISP0_DAT7__GPIO4_IO28 0x80000000 // {{external-gpio-23}}
MX6QDL_PAD_DISP0_DAT8__GPIO4_IO29 0x80000000 // {{external-gpio-24}}
MX6QDL_PAD_DISP0_DAT9__GPIO4_IO30 0x80000000 // {{external-gpio-25}}
MX6QDL_PAD_DISP0_DAT10__GPIO4_IO31 0x80000000 // {{external-gpio-26}}
MX6QDL_PAD_DISP0_DAT11__GPIO5_IO05 0x80000000 // {{external-gpio-27}}
MX6QDL_PAD_DISP0_DAT12__GPIO5_IO06 0x80000000 // {{external-gpio-28}}
MX6QDL_PAD_DISP0_DAT13__GPIO5_IO07 0x80000000 // {{external-gpio-29}}
MX6QDL_PAD_DISP0_DAT14__GPIO5_IO08 0x80000000 // {{external-gpio-30}}
MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09 0x80000000 // {{external-gpio-31}}
MX6QDL_PAD_DISP0_DAT16__GPIO5_IO10 0x80000000 // {{external-gpio-32}}
MX6QDL_PAD_DISP0_DAT17__GPIO5_IO11 0x80000000 // {{external-gpio-33}}
MX6QDL_PAD_DISP0_DAT18__GPIO5_IO12 0x80000000 // {{external-gpio-34}}
MX6QDL_PAD_DISP0_DAT19__GPIO5_IO13 0x80000000 // {{external-gpio-35}}
//MX6QDL_PAD_DISP0_DAT20__GPIO5_IO14 0x80000000 // {{external-gpio-36}}
//MX6QDL_PAD_DISP0_DAT21__GPIO5_IO15 0x80000000 // {{external-gpio-37}}
MX6QDL_PAD_EIM_A16__GPIO2_IO22 0x80000000 // {{external-gpio-38}}
MX6QDL_PAD_GPIO_18__GPIO7_IO13 0x80000000 // {{external-gpio-39}} (KEY_VOL_UP)
MX6QDL_PAD_NANDF_D0__GPIO2_IO00 0x80000000 // {{external-gpio-40}} (HOME)
MX6QDL_PAD_NANDF_D3__GPIO2_IO03 0x80000000 // {{external-gpio-41}} (SEARCH)
MX6QDL_PAD_NANDF_D2__GPIO2_IO02 0x80000000 // {{external-gpio-42}} (BACK)
MX6QDL_PAD_NANDF_D1__GPIO2_IO01 0x80000000 // {{external-gpio-43}} (MENU)
MX6QDL_PAD_GPIO_19__GPIO4_IO05 0x80000000 // {{external-gpio-44}} (KEY_VOL_DOWN)
// MX6QDL_PAD_DISP0_DAT22__GPIO5_IO16 0x80000000 // {{external-gpio-45}}
//MX6QDL_PAD_DISP0_DAT23__GPIO5_IO17 0x80000000 // {{external-gpio-46}}
MX6QDL_PAD_EIM_D25__GPIO3_IO25 0x80000000 // {{external-gpio-47}}
MX6QDL_PAD_KEY_ROW1__GPIO4_IO09 0x80000000 // {{external-gpio-48}}
MX6QDL_PAD_KEY_COL1__GPIO4_IO08 0x80000000 // {{external-gpio-49}}
MX6QDL_PAD_EIM_OE__GPIO2_IO25 0x80000000 // {{external-gpio-50}}
MX6QDL_PAD_EIM_CS1__GPIO2_IO24 0x80000000 // {{external-gpio-51}}
MX6QDL_PAD_EIM_CS0__GPIO2_IO23 0x80000000 // {{external-gpio-52}}
MX6QDL_PAD_EIM_D24__GPIO3_IO24 0x80000000 // {{external-gpio-53}}
MX6QDL_PAD_GPIO_8__GPIO1_IO08 0x80000000 // {{external-gpio-54}}
MX6QDL_PAD_GPIO_7__GPIO1_IO07 0x80000000 // {{external-gpio-55}}
>;
}; Finally, in imx6q-udoo-hdmi.dts enable ECSPI by including an ecspi1 node reference and setting the status property to "okay". dts-v1/;
#include "imx6q.dtsi"
#include "imx6qdl-udoo.dtsi"
#include "imx6qdl-udoo-externalpins.dtsi"
/ {
model = "UDOO Quad Board";
compatible = "udoo,imx6q-udoo", "fsl,imx6q"; mxcfb1: fb@0 {
compatible = "fsl,mxc_sdc_fb";
disp_dev = "hdmi";
interface_pix_fmt = "RGB24";
mode_str ="1920x1080M@60";
default_bpp = <32>;
int_clk = <0>;
late_init = <0>;
status = "okay";
};
};
&ecspi1 {
status = "okay";
};
Note: This could be done in .dtsi but the side effect is that any configuration including the dtsi will have ECSPI1 enabled by default. Finally it is necessary to rebuild the device tree and copy it to the FAT partition of the sdcard. This process is explained in next section. Rebuilding Image Requirements In order to build an image as well as an application it is necessary to install a tool to cross-compile code, this way we will be able to generate executable files for ARM architecture in our host machine. This tool is called meta-toolchain, the following commands are used to install it. In the following instructions it is considered that the build directory is ~/fsl-release_bsp/build_imx6qsabresd_qt5/ $ cd ~/fsl-release_bsp $ source setup-environment build_imx6qsabresd_qt5 $ bitbake meta-toolchain $ sh tmp/deploy/sdk/fsl-imx-x11-glibc-x86_64-meta-toolchain-cortexa9hf-vfp-neon-toolchain-<kernel>.sh Kernel In order to enable the kernel SPI driver it is necessary to set it in the menuconfig and rebuild zImage. Go to kernel directory within the Yocto build directory that we created. $ cd <build_directory>/tmp/work-shared/imx6qsabresd/kernel-source$ Launch menuconfig for imx $ source /opt/fsl-imx-x11/<kernel version>/environment-setup-armv7a-vfp-neon-poky-linux-gnueabi $ make imx_v7_defconfig $ make menuconfig Enable SPI Driver by going to Device Drivers --> SPI Support and seting '*' to SPI device Driver, save changes and then exit. And build zImage $ make zImage The generated file is located in <build_directory>/tmp/work-shared/imx6qsabresd/kernel-source/arch/arm/boot$ and it must be copied to the FAT partition of the SD card. Device Tree To build the device tree go to kernel directory within the Yocto build directory that we created. $ cd <build_directory>/tmp/work-shared/imx6qsabresd/kernel-source$ Build dtb file $ source /opt/fsl-imx-x11/<kernel version>/environment-setup-armv7a-vfp-neon-poky-linux-gnueabi $ make imx_v7_defconfig $ make imx6q-udoo-hdmi.dtb The generated file is located in <build_directory>/tmp/work-shared/imx6qsabresd/kernel-source/arch/arm/boot/dts$ and it must be renamed as imx6q-udoo.dtb and copied to the FAT partition of the SD card. Application Driver The SPI kernel driver uses a structure named spi_ioc_transfer which describes a single SPI transfer. It holds pointers to userspace buffers with transmit and receive data, length of buffers, speed, bits per word among other configurations. For further details you can refer to /include/uapi/linux/spi/spidev.h on your kernel source. In the next section the application driver is explained. The source files of the application driver as well as the device tree sources can be found in the attached.zip file. Main The following code shows function main where the SPI driver is initialized driver using "/dev/spidev0.0" which is listed under /dev (in the target root file system) after SPIdev driver is enabled in menuconfig and kernel is rebuilt as indicated in previous section. Then it reads who am i register just for sanity check purpose and initializes and calibrates the sensor and enters in an endless loop where it reads the sensor whenever there is new data ready. Raw data is returned in 6 bytes, so it is managed to get X, Y and Z values and finally these values are converted to G's values and it waits until a key is pressed to continue to read the following value. /****************************************************************************** * Main ******************************************************************************/
int main(){
//enableGPIO(); //In case of using interrupt instead of polling
file = spi_init("/dev/spidev0.0"); //dev
who();
FXLS8471Q_Init();
FXLS8471Q_Calibration();
while(1)
{
checkData();
if (DataReady) // Is a new set of data ready?
{
buffer = (unsigned char *)spi_read(OUT_X_MSB_REG, 6, file); // Read data output registers 0x01-0x06
printf("AccData[0] = 0x%X \n AccData[1] = 0x%X \n AccData[2] = 0x%X \n AccData[3] = 0x%X \n AccData[4] = 0x%X \n AccData[5] = 0x%X \n", AccData[0], AccData[1], AccData[2], AccData[3], AccData[4], AccData[5]);
Xout_14_bit = ((short) (AccData[0] << 8 | AccData[1])) >> 2; // Compute 14-bit X-axis output value
Yout_14_bit = ((short) (AccData[2] << 8 | AccData[3])) >> 2; // Compute 14-bit Y-axis output value
Zout_14_bit = ((short) (AccData[4] << 8 | AccData[5])) >> 2; // Compute 14-bit Z-axis output value
Xout_g = ((float) Xout_14_bit) / SENSITIVITY_2G; // Compute X-axis output value in g's
Yout_g = ((float) Yout_14_bit) / SENSITIVITY_2G; // Compute Y-axis output value in g's
Zout_g = ((float) Zout_14_bit) / SENSITIVITY_2G; // Compute Z-axis output value in g's
//printf(" X = %d Y = %d Z = %d \n\n", AccData[0], AccData[2], AccData[4]);
//printf("Xval = %d Yval = %d Zval = %d \n", Xout_14_bit, Yout_14_bit, Zout_14_bit);
printf(" XG = %f YG = %f ZG = %f \n\n", Xout_g, Yout_g, Zout_g);
getchar();
}
}
close(file);
} Init Driver The spi_init function opens a file for the driver "/dev/spidev0.0" which is passed as a parameter from main(), then the SPI configuration parameters are read just for informative purpose. Finally struct xfer which is of type spi_ioc_transfer is initialized. /********************************* SPIdev Init **********************************************/
int spi_init(char filename[40])
{
int file;
unsigned char mode, lsb, bits;
unsigned int baudrate = 524250, speed;
printf("SPI Init \n");
if ((file = open(filename,O_RDWR)) < 0)
{
printf("Failed to open the bus.");
/* ERROR HANDLING; you can check errno to see what went wrong */
com_serial=0;
exit(1);
}
if (ioctl(file, SPI_IOC_RD_MODE, &mode) < 0)
{
perror("SPI rd_mode");
return -1;
}
if (ioctl(file, SPI_IOC_RD_LSB_FIRST, &lsb) < 0)
{
perror("SPI rd_lsb_fist");
return -1;
}
if (ioctl(file, SPI_IOC_RD_BITS_PER_WORD, &bits) < 0)
{
perror("SPI bits_per_word");
return -1;
}
if (ioctl(file, SPI_IOC_RD_MAX_SPEED_HZ, &speed) < 0)
{
perror("SPI max_speed_hz");
return -1;
}
printf("%s: spi mode %d, %d bits %s per word, %d Hz max\n",filename, mode, bits, lsb ? "LSB first" : "MSB first", baudrate);
xfer[0].len = 3; /* Length of command to write*/
xfer[0].cs_change = 0; /* Keep CS activated */
xfer[0].delay_usecs = 0; //delay in us
xfer[0].speed_hz = 524250; //speed
xfer[0].bits_per_word = 8; // bites per word 8
xfer[1].len = 4; /* Length of Data to read */
xfer[1].cs_change = 0; /* Keep CS activated */
xfer[1].delay_usecs = 0;
xfer[1].speed_hz = 524250;
xfer[1].bits_per_word = 8;
printf("SPI Init Finished \n");
return file;
} Write The SPI communication is started with the falling edge on chip select pin. A write operation is initiated by transmitting a 1 for the R/W bit. Then the 8-bit register address, ADDR[7:0] is encoded in the first and second serialized bytes. Data to be written starts in the third serialized byte. The order of the bits is as follows: Byte 0: R/W, ADDR[6], ADDR[5], ADDR[4], ADDR[3], ADDR[2], ADDR[1], ADDR[0] Byte 1: ADDR[7], X, X, X, X, X, X, X Byte 2: DATA[7], DATA[6], DATA[5], DATA[4], DATA[3], DATA[2], DATA[1], DATA[0] The SPI communication is finished with the falling edge on chip select pin. A you can see below array buf keeps the destination address and the data to be transferred, then xfer structure is configured to point to buf as the transfer buffer and the length of the data es set to 2 + data size (the first 2 bytes are for the destination address which is splitted in 2 bytes). Finally the transfer is started by the ioctl command. /******************** Write a byte to the FXLS8471Q ***************************
* Byte 0: 1,ADDR[6],ADDR[5],ADDR[4],ADDR[3],ADDR[2],ADDR[1],ADDR[0]
* Byte 1: ADDR[7],0,0,0,0,0,0,0
* Byte 2: DATA[7],DATA[6],DATA[5],DATA[4],DATA[3],DATA[2],DATA[1],DATA[0]
******************************************************************************/
void spi_write(int registerAddress, int nbytes, char data, int file)
{
unsigned char buf[32];
int status;
memset(buf, 0, sizeof buf);
buf[0] = 0x80 | registerAddress;
buf[1] = 0x80 & registerAddress;
buf[2] = data;
xfer[0].tx_buf = (unsigned long)buf;
xfer[0].len = nbytes + 2; /* Length of command to write*/
status = ioctl(file, SPI_IOC_MESSAGE(1), xfer);
if (status < 0)
{
perror("SPI_IOC_MESSAGE");
return;
}
com_serial=1;
failcount=0;
} Below is the write operation which writes the value 0x3D to the CTRL_REG1 (0x2A). Read Similarly a read operation is initiated by transmitting a 0 for the R/W bit. Then the 8-bit register address, ADDR[7:0] is encoded in the first and second serialized bytes. The data is read from the MISO pin (MSB first). In this case the array buf keeps the address that is going to be read and the third byte is just a dummy byte to be transferred. Structure xfer keeps transfer buffer pointer which in this case is buf and receive buffer which is AccData, the lenght of the command to write and the length of data to read is also specified and finally the read command is executed with ioctl call. /********************** Read a byte from the FXLS8471Q ***********************
* Byte 0: 0,ADDR[6],ADDR[5],ADDR[4],ADDR[3],ADDR[2],ADDR[1],ADDR[0]
* Byte 1: ADDR[7],0,0,0,0,0,0,0
* Byte 2: 0,0,0,0,0,0,0,0
******************************************************************************/
char * spi_read(int registerAddress, int nbytes, int file)
{
int status;
memset(buf, 0, sizeof buf);
memset(AccData, 0, sizeof AccData);
buf[0] = 0x7F & registerAddress;
buf[1] = 0x80 & registerAddress;
buf[2] = 0x00;
xfer[0].tx_buf = (unsigned long)buf;
xfer[0].len = 2; /* Length of command to write*/
xfer[1].rx_buf = (unsigned long) AccData;
xfer[1].len = nbytes; /* Length of Data to read */
xfer[1].speed_hz = 524250;
status = ioctl(file, SPI_IOC_MESSAGE(2), xfer);
if (status < 0)
{
perror("SPI_IOC_MESSAGE");
return 0;
}
com_serial=1;
failcount=0;
return AccData;
} The screenshot below shows the read operation which reads the correct value 0x6A from the WHO_AM_I register (0x0D). Building and Run the SPI Application Driver In order to build the application please save the source files and the Makefile (attached) on any place on your host machine and go to that directory where you saved them. Then build the application with the Meta-toolchain using the Make file with the following commands. Build application driver $ cd <folder wher spi device driver source is saved> $ source /opt/fsl-imx-x11/<kernel version>/environment-setup-armv7a-vfp-neon-poky-linux-gnueabi $ make A spi_test file will be created in the same folder, copy this file into the sdcard on the Root File System partition in /home/root. Finally when booting the target execute the application. You must see the following output. Conclusion This document summarizes the steps to create a SPI application driver. As you saw it is necessary to rebuild the device tree and the kernel, and to do this it is necessary to install metatoolchain. The application driver uses the SPI kernel driver and its main functions are next: Init SPI Driver Read Write Init sensor Callibrate Sensor Polling data ready function I would say that the core of the application driver are the read and write functions which configure the SPI kernel driver and pass data to it in the format required by the sensor. For specific details on the driver please see the attached .zip file which contains the application driver code, the Makefile with the one it is build and the device tree sources. I hope you find this document useful. Carlos
View full article