【RTOS训练营】GPIO知识和预习安排 + 晚课提问

一、GPIO

下面我们来讲GPIO和I2C。

我们先来看看gpio的框图:

在嵌入式系统中,在一个芯片上面有CPU,有gpio,有串口,有USB等等外设模块。

有一些模块平时为了省电,他是不工作的。

你要去使用它,要先使能它,就是给他提供时钟。

一个芯片,它外面有密密麻麻的引脚,这些引脚接到芯片内部哪一个模块?

因此还需要去选择这些引脚的功能。

无论是什么芯片,对于gpio它操作都是这几个步骤:

第1步:使能GPIO模块

第2步:接着就是去设置芯片的外围引脚,让它连接到GPIO模块;

第3步:选择它的方向,选择输出还是输入;GPIO的意思就是通用的输入输出脚,有输入、也有输出;

第4步:就是去操作数值:

		4.1 对于输出引脚,我们可以让他输出高电平,低电平;
		4.2 对于输入引脚,我们可以去读取它的当前电平,得到高或低;

我们使用HAL库的时候,并不需要我们深入到寄存器,

所以我们大家来浏览一下代码,就可以了。

这些代码都是cubemx生成的,

如果想看到寄存器操作的话,在后面我们讲ARM架构的时候会深入到寄存器。

这个函数先使能了GPIO模块,
并且把引脚连接到了GPIO模块,
再去设置了他的方向,
最后,对于输出引脚就去写它的值,对于输入引脚,就去读他的值

二、预习安排

三、晚课学员提问

1. 问: 向链表放了一个元素,不算链表吧?

答: 一个链表可以是空,可以有一个元素,也可以有多个元素。只有一个元素,它也是链表。

2. 问: 晚课示例链表中的phead,在内存中也应该有地址吧?

答: pHead它是个变量, 是变量必定在内存里面,必定有地址。我们用这个pHead来保存第1个元素的地址。

3. 问: 晚课示例链表中的phead,为什么是NULL?

答: pHead是一个链表头,如果他不是空的话,他肯定等于某一个结构体的地址。这个结构体在内存里,它的地址一般来说都不会等于0。所以我们使用NULL(就是零),用来判断一个链表,它的值是不是有效的,也就是否指向一个结构体。

4. 问: 晚课示例链表中, A.next_addr = pHead 等于初始值 NULL,A.next_addr不是一直是NULL吗?

答: 在一开始的时候,A的next_address是NULL,这里是再一次复制,为了确保万无一失。

对于全局变量你没有给它赋值的话,它的初始值就是0,但是对于局部变量你没有给它赋值的话,它的初始值并不一定是0。
对于使用malloc分配出来的结构体,你没有给它赋值的话,它的初始值也并不一定是0。

5. 问: 晚课示例链表中, A.next_addr不设置NULL会发生什么?

答: 如果不设置为NULL的话,我们无法分辨next_addr是否有效。

6. 问: 晚课示例链表的删除中,在函数内定义局部变量tmp找特定节点的思路是怎么来的呢?自己做的话很有可能无法想到定义一个temp变量。

答: 要去遍历这个链表,要把里面的每个元素拿出来判断一下,那肯定得定义一个局部变量。

7. 问: 晚课示例链表的删除中,删是删了,但是删除的点在内存中还是占据着空间,实际用的时候,要不要考虑内存优化?

答: 常规的用法是:

  1. 先用malloc得到节点,
  2. 插入链表,
  3. 用完之后从链表中把它删除,所谓删除只是不在列表中而已,
  4. 最后我们用free把它释放掉

8. 问: 晚课示例链表的使用中,内存中list是不是会开启一个list node的内存?

答: 我们来画一下这个链表的内存图:

这个list结构体,它里面又有其他结构体。

这个RootNote它里面放的是指针:指针都是4字节。

9. 问: 晚课示例链表的使用中,根节点是干嘛的?

答: 你排队的时候总得找到队伍的头部呀,否则在茫茫人海中,你怎么去找到你的队伍。

10. 问: 晚课中的 “来看一下我们一个芯片,它外面有密密麻麻的引脚”,是不是CPU的外面?芯片指的是整个板卡嘛?CPU的引脚和哪个模块连接不都是确定好的么,还能选择吗?

答:

一个芯片里面有很多模块,就比如说上面的图里面,里面除了CPU之外,还有gpio,还有UART。

一个芯片的外面,你看它的周边那些引脚,是已经确定下来了,就是上面图中那些白色的发光的引脚。

但是这些引脚要接到芯片里面哪一个模块呢?这是可以编写程序来控制的。

就比如说上面图里那些蓝色的外围的引脚,
可以接到gpio的红色的引脚,也可以接到UART的绿色的引脚。

11. 问: 针对stm32 高电平是3.3v,原因是因为stm32的供电电源是3.3v决定的吗,如果某个芯片的供电电源是5V, GPIO输出的高电平就是5v吗?

答: 这样理解没问题,那个高电平它都是有一个范围的,并不是说一定要到达3.3V,一定要到达5V。比如引脚的电压是1.7伏到3.3伏,我们就可以认为它是高电平。(1.7V这个数字是随便举例的)

12. 问: 为什么要用HAL库,比标准库和操作寄存器好吗?

答: ST公司建议使用HAL库,为了效率,有库就用库,没库再操作寄存器。

13. 问:

答: 首先对于所有的引脚,默认情况下都是输入状态,不会影响到外面的电路,

如果你要把它配置成输出引脚,在变为输出引脚的瞬间,他就会输出高电平或者低电平。

那么在变为输出引脚的瞬间,你要让他输出高电平还是低电平?

是不是在设置为输出引脚之前,我们先去设置它的输出值?

这就是答案了。

14. 问: assert_param怎么理解?有效性?

答:

简单的说他就是一个判断语句:

void assert_param(expr)
{
  if (expr)
    assert_failed();
}

大概就这意思,他会打印某些语句。

15. 问:

答:

16. 问: CORTEX-M3(还是该说STM32)里面有哪些模块呢,我一直以为GPIO和串口都是板卡上的,那板卡上的那些都是啥,没有什么CAN控制器之类的吗?

答: 只要去打开芯片手册,一看就明白了,我给你演示一下。

你看这个图,这就是103这个芯片,里面左上角是CPU。

其他都是各种:片上设备。

所谓片上设备就是:在芯片里面、CPU之外那些设备,也就是GPIO,UART模块呀,flash等等。

在芯片之外,我们还可以连接各种设备,这些设备叫做片外设备,就比如说103可以通过串口来连接WiFi模块,这个WiFi模块就是103这个芯片之外的设备。

17. 问: 老师能否解答下,我的注释是不是理解正确。

答: 理解的非常到位。

18. 问: 老师能讲一下什么情况下用到堆和栈,都是存在RAM上还是FLASH上的呀?

答: 堆和栈它是不一样的,今晚下课之后,对这个问题比较感兴趣的同学,可以去看这两个视频。

堆和栈都是内存,不是flash,我来画一个图:

我们写好程序之后,烧写进flash。

开板断电,没有程序运行,没有人去使用内存。

开发板上电,程序运行的时候要去初始化全局变量。
再次默念我们的口诀:变量变量变量,可以变,必定在内存里。

但它这些全局变量,它的初始值在哪里,初始值肯定在程序里,肯定在flash上面。

所以这个程序的代码会使用这些初始值去写内存,就是去初始化变量。

好了,内存有一块区域,用来保存这些全局变量。

那么局部变量呢,保存在哪里?保存在栈里。

栈起什么作用?在C语言里面必须使用栈,栈也指向一块内存。

要想弄清楚栈、局部变量,他们是怎么使用的,这会涉及一些汇编。

我先讲完堆,再给大家讲栈。

我要假设你们知道了栈的作用,
内存有一部分用来保存全局变量,
有一部分用作栈,
剩下的那一部分怎么办?

剩下的那一部分你可以用也可以不用。

你想用的话,你就可以把它当做堆,也就是说你得去管理,在上面实现malloc函数,实现free函数。

在我们这个图里面,你看对于内存它分为三部分。

堆,就是一块空闲的内存,你去使用它,它就叫做堆,你不用他,他啥都不是。

我们怎么使用它呢,我来举一个最简单的例子:

我们假设这块内存的起始地址是addr1,结束地址是addr2。

我来写一个malloc函数,最简单的:

我给大家简单讲解一下这个函数:

这就是最简单的malloc函数,但是它只能够实现分配,不能够实现释放。

为什么不能够实现释放呢?

int addr = malloc(100);
free(addr); // 你怎么知道释放多大?

我们申请一块内存的时候,得到他的地址,释放的时候也传入他的地址。

在我们这个例子里面,我怎么知道你要释放了,这块内存多大呀?根本就没有办法知道。

所以我们使用这种方法实现的分配函数,它不支持释放,对于真正的分配函数,我们应该怎么做?

我再举一个稍微复杂点的例子:

我们来看这个图,他要去分配100字节的空间,他并不仅仅分配这100字节。

他还会额外分配一个头部,在头部里面会保存有100这个数字,以后去释放这块空间时:free(p)

他会根据这个P往前面查,找到这个头部,从头部就知道了,你要是释放的是100字节的空间。

所以对于堆,如果越界访问的话,就有可能会破坏掉头部信息,整个系统什么时候崩溃都不知道。

我们现在大概知道堆的原理之后,我们就来看看堆有什么作用。

就像我们之前讲的那个例子,你的班主任让你去记录每个同学的信息,
因为同学的人数是不能够事先确定的,这个时候你就可以使用堆来动态分配结构体

19. 问: malloc函数怎么确定可用的堆大小呢?

答: malloc函数本身没有办法确定可用的堆的大小,他只有一个返回值,要么成功要么失败。

20. 问: 右边的1、这种格式写法对吗?

答: 正确。

21. 问: 除了编码时注意,如何防止堆、栈的溢出呢?

答: 防止栈溢出的话,就尽量的少定义非常庞大的局部数组。

22. 问: 这样的话使用malloc函数不就是会有改写栈的风险么?

答: 你要确定malloc管理的内存的边界。

23. 问: 老师分配节点使用malloc,实在堆里面分配空间吗?

答: 是的

24. 问: 堆都是用malloc申请么?什么时候才会用到它还是不太理解。

答: malloc是从堆里分配空间,不是反过来说:堆是用malloc申请。

25. 问: 怎么知道堆的起始地址?是通过编译器分配的吗?

答: 我们可以通过编译器来确定,甚至可以直接定一个巨大的数组,把这个数组就当做堆,freertos里面就是把一个巨大的数组用作堆。

26. 问: 定义一个数组不就被放到全局变量或者栈嘛?

答: 是呀,堆的含义是什么?堆的含义就是一块空闲内存嘛,这个全局数组我不用它,它就是空闲内存。

27. 问: 老师,讲一下栈吧?

答: 对于栈,我找个程序,需要给大家展示汇编,才能讲清楚。

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的函数,你的返回地址是下面的那条语句?

了解这个流程就行了:

  1. main调用add
  2. 会把add的返回地址保持在某个寄存器里,比如LR

下面我们进入add函数:

请添加图片描述

你看现在就用到了栈,栈这块内存它的使用情况,我来画一个图:

请添加图片描述

在栈里面他使用了两个空间,
一个用来保存函数的返回地址LR,另外一个用来保存R3。

接下来我们定义了一个局部变量i,它的初始值是10。

volatile int i = 10;

我们说局部变量保存在栈里,它是怎么体现的呢?

请添加图片描述

看箭头的代码,他让寄存器R0等于10,然后把R0的值写到栈里面去。

我来画一个图:

看到了吧,变量i它在内存哪里呀?在栈里面。

所以看到这里我们就知道了:
1.栈,他会用来保存返回地址
2.也会用来给局部变量分配空间

大家不用灰心,这部分讲的内容超纲了,后面我们讲ARM架构的时候会从头教大家,先从汇编教起。

28. 问: 老师,后续先讲arm架构还是先freertos?

**答: **

1.先讲FreeRTOS快速入门
2.再讲ARM架构
3.最后讲 FreeRTOS内部实现

29. 问: 老师预习FREERTOS的课程,代码裁剪需要掌握嘛,如何有效掌握呢?

答: FreeRTOS它就几个文件,裁剪也裁剪不出什么东西,主要是配置各种宏。某个函数,你要去使用它的话就得去打开某个宏