嵌入式蓝桥杯笔记
一,新建项目
打开 STM32CubeMX,点击 help,Updater Settings,设置配置文件夹路径,将官方的配置放进去
点击 Start My project from MCU-ACCESS TO MCU SELECTOR,开始新建工程
选择芯片,我自己用的是 STM32G431CBT6,比赛用的是 STM32G431RBT6
进入芯片配置界面,依次设置:
RCC->High Speed Clock (HSE)->Crystal Ceramic Resonator(外部晶振)
SYS->Debug->Serial Wire(串行输出)
更改下图中的值,其中系统频率为 80 MHz (80,000,000 Hz,一秒钟振动八千万次)
二、点亮 LED
1. 项目结构说明
为了方便开发和管理代码,我们创建了以下三个自定义文件:
headfile.h:公共头文件,集中包含常用库头文件。
fun.c / fun.h:封装控制 LED 的功能函数。
main.c:主函数中调用功能函数。
2. 公共头文件(headfile.h)
1 2 3 4 5 6 7 8 9 10 11 12 13 #ifndef _headfile_h #define _headfile_h #include "stm32g4xx.h" #include "stdio.h" #include "string.h" #include "stdint.h" #include "main.h" #include "gpio.h" #include "fun.h" #endif
该文件在主函数中被引用,避免多处修改,提高可维护性
3. LED 控制函数
fun.h
1 2 3 4 5 6 #ifndef _fun_h #define _fun_h void led_show (uint8_t led, uint8_t mode) ;#endif
fun.c
1 2 3 4 5 6 7 8 9 10 11 12 #include "headfile.h" void led_show (uint8_t led, uint8_t mode) { HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET); if (mode) HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8 << (led - 1 ), GPIO_PIN_RESET); else HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8 << (led - 1 ), GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET); }
说明:
通过 GPIO_PIN_8 << (led - 1) 实现控制多个 LED(如 LED1~LED8),可以在对应代码位置按下 f12 查看原有定义。
mode 为 1 点亮,0 熄灭。
使用锁存器的使能引脚 PD2 控制 LED 状态稳定写入。
4. 主函数调用(main.c)
1 2 3 4 5 6 7 #include "main.h" #include "gpio.h" #include "headfile.h"
在 while 循环中调用点灯函数,例如点亮 LED1、LED4、LED8:
1 2 3 4 5 while (1 ) { led_show(1 , 1 ); led_show(4 , 1 ); led_show(8 , 1 ); }
注意 :自定义代码需写在 /* USER CODE BEGIN */ 与 /* USER CODE END */ 区域之间,避免被 STM32CubeMX 自动生成代码覆盖。
三,按键
根据上面的文档对按键的定义,需要配置上拉输入(默认未按下按键是高电平(电平被上拉))
多选,对应位置全部改成上拉:
正常生成后,gpio.c 里就会有相关引脚定义
fun.c 要加的代码:
1 2 3 4 5 6 7 8 9 unit8_t B1_state;unit8_t B1_last_state;void key_scan () { B1_state=HAL_GPIO_ReadPin(GPIOB,GPIO_Pin_0); if (B1_state==0 &&B1_last_state==1 ){ led_show(1 ,1 ); } B1_last_state=B1_state; }
fun.h 也要修改:
1 2 3 4 5 6 7 8 #ifndef _fun_h ##define _fun_h #include "stm32g4xx.h" void led_show (unit8_t led,unit8_t mode) ;void key_scan (void ) ;#endif
main.c 里调用按键扫描函数
1 2 3 4 5 while (1 ){ key_scan();
B1 按键按下,第一个 LED 点亮
代码下载之后,按键还没按下,此时 b1 读取高电平,if 不满足,然后把这个高电平赋值给 b1-last-state,一直这样循环,直到我们按下按键,这个时候 b1 等于 0,就点亮了。
边沿检测 :就是按下并且松开 led 才亮,如果不松开就不亮;只检测下降沿
修改 fun.c 里的按键扫描方法,以支持四个按键:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 uint8_t led_flag;uint8_t B1_state,B1_last_state=1 ,B2_state,B2_last_state=1 ,B3_state,B3_last_state=1 ,B4_state,B4_last_state=1 ;void key_scan () { B1_state=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0); B2_state=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1); B3_state=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2); B4_state=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0); if (B1_state==0 &&B1_last_state==1 ){ led_flag ^=1 ; } if (B2_state==0 &&B2_last_state==1 ){ led_flag ^=1 ; } if (B3_state==0 &&B3_last_state==1 ){ led_flag ^=1 ; } if (B4_state==0 &&B4_last_state==1 ){ led_flag ^=1 ; } B1_last_state=B1_state,B2_last_state=B2_state,B3_last_state=B3_state,B4_last_state=B4_state; led_show(1 ,led_flag); }
长按和短按区分
这里涉及到了定时器相关功能,可以先看下面这个视频先理解定时器的工作原理:
【STM32】第 16 集 动画告诉你, STM32 的定时器到底怎么回事
在这个程序中,使用了定时器 TIM3 来判断按键的按下时间,从而区分短按 和长按 。关键参数如下:
参数
值
含义
PSC(预分频器)
8000-1
分频后得到 10kHz 时钟(0.1ms 一次)
ARR(自动重装载值)
65535
最大计数值(约 6.5 秒)
CNT(当前计数值)
0~65535
按键按下持续时间(单位 0.1ms)
所以设置判断 CNT = 10000 就是 1 秒 ,用来区分长短按。
随便选一个基本定时器,选内部时钟,PSC=8000-1,ARR 默认最大值即可
在正常判断逻辑的基础上,再增加判断条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 unit8_t B1_state,B1_last_state=1 ;void key_scan () { B1_state=HAL_GPIO_ReadPin(GPIOB,GPIO_Pin_0); if (B1_state==0 &&B1_last_state==1 ){ TIM3->CNT=0 ; } else if (B1_state==0 &&B1_last_state==0 ){ if (TIM3->CNT>=10000 ){ count++; } } else if (B1_state==1 &&B1_last_state==0 ){ if (TIM3->CNT<10000 ){ count+=2 ; } } B1_last_state=B1_state; }
主函数加上定时器使能,while 循环前的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 MX_GPIO_Init(); LCD_Init(); LCD_Clear(Black); LCD_SetBackcolor(Black); LCD_SetTextColor(White); HAL_TIM_Base_Start(&htim3); while (1 ){
四,LCD
首先 headfile.h 里要引用 lcd.h
其次,做初始化准备(初始化方法通过查询官方 led 函数定义得)
主函数 while 循环前的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 MX_GPIO_Init(); LCD_Init(); LCD_Clear(Black); LCD_SetBackcolor(Black); LCD_SetTextColor(White); while (1 ){
此时编译下载。 LED 灯全亮, 因为 led 和 lcd 共用 io 口,全亮是正常的
fun.h 中声明 LCD 的显示函数:
1 2 3 4 5 6 7 8 9 #ifndef _fun_h ##define _fun_h #include "stm32g4xx.h" void led_show (unit8_t led,unit8_t mode) ;void key_scan (void ) ;void lcd_show (void ) ;#endif
fun.h 中实现 lcd_show 函数
1 2 3 4 5 char text[20 ];void lcd_shoW () { sprinf(text," text " ); LCD_DiaplayStringLine(Line0,(unit8_t *)text); }
为什么要对字符串进行强制类型转换?因为这个函数要输入的参数是 u8 的,u8 是无符号数据,char 有符号
将显示函数放在主函数循环里
1 2 3 4 5 6 while (1 ){ key_scan(); lcd_show();
可以看到,屏幕上显示了 text 字符
按键+LCD
利用按键。 B1 按下 Count 加加。 B2 按下 Count 减减
屏幕显示。第零行 Test。第三行显示 count。
下面是修改好了的 fun.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 52 53 54 #include "headfile.h" int count=0 ;void led_show (unit8_t led,unit8_t mode) { HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET); if (mode) HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8<<(led-1 ),GPIO_PIN_RESSET); else HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8<<(led-1 ),GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET); } unit8_t B1_state;unit8_t B1_last_state;unit8_t B2_state;unit8_t B2_last_state;unit8_t B3_state;unit8_t B3_last_state;unit8_t B4_state;unit8_t B4_last_state;void key_scan () { B1_state=HAL_GPIO_ReadPin(GPIOB,GPIO_Pin_0); B2_state=HAL_GPIO_ReadPin(GPIOB,GPIO_Pin_1); B3_state=HAL_GPIO_ReadPin(GPIOB,GPIO_Pin_2); B4_state=HAL_GPIO_ReadPin(GPIOA,GPIO_Pin_0); if (B1_state==0 &&B1_last_state==1 ){ count++; } if (B2_state==0 &&B2_last_state==1 ){ count--; } if (B3_state==0 &&B3_last_state==1 ){ led_show(2 ,1 ); } if (B4_state==0 &&B4_last_state==1 ){ led_show(2 ,0 ); } B1_last_state=B1_state; B2_last_state=B2_state; B3_last_state=B3_state; B4_last_state=B4_state; } char text[20 ];void lcd_shoW () { sprinf(text," text " ); LCD_DiaplayStringLine(Line0,(unit8_t *)text); sprinf(text," count:%d " ,count); LCD_DiaplayStringLine(Line3,(unit8_t *)text); }
LCD 高亮显示
在原有基础上加一个标志位,记录那一行用了高亮
LED 和 LCD 引脚冲突问题
在 LCD 设置前。将引脚配置成低电平
每一次在主函数循环之前锁住 led 灯相关的寄存器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 LCD_Init(); LCD_Clear(Black); LCD_SetBackColor(Black); LCD_SetTextColor(White); MX_GPIO_Init(); HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET); while (1 ) {
或者写下面这个版本,遍历所有灯状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 uint8_t led_state[8 ]={0 };void led (uint8_t led, uint8_t mode) { led_state[led] = mode; for (uint8_t i = 0 ; i < 8 ; i++) { HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET); if (led_state[i]) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8 << i, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8 << i, GPIO_PIN_SET); } HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET); } }
五,定时器中断实现 LED 闪烁
原理解释 :
在这个程序中,使用了定时器 TIM2 来判断按键的按下时间,从而区分短按 和长按 。关键参数如下:
参数
值
含义
PSC(预分频器)
8000-1
分频后得到 10kHz 时钟(0.1ms 一次)
ARR(自动重装载值)
10000-1
最大计数值(约 1 秒)
所以设置判断 ARR = 10000-1 就是 1 秒 ,用来每隔一秒自动调用中断回调函数。
随便选一个基本定时器,选内部时钟,PSC=8000-1,ARR 设置为 10000-1 即可
使中断使能
找到中断函数
在 headfile 里加入 tim.h
在 fun.c 里,实现回调函数,每隔一秒钟 count++
1 2 3 4 5 void HAL_TIM_PeriodElapsedCallback (TIM_HandleTypeDef *htim) { if (htim->Instance==TIM2){ count++; } }
在主函数进行计时器初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 MX_GPIO_Init(); MX_TIM4_Init(); MX_TIM2_Init(); HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET); HAL_TIM_Base_Start_IT(&htim2); while (1 ) { lcdshow(); key_scan();
中断函数不建议执行耗时间的调用
例如这个 led_show 函数,若将相关逻辑放在中断函数中
会全部闪亮,出现和之前一样的状态
因为控制 led 相关寄存器的代码写在循环里的,如果在还没有锁 led 寄存器的时候进入中断函数,就会出现 LED 和 LCD 引脚冲突问题
六,PWM
psc=800-1, arr=100-1, ccr=50
PSC = 799 : 预分频值为 799。
ARR = 99 : 自动重载值为 99。
CCR = 50 : 捕获/比较寄存器值为 50。
计算定时器计数时钟频率 :
Counter Clock = 80,000,000 Hz / (799 + 1) = 80,000,000 Hz / 800 = 100,000 Hz (即 100 kHz)
计算 PWM 频率 : PWM 频率由 ARR 决定。
PWM Frequency = Counter Clock / (ARR + 1) = 100,000 Hz / (99 + 1) = 100,000 Hz / 100 = 1,000 Hz (即 1 kHz)
计算 PWM 占空比 (Duty Cycle) : 占空比由 CCR 和 ARR 决定 (假设为 PWM 模式 1 或 2,向上计数)。
Duty Cycle = CCR / (ARR + 1) = 50 / (99 + 1) = 50 / 100 = 0.5 = 50%
目的/原因 : 这个配置是为了生成一个频率为 1 kHz,占空比为 50% 的 PWM 波 。
PSC (799) 将 80 MHz 分频到 100 kHz。ARR (99) 设定了 PWM 的周期长度为 100 个计数时钟周期 (100 / 100,000 Hz = 0.001 秒,即 1 kHz)。
CCR (50) 设定了在一个 PWM 周期内,输出高电平(或低电平,取决于 PWM 模式)的持续时间为 50 个计数时钟周期,占总周期 100 的一半,因此是 50% 的占空比
先按之前的操作建立一个空工程
选择 PA1 引脚,设置定时器 TIM2CH2(确保其他东西没有选这个定时器)
这里设置 PSC=800-1;ARR=100-1;
在主函数 while 循环附近,加上代码:
1 2 3 4 HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2); TIM2->CCR2=50 ;
编译运行,测 PA1,可以看到输出了 1000HZ 的方波
原理:比正常定时器多了个 CCR(compare,比较)字段,用于控制高电平占整个周期的频率
.
七,输入捕获测量 PWM 频率
PSC = 79 : 预分频值为 79。
计算定时器计数时钟频率 :
Counter Clock = 80,000,000 Hz / (79 + 1) = 80,000,000 Hz / 80 = 1,000,000 Hz (即 1 MHz)
定时器计数周期 :
Counter Period = 1 / Counter Clock = 1 / 1,000,000 Hz = 1 microsecond (µs)
理解捕获 : 输入捕获模式下,当指定的输入引脚检测到信号边沿(上升沿或下降沿)时,定时器当前的计数值 (CNT) 会被锁存到捕获/比较寄存器 (CCR) 中。
频率测量原理 : 通过捕获两次连续相同边沿 之间的时间,可以计算输入信号的周期,进而得到频率。
假设两次捕获的值分别为 CCR1 和 CCR2。
两次捕获之间的计数值差 Delta_Counts = CCR2 - CCR1 (需要考虑计数器溢出)。
输入信号的周期 T_input = Delta_Counts * Counter Period = Delta_Counts * 1 µs。
输入信号的频率 f_input = 1 / T_input = 1 / (Delta_Counts * 1 µs) = 1,000,000 / Delta_Counts Hz。
代码中计算频率的公式 : 频率 = 80000000 / (80 * 两次捕获之间的计数值差)
这里的 80000000 是系统时钟。
这里的 80 是 PSC + 1。
公式为 f_input = 80,000,000 / (80 * Delta_Counts)
f_input = (80,000,000 / 80) / Delta_Counts
f_input = 1,000,000 / Delta_Counts Hz。
此公式等价于用 1 MHz 的计数器时钟频率除以捕获到的计数值差来计算输入信号的频率。
目的/原因 : 这个配置将定时器的计数频率设置为 1 MHz,意味着每次计数代表 1 微秒 。这提供了一个较高的时间分辨率,适合测量上面配置的微秒级别的脉冲宽度或较高频率的信号。选择 PSC=79 是为了从 80 MHz 得到一个整数且方便计算的 1 MHz 计数频率。
用 PA7 引脚测量输出频率
这里的 tim17_Ch1
激活选择输入捕获
PSC 设置为 80-1
.
启用中断
.
留下显示函数,用于验证是否获取频率成功
.
在主函数 while 循环附近,加上代码:
1 2 3 4 5 6 7 HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET); HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2); TIM2->CCR2=50 ; HAL_TIM_IC_Start_IT(&htim17,TIM_CHANNEL_1);
加入输入中断的回调函数
按 CTRL+F 在 hal 库代码头文件里寻找 capture
.
在 fun.c 中实现回调函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 char text[20 ];void lcdshow () { sprintf (text," text " ); LCD_DisplayStringLine(Line0,(uint8_t *)text); sprintf (text," fre:%d " ,fre); LCD_DisplayStringLine(Line3,(uint8_t *)text); } uint32_t fre,capture_value;void HAL_TIM_PeriodElapsedCallback (TIM_HandleTypeDef *htim) { if (htim->Instance==TIM17){ capture_value=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1); TIM17->CNT=0 ; fre=8000 0000 /(80 *capture_value); } }
在主函数调用,并初始化 LCD
1 2 3 4 5 6 7 8 9 10 11 12 13 14 HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET); HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2); TIM2->CCR2=50 ; HAL_TIM_IC_Start_IT(&htim17,TIM_CHANNEL_1); while (1 ) { lcdshow();
.
fre 是 0 的看看是不是中断使能那个函数用错了,要带 IT (中断)的才行
频率显示 0 的检查一下有没有加使能函数
是零看看初始化函数位置放对没,放在 tim2 和 tim17 初始化之后
没成果的可以重新检查一下代码和配置(比如端口的模式)有没有对
这里的 led 怎么全亮了?
PD2 的引脚没初始化
PD2 没有初始化,默认高电平,初始化后才能写低电平
八,输入捕获两个 555 定时器输出的频率
PA15 引脚选择 TIM2_CH1
.
PB4 引脚选择 TIM16_CH1
.
TIM2 选输入捕获直接模式,PSC 设置 80-1,开启中断
.
TIM16_CH1 选择 Activate,选择输入捕获直接模式,PSC=80-1
.
fun.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 char text[20 ];uint32_t fre1,capture_value1,fre2,capture_value2;void lcdshow () { sprintf (text," text " ); LCD_DisplayStringLine(Line0,(uint8_t *)text); sprintf (text," count:%d " ,count); LCD_DisplayStringLine(Line2,(uint8_t *)text); sprintf (text," R39fre:%d " ,fre1); LCD_DisplayStringLine(Line3,(uint8_t *)text); sprintf (text," R40fre:%d " ,fre2); LCD_DisplayStringLine(Line4,(uint8_t *)text); } void HAL_TIM_PeriodElapsedCallback (TIM_HandleTypeDef *htim) { if (htim->Instance==TIM16){ capture_value1=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1); TIM16->CNT=0 ; fre1=8000 0000 /(80 *capture_value1); } if (htim->Instance==TIM2){ capture_value2=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1); TIM2->CNT=0 ; fre2=8000 0000 /(80 *capture_value2); } }
主函数代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 LCD_Init(); LCD_Clear(Black); LCD_SetBackColor(Black); LCD_SetTextColor(White); HAL_TIM_PWM_Start_IT(&htim2,TIM_CHANNEL_1); HAL_TIM_PWM_Start_IT(&htim16,TIM_CHANNEL_1); while (1 ) { lcdshow();
调整旋钮,可以看到值在变化:
。
九,ADC 测量
PB15 选 adc2in15
pb12 选 ADC1in11
都选 single-ended
headfile.h 引入 adc.h
fun.c:
1 2 3 4 5 6 7 8 9 void lcdshow () { sprintf (text," text " ); LCD_DisplayStringLine(Line0,(uint8_t *)text); HAL_ADC_Start(&hadc1); uint32_t adc_value = HAL_ADC_GetValue(&hadc1); sprintf (text," value:%d " ,adc_value); LCD_DisplayStringLine(Line2,(uint8_t *)text); }
主函数代码:
1 2 3 4 5 6 7 8 9 LCD_Init(); LCD_Clear(Black); LCD_SetBackColor(Black); LCD_SetTextColor(White); while (1 ) { lcdshow();
可以看到屏幕上的值在 0-4096 之间变化
.
添加电压转换函数
adc 值 = 3.3 * adc 捕获值 / 4096
原理 :
3.3 : 代表 ADC 的参考电压 Vref+ 是 3.3 V。这是 ADC 能够测量的最大电压(或电压范围的上限)。
adc 捕获值 : 这是 ADC 完成一次转换后读取到的原始数字值。
4096 : 代表 ADC 的分辨率级别数。STM32 上常见的 ADC 是 12 位的,其输出范围是 0 到 2^12 - 1,即 0 到 4095。因此总共有 4096 个级别。
公式解释 : 这个公式是将 ADC 读到的数字值(0-4096 范围)线性地映射到实际的模拟电压值(0-3.3V 范围)。
(adc捕获值 / 4096)表示当前读数占 ADC 满量程的比例。
将这个比例乘以参考电压 (3.3V),就得到了对应的模拟输入电压值。
fun.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 void lcdshow () { sprintf (text," text " ); LCD_DisplayStringLine(Line0,(uint8_t *)text); sprintf (text," R37_VOL:%d " ,get_vol(&hadc2)); LCD_DisplayStringLine(Line2,(uint8_t *)text); sprintf (text," R38_VOL:%d " ,get_vol(&hadc1)); LCD_DisplayStringLine(Line3,(uint8_t *)text); } double get_vol (ADC_HandleTypeDef *hadc) { HAL_ADC_Start(&hadc1); uint32_t adc_value = HAL_ADC_GetValue(&hadc1); return 3.3 *adc_value/4096 ; }
在 headfile.h 里声明函数
1 double get_vol (ADC_HandleTypeDef *hadc) ;
主函数代码:
1 2 3 4 5 6 7 8 9 LCD_Init(); LCD_Clear(Black); LCD_SetBackColor(Black); LCD_SetTextColor(White); while (1 ) { lcdshow();
九,串口通信
串口发送数据
设置为异步通信
PC4,PC5 改为 PA9,PA10
中断使能作用
后面用于接收的
主函数循环附近的代码如下:
1 2 3 4 5 6 7 8 while (1 ){ char text1[20 ];sprintf (text1,"hahahaha\r\n" );HAL_UART_Transmit(&huart1,(uint8_t *)text1,sizeof (text1),50 ); HAL_Delay(1000 );
串口调试
查找电脑端口,此电脑,管理查找
波特率和之前保持一致,115200
比赛中一般为 9600/115200
.
串口中断接收配置
在 fun.c 里实现串口中断回调函数(可用 ctrl+f 在 uart.h 里找)
1 2 3 4 5 6 7 uint8_t rec_data;void HAL_UART_RxCpltCallback (UART_HandleTypeDef *huart) { if (huart->Instance == USART1){ HAL_UART_Transmit(huart,&rec_data,1 ,50 ); HAL_UART_Receive_IT(huart,&rec_data,1 ); } }
注意,在 fun.h 里,声明接收数据变量为全局变量:
1 2 3 4 5 6 7 8 9 #ifndef __fun_H__ #define __fun_H__ #include "headfile.h" #include "stm32g4xx.h" void led_show (uint8_t led,uint8_t mode) ;void key_scan (void ) ;void lcdshow (void ) ;extern uint8_t rec_data;#endif
在主函数,要启用对应的串口中断
1 2 3 4 HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET); HAL_UART_Receive_IT(&huart1,&rec_data,1 );
利用定时器进行串口非定长数据接收,并处理数据
串口接收:
每次进入中断只能接收一个字节的数据
考虑错误情况:
每次接收一个字节就要判断该字节是否符合要求
接收完所有字符,判断字符串是否符合要求
解决方法:
利用定时器,处理串口接收
串口波特率=9600 bit/s 就是 1s 可以传输 9600bit
串口传输一次数据包含起始位,数据位,结束位,一共 10bit
10 *1/9600 = 0.00104s =1.04ms
头文件与变量定义
1 2 3 4 5 6 7 #include "headfile.h" char send_buff[20 ]; uint8_t rec_data; uint8_t count; uint8_t rec_flag; uint8_t rec_buff[20 ];
串口接收中断回调函数
1 2 3 4 5 6 7 8 void HAL_UART_RxCpltCallback (UART_HandleTypeDef *huart) { if (huart->Instance == USART1){ TIM4->CNT = 0 ; rec_flag = 1 ; rec_buff[count++] = rec_data; HAL_UART_Receive_IT(huart, &rec_data, 1 ); } }
串口数据处理函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void uart_data_rec () { if (rec_flag && TIM4->CNT > 15 ){ if (rec_buff[0 ]=='l' && rec_buff[1 ]=='a' && rec_buff[2 ]=='n' ) sprintf (send_buff, "lan\r\n" ); else if (rec_buff[0 ]=='q' && rec_buff[1 ]=='i' && rec_buff[2 ]=='a' && rec_buff[3 ]=='o' ) sprintf (send_buff, "qiao\r\n" ); else if (rec_buff[0 ]=='b' && rec_buff[1 ]=='e' && rec_buff[2 ]=='i' ) sprintf (send_buff, "bei\r\n" ); else sprintf (send_buff, "error!\r\n" ); HAL_UART_Transmit(&huart1, (uint8_t *)send_buff, sizeof (send_buff), 50 ); rec_flag = 0 ; memset (rec_buff, 0 , count); count = 0 ; } }
整体来看,这段代码实现了 USART1 串口数据的中断接收、基于定时器的接收完成判断以及按规则对接收数据的处理与回复功能。
可以在 if 最后加上 &&rec_buff[最后一位 + 1]==0,避免输入 lanqiao 也会返回 lan
可以在 if 最后加上 &&rec_buff[最后一位 + 1]==0,避免输入 lanqiao 也会返回 lan
。
会只输出 lan 因为对后面的数据不再进行判断
十,用 I2C 协议对 eeprom 读写
这段 C 语言代码,用于向 EEPROM 写入数据,函数借助 I2C 通信协议来实现操作
下面加的两个函数需要在对应头文件里声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void eeprom_write (uint8_t addr, uint8_t dat) { I2CStart(); I2CSendByte(0xa0 ); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); I2CSendByte(dat); I2CWaitAck(); I2CStop(); HAL_Delay(20 ); }
在 eeprom 设备手册里,可以了解到:
eeprom_read 函数:
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 uint8_t eeprom_read (uint8_t addr) { I2CStart(); I2CSendByte(0xa0 ); I2CWaitAck(); I2CSendByte(addr); I2CWaitAck(); I2CStop(); I2CStart(); I2CSendByte(0xa1 ); I2CWaitAck(); uint8_t dat = I2CReceiveByte(); I2CSendNotAck(); I2CStop(); return dat; }
主函数代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 LCD_Init(); LCD_SetTextColor(White); LCD_SetBackColor(Black); LCD_Clear(Black); I2CInit(); eeprom_write(0 ,10 ); uint8_t dat = eeprom_read(0 ); char text[20 ]; while (1 ){ sprintf (text, "%d" , dat); LCD_DisplayStringLine(Line0, (uint8_t *)text); }
总结:初始化外设 → 从 EEPROM 读取数据 → 将数据循环显示在 LCD 屏幕上
未存储时,显示默认值 255
.
存储后,显示存储的内容
.
十一,rtc 实时时钟
主要功能实现:
1.设置时间和日期
2.读取时间和日期
3.设置一个闹钟
fun.c 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 char text[20 ];RTC_TimeTypeDef sTime = {0 }; RTC_DateTypeDef sDate = {0 }; void lcd_show () { HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN); sprintf (text, "test" ); LCD_DisplayStringLine(Line0, (uint8_t *)text); sprintf (text, "%2d:%2d:%2d" , sTime.Hours, sTime.Minutes, sTime.Seconds); LCD_DisplayStringLine(Line1, (uint8_t *)text); sprintf (text, "%d-%d-%d-%d" , sDate.Year, sDate.Month, sDate.Date, sDate.WeekDay); LCD_DisplayStringLine(Line2, (uint8_t *)text); led_show(1 , led_mode); } void HAL_RTC_AlarmAEventCallback (RTC_HandleTypeDef *hrtc) { led_mode=1 ; }
灯点不亮的看看主函数 rtc 有没有使能
完结撒花!!!!!
📘 单独补充:STM32 定时器知识要点
一、用到的定时器分类
定时器类型
特点
用途举例
基本定时器(TIM6/TIM7)
无输入输出通道,只能内部计数
延时、中断、DAC 触发
通用定时器(TIM2~TIM5)
有多个通道,可用于 PWM、输入捕获、编码器等
PWM 控制、电机驱动等
二、核心参数公式
1. PWM 频率
f PWM = f timer_clk ( P S C + 1 ) × ( A R R + 1 ) f_{\text{PWM}} = \frac{f_{\text{timer\_clk}}}{(PSC + 1) \times (ARR + 1)}
f PWM = ( P S C + 1 ) × ( A R R + 1 ) f timer_clk
PWM频率 = 定时器时钟频率 ( 预分频器 + 1 ) × ( 自动重装值 + 1 ) \text{PWM频率} = \frac{\text{定时器时钟频率}}{(\text{预分频器}+1) \times (\text{自动重装值}+1)}
PWM 频率 = ( 预分频器 + 1 ) × ( 自动重装值 + 1 ) 定时器时钟频率
2. PWM 占空比
Duty Cycle = C C R A R R + 1 × 100 % \text{Duty Cycle} = \frac{CCR}{ARR + 1} \times 100\%
Duty Cycle = A R R + 1 C C R × 1 0 0 %
占空比 = 比较寄存器值(CCR) 自动重装值(ARR) + 1 × 100 % \text{占空比} = \frac{\text{比较寄存器值(CCR)}}{\text{自动重装值(ARR)}+1} \times 100\%
占空比 = 自动重装值( ARR ) + 1 比较寄存器值( CCR ) × 1 0 0 %
3. 基本定时器中断周期
T = ( P S C + 1 ) × ( A R R + 1 ) f timer_clk T = \frac{(PSC + 1) \times (ARR + 1)}{f_{\text{timer\_clk}}}
T = f timer_clk ( P S C + 1 ) × ( A R R + 1 )
定时周期(秒) = ( 预分频器 + 1 ) × ( 自动重装值 + 1 ) 定时器时钟频率 \text{定时周期(秒)} = \frac{(\text{预分频器}+1) \times (\text{自动重装值}+1)}{\text{定时器时钟频率}}
定时周期(秒) = 定时器时钟频率 ( 预分频器 + 1 ) × ( 自动重装值 + 1 )
三、常用寄存器
寄存器详细说明
寄存器
说明
详细解释
PSC
预分频器(Prescaler)
定时器的预分频器,决定定时器计数的频率。通过将系统时钟(timer_clk)除以 PSC + 1,可以降低定时器的计数频率。PSC 设置的数值越大,定时器计数速度越慢。
ARR
自动重装寄存器(Auto-Reload Register)
自动重装寄存器,决定定时器计数的周期。定时器从 0 数到 ARR 的值后会自动重置为 0,重新开始计数,形成一个周期。这个周期通常决定 PWM 输出的频率(PWM频率 = 1 / 周期)。
CNT
当前计数值(Counter)
当前计数器的值,表示定时器已经计数的时间。CNT 是一个递增的计数器,从 0 开始,直到它达到 ARR 的值。当计数值达到 ARR 时,定时器会溢出并重新从 0 开始。
CCRx
比较寄存器(Capture/Compare Register)
用于输出比较的寄存器。它的值控制了定时器的输出事件,比如 PWM 的高电平持续时间。当定时器的计数值(CNT)等于 CCRx 的值时,定时器会触发一个事件(如输出高电平或低电平)。在 PWM 模式下,CCRx 决定了高电平的持续时间。
CR1
控制寄存器(Control Register 1)
控制定时器的基本功能,如定时器启停、计数方向等。它用于配置定时器的工作模式,比如选择向上计数或向下计数,是否启用定时器等。常用的配置位有:CEN(计数器使能),DIR(计数方向),UDIS(更新使能)等。
寄存器之间的关系:
PSC 和 ARR 配合使用,控制了定时器的计数频率和周期。PSC 控制定时器的频率,ARR 控制计数器的周期。
CNT 是定时器的实际计数值,随着时钟和设置的 PSC、ARR 进行增减,当 CNT 达到 ARR 时,定时器溢出并触发相应事件。
CCRx 用于控制 PWM 输出时的占空比或定时器的输出比较。当计数器 CNT 与 CCRx 相等时,定时器会触发输出事件(比如切换引脚的电平)。
CR1 用于开启定时器以及选择定时器的工作模式。
四、基础功能总结
项目
基本定时器
通用定时器
输出 PWM
❌ 不支持
✅ 支持
中断功能
✅ 支持
✅ 支持
复用引脚输出
❌ 无
✅ 可映射到 IO 口输出
常见用途
延时、中断
PWM、捕获、测频