购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

4.1 C语言的基本应用

C语言是STM32嵌入式系统开发中的必备基础知识,虽然在系统开发过程中,不需要完全精通C语言,但是需要熟练掌握其基础操作,这样才能在以后的程序设计过程中更加得心应手。C语言知识博大精深,本节只对C语言基础操作进行简要介绍,以更好地进行后期的STM32嵌入式系统的设计和开发工作。如果读者的C语言基础知识比较扎实,则可以略过这一节的知识。

4.1.1 面向STM32的基本操作

1. 位操作

C语言的位操作就是对基本类型变量在位级别进行的操作,C语言支持与、或、异或、取反、左移和右移六种位操作。C语言基本位操作如表4.1所示。

表4.1 C语言基本位操作

位操作在STM32嵌入式系统设计中的应用主要在于对某些寄存器位的修改,如把寄存器中的某位或某几位清零、对寄存器某几位赋值、对寄存器某几位取反等,这部分内容在1.3.3节已经进行了相关的介绍,这里不再详述。

2. define宏定义

define是C语言中的预处理命令,使用define进行宏定义,可以提高源代码的可读性,为编程提供方便。

define宏定义的常见格式如下。

标识符为定义的宏名,字符串可以是常数、表达式或格式串等。例如:

上面的程序代码定义了标识符SYSCLK_FREQ_72MHz的值为72000000。define宏定义的其他知识,如宏定义带参数等,在这里不再详述。

3. ifdef条件编译

在单片机的程序开发过程中,当满足某条件时,则对某一组语句进行编译;当不满足某条件时,则编译另一组语句,此时常用的编程方法即ifdef条件编译。

ifdef条件编译的常见格式如下。

使用ifdef条件编译时,如果标识符已经被定义(一般用define宏命令进行定义)过,则对程序段1进行编译,否则编译程序段2。其中#else部分可以去掉,即

上述条件编译在STM32嵌入式系统设计中用得较多,在stm32f10x.h头文件中经常看到这样的语句,如:

上述的STM32F10X_HD是通过#define定义的。关于ifdef条件编译的内容就介绍到这里,学会如何使用就可以了。

4. extern变量声明

C语言的extern变量声明可以置于变量或函数之前,用于表示某变量或函数的定义位于其他文件中,提示编译器遇到此变量或函数时,需要在其他文件中寻找对应的定义。

注意

extern声明变量或函数可以有很多次,但是变量或函数的定义却只有一次。

在STM32标准函数库文件中会经常见到extern后面接同一个变量或函数,这个语句只是声明该变量或函数在其他文件中已经被定义了,而且它的定义有且只有一个,肯定可以在某个文件中找到该变量或函数的定义语句。下面通过一个例子说明一下extern变量声明的使用方法。

首先在main.c中定义一个全局变量id,id的初始化是在main.c中进行的,程序代码如下。

如果希望在test.c的test(void)函数中使用变量id,则需要在test.c中声明变量id是在外部定义的。如果不声明,变量id的作用域到不了test.c中,这会导致在test.c中无法使用变量id。test.c中的代码如下。

由于在test.c中声明了变量id在外部定义,因此test.c可以使用变量id。

5. typedef类型别名

当在程序设计过程中需要为现有类型创建一个新名字时,即需要定义类型别名时,则会用typedef。typedef可以简化变量的定义。typedef在嵌入式系统设计中用得较多的是定义结构体的类型别名和枚举类型。

上述程序代码定义了一个结构体变量GPIO,在此基础上,定义变量的方式如下。

但是,在STM32嵌入式系统设计过程中有很多这样的结构体变量需要定义,显得很烦琐。为了简化变量的定义方式,可以为结构体定义一个别名GPIO_TypeDef,这样就可以在其他地方通过别名GPIO_TypeDef定义结构体变量了。定义方法如下。

Typedef为结构体定义一个别名GPIO_TypeDef,可以方便地通过GPIO_TypeDef定义结构体变量。例如:

4.1.2 结构体的使用解析

1. 结构体与结构体指针

本节将简要讲解C语言的结构体。在STM32标准函数库中,有很多地方使用了结构体和结构体指针,这可能会让很多初学者感到困扰,其实结构体并没有多么复杂。声明结构体类型如下:

在声明结构体时,需要列出它包含的所有成员,成员列表包括每个成员的类型和名字。

结构体声明主要由三部分组成,即结构体名、成员列表和变量列表。而结构体变量可以在结构体声明时定义,当然也可以在声明之后定义。例如,在声明结构体时定义结构体变量:

如果在声明结构体之后定义,则在声明结构体时可以不需要加变量名列表,如:

在声明结构体之后,定义结构体变量所采用的方法如下。

例如:

结构体成员变量的引用方法如下。

如果要引用GPIOA中的成员CRL,其方法如下。

结构体指针变量的定义也是一样的,与其他变量没有区别。例如:

结构体指针成员变量引用方法是通过“->”符号实现的,如要访问GPIOA结构体指针指向的结构体的成员变量CRL,其方法如下。

通过前面的讲解,已经初步掌握了结构体和结构体指针的使用方法。但是,为什么在嵌入式系统开发过程中需要使用结构体与结构体指针呢?使用结构体可以带来什么好处呢?下面通过一个实例简要介绍一下结构体的作用。

在STM32嵌入式系统开发与设计过程中,会经常遇到初始化一个外设的情况,以串口为例,它的初始化状态主要是由属性决定的,如串口号、波特率、极性和模式等。对于这种情况,在没有学习结构体之前,一般会通过定义下面的函数来实现。

这种方式的确是有效的,而且在一些场合也是可取的。但是试想,在STM32嵌入式系统的开发过程中,如果再往这个函数中加入一个定义“字长”的属性参数,那么必然要去修改前面所定义的函数,即在括号中加入“字长”这个属性参数。函数定义会被修改为

修改一两次还好,但是如果这个函数的属性参数是随着开发过程的深入而不断增多的,则必须不断地修改函数定义,势必会在程序设计过程中带来很多不必要的麻烦。如果使用结构体就能很好地解决这个问题。通过结构体,只要改变结构体的成员变量,就可以实现前面修改属性参数的目的,从而避免了反复修改函数定义的麻烦。

结构体就是将多个变量组合成一个有机的整体,对于串口而言,BaudRate、usartx、parity、mode和wordlength等参数是一个有机的整体,它们都是用来设置串口参数的,所以可以通过定义一个结构体将它们组合在一起。事实上,STM32标准函数库就是这样定义的。

这样,在初始化串口时,属性参数就可以是USART_InitTypeDef类型的变量或指针变量。

因此,只需要修改结构体中的成员变量,就可以往结构体中加入新成员变量,从而不用修改函数定义就能够达到与添加函数属性参数同样的目的。这样做的好处是,无论在什么时候增加或减少成员变量,都不需要修改函数定义。所以,在以后的嵌入式系统设计过程中,如果遇到某几个变量是用来描述同一个对象的,则可以考虑将这些变量定义在一个结构体中,这样不仅可以为程序设计带来方便,还能够提高代码的可读性,不会让人感觉变量定义混乱。

当然结构体的作用还远不止这些,这里只是举一个简单的例子,通过常用的场景,帮助读者理解结构体的优势。后面还会进一步讲解结构体的其他知识。

2. 结构体与寄存器

前面介绍了结构体和结构体指针的使用方法,并简要讲解了结构体的作用。通过前面的实例可以发现,在实际的系统设计过程中,通过修改结构体成员变量的值就可以达到操作对应寄存器的目的,那么在STM32中,结构体究竟是如何与寄存器地址对应起来的呢?

首先回顾一下在51单片机中是怎么做的,在51单片机的开发中经常会引用一个reg51.h头文件,而这个reg51.h头文件实现了将外设名称与寄存器联系起来的作用。例如:

sfr是一种扩充数据类型,占用一个内存单元,值域为0~255,利用它可以访问51单片机内部的所有特殊功能寄存器。上述代码通过“sfr P0=0x80”这一语句定义“P0”为P0端口在片内的寄存器。向地址为0x80的寄存器设值的方法为

其实,STM32中的做法与51单片机中的做法很类似,但是因为STM32中的寄存器数量众多,如果以这样的方式逐一列出,则会耗费很大的篇幅,既不方便嵌入式系统的开发,也会显得过于杂乱无序。所以STM32采用的方式是通过结构体将寄存器组织在一起,并把结构体与地址逐一对应起来,从而在修改结构体成员变量的数值时,可以达到操作对应寄存器的目的,这些对应工作都是在stm32f10x.h文件中完成的。

下面以GPIOA的几个寄存器为例进一步进行讲解。由1.3节对寄存器的相关介绍可知,STM32中的寄存器都是32位的,每个寄存器占用4个地址,因此GPIOA的7个寄存器一共占用28个地址,其地址偏移范围为0x00~0x1B,这个地址偏移是相对GPIOA的基地址而言的。打开stm32f10x.h头文件,找到GPIO_TypeDef的定义,内容如下。

然后往下定位到:

从上述代码可以看出,GPIOA将GPIOA_BASE强制转换为GPIO_TypeDef指针。这段程序的意思是,将GPIOA指向地址GPIOA_BASE,而GPIOA_BASE存放的数据类型为GPIO_TypeDef。双击“GPIOA_BASE”打开之后,右击“Go to definition of”,就可以查看GPIOA_BASE(GPIOA基地址)的宏定义:

还可以通过同样的方法找到APB2PERIPH_BASE(APB2总线外设基地址)的宏定义:

再进一步,找到顶层PERIPH_BASE(片上外设基地址)的宏定义:

可以推算出GPIOA的基地址为

GPIOA_BASE=0x40000000+0x10000+0x0800=0x40010800

这与1.3节介绍的GPIOA的基地址完全吻合。同样,也可以推算出其他外设的基地址。掌握了外设基地址的计算方法,计算外设上相应寄存器的地址就不是什么难题了。寄存器的偏移地址即对应该外设基地址的地址偏移量,所以可以推算出每个寄存器的地址。以GPIOA为例:

GPIOA的寄存器地址=GPIOA基地址+寄存器相对GPIOA基地址的偏移量

那么,在结构体中如何实现这些寄存器与地址的逐一对应呢?这里涉及结构体的一个特征,即结构体内变量的存储空间是连续的。由于GPIOA是指向GPIO_TypeDef类型的指针,而且GPIO_TypeDef是结构体,所以可以推算出GPIOA成员变量所对应地址,如表4.2所示。

表4.2 GPIOA成员变量所对应的地址

把GPIO_TypeDef定义中的成员变量顺序和GPIOx寄存器地址映射顺序进行对比,可以发现,它们的顺序是完全一致的。如果不一致,则会导致地址混乱。这也是在STM32标准函数库中定义GPIOA->BRR=value的原因,其实质是对地址为0x40010814的寄存器GPIOA_BRR进行置位。可以看出,这和51单片机中定义“P0=value;”,即对地址为0x80的P0寄存器进行置位的原因是一样的。

我们不难发现,上述内容其实在1.3.3节中介绍STM32寄存器封装过程的时候就已经涉及了,这里只是从结构体的角度出发,介绍如何通过封装后的结构体和定义去寻求所需寄存器的地址,以此探求在STM32标准函数库中,结构体与寄存器地址的逐一对应关系,从而对STM32嵌入式系统设计中结构体的使用有更加深入的认识。 9tMEws2ojvOTpKV2I5cO7u/iub9/47nfeKIZnESOdl2O4lfR0QPlRL1svVaZ5yhJ

点击中间区域
呼出菜单
上一章
目录
下一章
×