1、指针
核心是变量、变量、变量,他既然能够变,肯定就在内存里,如下:
定义一个变量的时候,在内存里必定会分配一块对应的空间。
在上图里面我们定义一个int a,它是4个字节,
如果定义一个字符变量的话,它是一个字节,
如果定义一个结构体的话,结构体可能更大。
现在给变量a赋值,让a等于123。
简单来说就等于三条指令:
R0=123
那么我怎么通过变量P来操作变量a呢?
int a;
int *p;
p=&a;
*p=123;
我们怎么操作寄存器呢?
unsigned int *p = 某个地址;
使用的时候怎么做呢?
*p = 某个值;
unsigned int *p = 某个地址;
就是:
unsigned int *p;
p=某个地址;
2、结构体
下面我们来讲结构体。
我们怎么描述一个人,我们可以用一个结构体:
struct person{
int age;
char name[8];
};
我定义一个结构体变量: struct person wei;
wei.age=40;
p->age=40;
这两句指定的效果是完全一样的
我们应该使用结构体还是使用结构体指针来表示寄存器?
如果定一个结构体的话,结构体变量它还是变量呀,它就在内存里面分配一大块空间。
而我们要操作的是gpio啊,在内存里面分配一大块空间干嘛?
所以我们用的是结构体指针。
再看看这个图里面:
typedef struct
{
volatile unsigned int CRL;
volatile unsigned int CRH;
volatile unsigned int IDR;
volatile unsigned int ODR;
volatile unsigned int BSRR;
volatile unsigned int BRR;
volatile unsigned int LCKR;
}GPIO_TypeDef;
GPIO_TypeDef *gpioa;
gpioa=(GPIO_TypeDef *)0x40010800;
gpioa->ODR=1;
编号2的地方定义了一个结构体指针,这个指针它是一个变量,它在内存里面必定有一个空间。
在编号4的地方,我们使用这个指针,操作寄存器。
“->”:指向的xxx。
gpioa->ODR = 1; 翻译就是: gpioa指向 的 ODR,等于1
3、函数指针
我们的函数保存在哪里?
保存在flash哪里?
这个指针,它是函数的指针,也就是函数的位置,在32位处理器里面,它仍然是4个字节。
我们可以使用类比的方法,记忆函数指针:
int a; int add(int a,int b){return a+b;}
int *p; int(*pf)(int,int);
p=&a; pf=&add;
仔细看这个对比,左右两边对比一下
左边定义变量a,右边定义函数add;
左边定义 int指针,右边定义 函数指针;
左边赋值 指针,右边赋值 函数指针;
如图:
记住我们的口诀,变量变量,可以变,就是可读可写。
可读可写,只能在内存里面。你定义一个变量的时候,在内存里面必定会分配一块空间.
左边是int指针,右边是函数指针
int指针是变量,函数指针也是变量。
在图里面椭圆形的地方,就是这两个变量。
指针、指针,在32个处理器里面,指针必定是4字节。
不管你是字符指针,int的指针、函数指针,结构体指针通通都是四字节。
pf=add;
pf(1,2);
讲那么久的指针,就要用起来了。
现在我们的结构体指针跟HAL库就扯上了关系了。
4、链表
链表的操作实际上并不是很复杂,你只要把指针搞清楚就行。
你看有三个特务,ABC。
ABC串起来就变成了一个链。
我们来画图讲解一下
第一,定义了结构体变量A,B,C;
在内存里面就必定有这三个结构体变量所对应的空间
假设在内存里面分配了ABC三个结构体变量,
A.next_addr = &B;
B.next_addr = &C;
C.next_addr = NULL;
大家看到这个链表,实际上是非常枯燥的。
我们使用箭头来表示:
看看蓝色的箭头,只是为了让我们人类更加容易理解而已
列表的所有的复杂操作,都是从这些基础的知识里面扩展出来,比如双向链表、链表的插入和删除。
现在举一些应用的例子,然后再讲一下插入和删除操作。
我们举一个日常的例子,你们班10个学生,老师说要打印10个学生的信息,你可以用一个数组来做。
struct student{
char name[8];
int age;
}
void main()
{
int i;
struct student myclass_students[10];
for(i=0;i<10;i++);
{
scanf("%s",myclass_students[i].name);
scanf("%d",&myclass_students[i].age);
}
for(i=0;i<0;i++)
{
printf("name:%s,age:%d\n",myclass_students[i].name,
myclass_students[i].age);
}
}
你看这个程序,就是你可以输入这10个学生的名字年龄,然后再把它们打印出来。他使用数组来保存这些学生的信息。
如果你这个班级有100个学生怎么办呢?
你得把这个数组大小设置为100。
那如果这个学校还有一些超级班级,比如有1000个学生,也得把这个数组设置成1000项。
也就是说为了支持这些小班级、中班级、超大的班级,你这个程序里面你得把这个数据设置设置的超级大,缺点就是浪费空间。
再比如说,这100个学生里面中间有某一个人转学走了,你这个数组中间就会空出一项,那一项你就标为无效。
再比如说,本来你在班级里面有1000个学生再插班进来一个学生,你这个程序就没有办法处理,问题的根源在于这个数组的容量是定死的。
如果使用链表,就可以这样写:
struct student{
char name[8];
int age;
struct student *next;
}
void main()
{
int i;
struct student *student_head;
struct student *new_student;
while(1)
{
new_student malloc(sizeof(struct student));
scanf("%s",new_student->name);
scanf("%d",&new_student->age);
}
{
printf("name:%s,age:%d\n",myclass_students[i].name,
myclass_students[i].age);
}
}
它的诀窍在于对于每一个学生我都会临时分配一个结构体。
你有10个我就分配10个结构体,你有100个我就分配100个结构体,你有1000个我有1000个,我就分配1000个结构体。
我使用列表可以支持小班级、中班级,超大班级。
如果有人走的话,有人转走了,我可以把列表中那一位给删除掉。
如果有人插进来,我又可以重新分配一个结构体,把这个新的结构体放进链表。
这就是日常生活中的一个例子,在rtos里面,常使用链表来管理任务。后面讲rtos时再来讲具体的任务链表。
5、Q&A
问:
typedef struct
{
int a;
Char b;
Char buffer[100];
}X_x;
X_x w;
int *p=&w;
char a;
int *p;
p=&a;
*p=12;
*p=12;写4个字节,但是变量a只有1字节的空间
我们可以再扩展一下,这样写程序的时候,会出现莫名其妙的问题:
char a;
int *p;
p=&a;
*p='A';
这段代码会有警告,但运行起来不会有问题,为什么呢?为了追求效率,编译器也给char a分配了是4字节的空间
*p = ‘A’, 这个指令会写4个字节,错有错招,没什么后果。
再举个例子:
char a;
char b;
int *p;
p=&a;
*p='A';
如果a、b挨着存放,赋值变量a,就会覆盖变量b。
所以说大家写是C程序的时候,任何警告都要引起重视。
这里a、b也不一定是连续存放的,不同的编译器有不同的考虑,比如说优化等级不一样的时候它也不一样。
w占用了多少个字节?4+1+100? //应该是4+4+100
typedef struct
{
volatile unsigned int CRL;
volatile unsigned int CRH;
volatile unsigned int IDR;
volatile unsigned int ODR:
volatile unsigned int BSRR:
volatile unsigned int BRR:
volatile unsigned int LCKR;
}GPIO_TypeDef;
GPIO_TypeDef *gpioa;
gpioa=(gpio_TypeDef *)0x40010800;
gpioa->ODR=1;
我们去定义一个结构体类型的时候,只是去创建一种数据类型,就像创建char int 这些基本的类型一样。
int a;才分配空间,int b才分配空间,int不分配空间。
因此,struct person wei 才分配空间,struct person不分配空间。
你只要定义了一个变量,就肯定会分配它的空间。你只要定义了一个指针变量,就肯定会分配他的空间。
对于这个GPIO结构体:
编号为2的地方,它定义了结构体的变量,在图里面内存中,就分配了那个结构体。
编号为3的地方,他定义了一个结构体的指针,在内存中就分配了那个指针。
不管你用不用,一旦定义了必定会分配。
问: 结构体定义,是保存在flash的吧?
**答:**对于这个问题,大家可以反过来想一想。
Flash里面会保存chat这个类型吗?会保存int这个类型吗?会保存各种结构体的类型吗?
这些数据类型只是给C语言用而已,C语言最终要转换成汇编。在汇编里面,根本就没有这些数据类型。
这些数据类型只是给编译器使用的,让编译器来给那些变量分配空间而已。
最终编出来的程序里面根本就不含有这些数据类型。
问: 因为我学的没那么深,大小端那里也没听太懂,希望老师整理资料的时候多做一些解释。
**答:**我们来插讲一下大小端。
我们说一个变量,它在内存里面必定有对应的空间
我希望你们把这个口诀记到脑子里面去,变量变量,可以变化,可以变化,就是可以读,可以写。可读可写的话,只能在内存里面,所以说一个变量在内存里面必定有空间。
那么我们说:个十百千万,个位保存在哪里?
答案是 都可以!
这就引入了大小字节序。
我们举个例子:
**问:**咱们课程会讲 oled的芯片手册里面的 各个指令吗?感觉看手册很吃力,但是感觉底层驱动非常重要。
**答:**不会,本课程重点是RTOS。
问:
答: 首先如果你问的是那些寄存器的话,那些寄存器是GPIO里的寄存器,他们是有初始值的。
上面这个图里面红色方框中就是那些寄存器,那些硬件寄存器当然有初始值了。
不管你的程序怎么写,不管你程序怎么定义变量,跟我GPIO寄存器有什么关系呢?
你写程序,程序是否要操作GPIO寄存器,GPIO寄存器肯定都有初始值。
这个问题的核心在于定义一个变量,这个变量是在内存里面,
而我的硬件寄存器在另外一个GPIO模块上面,他们两个之间没有什么关系。
你使用指针来读写硬件寄存器时,才会去影响到硬件寄存器的值。
**问:**函数指针有啥用?目前很少用到。
**答:**函数指针用的非常非常多,你应该用的少是因为你们还没有接触到。
我来举个使用函数指针的例子,核心就在于让代码更加容易移植。
假设你们公司有一款产品, 要用到两款LCD,你可以在main中这样写代码。
void LCD_3_5_draw_logo(void)
{
}
void LCD_4_2_draw_logo(void)
{
}
void main()
{
#ifdef LCD_3_5
LCD_3_5_draw_logo();
#else
LCD_4_2_draw_logo();
#endif
}
使用一个宏开关,来决定使用哪一个函数。
再看一下,如果我们把这个程序分为两部分,main函数是APP,上面两个函数是驱动。
我是不是换一款屏幕就得重新写一下main函数:重新定义宏,重新编译。
那么有我们有没有办法呢?可以改进一下:
void LCD_3_5_draw_logo(void)
{
}
void LCD_4_2_draw_logo(void)
{
}
void main()
{
int type;
type=read_gpio();
if(type==LCD_3_5)
LCD_3_5_draw_logo();
else if(type==LCD_4_2)
LCD_4_2_draw_logo();
}
按这种方法,添加了一个新的函数,你使用不同的LCD时,我可以去读取某些引脚来判断你使用哪一个LCD。
在这种情况下,你即使更换了一款LCD,我也可以让这个main自动的去适应它。比前面那个好一点了。
但是,你们的公司这款产品它支持100款LCD,你就得加100个 if 判断,有些祖传代码有几十个上百个这样的判断,我们再怎么改进呢?
struct lcd_ops{
int type;
void(*draw_logo)(void);
};
void LCD_3_5_draw_logo(void)
{
}
void LCD_4_2_draw_logo(void)
{
}
struct lcd_ops lcds[]={
{LCD_3_5,LCD_3_5_draw_logo},
{LCD_4_2,LCD_4_2_draw_logo},
};
struct lcd *get_lcd()
{
struct lcd_ops* lcd;
lcd=get_lcd();
lcd->draw_logo();
}
来看看这段代码,main函数基本上就不用变
变的只是你上面的驱动程序,你添加100款LCD都没关系,你就在那个数据里面往里面添加LCD就好了。
**问:**老师,结构体的自引用,只能自引用指针吗?
**答:**自引用,是指自己引用自己吗?我暂且认为你是这样问的。
struct spy{
int val;
struct spy* next_addr;
};
int main()
{
struct spy A;
A.next_addr=&A;
}
经常用来表示:链表里只有它一个成员。
if (A.next_addr == &A)
printf(“只有一个”);
struct spy* next_addr;怎么理解?
**问:**老师你们有没有出单片机裸机课的?
**答:**有,我们有HAL库的教程,也有arm架构课程,从0写代码,不用任何HAL库:这属于RTOS训练营中的一部分