I have kind of a weird goal: to run my Photon without device OS. This came about after I did similar thing with an Arduino. I was able to program for the Arduino without using the Arduino IDE by looking at what commands the IDE runs to build and upload and copying them and ran them myself. I grabbed a copy of their libraries, built them to an statically linked elf file and ran these commands to convert from elf and upload it avr-objcopy -j .text -j .data -O ihex arduinoTest arduinoTest.hex && avrdude -p m2560 -c stk500v2 -P /dev/ttyACM0 -b 115200 -F -D -U flash:w:arduinoTest.hex
. I still used the Arduino bootloader to not have to ISP every time I wanted to upload a new firmware.
After a bit of research I found that ST makes their own HAL for the STM32F205RGY6. ST's HAL does all of what I would have used device OS for because I don't plan on using the cloud platform part of Particle. I downloaded it from here. After a bit of struggling I managed to get that to build to an elf with arm-none-eabi-gcc
. The next step was trying to get it to run on the micro controller. To do this I had to figure what the device OS build process does. I turned on verbose mode for make and read through the logs. I eventually came up with these commands to turn the elf file into one I can upload with dfu-util
. I still plan on using the Particle bootloader. The script is based off of module.mk
#!/usr/bin/bash
# This script turns an elf file into an uploadable dfu binary.
arm-none-eabi-objcopy -O binary STM32 STM32.bin.pre_crc
if [ -s STM32.bin.pre_crc ]; then
#
# Fix the CRC
#
#Get the size that the file should be with out the crc
preCrcSize=`stat -c %s STM32.bin.pre_crc`
#Remove the crc
head -c $(($preCrcSize - 38)) STM32.bin.pre_crc > STM32.bin.no_crc
#Take the crc and put it in another file
tail -c 38 STM32.bin.pre_crc > STM32.bin.crc_block
#Check if this value equals the checksum. This is used to check for the if it built right for device OS. We don't use this
#correctChecksum="0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20280078563412"
#if [ $correctChecksum = `xxd -p -c 500 STM32.bin.crc_block` ]; then
if [ true ]; then
shasum -a 256 STM32.bin.no_crc | cut -c 1-65 | xxd -r -p | dd bs=1 of=STM32.bin.pre_crc seek=$(($preCrcSize - 38)) conv=notrunc
head -c $((`stat -c %s STM32.bin.pre_crc` - 4)) STM32.bin.pre_crc > STM32.bin.no_crc
crc32 STM32.bin.no_crc | cut -c 1-10 | xxd -r -p | dd bs=1 of=STM32.bin.pre_crc seek=$(($preCrcSize - 4)) conv=notrunc
fi
rm STM32.bin STM32.bin.no_crc STM32.bin.crc_block
mv STM32.bin.pre_crc STM32.bin
#
# Add DFU Suffix
#
cp STM32.bin STM32.dfu
dfu-suffix -v 2B04 -p D006 -a STM32.bin
fi
My Code:
#define USE_FULL_ASSERT 1;
#include <stdlib.h>
#include <stm32f2xx_gpio.h>
#include <stm32f2xx.h>
extern "C" {
//ADD: Maybe do something when things break? This gets triggered when you send the wrong things to a function
void assert_failed(uint8_t* file, uint32_t line) {
return;
}
//These are functions that would normally be handled by the OS, but since we don't have one we just return an error.
int _close_r(int file) {
return -1;
}
int _lseek_r(int file, int ptr, int dir) {
return 0;
}
//ADD: Maybe make it if you try and read from cin then it sends it on the serial port?
int _read_r(int file, char *ptr, int len) {
return 0;
}
//ADD: Maybe make it if you try and write to cout then it sends it on the serial port?
int _write_r(int file, char *ptr, int len) {
return len;
}
}
int main(void) {
//GPIO_ToggleBits(GPIOA,GPIO_Pin_0);
GPIO_InitTypeDef config;
config.GPIO_Pin = GPIO_Pin_13;
config.GPIO_Mode = GPIO_Mode_OUT;
config.GPIO_Speed = GPIO_Speed_2MHz;
config.GPIO_PuPd = GPIO_PuPd_NOPULL;
config.GPIO_OType = GPIO_OType_OD;
GPIO_Init(GPIOA, &config);
//PA13 is D7
GPIO_WriteBit(GPIOA, GPIO_Pin_13, Bit_RESET);
}
I then upload STM32.dfu
to where system part 1 would normally be with sudo dfu-util -d 0x2B04:0xD006 -a 0 -s 0x8020000 -D STM32.dfu
, but after restarting the micro controller nothing happens. The led at pin D7
stays on. I used the schematics to determine that PA13
was connected to D7
I did a little bit of looking into how the bootloader works and it seems like it checks if you want to enter any of the special modes (DFU, etc.), and it jumps to 0x8020004
. See here for the jump, https:// github. com/particle-iot/device-os/blob/v3.3.1/bootloader/src/main.c#L189 for where the application address variable get set, and https:// github. com/particle-iot/device-os/blob/v3.3.1/platform/MCU/STM32F2xx/SPARK_Firmware_Driver/inc/flash_mal.h#L58 (only four links allowed in each post so I had to break them :/) for the real address. Since the firmware gets uploaded at 0x8020000
it seems like the fourth byte of the file is the first thing to execute.
I then took a peek at the elf and binary versions of system-part1
(system-part1.elf
and system-part1.bin
) in Ghidra. This yields something interesting. The first few hundred bytes are just zeros except for the first eight bytes. I guess those are the address that gets read from and jumped to.
I started trying to decode the instruction at 0x4
and I couldn't. It wasn't a valid instruction. This all makes no sense. It should be running this, but this would be an invalid instruction, but system-part1.bin
does work. Then I figured something out. It executes the data at address contained at 0x8020004
not what is at that address directly. It also moves the stack to the address contained at 0x8020000
. That makes a lot more sense. The addresses also seemed invalid until I realized that they were little endian.
The issue now is how do I make it put the starting function's address at the start of the file? What is the mystery gcc / ld / objcopy option that puts address of the start function at address 4? In Ghidra my file just has normal data (in this case the _init
function) starting at the top of the file after the elf header. Ghidra seems to point the symbols of the starting addresses at https:// github. com/particle-iot/device-os/blob/v3.3.1/modules/shared/stm32f2xx/inc/system_part1_loader.c. It also thinks that the data comes from these symbols system_part1_boot_table_start link_boot_table_start PLATFORM_DFU system_part1_start system_part1_boot_table
wherever those might be defined. Any Ideas?
For reference here is my CMakeLists.txt
file.
cmake_minimum_required(VERSION 3.10)
project(STM32 LANGUAGES CXX C)
set(CMAKE_C_COMPILER /usr/bin/arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-none-eabi-g++)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mcpu=cortex-m3 --specs=nosys.specs")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mcpu=cortex-m3 --specs=nosys.specs")
set(CMAKE_CXX_STANDARD "20")
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_EXPORT_COMPILE_COMMANDS "yes")
include_directories("${CMAKE_SOURCE_DIR}/src")
include_directories("${CMAKE_SOURCE_DIR}/libs/StdPeriph_Driver/inc")
include_directories("${CMAKE_SOURCE_DIR}/libs/CMSIS/Include")
# Define a variable containing a list of source files for the project
set(SRC_FILES
src/main.cpp)
# Define the build target for the executable
add_executable(${PROJECT_NAME}
${SRC_FILES} libs/StdPeriph_Driver/src/stm32f2xx_cryp.c libs/StdPeriph_Driver/src/stm32f2xx_dcmi.c libs/StdPeriph_Driver/src/stm32f2xx_gpio.c libs/StdPeriph_Driver/src/stm32f2xx_iwdg.c libs/StdPeriph_Driver/src/stm32f2xx_sdio.c libs/StdPeriph_Driver/src/stm32f2xx_wwdg.c libs/StdPeriph_Driver/src/stm32f2xx_adc.c libs/StdPeriph_Driver/src/stm32f2xx_cryp_des.c libs/StdPeriph_Driver/src/stm32f2xx_dma.c libs/StdPeriph_Driver/src/stm32f2xx_hash.c libs/StdPeriph_Driver/src/stm32f2xx_pwr.c libs/StdPeriph_Driver/src/stm32f2xx_spi.c libs/StdPeriph_Driver/src/stm32f2xx_can.c libs/StdPeriph_Driver/src/stm32f2xx_cryp_tdes.c libs/StdPeriph_Driver/src/stm32f2xx_exti.c libs/StdPeriph_Driver/src/stm32f2xx_hash_md5.c libs/StdPeriph_Driver/src/stm32f2xx_rcc.c libs/StdPeriph_Driver/src/stm32f2xx_syscfg.c libs/StdPeriph_Driver/src/stm32f2xx_crc.c libs/StdPeriph_Driver/src/stm32f2xx_dac.c libs/StdPeriph_Driver/src/stm32f2xx_flash.c libs/StdPeriph_Driver/src/stm32f2xx_hash_sha1.c libs/StdPeriph_Driver/src/stm32f2xx_rng.c libs/StdPeriph_Driver/src/stm32f2xx_tim.c libs/StdPeriph_Driver/src/stm32f2xx_cryp_aes.c libs/StdPeriph_Driver/src/stm32f2xx_dbgmcu.c libs/StdPeriph_Driver/src/stm32f2xx_fsmc.c libs/StdPeriph_Driver/src/stm32f2xx_i2c.c libs/StdPeriph_Driver/src/stm32f2xx_rtc.c libs/StdPeriph_Driver/src/stm32f2xx_usart.c)
add_custom_command(TARGET STM32 POST_BUILD
COMMAND ../elftodfu2.sh && sudo dfu-util -d 0x2B04:0xD006 -a 0 -s 0x8020000 -D STM32.dfu
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Uploading ..."
)