I welcome you to second part of this tutorial. 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 you can read my review Review of Renesas EK-RA2L1 Development Board and details about microcontroller are as part of Review of Renesas RA2L1 Microcontroller.
Due to tutorial length, I split it to two parts. This is second part of tutorial and first part is here. In first part I have shown how to create e2studio project, how to configure it, integrate SEGGER RTT library for printing debugging messages and finally, I shown and described using Renesas FSP I2C Driver. In this second part 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.
Writing Nokia 5510 SPI Display Driver
In the next section we will create second driver. I will show implementing double buffered display driver. I will allow user to draw bitmaps and texts to the display buffer. When the user finish screen, driver will switch buffers and transmit previously filled buffer to the display. I will transfer buffer to the display on background using DMA (It is referred as a Data Transfer Controller (DTC) on the RA2L1 platform). This means that at the time of transferring buffer to the display user can run its own code. For example, user can read temperature from HTU21DHTU21D sensor and/or fill second buffer with next screen while transfer is in progress. This is reason why we need two buffers – one available to user and second which do not change while transfer is in progress.
Nokia 5510 display is connected using unidirectional SPI bus with added D/C signal which identifies commands from pixel data. MISO wire is not used because display do not produce any output. We will manipulate D/C signal manually. Similarly, we will manipulate reset signal of display manually.
Nokia 5510 Display Structure
Display is structured to the lines. There are 6 lines. Each line has 8 vertical pixels in a column. So, display has 6 × 8 = 48 pixels in column. Display width is 84 pixels. Totally there are 48 × 84 = 4032 pixels. Because every pixel is encoded using single bit (display is black and white only), so we need buffer of size 4032 / 8 = 504 bytes. It is small display but still very useful and frequently used in many hobby projects. Display structure looks as follows:
Display holds row and column counter. When transferring data to the display we need transfer first line first, then we can send second line and so on. The structure of the single line is following:
The pixels are transmitted in columns. The first transferred byte describes content of first column in active row. So, data are not transferred row by row, but they are transferred as (8 pixels) columns of active line. This can look scared at the first time, but it is useful when drawing texts because texts are usually drawn per line. I will also draw bitmap in this tutorial. I must take this structure in account when encoding bitmaps. The minimum transfer unit is 1 byte, so you always need to transfer whole column of one line. The display holds line and column pointer. Every time when you do transfer it increments column pointer. If column pointer overflows the columns, then pointers are adjusted to the next line.
For now, we know everything important about display and its structure and we can move to writing code. But before we start writing code, we need to add Renesas FSP Driver to our project.
Adding SPI Driver to the project
Open configuration.xml file. Go to Stacks tab, then click New Stack and select Driver > Connectivity > SPI Driver on r_spi in the menu.
Then click the newly created stack, go to Properties window, change Callback to DISPLAY_SpiCallback, SPI Mode to SPI Operation, Full or Transmit Only Mode to Transmit Only Mode and reduce Bitrate from 16 MHz to 2 MHz (2000000).
That is all. Now we can start code our double buffered driver.
Implementing Nokia 5510 Display Driver
Now add Display.c and Display.h files. In these files we will implement our display driver.
In header file we will define constants describing dimensions of display, compute buffer size and define pin numbers for pins which we need to drive manually. After definition of macros, we will declare functions for initialization display, swapping and transferring buffer to the display and some operations against working buffer like writing string. The content of this file is following:
#include "fsp_common_api.h" #ifndef DISPLAY_H_ #define DISPLAY_H_ #define DISPLAY_WIDTH 84 #define DISPLAY_LINES 6 #define DISPLAY_BUFFER_SIZE (DISPLAY_WIDTH * DISPLAY_LINES) #define DISPLAY_RST_PIN IOPORT_PORT_01_PIN_13 #define DISPLAY_DATA_CMD_PIN IOPORT_PORT_01_PIN_09 fsp_err_t DISPLAY_Init(); fsp_err_t DISPLAY_Clear(); fsp_err_t DISPLAY_Commit(); fsp_err_t DISPLAY_PrintColumn(uint8_t columnBitmap); fsp_err_t DISPLAY_PrintChar(char ch); fsp_err_t DISPLAY_PrintString(char* str); fsp_err_t DISPLAY_SetCursor(); #endif
In the C file include FSP libraries, SEGGER RTT library, our Display.h header file and string.h which we will need alter.
#include "hal_data.h" #include "SEGGER_RTT/SEGGER_RTT.h" #include "Display.h" #include <string.h>
We need some global variables. We need two buffers (which I implement using single array with double size), some information which buffer is designed to be transferred to the display and which is used for user operations. We need flag similar to flag used in previous section notifying that SPI transfer has completed (in previous section it was used for passing completion information from I2C callback). Lastly, we will need two variables for storing cursor location. It will not hold the same value as hardware pointers inside display, but rather they will be used as a pointers to local working buffer.
static uint8_t displayBuffers[2 * DISPLAY_BUFFER_SIZE]; static int userBuffer = 0; static int transmitBuffer = 1; static volatile int isTransferComplete = 0; static int cursorX = 0; static int cursorY = 0;
At first, we will not implement DISPLAY_Init function, but we will implement function for issuing commands to the display first. This function we will use in DISPLAY_Init function later. Create following static function:
static fsp_err_t DISPLAY_Command(int isDataCommand, uint8_t command) { }
The first parameter enables to specify type of transfer. It allows us distinguish pixel data transfer and command transfer. Efficiently this parameter defines value of D/C signal at the time of transfer. Second parameter is value to be transmitted to the display. Display commands are simple, so we do not need to transfer arrays or complicated structures. Every display command fits to single byte. Function propagates error code form FSP calls which we will implement later, so return type is fsp_err_t.
The first task which we need to do in this function is setting D/C signal correctly. This task we can do using simple R_IOPORT_PinWrite call. Of course, we will handle errors.
fsp_err_t status; status = R_IOPORT_PinWrite(&g_ioport_ctrl, DISPLAY_DATA_CMD_PIN, !!isDataCommand); if (status) { SEGGER_RTT_printf(0, "R_IOPORT_PinWrite failed.\r\n"); return status; }
The second task is transfer of the command byte to the display. We need to clear completion flag from previous run. Then we needto start transfer using R_SPI_Write function and wait until transfer completes. I will describe details about R_SPI_Write function later.
isTransferComplete = 0; status = R_SPI_Write(&g_spi0_ctrl, &command, sizeof(command), SPI_BIT_WIDTH_8_BITS); if (status) { SEGGER_RTT_printf(0, "R_SPI_Write failed.\r\n"); return status; } while (isTransferComplete == 0) { R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MICROSECONDS); }
In case when nothing fails, we can return success status code.
return FSP_SUCCESS;
Now we have all prerequirements needed before writing DISPLAY_Init function. Let’s create it.
fsp_err_t DISPLAY_Init() { }
This function does following:
- Init SPI Driver
- Setup default state of D/C signal
- Generate reset signal to reset display
- Clear both buffers
- Issue initialization commands to configure display.
- Set display cursors
- Transfer first (empty) display buffer to the display
The first task is SPI Driver initialization. This can be done using very similar call which we have seen when initializing I2c driver in previous section. SPI bus driver is initialized using following R_SPI_Open call:
fsp_err_t status; status = R_SPI_Open(&g_spi0_ctrl, &g_spi0_cfg); if (status) { SEGGER_RTT_printf(0, "R_SPI_Open failed.\r\n"); return status; }
Setting D/C state you have already seen in DISPLAY_Command function. Now set signal to 0 in Init function:
status = R_IOPORT_PinWrite(&g_ioport_ctrl, DISPLAY_DATA_CMD_PIN, 0); if (status) { SEGGER_RTT_printf(0, "R_IOPORT_PinWrite failed.\r\n"); return status; }
Resetting display is done using reset signal connected which is connected to the MCU. Pin number is defined in macro in header file. Following code asserts reset and after about 10 milliseconds code desserts reset signal again. Finally, we wait yet another 10 milliseconds to prevent issuing commands before display properly initialize after reset.
status = R_IOPORT_PinWrite(&g_ioport_ctrl, DISPLAY_RST_PIN, 0); if (status) { SEGGER_RTT_printf(0, "R_IOPORT_PinWrite failed.\r\n"); return status; } R_BSP_SoftwareDelay(10, BSP_DELAY_UNITS_MILLISECONDS); status = R_IOPORT_PinWrite(&g_ioport_ctrl, DISPLAY_RST_PIN, 1); if (status) { SEGGER_RTT_printf(0, "R_IOPORT_PinWrite failed.\r\n"); return status; } R_BSP_SoftwareDelay(10, BSP_DELAY_UNITS_MILLISECONDS);
Clearing both buffers we can simply resolve using memset call:
memset(displayBuffers, 0, sizeof(displayBuffers));
Now we need to issue some configuration commands to the display. In fact, I do not remember their meaning. I always copy them from my previous projects and they just work. The last two commands are used for setting line and column pointers to zero. You can find description of every command in datasheet if you are interested in it.
DISPLAY_Command(0, 0x00); DISPLAY_Command(0, 0x21); DISPLAY_Command(0, 0x10); DISPLAY_Command(0, 0xCF); DISPLAY_Command(0, 0x20); DISPLAY_Command(0, 0x0C); DISPLAY_Command(0, 0x80); // X = 0 DISPLAY_Command(0, 0x40); // Y = 0
At last, we send one buffer (which was previously cleared) to the display. We will use DISPLAY_Commit function which we will implement later. In case when no error occurs, we can return success status code.
DISPLAY_Commit(); return FSP_SUCCESS;
In previous function we referred FSP Driver callback DISPLAY_SpiCallback. This callback is simple. It just set completion flag which is checked inside main program. I also added basic error checking mechanism, but it is not very important in case of unidirectional SPI bus.
void DISPLAY_SpiCallback(spi_callback_args_t* args) { if (args->event != SPI_EVENT_TRANSFER_COMPLETE) { SEGGER_RTT_printf(0, "Transmission failed %02x\r\n", args->event); } isTransferComplete = 1; }
The next function which I will show is function for swapping buffers and starting transmission of committed buffer to the display. Function is named DISPLAY_Commit().
fsp_err_t DISPLAY_Commit() { fsp_err_t status; }
This function is designed to do following tasks:
- Swap buffers
- Reset local cursor pointer to location 0, 0
- Wait until DMA transfer fired by previous Commit call finish.
- Reset hardware cursor pointer to location 0, 0
- Set D/C signal to data
- Start new DMA transfer
Swapping buffers on MCU side is easy. It is just swapping index variables.
int swap = userBuffer; userBuffer = transmitBuffer; transmitBuffer = swap;
Resetting cursor on MCU side is also easy. Resetting cursor on display side can be done using DISPLAY_Command function:
cursorX = 0; cursorY = 0; DISPLAY_Command(0, 0x80); // X = 0 DISPLAY_Command(0, 0x40); // Y = 0
Setting GPIO port state using R_IOPORT_PinWrite you have seen multiple times above, so this time it is exactly the same.
status = R_IOPORT_PinWrite(&g_ioport_ctrl, DISPLAY_DATA_CMD_PIN, 1); if (status) { SEGGER_RTT_printf(0, "R_IOPORT_PinWrite failed.\r\n"); return status; }
And now we must start DMA transfer. DMA Transfer will transfer specified buffer to the SPI peripheral at background. Starting DMA Transfer is done very simple using R_SPI_Write. In fact, we already used DMA transfers in DISPLAY_Command function. In this function we have called R_SPI_Write function for transferring (one) byte to the display. This function started DMA transfer at the background, and we have waited to the completion of transfer in the while loop after the call. This time we will do it differently. We start DMA transfer using R_SPI_Write and then we will not wait until it completes. We will let DMA transfer running at background and we will continue running our code. We must handle state that second transfer is triggered while previous one is in progress. We will resolve that in a way that we will wait before (not after) R_SPI_Write call until previous DMA transfer completes. Note that we must also prevent calling DISPLAY_Command while transaction is in progress because DISPLAY_Command internally starts another transfer.
So, next step is wait to completion of previous transfer. You need add following snippet before first DISPLAY_Command(0, 0x80); which you have written as a second step of DISPLAY_Commit function.
while (isTransferComplete == 0) { R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MICROSECONDS); }
Then we can go back at the end and start new transfer. We need to reset complete flag from previous run, calculate address of correct buffer to transfer and then transfer it.
isTransferComplete = 0; uint8_t* buff = displayBuffers + transmitBuffer * DISPLAY_BUFFER_SIZE; status = R_SPI_Write(&g_spi0_ctrl, buff, DISPLAY_BUFFER_SIZE, SPI_BIT_WIDTH_8_BITS); if (status) { SEGGER_RTT_printf(0, "R_SPI_Write failed.\r\n"); return status; }
After issuing this command we can return success.
return FSP_SUCCESS;
This is all. You may think that you have not seen any DMA transfer yet. It is because DMA transfer (on RA2L1 platform is referred as a DTC instead of DMA) are hidden inside library. It works out of box. This feature is enabled in default and it is very useful. If you for some reason need SPI Driver to do not use DMA transfers at background, you can disable it in properties windows of SPI Driver stack.
Now we can implement functions related to drawing objects to the display (in fact they affect only internal buffer).
Function DISPLAY_SetCursor is simple because we have cursor represented as a just two variables.
fsp_err_t DISPLAY_SetCursor(int x, int line) { cursorX = x; cursorY = line; return FSP_SUCCESS; }
DISPLAY_Clear function is also pretty simple. It fills whole (active) buffer by zeros and reset cursor positions to 0,0.
fsp_err_t DISPLAY_Clear() { for (int x = 0; x < DISPLAY_WIDTH; x++) { for (int y = 0; y < DISPLAY_LINES; y++) { displayBuffers[userBuffer * DISPLAY_BUFFER_SIZE + y * DISPLAY_WIDTH + x] = 0; } } cursorX = 0; cursorY = 0; return FSP_SUCCESS; }
Another quite simple function is DISPLAY_PrintColumn. This function allows you to set bitmap of the one line at current cursor location. It will be used later for printing characters and bitmaps which is not supported by my font (I will introduce font later). Function increment pointers after setting column bitmap. We need to calculate properly index to the buffer. Calculation consists of three members. First member userBuffer * DISPLAY_BUFFER_SIZE defines If first or second buffer is used. Second member calculate correct index of start of line in the active buffer. The last member adds location of active column. Final index can be used for accessing right variable in byte array. After setting column bitmap inside buffer, function will increment pointers and wrap them when any of them overflow.
fsp_err_t DISPLAY_PrintColumn(uint8_t columnBitmap) { displayBuffers[userBuffer * DISPLAY_BUFFER_SIZE + cursorY * DISPLAY_WIDTH + cursorX] = columnBitmap; cursorX++; if (cursorX == DISPLAY_WIDTH) { cursorX = 0; cursorY++; if (cursorY == DISPLAY_LINES) { cursorY = 0; } } return FSP_SUCCESS; }
The next function which I will need for printing strings is DISPLAY_PrintChar. Function relies on font definition which you need to define as two global variables:
uint8_t fontDefinition[] = { 0x0e, 0x11, 0x11, 0x0e, 0x12, 0x1f, 0x10, 0x12, 0x19, 0x15, 0x12, 0x11, 0x15, 0x15, 0x0a, 0x0c, 0x0a, 0x09, 0x1f, 0x17, 0x15, 0x15, 0x0d, 0x0e, 0x15, 0x15, 0x08, 0x01, 0x01, 0x1d, 0x03, 0x0a, 0x15, 0x15, 0x0a, 0x02, 0x15, 0x15, 0x0e, 0x1e, 0x05, 0x05, 0x1e, 0x1f, 0x15, 0x15, 0x0a, 0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x0e, 0x1f, 0x15, 0x15, 0x1f, 0x05, 0x05, 0x0e, 0x11, 0x15, 0x1d, 0x1f, 0x04, 0x04, 0x1f, 0x1f, 0x08, 0x10, 0x10, 0x0f, 0x1f, 0x04, 0x0a, 0x11, 0x1f, 0x10, 0x10, 0x1f, 0x02, 0x04, 0x02, 0x1f, 0x1f, 0x02, 0x04, 0x1f, 0x0e, 0x11, 0x11, 0x0e, 0x1f, 0x05, 0x05, 0x02, 0x0e, 0x11, 0x09, 0x16, 0x1f, 0x05, 0x0d, 0x12, 0x12, 0x15, 0x15, 0x09, 0x01, 0x01, 0x1f, 0x01, 0x01, 0x0f, 0x10, 0x10, 0x0f, 0x03, 0x0c, 0x10, 0x0c, 0x03, 0x07, 0x18, 0x07, 0x18, 0x07, 0x1b, 0x04, 0x04, 0x1b, 0x17, 0x14, 0x14, 0x0f, 0x19, 0x15, 0x15, 0x13, }; uint8_t fondIndexTable[] = { 0x00, 0x04, 0x07, 0x0b, 0x0f, 0x13, 0x17, 0x1b, 0x1f, 0x23, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x2b, 0x2f, 0x33, 0x37, 0x3a, 0x3d, 0x41, 0x45, 0x46, 0x4a, 0x4e, 0x51, 0x56, 0x5a, 0x5e, 0x62, 0x66, 0x6a, 0x6e, 0x73, 0x77, 0x7c, 0x81, 0x85, 0x89, sizeof(fontDefinition)};
The first variable define font bitmap and second array defines offsets to the first array for each supported letter. Note that font do not support whole ASCII and we need to recalculate codes of letters in DISPLAY_PrintChar function. For example, take 4 bytes at offset 0x27 in font array, convert them to binary and highlight one in binary representation, you can see letter A. Letter is rotated because our binary number represents column bitmap instead of row bitmap.
In DISPLAY_PrintChar function we need to handle lot of cases. We need handle space and new-line manually. We will store number of bytes pasted to display in increment variable which we will later use for incrementing pointers. In case of processing new line, we just modify cursor pointers. In case of processing space, we set two columns to empty bitmap and correctly increment pointers.
int increment = 0;
if (ch == ' ') { displayBuffers[userBuffer * DISPLAY_BUFFER_SIZE + cursorY * DISPLAY_WIDTH + cursorX + 0] = 0; displayBuffers[userBuffer * DISPLAY_BUFFER_SIZE + cursorY * DISPLAY_WIDTH + cursorX + 1] = 0; increment = 2; } else if (ch == '\n'){ cursorX = 0; cursorY++; if (cursorY == DISPLAY_LINES) { cursorY = 0; } return FSP_SUCCESS; }
Font do not distinguish uppercase and lowercase letters, so we convert lowercase letters to uppercase before processing.
if (ch >= 'a' && ch <= 'z') { ch -= 32; }
In case when we have valid letter or digit now, we find them in the font table and paste it to the display buffer. Finally we add one pixel space between every letter.
if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z')) { ch -= 48; size_t startIndex = fondIndexTable[ch]; size_t endIndex = fondIndexTable[ch + 1]; for (size_t i = startIndex; i < endIndex; i++) { displayBuffers[userBuffer * DISPLAY_BUFFER_SIZE + cursorY * DISPLAY_WIDTH + cursorX + increment++] = fontDefinition[i]; } displayBuffers[userBuffer * DISPLAY_BUFFER_SIZE + cursorY * DISPLAY_WIDTH + cursorX + increment++] = 0; }
After that we need to increment pointers according to increment variable and properly handle overflows. After that we can return success status code.
cursorX += increment; if (cursorX >= DISPLAY_WIDTH) { cursorX -= DISPLAY_WIDTH; cursorY++; if (cursorY == DISPLAY_LINES) { cursorY = 0; } } return FSP_SUCCESS;
Function PrintString will call PrintChar until it reaches end of string
fsp_err_t DISPLAY_PrintString(char* str) { fsp_err_t status; while (*str) { status = DISPLAY_PrintChar(*str++); if (status) { return status; } } return FSP_SUCCESS; }
And this is all from display driver. We can draw bitmaps using DISPLAY_PrintColumn and we can print strings. Now we can implement our application hal_entry.c. I will start writing hal_entry.c from the scratch. In this file we need to include libraries and our drivers:
#include "hal_data.h" #include "SEGGER_RTT/SEGGER_RTT.h" #include <stdio.h> #include "TemperatureSensor.h" #include "Display.h"
We need to define bitmap of logo which I want to draw on display.
const unsigned char RoadTestLogo[] = { 0x80, 0xe0, 0xf0, 0xf8, 0xfc, 0x7e, 0x3e, 0x7f, 0xdd, 0x09, 0x09, 0x89, 0x8f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xf8, 0x98, 0x98, 0x98, 0xf8, 0xf8, 0x60, 0x00, 0xc0, 0xf0, 0x78, 0x18, 0x18, 0x18, 0x78, 0xf0, 0xe0, 0x00, 0x00, 0xe0, 0xf8, 0x38, 0xf8, 0xc0, 0x00, 0x00, 0xf8, 0xf8, 0x18, 0x18, 0x18, 0xf8, 0xf0, 0xc0, 0x18, 0x18, 0xf8, 0xf8, 0x18, 0x18, 0x00, 0xf8, 0xf8, 0x98, 0x98, 0x98, 0x98, 0x00, 0x70, 0xf8, 0xd8, 0xc8, 0x98, 0x98, 0x90, 0x00, 0x18, 0x18, 0xf8, 0xf8, 0x18, 0x18, 0x00, 0x01, 0x01, 0x03, 0x03, 0x01, 0x00, 0xf8, 0xfc, 0xfa, 0xe1, 0xc1, 0x01, 0x03, 0x0f, 0x1f, 0x7f, 0xfe, 0xfc, 0xf8, 0x00, 0x0f, 0x0f, 0x01, 0x01, 0x03, 0x8f, 0x8e, 0x88, 0x80, 0x81, 0x87, 0x8f, 0x8c, 0x8c, 0x8c, 0x8f, 0xc7, 0x83, 0x88, 0x0f, 0x0f, 0x03, 0x03, 0x03, 0x0f, 0x0e, 0x00, 0x0f, 0x0f, 0x0c, 0x0c, 0x0c, 0x0f, 0x07, 0x01, 0x00, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x0d, 0x0d, 0x0d, 0x0d, 0x00, 0x06, 0x0e, 0x0c, 0x08, 0x0d, 0x0f, 0x07, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x07, 0x0f, 0x0f, 0x0f, 0x1e, 0x18, 0x38, 0x28, 0x24, 0x27, 0x25, 0x24, 0x24, 0x24, 0x24, 0x22, 0x21, 0x20, 0x18, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x04, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
Later I will add function that print Renesas EK-RA2L1 string and print defined logo. I will use it later in main. Because font do not support dash, I need to draw dash manually.
void showHeader() { DISPLAY_SetCursor(7, 0); DISPLAY_PrintString("Renesas EK"); DISPLAY_PrintColumn(0b00000100); DISPLAY_PrintColumn(0b00000100); DISPLAY_PrintColumn(0b00000100); DISPLAY_PrintColumn(0b00000000); DISPLAY_PrintString("RA2L1\n"); for (size_t i = 0; i < sizeof(RoadTestLogo); i++) { DISPLAY_PrintColumn(RoadTestLogo[i]); } }
In hal_entry function I will write some debugging message to inform that debugging channel is working:
SEGGER_RTT_WriteString(0, "starting...\r\n");
Then I will initialize IOPORT FSP Driver and my drivers for temperature sensor and display library.
fsp_err_t status; status = R_IOPORT_Open(&g_ioport_ctrl, &g_bsp_pin_cfg); assert(status == FSP_SUCCESS); status = DISPLAY_Init(); assert(status == FSP_SUCCESS); status = TEMPERATURE_Init(); assert(status == FSP_SUCCESS);
After all initializations I will draw display header defined above and commit this screen to the display.
showHeader(); status = DISPLAY_Commit(); assert(status == FSP_SUCCESS);
In infinite loop I will create new blank screen, print “temp:” string. I need to print colon manually because font do not support it.
DISPLAY_Clear(); showHeader(); DISPLAY_PrintString("temp"); DISPLAY_PrintColumn(0b00001010); DISPLAY_PrintColumn(0b00000000); DISPLAY_PrintColumn(0b00000000);
Then I will read temperature from sensor and print error in case of error.
float temperature; status = TEMPERATURE_ReadTemperature(&temperature); if (status != FSP_SUCCESS) { DISPLAY_PrintString("error"); } else { // … }
In case when everything went well, I need to print temperature. SDK use minimalist standard library which do not support printing float, so I need to print it manually. Next caveat is that font do not support dot and dash for printing decimal numbers and negative numbers, so I need to draw them manually also. Printing float with 2 decimals I resolved in a way that I multiply value by 100, ten convert this new float to decimal and use value / 100 for receiving digits at left from decimal dot and value % 100 to receive digits at right from decimal dot. Printing temperature looks as follows.
int temperatureInt = (int)(temperature * 100); if (temperatureInt < 0) { DISPLAY_PrintColumn(0b00000100); DISPLAY_PrintColumn(0b00000100); DISPLAY_PrintColumn(0b00000100); DISPLAY_PrintColumn(0b00000000); temperatureInt = -temperatureInt; } char buffer[20]; snprintf(buffer, sizeof(buffer), "%d", temperatureInt / 100); DISPLAY_PrintString(buffer); // decimal dot DISPLAY_PrintColumn(0b00010000); DISPLAY_PrintColumn(0b00000000); snprintf(buffer, sizeof(buffer), "%d", temperatureInt % 100); DISPLAY_PrintString(buffer);
Finally, at the end of infinite loop start transmission of actualy createad buffer and swap active buffer by DISPLAY_Commit function.
status = DISPLAY_Commit(); assert(status == FSP_SUCCESS);
Results
You can press debug button, connect display and sensor to pins defined in configuration.xml and you should see something like this on display:
Verifying DMA Transfers
Last thing which I want to show is that DMA transfer really works. I am showing it because from the code it was not clear that any DMA is used. I looked what is happening on the buses using logic analyser and I have seen following data. I added some notes to clarify what is going on, but you can see that at some point there are two bus operations (SPI managed by DTC and I2C managed by user code) at the same time, so DMA transfer really work.
What’s next?
This is all from this tutorial. You have seen usage of two bus drivers and DMA working out of the box. While it is not explicitly shown, internally we also used some interrupts and some other parts of RA2L1 MCU. I have done this project as part of my RoadTest review, and I used them for gathering information about development board, MCU, SDK, development environment, and documentation. If you are interested in detail about board, you can find them in my Review of Renesas EK-RA2L1 Development Board blog post. If you are interested in detail about microcontroller and related things like e2studio IDE and documentation, you can read about them in my blog post Review of Renesas RA2L1 Microcontrollers. Summary and RoadTest score you can find in my main page of my RoadTest review.