系列文章目录
留空
文章目录
- 系列文章目录
- 前言
- 一、LED模块
- 1.1 赛题要求
- 1.2 模块原理图
- 1.3 编写代码
- 1.4 赛题实战
- 二、LCD模块
- 2.1 赛题要求
- 2.2 模块原理图
- 2.3 编写代码
- 2.4 赛题实战
- 三、按键模块
- 3.1 赛题要求
- 3.2 模块原理图
- 3.3 编写代码
- 3.4 赛题实战
- 四、串口模块
- 4.1 赛题要求
- 4.2 模块原理图
- 4.3 编写代码
- 4.4 赛题实战
- 五、PWM模块
- 5.1 赛题要求
- 5.2 模块原理图
- 5.3 编写代码
- 5.4 赛题实战
- 六、ADC模块
- 6.1 赛题要求
- 6.2 模块原理图
- 6.3 编写代码
- 6.4 赛题实战
- 七、EEPROM模块
- 7.1 赛题要求
- 7.2 模块原理图
- 7.3 编写代码
- 7.4 赛题实战
- 八、RTC模块
- 8.1 赛题要求
- 8.2 模块原理图
- 8.3 编写代码
- 8.4 赛题实战
- 九、补充
- 总结
前言
自用
一、LED模块
1.1 赛题要求
(1)单独或几个 LED灯亮起/熄灭【不完整总结】
LED灯(LD1)以0.5秒的频率闪烁。
上限提醒指示灯以0.2秒为间隔闪烁,下限指示灯熄灭。
按下某个按键后可以启用或禁用LED指示灯功能,LED指示灯功能禁用后,所有指示灯处于熄灭状态。
指示灯LD2以0.1秒为间隔亮、灭闪烁报警,5秒后熄灭。
指示灯LD2以0.1秒为间隔亮、灭闪烁报警,5次后熄灭。
(2)LED流水指示
升降机上下行时,4个LED(LD5-LD8)组成流水灯用来表示升降机的运行方向。合理选择流水灯的流水方式和时间间隔。
1.2 模块原理图
我们可以把原理图分成两部分,一部分为左边一列LED灯,另一部分为SN74HC573ADWR锁存器和输出PC端。
左部分,LED灯为低电平点亮,初始化时设置为高电平熄灭。
右部分,SN74HC573ADWR芯片(U1),这是一款八位透明锁存器,具有三态输出缓冲区,属于 74HC 系列 高速 CMOS 逻辑集成电路,用于锁存数据。当输入在有效状态时(通常是高电平),这个锁存器能够实时地将输入数据传送到输出。当输入无效时,它会保持最后传入的数据。
各个引脚功能如下表
引脚名称 | 功能描述 |
---|---|
GND | 电源负极 |
VCC | 电源正极 |
输入 (D) | 数据输入引脚 |
输出 (Q) | 数据输出引脚 |
LE | 锁存控制引脚。高电平,保存最新的输入数据但不输出;低电平,不锁存直接输出数据 |
OE# | 输出使能引脚。低电平,输出数据;高电平,停止输出(高阻态) |
输入输出功能模式
OE# 就是芯片的开关,OE#为低时,芯片工作;OE#为高时,停止工作。蓝桥杯板子上的OE#接地,无需设置。
LE引脚通俗易懂一点理解,D值一直变化,根据LE的状态输出Q值。
D | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 |
---|---|---|---|---|---|---|---|---|---|---|
LE | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
Q | 1 | 2 | 3 | 3 | 3 | 6 | 7 | 8 | 9 | 0 |
- LE = 1 时,Q 直接输出 D。
- LE = 0 时,Q 保持上次的值,不更新。
- LE = 1 时,Q 输出当前 D 的值。
这个在LED和LCD的冲突引脚设置中非常有用!必须理解!
PC8 - 15同时控制LED和LCD,如果左侧锁存器Q直接输出 D,就会导致更新LCD时出现LED乱闪现象。
为了解决LED与LCD之间的冲突,我们需要合理利用锁存器的特性。我们跑代码时大部分时间在更新LCD页面,在更新LED时的一瞬间打开锁存器,将LED设置完成后关闭锁存器。
1.3 编写代码
1.3.1 分析代码(不想看分析的直接看1.3.2)
开始编写代码!
首先,我们一共有八个LED灯,控制方法有两种,一种是单独控制,一种是全部控制。
假设,现在要点亮LED1/3/5/7四个灯
第一种,单独控制
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_10,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_12,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_14,GPIO_PIN_RESET);
或
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8 | GPIO_PIN_10 | GPIO_PIN_12 | GPIO_PIN_14, GPIO_PIN_RESET);
单独控制太繁琐,每次都要写一长串!!
第二种,全部控制
HAL_GPIO_WritePin(GPIOC,0xFFFF,GPIO_PIN_RESET); //设置 选中(为1)的端口 为低电平
0xFFFF – 转成二进制 – 1111 1111 1111 1111 – 对应PC端,一共十六位,PC0到PC15全部为低电平
PC15 | PC14 | PC13 | PC12 | … | PC2 | PC1 | PC0 |
---|---|---|---|---|---|---|---|
1 | 1 | 1 | 1 | … | 1 | 1 | 1 |
再举个例子
HAL_GPIO_WritePin(GPIOC,0x0001,GPIO_PIN_RESET); //设置PC1为低电平
0x0001 – 转成二进制 – 0000 0000 0000 0001 – 对应PC端,只有PC1为1,那么这句代码就只设置PC1为低电平,其他引脚将保持原来的电平状态,无论它们之前是高还是低电平。
那么!我们现在点亮LED1/3/5/7四个灯,也就是设置PC8、PC10、PC12和PC14为低电平。
0101 0101 0000 0000 – 转成十六进制 – 0x5500
PC15 | PC14 | PC13 | PC12 | PC11 | PC10 | PC9 | PC8 | … |
---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | … |
代码如下
HAL_GPIO_WritePin(GPIOC,0x5500,GPIO_PIN_RESET);
因为,因为,因为!我们只需要控制PC8到15,所以我们只需要控制高八位,不需要后面低八位。
那么简化一下,0x5500
等同于 0x55 << 8
0x55 = 0x0055= 0000 0000 0101 0101 --> 再左移八位(<< 8) —> 0101 0101 0000 0000 = 0x5500
改过的代码如下
HAL_GPIO_WritePin(GPIOC,0x55 << 8,GPIO_PIN_RESET);
1.3.2 完整代码
首先,在BSP
中创建两个文件LED.c
和LED.h
LED.h
#ifndef __LED_H
#define __LED_H#include "main.h"void LED_Disp(uint16_t ucLED);#endif
LED.c
#include "LED.h"void LED_Disp(uint16_t ucLED)
{//先关闭所有灯,再点亮指定灯HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOC,ucLED << 8,GPIO_PIN_RESET);//关闭锁存(输出数据) 再开启锁存HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}
先设置好数据,再把锁存关闭,关闭的一瞬间输出数据,再立马开启锁存。
main.c
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "LED.h"
/* USER CODE END Includes */... 省略 .../* USER CODE BEGIN WHILE */while (1){LED_Disp(0x55);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}
1.4 赛题实战
(1)单独或多个灯长亮/灭
LED_Disp(0x01); // 假设为LED1亮
LED_Disp(0x00); // 假设为LED1灭
LED_Disp(0x15); // 假设为LED1、LED3、LED5亮
(2)LED 灯以 0.1 秒频率闪烁,五秒后关闭
void LED1_Blink_5s(void)
{if(uwTick - led1_uwTick > 5000) return; else{LED_Disp(0x01); // LED1 每 0.1 秒闪烁一次 HAL_Delay(100);LED_Disp(0x00); HAL_Delay(100); }if(led1_uwTick == 0) led1_uwTick = uwTick;
}
(3)LED 灯以 0.1 秒频率闪烁,五次后关闭
void LED1_Blink_5times(void)
{ for (int i = 0; i < 5; i++) // 循环 5 次 { LED_Disp(0x01); // LED1 每 0.1 秒闪烁一次 HAL_Delay(100);LED_Disp(0x00); HAL_Delay(100); } LED_Disp(0x00); // 5 次后熄灭 LED1
}
(4)LED流水指示
/*** 简单版 ***/
void LED_Running(uint8_t mode)
{if(mode == 1){for(int i=0;i < 8;i++){LED_Disp(0x01 << i);HAL_Delay(50);} }else if(mode == 2){for(int i=7;i >= 0;i--){LED_Disp(0x01 << i);HAL_Delay(50);} }
}/*** 稍微复杂版 ***/
void LED_Running(uint8_t mode)
{if(mode == 1){i = (i + 1) % 8; LED_Disp(0x01 << i);}else if(mode == 2){i = (i - 1 + 8) % 8;LED_Disp(0x01 << i);}
}
二、LCD模块
2.1 赛题要求
(1)显示背景色(BackColor):黑色
(2)显示前景色(TextColor):白色
(3)严格按照图示要求设计各个信息项的名称(区分字母大小写)和行列位置。
注:选手收到的嵌入式主板配套液晶屏驱动芯片控制器型号可能为ILI9328、ILI9325或uc8230,数据包中提供的液晶驱动代码能够兼容ILI9328、ILI9325、uc8230三种类型的液晶控制器,不需要任何改动。
2.2 模块原理图
这个是LCD的双排针,如下图,接下来我们看看每个引脚都有什么功能!
原理图与ILI9325对照如下表:
原理图引脚 | ILI9325 引脚 | 功能描述 |
---|---|---|
LCD_CS# | nCS | 片选信号,低电平时启用ILI9325,选择和访问LCD |
LCD_WR# | nWR/SCL | 写使能信号,低电平时启用写操作,在SPI模式下为时钟信号 |
LCD_RST# | nRESET | 复位信号,低电平初始化LCD,启动时需要执行电源复位 |
LCD_RS | RS | 寄存器选择信号,低电平选择索引或状态寄存器,高电平选择控制寄存器 |
LCD_RD | nRD | 读使能信号,低电平时启用读操作 |
LCD_D0 - D15 | DB[17:0] | 数据总线,用于并行数据传输,在不同宽度的接口模式下使用 |
LCD_HDR | 不直接对应 | |
VDD | VCI/VDD | 电源引脚,提供电源电压到LCD模块和控制器 |
GND | GND | 地线电源,用于连接系统地 |
要深入了解可以去查看数据手册,这儿并不需要我们理解,看看就好啦。
2.3 编写代码
底层代码不需要自己写,当天会给赛点资源包,里面就有完整的代码!
第一步,设置相关引脚。
在赛点资源包中main.c
有相关引脚定义初始化。
/*** @brief GPIO Initialization Function* @param None* @retval None*/
static void MX_GPIO_Init(void)
{GPIO_InitTypeDef GPIO_InitStruct = {0};/* GPIO Ports Clock Enable */__HAL_RCC_GPIOC_CLK_ENABLE();__HAL_RCC_GPIOF_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();/*Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15 | GPIO_PIN_0| GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4| GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8| GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12, GPIO_PIN_RESET);/*Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);/*Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_9, GPIO_PIN_RESET);/*Configure GPIO pins : PC13 PC14 PC15 PC0PC1 PC2 PC3 PC4PC5 PC6 PC7 PC8PC9 PC10 PC11 PC12 */GPIO_InitStruct.Pin = GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15 | GPIO_PIN_0| GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4| GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8| GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);/*Configure GPIO pin : PA8 */GPIO_InitStruct.Pin = GPIO_PIN_8;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/*Configure GPIO pins : PB5 PB8 PB9 */GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_9;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
我们可以直接复制到自己新创建的main.c
中,方法:蓝桥杯嵌入式省赛国赛程序题手把手教程-电子爱好者老王
或者 在cubeMX中重新设置,就不需要粘贴以上的代码,之前在LED中有设置过一部分的引脚,对照原理图或是赛点提供的底层代码,把剩下的引脚设置成output
模式,其他默认,如下:
第二步,把赛点资源包中lcd.c
和lcd.h
复制到自己的工程中。
还有一个fonts.h
。
最后呢,在赛点资源包main.c
中复制主函数中的代码到自己工程中:
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();/* USER CODE BEGIN 2 */LCD_Init();HAL_Delay(100); /* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */LCD_SetBackColor(Black);LCD_SetTextColor(White);LCD_Clear(Black);HAL_Delay(100);LCD_DisplayStringLine(Line4, (unsigned char *)" Hello,world. ");HAL_Delay(1000); /* USER CODE BEGIN WHILE */while (1){ /* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
上电下载程序,点亮!
2.4 赛题实战
我们根据赛题要求,练习一下改背景/字体颜色,在指定行列中写信息。
第一行,第七列开始Data
单词。
LCD_DisplayStringLine(Line1, (unsigned char *)" Data ");//"[空六格]Data[空]"
第三行,第五列开始电压显示字母和数字
。
#include "stdio.h"unsigned char LCD_Text[22];
float R37V = 3.02;sprintf((char *)LCD_Text," V:%0.2fV ",R37V);
LCD_DisplayStringLine(Line3,LCD_Text);
第五行,第五列开始模式切换显示字母
。
LCD_DisplayStringLine(Line5, (unsigned char *)" Mode:AUTO ");
这节非常简单,对吧!!
三、按键模块
3.1 赛题要求
(1)短按
(2)长按
(3)双击
(4)按键应进行有效的防抖处理,避免出现一次按下、多次触发等情形。
3.2 模块原理图
按键按下,PB1检测到低电平输入,按键松开,变回高电平。
3.3 编写代码
根据原理图,设置引脚为input
模式
首先,在BSP
中创建KEY.c
和KEY.h
uint8_t KEY_Scan(void)
: 这个函数是用于扫描按键的状态。监测GPIO引脚,并根据引脚的电平来判断按键是否被按下。如果某个引脚的电平为低(0),则返回对应的按键编号(1到4)。void KEY_Proc(void)
:这个函数是用于监测按键的按下和释放逻辑。
KEY.c
#include "KEY.h"__IO uint32_t uwTick_Key1 = 0;
uint16_t KEY_Val,KEY_Old,KEY_Up,KEY_Down;uint8_t KEY_Scan(void)
{uint8_t KEY_Num = 0;if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == 0) KEY_Num = 1;if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == 0) KEY_Num = 2;if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == 0) KEY_Num = 3;if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == 0) KEY_Num = 4;return KEY_Num;
}void KEY_Proc(void)
{if((uwTick - uwTick_Key1) < 50) return; //这行代码的作用是防抖动。//如果上次按键扫描时间距离当前时间太短(小于50毫秒),就不进行扫描,防止按键抖动。uwTick_Key1 = uwTick;KEY_Val = KEY_Scan();KEY_Down = KEY_Val & (KEY_Val ^ KEY_Old);KEY_Up = ~KEY_Val & (KEY_Val ^ KEY_Old);KEY_Old = KEY_Val;
}
KEY.h
#ifndef __KEY_H
#define __KEY_H#include "main.h"uint8_t KEY_Scan(void);
void KEY_Proc(void);#endif
3.3.1 短按
main.c
中直接调用
#include "KEY.h"... ...while (1){KEY_Proc();if(KEY_Val == 1){LED_Disp(0x01);}else if(KEY_Val == 2){LED_Disp(0x00);} }
}
按下按键1则LED1亮,按下按键2则LED2亮。
3.3.2 长按
在KEY.c
基础上,增加函数,还有KEY.h
中!
uint8_t KEY_State(void)
{uint8_t result = 0; // 初始化结果变量,用于保存返回的按键状态// 检查是否有按键按下if(KEY_Down){// 如果有按键按下,记录当前时间戳(uwTick)uwTick_Key2 = uwTick;}// 判断按键按下时间是否在800ms以内if(uwTick - uwTick_Key2 <= 800){// 如果按下时间小于等于800ms,表示是短按switch(KEY_Up) // 检查哪个按键释放{case 1: result = 1; break; // 按键1短按case 2: result = 2; break; // 按键2短按case 3: result = 3; break; // 按键3短按case 4: result = 4; break; // 按键4短按}}// 如果按键按下时间超过了800mselse if(uwTick - uwTick_Key2 > 800){// 如果按键按下超过800ms,表示是长按switch(KEY_Val) // 检查哪个按键被长按{case 1: result = 5; break; // 按键1长按case 2: result = 6; break; // 按键2长按case 3: result = 7; break; // 按键3长按case 4: result = 8; break; // 按键4长按}}return result; // 返回按键状态(短按或长按)
}
main.c
中调用
while (1){KEY_Proc();if(KEY_State() == 1){LED_Disp(0x01);}else if(KEY_State() == 11){LED_Disp(0x00);}}
3.3.3 双击
还有点问题,略
3.4 赛题实战
参考上面的代码,根据逻辑编写。
void KEY_STATE(void)
{switch(KEY_State()){case 1:// 对应的功能break;case 2:// 对应的功能break;case 3:// 对应的功能break;... ...case 8:// 对应的功能break;}
}
四、串口模块
4.1 赛题要求
(1)USART1,通信波特率:9600bps。
(2)电脑端发送–单片机接收命令,单片机发送–电脑端接收命令。
(3)识别出通过串口接收到的指令存在格式或逻辑错误。
4.2 模块原理图
串口(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)是一种常见的通信接口,用于设备之间的数据传输。它通过两根信号线(TXD和RXD)实现全双工通信:
- TXD:发送数据线(Transmit Data),用于发送数据。
- RXD:接收数据线(Receive Data),用于接收数据。
在这,我们只需要懂设置就好啦!想深入了解可以看看江协的视频!
4.3 编写代码
第一步,打开CubeMX。
配置串口(UART)
- 找到 Connectivity -> USART1。
- 将 Mode 设置为 Asynchronous(异步模式)。
- 设置以下参数:
- Baud Rate:波特率(9600)
- Word Length:数据位长度(默认 8 位)。
- Parity:校验位(默认 None)。
- Stop Bits:停止位(默认 1 位)。
- Data Direction:Receive and Transmit。
配置中断
如果使用中断接收数据:
- 在 NVIC Settings 中,勾选 USART1 global interrupt。
- 设置优先级(看情况)。
生成代码!
4.3.1 发送数据
简简单单一个函数!
- HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout):通过 UART 发送数据。
小栗子奉上:
char Tx_Data[30] = "你好\r\n"; // 定义并初始化数组HAL_UART_Transmit(&huart1, (uint8_t *)Tx_Data, strlen(Tx_Data), 50); // 发送数据
注意注意注意!
使用 strlen(Tx_Data)
和 sizeof(Tx_Data)
是不一样的
-
sizeof
:-
是一个运算符,用于计算变量或数据类型所占用的内存大小(以字节为单位)。
-
对于字符串,
sizeof
会计算整个字符数组的大小,包括末尾的空字符\0
。char str[] = "Hello"; size_t size = sizeof(str); // size = 6
字符串
"Hello"
占用 6 个字节(5 个字符 + 1 个\0
)。
-
-
strlen
:-
是一个函数,用于计算字符串的实际长度,不包括末尾的空字符
\0
。char str[] = "Hello"; size_t len = strlen(str); // len = 5
字符串
"Hello"
的长度是 5。
-
如果我们使用sizeof
,就需要在后面 -1。(很容易忘记,不推荐!)
HAL_UART_Transmit(&huart1, (uint8_t *)Tx_Data,sizeof(Tx_Data) - 1, 50); // 需要 - 1
4.3.1 接收数据
首先,确定CubeMX中配置好了中断。
简简单单两个函数!
- HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size):启动 UART 接收中断模式。
- HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart): UART 接收完成回调函数。
小栗子奉上:
char Rx_Data[30];
unsigned char rx_data,rx_num;int main(void)
{HAL_UART_Receive_IT(&huart1,(uint8_t *)&rx_data,1);while (1){sprintf((char *)LCD_Text," time:%s",car_time);LCD_DisplayStringLine(Line7,LCD_Text); }
}void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{ if(huart == &huart1) // 判断是否是 huart1 触发的中断{ Rx_Data[rx_num++] = rx_data; // 将接收到的数据存储到 Rx_Data 数组中,并递增 rx_numHAL_UART_Receive_IT(&huart1, &rx_data, 1); // 重新启动接收中断,准备接收下一个字节}
}
4.4 赛题实战
根据题目要求,需要串口接收和发送以下内容:
接收车辆入场信息:
-
格式:
停车类型:车辆编号:进入时间
-
例如:
CNBR:A392:200202120000
停车类型是CNBR(可能是普通停车场),车辆编号是A392,进入时间是2020年2月2日12:00:00。
接收车辆出场信息:
-
格式:
停车类型:车辆编号:退出时间
-
例如:
VNBR:D583:200202213205
停车类型是VNBR(可能是VIP停车场),车辆编号是D583,退出时间是2020年2月2日13:25:05。
输出计费信息:
-
格式:
停车类型:车辆编号:停车时长:费用
-
例如:
VNBR:D583:10:20.00
停车类型是VNBR(VIP停车场),车辆编号是D583,停车时长为10小时,停车费用为20.00元。
上面这些太复杂,到写真题时在具体写。这里只写简单的逻辑,电脑端从串口发送车辆信息到单片机,单片机对信息进行分析,最后单片机从串口发送信息到电脑端。
例如,电脑端发送CNBR:A392:200202120000
,单片机接收后返回时间信息20-02-02-12
。
下面写一下实现的代码!
char Rx_Data[30];
char Tx_Data[30];
char car_type[5],car_num[5],car_time[13];
int year,month,day,min,s;unsigned char LCD_Text[30];
unsigned char rx_data,rx_num;void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void Usart_Tx(void);
void Usart_Rx(void);int main(void)
{HAL_UART_Receive_IT(&huart1,(uint8_t *)&rx_data,1);while (1){KEY_Proc();if(KEY_State() == 1){Usart_Tx();}Usart_Rx();}
}/******************** 发送串口信息 *********************/
void Usart_Tx(void)
{//方法1HAL_UART_Transmit(&huart1,(uint8_t *)"CNBR:A392:200202120000",22,50);//方法2
// sprintf(Tx_Data,"CNBR:A392:200202120000");
// HAL_UART_Transmit(&huart1,(uint8_t *)Tx_Data,strlen(Tx_Data),50);
}/******************** 中断接收串口 *********************/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{ if(huart == &huart1) // 判断是否是 huart1 触发的中断{ Rx_Data[rx_num++] = rx_data; // 将接收到的数据存储到 Rx_Data 数组中,并递增 rx_numHAL_UART_Receive_IT(&huart1, &rx_data, 1); // 重新启动接收中断,准备接收下一个字节}
}/******************** 处理串口信息 *********************/
void Usart_Rx(void)
{if(rx_num == 22 && Rx_Data[4] == ':' && Rx_Data[9] == ':'){//处理接收信息sscanf(Rx_Data,"%4s:%4s:%12s",car_type,car_num,car_time); sprintf((char *)LCD_Text," type:%s",car_type);LCD_DisplayStringLine(Line5,LCD_Text); sprintf((char *)LCD_Text," num:%s",car_num);LCD_DisplayStringLine(Line6,LCD_Text); sprintf((char *)LCD_Text," time:%s",car_time);LCD_DisplayStringLine(Line7,LCD_Text); //处理时间信息sscanf(car_time,"%2d%2d%2d%2d%2d",&year,&month,&day,&min,&s); sprintf(Tx_Data,"%d-%d-%d-%d",year,month,day,min); HAL_UART_Transmit(&huart1,(uint8_t *)&Tx_Data,30,50);rx_num = 0; }
}
这儿,我们需要理解一下两个函数的使用。sscanf
和 sprintf
,C 标准库中用于格式化输入和输出的函数。
-
sscanf
用于从字符串中提取数据。
sscanf(car_time,"%2d%2d%2d%2d%2d",&year,&month,&day,&min,&s);
格式化字符串"%2d%2d%2d%2d%2d" 表示提取"car_time"中5 个 2 位整数。
提取的数据存储到变量 year, month, day, min, s 中。
-
sprintf
用于将数据格式化并写入字符串。
sprintf(Tx_Data,"%d-%d-%d-%d",year,month,day,min);
格式化字符串 “%d-%d-%d-%d” 表示输出 4 个整数"year,month,day,min"并用连字符分隔。
格式化后的数据存储到 Tx_Data 中。
然后,打开我们赛点资源包中的Tool
,找到STC-ISP
软件
五、PWM模块
5.1 赛题要求
PWM输出:
(1)使用PA6(PA7)输出频率固定为100Hz,占空比可调节的脉冲信号。
(2)使用PA6(PA7)输出PWM信号,频率1KHz,占空比为60%。
PWM输入捕获:
(1)测量PA15(PB4)引脚接入的脉冲信号。
(2)测量频率数据,频率数据单位为Hz,数据保留整数位。
5.2 模块原理图
XL555 是一款经典的 定时器集成电路,广泛应用于电子电路中,用于生成精确的定时信号、方波、脉冲或PWM信号。通过滑动变阻器 R40、固定电阻 R32(100Ω) 和电容 C20(10nF) 生成可调频率的PWM信号。(原理太多直接省略啦)
微控制器的 PA15 和 PB4 引脚用于监测信号。
设置输出PWM的原理
(1)定时器计数器(CNT):
- 定时器的计数器(CNT)从0开始递增,直到达到自动重载寄存器(ARR)的值,然后重新从0开始计数。
- 计数器的递增频率由定时器的时钟源和预分频器(PSC)决定。
(2)自动重载寄存器(ARR):
- ARR决定了PWM信号的周期。当计数器(CNT)达到ARR的值时,计数器会重置为0,同时PWM信号的一个周期结束。
- 公式:
PWM周期 = 系统时钟 / (PSC + 1) / (ARR + 1)
(3)捕获比较寄存器(CCR):
- CCR决定了PWM信号的占空比。当计数器(CNT)的值等于CCR的值时,PWM信号的输出电平会发生变化(例如从高电平变为低电平)。
- 公式:
占空比 = (CCR / (ARR + 1)) × 100%
捕获PWM的原理
(1)频率测量:
- 通过捕获两个上升沿(或下降沿)之间的时间差,可以计算出PWM信号的周期,从而得到频率。
- 公式:
频率 = 1 / 周期
。
(2)占空比测量:
- 通过捕获一个周期内高电平的时间(即上升沿到下降沿的时间),可以计算出占空比。
- 公式:
占空比 = (高电平时间 / 周期) × 100%
。
5.3 编写代码
5.3.1 输出PWM
第一步,打开CubeMX。
假设,我们现在使用PA6,对应就是TIM3的通道1输出PWM信号。
启用定时器并配置PWM模式
选择定时器 → 设置PSC、ARR、CCR → 生成代码。
示例配置(生成1kHz,50%占空比的PWM)
熟记以下公式:
- PWM频率 Freq = 系统时钟 / (PSC + 1) / (ARR + 1)
- 占空比 Duty = (CCR / (ARR + 1)) × 100%
PSC(预分频器)、ARR(自动重载值)、CCR(捕获/比较寄存器)。我们的系统时钟(时钟源)是80MHz,就是80后边6个0。
我们设置的PSC = 799,ARR = 99,CCR = 50(这个在代码里设置!这里假设50)
- Freq = 80 000000 / (799 + 1) / (99 + 1) = 80 000000 / 800 / 100 = 1000;
- Duty = (CCR / (ARR + 1)) × 100% = 50 / (99 + 1) × 100% = 50%。
第二步,记住输出PWM函数
以下是比赛编写输出PWM代码时用到的函数:
- HAL_TIM_PWM_Start: 启动PWM输出。
- HAL_TIM_PWM_Stop: 停止PWM输出。
- **__HAL_TIM_SetCompare(HANDLE, CHANNEL, COMPARE) **:设置 PWM 占空比。
下面是一个简单的例子
int main(void)
{... ...HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1); //启动指定定时器通道的PWM输出。__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, 50); //设置PWM的占空比50%。while (1){... ...}
}
5.3.2 捕获PWM
第一步,打开CubeMX。
假设,我们现在使用PA15,对应就是TIM8的通道输入捕获(R40)PWM信号。
配置输入捕获模式
- 选择定时器:找到 TIM8,设置时钟源为 Internal Clock。
- 配置通道:
- TIM2 Channel 1 → Input Capture direct mode。
- 参数设置:
- Prescaler (PSC):71(分频完为1MHz)。
- Counter Period (ARR):65535(16位定时器最大值,防止溢出)。
- 触发边沿:
- IC1 Polarity → Rising Edge(捕获上升沿)。
启用中断
- 在 NVIC Settings 中勾选 TIM2 global interrupt。
- 生成代码后,中断处理函数会自动生成。
还是刚刚那些公式:
- PWM频率 Freq = 系统时钟 / (PSC + 1) / (ARR + 1)
- 占空比 Duty = (CCR / (ARR + 1)) × 100%
这次我们是已知PSC,但是需要我们实时监测ARR和CCR的值!
我们设置的PSC = 79
- Freq = 80 000000 / (79 + 1) / (ARR + 1);
- Duty = (CCR / (ARR + 1)) × 100%。
第二步,记住输入捕获PWM函数
以下是比赛编写输出PWM代码时用到的函数:
- HAL_TIM_IC_Start_IT:启动某个定时器的某个通道的输入捕获功能,并且使能中断。
- HAL_TIM_IC_Stop_IT: 停止某个定时器的某个通道的输入捕获功能。
- HAL_TIM_ReadCapturedValue(const TIM_HandleTypeDef *htim, uint32_t Channel):读取某个定时器的某个通道捕获到的值。
- __HAL_TIM_SET_CAPTUREPOLARITY(HANDLE, CHANNEL, POLARITY):设置某个定时器的某个通道的捕获极性为上升沿或下降沿捕获。
- HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim):这是一个回调函数,当定时器捕获到信号时(上升沿或下降沿),系统会自动调用这个函数。
下面是一个简单的例子
uint32_t R40_Freq, R40_Duty; // 用于存储频率和占空比
uint32_t CCR1, CCR2, CCR3, High_CCR, Low_CCR, Period_CCR; // 用于存储捕获值和计算周期
uint8_t Capture_Flag = 1,Capture_end = 0; // 捕获标志,用于切换捕获状态... ...int main(void)
{... ...HAL_TIM_IC_Start_IT(&htim8, TIM_CHANNEL_1); //启动定时器8通道1的输入捕获,并启用中断while (1){Get_Freq(); // 在主循环中调用频率计算函数}
}void Get_Freq(void)
{ if (Capture_end == 0) return; // 如果捕获未完成,直接返回if (CCR1 < CCR3) Period_CCR = CCR3 - CCR1; // 计算周期(正常情况)else Period_CCR = 0xFFFF + 1 - CCR1 + CCR3; // 处理计数器溢出的情况 R40_Freq = 80000000 / 80 / Period_CCR; // 计算频率(假设系统时钟为80MHz,预分频为80)sprintf((char *)LCD_Test, " R40_Freq = %dHz ", R40_Freq); // 格式化频率值LCD_DisplayStringLine(Line5, LCD_Test); // 在LCD上显示频率值Capture_end = 0; // 重置捕获完成标志
}void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if (htim == &htim8) // 判断是否是定时器8触发的中断{switch (Capture_Flag) // 根据捕获标志切换捕获状态{case 1:CCR1 = HAL_TIM_ReadCapturedValue(&htim8, TIM_CHANNEL_1); //读取第一次捕获值Capture_Flag = 2; //切换到下一次捕获状态break;case 2:CCR2 = HAL_TIM_ReadCapturedValue(&htim8, TIM_CHANNEL_1); //读取第二次捕获值Capture_Flag = 1; //切换回下一次捕获状态Capture_end = 1; //捕获完成标志break; default:break;}HAL_TIM_IC_Start_IT(&htim8, TIM_CHANNEL_1); // 重新启动输入捕获}
}
5.4 赛题实战
5.4.1 输出PWM
uint16_t PA6_Duty;void Get_Duty(void);int main(void)
{HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1);while (1){KEY_Proc();Set_Duty();}
}void Set_Duty(void)
{if(KEY_State() == 1) // 如果按键1被按下{PA6_Duty += 10; // 增加PA6_Duty的值,每次增加10if(PA6_Duty >= 90) PA6_Duty = 0; // 如果PA6_Duty大于或等于90,则将其重置为0}// 设置TIM3的通道1的占空比__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, PA6_Duty);// 读取TIM3的通道1的当前占空比,并将其赋值给PA6_DutyPA6_Duty = __HAL_TIM_GetCompare(&htim3, TIM_CHANNEL_1);// LCD显示sprintf((char *)LCD_Test, " PA6_Duty = %d", PA6_Duty);LCD_DisplayStringLine(Line7, LCD_Test);
}
5.4.2 捕获PWM
uint32_t R40_Freq, R40_Duty;//用于存储频率和占空比
uint32_t CCR1, CCR2, CCR3, High_CCR, Low_CCR, Period_CCR;//用于存储捕获值和计算周期
uint8_t Capture_Flag = 1, Capture_end = 0;//Flag用于切换捕获状态,end表示捕获完成标志void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim); //定时器输入捕获回调函数声明
void Get_Duty(void); // 计算占空比函数声明
void Get_Freq(void); // 计算频率函数声明int main(void)
{HAL_TIM_IC_Start_IT(&htim8, TIM_CHANNEL_1); //启动定时器8通道1的输入捕获,并启用中断while (1){//Get_Freq(); // 在主循环中调用频率计算函数Get_Duty(); // 在主循环中调用占空比计算函数}
}/***************** 计算PWM占空比和频率 *****************/
void Get_Freq(void)
{ if (Capture_end == 0) return; // 如果捕获未完成,直接返回if (CCR1 < CCR3) Period_CCR = CCR3 - CCR1; // 计算周期(正常情况)else Period_CCR = 0xFFFF + 1 - CCR1 + CCR3; // 处理计数器溢出的情况 R40_Freq = 80000000 / 80 / Period_CCR; // 计算频率(系统时钟为80MHz,预分频为80)sprintf((char *)LCD_Test, " R40_Freq = %dHz ", R40_Freq); LCD_DisplayStringLine(Line5, LCD_Test); Capture_end = 0; // 重置捕获完成标志
}void Get_Duty(void)
{if (Capture_end == 0) return; // 如果捕获未完成,直接返回if (CCR1 < CCR2) High_CCR = CCR2 - CCR1; // 计算高电平时间(正常情况)else High_CCR = 0xFFFF + 1 - CCR1 + CCR2; // 处理计数器溢出的情况if (CCR1 < CCR3) Period_CCR = CCR3 - CCR1; // 计算周期(正常情况)else Period_CCR = 0xFFFF + 1 - CCR1 + CCR3; // 处理计数器溢出的情况 R40_Freq = 80000000 / 80 / Period_CCR; // 计算频率(系统时钟为80MHz,预分频为80)R40_Duty = High_CCR * 100 / (Period_CCR + 1); // 计算占空比(高电平时间占周期的百分比)sprintf((char *)LCD_Test, " R40_Freq = %dHz ", R40_Freq); LCD_DisplayStringLine(Line6, LCD_Test); sprintf((char *)LCD_Test, " R40_Duty = %d%% ", R40_Duty); LCD_DisplayStringLine(Line7, LCD_Test); Capture_end = 0; // 重置捕获完成标志
}void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if (htim == &htim8) // 判断是否是定时器8触发的中断{switch (Capture_Flag) // 根据捕获标志切换捕获状态{case 1:CCR1 = HAL_TIM_ReadCapturedValue(&htim8, TIM_CHANNEL_1);//第一次捕获值(上升沿)__HAL_TIM_SET_CAPTUREPOLARITY(&htim8,TIM_CHANNEL_1,TIM_INPUTCHANNELPOLARITY_FALLING);//设置为下降沿捕获Capture_Flag = 2; // 切换到下一次捕获状态break;case 2:CCR2 = HAL_TIM_ReadCapturedValue(&htim8, TIM_CHANNEL_1);//第二次捕获值(下降沿)__HAL_TIM_SET_CAPTUREPOLARITY(&htim8,TIM_CHANNEL_1,TIM_INPUTCHANNELPOLARITY_RISING);//设置为上升沿捕获Capture_Flag = 3; // 切换到下一次捕获状态break; case 3:CCR3 = HAL_TIM_ReadCapturedValue(&htim8, TIM_CHANNEL_1);//第三次捕获值(上升沿)Capture_end = 1; // 设置捕获完成标志Capture_Flag = 1; // 重置捕获状态break; default:break;}HAL_TIM_IC_Start_IT(&htim8, TIM_CHANNEL_1);//重新启动输入捕获}
}
六、ADC模块
6.1 赛题要求
(1)根据试题要求设计合理的电压数据采样频率,并对ADC采样到的电压数据进行有效的数字滤波。
(2)使用竞赛平台微控制器内部ADC测量电位器R37(R38)输出的电压信号。
6.2 模块原理图
滑动变阻器(也称为电位器)接在 VDD 和 GND 之间,并将滑动端连接到单片机的 ADC 引脚,这是一种常见的模拟信号采集电路。
非常简单!主要就是理解一下分压,不理解也没啥,记下就OK!
工作原理:
- 滑动变阻器的作用:
- 滑动变阻器是一个可调电阻,其总电阻值是固定的(10kΩ)。
- 滑动端将变阻器分为两部分电阻:R1 和 R2。
- 当滑动端移动时,R1 和 R2 的比例会发生变化,但 R1 + R2 的总电阻不变。
- 分压原理:
- 滑动变阻器与电源(VDD 和 GND)形成一个分压电路。
- 滑动端的电压由 R1 和 R2 的比例决定:Vadc = VDD * R2 / (R1 + R2)
- 当滑动端移动时,R1 和 R2 的比例变化,导致 Vadc 的电压变化。
- ADC 的作用:
- 单片机的 ADC(模数转换器)将滑动端的模拟电压(V_ADC)转换为数字值。
- ADC 的分辨率决定了数字值的精度(例如 12 位 ADC 的分辨率为 0~4095)。
6.3 编写代码
第一步,打开CubeMX。
以R37为例,对应的是PB15,也就是ADC2_IN15。
选择IN15 Single-ended
单通道,然后把Sampling Time
采样时间设置到最大。
为什么设置最大采样时间捏?
- 信号较弱或阻抗高:如果信号很弱(比如声音很小),或者信号源阻抗很高(比如信号线很长),ADC需要更多时间才能“听清楚”信号。
- 减少噪声干扰:如果信号中有噪声(比如杂音),较长的采样时间可以让ADC“过滤”掉这些噪声,得到更准确的值。
- 提高精度:采样时间越长,ADC采集的信号越稳定,结果越精确。
设置最大采样时间,就像让ADC“多听一会儿”信号,这样可以听清楚弱信号,过滤掉噪音,得到更准确的结果。
还有一个需要理解的,就是Resolution:ADC分辨率。
默认是,ADC 12-bit resolution:ADC分辨率为12位。我们不需要改动。
-
分辨率:ADC将模拟信号转换为数字信号时,能够区分的电压级别的数量。
就像尺子一样,有毫米级有厘米级。分辨率越高,ADC能够区分的电压级别越多,测量精度越高。
-
12位分辨率:ADC可以输出 2^12=4096 个不同的数字值(范围是0到4095)。
12位ADC可以将参考电压(如3.3V)分为4096个级别,每个级别对应的电压间隔为0.8mV。
第二步,记住启动和获取函数
启动ADC采样
HAL_ADC_Start(&hadc2);
- 启动ADC模块(
hadc2
)开始一次新的采样。 - ADC开始采集输入信号(如R37的电压),并将其转换为数字值。
获取ADC转换值
R37_Num = HAL_ADC_GetValue(&hadc2);
- 从ADC模块(
hadc2
)获取转换后的数字值,并存储到变量R37_Num
中。 - 将模拟信号(如R37的电压)转换为数字信号。
(ADC是自动将模拟电压转换为数字值,所以我们读取到的就是一个数字)
完整的如下:
uint16_t R37_Num = 0; // 存储ADC读取的原始值
float R37_Volt = 0; // 存储转换后的电压值
__IO uint32_t uwTick_ADC_Point = 0; // 记录上次读取ADC的Time Tick,和防止频繁读取 void Get_R37V(void)
{ if (uwTick - uwTick_ADC_Point < 50) return; // 如果小于50ms,则直接返回,避免频繁读取 uwTick_ADC_Point = uwTick; // 更新上次读取时间戳 HAL_ADC_Start(&hadc2); // 启动ADC以进行新一次的采样 R37_Num = HAL_ADC_GetValue(&hadc2); // 获取ADC转换值,并存储到 R37_Num // 将ADC数字值转换为对应的电压值 R37_Volt = R37_Num * 3.3 / 4095;
}
将数字值转换为实际电压的公式是:输入电压 = 数字值 × 参考电压 / 最大数字值
即:R37_Volt = R37_Num × (3.3 / 4095)
最后,在循环中调用!
while (1){Get_R37V();sprintf((char *)LCD_Test," R37_Volt = %0.2fV ",R37_Volt);LCD_DisplayStringLine(Line4,LCD_Test); }
完成了,嘻嘻。
6.4 赛题实战
同6.3,但是真题还要求“滤波”。
uint16_t R37_Num = 0; // 存储ADC读取的原始值
float R37_Volt = 0; // 存储转换后的电压值
__IO uint32_t uwTick_ADC_Point = 0; // 记录上次读取ADC的Time Tick,和防止频繁读取 void Get_R37V(void)
{ if (uwTick - uwTick_ADC_Point < 50) return; // 如果小于50ms,则直接返回,避免频繁读取 uwTick_ADC_Point = uwTick; // 更新上次读取时间戳 HAL_ADC_Start(&hadc2); // 启动ADC以进行新一次的采样 for(int i=1; i<=10;i++){R37_Num += HAL_ADC_GetValue(&hadc2);HAL_Delay(1);}// 将ADC数字值转换为对应的电压值 R37_Volt = R37_Num * 3.3 / 4095 / 10; R37_Num = 0;
}
七、EEPROM模块
7.1 赛题要求
(1)设定好的定时时间存储在EEPROM中。(小时分钟秒,共三个整数)
(2)设备重新上电时,应从E2PROM中载入上、下限提醒指示灯、上限电压、 下限电压参数。(整数/小数)
(3)通过E2PROM完成商品库存数量以及商品单价的存储。(整数/小数)
(4)通过竞赛平台上的E2PROM(AT24C02)保存商品库存数量和价格信息。(整数/小数)
存储位置要求如下:商品X库存数量存储地址:E2PROM内部地址0。
7.2 模块原理图
在原理图中,有两个芯片。
- M24C02-WMNSTP:EEPROM,用于存储数据,通过I2C通信。
- MCP4017-104ELT:数字电位器,用于调节电阻值,通过I2C通信。
此部分,我们只看存储模块。
M24C02-WMNSTP(EEPROM)
功能
- 电可擦除可编程只读存储器,用于存储小量数据(如配置参数、校准数据等)。
- 容量:2Kbit(256字节)。
引脚说明
- VCC (8):电源引脚(接正电压,通常3.3V或5V)。
- GND (4):接地引脚。
- SDA (5):I2C数据线,用于数据传输。
- SCL (6):I2C时钟线,用于同步通信。
- WC# (7):写保护引脚,低电平时允许写入,高电平时禁止写入。
- E1, E2, E3 (1, 2, 3):地址引脚都是低电平,用于设置设备地址。(记住这个!后面会用到)
既然我们用IIC通信,那我们就要了解单片机和AT24C02通信(读取和写入)的步骤!
首先,打开赛点资源包中的芯片手册。
(1)AT24C02的地址
设备地址:如果有多个设备,用于在I²C总线上唯一标识一个EEPROM设备。
地址格式
- 固定部分:前4位固定为
1010
。 - 可配置部分:由A₂、A₁、A₀引脚或页地址位(P0、P1、P2)决定。
- 读写位(R/W):最后1位,0=写,1=读。
这里有四种,我们是第一种!
型号 | 地址格式 | 说明 |
---|---|---|
1K/2K | 1 0 1 0 A₂ A₁ A₀ R/W | A₂、A₁、A₀由硬件引脚决定,支持8个设备地址。 |
我们的三个引脚都接了地(E1/2/3),那么我们读写的地址就是
- A₂、A₁、A₀接地(GND):即A₂=0、A₁=0、A₀=0。
- 设备地址:
- 写操作:
1010 000 0
→0xA0
(十六进制)。 - 读操作:
1010 000 1
→0xA1
(十六进制)。
- 写操作:
(2)字节写入
根据上图(8),以下是字节写入的步骤:
1. 开始信号(START) – 2. 发送设备地址[写](DEVICE ADDRESS +WRITE) – 3. 等待应答(ACK) – 4. 发送内存地址(WORD ADDRESS) – 5. 等待应答(ACK) – 6. 写入需要存储的数据(DATA) – 7.等待应答(ACK) – 8. 停止信号(STOP)
(3)随机读取
根据图11,以下是随机读取(读指定地址)的步骤如下:
1. 开始信号(START) – 2. 发送设备地址[写](DEVICE ADDRESS + WRITE) – 3. 等待应答(ACK) – 4. 发送要读取的内存地址(WORD ADDRESS) – 5. 等待应答(ACK) – 6. 重复起始条件(REPEATED START)-- 7. 发送设备地[读](DEVICE ADDRESS + READ) – 8. 等待应答(ACK) – 9. 读取数据(DATA)-- 10. 无应答信号(NOACK) – 10. 停止信号(STOP)
7.3 编写代码
第一步,将赛点资源包中i2c.c
和i2c.h
加入我们自己的工程中。
在i2c.h
,我们可以看到,官方已经给了我们底层代码,只需要理解就可以看着手册写啦
#ifndef __I2C_HAL_H
#define __I2C_HAL_H#include "stm32g4xx_hal.h" void I2CStart(void); // 启动I2C通信(发送起始条件)
void I2CStop(void); // 停止I2C通信(发送停止条件)
unsigned char I2CWaitAck(void); // 等待从设备的应答信号(ACK/NACK)
void I2CSendAck(void); // 主设备发送应答信号(ACK)
void I2CSendNotAck(void); // 主设备发送非应答信号(NACK)
void I2CSendByte(unsigned char cSendByte);// 主设备发送一个字节的数据
unsigned char I2CReceiveByte(void); // 主设备接收一个字节的数据
void I2CInit(void); // 初始化I2C外设#endif
第二步,根据手册进行编写(下面注释对应刚刚上面的步骤)
1. 写数据到EEPROM
void EEPROM_Write(uint8_t WriteAdd, uint8_t Data)
{I2CStart(); // 1. 开始信号(START)I2CSendByte(AT24C02_WriteAdd); // 2. 发送设备地址[写](DEVICE ADDRESS + WRITE)I2CWaitAck(); // 3. 等待应答(ACK)I2CSendByte(WriteAdd); // 4. 发送内存地址(WORD ADDRESS)I2CWaitAck(); // 5. 等待应答(ACK)I2CSendByte(Data); // 6. 写入需要存储的数据(DATA)I2CWaitAck(); // 7. 等待应答(ACK)I2CStop(); // 8. 停止信号(STOP)HAL_Delay(5); // 延时5ms,确保写入完成
}
2. 从EEPROM读取数据
uint8_t EEPROM_Read(uint8_t ReadAdd)
{uint8_t Data;I2CStart(); // 1. 开始信号(START)I2CSendByte(AT24C02_WriteAdd); // 2. 发送设备地址[写](DEVICE ADDRESS + WRITE)I2CWaitAck(); // 3. 等待应答(ACK)I2CSendByte(ReadAdd); // 4. 发送要读取的内存地址(WORD ADDRESS)I2CWaitAck(); // 5. 等待应答(ACK)I2CStart(); // 6. 重复起始条件(REPEATED START)I2CSendByte(AT24C02_ReadAdd); // 7. 发送设备地址[读](DEVICE ADDRESS + READ)I2CWaitAck(); // 8. 等待应答(ACK)Data = I2CReceiveByte(); // 9. 读取数据(DATA)I2CSendNotAck(); // 10. 无应答信号(NOACK)I2CStop(); // 11. 停止信号(STOP)return Data; // 返回读取的数据
}
然后,学会调用。
uint8_t data = 0xAB; // 要写入的数据
uint8_t eeprom_Address = 0x01; // 写入EEPROM的指定内存地址
uint8_t dataRead;// 写入数据到 EEPROM
EEPROM_Write(eeprom_Address, data);// 从 EEPROM 读取数据
dataRead = EEPROM_Read(eeprom_Address);
7.4 赛题实战
真题中,只有读取小数和整数。整数正常读取,小数我把它变成整数写入,读取时再转换回来。
/*************** EEPROM相关变量 **********************/
uint8_t EEP_Add[15] = {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F}; // EEPROM存储地址
uint8_t Hour[5]={1,3,5,7,10},Min[5]={2,4,6,8,45},Sec[5]={1,2,4,45,59}; // 存储时间数据
uint8_t H[5] = {0,0,0,0,0},M[5] = {0,0,0,0,0},S[5] = {0,0,0,0,0}; // 读取时间数据
uint8_t Shop_X = 23,Shop_Y = 10; // 存储商品个数
float Price_X = 1.0,Price_Y = 2.0; // 存储商品价格
uint8_t S_X = 0,S_Y = 0; // 读取的商品个数
float P_X = 0,P_Y = 0; // 读取的商品价格/***************** AT24C02存储和读取数据 *****************/// 存储商品数据到EEPROM
void EEPROM_WriteShop(void)
{EEPROM_Write(0x01,Shop_X); // 将Shop_X写入EEPROM地址0x01EEPROM_Write(0x02,Shop_Y); // 将Shop_Y写入EEPROM地址0x02EEPROM_Write(0x03,Price_X * 10); // 将Price_X放大10倍后写入EEPROM地址0x03EEPROM_Write(0x04,Price_Y * 10); // 将Price_Y放大10倍后写入EEPROM地址0x04LCD_DisplayStringLine(Line3,(unsigned char*)" Write ");sprintf((char *)LCD_Test,"X:%d(%0.1f),Y:%d(%0.1f)",Shop_X,Price_X,Shop_Y,Price_Y);LCD_DisplayStringLine(Line4,LCD_Test);
}// 从EEPROM读取商品数据
void EEPROM_ReadShop(void)
{S_X = EEPROM_Read(0x01); // 从EEPROM地址0x01读取商品X的个数S_Y = EEPROM_Read(0x02); // 从EEPROM地址0x02读取商品Y的个数P_X = (float)EEPROM_Read(0x03) / 10; // 从EEPROM地址0x03读取商品X的价格并缩小10倍P_Y = (float)EEPROM_Read(0x04) / 10; // 从EEPROM地址0x04读取商品Y的价格并缩小10倍LCD_DisplayStringLine(Line6,(unsigned char*)" Read ");sprintf((char *)LCD_Test,"X:%d(%0.1f),Y:%d(%0.1f)",S_X,P_X,S_Y,P_Y);LCD_DisplayStringLine(Line7,LCD_Test);
}// 存储时间数据到EEPROM
void EEPROM_WriteTime(void)
{for(int i = 0;i < 5;i ++) // 遍历5组时间数据{EEPROM_Write(EEP_Add[i],Hour[i]); // 将Hour[i]写入EEPROM地址EEP_Add[i]EEPROM_Write(EEP_Add[i+5],Min[i]); // 将Min[i]写入EEPROM地址EEP_Add[i+5]EEPROM_Write(EEP_Add[i+10],Sec[i]); // 将Sec[i]写入EEPROM地址EEP_Add[i+10]}LCD_DisplayStringLine(Line3,(unsigned char*)" Write ");sprintf((char *)LCD_Test,"Time:%02d-%02d-%02d ",Hour[1],Min[1],Sec[1]);LCD_DisplayStringLine(Line4,LCD_Test); sprintf((char *)LCD_Test,"Time:%02d-%02d-%02d ",Hour[4],Min[4],Sec[4]);LCD_DisplayStringLine(Line5,LCD_Test);
}// 从EEPROM读取时间数据
void EEPROM_ReadTime(void)
{for(int i = 0;i < 5;i ++) // 遍历5组时间数据{H[i] = EEPROM_Read(EEP_Add[i]); // 从EEPROM地址EEP_Add[i]读取Hour[i]M[i] = EEPROM_Read(EEP_Add[i+5]); // 从EEPROM地址EEP_Add[i+5]读取Min[i]S[i] = EEPROM_Read(EEP_Add[i+10]); // 从EEPROM地址EEP_Add[i+10]读取Sec[i]}LCD_DisplayStringLine(Line6,(unsigned char*)" Read ");sprintf((char *)LCD_Test,"Time:%02d-%02d-%02d ",H[1],M[1],S[1]);LCD_DisplayStringLine(Line7,LCD_Test); sprintf((char *)LCD_Test,"Time:%02d-%02d-%02d ",H[4],M[4],S[4]);LCD_DisplayStringLine(Line8,LCD_Test);
}
然后,在循环调用就好啦!
while (1){KEY_Proc();if(KEY_State() == 1){
// EEPROM_WriteShop();EEPROM_WriteTime();}else if(KEY_State() == 2){
// EEPROM_ReadShop();EEPROM_ReadTime();}}
- 按下按键1:将数据写入EEPROM,并在LCD上显示写入状态和数据。
- 按下按键2:从EEPROM读取数据,并在LCD上显示读取状态和数据。
特意放在最后,最最最最最重要也特别容易忘的!
要在int main
中初始化IIC
哦!
int main(void)
{I2CInit(); //IIC初始化while (1){... ...}
}
又完成了一个,耶耶耶!
八、RTC模块
8.1 赛题要求
(1)通过单片机片内RTC设计实现时钟功能。
8.2 模块原理图
RTC(Real-Time Clock)是STM32中独立于主系统的低功耗定时器,用于实现日历、闹钟、周期性唤醒等功能。
在CubeMX中,我们可以看到
RTC需要一个稳定的时钟源来计时,我们有以下三种选择:
- HSE(外部高速时钟):通常为8MHz或更高频率,可以通过分频器降低到32.768kHz。
- LSE(外部低速晶振):32.768kHz,精度高,常用于RTC。
- LSI(内部低速RC振荡器):约32kHz,精度较低,但无需外部晶振。
在很多板子上,会有一个纽扣电池,就是为了单片机掉电情况下可以长时间为RTC
和LSE
供电,这样即使主电源断开,RTC还能继续计时,时间信息不会丢失。
我们不需要配置这儿,蓝桥杯板上并没有LSE
,所以直接(默认)使用LSI
即可,LSE
频率大约是32kHz,虽然精度不如LSE
高,但足够满足RTC的基本计时需求。
8.3 编写代码
第一步,打开CubeMX。
启用RTC时钟源:激活RTC功能
- 在
RTC Mode and Configuration
标签页中:- 勾选
Activate Clock Source
(启用RTC时钟源)。 - 勾选
Activate Calendar
(启用日历功能)。
- 勾选
配置预分频器:RTC的时钟源(LSI)需要通过预分频器分频为1Hz(也就是1秒/一次计数)。
-
异步预分频(Asynchronous Predivider):
31
计算公式:异步分频后频率 = LSI频率 / (Asynchronous值 + 1)
32kHz / (31 + 1) = 1000Hz。(这一步将高频信号降到1000hz) -
同步预分频(Synchronous Predivider):
999
计算公式:最终频率 = 异步分频后频率 / (Synchronous值 + 1)
1000Hz / (999 + 1) = 1Hz。(最终得到精确的1Hz信号,驱动RTC的秒计数器)
设置初始时间与日期:在Calendar Time/Data
中设置初始时间(Time)和日期(Date)
- 时间格式:选择
BIN
(二进制格式),避免BCD编码的复杂性。(这儿没设置,代码中设置的BIN) - 示例设置:
- 时间:12:30:00(HOURS=12, MINUTES=30, SECONDS=0)。
- 日期:2023年10月1日(YEAR=23, MONTH=10, DATE=1)。
最后!还有一个,在Hour Format
选项中选择:
RTC_HOURFORMAT_24
(24小时制)–(默认)RTC_HOURFORMAT_12
(12小时制)
完成,生成代码!
第二步,开始编写代码。
首先,先认识一下两个结构体
在STM32 HAL库中,RTC的时间和日期是分开存储的,分别用两个结构体来表示:
RTC_TimeTypeDef
:用于存储时间(时、分、秒)。RTC_DateTypeDef
:用于存储日期(年、月、日、星期)。
这两个结构体是 HAL_RTC_GetTime
和 HAL_RTC_GetDate
函数的参数,用于接收从RTC模块读取的时间和日期数据。因此,必须先声明这两个结构体变量,才能调用函数并存储读取到的数据!!!
RTC_TimeTypeDef 结构体
typedef struct {uint8_t Hours; // 小时(0-23 或 1-12)uint8_t Minutes; // 分钟(0-59)uint8_t Seconds; // 秒(0-59)uint8_t TimeFormat; // 时间格式(RTC_HOURFORMAT_AM或PM,仅12小时制有效)uint32_t SubSeconds; // 子秒(未使用)uint32_t SecondFraction; // 秒分数(未使用)uint32_t DayLightSaving; // 夏令时配置(未使用)uint32_t StoreOperation; // 存储操作(未使用)
} RTC_TimeTypeDef;
RTC_DateTypeDef 结构体
typedef struct {uint8_t WeekDay; // 星期几(1-7,1=Monday)uint8_t Month; // 月份(1-12)uint8_t Date; // 日期(1-31)uint8_t Year; // 年份(0-99,表示2000-2099)
} RTC_DateTypeDef;
接下来,我们得记住考试中用到的两个RTC
函数。
HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format)
HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format)
HAL_RTC_GetTime
- 功能:从RTC模块读取当前时间。
- 参数:
hrtc
:RTC句柄。sTime
:存储时间的结构体(RTC_TimeTypeDef
)。Format
:时间格式(RTC_FORMAT_BIN
或RTC_FORMAT_BCD
)。
HAL_RTC_GetDate
- 功能:从RTC模块读取当前日期。
- 参数:
hrtc
:RTC句柄。sDate
:存储日期的结构体(RTC_DateTypeDef
)。Format
:日期格式(RTC_FORMAT_BIN
或RTC_FORMAT_BCD
)。
非常简单,直接代入!
RTC_TimeTypeDef RTC_Time;
RTC_DateTypeDef RTC_Data;
__IO uint32_t uwTick_RTC_point;void Get_RTC(void)
{if(uwTick - uwTick_RTC_point < 100) return; //避免频繁进入uwTick_RTC_point = uwTick;HAL_RTC_GetTime(&hrtc,&RTC_Time,RTC_FORMAT_BIN);HAL_RTC_GetDate(&hrtc,&RTC_Data,RTC_FORMAT_BIN); sprintf((char *)LCD_Test,"Time:%02d-%02d-%02d ",RTC_Time.Hours,RTC_Time.Minutes,RTC_Time.Seconds); LCD_DisplayStringLine(Line7,LCD_Test); sprintf((char *)LCD_Test,"Data:%02d-%02d-%02d ",RTC_Data.Year,RTC_Data.Month,RTC_Data.Date);LCD_DisplayStringLine(Line8,LCD_Test);
}
然后在循环中调用就好了!
while (1){Get_RTC();}
注:%02d
不是%2d
,用于少于两位在前面补零。
8.4 赛题实战
同8.3,后续若有其他再补充。
九、补充
后续进行补充
按键双击、DMA
总结
自用