请添加图片描述

通过Clion进行嵌入式开发

一、工具安装

1、安装Clion

因为众所周知的原因,Clion的安装就不解释了,有需要的同学自行检索

2、安装STM32CubeMX

正常去官网下载最新版的安装就行了:STM32CubeMX - STM32Cube initialization code generator - STMicroelectronics

3、安装OpenOCD

OpenOCD是用于对STM32进行下载仿真的工具,是一个开源软件包,Windows版本下从这里下载,下载好解压到一个目录就行(建议不要有空格,不要有中文),后面会在Clion中链接OpenOCD和CubeMX。

4、在Clion中链接两个工具包

接下来我们进行链接,Clion设置如图所示

image-20230320223651243

二、安装编译器

嵌入式开发大多数情况下使用C/C++,故我们需要C语言编译器,一般需要gccarm-gcc,我们选择MinGWarm-none-eabi-gcc

1、安装MinGW,通过MinGW安装gcc

在最新版的Clion中,已经内置了MinGW环境,所以理论上可以选择不安装,这里还是附上安装教程,供有需要的同学

安装 MinGW 的最简单方法是通过 mingw-get,它是一个图形用户界面 (GUI) 应用,可帮助你选择要安装哪些组件,并让它们保持最新。要运行它,请从项目主页下载 mingw-get-setup.exe

image-20230320234936380

打开EXE,修改安装目录(最好不要有空格),目前为止,你只安装了一个程序,更准确地说,这是一个称为 mingw-get 的专用的包管理器。启动 mingw-get 选择要在计算机上安装的 MinGW项目应用。

image-20230320235922383

安装完成后进行组件下载,把Basic Setup里面的组件全部勾选(也可也去掉不需要的语言编译器比如Objective-C)。

配置系统的环境变量,在Path环境变量里面添加一条,指向MinGW的bin文件夹:

image-20230321000430603

随后打开终端(或者cmd),如果已经打开,可以尝试关闭再打开

输入gcc -v,若得到如下输出,说明配置成功,若仍然有问题,可以尝试注销或者重启电脑

C:\Users\Striver>gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=E:/Program\ Files/JetBrains/CLion/bin/mingw/bin/../libexec/gcc/x86_64-w64-mingw32/11.2.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../gcc-11.2.0/configure --host=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 --build=x86_64-alpine-linux-musl --prefix=/win --enable-checking=release --enable-fully-dynamic-string --enable-languages=c,c++ --enable-libatomic --enable-libgomp --enable-libstdcxx-filesystem-ts=yes --enable-libstdcxx-time=yes --enable-seh-exceptions --enable-shared --enable-static --enable-threads=posix --enable-version-specific-runtime-libs --disable-bootstrap --disable-graphite --disable-libada --disable-libstdcxx-pch --disable-libstdcxx-debug --disable-libquadmath --disable-lto --disable-nls --disable-multilib --disable-rpath --disable-symvers --disable-werror --disable-win32-registry --with-gnu-as --with-gnu-ld --with-system-libiconv --with-system-libz --with-gmp=/win/makedepends --with-mpfr=/win/makedepends --with-mpc=/win/makedepends
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 11.2.0 (GCC)

2、安装arm-none-eabi-gcc

Windows到这里下载:Downloads | GNU Arm Embedded Toolchain Downloads – Arm Developer

下载zip包

image-20230320231634203

同理,解压到你指定的路径(建议不要有空格,不要有中文),随后配置环境变量,指向对应的bin目录

image-20230321001319009

同理,通过再命令行输入arm-none-eabi-gcc -v进行测试,输出如下,则成功

C:\Users\Striver>arm-none-eabi-gcc -v
Using built-in specs.
COLLECT_GCC=arm-none-eabi-gcc
COLLECT_LTO_WRAPPER=e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/lto-wrapper.exe
Target: arm-none-eabi
Configured with: /mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/src/gcc/configure --build=x86_64-linux-gnu --host=i686-w64-mingw32 --target=arm-none-eabi --prefix=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/install-mingw --libexecdir=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/install-mingw/lib --infodir=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/install-mingw/share/doc/gcc-arm-none-eabi/info --mandir=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/install-mingw/share/doc/gcc-arm-none-eabi/man --htmldir=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/install-mingw/share/doc/gcc-arm-none-eabi/html --pdfdir=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/install-mingw/share/doc/gcc-arm-none-eabi/pdf --enable-languages=c,c++ --enable-mingw-wildcard --disable-decimal-float --disable-libffi --disable-libgomp --disable-libmudflap --disable-libquadmath --disable-libssp --disable-libstdcxx-pch --disable-nls --disable-shared --disable-threads --disable-tls --with-gnu-as --with-gnu-ld --with-headers=yes --with-newlib --with-python-dir=share/gcc-arm-none-eabi --with-sysroot=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/install-mingw/arm-none-eabi --with-libiconv-prefix=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/build-mingw/host-libs/usr --with-gmp=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/build-mingw/host-libs/usr --with-mpfr=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/build-mingw/host-libs/usr --with-mpc=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/build-mingw/host-libs/usr --with-isl=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/build-mingw/host-libs/usr --with-libelf=/mnt/workspace/workspace/GCC-10-pipeline/jenkins-GCC-10-pipeline-338_20211018_1634516203/build-mingw/host-libs/usr --with-host-libstdcxx='-static-libgcc -Wl,-Bstatic,-lstdc++,-Bdynamic -lm' --with-pkgversion='GNU Arm Embedded Toolchain 10.3-2021.10' --with-multilib-list=rmprofile,aprofile
Thread model: single
Supported LTO compression algorithms: zlib
gcc version 10.3.1 20210824 (release) (GNU Arm Embedded Toolchain 10.3-2021.10)

3、在Clion中配置工具链

打开File->Settings->Build,Execution,Devlopment->ToolChains,输入MinGW的路径,Clion会自动识别,识别慢的话直接输入即可

建议自己新建一个环境,避免默认环境以后做C语言开发出现问题

image-20230321003239709

注意Debugger不要改,否则断点调试的时候无法连接。

BuildTool最新版Clion默认集成了MinGW,故Toolset部分理论上也可以不做修改,最新版Clion默认使用ninja,理论上应该也可以,如需使用make,修改路径即可。

顺便介绍下ninja,在Unix/Linux下通常使用Makefile来控制代码的编译,但是Makefile对于比较大的项目有时候会比较慢,看看上面那副漫画,代码在编译都变成了程序员放松的借口了。所以这个Google的程序员在开发Chrome的时候因为忍受不了Makefile的速度,自己重新开发出来一套新的控制编译的工具叫作Ninja,Ninja相对于Makefile这套工具更注重于编译速度。除了Chrome现在还有一些其他的比较大的项目也在开始使用Ninja,比如LLVM。

然后再CMake栏下确认一下工具链是否正确

image-20230321003611993

至此Clion环境配置完成,可以创建STM32项目了。

三、通过Clion进行标准库开发

1、创建CubeMX工程

image-20230321093430741

最新版Clion第一次创建完项目,首先会启动CubeMX,提示要下载固件包,下载即可(忘截图了),随后会弹出板卡选择窗口:

image-20230321093100884

这些配置文件是跟OpenOCD下载程序有关的,里面的板子很可能是没有我们自己要用的型号的,后面会介绍怎么自己建立这个配置文件,这里先点取消。

随后会生成一个ioc文件,这个文件跟使用STM32CubeMX直接创建的是一样的,点击图中的链接可以跳转到STM32CubeMX中打开这个ioc文件:

image-20230321093625854

先选择芯片型号,默认芯片型号是STM32F030F4Px,点击更改成你自己的芯片型号,CubeMX会根据对应的芯片生成对应的启动文件,到时候工程就使用它生成的启动文件。

image-20230321094038292 image-20230321094436986

由于我们不使用Hal库,所以不对芯片做任何配置,直接点击Project Manager

image-20230321183553678

要注意一下,就是在下面的设置中项目名称和路径一定要和在Clion中建立的一致,这样生成的工程文件才会覆盖Clion中的文件,否则会另外生成一个文件夹,Clion就无法读取了。

网上有教程选择的是SW4STM32,但是我们发现没有这个选项,根据网上的很多人都会要求把CubeMX降低到某个版本以下,但是一直使用低版本肯定不是解决问题的方法。其实在CLion文档里面就有解决方法。STM32CubeMX项目 |CLion 文档 (jetbrains.com)

Toolchain/IDE选择STM32CubeMX即可,右侧的勾,勾上

随后点击Generate Code

这里可以注意一下堆栈大小,一般0x200~0x400够用

这部分与Keil中启动文件内的Stack_SizeHeap_Size段码对应

可能会提示缺少固件包,按提示下载即可(此部分未截图)

image-20230321102739082

随后按提示覆盖Clion默认创建的工程

image-20230321183930883 image-20230321184020146

等代码生成完成,我们直接关闭,并关闭CubeMX,回到Clion

image-20230321184055547

可以看到,已经重新生成了相关的HAL代码,并设置了对应的型号。

image-20230321184200267

可能会再次弹出板卡选择窗口,我们先跳过(大概率没有我们想要的),后面会进行配置

image-20230321093100884

由于我们基于标准库进行开发,故需要进行调整

2、个人习惯建议

为了避免生成的代码main.c内太乱,建议勾上Generate peripheral initialization as a pair of’.c/.h’ files per peripheral ,让每个外设生成独立的.c/.h文件

image-20230321190540741

不勾:所有初始化代码都生成在main.c

勾选:初始化代码生成在对应的外设文件。 如GPIO初始化代码生成在gpio.c中。

3、标准库的移植

由于我们不使用HAL库,所以先把他生成的Drivers、Core文件删除,然后从Keil标准库模板工程中导入这些文件,笔者这里以野火点灯程序为例

image-20230321190940843

将野火的相关文件复制进Clion项目内

image-20230321191029832

复制后如图

image-20230321191650907

删掉不需要的文件(Keil生成的相关文件)

image-20230321214638124

由于野火官方的库归类还不错,故按此进行简单整理,主要目的是将.h归入Include,将.c归入Source文件夹,或者IncSrc也行,看个人习惯,便于后面修改Cmake文件,笔者简单整理后如图(轻喷,也不算太标准)。

image-20230321220555128

最后,将CubeMX生成的启动文件复制进来,并重命名覆盖,可以避免一些问题

image-20230321232424366

后知后觉,如果不想覆盖的话,直接删除原来的启动文件,也可以刷新CMake,这样,后面的编译也可以通过,如图:

image-20230322100448572

习惯建议:在项目文件夹中添加Startup文件夹,将启动文件放到里面,并修改CMakeList,将这个文件夹路径放进file里面,这也是后知后觉的,同样,通过刷新CMake,也可以通过编译,这部分后知后觉的补充内容可以先跳过,看完后面的部分再回来更容易理解

4、修改CMakeList

CLion中组织编译规则都是基于CMakeLists.txt文件的,如果熟悉CMake应该会觉得很方便很强大,不熟悉的也没事,基本不需要额外修改什么,只需要知道怎么在这个文件里面添加源码目录和include文件夹的路径就行了:

include_directories(
        Core/Inc
        UserApp
// 其他include目录
)


file(GLOB_RECURSE SOURCES
        "startup/*.*"
        "Drivers/*.*"
        "Core/*.*"
        "UserApp/*.*"
        "3rdParty/*.*"
// *.*表示通配符,也就是这个文件夹里的所有文件都会被编译
        )

笔者修改后如图,若修改后编译还是不通过,首先考虑这里添加的路径是否够全,特别是file中添加的需要包含IncludeSource,且include_directories要将所有包含.h文件的目录都添加进去,此外,考虑路径是否正确。

image-20230321235006162

做个小结:

  1. 需要用到的头文件,都要把路径写到cmake文件里面,编译器才能识别到。
  2. 把相关源文件和启动文件链接到编译器。使用GLOB命令使用通配符模式匹配来查找文件。file(GLOB SOURCES "src/*.*")使用这个通配符,表示所有.*结尾的文件都会包含到这个SOURCES变量。

5、编译工程

编译工程,每个项目都需要先设置CMake

在编译前,我们需要打开stm32f4xx.h,在里面加入两句话,这里的hi两个宏定义,一个代表标准库,一个是芯片型号

#define STM32F429_439xx
#define USE_STDPERIPH_DRIVER

这两个宏定义在Keil里是在此处定义的,所以移植到Clion我们要手动加上

Snipaste_2023-03-21_23-58-49

否则编译会出现如下错误:
错误信息

添加后如下图所示:

image-20230322000238558

后面可能会出现**No such file or directory,见招拆招,哪个地方定义有问题,就修改路径或者修改CMake文件

提供另外一种做法,如果不想修改源文件,也可以在CMakeList中添加宏定义

add_definitions(-DDEBUG -DUSE_STDPERIPH_DRIVER -DSTM32F429_439xx)

如图所示,上面注释掉的,是默认CubeMX生成的HAL库宏定义,我们在下面添加宏定义的作用就类似Keil中的设置:

image-20230322103026052

注:两者二选一即可,根据个人习惯来,若两边都做修改,就会有重新定义警告

顺便提一嘴,针对标准库:

  • STM32F429_439xx 宏:为了告诉 STM32 标准库,我们使用的芯片是 STM32F429 型号,使 STM32 标准库根据我们选定的芯片型号来配置。
  • USE_STDPERIPH_DRIVER 宏:为了让 stm32f4xx.h 包含 stm32f4xx_conf.h 这个头文件。

针对HAL库:

  • STM32F429xx 宏:为了告诉 STM32 HAL 库,我们使用的芯片是 STM32F429 型号,使 STM32 HAL 库根据我们选定的芯片型号来配置。
  • USE_HAL_DRIVER 宏:为了让 stm32F429xx.h 包含 stm32f4xx_hal_conf.h 这个头文件。

接下来的问题不同的人可能不一样,但是我们解决问题的思路基本是一样的,这里笔者进行记录

笔者接下来遇到的报错都是和FSMC相关的,如下:

image-20230322002046335

这是由于笔者使用的STM32F429,标准库中需要使用的是FMC,Clion也很聪明,甚至能猜出来你可能需要使用其他定义(虽然不完全对,至少排除错误做参考足够了),故理论上删除掉不支持的FSMC相关外设头文件和源文件stm32f4xx_fsmc.cstm32f4xx_fsmc.h即可通过编译,在Keil中对应的的做法则是通过右击文件Options for File来取消该文件参与编译:

Snipaste_2023-03-22_10-07-33

提一嘴FSMC和FMC:

  1. F1和F407使用的是FSMC(Flexible static memory controller),跟F429和H7带的FMC区别是不支持SDRAM,也就是差在字母static,使用FMC可以动态刷新SDRAM,来保持电量。
  2. FMC控制SRAM型存储器和NAND型存储器是异步控制,而控制SDRAM属于同步控制。同步和异步的区别是同步方式需要一个专门的时钟控制引脚。
  3. FMC配置中未用到引脚均可以继续用作通用I/O模式或者其它复用功能,仅需不配置FMC复用即可。

若删除后提示CMake缺少我们刚才删除的文件,无法通过编译,就刷新一下Cmake,再重新编译,如图:

image-20230322100448572

最后编译成功!

image-20230322103824178

有时候重新打开项目可能会出现编译图标(锤子)灰色的问题,这里给出一种解决方案,即打开CMakeList文件,随后重新加载CMake项目,可以解决,最简单粗暴的办法就是修改CMakeList中的add_executable字段

add_executable(${PROJECT_NAME}.elf ${SOURCES} ${LINKER_SCRIPT})
# 改为如下
add_executable(你的项目名称.elf ${SOURCES} ${LINKER_SCRIPT})

随后重新加载

6、烧录程序

Keil里面我们烧录程序的时候要指定使用的下载器(J-Link、ST-Link、CMSIS-DAP等),Clion烧录程序之前通用需要进行一些设置。

点击编译按钮旁边的配置栏下拉,选Edit Configurations,添加一个OpenOCD下载配置,打开配置窗口:

image-20230322131725347

可以看到没有设置板子的config文件所以出现警告错误,这个配置文件就是前面说的需要自己生成的文件。

我们在工程根目录下新建一个文件夹Config,在里面新建一个配置文件daplink.cfg(因为我这里使用的是野火fireDap作为仿真器),文件的内容如下:

# choose st-link/j-link/dap-link etc.
# choose CMSIS-DAP Debugger
adapter driver cmsis-dap
# select SWD port
transport select swd

# 0x10000 = 64K Flash Size
# 1MB on FireDebugger
set FLASH_SIZE 0x100000

source [find target/stm32f4x.cfg]

# download speed = 5MHz
# 5MHz on FireDebugger
adapter speed 5000

# connect under reset
reset_config srst_only

如果是用ST-Link的话:

# choose st-link/j-link/dap-link etc.
#adapter driver cmsis-dap
#transport select swd
source [find interface/stlink.cfg]
transport select hla_swd
source [find target/stm32f4x.cfg]
# download speed = 10MHz
adapter speed 10000

前两行设置了仿真器的类型和接口,下面几行指定了Flash大小芯片类型下载速度等,具体根据参数根据手册或者开发板资料来确定,我这里是以野火标准版DAP为例。

大概就是,引用了 OpenOCD 自带的配置,然后设置速度等,存起来后链接到 Board config file 即可。

如果对自己的芯片不知道怎么设置,可以参考OpenOCD自带的一系列配置文件,路径在OpenOCD安装目录的share\openocd\scripts下:

image-20230322132042361

只需要关注这几个目录:

  • board:板卡配置,各种官方板卡
  • interface:仿真器类型配置,比如ST-Link、CMSIS-DAP等都在里面
  • target:芯片类型配置,STM32F1xx、STM32L0XX等等都在里面

设置好配置文件之后,就可以点击下载或者调试按钮进行下载和在线调试了。

有时候,在配置文件中不要加reset_config srst_only这一句,会导致下载失败,这一句是指示系统重启的,删除不影响下载。

而有时候,又需要加这句,否则会无法下载,例如笔者使用的野火Fire-Dap,就需要加上这句,作用类似于Connect: under reset,否则有些情况下会出现下载失败的情况,如图:

Snipaste_2023-04-02_11-14-51

大家可以通过检索错误信息,灵活处理

随后我们下载,成功点亮LED!(这里移植的是野火板子的标准库点亮LED例程)

image-20230322135116961

7、调试程序

CLion里面是支持全功能的单步断点调试的,也能在代码里直接观察变量的值,非常舒服~

image-20230322135740477

若需要查看寄存器的值,则需要导入SVD文件,想要获取SVD文件,进入Search - STMicroelectronics,搜索自己的芯片系列即可找到压缩包:

image-20230403171427346

在压缩包中,找到符合我们芯片型号的文件,解压到适当位置,随后打开

image-20230403171551517 image-20230403171658937

选中我们要观察的寄存器即可,为了方便,我这里直接全选了,大家根据实际需要来即可

image-20230403171834441

随后我们就可以很方便查看寄存器的值了,比如笔者这里的GPIOH模式为输出模式,即01b

image-20230403172119238

8、解决串口printf()问题

这里还是以LED为例(就在LED的基础上加了个串口)

先提一嘴,以下内容,包含笔者的踩坑记录,读者们根据自己的需求食用,若不想看这段“废话”,请直接跳转到解决方案部分。

在Keil中开发的时候,为了使用printf(),我们会将其重定向到串口,具体实现是通过重定义stdio.h中的fputc(),并顺便重定向了scanf(),这部分同理,可通过重定义fgetc()来实现。

野火的代码如下:

/**
 * @brief 重定向c库函数printf到串口,重定向后可使用printf函数
 * @param ch 输入的字符
 * @param f 要写入的文件指定文件指针,交给 stdio 模块处理
 * @return 发送的字符,,交给 stdio 模块处理
 * @attention Keil中需要勾选 use MicroLIB,否则不能正常重定向
 * @attention 这里的 USARTx 可以根据需要来修改,一般都用 USART1
 */
int fputc(int ch, FILE *f) {

    /* 发送一个字节数据到串口 */
    /* 很简单,直接调用库函数中的 串口发送数据函数 */
    USART_SendData(USARTx, (uint8_t) ch);

    /*等待发送完毕*/
    while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);
    /* 返回发送的字符 */
    return ch;
}

/**
 * @brief 重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
 * @param f 要写入的文件指定文件指针,交给 stdio 模块处理
 * @return 接收到的字符,交给 stdio 模块处理
 * @attention Keil中需要勾选 use MicroLIB,否则不能正常重定向
 * @attention 这里的 USARTx 可以根据需要来修改,一般都用 USART1
 */
int fgetc(FILE *f) {

    /* 很简单,直接调用库函数中的接收 */
    /* 等待串口输入数据 */
    while (USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) == RESET);
    /* 返回接收到的字符 */
    return (int) USART_ReceiveData(USARTx);

}

注意:需要包含头文件#include <stdio.h>

原子的代码也同理,不过好像没有重定向scanf(),这里我也加上去:

/**
 * @brief 加入以下代码,支持 printf 函数,而不需要选择 use MicroLIB
 */
#if 1
/* 告知连接器不从C库链接使用半主机的函数 */
#pragma import(__use_no_semihosting)
/** 标准库需要的支持函数 */
struct __FILE {
    int handle;
    /* Whatever you require here. If the only file you are using is */
    /* standard output using printf() for debugging, no file handling */
    /* is required. */
};
/* FILE is typedef ’ d in stdio.h. */
FILE __stdout;

/** 定义_sys_exit()以避免使用半主机模式 */
_sys_exit(int x) {
    x = x;
}

/** 重定义 fputc 函数 */
int fputc(int ch, FILE *f) {
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    USART_SendData(USART1, (uint8_t) ch);
    return ch;
}
/** 重定义 fgetc 函数 */
/* 这部分是我自己加的,原子本来没有 */
int fgetc(FILE *f) {
    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
    return (int) USART_ReceiveData(USART1);
}

#endif

注意:需要包含头文件#include <stdio.h>

两者的代码整体上看起来都差不多,区别在于是否使用了MicroLib,野火的因为使用了微库,不会使用半主机模式,故代码比较简洁,原子则不需要依赖微库,故重写的代码多了一点,多出的部分就是用于避免半主机的。

这里提一嘴微库和半主机模式:

Keil官方对于微库的介绍如下:

MicroLib 是一个高度优化的库,适用于基于 ARM 的嵌入式应用程序 用 C 语言编写。与 ARM编译器工具链,MicroLib提供了显着的代码大小优势,对于许多嵌入式系统来说,微库是很有有必要的。

MicroLib和标准C库之间的主要区别是:

  • MicroLib专为深度嵌入式应用程序而设计。
  • MicroLib经过优化,与使用ARM标准库相比,使用的代码和数据存储器更少。
  • MicroLib被设计为在没有操作系统的情况下工作,但这并不妨碍它与任何操作系统或RTOS(如Keil RTX)一起使用。
  • MicroLib不包含文件I / O或宽字符支持。
  • 由于 MicroLib 已经过优化以最小化代码大小,因此某些函数的执行速度将比 ARM 编译工具中提供的标准 C 库例程慢。
  • MicroLib 和 ARM 标准库都包含在 Keil MDK-ARM 中。
  • 有关更多详细信息,请参阅与默认 C 库的区别

微库的一些问题:

  • MicroLib 是缺省 C 库的备选库。 它用于必须在极少量内存环境下运行的深层嵌入式应用程序。 这些应用程序不在操作系统中运行。
  • MicroLib 不会尝试成为符合标准的 ISO C 库
  • MicroLib 进行了高度优化以使代码变得很小。 它的功能比缺省 C 库少,并且根本不具备某些 ISO C 特性某些库函数的运行速度也比较慢,例如,memcpy()。

结合笔者查阅的资料,对于半主机,描述如下(不一定准确,若有问题,希望大佬们纠正):

  • 半主机是这么一种机制,它使得在ARM目标上跑的代码,如果主机电脑运行了调试器,那么该代码可以使用主机电脑的输入输出设备
  • 开发初期,可能开发者根本不知道该 ARM 器件上有什么输入输出设备,而半主基机制使得你不用知道ARM器件的外设,利用主机电脑的外设就可以实现输入输出调试
  • 要利用目标 ARM器件的输入输出设备(比如我们需要用到串口作为输入输出),首先要关掉半主机机制。然后再将输入输出重定向到 ARM 器件上,如printf()scanf()

微库和半主机模式的关系(摘自网络):

  • printf()之类的函数,使用了半主机模式,使用标准库(这里的标准库指代编译环境的库)会导致程序无法运行

  • 通过使用微库,可以不使用半主机模式

  • 若使用标准库,则需要添加代码:

    /*为确保没有从 C 库链接使用半主机的函数,因为不使用半主机,标准 C 库 stdio.h 中有些使用半主机的
    函数要重新写 ,您必须为这些函数提供自己的实现 */
    #pragma import(__use_no_semihosting)  // 确保没有从 C 库链接使用半主机的函数
    _sys_exit(int  x) //定义 _sys_exit() 以避免使用半主机模式
    {
    x = x;
    }
    struct __FILE  // 标准库需要的支持函数
    {
    int handle;
    };
    /* FILE is typedef ’ d in stdio.h. */
    FILE __stdout;
    
  • 在独立应用程序中,不太可能支持半主机操作。 因此,必须确保您的应用程序中没有链接C库半主机函数。

  • 为确保没有从C库链接使用半主机的函数, 必须导入符号 __use_no_semihosting

  • 可在您工程的任何 C 或汇编语言源文件中执行此操作,如下所示:

  • 在 C 模块中,使用 #pragma 指令:#pragma import(__use_no_semihosting)

  • 在汇编语言模块中,使用 IMPORT 指令:IMPORT __use_no_semihosting

  • 如果仍然链接了使用半主机的函数,则链接器会报告错误。

为什么要禁用半主机模式?

在嵌入式的编程中避免不了使用printffopenfclose等函数,但是因为嵌入式的程序中并没有对这些函数的底层实现,使得设备运行时会进入软件中断BAEB处,这时就需要__use_no_semihosting这 个声明,使程序遇到这些文件操作函数时不停在此中断处。

关于半主机模式更多知识,可以阅读这篇博客

由于在Clion中没有微库来调用,我们尝试原子的代码,编译后出现一个warning:

====================[ Build | 10_StdUSART_LED.elf | Debug-MinGW-STM32 ]=========
"E:\Program Files\JetBrains\CLion\bin\cmake\win\x64\bin\cmake.exe" --build F:\Programmer\JetBrainsProject\CLion\Embedded\STM32\Study_STD\10_StdUSART_LED\cmake-build-debug-mingw-stm32 --target 10_StdUSART_LED.elf -j 14
[1/2] Building C object CMakeFiles/10_StdUSART_LED.elf.dir/Plugins/PrintfRetarget/retarget.c.obj
F:/Programmer/JetBrainsProject/CLion/Embedded/STM32/Study_STD/10_StdUSART_LED/Plugins/PrintfRetarget/retarget.c:77:1: warning: return type defaults to 'int' [-Wimplicit-int]
   77 | _sys_exit(int x) {
      | ^~~~~~~~~
[2/2] Linking C executable 10_StdUSART_LED.elf
Memory region         Used Size  Region Size  %age Used
          CCMRAM:          0 GB        64 KB      0.00%
             RAM:        2656 B       192 KB      1.35%
           FLASH:        3400 B         1 MB      0.32%

Build finished

我们先忽略,到主函数中尝试调用printf(),再编译,问题就出来了:

====================[ Build | 10_StdUSART_LED.elf | Debug-MinGW-STM32 ]=========
"E:\Program Files\JetBrains\CLion\bin\cmake\win\x64\bin\cmake.exe" --build F:

[1/2] Building C object CMakeFiles/10_StdUSART_LED.elf.dir/Plugins/PrintfRetarget/syscalls.c.obj
[2/2] Linking C executable 10_StdUSART_LED.elf
FAILED: 10_StdUSART_LED.elf 
cmd.exe /C "cd . && E:\Programs\OpenSourcePrograms\DevTools\GCC\gcc-arm-none-eabi-10.3-2021.10\bin\arm-none-eabi-gcc.exe -g -Wl,-gc-sections,--print-memory-usage,-Map=F:/Programmer/JetBrainsProject/CLion/Embedded/STM32/Study_STD/10_StdUSART_LED/cmake-build-debug-mingw-stm32/10_StdUSART_LED.map -mcpu=cortex-m4 -mthumb -mthumb-interwork -T F:/Programmer/JetBrainsProject/CLion/Embedded/STM32/Study_STD/10_StdUSART_LED/STM32F429IGTX_FLASH.ld CMakeFiles/10_StdUSART_LED.elf.dir/Drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/system_stm32f4xx.c.obj CMakeFiles/10_StdUSART_LED.elf.dir/Drivers/STM32F4xx_StdPeriph_Driver/src/misc.c.obj CMakeFiles/10_StdUSART_LED.elf.dir/Drivers/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_adc.c.obj ..................................此处省略多项.................................................. CMakeFiles/10_StdUSART_LED.elf.dir/User/Source/main.c.obj CMakeFiles/10_StdUSART_LED.elf.dir/User/Source/stm32f4xx_it.c.obj -o 10_StdUSART_LED.elf   && cmd.exe /C "cd /D F:\Programmer\JetBrainsProject\CLion\Embedded\STM32\Study_STD\10_StdUSART_LED\cmake-build-debug-mingw-stm32 && arm-none-eabi-objcopy -Oihex F:/Programmer/JetBrainsProject/CLion/Embedded/STM32/Study_STD/10_StdUSART_LED/cmake-build-debug-mingw-stm32/10_StdUSART_LED.elf F:/Programmer/JetBrainsProject/CLion/Embedded/STM32/Study_STD/10_StdUSART_LED/cmake-build-debug-mingw-stm32/10_StdUSART_LED.hex && arm-none-eabi-objcopy -Obinary F:/Programmer/JetBrainsProject/CLion/Embedded/STM32/Study_STD/10_StdUSART_LED/cmake-build-debug-mingw-stm32/10_StdUSART_LED.elf F:/Programmer/JetBrainsProject/CLion/Embedded/STM32/Study_STD/10_StdUSART_LED/cmake-build-debug-mingw-stm32/10_StdUSART_LED.bin""
e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp\libg.a(lib_a-sbrkr.o): in function `_sbrk_r':
sbrkr.c:(.text._sbrk_r+0xc): undefined reference to `_sbrk'
e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp\libg.a(lib_a-writer.o): in function `_write_r':
writer.c:(.text._write_r+0x14): undefined reference to `_write'
e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp\libg.a(lib_a-closer.o): in function `_close_r':
closer.c:(.text._close_r+0xc): undefined reference to `_close'
e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp\libg.a(lib_a-fstatr.o): in function `_fstat_r':
fstatr.c:(.text._fstat_r+0x12): undefined reference to `_fstat'
e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp\libg.a(lib_a-isattyr.o): in function `_isatty_r':
isattyr.c:(.text._isatty_r+0xc): undefined reference to `_isatty'
e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp\libg.a(lib_a-lseekr.o): in function `_lseek_r':
lseekr.c:(.text._lseek_r+0x14): undefined reference to `_lseek'
e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: e:/programs/opensourceprograms/devtools/gcc/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp\libg.a(lib_a-readr.o): in function `_read_r':
readr.c:(.text._read_r+0x14): undefined reference to `_read'
Memory region         Used Size  Region Size  %age Used
          CCMRAM:          0 GB        64 KB      0.00%
             RAM:        3768 B       192 KB      1.92%
           FLASH:       11416 B         1 MB      1.09%
collect2.exe: error: ld returned 1 exit status
ninja: build stopped: subcommand failed.

很显然,直接在Clion中使用野火或者原子的代码重定向是行不通的,通过观察错误信息可以定位undefined reference to '_xxx',很显然因为我们使用的是ARM_GCC,而Keil是ARMCC,两者的库不一样,ARM_GCC缺少了这些定义导致这样重定向无法使用。

目前通过查阅资料,已知在GCC 中想要使用printf()函数是需要重定向_write()函数的(参照ARM Coretx-M4权威指南)。

怎么解决呢?这里给出解决方法

我们可以从官方给的例子中寻找答案,标准库中没找到,随后在官方给的HAL库中,我们找到了相关的定义,都在一个叫做syscalls.c的文件中,我们可以尝试复制过来直接用,在这里贴出其内容,方便需要的读者:

/**
*****************************************************************************
**
**  File        : syscalls.c
**
**  Abstract    : System Workbench Minimal System calls file
**
** 		          For more information about which c-functions
**                need which of these lowlevel functions
**                please consult the Newlib libc-manual
**
**  Environment : System Workbench for MCU
**
**  Distribution: The file is distributed “as is,” without any warranty
**                of any kind.
**
**  (c)Copyright System Workbench for MCU.
**  You may use this file as-is or modify it according to the needs of your
**  project. Distribution of this file (unmodified or modified) is not
**  permitted. System Workbench for MCU permit registered System Workbench(R) users the
**  rights to distribute the assembled, compiled & linked contents of this
**  file as part of an application binary file, provided that it is built
**  using the System Workbench for MCU toolchain.
**
*****************************************************************************
*/

/* Includes */
#include <sys/stat.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <sys/times.h>


/* Variables */
//#undef errno
extern int errno;
#define FreeRTOS
#define MAX_STACK_SIZE 0x2000

extern int __io_putchar(int ch) __attribute__((weak));
extern int __io_getchar(void) __attribute__((weak));

#ifndef FreeRTOS
  register char * stack_ptr asm("sp");
#endif


register char * stack_ptr asm("sp");

char *__env[1] = { 0 };
char **environ = __env;


/* Functions */
void initialise_monitor_handles()
{
}

int _getpid(void)
{
	return 1;
}

int _kill(int pid, int sig)
{
	errno = EINVAL;
	return -1;
}

void _exit (int status)
{
	_kill(status, -1);
	while (1) {}		/* Make sure we hang here */
}

int _read (int file, char *ptr, int len)
{
	int DataIdx;

	for (DataIdx = 0; DataIdx < len; DataIdx++)
	{
		*ptr++ = __io_getchar();
	}

return len;
}

int _write(int file, char *ptr, int len)
{
	int DataIdx;

	for (DataIdx = 0; DataIdx < len; DataIdx++)
	{
		__io_putchar(*ptr++);
	}
	return len;
}

caddr_t _sbrk(int incr)
{
	extern char end asm("end");
	static char *heap_end;
	char *prev_heap_end;

	if (heap_end == 0)
		heap_end = &end;

	prev_heap_end = heap_end;
	if (heap_end + incr > stack_ptr)
	{
//		write(1, "Heap and stack collision\n", 25);
//		abort();
		errno = ENOMEM;
		return (caddr_t) -1;
	}

	heap_end += incr;

	return (caddr_t) prev_heap_end;
}

int _close(int file)
{
	return -1;
}


int _fstat(int file, struct stat *st)
{
	st->st_mode = S_IFCHR;
	return 0;
}

int _isatty(int file)
{
	return 1;
}

int _lseek(int file, int ptr, int dir)
{
	return 0;
}

int _open(char *path, int flags, ...)
{
	/* Pretend like we always fail */
	return -1;
}

int _wait(int *status)
{
	errno = ECHILD;
	return -1;
}

int _unlink(char *name)
{
	errno = ENOENT;
	return -1;
}

int _times(struct tms *buf)
{
	return -1;
}

int _stat(char *file, struct stat *st)
{
	st->st_mode = S_IFCHR;
	return 0;
}

int _link(char *old, char *new)
{
	errno = EMLINK;
	return -1;
}

int _fork(void)
{
	errno = EAGAIN;
	return -1;
}

int _execve(char *name, char **argv, char **env)
{
	errno = ENOMEM;
	return -1;
}

在添加该文件并加入CMakeList后,我们再编译,这次果然不报错了,尝试下载程序测试,printf()仍然没起作用

接下来怎么办呢?我们继续查看官方给的例子,在main.c文件中找到了相关的定义,这里省略掉无关的代码,并适当添加注释做解释:

/* UART handler declaration */
UART_HandleTypeDef UartHandle;

/* 在我们的工程中 __GNUC__ 肯定是定义了的,因此这一段其实就只有 #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) 生效了 */
#ifdef __GNUC__
/* With GCC, small printf (option LD Linker->Libraries->Small printf
   set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */

/**
  * @brief  Retargets the C library printf function to the USART.
  * @param  None
  * @retval None
  */
/* 这里写了 PUTCHAR_PROTOTYPE ,这不就是上面定义的那个宏吗,也就是说这里重写了 int __io_putchar(int ch) 这个函数,但是这跟 printf() 有什么关系呢? */
PUTCHAR_PROTOTYPE
{
  /* Place your implementation of fputc here */
  /* e.g. write a character to the USART3 and Loop until the end of transmission */
  HAL_UART_Transmit(&UartHandle, (uint8_t *)&ch, 1, 0xFFFF);

  return ch;
}

看到这里我们就知道答案了,与在keil5中重定义fputs()函数不一样,在GCC编译器中(stm32标准库)需要重定义的是__io_putchar(int ch),而这个函数,在_write中被使用,即syscalls.c中重写了_write()函数。

int _write(int file, char *ptr, int len)
{
	int DataIdx;

	for (DataIdx = 0; DataIdx < len; DataIdx++)
	{
		__io_putchar(*ptr++);
	}
	return len;
}

在搞明白原理之后,我们只需用重定向__io_putchar(int ch) 的部分源码替换掉原子或者野火在Keil中重定向printf()的那部分代码,然后将syscalls.c文件添加到工程并添加到CMakeList中,使其编译就可以正常使用printf()函数了。

这里其实不把 syscalls.c 整个文件拿过来只要重写 _write() 的那部分也可以,但是看在这个文件体积也不大的份上还是把他拿过来吧,万一以后用上也方便。

故在我们的项目中重写__io_putchar(int ch),同理,实现scanf()getchar()

// 这部分可以根据实际情况写到头文件中
#include "stm32f4xx_usart.h"
#include <stdio.h>
#define USART_DEBUG USART1

// 这部分就是重定向的实现了,一般习惯写入源文件
#ifdef __GNUC__
/* With GCC, small printf (option LD Linker->Libraries->Small printf
   set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */

#ifdef __GNUC__
#define GETCHAR_PROTOTYPE int __io_getchar (void)
#else
#define GETCHAR_PROTOTYPE int fgetc(FILE * f)
#endif /* __GNUC__ */

/**
  * @brief  Retargets the C library printf function to the USART.
  * @param  None
  * @retval None
  */
PUTCHAR_PROTOTYPE {
    /* 发送一个字节数据到串口 */
    /* 很简单,直接调用库函数中的 串口发送数据函数 */
    USART_SendData(USART_DEBUG, (uint8_t) ch);

    /*等待发送完毕*/
    while (USART_GetFlagStatus(USART_DEBUG, USART_FLAG_TXE) == RESET);
    /* 返回发送的字符 */
    return ch;
}

/**
  * @brief  Retargets the C library scanf, getchar function to the USART.
  * @param  None
  * @retval None
  */
GETCHAR_PROTOTYPE {
    /* 很简单,直接调用库函数中的接收 */
    /* 等待串口输入数据 */
    while (USART_GetFlagStatus(USART_DEBUG, USART_FLAG_RXNE) == RESET);
    /* 直接返回接收到的字符 */
    return (int) USART_ReceiveData(USART_DEBUG);
}

关于半主机,通过以下方法禁用:

  • syscalls.c头部添加#pragma提醒编译器不使用半主机模式

    /* 告知连接器不从C库链接使用半主机的函数 */
    #pragma import(__use_no_semihosting)
    
  • 或者通过下面代码包住相关代码段

    #if !defined(OS_USE_SEMIHOSTING) //如果定义了OS_USE_SEMIHOSTING
    // 中间是代码
    #endif //#if !defined(OS_USE_SEMIHOSTING)
    

若想直接重写_write_read,建议在syscalls.c中对这两个函数添加弱定义__attribute__((weak)),例如:

__attribute__((weak)) int _read (int file, char *ptr, int len)
{
    int DataIdx;
    for (DataIdx = 0; DataIdx < len; DataIdx++)
    {
        *ptr++ = __io_getchar();
    }
    return len;
}

这样就可以避免重定义,并保留syscalls.c的完整性(原谅我的强迫症TAT),具体原理可以百度,这里简单提一嘴:

  • A,B两个模块,A模块调用了不确定B模块是否提供了函数,但是又不得不调用,这个时候在A模块中再申明一个弱符号函数,即用weak,如果外部提供了调用外部的,如果没提供调用申明的。

实测发现即使不禁用半主机也能正常使用printf(),不知道具体什么原因,但还是建议禁止,若有大佬清楚,希望能帮忙答疑解惑!

此外,浮点数打印会有点问题,比如double a = 3.1415926535; printf("%f", a);,串口调试助手看到只能精确到3.141593,其实这是因为C语言%f默认保留6位小数,可以通过添加小数点保留更多位,例如%.8f保留到小数点第八位,输出为3.14159265,扯到C语言基础了23333。

看到不少文章提到需要到CMakelist中添加编译参数,才能正常使用浮点数输出:

set(COMMON_FLAGS "-specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float")

笔者发现,即使不添加,仍然能正常输出浮点数,若有大佬清楚,希望能帮忙答疑解惑!

还有个小问题,若打印字符串不添加\n,串口是无法输出的(可能复位后,没输出的这些内容,会哗的一下突然先出来,再正常执行程序),对于像我这种强迫症,查阅资料后,找到的解决方案如下:

在main函数开始的时候,执行如下代码,设置buffer缓存为0,这样一有数据就发送,不然会等到缓存满或有回车换行符才发送。如果没有这句,你的printf又没\n,log就会打不出来。

setvbuf(stdout, NULL, _IONBF, 0) 

建议写在main的初始化最前面,或者初始化串口之前,写入这段代码

还有另一种方式就是再打印后,跟一行代码fflush(stdout);,这样会强制刷新缓存,就可以正常输出了,但每次都这么操作比较麻烦,所以不推荐。

造成这样的原因为(摘录一段话):

由于Unix上(GCC就是从这边过来的,感兴趣可以了解一下GUN发展史)标准输入输出都是带有缓存的,一般是行缓存。对于标准输出,需要输出的数据并不是直接输出到终端上,而是首先缓存到某个地方,当遇到行刷新标志或者该缓存已满的情况下,才会把缓存的数据显示到终端设备上。ANSI C中定义换行符\n可以认为是行刷新标志。所以,printf函数没有带\n是不会自动刷新输出流,直至缓存被填满才会刷新输出流。

  • 强制刷新标准输出缓存fflush(stdout)
  • 放到缓冲区的内容中包含\n
  • 缓冲区已满;
  • 需要从缓冲区拿东西到时候,如执行scanf

如果还是不行,别总想着问题是不是出在自己身上,换一个串口调试助手吧!微软应用商店的就不错!

PS:此处@伏特加(vofa+),可能给我喝多了吧,喜欢伏特加的界面,但万万没想到,加上零缓存代码后,不加\n一直不输出,能让我倒几杯伏特加,思考人生的问题,竟然和它有关!换个串口调试助手就正常了2333,希望伏特加官方能看到解决一下。。。

9、解决串口scanf()问题

试过的读者可能会发现printf确实正常了,但是scanfgetchar仍然有问题,在使用这两个函数的时候,STM32好像“卡死”了,笔者首先考虑是不是重定向函数编写出问题了,就单独拧出来测试了一下,写了一个函数:

/**
 * @brief 接收一个字符
 * @param pUSARTx USART 外设号
 * @param None
 */
uint8_t Usart_ReceiveByte(USART_TypeDef *pUSARTx) {

    /* 很简单,直接调用库函数中的接收 */
    /* 等待串口输入数据 */
    while (USART_GetFlagStatus(pUSARTx, USART_FLAG_RXNE) == RESET);
    /* 直接返回接收到的字符 */
    return (int) USART_ReceiveData(pUSARTx);
}

放入主函数替换getchar(),发现能正常接收数据,这就奇怪了

HAL的比较全,查资料注意到HAL的处理方式如下:

#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#define GETCHAR_PROTOTYPE int __io_getchar(void)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#define GETCHAR_PROTOTYPE int fgetc(FILE *f)
#endif

PUTCHAR_PROTOTYPE
{
  HAL_UART_Transmit(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

GETCHAR_PROTOTYPE
{
  uint8_t ch = 0;

  /* Clear the Overrun flag just before receiving the first character */
  __HAL_UART_CLEAR_OREFLAG(&huart?);

  /* Wait for reception of a character on the USART RX line and echo this
   * character on console */
  HAL_UART_Receive(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  HAL_UART_Transmit(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

开始我注意到这句话:__HAL_UART_CLEAR_OREFLAG(&huart?);,初步查阅资料说可能和串口溢出中断有关,对于标准库,笔者尝试了较长时间无果,感兴趣的可以查阅资料,如有大佬,也希望能够答疑

第二天,出现转机,查阅资料时,偶然注意到这段话:

Unfortunately, the default file automatically generated by STM32CubeIDE results in unexpected behavior when internal buffering of the input stream is enabled. The simplest solution to this issue is to disable buffering before any call to is made. Copy and paste the line of code provided in Listing 3 into the initialization section of the function.syscalls.c``scanf()``main()

Listing 3: Disable internal buffering for the input stream

setvbuf(stdin, NULL, _IONBF, 0);

The function should now function properly for all but the floating point format specifiers. To enable those, continue on to the next step.scanf()

大概意思是,当启用输入流的内部缓冲时,STM32CubeIDE自动生成的默认文件(我的理解是syscalls.c)会导致意外行为,我们需要首先执行setvbuf(stdin, NULL, _IONBF, 0);这段代码来禁用输入流内部缓冲

建议在main的初始化最前面,或者初始化串口之前,写入上面这段代码

添加代码后,问题解决!

除此以外,这篇文章还提到了,浮点数可能会出问题,需要加前面的编译选项,实际使用好像不加也没问题,希望知道的大佬能科普一下

set(COMMON_FLAGS "-specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float")

还有个问题就是scanf通常用于接收字符或者字符串,若接收整数或者浮点数等数据时,可能再有些串口调试助手上无论怎么发送数据都没有反应,这里可能需要到串口调试助手上设置追加\n或者在后面跟一个空格再发送,因为scanf()是有终止条件的(又回到C语言基础了23333):

scanf()函数接收输入数据时,遇以下情况结束一个数据的输入:

  • 空格回车跳格键;
  • 遇宽度结束;
  • 遇非法输入。

还有个小问题,Clion中使用scanf可能会有警告,貌似是因为安全性问题,我们可以选择忽略,感兴趣的读者可以看看[这篇博文](CLion 建议使用 ‘strtol’ 而不是 ‘scanf’ - IT工具网 (coder.work))

Clang-Tidy: 'scanf' used to convert a string to a floating-point value, but function will not report conversion errors; consider using 'strtod' instead

接下来就可以就可以愉快玩耍printf()scanf()getchar()了!

由于笔者能力有限,关于输入输出流缓冲的问题,感兴趣的读者可以自行查阅资料。

10、更优雅的方法实现串口输入输出流

上面踩了一系列坑,我们也大概知道是怎么去分析和实现重定向的了,接下来我们做一些封装操作,让其变得更优雅,当然笔者的代码不一定写的好,欢迎大佬们在评论区提出好的建议!若有更好的方法,也希望能够和大家分享!

首先是retarget.h

#ifndef _STDUSART_RETARGET_H__
#define _STDUSART_RETARGET_H__

#include <stdio.h>
#include "stm32f4xx_usart.h"

#ifdef __GNUC__
/* With GCC, small printf (option LD Linker->Libraries->Small printf
   set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */

#ifdef __GNUC__
#define GETCHAR_PROTOTYPE int __io_getchar (void)
#else
#define GETCHAR_PROTOTYPE int fgetc(FILE * f)
#endif /* __GNUC__ */

/**
 * @brief 注册重定向串口
 * @param usartx 需要重定向输入输出的串口
 */
void RetargetInit(USART_TypeDef *huart);

#endif //_STDUSART_RETARGET_H__

随后是retarget.c

#include "retarget.h"

/* 定义USART端口,用于注册重定向的串口(也可以用UART,根据实际情况来改写) */
static USART_TypeDef *sg_retargetUsart;

/**
 * @brief 注册重定向串口
 * @param usartx 需要重定向输入输出的串口
 */
void RetargetInit(USART_TypeDef *usartx) {
    /* 注册串口 */
    sg_retargetUsart = usartx;

    /* Disable I/O buffering for STDOUT stream, so that
     * chars are sent out as soon as they are printed. */
    setvbuf(stdout, NULL, _IONBF, 0);
    /* Disable I/O buffering for STDIN stream, so that
     * chars are received in as soon as they are scanned. */
    setvbuf(stdin, NULL, _IONBF, 0);

}

/**
  * @brief  Retargets the C library printf function to the USART.
  * @param  None
  * @retval None
  */
PUTCHAR_PROTOTYPE {
    /* 发送一个字节数据到串口 */
    /* 很简单,直接调用库函数中的 串口发送数据函数 */
    USART_SendData(sg_retargetUsart, (uint8_t) ch);

    /*等待发送完毕*/
    while (USART_GetFlagStatus(sg_retargetUsart, USART_FLAG_TXE) == RESET);
    /* 返回发送的字符 */
    return ch;

}

/**
  * @brief  Retargets the C library scanf, getchar function to the USART.
  * @param  None
  * @retval None
  */
GETCHAR_PROTOTYPE {

    /* 很简单,直接调用库函数中的接收 */
    /* 等待串口输入数据 */
    while (USART_GetFlagStatus(sg_retargetUsart, USART_FLAG_RXNE) == RESET);
    /* 直接返回接收到的字符 */
    return (int) USART_ReceiveData(sg_retargetUsart);

}

把两个文件写入CMakeList中即可,若编译不通过,缺少定义,请尝试添加syscalls.cCMakeList中。

可选项,禁用半主机,添加这里的代码syscalls.c相关部分。

这样,我们在主函数中就可以愉快使用printf()scanf()getchar()了!以下是部分关键代码示例:

/* 需要包含重定向头文件 */
#include "retarget.h"

/* -----只保留关键代码------ */

/**
 * 注册 USART1 用于重定向
 * 当然其他端口也行
 */
RetargetInit(USART1);

/**
 * 一定要记得初始化 USART1,
 * (或者你指定的其他端口)
 * 配置模式为 115200 8-N-1
 * 其他也行,只要你能通信上
 */
USART1_Config();

/* -----使用printf、scanf、getchar------ */
char ch;
char buf[100];
printf("Hello World!\n");
printf("Input your Char");
ch = getchar();
printf("Your char is: %c!\n", ch);
printf("Input your name: ");
scanf("%s", buf);
printf("Hello, %s!\n", buf);

前面说过,看到不少文章提到需要到CMakelist中添加编译参数,才能正常使用浮点数输出:

set(COMMON_FLAGS "-specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float")

笔者发现,即使不添加,仍然能正常输出浮点数,若有大佬清楚,希望能帮忙答疑解惑!

四、通过Clion进行HAL库开发

1、创建CubeMX工程

HAL库的开发相对就比较简单了,可以直接通过CubeMX图形化配置,然后生成初始化后的代码,我们再根据需要添加、移植功能即可

还是一样,我们以点灯为例,首先通过CLion创建CubeMX工程:

image-20230403152546169

随后会弹出板卡选择窗口:

image-20230321093100884

这些配置文件是跟OpenOCD下载程序有关的,里面的板子很可能是没有我们自己要用的型号的,后面会介绍怎么自己建立这个配置文件,这里先点取消。

随后会生成一个ioc文件,这个文件跟使用STM32CubeMX直接创建的是一样的,点击图中的链接可以跳转到STM32CubeMX中打开这个ioc文件:

image-20230321093625854

先选择芯片型号,默认芯片型号是STM32F030F4Px,点击更改成你自己的芯片型号,CubeMX会根据对应的芯片生成对应的启动文件,到时候工程就使用它生成的启动文件。

image-20230321094038292 image-20230321094436986

接下来我们来进行简单的配置

首先确认时钟源,进入工程后打开 RCC 选项,选择 Crystal/Ceramic Resonator,即使用外部晶振作为 HSE 的时钟源,否则后面的时钟配置无法选中HSE,配置如图:

image-20230403153641759

接下来配置IO,大家根据自己的需要配置IO模式、速度等参数,下面仅仅是一个简单的例子,我们点击右下角搜索需要配置的IO口,随后该IO会闪烁(如果有这个IO的话),因为要控制LED,我们选择输出模式(图中的1、2、3步)。

image-20230403153641759

接下来我们配置系统时钟:

以笔者使用的野火F429为例,开发板的外部晶振为 25MHz,我们填入 25;通道选择 HSE;PLLM 选择为/15;倍频系数 N 选择 为 x216;系统时钟选择 PLLCLK;系统时钟设定为 180Mz;APB1 分频系数选择为/4 即 PCLK1 位 45MHz;APB2 分频系数选择为/2 即 PCLK2 位 90MHz。如果嫌麻烦,简单粗暴的办法是,世界选中③HSE,并再⑦中填入180,随后CubeMX会自动调整最合适的参数。

image-20230403155006900

随后进一步配置 IO 的具体属性,到左侧配置GPIO为推挽输出,上拉,速度等等,也可以通过右侧的System View,点击GPIO进入具体配置界面。

image-20230403155006900

我们设置相关的参数,由于要控制LED,我们设置推挽输出,上拉模式,速度选择低速足矣,或者使用的默认电平,开漏输出,无上下 拉,低速模式也可以,引脚标签为 LED_R。

**你以为这样就结束了吗?**在配置工程前,我们还有最重要的一步,需要开启系统的Debug调试接口,如图:

image-20230403160042924

若不开启的话,可能出现的后果是:

  • 芯片被“锁”,本次下载以后将无法无法再下载程序!!!

这里声明一下,若出现这种问题,希望大家能自行搜索解决,此处就不给解决方案了,一方面,检索也是一种能力,另一方面,能增加你对boot的理解。笔者也是踩过坑的!

最后点击Project Manager

这部分和标准库创建CubeMX工程——最后部分内容基本一致,就不再赘述了。

强调一下,设置中项目名称和路径一定要和在Clion中建立的一致,这样生成的工程文件才会覆盖Clion中的文件,否则会另外生成一个文件夹,Clion就无法读取了。

网上有教程选择的是SW4STM32,但是我们发现没有这个选项,根据网上的很多人都会要求把CubeMX降低到某个版本以下,但是一直使用低版本肯定不是解决问题的方法。其实在CLion文档里面就有解决方法。STM32CubeMX项目 |CLion 文档 (jetbrains.com)

Toolchain/IDE选择STM32CubeMX即可,右侧的勾,勾上

随后点击Generate Code

这里可以注意一下堆栈大小,一般0x200~0x400够用

这部分与Keil中启动文件内的Stack_SizeHeap_Size段码对应

关于创建CubeMX的一些建议,参照3.2 个人习惯建议

2、添加自定义功能代码

此时我们回到Clion,就可以根据需要在主程序中添加自己需要的功能了,这里简单讲一下CubeMX生成代码main.c

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2023 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void) {
    /* USER CODE BEGIN 1 */

    /* USER CODE END 1 */

    /* MCU Configuration--------------------------------------------------------*/

    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();

    /* USER CODE BEGIN Init */

    /* USER CODE END Init */

    /* Configure the system clock */
    SystemClock_Config();

    /* USER CODE BEGIN SysInit */

    /* USER CODE END SysInit */

    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    /* USER CODE BEGIN 2 */

    /* USER CODE END 2 */

    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1) {
        /* USER CODE END WHILE */

        /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void) {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    /** Configure the main internal regulator output voltage
    */
    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

    /** Initializes the RCC Oscillators according to the specified parameters
    * in the RCC_OscInitTypeDef structure.
    */
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLM = 15;
    RCC_OscInitStruct.PLL.PLLN = 216;
    RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
    RCC_OscInitStruct.PLL.PLLQ = 4;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
        Error_Handler();
    }

    /** Activate the Over-Drive mode
    */
    if (HAL_PWREx_EnableOverDrive() != HAL_OK) {
        Error_Handler();
    }

    /** Initializes the CPU, AHB and APB buses clocks
    */
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                  | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK) {
        Error_Handler();
    }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void) {
    /* USER CODE BEGIN Error_Handler_Debug */
    /* User can add his own implementation to report the HAL error return state */
    __disable_irq();
    while (1) {
    }
    /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

其中MX_GPIO_Init()用于初始化刚才配置的GPIO,若配置其他外设,也会生成相应的初始化函数

我们关注的重点在于/* USER CODE BEGIN *//* USER CODE END */中间部分,这里包含了自定义宏、自定义数据类型、枚举、自定义初始化函数、自定义功能代码等等,建议将自己的代码以及调用的函数、声明等等对号入座,这样以后重新配置CubeMX的时候,可以避免被覆盖(不建议这么做,不确保是否会被覆盖,且每次重新生成代码,会将CMakeList覆盖),这里需要在生成代码前勾上Keep User Code when re-Generating,默认已经勾上了。

image-20230403164520792

接着继续讲main.h

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.h
  * @brief          : Header for main.c file.
  *                   This file contains the common defines of the application.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2023 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __MAIN_H
#define __MAIN_H

#ifdef __cplusplus
extern "C" {
#endif

/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx_hal.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Exported types ------------------------------------------------------------*/
/* USER CODE BEGIN ET */

/* USER CODE END ET */

/* Exported constants --------------------------------------------------------*/
/* USER CODE BEGIN EC */

/* USER CODE END EC */

/* Exported macro ------------------------------------------------------------*/
/* USER CODE BEGIN EM */

/* USER CODE END EM */

/* Exported functions prototypes ---------------------------------------------*/
void Error_Handler(void);

/* USER CODE BEGIN EFP */

/* USER CODE END EFP */

/* Private defines -----------------------------------------------------------*/
#define LED_R_Pin GPIO_PIN_10
#define LED_R_GPIO_Port GPIOH
/* USER CODE BEGIN Private defines */

/* USER CODE END Private defines */

#ifdef __cplusplus
}
#endif

#endif /* __MAIN_H */

如果要添加代码,同理,不过要吐槽一下LED_R的宏定义竟然出现在这里了,给人感觉有点乱。

随后我们添加自己的代码,来点亮我们的LED吧!由于CubeMX帮助我们完成了LED_R相关引脚的配置和定义,我们仅需到主循环While(1)中调用即可。

HAL_Delay(1000);    // 1000ms
HAL_GPIO_TogglePin(LED_R_GPIO_Port, LED_R_Pin);

3、一些习惯建议

仅仅代表本人的习惯,不一定适合所有人,如有更好的习惯,或者有建议,欢迎评论区留言!

CubeMX生成的主函数和初始化相关的文件都在Core文件夹中,为了便于管理自己的项目,我一般会将自己的代码和CubeMX生成的代码解耦,自己创建一个User文件夹,结构如下:

User	# 用户自定义代码
├─BSP	# 板级支持包
│  ├─Include	# 板级支持包头文件
│  └─Source		# 板级支持包源文件
├─Include	# 自定义头文件
└─Source	# 自定义源文件

随后两种做法:

  • 一种是直接将main.cmain.h移动到User/SourceUser/Include中,随后修改CMakeList,再添加自己需要的代码
  • 另一种则是自己建立main_user.cmain_user.h,分别放入两个文件夹,随后像Arduino那样,在main_user.c中定义setup()loop()函数,并在main_user.h中声明,修改CMakeList,将这俩包含进main.h随后填入CubeMX生成的main.c对应位置,随后到这两个函数中实现自己的初始化和主循环逻辑。

4、编译工程

编译工程,每个项目都需要先设置CMake

这里就不用像前面标准库那么复杂了,我们直接点击小锤子,基本上CubeMX生成的代码,一下子就能通过编译,很快!

若遇到问题,可以考虑是否是自己添加的程序语法有问题,然后考虑CMakeList文件中是否将自己的代码和头文件包含进去了,这里参考3.4 修改CMakeList

5、烧录程序

参照标准库部分**3.6 烧录程序**

6、调试程序

参照标准库部分**3.7调试程序**

7、解决串口printf()scanf()问题

有了标准库的经验,这里就好办了,若有疑问可以参照3.8 串口printf()

这里就直接简单粗暴给出解决方案

创建retarget.h,如下:

#ifndef _HAL_UART_RETARGET_H__
#define _HAL_UART_RETARGET_H__

#include <stdio.h>
#include "stm32f4xx_hal.h"

#ifdef __GNUC__
/* With GCC, small printf (option LD Linker->Libraries->Small printf
   set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */

#ifdef __GNUC__
#define GETCHAR_PROTOTYPE int __io_getchar (void)
#else
#define GETCHAR_PROTOTYPE int fgetc(FILE * f)
#endif /* __GNUC__ */

/**
 * @brief 注册重定向串口
 * @param usartx 需要重定向输入输出的串口
 */
void RetargetInit(UART_HandleTypeDef *huart);

#endif //_HAL_UART_RETARGET_H__

创建retarget.c,如下:

#include "retarget.h"

/* 告知连接器不从C库链接使用半主机的函数 */
#pragma import(__use_no_semihosting)

/* 定义USART端口,用于注册重定向的串口 */
static UART_HandleTypeDef *sg_hUart;

/**
 * @brief 注册重定向串口
 * @param usartx 需要重定向输入输出的串口句柄
 */
void RetargetInit(UART_HandleTypeDef *huart) {
    /* 注册串口 */
    sg_hUart = huart;

    /* Disable I/O buffering for STDOUT stream, so that
     * chars are sent out as soon as they are printed. */
    setvbuf(stdout, NULL, _IONBF, 0);
    /* Disable I/O buffering for STDIN stream, so that
     * chars are received in as soon as they are scanned. */
    setvbuf(stdin, NULL, _IONBF, 0);
}

/**
  * @brief  Retargets the C library printf function to the USART.
  * @param  None
  * @retval None
  */
PUTCHAR_PROTOTYPE {
    /* 发送一个字节数据到串口 */
    /* 很简单,直接调用 HAL 库中的 串口发送数据函数 */
    HAL_UART_Transmit(sg_hUart, (uint8_t *)&ch, 1, 0xFFFF);
    /* 返回发送的字符 */
    return ch;
}

/**
  * @brief  Retargets the C library scanf, getchar function to the USART.
  * @param  None
  * @retval None
  */
GETCHAR_PROTOTYPE {
    /* 用于接收数据 */
    uint8_t ch;
    /* 调用 HAL 库中的接收函数 */
    HAL_UART_Receive(sg_hUart, (uint8_t *) &ch, 1, 0xFFFF);
    /* 直接返回接收到的字符 */
    return (int) ch;
}

示例,只给出关键代码:

/* 需要包含重定向头文件 */
#include "retarget.h"

/* -----只保留关键代码------ */

/**
 * 注册 USART1 用于重定向
 * 当然其他端口也行
 */
RetargetInit(&g_hUart1);    // 该句柄为全局定义,用于初始化你需要用的串口(笔者为USART1)

/**
 * 一定要记得初始化 USART1,
 * (或者你指定的其他端口)
 * 配置模式为 115200 8-N-1
 * 其他也行,只要你能通信上
 */
USART1_Config();

/* -----使用printf、scanf、getchar------ */
char ch;
char buf[100];
printf("Hello World!\n");
printf("Input your Char");
ch = getchar();
printf("Your char is: %c!\n", ch);
printf("Input your name: ");
scanf("%s", buf);
printf("Hello, %s!\n", buf);

前面说过,看到不少文章提到需要到CMakelist中添加编译参数,才能正常使用浮点数输出:

set(COMMON_FLAGS "-specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float")

笔者发现,即使不添加,仍然能正常输出浮点数,若有大佬清楚,希望能帮忙答疑解惑!

五、常见问题

1、最大文件字符数问题

有时候有些类型,变量或者常量已经定义,但是编译就是报错找不到,可能和最大文件字符数有关,设置大一点即可,如图:

image-20230401170004609

笔者最早默认是500000,移植标准库的时候,改到1000000便解决了问题,随后使用HAL库的时候又遇到了这个问题,笔者又将其修改为5000000解决了问题。

2、CMSIS-DAP command CMD_INFO failed.

按照经验,笔者的理解是,可能是端口被占用导致OpenOCD无法建立和调试器的通信,通过重新拔插DAP-Link的USB线,可解决。

3、所选平台不支持Thumb模式

有时候编译遇到过这样的问题:

Error: selected processor does not support `rbit r0,r1' in Thumb mode

分析:

  • 因为笔者比较懒,不想多次打开CubeMX重新配置,就直接将前面生成好的相关文件Ctrl C V 了,此时CMakeList文件中的配置还时默认F0的,笔者偷懒只修改了头文件、源文件相关的内容,其实还有很多需要注意的地方,导致这个问题的原因是一个编译参数:-mcpu,指定的平台不同,指令集肯定会有区别,故导致此错误

    add_compile_options(-mcpu=cortex-m0 -mthumb -mthumb-interwork)
    add_link_options(-mcpu=cortex-m0 -mthumb -mthumb-interwork)
    
  • 默认生成的F0Cortex-M0,而笔者的F4Cortex-M4,修改为m4即可,建议大家根据自己的实际平台进行修改:

    add_compile_options(-mcpu=cortex-m4 -mthumb -mthumb-interwork)
    add_link_options(-mcpu=cortex-m4 -mthumb -mthumb-interwork)
    

顺便提一嘴Thumb:

ARM的CPU运行的状态2种状态:ARM与THUMB。

​ 1、CPU在不同状态运行不同的指令集。取决于 cpsr 寄存器其中的位。

​ 2、thumb 指令集为 arm 指令集的子集。ARM指令4byte,32位,Thumb指令2byte(thumb中bl指令是4字节),16位。Thumb分为:分支指令、数据传送指令、单寄存器加载和存储指令以及多寄存器加载和存储指令。thumb指令集没有协处理器指令、信号量(semaphore)指令以及访问cpsr或spsr的指令。

​ 3、两者区别。可将Thumb 指令看作ARM指令压缩形式的子集,它具有16位的代码密度,以空间换取效率。Thumb不是一个完整的体系结构,不能独立于ARM指令集单独使用。比如所有异常自动进入ARM状态。编写 Thumb 指令时,先要使用伪指令 CODE16 声明,且在ARM指令中要使用BX指令跳转到Thumb指令,以切换处理器状态。编写 ARM指令时则可使用伪指令CODE32声明。

4、编译thumb指令集程序:

​ ①、在makefile中添加选项" -mthumb "以供编译使用。

​ ②、汇编文件中指定thumb格式。" .code 32 “下的代码,使用arm指令集编译,” .code 16 "下面的代码,使用thumb指令集。

​ 切换指令集使用bx命令,bx指向的寄存器末尾数字为1表示thumb,为0表示arm指令。若要跳转到thumb指令,需要在目标地址后手动" +1 "。

​ bx执行时先判断末尾数字,然后再执行" PC= AND 0xFFFFFFFE "进行对齐。

​ ARM指令是字对齐(指令的地址后两位为[1:0]=0b00),Thumb是半字对齐(指令的地址后两位为[1:0]=0bx0,x为0或1)。指令的地址的最后一位必为0。

​ ③、thumb指令不能直接向pc赋值( ldr pc, =main ),要先将伪指令赋给某寄存器,再把寄存器赋给pc ( ldr r0, =main ldr pc, r0 )。

​ ④、thumb指令集与变量初始化的冲突(memcpy问题)。可以把变量声明为静态的,无需修改的话还可以用const修饰。(或者自己实现memcpy。。。)

——摘自简书

最后:笔者能力有限,所用的方法以及所写重定向代码考虑的不一定全面,若有问题,希望能和大家一起能补充,并提出解决

创作不易,喜欢的话,可以请笔者喝杯咖啡:

(理性打赏)

本文参考的文章

附上本文参考过的文章:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐