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);
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_13);
HAL_Delay(1000);
|
USART串口的基本函数
1 2 3 4 5
| HAL_UART_Transmit(&huart1,msg,2,HAL_MAX_DELAY);
HAL_UART_Receive(&huart1,msg,2,HAL_MAX_DELAY);
|
串口发送长字节数组
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; }
|
串口——中断接收定长数据

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); 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); OLED_ShowNum(1, 1, IntValue,3); } else if (strcmp(Name,"2")==0) { float FloatValue=atof(Value); } } 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); } Serial_RxFlag = 0; } }
|
ADC—规则组—单通道—非连续—采集电位器数据通过串口打印
修改Cmakelists.txt
为了支持printf输出%f,%.3f等类型数据,如果不修改Cmakelist,在串口中打印的%f将是空白的。
1 2 3 4
| 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); 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; printf("ADC_Value=%ld\r\n",ADC_Value); 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); 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); } }
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]); __HAL_TIM_SET_COUNTER(&htim2,0); 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配置


这里是使用的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); 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); HAL_ADC_Start(&hadc1); HAL_TIM_Base_Start(&htim3); 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); while(1){ encoder_cnt=__HAL_TIM_GET_COUNTER(&htim3); 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); for (uint8_t i=0;i<90;i++) {} HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_RESET); }
int main(void) { while(1){ __HAL_TIM_SET_COUNTER(&htim2 , 0); 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); 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); 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; pulse_width = (float)delta * 1e-6f; distance = pulse_width * 340.0f / 2.0f; printf("当前距离: %.2f m\r\n" , distance); 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然后读