2024年1月

一、计算机启动的基本流程

当电源通电后,计算机系统的启动过程始于主板上的固件,通常是BIOS(基本输入/输出系统)或UEFI(统一可扩展固件接口)。基本流程如下:

1、电源通电

用户按下计算机电源按钮,电源开始供给计算机各个组件。此时,CPU并没有直接开始执行指令。

2、BIOS/UEFI

  • 自检
    : 电源通电后,控制权首先交给BIOS(或UEFI)固件。BIOS/UEFI负责进行自检(POST,Power-On Self-Test)。在自检过程中,BIOS/UEFI检查计算机的硬件组件,确保它们正常工作。如果自检发现问题,系统可能会发出蜂鸣声或在屏幕上显示错误信息。
  • 初始化硬件
    : BIOS/UEFI在自检完成后,开始初始化系统中的硬件组件,包括主板、内存、CPU、显卡等。这确保这些硬件能够在系统启动时正常工作。

3、Boot Loader 引导加载程序

  • 加载
    :一旦硬件初始化完成,BIOS/UEFI从预定义的引导设备(通常是硬盘或固态硬盘)中加载引导加载程序。BIOS确定从哪个设备引导计算机,例如硬盘、固态硬盘、光驱等。BIOS会尝试加载设备上的引导扇区(boot sector)或引导记录(boot loader)。
  • 执行
    :引导加载程序被加载到内存中后,控制权传递给它。引导加载程序的任务是加载操作系统内核到内存,并将控制权传递给操作系统。

4、操作系统启动

引导加载程序执行后,操作系统内核开始运行。操作系统初始化并接管计算机的控制。此时,用户通常会看到操作系统的启动画面或登录界面。

以上就是计算机通电后的基本工作流程。

sequenceDiagram
participant 电源
participant BIOS/UEFI
participant Boot Loader
participant OS
电源->>BIOS/UEFI: 通电并交付控制权
BIOS/UEFI->>BIOS/UEFI: 自检和初始化硬件
BIOS/UEFI->>Boot Loader: 从引导设备加载引导加载程序到内存
BIOS/UEFI->>Boot Loader: 交付控制权
Boot Loader->>OS: 从分区加载操作系统内核到内存
Boot Loader->>OS: 交付控制权
OS->>OS: 初始化并运行操作系统

现代操作系统是从最古老的
8086
系统一步一步发展而来的,处理器厂商为了向后兼容,很多底层相关的原理都是一样的(如果不兼容,就会丢弃市场份额)。

二、电源通电(系统上电)

1、用户按下电源按钮

计算机开机时,通常是通过前面板上的电源按钮(Power button)来触发硬件发出PWRSW#(Power Switch)信号的。基本流程如下:

  • 1.连接电源按钮
    : 电源按钮通常是连接到主板上的一个小电路板或电缆上的。这个电路板上有一个按钮,用户按下按钮就能触发电源信号。
  • 2.PWRSW#信号
    : PWRSW#是一个开关信号。按下电源按钮时,这个信号会从低电平(通常是地GND)变为高电平,然后再变回低电平。这种变化可以被主板的电路所感知。
  • 3.EC接收信号
    :嵌入式控制器(Embedded Controller EC)接收到PWRSW#信号,标志着电源启动过程的开始。

电源按钮和主板连接
: 电源按钮通过一组线缆连接到主板上的一个特定的连接器(插脚),通常被标记为"PWRSW"(Power Switch),PWRSW#就是通过其发送给主板。

2、嵌入式控制器(Embedded Controller EC)

嵌入式控制器EC是挂在CPU的LPC(Low Pin Count)总线下的一颗嵌入主控芯片,嵌入式控制器的目的是帮助计算机(主要是笔记本)管理低速外设,像触摸板、矩阵键盘等,最重要的是计算机通过EC来做电源管理。

EC启动电源的流程:

  • 1.EC通知PMU
    : 主板上通常集成了一个电源管理电路(Power Management Circuit,PMU),EC通过一个特定的线路通知PMU启动电源。
  • 2.PMU启动PSU
    : 电源管理电路发出启动信号(一个由PMU产生的数字信号或电压信号)后,会激活电源供应单元(Power Supply Unit,PSU),使其开始为计算机各个部分提供电源,这个线路通常被称为“电源启动”(PS-ON)线路。
  • 3.PSU启动并供电
    :一旦接收到启动信号,PSU开始启动,并向计算机的各个部分提供电源。
sequenceDiagram
participant EC
participant PMU
participant PSU
EC->>PMU: 通过特定的线路通知PMU启动电源
PMU->>PSU: 发出启动信号(PS-ON)
PSU->>PSU: 启动并向计算机各个部分提供电源

3、电源供应单元(Power Supply Unit,PSU)

基本流程

电源供应单元(Power Supply Unit,PSU)是计算机硬件的关键组件之一,它负责将来自电源的交流电(AC)转换为计算机内部电子设备所需的直流电(DC),PSU(电源供应单元)通过内部的电压监控电路来确定各个部分的电压是否已经稳定。以下是这个过程的详细描述:

  • 1.PSU启动并供电
    :一旦接收到启动信号,PSU开始启动,并向计算机的各个部分提供电源。
  • 2.电压监控电路
    :PSU内部有一个电压监控电路,这个电路会实时监控PSU的所有输出电压。
  • 3.电压检测
    :电压监控电路会检测每个输出电压是否已经达到他们的正常值。例如,+12V的输出电压是否已经稳定在+12V,+5V的输出电压是否已经稳定在+5V,等等。
  • 4.电源正常信号
    :当电压监控电路检测到所有的输出电压都已经稳定在他们的正常值时,它会触发一个名为“电源正常”(POWERGOOD#或者PWROK#)的信号。这个信号是一个电压信号,通常在+5V左右。
sequenceDiagram
participant PSU
participant 计算机各部分
participant 电压监控电路
participant 电源正常信号
PSU->>计算机各部分: 启动并供电
PSU->>电压监控电路: 监控所有输出电压
电压监控电路->>电压监控电路: 检测每个输出电压是否正常
电压监控电路->>电源正常信号: 触发电源正常信号(+5V)

当开关电源启动之后,如果交流输入电压在额定范围之内,且各路直流输出电压达到允许的最低检测值(+5V 输出为4.75V 以上),那么经过100ms~500ms 的延时,P.G.电路就会发出“电源正常”的信号(P.OK 为高电平)。当电源交流输入电压降至安全工作范围以下或+5V电压低于4.75V 时,电源会送出“电源故障”信号(P.OK 为低电平)。P.G.信号非常重要,即使电源的各路直流输出都正常,如果没有P.G.信号或P.G.信号时序不对,都会造
成电脑不能开机。

供电顺序

当电源供应单元(Power Supply Unit,PSU)收到启动信号后,它会开始提供电源给计算机的各个部分。这个过程涉及到主板上的不同芯片组(例如南桥和北桥)、CPU、内存等组件。

以下是电源启动后各个部分供电的一般顺序:

  • 电源供应单元启动
    : 收到启动信号后,电源供应单元开始运行。现代电源通常是开关电源,其内部电子元件调整输出电压,确保其符合计算机系统的要求。
  • CPU供电
    : 电源供应单元提供电源给CPU。这通常涉及多个电源轨道,包括主要的+12V轨道和辅助的+5V和+3.3V轨道。CPU通常有自己的电源规格,供电参数由主板上的电源转换电路来管理。
  • 内存供电
    : 电源供应单元还提供电源给主板上的内存模块。内存通常需要+5V和+3.3V电源。
  • 南桥和北桥供电
    : 电源供应单元继续向主板上的芯片组提供电源。南桥和北桥是主板上的两个关键芯片组,它们负责管理与I/O、存储、网络等相关的功能。南桥通常较为周边,而北桥则更靠近CPU。现代计算机已经采用了集成的处理器(SoC)设计,其中 CPU 集成了原先北桥和南桥的功能。
  • 扩展卡和外围设备供电
    : 电源供应单元还为主板上的扩展卡(例如显卡、网卡)和外围设备提供电源。这些设备通常通过PCI Express插槽或其他连接器与主板相连。
  • 启动存储设备供电
    : 存储设备(如硬盘、固态硬盘)在电源启动后也会得到电源供应,以便可以被访问。

4、EC启动CPU

当嵌入式控制器(EC)收到PWROK(或POWERGOOD)信号后,它会开始启动计算机的启动过程。以下是一些具体的步骤:

  • 1.EC通知芯片组
    :EC监测来自电源供应单元(PSU)的PWROK信号,EC就会通知主板上的芯片组,发送名为PM_PWRBTN#的信号,开始计算机的启动过程。
  • 2.南桥和北桥的交互
    :南桥和北桥相互通信,确保电源管理的正常进行。南桥向北桥发送PLT_RST#信号,同时向CPU发送PWRGOOD#信号。
  • 3.CPU启动
    :北桥向CPU发送CPU_RST#信号(电平信号),通知CPU开始工作。此时,计算机的启动过程正式开始。
  • 4.EC启动其他设备
    :在计算机启动过程中,EC还可能负责启动和管理其他设备,如冷却风扇、硬盘驱动器等。
  • 5.EC监控电源状态
    :在整个启动过程中,EC会继续监控电源状态。如果出现任何问题,EC会立即关闭电源以保护硬件。
sequenceDiagram
participant EC
participant PSU
participant 芯片组
participant 南桥
participant 北桥
participant CPU
participant 其他设备
PSU->>EC: 发送PWROK信号
EC->>芯片组: 发送PM_PWRBTN#信号
芯片组->>南桥: 通知电源管理
南桥->>北桥: 发送PLT_RST#信号
南桥->>CPU: 发送PWRGOOD#信号
北桥->>CPU: 发送CPU_RST#信号
CPU->>CPU: CPU启动并开始执行指令
EC->>其他设备: 启动和管理其他设备
EC->>PSU: 监控电源状态

CPU_RST#信号通过主板上的电路传输到CPU,CPU内部有一个专门的接口用于接收CPU_RST#信号。这个接口通常被称为复位(Reset)引脚或复位线路。当CPU接收到CPU_RST#信号时,这个复位引脚会被激活,从而触发CPU的复位过程,使CPU开始执行预设的启动程序

三、BIOS

现代计算机基本都用UEFI替换了BIOS,但整体流程了原理是一致的,所以依然使用BIOS来说明CPU启动操作系统的初始过程。

1、介绍

BIOS(基本输入输出系统)是一种在计算机启动时运行的软件,它负责初始化和测试计算机硬件,并加载操作系统。以下是关于BIOS的一些详细信息:

  • 1.功能
    :BIOS的主要功能是在计算机启动时初始化和测试系统硬件(如CPU、内存、硬盘驱动器等),并加载操作系统。这个过程通常被称为引导(booting)或启动。
  • 2.ROM存储位置
    :BIOS通常存储在主板上的一个ROM(只读存储器,BIOS程序占64K大小)芯片中。这个ROM芯片通常被称为BIOS ROM。
  • 3.POST
    :在计算机启动时,BIOS会首先执行一个名为电源自检(Power-On Self Test,POST)的过程。POST会检查和测试计算机的硬件,确保所有硬件都正常工作。
  • 4.启动顺序
    :BIOS会根据预设的启动顺序来查找启动设备。这可能是硬盘驱动器、光驱、USB驱动器或者网络。一旦找到启动设备,BIOS就会加载设备上的启动扇区到内存,并跳转到那个地址来执行操作系统的加载程序。
  • 5.设置
    :BIOS还提供了一个设置界面,通常被称为BIOS设置或者CMOS设置。在这个界面中,用户可以更改各种系统设置,如启动顺序、系统时间和日期、硬件参数等。

2、CPU加载BIOS

以32位CPU为例,接收到CPU_RST#信号后,会执行以下步骤来加载BIOS:

  • CPU会将
    指令指针寄存器(IP)
    设置为
    0xFFFFFFF0
    ,这是CPU保留的最高的64KB的地址空间给BIOS使用的起始地址。(IP寄存器存储了CPU要执行的下一条指令的地址)
  • CPU通过地址总线将
    0xFFFFFFF0
    发送给ROM。ROM会根据地址总线上的地址,将对应的数据通过数据总线发送回CPU。CPU会将数据总线上的数据读入
    指令寄存器(IR)
    ,然后解码并执行。(IR寄存器存储了CPU当前要执行的指令的内容)
  • 一般情况下,ROM中的
    0xFFFFFFF0
    处的数据是一条跳转指令,它会将IP寄存器的值修改为
    0xF0000
    ,这是BIOS的实际入口地址。这样,CPU就可以跳转到
    0xF0000
    处,开始执行BIOS的代码(在
    0xF0000

    0xFFFFF
    之间,大小64K)。
  • BIOS的代码会对硬件进行检测和初始化,然后加载操作系统的
    引导程序(Boot Loader)
    ,最后将控制权交给操作系统。
sequenceDiagram
participant CPU
participant ROM
participant BIOS
CPU->>ROM: 将0xFFFFFFF0发送给ROM
ROM->>CPU: 将0xFFFFFFF0处的数据(跳转指令)发送给CPU
CPU->>CPU: 将跳转指令读入IR,解码并执行,将IP设置为0xF0000
CPU->>ROM: 将0xF0000发送给ROM
ROM->>CPU: 将0xF0000处的数据(BIOS代码)发送给CPU
CPU->>BIOS: 将BIOS代码读入IR,解码并执行
BIOS->>CPU: 执行BIOS代码,(检测硬件,然后加载Boot Loader)

扩展(可跳过)

1、为什么32位的CPU,在执行BIOS第一条指令时,要将值修改成了0xF0000

  • 早期的IBM PC使用的是
    16位
    的CPU,它的地址线有
    20位
    ,可以寻址
    1MB
    的地址空间,即从
    0x00000

    0xFFFFF
    。其中,最高的64KB的地址空间,即从
    0xF0000

    0xFFFFF
    ,是给ROM使用的,用于存储BIOS的代码。

  • 16位
    的CPU启动时,它会将
    IP寄存器
    的值设置为
    0xFFF0
    ,然后从ROM中的
    0xFFF0
    处开始读取BIOS的入口地址(一条跳转指令),然后跳转到
    0xF0000
    处执行BIOS的代码。
  • 后来,CPU的地址线增加到
    32位
    ,可以寻址
    4GB
    的地址空间,即从
    0x00000000

    0xFFFFFFFF
    。但是,为了保持和早期的IBM PC的兼容性,CPU仍然保留了最高的
    64KB
    的地址空间给BIOS使用,也就是从
    0xFFFFFFF0

    0xFFFFFFFF
  • 当32位的CPU启动时,它会将IP寄存器的值设置为
    0xFFFFFFF0
    ,然后从ROM中的
    0xFFFFFFF0
    处开始读取BIOS的入口地址,然后跳转到
    0xF0000
    处执行BIOS的代码。这样,32位的CPU就可以和16位的CPU使用相同的BIOS代码,而不需要修改。

因此,
32位
的CPU,在执行BIOS第一条指令时,将值修改成了
0xF0000
,是为了和
16位
的CPU保持一致,实现兼容性。

2、64位CPU的加载过程

64位CPU的启动过程和32位CPU的启动过程基本相同,只是有以下几点不同:

  • 64位CPU的地址线有64位,可以寻址更大的地址空间,从
    0x0000000000000000

    0xFFFFFFFFFFFFFFFF
  • 64位CPU的数据总线也有64位,可以一次传输更多的数据,提高了效率。
  • 64位CPU的
    指令指针寄存器(IP)

    指令寄存器(IR)
    也有64位,可以存储和执行更长的指令,增强了功能。
  • 64位CPU在启动时,会先进入兼容模式,即模拟32位CPU的工作方式,以保持和旧的BIOS的兼容性。在兼容模式下,CPU的地址线只使用32位,地址总线上的地址也只有32位,即从
    0x00000000

    0xFFFFFFFF
    。因此,CPU仍然会从
    0xFFFFFFF0
    开始读取BIOS的入口地址,然后跳转到
    0xF0000
    处执行BIOS的代码。
  • BIOS
    的代码会检测
    CPU的类型
    ,如果发现是
    64位CPU
    ,就会切换到
    长模式
    ,即64位CPU的正常工作方式。在长模式下,CPU的地址线使用64位,地址总线上的地址也有64位,即从
    0x0000000000000000

    0xFFFFFFFFFFFFFFFF
    。这样,CPU就可以寻址更大的地址空间,加载更大的操作系统。

四、Boot Loader

1、介绍

  • Boot Loader是一种程序
    ,存储在硬盘的特定区域,用于加载操作系统。它的作用是在BIOS完成硬件的检测和初始化后,从硬盘或其他设备中读取操作系统的核心文件,将其加载到内存中,然后将控制权交给操作系统,使其开始运行。
  • Boot Loader
    的功能是实现
    CPU

    实模式

    保护模式
    的切换,以及从硬盘到内存的数据传输。
    实模式
    是一种最简单的内存管理模式,它只能寻址
    1MB
    的地址空间,而且没有内存保护机制。
    保护模式
    是一种高级的内存管理模式,它可以寻址
    4GB
    (32位)的地址空间,而且有内存保护机制。操作系统一般都是在
    保护模式
    下运行的,因此,
    Boot Loader
    需要将
    CPU

    实模式
    切换到
    保护模式
    ,才能加载操作系统。
  • Boot Loader

    原理
    是利用
    硬盘的分区表

    文件系统的信息
    ,以及
    操作系统的核心文件
    ,实现从
    实模式

    保护模式
    的切换,以及从
    硬盘

    内存
    的数据传输。

硬盘
是一种非易失性的存储设备,它可以长期保存数据,但是它的速度较慢,而且
不能直接被CPU执行

内存
是一种易失性的存储设备,它可以短期保存数据,但是它的速度较快,而且
可以直接被CPU执行
。操作系统需要在内存中运行,因此,Boot Loader需要将硬盘中的操作系统的核心文件读取到内存中,才能启动操作系统。

2、种类

Boot Loader的类型有很多,不同的操作系统和硬件平台可能使用不同的Boot Loader。常见的Boot Loader有以下几种:

  • GRUB
    :全称为GNU GRand Unified Bootloader,是一种开源的,多操作系统的,可配置的Boot Loader,支持Linux,Windows,FreeBSD等多种操作系统,以及多种硬件平台,如x86,x86_64,ARM等。GRUB的特点是可以在启动时提供一个菜单,让用户选择要启动的操作系统,也可以在菜单中修改启动参数,或者进入命令行模式,执行一些高级的操作。
  • LILO
    :全称为LInux LOader,是一种早期的,专用于Linux的Boot Loader,支持Linux和DOS等操作系统,以及x86硬件平台。LILO的特点是简单,稳定,但是不够灵活,不能在启动时修改启动参数,也不能支持多种文件系统,如FAT32,NTFS等。
  • NTLDR
    :全称为NT Loader,是一种专用于Windows NT系列的Boot Loader,支持Windows NT,2000,XP,Server 2003等操作系统,以及x86硬件平台。NTLDR的特点是可以在启动时提供一个菜单,让用户选择要启动的操作系统,也可以在菜单中修改启动参数,或者进入恢复控制台,执行一些修复的操作。
  • BOOTMGR
    :全称为Boot Manager,是一种专用于Windows Vista及以后的Boot Loader,支持Windows Vista,7,8,10,Server 2008等操作系统,以及x86,x86_64,ARM等硬件平台。BOOTMGR的特点是可以在启动时提供一个菜单,让用户选择要启动的操作系统,也可以在菜单中修改启动参数,或者进入高级启动选项,执行一些高级的操作。

3、BIOS通过Boot Loader加载OS

  • BIOS在完成硬件的检测和初始化后,会读取
    CMOS
    中的设置,确定要从哪个设备启动,比如硬盘,光盘,U盘等。(
    CMOS
    是一种可读写的存储器,用于保存BIOS的配置信息)
  • BIOS会根据CMOS中的设置,选择一个启动设备(通常是硬盘),通过INT 13h中断调用来访问硬盘,然后读取该设备的第一个扇区,也就是
    主引导记录
    (Master Boot Record MBR)。
    MBR
    是一个
    512字节
    的数据块,包含了Boot Loader的一部分代码,以及硬盘的分区表。
  • BIOS会将MBR中的Boot Loader的代码复制到内存的
    0x7C00
    处,然后跳转到该地址,执行Boot Loader的代码。MBR中除了启动代码外,还包含一个
    分区表
    (Partition Table),它记录了硬盘上的分区信息,包括每个分区的起始扇区、大小、类型和状态(是否为活动分区)。
  • MBR中的启动代码会根据
    分区表
    来找到
    活动分区
    (Active Partition),也就是要启动的
    操作系统所在的分区
    ,然后从该分区的第一个扇区中读取
    卷引导扇区
    (Volume Boot Record VBR)。VBR也是一个512字节的扇区,它包含了一个
    卷引导记录
    (Volume Boot Record)和一个
    文件系统信息
    (File System Info)。
  • MBR中的启动代码会将读取到的
    VBR
    复制到内存地址
    0x7C00
    处,覆盖掉原来的MBR,然后跳转到该地址执行VBR中的启动代码。VBR中的启动代码会根据文件系统信息来找到
    操作系统内核文件
    (OS Kernel File),这是一个可执行文件,它包含了操作系统的核心功能和服务。
  • VBR中的启动代码会将读取到的
    OS Kernel File
    复制到内存中的一个合适的地址,然后跳转到该地址执行OS Kernel File中的启动代码。这时,操作系统就开始运行了,它会接管计算机的控制权,初始化各种设备驱动和系统服务,然后加载用户界面和应用程序,等待用户的输入。
sequenceDiagram
BIOS->>CMOS: 读取
CMOS->>BIOS: 配置信息
BIOS->>Hard Disk: INT 13h中断读取第一个扇区
Hard Disk->>BIOS: MBR
BIOS->>Memory: 加载MBR到0x7C00
BIOS->>Memory: Jump to 0x7C00
Memory->>Hard Disk: INT 13h读取活动分区的第一个扇区
Hard Disk->>Memory: VBR
Memory->>Memory: 加载VBR到0x7C00
Memory->>Memory: Jump to 0x7C00
Memory->>Hard Disk: INT 13h读取OS Kernel File
Hard Disk->>Memory: OS Kernel File
Memory->>Memory: 加载OS Kernel File到内存
Memory->>Memory: Jump to OS Kernel Address

至此计算机启动就完成了,后续工作就交由操作系统了

扩展阅读

主引导记录MBR(Master Boot Record)

MBR是主引导记录(Master Boot Record)的缩写,它是一种存储在硬盘的第一个扇区的数据块,用于存储Boot Loader的一部分代码,以及硬盘的分区表。MBR的作用是在BIOS完成硬件的检测和初始化后,从硬盘中读取Boot Loader的代码,将其加载到内存中,然后执行Boot Loader的代码,从而启动操作系统。

MBR的结构一般由以下几个部分组成:

  • 引导代码区
    :这是MBR的主要部分,它存储了Boot Loader的一部分代码,用于从硬盘的分区表中找到操作系统所在的分区,然后读取该分区的第一个扇区,也就是卷引导记录(VBR)。VBR是一种存储在硬盘分区的第一个扇区的数据块,用于存储Boot Loader的另一部分代码,以及文件系统的信息。引导代码区的大小和内容取决于具体的Boot Loader,一般占用MBR的大部分空间,约440字节左右。
  • 磁盘签名
    :这是MBR的一个可选部分,它存储了一个32位的数据,用于标识硬盘的唯一性,以便于操作系统和应用程序识别和管理硬盘。磁盘签名的大小和内容是可变的,一般占用MBR的4个字节。
  • 保留字
    :这是MBR的一个固定部分,它存储了一个16位的数据,用于填充MBR的空白部分,以便于MBR的对齐和校验。保留字的大小和内容是固定的,其值为0x0000,占用MBR的2个字节。
  • 分区表
    :这是MBR的一个重要部分,它存储了硬盘的分区信息,如分区的类型,大小,位置,状态,引导标志等。分区表是一个记录了硬盘分区信息的数据结构,用于告诉BIOS和操作系统硬盘的结构和位置。分区表的大小和内容取决于具体的硬盘,一般占用MBR的最后64个字节,包含4个分区表项,每个分区表项占用16个字节。
  • 结束标志
    :这是MBR的一个固定部分,它存储了一个16位的数据,用于标识MBR的结束,其值为0x55AA。结束标志的作用是告诉BIOS和Boot Loader这是一个有效的MBR,可以执行其中的引导代码。结束标志的大小和内容是固定的,占用MBR的最后两个字节。

卷引导记录VBR(Volume Boot Record)

VBR是卷引导记录(Volume Boot Record)的缩写,它是一种存储在硬盘分区的第一个扇区的数据块,用于存储Boot Loader的一部分代码,以及文件系统的信息。VBR的作用是在MBR中的Boot Loader将CPU从实模式切换到保护模式后,从硬盘分区中读取操作系统的核心文件,将其加载到内存中,然后将控制权交给操作系统,使其开始运行。

VBR的结构一般由以下几个部分组成:

  • 引导代码区
    :这是VBR的主要部分,它存储了Boot Loader的一部分代码,用于从文件系统中读取操作系统的核心文件,将其加载到内存中,然后跳转到该地址,执行操作系统的代码。引导代码区的大小和内容取决于具体的Boot Loader和文件系统,一般占用VBR的大部分空间,约400字节左右。
  • BPB
    (BIOS Parameter Block):这是VBR的一个重要部分,它存储了文件系统的基本信息,如扇区大小,簇大小,保留扇区数,FAT表数,根目录项数,总扇区数,卷标,文件系统类型等。BPB的作用是告诉Boot Loader和操作系统文件系统的结构和位置,以便正确地读取文件和目录。BPB的大小和内容取决于具体的文件系统,一般占用VBR的前面部分,约60字节左右。
  • 扩展引导代码区
    :这是VBR的一个可选部分,它存储了一些额外的引导代码,用于实现一些高级的功能,如错误处理,菜单选择,密码验证等。扩展引导代码区的大小和内容取决于具体的Boot Loader和文件系统,一般占用VBR的后面部分,约50字节左右。
  • 结束标志
    :这是VBR的一个固定部分,它存储了一个16位的数据,用于标识VBR的结束,其值为0x55AA。结束标志的作用是告诉BIOS和Boot Loader这是一个有效的VBR,可以执行其中的引导代码。结束标志的大小和内容是固定的,占用VBR的最后两个字节。

INT 13h

INT 13h是一个BIOS提供的中断服务,它可以让Boot Loader访问硬盘的任意扇区,而不需要知道硬盘的具体类型和参数。通过INT 13h,Boot Loader可以根据文件系统信息来定位OS Kernel File的位置,然后读取它到内存中。

INT 13h是一个很方便的服务,它可以屏蔽硬盘的底层细节,让Boot Loader只需要关注如何加载操作系统。如果没有INT 13h,Boot Loader就需要自己实现硬盘的驱动程序,这会增加Boot Loader的复杂度和大小,而Boot Loader的空间是非常有限的,只有512字节。

转载至我的博客
https://www.infrastack.cn
,公众号:架构成长指南

大家好,我是蜗牛哥,好多开发人员,尤其是没接触过 k8s 的人员对如何在k8s中部署一个 前后端应用很模糊,不知如何下手,所以本篇讲一下如何快速在 k8s 部署一个前后端应用,让大家对k8s有个快速认识

前置依赖

  • k8s集群,如果没有安装,请参考
    k8s 部署手册
  • kubectl ,客户端部署需要依赖

应用镜像构建

应用镜像构建不用自己去执行,相关镜像已经推送到 docker hub仓库,如果要了解过程和细节,可以看一下,否则直接跳到k8s yaml 文件配置章节

Java应用镜像构建

代码地址:
https://github.com/dongweizhao/backend

Java应用打包

调用
sh package.sh
会执行mvn的package命令,进行打包

编写Dockerfile

从target目录复制可执行jar

FROM openjdk:8-jre
COPY target/backend-0.0.1-SNAPSHOT.jar /app.jar
ENTRYPOINT java -jar ${JAVA_OPTS} /app.jar

镜像打包推送

执行
sh push.sh
推送至dockerhub仓库,镜像地址:
dweizhao/backend:latest

前端应用镜像构建

代码地址:
https://github.com/dongweizhao/frontend
前端项目结构如下

采用百度低代码平台amis进行开发

编写Dockerfile

拷贝前端工程dist目录至
/frontend
,并进行目录授权

from nginx
copy ./dist /frontend
run chown nginx.nginx /frontend -R
copy nginx.conf /etc/nginx/conf.d/default.conf

编写nginx.conf文件

server{
    listen 80;
    server_name localhost;
     root  /frontend;
     index index.html index.htm;

     location /login {
             try_files $uri $uri/ /login.html;
     }
}

推送镜像

执行
sh push.sh
推送dockerhub仓库,镜像地址:
dweizhao/frontend:latest

k8s yaml文件配置

以下是我们部署的服务在k8s 中路由示意图

后端服务配置

backend-dp.yaml

由于我们服务是无状态服务,使用Deployment进行部署,Deployment拥有更加灵活强大的升级、回滚功能,并且支持滚动更新

apiVersion: apps/v1
kind: Deployment
metadata:
  name:  backend
spec:
  selector:
    matchLabels:
      app:  backend
  replicas: 1
  template:
    metadata:
      labels:
        # service 会根据此标签来查找此pod
        app:  backend
        version: latest
    spec:
      containers:
        - name: backend
          image: "dweizhao/backend:latest"
          imagePullPolicy: Always

backend-svc.yaml

Service相当于Spring cloud中Ribbon的作用,提供了服务发现和负载均衡的功能,而不用关心具体服务实例有多少个,在 k8s 的服务实例就是Pod,这里我们使用
ClusterIP
类型,因为是通过Ingress在集群内访问,通过
app:backend
标签,来查找对应 pod,所以 pod 的label 必须包含
app:backend

apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  type: ClusterIP
  ports:
    - name: backend-http
      port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    # 根据标签查找 pod
    app: backend

前端服务配置

frontend-dp.yaml

前端镜像是一个 nginx的,

apiVersion: apps/v1
kind: Deployment
metadata:
  name:  frontend
spec:
  selector:
    matchLabels:
      app:  frontend
  replicas: 1
  template:
    metadata:
      labels:
        # service 会根据此标签来查找此pod
        app:  frontend
        version: latest
    spec:
      containers:
        - name: frontend
          image: "dweizhao/frontend:latest"
          imagePullPolicy: Always

frontend-svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: ClusterIP
  ports:
    - name: frontend-http
      port: 80
      targetPort: 80
      protocol: TCP
  selector:
    # 根据标签查找 pod
    app: frontend

Ingress配置

Ingress相当于nginx 的作用,匹配url转发请求至Service

注意:k8s不同的版本,对应的Ingress apiVerson 有点细微差别,所以要找到对应k8s版本的Ingress,我们的 k8s 版本为
v1.25.13

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
spec:
  ingressClassName: nginx
  rules:
    - host: k8sdemo.com
      http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: frontend
              port:
                number: 80
        #backend开头的 url,转发到后端服务 ,否则都转发到前端服务              
        - path: /backend
          pathType: Prefix
          backend:
            service:
              name: backend
              port:
                number: 8080

k8s部署

部署使用 kubectl 进行部署,如果没有请先安装,安装完成以后,把以上yaml文件保存至本地

创建命名空间

kubectl create namespace k8sdemo

部署

部署系统至
k8sdemo
空间下

kubectl create namespace k8sdemo && kubectl apply -f backend-dp.yaml -f backend-svc.yaml -f frontend-dp.yaml -f frontend-svc.yaml -f ingress.yaml -n k8sdemo

查看系统状态

#查看pod 启动状态
kubectl get pods -n k8sdemo
# 查看服务状态
kubectl get svc -n k8sdemo
# 查看Ingress状态
kubectl get ingress -n k8sdemo

host 解析

在 hosts 文件中,对
k8sdemo.com
域名映射,映射到k8s 的任意 node 节点上即可,假设IP为
172.18.2.53
,配置如下

172.18.2.53 k8sdemo.com

测试

在浏览器访问
k8sdemo.com
,如果出现以下结果则部署成功,可以看到正确请求到数据,这个数据为
backend
提供

总结

以上我们只是简单演示了下,如何在 k8s 中快速部署一个前后端应用,让你对在 k8s 操作有一个快速认识,但是此应用如果要在生产使用还要在做些配置,比如探针配置,因为后端服务的启动,可能是个假启动,必须要配置探针探活之后,才能让 service访问,否则导致请求异常,同时应用有些文件如果需要持久化,还需要配置存储卷等操作

扫描下面的二维码关注我们的微信公众帐号,在微信公众帐号中回复◉加群◉即可加入到我们的技术讨论群里面共同学习。

用 SpringBoot 和 SSE 打造动态前端更新的终极指南

你知道什么是开发者的梦魇吗?慢!慢!慢!在一个需要实时数据更新的应用中,如果数据像乌龟一样慢吞吞地爬行,那用户体验就会像坐过山车一样直线下降。所以今天,我们要化身为数据传输的超级英雄,用 SpringBoot 和 SSE(服务器发送事件)打造一个超酷、超快、而且超实时的数据流!

为什么选择 SSE?

在开始我们的冒险之前,先让我们来谈谈为什么要选择 SSE(服务器发送事件)。简单来说,SSE 就像是那个总是知道你需要什么并且在你还没说之前就把它送到你面前的超级服务员。它允许服务器主动将信息“推送”到客户端,而不是等待客户端来“询问”。想象一下,你正在看一场激动人心的球赛直播,而不是每五秒刷新一次页面,SSE 可以帮你实时看到每一个进球。是不是很酷?

创建控制器

好的,现在让我们开始编写一些代码。首先,我们需要创建一个 SpringBoot 控制器。这个控制器就像是魔法世界的大门,让所有神奇的事情开始发生。

@RestController
@RequestMapping("/user")
public class UserController {

    private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();

    @Autowired
    private UserMapper userMapper;

    @GetMapping(value = "/get",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public  SseEmitter getAllUsers(){
        SseEmitter emitter = new SseEmitter();
        this.emitters.add(emitter);

        emitter.onCompletion(() -> this.emitters.remove(emitter));
        emitter.onError((e) -> this.emitters.remove(emitter));
        emitter.onTimeout(() -> this.emitters.remove(emitter));

        return emitter;
    }
   @GetMapping("/add")
    public void addUser(){
        User user = new User();
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        user.setUsername(formatter.format(now));
        user.setPassword(UUID.randomUUID().toString());
        userMapper.addUser(user);
        sendToClients();
    }
	

我这里做的是从数据库查询数据实时推送到前端,你也可以换成任何你喜欢的方式,在下面的方法中

发送数据

现在,是时候学习一些发送数据的魔法咒语了。每当后端有新的数据更新时,我们就可以调用
sendToClients
方法,让这些数据像小精灵一样飞到每个客户端。

    public void sendToClients() {
        List<User> users = userMapper.getUsers();
        for (SseEmitter emitter : emitters) {
            try {
                emitter.send(users);
            } catch (IOException e) {
                emitter.completeWithError(e);
            }
        }
    }

前端实现

接下来,在前端的世界里,我们需要打开一个魔法视窗来接收这些数据。这个魔法视窗就是 JavaScript 的 EventSource。

<!DOCTYPE html>
<html>
<head>
    <title>SSE Example</title>
</head>
<body>
<div id="sse-data"></div>

<script>
    const sseData = document.getElementById("sse-data");

    const eventSource = new EventSource("/user/get");

    eventSource.onmessage = (event) => {
        sseData.innerHTML = event.data;
    };

    eventSource.onerror = (error) => {
        console.error("SSE Error:", error);
    };
</script>
</body>
</html>

整合流程

最后,让我们把这一切魔法整合在一起。启动你的 SpringBoot 应用,打开你的前端页面,你就会看到数据像水一样流畅地在你眼前流淌。不再是冰冷的静态页面,你的应用现在生动、活泼,充满了魔法的力量!


一. string 类

  • 很多应用程序都需要处理字符串。C语言在string.h(在++中为cstring)中提供了一系列的字符串函数,很多早期的C++实现为处理字符串提供了自己的类。

  • string类是由头文件string支持的(以头文件string.h和cstring支持对C风格字符串进行操纵的C库字符串函数,但不支持string类)。要使用类,关键在于知道它的公有接口,而string类包含大量的方法,其中包括了若干构造函数,用于将字符串赋给变量、合并字符串、比较字符串和访问各个元素的重载运算符以及用于在字符串中查找字符和子字符串的工具等。以string类包含的内容很多。

1. 构造字符串

  • 先来看string的构造函数。毕竟,对于类而言,最重要的内容之一是,有哪些方法可用于创建其对象。程序清单1使用了string的7个构造函数(用ctor标识,这是传统C++中构造函数的缩写)。表1简要地描述了这些构造函数,它首先按顺序简要描述了程序清单1使用的7个构造函数,然后列出了C++11新增的两个构造函数。使用构造函数时都进行了简化,即隐藏了这样一个事实:string实际上是模板具体化basic_string<char>的一个typedef,同时省略了与内存管理相关的参数。 size_type是一个依赖于实现的整型,是在头文件string中定义的。string类将
    string::npos定义为字符串的最大长度,通常为unsigned int的最大值。
    以表格中使用缩写
    NBTS
    (null终止string)来表示以空字符结束的字符串一一传统的C字符串。


    表1 string类的构造函数


    构 造 函 数

    描述

    string(const char * s)

    将string对象初始化为s指向的NBTS

    string(size_type n, char c)

    创建一个包含n个元素的string对象,其中每个元素都被初始化为字符c

    string(const string & str)

    将一个string对象初始化为string对象str(复制构造函数)

    string()

    创建一个默认的string对象,长度为0(默认构造函数)

    string(const char * s, size_type n)

    将string对象初始化为s指向的NBTS的前n个字符,即使超过了NBTS结尾

    template<class Iter>
    string(Iter begin, Iter end)

    将string对象初始化为区间[begin, end)内的字符,其中begin和end的行为就像指针,用于指定位置,范围包括begin在内,但不包括end

    string(const string & str, size_type pos, size_type n = npos)

    将一个string对象初始化为对象str中从位置pos开始到结尾的字符,或从位置pos开始的n个字符

    string(string && str) noexcept

    这是C++11新增的,它将一个string对象初始化为string对象str,并可能修改str(移动构造函数)

    string(initializer_list<char> il)

    这是C++11新增的,它将一个string对象初始化为初始化列表il中的字符


    程序清单1 :
    #include <iostream>
    #include <string>
    // using string constructors
    int main()
    {
        using namespace std;
        string one("Lottery Winner!"); // ctor #1
        cout << one << endl;           // overloaded <<
        string two(20, '$');           // ctor #2
        cout << two << endl;
        string three(one);             // ctor #3
        cout << three << endl;
        one += " Oops!";               // overloaded +=
        cout << one << endl;
        two = "Sorry! That was ";      //will Clean up the original string in object two
        three[0] = 'P';
        string four;                   // ctor #4
        four = two + three;            // overloaded +, =
        cout << four << endl;
        char alls[] = "All's well that ends well";
        string five(alls,20);          // ctor #5
        cout << five << "!\n";
        string six(alls+6, alls + 10); // ctor #6
        cout << six << ", ";
        string seven(&five[6], &five[10]); // ctor #6 again
        cout << seven << "...\n";
        cout << "Now four is :" << four << endl;
        string eight(four, 7, 16);     // ctor #7
        cout << eight << " in motion!" << endl;
        system("pause");
        return 0;
    }
    

    程序清单1 的输出:

    Lottery Winner!
    $$$$$$$$$$$$$$$$$$$$
    Lottery Winner!
    Lottery Winner! Oops!
    Sorry! That was Pottery Winner!
    All's well that ends!
    well, well...
    Now four is :Sorry! That was Pottery Winner!
    That was Pottery in motion!
    Press any key to continue . . .
    

2. string类输入

  • 有哪些输入方式可用呢? 对于C风格的字符串, 有3种方式:

    char info[100];
    cin >> info;             // read a word
    cin.getline(info, 100);  // read a line, discard \n
    cin.get(info, 100);      // read a line, leave \n in queue
    
  • 对于string 对象 有两种方式:

    string stuff;
    cin >> stuff;        // read a word
    getline(cin, stuff); // read a line, discard \n
    
  • 两个版本的getline() 都有一个可选参数, 用于指定使用什么字符作为读取的边界;

    cin.getline(info,100,':'); // read up to :, discard :
    getline(cin,stuff, ':');   // read up to :, discard :
    

    对于string版本的getline() 能够自动调整目标string 对象的大小, 使之刚好能存储输入的字符:

    char fname[10];
    string lname;
    cin >> fname;            // could be a problem if input size > 9 characters
    cin >> lname;            // can read a very, very long word
    cin.getline(fname, 10);  // may truncate input
    getline(cin, fname);     // no truncation
    

    自动调整大小的功能让 string版本的getline() 不需要指定要读取多少个字符的参数

  • string 版本的 getline() 从输入流中读取字符, 并将其放入目标string 中, 直到发生下面几种情况:


    • 到达文件尾的输入流的eofbit将被设置,这意味着方法fail()和eof()都将返回true;
    • 遇到分界字符(默认为\n) 在这种情况下, 将把分界字符从输入流中删除,但不存储它;
    • 读取的字符数达到最大允许值(string::npos和可供分配的内存字节数中较小的一个) 在这种情况下,将设置输入流的failbit,这意味着方法fail()将返回true。

    eofbit fail()等与
    流状态
    相关, 将在
    ~
    C++输入输出和文件
    -> 三. 使用cin进行输入 -> 2. 流状态
    ~
    中讲解

3. 使用字符串

  • 字符串比较
    string 类对全部6个关系运算符都进行了重载, 如果机器排列序列为
    ASCII码
    , 那么数字字符 < 大写字符 < 小写字符;
    对于每个关系运算符, 都以三种方式被重载, 以便将string对象和 另一个string对象,C风格字符串 进行比较 :

    #include <iostream>
    #include <exception>
    int main()
    {
        using namespace std;
        string snake1("cobra");
        string snake2("coaal");
        char snake3[20] = "cobra";
        if (snake1 < snake2) // operator<(const string &, const string &)
        {
            cout << "snake1 < snake 2" << endl;
        }
        if (snake1 == snake3) // operator==(const string &, const char *)
        {
            cout << "snake1 == snake3" << endl;
        }
    
        if (snake3 != snake2) // operator!=(const char *, const string &)
        {
            cout << "snake3 != snake2" << endl;
        }
        system("pause");
        return 0;
    }
    

    size() 和 length() 都返回字符串的字符数
    length()成员来自较早版本的string类, 而size()则是为STL兼容性而添加的

  • 字符串查找
    string::npos是字符串可存储的最大字符数, 通常是无符号int或long的最大取值;


    表2 重载的find()方法

    方 法 原 型

    描 述

    size_type find(const string & str,
    size_type pos = 0)const

    从字符串的pos位置开始,查找子字符串str。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回string :: npos

    size_type find(const char * s,
    size_type pos = 0)const

    从字符串的pos位置开始,查找子字符串s。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回string :: npos

    size_type find(const char * s,
    size_type pos, size_type n)

    从字符串的pos位置开始,查找s的前n个字符组成的子字符串。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回string :: npos

    size_type find(char ch,
    size_type pos = 0)const

    从字符串的pos位置开始,查找字符ch。如果找到,则返回该字符首次出现的位置;否则,返回string :: npos


    string 库还提供了相关的方法:
    rfind()、find_first_of()、find_last_of()、find_first_not_of()和find_last_not_of()
    ,它们的重载函数特征标都与find()方法相同。rfind()方法查找子字符串或字符最后一次出现的位置;find_first_of()方法在字符串中查找参数中任何一个字符首次出现的位置。例如,下面的语句返回 r 在“cobra”中的位置(即索引3),因为这是“hark”中各个字母在“cobra”首次出现的位置:
    int where = snake1.find_first_of("hark");
    

    find_last_of()方法的功能与此相同,只是它查找的是最后一次出现的位置。因此,下面的语句返回a在“cobra”中的位置:

    int where = snake1.find_last_of("hark");
    

    find_first_not_of()方法在字符串中查找第一个不包含在参数中的字符,因此下面的语句返回c在“cobra”中的位置,因为“hark”中没有c:

    int where = snake1.find_first_not_of("hark");
    

4. 其他string类方法

  • 很多, 就萝莉一些, 再挑几个讲. 其他的用到的时候就知道了

    a) =,assign() //赋以新值 
    b) swap() //交换两个字符串的内容 
    c) +=,append(),push_back() //在尾部添加字符 
    d) insert() //插入字符 
    e) erase() //删除字符 
    f) clear() //删除全部字符 
    g) replace() //替换字符 
    h) + //串联字符串 
    i) ==,!=,<,<=,>,>=,compare() //比较字符串 
    j) size(),length() //返回字符数量 
    k) max_size() //返回字符的可能最大个数 
    l) empty() //判断字符串是否为空 
    m) capacity() //返回重新分配之前的字符容量 
    n) reserve() //保留一定量内存以容纳一定数量的字符 
    o) [], at() //存取单一字符  at()索引无效时,会抛出out_of_range异常
    p) >>,getline() //从stream读取某值 
    q) << //将谋值写入stream 
    r) copy() //将某值赋值为一个C_string 
    s) c_str() //将内容以C_string返回  不可修改
    t) data() //将内容以字符序列指针形式返回 可修改
    u) substr() //返回某个子字符串 
    v)查找函数 
    w)begin() end() //提供类似STL的迭代器支持 
    x) rbegin() rend() //逆向迭代器 
    y) get_allocator() //返回配置器 
    
  • compare 返回值意义(吾用区间表示法表示):[小于,0,大于] , 0:相等

    string s("abcd"); 
    s.compare("abcd"); //返回0 
    s.compare("dcba"); //返回一个小于0的值 
    s.compare("ab"); //返回大于0的值 
    s.compare(s); //相等 
    //参数1:下标 2:字符个数 3:比较的对象 4:下标 5:字符个数
    s.compare(0,2,s,2,2); //用"ab"和"cd"进行比较 小于零 
    //参数1:下标 2:字符个数 3:比较的对象 4:字符个数
    s.compare(1,2,"bcx",2); //用"bc"和"bc"比较。
    
  • assign 重新分配

    s.assign(str); //字面意思
    //参数1:目标 2:下标 3:字符数
    s.assign(str,1,3);//如果str是"iamangel" 就是把"ama"赋给字符串 
    s.assign(str,2,string::npos);//把字符串str从索引值2开始到结尾赋给s 
    s.assign("gaint"); //字面意思
    s.assign("nico",5);//把’n’ ‘I’ ‘c’ ‘o’ ‘’赋给字符串 
    s.assign(5,'x');//把五个x赋给字符串 
    
  • append 附加

    s.append(str); 
    s.append(str,1,3);//不解释了 同前面的函数参数assign的解释 
    s.append(str,2,string::npos)//
    s.append("my name is jiayp"); 
    s.append("nico",5); 
    s.append(5,'x'); 
    s.push_back('a');//这个函数只能增加单个字符
    
  • insert 插入

    s.insert(0,"my name");
    s.insert(1, "m");
    s.insert(1,str); 
    
  • replace erase 替换和擦除

    s.replace(1,2,"nternationalizatio");//从索引1开始的2个替换成后面的C_string或string对象
    s.erase(13);//从索引13开始往后全删除 
    s.erase(7,5);//从索引7开始往后删5个 
    
  • substr 返回子字符串(新的string)

    s.substr();//返回s的全部内容 
    s.substr(11);//从索引11往后的子串 
    s.substr(5,6);//从索引5开始6个字符 
    
  • copy 复制并替换目标中原有的字符

    char str1[20] = "Hello";
    char str2[20] {0};
    string sl = "World";
    //参数1:目标对象 2:要copy的字符数 3:从sl的下标?开始
    sl.copy(str1, 5, 2);//
    cout << str1 << endl;
    
  • 方法capacity()返回当前分配给字符串的内存块的大小,而reserve()方法让您能够请求增大内存块

    #include <iostream>
    #include <string>
    int main()
    {
        using namespace std;
        string empty;
        string small = "bit";
        string larger = "Elephants are a girl's best friend";
        cout << "Sizes:\n";
        cout << "\tempty: " << empty.size() << endl;
        cout << "\tsmall: " << small.size() << endl;
        cout << "\tlarger: " << larger.size() << endl;
        cout << "Capacities:\n";
        cout << "\tempty: " << empty.capacity() << endl;
        cout << "\tsmall: " << small.capacity() << endl;
        cout << "\tlarger: " << larger.capacity() << endl;
        empty.reserve(50);
        cout << "Capacity after empty.reserve(50): "
             << empty.capacity() << endl;
        return 0;
    }
    
  • 如果您有string对象,但需要C风格字符串,该如何办呢?

    string filename;
    filename.c_str(); //返回c风格字符串
    

5. 字符串种类

  • 本节将string类看作是基于char类型的。事实上,正如前面指出的,string库实际上是基于一个模板类的:

    template<class charT, class traits = char _traits<charT>,
            class Allocator = allocator<charT> >
    basic_string {...};
    
  • 模板basic_string有4个具体化(特化),每个具体化都有一个typedef名称:

    typedef basic_string<char> string;
    typedef basic_string<wchar_t> wstring;
    typedef basic_string<char16_t> u16string;   // C++11
    typedef basic_string<char32_t> u32string ; // C++11
    

    这让您能够使用基于类型wchar_t、char16_t、char32_t和char的字符串。甚至可以开发某种类似字符的类,并对它使用basic_string类模板(只要它满足某些要求)。traits类描述关于选定字符类型的特定情况,如如何对值进行比较。对于wchar_t、char16_t、char32_t和char类型,有预定义的char_traits模板具体化,它们都是traits的默认值。Allocator是一个管理内存分配的类。对于各种字符类型,都有预定义的allocator模板具体化,它们都是默认的。它们使用new和delete。

1、准备材料

开发板(
正点原子stm32f407探索者开发板V2.4

ST-LINK/V2驱动
STM32CubeMX软件(
Version 6.10.0

keil µVision5 IDE(
MDK-Arm

CH340G Windows系统驱动程序(
CH341SER.EXE

XCOM V2.6串口助手
1个滑动变阻器

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板的
ADC实现单通道ADC采集
,具体为使用ADC1_IN5通道通过软件/定时器触发采集滑动变阻器上的分压

3、ADC概述

ADC即模拟数字转换,是将模拟电压量转换为数字量的一种手段,如下图所示为STM32F407单个ADC的结构框图
(注释1)
,其主要包括5个部分,分别为
①ADC电源引脚、②ADC输入引脚、③ADC触发源、④ADC转换规则和⑤ADC中断触发
,下面从这五个方面分别介绍STM32F407的ADC

①ADC电源引脚有VDDA、VSSA、VREF+和VREF-四个引脚,STM32的ADC采集范围为VREF- ≤ VIN ≤ VREF+,一般将ADC的负端参考电压与VSSA短接然后接地,将ADC的正端参考电压接VDDA即3.3V,这样
ADC的采集范围就设置为0~3.3V,对于12位分辨率的ADC,其采集数字量范围为0-4095
,这样就可以将0-3.3V的电压映射到0-4095的数字量,使其可以相互转化,ADC电源即参考电压引脚具体描述如下图所示(注释1)


STM32F407有三个ADC可供使用
,每个ADC又拥有16个通道ADCx_IN[15:0],其中ADC1还拥有Temperature Sensor Channel、Vrefint Channel和Vbat Channel三个内部通道,Temperature Sensor Channel通道用于测量芯片内部温度,范围为-40℃~125℃,精度为±1.5℃,Vrefint Channel用于测量内部参考电压,Vbat Channel用于测量备用电源电压的一半,ADC的转换主要依靠12位分辨率的片上模数转换器


注入通道和规则通道均有16个触发源
,可以选择定时器外部源触发/定时器比较捕获触发/软件常规触发,具体参看上图ADC框架中的紫色框框

④ADC启动转换时需要按照一定通道顺序转化,该顺序由规则通道和注入通道两者共同决定,其中
注入通道其数据寄存器为4*16位,因此可以同时转换四个通道,但是规则通道的数据寄存器只有一个16位的寄存器,因此必须一个通道一个通道的转换
,每转换完一个通道,就需要及时将转换完成的结果从规则通道数据寄存器中读出去,其中注入通道类似ADC通道转换过程的中断,如下图所示(注释2)


ADC的中断事件有DMA溢出、ADC转换结束、注入转换结束和模拟看门狗事件共四个事件
,前三个中断和其名字表述类似,当转换结束/溢出时就会产生中断,模拟看门狗可以设置ADC转换值的上限和下限,当超出限制之后就会产生中断,可以用于警报,如下表所示(注释1)

ADC有独立模式、二重和三重采集模式
,当只有ADC1启动时只能使用独立模式,当ADC1/2启动时可以使用二重采集模式,当ADC1/2/3/全部启动时可以使用三重采集模式,本实验只介绍独立模式

4、实验流程

4.0、前提知识

本实验为ADC独立模式单通道单次转换模式,主要利用ADC1_IN5通道由软件/定时器启动ADC转换,如果是软件手动启动的ADC转换则在单次转换模式下每次转换完成一次之后均需要再次手动启动ADC转换,另外需要注意ADC在开始精确转换之前需要一段稳定时间tSTAB,如下图所示为ADC转化所经过的路径

4.1、CubeMX相关配置

请先阅读“
STM32CubeMX教程1 工程建立
”实验3.4.1小节配置RCC和SYS

4.1.1、时钟树配置

系统时钟树配置均设置为STM32F407总线能达到的最高时钟频率,具体如下图所示

4.1.2、外设参数配置

本实验需要需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信

设置TIM3通用定时器溢出时间100ms,外部触发事件选择更新事件,参数详解请阅读“STM32CubeMX教程6 TIM 通用定时器 - 生成PWM波”实验,具体配置如下图所示

在Pinout & Configuration页面左边功能分类栏目Analog中单击其中ADC1,在Mode中勾选需要使用的输入通道,本实验为单通道转换实验,因此任意勾选IN0~IN15之间的任一通道均可,笔者勾选了IN5

在Configuration中对ADC1_IN5的转换参数进行配置,下面介绍一些比较重要的参数

------------------ ADCs_Common_Settings ------------------

①Mode(模式):
现在只能选择独立模式
,当同时启用了ADC1/2或ADC1/2/3时这里会出现多重ADC采集的模式可选

------------------------- ADC_Settings -------------------------

②Clock Prescaler(时钟分频):决定ADC转换的频率,分频越少ADC转换的频率越高,最少2分频,
一个通道一次ADC转换的总时间为N+12个ADC时钟周期,其中N为设置的采样次数Cycles

③Resolution(ADC转换精度):可以选择12/10/8/6位精度的转换值,精度选择不同需要的转换时钟周期也不同

④Data Alignment(数据对齐):由于规则数据寄存器为16位,但是最高转换精度为12位,因此
数据可以选择以右对齐/左对齐的方式放入寄存器

⑤Scan Conversion Mode(扫描转换模式):规则通道同时只能转换一个通道,启用该参数后,当规则通道中有多个通道等待转换时其
转换完当前通道会自动转换组内的下一个通道

⑥Continuous Conversion Mode(连续转换模式):启用该参数,ADC结束一个转换立即开始一个新的转换,与参数⑤共同启用,则
组内最后一个通道转换完毕后会立即切换到第一个通道继续转换

⑦DMA Continuous Requests(DMA请求):需要在DMA Settings中添加DMA请求后,该参数才可以使能

⑧End Of Conversion Selection(结束转换标志):选择是一个通道转换完就产生EOC标志,还是一个组内所有通道全部转换完才产生EOC标志

------------------ ADC_Regular_ConversionMode------------------

⑨NumberOfConversion(转换通道数量):常规规则通道希望转换的通道数量

⑩External Trigger Conversion Source(外部触发转换源)

⑪External Trigger Conversion Edge(外部触发转换边沿)

⑫Rank(规则通道排序)

------------------ ADC_Injected_ConversionMode------------------

⑬NumberOfConversion(注入通道转换模式通道数量):注入通道希望转换的通道数量

具体参数配置如下图所示

4.1.3、外设中断配置

在Pinout & Configuration页面左边System Core/NVIC中勾选ADC1/2/3全局中断,然后选择合适的中断优先级即可,步骤如下图所示

4.2、生成代码

请先阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节配置Project Manager

单击页面右上角GENERATE CODE生成工程

4.2.1、外设初始化调用流程

在生成的工程代码主函数main()中调用了MX_ADC1_Init()函数完成了对ADC1基本参数的配置,ADC常规规则通道/注入通道等参数配置

然后在ADC初始化函数HAL_ADC_Init()函数中调用了HAL_ADC_MspInit()函数对ADC1时钟和中断进行了使能,对中断优先级进行了配置,对ADC1_IN5输入引脚做了复用操作

如下图所示为ADC1单通道初始化的具体函数调用流程

4.2.2、外设中断调用流程

使能ADC1/2/3全局中断后在stm32f4xx_it.c中自动生成了TIM4的中断处理函数ADC_IRQHandler()

ADC_IRQHandler()调用了HAL库的ADC中断处理函数HAL_ADC_IRQHandler(),该函数处理所有的ADC中断事件

在ADC转换完成之后
最终调用了ADC转换完成中断回调函数HAL_ADC_ConvCpltCallback(),该函数为虚函数

如下图所示为ADC1单通道转换中断回调的具体函数调用流程

4.2.3、添加其他必要代码

在adc.c中重新实现ADC转换完成中断回调函数HAL_ADC_ConvCpltCallback(),在该函数中获取ADC的转换值,然后将其计算为电压值*1000,并将这两个值通过串口输出,具体代码如下图所示

源代码如下

/*转换完成中断回调*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    /*定时器中断启动单通道转换*/
    if(hadc->Instance == ADC1)
    {
        uint32_t val=HAL_ADC_GetValue(&hadc1);
        uint32_t Volt=(3300*val)>>12;
        printf("val:%d, Volt:%d\r\n",val,Volt);
    }
}

在主函数中以中断方式启动ADC转换,然后启动ADC的触发源TIM3定时器,具体代码如下图所示

5、常用函数

/*启动ADC转换*/
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc)
 
/*停止ADC转换*/
HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef *hadc)
 
/*以中断方式启动ADC转换*/
HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef *hadc)
 
/*停止ADC转换*/
HAL_StatusTypeDef HAL_ADC_Stop_IT(ADC_HandleTypeDef *hadc)
 
/*轮询ADC是否转换完毕*/
HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef *hadc, uint32_t Timeout)
 
/*获取ADC转换值*/
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef *hadc)
 
/*ADC转换完成中断回调函数*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)

6、烧录验证

6.1、实验具体流程

“配置USART1用于输出信息 -> 配置通用定时器TIM3实现100ms定时 -> TIM3外部触发事件选择为更新事件 -> 启动ADC1的通道5并配置相关参数 -> 外部触发转换源选择TIM3的外部触发事件 -> 启动ADC1/2/3全局中断 -> 重新实现ADC转换完成中断回调HAL_ADC_ConvCpltCallback()函数 -> 在回调函数中读取ADC转换值并通过串口输出 -> 在主函数中启动定时器和ADC转换”

6.2、实验现象

烧录程序,上电后打开串口,串口会每100ms传来一次ADC采集的数据,旋转滑动变阻器从一端到另一端,可以看到ADC采集到的值从0逐渐变为最大值4095

7、软件触发ADC采集

如果你希望不采用定时器触发中断采集的方式,而是想使用软件手动触发轮询采集的方式
,需要先将ADC规则转换模式中触发源修改为软件触发,然后使用HAL_ADC_Start()启动转换,使用HAL_ADC_PollForConversion()轮询检测是否转换完成,转换完成后使用可以HAL_ADC_GetValue()获取转换后的ADC值,具体参考如下图所示

源代码如下

HAL_ADC_Start(&hadc1);
if(HAL_ADC_PollForConversion(&hadc1,200)==HAL_OK)
{
    uint32_t val=HAL_ADC_GetValue(&hadc1);
    uint32_t Volt=(3300*val)>>12;
    printf("val:%d, Volt:%d\r\n",val,Volt);
}
HAL_Delay(500);

8、注释详解

注释1
:图片来自STM32F4xx中文参考手册 RM0090

注释2
:图片来自
【STM32】HAL库 STM32CubeMX教程九---ADC_cubemx adc-CSDN博客

参考资料

STM32Cube高效开发教程(基础篇)

更多内容请浏览
OSnotes的CSDN博客