基于51单片机的多线程操作系统设计
在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的切换。
综上所述,为了实现线程切换,需要:
- 搞清楚51单片机的中断现场保护过程。
- 知道如何在中断返回前修改现场。
一,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设置线程状态。
这个线性表具有如下功能:
- 在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++;
}
- 线程调度时,可以从线性表中找到下一个处于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单片机的多线程操作系统设计相关推荐
- 步进电机的计算机控制系统设计,基于.51单片机的步进电机控制系统设计.doc
基于51单片机的步进电机控制系统设计 中文摘要 步进电机是一种受,并且能将相应的或者的电动机.由于步进电机具有步距误差不积累.运行可靠.结构简单.惯性小.成本低等优点,因此,被广泛使用于计算机外围电路 ...
- 基于51单片机的语音采集系统设计(录音笔选择方案)
功能: 基于51单片机的语音实时采集系统 系统由STC89C52单片机+ISD4004录音芯片+LM386功放模块+小喇叭+LCD1602+按键+指示灯+电源构成 具体实现功能: (1)可通过按键随时 ...
- 基于51单片机的智能晾衣架系统设计
1.功能介绍 设计基于51单片机的智能晾衣架.主要功能如下: (1)控制晾衣架的升降. (2)具有限位开关功能. (3)具有无线遥控功能. (4)能够指示运行状态. (5)具有智能模式,可以根据环境光 ...
- 单片机c语言 王东锋,基于51单片机的输液报警系统设计
高扬 摘 要:本设计是以AT89C51单片机为核心,利用光电传感器来检测设备液体的流动情况.当吊瓶内有点滴落下时,可通过红外检测装 1前言 随着信息技术和计算机技术的发展,信息革命在工业.医疗和控制领 ...
- 28、基于51单片机空气智能加湿器系统设计
摘要 湿度的测量应用范围是很广的,对湿度测量系统的研究也具有深远意义,本课题针对国内外对湿度测量系统的研究与发展状况,分析了目前湿度测量系统存在的主要问题,设计了一种基于单片机的湿度测量系统,对某些有 ...
- 基于51单片机的电压检测系统设计(#0412)
电压.电流.功率是表征电信号能量大小的三个基本参量.在电子电路中,只要测量出其中一个参量就可以根据电路的阻抗求出其它二个参量.考虑到测量的方便性.安全性.准确性等因素,几乎都用测量电压的方法来测定表征 ...
- 3、基于51单片机语音识别控制三路开关系统设计
毕设帮助.开题指导.技术解答(有偿)见文末. 目录 摘要 一.硬件方案 二.设计功能 三.实物图 四.原理图 五.PCB图 六.Proteus仿真 七.程序源码 八.资料包括 摘要 语音识别是解决机器 ...
- 基于51单片机模拟乒乓球游戏机系统设计
毕设帮助.开题指导.技术解答(有偿)见文末. 目录 摘要 一.硬件方案 二.设计功能 三.实物图 四.原理图 五.PCB图 六.Proteus仿真 七.程序源码 八.资料包括 摘要 乒乓球游戏电路是一 ...
- 58、基于51单片机ADXL345计步器 卡路里系统设计
毕设帮助.开题指导.技术解答(有偿)见文末. 目录 摘要 一.硬件方案 二.设计功能 三.实物图 四.原理图 五.PCB图 六.keil4开发软件-程序 七.资料包括 摘要 计步器是一种颇受欢迎的日常 ...
- c语言单片机停车场收费系统,基于51单片机停车场车位引导系统设计
?周明彬 曾伊玲 摘要:在很多人流量大的地方,因为车辆集中的情况,所以每次经过停车场时都需要工作人员来指挥车辆,告诉车主停车场那些地方还有空余车位可以泊车.所以很多地方的旧停车场使用的管理方式,是十分 ...
最新文章
- asp.net控件开发基础(21)
- git submoule 更新_微软Surface Duo双屏手机键盘更新:支持分体式输入
- boost::callable_traits的is_reference_member的测试程序
- linux so文件统一目录,linux加载指定目录的so文件
- t-sql查询where in_产品操作MySQL第7篇 – 运算符 - IN
- php对象不公用属性赋值,php 框架 Model 公用的问题
- layui.open 关闭之后触发_JAVA虚拟机关闭钩子(Shutdown Hook)
- 快速排序算法--两个小人扔萝卜
- JeecgBoot框架学习
- 8086CPU的寻址方式
- 【Unity】由Unity资源的相对路径获取资源的AssetDatabase路径
- Excel序号删除某行之后不连贯?这样做可以智能更新表格序号!
- 明星热图|朱一龙环保主题大片出炉;李现为您开启新一年“红运”时刻;杨采钰成林清轩产品代言人...
- c语言知道ascII码求字母,c语言的ascii代码
- 沃顿商学院自我管理课——埃里克.格雷腾斯
- srs 直播连麦环境搭建
- SAP 薪酬计算流程
- 电信云服务器重装系统,天翼云主机操作(二)
- “点击欺诈”恶意软件藏有更大的威胁?
- 地产“破壁人” | 一点财经