I welcome you to this tutorial. In this tutorial you will see developing application using Renesas e2studio targeting Renesas RA2L1 microcontroller. This article was made as part of my RoadTest review about EK-RA2L1 board. If you are interested in details and my opinions about development board then you can read my review blog posts Review of Renesas EK-RA2L1 Development Board and Review of Renesas RA2L1 Microcontroller. In this article I will demonstrate simple demonstration project. Project use HTU21DHTU21D I2C temperature sensor for gathering temperature and SPI display commonly known under name Nokia 5510 for showing results to user. I will utilize DMA transfers for transferring screen buffer to the display at the background.
Due to length tutorial, I split this tutorial to two parts. This is first part of this article and second part is here. In this first part I will show how to create e2studio project, how to configure it, integrate SEGGER RTT library for printing debugging messages and finally, I will show and describe using Renesas FSP I2C Driver. In second part I will continue, and I will describe Nokia 5510 display, integration of Renesas FSP SPI Driver for communicating with display using DMA transfer and finally I will build final application integrating these two parts to final application.
Installing and starting Renesas e2studio
Before you use e2studio, you need to install them. Recommended way when working with RA family of microcontrollers is using installer with bundled FSP (Flexible Software Packages). You can download it from https://github.com/renesas/fsp/releases. Installer is easy to understand and straightforward. After installing e2studio you can start them. It is eclipse based IDE, so you need to confirm dialog window about workspace location every time you start e2studio. First time IDE will need to confirm some telemetry, so confirm dialog by pressing Apply.
Creating e2studio FSP project
At startup you will see startpage. You can create new project by clicking File > New > Renesas C/C++ project > Renesas RA.
New dialog window will appear. Confirm first page by clicking Next.
Enter some project name and confirm by Next.
At next page you need to select used development board. Then you can confirm dialog by clicking Next.
In this tutorial we will create standard executable application and we will not use any RTOS. So, just confirm dialog by clicking Next.
At next page select Bare Metal – Minimal as a template. I recommend unchecking Use Renesas Code Formatter every time when you want to use custom code formatting configuration instead of code formatting rules provided by Renesas. Finally, confirm dialog by clicking Finish.
If you get asked about opening FSP Configuration perspective, confirm dialog by Open Perspective. You can also check Remember my decision if you are bored by this dialog.
Now, close the start page.
Now you are seeing FSP configuration page. When you close this page, you can reopen it again by clicking configuration.xml in Project Explorer. This page is very important because It install selected drivers provided by Renesas to your project and generate lot of configuration structures for you. Before we start configuring this page, I also recommend doing some modifications in project setting.
Project settings
The first thing which I like to reconfigure is debugging configuration. The default configuration does not compile code on pressing debug button. It also always set breakpoint at both reset handler and main function. Finally, the last thing which I recommend reconfiguring is optimalization level. By default, project compiles code with optimalizations -O2 which makes debugging hard, so I recommend reconfiguring it to -O0 or -Og. All of these modifications are optional and you can use default settings if you like it.
Right click the project in the Project Explorer and click Debug As > Debug configurations …
Select configuration with the same name as project name in Renesas GDB Hardware Debugging section. Click Enable auto build on main page of this configuration.
Then go to Startup tab and uncheck Set breakpoint at main. Finally confirm dialog by clicking Apply and Close.
The right click project again and click Properties.
Expand C/C++ Build section, click Settings section, switch to Optimization section in pane content and change Optimization level to Optimize for debug (-Og). Then confirm dialog by clicking Apply and Apply and Close.
When you pressed apply button you most probably received message that you need to rebuild project. You can click Yer or No. It does not matter now.
Integrating SEGGER RTT
Before we start writing any code, I recommend integrate SEGGER RTT “library” to the project. SEGGER RTT is used for printing debugging messages in a more performant way than using UART. We can use this feature because EK-RA2L1 board has onboard SEGGER J-Link debugger which supports this kind of transports. e2studio do not include required library in projects created by template which we have used. You need to go to the EK-RA2L1 main page and download EK-RA2L1 Example Project Bundle - Sample Code. Downloading this file requires registration at Renesas website.
Then you need to unpack downloaded ZIP and open src folder of any available example. For example, open ek_ra2l1\iic_slave\iic_slave_ek_ra2l1_ep\e2studio\src. Then you need to copy whole SEGGER_RTT folder to src folder of your project in Project Explorer in e2studio. You should see following new 4 files in Project Explorer.
Adding and Configuring I2C Driver
The first feature which I will implement is communication with I2C Temperature sensor. The first task which we need to do is adding I2C driver to the project. At FSP Configuration page go to Stacks page, click New Stack, then select Driver > Connectivity > I2C Master Driver on r_iic_master.
The new stack will appear. Go to Properties window and change Timeout Mode of the newly created stack to Long Mode and Callback to the TEMPERATURE_I2C_Callback. Callback is name of the function which we will create later and is used for passing events from driver to your code.
Now we have included and configured driver. Now you can press green Generate Project Content button.
Next step is creating source files for our code. Let’s create TemperatureSensor.c and TemperatureSensor.h files in our src folder. Right click the src folder in Project Explorer, select New and then click File.
Set File name to TemperatureSensor.c and confirm by Finish.
Similarly, create TemperatureSensor.h file. In H file we just define two functions that we will implement in C file later. One of them is responsible for initialization and second one for reading temperature. Because we will base our small library on Renesas FSP we will return fsp_err_t status from both functions. Because of using this data type, we need to include hal_data.h file. We will also define some macros containing I2C address of sensor and command codes. The last construct used in this file is ifdef preventing generating duplicate function declarations when including TemperatureSensor.h multiple times. Copy following content to the TemperatureSensor.h file.
#include "hal_data.h" #ifndef TEMPERATURESENSOR_H_ #define TEMPERATURESENSOR_H_ #define HTU21D_I2C_ADDRESS 0x40 #define HTU21D_CMD_TEMPERATURE_HOLD 0xE3 #define HTU21D_CMD_TEMPERATURE_NOHOLD 0xF3 fsp_err_t TEMPERATURE_Init(); fsp_err_t TEMPERATURE_ReadTemperature(); #endif
Go to C file. We will need to include three files. Hal_data.h for getting standard FSP constructs, SEGGER_RTT for printing functions and finally our TemperatureSensor.h.
#include "hal_data.h" #include "SEGGER_RTT/SEGGER_RTT.h" #include "TemperatureSensor.h"
The first function which we will implement is TEMPERATURE_Init function
fsp_err_t TEMPERATURE_Init() { }
The main task of this function is initializing underlying I2C Driver. Renesas functions are named with R_ prefix followed by name of target module. Names of I2C driver functions starts with R_IIC_MASTER prefix. We need to call R_IIC_MASTER_Open function for initialize I2C driver. This function returns fsp_err_t. fsp_err_t is efficiently integer. 0 means no error and non-zero value means error. Function R_IIC_MASTER_Open requires two parameters. The first is handle (pointer) to the controller instance and second is pointer to the configuration. Both structures were generated by utility which we used for adding stack. Configured stack created g_i2c_master_ctrl as a handle g_i2c_master0_cfg structures for us. These structures contains parameters which we have configured in GUI tool. So, we just need to pass these two structures to the Open function. Structure suffixed by _ctrl is something like handle and we need to pass pointer to this structure to every FSP call related to I2C.
fsp_err_t err; err = R_IIC_MASTER_Open(&g_i2c_master0_ctrl, &g_i2c_master0_cfg);
After calling this function we need to check status. In case of error, we will print debugging message and promote error code to the caller.
if (err != FSP_SUCCESS) { SEGGER_RTT_WriteString(0, "R_IIC_MASTER_Open failed.\r\n"); return err; }
This is all from main content of this function, but we will do one more thing. I2C bus is bus which is sensitive to deadlock in case of bus errors. Deadlocks occur for example when master (which generates clock on this bus) for some reason reboots in the middle of transfer and slave (which controls data bus) waits to the next clock edge which never occurs because master generating them rebooted. HTU21DHTU21D has no timeout feature so we must carefully fix this possible issue manually. One possible solution is generating some clock cycles at the startup which free all devices on the bus from this deadlock. Renesas RA2L1 MCU has feature for doing this but this feature is not exposed using FSP Driver, so we must trigger it manually using register accesses. Because we will call this function every time when some bus error occur, I will move this logic to separate new function TEMPERATURE_ResetBus. For now, you can complete Init function by calling this new function.
return TEMPERATURE_ResetBus();
Now declare TEMPERATURE_ResetBus function.
static fsp_err_t TEMPERATURE_ResetBus() { }
As mentioned above, first task of this function is generating some dummy clock cycles. I will generate 20 of them which is enough to terminate all possible transaction on bus. Generation is done by writing 1 to the CLO bit in configuration register and waiting until hardware sets it back to 0. Registers you can access using control variable (which you have already seen as a first parameter in R_IIC_MASTER_Open call in TEMPERATURE_Init function. Register block is directly accessible using p_reg field of this structure.
for (int i = 0; i < 20; i++) { g_i2c_master0_ctrl.p_reg->ICCR1_b.CLO = 1; while (g_i2c_master0_ctrl.p_reg->ICCR1_b.CLO) { } }
The second task related to resetting bus is that we need to terminate any pending I2C operation on our side. We can do this by R_IIC_MASTER_Abort function. This is not important for resolving I2c deadlock, but it is important in case when some error occur on our side and we need to reset I2C controller. Function expects pointer to controller structure. If no errors occur, then we will return success status code. Whole ResetBus function looks as follows:
static fsp_err_t TEMPERATURE_ResetBus() { fsp_err_t err; // issue 20 dummy cycles for (int i = 0; i < 20; i++) { g_i2c_master0_ctrl.p_reg->ICCR1_b.CLO = 1; while (g_i2c_master0_ctrl.p_reg->ICCR1_b.CLO) { } } err = R_IIC_MASTER_Abort(&g_i2c_master0_ctrl); if (err != FSP_SUCCESS) { SEGGER_RTT_WriteString(0, "R_IIC_MASTER_Abort failed.\r\n"); return err; } return FSP_SUCCESS; }
The second function which we need to implement is function for reading temperature from sensor. At beginning we will declare variable for handling errors from inner calls.
fsp_err_t TEMPERATURE_ReadTemperature(float *temperature) { fsp_err_t err; }
In this function we need
- Generate start condition
- Transmit HTU21DHTU21D I2C address 0x40 and write direction
- Transmit command 0xF3 for starting temperature measurement inside sensor
- Generate stop sequence
- Wait at least 50 milliseconds for completion of measurement
- Transmit HTU21DHTU21D I2C address 0x40 and read direction
- Receive 3 bytes from sensor
- Generate stop sequence
- Parse and return temperature
Most I2C stuff is handled by FSP Driver automatically at background.
The first thing which we need to do is configure slave address of targeted device. We can do this using R_IIC_MASTER_SlaveAddressSet function. Function requires three parameters. First parameter is pointer to control structure (It is pointer to structure suffixed by _ctrl. We have seen it in all previous R_IIC_MASTER calls specified as first parameter). Second parameter is device address and third parameter is enum value specifying that we are using standard 7bit address and we are not using extended 10bit addresses.
err = R_IIC_MASTER_SlaveAddressSet(&g_i2c_master0_ctrl, HTU21D_I2C_ADDRESS, I2C_MASTER_ADDR_MODE_7BIT);
if (err != FSP_SUCCESS) {
SEGGER_RTT_WriteString(0, "R_IIC_MASTER_SlaveAddressSet failed.\r\n");
return err;
}
This operation does not trigger any operation on bus, so we do not need to reset bus when error occur.
The next task needed to do is starting write transfer. We will start write transfer using R_IIC_MASTER_Write function and we will pass single byte buffer to transfer. Function do transfer at background and gives us result using callback which we have configured in GUI. We need to define this callback now. Callback receive status as an argument. We will just store it to the global variable. We will use this global variable later. Define i2c_event global variable of type i2c_master_event_t at the beginning of file and define following callback function:
static i2c_master_event_t i2c_event = 0; void TEMPERATURE_I2C_Callback(i2c_master_callback_args_t *p_args) { i2c_event = p_args->event; }
Go back to TEMPERATURE_ReadTemperature function. Before we start transaction, we will reset i2c_event global variable from previous run. Then we start transaction and wait for result (in other words we will wait until global variable change). Because we are transmitting single byte, we can use just simple variable and pass pointer to this variable to the R_IIC_MASTER_Write function as a buffer.
uint8_t command = HTU21D_CMD_TEMPERATURE_NOHOLD; i2c_event = 0; err = R_IIC_MASTER_Write(&g_i2c_master0_ctrl, &command, 1, false); if (err != FSP_SUCCESS) { TEMPERATURE_ResetBus(); SEGGER_RTT_WriteString(0, "R_IIC_MASTER_Write failed.\r\n"); return err; } uint32_t timeout = 1000; while ((i2c_event == 0) && timeout--) { R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MILLISECONDS); } if (i2c_event == 0) { TEMPERATURE_ResetBus(); SEGGER_RTT_printf(0, "Detected I2C timeout.\r\n"); return FSP_ERR_TIMEOUT; } if (i2c_event != I2C_MASTER_EVENT_TX_COMPLETE) { TEMPERATURE_ResetBus(); SEGGER_RTT_printf(0, "Unexpected I2C event %02x (expected %02x).\r\n", i2c_event, I2C_MASTER_EVENT_TX_COMPLETE); return FSP_ERR_ABORTED; }
In this point if no error occurred, then we can procced to next state. We need to wait at least 50 milliseconds (I will use 75 ms to include some tolerance) and then we can read result from sensor. Reading is very similar to writing. We will reset global variable, start transaction, wait for callback and then we evaluate result.
R_BSP_SoftwareDelay(75, BSP_DELAY_UNITS_MILLISECONDS);
uint8_t output[3]; i2c_event = 0; err = R_IIC_MASTER_Read(&g_i2c_master0_ctrl, output, sizeof(output), false); if (err != FSP_SUCCESS) { TEMPERATURE_ResetBus(); SEGGER_RTT_WriteString(0, "R_IIC_MASTER_Read failed.\r\n"); return err; } timeout = 1000; while ((i2c_event == 0) && timeout--) { R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MILLISECONDS); } if (i2c_event == 0) { TEMPERATURE_ResetBus(); SEGGER_RTT_printf(0, "Detected I2C timeout.\r\n"); return FSP_ERR_TIMEOUT; } if (i2c_event != I2C_MASTER_EVENT_RX_COMPLETE) { TEMPERATURE_ResetBus(); SEGGER_RTT_printf(0, "Unexpected I2C event %02x (expected %02x).\r\n", i2c_event, I2C_MASTER_EVENT_TX_COMPLETE); return FSP_ERR_ABORTED; }
Finally we need to parse temperature (float) from output array. I will use _HTU21D_ParseTemperature function which I use in other projects utilizing this sensor. After parsing value, we can return success.
_HTU21D_ParseTemperature(temperature, output); return FSP_SUCCESS;
Function _HTU21D_ParseTemperature looks as follows:
void _HTU21D_ParseTemperature(float* outputTemperature, uint8_t* buffer) { uint16_t rawValue = (((uint16_t) buffer[0]) << 8) | (((uint16_t) buffer[1]) << 0); // remove status from data rawValue &= ~(0x03); *outputTemperature = -46.85 + 175.72 * (float) rawValue / (float) ((uint32_t)1 << 16); }
Now go to the hal_entry.c file in src folder. I will modify this file a lot because RA2L1 has no support for TrustZone and most content of this file is useless in most cases on this MCU. At beginning we will include FSP hal_data.h, SEGGER RTT, stdio, and our TemperatureSensor.h.
#include "hal_data.h" #include "SEGGER_RTT/SEGGER_RTT.h" #include <stdio.h> #include "TemperatureSensor.h"
Remove definition and declaration of R_BSP_WarmStart including wrapping macros. Remove content inside BSP_TZ_SECURE_BUILD macros because this is not used on RA2L1 platform.
In hal-entry function create variable for handling status.
fsp_err_t status;
Then you can write some debugging message to check that hal_entry was correctly called and debugging output works.
SEGGER_RTT_WriteString(0, "starting...\r\n");
At the beginning of effective code, we need to initialize IOPORT driver. This driver was included in the project by default, but we removed it when we were cleaning hal_entry.c file. In hal_entry I will use assert function for checking for errors which should never occurs. It simplifies debugging because debugger can stop at these lines when error occurs. But of course, you should never use it for checking at errors at production.
status = R_IOPORT_Open(&g_ioport_ctrl, &g_bsp_pin_cfg); assert(status == FSP_SUCCESS);
Then we will initialize our mini library
status = TEMPERATURE_Init(); assert(status == FSP_SUCCESS);
and finally in inifinite loop we can read temperatures and print them to debugging output. SEGGER_RTT_printf function has no support floats so we need to use a little trick for receiving part before and after decimal dot.
while (1) { float temperature; status = TEMPERATURE_ReadTemperature(&temperature); if (status != FSP_SUCCESS) { SEGGER_RTT_WriteString(0, "Error while reading temperature\r\n"); } else { int temperature100 = (int)(temperature * 100); SEGGER_RTT_printf(0, "temperature: %2d.%d\r\n", temperature100 / 100, temperature100 % 100); } }
This is all. You can press debug button. Open J-Link RTT Viewer (which you can download from SEGGER website), connect to the target and see outputs from your application. You should see something like following screenshot. In second part of this tutorial, I will implement driver and small library for controlling display and make final project showing temperature on display instead of showing it in debugging output only.