RT_Thread内核源码分析(一)——CM3内核和上下文切换

目录

一、程序存储分析

[1.1 CM3内核寻址空间映射](#1.1 CM3内核寻址空间映射)

[1.2 程序静态存储和动态执行](#1.2 程序静态存储和动态执行)

二、CM3内核相关知识

[2.1 操作模式和特权极别](#2.1 操作模式和特权极别)

[2.2 环境相关寄存器](#2.2 环境相关寄存器)

[2.2.1 通用寄存器组,](#2.2.1 通用寄存器组,)

[2.2.2 状态寄存器组](#2.2.2 状态寄存器组)

[2.2.3 模式切换环境自动保存](#2.2.3 模式切换环境自动保存)

[2.2.4 函数调用形参位置](#2.2.4 函数调用形参位置)

[2.3 中断](#2.3 中断)

[2.3.1 PendSV(可悬起软中断](#2.3.1 PendSV(可悬起软中断))

[2.3.2 SysTick(系统定时器),](#2.3.2 SysTick(系统定时器),)

三、RT_Thread线程结构

四、RT_Thread启动分析

五、RT_Thread上线文切换

[5.1 上下文切换本质](#5.1 上下文切换本质)

[5.2 上下文切换场景](#5.2 上下文切换场景)

[5.3 上下文切换源码实现](#5.3 上下文切换源码实现)

[5.3.1 启动第一个线程](#5.3.1 启动第一个线程)

[5.3.2 调度中上下文切换](#5.3.2 调度中上下文切换)

[5.3.3 上下文切换源码分析](#5.3.3 上下文切换源码分析)

[5.3.4 上下文切换逻辑图](#5.3.4 上下文切换逻辑图)

[5.4 上下文切换实例分析](#5.4 上下文切换实例分析)


对于实时操作系统来说,上下文切换是线程抢占或轮询的根本,要想吃透操作系统,必需先搞明白上下文切换的过程,若要搞明白上线文切换,必然少不了与CPU内核打交道,本文基于STM32F10X系列单片机( 内核为Cortex-M3核,简称CM3),讲解RT_Thread Nano实时操作系统的上下文切换。

本章基于RT_Thread Nano V3.1.5版本分析

本章基于Cortex-M3内核和Keil5编译器分析,详细内核知识参考《CM3权威指南CnR2 宋岩 译》

一、程序存储分析

1.1 CM3内核寻址空间映射

如下图所示:

对于STM32F103ZE型号的单片机,Code区采用容量为512kB总线接口的NorFlash;SRAM区为64kB的SRAM存储器,暂不考虑扩展SRAM。

1.2 程序静态存储和动态执行

使用keil5编译STM32程序后,程序存储分析如下图所示:

各存储区说明:

|---------|-------|-----|----------|--------------|--------------------------------------|
| 存储区 || 属性 | 存储位置 | 运行位置 | 存储内容 |
| CODE || 只读 | NorFlash | NorFlash | **代码区:**所有程序指令。 (起始为MSP初始化值+Reset向量) |
| RO-DATA || 只读 | NorFlash | NorFlash | **常量区:**const修饰的常量、字符串。 |
| RW-DATA || 读/写 | NorFlash | SRAM | **变量区:**初始化为非0的全局、静态变量。 |
| ZI-DATA | 变量区 | 读/写 | 无 | SRAM (运行中分配) | **变量区:**未初始化和初始化为0的全局、静态变量。 |
| ZI-DATA | heap | 读/写 | 无 | SRAM (运行中分配) | **堆区:**使用编译器微库,用于malloc申请动态内存。 |
| ZI-DATA | stack | 读/写 | 无 | SRAM (运行中分配) | **栈区:**特权级线程(Thread)、中断(异常)模式的局部变量。 |

(1)单片机内部FLASH启动模式下,ICode总线只能从NorFLASH取指令,代码段只能在NorFlash运行。

(2)总线接口的NorFlash可以用作只读内存,RO-DATA(只读数据)可以不搬移至内存。

(2)heap段使用需要启动keil的微库,才能通过malloc等操作进行动态内存申请,操作系统可以使用该方式分配总的动态内存区,但不要使用该方式分配任务栈,任务栈申请使用操作系统自带的方式。

(3)stack段默认给特权级线程、中断(异常)模式使用,由MSP寄存器控制出入栈,如果切换到用户模式,自动切换为进程栈指针寄存器(PSP),用户模式需要重新申请栈区,PSP指向用户栈区;操作系统中的动态内存区可以根据需求设置在RW-DATA段、ZI-DATA堆区、ZI-DATA变量区。

(4)ZI-DATA区:该类变量初始值全为0,故不体现在静态存储中,在运行态由指令进行内存分配,分配代码由Keil编译器自动生成,我们只需要在工程配置和启动文件中将存储器参数和堆栈申请空间配置好就行,如下所示:

Keil编译器存储器设置NorFLASH、SRAM:

启动文件(.s)中 主程序堆(heap)空间分配:

启动文件(.s)中 主程序栈(stack)空间分配:

编译结果 :

Map文件查看:

**二、**CM3内核相关知识

2.1 操作模式和特权极别

  1. CM3有2种操作模式: 处理者模式(或中断(异常)模式 handler mode )、线程(Thread mode)模式
  2. CM3有2种权利级别: 特权级、用户级,特权级使用MSP栈指针寄存器,用户级使用PSP栈指针寄存器。两种级别的栈相互独立。
  3. 处理者模式(handler mode)只能运行在特权级别。
  4. 用户级对系统控制空间(SCS)的访问将被阻止------该空间包含了配置寄存器组以及调试组件的寄存器组。还禁止使用 MRS/MSR 访问,除了 APSR 之外的特殊功能寄存器。如果以身试法,则对于访问特殊功能寄存器的,访问操作被忽略;而对于访问 SCS 空间的,将触发fault异常。
  5. 软件触发中断寄存器可以在用户级下访问以产生软件中断(利用这个特性实现用户模式到特权模式转变)。

各种模式之间的切换:

|------------------------------------|----------------------------------------------------|-----------|
| 模式切换 | 触发条件 | 栈指针 |
| 特权级Thread 模式 | 上电启动 | MSP |
| 特权级Thread 模式->特权级handler模式 | 中断、异常触发 R14(LD)更新为0XFFFFFFF9 | MSP |
| 特权级Thread 模式->用户级Thread 模式 | 操作寄存器CONTROL[0]置1 | MSP切换为PSP |
| 特权级handler模式->特权级Thread 模式 | 调用指令"BX R14" 其中R14=0XFFFFFFF9 | MSP |
| 特权级handler模式->用户级Thread 模式 | 调用指令"BX R14" 其中R14=0XFFFFFFFD | MSP切换为PSP |
| 用户级Thread 模式->特权级handler模式 | 中断、异常触发 R14更新为0XFFFFFFFD | PSP切换为MSP |
| 用户级Thread 模式->特权级Thread模式 | 主动触发异常(SVCall异常) 调用指令"BX R14"返回异常 其中R14=0XFFFFFFF9 | PSP切换为MSP |

2.2 环境相关寄存器

CM3用于上下文切换的寄存器主要包括通用寄存器组、状态寄存器组。

2.2.1 通用寄存器组,

如下图所示:

(1)通用寄存器

R0-R12 都是 32 位通用寄存器,用于数据操作、暂存。绝大多数 16 位 Thumb 指令只能访问 R0-R7,而 32 位 Thumb-2 指令可以访问所有寄存器。

(2)堆栈指针(SP)

R13用于栈指针(SP),指向栈区(stack),通过入栈出栈分配和释放临时变量。比如一个函数中定义了一些临时变量,调整SP指针(入栈)分配临时变量的存储空间,函数返回时,调整SP指针(出栈)释放掉堆栈空间,所以临时变量初始值是个不确定的值,因为出栈仅是调整SP指针,并不对栈空间进行归零等操作,且临时变量不宜过多,防止栈空间溢出。

CM3内核拥有两个堆栈指针:

主堆栈指针(MSP :特权级别下使用,复位后缺省使用的堆栈指针,初始值指向内存ZI-DATA栈区(Stack)的栈底(一般是高地址),代码区第一条指令就是MSP的初始化值,装置上电会先更新MSP,然后在执行reset。

进程堆栈指针(PSP :用户级别下使用,在操作系统中,指向任务的栈区,任务栈区一般从操作系统动态内存区分配,操作系统动态内存区可以根据可以根据需求设置在RW-DATA段、ZI-DATA堆区、ZI-DATA变量区。

(3)连接寄存器(LD)

R14用作连接寄存器(LD),连接寄存器当调用一个子程序时,R14存储指令返回地址,如果只有 1 级子程序调用的代码无需访问内存(堆栈内存),从而提高了子程序调用的效率。如果多于 1 级,则需要把前一级的 R14 值压到堆栈里。

程序进入handler模式(中断(异常))时,R14自动入栈保存,保存后R14更新为0xFFFFFFFX,通过指令"BX R14"进行异常返回。X的bit0为1表示返回thumb状态,bit1和bit2表示返回后sp用msp还是psp及返回到特权级别还是用户级别。合法的返回值如下所示:

|-------------|-----------------------------------------------|
| 0xFFFF_FFF1 | 返回handler模式**【应用于中断嵌套的场景】** |
| 0xFFFF_FFF9 | 返回线程模式,并使用主堆栈(SP=MSP)【返回特权级线程(Thread)模式】 |
| 0xFFFF_FFFD | 返回线程模式,并使用线程堆栈(SP=PSP)【返回用户级线程(Thread)模式】 |

(4) 程序计数寄存器(PC)

R15用作程序计数寄存器(PC),程序计数寄存器指向NorFLASH代码区。正常运行,取指令完成,PC自动加1,既PC指向下一条指令,如果执行跳转指令或者直接修改PC寄存器的值, 就能改变程序的执行流。

2.2.2 状态寄存器组

状态字寄存器组包括:应用程序 PSR(APSR)、 中断号 PSR(IPSR)、执行 PSR(EPSR),环境保存时是三个寄存器会合并为一个32位xPSR进行保存。

2.2.3 模式切换环境自动保存

与一些高端内核不同,CM3由线程模式(Thread)进入handler模式会自动进行部分寄存器的入栈保存,下图参考自《CM3权威指南CnR2 宋岩 译》:

2.2.4 函数调用形参位置

ARM系列平台,函数形参按从左向右顺序存放在寄存器r0,r1,r2,r3里,超过4个参数值传递则放栈里。

比如函数:

cpp 复制代码
void test(int iv1,int iv2,int iv3,int iv4,int iv5,int iv6);

形参:iv1、iv2、iv3、iv4分别存入寄存器r0、r1、r2、r3进行传递。

形参:iv5、iv6则入栈传递。

2.3 中断

RT_Thread操作系统调度器涉及CM3的2个中断,**PendSV用于上下文切换,SysTick提供时间片,**如下图:

2.3.1 PendSV(可悬起软中断)

该中断可以在高优先级中断(异常)中设置为悬起,等所有高优先级中断返回后,再执行PendSV,俗称"缓期执行",所以PendSV的优先级一般会设置为最低。

也可以在用户级或特权级线程模式调用该服务进入handler模式。

在操作系统中,用于上下文切换,悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。

2.3.2 SysTick(系统定时器),

可编程的定时中断(自检),为操作系统提供时间片,进行轮询和抢占式调度。设置方法可以参考《CM3权威指南CnR2 宋岩 译》。

三、RT_Thread线程结构

RT_Thread操作系统每个线程有独立的线程控制块(TCB)和线程栈,线程栈主要用于分配临时变量,在线程切换和初始化时,存储线程环境(通用寄存器+状态寄存器),寄存器排列顺序固定,如下图所示,可分为自动出入栈部分和手动出入栈部分,自动出入栈部分在CM3内核模式切换时由硬件自动实现(见2.2.3章节),手动出入栈部分则需要上下文切换代码实现。

四、RT_Thread启动分析

在分析操作系统上下文切换之前,先从宏观上分析操作系统启动和运行的过程。

(1)程序运行先进入特权级Thread模式,系统初始化完成后,取最高优先级线程作为第一个线程运行,执行启动第一个线程接口,触发PendSv中断,进入特权级Handler模式,寄存器LD强制为0XFFFFFFF9;执行上下文切换代码,将第一个线程中的环境(通用寄存器组+状态寄存器组)出栈,并通过执行BX 0XFFFFFFFD,跳转到用户级线程模式执行第一个线程的代码,此时使用的堆栈指针为PSP,指向第一个线程的线程栈。

(2)启动第一个线程接口同时会启动时间片中断(SysTick),为操作系统提供心跳和定时器周期轮询线程的功能,这样操作系统正常运行。

(3)由图中可以看出,操作系统正常运行后,只在用户级Thread模式和特权级Handler模式之间切换,如果不出现异常是不会在返回特权级Thread模式的。

五、RT_Thread上线文切换

5.1 上下文切换本质

RT_Thread操作系统进行上下文切换方法是挂起PendSv中断,在中断中将线程模式下的寄存器(通用寄存器+状态寄存器)入栈到当前线程栈,从另一线程(高优先级)的线程栈中出栈环境保存的寄存器数值,然后恢复到线程模式;本质是保存和切换寄器(通用寄存器+状态寄存器)数值。

5.2 上下文切换场景

RT_Thread操作系统支持抢占式调度,在线程运行过程中会涉及到上下文切换,其场景可分为2类:

被动切换: 当有更高优先级线程就绪时,调度器强制将当前线程寄存器状态入栈,将高优先级线程寄存器状态出栈,实现任务切换;此过程一般在定时器线程或SysTick中断中触发

**主动切换:**当前任务主动进入阻塞(vTaskDelay)、接收消息阻塞、接收信号阻塞、释放新线程等;会主动触发上下文切换。

5.3 上下文切换源码实现

上下文切换接口和相关变量如下所示:

cpp 复制代码
// 相关函数接口

/************【1】启动第一个线程********************************************/
void rt_hw_context_switch_to(rt_ubase_t to);
/************【2】线程中上线文切花*******************************************/
void rt_hw_context_switch(rt_ubase_t from, rt_ubase_t to);
/************【3】中断中上下文切换*******************************************/
void rt_hw_context_switch_interrupt(rt_ubase_t from, rt_ubase_t to);

// 相关变量
rt_uint32_t rt_interrupt_from_thread;         // 指向原线程栈顶指针
rt_uint32_t rt_interrupt_to_thread;           // 指向目的线程栈栈顶指针
rt_uint32_t rt_thread_switch_interrupt_flag;  // 上线文正在切换中标志

5.3.1 启动第一个线程

操作系统启动时会选择最高优先级的线程作为第一个线程启动,调用方式如下,形参为线程的栈顶指针地址,根据2.2.4章节 描述,CM3内核会自动使用R0寄存器传递形参(rt_uint32_t)&to_thread->sp。

cpp 复制代码
rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp);

第一个线程汇编启动接口如下所示,设置好目的线程rt_interrupt_to_thread和上下文切换标志rt_thread_switch_interrupt_flag后,触发PendSv中断,在PendSv中断中执行上下文切换。

cpp 复制代码
;函数声明
rt_hw_context_switch_to    PROC
EXPORT rt_hw_context_switch_to
 
   ;【1】目的线程rt_interrupt_to_thread指向启动线程栈指针,即(rt_uint32_t)&to_thread->sp
    LDR     r1, =rt_interrupt_to_thread
    STR     r0, [r1]
   ;【2】原线程rt_interrupt_from_thread指向空,设置为0
    LDR     r1, =rt_interrupt_from_thread               
    MOV     r0, #0x0
    STR     r0, [r1]
    ;【3】上下文切换标志rt_thread_switch_interrupt_flag 设置为1
    LDR     r1, =rt_thread_switch_interrupt_flag    
    MOV     r0, #1
    STR     r0, [r1]
    ;【4】设置中断PendSV和中断SysTick优先级
    LDR     r0, =NVIC_SYSPRI2 
    LDR     r1, =NVIC_PENDSV_PRI
    LDR.W   r2, [r0,#0x00]                            ; read
    ORR     r1,r1,r2                                  ; modify
    STR     r1, [r0]                                  ; write-back

    ;【5】挂起PendSv (上下文切换)
    LDR     r0, =NVIC_INT_CTRL                          
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]

    ;【6】恢复 MSP
    LDR     r0, =SCB_VTOR                                    
    LDR     r0, [r0]
    LDR     r0, [r0]
    MSR     msp, r0

   ;【6】在处理器级别启用中断,中断启用后,由于PendSv中断已经挂起,代码会跳转到PendsV中断执行
    CPSIE   F                                                        
    CPSIE   I

    ;【7】跳转PendsV中断后,系统在用户级Thread模式和特权级handler模式间切换,永远运行不到此处!
    ENDP                                                            

5.3.2 调度中上下文切换

操作系统调度正常运行后,时间片中断或线程可能会启动上下文切换,调用接口函数如下所示,根据2.2.4章节 描述,CM3内核会自动使用R0寄存器 传递形参(rt_uint32_t)&from_thread->sp,R1寄存器传递形参(rt_uint32_t)&to_thread->sp。

cpp 复制代码
 rt_hw_context_switch((rt_ubase_t)&from_thread->sp, (rt_ubase_t)&to_thread->sp);
 rt_hw_context_switch_interrupt((rt_ubase_t)&from_thread->sp, (rt_ubase_t)&to_thread->sp);

2种函数汇编代码虽然相同,但是内核模式不同,线程中执行接口是在用户级Thread模式,而时间片中断执行接口,是在特权级Handler模式。

执行接口之前均对高优先级中断进行了屏蔽,所以,以下汇编执行过程不会中断。

汇编启动接口如下所示, 设置好如下参数,

rt_uint32_t rt_interrupt_from_thread; // 原线程栈指针
rt_uint32_t rt_interrupt_to_thread; // 目的线程栈指针
rt_uint32_t rt_thread_switch_interrupt_flag; // 上线文正在切换中标志

触发PendSv中断,在PendSv中断中执行上下文切换。

cpp 复制代码
rt_hw_context_switch_interrupt
    EXPORT rt_hw_context_switch_interrupt
rt_hw_context_switch    PROC
    EXPORT rt_hw_context_switch

    ;【1】上下文切换标志rt_thread_switch_interrupt_flag判断
    LDR     r2, =rt_thread_switch_interrupt_flag
    LDR     r3, [r2]
    ;【1.1】上下文切换标志rt_thread_switch_interrupt_flag为1,跳至_reswitch 
    CMP     r3, #1
    BEQ     _reswitch                                     
    ;【1.2】上下文切换标志rt_thread_switch_interrupt_flag不为1,赋值为1
    MOV     r3, #1
    STR     r3, [r2]                                             
    ;【2】rt_interrupt_from_thread 指向源线程栈指针
    LDR     r2, =rt_interrupt_from_thread
    STR     r0, [r2]
_reswitch
    ;【3】rt_interrupt_to_thread指向目标线程栈指针
    LDR     r2, =rt_interrupt_to_thread 
    STR     r1, [r2]
    ;【4】触发PendSv异常(进行上下文切换)
    LDR     r0, =NVIC_INT_CTRL 
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]
    ;【5】跳出
    BX      LR
    ENDP

5.3.3 上下文切换源码分析

5.3.1章节5.3.2章节可见,不管是启动第一个线程还是系统运行中线程切换,最终执行的位置是PendSv中断,并且在执行中断前,已经通过全局变量传输参数。

rt_uint32_t rt_interrupt_from_thread; // 原线程栈指针
rt_uint32_t rt_interrupt_to_thread; // 目的线程栈指针
rt_uint32_t rt_thread_switch_interrupt_flag; // 上线文正在切换中标志

如果是启动第一个线程,CM3内核会从特权级Thread模式进入特权级Handler模式,R14数值更新为0XFFFFFFF9,执行完上下文切换后, R14数值更新为0XFFFFFFFD,通过指令BX 0xFFFFFFFD返回到用户级Thread模式。

如果,操作系统调度启动后,线程中进行上下文切换,会先屏蔽中断,再挂起PendSv,等恢复中断后,PendSv才能响应,CM3内核会从特权级Thread模式进入特权级Handler模式,R14数值更新为0XFFFFFFFD;寄存器R0、R1、R2、R3、R12、R14(LR)、R15(PC)、XPSR

自动通过指针PSP入栈,堆栈指针切换为MSP。

如果,时间片执行上下文切换,等时间片中断完全执行完成后,才能响应PendSv中断。

上下文切换汇编源码分析如下:

cpp 复制代码
PendSV_Handler   PROC
EXPORT PendSV_Handler
    ; 【0.1】如果用户级线程模式进入中断,R0、R1、R2、R3、R12、R14(LR)、R15(PC)、XPSR
    ;  自动通过指针PSP入栈,堆栈指针切换为MSP,
    ; 【1】屏蔽除NMI和Fault外的中断
    MRS     r2, PRIMASK      ;中断屏蔽寄存器PRIMASK暂存,用于恢复
    CPSID   I                ;屏蔽除NMI和Fault中断
    ; 【2】判断上下文切换标志标志rt_thread_switch_interrupt_flag
    LDR     r0, =rt_thread_switch_interrupt_flag  
    LDR     r1, [r0]            
    ; 【2.1】上下文切换未进行,正常切换,否则执行pendsv_exit,退出切换                                              
    CBZ     r1, pendsv_exit                                            
    ; 【3】清除上下文切换标志标志rt_thread_switch_interrupt_flag
    MOV     r1, #0x00 
    STR     r1, [r0] 
    ; 【4】判断原线程rt_interrupt_from_thread,
    LDR     r0, =rt_interrupt_from_thread                      			 
    LDR     r1, [r0]
    ; 【4.1】原线程rt_interrupt_from_thread无效,跳至switch_to_thread,直接恢复目的线程
    CBZ     r1, switch_to_thread 
    ; 【4.2】原线程rt_interrupt_from_thread有效,对原线程进行环境保存
    MRS     r1, psp            ;R1用作原线程栈指针
    STMFD   r1!, {r4 - r11}    ;向原线程栈入栈R4--R11,同时调整R1
    LDR     r0, [r0]           ;取原线程控制块栈指针*sp,即rt_interrupt_from_thread
    STR     r1, [r0]           ;sp更新为最新的栈指针,即完成入栈操作的R1
    ;【5】对目的线程进行环境恢复
switch_to_thread
    LDR     r1, =rt_interrupt_to_thread
    LDR     r1, [r1]
    LDR     r1, [r1]            ; R1用作目的线程栈指针
    LDMFD   r1!, {r4 - r11}     ; 从目的线程栈出栈 R4-R11,同时调整R1
    MSR     psp, r1             ; 线程模式堆栈指针psp更换为目的线程栈指针,即完成出栈操作的R1
    ;【6】完成手动部分,恢复屏蔽,进行自动出栈
pendsv_exit
    MSR     PRIMASK, r2         ; 恢复屏蔽寄存器
    ORR     lr, lr, #0x04       ; 出栈默认寄存器
    BX      lr                  ; 执行BX 0xFFFFFFFD,返回用户级线程模式,根据堆栈指针psp,
                                ; 自动出栈R0、R1、R2、R3、R12、R14(LR)、R15(PC)、XPSR
    ENDP

5.3.4 上下文切换逻辑图

5.4 上下文切换实例分析

假设线程A由运行态切换到了阻塞态,而线程B是当前最高优先级的就绪态任务,线程A主动启动PendSv中断,上下文切换过程如下所示:

由上图可见,对比切换前和切换后,线程A和线程B的栈状态正好相反,线程A进行了环境保存,线程B进行了环境恢复。

相关推荐
m0_7393128730 分钟前
【STM32】项目实战——OV7725/OV2604摄像头颜色识别检测(开源)
stm32·单片机·嵌入式硬件
嵌入式小章40 分钟前
基于STM32的实时时钟(RTC)教学
stm32·嵌入式硬件·实时音视频
徐嵌2 小时前
STM32项目---水质水位检测
stm32·单片机·嵌入式硬件
徐嵌2 小时前
STM32项目---畜牧定位器
c语言·stm32·单片机·物联网·iot
stm32发烧友3 小时前
基于STM32的智能家居环境监测系统设计
stm32·嵌入式硬件·智能家居
hairenjing112310 小时前
使用 Mac 数据恢复从 iPhoto 图库中恢复照片
windows·stm32·嵌入式硬件·macos·word
u01015265814 小时前
STM32F103C8T6学习笔记2--LED流水灯与蜂鸣器
笔记·stm32·学习
北京迅为18 小时前
【北京迅为】《STM32MP157开发板嵌入式开发指南》-第七十八章 Qt控制硬件
linux·stm32·单片机·嵌入式硬件
llhm1 天前
stm32 踩坑笔记
stm32·单片机·嵌入式硬件
charlie1145141911 天前
从0开始的STM32之旅8 串口通信(II)
stm32·单片机·嵌入式硬件·c·串口通信