Help running Photon without Device OS

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 ..."
        )

@bakl - Welcome to the Particle community!

Is this just some sort of fun personal challenge to see if it can be done? Maybe to learn about the device OS build process or what’s the problem you are trying to solve by not using device OS?

In general, what I love about the Particle platform is so much of this complexity is abstracted away that I don’t even have to think about it. “It just works!”

If you don’t plan on connecting it to the Particle cloud, you can just use system mode manual. SYSTEM_MODE(MANUAL);

For the Photon, I would start from a different direction. It contains a USI WM-N-BM-09 and it runs the Broadcom/Cypress WICED stack for Wi-Fi. If you start from the being a generic STM32F205, you'll never get the Wi-Fi working because it's not a standalone controller, it's a mix of hardware and proprietary software in WICED. You can download WICED for free from Cypress, however.

The system-part1 binary contains a prefix and suffix block, in addition to the code and the CRC. If you don't have the valid blocks, the bootloader will not run the binary. If you really want to use the Particle bootloader, I'd fake a monolithic binary header for the Photon platform.

I believe you will make your life easier by getting a ST-LINK/V2 clone (they're under $10) and not using the Particle bootloader. Then you'll have a plain STM32 like any other dev board and you can just flash it to address 0.

2 Likes

I know it would be way easier if I just bought something off the shelf. This is kind of a challenge for myself. Is this the right thing? I think cypress got bought out by Infineon.

Would this programmer work? Also do I use OpenOCD to program it?

That should be the right debugger and that does look like the right WICED software.

OpenOCD is usually the best option with the clone debuggers. Some of them work with the real ST-LINK/V2 Windows software, though that's not technically legal.

To get my Clone STLINK-V2 working I had to use the STM32CubeProgrammer and update the firmware. I was getting this error before I did it. This was generated with sudo openocd -d -f interface/stlink.cfg -f target/stm32f2x.cfg

Debug: 131 13 stlink_usb.c:3093 stlink_dump_speed_map(): Supported clock speeds are:
Debug: 132 13 stlink_usb.c:3096 stlink_dump_speed_map(): 4000 kHz
Debug: 133 13 stlink_usb.c:3096 stlink_dump_speed_map(): 1800 kHz
Debug: 134 13 stlink_usb.c:3096 stlink_dump_speed_map(): 1200 kHz
Debug: 135 13 stlink_usb.c:3096 stlink_dump_speed_map(): 950 kHz
Debug: 136 13 stlink_usb.c:3096 stlink_dump_speed_map(): 480 kHz
Debug: 137 13 stlink_usb.c:3096 stlink_dump_speed_map(): 240 kHz
Debug: 138 13 stlink_usb.c:3096 stlink_dump_speed_map(): 125 kHz
Debug: 139 13 stlink_usb.c:3096 stlink_dump_speed_map(): 100 kHz
Debug: 140 13 stlink_usb.c:3096 stlink_dump_speed_map(): 50 kHz
Debug: 141 13 stlink_usb.c:3096 stlink_dump_speed_map(): 25 kHz
Debug: 142 13 stlink_usb.c:3096 stlink_dump_speed_map(): 15 kHz
Debug: 143 13 stlink_usb.c:3096 stlink_dump_speed_map(): 5 kHz
Debug: 144 22 stlink_usb.c:1086 stlink_usb_error_check(): STLINK_JTAG_GET_IDCODE_ERROR
Error: 145 22 stlink_usb.c:3748 stlink_open(): init mode failed (unable to connect to the target)
Debug: 146 23 stlink_usb.c:1659 stlink_usb_exit_mode(): MODE: 0x01
Debug: 147 23 hla_layout.c:36 hl_layout_open(): failed
Debug: 148 23 command.c:544 run_command(): Command 'transport init' failed with error code -4
User : 149 23 command.c:608 command_run_line(): 
Debug: 150 23 command.c:544 run_command(): Command 'init' failed with error code -4
User : 151 23 command.c:608 command_run_line(): 
Debug: 152 23 target.c:2199 target_free_all_working_areas_restore(): freeing all working areas
Debug: 153 24 hla_interface.c:119 hl_interface_quit(): hl_interface_quit

You must have the device in DFU mode for debugging to start. To get it into DFU mode you hold the reset and setup buttons, release the reset button, and keep holding the setup button until the LED blinks yellow.
I'm still yet to get my own firmware working, but I wanted to document how I got here in case anyone else ever tries to do what I'm doing or has that error.
Here are some useful links that I used.

A brief update with the openOCD commands.

To disable flash protect
openocd -f interface/stlink.cfg -f target/stm32f2x.cfg -c "init" -c "reset halt" -c "flash protect 0 0 8 off" -c "reset" -c "exit"

To enable flash protect
sudo openocd -f interface/stlink.cfg -f target/stm32f2x.cfg -c "init" -c "reset halt" -c "flash protect 0 0 0 on" -c "flash protect 0 5 8 on" -c "reset" -c "exit"

To flash with STM32.bin
sudo openocd -f interface/stlink.cfg -f target/stm32f2x.cfg -c "init" -c "reset halt" -c "program STM32.bin 0x08000000" -c "reset" -c "exit"

I also found GitHub - STM32-base/STM32-base-F2-template: A template for using STM32F2 series devices with the STM32-base project. and the associated guides very helpful