|
本文提出一个面向嵌入式环境的跨平台抽象库(unisym),目标是让同一套驱动与系统代码能够分别运行于多种微控制器与更高性能的片上系统之上,而无需为每种目标重写驱动逻辑。 框架在接口层保持平台无关,平台相关的差异被收束到实现内部,调用方所见是一个稳定、一致的抽象接口;更换目标平台时通常只需做少量调整,业务逻辑本身无需改动。寄存器访问被封装为一层统一的代理机制,保留了对底层资源的直接操作语义,同时借助类型检查与一致的读写方式,降低手工位的直接拼凑可能引入的错误。在接口设计上,本工作不追求对各家 HAL 的逐一对齐,而是倾向于收敛:多阶段、多命名习惯的操作被合并为单一方法配合少量参数的形式,超时、中断与回调等通用关注点采用统一约定,从而在保持表达能力的同时缩小调用面,使 API 意图更易读、更易维护。 在体系结构层面,unisym 并不只覆盖外设驱动,而是将上位的外设驱动与系统级支撑纳入同一抽象模型,包括时钟管理、缓存与存储管理、内存管理,以及终端、图层与字体渲染等图形界面能力。这使得从最底层的一个外设操作,到构建具备图形界面的完整应用,始终处于同一套接口与心智模型之下,避免在不同生态的碎片接口之间反复拼接。 在代码组织上,该框架强调跨平台差异的清晰界定与内聚封装 —— 平台相关的部分被集中管理,平台无关的接口保持稳定,使新增平台或新增外设时对既有代码的扰动降到最低。代码同时遵循一致的命名、缩进与文件约定,寄存器访问、超时与回调机制都依据统一规范,而非各外设各自为政,从而降低审阅与迁移成本,也让跨平台比对的结构更加清晰。 例程地址:https://github.com/dosconio/alicewiki/tree/main/tutorial/mcudev/demo Unisym 外设接口与使用教程GPIOGPIO 用来控制 MCU 的普通输入输出引脚。通过 Unisym,可以用统一的 C++ 接口完成常见操作,例如点亮 LED、读取按键、配置上拉下拉、开启外部中断、配置复用功能等。 最常见的写法是先取得一个引脚对象:
1. 公开接口速查1.1
|
| 方法 | 作用 | 参数 | 返回值 |
|---|---|---|---|
operator=(bool val) |
设置引脚输出电平。 | val 为 true 时输出高电平,为 false 时输出低电平。 |
返回当前引脚引用,便于链式或连续赋值。 |
operator=(GPIN pin) |
把另一个引脚当前读到的电平写到本引脚。 | pin 是被读取的源引脚。 |
返回当前引脚引用。 |
operator bool() |
读取引脚输入电平。 | 无。 | 高电平返回true,低电平返回 false。 |
setMode(GPIOMode mod, GPIOSpeed spd = defa_speed, bool autoEnClk = true) |
配置 GPIO 工作模式和输出速度。 | mod 是输入、输出或复用模式;spd 是输出速度;autoEnClk 为 true 时自动打开端口时钟。 |
返回当前引脚引用。 |
canMode() |
复位引脚到默认状态(对标 HAL 的HAL_GPIO_DeInit)。 |
无。 | 复位成功返回true,失败返回 false。 |
setPull(bool pull_up) |
配置输入上下拉。 | pull_up 为 true 时上拉,为 false 时下拉。 |
无返回值。 |
Toggle() |
翻转当前输出电平。 | 无。 | 无返回值。 |
Lock(bool tolock_orunlock = true) |
锁定 GPIO 配置,防止后续误改。 | 通常不传参数;STM32 上锁定后不能软件解锁,false 不能解除硬件锁。 |
无返回值。 |
isInput() |
判断当前引脚是否处于输入类模式。 | 无。 | 是输入类模式返回true,否则返回 false。 |
setInterrupt(Handler_t f) |
设置该 EXTI line 的中断回调函数。 | f 是中断处理函数。 |
无返回值。 |
setInterruptPriority(byte preempt, byte sub_priority) |
设置该引脚对应外部中断的优先级。 | preempt 是抢占优先级;sub_priority 是子优先级。 |
无返回值。 |
enInterrupt(bool enable = true) |
使能或关闭该引脚对应的外部中断。 | enable 为 true 时使能,为 false 时关闭。 |
无返回值。 |
setMode(GPIORupt::RuptEdge edg, Handler_t f = 0) |
把引脚配置为 EXTI 中断触发源。 | edg 是触发边沿;f 是可选回调函数。 |
返回当前引脚引用。 |
setMode(GPIOEvent::EventEdge edg) |
把引脚配置为 EXTI 事件触发源。 | edg 是事件触发边沿。 |
返回当前引脚引用。 |
_set_alternate(byte selection) |
设置复用功能 AF 编号。 | selection 是 AF 编号。 |
设置成功返回true,不支持或参数无效返回 false。 |
_set_alternate() 只适用于 STM32F4、STM32H7、STM32MP13 这类带 AFR 寄存器的目标。STM32F1 的复用重映射不使用这个接口。
GPORT 端口对象GPORT 表示一个 GPIO 端口,例如 GPIOA、GPIOB、GPIOC。
| 方法 | 作用 | 参数 | 返回值 |
|---|---|---|---|
getID() |
获取端口编号。 | 无。 | 返回端口编号,例如 A 口对应 0,B 口对应 1。 |
enClock(bool enable = true) |
打开或关闭该 GPIO 端口时钟。 | enable 为 true 时打开,为 false 时关闭。 |
无返回值。 |
operator stduint() |
读取整个端口输入值。 | 无。 | 返回端口输入寄存器快照。 |
operator[](int pinid) |
取得端口中的某个引脚。 | pinid 是 0 到 15 的引脚编号。 |
返回对应GPIN 引脚引用。 |
operator=(stduint val) |
写整个端口输出值。 | val 是要写入端口的输出值。 |
返回当前端口引用。 |
operator=(GPORT pot) |
把另一个端口当前读到的值写到本端口。 | pot 是被读取的源端口。 |
返回当前端口引用。 |
operator[](GPIOReg reg) |
访问 GPIO 端口寄存器。 | reg 是寄存器枚举值。 |
返回寄存器引用,可读写。 |
整端口写入会同时影响该端口上的多个引脚。只控制单个引脚时,优先使用 GPIOx[n] = true、GPIOx[n] = false 或 GPIOx[n].Toggle()。
以下函数是自由函数(非成员函数),仅限 STM32 平台,用于操作 EXTI pending 标志和软件触发。
| 函数 | 作用 | 参数 | 返回值 | 适用平台 |
|---|---|---|---|---|
EXTI_GetPending(pin) |
查询 EXTI line 是否有 pending 标志。 | pin 是 GPIN 引脚。 |
有 pending 返回true,否则 false。 |
STM32 全系列 |
EXTI_ClearPending(pin) |
清除 EXTI line 的 pending 标志。 | pin 是 GPIN 引脚。 |
无返回值。 | STM32 全系列 |
EXTI_SoftwareTrigger(pin) |
对 EXTI line 产生软件中断触发。 | pin 是 GPIN 引脚。 |
无返回值。 | STM32 全系列 |
EXTI_GetPendingRising(pin) |
查询上升沿 pending 标志。 | pin 是 GPIN 引脚。 |
有 pending 返回true,否则 false。 |
STM32MP13 |
EXTI_GetPendingFalling(pin) |
查询下降沿 pending 标志。 | pin 是 GPIN 引脚。 |
有 pending 返回true,否则 false。 |
STM32MP13 |
EXTI_ClearPendingRising(pin) |
清除上升沿 pending 标志。 | pin 是 GPIN 引脚。 |
无返回值。 | STM32MP13 |
EXTI_ClearPendingFalling(pin) |
清除下降沿 pending 标志。 | pin 是 GPIN 引脚。 |
无返回值。 | STM32MP13 |
各平台 EXTI pending 寄存器差异:
| 平台 | Pending 寄存器 | 特点 |
|---|---|---|
| STM32F1 | PR |
统一 pending,写 1 清除(w1c) |
| STM32F4 | PR |
统一 pending,写 1 清除(w1c) |
| STM32H7 | PR1/PR2/PR3(分 Core) |
D1 PR1 用于 GPIO(pin 0-15) |
| STM32MP13 | RPR1/FPR1(分上升/下降沿) |
上升沿和下降沿各自独立 pending |
GPIO 全局入口GPIO 是全局 GPIO 入口对象,用来按端口名取得端口对象。
| 方法 | 作用 | 参数 | 返回值 |
|---|---|---|---|
operator[](unsigned portid) |
取得指定 GPIO 端口。 | 可以传字符形式的端口名,例如'A'、'B'、'C'。 |
返回对应GPORT 端口引用。 |
常用端口已经有宏定义,通常直接写 GPIOA、GPIOB、GPIOC 即可:
GPIN& pa0 = GPIOA[0];
GPIN& pc13 = GPIOC[13];
#include <cpp/MCU/ST/STM32F1>
using namespace uni;
void led_init()
{
GPIN& LED = GPIOC[13];
LED.setMode(GPIOMode::OUT_PushPull, GPIOSpeed::Atmost_2MHz);
}
void led_on()
{
GPIOC[13] = true;
}
void led_off()
{
GPIOC[13] = false;
}
void led_toggle()
{
GPIOC[13].Toggle();
}
如果开发板上的 LED 是低电平点亮,那么 true 和 false 对应的亮灭关系会相反,按实际电路调整即可。
#include <cpp/MCU/ST/STM32F1>
using namespace uni;
void key_init()
{
GPIN& KEY = GPIOA[0];
KEY.setMode(GPIOMode::IN_Pull);
KEY.setPull(true);
}
bool key_pressed()
{
return GPIOA[0] == false;
}
setPull(true) 表示上拉,setPull(false) 表示下拉。上拉输入时,常见的低电平按键电路会在按下时读到 false。
如果外部硬件已经提供了上下拉,也可以使用浮空输入:
GPIOA[0].setMode(GPIOMode::IN_Floating);
按键、传感器告警脚等信号可以接到 EXTI 外部中断。
#include <cpp/MCU/ST/STM32F1>
using namespace uni;
static volatile bool key_event;
void on_key_down()
{
key_event = true;
}
void key_irq_init()
{
GPIN& KEY = GPIOA[0];
KEY.setMode(GPIOMode::IN_Pull);
KEY.setPull(true);
KEY.setMode(GPIORupt::Negedge, on_key_down);
KEY.setInterruptPriority(2, 0);
KEY.enInterrupt();
}
void main_loop()
{
if (key_event) {
key_event = false;
// 在这里处理按键事件
}
}
外部中断支持三种边沿:
GPIORupt::Posedge // 上升沿触发
GPIORupt::Negedge // 下降沿触发
GPIORupt::Anyedge // 双边沿触发
中断回调函数应该尽量短,只做置位标志、记录状态等简单工作。耗时处理建议放到主循环或任务里完成。
如果只希望 GPIO 触发事件,而不是进入中断,可以使用事件接口:
#include <cpp/MCU/ST/STM32F1>
using namespace uni;
void key_event_init()
{
GPIN& KEY = GPIOA[0];
KEY.setMode(GPIOMode::IN_Pull);
KEY.setPull(true);
KEY.setMode(GPIOEvent::Negedge);
}
事件不会调用中断回调,也不会自动打开 NVIC。它适合配合 WFE、低功耗等待或其它事件唤醒机制使用。
事件也支持三种边沿:
GPIOEvent::Posedge
GPIOEvent::Negedge
GPIOEvent::Anyedge
STM32F1 有一个容易和 EXTI 事件混淆的功能:Cortex-M3 EVENTOUT 信号。
EXTI 事件是外部引脚边沿 → 内核(输入方向);而 EVENTOUT 是反方向——内核 → 外部引脚(输出方向)。CPU 执行 SEV 指令时,Cortex-M3 内核产生一个 EVENTOUT 脉冲,经过 AFIO 的 EVCR 寄存器路由到指定的 GPIO 引脚上输出。
这是 STM32F1 独有的功能——F4/H7/MP13 的 SYSCFG 没有 EVCR 寄存器。
| 函数 | 作用 |
|---|---|
EVENTOUT_Config(pin) |
指定 EVENTOUT 信号输出到哪个引脚 |
EVENTOUT_Enable() |
使能 EVENTOUT 输出 |
EVENTOUT_Disable() |
关闭 EVENTOUT 输出 |
#include <cpp/MCU/ST/STM32F1>
using namespace uni;
void eventout_demo()
{
// 将 Cortex EVENTOUT 信号路由到 PC13
EVENTOUT_Config(GPIOC[13]);
EVENTOUT_Enable();
// 之后执行 SEV 指令会在 PC13 上产生一个脉冲
__SEV();
// 不再需要时关闭
EVENTOUT_Disable();
}
SEV 指令的执行时序EXTI 事件(GPIOEvent) |
EVENTOUT | |
|---|---|---|
| 方向 | 引脚 → 内核 | 内核 → 引脚 |
| 触发源 | 外部引脚边沿 | CPU 执行SEV 指令 |
| 等待方式 | WFE 等待外部事件 |
输出脉冲给外部设备 |
| 适用平台 | STM32 全系列 | 仅 STM32F1 |
STM32F1 没有 AFR 寄存器,外设引脚重映射通过 AFIO_MAPR / AFIO_MAPR2 寄存器完成。Unisym 提供 AFIORemap 枚举 + AFIO_Remap() 函数,替代 HAL 的 76 个 __HAL_AFIO_REMAP_* 宏。
// 单 bit 重映射:第二参数 bool
void AFIO_Remap(AFIORemap remap, bool enable);
// 多 bit 字段重映射:第二参数 byte → 使用 AFIO_RemapConfig
void AFIO_RemapConfig(AFIORemap remap, byte value);
单 bit(用 bool 版本):
| 枚举值 | 寄存器 | 位 | 说明 |
|---|---|---|---|
SPI1 |
MAPR | 0 | SPI1 重映射 |
I2C1 |
MAPR | 1 | I2C1 重映射 |
USART1 |
MAPR | 2 | USART1 重映射 |
USART2 |
MAPR | 3 | USART2 重映射 |
TIM4 |
MAPR | 12 | TIM4 重映射 |
PD01 |
MAPR | 16 | PD0/PD1 重映射 |
TIM5CH4 |
MAPR | 17 | TIM5_CH4 重映射 |
SPI3 |
MAPR | 28 | SPI3 重映射(互联型) |
ETH |
MAPR | 21 | 以太网重映射(互联型) |
CAN2 |
MAPR | 22 | CAN2 重映射(互联型) |
MII_RMII |
MAPR | 23 | MII/RMII 选择(互联型) |
ADC1_ETRGINJ |
MAPR | 18 | ADC1 外部触发注入 |
ADC1_ETRGREG |
MAPR | 19 | ADC1 外部触发规则 |
ADC2_ETRGINJ |
MAPR | 20 | ADC2 外部触发注入 |
ADC2_ETRGREG |
MAPR | 21 | ADC2 外部触发规则 |
TIM2ITR1 |
MAPR | 29 | TIM2 ITR1 → USB/ETH(互联型) |
PTP_PPS |
MAPR | 30 | 以太网 PTP_PPS(互联型) |
TIM9 |
MAPR2 | 5 | TIM9 重映射 |
TIM10 |
MAPR2 | 6 | TIM10 重映射 |
TIM11 |
MAPR2 | 7 | TIM11 重映射 |
TIM12 |
MAPR2 | 12 | TIM12 重映射 |
TIM13 |
MAPR2 | 8 | TIM13 重映射 |
TIM14 |
MAPR2 | 9 | TIM14 重映射 |
TIM15 |
MAPR2 | 0 | TIM15 重映射 |
TIM16 |
MAPR2 | 1 | TIM16 重映射 |
TIM17 |
MAPR2 | 2 | TIM17 重映射 |
CEC |
MAPR2 | 3 | CEC 重映射 |
TIM1DMA |
MAPR2 | 4 | TIM1 DMA 重映射 |
TIM67DACDMA |
MAPR2 | 11 | TIM6/7 + DAC DMA 重映射 |
FSMC_NADV |
MAPR2 | 10 | FSMC NADV 断开 |
MISC |
MAPR2 | 13 | 杂项重映射 |
多 bit 字段(用 byte 版本):
| 枚举值 | 寄存器 | 位域 | 值 |
|---|---|---|---|
USART3 |
MAPR | 5:4 | 0=NoRemap, 1=Partial, 2=Full |
TIM1 |
MAPR | 7:6 | 0=NoRemap, 1=Partial, 3=Full |
TIM2 |
MAPR | 9:8 | 0=NoRemap, 1=Partial1, 2=Partial2, 3=Full |
TIM3 |
MAPR | 11:10 | 0=NoRemap, 2=Partial, 3=Full |
CAN1 |
MAPR | 14:13 | 0=PA11, 2=PB8, 3=PD0 |
SWJ |
MAPR | 26:24 | 0=Full, 1=NoJNTRST, 2=NoJTAG, 4=Disable |
#include <cpp/MCU/ST/STM32F1>
using namespace uni;
void remap_demo()
{
// 单 bit:使能 SPI1 重映射
AFIO_Remap(AFIORemap::SPI1, true);
// 多 bit:TIM3 完全重映射到 PC6/7/8/9
AFIO_RemapConfig(AFIORemap::TIM3, 3); // 3=Full
// CAN1 重映射到 PB8/PB9
AFIO_RemapConfig(AFIORemap::CAN1, 2); // 2=Remap2(PB8)
// SWJ:禁用 JTAG,保留 SWD
AFIO_RemapConfig(AFIORemap::SWJ, 2); // 2=NoJTAG
// 取消 SPI1 重映射
AFIO_Remap(AFIORemap::SPI1, false);
// TrustZone:将 PB7 设为 Secure(仅 MP13)
GPIO_SecurePin(GPIOB[7]);
if (GPIO_IsPinSecured(GPIOB[7])) {
// Secure 引脚,NonSecure 代码无法访问
}
GPIO_NonSecurePin(GPIOB[7]); // 恢复为 NonSecure
// 原子多引脚写入:同时 set PA5/PA7, reset PA1/PA3
GPIOA.maset(0x00A0, 0x000A); // setMask=bit5,7 resetMask=bit1,3
}
ADC2_ETRGREG 和 ETH 占同一位(bit 21),不同子型号含义不同,不会同时使用SWJ 的值不是连续的:0/1/2/4(没有 3),因为 bit 26 独立控制全禁用FSMC_NADV 使能=true 表示断开 NADV(反直觉),与 HAL 宏 FSMCNADV_DISCONNECTED 对应USART、SPI、I2C、TIM、SDIO 等外设通常需要把 GPIO 配置成复用功能。
在 STM32F4、STM32H7、STM32MP13 上,除了设置 GPIO 模式,还要设置 AF 编号。例如 PA9 配置为 USART1_TX:
#include <cpp/MCU/ST/STM32F4>
using namespace uni;
void usart1_tx_pin_init()
{
GPIOA[9].setMode(GPIOMode::OUT_AF_PushPull, GPIOSpeed::High)
._set_alternate(7);
}
AF 编号请以具体芯片的数据手册为准。
STM32F1 没有 AFR 寄存器,普通默认复用只需要设置 GPIO 模式:
#include <cpp/MCU/ST/STM32F1>
using namespace uni;
void usart1_tx_pin_init()
{
GPIOA[9].setMode(GPIOMode::OUT_AF_PushPull, GPIOSpeed::Atmost_50MHz);
}
如果 STM32F1 需要外设重映射,需要另外配置 AFIO remap。
GPIO lock 可以锁定引脚配置,防止后续代码误改 GPIO 模式。
#include <cpp/MCU/ST/STM32F4>
using namespace uni;
void led_lock_init()
{
GPIOA[5].setMode(GPIOMode::OUT_PushPull, GPIOSpeed::High);
GPIOA[5].Lock();
}
STM32 的 GPIO lock 是硬件单向锁。锁定后软件不能解锁,只能等下一次 reset 后解除。因此它适合在板级初始化完成后使用,不适合调试阶段反复调用。
canMode() 用于将 GPIO 引脚复位到默认状态,对标 HAL 库的 HAL_GPIO_DeInit()。
调用 canMode() 后,引脚会被恢复到复位后的默认状态:
| 平台 | 复位后状态 |
|---|---|
| STM32F1 | 浮空输入,ODR 清零,EXTI 映射清除 |
| STM32F4 | 输入模式,AFR/PUPDR/OTYPER/OSPEEDR 清零,EXTI 映射清除 |
| STM32H7/MP13 | 模拟模式,AFR/PUPDR/OTYPER/OSPEEDR 清零,EXTI 映射清除 |
| CW32F030 | 输入模式,OPENDRAIN/SPEED/ANALOG 清零 |
| MSP432P4 | 输入方向 |
对于已配置为 EXTI 中断/事件源的引脚,canMode() 会同时清除 EXTI 映射、中断屏蔽、事件屏蔽和边沿触发配置。
#include <cpp/MCU/ST/STM32F4>
using namespace uni;
void pin_reset_demo()
{
// 先配置引脚
GPIOA[5].setMode(GPIOMode::OUT_PushPull, GPIOSpeed::High);
GPIOA[5] = true;
// 复位引脚到默认状态
if (GPIOA[5].canMode()) {
// 复位成功,引脚现在是输入模式
// 可以重新配置为其他用途
}
}
动态切换外设功能:
void switch_to_uart()
{
// 先复位引脚
GPIOA[9].canMode();
GPIOA[10].canMode();
// 重新配置为 USART1
GPIOA[9].setMode(GPIOMode::OUT_AF_PushPull, GPIOSpeed::High)
._set_alternate(7);
GPIOA[10].setMode(GPIOMode::IN_Pull)._set_alternate(7);
}
void switch_to_timer()
{
// 先复位引脚
GPIOA[9].canMode();
// 重新配置为 TIM1_CH2
GPIOA[9].setMode(GPIOMode::OUT_AF_PushPull, GPIOSpeed::High)
._set_alternate(1);
}
清除 EXTI 配置:
void disable_key_irq()
{
// canMode() 会自动清除 EXTI 映射和中断配置
GPIOA[0].canMode();
}
canMode() 是单向操作,无法恢复之前的配置。调用前应确认是否需要保存当前状态。Lock() 调用后),canMode() 可能无法生效。除了单个 pin,也可以操作整个 GPIO 端口:
#include <cpp/MCU/ST/STM32F4>
using namespace uni;
void port_demo()
{
GPIOB.enClock();
GPIOB = 0x00FF;
stduint input = GPIOB;
(void)input;
}
整端口写入会影响该端口上的所有输出引脚。如果只想控制单个引脚,优先使用 GPIOx[n] = true、GPIOx[n] = false 或 GPIOx[n].Toggle()。
GPIOA[5].setMode(GPIOMode::OUT_PushPull);
GPIOA[5].setMode(GPIOMode::OUT_PushPull, GPIOSpeed::High);
GPIOA[5] = true;
GPIOA[5] = false;
GPIOA[5].Toggle();
GPIOA[0].setMode(GPIOMode::IN_Pull);
GPIOA[0].setPull(true);
bool level = GPIOA[0];
GPIOA[0].setMode(GPIORupt::Negedge, on_key_down);
GPIOA[0].setInterruptPriority(2, 0);
GPIOA[0].enInterrupt();
GPIOA[0].setMode(GPIOEvent::Negedge);
GPIOA[9].setMode(GPIOMode::OUT_AF_PushPull, GPIOSpeed::High)
._set_alternate(7);
GPIOA[5].Lock();
GPIOA[5].canMode();
// EXTI 辅助函数
EXTI_GetPending(GPIOA[0]);
EXTI_ClearPending(GPIOA[0]);
EXTI_SoftwareTrigger(GPIOA[0]);
// STM32MP13 独有:分边沿 pending
EXTI_GetPendingRising(GPIOA[0]);
EXTI_ClearPendingFalling(GPIOA[0]);
setMode() 默认会自动打开 GPIO 端口时钟。setPull(true) 是上拉,setPull(false) 是下拉。_set_alternate() 不是同一种机制。EXTI_GetPending / EXTI_ClearPending / EXTI_SoftwareTrigger 仅适用于 STM32 平台(F1/F4/H7/MP13)。CW32 和 MSP432 没有 EXTI 硬件。EXTI_ClearPending 使用写 1 清除(w1c)语义,不要用读-改-写方式操作。EXTI_GetPendingRising / EXTI_GetPendingFalling 分别查询。
技术手册stm32f1xx中的一个小问题
STM32L562DK探索板——继续深入了解Audio Play的工作流程
STM32L562DK探索板——触摸菜单实验之触摸测试
STM32L562DK探索板——触摸菜单实验之LED闪烁
STM32L562DK探索板——给Audio Play添加音量控制功能
STM32L562DK探索板——重写Led闪烁demo
STM32L562DK探索板——触摸菜单的实验
STM32L562DK探索板——简单的显示变量函数
经验分享 | STM32 IAP应用时的跳转注意事项
实战经验 | 碰到hardfault问题你还在死磕代码吗?
微信公众号
手机版