【蓝桥杯嵌入式】各模块学习总结

server/2025/2/28 6:16:30/

系列文章目录

留空


文章目录

  • 系列文章目录
  • 前言
  • 一、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值。

D1234567890
LE1110011111
Q1233367890
  • 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全部为低电平

PC15PC14PC13PC12PC2PC1PC0
1111111

再举个例子

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

PC15PC14PC13PC12PC11PC10PC9PC8
01010101

代码如下

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.cLED.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 - led_uwTick < 100) return;led_uwTick = uwTick;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(uwTick - led_uwTick < 100) return;led_uwTick = uwTick;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(uwTick - led_uwTick < 100) return;led_uwTick = uwTick;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_RSRS寄存器选择信号,低电平选择索引或状态寄存器,高电平选择控制寄存器
LCD_RDnRD读使能信号,低电平时启用读操作
LCD_D0 - D15DB[17:0]数据总线,用于并行数据传输,在不同宽度的接口模式下使用
LCD_HDR不直接对应
VDDVCI/VDD电源引脚,提供电源电压到LCD模块和控制器
GNDGND地线电源,用于连接系统地

要深入了解可以去查看数据手册,这儿并不需要我们理解,看看就好啦。

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.clcd.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.cKEY.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)

  1. 找到 Connectivity -> USART1
  2. Mode 设置为 Asynchronous(异步模式)。
  3. 设置以下参数:
    • Baud Rate:波特率(9600)
    • Word Length:数据位长度(默认 8 位)。
    • Parity:校验位(默认 None)。
    • Stop Bits:停止位(默认 1 位)。
    • Data Direction:Receive and Transmit。

在这里插入图片描述

配置中断

如果使用中断接收数据:

  1. NVIC Settings 中,勾选 USART1 global interrupt
  2. 设置优先级(看情况)。

在这里插入图片描述

生成代码!

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;	}
}

这儿,我们需要理解一下两个函数的使用。sscanfsprintf ,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信号。(原理太多直接省略啦)

微控制器的 PA15PB4 引脚用于监测信号。

在这里插入图片描述

设置输出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 1Input Capture direct mode
  • 参数设置
    • Prescaler (PSC):71(分频完为1MHz)。
    • Counter Period (ARR):65535(16位定时器最大值,防止溢出)。
  • 触发边沿
    • IC1 PolarityRising 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 模块原理图

滑动变阻器(也称为电位器)接在 VDDGND 之间,并将滑动端连接到单片机的 ADC 引脚,这是一种常见的模拟信号采集电路。

非常简单!主要就是理解一下分压,不理解也没啥,记下就OK!

在这里插入图片描述

工作原理:

  1. 滑动变阻器的作用
    • 滑动变阻器是一个可调电阻,其总电阻值是固定的(10kΩ)。
    • 滑动端将变阻器分为两部分电阻:R1R2
    • 当滑动端移动时,R1R2 的比例会发生变化,但 R1 + R2 的总电阻不变。
  2. 分压原理
    • 滑动变阻器与电源(VDD 和 GND)形成一个分压电路。
    • 滑动端的电压由 R1R2 的比例决定:Vadc = VDD * R2 / (R1 + R2)
    • 当滑动端移动时,R1R2 的比例变化,导致 Vadc 的电压变化。
  3. 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/2K1 0 1 0 A₂ A₁ A₀ R/WA₂、A₁、A₀由硬件引脚决定,支持8个设备地址。

我们的三个引脚都接了地(E1/2/3),那么我们读写的地址就是

  • A₂、A₁、A₀接地(GND):即A₂=0、A₁=0、A₀=0。
  • 设备地址:
    • 写操作:1010 000 00xA0(十六进制)。
    • 读操作:1010 000 10xA1(十六进制)。

(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.ci2c.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,精度较低,但无需外部晶振。

在很多板子上,会有一个纽扣电池,就是为了单片机掉电情况下可以长时间为RTCLSE供电,这样即使主电源断开,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_GetTimeHAL_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_BINRTC_FORMAT_BCD)。

HAL_RTC_GetDate

  • 功能:从RTC模块读取当前日期。
  • 参数
    • hrtc:RTC句柄。
    • sDate:存储日期的结构体(RTC_DateTypeDef)。
    • Format:日期格式(RTC_FORMAT_BINRTC_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


总结

自用


http://www.ppmy.cn/server/171228.html

相关文章

Xcode如何高效的一键重命名某个关键字

1.选中某个需要修改的关键字&#xff1b; 2.右击&#xff0c;选择Refactor->Rename… 然后就会出现如下界面&#xff1a; 此时就可以一键重命名了。 还可以设置快捷键。 1.打开Settings 2.找到Key Bindings 3.搜索rename 4.出现三个&#xff0c;点击一个地方设置后其…

派可数据BI接入DeepSeek,开启智能数据分析新纪元

派可数据BI产品完成接入DeepSeek&#xff0c;此次接入标志着派可数据BI在智能数据分析领域迈出了重要一步&#xff0c;将为用户带来更智能、更高效、更便捷的数据分析体验。 派可数据BI作为国内领先的商业智能解决方案提供商&#xff0c;一直致力于为用户提供高效、稳定易扩展…

Springboot + Ollama + IDEA + DeepSeek 搭建本地deepseek简单调用示例

1. 版本说明 springboot 版本 3.3.8 Java 版本 17 spring-ai 版本 1.0.0-M5 deepseek 模型 deepseek-r1:7b 需要注意一下Ollama的使用版本&#xff1a; 2. springboot项目搭建 可以集成在自己的项目里&#xff0c;也可以到 spring.io 生成一个项目 生成的话&#xff0c;如下…

【Android】ViewPager的使用

AndroidManifest.xml <?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"><applicationandroid:allowBac…

【Go】十七、grpc 服务的具体功能编写

服务的具体编写 获取品牌信息的基础逻辑 我们为了便于测试&#xff0c;可以先把方法写成下面这样&#xff1a; type GoodsServer struct {proto.UnimplementedGoodsServer }之后再 test/brands.go 中进行编写测试代码&#xff1a; // 创建客户端 var brandClient proto.Goo…

ESP32移植Openharmony外设篇(9)NB-IOT

NB-IOT&#xff08;窄带物联网&#xff09; 模块介绍 NB-IoT&#xff08;Narrowband Internet of Things&#xff09;是一种低功耗广域物联网&#xff08;LPWAN&#xff09;技术&#xff0c;专为低功耗、低数据速率和大规模连接的物联网应用而设计。它采用窄带宽信道和低复杂…

QtPropertyBrowser实现属性管理中的下拉框

#include <QtWidgets> #include <QtPropertyBrowser>class Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr) : QWidget(parent) {// 创建属性浏览器QtTreePropertyBrowser *browser new QtTreePropertyBrowser(this);// 创建枚举属性管…

盲视观测者效应:认知的量子诗学 AI回复盲人双缝实验

&#x1f30c; **《盲视观测者效应&#xff1a;认知的量子诗学》** ### **一、盲视者的波函数坍缩** 当盲人"观察"双缝实验时&#xff1a; - 他的视觉皮层正在用触觉重构量子态 - 指尖的震动频率 ≈ 光子的概率波函数 - 导盲杖的敲击声 新的观测暴力系…