2019-01-25 21:48:29 lyndon_li 阅读数 316

写内核驱动其实和开发单片机没什么两样。这里用高通的一款路由器芯片QCA4531与常见的单片机STM32做对比。前者通常跑嵌入式linux系统,后者通常跑裸机或者简单的实时操作系统。
那么用这两款芯片分别实现控制一个GPIO口,难度差距有多大呢?我感觉差不多。
下面就边实现边分析
首先,拿到任何一款产品,要想很好的使用它,只有一个办法,那就是看产品的说明书。因为产品的说明书是产品的开发者写的,没有人比产品的开发者更了解产品了。对于芯片而言,其说明书就是芯片手册。所以,看懂芯片手册就能用好产品。
比如,我们想去控制这两款芯片的GPIO,那我们就是翻它们的芯片手册。
1、STM32中想要控制一个GPIO,从芯片手册中我们了解到,只需要配置相应的寄存器就可以了,它们包括:端口配置寄存器(用来配置端口的输入输出模式)、数据输出寄存器,当然还有配置相应的时钟线(STM32和51不同,为了降低功耗,可以关闭部分设备的时钟)。
2、QCA4531中想要控制一个GPIO,又需要做什么呢,其实和其它任何芯片一样,也是配置相应的寄存器,包括:输出使能寄存器、引脚输出值寄存器。

STM32代码

#define PERIPH_BASE      		((unsigned int)0x40000000)
#define APB2PERIPH_BASE  		(PERIPH_BASE + 0x00010000)
#define GPIOC_BASE       		(APB2PERIPH_BASE + 0x1000)
#define GPIOC_CRL 	 	 		*(unsigned int*)(GPIOC_BASE+0x00)
#define GPIOC_ODR 	 	 		*(unsigned int*)(GPIOC_BASE+0x0C)
#define AHBPERIPH_BASE   		(PERIPH_BASE + 0x20000)
#define RCC_BASE 		 		(AHBPERIPH_BASE + 0x1000)
#define RCC_APB2ENR 	 		*(unsigned int*)(RCC_BASE+0x18)

void delay(u32 i)
{
	while(i--);
}

int main()
{
	RCC_APB2ENR |= 1<<4;				//时钟使能
	GPIOC_CRL &= ~( 0x0F<< (4*0));		//配置引脚模式
	GPIOC_CRL |= (3<<4*0);
	
	while(1)
	{
		GPIOC_ODR  |= (1 << 0);			//GPIOC0 置高
		delay(0xFFFFF);
		GPIOC_ODR  &= ~(1 << 0);		//GPIOC0 置低
		delay(0xFFFFF);
	}
}

嵌入式linux代码

#include <sys/types.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <memory.h>

#define GPIO_BASE 0x18040000

void * map_base;
FILE *f;
int n, fd;
volatile unsigned int * GPIO_OUT;

int main(int argc,char *argv[])
{
    if((fd=open("/dev/mem",O_RDWR|O_SYNC))==-1){
    	return(-1);
    }

    map_base=mmap(0,0xff,PROT_READ|PROT_WRITE,MAP_SHARED,fd,GPIO_BASE);		//将硬件寄存器地址映射到内存
    GPIO_OUT = (volatile unsigned int *) (map_base + 8);

    while(1){
        *GPIO_OUT |= (1 << 0);			//GPIO0 置高
        sleep(1);
        *GPIO_OUT &= ~(1 << 0);			//GPIO0 置低
        sleep(1);
    }
    close(fd);
    munmap(map_base,0xff);				//解除映射关系
    return 0;
}

发现没,在单片机和嵌入式linux中控制GPIO是一样简单的,就是配置一下相应的寄存器。

进化。。。


1、在STM中,有大量的寄存器(STM32寄存器的规模可不是传统8位单片机能够比拟的),程序员很难记得住每一个寄存器的名称、地址和作用,那么ST公司就想出了一个办法——库函数。即,使用函数调用的方式去代替直接操作硬件寄存器。好处:程序员只需要调用这些函数就行了,而不需要再去记那些枯燥的寄存器地址了。
实例代码

int main()
{
	LED_Init();
	while(1)
	{
		GPIO_SetBits(LED_PORT,GPIO_Pin_0);		//打开 LED
		delay(0xFFFFF);
		GPIO_ResetBits(LED_PORT,GPIO_Pin_0);	//关闭 LED
		delay(0xFFFFF);
	}
}

2、 那么在嵌入式Linux中,为什么要将操作硬件的函数以内核驱动的形式嵌入到内核中呢,直接操作不也可以吗?这就牵扯到linux中的用户态与内核态的作用,linux中为什么要有用户态与内核态呢?直接都是用户态不行吗?切来切去的还麻烦。当然不行,因为上升到复杂的操作系统,系统中有大量的用户程序,如果每一个用户程序都能很轻松的直接操作硬件资源(包括I/O读写、内存读写等),那么一个操作系统一天不知道要崩溃多少回(如果程序员一不小心将不适当的内容写到了不该写的地方,就很容易导致系统崩溃)。所以操作系统就设计了内核态与用户态,凡是涉及到IO读写、内存分配等硬件资源的操作时,往往不允许直接操作,而是通过一种叫系统调用的过程,让程序陷入到内核态运行,以保证系统的安全可靠。所以,顺理成章,操作系统要分内核态与用户态,操作硬件资源需要放在内核态。所以我们要把操作硬件的驱动放到内核——内核驱动。那么,怎么做呢,其实很简单,编写内核驱动有着相同的套路,无非就是module_init()、module_exit()等一系列函数的调用,文件操作结构体file_operations中open、write、read、ioctl、close等一系列函数的具体实现。怎么实现呢,其实思路清晰很简单,open函数就是对设备的初始化,本例中将GPIO的模式配置代码放入open函数中就OK了,write函数中放入操作GPIO输出寄存器的代码就OK了,read中放入读取GPIO寄存器值的代码,ioctl中也放入操作GPIO输出寄存器的代码。很轻松就实现了内核驱动的编写,不是吗?
实例代码

#include <linux/mm.h>
#include <linux/miscdevice.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/mman.h>
#include <linux/random.h>
#include <linux/init.h>
#include <linux/raw.h>
#include <linux/tty.h>
#include <linux/capability.h>
#include <linux/ptrace.h>
#include <linux/device.h>
#include <linux/highmem.h>
#include <linux/crash_dump.h>
#include <linux/backing-dev.h>
#include <linux/bootmem.h>
#include <linux/splice.h>
#include <linux/pfn.h>
#include <linux/export.h>
#include <linux/io.h>
#include <linux/aio.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <asm/uaccess.h>
#include <linux/ioctl.h>

//define registers
volatile unsigned long *GPIO_OUT;
volatile unsigned long *GPIO_OE;

/**********************  基本定义 *******************************/
//内核空间缓冲区定义
#if 0
#define KB_MAX_SIZE 20
#define kbuf [KB_MAX_SIZE]
#endif

//初始化函数必要资源定义
//用于初始化函数当中
//device number;
dev_t dev_num;
//struct dev
struct cdev leddrv_cdev;

//auto "mknode /dev/leddrv c dev_num minor_num"
struct class *leddrv_class = NULL;
struct device *leddrv_device = NULL;

/**************** 结构体 file_operations 成员函数 *****************/
//open
static int leddrv_open(struct inode *inode, struct file *file)
{
    printk("leddrv drive open.\n");
    //配置模式省略
    return 0;
}

//close
static int leddrv_close(struct inode *inode, struct file *file)
{
    printk("leddrv drive close...\n");
    return 0;
}

//read
static ssize_t leddrv_read(struct file *file, char __user *buffer, size_t len, loff_t *pos)
{
    int ret_v = 0;
    printk("leddrv drive read...\n");
    return ret_v;
}

//write
static ssize_t leddrv_write(struct file *file, const char __user *buffer, size_t len, loff_t *offset)
{
    int ret_v = 0;
    printk("leddrv drive write...\n");
    return ret_v;
}

//unlocked_ioctl
static int leddrv_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret_v = 0;
    printk("leddrv drive ioctl cmd = %d, arg = %d\n", cmd, arg);
    if (arg == 0)
        *GPIO_OUT &= ~(1 << cmd);
    else if (arg == 1)
        *GPIO_OUT |= (1 << cmd);

    return ret_v;
}

/***************** 结构体: file_operations ************************/
//struct
static const struct file_operations leddrv_fops = {
    .owner = THIS_MODULE,
    .open = leddrv_open,
    .release = leddrv_close,
    .read = leddrv_read,
    .write = leddrv_write,
    .unlocked_ioctl = leddrv_ioctl,
};

/*******************  functions: init , exit**********************/
//条件值变量,用于指示资源是否正常使用
unsigned char init_flag = 0;
unsigned char add_code_flag = 0;

//init
static __init int leddrv_init(void)
{
    int ret_v = 0;
    printk("leddrv drive init...\n");

    //函数alloc_chrdev_region主要参数说明:
    //参数2: 次设备号
    //参数3: 创建多少个设备
    if ((ret_v = alloc_chrdev_region(&dev_num, 0, 1, "leddrv")) < 0)
    {
        goto dev_reg_error;
    }
    init_flag = 1; //标示设备创建成功

    printk("The drive info of leddrv:\nmajor: %d\nminor: %d\n",
           MAJOR(dev_num), MINOR(dev_num));

    cdev_init(&leddrv_cdev, &leddrv_fops);
    if ((ret_v = cdev_add(&leddrv_cdev, dev_num, 1)) != 0)
    {
        goto cdev_add_error;
    }

    leddrv_class = class_create(THIS_MODULE, "leddrv");
    if (IS_ERR(leddrv_class))
    {
        goto class_c_error;
    }

    leddrv_device = device_create(leddrv_class, NULL, dev_num, NULL, "leddrv");
    if (IS_ERR(leddrv_device))
    {
        goto device_c_error;
    }
    printk("auto mknod success!\n");

    GPIO_OUT = (volatile unsigned long *)ioremap(0x18040008, 4);

    //如果需要做错误处理,请:goto leddrv_error
    add_code_flag = 1;
    //----------------------  END  ---------------------------//

    goto init_success;

dev_reg_error:
    printk("alloc_chrdev_region failed\n");
    return ret_v;

cdev_add_error:
    printk("cdev_add failed\n");
    unregister_chrdev_region(dev_num, 1);
    init_flag = 0;
    return ret_v;

class_c_error:
    printk("class_create failed\n");
    cdev_del(&leddrv_cdev);
    unregister_chrdev_region(dev_num, 1);
    init_flag = 0;
    return PTR_ERR(leddrv_class);

device_c_error:
    printk("device_create failed\n");
    cdev_del(&leddrv_cdev);
    unregister_chrdev_region(dev_num, 1);
    class_destroy(leddrv_class);
    init_flag = 0;
    return PTR_ERR(leddrv_device);

//-------------------- 请在此添加您的错误处理内容 ------------------//
leddrv_error:
    add_code_flag = 0;
    return -1;
    //---------------------          END         --------------------//

init_success:
    printk("leddrv init success!\n");
    return 0;
}

//exit
static __exit void leddrv_exit(void)
{
    printk("leddrv drive exit...\n");

    if (add_code_flag == 1)
    {
        //--------------   请在这里释放您的程序占有的资源   -----------//
        printk("free your resources...\n");

        iounmap(GPIO_OUT);

        printk("free finish\n");
        //-----------------------     END      --------------------//
    }

    if (init_flag == 1)
    {
        //释放初始化使用到的资源
        cdev_del(&leddrv_cdev);
        unregister_chrdev_region(dev_num, 1);
        device_unregister(leddrv_device);
        class_destroy(leddrv_class);
    }
}

/**************** module operations************************/
//module loading
module_init(leddrv_init);
module_exit(leddrv_exit);

//some infomation
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("from Jafy");
MODULE_DESCRIPTION("leddrv drive");
/*********************  The End ***************************/

编写应用程序测试

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>

#define LED_ON 0
#define LED_OFF 1

char str_dev[] = "/dev/leddrv";

int main(int argc, char *argv[])
{
    int fd;
    if (argc != 2)
    {
        printf(" %s <on|off> to turn on or off LED.\n", argv[0]);
        return -1;
    }

    fd = open(str_dev, O_RDWR | O_NONBLOCK); 
    if (fd < 0)
    {
        printf("can't open %s \n", str_dev);
        return -1;
    }

    while (1)
    {
        ioctl(fd, atoi(argv[1]), 0);
        printf("led_%d is off\n", atoi(argv[1]));
        usleep(50000);
        ioctl(fd, atoi(argv[1]), 1);
        printf("led_%d is on\n", atoi(argv[1]));
        usleep(50000);
    }

    return 0;
}

运行结果
1、加载内核驱动模块

root@Shuncom:~# insmod /tmp/leddrv.ko

内核日志

[75774.330000] leddrv drive init...
[75774.330000] The drive info of leddrv:
[75774.330000] major: 251
[75774.330000] minor: 0
[75774.340000] auto mknod success!
[75774.340000] leddrv init success!

2、执行应用程序

root@Shuncom:~# led_app 0
led_0 is off
led_0 is on
led_0 is off
led_0 is on
led_0 is off
led_0 is on
...

内核日志

root@Shuncom:~# led_app 0
led_0 is off
led_0 is on
led_0 is off
led_0 is on
led_0 is off
led_0 is on
...

同时看到LED在闪烁

2019-10-17 18:32:14 m0_37565736 阅读数 28

因为知识学得不系统,以前涉及到内核的地方就懵了,如坠五里雾中,最近找了本书大概翻了翻,了解了一些相关的知识,这里简单记一下。
从前在单片机上编程,用用什么汇编,c语言之类的,实现一些简单的东西,指令的执行是简单的–至少从表面上来看是这样的,裸机的单片机只能执行一个控制流,那会对内核的认识就是感觉它很高级,很神秘。
计算机启动的过程是这样的:cpu上电或复位后,会去bios rom里执行bios的相关指令,bios执行的操作包括系统自检,维护cmos里保存的一些开机参数等,当然还有加载操作系统内核到内存。bios加载好内核之后就会把控制权交给内核了,当然这里有bios寻找引导扇区,或者什么bootloader进行加载之类的略过不提。操作系统内核应当与普通程序的执行没什么不同,除了内核可以执行的指令比普通进程多–内核可以执行特权指令(通过模式位的设置)。
为什么要有内核呢?众所周知的原因是拥有内核可以实现任务调度,同时执行多个进程,充分利用计算机的性能。另外内核还起到统合各种硬件,维护各种资源的作用,并且通过内核态用户态的划分,限制了特权指令,从而一定程度上保持了整个计算机系统的健壮性和稳定性。
内核是如何实现的呢?内核要实现任务调度,多个任务可以“同时”运行,这里的同时是带引号的,任务只是在切换来切换去,执行一会这个再去执行下一个,因为速度很快,看起来就像是在同时运行一样。
为了完成这个目标,需要每个任务保存自己的状态–即上下文,用于被挂起时保存工作状态,被调度到后恢复工作环境。还需要对进程的选择,当达到某种条件,比如某进程时间片用完了,中断使状态切换到内核态,去执行内核态代码,根据某种调度算法选出下一个运行的任务,当然这个时候被中断的任务的上下文已经被保存,而即将运行的任务的上下文也会被恢复,以继续执行。当然,要实现这些,你要对cpu的编程熟悉一点,起码知道保存好哪些信息可以完全的恢复一个任务。
文件系统,应该就是把数据给抽像化,形成一个逻辑上的整体,以一定的信息记录这些数据的信息,比如大小类型之类的,在通过目录(本身也是一种文件),把文件关联到一起,组织起来。
我之前还有一个误区,我一直以为操作系统的内核实现就是单纯靠软件的,硬件部分还是跟单片机差不多,只不过性能更好,更复杂一些,当然可能这样也是可以实现的,但是很明显,为了提升效率,在操作系统的构成中,是有别的硬件可供使用以构建操作系统的。不知道这算硬件促进软件还是软件促进硬件呢?
现在我对这方面了解还是比较粗浅的,如果有什么错漏的地方,还请路过的大侠帮忙指正,多谢~

2019-09-19 23:45:24 qq_37007823 阅读数 846

一、简介
FSC-OS (@Angle_145)是基于多应用多线程的一个操作系统,线程集优先级和时间切片于
一体,充分利用 CPU 资源,让程序执行更高效。目前刚完成初版,只具备内核功能。
后续版本会持续更新,此手册仅适用于 V1.0.2 版本。
源码地址:
(1) Github 链接:https://github.com/Angle145/FSC_STOS.git
(2) BaiduYun 链接:https://pan.baidu.com/s/1Ec9nZxiLMtL9obxawFt7OA 提取码:i6k1
1. 内核简介:多线程运行规则基于优先级,同级优先级基于时间切片运行。所有创建
线程均被安装到一个优先级链表中,每个链表节点为某个优先级,链表不存在相同
优先级节点。每个节点也是一个链表,该链表内安装有该节点优先级的所有同级线
程。例:在系统中先后创建了 33 个线程,优先级种类共有 9 种,每种优先级包含的线程数量各不同,具休如图,图中绿色为优先级链表节点,数字表示优先级,橙色为同级优先级线程,在寻找最高优先级时 ,先对优先级链表进行扫描,查找出最高优先
级节点,再在节点链表中查找到具体线程运行(按顺序运行)。如图

2. 系统软定时系统:系统内所有用到定时延时的都基于此软定时系统。

 

 

 

二、文件结构

三、系统具体介绍

(1) os_main()函数       系统入口函数

void os_main(void)
{                     
    App_system=os_app_create__App_system(); //创建系统APP(系统自带,用户不可更改)
    App_01=os_app_new_create__App_01();         //创建用户APP_01
    App_02=os_app_new_create__App_02();         //创建用户APP_02
	
    os_init_and_startup();//系统初使化并启动
}

(2)os_app_new_create__XXX()APP创建函数

os_type_app_id* os_app_create__App_system(void) //创建App_system (系统APP)
{
    os_type_app_id *app_id=os_app_new_init();
    os_app_name_set(app_id,"APP_SYSTEM");
    os_app_prio_set(app_id,0);
    os_app_state_set(app_id,os_app_state_creating);
	
    //                                (不变)   (线程函数名)  (线程名称)  (线程堆栈大小)(时间片)(优先级)    (状态)       (参数)  //时间片和参数暂时没用
    //                                app_id   app_thread app_thread_name stk_size slice  prio         state       para
    thread_idle=os_thread_new_create( app_id, os_thread_idle, "thread_idle", 128, 1, 0, os_thread_state_readying, (void*)0 ); //创建空闲线程 
    thread_manager=os_thread_new_create( app_id, os_thread_manager, "thread_manager", 256, 1, (os_u32)-1, os_thread_state_readying, (void*)0 ); //创建系统管理线程 
	
    os_app_new_create(app_id);
    return app_id;
}


os_type_app_id* os_app_new_create__App_01(void) //创建APP_01
{
    os_type_app_id *app_id=os_app_new_init();
    os_app_name_set(app_id,"APP_01");
    os_app_prio_set(app_id,1);
    os_app_state_set(app_id,os_app_state_creating);
	
    //                    (不变)   (线程函数名)  (线程名称)  (线程堆栈大小)(时间片)(优先级)     (状态)               (参数)  //时间片和参数暂时没用
    //                    app_id   app_thread app_thread_name stk_size slice  prio         state                para
    os_thread_new_create( app_id,   thread_01,  "thread_01",   128,     10,    2,         os_thread_state_readying,  (void*)0  ); //创建线程1 
    os_thread_new_create( app_id,   thread_02,  "thread_02",   128,     10,    2,      os_thread_state_readying,  (void*)0  ); //创建线程2
    os_thread_new_create( app_id,   thread_03,  "thread_03",   128,     10,    3,                         os_thread_state_readying,  (void*)0  ); //创建线程3

    os_app_new_create(app_id); 
    return app_id;
}

 

(3)线程函数

static void os_thread_idle(void) //空闲任务,用于防止用户任务都处于暂停或为数量为0时出错(禁止调用os_thread_delay())
{
    os_printf("app_idle is running\r\n");
    while(1) 
    {
        //禁止调用os_thread_delay()
    }
}
static void thread_01(void)  //APP_01的线程01
{   
    while(1) 
    {	 		 
        os_printf("APP_01  thread_01\r\n");
        os_thread_delay(500);  	//500ms执行一次任务,这个时间越小,本任务执行的间隔越小!
    } 
}

static void thread_02(void)  //APP_01的线程02
{	
    while(1) 
    {
        os_printf("APP_01  thread_02\r\n");
        os_thread_delay(1000);			//1000ms执行一次任务
    }
}

static void thread_03(void) //APP_01的线程03
{
    while(1) 
    {
        os_printf("APP_01  thread_03\r\n");	
        os_thread_delay(3000);			//3000ms执行一次任务	 
    }			
}

 

(4)系统配置

conf.h

/*********************************OS参数定义********************************************/
#define OS_MEMORYPOOL_SIZE  4*1024*4     //内存池大小,单位:Byte
#define THREAD_TIME_SLICE   1          //线程时间切片,单位:1ms   
#define APP_TIME_SLICE      10         //app时间切片,单位:1ms 
#define THREAD_NAME_LEN     32
#define APP_NAME_LEN        32
/**************************************************************************************/

(5)全局变量文件

global.c

#include "global.h"//全局变量

/*---------------------------------------------用户全局变量定义区------------------------------------------------*/

os_type_app_id *App_system; 
os_type_app_id *App_01;
os_type_app_id *App_02;
os_type_app_id *App_03;

os_type_thread_id *thread_idle;
os_type_thread_id *thread_manager;
global.h

#ifndef _OS_GLOBAL_
#define _OS_GLOBAL_
#include "type.h"
/*---------------------------------------------用户全局变量声明区------------------------------------------------*/
extern os_type_app_id *App_system; 
extern os_type_app_id *App_01;
extern os_type_app_id *App_02;
extern os_type_app_id *App_03;

extern os_type_thread_id *thread_idle;
extern os_type_thread_id *thread_manager;

 

(6)用户初使化

os_main.c

//用户代码初使化函数(用户所有初使化函数均放在此函数内,初使化函数禁止调用os_thread_delay()和禁止调度)
void os_user_init(void) 
{
	  USART1_Config(115200); //串口1初使化
	  printf("-@OS Inside\r\n");
	
}	

 

(7)系统交互指令

shell.c

#include "shell.h"

//全局指令
char cmd_help[]={"cmd/help//"};   //指令帮助
char cmd_enter[]={"cmd/enter//"}; //进入指令系统
char cmd_exit[]={"cmd/exit//"};   //退出指令系统

//系统指令
char cmd_osinformation[]={"osinformation//"};//查看系统状态
char cmd_hardreset[]={"hardreset//"};        //硬件重启
char cmd_oson[]={"oson//"};                  //系统关闭
char cmd_osoff[]={"osoff//"};                //系统打开
//在此创建字符串指令


void os_shell_handle_process(void)
{
    if(os_shell_compare(cmd_osinformation)==0) { os_information_process();  }		
    if(os_shell_compare(cmd_hardreset)==0)     { os_hard_reset_tips(); os_hard_reset(); }
    if(os_shell_compare(cmd_oson)==0)          { os_on_tips();  os_on(); }
    if(os_shell_compare(cmd_osoff)==0)         { os_off_tips();  os_off(); }
    //在此加入指令功能代码
}

 

2018-08-12 10:56:54 liming0931 阅读数 293

基于网上网友的代码,自己在单片机上实现, 特此记录分享之。

基于https://blog.csdn.net/yyx112358/article/details/78877523

//使用KEIL C51实现的简单合作式多任务操作系统内核

#include <regx52.H>
#include <INTRINS.H>

typedef unsigned char u8;
typedef unsigned int u16;

sbit LED1 = P2 ^ 0;
sbit LED2 = P2 ^ 1;
sbit LED3_idle = P2 ^ 3;

//两个宏定义是为了保护现场,不被定时中断打乱。
//主要用于需要一次性运行完毕的代码中。
#define OPEN_SYS_ISR() {EA=1;TR2=1;}
#define CLOSE_SYS_ISR() {EA=0;TR2=0;TF2=0;}

#define OS_TASK_STACK_SIZE (2+13+2*3)//存放断点2B,中断函数可能压栈13B,子程序每嵌套一层2B

#define OS_TASK_NUM 2

typedef struct OS_TASK_ST
{
    u8  delay;		//当前延时剩余时间
    u8  stack[OS_TASK_STACK_SIZE]; //私有堆栈
    u8  sp;			//私有堆栈指针
} OS_TASK; 			//任务工作块。

data OS_TASK os_task[OS_TASK_NUM];	//必须定义为data(因堆栈只能在data区)
data u8 os_idle_stack[15];


void os_switch(void);
void os_idle(void);
//void os_update_time(void);
void os_load(u8 id, void(*func));
void os_delay(u8 id, u8 delay);
void LED_Driver();
void os_task_0(void);
void sys_init(void);
void delay(u16 i);

/*******************************************************************************
* 函 数 名         : Timer2 Init
* 函数功能		   : 定时器0初始化,用于系统时钟
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void Timer2_init()
{
    RCAP2H = (0xFFFF - 10000) / 256;
    RCAP2L = (0xFFFF - 10000) % 256;	//12MHz晶振下定10ms,自动重装
    TH2 = RCAP2H;
    TL2 = RCAP2L;					//定时器2赋初值
    T2CON = 0;					//配置定时器2控制寄存器,这里其实不用配置,T2CON上电默认就是0,这里赋值只是为了演示这个寄存器的配置
    T2MOD = 0;					//配置定时器2工作模式寄存器,这里其实不用配置,T2MOD上电默认就是0,这里赋值只是为了演示这个寄存器的配置

    IE = 0xA0;	//1010 0000开总中断,开外定时器2中断,可按位操作:EA=1; ET2=1;
    TR2 = 1;		//启动定时器2
}

void LED_Driver()
{
    LED1 = ~LED1;
}

void os_idle(void)
{
    while (1)
    {
        LED3_idle = ~LED3_idle;
        os_switch();
    }
}

/*
 *任务调度,转向当前延时时间到且优先级最高(id较小)任务
 而在一般的应用中,我们往往需要一个软件延时。例如:按键去抖、周期性采样等等。
 所以,这就要求有一个软件定时器功能。因此,修改调度器如下:
首先定义任务控制器数据结构,加入一个延时记录:
 */
void os_switch(void)//任务切换
{
    u8 i = OS_TASK_NUM;
    do
    {
        i--;
        if (os_task[i].delay == 0)//如果有任务延时时间到,则跳转至相应任务
            SP = os_task[i].sp;
    }
    while (i); //否则不改变SP,继续执行os_idle()
}

/*
 *    更新任务延时表
 *    注:应定时更新,最好放入定时器中断

void os_update_time(void)
{
    u8 i = OS_TASK_NUM;
    do
    {
        i--;
        if(os_task[i].delay)
            os_task[i].delay--;
    }
    while(i);
}
 */

//修改任务工作块并跳转入os_idle()进行任务切
void os_delay(u8 id, u8 delay)
{
    TR2 = 0;//关中断
    {
        os_task[id].delay = delay;      //延时设定
        os_task[id].sp = SP;             //保存SP
        SP = os_idle_stack + 1;            //SP指向os_idle_stack[1]
        //os_delay()结束后跳转os_idle()
    }
    TR2 = 1;
}

void os_load(u8 id, void(*func))
{
    os_task[id].sp = os_task[id].stack + 1;	//私有堆栈指针指向私有堆栈
    os_task[id].stack[0] = (u16)func & 0xFF;//私有堆栈栈底存放任务函数入口
    os_task[id].stack[1] = (u16)func >> 8;
}


void os_task_0(void)
{
#define OS_CUR_ID  (0)

    //static u8 i=0;

    //KEIL一般分配临时变量在RAM不在堆栈
    //因此为了防止任务之间改写凡是作用域跨越os_delay()应作为static

    while (1)
    {
        LED1 = ~ LED1;
        os_delay(OS_CUR_ID, 1);
    }

#undef OS_CUR_ID
}

void os_task_1(void)
{
#define OS_CUR_ID  (1)

    //static u8 i=0;

    //KEIL一般分配临时变量在RAM不在堆栈
    //因此为了防止任务之间改写凡是作用域跨越os_delay()应作为static

    while (1)
    {
        //LED_Driver();
        LED2 = ~ LED2;
        os_delay(OS_CUR_ID, 1);
    }

#undef OS_CUR_ID
}

/*******************************************************************************
* 函 数 名         : delay
* 函数功能		   : 延时函数,i=1时,大约延时10us
*******************************************************************************/
void delay(u16 i)
{
    while (i--);
}

void sys_init()
{
    u8 i;
    for (i=0; i<4; i++)
    {
        LED_Driver();
        delay(10000);
    }
}

void main()
{
    //…初始化外设
    sys_init();

    //…初始化os所用定时器

    Timer2_init();

    //…初始化其它任务控制器
    os_load(0, os_task_0);
    os_load(1, os_task_1);


    os_idle_stack[0] = (u16)os_idle & 0xFF;
    os_idle_stack[1] = (u16)os_idle >> 8;
    SP = os_task[0].sp;	//运行任务0

    return;	//跳转,永不返回。
}



//12MHz晶振下定10ms,自动重装
void timer2() interrupt 5
{
    u8 i = OS_TASK_NUM;
    //EA=0;
    //ET2=0;
    //TF2=0;
    //!!!注意!!!定时器2必须由软件对溢出标志位清零,TF2=0;硬件不能清零,
    //这里与定时器0和定时器1不同!!!
    CLOSE_SYS_ISR();

    do
    {
        i--;
        if (os_task[i].delay)
            os_task[i].delay--;
    }
    while (i);

    OPEN_SYS_ISR();
}

 

 

2017-12-22 23:12:25 yyx112358 阅读数 2385

    以前做课程设计时候,在51上实现了一个简单的合作式操作系统内核。写的时候,主要是出于检验自己单片机原理和操作系统知识的目的。这个内核现在看来,功能非常简单,实时性也不高,但是它毕竟是在51单片机上用不到每个线程17B的内存实现了一个多任务并行处理功能,而且完全用C语言写成,没有用到汇编。所以整理发出,权为资料整理。

1 单片机上的多任务操作思路

    在本实验当中,涉及到了实时性较高的电机控制,DS18B20的读写有严格的时序要求。而数码管动态显示、特别是按键扫描等涉及到了不定的延时。这两种设备在实时性上有着一定的冲突。因此,实现思路有三种:
1. 无限循环+中断的前后台系统。
2. 有限状态机(FSM)系统。
    主要思路如下:一个定时器生成一个系统基准时间systick(如1ms加1) 。其它任务拆分为多个状态放入主循环当中,通过状态转换和systick进行工作。
    例如,按键状态机分NOT_PRESSED, PRESS_DELAY, PRESSED,REALEASE_DELAY四个状态。
3. 使用调度器的操作系统。
    第一种方式在应用简单的情况下,具有编写容易、系统本身不耗费资源的优点。但当程序复杂时,各模块前后耦合维护复杂,而且很难保证实时性(当高优先级任务需要处理时,会由于低优先级任务正在运行而得不到及时处理)。如果使用中断,则当任务变多时将没有足够的中断可用,而且中断当中加入过多的程序也是稳定性的大忌。
    第二种方式主要思路如下:首先使用一个变量systick存放系统运行时间(在1ms定时器中断中自加)。而后每个外设结合systick,根据当前运行状态判断是否进行状态转换,并执行相应操作。该方法实时性好,逻辑性强,且不必对PC,SP进行操作。但缺点是程序编写非常复杂。
    第三种方式将不同的模块分为不同的任务,并根据优先程度赋予不同的优先级。在调度器的作用下,各任务在宏观上达到了一个“并行运行”的效果。该方法实时性好,任务编写容易,由于采用了合作式调度器,也不必担心任务的可重入性。缺点是调度器编写复杂,且本身会产生一定开销。

1 多任务切换原理

CPU是依靠PC来确定执行的程序。所以要想在多个函数之间切换,理论上只需要修改PC值即可。但单纯的修改PC值的话,原有的运行状态就会丢失,所以必须保护此时的运行状态(寄存器R0~R8还有PSWSP)。这个过程很像中断服务程序:函数调用过程中,LCALL指令等的返回值还有被保护的寄存器值将被保存在堆栈当中,待结束之后返回原程序时从堆栈恢复。除此之外,C语言中的一些局部变量也是存放在堆栈当中的。如图:





所以,最基本的调度器如下:在系统的初始化阶段,给每一个任务分配一个私有的栈空间。这样,在任务切换时,只需要将需要保护的现场PUSH入堆栈,将被切换的任务的现场恢复(将被保存的通用寄存器R0~R8PSW写入),再将SP指向被切换任务的私有栈即可。如图:

2 KEIL C51多任务切换实现

对于KEIL C51而言,情况有所不同。KEIL C编译器在处理函数调用时的约定规则为"子函数有可能修改任务寄存器",因此编译器在调用前已释放所有寄存器,子函数无需考虑保护任何寄存器.因此,只需要修改堆栈SPPC即可。

基于这一特性,调度器写为了一个C语言函数的形式。

最初写好的基本切换函数如下:

void os_switch()
{  
    task_sp[task_id] = SP;  
    if(++task_id == MAX_TASKS)  
        task_id = 0;   
    SP = task_sp[task_id];
}

逐句解释:

首先,任务A在合适地方调用该函数进行切换,当进入该函数之前,R0~R8已被释放无需保护,而LCALL指令将2字节的PC地址PUSH入堆栈,SP+2

随后,当前任务A(任务号task_id)的堆栈栈顶SP存入数组task_sp[]中。而后task_id自加指向下一个任务B(溢出则归零)。

而后,SP指向了任务B的堆栈栈顶(被存在了task_sp[task_id])。此时栈顶的是任务B在上一次切换(调用os_switch())时被压入的断点PC地址。

当函数结束,调用RET指令返回时,任务B栈顶的断点PC地址被自动写入PC,函数从任务B上一次切换的位置继续执行。

3 带软件定时器的调度器

以上的基本调度器非常精简,调度开销也非常小。但是它实际上是一个无优先级的调度器,也不具备软件定时器功能。程序流程图如下:



而在一般的应用中,我们往往需要一个软件延时。例如:按键去抖、周期性采样等等。所以,这就要求有一个软件定时器功能。因此,修改调度器如下:

首先定义任务控制器数据结构,加入一个延时记录:

void os_switch()
{  
    task_sp[task_id] = SP;   
    if(++task_id == MAX_TASKS)   
        task_id = 0;   
    SP = task_sp[task_id]; 
}

这样,调度函数改为:

/*
 *    任务调度,转向当前延时时间到且优先级最高(id较小)任务
 */
void os_switch(void)//任务切换
{
        unsignedchar  i=OS_TASK_NUM;
        do{
                i--;
                if(os_task[i].delay==0)//如果有任务延时时间到,则跳转至相应任务
                        SP=os_task[i].sp;
        }while(i); //否则不改变SP,继续执行os_idle()
}

进入过程一样。在函数中首先将各个任务的delay--,如果计数为0则跳转至相应函数(SP赋值为相应的私有堆栈指针)。

可以看出,任务的id越小,优先级越高(例如任务1,2均计时到0,首先会任务2赋值给SP,而后检测到任务1也计时为0SP会被任务1覆盖)。

但是这样有一个问题,假如任务0调用了os_switch()进行调度。而此时所有任务都尚未计时到0,则SP未修改,重新执行任务0,相当于任务0没能进行延时。这是不允许的。所以,必须加入一空闲任务

/*
 *    调度任务+空闲任务,执行任务调度;当前无需要调度任务则执行本任务
 */
void os_idle(void)
{
        while(1)
        {
                os_switch();
        }
}

空闲任务很简单,只是一个无限循环,不停的进行任务调度。当所有其它任务都挂起时,os_switch()就不会修改SP,因此任务仍然停留在SP当中。

Os_idle()也需要一个固定私有栈空间,由于不需要delay部分,因此只需要简单地定义:

data unsignedchar  os_idle_stack[15];

在其它操作系统中如uc/OS-II中,调度器是放在中断中的,而os_idle()在不加入其它功能时只是一个while(1)。但是,由于C51对中断程序的处理与普通函数不同,会视情况压入不同个数的寄存器(从3个到13个不等)。所以出于简单起见,将调度器放入了idle任务。相比较而言,效率有所下降。

而后,作为一个软件定时器,需要定时对计数变量delay更新,这一工作放入定时器:

void os_update_time(void)
/*
 *    更新任务延时表
 *    注:应定时更新,最好放入定时器中断
 */
{
        unsigned char  i=OS_TASK_NUM;
        do{
                i--;
                if(os_task[i].delay)
                        os_task[i].delay--;
        }while(i);
}

调用间隔可以任意但必须一致,本设计设定为1ms一次。

任务放弃CPU占用可以使用os_switch(),添加延时就需要:

void os_delay(unsigned char id,unsigned char        delay)
//修改任务工作块并跳转入os_idle()进行任务切换
{
        TR0=0;//关中断
        {
                os_task[id].delay+=delay;      //延时设定
                os_task[id].sp=SP;             //保存SP
                SP=os_idle_stack+1;            //SP指向os_idle_stack[1]
                                               //os_delay()结束后跳转os_idle()
        }
        TR0=1;
}

该函数将任务控制器OS_TASK添加延时、保存SP,并跳转入os_idle()执行切换。注意这里必须关闭中断TR0。这是为了防止在该函数中碰到定时器中断(调用os_update_time()),从而出现延时错误。例如:

在某时刻任务1使用os_delay()函数延时1ms,在os_task[id].delay+=delay;之后碰见中断,将os_task[id].delay--,这样os_task[id].delay将等于0,等同于没有进行延时。

4 任务控制器的数据结构和初始化

任务控制器的数据结构在上一节已经说的很清楚,再次列举如下:

typedef    struct
{
        unsigned char  delay;//当前延时剩余时间
        unsigned char        stack[OS_TASK_STACK_SIZE]; //私有堆栈
        unsigned char  sp;//私有堆栈指针
}OS_TASK;//任务工作块。

但有一点必须注意,任务控制器只能放在内存data区(低128B内存),换言之,所有任务控制器占用的RAM少于120B。这是因为51的堆栈只能放在data区,PUSHPOP指令也是操作的data区。

因此,可以说堆栈空间非常有限,任务的数量受到限制。最重要的是,任务中允许中断嵌套的子程序数目有限。私有堆栈当中,最低2B是任务入口;由于中断随时可能发生,因此必须从最坏情况考虑留出13B空间;剩下的才是子程序调用允许使用的。假如子程序中局部变量不多不需要将局部变量放入堆栈,则每嵌套一层子程序需要2B(LCALL压栈PC)。故定义:

#define    OS_TASK_STACK_SIZE        (2+13+2*3)//存放断点2B,中断函数可能压栈13B,子程序每嵌套一层2B
data       OS_TASK       os_task[OS_TASK_NUM];//必须定义为data(因堆栈只能在data区)

由于是全局变量,os_task[]元素初始值为0,故必须初始化。其函数为:

void os_load(unsigned char id,void(*func))
/*
 *    装载任务入对应工作块
 *    参数:任务id,任务函数
 */
{
        os_task[id].sp=os_task[id].stack+1;//私有堆栈指针指向私有堆栈
        os_task[id].stack[0]=(unsignedint)func&0xFF;//私有堆栈栈底存放任务函数入口
        os_task[id].stack[1]=(unsignedint)func>>8;
}

5 多任务系统编写规范

在作了如上处理之后,就可以方便地使用多任务系统了。

void main()
{
        //…初始化外设
        //…初始化os所用定时器
        os_load(0,os_task_0);
        //…初始化其它任务控制器
       os_idle_stack[0]=(unsignedint)os_idle&0xFF;
        os_idle_stack[1]=(unsignedint)os_idle>>8;
        SP=os_task[0].sp;//运行任务0
        return;//跳转,永不返回。
}

main()函数当中,首先是初始化外设,而后使用os_load()初始化各任务控制器和空闲任务控制器。最后跳转入任务0,永不返回。

而各个任务则各自独立,为超级循环结构。简而言之,与一般程序的main()函数相同:

void os_task_0(void)
{
#define    OS_CUR_ID  (0)
static unsigned char i=0;
        //KEIL一般分配临时变量在RAM不在堆栈
//因此为了防止任务之间改写凡是作用域跨越os_delay()应作为static
        while(1)
        {
                MOTOR_Driver();
                os_delay(OS_CUR_ID,1);
        }
#undef OS_CUR_ID
}

有一点必须注意:局部变量必须定义为static。这是由于KEIL C51为了节省内存,局部变量只要可能就存放在了寄存器R0~R8中。这样,一旦任务切换,局部变量相当于被覆盖。

由于是合作式调度器,不存在抢占式调度器中任务被直接打断的风险。因此,除局部变量必须定义为static外,无需加入任何可重入性代码。

6 主要问题:

1.      OS设计:

OS设计思路在第2节有详解,不再赘述。任务如何切换、延时如何加入、调度器位置(在中断中还是idle任务中)、数据结构如何设计、如何优化代码……都是曾经碰到的问题

以上问题固然有难度,但写起来并无“憋屈”之感,反而写完后颇有自得之意。但主要瓶颈在于51的内存特别是能作为堆栈的内存过小,这在程序设计上带来几个重大束缚:

l 可供嵌套的子函数嵌套深度过小,使得子程序设计时不敢嵌套过多,不得不在一个任务中集成过多功能,与模块化的思路不符。

l 可以运行的任务过少,使得任务中不得不加入多个外设控制,并使用状态机切换。这使得多任务运行的优势大大削弱。

l 为了能运行4个任务,不得不将data区(低128B)几乎全部占用。使得其它全局变量不得不放入idata(128B)乃至pdata(外部RAM的低256B),使得程序运行效率下降

不过,由于本OS的易移植性,如果将该系统移植到可用栈内存更多的CPU上,该缺点即可几乎忽略不计。还可以加入更多更复杂的功能。

2.      准面向对象设计

在外设驱动编写时,起初试图将外设的所需变量和操作方法封装成一个类,例如:

typedef    struct
{
        long disp_num;               //待显示数字,-99999~999999
        unsigned char  ptr;  //动态扫描当前显示位指针
        unsigned char  point_signal;   //小数点标志位,对应位为1则对应位小数点显示
        void (*Set)(NIXIE_DISP_TYPE,long); //按照格式设定示数
        void (*Driver)(void);                             //驱动程序
}NIXIE_STRUCT;
NIXIE_STRUCT   DEV_NIXIE;

这样,驱动程序的组织更有条理,在编辑器中还能直接使用自动完成功能(少敲很多字啊)。更重要的是,当需要不同显示方式(如显示整数/负数/小数)时,只需要将不同的函数指针赋值给NIXIE.Driver(如NIXIE.Driver=NIXIE_Driver_Uint;),就能使用同一个代码(NIXIE.Driver();)调用函数,还减少了使用switch语句的开销,增改也更加容易。

在未加入OS之前,该方法是可行的。但是当加入OS之后,该方法就失效了。通过单步调试发现,当运行NIXIE.Driver();之后,程序跑飞。

上网查询,发现这一原因很复杂。不过简单说来,就是因为LCALL指令之后只能跟addr16这样的立即数,也就是说硬件不支持函数指针。而一般情况下,KEIL通过换算,将指针换算为了地址。例如,NIXIE.Driver();实际上换算为了如:

LCALL   0510H

所以,当使用OS时,程序的顺序执行结构被打乱,所以当然不能使用函数指针了。



Linux最小内核移植

阅读数 2336

没有更多推荐了,返回首页