第五天:使用嵌入式操作系统RTOS控制单片机

目录

一:RTOS简介

二:Cube配置FreeRTOS 

三:Cube中创建多任务,实现多任务处理

四:Keil中进行任务函数的编写                    

五:直接在Keil中创建新的进程

六:使用摇杆控制多电机 

1.使用两个摇杆控制两个电机(详见第三天的笔记)

2.使用一个摇杆控制两个电机(同转速同方向)

3.使用一个摇杆控制两个电机(同转速不同方向)

4.使用多线程控制2个电机和LED霓虹

补充:                                             C语言extern的使用 

七:学用Monitor视觉观测PID


一:RTOS简介

操作系统 (Operating System) 的本质是一个帮助用户进行功能管理的软件。操作系统运行 在硬件之上,为其他工作的软件执行资源分配等管理工作。

一般称呼不使用操作系统的单片机开发方式为“裸机开发”,当进行裸机开发时,需要自己 设计循环,中断,定时等功能来控制各个任务的执行顺序。 而使用操作系统进行开发时,只需要创建任务,操作系统会自动按照一些特定的机制自动进 行任务的运行和切换。 除了任务管理之外,操作系统还可以提供许多功能,比如各个任务之间的通信,同步,任务 的堆栈管理,控制任务对重要资源的互斥访问等。 由于单片机的资源比较少,显然无法运行如 Windows,MacOS 等计算机操作系统,一般在 单片机上运行的是经过专门设计的嵌入式实时操作系统 (RTOS)

二:Cube配置FreeRTOS 

 本课程使用的RTOS是freeRTOS——

(1)首先打开 Middleware 标签,选中其中的 FREERTOS 选项,进入 FREERTOS 的配置页面。

(2)在 Mode 页面下,选择 Interface 的版本,这里选择 CMSIS_V1。CMSIS 是由 Keil 提供的 一套特殊的函数接口,他对 freeRTOS 的功能函数进行了封装,使其变得更加易用,在使用 freeRTOS 时,不需要再去直接调用 freeRTOS 的函数,只需要调用 CMSIS 为我们提供的 函数即可,目前 CMSIS 有 V1 和 V2 两个版本。

(3)通过以上方式就可以完成 freeRTOS 的开启,在生成代码之后,freeRTOS 会自动被移植到 工程中。接着在 Configuration 页面下进行 freeRTOS 的配置(可以按默认的来),其中包括:

        名称                                                                            功能

USE_PREEMPTION                                      是否支持抢占机制,支持则设为 Enabled

TICK_RATE_HZ                                             系统时钟速率,时钟按照该速率为诶 freeRTOS 中各                                                                         个任务 执行计时,设置为 1000Hz,则每个任务的最                                                                         小调度时间为 1ms

MAX_PRIORITIES                                         最大优先级数量,默认为 7

MINIMAL_STACK_SIZE                               最小任务栈大小,每当创建一个任务时,都需要为该                                                                        任务分 配一定大小的栈空间,任务需要使用的变量等                                                                        都存储在该栈 空间中。默认的最小值为 128 个字。 

MAX_TASK_NAME_LEN                             最大任务名称长度,上限为16

(4)同时还要去Systerm Core的SYS中将Timebase Source进行更改,这里改成TIM13.

三:Cube中创建多任务,实现多任务处理

在 freeRTOS 的配置页面中的 Configuration 下,选中 Tasks and Queues 标签页(要把标签页往上拉放大才能看到默认任务),存在一个已经创建的默认任务为 “defaultTask”,我们也可以Add一下 新建任务——       点击进入配置选项修改 :   

Task Name                        任务名称    

Priority                              任务创建时的优先级

Stack Size(Words)       任务栈的大小,默认单位为字

Entry Function                   任务函数的入口

Code Generation Option 任务函数代码生成方式,设置为 Default 则会产生一个普通的任务函数:                                           As weak:产生一个用__weak 修饰符修饰的任务函数;

                                         As external:产生一个外部引用的任务函数用户需要自 己实现该函数;                                           Default:产生一个默认格式的任务函数,用户需要在该函 数实现功能 

关于As weak——

由于选择了通过__weak 修饰符创建一个弱函数,可以再在别处实现该任务函数。程序执行 时会自动寻找到这个另外实现的任务函数。

关于 As external——

由于新建了任务“LED_GREEN”,并且设置为 As external,故而需要再在别处实现任务函数。 程序执行时会自动寻找到实现的任务函数。


至于任务函数,可以写在main的/* USER CODE BEGIN 4 */和/* USER CODE END 4 */注释之间。

四:Keil中进行任务函数的编写                    

打开 freertos.c 文件,在 MX_FREERTOS_Init 中,可以找到我们在Cube创建的进程的代码。

找到对应的函数,

比如如下在Cube中创建的点灯任务:(注意把Cube中的对应引脚打开!!!

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */

/* USER CODE END FunctionPrototypes */

void red_led_task(void const * argument);
extern void green_led_task(void const * argument);

并且可以在此文件底部找到:

__weak void red_led_task(void const * argument)
{
  /* USER CODE BEGIN red_led_task */
  /* Infinite loop */
  for(;;)
  {
    osDelay(1);
  }
  /* USER CODE END red_led_task */
}

因为red_led_task设置的是As weak选项,所以我们可以在其他地方重新编写这个函数而无需在此处编写,那么程序会优先并只执行用户重新编写的此函数,用的时候不用加__weak前缀。

而extern void green_led_task也需要编写。

所以只要在其他地方重新编写这两个函数代码就可以实现这两个任务了。

五:直接在Keil中创建新的进程

Cube中只需要开启FreeRTOS的CMSIS_V1,无需新增进程就可以在keil中创建新的进程了。

只需要在keil中的freertos.c中找到Cube默认生成的一个任务:

StartDefaultTask——即可,

我们只需要找到所有出现了defaultTask的地方并照葫芦画瓢即可无需在CubeAdd任务也能实现创建新的进程了。

示例:

/* USER CODE BEGIN Variables */

/* USER CODE END Variables */
//在keil中新建进程
//ֻ只要在RTOS.c文件中出现了默认(default)进程的地方就需要补充上自己的进程,就可以编写对应的进程函数了
osThreadId defaultTaskHandle;
//格式: osThreadId  任务函数名+Handle
osThreadId LED_red_TaskHandle;
osThreadId LED_blue_TaskHandle;

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */

/* USER CODE END FunctionPrototypes */

//然后这里也要加上对应函数名
void StartDefaultTask(void const * argument);
void LED_red(void const * argument);
void LED_blue(void const * argument);

void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */

然后在void MX_FREERTOS_Init初始化里面:

  /* Create the thread(s) */
  /* definition and creation of defaultTask */
  osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
  defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
  //然后这里也是一样参照默认进程
  //其中第一个参数为自定义的任务名称(task name)
  osThreadDef(LED_red, LED_red, osPriorityNormal, 0, 128);
  LED_red_TaskHandle = osThreadCreate(osThread(LED_red), NULL);//这里也要更改任务名
  osThreadDef(LED_blue, LED_blue, osPriorityNormal, 0, 128);
  LED_blue_TaskHandle = osThreadCreate(osThread(LED_blue), NULL);  

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
  /* USER CODE END RTOS_THREADS */

之后就可以在其他地方编写对应的任务函数了。 

六:使用摇杆控制多电机 

1.使用两个摇杆控制两个电机(详见第三天的笔记)

2.使用一个摇杆控制两个电机(同转速同方向)

如果只是在前一个的基础上对“将电流数据发给电机的函数进行改动”,比如:

 set_rpm[0]=rc_ctrl.rc.ch[1]*15;    
 give_current[0]=PID_calc(&Pid[0],motor_data[0].speed_rpm,set_rpm[0]);  

can_cmd_motor(&hcan1,0x200,give_current[0],give_current[1],give_current[0],give_current[3]);    

其中第一句代码用ch[1]也就是通道1(对应右摇杆y方向)来控制转速 ,赋值给set_rpm[0],对应期望值数组的第一个元素;       

然后将计算好的电流1(对应数组小标0)不仅发给ID为1的电调,也发给了ID为3的电调,那么就理应将计算到同一控制通道(右摇杆y方向)的同一电流值传给ID为1和3的电机,从而实现一个摇杆控制两个电机——只是实时转速和方向一致。 


但是这样遇到一个问题:两个电机实际会不以一个转速转动且转速比单独控制的要小,就好像两个电机将这一个电流进行了分流似的。

解决方法是:不要将两个电机的电流值放在数组的同一元素里,而是像之前两个摇杆驱动两个电机那样分开存储对应的电流值,只是目标期望所对应的摇杆通道一致,改动结果如下:

set_rpm[0]=rc_ctrl.rc.ch[1]*15; 

give_current[0]=PID_calc(&Pid[0],motor_data[0].speed_rpm,set_rpm[0]); 

give_current[2]=PID_calc(&Pid[2],motor_data[2].speed_rpm,set_rpm[0]); 

can_cmd_motor(&hcan1,0x200,give_current[0],give_current[1],give_current[2],give_current[3]);    HAL_Delay(5);

这样一个摇杆控制两个电机的转速将一致。 

3.使用一个摇杆控制两个电机(同转速不同方向)

将两个目标期望分别给正负即可:

give_current[0]=PID_calc(&Pid[0],motor_data[0].speed_rpm,    -set_rpm[0]);  

give_current[2]=PID_calc(&Pid[2],motor_data[2].speed_rpm,    set_rpm[0]); 

4.使用多线程控制2个电机和LED霓虹

1.目标:创建两个线程,一个线程用来控制电机,一个线程用来点灯

2.注意事项:

(1)记得Cube每重新生成一次代码,就要及时在remote_control.c中将新生成的重定义!

(2)开了多线程之后程序不执行main的主循环!!!所以要给控制电机重新写一个函数!!!

(3)如果移植或复制工程后程序不能烧录并显示CoreM4的字样,那么去魔术棒的debug的Setting的Flash处将Erase(擦除) Sectors改成Erase Full chip,即将部分擦除改成全擦除,等到可以烧录后可以把此选项改回来(因为每次烧录都要全部擦除很费时间!)。

(4)多线程中的延时函数osDelay和HAL_Delay是不一样的!!!所以在把原来mian中主循环的控制电机PID的代码Copy进对应的任务进程的函数里面后要把延时函数改了!不然不仅这个线程的任务完成不了,还会影响到其他线程,即另一个点灯的线程的灯也点不了!!!

因为不同的Delay函数内部是不一样的!内容不同会导致进行多线程的时候时序混乱而影响进程。

3.步骤略

4.拓展:摇杆控制LED亮度

使用多线程可以实现灯的闪烁和摇杆控制电机同时进行,那么如果想要实现摇杆控制LED亮灭,需要将PID_task的优先级调的比LED_task的高。然后就可以在PID_task的函数里面编写摇杆控制LED亮度的代码,并且能够实现不动摇杆LED闪烁,动摇杆控制LED亮灭(摇杆控制LED的优先级更高)。

下面是摇杆控制电机和LED亮度


补充:                                             C语言extern的使用 

 extern 不是在定义的时候写,而是在其他文件调用另外文件的某个变量才要把这个变量extern一下,程序在执行那个文件的时候会根据extern这个关键字去其他文件里面找到那个变量的定义。从而实现在其他文件里面调用另外文件的变量。


七:学用Monitor视觉观测PID

1.观测前的准备工作——

My Variables蓝色框框->  Executable选择路径

-> Folder处复制工程文件夹中的MDK中的和keil名同名的那个文件夹,打开,点击上方那个条复制该文件的文件路径,复制到Folder处

->点击Flie->更新列表,在列表中找到想要观察的参数,更新,完成

->    然后在myProte_Out和In两个框框处选择Tlink即可-

>点击右上角Ready,完成部署

->再点击旁边的DashBoard即可实时观察数据   

2.Monitor可以检测keil中的任意参数,只要将对应的keil烧录好即可控制摇杆观察各PID参数了。

pid_init(&Pid[i],2,0,0,1200,200);        //PID初始化函数
 

跳转到定义:

void pid_init(pid_t*pid,float kp,float ki,float kd,float max_out,float max_iout)
{
    pid->kp=kp;
    pid->ki=ki;
    pid->kd=kd;
    pid->max_out=max_out;
    pid->max_iout=max_iout;
    pid->error_before=0;
    pid->iout=0;

其中:

(1)kp是比例系数,对应快速接近期望的能力,所以kp越大接近期望的用时越短;

(2)max_iout是积分限幅,max_out是输出限幅;

(3)iout是积分输出,其是   误差error的积分,也就是每时刻的(期望-当前)的积分,其越大,超过max_iout(积分限幅)的幅值容易越大。在PID函数里面进行积分输出超过上限的判断,超过上限后限制在积分限幅(out也是同理)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值