This tutorial shows how to use VisualGDB to build a project using the Keil MDK-ARM compiler, use VisualGDB IntelliSense including CodeMap and debug it with Visual Studio.
- Before you begin, ensure you can build your Keil project with the uVision IDE:
- Open Visual Studio and begin creating a new Embedded VisualGDB project:
- Select “Import a project” and “Generate a Makefile” options:
- On the next page choose the ARM toolchain and then select “specify flags manually” and leave the flags empty for now:
- Specify the directory containing your Keil project. If you do not have too many files, you can select “show all source files together” to flatten the Keil directory structure:
- Select a debug method. In this example we will use OpenOCD with ST-Link integrated in the STM32F411RE Nucleo board:Do not forget to manually select the device type. As you are specifying the flags manually, VisualGDB won’t be able to detect it.
- By default VisualGDB will not import the assembly startup file (.s), so copy it to the new project directory and add it to the project via context menu:
- Now we need to import the compiler and linker flags from the uVision project. Open target properties in uVision and copy the compiler control string:
- The options specified there need to be split into 3 categories:
- Include directories and preprocessor macros. Those options are important for IntelliSense.
- Keil-specific options. Those options are not gcc-compatible and would produce IntelliSense errors.
- Options like “-c” and “-o” that specify file names. They are automatically added by the Makefile.
The table below classifies the options for a sample uVision project:
Option Meaning Category -c Compile file Ignored -g Produce debug symbols Irrelevant for IntelliSense -O0 Disable optimization Irrelevant for IntelliSense –apcs=interwork Specify function call convention Keil-specific –split_sections Equivalent to -ffunction-sections Keil-specific -I <…> Specify include search path Relevant for IntelliSense -D<…> Define a preprocessor macro Relevant for IntelliSense -o “…” Output file Ignored –depend “…” Dependency file Ignored
- First of all, we will add IntelliSense-relevant options to the project properties. Open VisualGDB Project Properties on the first page and specify the defines and include directories extracted from uVision flags:Note that you can specify paths relative to the project directory (e.g. RTE means <project directory>\RTE).
- Go to the Makefile Settings page and remove all GCC-specific flags except -g and -O0 also present in the uVision flags (also disable the binary file generation):We will specify Keil-specific flags later. If you enter them here, it will confuse the Clang-based IntelliSense engine.
- Press OK. VisualGDB will recheck the flags and update IntelliSense. Open your source file and ensure that there are no IntelliSense errors and the syntax coloring is working correctly:
- Now we will change the Makefile to use the Keil compiler instead of GCC and will provide the Keil-specific flags. Open the mcu.mak file:
- The mcu.mak file specifies the compiler tools (CC, CXX, LD and AR) and the common flags that are used by the IntelliSense engine. As we don’t want to break IntelliSense, instead of modifying this file, we will specify them in a different place. Before that, open the Keil Target options and take a note of the linker flags:
- Now we will specify the Keil-specific flags. Create a file called keil.mak in the project directory and put the remaining C/C++ flags and linker flags there as well as the overrides for CC,CXX and other tools:Here is a sample contents of keil.mak:
123456789101112131415161718192021222324252627KEIL_ROOT := C:/Keil_v5/ARM/ARMCCCC := $(KEIL_ROOT)/bin/armcc.exeCXX := $(CC)LD := $(KEIL_ROOT)/bin/armlink.exeAR := $(KEIL_ROOT)/bin/armar.exeFROMELF := $(KEIL_ROOT)/bin/fromelf.exeCOMMONFLAGS += --cpu Cortex-M4.fp --apcs=interworkLDFLAGS += --cpu Cortex-M4.fp \--ro-base 0x08000000 \--entry 0x08000000 \--rw-base 0x20000000 \--entry Reset_Handler \--first __Vectors \--strict \--summary_stderr \--info summarysizes \--map \--xref \--callgraph \--symbols \--info sizes \--info totals \--info unused \--info veneers \--list "$(BINARYDIR)/stm32demo.map"
- Finally we need to adjust the Makefile. First, specify “ADDITIONAL_MAKE_FILES += keil.mak” between the two include directives:
123include $(CONFIGURATION_FLAGS_FILE)ADDITIONAL_MAKE_FILES += keil.makinclude $(ADDITIONAL_MAKE_FILES)
Then replace all occurences of “-MD -MF” with
“–depend”. A typical C++ build rule will look like this:
12$(BINARYDIR)/%.o : %.cpp $(all_make_files) |$(BINARYDIR)$(CXX) $(CXXFLAGS) -c $< -o $@ --depend $(@:.o=.dep)
- If you build your project now you will get several errors regarding missing HAL symbols:
- To fix that, first locate the HAL folder via uVision:
- The source files in that folder implement various HAL functions:
- Right-click in Solution Explorer, select Add->Existing Item and add the HAL source files to the project.Then build your solution. Double-check in the build log that VisualGDB used the Keil compiler and linker:
- Now press F5 to begin debugging your program. Once you verify that the LED is blinking, set a breakpoint somewhere in your program loop and verify that you can evaluate the variables:
- Right-click on the main() function, open CodeMap and select “show called functions” to verify the Clang IntelliSense:
- The ELF file produced by the Keil linker will have one fundamental problem. If you load it with GDB, the global variables will have a value of -1 instead of the normal initial values:
- This happens because of the differences between
the GNU and the Keil linker. The GNU linker computes 2
different addresses for the data section: the address where
it will be loaded (VMA) and the address in the FLASH memory
where its contents will be stored (LMA). GDB then puts the
section contents at the LMA address and the startup code
copies it to VMA. Keil linker sets the LMA = VMA, so instead
of writing the data section to FLASH, GDB puts it directly
into RAM where it gets overwritten by the Keil statup code
that expects it to be in the FLASH. To fix this we first
need to find out the section name. Run arm-eabi-objdump.exe -x <ELF FILE> on your ELF file (objdump tool is a part of the GCC toolchain):
12345678Sections:Idx Name Size VMA LMA File off Algn0 ER_RO 0000168c 08000000 08000000 00000034 2**2CONTENTS, ALLOC, LOAD, READONLY, CODE1 ER_RW 00000020 2000000020000000 000016c0 2**2CONTENTS, ALLOC, LOAD, DATA2 ER_ZI 00000660 20000020 20000020 000016e0 2**3ALLOC
- We will fix this in two steps: first we’ll run the Keil tool called fromelf.exe to convert the ELF file into a .bin file that will reflect the FLASH memory contents the way Keil tools expect it:
1fromelf.exe --bin --output <bin file> <elf file>
Then we will edit the .elf file using the objcopy tool to replace the data that gets copied to the FLASH memory with the contents of the .bin file. This will ensure that GDB puts the initialization data into the FLASH in the same way as Keil expects it:
1234arm-eabi-objcopy.exe --remove-section ER_RO \--add-section ER_RO=<bin file> \--set-section-flags ER_RO=CONTENTS,ALLOC,LOAD,READONLY,CODE \--change-section-address ER_RO=<previous address of ER_RO>\
- The modified fragment of the Makefile will look this way (section names and addresses depend on the device and should be taken from the dump of the original elf file):
12345678910111213ifeq ($(TARGETTYPE),APP)ROM_SECTION_NAME := ER_RO$(BINARYDIR)/$(TARGETNAME): $(all_objs) $(EXTERNAL_LIBS)$(LD) -o $(@:.elf=.axf) $(LDFLAGS) $(START_GROUP) $(all_objs) \$(LIBRARY_LDFLAGS) $(END_GROUP)$(FROMELF) --bin --output $(@:.elf=.bin) $(@:.elf=.axf)$(OBJCOPY) --remove-section $(ROM_SECTION_NAME) \--add-section $(ROM_SECTION_NAME)=$(@:.elf=.bin) \--set-section-flags \$(ROM_SECTION_NAME)=CONTENTS,ALLOC,LOAD,READONLY,CODE \--change-section-address $(ROM_SECTION_NAME)=0x08000000 \$(@:.elf=.axf) $@endif
The diagram below explains the dependencies between the .axf (KEIL ELF), .bin and the final .elf files:
- Now build the project and verify that the variables are initialized properly:
- If they are still broken, double-check that the ROM section has been placed at the correct address and that its size matches the size of the bin file by inspecting the dump produced by the arm-eabi-objdump tool:
123456789Sections:Idx Name Size VMA LMA File off Algn0 ER_RW 00000020 20000000 20000000 00000054 2**2CONTENTS, ALLOC, LOAD, DATA1 ER_ZI 00000660 20000020 20000020 00000000 2**3ALLOC<...>11 ER_RO 000016ac 08000000 08000000 00047460 2**0CONTENTS, ALLOC, LOAD, READONLY, CODE
- As we have created the project manually, VisualGDB won’t display the peripheral registers for your device. To fix that, create a normal VisualGDB project for your device and search the <MCU FAMILY>.xml file in the project directory for the MCUDefinitionFile element, e.g.:
- Find the file (it will have a .gz extension) in %LOCALAPPDATA%\VisualGDB\EmbeddedBSPs and copy it to the directory containing the mcu.xml file of the imported Keil project. Then modify the mcu.xml file to reference the device definition file:
- Start debugging your project again and verify that the hardware registers are now shown:
- Finally we will fix the parsing of Keil error messages. As they use a different syntax than GCC, VisualGDB won’t recognize them by default. You can reproduce this by adding a simple function that will produce a warning and an error and trying to build your file:
- In order to support the “<file>, line <line>” format, download the BuildMessageTemplates.xml file and save it into the VisualGDB directory under Program Files. If you build your project now, Visual Studio will show the errors and warnings correctly:
- If you are actively using VisualGDB with the Keil compiler, let us know on our forums. Once we gather enough feedback, we will simplify the integration with the Keil tools in the next VisualGDB versions.