前言:
只讲下一路自己学习单片机的一些理解,不会太强调说介绍某个知识点,沟通孤立的课程把,本来上课是讲PPT,然后课程作业,然后实践和考核,老师会挑出特定知识点来进行考核,但不会进行连接,这些之间有一定沟壑,需要知识来连接,而网上也鲜有视频连接这一切(计算机可能有把,某些厉害的老师),有错误还请指出
也可以给我建议,可以丰富内容,我做这些,一是为了总结,二是为了帮助其他人,希望大家能互相帮助。
从一条指令来认识CPU
0100 (操作码) | 001 (R1) | 010 (R2) | 001000 (8)
这条指令的意思就是CPU执行加法操作 将R2 + 8 的结果存储在R1寄存器。转化为 ADD R1, R2, #8
操作码:
CPU里面有很多运算单元,比如加法器,减法器,CPU指令,精简指令集中的指令可能就是对应这些运算单元的功能
操作数:
操作数,字面意思就是被操作的数,比如1+1=2,这里的数字。重要的是如何获取操作数,涉及到一个概念,寻址,有很多种寻址方式,至于如何寻址,我会在接下来介绍一下数字电路逻辑门组成一个运算单元
(功能非常简单)。
运算逻辑与数字电路的运算逻辑实现(忘记哪本书介绍的了,这个举下例):
实现两数加法(1位):
输入:
A1 A2
输出(这里是两位的):
A1与A2之和
真值表:
0 | 0 | 0 | 0 | 0 + 0 = 0 (二进制 00) |
0 | 1 | 0 | 1 | 0 + 1 = 1 (二进制 01) |
1 | 0 | 0 | 1 | 1 + 0 = 1 (二进制 01) |
1 | 1 | 1 | 0 | 1 + 1 = 2 (二进制 10) |
这里是运算单元的(当然只是一位的,现在计算机一般都是64位,单片机就是8,32位),那么还有一个关键的,寻址。先介绍一下,计算机内部最重要的一个部分就是存储,需要获得存储单元内部的值
存储结构体如下:
1011 0101,例如访问这样一个地址的值,有行译码器,列译码器 11 5 ,(数字电路几下38译码器),获得这样一个单元的值,有读有写的(右下角)。
我们了解了操作码实际是啥,CPU如何获取数据(屏蔽了中间总线和一些必要的运算单元,在学习的时候,总是需要屏蔽掉一些东西,把一片功能抽象成黑盒,内部的实现细节不出探讨(只关心输入输出以及对应公式)
,从高层,了解干了啥,如何实现,再深入)
好了到这里我们可以建立一个模型:
(搞错了,是flash)
对了,指令的也有获取的哦,其实指令也是数据,在单片机,有散列文件,规定了指令存储在flash里面,因为它存储久。
CPU内部结构如下。具体可以去看科普视频,讲解了CPU如何运作。
时钟:时钟在单片机系统运行起到的作用(之后完善):
CPU是循环往复执行取值,解析指令,执行指令,获取数据,操作。那么需要一个时间周期来确认一个指令执行完。
一个完整系统:
[一张MCU图,包括CPU ,总线--Ibus Dbus,外部总线,存储单源,ram,rom,外设接口]
除了已经介绍完的,首先依然从指令来开始介绍微机原理(已经说明了CPU做的事情,无时无刻的取值,操作)
指令包括:
[一个表:包含的操作 给出汇编指令]
[介绍寻址方式] [举例寻址大小,8051扩容]
存储:介绍每类存储的特点,作用
中断:介绍中断概念,中断控制器,CPU遇到错误中断,进入hardfault,如何进入的?实际的案例(比如从stm32下举个例子)
(其他想到再来补充把)
STM32 MCU内部图:
stm32作为常用的一款mcu,这一章,我将引出时钟,中断,外部存储和外部接口(主要学习的),以及给出如何具体的学习公式(除了去了解外设的配置流程,通用的阅读操作寄存器也是非常重要的)
时钟配置:
也就是HSI ,HSE ,PLL时钟,再到外设时钟配置
从上一章,可以基本认识CPU作为计算器的了。但我们现在看到CPU不仅仅是作为计算器了,有非常多的功能了,是负责一个系统的运算器了。
举例这样一个场景:
妈妈正在照护两个儿子,小儿子刚出生需要哄着睡(比较紧急),大儿子需要喂奶(低优先级),妈妈需要完成两个行为,小儿子醒来需要先哄,直到睡眠,大儿子需要喂奶。
在小儿子睡着,妈妈先给大儿子喂奶,那么当小儿子吵醒,打断了妈妈,然后妈妈哄好后再去喂奶。这个小儿子哭泣就是中断,考虑到系统有更紧急的事情,这样就设计了中断。
NVIC:
中断控制器,
stm32f103中,指令通常存在于flash中,然后数据一般存储在SRAM中,这些都是写在散列文件,烧录,会把对应的数据写在SRAM中
STM32裸机开发(6) — Keil-MDK下散列文件的分析_stm32散列文件分析-CSDN博客
第一个ROXO,只读,是指令(记住Linux下chmod+x 文件,就是赋予了可执行数学),RW ZI段,全局变量和未初始化的变量。关于链接和记载地址,flash非易存储器,不容易丢失保存的指令和数据,运行时,数据(会被频繁访问的),会被放到RAM中
谁实现的呢?CPU(其实没关注这个细节)。网上搜的是CPU,有这段汇编代码。
学习Stm32
学习stm32,主要是学习,时钟配置,NVIC,EXTI,GPIO,外设接口UART,SPI(学协议,通信流程,通信数据格式,实际中需要学接口配置,学抓取数据包,学习可能的问题),以及开门狗等等
其实可以看到NVIC是stm32 muc 内核的,其他都是挂载数据总线下的APB1 APB2,AHB总线下,也就是类似RAM中的内存(可读可写)。
映射的地址如下:
这些外设,GPIO,SPI等等通常有三类寄存器,数据寄存器,状态寄存器,控制寄存器。在stm32调试的时候会去看这几个寄存器。
学习外设的建议是:
stm32已经写好了每个外设寄存器,也定义好了,比如说,把某个外设初始化需要的操作,
就类似这样
#define APB1 0x400000000 APB1下起始地址
#define ADC1_XX APB1+偏移地址
这些在stm32f10x.h下有定义
如图
那么 GPIOA_CRL 就是这个GPIOA
如果需要操作的话就是
*GPIO_CRL |= 1<<7; 把第七位设置为1 。
相关文件已经帮你做好了,他们这样定义的
typedef struct ADC_Iiit_type*{
int reg;
int xx;(刚好四个字节)
}ADC_Iiit_type;
需要记忆的点就是:
1.每个模块的功能,模式
2.配置该模式的流程和方法
1.开时钟
2.配置结构体,需要记下这个结构体的成员 -- 配置中断啥的,中断函数。(这个部分有多个模块配置,才能组成一个功能)
3.使能
哦,还有一个重要的点:NVIC,这个,不大好讲,自己去看吧,NVIC,即使学习RTOS也不需要太那啥,你可能需要关注三个寄存器:
可能还需要记一个寄存器向量表偏移寄存器,这个表保存了中断需要的函数的地址。
学习RTOS
如何,CPU中寄存器
R13、R14、R15
LR寄存器:函数调用结束加载LR的值到PC用以返回。所以在线程调度,需要把LR寄存器的值保存到新线程TCB(记录了栈等一些信息)的栈。Thumb指令下,需要把LR寄存器的0位设置为1
R15:PC寄存器,指向下一条指令
特殊寄存器:
用于屏蔽寄中断,表示处理器状态等。
线程切换:
概念:
就是不同的任务(函数),里面大概率含有while循环,有while循环就不会返回,CPU控制权就一直在这个函数里面,不会切换,但是实际上就是CPU不停的执行while循环的任务,如果想切换,那么就可以在中断里面把PC寄存器的值更改,回到正常态,就执行其他任务了,因为PC寄存器的被更改了
TCB:
包含了CPU上下文,线程调读信息。
进入中断前,把寄存器信息保存到栈中,进入中断,在systick_handlder会判断该执行哪个任务了,schedule中有判断,在pendsv手动触发和悬挂(最低优先级)
一、 线程通信 (Inter-Thread Communication)
通信的目的是传递数据,即一个线程如何将信息发送给另一个线程。
1. 全局变量 (Global Variables)
概念: 这是最基础、最直接的通信方式。在程序的全局作用域定义一个变量,所有线程都可以读取和修改它。
工作方式: 线程 A 修改全局变量的值,线程 B 读取这个值,就完成了一次信息的传递。
优点:
实现极其简单。
缺点:
线程不安全:必须配合同步机制(如互斥锁)来使用,否则会产生数据竞争和不一致的问题。
耦合度高: 所有线程都依赖这个全局变量,使得代码难以维护和理解。
无法实现阻塞等待: 消费线程只能通过不断地轮询(空转 CPU)来检查数据是否更新,效率低下。
使用场景: 仅适用于传递非常简单、非关键的状态标志,并且必须加锁保护。
2. 消息队列 (Message Queue)
概念: 一个由操作系统内核管理的消息缓冲区,本质上是一个线程安全的先进先出 (FIFO) 队列。它用来在线程间传递固定大小或可变大小的数据块(消息)。
工作方式:
发送线程(生产者)调用 send API 将一条消息放入队列的尾部。
接收线程(消费者)调用 receive API 从队列的头部取走一条消息。
自带同步机制:
如果队列满了,发送线程会自动阻塞(睡眠),直到队列有空位。
如果队列为空,接收线程会自动阻塞(睡眠),直到队列中有新消息。
优点:
异步解耦: 生产者和消费者不需要同时在线,可以将数据“寄存”在队列中,实现了时间和逻辑上的解耦。
线程安全: 队列本身由 RTOS 内核保证其操作的原子性,用户通常不需要额外加锁。
一对多/多对一通信: 可以支持多个线程向同一个队列发送消息,或从同一个队列接收消息。
使用场景: 最常用、最通用的线程间通信方式。非常适合日志系统、事件处理、任务分发等生产者-消费者模型。
3. 邮箱 (Mailbox)
概念: 可以看作是消息队列的一个简化版或特例。它通常只用来传递一个固定大小的“邮件”,这个“邮件”通常是一个整数或者一个指针。
与消息队列的区别:
容量: 邮箱的容量通常很小,很多实现中只有一个槽位。
内容: 消息队列可以传递任意大小的数据块,而邮箱通常只传递一个字长的数据(比如一个 32 位的指针)。
工作方式:
发送线程 post 一封“邮件”(比如一个指向某个数据缓冲区的指针)。
如果邮箱是满的(比如那一个槽位已经被占用),发送线程会阻塞。
接收线程 pend 等待邮件。如果邮箱为空,接收线程会阻塞。
优点:
比消息队列更轻量级,开销更小。
非常适合用来传递数据的所有权(比如传递一个动态分配的内存缓冲区的指针),而不是数据本身。
使用场景: 当你只需要传递一个简单的控制信息或一个数据指针时,邮箱是比消息队列更高效的选择。
二、 线程同步 (Thread Synchronization)
同步的目的是协调时序,即控制多个线程的执行顺序,确保它们在访问共享资源时不会发生冲突。
1. 锁 (Lock) / 互斥量 (Mutex)
概念: 一种排他性的同步机制,用来保护一个“临界区 (Critical Section)”(即访问共享资源的代码块)。Mutex 来源于 Mutual Exclusion(互斥)。
工作方式:
它就像一个会议室的钥匙”。任何线程想进入会议室(临界区),都必须先拿到这把唯一的钥匙。
线程在进入临界区前 lock (获取锁)。如果锁已被其他线程持有,则该线程阻塞等待。
线程离开临界区后 unlock (释放锁),让其他等待的线程有机会获取它。
优点:
逻辑简单,是解决数据竞争问题的最基本、最直接的工具。
缺点:
可能导致性能瓶颈,因为临界区是串行执行的。
可能产生死锁 (Deadlock)(两个或多个线程互相等待对方释放的锁)。
可能产生优先级反转 (Priority Inversion)。
使用场景: 保护任何可能被多个线程同时修改的共享资源,如全局变量、设备寄存器、数据结构等。
2. 信号量 (Semaphore)
概念: 一种更广义的同步工具,它是一个计数器,用来管理对一组有限数量资源的访问。
工作方式:
信号量有一个初始计数值。
wait (或 pend, P 操作): 尝试获取一个资源。如果信号量计数 > 0,则将其减 1 并继续执行。如果计数 = 0,则线程阻塞等待。
post (或 signal, V 操作): 释放一个资源。将信号量计数加 1,并可能唤醒一个正在等待的线程。
与互斥锁的区别:
互斥锁的计数值只能是 0 或 1(一把钥匙),是二值信号量的特例。
信号量的计数值可以是任意非负整数,可以允许多个线程同时访问资源(只要不超过计数值)。
所有权: 互斥锁强调“谁上锁,谁解锁”。而信号量没有所有权概念,任何线程都可以 post (释放) 一个信号量。
使用场景:
资源计数: 如管理一个数据库连接池,信号量初始值为连接数。
线程同步: 一个线程完成某项工作后,通过 post 信号量来通知另一个正在 wait 的线程可以开始工作了。
3. 事件集 (Event Group / Event Flags)
概念: 一个位掩码 (bitmask),通常是 8、16 或 32 位。线程可以等待这个掩码中的一个或多个“事件位”被置位。
工作方式:
一个线程可以 wait (等待) 事件集中的特定位组合。等待可以是 AND(等待所有指定的位都被置位)或 OR(等待任意一个指定的位被置位)。
其他线程或中断服务程序可以通过 set (置位) 操作来设置事件集中的一个或多个位。
当等待的条件满足时,等待的线程被唤醒。
优点:
非常灵活:能够让一个线程等待多个复杂的、组合的事件发生,这是信号量或条件变量难以优雅实现的。
缺点:
不传递数据,只用于同步。
使用场景:
一个初始化线程需要等待多个外设(如 WiFi、SD卡、传感器)都初始化完成后才能继续执行。每个外设初始化完成就 set 对应的事件位,初始化线程 wait 所有这些位的 AND 组合。
一个任务需要等待“有数据到达”或“用户按下取消按钮”这两个事件中的任意一个发生。