【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显示

附录

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/926382.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

复杂网络(四)

一、规则网络 孤立节点网络全局耦合网络&#xff08;又称完全网络&#xff09;星型网络一维环二维晶格 编程实践&#xff1a; import networkx as nx import matplotlib.pyplot as pltn 10 #创建孤立节点图 G1 nx.Graph() G1.add_nodes_from(list(range(n))) plt.figure(f…

安规题库;安全测试题;安规刷题;大风车excel

一、电网安规题库、试题 安规真的是年年考&#xff0c;天天背&#xff0c;时不时抽考 往往需要利用上下班的零碎时间来练习和记忆 分享一套电网真题、原题&#xff08;150道选择题&#xff09; 上面的试题&#xff0c;我已经导入到刷题工具&#xff08;大风车excel&#xff…

ElasticSearch学习篇19_《检索技术核心20讲》搜推广系统设计思想

目录 主要是包含搜推广系统的基本模块简单介绍&#xff0c;另有一些流程、设计思想的分析。 搜索引擎 基本模块检索流程 查询分析查询纠错 广告引擎 基于标签倒排索引召回基于向量ANN检索召回打分机制&#xff1a;非精确打分精准深度学习模型打分索引精简&#xff1a;必要的…

BWO-CNN-BiGRU-Attention白鲸优化算法优化卷积神经网络结合双向门控循环单元时间序列预测,含优化前后对比

BWO-CNN-BiGRU-Attention白鲸优化算法优化卷积神经网络结合双向门控循环单元时间序列预测&#xff0c;含优化前后对比 目录 BWO-CNN-BiGRU-Attention白鲸优化算法优化卷积神经网络结合双向门控循环单元时间序列预测&#xff0c;含优化前后对比预测效果基本介绍模型描述程序设计…

Matlab Simulink HDL Coder FPGA开发初体验—计数器

目录 一、Simulink设计及仿真二、Verilog HDL代码转换1、参数配置2、HDL代码生成 三、ModelSim仿真分析1、使用自己编写的Testbench文件进行仿真2、使用HDL Coder生成的Testbench文件进行仿真 前言 Simulink HDL Coder‌是一款将Simulink和Stateflow模型转化为可综合的Verilog和…

RAG数据拆分之PDF

引言RAG数据简介PDF解析方法及工具代码实现总结 二、正文内容 引言 本文将介绍如何将RAG数据拆分至PDF格式&#xff0c;并探讨PDF解析的方法和工具&#xff0c;最后提供代码示例。 RAG数据简介 RAG&#xff08;关系型属性图&#xff09;是一种用于表示实体及其关系的图数据…

Postman设置接口关联,实现参数化

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 postman设置接口关联 在实际的接口测试中&#xff0c;后一个接口经常需要用到前一个接口返回的结果&#xff0c; 从而让后一个接口能正常执行&#xff0c;这…

不玩PS抠图了,改玩Python抠图

网上找了两个苏轼的印章图片&#xff1a; 把这两个印章抠出来的话&#xff0c;对于不少PS高手来说是相当容易&#xff0c;但是要去掉其中的水印&#xff0c;可能要用仿制图章慢慢描绘&#xff0c;图章的边缘也要慢慢勾画或者用通道抠图之类来处理&#xff0c;而且印章的红色也不…

构造函数与析构函数错题汇总

构造函数不能定义返回类型&#xff0c;也没有返回类型。 堆、栈、静态存储区。栈上的对象main函数结束就释放&#xff0c;堆上的需要手动释放&#xff0c;静态存储区的在所在作用域的程序结束时释放。这里static在main函数内&#xff0c;是局部变量&#xff0c;所以作用域为…

模拟器快速上手,助力HarmonyOS应用/服务高效开发

文章目录 1 创建模拟器1&#xff09;打开设备管理界面2&#xff09;设置本地模拟器实例存储路径3&#xff09;创建一个模拟器&#xff08;1&#xff09;选择模拟器设备&#xff08;2&#xff09;创建模拟器&#xff08;3&#xff09;启动模拟器&#xff08;4&#xff09;关闭模…

如何估算自然对流传热系数

介绍 一般来说&#xff0c;对流可以定义为通过加热流体&#xff08;例如空气或水&#xff09;的运动来传递热量的过程。 自然对流&#xff08;对流的一种特定类型&#xff09;可以定义为流体在重力作用下由于较热因此密度较小的物质上升&#xff0c;而较冷且密度较大的物质下…

【Git 工具】用 IntelliJ IDEA 玩转 Git 分支与版本管理

文章目录 一、使用 IDEA 配置和操作 Git1.1 查看 Idea 中的 Git 配置1.2 克隆 Github 项目到本地 二、版本管理2.1 提交并推送修改2.2 拉取远程仓库2.3 查看历史2.4 版本回退 三、分支管理3.1 新建分支3.2 切换分支3.2 合并分支3.4 Cherry-Pick 参考资料 一、使用 IDEA 配置和操…

本地学习axios源码-如何在本地打印axios里面的信息

1. 下载axios到本地 git clone https://github.com/axios/axios.git 2. 下载react项目, 用vite按照提示命令配置一下vite react ts项目 npm create vite my-vue-app --template react 3. 下载koa, 搭建一个axios请求地址的服务端 a.初始化package.json mkdir koa-server…

【深度学习基础】预备知识 | 微积分

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈PyTorch深度学习 ⌋ ⌋ ⌋ 深度学习 (DL, Deep Learning) 特指基于深层神经网络模型和方法的机器学习。它是在统计机器学习、人工神经网络等算法模型基础上&#xff0c;结合当代大数据和大算力的发展而发展出来的。深度学习最重…

微信小程序构建npm失败,没有找到可以构建的npm包

方法&#xff1a;打开终端输入 npm init -y npm install 或 yarn install我用 npm install 下载后并没有出现node_modules, 又用 yarn install 下载&#xff0c;成功了 下载好后&#xff0c;在project.config.json文件添加 "showShadowRootInWxmlPanel": true, …

Ubuntu 24.04使用docker安装Node-Red

Node-Red是开源是可视化的流计算软件&#xff0c;在Ubuntu 24.04版本下&#xff0c;很容易通过docker进行安装。 本人环境信息如下&#xff1a; Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-45-generic x86_64)ubuntupascalming-ubuntu:~$ docker -v Docker version 24.0…

11.26深度学习_神经网络-数据处理

一、深度学习概述 1. 什么是深度学习 ​ 人工智能、机器学习和深度学习之间的关系&#xff1a; ​ 机器学习是实现人工智能的一种途径&#xff0c;深度学习是机器学习的子集&#xff0c;区别如下&#xff1a; ​ 传统机器学习算法依赖人工设计特征、提取特征&#xff0c;而深…

Kotlin的object修饰符定义类似Java的静态类/静态方法

Kotlin的object修饰符定义类似Java的静态类/静态方法 //类似Java的static类 object StaticCls {//类似Java静态变量private var num 0//类似Java的静态方法fun updateVal(n: Int) {num n}fun getVal(): Int {return num} }class MyTest() {fun setVal() {StaticCls.updateVal…

接口隔离原则理解和实践

在软件开发中&#xff0c;设计原则是指导我们编写高质量代码的重要准则。接口隔离原则&#xff08;Interface Segregation Principle, ISP&#xff09;是面向对象设计原则中的一条重要原则。ISP原则指出&#xff0c;客户端不应该依赖它不需要的接口&#xff0c;类间的依赖关系应…

SQL进阶——C++与SQL进阶实践

在C开发中&#xff0c;SQL数据库的操作是开发者常见的任务之一。虽然前面我们已经介绍了如何在C中通过数据库连接执行基本的SQL查询&#xff0c;但在实际项目中&#xff0c;我们通常需要更加复杂和高效的数据库操作。存储过程与函数的调用、复杂SQL查询的编写、以及动态构造SQL…