蓝桥杯嵌入式笔记

蓝桥杯嵌入式笔记
Skyforever嵌入式蓝桥杯笔记
一,新建项目
- 打开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 |
该文件在主函数中被引用,避免多处修改,提高可维护性
3. LED 控制函数
fun.h
1 |
|
fun.c
1 |
|
说明:
- 通过
GPIO_PIN_8 << (led - 1)
实现控制多个 LED(如 LED1~LED8),可以在对应代码位置按下f12查看原有定义。 mode
为1
点亮,0
熄灭。- 使用锁存器的使能引脚
PD2
控制 LED 状态稳定写入。
4. 主函数调用(main.c)
1 | /* Includes ------------------------------------------------------------------*/ |
在 while
循环中调用点灯函数,例如点亮 LED1、LED4、LED8:
1 | while (1) { |
注意:自定义代码需写在 /* USER CODE BEGIN */
与 /* USER CODE END */
区域之间,避免被 STM32CubeMX 自动生成代码覆盖。
三,按键
根据上面的文档对按键的定义,需要配置上拉输入(默认未按下按键是高电平(电平被上拉))
多选,对应位置全部改成上拉:
正常生成后,gpio.c里就会有相关引脚定义
fun.c要加的代码:
1 | unit8_t B1_state; |
fun.h也要修改:
1 |
|
main.c里调用按键扫描函数
1 | /* USER CODE BEGIN WHILE */ |
B1 按键按下,第一个 LED 点亮
代码下载之后,按键还没按下,此时 b1 读取高电平,if 不满足,然后把这个高电平赋值给 b1-last-state,一直这样循环,直到我们按下按键,这个时候 b1 等于 0,就点亮了。
边沿检测:就是按下并且松开 led 才亮,如果不松开就不亮;只检测下降沿
修改fun.c里的按键扫描方法,以支持四个按键:
1 | uint8_t led_flag; |
长按和短按区分
这里涉及到了定时器相关功能,可以先看下面这个视频先理解定时器的工作原理:
【STM32】第16集 动画告诉你, STM32的定时器到底怎么回事
在这个程序中,使用了定时器 TIM3
来判断按键的按下时间,从而区分短按和长按。关键参数如下:
参数 | 值 | 含义 |
---|---|---|
PSC(预分频器) | 8000-1 | 分频后得到10kHz时钟(0.1ms一次) |
CNT(当前计数值) | 0~65535 | 按键按下持续时间(单位0.1ms) |
所以设置判断 CNT = 10000
就是 1秒,用来区分长短按。
随便选一个基本定时器,选内部时钟,PSC=8000-1,ARR 默认最大值即可
在正常判断逻辑的基础上,再增加判断条件:
1 | unit8_t B1_state,B1_last_state=1;//初始化,避免只进入短按,无法进入长按 |
主函数加上定时器使能,while循环前的内容如下:
1 | /* Initialize all configured peripherals */ |
四,LCD
首先headfile.h里要引用lcd.h
其次,做初始化准备(初始化方法通过查询官方led函数定义得)
主函数while循环前的内容如下:
1 | /* Initialize all configured peripherals */ |
此时编译下载。 LED 灯全亮, 因为 led 和 lcd 共用 io 口,全亮是正常的
fun.h 中声明LCD 的显示函数:
1 |
|
fun.h中实现lcd_show函数
1 | char text[20];//一行只能显示20个字符 |
为什么要对字符串进行强制类型转换?因为这个函数要输入的参数是 u8 的,u8是无符号数据,char有符号
将显示函数放在主函数循环里
1 | /* USER CODE BEGIN WHILE */ |
可以看到,屏幕上显示了text字符
按键+LCD
利用按键。 B1 按下 Count 加加。 B2 按下 Count 减减
屏幕显示。第零行 Test。第三行显示 count。
下面是修改好了的fun.c代码:
1 |
|
LCD 高亮显示
在原有基础上加一个标志位,记录那一行用了高亮
LED 和 LCD 引脚冲突问题
在 LCD 设置前。将引脚配置成低电平
每一次在主函数循环之前锁住led灯相关的寄存器:
1 | /* USER CODE BEGIN SysInit */ |
或者写下面这个版本,遍历所有灯状态:
1 | uint8_t led_state[8]={0}; |
五,定时器中断实现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 秒钟产生一次更新事件(例如触发一次中断)。
- 首先通过较大的 PSC (7999) 将 80 MHz 的高速时钟乘以8000,分频到一个较低、好计算的频率 (10 kHz),
- 然后通过 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 | void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){ |
在主函数进行计时器初始化
1 | /* Initialize all configured peripherals */ |
中断函数不建议执行耗时间的调用
例如这个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 | /* USER CODE BEGIN 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
是系统时钟。 - 这里的
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 | /* USER CODE BEGIN 2 */ |
加入输入中断的回调函数
按 CTRL+F 在hal库代码头文件里寻找 capture
.
在fun.c中实现回调函数:
1 | char text[20]; |
在主函数调用,并初始化LCD
1 | /* USER CODE BEGIN 2 */ |
.
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 | char text[20]; |
主函数代码:
1 | /* USER CODE BEGIN 2 */ |
调整旋钮,可以看到值在变化:
。
九,ADC 测量
PB15选adc2in15
pb12选ADC1in11
都选single-ended
headfile.h引入adc.h
fun.c:
1 | void lcdshow(){ |
主函数代码:
1 | /* USER CODE BEGIN SysInit */ |
可以看到屏幕上的值在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 | void lcdshow(){ |
在headfile.h里声明函数
1 | double get_vol(ADC_HandleTypeDef *hadc); |
主函数代码:
1 | /* USER CODE BEGIN SysInit */ |
九,串口通信
串口发送数据
设置为异步通信
PC4,PC5 改为 PA9,PA10
中断使能作用
后面用于接收的
主函数循环附近的代码如下:
1 | /* USER CODE BEGIN WHILE */ |
串口调试
查找电脑端口,此电脑,管理查找
波特率和之前保持一致,115200
比赛中一般为 9600/115200
.
串口中断接收配置
在fun.c里实现串口中断回调函数(可用ctrl+f在uart.h里找)
1 | uint8_t rec_data; |
注意,在fun.h里,声明接收数据变量为全局变量:
1 |
|
在主函数,要启用对应的串口中断
1 | /* USER CODE BEGIN 2 */ |
利用定时器进行串口非定长数据接收,并处理数据
串口接收:
-
每次进入中断只能接收一个字节的数据
-
考虑错误情况:
-
每次接收一个字节就要判断该字节是否符合要求
-
接收完所有字符,判断字符串是否符合要求
-
解决方法:
- 利用定时器,处理串口接收
- 串口波特率=9600 bit/s就是1s可以传输9600bit
- 串口传输一次数据包含起始位,数据位,结束位,一共10bit
- 10 *1/9600 = 0.00104s =1.04ms
头文件与变量定义
1 |
|
串口接收中断回调函数
1 | void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){ |
串口数据处理函数
1 | void uart_data_rec(){ |
整体来看,这段代码实现了 USART1 串口数据的中断接收、基于定时器的接收完成判断以及按规则对接收数据的处理与回复功能。
可以在 if 最后加上 &&rec_buff[最后一位 + 1]==0,避免输入 lanqiao 也会返回 lan
可以在 if 最后加上 &&rec_buff[最后一位 + 1]==0,避免输入 lanqiao 也会返回 lan
。
会只输出 lan 因为对后面的数据不再进行判断
十,用I2C协议对eeprom 读写
这段 C 语言代码,用于向 EEPROM写入数据,函数借助 I2C通信协议来实现操作
下面加的两个函数需要在对应头文件里声明
1 | void eeprom_write(uint8_t addr, uint8_t dat)//参数列表为要写入数据的rom地址和要写入地址的数据 |
在eeprom设备手册里,可以了解到:
- 0xa0 为写的操作
- 0xa1 为读的操作
eeprom_read 函数:
1 | uint8_t eeprom_read(uint8_t addr)//参数为要读取数据的ROM地址 |
主函数代码
1 | /* USER CODE BEGIN 2 */ |
总结:初始化外设 → 从 EEPROM 读取数据 → 将数据循环显示在 LCD 屏幕上
未存储时,显示默认值255
.
存储后,显示存储的内容
.
十一,rtc 实时时钟
主要功能实现:
1.设置时间和日期
2.读取时间和日期
3.设置一个闹钟
fun.c代码
1 | char text[20]; |
灯点不亮的看看主函数 rtc 有没有使能
完结撒花!!!!!
📘 单独补充:STM32 定时器知识要点
一、用到的定时器分类
定时器类型 | 特点 | 用途举例 |
---|---|---|
基本定时器(TIM6/TIM7) | 无输入输出通道,只能内部计数 | 延时、中断、DAC触发 |
通用定时器(TIM2~TIM5) | 有多个通道,可用于 PWM、输入捕获、编码器等 | PWM控制、电机驱动等 |
二、核心参数公式
1. PWM频率
- 中文释义:
2. PWM占空比
- 中文释义:
3. 基本定时器中断周期
- 中文释义:
三、常用寄存器
寄存器详细说明
寄存器 | 说明 | 详细解释 |
---|---|---|
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、捕获、测频 |