单片机

每项新应用设计都需要一个单片机或微处理器。当在两者之间选择其一时,需要考虑一些因素。以下是微处理器、单片机的概述和对比。

考虑选择微处理器(MPU)或者单片机(MCU)时,应用类型通常是关键因素。

另一方面,最终选择取决于诸如操作系统和内存之类的因素。不过,有时可以将微处理器和单片机内核结合使用,这称作异构架构。

1、操作系统

对于一些基于Linux或安卓等操作系统的计算机密集型工业和消费类应用,需要大量高速连接或功能范围广泛的用户接口,微处理器就是最佳选择。

这是因为大多数单片机都没有操作系统,而只有裸机程序,借助于顺序处理循环和状态机,几乎无需任何人工干预即可运行程序。

然而,许多高性能单片机可以支持诸如FreeRTOS之类的实时操作系统(RTOS),从而以确定性方式实时响应需要硬实时行为的应用程序。

作为具有许多免费软件、广泛硬件支持和不断发展的生态系统的通用操作系统,嵌入式Linux取得了巨大的成功。

它的另一个优点就是没有用户或授权许可费用。不过,与嵌入式Linux一起运行的应用程序至少需要300至400 DMIPS(ARM-Dhrystone MIPS)性能,因此较适合使用微处理器。单片机没有足够的计算能力和内存来应付此类应用。

如果是用于复杂或对实时性要求高的控制系统, RTOS则很有用,但至少要配合50 DMIPS的高性能单片机。这比嵌入式Linux所需的性能要求要少得多。传统的RTOS设计精简,因此可以在单片机上运行。

针对实时计算硬件时,这是合理的,例如用于车辆的防抱死系统,若响应时间过长会带来致命的后果。即使必须支持大量的功能、中断源和标准通信接口,也建议使用带有RTOS的单片机。

2、内存

微处理器与单片机之间的另一个主要区别是,微处理器依赖外部存储器来保存和执行程序,而单片机则依赖嵌入式闪存。

在微处理器中,程序通常存储在非易失性存储器中,例如eMMC或串行闪存。在启动过程中,将其加载到外部DRAM中并在此执行启动程序。DRAM和非易失性存储器都可以具有几百兆甚至几千兆字节容量,这意味着微处理器几乎从来不受存储容量限制。

但有一个潜在缺点:外部存储器或许会使得PCB布局的设计变得更加复杂。

即使是当前的高性能单片机,例如由意法半导体(STMicroelectronics)生产的STM32H7,最多也仅提供2 MB程序内存,对于许多需要操作系统的应用而言可能不足。由于程序位于片上内存中,因此其优点是执行启动和重置过程的速度明显更快。

3、计算能力

计算能力是典型的选择因素。不过,在这方面,微处理机与单片机之间的界线变得模糊了。例如,如果你将ARM体系结构视为单片机和微处理器市场中分布最广泛的体系结构之一,这就变得显而易见了。ARM提供了不同的处理器体系结构以满足各种要求:

Cortex-A提供了最高性能,并且已经针对综合操作系统进行了优化。它们主要部署在功能强大的设备中,比如智能手机或服务器。
Cortex-M较小,具有更多的片上外设,但是能耗较低,并且针对嵌入式应用进行了优化。

Dhrystone是比较不同处理器性能的测试基准。根据该基准,普通平价单片机具有30 DMIPS,而当前性能最高的单片机(包括嵌入式程序闪存)与这些平价单片机的差距高达1027 DMIPS。相比之下,微处理器的起步点约为1000 DMIPS。

4、能耗

单片机在能耗方面表现出色,要比微处理器低很多。尽管微处理器具有节能模式,但其能耗仍然比典型的单片机高得多。

而且,微处理器使用外部存储器,因此较难切换到节能模式。对于需要较长的电池运行时间,并且很少使用或没有用户接口的超低功耗应用,单片机是更好的选择,尤其是对于消费类电子产品或智能电表来说。

5、连接性

大多数单片机和微处理器都配备了所有常规外围设备接口。但是,如果用户需要的是超高速外围设备,在单片机里是找不到例如千兆以太网这种相关接口的。尽管这实际上已成为微处理器中的标准功能单片机。这是十分合理的,因为单片机几乎无法处理这些高速接口所产生的数据量。一个关键问题是:是否有足够的带宽和通道来处理爆发的数据量?

6、实时表现

当实时性能是最重要的考虑因素时,单片机绝对是首选。凭借处理器内核、嵌入式闪存和软件(RTOS或裸机OS),单片机可以出色地完成实时任务。

因为Cortex-A微处理器使用高性能的流水线,用户可以看到在跳转和中断期间,随着流水线的深度不断增加,延迟时间也随之升高。由于OS与微处理器一起执行多任务,因此很难实现硬实时操作。

7、系统基础IC

由于电源已经集成在单片机中,因此它们仅需要一个单电平电源。另一方面,微处理器需要许多不同电压的电源来为内核和其它组件供电,所以通常需要一个特殊配置的电源管理IC(即所谓的系统基础芯片)来进行供电管理。

8、总结

很难说微处理器或单片机哪个才是更好的选择,但经验法则是,你应该始终权衡各种利弊条件。以下几点可以用作大致指导:

单片机非常适合以能耗为主要关注点,且价格较低的移动应用以及具有实时需求的应用。
微处理器则非常适合与操作系统一起运行并需要高速接口的密集计算应用。游戏和其他图形密集型应用使用特殊的微处理器进行联网处理。

免责声明:本文内容来源于网络,版权归原作者所有。如涉及作品版权问题,请联系删除。

围观 24

从单片机上知道,在上电的那一刻,MCU的程序指针会被初始化为上电复位时的地址,从那个地址处读取将要执行的指令,由此程序在MCU上开始执行(当然在调用程序的 main之前,还有一系列其他的初始化要做,如堆栈的初始化,不过这些我们很少会去修改)。PC在上电时,和MCU差不多,不过读取的是BIOS,有它完成了很多初始化操作,最后,调用系统的初始化函数,将控制权交给了操作系统,于是我们看到了Windows,Linux系统启动了。

如果将操作系统看作是在处理器上跑的一个很大的裸机程序(就是直接在硬件上跑的程序,因为操作系统就是直接跑在CPU上的,这样看待是可以的,不过这个裸机程序功能很多很强大),那么操作系统的启动很像MCU程序的启动。前者有一个很大的初始化程序完成很复杂的初始化,后者有一段不长的汇编代码完成一些简单的初始化。这一点看,它们在流程上是很相似的。

如果是系统上的程序启动呢?它们是由系统来决定的。Linux上在shell下输入./p后,首先检查是否是一个内建的shell命令;如果不是,则shell假设它是一个可执行文件(Linux上一般是elf格式),然后调用一些相关的函数,将在硬盘上的p文件的内容拷贝到内存(DDR RAM)中,并建立一个它的运行环境(当然这里边还有内存映射、虚拟内存、连接与加载等一些其它东西),准备执行。

由以上可知,单片机上的程序和平时在系统上运行的程序,在启动时差异是很大的(如果将程序调用main以前的动作,都抽象为初始化的话,程序的启动可以简化为:建立运行环境+调用main函数,这样程序的执行差异是不大的)。因为单片机上跑的程序(裸机程序)是和操作系统一样跑在硬件上的,它们属于一个层次的。过去之所以没有区分出单片机上的程序和PC机上的程序的一些差异,就是没有弄明白这一点。

由此,以前的一些疑惑也就解开了。为什么在单片机上的程序不怎么使用malloc,而PC上经常使用?因为单片机上没有已经写好的内存管理算法的代码,而在PC上操作系统里运行的程序,libc已经把这些都做了,只需要调用就可以了。如果在单片机上想用动态内存,也可以,但是这些代码要自己去实现,并定义一个相应的malloc,有时候一些公司会给提供一些库函数可能会实现malloc,但是因为单片机上RAM内存十分有限,如果不知道它的运行方式,估计会很危险。同样,因为在PC的系统上运行的程序与裸机程序不同,裸机程序不会有动态链接,有的只是静态链接。

关于程序在执行时,从哪里读取指令,哪里读取数据,也曾因为没有弄清楚系统上的程序和裸机程序之间的区别,而疑惑了很久。虽然在《微型计算机原理》课上知道程序运行时,从内存中读取指令和数据进行执行和回写。但是单片机上只有几K的RAM,而Flash一般有几十K甚至1M,这个时候指令和数据都在内存中吗?(这里指的内存仅指RAM,因为PC上我们常说的内存就是DDR RAM memory,先入为主以至于认为单片机上也是这样,还没有明白其实RAM和Flash都是内存)?这不可能,因为课上老师只说内存,但是PC上内存一般就是DDR RAM,不会是硬盘,硬盘是保存数据的地方;由此类比时,自己把自己弄晕菜了,单片机的RAM对应于DDR RAM,那Flash是不是就对应于硬盘了呢?在CSAPP上明白了,PC上之所以都在DDR RAM上,是速度的因素。硬盘的速度太慢,即使是即将到来的SSD比起DDRRAM,还是差着几个数量级,所以拷贝到DDRRAM中。这时,一个程序的代码和数据是连续存放的,其中代码段是只读区域,数据段是可读写区域(这是由操作系统的内存管理机制决定的)。运行时,再将它们拷贝到速度更快的SRAM中,以得到更快的执行速度。

而对于单片机而言,工作频率也就几M,几十M,从Flash中与从RAM中读的差异可能并不明显,不会成为程序执行的瓶颈(而对于PC而言,Flash的速度太慢,DDR RAM的速度也是很慢,即使是SRAM也是慢了不少,于是再提高工作频率也提高不了程序的执行速度,所以一个瓶颈出现了。为了提高CPU的使用率,换个角度想一下,既然不能减少一段程序的执行时间,就在同样的时间执行更多的程序,一个核执行一段程序,两个核就可以执行两段程序,于是多核CPU成为了现在的主流)。所以裸机程序指令就在Flash(Flash memory)中存放,而数据就放在了RAM中(Flash的写入次数有限制,同时它的速度和RAM还是差很多)。更广泛说,在单片机上RAM存放data段、bss段、堆栈段;ROM(EPROM、EEPROM、Flash等非易失性存储设备)存放代码、只读数据段。本质上说,这和PC上程序都在RAM中存放是一样的,PC 上是操作系统规定了可读与可写,而单片机上是依靠不同的存储设备区分了可读与可写(当然现在的Flash是可读写的,如果Flash没有写入次数限制,速度又可以和RAM相差不多,单片机上是不是只要Flash就可以了呢(直接相当于PC上的DDRRAM)?这样成本也会比一个RAM,一个Flash低,更节省成本,对于生产商更划算)。

对于单片机程序执行时指令和数据的存放与读取,理解如下:

对单片机编程后,程序的代码段、data段、bss段、rodata段等都存放在Flash中。当单片机上电后,初始化汇编代码将data段、bss段复制到RAM中,并建立好堆栈,开始调用程序的main函数。以后,便有了程序存储器和数据存储器之分,运行时从Flash(即指令存储器,代码存储器)中读取指令 ,从RAM中读取与写入数据。RAM存在的意义就在于速度更快。

无论是单片机也好,PC也罢,存在的存储器金字塔都是一致的,速度的因素、成本的限制导致了一级级更快的存储器的更快速度与更高的成本。应该说,对于程序执行的理解,就是存储器金字塔的理解。

注:

那么,什么是RAM,ROM和Flash呢?尽管他们都是计算机内存的一种形式,但是RAM,ROM,FLASH它们三个都以各自的方式和他们存储的数据进行交互。下面对每种内存有一个简短的说明。

RAM:表示随机访问内存(random access memory):微处理器可以读写访问的内存。当我们创建一些东西时,它是在内存中完成的。RAM是内存,反之亦然。

ROM:表示只读内存:微处理器可以读ROM,但是不能写入或修改。ROM是永久性的。ROM芯片经常保存一些重要且永不改变的特殊计算机指令。无论何时,微处理器都可以访问到存储在ROM上的信息。因为这些指令不可被擦出,所以他们保存在ROM中。

Flash Memory:是一种兼具RAM和ROM二者性质的特殊内存。我们可以像操作RAM一样,向Flash 内存写入数据;但是它又像ROM一样,数据在掉电时不丢失。悲剧的是,Flash 内存没有RAM那么快,所以任何时候都不要指望它能取代标准的计算机内存。

来源:网络转载

围观 98

每项新应用设计都需要一个单片机或微处理器。当在两者之间选择其一时,需要考虑一些因素。以下是微处理器、单片机的概述和对比。

考虑选择微处理器(MPU)或者单片机(MCU)时,应用类型通常是关键因素。另一方面,最终选择取决于诸如操作系统和内存之类的因素。不过,有时可以将微处理器和单片机内核结合使用,这称作异构架构。

操作系统

对于一些基于Linux或安卓等操作系统的计算机密集型工业和消费类应用,需要大量高速连接或功能范围广泛的用户接口,微处理器就是最佳选择。这是因为大多数单片机都没有操作系统,而只有裸机程序,借助于顺序处理循环和状态机,几乎无需任何人工干预即可运行程序。然而,许多高性能单片机可以支持诸如FreeRTOS之类的实时操作系统(RTOS),从而以确定性方式实时响应需要硬实时行为的应用程序。

作为具有许多免费软件、广泛硬件支持和不断发展的生态系统的通用操作系统,嵌入式Linux取得了巨大的成功。它的另一个优点就是没有用户或授权许可费用。不过,与嵌入式Linux一起运行的应用程序至少需要300至400 DMIPS(ARM-Dhrystone MIPS)性能,因此较适合使用微处理器。单片机没有足够的计算能力和内存来应付此类应用。

如果是用于复杂或对实时性要求高的控制系统, RTOS则很有用,但至少要配合50 DMIPS的高性能单片机。这比嵌入式Linux所需的性能要求要少得多。传统的RTOS设计精简,因此可以在单片机上运行。针对实时计算硬件时,这是合理的,例如用于车辆的防抱死系统,若响应时间过长会带来致命的后果。即使必须支持大量的功能、中断源和标准通信接口,也建议使用带有RTOS的单片机。

内 存

微处理器与单片机之间的另一个主要区别是,微处理器依赖外部存储器来保存和执行程序,而单片机则依赖嵌入式闪存。在微处理器中,程序通常存储在非易失性存储器中,例如eMMC或串行闪存。在启动过程中,将其加载到外部DRAM中并在此执行启动程序。DRAM和非易失性存储器都可以具有几百兆甚至几千兆字节容量,这意味着微处理器几乎从来不受存储容量限制。但有一个潜在缺点:外部存储器或许会使得PCB布局的设计变得更加复杂。

即使是当前的高性能单片机,例如由意法半导体(STMicroelectronics)生产的STM32H7,最多也仅提供2 MB程序内存,对于许多需要操作系统的应用而言可能不足。由于程序位于片上内存中,因此其优点是执行启动和重置过程的速度明显更快。

计算能力

计算能力是典型的选择因素。不过,在这方面,微处理机与单片机之间的界线变得模糊了。例如,如果你将ARM体系结构视为单片机和微处理器市场中分布最广泛的体系结构之一,这就变得显而易见了。ARM提供了不同的处理器体系结构以满足各种要求:

Cortex-A提供了最高性能,并且已经针对综合操作系统进行了优化。它们主要部署在功能强大的设备中,比如智能手机或服务器。

Cortex-M较小,具有更多的片上外设,但是能耗较低,并且针对嵌入式应用进行了优化。

Dhrystone是比较不同处理器性能的测试基准。根据该基准,普通平价单片机具有30 DMIPS,而当前性能最高的单片机(包括嵌入式程序闪存)与这些平价单片机的差距高达1027 DMIPS。相比之下,微处理器的起步点约为1000 DMIPS。

能 耗

单片机在能耗方面表现出色,要比微处理器低很多。尽管微处理器具有节能模式,但其能耗仍然比典型的单片机高得多。而且,微处理器使用外部存储器,因此较难切换到节能模式。对于需要较长的电池运行时间,并且很少使用或没有用户接口的超低功耗应用,单片机是更好的选择,尤其是对于消费类电子产品或智能电表来说。

连接性

大多数单片机和微处理器都配备了所有常规外围设备接口。但是,如果用户需要的是超高速外围设备,在单片机里是找不到例如千兆以太网这种相关接口的。尽管这实际上已成为微处理器中的标准功能单片机。这是十分合理的,因为单片机几乎无法处理这些高速接口所产生的数据量。一个关键问题是:是否有足够的带宽和通道来处理爆发的数据量?

实时表现

当实时性能是最重要的考虑因素时,单片机绝对是首选。凭借处理器内核、嵌入式闪存和软件(RTOS或裸机OS),单片机可以出色地完成实时任务。因为Cortex-A微处理器使用高性能的流水线,用户可以看到在跳转和中断期间,随着流水线的深度不断增加,延迟时间也随之升高。由于OS与微处理器一起执行多任务,因此很难实现硬实时操作。

系统基础IC

由于电源已经集成在单片机中,因此它们仅需要一个单电平电源。另一方面,微处理器需要许多不同电压的电源来为内核和其它组件供电,所以通常需要一个特殊配置的电源管理IC(即所谓的系统基础芯片)来进行供电管理。

结 语

很难说微处理器或单片机哪个才是更好的选择,但经验法则是,你应该始终权衡各种利弊条件。以下几点可以用作大致指导:

单片机非常适合以能耗为主要关注点,且价格较低的移动应用以及具有实时需求的应用。

微处理器则非常适合与操作系统一起运行并需要高速接口的密集计算应用。游戏和其他图形密集型应用使用特殊的微处理器进行联网处理。

来源:网络转载

围观 27

从数据存储类型来说,8051系列有片内、片外程序存储器,片内、片外数据存储器,片内程序存储器还分直接寻址区和间接寻址类型,分别对应code、data、xdata、idata以及根据51系列特点而设定的pdata类型。

使用不同的存储器,将使程序执行效率不同,在编写C51程序时,最好指定变量的存储类型,这样将有利于提高程序执行效率(此问题将在后面专门讲述)。与ANSI-C稍有不同,它只分SAMLL、COMPACT、LARGE模式,各种不同的模式对应不同的实际硬件系统,也将有不同的编译结果。

在51系列中data,idata,xdata,pdata的区别:

data:固定指前面0x00-0x7f的128个RAM,可以用acc直接读写的,速度最快,生成的代码也最小。

idata:固定指前面0x00-0xff的256个RAM,其中前128和data的128完全相同,只是因为访问的方式不同。idata是用类似C中的指针方式访问的。汇编中的语句为:mox ACC,@Rx.(不重要的补充:c中idata做指针式的访问效果很好)

xdata:外部扩展RAM,一般指外部0x0000-0xffff空间,用DPTR访问。

pdata:外部扩展RAM的低256个字节,地址出现在A0-A7的上时读写,用movx ACC,@Rx读写。这个比较特殊,而且C51好象有对此BUG,建议少用。但也有他的优点,具体用法属于中级问题,这里不提。

单片机C语言unsigned char code table[] code是什么作用?

code的作用是告诉单片机,我定义的数据要放在ROM(程序存储区)里面,写入后就不能再更改,其实是相当与汇编里面的寻址MOVX(好像是),因为C语言中没办法详细描述存入的是ROM还是RAM(寄存器),所以在软件中添加了这一个语句起到代替汇编指令的作用,对应的还有data是存入RAM的意思。

程序可以简单的分为code(程序)区,和data(数据)区,code区在运行的时候是不可以更改的,data区放全局变量和临时变量,是要不断的改变的,cpu从code区读取指令,对data区的数据进行运算处理,因此code区存储在什么介质上并不重要。

像以前的计算机程序存储在卡片上,code区也可以放在rom里面,也可以放在ram里面,也可以放在flash里面(但是运行速度要慢很多,主要读flash比读ram要费时间),因此一般的做法是要将程序放到flash里面,然后load到ram里面运行的;DATA区就没有什么选择了,肯定要放在RAM里面,放到rom里面改动不了。

bdata如何使用它呢?

若程序需要8个或者更多的bit变量,如果你想一次性给8个变量赋值的话就不方便了,(举个例子说说它的方便之处,想更深入的了解请在应用中自己琢磨)又不可以定义bit数组,只有一个方法

char bdata MODE;

sbit MODE_7 = MODE^7;

sbit MODE_6 = MODE^6;

sbit MODE_5 = MODE^5;

sbit MODE_4 = MODE^4;

sbit MODE_3 = MODE^3;

sbit MODE_2 = MODE^2;

sbit MODE_1 = MODE^1;

sbit MODE_0 = MODE^0;

8个bit变量MODE_n就定义好了

这是定义语句,Keilc的特殊数据类型。记住一定要是sbit

不能bit MODE_0 = MODE^0;

赋值语句要是这么写C语言就视为异或运算。

keil生成的文件:

.plg:编译器编译结果

.hex和.bin:可执行文件

.map和.lst:链接文件

.o:目标文件

.crf、.lnp、.d和.axf:调试文件

.opt:保存工程配置信息

.bak:工程备份文件

M51文件,startup文件。



来源:网络,转载此文目的在于传递更多信息,版权归原作者所有。

围观 977

算法(Algorithm):计算机解题的基本思想方法和步骤。

算法的描述:是对要解决一个问题或要完成一项任务所采取的方法和步骤的描述,包括需要什么数据(输入什么数据、输出什么结果)、采用什么结构、使用什么语句以及如何安排这些语句等。通常使用自然语言、结构化流程图、伪代码等来描述算法。

而单片机会用到什么常用的C语言?

一、计数、求和、求阶乘等简单算法

此类问题都要使用循环,要注意根据问题确定循环变量的初值、终值或结束条件,更要注意用来表示计数、和、阶乘的变量的初值。

例:用随机函数产生100个[0,99]范围内的随机整数,统计个位上的数字分别为1,2,3,4,5,6,7,8,9,0的数的个数并打印出来。

本题使用数组来处理,用数组a[100]存放产生的确100个随机整数,数组x[10]来存放个位上的数字分别为1,2,3,4,5,6,7,8,9,0的数的个数。即个位是1的个数存放在x[1]中,个位是2的个数存放在x[2]中,……个位是0的个数存放在x[10]。

void main()
{
int a[101],x[11],i,p;
for(i=0;i<=11;i++)
x=0;
for(i=1;i<=100;i++)
{
a=rand() % 100;
printf("%4d",a);
if(i%10==0)printf("\n");
}
for(i=1;i<=100;i++)
{
p="a"%10;
if(p==0) p="10";
x[p]=x[p]+1;
}
for(i=1;i<=10;i++)
{
p="i";
if(i==10) p="0";
printf("%d,%d\n",p,x);
}
printf("\n");
}

二、求两个整数的最大公约数、最小公倍数

分析:求最大公约数的算法思想:(最小公倍数=两个整数之积/最大公约数)
(1) 对于已知两数m,n,使得m>n;
(2) m除以n得余数r;
(3) 若r=0,则n为求得的最大公约数,算法结束;否则执行(4);
(4) m←n,n←r,再重复执行(2)。

例如: 求 m="14" ,n=6 的最大公约数. m n r
14 6 2
6 2 0
void main()
{ int nm,r,n,m,t;
printf("please input two numbers:\n");
scanf("%d,%d",&m,&n);
nm=n*m;
if (m
{ t="n"; n="m"; m="t"; }
r=m%n;
while (r!=0)
{ m="n"; n="r"; r="m"%n; }
printf("最大公约数:%d\n",n);
printf("最小公倍数:%d\n",nm/n);
}

三、判断素数

只能被1或本身整除的数称为素数 基本思想:把m作为被除数,将2—INT( )作为除数,如果都除不尽,m就是素数,否则就不是。(可用以下程序段实现)
void main()
{ int m,i,k;
printf("please input a number:\n");
scanf("%d",&m);
k=sqrt(m);
for(i=2;i
if(m%i==0) break;
if(i>=k)
printf("该数是素数");
else
printf("该数不是素数");
}
将其写成一函数,若为素数返回1,不是则返回0
int prime( m%)
{int i,k;
k=sqrt(m);
for(i=2;i
if(m%i==0) return 0;
return 1;
}

四、验证哥德巴赫猜想

(任意一个大于等于6的偶数都可以分解为两个素数之和)

基本思想:n为大于等于6的任一偶数,可分解为n1和n2两个数,分别检查n1和n2是否为素数,如都是,则为一组解。如n1不是素数,就不必再检查n2是否素数。先从n1=3开始,检验n1和n2(n2=N-n1)是否素数。然后使n1+2 再检验n1、n2是否素数,… 直到n1=n/2为止。

利用上面的prime函数,验证哥德巴赫猜想的程序代码如下:

#include "math.h"
int prime(int m)
{ int i,k;
k=sqrt(m);
for(i=2;i
if(m%i==0) break;
if(i>=k)
return 1;
else
return 0;
}
main()
{ int x,i;
printf("please input a even number(>=6):\n");
scanf("%d",&x);
if (x<6||x%2!=0)
printf("data error!\n");
else
for(i=2;i<=x/2;i++)
if (prime(i)&&prime(x-i))
{
printf("%d+%d\n",i,x-i);
printf("验证成功!");
break;
}
}

五、排序问题

1.选择法排序(升序)

基本思想:
1)对有n个数的序列(存放在数组a(n)中),从中选出最小的数,与第1个数交换位置;
2)除第1 个数外,其余n-1个数中选最小的数,与第2个数交换位置;
3)依次类推,选择了n-1次后,这个数列已按升序排列。

程序代码如下:

void main()
{ int i,j,imin,s,a[10];
printf("\n input 10 numbers:\n");
for(i=0;i<10;i++)
scanf("%d",&a);
for(i=0;i<9;i++)
{ imin="i";
for(j=i+1;j<10;j++)
if(a[imin]>a[j]) imin="j";
if(i!=imin)
{s=a; a=a[imin]; a[imin]=s; }
printf("%d\n",a);
}
}

2.冒泡法排序(升序)

基本思想:(将相邻两个数比较,小的调到前头)
1)有n个数(存放在数组a(n)中),第一趟将每相邻两个数比较,小的调到前头,经n-1次两两相邻比较后,最大的数已“沉底”,放在最后一个位置,小数上升“浮起”;
2)第二趟对余下的n-1个数(最大的数已“沉底”)按上法比较,经n-2次两两相邻比较后得次大的数;
3)依次类推,n个数共进行n-1趟比较,在第j趟中要进行n-j次两两比较。

程序段如下

void main()
{ int a[10];
int i,j,t;
printf("input 10 numbers\n");
for(i=0;i<10;i++)
scanf("%d",&a);
printf("\n");
for(j=0;j<=8;j++)
for(i=0;i<9-j;i++)
if(a>a[i+1])
{t=a;a=a[i+1];a[i+1]=t;}
printf("the sorted numbers:\n");
for(i=0;i<10;i++)
printf("%d\n",a);
}

3.合并法排序(将两个有序数组A、B合并成另一个有序的数组C,升序)

基本思想:
1)先在A、B数组中各取第一个元素进行比较,将小的元素放入C数组;
2)取小的元素所在数组的下一个元素与另一数组中上次比较后较大的元素比较,重复上述比较过程,直到某个数组被先排完;
3)将另一个数组剩余元素抄入C数组,合并排序完成。

程序段如下:

void main()
{ int a[10],b[10],c[20],i,ia,ib,ic;
printf("please input the first array:\n");
for(i=0;i<10;i++)
scanf("%d",&a);
for(i=0;i<10;i++)
scanf("%d",&b);
printf("\n");
ia=0;ib=0;ic=0;
while(ia<10&&ib<10)
{ if(a[ia]
{ c[ic]=a[ia];ia++;}
else
{ c[ic]=b[ib];ib++;}
ic++;
}
while(ia<=9)
{ c[ic]=a[ia];
ia++;ic++;
}
while(ib<=9)
{ c[ic]=b[ib];
b++;ic++;
}
for(i=0;i<20;i++)
printf("%d\n",c);
}

六、查找问题

顺序查找法(在一列数中查找某数x)

基本思想:一列数放在数组a[1]---a[n]中,待查找的数放在x 中,把x与a数组中的元素从头到尾一一进行比较查找。用变量p表示a数组元素下标,p初值为1,使x与a[p]比较,如果x不等于a[p],则使p=p+1,不断重复这个过程;一旦x等于a[p]则退出循环;另外,如果p大于数组长度,循环也应该停止。(这个过程可由下语句实现)

void main()
{ int a[10],p,x,i;
printf("please input the array:\n");
for(i=0;i<10;i++)
scanf("%d",&a);
printf("please input the number you want find:\n");
scanf("%d",&x);
printf("\n");
p=0;
while(x!=a[p]&&p<10)
p++;
if(p>=10)
printf("the number is not found!\n");
else
printf("the number is found the no%d!\n",p);
}

思考:将上面程序改写一查找函数Find,若找到则返回下标值,找不到返回-1

②基本思想:一列数放在数组a[1]---a[n]中,待查找的关键值为key,把key与a数组中的元素从头到尾一一进行比较查找,若相同,查找成功,若找不到,则查找失败。(查找子过程如下。index:存放找到元素的下标。)

void main()
{ int a[10],index,x,i;
printf("please input the array:\n");
for(i=0;i<10;i++)
scanf("%d",&a);
printf("please input the number you want find:\n");
scanf("%d",&x);
printf("\n");
index=-1;
for(i=0;i<10;i++)
if(x==a)
{ index="i"; break;
}
if(index==-1)
printf("the number is not found!\n");
else
printf("the number is found the no%d!\n",index);
}

七、二分法

在一个数组中,知道一个数值,想确定他在数组中的位置下标,如数组:A[5] = {1,2,6,7,9};我知道其中的值为6,那么他的下标位置就是3。

int Dichotomy(int *ucData, int long, int num)
{
int iDataLow = 0 ;
int iDataHigh = num - 1;
int iDataMIDDLE;
while (iDataLow <= iDataHigh)
{
iDataMIDDLE = (iDataHigh + iDataLow)/2;
i f (ucData[iDataMIDDLE] > long)
{
iDataHigh = iDataMIDDLE - 1 ;
}
else if (ucData[iDataMIDDLE] < long)
{
iDataLow = iDataMIDDLE + 1 ;
} else{
return iDataMIDDLE ;
}
}
}

八、限幅滤波法

对于随机干扰 , 限幅滤波是一种有效的方法;

基本方法:比较相邻n 和 n - 1时刻的两个采样值y(n)和 y(n – 1),根据经验确定两次采样允许的最大偏差。如果两次采样值的差值超过最大偏差范围 ,认为发生可随机干扰 ,并认为后一次采样值y(n)为非法值 ,应予删除 ,删除y(n)后 ,可用y(n – 1) 代替y(n);若未超过所允许的最大偏差范围 ,则认为本次采样值有效。

下面是限幅滤波程序:( A 值可根据实际情况调整,value 为有效值 ,new_value 为当前采样值滤波程序返回有效的实际值 )

#define A 10
char value;
char filter()
{ char new_value;
new_value = get_ad();
if ( ( new_value - value > A ) || ( value - new_value > A )) return value;
return new_value;
}

九、中位值滤波法

中位值滤波法能有效克服偶然因素引起的波动或采样不稳定引起的误码等脉冲干扰;

对温度 液位等缓慢变化的被测参数用此法能收到良好的滤波效果 ,但是对于流量压力等快速变化的参数一般不宜采用中位值滤波法;

基本方法:对某一被测参数连续采样 n次(一般 n 取奇数) ,然后再把采样值按大小排列 ,取中间值为本次采样值。

下面是中位值滤波程序:

#define N 11
char filter()
{ char value_buf[N], count,i,j,temp;
for ( count=0;count
{ value_buf[count] = get_ad(); delay(); }
for (j=0;j
{ for (i=0;i
{ if ( value_buf>value_buf[i+1] )
{temp = value_buf; value_buf = value_buf[i+1]; value_buf[i+1] = temp; }
}
}
return value_buf[(N-1)/2];
}

十、算术平均滤波法

算术平均滤波法适用于对一般的具有随机干扰的信号进行滤波。这种信号的特点是信号本身在某一数值范围附近上下波动 ,如测量流量、 液位;

基本方法:按输入的N 个采样数据,寻找这样一个 Y ,使得 Y 与各个采样值之间的偏差的平方和最小。

编写算术平均滤波法程序时严格注意:

1. 为了加快数据测量的速度,可采用先测量数据 存放在存储器中 ,测完 N 点后 ,再对 N 个数据进行平均值计算;

2. 选取适当的数据格式 ,也就是说采用定点数还是采用浮点数。其程序如下所示:

#define N 12
char filter()
{int sum = 0,count;
for ( count=0;count
{ sum+=get_ad(); delay();}
return (char)(sum/N);
}

十一、递推平均滤波法

基本方法:采用队列作为测量数据存储器 , 设队列的长度为 N ,每进行一次测量,把测量结果放于队尾 ,而扔掉原来队首的一个数据,这样在队列中始终就有 N 个 “最新” 的数据。当计算平均值时,只要把队列中的 N 个数据进行算数平均,就可得到新的算数平均值。这样每进行一次测量,就可得到一个新的算术平均值。

#define N 12
char value_buf[N],i=0;
char filter()
{ char count; int sum=0;
value_buf[i++] = get_ad();
if ( i == N ) i = 0;
for ( count=0;count
sum = value_buf[count];
return (char)(sum/N);
}

十二、一阶滞后滤波法

优点:对周期性干扰具有良好的抑制作用,适用于波动频率较高的场合;

缺点:相位滞后,灵敏度低.滞后程度取决于a值大小。不能消除滤波频率高于采样频率的1/2的干扰信号。

程序如下:

#define a 50
char value;
char filter()
{ char new_value;
new_value = get_ad();
return (100-a)*value + a*new_value;
}

十三、PID控制算法

在过程控制中,按偏差的比例(P)、积分(I)和微分(D)进行控制的PID控制器(亦称PID调节器)是应用最为广泛的一种自动控制器;
对于过程控制的典型对象──“一阶滞后+纯滞后”与“二阶滞后+纯滞后”的控制对象,PID控制器是一种最优控制;
PID调节规律是连续系统动态品质校正的一种有效方法,它的参数整定方式简便,结构改变灵活(PI、PD、…)。

一 模拟PID调节器

PID调节器各校正环节的作用:

比例环节:即时成比例地反应控制系统的偏差信号e(t),偏差一旦产生,调节器立即产生控制作用以减小偏差;

积分环节:主要用于消除静差,提高系统的无差度。积分时间常数TI越大,积分作用越弱,反之则越强;

微分环节:能反应偏差信号的变化趋势(变化速率),并能在偏差信号的值变得太大之前,在系统中引入一个有效的早期修正信号,从而加快系统的动作速度,减小调节时间。

PID调节器是一种线性调节器,它将给定值r(t)与实际输出值c(t)的偏差的比例(P)、积分(I)、微分(D)通过线性组合构成控制量,对控制对象进行控制。

程序片段如下:

#include
#include
typedef struct PID {
double SetPoint; // 设定目标Desired value
double Proportion; // 比例常数Proportional Const
double Integral; // 积分常数Integral Const
double Derivative; // 微分常数Derivative Const
double LastError; // Error[-1]
double PrevError; // Error[-2]
double SumError; // Sums of Errors
} PID;

主程序:

double sensor (void)
{
return 100.0; }
void actuator(double rDelta)
{}
void main(void)
{
PID sPID;
double rOut;
double rIn;
PIDInit ( &sPID );
sPID.Proportion = 0.5;
sPID.Derivative = 0.0;
sPID.SetPoint = 100.0;
for (;;) {
rIn = sensor ();
rOut = PIDCalc ( &sPID,rIn );
actuator ( rOut );
}
}

十四、开根号算法

单片机开平方的快速算法

因为工作的需要,要在单片机上实现开根号的操作。目前开平方的方法大部分是用牛顿迭代法。我在查了一些资料以后找到了一个比牛顿迭代法更加快速的方法。不敢独享,介绍给大家,希望会有些帮助。

1. 原理

因为排版的原因,用pow(X,Y)表示X的Y次幂,用B[0],B[1],...,B[m-1]表示一个序列,其中[x]为下标。

假设:
B[x],b[x]都是二进制序列,取值0或1。

M = B[m-1]*pow(2,m-1) + B[m-2]*pow(2,m-2) + ... + B[1]*pow(2,1) + B[0]*pow(2,0)
N = b[n-1]*pow(2,n-1) + b[n-2]*pow(2,n-2) + ... + b[1]*pow(2,1) + n[0]*pow(2,0)
pow(N,2) = M

(1) N的最高位b[n-1]可以根据M的最高位B[m-1]直接求得。
设 m 已知,因为 pow(2, m-1) <= M <= pow(2, m),所以 pow(2, (m-1)/2) <= N <= pow(2, m/2)
如果 m 是奇数,设m=2*k+1,
那么 pow(2,k) <= N < pow(2, 1/2+k) < pow(2, k+1),
n-1=k, n=k+1=(m+1)/2
如果 m 是偶数,设m=2k,
那么 pow(2,k) > N >= pow(2, k-1/2) > pow(2, k-1),
n-1=k-1,n=k=m/2
所以b[n-1]完全由B[m-1]决定。
余数 M[1] = M - b[n-1]*pow(2, 2*n-2)

(2) N的次高位b[n-2]可以采用试探法来确定。
因为b[n-1]=1,假设b[n-2]=1,则 pow(b[n-1]*pow(2,n-1) + b[n-1]*pow(2,n-2), 2) = b[n-1]*pow(2,2*n-2) + (b[n-1]*pow(2,2*n-2) + b[n-2]*pow(2,2*n-4)),
然后比较余数M[1]是否大于等于 (pow(2,2)*b[n-1] + b[n-2]) * pow(2,2*n-4)。这种比较只须根据B[m-1]、B[m-2]、...、B[2*n-4]便可做出判断,其余低位不做比较。
若 M[1] >= (pow(2,2)*b[n-1] + b[n-2]) * pow(2,2*n-4), 则假设有效,b[n-2] = 1;
余数 M[2] = M[1] - pow(pow(2,n-1)*b[n-1] + pow(2,n-2)*b[n-2], 2) = M[1] - (pow(2,2)+1)*pow(2,2*n-4);
若 M[1] < (pow(2,2)*b[n-1] + b[n-2]) * pow(2,2*n-4), 则假设无效,b[n-2] = 0;余数 M[2] = M[1]。

(3) 同理,可以从高位到低位逐位求出M的平方根N的各位。
使用这种算法计算32位数的平方根时最多只须比较16次,而且每次比较时不必把M的各位逐一比较,尤其是开始时比较的位数很少,所以消耗的时间远低于牛顿迭代法。

3. 实现代码

这里给出实现32位无符号整数开方得到16位无符号整数的C语言代码。

/****************************************/
/*Function: 开根号处理 */
/*入口参数:被开方数,长整型 */
/*出口参数:开方结果,整型 */
/****************************************/
unsigned int sqrt_16(unsigned long M)
{
unsigned int N, i;
unsigned long tmp, ttp; // 结果、循环计数
if (M == 0) // 被开方数,开方结果也为0
return 0;
N = 0;
tmp = (M >> 30); // 获取最高位:B[m-1]
M <<= 2;
if (tmp > 1) // 最高位为1
{
N ++; // 结果当前位为1,否则为默认的0
tmp -= N;
}
for (i=15; i>0; i--) // 求剩余的15位
{
N <<= 1; // 左移一位
tmp <<= 2;
tmp += (M >> 30); // 假设
ttp = N;
ttp = (ttp<<1)+1;
M <<= 2;
if (tmp >= ttp) // 假设成立
{
tmp -= ttp;
N ++;
}
}
return N;
}

本文内容来源于网络,文章转载只为学术传播,如涉及侵权请联系删除。

围观 116

01:前言

全局变量简直就是嵌入式系统的戈兰高地。冲突最激烈的双方是:1. 做控制的工程师, 2. 做非嵌入式的软件工程师。

02:做控制的工程师特点

他们普遍的理解就是“变量都写成全局该有多方便”。我之前面试过一个非常有名的做控制实验室里出来的PhD/Master,前前后后陆续有快十个人。面试问题是用C写PID。到后面的几位面试的时候我都觉得没有看的意义了,因为全都写的是同一个风格。大概就是这样的:

float SetSpeed;
float err;
float err_last;
float Kp,Ki,Kd;
float integral;
float result;

float PID(float speed)
{
    err=SetSpeed-speed;
    integral+=err;
    result=Kp*err+Ki*integral+Kd*(err-err_last);
    err_last=err;
    return result;
}

代码的特点就是所有的变量一定定义在函数外面。问他们为什么,回答是“全局变量方便调试”。

事实上在学校里做搞自动控制的人最重要的根本就是控制的结果,而不是代码本身。代码只要能工作就行。变量名污染,低耦合之类的和他们就不在同一个世界。进了公司有些人代码质量会变好,但有的还是会延续之前的习惯。前公司代码库里面凡是看不懂的代码一律都是那一两个Control Engineer写的,写完了还会用自己的名字给函数命名的那种。

要成为一个资深的嵌入式工程师相当难,一方面要有非常扎实的理论知识,同时也要有相当的那种大型的、高频CPU、多层PCB板的设计经验。嵌入式硬件工程师要学的课程主要有模拟电路设计、数字电路设计、电磁波理论等。熟悉常用的放大电路、滤波电路、电源电路设计和分析。

03:做非嵌入式的软件工程师特点

代码的特点就是所有的静态变量都不可以定义在.h文件里,必须写在.c文件里以确保别的文件没法访问它们。

别的文件真要访问怎么办?那就给每一个变量写get/set函数啊!问题是静态变量写在.c文件里编译器是没法优化get/set的。结果就大面积的变量访问要花几倍的CPU时间去做get/set的函数调用。嵌入式项目很多情况下对硬件的压榨是很极端的,CPU利用率90%都不算什么,顶到97%都是有的。(注意下这些项目是实时性要求很高的,晚一个毫秒算不完都不行。不是跑在电脑上鼠标卡一卡也无所谓的。)然后为了封装性,在代码里面塞这么多get/set吗?

04:总结

总的来说嵌入式软件里大部分的代码都是中断驱动的,天生就有很多变量是没法使用参数传递的。全局变量的存在是因为正义站在这边。但是嵌入式软件远远没有特殊到不需要按照正常软件工程方法去管理的地步。要真有人认为“嵌入式软件只要能工作就成,代码丑一点无所谓的”纯粹是软件工程水平不行,不是因为控制水平太高。

全局变量一定是要用的,管理它们也很重要。一些基本的代码规则:

如果只是文件内调用,全局变量只能写在这个.c文件里,不要写进.h文件。

如果有文件外调用,全局变量要写在.h文件里。

.h里面的全局变量全局可读,但是只有本文件组可以写。别的文件要写请调用set函数。

所有的全局变量无论在.h还是.c里面都要包成同名struct。哪怕只有一个变量也要写进struct里面。比如PID.c里面有一个pid_S,PID.h里面有个PID_S。这样其他人不仅可以立即识别出一个变量是project内global/文件内static/函数内local,同时还能轻松追溯到这个函数是属于哪个文件的。

不要写函数内的static变量。函数内的static变量在实际的项目中几乎就是bug生成器,没法简单的reset。而且对unit test非常不友好。

本文系网络转载,转载此文目的在于传递更多信息,版权归原作者所有。如有不妥请联系删除。

围观 42

负电压的产生电路图原理

在电子电路中我们常常需要使用负电压,比如说我们在使用运放的时候常常需要建立一个负电压。下面就简单的以正5V电压到负电压5V为例说一下它的电路。

通常需要使用负电压时一般会选择使用专用的负压产生芯片,但这些芯片都比较贵,比如ICL7600,LT1054等。差点忘了MC34063了,这个芯片使用的最多了,关于34063的负压产生电路这里不说了,在datasheet中有的。下面请看我们在单片机电子电路中常用的两种负电压产生电路。

单片机中常用的负电压是怎样产生的?

现在的单片机有很多都带有了PWM输出,在使用单片机的时候PWM很多时候是没有用到的,用它辅助产生负压是不错的选择。

上面的电路是一个最简单的负压产生电路了。使用的原件是最少的了,只需要给它提供1kHz左右的方波就可以了,相当简单。这里需要注意这个电路的带负载能力是很弱的,同时在加上负载后电压的降落也比较大。

由于上面的原因产生了下面的这个电路:

单片机中常用的负电压是怎样产生的?

负电压产生电路分析

电压的定义:电压(voltage),也称作电势差或电位差,是衡量单位电荷在静电场中由于电势不同所产生的能量差的物理量。其大小等于单位正电荷因受电场力作用从A点移动到B点所做的功,电压的方向规定为从高电位指向低电位的方向。

说白了就是:某个点的电压就是相对于一个参考点的电势之间的差值。V某=E某-E参。一般把供电电源负极当作参考点。电源电压就是Vcc=E电源正-E电源负。

想产生负电压,就让它相对于电源负极的电势更低即可。要想更低,必须有另一个电源的介入,根本原理都是利用两个电源的串联。电源2正极串联在参考电源1的负极后,电源2负极就是负电压了。

单片机中常用的负电压是怎样产生的?

一个负电压产生电路:利用电容充电等效出一个新电源,电容串联在GND后,等效为电源2,则产生负电压。

单片机中常用的负电压是怎样产生的?

1、电容充电:当PWM为低电平时,Q2打开,Q1关闭,VCC通过Q2给C1充电,充电回路是VCC-Q2-C1-D2-GND,C1上左正右负。

单片机中常用的负电压是怎样产生的?

2、电容C1充满电。

单片机中常用的负电压是怎样产生的?

3、电容C1作为电源,C1高电势极串联在参考点。C1放电,从C2续流,产生负电压。

当PWM为低电平时,Q2关闭,Q1打开,C1开始放电,放电回路是C1-C2-D1,这实际上也是对C2进行充电的过程。C2充好电后,下正上负,如果VCC的电势为5点几伏,就可以输出-5V的电压了。

单片机中常用的负电压是怎样产生的?

产生负电压(-5V)的方案

单片机中常用的负电压是怎样产生的?

7660和MAX232输出能力有限,做示波器带高速运放很吃力,所以也得用4片并联的方式扩流。

第一版是7660两片并联的。

用普通的DC/DC芯片都可以产生负电压,且电压精确度同正电压一样,驱动能力也很强,可以达到300mA以上。

一般的开关电源芯片都能产生负电压,实在不行用开关电源输出的PWM去推电荷泵,也可以产生较大的电流,成本也很低,不知纹波要求多少,电荷泵用LC滤波之后纹波相当小的。7660是电荷泵,所以电流很小。

整个示波器的设计,数字电源的+5V和模拟电源的+5V是分开供电的,但是数字地和模拟地应该怎么处理呢?

数字地和模拟地是一定要连在一起的,不然电路没法工作。

数字部分的地返回电流不能流过模拟部分地,两个地应该在稳定的地参考点连在一起。

负电压的意义

1、人为规定。例如电话系统里是用-48V来供电的,这样可以避免电话线被电化学腐蚀。当然了,反着接电话也是可以工作的,无非是电压参考点变动而已。

2、通讯接口需要。例如RS232接口,就必须用到负电压。-3V~-15V表示1,+3~+15V表示0。这个是当初设计通讯接口时的协议,只能遵守咯。PS:MAX232之类的接口芯片自带电荷泵,可以自己产生负电压。

3、为(非轨到轨)运放提供电源轨。老式的运放是没有轨到轨输入/输出能力的,例如OP07,输入电压范围总是比电源电压范围分别小1V,输出分别小2V。这样如果VEE用0V,那么输入端电压必须超过1V,输出电压不会低于2V。这样的话可能会不满足某些电路的设计要求。为了能在接近0V的输入/输出条件下工作,就需要给运放提供负电压,例如-5V,这样才能使运放在0V附近正常工作。不过随着轨到轨运放的普及,这种情况也越来越少见了。

4、这个比较有中国特色,自毁电路。一般来说芯片内部的保护电路对于负电压是不设防的,所以只要有电流稍大,电压不用很高的负电压加到芯片上,就能成功摧毁芯片。

本文来源网络,如果原作者不支持咱们转发,请联系删除,谢谢!

围观 142

振荡器由晶振、电容、电阻组成,部分还有电感,它以晶体频率产生脉冲序列。以英锐恩单片机为例,EN8F156便拥有稳定的时钟源。但不是所有单片机的时钟源都一样,由于每条指令在一定数量的时钟周期后执行,具体取决于单片机的架构。比如,有些人支持使用PLL电路在内部进一步提高时钟速率的能力。


目前,有许多类型的时钟源。可以使用简单的RC电路作为时钟源,但是它们并不精确,因为它们会随温度而发生变化。这类时钟源适用于正常处理和SPI等同步协议。但是对于UART/RS-232,这不是一个好的选择,因为没有外部时钟与数据一起发送,而是以波特率或协议定时的形式将时钟嵌入数据(信号)中。这会降低单片机器对接收数据进行采样的能力,以及正确传输数据的能力。

有些单片机的晶振是外部的,这样可能会更加精确。除此之外,一些单片机器没有内置时钟源,但大多数都有某种RC电路,优点是这类单片机都相当便宜。

大多数单片机器都支持时钟分频器,但它们的频率可能并不精确,因此可能需要使用外部频率。比如,实时时钟通常需要外部32.768KHz晶振。这些都要求非常精确,因为它们是在计算时间。如果不准确,时间相差可能很大。有些单片机内置32KHzRC低速时钟,不过这不足以用来计算时间。

一些单片机器内部和外部支持多个时钟,这一切都取决于单片机和应用程序。

转自:畅学单片机,转载此文目的在于传递更多信息,版权归原作者所有,如涉及侵权,请联系删除。

围观 56

页面

订阅 RSS - 单片机