【STM32+HAL】FreeRTOS学习小札
一、RTOS程序任务结构
如上图所示,在实时操作系统中,开发人员可以把要实现的功能划分为多个任务,每个任务负责实现其中的一部分,每个任务都是一个很简单的程序,通常是一个死循环。
二、多任务系统基本概念
1、FreeRTOS相关函数
内核控制
o s D e l a y 函 数 是用户常用的毫秒级延时函数,osKernelGetTickCount是RTOS内核计数的获取函数,在系统内核节拍频率为默认的1000Hz时,这个内核计数即为系统启动到当前时刻的累计时间毫秒数(简称系统时间戳)。这两个函数是CMSIS多任务程序设计中常用的系统函数。
任务管理函数
2、任务及任务管理
除了任务创建与终止执行,常见任务管理还包括暂停任务与恢复任务两种操作。 osThreadSuspend可以让任务A挂起,保持在阻塞状态,直到其他任务使用
osThreadResume唤起任务A。如果其他任务的优先级低于任务A,那么唤起任务A的瞬间系统内核就会上下文切换至任务A,即马上运行。程序设计时,如果要暂停某个任务运行而后在某个时刻又恢复该任务的运行,就应该使用osThreadSuspend暂停任务而不是用osThreadTerminate终止任务。因为终止任务后只能用osThreadNew再新建一个任务,然而新建任务的内部变量值并不能恢复到终止任务时的状态。
3、任务优先级
任务优先级是RTOS内核给任务指定的优先等级,它决定了任务获 取CPU资源的优先次序。和STM32的中断优先级不同,在FreeRTOS中的任务优先级数值越小,任务的优先次序越低。FreeRTOS允许创建多个相同优先级的任务,同优先级任务将使用时间片算法进行调度。
IRQ任务:IRQ任务是在中断中进行触发的任务,此类任务可以设置较高优先级,其优先级数值可以设置为osPriorityHigh及其以上的几个优先级。
高优先级后台任务:如按键检测、串口消息处理等,都可以归类为这一类任务,其优先级数值可以设置为osPriorityNormal及其附近的几个优先级。
低优先级的时间片调度任务:如GUI界面显示、LED数码管显示等不需要实时执行的都可以归为这一类任务,这一类任务的优先级可以是osPriorityLow及其上下附近的几个优先级。
特别注意的是,IRQ任务和高优先级任务必须设置为阻塞式(任务函数循环中有调用消息等待或者延时等函数),只有这样,高优先级任务才会释放CPU资源,从而让低优先级任务有机会运行。
4、消息队列
CMSIS常见的任务间通信方式有消息队列(Message Queues)、信号量(Semaphores)互斥锁(Mutexes)和任务通知(Notifications)。这几种通信方式中,除了任务通知,其他几种方式都是基于消息队列来实现的。
消息队列作为任务间通信的一种数据结构,支持在任务与任务间、中断和任务间传递消息内容。通常情况下,消息队列的数据结构实现是一种FIFO(先入先出)缓冲区,即最先写入的消息也是最先被读出的。消息队列可以存储有限个具有确定长度的数据单元,可以保存的最大单元数目被称为队列的“深度” ,在消息队列创建时需要设定其深度和每个单元的大小。
当某个任务试图从一个消息队列读取数据时,可以指定一个阻塞超时时间。在这段时间内,如果消息队列为空,该任务将保持阻塞状态以等待新的消息,当其它任务或中断中向其等待的消息队列写入了数据,该任务将自动从阻塞状态转入就绪状态。
5、任务通知
FreeRTOS的每一个任务都有一个32位的通知(标志)值,用户可以用这个通知值保存一个32位整数或指针值。大多数情况下,任务通知可以替代二值信号量、计数信号量、事件标志,也可替代长度为1的消息队列。而且相对之前使用信号量、消息队列和事件标志时必须先创建(定义)对应的数据对象,使用任务通知不需要另外创建通知变量,显然更为灵活。
FreeRTOS的CMSIS v2封装中,任务通知又称为线程标志(ThreadFlag),任务通知的常用函数如下表所示:
6、内存管理
程序运行时的内存管理包括堆和栈两个概念:
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
栈又称堆栈,是用户存放程序临时创建的局部变量,在函数内部定义的变量(不包括static声明的静态变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的任务栈中,调用结束时函数的返回值也会被存放回栈中。由于栈的后进先出(LIFO)特点,所以栈特别方便用来保存/恢复调用现场。
7、程序存储空间
关于程序的存储空间,一个程序本质上都是由bss段、data段、text段三个组成的,在嵌入式系统的设计中了解这三个概念也非常重要,牵涉到嵌入式系统运行时的内存大小分配,存储单元占用空间大小的问题。
bss段通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时bss段部分将会清零。
data段也称为数据段,程序编译完成之后,已初始化的全局变量保存在data段中,未初始化的全局变量保存在bss段中。
text段也称为代码段,通常是指用来存放程序执行代码的一块内存区域。对嵌入式系统程序,text和data段都在可执行文件中(只读存储空间),而bss段不在可执行文件中,由系统运行时初始化。一般而言,MDK工程编译成功后,全局变量和静态变量会存放到bss段和data段空间,如果是用了const修饰的不可变全局变量,会存储到text段空间。
MDK工程编译成功后,输出信息中有一条PromgramSize语句,如“ProgramSize: Code=13866 RO-data=578 RW-data=16 ZI-data=20488”说明了程序存储信息。
Code:即代码域,对应上页中的text段,这些内容被存储到ROM区。
RO-data:即只读数据域,指程序中的只读数据,其也被存储在ROM区。
RW-data:即可读写数据域,它指初始化为“非0值”的可读写数据,程序运行时它们会常驻在RAM区。
ZI-data:即0初始化数据(包括未初始化的全局变量),程序刚运行时这些数据初始值全都为0,它们也常驻在RAM区。ZI-data与RW-data的区别除了初值不同,其对应的存储空间也不同,RW-data和RO-data都是上一页中的data段,需要占用ROM空间,而ZI-data则对应bss段,仅占用RAM空间。
所以,根据上述的Promgram Size信息,可以计算程序大小为:
占用ROM = “Code” + “RO-data” + “RW-data” = 13866 + 578 + 16 = 14460
占用RAM = “RW-data” + “ZI-data” = 16 + 20488 = 20504
三、CUBEMX配置
1、开启FreeRTOS
2、添加任务
修改任务名、优先级…
3、修改SYS模块中的基准时钟源,修改为TIM7
四、FreeRTOS应用编译器V6选择
工程默认的编译器是V5版本编译器,为了提高编译效率,编译器列表中可以选择V6版本编译进行编译,编译速度将大大提高。
为此,使用V6编译器时还需要修改 FreeRTOS源码中的两个文件。
1、打开CubeMX软件安装固件包的目录:
我存放的地址如下:
E:\STM32cubemx\STM32Cube_FW_F4_V1.28.1\Middlewares\Third_Party\FreeRTOS\Source\portable\GCC\ARM_CM4F
2、找到图中的两个GCC版本移植文件,进行复制
3、将上面两个文件粘贴替换工程文件夹中的RVDS版本移植文件
4、复制替换后,在 MDK中设置工程选项,选择V6版本编译器编译即可。
要注意的是,每次用CubeMX导出工程后,这两个文件都会恢复为RVDS版本。更直接的方法是替换固件包中FreeRTOS源码目录下的RVDS版本移植文件。不过这个做法会导致以后所有使用FreeRTOS的工程都只能用V6版本编译器进行编译。
五、示例程序
实验一:流水灯
1、新建按键、数码管任务
2、默认任务StartDefaultTask
根据按键任务的返回值控制流水灯的循环方向
SetLeds()、ScanKey()、DispSeg()等函数详见附录部分
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
uint8_t sta = 0x01;
uint8_t dir = 1;
uint16_t key_dat = 0;
/* Infinite loop */
for(;;)
{
SetLeds(sta); // 调用SetLeds函数设置LED状态
// 任务通知相关操作
int key = osThreadFlagsWait(KEY_MASK, osFlagsWaitAny, 0); // 等待按键任务通知标志,返回值赋给key
// 如果K1按下,流水灯反向
if(key == K1_Pin) dir =!dir;
/*
// 消息队列相关操作
if(osMessageQueueGet(RX1_QueueHandle, &key_dat, 0, 10) == osOK)// 从消息队列获取消息
if(key_dat == K1_Pin) dir =!dir;
*/
// 根据dir的值更新sta的值
if(dir) sta = (sta < 0x80)? (sta << 1) : 0x01;
else sta = (sta > 0x01)? (sta >> 1) : 0x80;
osDelay(100);
}
/* USER CODE END StartDefaultTask */
}
3、按键任务
用任务通知或消息队列的方式传递当前读取的按键值
void StartKeyTask(void *argument)
{
/* USER CODE BEGIN StartKeyTask */
uint8_t brun = 1;
/* Infinite loop */
for(;;)
{
uint16_t key = ScanKey(); // 调用ScanKey函数扫描按键
if (key > 0)
{
// 设置defaultTaskHandle任务的任务通知标志为按键对应的key
osThreadFlagsSet(defaultTaskHandle, key);// 用于通知defaultTaskHandle任务有按键事件发生
// osMessageQueuePut(RX1_QueueHandle,&key,0,0); //任务通知的方式来传递按键信息
// 避免一次按键操作被多次误处理
while (ScanKey() > 0);
if (K3_Pin == key)
{
brun =!brun;
// 恢复defaultTaskHandle任务的执行
if (brun)
osThreadResume(defaultTaskHandle);
// 暂停defaultTaskHandle任务的执行
else
osThreadSuspend(defaultTaskHandle);
}
}
else
{
// 如果没有按键按下,设置defaultTaskHandle任务的任务通知标志为K6_Pin左移1位的值
osThreadFlagsSet(defaultTaskHandle, K6_Pin << 1);
}
osDelay(100);
}
/* USER CODE END StartKeyTask */
}
4、数码管显示任务
通过读取按键任务中获取的按键值进行数码管显示
void SegTask(void *argument)
{
/* USER CODE BEGIN SegTask */
char buf[20] = {"1234"};
/* Infinite loop */
for(;;)
{
int key = osThreadFlagsWait(KEY_MASK,osFlagsWaitAny,0);
uint16_t key_dat = 0;
if(key > 0)
{
if(key & K1_Pin) key_dat = key_dat + 1;
if(key & K2_Pin) key_dat = key_dat * 10 + 2;
if(key & K3_Pin) key_dat = key_dat * 10 + 3;
if(key & K4_Pin) key_dat = key_dat * 10 + 4;
if(key & K5_Pin) key_dat = key_dat * 10 + 5;
if(key & K6_Pin) key_dat = key_dat * 10 + 6;
buf[0] = key_dat/1000;
buf[1] = (key_dat/100)%10;
buf[2] = (key_dat/10)%10;
buf[3] = key_dat%10;
sprintf(buf,"%04d",key_dat);
}
DispSeg(buf);
}
/* USER CODE END SegTask */
}
5、成果展示
按键控制流水灯
实验二:串口通信
1、开启异步通信串口1、2
2、添加串口任务
3、串口任务StartUartTask
通过上位机发送指令,通过串口接收数据后控制流水灯的状态
/* USER CODE END Header_StartUartTask */
void StartUartTask(void *argument)
{
/* USER CODE BEGIN StartUartTask */
/* Infinite loop */
HAL_UARTEx_ReceiveToIdle_IT(&huart1, rx_buf, 127); // 开启空闲中断接收
HAL_UARTEx_ReceiveToIdle_IT(&huart2, rx_buf2, 127); //串口2
for(;;)
{
char dat[128]; // 定义消息队列读取缓冲
if(osMessageQueueGet(RX1_QueueHandle, dat, NULL, 10) == osOK)
{
printf("%s", dat); // 打印获取的消息
// 对应不同的串口命令,向默认任务发送不同的任务通知
if (strstr(dat, "START") == dat)
osThreadFlagsSet(defaultTaskHandle, 0x10);
else if (strstr(dat, "STOP") == dat)
osThreadFlagsSet(defaultTaskHandle, 0x20);
else if (strstr(dat, "SPEED") == dat) {
uint8_t sp = dat[5] - '0'; // 提取速度数值
if (sp > 0 && sp < 10) // 将数值1-9发送通知给默认任务
osThreadFlagsSet(defaultTaskHandle, sp);
}
}
osDelay(1);
}
/* USER CODE END StartUartTask */
}
4、默认任务StartDefaultTask
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
uint8_t sta, dir, brun, speed = 5;
sta = dir = brun = 1; // LED灯初始状态
/* Infinite loop */
for(;;)
{
osDelay((10 - speed) * 100); // 流水灯间隔时间
if (brun) { // 如果流水灯运行
if (sta == 0x01 || sta == 0x80)
dir = !dir;
sta = dir ? (sta >> 1) : (sta << 1);
// 打印流水灯状态
for (int i = 0; i < 8; ++i)
printf("%s", (sta & (0x01 << i)) ? "●" : "○");
printf("\r\n");
}
SetLeds(sta); // 调用亮灯函数,刷新LED灯
// 等待任务通知,并设置10毫秒超时
uint32_t flag =
osThreadFlagsWait(0xFF, osFlagsWaitAny, 10);
switch (flag) {
case 0x10: brun = 1; break; // 启动流水灯
case 0x20: brun = 0; break; // 暂停流水灯
default:
if (flag > 0 && flag <10) // 流水灯速度
speed = flag;
break;
}
}
/* USER CODE END StartDefaultTask */
}
5、串口空闲中断接收数据
/* 串口空闲中断 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart1) {
rx_buf[Size] = '\0'; // 接收数据末尾添加字符串结束符
osMessageQueuePut(RX1_QueueHandle, rx_buf, NULL, 0); // 发送消息
__HAL_UNLOCK(huart); // 解锁串口状态
HAL_UARTEx_ReceiveToIdle_IT(&huart1, rx_buf, 127); // 再次开启接收
}
else if (huart == &huart2) {
rx_buf2[Size] = '\0'; // 接收数据末尾添加字符串结束符
osMessageQueuePut(RX1_QueueHandle, rx_buf2, NULL, 0); // 发送消息
__HAL_UNLOCK(huart); // 解锁串口状态
HAL_UARTEx_ReceiveToIdle_IT(&huart2, rx_buf2, 127); // 再次开启接收
}
}
6、成果展示
串口通信
实验三:OLED显示
1、软件模拟IIC引脚设置,IIC任务开启
2、默认任务StartDefaultTask
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
uint8_t sta = 0x01; // 流水灯初始状态(L1亮)
uint16_t cnt = 150;
/* Infinite loop */
for(;;) {
SetLeds(sta); // 流水灯亮灯
// 等待按键消息
int key = osThreadFlagsWait(KEY_MASK, osFlagsWaitAny, 0);
if(statue == 1) //LED 模式
{
if (key == K1_Pin) // K1按下
dir = !dir;
}
else if(statue == 3) //参数模式
{
if (par_sta == 0) // 方向模式
{
if (key == K3_Pin) dir = 1;
else if (key == K2_Pin) dir = 0;
}
else // 速度模式
{
if(key == K3_Pin) cnt = (cnt > 249) ? 250 : cnt + 50;
else if(key == K2_Pin) cnt = (cnt < 51) ? 50 : cnt - 50;
speed = cnt / 50;
}
}
if (dir) sta = (sta < 0x80) ? (sta << 1) : 0x01;
else sta = (sta > 0x01) ? (sta >> 1) : 0x80;
osDelay(cnt);
}
/* USER CODE END StartDefaultTask */
}
3、GUI切换任务
void StartGUITask(void *argument)
{
/* USER CODE BEGIN StartGUITask */
GUI_Init();
/* Infinite loop */
for(;;) { // 任务循环
switch (g_sta) { // 根据当前显示状态调用对应绘图函数
default: break;
case GUI_LOGO: UILogo(); break;
case GUI_MAIN: UIMain(); break;
case GUI_LED: UILeds(); break;
case GUI_KEY: UIKeys(); break;
case GUI_PARA: UIPara();break;
}
osDelay(10);
}
/* USER CODE END StartGUITask */
}
4、按键任务StartKeyTask
void StartKeyTask(void *argument)
{
/* USER CODE BEGIN StartKeyTask */
/* Infinite loop */
for(;;)
{
uint8_t key = ScanKey(); // 扫描按键键码
if (key > 0) { // 有按键按下
while (ScanKey() > 0); // 等待按键放开,防止按键连按
osThreadFlagsSet(defaultTaskHandle, key); // 向默认任务发送key通知
}
switch (key)
{
case K1_Pin:
if (GUI_MAIN == g_sta) statue = (statue == 1) ? statue : (statue - 1);
Show_sta_select(statue); par_sta = 1;
break;
case K4_Pin: if (GUI_MAIN == g_sta) statue = (statue == 3) ? statue : (statue + 1);
Show_sta_select(statue); par_sta = 0;
break;
case K5_Pin: if (GUI_MAIN == g_sta) g_sta = g_sta_select;
break;
case K6_Pin: if (GUI_MAIN == g_sta) g_sta = GUI_LOGO;
else g_sta = GUI_MAIN;
break;
default: break;
}
osDelay(10);
}
/* USER CODE END StartKeyTask */
}
5、UI界面设计函数
void UILogo(void)
{
static uint32_t tick = 0; // 定义静态变量,存储进入界面时的时间戳
if (0 == tick) tick = osKernelGetTickCount(); // 开始进入界面时,记录时间戳
GUI_Clear(); // 屏幕内容清空
GUI_DrawBitmap(&bmLOGO,
(128 - bmLOGO.XSize) / 2, (64 - bmLOGO.YSize) / 2); // 居中显示校徽图片
GUI_Update(); // 刷新屏幕显示
if (osKernelGetTickCount() >= tick + 2000) { // 如果当前时间已经超过进入时间2秒
g_sta = GUI_MAIN; // 界面状态跳转到主菜单界面
tick = 0; // 时间戳清零,以备再次进入
}
}
void UIMain(void)
{
GUI_Clear(); // 屏幕内容清空
GUI_SetFont(&GUI_FontHZ_SimSun_16); // 设置文本字体为16号宋体
GUI_DispStringHCenterAt("主菜单", 64, 0); // 屏幕正上方居中显示标题
if (GUI_LED == g_sta_select) { // 如果待选界面为LED状态
GUI_DispStringHCenterAt("* LED状态", 64, 16);
GUI_DispStringHCenterAt("按键状态", 64, 32);
GUI_DispStringHCenterAt("参数设置", 64, 48);
}
else if(g_sta_select == GUI_KEY){ // 如果待选界面为按键状态
GUI_DispStringHCenterAt("LED状态", 64, 16);
GUI_DispStringHCenterAt("* 按键状态", 64, 32);
GUI_DispStringHCenterAt("参数设置", 64, 48);
}
else if(g_sta_select == GUI_PARA){
GUI_DispStringHCenterAt("LED状态", 64, 16);
GUI_DispStringHCenterAt("按键状态", 64, 32);
GUI_DispStringHCenterAt("* 参数设置", 64, 48);
}
GUI_Update(); // 刷新屏幕显示
}
void UILeds(void)
{
GPIO_TypeDef* LED_Ports[8] = { // 定义LED灯端口数组,如果8个灯都是一个Port,可以不用
L1_GPIO_Port, L2_GPIO_Port, L3_GPIO_Port, L4_GPIO_Port,
L5_GPIO_Port, L6_GPIO_Port, L7_GPIO_Port, L8_GPIO_Port};
uint16_t LED_Pin[8] = {L1_Pin, L2_Pin, L3_Pin, L4_Pin, L5_Pin, L6_Pin, L7_Pin, L8_Pin}; // 引脚数组
GUI_Clear(); // 屏幕内容清空
GUI_SetFont(&GUI_FontHZ_SimSun_16); // 设置文本字体为16号宋体
GUI_DispStringHCenterAt("LED状态", 64, 0); // 屏幕正上方居中显示标题
int tx, ty; // 定义绘图临时变量
tx = ty = 12;
for (int i = 0; i < 8; ++i) { // 循环遍历8个LED灯
GUI_DrawRect(16 * i + 1, 20, 16 * i + tx, 20 + ty); // 绘制边框
if (HAL_GPIO_ReadPin(LED_Ports[i], LED_Pin[i]) == GPIO_PIN_RESET) // 读取端口电平
GUI_FillRect(16 * i + 1, 20, 16 * i + tx, 20 + ty); // 如果亮灯则填充矩形
}
GUI_Update(); // 刷新屏幕显示
}
void UIKeys(void)
{
GUI_Clear(); // 屏幕内容清空
GUI_SetFont(&GUI_FontHZ_SimSun_16); // 设置文本字体为16号宋体
GUI_DispStringHCenterAt("按键状态", 64, 0);// 屏幕正上方居中显示标题
GPIO_PinState ps = HAL_GPIO_ReadPin(K1_GPIO_Port, K1_Pin);// 定义变量,读取K1按键电平
GUI_DispStringAt(GPIO_PIN_RESET == ps ? "●" : "○", 26, 20); // 绘制不同符号表示按键按压状态
ps = HAL_GPIO_ReadPin(K4_GPIO_Port, K4_Pin); // K4按键
GUI_DispStringAt(GPIO_PIN_RESET == ps ? "●" : "○", 26, 48);
ps = HAL_GPIO_ReadPin(K2_GPIO_Port, K2_Pin); // K2按键
GUI_DispStringAt(GPIO_PIN_RESET == ps ? "●" : "○", 6, 34);
ps = HAL_GPIO_ReadPin(K3_GPIO_Port, K3_Pin); // K3按键
GUI_DispStringAt(GPIO_PIN_RESET == ps ? "●" : "○", 46, 34);
ps = HAL_GPIO_ReadPin(K5_GPIO_Port, K5_Pin); // K5按键
GUI_DispStringAt(GPIO_PIN_SET == ps ? "●" : "○", 86, 34); // 注意电平方式和前4个按键不同
ps = HAL_GPIO_ReadPin(K6_GPIO_Port, K6_Pin); // K6按键
GUI_DispStringAt(GPIO_PIN_SET == ps ? "●" : "○", 106, 34);
GUI_Update(); // 刷新屏幕显示
}
void UIPara(void)
{
GUI_Clear(); // 屏幕内容清空
GUI_SetFont(&GUI_FontHZ_SimSun_16); // 设置文本字体为16号宋体
GUI_DispStringHCenterAt("参数设置", 64, 0); // 屏幕正上方居中显示标题
GUI_DispStringHCenterAt("速度:", 30, 20);
GUI_DispStringHCenterAt("方向:", 30, 40);
sprintf(str_sp, "%d", speed);
GUI_DispStringHCenterAt(str_sp, 64, 20);
if(dir) GUI_DispStringHCenterAt("->", 64, 40);
else GUI_DispStringHCenterAt("<-", 64, 40);
GUI_Update(); // 刷新屏幕显示
}
void Show_sta_select(uint8_t x)
{
switch (x)
{
case 1:g_sta_select = GUI_LED;
break;
case 2:g_sta_select = GUI_KEY;
break;
case 3:g_sta_select = GUI_PARA;
break;
default:
break;
}
}
6、成果展示
OLED显示
实验四:RTC时钟
1、选择时钟源
2、开启RTC,以及开启闹铃和唤醒功能
3、更改rtc.c文件
/* USER CODE BEGIN Header */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "rtc.h"
/* USER CODE BEGIN 0 */
uint16_t RTC_Year = 2024; // 年
uint8_t RTC_Mon = 12; // 月
uint8_t RTC_Dat = 6; // 日
uint8_t RTC_Hour = 20; // 时
uint8_t RTC_Min = 15; // 分
uint8_t RTC_Sec = 0; // 秒
uint8_t RTC_PSec = 0; // 百分秒
/* USER CODE END 0 */
RTC_HandleTypeDef hrtc;
/* RTC init function */
void MX_RTC_Init(void)
{
/* USER CODE BEGIN RTC_Init 0 */
/* USER CODE END RTC_Init 0 */
RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};
RTC_AlarmTypeDef sAlarm = {0};
/* USER CODE BEGIN RTC_Init 1 */
/* USER CODE END RTC_Init 1 */
/** Initialize RTC Only
*/
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 127;
hrtc.Init.SynchPrediv = 255;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
if (HAL_RTC_Init(&hrtc) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN Check_RTC_BKUP */
if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) == 0x5050) // 是否第一次配置
return; // 如果不是第一次配置,直接返回,后续初始化动作不需要执行了
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0x5050); // 第一次配置,写入标记
/* USER CODE END Check_RTC_BKUP */
/** Initialize RTC and set the Time and Date
*/
sTime.Hours = 20;
sTime.Minutes = 15;
sTime.Seconds = 0;
sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sTime.StoreOperation = RTC_STOREOPERATION_RESET;
if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
{
Error_Handler();
}
sDate.WeekDay = RTC_WEEKDAY_FRIDAY;
sDate.Month = RTC_MONTH_DECEMBER;
sDate.Date = 6;
sDate.Year = 24;
if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)
{
Error_Handler();
}
/** Enable the Alarm A
*/
sAlarm.AlarmTime.Hours = 0;
sAlarm.AlarmTime.Minutes = 0;
sAlarm.AlarmTime.Seconds = 0;
sAlarm.AlarmTime.SubSeconds = 0;
sAlarm.AlarmTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sAlarm.AlarmTime.StoreOperation = RTC_STOREOPERATION_RESET;
sAlarm.AlarmMask = RTC_ALARMMASK_NONE;
sAlarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_ALL;
sAlarm.AlarmDateWeekDaySel = RTC_ALARMDATEWEEKDAYSEL_DATE;
sAlarm.AlarmDateWeekDay = 1;
sAlarm.Alarm = RTC_ALARM_A;
if (HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN) != HAL_OK)
{
Error_Handler();
}
/** Enable the WakeUp
*/
if (HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 0, RTC_WAKEUPCLOCK_RTCCLK_DIV16) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN RTC_Init 2 */
SetRTCTime(RTC_Hour, RTC_Min, RTC_Sec); // 设置日历时间为默认初始时间
SetRTCDate(RTC_Year, RTC_Mon, RTC_Dat);
/* USER CODE END RTC_Init 2 */
}
void HAL_RTC_MspInit(RTC_HandleTypeDef* rtcHandle)
{
RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0};
if(rtcHandle->Instance==RTC)
{
/* USER CODE BEGIN RTC_MspInit 0 */
/* USER CODE END RTC_MspInit 0 */
/** Initializes the peripherals clock
*/
PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;
PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK)
{
Error_Handler();
}
/* RTC clock enable */
__HAL_RCC_RTC_ENABLE();
/* RTC interrupt Init */
HAL_NVIC_SetPriority(RTC_WKUP_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(RTC_WKUP_IRQn);
HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
/* USER CODE BEGIN RTC_MspInit 1 */
/* USER CODE END RTC_MspInit 1 */
}
}
void HAL_RTC_MspDeInit(RTC_HandleTypeDef* rtcHandle)
{
if(rtcHandle->Instance==RTC)
{
/* USER CODE BEGIN RTC_MspDeInit 0 */
/* USER CODE END RTC_MspDeInit 0 */
/* Peripheral clock disable */
__HAL_RCC_RTC_DISABLE();
/* RTC interrupt Deinit */
HAL_NVIC_DisableIRQ(RTC_WKUP_IRQn);
HAL_NVIC_DisableIRQ(RTC_Alarm_IRQn);
/* USER CODE BEGIN RTC_MspDeInit 1 */
/* USER CODE END RTC_MspDeInit 1 */
}
}
/* USER CODE BEGIN 1 */
HAL_StatusTypeDef ReadRTCDateTime(void) // 读取RTC日期时间
{
RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};
if (HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN) ==HAL_OK)
{
if (HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN) ==HAL_OK)
{
RTC_Year = 2000 + sDate.Year; RTC_Mon =sDate.Month;
RTC_Dat = sDate.Date; RTC_Hour =sTime.Hours;
RTC_Min = sTime.Minutes; RTC_Sec =sTime.Seconds;// 百分秒计算,0.01秒误差
RTC_PSec = (255 - sTime.SubSeconds) * 99 / 255;
return HAL_OK;
}
}
return HAL_ERROR;
}
HAL_StatusTypeDef SetRTCDate(int year, int mon, int date) // 设置年月日
{
RTC_DateTypeDef sDate = {0};
sDate.Year = year % 2000; sDate.Month = mon; sDate.Date = date;
if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) == HAL_OK)
return HAL_OK;
return HAL_ERROR;
}
HAL_StatusTypeDef SetRTCTime(int hour, int min, int sec) // 设置时分秒
{
RTC_TimeTypeDef sTime = {0};
sTime.Hours = hour; sTime.Minutes = min; sTime.Seconds = sec;
if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
return HAL_OK;
return HAL_ERROR;
}
/* USER CODE END 1 */
4、freertos.c——defaulttask
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
uint8_t bdot = 0; // 秒闪变量
char dat[8] = ""; // 数码管显示字符串
int min = 0;
int sec = 0;
uint8_t flag = 0;
uint32_t tick = osKernelGetTickCount(); // 时间戳变量
/* Infinite loop */
for(;;)
{
if (osKernelGetTickCount() >= tick + 1000) {
tick = osKernelGetTickCount();
if(ReadRTCDateTime() == HAL_OK)
{
seg[0] = RTC_Min / 10;
seg[1] = RTC_Min % 10;
seg[2] = RTC_Sec / 10;
seg[3] = RTC_Sec % 10;
}
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
5、freertos.c——guitask
void StartSegTask(void *argument)
{
/* USER CODE BEGIN StartSegTask */
/* Infinite loop */
for(;;)
{
for (int i = 0; i < 4; ++i)
{
if(bsetting) //设置模式
{
if((osKernelGetTickCount() % 1000) < 300) Write595(i,0xFF,0);
else
{
if(i == 1) Write595(i,seg[i],1);
else Write595(i,seg[i],0);
}
}
else
{
if(i ==1 && (osKernelGetTickCount() % 1000) < 500) Write595(i,seg[i],1);
else Write595(i,seg[i],0);
}
osDelay(5);
}
osDelay(1);
}
/* USER CODE END StartSegTask */
}
实验五:OLED显示按键状态(消息队列)
按键按下显示“1”,不按则显示“0”
void StartKeyTask(void *argument)
{
/* USER CODE BEGIN StartKeyTask */
/* Infinite loop */
for(;;)
{
uint16_t key = ScanKey();
if (key > 0)
osThreadFlagsSet(GUITaskHandle, key);
osDelay(10);
}
/* USER CODE END StartKeyTask */
}
/*
...
*/
void StartGUITask(void *argument)
{
/* USER CODE BEGIN StartGUITask */
GUI_Init();
/* Infinite loop */
for(;;)
{
uint32_t key = osThreadFlagsWait(KEY_MASK, osFlagsWaitAny, 0);
GUI_SetFont(&GUI_FontHZ_SimSun_16);
GUI_Clear();
/* 按键key以二进制的方式传递 */
if(key >= 64 && key != -3) {key = key - 64; GUI_DispStringAt("1",106,34);}
else GUI_DispStringAt("0",106,34);
if(key >= 32 && key != -3) {key = key - 32; GUI_DispStringAt("1",86, 34);}
else GUI_DispStringAt("0",86, 34);
if(key >= 16 && key != -3) {key = key - 16; GUI_DispStringAt("1",26, 48);}
else GUI_DispStringAt("0",26, 48);
if(key >= 8 && key != -3) {key = key - 8; GUI_DispStringAt("1",46, 34);}
else GUI_DispStringAt("0",46, 34);
if(key >= 4 && key != -3) {key = key - 4; GUI_DispStringAt("1",6, 34);}
else GUI_DispStringAt("0",6, 34);
if(key >= 2 && key != -3) {key = key - 2; GUI_DispStringAt("1",26, 20);}
else GUI_DispStringAt("0",26, 20);
GUI_Update();
osDelay(10);
}
/* USER CODE END StartGUITask */
}
实验六:数码管显示数字钟
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
enum { SEC, MIN} mode; // 数字钟修改时间的对象
// 数字钟时钟参数结构体
struct {
uint8_t m;
uint8_t s;
uint8_t en;
uint8_t CurrMode;
} clock = {0, 0, 0, SEC};
uint32_t tick = 0; // 数字钟时钟计数
uint32_t tick_2 = 0; // 数码管闪烁时钟计数
uint8_t SelDigLedOn = 1; // 数码管闪烁标志
char buf[10] = "0000"; // 数字钟显示缓冲区
sprintf(buf, "%02d.%02d", clock.m, clock.s);
uint32_t flag = osThreadFlagsWait(0xFF, osFlagsWaitAny, 10); // 等待任务通知
/* Infinite loop */
for(;;) {
/* 正计时 */
/*
if( clock.en == 1){
if( osKernelGetTickCount() >= tick + 1000 ){
tick = osKernelGetTickCount();
clock.m = ( clock.m + ( ( clock.s+ 1)/ 60) ) % 60 ;
clock.s = ( clock.s + 1) % 60 ;
}
sprintf(buf, "%02d.%02d", clock.m, clock.s);
}
*/
/* 倒计时 */
if( clock.en == 1){
if( osKernelGetTickCount() >= tick + 1000 ){
tick = osKernelGetTickCount();
clock.m = ((clock.m == 0 && clock.s == 0) ? 59 : (clock.m - ((clock.s == 0) ? 1 : 0)));
clock.s = clock.s > 0 ? ( clock.s - 1) : 59 ;
}
sprintf(buf, "%02d.%02d", clock.m, clock.s);
}
else{
tick = osKernelGetTickCount();
switch (flag)
{
case K2_FLAG:
clock.CurrMode = (clock.CurrMode+1)%2 ;
break;
case K1_FLAG:
if( clock.CurrMode == SEC){
clock.s = ( clock.s + 1) % 60 ;
}else if( clock.CurrMode == MIN){
clock.m = ( clock.m + 1) % 60 ;
}
break;
case K4_FLAG:
if( clock.CurrMode == SEC){
clock.s = ( clock.s - 1 + 60) % 60 ;
}else if( clock.CurrMode == MIN){
clock.m = ( clock.m - 1 + 60) % 60 ;
}
break;
default:
break;
}
if( SelDigLedOn)
sprintf(buf, "%02d.%02d", clock.m, clock.s);
else{
if( clock.CurrMode == SEC)
sprintf(buf, "%02d. ", clock.m);
else if( clock.CurrMode == MIN)
sprintf(buf, " .%02d", clock.s);
}
}
LEDSeg_ScanDisp(buf);
if( osKernelGetTickCount() >= tick_2 + 500 ){
tick_2 = osKernelGetTickCount();
SelDigLedOn = !SelDigLedOn;
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
附录
1、SetLeds():LED亮灭控制函数
void SetLeds(uint8_t dat)
{
HAL_GPIO_WritePin(L1_GPIO_Port, L1_Pin,
(dat & 0x01) ? GPIO_PIN_RESET : GPIO_PIN_SET);
HAL_GPIO_WritePin(L2_GPIO_Port, L2_Pin,
(dat & 0x02) ? GPIO_PIN_RESET : GPIO_PIN_SET);
HAL_GPIO_WritePin(L3_GPIO_Port, L3_Pin,
(dat & 0x04) ? GPIO_PIN_RESET : GPIO_PIN_SET);
HAL_GPIO_WritePin(L4_GPIO_Port, L4_Pin,
(dat & 0x08) ? GPIO_PIN_RESET : GPIO_PIN_SET);
HAL_GPIO_WritePin(L5_GPIO_Port, L5_Pin,
(dat & 0x10) ? GPIO_PIN_RESET : GPIO_PIN_SET);
HAL_GPIO_WritePin(L6_GPIO_Port, L6_Pin,
(dat & 0x20) ? GPIO_PIN_RESET : GPIO_PIN_SET);
HAL_GPIO_WritePin(L7_GPIO_Port, L7_Pin,
(dat & 0x40) ? GPIO_PIN_RESET : GPIO_PIN_SET);
HAL_GPIO_WritePin(L8_GPIO_Port, L8_Pin,
(dat & 0x80) ? GPIO_PIN_RESET : GPIO_PIN_SET);
}
2、ScanKey():按键扫描函数
uint8_t ScanKey(void)
{
uint8_t key = 0;
if (HAL_GPIO_ReadPin(K1_GPIO_Port, K1_Pin) == GPIO_PIN_RESET)
key |= K1_Pin;
if (HAL_GPIO_ReadPin(K2_GPIO_Port, K2_Pin) == GPIO_PIN_RESET)
key |= K2_Pin;
if (HAL_GPIO_ReadPin(K3_GPIO_Port, K3_Pin) == GPIO_PIN_RESET)
key |= K3_Pin;
if (HAL_GPIO_ReadPin(K4_GPIO_Port, K4_Pin) == GPIO_PIN_RESET)
key |= K4_Pin;
if (HAL_GPIO_ReadPin(K5_GPIO_Port, K5_Pin) == GPIO_PIN_SET)
key |= K5_Pin;
if (HAL_GPIO_ReadPin(K6_GPIO_Port, K6_Pin) == GPIO_PIN_SET)
key |= K6_Pin;
if (key > 0) {
osDelay(10);
uint8_t key2 = 0;
if (HAL_GPIO_ReadPin(K1_GPIO_Port, K1_Pin) == GPIO_PIN_RESET) key2 |= K1_Pin;
if (HAL_GPIO_ReadPin(K2_GPIO_Port, K2_Pin) == GPIO_PIN_RESET) key2 |= K2_Pin;
if (HAL_GPIO_ReadPin(K3_GPIO_Port, K3_Pin) == GPIO_PIN_RESET) key2 |= K3_Pin;
if (HAL_GPIO_ReadPin(K4_GPIO_Port, K4_Pin) == GPIO_PIN_RESET) key2 |= K4_Pin;
if (HAL_GPIO_ReadPin(K5_GPIO_Port, K5_Pin) == GPIO_PIN_SET) key2 |= K5_Pin;
if (HAL_GPIO_ReadPin(K6_GPIO_Port, K6_Pin) == GPIO_PIN_SET) key2 |= K6_Pin;
if (key == key2) return key;
else return 0;
}
return 0;
}
3、DispSeg():数码管显示函数
void Write595(uint8_t sel, uint8_t num, uint8_t bdot)
{
// 共阴数码管,'0'~'9','A'~'F' 编码
static const uint8_t TAB[16] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07,
0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71};
// 74HC138关数码管显示
HAL_GPIO_WritePin(A3_GPIO_Port, A3_Pin, GPIO_PIN_RESET);
uint8_t dat = TAB[num & 0x0F] | (bdot ? 0x80 : 0x00);
if (' ' == num) dat = 0; // 空格关闭显示
else if ('.' == num) dat = 0x80;// 单独小数点显示
else if ('-' == num) dat = 0x40;// 负号显示
else if (num > 0x0F) dat = num;// 其余数值按实际段码显示
// 595串行移位输入段码
for (uint8_t i = 0; i < 8; ++i) {
HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(SER_GPIO_Port, SER_Pin, (dat & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET);
dat <<= 1;
HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_SET);
}
// DISLK脉冲锁存8位输出
HAL_GPIO_WritePin(DISLK_GPIO_Port, DISLK_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(DISLK_GPIO_Port, DISLK_Pin, GPIO_PIN_SET);
// 4位数码管片选
HAL_GPIO_WritePin(A0_GPIO_Port, A0_Pin, (sel & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(A1_GPIO_Port, A1_Pin, (sel & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(A2_GPIO_Port, A2_Pin, GPIO_PIN_RESET);
// 74HC138开数码管显示
HAL_GPIO_WritePin(A3_GPIO_Port, A3_Pin, GPIO_PIN_SET);
}
// 4位数码管动态扫描显示
void DispSeg(char dat[8])
{
uint8_t sel = 0; // 数码管位选
uint8_t bdot = 0; // 是否有小数点
for(uint8_t i = 0; i < 8; ++i) {
uint8_t num = dat[i];
if (dat[i] != '.') {
if (dat[i + 1] == '.')
bdot = 1; // 下一位小数点合并到当前位显示
}
else { // 小数点处理
if (bdot) {
bdot = 0;
continue; // 跳过已经合并显示的小数点
}
}
// 十六进制字符显示支持
if (num >= '0' && num <= '9') num -= '0';
else if (num >= 'A' && num <= 'F')
num = num - 'A' + 10;
else if (num >= 'a' && num <= 'f')
num = num - 'a' + 10;
// 点亮对应数码管
Write595(sel++, num, bdot);
osDelay(3); // 延时3毫秒
if (sel >= 4) // 只显示4位数码管
break;
}
}
作者:南极熊ii