C语言基础

1、代码引入

int a;//定义整型变量a
volatile unsigned int *p;/*定义一个指向unsigned int类型数据的指针p,
该指针所指向的数据被标记为volatile(防止编译器进行编译优化,导致原来的值发生变化)*/
void main()
{
a=123;
p=(volatile unsigned int *)(0x40010800 + 0x0c); /* GPIOA_ODR寄存器的地址 */
*p=1;
}

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

(1)这段代码涉及哪些硬件?

  • Flash、RAM、GPIO、CPU
    (2)程序保存在哪里?
  • Flash
    (3)变量a、p保存在哪里?
  • RAM
    (4)操作p时,操作哪里?
  • GPIO
    (5)谁来执行这个程序?
  • CPU

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

如下图(图中的地址是假设的)
在这里插入图片描述

2、全局变量a、p:

int a;
volatile unsigned int *p;

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

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

  • 代码保存在flash;
  • CPU读取Flash得到代码;
  • CPU根据代码指示写变量a;

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

  • 代码保存在flash;
  • CPU读取Flash得到代码;
  • CPU根据代码指示写变量a;
    执行完后面两行代码后:
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得到指令
  • 怎么读Flash?用地址
  • 得到什么?得到指令,也就是Flash上的数据
    在这里插入图片描述
    CPU这个大爷,向外面发出地址,读到Flash上的数据。这些数据就是指令,然后CPU执行指令

6、深入CPU的指令执行

(1)指令a=123?

  • 怎么写变量a?用地址
  • 做了什么?把数值123写到了内存里
    在这里插入图片描述
    (2) *指令p = (volatile unsigned int )(0x40010800 + 0x0c)

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

  • 怎么写变量p?用地址
  • 做了什么?把数值0x4001080c写到了内存里
    (3)指令*p=1

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

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

7、程序核心

这个程序的核心是什么?写地址!最核心是什么:地址!
我们以葫芦娃举例:
在这里插入图片描述
这位大爷,怎么使唤葫芦娃?

  • 叫名字, 7个葫芦娃都编号1234567;
  • 使唤
    在电子系统中,CPU也是大爷,外面的RAM、GPIO、Flash就是儿子。CPU要访问RAM、GPIO、Flash,也要先点名:发出地址:
    在这里插入图片描述
    (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

根源在于:传话人,根据大爷发出的地址,判断要访问哪个设备,就去选中设备。地址!地址!地址!非常重要!
(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位的。用一段伪代码来表示的话,就是这样一个结果:

char *pc;
short *ps;
int *p1;
struct xxx *px;

sizeof(pc)==4;
sizeof(ps)==4;
sizeof(p1)==4;
sizeof(px)==4;

8、Q&A

:stm系列代码运行在片上flash吗?
:我们假设它在Flash上运行,讲到ARM架构时可以让程序在RAM里运行。

:CPU如何访问Flash、RAM、GPIO这些内存或外设?
:CPU大爷使用不同的地址,访问RAM,GPIO,FLASH。从这个角度看,GPIO、RAM、Flash地位相同。
a. RAM的特性就是:写什么进去,读出来仍然是什么。
b. Flash的特性就是:不能轻易写数据进去,可以读。
c. GPIO的特性就是:写数据进去有特殊的含义,可能是让引脚变为高电平、低电平。
这几个儿子,功能都不一样。但是在大爷眼里,都是儿子,都有地址,都是使用地址来访问的。

:地址可以随便写吗,比方说写地址0?
:这个地址存在对应的设备才可以,就想7个葫芦娃,你想使唤第8个,那是不存在的。也就是说,访问的地址必须是芯片规定给的地址范围才可以。你写一个地址,涉及芯片时,这个地址没有设备:写它的话,就没有任何作用。

:a=123这个怎么看是写内存的地址?地址是123吗?写到哪里去了?
:a是变量,变量可读可写,只能在RAM里。123是要写入的值,地址在链接程序时分配的,要查看分配的是哪个地址可以运行的时候调试查看

:“Flash的特性就是:不能轻易写数据进去,可以读” 不太理解
:我们开发程序时,烧写程序,就是烧写到Flash上,断电后,Flash的内容也不会丢失,Flash上存的是程序,很宝贵,无法简单地写数据进去,必须经过特殊步骤:比如擦除后才能写,比如写的时候要先解锁。如果Flash能轻易写数据进去,你的程序就很容易被破坏。

:前面例程的变量a和p在flash烧录文件里也是以地址+数据形式存在吗,这里的“地址”可以自定义分配吗?
:a = 123; p =xxx;在Flash中是这样保存的:假设编译器给a、p分配的地址分别是addr1, addr2。a = 123; 变成几条汇编指令,如下:
MOV R0, ADDR1MOV R1, #123STR R1, [R0]
看不懂汇编没关系,ARM架构里会讲。翻译一下:
a = 123; 变成几条汇编指令:
把addr1的值放进CPU的内部寄存器R0;
把123的值放进CPU的内部寄存器R1;
把R1的值写到一个地址上去,哪个地址?R0里就是地址值;
关键的地方来了:a = 123:
把123这个数,写到变量a去,就是写addr1对应的内存;
在汇编码中,隐含有了addr, 隐含有了123;
执行完汇编指令,来自Flash的数值123,被写到了内存addr1 地方;

:p = (volatile unsigned int *)(0x40010800 + 0x0c)中的volatile unsigned in可以不写吗?
:可以不写,会有警告

:cpu不是将flash数据读取到RAM中,在RAM中执行吗?还可以直接读gpio地址吗?
:这是不对的。CPU是把Flash的数据,读入CPU内部,在CPU内部执行。对RAM的访问只有2种:读、写。
在这里插入图片描述
看这个 a = a + b:
从内存读a,存入CPU;
从内存读b,存入CPU;
CPU内部:val = a + b;
把val写到内存a处;
对于内存RAM,只有读、写操作;对于Flash,在这里只有读操作,CPU从Flash上读到执行,在CPU内部执行指令:

:内存管理器需要配置各个外设的地址范围吗?或者说可以配置吗?需要配置吗?
: 一般无法配置;
无论是103,还是其他芯片比如IMX6ULL,芯片手册都会有一章:memory map,里面讲的就是,芯片上各个设备的地址范围;

:变量和数据是分开在两片区域存储的 ?
:变量a,地址为0x20000000,链接程序时就确定了,程序运行时它在内存里,注意是运行时
a=123这是指令,指令里有数据123,它在Flash上;
执行完a=123后,内存地址0x20000000处的值就被写为123;
123这个值,来自Flash,被传到了RAM

:汇编里的寄存器,和GPIO这些寄存器有什么区别,是访问方式和地址不同么?

在这里插入图片描述
寄存器,这个词取得很不好。CPU内部的寄存器,GPIO上的寄存器,完全不是一回事:

  • CPU里面的寄存器,使用汇编指令来读写;
  • GPIO上的寄存器,像内存一样,CPU发出地址信号、数据信号,来读写它;

:这个传话人是我们编程写好吗?比如发地址给它,然后发出片选信号?
:芯片设计时,硬件就设计好了,软件无法更改。

:地址长度为什么都是4?
:①首先,这里是有个前提的:是在32位的机器下。
②这32位就限制了内存地址的长度只能是32位也即是4个字节,所以我们使用sizeof获得一个指针变量的长度时,就只能得到一个结果,那就是4个字节长度。
③对于32位CPU,CPU大爷能发出32条地址线,所以地址的值,就是用32bit来表示。
④各种变量大小不同,但是它的地址,准确地说首地址,都是32位的。
⑤指针变量,用来保存地址,所以任何的指针变量,sizeof(XXX *)都是4字节,也就是32位。

:char buf[1000] ,没初始化,sizeof(buf)也是4吧?
:是1000。没初始化的意思只是里面的值没设置,但是占用的空间CPU给你保留下来了。


ldr r0, =0x20000000
ldr r1, =123
str r1, [r0]
怎么判断是数据还是地址?
:前面无法判断是数据还是地址,用起来是才知道是数据还是地址。在第3条指令:STR R1, [R0] 这是一条写内存指令,R1是数据,R0是地址。如果你这样写:STR R0, [R1],那么 R0是数据,R1是地址。STR是store的意思。

:能不能这样理解程序运行的过程:
程序存储在Flash中(待处理) CPU每次执行一句代码(处理一句代码),将每次执行的结果存储到RAM中(无论堆还是栈),如果需要就在RAM中提取数据(比如GPIOS-ODR寄存器)。
:结果不一定保存到RAM,比如:*p = 1;p指向GPIO寄存器,那么这条指令就是 去写GPIO寄存器,结果是写到了GPIO去了。当然,p的值是来自RAM。“每次执行的结果存储到RAM中”: 错在“每次

:arm指令集和thumb指令集都是32为寻址吗?
:是的。Flash上每条thumb指令时16位的,但是CPU去读Flash、读写RAM、读写GPIO时,发出的地址线是32位的。CPU得到了16位的指令,根据16位的指令:比如 STR R0, [R1], 它是把R1的32位数值发送到地址线去。所以thumb还是arm指令,并不影响寻址的地址线长度。

:在Linux里运行自己写的程序,可以删除自己的可执行文件,为什么说单片机里的程序在Flash?
:Linux是把程序读入RAM,然后运行;单片机也可以把Flash上的程序读入RAM运行,然后擦除Flash,只是单片机RAM没那么大,我们可以不把Flash上的代码复制到RAM。

:提到首地址了,老师有时间可以讲一下pack指令吗?什么情况下需要使用对齐,如何使用:
struct { char c; int a;};
:首先,这个结构体多大?按理说,char是1字节,int是4字节,所以这结构体是5字节。但是,如果这样分配空间的话,int a的地址就是奇数,访问效率不高。所以,这个结构体对于char c,仍然分配4字节,即使只用1字节。我们能否强制让它分配5字节?可以,用pack指令,具体用法可以百度搜索。

:cortex-m系列我可以理解为代码位于Flash上,变量位于RAM上吗?这样从Flash取指令速度会不会受限?这是cortex-m系列的特点吗?
:这样理解没什么问题。其实是可以将M系列Flash上的代码放到其RAM里去运行的,如果RAM空间足够的话。在Flash上速度确实会慢一点,基于成本考虑可以忍。

:FreeRTOS的任务堆栈一般是动态分配的吗?动态分配一般不会溢出的吧?
:动态分配更方便,但是一些追求极致稳定的系统不允许动态分配。

:在32位将其中,结构体为什么分配四字节对齐访问效率会更高?
:首先看下这张图:
在这里插入图片描述
CPU去访问位宽为32的RAM时,硬件上一次读写是以32位为单位的,比如:即使读一个char c,内存控制器也是去RAM上读到32位的数据就是4字节数据,把其中一字节返回给CPU。
①对于上图的变量a,可以一次性读、写完;
②对于上图的变量b,要读2次:第1次读得到下图椭圆中的4字节:
在这里插入图片描述
第2次读,得到下图椭圆中的4字节:
在这里插入图片描述
然后组合下图中的椭圆中的内容:
在这里插入图片描述
这样的操作有个什么问题呢?效率特别的低下,速度特别的慢!而且硬件还不一定支持这样寻址读取。

:有个内存控制器用片选帮忙寻址相应设备,既然每个设备都有自己的固定的内存地址,为什么还要需要片选选中某个设备呢
:因为大家共享地址线、数据线,纯粹的通过地址线寻址CPU是无法区分找到的是谁家。

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值