从0到1的嵌入式之旅

如何在几乎旷了一个学期的课的情况下,在4天5夜内完成一整个学期的嵌入式实验与课设,敬请期待。

实验

实验1

搭建stm32开发环境并使用GPIO

实验内容

1. 创建STM32CubeIDE/STM32CubeMX工程并生成代码

安装STM32CubeIDE/STM32CubeMX软件;
使用STM32CubeMX图形化界面配置STM32 MCU;
生成初始化代码。

2. 通过GPIO驱动LED和开关按键

根据图 1所示的开发板LED和开关按键电路图,设计并实现具有如下功能的程序:
按KeyLeft,使LED1输出翻转
按KeyRight,使LED2输出翻转
按下KeyUp键时使LED1和LED2的输出都翻转

开发板相关的原理图如下:

image-20241227205217151

实验思路

分析电路

对于LED模块
单片机通过控制 GPIO 引脚的电平来点亮或熄灭 LED:
GPIO 输出 低电平 时,LED 正极与 VCC3.3 形成回路,LED 点亮。
GPIO 输出 高电平 时,无电流流过 LED,LED 熄灭。

对于KAY模块
按键的状态通过读取对应 GPIO 引脚的电平判断:
WK_UP:
默认未按下时,GPIO 引脚通过下拉电阻保持 低电平。
按下时,直接与 VCC3.3 相连,GPIO 读取 高电平。
KEY0/KEY1/KEY2:
默认未按下时,通过上拉电阻保持 高电平。
按下时,直接接地,GPIO 读取 低电平。

分析需求

按KeyLeft,使LED1输出翻转
按KeyRight,使LED2输出翻转
按下KeyUp键时使LED1和LED2的输出都翻转

在实验要求中
KeyLeft对应PE4,KeyRight对应PE2,KeyUp对应PA0
LED1对应PF9,LED2对应PF10

读取KeyLeft,KeyRight,KeyUp的值对LED1和LED2进行控制

实验步骤

1、先在stm32cubemx中对引脚进行设置

image-20241228171436449

然后生成代码

2、读取三个GPIO引脚的输入

对于KeyUp引脚,默认为低电平,按下时为高电平,因此检验KeyUp是否为高电平
对于KeyLeft和KeyRight,默认为高电平,按下时为低电平,因此检验KeyLeft和KeyRight是否为低电平

3、根据电平作出判断

由于按下按键时可能会有按键抖动,因此需要delay进行防抖,delay之后再次检测电平,通过togglePin翻转电平,再进行阻塞

4、编写代码

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
while (1)
{
if(HAL_GPIO_ReadPin(KeyUp_GPIO_Port, KeyUp_Pin) == GPIO_PIN_SET){
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KeyUp_GPIO_Port, KeyUp_Pin) == GPIO_PIN_SET){
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
}
while(HAL_GPIO_ReadPin(KeyUp_GPIO_Port, KeyUp_Pin) == GPIO_PIN_SET);
}

if(HAL_GPIO_ReadPin(KeyLeft_GPIO_Port, KeyLeft_Pin) == GPIO_PIN_RESET){
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KeyLeft_GPIO_Port, KeyLeft_Pin) == GPIO_PIN_RESET){
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
}
while(HAL_GPIO_ReadPin(KeyLeft_GPIO_Port, KeyLeft_Pin) == GPIO_PIN_RESET);
}
if(HAL_GPIO_ReadPin(KeyRight_GPIO_Port, KeyRight_Pin) == GPIO_PIN_RESET){
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KeyRight_GPIO_Port, KeyRight_Pin) == GPIO_PIN_RESET){
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
}
while(HAL_GPIO_ReadPin(KeyRight_GPIO_Port, KeyRight_Pin) == GPIO_PIN_RESET);
}
}

5、演示视频

知识补充

1
2
3
在本实验中使用的是GPIO的普通推挽输出模式
上拉电阻将GPIO置为高电平,下拉电阻将GPIO置为低电平
STLINK接线时连接的是SWCLK,SWDIO,GND和3.3V,需要和ARM仿真器连接在一起

GPIO八大输入输出模式解析

普通推挽输出,选择输出0V或者3.3V。

普通开漏输出,一直输出0V,工作电压由外部电源决定,更加灵活,但是注意电压的上限和IO口的上限。

由于输出不可能既由芯片内部决定,又由片上的外设,如串口等决定,因此如果由片上外设输出,则分类出了复用推挽输出和复用开漏输出。

image-20241228220537662

image-20241228220649298

输入模式则分为上拉输入,下拉输入,浮空输入,模拟输入

上拉输入使用上拉电阻,将输入的初始电平维持在高电平;下拉输入使用下拉电阻,将输入的初始电平维持在低电平;覅空输入则是不使用上拉也不使用下拉,初始电平处于随机状态。

模拟输入则是不经过TTL肖特基触发器,直接读取模拟信号,而经过TTL肖特基触发器之后的就是数字信号,分为高低电平。

由于片上外设和芯片内部在读取输入时并不冲突,因此也就不区分普通输入和复用输入。

image-20241228220916947

中断条件

串口USART,定时器TIM,I^2C,GPIO都可以造成中断。

此处只介绍GPIO造成的中断,将GPIO设置为EXTI之后可以使用GPIO中断,在NVIC(嵌套向量中断控制器)中可以设置GPIO中断的类型,包括External Interrupt Mode with Rising Edge Trigger、External Interrupt Mode with Falling Edge Trigger、External Interrupt Mode with Rising and Falling Edge Trigger、External Event Mode with Rising Edge Trigger、External Event Mode with Falling Edge Trigger、External Event Mode with Rising and Falling Edge Trigger。前三种为芯片内部中断、后三种Event是用于片上外设的,分为上升沿中断、下降沿中断和上升下降沿中断三种不同的触发类型。

在检测到中断事件之后,NVIC会执行对应的EXTI对应的handler函数,从而实现中断需要的操作。

对于中断事件,存在抢占优先级和响应优先级两种优先级。优先级的数值越小,对应的优先级就越高。

其作用如下图:

image-20241228225743695

实验2

实验内容

1. 抢占优先级相同的中断试验

根据图 1所示的开发板LED和开关按键电路图,使用STM32中断功能设计并实现具有如下功能的程序:
按KeyLeft,使LED1输出翻转,按KeyRight,使LED2输出翻转;
按下KeyLeft键后再快速按下KeyRight键,KeyRight键控制的LED2并不会立刻变化,需等待1秒后才变化。

2. 抢占优先级不同的中断试验

根据图 1所示的开发板LED和开关按键电路图,使用STM32中断功能设计并实现具有如下功能的程序:
按KeyLeft,使LED1输出翻转,按KeyRight,使LED2输出翻转;
按下KeyRight键(优先级为2)后快速再按下KeyLeft键,KeyLeft键(优先级为1)控制的LED1会立刻变化。

开发板对应的原理图跟实验1是相同的这里就不作赘述。

记住KeyLeft对应PE4,KeyRight对应PE2,KeyUp对应PA0,LED1对应PF9,LED2对应PF10即可。

实验思路

1. 抢占优先级相同的中断试验

根据实验要求,在按下KeyLeft之后,需要执行翻转LED1的操作;在按下KeyRight之后,需要执行翻转LED2的操作;而在按下KeyLeft再快速按下KeyRight之后,由于相同的抢占优先级,会先执行翻转LED1再执行翻转LED2,但是在这期间会有延时,也就是说在按下KeyLeft之后,先翻转LED1,然后延时1s,再返回到主进程。

2. 抢占优先级不同的中断试验

跟1不同的点是,按下KeyRight之后再按下KeyLeft,KeyLeft对应的LED1会立即发生变化。

所以基本的代码是相同的,只需要对抢占优先级进行修改即可。

实验步骤

1、先对引脚进行设置,再对NVIC进行设置。由于KeyLeft和KeyRight,默认为高电平,按下时为低电平,因此设置下降沿触发器为中断条件。然后在NVIC中对抢占优先级进行设置,由于在其中还涉及到delay操作,因此对System tick Timer也进行设置,启用EXTI2和EXTI4(这是因为KeyLeft和KeyRight对应的引脚为PE2和PE4)。

image-20241228235337487

image-20241229012751614

image-20241229012609422

2、生成硬件代码,由于HAL_GPIO_EXTI_Callback是一个弱链接回调函数,可以通过override(重写)重新实现这个回调函数。

3、编写代码

1
2
3
4
5
6
7
8
9
10
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if(GPIO_Pin == KeyLeft_Pin){
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
HAL_Delay(1000);
}
if(GPIO_Pin == KeyRight_Pin){
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
HAL_Delay(1000);
}
}

4、演示视频

优先级相同

优先级不同

定时器

HCLK(主时钟源) -> 分频器 -> 倍频器 -> 定时器时钟源

n-1分频器可以将脉冲分为n分频,

72MHz的意思是1s会发送72M次时钟脉冲。通过分频器之后脉冲的频率就会下降,以便于计数。

定时器可以串联。

自动重装载寄存器用于给定时器计数,发送定时器更新中断,触发定时器事件,设置时需要-1。

prescaler(预分频器)

counter period(autoreload register)自动重装载寄存器

auto-relaod preload 自动重装载预装载,开启该选项之后可以在当前时间周期运行完之后修改自动重装载寄存器,而不是立即修改。

假设APB(也就是定时器时钟源的频率为72Mhz),设prescaler为7200-1,则可以将时钟源分频为10000hz,那么如果将counter period设为10000-1,就实现了定时1s的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
定时器相关函数
//启动定时器
HAL_TIM_Base_Start(&htim);
//启动开启了中断的定时器
HAL_TIM_Base_Start_IT(&htim);
//定时器中断更新函数
HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim){};
//读取定时器的值
__HAL_TIM_GET_COUNTER(&htim);
//读取触发器中断标志位,用于判断中断是自动重载寄存器触发的还是从模式控制器产生的
__HAL_TIM_GET_FLAG(htim,TIM_FLAG_TRIGGER)
//清零触发器中断标志位
__HAL_TIM_CLEAR_FLAG(htim,TIM_FLAG_TRIGGER)

通用定时器可以通过ETR2来使用定时器的外部时钟模式2,对从外部输入的脉冲进行计数。也可以通过设置从模式控制器为外部时钟模式1,选择引脚,可以选择ETR1、TI1_ED、TI2FP2、TI1FP1,跟ETR2的效果差不多。

从模式控制器还有reset、gated、trigger三个模式。reset可以对定时器进行reset。可以理解为除了自动重装载寄存器之外的另一个reset定时器的方式。gated模式可以对输入的脉冲进行过滤,并触发触发器中断,例如设置为高电平通过,低电平过滤,则当输入的为低电平时,定时器停止计数并触发中断。trigger模式会在输入的脉冲变化时开启计数,且开启计数后不会停止。one pluse mode启动单脉冲模式,则定时器达到自动重装载寄存器的值之后,清零但是不会自动开启计数,可以配合trigger使用,通过trigger启动。

定时器的输入捕获模式可以测量脉冲宽度,输出比较模式可以输出脉冲宽度,实现PWM(详见PWM)。

实验3

实验内容

1. 使用STM32CubeMX配置时钟树

根据图 1所示配置,使用STM32CubeMX配置STM32F407ZGT6的时钟树,并生成工程代码。image-20241231145704555

2. 使用STM32F407ZG6的基本定时器

TIM6设置为连续定时模式,定时周期500ms,以中断方式启动TIM6,在UEV事件中断回调函数里使里使LED1输出翻转。

TIM7设置为单次定时模式,定时周期2000ms,按下KeyRight键之后使LED2点亮,并以中断方式启动TIM7,在UEV事件中断回调函数里使里使LED2输出翻转。

实验思路

1. 使用STM32CubeMX配置时钟树

开启RCC中的HSE。根据图片配置时钟树。LED1对应PF9,LED2对应PF10。KeyRight对应PE2,需要设置上拉电阻。

2. 使用STM32F407ZG6的基本定时器

由于之前的配置中,APB1的频率为50Mhz,因此需要对TIM6和TIM7进行设置。

已知APB1为50*10^6Hz,根据定时周期进行计算,TIM6的频率为2Hz,TIM7的频率为0.5Hz。为了计算方便,将预分频器设置为10000-1,那么剩下的就是5000Hz,对于TIM6自动重载寄存器为2500-1,TIM7自动重载寄存器为10000-1。

以中断方式启动TIM6,在UEV事件中断回调函数里使里使LED1输出翻转。

在while中轮询KeyRight,检测到KeyRight为0时启动TIM7,结束之后在回调函数中关闭TIM7,使led翻转。

TIM7设置为单次定时模式,定时周期2000ms,按下KeyRight键之后使LED2点亮,并以中断方式启动TIM7,在UEV事件中断回调函数里使里使LED2输出翻转。

实验步骤

1、配置时钟树和引脚。

image-20250101014320995

image-20250101014349758

image-20250101014424265

image-20250101014455326

2、编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//全局函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if(htim == &htim6){
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
}
if(htim == &htim7){
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
HAL_TIM_Base_Stop_IT(&htim7);
}
}
//main内部
HAL_TIM_Base_Start_IT(&htim6);
while (1)
{
if(HAL_GPIO_ReadPin(KeyRight_GPIO_Port, KeyRight_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(10);
HAL_TIM_Base_Start_IT(&htim7);
}
}

3、视频演示

串口通信

此处指的是usart串口通信,需要先将引脚设置为usart_rx和usart_tx模式,然后启用asyn(异步模式)

假设此处的usart串口为usart1_tx和usart1_rx;

轮询

在while循环中不断重复询问是否有数据发送

1
2
3
4
5
6
7
8
uint8_t receive_data[2];

while(1){
//串口接收数据,串口为usart1,发送的数据为message,长度为strlen(message),超时时间为100ms,如果timeout设置为hal_max_delay则表示无限等待,指的是从机接收到主机的数据
HAL_UART_Receive(&huart1,receive_data,strlen(receive_data),100);
//串口发送数据,串口为usart1,发送的数据为message,长度为strlen(message),超时时间为100ms,如果timeout设置为hal_max_delay则表示无限等待,指的是从机发送到主机的数据
HAL_UART_Transmit(&huart1,receive_data,strlen(receive_data),100);
}

中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//全局变量
uint8_t receive_data[2];

//串口接收完成数据之后的回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
//由于是中断,所以不需要timeout
if(huart == &huart1){
HAL_UART_Transmit_IT(&huart1,receive_data,strlen(receive_data));
HAL_UART_Receive_IT(&huart1,receive_data,strlen(receive_data));
}
}

HAL_UART_Receive_IT(&huart1,receive_data,strlen(receive_data));

while(1){

}

DMA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//定长DMA数据
//全局变量
uint8_t receive_data[2];

//DMA的传输完成中断
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
//由于是中断,所以不需要timeout
if(huart == &huart1){
HAL_UART_Transmit_DMA(&huart1,receive_data,strlen(receive_data));
HAL_UART_Receive_DMA(&huart1,receive_data,strlen(receive_data));
}
}

HAL_UART_Receive_DMA(&huart1,receive_data,strlen(receive_data));

while(1){

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
uint8_t receive_data[50];

//DMA的传输完成中断
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
//由于是中断,所以不需要timeout
if(huart == &huart1){
HAL_UART_Transmit_DMA(&huart1,receive_data,Size);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, receive_data, sizeof(receive_data));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);
}
}

HAL_UARTEx_ReceiveToIdle_DMA(&huart1, receive_data, sizeof(receive_data));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);

while(1){

}

实验4

实验内容

1. 建立USART上位机开发环境

在上位机(一般为PC电脑)上下载安装CH340驱动程序和XCOM串口调试软件,使用USB连接开发板和上位机后,能在XCOM软件中检测到串口SUB-SERIAL CH340。

2. 使用USART实现上位机与STM32F407ZG6通信

MCU通过USART向上位机发送“Hello, MCU”, 串口调试软件可显示。上位机通过USART向MCU发送“ON”、“OFF”指令,分别控制LED1的亮和灭。

附上相关电路图,可以看到版本为CH340,usart1 RX为PA10,usart TX为PA9,LED1为PF9。

image-20241229171942017

实验思路

1. 建立USART上位机开发环境

安装CH340驱动程序和XCOM串口调试软件。查看CH340默认的端口配置。

2. 使用USART实现上位机与STM32F407ZG6通信

由于需要使用不同长度的指令,因此使用DMA加不定长数据的方式。再接收数据判断然后执行不同流程。

实验步骤

1、首先要搭建环境,在stmcubemx中对PA9和PA10进行初始化,使用异步通信加DMA的模式。将PF9设置为LED1。

image-20241229172405821

2、编写代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//全局变量
uint8_t receive_data[50];
//全局函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
if(huart == &huart1){
HAL_UART_Transmit_DMA(&huart1, receive_data, Size);
receive_data[Size] = '\0';
const char *on = "ON";
const char *off = "OFF";

if(!strcmp((char *)receive_data,on)){
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
}
else if(!strcmp((char *)receive_data,off)){
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
}
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, receive_data, sizeof(receive_data));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);
}
}
//main函数
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, receive_data, sizeof(receive_data));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);

3、测试串口是否连接成功。

image-20241231144542469

4、演示视频

实验5

实验内容

1. 集成和调式SPI驱动程序

使用教材或开发板厂商提供的驱动程序文件,将w25flash.h、w25flash.c集成到工程。通过修改调试,使得驱动程序可以正常运行。

2. 通过SPI读写Flash存储芯片BY25Q128AS

MCU在Flash中写入“Hello, MCU”等内容。MCU重启后,从Flash中读取内同,通过USART发送到上位机串口调试软件显示。

实验思路

1. 集成和调式SPI驱动程序

将驱动文件合并到工程中。

2. 通过SPI读写Flash存储芯片BY25Q128AS

通过驱动文件中的函数对flash进行读写。

实验步骤

1、配置引脚

image-20250101045445534

注意cubemx自动配置的引脚可能是错误的。

image-20250101045508421

2、编写代码

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
55
56
57
58
#include "w25flash.h"
#include "stdio.h"
#include "string.h"

#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}

//定义函数
void Flash_Write(void)
{
uint8_t blobkNo = 0;
uint16_t sectorNo = 0;
uint16_t pageNo = 0;
uint32_t memAddress = 0;

printf("---------------------\r\n");
//写入Page0两个字符串
memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo, pageNo); //Page0的地址
uint8_t bufStr1[] = "Hello MCU";
uint16_t len = 1 + strlen("Hello MCU"); //包括结束符'\0'
Flash_WriteInPage(memAddress, bufStr1, len); //在Page0的起始位置写入数据
printf("Write in Page0:0\r\n%s\r\n", bufStr1);
printf("---------------------\r\n");
}
//读取Page0的内容
void Flash_Read(void)
{
uint8_t blobkNo=0;
uint16_t sectorNo=0;
uint16_t pageNo=0;

printf("---------------------\r\n");
//读取Page0
uint8_t bufStr[50]; //Page0读出的数据
uint32_t memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo,pageNo);
Flash_ReadBytes(memAddress, bufStr, 50); //读取50个字符
printf("Read from Page0:0\r\n%s\r\n",bufStr);
printf("---------------------\r\n"

//main函数中循环外部
uint16_t ID = Flash_ReadID();
printf("W25Q128 ID:0x%x\r\n",ID);
printf("---------------------\r\n");
//清空flash
uint32_t globalAddr=0;
Flash_EraseBlock64K(globalAddr);

Flash_Write();
HAL_Delay(1000);
Flash_Read();

3、视频演示

参考链接

1
https://www.cnblogs.com/lc-guo/p/17965537

实验6

实验内容

1. 移植鸿蒙LiteOS-M到STM32

掌握如何将 LiteOS-M 系统移植到 STM32平台上,理解嵌入式操作系统移植的基本原理和流程,学习如何适配硬件平台。

2. 开发基于鸿蒙LiteOS-M的多任务程序

掌握鸿蒙 LiteOS-M 操作系统的多任务管理功能,学习如何在实时操作系统中创建、调度和管理多个任务。

实验思路

1、对代码进行编译。

2、创建任务,查看任务是否完成。

实验步骤

跟着【OpenHarmony】移植 3.1 版本系统到 STM32_openharmony移植到stm32-CSDN博客一步一步操作。

代码

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include "los_task.h"
#include "los_typedef.h"
#include "los_sys.h"
#include "los_api_task.h"
#include "usart.h" // 假设已实现 USART 的初始化与发送功能

#define TASK1_PRIORITY 5
#define TASK2_PRIORITY 6
#define TASK1_STACK_SIZE 0x1000
#define TASK2_STACK_SIZE 0x1000

// 任务ID
UINT32 g_task1Id;
UINT32 g_task2Id;

// 任务1函数:每隔1秒打印内容
VOID Task1(VOID) {
while (1) {
USART_SendString("Task1: Running...\r\n"); // USART 输出
LOS_TaskDelay(1000); // 延时1秒,单位是毫秒
}
}

// 任务2函数:每隔5秒挂起和恢复任务1
VOID Task2(VOID) {
while (1) {
// 挂起任务1
LOS_TaskSuspend(g_task1Id);
USART_SendString("Task2: Task1 Suspended\r\n");

// 延时5秒
LOS_TaskDelay(5000);

// 恢复任务1
LOS_TaskResume(g_task1Id);
USART_SendString("Task2: Task1 Resumed\r\n");

// 延时5秒
LOS_TaskDelay(5000);
}
}

// 主函数:创建任务
VOID AppMain(VOID) {
// 初始化 UART
USART_Init(); // 假设已实现初始化函数

// 任务属性结构体
TSK_INIT_PARAM_S task1Params;
TSK_INIT_PARAM_S task2Params;

// 初始化任务1参数
task1Params.pfnTaskEntry = (TSK_ENTRY_FUNC)Task1;
task1Params.uwStackSize = TASK1_STACK_SIZE;
task1Params.pcName = "Task1";
task1Params.usTaskPrio = TASK1_PRIORITY;
task1Params.uwResved = LOS_TASK_STATUS_DETACHED;

// 初始化任务2参数
task2Params.pfnTaskEntry = (TSK_ENTRY_FUNC)Task2;
task2Params.uwStackSize = TASK2_STACK_SIZE;
task2Params.pcName = "Task2";
task2Params.usTaskPrio = TASK2_PRIORITY;
task2Params.uwResved = LOS_TASK_STATUS_DETACHED;

// 创建任务1
if (LOS_TaskCreate(&g_task1Id, &task1Params) != LOS_OK) {
USART_SendString("Error: Failed to create Task1\r\n");
return;
}

// 创建任务2
if (LOS_TaskCreate(&g_task2Id, &task2Params) != LOS_OK) {
USART_SendString("Error: Failed to create Task2\r\n");
return;
}
}

视频演示

PWM

使用数字信号模拟模拟信号。

PWM是使用定时器的输出比较模式实现的,该模式存在冻结模式(直接输出),强制有效(一直为有效电平),强制无效(一直为无效电平),匹配时有效,匹配时无效,匹配时翻转等,其中PWM是由PWM模式实现的。

向上计数模式(计数器从0到自动重装载寄存器)
PWM模式1,计数器比比较寄存器小时有效,大于等于时无效。
PWM模式2,计数器比比较寄存器小时无效,大于等于时有效。
向下计数模式(计数器从自动重装载寄存器到0)
PWM模式1,计数器比比较寄存器小于等于时有效,大于时无效。
PWM模式2,计数器比比较寄存器小时等于无效,大于时有效。
向上向下计数模式(计数器从0到自动重装载寄存器到0)
PWM模式1,计数器比比较寄存器小于等于时有效,大于时无效。
PWM模式2,计数器比比较寄存器小时等于无效,大于时有效。

image-20241231222508567

有效电平和无效电平不一定对应高和低电平,可以调节。

1
2
3
4
5
//PWM相关函数
//启动PWM
HAL_TIM_PWM_Start(htim,TIM_CHANNEL);
//设置比较寄存器的值
__HAL_TIM_SET_COMPARE(htim,TIM_CHANNEL,value);

实验7

实验内容

1. 集成和调试TFT LCD显示驱动程序(FSMC模块和LCD模块)

使用教材或开发板厂商提供的驱动程序文件,将font.h、tftlcd.h和tftlcd.c集成到工程。通过修改调试,使得驱动程序可以正常运行,并在开发板的TTF LCD上显示“Hello, MCU”。

2. 启用和配置实时时钟(RTC模块)

在LCD上显示日期和时间,秒数每秒变化一次。

3. 启用配置闹钟(GPIO模块)

设置一组或多组闹钟,闹钟触发时控制开发板上蜂鸣器响5秒、LED1每0.5秒翻转一次。

4. 上位机设置时钟(USART模块)

将时间转换为字符串之后通过串口发送给上位机串口调试软件显示,每秒刷新一次。上位机向MCU发送时间修改指令,MCU能够解析和执行指令,并按照指令修改时间。

实验思路

对功能进行逐个实现。

先完成RTC模块,闹钟模块,再完成LCD模块和USART模块。

实验步骤

1、启动TFTLCD,首先要对FSMC进行初始化。

image-20250101172210369

2、根据驱动,对TFTLCD进行初始化。测试TFTLCD的效果。

1
2
3
4
5
6
7
8
9
10
11
12
TFTLCD_Init();
FRONT_COLOR=BLACK;
LCD_ShowString(10,10,tftlcd_data.width,tftlcd_data.height,12,"Hello World!");
LCD_ShowString(10,30,tftlcd_data.width,tftlcd_data.height,16,"Hello World!");
LCD_ShowString(10,50,tftlcd_data.width,tftlcd_data.height,24,"Hello World!");
LCD_ShowFontHZ(10, 80,"普中科技");
LCD_ShowString(10,120,tftlcd_data.width,tftlcd_data.height,24,"www.prechin.cn");
LCD_Fill(10,150,60,180,GRAY);
uint16_t color=0;
color=LCD_ReadPoint(20,160);
LCD_Fill(100,150,150,180,color);
printf("color=%x\r\n",color);

3、启动RTC,RTC需要使用内部时钟,需要进行配置。

image-20250101172807179

image-20250101172756141

4、对uart进行配置

image-20250101203449436

5、完成实验要求

1
2
//在LCD上输出Hello, MCU
LCD_ShowString(10,130,tftlcd_data.width,tftlcd_data.height,24,"Hello,MCU");
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
//启用闹钟,配置RTC的回调函数
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc){
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
if(HAL_RTC_GetTime(hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
{
HAL_RTC_GetDate(hrtc, &sDate, RTC_FORMAT_BIN);
year = sDate.Year;
month = sDate.Month;
weekday = sDate.WeekDay;
date = sDate.Date;
hour = sTime.Hours;
minute = sTime.Minutes;
second = sTime.Seconds;
char str[30];
sprintf(str,"%02d:%02d:%02d",hour,minute,second);
LCD_ShowString(10,130,tftlcd_data.width,tftlcd_data.height,24,str);
}
}
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc){
char infoA[]="Alarm A(xx:xx:30) trigger: \r\n";
printf("%s", infoA);
HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
HAL_GPIO_TogglePin(BEEP_GPIO_Port, BEEP_Pin);
HAL_Delay(500);
HAL_GPIO_TogglePin(BEEP_GPIO_Port, BEEP_Pin);
}
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
//使用串口控制时间
int ParseAndSetRTC(const char *input_string, RTC_HandleTypeDef *hrtc) {
// 检查输入字符串是否为 NULL
if (input_string == NULL || strlen(input_string) != 12 || strncmp(input_string, "change", 6) != 0) {
return -1; // 输入字符串格式不正确
}

// 提取小时、分钟和秒
char hour_str[3] = {0};
char minute_str[3] = {0};
char second_str[3] = {0};

strncpy(hour_str, input_string + 6, 2);
strncpy(minute_str, input_string + 8, 2);
strncpy(second_str, input_string + 10, 2);

int hour = atoi(hour_str);
int minute = atoi(minute_str);
int second = atoi(second_str);

// 验证时间是否合法
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
return -1; // 时间无效
}

// 设置 RTC 时间
RTC_TimeTypeDef sTime = {0};
sTime.Hours = hour;
sTime.Minutes = minute;
sTime.Seconds = second;
sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sTime.StoreOperation = RTC_STOREOPERATION_RESET;

HAL_RTC_SetTime(hrtc, &sTime, RTC_FORMAT_BIN);
}

//DMA的传输完成中断
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
//由于是中断,所以不需要timeout
if(huart == &huart1){
HAL_UART_Transmit_DMA(&huart1,receive_data,Size);
ParseAndSetRTC(receive_data, &hrtc);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, receive_data, sizeof(receive_data));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);
}
}

4、视频演示

课程设计

万年历

b课设写得心力交瘁,写不下去blog了,感觉这辈子都不会再玩嵌入式了,太难受了,错都不知道错哪。

课设内容

支持LCD屏幕上显示日期、时间、星期等信息(FSMC、LCD、RTC);
支持开关按键/触摸按键等方式设置日期、时间(GPIO);
支持触摸屏设置设置日期、时间(IIC、触控屏);
支持向上位机实时报送当前日期、时间,支持上位机修改日期时间(USART);
支持开机欢迎语(SPI/IIC、FLASH/EEPROM);
支持设定闹钟,闹钟提醒为蜂鸣器(RTC、GPIO)。

实现思路

实现LCD屏幕显示时间功能,需要配置FSMC、RTC和LCD驱动

image-20250103133522235

实现开关按键/触摸按键等方式设置日期、时间,只需要设置开关,然后在LCD屏幕上显示进度。

支持触摸屏设置设置日期、时间,需要配置IIC和触摸屏驱动,如果需要存储校准数据,还需要通过SPI连接EEPROM

支持向上位机实时报送当前日期、时间,支持上位机修改日期时间,需要配置uart

支持开机欢迎语(SPI/IIC、FLASH/EEPROM),需要配置SPI和FLASH驱动

支持设定闹钟,闹钟提醒为蜂鸣器,需要配置RTC的闹钟功能,以及BEEP。

IIC部分电路

image-20250102214135834

触摸屏的触摸芯片是cst716

实现步骤

为了防止有些引脚冲突,先设置重要部位的引脚

1、实现LCD屏幕显示时间功能

配置FSMC引脚,还有LCD_BL。占用的引脚为PD0、PD1、PD4、PD5、PD8、PD9、PD10、PD14、PD15、PE7、PE8、PE9、PE10、PE11、PE12、PE13、PE14、PE15、PF12、PG12、PB15,跟电路图中的引脚匹配。

image-20250103140314788

为RTC配置时钟树,启用外部时钟。占用引脚PC14、PC15、PH0、PH1

image-20250103141336316

配置RTC。占用引脚为PC13

image-20250103141421595

2、实现开关按键/触摸按键等方式设置日期、时间。占用引脚PA0、PE2、PE3、PE4

image-20250103151544108

3、实现触摸屏设置设置日期、时间,需要配置IIC和触摸屏驱动

直接使用现成的驱动,不要在cubemx中配置

4、支持开机欢迎语(SPI/IIC、FLASH/EEPROM),需要配置SPI和FLASH驱动

flash使用现成驱动,spi在cubemx中配置

5、支持设定闹钟,闹钟提醒为蜂鸣器,需要配置RTC的闹钟功能,以及BEEP。

设置rtc和beep的引脚。

6、实现功能,主要是实现触摸控制和按键控制。

首先将显示时间页面设置为状态1,那么修改时间页面则设置为状态2。

在状态2时可以通过触摸屏或者开关按键修改时间。

如果以触摸屏方式修改时间,需要先为触摸屏配置,触摸屏的触摸芯片为CST716,通过IIC进行通信,所以需要配置IIC,在驱动中有给出IIC的配置方法。配置好触摸屏之后,将屏幕划分为不同的部分,其中一部分对应要修改的时间,分别为年月日时分秒,另外的分别为0123456789和confirm,通过这两部分配置触摸时得到的触摸坐标,就能实现触摸按钮的功能。通过confirm可以退出状态2,回到状态1,并且将状态2中设置的时间赋值为LCD屏上显示的时间。

如果采用开关按键修改时间,那么需要先开启开关按键的中断,由于开关一共有四个,分为上下左右,上下可以调节时间的值,左右则可以在年月日时分秒中切换,达到修改时间的效果。