您好,欢迎访问代理记账网站
  • 价格透明
  • 信息保密
  • 进度掌控
  • 售后无忧

单片机开发常用C语言复习资料

文章目录

  • 前言
  • 一、位操作
    • (一)基础知识
    • (二)实际应用
      • (1)不改变其他位的值的状况下,对某几个位进行设值。
      • (2)移位操作提高代码的可读性。
      • (3)~取反操作使用技巧
  • 二、define 宏定义
  • 三、 ifdef 条件编译
  • 四、extern 变量申明
  • 五、typedef 类型别名
  • 六、结构体


前言

这里主要是简单的复习一下几个 C 语言基础知识点,引导那些 C 语言基础知识不是很扎实的用户能够快速开发 STM32 程序。


提示:以下是本篇文章正文内容

一、位操作

(一)基础知识

C 语言支持如下 6 种位操作

运算符含义运算符含义
&按位与~取反
I按位或<<左移
^按位异或>>右移

按位与:全“1”为1,有“0”为0;
按位或:全“0”为0,有“1”为1;
按位异或:相异为1,相同为0;
按位取反:反“1”为0,反“0”为1;

按位左移的运算符为<<,其规则如下:

  1. 按照二进制值每位向高位(书写上是向左)移动n位;
  2. 最高位(最左边的)n位舍去;
  3. 最低位(最右边)填加n个0.

按位右移的运算符为>>,其规则如下:

  1. 按照二进制值每位向低位(书写上是向右)移动n位;
  2. 最高位(最右边的)n位舍去;
  3. 最低位(最左边)填加n个0.

(二)实际应用

(1)不改变其他位的值的状况下,对某几个位进行设值。

这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,然后用“|”操作符设值。比如我要改变 GPIOA-> BSRRL 的状态,可以先对寄存器的值进行“&”

清零操作

GPIOA-> BSRRL &=0XFF0F; //将第 4-7 位清 0

然后再与需要设置的值进行|或运算

GPIOA-> BSRRL |=0X0040;//设置相应位的值,不改变其他位的值

(2)移位操作提高代码的可读性。

移位操作在单片机开发中也非常重要, 我们来看看下面一行代码

GPIOx->ODR = (((uint32_t)0x01) << pinpos);

这个操作就是将 ODR 寄存器的第 pinpos 位设置为 1,为什么要通过左移而不是直接设置一个固定的值呢?其实,这是为了提高代码的可读性以及可重用性。这行代码可以很直观明了的知道,是将第 pinpos 位设置为 1。如果你写成

GPIOx->ODR =0x0030;

这样的代码就不好看也不好重用了。

(3)~取反操作使用技巧

SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为 0,同时其他位都保留为 1,简单的作法是直接给寄存器设置一个值:

TIMx->SR=0xFFF7

这样的作法设置第 3 位为 0,但是这样的作法同样不好看,并且可读性很差。看看库函数代码中怎样使用的:

TIMx->SR = (uint16_t)~TIM_FLAG;

而 TIM_FLAG 是通过宏定义定义的值:

#define TIM_FLAG_Update ((uint16_t)0x0001)
#define TIM_FLAG_CC1 ((uint16_t)0x0002)

看这个应该很容易明白,可以直接从宏定义中看出 TIM_FLAG_Update 就是设置的第 0 位了,可读性非常强。

二、define 宏定义

define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供方便。常见的格式:

#define 标识符 字符串

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

#define PLL_M 8

定义标识符 PLL_M 的值为 8。

三、 ifdef 条件编译

单片机程序开发过程中,经常会遇到一种情况, 当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。 条件编译命令最常见的形式为:

#ifdef 标识符
程序段 1
#else
程序段 2
#endif

它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编译,否则编译程序段 2。 其中#else 部分也可以没有,即:

#ifdef
程序段 1
#endif

这个条件编译在MDK里面是用得很多的,在stm32f4xx.h这个头文件中经常会看到这样的语句:

#if defined (STM32F40_41xxx)
	STM32F40x 系列和 STM32F41x 系列芯片需要的一些变量定义
#end

而(STM32F40_41xxx 则是我们通过#define 来定义的。

四、extern 变量申明

C 语言中 extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。 这里面要注意,对于 extern 申明变量可以多次,但定义只有一次。在我们的代码中你会看到看到这样的语句:

extern u16 USART_RX_STA;

这个语句是申明 USART_RX_STA 变量在其他文件中已经定义了,在这里要使用到。所以,你肯定可以找到在某个地方有变量定义的语句:

u16 USART_RX_STA;

的出现。下面通过一个例子说明一下使用方法。
在 Main.c 定义的全局变量 id, id 的初始化都是在 Main.c 里面进行的。
Main.c 文件

u8 id;//定义只允许一次
main()
{
	id=1;
	printf("d%",id);//id=1
	test();
	printf("d%",id);//id=2
}

但是我们希望在main.c的 changeId(void)函数中使用变量id,这个时候我们就需要在main.c里面去申明变量 id 是外部定义的了,因为如果不申明,变量 id 的作用域是到不了 main.c 文件中。看下面 main.c 中的代码:

extern u8 id;//申明变量 id 是在外部定义的,申明可以在很多个文件中进行
void test(void){
	id=2;
}

在 main.c 中申明变量 id 在外部定义,然后在 main.c 中就可以使用变量 id 了。

五、typedef 类型别名

typedef 用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。typedef 在 MDK 用得最多的就是定义结构体的类型别名和枚举类型了。

struct _GPIO
{
	__IO uint32_t MODER;
	__IO uint32_t OTYPER;};

定义了一个结构体 GPIO,这样我们定义变量的方式为:

struct _GPIO GPIOA;//定义结构体变量 GPIOA

但是这样很繁琐, MDK 中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别名 GPIO_TypeDef,这样我们就可以在其他地方通过别名 GPIO_TypeDef 来定义结构体变量了。
方法如下:

typedef struct
{
	__IO uint32_t MODER;
	__IO uint32_t OTYPER;} GPIO_TypeDef;

Typedef 为结构体定义一个别名 GPIO_TypeDef,这样我们可以通过 GPIO_TypeDef 来定义结构体变量:

GPIO_TypeDef _GPIOA,_GPIOB;

这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。

六、结构体

经常很多用户提到,他们对结构体使用不是很熟悉,但是 MDK 中太多地方使用结构体以及结构体指针,这让他们一下子摸不着头脑,学习 STM32 的积极性大大降低,其实结构体并不是那么复杂,这里我们稍微提一下结构体的一些知识,还有一些知识我们会在下一节的“寄存器地址名称映射分析”中讲到一些。
声明结构体类型:

Struct 结构体名
{
成员列表;
}变量名列表;

例如:

Struct U_TYPE {
	Int BaudRate
	Int WordLength;
}usart1,usart2;

在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:

Struct 结构体名字 结构体变量列表 ;

例如:

 struct U_TYPE usart1,usart2;

结构体成员变量的引用方法是:

结构体变量名字.成员名

比如要引用 usart1 的成员 BaudRate,方法是:

usart1.BaudRate;

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

struct U_TYPE *usart3; //定义结构体指针变量 usart1;

结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 usart3 结构体指针指向的结构体的成员变量 BaudRate,方法是:

Usart3->BaudRate;

上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里,有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实例回答一下这个问题。

在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如串口,它的初始化状态是由几个属性来决定的,比如串口号,波特率,极性,以及模式等。对于这种情况,在我们没有学习结构体的时候,我们一般的方法是:

void USART_Init(u8 usartx,u32 u32 BaudRate,u8 parity,u8 mode);

这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里面再传入一个参数,那么势必我们需要修改这个函数的定义,重新加入字长这个入口参数。于是我们的定义被修改为:

void USART_Init (u8 usartx,u32 BaudRate, u8 parity,u8 mode,u8 wordlength );

但是如果我们这个函数的入口参数是随着开发不断的增多,那么是不是我们就要不断的修改函数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?

这样如果我们使用到结构体就能解决这个问题了。我们可以在不改变入口参数的情况下,只需要改变结构体的成员变量,就可以达到上面改变入口参数的目的。

结构体就是将多个变量组合为一个有机的整体。上面的函数,BaudRate,wordlength,Parity,mode,wordlength 这些参数,他们对于串口而言,是一个有机整体,都是来设置串口参数的,所以我们可以将他们通过定义一个结构体来组合在一个。 MDK 中是这样定义的:

typedef struct
{
	uint32_t USART_BaudRate;
	uint16_t USART_WordLength;
	uint16_t USART_StopBits;
	uint16_t USART_Parity;
	uint16_t USART_Mode;
	uint16_t USART_HardwareFlowControl;
} USART_InitTypeDef;

于是,我们在初始化串口的时候入口参数就可以是 USART_InitTypeDef 类型的变量或者指针变量了, MDK 中是这样做的:

void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);

这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需要修改函数定义就可以达到修改入口参数同样的目的了。 这样的好处是不用修改任何函数定义就可以达到增加变量的目的。

理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可以提高你的代码的可读性。

使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作用就远远不止这个了,同时, MDK 中用结构体来定义外设也不仅仅只是这个作用,这里只是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。


分享:

低价透明

统一报价,无隐形消费

金牌服务

一对一专属顾问7*24小时金牌服务

信息保密

个人信息安全有保障

售后无忧

服务出问题客服经理全程跟进