LV16-03-STM32基础知识

本文主要是STM32基础知识的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
  • 通用
分类 网址说明
官方网站https://www.arm.com/ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.st.com/content/st_com/zh.htmlST官方网站,在这里我们可以找到STM32的相关文档
https://www.stmcu.com.cn/意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档
http://elm-chan.org/fsw/ff/00index_e.htmlFatFs文件系统官网
教程书籍《ARM Cortex-M3权威指南》ARM公司专家Joseph Yiu(姚文祥)的力作,中文翻译是NXP的宋岩
《ARM Cortex-M0权威指南》
《ARM Cortex-M3与Cortex-M4权威指南》
开发论坛http://47.111.11.73/forum.php开源电子网,正点原子的资料下载及问题讨论论坛
https://www.firebbs.cn/forum.php国内Kinetis开发板-野火/秉火(刘火良)主持的论坛,现也做STM32和i.MX RT
https://www.amobbs.com/index.php阿莫(莫进明)主持的论坛,号称国内最早最火的电子论坛,以交流Atmel AVR系列单片机起家,现已拓展到嵌入式全平台,其STM32系列帖子有70W+。
http://download.100ask.net/index.html韦东山嵌入式资料中心,有些STM32和linux的相关资料也可以来这里找。
博客参考http://www.openedv.com/开源网-原子哥个人博客
http://blog.chinaaet.com/jihceng0622博主是原Freescale现NXP的现场应用工程师
cortex-m-resources这其实并不算是一个博客,这是ARM公司专家Joseph Yiu收集整理的所有对开发者有用的官方Cortex-M资料链接(也包含极少数外部资源链接)
  • STM32
STM32STM32 HAL库开发实战指南——基于F103系列开发板野火STM32开发教程在线文档
STM32库开发实战指南——基于野火霸道开发板野火STM32开发教程在线文档
  • SD卡
SD Association提供了SD存储卡和SDIO卡系统规范
点击查看相关文件下载
STM32F103xx英文数据手册STM32F103xC/D/E系列的英文数据手册
STM32F103xx中文数据手册STM32F103xC/D/E系列的中文数据手册
STM32F10xxx英文参考手册(RM0008)STM32F10xxx系列的英文参考手册
STM32F10xxx中文参考手册(RM0008)STM32F10xxx系列的中文参考手册
Arm Cortex-M3 处理器技术参考手册-英文版Cortex-M3技术参考手册-英文版
STM32F10xxx Cortex-M3编程手册-英文版(PM0056)STM32F10xxx/20xxx/21xxx/L1xxxx系列Cortex-M3编程手册-英文版
SD卡相关资料——最新版本有关SD卡的一些资料可以从这里下载
SD卡相关资料——历史版本有关SD卡的一些历史版本资料可以从这里下载,比如后边看的SD卡2.0协议
SD 2.0 协议标准完整版这是一篇关于SD卡2.0协议的中文文档,还是比较有参考价值的,可以一看

一、STM32简介

1. STM32与ARM

在2004年, ARM公司推出新一代Cortex内核后, ST公司抓住机遇,在很短的时间内就向市场推出了一系列的32位微控制器, 同时提供基于库的开发模式,加快用户研发周期。 STM32就是ST公司基于ARM Cortex-M系列内核设计的微控制器,专为高性能、低功耗、低成本场景设计。

STM32如今产品系列非常丰富, 主要为ARM Cortex-M内核系列MCU,也开始涉及ARM Cortex-A内核
系列MPU,如下图所示。

image-20230417223005421

按应用特性分类,可分为无线WB/WL系列、超低功耗L0/L1/L3/L5系列、主流G0/G4/F0/F1/F3系列、高性能F2/F4/F7/H7系列、全新的MP1系列。

STM32的处理器种类众多, 通过了解STM32的命令规范,可以了解整个STM32家族产品, 也方便以后
芯片选型, STM32 MCU系列命名规则(STM32 芯片命名规则,下图仅适用于 MCU) :

image-20230417223152126

其实每一款芯片都会有一个datasheet,也就是数据手册,在数据手册中会对芯片的命名有详细的说明,例如:

image-20230417223503942

总的来说ARM公司售卖Cortex-M系列的授权, 意法半导体(STMicroelectronics, ST)购买了该授权,生产了一些列STM32产品, 这些使用ARM技术的处理器,都习惯称为ARM处理器 。

2. STM32资料

这一部分主要是记录一下查找ARM和STM32官方文档的时候的一些相关链接。

2.1 ST官网

这个网站是ST的官方网站,所有的文档都可以从这里找,例如STM32相关的内容,我们就可以进入【产品】→【微控制器与微处理器】,然后我们点进入就可以看到相关的产品及相关资料啦。

image-20230408162122910

对于几乎所有的MCU,都会有数据手册参考手册,这两个手册必须下载。对于STM32系列的MCU, 通常还会有Cortex-M3/M4编程手册闪存编程手册、 勘误手册、官方固件包示例源码、官方固件包用户手册、 HAL库用户手册等,这些资料根据需求下载。 对于Cortex-M3/M4架构的的MCU, 如果需要深入了解Cortex-M3/M4内核,还需要Cortex-M3/M4权威指南,这个供我们深入学习。

2.1.1 搜索?

我们以STM32F103ZET6为例来看一看资料都在哪里:

image-20230420215031690

随后显示该搜索条件匹配的产品,如下图所示,可以看到只有一款产品符合搜索条件,点击该产品进入详细信息。

image-20230420215214197

2.1.2 文档?

在产品详细页,切换到“Documentation”标签, 可以看到该产品的所有文档手册,如下图所示为部分截图。

image-20230420215311634

后来网站都显示为中文了,如果打开都是英文页面的话,对应如下:

  • “ Product Specifications”(产品规格):也就是数据手册, 包含该系列MCU的整体描述、引脚描述、内存映射、电气特性、封装信息、订购信息等。在芯片选型、 原理图设计、 PCB设计、代码编程等开发环节,都会需要该文档;

  • “ Reference Manuals”(参考手册): 包含该系列MCU各外设寄存器的详细描述, 在代码编程时,需要找到对应外设章节, 仔细阅读;

  • “ Programming Manuals”(编程手册): 包含闪存编程手册和Cortex-M3内核编程手册, 一些资源是在内核里的,比如NVIC和SysTick, 此时在参考手册里找不到相关寄存器信息,就需要在Cortex-M3内核编程手册里查找;

  • “ Errata Sheets”( 勘误手册) : 包含该MCU内核、 外设资源的限制,解决方案等,在调试中出现了bug,可以看看该手册是否有类似记录;

2.1.3 开发工具?

除了这些文档外, 通常半导体芯片制造厂还会为MCU提供一些示例程序, ST也不例外。 在前面的产品详细页,切换到“ Tools & Software”标签,可以看到该产品的所有工具软件,比如各种硬件开发工具、软件开发工具、元件模块、评估工具软件、 MCU/MPU嵌入式软件、 产品评估工具、解决方案评估工具、工程服务、培训课程等 :

image-20230420215929780

比如我们可以在这里找到Keil MDK开发工具:

image-20230420224355525

还可以找到图形化开发工具STM32CubeMX,不过这里好像链接有些问题,点进去什么也没得,不过问题不大:

image-20230420224605541

2.1.4 固件包?

在上边开发工具里还会有一个 MCU & MPU Embedded Software 的栏目,也就是 MCU 及 MPU嵌入式软件:

image-20230420225046300

我们下载下来看一下,我们点进去后点击 Get Software就会来到下载的页面,选择和这个Get latest,然后会让我们登录,登录后就可以下载了。

image-20230420225341112

2.1.5 标准外设库?

之前学习的时候最开始学习的就是标准库开发,那库在哪里下载?

在这里【官网】→【工具与软件】→【嵌入式软件】→【微控制器软件】→【STM32微控制器软件】

image-20230420230207229

比如图中的标准库:STM32标准外设软件库 - 意法半导体STMicroelectronics

2.2 ST中文官网

这里会有大量的中文文档,好像也是ST的官方网站,我们想要找自己使用的芯片的相关文档的话,就可以从下图的地方寻找:

image-20230408162420343

那我们平时在网上看到的那些中文文档在哪里呢?我们可以进入首页后【设计资源】→【中英译文】

image-20230408162808406

然后在下边选择我们的单片机的系列,这样就可以找到一些相关的中文文档啦,不过这里似乎不太好用,因为我没找到我想找的中文文档。

image-20230408162919664

然后一看,右上角不是有搜索框嘛,这不试一下?我们以STM32F1为例,搜索结果如下:

image-20230408164622525

然后这里就会显示所有相关的中文的文档啦,下图中框的,我们打开看的话就会发现,这个文档就是我们平时看到的STM32中文参考文档的资料啦

image-20230408164718576

我们下载下来后,以正点原子提供的参考资料为准对比一下,看看是不是就是这一个,对比发现,是一样的,所以呢,我们完全可以从官网找到一些相关的中文文档。

image-20230408165138311

2.3 ST社区

有一些资料我们也可以去ST的社区查找:意法半导体STM32/STM8技术社区 - 提供最新的ST资讯和技术交流 (stmcu.org.cn)

二、最小系统

单片机最小系统是指用最少的电路组成单片机可以工作的系统,通常最小系统包含: 电源电路、时钟电路、复位电路、 调试/下载电路,对于STM32还需要启动选择电路。

1. 电源电路

不同的MCU的工作电压可能是不一样的,比如51单片机通常为5V,而STM32单片机通常为3.3V。 因此,通常需要查阅该MCU的数据手册才能确定工作电压和规范。

我们打开STM32F103xE数据手册(Datasheet - STM32F103xC, STM32F103xD, STM32F103xE - High-density performance line Arm®-based 32-bit MCU with 256 to 512KB Flash, USB, CAN, 11 timers, 3 ADCs, 13 communication interfaces) 找到“ 5.1.6 Power supply scheme ” 电源方案小结,可以看到如下图框图。

image-20230417225559820

根据数据手册的“ 5.3.1 General operating conditions”的表10 ,可得知上图中的各电压值大小。

image-20230417225902352

(1)VDD-VSS: 标准工作电压;电压范围: 2V3.6V; 从VDD1 ~ VDD11, VSS1 ~ VSS11共有11组;需要11个100nF和1个4.7uF去耦电容; 经过MCU内部Regulator电源管理, 为CPU、 存储器等供电;
(2)VDDA-VSSA: 模拟工作电压;电压范围: 2V
3.6V(未使用ADC) 2.4V3.6V(使用ADC);需要1个10nF和1个1uF去耦电容; 由VDDA输入,为ADC、 DAC等供电;
(3)VREF+-VREF-: 基准参考电压;电压范围: 2.4V
VDDA; 可以使用独立参考电压VREF(需10nF+1uF高频滤波电容), 也可使用VDDA输入,为ADC、 DAC等作基准参考电压;
(4)VBAT: RTC备用电源; 电压范围: 1.8V~ 3.6V; 通常使用纽扣电池外部供电,当主电源VDD掉电后,VBAT为实时时钟( Real-Time Clock, RTC) 和备份寄存器供电(此时功耗超低) ;

2. 时钟电路

MCU是一个集成芯片, 由非常复杂的数字电路和其它电路组成, 需要稳定的时钟脉冲信号才能保证正常工作。 时钟如同人体内部的心脏一样,心脏跳动一下, 推动血液流动一下。时钟产生一次, 就推动处理器执行一下指令。除了CPU,芯片上所有的外设(GPIO、 I2C、 SPI等)都需要时钟,由此可见时钟的重要性。

芯片运行的时钟频率越高,芯片处理的速度越快,但同时功耗也越高。为了功耗和性能兼顾,微处理器一般有多个时钟源,同时还将时钟分频为多个大小,适配不同需求的外设。

STM32的时钟树我们需要在STM32参考手册(STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm®-based 32-bit MCUs - Reference manual)中查找,我们打开手册到 “ 7.2 Clocks ”,则有如下时钟树:

image-20230417230846857

可以看到一共有四个时钟源:

(1)HSI(High Speed Internal clock signal):

HSI是内部的高速时钟信号,频率8MHz。因为是内部提供,可以降低成本,缺点是精度较差。

(2)HSE(High Speed External clock signal):

HSE是外部的高速时钟信号,需要外部电路晶振,输入频率范围要求为4-16MHz。因为需要外部电路提供,成本会增加,但精度较好。

(3)LSE(Low Speed External clock signal):

LSE是外部的低速时钟信号,需要外部电路晶振,输入频率范围要求为32.768KHz。 一般用于RTC实时时钟。

(4)LSI(Low Speed Internal clock signal):

LSI是内部的低速RC振荡器,频率40KHz。一般用于看门狗、 RTC实时时钟等。对于STM32F103系列的MCU, 都需要一个高速时钟和一个低速时钟,而这两个时钟可以选择使用内部时钟源节约成本,也可以选择外部时钟源输入提高精度。 如果使用内部时钟源,则无需设计外部电路,反之,则需要时钟电路。

继续查看STM32数据手册的“ 5.3.6 External clock source characteristics ”,可以看到下图所示的外部时钟输入参考电路。

  • HSE 典型应用(8MHz)

对于HSE,当晶振为8MHz时, CL1和CL2的容值范围为5pF~25pF。 REXT用于产生负反馈,保证放大器工作在高增益的线性区, 同时也起到限流作用,通常在兆欧级,具体由晶振决定。

image-20230417231317291
  • LSE 典型应用(32.768KHz)

对于LSE,当晶振为32.768KHz时, CL1和CL2的容值范围为5pF~15pF。之所以选择32.768KHz,是因为
32768=215, 分频设置寄存器通常为2n的形式,这样经过15次分频就很容易得到1Hz的频率。

image-20230417231355101

3. 复位电路

嵌入式系统中, 由于外界环境干扰, 难免出现程序跑飞或死机, 这时就需要复位让MCU重新运行。在打开STM32F103xE数据手册(Datasheet - STM32F103xC, STM32F103xD, STM32F103xE - High-density performance line Arm®-based 32-bit MCU with 256 to 512KB Flash, USB, CAN, 11 timers, 3 ADCs, 13 communication interfaces)中查找,我们打开手册到 “ 5.3.15 NRST pin characteristics ”,则有如下复位电路:

image-20230417231603407

该电路将一个按键接在了NRST引脚,一旦按键按下,NRST就会接地,拉低NRST,实现复位 。

4. 调试/下载电路

不同的MCU,调试/下载的方式可能不一样。比如51系列单片机,使用串口下载程序,同时也使用仿真调试。 对于STM32, 可以使用串口下载程序,也能使用串口打印进行简单调试,但STM32支持更高效的JTAG( Joint Test Action Group) 调试接口和SWD( Serial Wire Debug) 调试接口。

STM32 的 JTAG 与 SWD 的接口共用, 如果下载器支持 SWD 调试模式, 则接上 JTAG 的时候同样可以使用 SWD模式。 常见的支持 SWD 模式的下载器有 JLINK V7/V8 和 ST LINK 等。

所涉及的引脚我们也可以查看STM32的参考手册(STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm®-based 32-bit MCUs - Reference manual)的“ 9.3.5 JTAG/SWD alternate function remapping ”一节:

image-20230417231931946

关于更加详细的内容可以查看STM32参考手册(STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm®-based 32-bit MCUs - Reference manual)的 “ 31 Debug support (DBG) ” 一节,有更为详细的说明。

一般设计原理图如下:

image-20230417232058730

4.1 JTAG协议简介

JTAG(Joint Test Action Group,联合测试行动小组)是一种国际标准测试协议(IEEE 1149.1兼容),主要用于芯片内部测试。可以对 FLASH
等器件进行程序的编程和烧写。 标准的 JTAG 接口是模式选择(TMS)、 时钟(TCK)、 数据输入(TDI)、 数据输出(TDO) 四线制接线。 它可以对多个串联在同一个接口上的器件分别进行测试。 现在多数的高级器件都支持JTAG协议,如ARM、DSP、FPGA器件等。相关JTAG引脚的定义为:

引脚定义 说明
TMS 模式选择,TMS用来设置JTAG接口处于某种特定的测试模式;
TCK 时钟输入
TDI 数据输入,数据通过TDI引脚输入JTAG接口;
TDO 数据输出,数据通过TDO引脚从JTAG接口输出;

4.2 SWD协议简介

串行调试(Serial Wire Debug),与 JTAG 相比,SWD只要两根线,分别为:SWCLK和SWDIO,减少了对单片机GPIO口的占用:

引脚定义 说明
SWDIO 串行数据线,用于数据的读出和写入
SWDCLK 串行时钟线,提供所需要的时钟信号

SWD 模式比 JTAG 在高速模式下面更加可靠。 在大数据量的情况下面 JTAG 下载程序会失败, 但是 SWD 发生的几率会小很多。基本使用 JTAG 仿真模式的情况下是可以直接使用 SWD 模式的, 只要仿真器(仿真器是啥?后边写STM32工具相关的时候会有笔记说明)支持。

5. 启动选择电路

不同的MCU, 启动的方式的种类可能不一样。比如51系列单片机, 只能从内置存储器读取数据启动,因此没有启动选择的必要。

对于STM32, 可以从内置存储器启动(默认) ,可以从系统存储器(用于从USART1下载程序),可以从内部SRAM启动(调电消失,可用于调试) , 出现多个启动方式, 就需要启动选择。STM32通过BOOT1和BOOT2引脚的电平组合进行启动选择这一部分我们可以查看STM32的参考手册(STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm®-based 32-bit MCUs - Reference manual)的“ 3.4 Boot configuration ”。

image-20230417232300976

(1)BOO1为任意, BOOT0为0,开发板上电, MCU将从内部主存储器读取数据启动,是最常用的启动方式。

(2)BOO1为0, BOOT1为1,开发板上电, MCU将从系统存储器读取数据启动, 在系统存储器里面厂家烧写的串口下载程序, 此时可以通过USART1烧写新程序到主存储器。

(3)BOO1为1, BOOT1为1,开发板上电, MCU将直接从内部SRAM启动,SRAM的烧写次数寿命比Flash更多,可用于调试。

通常,我们只使用主存储器启动即可。 从系统存储器启动,实现从串口下载程序也逐渐被淘汰, STM32的高端MCU已经不支持该方式下载。从SRAM启动也没什么必要,目前Flash的烧写寿命次数也远远超过用户实际烧写次数。

三、嵌入式C语言

其实之前就学习过C预言了,语法什么的在嵌入式中都是一模一样的,就是有些数据类型还有相关操作可以再熟悉一下。

1. STM32支持的数据类型

以STM32F103ZE这一款芯片为例,这是一块32bit的MCU,基本数据类型在此款芯片中的数据长度,以及在HAL库函数中的定义( stdint.h文件中的定义,采用C99标准)如下图:

image-20230418221356751

建议在开发过程中使用库定义的数据类型, 来定义变量或函数, 比如unsigned char a, 使用uint8_t a。

2. 位运算

2.1 运算符

这个操作我们在对STM32进行编程的时候会将常用到。位运算是指二进制位之间的运算。在嵌入式系统设计中, 常常要处理二进制的问题,例如将某个寄存器中的某一个位置1或者置0, 将数据左移5位等。常用的位运算符如表 下表:

序号 运算符 含义 序号 运算符 含义
(1) & 按位与 (2) | 按位或
(3) ~ 按位取反 (4) << 左移
(5) >> 右移 (6) ^ 按位异或
  • (1)按位与运算符( & )

参与运算的两个操作数,每个二进制位进行“与” 运算,若两个都为1,结果为1,否者为0。

1
1011 & 1001 = 1001

第一位都为1,结果为1;第二位都为0,结果为0;第三位一个为1,一个为0,结果为0;第四位都为1,结果为1。 最后结果为1001。

  • (2)按位或运算符( | )

参与运算的两个操作数,每个二进制位进行“ 或”运算,若两个都为0,结果为1,否者为1。

1
1011 | 1001 = 1011

第一位都为1,结果为1;第二位都为0,结果为0;第三位一个为1,一个为0,结果为1;第四位都为1,结果为1。最后结果为1011。

  • (3)按位取反运算符( ~ )

按位取反运算符用于对一个二进制数按位取反。

1
~1011 = 0100

第一位为1, 取反为0;第二位为0, 取反为1;第三位为1, 取反为0,结果为1;第四位为1, 取反为0。最后结果为0100。

  • (4)(5)左移( << )和右移( >> ) 运算符

左移( << )运算符用于将一个数左移若干位,右移( >> )运算符用于将一个数右移若干位。

1
2
3
unsigned char val = 1011 1001
val << 3 = 1011 1001 << 3 = 1100 1000
val >> 3 = 1011 1001 >> 3 = 0001 0111

若val=val<<3,表示val左移3位,然后赋值给val,左移过程中,高位移出去后被丢弃, 低位补0,最后val结果为11001000;若val=val>>3,表示val右移3位,然后赋值给val, 右移过程中, 低位移出去后被丢弃, 高位补0,最后val结果为00010111。

  • (6)按位异或运算符

简单来说就是相同为1不同为0,经过三次异或可以交换两个数的值。

1
2
3
4
5
6
a = 0x3 = 0011
b = 0x5 = 0110

a = a ^ b = 0011 ^ 0110 = 1010 = 0xa;
b = a ^ b = 1010 ^ 0110 = 0011 = 0x3;
a = a ^ b = 1010 ^ 0011 = 0110 = 0x5;

2.2 清0或置1

在嵌入式中,经常使用位运算符实现清0或置1。

例如, MCU的ODR寄存器控制引脚的输出电平高低,寄存器为32位, 每位控制一个引脚的电平。假设需要控制GPIOB的1号引脚输出电平的高低,设置该寄存器第0位为1,输出高电平,设置该寄存器第0位为0,输出低电平。

1
2
3
#define GPIOB_ODR (*(volatile unsigned int *)(0x40010C0C))
GPIOB_ODR &= ~(1<<0);
GPIOB_ODR |= (1<<0);

第1行: 使用#define定义了GPIOB_ODR 对应的内存地址为0x40010C0C。 该地址为MCU的ODR寄存器地址。

第2行: GPIOB_ODR &= (1<<0)实际是GPIOB_ODR = GPIOB_ODR & ~(1<<0), 先将GPIOB_ODR和(1<<0)的进行与运算,运算结果赋值给GPIOB_ODR。 1<<0 的值为 00000000 00000000 00000000 00000001,
再取反为11111111 11111111 11111111 11111110, 则GPIO_ODR的第0位和0与运算, 结果必为0,其它位和1
运算,由GPIO_ODR原来的值决定结果。这就实现了,只将GPIO_ODR的第0位清0,其它位保持不变的效果,实现了单独控制对应引脚电平输出低。

第3行: GPIOB_ODR |= (1<<0)实际是GPIOB_ODR = GPIOB_ODR | (1<<0),先将GPIOB_ODR和(1<<0)的进行或运算,运算结果赋值给GPIOB_ODR。 1<<0的值为00000000 00000000 00000000 00000001,则GPIO_ODR的第0位和0或运算,结果必为1,其它位和0运算,由GPIO_ODR原来的值决定结果。这就实现了,只将GPIO_ODR的第0位置1,其它位保持不变的效果,实现了单独控制对应引脚电平输出高。

3. 非的使用

这个我们在编程的过程中也经常用到,非的结果只有两种,就是0和非0.

1
2
3
!0   ==> 1
!1 ==> 0
!100 ==> 0

4. __weak与__attribute__((weak))

为什么要了解这个?因为后边要用HAL库进行开发,HAL库里边好多好多这种符号,这里还是要了解一下的。前边学习C语言的时候,有专门学习过 __attribute__,具体可以看《LV01-17-C语言-attribute机制》。注意这一小节的笔记是后边补充过来的,由于MDK可以很好的看到代码生成的汇编,调试的时候可以看到代码和汇编的对应关系,所以这里用了后边创建的HAL库空工程。注意把工程的调试方式改成软件仿真,不改好像也没啥问题,不影响:

image-20230501164207687

4.1 两者关系

GNU 的编译器(gcc)扩展了一个关键字 __attribute__,通过该关键字,用户可以在声明时指定特殊的属性,使用时该关键字后跟双括号内的属性,例如:__attribute__((属性名字))。属性名字都是定义好的,weak 属性就是其中之一:__attribute__((weak))。

在 ARM 编译器(armcc)中,支持和 GCC 相同的关键字 __attribute__,使用方式也基本相同,如下:

1
2
__attribute__((attribute1, attribute2, ...))            // 例如:void * Function_Attributes_malloc_0(int b) __attribute__((malloc));
__attribute__((__attribute1__, __attribute2__, ...)) // 例如:static int b __attribute__((__unused__));

除此之外,ARM 编译器(armcc)还扩展了一个关键字 __weak,例如:__weak void f(void); 或者 __weak int i;。ARM 的汇编器(armasm)以另一种方式 [WEAK] 支持该特性。在许多源码中,经常通过宏定义的形式来定义关键字,例如 上面linux 中的 __weak 就是 宏定义的 __attribute__((weak))

4.2 强弱符号

在 GCC 中,被 __attribute__((weak)) 修饰的符号,称之为 弱符号(Weak Symbol)。例如:弱函数、弱变量;没有 __attribute__((weak)) 修饰的符号被称为强符号。在 ARM 中,没有弱符号和强符号这种叫法,只有个弱引用(Weak References) 和 非弱引用(non-weak reference ) 、 弱定义(Weak definitions) 和 非弱定义(non-weak definition)。需要注意的是编译器和汇编器都可以输出弱符号。

  • 非弱引用

非弱引用就是我们平常使用的对于非弱函数或者弱变量的引用。如果链接器无法在到目前为止已加载内容中解析对正常非弱符号的引用问题,则 它会尝试通过在库中找到符号 来解决此问题:如果找不到此类引用,则链接器将报告错误。如果解析了这样的引用,则从入口点可以通过至少一个非弱引用来访问的节区被标记为已使用。这样可以确保链接器不会将该节作为未使用的节删除。 每个非弱引用都必须通过一个定义来解决。 如果有多个定义,则链接器将报告错误。

  • 弱引用

引用弱声明的函数或者变量的引用即为弱引用。 链接器不会从库中加载对象来解析弱引用。仅当由于其他原因在镜像中包含了定义时,它才能解析弱引用。弱引用不会导致链接器将包含定义的节区标记为已使用,因此链接器可能会将其标记为未使用而删除。

4.3 __weak可以用在哪?

__weak 关键字可以应用于函数和变量的声明以及函数定义。

4.3.1 声明

__weak 可以用于函数声明或者变量的声明。对于声明,此存储类指定一个 extern 对象声明,即使该对象不存在,对于该声明的引用也不会导致链接器对未解析的引用(找不到定义的引用)当做错误来处理。

如果在当前编译单元中可以找到 __weak 声明定义,则会用找到的定义替换 __weak 引用;对于找不到定义 __weak 的声明(函数或变量),编译器做如下处理:

  • 引用被解析为分支连接指令 BL,找不到定义 __weak 的声明(函数或变量)等效于将被引用的分支为 NOP。

  • 最后直接将引用替换为 NOP 指令。

注意:必须是在当前编译单元,不再当前编译单元的没有意义(例如 func1 在 main.c 中只有__weak 声明,但是没有定义)。具体看下图的测试代码(这里):

image-20230501170931048

如上图所示,func1使用了__weak定义,也使用了__weak声明,并且在 main.c 中重写了,相当于在main.c中重新进行了定义,那么就会用重新进行的定义来替换__weak引用,所以引用被解析为BL,被引用分支就是func2函数,所以最终会跳转到main.c中定义的func2函数执行,,而func1并未使用__weak定义,但是月使用了__weak声明,然后在main.c中也并未重定义,所以直接被替换为NOP了。

【注意】我经过测试得出的结论(MDK v5.29.0):

(1)__weak声明函数,若定义时使用了__weak来修饰,那么这个函数就是一个弱函数,可以被重写,并且实际运行时不会被调用;但是定义时若是没有使用__weak,此时函数会被识别为弱函数,但是,无法重写,重写的话会报重定义错误,并且实际运行时也不会被运行。

(2)__weak定义函数,若使用__weak来修饰声明函数,那么这个函数就是一个弱函数,可以被重写,实际运行时此弱函数不会被调用;若是没有使用__weak来修饰声明函数,那么这个函数会被识别成非弱函数,但是,此函数依然可以重写,若是重写了这个函数,具体最终调用的我测过了,是重写的那一部分代码,若是没有重写,那么就会调用__weak定义的那一部分代码。

4.3.2 定义

用 __weak 定义的函数弱输出其符号。弱定义的函数的行为类似于正常定义的函数,除非将同名的非弱定义的函数链接到同一镜像中。 如果在同一镜像中同时存在非弱定义函数和弱定义函数,则对该函数的所有调用都会解析为调用非弱函数,否则直接使用弱定义的函数(与上面的若声明不同)。

上边什么意思呢?简单来说意思就是,我们的工程中,存在一个弱定义函数,我们要是没有重定义这个函数,那么链接的时候会使用弱定义的函数,若是重写了弱定义的函数,那么我们重写的非弱定义函数就会覆盖掉弱定义的函数,HAL库中,我们重写一些函数也是这个道理。

如果想要使用多个弱定义,则除非使用链接器选项 –muldefweak,否则链接器会生成一条错误消息。在这种情况下,链接器随机选择一个供所有调用来使用。使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//=============================================
/* weak_test.h !!!注意所在文件不同!!! */
void func1(void);
void func2(void);
//=============================================
/* weak_test.c !!!注意所在文件不同!!! */
void func1(void)
{
func2(); /* 这里将替换为 main.c 中的 func2 */
}

__weak void func2(void) /* 弱定义 */
{

}
//=============================================
/* main.c !!!注意所在文件不同!!! */
void func2(void)
{

}

int main (void)
{
func2();
}

4.4 __weak使用的限制

(1)函数或变量不能在同一编译中同时弱和非弱地使用。

1
2
3
4
5
6
7
8
9
10
11
12
void func(void);
void g()
{
func(); /* 非弱函数引用 */
}

__weak void func(void);
void h()
{
func(); /* 弱函数引用 */
}

(2)不能在定义函数或变量的同一编译中使用弱函数或弱变量,如下将导致编译错误(正确的使用方式参考上面的使用示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
/* weak_test.c 如下同一文件中的定义及使用将报错 */
__weak void func(void);

void h()
{
func();
}

void func()
{

}

(3)弱函数不能是内联函数

4.5 __attribute__((weak))用在哪?

__attribute__关键字使您可以指定变量或结构字段,函数和类型的特殊属性(与具体属性)。该关键字的作用与 __weak 的作用基本是一样的,在使用时有些不同,此外在某些情况下,编译的处理也有些区别。

4.5.1 声明

这个参数是 GUN 编译器的一个扩展,ARM 编译器也支持该关键字。__attribute__((weak)) 可以声明弱变量,并且其声明方式与 __weak 相比更加灵活。除了 __weak 的声明方式,我们还可以用

1
extern int Variable_Attributes_weak_1 __attribute__((weak));

__attribute__((weak)) 可以声明弱函数,其声明方式与 __weak 相比更加灵活。除了 __weak 的声明方式,我们还可以用:

1
extern int Function_Attributes_weak_0 (int b) __attribute__((weak));

任何包含了 __attribute__((weak)); 声明的文件的中的同名函数定义,都将被当做弱函数。

image-20230501174643348

4.5.2 定义

用 __attribute__((weak))定义的函数弱输出其符号(与 __weak相同)。其使用方式有以下两种:

1
2
3
4
5
6
7
8
9
10
__attribute__((weak)) void func1(void)
{
printf("Weak func1!\r\n");
}
/* 或者 */
void __attribute__((weak)) func1(void)
{
printf("Weak func1!\r\n");
}

4.6 两者的区别

网上找到的区别如下,但是我对这两个区别持怀疑态度,因为在前边__weak的声明一小节的例子中,显然使用func1并未使用__weak定义,但是使用了__weak来声明,最终也被识别为弱函数了:

(1)__weak 和 __attribute__((weak)) 在声明和定义的时候,其所处的位置有不同。

(2)__weak 仅在函数定义中使用时才会生成弱函数。而在任何情况下(声明和定义) __attribute__((weak)) 都会生成弱函数,无论是用于函数定义还是用于函数声明中!

四、STM32开发

1. 开发方式

这篇笔记是已经对STM32有了一定的了解后才写的,其实STM32的开发大概有四种方式

  • (1)汇编,强的一批,我们可以直接使用内部寄存器和STM32的寄存器来使用汇编开发,难度可想而知。
  • (2)C语言+寄存器开发,这种相对容易,毕竟C语言我们都学过,无非是寄存器超级多。
  • (3)C语言+标准库,这个是我入门STM32的时候最先使用的库,一般都用这个。
  • (4)C语言+HAL库,HAL库配合STM32CubeMX软件使用,由于图形化的配置界面,这个HAL库据说是ST以后主推的库。

2. 库开发与寄存器开发的关系

一般我们都是从学 51 单片机开发转而学习 STM32 开发,习惯了 51 单片机的寄存器开发方式,突然一个 ST 官方库摆在面前会一头雾水,不知道从何下手。

下那么STM32 固件库到底是什么,和寄存器开发有什么关系?其实一句话就可以概括:固件库就是函数的集合,固件库函数的作用是向下负责与寄存器直接打交道,向上提供用户函数调用的接口(API)。

在 51 的开发中我们常常的作法是直接操作寄存器,比如要控制某些 IO 口的状态,我们直接操作寄存器:

1
P0 = 0x11;

而在 STM32 的开发中,我们同样可以操作寄存器:

1
GPIOx->BRR = 0x0011;

这种方法当然可以,但是这种方法的劣势是我们需要去掌握每个寄存器的用法,才能正确使用STM32,而对于 STM32 这种级别的 MCU,数百个寄存器记起来又是谈何容易。于是 ST(意法半导体)推出了官方固件库,固件库将这些寄存器底层操作都封装起来,提供一整套接口(API)供开发者调用,大多数场合下,我们不需要去知道操作的是哪个寄存器,我们只需要知道调用哪些函数即可 ,比如上面的控制 BRR 寄存器实现电平控制,官方库封装了一个函数:

1
2
3
4
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
GPIOx->BRR = GPIO_Pin;
}

这个时候我们不需要再直接去操作 BRR 寄存器了,只需要知道怎么使用 GPIO_ResetBits()这个函数就可以了。 在对外设的工作原理有一定的了解之后,再去看固件库函数,基本上函数名字能告诉我们这个函数的功能是什么,该怎么使用, 这样是不是开发会方便很多?

任何处理器,不管它有多么的高级,归根结底都是要对处理器的寄存器进行操作。但是固件库不是万能的,如果想要把 STM32 学透, 光读 STM32 固件库是远远不够的。还是要了解一下 STM32 的原理,而这些原理了解了,在进行固件库开发过程中才可能得心应手游刃有余。

库函数的引入,大大降低了 STM 主控芯片开发的难度。 ST 公司为了方便用户开发 STM32芯片开发提供了三种库函数,从时间产生顺序是:标准库、 HAL 库和 LL 库。目前 ST 已经逐渐暂停对部分标准库的支持, ST 的库函数维护重点对象已经转移到 HAL 库和 LL 库上。

3. STM32 固件库与 CMSIS 标准

3.1  CMSIS的来历

STM32 固件库就是函数的集合,那么对这些函数有什么要求呢?这里就涉及到一个 CMSIS 标准的基础知识,这部分知识可以从《Cortex-M3 权威指南》中了解到,这里大概提一下。

STM32 和 ARM 以及 ARM7是什么关系?其实 ARM 是一个做芯片标准的公司,它负责的是芯片内核的架构设计,而 TI, ST 这样的公司,他们并不做标准,他们是芯片公司,他们是根据 ARM 公司提供的芯片内核标准设计自己的芯片。所以,任何一个做 Cortex-M3 芯片,他们的内核结构都是一样的,不同的是他们的存储器容量, 片上外设, IO 以及其他模块的区别。所以我们会发现,不同公司设计的 Cortex-M3 芯片他们的端口数量,串口数量,控制方法这些都是有区别的, 这些资源他们可以根据自己的需求理念来设计。同一家公司设计的多种 Cortex-m3 内核芯片的片上外设也会有很大的区别,比如 STM32F103RBT 和 STM32F103ZET,他们的片上外设就有很大的区别。我们可以通过《Cortex-M3 权威指南》中的一个图来了解一下:

image-20230420231930503

从上图可以看出,芯片虽然是芯片公司设计,但是内核却要服从 ARM 公司提出的 Cortex-M3内核标准了,理所当然,芯片公司每卖出一片芯片,需要向 ARM 公司交一定的专利费。既然大家都使用的是 Cortex-M3 核,也就是说,本质上大家都是一样的,这样 ARM 公司为了能让不同的芯片公司生产的 Cortex-M3 芯片能在软件上基本兼容,和芯片生产商共同提出了一套标准 CMSIS 标准(Cortex Microcontroller Software Interface Standard) ,翻译过来是“ARM Cortex™ 微控制器软件接口标准”。 ST 官方库就是根据这套标准设计的。这里我们来看看基于 CMSIS 应用程序基本结构:

image-20230420232215474

CMSIS 分为 3 个基本功能层:

(1)核内外设访问层: ARM 公司提供的访问,定义处理器内部寄存器地址以及功能函数。

(2)中间件访问层:定义访问中间件的通用 API,也是 ARM 公司提供。

(3)外设访问层:定义硬件寄存器的地址以及外设的访问函数。

从图中可以看出, CMSIS 层在整个系统中是处于中间层,向下负责与内核和各个外设直接打交道,向上提供实时操作系统用户程序调用的函数接口。如果没有 CMSIS 标准,那么各个芯片公司就会设计自己喜欢的风格的库函数,而 CMSIS 标准就是要强制规定,芯片生产公司设计的库函数必须按照 CMSIS 这套规范来设计。

3.2 CMSIS 标准

根据一些调查研究表明, 软件开发已经被嵌入式行业公认为最主要的开发成本,为了降低这个成本, ARM 与 Atmel、 IAR、 KEIL、 SEGGER 和 ST 等诸多芯片和软件工具厂商合作, 制定了一个将所有 Cortex 芯片厂商的产品的软件接口标准化的标准 CMSIS(Cortex Microcontroller Software Interface Standard)。下面来看 ARM 官方提供的 CMSIS 规范架构, 如图 :

image-20240113112812542

从图中可以看出这个标准分级明显, 从用户程序到内核底层实现做了分层。按照这个分级,HAL 库属于 CMSIS-Pack 中的“Peripheral HAL”层。CMSIS 规定的最主要的 3 个部分为:核内外设访问层(由 ARM 负责实现)、 片上外设访问层和外设访问函数(后面两个由芯片厂商负责实现)。ARM 整合并提供了大量的模版,各厂商根据自己的芯片差异修改模版, 这其中包括汇编文件 startup_device.s、 system_.h 和 system_.c 这些与初始化和系统相关的函数。

结合 STM32F1 的芯片来说,其 CMSIS 应用程序的简单结构框图,不包括实时操作系统和中间设备等组件,CMSIS 分级下的 stm32f1 的文件分布 结构如图 :

image-20240113113102109

一个简单的例子,我们在使用 STM32 芯片的时候首先要进行系统初始化, CMSIS 规范就规定, 系统初始化函数名字必须为 SystemInit,所以各个芯片公
司写自己的库函数的时候就必须用 SystemInit 对系统进行初始化。 CMSIS 还对各个外设驱动文件的文件名字规范化,以及函数名字规范化等等一系列规定。上一节讲的函数GPIO_ResetBits 这个函数名字也是不能随便定义的,是要遵循 CMSIS 规范的。至于 CMSIS 的具体内容就不多说了,网上资料很多。

4. 三种官方库

ST 先后提供了两套固件库:标准库和 HAL 库

STM32 芯片面市之初只提供了丰富全面的标准库,大大便利了用户程序开发,为广大开发板所推崇,同时也为 ST 积累了大量标准库用户。

大约到 2014 年左右, ST 在标准库的基础上又推出了 HAL 库。实际上, HAL 库和标准库本质上是一样的,都是提供底层硬件操作 API,而且在使用上也是大同小异。

据说 ST 官方之所以这几年大力推广 HAL 库,是因为 HAL 的结构更加容易整合 STM32Cube,而 STM32CubeMX 是 ST 这几年极力推荐的程序生成开发工具。所以后来新出的 STM32 芯片, ST 直接只提供 HAL 库。

那么是使用 HAL 库还是标准库好呢?其实 HAL库和标准库都非常强大,对于目前标准库支持的芯片采用标准库开发也非常方便实用。不需要纠结自己学的是 HAL 库还是标准库,无论使用哪种库,只要理解了 STM32 本质,任何库都是一种工具,使用起来都非常方便。学会了一种库,另外一种库也非常容易上手,程序开发思路转变也非常容易。

4.1 标准外设库

标准外设库(Standard Peripherals Library)是对 STM32 芯片的一个完整的封装,包括所有标准器件外设的器件驱动器, 是 ST 最早推出的针对 STM 系列主控的库函数。标准库的设计的初衷是减少用户的程序编写时间,进而降低开发成本。几乎全部使用 C 语言实现并严格按照“Strict ANSI-C”、 MISRA-C 2004 等多个 C 语言标准编写。但标准外设库仍然接近于寄存器操作,主要就是将一些基本的寄存器操作封装成了 C 函数。开发者仍需要关注所使用的外设是在哪个总线之上,具体寄存器的配置等底层信息。

image-20240113113729681

ST 为各系列提供的标准外设库稍微有些区别。例如, STM32F1x 的库和 STM32F3x 的库在文件结构上就有些不同,此外,在内部的实现上也稍微有些区别,这个在具体使用(移植)时,需要注意一下!但是,不同系列之间的差别并不是很大,而且在设计上是相同的。 STM32 的标准外设库涵盖以下 3 个抽象级别:

  • 包含位域和寄存器在内的完整的寄存器地址映射

  • 涵盖所有外围功能(具有公共 API 的驱动器)的例程和数据结构的集合。

  • 一组包含所有可用外设的示例,其中包含最常用的开发工具的模板项目。

关于更详细的信息,可以参考 ST 的官方文档《STM32 固件库使用手册中文翻译版》,文档中对于标准外设库函数命名、文件结构等都有详细的说明, 这里就不多介绍了。值得一提的是由于 STM32 的产品性能及标准库代码的规范和易读性以及例程的全覆盖性,使 STM32 的开发难度大大下降。 但 ST 从 L1 以后的芯片 L0、 L4 和 F7 等系列就没有再推出相应的标准库支持包了。

4.1.1 文件下载

我们下载一个标准库,我们来这个STM32标准外设库下载:STM32标准外设软件库 - 意法半导体STMicroelectronics

image-20230420233318141

4.1.2 标准库介绍

接下来我们来看一下目录结构,前面我们下载完解压,大概的目录结构如下所示:

image-20230421193945130

(1)Libraries 文件夹下面有 CMSIS 和 STM32F10x_StdPeriph_Driver 两个目录,这两个目录包含固件库核心的所有子文件夹和文件。其中CMSIS目录下面是启动文件 ,STM32F10x_StdPeriph_Driver 放的是 STM32 固件库源码文件。源文件目录下面的 inc 目录存放的是 stm32f10x_xxx.h 头文件,无需改动。 src 目录下面放的是 stm32f10x_xxx.c 格式的固件库源码文件。每一个.c 文件和一个相应的.h 文件对应。这里的文件也是固件库的核心文件,每个外设对应一组文件。Libraries 文件夹里面的文件在我们建立工程的时候都会使用到。

(2)Project 文件夹下面有两个文件夹。顾名思义, STM32F10x_StdPeriph_Examples 文件夹下面存放的 ST 官方提供的固件实例源码,在以后的开发过程中,可以参考修改这个官方提供的实例来快速驱动自己的外设,很多开发板的实例都参考了官方提供的例程源码。 STM32F10x_StdPeriph_Template 文件夹下面存放的是工程模板。

(3)Utilities 文件下就是官方评估板的一些对应源码,这个可以忽略不看。

(4)根目录中还有一个 stm32f10x_stdperiph_lib_um.chm 文件,直接打开可以知道,这是一个固件库的帮助文档,这个文档非常有用,只可惜是英文的,在开发过程中,这个文档会经常被使用到。

4.1.3 关键文件介绍

主要是Libraries 目录下面几个重要的文件:

  • CoreSupport 目录
image-20230421194328019

core_cm3.c 和 core_cm3.h 文件位于Libraries\CMSIS\CM3\CoreSupport 目录下面的,这个就是 CMSIS 核心文件,提供进入 M3 内核接口,这是 ARM 公司提供,对所有 CM3 内核的芯片都一样。我们永远都不需要修改这个文件。

  • DeviceSupport目录
image-20230421194453728

这个目录下面有三个文件: system_stm32f10x.c, system_stm32f10x.h 以及 stm32f10x.h 文件。

(1)system_stm32f10x.c 和对应的头文件 system_stm32f10x.h 文件的功能是设置系统以及总线时钟,这个里面有一个非常重要的 SystemInit()函数,这个函数在我们系统启动的时候都会调用,用来设置系统的整个时钟系统。

(2)stm32f10x.h 这个文件就相当重要了,只要做 STM32 开发,几乎时刻都要查看这个文件相关的定义。这个文件打开可以看到,里面非常多的结构体以及宏定义。 这个文件里面主要是系统寄存器定义申明以及包装内存操作, 对于这里是怎样申明以及怎样将内存操作封装起来的。

(3) DeviceSupport\ST\STM32F10x 同一级还有一个 startup 文件夹,这个文件夹里面放的文件是启动文件。在\startup\arm 目录下,我们可以看到 8 个 startup 开头的.s 文件。

image-20230421194755261

这里之所以有 8 个启动文件,是因为对于不同容量的芯片启动文件不一样。对于 103 系列,主要是用其中 3 个启动文件:

1
2
3
startup_stm32f10x_ld.s : 适用于小容量 产品
startup_stm32f10x_md.s : 适用于中等容量产品
startup_stm32f10x_hd.s : 适用于大容量产品

这里的容量是指 FLASH 的大小.判断方法如下:

小容量: FLASH ≤ 32K
中容量: 64K≤FLASH ≤ 128K
大容量: 256K ≤ FLASH

STM32F103ZET6和stm32F103RCT6芯片都属于大容量产品,所以我们的启动文件选择 startup_stm32f10x_hd.s,对于中等容量芯片请要选择 startup_stm32f10x_md.s 启动文件,小容量芯片选择 startup_stm32f10x_ld.s。启动文件到底什么作用,其实我们可以打开启动文件进去看看。启动文件主要是进行堆栈之类的初始化, 中断向量表以及中断函数定义。启动文件要引导进入 main 函数。 Reset_Handler中断函数是唯一实现了的中断处理函数,其他的中断函数基本都是死循环,启动文件我们后边会详细的去分析。

  • 其他几个文件

还有其他几个文件 stm32f10x_it.c,stm32f10x_it.h 以及 stm32f10x_conf.h 等文件。 stm32f10x_it.c 里面是用来编写中断服务函数,中断服务函数也可以随意编写在工程里面的任意一个文件里面。stm32f10x_conf.h 文件打开可以看到一堆的#include,这里建立工程的时候,可以注释掉一些不用的外设头文件。

4.2 HAL库  

HAL 是 Hardware Abstraction Layer 的缩写,即硬件抽象层。是 ST 为可以更好的确保跨STM32 产品的最大可移植性而推出的 MCU 操作库。这种程序设计由于抽离应用程序和硬件底层的操作, 更加符合跨平台和多人协作开发的需要。

HAL 库是基于一个非限制性的 BSD 许可协(Berkeley Software Distribution)而发布的开源代码。 ST 制作的中间件堆栈(USB 主机和设备库, STemWin)带有允许轻松重用的许可模式,只要是在 ST 公司的 MCU 芯片上使用,库中的中间件(USB 主机/设备库,STemWin)协议栈即被允许修改,并可以反复使用。至于基于其它著名的开源解决方案商的中间件( FreeRTOS,FatFs, LwIP 和 PolarSSL)也都具有友好的用户许可条款。

HAL 库是从 ST 公司从自身芯片的整个生产生态出发,为了方便维护而作的一次整合, 以改变标准外设库带来各系列芯片操作函数结构差异大、分化大、不利于跨系列移植的情况。相比标准外设库, STM32Cube HAL 库表现出更高的抽象整合水平, HAL 库的 API 集中关注各外设的公共函数功能,这样便于定义一套通用的用户友好的 API 函数接口,从而可以轻松实现从一个 STM32 产品移植到另一个不同的 STM32 系列产品。但由于封闭函数为了适应最大的兼容性, HAL 库的一些代码实际上的执行效率要远低于寄存器操作。但即便如此, HAL 库仍是 ST未来主推的库。

STM32Cube 目前几乎支持 STM32 全系列,我主要是使用的STM32F1系列,所以这里主要写STM32CubeF1 相关知识。如果使用的是其他系列的 STM32 芯片,请到ST 官网下载对应的 STM32Cube 包即可。

4.2.1 HAL库能做什么

使用标准库可以忽略很多芯片寄存器的细节,根据提供的接口函数快速配置和使用一个 STM32 芯片, 使用 HAL 库也是如此。 不论何种库, 本质都是配置指定寄存器使芯片工作在我们需要的工作模式下。 HAL 库在设计的时候会更注重软硬件分离。HAL 库的 API 集中关注各个外设的公共函数功能,便于定义通用性更好、更友好的 API 函数接口,从而具有更好的可移植性。 HAL 库写的代码在不同的 STM32 产品上移植,非常方便。

我们需要学会调用 HAL 库的 API 函数,配置对应外设按照我们的要求工作,这就是 HAL库能做的事。但是无论库封装得多高级,最终还是要通过配置寄存器来实现。所以我们学习 HAL库的同时,也建议同时学习外设的工作原理和寄存器的配置。

HAL 库还可以和 STM32CubeMX(图形化软件配置工具)配套一起使用,开发者可以使用该工具进行可视化配置,并且自动生成配置好的初始化代码,大大的节省开发时间。

4.2.2 文件下载

在哪下载?还是在官网:STM32CubeF1 - STM32Cube MCU包,用于STM32F1系列(HAL、底层API和CMSIS(CORE、DSP和RTOS)、USB、TCP/IP、File system、RTOS和Graphic - 附带在以下ST板上运行的示例:STM32 Nucleo、探索套件和评估板) - 意法半导体STMicroelectronics

网页登陆:st.com/content/st_com/en.html,在打开的页面中依次选择:“【Tools & Software】→【Ecosystem】→【STM32Cube】 →【新页面】→【Prodcut selector】, 如图

image-20240113114453925

在展开的页面中选择我们需要和固件,这展开“STM32CubeF1”即可看到我们需要的 F1 的安装包, 按下图操作, 在新的窗口中拉到底部,选择适合自己的下载方式, 注册帐号即可获取相应的驱动包。

image-20240113114632225

官网同时还未我们提供了一个git仓库:GitHub - STMicroelectronics/STM32CubeF1: STM32Cube MCU Full Package for the STM32F1 series - (HAL + LL Drivers, CMSIS Core, CMSIS Device, MW libraries plus a set of Projects running on all boards provided by ST (Nucleo, Evaluation and Discovery Kits))

通过这两种途径,我们都可以下载。

4.2.3 HAL库介绍

我们通过这种途径下载的固件包其实是HAL库,后边再介绍这个库与标准库的关系,现在我们看一下这个固件包的目录结构:

image-20230421200129597
  • “ Documentation”: 里面是固件包的详细说明文档;

  • “ Drivers”:Drivers 文件夹包含 BSP, CMSIS 和 STM32F1xx_HAL_Driver 三个子文件夹。

(1)BSP 文件夹 :也叫板级支持包,此支持包提供的是直接与硬件打 交道的 API,例如触摸屏, LCD, SRAM 以及 EEPROM 等板载硬件资源等驱动。 BSP 文件夹下面有多种 ST 官方 Discovery开发板, Nucleo开发板以及 EVAL 板的 硬件驱动 API 文件,每一种开发板对应一个文件夹。

(2)CMSIS 文件夹:Cortex微控制器软件接口标准( Cortex Microcontroller Software Interface Standard) ,包含STM32F1符合CMSIS标准的软件抽象层相关文件。文件夹内部文件比较多。主要包括 DSP 库(DSP_LIB 文件夹), Cortex-M 内核及其设备文件 (Include 文件夹), 微控制器专用头文件/启动代码/ 专用系统文件等(Device 文件夹)。

(3)STM32F1xx_HAL_Driver 文件夹:这个文件夹非常重要,它包含了所有的 STM32F1xx 系列 HAL 库头文件和源文件,也就是所有底层硬件 抽象层 API 声明和定义。它的作用是屏蔽了复杂的硬件寄存器操作,统一了外设的接口函数。该文件夹包含 Src 和 Inc 两个子文件夹,其中 Src 子文件夹 存放的是.c 源文件, Inc 子文件夹存放的是与之对应 的.h 头文件。每个.c 源文件对应一个.h 头文件。比如gpio相关的 API 的声明和定义在文 件 stm32f1xx_hal_gpio.h 和 stm32f1xx_hal_gpio.c 中。

  • “ Middlewares”:该文件夹下面有 ST 和 Third_Party 两个子文件夹。 ST 文件夹下面存放的是 STM32 相关的一些文件,包括 STemWin 和 USB 库等。Third_Party 文件夹是第三方中间件,这些中间价都是非常成熟的开源解决方案。

(1)ST 子文 件夹

文件夹 说明
STemWin 文件夹 ST提供的图形GUI库“ STemWin” 。
STM32_USB_Device_Library 文件夹 USB 从机设备支持包。
STM32_USB_Host_Library 文件夹 USB 主机设备支持包。

(2)Third_Party 子文件夹

文件夹 说明
FatFs 文件夹 FAT 文件系统支持包。采用的 FATFS 文件系统。
FreeRTOS 文件夹 FreeRTOS 实时系统支持包。
LwIP 文件夹 LwIP 网络通信协议支持包。
  • “ Projects”: 以ST官方公板为载体,提供的外设应用示例,比如GPIO、 UART、 ADC、 RTC等。这些实例工程是可以直接编译的,每个文件夹对应一个 ST 官方的 Demo板。每个工程下面都有一个 MDK-ARM子文件夹,该子文件夹内部会有名称为 Project.uvprojx 的工程文件,我们只需要点击它就可以
    在 MDK 中打开工程。 例如,下图就是Project目录下的一个UART的串口例程,我们双击下边的Project.uvprojx文件就可以打开这个工程。
image-20230421215634134
  • “ Utilities”:一些字体、日志、多媒体等杂项示例,日常使用的并不多。
  • 其它几个文件

文件夹中还有几个单独的文件, 用于声明软件版本或者版权信息, 我们使用 ST 的芯片已经默认得到这个软件的版权使用授权,可以简单了解一下各文件的内容,实际项目中我们一般不添加。

License.md:用于声明软件版权信息的文件。

package.xml: 描述固件包版本信息的文件。

Release_Notes.html:超文本文件,用浏览器打开可知它是对固件包的补充描述和固件版本更新的记录说明。

4.2.4 CMSIS 文件夹关键文件

我们来分析一下 CMSIS 文件夹:由命名可知, 该文件夹和前面提到的 CMSIS 标准是一致的, CMSIS 为软件包的内容制定了标准, 包括文件目录的命名和内容构成, 5.7.0 版本CMSIS 规定软件包目录如表:

image-20240113115532703

知道了 CMSIS 规定的组件及其文件目录的大概内容后,我们再来看看 ST 提供的 CMSIS文件夹,如上节提到的, 它的位置是“ STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS”。打开文件夹内容如下图所示, 可以发现它的目录结构完全按照 CMSIS 标准执行, 仅仅是作了部分删减。

image-20240113115609165

CMSIS 文件夹中的 Device 和 Include 这两个文件夹中的文件是我们工程中最常用到的。下面对这两个文件夹作简单的介绍:

  • (1)Device 文件夹
image-20240113120512319
  • (2)Include 文件夹

Include 文件夹存放了符合 CMSIS 标准的 Cortex-M 内核头文件。 对于 STM32F1 的工程,我们只要把我们需要的添加到工程即可,需要的头文件有: cmsis_armcc.h、 cmsis_armclang.h、 cmsis_compiler.h、 cmsis_version.h、core_cm3.h 和 mpu_armv7.h。这几个头文件,对比起来,我们会比较多接触的是 core_cm3.h。core_cm3.h 是内核底层的文件,由 ARM 公司提供,包含一些 AMR 内核指令, 如软件复位, 开关中断等功能。今后在需要的例程再去讲解其程序,现在要提到的是它包含了一个重要的头文件 stdint.h。

stdint.h 是从 c99 中引进的一个标准 C 库的文件。在 2000 年 3 月, ANSI 采纳了 C99 标准。 ANSI C 被几乎所有广泛使用的编译器(如: MDK、 IAR) 支持。多数 C 代码是在 ANSI C基础上写的。任何仅使用标准 C 并且不和硬件相关的代码, 在任意平台上用遵循 ANSI C 标准的编译器下能编译成功。 就是说这套标准不依赖硬件,独立于任何硬件,可以跨平台。stdint.h 可以在 MDK 安装目录下找到,如 MDK5 安装在 C 盘时,可以在路径:C:\Keil_v5\ARM\ARMCC\include 找到。 stdint.h 的作用就是提供了类型定义,其部分类型定义代码如下:

1
2
3
4
5
6
7
8
9
10
/* exact-width signed integer types */
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed __INT64 int64_t;
/* exact-width unsigned integer types */
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned __INT64 uint64_t;

4.2.5 关键文件介绍补充笔记

  • (1)HAL 库关键文件介绍如下表 :
文件 描述
sm32f1xx_hal.c 包含 HAL 通用 API(比如 HAL_Init,HAL_DeInit,HAL_Delay 等)。
stm32f1xx_hal.h HAL 的头文件,它应被客户代码所包含。
stm32f1xx_hal_conf.h HAL 的配置文件,主要用来选择使能何种外 设以及一些时钟相关参数设置。其本身应该 被客户代码所包含。
stm32f1xx_hal_def.h 包含 HAL 的通用数据类型定义和宏定义
  • (2)stm32f1xx_it.c/stm32f1xx_it.h 文件

stm32f1xx_it.h 中主要是一些中断服务函数的申明。stm32f1xx_it.h 中是这些中断服务函数定义,而这些函数定义除了 Systick 中断服务函数SysTick_Handler 外基本都是空函数,没有任何控制逻辑。一般情况下,我们可以去掉这两个文件,然后把中断服务函数写在工程中的任何一个可见文件中。

  • (3)stm32f1xx.h 头文件

头文件 stm32f1xx.h 是所有 stm32f1 系列的顶层头文件。使用 STM32F1 任何型号的芯片,都需要包含这个头文件。同时,因为 stm32f1 系列芯片型号非常多, ST 为每种芯片型号定义了一个特有的片上外设访问层头文件,比如 STM32F103系列, ST 定义了一个头文件 stm32f103xx.h,然后 stm32f1xx.h 顶层头文件会根据工程芯片型号,来选择包含对应芯片的片上外设访问层头文件。我们可以打开 stm32f1xx.h 头文件可以看到,里面有如下几行代码:

1
2
3
4
5
6
7
8
9
#if defined(STM32F100xB)
#include "stm32f100xb.h"
// ... ...
#elif defined(STM32F101xE)
#include "stm32f101xe.h"
// ... ...
#else
#error "Please select first the target STM32F1xx device used in your application(in stm32f1xx.h file)"
#endif

我们以 stm3f103 为例,如果定义了宏定义标识符 STM32F103xx,那么头文件 stm32f1xx.h 将会包含头文件 stm32f103xx.h。所以头文件 stm32f103xx.h 一定会被整个工程所引用。

  • (4)stm32f103xx.h 头文件

stm32f103xx.h 是 stm32f103 系列芯片通用的片上外设访问层头文件,只要我们进行 stm32f103 开发,就必然要使用到该文件。打开该文件我们可以看到里面主要是一些结构体和宏定义标识符。这个文件的主要作用是寄存器定义声明以及封装内存操作。

  • (5)system_stm32f1xx.c/system_stm32f1xx.h 文件

头文件system_stm32f1xx.h和源文件system_stm32f1xx.c主要是声明和定义了系统初始化函数 SystemInit 以及系统时钟更新函数SystemCoreClockUpdate。 SystemInit 函数的作用是进行时钟系统的一些初始化操作以及中断向量表偏移地址设置,但它并没有设置具体的时钟值,这是与标准库的最大区别,在使用标准库的时候, SystemInit 函数会帮我们配置好系统时钟配置相关的各个寄存器。在启动文件 startup_stm32f103xx.s 中会设置系统复位后,直接调用 SystemInit 函数进行系统初始化。 SystemCoreClockUpdate 函数是在系统时钟配置进行修改后,调用这个函数来更新全局变量 SystemCoreClock 的值,变量 SystemCoreClock 是一个全局变量,开放这个变量可以方便我们在用户代码中直接使用这个变量来进行一些时钟运算。

  • (6)stm32f1xx_hal_msp.c 文件

MSP,全称为 MCU support package,这里大家只需要知道,函数名字中带有 MspInit 的函数,它们的作用是进行 MCU 级别硬件初始化设置,并且它们通常会被上一层的初始化函数所调用,这样做的目的是为了把MCU相关的硬件初始化剥夺出来,方便用户代码在不同型号的MCU上移植。

stm32f1xx_hal_msp.c 文件定义了两个函数 HAL_MspInit 和 HAL_MspDeInit。这两个函数分别被文件 stm32f1xx_hal.c 中的 HAL_Init 和 HAL_DeInit 所调用。 HAL_MspInit 函数的主要作用是进行 MCU 相关的硬件初始化操作。例如我们要初始化某些硬件,我们可以硬件相关的初始化配置写在 HAL_MspDeinit 函数中。这样的话,在系统启动后调用了 HAL_Init之 后 , 会自动调用硬件初始化函数 。实际上,我们在工程模板中直接删掉stm32f1xx_hal_msp.c 文件也不会对程序运行产生任何影响。

  • (7)startup_stm32f103xe.s 启动文件

STM32 系列所有芯片工程都会有一个.s 启动文件。对于不同型号的 stm32 芯片启动文件也是不一样的。我们的开发板是 STM32F103 系列,所以我们需要使用与之对应的启动文件startup_stm32f103xe.s。启动文件的作用主要是进行堆栈的初始化,中断向量表以及中断函数定义等。启动文件有一个很重要的作用就是系统复位后引导进入 main 函数。打开启动文件 startup_stm32f103xe.s,可以看到下面几行代码:

1
2
3
4
5
6
7
8
9
10
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP

Reset_Handler 在我们系统启动的时候会执行,这几行代码的作用是在系统启动之后,首先调用 SystemInit 函数进行系统初始化,然后引导进入 main 函数执行用户代码。

4.2.6 __weak 修饰符

在 HAL 库中,很多回调函数前面使用__weak 修饰符,weak 顾名思义是“弱”的意思,所以如果函数名称前面加上__weak 修饰符,我们一般称这个函数为“弱函数”。加上了__\weak 修饰符的函数,用户可以在用户文件中重新定义一个同名函数,最终编译器编译的时候,会选择用户定义的函数,如果用户没有重新定义这个函数,那么编译器就会执行__weak 声明的函数,并且编译器不会报错。

比如我们打开工程模板(后边再建),找到并打开文件stm32f1xx_hal.c 文件,里面定义了一个函数 HAL_MspInit,定义如下:

1
2
3
4
__weak void HAL_MspInit(void)
{

}

可以看出, HAL_MspInit 函数前面有加修饰符__weak。同时,在该文件的前面有定义函数HAL_Init,并且 HAL_Init 函数中调用了函数 HAL_MspInit。

1
2
3
4
5
6
HAL_StatusTypeDef HAL_Init(void)
{
//此处省略部分代码
HAL_MspInit();
return HAL_OK;
}

如果我们没有在工程中其他地方重新定义 HAL_MspInit()函数,那么 HAL_Init 初始化函数执行的时候,会默认执行 stm32f1xx_hal.c 文件中定义的 HAL_MspInit 函数,而这个函数没有任何控制逻辑。如果用户在工程中重新定义函数 HAL_MspInit,那么调用 HAL_Init 之后,会执行用户自己定义的 HAL_MspInit 函数而不会执行 stm32f1xx_hal.c 默认定义的函数。也就是说,表面上我们看到函数 HAL_MspInit 被定义了两次,但是因为有一次定义是弱函数,使用了__weak修饰符,所以编译器不会报错。

__weak 在回调函数的时候经常用到。这样的好处是,系统默认定义了一个空的回调函数,保证编译器不会报错。同时,如果用户自己要定义用户回调函数,那么只需要重新定义即可,不需要考虑函数重复定义的问题,使用非常方便,在 HAL 库中__weak 关键字被广泛使用。

4.2.7 Msp 回调函数执行过程

我们打开一个下载的固件库包中的工程,看一下工程长啥样,我们选择下边路径中的这个UART串口工程实例:

1
STM32Cube_FW_F1_V1.8.0\Projects\STM3210E_EVAL\Examples\UART\UART_Printf\MDK-ARM

我们来看一下串口的初始化:

image-20230421222938085

用户在main函数中直接初始化串口,设置串口 相关参数,包括波特率,停止位,奇偶校验位等,并且最终是通过调用 HAL_UART_Init 函数进行参数设置。在函数 HAL_UART_Init 内部,通过判断逻辑判断如果串口还没有进行初始话,那么会调用函数HAL_UART_MspInit 进行相关初始化设置 。同时,我们可以看到,在文件 stm32f1xx_hal_uart.c 内部,有定义一个弱函数 HAL_UART_MspInit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// stm32f1xx_hal_uart.c
/**
* @brief UART MSP Init.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @retval None
*/
__weak void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
/* NOTE: This function should not be modified, when the callback is needed,
the HAL_UART_MspInit could be implemented in the user file
*/
}

而在 stm32f1xx_hal_msp.c 文件中对此函数进行了重写,重写后的函数 HAL_UART_MspInit 则主要进行串口 GPIO 引脚初始化设置。

这里定义的弱函数 HAL_UART_MspInit 是一个空函数,没有任何实际的控制逻辑。__weak 修饰符定义的弱函数如果用户自己重新定义了这个函数,那么会优先执行用户定义函数。所以,实际上在函数 HAL_UART_Init 内部调用的 HAL_UART_MspInit()函数,最终执行的是用户在 stm32f1xx_hal_msp.c 中自定义的HAL_UART_MspInit()函数。

那么整个串口初始化的过程为: 【main.c】 → 【HAL_UART_Init()】→ 【HAL_UART_MspInit()】。

为什么串口相关初始化不在 HAL_UART_Init 函数内部一次初始化而还要调用函数 HAL_UART_MspInit()呢?

这实际就是 HAL 库的一个优点,它通过开放一个回调函数 HAL_UART_MspInit(),让用户自己去编写与串口相关的 MCU 级别的硬件初始化,而与 MCU 无关的串口参数相关的通用配置则放在 HAL_UART_Init。

我们要初始化一个串口,首先要设置和 MCU 无关的东西,例如波特率,奇偶校验,停止位等,这些参数设置和 MCU 没有任何关系,可以用STM32F1,也可以是 STM32F2/F3/F4/F7上的串口。而一个串口设备它需要一个 MCU 来承载,例如用 STM32F1 来做承载, PA9 做为发送, PA10 做为接收, MSP 就是要初始化 STM32F1 的 PA9,PA10,配置这两个引脚。 所以 HAL驱动方式的初始化流程就是: HAL_USART_Init()→HAL_USART_MspInit() , 先初始化与 MCU无关的串口协议 ,再初始化 与 MCU 相关的串口引脚 。在 STM32 的 HAL 驱动中HAL_PPP_MspInit()作为回调, 被 HAL_PPP_Init()函数所调用。 当我们需要移植程序到 STM32F1平台的时候,我们只需要修改 HAL_XXX_MspInit 函数内容而不需要修改 HAL_XXX_Init 入口参
数内容。

所以在 STM32 的 HAL 库中,大部分外设都有回调函数 HAL_MspInit()。

4.2.8 HAL库中断处理

中断是 STM32 开发的一个很重要的概念,这里我们可以简单地理解为: STM32 暂停了当前手中的事并优先去处理更重要的事务。而这些“更重要的事务”是由软件开发人员在软件中定义的。关于 STM32 中断的概念。由于 HAL 库中断处理的逻辑比较统一,我们将这个处理过程抽象为下图所表示的业务逻辑:

image-20240113121103358

以上的流程大概就是: 设置外设的控制句柄结构体PPP_HandleType 和初始化 PPP_InitType 结构体的参数, 然后调用 HAL 库对应这个驱动的初始化 HAL_PPP_Init(),由于这个 API 中有针对外设初始化细节的接口 Hal_PPP_Mspinit(), 我们需要重新实现这个函数并完成外设时钟、 IO 等细节差异的设置,完成各细节处理后,使用HAL_NVIC_SetPriority()、 HAL_NVIC_EnableIRQ()来使能我们的外设中断;定义中断处理函数 PPP_IRQHandler ,并在中断函数中调用 HAL_ppp_function_IRQHandler()来判断和处理中断标记; HAL 库中断处理完成后,根据对应中的调用我们需要自定义的中断回调接口 HAL_PPP_ProcessCpltCallback();如串口接收函数 HAL_UART_RxCpltCallback(), 我们在这个函数中实现我们对串口接收数据想做的处理;中断响应处理完成后, stm32 芯片继续顺序执行我们定义的主程序功能,按照以上处理的标准流程完成了一次中断响应。

4.3 LL库

LL 库(Low Layer)目前与 HAL 库捆绑发布,它设计为比 HAL 库更接近于硬件底层的操作,代码更轻量级, 代码执行效率更高的库函数组件, 可以完全独立于 HAL 库来使用,但 LL库不匹配复杂的外设,如 USB 等。所以 LL 库并不是每个外设都有对应的完整驱动配置程序。使用 LL 库需要对芯片的功能有一定的认知和了解,它可以:

  • 独立使用,该库完全独立实现,可以完全抛开 HAL 库,只用 LL 库编程完成。
  • 混合使用,和 HAL 库结合使用。

对于 HAL 库和 LL 库的关系, 如下图的软件框架所示, 可以看出它们设计为彼此独立的分支, 但又同属于 HAL 库体系。

image-20240113114126151