【RTOS训练营】站在更高的角度学习C语言

一. 前言

C语言基础,有些同学基础扎实,有同学能用但是理解不深,这个训练营的重点在于RTOS和芯片架构,对C语言的要求也不算高. 结构体、指针、链表,掌握这三点就可以,基本不涉及复杂的语法,基础弱的同学,可以看唐老师的C语言视频,免费的。

我们并不需要停下来单独去学习C语言,C语言可以在RTOS的学习过程中再慢慢精进,不用担心。

唐老师的C语言课程在我们官网(www.100ask.net):


现在,我们一起来站在更高角度学习C语言

二. 代码引入

int a;
volatile unsigned int *p;
void main()
{
	a = 123;

	p = (volatile unsigned int *)(0x40010800 + 0x0c); /* GPIOA_ODR寄存器的地址 */
  *p = 1;
}

1. 从这段代码可以引出几个问题:

①这段代码涉及哪些硬件?

  • Flash、RAM、GPIO、CPU

②程序保存在哪里?

  • Flash

③变量a、p保存在哪里?

  • RAM

④操作p时,操作哪里?

  • GPIO

⑤谁来执行这个程序?

  • CPU
  • 硬件方面,至少涉及这4部分:Flash、RAM、GPIO、CPU。

1.1 大家先看这个动图(图中的地址是假设的):

请添加图片描述

2. 全局变量a、p:

int a;
volatile unsigned int *p;

这两行代码,会在RAM中分配空间给这2个变量(图中的地址是假设的)
请添加图片描述
变量a、p的地址,你是控制不了的,编译器分配的地址

3. 执行main函数的代码:a = 123;是怎么进行的呢?

  • 代码保存在Flash;

  • CPU读取Flash得到代码;

  • CPU根据代码指示,写变量a;

4. p = (volatile unsigned int *)(0x40010800 + 0x0c);的执行

  • 代码保存在Flash;

  • CPU读取Flash得到代码;

  • CPU根据代码指示,写变量p;

执行完下面两行代码后:

	a = 123;

	p = (volatile unsigned int *)(0x40010800 + 0x0c); /* GPIOA_ODR寄存器的地址 */

内存情况如下图(图中的地址是假设的):

请添加图片描述
大家可以看到:

  • 假设CPU将地址0x2000 0000分配给了a,0x2000 1000分配给了p;

  • 对变量a和变量p的写操作都是在写RAM(在STM32F103中地址0x2000 0000开始的一片内存是映射的是RAM空间);

  • 当对变量a进行赋值时,CPU就在a所在的地址空间 ,即从地址0x2000 0000开始的一小段空间(根据a的类型和cpu的位数决定a占用多少长度的空间)写入数据;

  • 对于变量p的写操作也是类似的;

执行那两行代码a = 123;``p = (volatile unsigned int *)(0x40010800 + 0x0c);的结果就是:

  • 在RAM地址0x2000 0000处写入了一个数据是123;

  • 在RAM地址0x2000 1000处写入了一个数据是(0x40010800 + 0x0c)0x4001 080C

  • 又根据指针的定义, 我们对
    p = (volatile unsigned int *)(0x40010800 + 0x0c);这个操作就得到了一个信息: 指针p指向的地址是0x4001 080C

5. *p = 1

*p = 1;

我们刚才已经知道, 指针p指向的地址是0x4001 080C,所以这一行代码的作用就是让CPU在p指向的地址写入一个数据1,即在地址0x4001 080C处写入数据1。我们在前面以STM32F103的内存空间举例得知地址0x2000 0000开始的一片内存映射的是RAM空间,那么地址0x4001 080C也是RAM空间嘛?不是的。根据STM32的手册我们可以发现地址0x4001 080C处映射的其实是STM32F103的引脚GPIOA(GPIO:通用输入输出)的输出寄存器ODR。

也就是说, *p = 1就是让地址0x4001 080C保存的数据变成了1,对于STM32F103而言,完成的功能就是让GPIOA的寄存器ODR的最低位bit0 = 1,最终表现出来的结果我们放在后面的课程说。

在整个过程中,最重要的是:地址

CPU读Flash得到指令

CPU这个大爷,向外面发出地址,读到Flash上的数据。这些数据就是指令,然后CPU执行指令

6. 深入CPU的指令执行

6.1 指令a = 123?

  • 怎么写变量a?用地址

  • 做了什么?把数值123写到了内存里

请添加图片描述

6.2 指令p = (volatile unsigned int *)(0x40010800 + 0x0c)

​ CPU得到这条指令后,怎么执行?

  • 怎么写变量p?用地址
  • 做了什么?把数值0x4001080c写到了内存里

6.3 指令*p = 1

​CPU得到这条指令后,怎么执行?

  • 怎么写GPIO寄存器?用地址;
  • 做了什么?把数值1写到了地址0x4001080c上,就是写到了GPIOA_ODR寄存器,导致LED变亮或熄灭;

7. 程序核心

这个程序的核心是什么?写地址
最核心是什么:地址

我们以葫芦娃举例:


这位大爷,怎么使唤葫芦娃?

  • 叫名字, 7个葫芦娃都编号1234567;
  • 使唤

在电子系统中,CPU也是大爷,外面的RAM、GPIO、Flash就是儿子。CPU要访问RAM、GPIO、Flash,也要先点名:发出地址:

7.1 地址和内存

RAM很大,CPU读写数据时,是不是要发出地址给RAM,再收发数据?假设我们有个这样的硬件框图:


我们说RAM、GPIO、Flash,是兄弟,是平等的,都给CPU大爷使唤,那么CPU大爷是如何访问这多个平等关系的设备的呢?

​大家看,设备1、设备2都需要地址线,都需要数据线。CPU大爷发出地址,同时到达设备1、设备2;CPU大爷发出数据,同时到达设备1,设备2;那么问题来了:这数据给谁的啊?

​所以,这些连线不够!还需要一个叫做片选信号的信号线和一个内存管理器,如图所示:


CPU大爷,和它的儿子之间,需要插入一个传话人,这个传话人叫:内存管理器/内存控制器。因为RAM、GPIO、FLASH都是同类的设备,都有地址,都能读、写。都 “类似” 内存。假设设备1的地址范围是XXX-YYY,假设设备2的地址范围是AAA-BBB,访问它们的过程是这样的:

  • CPU大爷发出地址 addr;
  • 内存管理器/内存控制器发现addr 处于 XXX-YYY之间,就知道了,哦,你要访问设备1;
  • 内存管理器/内存控制器就把cs1设置为有效值,表示说:CPU大爷选中你了;同时,cs2保持无效值,也就是设备2没被选中,设备2就保持沉默;
  • 所以,CPU发出的addr、数据,只会影响到设备1

根源在于:传话人,根据大爷发出的地址,判断要访问哪个设备,就去选中设备。地址!地址!地址!非常重要!

7.2 深入地址

什么叫地址?

​在一栋安居房里,政府故意把房子设计得很小,每个房间都是单独的。

  • 单身汉只有1房:char c;
  • 夫妻有2房:short a;
  • 有娃的家庭有4房:int b;
  • 大家庭有8房:char buf[8];

变量在内存哪个位置?我们一般无法决定,链接时确定的。


每个单间,都有一个地址。char c;占据一个字节,有一个地址;short a;占据2字节,有2个地址值;int b;占据4字节,有4个地址值;但是,我们去拜访这些居民时,只使用一个地址:首地址

在同一栋楼里,大家的地址都是类似的:XX街道XXX小区XXX栋XXX房。在C语言里,就是:char, short, int, struct 、字符串,它们的首地址都是类似的:位数都一样。对于32位CPU,地址都是32位的。用一段伪代码来表示的话,就是这样一个结果:
请添加图片描述