MCU基础
MCU(Microcontroller Unit,微控制器),将CPU、内存(RAM/ROM)、外设(GPIO、UART、ADC等)集成在单一芯片上的微型计算机系统,专为嵌入式控制设计。例如:STM32(ARM Cortex-M)、ESP32(Wi-Fi/BLE)
SoC(System on Chip,片上系统),在单芯片上集成完整的系统功能,包括CPU、GPU、内存控制器、高速接口(USB/PCIe)、甚至AI加速器等,类似一台微型电脑。例如:高通骁龙、苹果A系列、华为麒麟
看似单片机系统复杂多样,但实际上现代单片机的代码如同计算机程序一样方便,厂商提供了丰富的底层驱动库,无须在底层上花费太多实践,可与专注于应用功能实现。
1. 芯片摘要
当我们拿到一款处理器时,或者说在芯片选型的过程中,我们需要对芯片的主要性能参数有一定的了解。这时候芯片手册是最重要的参考资料,我们可以快速的从中提取关键信息,来判断芯片是否符合我们的需求。
一般芯片厂商通常会提供以下几类文档(一般官网获取):
- 《xx系列数据手册》 数据手册包含单片机的主要参数信息,如串口数量、引脚定义、电气特性等,是获取关键信息的首要资料。
- 《xx系列参考手册》 参考手册(有时称为硬件手册)详细描述了单片机的体系结构、寄存器、地址映射、电源管理、复位机制等,主要面向开发者。
- 《xx系列MCU开发工具用户手册》 该手册介绍开发工具和环境的使用方法,提供示例程序和操作说明,帮助开发者快速上手。
- 《xx系列勘误表》 勘误表列出了芯片已知的问题和修正建议,是开发和调试过程中不可忽视的参考资料。
通过芯片摘要,我们可以从数据手册中快速了解其主要特性和适用场景。
重点信息包括:
- 内核类型与主频:如 ARM Cortex-M4,最高主频200MHz。
- 存储资源:如512KB Flash,256KB SRAM。
- 外设接口:如多路UART、SPI、I2C、CAN、USB、ADC、DAC等。
- 引脚数量与封装形式:如LQFP100、LQFP64等,决定了可用I/O数量和布局。
- 电气特性:工作电压范围(如2.7V~3.6V)、I/O口驱动能力等。
- 功耗参数:如待机功耗、运行功耗,影响应用场景(低功耗/高性能)。
- 工作温度范围:如-40℃~85℃,决定是否适用于工业环境。
以华大 HC32F460 系列为例,数据手册摘要如下:
| 参数 | 说明 |
|---|---|
| 内核 | ARM Cortex-M4(带FPU) |
| 主频 | 最高200MHz |
| Flash | 512KB |
| SRAM | 256KB |
| 外设 | 4×UART, 4×SPI, 3×I2C, 1×CAN, 2×ADC, 2×DAC, PWM等 |
| 封装 | LQFP100, LQFP64等 |
| 工作电压 | 2.7V~3.6V |
| 工作温度 | -40℃~85℃ |
通过这些关键信息,可以初步判断该芯片是否满足项目需求,并为后续详细选型和开发打下基础。
2. AMR单片机启动流程分析与BootLoader
启动单片机机是我们的第一步。我们常用的单片机内核基本上都采用ARM架构,对AMR架构有一定的理解,也许有助于我们后续开发过程中问题的定位与分析?不过咱也不过是拾人牙慧,浮于表面罢了。
2.1. ARM架构
在理解ARM架构之前,我们首先需要明确ARM在芯片产业中的定位。ARM公司本身并不生产芯片,而是提供处理器架构与核心设计。芯片厂商则基于这些ARM内核,进行集成或定制化开发,形成最终的处理器产品。
在这个过程中,ARM内核主要负责指令执行与程序运行,而芯片的实际功能则由围绕内核的各类外设实现。芯片厂商会在ARM内核之外集成多种硬件模块,例如GPIO、UART、ADC等,这些外设才是我们实现具体功能(如控制LED、串口通信、模数转换)的关键所在。
因此,在嵌入式开发中,我们通常通过寄存器直接编程或调用芯片厂商提供的软件库(如STM32的HAL库、HC32的LL库)来配置和控制这些外设。ARM内核的作用,则是确保我们编写的代码能够被正确、高效地执行。
其中ARM架构它定义了最基础、最核心的规则,像指令集,数据格式、硬件逻辑。属于标准和规范。
ARM内核,是ARM公司根据ARM架构设计好的“发动机”,芯片厂商可以直接把现成的“发动机”装进自己的“汽车”(芯片)里。大部分芯片厂商应该就是直接拿来用,但是有的认为它不够好,会自研,但仍遵从ARM架构。
除了ARM架构外,RISC-V目前也是发展迅猛,凭借开源,无授权费的优势快速发展中。
// todo 关于架构内容颇多,个人理解有限。有机会的话另起一页再展开吧。
2.3. ARM程序
在了解启动流程之前,我们先熟悉一下我们的程序。
我们知道,如果使用Keil编译完成后,可在生成的.map文件中中查看RO、RW、ZI的数据大小;同样,如果使用GCC编译可以在Cmake中使用size命令,查看text、data、bss的数据大小。它们的效果是一样的,这是我们的程序在程序编译阶段数据存储的分布情况!
[build] text data bss dec hex filename
[build] 110252 76 4020 114348 1beac E:/MyWorkspace/Project/EvaporativeCooling/gcc/build/output/EvaporativeCooling.elf
text -> RO : 代码 + 只读数据(Flash)
data -> RW : 已初始化的变量(Flash存储初始值,在上电启动好后拷贝到RAM,运行期间修改的是RAM副本)
bss -> ZI : 未初始化的变量(RAM)
dec = text + data + bss : 总计
Flash空间占用 = text + data
RAM空间占用 = data + bss
注意:这里再编译阶段只是确定全局变量需要占多少地址空间,栈大概需要多大。但它并不在RAM上创建这些变量。在运行阶段变量才会被创建,断电则丢失
从编译到运行的整个过程:
flowchart TB
A[源代码<br>.c/.cpp 文件] --> B[编译与链接]
subgraph B [编译阶段-在PC上]
B1[编译器Compiler]
B2[链接器Linker]
B1 --> B2
end
B --> C[生成二进制文件<br>.bin/.hex]
C --> D[烧录/下载]
D --> E[拷贝到MCU Flash]
E --> F1[MCU上电执行]
subgraph F [运行阶段-在MCU上]
subgraph F2_sub [运行时动态存储]
subgraph F2_Flash[从Flash读取指令]
F2_Flash_1[机器码指令]
F2_Flash_2[只读常量数据]
F2_Flash_3[初始化数据的初始值]
end
subgraph F2_RAM[在RAM中创建/修改数据]
F2_RAM_1[全局/静态变量<br>当前值]
F2_RAM_2[堆Heap<br>动态分配的数据]
F2_RAM_3[栈Stack<br>局部变量、函数调用]
end
end
F1 --> F2_sub
end
B --> G[符号表<br>内存布局规划]
G -.->|为运行阶段提供蓝图| F2_sub
2.4. 存储映射表
以华大HC32F460为例,参考RM_HC32F460_F45x_A460系列参考手册_Rev1.51,第一章 存储器映射(Memory mapping)内容。
HC32F460支持4GB的线性地址空间,地址从0x0000_0000到0xFFFF_FFFF。
Q: 为什么Flash只有512K,却有4G地址空间?
A: 因为地址空间 != 物理内存。ARM Cortex-M使用内存映射I/O,所有外设都映射到地址空间。这样设计统一访问方式,用相同的访问方式访问不同的设备,代码也可以在不同的芯片间移植。编译器只需要生成固定地址的代码即可。
4GB是CPU的”视角”,512KB是实际的”存储能力”。就像你有整个城市的地址地图(4GB地址空间),但只实际拥有几栋房子(512KB Flash + 128KB RAM)
这里的类比这里非常形象了!
Q: 为什么要这么设计?
A: 首先要知道这个规则是由芯片内核架构(ARM)规定的。这样设计它能够统一编址与预留空间,给各个芯片厂商预留空间,增加内核的兼容性、扩展性和可移植性,实现了“天下大一统”。比如芯片厂商想推出更大的Flash型号的芯片,只要在同一个代码区地址范围内,换一个更大的物理Flash即可,不需要改变内核架构。而且预留了一些像外部RAM区域,如果你需要更大的容量内存,可以外接SDRAM,将物理地址映射到预留的片外RAM区域即可,当然如果你不用也是没有关系的。
HC32F460 4GB地址空间布局 (Cortex-M):
0x0000 0000 ┌─────────────────────────┐
|0x0000_0000|Flash(512K) |
│ Code |─────────────│ ← 启动位置可选,指向Flash或RAM,通常调试时需要从RAM启动
|0x1FFF_8000|SRAM(32K) |
0x1FFF FFFF ├─────────────────────────┤
│ Empty │ ← 无物理内存,访问会 fault
0x2000 0000 ├─────────────────────────┤
│ SRAM │ ← 64K+64K+28K+4K==160K RAM在这里
0x2002_6FFF ├─────────────────────────┤
│ │
│ Empty │
│ │
0x4000 0000 ├─────────────────────────┤
│ Peripheral │ ← GPIO, USART, SPI等
0x400F FFFF ├─────────────────────────┤
│ │
│ External RAM │ ← 外接SDRAM, NOR Flash等
0xDFFF FFFF ├─────────────────────────┤
│ System │ ← 内核私有外设NVIC, SysTick, MPU等
0xFFFF FFFF └─────────────────────────┘
Q: 启动位置可选?
A:
在芯片启动时,决定0x0000_0000这个地址是连接到Flash还是SRAMH。正常程序运行模式从Flash启动,调试或特殊启动模式从RAM启动,有些MCU可以通过Boot引脚设置为从RAM启动,CPU从SRAMH取指。此时,要执行的程序必须已经被预先加载到SRAMH中。常规调试过程中确实需要擦写Flash,而“从RAM启动”的调试模式正是为了规避这个问题而设计的高级功能。它通过牺牲对程序大小的包容性(程序必须能放进RAM),换来了极致的调试体验和速度,特别适合在开发关键算法或驱动时进行密集的、反复的调试。
可以看到,HC32F4xx系列单片机的Flash的地址是写在0x0000_0000,所以当选择从Flash启动后,可以直接正常进入程序。但是像码农爱学习在其文章单片机程序烧录的3种方式(ISP、ICP、IAP)是什么?2中提到的STM32F4xx系列单片机,他的Flash(Main Memory)物理地址实际是从0x0800_0000开始,然而,ARM Cortex-M内核规定其必须从地址0x0000_0000开始读取栈指针和复位向量。为了解决这个矛盾,STM32芯片内部设计了一个地址重映射机制。
利用BOOT引脚(如BOOT0, BOOT1)的选择将哪一块物理存储器映射到内核要求的这个0x0000_0000起始的地址空间。
- 当选择从主Flash启动时,0x0000_0000的访问被重定向到0x0800_0000开始的主Flash。
- 当选择从系统存储器启动时,0x0000_0000的访问被重定向到内部Bootloader的ROM地址。
- 当选择从内置SRAM启动时,0x0000_0000的访问被重定向到SRAM的起始地址(如0x2000_0000)。
因此,对于绝大多数从主Flash启动的应用程序,开发者只需要将程序链接到0x0800_0000即可,芯片硬件会自动处理好从0x0000_0000的访问。
然而华大的芯片就没有这种烦恼,因为目前Flash物理起始地址就是0x0000_0000,当然也可以自己设计Bootloader进行重定向。
2.5. 启动流程分析
此处关于MCU启动流程的分析,主要参考了赤诚Xie与林接接等博主的相关论述34。
网上启动流程版本有很多,我按照个人的理解做了一些整理,咱就是丑陋的缝合怪,哈哈。因为我也不知道如何去验证这个启动流程的正确性,官方文档里也没有找到这块内容的详细介绍,所以这里只能膜拜各位大佬的理解了。
- 复位
- CPU从0x0000_0000(实际是Flash硬件的0x0000_0000)读取MSP(主栈指针)初始值,
- 从0x0000_0004(实际是Flash的0x0000_0004)读取复位向量(硬件自动加载到PC指针)
- 启动代码执行
- 通过复位向量跳转到复位处理函数(Reset_Handler),执行Reset_Handler代码,此处开始执行启动文件
- 初始化 .data 段(从Flash到RAM)
- 清空 .bss 段
- 其他咱就看不懂了
- 执行SystemInit函数, 初始化系统时钟
- 跳转到main
- 通过复位向量跳转到复位处理函数(Reset_Handler),执行Reset_Handler代码,此处开始执行启动文件
- main函数
- 初始化外设
- 使能中断
- 主循环
- 中断处理
- 系统相应中断
- 中断执行完成回归main函数
HC32F460 GCC版本启动代码(Reset handler部分):
/*
;<h> Reset handler start.
*/
.section .text.Reset_Handler
.align 2
.weak Reset_Handler
.type Reset_Handler, %function
.globl Reset_Handler
Reset_Handler:
/* Single section scheme.
*
* The ranges of copy from/to are specified by following symbols
* __etext: LMA of start of the section to copy from. Usually end of text
* __data_start__: VMA of start of the section to copy to
* __data_end__: VMA of end of the section to copy to
*
* All addresses must be aligned to 4 bytes boundary.
*/
ClrSramSR:
ldr r0, =0x40050810
ldr r1, =0x1F
str r1, [r0]
/* Copy data from read only memory to RAM. */
CopyData:
ldr r1, =__etext
ldr r2, =__data_start__
ldr r3, =__data_end__
CopyLoop:
cmp r2, r3
ittt lt
ldrlt r0, [r1], #4
strlt r0, [r2], #4
blt CopyLoop
CopyData1:
ldr r1, =__etext_ret_ram
ldr r2, =__data_start_ret_ram__
ldr r3, =__data_end_ret_ram__
CopyLoop1:
cmp r2, r3
ittt lt
ldrlt r0, [r1], #4
strlt r0, [r2], #4
blt CopyLoop1
/* This part of work usually is done in C library startup code.
* Otherwise, define this macro to enable it in this startup.
*
* There are two schemes too.
* One can clear multiple BSS sections. Another can only clear one section.
* The former is more size expensive than the latter.
*
* Define macro __STARTUP_CLEAR_BSS_MULTIPLE to choose the former.
* Otherwise define macro __STARTUP_CLEAR_BSS to choose the later.
*/
/* Single BSS section scheme.
*
* The BSS section is specified by following symbols
* __bss_start__: start of the BSS section.
* __bss_end__: end of the BSS section.
*
* Both addresses must be aligned to 4 bytes boundary.
*/
/* Clear BSS section. */
ClearBss:
ldr r1, =__bss_start__
ldr r2, =__bss_end__
movs r0, 0
ClearLoop:
cmp r1, r2
itt lt
strlt r0, [r1], #4
blt ClearLoop
ClearBss1:
ldr r1, =__bss_start_ret_ram__
ldr r2, =__bss_end_ret_ram__
movs r0, 0
ClearLoop1:
cmp r1, r2
itt lt
strlt r0, [r1], #4
blt ClearLoop1
SetSRAM3Wait:
ldr r0, =0x40050804
mov r1, #0x77
str r1, [r0]
ldr r0, =0x4005080C
mov r1, #0x77
str r1, [r0]
ldr r0, =0x40050800
mov r1, #0x1100
str r1, [r0]
ldr r0, =0x40050804
mov r1, #0x76
str r1, [r0]
ldr r0, =0x4005080C
mov r1, #0x76
str r1, [r0]
/* Call static constructors */
bl __libc_init_array
/* Call the clock system initialization function. */
bl SystemInit
/* Call the application's entry point. */
bl main
bx lr
.size Reset_Handler, . - Reset_Handler
/*
;<h> Reset handler end.
*/
2.6. BootLoader
此处内容大部分参考了无际单片机编程相关论述5,他写的简单易懂,膜拜🙇♂️。
Q: 单片机Bootloader能解决什么问题?
A: Bootloader就像是给单片机安装了一个“智能操作系统”,解决了传统单片机开发中的几个痛点:
- 更新程序太麻烦。Bootloader使得单片机可以通过各种通信接口(4G模块、串口等方式)来接收新的程序文件(固件),并自己将自己“重新编程”。这个过程被称为IAP(In-Application Programming)。
- 升级失败,“变砖”问题。支持程序备份和回滚。它会先把旧程序好好保存起来,等确认新程序完全没问题之后,再正式启用。
- 自定义启动逻辑,增加开发效率。
Q: Bootloader, IAP, OTA?
A: 这三个词经常一起出现,关系紧密但含义不同:
- Bootloader:指那段程序本身,它提供了加载应用程序和执行固件更新的基础能力;
- IAP: 自己将自己“重新编程”,这个过程被称为IAP。算是一种方法或技术手段;
- OTA:(Over-The-Air) 是一种固件交付方式,特指通过无线通信(Wi-Fi, Bluetooth, 蜂窝网络等)将新的固件包发送到设备。设备接收到OTA包后,通常会利用其IAP能力来完成实际的烧录更新。
所以,可以说:OTA是实现远程固件更新的一种高级方式,它依赖于设备的IAP能力,而IAP能力的实现往往离不开一个健壮的Bootloader。
2.6.1. BootLoader工作原理
BootLoader(引导加载程序),顾名思义就是引导程序从正确的地方开始。就是在原本流程中启动主程序运行之前,增加了一段引导加载程序的处理。引导加载程序需要做的就是最低限度的硬件初始化(时钟、GPIO、通信),然后决定是否启动主程序,或者是进入特殊模式(接收新固件、启动新程序)。其通常烧录在单片机Flash的起始地址。
1. Bootloader启动:启动和正常的单片机启动一样,只不过这里需要先经过引导加载程序,再进行后续的流程。
CPU上电执行 --> 从向量表中取出Flash起始地址 --> 执行复位处理程序 --> 跳转Bootloader代码。
2. 硬件初始化:Bootloader不需要像主应用程序那样初始化所有的外设。它只需要初始化最基本的硬件,满足它的核心功能需求即可。
- 时钟系统:让CPU和外设跑起来。
- GPIO:可能需要用来检测某个引脚状态,判断是否进入升级模式,或者控制LED指示状态。
- 通信接口:用于接收新固件的接口,比如UART, SPI, I2C, CAN, USB等。
3. BootLoader处理逻辑:硬件初始化完成后,需要决定接下来要做什么?
- 升级?
- 跳转主应用程序?
这里通常通过检测GPIO引脚、检查Flash标志位、监听通信、检测应用程序有效性等方式来进行模式选择。
2.6.2. BootLoader程序设计
//todo 手动去实现一下啊
3. 时钟系统
不同的的单片机时钟树的结构存在一些差异,以HC32F460来说,时钟控制单元包含六个时钟源和分频器电路:
- 一个外部高速振荡器(XTAL);
- 一个外部低速振荡器(XTAL32);
- 两个PLL时钟(分频、倍频);
- 一个内部高速振荡器(HRC);
- 一个内部中速振荡器(MRC);
- 一个内部低速振荡器(LRC);
- 一个SWDT专用内部低速振荡器;
- 时钟预分频器、时钟多路复用和时钟门控电路。
Q: 为什么需要这么多时钟?
A: 一方面要知道时钟频率越高,单位时间内执行的指令越多,针对性能强大的CPU,就需要提供更高的时钟信号。另一方面像高速外设SPI、USB这类信号要求高速时钟频率才能正常的工作,所以需要高频时钟。但是像I2C、UART这种低速外设,高速又有些浪费。因此像电源树设计一样,针对不同的模块设计了不同的时钟频率。
对此,通过分频器从主时钟派生不同频率,避免为每个外设单独设计时钟源。
因为博客不计划贴图,所以简单用mermaid简单示意一下,详细可以参考,《xx参考手册》1-时钟控制器章节-时钟系统框图。
graph LR
PLLSRC[PLLSRC<br>选择时钟源]
XTAL[XTAL<br>外部高速时钟]
HRC[HRC<br>内部高速时钟]
CKSW[CKSW<br>选择器、分频器]
JTCK_SWCLK[JTCK_SWCLK<br>计数器用时钟]
SYSCK[SYSCK<br>系统时钟]
subgraph 时钟源倍频处理
%% 时钟源
XTAL --> PLLSRC
HRC --> PLLSRC
end
%% PLL
PLLSRC --> CKSW
subgraph 分频到工作时钟
%% 分频到总线
CKSW --> SYSCK
end
subgraph 外设时钟
%% 总线到外设
CKSW --> HCLK
CKSW --> EXCLK
CKSW --> PCLK0
end
subgraph 特殊时钟
%% 特殊时钟
JTCK_SWCLK
end
说明
- 我们通常不使用芯片内部时钟源,因为高速内部时钟信号根据温度和环境的情况频率会飘移,不稳定;
- 时钟的配置原则应该是从后向前配置,即从外设向晶振方向配置;
- 各时钟之间需遵守频率倍数规则;
3.1. 波特率配置计算
波特率(Baud Rate,每秒传输的符号数)的配置与时钟源是密切相关的,我们以串口的波特率配置为例,来实际梳理一下时钟的配置。
同样以HC32F460为例:
现在假设我们的主频SYSCK 168MHz,想用USART1。通过查芯片参考手册可知,USARTn(n=1~4)采用的是PCLK1时钟。PCLK1由主频通过2分频得到,即PCLK1 82MHz。
根据UART模式下波特率计算公式(无小数部分):
\[B = \frac{C}{8 \times (2 - \text{OVER8}) \times (\text{DIVInteger} + 1)}\]关键参数:
- B:波特率
- C:时钟源
- OVER8:过采样模式,即一位数据传输期间的基本时钟数。不像是人话;
过采样技术,就是在一位数据间进行多次信号采集,之后得出得结果就可以判断当前位是否正确,可以有效的辨别数据是否被干扰造成的数据丢失。采样时钟可以是波特率时钟的16倍或8倍
- DIVInteger:整数分频寄存器,是我们的设置波特率大小的主要参数。
所以假设我们需要2400波特率,16分频:
84000000/16/(8*(2-0)*(DIVInteger+1)) = 2400
DIVInteger = 136.72 √
但是当我们设置4分频时,这里时配不出来2400Hz的:
84000000/4/(8*(2-0)*(DIVInteger+1)) = 2400
DIVInteger = 546.875 ×
注意:寄存器DIVInteger只有8位,若计算值超出,需调整分频或降低波特率。
当然还有波特率误差计算公式,只要误差在1%以内是可以接受的,若误差超出需求(如>1%),需调整时钟源(C)或分频模式(PSC/OVER8)。
如果是带小数分频寄存器,可以先计算理论分频系数后,得出整数部分,再去计算小数部分,最后验证误差是否合理。
\[DIVInteger = \frac{C}{B \times 16}\]3.2. 定时器
单片机常用定时器有通用定时器、高级定时器、系专用定时器。
3.2.1. 系统专用定时器 SysTick
Cortex-M内核内置的24位倒计时定时器,功能固定,通常仅提供周期性中断。纯内部使用!不与芯片引脚关联。
3.2.2. 通用定时器 Timer
以华大为例,有两个通用定时器Timer0、TimerA,Timer4,各自具备的能力并不相同。
- Timer0:可以实现同步计数、异步计数两种基本定时器,仅计时用。定时器有包含A、B两个通道(比较器、计数器),可以在计数期间产生比较事件,触发中断。
- TimerA:有6个TimerA单元,具有16位计数宽度(即最大计数值为2^16)、8路PWM输出的定时器。可以输出三角波锯齿波两种波形模式,可生成各种PWM波形(如中心对齐PWM、边沿对齐PWM、甚至是正弦波)。
- Timer4:更强大,一个用于三相电机控制的定时器模块?!
3.2.3. 高级定时器 Timer
同样有高级定时器Timer6
- Timer6:是一个16位计数宽度的高性能定时器,可用于计数产生不同形式的时钟波形,输出以供外部使用。该定时器支持三角波和锯齿波两种波形模式,可生成各种 PWM 波形;有3个Timer6单元
todo,区别差异对比
3. 复位电路
4. 中断系统
5. GPIO复用
6. DMA控制器
低功耗模式
电源控制
flash编程
ESD和锁存保护
7. 其他常见基本外设
2. I/O口
I/O口既可用作输入,也可用作输出。
本质上单片机就是对I/O口的控制,无论单片机对外界进行何种控制,都是通过I/O口进行的。接受外部的控制,通过I/O来感受外部的电压。
1.1. I/O口结构
推完输出:可以直接输出高电平或低电平,无需外部上拉电阻。本质上由两个MOS管(PMOS和NMOS)组成,一个负责拉高,一个负责拉低。
开漏输出:依赖上拉电阻输出高电平,如果不接上拉电阻,就是高阻态。只有下拉NMOS管,无上拉PMOS管。高电平需依赖外部上拉电阻。
浮空输出:电平状态不确定,易受噪声干扰。输入引脚既不接上拉也不接下拉,处于高阻态(无驱动能力)。
PS:上拉电阻就是将不确定的信号通过一个电阻拉到高电平,同时此电阻起到一个限流的作用,下拉就是下拉到低电平。
常见问题&解决办法
单片机程序烧录过程中“串电压”问题
像JLink、STLink这类仿真器一般有提供供电引脚给单片机供电。但是仿真器和目标设备(如MCU开发板)分别独立供电时,两者之间的电源系统因电势差或共地问题导致的电压异常,轻则通信失败、重则器件烧毁。
解决办法:所以在程序烧录过程中需要注意电源供电问题,确保统一供电(优先使用目标板为仿真器供电,或关闭目标板电源,仅通过仿真器供电)、共地。
以JLink为例,JLink下载接口支持SWD和JTAG接口,他们是两种调试协议标准
- SWD,ARM推出的两线调试接口,两个引脚SWDIO(串行数据输入/输出)、SWCLK(串行时钟),仅支持ARM架构
- JTAG是一种标准的调试接口,四个引脚TMS(测试模式选择)、TCK(测试时钟)、TDI(测试数据输入)、TDO(测试数据输出)
像我们常用的JLink(盗版),VCC是可以作为输出的,通过内部跳线帽可以直接让Pin1、pin2直接输出3.3V高电平。如果不是用跳线帽的话可以通过JLink Commander的power命令来控制电压的输出与否。如果没有JLink供电是无法与芯片连接的,所以通常直接用用跳线帽输出3.3V。所以此处针对3.3V的系统来说是没有问题的。
“ARM仿真器”问题
公司用的ARM仿真器,本来正常使用结果今天这个烧录器一直闪绿灯,我也识别不到JLink,看了下网上的说法,以为是驱动出问题了?尝试重新装了驱动,没有效果。最后换了一个仿真器,能够正常识别。
但是问题来了,重装驱动后,发现程序Keil烧录按钮灰掉了,并且设置JLink会弹出,不能加载JL2CM3.dll驱动!
尝试改了环境变量,无果。
参考 https://zhuanlan.zhihu.com/p/656208641 这篇文章解决。
解决方法:是由于装Jlink驱动的时候,勾选了JLinkARM.dll装进Keil MDK里面去了导致。可以下载老版的Keil,把“Keil安装目录\ARM\Segger”里的库都替换。但是我发现网上下载下来的JLink库太老了,找不到我现在用的芯片,于是直接从新版JLink,找到对应的库替换进去,解决。
不过JLink提示更新的弹窗,更新了就不弹了。
参考资料
Joseph Yiu. (2014). 《ARM Cortex-M3与Cortex-M4权威指南(第3版,中译)》
-
码农爱学习. (2021). 单片机程序烧录的3种方式(ISP、ICP、IAP)是什么?. https://zhuanlan.zhihu.com/p/367821312 ↩
-
赤诚Xie. (2024). MCU的启动到bootloader原理详解. https://www.cnblogs.com/chicheng/p/18267699 ↩
-
林接接. (2025). 典型arm32位单片机启动流程(从上电到main.c). https://www.cnblogs.com/jiejielin-blogs/p/19008377 ↩
-
无际单片机编程. (2025) 一文读懂Bootloader:从原理到OTA应用. https://www.eet-china.com/mp/a397421.html ↩