Zeriodical

埋头开荒,双商破冰,扬短避长,屎上醉男。


  • Home

  • Tags

  • Categories

  • Archives

优雅地定制 UITableViewController

Posted on 2019-03-10 | In iOS , UI

UITableViewController 是专门设计用来应付UITableView的视图控制器,讲道理用它是比用普通的ViewController + Data Source & Delegate 要方便很多的,它有很多便利的功能能加速我们开发TableViewController。

  • clearSelectionOnViewWillAppear
    若是在UIViewController上自己实现倒也是很简单。其实这个功能的名字几乎已经完全说明了他是怎么实现的。

  • 键盘监听与输入框防遮挡
    这个功能可以说是UITableViewController一大非常便利的功能。它提供了在列表视图中非常完美的输入框键盘防遮挡功能。只要你聚焦于其内部的某个输入框,他一定会以一种十分恰当的方式让聚焦框露出来。
    但我们自己在UIViewController上实现的类似功能是有些费劲的。

  • Refresh Control
    其内部的一个好处就是,在目前主流兼容的版本中,你不需要再做版本适配,因为它是从iOS 6起兼容的。而UIScrollView中的相同api是从iOS 10 起。如果你的项目是iOS 10以上起步的话,就忽略这个优点吧。

问题

UITableViewController的主控视图(UIViewController.view)是UITableView,而且他是继承了UIScrollView,所以当你直接往上面添加一个子视图(比如Toast、浮动按钮)的时候,你会发现这些子视图会跟着你页面一起上下滚动。
emmmm… 我当时在给公司项目接入toast的时候就是遇到了这个问题,所以我一怒之下抛弃了UITableViewController

UIViewController + Data Source & Delegate

如果我们直接用UIViewController,并使tableView作为其view的子视图的话,toast的问题就可以解决,因为toast所坐落的父视图并不是一个滚动视图。
就酱,我以为从此我找到了究极解决方案,直到有一天在我写Terrace这个库的时候,一个UITableViewCell内置输入框键盘防遮挡的效果迟迟不能实现成我满意的样子。就在这个时候,我想起了曾经被我抛弃的UITableViewController处理起键盘防遮挡如丝顺滑。我决定再试一次UITableViewController。

Customizing

关键问题是要给tableView加个座垫儿。

作为子控制器

从iOS 5开始,苹果为UIViewController增加了子视图控制器概念,但类似的概念并不算苹果第一次提出,UINavigationController不就是导航栏爸爸管着一群视图控制器崽子们吗?

UITableViewController 做为子控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
class WrappedTableViewController : UITableViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 被父控制器添加为子视图后配置约束
if let superview = self.parent?.view {
self.tableView.translatesAutoresizingMaskIntoConstraints = false
self.tableView.topAnchor.constraint(equalTo: superview.topAnchor).isActive = true
self.tableView.bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true
self.tableView.leftAnchor.constraint(equalTo: superview.leftAnchor).isActive = true
self.tableView.rightAnchor.constraint(equalTo: superview.rightAnchor).isActive = true
}
}
}

UIViewController 父控制器:

1
2
3
4
5
6
7
8
class ViewController: UIViewController {
private lazy var tableViewController = WrappedTableViewController(style: .plain)
override func viewDidLoad() {
super.viewDidLoad()
self.addChild(self.tableViewController)
self.view.addSubview(self.tableViewController.tableView)
}
}

这种方式可能会显得仅仅起包裹作用的父控制器没什么事做,当一个座垫儿是不是有点大材小用了。

改变控制的tableView

1
var tableView: UITableView! { get set }

Returns the table view managed by the controller object.

既然它的目的是返回由控制器控制的table view object,我们可以通过override来达到我们的目的,告诉它谁才是真正的主角!

这次作为Container的就可以是一个UIView,而不是用UIViewController。

1
2
3
4
class ContainerView : UIView {
var tableView = UITableView()
// ... view configurations
}

敲黑板敲黑板!注意啦注意啦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TableViewController : UITableViewController {
let containerView = ContainerView()

// 控制我们自己的TableView
override var tableView: UITableView! {
get { return self.containerView.tableView }
set {
self.containerView.tableView = newValue
newValue.delegate = self
newValue.dataSource = self
}
}

override func loadView() {
self.view = self.containerView // 将主视图替换为自定义基底视图
self.tableView.delegate = self
self.tableView.dataSource = self
}
}

首先在loadView配置本控制器显示的主视图不再是默认的tableView,而是我们准备的containerView。然后让tableView的getter和setter都指向我们自己的tableView,别忘了指定dataSource和delegate。

写在最后

当时我在tableView上解决toast问题和输入框键盘遮挡问题,第一次使用UITableViewController的时候也是我第一次用UITableView的时候。当时还并没有深入了解视图控制器对视图的加载时机和自定义方式,直到后来学会了利用loadView方法。但那个时候我已经在UIViewController + DataSource & Delegate 的方式中走了很久,再直到后来一次在这个方式基础之上处理输入框键盘遮挡问题时觉得很麻烦,才在一次将注意力移回到UITableViewController上。

Objective-C的内存管理艺术:入门

Posted on 2017-07-18 | Edited on 2019-03-01 | In iOS

文章框架

前言

呀!!再也不三心二意了,从今往后专心搞iOS!嗯!
从大一开始就嚷嚷对iOS热爱的很,结果呢,就毕设写了个垃圾,咦!
所以现在起,iOS就是我的主线了!先写这篇垃圾文章壮壮气势!

本篇内容非常浅显的谈谈Objective-C中的内存管理,适合那些会写点简单的App但从未认真对待过内存管理的初级iOS工程师阅读,在整个内存管理的艺术之路上入个门。


传统内存管理

传统的内存管理我想从C语言说起。当你用malloc函数申请一片空间的时候(或是C++的new),就一定要用free函数释放掉(或是C++的delete)。

但是在面向对象的世界,我们一切打交道的东西是对象,这玩意你可不能说来就来说走就走,来就得初始化一下(构造函数,空间申请+变量初始化),走就得垂死挣扎一下(析构函数,遗产管理+空间释放)。

Objective-C是一门面向对象的语言,所以其与之对应的方法就是alloc、init和dealloc。dealloc就是析构函数。其构造函数相当于先调用alloc再init,说白了就是他把空间申请和变量初始化分的很清楚,alloc就是空间申请,init就是变量初始化,这俩通常是成对儿出现的。你问只alloc不init行不行,可以!没问题!只不过你的私有成员表示很郁闷:谁来给我们赋初值啊?

定义

  • 构造函数
    也就是构造的时候,在objective-c中定义构造函数时不用你去申请空间,只是写好初始化方法init就好了
1
2
3
4
5
6
- (instanceType) init {
[super init]; // 别忘了要把祖宗也都构造上
if(self){
// 需要初始化的东西
}
}
  • 析构函数
    析构函数,在对象被释放之前,对象总要那么挣扎几下才会安心的去的。不过你也可以让你的对象没有任何想说的直接挂掉,这就可能会出现问题:这财产归谁啊?还是彻底不要了?不能扔那不管啊!
1
2
3
4
- (void) dealloc {
// 遗嘱
[super dealloc]; // 株连九族
}

注意,这里超类的析构函数要最后调用。

调用

  • 构造函数
1
ClassName* instance = [[ClassName alloc] init]; // 先构造再初始化

如果说你的构造函数不需要什么参数,直接走默认

1
ClassName* instance = [ClassName new]; //  等价于上面的

如果你很奔放的不需要初始化

1
ClassName* instance = [ClassName alloc]; // 完全没问题! 没毛病~
  • 析构函数
1
[instance dealloc];

至此,在Objective-C中最基本的动态内存管理算是介绍完了。你要是不看后面的直接这么玩,没问题!

那么问题来了:遇到复杂的情况,这种传统模式就很捉急。
比如我要在指针A中引用空间M,引用B也要引用对象M,对象C也来凑热闹引用了对象M。哇!这对象M简直是香饽饽。A说了:“这盘香饽饽是我做的,我要吃,谁最后吃完谁收拾剩饭”;B说了:“这盘香饽饽我要吃,谁最后吃完谁收拾剩饭”;C说:“我不吃,我就看看。”

好,最后这盘香饽饽什么时候收拾?对应在程序里面也就是什么时候调用[M dealloc]?

谁最后吃完谁收拾呗,说得好听!谁知道程序里面谁最后吃完啊,对应着用上面的方式A B C里面都引用了M,A和B谁最后吃完那完全是看心情的,都任性的很呢!


引用计数管理

引用计数是个好方法,能解决上面的香饽饽问题。也就是这种机制可以让程序知道到底是谁最后吃完了香饽饽。
其基本思想:用一个计数器,来一个吃香饽饽的就+1,吃完一个就-1,减到0了就释放。

手动引用计数(MRC, Manual Reference Counting)

或称为MRR(Manual Retain Release)。这算是最基本的引用计数操作。

  • +1
    对应的方法就是retain。但除此之外,由alloc、new、copy、mutableCopy这样直接生成对象的方法会直接让引用计数自动+1,加给谁?就是=左边的那位。
    所以像前面的这种
    1
    ClassName* instance = [[ClassName alloc] init];

instance所指向的对象引用计数会直接+1。
你说我没有左边那位是不是也要+1呀?是!
如果你直接这样写:

1
[[ClassName alloc] init]

等你去编译的时候,编译器就给你改成

1
2
id tmp = [[ClassName alloc] init]
[tmp autorelease]

是的,他就偏往=左边临时放一位,id相当于C语言中的void *指针,这里不谈。autorelease方法本篇也不谈,你可以暂时把它看成release也就是我下面-1部分要说的。
就像前面的香饽饽情形中,香饽饽M是A做的,A和B都要吃,C只看不吃:

1
2
3
ClassName* pointerA = [[ClassName alloc] init];  // 引用计数+1
ClassName* pointerB = [pointerA retain]; // 引用计数+1
ClassName* pointerC = pointerA; // 引用计数不改变,C:我就静静的看着你们吃

引用计数为2,也就是有两位正在享用香饽饽。

  • -1
    吃完了,就别直接收拾了,得看看其他人还有没有吃完是我们中华民族的传统美德,为了经这种美德延续下去,你调用个release就行啦(但别忘了,赋nil)。
    A吃完了:
    1
    2
    [instanceA release];  // 引用计数-1
    instanceA = nil;

B吃完了:

1
2
[instanceB release];  // 引用计数-1
instance = nil;

当引用计数减为0时,就释放空间,也就是收拾剩饭。
为什么要后面赋nil:因为release之后,指针还是指向那片空间,如果等那个空间被释放了,一不小心又通过那个指针(这时这个指针有了一个响当当的名字:悬垂指针,详见注释2)访问了一下那片空间,那就很危险了。如果那片被回收的区域没有被其他程序覆写,那还好,你可能还侥幸能访问到那里的东西。但如果那片空间被其他人覆写了,这就是非法访问了,非法访问的后果就是crash!
C说了:我要搞事情!

1
2
[instanceC release];
instanceC = nil; // 搞完事就跑路真刺激!

行不行?还真可以!无论是编译阶段还是运行时他都不会报错的。这就等于C在饭局中冒充一个用餐的喊了句:我吃完啦!最后计数器一算:噢,客人都吃完走了,可以收拾饭桌了。结果最后真正还在吃的那位:MMP!
程序中是这样的:

1
2
3
4
5
6
[instanceA release];  // 引用计数-1
instanceA = nil;
[instanceC release]; // 引用计数-1,此时引用计数已经为0了,自动调用dealloc
instanceB = nil;
[instanceB release]; // 很有可能还没执行到这一步程序已经崩溃了,因为B先生开始发疯:我的饭呢?!
instanceC = nil;

就算B先生恰好也吃完了,在他说“我吃完了”的时候,发现没人鸟他了,这时B先生又疯了!所以程序又崩了。
这个疯了在程序里指的就是B先生变成了悬垂指针。

另外,如果你想查看引用计数就调用[instance retainCount],它会返回一个无符号整形,也就是引用计数的值。
你会发现当减到0的时候,如果你再调用retainCount的话,其值仍然为1,首先你这样做是非常危险的,因为他已经被释放了,你现在是在访问悬垂指针。如果能访问到值而没有崩溃当然是因为那块被回收的空间还没有被覆写,其次它的计数值没有变为0是因为没有必要了,这块空间已经被释放了,也没必要去给它计算多一次,而且这个1本是你不该知道的!

至此,你可以发现MRC还是很屌的,只要你别犯贱,一般不会出现什么问题了,也完美解决了之前的香饽饽问题。

自动引用计数(ARC, Automatic Reference Counting)

自动引用计数的出现减少了很多的内存管理代码,从而交给编译器自动去做。是的,这玩意儿是编译器层面的(但不完全是,也需要运行时的帮助,但认识它,透过它编译时所做的事就够了)。

所有权修饰符
  • __strong
    强修饰符,默认修饰。修饰对象属性的时候写成@property(strong)
    1
    2
    3
    4
    5
    {
    id creator = [[NSObject alloc] init]; // 这个也是__strong修饰的,默认修饰
    id __strong strongRef = creator;
    // something else
    }

相当于MRC中的

1
2
3
4
5
6
7
{
id creator = [[NSObject alloc] init];
id strongRef = [creator retain];
// something else
[strongRef release];
[creator release];
}

意思就是,只要是给我strong引用赋值的,我都会retain那个对象。
而且那个__strong可以不写出来,因为它是在ARC下默认的所有权修饰符。
而且在这个修饰的变量在其生命周期结束后,会自动调用release。并不是动态的调用release,而是ARC会帮你在那个生命周期结束的位置自动写上release。

  • __weak
    弱修饰符,修饰对象属性的时候写成@property(weak)。带有此修饰符的指针变量所指向的对象被释放后自动赋nil。本修饰符在iOS4以上支持,若是之前的版本就用__unsafe_unretain修饰符代替(对应的属性修饰为@property(assign))。但是它不会自动赋nil,所以它是不安全的。
    1
    2
    3
    4
    5
    {
    id creator = [[NSObject alloc] init];
    id __weak weakRef = creator;
    // something else
    }

相当于MRC中的

1
2
3
4
5
6
7
{
id creator = [[NSObject alloc] init];
id weakRef = creator;
// something else
weakRef = nil; // 若引用所指向的对象的引用计数为0后就自动赋nil
[creator release];
}

总的来说就是ARC帮你在编译时写了MRC要写的东西,MRC中的那些方法在ARC是不支持的(retain、release、autorelease、dealloc、retainCount不可以人为调用,因为ARC已经帮你写好了)。所以并不是说有了ARC就完全不用担心内存管理,有些需要在运行时管理的内存还是不能太依赖ARC。它只是帮你从那些一般的MRC操作中释放出来。


结语

可以发现从传统到MRC再到ARC是一层一层进化而来,在传统上添加一个引用计数器就是MRC,编译器接手引用计数就变成了ARC。
除此之外上述之外,还有autoreleasepool,本文不提。有兴趣的朋友自行翻资料吧,这篇文章只是在Objective-C内存管理方面带你入个门。


注释

  1. 野指针(Wild Pointer):在声明指针变量时,有些开发语言并不会帮你自动赋nil。所以这个指针可能在刚声明的时候是指向一个迷の区域的。这样的指针就是野指针。
  2. 悬垂指针(Dangling Pointer):悬垂指针就是本指向一个曾经有对象(该指针曾经指向的对象)的空间,但现在是被回收的空间。
  3. 内存泄漏(Memory Leak):内存泄漏指的是内存中存在一些对象实体,他们占用空间,却没有任何指针变量指向他们,所以他们不会被释放掉。

LOX: 利用VSCode进行C/C++开发

Posted on 2017-06-07 | Edited on 2019-03-01 | In LOX

文章框架

脑图



前言

LOX(Lightweight Open-source Xplatform)是我正在编写的一个全新文集,意思就如其名:轻量级-开源-跨平台。就是这个系列中的文章和例程都符合轻量级(Lightweight)、开源(Open-Source)、跨平台(Cross-Platform)。嗯,我这人吧,就好起一些新的名词。
本篇作为LOX系列的第一篇,也向你展示了一个最简单的LOX项目。如果想看一个稍微复杂一点的LOX项目,可以clone我Github上的一个Repo:2048 CLI。欢迎来搞~


单源文件

先从简单的来,一个源文件。

工具

进行本次LOX开发所需要工具,点击下面的Title进入官网下载。

  • VSCode (Visual Studio Code)

    VSCode是微软出品的一款非常良心的轻量级编辑器,虽说是完全照着Sublime Text弄出来的,但它免费啊,还开源啊,所以我也就不深究模仿不模仿了。(嗨呀,原则呢?嗯?)

需要的插件

插件 标识符 说明
C/C++ ms-vscode.cpptools C/C++语言支持,简单的编辑、编译、调试等功能。
  • MinGW (Minimalist GNU for Windows)

    这个工具是在Windows上需要的。因为Windows没有GNU工具包,MinGW就是为Windows而生的GNU工具包。

需要安装的包

包 说明
mingw32-gcc.bin GNU C编译器
mingw32-gcc-g++.bin GNU C++编译器
mingw32-gdb.bin GNU 调试器

环境变量
在系统变量的PATH中,添加MinGW安装的根目录中的bin文件夹(如C:\MinGW\bin),且尽量将这一项往上移(为了优先搜索该目录)。

设置过程
完成这一步后,如果打开了VSCode,记得要重启VSCode。


编码

建立工作空间

新建一个文件夹 - 右键 - Open with Code。使用这种方法的前提是你在安装VSCode时选中了“”
Open with Code
或者在VSCode中 - 文件 - 打开文件夹,选中一个空文件夹作工作空间。

新建源文件
鼠标移动到新建的文件夹的根目录处(我的是“HelloWorld”),点击下图蓝色框住的图标(新建文件)。
新建按钮
之后在其中输入文件名(包括后缀名),本例的后缀名写”.c”或”.cpp”。然后在其中写个最简单的HelloWorld。

代码

1
2
3
4
5
#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}

编码时,如果想自动补全,需要在包含指定头文件后保存代码,然后才会启动自动补全。
也就是说,它只对保存后的代码进行探测。


生成

生成快捷键:Ctrl(Mac: Command)+Shift+B
第一次生成时,VSCode会提示找不到生成的配置文件task.json,点击“配置生成任务”。
配置生成任务
在给出的模板中选择Others。在打开的task.json中改为下述代码。

1
2
3
4
5
6
7
{
"version": "0.1.0",
"command": "gcc",
"isShellCommand": true, // 是否为Shell命令
"args": ["-g","${file}","-o","${fileDirname}/${fileBasenameNoExtension}.out"],
"showOutput": "always"
}

其实作用就是代码中所述的。
"command": "gcc"和"args": ["-g","${file}","-o","${fileDirname}/${fileBasenameNoExtension}.out"]就相当于在Shell)中直接键入gcc -c 源文件.c -o 源文件所在目录/源文件.out。将鼠标放在Key(冒号前面的)上会显示所对应的意思(下面的Launch.json同)。
简单的解释一下其中几个$标识

标识 作用
${file} 当前文件的完整文件名(包括路径、文件名、后缀名)
${fileDirname} 当前文件的路径
${fileBasenameNoExtension} 当前文件的文件名(不包括路径、后缀名)

其中所说的“当前文件”指的是你在VSCode编辑器中打开的并正在编辑的文件。
当前正在编辑的文件

完成task.json的配置后,我们缩写的程序已经可以运行了。生成的程序就是源文件所在根目录中出现的与源文件同名但后缀名为.out的文件。


调试

我们当然不能仅仅满足于生成出程序就行的,大部分情况我们是要调试的。

调试快捷键:F5

第一次按F5时和生成时一样,找不到配置文件,在打开的模板中选C++ (GDB/LLDB)(可以跨平台)。
生成的配置文件为launch.json

将launch.json改为下述代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch", // 本配置的名称,随便起
"type": "cppdbg",
"request": "launch", // 如果调试的类型为附加进程,需将这里改为attach
"program": "${fileDirname}/${fileBasenameNoExtension}.out", // 要调试的程序路径
"stopAtEntry": false, // 是否在起点处停顿
"cwd": "${workspaceRoot}",
"externalConsole": true, // 在外部控制台运行。若为false,则运行在VSCode自带的控制台中
"linux": { // Linux 系统下的配置
"MIMode": "gdb"
},
"osx": { // OS X系统下的配置
"MIMode": "lldb"
},
"windows": { // Windows 系统下的配置
"MIMode": "gdb",
"miDebuggerPath": "gdb.exe"
}
}
]
}

Windows默认是没有gdb或lldb的,所以我们需要安装MinGW中的gdb,并在这里设置gdb的路径。只写程序名是因为设置了环境变量。
完成launch.json的配置并切换到源代码编辑页之后

再一次:F5

打上断点

合影留念



多源文件

什么!你不满足于单源文件开发?!来来来,我给你看个宝贝。

的确,单源文件开发一般也就是个做做算法题,现在随便写个像样的工程都是多个源文件编译链接一条龙的。这样VSCode可以吗,也可以!但需要新的帮手:CMake。

工具

  • VSCode (Visual Studio Code)

新需要的插件
实际上通过CMake在VSCode上进行项目开发可以不需要任何插件,但是用上这些插件之后你会发现这个过程会变得特别方便!
除去单源文件中所提到的cpptools,还需要下面的插件。

插件 标识符 说明
CMake twxs.cmake 提供CMake语法支持,包括高亮和自动补全等
CMake Tools vector-of-bool.cmake-tools 这个屌!完全把CMake封装成一套VSCode底边栏的工具集
  • MinGW (Minimalist GNU for Windows)

    同样这一步仅在Windows上做。

需要新安装的包

包 说明
mingw32-make.bin GNU Make,根据makefiles(在这里makefiles由CMake搞定)生成项目

为了方便调用,我一般会把mingw32-make.exe在其目录中复制一份出来,命名为make.exe。
这一步某种程度意义上讲还蛮重要的,第一是方便自己调用(直接在shell里make),第二是方便vscode的插件调用(如果不弄一个cmake.exe出来的话,vector-of-bool.cmake-tools会报错,不过具体锅是vector-of-bool.cmake-tools还是twxs.cmake的也不太清楚,目前vector-of-bool已经把这个问题(#157)标记为bug,并打算在0.10.0版本解决。)。

  • CMake

    安装时记得将Add CMake to system PATH勾上。是for all users呢还是for current user你自己看咯。

编码

上个例子是C,那这个就上C++好了。

目录结构

我就不同目录下多文件了啊,那没啥意思。咱上多层级目录多源文件的。
目录结构

代码

  • Printer.h
1
2
3
4
5
6
7
#pragma once
class Printer{
public:
void print();
Printer();
~Printer();
};
  • Printer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Printer.h"
#include <iostream>
using namespace std;
void Printer::print(){
cout<<"Hello World"<<endl;
}

Printer::Printer() {
cout<<"Printer Object Constructed"<<endl;
}

Printer::~Printer(){
cout<<"Printer Object Destructed"<<endl;
}
  • ‘main.cpp’
1
2
3
4
5
6
7
#include "Lib/Printer.h"
int main(){
Printer* printer = new Printer();
printer->print();
delete printer;
return 0;
}

生成项目

编写CMakeLists

每一个包含源文件的目录中都要编写CMakeLists.txt

  • CMakeLists.txt
    根目录中的CMakeLists.txt,一般程序入口(main函数)在此。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 使用CMake Tools插件(可选,如果这个项目去到一个没有这个插件的机器也同样可以生成项目)
include(CMakeToolsHelpers OPTIONAL)

# CMake 最低版本号要求
cmake_minimum_required(VERSION 2.8)

# 项目名称
project(CMakeTest)

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_ROOT_SRCS变量
aux_source_directory(. DIR_ROOT_SRCS)

# 添加 Lib子目录
add_subdirectory(Lib)

# 指定生成目标
add_executable(CMakeTest main.cpp ${DIR_ROOT_SRCS})

# 添加链接库
target_link_libraries(CMakeTest PrinterLib)
  • Lib/CMakeLists.txt
    子目录中的CMakeLists.txt,一般将子目录中的源文件编译为静态链接库。
1
2
3
4
5
6
7
8
include(CMakeToolsHelpers OPTIONAL)

cmake_minimum_required(VERSION 2.8)

aux_source_directory(. DIR_LIB_SRCS)

# 生成链接库
add_library(PrinterLib ${DIR_LIB_SRCS})

Build

完成上面步骤后,就可以Build了,如果你安装了vector-of-bool.cmake-tools插件,VSCode左下角的底边栏会有Build按钮。
Build Button
点击Build后,选择Debug(为了下一步演示调试,若不调试就Release)
Build Setting
输出框会不停往出喷东西,只要最后输出了[vscode] cmake exited with return code 0,就说明Build成功。
Build Output

目录结构

目录结构
新增加的build文件夹是vector-of-bool.cmake-tools插件干的,它做的很好,如果我们手动cmake - make,那些生成文件会跑的到处都是。
其中build/CMakeTests.exe就是我们生成的可执行程序。


调试

调试设置

文件 - 首选项 - 设置
在CMake Tools configuration中找到cmake.debugConfig,生成设置:

1
2
3
4
5
"cmake.debugConfig": {
"miDebuggerPath": "gdb.exe", // Windows 下指定gdb路径(已添加到PATH)
"externalConsole": true, // 使用外部控制台
"stopAtEntry": false // 在起点处停顿(噢!在这停顿!)
},

Start Debugging

藏起来的Debug按钮

刚Build完你可能看不到这两个按钮:
调试按钮
其实他们藏起来了… 这应该是个Bug,我已经反馈给作者了。

重启VSCode后他们就会出现,或者直接点那两个藏起来的按钮(还有这种操作?!):先点右边选择调试目标,再点左边开始调试。

合影留念

完结撒花



结语

Code Everywhere, Build Everywhere.
这样的搭配是不是很爽呢?除了MinGW是Windows特有的矫情外。其余的在任何平台都是一样的。虽说现在VSCode和上面的大部分插件都运行的不太稳定,但是我还是很喜欢这种组合进行跨平台的轻量级开发的。


FAQ

  • ####Q: VSCode或编译器找不到头文件?
    A: 如果VSCode用绿色波浪线给你划出那些找不到源的代码时,将鼠标停留在波浪线上,然后会出现一个小 灯泡。点“Add include path to settings.”。然后它会自动帮你建立一个名为“c_cpp_properties.json”的文件。
    1
    2
    3
    4
    5
    6
    "name": "Mac",
    "includePath": [
    "${workspaceRoot}",
    "/usr/include",
    "/usr/local/include"
    ],

你会在其中找到类似上述代码块的地方,根据“name”后面所描述的系统,在其下的“includePath”中手动添加头文件目录。JSON语法。

  • ####Q: 为什么路径动不动就写成${fileDirname}/${fileBasenameNoExtension}.out?
    A: 这样做的好处就是可以在同一配置下进行多个单文件的编译开发,不用每次生成和调试的时候都去写配置文件。使用场景嘛,主要就是写写算法题之类的。
    我写算法题时的目录结构
    .out后缀名就是自己瞎起的。不同平台的可执行后缀名都不一样,这样写就跟谁都不沾边了…

  • ####Q: 如果我不用vector-of-bool.cmake-tools插件,要如何Build?
    A: 写完CMakeLists.txt后,打开VSCode中自带的终端(点击底边栏的输出按钮)

输出按钮

终端
键入:cmake .会生成系统对应的项目(如果你是Windows并安装了Visual Studio,他就会生成VS项目)。
若想生成MinGW Makefiles则键入cmake -G "MinGW Makefiles" .(注意区分大小写),之后会为你生成makefiles,然后再键入make(若没制作前面所提到的mingw32-make.exe的名为make.exe的副本,则键入mingw32-make),最终生成可执行文件。

参考

  • Doc: C/C++ for VS Code (Preview)
  • CMake 入门实战

51单片机实战:物联网初步のESP8266无线网络模块

Posted on 2017-02-10 | Edited on 2019-03-01 | In 扯会儿单片机开发

文章框架

文章框架



前言

扯单一周目BOSS出现!请速速讨伐!
ESP8266是我第一个,也是唯一一个接触过的无线网络模块,我非常喜欢!大部分网上的教程都是ARM架构单片机配用ESP8266。我比较抠门,给大家上51版的。
我记得当时和老板承诺可以做成无线通信的时候我也不是很确定,心里一直打鼓。好在最后做出来了,很有成就感!所以今天的代码案例是我把我之前在公司写的项目代码简化之后分享给大家的。虽然结构有点恶心,但应该还是入得了眼的。
如果有没看懂的地方,大家评论区告诉我。

需求

利用ESP8266芯片,通过无线网络同电脑建立TCP连接。接收电脑传来的消息并显示在LCD上。

每条信息以\结尾。
每条信息字符数不超过32。
信息内容只能是ASCII表内的字符。


清单

硬件

学习板
  • 简介
    由于这次要用到WIFI模块,没法用Proteus模拟,所以就上真家伙。
    自己焊板子就太麻烦了,所以就买学习板(其实我不会焊)。
    QX-MINI51
    这是我入的学习板,你也可以入其他的,如果把握不大就跟我入一样的。
    这个是我在淘宝上小挑了一阵子选的板子:QX-MINI51。尺寸不大不小,想玩的基本都有。配备的单片机是STC89C52。

    淘宝链接

  • 电路
    下面是QX-MINI51在本节会用到的电路图
    STC89C52
    使用89C52芯片,和我们之前用的单片机一样。
    供电&USB串口
    CH340芯片是用于普通串口和USB转换的,其上的RXD为串口输入,TXD为串口输出。UD+和UD-对应USB数据线。
    虽说是用了USB,其实和普通串口通信线用法一致,只是人家成品方便我们使用封装成了USB。这里不深究。
    流水灯
    流水灯在本节中的角色是辅助调试(具体的看后面),这里有用的信息就是让你知道这8个流水灯接哪了。

ESP8266
  • 简介
    主角,简而言之就是Wi-Fi模块,属于网络层以上的设备。拥有MAC地址和IP地址,支持UDP和TCP。
    ESP8266
    这款的型号是ESP8266-01,其他的和QX-MINI51配合使用不太方便。
    淘宝链接
  • 参数
型号 主芯片 无线标准 工作电压 安全机制 支持模式
ESP8266-01 ESP8266 IEEE 802.11b/g/n 3.3V WEP/WPA-PSK/WPA2-PSK STA、AP、STA+AP

STA 模式:ESP8266模块通过路由器连接互联网,手机或电脑通过互联网实现对设备的远程控制。
AP 模式:ESP8266模块作为热点,实现手机或电脑直接与模块通信,实现局域网无线控制。
STA+AP 模式:两种模式的共存模式,即可以通过互联网控制可实现无缝切换,方便操作。

引脚图
上图对8个针脚进行说明。

UTXD GND CH_PD GPIO2 GPIO16 GPIO0 VCC URXD
发送 接地 高电平工作 \ \ \ 电源 接收

本例只用到这五个引脚(画“\”的不用),其他引脚的说明资料请自行到淘宝链接处下载。

  • AT指令
    操作ESP8266芯片是靠AT指令的,类似之前的操作LCD1602(《51单片机实战:液晶显示器のLCD1602》)。操作LCD1602的指令都是一个字节的十六进制码,比较难记和理解。AT指令是字符串形式的指令,一般都是单词缩写,对可笑的人类比较友好。
    之前和LCD1602交互是靠并口传输,而这次是用串口传输(串口传输简例:《51单片机实战:与计算机异步串行通信》),所以这次的Wi-Fi通信是建立在串口通信基础上的。

    AT指令集下载链接
    AT指令使用示例下载链接

  • 波特率
    注意,这个模块的默认波特率是115200,本例也是根据这个波特率进行演示的。若想改变波特率请使用以下语句进行修改:

1
AT+CIOBAUD=<baudrate>,<databits>,<stopbits>,<parity>,<flow control>

如:

1
AT+CIOBAUD=9600,8,1,0,0

软件

程序 说明 下载
UartAssist 串口调试助手,用来给单片机发送消息 度娘网盘
NetAssist 网络调试助手,在电脑端建立TCP连接与单片机的ESP8266进行通信 度娘网盘
STC-ISP STC单片机工具集,很强大,可烧录程序,可串口调试等等 度娘网盘
CH340驱动 CH340的USB驱动,如果没有这个驱动,你的电脑可能识别不到单片机(识别到CH340就相当于识别到单片机的串口。) 度娘网盘
51单片机波特率初值计算工具 用于计算在各种波特率和晶振频率等参数下计时器的初值,属于辅助工具,省的还得算。 度娘网盘

分析

调试

在开始用单片机直接和无线模块通信之前,首先要绕过单片机直接和无线模块通信以确定其可以使用和接入默认网络(接入一个热点后,模块每次断电后启动都会自动连接该热点),这样可以让单片机少做很多事情(要知道我们用的是51单片机,硬件资源极其有限,能省则省)。

  • TXD/RXD反接
    首先要绕过单片机直接给ESP8266下指令,就要用电脑的串口。但ESP-01是针脚接口,所以我们可以利用QX-MINI51上的针脚和USB接口。

    回看前面QX-MINI51的主控芯片和USB的电路图,可以发现,单片机的串口引脚是被并联式的暴露再外的,分别为:P30 - P31和USB,前者为针脚接口,后者为USB接口。且它们的RXD和TXD的数据信息是一摸一样的,注意并不是两个独立的串口,是同一个串口。比方说,单片机要往出发送一个1,P30针脚和USB都会是往出发送一个1。

    所以我们利用这个特点,将ESP8266的TXD接到P31(单片机的TXD),RXD接到P30(单片机的RXD),这样就等于让ESP8266直接和USB打交道,也就是和电脑直接打交道了。

    为什么叫反接,我一般管RXD对TXD叫正接。嗯…
    接线图
    强势秀一波画工!
    我觉得应该可以看得懂吧,ESP8266的引脚说明请看前面的引脚图。看红色号码对应接线,其中4、5、6号引脚不用。

实物图
这个图就很难看出线是怎么连的了,所以你要忍受我丑陋的画工。

  • 接入网络
    打开串口调试助手,调好参数
    串口调试助手,设置参数
    其中串口号你连的哪个串口就设置哪个串口号,我是连的COM3。
    另一个要注意的就是波特率要115200(ESP-01的默认波特率,也可以统一改为9600)。
    打开串口后,给开发板上电。你的串口调试助手会有信息出来(文本显示,不要十六进制显示)。
1
2
3
4
5
?諄MEM CHECK FAIL!!!
d{$弬s
Ai-Thinker Technology Co. Ltd.

invalid

显示的信息类似上面,你可以先给个测试命令AT看看是否可以接受指令

注意!每个指令后要跟回车再发送!

如果返回OK则说明指令可以被接收并识别。

如果下面的指令都会返回ERROR(在没有给错指令的情况下),可以尝试AT+RST重启模块。

1. 更改模式
指令:AT+CWMODE?
一般情况下,第一次使用会返回2,也就是AP模式,我们不用它发热点,所以要改回Station模式(模式1)。

指令:AT+CWMODE?
一般情况下,第一次使用会返回2,也就是AP模式,我们不用它发热点,所以要改回Station模式(模式1)。

指令:AT+CWMODE=1
返回:OK则成功

2. 接入热点(连Wi-Fi)
指令:AT+CWJAP=<SSID>,<Password>
参数:处填写热点名称,处填写密码。两者都要用双引号括起来。
例如:AT+CWJAP="CMCC","123456"

指令:AT+CWJAP=<SSID>,<Password>
参数:处填写热点名称,处填写密码。两者都要用双引号括起来。
例如:AT+CWJAP="CMCC","123456"

返回:WIFI CONNECTED:连接到热点
返回:WIFI GOT IP:分配到IP,走到这一步,就算已经连入到热点了。

如果忘记SSID了,想看一下可以使用下面的指令,列出广播的SSID(隐藏的不会显示)。
指令:AT+CWLAP

3. 连接TCP服务器
首先打开NetAssist,设置TCP Server,然后建立连接(注意防火墙)。注意,Server必须在Client所在内网或其外网(我的是在同一个内网)。
网络调试助手

首先打开NetAssist,设置TCP Server,然后建立连接(注意防火墙)。注意,Server必须在Client所在内网或其外网(我的是在同一个内网)。
网络调试助手

指令:AT+CIPSTART=<Type>,<DomainName>,<Port>
参数:处写TCP或UDP,处写域名,处写端口号。
本例:AT+CIPSTART="TCP","192.168.1.110",1234
返回:

1
2
3
CONNECT

OK

说明连接成功。
在NetAssist中,数据接收框的下面有一个连接对象,点开后发现除了All Connections之外,多了一个客户,就确定客户连接到服务器了。
NetAssist
在下方文本框输入信息后发送,可在串口调试助手中看到ESP8266所接收到的信息
UartAssist收到的信息:+IPD,10:Hello 简书

到这里就说明网络连接及建立TCP都可以顺利完成,在下面的单片机操作中就会变得方便很多。

注意,ESP8266每次断电后重新上电,最多只会自动连到之前连接的热点,但不会自动连接到TCP服务器,所以,建立连接要交给单片机来做。

问题

  • 波特率
    本例要使用的波特率为115200的,不是9600。引申出来的问题就是:算初值(方法详见:《51单片机实战:与计算机异步串行通信》 - 知识点 - 波特率 - 溢出率)。
    手算真的很麻烦,因为在参数固定的情况下,算起来已经很烦了,更何况那些参数可能还会变得情况。
    所以上神器:
    初值计算器

  • 反馈及识别
    我们要做的是让单片机在收到ESP8266回馈的WIFI GOT IP后发出连接TCP服务器的指令。
    发送指令不用多说,只要记得在后面跟”换行(CR)”和”新行(NL)”字符就行(这两个是不一样的)。
    1. 关键字符配对
    如果是电脑程序的高级语言编程,这个问题就不存在了。但是我们给单片机写程序就要牢记它的一些点(《扯会儿单片机开发:开始》),比如硬件资源十分紧张。
    一个是因为它存储空间很小,另一个是因为晶振太慢,所以我们要想办法缩减配对时间。因为单片机的使用往往很单一,所以策略都是根据情况来设定的。比如本例,单片机接收的回馈无非那么几种,所以我制定的策略就是关键字符配对。

    比如识别WIFI GOT IP,我只是别第一个字符W和第6个字符G,就说明我收到的就是这个回馈信息。如果收到第一个是W,只有这条指令的第六个字符是G,所以就可以确定。如果是出现干扰,可能性也是比较低的,我们的交互是在网络层的,下层也可以把出错的报文挡掉。

    总体来说还是蛮可靠的,如果你有更棒的方法,请在评论区告诉我,大家一起学习进步。

    2. 利用流水灯
    上面就是理论工作,已经做得7788了。但实际经验告诉我,如果的代码哪里也出岔子了在51单片机开发中真的比较难发现,他不像高级程序语言可以报错,可以try - catch。但这个就别想了,所以我们要自己想办法,让他可以反馈我们的程序至少是正常运行的,这样可以节省很多时间。

    我在这里介绍的我的方法是利用LED,或成组的流水灯(更好)。承租的流水灯一般是8个,可以直接显示一个字节的信息,可能有人问为什么不用LCD?你若会LCD或者看过《51单片机实战:液晶显示器のLCD1602》就会发现,LCD本身的开发就有点复杂了。你很难保证这个子程序运行正常,更别提用它抓错了。只能是不推荐哈,我觉得不可靠。LED的话就很简单了,就是个关开,一般不会出错。

    QX-MINI51开发板上自带流水灯,你要是入的其他开发板,一般都是有的。如果没有,就单买LED回来(如果不知道去哪买,链接)。

    我一般会怎么做呢,先让流水灯直接显示串口接收的数据,为了知道我们至少是能接收到东西的。然后利用上面的关键字符识别,收到WIFI CONNECTED亮一号灯,收到WIFI GOT IP亮二号灯,收到CONNECT OK亮三号灯。这样,根据灯亮灭的情况就能确定哪一步出了问题,然后定位到代码或者设置。

    还是那句话,如果你有更棒的方法,欢迎在评论区交流。

到这里就做完了所有理论上的准备工作,也就是理论上我们已经可以实现这个程序了,下面代码实现。


代码

说下这次代码比较新颖的地方,一个是用到了.c和.h文件组合的模块形式,另一个是用到了函数指针(函数名也被括起来的那个)。前者这种写法是为了将各子功能模块化,.h文件里的内容相当于面向对象编程里的public,.c文件一个是实现.h内的函数,另一个就是隐藏函数和变量,相当于private。后者是用来充当高级语言中的“事件”,让函数调用变得更加灵活,其具体用法请自己查资料或者看相关C语言书籍。

总而言之,这次的代码是模块化和事件化的,类似面向对象的编码风格。

  • 准备
    因为用于通讯,所以我把ASCII内所有的特殊字符都写到一个头文件里。虽然本例只用到其中的几个,但以后重复利用这个头文件。

    ASCII.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#ifndef __ASCIIS__
#define __ASCIIS__

#define NUL 0x00 // NULL
#define SOH 0x01 // Start of Heading
#define STX 0x02 // Start of Text
#define ETX 0x03 // End of Text
#define EOT 0x04 // End of Transmission
#define ENQ 0x05 // Enquiry
#define ACK 0x06 // Acknowledge
#define BEL 0x07 // Bell
#define BS 0x08 // Backspace
#define HT 0x09 // Horizontal Tab
#define LF 0x0A // Line Feed
#define NL 0x0A // New Line
#define VT 0x0B // Vertical Tab
#define FF 0x0C // Form Feed
#define NP 0x0C // New Page
#define CR 0x0D // Carriage Return
#define SO 0x0E // Shift Out
#define SI 0x0F // Shift In
#define DLE 0x10 // Data Link Escape
#define DC1 0x11 // Device Control 1
#define DC2 0x12 // Device Control 2
#define DC3 0x13 // Device Control 3
#define DC4 0x14 // Device Control 4
#define NAK 0x15 // Negative Acknowledge
#define SYN 0x16 // Synchronous Idle
#define ETB 0x17 // End of Transmission Block
#define CAN 0x18 // Cancel
#define EM 0x19 // End of Medium
#define SUB 0x1A // Substitute
#define ESC 0x1B // Escape
#define FS 0x1C // File Separator
#define GS 0x1D // Group Separator
#define RS 0x1E // Record Separator
#define US 0x1F // Unit Separator
#define SP 0x20 // Space

#endif
  • LCD
    这里都是关于LCD1602显示器的主要代码,与文章《51单片机实战:液晶显示器のLCD1602》所讲的一样,这里就不给出注释了。

lcd1602.h

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef __LCD1602__
#define __LCD1602__

typedef bit BOOL;

void LCD_writeCmd(unsigned char cmd); //写命令
void LCD_writeData(unsigned char dat); //写数据
void LCD_writeLine(unsigned char *line); //写行数据
void LCD_init(); //初始化
void delay(unsigned int z); //粗略的延时器

#endif

lcd1602.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <reg52.h>
#include "lcd1602.h"

#define LCD_CLEAR 0x01
#define LCD_Display_Mode 0X38

#define DISPLAY_OFF 0x08
#define DISPLAY_ON_NO_CURSOR 0x0c
#define DISPLAY_ON_WITH_CURSOR_NO_BLINK 0x0e
#define DISPLAY_ON_WITH_CURSOR_BLINK 0x0f

#define AUTO_BACK_STEP 0x04
#define AUTO_NEXT_STEP 0x06
#define AUTO_DISPLAY_MOVE_LEFT 0x07
#define AUTO_DISPLAY_MOVE_RIGHT 0x05

#define ALL_MOVE_LEFT 0x18
#define ALL_MOVE_RIGHT 0x1c
#define CURSOR_MOVE_LEFT 0x10
#define CURSOR_MOVE_RIGHT 0x14

#define FIRST_ROW 0x80
#define SECOND_ROW FIRST_ROW+0x40

sbit enable = P0^5;
sbit RS = P0^7;
sbit RW = P0^6;

void delay(unsigned int z)
{
unsigned int x,y;
for(x=z;x>0;x--)
for(y=220;y>0;y--);
}

void LCD_writeCmd(unsigned char cmd){
RS = 0;
P2 = cmd;
delay(5);
enable = 1;
delay(5);
enable = 0;
}

void LCD_writeData(unsigned char dat)
{
RS = 1;
P2 = dat;
delay(5);
enable = 1;
delay(5);
enable = 0;
}

void LCD_writeLine(unsigned char *line){
unsigned char i=0;
BOOL flag = 0;
LCD_writeCmd(LCD_CLEAR); //每次送来信息都清屏,可以一直刷新显示送来的信息。
LCD_writeCmd(FIRST_ROW);
while(line[i] != '\0'){
LCD_writeData(line[i++]);
if(i>15 && flag == 0){
LCD_writeCmd(SECOND_ROW);
flag = 1;
}
delay(5);
}
}

void LCD_init()
{
RW = 0;
enable = 0;
LCD_writeCmd(LCD_Display_Mode);
LCD_writeCmd(DISPLAY_ON_NO_CURSOR);
LCD_writeCmd(AUTO_NEXT_STEP);
LCD_writeCmd(LCD_CLEAR);
}
  • 串口通信
    这里用于单片机和ESP8266交互,与文章《51单片机实战:与计算机异步串行通信》相似。其中的不同点前面已经说过。

stc52ser.h

1
2
3
4
5
6
7
8
9
10
11
#ifndef __STC52_SER__
#define __STC52_SER__

extern void (*SerialPort_Event_ByteReceived)(unsigned char byte); //事件:串口接收到字节

void SerialPort_Init_Low(); //初始化为11.0592MHz下的9600波特率
void SerialPort_Init_High(); //初始化为22.1184下的115200波特率
void SerialPort_SendByte(unsigned char byte); //发送一个字节
void SerialPort_SendData(unsigned char* bytes); //发送一组字节

#endif

stc52ser.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <reg52.h>
#include "ASCIIS.h"
#include "stc52ser.h"

//Byte Received Event
void (*SerialPort_Event_ByteReceived)(unsigned char byte);

//initialize registers pertinent to serial port
void SerialPort_Init_Low(){
//set and run Timer1
//mode2: 8bit, auto reload initial value
//9600bps and 11.0592MHz => 0xfd(initial value)
TMOD = 0x20;
TH1 = 0xfd;
TL1 = 0xfd;
TR1 = 1;

//set serial port configuration and enable receive
//mode1: asyc 10bit(8 data bit), alterable baud rate
SM0 = 0;
SM1 = 1;
REN = 1;

//set interruption
//enable all and serial port interruption
EA = 1;
ES = 1;
}

//initialize registers pertinent to serial port for esp8266
void SerialPort_Init_High(){
//SMOD = 1
PCON |= 0x80;

//set and run Timer1
//mode2: 8bit, auto reload initial value
//115200bps and 22.1184MHz => 0xfd(initial value)
TMOD = 0x20;
TH1 = 0xff;
TL1 = 0xff;
TR1 = 1;

//set serial port configuration and enable receive
//mode1: asyc 10bit(8 data bit), alterable baud rate
SM0 = 0;
SM1 = 1;
REN = 1;

//set interruption
//enable all and serial port interruption
EA = 1;
ES = 1;
}

//Send a byte
void SerialPort_SendByte(unsigned char byte){
ES = 0;
SBUF = byte;
while(!TI);
// transmit interrupt
TI = 0;
ES = 1;
}

//Send a data of byte sequence end by 'EOT'
void SerialPort_SendData(unsigned char* bytes){
int i = 0;
while(bytes[i] != EOT){
SerialPort_SendByte(bytes[i]);
i++;
}
}


//Occured when byte received
void receivedInterruped() interrupt 4 {
TR0 = 0;
(*SerialPort_Event_ByteReceived)(SBUF);
while(!RI);
RI = 0;
}

简单说一下,这里留了两个初始化函数,SerialPort_Init_Low()用于11.0592MHz下的9600波特率,SerialPort_Init_High()用于22.1184MHz下的115200波特率(此例用这个)。这样写只是为了以后可以重用(软工狗的矫情)。

  • 无线
    这里是最主要的代码,都是关于ESP8266的,也是作为一个模块给主函数调用。

esp8266.h

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef __ESP8266__
#define __ESP8266__

extern void (*ESP01_Event_WifiConnected)(); //事件:Wi-Fi已连接
extern void (*ESP01_Event_IpGot)(); //事件:IP地址已获得
extern void (*ESP01_Event_TcpServerConnected)(); //事件:已连接到TCP服务器
extern void (*ESP01_Event_MsgReceived)(unsigned char* head); //事件:已获得消息,head为消息数组头

void ESP01_Init(); //无线模块初始化
void ESP01_ConnectToTCPServer(); //连接TCP服务器

#endif

esp8266.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#include <reg52.h>
#include "stc52ser.h"
#include "ASCIIS.h"
#include "esp8266.h"

#define BUFFER_MAX_SIZE 99 //缓冲区大小
unsigned char buffer[BUFFER_MAX_SIZE]; //缓冲区:用于存放从ESP8266接收来的各种信息

//连接到TCP服务器的指令:AT+CIPSTART="TCP","192.168.1.110",1234。后面的CR和NL是AT指令的固定结尾,EOT用于SerialPort_SendData发送时识别结尾。
code unsigned char cmd_connectToTCPServer[] = {0x41, 0x54, 0x2B, 0x43, 0x49, 0x50, 0x53, 0x54, 0x41, 0x52, 0x54, 0x3D, 0x22, 0x54, 0x43, 0x50, 0x22, 0x2C, 0x22, 0x31, 0x39, 0x32, 0x2E, 0x31, 0x36, 0x38, 0x2E, 0x31, 0x2E, 0x31, 0x31, 0x30, 0x22, 0x2C, 0x31, 0x32, 0x33, 0x34, CR, NL, EOT};
int counter = 0; //用于ESP8266的执行步骤计数
int writeIndex = 0; //缓冲区写索引

void (*ESP01_Event_WifiConnected)();
void (*ESP01_Event_IpGot)();
void (*ESP01_Event_TcpServerConnected)();
void (*ESP01_Event_MsgReceived)(unsigned char* head);

//注意:下面代码推荐从后往前看,从注释标"1. "处开始。

void prepareForData(unsigned char byte); //因为第四步和第三步会相互调用,所以这里只是做了个声明(C语言的矫情点)。

//4. 将信息插入到缓冲区并送给单片机。
void insertDataIntoBuffer(unsigned char byte){
if(byte == '\\'){
//检测到'\'后,将信息送出到单片机
buffer[writeIndex] = '\0';
(*ESP01_Event_MsgReceived)(buffer);
SerialPort_Event_ByteReceived = &prepareForData; //回到第三步,准备接收下一条信息
writeIndex = 0;
return;
}
buffer[writeIndex++] = byte;
}

//3. 准备信息:这里是过度步骤,前面可以观察到,ESP8266在接收发来的信息时是有个头的,这里的作用就是去头。
void prepareForData(unsigned char byte){
if(byte == ':'){
SerialPort_Event_ByteReceived = &insertDataIntoBuffer;
writeIndex = 0;
}
}

//识别回馈指令:用于识别接收到的是WIFI CONNECTED(连上热点)还是WIFI IP GOT(获得IP)还是CONNECT(连上TCP服务器)
void parseCmd(){
switch(counter){
case 1:
if(buffer[0] == 'W' && buffer[5] == 'C'){
(*ESP01_Event_WifiConnected)();
counter += 1;
}
break;
case 2:
if(buffer[0] == 'W' && buffer[5] == 'G'){
(*ESP01_Event_IpGot)();
counter += 1;
}
break;
case 3:
if(buffer[0] == 'A' && buffer[3] == 'C')
counter += 1;
break;
case 4:
if(buffer[0] == 'C' && buffer[3] == 'N' && buffer[6] == 'T'){
(*ESP01_Event_TcpServerConnected)();
SerialPort_Event_ByteReceived = &prepareForData; //连接到TCP服务器后,进入第三步。
}
}
}

//2. 这里开始向缓冲区存储信息,用于识别。
void insertBuffer(unsigned char byte){
if(byte == NL){
//收到尾(NL)后,将缓冲区的回馈信息送去识别
parseCmd();
writeIndex = 0;
return;
}
buffer[writeIndex++] = byte;
}

//1. 接收头:头是无用信息,但我们要通过头里面的一些字符,推算出什么时候到达第二步(WIFI CONNECTED)
void headerReceived(unsigned char byte){
if(byte == NL){
//头内有5个NL,只要数够5个,下一个就是第二步的内容了。
if(++counter == 5){
SerialPort_Event_ByteReceived = &insertBuffer; //跳到第二步
counter = 1;
}
}
}

//同.h中的声明
void ESP01_Init(){
SerialPort_Init_High();
SerialPort_Event_ByteReceived = &headerReceived; //事件注册
}

//同.h中的声明
void ESP01_ConnectToTCPServer(){
SerialPort_SendData(cmd_connectToTCPServer);
}
  • 主函数

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <reg52.h>
#include "lcd1602.h"
#include "esp8266.h"

//连接到Wi-Fi后,亮第一个灯
void EventHandler_WifiConnected(){
P1 &= 0xFE;
}

//获得IP后,亮第二个灯
void EventHandler_IpGot(){
P1 &= 0xFD;
ESP01_ConnectToTCPServer();
}

//连接到TCP服务器后,亮第三个灯
void EventHandler_TcpServerConnected(){
P1 &= 0xFB;
}

//将ESP8266送来的信息,送去LCD显示。
void EventHandler_MsgReceived(unsigned char* head){
LCD_writeLine(head);
}

//初始化
void init(){
ESP01_Event_WifiConnected = &EventHandler_WifiConnected; //事件注册
ESP01_Event_IpGot = &EventHandler_IpGot; //事件注册
ESP01_Event_TcpServerConnected = &EventHandler_TcpServerConnected; //事件注册
ESP01_Event_MsgReceived = &EventHandler_MsgReceived; //事件注册
ESP01_Init();
LCD_init();
}

void main(){
init();
while(1);
}
  • 编译
    编译前要设置一下目标参数,如下图。
    目标设置
    注意这里要改成XDATA,不然编译通不过的。

效果

开始前请确定在同一个网络下,并且服务端已开启。

初始化效果
我只是把单片机连到移动电源上了。

服务端
为了能让1602第一行显示Hello,第二行显示World,中间可以留了11个空格。

客户端


结语

猴!到这里这个程序就算完成了。这个比那些用手机控制开关灯要复杂一些。所以你只要掌握了这个例子,那些都不在话下了。
扯单一周目BOSS正式刷完,这个系列的文章将会暂告一段落,因为接下来笔者又要去考试了,还有什么噼里啪啦科三学车,烦。消失一小阵子后我会再次诈尸的!

恭喜你获得一周目BOSS神装:物联网神技!

51单片机实战:与计算机异步串行通信

Posted on 2017-01-29 | Edited on 2019-03-01 | In 扯会儿单片机开发

文章框架

文章框架



前言

猴!今儿扯串口,相对于并行——一口气全把数据扔过去,串行显得更加稳重——一位一位来。
串行就是这样,只需要一条数据线(全双工和同步串行时两条),一位一位的传过去。为了让大家在直到你是在给我传数据而不是外面的噪音或者是胡说八道,所以串行数据的各位要组装帧(看正文中的帧格式)。乍一看,这种方式跟并行比肯定慢的一腿。但实际上,多亏了它的稳定性,可以在波特率极高的情况下依然保持稳定,这是并行所办不到的(传的快了或距离远了就张牙舞爪了),所以发展到现在,串口已经把并口甩走几条街啦。

并口传输的例子:《51单片机实战:液晶显示器のLCD1602》

除此之外,串行传输分同步和异步。同步除了传输数据外,还要传输时钟信号,以保持双方同步。另一种,异步,就没这么麻烦了,也是本例中要讲到的,各自走各自的时钟就好,只要帧格式和波特率都商量好是一样的就好。


知识点

电平

电平 高电平 低电平 说明
TTL +5V 0V Transistor–Transistor Logic。常用于设备内部的数据传输,10英尺内。
RS-232C -12V +12V RS(recommended standard)代表推荐标准,232是标识号,C代表RS232的最新一次修改(1969),用于计算机串口

电平之前在文章《51单片机实战:液晶显示器のLCD1602》中介绍过,那里只说了TTL,本例中由于要和计算机打交道,所以多了一种电平:RS-232C

在单片机中是TTL,电脑那边传出和接收都是RS232,所以两种电平需要作转换。
MAX232
当当当!它就是干这活的。

T#IN T#OUT R#IN R#OUT VS C
TTL输入 TTL输出 RS232输入 RS232输出 电源 接电容

举个栗子,比如单片机从T1IN输入TTL电平,转换好的RS232电平就从R1OUT输出。其他的照猫画虎,这里不详细说这个东西,因为咱们在Proteus里干活,用不着转换(Proteus光环)。

####波特率(Baud Rate)
在此描述串行传输数据速率。
正儿八经的说,波特率乃码元的传输速率,即每秒传输的码元个数(码元可以是任意进制的),并不是什么每秒传输的比特数,大家注意。
波特来源于一个人的名字:Jean-Maurice-Émile Baudot,因此简写为Baud,单位符号:Bd。波特率可简写成Bd/s。

在串口通信中,其码元就是二进制信号,所以波特率的数值等于比特率数值,但你不能说波特率就是比特率啊!

单片机的串口通信有四种方式(各方式具体是干什么的,别着急,在后面),其中方式0和方式2的波特率是固定的。方式1和方式3的波特率是可变的,其脉冲周期由定时器1溢出产生。

方式 波特率
0 f / 12
1 ((2^SMOD) / 32) × (T1溢出率)
2 ((2^SMOD) / 64) × f
3 ((2^SMOD) / 32) × (T1溢出率)

其中f是系统晶振频率,T1是计时器1,SMOD是PCON中的最高位(PCON见相关寄存器的第一个)。

可以从上述公式看出,波特率不可变是因为直接与系统晶振频率相关(晶振频率不可变,除非换晶振),而可变是因为直接与T1的溢出率相关(溢出率可以改变)。

溢出率
在之前定时器应用的例子(《51单片机实战:定时器与数码管的应用》)中,我们计算的是溢出周期,也就是多长时间会溢出一次。这次我们用到的溢出率其实是同一个东西,取倒数就可以了。

详见:《51单片机实战:定时器与数码管的应用》 - 知识点 - 定时器/计数器 - 初值

  • 使用定时器1的方式2

    定时器的方式2是一个自动重装初值的8位定时器。低8位(TLX)用于自加计时,高8位(THX)保存每次自动重装的初值。

    所以,用于产生脉冲周期的定时器的
    溢出周期 = [(2^8) - i] × 12 / f
    其中, i为定时器初值,f还是晶振频率。

    溢出率 = 1 / 溢出周期

11.0592MHz
为什么要用这么蹩脚的数字作晶振频率哈,就是跟这里有关。如果你已经用上述公式计算过串口方式1下的12MHz和11.0592MHz在9600波特率下的定时器初值,你就会发现,前者得出一个小数,而后者是个整数。
我们可没办法用小数赋初值,所以你若用近似的整数作初值,就意味着会产生误差。

  • 常用:11.0592MHz & 9600Bd ⇒ THX = TLX = 0xfd

若用其他的晶振和波特率的话,请自行按前面的公式计算。

帧格式

串行传输按比特来,一个个比特组成一个帧,帧需要一定的格式才能被双方识别这是一个帧信息。

S D P E
起始位 数据位 奇偶校验位 终止位
标明帧头 数据信息 用于检验此帧是否出错 标明帧尾
1位,低电平 可以是5、6、7、8位 可加可不加,可奇校验也可偶校验 可以是1、1/2、2位,高电平

相关寄存器

PCON

电源管理寄存器,用于管理单片机的电源部分。
字节地址:87H,不能位寻址,reg52.h中已定义,单片机复位时全部清零。

位 7 6 5 4 3 2 1 0
符号 SMOD (SMOD0) (LVDF) (P0F) GF1 GF0 PD IDL
说明 串口方式为1、2、3时,设置串口波特率的速率 STC单片机特有功能 STC单片机特有功能 STC单片机特有功能 通用工作标志位 通用工作标志位 掉电模式 空闲模式
值 0:正常;1:加倍 \ \ \ \ \ 1:进入掉电模式 1:进入空闲模式

上表中出现的“串口方式”见下表的SM0和SM1。

SCON

串口控制寄存器,用于设定串口工作方式。
字节地址:98H,可位寻址,reg52.h中已定义,单片机复位时全部清零。

位 7 6 5 4 3 2 1 0
符号 SM0 SM1 SM2 REN TB8 RB8 TI RI
说明 工作方式选择位 工作方式选择位 多机通信控制位 允许串行接收位 方式2、3时,发送数据的第9位 方式2、3时,接收数据的第9位 发送中断标志 接收中断标志
值 看下表 看下表 与本例无关懒得说 1:允许串口接收数据 \ \ 第八位发送结束时,硬件置1 第八位接收结束时,硬件置1
SM0 SM1 方式 说明
0 0 0 同步移位寄存器方式
0 1 1 10位异步收发(8数据位),波特率可变
1 0 2 11位异步收发(9数据位),波特率固定
1 1 3 11位异步收发(9数据位),波特率可变

上表中波特率可变的方式,都由定时器1的溢出率控制。


实例

需求说明

当单片机接收到字符a时,点亮一个LED灯。传送方式:9600波特率,8数据位,无校验位,1停止位。

程序清单

本例中我就不写电脑端程序了,直接用现成的。

程序 说明 下载
VSPD Visual Serial Port Driver,用于建立虚拟串口连接,因为我们还是用Proteus模拟接口,所以需要VSPD模拟串口之间连接起来 度娘网盘
UartAssist 串口调试助手,用来给单片机发送消息 度娘网盘

电路

电路

注意,这里面我没有放转换电平转换芯片(MAX232),只有在Proteus里可以这么干,现实中焊板子还是要做电平转换的,这里这个软件给简化了。

COMPIM
COMPIM
乃虚拟9Pin串口,模拟前记得要设置参数。
参数设置
注意,波特率要设置虚拟波特率那个,物理波特率在本例中没用。

虚拟终端
右下角那个东西是虚拟终端(Virtual Terminal),他可以直接截获串口传来的消息然后显示出来。很方便做这方面调试时使用。
Virtual Terminal

路径:边栏 → instruments → virtual terminal

如果在调试的时候不小心把它的终端窗口关了,再次打开路径:菜单 → debug - virtual terminal,注意是在启动调试的情况下。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <reg52.h>
#define uchar unsigned char
#define uint unsigned int

char a; //用于从缓冲区中接收数据,虽然在此例中显得有点多此一举,但最好还是把东西放到自家变量中放心。

//初始化函数
void init(){
a = 0;

//定时器初始化
TMOD = 0x20; //定时器1的方式2
TH1 = 0xfd;
TL1 = 0xfd;
TR1 = 1;

//串口初始化,方式1
SM0 = 0;
SM1 = 1;
REN = 1; //允许接收

EA = 1; //中断总闸·开!
ES = 1; //串口中断·开!
}

void main() {
init();
while(1);
}

void com() interrupt 4{
a = SBUF; //SBUF为串口接收缓冲区
while(!RI); //判断接收是否完毕
if(a == 0x61) //如果是'a',亮灯
P0 = 0;
RI = 0; //准备下一次接收
}

效果

  • 发送c
    调试助手发送数据
    注意这里串口调试助手中的通讯设置那一块,要和Proteus中COMPIM元件保持一致。
    Proteus
    可以看出发送c后,端口正确收到了c,但并没有亮灯。

  • 发送a
    Proteus
    收到a后,亮灯。


结语

大年初二,拜访完姥姥家就该看看单片机怎么玩,你说是吧!这两天快马加鞭了,下一站:一周目大BOSS。各位加油。

51单片机实战:液晶显示器のLCD1602

Posted on 2017-01-27 | Edited on 2019-03-01 | In 扯会儿单片机开发

文章框架

文章框架



前言

好吧,最终我还是决定把LCD和串口通信分开写。

首先祝各位新春快乐,鸡年大吉。上班的事业有成,上学的天天向上。过大年呢,还真没啥心情码字。

借着爆竹声咱扯会儿LCD(液晶显示器,Liquid Crystal Display)。今天要实战的这款俗称为LCD1602,尤其注意这个1602,他说明了这款显示器的显示能力:每行16个字符,共2行,乃字符显示器(仅ASCII)。


参数

显示容量 芯片工作电压 工作电流 模块最佳工作电压
16×2 Char 4.5~5.5V 2.0mA(5.0V) 5.0V

引脚

Vss Vdd VO RS R/W E D0~7 BLA BLK
接地 正极 对比度调节 数据(H)/命令(L)选择端 读(H)/写(L)选择端 使能(Enable)信号 数据口 背光电源正极 背光电源负极

LCD上也有一个单片机,用于控制屏幕显示。我们并不是直接操作那块屏幕,而是与那个单片机交互。
其中D0~7这8个数据口就是用于交互的,为并行传输。

指令

因为我们要和LCD内嵌的单片机交互,所以需要指令。下面所列的东西都是当RS为低电平时发送的(若为高电平,就识别为数据)。

  • 数据指针
第一行 第二行
0x80 0xC0 (0x80+0x40)
  • 显示相关

模式:

指令 功能
0x38 设置16×2显示,5x7点阵,8位数据接口

方式:

0 0 0 0 1 D C B
\ \ \ \ \ Display,1:开显示 Cursor,1:显示光标 Blink,1:光标闪烁
0 0 0 0 0 1 N S
\ \ \ \ \ \ Next,1:读/写一个字符后,指针自动加1 Shift,1:写字符时,相对字符静止的屏幕移动

清屏:

指令 功能
0x01 数据指针清0且所有指针清空
0x02 仅数据指针清0
  • 操作
指令 功能
0x10 光标左移
0x14 光标右移
0x18 整体左移
0x1c 整体右移

电平

简单说下,逻辑电路中只有高电平和低电平,也就是程序里面的1和0。但是在物理层面上,它需要一个具体的表现,然后整理成标准。
TTL就是本例中要用到的一种电平,单片机和LCD通过引脚传递电信号,从而达到1和0的传递。
TTL中的低电平(0)表现为0V,高电平(1)表现为5V


实例

界内显示

  • 电路

电路

在Proteus里,1602就是LM016L,除了没有背光灯电源外用法一致(VEE是对比度调节,本例不用)。
RP1为上拉电阻,用于提高电压。由于这款单片机的P0引脚组的电压低于5V,所以需要上拉至5V,达到TTL的标准。
那个圆形的刻着Volts字样的东西是电压表,连接两个没被上拉电压的P0引脚,具体数值看后面的演示图。

  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <reg52.h>
#define uchar unsigned char
#define uint unsigned int

//coefficient of 1602 display(16 row 2 col. 5*7px per Char)
#define LCD_CLEAR 0x01 //宏定义:清屏
#define DISPLAY_MODE_1602 0x38 //宏定义:1602显示模式

#define DISPLAY_OFF 0x08 //宏定义:关显示
#define DISPLAY_ON_NO_CURSOR 0x0c //宏定义:开显示且无光标
#define DISPLAY_ON_WITH_CURSOR_NO_BLINK 0x0e //宏定义:开显示且有光标但不闪烁
#define DISPLAY_ON_WITH_CURSOR_BLINK 0x0f //宏定义:开显示且有光标且闪烁

#define AUTO_BACK_STEP 0x04 //宏定义:读/写时指针自动减1
#define AUTO_NEXT_STEP 0x06 //宏定义:读/写时指针自动加1
#define AUTO_DISPLAY_MOVE_LEFT 0x07 //宏定义:字符相对静止,整屏左移
#define AUTO_DISPLAY_MOVE_RIGHT 0x05 //宏定义:字符相对静止,整屏右移

#define ALL_MOVE_LEFT 0x18 //宏定义:屏幕左移
#define ALL_MOVE_RIGHT 0x1c //宏定义:屏幕右移
#define CURSOR_MOVE_LEFT 0x10 //宏定义:光标左移
#define CURSOR_MOVE_RIGHT 0x14 //宏定义:光标右移

#define FIRST_ROW 0x80 //宏定义:第一行头地址
#define SECOND_ROW FIRST_ROW+0x40 //宏定义:第二行头地址


uchar code fst[] = "Hello World!"; //第一行要显示的数据数组
uchar code sec[] = ""; //第二行要显示的数据数组
uchar num; //字符计数
sbit enable = P0^5; //使能端
sbit RS = P0^7; //数据/命令切换
sbit RW = P0^6; //读/写切换

sbit anode = P0^0; //连接电压表阳极
sbit cathode = P0^1; //连接电压表阴极

//粗制的延时器,没走一次这个函数大约为1ms,适用于11.0592MHz及附近
void delay(uint z)
{
uint x,y;
for(x=z;x>0;x--)
for(y=110;y>0;y--);
}

//写命令
void writeCmd(uchar cmd)
{
RS = 0; //切换为写命令模式
P2 = cmd;
delay(1); //注意
enable = 1; //执行!
delay(1); //注意
enable = 0; //执行完毕!
}

void writeDat(uchar dat){
RS = 1; //切换为数据模式
P2 = dat;
delay(1); //注意
enable = 1;
delay(1); //注意
enable = 0;
}

//初始化函数
void init(){
anode = 1;
cathode = 0;

RW = 0; //写模式,本例只往LCD写数据
enable = 0;
writeCmd(DISPLAY_MODE_1602); //发送命令:1602模式
writeCmd(DISPLAY_ON_WITH_CURSOR_BLINK); //发送命令:开始显示并闪烁光标
writeCmd(AUTO_NEXT_STEP); //发送命令:数据指针自动加1
writeCmd(LCD_CLEAR); //发送命令:清屏
}

void main(){
init();
writeCmd(FIRST_ROW); //发送命令:开始从第一行写入
for(num=0;num<=12;num++){
writeDat(fst[num]); //发送数据,每次一字节
}
while(1);
}

好,说一下上面代码中标//注意的地方,全都是delay(1)。
为了什么呢,不是蛋疼,是因为单片机给LCD传送信号时,数据是要放在数据线上的,要是LCD还没读完单片机给它发的啥就把内容撤走的话,就会造成数据丢失。
就是这个道理,为了传输稳定,所以延时一小会儿。这个延时的数值需要大家自己去试,并不一定所有的情况都延时大约1ms就够的。

  • 效果

GIF.gif

越界显示

本例用于显示字符数超过16个的情况。
代码改动

1
2
uchar code fst[] = "1234567890ABCDEF";  //第一行要显示的数据数组
uchar code sec[] = "1234567890ABCDEFGHIJK"; //第二行要显示的数据数组
  • 初始化函数
1
2
3
4
5
6
7
8
9
10
11
void init(){
anode = 1;
cathode = 0;

RW = 0;
enable = 0;
writeCmd(DISPLAY_MODE_1602);
writeCmd(DISPLAY_ON_NO_CURSOR); //换成不闪的,虽然跟这个新需求没什么联系,就是给你演示下效果
writeCmd(AUTO_NEXT_STEP);
writeCmd(LCD_CLEAR);
}
  • 主函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void main(){
init();
writeCmd(FIRST_ROW); //发送命令:开始从第一行写入
for(num=0;num<=16;num++){
writeDat(fst[num]);
}
writeCmd(SECOND_ROW); //发送命令:开始从第二行写入
for(num=0;num<=20;num++){
writeDat(sec[num]);
}
while(1){
//向左移动三次,每次间隔500ms
for(num=0;num<=3;num++) {
writeCmd(ALL_MOVE_LEFT); //发送命令:整屏左移
delay(500);
}
delay(1000); //暂停大约1s
//向右移动三次,每次间隔500ms
for(num=0;num<=3;num++) {
writeCmd(ALL_MOVE_RIGHT); //发送命令:整屏右移
delay(500);
}
delay(3000); //暂停大约3s后开始下一轮
}
}

效果
效果
移屏只移3个字符距离,所以并没有把第二行的K显示出来。


结语

这次单讲LCD的入门应用,送给不爱看春晚的你。前两天搞定了科二考试,年后准备科三了。《扯单》系列的一周目大概还差两三篇就完结了,下集预告:串口应用。好了,看完文章实践实践后就该打麻将打麻将,该放炮仗放炮仗吧!总之大家吃好玩好。

51单片机实战:定时器与数码管的应用

Posted on 2017-01-14 | Edited on 2019-03-01 | In 扯会儿单片机开发

文章框架

框架脑图



前言

好久不《扯单》了!今儿来一发应用篇:用定时器和数码管做一个单十六进制位的计时器。从去年六月底辞职到现在已经过去半年多了,这方面的东西好像也扔的七七八八了(哭笑)。最近考驾照,需要花半天的时间练车,另半天的时间就是复习,然后总结到这里给需要的朋友参考。我在公司的项目是关于智能仓拣的,为了省成本选择51单片机,为了通信方便也用到了无线通信。所以我的《扯单》系列的第一周目会扯到ESP8266的无线通信部分(对物联网感兴趣的朋友有福,可慢慢等我更新)。
今天这部分也算是对《扯会儿单片机开发:中断》的一个应用,顺便也用到了数码管,都是很基本的东西。下一步写通信,先有线的串口通信,我会配合LCD做示范。然后就是一周目BOSS:无线网络(Wi-Fi)通信。

嗯,总之我目前是这么打算的,到时候会不会这样我就不知道了哈(任性到不能自己)。好,下面开始今天的正文。


知识点

数码管(7-Segment Display)

图片来源于百度
数码管图示

简介
我觉得看完图片我也不用做什么介绍,大家应该都见过这东西,最多最多可能就是不知道它叫数码管。
数码管的本质是几个并联的发光二极管(LED)。如果不熟悉发光二极管在单片机中应用的可参考文章:《51单片机实战:Proteus、Keil入门及点亮一个虚拟LED》
本篇应用了上图的七段一位数码管,也就是说,它本质上就是由七个发光二极管并联起来组成的。虽说上图的1位数码管是有小数点的,你可以理解为那是第八个LED。但是本篇运用的是Proteus中模拟的数码管,并没有那个小数点,看到后面你就知道啦,但要注意区分。

分类

  • 共阳(Common Anode)
    Proteus模拟元件:7SEG-COM-ANODE
    如图就是Proteus中模拟的共阳数码管,上面单独的那个接线是7个数码管共同的阳极接线,左边七个接线是它们各自的阴极接线,所以你也就理解了“共阳”就是他们的阳极是在一起的。

  • 共阴(Common Cathod)
    Proteus模拟元件:7SEG-COM-CATHODE
    与共阳同理,下面单独的那个接线是7个数码管共同的阴极接线,左边七个接线是他们各自的阳极接线,“共阴”就是他们的阴极是在一起的。

  • BCD(Binary Coded Decimal Display)
    Proteus模拟元件:7SEG-BCD
    这个就比较特殊了,它是四个接线(分别对应十进制数字的8、4、2、1)。他只能显示十六进制的0~15(详细请自查阅二进制、十进制、十六进制的转换,这里不作讨论),也就是0、1、2、3、4、5、6、7、8、9、A、b(大写的没法区分)、C、d(大写的没法区分)、E、F。所以如果只作上述十六进制数字的表示,这个操作起来最方便,但是没有灵活性,不像共阳和共阴的可以表示很多鬼畜的东西出来。

演示
你看了BCD可能还清楚要怎么给它数据,但共阳共阴的哪知道它哪个接线对应哪个LED啊?
好,我给你图,有点丑别太在意:
接线与其对应的LED示意图
解释一下哈,这是我用平板画的,圈里面的数字对应的就是上面分类中共阳和共阴数码管左侧接线从上到下的次序。旁边的八位二进制数字是代表哪一位控制哪一个LED(第八位没用。共阳0亮1灭,共阴0灭1亮)。
下面用Proteus + Keil(若不会使用详见:《51单片机实战:Proteus、Keil入门及点亮一个虚拟LED》)做一个小程序给大家演示一下三者的区别。

  • 电路
    电路
    其中左侧的是BCD(Proteus模拟元件:7SEG-BCD),右上为共阳(Proteus模拟元件:7SEG-COM-ANODE),右下为共阴(这个是不带共阴接线的,接起来比较简单,带接线的接起来有点麻烦,但原理一样。Proteus模拟元件:7SEG-DIGITAL)。

  • 代码

1
2
3
4
5
6
7
8
9
#include <reg52.h>
#define VALUE 0x04 //对应的二进制为:0000 0100

void main() {
P0 = VALUE; //连接电路中的共阳数码管
P1 = VALUE; //连接电路中的BCD
P2 = VALUE; //连接电路中的共阳数码管
while(1); //让单片机运行卡在这里,为了一直运行,不然数码管都会灭掉,大家以后写程序也要注意
}

#define为预编译指令中的宏定义指令,只由编译器解释,译为将文中所有的VALUE替换为0x04。关于其二进制0000 0100可以回头看一下那个丑不拉几的草图。

  • 效果
    效果
    可以看出共阳和共阴都是对LED亮灭的操作,且正好相反。而BCD可以直接解读你给的数字。所以如果我们用共阳或共阴的数码管显示信息,就需要在代码中制作一个编码表。

定时器/计数器

简介
首先,“定时器/计数器”说的是一个东西,因为它既能计时也能计数。其次,它与数码管不一样,不是独立出来的配件,而是存在于单片机内部的一个独立的硬件部分,依赖晶振产生固定的时间间隔,产生了一定量的固定时间间隔后会引发定时器中断(参见:《扯会儿单片机开发:中断》),从而将其产生的时间信息传送给由CPU执行的主程序中。
相关寄存器

  • TMOD
    TMOD为定时器/计数器工作方式寄存器,用于确定其工作方式和功能选择。
    字节地址:89H,不能位寻址,reg52.h中已定义,单片机复位时全部清零。
位序号 7 6 5 4 3 2 1 0
位符号 GATE C/T M1 M0 GATE C/T M1 M0

你会发现,低四位和高四位格式是一样的,因为低四位(0~3)用于设置定时器0,高四位(4~7)用于设置定时器1。设置的内容都是GATE、C/T、M1、M0。

GATE C/T M1M0
门控制位 计数器还是定时器 工作方式
0:仅受TCON的TR位控制。1:由TR和外部中断一起控制。 0:定时器。1:计数器 见下表
M1 M0 工作方式
0 0 方式0,为13位定时器/计数器
0 1 方式1,为16位定时器/计数器
1 0 方式2,8位初值自动重装的8位定时器/计数器
1 1 方式3,仅适用于T0,分成两个8位计数器,T1停止计数
  • TCON
    TCON为定时器/计数器控制寄存器,用于控制其启动、停止,标志其溢出和中断情况。
    字节地址:88H,能位寻址,reg52.h中已定义,单片机复位时全部清零。
位序号 7 6 5 4 3 2 1 0
位符号 TF1 TR1 TF0 TR0 IE1 IT1 IE0 IT0

高八位与其运行和溢出有关,第八位与外部中断有关。

高八位中:

TF TR
溢出标志位 运行控制位
溢出时,由硬件置1 1:启动定时器

TF0和TR0对应定时器0,TF1和TR1对应定时器1。

低八位中:

IE IT
外部中断请求标志 外部中断触发方式选择位

IE0与IT0对应外部中断0,IE1与IT1对应外部中断1。因为此例与这里无关,不作详细介绍,有兴趣请自查资料。

初值

  • 简介
    定时器的实质是,由机器频率向一个16位寄存器累加,累加满溢出时触发中断。为了产生一个我们想要的时间间隔,比如说1s,所以我们要在这个寄存器里设定一个初值,以至于让它在这个初值上累加可以产生一个1s的倍数。这样我们就得到了稳定的时间间隔。
    这个寄存器分为TH(高八位)和TL(低八位)。所以我们需要把计算好的初值分成两部分分别放入TH和TL。

  • 过程
    首先,我们通过单片机的晶振频率得知其时钟周期,再尤其乘以12得到机器周期。每一个机器周期在寄存器内+1,直到加满溢出产生中断。

  • 例子
    若单片机频率为12Mhz,其时钟周期就是1/12μs,机器周期为1μs,也就是每1μs寄存器+1。16位的寄存器加到溢出最多需要(2^16)-1=65535μs,溢出也需要一个机器周期,所以总共要65536μs。但这个值太别扭,和我们要的1s没什么关系。我们最好让它记50000μs产生一次中断,所以其初值就设为65536-50000=15536。但我们还要将这个值分别放在高八位和低八位,所以要将这个十进制数,转换为4位十六进制数再分开赋值。
    十进制计算法:TH = 15536/256; TL = 15536%256;,进制计算问题这里不细讨论。
    这样的话,每50ms就会产生一次中断。我们只要用程序判断其中断20次就记1s。


电路

这里开始进入今天实践正题,虽说电路和上面知识点的电路是一样的,但要记得把单片机的时钟频率(Clock Frequency)要调成12MHz。
电路


代码

代码部分我分为两个部分,一个是自定义的关于数码管编码表的头文件7seg.h,另一个是主程序源代码文件main.c
本例要实现的是在12MHz的时钟频率下,每隔一秒数码管显示的数字加一。

  • 7seg.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef __7SEG_H__
#define __7SEG_H__

//共阳数码管的十六进制数编码表
unsigned char code hexForCommonAnode[] = {
0x40, 0x79, 0x24, 0x30,
0x19, 0x12, 0x02, 0x78,
0x00, 0x10, 0x08, 0x03,
0x46, 0x21, 0x06, 0x0e
};

//共阴数码管的十六进制数编码表
unsigned char code hexForCommonCathode[] = {
0x3f, 0x06, 0x5b, 0x4f,
0x66, 0x6d, 0x7d, 0x07,
0x7f, 0x6f, 0x77, 0x7c,
0x39, 0x5e, 0x79, 0x71
};

#endif
  • main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <reg52.h>
#include <7seg.h> //包含自定义的编码表头文件

unsigned char THx = (65536-50000)/256; //存储高八位寄存器初值的临时变量
unsigned char TLx = (65536-50000)%256; //存储低八位寄存器初值的临时变量
unsigned char counter = 0; //用于记录产生中断的次数
unsigned char digit = 0; //用作编码表索引

//初始化函数:用于初始化各种参数
void init() {
TMOD = 0x01; //设置定时器0,GATE = 0, C/T = 0 , M1M0 = 01(方式1,16位定时器/计数器)

//赋初值
TH0 = THx;
TL0 = TLx;

EA = 1; //中断总闸·开!
ET0 = 1; //定时器0中断·开!
TR0 = 1; //定时器0·运行!
}

//刷新函数:每调用一次就更新一次数码表的显示方式。
void refresh(){
counter = 0; //中断计数器清零
P0 = hexForCommonAnode[digit]; //根据索引找编码表,将显示方式的二进制位丢给共阳数码管
P2 = hexForCommonCathode[digit]; //根据索引找编码表,将显示方式的二进制位丢给共阴数码管
P1 = digit; //索引就是要显示的那个数字,可直接丢给BCD显示出来

//数满后索引(数字)清零
if(++digit >= 16)
digit = 0;
}

//主函数
void main() {
init(); //初始化
refresh(); //先把0显示出来
while(1); //卡住
}

//定时器0的中断函数:由定时器中断自动调用,你只需要写好中断后要怎么处理就好
void timeInt_T0 () interrupt 1 {
//每中断一次都要重新赋初值
TH0 = THx;
TL0 = TLx;

//记够20次中断后,刷新显示
if(++counter == 20)
refresh();
}

效果

效果


结语

好了,今天就《扯单》到这里,下次不出意外的话就开始扯串口通信了。如果你对本文有什么感想和意见,欢迎在评论区畅所欲言!关于深度的话,我会慢慢加,不过也是在力所能及的范围内,毕竟只是个软工狗,还请谅解。

本次实践文中加入了小部分的理论知识介绍和文章框架脑图,不知道大家喜不喜欢,有什么想法就在评论区告诉我。


参考资料

1.《51单片机C语言教程》,郭天祥 著,电子工业出版社

扯会儿单片机开发:中断

Posted on 2016-07-01 | Edited on 2019-03-01 | In 扯会儿单片机开发

前言

读到这篇,你最好已经有了一些基础知识和单片机入门编程能力,接下来聊聊中断(Interrupt)。
用程序员的角度说,单片机的中断机制有点像面向对象中的事件,只不过单片机中的这些“事件”是由其他的部件引发。


什么是中断

简而言之,就是打断处理器且让其处理那个打断它的事件。就好像你在玩电脑,然后家里的暖壶响了,这时你会放下眼前的事,去厨房关火。水开了、壶响了就是中断的触发,关掉煤气灶就是中断要处理时的程序。当你关了火就可以继续回来做眼下的事情。中断也是这个道理。

操作系统中断

操作系统的发展过程,大体上就是一个想方设法不断提高资源利用率的过程,而提高资源利用率就需要在程序并未使用某种资源的时候,把它对那种资源的占有权释放掉,而这个行为,就需要通过中断实现。

从上述引段中可以看出,操作系统中的中断机制引入的初衷就是为了提高多道程序设计的资源利用率的。

单片机中断

在单片机中,换汤不换药,也主要是为了处理发生的中断事件而设定的


单片机开始运行后,会开始执行主程序中的代码,当遇到中断时,单片机就会暂停读取主程序代码,转而读取中断代码,当执行完中断代码后,继续手下的主程序代码。
大部分单片机并没有操作系统,比如笔者最近把玩的51系列。所以也谈不上他们会有多道程序设计,更不用说进程那些概念。因为单片机的大部分用途都是只运行单一一个程序就够了。


52单片机

说回到52单片机,也就是stc89c52或at89c52之类的。

类别

它有三种中断类型

类型 说明
外部中断 比较通用,外部中断引脚收到信号后发生中断
串口中断 串口RXD引脚收到信号后发生中断,往往是接受够一个字节后触发
定时器中断 每当定时器计数溢出后触发

共6个中断源,其中2个外部中断源,3个定时器中断源,1个串口中断源。这里仅仅讨论52单片机内的中断设置。

相关寄存器

中断涉及到两个寄存器,一个是中断允许寄存器(IE,Interrupt Enablement),另一个是中断优先级寄存器(IP,Interrupt Priority)。
中断允许寄存器(IE)
各中断源的开关,字节地址:A8H(reg52.h中已定义sfr IE = 0xA8;),可以位寻址(A8H~AFH),复位时全部清零。

位序号 位地址 位符号 说明
D7 AFH EA 全局中断允许位,就像电闸上总闸,是所有开启任何中断的前提,若关闭,则关闭所有中断。
D6 – – 这一位为空,无用。
D5 ADH ET2 定时器2中断允许位,若打开则允许定时器2溢出后引发中断。
D4 ACH ES 串口中断允许位,若打开则允许RXD引脚接受到1个字节的信息后引发中断。
D3 ABH ET1 定时器1中断允许位,若打开则允许定时器1溢出后引发中断。
D2 AAH EX1 外部中断1中断允许位,若打开则允许外部中断引脚接收信号后引发中断。
D1 A9H ET0 定时器0中断允许位,若打开则允许定时器0溢出后引发中断。
D0 A8H EX0 外部中断0中断允许位,若打开则允许外部中断引脚接收信号后引发中断。

以上全部位的值为1是打开,0是关。
中断优先寄存器(IP)
用于设定各中断源的优先级,字节地址B8H(reg52.h中已定义sfr IP = 0xB8;),可以位寻址(B8H~BFH),复位时全部清零。

位序号 位地址 位符号 说明
D7 – – 这一位为空,无用。
D6 – – 这一位为空,无用。
D5 – – 这一位为空,无用。
D4 BCH PS 串口中断优先级控制位。
D3 BBH PT1 定时器1中断优先级控制位。
D2 BAH PX1 外部中断1优先级控制位。
D1 B9H PT0 定时器0中断优先级控制位。
D0 B8H PX0 外部中断0优先级控制位。

以上各位值为1时为高优先级,0位低优先级。
52单片机中,供用户设置的有两种优先级,高与低。他们遵循以下三条规则:

1.低优先级中断源可被高优先级中断源中断,高优先级中断源不能被任何中断源中断。
2.一种不论高低的中断源一旦得到响应,与之同级的中断源不可中断它。
3.当同时收到几个同优先级的中断源时,响应优先级按其默认中断级别执行。

各中断源默认中断级别及中断序号

中断源 默认中断级别 序号(C语言)
外部中断0 1st 0
定时器0中断 2nd 1
外部中断2 3rd 2
定时器1中断 4th 3
串口中断 5th 4
定时器2中断 6th 5

参考文献

1.《51单片机C语言教程》,郭天祥 著,电子工业出版社
2.《操作系统联考复习指导》,王道论坛 组编,电子工业出版社
3.《现代操作系统》,Andrew S. Tanenbaum 著,机械工业出版社
4.单片机中断的IE和IP寄存器,电工学习网

51单片机实战:Proteus、Keil入门及点亮一个虚拟LED

Posted on 2016-06-09 | Edited on 2019-03-01 | In 扯会儿单片机开发

前言

相信你在看完《扯会儿单片机开发:开始》后,对单片机开发的基础知识有了一定的了解。这一次我们来实战一番,在Proteus中模拟一个单片机界的”HelloWorld” — 点亮一个LED。


电路

在我们开始编码之前,要先把电路画好。我们要通过程序去控制一个LED的明灭,所以需要一个单片机和一个LED,当然,还有它们之间说不清道不明的关系。

Proteus

Proteus软件是英国Lab Center Electronics公司出版的EDA工具软件。它可以仿真单片机逻辑和元件之间的电路,我们这次写好的单片机程序就是交给它来模拟运行。
Proteus 8启动画面
我所用的是Proteus 8.4 SP0
Proteus 8.4 SP0 启动图标
如果你用的是早期版本,可能启动图标不一样。我之前用的7.8版的Proteus会有两个子程序,如果你的也是的话,点那个ISIS(阿拉胡阿克巴!)就是了。

建项

现在要做的就是在Proteus中画个可以控制LED的电路出来。

  1. 打开Proteus
    主界面
  2. 建个项目
    点击主界面中的新建工程
    新建第一步:输入名称和设置路径
    在名称那里输入项目名称,在路径处设置项目工程文件存储的路径,下一步。
    新建第二步:选择原理图
    这里我们选择默认DEFAULT,选下一步。
    新建第三步:选择模板
    选择不创建PCB布板设计,下一步。
    新建第三步:选择固件
    选择没有固件项目,下一步。
    新建第四步:总结
    点击完成后建项成功。
    其实我也是第一次用Proteus 8,之前的版本建项步骤没这么繁琐,我这么说是因为我也不咋知道那些中间的设置项都能干嘛,还没试过(我好水,这样子自己揭露自己真的好嘛)。

开始画电路

创建好项目后如下图所示
主编辑界面

  • 列个需要的元件清单
    1. 单片机
    2. LED
    3. 电源

点击左侧的P添加元件
左侧的`P`
在元器件选择界面中,用关键字搜索我们需要的器件。
元器件选择界面
我们需要一个51单片机,这个项目就选择at89c52吧,输入”c52”搜索。
"c52"的搜索结果
中间那里会列出匹配的结果,你可以一个一个选择,然后观察右侧的预览图来确定是不是你要的菜。你还可以在右下角那里选择其封装。在本项目中,我们需要AT89C52这道菜的DIL40的封装。
点击确定后,你的鼠标会变成一根儿笔,再点击一下左键,刚才的器件就会出现,然后就可以摆放它的位置了。
准备摆放
部署后
然后重复刚才的操作,去找LED。
“LED”的搜索结果
可以看到,与LED相关的元件是比较多的。这里我们选择图示中的那款黄色发光二极管LED-YELLOW(模拟的时候,效果会比较明显)。
电源的话,就要去另一个地方找了,点一下左边栏的终端模式。
image.png
选择列表中的POWER,然后和前面一样,把它画出来。
画上LED和电源
途中的小雨伞就是电源。

然后连接它们,画线路。
用笔一样的鼠标点击那些触角就可以将他们连接起来。连好后如下图:
需要的电路
注意电源跟LED的哪个引脚连?
二极管是固定电流方向的,要按照图中的方式连接,具体为什么就要你自己查资料了。

到这里我们的第一大步就走完了。


代码

Keil

Keil是我们写单片机代码要用到的IDE,它支持汇编、C和C++的编译,还是很不错的。不过,没有代码补全。
Keil μ5 的启动画面
选择下载时,要根据你所开发的单片机选择具体的IDE,它分为ARM、C51、C166、C251四种。你也可以装多个,然后它们会在同一个IDE下显示并使用。

根据我们刚才在电路板上画的是at89c52单片机,所以我们就选用C51版本的。
主界面

建项

点击菜单栏的Project - New μVersion Project
选择工程存储路径
这里选择你的项目工程要存储的路径,他不会像Visual Studio或Xcode那样帮你为项目或解决方案自动生成文件夹,这个你要注意,你最好自己建一个项目文件夹,然后选择它去存储。

项目建立好后,左边栏会显示当前项目的文件结构。然后我们新建源文件,右击Source Group,选择Add New Item to Group 'Source Group 1'。添加源文件的前提必须是在一个Group下,这里选择用它默认的Source Group,你也可以自己新建一个Group。

选择C File (.c),输入文件名,然后点击Add
新建窗口
这样我们就添加了一个源代码文件。
添加了源文件

C代码

我们要实现功能的代码如下所示。

1
2
3
4
5
6
#include <reg52.h>
sbit led = P0^1;
void main()
{
led = 0;
}

一步一步来,首先,第一行所包含的reg52.h文件中定义了52单片机基础的特殊功能寄存器和特殊功能位。其内部的介绍是:

Header file for generic 80C52 and 80C32 microcontroller.

可以通过右键reg52.h - Open ducment <reg52.h>打开其文件。

打开后,可以看到其源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/*--------------------------------------------------------------------------
REG52.H

Header file for generic 80C52 and 80C32 microcontroller.
Copyright (c) 1988-2002 Keil Elektronik GmbH and Keil Software, Inc.
All rights reserved.
--------------------------------------------------------------------------*/

#ifndef __REG52_H__
#define __REG52_H__

/* BYTE Registers */
sfr P0 = 0x80;
sfr P1 = 0x90;
sfr P2 = 0xA0;
sfr P3 = 0xB0;
sfr PSW = 0xD0;
sfr ACC = 0xE0;
sfr B = 0xF0;
sfr SP = 0x81;
sfr DPL = 0x82;
sfr DPH = 0x83;
sfr PCON = 0x87;
sfr TCON = 0x88;
sfr TMOD = 0x89;
sfr TL0 = 0x8A;
sfr TL1 = 0x8B;
sfr TH0 = 0x8C;
sfr TH1 = 0x8D;
sfr IE = 0xA8;
sfr IP = 0xB8;
sfr SCON = 0x98;
sfr SBUF = 0x99;

/* 8052 Extensions */
sfr T2CON = 0xC8;
sfr RCAP2L = 0xCA;
sfr RCAP2H = 0xCB;
sfr TL2 = 0xCC;
sfr TH2 = 0xCD;


/* BIT Registers */
/* PSW */
sbit CY = PSW^7;
sbit AC = PSW^6;
sbit F0 = PSW^5;
sbit RS1 = PSW^4;
sbit RS0 = PSW^3;
sbit OV = PSW^2;
sbit P = PSW^0; //8052 only

/* TCON */
sbit TF1 = TCON^7;
sbit TR1 = TCON^6;
sbit TF0 = TCON^5;
sbit TR0 = TCON^4;
sbit IE1 = TCON^3;
sbit IT1 = TCON^2;
sbit IE0 = TCON^1;
sbit IT0 = TCON^0;

/* IE */
sbit EA = IE^7;
sbit ET2 = IE^5; //8052 only
sbit ES = IE^4;
sbit ET1 = IE^3;
sbit EX1 = IE^2;
sbit ET0 = IE^1;
sbit EX0 = IE^0;

/* IP */
sbit PT2 = IP^5;
sbit PS = IP^4;
sbit PT1 = IP^3;
sbit PX1 = IP^2;
sbit PT0 = IP^1;
sbit PX0 = IP^0;

/* P3 */
sbit RD = P3^7;
sbit WR = P3^6;
sbit T1 = P3^5;
sbit T0 = P3^4;
sbit INT1 = P3^3;
sbit INT0 = P3^2;
sbit TXD = P3^1;
sbit RXD = P3^0;

/* SCON */
sbit SM0 = SCON^7;
sbit SM1 = SCON^6;
sbit SM2 = SCON^5;
sbit REN = SCON^4;
sbit TB8 = SCON^3;
sbit RB8 = SCON^2;
sbit TI = SCON^1;
sbit RI = SCON^0;

/* P1 */
sbit T2EX = P1^1; // 8052 only
sbit T2 = P1^0; // 8052 only

/* T2CON */
sbit TF2 = T2CON^7;
sbit EXF2 = T2CON^6;
sbit RCLK = T2CON^5;
sbit TCLK = T2CON^4;
sbit EXEN2 = T2CON^3;
sbit TR2 = T2CON^2;
sbit C_T2 = T2CON^1;
sbit CP_RL2 = T2CON^0;

#endif

可以看到,里面声明了所有基础的功能寄存器、I/O寄存器和各种功能位。
还有一点,建议大家在定义自己的头文件的时候也写上#ifndef - #define - #endif这样的结构来保持自己的头文件在全局中保持唯一而不被重复引入。

回头看源码的第二行。

1
sbit led = P0^1;

意思是声明一个位寻址变量,寻址到P0.1引脚。在上面P0的声明中,可以看到它的地址是0x80,这个地址是P0八个引脚起始(也就是P0.0)的地址,这个地址的高四位代表这组I/O引脚的片选地址,低四位表示其内部的位选地址。这里的led的位选地址根据亦或运算符^算出,也就是P0.1的引脚地址。
注意,位寻址变量必须在外部定义,不能在内部,sfr也一样。

最后。

1
2
3
4
void main()
{
led = 0;
}

在主函数中执行,使led所指向的特殊功能位置低电平。因为我们之前的电路中LED一端连着电源,也就是高电平,另一端连着单片机的P0.1引脚,所以需要将P0.1置低电平,从而使电路连通。

以上,我们就完成了代码编写工作。


演示

生成HEX文件

单片机看不懂C,它只读二进制的机器码,所以我们需要Keil生成一个十六进制的HEX文件(十六进制可以说是服务于二进制的,它与二进制可以非常方便地相互转换,其主要用于存储大量的二进制。一位十六进制可以表示4位二进制)。

还是在Keil中,右击Target 1 - 选择Options for Target 1

选择Output选项卡 - 勾上Create Hex File - OK
目标设置页

编译

设置好目标生成选项后,点击左上方的Build(如图按钮)。

下方会输出编译信息
编译输出信息
我们可以从中看到:内部数据data、外部数据xdata、代码量code、编译错误和警告0 Error(s), 0Warning(s)还有编译时间Build Time Elapsed: 00:00:01的信息。
总的来说,只要0 Error(s),就说明编译通过了。

导入Proteus中的单片机

生成好的Hex文件在Keil项目目录下的Objects文件夹中。
回到Proteus中,右击at89c52(或双击)唤出设置页。

元件编辑页

点击Program File右侧的浏览按钮,选择刚才编译出来的Hex文件,然后点击确定。

看看效果

点击Proteus左下角的‘播放’按钮
播放按钮
效果
可以看到,小黄灯被我们点亮了。
MISSION COMPLETE!


结语

我们从一开始的Proteus建项、画电路,到Keil的建项、编码,再到最后的Keil编译、Proteus模拟。
这就是模拟电路的单片机开发的一个完整过程了。嗯,看完这篇文章后你也一定也按捺不住要干个痛了吧!那就赶紧去爽♂爽的实践一番吧。

扯会儿单片机开发:开始

Posted on 2016-06-03 | Edited on 2019-03-01 | In 扯会儿单片机开发

前言

正在写作这篇文章的笔者是一名大四的软件工程学生,因为实习工作的需要,奉命研究单片机开发,所以我所扯的内容可能更适合跟我的情况比较相同的人。
如果你懂一些计算机原理和C语言开发,读下面的文章就不会很费劲。但如果不懂也没关系,那就交叉学习吧。


什么是单片机

从CPU说起。

CPU(Central Processing Unit, 中央处理器)

CPU由运算器、缓存器及总线构成。其中,运算器是指ALU(Arithmetic logic unit,算术逻辑单元),是CPU的核心;缓存器(Cache)可分为一级缓存、二级缓存和三级缓存,容量逐步递增,读写速度速度递减,成本递减;总线(Bus))分为数据、控制和状态三种。典型代表:8086。

  • 物理结构
    从物理结构上看,CPU可分为逻辑部件、寄存器部件和控制部件。逻辑部件主要用于定点或浮点算术运算操作、移位操作和逻辑操作;寄存器部件可分为通用、专用和控制。每一种寄存器都有它自己的用处;控制部件主要用于指令译码和发出控制信号。
  • 性能参数
    作为数据处理的中心,最主要的参数可以说是主频和外频了。先说外频,外频是基准频率,它决定了主板的运行速度,人们所说的超频,超的就是外频。
    主频=外频*倍频
    主频,又叫时钟频率。由上述公式可以看出主频由外频决定。主频表示CPU内数字脉冲信号震荡的速度,它可以说明CPU的运算和处理数据的速度。简而言之,主频越高,CPU越快。CPU每做一个动作,处理数据或发布命令,都要随着主频的节奏来。所以这是个很有节奏感的东西。

计算机(Computer)

依照冯诺依曼体系,计算机的五大组成部件为运算器、控制器、存储器、输入设备和输出设备。其中运算器和控制器合起来就是CPU;存储器又可以拆分为内存和外存;输入和输出设备就是键盘、鼠标、显示器等。

  • 处理器
    运算器和控制器可以合称为处理器,CPU主要就是这两个部件。前者负责数据处理,后者发布命令。
  • 存储器
    存储器主要是指内存(RAM,Random Access Memory,随机存取存储器),靠电容的充放电存储电容信号进而可在逻辑上表示数字信号。由于电容充放电速度很快,所以内存的读写速度仅次于CPU中的缓存。但又因为是电容,所以无法在不通电的情况下存储这些信息。这样就催生了ROM,为了保存数据但只能写入一次的只读存储器。再后来为了能擦了重写就出现了EPROM和EEPROM等可擦写ROM。硬盘、U盘等都是前者的衍生物,他们都统称为外存(external storage),其主要作用就是在断电的时候能够保存数据。因为涉及到物理动作(掰硬盘里面的小磁针、在光盘上烫洞),所以相对于内存的速度要慢上要几个档次。
  • I/O
    I/O就是与之交互的基础了,通过输入设备来告诉它,通过输出设备来告诉你。就是机器交互的耳朵和嘴了。

单片机(MCU, Microcontroller Unit)

是一种集成电路(IC, Integrated Circuit),其实应该叫微控制器,单片机这个名字主要是指之前的SCM(Single Chip Micyoco, 嗯… Micyoco是个神奇的单词)。而现在MCU是由SCM发展而来的,在高级点就可以说是SoC(System on Chip)。但后面的我所提到的单片机就是说MCU啦(因为我主要玩了下51单片机,这个是什么鬼我后面会说)。

说正事儿,单片机可以说是“浓缩的计算机”而不是“浓缩的CPU”,首先维基百科对他的解释是:

a small computer on a integrated circuit。

其次是因为它内部不仅仅有运算器、控制器和寄存器这种CPU三件套,他还常封装有RAM、ROM这种存储器部件,还有I/O口供人们编程及操作。所以总结来说,他有处理器、存储器和I/O,也算是符合冯诺依曼体系的“浓缩计算机”。

  • 应用领域
    说到这种“浓缩计算机”的应用,可以说是广到没朋友。智能仪表、实时工控、通讯设备、导航系统、家用电器等等。随着物联网(IoT)时代的到来,单片机可谓再一次大展神威,我们手中的控制端可能是手机这种高级玩意儿,但是用来控制设备电路的接收端就是单片机要做的事情了。让家用电器接入网络,就是在其内部放一块带Wifi模块的单片机即可。
  • 封装类型
    刚才说到它算一个计算机,他的那些部件都是怎么放在一起的?这就涉及到封装问题了。以下列出主要的单片机封装形式。
封装 全称 中文名称
DIP Dual In-Line Package 双列直插式封装
PLCC Plastic Leaded Chip Carrier 带引线的塑料芯片封装
QFP Quad Flat Package 塑料方形扁平式封装
PGA Pin Grid Array Package 插针网络阵列封装
BGA Ball Grid Array Package 球栅阵列封装

这里不做详细描述,具体资料可点击封装名称进入查阅。


51单片机

你若是初学单片机,并且直接上网去查“单片机入门”之类的关键词,你绝对会看到“51单片机什么什么的”。谁知道51单片机是啥玩意,一开始我还以为51是个牌子,然后就开始吐槽满世界的广告啊,能不能安安静静的学个单片机啊。

什么是51单片机

百度百科的摘要是这样说的:

51单片机是对所有兼容Intel 8031指令系统的单片机的统称。该系列单片机的始祖是Intel的8004单片机,后来随着Flash rom技术的发展,8004单片机取得了长足的进展,成为应用最广泛的8位单片机之一,其代表型号是ATMEL公司的AT89系列,它广泛应用于工业测控系统之中。很多公司都有51系列的兼容机型推出,今后很长的一段时间内将占有大量市场。51单片机是基础入门的一个单片机,还是应用最广泛的一种。需要注意的是51系列的单片机一般不具备自编程能力。

维基百科是这样说的:

The page “51-MCU“ does not exist. You can ask for it to be created, but consider checking the search results below to see whether the topic is already covered.

嗯,看来这个词是国内出来的。根据百度百科上的内容我们可以了解到如下信息

Key Value
指令集 8031
始祖 Intel 8004 单片机
处理器 8位
I/O线 32条
  • 这跟“51”有个蛋的关系啊?!
    后来了解到,这款单片机的起点是Intel的MCS51系列单片机,此系列包括:8031,8051,8751,8032,8052,8752等,后来Intel普度众生,将技术传了下去。人们将后来的那些单片机都统称为51单片机。

  • 那为毛我入个门就给我个51单片机啊?!
    因为此款单片机应用非常广,而且支持在线编程(ISP, In-System Programmability),用它可以搭建比较为复杂的系统,相较于其他8位单片机(AVR、PIC)而言,他出现较早,可以获取大量学习资料。所以…

引脚

单片机的操作向来靠引脚,51系列也不除外。常见的是40脚DIP封装,以此为例说说以下几类引脚。

  • 电源
引脚 说明
Vcc 电源,用于引入正电平
GND 地线,引入负电平

关于电平的资料请参考:逻辑电平

  • 时钟
引脚 说明
XTAL2 片内震荡电路输出端
XTAL1 片内震荡电路输入端
  • 编程控制
引脚 说明
RST 用于复位
PSEN 当有扩展ROM时用,在读外部ROM时,低电平有效
ALE/PROG 当有扩展RAM时用
EA/Vpp 当有扩展ROM时用,高电平读取内部ROM,低电平读取外部ROM
  • I/O
引脚 说明
P0 双向,8位,三态
P1 准双向,8位
P2 准双向,8位
P3 准双向,8位,拥有第二功能

P3第二功能定义:

标号 第二功能 说明
P3.0 RXD 串行输入口
P3.1 TXD 串行输出口
P3.2 INT0 外部中断0
P3.3 INT1 外部中断1
P3.4 T0 定时器/计数器0外部输入端
P3.5 T1 定时器/计数器1外部输入端
P3.6 WR 外部数据存储器写脉冲
P3.7 RD 外部数据存储器读脉冲

扩展的关键字

单片机编程支持汇编和C,其他的我没用过也不太清楚。如果入门的话最好的选择就是C啦,首先你很有可能学过C,就算没学过也很好上手,去买本谭浩强的《C程序设计》吧。

除了标准的ANSI C关键字外,单片机编程还扩展了如下几个类型的关键字:

  • 位标量说明
关键字 说明
bit 声明一个位标量或位类型函数
sbit 声明一个可位寻址变量,必须定义为外部变量
  • 特殊功能寄存器声明
关键字 说明
sfr 声明一个特殊功能寄存器,必须定义为外部变量
sfr16 声明一个16位的特殊功能寄存器,必须定义为外部变量
  • 存储器类型说明
关键字 说明
data 直接寻址的内部数据存储器
bdata 可位寻址的内部数据存储器
idata 间接寻址的内部数据存储器
pdata 分页寻址的外部数据存储器
xdata 外部数据存储器
code 程序存储器
  • 其他
关键字 说明
interrupt 定义一个中断函数
reentrant 定义一个再入函数
using 定义芯片的工作寄存器

STC89C52

我的第一个实体单片机开发,就是这款了,STC89C52,可以说是增强型8051单片机。来,先看一下它的配置:

  • 处理器
Key Value
核心 MCS-51
最高运作频率 35MHz
  • 物理参数
关键字 说明
I/O 39
工作温度 商业级:0°C~75°C;工业级:-40°C~85°C
封装 DIP
  • 存储器
关键字 说明
RAM 512B
ROM 8KB
EEPROM 4KB
  • 功能
关键字 说明
定时器 3个
串口 1个,全双工
外部中断 4个
看门狗 支持
内置复位 支持
ISP/IAP 支持
  • 可选模式
关键字 说明
掉电模式 支持
空闲模式 支持

我的这款的具体型号为:STC89C52_35I_PDIP40_1550HHT352.C90C
关于标识命名规则请参阅:STC全系列芯片命名规则说明


软件准备

既然要编程,我们就需要开发环境,你可以选择Keil,也可以选择加载Keil插件的Eclipse。
代码写好后,通过编译器编译成单片机可以识别的机器码。机器码通常会保存为.hex文件,这个文件可以用那些十六进制文件查看器查看。这个文件的数据要写入到单片机的程序存储器中,通常为一个ROM。通常买的开发板都会集成串口及其芯片,还有配套的下载软件。你只需要安装好它的串口芯片的驱动程序,然后用下载器直接下载就好。如果是自己焊的板子就会麻烦一些了。这里不做说明,因为笔者还没干过。
还有一种就是通过Proteus模拟,在没有实体板的情况下可以考虑这种方式。Proteus可以模拟单片机的逻辑线路和演示效果。向里面的逻辑单片机下载程序的话,更是十分方便,在Proteus内可以很方便的操作完成。若是要模拟单片机与PC通信,那你就需要一个模拟串口连接的程序了—VSPD(Virtual Serial Port Driver)。

  • 列个清单
程序 说明
Keil 单片机开发的IDE,支持编译c、asm、c++。
单片机串口驱动 用于让计算机识别单片机
Proteus 模拟集成电路,可以演示效果
VSPD 模拟串口连接
程序下载程序 用于将编译好hex文件下载到单片机

一个软工狗的总结

作为一个软工狗,我对我们专业的理解是更专注于一个软件的工程项目的始终而不是技术实现,偏向于“工”。而单片机开发是更加贴近计算机底层的一种开发,更偏向于“理”,而且也不会采用面向对象的思想。所以,比起之前所做的网页开发也好,客户端开发也罢,都显得更加不友好。

  • 内存可能不够用
    在开发过程中会开始考虑内存够不够用这种问题,这个是现代IT学子几乎不会考虑的问题,不说电脑,手机的内存都大的一腿,但是来到单片机的地盘,你就得考虑了,考虑好不要动不动就上个double(我目前用的最长的变量类型也就是unsigned int),而且为了拓展范围长度还要考虑是不是要把变量声明称无符号类型,更严重的情况可能还要掰指头数清楚你的变量有没有声明的太多。
  • 一切从优
    也是因为硬件资源的限制,所以在做一些实现的时候你不能只考虑如何实现,还要考虑单片机吃不吃得起你的代码。所以在这个领域,面向对象这种思想是基本弃用的(面向对象的思想向来是对程序员友好,对机器不太友好的)。考虑好你程序的时间及空间复杂度,因为单片机不仅内存小,主频一般也特别低。
  • 二进制是爸爸
    好好的巩固一下之前所学的C语言吧,还有二进制操作才是爸爸级别的操作。说到这一点其实还是因为硬件资源的限制。各种且或非、异或、移位会让代码变得高效,虽然我们之前并不习惯这样做。其实很多牛逼的程序员都会很擅长使用二进制操作,因为这样很对机器的口味(1024吧,凑个整)。
  • 很好玩!
    不像客户端或web开发,我们只是在调用,很少有思维上的突破。我觉得单片机开发会更灵活,你可以去网上花十块钱不到买各种小部件添加上去调用着玩。自己设计小型自动电路是我觉得再有趣不过的事情了。
  • 怎么个开始
    要是没学过C语言,先去把C学了。若是学好了,那就了解基础知识后直接实战。关于实战,我的建议是,先在Proteus上模拟,等稍微熟练再往实体开发板里写。一个是因为Proteus模拟,能简单实践一下画板子,可以学习简单的线路,但又不像真正画板子那样麻烦。另一个是因为单片机的程序存储器的擦写次数是有限的,虽然这个次数也不用特别在意,但是能少一次就少一次嘛。到了实体开发板阶段,就不是为了验证你的程序逻辑了,而是解决实际问题,因为Proteus并不会完美模拟出实际情况,这也是由模拟到实践的真正过度。最后,可以自己买各种工具(洞洞板、锡丝、松香、烙铁、吸锡器、烙铁架)来焊个板子,在自己焊的板子上编程。这样就完成了从焊到编再到实现的一条龙DIY。

我很推荐软工狗们尝试单片机开发,因为这可以让你对计算机原理和底层有更深的认识,还可以体会到设计并实现各种DIY小器件的乐趣。望各位就着汗水尽情释放创造力吧!

玩的开心~

Jesse Hao

10 posts
4 categories
6 tags
GitHub E-Mail
© 2019 Jesse Hao
Powered by Hexo v3.8.0
|
Theme – NexT.Muse v7.0.0