Day3 进入32位模式并导入C语言

文章目录

      • [1. 制作真正的IPL](#1. 制作真正的IPL)
      • [2. harib00b例程](#2. harib00b例程)
      • [3. harib00c例程](#3. harib00c例程)
      • [4. harib00d例程](#4. harib00d例程)
      • [5. harib00g例程](#5. harib00g例程)
      • [5. harib00h例程](#5. harib00h例程)
      • [5. harib00i例程](#5. harib00i例程)
      • [5. harib00j例程](#5. harib00j例程)

本章的操作系统文件叫做haribote.img。

1. 制作真正的IPL

对helloos5中的ipl.nas添加内容:

shell 复制代码
; 读磁盘
		MOV		AX,0x0820
		MOV		ES,AX
		MOV		CH,0			; 磁道号的低8位(柱面号)
		MOV		DH,0			; 磁头号
		MOV		CL,2			; 低5位放入所读的起始扇区,位7-6表示磁道号的高2位

		MOV		AH,0x02			; AH=0x02 : 读盘
		MOV		AL,1			; 处理1个扇区
		MOV		BX,0
		MOV		DL,0x00			; 需要进行写操作的驱动器号,A驱动器
		INT		0x13			; 磁盘服务程序
		JC		error			; 如果进位(jump if carry)

本次新增了一个JC(jump if carry)指令,表示如果进位为1的话,就跳转。本代码中它与上一行的INT 0x13组合使用,如果INT 0x13返回值使进位标志位(carry flag)为置位为1,则执行JC。

另外还新增了INT 0x13指令,目的就是调用BIOS的0x13函数,磁盘读、写、扇区校验(verify)或者寻道(seek):

  • AH=0x02;:读盘。
  • AH=0x03;:写盘。
  • AH=0x04;:校验。
  • AH=0x0c;:寻道。
  • AL=处理对象的扇区数;:只能处理连续的扇区。
  • CH=柱面号 & 0xff;
  • CL=扇区号(0~5bits) | (柱面号 & 0x300) >> 2;
  • DH=磁头号;
  • ES:BX=缓冲区地址;:校验及寻道时不使用。
  • INT 0x13 返回值:
    • FLAGS.CF==0;:没有error,AH==0。
    • FLAGS.CF==1;:有error,错误码存入AH(类似于重置reset功能)。

柱面(cylinder)就是同心圆环,扇区(sector)是将圆环划分成的圆弧长条。一张软盘由80个柱面(C0, C1, C2...),2个磁头(H0和H1),每个柱面被划分为18个扇区(S1, S2, S3...),每个扇区是512字节容量,因此一张软盘的容量为80*2*18*512 = 1474569Byte = 1440KB

启动区IPL位于C0-H0-S1位置。

一个16bits寄存器只能寻址64KB(0xffff),于是设计了一个起辅助作用的段寄存器(segment register),用于指定缓冲区的内存地址。当使用段寄存器时,以ES:BX的方式来表示地址,写作MOV AL, [ES:BX],代表ES*16+BX,即最大可寻址0x10FFEF(1114095,大约1MB的内存)。设置ES=0x0820,BX=0,即将C0-H0-S2扇区内容加载到0x8200 ~ 0x83ff地址区域。并没有特殊的含义,只是由于启动区加载到0x7c00 ~ 0x7dff区域,所以0x07e00 ~ 0x9fbf之间没有特别用途,操作系统可以随意使用,因此就选择了0x8200之后的区域。

实际上只要期望指定地址,都会用到段寄存器,如果没有显式指定,那就是默认使用DS(数据段)段寄存器。例如:

  • MOV CX, [1234]实际上表示MOV CX, [DS:1234]
  • MOV AL, [SI]实际上表示MOV AL, [DS:SI]

因为存在这样的规则,所以DS必须首先初始化为0。

2. harib00b例程

bash 复制代码
# 添加多次读取软盘的操作
; 读磁盘

		MOV		AX,0x0820
		MOV		ES,AX
		MOV		CH,0			; 柱面0
		MOV		DH,0			; 磁头0
		MOV		CL,2			; 扇区2

		MOV		SI,0			; 记录失败次数的寄存器
retry:
		MOV		AH,0x02			; AH=0x02 : 读磁盘
		MOV		AL,1			; 1个扇区
		MOV		BX,0
		MOV		DL,0x00			; 磁盘驱动器A
		INT		0x13			; BIOS中的磁盘代号
		JNC		fin				; jump if not carry,如果不进位跳转到fin
		ADD		SI,1			; SI = SI + 1
		CMP		SI,5			; SI与5做比较
		JAE		error			; SI >= 5 跳转到error (jump if above or equal)
		MOV		AH,0x00
		MOV		DL,0x00			; 磁盘驱动器A
		INT		0x13			; 重置驱动器
		JMP		retry

存在一次读取软盘失败的情况,因此这段代码尝试读取5次。

  • JNC表示jump if not carry,如果进位标志位为0的话,就跳转。
  • JAE表示jump if above or equal,如果大于或等于时跳转。

再跳转到retry,即重新尝试读取软盘之前,复位了AH和DL寄存器,这两个操作可以认为是"系统复位",它复位了软盘的状态。

3. harib00c例程

bash 复制代码
; 读磁盘2~18扇区

		MOV		AX,0x0820
		MOV		ES,AX
		MOV		CH,0			; 柱面0
		MOV		DH,0			; 磁头0
		MOV		CL,2			; 扇区2
readloop:
		MOV		SI,0			; 记录失败次数的寄存器
retry:
		MOV		AH,0x02			; AH=0x02 : 读磁盘
		MOV		AL,1			; 1个扇区
		MOV		BX,0
		MOV		DL,0x00			; 磁盘驱动器A
		INT		0x13			; BIOS的磁盘驱动器
		JNC		next			; jump if not carry, 如果不进位(没有错误)则跳转next
		ADD		SI,1			; SI = SI + 1
		CMP		SI,5			; SI和5作比较
		JAE		error			; SI >= 5 ,则跳转到error(jump if above or equal)
		MOV		AH,0x00
		MOV		DL,0x00			; A驱动器
		INT		0x13			; 重置BIOS驱动器
		JMP		retry
next:
		MOV		AX,ES			; 把内存地址后移0x200,即512个字节
		ADD		AX,0x0020		; 对ES+20H或者对BX+512D,都是读取下一个扇区(20H = 512D/16)
		MOV		ES,AX			; 因为没有ADD ES,0x020指令,所以这里稍微绕个弯
		ADD		CL,1			; CL = CL + 1,扇区寄存器+1
		CMP		CL,18			; 比较CL和18
		JBE		readloop		; CL <= 18 跳转readloop(jump if below or equal)
  • JBE表示jump if below or equal,小于或等于则跳转。

本次是使用循环,每次读取一个扇区,其实如果将AL设置为17,似乎也能将第2~18共17个扇区读取到内存中,但是存在安全隐患。

4. harib00d例程

bash 复制代码
CYLS	EQU		10
(省略中间的一部分...)
; 读取10个柱面

		MOV		AX,0x0820
		MOV		ES,AX
		MOV		CH,0			; 柱面0
		MOV		DH,0			; 磁头0
		MOV		CL,2			; 扇区2
readloop:
		MOV		SI,0			; 记录失败次数的寄存器
retry:
		MOV		AH,0x02			; AH=0x02 : 读入磁盘
		MOV		AL,1			; 1个扇区
		MOV		BX,0
		MOV		DL,0x00			; A驱动器
		INT		0x13			; 调用BIOS的第0x13号程序
		JNC		next			; 0x13运行正常(进位标志位为0)跳转到next
		ADD		SI,1			; SI = SI+1
		CMP		SI,5			; 比较SI与5
		JAE		error			; if SI >= 5; 跳转到 error
		MOV		AH,0x00
		MOV		DL,0x00			; A驱动器
		INT		0x13			; 重置驱动器
		JMP		retry
next:
		MOV		AX,ES			; 把内存地址后移0x200
		ADD		AX,0x0020
		MOV		ES,AX			; 相当于 ADD ES,0x020 
		ADD		CL,1			; CL = CL + 1
		CMP		CL,18			; 比较CL与18
		JBE		readloop		; if CL <= 18;跳转到readloop
		MOV		CL,1			; if CL > 18; CL = 1
		ADD		DH,1			; DH = DH + 1
		CMP		DH,2			; 比较DH与2
		JB		readloop		; if DH < 2; 跳转到readloop
		MOV		DH,0			; if DH >= 2; DH = 0
		ADD		CH,1			; CH = CH + 1
		CMP		CH,CYLS			; 
		JB		readloop		; if CH < CYLS; 跳转到readloop
  • JB表示如果小于。
  • EQU用于定义一个变量,类似于C语言的#define,源文件首行的CYLS EQU 10就是表示声明CYLS(cylinder)为10。
    截至目前,已经可以把软盘最初的102 18*512=184 320Byte = 180KB内容读到内存了,将被加载在内存的0x08200 ~ 0x34fff地址区域。如果执行make stall就可以将程序安装到磁盘上。
    (184320 - 512) = 183808Byte = 0x2ce00(磁盘的首个512字节被加载到0x7c00了,所以以后的数据总共183808字节)
    0x08200 + 0x2ce00 = 0x35000(从0x8200地址开始加载183808字节内容其实也可以直接使用0x08000 + 0x2d000 = 0x35000计算)

5. harib00g例程

再次之前,一直做的是启动区ipl的开发。接下载才正式开始操作系统。

先以一个非抢占cpu的死循环作为一个基础文件:

bash 复制代码
# 文件命名为 haribote.nas
fin:
	HLT
	JMP fin

使用nask编译haribote.nas,输出haribote.sys。

将haribote.sys保存到(或集成到)haribote.img中,具体操作步骤可以理解为:

  • 将haribote.img挂载到某目录(或某文件夹)下;
  • 将haribote.sys放置在该目录(或该文件夹)中;
  • 卸载haribote.img。

这样就成功将haribote.sys保存到了haribote.img中。

在Makefile中:

shell 复制代码
# 节选
TOOLPATH = ../z_tools/
MAKE     = $(TOOLPATH)make.exe -r
EDIMG    = $(TOOLPATH)edimg.exe

default :
	$(MAKE) img

haribote.img : ipl.bin haribote.sys Makefile
	$(EDIMG)   imgin:../z_tools/fdimg0at.tek \
		wbinimg src:ipl.bin len:512 from:0 to:0 \
		copy from:haribote.sys to:@: \
		imgout:haribote.img

img :
	$(MAKE) haribote.img

制作一个完整的haribote.img,需要依赖ibl.bin和haribote.sys两个二进制文件。通过make img指令生成haribote.img。新做成的haribote.img与无haribote.sys的img比较可以看到:

在0x2600和0x4200地址处的差别。可见0x2600存放了haribote.sys的文件名,0x4200存放了haribote.sys的文件内容。

内存应该如何处理磁盘上haribote.sys的文件内容呢,haribote.sys才是操作系统的本体。0x4200 + 0x8000 = 0xc200,即haribote.sys应该位于内存的0xc200处。因此haribote.nas源文件的开头应该指定内存地址ORG 0xc200

但是当前的操作系统只有一个不抢占cpu的循环代码,因此为了验证操作系已经加载到指定位置,就在操作系统中添加一些基础的显示配置:

bash 复制代码
; haribote-os
; TAB=4

		ORG		0xc200			; 从内存0xc200开始,0x8000+0x4200 = 0xc200

		MOV		AL,0x13			; VGA图形模式320x200x8bit彩色
		MOV		AH,0x00
		INT		0x10			; 调用显卡BIOS
fin:
		HLT
		JMP		fin

设置显卡模式:

  • AH = 0x00;
  • AL = 模式
    • 0x03:16色字符模式,80*25
    • 0x12:VGA图形模式,6404804位彩色模式,独特的4面存储模式。
    • 0x13:VGA图形模式,3202008位彩色模式,调色板模式。
    • 0x6a:扩展VGA图形模式,8006004位彩色模式,,独特的4面存储模式。(有的显卡不支持)
  • 返回值:无

当前设置了AL为0x13,因此执行make imgmake run之后,虚拟机的画面应该会变为一片漆黑,光标也会消失。

5. harib00h例程

CPU的16位模式和32位模式中,机器语言的命令代码不同,同样的机器语言,解释方法也不同,所以16位模式的机器语言在32位模式下无法运行,反之亦然。

BIOS是16位机器语言写的,如果使用32位模式,就无法调用BIOS功能了。所以将期望调用BIOS的操作放在开头来做。

除了harib00g中配置的画面模式,还需要获取键盘状态,例如,NumLock等。

直接在haribote.nas中修改:

bash 复制代码
; haribote-os
; TAB=4

; BOOT_INFO
CYLS	EQU		0x0ff0			; 设定启动区
LEDS	EQU		0x0ff1
VMODE	EQU		0x0ff2			; 关于颜色的信息,颜色的位数
SCRNX	EQU		0x0ff4			; 分辨率X
SCRNY	EQU		0x0ff6			; 分辨率Y
VRAM	EQU		0x0ff8			; 图像缓冲区的开始地址

		ORG		0xc200			; 本文件会被加载到该内存地址

		MOV		AL,0x13			; VGA显卡 320x200x8bit颜色
		MOV		AH,0x00
		INT		0x10
		MOV		BYTE [VMODE],8	; 记录画面模式
		MOV		WORD [SCRNX],320
		MOV		WORD [SCRNY],200
		MOV		DWORD [VRAM],0x000a0000

; 用BIOS取得键盘上各种LED指示灯的状态
		MOV		AH,0x02
		INT		0x16 			; keyboard BIOS
		MOV		[LEDS],AL

fin:
		HLT
		JMP		fin

配置画面模式之后,把所配置的信息保存在内存中,以备后续切换画面模式时试用。

暂且将启动时的信息命名为BOOT_INFO。

VRAM\]表示的是显卡内存(video RAM),是一个用来显示画面的内存。它的各个地址对应画面的像素,可以利用这一机制在画面上绘制有颜色的图案。 VRAM实际上分布在内存的很多地方,这是由于不同的画面模式有不同的像素数,所以当画面模式不同时可能使用不同的VRAM地址。当前在INT 0x10画面模式下,VRAM是0xa0000 \~ 0xaffff这64KB。 #### 5. harib00i例程 > 开始使用c语言。 具体的逻辑后续会在c语言源文件中实现,因此将haribote.nas重命名为asmhead.nas,并添加用于调用c语言函数的代码。 ```bash # asmhead.nas ; haribote-os boot asm ; TAB=4 BOTPAK EQU 0x00280000 DSKCAC EQU 0x00100000 DSKCAC0 EQU 0x00008000 ; BOOT_INFO CYLS EQU 0x0ff0 LEDS EQU 0x0ff1 VMODE EQU 0x0ff2 SCRNX EQU 0x0ff4 SCRNY EQU 0x0ff6 VRAM EQU 0x0ff8 ORG 0xc200 ; �����[�h���� MOV AL,0x13 ; VGA�O���t�B�b�N�X�A320x200x8bit�J���[ MOV AH,0x00 INT 0x10 MOV BYTE [VMODE],8 ; �����[�h��������iC�����Q����j MOV WORD [SCRNX],320 MOV WORD [SCRNY],200 MOV DWORD [VRAM],0x000a0000 ; �L�[�{�[�h��LED����BIOS�������� MOV AH,0x02 INT 0x16 ; keyboard BIOS MOV [LEDS],AL ; PIC����������������t���������� ; AT���@��d�l���APIC������������A ; �����CLI�O����������A����n���O�A�b�v��� ; PIC�������������� MOV AL,0xff OUT 0x21,AL NOP ; OUT����A��������������@������������ OUT 0xa1,AL CLI ; ����CPU���x������������~ ; CPU���1MB�����������A�N�Z�X��������AA20GATE���� CALL waitkbdout MOV AL,0xd1 OUT 0x64,AL CALL waitkbdout MOV AL,0xdf ; enable A20 OUT 0x60,AL CALL waitkbdout ; �v���e�N�g���[�h��s [INSTRSET "i486p"] ; 486������g�������L�q LGDT [GDTR0] ; �b��GDT���� MOV EAX,CR0 AND EAX,0x7fffffff ; bit31��0����i�y�[�W���O��~����j OR EAX,0x00000001 ; bit0��1����i�v���e�N�g���[�h��s����j MOV CR0,EAX JMP pipelineflush pipelineflush: MOV AX,1*8 ; ��������\�Z�O�����g32bit MOV DS,AX MOV ES,AX MOV FS,AX MOV GS,AX MOV SS,AX ; bootpack��]�� MOV ESI,bootpack ; �]���� MOV EDI,BOTPAK ; �]���� MOV ECX,512*1024/4 CALL memcpy ; �����f�B�X�N�f�[�^��{�����u��]�� ; ����u�[�g�Z�N�^��� MOV ESI,0x7c00 ; �]���� MOV EDI,DSKCAC ; �]���� MOV ECX,512/4 CALL memcpy ; �c��S�� MOV ESI,DSKCAC0+512 ; �]���� MOV EDI,DSKCAC+512 ; �]���� MOV ECX,0 MOV CL,BYTE [CYLS] IMUL ECX,512*18*2/4 ; �V�����_�����o�C�g��/4���� SUB ECX,512/4 ; IPL������������ CALL memcpy ; asmhead����������������S����I������A ; �����bootpack��C��� ; bootpack��N�� MOV EBX,BOTPAK MOV ECX,[EBX+16] ADD ECX,3 ; ECX += 3; SHR ECX,2 ; ECX /= 4; JZ skip ; �]�������������� MOV ESI,[EBX+20] ; �]���� ADD ESI,EBX MOV EDI,[EBX+12] ; �]���� CALL memcpy skip: MOV ESP,[EBX+12] ; �X�^�b�N�����l JMP DWORD 2*8:0x0000001b waitkbdout: IN AL,0x64 AND AL,0x02 JNZ waitkbdout ; AND������0�������waitkbdout�� RET memcpy: MOV EAX,[ESI] ADD ESI,4 MOV [EDI],EAX ADD EDI,4 SUB ECX,1 JNZ memcpy ; �����Z�������0�������memcpy�� RET ; memcpy��A�h���X�T�C�Y�v���t�B�N�X�����Y�������A�X�g�����O���������� ALIGNB 16 GDT0: RESB 8 ; �k���Z���N�^ DW 0xffff,0x0000,0x9200,0x00cf ; ��������\�Z�O�����g32bit DW 0xffff,0x0000,0x9a28,0x0047 DW 0 GDTR0: DW 8*3-1 DD GDT0 ALIGNB 16 bootpack: ``` 一个基础的c语言文件bootpack.c,可惜c语言中没有类似HLT指令的函数操作: ```c void HariMain(void) { fin: goto fin; } ``` 后续的玩法是这样的: * 使用ccl.exe编译器从c源文件(bootpack.c或其他)生成bootpack.gas(汇编语言); * 使用gas2nask.exe从bootpack.gas生成bootpack.nas(翻译为nas文件); * 使用nask.exe从bootpack.nas生成bootpack.obj(目标文件); * 使用obj2bim.exe从bootpack.obj生成bootpack.bim(这一步做链接,生成binary image); * 使用bim2hrb.exe从bootpack.bim生成bootpack.hrb(对.bim文件制作纸娃娃格式); * 将bootpack.hrb和asmhead.bin合并在一起就成了haribote.sys。 单个的目标文件(.o或.obj)并不是完整的机器语言,需要把所有的目标文件链接起来。最终生成一个二进制文件映像(binary image)。所谓映像,可以这样理解:映像文件并不是文件本来的状态,而是一种代替形式。 所以实际上.bim文件也不是本来的形态,而是一个替代形式,并且不是完整品。它只是将所有的目标文件链接在一起组成了一个完整的机器语言文件,为了能实际使用,还需要针对不同操作系统的要求做必要的加工,例如用于识别的文件头,压缩等。 本次为了做成适合"纸娃娃"操作系统,专门使用bim2hrb.exe工具:输入.bim文件输出.hrb文件。 针对以上步骤Makefile文件做了很多修改: ```shell # 节选 TOOLPATH = ../z_tools/ INCPATH = ../z_tools/haribote/ MAKE = $(TOOLPATH)make.exe -r NASK = $(TOOLPATH)nask.exe CC1 = $(TOOLPATH)cc1.exe -I$(INCPATH) -Os -Wall -quiet GAS2NASK = $(TOOLPATH)gas2nask.exe -a OBJ2BIM = $(TOOLPATH)obj2bim.exe BIM2HRB = $(TOOLPATH)bim2hrb.exe RULEFILE = $(TOOLPATH)haribote/haribote.rul EDIMG = $(TOOLPATH)edimg.exe IMGTOL = $(TOOLPATH)imgtol.com COPY = copy DEL = del # �f�t�H���g���� default : $(MAKE) img # �t�@�C������K�� ipl10.bin : ipl10.nas Makefile $(NASK) ipl10.nas ipl10.bin ipl10.lst asmhead.bin : asmhead.nas Makefile $(NASK) asmhead.nas asmhead.bin asmhead.lst bootpack.gas : bootpack.c Makefile $(CC1) -o bootpack.gas bootpack.c bootpack.nas : bootpack.gas Makefile $(GAS2NASK) bootpack.gas bootpack.nas bootpack.obj : bootpack.nas Makefile $(NASK) bootpack.nas bootpack.obj bootpack.lst bootpack.bim : bootpack.obj Makefile $(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \ bootpack.obj # 3MB+64KB=3136KB bootpack.hrb : bootpack.bim Makefile $(BIM2HRB) bootpack.bim bootpack.hrb 0 haribote.sys : asmhead.bin bootpack.hrb Makefile copy /B asmhead.bin+bootpack.hrb haribote.sys haribote.img : ipl10.bin haribote.sys Makefile $(EDIMG) imgin:../z_tools/fdimg0at.tek \ wbinimg src:ipl10.bin len:512 from:0 to:0 \ copy from:haribote.sys to:@: \ imgout:haribote.img ``` 纸娃娃操作系统的主函数是HariMain,如果本例程执行后是黑屏的,表示是正常的。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/45900642aa094978a75ce8842979a57e.png) #### 5. harib00j例程 还是使用nask汇编语言写一个HLT的函数,文件命名为naskfunc.nas。 ```shell # naskfunc.nas ; naskfunc ; TAB=4 [FORMAT "WCOFF"] ; 制作目标文件的模式(不明白这个) [BITS 32] ; 制作32位模式用的机器语言 ; 制作目标文件的信息 [FILE "naskfunc.nas"] ; 原文件名信息 GLOBAL _io_hlt ; 程序中包含的函数名 ; 实际的函数 [SECTION .text] ; 目标文件中写了这些之后再写程序 _io_hlt: ; void io_hlt(void); HLT RET ``` 在CPU指令中,HLT属于I/O指令,MOV属于传送指令,ADD属于演算指令。所以这个函数起名了io_hlt,可以使用c语言`void io_hlt(void);`声明这个函数。 将输出格式定为WCOFF(有点不理解),并设定为32位机器语言模式。 在nask语言模式下,需要设定文件名信息,再写明函数的函数名,前面加下划线"_",目的是能够与C语言函数链接。需要链接的函数名使用"GLOBAL"指令声明。 在bootpack.c中调用io_hlt函数: ```shell # bootpack.c void io_hlt(void); // 声明函数 void HariMain(void) { fin: io_hlt(); /* 调用函数 */ goto fin; } ``` 当然运行起来仍然是一个黑屏。

相关推荐
march_birds32 分钟前
FreeRTOS 与 RT-Thread 事件组对比分析
c语言·单片机·算法·系统架构
小麦嵌入式1 小时前
Linux驱动开发实战(十一):GPIO子系统深度解析与RGB LED驱动实践
linux·c语言·驱动开发·stm32·嵌入式硬件·物联网·ubuntu
jelasin2 小时前
LibCoroutine开发手记:细粒度C语言协程库
c语言
篝火悟者2 小时前
自学-C语言-基础-数组、函数、指针、结构体和共同体、文件
c语言·开发语言
神里流~霜灭4 小时前
蓝桥备赛指南(12)· 省赛(构造or枚举)
c语言·数据结构·c++·算法·枚举·蓝桥·构造
双叶8364 小时前
(C语言)单链表(1.0)(单链表教程)(数据结构,指针)
c语言·开发语言·数据结构·算法·游戏
艾妮艾妮5 小时前
C语言常见3种排序
java·c语言·开发语言·c++·算法·c#·排序算法
charlie1145141915 小时前
STM32F103C8T6单片机硬核原理篇:讨论GPIO的基本原理篇章1——只讨论我们的GPIO简单输入和输出
c语言·stm32·单片机·嵌入式硬件·gpio·数据手册
矿渣渣5 小时前
int main(int argc, char **argv)C语言主函数参数解析
c语言·开发语言
阿让啊5 小时前
bootloader+APP中,有些APP引脚无法正常使用?
c语言·开发语言·stm32·单片机·嵌入式硬件