1.RT-Thread操作系统简介

        RT-Thread操作系统也是一款嵌入式实时操作系统。他是一个嵌入式实时多线程操作系统,基本属性之一是支持多任务,允许多个任务同时运行并不意味着处理器在同一时刻真地执行了多个 任务。事实上,一个处理器核心在某一时刻只能运行一个任务,由于每次对一个任务的执行时间很短、任务与任务之间通过任务调度器进行非常快速地切换。RT-Thread架构如下图:

对于RT-Thread的开发官方推出的 RT-Thread Studio IDE,这是一个集成开发环境,可以极大地简化工程创建、配置、编译和调试的流程。此外,还有 Env 等辅助配置工具,可以方便地进行系统功能裁剪和软件包管理。对有RT-Thread的开发库我们可以使用RT-Thread封装的库也可以直接使用HAL库。

2.RT-Thread操作系统启动原理

        系统首先进入汇编文件startup_stm32f4xx.s开始运行,主要调用SystemInit __main函数; 然后跳转到c代码,进行rt-thread系统功能初始化,主要在于理解$ Sub $$ main函数和 $Super$$函数对main函数执行前的补丁修复过程, 启动main函数。启动主要分为三个阶段:汇编启动,RT-Thread内核启动,应用程序执行。在汇编启动阶段主要是对C语言运行的硬件环境进行配置,初始化栈堆指针,初始化数据段,跳转到系统初始化,进入程序入口。在RT-Thread内核启动阶段主要构建内核运行环境,关闭中断并初始化硬件,初始化系统滴答定时器,初始化控制台,自动执行通过board导出的初始化函数,初始化系统内核对象为多线程做准备,创建主线程,创建系统线程并启用调度器这里调度器正式启动。应用程序执行,在这个阶段后就可以自己创建线程并使用RT-Thread相应的信号量,消息队列等功能了。

3.RT-Thread操作系统驱动外设

(1)GPIO口的输出输入

        我们通过GPIO口的电平输出来点亮LED灯,我们通过CubeMX和RT-Thread Studio联合编程来创建工程,在该工程中CubeMX的生成的文件不会直接参与编译,我们使用其来配置基本的时钟或者初始化等等,或者可以生成一些例程程序供我们使用。CubeMX的基本配置我们就不在讲述就是对一些引脚或时钟进行配置可以参考前面的文章。下面我们来看程序实现:

#define HAL 0
#define LED1_PIN      GET_PIN(A,6)
#define LED2_PIN      GET_PIN(A,4)
#define LEDSYS_PIN    GET_PIN(A,3)//引脚宏定义rtthread中的使用方法

int main(void)
{
    int count = 1;
#if HAL
    MX_GPIO_Init();
#else
    rt_pin_mode(LED1_PIN,PIN_MODE_OUTPUT);
    rt_pin_mode(LED2_PIN,PIN_MODE_OUTPUT);
    rt_pin_mode(LEDSYS_PIN,PIN_MODE_OUTPUT);//感觉又回到了标准库定义引脚输出模式的时候
#endif
    while (count++)
    {
        LOG_D("LED1 LED2 LEDSYS ON");
#if HAL
        HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(LEDSYS_GPIO_Port, LEDSYS_Pin, GPIO_PIN_RESET);
#else
        rt_pin_write(LED1_PIN, PIN_LOW);
        rt_pin_write(LED2_PIN, PIN_LOW);
        rt_pin_write(LEDSYS_PIN, PIN_LOW);
#endif
        rt_thread_mdelay(1000);
        LOG_D("LED1 LED2 LEDSYS OFF");
#if HAL
        HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
        HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET);
        HAL_GPIO_WritePin(LEDSYS_GPIO_Port, LEDSYS_Pin, GPIO_PIN_SET);
#else
        rt_pin_write(LED1_PIN, PIN_HIGH);
        rt_pin_write(LED2_PIN, PIN_HIGH);
        rt_pin_write(LEDSYS_PIN, PIN_HIGH);
#endif
        rt_thread_mdelay(1000);
    }

    return RT_EOK;
}

可见我们不仅可以使用我们前面所讲的HAL直接来对LED进行控制,也可以采用RT-Thread封装的库来对LED进行控制,我们来详细了解一下RT-Thread库来对LED进行控制的程序,首先我们使用使用了GET_PIN来获取对应的GPIO引脚来进行宏定义,然后我们调用RT-Thread库中对io口模式设置的函数来设置对应的io口模式。其中的LOG_D是日志输出信息显示,可以当作printf来进行调试信息输出,在以后的开发过程中可以对日志进行存储然后查看来判断程序执行的状态。然后通过rt库的写引脚函数来更改对应引脚电平的高低来控制LED的亮灭,使用rt的延时函数来进行1s钟的延时。

对于输入我们可以对GPIO口引脚电平的读取来判断按键是否按下,程序如下:

#define KEY_OK        GET_PIN(G,8)
#define KEY_ESC       GET_PIN(A,8)

这里我们对按键进行了宏定义。

int main(void)
{
    int count = 1;
    rt_pin_mode(LED1_PIN,PIN_MODE_OUTPUT);//设置LED1为推挽输出模式
    rt_pin_mode(KEY_OK, PIN_MODE_INPUT_PULLUP);
    rt_pin_mode(KEY_ESC, PIN_MODE_INPUT_PULLUP);//设置按键为上拉输入模式
    while (count++)
    {
        if(rt_pin_read(KEY_OK) == PIN_LOW)
        {
            rt_thread_mdelay(30);
            if(rt_pin_read(KEY_OK) == PIN_LOW)
            {
                rt_pin_write(LED1_PIN, PIN_LOW);
                LOG_D("KEY_OK Pressed");
            }
        }
        if(rt_pin_read(KEY_ESC) == PIN_LOW)
        {
            rt_thread_mdelay(30);
            if(rt_pin_read(KEY_ESC) == PIN_LOW)
            {
                rt_pin_write(LED1_PIN, PIN_HIGH);
                LOG_D("KEY_ESC Pressed");
            }
        }



    }

    return RT_EOK;
}

然后我们通过引脚模式配置函数将两个按键接入的引脚,配置为上拉输入模式。然后通过读取引脚电平状态函数判断按键是否按下,若按下则控制LED灯的亮灭和显示一些信息。

(2)外部中断的实现

        在RT-Threa中显现外部中断的程序及详解如下:

 //注册按键处理函数
    rt_pin_attach_irq(KEY_OK, PIN_IRQ_MODE_FALLING, KEY_OK_IRQ_Handler, RT_NULL);
    //使能按键中断
    rt_pin_irq_enable(KEY_OK, PIN_IRQ_ENABLE);

首先我们调用对应库函数来注册按键处理函数,该函数的参数分别是要产生外部中断的引脚,中断模式我们这里设置为下降沿触发,中断处理函数该参数是一个函数指针填入参数是填入函数名即可,中断处理函数的参数。然后使能按键中断即可开启外部中断啦,可见RT-Thread的库需要的配置会比使用CubeMX进行更多一点的配置。

void KEY_OK_IRQ_Handler(void *args)
{
    LOG_D("KEY_OK Pressed");
}

然后编写一个中断处理函数,当中断发生时该函数就会被调用。

(3)多线程实现

        我们创建多个线程来同时控制多个LED灯,程序及详解如下:

//创建线程
    rt_thread_t thread_led1 = rt_thread_create("LED1",
                                               led1_thread_entry,
                                               RT_NULL,
                                               1024,
                                               THREAD_PRIORITY,
                                               THREAD_TIMESLICE);
    //启动线程
    if(thread_led1 != RT_NULL)
    {
        rt_thread_startup(thread_led1);
    }
    //创建线程
    rt_thread_t thread_led2 = rt_thread_create("LED2",
                                               led2_thread_entry,
                                               RT_NULL,
                                               1024,
                                               THREAD_PRIORITY,
                                               THREAD_TIMESLICE);
    //启动线程
    if(thread_led2 != RT_NULL)
    {
        rt_thread_startup(thread_led2);
    }

我们通过动态创建线程的函数来创建一根led1和led2线程,该函数的参数分别是线程名称,线程入口函数,线程入口函数参数,线程栈大小,线程优先级,线程时间片。后面两个参数我们采用的是宏定义如下:

#define THREAD_PRIORITY 25 //线程优先级
#define THREAD_TIMESLICE 5 //线程时间片

函数的返回值是创建出的线程的一个结构体,判断该结构是否有内容也就是不为空即线程创建是否成功,若成功则启动线程。

void led1_thread_entry(void *parameter)
{
    while(1)
    {
        rt_pin_write(LED1_PIN, PIN_LOW);
        rt_thread_mdelay(100);
        rt_pin_write(LED1_PIN, PIN_HIGH);
        rt_thread_mdelay(100);
    }
}

void led2_thread_entry(void *parameter)
{
    while(1)
    {
        rt_pin_write(LED2_PIN, PIN_LOW);
        rt_thread_mdelay(500);
        rt_pin_write(LED2_PIN, PIN_HIGH);
        rt_thread_mdelay(500);
    }
}

编写两个线程入口函数,当线程启动后系统调度器会自动对各个线程进行调用。然后我们看一下静态创建线程的程序:

int ledsys_thread_init(void)
{
    //设置led为推挽输出模式
    rt_pin_mode(LEDSYS_PIN,PIN_MODE_OUTPUT);
    //静态创建线程
   rt_thread_init(&ledsys,                        //线程句柄
                  "LEDSYS",                       //线程名称
                  ledsys_thread_entry,            //线程入口函数
                  RT_NULL,                        //线程入口函数参数
                  ledsys_stack,                   //线程栈的起始地址
                  sizeof(ledsys_stack),           //线程栈的大小
                  THREAD_PRIORITY-1,              //优先级
                  THREAD_TIMESLICE);              //时间片
   //启动线程
    rt_thread_startup(&ledsys);
    return RT_EOK;
}
INIT_APP_EXPORT(ledsys_thread_init);              //开机自动启动
                                                  //原理:编译器将标记了INIT_APP_EXPORT的函数放到特定的内存段形成一个初始化函数表,
                                                  //在rt_thread内核初始化时会自动执行无需手动调用

调用库中的静态创建线程函数创建一个线程,函数的参数已经注释给出,然后启动线程。然后将该封装的线程初始化函数通过INIT_APP_EXPORT导出为自动启动,这里再内核初始化阶段会自动调用就不需要再调用了。

/静态创建线程相关参数
ALIGN(RT_ALIGN_SIZE)             //内存对齐,确保线程栈内存起始地址按RT_ALIGN_SIZE对齐
static char ledsys_stack[256];   //线程栈内存空间 , 最小稳定的内存 256字节
static struct rt_thread ledsys;  //线程句柄
#define THREAD_PRIORITY 25       //线程优先级
#define THREAD_TIMESLICE 5       //线程时间片

void ledsys_thread_entry(void *parameter)
{
    while(1)
    {
        rt_pin_write(LEDSYS_PIN, !rt_pin_read(LEDSYS_PIN));
        rt_thread_mdelay(1000);
    }
}

然后是静态创建线程的相关参数和线程入口函数,后面我们以此线程作为系统运行状态指示灯。

(4)LCD和触摸屏驱动

        lcd的驱动我们和HAL库的驱动一样,首先我们通过CubeMX配置相关FSMC来驱动LCD屏,CubeMX配置和前面文章的配置一样,配置成功后会生成FSMC初始化程序,但是我们studio不会进行调用,所以我们要将其移植到lcd文件中。我们将我们前面所写的lcd驱动文件移植到该工程中,并且进行一些改进,程序及详解如下:

//FSMC初始化,因为不是自动调用所以要将CubeMX下的该初始化放到给文件下来初始化
extern SRAM_HandleTypeDef hsram4;
static void FSMC_Init(void)
{
    /* USER CODE BEGIN FSMC_Init 0 */
    /* USER CODE END FSMC_Init 0 */
    FSMC_NORSRAM_TimingTypeDef Timing = {0};
    FSMC_NORSRAM_TimingTypeDef ExtTiming = {0};
    /* USER CODE BEGIN FSMC_Init 1 */
    /* USER CODE END FSMC_Init 1 */
    /** Perform the SRAM4 memory initialization sequence
     */
    hsram4.Instance = FSMC_NORSRAM_DEVICE;
    hsram4.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
    /* hsram4.Init */
    hsram4.Init.NSBank = FSMC_NORSRAM_BANK4;
    hsram4.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE;
    hsram4.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM;
    hsram4.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16;
    hsram4.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE;
    hsram4.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW;
    hsram4.Init.WrapMode = FSMC_WRAP_MODE_DISABLE;
    hsram4.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS;
    hsram4.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE;
    hsram4.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE;
    hsram4.Init.ExtendedMode = FSMC_EXTENDED_MODE_ENABLE;
    hsram4.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;
    hsram4.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE;
    hsram4.Init.PageSize = FSMC_PAGE_SIZE_NONE;
    /* Timing */
    Timing.AddressSetupTime = 0;
    Timing.AddressHoldTime = 15;
    Timing.DataSetupTime = 4;
    Timing.BusTurnAroundDuration = 0;
    Timing.CLKDivision = 16;
    Timing.DataLatency = 17;
    Timing.AccessMode = FSMC_ACCESS_MODE_A;
    /* ExtTiming */
    ExtTiming.AddressSetupTime = 0;
    ExtTiming.AddressHoldTime = 15;
    ExtTiming.DataSetupTime = 4;
    ExtTiming.BusTurnAroundDuration = 0;
    ExtTiming.CLKDivision = 16;
    ExtTiming.DataLatency = 17;
    ExtTiming.AccessMode = FSMC_ACCESS_MODE_A;
    if (HAL_SRAM_Init(&hsram4, &Timing, &ExtTiming) != HAL_OK)
    {
        Error_Handler();
    }
    /* USER CODE BEGIN FSMC_Init 2 */
    /* USER CODE END FSMC_Init 2 */
}

将CubeMX生成的FSMC初始化程序,移植并修改一下即可,修改后的程序如上。

void LCD_BLK_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LCD_BLK_GPIO_Port, LCD_BLK_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin : LCD_BLK_Pin */
GPIO_InitStruct.Pin = LCD_BLK_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LCD_BLK_GPIO_Port, &GPIO_InitStruct);
/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}

对于LCD屏显示的驱动要对背光进行初始化配置,我们也可以通过配置CubeMX来生成该初始阿虎程序然后复制过来即可。

//封装LCD驱动初始化,调用初始化函数对LCD驱动所需的配置进行初始化
int LCD_Device_Init(void)
{
    LOG_I("LCD_Device_Init");
    FSMC_Init();\
    LCD_BLK_Init();
    LCD_Init(); // lcd 初始化
    return RT_EOK ;
}
INIT_APP_EXPORT(LCD_Device_Init);//设置该函数自动启动

然后进行封装并将该初始化函数导出,系统就会自动启动初始化无需我们调用了。

MSH_CMD_EXPORT(lcd_test, lcd display test);//将该函数的调用设置成命令时调用

然后将该文件下的测试函数导出为命令,当再终端输入对应命令时就会调用该函数对LCD驱动进行测试了。

接下来我们再次通过CubeMX配置触摸屏,然后对生成的初始化程序进行移植如下:

//xpt2046初始化  		    
void XPT2046_Init(void)
{
    //在这里对触摸屏相关的io口进行初始化配置
    GPIO_InitTypeDef GPIO_InitStruct = {0};

      /* GPIO Ports Clock Enable */
      __HAL_RCC_GPIOF_CLK_ENABLE();
      __HAL_RCC_GPIOA_CLK_ENABLE();
      __HAL_RCC_GPIOB_CLK_ENABLE();


      /*Configure GPIO pin Output Level */
      HAL_GPIO_WritePin(T_CLK_GPIO_Port, T_CLK_Pin, GPIO_PIN_SET);

      /*Configure GPIO pin Output Level */
      HAL_GPIO_WritePin(GPIOB, T_CS_Pin|T_MOSI_Pin, GPIO_PIN_SET);


      /*Configure GPIO pin : T_CLK_Pin */
      GPIO_InitStruct.Pin = T_CLK_Pin;
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
      GPIO_InitStruct.Pull = GPIO_NOPULL;
      GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
      HAL_GPIO_Init(T_CLK_GPIO_Port, &GPIO_InitStruct);

      /*Configure GPIO pins : T_CS_Pin T_MOSI_Pin */
      GPIO_InitStruct.Pin = T_CS_Pin|T_MOSI_Pin;
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
      GPIO_InitStruct.Pull = GPIO_NOPULL;
      GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
      HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

      /*Configure GPIO pin : T_MISO_Pin */
      GPIO_InitStruct.Pin = T_MISO_Pin;
      GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
      GPIO_InitStruct.Pull = GPIO_PULLUP;
      HAL_GPIO_Init(T_MISO_GPIO_Port, &GPIO_InitStruct);

      /*Configure GPIO pin : T_PEN_Pin */
      GPIO_InitStruct.Pin = T_PEN_Pin;
      GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
      GPIO_InitStruct.Pull = GPIO_PULLUP;
      HAL_GPIO_Init(T_PEN_GPIO_Port, &GPIO_InitStruct);
   
		if(lcddev.dir)  //如果竖屏调转 X Y
		{
			CMD_RDX=0X90;
			CMD_RDY=0XD0;	
      xFactor=-0.09195402;	//横屏校准参数 
      yFactor=0.06736275;
      xOffset=348;
      yOffset=-19;			
		}
		else				    
		{
			CMD_RDX=0XD0;
			CMD_RDY=0X90;
      xFactor=0.06671114;	  //竖屏校准参数 
      yFactor=0.09117551;
      xOffset=-11;  
      yOffset=-18;				
		}										 
}

我们通过的是软件模拟SPI来对触摸屏进行驱动具体可以看前面的文章。

//触摸屏初始化
int Touchpad_Device_Init(void)
{
    XPT2046_Init();
    LOG_I("Touchpad_Device_Init");
    LCD_ShowString(10,10,200,24,24,(uint8_t *)"STM32F407ZG!");
    LCD_ShowString(10,40,200,24,24,(uint8_t *)"TOUCH TEST");
    Clear_Screen();
    return RT_EOK ;
}
INIT_APP_EXPORT(Touchpad_Device_Init);//自动调用初始化函数

然后我们将其初始化封装起来,并且导出,系统同样也会自动调用该初始化函数了。

//电阻触摸屏测试函数
void touchpad_test(void)
{ 
	while(1)
	{
		XPT2046_Scan(0); 		 
	
		if(Xdown<lcddev.width && Ydown<lcddev.height)
		{	
			if(Xdown>(lcddev.width-40)&&Ydown>lcddev.height-18) 
					Clear_Screen();  //清空屏幕
			else 
					Draw_Point(Xdown,Ydown,RED);		//画图	  			   
		} 
	}
}
MSH_CMD_EXPORT(touchpad_test, lcd touchpad draw point test);

然后我们同样将触摸屏的测试函数导出成命令,当输入对应命令后就可以执行了。

      

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐