在51单片机上实现多线程操作系统

编译环境:keil5

芯片型号:STC89C52

目录

思路简介.......................................................................................................................... 2

一,MCU的中断现场保护................................................................................................ 2

二,读取和修改MCU的中断现场.................................................................................... 4

小实验:在MCU上实现两个线程之间的切换..................................................................... 4

遇到问题:不能为每个线程分配一个栈?........................................................................... 9

三,实现每个线程独立拥有一个栈................................................................................... 9

遇到问题:线程之间的局部变量相互冲突?...................................................................... 11

四,管理多个线程.......................................................................................................... 12

五,紧急事件处理.......................................................................................................... 14

六,最终关键代码........................................................................................................ 16

思路简介

实现一个多线程操作系统,前提是实现线程切换。毕竟,多线程的本质就是芯片不断的在不同的线程之间切换执行。通常的实现方法,是通过中断机制。中断时,MCU会保存现场,然后开始执行中断服务程序。我们在中断服务程序中,把所谓的“中断现场”替换掉。此后,当芯片返回到普通的函数执行时,实际上是返回到了替换掉的现场,而不再是中断前保护好的现场。这样,就成功的把一个执行中的线程,通过中断打断,切换到了另一个线程。

如下图所示,假设MCU本来在运行线程A,若此时发生了中断,就会把A的现场保护起来,转去执行中断服务函数。在中断函数返回时,如果没有人为干涉,理应返回到A的现场继续运行线程A。当然,A的现场是存储在一片内存空间的。如果在中断函数返回前,人为的把这片内存空间的内容,修改成B的现场。那么,中断函数返回时,便是返回到B的现场,而不再是A的现场。这样,就完成了线程A到线程B的切换。

综上所述,为了实现线程切换,需要:

  1. 搞清楚51单片机的中断现场保护过程。
  2. 知道如何在中断返回前修改现场。

一,MCU的中断现场保护

我们首先使用keil5 IDE新建一个工程,写一个简单的代码,只是包含main函数、while(1)循环、定时器0和它的中断服务函数。此时while(1)就看作是普通线程。我们可以看下图,得知普通线程的程序位置在0x0336.此时栈位置为0x20.寄存器的值也很清楚。

当进入中断服务程序时,我们可以发现,在真正执行中断服务程序的代码前,还执行了13句PUSH指令。这些指令很明显是把A,B,DPTR,PSW,R0~R7的值保存起来。现在的SP变成了0x2f,证明一共保存了(0x2f-0x20=0x0f)个字节的数据。  从内存查看窗口来看,0x23~0x2f的值都是之前的寄存器值,这是对应上的。但我们还发现,除了这13个值外,还额外保存了0x21~0x22这2个字节的值。比对后知道,0x0336刚好是之前的PC值。那这是不是由硬件自动保存的呢?(是)

我们继续观察,可以看到,中断函数返回前,又继续执行了一系列POP指令,刚好把之前PUSH的数据又复原了。当执行RETI之后,又回到了普通线程,此时PC又是0x0336,sp还是0x20。

经过以上实验,我们可以得到如下结论:

  • 中断发生时,执行中断服务函数前,会SP++,然后自动保存当前的PC值(2字节)。随后执行13个PUSH指令,保存A,B,DPTR,PSW,R0~R7.
  • 中断服务函数执行完毕,还需要先反序POP之前保存的13个寄存器,最后执行RETI指令弹出PC值。
  • 压栈过程是,SP++,数据入栈。出栈过程是,数据出栈,然后SP--。

可以制作一张表,表明执行中断函数第一句C语言代码前,寄存器值和地址的关系。

地址

寄存器值

SP

R7

SP-1

R6

SP-2

R5

SP-3

R4

SP-4

R3

SP-5

R2

SP-6

R1

SP-7

R0

SP-8

PSW

SP-9

DPL

SP-10

DPH

SP-11

B

SP-12

ACC

SP-13

PC_H

SP-14

PC_L

SP-15是进入中断前的SP值

但是要注意,这只是刚进入中断时的对应关系。如果在中断服务函数中又调用了函数,那在这个函数里面,还要考虑到函数调用时产生的压栈。

二,读取和修改MCU的中断现场

通过前面的研究,我们已经弄懂了51单片机中断的现场保护和恢复过程。我们现在要讨论的问题是,如何把这个现场读取和修改。

我们假定,每一个线程都是单独有一个”现场存储器”的,用来在中断发生时保存现场。

假设现在有两个线程:线程A和线程B,且线程A正处于运行状态。我们进入到中断服务函数后,应该先把现场读出来,存储在线程A的现场存储器里面。因为这是打断了线程A才进入的中断,那么这个现场理应交给A存储。然后,用线程B的现场存储器的内容覆盖掉中断现场。之后,中断返回才能返回到线程B。

小实验:MCU上实现两个线程之间的切换

我们现在可以具体写代码实现线程切换,先只考虑最简单的情况:系统中只有两个自定义的线程:线程A和线程B。切换次序是A->B->A->B…这样的来回切换。

首先定义一个线程管理块的结构体thread_block,包含了现场信息、SP初始化值和线程对应的函数实体。

在线程创建的时候,需要指定默认的PC值和SP值。这里要注意,51单片机的函数指针是16位宽的,所以使用unsigned int进行强制转换。

//线程创建:创建一个线程之后立即投入运行

void thread_create(thread_block* block,thread_pfun pfun,char* sp_init)

我们再定义线程A和线程B。

//线程A

void thread_a_fun(void* agrv)

{

while(1)

{

EA = 0;

Uart_puts("thread_a_fun\n");

EA = 1;

}

}

char thread_a_stack[10];

thread_block thread_a;

//线程B

void thread_b_fun(void* agrv)

{

while(1)

{

EA = 0;

Uart_puts("thread_b_fun\n");

EA = 1;

}

}

char thread_b_stack[10];

thread_block thread_b;

我们要注意的是,在没有创建线程A和线程B之前,运行的main函数本身也是一个线程。所以我们暂时定义一个标志位thread_change,代表当前运行的线程。

thread_change = 0表示当前运行进程是main

thread_change = 1表示当前运行进程是线程A

thread_change = 2 表示当前运行进程是线程B

MCU上电,实际上的线程切换次序是:main->线程A->线程B->线程A->线程B…这样循环执行。中断服务程序如下:

void Timer0_IRQ(void) interrupt 1

{

EA =0;

TL0 = (65536 - TIMER_RELOAD )%256;

TH0 = (65536 - TIMER_RELOAD )/256;

if(p_timer0_callback != 0)

{

p_timer0_callback();

}

EA = 1;

}

int thread_change = 0;

void timer_Handler(void)

{

char *sp =(char *)SP;

sp-=2; //减去函数调用自身的压栈

if(thread_change == 0)

{

//把线程A的现场写入中断现场

*(sp)   = thread_a.R7 ;

*(sp-1) = thread_a.R6 ;

*(sp-2) = thread_a.R5 ;

*(sp-3) = thread_a.R4 ;

*(sp-4) = thread_a.R3 ;

*(sp-5) = thread_a.R2 ;

*(sp-6) = thread_a.R1 ;

*(sp-7) = thread_a.R0 ;

*(sp-8) = thread_a.PSW ;

*(sp-9) = thread_a.DPL ;

*(sp-10)= thread_a.DPH ;

*(sp-11)= thread_a.B    ;

*(sp-12)= thread_a.ACC ;

*(sp-13)= thread_a.PC_H;

*(sp-14)= thread_a.PC_L;

thread_change =1;

}

else if(thread_change == 1)

{

//把中断现场保存到线程A

thread_a.R7     = *(sp);

thread_a.R6     = *(sp-1);

thread_a.R5     = *(sp-2);

thread_a.R4     = *(sp-3);

thread_a.R3     = *(sp-4);

thread_a.R2     = *(sp-5);

thread_a.R1     = *(sp-6);

thread_a.R0     = *(sp-7);

thread_a.PSW    = *(sp-8);

thread_a.DPL    = *(sp-9);

thread_a.DPH    = *(sp-10);

thread_a.B      = *(sp-11);

thread_a.ACC    = *(sp-12);

thread_a.PC_H   = *(sp-13);

thread_a.PC_L   = *(sp-14);

//把线程B的现场写入中断现场

*(sp)   = thread_b.R7 ;

*(sp-1) = thread_b.R6 ;

*(sp-2) = thread_b.R5 ;

*(sp-3) = thread_b.R4 ;

*(sp-4) = thread_b.R3 ;

*(sp-5) = thread_b.R2 ;

*(sp-6) = thread_b.R1 ;

*(sp-7) = thread_b.R0 ;

*(sp-8) = thread_b.PSW ;

*(sp-9) = thread_b.DPL ;

*(sp-10)= thread_b.DPH ;

*(sp-11)= thread_b.B    ;

*(sp-12)= thread_b.ACC ;

*(sp-13)= thread_b.PC_H;

*(sp-14)= thread_b.PC_L;

thread_change = 2;

}

else if(thread_change == 2)

{

//把中断现场保存到线程B

thread_b.R7     = *(sp);

thread_b.R6     = *(sp-1);

thread_b.R5     = *(sp-2);

thread_b.R4     = *(sp-3);

thread_b.R3     = *(sp-4);

thread_b.R2     = *(sp-5);

thread_b.R1     = *(sp-6);

thread_b.R0     = *(sp-7);

thread_b.PSW    = *(sp-8);

thread_b.DPL    = *(sp-9);

thread_b.DPH    = *(sp-10);

thread_b.B      = *(sp-11);

thread_b.ACC    = *(sp-12);

thread_b.PC_H   = *(sp-13);

thread_b.PC_L   = *(sp-14);

//把线程A的现场写入中断现场

*(sp)   = thread_a.R7 ;

*(sp-1) = thread_a.R6 ;

*(sp-2) = thread_a.R5 ;

*(sp-3) = thread_a.R4 ;

*(sp-4) = thread_a.R3 ;

*(sp-5) = thread_a.R2 ;

*(sp-6) = thread_a.R1 ;

*(sp-7) = thread_a.R0 ;

*(sp-8) = thread_a.PSW ;

*(sp-9) = thread_a.DPL ;

*(sp-10)= thread_a.DPH ;

*(sp-11)= thread_a.B    ;

*(sp-12)= thread_a.ACC ;

*(sp-13)= thread_a.PC_H;

*(sp-14)= thread_a.PC_L;

thread_change = 1;

}

}

虽然代码写的很笨,但含义是清晰的。在开始阶段,我们尽量做的简单。我们用串口测试一下效果。可以看到,线程A和线程B在切换执行。

遇到问题:不能为每个线程分配一个栈?

现在我们已经可以在线程A对应的函数和线程B对应的函数之间来回切换。但我们还没有考虑过栈的问题。之前说过,每个线程都各自拥有一个独立的栈。但是51单片机的中断返回指令RETI,却并没有考虑恢复SP。如果我们在中断服务程序中修改了SP,那么中断恢复现场时的POP指令便会弹出错误的数据。所以按照目前的做法,51单片机貌似是无法实现每个线程单独有一个栈的。不仅如此,在中断服务函数看来,所有的线程都是共用同一个SP,所以在每个线程执行时,必须保证切出线程时的SP值,和切入时的一致。为了这一点,必须在线程对应的函数体中加入关中断和开中断,避免SP被修改时遭到线程切换。下图讲述了目前的线程切换原理。图中,SP-X表示属于在运行X时的栈顶指针。

三,实现每个线程独立拥有一个栈

我们之前讲到,由于51单片机只有一个SP寄存器,所以无法为线程设置栈的同时,正确的恢复线程现场。

面对这个问题,我们可以这样解决:假设现在要切换到线程B,我们不把这个线程B的现场写入到中断时保存现场的内存空间,而是先让SP指向线程B的独立栈,然后把现场写入到独立栈指向的内存空间。这样,当中断现场恢复时,保证了现场恢复的正确性。出栈完毕,又刚好让SP指向了新线程的栈顶。

这种方法随时会借用线程栈来存储现场。这意味着,每个线程栈必须时刻保持着有一定的余量,以支持随时可能发生的线程切换。

按照这种思路,我们修改一下中断服务回调函数的原型,让它能够返回ret_sp值。这个ret_sp便是现场所在的新的内存空间。此时p_timer0_callback指向的函数实际上是char thread_change(void)。函数执行之后,返回my_sp。然后在中断服务函数中使用它修改SP。

void timer0_irq() interrupt 1

{

EA = 0;

TL0 = (65536 - TIMER_RELOAD )%256;

TH0 = (65536 - TIMER_RELOAD )/256;

if(p_timer0_callback != 0)

{

SP = p_timer0_callback();

}

EA = 1;

}

此时,我们可以看到,执行正常。调用putchar是需要使用栈的。所以可以证明,线程中已经可以使用栈了。

到此为止,线程切换的思路就作了一些改进,如下图所示。

创建线程是比较占用内存的。主要有两处:

1,线程的信息块,内部包含着现场信息。

2,线程的独立栈,需要预留一个现场的内存空间。

遇到问题:线程之间的局部变量相互冲突?

在测试系统性能的时候,还发现了一个问题。就是在线程A的函数体中定义的变量,会在线程B中被改写,具体现象如下图所示。这是怎么回事?我们不是已经为每个线程分配了独立的栈吗?

经过反汇编调试工具发现,编译器根本没有为x分配栈空间。它是使用的一个绝对地址0x44,来保存的。也就是对于51编译环境下的C语言,有时局部变量根本不用栈。

这是因为51单片机的内存资源非常有限,Keil IDE环境对代码进行编译时采用了一种叫”覆盖技术”的优化手段。这个x的内存空间是时分复用的,会被别的线程修改。总之,用全局变量比较妥当。

四,管理多个线程

为了方便,可以定义一个线性表来存放所有thread_block的地址,便于管理所有线程。为线程提供两种状态:正常normal和挂起suspend。可用函数thread_normal和thread_suspend设置线程状态。

这个线性表具有如下功能:

  1. 在thread_create函数被调用时,把新线程的地址加入到线性表中。以下是线程创建函数。

void thread_create(thread_block* block,thread_pfun pfun,char* sp_init,char *name)

{

block->PC_L =  (unsigned int)pfun%256 ;

block->PC_H =  (unsigned int)pfun/256 ;

block->sp = sp_init;

block->status = normal;

block->name = name;

g_thread_manage_list.list[g_thread_manage_list.list_len] = block;

g_thread_manage_list.list_len++;

}

  1. 线程调度时,可以从线性表中找到下一个处于normal状态的线程,作为即将运行的线程。以下是线程调度函数。

thread_block* thread_schedule(void)

{

static int next_thread = 0;

do

{

next_thread++;

if(next_thread >= g_thread_manage_list.list_len)

{

next_thread = 0;

}

}

while(g_thread_manage_list.list[next_thread]->status != normal );

return g_thread_manage_list.list[next_thread];

}

测试结果:可以看到,线程A、B、C是被切换着执行的。

五,紧急事件处理

一个实时操作系统,最明显的特点就是实时性。从实际生活的角度来讲,它就是指在发生紧急事件时,系统能无条件的切换到处理紧急事件的线程来运行,而不管当时正在做什么。比如,你正在用手机玩游戏或者听歌,但是当接到电话时,手机是强制性的进行来电显示,这可以视为一种紧急事件的处理。

之前说到,我们设计的操作系统,是把多个线程用线性表组织起来,然后按次序轮番调度。假如有线程A,B,C。正常的调度流程是线程A->线程B->线程C->线程A->线程B->线程C…。假设系统正在运行线程C,此时发生了一个紧急事件,需要立即处理,且线程B负责处理该事件。难道就只能坐着等系统调度完线程C、线程A之后,才能调度线程B来处理该事件吗?如果系统中存在N个线程,那么在最坏的情况下,紧急事件是否要等到N-1个线程调度完毕,才能被响应?

为了解决这个问题,需要给我们的系统设计一个功能,就是除了在正常的轮番调度机制之外,还要加一个随机调度。即:允许任意指定一个线程,在任意时刻调用它,而不论系统当前正在做什么。

我们可以查看thread_change函数,它的功能主要是把当前现场保存在g_thread_manage_list.thread_cur指向的线程中,而切换到g_thread_manage_list.thread_next指向的线程运行。然后会由thread_schedule函数生成下一次要调度的线程,赋值给g_thread_manage_list.thread_next。我们想要系统调度任意一个线程,只需要人为的给g_thread_manage_list.thread_next赋值即可,即:

void thread_set_next_thread(thread_block* block)

{

if(block != 0)

{

g_thread_manage_list.thread_next = block;

}

}

执行了thread_set_next_thread函数之后,系统便暂时性的不管轮番调度的规则,而在下一次线程切换时,切换到该函数指定的线程去运行。执行完指定的线程后,才会继续遵循轮番调度的规则。

还是以之前的例子,当系统正在运行线程C时,发生了紧急事件。MCU检测到紧急事件,然后执行了thread_set_next_thread(线程B),那么调度流程进行如下改变。

原来的:

线程A->线程B->线程C->线程A->线程B->线程C…

改变为:

线程A->线程B->线程C->线程B->线程B->线程C…

这就相当于,负责处理紧急事件的线程B,抢占了一次线程A的运行机会。

此时,我们实现了任意指定一个线程执行,但还不一定是任意时刻。因为就算指定了下一次切换到线程B,但是要等到下一次做线程切换,也需要不少时间。假设系统配置是每隔10ms进行一次线程切换,那么最差有可能要等10ms,才会执行线程B。所以在这种紧急事件中,我们不能等待系统自发的做线程切换,而是要手动的立马触发一个线程切换。在此次的实验中,是使用51单片机的定时器0进行中断触发的。所以我们可以故意置位定时器0的溢出标志位TF0,而实现立即进行上下文切换的效果。

#define TIMER_INT()                    TF0 = 1

为了测验功能,我们配置了MCU的外部中断0,在对应的引脚连接一个按钮,按下按钮触发中断。这个中断可以视为紧急事件。而在外部中断0的服务函数中,主要是强制切换到线程B。关键代码如下:

……

printf("\nSet thread b\n");

thread_set_next_thread(&thread_b);

TIMER_INT();

……

为了方便观察,把线程B的功能修改为打印字符’b’。运行结果如下图所示。我们可以发现,不论系统当前运行在哪个线程,只要按下了按钮,就会触发任意时间的线程切换,然后强制转到了打印’b’字符的线程B。之后又继续执行轮番调度。

六,最终关键代码

1,Thread.c

#include "Thread.h"

thread_manage_list idata  g_thread_manage_list = {{0},0,0,0};

void thread_create(thread_block* block,thread_pfun pfun,char* sp_init,char *name)

{

block->PC_L =  (unsigned int)pfun%256 ;

block->PC_H =  (unsigned int)pfun/256 ;

block->sp = sp_init;

block->status = normal;

block->name = name;

g_thread_manage_list.list[g_thread_manage_list.list_len] = block;

g_thread_manage_list.list_len++;

}

char thread_change(char sp)

{

char *my_sp =(char *)sp;

thread_block* p_th_block = 0;

p_th_block = g_thread_manage_list.thread_cur;

if(p_th_block != 0)

{

p_th_block->sp   = (char)(my_sp - THREAD_CONTEXT_SIZE);

p_th_block->R7          = *(my_sp);

p_th_block->R6          = *(my_sp-1);

p_th_block->R5          = *(my_sp-2);

p_th_block->R4          = *(my_sp-3);

p_th_block->R3          = *(my_sp-4);

p_th_block->R2          = *(my_sp-5);

p_th_block->R1          = *(my_sp-6);

p_th_block->R0          = *(my_sp-7);

p_th_block->PSW = *(my_sp-8);

p_th_block->DPL = *(my_sp-9);

p_th_block->DPH = *(my_sp-10);

p_th_block->B   = *(my_sp-11);

p_th_block->ACC = *(my_sp-12);

p_th_block->PC_H= *(my_sp-13);

p_th_block->PC_L= *(my_sp-14);

}

p_th_block = g_thread_manage_list.thread_next;

my_sp          = (char *)(p_th_block->sp + THREAD_CONTEXT_SIZE);

*(my_sp)   = p_th_block->R7 ;

*(my_sp-1) = p_th_block->R6 ;

*(my_sp-2) = p_th_block->R5 ;

*(my_sp-3) = p_th_block->R4 ;

*(my_sp-4) = p_th_block->R3 ;

*(my_sp-5) = p_th_block->R2 ;

*(my_sp-6) = p_th_block->R1 ;

*(my_sp-7) = p_th_block->R0 ;

*(my_sp-8) = p_th_block->PSW ;

*(my_sp-9) = p_th_block->DPL ;

*(my_sp-10)= p_th_block->DPH ;

*(my_sp-11)= p_th_block->B ;

*(my_sp-12)= p_th_block->ACC ;

*(my_sp-13)= p_th_block->PC_H;

*(my_sp-14)= p_th_block->PC_L;

g_thread_manage_list.thread_cur = g_thread_manage_list.thread_next;

g_thread_manage_list.thread_next = thread_schedule();

return (char)my_sp;

}

2,Thread.h

#ifndef _THREAD_H_

#define _THREAD_H_

#define THREAD_LIST_LEN                                            5

#define THREAD_CONTEXT_SIZE                                 15

typedef void(*thread_pfun)(void);

typedef enum thread_status {normal,suspend} enum_thread_status;

typedef struct thread_block

{

char PC_L;

char PC_H;

char ACC;

char B;

char DPH;

char DPL;

char PSW;

char R0;

char R1;

char R2;

char R3;

char R4;

char R5;

char R6;

char R7;

char sp;

enum_thread_status status;

char* name;

}thread_block;

typedef struct thread_manage_list

{

int                                                   list_len;

thread_block*     list[THREAD_LIST_LEN];

thread_block*   thread_cur;

thread_block*   thread_next;

}thread_manage_list;

void thread_create(thread_block* block,thread_pfun pfun,char* sp_init,char *name);

void thread_normal(thread_block* block);

void thread_suspend(thread_block* block);

void thread_start(void);

char thread_change(char sp);

void thread_set_next_thread(thread_block* block);

#endif

3,定时器0中断服务函数

...

SP = p_timer0_callback(SP); //使用多线程操作系统

...

4,main.c

#include <reg52.h>

#include "UART.h"

#include "timer.h"

#include "Thread.h"

#include "exti_interrupt.h"

//线程A

char  idata thread_a_stack[30];

thread_block  thread_a;

void thread_a_fun()

{

static int test_data = 0;

while(1)

{

if(test_data >= 10)

{

test_data = 0;

}

putchar(test_data + '0');

test_data++;

}

}

//线程B

char idata thread_b_stack[30];

thread_block   thread_b;

void thread_b_fun()

{

static int test_data = 'a';

while(1)

{

if(test_data > 'z')

{

test_data = 'a';

}

putchar('b');

test_data++;

}

}

//线程C

char idata thread_c_stack[30];

thread_block   thread_c;

void thread_c_fun()

{

static int test_data = 'A';

while(1)

{

if(test_data > 'Z')

{

test_data = 'A';

}

putchar(test_data);

test_data++;

}

}

void EXTI0_int_Handler()

{

putchar('\n');

thread_set_next_thread(&thread_b);

TIMER_INT();

}

void  main()

{

Uart_init();

putchar('$');

thread_create(&thread_a,thread_a_fun,thread_a_stack,"thread_a");

thread_create(&thread_b,thread_b_fun,thread_b_stack,"thread_b");

thread_create(&thread_c,thread_c_fun,thread_c_stack,"thread_c");

Timer0_set_callback(thread_change);

Timer0_Init();

exti0_interrupt_set_callback(EXTI0_int_Handler);

exti_interrupt_init();

thread_start();

while(1)

{

}

}

5,工程所有源码和可执行的仿真等资料。

基于51单片机的多线程操作系统设计相关推荐

  1. 步进电机的计算机控制系统设计,基于.51单片机的步进电机控制系统设计.doc

    基于51单片机的步进电机控制系统设计 中文摘要 步进电机是一种受,并且能将相应的或者的电动机.由于步进电机具有步距误差不积累.运行可靠.结构简单.惯性小.成本低等优点,因此,被广泛使用于计算机外围电路 ...

  2. 基于51单片机的语音采集系统设计(录音笔选择方案)

    功能: 基于51单片机的语音实时采集系统 系统由STC89C52单片机+ISD4004录音芯片+LM386功放模块+小喇叭+LCD1602+按键+指示灯+电源构成 具体实现功能: (1)可通过按键随时 ...

  3. 基于51单片机的智能晾衣架系统设计

    1.功能介绍 设计基于51单片机的智能晾衣架.主要功能如下: (1)控制晾衣架的升降. (2)具有限位开关功能. (3)具有无线遥控功能. (4)能够指示运行状态. (5)具有智能模式,可以根据环境光 ...

  4. 单片机c语言 王东锋,基于51单片机的输液报警系统设计

    高扬 摘 要:本设计是以AT89C51单片机为核心,利用光电传感器来检测设备液体的流动情况.当吊瓶内有点滴落下时,可通过红外检测装 1前言 随着信息技术和计算机技术的发展,信息革命在工业.医疗和控制领 ...

  5. 28、基于51单片机空气智能加湿器系统设计

    摘要 湿度的测量应用范围是很广的,对湿度测量系统的研究也具有深远意义,本课题针对国内外对湿度测量系统的研究与发展状况,分析了目前湿度测量系统存在的主要问题,设计了一种基于单片机的湿度测量系统,对某些有 ...

  6. 基于51单片机的电压检测系统设计(#0412)

    电压.电流.功率是表征电信号能量大小的三个基本参量.在电子电路中,只要测量出其中一个参量就可以根据电路的阻抗求出其它二个参量.考虑到测量的方便性.安全性.准确性等因素,几乎都用测量电压的方法来测定表征 ...

  7. 3、基于51单片机语音识别控制三路开关系统设计

    毕设帮助.开题指导.技术解答(有偿)见文末. 目录 摘要 一.硬件方案 二.设计功能 三.实物图 四.原理图 五.PCB图 六.Proteus仿真 七.程序源码 八.资料包括 摘要 语音识别是解决机器 ...

  8. 基于51单片机模拟乒乓球游戏机系统设计

    毕设帮助.开题指导.技术解答(有偿)见文末. 目录 摘要 一.硬件方案 二.设计功能 三.实物图 四.原理图 五.PCB图 六.Proteus仿真 七.程序源码 八.资料包括 摘要 乒乓球游戏电路是一 ...

  9. 58、基于51单片机ADXL345计步器 卡路里系统设计

    毕设帮助.开题指导.技术解答(有偿)见文末. 目录 摘要 一.硬件方案 二.设计功能 三.实物图 四.原理图 五.PCB图 六.keil4开发软件-程序 七.资料包括 摘要 计步器是一种颇受欢迎的日常 ...

  10. c语言单片机停车场收费系统,基于51单片机停车场车位引导系统设计

    ?周明彬 曾伊玲 摘要:在很多人流量大的地方,因为车辆集中的情况,所以每次经过停车场时都需要工作人员来指挥车辆,告诉车主停车场那些地方还有空余车位可以泊车.所以很多地方的旧停车场使用的管理方式,是十分 ...

最新文章

  1. asp.net控件开发基础(21)
  2. git submoule 更新_微软Surface Duo双屏手机键盘更新:支持分体式输入
  3. boost::callable_traits的is_reference_member的测试程序
  4. linux so文件统一目录,linux加载指定目录的so文件
  5. t-sql查询where in_产品操作MySQL第7篇 – 运算符 - IN
  6. php对象不公用属性赋值,php 框架 Model 公用的问题
  7. layui.open 关闭之后触发_JAVA虚拟机关闭钩子(Shutdown Hook)
  8. 快速排序算法--两个小人扔萝卜
  9. JeecgBoot框架学习
  10. 8086CPU的寻址方式
  11. 【Unity】由Unity资源的相对路径获取资源的AssetDatabase路径
  12. Excel序号删除某行之后不连贯?这样做可以智能更新表格序号!
  13. 明星热图|朱一龙环保主题大片出炉;李现为您开启新一年“红运”时刻;杨采钰成林清轩产品代言人...
  14. c语言知道ascII码求字母,c语言的ascii代码
  15. 沃顿商学院自我管理课——埃里克.格雷腾斯
  16. srs 直播连麦环境搭建
  17. SAP 薪酬计算流程
  18. 电信云服务器重装系统,天翼云主机操作(二)
  19. “点击欺诈”恶意软件藏有更大的威胁?
  20. 地产“破壁人” | 一点财经

热门文章

  1. 单片机基础项目(上)
  2. chromecast 协议_Chromecast和Android TV有什么区别?
  3. 全面解读“资金二清”与“信息二清”
  4. 10天java基础学习笔记五
  5. @Aspect不生效
  6. 2019 NLP大全:论文、博客、教程、工程进展全梳理(长文预警)
  7. c语言学生班级通讯录,C语言做学生通讯录
  8. 笔记本电脑 用 VGA 线 外接显示器 频闪
  9. xy轴坐标图数字表示_求坐标x轴、y轴公式-x轴y轴-数学-潘遮驴同学
  10. Codevs 1183 泥泞的道路