In this tutorial we will explain how to use the BMP180 I2C sensor with the Kria KR260 Robotics Starter Kit using a Microblaze V processor. We will do the hardware design with Vivado 2025.1 and software application with Vitis Unified 2025.1. You can also download the hardware design (XSA file) and the main C file for the application on this public github repository : https://github.com/LogicTronixInc/Kria-KR260-MicroblazeV-BMP180-I2C-Sensor. The tcl file (useful to recreate the block design quickly) and the constraint file are also available.
1 - Hardware design in VivadoCreate the projectFirst create a project in Vivado. Enter the project name, choose a project location and select the create project subdirectory option.
Then for the project type leave the default option RTL Project selected. Also check option under RTL project : Do not specify sources at this time and un-check (disable) Project is an extensible Vitis platform.
In the Default Part part go to the Boards tab and search for KR260. Then select the KRIA KR260 Robotics Starter Kit SOM. Click on "Connections" under the board name to manage the board connections. Link Connector 1 on K26 SOM (SOM240_1) to Robotics Starter Kit carrier(SOM240_1) and Connector 2 on K26 SOM (SOM240_2) to Robotics Starter Kit carrier(SOM240_2) :
Then check the new project summary and click on Finish to create the project.
Block designTo create the block design you can refer to this tutorial : combining-microblaze-the-zynq-mpsoc (in the Zynq Ultrascale+ parameters, in PS-PL Configuration, under PS-PL Interfaces>Slave Interface>AXI HP just enable AXI HP1 FPD, no need to enable HP0).
More details about the MicroBlaze V Interface and details can be found at - UG1629-MicroBlaze-V-UserGuide
After doing a design like in this tutorial we need to add an AXI IIC IP and connect it (use the Run Connection Automation option). Its slave AXI port is connected to a master AXI port on the AXI interconnect, its clk is connected to the clk0 of the Zynq UltraScale+ and its reset port to the peripheral_aresetn of the Processor Reset System. Make the IIC port external. Double click on the IP, the Board Interface should be Custom.
Our block design looks like this :
Regenerate Layout (circle arrow button) and validate the design (button with a check sign in a square).
Generate output products (generate block design)Right click on the block design in the Sources window (the.bd file) and select Generate output products. You can also click on Generate Block Design from the Flow Navigator, it will open the same Generate Output Products window. Click on generate (leave the default options). It will run out-of-context modules. If any modification is made to an IP, its synthesis will need to be done again.
HDL WrapperRight click on the block design and select Create HDL Wrapper. Select Let Vivado manage wrapper and auto-update and click OK.
ConstraintsIn the flow navigator pane, click on Add sources and select Add or create constraints. Create a new file named Constraint.xdc with this content :
set_property PACKAGE_PIN H11 [get_ports IIC_scl_io]
set_property PACKAGE_PIN G10 [get_ports IIC_sda_io]
set_property IOSTANDARD LVCMOS33 [get_ports IIC_scl_io]
set_property IOSTANDARD LVCMOS33 [get_ports IIC_sda_io]We use set_property IOSTANDARD LVCMOS33 because we need 3.3V for both SCL and SDA ports.
To know how to write the constraints for the PACKAGE_PIN properties, you need to check the schematics of Kria KR260 Robotics Starter Kit and the raw pinout constraint file for the KR26 SoM.
We will connect the SCL pin of the sensor to Pmod #2 pin number 2 on the Kria board and SDA pin of the sensor to Pmod #2 pin number 4. As we can see on the Kria KR260 schematic (xtp743-kr260-schematic.zip), Pmod #2 pin number 2 is connected to PMOD2 HDA06 and Pmod #2 pin number 4 is connected to PMOD2 HDA07.
On the KR260 schematic(xtp743-kr260-schematic.zip), in the SOM240_1 CONNECTOR part, we see that HDA06 is connected to C18 and HDA07 to C19.
In the raw pinout constraint file for the KR26 SoM (K26 SOM XDC -XTP685), we can see that for the SoM connector 1 C18 is linked to the pin H11 of the KR26 SoM and C19 is linked to the pin G10 :
set_property PACKAGE_PIN G10 [get_ports "som240_1_c19"] ;# Bank 45 VCCO - som240_1_b13 - IO_L3N_AD13N_45
set_property PACKAGE_PIN H11 [get_ports "som240_1_c18"] ;# Bank 45 VCCO - som240_1_b13 - IO_L3P_AD13P_45Run synthesis, run implementation and generate bitstreamClick on Generate Bitstream from the flow navigator. It will also run the synthesis and the implementation. Leave the defaults settings and click OK.
Export hardwareTo export the hardware design, go to File > Export and click on Export Hardware. Check the Include bitstream option.
It will generate an XSA file that you will use in Vitis to create the application to get data from the sensor.
The hardware design part is now done. To switch to vitis and do the software design click on Tools > Launch Vitis IDE.
2 - Software in VitisWe use Vitis to create an application to read data from the BMP180 sensor and print temperature, pressure and altitude.
Create a new workspaceClick on Set Workspace. Create an empty directory and click on OPEN.
Create the platform componentIn the Vitis explorer window, click on Create Platform Component. Choose a name, then choose the previously exported XSA file. It takes Vitis some time to read the XSA. In the Select Operating System and Processor part, select standalone for the Operating system and microblaze_riscv_0 for the Processor. Select the Generate Boot artifacts and Generate PMU Firmware options. Then finish the platform component creation.
After it is generated, build the platform component using the flow tab in the vitis component window. The build of the application we will create after doesn't re-build the platform component so if you modify it you will have to re-build it.
Check UART portsThe KR260 BSP should already be configured the way we want it to be, but let's check it.
Open vitis-comp.json file in the Setting folder of the platform component. We will check the parameters standalone_stdin and standalone_stdout in three different locations :
psu_cortexa53_0 > zynqmp_fsbl > Board Support Package > standalone
psu_pmu_0 > zynqmp_pmufw > Board Support Package > standalone
microblaze_riscv_0 > standalone_microblaze_riscv_0 > Board Support Package > standalone
The standalone_stdin and standalone_stdout parameters should be set to psu_uart_1 because of the configuration we did in the hardware design. If not change those parameters to psu_uart_1, then go to Board Support Package and click on Regenerate BSP (for the 3 locations).
Build the platform component again if you made some changes.
Create the application componentIn the welcome tab click on Examples. Select the "Hello World" application and click on Create Application Component from Template. Change the component name as you wish. In the Select Platform part, choose the platform component previously created, click next. In the select domain part there should be only the standalone_microblaze_riscv_0 choice. Click Next and then finish.
Build the application to check for errors.
Understanding how to use the I2C sensor with the datasheetOn the BMP180 sensor datasheet, we can see the measurement flow to get the temperature and pressure :
It is also explained in more details how to get the data and do the calculations to obtain the correct values :
We can also get the altitude from the pressure by using this calculation :
We can see more details about registers and their addresses on the sensor memory map :
To read a value from the data registers we first need to write a value in the measurement control register to select what data we want in the data registers and then wait.
To know the time we need to wait, check the max conversion time :
Then we can read the values in the data registers, get UT or UP from those values, and calculate the real temperature or pressure value.
To read a value in a register, we first need to write the address of the register we wish to read, followed by an I2C restart. Then we can do the read operation.
For example, to read the temperature value we first need to write 0x2E into the control register (address 0xF4) and wait 4.5 ms. Then we need to read the data in the registers out_msb (address 0xF6) and out_lsb (address 0xF7) by doing this :
- a write sending 0xF6 followed by an I2C restart
- do the read operation (reading two bytes of data because we want the data of the 0xF6 and 0xF7 registers).
We can obtain UT value from those data. To have the real temperature value from UT we need to do some calculation, using some calibration data.
Includes, macros definition, global variables definition and function declarationIn the main c file of the application, we add the necessary includes.
We define some macros :
- one for the base address for the sensor (you can find it in the xparameters.h file in the includes of the application)
- one for the address of the I2C sensor on the I2C bus (0x77)
- a few for the registers addresses according to the sensor datasheet.
We also add some global variables and declare the functions we will be using.
#include <stdio.h>
#include <unistd.h>
#include <math.h>
#include "platform.h"
#include "xil_printf.h"
#include "xiic.h"
// Base address of the AXI I2C IP
#define AXI_IIC_BASE_ADDR XPAR_AXI_IIC_0_BASEADDR
// Address of the I2C sensor on the I2C bus
#define IIC_SLAVE_ADDR 0x77
// Registers addresses
#define ID_REG_ADDR 0xD0
#define CONTROL_REG_ADDR 0xF4
#define CALIB_REG_START_ADDR 0xAA
// Calibration data registers
#define CAL_AC1_REG 0xAA
#define CAL_AC2_REG 0xAC
#define CAL_AC3_REG 0xAE
#define CAL_AC4_REG 0xB0
#define CAL_AC5_REG 0xB2
#define CAL_AC6_REG 0xB4
#define CAL_B1_REG 0xB6
#define CAL_B2_REG 0xB8
#define CAL_MB_REG 0xBA
#define CAL_MC_REG 0xBC
#define CAL_MD_REG 0xBE
// Standard atmospheric pressure
#define atm_pressure 101325
// Buffers to write/read values to I2C device
u8 SendBuffer [2];
u8 RecvBuffer [3];
u8 Calib_Data [22];
// Calibration data variables
short AC1 = 0;
short AC2 = 0;
short AC3 = 0;
unsigned short AC4 = 0;
unsigned short AC5 = 0;
unsigned short AC6 = 0;
short B1 = 0;
short B2 = 0;
short MB = 0;
short MC = 0;
short MD = 0;
// Intermediates variables used to calculate temperature and pressure
long X1 = 0;
long X2 = 0;
long X3 = 0;
long B3 = 0;
unsigned long B4 = 0;
long B5 = 0;
long B6 = 0;
unsigned long B7 = 0;
// Oversampling ratio of the pressure measurement
short oss = 0;
// Function declaration
int is_sensor_detected();
void read_calibration_data();
u16 read_raw_temp();
u16 read_raw_press();
float calculate_real_temp(u16 UT_raw_temp);
float calculate_real_press(u16 UT_raw_temp, u16 UP_raw_press);
float calculate_altitude(int press);
void uart_print_number_with_fractional_part(float n); definitionMain functionIn the main function we first initialize the hardware platform, check if the communication with the sensor is working, and read the calibration data.
Then we do the following steps in a loop :
- Read raw temperature value
- Read raw pressure value
- Calculate real temperature value
- Calculate real pressure value
- Calculate altitude
- Print temperature to UART
- Print pressure to UART
- Print altitude to UART
- wait for 1 second.
int main()
{
init_platform();
xil_printf("Beginning I2C sensor application\n\r");
// Check if the communication is working
if(is_sensor_detected() == 0) return 0;
// Read calibration data
read_calibration_data();
u16 raw_temp_UT = 0;
u16 raw_press_UP = 0;
while (1){
// Read raw temperature value
raw_temp_UT = read_raw_temp();
// Read raw pressure value
raw_press_UP = read_raw_press();
// Calculate real temperature value
float temperature = calculate_real_temp(raw_temp_UT);
// Calculate real pressure value
int pressure = calculate_real_press(raw_temp_UT, raw_press_UP);
// Calculate altitude
int altitude = calculate_altitude(pressure);
// Print temperature to UART
xil_printf("Temperature ... ");
uart_print_number_with_fractional_part(temperature);
xil_printf(" °C\n\r");
// Print pressure to UART
xil_printf("Pressure ...... %d hPa\n\r", (pressure/100));
// Print altitude to UART
xil_printf("Altitude ...... %d m\n\r", altitude);
xil_printf("\n\r");
usleep(1000000);
}
cleanup_platform();
return 0;
}Check if the sensor is detectedTo check if the sensor is detected, we read the value in the id register (address 0xD0). It is a fixed value (0x55).
As explained before, to read a value in a register, we first need to write the address of the register we wish to read, followed by an I2C restart. Then we can do the read operation.
/** @brief Checks if the I2C sensor is detected
*
* @return 1 if the sensor is detected, 0 if not
*/
int is_sensor_detected()
{
SendBuffer[0] = ID_REG_ADDR;
XIic_Send(AXI_IIC_BASE_ADDR,IIC_SLAVE_ADDR,(u8 *)&SendBuffer, 1,XIIC_REPEATED_START);
XIic_Recv(AXI_IIC_BASE_ADDR,IIC_SLAVE_ADDR,(u8 *)&RecvBuffer, 1,XIIC_STOP);
if(RecvBuffer[0]==0x55){
xil_printf("Sensor Detected\n\r");
return 1;
}
else{
xil_printf("Sensor NOT Detected\n\r");
return 0;
}
}Calibration dataTo calculate the real temperature and pressure values, we need some calibration data contained in the registers with addresses from 0xAA to 0xBF. So we read the data of those registers and put them in global variables.
/** @brief Reads the calibration data from the calibration registers
*
* @return void
*/
void read_calibration_data()
{
SendBuffer[0] = CALIB_REG_START_ADDR;
XIic_Send(AXI_IIC_BASE_ADDR,IIC_SLAVE_ADDR,(u8 *)&SendBuffer, 1,XIIC_REPEATED_START);
XIic_Recv(AXI_IIC_BASE_ADDR,IIC_SLAVE_ADDR,(u8 *)&Calib_Data, 22,XIIC_STOP);
usleep(1000000);
AC1 = ((Calib_Data[0] << 8) | Calib_Data[1]);
AC2 = ((Calib_Data[2] << 8) | Calib_Data[3]);
AC3 = ((Calib_Data[4] << 8) | Calib_Data[5]);
AC4 = ((Calib_Data[6] << 8) | Calib_Data[7]);
AC5 = ((Calib_Data[8] << 8) | Calib_Data[9]);
AC6 = ((Calib_Data[10] << 8) | Calib_Data[11]);
B1 = ((Calib_Data[12] << 8) | Calib_Data[13]);
B2 = ((Calib_Data[14] << 8) | Calib_Data[15]);
MB = ((Calib_Data[16] << 8) | Calib_Data[17]);
MC = ((Calib_Data[18] << 8) | Calib_Data[19]);
MD = ((Calib_Data[20] << 8) | Calib_Data[21]);
}Get temperatureTo get the temperature we write 0x2E in the control register (address 0xF4), wait for 4.5 ms. Then we read the data of the 0xF6 and 0xF7 data registers. The raw temperature value UT from which we will calculate the real temperature value is composed of 0xF6 register data (MSB) and 0xF7 register data (LSB).
/** @brief Reads raw temperature from the sensor
*
* @return Raw temperature UT
*/
u16 read_raw_temp()
{
// Writes to the device are simple accesses which consist of
// the register address and the data followed by an I2C stop.
// Here we write 0x2E value in the control register (0xF4),
// so the sensor will be configured to send the temperature value
// (and not pressure) when we access the value registers.
SendBuffer[0] = CONTROL_REG_ADDR;
SendBuffer[1] = 0x2E;
XIic_Send(AXI_IIC_BASE_ADDR, IIC_SLAVE_ADDR,(u8 *)&SendBuffer, 2,XIIC_STOP);
usleep(4500); // wait 4.5 ms as mentioned in the BMP180 sensor datasheet
// Reads require an initial write to select the register we wish to read,
// followed by an I2C restart before performing the read operation.
// Here we want to read in the out_lsb and out_msb registers (address 0xF6 and 0xF7 respectively)
SendBuffer[0] = 0xF6;
XIic_Send(AXI_IIC_BASE_ADDR, IIC_SLAVE_ADDR, (u8 *)&SendBuffer, 1, XIIC_REPEATED_START);
XIic_Recv(AXI_IIC_BASE_ADDR, IIC_SLAVE_ADDR, (u8 *)&RecvBuffer, 2, XIIC_STOP);
// UT value, MSB in 0xF6 register, LSB in 0xF7 register
return (RecvBuffer[0] << 8) + ( RecvBuffer[1] );
}This calculates real temperature from the raw temperature UT obtained before and some calibration data :
/** @brief Calculates real temperature from the raw temperature value obtained from the BMP180 sensor
*
* @param UT_raw_temp The raw temperature obtained from the BMP180 sensor
* @return Real temperature value
*/
float calculate_real_temp(u16 UT_raw_temp)
{
X1 = ((UT_raw_temp-AC6) * (AC5/(pow(2,15))));
X2 = ((MC*(pow(2,11))) / (X1+MD));
B5 = X1+X2;
long temp_x10 = (B5+8)/(pow(2,4));
return temp_x10/10.0;
}We can't print a float number with xil_printf so we use this function to print the temperature with one digit after the decimal point :
/** @brief Prints a number with one digit after the decimal to uart
*
* xil_printf doesn't support %f. It only supports %d,l,x,c,s.
* To print to UART a number with digits after the decimal point
* we need to use xil_printf with two integers,
* one for the integer part and one for the fractional part
*
* @param n number to print
* @return void
*/
void uart_print_number_with_fractional_part(float n)
{
int number_integer_part, number_fractional_part;
number_integer_part = n;
number_fractional_part = (n - number_integer_part)*10;
xil_printf("%d,%d", number_integer_part, number_fractional_part);
}Get pressureTo get the pressure we write 0x34 + (oss<<6) in the control register and wait depending on the oss value. Then we read the data of the 0xF6, 0xF7 and 0xF8 data registers. We obtain the raw pressure value UP from which we will calculate the real pressure value.
/** @brief Reads raw pressure from the sensor
*
* @return Raw pressure UP
*/
u16 read_raw_press()
{
SendBuffer[0] = CONTROL_REG_ADDR;
SendBuffer[1] = 0x34 + (oss<<6); // 0x34 if oss == 0, 0x74 if oss == 1, 0xB4 if oss == 2, 0xF4 if oss == 3,
XIic_Send(AXI_IIC_BASE_ADDR, IIC_SLAVE_ADDR,(u8 *)&SendBuffer, 2,XIIC_STOP);
// wait as mentioned in the BMP180 sensor datasheet
switch (oss)
{
case (0):
usleep(4500);
break;
case (1):
usleep(7500);
break;
case (2):
usleep(13500);
break;
case (3):
usleep(25500);
break;
}
SendBuffer[0] = 0xF6;
XIic_Send(AXI_IIC_BASE_ADDR, IIC_SLAVE_ADDR, (u8 *)&SendBuffer, 1, XIIC_REPEATED_START);
XIic_Recv(AXI_IIC_BASE_ADDR, IIC_SLAVE_ADDR, (u8 *)&RecvBuffer, 3, XIIC_STOP);
// UP value, MSB in 0xF6 register, LSB in 0xF7 register, optionally XLSB in 0xF8 register
return (((RecvBuffer[0]<<16) + (RecvBuffer[1]<<8) + RecvBuffer[2]) >> (8-oss));
}We calculate the real pressure value using UP and some calibration data we read before. We also use UT to calculate some variables we need for the calculation.
/** @brief Calculates real pressure from the raw pressure value obtained from the BMP180 sensor
*
* @param UT_raw_temp The raw pressure obtained from the BMP180 sensor
* @return Real pressure value
*/
float calculate_real_press(u16 UT_raw_temp, u16 UP_raw_press)
{
float press = 0;
X1 = ((UT_raw_temp-AC6) * (AC5/(pow(2,15))));
X2 = ((MC*(pow(2,11))) / (X1+MD));
B5 = X1+X2;
B6 = B5-4000;
X1 = (B2 * (B6*B6/(pow(2,12))))/(pow(2,11));
X2 = AC2*B6/(pow(2,11));
X3 = X1+X2;
B3 = (((AC1*4+X3)<<oss)+2)/4;
X1 = AC3*B6/pow(2,13);
X2 = (B1 * (B6*B6/(pow(2,12))))/(pow(2,16));
X3 = ((X1+X2)+2)/pow(2,2);
B4 = AC4*(unsigned long)(X3+32768)/(pow(2,15));
B7 = ((unsigned long)UP_raw_press-B3)*(50000>>oss);
if (B7<0x80000000)
{
press = ((float)B7*2)/B4;
} else
{
press = ((float)B7/B4)*2;
}
X1 = (press/(pow(2,8)))*(press/(pow(2,8)));
X1 = (X1*3038)/(pow(2,16));
X2 = (-7357*press)/(pow(2,16));
press = press + (X1+X2+3791)/(pow(2,4));
return press;
}Get altitudeThe altitude can be obtained from the pressure value with the formula found on the datasheet, using the standard atmospheric pressure.
/** @brief Calculates altitude from the pressure
*
* @param UT_raw_temp The real pressure value
* @return Altitude
*/
float calculate_altitude(int press)
{
float alt = 44330*(1-(pow(((float)press/(float)atm_pressure), 1/5.255)));
return alt;
}To be able to build we need to modify the build parameters. Go on Build with the mouse and the parameters options will appear. Click on it. Go to Libraries and under Libraries (-l) click on Add Item. Add "m" library (stands for libm). When linking, it will search for libm library that contains <math.h>.
If we don't add this option there will be a build error because of the pow in the altitude calculation.
Now that we have written all the code, build the application.
Run the application on the boardConnect the BMP180 sensor to the board (according to the constraints we gave in the hardware design) and plug in the board. Use a serial port terminal, for example gtkterm, to view the printed messages.
To run the application we need to use JTAG boot. To do this create a tcl script with the following content :
proc boot_jtag { } {
############################
# Switch to JTAG boot mode #
############################
targets -set -filter {name =~ "PSU"}
# update multiboot to ZERO
mwr 0xffca0010 0x0
# change boot mode to JTAG
mwr 0xff5e0200 0x0100
# reset
rst -system
}Then in the XSDB console (in Vitis go to Vitis, XSDB Console) run :
connect
source <tcl_file_path>
boot_jtagNote - XFsbl_HookBeforeHandoff() needs to be updated at XFsbl_hooks.c as mentioned at combining-microblaze-the-zynq-mpsoc.Then run the application with the run in Vitis IDE.
This is the output we see on the serial terminal :
Output on the serial terminal
We are able to get the temperature, pressure and altitude from the BMP180 I2C sensor !
Kudos to Cédric , our team member, for creating this in-depth tutorial on MicroBlazeV, Kria KR260 and BMP180 sensor integration in Baremetal flow with 2025.1 tool.
LogicTronix is AMD-Xilinx partner for FPGA Design and Machine Learning Acceleration.
Disclaimer - the product names, logos and information used in this tutorial are for identification purposes only and may be trademarks of their respective companies.









Comments