一. 前言
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;
是怎么进行的呢?
a = 123;
- 代码保存在Flash;
- CPU读取Flash得到代码;
- CPU根据代码指示,写变量a;
4. p = (volatile unsigned int *)(0x40010800 + 0x0c);
的执行
p = (volatile unsigned int *)(0x40010800 + 0x0c);
- 代码保存在Flash;
- CPU读取Flash得到代码;
- CPU根据代码指示,写变量p;
执行完下面两行代码后:
a = 123;
p = (volatile unsigned int *)(0x40010800 + 0x0c); /* GPIOA_ODR寄存器的地址 */
复制
内存情况如下图(图中的地址是假设的):
大家可以看到:
- 假设CPU将地址
分配给了a,0x2000 0000
分配给了p;0x2000 1000
- 对变量a和变量p的写操作都是在写RAM(在STM32F103中地址
开始的一片内存是映射的是RAM空间);0x2000 0000
- 当对变量a进行赋值时,CPU就在a所在的地址空间 ,即从地址
开始的一小段空间(根据a的类型和cpu的位数决定a占用多少长度的空间)写入数据;0x2000 0000
- 对于变量p的写操作也是类似的;
执行那两行代码
a = 123;``p = (volatile unsigned int *)(0x40010800 + 0x0c);
的结果就是:
- 在RAM地址0x2000 0000处写入了一个数据是123;
- 在RAM地址0x2000 1000处写入了一个数据是
即(0x40010800 + 0x0c)
;0x4001 080C
- 又根据指针的定义, 我们对
这个操作就得到了一个信息: 指针p指向的地址是p = (volatile unsigned int *)(0x40010800 + 0x0c);
。0x4001 080C
5. *p = 1
*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得到指令
- 怎么读Flash?用地址
- 得到什么?得到指令,也就是Flash上的数据
CPU这个大爷,向外面发出地址,读到Flash上的数据。这些数据就是指令,然后CPU执行指令
6. 深入CPU的指令执行
6.1 指令 a = 123
?
a = 123
- 怎么写变量a?用地址
- 做了什么?把数值123写到了内存里
6.2 指令 p = (volatile unsigned int *)(0x40010800 + 0x0c)
p = (volatile unsigned int *)(0x40010800 + 0x0c)
CPU得到这条指令后,怎么执行?
- 怎么写变量p?用地址
- 做了什么?把数值0x4001080c写到了内存里
6.3 指令 *p = 1
*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位的。用一段伪代码来表示的话,就是这样一个结果: