下面我们来讲GPIO和I2C。
我们先来看看gpio的框图:
在嵌入式系统中,在一个芯片上面有CPU,有gpio,有串口,有USB等等外设模块。
有一些模块平时为了省电,他是不工作的。
你要去使用它,要先使能它,就是给他提供时钟。
一个芯片,它外面有密密麻麻的引脚,这些引脚接到芯片内部哪一个模块?
因此还需要去选择这些引脚的功能。
无论是什么芯片,对于gpio它操作都是这几个步骤:
第1步:使能GPIO模块
第2步:接着就是去设置芯片的外围引脚,让它连接到GPIO模块;
第3步:选择它的方向,选择输出还是输入;GPIO的意思就是通用的输入输出脚,有输入、也有输出;
第4步:就是去操作数值:
4.1 对于输出引脚,我们可以让他输出高电平,低电平;
4.2 对于输入引脚,我们可以去读取它的当前电平,得到高或低;
我们使用HAL库的时候,并不需要我们深入到寄存器,
所以我们大家来浏览一下代码,就可以了。
这些代码都是cubemx生成的,
如果想看到寄存器操作的话,在后面我们讲ARM架构的时候会深入到寄存器。
这个函数先使能了GPIO模块,
并且把引脚连接到了GPIO模块,
再去设置了他的方向,
最后,对于输出引脚就去写它的值,对于输入引脚,就去读他的值
答: 一个链表可以是空,可以有一个元素,也可以有多个元素。只有一个元素,它也是链表。
答: pHead它是个变量, 是变量必定在内存里面,必定有地址。我们用这个pHead来保存第1个元素的地址。
答: pHead是一个链表头,如果他不是空的话,他肯定等于某一个结构体的地址。这个结构体在内存里,它的地址一般来说都不会等于0。所以我们使用NULL(就是零),用来判断一个链表,它的值是不是有效的,也就是否指向一个结构体。
答: 在一开始的时候,A的next_address是NULL,这里是再一次复制,为了确保万无一失。
对于全局变量你没有给它赋值的话,它的初始值就是0,但是对于局部变量你没有给它赋值的话,它的初始值并不一定是0。
对于使用malloc分配出来的结构体,你没有给它赋值的话,它的初始值也并不一定是0。
答: 如果不设置为NULL的话,我们无法分辨next_addr是否有效。
答: 要去遍历这个链表,要把里面的每个元素拿出来判断一下,那肯定得定义一个局部变量。
答: 常规的用法是:
答: 我们来画一下这个链表的内存图:
这个list结构体,它里面又有其他结构体。
这个RootNote它里面放的是指针:指针都是4字节。
答: 你排队的时候总得找到队伍的头部呀,否则在茫茫人海中,你怎么去找到你的队伍。
答:
一个芯片里面有很多模块,就比如说上面的图里面,里面除了CPU之外,还有gpio,还有UART。
一个芯片的外面,你看它的周边那些引脚,是已经确定下来了,就是上面图中那些白色的发光的引脚。
但是这些引脚要接到芯片里面哪一个模块呢?这是可以编写程序来控制的。
就比如说上面图里那些蓝色的外围的引脚,
可以接到gpio的红色的引脚,也可以接到UART的绿色的引脚。
答: 这样理解没问题,那个高电平它都是有一个范围的,并不是说一定要到达3.3V,一定要到达5V。比如引脚的电压是1.7伏到3.3伏,我们就可以认为它是高电平。(1.7V这个数字是随便举例的)
答: ST公司建议使用HAL库,为了效率,有库就用库,没库再操作寄存器。
如果你要把它配置成输出引脚,在变为输出引脚的瞬间,他就会输出高电平或者低电平。
那么在变为输出引脚的瞬间,你要让他输出高电平还是低电平?
是不是在设置为输出引脚之前,我们先去设置它的输出值?
这就是答案了。
assert_param
怎么理解?有效性?答:
简单的说他就是一个判断语句:
void assert_param(expr)
{
if (expr)
assert_failed();
}
大概就这意思,他会打印某些语句。
答:
答: 只要去打开芯片手册,一看就明白了,我给你演示一下。
你看这个图,这就是103这个芯片,里面左上角是CPU。
其他都是各种:片上设备。
所谓片上设备就是:在芯片里面、CPU之外那些设备,也就是GPIO,UART模块呀,flash等等。
在芯片之外,我们还可以连接各种设备,这些设备叫做片外设备,就比如说103可以通过串口来连接WiFi模块,这个WiFi模块就是103这个芯片之外的设备。
答: 理解的非常到位。
答: 堆和栈它是不一样的,今晚下课之后,对这个问题比较感兴趣的同学,可以去看这两个视频。
堆和栈都是内存,不是flash,我来画一个图:
我们写好程序之后,烧写进flash。
开板断电,没有程序运行,没有人去使用内存。
开发板上电,程序运行的时候要去初始化全局变量。
再次默念我们的口诀:变量变量变量,可以变,必定在内存里。
但它这些全局变量,它的初始值在哪里,初始值肯定在程序里,肯定在flash上面。
所以这个程序的代码会使用这些初始值去写内存,就是去初始化变量。
好了,内存有一块区域,用来保存这些全局变量。
那么局部变量呢,保存在哪里?保存在栈里。
栈起什么作用?在C语言里面必须使用栈,栈也指向一块内存。
要想弄清楚栈、局部变量,他们是怎么使用的,这会涉及一些汇编。
我先讲完堆,再给大家讲栈。
我要假设你们知道了栈的作用,
内存有一部分用来保存全局变量,
有一部分用作栈,
剩下的那一部分怎么办?
剩下的那一部分你可以用也可以不用。
你想用的话,你就可以把它当做堆,也就是说你得去管理,在上面实现malloc函数,实现free函数。
在我们这个图里面,你看对于内存它分为三部分。
堆,就是一块空闲的内存,你去使用它,它就叫做堆,你不用他,他啥都不是。
我们怎么使用它呢,我来举一个最简单的例子:
我们假设这块内存的起始地址是addr1,结束地址是addr2。
我来写一个malloc函数,最简单的:
我给大家简单讲解一下这个函数:
这就是最简单的malloc函数,但是它只能够实现分配,不能够实现释放。
为什么不能够实现释放呢?
int addr = malloc(100);
free(addr); // 你怎么知道释放多大?
我们申请一块内存的时候,得到他的地址,释放的时候也传入他的地址。
在我们这个例子里面,我怎么知道你要释放了,这块内存多大呀?根本就没有办法知道。
所以我们使用这种方法实现的分配函数,它不支持释放,对于真正的分配函数,我们应该怎么做?
我再举一个稍微复杂点的例子:
我们来看这个图,他要去分配100字节的空间,他并不仅仅分配这100字节。
他还会额外分配一个头部,在头部里面会保存有100这个数字,以后去释放这块空间时:free(p)
。
他会根据这个P往前面查,找到这个头部,从头部就知道了,你要是释放的是100字节的空间。
所以对于堆,如果越界访问的话,就有可能会破坏掉头部信息,整个系统什么时候崩溃都不知道。
我们现在大概知道堆的原理之后,我们就来看看堆有什么作用。
就像我们之前讲的那个例子,你的班主任让你去记录每个同学的信息,
因为同学的人数是不能够事先确定的,这个时候你就可以使用堆来动态分配结构体
答: malloc函数本身没有办法确定可用的堆的大小,他只有一个返回值,要么成功要么失败。
答: 正确。
答: 防止栈溢出的话,就尽量的少定义非常庞大的局部数组。
答: 你要确定malloc管理的内存的边界。
答: 是的
答: malloc是从堆里分配空间,不是反过来说:堆是用malloc申请。
答: 我们可以通过编译器来确定,甚至可以直接定一个巨大的数组,把这个数组就当做堆,freertos里面就是把一个巨大的数组用作堆。
答: 是呀,堆的含义是什么?堆的含义就是一块空闲内存嘛,这个全局数组我不用它,它就是空闲内存。
答: 对于栈,我找个程序,需要给大家展示汇编,才能讲清楚。
int add(int a, int b)
{
volatile int i = 10;
while (i--);
return a+b;
}
int main(void)
{
int c;
c = add(1,2);
return 0;
}
1.main函数调用add函数
add的函数执行完之后,返回到内函数的哪里?
我们可以看得出来他会返回到这个语句:return 0
。
但是在add的函数里面,他怎么知道返回到这个语句?
是不是得由main函数告诉他呀?
是的,那么main函数怎么告诉add的函数,你的返回地址是下面的那条语句?
了解这个流程就行了:
下面我们进入add函数:
你看现在就用到了栈,栈这块内存它的使用情况,我来画一个图:
在栈里面他使用了两个空间,
一个用来保存函数的返回地址LR,另外一个用来保存R3。
接下来我们定义了一个局部变量i,它的初始值是10。
volatile int i = 10;
我们说局部变量保存在栈里,它是怎么体现的呢?
看箭头的代码,他让寄存器R0等于10,然后把R0的值写到栈里面去。
我来画一个图:
看到了吧,变量i它在内存哪里呀?在栈里面。
所以看到这里我们就知道了:
1.栈,他会用来保存返回地址
2.也会用来给局部变量分配空间
大家不用灰心,这部分讲的内容超纲了,后面我们讲ARM架构的时候会从头教大家,先从汇编教起。
**答: **
1.先讲FreeRTOS快速入门
2.再讲ARM架构
3.最后讲 FreeRTOS内部实现
答: FreeRTOS它就几个文件,裁剪也裁剪不出什么东西,主要是配置各种宏。某个函数,你要去使用它的话就得去打开某个宏