文章目录

  • 文章内容
    • 1. 位运算
      • 1.1 初级应用
      • 1.2 进阶应用
        • 1.2.1. 给寄存器某一位置1
        • 1.2.2. 给寄存器某一位清0
        • 1.2.3. 翻转寄存器某一位
        • 1.2.4. 数据的字节分解
    • 2. 宏定义和预编译
      • 2.1 理论指导
      • 2.2 看到一个宏定义在数据切分字节上的妙用
      • 2.3 define和typedef的使用区分
    • 3. 字符串
      • 3.1 终止符问题
    • 4. 数据类型
      • 4.0 记录一个基础问题
      • 4.1 有符号无符号
      • 4.2 数据类型的字节数
      • 4.3 布尔类型
    • 5. 变量的类型
      • 5.1 const,static,volatile,extern关键字辨析
    • 6. 结构体struct与联合union
      • 6.1 参考链接
      • 6.2 结构体语法总结
      • 6.3 联合union语法总结
    • 7. case语句使用

文章内容

  该文章主要是总结一些在编写单片机程序及其他相关实践中学到的C语言技巧,读者应具有C语言基础。

1. 位运算

  因为计算机存储数据都是按字节(Byte,B)储存的,一个字节含有8位(bit,b),直接对数据的位进行操作,可以使运算速度加快。下面是常用的运算符

1.1 初级应用

  • 按位取反 ~:用于将数字的每一位二进制翻转,比如 ~(0110 1001) = 1001 0110。
  • 按位与 &:全1为1,其余为0,比如 (1010 0101) & (0001 0111) = (0000 0101)
  • 按位或 |:全0为0,其余为1,比如 (1010 0101) | (0001 0111) = (1011 0111)
  • 按位异或 ^:相同为0,不同为1,比如 (1010 0101) | (0001 0111) = (1011 0010)
  • 左移 <<:将其左边的操作数的值的每一位向左移动,移动的位数由右边的操作数指定,移出的数字舍弃,右边自动补0,比如 (1010 0101) << 3 结果为 (0010 1000),(由于单片机中需要用到的数据大多为无符号数,故此处不讨论有符号数的情况,详情请参考链接)左移n位相当于乘以2的n次方(前提是没超过字节范围)
  • 右移 >>:将其左边的操作数的值的每一位向左移动,移动的位数由右边的操作数指定,移出的数字舍弃,左边自动补0,比如 (1010 0101) >> 3 结果为 (0001 0100),同样不考虑有符号数,右移n位相当于除以2的n次方

1.2 进阶应用

1.2.1. 给寄存器某一位置1

  单片机的程序当中可能会涉及到寄存器的读写,而寄存器在程序中体现为一个固定字节数的变量,寄存器的某些位控制着某些模块的特定功能,因此对某一位的操作就很有必要了。

P1OUT |= 00000010;   //P1OUT是一个8位寄存器,可看作一个单字节数据
//equal to this:
P1OUT = P1OUT | 0000010;

  上述程序就是将P1OUT寄存器的第2位置1,其他位保持不变,因为 1 | 0 = 10 | 0 = 0,即一个数或上0,其值保持不变;或上1,则变为1。

1.2.2. 给寄存器某一位清0

  有置1的操作,就必然要有清0的操作:

P1OUT &= ~(00000010);  //P1OUT是一个8位寄存器,可看作一个单字节数据
//equal to this:
P1OUT = P1OUT & ~(00000010);

  上述程序是将寄存器P1OUT的第2位清0,其他位保持不变,因为 0 & 1 = 01 & 1 = 1,即任意数与上1,其值保持不变;与上0,则变为0。

1.2.3. 翻转寄存器某一位

  入门程序:LED闪烁就可以使用翻转输出电平的方法实现。

P1OUT ^= ~(00000010);
//equal to this:
P1OUT = P1OUT ^ ~(00000010);

  上述程序是将寄存器P1OUT的第2位进行翻转,其他位保持不变,因为 0 ^ 0 =1, 1 ^ 0 =0, 相当于一个数和0异或,1变成0,0变成1,相当于翻转电平。

1.2.4. 数据的字节分解

  在使用单片机的串口传输数据时,一般一次只能传输一个字节,而且传输时是一位一位传输的,因此对于多字节数据,需要先对字节分解,再进行传输,用移位运算符就是一个不错的解决方法。

/****分字节*****/
int a; //假设为2字节
char c1, c2;
c1 = a;  //默认取低8位
c2 = a >> 8;  //取a的高8位/****按位传输*****/
for(i=0; i<8; i++)
{if((zdata << i) & 0x80)  //取最高位,注意此处的zdata的值始终没有改变!{P1OUT |= BIT1; //SID = 1;}else{P1OUT &= ~BIT1;//SID = 0;}
}

2. 宏定义和预编译

2.1 理论指导

  参考这个链接
  其中有一点我觉得非常巧妙,那就是使用预编译实现包含头文件。

参考链接1
参考链接2

  要特别注意:如果有#ifdef或者#ifndef,在后面必须要有#endif,就像是一对花括号有其一必须要有其二,否则就会报错:unterminated conditionals。参考链接

2.2 看到一个宏定义在数据切分字节上的妙用

#define  BYTE0(some)  *((char *)(&some))
#define  BYTE1(some)  *((char *)(&some) + 1)
#define  BYTE2(some)  *((char *)(&some) + 2)
#define  BYTE3(some)  *((char *)(&some) + 3)

这个操作实现的是对数据some进行切分字节操作,非常巧妙。

2.3 define和typedef的使用区分

  最近发现一个小问题,就是一下子忘掉了define的语法,不确定到底是前面代替后面还是后面代替前面,和朋友交流之后确定了,顺便交流了一下typedef的用法,总结一下。

#define A B C

表示用A代替“B C”,即第一个空格后面的内容代替第二个空格后面所有的内容。所以在单片机中,常用短字符代替unsigned intunsigned char,比如:

#define uint unsigned int
#define uchar unsigned char

  但是这样会存在一个问题,那就是C语言中的指针char *,比如

#define CH char *
CH a, b;

等效于char * a, b,这样a就是char *类型,而bchar类型,因此如果需要定义新的类型,最好使用typedef,其语法如下:

typedef unsigned char uchar;
typedef unsigned int uint;

需要与define语法进行区分,首先是顺序的问题,typedef将前面的数据类型定义为后面自定义的名词,而define用前面自定义的名词去代替后面的数据类型,其次就是typedef语句后面需要加分号。

3. 字符串

3.1 终止符问题

  在初始化一个字符数组(字符串)时,有很多种方式,哪种情况下有终止符?

char str1[7] = "Hello!";   //默认添加终止符
char str2[7] = {'H','e','l','l','0','!'};  //默认添加终止符
char str3[7] = {'H','e','l','l','0','!','\0'}; //显示声明终止符
char str4[] = "Hello!";  //编译器自动为str4分配7个字节
char str5[14] = "Hello!";  //留下空余位置,但仍然有终止符

  一般来说,初始化一个字符数组,系统默认会在最后添加一个终止符 ‘\0’ (ASCII值为0),因此初始化时,必须要多申请一个位置,不然会报错。不过如果使用strlen函数求字符串的长度时,不会算入\0。
  但是程序允许有不带终止符的字符串,比如在程序运行过程中,最后一位不设置为\0,也是可以的,但这样在使用一些函数会出现错误,因为一般内置的函数处理字符串都以最后的\0为分界。

4. 数据类型

4.0 记录一个基础问题

int a = 0b01101; //0b 开头,表明为二进制
int b = 0xA5; //0x 开头,表明为十六进制
int c = 12; //单独一个数字,默认为十进制
int d = 012; //0开头表示八进制,即012表示 1*8+2*1=10

4.1 有符号无符号

  首先需要明确的是,有符号和无符号只会影响数据类型的取值范围,而不会影响字节数!(虽然很基础,但对于快要忘了大一C语言的人还是有用的)
  齐次需要注意的是,整形数据类型默认格式不一定是有符号型,这个和编译器有关,可以用下面的程序测试一下:

void char_type()
{char c=0xFF;if(c==-1)printf("signed");else if(c==255)printf("unsigned");elseprintf("error!");
}

参考链接

4.2 数据类型的字节数

  一个整形数据到底占几个字节的问题,可以说是写单片机程序的基础,一开始,我以为这个和单片机的位数有关(是不是8位单片机的数据长度就比16位单片机短?),但后来才知道,单片机的位数的含义是单片机一次能处理多少位数据的意思,并不直接决定单片机的性能。经过查找资料才知道数据类型的长度和编译器直接关联。所以这个东西没有规律可循,只能记忆。参考链接

4.3 布尔类型

  关于BOOLboolBoolean的辨析:学习链接

5. 变量的类型

5.1 const,static,volatile,extern关键字辨析

以下内容搬运自:https://www.cnblogs.com/nanqiang/p/9897891.html

const关键字:

  1. 阻止一个变量被改变,可使用const,在定义该const变量时,需先初始化,以后就没有机会改变他了;
  2. 对指针而言,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
  3. 在一个函数声明中,const可以修饰形参表明他是一个输入参数,在函数内部不可以改变其值
  4. 对于类的成员函数,有时候必须指定其为const类型,表明其是一个常函数,不能修改类的成员变量;
  5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

static关键字:

  • static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。再次调用该函数可以再次使用。
  • static修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是extern外部声明也不可以
  • static修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。Static修饰的局部变量存放在全局数据区的静态变量区。初始化的时候自动初始化为0
  • 不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用static修饰
  • 考虑到数据安全性(当程想要使用全局变量的时候应该先考虑使用static)

  在C++中static关键字除了具有C中的作用还有在类中的使用,在类中,static可以用来修饰静态数据成员和静态成员函数。

  静态数据成员
  (1)静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变。
  (2)静态数据成员是在程序开始运行时被分配空间,到程序结束之后才释放,只要类中指定了静态数据成员,即使不定义对象,也会为静态数据成员分配空间。
  (3)静态数据成员可以被初始化,但是只能在类体外进行初始化,若为对静态数据成员赋初值,则编译器会自动为其初始化为0
  (4)静态数据成员既可以通过对象名引用,也可以通过类名引用。

  静态成员函数
  (1)静态成员函数和静态数据成员一样,他们都属于类的静态成员,而不是对象成员。
  (2)非静态成员函数有this指针,而静态成员函数没有this指针。
  (3)静态成员函数主要用来方位静态数据成员而不能访问非静态成员。

volatile关键字:
  先来看看最常见的解释:

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

  不理解?没关系,继续看下去,之后你就会理解上面这段话了。

  要理解volatile这个关键字,首先需要知道什么是编译器优化:

  • 在一次线程内, 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值,因为直接从内存读取变量会相对较慢。
  • 当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致。
  • 当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致

  所以,本质上来说,用volatile修饰一个变量,就是告诉编译器:每次读取这个变量,都要去内存里面读取原始地址,而不要去读取寄存器中的“备份”,不然就可能会读取错误! 。举个例子:

static int i=0;int main(void)
{ ... while (1) { if (i) dosomething(); }
}/* Interrupt service routine. */
void ISR_2(void)
{ i=1;
}

  程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。
  到此,再去看最初的解释是不是更加清楚了?

下面是volatile变量的几个例子
   1). 并行设备的硬件寄存器(如:状态寄存器)
   2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
   3). 多线程应用中被几个任务共享的变量

参考链接

extern关键字:

参考链接:https://blog.csdn.net/qq_34805255/article/details/88413156

  一种非常常见的使用场合:定义一个全局变量,但是需要跨文件访问时,就需要使用到extern关键字,如下所示:

/**main.c文件**/
uint16_t num;int main()
{....
}/**Peripheral.c**/
extern uint16_t num;  //由于在另外一个文件中也用到了这个变量,如果不加这句代码会报错,
//因此需要加上,而且这样定义的num还是之前那个num,只占一个变量的存储空间。void Handler(void)
{num = 1;
}

6. 结构体struct与联合union

6.1 参考链接

  • 结构体struct和联合体union最全讲解 - CSDN
  • C语言中的结构体(struct) - CSDN
  • 结构体的声明,定义及其初始化,C语言结构体完全攻略 - C语言中文网
  • 结构体与位域的对齐 - CSDN
  • 结构体定义时不写结构体名会有什么影响吗?- 知乎
  • 结构体中的冒号 - CSDN
  • 位域定义 - 博客园

6.2 结构体语法总结

  结构体是将不同类型的数据按照一定的功能需求进行整体封装,封装的数据类型与大小均可以由用户指定。
  声明一个结构体的语法如下:

struct A
{int a;char b;
};

  需要注意的是,在声明一个结构体时,不能包含本身,但是可以包含本身的指针【链表的构成基础】(因为指针所占的字节数是固定的),而且还可以嵌套其他的结构体:

struct A
{int a;char b;struct A *B;   //结构体本身的指针struct S S1;   //另一个结构体S定义的变量S1
};

  声明结构体类型仅仅是声明了一个类型,系统并不为之分配内存,就如同系统不会为类型 int 分配内存一样。只有当使用这个类型定义了变量时,系统才会为变量分配内存。所以在声明结构体类型的时候,不可以对里面的变量进行初始化还有一个资料显示:“结构体名”的命名规范是全部使用大写字母。
  如果需要定义一个结构体变量,可以直接在声明时最后的分号前加上。

struct A
{int a;char b;
}A_a;/**与下面程序等效**/
struct A
{int a;char b;
};struct A A_a;

  需要注意与带有typedef的语句进行区分。

typedef struct A
{int a;char b;
}AAA;    //相当于声明一个结构体A并将其重命名为AAAAAA A_a;  //定义一个结构体A类型的变量A_a

  补充:有时候定义结构体会直接省略结构体的类型名:

typedef struct
{int a;char b;
}AAA;

  因为本身结构体名只不过是一个标签罢了,定义变量时还是得加上关键词struct
  结构体内部成员的访问有两种方式:

typedef struct A
{int a;char b;
}AAA;AAA A1;
d = A1.a;     //访问内部成员用符号”.“AAA *A2;
c = A2->b;   //定义一个指针,访问内部成员使用”->“

结构体内存对齐问题
  我们可能在学C语言的时候,会依稀记得一个结论:结构体占有的内存大小就是其内部所有成员占有内存之和。但是实际上,某些硬件平台要求数据的存储必须从特定的地址开始,这就有了结构体成员内存对齐的要求,也就是成员之间可能会存在一些“空位置”,这就导致结构体实际占据的内存大小要比内部成员占有内存之和要大一些。

  这个“几字节边界上对齐”是指从结构体内存起点开始算的,比如int只能在第0,4,8字节处开始存储,所以结构体成员定义顺序的不同会导致最后结构体占有的内存大小不一样。

结构体中的位域
  在结构体中定义成员变量,有时候不需要完整的一个数据类型的宽度,比如一个布尔量只需要1位来表示,为了节省内存空间,可以采用位域的语法。

  所谓 “位域” 是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

  其语法如下所示:

struct bs
{int a:8;int b:2;int c:6;
}data;

  这几句代码表示 “data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位”。但是位域的使用需要注意三点:

  • 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。比如:
struct bs
{unsigned a:4;unsigned :0; /*空域*/unsigned b:4; /*从下一单元开始存放*/ unsigned c:4;
}
//这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。
  • 位域可以没有域名,但是这样的位域不能使用。比如:
struct k
{int a:1;int :2; /*该2位不能使用*/ int b:3; int c:2;
};
  • 位域的位数不能超过数据类型,比如51单片机中的int为16位,则位域不能超过16位。

综上所述,其实位域也算是一种数据类型,只不过是二进制位定义的罢了。

6.3 联合union语法总结

  union即为联合,其最大的特征就是其内部成员共用一片内存,且其各自首地址相同。因此,对于结构体内部的成员,有一个特殊的性质:那就是访问任意一个变量,其他的变量都会自动更改。

union A
{int a;int b;
};int main()
{union A AAA;  //定义一个联合AAA.a = 10;printf("%d", AAA.b);  //此时输出的b的值就是a所被赋的值
]

  从这个程序可以看出,所谓的联合实际是开辟出了一个空间,然后确定了空间的首地址,其内部的各个变量就像是这小片内存中访问的“接口”一样,可以随意访问。这也体现出union这个数据类型的灵活之处。至于这个union到底占据多大的内存,是按内部变量占据内存最大的来算。
  下面举一个利用联合实现二进制位连接的妙用

typedef union
{struct{unsigned short Data : 8;unsigned short Addr : 4;unsigned short : 4;}B;unsigned short R;
}T;

  这个程序需要处理一个双字节的数据,但是这个双字节包含了三个部分,如果一起赋值较为麻烦,因此考虑使用位域的数据类型,同时利用联合将三个打散的部分连接起来,即赋值的时候各部分单独赋值,读取的时候一次性读取。妙!

7. case语句使用

参考链接

【单片机】C语言总结相关推荐

  1. c语言程序设计分段定时器,单片机C语言编程定时器的几种表达方式

    原标题:单片机C语言编程定时器的几种表达方式 吴鉴鹰单片机开发板地址 店铺:[吴鉴鹰的小铺] 地址:[https://item.taobao.com/item.htm?_u=ukgdp5a7629&a ...

  2. 单片机如何使用?51单片机C语言编程实例有哪些?

    大家好,我是无际单片机编程的徐明,今天和大家一起探讨一下"单片机如何使用?" 单片机如何使用,我们要知道单片机在哪里使用? 单片机是很多电子产品的核心器件,它具有一定的逻辑判断和事 ...

  3. 单片机c语言编译软件6,eUIDE下载-单片机c语言编译器 v1.07.32.23 官方版 - 安下载...

    eUIDE是一款专业的单片机c语言编译器,EM78系列集成开发环境是面向项目的ELAN EM78系列微控制器的开发工具,它包括UICE开发在线仿真器和eUIDE软件工具:eUIDE是基于PC端的UIC ...

  4. 访问外部扩展C语言编程,单片机C语言编程(系统扩展IC)8.ppt

    单片机C语言编程(系统扩展IC)8 第8章 单片机系统扩展 第8章 单片机系统扩展 目 录 8.1 扩展并行三总线 8.2 扩展简单并行输入/输出口 8.3 扩展并行数据存储器 8.4 串行扩展总线接 ...

  5. 单片机c语言编程教学大纲,《单片机C语言编程》教学大纲

    <单片机C语言编程>教学大纲 课程代码:000002336 课程英文名称:Microcontroller C Programming Language 课程总学时:24 讲课:16 实验: ...

  6. pic单片机延时程序C语言,PIC单片机C语言延时程序和循环子程序实现方法

    PIC单片机C语言延时程序和循环子程序实现方法 很多朋友说C中不能精确控制延时时间,不能象汇编那样直观. 其实不然,对延时函数深入了解一下就能设计出一个理想的框价出来. 一般的我们都用 for(x=1 ...

  7. 单片机c语言程序设计实训报告,(整理)单片机C语言程序设计实训100例.doc

    (整理)单片机C语言程序设计实训100例.doc .单片机C语言程序设计实训100例基于8051Proteus仿真案例第 01 篇 基础程序设计01闪烁的LED/* 名称闪烁的LED说明LED按设定的 ...

  8. c语言错误 xef代表什么,单片机C语言代码手册 含100多个经典C程序

    1 单片机单片机 C 语言代码手册语言代码手册 1 LED 灯灯 点亮一个点亮一个 LED include void main while 1 P0 0 x01 P2 0 x7d 流水灯闪烁流水灯闪烁 ...

  9. pic单片机c语言存储器定义,PIC单片机C语言程序设计1 7.PDF

    PIC单片机C语言程序设计1 7 学电子跟我来FOLLOW ME PIC 单片机C 语言程序设计(1) ◆ 丁锦滔 编者按:为了帮助具有PI C 单片机汇编语言知识的技术人员或工程师,快速掌握利用C ...

  10. C语言对p1口取反,单片机c语言编程基础(5页)-原创力文档

    单片机的外部结构: 1. DIP40双列直插: 2. P0,P1,P2,P3四个8位准双向I/O引脚:(作为I/O输入时,要先输出高电平) 3. 电源VCC(PIN40)和地线GND(PIN20): ...

最新文章

  1. 写得太好了!树莓派安装docker
  2. 从生活角度学习应用程序、虚拟目录、应用程序池(解惑篇)
  3. 10人以下小团队管理手册-学习笔记
  4. Linux服务器出现:No space left on device的解决方法
  5. java 数据类型转换的一场_Java数据类型之间的转换
  6. android gdbserver
  7. 5G及移动边缘计算(MEC)
  8. 3-产品经理学习笔记之产品经理的工作职责和能力模型
  9. Stream系列(六)Match方法使用
  10. mysql的auto_increment报错1467
  11. ArcGIS 每天一个高级制图技巧:5 lyr和UpdateLayer方法实现样式复用
  12. cocos2d-x实现一个PopStar(消灭星星)游戏的逻辑分析及源码
  13. 403forbidden提示没有权限
  14. python根据模板中的MML,批量生成小区脚本
  15. 计算机产品可以进项抵扣,企业购入的软件产品可以全额抵扣进项税吗?
  16. unity2018 Image使用Sliced九宫格进行调整
  17. 微信多订单合并付款_拼多多只能微信支付吗?拼多多合并支付有什么优势?
  18. java中常用的工具类
  19. ROS 下的仿真小乌龟
  20. 非标自动化设备涉及的行业有哪些?

热门文章

  1. 查找算法06-哈希查找
  2. 记一次服务器清除挖矿木马操作记录
  3. 原来jdk自带了这么好玩的工具 > jstat使用教程
  4. 有关于计算机音乐论文题目,音乐类的论文题目写什么好啊?
  5. LWJGL入门指南:使用《我的世界》(Minecraft)同款游戏库开发一个超级“简单”的3D射击游戏
  6. 全局变量和局部变量的区别
  7. Java开发工具之使用cmd安装MySQL数据库
  8. 从计算机应用基础中学到了什么,计算机应用基础教学心得体会.docx
  9. Python代码练习
  10. U盘防毒,我有绝招。。。