嵌入式C语言强化学习——(嵌入式学习路线1)
前言
从零开始记录自己学习嵌入式,之前看稚晖君的视频,里面的费曼学习法觉得对自己很有帮助,所以准备把自己学习到的东西分享出来,便于学的更深(符合费曼学习法)
众所周知,嵌入式编一般是使用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;elselast = last->next;}last->next = newspy;newspy->next = NULL;}
}
有了链表的插入,那么必然有链表的删除,如何删除链表呢。
void remove_spy(p_spy old_spy)
{p_spy p_leftif (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开始时,才开始初始化的。
最后说的话
终于把这个弄完了,我的拖延症啊。感谢韦东山老师这么好的课程。这也是作为自己学习的后记,欢迎大家进行探讨,文章中还有很多细节的地方需要完善,欢迎大家提出意见,以后会逐渐改善的。
嵌入式C语言强化学习——(嵌入式学习路线1)相关推荐
- 嵌入式C语言编程课件,嵌入式系统C语言编程基础PPT课件
<嵌入式系统C语言编程基础PPT课件>由会员分享,可在线阅读,更多相关<嵌入式系统C语言编程基础PPT课件(81页珍藏版)>请在人人文库网上搜索. 1.嵌入式系统C语言编程基础 ...
- 嵌入式 c语言 面试题,嵌入式面试题-持续更新
1.用预处理指令#define声明一个常数,用以表示1年中有多少秒(忽略闰年问题). #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL //最后的U ...
- 嵌入式c语言开发闹钟,嵌入式电子闹钟()时钟课程设计.doc
#include //头文件 #include #define uchar unsigned char //宏定义 #define uint unsigned int //位定义 sbit rs=P2 ...
- uml+oopc嵌入式c语言开发精讲_当前火爆的嵌入式领域,为什么选择C语言作为开发语言?了解一下...
C语言和嵌入式C编程有什么区别?其区别在于嵌入式的C语言是跑在嵌入式的开发板上的,CPU和电脑不一样,所以编译器也是不一样的,生成的可执行程序也是不一样的.选择嵌入式开发语言归结于嵌入式系统开发的特点 ...
- 知名外企嵌入式C语言笔试试题
知名外企嵌入式C语言笔试试题 嵌入式C语言笔试试题 知名外企嵌入式C语言笔试试题 一. 选择题 二. 填空题 三.写出程序的运行结果 一. 选择题 01.C语言合法的常量是() A-45 B.078 ...
- 05 C语言框架讲解(嵌入式C语言要学哪些内容,学习路线)
C语言框架讲解(C语言要学哪些内容,学习路线) b站配套视频地址 CSDN有图阅读地址 Gitee文档源码地址 作者 将狼才鲸 日期 2021-07-17 注:gitee暂不支持markdown me ...
- linux嵌入式如何学习,嵌入式Linux要如何学习?嵌入式linux学习路线讲解
在学习嵌入式linux之前,一定要有C语言基础,而且是越熟练越好.对于汇编基础来说没有那么重要,当然对于C语言来说起码能够熟练写出一个数组排序.输入数字求和之类的程序代码.当然如果不熟悉的话就需要多写 ...
- 初步认识嵌入式:Linux的初步了解与C语言基础的初步学习
一:首先了解了嵌入式的基本知识结构: 基础: 编程 相关 C语言编程 C语言基础 Linux系统基础 C语言的高级编程 如何使用C语言 数据结构: 如果更好的编程程序 应用层: 侧重 应用软件 程序 ...
- 新手小白学习嵌入式系统怎么学?嵌入式学习路线
嵌入式可以说是当下有前途的IT应用领域,小到电子词典.手机,大到航空航天设备都是典型的嵌入式系统,因此,通过嵌入式技术人才的职业发展空间是非常大的.那么想进入嵌入式行业,新手小白学习嵌入式系统怎么学呢 ...
最新文章
- python基础教程3-Python基础教程(三)
- 集合添加数据类型出现:unhashable type: ‘list‘
- c语言通讯录以及写入文件,学C三个月了,学了文件,用C语言写了个通讯录程序...
- Amazon Aurora 深度探索
- 可燃气体浓度多少合格_安燃无恙 | 可燃气体报警器的常见故障处理
- 苹果cms10 官方QQ微信防红防封代码
- HashSet底层存储元素的源码分析
- MongoDB,无模式文档型数据库简介
- Ubuntu下Postfix邮件服务器安装及基本的设置
- java中根据秒生成cron表达式_在线Cron表达式生成器
- Origin—在曲线的每个峰值处均使用高斯函数进行拟合
- android通过经纬度获取地址,Android编程实现根据经纬度查询地址并对获取的json数据进行解析的方法...
- 非线性优化问题---MATLAB
- 基于深度学习的核磁共振影像重建
- 如果域名被劫持该怎么办?有什么应对方法?
- python数据分析-concat合并表,报错InvalidIndexError: Reindexing only valid with uniquely valued Index objects
- 前端vue3 tsx风格开发
- 智能手机 + 机器学习 = 个人终端的未来
- 如何修改Bootstrap Table中table-striped的颜色
- JavaScript自定义求和函数
热门文章
- windows下nslookup命令
- 视频教程-仿淘宝客户端电商平台android初级速成-Android
- VS2010连接数据库的操作(SQLServer2005/2008 以及Access2007/2003等)
- JPA基础知识----JPA 基本注解,JPA API
- 基于74LS161实现的多功能数字钟
- Scala学习小计 - 什么是模式匹配(pattern-matchin)?
- Solidity基础教程2——Safemath
- MongoDB勒索事件中,DBA们到底该学到什么?
- 业务设计师(产品经理)P级晋升必备职能(P3-P7)
- 皕杰报表在chrome中emitter=print无效问题的解决