前言
从零开始记录自己学习嵌入式,之前看稚晖君的视频,里面的费曼学习法觉得对自己很有帮助,所以准备把自己学习到的东西分享出来,便于学的更深(符合费曼学习法)
众所周知,嵌入式编一般是使用C语言(也有用python之类的,比如树莓派pico,以后的文章会写到),所以C语言的基础很重要。今天看了韦东山老师的嵌入式C语言加强课,来总结一下。
课程链接:韦东山老师嵌入式C语言加强,全天8小时直播,吐血整理可以分集观看!
口诀
韦东山老师两个口诀:
- 变量能变,可读可写,必放内存
- 指针放地址,32位处理器地址必32位,所有类型地址指针均是4个字节(1个字节8bit)
变量和指针
在学习变量之前,首先知道单片机芯片的内部构成。一般单片机由以下几项构成。
-
CPU
CPU类似于单片机的大脑,可以进行数据的运算和指令的执行 -
RAM(Random Access Memory)
内存一般是用于存放变量的地方。变量就像一个东西一样被放在内存中。RAM被读取速率快,缺点是掉电就会丢失数据。用它的英文名字来记住他 Random Access Memory(随机存取存储器) -
FLASH(闪存)
闪存是ROM(Read Only Memory)中的一种,它不仅具备电子可擦除可编辑(EEPROM)的性能,还不会断电丢失数据同时可以快速读取数据。 -
总线
-
外设
-
电源
-
PLL锁相环
-
时钟管理器
总线之类的会在后面的文章中提到(挖坑ing),现在来说RAM和FLASH。变量是放在RAM中的,也就相当于程序每次重新启动之后,系统会重新的在内存中为变量开辟位置,同时根据程序中的值,为变量赋值。同时在有的单片机程序中,定义了但是没有被使用的变量,不会被分配地址。这和单片机的性质有关。单片机和那些资源丰富的机器比起来,内存的管理也是很重要的。有时我们并不想自己未使用的变量被被忽略
int main(void)
{
int a;
char c;
char buf[100];
}
例如在上面的程序中,a, c, buf[100],在程序中并没有被使用,所以编译器不会分配地址给他。但是有时我们需要我们为了防止编译器优化它,就会使用到volatile(易变的)这个关键字。
int main(void)
{
volatile int a;
volatile char c;
volatile char buf[100];
}
如果想知道 a 的地址,可以在调试中,对 a 变量进行取址,便可以查看到。但是在生成的map文件中,无法看到。因为局部变量是在运行时,在栈中临时生成的。在map文件中,你查看到的是全局变量,静态变量。
volatile在嵌入式中是一个很重要的关键字。
例如:
int main(void)
{
int a;
a=1;
a=2;
}
在上述程序中,正常情况下a=1会被编译器优化掉,但如果在定义a时,在前面加上volatlile。可以保证关于a的所有指令都被执行。同时在一般的程序中,为了优化,变量值的一般都是先从RAM中被读到CPU中,使用时,现在CPU中去读变量的值,修改时,也是在CPU中去改变变量的值。并不会去写内存。加上volatile后,确保了每次CPU会每次去读写内存。而不是在CUP中改变。一般来说访问硬件寄存器时,会加上volatile.
与volatile相似,const也是一个C语言中的关键字。
const 表示这个量可以被放在FLASH中,被视为常量,不必放在内存中,可以节约内存。同时被const修饰的变量不可以被改变。
关键字中的static,表示为,这个变量的工作范围仅在我这个文件之中,而不加static,则表示这个变量在整个程序的范围之中。当在文件中已经用static定义了变量,则优先使用文件中用static定义的变量。
关键字的extern,它修饰在变量前的话,表示这个变量已经在其他文件中定义了,被这个文件拿来使用。就是让程序知道,他是一个外部变量。同时它仅仅可以被用于声明这是一个什么东西,后面不可以接可执行语句。
指针始终是一个变量,在32位的处理器之中,始终是4个字节的。
在函数中定义的不同类型的变量:
在MAP文件中显示的变量分配的地址:
一般来说,只读的常量是会被放在FLASH中以节省内存地址,但是有时编译器也会把常量放在RAM中,方便调用。
同时在C语言中,我们想知道变量的大小,可以使用sizeof()来进行测量,sizeof内的变量会被自动替换成变量的类型。如果要知道指针的大小(由以上的P为例子),应使用sizeof(p),而不是sizeof(*p)。
关键字struct
struct就必定会提到结构体。在程序中,我们想重复的使用很多种类型相同,但是数据不同的变量。例如:
传统方式:
char *name = "mao_nan_bei";
int grade = 12;
char *name2 = "gou_dong_xi";
int grade2 = 11;
...
这样的方式定义少量的还行,但是定义大量的话,不仅工程量大,也不易于查找自己想要的类型。但是使用结构体的话,可以解决这个问题。
结构体方式
struct people{
char *name;
int grade;
};
struct people.mao = {"mao_nan_bei",12};
printf("name=%s,grade=%d", mao.name,mao.garde);
结构体中既可以有基础的变量类型,也可以有其他的结构体,例如
struct people{
char *name;
int grade;
};
struct class{
char *nick_name;
struct people student[100];
};
对于结构体我们同时要明确,声明结构体是不占空间的,定义结构体才会占用一定的空间。(变量才会被分配空间)。在结构体中,我们使用char *name 而不是char name[100];是为了节省内存空间。
通过指针进行赋值
讲指针之前,我们先对变量赋值的一个过程进行一个简单的描述、
例如:
int a = 3;
上面这行指令赋值的过程,为CPU先读FLASH得到指令,在内存中开辟一个int大小的空间,最终再将变量的值放入空间之中。指针中始终存放着地址,可以通过访问地址的方式,来改变变量中的值。在结构体中,也可以使用指针,如下:
int *p;
struct people *pt;
int a;
p = &a;
*p=123;
pt = &mao;
pt->grad = 16;
typedef使用方法
typedef一般是用于创建类型别名的。一般struct会和typedef联合在一起使用。以便于使用结构体。
typedef struct people{
char *name;
int grade;
}people;
people.mao = {"mao_nan_bei",12};
结构体指针&函数指针
C语言在结构体中也可以使用指针
struct student{
char *name;
int age;
struct student classmate;
};
以上的写法是错误的,一直递归也无法知道结构体的大小。但是可以使用的方式进行定义。
typedef struct student{
char *name;
int age;
struct student *classmate;
char *sex;
}student,*pstudent;
由于指针的大小是被确定的,所以使用这种方式,可以使结构体的大小确定,不至于被无限套娃。同时利用typedef确定了结构体名称为student,结构体的指针为pstudent。
当我们想根据结构体中相同变量的不同值来确定是使用什么样的函数时,我们可以通过判断结构体中这个变量的方式来判断该使用什么样得函数。但是这样的话,当结构体数量过多时,会使得有很多的判断,这时我们可以使用函数指针。
如何使用函数指针呢‘
void (*play_ball)(void);
函数指针在32位的处理器中,也仅有4个字节。函数指针可以直接使用函数名为地址,不需要使用取址符号。
typedef struct student{
char *name;
int age;
void (*good_work)(void);
struct student *classmate;
char *sex;
}student,*pstudent;
链表
明确了指针的概念之后,链表就更简单了。为了更好的理解,我们用间谍来举例子。
typedef struct spy{
char *name;
struct spy *next;
} spy,*p_spy;
p_spy head = NULL;
spy A = {"A",NULL};
spy B = {"B",NULL};
spy C = {"C",NULL};
A.next = &B;
B.next = &C;
C.next = NULL;
head = &A;
A的下线是B,B的下线是C。head存贮的是A的地址,而A.next中存放的是B的地址,B.next中存放的是C的地址。如何打印出全部的对象呢。可以使用指针的方式。
while(head)
{
printf("%s",head->name);
head = head->next;
}
其实初始时,head取址A,在输出A的值之后,由于A.next中存放的是B的地址,B的地址被赋值给head,相当于对B的地址进行取址。
链表的实质,即是自身包含下一个变量的地址。那么链表如何插入新得变量呢。可以使用以下的方式。
void insert_spy(p_say newspy)
{
p_spy last;
if (head==NULL)
{
head = newspy;
newspy->next = NULL;
}
else{
/*先找到链表的最后一项,last*/
last = head;
while(last)
{
if(last->next == NULL) //找到了
break;
else
last = last->next;
}
last->next = newspy;
newspy->next = NULL;
}
}
有了链表的插入,那么必然有链表的删除,如何删除链表呢。
void remove_spy(p_spy old_spy)
{
p_spy p_left
if (head == oldspy)
{
head = oldspy->next;
}
else
{
//找出oldspy的上线
left = head;
while(left)
{
if(left->next == oldspy)
break;
else
{
left =left->next;
}
}
if(left)
{
left->next = oldspy-next;
}
}
}
ARM架构和汇编
回到一个简单的程序。
int a = 1;
int b = 2;
a = a + b ;
a+b的操作经历了几步呢,经历了四步。运算全部是在CPU中完成的,第一步,先使CPU在RAM中读取a的值,再用CPU在RAM中读取b的值,然后在CPU中计算a+b。第四步,将a+b的值写入ARM。既然要从RAM中取值,那么CPU内部必然存在可以存放数据的单元,这一单元被称为寄存器。可以用汇编指令直观表示a+b的的过程。汇编指令被写在FLASH中。
a+b的汇编表示。
LDR R0,[a]
LDR R1.[b]
ADD R0,R0,R1
STR R0,[a]
常用的汇编语言如下:
汇编语句 | 执行意义 |
---|---|
LDR R0,[R1] | 表示读取R1地址上的数据保存到R0中 |
LDM SP,{FP,SP,PC} | 按照高编号寄存器存放高地址内存值的原则,分别将FS,SP,PC所对应内存上的数据写到栈空间 |
STR R0,[R1] | 表示将R0的值写到R1所对应的内存空间上 |
B main | 表示跳转到main函数执行 |
MOV r0,r1 | 表示把r1的值赋值给r0 |
ADD | 把两个操作数加起来把结果放到目的寄存器中 |
BL | 是另一个分支指令 |
BIT | 位清除指令 |
几个核心问题
- 全局变量的初始化
- 局部变量在哪
- 局部变量的初始化
- 栈的作用
全局变量在断电时,是不会存在于RAM中的,在通电运行时,才会存在于RAM中。全局变量的初始值是来自于代码(FLASH)中的。初始化全局变量有两种方式。
第一种方式:现在FLASH上的某个位置读取到数据,然后将值写道全局变量所在的地址上。这个方法简单,但是效率很低。
第二种方法:把多个数据整合成一个数据段,在运行数据时,将数据段整段整段的拷贝在RAM中。类似于memcpy。
那么对于初始化为0,或者没有初始化的道得全部变量怎么初始化呢。像这种变量,在内存中都放置在ZI段上。类似于MEMSET,初始化时,将ZI段全部清零。之后采取调用main函数。
在main函数之中,我们如何理解局部变量呢。这就要提到栈了。局部变量是在栈中的。栈是什么呢,栈是一块空闲的内存。在main中,函数的调用分为两步。先记录返回地址,再执行函数。在函数中调用函数的话,就涉及到最开始函数的地址被弄会被覆盖,此时最开始的函数地址会被放入栈中。局部变量是在main开始时,才开始初始化的。
最后说的话
终于把这个弄完了,我的拖延症啊。感谢韦东山老师这么好的课程。这也是作为自己学习的后记,欢迎大家进行探讨,文章中还有很多细节的地方需要完善,欢迎大家提出意见,以后会逐渐改善的。