未来计算的革命:探索存算一体芯片的潜力与挑战

未来计算的革命:探索存算一体芯片的潜力与挑战

前言

本文学习自:存算一体芯片深度产业报告——作者:量子位

报告链接:存算一体是啥新趋势?值得教授学者纷纷下海造芯 | 附报告下载 - 知乎 (zhihu.com),侵删!

存算一体技术概述

“存算一体”技术的起因在于传统冯诺依曼架构芯片无法满足不断提升的算力与低功耗需求。随着晶体管的体积越来越小,摩尔定律逐渐失效(会引发量子隧穿等反应),导致存储器与处理器之间的数据传输成为 CPU 性能的瓶颈,相对较长时间的数据搬运导致算力受限。

为了解决这个问题,我们需要新的芯片架构。主要有三种解决方式:

  1. 近存计算:缩短处理器芯片与存储器芯片的距离,减少数据搬运损耗。
  2. 内存储计算:处理器和存储器位于同一芯片的不同电路单元中。
  3. 内存执行计算:通过在存储器颗粒上嵌入算法,由存储器芯片内部的存储单元完成计算操作。

产业分析

image-20240129171851417

存算一体芯片落地的优势在于在算力提高的同时,芯片体积的减小和数据传输功耗的减少,使得芯片良率、成本、功耗等都有所改善。然而,实际应用的挑战主要在于评估市场需求和客户转换成本。重要的考量是,大规模采用新型芯片是否能够在成本和能耗方面带来足够的改进,以证明其经济效益;新架构芯片的先进生产工艺制造能力;客户对低功耗和高算力的需求;以及封装、测试、工具链、EDA 等相辅相成的产业链生态仍缺乏相应的研发公司。

当下大多数初创公司的思路是先聚焦特定场景,在垂直领域内站稳脚跟后技术外溢到更丰富的应用场景。主要的应用场景包括小算力低功耗场景(知存科技、九天睿芯和闪易半导体等)和大算力场景(千芯科技,后摩智能等)。

目前已知的商业模式主要分为三种:IP授权,定制/联合开发以及自主SoC芯片。

当前国内外存算一体技术发展特征如下:

  • 成立时间不同会影响技术路线选择,国内外实现产品化的公司数量不多,离规模化还有一定距离
  • 技术路线:大公司选择最容易落地的,初创公司在确保技术先进性基础上选择最容易落地的
  • 国外已形成完整的自研技术链,大规模量产上国内外均未实现突破
  • 不同的业务场景均已呈现出各自的优势,在商业模式上国内外都处在探索阶段
  • 虽然业内尚未形成完整的生态,产业链部分环节已经出现针对存算一体进行技术研发的公司

未来展望

为了推动存算一体技术的未来发展,重点应放在解决关键技术难题上,并且寻找适合快速应用推广的场景。随着新型存储器技术,尤其是RRAM和MRAM的不断进步,预计将大幅推进存算一体架构的发展。这些技术的应用,尤其是在终端推理和物联网领域,预示着存算一体技术将在这些领域发挥重要作用。为了实现从初步商业化到大规模商业化的转变,技术创新与产业发展必须紧密协同,共同推动这一技术的成熟和应用普及。

个人感悟

作为一名物联网工程专业的大学生,深入了解存算一体技术让我领略了科技创新的魅力及其在未来应用的广阔前景。虽然我尚缺乏商业模式和产品上市的实践经验,但这次学习经历让我认识到理论与实践结合的重要性,以及跨学科知识对于技术创新的贡献。

通过研究这一技术,我明白了在物联网设备设计中,如何有效融合硬件和软件来提高性能同时降低能耗的重要性。这一认识不仅提升了我的专业技能,也激发了我对如何将技术创新转化为实际应用的深入思考。

此外,我也看到了自己在商业知识和市场分析方面的不足,这提示我在未来的学习中需更多关注这些领域。我期待将这次学习的感悟转化为动力,在未来的学习和工作中不断探索、学习和创新,为智能化世界贡献我的力量。

STM32入门——基于野火 F407 霸天虎课程学习

前言

博主开始探索嵌入式以来,其实很早就开始玩 stm32 了。但是学了一段时间之后总是感觉还是很没有头绪,不知道在学什么。前前后后分别尝试了江协科技、正点原子、野火霸天虎三次 stm32 的课程学习。江协科技的 stm32f103c8t6 课程看了一段时间,感觉对一些外设的调用方法有一个基础的认知了,但是没有很明白到底在学什么;正点原子则是有点听不懂,半字也借给同学了就有一段时间没学,感觉自认为 stm32 学的有两把刷子了。后来听前辈说江协科技的 stm32 课程不如 51 单片机的质量好,其实课程讲的一般,我就想:是不是应该重新好好学一下 stm32 课程了。

这次选择的是野火的 F407 霸天虎课程,第一是听大家说,入门选野火或者正点最好。第二是野火的大师进阶篇的一些内容,涉及到一些原理等的学习讲解,我觉得对我会非常有帮助,因此正好就买了这款开发板从入门到中级到大师原理一起学习了。

本系列博文笔记主要基于野火相应课程,b站地址:野火F407开发板-霸天虎视频-【入门篇】_哔哩哔哩_bilibili ,仅供学习参考不做任何商业用途使用,侵删!

调试器介绍

我选择的是高速版,支持 SW 和 JTAG 两种连接方式。SW 模式则只需要连接 VREF(3V3), TMS(数据), TCK, RESET, GND 五个引脚。

程序烧录配置

芯片型号:STM32F407ZGTX。

DAP 仿真烧录自然非常简单。

串口一键下载 ISP 下载速度慢,不能调试,但是成本很低。可以使用 FlyMcu 等软件。

2023.11.2 补充。

警告,建议如果 flymcu 不能烧录,就不要尝试这种玩法了,看看课学学得了。因为我自己乱捣鼓一通后把开发板锁了。

下面的内容我不太清楚具体是哪一步出现了锁死 flash 的问题,总之不要尝试!学习一下理论得啦。

如果和我一样锁死了,请见野火大师篇程序,里面有一个解除写保护的代码,运行一下。

ISP 下载方式:允许我们不拆下芯片来下载。对于上个世纪嵌入式学习来说这是一个很大的突破,因为当时是要把芯片拆下来烧录编程的。

ISP 厂商出产的时候就选定了一种串行外设对芯片内部 FLASH 进行编程,我们不能修改。常用串口下载方式,成本低,但是不能调试仿真。

普通 ISP 需要手动配置 boot loader,一键 ISP 不用,硬件电路和上位机配合达到一键下载的效果(手动配置:00是用户闪存启动,10是系统 SRAM/ISP 启动,普通 ISP 要手动改跳线帽)。

一键下载电路的具体原理流程如下:

  1. RTS 低电平,Q1 是一个 PNP 三极管,导通,BOOT0 拉高。
  2. DTS 高电平,Q2 NPN 导通,U18 是一个由 EN 控制开关的模拟开关,2 脚被导通为低电平,连接1脚拉低 NRST 复位。程序下载执行。
  3. U18 模拟开关的作用是稳定电路。开发板复位的时候 DTR RTS 是不稳定的状态,如果没有这个模拟开关,DTR RTS 可能进入 ISP 状态,复位,进入 ISP 状态,复位,进入……一直运行不起来了。模拟开关右侧电容使得 VCC 需要花一点时间充电给 EN,而不是立刻激活 EN(EN 1.8V 左右)。这时候 DTR RTS 已经稳定了,可以导通 U18 12 引脚来给 NRST 复位了。

1698858387784

不过 FlyMcu 实际配置方式是反过来的,因为他的协议是 232(+3+15 是 0,-3-15 是 1),和 TTL(3.3v 是1,0v 是0)正相反。

但是实际操作的时候可能遇到一种状况:部分开发板无法使用 FlyMcu 写入。我就碰到了。解决办法是使用 stm32 cube programmer 烧录程序。

配置如下:开发板上 boot 连接 3v3,RTS DTR=0,选中 read unprotect,建立连接后再烧录程序。

但是不知道是波特率或者校验位的问题,我每次能成功烧录进去,然后过一会就显示断开找不到设备了。可能是因为波特率没有76800的选项。

STM32 介绍

正点原子网课:单片机和电脑的类比:内存是 SRAM,硬盘是 FLASH,主板是外设。

st:意法半导体公司,SoC 厂商。

m:微控制器。微控制器和微处理器相比性能比较拉一点,主频低,微处理器能跑一些大 os(linux)。

32:32位微控制器。

正点原子网课:8051,X86 属于 CISC;ARM, MIPS, RISC-V 属于 RISC.

image-20230401221223643

冯诺依曼和哈佛结构的主要区别:程序存储器和数据存储器是否分开存储。不分开是冯诺依曼,分开是哈佛。哈佛执行效率更高,冯诺伊曼资源占据更少。

CORTEX-M 系列介绍
ARM 公司(做精简指令集计算机的)只设计内核架构和授权知识产权,不参与设计芯片,给其他合作公司授权设计芯片。半导体厂商再根据架构完善周边电路并制作芯片。现在95%手机、平板都是 ARM 架构的, ARM 公司是真的牛。

image-20230401221658811

其优点在于低功耗低成本高性能,且支持16/32位双指令集。

ARM 有9个版本,从 v6 开始出现 cortex 的命名。

image-20230401221918218

随着需求不断发展,stm32 在一众 8/16位 MCU 中脱颖而出。

stm32 自带许多通信接口,如 spi i2c uart 等;扫地机,无人机,手环等都可以是 stm32 的作品。

如何选型?以下是几大类 stm32 的特点。

image-20231102013709264

本课程学习使用的开发板命名方式:

image-20231102013830412

选型:满足项目需求的前提下,尽可能选便宜的,比如主频低,功耗低,引脚少,flash 少。

引脚分配:

1698860405030

看手册的重点:

1698860693691

外设资源,芯片功能,引脚,引脚大致分类,内存,封装……

哎想起前两天面试被问,如果选型 MCU 我应该看哪些因素。我只想到了外设和内存hhh。属于是只会写代码的笨比了。这也是我开始重新看野火课程的原因之一。

寄存器

虽然正式编程没有必要用寄存器编程,通常都是库函数或者 hal 库。但是还是有必要学一下原理的。

寄存器映射

芯片视图如下。

丝印:芯片上印的信息。型号,内核,生产批次等。

引脚:左上角是有小圆点的,从左上-左下-右下-右上逆时针看。或者如果没有小圆点,把丝印方向摆正,从左上角开始看。

image-20231102121423731

芯片内部组成:

image-20231102121841662

寄存器映射:32位,2^32^=4GB,因此所有程序都需要通过内存 4GB 去映射访问。

image-20231102122058475

block7:M4 芯片内外设,比如一些通信总线这些都算外设。

block1:内存。

block0:代码。不过实际上由于设计工艺的问题,block0 block1 都只用了很少的一部分来存代码或者作为内存。

外设寄存器放在 block2 中。根据不同块速度不一样,又具体分为不同速度的外设(AHB APB)。

总线速度:AHB>APB2>APB1. APB1 是较低速的外设,包括 I2C UART SPI 看门狗等。

我们想要操作特定的外设,其实就是控制他的寄存器。控制寄存器就要找到寄存器相应的地址往里面写入数据,寄存器地址就是内存中的地址映射。

比如 GPIOF 我们想让其端口全部输出高电平。我们查找 stm32f407 手册,发现 GPIOF 的地址是 0x40021400,GPIOF 的 ODR(output data register)相对起始地址的偏移地址是14,则我们需要给 0x40021414 的地址写入数据 0xFFFF.

image-20231102123404148

51 单片机库函数中封装的 reg51.h 中,利用 sfr 定义寄存器地址;而 stm32 库函数中使用宏定义,这些就是寄存器映射操作。对芯片里一个特殊功能的内存单元起别名的过程就是寄存器映射。 给这个地址再分配一个地址交重映射,stm32 中不咋常用。

C语言对寄存器的封装

这样逐个地址,哪怕已经进行了寄存器映射,还是很复杂。

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
/* GPIO 外设基地址 */
#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000)
#define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400)
#define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800)
#define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00)
#define GPIOE_BASE (AHB1PERIPH_BASE + 0x1000)
#define GPIOF_BASE (AHB1PERIPH_BASE + 0x1400)
#define GPIOG_BASE (AHB1PERIPH_BASE + 0x1800)
#define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00)

/* GPIO 寄存器列表 */
typedef struct {
uint32_t MODER; /*GPIO 模式寄存器 地址偏移: 0x00 */
uint32_t OTYPER; /*GPIO 输出类型寄存器 地址偏移: 0x04 */
uint32_t OSPEEDR; /*GPIO 输出速度寄存器 地址偏移: 0x08 */
uint32_t PUPDR; /*GPIO 上拉/下拉寄存器 地址偏移: 0x0C */
uint32_t IDR; /*GPIO 输入数据寄存器 地址偏移: 0x10 */
uint32_t ODR; /*GPIO 输出数据寄存器 地址偏移: 0x14 */
uint16_t BSRRL; /*GPIO 置位/复位寄存器低 16 位部分 地址偏移: 0x18 */
uint16_t BSRRH; /*GPIO 置位/复位寄存器高 16 位部分 地址偏移: 0x1A */
uint32_t LCKR; /*GPIO 配置锁定寄存器 地址偏移: 0x1C */
uint32_t AFR[2]; /*GPIO 复用功能配置寄存器 地址偏移: 0x20-0x24 */
} GPIO_TypeDef;

/* 使用 GPIO_TypeDef 把地址强制转换成指针 */
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)

所有外设都是如此,首先定义总线 APB AHB 地址,然后每个外设在其基础上进行偏移,每个外设的不同部分再在该外设基址上进行偏移。

新建工程

寄存器方式

要命啊,一看名字我就不想试。寄存器新建不得麻烦死。

哎算了为了学习原理,干了。

我们尝试自己写一个寄存器的库函数来引用。

首先我们需要引用 st 官方启动文件 stmf4xx.s,具体用途后面章节再展开讲解。然后我们自己新建一个 stm32f4xx.h 文件来映射寄存器。不过只是把这个文件包含进项目,编译会报错:

1
.\Objects\led_reg.axf: Error: L6218E: Undefined symbol SystemInit (referred from startup_stm32f40xx.o).

进入启动文件后,可以看到这么一个函数:

1
2
3
4
5
6
7
8
9
10
11
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main

LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP

import 的作用相当于 extern,所以没有找到这个函数的定义,需要我们自己去定义。这就是为什么简单引入了启动文件会报错。

而 __main 是当我们定义了 main() 函数后,编译器会自动链接一些c语言库定义好的函数,用于初始化堆栈并且调用我们的 main().

注意,如果想要生成 __main 函数,必须勾选下面这一项。

image-20231102200243099

野火你讲的是真好啊。我之前草草学了学 stm32 单片机用法,比赛的时候自己想移植代码,改了启动文件也不好使,就是报错。原来是这个原因。

那么我们只需要定义这么一个函数,哪怕内容是空都无所谓。

最终我们定义的初步项目框架如下:

1698926683862

stm32f4xx.h:内容为空,有这么个东西就行。

main.c:

1
2
3
4
5
6
7
8
9
10
11
#include "stm32f4xx.h"

int main(){
while(1){

}
}

void SystemInit(){

}

好了,这个程序可以烧录到板子上的。烧录成功之后没有任何反应(因为本来程序也没做什么哈哈),但是这就是一个大进步了。

点灯——51单片机版

51单片机版就是引用 reg51.h 头文件,在其中声明了各个引脚的地址。我们只需要直接给引脚赋值即可。

调用代码:

1
2
3
4
5
6
7
8
#include "reg51.h"

#ifdef 0
void main(){
PA0=0xFE;
while(1){}
}
#endif

接下来我们需要定义 LED 灯的寄存器位置。阅读原理图如下:

1698939342199

大致可以看出,板子上的这个 RGB LED 通过三个引脚来控制 RGB 亮度。输出低电平则导通点亮。

具体输出方式是通过 ODR 进行输出。查找 stm32f4xx 中文参考手册可见:

1698940158585

1698940257044

那么我们就要给 0x4002 1400 +14 的地址赋值,让 1<<6 1<<7 1<<8 的位分别赋值为低电平.

1
2
3
4
5
6
int main(){
*(unsigned int *)(0x40021400+14)&=~(1<<6);
while(1){

}
}

然而这样也不亮。亮就怪了,stm32 寄存器是需要先做初始化配置的。

点灯——stm32 版

首先我们要设置 GPIO 模式。

1698940961543

想点灯 输出高低电平,是 01 通用输出模式。

1
2
*(unsigned int *)(0x40021400+0)&=~(3<<(6*2)); 
*(unsigned int *)(0x40021400+0)|=(1<<(6*2));

意思是先把 PF6 模式位置为00,然后赋值为01通用输出。

配置完模式之后,还需要配置时钟,stm32 每个外设都需要配置时钟。

前面提到过 GPIO 是在 AHB1.

1699103516061

1699103724261

全部代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "stm32f4xx.h"

int main(){
//RCC
*(unsigned int *)(0x40023800+0x30)|=(1<<5);

//Mode
*(unsigned int *)(0x40021400+0)&=~(3<<(6*2));
*(unsigned int *)(0x40021400+0)|=(1<<(6*2));


*(unsigned int *)(0x40021400+0x14)&=~(1<<6);
while(1){

}
}

void SystemInit(){

}

接下来,我们把这几个地址值提取出来,宏定义映射寄存器。

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
//stm32f4xx.h
/* 用来存放寄存器映射相关的代码 */
#define RCC_AHB1_ENR *(unsigned int *)(0x40023800+0x30)
#define GPIOF_MODER *(unsigned int *)(0x40021400+0)
#define GPIOF_ODR *(unsigned int *)(0x40021400+0x14)

//main.c
#include "stm32f4xx.h"

int main(){
//RCC
RCC_AHB1_ENR|=(1<<5);

//Mode
GPIOF_MODER&=~(3<<(6*2));
GPIOF_MODER|=(1<<(6*2));


GPIOF_ODR&=~(1<<6);
while(1){

}
}

void SystemInit(){

}

点灯——流水灯闪烁

利用软件延时实现 RGB 流水灯闪烁。很简单,前面已经看了3个 LED 通道 PF678 了。

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
#include "stm32f4xx.h"

void delay_ms(int time);

int main(){
//RCC
RCC_AHB1_ENR|=(1<<5);

//Mode
GPIOF_MODER&=~(3<<(6*2));
GPIOF_MODER|=(1<<(6*2));
GPIOF_MODER&=~(3<<(7*2));
GPIOF_MODER|=(1<<(7*2));
GPIOF_MODER&=~(3<<(8*2));
GPIOF_MODER|=(1<<(8*2));

while(1){
GPIOF_ODR|=(7<<6);
GPIOF_ODR&=~(1<<6);
delay_ms(1000);
GPIOF_ODR|=(7<<6);
GPIOF_ODR&=~(1<<7);
delay_ms(1000);
GPIOF_ODR|=(7<<6);
GPIOF_ODR&=~(1<<8);
delay_ms(1000);
}
}

void SystemInit(){

}

//毫秒级的延时
void delay_ms(int time)
{
int i=0;
while(time--)
{
i=4000;
while(i--) ;
}
}

点灯——GPIO 具体功能框图对应

GPIO:通用输入输出引脚。我们可以通过编程来输出或者读取数据。大部分 GPIO 是已经连接、定义好了一些功能(比如上面尝试过的 PF6 LED),有的引脚有多个功能支持重新映射。

STM32 GPIO 除了 adc 是 3.3v,其他 GPIO 都是 5v 容忍。

GPIO 框图(重点)如下:

image-20231104221140325

先从输出开始看。最右侧的 IO 引脚是连接在芯片周围一圈的144个引脚之一。除了 IO 引脚,此图中其他所有部分都是封装在芯片内部我们看不到的。

往左有两个保护二极管。当电压大于 5V,电流会往上 VDD_FT 走。当电压为负电压,电流会由 VSS 往 IO 引脚走。

上下拉电阻:比武外接一个低电平工作的设备,但是我们不希望一上电外设就工作,可以设置上拉电阻,稳定一段时间。

GPIO 输出的数据来源:复位寄存器 BSRR,或者 ODR 设置(图中的3下路部分)。复位寄存器高16位复位(写1置0)低16位置位(写1置1),置位优先级更高。

配置 GPIO 模式(输入/输出,选择哪一路)通过前面用过的 MODER 配置。

输出模式(图中输出控制部分)配置端口输出类型寄存器 OTYPER,比如推挽输出,开漏输出。

推挽输出:有直接驱动能力,输出0就是低电平,输出1就输出可以工作的高电平。原理是采用了一个放大的电路?

1699356797818

输入(INT)为高电平时,反向后 PMOS 导通,输出高电平。输入为低电平时,反向后 NMOS 导通,输出低电平。我们可以用一个小电流去驱动出来一个大电流。

开漏输出:自己本身没有输出高电平的手段。低电平可以接地,高电平没有 PMOS 管,是浮空状态。需要外接一个电阻。

1699357078909

stm32 输出 5V 电压的方法就是开漏输出外接电阻。通过接两个三极管的方式反向。

1699357296750

框图中的模拟部分输入输出则不用配置这些模式信息,直接由外设接到保护二极管再接到输出引脚。

框图中的输入部分经过保护电压后,还需要施密特触发器调整一下。比如原来电压的数值并非精确的0或 3.3V,施密特触发器将高于 1.8V 的全部视作1,低于的全部视作0后输入芯片。模拟部分则不需要经过施密特触发器。

因此配置 GPIO 输出的步骤如下:

  1. GPIO 功能,通用输出、复用功能、模拟输入等 MODER;
  2. 输出推挽 or 开漏 OTYPER;
  3. 输出速度 OSPEEDR;
  4. 上下拉电阻是否需要开启 PUPDR;
  5. 具体输出内容 BSRR or ODR.

输入部分后面输入实验介绍~

按整个流程重新串一遍代码,如下:(其实和前面差不多,就是重新按照流程串了一遍)

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
/* 用来存放寄存器映射相关的代码 */
#define RCC_BASE (unsigned int *) 0x40023800
#define GPIOF_BASE (unsigned int *) 0x40021400

#define RCC_AHB1ENR *(RCC_BASE+0x30)

#define GPIOF_MODER *(GPIOF_BASE+0x00)
#define GPIOF_OSPEEDR *(GPIOF_BASE+0x08)
#define GPIOF_PUPDR *(GPIOF_BASE+0x0C)
#define GPIOF_ODR *(GPIOF_BASE+0x14)
#define GPIOF_BSRR *(GPIOF_BASE+0x18)

//main.c
#include "stm32f4xx.h"

int main()
{
RCC_AHB1ENR |= (1<<5);
GPIOF_MODER &= ~(3<<(6*2));
GPIOF_MODER |= (1<<(6*2));
while (1)
{
}
}

void SystemInit()
{
}

烧录前记得勾选:use MicroLib.

构建库方式

点灯——自己尝试构建库函数版

寄存器方法了解到这里就好,野火课程主要是库函数写代码。首先我们自己尝试构建一下库函数。

还是基于上次实验代码修改即可。首先对 .h 文件做一些修改:

1
2
3
4
#ifndef __STM32F4XX_H__
#define __STM32F4XX_H__

#endif

这个是防止多次引用头文件重复定义。

然后,像之前一条条定义太麻烦了。其实我们注意到每个寄存器都是4字节,我们可以用固定大小的结构体定义。比如 GPIO ABCDEF 结构都一样,我们只需要统一定义结构体和各自的基址即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdint.h>	// 包含了 uint32_t uint16_t
/* GPIO寄存器列表 */
typedef struct
{
uint32_t MODER; /*GPIO模式寄存器 地址偏移: 0x00 */
uint32_t OTYPER; /*GPIO输出类型寄存器 地址偏移: 0x04 */
uint32_t OSPEEDR; /*GPIO输出速度寄存器 地址偏移: 0x08 */
uint32_t PUPDR; /*GPIO上拉/下拉寄存器 地址偏移: 0x0C */
uint32_t IDR; /*GPIO输入数据寄存器 地址偏移: 0x10 */
uint32_t ODR; /*GPIO输出数据寄存器 地址偏移: 0x14 */
uint16_t BSRRL; /*GPIO置位/复位寄存器 低16位部分 地址偏移: 0x18 */
uint16_t BSRRH; /*GPIO置位/复位寄存器 高16位部分 地址偏移: 0x1A */
uint32_t LCKR; /*GPIO配置锁定寄存器 地址偏移: 0x1C */
uint32_t AFR[2]; /*GPIO复用功能配置寄存器 地址偏移: 0x20-0x24 */
} GPIO_TypeDef;

# define GPIOF ((GPIO_TypeDef *)GPIOF_BASE)

main.c 中可以把对应寄存器替换为 GPIOF->寄存器名了。

然后我们直接对寄存器做操作,还是有点直接了,最好是我们不需要关注寄存器有哪些,直接调用一个 GPIO 设置函数即可使用,封装性可移植性都会好很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//stm32f4xx_gpio.c
void GPIO_SetBits(GPIO_TypeDef * GPIOx, uint16_t GPIO_Pin){
GPIOx->BSRRL=GPIO_Pin;
}

void GPIO_ResetBits(GPIO_TypeDef * GPIOx, uint16_t GPIO_Pin){
GPIOx->BSRRH=GPIO_Pin;
}

//stm32f4xx_gpio.h
#include "stm32f4xx.h"

#ifndef __STM32F4XX_GPIO_H__
#define __STM32F4XX_GPIO_H__

void GPIO_SetBits(GPIO_TypeDef * GPIOx, uint16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef * GPIOx, uint16_t GPIO_Pin);

#endif

比如首先我们简单写了这样一个置位函数,使用方法为 GPIO_SetBits(GPIOF_Base,1<<6) .

以及我们可以在 stm32f4xx_gpio.h 里批量定义:

1
2
3
#define GPIO_Pin_6          (uint16_t)(1<<6)
#define GPIO_Pin_7 (uint16_t)(1<<7)
#define GPIO_Pin_8 (uint16_t)(1<<8)

这样 GPIO 使用用 Set Reset 函数已经非常规范了。那么初始化操作我们也可以封装成一个函数。

初始化需要设置 MODER PUPDR OSPEEDR OTYPER,我们可以定义一个结构体用于存储这些初始化变量,初始化的时候新建一个这样的结构体并赋值,传入初始化函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//stm32f4xx_gpio.h
//每个模块具体的值可以定义一个枚举类型。
typedef enum
{
GPIO_Mode_IN=0x00;
GPIO_Mode_OUT=0x01;
GPIO_Mode_AF=0x02;
GPIO_Mode_AN=0x03;
}GPIOMode_TypeDef;

typedef struct
{
uint16_t GPIO_Pin;
GPIOMode_TypeDef MODER;
GPIOPuPd_TypeDef PUPDR;
GPIOOType_TypeDef OTYPER;
GPIOOSpeed_TypeDef OSPEEDR;
}GPIO_InitTypeDef;

具体使用的时候首先我们初始化一个 GPIO_InitTypeDef 变量,并且给其中的每一个子元素都赋值。然后传入 GPIO_Init 函数中,里面就是一系列根据手册而来的位操作,这里我感觉前面原理懂差不多就不用非跟着敲了。

分析 stm32 固件库函数

前面基本上都是了解固件库编程,从51过渡到 stm32. 后面所有固件编程固件库的使用方法都和前面的 GPIO 类似。

固件是什么?其实就是程序,固化到 EEPROM 或 FLASH 中,操作最底层的设备。不是具体的应用,而是只操作最底层的设备。比如点灯算应用,给应用工程师提供库函数的工作是固件工程师的。

stm32 官方 stmf4 固件库下载地址:

STM32标准外设软件库: 相关产品

image-20231108222206160

1
2
3
4
5
6
7
8
9
10
11
$ ls
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2023/11/8 22:16 Libraries
d----- 2023/11/8 22:16 Project
d----- 2023/11/8 22:17 Utilities
d----- 2023/11/8 22:17 _htmresc
-ar--- 2023/11/8 22:15 88007 Package_license.html
-ar--- 2023/11/8 22:15 19611 Package_license.md
-ar--- 2023/11/8 22:15 152599 Release_Notes.html
-ar--- 2023/11/8 22:15 37185187 stm32f4xx_dsp_stdperiph_lib_um.chm

.chm:使用帮助文档。

.html .md:一些版本更新,包许可证相关信息。

Utilities:一些第三方其他软件。

Project:样例,模板。

Libraries:库,CMSIS 是一些 ARM 公司的标准,Driver 是固件。inc 是头文件,src 是c文件。

我们根据上节课写的项目来进行库函数文件功能分析。

文件名 所属类别 功能
startup_stm32f40xx.s 片上外设 汇编启动文件
stm32f4xx.h 片上外设 外设寄存器映射
system_stm32f4xx.c / system_stm32f4xx.h 片上外设 初始化系统时钟
stm32f4xx_xxxx.c / stm32f4xx_xxxx.h 内核 内核寄存器映射
core_cm4.h 内核 内核寄存器映射
core_cmFunc.h / core_cmSimd.h 内核 内核外设的一些操作函数
misc.c / misc.h 内核 中断相关函数(优先级分组,系统中断)
stm32f4xx_it.c / stm32f4xx_it.h 内核 中断服务函数(所有中断入口)
main.c main 函数
  1. startupxxxx.s:启动文件。
  2. stm32f4xx.h:外设寄存器映射。
  3. 跳到 system_Init 函数,这个函数当时我们为了执行只写了一个空函数,而 stm32 官方固件库模板里面是有的,在 system_stm32f4xx.c 里,初始化系统时钟。
  4. stm32f4xx.c:具体外设驱动,比如上节课写的 gpio。
  5. core_cm4.h:内核寄存器映射。
  6. misc:中断。

构建库函数

创建一个通用的模板,后面写程序直接使用这个模板。

1
2
3
4
5
6
7
8
9
$ ls
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2023/11/8 23:27 Libraries
d----- 2023/11/8 23:27 Listing
d----- 2023/11/8 23:27 Output
d----- 2023/11/8 23:27 Project
d----- 2023/11/8 23:27 User
-a---- 2020/2/27 13:45 401 keilkill.bat

前面都是新建的文件夹,keilkill.bat 是从 keil 编译程序中复制出来的一个脚本,可以删掉中间文件。

把固件库 Lib 里的 CMSIS 和 Driver 文件拷贝到 Libraries 文件夹中。CMSIS 中只保留 Device Include 文件夹。Device 中包含外设相关(比如 stm32f4xx.h system_stm32f4xx),Include 中只包含内核相关。

把 main.c stm32f4xx_it.c / stm32f4xx_it.h stm32f4xx_conf.h 拷贝到 User 文件夹中。

在 Project 文件夹里可以包含多给项目文件,不光只有 Keil 的。比如 IAR 的我们新建一个 IAR 文件夹,Keil 我们新建一个 RVMDK(uv5) 文件夹。RealView 是包含不止 MDK 的开发工具集合的称呼,MDK 是 MCU 开发工具集成包,uVersion 是 IDE,Keil 是公司名字。

uVision 里新建工程,新建在 RVMDK(uv5) 文件夹下。

新建组、添加文件如下:

image-20231109002542997

STM32F4xx_StdPeriph_Driver 添加 STM32F4xx_StdPeriph_Driver/src 下的所有文件,屏蔽掉 dma2d fmc ltdc,后两个是 sd 和 lcd 屏幕组件。

头文件如下:

image-20231109002823229

宏定义如下:

USE_STDPERIPH_DRIVER,STM32F40_41xxx

我现在好想明白为什么宏定义在这里了,这样后面换单片机型号的时候可以直接修改这个宏定义。

PS:我下载的是 1.8.1 版本 stm32f4xx.h 库函数,里面出现了一段重复定义导致编译产生了200多个 warning。我把下面那一段删掉了就好了。

jingqing3948_1-1699463889291.png

Output 里设置 Output 文件夹,不然都在 Proj 里太乱。

记得勾选 MicroLib。

点灯——官方库函数版

在 User 文件夹中新建 LED 文件夹,里面新建 bsp_led.c,代表板级支持包 LED 代码,也就是只针对我们当前这一款开发板的点灯程序。

  1. 设置时钟:rcc 时钟,在 stm32f4xx_rcc.c 中:

  2. ```c
    /**

    • @brief Enables or disables the AHB1 peripheral clock.
    • @note After reset, the peripheral clock (used for registers read/write access)
    •     is disabled and the application software has to enable this clock before 
      
    •     using it.   
      
    • @param RCC_AHBPeriph: specifies the AHB1 peripheral to gates its clock.
    •      This parameter can be any combination of the following values:
      
    •        @arg RCC_AHB1Periph_GPIOA:       GPIOA clock
      
    •        @arg RCC_AHB1Periph_GPIOB:       GPIOB clock 
      
    •        @arg RCC_AHB1Periph_GPIOC:       GPIOC clock
      
    •        @arg RCC_AHB1Periph_GPIOD:       GPIOD clock
      
    •        @arg RCC_AHB1Periph_GPIOE:       GPIOE clock
      
    •        @arg RCC_AHB1Periph_GPIOF:       GPIOF clock
      
    •        @arg RCC_AHB1Periph_GPIOG:       GPIOG clock
      
    •        @arg RCC_AHB1Periph_GPIOG:       GPIOG clock
      
    •        @arg RCC_AHB1Periph_GPIOI:       GPIOI clock
      
    •        @arg RCC_AHB1Periph_GPIOJ:       GPIOJ clock (STM32F42xxx/43xxx devices) 
      
    •        @arg RCC_AHB1Periph_GPIOK:       GPIOK clock (STM32F42xxx/43xxx devices)  
      
    •        @arg RCC_AHB1Periph_CRC:         CRC clock
      
    •        @arg RCC_AHB1Periph_BKPSRAM:     BKPSRAM interface clock
      
    •        @arg RCC_AHB1Periph_CCMDATARAMEN CCM data RAM interface clock
      
    •        @arg RCC_AHB1Periph_DMA1:        DMA1 clock
      
    •        @arg RCC_AHB1Periph_DMA2:        DMA2 clock
      
    •        @arg RCC_AHB1Periph_DMA2D:       DMA2D clock (STM32F429xx/439xx devices)  
      
    •        @arg RCC_AHB1Periph_ETH_MAC:     Ethernet MAC clock
      
    •        @arg RCC_AHB1Periph_ETH_MAC_Tx:  Ethernet Transmission clock
      
    •        @arg RCC_AHB1Periph_ETH_MAC_Rx:  Ethernet Reception clock
      
    •        @arg RCC_AHB1Periph_ETH_MAC_PTP: Ethernet PTP clock
      
    •        @arg RCC_AHB1Periph_OTG_HS:      USB OTG HS clock
      
    •        @arg RCC_AHB1Periph_OTG_HS_ULPI: USB OTG HS ULPI clock
      
    • @param NewState: new state of the specified peripheral clock.
    •      This parameter can be: ENABLE or DISABLE.
      
    • @retval None
      */
      void RCC_AHB1PeriphClockCmd(uint32_t RCC_AHB1Periph, FunctionalState NewState)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22

      其他部分基本也是参照库函数(主要是 stm32f4xx_gpio.h)最终呈现如下:

      ```c
      #include "bsp_led.h"

      void LED_GPIO_Config(void){
      //RCC set function in stm32f4xx_rcc.h
      RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);
      {
      //Init structure
      GPIO_InitTypeDef GPIO_InitStructure;
      GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT;
      GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;
      GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;
      GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;
      GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
      //init function
      GPIO_Init(GPIOF,&GPIO_InitStructure);
      }

      }

    置位可以使用 GPIO_SetBitsGPIO_ResetBits

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    int main()
    {
    int i;
    LED_GPIO_Config();


    *(unsigned int *)(0x40021400+0x14)&=~(1<<6);
    while (1)
    {
    GPIO_ResetBits(GPIOF,GPIO_Pin_6);
    i=12000000;
    while(i--);
    GPIO_SetBits(GPIOF,GPIO_Pin_6);
    i=12000000;
    while(i--);
    }
    }

    没有上下拉的时候推挽输出会直接被 ODR 值所影响,哪怕没有赋值其中本来的值也会影响。所以推挽输出无上下拉,不置位 LED 也会被点亮,因为 ODR 默认值0.

输入——按键点灯

开发板按键电路如下:

image-20231109191525189

按键未按下接地,按下后为高电平。电容起到消抖作用,软件处理就不需要手动延时消抖了。

编程没啥难度,就是改了一下输入模式。使用 ReadInputDataBits 读取。

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
//bsp_button.c
#include "bsp_button.h"

void Button_GPIO_Config(void){
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
{
GPIO_InitTypeDef GPIOInitStruct;
GPIOInitStruct.GPIO_Mode=GPIO_Mode_IN;
GPIOInitStruct.GPIO_Pin=GPIO_Pin_0;
GPIOInitStruct.GPIO_OType=GPIO_OType_PP;
GPIOInitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIOInitStruct.GPIO_PuPd=GPIO_PuPd_NOPULL;

GPIO_Init(GPIOA,&GPIOInitStruct);
}
}

//main.c
#include "stm32f4xx.h"

#include "bsp_led.h"
#include "bsp_button.h"

int main()
{
//RCC
LED_GPIO_Config();
Button_GPIO_Config();
while (1)
{
if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0))GPIO_SetBits(GPIOF, GPIO_Pin_6);
else GPIO_ResetBits(GPIOF, GPIO_Pin_6);
}
}

实现按键按下后翻转:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
//RCC
LED_GPIO_Config();
Button_GPIO_Config();
while (1)
{
if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==Bit_SET){
while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==Bit_SET);
GPIO_ToggleBits(GPIOF, GPIO_Pin_6);
}
}
}

要等到按键松开的时候再翻转,轮询直到松开。

位带操作

之前51单片机常见位定义。比如 PA 引脚有8个 IO 口,我们可以定义 sbit LED1=PA^0 这样单独操作某一位。

stm32 里没有直接的位定义方式。一种解决办法是我们利用与或操作不影响其他位的同时操作特定位;另一种就是位带操作。

stm32 里有一部分别名区域,用于映射外设、SRAM 中特定的位带区,我们操作这一部分别名区域时就可以实现对外设、SRAM 位带区与的位操作。

片上外设位带区:0X4000 00000X400F 0000,别名区:0X4200 00000X43FF FFFF,包含 APB12,AHB1 外设。

SRAM 位带区:0X2000 00000X200F 0000,别名区:0X2200 00000X23FF FFFF

image-20231109220918350

外设地址 A 别名地址为:AliasAddr= =0x42000000+ (A-0x40000000)84 +n*4 (n是位序号)

SRAM 地址 A 别名地址为:AliasAddr= =0x22000000+ (A-0x20000000)84 +n*4

扩大了32倍,可以对32位寄存器中的每一位进行操作。

统一公式:\#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x000FFFFF)<<5)+(bitnum<<2))

使用:比如我们操作一个 GPIO 的位操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x000FFFFF)<<5)+(bitnum<<2))
// 把一个地址转换成一个指针
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
// 把位带别名区地址转换成指针
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))

// GPIO ODR 和 IDR 寄存器地址映射
#define GPIOF_ODR_Addr (GPIOF_BASE+20)
#define GPIOF_IDR_Addr (GPIOF_BASE+16)

// 单独操作 GPIO 的某一个 IO 口,n(0,1,2...15),
// n 表示具体是哪一个 IO 口
#define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出
#define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入

//使用示例
PFout(6)= 0;

这个概念学学就好,好像实际应用没啥意义。我们微处理器考试也考过hh。

Win11磁贴配置

前言

最近电脑还是升级到 win11 了。我之前采用的美化方案是桌面上的图标全部移到 win10 开始菜单里的全屏菜单上,用磁贴贴一排。每次要访问文件的时候都去开始菜单里找,而不是放在桌面上,这样桌面也可以空出来欣赏壁纸。参考配置链接:如何让Windows 10系统桌面变得更好看? - 知乎 (zhihu.com)

但是升级到 win11 对我而言影响最大的就是压根没有全屏桌面和磁贴功能了。因此我搜了很多解决方案,加上一些自己的改进,最终把桌面磁贴恢复成如上图所示。一个自己喜欢看的桌面还是会对生产力的提高有很大帮助的。

image-20230712234132339

软件安装:start11

首先,恢复开始菜单这一操作最离不开的就是 start11 这款软件。他让 win11 的开始菜单又有了全屏菜单选项,也支持了自定义磁贴在上面。虽然调整磁贴位置的时候偶尔会花几秒重启,但大多数时候还是没问题的。

image-20230712235022366

正版软件是免费试用的,欢迎付费支持原作者。或采用博主的同款方案:

链接:https://pan.baidu.com/s/1HY0WuV7ynlXdBI7qfuWZzg?pwd=1fru
提取码:1fru
–来自百度网盘超级会员V2的分享

start11 配置

首先如果是按博主的磁贴方法配置,就要选“win10配置”。或者你觉得其他风格也还不错都可以选。并且 ENABLE start11.

image-20230712235704628

点击“配置菜单”,进行如下配置:

image-20230712235738298

在“自定义菜单视觉外观”里,可以设置全屏菜单的颜色、透明度等,比如我使用的是有一定透明度的毛玻璃的样式。

image-20230713000601091

然后在“控制”栏里设定如何打开 start11,确保可以打开:

接着,点击 win 图标就可以打开全屏菜单了。

磁贴配置

对于大部分软件,只要右键-固定到开始屏幕/固定到 start11,就可以在全屏菜单里看到刚刚贴上的磁贴了。

如果贴失败,可以尝试以下的方法:

  • 右键快捷方式,点击“打开文件所在位置”,再尝试把该文件的 .exe 文件固定到开始屏幕。
  • 反复尝试,因为可能有一定的延迟。可以取消固定再次固定,等待一会看开始菜单是否出现。

然后可以手动分组磁贴(把他们移到临近的位置),调整磁贴背景色,调整磁贴大小(有小正方形,中正方形,长方形,大正方形四种可以选择),调整磁贴布局位置。

image-20230713000428410

图片磁贴配置

这里是最自由发挥的部分。高情商:自由发挥。低情商:都要自己做很麻烦。

之前 win10 是有一款快捷工具可以输入自定义图片,按自己想要的格式裁剪并自动在全屏菜单中输出的,叫 Tile Genie.

image-20230713000854968

但是它好像并不能在 start11 中使用。我尝试了一下导出的都是不能显示的图片块,所以只能放弃这种方法。如果读者的 Tile Genie 是没有问题可以正常显示图片那再好不过了,后面的内容都可以不用看了。

如果导出失败……我采用的方法是手动裁剪固定图片。很笨,但是有结果。

image-20230713001102790

首先,自行裁剪图片,计算公式为:中正方形 150*150,长方形 306*150,大正方形 306*306,边界线是6(像素).

我采用的图像裁剪方法是:免费在线裁剪图像文件 (iloveimg.com)

1689178422454

然后把导出图片找一个合适的地方存储起来,注意贴上磁贴之后就不能再移动修改这些图片了。

在全屏菜单中右键-固定文件,选定文件路径添加。

image-20230713001454756

刚固定上是这种形式:

1689178534062

然后右键-调整大小,调整为想要的大小。

image-20230713001613170

image-20230713001632462

最后一步,右键-图标-选择自定义磁贴图像,再次选择此文件,然后他就被当做图标全屏显示了。

image-20230713001653804

ubuntu 安装 emscripten 时 install latest 安装报错问题

学习官网参考:Compiling a New C/C++ Module to WebAssembly - WebAssembly | MDN (mozilla.org)

报错信息

形如:

1
2
Error: Downloading URL 'https://storage.googleapis.com/webassembly/emscripten-releases-builds/linux/b90507fcf011da61bacfca613569d882f7749552/wasm-binaries.tbz2': <urlopen error [Errno 104] Connection reset by peer>
error: installation failed!

OS:

1
2
$ uname -a
Linux jingqing 5.19.0-35-generic #36~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Feb 17 15:17:25 UTC 2 x86_64 x86_64 x86_64 GNU/Linux

产生错误原因分析

emsdk install latest报错(因为从谷歌中下载,cmd中命令形式访问不到google)
版权声明:本文为CSDN博主「小白啥时候能进阶成功」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34754747/article/details/103815349

非常感谢博主的答疑解惑,不过我没有看得太懂原文中的解决方案,网上搜到的大多数方案也是 windows 环境下的解决方案,因此我决定自己写一个 ubuntu 系统下的补安装。

解决方案

说白了就是我自己复制链接到浏览器里,下载安装这几个包,放到 emsdk 的指定位置。

这里有两个要注意的点,这一部分主要是分析,不想看的同学可以直接跳到具体步骤处:

  1. emsdk install 的默认安装规则是:不管你有没有安装过这些包,我 install latest 都是重新安装,保证最新版本。但是现在问题是 install latest 有问题,我要手动安装包放进去。
    我们打开 emsdk.py 通过搜索关键词可以找到报错信息的位置:

    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
    # ./emsdk.py:675

    # On success, returns the filename on the disk pointing to the destination file that was produced
    # On failure, returns None.
    def download_file(url, dstpath, download_even_if_exists=False, filename_prefix=''):
    debug_print('download_file(url=' + url + ', dstpath=' + dstpath + ')')
    file_name = get_download_target(url, dstpath, filename_prefix)
    if os.path.exists(file_name) and not download_even_if_exists:
    print("File '" + file_name + "' already downloaded, skipping.")
    return file_name
    try:
    u = urlopen(url)
    mkdir_p(os.path.dirname(file_name))
    with open(file_name, 'wb') as f:
    file_size = get_content_length(u)
    if file_size > 0:
    print("Downloading: %s from %s, %s Bytes" % (file_name, url, file_size))
    else:
    print("Downloading: %s from %s" % (file_name, url))

    file_size_dl = 0
    # Draw a progress bar 80 chars wide (in non-TTY mode)
    progress_max = 80 - 4
    progress_shown = 0
    block_sz = 256 * 1024
    if not TTY_OUTPUT:
    print(' [', end='')
    while True:
    buffer = u.read(block_sz)
    if not buffer:
    break

    file_size_dl += len(buffer)
    f.write(buffer)
    if file_size:
    percent = file_size_dl * 100.0 / file_size
    if TTY_OUTPUT:
    status = r" %10d [%3.02f%%]" % (file_size_dl, percent)
    print(status, end='\r')
    else:
    while progress_shown < progress_max * percent / 100:
    print('-', end='')
    sys.stdout.flush()
    progress_shown += 1
    if not TTY_OUTPUT:
    print(']')
    sys.stdout.flush()
    except Exception as e:
    errlog("Error: Downloading URL '" + url + "': " + str(e))
    if "SSL: CERTIFICATE_VERIFY_FAILED" in str(e) or "urlopen error unknown url type: https" in str(e):
    errlog("Warning: Possibly SSL/TLS issue. Update or install Python SSL root certificates (2048-bit or greater) supplied in Python folder or https://pypi.org/project/certifi/ and try again.")
    rmfile(file_name)
    return None
    except KeyboardInterrupt:
    rmfile(file_name)
    exit_with_error("aborted by user, exiting")
    return file_name

    大致一看能看明白逻辑,如果 download_even_if_exists = True 那么无论包是否已经存在都要安装,否则为 False 就只安装不存在的包,我们需要为 False。

    搜索函数名查看在哪里使用了这个函数:

    1
    2
    3
    # ./emsdk.py:1411

    received_download_target = download_file(url, download_dir, not KEEP_DOWNLOADS, filename_prefix)

    这个 KEEP_DOWNLOADS 是一个环境变量,默认为0,我们需要他为1,传入函数的参数则为0(False),即已存在文件不再重复下载。

    在终端输入 ./emsdk --help 可以看到提示信息如下:

    1
    2
    3
    4
    5
    Environment:
    EMSDK_KEEP_DOWNLOADS=1 - if you want to keep the downloaded archives.
    EMSDK_NOTTY=1 - override isatty() result (mainly to log progress).
    EMSDK_NUM_CORES=n - limit parallelism to n cores.
    EMSDK_VERBOSE=1 - very verbose output, useful for debugging.

    也就是说只要安装时单独指定此变量值为1即可。

  2. 第二步就是如何下载文件了。下载什么文件?放到哪个目录下?

    这里大家可以通过 download_file 的 print debug 调试来查看他校验文件是否存在是去哪里校验的,我就不再具体展开讲调试步骤了,结论就是:他在 emsdk/downloads/ 目录下先查找一下待下载的压缩包是否存在,那么我们复制报错信息中的 url 下载文件到这个 downloads 文件夹下即可(没有就新建)。

具体步骤

  1. 首先要安装所缺的所有包,一个个安装,报错信息里提示什么安装什么。比如文章开头的报错信息中下载链接是:https://storage.googleapis.com/webassembly/emscripten-releases-builds/linux/b90507fcf011da61bacfca613569d882f7749552/wasm-binaries.tbz2,就先安装这个。

    node: https://storage.googleapis.com/webassembly/emscripten-releases-builds/linux/b90507fcf011da61bacfca613569d882f7749552/wasm-binaries.tbz2

    wasm-binaries: https://storage.googleapis.com/webassembly/emscripten-releases-builds/linux/b90507fcf011da61bacfca613569d882f7749552/wasm-binaries.tbz2

    安装完成后要重命名 b90507fcf011da61bacfca613569d882f7749552-wasm-binaries.tbz2。

  2. 移入 emsdk/downloads 文件夹下,不用解压。

  3. 执行 EMSDK_KEEP_DOWNLOADS=1 变量赋值。

  4. 执行 ./emsdk install latest

1
2
3
4
5
6
7
jingqing3948@jingqing:~/Webassembly/emsdk$ ./emsdk install latest
Resolving SDK alias 'latest' to '3.1.44'
Resolving SDK version '3.1.44' to 'sdk-releases-b90507fcf011da61bacfca613569d882f7749552-64bit'
Installing SDK 'sdk-releases-b90507fcf011da61bacfca613569d882f7749552-64bit'..
Skipped installing node-16.20.0-64bit, already installed.
Skipped installing releases-b90507fcf011da61bacfca613569d882f7749552-64bit, already installed.
All SDK components already installed: 'sdk-releases-b90507fcf011da61bacfca613569d882f7749552-64bit'.

好哎,看来是自己单独安装的文件包都可以用,他会自己解压文件包后提示 All SDK components already installed。

接下来就是下一步:./emsdk activate latest.

最后是 source ./emsdk_env.sh 配置好环境变量。

北邮国院物联网RFID课程笔记

[TOC]

主要围绕提纲里的所有问题展开,没有拓展内容,Exam oriented Study。

关注微信公众号:灰海宽松,回复 “RFID” 可获取本文pdf格式。

RFID

1. Introduction

Comparison of different automatic identification technologies

首先明确一下比较对象。human identification(cost too high)是人力识别就不用说了。

fingerprint identification:

  • stability 稳定,精确度高;
  • high speed, 快速匹配;
  • security issues: 容易被复制。

face recognition:

  • easy to be influenced by surroundings, hair, age…

speech recognition:

  • easy to use and accept by user;
  • not involve privacy;
  • due to international standards, is hard to promoting

1d barcode:

  • limit storage capacity, 点线组合少;
  • need to combine with database;
  • barcode size is large;
  • poor fault tolerance, 本来就需要摄像头可见,如果被污损遮挡很容易就无法识别;

2d barcode recognition:

  • larger storage capacity;
  • high information density;
  • powerful fault tolerance;
  • support for encryption 容量大了就支持更多编码解码等安全措施了。

rfid:

  • low cost;
  • low power consumption;
  • high accuracy;
  • non-contract, fast speed; 不用接触(哪怕是visual,薄纱条码)
  • certain computing and storage capabilities;

主要考虑各个的缺点,人脸和声音特征点多速度慢,而且人脸容易被影响,声音由于国际标准技术难以提升;条码需要视觉可见;指纹容易被盗取。

The main features of RFID

  • Non-contact automatic and rapid identification 快速薄纱复杂的人脸和声音,无接触薄纱条码和指纹

  • Permanently store a certain amount of data 永久存储一定量数据

  • Simple logical processing 其包含的简单逻辑电路允许做一定的逻辑处理,比如安全协议、算法

  • Reflection signal strength is affected by the distance and other factors significantly 信号受到距离,读写器功率,其他信号,其他标签的干扰

  • Low cost, can be deployed at a large scale

Constraints of RFID technology

1687019780697

Core technologies of RFID

Anti-collision mechanism:rfid并不支持传统的cmsa/ca无线通信协议,需要采取一些措施防碰撞(reader-reader, tag-reader, tag-tag)

Efficient information storage, retrieval and mining: 尽量节能的信息存储,检索,挖掘

Make full use of the attenuation laws of backscatter signal to assist in positioning and mobile behavior sensing: 我们知道rfid信号会随着距离衰减。反之我们也可以利用这一点来定位物体位置和移动行为感知。

Security certification and privacy protection: 如何利用逻辑门电路校验安全性。

The advantage of RFID in IoT, and the development trend

充电方式:Backscatter, small node and indefinitely time of endurance. but rely on reader, one to many centralized communication 利用无线电 ratio signal 充电的方式

ptp communication: 建立 channel awareness technologies 使得支持被动点对点通信来建立分布式系统

Combine with Sensors: 开发更多应用方式。

RFID and IoT:

  • embed intelligence in the physical object, so that simple physical objects can also “say”.
  • allows a physical object to be uniquely identified in a way similar to the “IP address” of a computing node in the Internet.
  • provides a low-cost communication way to achieve effective communication between nodes.
  • makes the physical objects in a passive environment achieve “passive intelligence“, providing fundamental guarantee for the “thing-thing connection”

2. Identification

简单说RFID就是物体上贴tag,用reader上的antenna去读取,这三个是主要组成。

Reader’s function

Energy supply: 比如有的标签自身不带能量需要reader提供信号中蕴含的能量

Communication: 最基本的功能,和tag识别,通信

Security Assurance: 比如加密解密

扩展功能:比如自组网 ad-hoc, 管理天线 antenna management 中间件接口 interface of middle components 连接外设 connecting peripherals

Reader’s classification

按频率:LF HF算低频,UHF和SHF算高频(ultra super),高频数据传输速度快,距离远,但是衰减快 signal attenuation,收到障碍物影响大 sensitive to obstacles。

按外观:

  • Fixed 固定有线的,高度集成,快速启动 set up
  • portable 可移动的像手持手机一样,small, charging battery, easy to move
  • Industrial 为工厂目的而生,比如集成其他 sensor

Influencing factors of R&W range

许多东西都有说明书,规范,来提醒我们怎么不把东西玩坏比如手机提示不要放水里玩。

RFID的R&W range是其中一种。影响因素如下:

  • The way that antenna is coupled 天线耦合方式,比如把两个天线绑一起太近互相干扰。

  • The output power of the reader’s RF signal 功率,太低可能无法激发tags

  • The frequency of RF carrier signal 合适的频率

  • Antenna direction 天线,读取器天线和标签天线极性方向 polarization 相匹配时识别范围最大

  • Operation environment condition

  • Movement speed of tags

Reader’s components and their functions

Signal Processing and Control Module: 主要是控制功能,协调一些本地计算

  • Communicate with upper computer, and execute command from it
  • Control communication process with tags
  • Encode and decode signal
  • Perform anti-collision algorithm
  • Encrypt and decrypt the data transferred between reader and tag
  • Identity certification between reader and tag

Inductively Coupled RF Module: 主要是产生能量和调制发送信号功能

  • Generate high frequency send energy, activate RF tags and provide energy (passive RF tags)
  • Modulate signal to sent, transferring data to RF tags
  • Receive and demodulate RF signal from RF tags.

Tag’s functions

  • data storage
  • energy harvesting 吸收能量,与reader的 energy supply 对应
  • contactless with R&W 不用接触就能通信,与 reader 的 communication with tags 对应
  • Security Encryption 与 reader 的 Security Assurance 对应
  • Collision Concessions 碰撞让步

Tag classification: by package form, by power source, by work frequency, by R&W capability

Package form 也就是外观上的分类:

  • card-like
  • label-like
  • Implantable, 比如动物植物体内
  • Accessories-like 附件类标签,比如纽扣型的,这一类主要是方便携带

By Power Source 按能源供应方式分类:

  • active 自己有电池供电
  • passive 依靠 carrier signal 读取器发来的载波信号获取能量
  • semi-passive 有电池作为后备隐藏能源,平时主要是passive 方式

By Work Frequency

LF HF UHF。UHF读写性能,距离最好,更多会使用 active 型。

By R&W Capability

read-only 和 R&W 两种,结构复杂度也有所不同

Two work modes of RFID middleware

interactive, independent。

image-20230618045820043

交互模式大概就是一直接收主机的命令,你让我读我就读,读完把结果还给你。

独立模式是可以不接收主机命令自行按预设的程序和读取到的结果信息循环执行指令,并将结果返回给主机。

3. Wireless Communication Principle of RFID

Different work principles of different carrier frequency

不同频率载波也适用不同的工作原则。

前面已经有所涉及,比如LF HF适用于近距离,UHF SHF适合远距离。

前者适用 Inductively Coupled RF Module 电感耦合,通过感应方式获取能量。

后者适用 Electromagnetic Backscatter Coupled RF Module 电磁反向散射耦合,持续不断发送射频信号来供给能量。backscatter 指的是接收机信号调制后通过发送机天线产生可被识别的信号。

两者的能量消耗都和距离平方成正比 squared distance

Signal voltage and energy: dB, dBm,重点:如何计算

变化的电压通常用 $V(t)=v_0cos(\omega t)$ 表示。

功率P=VI=V^2/R这不用多说。平均功率 $=\frac{v_0^2}{2R}$ 很简单推因为正余弦平均就是/根2.

相对变化 The relative change,这是一个比较新鲜的而且信号变化中比较重要的指标。

$G_{dB}=10log_{10}\frac{P2}{P1}$

参考功率 referenced power $dBm=10log_{10}\frac{P}{10^{-3}}$

dBm单位是功率的W,GdB单位是dB,代表一个比值。

Modulation of reader signal: OOK and its problem, solution: PIE; Tag encoding: FM0

一些阅读器通过调制使得正弦电压信号携带信息的方法。

OOK:on off keying,高功率1低功率0.

1687036831437

问题在于,低功率0的部分标签没法被激活,也无法正常工作。也就是说0信号标签压根启动不了,没法接收0信号。

PIE解决方法:长高功率是1,短高功率是0.

1687036892748

然后涉及到tags对reader发来的信号进行解码。空间中的信号发过来是有方向的矢量叠加,tags如何通过编码机制识别信号?

FM0编码方式:位窗起始处翻转信号表示1,中间翻转表示0.

1687037382818

FM0属于 FSK frequent shift key 通过信号变化频率来识别的机制。

Link budget: forward link budget and backward link budget 发射过程中能量增减的总和

reader transmit energy(+) path loss(-) tag activate energy(-)

  • pass loss: 读取器天线向360度的发送能量。其中只有一部分区域可以被tags antenna读取到,这一部分被称作 Effective Aperture (Ae) of the tag antenna。能量=有效面积*密度 $P_t=\rho A_e$ 。总共发送的能量比收到的能量就等于总表面积比有效面积 $\frac{P_{TX}}{P_{RX}}=\frac{A_e}{4\pi r^2}$

image-20230618174327976

来看上例,发送方30dBm对应1W,tag接收到-10dBm对应10^-4W. 然后5dB的衰减到-15dBm。这个5dB衰减就是两个dBm做差得到的。

所以,dBm相当于对功率P的另一种衡量方式,为什么这么麻烦的要用log来表示?因为两个dBm的差值就是分贝(放大系数),所以由一个dBm能量转到另一个只需要加减两者间差的分贝即可,很方便。

从tags反射回来的信号 reflection link 和路径四次方成反比 inversely proportional. $P_{RX,back}:\frac{1}{r^4}$

Antenna gain and polarization, EIRP

antenna gain: 输入条件相同情况下,实际情况某一点能量密度/理想条件下的密度单元。反应了天线 concentrates the input power 的能力。就比如把阅读器放中间,标签围一圈,360度去读取周围标签对能量消耗就大,可能因此传输距离也近;但是如果把标签集中放在一块区域,周围放置的 reader 利用定向天线 Directional antenna,固定读取某一个角度范围内的tags能量利用效率就高。

Polarization:事物在一定条件下发生极化 polarization,使得其表现的和原有状态不一样 its properties deviate from the original state。

EIRP, Equivalent Isotropic Radiated Power: 天线在所指方向上获得最大增益效果 maximum gain effect 所需要的能量。

For example, FCC regulations in the United States, a non-irradiated transmitter can transmit 1W of energy signals, and can use 6dBi antenna; antenna gain increased by 1dB, transmission energy needs to be reduced by 1dB. In fact, FCC is not more than 36dBm(30dBm+6dBi).

directional gain: radiation density of one direction d / average value in all direction

power gain: radiation efficiency of that direction G

平面角:单位rad,比如圆周180度单位角=2pi rad

立体角:单位sr,比如球面立体角=4pi sr

能量增益G的计算方法是4pi/立体角大小。比如波束宽度72°也就是2pi/5大概是1.25rad, $G=\frac{4\pi}{1.25^2}$

dipole antenna: 垂直于轴沿各个方向发送信号,比全向天线 omnidirectional antenna 小2.2dB。

Effective aperture $A=G\frac{\lambda ^2}{4\pi}$

$P_{RX}=P_{TX}G_{RX}G_{TX}(\frac{\lambda}{4\pi r})^2$

image-20230619104251622

$R_{forward}=\frac{\lambda}{4\pi}\sqrt{\frac{P_{TX,reader}T_bG_{reader}G_{tag}}{P_{min,tag}}}$

$R_{reverse}=\frac{\lambda}{4\pi}\sqrt[4]{\frac{P_{TX,reader}T_bG_{reader}^2G_{tag}^2}{P_{min,reader}}}$

4. Tag Identification Protocol

Checksum procedure: parity checks, LRC, CRC

奇偶校验不多说,查1的个数,poor error recognition。电路通过所有位异或是偶校验,结果为1说明有错误;再取反是奇校验。

LRC longitudinal redundancy check (LRC) procedure 循环冗余检测,所有字节进行异或运算,得到的结果是LRC校验码。也就是说数据发送到终点后,所有字节(数据和LRC)进行字节异或运算结果应该为0. 也有一些错误无法纠正,主要用于小的数据块校验。

CRC (cyclic redundancy check) procedure

1687142701838

接收方计算原数据+CRC数据拼接起来的CRC数据值,应该为0. 不能纠错,不过检错效率很高。

ASK, FSK, PSK

amplitude Shift Keying: 幅度调制,y轴上的调制。

1687142836442

计算方法2:duty factor: $m=1-\frac{u_1}{u_0}$

$U_{ASK}(t) =(m·u_{code}(t)+1−m)·u_{HF}(t)$

Frequency shift keying: 频率上的改变。

1687143205258

Phase shift keying: 频率相位翻转180.

1687143252934

1687143276722

Difficulty of traditional anti-collision algorithms for solving collision detection between RFID tags

Compared with the reader, limited by hardware resources, tags have very limited storage capacity and computing.

标签受制于硬件资源,存储容量和计算能力都不高。

TDMA, FDMA, CSMA

首先主要有两种方式,一个是reader broadcast 广播到诸多 tags,一个是多个 tags Multi-access 每个tags单独访问reader。

TDMA FDMA是multi-access, CSMA是broadcast

FDMA: 多个频率通道 several frequency channels 传输数据。

TDMA:

image-20230619212501183

ALOHA based protocols: pure ALOHA, S-ALOHA, FSA, DFSA, Q 算法。重点:性能分析、执行过程

Pure ALOHA algorithm:收到成功确认 ack 后就不再发送。否则一直随机等待后继续发送。简单但是通道利用率 channel utilization 低,poor performance.

offered load G:单位时间 tau 里同时发送的应答器数量

s-aloha: 规定时间片 slot,一个时间片只能发一次,冲突就下一次时间片去发。channel utilization 几乎是 pure 的两倍。

$S = G × e^{-G}$ G=1最大

frame S-ALOHA: 规定一个周期 frame,包含若干个 slots,会更加有组织有秩序。reader 广播一个 frame length,tags 自己选择组织时间片(0~f-1),每个时间片开始 reader 轮询一下tag里sn信号是不是0,是0就发送,不是0就-1.

conflict slot, single slot, idle slot(空)

逻辑,电路设计,内存都比较简单,但是 frame length 长度不固定。tags 远远多于 frame length 冲突时间片就太多,tags 太少空时间片太多太浪费。负载 G=1 也就是 length=tags 利用率最好。

image-20230620001137399

DFSA:利用以前的 frame 冲突反馈结果,和一些机器学习算法推测合适的 frame length。

EPC Global(第五章介绍)规范里使用了一种Q算法。简单说就是如果冲突太多了,当前 frame 就别继续了,中断,新开一个大容量 frame. 同理 空闲太多了就新开一个小 frame。

image-20230620002424045

Qfp是指定的初始值。每次先取整,然后发起 query。

没有回复:Qfp-C C是一个参数,比如0.1.

有冲突>1:+C。注意有上下限。

ALOHA 算法公平。但是可能发生饥饿 ,比如有一个 tag 每次都是有冲突的 slot,一直没有办法被处理。

Binary tree based protocols: BT, QT, 重点:执行过程

第二种算法,基于二进制数。就像二叉树不断拆分冲突的结点变为两个结点,直到节点里只有一个 tag。

random binary tree BT:随机。

binary query tree QT:排序,查询。

每一个 tag 需要有一个计数器来记录自己的状态。

image-20230620003431357

每一个tag都会被识别,不会饥饿,但是需要存储每个tag的状态。

比如看下面的例子:

image-20230620004226401

首先 tag1234 随机选一个数,比如选了0010,SN分别加自己选的数。

image-20230620010150624

找SN=0的,发现有是有,但是他们几个都冲突了。那么继续分,比如1011,SN=1021

image-20230620010254069

2的SN=0而且不冲突,把2读取了之后2不再继续参与。然后当有tag读取后,所有其他SN-=1

image-20230620010408026

=0的是14,但是他俩冲突。然后再重新划分一下,比如011, SN=0021

image-20230620010452073

然后处理1,其他-=1,处理4,其他-=1,处理3.

QT 不需要存储状态,如何实现?读取tag的序列号比较。

image-20230620010802734

不会饿死,也不需要一个可以读写的cnt,识别的时间和 tag id 有关。

image-20230620011058481

Binary search: Manchester code instead of NRZ code, 重点:执行过程

具体分辨哪一位有冲突。1代表冲突。

image-20230620011804411

NRZ混合没法检测错误。

image-20230620011902936

曼彻斯特可以,一个上升一个下降,合起来是0或者1.

image-20230620011942870

查询的流程:

  • request:发送一个序列号给tags的transponder,如果tags的序列号小于给定序列号返回。
  • select:给定一个特定序列号,返回等序列号的tag。
  • read_data:返回所选tag的信息。
  • unselect:读取完data了,这个tag退出选择流程。

image-20230620012706251

第一次迭代:返回uplink是所有transponder的id的共同信息(通过曼彻斯特编码找出没有冲突的位)。046位冲突了(从右往左),8个可能。

第二次迭代:限定 bit6 为0的request。发现有3个还是冲突04位(最高位冲突位=0,其他冲突位=1,如果range是大于等于,则正好相反)。

第三次迭代:限定bit4为0的request……

image-20230620013603192

长度 L(N)=log2(N)+1

Dynamic binary search, 重点:执行过程

Binary Search 是每次都传输完整二进制字符串. 其实我们只需要动态改变的部分.

比如我们查询1010 1111 1111, 那返回值前面一定是1010呀, 就不用传输了. 前缀叫 NVB, Number of Valid Bits

每次请求发送的信息: Request+NVB=4+1010

Advantages and disadvantages of ALOHA based anti-collision algorithm

simple

good identification performance

results can be statistically analyzed 结果可以被统计化分析

缺点就是可能 starvation 饥饿,delay trend to ∞

Advantages and disadvantages of binary tree based anti-collision algorithm

simple

intermediate state variables 不需要存储中间状态变量(QT)

缺点:查询时间受到 tags id 和 长度限制,比如二叉树沿着一个方向一直偏。

5. EPCglobal Standard & protocol

Concept of EPC global network

EPCglobal Network: a technology that

  • allows trading partners to document and determine the location of individual goods
  • if possible in real time
  • additional information: such as 生产使用日期,能否被贸易伙伴交换

Five basic services of EPC global network, interaction of different components of EPCglobal network

Electronic product code (EPC)

The identification system

EPCglobal Middleware

Discovery Service (DS)

EPC Information Services (EPCIS)

EPC码是唯一标识对象的代码。识别系统包括对象上的可被读取的包含EPC码的transponder和读取器reader可以识别EPC,然后通过EPCglobal Middleware传到网上,通过DS在 EPCglobal network 查找EPC码的相关信息(包括object naming service)。可以通过EPCIS和其他贸易伙伴交换EPC相关信息。

这其中的交互:

image-20230619112053963

transponder and reader : data acquisition

Middleware

Discovery services

EPC Information Services : access to EPC-related data

EPC code 组成

Domain Manager Number + Object Class Number + Serial Number

Basic procedures of the EPC Network

EPC码用于标识对应对象

all information about the object 在EPCGlobal Network里注册 administer

each company in the EPCglobal Network: 各个公司管理数据集和数据对象

access rights to object data: 包含在EPCIS里,指明了trading partners 之间访问权限

  1. the manufacturer:把transponder和product绑定

  2. all data assigned to the product:在EPCIS里

  3. EPCIS registers the entries with EPC Discovery Services:注册了DS之后方能找得到EPCIS

  4. product:卖给零售商 retailer

  5. At the retailer’s goods-in point 数据存储在零售商EPCIS中

  6. registered by EPCIS with EPC Discovery Services

  7. The company prefix send to root EPCIS

  8. root -> local -> the EPCIS

Binary tree based variant algorithm for EPCglobal Class 0

这种tag是只读的,制造商赋值。

1687148304873

EPCglobal C1 G1: PingID; C1G2: four commands (是什么,分别干什么用的), two types of performance trade-offs

EPC C1G1:查询tags EPC的一种标准。

被动标签,支持kill和lock两种操作。

pingID:掩码,用于查询tag EPC

1687197150310

EPC C1G2 有 OSI 的七层模型,两条数据链路(R-T)

1687197271532

1687198440696

上电 ready

发 query 命令,aribtrate 仲裁。选择随机数生成时间片。

slot=0 的开始 reply

tag 发 ack 给 reader,acknowledged 状态。

tag 收到 reader 的命令后进入 open,校验后进入 secured,完成 killed。

4个识别 tags 的命令:Select command, Query command, QueryRep command, QueryAdjust command

select 指明要查哪些 tags 的集合。

query 启动新的识别过程。

Rep 开启下一轮 slot 查询,标签 SN–,到0时读取。

Adjust 调整时隙数,选择新的时隙计数器等。

两大性能问题:

  • Build a set of tags involved in the recognition process,如何建立正确的tags集合来查询(select 和 query 负责)
  • Select the way of data encoding, for the readerto-tag, the tag-to-reader, the reader itself and the tag itself 根据环境调整编码方式

北邮国院物联网Microprocessor微处理器笔记

[TOC]

主要围绕提纲里的所有问题展开,没有拓展内容,Exam oriented Study。

关注微信公众号:灰海宽松,回复 “微处理器” 可获取本文pdf格式。

Introduction-随便聊

嵌入式系统是什么?专用的计算机系统。为专门功能可能对计算机架构,外设等做出一些取舍。

通常的限制:Cost(比如大量部署传感器节点),Size and weight limits(特定应用场景,比如下水道流量检测系统,需要体积小的节点),Power and energy limits(比如部署在极端环境下,喜马拉雅山顶采集节点,不方便去充电),Environment(防水,防高温等)

MCU MPU两种嵌入式系统区别:focus on 控制 还是 处理。控制比如点灯,机械臂,电机这些都是。处理比如摄像头采集到的数据进行图像处理。

编程语言:靠近计算机底层,主要使用汇编和c。

OS:嵌入式系统里不一定有操作系统结构。操作系统这个东西说白了就是更好地帮助管理计算机资源调度用的。现在我们来分析一下我们lab2的代码主函数:

1
2
3
4
5
6
7
8
9
10
void main(){
//background
while(1){

}
}

void IRQ_Handler(){
//interrupt handler function, frontground
}

后台部分:一个循环,重复去执行要做的任务。

这种方法乍一看也没啥问题。但是想想这样的计算机能做什么,只能按顺序执行一遍又一遍所有任务,甚至没法变顺序。

前台部分:中断处理,我们lab2里的uart_rx_isr函数,一般也用IRQ_Handler(实际上如果对lab2里的uart_rx_isr溯源一下,就会发现其实他也是被IRQ_Handler调用的,这个方法在启动对应中断时,触发中断就会自动调用)。

前后台合起来的系统还是一个裸机无os系统,只不过加了中断之后允许我们用中断的任务去打断后台轮询,改变一下执行顺序。比如串口中断发个数过来,CPU把手头后台的事情放下,去处理一下前台中断,处理完了再回来。

我们课程仅限于裸机开发的内容。

计算机系统简要介绍

Von Neumann Architecture

运算器控制器 (合在CPU中) 存储器 main memory 输入设备输出设备 IO,以及三条传输总线:数据,控制,地址 data bus / control bus / address bus.

1686650488954

前面介绍过MPU重点在于数据计算处理,MCU则是控制,因此MPU不需要一些外设去控制外接的组件。

Harvard Architecture

和冯诺依曼区别就是在于指令和数据分开存储。这样寻指取指取数效率高。

Stored Program Concept

主要两个部分:RAM存储程序和数据,ROM存储不变只读的程序和数据。

cpu执行指令就是三个步骤的重复执行:fetch decode execute 取指解码执行

assembly

如果高级语言相当于人话翻译给计算机,汇编语言相当于计算机语言翻译给我们。更贴近底层,因此运行效率也更高,而且可以直接操作硬件。

1
2
3
ADD r3, r1, r2 	;r3 = r1 + r2
SUB r3, r0, r3
MOV r2, r1 ;r2 = r1

; 是注释。变量r123是寄存器register,是可以操纵硬件的部分,我们可以通过对其赋值来操作硬件。

高级语言通过 compiler 翻译为汇编语言,汇编语言通过 assembler翻译为二进制机器语言。

ARM架构

ARM是一个指令集,前面讲的几个汇编指令这些都算做指令。

ARM公司有意思的地方是,他们不做ARM设备,他们只设计指令集架构,然后授权(知识产权核,IP核)给其他半导体厂商做。

A:application,主打高性能,手机电脑有许多就是ARM架构的。

R:realtime,主打实时,比如车联网对实时性要求很高。

M:microcontroller,应用于小型嵌入式系统,我们使用的板子。

m系列有m0到m7(简单说就是性能逐渐增加?),而且向下兼容即m7兼容m0~m6.

SoC

我们的板子上有一个黑色的小芯片,上面写着stm32blabla一串字符。这个就是整个板子的核心,相当于囊括了上文提到的计算机架构的芯片结构,system on chips。

1686658267695

设计soc规则:首先选用IP核,设计ARM处理器,外加一系列存储、IO外设结构,全部集成在黑芯片上。

ARM处理器 processor 是 architecture 的具体涵盖,多了很多新内容比如定时器。

我们主要学习m4架构。

1686658682835

只看非optional大概了解即可,处理器核访问代码,数据接口。

register

前面我们已经简单介绍了register。事实上如果想对内存中数据做处理,也要先拿到处理器核中的寄存器里做运算,然后返回回去。

arm register 如下:

1686659016141

通用寄存器:临时变量,可以存储计算数据之类的。

SP:栈顶指针寄存器,指向栈顶。

LR:函数返回用,保存返回地址。比如要调用函数了,把PC的值存入LR,然后PC跳转到函数起始位置;函数返回的时候LR的值还给PC。

PC:指向程序当前执行到的位置(下一个要执行的指令的地址)程序计数器。每条指令取了之后PC自动加一条指令,比如32位指令集PC+=4B。

PSR系列是状态寄存器,指明当前程序状态。比如当前是用户模式还是内核模式?IPSR指明当前是否允许中断?等。

xPSR包括:

  • APSR:计算用,如标志是否进位,结果是否为0,是否为负,是否溢出等。
  • IPSR:中断处理相关。
  • EPSR:执行相关,指明指令集,中断是否继续等信息。

1686659814546

Memory Map

m4有4g的内存空间默认映射到一片空间中,用户也可以根据自己喜好修改。有存储代码的code region,存储数据的sram region,存储外设的peripheral region,external ram region,external device region,Internal Private Peripheral Bus (PPB)。

1686669411390

Bit-band Operations

位带操作。

如果我们要读写32位数据中的某一位,比如第三位(从左往右是31:0,第三位是右边第4个),有的寄存器允许我们直接获取r[3],但是大多数是不允许直接获取的。

如何处理?如果写入1,那么r|0000 0000 0000 0000 0000 0000 0000 1000.

如果写入0,那么 r & 1111 1111 1111 1111 1111 1111 1111 0111.

读取:看 r & 0000 0000 0000 0000 0000 0000 0000 1000 结果是否为0.

这样很麻烦,比如我们要给0x2000 0000处的数据第3位写1,详细汇编代码:

image-20230613232342659

LDR是把后面的数据加载到前面的寄存器中,[R1]是把R1的值当做一个地址,取得其中存储的数据。

这样挺麻烦的,但是因为有内存映射我们可以直接写入和获取“位带别名地址”中的数据。

image-20230613232721037

image-20230613232713587

0x2000 0000处的第0位到第31位分别是:

0x2200 0000

0x2200 0004

0x2200 0008

0x2200 000c……

0x2200 007c

所以直接获取,修改0x2200 000c的数据即可。

0x2000 0000映射到0x2200 0000是 sram 区域映射,0x4000 0000映射到0x4200 0000是外设 peripheral 区域映射。

操作更快,指令更少,而且只访问一位更安全,比如刚取出0x2000 0000的32位数据,这时候中断修改了0x2000 0000的数据,这时我们取得的数据就是旧的错误数据了,修改完第3位再写回去,相当于中断白改了。

Program Image

1686670722574

vector:向量表,存储比如main堆栈的地址(MSP),异常的地址等信息。

start-up:板子上电或rst时的启动代码。

program code:我们烧进去的程序代码。

c lib code:库函数代码。

复位时,先读取 msp 地址找到 main 在哪。然后读取 reset vector 执行 BIOS 初始化代码,再开始读取第一条,第二条指令……

1686670972039

Endianness

两种存储规范。

比如十进制数字,1234,一千二百三十四。然后我们记录到数据库中,地址从低到高存储为4321,权值大的位1存在地址最高处,这就是大端存储 Big endian。否则,权值大的位存在地址低处,1234地址从低到高,就是小端存储 Little endian。m4两种方法都支持。

术语“little endian(小端)”和“big endian(大端)”出自Jonathan Swift的《格列佛游记》(Gulliver’s Trabels)一书,其中交战的两个派别无法就应该从哪一端(小端还是大端)打开一个半熟的鸡蛋达成一致。

一下是Jonathan Swift在1726年关于大小端之争历史的描述:

“……下面要告诉你的是,Lilliput和Blefuscu这两大强国在过去36个月里一直在苦战。战争开始是由于以下的原因:我们大家都认为,吃鸡蛋前,原始的方法是打破鸡蛋较大的一端,可是当今皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋是碰巧将一个手指弄破了,因此他的父亲,当时的皇帝,就下了一道敕令,命令全体臣民吃鸡蛋时打破鸡蛋较小的一端,违令者重罚。老百姓们对这项命令极为反感。历史告诉我们,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。这些叛乱大多都是由Blefuscu的国王大臣们煽动起来的。叛乱平息后,流亡的人总是逃到那个帝国去寻救避难。据估计,先后几次有11000人情愿受死也不肯去打破鸡蛋较小的一端。关于这一争端,曾出版过几百本大部著作,不过大端派的书一直是受禁的,法律也规定该派的任何人不得做官。”(此段译文摘自网上蒋剑锋译的 《格列佛游记》第一卷第4章。)

在他那个时代,Swift是在讽刺英国(Lilliput)和法国(Blefuscu)之间的持续的冲突。Danny Cohen,一位网络协议的早期开创者,第一次使用这两个术语来指代字节顺序,后来这个术语被广泛接纳了.

大端、小端基础知识 - 知乎 (zhihu.com)

Instruction Set

指令集。早期arm指令集32位,性能好能实现的功能强大。但是太长了处理效率低。

thumb-1 指令集16位,处理效率高了,性能也降了。早期arm架构如果是支持两种指令集的,就要频繁切换模式,效率低。

后来thumb-2指令集包含早期16位和新的32位,和arm指令集的混合指令集性能没减太多,代码量和处理效率还高了。

Assembly

汇编语法。

顺序结构

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
label							; 可省略,用于跳转到此位置
助记符 operand1, operand2, … ; Comments

MOV r1, #0x01 ; 数据0x01放入r1
MOV r1, #'A' ; 数据A的ascii码放入r1
MOV R0, R1 ; move R1 into R0
MOVS R0, R1 ; move R1 into R0, 并且更新APSR的状态

LDR R1, [R0] ; R0存的是一个地址值如0x2000 0000, 这个指令是取出R0代表的地址中的数据存入R1
STR R1, [R0] ; 写回去
LDR R0, =0x12345678 ; Set R0 to 0x12345678
; 等效于:
; LDR R0, [PC, #offset]
; ...
; DCD 0x12345678
; 也就是先在文档末尾的一条指令里写入数据0x12345678,然后编译器自动计算PC+多少offset到达DCD的位置,把其值返给R0
; DCD是声明一个字 32bit,DCB是声明一个Byte
; 如果多个数值的声明可以用标签声明
LDR R3, =MY_NUMBER

ALIGN 4 ; 字要先用这个声明,代表停止长度
MY_NUMBER DCD 0x2000ABCC
HELLO_TEXT DCB “Hello\n”, 0 ; Null terminated string


LDRB R1, [R0] ; B: 只写8位,就是说R0地址处的数据写入R1后,R1高24位清零
SDRH R1, [R0] ; H: 只写16位

LDRSH R1, [R0] ; 视作signed有符号数,写16位

LDRB R0, [R1, #0x3] ; 从R1+3读取一个字节给R0
LDR R3, [R0, R2, LSL #2] ; 从R0+(R2<<2)读取一个字节给R3
LDR R0, [R1], #4 ; 赋完值后,令R1=R1+4

ADD R0, R0, R1
ADDS R0, R0, R1 ; 加完更新APSR状态,比如有溢出或者进位则更新
ADC R0, R1, R2 ; R1+R2还要+APSR的carry位

; SUB SBC类似

MUL R0, R1, R2
UDIV R0, R1, R2
SDIV R0, R1, R2 ; signed

例题:应该是因为有可能减成负的所以signed

1686672985123

指令有1字长,半字长的。hw1是指明功能用的,hw2是一些拓展比如立即数。

1686713591015

地址从低到高分别是:4F F0 0A 00 0A 68 10 44……

PC每次取到半个字 hw,就+2B跳转到下一个hw。

选择结构

1
2
3
4
5
6
7
8
9
10
	CMP R0, R1						; 相当于if,比较后更新APSR。EQ= LT< GT> LE<= GE >=
BEQ BRANCH_1 ; B是跳转,BL是跳转到函数执行完后返回,BX是根据地址最低位判断目标地址是arm还是thumb在决定跳转到整字还是半字。bx操作数不能是立即数,必须是寄存器
B BRANCH_2

BRANCH_1
...
B IFEND ; 不写这个就继续执行BRANCH_2了,像switch的break
BRANCH_2
...
B IFEND

循环结构

1
2
3
4
5
6
7
8
WHILE_BEGIN 
UDIV R2, R0, R1 ; R2 = n / x
MUL R3, R2, R1 ; R3 = R2 * x
CMP R0, R3 ; n == (n / x) * x
BEQ WHILE_END
SUBS R1, R1, #1 ; x--
B WHILE_BEGIN ; loop back
WHILE_END

Stack

内存中有一片内存空间类似栈的数据结构。SP指针指向栈顶。

这个栈地址是从高到低的,也就是存入数据 SP–,取出数据 SP++,类似一个翻转过来的,倒着的书堆。

满堆栈:sp指针指向最后一个栈顶数据。

空堆栈:指向最后一个数据的下一个要放入数据的空位置。

我们的课程中使用空堆栈,指向下一个空位置,存数据就先存入再SP-4,取数据就先SP+4再出栈。不过这两条指令都不需要我们手动执行,有专门的指令:

1
2
PUSH {R0, R4-R7} 	; Push r0, r4, r5, r6, r7
POP {R2-R3, R5} ; Pop to r2, r3, r5。入栈出栈顺序不是按照书写顺序而是自动根据寄存器地址,高地址值给高地址寄存器

存入5个数据和取出3个数据。

Functions

BL先保存当前PC值到LR,然后PC跳转到函数地址,

BX LR跳转到LR中的地址用于函数返回。

Architecture Procedure Call Standard (AAPCS) :规范定义哪些寄存器主函数和函数通用,哪些是独有的。

arm AAPCS规定:r0-r3是通用寄存器(类似全局变量),但main和函数的R4 – R8, R10-R11不通用(类似临时变量,到了函数里这些值就变了,不是原函数的),要压入栈保存。函数调用和返回的时候要保存和恢复通用寄存器值。这些由调用原函数的子函数 callee-procedure 执行。

简单的参数的函数调用:传参给R0-R3作为函数参数,R4-R11压入栈,然后跳转到函数处。

1686733381292

Program Memory Use

ROM里都是只读数据,比如常量常数。

1686733480351

const, static, volatile

貌似是不会过多涉及具体代码实现的部分,就先简单介绍一下了。

const 就是定义常量变量,定义后无法再次修改。

static 通常定义静态函数,静态函数里的值是通用的,也就是每次调用该函数其值都是接着上次调用该函数的值继续。

volatile:一个在嵌入式里挺重要的东西,软考题里出现过几次。大概就是禁止编译器优化该变量来防止不必要的错误。

比如编译器优化num变量,这样每次修改num变量的值的时候都不会立刻写入内存中,可能会先把修改时的值写入寄存器,函数返回时写回内存。

现在比如我们在main中num+=5, 修改值后的num暂时存在寄存器里。然后我们调用中断,从内存中读取当前num的值并+1.但是内存中值还没改,还是原值。返回后,main再把自己手中的num值写回内存,最后内存中num值只+5,而不是我们期望的+6.

volatile 声明后的变量不会做这样的优化,值改变了就立刻写回内存,虽然可能效率低但是安全。

Interrupts

比如我们程序的逻辑是按键按下的时候点亮小灯。第一种做法是 Polling 轮询,一直看:按键按下了吗?没有。按下了吗?没。按下了吗?……

这样主要是效率低浪费CPU资源,如果为了节约资源轮询间隔大了,又不能及时响应。

中断允许CPU专心处理background的事情,触发中断的时候先放下后台处理前台。对于无os的裸机也能实现简单的多线程切换。

异常处理流程

  1. 结束当前正在执行的指令。

  2. 当前模式寄存器值压栈保存。

    image-20230614171134845

  3. 切换模式。

  4. PC LR更新(根据异常处理器提供的值)。PC去查中断向量表,看要跳到哪里,EXC_RETURN Code赋值给LR。

  5. 更新IPSR状态。

  6. 开始执行异常代码。

  7. 退出,BX LR把 EXC_RETURN Code 值返回给PC。

  8. 出栈。

Timing

中断执行也是耗时的,需要一定的时间保存源程序状态,执行中断,恢复。

FMax_Int:最大中断执行频率,即:单位时间内最多执行几次中断。

F_CPU:CPU频率,即:单位时间内CPU有多少次指令周期。

C_ISR:执行中断内容需要多少周期。

C_Overhd:中断保存、恢复数据等准备工作用多少周期。

中断一次执行所需周期:C_Overhd+C_ISR

因此, $F_{Max_Int=}F_{CPU}/(C_{ISR}+C_{Overhd})$

U_int:中断处理实际消耗的利用率,上面那个毕竟是最大值。

$U_{int}=F_{Int}/F_{Max_Int}$

中断执行速度(和频率一样):F_Int

非中断执行速度:(1-U_Int)*F_Int

GPIO

General Purpose Input Output,

Memory-Mapped IO

把设备,控制等寄存器映射到内存里。好处就是访问设备方式和内存一样,也不用设计复杂的IO电路,便捷;缺点在于占用了内存空间。

Peripheral-Mapped IO

IO有一块专门的存储区域,和内存不一样,也有专门的不同的电路指令去访问IO。好处就是节省内存空间,也能清晰的知道什么时候发生IO了;缺点在于开发、设计上的造价增加。

GPIO

通用IO可以判断引脚高低电平,可以给引脚赋值高低电平进行控制。

stm32有几组GPIO,每个有16个Pin,可以配置为input output pullin pullup等模式,以及定时器、串口、中断等功能。

什么是上下拉模式?如果不设置为上下拉,引脚浮空的时候(没有设置输入为高或低电平的时候)浮空引脚可能收到电磁波干扰等等问题导致输入状态不确定,有0有1的,容易造成错误。

下拉:三极管控制默认接地,无输入的时候默认低电平。

上拉:三极管控制默认接Vdd 芯片工作电压。

1686742220470

大多数引脚是这两个功能都有的,我们初始化GPIO的时候选用一个,寄存器根据值控制接通相应电路。

image-20230614193552367

输入输出信号真的可以被称为“信号”。输入规定为0-0.5视作低电平,0.5-Vdd视作高电平,范围以外的值无效。输出电流也只有5mA左右是没有能力直接驱动一些设备的,我们可以通过一些电路比如三极管,放大器等,电路接收到信号得知”需要输出驱动电流了“然后输出大电流。

Control

每个GPIO口有:

4 * 32bit configuration registers: 配置相关信息,比如in/out,上啦下拉,开漏输出或推挽输出,输出频率等。

  • 推挽输出 push-pull:能输出高低电平。
  • 开漏输出 open-drain:没有能力输出高电平,想输出高电平需要设置上拉电路来输出。

2 * 32bit data registers: 输入输出数据寄存器。

1 * 32bit set/reset registers: 设置或复位寄存器。

1 * 32bit locking registers: 锁定寄存器。

2 * 32bit alternate function selection register.

Mode

如图,32个Pin,每个两位来设置4种模式(in out 可选 模拟)。

1686743308300

Pull

只有3种模式(无pull,上拉,下拉)。

1686743360055

data

输入输出数据寄存器分开的。

1686743538223

CMSIS

先说一下考试定义:

CMSIS transforms memory mapped registers into C structs

1
#define PORT0 ((struct PORT*)0x2000030)

1686747214412

再说一下和一些嵌入式前辈讨论的理解,以下内容不许考试写:

李肯老师:arm-M推出的一系列API和软件组件,包括核心功能、DSP库、RTOS支持和调试接口等。

李肯老师:如果芯片厂不想再多一层,CMSIS就够用;但有的厂商会再在上面封一层,可能叫driver层。

李肯老师:另外CMSIS有个限定,就是ARM的ARM Cortex-M处理器;虽然它很常见,但并不是所有的处理器都是这个内核;这个需要注意。

榊:这种与内核相关的文件,比如启动文件,内核文件是CMSIS规定。

a6953d9ebb72f2ecd6cc4dbf569d406

榊:对比STM32F103和GD32E23的启动文件,我们会发现是一样的:

711dd8daa73cb3dd198e50f32f6f86a

榊:而芯片厂商要做的是根据这个arm规定的接口二次开发库函数。

李肯老师c站账号:架构师李肯的博客_CSDN博客-程序人生,粉丝福利领域博主

榊老师c站账号:风正豪的博客_CSDN博客-C语言,MSP430F5529,Linux领域博主

平时李肯老师的交流群会讨论很多嵌入式相关问题,欢迎有兴趣的同学来学习[Doge]

以上内容感兴趣的看个乐呵。

例:

1
2
3
4
5
6
7
8
9
10
11
12
typedef enum {
Reset, //!< Resets the pin-mode to the default value.
Input, //!< Sets the pin as an input with no pull-up or pull-down.
Output, //!< Sets the pin as a low impedance output.
PullUp, //!< Enables the internal pull-up resistor and sets as input.
PullDown //!< Enables the internal pull-down resistor and sets as input.
} PinMode;

gpio_set_mode(P1_10, Input);
gpio_set_mode(P2_8, Output);
int PBstatus=gpio_get(P1_10);
gpio_set(P2_8, 1);

以上代码是老师提供的driver,大意就是选定pin,传入特定参数,即可设置模式,设置输出。

感兴趣可以看看我的这篇文章,如果使用arm定义的cmsis直接去开发也是可以的:

STM32 学习笔记_4 GPIO:LED,蜂鸣器,按键,传感器的使用_灰海宽松的博客-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stm32f10x.h"
int main(void){
/* 控制gpio需要三个步骤:开启rcc时钟,初始化,输入输出函数控制 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

GPIO_SetBits(GPIOA,GPIO_Pin_0);
while(1){}
}

drivers二次开发,可以帮助简化。

当然这一段都是题外话了。考试就理解为“cmsis是变量宏定义直接映射到寄存器上;drivers是对其添加进一步行为”即可。

Serial Communication

串口通信,一种发送消息的通信方式。

串,指的是发数据的方式:一位一位串行发,并行是可能有多路通道,每路同时发一个数据,多路同时到达。

串口通信有单工 Simplex,半双工 Half Duplex,全双工 Full Duplex。

两种传输方式:同步 Synchronous,共用一个时钟;异步 Asynchronous,有各自的时钟。

同步很简单,发送方接收方比如都规定时钟信号下降沿收发。

1686749582568

异步:需要通过异步通讯协议 Asynchronous Comm. Protocol 来协调。

1686749956698

1位起始位标志开始传输,7/8/9位数据位,1位可选奇偶校验位,1位停止位。

RT两方需要有相同的波特率。

当然这只是最简单的串口通信因为只有双方。如果更多方通信我们需要校验地址来判断是哪个发给哪个;数据需要更复杂的校验方式。

异步通信不需要同步时钟之类电路,开销小,但是开发起来难度大一些因为需要起始结束位啥的。

RS232

异步通信,Reversed Polarity 标准电压(-3-15是1,315是0.还有一些其他标准比如TTL是+5为1,-5为0.)

发送数据有两种类型,ascii码和二进制,都得转化为二进制传输。

uart

针对stm32f401.

全双工异步串口。

为了处理RT缓冲数据(因为发收数据需要时间)我们可以通过缓冲区数组,头指针表示已经发到的位置,尾指针表示要发的数据的结尾。增加新数据,尾指针++;发一个数据,头指针++直到碰到尾。

原来发送方一直是发高电平,start frame 起始帧是1帧低电平来表示开始发数据了。

如何判断是1帧低电平?通过在这一帧里多次采样判断是不是真的是一帧低电平。

为什么多次采样?因为异步两个信号有一定的偏移,多次采样准,能确定是不是真的一整帧都低电平。

采样是有一定采样率的,不是说真的能像模拟信号一样一直采。

采样率 oversampling=16: 这个是最大可以达到的采样频率而不是真的一帧采了16次。

1686756691253

接收方首先第一次检测到0位,开始怀疑:有可能是串口有消息。这是start frame的第一次采样。

然后每隔一帧检测一次,3 5 7检测3次,如果2个都是0,说明确实有可能。

然后连着检测8910,如果还是2个0,说明确实是start frame。

1686756881390

8采样率因为采样间隔长了,更容易碰到左右边界的高电平,所以容错率低。但是速度更快。

计算

波特率计算:

$T_x/R_x(baud)=\frac{f_{PCLK}}{8*(2-OVER8)*USARTDIV}$

OVER8是过采样率,fPCLK是时钟频率。

USARTDIV是一个浮点数

USARTDIV浮点数怎么存储?通过算法转化为十六进制。

1686759124660

小数部分用一个16进制位表示,比如例1是C也就是12,转换后即为12/16也就是0.75.

例2转换为一位16进制,就是0.62*16约等于10也就是A。

整数部分直接转换十六进制即可,例2的25转为19,例1的27转为1B。

然后整数小数部分拼接起来(最多3个整数位,1个小数位,32位寄存器)。

Timer

想让程序定时运行,比如led 1s闪烁一次。如何做到?

第一种方法是愚蠢的delay延时,我自己估算一下:嗯,delay(2000)差不多1s。然后在程序中delay,点亮,delay,熄灭……

太浪费资源了。

第二种方法,32是有定时器中断的。

定时器中断大概原理是,32上有时钟晶振按固定频率周期输出0101010……定时器里有一个cnt,收到一个时钟晶振就++。

我们可以设置定时器溢出值,比如溢出值是1000,cnt加到1000会自动触发定时器中断。然后归0,继续++。

image-20230615014136641

执行周期数量:1+1+1+1+(0xFFFFFFFF一直-1-1-1直到变为0x00FFFFFF的循环次数)+(r0+1的执行次数,1次)

定时器也有一些扩展方法,比如我们可以设定++还是–;可以设定信号源是时钟或者外部输入的方波信号;可以读取计数值……

我们课件常用方法好像是–到0触发中断,然后恢复初值。

1686764952191

PWM

PWM这个东西是什么?

PWM(Pulse Width Modulation)脉冲宽度调制,在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域。

就好比说,你骑自行车速度只能是100和0,模拟电信号只能输出高低。

但是呢,你骑自行车是有惯性的,以100速度蹬一脚,以0速度蹬1脚,100速度蹬一脚……

整体来看你的自行车平均速度是50(我们假设加速度不需要时间哈)

这个应用场景有很多,比如设定led闪烁频率:高低高低高低……,因为频率极高,我们肉眼看不出来在闪,给我们呈现的视觉效果就是以一半的亮度在亮。高低低高低低就是1/3亮度。

比如电机通过这个方式调速度。

那么他有什么应用场景。第一,输入捕获 Input capture。

对于一个这种有惯性的系统,我们也可以反过来读取其波形来判断其速度。比如电机放一个转速检测传感器,把输入波形作为定时器的时钟源信号,定时器一直++:检测上升下降沿时记录cnt值,通过差值比较计算时间间隔。

image-20230615020829378

第二,输出比较 output compare。

定时器一直++,与预先设定好的阈值比较,如果相等触发中断输出。

1686767856060

这就是PWM。占空比相当好算。

Low Power Timer

我们目前假设的是CPU一直运作的,只是在后台和前台之间切换。有一种低功耗定时器使得没有发生定时器中断的时候CPU被置为低功耗状态,只有发生定时器中断的时候才启动。(使用 __WFI() wait for instruction 指令)

image-20230621235021294

SysTick

M系列自带的一个系统时钟,使用处理器时钟或者参考时钟作为时钟源。

有四位寄存器:

image-20230621235527699

每次赋值是load,一直–到0时重新load赋值。ctrl是控制启用系统时钟。这个是CMSIS有提供的数据结构和相关操作函数的时钟处理部分。

image-20230621235805628

init 参数是中断间隔的毫秒数。timer_set_callback() 里跟一个可以是自己定义的函数,使得触发定时器中断时该函数被执行。以上代码意思是每隔100ms LED灯翻转一次,且 CPU 常态下处于低功耗状态。

I2C

连接多个模块的传输方案:I2C,使用两根总线。

image-20230622000302459

两根总线分别是时钟总线 SCL 和数据总线 SDA。

通信过程

现在我们串一遍I2C上一个模块(master)要给另一个模块(slave)发消息的过程。

1687363434463

  1. MCU 使用一定的方法标识自己开始传输了。
  2. MCU 发送 LCD slave 的地址+一位读写位,其他模块接收到发现地址不是自己的,就不做处理。
  3. LCD 接收到后知道目标是自己,于是返回 ack。
  4. MCU 收到 ACK 后发送一帧数据。
  5. 发送完 MCU 等着 ACK,收到 ACK 后继续发送下一帧数据。
  6. 一直发送到发送停止位 stop 结束。

image-20230622000920336

数据长度可以设置,比如789.

总线上的器件是开漏输出的半双工通信。

image-20230713135505107

默认总线是上拉电阻拉成高电平。

当器件输出 out 为低电平时,总线导通到接地,总线被拉低(整条总线都被拉低)。江协科技老师举的例子很好,就像公交车上的一根横杆,有人拉住横杆拽下来,整条横杆都被拉低了,其他人都知道“横杆被一个人拉低了,说明有人正在使用总线”。

然后是总线传输数据的方式,SCL SDA 两根总线在何种情况下表示 start stop 0 1 bit?

image-20230713140225426

首先都是 SCL 为高电平时 SDA 的数值才有意义。

SDA 从高到低,表示 start 位。从低到高,表示 stop 位。

start 位后,SDA 高电平表示1,低电平表示0.

发送完 1byte 数据后,总线保持拉高状态。如果接收方把总线拉低了,发送方发现总线1→0了(不是发送方自己拉的,是接收方给他拉下来的,但是发送方能察觉到),说明接收方成功接收了并且拉了拉总线以示“收到”。如果 SDA 还是保持在高电平,说明接收方没有成功收到或者成功发送 ACK。

image-20230713140822834

问题处理

I2C 是一种很简单的主从通信协议了,但是局限性也很多,比如7 bit 的地址线只允许 2^7 个设备;一次顶多两个设备主从通信;一个设备的快慢会影响到整条总线的通信等。

问题1:从设备处理速度太慢了,赶不及在下一个时钟周期接收新数据帧怎么办?

方法:clock stretching, 拉低一段时间 SCL 假装下一个时钟周期还没到。

image-20230713141906054

问题2:多个设备同时发数据冲突了怎么办?

方法:Bus Aribitation,前面我们知道总线被一个设备拉低了,所有设备都能接收到总线拉低的信号。因此如果两个设备同时开始发信息,前面数据一致都无所谓,等到第一次数据不一致的时候,一个设备发送数据0,一个发送数据1,这时 SDA 总线被 DATA2 的0拉低了。

image-20230713142029471

发送 DATA1 数据的设备就明白了:有人同时在和我一起发数据,因此总线不是我预期的1而是被他拉低为0了。那我 quit,你发吧。然后就只有 DATA2 发送的数据了。

问题3:以上发送的数据每次都是 1byte 8bits 很正好。那如果要发送的地址不是 8bits 呢?

方法:少于 8bits 用一些固定的额外的 start 位填充,多于 8bits 的地址用两个 bytes,不够的也是用额外的 start 位填充。

image-20230713143052018

问题3:如果我 master 发完数据,想紧接着再收数据,变成 slave,可行吗?

方法:通过一个 sr 信号,也就是 repeat start 重发 start 位,来标识自己是 read 而不再是 write 了重新开始通信。

image-20230713143601570

编址格式

slave 地址编址有一些固定格式。

image-20230713143744619

0000 000 0:广播,对所有 slave 结点讲话。如果 slave 无视(NACK),就不会参与广播。如果返回 ACK 就参与进来了。不过多个 slave 都返回 ACK 的话 master 是不知道都有谁回应了的。

第二个 byte 发送一些行为相关,比如:start,clear,reset software

编程应用

slave mode:

  • I2C 设备默认工作在 slave mode。
  • 外设时钟在 I2C_CR2 寄存器中编程。频率介于 2kHz~100kHz。
  • 硬件自动等待发过来的 start 和 addr 信息。
  • 如果 addr 信息和 OAR1 中存储的地址相同,说明目标是自己。如果 ACK 位为1,则发送 ack pulse。
  • 设置 ADDR 位,1表示匹配。
  • 如果 ITEVFEN 就是中断事件 flag 为1,则生成中断。
  • TRA 位标明 slave 是 R 还是 T 模式(收 or 发)。
  • BTF 位标识收没收完。

1689255491311

image-20230713214410998

这么说起来还是有点混乱 I2C 到底经历了哪些才顺利发送了数据?

首先,从主模式的概念。master 主模式驱动时钟信号,发起传输;slave 从模式响应传输。

主模式

用于主发送数据的 I2C 传输序列图

发送:

所有 EV 事件都会拉低 SCL,直到相应软件序列执行完成。

S:start 事件。比如CR2 寄存器中设置外设时钟,配置时钟寄存器,上升时钟寄存器,使能 CR1 来启用时钟,CR1 中设置 start 位,等待总线被拉低表示就绪,发送启动信号,并切换为主模式。

EV5:启动事件成功进行,设置 SB 寄存器=1. SB 寄存器=1后才可以进行地址阶段,执行完地址阶段会自动清除 SB 和 EV5 事件。

Address:地址阶段。传输7位地址+1位读写位,然后等待从机的 ack。收到 ack 进入 EV6.

EV6:设置 addr 位=1代表地址阶段顺利执行, master 收到 ack了。清除 EV6 后自动进入 EV8.

EV8:设置 TxE ,准备写入主机要传入的数据。TxE 表示数据寄存器为空可以写入。每次数据写入 DR 都会清空 TxE 和 EV8 事件。写完数据数据传过去了,主机收到 ack 后继续传输。以 BTF=1 表示数据传输的结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void i2c_write(uint8_t address, uint8_t *buffer, int buff_len) {
int i = 0;
// Send in sequence: Start bit, Contents of buffer 0..buff_len, Stop
while (((I2C1->SR2>>1)&1)); // wait until I2C1 is not busy anymore
I2C_GenerateSTART(I2C1, ENABLE); // Send I2C1 START condition
// wait for I2C1 EV5 --> Slave has acknowledged start condition
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// Send slave Address for write then wait for EV6
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Transmitter);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
while (i < buff_len){
I2C_SendData(I2C1, buffer[i]); // send data then wait for EV8_2
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
i++;
}
I2C_GenerateSTOP(I2C1, ENABLE); // send stop bit
}

image-20230714110003657

接收:

前面和 master transmit 都一样。

TxE 改为 RxE 了,=1标识接收到了数据。

master 自己设置 stop 事件后(发送 NACK)停止接收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void i2c_read(uint8_t address, uint8_t *buffer, int buff_len) {
int i = 0;
// Start bit, Contents of buffer from 0..buff_len, sending a NACK
// for the last item and an ACK otherwise, Stop bit
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); //EV5
// Send slave Address for write then wait for EV6
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Receiver);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
I2C_AcknowledgeConfig(I2C1, ENABLE); // going to send ACK
while (i < buff_len - 1){
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); //EV7
buffer[i] = I2C_ReceiveData(I2C1); // get data byte
i++;
}
I2C_AcknowledgeConfig(I2C1, DISABLE); // going to send NACK
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); //EV7
buffer[i] = I2C_ReceiveData(I2C1); // get the last byte
I2C_GenerateSTOP(I2C1, ENABLE); // send stop
}

从模式

1689259086166

发送:

start 启动事件由 master 发起。从机校验地址并决定是否发送 ack 位。

EV1:设置 addr 位表示地址匹配。

EV3-1:设置 TxE 位,开始传入数据。一直到主机返回 NACK 表示不想再要数据了,或者 AF=1 说明 ack 失败了为止。

1689259113895

接收:

前面到 EV1 和 slave transmit 都一样。

  1. 数据从 DR 寄存器中读。
  2. 读入一个 byte 后,如果 ack 位已经设置,则返回 ack 信息。
  3. RxE 位是接收数据的状态寄存器。
  4. 主机生成停止条件时停止。

异常情况:

总线错误,NACK,仲裁失败,时钟异常超时。

image-20230714110916968

致敬未来的攻城狮计划

前言

这回参加的是csdn李肯老师的攻城狮计划,简单说就是我白嫖板子,输出学习笔记。

552ca9fa46d8c1c7f192cdad4d207d4

板子是瑞萨的CPK_RA2E1,还有触摸元件,看起来很有意思hh。

环境搭建

一开始决定采取vscode搭建的方式。后期进行到最后一步——cmake build的时候一直显示语法错误,肯哥表示是环境配置不全,但是我反反复复根据官网和其他博主的文章检查了不下10遍都不知道问题何在。最终决定还是老老实实用keil。

  1. keil导入瑞萨包。官网下载地址:Arm Keil | Renesas

  2. 下载rasc软件。对于这个东西我的理解是类似stm32cubemx,可以快速初始化项目的软件。该软件可以在瑞萨官网下载:https://www2.renesas.cn/kr/en/software-tool/ra-smart-configurator

  3. 接下来我们尝试新建编译一个项目。这是我第一次没有跟着一块有完整开发流程的板子的视频课,而几乎完全是自己检索资料探索尝试的项目构建,因此碰到了很多弯路。首先rasc软件我们新建一个项目。

    项目起名

    板子型号这里是根据我的学习板设置的。IDE一定记得改为keil。

    image-20230601022545320

后两页选择 no rtos 和 minimal 即可,因为我们现在的目的只是尝试编译通过一个项目。

  1. 添加完成后,点击generate code 生成相关的项目代码。然后用keil打开,尝试编译。

  2. 我第一次遇到的问题是报了19个错。我还以为是pack导入的不对,但是后来搜了一下发现不是那么回事,是编译方式选错了。参考文章:keil出现大量未知语法错误(系统移植)_portforce_inline_IT小生lkc的博客-CSDN博客

    image-20230601022913830

  3. 于是我把编译器改为version5,编译到一半,再次报错:error: A3903U: Argument ‘Cortex-M7.fp.sp’ not permitted for option cpu’.。这个问题原因是因为编译器版本太低,于是我又去下载了新的keil5.

  4. 再次编译的时候提示我,不能用version5,新版本已经不支持了。于是我又改为version6进行编译。这次非常顺利!

image-20230601023151749

下载

终于考完试了,然而攻城狮的截止期限也快到了QAQ,得尽快水(划掉)写几篇文章了!

先争取可以成功下载一个空的程序。

先对上一篇文章下载 DFP 也就是 keil MDK Software Packs 做一个补充。我们要下载的是 RA_DFP,下载地址为:Arm Keil | Renesas RA_DFP

至于版本我看到有前辈使用 3.5.0 的版本可以成功运行,而我下载的是4+的版本也可以。只不过在选择 device 的时候会有一点不同:
image-20230622160103209

可以看到 4.1.0 的版本无法细化选择到 R7FA2E1A92DFM,只能选择大类 A9. 不过经过下面的烧录尝试,是没有问题的。

流程:基于上次的空项目,用keil打开,编译,下载成功。

一直到编译的步骤前面都做完了。下载主要需要以下几个步骤:

  1. 引入项目的 src 文件夹。在 options for target - c/c++ 里引入即可很简单。image-20230622154546545
  2. debug 模式设置为 jlink 模式。设置完之后插上板子设置配置,这里因为我的jlink版本太低出现了一个报错:unkown to this version of the jlink software。解决办法就是在官网上下载了一个新版本的jlink(官网链接:https://www.segger.com/downloads/jlink,我选择的是 windows 版本),下载好后直接会提示“检测到你电脑里的 keil 环境,请问是否更新其 jlink 调试器”,更新后重新启动就没有问题了。
  3. 上一步参考文章: keil识别不到芯片,提示unkown to this version of the jlink software_keil识别不到单片机_王小琪0712的博客-CSDN博客 里面也有如果没有提示自动更新 jlink 调试器的选项如何手动进行更新的步骤,建议多多支持原作者。
  4. 只是选择了 jlink 调试器也并不算设置完成。如果这个时候点击下载,会提示“找不到 flash”,也就是还没有配置完成,需要设置闪存。首先确保插上了板子,然后打开 jlink 的setting:image-20230622155204869
  5. 如果显示下图说明板子被正常识别。如果没有显示这些数据,可能是 jlink 的版本还是过低,或者线坏了,或者还未下载对应 rcsa 包。image-20230622155322848
  6. 接下来我们配置 flash download. 打开这个页面后点击 add,添加图中所示的这款型号芯片,然后 start 和 size 应该就会自动配置成和图中一样的情形,这样就算成功了。image-20230622155451715
  7. 点击 load,如果显示如下信息说明成功下载程序。image-20230622155546525

刚插上板子的时候板子上是有白色的 power 指示灯和一个红蓝交替闪烁灯。下载空程序之后,应该只有电源指示灯还在亮。

de933bcec619535b3e8b7fe60f61238

点亮LED

本文主要参考文章:【致敬未来的攻城狮计划】— 连续打卡第十一天:FSP固件库开发点亮第一个灯。_嵌入式up的博客-CSDN博客

在32阶段我们已经接触过类似做法了。初始化引脚模式(可以手动库函数,或者在工具包图形化界面里配置),设置引脚输出值。

设置 FSP Smart Configurator

像上次一样创建一个项目。

首先我们翻一下RA2E1的数据手册看看led在哪。

image-20230624005444720

如图所示,一红一蓝,502 501,输出高电平亮。

因此 configurator 里的pin如图所示设置501 502为output initial high

image-20230624005611958

配置完成后点击右上角 generate project content,输出更新配置到该项目中。

Keil代码编写

接下来就是编写keil里,驱动两个led灯输出高电平的部分了。

image-20230624011259206

hal_entry.c 是相当于 main.c 的入口函数。其他都是 configurator 提供的配置函数。

引脚设置已经设置好了。我们打开 pin_data.c 可以看到:

image-20230624011748710

这就代表确实初始化配置加进代码里了。

然后在 hal_entry.c 里是通过这个 open 函数在 warm_start 里初始化了。

image-20230624012345948

接下来我们需要一个写入位函数。在 r_ioport.c 里。

image-20230624012543274

参数1:固定参数,传入 &p_ctrl。

参数2:引脚,老方法 goto the definition

image-20230624013133790

参数3:电平。

image-20230624013206720

然后就简单了,只需要在主函数里调用write函数写亮led。

1
2
3
4
5
6
7
8
9
10
11
12
13
void hal_entry(void)
{
/* TODO: add your own code here */
while(1){
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_01, BSP_IO_LEVEL_HIGH);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_02, BSP_IO_LEVEL_HIGH);
}

#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}

实验结果

image-20230624013456059

按键模块

后台轮询

按键也是一个比较简单的模块,主要是为了学习IO输入模式。

查看RA2E1电路图可见:

image-20230624133704709

按键相关引脚是004引脚,默认上拉高电平,按下接地为低电平。

首先第一步还是设置对应引脚。类似上一期设置LED的方式,只不过Mode改为Input mode。

image-20230624133750237

设置好之后仍然是记得generate。

然后就是程序编写。首先还是明确一下开发流程。首先我们尝试后台轮询的按键检测。在while里不断检测按键电平,如果为高点亮蓝灯,如果为低点亮红灯。

点亮好写,上一次已经尝试过write函数。那么我们接下来再去看类似的read函数。

image-20230624135423601

第一个参数还是传入固定的&g_ioport_ctrl。第二个是引脚。第三个是存放我们要存储的读取按键的值。比如传入变量state的地址 &state,函数执行结束后state的值就是读取的按键电平。

主函数编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void hal_entry(void)
{
bsp_io_level_t state;
/* TODO: add your own code here */
while(1){
R_IOPORT_PinRead(&g_ioport_ctrl, BSP_IO_PORT_00_PIN_04,&state);
if(state==BSP_IO_LEVEL_HIGH){
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_01, BSP_IO_LEVEL_HIGH);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_02, BSP_IO_LEVEL_LOW);
}
else{
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_01, BSP_IO_LEVEL_LOW);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_05_PIN_02, BSP_IO_LEVEL_HIGH);
}
}

#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}

实现效果:

8d99e5ae0be2c97e2e2bb44a1ed92f6

59b0f842762bb1349190932bdcb4e74

活动总结

一转眼攻城狮计划就已经到了最后一天了。
5月我还处在一个迷茫期,那时候刚刚入坑嵌入式,只学了几款电子积木,对整个体系也不是很清楚,也不知道应该学些什么。因为刚刚转到硬件领域,也缺少相关经历和能力证明,科研实习面试都失败了。

一开始得到的少许鼓励来源于C站推荐的一系列交流会活动。我去参加了RTThread的学习营,第一次尝试在单片机开发中加入RTOS系统,也算是跳脱出自己当时缓慢的按部就班学习路线,如井底之蛙般弹出脑袋窥探了一眼外面的风景。

后来偶然收到李肯老师的攻城狮活动邀请。虽然因为选在了期末周,自己又比较懒,基本没更几篇文章hh。但是借助李肯老师的交流活动认识了很多嵌入式领域的前辈,同伴,从每天的交流话题中也能学到很多。

image-20230624180328147

现如今,虽然学到的知识相比之前可能没有太多,但是整个系统的框架,从硬件到ISA,操作系统解释器等系统软件,汇编语言和机器语言的转化等……确实见识到了很多。

再聊到本次活动。虽然自己没有做过多的尝试,但是因为之前我也只尝试过51和32的库函数开发,rcsa的configurator配置本身对我来说就是一种新奇的开发模式。与32的hal库对比更能让我体会到这些driver的便捷之处,也开始适应这种开发方式。

非常感谢给予这次机会的李肯老大!也期望自己能保持这股热情继续学下去~

51单片机入门

[TOC]

软件下载

开发:Keil。如果想要没乱码的中文注释,那么设置编码方式为 UTF8 或 GB2312。

程序文件下载到单片机:STC/普中(STC需要冷启动,先点击下载再开启单片机电源)

介绍

Micro Controller Unit, MCU 单片机,其中包含了CPU RAM ROM 输入输出设备 等一系列电脑硬件常用功能。

功能:通过传感器采集数据,通过CPU处理数据,控制硬件。

可以说是一个性能低的小电脑,是了解计算机原理的很好的学习方法。

右上角的跳线帽使用数码管时跳到VCC,使用点阵时跳到GND。

STC89C52RC 命名规则

STC:芯片为 STC 公司生产的产品。

8:该芯片为 8051 内核芯片。

9:表示内部含有 Flash EEPROM 存储器,还有如 80C51 中 0 表内部含有 MaskROM(掩模 ROM)存储器;如 87C51 中 7 表示内部含有 EPROM(紫外线可擦除 ROM)存储器。

C–表示该器件为 CMOS 产品。还有如 89LV52 和 89LE58 中的 LV 和 LE 都表示 该芯片为低电压产品(通常为 3.3V 电压供电);而 89S52 中 S 表示该芯片含有 可串行下载功能的 Flash 存储器,即具有 ISP 可在线编程功能。

5–固定不变。

2:表示该芯片内部程序存储(FLASH)空间大小,1 为 4KB,2 为 8KB,3 为 12KB,即该数乘以 4KB 就是芯片内部的程序存储空间大小。程序空间大小决定了 一个芯片所能装入执行代码的多少。一般来说,程序存储空间越大,芯片价格也 越高,所以我们再选择芯片的时候要根据自己需求选择合适芯片。 RC–STC 单片机内部 RAM(随机读写存储器)为 512B。还有如 RD+表示内部 RAM 为 1280B。还有芯片会省略此部分

image-20220828144814559

芯片介绍

芯片在 PDIP 里。黑色的部分 PDIP 是一种封装方式,可能还有 LQFP 等(一个正方形的形状)封装方式。

8051 内核基本上都是中间绿色块的样子,只是外设、封装等方式不同。

image-20220828145334929

管脚图:

Vcc 是电源,XTAL 管时钟,RST 是复位,等等。

image-20220828145345268

image-20230125214429628

整个是一个总线结构,所有外设都挂在上面。如最下面一行左边是晶振,右边是外部引脚。

只有这一个单片机是不能运行的,看我们的开发板上面还外接了好多好多外设呢。能让单片机运行的最小应用系统如下:

image-20230125214745143

三角是正极,三线符号是负极。

首先需要 Vcc 接正,GND 接负。

然后需要接晶振。没有晶振单片机程序无法一条条往下执行,有了晶振按照固定的周期才能一条条往下执行。晶振就是板子上银色的椭圆形的一个东西,频率写在上面,一般是有12MHz和11.多MHz的两种(有的芯片自带晶振。不过很明显我们的芯片并不自带)。

然后还有复位电路,让程序回到第一条的位置。

开发板介绍

image-20220903113237968

中间黑色的是刚刚介绍的单片机。拉起拉杆,可以取下单片机,但放回时一定不能放反。单片机有缺口的一端左侧从01开始,逆时针逐渐增大到40。

右侧中间有8个 LED 灯,我是点灯大师!

下面是一个矩阵按键,用户可以通过按按键输入。

最下面一行右侧有个红外接收传感器,接收红外线的。

左边无线模块,8个插孔的,做无线模块(如2.4G)用的。

再左边四个独立按键。

最左下是 USB 自动下载模块,插上 USB 线后按开关就会自动下载程序,不用了解。

DS1302 时钟芯片,可以做一个小时钟,读取时间。

红色按钮是 RST 按钮。

AD/DA 模数转换器,使单片机在数字与模拟领域之间转化。

74H595 可以扩展出更多的 IO 口。

步进电机可以精确控制脚步(转一圈、转半圈)。比如空调会用。

超声波模块可以测距。

蜂鸣器模块可以放歌。但我()()()()。

138译码器控制数码管,也可以扩展 IO 口。

24c02 也是一种 ROM,还是 EEP ROM(掉电不丢失)。其实单片机自带的 Flash ROM 更先进,但是只能用来存储程序。

温度传感器可以用来检测温度。

74HC245 可以驱动数码管(我的单片机是 HC138)。

左上角的电位器和排座用于接显示屏。电位器可以调整显示屏的亮度。

最大的黑色方阵是一个 LED 点阵。可以点亮8*8的方阵,甚至用来做动画。

之后的课程中还会详细介绍每一个模块,以及对应的电路图。

逻辑运算

&与,|或,!非,⊙同或(相同结果才=1),异或⊕(不同结果才为1)

C语言语法

int 16位,char 8位。

image-20220904115124141

image-20220904115224027

基本语法其他的都好说,再复习一下位运算。

image-20220904145323095

左右移补0.

位运算符也可以参与成为复合赋值运算符,如^=, <<=

逗号运算符=最后一个表达式的值

image-20220904145638619

函数在C语言基础上做的拓展

重入函数

在函数形参括号后加修饰符 reentrant,代表这个函数是重入函数,可以被递归调用,但这样就不能有bit变量,也不能进行位运算。

中断函数

在函数形参括号后加修饰符 interrupt m,系统编译时把对应函数转化为中断函数,自动加上程序头段和尾段,并按 51 系 统中断的处理方式自动把它安排在程序存储器中的相应位置。

在该修饰符中,m 的取值为 0~31,对应的中断情况如下:

0——外部中断 0

1——定时/计数器 T0

2——外部中断 1

3——定时/计数器 T1

4——串行口中断

5——定时/计数器 T2

其它值预留。

外部函数

如果要调用的函数不在本文件内,在其他文件内,定义函数时函数开头要加 extern 修饰符。

sfr sbit

用于定义特殊功能寄存器或特殊位。

1
2
sfr P0=0x80;//把地址 0x80 处的寄存器定义为 P0
sbit P0_1=P0^1;//取第一位定义为 P0_1

其实头文件 regx52.h 中都有。

能不能给位单独赋值要看是不是可位寻址。因为物理地址有限,每8个寄存器只能有一个可位寻址。

51单片机最小系统组成

  • 晶振电路,提供时钟,相当于心脏
  • 复位电路,系统运行不正常时可以重启
  • 电源电路,注意单片机的供电电压要求
  • 下载电路,烧入程序

另外注意,单片机的P0口是漏级开路,输出高电平会导致高阻态,因此输出高电平时要接上拉电阻,通常选择 4.7K~10K 阻值。

程序编写前言

新建项目 new μversion project

选择 CPU 型号:Keil 中没有完全对应的 STC89C52 版本,用Atmel 中的 AT89C52 即可,不用把8051启动文件添加到工程中。

AT 和 STC 是两种型号的单片机。有的 STC 单片机上面还有 AT 接口,AT 使用那个接口烧录程序。STC 就用 USB 下载。

新建好后有一个文件夹:source group,代码文件都在其中。

选中该文件夹,右键新建new item,新建c语言文件。可以选c/cpp/asm

在魔术棒 Output 选项中添加 “ create HEX file”.

程序框架

1
2
3
4
5
6
7
#include "reg52.h"
void main()
{
while(1)
{
}
}

编译:translate按钮

建立:build按钮,也有编译的作用,只编译发生变动的文件。

重新建立:rebuild,编译所有文件(速度慢不建议)。

报错如果显示:缺少root segment根段,即没有找到主函数。

头文件作用

#include<reg52.h>和`#include “reg52.h”都可以。区别在于<>直接去软件安装处搜索头文件,而””先在该项目下查找头文件,找不到再去软件安装处,再找不到就报错。

查看头文件可以在左侧的结构树对应的c文件目录下打开,或者右键“reg52.h” open 打开。

该头文件中定义了52单片机内部所有功能寄存器,把地址值如0x80赋值给P0等端口。

程序烧录

程序编译建立没有错误,也开启了魔术棒创建 HEX 文件选项,那么 build 后就会在对应路径中找到生成的 HEX 文件。

在 STC-ISP 中选定单片机型号、串口、晶振频率(可以直接看开发板上的晶振上面有写),选择对应的 HEX 文件,先断电开发板,再点击下载,再开机,就可以查看呈现在开发板上的效果。

HELLO WORLD——LED部分

LED 发光二极管。

image-20230125222255962

image-20230125222332854

下面两个黑色的方块就是8个电阻。电阻是限流作用,防止电流过大烧毁 LED。

电阻上面写着小小的“102”,代表10*10^2,即1kΩ。

每个 LED 正极是一定通电流的,如果负极接地,那么这个 LED 被点亮。否则两头都是高电平点不亮(这里的电平是 TTL 电平,高5低0)。

单片机如何驱动高低电平?在 MCU 内,CPU 接到指令(如P2^0口赋1,即高电平)CPU 把数据写入寄存器,寄存器数据通过驱动器放大后变为5V/0V 电平输出。

点亮 LED

GPIO(general purpose input output) 即通用输入输出端口,可以通过软件控制其输入和输出.

image-20220905111036720

  • 电源引脚: Vcc, GND
  • 晶振引脚:XTAL1 2
  • 复位引脚:RST VPD,不做其他功能。
  • 下载引脚:TXD RXD
  • GPIO引脚:Px.x的都是 GPIO 引脚,大致分为P0 P1 P2 P3,每组8个IO,P3还有附加功能,比如串口、外部中 断、计数器等。每个引脚每次只能使用一个功能。
1
2
3
4
5
6
7
8
9
10
11
#include "reg52.h"
sbit LED1=P2^0; //将 P2.0 管脚定义为 LED1
//我们也可以直接给P2整个赋值。比如P2=0xFE,即1111 1110,就只会点亮最后一个 LED 灯,和 P2^0=0 效果是一样的。
//另,我们的这种做法只是寻找特殊寄存器P2的第几位。而头文件 REGX52.H 中是真正包含所有引脚信息的,如P2_0 就是2.0引脚,也能起到一样的效果。
void main()
{
LED1=0; //LED1 端口设置为低电平,就会被点亮
while(1)//单片机默认不断执行主程序。如果没有这个死循环,单片机就会不断点亮点亮点亮点亮……不如点亮一次之后无限延时。
{
}
}

编译结果里面的几个数据的意义:

code:表示程序所占用 FLASH 的大小。

data:数据储存器内部 RAM 占用大小。

xdata:数据储存器外部 RAM 占用大小。

LED 闪烁

只需要点亮——延时——熄灭——延时循环即可。

单片机频率单位是 MHz 兆赫兹,所以只是单纯的亮灭亮灭肉眼看不出亮灭的效果。所以需要延时。

延时可以写一个这样的函数:

1
2
3
4
typedef unsigned int u16
void delay(u16 ten_us){
while(ten_us--);
}

u16 代表16位的无符号整型数据。这是一个比较常用的定义,unsigned char 定义为 u8, unsigned int 定义为 u16。当 ten_us 超出 u16 的范围后,跳出 while 循环。

然后就LED1=0;delay(50000);LED1=1;delay(50000);循环即可.

但是,STC-ISP 可以根据晶振频率和要延时的时间生成延时函数,真的牛!不过注意软件上标明的适用系列版本。

image-20230125224132185

其中 _nop_() 函数包括在 INTRINS.H 头文件中,是一个空语句,就只会产生延时的效果。

不过 STC-ISP 只能生成固定时长的延时函数。如果想要像自己写的那个 delay() 函数一样传入参数,延时对应长度的毫秒/微秒呢?

很简单,我们先生成延时1毫秒/微秒的函数,然后把函数中的内容重复执行传入参数遍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Delay1ms(unsigned int xms)		//@11.0592MHz
{
unsigned char i, j;
while(xms--){//这里是修改过的
_nop_();
_nop_();
_nop_();
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}
}

Keil 软件仿真

使用仿真功能查看 LED 闪烁案例中的实际延时时间。

  1. 点击魔术棒,选择 Target 选项卡,设置 Xtal 为12M或11.0592M,根据开发板晶振修改对应值。
  2. 点击黑色放大镜中有红色d的仿真按钮,进入仿真页面

image-20220905153847511

我们要关注的参数是sec。

  1. 点击RST按钮重新复位系统参数,sec 变为0。然后在要调试的行前双击,就会出现红色块的断点,点击8运行时就会直接运行到断点处。再次点击就会运行到下一处断点处。
  2. 点击红色标记8运行,运行到36行时显示用时:0.00039s,再次点击运行到37行,用时:0.45s
  3. 可见delay花费时间约为0.45s

LED 流水灯

学会了点亮和延时,流水灯的原理就很好懂了。就是给P2的所有端口赋值为:1111 1110,每次只有一个为0即点亮,这个点亮的0从最高位逐渐降到最低位。

取反后即为:

1000 0000

0100 0000

0010 0000

0001 0000

0000 1000

0000 0100

0000 0010

0000 0001

也就是一个移位运算,0x01<<i的循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "reg52.h"
# define LED P2
void delay(unsigned int i){
while(i--){}
}
void main()
{
while(1)
{
int i=0;
for(i;i<8;i++){
LED=~(0x01<<i);
delay(50000);
}
}
}

移位函数

位运算的移位操作只能补0,但是 Keil C51 软件内有对应的移位库函数,左移_crol_(),右移_cror_(),包含在 intrins.h 库中

移位函数会把移出去的位补到空位,一个循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "reg52.h"
#include "intrins.h"
# define LED P2
void delay(unsigned int i){
while(i--){}
}
void main()
{
LED=~(0x01);
delay(50000);
while(1)
{
LED=_crol_(LED,1);
delay(50000);
}
}

蜂鸣器实验

蜂鸣器简单地说,就是电磁线圈和磁铁对振动膜的作用。

单片机的是无源蜂鸣器,不能一直充电,需要外部控制器发送震荡信号,可以改变频率产生不同的音色、音调。

大多数有源蜂鸣器则没有这个效果,有源蜂鸣器外观与之相同,内部自带震荡源,接上电就能响,但不能改变频率。

image-20230207154444964

我们知道三极管的作用是不用单片机自己直接驱动单片机。

另一种方法是步进电机。

image-20230207155956609

ULN 2003,高电压 高电流驱动器,给信号就被驱动。IN 取反输出 OUT。

简谱

image-20230207163112788

首先整个谱大概分为几个区。大字组、小字组、小字1组、小字2组。每个组之间差8度,每相邻的两个键(如黑白)差半音,相邻的两个同色键差一个全音。

几个白键的表示方法就是下面的简谱,差半音的黑键用左上角的#表示升半音,b表示降半音。

演奏两大要素:音高和时值。

image-20240301121053848

谱上一个数字是1/4 音符,二分是其两倍,数字加个横线 - 。全音符就是(2 - - -)。这个线叫增时线。

八分是其1/2,数字下加一条线(2).再/2就再加一条,叫减时线。

试着识一个完整的谱:

image-20230207233115087

4/4:以四分音符为一拍,每小节有四拍。

第二节 $\dot{1}$ · 上面的点我们知道代表高音,后面的点代表:前一位音符延长1/2长度,即四分音符+1/2的四分音符。也就是3/8哈哈哈。

看第一节,一般连着两个八分音符就把 underline 连起来。但是这种哪怕是一个音,中间也要先断开再重响。比如右上角的3 3

升音和降音在本小节中有效。如第三行的 7 #4 4 7 ,两个4都是升音。

不过如果顶端画了延音线,就是连起来的不用断开。如中间的 $\widehat{7 7}$,拆开写是为了好读谱。

接下来就是如何把谱转化为单片机代码。左上角 1=c 说明是c调的。d大调会出现黑键,c调只有白键。

image-20230208000727170

音具体是怎么定义的?首先以中音a为基准,高音a是其2倍,低音a是其1/2。

中间每次升音都是等比数列递增的,即*2的1/12次方

使用蜂鸣器

响起来很简单:不断反转 P1^5 口(是不是这个口得看自己的板子型号)。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main()
{
u16 i=2000;//决定时值
while(1){
while(i--)
{
BEEP=!BEEP;
delay10Us(100);//决定音高
}
i=2000;
BEEP=0;
}
}

时值还好确认,音高怎么说?

首先我们有上图的音符与频率对照表。我们把频率转化为周期,即1/频率。这里周期单位是us。

image-20230208093509168

然后周期时长转化为机器周期,即记一个数需要的时间。我们看看需要多少机器周期。

1机器周期=12时钟周期,时钟周期=1/单片机晶振。比如对于我的11.0592MHZ 晶振,机器周期=12/11.0592MHZ (单位:us)。

据此把“需要切换的周期时长”转化为“需要切换的周期需要执行几次指令”。即周期/机器周期。如果是12MHZ 晶振这一步相当于没有。

image-20230208094537336

然后电平从低到高,从高到低才是一个周期。所以实际电平反转一次的周期是周期的一半。

image-20230208095158230

我们知道定时器原理是 TH TL 加至65536触发中断。因此重装载值(定时器初值)=65536-取整值。

音符 频率 周期 需要的机器周期数 需要的机器周期数/2 取整 重装载值
1 262 3816.794 3517.557252 1758.778626 1759 63777
1# 277 3610.108 3327.075812 1663.537906 1664 63872
2 294 3401.361 3134.693878 1567.346939 1567 63969
2# 311 3215.434 2963.344051 1481.672026 1482 64054
3 330 3030.303 2792.727273 1396.363636 1396 64140
4 349 2865.33 2640.687679 1320.34384 1320 64216
4# 370 2702.703 2490.810811 1245.405405 1245 64291
5 392 2551.02 2351.020408 1175.510204 1176 64360
5# 415 2409.639 2220.722892 1110.361446 1110 64426
6 440 2272.727 2094.545455 1047.272727 1047 64489
6# 466 2145.923 1977.682403 988.8412017 989 64547
7 494 2024.291 1865.587045 932.7935223 933 64603
1 523 1912.046 1762.141491 881.0707457 881 64655
1# 554 1805.054 1663.537906 831.7689531 832 64704
2 587 1703.578 1570.017036 785.0085179 785 64751
2# 622 1607.717 1481.672026 740.8360129 741 64795
3 659 1517.451 1398.482549 699.2412747 699 64837
4 698 1432.665 1320.34384 660.1719198 660 64876
4# 740 1351.351 1245.405405 622.7027027 623 64913
5 784 1275.51 1175.510204 587.755102 588 64948
5# 831 1203.369 1109.025271 554.5126354 555 64981
6 880 1136.364 1047.272727 523.6363636 524 65012
6# 932 1072.961 988.8412017 494.4206009 494 65042
7 988 1012.146 932.7935223 466.3967611 466 65070
1 1046 956.0229 881.0707457 440.5353728 441 65095
1# 1109 901.7133 831.018936 415.509468 416 65120
2 1175 851.0638 784.3404255 392.1702128 392 65144
2# 1245 803.2129 740.2409639 370.1204819 370 65166
3 1318 758.7253 699.2412747 349.6206373 350 65186
4 1397 715.8196 659.6993558 329.8496779 330 65206
4# 1480 675.6757 622.7027027 311.3513514 311 65225
5 1568 637.7551 587.755102 293.877551 294 65242
5# 1661 602.047 554.846478 277.423239 277 65259
6 1760 568.1818 523.6363636 261.8181818 262 65274
6# 1865 536.193 494.155496 247.077748 247 65289
7 1976 506.0729 466.3967611 233.1983806 233 65303

使用方法:TH=重装载值/256,TL=重装载值%256.

音高从低到高逐位响起代码:

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
#include "reg52.h"
#include "Delay.h"
#include "Timer0.h"
sbit beep=P1^5;

unsigned int beep_table[]={//可以加个0代表不响的0
63777,63872,63969,64054,64140,64216,64291,64360,64426,64489,64547,64603,
64655,64704,64751,64795,64837,64876,64913,64948,64981,65012,65042,65070,
65095,65120,65144,65166,65186,65206,65225,65242,65259,65274,65289,65303
};

unsigned char beep_select=0;

void main(){
unsigned char i;
timer0Init();
while(1){
beep_select++;
delayMs(50);//时值
}
}

void timer0Interrupt() interrupt 1
{
TH0 = beep_table[beep_select]/256; // 因为触发中断时,TH TL 归零,所以记得赋初值!
TL0 = beep_table[beep_select]%256;
beep=!beep;
}

编曲:

image-20230208125043337

根据乐谱写一个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int little_star[]={12, 12, 19, 19,
21, 21, 19, //增时线
17, 17, 16, 16,
14, 14, 12,
19, 19, 17, 17,
16, 16, 14,
19, 19, 17, 17,
16, 16, 14,
12, 12, 19, 19,
21, 21, 19,
17, 17, 16, 16,
14, 14, 12
};

遍历数组,得到的音高再去 beep_table 中获取重装载值。

1
2
TH0 = beep_table[little_star[beep_select]]/256; // 因为触发中断时,TH TL 归零,所以记得赋初值!
TL0 = beep_table[little_star[beep_select]]%256;

但是播放起来都是连着的,听起来效果并不好。可以每次播完一个音先关闭中断并延时一段时间,再继续播放。

1
2
3
4
5
6
7
while(1){
beep_select++;
delayMs(50);
TR0=0;
delayUs(1);
TR0=1;
}

増时线如何处理?中间是不断开一直想的,因此需要几个特定的音符delay时间更长一些。怎么区分哪些音符加长哪些不加呢?

最好还是存储乐谱时搞一个二维数组(逻辑上物理上都可以),既能存储音高,也能存储时值。

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
unsigned int little_star[]={12, 4,
12, 4,
19, 4,
19, 4,
21, 4,
21, 4,
19, 8, //增时线
17, 4,
17, 4,
16, 4,
16, 4,
14, 4,
14, 4,
12, 8,
19, 4,
19, 4,
17, 4,
17, 4,
16, 4,
16, 4,
14, 8,
19, 4,
19, 4,
17, 4,
17, 4,
16, 4,
16, 4,
14, 8,
12, 4,
12, 4,
19, 4,
19, 4,
21, 4,
21, 4,
19, 8,
17, 4,
17, 4,
16, 4,
16, 4,
14, 4,
14, 4,
12, 8,
0xFF,4//终止标志防越界
};

如果数组大小超限,在魔术棒-Target-Memory Model 中选择第三个。不过这只是治标不治本,因为 RAM 只有512字节所以存不下太长。可以在定义数组时加上关键词 code 来存在 ROM 8K 的闪存中。不过这样的数组是只读的。

当然这样找索引比较麻烦。最好是索引全部重新宏定义。

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
//音符与索引对应表,P:休止符,L:低音,M:中音,H:高音,下划线:升半音符号#
#define P 0
#define L1 1
#define L1_ 2
#define L2 3
#define L2_ 4
#define L3 5
#define L4 6
#define L4_ 7
#define L5 8
#define L5_ 9
#define L6 10
#define L6_ 11
#define L7 12
#define M1 13
#define M1_ 14
#define M2 15
#define M2_ 16
#define M3 17
#define M4 18
#define M4_ 19
#define M5 20
#define M5_ 21
#define M6 22
#define M6_ 23
#define M7 24
#define H1 25
#define H1_ 26
#define H2 27
#define H2_ 28
#define H3 29
#define H4 30
#define H4_ 31
#define H5 32
#define H5_ 33
#define H6 34
#define H6_ 35
#define H7 36

数码管

动态数码管原理是什么?首先我们知道每个数码管都有 abcdefg 七个段。

8个数码管那我们按理来说是需要8*7个引脚,很浪费。

于是设计了动态数码管。首先所有数码管是共阴极的。然后我们选中哪一个数码管阴极赋0,就会启动哪一个数码管,传入的 abcdefg 就会点亮该数码管的对应段。

然后8个数码管像流水灯一样,以极高频率依次点亮,肉眼看到的就是8个数码管都被点亮且呈现出不同的图案。

image-20230126130808285

开发板上的数码管是共阴极数码管,所有位共接一个阴极,给对应ABCDEF输入高电平点亮。

直接引脚:

直接引脚

image-20230126125258985

74译码器使用3位 bit 输入表示8种状态,调整 LED1~8 哪一个输出低电平,代表要启动8个数码管的哪一个的公共端。

输入的三位从最低位到最高位分别是P2^2, P2^3, P2^4,代表数码管从左到右的第几位是输入取反。

比如P2^4=1, P2^3=1, P2^2=0, 输入就是110,取反后就是001,就是从左到右第1位数码管(从第0位开始)。

VCC 和 GND 是使能,接到译码器上一上电就工作。

image-20230126005938488

P00P07 代表控制当前数码管的 ag 显示形式,接到 74HC245 缓冲器上而不是直接接到数码管上,使得单片机不用直接驱动数码管,Ai 连到 Bi 上。

OE 是使能,接地工作不接地不工作的原理。

DIR 是规定方向,高电平从左边读取数据传输到右边。低电平从右边读数据到左边。开发板上有个 J21 跳线帽,可以调整是 GND 与 LE 相连还是 VCC 与 LE 相连,也就是高电平输入 DIR 还是低电平输入 DIR。

数码管上面的 COM 是公共端,选中哪一个公共端(使得其=0,因为是共阴极)就是调整哪一个数码管的点亮方式。

点亮一位数码管代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "reg52.h"
#define Display P0
unsigned char display_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
/*具体显示什么图案是怎么推算出来的呢?首先 abcdefg 和 dp 小数点段一共有8个要控制的段。
比如我们要呈现数字6,就是 acdefg 亮,b和小数点不亮。
因为数码管是共阴极,所以我们想让哪个段亮哪个段就输入高电平,和 LED 相反。
所以P00~P07 的输入应该是 1011 1110
然后我们直接给 P0 赋值的话,是 P0_7 在最高位,P0_0 在最低位,所以输入应该正好反过来,0111 1101,即0x7d。*/
void main()
{
Display=display_code[0];
while(1)
{}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define SMG_A_DP_PORT P0 //使用宏定义数码管段码口
sbit LSA=P2^2;
sbit LSB=P2^3;
sbit LSC=P2^4;

for(i=0;i<8;i++){
//这个程序是从左到右显示12345678.当然完全也可以把确定哪一位数码管和确定要显示的数字封装成一个函数,点亮会更方便。
switch(i){
case 0: LSC=1;LSB=1;LSA=1;break;//从左往右第0个数码管
case 1: LSC=1;LSB=1;LSA=0;break;
case 2: LSC=1;LSB=0;LSA=1;break;
case 3: LSC=1;LSB=0;LSA=0;break;
case 4: LSC=0;LSB=1;LSA=1;break;
case 5: LSC=0;LSB=1;LSA=0;break;
case 6: LSC=0;LSB=0;LSA=1;break;
case 7: LSC=0;LSB=0;LSA=0;break;
break;
}
SMG_A_DP_PORT=gsmg_code[i];
delay_10us(100);
SMG_A_DP_PORT=0x00;//消影
//为什么要消影? 不延时的话动态数码管会反复重复 位选 段选 位选 段选……位选之后不一定及时段选,可能前一位的位选就会赋给后一位。因此需要消影。
}

我们这种动态数码管扫描方式是单片机直接扫描,硬件会简单很多,但是会占据大量的 CPU 时间。有的动态数码管自带显存和扫描电路,只要告诉他要显示什么他会自动扫描显示。

按键

内部有一个金属片,按下后电路接通。

image-20230125234043319

和学习 LED 时类似,所有 IO 口一开始都是高电平,我们给其接地就变成低电平了。

按下按键后一段时间内电平会高低抖动。

image-20230125235945530

1,先设置 IO 口为高电平(由于开发板 IO 都有上拉电阻,所以默认 IO 为高 电平)。

2,读取 IO 口电平确认是否有按键按下。

3,如有 IO 电平为低电平后,延时几个毫秒。

4,再读取该 IO 电平,如果仍然为低电平,说明按键按下。

5,执行按键控制程序。

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
#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
//定义独立按键控制脚
sbit KEY1=P3^1;
sbit KEY2=P3^0;
sbit KEY3=P3^2;
sbit KEY4=P3^3;
//定义 LED1 控制脚
sbit LED1=P2^0;
sbit LED2=P2^1;
sbit LED3=P2^2;
sbit LED4=P2^3;
//使用宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0

void delay_10us(u16 ten_us)
{
while(ten_us--);
}

u8 key_scan(u8 mode)
{
static u8 key=1;
if(mode)key=1;//连续扫描按键
if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))//任意按键按下
{
delay_10us(1000);//10ms 消抖
/*另一种消抖的方法:
if(KEY==0){
delay_ms(20);
while(KEY==0);
delay(20ms);
}
这个是针对按下按钮又抬起按钮之后的执行。*/
key=0;
if(KEY1==0)
return KEY1_PRESS;
else if(KEY2==0)
return KEY2_PRESS;
else if(KEY3==0)
return KEY3_PRESS;
else if(KEY4==0)
return KEY4_PRESS;
}
else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) //无按键按下
{
key=1;
}
return KEY_UNPRESS;
}
void main()
{
u8 key=0;
while(1)
{
key=key_scan(0);
if(key==KEY1_PRESS)//检测按键 K1 是否按下
LED1=!LED1;//LED1 状态翻转
if(key==KEY2_PRESS)//检测按键 K2 是否按下
LED2=!LED2;//LED1 状态翻转
if(key==KEY3_PRESS)//检测按键 K3 是否按下
LED3=!LED3;//LED1 状态翻转
if(key==KEY4_PRESS)//检测按键 K4 是否按下
LED4=!LED4;//LED1 状态翻转
}
}

矩阵按键

为了减少 IO 口的占用,用4个 IO 口代表行,4个 IO 口代表列。

类似动态数码管快速扫描实现几乎同时点亮的效果,矩阵键盘也是快速扫描。

image-20230126221349232

主要有两种扫描方法

行列式扫描法:每次给某一列赋值为0,然后检测这一列有无按钮按下。

​ 按行扫描:通过设置 P17 16 15 14 中的一个为低电平来选择扫描哪一行。根据 P10 P11 P12 P13 的输入判断是哪一列。但是 P15 口是蜂鸣器,不断反转会响。所以最好还是用按列扫描。

线翻转扫描方法:给所有列赋1,给所有行赋0,先判断在哪一行;然后用同样的方法判断在哪一列。

这里有一点问题就是:本单片机是准双向口输出,每个口既能做输入也能做输出而不用重新配置口线输出状态。其实这样相当于单片机一个引脚输出高电平,直接与另一个为低电平的引脚相连接。不会短路吗?

单片机的处理方法是这样的:内部 VCC 电源还附带一个下拉电阻。低电平的驱动力比高电平强,高电平直接接低电平就会被变为低电平,而不会短路。

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
115
116
#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
#define KEY_MATRIX_PORT P1 //使用宏定义矩阵按键控制口
#define SMG_A_DP_PORT P0 //使用宏定义数码管段码口
//共阴极数码管显示 0~F 的段码数据
u8 gsmg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};

void delay_10us(u16 ten_us)
{
while(ten_us--);
}

u8 key_matrix_ranks_scan(void)
{
u8 key_value=0;
KEY_MATRIX_PORT=0xf7;//1111 0111 给第一列也就是 P1_3 赋值 0,其余全为 1
if(KEY_MATRIX_PORT!=0xf7)//判断第一列按键是否按下
{
delay_10us(1000);//消抖。因为这里用了类似动态数码管的循环扫描的方法。
switch(KEY_MATRIX_PORT)//保存第一列按键按下后的键值
{
case 0x77: key_value=1;break;//0111 0111
case 0xb7: key_value=5;break;//1011 0111
case 0xd7: key_value=9;break;//1101 0111
case 0xe7: key_value=13;break;//1110 0111
}
}
while(KEY_MATRIX_PORT!=0xf7);//等待按键松开
KEY_MATRIX_PORT=0xfb;//给第二列赋值 0,其余全为 1
if(KEY_MATRIX_PORT!=0xfb)//判断第二列按键是否按下
{
delay_10us(1000);//消抖
switch(KEY_MATRIX_PORT)//保存第二列按键按下后的键值
{
case 0x7b: key_value=2;break;
case 0xbb: key_value=6;break;
case 0xdb: key_value=10;break;
case 0xeb: key_value=14;break;
}
}
while(KEY_MATRIX_PORT!=0xfb);//等待按键松开
KEY_MATRIX_PORT=0xfd;//给第三列赋值 0,其余全为 1
if(KEY_MATRIX_PORT!=0xfd)//判断第三列按键是否按下
{
delay_10us(1000);//消抖
switch(KEY_MATRIX_PORT)//保存第三列按键按下后的键值
{
case 0x7d: key_value=3;break;
case 0xbd: key_value=7;break;
case 0xdd: key_value=11;break;
case 0xed: key_value=15;break;
}
}
while(KEY_MATRIX_PORT!=0xfd);//等待按键松开
KEY_MATRIX_PORT=0xfe;//给第四列赋值 0,其余全为 1
if(KEY_MATRIX_PORT!=0xfe)//判断第四列按键是否按下
{
delay_10us(1000);//消抖
switch(KEY_MATRIX_PORT)//保存第四列按键按下后的键值
{
case 0x7e: key_value=4;break;
case 0xbe: key_value=8;break;
case 0xde: key_value=12;break;
case 0xee: key_value=16;break;
}
}
while(KEY_MATRIX_PORT!=0xfe);//等待按键松开
return key_value;
}

u8 key_matrix_flip_scan(void)//另一种扫描方式,这个函数暂时没有投入使用
{
static u8 key_value=0;
KEY_MATRIX_PORT=0x0f;//给所有行赋值 0,列全为 1
if(KEY_MATRIX_PORT!=0x0f)//判断按键是否按下
{
delay_10us(1000);//消抖
if(KEY_MATRIX_PORT!=0x0f)
{
//测试列
KEY_MATRIX_PORT=0x0f;
switch(KEY_MATRIX_PORT)//保存行为 0,按键按下后的列值
{
case 0x07: key_value=1;break;
case 0x0b: key_value=2;break;
case 0x0d: key_value=3;break;
case 0x0e: key_value=4;break;
}
//测试行
KEY_MATRIX_PORT=0xf0;
switch(KEY_MATRIX_PORT)//保存列为 0,按键按下后的键值
{
case 0x70: key_value=key_value;break;
case 0xb0: key_value=key_value+4;break;
case 0xd0: key_value=key_value+8;break;
case 0xe0: key_value=key_value+12;break;
}
while(KEY_MATRIX_PORT!=0xf0);//等待按键松开
}
}
else
key_value=0;
return key_value;
}

void main()
{
u8 key=0;
while(1)
{
key=key_matrix_ranks_scan();
if(key!=0)
SMG_A_DP_PORT=gsmg_code[key-1];//得到的按键值减 1 换算成数组下标
}
}

IO 扩展(串转并)-74HC595

前面接的一些输入输出设备都是直接连接的单片机 IO 口,单片机仅有的 IO 口非常有限。而使用 IO 扩展可以大量增加可使用的端口。比如后面要使用的 LED 点阵,8*8个格子,使用扩展 IO 输入就更为合适。如果多级联一个,就又有了8位输出,能实现16*16的点阵。

由图可知,OE 低电平有效,因此 LED 点阵旁的跳线帽一定要接到 OE-GND 一端。

74HC595 是一个位移缓存器,有8位串行输入、并行输出,并行输出是三态输出(高电平、低电平、高阻抗)。比如一次输入一个比特,输入八次,并行输出可以输出一个8位的字 1010 1010.

输出是由 VCC 驱动的,原理有那么一点像三极管。因为单片机内部是弱上拉,输出不足以点亮 LED 点阵,所以抛开 IO 口不够的问题,也不能直接接到 LED 点阵上,至少要有三极管。

点亮 LED 点阵

image-20230130004758073

image-20230130004829583

传入数据如列是0100 0000,行是0000 0001,则代表最后一行第二列的点会被点亮。

SRCLK:移位寄存器,数据先传输到移位寄存器中。移位寄存器上升沿时移位,再接收下一次数据。

RCLK:存储寄存器。存储寄存器上升沿时把寄存器中所有数据都通过端口输出。

相当于手枪,每次 SRCLK 上升时我们填入一枚子弹,RCLK 上升时把弹夹塞入。

QH 是级联用的。

列数据直接输入引脚,行数据输入 IO 拓展。

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
#include "reg52.h"
//编写程序先定义管脚和端口。管脚用sbit,端口宏定义
#define LED_MATRIX P0
sbit SRCLK=P3^6;
//因为 RCLK 是关键字不能被复用了
sbit rCLK=P3^5;
sbit SER=P3^4;
unsigned char hc_led_arr[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
void delay(int i){while(i--);}

void hc_write_data(unsigned char c){
//要传入8个输入,需要循环
int i=0;
for(i;i<8;i++){
//注意 芯片传数据先传高位 再传低位,所以要反着写。这个问题在write函数内部解决,传入的数据和想要的形式一样就好。
//通过移位获取
SER=c>>7;
//想获取下一位寄存器,需要移位寄存器移位。需要创造上升沿
SRCLK=0;
//芯片一般给定一个延时时间,经过这个时间之后才能处理完毕
delay(1);
SRCLK=1;
delay(1);
//让传入数据的次高位变为下次循环的高位
c<<=1;
}
//最后通过存储寄存器的上升沿,传输全部数据
rCLK=0;
delay(1);
rCLK=1;
}
void main(){
LED_MATRIX=0x00;
while(1){
int i=0;
for(i;i<8;i++){
hc_write_data(0x00);//消隐
hc_write_data(hc_led_arr[i]);
delay(500000);
}
}
}

比如0000 0001,传入LED阵列的数据是:每轮循环传入最高位的值,并且所有数据向左移动一位。因此前7轮 SER 传入都是0,最后一轮 SER 传入1,最下面一行全亮。

LED 点阵实验

上面的方法只能确定某一具体的行被点亮。可不可以具体确定哪些点点亮的方法?

我们让想被点亮的点列为低电平,行为高电平,就会被点亮。如果我们只想点亮第一行第一列的点,只需行脚只有第一行接高电平,列脚只有第一列接低电平即可。

所以只要先让第一列为低电平,其他列为高天平来只读取第一列,遍历所有行检查第一列哪些点应该被点亮;然后第二列,第三列……一次类推,每轮循环不用消除上次的结果即可。

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
#include "reg52.h"
//编写程序先定义管脚和端口。管脚用sbit,端口宏定义
#define LED_MATRIX P0
sbit SRCLK=P3^6;
sbit rCLK=P3^5;
sbit SER=P3^4;
unsigned char hc_led_arr[8]={0x38,0x7C,0x7E,0x3F,0x3F,0x7E,0x7C,0x38};
unsigned char col[8]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe};
void delay(int i){while(i--);}

void hc_write_data(unsigned char c){
//要传入8个输入,需要循环
int i=0;
for(i;i<8;i++){
//注意 芯片传数据先传高位 再传低位,所以要反着写
//通过移位获取
SER=c>>7;
//想获取下一位寄存器,需要移位寄存器移位。需要创造上升沿
SRCLK=0;
//芯片一般给定一个延时时间,经过这个时间之后才能处理完毕
delay(1);
SRCLK=1;
delay(1);
//让传入数据的次高位变为下次循环的高位
c<<=1;
}
//最后通过存储寄存器的上升沿,传输全部数据
rCLK=0;
delay(1);
rCLK=1;
}
void main(){
LED_MATRIX=0x00;
while(1){
int i=0;
for(i;i<8;i++){
LED_MATRIX=col[i];
hc_write_data(hc_led_arr[i]);
//不知道为什么,下面两部分不写图形会偏移。不知道会不会有大佬解答一下
delay(100);
hc_write_data(0x00);
}
}
}

点阵的具体图案生成方法:字模提取软件。

image-20220908234813283

步进电机

电脉冲信号转化为角位移。

注意步进电机红色线接到5V的地方。以下程序意为:启动步进电机后,按按钮1旋转方向改变,按按钮2加速,按按钮3减速。

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
#include "reg52.h"
typedef unsigned int u16;
typedef unsigned char u8;
sbit IN1_A=P1^0;
sbit IN2_B=P1^1;
sbit IN3_C=P1^2;
sbit IN4_D=P1^3;

//定义独立按键控制脚
sbit KEY1=P3^1;
sbit KEY2=P3^0;
sbit KEY3=P3^2;
sbit KEY4=P3^3;
//使用宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0
#define STEPMOTOR_MINSPEED 1
#define STEPMOTOR_MAXSPEED 5

void delay(u16 ten_us)
{
while(ten_us--);
}

void step_motor_28BYJ48_send_pulse(u8 step,u8 dir){
u8 temp=step;
if(dir==0)temp=7-step;//逆时针旋转
switch(temp)//8 个节拍控制:A->AB->B->BC->C->CD->D->DA
{
case 0: IN1_A=1;IN2_B=0;IN3_C=0;IN4_D=0;break;
case 1: IN1_A=1;IN2_B=1;IN3_C=0;IN4_D=0;break;
case 2: IN1_A=0;IN2_B=1;IN3_C=0;IN4_D=0;break;
case 3: IN1_A=0;IN2_B=1;IN3_C=1;IN4_D=0;break;
case 4: IN1_A=0;IN2_B=0;IN3_C=1;IN4_D=0;break;
case 5: IN1_A=0;IN2_B=0;IN3_C=1;IN4_D=1;break;
case 6: IN1_A=0;IN2_B=0;IN3_C=0;IN4_D=1;break;
case 7: IN1_A=1;IN2_B=0;IN3_C=0;IN4_D=1;break;
default:break;
}
}


u8 key_scan(u8 mode)
{
static u8 key=1;
if(mode)key=1;//连续扫描按键
if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))//任意按键按下
{
delay(1000);//消抖
key=0;
if(KEY1==0)
return KEY1_PRESS;
else if(KEY2==0)
return KEY2_PRESS;
else if(KEY3==0)
return KEY3_PRESS;
else if(KEY4==0)
return KEY4_PRESS;
}
else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) //无按键按下
{
key=1;
}
return KEY_UNPRESS;
}

void main(){
u8 key=0;
u8 dir=0;
u8 speed=STEPMOTOR_MAXSPEED;
u8 step=0;
while(1){
key=key_scan(0);
switch(key)
{
case KEY1_PRESS:dir=!dir;break;
case KEY2_PRESS:if(speed>STEPMOTOR_MINSPEED)speed-=1;break;
case KEY3_PRESS:if(speed<STEPMOTOR_MAXSPEED)speed+=1;break;
default:break;
}
step_motor_28BYJ48_send_pulse(step++,dir);
step%=8;
delay(speed*1000);
}
}

中断

使单片机能对外部或者内部随机发生的事件实时处理。

分时操作,实时响应,可靠性高。

中断相应条件:首先我们要确保相关配置都准备好了,CPU 允许中断,中断源允许中断,然后发生中断事件时才会正确触发中断。

中断可能还会被优先级更高的中断打断,支持这种操作的系统叫多级中断系统。

STC89C52 有8个中断,4外部,3定时器,1串口。

image-20230127002315074

这是传统51单片机定时器中断结构,原理与 STC89C52 相近。

image-20230127002418325

通过配置寄存器控制线路连接。比如上图中的开关就是由寄存器控制。

EA ENABLE ALL:即使能所有中断。

ET:中断允许位。

PT:中断优先级。只有一个 PT 只能决定是高或低两种优先级。更多的中断优先级寄存器可以决定更多中断优先级。

TCON 部分: time controller,不属于 CPU 部分,等到定时器部分展开叙述。

代码编写:主程序中需要包含:

1
2
3
4
5
6
7
8
9
10
EA=1;//总中断开关:打开
EX0=1;//外部中断0开关:打开。
IT0=0/1;//外部中断触发方式的选择。如下降沿触发,或低电平触发。
//如果要配置外部中断1,则改为EX1和IT1

//中断服务函数
void int0() interrupt 0 using 1//using 1 可省略
{

}

定时器中断实验

本章利用单片机自带的定时器来实现之前做过的操作:LED灯间歇闪烁。一直以来实现的方法都是借助while循环来拖延时间。定时器不仅更加准确,还可以节省下 CPU 的资源。

STC89C52 有3个定时器。

CPU时序的相关知识

振荡周期:为单片机提供信号的振荡源的周期(晶振周期)。12MHZ 的晶振振荡周期就是1/12us, 求倒数。

状态周期:两个振荡周期=1状态周期s(时钟周期)。

机器周期:6状态周期=1机器周期。

指令周期:完成一条指令所用的全部时间,以机器周期为单位。

定时器的相关知识

定时器又可以计数,也叫计数器。不需要CPU参与自己就能完成任务,根据脉冲信号对寄存器中数据+1。来一个脉冲定时器+1,加到全为1后输出一个脉冲并归0.同时,向CPU发出计时器中断信息。

一般有四种工作模式:

13位定时器,16位定时器,8位自动重装模式,双8位计数器。

我们的工作模式用的是16位。

串口通信中为了精度考虑要使用自动重装模式。因为我们要自己设置每次溢出产生中断后 TH TL 的初值,这样吧比较容易出错且有点慢。8位自动重装模式就是舍弃了16位存储数据(到65535),只采用后八位 TL 计数(到255),初始值TH TL 赋一样的初值。TL 每次溢出,TH 把自己的值赋给 TL,这样就不用我们自己手动重新赋初值了,初值一直保存在 TH 中。

而且,在本例中我们使用定时器中断就是为了产生中断时给定时器赋初值并让灯切换状态。使用8位自动重装模式后,不用手动给定时器赋初值了;如果没有其他的需要定时器溢出时必须做的操作,定时器可以不用设置中断,起到一个可以看时间但是不会响的闹钟的功能,即 ET=0。

image-20230126235200709

左上角支路是时钟功能,左下角支路是计数功能,最终实现中断功能。

TH TL 寄存器最大能存储到65535.每来一次脉冲+1,加到最大值时 flag 申请中断。

SYSCLK 是晶振周期。另一个时钟是 T0 引脚,如果启用 T0 引脚定时器就变成计数器了,每来一个脉冲+1。

默认使用12T 的分频,把 12MHZ 分成12份,每一份就是1us。这个单片机上是没有对应调整的寄存器的,如果想使用 6T 的分频需要在 STC-ISP 中选择使能 6T 模式。

CT 是一位寄存器,赋1为C,即计数器;赋0为T,即时钟(T上面的横线就代表0时)。

每个定时器主要有两种寄存器:TCON TMOD。

TCON 包括:TF, TR, IE, IT。

image-20230127114845707

​ TF 可见上图主路,TH TL 被允许计数后周期性+1计数,加到最大值时 TF=1,并发起中断。处理完中断后恢复为0.

​ TR 可见上图支路,是开启中断的条件之一。

​ IE 是外部中断。

​ IT 是设置中断触发模式,比如设置为0是低电平触发,设置为1是下降沿触发。

TMOD 是不可寻址的寄存器,也就是只能整体赋值,不能像 P2 一样分开给每个变量赋值。包含:GATE, CT, M0, M1.

image-20230127114908062

​ GATE 用于开启定时器。当 GATE 打开, TR=1(timer reset)且$\overline{INT1/INT0}$ 为高(即打开中断引脚)时定时器开始工作。这一部分内容对应上图电路中的左下角。

​ M0M1 用于选定时钟的4个模式。比如16 位就是01. 两者包含一个叫做 TMOD 的不可位寻址的变量里,

​ CT就是打开定时器的计时还是时钟功能。

开启定时器计数功能及总中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void time0_init(void)
{
TMOD&=0xF0;//设置定时器模式
TMOD|=0X01;//我们知道 TMOD 是不可位寻址,也就是里面虽然既包含了定时器1和0的寄存器,但是我们赋值只能一整个赋值。
//如果直接给 TMOD=0x01,就会影响定时器1的值。因此我们用这两部先清空后四位,再单独给后四位赋值为0x01。

//TCON 中的 IE IT 对应就是支路图中的 INT0,因为我们开启了 GATE,或电路,因此 IE IT 不用设置也行。
//我们知道 TH TL 合起来达到65535,也就是过了 65535个机器周期 后会触发中断。
//比如我们现在想1ms触发一次中断,怎么处理呢?
//12MHZ 下1us一个周期,1ms 1000个周期,因此我们每次设置初始值为64535,变为65535正好需要1ms。
//11.0592MHz除12为921600Hz,就是1us 0.9216 个机器周期,因此初值为65535-922=64613.
//我们可以给 TH TL 赋初始值,64535,这样只要过 1000us 就会触发中断。
//然后因为 TH 和 TL 拼接变为一整个16位的寄存器,所以 TH 是高8位,TL 是低8位,分别用计算出的初值/256 %256得到最终结果。
TH0=0XFC; //64613/256=252=0xFC,我的单片机是 11.0592 MHZ
TL0=0X65;//64613%256=0x65

TF0=0;//归零,防止刚配置好就产生中断。可有可无
TR0=1;//打开定时器,开启中断条件之一
ET0=1;//打开定时器 0 中断允许
EA=1;//打开总中断
PT0=0;//设置中断优先级为低。默认也是低,不写也没关系。
}

STC-ISP 上也有生成定时器函数。不过 AUXR 设置定时器时钟那一步是针对最新版本可以调整单片机定时器使能而添加的,我们的单片机加上会报错,需要删掉。

另外需要手动添加 ET EA PT。

image-20230127164307914

LED灯间隔1s闪烁代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "reg52.h"
sbit LED=P2^0;
void time0_init(void);//不再重写了~

void time0() interrupt 1{
static int i;
TH0=0XFC; //因为触发中断时,TH TL 归零,所以记得赋初值!
TL0=0X65;
i++;
if(i==1000)
{
i=0;
LED=!LED;//闪烁
}
}
void main(){
time0_init();
while(1){}
}

外部中断实验

运行程序前,请摘下红外接收传感器。因为共用P3^2引脚,会干扰实验结果。

51单片机都有2个外部中断。STC89C5X系列有INT0~INT3四个。

对于三个参数的初始化,一般用一个init函数执行,在main的最开头。

本例中,我们用按键3作为外部中断源。按下按键3就会产生中断。中断执行的指令就是点亮或熄灭LED灯。

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
#include "reg52.h"
sbit LED=P2^0;
sbit KEY3=P3^2;
void delay(int ten_us)
{
while(ten_us--);
}
void interrupt_init(){
EA=1;//总中断开关:打开
EX0=1;//外部中断0开关:打开。
IT0=1;//外部中断触发方式的选择。如下降沿触发,或低电平触发。
}

void int0() interrupt 0{
delay(1000);
if(KEY3==0){
LED=!LED;
}
}

void main(){
interrupt_init();
while(1){

}
}

通信

通信基础知识

单片机还可以通过IO口实现多种通信。

串行通信:一条数据线,一次发1bit,发很久。

并行通信:多条数据线,同时发送,发的速度快多了但是费用高、接收困难、抗干扰性差。

异步通信:发送和接收方时钟可以不用完全一致。

同步通信 :发送和接收方时钟要完全一致。

单工、半双工、全双工通信:数据的传输方式,略。

比特率:位/s。

波特率:码元/s。

溢出率:比如13us溢出一次。溢出率就是1/13us。

校验位:如奇偶校验位。

停止位:分隔数据帧。

(有那么一点点感谢通原了~)

串口

串口通信,指外设和计算机之间通过数据线等传输数据的一种通信方式。比如RS-系列,大多数计算机应该都有对应的梯形接口。51单片机内自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现与计算机之间的串口通信!

单片机串口通信的管脚:VCC TXD(发送数据,串行输出)RXD(接收数据,串行输入)SGND(信号接地)。

电脑的串口还有很多管脚,如 RTS CTS,单片机的相对简单很多。

TXD和RXD用正负电压表示逻辑1和0,51单片机采用TTL 晶体管-晶体管逻辑集成电路,用高低电平表示逻辑状态(+5V:1;0V:0),所以需要转换型时候才能与计算机串口通信。

还有两种电平状态:RS232,315V 表示低(注意),-15-3V 表示高。RS485,两线压差(差分信号)26V 表示高,-6-2V 表示低。

image-20230128231105461

STC89C52系列有一个通用异步收发器(UART P30 P31端口),有四种工作模式。

  • 模式0:同步移位寄存器;
  • 模式1:8位UART,波特率可变(常用);
  • 模式2:9位UART,波特率固定;
  • 模式3:9位UART,波特率可变.

image-20230129013008311

TXD RXD 直接接到单片机 P30 P31 上. 另一端是单片机上自带的,我们把数据线连到电脑上就接上了。

image-20230129014312317

溢出率到波特率的计算见图。

串口助手和单片机要规定好发送数据的形式。

image-20230129193307966

串口数据缓存寄存器:SBUF。物理上是接收和发送两个寄存器,实际上共用相同的地址,只是使用时有不同的意义。我们只需要把数据放入其中就行,发送原理暂不用弄明白。

SCON:串口控制寄存器。控制电路。包含:

​ SM0,SM1:设置工作方式。比如我们采用8位 UART,就赋值01.

​ SM2:与工作方式1无关。

​ REN:是否允许串行接收状态。1允许接收。

​ TB8 RB8:接收到的第9位数据,与工作方式1无关。

​ TI RI:发送接收中断请求标志位。代表发送完了。硬件赋1,需要用软件复位。

赋值的话只有 SM0 SM1=01,和 REN 需要注意,其他的初始值都=0。

PCON:电源管理。包含:

​ SMOD:可见支路图,用于设置波特率是否加倍。

​ SMOD1:纠错或协助 SM0 设置工作方式。

IE:打开中断。

移位寄存器会触发对应中断。在中断图中的 TI RI,触发的是同一个中断。

实施串口通信

STC-ISP自带一个串口调试助手。

image-20230103020705868

串口选择左侧和串口号一致的选项。

STC89C52串口初始化函数:

1
2
3
4
5
6
7
8
9
10
11
void uart_init(void)
{
TMOD|=0X20; //设置计数器工作方式 2
SCON=0X50; //设置为工作方式 1。40是 REN 关闭,50是打开,代表单片机是否可以接收数据
PCON=0X80; //波特率加倍,0就是不加倍
TH1=0XFA; //计数器初始值设置,根据波特率为9600
TL1=0XFA;
ES=1; //打开接收中断
EA=1; //打开总中断
TR1=1; //打开计数器1
}

初始化函数也可以在 STC-ISP 中生成。这里会发现 12MHZ 的晶振相较 11.0592 MHZ 的晶振误差较大,要通过波特率加倍才能减少一些。这就是 11.0592 MHZ 晶振的设计原因。

单片机向电脑发送数据:给SBUF赋值即可。

在程序中发送可以直接SBUF=0X11;单片机就会收到11的信息,点击复位按钮后可以在串口助手的接收缓冲区中看到。

赋值后需要一段时间才能发送完成,发送完成后TI不再是0.

1
2
3
4
5
6
void main(){
uart_init();
SBUF=0x11;
while(TI==0);TI=0;//=1说明发送完成,然后手动复位
while(1){}
}

这样就在程序中发送了11信息。

电脑给单片机发送数据:通过串口助手发信息可以通过串口中断interrupt 4实现。

1
2
3
4
5
6
7
8
9
10
11
void uart() interrupt 4 //串口通信中断函数
{

u8 rec_data;
P2=0x00;//这一句使得函数成功触发时LED灯全亮,便于调试
if(RI==1){P2=~SBUF;RI=0;}//因为发送和接收中断共用4中断,这句用于区分具体是发送还是接收中断
//如果是接收中断,RI=1,那么只简单执行这两句即可。
SBUF=rec_data; //将接收到的数据放入到发送寄存器
while(!TI); //等待发送数据完成
TI=0; //清除发送完成标志位
}

以上程序可以将发送缓冲区中输入的数据发给单片机,单片机再在接收缓冲区中原封不动地呈现出来。

文本模式和 HEX 模式就是文本和 ASCII 码的转换。

封装头文件;绘制LED动画

类似C语言的语法,部分函数等内容可以封装到头文件里,需要的时候引入到source file 中,再在 include 中指明即可正常使用。

编写.h文件:如:

image-20230103162731330

引入.h文件:右键左侧的.c文件→options for file→C51→include path→找到.h文件所在的文件夹并选中,注意一定不能有中文路径。然后就可以使用.h文件中定义的变量和函数了,注意不能重复定义

什么内容封装到函数里呢?静态的方法待调用的封装进去。逻辑判断后决定使用哪个方法这类的就不用放进函数里了,因为逻辑判断很可能经常改。

接下来就自己试着先把delay函数和矩阵LED绘制函数写入头文件,然后制作矩阵动画,这样动起来也会更方便一些。

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
//h file
#include "reg52.h"
typedef unsigned char u8;
typedef unsigned int u16;

sbit SRCLK = P3 ^ 6;
sbit rCLK = P3 ^ 5;
sbit SER = P3 ^ 4;

/**
* Function name: time0_init
* Function paremeter: void
* Function performance: 初始化定时器。只有执行此函数后才能使用单片机的定时器功能
*/
void time0_init(void)
{
TMOD |= 0X01; // 选择为定时器 0 模式,工作方式 1
TH0 = 0XFC; // 给定时器赋初值,定时 1ms
TL0 = 0X18;
ET0 = 1; // 打开定时器 0 中断允许
EA = 1; // 打开总中断
TR0 = 1; // 打开定时器
}
/**
* Function name:time0
* Function paremeter: void
* Function performance: 启动定时器。定时器计数器time_counter在0~10000之间循环。
*/
/*
void time0() interrupt 1{
TH0=0XFC; //给定时器赋初值,定时 1ms
TL0=0X18;
time_counter++;
bit pass_1s=0;
if(time_counter==1000)
{
time_counter=0;
pass_1s=~pass_1s;
}
}
*/
void delay(u16 delay_10us){
while(delay_10us--);
}

void hc_write_data(unsigned char c)
{
// 要传入8个输入,需要循环
int i = 0;
for (i; i < 8; i++)
{
// 注意 芯片传数据先传高位 再传低位,所以要反着写
// 通过移位获取
SER = c >> 7;
// 想获取下一位寄存器,需要移位寄存器移位。需要创造上升沿
SRCLK = 0;
// 芯片一般给定一个延时时间,经过这个时间之后才能处理完毕
delay(1);
SRCLK = 1;
delay(1);
// 让传入数据的次高位变为下次循环的高位
c <<= 1;
}
// 最后通过存储寄存器的上升沿,传输全部数据
rCLK = 0;
delay(1);
rCLK = 1;
}

void matrix_led_animation(u8 hc_led_arr[])
{
unsigned char col[8]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe};
int i=0;
P0=0x00;

for(i;i<8;i++){
P0=col[i];
hc_write_data(hc_led_arr[i]);
//不知道为什么,下面两部分不写图形会偏移。不知道会不会有大佬解答一下
delay(1);
hc_write_data(0x00);
}

}

然后就是利用取模软件得到要绘制的图案的字模。这里我选定的图案是之前圣诞节临摹过的像素画中”Merry Christmas“的字体。参照来源:圣诞节 像素画 圣诞树🎄_哔哩哔哩_bilibili

image-20230104123321341

绘制部分代码:

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
#include "reg52.h"
#include <MyHFile.H>
u8 anime_row[]={
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C,
0x40, 0x7C, 0x40, 0x7C, 0x00, 0x1C, 0x14, 0x18,
0x00, 0x1C, 0x10, 0x00, 0x1C, 0x10, 0x00, 0x1D,
0x05, 0x1F, 0x00, 0x7C, 0x44, 0x44, 0x44, 0x00,
0x7C, 0x10, 0x1C, 0x00, 0x1C, 0x10, 0x00, 0x5C,
0x00, 0x04, 0x1C, 0x10, 0x00, 0x7C, 0x14, 0x00,
0x1C, 0x10, 0x1C, 0x10, 0x1C, 0x00, 0x0C, 0x14,
0x1C, 0x00, 0x04, 0x1C, 0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00
};
void main()
{
int cnt = 0;
while (1)
{
for(cnt;cnt<62;cnt++){
u8 row[8]={0,0,0,0,0,0,0,0};
row[0]=anime_row[cnt];
row[1]=anime_row[cnt+1];
row[2]=anime_row[cnt+2];
row[3]=anime_row[cnt+3];
row[4]=anime_row[cnt+4];
row[5]=anime_row[cnt+5];
row[6]=anime_row[cnt+6];
row[7]=anime_row[cnt+7];
matrix_led_animation(row);
delay(5000);
}
cnt=0;
}
}

虽然因为每次绘制完成必须擦除再重新绘制,导致看起来一闪一闪的。

不过最终的动画效果还是不错的~

IIC, AT24C02

掉电不丢失的存储器。存储结构是 E2PROM,通讯方式是 I2C。

EEPROM

存储器主要分为 RAM 易失性存储器和 ROM 非易失性存储器,RAM 存取快但掉电丢失,ROM 正相反。

image-20230217001811596

SRAM:锁存器,速度最快。

DRAM:电容,充电1放电0. 但是电容漏电比较严重,需要扫描器每隔一段时间检查一下是否漏电,漏电再充。

ROM 名字来源于 read-only,但是后期的非易失性存储器都可以写入,只是沿用了名字。

MASK ROM:不可写入。

PROM:只能写入一次。

EPROM:可擦除,但是形式比较麻烦,要用紫外线照射很久。

E2PROM:电擦除更加方便,我们现在的单片机所用。但是容量小。

FLASH:应用广泛。

image-20230217060513510

这只是一个有助理解的简化模型~

选定一些结点导通,比如我们读取第一行地址1处的数据,发现第1/2/3个结点导通,其他节点都未导通,则从最下端读到的该处地址的数据为:1110 0000。

MASK ROM 中,两条线中没有二极管,则两条线都通电流就会导通。有二极管后,数据总线无法流下来,为低电平。

PROM 中如何实现可编程写入一次?蓝色的二极管容易被击穿。被击穿后二极管的通路变为断路,导通;否则和 MASK ROM 低电平原理一样。

EPROM 是二极管可以恢复。

选中哪条总线可以通过三位138译码器的输入决定。

image-20230217062720840

VCC 范围:1.8~5.5v

WE/WR:写保护,WR 高电平保护,不能写入。可以看到 WE 标明低电平启用,而且单片机上的 EEPROM 直接 WE 接地了。

SCL SDA:I2C 接口。

E1 E2:I2C 地址。

image-20230217063431839

大概是能看懂的。SCL SDA 传入数据后,看是 R/W 哪个地址里的数据,通过 DEC 译码器找到地址 R/W,通过串行多路复用器 serial MUX 把并行数据转化为串行并输出。

I2C

I2C(Inter-Integrated Circuit BUS)总线是由 PHILIPS 公司开发的两线式 串行总线,用于连接微控制器及其外围设备。因为各家自己开发的数据总线可能不通用,PHILIPS 公司开发了这款统一通信标准。这样不仅方便了芯片开发,也便于大家学习。

I2C 结构

I2C 只有两根双向信号线,一根是 SDA 数据线,一根是 SCL 时钟线。是同步、半双工的通信线,带数据应答。

所有设备的 SCL 和 SDA 都连在总线上。设备的 SCL SDA 是开漏输出的(高电平直接接地),SCL 和 SDA 又要添加弱上拉电阻。这两共同作用实现了“线与”的功能,避免各个设备通信相互干扰。

如下图所示,设备上的 SCLK SDA 是开漏输出。如果给高电平导通开关, IN 也是接地,该设备就无法读取输入数据不会被干扰。否则如果是低电平关闭开关,数据会流出 DATA IN。

image-20230217072355835

主机:启动数据传送并产生时钟信号的设备;

从机:被主机寻址的器件;

多主机:同时有多于一个主机尝试控制总线但不破坏传输;

主模式:用 I2CNDAT 支持自动字节计数的模式; 位 I2CRM,I2CSTT,I2CSTP 控制数据的接收和发送;

从模式:发送和接收操作都是由 I2C 模块自动控制的;

仲裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并 使传输不被破坏的过程;

同步:两个或多个器件同步时钟信号的过程;

发送器:发送数据到总线的器件;

接收器:从总线接收数据的器件。

I2C 协议

①时钟信号周期性的高电平低电平。只有时钟信号为低电平时,数据信号才能变化。

②起始和停止信号:SCLK 高电平时,SDA 由高→低为开始,低→高为结束。

④发送数据:

image-20230217111133747

先发 start 标识,后跟一个字节,第一个字节是地址+读写位。前四位固定,比如 24C02 是1010. 后三位是地址。收到接收应答后就开始发送数据。读写位 $R/\overline{W}$ .

SCLK 低电平时,SDA 先设置好要传的数据。然后 SCLK 变为高电平后 SDA 不允许再数据变化,SCLK 先读取数据。这样发送一位,SCLK 再变为低电平,如此循环。

发送器件传输完一个字节8位数据后,后面必须紧跟一个ACK/NACK校验位,判断接受是否完成,数据传送是否可以继续。接收方会让主机在发完的下一个时钟信号收到接收应答。

image-20230104204527296

image-20230217111807804

复合数据帧格式:先发再收。

image-20230217113542179

字节写:规定一个字节数据地址,写入和读取都去那里。

image-20230217113728972

随机读:

image-20230217192714230

③总线寻址方式

从机有自己的地址,总线寻址采用7或10位的寻址位数。如7位:位定义D0表示数据传送方向位(是主机从从机读取数据?还是主机向从机写数据?代表这个从机是接收器还是发送器),D7~D1是从机地址位。

主机发送一个地址后,所有从机前7位地址和主机比较,如果相同再判断从机是接收还是发送器。

从机地址包含固定位和可编程位,可编程位决定了这个部件可以最多有多少个接入总线。如4位固定位,3位可编程位,说明2^3=8个最多可以接入总线。

④数据传输

起始信号 S+7位从机地址+数据方向位+ACK+数据+NACK+终止信号。如果主机还想要新的数据传送,可以不终止,继续发出起始信号向另一从机寻址。

数据方向位=0:主机向从机发送数据。主机一直发到从机返回 NACK 为止。

数据方向位=1:从机向主机发送数据,发送到主机返回 NACK 为止。

AT24C02

是一个2k位串行CMOS,主板上的主板上的一块可读写的并行或串行FLASH芯片。该芯片有 I2C 接口,是个从机。而且有写保护功能,其中写入的数据断电不丢失。

我们可以通过单片机的模拟 IIO 功能,将数据写入该芯片永久存储,下次断电时也能访问。

创建多文件工程

下面编写的程序要求:

设计一个系统,可以写入、读取 AT24C02 中的数据,并将其中的数据用数码管显示出来。

这个系统涉及之前学过的按键、数码管信息,还涉及新的 AT24C02 的使用,三部分代码。由于内容太多,所以这次我们创建一个多文件系统,以后也会用这个系统模板更好的管理文件。

本项目主要包含:

App 文件夹:存储各类函数

Obj 文件夹:存放编译产生的 hex 文件、列表清单等。

Public 文件:存放公共文件,如延时、变量重定义。

User:存放 main.c 等主函数文件。

创建步骤:

①新建一个项目文件夹,在该项目文件夹中新建以上四个文件夹。

②在 Keil 中新建项目,选中这个项目文件夹。选择不复制 StartUp 代码。

③点击魔术棒右边的三色方块,创建三个分组 Group。这里创建的分组是一个逻辑上的分组,并不是指选中这三个具体的文件夹。与三个文件夹起名相同是为了方便管理.

image-20230105115404825

④编写程序.

清楚了我们的需求,我们想一下要写那些代码,放在什么地方。

公共内容:public 中。

使用按钮代码:App中。

使用数码管代码:App中。

使用 AT24C02 代码:App 中。

调用以上几部分内容代码:main 中。

image-20230105134459205

把普中部分代码复制到单片机中,大意:按键1将当前数据写入芯片,按键2读取芯片中存储的数据,按键3将当前数据+1,按键4将当前数据清零。

iic:定义i2c总线的一些方法。如开始等待接收数据、结束关闭、ack、nack、wait ack、读取写入数据等方法。

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
115
116
117
118
119
120
121
/////////iic.h
#ifndef _iic_H
#define _iic_H

#include "public.h"

//定义EEPROM控制脚
sbit IIC_SCL=P2^1;//SCL时钟线
sbit IIC_SDA=P2^0;//SDA数据线


//IIC所有操作函数
void iic_start(void); //发送IIC开始信号
void iic_stop(void); //发送IIC停止信号
void iic_write_byte(u8 txd); //IIC发送一个字节
u8 iic_read_byte(u8 ack); //IIC读取一个字节
u8 iic_wait_ack(void); //IIC等待ACK信号
void iic_ack(void); //IIC发送ACK信号
void iic_nack(void); //IIC不发送ACK信号

#endif


///////////iic.c
#include "public.h"
#include "iic.h"

void iic_start()
{
IIC_SCL = 1; // SCL为高电平时,SDA的数据才有效
IIC_SDA = 1;
delay(10);
IIC_SDA = 0; // SCL SDA 都由高变低,是起始标志

delay(10);
IIC_SCL = 0;
}

void iic_stop()
{
IIC_SCL = 1;
IIC_SDA = 0;
delay(10);
IIC_SDA = 1;
delay(10);
}

void iic_ack()
{
IIC_SCL = 0;
IIC_SDA = 0;
delay(10);
IIC_SCL = 1;
delay(10);
IIC_SCL = 0;
}

void iic_nack()
{
IIC_SCL = 0;
IIC_SDA = 1;
delay(10);
IIC_SCL = 1;
delay(10);
IIC_SCL = 0;
}

u8 iic_wait_ack()
{
u16 time_temp = 0;
IIC_SCL = 1;
while (IIC_SDA)
{ // 等待数据变成0
if (++time_temp > 100)
{
iic_stop();
return 1;
}
}
IIC_SCL = 0;
return 0;
}

u8 iic_read_byte(u8 ack)
{
u8 i = 0, receive = 0;
for (i; i < 8; i++)
{
IIC_SCL = 0;
delay(1);
IIC_SCL = 1;
receive <<= 1;
if (IIC_SDA)
receive += 1;
delay(1);
}
if (!ack)
iic_nack();
else
iic_ack();
return receive;
}

void iic_write_byte(u8 dat)
{
u8 i = 0;
IIC_SCL = 0;
for (i; i < 8; i++)
{
if ((dat & 0x80) > 0)
IIC_SDA = 1;
else
IIC_SDA = 0;
dat <<= 1;
delay(1);
IIC_SCL = 1;
delay(1);
IIC_SCL = 0;
delay(1);
}
}

24c02:主要就是芯片调用iic来写入或取出数据的函数。看上去倒没有多少自己的东西。

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
/////////24c02.h
#ifndef _24c02_H
#define _24c02_H

#include "public.h"
void at24c02_write_one_byte(u8 addr, u8 dat);
u8 at24c02_read_one_byte(u8 addr);
#endif

/////////24c02.c
#include "public.h"
#include "24c02.h"
#include "iic.h"

void at24c02_write_one_byte(u8 addr, u8 dat){
iic_start();
iic_write_byte(0xA0);//写命令
iic_wait_ack();
iic_write_byte(addr);//发送写地址
iic_wait_ack();
iic_write_byte(dat);
iic_wait_ack();
iic_stop();//停止条件
delay(1);
}

u8 at24c02_read_one_byte(u8 addr){
u8 temp;
iic_start();
iic_write_byte(0xA0);//写命令
iic_wait_ack();
iic_write_byte(addr);//发送写地址
iic_wait_ack();
iic_write_byte(0xA1);//进入接收模式
iic_wait_ack();
temp=iic_read_byte(0);//读取条件
iic_stop();//停止条件
return temp;
}

public.h和.c里面就是装了u8 u16 和 delay 函数的定义。不用多说。

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
#include <reg52.h>
#include <public.h>
#include <key.h>
#include <display.h>
#include <24c02.h>
#define EEPROM_ADDRESS 0//数据存入EEPROM 的起始地址
void main(){
u8 key_temp=0;
u8 save_value=0;//每次刚打开单片机时,起始值都=0
u8 save_buf[3];
while(1){
key_temp=key_scan(0);//单词扫描按键获取当前按键
switch(key_temp){//按键1:写入

case KEY1_PRESS:
at24c02_write_one_byte(EEPROM_ADDRESS,save_value);
break;
case KEY2_PRESS:
save_value=at24c02_read_one_byte(EEPROM_ADDRESS);
break;
case KEY3_PRESS:
if(save_value<255)save_value++;
break;
case KEY4_PRESS:
save_value=0;
break;
default:
break;
}
save_buf[0]=save_value/100;
save_buf[1]=save_value/10%10;
save_buf[2]=save_value%10;
smg_display(save_buf,5);
}
}

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
void main(){
unsigned char dat=0,KeyNum;
LCD_Init();
//LCD_ShowString(1,1,"Hello!");
//AT24C02_WriteByte(1,66);//256 位字节,存入数据不 >256
//delayMs(50);//两次写之间需要的时间间隔
//AT24C02_WriteByte(2,34);//256 位字节
//delayMs(50);
//dat=AT24C02_ReadByte(1);
//delayMs(50);
//dat1=AT24C02_ReadByte(2);
LCD_ShowNum(2,1,dat,3);
while(1){
KeyNum=key_scan(0);
switch(KeyNum){
case KEY1_PRESS:
if(dat<255)dat+=1;
LCD_ShowString(1,1,"++");
LCD_ShowNum(2,1,dat,3);
break;
case KEY2_PRESS:
if(dat>0)dat-=1;
LCD_ShowString(1,1,"--");
LCD_ShowNum(2,1,dat,3);
break;
case KEY3_PRESS:
AT24C02_WriteByte(1,dat);
LCD_ShowString(1,1,"Write Complete!");
delayMs(50);
LCD_ShowNum(2,1,dat,3);
break;
case KEY4_PRESS:
dat=AT24C02_ReadByte(1);
LCD_ShowString(1,1,"Read Complete!");
LCD_ShowNum(2,1,dat,3);
break;
}
}
}

运行效果:

一开始运行,显示当前数字000.

按下按钮1,数字++。

按下按钮2,数字–。

按下按钮3,当前数字会写入。

按下按钮4,会读取 AT24C02 中存储的数字显示出来。断电也能保存。

一个字节确实只能存到 255. 我们可以拿 12 两个存储地址合起来存储一个数据,一个存储高八位 /256,一个存储低八位 %256. 取数据的时候 datl|=dath<<8;

写入多位时,延时间隔应该>5ms!因为 at24c02 的写入还是很慢的。

温度传感器 DS18B20

单片机上自带的温度传感器模块:DS18B20。是一种常见的数字温度传感器。不用 ADC,自己处理好数据后放到 RAM 中,我们取出来就能用。

总线结构,可以把很多设备挂到总线上,省 IO 口;有温度报警装置;寄生供电(总线高电平可以供电).

寄生供电需要添加强上拉电阻。

传感器很简单,只有三个引脚:

image-20230322222620666

DQ 是 P37。

image-20230322232422937

具体结构如上图。寄生供电使得 DQ 也可以供电。

ROM 是寻址用,可以跳过。

SCRATCHPAD:暂存器,读取的数据在其中。

EEPROM:存储一些配置信息,比如报警阈值,精度。

image-20230322235814438

DS18B20 采用异步半双工的单总线,加上寄生供电,使得只要两根线就能驱动。不过这种结构并不常见。

我们将检测到的温度通过数码管显示出来。数码管的传入函数和IIC的一样,将外部dat数组传给数码管。

init

主机输出低电平拉低总线 480960 us,释放总线;外部上拉电阻把总线拉高,延时 1560 us 进入接收模式。接着 DS18B20 拉低总线 60~240 us。

我们要在主机释放总线后,检测一下一定时间内总线是否被拉低了,拉低了说明有从机。然后再检测一定时间内从机是否把总线又释放了。

发送一位数据

主机拉低总线 60~120 us 后释放:0.

主机拉低总线 1~15 us :1.

一般发送间隔 1ms。

从机拉低 30us 读取数据。

读取一位数据

主机拉低总线 15us,接近结束时释放,看一下总线是否释放。

如果释放了,说明从机在发送数据1,在 15us 内就释放了。

如果没释放,说明从机还在拉着总线,是打算拉 60us 以上的0.

至于发送读取是否会混淆,无需担心,因为总线发送给主机指令来决定其作用。

发送 接收字节

连续读或发1个字节 8次。低位在前。

整体流程

从机复位,主机检测从机是否响应;

ROM 指令+本指令是 R/W;

功能指令+本指令是 R/W。

image-20230324152054926

因为只有一个从机,所以这里用 skip rom 指令 跳过地址校验。

功能指令:

  • 转换温度:该指令一发出,立刻转换温度后放在暂存器中。
  • 写入寄存器:只写3个字节。TH TL configuration register。
  • 读寄存器:读出寄存器数据。我们知道温度值只是前2个字节,所以简单处理只读2个字节即可。
  • copy:寄存器指令写入 EEPROM。RECALL 相反。
  • read power supply:检测是否寄生供电,这里我们也不需要。

温度变换:init-跳过 ROM-转换温度。

温度读取:init-跳过 ROM-读取温度-持续读取。

温度>0:*0.625

温度<0:取反+1,再*0.625

具体代码解释在下文的注释中。

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#include "intrins.h"
sbit DS18B20_PORT=P3^7;

//初始化序列:首先拉低总线至少480us,以产生复位脉冲。接着主机释放总线,延时至少15us,进入接收模式
void ds18b20_reset(){
DS18B20_PORT=0;//拉低DQ
delay(75);//拉低750us
DS18B20_PORT=1;//DQ=1
delay(2);
}

u8 ds18b20_check(){
u8 time_temp=0;
while(DS18B20_PORT&&time_temp<20){
time_temp++;
delay(1);
}
if(time_temp>=20)return 1;//超时跳出,而不是因为DQ变低跳出
else time_temp=0;
while((!DS18B20_PORT)&&time_temp<20){
time_temp++;
delay(1);
}
if(time_temp>=20)return 1;
return 0;
}

u8 ds18b20_init(){
ds18b20_reset();
return ds18b20_check();
}

/*写时序:写1时序:拉低后延时2us,释放后延时60us.
写0时序:拉低后延时60us,释放后延时2us。*/
void ds18b20_write_byte(u8 dat){
u8 i=0;
u8 temp=0;
for(i;i<8;i++){
temp=dat&0x01;dat>>=1;
if(temp){
DS18B20_PORT=0;
_nop_();_nop_();
DS18B20_PORT=1;
delay(6);
}
else{
DS18B20_PORT=0;
delay(6);
DS18B20_PORT=1;
_nop_();_nop_();
}
}
}

//读时序:至少需要60us。时序起始后15us内读数据(采样总线)。
//典型的读时序过程为:主机输出低电平延时 2us,然后主机转入输入模式延时12us,然后读取单总线当前的电平,然后延时 50us。
u8 ds18b20_read_bit(){
u8 dat=0;
DS18B20_PORT=0;
_nop_();_nop_();
DS18B20_PORT=1;
_nop_();_nop_(); //该段时间不能过长,必须在15us内读取数据
if(DS18B20_PORT)dat=1; //如果总线上为1则数据dat为1,否则为0
else dat=0;
delay(5);
return dat;
}

u8 ds18b20_read_byte(){
u8 i=0;
u8 dat=0;
u8 temp=0;
for(i;i<8;i++){
//先低位后高位
temp=ds18b20_read_bit();
dat=(temp<<7)|(dat>>1);//读取到的先放到最高位,然后逐渐向低位移动
}
return dat;
}

void ds18b20_start(){
ds18b20_init();
ds18b20_write_byte(0xcc);//跳过ROM命令,此命令通过允许总线主机不提供64位ROM编码而访问存储器操作来节省时间。
ds18b20_write_byte(0x44);//转换命令
}
float ds18b20_read_temperature(){
u8 datl=0,dath=0;
u16 val=0;
float temp;
ds18b20_start();
ds18b20_init();
ds18b20_write_byte(0xcc);
ds18b20_write_byte(0xbe);//读存储器命令,读出温度
datl=ds18b20_read_byte();//低字节
dath=ds18b20_read_byte();//高字节
val=(dath<<8)+datl;
if((val&0xf800)==0xf800)//负温度
{
val=(~val)+1;//取反+1
temp=val*(-0.0625);//计算方法如此。*0.0625,如果是负数要先取反+1。
}
else temp=(0.0625*val);
return temp;
}

void main(){
int cnt=0;//每50次循环读取一次温度
int temp_value;
u8 temp_buf[5];
ds18b20_init();
while(1){
cnt++;
if(cnt%50==0){
cnt%=50;
temp_value=ds18b20_read_temperature()*10;
if(temp_value<0){
temp_value=-temp_value;
temp_buf[0]=0x40;//负号
}
else temp_buf[0]=0x00;//不显示负号
temp_buf[1]=gsmg_code[temp_value/1000];
temp_buf[2]=gsmg_code[temp_value%1000/100];
temp_buf[3]=gsmg_code[temp_value%100/10]|0x80; //加小数点
temp_buf[4]=gsmg_code[temp_value%10];
smg_display(temp_buf,3);
}
}
}

时钟

DS1302 时钟芯片。开发板上已经集成的芯片,本次实验目的是用数码管显示出当前时间,以hh-mm-ss的格式。

image-20230130130648915

VCC2:电源

VCC1:备用电源,即单片机断电后维持时钟继续运行的。STC89C52 是没有的,即断电就停止运行。

X1X2:晶振。

CE:使能。

IO:输入输出。

SCLK:时钟。

操作流程就是将数据写入 DS1302 的寄存器来设置当前时间格式,然后 DS1302 时钟运作后我们再将寄存器中数据读出。

DS1302 中存储顺序是秒分时日月周年,存储格式是 BCD 码。

image-20230110142624173

首先,CE/RST在整个读写过程中要保持是高电平,一次字节读写完毕后要返回低电平。

然后,控制指令字输入后的下一个 SCLK 上升沿数据写入,下降沿数据读出,在图中可以看得出来,都是从低位0先读,最后出高位7. 第一位代表读或写,后四位代表地址(年、月、日、时、分、秒有着不同的地址),R/C 代表存取 RAM 数据还是读取时钟数据。最高位只有=1才能启用时钟。后面八位是读或写的数据。

读时序注意:因此写命令的第八个上升沿结束后,紧接着第八个下降沿就开始读数据了。

写时序注意:先关闭写保护 WP,1是只读,0才可以写。

image-20230206234252279

我们先试着写入一个秒初值,然后读取时钟里的秒数值,应该是1s一加。

记得 BCD 码和十进制的转换。

image-20230206015234658

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
#include"REG52.H"
#include"LCD1602.h"
sbit DS1302_CLK=P3^6;//时钟管脚
sbit DS1302_CE=P3^5;//复位管脚
sbit DS1302_IO=P3^4;//数据管脚

typedef unsigned char u8;
void ds1302Init(){
DS1302_CE=0;
DS1302_CLK=0;
}

void ds1302WriteByte(u8 command,u8 dat)
{
u8 i;
DS1302_CE=1;

for(i=0;i<8;i++)
{
DS1302_IO=command&(0x01<<i);//从低到高写入数据
DS1302_CLK=1;
DS1302_CLK=0;
//需要查询最小执行时间。不过这里执行时间都大于最小时间了。
}
for(i=0;i<8;i++)
{
DS1302_IO=dat&(0x01<<i);//从低到高写入数据
DS1302_CLK=1;
DS1302_CLK=0;
//需要查询最小执行时间。不过这里执行时间都大于最小时间了。
}
DS1302_CE=0;
}

u8 ds1302ReadByte(u8 command)
{
u8 i;
u8 dat=0x00;//全局变量会有初值0,局部变量不会。data 是要写入的数据
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=command&(0x01<<i);//从低到高写入数据
DS1302_CLK=0;
DS1302_CLK=1;//这里为什么和 write 是相反的?因为我们注意到 read 是先上升沿读入8位,再切换为下降沿读入8位。、
//如果还是先1后0,读入第8位时不仅会把上升沿读掉,下降沿也会读掉,导致错过第九位。
//需要查询最小执行时间。不过这里执行时间都大于最小时间了。
}
for(i=0;i<8;i++)
{

DS1302_CLK=1;
DS1302_CLK=0;
//需要查询最小执行时间。不过这里执行时间都大于最小时间了。
if(DS1302_IO)dat|=(0x01<<i);//从低到高写入数据
}
DS1302_CE=0;
DS1302_IO=0;//读取前要归0,因为内部是以 BCD 码格式存储。
return dat;
}

void main(){
u8 second;
LCD_Init();
ds1302Init();
//ds1302WriteByte(0x8E,0x00);//如果读出数据>59,可能是处于 wp 写保护,需要通过这句关闭
ds1302WriteByte(0x80,0x01);//写入55s,就是0x55,与内部BCD码对应
LCD_ShowString(1,1,"DS1302");
second=ds1302ReadByte(0x81);
while(1){LCD_ShowNum(2,1,second/16*10+second%16,3);}
}

写入其他变量时分秒都一样。就是更换不同的地址。

不过全部定义变量明显太麻烦。可以先 define 定义了所有地址,再定义一个数组存所有值。

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
u8 ds1302_address[7]={0x80, 0x82, 0x84, 0x86, 0x88, 0x8c};// second, minute, hour, date, month, year
u8 ds1302_time[7]={0x55, 0x59, 0x11, 0x07, 0x02, 0x23};

void main(){
u8 i;
u8 time_temp[6];
u8 temp;
LCD_Init();
ds1302Init();
//ds1302WriteByte(0x8E,0x00);//如果读出数据>59,可能是处于 wp 写保护,需要通过这句关闭
ds1302WriteByte(0x80,0x01);
ds1302WriteByte(0x82,0x37);
for(i=0;i<6;i++){
ds1302WriteByte(ds1302_address[i], ds1302_time[i]);
}
LCD_ShowString(1,1," - - ");
LCD_ShowString(2,1," : : ");
while(1){
for(i=0;i<6;i++){
temp=ds1302ReadByte(ds1302_address[i]+1);
time_temp[i]=temp/16*10+temp%16;
}
for(i=0;i<6;i++){
LCD_ShowNum(1+i/3,1+3*(i%3), time_temp[5-i],2);
}
//LCD_ShowNum(2,1,minute/16*10+minute%16,2);
//LCD_ShowNum(2,3,second/16*10+second%16,2);
}
}

然后我们让按键可以修改时钟值,这样用户可以手动更改时钟。

按键1切换切换要调整的位,按键2切换回第0位,按键3+1,按键4-1.

难点主要在于+ -的溢出情况。这里我们不考虑秒+1进位对分钟或小时等影响,大多数闹钟也是这样设计的。59+1就归零,0-1就变59.

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
if(key){
switch(key){
case KEY1_PRESS: time_set_select=0;break;
case KEY2_PRESS: time_set_select=(time_set_select+1)%6;break;
case KEY3_PRESS:
if(time_set_select<=1&&ds1302_time[time_set_select]==59){//second, minute 59-00
ds1302_time[time_set_select]=0;
}
else if(time_set_select==2&&ds1302_time[time_set_select]==23)ds1302_time[time_set_select]=0;//hour 23-00
else if(time_set_select==3&&ds1302_time[time_set_select]==maxDate(ds1302_time[3],ds1302_time[4],ds1302_time[5]))ds1302_time[time_set_select]=1;//date 超过最大日期 -01
else if(time_set_select==4&&ds1302_time[time_set_select]==12)ds1302_time[time_set_select]=1;//month 12-00
else if(time_set_select==5&&ds1302_time[time_set_select]==99)ds1302_time[time_set_select]=0;//year 99-00
else ds1302_time[time_set_select]++;break;
case KEY4_PRESS:
if((ds1302_time[time_set_select]&&time_set_select!=3&&time_set_select!=4)
||ds1302_time[time_set_select]>1)ds1302_time[time_set_select]--;//date month!=0
else if(time_set_select<=1)ds1302_time[time_set_select]=59;
else if(time_set_select==2)ds1302_time[time_set_select]=23;
else if(time_set_select==3)ds1302_time[time_set_select]=maxDate(ds1302_time[3],ds1302_time[4],ds1302_time[5]);
else if(time_set_select==4)ds1302_time[time_set_select]=12;
else if(time_set_select==5)ds1302_time[time_set_select]=99;

break;
}
if(key>=3)timeSet();

计算日期最大值函数:

1
2
3
4
5
6
7
u8 maxDate(u8 date, u8 month,u8 year){
if(month==1||month==3||month==5||month==7||month==8||month==10||month==12)return 31;
else if((month==4||month==6||month==9||month==11)&&date==31)return 30;
else if(year%4)return 28;
else if(year%4==0&&year%100)return 28;
else return 29;
}

有个弊端就是不知道现在在操作哪一位(只能自己记住)。可以利用定时器的定时闪烁,让在被操作的位不停闪烁。(重复写入空格或数值。)

还有就是按下按键,时钟会停走,不松开就一直停着。可以改按键触发条件为按下或弹起的上升沿或下降沿,避免按键处理函数死循环。

红外传感器

遥控器通过红外 LED 发送调制后的信号,开发板上的红外接收模块接收遥控器的红外线。

单工异步,940nm 波长(还有一种 250nm 的N,可见光),EC 通信标准。

image-20230328000312504

38KHz:红外线频率。

IN:发送的方波。

image-20230328001955152

红外接收模块中会自动帮我们滤出 In 部分。

image-20230328002724563

空闲状态:OUT 输出高电平。

发送高电平:OUT 输出高电平。

发送低电平:OUT输出低电平,代表有数据。

image-20230328004940943

38kHz 属于是底层信息,所以协议层不给予展示,类似类的封装。

9ms 低+4.5ms 高:start。

后面跟四个字节数据。反码用于数据验证。

560us 低+560us 高表示0,560us 低+1690us 高表示1. 结束的最后一个高电平后面要跟一个下降沿表示终止。

一直扫描是效率很低的做法,所以 out 是接在外部中断上的。

外部中断

image-20230328115258479

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main(){
LCD_Init();
LCD_ShowString(1,1,"A");
IT0=1;
IE0=0;
EX0=1;
EA=1;
PX0=1;
while(1){
LCD_ShowNum(2,1,num,2);
}
}

void Int0_Routine() interrupt 0 {
num++;
}

中断和第三个按键 P3^2 接在一起,因此按下按键3时就会下降沿触发中断。

如果改为低电平触发,即 IT0=0,按下按键就一直触发。

接收到的数据会以2位的二进制位展现在数码管上,这里遥控器上的按钮并不是按几数码管就会显示几的注意一下。重点只是在于看遥控器不同的信号单片机能否区分和识别。

image-20230328145734093

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
//ired.h
#ifndef _ired_H
#define _ired_H

#include "public.h"

//管脚定义
sbit IRED=P3^2;

//声明变量
extern u8 gired_data[4];


//函数声明
void ired_init(void);

#endif

//ired.c
#include "ired.h"

u8 gired_data[4];//存储4个字节接收码(地址码+地址反码+控制码+控制反码)

/*******************************************************************************
* 函 数 名 : ired_init
* 函数功能 : 红外端口初始化函数,外部中断0配置
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void ired_init(void)
{
IT0=1; //下降沿触发
EX0=1; //打开中断0允许
EA=1; //打开总中断
IRED=1; //初始化端口
}

void ired() interrupt 0 //外部中断0服务函数
{
u8 ired_high_time=0;
u16 time_cnt=0;
u8 i=0,j=0;
//引导信号有9ms的低电平和4.5ms的高电平,先把这两部分读掉,并且如果太长时间引导信号没有发生相应的变化就先跳出,省的系统死机。我们给引导信号10ms和5ms的机会。
if(IRED==0)
{
time_cnt=1000;
while((!IRED)&&(time_cnt))//等待引导信号9ms低电平结束,若超过10ms强制退出
{
delay_10us(1);//延时约10us
time_cnt--;
if(time_cnt==0)return;
}
if(IRED)//引导信号9ms低电平已过,进入4.5ms高电平
{
time_cnt=500;
while(IRED&&time_cnt)//等待引导信号4.5ms高电平结束,若超过5ms强制退出
{
delay_10us(1);
time_cnt--;
if(time_cnt==0)return;
}
//接下来是读取地址、地址反码、控制、控制反码。
for(i=0;i<4;i++)//循环4次,读取4个字节数据
{
for(j=0;j<8;j++)//循环8次读取每位数据即一个字节
{
time_cnt=600;
while((IRED==0)&&time_cnt)//等待数据1或0前面的0.56ms结束,若超过6ms强制退出
{
delay_10us(1);
time_cnt--;
if(time_cnt==0)return;
}
time_cnt=20;
while(IRED)//等待数据1或0后面的高电平结束,若超过2ms强制退出
{
delay_10us(10);//约0.1ms
ired_high_time++;
if(ired_high_time>20)return;
}
gired_data[i]>>=1;//先读取的为低位,然后是高位
if(ired_high_time>=8)//如果高电平时间大于0.8ms,数据则为1,否则为0
gired_data[i]|=0x80;//最高位赋1
ired_high_time=0;//重新清零,等待下一次计算时间
}
}
}
if(gired_data[2]!=~gired_data[3])//校验控制码与反码,错误则清空后返回
{
for(i=0;i<4;i++)
gired_data[i]=0;
return;
}
}
}

main.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main()
{
u8 ired_buf[3];

ired_init();//红外初始化

while(1)
{
ired_buf[0]=gsmg_code[gired_data[2]/16];//将控制码高4位转换为数码管段码
ired_buf[1]=gsmg_code[gired_data[2]%16];//将控制码低4位转换为数码管段码
ired_buf[2]=0X76;//显示H的段码
smg_display(ired_buf,6);
}
}

LCD1602

liquid crystal display 液晶显示屏,一种字符型液晶显示模块,可以显示 16*2 个字符,每个字符是 5*7 点阵。

image-20230203004049189

P0 P2 会和数码管、LED 一定程度上冲突。

  1. 地。

  2. Vcc。

  3. 调对比度的。

  4. RS:数据指令端。1代表 DB 是数据,0代表是指令。

  5. RW:1读0写。

  6. E:类似时钟的使能。高电平有效,下降沿执行。

  7. DB:并行输入。一个字节长。

  8. BG:背光灯电源。

内部结构图

image-20230221024102411

类似 SMG,想显示1并不是直接输入1,而是操纵数码管右侧一竖被点亮。字模库起的就是这个作用,里面有固定的 ROM 和用户课自定义的 RAM。

DDRAM 长于屏幕,可以通过移平实现滚动效果。

AC address controller,可以自动移位写入数据。

字模库中的数据大多数和 ASCII 码是一样的。

指令

image-20230221195404983

image-20230221195434495

image-20230221195450094

image-20230221200656434

image-20230221200839017

image-20230221201026593

image-20230221201247867

image-20230221201355749

image-20230222162315672

image-20230221205604331

初始化指令

初始化要做哪些操作?

规定显示区域(如8位数据接口,2行显示,5*7点阵,即为0011 10xx,如果取0即为 0x38.)。

显示的模式设置(如开启显示,关闭光标,关闭光标闪烁:0000 1101,即 0x0D)。

进入模式设置(如读写后光标++,屏幕不动:0000 0110,0x06。如果是滚动屏幕则为)。

清屏(0x01)。

显示指令

先设置 DDRAM 初始地址,0x80|AC(开头的8是 DDRAM 固定指令信息不能改。后面的全是0,与 AC 光标位置做与,AC 不同位置的值见 DDRAM 地址表).

然后发送数据。

时序

image-20230221210035721 image-20230221210102393 image-20230221210614193

模块化

第一阶段我们先编写 LCD1602 模块化编程代码,这一部分主要显示静态内容,用于程序编写过程中显示变量进行调试。

目标模块化函数:

image-20230203004415880
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//LCD1602.h
#ifndef __LCD1602_H__
#define __LCD1602_H__

#include "reg52.h"
#include "intrins.h"
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_E=P2^7;
#define LCD_DATAPORT P0

void LCD_Delay1ms(); //@11.0592MHz
void LCD_WriteCommand(unsigned char Command);
void LCD_WriteData(unsigned char Data);
void LCD_Init();
void LCD_SetCursor(unsigned char line, unsigned char column);
unsigned int LCD_Pow(unsigned char x, unsigned char y);
void LCD_ShowChar(unsigned char line, unsigned char column, unsigned char c);
void LCD_ShowString(unsigned char line, unsigned char column, unsigned char str[]);
void LCD_ShowNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length);
void LCD_ShowSignedNum(unsigned char line, unsigned char column, int num, unsigned char length);
void LCD_ShowHexNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length);
void LCD_ShowBinNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length);
#endif
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
115
116
117
118
119
120
121
122
123
124
//LCD1602.c
#include "LCD1602.H"

void LCD_Delay1ms() //@11.0592MHz
{
unsigned char i, j;

_nop_();
_nop_();
_nop_();
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}

void LCD_WriteCommand(unsigned char Command){
LCD_RS=0;
LCD_RW=0;
LCD_E=0;
LCD_DATAPORT=Command;
LCD_Delay1ms();
LCD_E=1;
LCD_Delay1ms();
LCD_E=0;
}

void LCD_WriteData(unsigned char Data){
LCD_RS=1;
LCD_RW=0;
LCD_E=0;
LCD_DATAPORT=Data;
LCD_Delay1ms();
LCD_E=1;
LCD_Delay1ms();
LCD_E=0;
}

void LCD_Init(){
LCD_WriteCommand(0x38);
LCD_WriteCommand(0x0C);
LCD_WriteCommand(0x06);
LCD_WriteCommand(0x01);
}

void LCD_SetCursor(unsigned char line, unsigned char column){
if(line==1){
LCD_WriteCommand(0x80|(column-1));
}
else{
LCD_WriteCommand(0x80|(column-1)+0x40);
}
}

unsigned int LCD_Pow(unsigned char x, unsigned char y){
unsigned char i=y;
unsigned int res=1;
for(;i>0;i--)res*=x;
return res;
}

void LCD_ShowChar(unsigned char line, unsigned char column, unsigned char c){
LCD_SetCursor(line, column);
LCD_WriteData(c);
}

void LCD_ShowString(unsigned char line, unsigned char column, unsigned char str[]){
unsigned int i=0;
LCD_SetCursor(line, column);
while(str[i]!='\0'){
LCD_WriteData(str[i]);
i++;
}
}

void LCD_ShowNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length){
unsigned char i;
unsigned base;
LCD_SetCursor(line, column);
for(i=length;i>0;i--){
base=LCD_Pow(10, i-1);
LCD_WriteData(num/base%10+'0');
}
}

void LCD_ShowBinNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length){
unsigned char i;
unsigned char base;
unsigned char single_num;
LCD_SetCursor(line, column);
for(i=length;i>0;i--){
base=LCD_Pow(2, i-1);
single_num=num/base%2+'0';
LCD_WriteData(single_num);
}
}

void LCD_ShowHexNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length){
unsigned char i;
unsigned char base;
unsigned char single_num;
LCD_SetCursor(line, column);
for(i=length;i>0;i--){
base=LCD_Pow(16, i-1);
single_num=num/base%16;
if(single_num<=9)single_num+='0';
else single_num+='A'-10;
LCD_WriteData(single_num);
}
}

void LCD_ShowSignedNum(unsigned char line, unsigned char column, int num, unsigned char length){
unsigned char i;
unsigned base;
LCD_SetCursor(line, column);
if(num>0)LCD_WriteData('+');
else {LCD_WriteData('-');num=-num;}
for(i=length;i>0;i--){
base=LCD_Pow(10, i-1);
LCD_WriteData(num/base%10+'0');
}
}

如果希望屏幕滚动,一定时间执行一次LCD_WriteCommand(0x18); 屏幕左移指令。

直流电机

电能转机械能。

image-20230327001703813

第二种驱动方式可以双向,电机可以双向驱动,转向不同。

直接驱动电机还起到电感的作用,断开电源后电机产生电压,可以形成一个回路慢慢消耗掉。

PWM:脉冲信号调制。

image-20230327003516983

比如调 LED 灯,我们可以加一个有电位器的电阻(滑动变阻器)。

电机可能这种方式有局限性,比如电阻太大直接不转,驱动不起来,太小烧毁。

脉冲调制比如:“转2s”“停1s”“转2s”“停1s”……因为电机有惯性,所以可行。

示例:LED 流水灯:

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
sbit LED=P2^0;

void delay(unsigned char i) //@11.0592MHz
{
while (--i);
}

void main(){
unsigned char period=100;
unsigned char ti;
unsigned char i;
while(1){
for(ti=0;ti<100;ti++){
for(i=0;i<20;i++){
LED=0;
delay(ti);
LED=1;
delay(period-ti);
}
}
for(ti=100;ti>0;ti--){
for(i=0;i<20;i++){
LED=0;
delay(ti);
LED=1;
delay(period-ti);
}
}
}
}

可以利用 timer 来计数,我们知道 timer 是一直增加的,我们可以设置一个比较值,当 timer 大于比较值时输出1,小于时输出0类似这样。

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
#include "regx52.h"
#include "delay.h"
#include "nixie.h"
#include "timer0.h"
#include "key.h"

sbit LED=P1^0 ;

unsigned char compare;
unsigned int T0Count;

void main(){
unsigned char key;
unsigned char level=1;
time0Init();
compare=5;
while(1){
key=key_scan(0);
if(key==KEY1_PRESS)level++;
else if(key==KEY2_PRESS)if(level>1)level--;
level%=5;
if(level==0)level++;
if(level==1)compare=0;
else if(level==2)compare=50;
else if(level==3)compare=75;
else if(level==4)compare=100;
Nixie_Scan(1,level);
}
}

void Timer0_Routine() interrupt 1
{
TL0 = 0xAE;
TH0 = 0xFB;
T0Count++;
if(T0Count>=100)
{
T0Count=0;
}
if(T0Count<compare){
LED=1;
}
else if(T0Count>compare)LED=0;
}

ADC

使得调节开发板上的电位器时,数码管上能够显示 AD 模块 采集电位器的电压值且随之变化。

开发板上有三个应用:光敏电阻,热敏电阻,电位器。

一般 AD 转换有多个输入,提高使用效率。

image-20230327170005984

ADC 通过地址锁存与译码判断采用哪个输入。

image-20230327172256586

运算放大器,可以作为电压比较器、同相反相放大器、电压跟随器

T 型电阻网络 DA 转换器:

image-20230327195138979

低通滤波器:输入是有直交流两个分量的,可以通过低通滤波器提取出直流。电压跟随器让驱动能力增加。

image-20230327195154405

da 简单些,因为d值是固定的,根据d调整a即可。

ad 怎么判断电压大小?我们用一个电压值和给定电压作比较,看大于还是小于,逐渐逼近来找近似值。

分辨率:精细程度。比如8位的 ad 可以把 5v 转换到 0~255 范围。

转换速度:最大采样/建立频率。

XPT2046 采用 SPI,的时序在上升沿输入,下降沿输出,可实现输入再输出。

image-20230327213950578

采用单端模式(触摸屏查分会更好一些)。

PD1=1 采用内部参考电压,内部电压为 2.5v,我们知道adc 映射范围是 0~5v,所以1采用内部电压不如0采用5v的外部电压。

A 地址配置:

image-20230327220710512

VBAT:电池电压。

AUX:辅助电压。

XP YP:XY 正极。

读取指令并 ad 转化:

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
#define XPT2046_XP_8 0x9C    // 1001 1100
#define XPT2046_YP_8 0xDC // 1101 1100
#define XPT2046_VBAT_8 0xAC // 1010 1100
#define XPT2046_AUX_8 0xEC // 1110 1100
#define XPT2046_XP_12 0x94 // 1001 0100
#define XPT2046_YP_12 0xD4 // 1101 0100
#define XPT2046_VBAT_12 0xA4 // 1010 0100
#define XPT2046_AUX_12 0xE4 // 1110 0100

unsigned int XPT2046_ReadAD(unsigned char command)
{
unsigned char i;
unsigned char temp=command;
unsigned int ADValue = 0;
XPT2046_DCLK = 0;
XPT2046_CS = 0;
for (i = 0; i < 8; i++)
{
XPT2046_DIN = temp >> 7;
temp <<= 1;
XPT2046_DCLK = 1;
XPT2046_DCLK = 0;
}

for (i = 0; i < 12; i++)
{
XPT2046_DCLK = 1;
XPT2046_DCLK = 0;
if (XPT2046_DOUT)
ADValue |= (0x0800 >> i);
}
XPT2046_CS = 1;
//for 12 bit:
if(command&0x08)return ADValue>>4;
else return ADValue;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main()
{
unsigned int ADValue = 0;
LCD_Init();
LCD_ShowString(1, 1, "ADC NTC RG");
while (1)
{
ADValue = XPT2046_ReadAD(XPT2046_XP_8);//电位器
LCD_ShowNum(2, 1, ADValue, 3);
ADValue = XPT2046_ReadAD(XPT2046_YP_8);//热敏电阻
LCD_ShowNum(2, 5, ADValue, 3);
ADValue = XPT2046_ReadAD(XPT2046_VBAT_8);//光敏电阻
LCD_ShowNum(2, 9, ADValue, 3);
delayMs(10);
}
}

dac 也是采用 pwm 的原理,改变01 叫错频率来模拟灯的亮度(输出口:P2^1)。所以 dac 用处不广泛,因为可以被代替。

完结:库函数封装说明

从12月开始决定入嵌入式开始,经历了5个月的学习,终于把入门模块基本弄懂并且跟着代码敲了一遍。

在这期间跟随普中课程老师学习到的封装习惯让我主键开始留意每做完一个模块后就进行调试并把该模块封装起来,供下次使用该模块时可以快捷导入。

后来浅了解了一些 git 的项目管理后,我又把封装好的函数发布到 github repository 上。

单片机版本:STC89C52RC

github 仓库地址:Jingqing3948/MySTC89C52RCFunction: 学习 STC89C52RC 单片机时的一些封装好的库函数。 (github.com)

函数包含:

image-20230328184524152

AT24C02:掉电不丢失存储器。

DS18B20:温度传感器。

delay:stc-isp 生成的基于 11.0592MHz 晶振的延时函数。

IIC:IIC 总线的使用,配合 AT24C02 模块使用。

IR:红外遥控模块,包括外部中断、定时器等内容。

Key:四个独立按键。

LCD1602:LCD1602 显示屏。

NiXie:数码管。

Timer:定时器中断函数。

public.h:无用,定义了 u8 u16 两个变量。

有问题欢迎随时与博主沟通。如侵犯他人权益会尽快删除!

王道操作系统网课笔记

[TOC]

介绍

操作系统是什么?

计算机结构大概分为四层:

  • 用户
  • 应用程序
  • 操作系统
  • 硬件

操作系统是一类系统软件,调度硬件资源,合理分配管理软件(因此操作系统又被称作资源管理器(resource manager))。

image-20221026211255236

程序要运行首先要被放到内存中,然后才能被 CPU 处理;运行中的程序叫进程。

双击打开 QQ.exe,对应进程就会被放到内存中;

QQ 正常运行过程中,对应进程被 CPU 处理。

QQ 若想调用摄像头等,操作系统会把相应硬件分配给他。

计算机还会提供用户和硬件之间的接口。主要分为三种:GUI 接口,命令接口,程序接口。

联机/交互式命令接口:用户说一句,系统做一句。(cmd)

脱机/批处理命令接口:用户说一堆,系统做一堆。(.bat)

程序接口:通过程序才能调用。(.dll)

只有硬件的计算机叫裸机;操作系统将硬件资源转换为通用的、强大的虚拟形式,有时操作系统也被称为虚拟机。操作系统提供几百个系统调用(system call)供其他应用程序使用,实现运行程序、访问内存和设备等操作,也可以说操作系统为其他应用程序提供了一个标准库(standard library)。

操作系统几大特征

操作系统围绕以下几大主题展开:

  • 虚拟化(virtualize):尽管一般只有一个 CPU,但是能同时进行多个进程,造成多个 CPU 的假象。多个程序实例同时用到一片内存地址时,却能各运行各的,值互不干扰。实际上每个进程是在访问自己的私有虚拟内存空间(virtual address space),虚拟内存通过一定的规则映射到物理内存上,运行中的程序的物理内存是完全独立的。
  • 并发(concurrency)不是并行!并行是同时发生,并发是交替发生。单核计算机就会采用并发的程序运行方式。现在尽管都有四核计算机,可以进行4个程序的并行操作,但是并发仍然很重要。
  • 共享:系统中的某些资源供多个进程使用。

​ 互斥型共享就是一次只能一个程序用,如摄像头;

​ 同时型共享就是两个进程交替使用,如 QQ 微信 同时发送文件。

  • 异步性:并发执行的程序有时候会卡住。比如 AB 程序都要用同一个地址,A先用了,B用的时候就要等A用完释放才能用。

操作系统历史

手工操作阶段:用户手工打点,给机器。人机协调不均衡,资源分配不均匀。

批处理阶段——单道批处理系统:用户打好的点交给磁带,磁带读入计算机速度快得多(监督程序,早期的操作系统)。但是利用率仍然很低。

批处理阶段——多道批处理系统:每次内存中同时读入多个程序。多个程序并发执行,有了”中断“的概念。但是用户在程序执行的时候没法干涉,人机交互很差。

image-20221202180444486

分时操作系统:计算机以时间片为单位轮流给所有用户提供服务。用户的请求可以被及时响应,解决了人机交互问题;各个用户之间也感受不到其他用户的存在。但是众生平等,没有优先级。

实时操作系统:优先度高的任务可以先被处理,并且必须在给定的时限内完成任务。

image-20221202181100161

OS 运行机制和体系结构

计算机中的指令有的安全(加减乘除运算),有的危险一点(清空内存)。因此需要通过权限控制限制用户能执行的指令。

具体实现方法为:CPU 处于核心态(管态)时可以执行所有指令;处于用户态(目态)时只能执行非特权指令。

内核程序是系统的管理者,可以执行所有指令,运行在核心态;

应用程序只能执行非特权指令,运行在用户态。

image-20221202214904545

操作系统的内核包含橙黄两部分:大内核。

只包含黄色部分:微内核。

各自的缺点:大内核组织结构混乱,难以维护;微内核频发切换,性能低。

中断

一开始的计算机只是简单的串行执行程序。

现在的操作系统不仅可以并发执行程序,而且收到中断指令时,CPU 会切换到内核模式,中断当前程序的执行,按中断指令调整程序执行顺序,然后恢复到用户态继续执行。

中断分内中断、外中断。区别在于中断指令来自于 CPU 内部还是外部。

image-20221202224925813

系统调用

我们知道计算机硬件为了供用户使用,向上层提供了一些接口。用户直接使用的接口叫命令接口;用户通过应用程序间接使用的接口叫程序接口。系统调用是操作系统提供给应用程序的接口。

系统调用可以增加安全性,不让用户可以直接随意访问所有功能。如两个人去打印店用打印机,第一个人打到一半第二个人发送了他的打印任务请求,可打印机最终还是有序地把两个人的任务分别打印好了。如果用户能直接让打印机打印自己的任务,不加协调,无法实现这样的结果。

哪些操作要通过系统调用的方式进行?凡是和资源相关的。这样可以保证系统的安全性和稳定性。

image-20221203005839292

编程语言提供的一些库函数也是从下往上提供的一些封装的功能。但库函数不一定是系统调用。如求绝对值的库函数,这个库函数就不是系统调用,用户直接就能访问。

现在大多数系统调用都是高级语言中封装的部分库函数。

image-20221203012249371

陷入指令核心态不能执行,可以理解为:核心态只能执行系统调用,不能发起系统调用给自己。

进程

进程(process)是操作系统中最基本的抽象。

进程就是运行中的程序,程序本身只是存放在磁盘上的一些静态指令,是操作系统让其运行起来。

现在我们的计算机可以同时运行上百个进程,是通过虚拟化 CPU 而实现。每个进程只运行一个时间片段,然后跳转到其他进程,造成多个进程同时运行的假象。

内存中存放每个进程的程序段和数据段,但内存怎么知道哪个是哪个进程的?通过一种数据结构叫进程控制块(PCB)找到对应进程的额程序段和数据段。程序段、数据段、PCB 组成了进程实体。

image-20221203150903451

image-20221203151228535

操作系统需要一些低级机制(mechanism)切换程序运行,如上下文切换(context switch,停止当前程序,并运行另一个程序);还需要一些智能决定要切换到哪个程序,如策略(poliicy,根据一些算法判断要运行哪个程序,如“哪个程序在上一分钟运行的时间更长?”)

进程的几种状态

image-20221203151433504

image-20221203160553246

就绪态就是除了处理机其他资源都准备就绪了。阻塞态还需要准备资源才能进入就绪态。

image-20221203160853843

进程控制

就是这几种进程状态之间的切换。

通过两个指针实现:就绪队列指针和阻塞队列指针,用于存放就绪和阻塞的进程(阻塞队列可能还有好几个,按阻塞原因分组)。image-20221203174739750

状态切换使用原语,因为原语执行过程中不会受到中断的干扰。

image-20221203174850603

原语做的操作无非是:1. 修改 PCB;2. 把 PCB 放到对应队列中;3. 分配/释放资源。

image-20221203220754958

进程通信

进程之间互相通信,安全起见不能直接进行。

  1. 共享存储,共用一块存储空间。有基于数据结构的分享(给定数据结构存储方式)和基于存储区的分享(内存中划定一块存储区,进程自己决定怎么存储)。

    共享的缓冲区叫做管道,如果只采用一个管道只能使用半双工型,全双工型需要两根管道。两个进程访问该管道要互斥的访问。

    管道写满才能读,读空了才能写。

    读出来的数据就直接被丢弃了。所以安全起见只能有一个读的进程。

  2. 消息传递。类似计网的数据报,消息封装好之后发给另一个进程的消息队列。

线程

让一个进程可以并发执行多个任务。比如 qq 聊天的同时可以发文件收消息发消息。

一个进程包含多个线程。线程是调度, CPU 的程序执行单元,进程是资源分配的单元,比如把显示器资源分配给 QQ。

image-20221204161307495

线程有两种实现方式:

  1. 用户级线程 (ULT),进程的切换由应用程序实现,而不需要操作系统管理,因此用户态下就能实现线程的切换,且线程的存在对用户透明,对操作系统不透明。
  2. 内核级线程(KLT),线程管理靠操作系统在核心态下实现、
  3. 两者组合的形式,n 个用户级线程映射到 m 个内核级线程上。

操作系统分配 CPU 处理机只能分配给内核级线程(因为用户级线程对操作系统来说不透明)。所以如果有三个用户级线程,但这个应用程序只有两个内核级线程,最多也只能被分配到两个处理机,最多也只能有两个用户线程并发执行(哪怕这个操作系统是三核的,四核的,有很多核空闲出来)。

几个用户级进程映射到几个内核级进程上?这就是多线程模型问题。

  1. 多个用户级进程映射到一个内核级进程上。

    image-20221204162042367

    线程切换不用在核心态下进行,切换效率高,但是并发度不高。

  2. 一对一。

    image-20221204162149252

    优缺点正好和1相反。

  3. n 对 m,用户级线程多于内核级线程,较为折中。

    image-20221204162233095

进程调度和切换

线程数往往多于处理机数,因此要考虑按怎样的算法分配处理机。

进程调度和切换的区别是什么?

进程调度先选再切换。

进程切换包括:

  1. 保存原来运行的数据
  2. 恢复新进程的数据。

切换会影响效率。

调度层次1:高级调度(→就绪态)

首先先不说处理机够不够处理内存中的线程,有时候线程多到内存中放不下。高级调度需要按一定的原则从外存中挑选一些作业放到内存中并建立进程(PCB),让他们有进一步竞争处理机的机会。主要解决的是调入问题。

调度层次2:中级调度(挂起态→就绪态)

引入虚拟存储技术之后,暂时不能运行的进程可以先调至外存等待(挂起)。等可以运行再拿回来,这样能提高内存利用率和系统吞吐量。

其对应的 PCB 并不会一起移出内存,而是存储了被挂起的进程的信息,被放到内存中的挂起队列里。

中级调度就是挑选挂起的进程调入内存。

引入挂起的进程实际上可以说是有七种状态。不能运行的进程都会先放到就绪挂起态或阻塞挂起态,能运行再回到内存(有的操作系统阻塞挂起态直接回到就绪挂起态)。注意挂起和阻塞的区别!

image-20221204164038790

调度层次3:初级调度(就绪态→运行态)

就是从就绪队列中按一定算法挑一个进程来执行。

调度时机

当前运行的进程主动(运行完了,或者异常终止)或被动放弃处理机,就会发生调度。

有以下几种情况不能调度:

  1. 进程在处理中断时。
  2. 进程在操作系统内核程序临界区中。(临界资源及临界区(内核/普通)以及三种进程不能切换的情况_Unstoppable~~~的博客-CSDN博客_内核临界区 在此感谢这位博主!调度区和调度资源是两回事,调度区又分普通调度区和内核调度区。)
  3. 原语执行时.

调度方式

非剥夺/非抢占调度方式:只允许进程主动放弃处理机。哪怕有更紧急的进程到达,也不会把当前正在使用处理机的进程挤开。开销小,但没法处理紧急情况。

剥夺/抢占调度方式:允许进程主动或被动放弃处理机。

调度算法评价指标

CPU 利用率:CPU 有活干的时间/总时间。

系统吞吐量:完成了的作业道数/总用时。

周转时间:提交作业到作业完成用时。包括在外存等待高级调度→在内存就绪队列等待低级调度→在 CPU 上执行→等待 I/O 操作完成的时间。

注意概念问题,进程是运行中的程序,所以作业在外存的时候不可以被称为进程。进入内存才创建了进程。

带权周转时间:周转时间/实际运行时间。越小越好(排队用时少吧)。

等待时间:作业等待处理机状态的时间之和。(不包括 IO)

响应时间:用户提交请求到首次产生响应的时间。

调度算法

早期批处理调度算法:只根据等待时间和预估处理时间调度,不考虑是否紧急。

算法名 思想 规则 用于何种调度 是否可抢占 优缺点 是否会导致饥饿(某个作业长期得不到服务)
先来先服务 FCFS 公平 先来后到 都用 非抢占式 公平;但排在后面的短作业体验差 不会
短作业优先 SJF 让平均等待、周转、带权周转时间最短 最短的作业优先服务 都用 非抢占式(最短剩余优先算法是抢占式的,当有进程入队的时候立刻调度。平均时间抢占式的更少) 平均时间少,但对长作业不公平,可能“饥饿
高响应比优先 HRRN 综合考虑等待时间和处理时间 $\frac{等待时间+服务时间}{服务时间}$,优先执行响应比大的 都用 非抢占式(当前进程主动结束时才进行调度) 综合考虑了等待时间和服务时间,长作业等久了也会执行 不会

后期交互式系统算法:

算法名 思想 规则 用于何种调度 是否可抢占 优缺点 是否会导致饥饿
时间片轮转 RR 公平轮流地为所有进程服务 按先来后到,轮流给各个进程一个时间片执行。如果没执行完就交给下一个进程,然后重新到队尾排队(时间片大小要合适。太大就是 FCFS 算法了,太小效率低) 进程调度 公平;响应快;但是频繁切换效率低,不区分紧急程度 不会
优先级调度 按紧急程度处理 优先度高的先执行 都用 可以优先处理紧急任务;但总是高优先级任务到来可能饥饿
多级反馈队列调度算法 根据时间片计算优先级 进程刚到达放入1队列,一个时间片内没完成放入2队列,还没完成一直往后放,如果已经在最后一个队列了就重新放到该队列结尾。1队列优先级最高 进程调度 综合了各个调度算法优点

image-20221207130426052

进程同步和互斥

虽然之前提过进程是异步的,各个进程相互独立,但是有的工作是有顺序的,比如先读入再写。

同步,即相互制约,指部分工作的次序需要协调。

互斥:一些共享资源不允许多个进程同时访问。比如一次只允许一个进程访问的资源叫临界资源。

image-20221207135453437

进程互斥遵循以下原则

  1. 空闲让进
  2. 忙则等待
  3. 有限等待,防止饥饿,不让进程等太久
  4. 让权等待,如果该进程进不去,那就不给他了,赶紧给别人。

进程互斥软件实现方法

  1. 单标志法 image-20221207140305743

​ 一开始只允许 P1 访问。一直到P1把 turn 变为0,然后切时间片的时候才能交给 P0. 两者交替访问。

​ 但是违背了空闲让进。如果只有 P0 想访问临界区,就一直进不去。

  1. 双标志先检查法

    image-20221207140814172

    判断对方有没有在访问。

    但是如果按照①⑤③②⑥⑦的顺序,两者会同时访问临界资源,违反了忙则等待。

  2. 双标志后检查法

    先上锁再检查。但是两个进程要是都先锁住了,就都执行不了了,可能出现死锁问题。

  3. Peterson 算法

    image-20221207141722082

    可惜没有遵守让权等待。

进程互斥硬件实现方法

  1. 中断屏蔽方法

    访问临界资源的时候把中断关上;访问完了打开,再允许调度。但是用户态不适用(用户态不应该搞中断的问题),而且对多处理机不适用。

  2. TestAndSet 指令

    是硬件实现方法,下图为软件实现方式。

    TS 软件实现方式

    如果 lock 原来是 true,那么其他进程访问的时候一直卡在 while 处。知道访问临界区的进程退出并解锁,其他进程访问的时候 lock 才是 false,才能跳出循环。

    但是无法解决让权等待问题,如果有一个进程把 lock 变成 true 之后又进不去临界区,其他进程就永远无法访问,一直忙等。

  3. Swap 指令

    image-20221207181932099

    逻辑上等同于 TS,也无法解决让权等待问题。

  4. 信号量

    信号量用于表示系统中某种资源的数量。我们用一对原语(等待 P,信号 V)对信号量做操作.信号量涉及到的三个操作就是初始化、P、V。

    image-20221207183421978

    然而这样的信号量也没能解决让权等待问题。不过一个记录型信号量可以解决。

    image-20221207194128022

    释放完资源,S.value≤0 说明还有进程在阻塞队列中,直接把当前刚释放的处理机给阻塞队列的进程执行。

    这样如果该进程进不去自己的处理机,就会把自己调整到堵塞态,把处理机让出来给别人,解决了让权等待问题。

信号量实现进程同步和互斥

互斥

image-20221207202126671

同步:如怎样保证2一定在4之前执行

image-20221207203235758

image-20221207203533381

进程种类

生产者和消费者

生产者把产品放入未满的缓冲区,消费者从未空的缓冲区取出。两者要互斥地对缓冲区访问;同时如果生产者要放入满缓冲区要先等消费者取出,消费者要从空缓冲区取要先等生产者放,这两件事是同步关系。

image-20221207205103231

注意 决定互斥的 mutex 顺序不能和决定同步的 empty full 颠倒!!!

image-20221208003209311

如上图,如果先进行①②,则互斥锁打开,生产者因为仓库已满无法放入,阻塞。消费者又因为互斥锁打开无法取东西,阻塞,就死锁了。

多生产者 多消费者

image-20221208003430037

缓冲区大小(盘子)如果为1,那么不用互斥变量也能解决。

image-20221208003840866

因为苹果、橘子、盘子变量同时只能有一个=1,每次最多也只能有一个变量访问。如果缓冲区变为2就需要 mutex 了,父母同时放入水果可能覆盖对方的值。

吸烟者问题

image-20221208004256422

image-20221208020707796

因为只有一个缓冲区,所以不需要 mutex 就能实现互斥。

吸烟者问题其实是实现了一个生产者生产多种产品的问题。此例中生产者生产的产品顺序固定,还可以修改生产顺序逻辑。

读者-写者问题

允许多人同时读,但是有人在写的时候其他人不能读写。

读进程有限的解决办法:mutex 用于所有读进程互斥访问 count 变量,当 count 变量=0时才可写入。但是写进程有饿死的风险。

image-20221208132742538

如果再加一个写的互斥信号量,就能让写操作优先于读操作了。

image-20221208132713723

哲学家进餐问题

一个进程同时持有多个临界资源的情况。

image-20221208193715205

如果只是简单地用两个信号量判断左右两根筷子是否空闲,可能所有进程并发拿起了左手的筷子,并发地卡住了右手的筷子,造成死锁。

有几种解决方案:①拿两只筷子的行为添加一个互斥信号量。这样一个哲学家拿不起来被阻塞的时候,其他哲学家也不会尝试拿。

image-20221209013159819

②奇数先拿左手,偶数先拿右手,这样相邻的人拿筷子就会互斥。

管程

信号量挺琐碎的,而且容易出错,顺序错了都会影响结果。

管程是什么

管程内的数据只有在管程内的过程(函数)才能访问;一次只允许一个进程进入管程。

image-20221209130200400

monitor 是 java 语法的管程,每次只允许一个进程访问(互斥),进程只能通过管程提供的特定入口进入。我们可以自己定义逻辑判断,让进程等待或释放(同步)。

关键字 synchronized 修饰的函数同一时间段内只能被一个进程访问。

死锁

A等B,B等C,C等A,都在等对方手里的资源。

和饥饿不一样,饥饿是如果一直来新进程自己可能一直无法继续。

死锁的四个条件:

  1. 互斥,对某一资源互斥使用。
  2. 不剥夺,不能抢资源。
  3. 请求和保持,在新资源还在请求时可以保持自己手里已有的资源。
  4. 循环等待,存在资源的循环等待链。比如有12两个资源,进程一申请顺序:12,进程二申请顺序:21,两者正好互相锁住。

可能发生死锁的情况:

  • 竞争不能共用的系统资源时;
  • 进程推进顺序不当;(哲学家拿筷子,都先拿左手的)
  • 信号量使用不当(如生产者消费者一例,先互斥锁再信号量锁。生产者先进入满仓库,因为当前只有自己进程进入仓库所以互斥锁不干扰;但是仓库已满,无法放入导致阻塞;消费者又因为生产者正在访问,互斥锁限制导致阻塞)。

预防死锁

破坏四个条件之一。

  1. 把资源改为可以共享使用的;不过不是所有设备都能强行改的。

  2. 剥夺:要么如果当前进程资源不足时立刻全部释放资源,等一会再重新运行;要么根据优先级,高优先级抢低优先级的资源使用。但是比较复杂而且会影响前一个进程,常用于易于保存和恢复状态的进程(如 CPU);效率低;方案一还可能导致饥饿。

  3. 请求保持:采用静态分配方法,进程开始运行时就把所有需要的资源都给他,全程让他运行。但是可能有的资源这个进程就用一两下,一直占着会比较浪费;可能导致饥饿。

  4. image-20221209222856008

    比如上例,两个进程申请资源顺序都从小到大,先1后2,就不会死锁了。但是实际资源使用顺序可能并不是从小到大,效率低;而且增添设备要修改编号,不方便;而且用户编程要注意顺序,比较费事。

避免死锁

image-20221209224517132

安全序列就是按某种顺序分配资源,所有进程都能顺利得到,不会死锁。存在一种安全序列的情况,那么当前系统就是处于安全状态。

银行家算法:

image-20221209232829502

如图,剩余资源 (3, 3, 2),视作一个一维数组。P0 全分配也不够,不行;P1可以,全分配给P1后P1归还资源,剩余资源数变成 (5, 3, 2);然后P2不够,P3可以,变成 (7, 4, 3);p4 变成 (7, 4, 5); P1 变成 (7, 5, 5),最后 P2 P4,五轮循环全部分配。

最大需求:Max

已分配:Allocation

最多还需要:Need

当次发起申请的请求量:Request

Request≥Need:出错了

Request≤Available:说明有多余空闲。系统先尝试分配一下,成功后证明安全,正式分配。

检测、消除死锁

点表示进程,方框表示资源,箭头表示分配。如果所有箭头都能被顺利消除,证明不会发生死锁。

image-20221210010155006

P1向R2要一个。R2给了P2一个自己还剩下一个,所以可以要到。(P1释放后,就能把P1的所有边去掉了)

P2向R1要一个。但是R1三个都给出去了,所以P2要不到,阻塞了。

P1运行完释放,R1里就有两个空闲的了,P2就能要到了。

下例就没法全部消除,死锁了。只有P3能正常运行并释放。

image-20221210011729366

解决办法:可以挂起或终止死锁的部分节点,或者回退到没有发生死锁的断点。

内存

存放数据的硬件,程序要先被放到内存中才能被处理.

代码被编译成指令,通常还会涉及到几个地址。比如一个加法指令涉及的三部分(A,B,C)A代表:这是一个加法指令;B C代表:把B中的数据加到C中。

指令中采取的是相对的逻辑地址,因为还不能确定物理存到了哪里。

image-20221210140802510

装入有三种:

  1. 绝对装入,编译时就知道要放到哪个物理地址里,编译时直接采用物理地址;适用于单道程序环境。
  2. 静态重定位装入,编译时采用相对地址,装入时根据相对地址存入到物理地址。但是如果内存中容量不够,就不能装入。用于早期操作系统。
  3. 动态重定位装入,刚进内存不会装入,等到程序运行时再装入。允许程序运行中在内存里移动。现代操作系统。

链接也有三种,装入前链接成一块,边装入边链接,运行时再链接。

内存管理

内存中存了多少?空闲多少?进程分配到哪里?怎么释放?内存扩充(游戏60G,内存4G,采用虚拟内存的方式扩充)、地址转换(就是装入。逻辑地址和物理地址之间的链接 是操作系统解决,程序员不用管)、内存保护。

覆盖与交换技术

覆盖技术:解决内存太小的问题。内存中分固定区和覆盖区。

image-20221210181822311

缺点在于固定区覆盖区要程序员自己规定,不透明。

交换技术:把内存中某些进程拿出来,再把某些进程换进去。磁盘中专门有一块交换区,追求交换的速度,采用连续存储方式,IO 比文件区要快得多。缺页率高时换出得多。优先换出阻塞和优先级低的进程。

覆盖是同一个进程中的,交换是多个进程中的。

内存保护限制每个进程只能访问自己范围内的数据。可以设置上下限寄存器;或者重定向寄存器决定上限,界地址寄存器代表最大长度。

内存分配

连续分配:内存分为系统区和用户区。系统区存放操作系统相关数据;用户区存进程,只能存一个。实现简单,没有外部碎片(内存空闲区域太小,没法分配给进程的情况),可以通过覆盖技术扩大内存,不一定要保护;但是利用率低,而且有内部碎片(分配给某个进程的内存区域,有些部分没有用到)。

固定分区分配:内存分成很多个分区,每个分区只装一道作业。

(分区大小相等,缺乏灵活性,但是适用于一台计算机控制多个相同对象的场合;

也可以设置不同大小的分区,适用于各种作业)

操作系统需要叫分区说明表的数据结构,让内核程序知道哪些分区可用不可用,起始位置,大小等。没有外部碎片,但是有内部碎片;而且可能有过大的用户程序,所有分区都满足不了,就只能覆盖,降低效率。

动态分区分配:根据进程大小动态建立分区。系统区大小都是不固定的。可以采取空闲表或空闲链的数据结构存储信息。

动态分配的算法:

  1. 要插入的进程比空闲区小:更新空闲区起始位置和大小。
  2. 要插入的进程和空闲区一样大:直接在空闲分区表中删掉这一条空闲区的记录。

动态分区回收算法

  1. 要回收的分区前面或后面有一块空闲:更新那块空闲的起始位置和大小。
  2. 前后没有空闲:新增一条空闲记录。
  3. 前后都有空闲:空闲表中两条记录 更新成一整条。

动态分配算法 没有内部碎片,但有外部碎片。可以通过“拼凑”来解决。

具体怎么选择空闲分区分配?

动态分区分配算法

首次适应算法:从小地址到大逐渐找。空闲区按地址从小到大存储在空闲链中。

最佳适应算法:优先找能容得下的最小的空闲区,大的留着给大的进程预备着。空闲分区从小到大存储在空闲链中。小碎片会越来越多,导致外部碎片。

最坏适应算法:优先使用最大的空闲分区,避免外部碎片。但是大进程到来的时候可能插入不进来了。

临近适应算法:因为首次适应算法每次都从头找,头部可能有很多小碎片,每次又要遍历。临近适应算法每次从上次结束的地方开始查找。也是优先使用最大分区,类似最坏适应算法。

基本分页存储算法

是一种非连续分配。

每个分区10MB,A进程23MB,可以拆成10+10+3MB存储。

但是这样导致第三个分区内部碎片达到7MB。如果分区大小为2MB,那么只有最后一个分区有1MB内部碎片。

每个分区是一个页框,有页框号,从0开始。把进程分配成页框的大小,叫做一个个页,有页号。

分的页并不是连续存储在分区中的,可以不连续存储,根据逻辑地址找页与页之间的关系。

页号:逻辑地址/页面长度。

偏移量:逻辑地址%页面长度。

如第80个内存单元,页面长度50,那么页号=1,偏移量=30.

如果页面大小是2的整数次幂,那么页面范围就是00000……0001000……0000到00000……00010111……11111,末尾的部分就是页面偏移量。

页表存储页面信息,页表项包含页号和块号信息,每个页表项应该能表示出所有块数的信息。比如有2^20个块,则每个页表项要有20种状态,20位即至少三个字节。(但是通常采用4个字节 这样让总内存数可以等于证书个页表项)

基本地址变换机构

用于实现分页管理逻辑地址转变为物理地址的操作。

image-20221211132202245

基本地址变换结构 每次要访问两次内存。

查页表,知道要访问的数据的位置:一次;

去访问数据:二次。

具有快表的地址变换机构

image-20221211191228059

让最近访问过的页表项存到快表中。

image-20221211194853670

如果快表中有该页表项,直接取出该项计算出物理地址,就不用到慢表中查表了。

访问快表命中了,就只需要一次访存。

image-20221211202653892

两级页表

  1. 单极页表必须连续存储;
  2. 并不是整张页表都会被频繁访问到。

可以把长长的页表再分成离散的几块,即第二级页表。又叫顶级页表。这样就解决了问题1.

然后对于没有进内存的目标页,访问的时候产生缺页中断,然后调入页。

image-20221212121134020

但是没有快表的话,两级页表要访存3次。

基本分段存储管理方式

按逻辑把程序分为一个个段(如 主函数,sum 函数……)分段离散的存储到内存中。可读性更高。

段号规定了可以有多少个段,段内地址规定了段的大小。

当然啦,为了查找到哪个段放在哪里,也需要段表。段表每一条包含段号、基址(起始位置)、段长。

image-20221212153212634

image-20221212154131290

段表寄存器存储在系统区的 PCB 中,即段表的位置。

相比分页,分段是有意义的,是二维的,用户既要给出段名(如 main 函数的段),又要给出地址。

分段也更容易实现信息共享和保护。首先什么代码可以共享?不能修改的代码可以共享,如常量,防止多个进程并发访问会出问题。

然后分段是按逻辑分的。比如每个函数分一个段。分页是按大小直接截断的。所以分段可能提取出可以共享的代码片段,分页会更难一些。

image-20221212154510085

分段也是两次访存,也可以尝试快表。

段页式管理方式

分段和分页的缺点是什么?

分页划分固定分区,但不便于信息共享和保护。

分段根据程序分配大小不确定的分区,可能有外部碎片(紧凑还是付出代价很大的)。而且进程太大的话也难以找到一大块连续区域。

那,把大的段分页就好了。

分页的页表包含:页号 页内偏移量信息。分段的段表包含:段号 段长 基址信息(段号可以隐含)。

段页式系统的逻辑地址结构 段包含:段号 页表长 页存放块号(起始地址)。页包含:页号 页面存放的内存块号。

image-20221212172648263

虚拟内存

归根结底,以上的内存分配方法需要把整个程序都装入内存运行。而且由于内存的驻留性,程序运行完之前整个都一直留在内存中。

第一,没必要,程序的某些部分不常使用,不用一直占在内存里,内存利用率低。第二,太大的程序装不进来。第三,很多程序排队时,这样一次只能运行很少的几个,并发性差。

根据之前快存学到的局部性原理,可以把常用的部分留到内存中,不常用的拿出去。要用到的不在内存里,就拿进来;内存太满,就把不常用的再拿出去。

image-20221212180126990

虚拟内存有多次性(分多次装入)、对换性(可以换出来)、虚拟性。

存储方式:采用连续型并不合适,因为要分多次装入。改用请求式管理。

请求分页管理

和基本分页管理的区别在于:1. 要访问的信息可能在内存中,也可能不在。不在的话要调入进来。要存储该页是否在内存中的信息。

  1. 暂时不用的可以换出去。如果在内存中做过修改,那么不用拿回到外存;如果做过修改了就要了。

image-20221212184343182

先根据状态位判断要访问的页在不在内存中,不在里面,就先产生缺页中断,调入内存。如果有空闲块就插到空闲块里,没有就根据页面置换算法换出来一个不常用的。调出内存的页面如果发生改变就要写入外存,没有就直接丢掉。最后还要修改请求页表中的状态信息

image-20221212191632934

缺页中断是自行产生的内中断。

image-20221212185821630

几个值得注意的点:

  1. 我们知道,如果内存中的内容被修改(发生了”写“操作),状态位中的修改位变为1,移出内存时要再写入外存。实际上修改位的改变不一定用在内存中修改,可能只修改快表中的修改位即可,这样少访存。
  2. 换入换出页面太频繁,开销会很大。
  3. 页面调入内存后,直接就放到快表里,之后访问就访问快表就行。

页面置换算法

追求更小的缺页率,这样换入换出更少。

最佳置换算法 OPT:选择不再使用,或者最长时间内不再被访问的页面淘汰。

image-20221212194302144

插入701之后满了,再想插入2就要顶掉一个。看看后面要访问的页面,7是最不着急的,先把7顶掉。后面以此类推。

注意缺页不一定就会发生页面置换。如果还有块空闲就不用。

但最大问题就是操作系统无法提前预判后面的页面访问序列。

先进先出置换算法 FIFO:最早进来的页面最早被淘汰。就是队列的数据结构。

image-20221212194655703

但是这就是再猜啊。怎么能因为用的早就觉得下一秒他不会用了呢?买彩票呢。可能会引发 Belady 异常(为进程分配的物理块变大时,缺页次数反而变多)。

最近最久未使用置换算法 LRU:

哪个最久没用过,就先替换掉哪个。

image-20221212195347896

性能确实好,但是开销大,实现困难。

时钟置换算法 CLOCK:每个页面添加一个访问位,初值都是0,访问过了改为1.每次优先替换掉0的页面。如果全为1,则全置0后再次扫描。

改进的时钟置换算法:同时考虑访问位和修改位。

替换:第一轮扫描找0,0的替换。这种不仅最近没用过,而且不用写入外存。

第二轮找0,1的替换。这种最近没用过,但是之前修改过,要写入外存。

如果这两轮都没找到,说明所有的第一位都是1.全部置0,再进行扫描。

第三轮找0,0,类似第一轮。

第四轮找0,1,类似第二轮。

页面分配策略

驻留集:请求分页存储管理中分给内存的物理块的集合。虚拟存储中,驻留集大小一般小于进程总大小。太小,频繁缺页出入内存效率低;太大,并发性降低。

固定分配:一开始给定每个进程固定大小的驻留集。

动态分配:根据运行过程中的情况动态分配大小。

局部置换:缺页时只能当前进程自己的物理块置换需要的页进来。

全局置换:可以用空闲的物理块,或其他进程的物理块置换。全局置换大小肯定不固定,肯定是动态分配。

image-20221213010232817

调入哪些页?

首次调入时,根据局部性原理,某个页相邻的页也会容易用到。因此一调调一小片。

运行中缺页调入时,只调缺少的页。

从何处调入页面?

image-20221213011456020

抖动/颠簸现象:刚换出去的页面又换进来。主要原因是驻留集太少了,少于要频繁使用的块。

工作集:运行时进程实际访问的页面集合。驻留集不应小于工作集。

文件

有信息的数据集合。

文件包含的信息:文件名、标识符(操作系统要看)、类型、大小、创建修改时间、所有者、安全信息。

文件管理

文件分为无结构的流式文件和有结构的记录式文件。记录式文件由一条条记录组成。

文件存放在根目录里的目录里。

操作系统应该向上提供给用户的功能:CRUD,打开和关闭文件。

文件存放在外存类似进程在内存中,是分块存放的(救命啊,我刚把那块学过去)。

初次子海外,操作系统还应该提供文件共享和保护功能。

文件的逻辑结构

无结构文件(如txt)很简单。

有结构文件一般有关键字区分各个记录;记录存储长度不同又分为定长和可变长。

有结构文件逻辑结构:

  1. 顺序/链式存储。顺序定长存储可以实现随机存取,想找第i位直接起始位置+i*单位长度即可。顺序可变长无法计算,链式存储不连续也无法实现随机存取。顺序定长存储如果物理上也采用顺序存储,则可实现快速检索。

  2. 索引文件。对于可变长记录文件,可以建立一个定长的索引表,包含索引号、长度、起始位置指针的信息。检索速度很高。但是索引表和记录数一样,占的空间不小。

  3. 索引顺序文件,一条索引代表一组记录。可能查找速度还是很慢,那就建立多级索引。

    类似数据结构中学到的中间表,如果索引中只存储必要的少部分信息(文件名,指针),索引占的小,能放更多的索引,用更少的磁盘块存储,就平均需要访问更少的磁盘块就能找到文件。

image-20221213034225243

​ 外存中的索引节点叫磁盘索引节点,内存中的叫索引节点,可能包含更多信息,如文件是否被修改、同时有几个进程在访问等。

文件目录

文件控制块 FCB 中存储文件名、类型(是否是目录)、权限、地址等信息。

目录支持的功能有:搜索、创建、删除、显示、修改文件。

早期操作系统只支持单文件目录,那就不能重名了。

早期多用户操作系统支持双文件目录,一个主文件目录,其中包含多个用户目录。不同用户目录各自文件可以重名。但是用户自己没办法创建多级目录。

后来的多级目录结构支持多级目录了。

引入当前目录概念:如果没有此概念,我们要找根目录下 /目录1/目录2/照片.jpg,需要三次访存。根目录找目录1,目录1找目录2,目录2找jpg。要是有当前目录的相对路径就会方便得多。

树形目录结构缺点在于不能共享文件。

无环图目录结构

image-20221213040035339

不同用户不同目录下可以访问到相同的共享文件。

共享文件要设置共享计数器。当有用户取消共享后,要删除共享信息,共享计数器–。减为0时删除共享节点。

文件的物理结构

很多操作系统中,磁盘块和内存块、页大小相同,成块成块拿进来。

类似内存,文件存储的逻辑地址分为逻辑块号和块内地址两部分。

连续分配

自不必多说物理块号=起始块号+逻辑块号。支持顺序访问和随机访问。但是不方便拓展,比如13块的A文件想扩展,但是4~6是B文件,不是空闲文件,A文件想拓展只能整体挪到空闲区域;而且还可能产生大量磁盘碎片。

链接分配

隐式链接:FCB存储起始块号和结束块号。像链表一样从第一个找到结尾(每个磁盘块中包含指向下一个盘块的指针,但是这对用户来说是透明的),没法随机存取,但是拓展方便。

显示链接:FCB 中包含起始块号,此外还有一张文件分配表 FAT,其中包含所有块号的下一块指针。隐式链接想找一个块,要先在 FCB 中找到起始位置,再读磁盘,找磁盘里的下一块。显示连接可以先不用读磁盘,根据分配表推测出要找的逻辑块的物理地址再去读磁盘,访问速度更快。

链接分配都不会有外部碎片。

索引分配

每个文件建立一个索引表。索引表存放的磁盘块叫索引块,文件数据存放的磁盘块叫数据块。FAT是一个磁盘对应一张,索引表是一个文件对应一张。

image-20221213102643626

也支持随机存取,也方便拓展,但是索引表占空间。

要是文件太大,索引表一个索引块存不下,需要多个。

可以让索引块之间链接起来。但是不支持顺序存储,要找最后一块就要从头便历。

可以建立多级索引。每级大小不超过一个数据块。k级表访问数据,要访问k+1次磁盘块(k次查找位置,1次查找数据)。

混合索引:

image-20221213104019760

小文件层级小一点。因小文件访问可能频繁一些,就少访问几次。

文件存储空间管理

操作系统的盘有什么用?又叫文件卷,每个文件卷都包含目录区和文件区。

空闲表法

分配磁盘块给用户:类似动态分区分配,可采用首次适应、最佳适应、最坏适应等。

回收磁盘块:也类似动态分区分配,考虑前后有无空闲块。

空闲链表法

image-20221213105751349

空闲盘块链分配:空闲链从链头摘下来k个空闲块,并修改链头位置。

空闲盘块链回收:回收的空闲块挂到空闲链结尾,并修改链尾位置。

空闲盘区链分配:先按算法找到合适的空闲盘区。如果没有合适的,也可以把不同盘区的盘块同时给一个文件。

空闲盘区链回收:如果和空闲盘区挨着,直接合并。否则变成一个单独的空闲盘区挂到队尾。

位示图法

一定注意从0还是1开始!

image-20221213111306083

分配:扫描位图,找到连续的k个0,修改为1.

回收:算出回收盘块的位图字号、位号,置为0.

成组链接法

大文件不适用空闲块法。

超级块存放在内存中,其中包含下一组空闲块块数和块号。

如果没有下一级了,下一组空闲盘块数可以用特殊标识符如-1表示。

image-20221213125105862

空闲分配:如果<100个,从超级块分配就够。

如果=100个,不能直接分配超级块因为这样超级块对后面的链接就消失了。要先把300的内容提到超级块作为新的超级块,再分配。

image-20221213130944587

回收:直接加到超级块上。如果超级块最大大小为100,已经满了,还要回收,就要让新回收的块作为新的超级块,并指向原来的超级块。

image-20221213131331129

文件基本操作

文件创建

需要关注:文件名;文件路径;需要的外存空间。

  1. 在外存中找到合适大小的内存空间;
  2. 在目录表中更新新文件的信息。

文件删除

  1. 根据目录表找到该目录项;
  2. 外存中回收内存;
  3. 目录表中删除该文件信息。

打开文件

需要用户提供的信息:文件名;文件目录;打开方式(读;写;……)

操作系统先去目录表找到对应的文件,复制到内存中。并且把目录项复制到内存系统的“打开文件表”中。

image-20221213133814373

关闭文件

删除用户打开文件表的对应项;回收空间;系统打开文件表的打开计数器-1,减到0则删除打开文件表的对应项。

读文件

指明要读的文件,要读入多少数据,读入的数据在内存中的位置。读入指定大小放入内存中。

写文件

和read很像。最后再通过write系统调用写回外存。

文件共享

基于索引节点的共享方式(硬链接):

image-20221213162136121

count说明还有几个进程在共享该文件。

要删除时,count–,若>0则不能删除,=0才能删除。

基于符号链的共享方式(软链接):

image-20221213162634924

删掉文件1,软链接仍然存在,只是无法通过软链接去访问文件1了。

因为访问共享文件要查询多级目录,进行多次 IO,因此采用软链接。

文件保护

口令保护:规定一个口令,用户要说对应口令才能访问。但是口令保存在系统内部,不安全。

加密保护:用密码对文件加密。如异或加密。有点费时。

访问控制:文件的 FCB 中增加一个访问控制列表 ACL,记录用户可以有哪些权限(读写运行ls)。以组为单位,如:管理员,文件主,文件主的伙伴,陌生人。

文件系统的层次结构

image-20221213165015538

image-20221213165347348

磁盘

image-20221213172411000

image-20221213172609205

image-20221213172629925

image-20221213172637487

磁盘调度算法

image-20221213181331914

先来先服务算法 FCFS:就是单纯的先处理先来的进程。

image-20221213182105548

最短寻找时间优先 SSTF:先找离当前磁道近的。

image-20221213182454524

扫描算法 SCAN:只有磁道移到最外侧之后才允许往回移动,避免 SSTF 的左右横跳。

image-20221213183053980

LOOK 调度算法:改进 SCAN 算法,观察到当前磁道已经是访问请求最右边的磁道后,就可以立即改变磁道移动方向往回。

image-20221213183329408

循环扫描算法 C-SCAN:

image-20221213184440873

C-LOOK 算法:

image-20221213184532367

减少延迟时间

磁盘一直旋转的。如果要读几个相邻的扇区,读了第一个处理的过程中磁盘还在转,处理好了的时候可能又转到不知道哪里去了。可能就会产生很长的延迟时间。

解决办法:交替编号。逻辑上相邻的扇区物理上分开。

image-20221213192126906

为什么磁盘的物理地址是(柱面号,盘面号,扇区号)而不是(盘面号,柱面号,扇区号)?因为更改柱面号需要移动磁头臂,更改盘面号不用移动臂,只需要激活相邻盘面的磁头即可。可以减少磁头移动消耗的时间。

磁盘管理

初始化:

​ 物理格式化:把磁盘划分为扇区。扇区包含头、尾、中间数据部分。头尾会存放一些扇区校验码之类的信息。

​ 磁盘分区:分为几个文件卷。

​ 逻辑格式化:创建文件系统(根目录、管理空间的数据结构如位示图、空闲分区表等)。

​ 磁盘的初始化程序:放在哪里?

​ ROM 只读存储器中的数据出厂时就写好了且不能更改,集成在主板上。但是磁盘的初始化程序说不定以后会更新换代,ROM 中的内容又不能更新,因此初始化程序不放在 ROM 中,而是放在磁盘(C)里。初始化程序的装入程序写在 ROM 中。

坏块的管理:坏掉的扇区。简单的磁盘直接在 FAT 中标记出来防止被使用到(对操作系统不透明)。复杂的磁盘交给磁盘控制器维护坏块链表,而且保留一些备用分区(对操作系统透明)。

I/O 设备

I/O 设备分类

按使用特性分类

人机交互类外设:如鼠标打印机键盘等。数据传输慢。

存储设备:移动硬盘、光盘等,数据传输速率快。

网络通信设备:调制解调器等用于网络通信,速度中等。

按速率分类

低速设备:鼠标键盘。

中速:激光打印机。

高速:移动硬盘等。

按信息交换的单位分类

块为单位:磁盘。

字符为单位:鼠标键盘等。

I/O 控制器

IO设备包括:

  • 机械部件:用于执行具体 IO 操作的,如鼠标按钮、显示器屏、磁盘盘面。

  • 电子部件:插入主板扩充槽的印刷电路板。

CPU 要通过 IO 控制器作为中介才能控制机械部件。

image-20221213200720497

image-20221213200846891

一个 IO 控制器可能控制多个设备;而且可能有多个寄存器。有的操作系统让这些寄存器存到内存里,叫做内存映像 IO;有的采用专门的地址,即寄存器独立编址。独立编址不在内存里,因此还要设置专门的指令来实现对其的操作,还要指明具体对哪个控制器操作。

IO 控制方式

关注:一次读写操作的流程;CPU 干预的频率;数据传送单位;数据流向;优缺点。

程序直接控制

读:

image-20221213201440173

重点在轮询。CPU要不断轮询。

数据传送单位:每次一个字。

数据流向:内存和 IO 设备经由 CPU 读写。每个字读写都需要 CPU 帮助。

简单,但是 CPU 和 IO 只能串行工作,CPU 一直轮询检查效率也很低。

中断驱动方式

CPU 发出读写命令后,等待 IO 的进程暂时阻塞,先运行其他程序。IO 完成后控制器会向 CPU 发一个中断信号,CPU 收到后继续执行。

image-20221213203221153

CPU 执行完每个指令的周期末尾检查中断。中断处理过程需要保存、恢复进程的运行环境,也需要一定时间开销。

CPU 只有 IO 开始时干预一下,等待 IO 过程中就运行其他进程了。

数据传送单位:每次一个字。

数据流向:内存和 IO 设备经由 CPU 读写。每个字读写都需要 CPU 帮助。

相比程序直接控制,CPU 利用率高一点了。但是一个字一个字的传,速度还是慢。

DMA 方式

数据传输单位是块;设备和内存之间数据传输不用每次都经过 CPU ,只有开始传输或结束时才需要干预。

image-20221213214114491

缺点:CPU 每发一条 IO 指令,只能读写几次连续的数据块。离散的数据块就要多次中断。

通道控制方式

通道相当于简化版的 CPU。CPU向通道发出指令,指明通道程序在内存中的位置,并指明要操作的是哪个 IO 设备,然后 CPU 就去运行其他程序了。

image-20221213215048076

通道能执行的指令很单一,而且放在内存中。

CPU 干预次数极少,效率也高,就是需要专门的通道程序支持。

I/O 软件层次

image-20221213215341827

设备独立性软件:向上提供接口(入库函数)。校验用户是否有权限使用当前设备。差错处理。分配和回收设备。管理数据缓冲区。建立逻辑设备名到物理设备名的映射,并根据实际的物理设备选择合适的驱动程序。

image-20221213220547401

可以只设立一个系统 LUT,但是所有用户的逻辑设备名不能重复;也可以给每个用户设计一个。

为什么不同设备驱动程序也不同?因为不同设备内部结构也不一样。比如不同打印机内部寄存器数量可能不一样。驱动程序一般作为单独的进程。

设备驱动程序:主要负责具体控制硬件设备。

中断处理程序:IO 顺利完成后,进行中断处理。并从设备读入一个字长的数据。

I/O 核心子系统

假脱机技术

脱机技术是什么?我们记得最早期的计算机是人手动打孔纸带放入计算机中的,因为人打孔太慢,CPU 运行再快也得等着。

批处理几段引入了脱机输入,先通过外围控制机把数据输入到更快速的磁带上再让主机读入。脱机指的是脱离主机控制的 IO 操作。

不仅 IO 快了,而且 CPU 忙的时候用户也可以先处理数据到磁带上。

假脱机技术 SPOOLing 用软件模拟脱机技术。

image-20221213222958979

输入输出进程模拟外围控制机。

借助 SPOOLing 技术,可以让打印机实现共享。收到用户的打印请求时,在输出井里申请空闲缓冲区(在磁盘上),并放入要打印的数据;且给用户进程申请一张空白的打印请求表,里面存储打印的相关信息,并把该表挂到假脱机文件队列上。打印机空闲时从队列中取出打印请求表,根据表取出打印的数据到输出缓冲区,再到打印机打印。

设备的分配和回收

分配设备要考虑:设备属性(独占?共享?虚拟?虚拟就是假脱机技术等独占改成共享)。设备分配算法(FCFS 优先级高的优先 短任务优先)。安全性

分配分为静态分配和动态分配。

设备分配管理中的数据结构

image-20221213224041439

设备控制表 DCT

image-20221213224208534

控制器控制表 COCT

image-20221213224340187

通道控制表 CHCT

系统设备表 SDT

image-20221213224501959

设备分配步骤

  1. 根据进程的物理设备名,去 SDT 找设备。
  2. 根据 SDT 找 DCT,空闲就直接把设备分给这个进程,忙碌就把这个进程的 PCB 挂到设备等待队列中。
  3. 根据 DCT 找到 COCT,空闲就分配,不空闲就等待。
  4. 根据 COCT 找到 CHCT,空闲就分配,不空闲就等待。

以上方法缺点在于用户要知道物理设备名,不透明;而且只指定这一个设备,如果该设备坏了或者堵塞哪怕其他设备能用也无法切换。

可以建立逻辑设备,用逻辑设备找物理设备。

SDT 中设备类型就是逻辑设备名。

通过逻辑设备表 LUT 建立逻辑设备名和物理设备名之间的映射关系。

缓冲区

缓冲区是一个存储区域,可以用专门的硬件寄存器,也可以用内存做。速度快,成本高,容量小,比如快表。本节中介绍的主要是内存缓冲区。

可以缓冲 CPU 和 IO 之间速度不匹配的矛盾,进而 CPU 中断次数也会减少;解决数据颗粒度不匹配的问题;提高 CPU 与 IO 的并行性。

单缓冲区

某用户进程请求设备读入若干个块的数据。。如果采用单缓冲策略,主存中会被分配一个缓冲区(一般是一个块大小)。缓冲区中只有空的时候才能冲入数据,只有非空的时候才能传出数据。

image-20221213231116133

image-20221213231551012

双缓冲区

一满一空,可以空的边读,满的边往工作区写。

image-20221213232144595

image-20221213232106544

用时=Max(C+M, T)

如果两台机器各配置2个缓冲区,就能同时收发了。

循环缓冲区

多个大小相同的缓冲区链接成一个循环队列。

image-20221213232951510

缓冲池

放满了各种各样缓冲区的池子。

image-20221213233107985

输入:取出一个空缓冲区挂到收容输入队列中,输入放到收容输入队列中,装好了挂到输入队列中。

提取输入:从输入队列取下来,放到提取输入队列中提取到用户进程,再挂回空缓冲区。

大作业:Java 程序设计-Java-语言-Wordle

Wordle 游戏介绍

Wordle的游戏规则很简单,玩家需要猜出程序每天指定的一个5位英语单词谜底。

玩家可以随意提交一个英语单词,但必须是字典里有的,不能胡乱拼写。

如果字母在谜底中出现且位置对了就显示绿色,字母出现了但位置不对就显示黄色,字母在答案的单词中没出现就显示灰色。

根据反馈信息再进行下一轮猜测,在6次尝试之内猜出就算赢。
来源:https://news.mydrivers.com/1/813/813695.htm#:~:text=Wordle%E7%9A%84%E6%B8%B8,%E4%BD%8D%E8%8B%B1%E8%AF%AD%E5%8D%95%E8%AF%8D%E8%B0%9C%E5%BA%95%E3%80%82

在这里插入图片描述

成果图示

博主大二期间学习的java课程大作业,就是写一款 wordle 游戏,主要考察 GUI 界面的开发。
最终成果图示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

玩法介绍

点击HELP 查看帮助,点击 START 开始游戏。
本游戏中没有虚拟键盘,输入框也不能获取鼠标焦点,只能通过键盘键入字母,回车检查答案。
敲下回车后,字母就会呈现灰色、黄色、绿色三种状态,并换到下一行开始下一次猜单词机会。当猜中答案或六轮游戏结束时,弹出小窗,提示用户游戏胜利/失败。
注意:

  1. 检查用户输入。本游戏中用户只能输入字母,当用户试图键入数字或符号时会提示只能输入字母。当用户输入不足5位就尝试检查,或尝试输入超出5位时会提示输入必须为5位字母。当用户输入非单词尝试检查会提示输入必须为词库中存在的单词(词库:words.txt,可以自己更新)
  2. 一些小的注意点。当用户输入中有两个e,都不在正确的位置上,那这两个e只有一个会显示黄色就够了,另一个显示灰色,否则会影响用户的判断。

代码

Java_Wordle_Game github下载地址
README:帮助信息
javadoc:生成的 javadoc
Test:测试图片及说明
words.txt:词库

运行方法:

1
2
javac Main.java
java Main

在此感谢老师同学对此项目的帮助指导!
欢迎大家star支持[Doge]有问题也可以与博主交流~

  • Copyrights © 2015-2024 Jingqing3948
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信