STM32 HAL库串口(UART/USART)调试经验(一)——串口通信基础知识+HAL库代码理解
Written by Restar_xt at UESTC LIREN_B 303 For reference only to the class.4 in the College of Optoelectronic Science and Engineering of UESTC
(一)Serial communication protocol(串口通信协议)概述
通信协议
通俗来说通信就是指数据的收发,涉及到数据的收发就必然涉及到怎么收、怎么发、什么时候开始发、什么时候开始收,于是工程师们就推出了一种规定——通信协议,这套规定主要由三部分构成:
1.语法:数据传输的格式、编码形式。电平高低(逻辑信号)。 2.语义:通信的内容,就是所传输的信息,例如:控制信息(stm32常用蓝牙/ESP32实现手机端与单片机的通信,例如:用蓝牙传输某个变量值,stm32接收到该变量后执行对应的任务,常见的为switch-case匹配变量值,进而执行对应语句)。 3.时序:主要规定了数据什么时候接收、什么时候发送以及通信的速率等等。
串行通信
在现代计算机通信中,几乎所有通信都是串行通信方式,什么是串行呢? 串行和并行是两种通信中的数据传输方式,二者有着本质不同。 串行是指:同一时刻只能传递一个信号使用一根信号线,一字节(1 byte)一字节地向外传送数据。每一位数据的发送是需要等待上一位发送完毕后才能发送。 并行是指:同一时刻可以传递多个信号,每个信号各占一根信号线,每一位数据在同一时刻同时完成传输。 ps:这里可以联想电阻串联和并联,在串联电阻网络里电流是依次流过每个电阻,在并联电阻网络里,电流是同一时刻流经各个电阻。 以下面这个例子为载体,对比两种通信方式。 传输1字节(8位,1byte=8bit)数据时,并行通信需要8根数据线,耗时1T;而串行通信只有一根数据线,传送8bit数据需要耗时8T。
串行:通讯效率较低,但是对信号线路要求低,抗干扰能力强,同时成本也相对较低,一般用于与计算机与外部设备,或者长距离的数据传输。 并行:通讯效率高,但是对信号线路要求也很高,一般应用于快速设备之间采用并行通信,譬如CPU 与存储设备、存储器与存储器、主机与打印机等都采用并行通讯。
根据串行通信中实现数据的同步收发方式的不同,串行通信方式又分为同步串行和异步串行。
异步串行
采用固定的通信格式,数据以相同的帧格式传送。每一帧由起始位、数据位、奇偶校验位和停止位组成。(ps:关于起始位等的介绍见下文STM32CubeMX配置部分) 当设备开始发送数据时,设备先发一个逻辑0的信号作为数据发送即将开始的标志,当接收端接收到0时即会打开相应的通道进行数据的接收,在逻辑0信号后的则是所要传输的数据,当传输完毕后发送端将发送一个逻辑1信号,当接收端接收到逻辑1信号时将停止接收数据,这样一帧数据的发送与接收便已经完成。(ps:若一帧数据发送完毕后并没有紧接着再发送一帧数据,那么信号线上逻辑1信号则会保持,表示这个线路正处于空闲状态,这里对空闲的理解将会有助于后续空闲中断的理解)
同步串行(
S
P
I
SPI
SPI、
I
2
C
I^2C
I2C)
同步通信时,通信双方共用一个时钟,这是同步通信区分于异步通信的最显著的特点。在异步通信中,每个字符要用起始位和停止位作为字符开始和结束的标志,以致占用了时间。所以在数据块传送时,为提高通信速度,常去掉这些标志,而采用同步通信。同步通信中,数据开始传送前用同步字符来指示(常约定1~2 个),并由时钟来实现发送端和接收端的同步,即检测到规定的同步字符后,下面就连续按顺序传送数据,直到一块数据传送完毕。同步传送时,字符之间没有间隙,也不要起始位和停止位,仅在数据开始时用同步字符SYNC来指示,,这里不再赘述同步通信,关于同步通信的详细知识将在
S
P
I
SPI
SPI、
I
2
C
I^2C
I2C中进行介绍。
(二)STM32串口通信实验——HAL库(Hardware Abstraction Layer)代码理解
ps:HAL库(Hardware Abstraction Layer)设计思想:HAL层的根本设计目的是在于上层软件可以通过常规的通用接口访问MCU的某些资源,而不用去关心“是哪个MCU”/“MCU哪部分”在做的,模糊了最底层的函数实现。HAL库的设计思想具体见STM32_HAL_SUMMARY_NOTE
2.1串口句柄
在uart.c中可以看到以下代码,两行代码分别表示uart1的句柄、uart2的句柄。什么是句柄?对于定时器(TIMER)、串口(UART)和模数转换器(ADC)等功能较为复杂的外设,HAL库设计了一个名为外设句柄的数据类型PPP_HandleTypeDef(PPP表示外设名称)。外设句柄作为外设的一个标识符,一般采用结构体类型实现,结构体的成员变量对应外设的工作参数。
UART_HandleTypeDef huart1;
UART_HandleTypeDef huart2;
在keil中右击UART_HandleTypeDef,点击Go To Definition,跳转至stm32fxxx_hal_uart_h文件,可以看到以下代码。
typedef struct __UART_HandleTypeDef
{
USART_TypeDef *Instance;
UART_InitTypeDef Init;
uint8_t *pTxBuffPtr;
uint16_t TxXferSize;
__IO uint16_t TxXferCount;
uint8_t *pRxBuffPtr;
uint16_t RxXferSize;
__IO uint16_t RxXferCount;
__IO HAL_UART_RxTypeTypeDef ReceptionType;
DMA_HandleTypeDef *hdmatx;
DMA_HandleTypeDef *hdmarx;
HAL_LockTypeDef Lock;
__IO HAL_UART_StateTypeDef gState;
__IO HAL_UART_StateTypeDef RxState;
__IO uint32_t ErrorCode;
} UART_HandleTypeDef;
不难发现,这段代码就是类型为 __UART_HandleTypeDef,别名为UART_HandleTypeDef的结构体的定义! 我们不妨再看串口句柄这两行代码:
UART_HandleTypeDef huart1;
UART_HandleTypeDef huart2;
第一行表示:名为huart1的、类型为UART_HandleTypeDef型的一个结构体 第二行表示:名为huart2的、类型为UART_HandleTypeDef型的一个结构体
相信到这里,大家对HAL库设计了一个名为外设句柄的数据类型PPP_HandleTypeDef(PPP表示外设名称)。外设句柄作为外设的一个标识符,一般采用结构体类型实现,结构体的成员变量对应外设的工作参数这句话已经理解清楚了。接下来我们逐行分析串口句柄代码。
2.1.1 串口实例:
USART_TypeDef *Instance;
此处我们将该句代码改写为如下更好理解:
USART_TypeDef* Instance;
单纯阅读代码可知 Instance是USART_TypeDef这个类型的结构体的指针变量,一定要注意这里结构体的嵌套关系!
<1>首先最外层结构体——句柄结构体:UART_HandleTypeDef <2>其次句柄结构体里又单独设置了一个指针变量Instance,指向另一个结构体:USART_TypeDef 那么也就是说每一个串口句柄都对应着一个USART_TypeDef结构体!
根据HAL库的注释,可以知道Instance其实指向的是uart寄存器组的基地址,不难猜想这个名为USART_TypeDef的结构体的成员变量实际上就是寄存器组。右击USART_TypeDef,点击Go To Definition跳转至stm32fxxxxb.h可以看到以下代码:
typedef struct
{
__IO uint32_t SR;
__IO uint32_t DR;
__IO uint32_t BRR;
__IO uint32_t CR1;
__IO uint32_t CR2;
__IO uint32_t CR3;
__IO uint32_t GTPR;
} USART_TypeDef;
阅读以上代码不难验证我们的猜想,这个结构体正是uart的寄存器组配置,如:SR(Status register状态寄存器),DR(Date register数据寄存器),BRR(Baud rate register波特率寄存器)等等,这也为我们直接操作寄存器提供了说明,关于这些寄存器的详细信息需要查阅相应的data sheet。那么这个指针变量Instance到底有什么用呢?请大家想一想我们在哪里见过instance呢?没错在uart.c文件中的MX_USARTx_UART_Init(void)见到过,为加深印象,此处粘贴出MX_USARTx_UART_Init(void)的完整代码:
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
在这里我们只看huart1.Instance = USART1; 这行代码,首先第一点huart1.Instance表示的是结构体huart1中的成员变量Instance,有同学可能会问这个huart1的结构体在哪定义了呢?其实huart1这个结构体就是串口1的句柄定义的!->>UART_HandleTypeDef huart1;(该句柄实际上就是名为huart1的UART_HandleTypeDef型结构体)那么问题来了,为什么Instance这个成员变量等于USART1呢? 为了解决这个问题,在keil中右击USART1,点击Go To Definition可以看到如下代码:
#define USART1 ((USART_TypeDef *)USART1_BASE)
原来USART1是一个宏!而且这个宏是将USART1_BASE(常量)强制转换为USART_TypeDef型结构体指针,常量强制转换为结构体指针是什么意思呢?常量强制转化为一个结构体指针,该常量为结构体的起始地址。也就是说USART1_BASE被强制转换为USART_TypeDef的起始地址,内存空间将以USART_TypeDef类型进行构建,首址为USART1_BASE,空间内有成员,按其固有类型顺序依次分配内存空间。
于是有huart1.Instance = ((USART_TypeDef *)USART1_BASE)
而Instance又是USART_TypeDef结构体的指针
那么整个分析下来逻辑是这样的: 1.我们在cubeMX中配置好串口1后,自动生成串口一的串口句柄UART_HandleTypeDef huart1; ,其实质是自动创建了一个名为huart1的UART_HandleTypeDef型的结构体 2.这个huart1结构体的第一个成员变量Instance是一个USART_TypeDef型结构体型指针(USART_TypeDef*)Instance; (这里是结构体嵌套,UART_HandleTypeDef的成员变量是USART_TypeDef的指针,通过这种方式实现了UART_HandleTypeDef型结构体嵌套USART_TypeDef型结构体) 3.在串口1的抽象初始化中MX_USART1_UART_Init(void) 中将huart1结构体中Instance这个成员变量赋值为USART1这个宏huart1.Instance = USART1; ,而这个宏又是USART_TypeDef型结构体的首地址#define USART1 ((USART_TypeDef *)USART1_BASE) ,也就是huart1.Instance = ((USART_TypeDef *)USART1_BASE) 实现了huart1结构体中嵌套的USART_TypeDef结构体的首地址分配! 4.因为USART_TypeDef结构体成员变量是串口寄存器组,那么Instance作为整个寄存器组的基地址,就可以通过Instance来访问底层寄存器组!换句话说便实现了串口1的寄存器组地址分配! 串口实例,主要是串口寄存器基地址的定义,用于访问硬件寄存器。具体方法是:先将串口所用到的所有寄存器封装成一个USART_TypeDef类型的结构体,然后将串口外设的首地址将之转化为该结构体类型的指针,再根据结构体指针+成员变量(基地址+偏移量)的形式访问底层寄存器。
例:UART1 -> SR
关于外设寄存器地址分配,STM32的逻辑如下:(仍以UART1为例)
以下代码在stm32f103xb.h文件
#define PERIPH_BASE 0x40000000UL
(外设存储器映射)
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000UL)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000UL)
——————————————————————————————————————————————————————————————————
#define TIM2_BASE (APB1PERIPH_BASE + 0x00000000UL)
#define TIM3_BASE (APB1PERIPH_BASE + 0x00000400UL)
#define TIM4_BASE (APB1PERIPH_BASE + 0x00000800UL)
#define RTC_BASE (APB1PERIPH_BASE + 0x00002800UL)
#define WWDG_BASE (APB1PERIPH_BASE + 0x00002C00UL)
#define IWDG_BASE (APB1PERIPH_BASE + 0x00003000UL)
#define SPI2_BASE (APB1PERIPH_BASE + 0x00003800UL)
#define USART2_BASE (APB1PERIPH_BASE + 0x00004400UL)
#define USART3_BASE (APB1PERIPH_BASE + 0x00004800UL)
——————————————————————————————————————————————————————————————————
#define TIM2 ((TIM_TypeDef *)TIM2_BASE)
#define TIM3 ((TIM_TypeDef *)TIM3_BASE)
#define TIM4 ((TIM_TypeDef *)TIM4_BASE)
#define RTC ((RTC_TypeDef *)RTC_BASE)
#define WWDG ((WWDG_TypeDef *)WWDG_BASE)
#define IWDG ((IWDG_TypeDef *)IWDG_BASE)
#define SPI2 ((SPI_TypeDef *)SPI2_BASE)
#define USART2 ((USART_TypeDef *)USART2_BASE)
#define USART3 ((USART_TypeDef *)USART3_BASE)
#define I2C1 ((I2C_TypeDef *)I2C1_BASE)
#define I2C2 ((I2C_TypeDef *)I2C2_BASE)
#define USB ((USB_TypeDef *)USB_BASE)
#define CAN1 ((CAN_TypeDef *)CAN1_BASE)
#define BKP ((BKP_TypeDef *)BKP_BASE)
#define PWR ((PWR_TypeDef *)PWR_BASE)
#define AFIO ((AFIO_TypeDef *)AFIO_BASE)
#define EXTI ((EXTI_TypeDef *)EXTI_BASE)
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *)GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *)GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *)GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *)GPIOE_BASE)
#define ADC1 ((ADC_TypeDef *)ADC1_BASE)
#define ADC2 ((ADC_TypeDef *)ADC2_BASE)
#define ADC12_COMMON ((ADC_Common_TypeDef *)ADC1_BASE)
#define TIM1 ((TIM_TypeDef *)TIM1_BASE)
#define SPI1 ((SPI_TypeDef *)SPI1_BASE)
#define USART1 ((USART_TypeDef *)USART1_BASE)
——————————————————————————————————————————————————————————————————
typedef struct
{
__IO uint32_t SR;
__IO uint32_t DR;
__IO uint32_t BRR;
__IO uint32_t CR1;
__IO uint32_t CR2;
__IO uint32_t CR3;
__IO uint32_t GTPR;
} USART_TypeDef;
——————————————————————————————————————————————————————————————————
示例:利用C语言访问成员变量的语法规定访问寄存器组
指针+ "->" + 成员变量名
USART1 -> SR = 0xFFFF;
2.1.2 串口初始化参数配置:
UART_InitTypeDef Init;
具体初始化配置见2.2串口初始化。
2.1.3 发送数据缓冲区的首地址:
uint8_t *pTxBuffPtr;
2.1.4 待发送数据个数:
uint16_t TxXferSize;
2.1.5 发送数据计数器:
__IO uint16_t TxXferCount;
2.1.6 接收数据缓冲区的首地址:
uint8_t *pRxBuffPtr;
2.1.7 待接收数据个数:
uint16_t RxXferSize;
2.1.8 接收数据计数器:
__IO uint16_t RxXferCount;
2.1.9 正在进行的数据接收模式:
__IO HAL_UART_RxTypeTypeDef ReceptionType;
ps:这里引用HAL库注释
*@brief HAL UART Reception type definition
* @note HAL UART Reception type value aims to identify which type of Reception is ongoing.
* It is expected to admit following values :
* HAL_UART_RECEPTION_STANDARD = 0x00U,
* HAL_UART_RECEPTION_TOIDLE = 0x01U,
*/
2.1.10 DMA发送模式结构体型指针
DMA_HandleTypeDef *hdmatx;
2.1.11 DMA接收模式结构体型指针
DMA_HandleTypeDef *hdmarx;
2.1.12 保护锁类型定义
HAL_LockTypeDef Lock;
2.1.13 串口全局+发送工作状态
__IO HAL_UART_StateTypeDef gState;
2.1.14 串口接收工作状态
__IO HAL_UART_StateTypeDef RxState;
2.1.15 串口错误代码
__IO uint32_t ErrorCode; parameters */
2.2串口初始化
串口初始化主要分为两部分 @串口的抽象初始化:主要指HAL库对串口通信协议的抽象,如波特率、奇偶校验位、起始位、停止位 @串口的承载初始化:主要指HAL库对外设在MCU层面的资源分配,例如管脚分配与使能、时钟分配与使能等 下面分别对两部分初始化做介绍
@串口的抽象初始化 在STM32CubeMX+Keil开发环境下生成的文件夹Application/User/Core中打开uart.c文件可以看到如下代码:
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
}
代码分析 这部分代码为HAL库对串口外设的抽象初始化,主要是对串口通信时的通信协议做规定,下面将对这部分通信协议初始化做单独分析
huart1.Init.BaudRate = 115200;
波特率(BaudRate):在信息传输通道中,携带数据信息的信号单元叫码元,单位时间内通过信道传输的码元数称为码元传输速率,简称波特率。
如何更好地理解波特率这个概念呢? 首先要明确一点,波特率数值表示的是信息传递的速率快慢,由波特率的严谨定义可知,波特率描述的是单位时间内通过信道传输的码元数,又称为码元传输速率,那么问题来了——什么是码元?查阅码元的严谨定义——在数字通信中常常用时间间隔相同的符号来表示一个二进制数字,这样的时间间隔内的信号称为(二进制)码元。 那么问题又来了,什么是符号?查阅符号的严谨定义,可知此处的符号并非我们常见的+ = - * /这些数学符号,此处的符号指的是用于表示某数字码型的一定相位或幅度值的一段正弦载波。想必未接触通信原理的同学学到这里一定是云里雾里,那么如何通俗地理解波特率这一概念呢?我们可以将码元看作为一段携带信息(二进制数)的脉冲信号,也就是把码元看作是信息的载体,码元(脉冲信号)本身的振幅、相位等特征即是对所携带信息的反映。例如:将一段脉冲信号的振幅规定为低(00)、中(01)、高(10)、极高(11)四种,那么当我们接收到一个码元发现它的振幅是低的时候,我们就接受到了它所负载的信息-00。那么由此可见一个二进制码元可以承载1byte(2bit)的信息,由于波特率是码元传输速率(传符号率/传码元率),而比特率是信息传输速率(传信号率),那么此时波特率==1/2比特率。此处存在一个公式
I
=
S
?
l
o
g
2
N
I = S*log_2{N}
I=S?log2?N I代表波特率,N代表码元承载的信息比特数。如果对波特率、码元理解还不够彻底的话,可以这样理解:码元就是一个波!这个波上承载有信息,波特率指的就是波的传播速率!也就是单位时间内接收到的波的个数! PS:此处只是方便各位读者理解!举例有所不当
huart1.Init.WordLength = UART_WORDLENGTH_8B;
数据帧字长(WordLength):描述的是串口通信时一帧数据的真实长度,如上述展示的代码表示的含义为串口传递一帧数据的长度为8bit(1 byte),从下图可以更直观地看出串口通信的协议格式,起始位(1bit)+数据位(8bit)+校验位(1bit)+停止位(1bit)
huart1.Init.StopBits = UART_STOPBITS_1;
停止位(StopBits):是串口通信结束的标志,当接收端接收到逻辑1时表明这一帧数据接受结束
huart1.Init.Parity = UART_PARITY_NONE;
奇偶校验位(Parity):是众多验错方式中最简单的一种
@什么是奇偶校验呢? 众所周知,在两个设备进行通讯的过程中最常见的错误就是数据有一位出现问题,例如本应该要传输01010101,结果却接收到了01010100,那么如何检验接收的信息有问题呢?没错,最简单的就是奇偶校验。奇偶校验的原理是在原本要传输的数据后(/前)增加一位校验位。例如01010101为所要传输的数据,如果采用奇校验,那么发送端发送的应该是010101011,为什么此时校验位是1呢(标红位置为增添的校验位)?因为奇校验为奇数校验,也就是说要保证传输的数据位+校验位中1的个数为奇数,同理如果采用偶校验,就是要保证数据位+校验位中1的个数为偶数,仍以01010101为要传输的数据,那么在采用偶校验的情况下实际传输的应为010101010。 @了解了奇偶校验是什么后,那么奇偶校验是怎么检查出数据错误呢? 以奇校验01010101为例: ···若传输过程中数据没有出现错误,则接收端接收到010101011,经检验这组数据中1的个数为奇数个,那么证明这组数据传输过程中没有出现错误。 ···若数据在传输过程中发生了1位错误,即原本发送的数据为010101011变成了010101111,接收端接收到010101111后发现这组数据中1的个数为偶数,不为奇数!则说明这组数据错误(ps:一般情况下,奇偶校验位不会发生数据错误,此处只考虑数据位错误) ···若数据在传输过程中发生了2位错误,即原本发送的数据为010101011变成了010111111,接收端接收到010101111后发现这组数据中1的个数为奇数,则根据奇偶校验原理说明这组数据正确???显然这就是奇偶校验的弊端!奇偶校验虽然简单,但是验错率低!
huart1.Init.Mode = UART_MODE_TX_RX;
通信模式(Mode):即接收又发送(全双工)
@串口的承载初始化 在STM32CubeMX+Keil开发环境下生成的文件夹Application/User/Core中打开uart.c文件可以看到如下代码:
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
__HAL_AFIO_REMAP_USART1_ENABLE();
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
else if(uartHandle->Instance==USART2)
{
__HAL_RCC_USART2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_3;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_SetPriority(USART2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART2_IRQn);
}
}
可以看出这部分是ST公司对串口外设提供的MSP(单片机的具体解决方案),主要是IO引脚分配、中断优先级分配等等
如有错漏,敬请批评指正! 关于STM32寄存器地址分配可以参看:单片机STM32学习笔记之寄存器映射详解 关于HAL库的理解可以参看:STM32_HAL_SUMMARY_NOTE
|