嵌入式C语言强化学习——(嵌入式学习路线1)

article/2025/8/22 20:05:20

前言

从零开始记录自己学习嵌入式,之前看稚晖君的视频,里面的费曼学习法觉得对自己很有帮助,所以准备把自己学习到的东西分享出来,便于学的更深(符合费曼学习法)
众所周知,嵌入式编一般是使用C语言(也有用python之类的,比如树莓派pico,以后的文章会写到),所以C语言的基础很重要。今天看了韦东山老师的嵌入式C语言加强课,来总结一下。

课程链接:韦东山老师嵌入式C语言加强,全天8小时直播,吐血整理可以分集观看!

口诀

韦东山老师两个口诀:

  • 变量能变,可读可写,必放内存
  • 指针放地址,32位处理器地址必32位,所有类型地址指针均是4个字节(1个字节8bit)

变量和指针

在学习变量之前,首先知道单片机芯片的内部构成。一般单片机由以下几项构成。

  • CPU
    CPU类似于单片机的大脑,可以进行数据的运算和指令的执行

  • RAM(Random Access Memory)
    内存一般是用于存放变量的地方。变量就像一个东西一样被放在内存中。RAM被读取速率快,缺点是掉电就会丢失数据。用它的英文名字来记住他 Random Access Memory(随机存取存储器)

  • FLASH(闪存)
    闪存是ROM(Read Only Memory)中的一种,它不仅具备电子可擦除可编辑(EEPROM)的性能,还不会断电丢失数据同时可以快速读取数据。

  • 总线

  • 外设

  • 电源

  • PLL锁相环

  • 时钟管理器

总线之类的会在后面的文章中提到(挖坑ing),现在来说RAM和FLASH。变量是放在RAM中的,也就相当于程序每次重新启动之后,系统会重新的在内存中为变量开辟位置,同时根据程序中的值,为变量赋值。同时在有的单片机程序中,定义了但是没有被使用的变量,不会被分配地址。这和单片机的性质有关。单片机和那些资源丰富的机器比起来,内存的管理也是很重要的。有时我们并不想自己未使用的变量被被忽略

int main(void{
int a;
char c;
char buf[100];
}

例如在上面的程序中,a, c, buf[100],在程序中并没有被使用,所以编译器不会分配地址给他。但是有时我们需要我们为了防止编译器优化它,就会使用到volatile(易变的)这个关键字。

int main(void{
volatile int a;
volatile char c;
volatile char buf[100];
}

如果想知道 a 的地址,可以在调试中,对 a 变量进行取址,便可以查看到。但是在生成的map文件中,无法看到。因为局部变量是在运行时,在栈中临时生成的。在map文件中,你查看到的是全局变量,静态变量。

volatile在嵌入式中是一个很重要的关键字。
例如:

int main(void{int a;a=1;a=2;
}

在上述程序中,正常情况下a=1会被编译器优化掉,但如果在定义a时,在前面加上volatlile。可以保证关于a的所有指令都被执行。同时在一般的程序中,为了优化,变量值的一般都是先从RAM中被读到CPU中,使用时,现在CPU中去读变量的值,修改时,也是在CPU中去改变变量的值。并不会去写内存。加上volatile后,确保了每次CPU会每次去读写内存。而不是在CUP中改变。一般来说访问硬件寄存器时,会加上volatile.

与volatile相似,const也是一个C语言中的关键字。

const 表示这个量可以被放在FLASH中,被视为常量,不必放在内存中,可以节约内存。同时被const修饰的变量不可以被改变。

关键字中的static,表示为,这个变量的工作范围仅在我这个文件之中,而不加static,则表示这个变量在整个程序的范围之中。当在文件中已经用static定义了变量,则优先使用文件中用static定义的变量。

关键字的extern,它修饰在变量前的话,表示这个变量已经在其他文件中定义了,被这个文件拿来使用。就是让程序知道,他是一个外部变量。同时它仅仅可以被用于声明这是一个什么东西,后面不可以接可执行语句。

指针始终是一个变量,在32位的处理器之中,始终是4个字节的。

在函数中定义的不同类型的变量:
在函数中定义的不同类型的变量
在MAP文件中显示的变量分配的地址:
在MAP文件中显示的变量分配的地址
一般来说,只读的常量是会被放在FLASH中以节省内存地址,但是有时编译器也会把常量放在RAM中,方便调用。
同时在C语言中,我们想知道变量的大小,可以使用sizeof()来进行测量,sizeof内的变量会被自动替换成变量的类型。如果要知道指针的大小(由以上的P为例子),应使用sizeof(p),而不是sizeof(*p)。

关键字struct

struct就必定会提到结构体。在程序中,我们想重复的使用很多种类型相同,但是数据不同的变量。例如:
传统方式:

char *name = "mao_nan_bei";
int grade = 12;char *name2 = "gou_dong_xi";
int grade2 = 11;
...

这样的方式定义少量的还行,但是定义大量的话,不仅工程量大,也不易于查找自己想要的类型。但是使用结构体的话,可以解决这个问题。

结构体方式

struct people{char *name;int grade;
};struct people.mao = {"mao_nan_bei",12};printf("name=%s,grade=%d", mao.name,mao.garde);

结构体中既可以有基础的变量类型,也可以有其他的结构体,例如

struct people{char *name;int grade;
};
struct class{char *nick_name;struct people student[100];
};

对于结构体我们同时要明确,声明结构体是不占空间的,定义结构体才会占用一定的空间。(变量才会被分配空间)。在结构体中,我们使用char *name 而不是char name[100];是为了节省内存空间。

通过指针进行赋值

讲指针之前,我们先对变量赋值的一个过程进行一个简单的描述、
例如:

int a = 3;

上面这行指令赋值的过程,为CPU先读FLASH得到指令,在内存中开辟一个int大小的空间,最终再将变量的值放入空间之中。指针中始终存放着地址,可以通过访问地址的方式,来改变变量中的值。在结构体中,也可以使用指针,如下:

int *p;
struct people *pt;
int a;
p = &a;
*p=123;pt = &mao;pt->grad = 16;

typedef使用方法

typedef一般是用于创建类型别名的。一般struct会和typedef联合在一起使用。以便于使用结构体。

typedef struct people{char *name;int grade;
}people;people.mao = {"mao_nan_bei",12};

结构体指针&函数指针

C语言在结构体中也可以使用指针

struct student{char *name;int age;struct student classmate;
};

以上的写法是错误的,一直递归也无法知道结构体的大小。但是可以使用的方式进行定义。

typedef struct student{char *name;int age;struct student *classmate;char *sex;
}student,*pstudent;

由于指针的大小是被确定的,所以使用这种方式,可以使结构体的大小确定,不至于被无限套娃。同时利用typedef确定了结构体名称为student,结构体的指针为pstudent。
当我们想根据结构体中相同变量的不同值来确定是使用什么样的函数时,我们可以通过判断结构体中这个变量的方式来判断该使用什么样得函数。但是这样的话,当结构体数量过多时,会使得有很多的判断,这时我们可以使用函数指针。

如何使用函数指针呢‘

void (*play_ball)(void);

函数指针在32位的处理器中,也仅有4个字节。函数指针可以直接使用函数名为地址,不需要使用取址符号。

typedef struct student{char *name;int age;void (*good_work)(void);struct student *classmate;char *sex;
}student,*pstudent;

链表

明确了指针的概念之后,链表就更简单了。为了更好的理解,我们用间谍来举例子。

typedef struct spy{char *name;struct spy *next;
} spy,*p_spy;p_spy head = NULL;
spy A = {"A",NULL};
spy B = {"B",NULL};
spy C = {"C",NULL};A.next = &B;
B.next = &C;
C.next = NULL;
head = &A;

A的下线是B,B的下线是C。head存贮的是A的地址,而A.next中存放的是B的地址,B.next中存放的是C的地址。如何打印出全部的对象呢。可以使用指针的方式。

while(head)
{printf("%s",head->name);head = head->next;
}

其实初始时,head取址A,在输出A的值之后,由于A.next中存放的是B的地址,B的地址被赋值给head,相当于对B的地址进行取址。
链表的实质,即是自身包含下一个变量的地址。那么链表如何插入新得变量呢。可以使用以下的方式。

void insert_spy(p_say newspy)
{p_spy last;if (head==NULL){head = newspy;newspy->next = NULL;}  else{/*先找到链表的最后一项,last*/last = head;while(last){if(last->next == NULL) //找到了break;elselast = last->next;}last->next = newspy;newspy->next = NULL;}
}

有了链表的插入,那么必然有链表的删除,如何删除链表呢。

void remove_spy(p_spy old_spy)
{p_spy p_leftif (head == oldspy){head  = oldspy->next;}else {//找出oldspy的上线left = head;while(left){if(left->next == oldspy)break;else{left =left->next;}}if(left){left->next = oldspy-next;}}	
}

ARM架构和汇编

回到一个简单的程序。

int a  = 1;
int b  = 2;
a = a + b ;

a+b的操作经历了几步呢,经历了四步。运算全部是在CPU中完成的,第一步,先使CPU在RAM中读取a的值,再用CPU在RAM中读取b的值,然后在CPU中计算a+b。第四步,将a+b的值写入ARM。既然要从RAM中取值,那么CPU内部必然存在可以存放数据的单元,这一单元被称为寄存器。可以用汇编指令直观表示a+b的的过程。汇编指令被写在FLASH中。

a+b的汇编表示。

LDR R0,[a]
LDR R1.[b]
ADD R0,R0,R1
STR R0,[a]

常用的汇编语言如下:

汇编语句执行意义
LDR R0,[R1]表示读取R1地址上的数据保存到R0中
LDM SP,{FP,SP,PC}按照高编号寄存器存放高地址内存值的原则,分别将FS,SP,PC所对应内存上的数据写到栈空间
STR R0,[R1]表示将R0的值写到R1所对应的内存空间上
B main表示跳转到main函数执行
MOV r0,r1表示把r1的值赋值给r0
ADD把两个操作数加起来把结果放到目的寄存器中
BL是另一个分支指令
BIT位清除指令

几个核心问题

  • 全局变量的初始化
  • 局部变量在哪
  • 局部变量的初始化
  • 栈的作用

全局变量在断电时,是不会存在于RAM中的,在通电运行时,才会存在于RAM中。全局变量的初始值是来自于代码(FLASH)中的。初始化全局变量有两种方式。

第一种方式:现在FLASH上的某个位置读取到数据,然后将值写道全局变量所在的地址上。这个方法简单,但是效率很低。

第二种方法:把多个数据整合成一个数据段,在运行数据时,将数据段整段整段的拷贝在RAM中。类似于memcpy。

那么对于初始化为0,或者没有初始化的道得全部变量怎么初始化呢。像这种变量,在内存中都放置在ZI段上。类似于MEMSET,初始化时,将ZI段全部清零。之后采取调用main函数。

在main函数之中,我们如何理解局部变量呢。这就要提到栈了。局部变量是在栈中的。栈是什么呢,栈是一块空闲的内存。在main中,函数的调用分为两步。先记录返回地址,再执行函数。在函数中调用函数的话,就涉及到最开始函数的地址被弄会被覆盖,此时最开始的函数地址会被放入栈中。局部变量是在main开始时,才开始初始化的。

最后说的话

终于把这个弄完了,我的拖延症啊。感谢韦东山老师这么好的课程。这也是作为自己学习的后记,欢迎大家进行探讨,文章中还有很多细节的地方需要完善,欢迎大家提出意见,以后会逐渐改善的。


http://chatgpt.dhexx.cn/article/vT5QJb3Z.shtml

相关文章

嵌入式C语言开发

这是我的第一篇对ARM有有疑问并展开调查的文档总结,以备之后查阅。 首先的疑惑是发现之前的源码中都是在对寄存器做操作,所以对寄存器的地址是怎么被确定的就很疑惑。 搜索引擎关键词:嵌入式 寄存器 基地址 使用 参考《嵌入式15——HAL 库中…

嵌入式C语言编译过程

1.预处理 gcc -E -o a.i 001.c 把001.c输出为a.s文件。预处理的本质是把宏定义替换处理。#define ABC 123 2.编译 gcc -S -o a.s 001.c 3.汇编 gcc -c -i a.s 001.c 把c文件生成汇编文件 4.链接 gcc -o build 001.c把001.c生成build.exe可执行文件,从预处理开始。

嵌入式C语言自我修养分享课件

一.异构计算 1.背景: 随着物联网、大数据、人工智能时代的到来,海量的数据分析和大量复杂的运算对CPU 的算力要求越来越高,CPU 的大部分资源用于缓存和逻辑控制,适合运行各种复杂的串行程序,但是单核或者多核CPU处理性…

标准c语言与嵌入式,嵌入式C语言与C语言的区别

嵌入式C语言与C语言的区别:最常用的系统编程语言是C,它是在汇编语言中使用的一种简单的编程语言,源代码采用自由格式。Embeddedc是c语言用于编写嵌入式软件的扩展,这两者有什么区别? 首先是启动过程 1.普通C语言程序的…

嵌入式 C 语言(上)

目录 基础知识数据类型const 用法作用域与 static 用法extern 用法 基础知识 嵌入式C语言和普通C语言在语法上几乎没有差别,其主要差别在于普通C语言的运行环境是OS之上,有很多的标准库函数支撑调用,分配的内存是电脑的内存,其处…

嵌入式C语言

文章目录 一、学会使用char/short/int关键字二、学会使用 if & switch三、学会使用 for & while四、学会使用static关键字五、学会使用define关键字六、学会使用typedef关键字七、学会使用enum关键字八、学会使用struct类型九、学会使用指针类型十、学会使用回调函数十…

嵌入式开发之C语言基础(一)

目前是一名大二升大三的学生,早就有想法写博文了,但是因为自己的拖延一直到现在才开始着手。关于博文,我有以下打算:把C语言再重新过一遍,毕竟嵌入式开发C语言的重要性不言而喻;接下来我还会有一些关于单片…

极简嵌入式C语言教程——从入门到入土(1)

文章目录 第一章&#xff1a;入门1.Hello World!2.C语言标识符与关键字(1)标识符&#xff1a;(2)关键字 3.数据类型与运算符(1)变量与常量<1>变量<2>变量的定义<3>常量 (2)运算符<1>算术运算符<2>自增、自减运算符<3>赋值与赋值组合运算符…

STM32中常用的C语言知识点,开始复习!

要学嵌入式&#xff0c;关注我要学嵌入式&#xff0c;嵌入式猛男的加油站。 C语言是单片机开发中的必备基础知识&#xff0c;这里就列举部分STM32学习中会遇见的C 语言基础知识点。 01 位操作 下面我们先讲解几种位操作符&#xff0c;然后讲解位操作使用技巧。C语言支持如下6中…

嵌入式C语言(入门必看)

目录 STM32的数据类型 const关键字 static 关键字 volatile关键字 extern关键字 struct结构体 enum typedef #define 回调函数 #ifdef 、#ifndef、#else 、#if 嵌入式开发中既有底层硬件的开发又涉及上层应用的开发,即涉及系统的硬件和软件,C语言既具有汇编语言操…

用单层感知器完成逻辑或运算的学习过程

用单层感知器完成逻辑或运算的学习过程 这道题目是我“认知科学”&#xff08;专业必修/doge&#xff09;课程的结课作业题之一&#xff0c;目的在于加深对单层感知器的理解&#xff0c;对于后续学习神经网络打下基础。 有关知识&#xff1a; B站有关视频 单层感知器的学习过…

python中逻辑运算_【多选题】Python 中用于表示逻辑与、逻辑或、逻辑非运算的关键字分别是( ) A. and B. add C. or D. not...

【多选题】Python 中用于表示逻辑与、逻辑或、逻辑非运算的关键字分别是( ) A. and B. add C. or D. not 更多相关问题 A.He has been asked to join the committee.B.There are several new people on the c A.fill inB.equipC.fitD.prove A.encounterB.discernC.seeD.fre…

逻辑运算符简介, 逻辑与,逻辑或,逻辑非和逻辑运算符里的短路运算规则

1&#xff0c;逻辑与 && 符号两边都为true&#xff0c;结果才为true 一假则假 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"…

R语言的逻辑与、逻辑或和元素逻辑与、元素逻辑或的区别

版权声明&#xff1a;转载请注明作者&#xff08;独孤尚良dugushangliang&#xff09;出处&#xff1a;https://blog.csdn.net/dugushangliang/article/details/116463648 参阅&#xff1a;https://www.runoob.com/r/r-basic-operators.html 下图为R运行结果。首先对a、b赋值&a…

Java逻辑操作符——逻辑非、逻辑与、逻辑或和逻辑异或

先上一段java代码&#xff0c;通过具体例子来理解抽象概念 public class 布尔值 {public static void main(String[] args) {boolean 逻辑非的值_测试1 true;boolean 逻辑非的值_测试2 false;System.out.println("逻辑非的值_测试1:"!逻辑非的值_测试1);System.ou…

逻辑与和按位与、逻辑或和按位或的区别

首先分别明确一下他们各自的概念。 按位与和按位或 按位与和按位或都属于位操作符。 注意&#xff1a;位操作符的操作数必须是整数。 按位与“&” 按二进制位对应的位进行与运算&#xff0c;对应位都为1时&#xff0c;结果才为1 3&5 3的二进制&#xff1a; 00000…

JS中的逻辑与和逻辑或

JS中的逻辑或||符号&#xff1a; 从字面上来说&#xff0c;只有前后都是 false 的时候才返回 false&#xff0c;否则返回 true。 console.log(5 > 6|| 6 > 5) //返回true5>6为false 但是 6>5为true 所以返回 true 总结&#xff1a;一真为真 特殊运算方法&#xff…

逻辑或( || )和逻辑与( )的关系

逻辑或&#xff0c;符号为“||”&#xff0c;只有操作数都是假&#xff0c;结果才是假。&#xff08;全假才为假&#xff09; 逻辑与&#xff0c;符号为“&&”&#xff0c;只有操作数都是真&#xff0c;结果才是真。&#xff08;全真才为真&#xff09; 如下图&#xf…

计算机逻辑与 或 非的表达式,计算机算数和,逻辑与,逻辑或,逻辑非分别是什么意思...

蔷祀的回答: 1、算术和:算术和就是所有的加数都是非负的(整数或0)得到的和。 2、逻辑与:逻辑与即1101 & 0100,就是按位相与,与的概念可以同俗的理解为,一个电路有两个串联的开关,只有同时关闭两个开关电路才通,打开任意一个开关电路都不通,所以那两个数逻辑与的结…

逻辑与(),逻辑或(||),and(),or(|)

一、背景、 这四个逻辑运算符&#xff0c;大家都知道&#xff0c;但是有时候会凌乱&#xff0c;再者就是我自己想写一点基础的东西&#xff0c;巩固一下自己&#xff0c;也算是一种笔记&#xff0c;不但自己会了&#xff0c;还可以分享给大家一起学习。 二、目的、 巩固自己…