我离不开PlatformIO和VS Code,但奈何STM32CubeMX又太好用,所以我要将STM32CubeMX创建的内容导入到PlatformIO项目中,结合两者的优点。
本文将使用我最熟悉的STM32F103C8T6。
这些东西没有是从哪儿学来的,全都是实践的经验(教训)。
创建一个PlatformIO项目
在PlatformIO中创建项目,板子选STM32F103C8,框架用stm32cube。
这样子可以创建一个空白的PlatformIO项目。
创建一个STM32CubeMX项目
我强烈建议在设计硬件时就把STM32CubeMX项目建出来,能方便确定硬件设计时的接线,这次我是对着数据手册设计的硬件,创建项目时设置引脚就提心吊胆的。
我的这个硬件使用了两个串口、一个USB、一个数模转换。
(但是我在之后又加了DMA、看门狗等)
创建出来的项目就当作一个临时项目,需要什么就拿什么复制到自己的项目。
配置功能
在Pinout & Configuration中,能够看到有System Core(系统核心)、Analog(模拟)、Timers(计时器)、Connectivity(接口)、Computing(计算)、Middleware and Software Packs(软件包)。
如果焊了外部晶振,需要在系统核心的RCC中配置,其中 旁路时钟源是有源晶振、晶体振荡器是无源晶振。
如果需要使用USB,那么需要使用倍频器,如果需要使用倍频器,那么需要使用外部晶振。

一定要在一开始创建项目的时候把用什么选好,如果想不清楚就多选一些,后边再改就比较麻烦了。
虽然FreeRTOS不好往PlatformIO那边移动,但是要用的话还是要选一下的,STM32CubeMX会把需要改的东西都改好。
导入生成代码
在STM32CubeMX中可以点击右上角生成代码,生成的代码大概长这个样子:
├─Core
│ ├─Inc
│ └─Src
├─Drivers
│ └─ CMSIS
├─EWARM
├─Middlewares
│ ├─ST
│ │ └─STM32_USB_Device_Library
│ └─Third_Party
│ └─FreeRTOS
└─USB_DEVICE
├─App
└─TargetPlatformIO的项目长这个样子:
├─.pio
│ └─build
│ └─genericSTM32F103C8
├─.vscode
├─include
├─lib
├─src
└─testMiddlewares、Third_Party里面的东西忽略,因为PlatformIO这边有这些东西。
FreeRTOS可以用PlatformIO安装FreeRTOS Kernel这个库。
接下来只需要把头文件复制到include,源文件复制到src就好啦。
PlatformIO这边比较随意,单片机的程序结构也比较轻量,也可以把一些东西按模块放到lib里面。
设计坑
多引几种接口出来,很可能就会用来调试。
不要自信的以为自己写软件的时候不用调试。
不熟悉的东西一定要看清楚数据手册,一点不差的照抄别人的。
给USB D+上接上拉电阻。
把直插的元件集中设计在边缘(尤其是排针),万一焊不好又要回加热台返工,中间的排针只能拆了。
复位引脚的上拉电阻不要太大,会拉不上去。(看别人都是10K,我设计了47K,但总是出问题,换成10K就没问题了)
硬坑
一定要启用调试,否则ST Link在烧录完之后就连不上机器了。
如果程序导致连不上编程器了,那么可以按住Reset,在编程器连接时再松开。
助焊剂一定要洗干净,导电的助焊剂会造成各种离谱问题。
出现了离谱的硬件问题,板子再清洗一遍。
芯片留余量,但一般芯片不会坏,测试可以做两个样品,遇到离谱问题换着试。
只要不是暴力开高温(像我加热台都是用化锡的最低温度)LED一般是不管烤多久都不会坏的(我烤了数10分钟都没问题),怀疑LED时先看看自己的程序是不是有问题。
GPIO是要启用时钟的,灯不亮,偶尔莫名其妙亮一亮看看是不是没启用GPIO时钟。
STM32CubeMX提供的代码如果知道在做什么那么可以改,最好不要删,尤其是你看上去是个低级错误的额外代码,比如,你看到连续四行一模一样设置GPIO时钟的代码,不要删(因为他们是ABCD,不太显眼,看上去是一样的)。
如果一个芯片正常工作很烫(比如我的充电芯片),那么它长时间工作大概率会烧穿,给它加个散热器被动散热吧...
有电池的话,不要焊完自信装电池,电池的电流可以很大,有短路会送走LOD甚至主控,建议先插电脑上。
有问题顶多送走个STLink
软坑(代码BUG Review)
计时器数据问题
用了FreeRTOS之后,HAL_GetTick()就无法获取到值了,因为系统计时器被FreeRTOS抢了,HAL拿不到时间了,HAL_Delay()这个函数底层是循环检查Tick,所以也不能用。
串口数据接收中断问题
如果实现了HAL_UART_RxCpltCallback不会被触发,那么检查项目有没有void USARTx_IRQHandler(void)这样子的函数,没有就是可能之前图形界面里没配置相关的中断,需要手动实现这个函数和在init里使用HAL_NVIC_EnableIRQ启用中断(同时使用HAL_NVIC_SetPriority设置优先级,亲测RTOS下5的优先级是能用的)。
栈溢出的问题
我用了RTOS,并且设置了#define configCHECK_FOR_STACK_OVERFLOW 2、#define configUSE_STACK_OVERFLOW_HOOK 1在堆栈溢出时触发回调打印。
经测试,检测到溢出一定会出莫名其妙的问题,但并非溢出一定检测到,程序出莫名其妙的问题可以考虑往上加加堆栈。
(像我这种在打印调试文本宏里面写了sprintf的只要程序在测试阶段跑起来了,生产环境就不会溢出了吧~)
互斥锁的问题
FreeRTOS里面有二值信号量和互斥锁,互斥锁功能高级一些,写Go习惯的我,当然就直接Mutex啦,炸了,迷惑了很久。
xSemaphoreCreateMutex()创建的互斥锁因为高级,所以ISR里面不可以使用,使用xSemaphoreCreateBinary()创建二值信号量。
修改频率的问题
机器上电的时候用的是内部晶振,需要用代码切换。
这个切换的代码一般第一次执行总是能成功的,除非没焊好。
但如果为了降低功耗需要运行时切换频率,那么需要注意。
锁相环倍频修改之前必须先让系统频率不要使用锁相环,否则会失败。
修改之后需要重新初始化串口、重新计算毫秒分频。
Printf浮点数的问题
无论是printf还是sprintf都无法使用 %f 占位符,考虑其他方法。
sprintf栈使用
虽然sprintf需要事先创建缓冲区,对人来说很难用,但它对机器来说也不是个省油的灯,在我的Debug宏中大量使用了它,它会使用大量的栈空间。
CBOR解码问题
为了减小硬件负担和带宽,我没用JSON,而是用了CBOR。
tinycbor这个库在访问套在Map里面的Map的时候需要执行进入操作,进入后就不能对外面的map操作,需要再退出,对于嵌入式硬件程序上需要让一段程序解析外面的,一段程序解析里面的不友好。
错误的操作
进入Map
拷贝一份迭代器
退出Map
继续进行外层操作
开始内层操作,使用拷贝的迭代器
经测试,最终使用的时候似乎存在未定义行为,迭代器会死循环。
正确的操作
拷贝一份没有进入Map的迭代器
继续进行外层操作
使用拷贝的迭代器进入Map
开始内层操作
很神奇。
ADC转换时间问题
如果程序测出来的电压总是比手动量出来低,而且还是一本正经的低(多次测量值一样),考虑增加ADC转换的时间,尤其是像我这样外面还加了个100K电阻还没个电容的。
串口读写错误问题
莫名奇妙板子摸一下就复位了。是不是因为接受了HAL_UART_ErrorCallback 回调,而且以为它很致命,直接在里面写了断言之类的?
实际上这个错误很容易出现。