[下载最新版的SDK,里面有现成的SES的工程,无需再手动搭建,本文仅做参考。文末的参考链接,值得一看。]

1. 简介

SEGGER Embedded Studio(SES)是推出过J-Link的公司SEGGER推出的嵌入式开发IDE。虽然SES 比较小众,但是它有一些特别突出的优势:

(1)对非商业使用免费。

注意,是全功能、不限时免费。Keil和IAR的试用版都对二进制文件的大小有限制。

(2)编辑器很友好。

Keil与IAR的编辑器对于用户体验的忽视,简直达到了令人发指的程度。这里吐槽一下Keil,关闭标签操作连快捷键都没有,每次打开新标签都得用鼠标关闭,极其影响效率。

SES在编辑器方面做的非常用心,比如超快的智能提示,代码格式化,Ctrl+点击跳转到函数实现,等等功能。

(3)配置灵活。

举个例子,做nRF52 BLE开发,需要先下载Softdevice的Hex文件,再下载Application的Hex文件。一般都是依次下载,或者用批处理脚本。SES在设置界面预留了3个Bootloader位置,在用户程序下载前自动下载Bootloader。

(4)跨平台。

Linux跟Mac也可以快速进行嵌入式开发了。

本文利用SES搭建Nordic nRF52832的开发环境,在SES上开发调试Nordic BLE程序。

2. 下载安装

SES下载地址:https://www.segger.com/downloads/embedded-studio

我的电脑上已经安装好了Nordic SDK13和Keil 5,这也是必需的软件。

安装完毕,软件的样子如下:

SES_Dev_GUI

点开菜单项:Tools > Package Manager,下载两个项目:

  • CMSIS-CORE Support Package (不是CMSIS 5 CMSIS-CORE Support Package)
  • nRF CPU Support Package

此时软件已经装完,但是我们还需要额外下载以下几个文件:

文件 含义
ses_nrf52_startup.s 启动文件,包含了中断向量表
flash_placement_nrf52832.xml Flash布局描述文件,描述Flash和RAM的分配
thumb_crt0_nrf52832.s ARM基础文件

在SDK\component\toolchain新建一个ses文件夹,将这几个文件放这里备用。在我的电脑上路径是C:\Nordic\SDK\13.1.0\components\toolchain\ses。

3. 导入Keil工程

以ble_app_template工程为例,先用Keil运行该工程,确保能够正常编译和运行。

启动SES,打开File > Import IAR/Keil Project…,选择该Keil工程文件*.uvprojx,选择“Internal Toolchain”一项,如下:

SES_Dev_Import_Keil

打开该工程后,按F7构建工程,会报错。

第一个错误说nrf.h不存在。这是因为Keil有PACK系统,包含了该文件,SES这里需要手动添加Include目录。

执行下述步骤:

1. 选中工程Project 'ble_app_template_pca10040_s132',而非选中某个文件。

2. 点击Edit Options按钮,或菜单Project > Edit Options...。

SES的文件管理系统比较复杂,每个文件或文件夹都可以设置自己的Option,最终再进行逻辑合并形成最终的Option。所以这里要选择工程项目,然后再打开Edit Options对话框。如下图所示:

SES_Dev_Edit_Options

默认情况下,Build Configuration处显示的是SoftDevice的配置项,这里要先选中应用程序的配置项,然后找到Code > Preprocessor > User Include Directories,在里面加入../../../../../../components/device,如下图:

SES_Dev_Preprocessor

SoftDevice是Keil工程上用来方便下载SDK协议栈的编译配置项,在SES中没有用,可以直接删掉。打开菜单:Project > Build Configurations…,删除SoftDevice项,如下:

SES_Dev_Remove_Build_Config

这时再按F7编译,提示section .fs_data相关错误,说明内存布局有问题,需要配置Flash_Placement.xml和thumb_crt0.s文件。

这里不讨论这些文件的内容含义,简单讲就是告诉SES,该工程的Flash内容和RAM内容分别放在哪里。更多的信息可以参考帮助文件,讲的很清楚。

在SES的文件目录中展开Internal Files,删除Cortex_M_Startup.s和thumb_crt0.s。在该文件夹上点右键,选择Add Existing File…,添加前面下载的thumb_crt0_nrf52832.s和ses_nrf52_startup.s,同时添加C:\Nordic\SDK\13.1.0\components\toolchain\system_nrf52.c。如下图:

SES_Dev_Add_Internal_Files

选择工程文件,打开Edit Options对话框,利用搜索框功能,找到Linker > Section Placement File,输入前面下载的flash_placement_nrf52832.xml路径:

C:/Nordic/SDK/13.1.0/components/toolchain/ses/flash_placement_nrf52832.xml

这时编译一下,就可以编译成功了,会看到非常醒目的进度条:

SES_Dev_Build_Complete

如果是一个纯Peripheral工程,不包含SoftDevice,则配置到此结束。

但是本文介绍BLE工程,需要用到SoftDevice,到这里还没有完,因为Nordic nRF52832的Flash结构是下面是SoftDevice,上面是Application,所以需要设置Application存在Flash的位置,给SoftDevice留下位置。

选择工程文件,打开Edit Options对话框,找到Section Placement Macros,输入

FLASH_START=0x1F000
SRAM_START=0x20002000

然后再按F7编译一下,会看到进度条发生了变化。

SES_Dev_Flash_Placement_Config

既然给SoftDevice留下了位置,下面配置如何烧写SoftDevice。

1. 打开工程的Edit Options
2. 找到Preprocessor > Preprocessor Definitions,增加 NO_VTOR_CONFIG
3. 找到Loader > Additional Load File[0],输入
C:/Nordic/SDK/13.1.0/components/softdevice/s132/hex/s132_nrf52_4.0.2_softdevice.hex

到此,所有的配置完全结束,编译一下,连上开发板就可以下载运行了。

4. 小技巧

(1)利用工程宏变量(Project Macro),简化路径的输入

在工程的Edit Options窗口中找到Project Macros,输入:nRF52_SDK13_Dir=C:/Nordic/SDK/13.1.0

然后所有对SDK文件的引用,都可以用$(nRF52_SDK13_Dir)来代替。

比如User Include Directories里面的内容就可以变成:

$(PackagesDir)/CMSIS_4/CMSIS/Include
$(nRF52_SDK13_Dir)/components
$(nRF52_SDK13_Dir)/components/device
...

看起来更加清爽,而且不容易出错。

(2)手动编辑emProject工程文件

从Keil导入的工程,默认的Build Configuration名字为nrf52832_xxaa,以芯片型号来命名很容易引起误解。下面通过编辑emProject文件来更改这个命名。

退出SES,用记事本打开emProject文件。

找到

<configuration  Name="nrf52832_xxaa" ...

项,显然这个Name属性就是编译配置的名称,在文件内搜索该字符串,发现有两个地方用到了,直接替换成Debug,即可。

此外,从Keil导入的工程,会多一个Internal的编译配置,我不知道这个配置项是什么作用,颇为干扰,也可以通过编辑emProject文件来删除Internal配置。

(3)更改目录结构

Nordic SDK13设计的目录结构非常繁复,目录结构很深,这么做的目的是为了保持不同编译器(Keil/IAR/GCC),不同版本SDK(11/12/13)之间的形式统一,方便用户选择。

但是对于开发者,通常在一段时间内只会使用一个SDK版本和编译器版本,深目录比较麻烦。

因为编辑emProject很容易,所以我们可以把整个工程目录结构改成如下形式:

SES_Dev_Change_Project_Stucture

图中,工程目录名字是ble_template,工程文件放在工程目录中,源文件都放在source文件夹中,output是SES的输出文件。

(4)CMSIS Configuration Wizard

Nordic nRF52的SDK中,每个工程都配备一个sdk_config.h文件,配合Keil可以可视化开关某些模块,如下图:

SES_Dev_Keil_Wizard

这个可视化配置工具比较方便,而且可以避免出错。

在SES中,可以借助第三方免费工具 CMSIS Configuration Wizard实现类似的功能,在该软件中打开sdk_config.h效果如下:

SES_Dev_CMSIS_Config_Wizard

该工具下载地址:https://sourceforge.net/projects/cmsisconfig/?source=directory

参考

https://devzone.nordicsemi.com/blogs/1032/segger-embedded-studio-a-cross-platform-ide-w-no-c/

(完)

MCU中几个常用的段类型如下:

Section R/W 描述
.data RW 带有初值的静态或全局变量
.rodata RO 只读变量
.bss RW 没有初值或初值为0的静态或全局变量。该段也称为.zi(Zero Initialized)
.text RO 代码段。该段也称为.code

.data和.bss段称为readwrite data,.rodata段称为readonly data, .text段称为readonly code。

在IAR平台下,工程编译后生成的.map文件中包含这几项的使用情况,如下:

  3 252 bytes of readonly  code memory
    106 bytes of readonly  data memory
  1 357 bytes of readwrite data memory

设置项目的输出文件格式为bin文件,观察bin文件的大小:

Bin File Size

可以看到Bin文件的大小恰好等于上述前两项之和。

Bin文件仅包含数据内容,相当于Flash,于是得到如下结论:

Used Flash = readonly code memory + readonly data memory
Used RAM   = readwrite data memory

疑问

ARM的映像文件包含了.rodata, .text和.data几个段的数据,为什么在这里bin文件大小却等于.rodata和.text两项的和呢?

一个工程代码总是以main作为入口函数,芯片上电时需要完成软硬件初始化工作,为执行main函数做准备。

复位流程

下图为MCU的映像文件部分内容:

MCU_Startup_Image

所谓映像文件,就是工程编译完成后生成的bin文件。

映像文件起始位置存放中断向量表(Vector Table),然后依次存放程序代码和其他数据。中断向量表的第一项为栈顶指针MSP(Main Stack Pointer)的初值,第二项为复位向量,它指向了程序的第一个指令,即复位处理函数Reset_Handler。

MCU_Startup_Reset_Sequence

如上图所示,MCU复位时,依次完成三个任务:

  1. 从向量表的第一项(@0x00000000)中取出MSP初值
  2. 从复位向量(@0x00000004)中取出Reset_Handler函数地址
  3. 跳转到Reset_Handler函数位置并执行

在Reset_Handler函数中,MCU执行软硬件初始化,包括Flash和RAM位置、时钟和PLL、静态和全局变量等。

下面详细介绍Reset_Handler的来龙去脉。

启动文件

启动文件定义了中断向量表、Reset_Handler函数,它直接决定了启动流程。

启动文件可以是C语言源文件文件(.c),也可以是汇编语言文件(.s)。IAR平台下启动文件通常是个汇编文件。

汇编格式的启动文件名字通常为startup_<< part_number >>.s,同一个型号芯片的启动文件一般相同。

下面以STM32L152芯片为例,打开startup_stm32l152xe.s文件,去掉注释和重复内容后,如下:

        MODULE  ?cstartup

        ;; Forward declaration of sections.
        SECTION CSTACK:DATA:NOROOT(3)
        SECTION .intvec:CODE:NOROOT(2)

        EXTERN  __iar_program_start
        EXTERN  SystemInit        
        PUBLIC  __vector_table

        DATA
__vector_table
        DCD     sfe(CSTACK)
        DCD     Reset_Handler             ; Reset Handler

        DCD     NMI_Handler               ; NMI Handler
        DCD     HardFault_Handler         ; Hard Fault Handler
        DCD     MemManage_Handler         ; MPU Fault Handler
        DCD     BusFault_Handler          ; Bus Fault Handler
        DCD     UsageFault_Handler        ; Usage Fault Handler
        
        <<Many More>>
        
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Default interrupt handlers.
;;
        THUMB

        PUBWEAK Reset_Handler
        SECTION .text:CODE:REORDER:NOROOT(2)
Reset_Handler
        LDR     R0, =SystemInit
        BLX     R0
        LDR     R0, =__iar_program_start
        BX      R0
        
        PUBWEAK NMI_Handler
        SECTION .text:CODE:REORDER:NOROOT(1)
NMI_Handler
        B NMI_Handler
        
		<<Many More>>
                
        END

这段程序实现了一个cstartup的模块,该模块分为三个部分,第一部分做全局声明以及数据的导入和导出,第二部分为DATA数据,定义了向量表,第三部分为Thumb指令,实现了各个中断处理函数。

第一部分,声明CSTACK段和.intvec段,各自的类型为DATA和CODE,EXTERN行表示导入外部函数,PUBLIC行表示其他文件可以使用该向量表。

第二部分,定义向量表__vector_table的内容,第一项SFE(CSTACK)表示CSTACK内存块的末尾地址,该项将赋给栈顶指针MSP,第二项Reset_Handler的实现部分在第三部分中被定义,后续各项分别对应着向量表的各个中断向量。DCD指令表示生成一个32-bit常数。

第三部分给出了向量表中各个中断处理函数的实现,其中Reset_Handler的实现如下:

Reset_Handler
        LDR     R0, =SystemInit
        BLX     R0
        LDR     R0, =__iar_program_start
        BX      R0

其中BLX和BX均表示跳转指令,这几行代码可以理解为先调用SystemInit函数,再调用__iar_program_start函数。

SystemInit函数是ST的库函数,它完成基本的时钟配置,同时设置中断向量表在Flash中的基地址。该地址可以通过IAR的Option -> Linker -> Config -> Edit设置窗口进行设置,如下所示:

MCU_Startup_intvec_Setting

也可以通过链接文件(.icf)进行手动设置。打开链接文件(stm32l152xe_flash.icf),找到相应的代码行:

define symbol __ICFEDIT_intvec_start__ = 0x08000000;

__iar_program_start函数是IAR平台自带的函数,它的源文件为C:\Program Files (x86)\IAR Systems\Embedded Workbench 7.5\arm\src\lib\thumb\cstartup_M.c,其内容如下:

void __iar_program_start( void )
{
  __iar_init_core();
  __iar_init_vfp();
  __cmain();
}

前两个函数被封装在C:\Program Files (x86)\IAR Systems\Embedded Workbench 7.5\arm\lib\rt7M_tl.a库文件中,无法得知其函数内容。

在Windows下可以使用命令行工具nm.exe查看.a库里面的符号列表,使用下列命令可以将符号列表输出到指定的文件中:

nm rt7M_tl.a >>symbol_list.txt

__cmain函数可以在C:\Program Files (x86)\IAR Systems\Embedded Workbench 7.5\arm\src\lib\thumb\cmain.s文件中找到源码,但是汇编代码不易理解,这里通过反汇编代码,查看跳转逻辑。

打开IAR调试界面,使用反汇编窗口,通过搜索定位到__iar_program_start函数,可以看到它的前两行是空操作,第三行跳转到?main函数。

MCU_Startup_Deassembly_Window

继续跟踪该函数,将其反汇编代码复制出来:

__cmain:
    0x8000328: 0xf000 0xf80b  BL        __low_level_init        ; 0x8000342
    0x800032c: 0x2800         CMP       R0, #0
    0x800032e: 0xd001         BEQ.N     _call_main              ; 0x8000334
    0x8000330: 0xf7ff 0xffdc  BL        __iar_data_init3        ; 0x80002ec

第一行__low_level_init函数总是返回常数1,第二行比较R0寄存器是否为0,第三行在R0 != 0时候执行,__call_main函数直接跳转到C语言的main函数,第四行在R0 == 0时执行__iar_data_init3函数。

__iar_data_init3函数也被封装在rt7M_tl.a中,通过单步追踪发现它调用了两个子函数:__iar_zero_init3和__ iar_copy_init3。

__iar_zero_init3给.bss段内的数据赋0,__ iar_copy_init3给.data段内的赋值。

这两个子函数能够在C:\Program Files (x86)\IAR Systems\Embedded Workbench 7.5\arm\src\lib\init目录下找到各自源码。

赋值结束后,跳转至_call_main函数,进而转到C语言的main函数。

相关参考

The Definitive Guide to the ARM® Cortex-M3

IAR Help File

Booting Process

cortex-m3启动代码详解

STM32 Main里做了什么

(完)