一、前言
为什么写这一篇博客呢,主要是大学时期搞单片机两年,手头上有开发板两个,各种小型板好几个,放着浪费。现在工作这么多年,已经对编译、环境、linux等很熟悉了。想着将这个技能重新拾起来,顺便给后代铺条路。
本文是基于linux上使用stm32cubemx生成工程,使用jlink烧写stm32,开发环境是vscode(宇宙第一编辑器),语言C/C++,可能扩展一下使用golang开发更方便。
二、环境搭建
我用的archlinux,不过其他系统都一样。需要安装下面的几个软件。
1. stm32cubemx 图形化配置stm32芯片生成hal库
这个东西官网有linux版,可以直接到官网搜索下载。因为用的是arch,所以直接aur安装就好了
1 | yay -S stm32cubemx |
2. arm-none-eabi-gcc 交叉编译工具
这个肯定有linux版,也是直接官网下载就好了。这个也有官方的包,pacman就能下载,不用aur,其他linux应该apt或yum也能安装
1 | arm-none-eabi-newlib 一些c标准库文件 |
3. JLinkExe jlink下载工具
为什么是这个不使用openocd,主要是openocd对于jlink的支持不太完整(也可能是我不会用,但是我研究了好久没成功)。我烧写的时候发现如果我配置stm32的时候没有将debug信息配置好jtag,烧写就成了一次性工程,烧完之后再也连不上了。但是使用官方jlink就每这个问题,就算不配置debug也可以强行烧写。
同样的,官网直接下载linux版,我使用aur
1 | yay -S jlink-software-and-documentation |
4. 其他软件
看自己需要哈,我装了下面几个
- vscode用来写代码,也可以vim、kate、notepad啥的
- make用来编译使用makefile
- cmake也可以替代make命令使用CMakeLists.txt编译
三、上手开发(先点个灯)
打开stm32cubemx,选芯片配置

我使用的是stm32f103zet6的开发板,所以选择这个芯片

第一步先配置一下时钟晶振,因为开发板默认外置了一个8MHz的晶振,所以我配置一个高速时钟选晶体/陶瓷共振器(crystal/ceramic resonator)

在时钟配置界面选择好HSE和PLLCLK,然后将时钟设置到最大的72MHz回车自动计算前面的参数就配置好了

开发板上面的8个灯对应的是PC0-PC7,那么就回到芯片界面配置一下gpio用来点灯,可以搜索PC0,找到芯片上闪光的那个,单击选择GPIO Output就可以了。我选了8个引脚都打开。

开始生成代码,为了看懂编译过程和命令,我先使用makefile搞,后面搞懂之后用cmake会更方便

选好后点击右上角的GENERATE CODE生成代码就好了。找到生成目录,用vscode打开这个目录。

目录结构如上,test.loc是stm32cubemx的工程文件,其他都是代码文件相关,有一个汇编的s文件,一个链接用的ld文件。
改改代码,在main.c中找到while(1),在里面输入点灯的操作,要写在USER CODE BEGIN x里面,否则重新配置芯片之后会将代码给覆盖了
1 | // Core/Src/main.c |
然后打开终端直接make就可以输出一个build/test.bin文件,可以用来烧写了
注:stm32cubemx的6.12.1版本有bug,报错STM32F103ZETx_FLASH.ld:56: synax error
修改很简单,在这个文件里面按照下面的方式修改
1 | _estack = ORIGIN() + LENGTH(); /* end of RAM */ |
1 | /* Initialized data sections goes into RAM, load LMA copy after code */ |
1 | .bss : |
1 | /* User_heap_stack section, used to check that there is enough RAM left */ |
然后再make就不会报错了
四、烧写stm32
1. jlink使用jtag烧写
直接上脚本吧,看着清晰一点
1 | !/bin/bash |
使用JLinkExe执行脚本进行烧录,命令行只需要执行命令bash flash.sh build/test.bin就可以烧到单片机里面,然后就能看到效果了

五、调试stm32
1. jlink使用jtag调试
想要调试需要在stm32cubemx上选好调试的方式,把引脚配置好,否则会调试不起来。这里使用jtag-5调试

然后在vscode里面安装一个插件Cortex-Debug,之后在生成一个launch.json,选择Cortex Debug

需要加一些东西的改动,配置gdb路径,二进制路径,设备型号,调试方式等

F5开始调试就好了

六、进一步开发
1. 系统滴答时钟
stm32配置完外部晶振时钟源后会有一个默认的时钟中断用于记录tick,这个时钟是固定的1ms(暂时没找到不修改代码调整的方式)。HAL库中systick的配置
1 | // Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal.c |
可以看到时钟被调整为1ms,传入HAL_SYSTICK_Config的是计数器的值,计数到达这个值触发中断。算一下
- SystemCoreClock是核心时钟的频率,也就是计数器计数频率,如果什么都不除,这里就会造成1s的中断计数(8MHz计数8M,就是1s一次)
- 除以1000就是代表搞1ms中断
- uwTickFreq定义如下,代表真实想要的中断频率,默认就是1KHZ也就是除以1,还是1000,对应的正好是1ms中断
1 | // Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal.h |
- 系统时钟中断触发函数定义为
SysTick_Handler
1 | // Core/Src/stm32f1xx_it.c |
这个HAL_IncTick是个__weak定义的,说明可以用户自定义,从里面的定义可以看出,这个uwTick保持ms级别计数。
- uwTickFreq = 1,1ms,uwTick+1
- uwTickFreq = 10,10ms,uwTick+10
uwTick和uwTickFreq对外暴露了,定义在这里
1 | // Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal.h |
我们可以重写这个HAL_IncTick作为我们自己的定时器处理函数,可以从一个1ms定时器搞出很多个定时器。
1 | volatile unsigned long jiffies = 0; |
虽然uwTick作为ms计数很不错,这里搞个好玩的,把linux的jiffies搞进来使用,单独记录一个。主要是想要应用层代码和HAL完全隔离,不想要太多依赖HAL库,这样好移植
2. uart串口日志打印
滴答时钟配置完了,现在调试手段还是只能通过单步断点,断点调试太麻烦也太不稳定。参考linux内核的方式将日志加到串口上,通过串口输出一下日志。
串口的配置很简单,这里使用UART1,主要是因为引脚比较好接线。

使用异步模式UART,同步模式代表的是USART会多一个clk的线。不配置中断,只是为了打印日志所以不搞太复杂,中断还要考虑实现buffer来处理缓冲区问题,可以但没必要,简单最好。
配置完就可以生成代码了,由于我们没开启中断,而且用来打日志,就只需要一个函数HAL_UART_Transmit。工程生成出来之后,可以看到main.c中有了对应代码。
1 | // Core/Src/main.c |
可以看到,配置很简单。但是我们是要搞日志输出的,写一下日志输出函数,分为log.c和log.h
1 | // log.h |
对应源文件如下
1 | // log.c |
实现很简单,在主函数里面使用一下LOG_INFO("hello, main!")就可以从串口输出日志了,开发板本身有232芯片直接把uart1转成usb了,插线到电脑上就可以使用了。
linux读取串口信息
我使用的是ch340的串口转USB的东西,CH340系列在linux上需要看看设备有没有驱动,没有需要下载
1 | => modinfo ch341 |
下载地址在 https://www.wch.cn/download/CH341SER_LINUX_ZIP.html
驱动没问题后,可以从dmesg中看到usb插上后映射到/dev/ttyUSBx,就可以使用minicom来进行串口读写了,打开命令如下
1 | -D /dev/ttyUSB0 打开设备是/dev/ttyUSB0 |
minicom快捷键
Ctrl+a,x: 退出Ctrl+a,z: 打开帮助面板Ctrl+a,c: 清屏Ctrl+a,w: 启禁用自动换行Ctrl+a,e: 启禁用输入显示
3. pwm呼吸灯
七、知识说明
1. usart和uart
- UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)
- USART(Universal Synchronous/Asynchronous Receiver/Transmitter,通用同步/异步收发器)
usart的同步模式会多一个clk线,CLK线上的时钟信号通常由主设备(通常是发送方)产生,并通过此线传输给从设备(通常是接收方)。主设备会通过改变CLK线的时钟频率或周期来控制数据的传输速率和格式。
2. gpio
2.1. gpio mode
- Output Push-Pull:
- 工作原理:在这种模式下,GPIO引脚既有一个上拉电阻,也有一个下拉电阻。当输出为高电平时,上拉电阻将引脚拉高到电源电压;当输出为低电平时,下拉电阻将引脚拉低到地线电压。
- 应用场景:这种模式是最常见的GPIO输出模式,因为它提供了稳定的电平输出和快速的切换速度。
- 优点:可以快速切换高电平和低电平,并且由于有内部上拉或下拉电阻的存在,即使在外部条件导致引脚浮动时,也能快速恢复到稳定状态。
- Output Open Drain:
- 工作原理:在这种模式下,GPIO引脚没有内部的上拉电阻,只有下拉电阻。当输出为高电平时,需要外部电路(如上拉电阻)来维持高电平;而当输出为低电平时,引脚会通过内部的下拉电阻直接连接到地线。
- 应用场景:这种模式常用于需要与其他系统或模块进行线与(line-to-line)通信的场合,如I2C或SPI通信。在这种通信中,多个设备共享数据线,而Open Drain模式允许多个设备在数据线上同时处于高电平状态(通过外部上拉电阻),但只有一个设备可以处于低电平状态。
- 优点:适合于多设备共享数据线的场景,并且可以更灵活地与其他系统进行通信协议的交互。
2.2. gpio pull-up/pull-down
主要用于输入模式,当没有外部输入时,引脚的默认状态

