如何优雅地开发STM32

最近由于烹饪机器人项目的需要,我学习了STM32。众所周知,STM32的主流开发平台是keil,但是keil这种上世纪风格的开发平台又不能赢得我的芳心,而像IAR这类软件又因为缺少代码补全的功能,应该没有人会喜欢写代码如记事本吧。思来想去,决定学习各位大佬的做法,用CLion+OPENOCD+STM32CubeMX来进行STM32的开发。

注:本文主要综合稚晖君大佬以及其他知乎大佬的教程,附加个人的踩坑过程,进行必要的总结,方便以后复习。

因为对于Keil并不熟悉,只有在大二下学期学习51单片机的时候使用过几次,繁琐的配置、烧录流程以及弱智的代码补全功能让我并不想回忆,我就不对于Keil开发stm32的配置过程进行详细介绍了。

那么话不多说,我们开始介绍CLion开发stm32的配置介绍吧。

本篇介绍采用Cube MX快速开发平台、配合ST的HAL库,可以快速地建立工程,不仅降低了代码的编写成本,同时也方便了代码的移植。

1.环境及所需工具

软件环境

  • Windows 10
  • STM32CubeMX
  • CLion-2021
  • MinGW
  • OpenOCD
  • arm-none-eabi-gcc

硬件环境

  • STM32F407ZGT6
  • ST-Link、DAP-Link

工具安装

  1. STMCubeMX

    这个直接去官网下载最新版的就可以了。

  2. OpenOCD

    OpenOCD
    OpenOCD是用于对STM32进行下载仿真的工具,是一个开源软件包,Windows版本下从这里下载,下载好解压到一个目录就行,后面会在Clion中链接这个目录:

如何在CLion中配置OpenOCD:

  1. MinGW

    CLion需要使用MinGW环境来配置工具链,即配置C、C艹的环境,安装方法如下:

    首先去MinGW主页下载最新版本的MinGW: Minimalist GNU for Windows,这是MinGW的安装器:

下载之后,打开exe进行安装,修改安装目录(目录中不要有空格,尽量不要存在中文),安装完成后进行组件的下载,把 Basic Setup 里面的组件全部勾选,进行相应的下载。(注:下载安装时间比较漫长,慢慢等待……)

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

重启电脑,在cmd窗口输入gcc-v验证安装是否成功:

image-20210805224833945

  1. arm-none-eabi-gcc

Windows可以到这里下载,选择ZIP压缩包形式的:

image-20210806104621460

将ZIP解压缩到一个文件夹,并将安装目录下的bin文件夹添加到环境变量中:

image-20210806110841084

然后重启,环境变量生效之后,可以在命令行中用以下arm-none-eabi-gcc -v测试:

image-20210806111226023

以上就表示安装完成了。

CLion配置

Clion是基于CMake来管理项目的,所以首选我们需要配置好预设的MinGW和CMake环境。

打开CLion,File->Settings->Build,Execution,Deployment->Toolchains,添加一个MinGW环境,如下图所示:

image-20210806112049131

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

查看CMake中工具链是否正确:

image-20210806112654187

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

2.在CLion中创建STM32工程

创建CubeMX工程

选择File->New Project,创建STM32CubeMX的项目:

image-20210806113216284

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

image-20210806113333789

默认选中的芯片型号是STM32F030F4Px,我们可以在CubeMX中重新选择自己需要的芯片,一切操作都和使用Keil开发没有区别。

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

另外生成的IDE类型选择是SW4STM32

image-20210806113934520

每次Generate之后,弹窗直接点击Close,CLion中会自动更新文件。

image-20210806114925158

第一次设置完,回到CLion界面时,会弹出一个板卡选择窗口,如下:

image-20210806115030498

这里我们先不急着去选择,这些cfg配置文件主要是与我们之后OpenOCD下载程序相关,里面的板子很有可能没有我们想要的型号,那么后面我们再进行相关配置文件的修改,这里先点击取消。

编译工程

若IDE中的CMake中没有出现红色报错,说明我们的工程配置文件就没有问题。

image-20210806115419831

顶栏的三个图标分别是编译、下载、调试

image-20210806115615175

点击编译后,可以在IDE下方的Messages中看到我们的编译输出:

image-20210806115746418

可以看到,成功地生成了用于烧写的**.bin.hex**文件。

烧录程序&在线调试

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

点击编译旁边的配置栏,选择Edit Configurations…

image-20210806120004516

进入到配置界面:

image-20210806120139716

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

我们在工程根目录下新建一个文件夹config,在里面新建一个配置文件st-link.cfg(因为我使用的是正点原子的ST-link作为仿真器),文件的内容如下:

# This is an STM32F4 discovery board with a single STM32F407VGT6 chip.
# http://www.st.com/internet/evalboard/product/252419.jsp

source [find interface/stlink.cfg]

transport select hla_swd

# increase working area to 64KB
#set WORKAREASIZE 0x10000

source [find target/stm32f4x.cfg]

#reset_config srst_only

注意要注释掉 reset_config srst_only,据说这一句是指示系统重启的,删除不影响下载。

采用DAP-Link下载器的,增加文件的内容为:

# choose st-link/j-link/dap-link etc.
adapter driver cmsis-dap

transport select swd

source [find target/stm32f4x.cfg]

当然我写的也不一定全部情况下都适用,如果不清楚如何设置的同学,可以参考OpenOCD自带的一些配置文件,路径就在OpenOCD安装目录下的share\openocd\scripts中:

image-20210806121334870

只需要关注这几个目录:

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

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

注:CLion的界面简洁美观,提供了非常方便的代码补全功能,并且有断点调试、变量查看等功能,但是目前还不支持查看寄存器的值,也是急需改善的地方吧

3.其他问题

编译错误问题

如果移动了工程文件夹的话,最好打开.ioc文件重新Generate一下再编译,可以解决很多问题。

printf重定向问题[^1]

在Keil里面为了使用printf函数我们需要重定向fputc函数:

int fputc (int ch, FILE *f)
{
(void)HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
return ch;
}

其中的FILE定义在stdio.h头文件中,所以需要在项目中包含这个头文件,但是经过测试发现,Keil里面包含的是MDK\ARM\ARMCC\include这个目录下的stdio.h,而在Clion中是不会链接到这个文件的。因此如果在Clion中也按之前的方法进行重定向,会发现printf没有任何输出。

在Clion中链接的是GNU-Tools-ARM-Embedded\arm-none-eabi\include里面的stdio.h,如果仍然想使用printf函数功能,则需要进行如下操作:

新建一个retarget.h文件内容如下:

#ifndef _RETARGET_H__
#define _RETARGET_H__

#include "stm32f1xx_hal.h"
#include <sys/stat.h>
#include <stdio.h>

void RetargetInit(UART_HandleTypeDef *huart);

int _isatty(int fd);

int _write(int fd, char *ptr, int len);

int _close(int fd);

int _lseek(int fd, int ptr, int dir);

int _read(int fd, char *ptr, int len);

int _fstat(int fd, struct stat *st);

#endif //#ifndef _RETARGET_H__

再新建一个retarget.c文件内容如下:

#ifndef _RETARGET_H__
#define _RETARGET_H__

#include "stm32f1xx_hal.h"
#include <sys/stat.h>
#include <stdio.h>

void RetargetInit(UART_HandleTypeDef *huart);

int _isatty(int fd);

int _write(int fd, char *ptr, int len);

int _close(int fd);

int _lseek(int fd, int ptr, int dir);

int _read(int fd, char *ptr, int len);

int _fstat(int fd, struct stat *st);

#endif //#ifndef _RETARGET_H__再新建一个retarget.c文件内容如下:#include <_ansi.h>
#include <_syslist.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/times.h>
#include <retarget.h>
#include <stdint.h>

#if !defined(OS_USE_SEMIHOSTING)

#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2

UART_HandleTypeDef *gHuart;

void RetargetInit(UART_HandleTypeDef *huart)
{
gHuart = 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);
}

int _isatty(int fd)
{
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 1;

errno = EBADF;
return 0;
}

int _write(int fd, char *ptr, int len)
{
HAL_StatusTypeDef hstatus;

if (fd == STDOUT_FILENO || fd == STDERR_FILENO)
{
hstatus = HAL_UART_Transmit(gHuart, (uint8_t *) ptr, len, HAL_MAX_DELAY);
if (hstatus == HAL_OK)
return len;
else
return EIO;
}
errno = EBADF;
return -1;
}

int _close(int fd)
{
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 0;

errno = EBADF;
return -1;
}

int _lseek(int fd, int ptr, int dir)
{
(void) fd;
(void) ptr;
(void) dir;

errno = EBADF;
return -1;
}

int _read(int fd, char *ptr, int len)
{
HAL_StatusTypeDef hstatus;

if (fd == STDIN_FILENO)
{
hstatus = HAL_UART_Receive(gHuart, (uint8_t *) ptr, 1, HAL_MAX_DELAY);
if (hstatus == HAL_OK)
return 1;
else
return EIO;
}
errno = EBADF;
return -1;
}

int _fstat(int fd, struct stat *st)
{
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
{
st->st_mode = S_IFCHR;
return 0;
}

errno = EBADF;
return 0;
}

#endif //#if !defined(OS_USE_SEMIHOSTING)

添加这两个文件到工程,更新CMake,编译之后会发现,有几个系统函数重复定义了,被重复定义的函数位于Src目录的syscalls.c文件中,我们把里面重复的几个函数删掉即可。

大概位置在140-160行的位置:

image-20210806150442642

在main函数的初始化代码中添加对头文件的引用并注册重定向的串口号:

#include "retarget.h"

RetargetInit(&huart1);

这样就可以像之前一样愉快地使用scanfprintf啦!

[^1]:添加完.h.c文件之后,最好File->Update Cmake Project with STM32CubeMX更新一下CMake,这样可以减少很多错误

image-20210806151206712