简介

PlatformIO是下一代的嵌入式IDE,关于其基本介绍,欢迎查看我的上一篇文章:使用下一代的嵌入式IDE-PlatformIO 教程

STM32CubeMX是ST官方的代码生成工具,作为一个从前端转嵌入式的程序员,对各种寄存器配置真的是感到无语。比如开启串口1,明明一个函数能完成的事,非要让你去写几十行代码,配置RCC、配置GPIO、配置USART三种加起来数十个寄存器,而且每种寄存器的配置内容、配置顺序还隐藏的很深,官方文档也是一笔带过。
这实在是不符合软件工程的解耦思想。STM32CubeMX则可以让你点点点就能完成上述的工作。

这篇博客将要介绍的是结合使用PlatformIO和STM32CubeMX。

预先需要了解的知识

  • PlatformIO IDE的安装及基本使用
  • STM32CubeMX 的安装及基本使用
  • STM32系列MCU的基本编程方法
  • arm-none-eabi-gcc编译器的概念,gcc编译器的常用参数
  • makefile的基本概念
  • scons构建系统的基本概念
  • python的基础语法

整体架构图

在这里插入图片描述解释:使用STM32CubeMX和PlatformIO生成同一个工程。之后的scons构建工具、gcc编译器、jlink、GDB等工具都是已经集成到了PlatformIO中了。
生成了工程之后,需要配置一下scons构建工具的配置文件,然后点击编译调试,platfromIO会自动的帮我们完成之后的工作:调用scons、调用gcc、调用jlink、启动GDB server等。

一.生成工程

1.1 打开STM32CubeMX:

  • 选择你的MCU,这里我用的是STM32F103RC
  • RCC中开启外部HSE时钟,外部时钟比HSI更稳定些
  • 开启DEBUG 4线
  • 打开串口1用于打印调试信息,波特率115200,校验位0,停止位1
  • 切换到Project Manage选项,Code Generator中选择Copy only the necessary library files
  • 切换到Project, Toolchain/IDE选择makefile
  • 最后点击生成工程。

1.2 生成PlatformIO工程:

打开vscode,会自动弹出PlatformIO的主页。点击New Project,生成一个项目,弹出项目基本信息填写框。

这里的基本信息需要注意,Name项目名称需要和CubeMX中的一样,Board芯片也必须选择正确,Location项目目录需要和CubeMX中的一样。这样生成的工程和STM32CubeMX可以无缝结合。

作者注:还有另外一个骚气一点的办法。由于PlatformIO是根据项目中有没有platformio.ini文件来判断是否是PlatformIO项目的。同时这个文件又十分的简单,只有几行代码。所以另外一种办法是手动在CubeMX生成的工程中加上platformio.ini文件即可。

在这里插入图片描述

二:修改PlatformIO配置文件

在上面生成PlatformIO项目时,细心的读者已经发现了有一个Framework选项我没有提到。

其实原因是这样:STM32CubeMX的原理是,解析你在GUI界面上选择的外设,生成使用HAL库的代码,并同时将HAL库添加到工程中;而PlatformIO的原理是,不生成任何代码,但自动下载HAL库,并放在C:/.platformio目录下去,并在编译时自动使用C:/platformio下的HAL库。

这样你会发现,两者使用的HAL库其实不是同一个,两者的细微差异会导致编译失败。而由于我们实际使用的是CubeMX生成的代码,所以我们需要使用是 CubeMX的HAL库

综上所述,需要修改platformIO的配置文件:platfomio.ini如下(每一行代码的作用见注释):

[platformio]
src_dir = ./

[env:genericSTM32F103RC]
platform = ststm32
board = genericSTM32F103RC
/* 注释掉下面framework这一行(ini文件中分号表示注释)*/
/* 表示不使用plateformIO的HAL库 */
;framework = stm32cube 
/* 表示使用项目目录下的HAL库以及RT-threa */
build_flags =         
  -D STM32F103xE
  -IInc
  -IDrivers/CMSIS/Include
  -IDrivers/CMSIS/Device/ST/STM32F1xx/Include
  -IDrivers/STM32F1xx_HAL_Driver/Inc
  -IDrivers/STM32F1xx_HAL_Driver/Inc/Legacye/

/* 表示使用项目目录下的HAL库以及RT-thread */
src_filter = +<src/> +<startup_stm32f103xe.s> +<Drivers/> +<Middlewares/> 
/* 表示使用项目目录下的链接文件 */
board_build.ldscript = ./STM32F103RCTx_FLASH.ld
debug_tool = jlink

此时你应该可以正常进行编译和调试了。

在这里插入图片描述

三.再多一点,接入rt-thread RTOS操作系统

今天(20200903)公司聚餐,也算是为几位即将离职的同事送行。 以前在美团,对离职都比较避讳;现在到了小公司,周围的同事对离职反而看的很开。有四位将要离职的同事,一位是和我关系很不错的实习生,哎,我也想再回到学校体验无忧无虑的生活;一位是刚入职3周的新同事,有更好的机会,领导也放人并且祝福他;一位是华为过来的同事,他走了之后,我就是唯一一个来自大公司的了;最后一位则是我的直属领导,虽然技术能力并没有出神入化,但是有很强的技术精神,我比较敬佩。回家路上我又想了很多,我19年本科毕业就进入了美团,没有进入到最理想的BAT一线公司。在美团干了1年前端之后,发现自己其实对前端不感兴趣;来到小公司干了嵌入式之后,虽然对嵌入式比较感兴趣,但是薪资比同龄人低了一大截。 哎,人生到底追求的是什么,赢得什么才算胜利呢?

废话少说,言归正传。下面记录一下怎么再PlatformIO+STM32CubeMX的基础上再加上RT-Thread

一般而言,对于真正的嵌入式应用,RTOS是不可或缺的一环,需要把这一环打通,才能把PlatformIO投入真正的使用。

在上述第一步、第二步的项目的基础上,我们进行进一步的修改:

3.1 增加RT-thread的源文件,请参考官方链接:基于 CubeMX 移植 RT-Thread Nano

  • 使用CubeMX打开第二步中的项目,参考上述链接,添加rt-thread软件包。
  • 参考上述链接,在最左侧Additional Software处勾选软件包
  • 参考上述链接,System Core - NVIC - Code generation选项,取消勾选Time base: System tick timerPendable request for system serviceHard fault interrupt三个中断的生成
  • Connectivity中开启串口1用于rtthread shell交互。
  • System Core - NVIC - NVIC中Enable串口1中断
  • System Core - NVIC - Code generation取消勾选串口1中断的Call HAL handle,我们需要自己处理串口中断。
  • 切换到 Project Manager页面,勾选 Do not generate the main(),我们需要自己写main函数。
  • 再切换到 Project Manager页面下的 Advanced Settings,会看到有三个函数MX_GPIO_INITSystemClock_ConfigMX_USART1_UART_init,均勾选上Not Generate Function Call ,我们需要手动调用外设初始化。
  • 还是在上述页面,均 取消勾选 Visibility(Static),我们需要手动调用外设初始化,所以函数不能是Static静态函数。
  • 最后点击生成工程

3.2 将rt-thread源文件添加到编译链

这一步很简单,只需要在platformio.ini文件中加入即可:

  • build_flag中加入相关头文件:
build_flags = 
  -D STM32F103xE
  -IInc
  -IDrivers/CMSIS/Include
  -IDrivers/CMSIS/Device/ST/STM32F1xx/Include
  -IDrivers/STM32F1xx_HAL_Driver/Inc
  -IDrivers/STM32F1xx_HAL_Driver/Inc/Legacy
  /* 增加下面两行 */
  -IMiddlewares/Third_Party/RealThread_RTOS_RT-Thread/components/finsh/
  -IMiddlewares/Third_Party/RealThread_RTOS_RT-Thread/include/
  • src_filter 中加入Middlewares文件夹:
src_filter = +<src/> +<startup_stm32f103xe.s> +<Drivers/> +<Middlewares/>

3.3 移植rtthread:

这一步其实和PlatformIO没多大关系了,主要是rtthread的移植工作,所以我将其放在附录,有兴趣的请移步附录A。

四.做技术就是要贪心,再接入Modbus协议栈

TODO,这部分就不详细说明了,如果你有需要欢迎在下面留言,我会根据情况更新的。
这部分只需要在第三步的基础上,在编译链中加上Modbus的相关文件,然后移植Modbus协议栈即可,请参考我的这篇文章:STM32 MODBUS协议-简介及接入 FreeMODBUS

参考CubeMX_script.py

Import("env")
import os

env.Prepend(CCFLAGS=[
  "-IFreeModbus/port",
  "-IFreeModbus/modbus/include",
  "-IFreeModbus/modbus/rtu"
])
modbusfiles = [
  os.path.join("$BUILD_SRC_DIR",  "./FreeModbus/modbus/mb_m.c"),
  os.path.join("$BUILD_SRC_DIR",  "./FreeModbus/modbus/rtu/mbcrc.c"),
  os.path.join("$BUILD_SRC_DIR",  "./FreeModbus/modbus/rtu/mbrtu_m.c"),
  os.path.join("$BUILD_SRC_DIR",  "./FreeModbus/modbus/functions/mbfuncother.c"),
  os.path.join("$BUILD_SRC_DIR",  "./FreeModbus/modbus/functions/mbfuncinput_m.c"),
  os.path.join("$BUILD_SRC_DIR",  "./FreeModbus/modbus/functions/mbutils.c"),
  os.path.join("$BUILD_SRC_DIR",  "./FreeModbus/port/rtt/port.c"),
  os.path.join("$BUILD_SRC_DIR",  "./FreeModbus/port/rtt/portevent_m.c"),
  os.path.join("$BUILD_SRC_DIR",  "./FreeModbus/port/rtt/portserial_m.c"),
  os.path.join("$BUILD_SRC_DIR",  "./FreeModbus/port/rtt/porttimer_m.c"),
]
env.Append(PIOBUILDFILES= modbusfiles)

附录A: 移植RT-thread

1.Middlewares\Third_Party\RealThread_RTOS_RT-Thread\bsp\board.c
修改rt_hw_board_init函数:增加初始化外设部分代码

#include "main.h"
extern void SystemClock_Config(void);
extern void MX_GPIO_Init(void);
extern void MX_USART1_UART_Init(void);
extern UART_HandleTypeDef huart1;
/* 调试串口1接收数据的消息队列buffer */
static uint8_t consoleInputBuffer[256];
struct rt_messagequeue consoleInputMQ;
void rt_hw_board_init()
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
   
    /* 使用串口1作为调试串口,初始化一个消息队列保存串口1接收到的数据,并手动开启串口中断 */
    rt_err_t error = rt_mq_init(&consoleInputMQ,"consoleInputMQ",consoleInputBuffer,
                                1,sizeof(consoleInputBuffer),RT_IPC_FLAG_FIFO);
    RT_ASSERT(error == RT_EOK);
    SET_BIT(huart1.Instance->CR1, USART_CR1_PEIE | USART_CR1_RXNEIE);

    /* System Clock Update */
    SystemCoreClockUpdate();
    
    /* System Tick Configuration */
    _SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);

    /* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INIT
    rt_components_board_init();
#endif

#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
    rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif
}

增加一个函数用于输出调试信息:

void rt_hw_console_output(const char *str)
{
    rt_size_t i = 0, size = 0;
    char a = '\r';

    __HAL_UNLOCK(&huart1);

    size = rt_strlen(str);
    for (i = 0; i < size; i++)
    {
        if (*(str + i) == '\n')
        {
            HAL_UART_Transmit(&huart1, (uint8_t *)&a, 1, 50);
        }
        HAL_UART_Transmit(&huart1, (uint8_t *)(str + i), 1, 50);
    }
}

2.Src\stm32f1xx_it.c中:
修改串口1中断函数:

#include "rtthread.h"
extern struct rt_messagequeue consoleInputMQ;
void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
  if(huart1.Instance->SR & USART_SR_RXNE)
  {
    uint8_t data =  (uint8_t)(huart1.Instance->DR & (uint8_t)0x00FF);
    rt_mq_send(&consoleInputMQ, &data, 1);
  }
  /* USER CODE END USART1_IRQn 0 */
  /* USER CODE BEGIN USART1_IRQn 1 */
  /* USER CODE END USART1_IRQn 1 */
}

3.Inc\rtconfig.h中:修改注释,开启FINSH控制台和消息队列。

/*注释掉 __CC_ARM,强制开启FINSH控制台*/
//#if defined(__CC_ARM) || defined(__CLANG_ARM)
#include "RTE_Components.h"
#if defined(RTE_USING_FINSH)
#define RT_USING_FINSH
#endif  //RTE_USING_FINSH
//#endif  //(__CC_ARM) || (__CLANG_ARM)

...

/* 开启消息队列 */
// <c1>Using Message Queue
//  <i>Using Message Queue
#define RT_USING_MESSAGEQUEUE

4.增加main.c中的main函数:

#include "rtthread.h"
int main(void) {
  rt_kprintf("Hello RT-thread! \r\n");
  while(1) {
  }
}

5.修改startup_stm32f103xe.s启动文件

  99行 bl main 

修改为

  bl entry

参考:此链接第99行。 这一行的意思是,初始化静态变量完成之后的跳转位置,默认是main,需要修改成rtthread中components.c 中的 157 行 entry 函数。

6.修改链接脚本,在.text段中增加如下代码:意思是保留 rti_fn段的代码,这是rtthread中的要求的。rtthread中的INIT_BOARD_INIT的原理其实就是把函数声明成rti_fn段,然后启动的时候再去寻找rti_fn段代码执行。
参考:此链接

.text{
	...
	...
	...
    /* section information for finsh shell */
    . = ALIGN(4);
    __fsymtab_start = .;
    KEEP(*(FSymTab))
    __fsymtab_end = .;

    . = ALIGN(4);
    __vsymtab_start = .;
    KEEP(*(VSymTab))
    __vsymtab_end = .;

    /* section information for initial. */
    . = ALIGN(4);
    __rt_init_start = .;
    KEEP(*(SORT(.rti_fn*)))
    __rt_init_end = .;
    ...
    ...
    ...
}

7.修改链接link参数,增加一个链接参数:
参考:此链接
首先在platformio.ini中增加一个脚本

extra_scripts = pre:CubeMX_script.py

然后写这个脚本的代码:CubeMX_script.py

Import("env")

env.Prepend(LINKFLAGS=[
  "--specs=nosys.specs"
])

附录B:

前两步的工程代码:
https://github.com/jiladahe1997/CSDN_PlatformIO_CubeMX_demo


版权声明:本文为jiladahe1997原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/jiladahe1997/article/details/108371747