Introduction to MCUBoot using the Renesas RA Family Part 4 | Renesas RA - 13
Published
Downloading the Firmware File into the MCU using Native Bootloader
Hi again! In this part we’ll mainly put MCUboot aside and will talk about the built-in bootloader which is present in all RA MCUs. This bootloader can operate via either UART or USB interface. In the last case the MCU is recognized by the PC as a virtual COM post, so from the PC perspective there is no difference between these two modes. The USB interface is only present in the MCU with the pin number over 32.
Let’s consider first how to put the MCU into the bootloader mode.
Entering the Bootloader Mode
Every RA MCU has a pin which is called “MD”. For example in the RA2A1 MCU which I have, this pin is merged with P201 (Fig. 1)
Depending on the state of this pin while resetting the device, the MCU can run either in the normal mode, which is called “Single-Chip Mode” in the User’s manual, if this pin is high, or in bootloader mode (“SCI / USB Boot Mode”), if this pin is low.
Normally, this pin is pulled up, so the MCU runs in normal mode. Which is: the program runs from the code area of the flash memory implementing the user’s code.
To run the bootloader you need to pull the MD pin low, then pull the RES pin (#25 in Fig. 1, next to the MD pin) low before setting it high again. After that, the MCU will start in boot mode. The bootloader code is located in the read-only memory, and can’t be changed by the user. In this mode the user’s code is not implemented until the next MCU reset (under the condition that MD pin is high).
If you have a development board, this process is simplified by the dedicated “Boot config” jumper and the “Reset” button. I will show you what I mean with the example of my EK-RA2A1 board (Fig. 2). Other boards have similar elements as well, so please refer to the User’s Manual of your specific board.
The jumper “Boot config” is marked as #1 in Fig. 2. As follows from the symbolic description of its operation drawn in the board itself, to run the MCU in the normal mode you need to set the jumper vertically, shorting the two left pins. And if you want to switch to the bootloader mode, you need to put it horizontally, shorting the two bottom pins. Let’s do this.
Now we need to connect the board to the PC. As you can see, it has two USB connectors: Debug USB (#3 in Fig. 2) and Device USB (#4 in Fig. 2). The first one is connected internally to the on-board J-Link OB debugger and is used to program and debug the MCU. The last one is connected directly to the USB_DP and USB_DM pins of the MCU (see Fig. 1). For some reason the Device USB connector doesn’t provide power to the board. So if you connect your board just with it, nothing will work. To provide the power, you need either to use some external power source, or to connect your board to the PC with both USB sockets at once (I used this method) using two separate USB cables and ports of the PC.
After you connect the board, the MCU will still be in normal mode. To switch to boot mode, you need to press and release the “Reset” button (#2 in Fig. 2). After that in the Device manager you will see the extra COM port (Fig. 3).
This means that the MCU has successfully switched to the boot mode. Memorize the number of the COM port, you will need it later.
If your board doesn’t have the device USB port (for example, FBP-RA4E1) or you use your custom board without a USB connection, you can use the SCI boot mode. In this mode everything is the same except for the connection of the MCU to PC. You need some external USB-to-UART converter (the most popular are based on CH340, CP2102, or FT232 chips) which should be connected to the Rx/Tx pins of the SCI9 module. Please be attentive here: the bootloader uses pin P110 as Rx, and pin P109 as Tx. For example in my EK-RA21 board there are two pairs of the different RX9/TX9 pins (Fig. 4).
The pins in the left part of Fig. 4 are connected to the P101 and P407 pins of the MCU, so they shouldn’t be used. We need pins P110 and P109 like in the right part of the Fig. 4, as I mentioned before.
Then you connect RX9 pin of the board to the TX pin of the USB-to-UART converter, and TX9 pin of the board to the RX pin of the converter, plug the converter into the USB port of the PC, open the device manager, find the new COM port (as in Fig. 3) and memorize its number.
Using Renesas Flash Programmer
Renesas provides software which allows downloading a program into the MCU in boot mode. It’s called “Renesas Flash Programmer” and can be downloaded from this page. At the moment of writing this tutorial the latest version is 3.10.00. Let’s download the installation file corresponding to your operating system and install the “Renesas Flash Programmer” software.
The installation process doesn’t have any peculiarities so I will not describe it here. Just set all the checkboxes and click the “Install” button.
When you run the application for the first time, you will see the following window (Fig. 5).
First thing we need to do is to select the “New Project…” from the “File” menu. Then, in the opened window make the changes shown in Fig. 6
First, make sure that you have selected the RA Microcontroller in the upper drop-down list.
Then give some name to your project. I’ve called it “Blinky”. Next, select the Project folder. I’ve left it without changes, but you can select whichever folder you want.
In the Communication part, you need to select the “COM port” in the Tool drop-down list and make sure that the COM port has the same number as you saw in the device manager (Fig. 3). In my case they both are COM3, so everything’s fine. After making all these changes, click the “Connect” button. If everything is OK, you will see some information about the MCU in the output field and notification: “Operation completed” (Fig. 7).
Now let’s click the “Browse…” button in the “Program File” field, and navigate to the very first project “Test” which we have created in Tutorial 2. In the project folder we need to open the “Debug” folder and select the “Test.srec” file (Fig. 8). This is the output file generated by the linker which the Renesas Flash Programmer can handle.
After selecting the firmware file you need to press the “Reset” button on your board to reinitialize the MCU and then press the “Start” button at the main window of the Renesas Flash Programmer. After that you will see that “Operation completed” and some information about what has been done (Fig. 9).
Now, to run the blinky application, you need to set the “Boot config” jumper into the “Internal Flash” position (Fig. 2) and press the “Reset” button. After that you will see LED1 blinking which means that we have done everything correctly.
The Renesas Flash Programmer application is good but unfortunately I didn’t find the options which are required to download the image files to run with the MCUboot.
First, it can accept only image files in the text format, like “.srec”, “.hex” etc. The “.bin.signed” file generated by MCUboot is a binary, and the Renesas Flash Programmer can’t open it. The second limitation is that you can’t select the address to which you want to write the image file, so it always is written at the address 0x0000 which also doesn’t suit us. Maybe in the next versions these options will be added but for now we have what we have, or better to say “don’t have”.
So, you can use this application to download the firmware file into the MCU without the bootloader, but a regular one, without MCUboot support.
To solve this issue I delved into the documentation of the built-in bootloader in order to learn how it works and to create my own application which will be free from these annoying limitations.
Detailed Description of the Built-in Bootloader
Renesas provides 3 (!!!) application notes which describe the application of the built-in bootloader: “System Specifications for Standard Boot Firmware for RA Family” applicable for the RA2A1, RA4M1, RA4W1, RA6M1, RA6M2, RA6M3, and RA6T1 groups, “System Specifications for Standard Boot Firmware Application note” applicable for RA2L1, RA2E1, and RA2E2 groups, and “Renesas RA Family Standard Boot Firmware for the RA family MCUs Based on Arm® Cortex®-M33” applicable for RA4M2, RA4M3, RA6M4, RA6M5, RA4E1, RA6E1, and RA6T2 groups.
The first two application notes are compatible and have the same commands description, while the last one is quite different and incompatible with the previous ones. As I’m using the RA2A1 MCU, I will stick to the first application note, so my software is only compatible with the RA2A1, RA4M1, RA4W1, RA6M1, RA6M2, RA6M3, RA6T1, RA2L1, RA2E1, and RA2E2 groups. If you want, you can expand it to other groups as well, as I will provide the full source code of my software at the end of this tutorial.
The mentioned application notes are surprisingly good, so you don’t need to search the information in different sources to find the required information, you just open them and get all you need.
I will not quote the whole application note here because you can easily read it from the Renesas site. I will just highlight some moments where you need to pay attention.
The first thing that I missed initially and then couldn’t understand why nothing worked, was the beginning communication sequence which is present in Fig. 14 and Fig. 15 of the application note. Let me reproduce the latter figure here (Fig. 10).
The first action after releasing the reset pin is USB enumeration (if you use the USB boot). This action is implemented automatically without any participation of us.
The next phase is Communication setting. In this phase we first need to send the 0x00 bytes to the MCU until it responds with 0x00 as well. In practice, you can send the packet of ten 0x00 bytes, after which the MCU will send the ACK (0x00) byte back. Then you need to send the Generic code byte 0x55, at which MCU will respond with the Boot code byte. For MCUs based on Cortex-M23 and Cortex-M4 this code is 0xC3, and for MCUs based on Cortex-M33 core this code is 0xC6. So at this moment our application already can distinguish the MCU type and select one or another programming sequence. As I said, in my program I only deal with the first two cores.
After receiving the Boot code, the Communication setting phase is considered successful and the Command acceptance phase starts. Here we first send the Inquiry command to the MCU. If the last sends back the OK response, this means that no authentication ID is set for the current MCU, and thus we can proceed to sending the other commands. If the MCU sends the Flow Error response (Fig. 11), this means that the authentication ID has been set, and now we need to enter this ID using the ID Authentication command. If the sent ID matches the one saved in the MCU memory, this phase is considered as successful, and we can now send any command to the MCU.
In my program the last functionality is also not done, so if you need secure booting support, please add the ID authentication command to the software code by yourself.
The next command flow is presented in the Fig. 29, 30 of the application note (see Fig. 12, 13 below).
First, we send the Signature request command, as a result of which we receive the following information about the MCU:
- SCI operating clock frequency;
- Recommended maximum UART baud rate of the device;
- Number of recordable areas (NOA) which is the number of the flash areas to which we can write some data. Usually there are at least 3 of them: Code flash, Data flash, and Configuration area;
- Type code, which is 0x02 for RA2/RA4 series, and 0x03 for RA6 series (regardless the core type);
- Boot firmware version, just for the reference.
After this command we get the specific information about each of the available memory areas by sending the Area information request command (see Fig. 12). As a response to it we receive the following information:
- Kind of area:
- 0x00 - Code flash;
- 0x01 - Data flash;
- 0x02 - Configuration area;
- Start address;
- End address;
- Erase access unit (the minimal number of bytes that can be erased at once in this area);
- Write access unit (the minimal number of bytes that can be written at once in this area).
The information received by this command is very important to correctly implement the erasing and writing to flash memory.
Now we can switch to the flow in Fig. 13. To write to the memory we first need to send the Erase command in which we mention the start and end address of the memory to be erased. These addresses should be within the range corresponding to the data received by the Area information request command. Also, the number of bytes to erase should be multiple by the erase access unit. If everything is fine, we can send the Write command, in which we also mention the start and end addresses to write to, and they should be multiple by the write access unit.
After finishing the writing, you can send the Read command to read the content of the memory in order to verify the validity of the operation.
Now, as we have considered the operation flow, let’s consider the format of commands which is described in point 3.3.1 of the application note (Fig. 14).
Actually there is nothing to add to it, everything is quite clear. You should make sure that the first byte is 0x01, the last byte is 0x03, the LNH and LNL fields correspond to the real packet length, and the SUM byte is correct to the current packet. As stated in Fig. 14, calculation of the sum is quite simple:
SUM = - (LNH + LNL + COMMAND + Command information[0] + Command information[1] + … + Command information[LNH * 256 + LNL - 2]
If you have all these bytes in the unsigned char format, the SUM value will be calculated correctly without any additional conversions and calculations. I’ll show you examples later.
The commands list is presented in Table 11 of the application note (Fig. 15).
As you see, there are just eight commands, and we have considered almost all of them before except for the Baud rate setting command. This is an optional command which allows to speed up the communication process. Initially in the SCI boot mode the default speed is just 9600 baud, so it’s recommended to increase somewhere in the beginning of the Command acceptance phase.
As the response at the Command packet, MCU sends the response with either OK or Error information. The list of possible statuses is presented in Table 12 of the application note (Fig. 16).
As you see, the list of possible statuses is much longer than the list of the commands. And also for each command the same error can be caused by different reasons, which fortunately are described in detail.
Let’s now consider in more detail some commands and possible responses of the MCU to it. Let it be the Baud rate setting command which I mentioned recently. It’s described in the p. 3.4.10 of the application note.
The packet sent from the PC to the MCU is the following:
As you see the length of the command is 5 bytes (LNH * 256 + LNL). Please note that the length is just the sum of the COM and BRT fields (see Fig. 14), and the total packet length is (LNH * 256 + LNL + 5) bytes. The BRT field is 4 bytes and represents the baud rate to set. For example if you want to set the 115200 baud speed, this field should be 0x00, 0x01, 0xC2, 0x00, because 115200 = 0x01C200.
Let’s now calculate the SUM byte:
SUM = - (LNH + LNL + COM + BRT[0] + BRT[1] + BRT[2] + BRT[3]) = - (0x00 + 0x05 + 0x34 + 0x00 + 0xC2 + 0x01 + 0x00) = - 0xFC = 0x04
So the whole packet will be 0x01 0x00 0x05 0x34 0x00 0x01 0xC2 0x00 0x04 0x03.
The MCU can respond in two ways: OK packet (Fig. 18) or Error packet (Fig. 19).
The other commands are described in a similar way so, as I said before, you don’t need anything else except for this application note to write your software.
Now, as we’re familiar with the commands format and flow, let’s consider the software that I have written.
Software Description
The software is written in the popular-in-certain-circles-of-programmers environment called Qt. This is the cross-platform environment which allows writing a program in C++ and Python languages. Unlike pure C++ it has a convenient approach called “signal-slot” which allows linking some user function (slot) with the function that is called automatically when an event happens (signal).
I’m quite new in Qt, so please don’t judge me too harshly if you have more experience in it. Actually, this is my first application written in this environment.
I decided to make this software without a GUI, just as a command line application with parameters.
I will not consider the whole code here, just some parts of it to illustrate the approach. Also I will not describe the C++ basics considering that the reader is already familiar with them.
As I mentioned before, the application is written in Qt, the current version of which is 6.2.3. It consists of three source files: “main.cpp” which is the main file of the program, “bootloader.cpp” and “bootloader.h” which consist of code of the bootloader class which implements the whole work about communication with the MCU in the boot mode.
Let’s consider the content of the “main.cpp” file first.
#include <QCoreApplication>
#include <QTextStream>
#include <bootloader.h>
#include <QCommandLineParser>
QString port_name; //The name of the com port
QString file_name; //The firmware file name
int32_t address; //The address where to write the firmware
bootloader bootloader; //Object of the bootloader class
void resultChanged (OperationResults m_operationResult)
{
QTextStream standardOutput(stdout);
switch (m_operationResult)
{
case OperationResults::RES_OK: /*standardOutput << "Operation successful\n";*/ break;
case OperationResults::RES_IN_PROGRESS: /* standardOutput << "Operation is still in progress\n"; */break;
case OperationResults::RES_TIMEOUT: standardOutput << "Response timeout\n"; break;
case OperationResults::RES_SERIAL_PORT_ERROR: standardOutput << "Serial port communication error\n"; break;
case OperationResults::RES_WRONG_RESPONSE: standardOutput << "Wrong format of the device response packet\n"; break;
case OperationResults::RES_COM_ERROR: standardOutput << "Communication setting error\n"; break;
case OperationResults::RES_UNSUPPORTED_COMMAND: standardOutput << "Unsupported command\n"; break;
case OperationResults::RES_PACKET_ERROR: standardOutput << "Wrong packet format\n"; break;
case OperationResults::RES_CHECKSUM_ERROR: standardOutput << "Wrong checksum\n"; break;
case OperationResults::RES_FLOW_ERROR: standardOutput << "Flow error\n"; break;
case OperationResults::RES_ADDRESS_ERROR: standardOutput << "Wrong memory address\n"; break;
case OperationResults::RES_BAUD_RATE_MARGIN_ERROR: standardOutput << "Baud rate margin error\n"; break;
case OperationResults::RES_PROTECTION_ERROR: standardOutput << "Memory protection error\n"; break;
// case OperationResults::RES_ID_MISMATCH_ERROR: standardOutput << "Authentication ID mismatch\n"; break;
case OperationResults::RES_PROGRAMMING_DISABLE_ERROR: standardOutput << "Programming disable error\n"; break;
case OperationResults::RES_ERASE_ERROR: standardOutput << "Flash memory erase error\n"; break;
case OperationResults::RES_WRITE_ERROR: standardOutput << "Flash memory write error\n"; break;
case OperationResults::RES_SEQUENCER_ERROR: standardOutput << "Sequencer error\n"; break;
case OperationResults::RES_NO_FILE: standardOutput << "Specified firmware file doesn't exist\n"; break;
case OperationResults::RES_VERIFICATION_ERROR: standardOutput << "Verification error\n"; break;
}
standardOutput.flush();
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
bool ok; //The result of the operation
port_name = "";
file_name = "";
address = -1;
QCoreApplication::setApplicationName("RA-bootloader");
QCoreApplication::setApplicationVersion("1.0");
QCommandLineParser parser;
parser.setApplicationDescription("RA family native UART/USB bootloader");
parser.addHelpOption();
parser.addVersionOption();
// An option with the COM port name
QCommandLineOption com_port_name_option(QStringList() << "p" << "port",
QCoreApplication::translate("main", "The name of the COM port by which the RA MCU is recognized by the system."),
QCoreApplication::translate("main", "COMn or /dev/ttySn"));
parser.addOption(com_port_name_option);
// An option with the flash address
QCommandLineOption address_option(QStringList() << "a" << "address",
QCoreApplication::translate("main", "The address of the flash memory where to write the firmware file."),
QCoreApplication::translate("main", "0x2800"));
parser.addOption(address_option);
// An option with the file name
QCommandLineOption file_name_option(QStringList() << "f" << "file",
QCoreApplication::translate("main", "The name of the firmware file (with the whole path if necessary)."),
QCoreApplication::translate("main", "firmware.bin.signed"));
parser.addOption(file_name_option);
// Process the actual command line arguments given by the user
parser.process(app);
if (parser.isSet(com_port_name_option))
port_name = parser.value(com_port_name_option);
if (parser.isSet(address_option))
address = parser.value(address_option).toInt(&ok, 16);
if (parser.isSet(file_name_option))
file_name = parser.value(file_name_option);
if ((port_name == "") || (address == -1) || (file_name == ""))
{
QTextStream(stdout) << "Some parameters are missing. Please run the application with the -h option to get the help";
return -1;
}
else
{
QTextStream(stdout) << "File name: " << file_name << "\r\nCOM port name: " << port_name << "\r\nMemory address:" << QString("0x%1").arg(address, 4, 16, QLatin1Char('0')) << "\r\n";
}
if (bootloader.open_com_port(port_name)) //If COM port was opened successfully
{
bootloader.set_file_name(file_name); //Set the file name
bootloader.set_address(address); //And set the flash memory address
QObject::connect(&bootloader, &bootloader::operation_result_changed, resultChanged); //Connect the operation_result_chamged signal to the resultChanged function
bootloader.write_firmware(); //Write the firmware file
}
return app.exec();
}
In lines 1-4 we include the required modules. In lines 6-9 we declare the variables:
- port_name is the name of the COM port by which the MCU is connected to the PC.
- file_name if the name of the firmware binary file. Warning! This application works only with the binary files, so it will not download the “.hex” or “.srec” files correctly.
- address is the start flash memory address where the firmware should be written.
- bootloader is the object of the bootloader class.
In lines 11-38 there is the function resultChanged which is the slot connected to the operation_result_change signal of the bootloader class. This function is invoked automatically every time when the operation result of the communication with the MCU changes. In this function we just send to the standard output the description of all errors that may happen during operation. These errors include both bootloader-related ones (see Fig. 16) and application-related ones, like absence of file or communication timeout.
The main function of the program is located in lines 40-107. First we initialize the variables (lines 46-48) with the default values. In lines 50, 51 we set the name and the version of the application required by the “help” parameter of the command line parser parser, declared in line 53.
In lines 59-62 we add a new parameter to the parser which is “p” or “port” and followed by the port name.
In lines 65-68 we add the “a” or “address” parameter followed by the memory address in hexadecimal format.
In lines 71-74 we add the “f” or “file” parameter followed by the firmware file name to be downloaded.
Example of how this custom bootloader works
Let me explain now how this all works. The command line parameters are called after the name of the application in the command line. An example of using the application is:
bootloader.exe -p COM1 -a 0x2800 -f “D:\project\firmware.bin.signed”
or equally:
bootloader.exe --port COM1 --address 0x2800 --file “D:\project\firmware.bin.signed”
You also can run the application with the parameter “-h” or “--help” to get the full information about the application (Fig. 20).
In line 77 we run the parser to parse the command line parameters. Then we check if some parameter is present, and then set the corresponding variable (lines 79-86). In line 88 we check if any of the required parameters was not set. In this case we display the corresponding message (line 90) and close the application (line 91). Otherwise we display the parameters (line 95).
In line 98 we call the open_com_port method of the bootloader class. If the port was opened successfully, we pass the file_name and address values to the bootloader class (lines 100, 101).
In line 102 we link the signal operation_result_changed of the bootloader class with the resultChanged function.
In line 103 we call the write_firmware method which starts the sequence of the commands to write and verify the firmware according to Fig. 10-13.
In line 106 we execute the core application app.
Let’s now briefly consider the implementation of the bootloader class. The content of the “bootloader.h” file is the following:
#ifndef BOOTLOADER_H
#define BOOTLOADER_H
#include <QByteArray>
#include <QtSerialPort/QSerialPort>
#include <QTextStream>
#include <QTimer>
#include <QDir>
#include <QFile>
#include <QObject>
//Commands that can be issued
enum class Commands
{
COM_NONE,
COM_LOW_PULSE,
COM_GEN_CODE,
COM_INQUIRY,
COM_ERASE,
COM_WRITE,
COM_READ,
// COM_ID_AUTH,
COM_BAUD_RATE,
COM_SIGN_REQ,
COM_AREA_INFO_REQ
};
//Result of the operation
enum class OperationResults
{
RES_OK,
RES_IN_PROGRESS,
RES_TIMEOUT,
RES_SERIAL_PORT_ERROR,
RES_COM_ERROR,
RES_WRONG_RESPONSE,
RES_UNSUPPORTED_COMMAND,
RES_PACKET_ERROR,
RES_CHECKSUM_ERROR,
RES_FLOW_ERROR,
RES_ADDRESS_ERROR,
RES_BAUD_RATE_MARGIN_ERROR,
RES_PROTECTION_ERROR,
// RES_ID_MISMATCH_ERROR,
RES_PROGRAMMING_DISABLE_ERROR,
RES_ERASE_ERROR,
RES_WRITE_ERROR,
RES_SEQUENCER_ERROR,
RES_NO_FILE,
RES_VERIFICATION_ERROR
};
class bootloader : public QObject
{
Q_OBJECT
public:
explicit bootloader(QObject *parent = nullptr);
~bootloader();
bool open_com_port(QString port_name); //Set the name of the COM port and open it
void set_address (int32_t address) {m_address = address;} //Set the flash memory address
void set_file_name (QString file_name) {m_file_name = file_name;} //Set the firmware file address
void write_firmware (void) {send_low_pulse();} //The function that implements the whole sequence to write the firmware to the correct address
signals:
void operation_result_changed(OperationResults);//Is invoked when the operation_result is changed
private slots:
void read_data(); //Reading and processing the data from COM port
void handle_timeout(); //Handle the timeout of the COM port
void handle_error(QSerialPort::SerialPortError error);//Handle the COM port errors
private:
bool send_low_pulse (void); //Send 0x00 ten times to initiate the communication
bool send_generic_code (void); //Send generic code to finalize communication setting phase
bool send_inquiry_command (void); //Send the Inquiry command
bool send_signature_request_command (void); //Send the Signature request command
bool send_area_request_command (uint8_t noa); //Send the Area information request command
bool send_baud_rate_command (uint32_t br); //Send the Baud rate settings command
bool send_erase_command (uint32_t start_addr, uint32_t length);//Send the Erase command
bool send_write_command (uint32_t start_addr, uint32_t length);//Send the Write command
bool send_data (char *data, uint16_t length); //Send the data to the MCU flash
bool send_read_command (uint32_t start_addr, uint32_t length);//Send the Read command
bool send_ok (void); //Send OK to the MCU after reading the data
QSerialPort m_serial_port; //COM port pointer
QByteArray m_buf_rx, m_buf_tx; //COM port read and write buffers
QTextStream m_standard_output; //Standard output text stream
QTimer m_timer; //Timer for COM port receive timeout recognition
char s[1100]; //Array to send the string to COM port
Commands m_command; //The name of the current command
OperationResults m_operation_result;//Result of the implementation of the current command
QString m_file_name; //Full name of the firmware file
QFile m_file; //Firmware file pointer
int32_t m_address; //Address of the flash memory
uint32_t m_sci; //SCI operating clock frequency
uint32_t m_rmb; //Recommended maximum baudrate
uint8_t m_noa; //Number of recordable areas
uint8_t m_cur_area; //Current area number
uint8_t m_typ; //Type code
uint8_t m_bfv_h, m_bfv_l; //Boot firmware version
struct m_area_infos //Information about the memory area
{
uint8_t koa; //Kind of area
uint32_t sad; //Start address of the area
uint32_t ead; //End address of the area
uint32_t eau; //Erase access unit
uint32_t wau; //Write access uint
} m_area_info [10];
uint32_t m_file_size; //Size of the firmware file
int open_serial_port(); //Opening of the COM port
void close_serial_port(); //Closing of the COM port
void write_data(const QByteArray &data); //Writing to the COM port
bool command_common_part (Commands com);//Common part of all commands
};
#endif // BOOTLOADER_H
In lines 5-12 we include all required libraries.
In lines 15-28 we declare the enum class Commands in which we enumerate all the commands according to Fig. 15. Please note that the command COM_ID_AUTH (which stands for the ID Authentication command) is commented out (line 24) because, as I mentioned before, I didn’t add the secure booting support for now.
In lines 31-53 we declare the enum class OperationResults in which all possible results of operation are enumerated. The ID_MISMATCH_ERROR is also commented out (line 46).
In lines 55-118 there is the description of the class bootloader.
In lines 62-66 there is the declaration of the public functions which we have already met in the “main.c” file.
In line 68 there is the declaration of the operation_result_changed function, which is declared as a signal (line 67). We also have met this function in the “main.c” file.
In lines 71-73 we declare the private slots of the class:
- read_data which is called when the data is received from the COM port;
- handle_timeout which is called when there is no response from the MCU within the specified time;
- handle_error which is called when some internal COM-port error happens.
In lines 76-86 there are private methods which are used to send the corresponding command to the MCU.
In lines 88-112 there is the declaration of the private variables. You can see their meaning in the comments.
In lines 114-117 there are low-level private methods to operate with the COM-port.
The “bootloader.cpp” file which consists of the class implementation is about 600 lines long, so I will present here just some parts of it.
bootloader::bootloader(QObject *parent)
: QObject{parent},
m_standard_output(stdout)
{
//Connecting slots to the corresponding signals
connect(&m_serial_port, &QSerialPort::readyRead, this, &bootloader::read_data);
connect(&m_serial_port, &QSerialPort::errorOccurred, this, &bootloader::handle_error);
connect(&m_timer, &QTimer::timeout, this, &bootloader::handle_timeout);
m_timer.setSingleShot(true); //COM port timeout timer mode is "single shot"
}
//-----------------------------------------------------------------------------------------
bool bootloader::send_low_pulse (void)
{
m_buf_tx.resize(10);
for (uint8_t i = 0; i < 10; i ++)
m_buf_tx[i] = 0x00;
return command_common_part (Commands::COM_LOW_PULSE); //Call the Command_common_part method to transmit the command to the device
}
//-----------------------------------------------------------------------------------------
bool bootloader::send_write_command (uint32_t start_addr, uint32_t length)
{
uint8_t check_digit = 0;
uint32_t end_addr = start_addr + length - 1;
m_buf_tx.resize(14);
m_buf_tx[0] = 0x01;
m_buf_tx[1] = 0x00;
m_buf_tx[2] = 0x09;
m_buf_tx[3] = 0x13;
m_buf_tx[4] = start_addr >> 24;
m_buf_tx[5] = start_addr >> 16;
m_buf_tx[6] = start_addr >> 8;
m_buf_tx[7] = start_addr & 0xFF;
m_buf_tx[8] = end_addr >> 24;
m_buf_tx[9] = end_addr >> 16;
m_buf_tx[10] = end_addr >> 8;
m_buf_tx[11] = end_addr & 0xFF;
for (uint8_t i = 1; i <= 11; i ++)
check_digit += (uint8_t)m_buf_tx[i];
m_buf_tx[12] = -check_digit;
m_buf_tx[13] = 0x03;
return command_common_part (Commands::COM_WRITE); //Call the Command_common_part method to transmit the command to the device
}
//-----------------------------------------------------------------------------------------
bool bootloader::send_data (char *data, uint16_t length)
{
uint8_t check_digit = 0;
m_buf_tx.resize(length + 6);
m_buf_tx[0] = 0x81;
m_buf_tx[1] = (length + 1) >> 8;
m_buf_tx[2] = (length + 1) & 0xFF;
m_buf_tx[3] = 0x13;
for (uint16_t i = 0; i < length; i ++)
{
m_buf_tx[i + 4] = data[i];
check_digit += (uint8_t)m_buf_tx[i + 4];
}
for (uint8_t i = 1; i <= 3; i ++)
check_digit += (uint8_t)m_buf_tx[i];
m_buf_tx[length + 4] = -check_digit;
m_buf_tx[length + 5] = 0x03;
return command_common_part (Commands::COM_WRITE); //Call the Command_common_part method to transmit the command to the device
}
void bootloader::read_data()
{
uint32_t file_size;
char file_data[1024];
uint16_t bytes_read, bytes_read_aligned;
m_buf_rx.append(m_serial_port.readAll()); //Read data from the COM port
if (m_command == Commands::COM_LOW_PULSE)
{
if (m_buf_rx[0] == 0x00)
{
m_operation_result = OperationResults::RES_OK;
emit operation_result_changed(m_operation_result);
m_buf_rx.clear();
send_generic_code();
}
else
{
m_operation_result = OperationResults::RES_COM_ERROR;
emit operation_result_changed(m_operation_result);
}
}
else if (m_command == Commands::COM_GEN_CODE)
{
if ((uint8_t)m_buf_rx[0] == 0xC3)
{
m_operation_result = OperationResults::RES_OK;
emit operation_result_changed(m_operation_result);
m_buf_rx.clear();
send_inquiry_command();
}
else
{
m_operation_result = OperationResults::RES_COM_ERROR;
emit operation_result_changed(m_operation_result);
}
}
else //All other commands
{
int len = m_buf_rx.size(); //The length of the received data
if (len >= 3) //If the last character is 0x03 (End of packet) and the message length is greater or equal than 7
{
int packet_length = m_buf_rx[1] * 256 + m_buf_rx[2];//Read the claimed packet length
if (len == (packet_length + 5)) //If the packet length corresponds to the claimed one
{
if ((m_buf_rx[len - 1] == 0x03) && ((uint8_t)m_buf_rx[0] == 0x81))//And if the last character ix 0x03 (end of packet) and the first character is 0x81 (SOD)
{
m_timer.stop(); //Then stop the timer
#ifdef DEBUG
m_standard_output << "Data received successfully: ";
for (int i = 0; i < len; i ++)
{
m_standard_output << QString("0x%1").arg((uint8_t)m_buf_rx[i], 2, 16, QLatin1Char('0')) << " ";
}
m_standard_output << "\r\n";
m_standard_output.flush();
#endif
if ((m_buf_rx[3] & 0x80) == 0x80) //The third byte is RES. It its 7'th bit is 1 then there is some error
{
switch ((uint8_t)m_buf_rx[4])
{
case 0xC0: m_operation_result = OperationResults::RES_UNSUPPORTED_COMMAND; break;
case 0xC1: m_operation_result = OperationResults::RES_PACKET_ERROR; break;
case 0xC2: m_operation_result = OperationResults::RES_CHECKSUM_ERROR; break;
case 0xC3: m_operation_result = OperationResults::RES_FLOW_ERROR; break;
case 0xD0: m_operation_result = OperationResults::RES_ADDRESS_ERROR; break;
case 0xD4: m_operation_result = OperationResults::RES_BAUD_RATE_MARGIN_ERROR; break;
case 0xDA: m_operation_result = OperationResults::RES_PROTECTION_ERROR; break;
// case 0xDB: m_operation_result = OperationResults::RES_ID_MISMATCH_ERROR; break;
case 0xDC: m_operation_result = OperationResults::RES_PROGRAMMING_DISABLE_ERROR; break;
case 0xE1: m_operation_result = OperationResults::RES_ERASE_ERROR; break;
case 0xE2: m_operation_result = OperationResults::RES_WRITE_ERROR; break;
case 0xE7: m_operation_result = OperationResults::RES_SEQUENCER_ERROR; break;
}
emit operation_result_changed(m_operation_result);
m_buf_rx.clear();
m_command = Commands::COM_NONE;
QCoreApplication::exit(-1);
}
uint8_t sum = 0;
for (uint16_t i = 1; i < len - 1; i ++) //Calculating the response packet checksum
sum += (uint8_t)m_buf_rx[i];
if (sum != 0) //If checksum is not 0 then emit error "wrong response"
{
m_operation_result = OperationResults::RES_WRONG_RESPONSE;
emit operation_result_changed(m_operation_result);
m_buf_rx.clear();
m_command = Commands::COM_NONE;
QCoreApplication::exit(-1);
}
m_operation_result = OperationResults::RES_OK;
emit operation_result_changed(m_operation_result);
switch (m_command) //Check the commands
{
case Commands::COM_NONE: break; //No commands
case Commands::COM_LOW_PULSE: break; //Low pulses in the communication establishment phase
case Commands::COM_GEN_CODE: break; //Generic code in the communication establishment phase
case Commands::COM_INQUIRY: //Inquiry command
m_buf_rx.clear();
send_signature_request_command();
break;
……………………………
case Commands::COM_WRITE: //Write flash memory command
if (!m_file.atEnd())
{
bytes_read = m_file.read(file_data, 1024);
bytes_read_aligned = bytes_read;
for (uint8_t i = 0; i < m_noa; i ++) //Checking all memory areas to find the code flash area
{
if (m_area_info[i].koa == 0x00) //If the area is the code flash
{
bytes_read_aligned /= m_area_info[i].wau; //Divide the bytes_read_aligned by the write access unit
bytes_read_aligned *= m_area_info[i].wau; //Now the bytes_read_aligned is multiple by the write access uint
if (bytes_read_aligned < bytes_read) //If now the bytes_read_aligned is less than the real file size
bytes_read_aligned += m_area_info[i].wau; //Then add the write access unit
break;
}
}
send_data(file_data, bytes_read_aligned);
m_buf_rx.clear();
}
else
{
m_file.close();
m_buf_rx.clear();
m_file.open(QIODevice::ReadOnly);
m_standard_output << "Verifying data...\r\n";
m_standard_output.flush();
send_read_command(m_address, file_size);
} break;
………………………………
//-----------------------------------------------------------------------------------------
bool bootloader::command_common_part (Commands com)
{
if (m_operation_result != OperationResults::RES_IN_PROGRESS) //If there is no other operation in progress
{
m_command = com; //Set the corresponding command value
m_operation_result = OperationResults::RES_IN_PROGRESS; //Set the current operation result as "in progress"
write_data (m_buf_tx); //Send data to the COM port
m_timer.start(5000); //Start timer with the 5 seconds timeout
emit operation_result_changed(m_operation_result); //Emit the function to notify the main program about changing the result of the operation
#ifdef DEBUG
m_standard_output << "Data length is " << m_buf_tx.length() << ": "; //Write the command length to console
for (int i = 0; i < m_buf_tx.length(); i ++)
{
m_standard_output << QString("0x%1").arg((uint8_t)m_buf_tx[i], 2, 16, QLatin1Char('0')) << " ";
}
m_standard_output << "\r\n";
m_standard_output.flush(); //Flush the output stream
#endif
return true; //Return true if data has been sent
} return false; //Return false if there was another command in progress and data has not been sent
}
Even though there is only half of the file, it’s still very long.
In lines 6-16 there is the constructor of the class, in which we connect the class slots to the corresponding signals (lines 11-13) and configure timer m_timer which is used to handle the response timeout to run in the single shot mode (line 15).
In lines 72-78 there is the implementation of the send_low_pulse method. I showed it here as an example of how the command to be sent to the MCU is formed. The command is located in the m_buf_tx variable of the QByteArray type. First, we set the size of the array (line 74) then fill it with zeros (lines 75-76) and finally invoke the method command_common_part with the parameter corresponding to the current command, which is COM_LOW_PULSE in the current case (line 77). The send_low_pulse and send_generic_code are not actually real commands, and they are processed in a different way than others (we will consider this soon).
Now let’s consider an example of forming the regular command in the method send_write_command (lines 174-196). This command accepts two parameters: start address and end address. And the method also has two parameters: start_address which corresponds to the command parameter, and length which is the data length to write. The end_address is calculated in a simple way (line 177):
end_address = start_address + length - 1;
Please pay attention that both start and end addresses should be multiple of the write access unit about which I talked before.
In lines 179-190 we form the command according to p.3.4.7 of the application note. In line 190-193 we calculate the checksum in the way I explained earlier. Finally, we invoke the command_common_part method, but this time with the parameter COM_WRITE. This parameter is used to distinguish in the read_data method, from which command we got the response.
The next method send_data (lines 199-217) is followed by the previous command and sends the data to be written to the flash memory of the MCU. Its format is also described in the same p.3.4.7 of the application note.
Let’s now switch to the end of the file and consider the method command_common_part (lines 566-577). First, we check if the other command is not being implemented (line 558). If it is then we return false and leave the function (line 576). Otherwise we process the command. First, we assign the m_command variable with the parameter transmitted to this method (line 560). Then we set the m_operation_result as RES_IN_PROGRESS to indicate that the command is being implemented now (line 561). Then we send the m_buf_tx content to the COM-port (line 562) and start the timer with the 5 seconds timeout (line 563). If within this time there is no response from the device, the handle_timeout slot is called automatically. In line 564 we emit the signal operation_result_changed to notify all connected slots about this event. Lines 565-573 are enabled only if the macro DEBUG is defined. They allow writing to the console the data that is transmitted. Finally, we return true to indicate that the data has been sent successfully and leave this function (line 574).
Now let’s consider the biggest and one of the most important methods of this program called read_data (lines 259-528). Here I presented just part of it because the rest is quite similar.
In lines 261-263 we declare the required local variables.
In line 264 we read all the data received from the COM-port and append it to the m_buf_rx array. Then we process the response at the low pulses (COM_LOW_PULSE) sequence (lines 265-269), then the response at the generic code (COM_GEN_CODE) (lines 280-294).
These two are non-standard responses unlike the ones to the regular commands, and quite similar. First we check if we receive the correct byte (lines 267, 282). If it is so, we set the m_operation_result as RES_OK (lines 269, 284) and emit the operation_result_changed signal (lines 270, 285). Then we clear the m_rx_buffer (lines 271, 286) and send the next command. In the first case it’s send_generic_code (line 272), and in the second case it’s send_inquiry_command (line 287). In this way we implement something like the finite state machine by sending the commands in the required sequence according to Fig. 10, 12, 13.
If the received byte is incorrect, we set the m_operation_result as RES_COM_ERROR (lines 276, 291) and emit the operation_result_changed signal (line 277, 292).
All other commands are processed in the same way. First, we get the length of the received data and save it into the len variable (line 297). If len is greater than 3 (line 298), which means that we already have received the LNH and LNL bytes of the packet, then we calculate the packet length claimed by the MCU and save it into the packet_length variable (line 300). As you remember, the real length of the packet is 5 bytes greater than the value sent in the LNH and LNL bytes. That’s why we check if len is equal to the packet_length + 5 (line 301). This means that we have received the whole packet. Now we need to check its validity. We do it on several levels.
First, we check if the first byte is 0x81 and the last byte is 0x03 (line 303). In this case the packet format is considered to be correct. We stop the response timeout timer (line 305), send the received data to the console if the DEBUG macro is defined (lines 306-314), and check the third byte of the m_buf_rx (line 315). This byte is called RES and consists of the operation result from the MCU. If its upper bit is 0 then the result is OK, and if it’s 1 then some error has occurred. In this case we check the next byte of the m_buf_rx (line 317) to get the code of the error. Then we set the m_operation_result according to Fig. 16 (lines 317-331) and emit the operation_result_changed signal (line 332). Then we clear the buffer (line 333), set m_command as COM_NONE (line 334) and exit the application (line 335).
If we have passed this check, we get to line 338, where we start calculating the checksum of the received packet. If the checksum is not 0 (line 341) then we emit the operation_result_changed signal with the parameter RES_WRONG_RESPONSE and exit the application (lines 343-347).
Otherwise we consider the received packet as valid and set the m_operation_result as RES_OK (line 350).
In line 353 we start the switch construction in which we check all the commands and write the reaction on them.
The first three values (COM_NONE, COM_LOW_PULSE, and COM_GEN_CODE) are present here only to omit the warning that all enumerated positions should be present in the switch construction.
On receiving the COM_INQUIRY command (line 358) we clear the m_buf_rx array (line 359) and call the send_signature_request command (line 360).
I have skipped the description of the next commands, you can find them in the source file and deal with them by yourself. There was only one weird thing, which is not listed here, about setting the UART baud rate. After sending the Signature request command, we get the recommended maximum baud rate value. In my case it is 1 500 000 baud. But when I try to set this value in the SCI boot mode, there is no more response from the MCU. And only if I set the 115200 baud, everything works fine, yet slow. For the USB boot mode this problem doesn’t exist. I don’t know why it is so but to make the program operate in both modes, I only increased the baud rate to 115200 baud (you can find this in lines 412 and 416 of the “bootloader.cpp” file).
We will consider in more detail the Write command, as it has certain peculiarities (lines 458-486).
First, we check if we have reached the end of the file (line 459). If not, then we try to read 1024 bytes from the file (line 461) and save the actual number of read bytes in the bytes_read variable. Then we need to align this value to the write access unit. What does this mean? For example, we have a data chunk of 900 bytes, and the write access unit is 8 bytes. This means that the smallest possible size of data to write to flash memory is 8 bytes. If we divide 900 by 8, we get 112.5. This means that there are 112 full parts of 8 bytes and another unaligned 4 bytes. If we try to send such a data chunk to the MCU, it will give an error. To avoid this, we need to add 4 dummy bytes to the end of the data chunk and send 904 bytes to the MCU. In this case, everything will work fine.
Let’s see how it’s done in the program. We first copy the bytes_read value into the bytes_read_aligned variable (line 462). Then we in the loop check all memory areas (line 463) to find the code flash area, which has the kind of area (KOA) value of 0x00 (line 465). Once we find it, we divide the bytes_read_aligned variable by the write access unit (line 467) and then multiply it by the same value (line 468). This makes sense because the division is integer, and the fractional part is ignored. Then we check if the new value is less than the initial bytes_read (line 469), and if it is so, we add one more write access unit to it (line 470).
In our example:
900 / 8 = 112;
112 x 8 = 896;
as 896 < 900, we add 8 to 896 and finally get 904.
Then we send the data to the MCU (line 474) and clear the receive buffer (line 475).
If we have reached the end of the file (line 477), we then close it (line 479), clear the receive buffer (line 480), open the file again to start the verification (line 481), and call the send_read_command to start the reading from the flash memory (line 484).
OK, I think with the provided information you can deal with the rest of the code and even modify it as you need. Now let’s do some practical work and download the MCUboot, the primary slot and the secondary slot using our application.
For clarity, let’s use the “mcuboot_overwrite_with_signature” and “blinky_overwrite_with_signature” projects from the first part of this tutorial.
First we need to open the “mcuboot_overwrite_with_signature” project and call its properties. We now need to generate the binary output file that our program can recognize. In the Properties window we need to expand the “C/C++ Build” list in the left part, and select the “Settings”, then in the central part select the “General” point of the “GNU Arm Cross Create Flash Image” list, and in the right part select the “Raw binary” in the “Output file format” drop-down list. Also set the checkboxes “Section -j .text” and “Section -j .data” (Fig. 21).
Then click “Apply and Close”, and build the project. After that you should see the “mcuboot_overwrite_with_signature.bin” file in the “Debug” folder of the project (Fig. 22).
Now let’s download this file to the MCU using our bootloader application. I have Windows 10, so I will show how to do it in this operating system. First find the bootloader.exe file and copy the full path to it into the buffer. In my case it’s located in the “d:\Prog\Qt\build-Bootloader-Desktop_Qt_6_3_1_MinGW_64_bit-Debug\” folder.
Connect the MCU to the PC using either the Device USB port or the USB-to-UART connector, set the MD pin low by setting the “Boot” jumper into the “SCI/USB Boot” position (Fig. 2) and press the Reset button to enter the boot mode.
Open the command prompt by using the combination Win+R, and typing the “cmd” in the opened window (Fig. 23).
Then navigate to the “bootloader.exe” folder using the “cd” command (Fig. 24).
Now let’s execute our program by writing the following command (Fig. 25).
I already explained above how to find the name of the COM-port (Fig. 3). The address should be 0x0000, and the file name of the file with the full path can be found in e2 studio if you look at the properties of the “mcuboot_overwrite_with_signature.bin” file (Fig. 26).
Now you can click the Enter button and see the result of operation of the application (Fig. 27).
Well, everything seems to be fine. Now the MCUboot is in the flash memory of the MCU. Now we need to download the user’s application into the primary slot. As you remember from the first part, it starts at the address 0x3800, and the secondary slot starts at 0x8800.
Let’s now open the “blinky_overwrite_with_signature” project and build it. Find the full path to the file “blinky_overwrite_with_signature.bin.signed” (Fig. 28).
Click the Reset button on your board to run the boot mode once again. Modify the parameters of the command given in the command line according to Fig. 29 and click Enter to execute it.
You should see the same result as in Fig. 27 but with a different file name and address. Let’s now check if MCUboot can run our application. To do this, put the “Boot config” jumper into the “Internal flash” position (Fig. 2) and click the Reset button. You should see that the LED starts to blink. This means that we have done everything correctly.
Now let’s open the “hal_entry.c” file of the “blinky_overwrite_with_signature” project and change the blinking frequency (Fig. 30).
Now let’s rebuild this application, then put the “Boot config” jumper into the “SCI/USB Boot” position again and press Reset to switch to the boot mode one more time. Modify the command by changing the address to the 0x8800 (Fig. 31) and run the bootloader application one more time.
After finishing the downloading process, return the jumper into the “Internal flash” mode and reset the MCU. Now the LED should blink faster. This means that everything works as desired, and our application can really download any binary files into any address of the code memory.
And that’s all about the native bootloader of the RA MCUs I wanted to tell you. As you can see it’s not very difficult to use it. The application, as I mentioned before, is cross-platform so you can run it on any supported by the Qt operating system. You even can run it on Raspberry Pi, creating a portable programmer which you can take everywhere.
I don’t give you any homework this time. If you want and have the required knowledge you can try to adapt the application for your needs. Otherwise just use it and enjoy. If you have any questions about it, please feel free to write them as the comments under this tutorial.
Get the latest tools and tutorials, fresh from the toaster.