HAL库

HAL的初始化配置,一般都通过CubeMX来配置,GPIO的模式(输入/输出/中断),定时器配置,串口配置等

GPIO的一些基本函数

1
2
3
4
5
6
7
8
HAL_GPIO_WritePin(GPIOA, LED_G_Pin|LED_B_Pin, GPIO_PIN_SET);
//写GPIO的电平
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
//翻转GPIO的电平
HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_13);
//读取GPIO的电平
HAL_Delay(1000);
//延时1000ms

USART串口的基本函数

1
2
3
4
5
HAL_UART_Transmit(&huart1,msg,2,HAL_MAX_DELAY);
//串口从msg中发送2个字节的数据
//msg是自己定义的长度为2的数组
HAL_UART_Receive(&huart1,msg,2,HAL_MAX_DELAY);
//串口接收2个字节的数据,存到msg中

串口发送长字节数组

1
2
3
4
5
#include "string.h"
char msg[]="helloworld\r\n"


HAL_UART_Transmit(&huart1,(uint8_t *)msg,strlen(msg),HAL_MAX_DELAY);

串口printf打印重定向

1
2
3
4
5
6
7
8
#include <stdio.h>

int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,10);
return ch;
}
//在main.c中,主函数之外的下面添加这个函数,就可以用printf在串口中打印文字

串口——中断接收定长数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define LENGTH 5 //宏定义数组的长度
uint8_t RXBuff[LENGTH];

int main(){
HAL_UART_Receive_IT(&huart1 ,RXBuff ,LENGTH); //使能接收中断
printf("当前已开启接收中断,等待数据发送!\r\n");
while(1){

}
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) //回调函数
{
if(huart -> Instance == USART1) // 判断发生接收中断的串口
{
printf("\r\n已经接收到数据,数据如下:");
HAL_UART_Transmit_IT(&huart1 ,RXBuff ,LENGTH); // 回显发送的数据
HAL_UART_Receive_IT(&huart1 ,RXBuff ,LENGTH); //使能接收中断
}
}

int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,10);
return ch;
}

现象是,在串口助手中的发送区发送5个字节的数据比如”ABCDE“,接收区会显示”ABCDE“

串口——中断接收不定长数据(这个比定长好用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define LENGTH 64 //宏定义数组的长度
uint8_t RXBuff[LENGTH];

int main(){
HAL_UARTEx_ReceiveToIdle_IT(&huart1 ,RXBuff ,LENGTH); //使能接收中断
printf("当前已开启接收中断,等待数据发送!\r\n");
while(1){

}
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart,uint16_t Size) //回调函数
{
if(huart -> Instance == USART1) // 判断发生接收中断的串口
{
printf("\r\n已经接收到数据,数据如下:");
HAL_UART_Transmit_IT(&huart1 ,RXBuff ,LENGTH); // 回显发送的数据
HAL_UARTEx_ReceiveToIdle_IT(&huart1 ,RXBuff ,LENGTH); //使能接收中断
}
}

int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,10);
return ch;
}

DMA与串口应用

Transmit_IT:CPU 自己亲手一字节把数据递给串口,发完通知你。

Transmit_DMA:交给 DMA 小弟搬运,CPU 该干啥干啥,搬完通知你。

ReceiveToIdle_IT:CPU 亲手接每一个字节,对方停发 / 收满数量就通知你。

ReceiveToIdle_DMA:DMA 小弟帮你收字节,CPU 摸鱼,对方停发 / 收满就通知你。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define LENGTH 64 //宏定义数组的长度
uint8_t RXBuff[LENGTH];

int main(){
HAL_UARTEx_ReceiveToIdle_DMA(&huart1 ,RXBuff ,LENGTH); //使能接收中断
printf("当前已开启接收中断,等待数据发送!\r\n");
while(1){

}
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart,uint16_t Size) //回调函数
{
if(huart -> Instance == USART1) // 判断发生接收中断的串口
{
printf("\r\n已经接收到数据,数据如下:");
HAL_UART_Transmit_DMA(&huart1 ,RXBuff ,LENGTH); // 回显发送的数据
HAL_UARTEx_ReceiveToIdle_DMA(&huart1 ,RXBuff ,LENGTH); //使能接收中断
}
}

int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,10);
return ch;
}

蓝牙串口模块

代码和串口的代码一模一样。

串口USART模块是:

电脑端串口助手发送数据——>串口模块——>单片机——>串口模块——>电脑端串口助手接收数据

蓝牙串口模块是:

手机端蓝牙串口助手发送数据——>蓝牙模块——>单片机——>蓝牙模块——>手机端蓝牙串口助手接收数据

江协科技蓝牙助手读取滑杆和摇杆数据

使用了#include "stdlib.h"和#include "string.h"的函数

使用strcmp函数判断字符串内的内容是否是我想要的。

使用strtok函数分割字符串,并用char *tag,char *Name,char *Value去接收分割后形成的子串数据。

使用atoi以及atof函数将字符数据(ACSII)转化为Int和Float数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
while (1)
{
if (Serial_RxFlag == 1) //如果接收到数据包
{
OLED_ShowString(4, 1, " ");
OLED_ShowString(4, 1, Serial_RxPacket); //OLED清除指定位置,并显示接收到的数据包
char *tag=strtok(Serial_RxPacket,",");
if (strcmp(tag,"slider")==0) {
char *Name=strtok(NULL,",");
char *Value=strtok(NULL,",");
if (strcmp(Name,"1")==0) {
uint8_t IntValue=atoi(Value);//atoi是ACSII to Int
OLED_ShowNum(1, 1, IntValue,3);
//printf("Slider,1,%d\r\n",IntValue);
}
else if (strcmp(Name,"2")==0) {
float FloatValue=atof(Value);//atof是ACSII to Float
//printf("Slider,2,%f\r\n",FloatValue);
}
}
else if (strcmp(tag,"joystick")==0) {
char *LX=strtok(NULL,",");
char *LY=strtok(NULL,",");
char *RX=strtok(NULL,",");
char *RY=strtok(NULL,",");
int8_t IntLX=atoi(LX);
int8_t IntLY=atoi(LY);
int8_t IntRX=atoi(RX);
int8_t IntRY=atoi(RY);
OLED_ShowSignedNum(2,1,IntLX,3);
OLED_ShowSignedNum(2,5,IntLY,3);
OLED_ShowSignedNum(3,1,IntRX,3);
OLED_ShowSignedNum(3,5,IntRY,3);
//printf("Joystick,%d,%d,%d,%d\r\n",IntLX,IntLY,IntRX,IntRY);
}
Serial_RxFlag = 0;
}
}
//

ADC—规则组—单通道—非连续—采集电位器数据通过串口打印

修改Cmakelists.txt

为了支持printf输出%f,%.3f等类型数据,如果不修改Cmakelist,在串口中打印的%f将是空白的。

1
2
3
4
# 新增:开启printf浮点支持
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
-u_printf_float
)

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint32_t ADC_Value=0;
float Voltage;
int main(void)
{
HAL_ADCEx_Calibration_Start(&hadc1);//校准ADC,一个固定的动作
while(1){
HAL_ADC_Start(&hadc1);
if (HAL_ADC_PollForConversion(&hadc1,50)==HAL_OK) {
ADC_Value=HAL_ADC_GetValue(&hadc1);
Voltage=(float)ADC_Value/4095*3.3f;
//3.3:double 双精度浮点数(64 位),3.3f:float 单精度浮点数(32 位)
printf("ADC_Value=%ld\r\n",ADC_Value);//这里用ld比较好,因为ADC_Value是uint32_t的,属于长字节
printf("Voltage: %.3f\r\n",Voltage);
HAL_Delay(500);
}
}
}

int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,10);
return ch;
}

CubeMX的ADC配置

ADC—规则组—多通道—非连续—采集光敏/热敏/电位器数据通过串口打印

循环启动ADC转换,ADC会每次会按顺序读一个通道,比如第一次启动,ADC读Pin4,第二次启动,ADC读Pin5,第三次启动,ADC读Pin6,第四次启动,ADC读Pin4,第五次启动,ADC读Pin5。

先看CubeMX配置:

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint32_t ADC_Value[3];
float Voltage[3];
int main(void)
{
HAL_ADCEx_Calibration_Start(&hadc1);
while(1){
for(int i=0;i<3;i++){
HAL_ADC_Start(&hadc1);
if(HAL_ADC_PollForConversion(&hadc1,50)==HAL_OK){
ADC_Value[i]=HAL_ADC_GetValue(&hadc1);
Voltage[i]=(float)ADC_Value[i]/4095*3.3f;
}
}
printf("Pin4:%.3f,Pin5:%.3f,Pin6:%.3f\r\n",Voltage[0],Voltage[1],Voltage[2]);
HAL_Delay(1000);
}

ADC—规则组+注入组—多通道—非连续—采集光敏/热敏数据通过串口打印

先看CubeMX的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
uint32_t ADC_Value[2];
float Voltage[2];
int main(void)
{
HAL_ADCEx_Calibration_Start(&hadc1); //校准ADC
while(1){
HAL_ADC_Start_IT(&hadc1);
HAL_ADCEx_InjectedStart_IT(&hadc1);
HAL_Delay(1000);
printf("PIN4:%.3f,PIN5:%.3f\r\n",Voltage[0],Voltage[1]);
}
}

//规则组的中断回调函数
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
ADC_Value[0]=HAL_ADC_GetValue(hadc);
Voltage[0]=(float)ADC_Value[0]/4095*3.3f;
}
}

//注入组的中断回调函数
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
ADC_Value[1]=HAL_ADCEx_InjectedGetValue(hadc,ADC_INJECTED_RANK_1);
Voltage[1]=(float)ADC_Value[1]/4095*3.3f;
}
}

int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,10);
return ch;
}

DMA+ADC规则组—多通道—非连续—采集数据串口打印(推荐,写法非常简单)

DMA中断默认开启,DMA中断调用了ADC的回调函数

CubeMX配置

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
uint16_t ADC_Value[2];
float Voltage[2];
int main(void)
{
HAL_ADCEx_Calibration_Start(&hadc1);//校准
while(1){
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ADC_Value, 2);
HAL_Delay(1000);
}
}

//DMA搬运完成后会触发ADC规则组的回调函数,并且把数据直接俄放在了ADC_Value数组中,所以在这里直接转换和输出就可以了
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
Voltage[0]=(float)ADC_Value[0]/4095*3.3f;
Voltage[1]=(float)ADC_Value[1]/4095*3.3f;
printf("PIN4:%.3f,PIN5:%.3f\r\n",Voltage[0],Voltage[1]);
}
}

int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,10);
return ch;
}

TIM通用定时器基础定时中断应用——定时触发中断翻转GPIO电平

CubeMX配置

Slave Mode和Trigger Source是指定时器作为从模式。

就是定时器由外部响应后触发,Trigger Source是指触发源,这里用不到。

Clock Source是指定时器的时钟源,这里选择内部时钟源。

Prescaler是预分频器,72MHz/7200=10KHz,相当于每秒计1w个数,而Counter(CNT)=1000-1,及计1000个数就触发中断。综合相当于0.1秒触发一次中断。

通过定时器实现间隔1000ms翻转GPIO的电平,以及通过HAL库中的宏定义/库函数修改ARR的值来修改定时器的定时周期,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint8_t Mode=0;
uint32_t arr[2]={1000-1,10000-1};

int main(void)
{
HAL_TIM_Base_Start_IT(&htim2);//启动定时器中断
while (1)
{
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_RESET) {
Mode=!Mode;
HAL_TIM_Base_Stop_IT(&htim2); //关闭定时器
__HAL_TIM_SET_AUTORELOAD(&htim2,arr[Mode]); //修改ARR
__HAL_TIM_SET_COUNTER(&htim2,0); //清零CNT
HAL_TIM_Base_Start_IT(&htim2); //开启定时器
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_RESET) {}
}
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim) {
if (htim->Instance == TIM2) {
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}

TIM通用定时器应用———PWM控制SG90舵机

用两个按键去控制舵机旋转特定的角度。

!一开始怎么也跳不通,最后豆包发现是时钟配置不对,配置的频率太低了。

控制SG90,一定要让PWM的周期为20ms,然后设置特定的占空比,来控制SG90。

CubeMX配置

![](/Users/dhwen/Library/Application Support/typora-user-images/image-20260809222048011.png)

这里是使用的PB0,TIM3的3通道作为PWM输出通道,不涉及中断。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define SG90_MIN 500
#define SG90_STEP 500
#define SG90_MAX 2500
uint16_t SG90_Angle=500;
int main(void)
{
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
__HAL_TIM_SET_COMPARE(&htim3 , TIM_CHANNEL_3 , SG90_Angle);//开始给定一个角度45度
while(1){
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_13) == GPIO_PIN_RESET) {
SG90_Angle=SG90_Angle-SG90_STEP;
if (SG90_Angle<=500) {
SG90_Angle=SG90_MIN;
}
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, SG90_Angle);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
HAL_Delay(300);
while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_13) == GPIO_PIN_RESET) {}
}


if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_RESET) {
SG90_Angle=SG90_Angle+SG90_STEP;
if (SG90_Angle>=2500) {
SG90_Angle=SG90_MAX;
}
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, SG90_Angle);
HAL_Delay(300);
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_RESET) {}
}
}
}

TIM触发ADC采集

!!!!!!!!!

Clion默认printf无法输出float数据,需要在cmakelist中添加一行代码

1
2
3
>target_link_options(${PROJECT_NAME} PRIVATE
-u _printf_float
>)

CubeMX配置

定时器设置为TRGO(Trigger Output)外部触发,触发更新事件。

定时器选择规则组,触发源选择定时器3的触发更新事件。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
uint32_t ADC_Value=0;
float Voltage;
float tem;
int main(void)
{
HAL_ADCEx_Calibration_Start(&hadc1); //ADC校准
HAL_ADC_Start(&hadc1); //启动ADC
HAL_TIM_Base_Start(&htim3); //启动TIM
HAL_Delay(2000);
while(1){
if(HAL_ADC_PollForConversion(&hadc1,50)==HAL_OK){
ADC_Value=HAL_ADC_GetValue(&hadc1);
Voltage=((float)ADC_Value)/4095*3.3f;
tem=(1.43f-Voltage)/0.0043f+25;
printf("V: %.3f`c\r\n",Voltage);
printf("tem: %.3f`c\r\n",tem);
}
}
}

int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,10);
return ch;
}

TIM编码器计数

STM32 的编码器接口,本质是定时器的一个硬件工作模式

编码器的核心工作是:系统检测 A/B 相脉冲边沿,硬件定时器自动加减计数。STM32 把这套电路直接做在了通用定时器内部,作为定时器的一种工作模式,因此编码器功能必须依附定时器。

CubeMX配置

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int16_t encoder_cnt=0;
int16_t encoder_last=0;
uint32_t no_move_time=0;

int main(void)
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
//这里的all是指通道1和通道2都打开
while(1){
encoder_cnt=__HAL_TIM_GET_COUNTER(&htim3);//读取当前CNT的值
if (encoder_cnt!=encoder_last) {
no_move_time=HAL_GetTick();//记住现在的时间
int16_t diff = encoder_cnt-encoder_last;//获得相对位移值
if (diff<0) {
printf("逆时针旋转:%d,当前位置:%d\r\n",diff,encoder_cnt);
}
else {
printf("顺时针旋转:%d,当前位置:%d\r\n",diff,encoder_cnt);
}
encoder_last=encoder_cnt;//
}
else {
if (HAL_GetTick()-no_move_time>2000) {
printf("当前编码器静止超过2秒,当前位置: %d\r\n", encoder_cnt);
}
}
if(HAL_GPIO_ReadPin(GPIOA , KEY_IT_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(20); //消抖
if(HAL_GPIO_ReadPin(GPIOA , KEY_IT_Pin) == GPIO_PIN_RESET)
{
printf("SW按钮被按下 \r\n");
while(HAL_GPIO_ReadPin(GPIOA , KEY_IT_Pin) == GPIO_PIN_RESET); //等待松手
}
}
}

int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,10);
return ch;
}

TIM配置超声波测距器

CubeMX配置(PB3——TIM2通道2的直接捕获模式和PA15,推挽输出)

这里通道2为直接捕获,信号源为PB3,而通道1为间接捕获,他正常应该对应PA0,间接捕获意味着他直接复用了另外一个通道的内部信号,也就是通道2的信号,所以他不需要芯片引脚引出。

内部信号流向:
Direct(直接):通道 2 → 读取PB3 硬件引脚 TI2(外部真实引脚)
Indirect(间接):通道 1 不去读 TIM2_CH1 引脚,内部直接拿 TI2(PB3 引脚经过滤波后的内部信号 TI2FP2)给 CH1 的捕获寄存器 CCR1 用。
所以你现在:ECHO 接 PB3 (TIM2_CH2)
CH2:Direct 模式,读取 PB3 真实引脚;
CH1:Indirect 间接模式,内部直接拿 PB3 的信号,不需要 PA0 引脚,PA0 完全可以悬空,不用配置。
👉两个捕获寄存器 CCR1、CCR2,同时读取来自 PB3 这同一个引脚上的 ECHO 脉冲信号。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
uint32_t start_time=0;
uint32_t end_time=0;
float pulse_time=0.0f;
float distance=0.0f;

void TRIG_Start(void) {
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_SET);
//设置TRIG,引脚为高电平,开始发出超声波脉冲
for (uint8_t i=0;i<90;i++) {}
//简单延时,用于产生大约10微妙的脉冲宽度(72MHz)
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_RESET);
//设置TRIG引脚为低电平,完成触发
}

int main(void)
{
while(1){
//清零定时器计数器CNT
__HAL_TIM_SET_COUNTER(&htim2 , 0);
// 启动输入捕获功能(通道1为间接,捕获下降沿;通道2为直接,捕获上升沿)
// 注意:必须在触发前先开启捕获,否则可能错过回波信号
HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_1); // 捕获下降沿
HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_2); // 捕获上升沿
// 发送超声波触发信号
TRIG_Start();
// 等待回波信号上升沿捕获完成(即开始计时)
while(__HAL_TIM_GET_FLAG(&htim2 , TIM_FLAG_CC2) == RESET);
// 清除上升沿中断标志位
__HAL_TIM_CLEAR_FLAG(&htim2 , TIM_FLAG_CC2);
// 读取通道2捕获的上升沿时间(起始时间)
start_time = HAL_TIM_ReadCapturedValue(&htim2 , TIM_CHANNEL_2);
// 等待回波信号下降沿捕获完成(即停止计时)
while(__HAL_TIM_GET_FLAG(&htim2 , TIM_FLAG_CC1) == RESET);
// 清除上升沿中断标志位
__HAL_TIM_CLEAR_FLAG(&htim2 , TIM_FLAG_CC1);
// 读取通道1捕获的下降沿时间(结束时间)
end_time = HAL_TIM_ReadCapturedValue(&htim2 , TIM_CHANNEL_1);
// 停止捕获(为避免干扰下次采样)
HAL_TIM_IC_Stop(&htim2, TIM_CHANNEL_1);
HAL_TIM_IC_Stop(&htim2, TIM_CHANNEL_2);
//计算计数差
uint32_t delta = end_time - start_time;
//换算成脉宽(单位:s)
pulse_width = (float)delta * 1e-6f;
//根据声速换算成距离(单位:m)
distance = pulse_width * 340.0f / 2.0f;
printf("当前距离: %.2f m\r\n" , distance);
//500ms测量一次
HAL_Delay(500);
}
}

TIM补充知识:TIM输入捕获模式,必须用特定的GPIO,具有TIM功能的,以及TIM实现硬件PWM,都需要用特定的GPIO(可以复用为定时器通道的,比如PB0可以复用为TIM3_CH3)

SPI

SPI的4种通讯模式,CPOL为0/1,以及CPHA为0/1,这样就组合出4种模式

收发的逻辑就是等待TXE标志位为1然后写,等待RXNE为1然后读