STM32学习笔记1.2 STM32的开发方式——写给电信学部学生科技协会的朋友们

你如果用过Arduino,应该对一个蓝绿色的C语言编程界面非常熟悉。你在里边写了一个叫做“Setup”的函数——在里边运行对各种外设的初始化函数,又写了一个叫“Loop”的函数,在里边写各种外设实现功能的函数之后,你的Arduino就能实现各种你想要的功能了。
如你所见,Arduino的运行步骤可以总结成:配置外设初始化->运行外设所需功能->死循环。STM32是否需要这些东西呢?
答案是肯定的,并且基本的思路几乎完全一致。
不过在了解具体的流程之前,我们需要先知道一些大略的东西。

引入

首先来看一个问题,STM32的程序是怎么编写的呢?
与你写C语言程序的过程基本一致,你需要首先装好编译器,配置好编程环境,这些会在实践课程时详细地讲给你。你只需要像你写HelloWorld一样写代码,编译连接生成二进制文件,然后……然后怎么办呢?
很简单,把二进制文件刷进STM32,给它供电,复位,程序就能在STM32上跑起来了。至于怎么刷进去,实践课程时会详细地讲给你,你只需要准备好你的开发板或最小系统,准备好一个叫做STLINK或者JTAG的东西就可以了,把它们按一定方式接到开发板上,再在电脑上操作一下,就能给你的MCU烧入程序了。
开发使用的软件在实践课程上会教给你怎么用,现在我们应该了解一些更理论的东西,STM32的开发方式。大体上分为三种:寄存器编程,标准固件库函数编程和HAL库函数编程。
先明确一点,STM32也是计算机,你在开发工具中写的代码最终会在你的STM32上运行,而不是在你的PC上运行,所以谨记,你所做各种操作都是对STM32的操作,尤其是对内存和寄存器的管理要特别留心。也不要滥用诸如malloc、printf之类的函数,它们不是为了STM32设计的,因此使用上需要谨慎,甚至需要适当地修改。
如果你还不熟悉C语言的位操作结构体指针等内容,请一定要仔细学习它们,它们对今天的内容和日后的开发都至关重要。本文默认你对这些东西已经烂熟于心,不会再帮你复习和补课了。此外,多个文件的编译你应该也没有尝试过多少,这个也必须熟练掌握。我们会在之后的实践中教给大家。

寄存器与寄存器编程

“寄存器”的概念对所有的计算机都相当重要——对一般计算机,寄存器用于存储CPU马上就要处理的指令、数据等等,是一类速度极快的存储器。不过,单片机开发中我们常常提到的“寄存器”与你所使用的X86电脑的寄存器还稍有区别。这里的寄存器用于配置各种外设的功能,以及标志外设的工作状态。它们可以被C语言以内存的形式(即指针)访问(相比之下,X86下的C语言寄存器变量没有办法通过地址访问,甚至修改都要受限),因此你只需要把它们当成“有特定功能的内存空间”来处理就好了。
更让我们感到开心的事是,同一个外设的寄存器地址都是连续排列的,譬如CR寄存器是0X00000000,大小是四个字节,那么紧随其后的CR2地址就是0X00000004。巧的是,我们的结构体在C语言中的存储结构,也是一个字段紧跟着一个字段的。譬如在Dev C++下运行下面这段代码:

#include <stdio.h>

typedef struct{
	int a;
	int b;
	int c;
	int d;
}Test;

int main(){
	Test test_struct={0,0xABCDEF,0xFF,0.12};
	printf("The address of struct is: %p \n", &test_struct);
	printf("The address of a is: %p \n", &(test_struct.a));
	printf("The address of b is: %p \n", &(test_struct.b));
	printf("The address of c is: %p \n", &(test_struct.c));
	printf("The address of d is: %p \n", &(test_struct.d));

	return 0;
}

我的32位环境下,某次运行的结果是:

The address of struct is: 004FF780
The address of a is: 004FF780
The address of b is: 004FF784
The address of c is: 004FF788
The address of d is: 004FF78C

你可以发现,a的地址与结构体的地址相等,a与b地址之间的差正好是int型变量的长度(4个字节),之后的依次类推。事实上,如果结构体中的每一个字段都是相同数据类型,结构体中的所有字段就都是连续存放的,且结构体所在地址与第一个字段所在地址一致。
这可以让我们用结构体的方式定义、访问和修改寄存器,例如F103的I2C外设的寄存器结构体定义如下:

typedef struct
{
  __IO uint16_t CR1;
  uint16_t  RESERVED0;
  __IO uint16_t CR2;
  uint16_t  RESERVED1;
  __IO uint16_t OAR1;
  uint16_t  RESERVED2;
  __IO uint16_t OAR2;
  uint16_t  RESERVED3;
  __IO uint16_t DR;
  uint16_t  RESERVED4;
  __IO uint16_t SR1;
  uint16_t  RESERVED5;
  __IO uint16_t SR2;
  uint16_t  RESERVED6;
  __IO uint16_t CCR;
  uint16_t  RESERVED7;
  __IO uint16_t TRISE;
  uint16_t  RESERVED8;
} I2C_TypeDef;

(解释一下,uint16_t是经过typedef的无符号短整型,类似地还会有uint32_t(无符号整型),uint8_t(字符,当然也可以做数字)等类型定义。这样便于开发人员一目了然地得知变量的长度,充分利用单片机上宝贵的存储空间 。)
如果你要访问I2C1的寄存器的CR1段,使其第n(从零开始数)位变成1,这一步的实际意义是配置I2C1外设的某个功能。你只需要在数据手册中找到I2C1外设寄存器的基地址(我这里懒得查了,用0x00000000代替,其实我们会专门定义一个叫I2C1的全局变量用于存放这个地址),然后执行:

(I2C_TypeDef*)(0x00000000)->CR1|=0x0001<<n

这里使用了一步强制类型转换,因为0x00000000本来是整型的,要将当做指针访问就必须转换类型。事实上,强制类型转换在单片机开发中还有许多重要的作用和大坑在等着我们。
利用修改寄存器的方式开发,也是所有单片机的最基本开发方式。你可以打开“参考手册”和“数据手册”查阅到每个外设的寄存器们的功能和位置,有诸如控制寄存器“CR”,状态寄存器“SR”,数据寄存器“DR”等等类型,每个位或若干个位都代表了当前外设的设置、或者运行状态、或者通信的数据。记住所有这些寄存器的功能和配置方法,是STM32开发者的必备素质。
好吧,逗你玩的,STM32F有数百个寄存器,很难全部记忆清楚,更别说方便高效地编程了。我们有更好的“库函数”方式来控制这些寄存器进而控制外设。但你必须会查询参考手册,并且会在参考手册的指导下配置和读取一些重要的寄存器,这有助于加深你对外设工作的理解,也有助于在关键时刻救命。而且对于51等简单的单片机,寄存器是最重要的开发方式。这些比STM32都简单的东西你必须学会才能靠这个吃饭:没人会丢下更便宜性能足够的51不用去用更贵性能也过剩的STM32吧。
STM32F所有的寄存器结构体的定义都位于STM32FXXX.h中。

库函数

“固件库函数”是什么?
是一系列C语言底下的C语言函数,由ST官方推出,打包成一大堆文件交给开发者,里面有各种各样的.c,.h和汇编文件。运行它们可以有规律地按要求读写寄存器,进而更方便地实现外设的功能。每个外设都有自己的一系列操作,都被定义成函数,每个外设通用的大概有如下几类:

  1. XXX_Init(XXXx,&XXX_InitStruct):初始化一个外设,根据你传入的XXX_InitStruct的各项的值,去修改XXXx外设的控制寄存器。XXX_InitStruct的各字段都是数字,但它们有对应的宏定义,因而可以直观地看出它们对应的功能状态。
  2. XXX_Cmd((XXXx, ENABLE/DISABLE):使能一个外设,前面的Init只是设置好了外设的参数值,真正启动这个外设,需要Cmd函数来修改控制寄存器的某个位。
  3. XXX_ITConfig():配置外设的中断,修改的也是外设的一些寄存器
  4. XXX_GetFlagStatus()和XXX_ClearFlag(),通常是读取和清除外设的状态寄存器中的一些标志位,这些标志位可能标志着外设进入了某种事件或状态,进而可以读取外设的状态。

除了这些,每个外设还有它们特有的寄存器,因而会有自己特有的操作函数,利用这些操作函数会实现更多的具体功能。但是这些函数无一例外,都在操作外设的寄存器。
如果说前面把寄存器由地址宏定义成结构体,每个字段都代表一个寄存器,有特定的名称能让人稍微看得懂这个寄存器是什么功能是第一层封装,那么库函数无疑就是让人基本能看得懂现在系统在拿寄存器们做什么的第二层封装了。库函数不过我们操作寄存器的工具,只是这些工具对应了具体的功能而已。比如

GPIOA->ODR |= 0x0001;

基本完全等价于:

GPIO_SetBits(GPIOA, GPIO_Pin_0);

但是如果你对GPIO的寄存器非常熟悉,还可能会知道第一个是在让PA0输出高电平,但是如果你记不清楚或者记混了,就可能一头雾水了。而且有时,一个库函数(比如XXX_Init)会操作不止一个寄存器,一行代码解决一大堆问题,比你一个一个解决,要容易得多吧。

使用库函数配置并使用外设的一般步骤如下:

  1. 先用RCC_AXBPeriphClockCmd函数打开外设对应的时钟(因为复位时的外设时钟是关闭的,为了省电。而必须开启时钟外设才能工作)
  2. 定义一个XXX_InitTypeDef类型的结构体,名字随便起,一般叫XXX_InitStruct,其中的字段对应你要配置的外设功能状态,修改为你需要的值,用于初始化外设。
  3. 配置一些具体的细节,比如连接GPIO复用功能,配置中断,设置初始的输出状态等等。
    执行XXX_Init()函数,第一个参数是外设名,第二个参数指向之前的InitStruct的地址,这个函数会帮你把你之前需要的外设功能状态转换成对寄存器的操作(这类函数也是库函数里行数最多最不容易读懂的函数了,不过你也没必要读懂),
  4. 最后,执行XXX_Cmd来使能外设,之后用其他库函数实现特定的功能就可以了。

库函数的文件结构和调用,定义方法在以后的工程模板章节讲解。

HAL库函数

库函数已经是非常伟大的发明了,把修改纷繁复杂的寄存器变成修改可以阅读的英文单词和几个数字,真的非常不错。不过库函数也存在一些问题:配置过程稍显复杂,不同型号之间的移植存在细节上的困难。因此,F1和F4的标准库函数早已不被ST公司推荐,它们在十年前就停止了更新维护。
取而代之的是像我们大步走来的HAL库,以及它配套的人性化配置工具STM32CubeMX!尤其是后者,我愿称之为近20年内MCU软件领域最伟大的发明,有之一。
HAL库其实也是一系列升级版的库函数,但是,HAL库除了之前讲的那些对单个外设的设置和操作外,还定义了一些可能用到多个外设或者一个外设的更高级功能的函数(比如定时器延时),而且不同型号MCU的函数名和结构完全相同(除非外设功能有大的差异),极大方便了移植——以前F1移植F4要改GPIO初始化,改端口复用,改EXTI,改DMA FIFO……现在,通通不需要!
这些或许不是太吸引初学者,但是下一条,初学者肯定喜欢——使用Hal库不需要写复杂的外设初始化代码,在CubeMX的图形界面里,在引脚上点选设置几下,在下拉框里选择好功能,文本框里输入你需要的一些参数,最后生成一个对应的工程模板,你所需的外设就初始化好了,你只需要在模板里面添加自己的代码就好了。当然要修改初始化设置,也可以调用Hal库里的初始化函数在编程时进行。在图形界面里操作,相比在代码窗口里自己从零开始,是不是容易得多呢?
这两张是
在这里插入图片描述
以上两张分别是采用标准库和CubeMX配置一个定时器外设的过程,你应该知道你喜欢哪个。

不过,虽然ST在大力推广HAL,仍然有一大群国内开发者(包括我自己)坚守着标准库函数的阵地,因为拜某某原子和某火两大著名开发板开发商所赐,标准库函数大行其道(其实也归功于他们,STM32能在国内如此普及)了很多年,许多人的编程习惯已经根深蒂固,且HAL库的某些特性(比如用户代码放在这里那里的限制以及不太一样的函数名)使他们直呼承受不来,因此标准库应该还会生存一段时间。这并不重要,HAL无非是换了一种方式来封装了对寄存器的操作,本质和标准库,和寄存器结构体操作是完全一致的。

当然,你问HAL库之后会有什么,我也不知道。
什么?完全图形化编程?那还叫编程吗?那不就变成arduino+scratch儿童编程了吗,我们这些未来嵌入式软件工程师的颜面还何在(逃

之后,我们就来利用最基本的GPIO外设,来对比体会一下三种编程方式吧。
(未完待续)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值