蓝桥杯嵌入式笔记

嵌入式蓝桥杯笔记

一,新建项目

  1. 打开STM32CubeMX,点击help,Updater Settings,设置配置文件夹路径,将官方的配置放进去
  2. 点击 Start My project from MCU-ACCESS TO MCU SELECTOR,开始新建工程
  3. 选择芯片,我自己用的是STM32G431CBT6,比赛用的是STM32G431RBT6
  4. 进入芯片配置界面,依次设置:
    • RCC->High Speed Clock (HSE)->Crystal Ceramic Resonator(外部晶振)
    • SYS->Debug->Serial Wire(串行输出)
    • 更改下图中的值,其中系统频率为 80 MHz (80,000,000 Hz,一秒钟振动八千万次)
    • image-20250401220116028
    • image-20250401220423306

二、点亮 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);
// 控制 LED
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查看原有定义。
  • mode1 点亮,0 熄灭。
  • 使用锁存器的使能引脚 PD2 控制 LED 状态稳定写入。

4. 主函数调用(main.c)

1
2
3
4
5
6
7
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"

/* USER CODE BEGIN Includes */
#include "headfile.h"
/* USER CODE END Includes */

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 自动生成代码覆盖。

三,按键

根据上面的文档对按键的定义,需要配置上拉输入(默认未按下按键是高电平(电平被上拉))

image-20250404182759516

多选,对应位置全部改成上拉:

正常生成后,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){//按键B1按下
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
/* USER CODE BEGIN WHILE */
while (1)
{
key_scan();
/* USER CODE END WHILE */

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){//按键B1按下
TIM3->CNT=0;//计数器清零
}
else if(B1_state==0&&B1_last_state==0){//按键B1一直按下
if(TIM3->CNT>=10000){//按键b1长按,计数器数了一万次,约等于现实的1ms
count++;
}
}
else if(B1_state==1&&B1_last_state==0){//按键B1松开
if(TIM3->CNT<10000){//按键b1短按
count+=2;
}
}

//中间再写四个if写成其他按键的判断逻辑

B1_last_state=B1_state;
}

主函数加上定时器使能,while循环前的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
LCD_Init(); // 初始化 LCD
LCD_Clear(Black); // LCD 清屏为黑色背景
LCD_SetBackcolor(Black); // 设置文字背景为黑色
LCD_SetTextColor(White); // 设置文字颜色为白色

HAL_TIM_Base_Start(&htim3); //启动化计数器
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{

四,LCD

首先headfile.h里要引用lcd.h

其次,做初始化准备(初始化方法通过查询官方led函数定义得)

主函数while循环前的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
LCD_Init(); // 初始化 LCD(建议加在gpio初始化的前面)
LCD_Clear(Black); // LCD 清屏为黑色背景
LCD_SetBackcolor(Black); // 设置文字背景为黑色
LCD_SetTextColor(White); // 设置文字颜色为白色
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
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];//一行只能显示20个字符
void lcd_shoW(){
sprinf(text," text ");//拷贝后面的一串字符到前面字符串里
LCD_DiaplayStringLine(Line0,(unit8_t *)text);//在第一行显示这个字符串
}

为什么要对字符串进行强制类型转换?因为这个函数要输入的参数是 u8 的,u8是无符号数据,char有符号

将显示函数放在主函数循环里

1
2
3
4
5
6
/* USER CODE BEGIN WHILE */
while (1)
{
key_scan();
lcd_show();
/* USER CODE END WHILE */

可以看到,屏幕上显示了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){//按键B1按下
count++;//计数++
}
if(B2_state==0&&B2_last_state==1){//按键B2按下
count--;//计数--
}
if(B3_state==0&&B3_last_state==1){//按键B3按下
led_show(2,1);
}
if(B4_state==0&&B4_last_state==1){//按键B4按下
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];//一行只能显示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
 /* USER CODE BEGIN SysInit */
LCD_Init();
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
/* USER CODE END SysInit */

/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);//关掉相关寄存器
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
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 闪烁

原理解释

  • PSC = 7999: 预分频值为 7999。

  • ARR = 9999: 自动重载值为 9999。

  • 计算定时器计数时钟频率:

    • Counter Clock = 80,000,000 Hz / (7999 + 1) = 80,000,000 Hz / 8000 = 10,000 Hz (即 10 kHz)
  • 计算定时器更新事件频率:

    • Update Frequency = Counter Clock / (ARR + 1) = 10,000 Hz / (9999 + 1) = 10,000 Hz / 10000 = 1 Hz
  • 计算定时器更新事件周期:

    • Period = 1 / Update Frequency = 1 / 1 Hz = 1 second
  • 目的/原因:

    这个配置是为了让定时器每隔 1 秒钟产生一次更新事件(例如触发一次中断)。

    1. 首先通过较大的 PSC (7999) 将 80 MHz 的高速时钟乘以8000,分频到一个较低、好计算的频率 (10 kHz),
    2. 然后通过 ARR (9999) 设置计数器从 0 数到 9999(总共 10000 个计数周期),刚好是 10000 次 10000 Hz = 1 秒。

在这个程序中,使用了定时器 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
 /* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM4_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
HAL_TIM_Base_Start_IT(&htim2);//触发中断
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
lcdshow();
key_scan();
/* USER CODE END WHILE */

中断函数不建议执行耗时间的调用

例如这个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 波
    1. PSC (799) 将 80 MHz 分频到 100 kHz。ARR (99) 设定了 PWM 的周期长度为 100 个计数时钟周期 (100 / 100,000 Hz = 0.001 秒,即 1 kHz)。
    2. CCR (50) 设定了在一个 PWM 周期内,输出高电平(或低电平,取决于 PWM 模式)的持续时间为 50 个计数时钟周期,占总周期 100 的一半,因此是 50% 的占空比

先按之前的操作建立一个空工程

选择PA1引脚,设置定时器TIM2CH2(确保其他东西没有选这个定时器)

这里设置PSC=800-1;ARR=100-1;

image-20250406202158493

在主函数while循环附近,加上代码:

1
2
3
4
 /* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);//启动PWM输出
TIM2->CCR2=50;//高电平计数50
/* USER CODE END 2 */

编译运行,测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 是系统时钟。
    • 这里的 80PSC + 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

.image-20250406205122603

启用中断

.

留下显示函数,用于验证是否获取频率成功

.

在主函数while循环附近,加上代码:

1
2
3
4
5
6
7
 /* USER CODE BEGIN 2 */
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);//输入捕获中断启动
/* USER CODE END 2 */

加入输入中断的回调函数

按 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
  /* USER CODE BEGIN 2 */
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
// HAL_TIM_Base_Start_IT(&htim2);
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);
TIM2->CCR2=50;

HAL_TIM_IC_Start_IT(&htim17,TIM_CHANNEL_1);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
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){//频率输出1 R39
capture_value1=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
TIM16->CNT=0;
fre1=8000 0000/(80*capture_value1);
}
if(htim->Instance==TIM2){//频率输出2 R40
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
 /* USER CODE BEGIN 2 */
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);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
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);//R30
sprintf(text," value:%d ",adc_value);
LCD_DisplayStringLine(Line2,(uint8_t *)text);
}

主函数代码:

1
2
3
4
5
6
7
8
9
  /* USER CODE BEGIN SysInit */
LCD_Init();
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
/* USER CODE END SysInit */
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);//R30
return 3.3*adc_value/4096;
}

在headfile.h里声明函数

1
double get_vol(ADC_HandleTypeDef *hadc);

主函数代码:

1
2
3
4
5
6
7
8
9
  /* USER CODE BEGIN SysInit */
LCD_Init();
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
/* USER CODE END SysInit */
while (1)
{
lcdshow();

九,串口通信

串口发送数据

设置为异步通信

PC4,PC5 改为 PA9,PA10

中断使能作用

后面用于接收的

主函数循环附近的代码如下:

1
2
3
4
5
6
7
8
/* USER CODE BEGIN WHILE */
while (1)
{
char text1[20];
sprintf(text1,"hahahaha\r\n");
HAL_UART_Transmit(&huart1,(uint8_t *)text1,sizeof(text1),50);
HAL_Delay(1000);
/* USER CODE END WHILE */

串口调试

查找电脑端口,此电脑,管理查找

波特率和之前保持一致,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" // Device header
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
 /* USER CODE BEGIN 2 */
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
HAL_UART_Receive_IT(&huart1,&rec_data,1);
/* USER CODE END 2 */

利用定时器进行串口非定长数据接收,并处理数据

串口接收:

  • 每次进入中断只能接收一个字节的数据

  • 考虑错误情况:

    1. 每次接收一个字节就要判断该字节是否符合要求

    2. 接收完所有字符,判断字符串是否符合要求

解决方法:

  • 利用定时器,处理串口接收
  • 串口波特率=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){ // 若接收到数据并且超时判断通过,超时判断比1.04多0.5毫秒左右
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)//参数列表为要写入数据的rom地址和要写入地址的数据
{
I2CStart();//准备发送

I2CSendByte(0xa0);//找rom设备,地址是a0
I2CWaitAck();//等待设备发送确认信号

I2CSendByte(addr);//要向rom设备地址为addr的位置写数据
I2CWaitAck();//等待设备发送确认信号

I2CSendByte(dat);//向该地址写入数据
I2CWaitAck();//等待设备发送确认信号

I2CStop();//结束本次通信

HAL_Delay(20);//写入数据需要时间
}

在eeprom设备手册里,可以了解到:

  • 0xa0 为写的操作
  • 0xa1 为读的操作

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)//参数为要读取数据的ROM地址
{
//设置读取地址阶段
I2CStart(); //开始通信,准备发送写地址

I2CSendByte(0xa0); //发送EEPROM写地址
I2CWaitAck(); //等待确认

I2CSendByte(addr); //发送要读取数据的地址
I2CWaitAck(); //等待确认

I2CStop(); //结束写地址设置


//数据读取阶段
I2CStart(); //重新开始通信,准备读取数据

I2CSendByte(0xa1); //发送EEPROM读地址
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
  /* USER CODE BEGIN 2 */
LCD_Init(); // 初始化LCD
LCD_SetTextColor(White); // 设置字体为白色
LCD_SetBackColor(Black); // 设置背景为黑色
LCD_Clear(Black); // 黑色清屏
I2CInit(); // 初始化I2C总线

eeprom_write(0,10);//向第零个地址写入数据10
uint8_t dat = eeprom_read(0); // 读取地址0的数据
char text[20]; // 显示用字符串缓冲区
/* USER CODE END 2 */

while (1){
sprintf(text, "%d", dat); // 数据转为字符串
LCD_DisplayStringLine(Line0, (uint8_t *)text); // 显示在LCD第0行
}

总结:初始化外设 → 从 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)//这是一个 RTC 闹钟事件的回调函数
{
led_mode=1;//控制灯的翻转
}

灯点不亮的看看主函数 rtc 有没有使能

完结撒花!!!!!

📘 单独补充:STM32 定时器知识要点

一、用到的定时器分类

定时器类型 特点 用途举例
基本定时器(TIM6/TIM7) 无输入输出通道,只能内部计数 延时、中断、DAC触发
通用定时器(TIM2~TIM5) 有多个通道,可用于 PWM、输入捕获、编码器等 PWM控制、电机驱动等

二、核心参数公式

1. PWM频率

fPWM=ftimer_clk(PSC+1)×(ARR+1)f_{\text{PWM}} = \frac{f_{\text{timer\_clk}}}{(PSC + 1) \times (ARR + 1)}

  • 中文释义:

PWM频率=定时器时钟频率(预分频器+1)×(自动重装值+1)\text{PWM频率} = \frac{\text{定时器时钟频率}}{(\text{预分频器}+1) \times (\text{自动重装值}+1)}

2. PWM占空比

Duty Cycle=CCRARR+1×100%\text{Duty Cycle} = \frac{CCR}{ARR + 1} \times 100\%

  • 中文释义:

占空比=比较寄存器值(CCR)自动重装值(ARR)+1×100%\text{占空比} = \frac{\text{比较寄存器值(CCR)}}{\text{自动重装值(ARR)}+1} \times 100\%

3. 基本定时器中断周期

T=(PSC+1)×(ARR+1)ftimer_clkT = \frac{(PSC + 1) \times (ARR + 1)}{f_{\text{timer\_clk}}}

  • 中文释义:

定时周期(秒)=(预分频器+1)×(自动重装值+1)定时器时钟频率\text{定时周期(秒)} = \frac{(\text{预分频器}+1) \times (\text{自动重装值}+1)}{\text{定时器时钟频率}}

三、常用寄存器

寄存器详细说明

寄存器 说明 详细解释
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(更新使能)等。

寄存器之间的关系:

  • PSCARR 配合使用,控制了定时器的计数频率和周期。PSC 控制定时器的频率,ARR 控制计数器的周期。
  • CNT 是定时器的实际计数值,随着时钟和设置的 PSCARR 进行增减,当 CNT 达到 ARR 时,定时器溢出并触发相应事件。
  • CCRx 用于控制 PWM 输出时的占空比或定时器的输出比较。当计数器 CNTCCRx 相等时,定时器会触发输出事件(比如切换引脚的电平)。
  • CR1 用于开启定时器以及选择定时器的工作模式。

四、基础功能总结

项目 基本定时器 通用定时器
输出PWM ❌ 不支持 ✅ 支持
中断功能 ✅ 支持 ✅ 支持
复用引脚输出 ❌ 无 ✅ 可映射到IO口输出
常见用途 延时、中断 PWM、捕获、测频