由于工作原因,已经许久没有接触飞控了。18年的时候曾经写过一个半成品开源飞控,是基于Keil MDK这款商业IDE开发的,只能在Windows下运行。由于笔者现在绝大部分时间都在使用Linux,偶尔想回顾一下以前的代码,竟不太方便,因此萌生了将原先的飞控项目移植到Linux下的想法。
目前国内单片机开发的教程绝大部分都是基于Keil的,Keil MDK作为一款商业IDE,的确存在它的优势:集成度高、上手快、编译器优化较好等。但是其劣势也很明显,首先这是款商业软件,需要付费,虽然绝大部分人所使用的是盗版,在国内个人使用不是什么大问题,但是如果是公司使用的话,极可能会收到起诉函(笔者工作后已经碰见过数起案例了)。其次最重要的是无法跨平台,只能在Windows下使用,这也是笔者想要替换它的最大原因。最后,Keil MDK的编辑器功能在2019年看来,已经算是古董级别了。。。
Linux平台下,并没有一款成熟好用的单片机IDE。笔者这里推荐的是较为热门的开源代码编辑器:VSCode,前面有几篇博文也均介绍了如何使用VSCode开发APM之类的开源飞控,感兴趣的可以去看一下,这里就不详细介绍了。
编译器使用gcc,这是一款支持多种编程语言的开源编译器。虽然对代码的优化程度有所不及Keil内置的armcc商业编译器,但也足够我们使用了,主流开源嵌入式项目基本全部使用gcc。
IDE的便利之处在于自动搞定了编译及链接规则,解放了双手。现在我们要在不依赖IDE的情况下,手动编写编译规则,大名鼎鼎的Makefile便是这样的工具。但很多时候手动编写Makefile较为繁琐,工作量很大。所以通常我们会使用更加高级的自动构建工具,比如CMake,同样类似的工具还有SCons、Waf(APM项目使用的工具)等等。这里笔者选择使用CMake,原因很多无需赘述(其实只是因为对CMake更熟点),非常多的开源C/C++项目都是基于CMake构建的。
CMake是一个跨平台的自动构建工具,使用简单的描述语句,就能自动生成Makefile或其它project文件(和Makefile同级别的还有ninja之类的)。由于VSCode、CMake及gcc编译工具链都可以跨平台,因此这套开发编译系统同样能在Windows下运行,MDK可以再见了。
gcc编译工具链可从下面链接下载,版本的话没有特殊要求,最新的便可以。
https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads
Linux下具体的安装方式可以参考前面一篇博文:使用VSCode打造APM飞控的编译+烧录+调试一体的终极开发环境。
安装完后在终端中输入
arm-none-eabi-gcc --version
如果正确输出版本信息表示已经安装成功。
首先到Github上把天穹飞控clone下来,然后在项目根目录下新建一个CMakeLists.txt。
由于网上资料丰富,这里就不仔细介绍CMake语法了,可自行百度或者谷歌,下面只会简单说明所使用到的CMake语句。
cmake_minimum_required(VERSION 3.10)
project(BlueSkyPilot)
这里指定了项目所需求的CMake最低版本,并定义了工程名。
由于飞控使用C语言编写,因此这里指定C编译器为arm-none-eabi-gcc,注意前面必须要正确安装了编译工具链,CMake才能识别。
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
下面还定义了几个需要用到的工具,其中arm-none-eabi-objcopy用于输出bin文件,arm-none-eabi-size用于查看文件占用flash和ram的大小。
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)
如果你使用过MDK开发STM32,可能会记得需要添加几个宏定义,这个主要是STM32固件库的需求,这里就照搬过来了。
add_definitions(
-DSTM32F40_41xxx
-DUSE_STDPERIPH_DRIVER
-DARM_MATH_CM4
)
其中STM32F40_41xxx指定了所使用的单片机型号(仅跟STM32固件库有关系)。
set(MCU_FLAGS "-mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16")
set(CMAKE_C_FLAGS "${MCU_FLAGS} -w -Wno-unknown-pragmas")
set(CMAKE_C_FLAGS_DEBUG "-O0 -g2 -ggdb")
set(CMAKE_C_FLAGS_RELEASE "-O3")
CMAKE_C_FLAGS表示编译C语言文件时需要附带的编译选项,其中:
而CMAKE_C_FLAGS_DEBUG和CMAKE_C_FLAGS_RELEASE分别是Debug模式和Release模式下需要附带的编译选项。默认情况下编译器会使用Debug模式(但是也不会调用CMakeLists.txt中的Debug编译选项),因此需要我们在CMakeLists.txt中或者运行CMake时指定使用哪种编译模式。
在CMakeLists中可通过如下语句设置编译模式:
set(CMAKE_BUILD_TYPE "Debug")
或
set(CMAKE_BUILD_TYPE "Release")
这两种模式区别在于,Debug模式不会对代码做任何优化,并可以生成汇编文件和Debug链接信息,这样才能使用gdb工具在线调试代码并设置断点。而Release模式可以设置优化级别,能够减小固件体积,并在一定程度上加快代码运行速度。
使用include_directories语句,设置头文件路径,这样CMake才能识别到代码中引用的头文件,如下:
include_directories(
STMLIB
STMLIB/inc
STMLIB/CORE
STMLIB/USB
STMLIB/USB/STM32_USB_Device_Library/Class/cdc/inc
STMLIB/USB/STM32_USB_Device_Library/Core/inc
STMLIB/USB/STM32_USB_OTG_Driver/inc
STMLIB/SDIO
FreeRTOS/Source/include
FreeRTOS/Source/portable/GCC/ARM_CM4F
FatFs
MAVLINK
MAVLINK/common
SRC/CONTROL
SRC/DRIVER
SRC/LOG
SRC/MATH
SRC/MESSAGE
SRC/MODULE
SRC/NAVIGATION
SRC/SENSOR
SRC/SYSTEM
SRC/TASK
${CMAKE_CURRENT_BINARY_DIR}
)
gcc编译器的最终输出有三种:静态库、动态库、可执行文件。其中静态库用于链接生成可执行文件,而动态库在程序运行时才会调用,而单片机上不存在什么动态链接机制,因此这个可以略过。
理论上可以一步直接链接所有C源文件,直接生成最终的可执行文件,但当源文件过多时,这样不便于管理,因此笔者选择将一些耦合性较低的单元生成静态库,最终再链接成可执行文件,主要使用到了add_library这个语句。
比如生成FreeRTOS库:
add_library(freertos
FreeRTOS/Source/tasks.c
FreeRTOS/Source/list.c
FreeRTOS/Source/queue.c
FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c
FreeRTOS/Source/portable/MemMang/heap_4.c
)
其中FreeRTOS中的port.c是针对不同编译器的移植文件,需要选择相对应的版本。
生成STM32固件库:
add_library(stm32_lib
STMLIB/startup_stm32f40_41xxx.s
STMLIB/src/misc.c
STMLIB/src/stm32f4xx_adc.c
STMLIB/src/stm32f4xx_can.c
STMLIB/src/stm32f4xx_dma.c
STMLIB/src/stm32f4xx_flash.c
STMLIB/src/stm32f4xx_rcc.c
STMLIB/src/stm32f4xx_gpio.c
STMLIB/src/stm32f4xx_tim.c
STMLIB/src/stm32f4xx_spi.c
STMLIB/src/stm32f4xx_pwr.c
STMLIB/src/stm32f4xx_sdio.c
STMLIB/src/stm32f4xx_usart.c
STMLIB/src/stm32f4xx_syscfg.c
STMLIB/system_stm32f4xx.c
STMLIB/USB/usb_bsp.c
STMLIB/USB/usbd_desc.c
STMLIB/USB/STM32_USB_Device_Library/Class/cdc/src/usbd_cdc_core.c
STMLIB/USB/STM32_USB_Device_Library/Core/src/usbd_ioreq.c
STMLIB/USB/STM32_USB_Device_Library/Core/src/usbd_req.c
STMLIB/USB/STM32_USB_Device_Library/Core/src/usbd_core.c
STMLIB/USB/STM32_USB_OTG_Driver/src/usb_core.c
STMLIB/USB/STM32_USB_OTG_Driver/src/usb_dcd_int.c
STMLIB/USB/STM32_USB_OTG_Driver/src/usb_dcd.c
)
这里值得注意的是,由于启动文件startup_stm32f40_41xxx.s使用的是汇编语言,默认情况下会直接被CMake忽略,需要设置该文件属性为C语言,即使用C编译器去编译该汇编文件:
set_property(SOURCE STMLIB/startup_stm32f40_41xxx.s PROPERTY LANGUAGE C)
这里笔者曾经试过使能CMake识别汇编文件,并设置汇编编译器,但编译出错,原因还要待后人来解答。
另外STM32的启动文件是分编译器的,在官方库中能够找到针对不同编译器编写的启动文件,注意区分。
上面示例的两个静态库都能够单独编译,不依赖外部文件,若是库中使用了外部函数,则需要使用target_link_libraries来链接外部库,比如编译文件系统库FatFs时,需要链接前面编译好的STM32固件库:
add_library(fatfs
FatFs/diskio.c
FatFs/ff.c
FatFs/option/ccsbcs.c
STMLIB/SDIO/stm32f4_sdio_sd_LowLevel.c
STMLIB/SDIO/stm32f4_sdio_sd.c
)
target_link_libraries(fatfs
stm32_lib
)
上面编译静态库时都是直接指定源文件路径,当代码文件较多时,这样操作并不太方便,可以使用file获取某目录下的文件列表:
file(GLOB SRC_DRIVER SRC/DRIVER/*.c)
file(GLOB SRC_CONTROL SRC/CONTROL/*.c)
file(GLOB SRC_LOG SRC/LOG/*.c)
file(GLOB SRC_MATH SRC/MATH/*.c)
file(GLOB SRC_MESSAGE SRC/MESSAGE/*.c)
file(GLOB SRC_MODULE SRC/MODULE/*.c)
file(GLOB SRC_NAVIGATION SRC/NAVIGATION/*.c)
file(GLOB SRC_SENSOR SRC/SENSOR/*.c)
file(GLOB SRC_SYSTEM SRC/SYSTEM/*.c)
file(GLOB SRC_TASK SRC/TASK/*.c)
使用上述文件列表编译飞控主体代码
add_library(${PROJECT_NAME}
${SRC_DRIVER}
${SRC_CONTROL}
${SRC_LOG}
${SRC_MATH}
${SRC_MESSAGE}
${SRC_MODULE}
${SRC_NAVIGATION}
${SRC_SENSOR}
${SRC_SYSTEM}
${SRC_TASK}
)
target_link_libraries(${PROJECT_NAME} -lm
stm32_lib
fatfs
freertos
)
这里除了要链接上面编译的所有静态库之外,使用-lm来单独链接math库。
编译STM32程序时,需要链接ld文件,主要作用在于告诉编译器单片机的Flash和RAM大小以及地址分布,这个文件在ST提供的STM32固件库中可以找到。
set(LINKER_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/STMLIB/STM32F405RGTx_FLASH.ld)
set(CMAKE_EXE_LINKER_FLAGS
"--specs=nano.specs -specs=nosys.specs -nostartfiles -T${LINKER_SCRIPT} -Wl,-Map=${PROJECT_BINARY_DIR}/${PROJECT_NAME}.map,--cref -Wl,--gc-sections"
)
链接主程序入口文件main.c和前面生成的静态库,得到.elf后缀的可执行文件:
add_executable(${PROJECT_NAME}.elf SRC/main.c)
target_link_libraries(${PROJECT_NAME}.elf
${PROJECT_NAME}
)
某些时候可能需要用到hex或者bin格式的文件,可以使用arm-none-eabi-objcopy工具来生成。
首先设置文件路径
set(ELF_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.elf)
set(HEX_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.hex)
set(BIN_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.bin)
生成hex和bin文件并输出显示文件大小
add_custom_command(TARGET "${PROJECT_NAME}.elf" POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -Obinary ${ELF_FILE} ${BIN_FILE}
COMMAND ${CMAKE_OBJCOPY} -Oihex ${ELF_FILE} ${HEX_FILE}
COMMENT "Building ${PROJECT_NAME}.bin and ${PROJECT_NAME}.hex"
COMMAND ${CMAKE_COMMAND} -E copy ${HEX_FILE} "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.hex"
COMMAND ${CMAKE_COMMAND} -E copy ${BIN_FILE} "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.bin"
COMMAND ${CMAKE_SIZE} --format=berkeley ${PROJECT_NAME}.elf ${PROJECT_NAME}.hex
COMMENT "Invoking: Cross ARM GNU Print Size"
)
至此,CMakeLists.txt就编写完毕了,可以开始编译飞控代码。
首先确认已经安装了CMake,通过cmake --version
查看当前CMake版本。
在飞控项目根目录下建立build文件夹
mkdir build
进入build文件夹,运行CMake生成Makefile文件
cd build
cmake ..
也可以附带编译模式选项,选择是编译Debug还是Release版本
cmake -DCMAKE_BUILD_TYPE=Debug ..
Makefile文件构建完毕后,使用make命令编译代码:
make -j
加上-j选项使用多线程编译,实测在笔者的笔记本电脑上只花费了4秒,速度还算可以。
出现如下输出便表示编译成功了!
可以看到输出的bin文件大小为247K,如果编译Release版本的话,大概只有200K左右。
Ninja是一个更加轻量级的编译构建系统,速度比make更快,我们可以利用CMake生成Ninja文件,代替make,加快编译速度。
sudo apt-get install ninja-build
cmake -G Ninja ..
使用OpenOCD,配合VSCode的Cortex-Debug插件,便能够实现F5一键烧录+在线调试,可以参考笔者前面写的一篇博文:使用VSCode打造APM飞控的编译+烧录+调试一体的终极开发环境 ,此处就不再赘述了,实际效果如下图:
附上天穹飞控的Cortex-Debug配置:
{
"version": "0.2.0",
"configurations": [
{
"name": "Cortex Debug",
"cwd": "${workspaceRoot}",
"executable": "./build/BlueSkyPilot.elf",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"configFiles": [
"interface/jlink.cfg",
"target/stm32f4x.cfg"
]
}
]
}
gcc编译器不支持原先在MDK中使用的__nop()语句,需要额外加上一个宏定义:
#define __nop() asm("nop")
可移步Github查看飞控项目最新可用的CMakeLists.txt:
https://github.com/loveuav/BlueSkyFlightControl
cmake_minimum_required(VERSION 3.10)
project(BlueSkyPilot)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)
add_definitions(
-DSTM32F40_41xxx
-DUSE_STDPERIPH_DRIVER
-DARM_MATH_CM4
)
#set(CMAKE_BUILD_TYPE "Debug")
#set(CMAKE_BUILD_TYPE "Release")
set(MCU_FLAGS "-mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16")
set(CMAKE_C_FLAGS "${MCU_FLAGS} -w -Wno-unknown-pragmas")
set(CMAKE_C_FLAGS_DEBUG "-O0 -g2 -ggdb")
set(CMAKE_C_FLAGS_RELEASE "-O3")
include_directories(
STMLIB
STMLIB/inc
STMLIB/CORE
STMLIB/USB
STMLIB/USB/STM32_USB_Device_Library/Class/cdc/inc
STMLIB/USB/STM32_USB_Device_Library/Core/inc
STMLIB/USB/STM32_USB_OTG_Driver/inc
STMLIB/SDIO
FreeRTOS/Source/include
FreeRTOS/Source/portable/GCC/ARM_CM4F
FatFs
MAVLINK
MAVLINK/common
SRC/CONTROL
SRC/DRIVER
SRC/LOG
SRC/MATH
SRC/MESSAGE
SRC/MODULE
SRC/NAVIGATION
SRC/SENSOR
SRC/SYSTEM
SRC/TASK
${CMAKE_CURRENT_BINARY_DIR}
)
set_property(SOURCE STMLIB/startup_stm32f40_41xxx.s PROPERTY LANGUAGE C)
add_library(stm32_lib
STMLIB/startup_stm32f40_41xxx.s
STMLIB/src/misc.c
STMLIB/src/stm32f4xx_adc.c
STMLIB/src/stm32f4xx_can.c
STMLIB/src/stm32f4xx_dma.c
STMLIB/src/stm32f4xx_flash.c
STMLIB/src/stm32f4xx_rcc.c
STMLIB/src/stm32f4xx_gpio.c
STMLIB/src/stm32f4xx_tim.c
STMLIB/src/stm32f4xx_spi.c
STMLIB/src/stm32f4xx_pwr.c
STMLIB/src/stm32f4xx_sdio.c
STMLIB/src/stm32f4xx_usart.c
STMLIB/src/stm32f4xx_syscfg.c
STMLIB/system_stm32f4xx.c
STMLIB/USB/usb_bsp.c
STMLIB/USB/usbd_desc.c
STMLIB/USB/STM32_USB_Device_Library/Class/cdc/src/usbd_cdc_core.c
STMLIB/USB/STM32_USB_Device_Library/Core/src/usbd_ioreq.c
STMLIB/USB/STM32_USB_Device_Library/Core/src/usbd_req.c
STMLIB/USB/STM32_USB_Device_Library/Core/src/usbd_core.c
STMLIB/USB/STM32_USB_OTG_Driver/src/usb_core.c
STMLIB/USB/STM32_USB_OTG_Driver/src/usb_dcd_int.c
STMLIB/USB/STM32_USB_OTG_Driver/src/usb_dcd.c
)
add_library(freertos
FreeRTOS/Source/tasks.c
FreeRTOS/Source/list.c
FreeRTOS/Source/queue.c
FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c
FreeRTOS/Source/portable/MemMang/heap_4.c
)
add_library(fatfs
FatFs/diskio.c
FatFs/ff.c
FatFs/option/ccsbcs.c
STMLIB/SDIO/stm32f4_sdio_sd_LowLevel.c
STMLIB/SDIO/stm32f4_sdio_sd.c
)
target_link_libraries(fatfs
stm32_lib
)
file(GLOB SRC_DRIVER SRC/DRIVER/*.c)
file(GLOB SRC_CONTROL SRC/CONTROL/*.c)
file(GLOB SRC_LOG SRC/LOG/*.c)
file(GLOB SRC_MATH SRC/MATH/*.c)
file(GLOB SRC_MESSAGE SRC/MESSAGE/*.c)
file(GLOB SRC_MODULE SRC/MODULE/*.c)
file(GLOB SRC_NAVIGATION SRC/NAVIGATION/*.c)
file(GLOB SRC_SENSOR SRC/SENSOR/*.c)
file(GLOB SRC_SYSTEM SRC/SYSTEM/*.c)
file(GLOB SRC_TASK SRC/TASK/*.c)
add_library(${PROJECT_NAME}
${SRC_DRIVER}
${SRC_CONTROL}
${SRC_LOG}
${SRC_MATH}
${SRC_MESSAGE}
${SRC_MODULE}
${SRC_NAVIGATION}
${SRC_SENSOR}
${SRC_SYSTEM}
${SRC_TASK}
)
target_link_libraries(${PROJECT_NAME} -lm
stm32_lib
fatfs
freertos
)
set(LINKER_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/STMLIB/STM32F405RGTx_FLASH.ld)
set(CMAKE_EXE_LINKER_FLAGS
"--specs=nano.specs -specs=nosys.specs -nostartfiles -T${LINKER_SCRIPT} -Wl,-Map=${PROJECT_BINARY_DIR}/${PROJECT_NAME}.map,--cref -Wl,--gc-sections"
)
add_executable(${PROJECT_NAME}.elf SRC/main.c)
target_link_libraries(${PROJECT_NAME}.elf
${PROJECT_NAME}
)
set(ELF_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.elf)
set(HEX_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.hex)
set(BIN_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.bin)
add_custom_command(TARGET "${PROJECT_NAME}.elf" POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -Obinary ${ELF_FILE} ${BIN_FILE}
COMMAND ${CMAKE_OBJCOPY} -Oihex ${ELF_FILE} ${HEX_FILE}
COMMENT "Building ${PROJECT_NAME}.bin and ${PROJECT_NAME}.hex"
COMMAND ${CMAKE_COMMAND} -E copy ${HEX_FILE} "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.hex"
COMMAND ${CMAKE_COMMAND} -E copy ${BIN_FILE} "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.bin"
COMMAND ${CMAKE_SIZE} --format=berkeley ${PROJECT_NAME}.elf ${PROJECT_NAME}.hex
COMMENT "Invoking: Cross ARM GNU Print Size"
)