Posted on  Updated on 

Arduino uno 开发

前言

写下此文字时,是24年6月10日,我刚刚结束24年6月1,2日的中国机器人及人工智能大赛的北京市省赛和6号的模电考试,省赛是线下比赛,第一天调试不顺,第二天比赛也是匆匆忙忙,十分混乱。比完后内心感慨,思绪良久。七八号左右出成绩,幸运的是,二队省一,一队三队省二,基本上都可以进国赛。虽然十五号四级,但内心有一种强烈的欲望,想要对自己进行整理,生理上的,心理上的。我开始对我的技术栈进行整理,决定从最简单的auduino开始,这也是我写下本文的初衷,对知识进行整理。
本文大概分为四个部分,第一部分,arduino基础知识学习,第二部分,外设的使用,第三部分,电机的驱动,第四部分,实战。第三部分按理应放在第二部分,但个人认为电机驱动部分相对重要,就单列一部分了。我将遵循简约,准确的原则开始编写本文。本文并不针对初学者,需要一些C语言知识,电学知识作为前置知识,本文不介绍此部分。
本文使用了chat,加速本文的开发。

第一章 Arduino uno初识

1.1 Arduino简介

Arduino 旗下系类有很多,其中用途广泛的有四种,Arduino UNO,Arduino mega 2560,Arduino nano,Arduino uno wifi.

顺序从左到右对应。
Arduino UNO为通用型号,mega 2560引脚多,功能更强大,Arduino nano,板子小巧,质量轻,体积小,Arduino UNO wifi,多了个wifi功能,建议别用,直接用esp32.
本文主要针对的是arduino uno进行开发介绍。
中文网站如下

https://arduino.nxez.com/

1.2 Arduino 引脚功能介绍

引脚分为5个区,区域如下

1区为引脚供电区,
- IORES,输入输出参考电压
- RESET,复位引脚,默认输出为1,下拉即可复位。
- 3.3V和5V是供电引脚,uno可通过USB输入电源,
此时,3.3V和5V可对外供电,也可通过3.3V和5V
对单片机进行供电,不使用usb供电
- GND,地线
- Vin,外部电源为开发板供电

2区为模拟口引脚区(analog)
ADC表示模拟到数字转换器。 ADC是用于将模拟信号
转换为数字信号的电子电路。模拟信号的这种数字表示
允许处理器(其是数字设备)测量模拟信号并在其操作中使用它。

Arduino引脚A0-A5能够读取模拟电压。在Arduino上,ADC具有10位分辨率,这意味着它可以通过1,024个数字电平表示模拟电压。 ADC将电压转换成微处理器可以理解的位。
模拟引脚只能模拟输入,不能模拟输出。
3区为ICSP插头接口区

4区为同3区,部分板子无此区域
5区为数字引脚区(digital)
Arduino Uno的引脚0-13用作数字输入/输出引脚。其中,引脚13连接到板载的LED灯;
引脚3、5、6、9、10、11具有PWM功能

以下是 Arduino Uno 的一些关键硬件规格:
• 微控制器:ATmega328P
• 工作电压:5V
• 输入电压(推荐):7-12V
• 输入电压(限制):6-20V
• 数字I/O引脚:14(其中6个提供PWM输出)
• 模拟输入引脚:6
• 每个I/O引脚的直流电流:20 mA
• 3.3V引脚的直流电流:50 mA
• 闪存:32 KB(ATmega328P),其中0.5 KB用于引导加载程序
• SRAM:2 KB(ATmega328P)
• EEPROM:1 KB(ATmega328P)
• 时钟速度:16 MHz

1.3 Arduino 供电与取电

Arduion UNO 的供电方式有四种
1. USB接口供电(5V)
2. 外部电源接口供电,直流电源电压必须为7V ~ 12V
3. Vin供电引脚供电,供电电压7~12V
4. 5V引脚5V供电。

注意通过VIN引脚供电,有一定可能性造成降压芯片烧毁,所以,如果电源12V,走外部电源接口供电,如果电源5V,走5V引脚供电。

3.3V无法通过3.3V引脚供电,会造成单片机工作异常。

大功率用电模块不要走单片机的引脚供电,一方面,供电不足,另一方面,可能造成烧毁或异常。

1.4 Arduino 引脚设置

打开Arduino的开发IDE,插上arduino,使用USB接口,另一端接入电脑,选择端口和开发板。IDE的安装和端口选择及中文语言选择自行百度,这里略过。IDE如下。

Setup(){}函数为初始化函数,会在arduino开机时执行一次。
Loop(){}函数为循环函数,主程序会在这里循环执行。

我们在这里学习数字和模拟引脚的设置。

1.4.1数字引脚

数字引脚即可以输入也可以输出。该类引脚有两种状态,高电平(1)与低电平(0),高低电平的确定由参考电压(AREF)确定。每个数字I/O口(input,输入,output,输出)可提供最高40mA的电流和5V的电压。
数字I/O口定义函数。

pinMode(pin,mode)

pinMode()函数为数字引脚定义函数,第一个参数pin,为引脚标号,即要设置那个引脚,范围为0-13,即数字引脚区。Mode有三种模式,INPUT(输入),INPUT_PULLUP(带上拉电阻输入),OUTPUT(输出)。

digitalWrite(pin,mode);

设置引脚输出状态后,可用digitalWrite函数设置引脚置为0还是1,不设置的话默认为LOW,低电平。第一个参数为引脚标号,第二个参数为状态,LOW或者HIGH;

digitalRead(pin);

digitalRead(pin)该函数为读取引脚状态,返回值为int型。结果为HIGH,LOW。
注意:在进行读取引脚操作前,需先对引脚输入模式进行定义

1.4.2模拟引脚

使用模拟引脚时,在模拟方面只能模拟输入无法模拟输出。

int b=analogRead(A0);

A0为模拟口的一个引脚,可替换为A0,A1- A5;
返回值为0-1023一个数字,模拟引脚内置10位A/D转换器,能将0~5V的电压转换为0-1023的数值,读取精度为5V/210即,每单位4.9mV.
模拟口也可作为数字引脚使用。使用如下

pinMode(A0,OUTPUT);
digitalWrite(A0,HIGH);

A0A5也可用14-19表示。
1.4.2PWM引脚
在数字引脚上,我们可以观察到有的数字引脚旁边带
号,这说明此引脚具有PWM输出的功能。如何使用这些引脚输出PWM波呢?
首先,这些引脚是数字引脚区,应先设置引脚输出状态,再设置PWM波的占空比。

pinMode(3,OUTPUT);
analogWrite(3,100);

通过anglogWrite(pin,Value);设置占空比,占空比范围为0-255。PWM波不是模拟输出,arduino的PWM波输出频率较低,一千Hz以内,后续再详细介绍PWM.

1.5 Arduino串口通讯

通过USB线将板子和计算机连接,可实现串口通讯,UNO的硬件串口资源仅有一个,一个串口资源,仅支持1对1,板子与计算机通讯时不要使用其他模块占用此通道,否则可能造成串口传输乱码,或者下载失败。
本小节仅讨论计算机与板子的串口操作,串口的其他操作后续内容后面章节补充

1.5.1串口初始化

使用 Serial.begin(baudRate) 函数来初始化串口通信,参数 baudRate 是波特率(数据传输速率),常见值是 9600, 115200 。初始化内容一般放在setup函数里。如下。

1
2
3
void setup() {
Serial.begin(9600); // 初始化串口通信,波特率为9600
}

1.5.2发送数据

• Serial.print(data):发送数据,但不换行。
• Serial.println(data):发送数据并换行。
可用串口接收器接受。

1.5.3接受数据

• Serial.available():返回串口缓冲区中的字节数。
• Serial.read():读取串口缓冲区中的一个字节,返回值为 -1 表示没有数据可读。
• Serial.readString():读取串口缓冲区中的字符串,直到超时(需要 Serial.setTimeout() 配合)。
• Serial.readStringUntil(char terminator):读取串口缓冲区中的字符串,直到遇到终止符(如换行符)。
Serial.setTimeout(500); // 设置超时为500毫秒

1.5.4串口综合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void setup() {
Serial.begin(9600); // 初始化串口,波特率为9600
Serial.setTimeout(1000); // 设置读取操作的超时时间为5秒,可注释
Serial.println("Enter a string (timeout 5 seconds):"); //提示用户输入字符串
}

void loop() {
// 检查是否有数据可读
if (Serial.available() > 0) {
String receivedString = Serial.readString(); // 读取串口缓冲区中的字符串
Serial.print("You entered: ");
Serial.println(receivedString); // 打印接收到的字符串
}

delay(500); // 延迟500毫秒
}

操作时,要注意程序运行在uno上,主体是单片机而不是计算机。

1.6 Arduino AREF与IOREF的使用

部分arduino的板子的IOREF引脚会该成5V供电引脚 ,查阅原理图,IOREF被锁死。

AREF
在mega328中,有六个模拟输入引脚,这些模拟输入引脚简单的说测量的是输入的电压值然后用 0~ 1023来表示电压的大小,当然,这个值有一个范围,通常来说是0~5V,查阅UNO的规格书可以知道UNO是具有10位的ADC。

10位ADC以为着什么呢?以为着它能将0 ~ 5 V的电压分成1024份(2^10),算出来即为4.882mV的测量精度,举个例子就是0V的时候测量结果为0,5V为1023,3V约为615,但是有一种情况就是你输入的电压最大是3.3V,测量出来的最大值也就是675,这个时候你或许会想到map,确实这不失为一个好办法。

但是AREF提供了另一个更好的解决办法,即在AREF接入一个最大模拟输入量作为参考值(比如上面所说的3V3),这样不仅能直接读到1023,更为重要的是精度提高,用3V3作为AREF的时候精度是多少?? 3.3/1023 ==3.223mV,测量精度提高了。

使用外部电源注意事项:
- 确保外部参考电压在 0 到 Vcc 之间,对于 5V Arduino 板,电压应在 0 到 5V 之间。
- 外部参考电压不应超过 Vcc,否则可能会损坏 Arduino 板。
- 使用一个稳定的电压源作为外部参考电压。如果外部参考电压不稳定,ADC 的精度会受到影响。
- 如果已经连接了外部参考电压,不要在代码中使用 analogReference(DEFAULT); 或 analogReference(INTERNAL);。这样可能会引起冲突和不正确的读数。

1
2
3
4
5
6
7
8
9
void setup() {
Serial.begin(115200);
analogReference(EXTERNAL); // 使用外部参考电压
}

void loop() {
int a = analogRead(A0);
Serial.println(a);
}

1.7 Arduino 上拉电阻

上拉电阻是什么?在I/O口模式设置中,输入模式有两种,INPUT和INPUT_PULLUP,这两者有什么区别吗?
本节将解答上面的问题。
输入模式的引脚相当于电压表的探头,如果测得电压比基准电压高,则为1,比基准电压低为0;
首先我们要了解一个概念,引脚悬空,即当某个引脚处于输入模式时,而该引脚什么也没有接的情况称之为引脚悬空。灵魂画手上线,如下图。

引脚悬空会有什么问题呢?会造成读取的引脚状态不对,此时是高电平还是低电平呢?无法确定。

1
2
3
4
5
6
7
8
9
10
11
12
void setup() {
Serial.begin(115200);
pinMode(2,INPUT);
}

void loop() {
delay(50);
int a =digitalRead(2);
Serial.println(a);

}

打印结果如下,特别是将手指捏在悬空引脚尤为明显。

而将引脚模式设置为输入上拉后,输出不飘了,为1.

pinMode(2,INPUT_PULLUP);

Arduino内部的上拉电阻为20K欧,一般上拉电阻为10K欧

此时引脚不接东西时,测的电压为VCC,即高电平,上拉电阻阻值较大,能够限流,防止大电流灌入引脚。当引脚接GND时,此时pin测的电压为GND的电压,为低电平。
不使用单片机的内置上拉电阻,自行在输入引脚外接一个阻值大小合适电阻是相同的效果。上拉电阻稳定输入引脚状态,克服了引脚悬空带来的引脚测量值不准的问题。

1.8 Arduino 定时器资源

Arduino Uno 基于 ATmega328P 微控制器,该微控制器包含三个硬件定时器资源:Timer0、Timer1 和 Timer2。每个定时器有不同的特性和用途,具体如下:

Timer0
类型:8位定时器
用途:主要用于 Arduino 内部计时功能,例如 millis() 和 delay() 函数。它可以生成中断,用于更新系统时间。尽量不要更改这个定时器。
PWM 引脚:5和 6(对应 Arduino 引脚)
Timer1
类型:16位定时器
用途:由于其较高的分辨率,Timer1 常用于需要精确时间控制的任务,例如 Servo 库。它也可以生成高精度的 PWM 信号。
PWM 引脚:9 和 10(对应 Arduino 引脚)
Timer2
类型:8位定时器
用途:通常用于生成较低频率的 PWM 信号或执行其他定时任务。其频率设置比较灵活,适用于音频生成等应用。
PWM 引脚:3 和 11(对应 Arduino 引脚)

中断频率(Hz)=(Arduino时钟速度16MHz)/(预分频器*(比较匹配寄存器+ 1))

WGM02、WGM01、WGM00:这三个位用于选择相位校正(phase correct)模式、快速(Fast)模式或CTC模式。
相位校正模式:计数器在达到最大值(255)后开始减小,形成一个三角形波形。
快速模式:计数器达到最大值后归零,形成锯齿波形。
CTC模式:计数器在达到OCR0A(比较寄存器A)后归零,形成可变的锯齿波形。
CS02、CS01、CS00:用于控制预分频系数,即将系统时钟分频得到计时器时钟。
分频系数1:无分频。
分频系数8:系统时钟除以8。
比较寄存器A、B:除了设置定时器外,还有两个寄存器用于控制PWM比较值,从而控制占空比。
COM0A1、COM0A0、COM0B1、COM0B0:用于设置PWM输出引脚的电平。
COM0A1/COM0B1、COM0A0/COM0B0为1、1时,当OCR比较寄存器小于计数器值时引脚输出0,大于计数器值时输出1。
COM0A1/COM0B1、COM0A0/COM0B0为1、0时,当OCR比较寄存器小于计数器值时引脚输出1,大于计数器值时输出0。
CTC模式下,COM0A1/COM0B1、COM0A0/COM0B0为0、1时,每当计数器溢出,引脚输出取反。

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
int toggle0, toggle1, toggle2;

void setup() {
cli(); // 关闭全局中断

// 设置定时器0为10kHz(100us)
TCCR0A = 0; // 将整个TCCR0A寄存器设置为0
TCCR0B = 0; // 将整个TCCR0B寄存器设置为0
TCNT0 = 0; // 将计数器值初始化为0
// 设置计数器为10kHz,即100us
OCR0A = 24; // 比较匹配寄存器 = [16,000,000Hz /(预分频器 * 所需中断频率)] - 1
// 比较匹配寄存器 = 24, 中断间隔 = 100us 即中断频率10kHz
TCCR0A |= (1 << WGM01); // 打开CTC模式
TCCR0B |= (1 << CS01) | (1 << CS00); // 设置CS01位为1,CS00位为1(64倍预分频)
TIMSK0 |= (1 << OCIE0A); // 启用定时器比较中断

// 设置定时器1为1kHz
TCCR1A = 0; // 将整个TCCR1A寄存器设置为0
TCCR1B = 0; // 将整个TCCR1B寄存器设置为0
TCNT1 = 0; // 将计数器值初始化为0
// 设置计数器为1kHz,即1ms
OCR1A = 1999; // = (16*10^6)/(1000*8) - 1 (must be <65536)
TCCR1B |= (1 << WGM12); // 打开CTC模式
TCCR1B |= (1 << CS11); // 设置CS11位为1(8倍预分频)
TIMSK1 |= (1 << OCIE1A); // 启用定时器比较中断

// 设置定时器2为8kHz
TCCR2A = 0; // 将整个TCCR2A寄存器设置为0
TCCR2B = 0; // 将整个TCCR2B寄存器设置为0
TCNT2 = 0; // 将计数器值初始化为0
// 设置计数器为8kHz
OCR2A = 249; // = (16*10^6) / (8000*8) - 1 (must be <256)
TCCR2A |= (1 << WGM21); // 打开CTC模式
TCCR2B |= (1 << CS21); // 设置CS21位为1(8倍预分频)
TIMSK2 |= (1 << OCIE2A); // 启用定时器比较中断

sei(); // 打开全局中断
}

// 中断0服务函数
ISR(TIMER0_COMPA_vect) { // timer0中断,产生频率为10kHz / 2 = 5kHz的脉冲波
if (toggle0) {
digitalWrite(8, HIGH);
toggle0 = 0;
} else {
digitalWrite(8, LOW);
toggle0 = 1;
}
}

// 中断1服务函数
ISR(TIMER1_COMPA_vect) { // timer1中断,产生频率为2Hz / 2 = 1Hz的脉冲波
if (toggle1 >= 500) {
digitalWrite(13, HIGH);
}
if (toggle1 <= 500) {
digitalWrite(13, LOW);
}
toggle1 += 1;
if (toggle1 >= 1000) {
toggle1 = 0;
}
}

// 中断2服务函数
ISR(TIMER2_COMPA_vect) { // timer2中断,产生频率为8kHz / 2 = 4kHz的脉冲波
if (toggle2) {
digitalWrite(9, HIGH);
toggle2 = 0;
} else {
digitalWrite(9, LOW);
toggle2 = 1;
}
}
// 主循环函数
void loop() {
}

1.9 其他

板间通讯。
如果想对板子进行更细致的操作,建议更换STM32,或者其他单片机。

第二章 常用模块的使用

2.1 红外对光管的检测

应用场合:
1.电度表脉冲数据采样
2.传真机碎纸机纸张检测
3.障碍检测
4.黑白线检测

DO 为数字输出口
AO为模拟输出口
可5V或3.3V供电。

中间的螺旋可以调节探测的距离。
当红外接收器接受到信号时,开光指示灯亮起。DO为低电平,检测这个端口的值,即可知道探测状态。

测试
5V供电,DO引脚接3号,AO接A0

1
2
3
4
5
6
7
8
9
10
11
12
13
void setup() {
pinMode(3,INPUT_PULLUP);//使用三号数字引脚探测DO
Serial.begin(115200); // 初始化串口,波特率为115200
}

void loop() {
// put your main code here, to run repeatedly:
int a = digitalRead(3); //变量a接受数字值
int b = analogRead(A0); //变量b接受模拟值
Serial.println(a);
Serial.println(b);
delay(100);
}

当开关灯亮时,一个模拟值,一个数字值。串口显示器显示如下

2.2光敏/热敏传感器的检测

这里以光敏传感器为例,热敏检查代码相同。
应用场景
1. 暗室监测
2. 熄屏亮灯
此类传感器与红外检查方法基本类似,将检测的模拟值,根据
实际情况转化为实际量,温度,光照等。

接线:DO接数字引脚2,AO接模拟引脚A0。
原理,这两个相当于电阻,电阻值随环境变化而变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
void setup() {
pinMode(2,INPUT_PULLUP);
Serial.begin(115200);
}

void loop() {
int a = digitalRead(2);
int b = analogRead(A0);
Serial.print("a=");
Serial.println(a);
Serial.print("b=");
Serial.println(b);
}

读到的模拟量需根据光敏电阻的参数转化为具体的亮度。

2.3超声波传感器

应用场合:
1.机器人避障和导航
2.距离测量
3.接近测量
4.智能垃圾桶

可3.3V或者5V输入

HC-SR04超声波测距模块可提供
2cm-60cm的非接触式距离感测功能,
测距精度可达高到3mm;模块包括超声波发射器、接收器与控制电路。
超声波测距模块——触发信号后发射超声波,当超声波投射到物体而反射回来时,模块输出——回响信号,以触发信号和回响信号间的时间差,来判定物体的距离。
使用
首先要安装NewPing 库。
注意,该库会调用定时器2资源。
网上的很多关于超声波模块的教程,不使用定时器,并且使用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
#include "NewPing.h"

#define TRIGGER_PIN 2 //引脚定义
#define ECHO_PIN 3
#define MAX_DISTANCE 60 //设置最大距离
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE);

float duration, distance;

int iterations = 5; //求平均的次数

void setup() {
Serial.begin (115200);
}

void loop() {
duration = sonar.ping_median(iterations); //多次测量时间求平均值
distance = (duration / 2) * 0.0343; //将测量的时间转换成距离

Serial.print("Distance = ");
if (distance >= MAX_DISTANCE || distance <= 2) {
Serial.println("Out of range");
}
else {
Serial.print(distance);
Serial.println(" cm");
}
}

经测试,结果如下

注意,当靠的太近时,距离不准,会增加。本代码对数据只进行求平均处理,可用其他方法进行滤波。减小误差。

2.4 4PIN-OLED 小屏幕的驱动

应用场景:
1. 显示输出
2. 多级菜单
3. 表情显示

可3.3V或者5V输入,一般使用有多重库函数,
这里使用u8g2库
SCL接A5,SDA接A4,示例如下

使用:
首先要安装u8g2库,该库可以驱动4pin,0.96寸OLED小屏幕

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
#include <U8g2lib.h>                                       //u8g2库

#define SCL A5 //引脚定义,可替换为数字引脚
#define SDA A4
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0,SCL,SDA); //配置构造函数
int a=66; //定义变量a=66

void setup()
{
u8g2.begin(); //启动u8g2驱动程序
u8g2.clearBuffer(); //清空显示屏缓存
}

void loop()
{
face(); //调用函数,显示图案
delay(1000); //持续一秒钟
heart(); //调用函数,显示英文字母和图案
delay(1000); //持续一秒钟
letter(a); //调用函数,显示中文和变量
delay(1000); //持续一秒钟
}

void face()
{
u8g2.clearBuffer(); //清空显示屏缓存

u8g2.drawCircle(56,40,8,U8G2_DRAW_LOWER_LEFT); //画四分之一圆,圆心坐标(56,44),半径8
u8g2.drawCircle(56,40,8,U8G2_DRAW_LOWER_RIGHT); //画四分之一圆
u8g2.drawCircle(72,40,8,U8G2_DRAW_LOWER_LEFT); //画四分之一圆
u8g2.drawCircle(72,40,8,U8G2_DRAW_LOWER_RIGHT); //画四分之一圆
u8g2.drawCircle(56,41,8,U8G2_DRAW_LOWER_LEFT); //加粗画四分之一圆
u8g2.drawCircle(56,41,8,U8G2_DRAW_LOWER_RIGHT); //加粗画四分之一圆
u8g2.drawCircle(72,41,8,U8G2_DRAW_LOWER_LEFT); //加粗画四分之一圆
u8g2.drawCircle(72,41,8,U8G2_DRAW_LOWER_RIGHT); //加粗画四分之一圆
u8g2.drawLine(40,18,20,30); //画斜线,两端点坐标分别是(40,18)(20,30)
u8g2.drawLine(88,18,108,30); //画斜线
u8g2.drawLine(40,17,20,29); //加粗画斜线
u8g2.drawLine(88,17,108,29); //加粗画斜线

u8g2.sendBuffer(); //加载以上内容
}

void heart()
{
u8g2.clearBuffer(); //清空显示屏缓存

u8g2.setFont(u8g2_font_open_iconic_human_2x_t); // 设置字体
u8g2.drawGlyph(58,30,66); // 画心,符号左下角坐标为(58,36),符号编号为66
u8g2.setFont(u8g2_font_unifont_t_chinese2); //设置字体
u8g2.drawUTF8(58+20,30,"ZFY"); //显示英文,左下角位置坐标为(78,30)
u8g2.drawUTF8(58-14-16,30,"JHR"); //显示英文,左下角位置坐标为(28,30)

u8g2.sendBuffer(); // 加载以上内容
}

void letter(int a)
{
u8g2.clearBuffer(); //清空显示屏缓存

//int8_t a=u8g2.getMaxCharHeight(); //获取最大高度
//int8_t b=u8g2.getMaxCharWidth(); //获取最大宽度

u8g2.setFont(u8g2_font_unifont_t_chinese2); //设置字体
u8g2.drawUTF8(20,17,"智能检测系统"); //显示文字,左下角位置坐标为(20,17)
u8g2.drawUTF8(50,34,":"); //显示:,左下角坐标为(50,34)
u8g2.drawUTF8(0,34,"位移为:");
u8g2.setCursor(64,34); //设置将要打印变量的左下角坐标
u8g2.drawUTF8(0,34,"位移为:");

u8g2.print(a); //打印变量a

u8g2.sendBuffer(); // 加载以上内容
}

屏幕显示还有ISP协议的,这种一般为7PIN,这里只介绍相对简单,接线少,使用I2C协议的4pin,0.96寸OLED屏幕。

2.5温湿度传感器

使用DHT11,温湿度传感器还有其他类型DHT22等
应用场景
1. 温湿度检测
2. 小闹钟配件

引脚正对着自己,从左至右。

使用,安装库Adafruit Unified Sensor及DHT-sensor-library两个库

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
// 需要以下 Arduino 库:
// - DHT 传感器库:https://github.com/adafruit/DHT-sensor-library
// - Adafruit 统一传感器库:https://github.com/adafruit/Adafruit_Sensor

#include "DHT.h"

#define DHTPIN 2 // 连接到 DHT 传感器的数字引脚
// Feather HUZZAH ESP8266 注意:使用引脚 3, 4, 5, 12, 13 或 14 --
// 引脚 15 也可以使用,但在上传程序时必须断开 DHT 连接。

// 取消注释你正在使用的传感器类型!
#define DHTTYPE DHT11 // DHT 11
//#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321
//#define DHTTYPE DHT21 // DHT 21 (AM2301)

// 将传感器的引脚 1(左边的)连接到 +5V
// 注意:如果使用 3.3V 逻辑电平的板子(如 Arduino Due),将引脚 1 连接到 3.3V 而不是 5V!
// 将传感器的引脚 2 连接到 DHTPIN
// 将传感器的引脚 3(右边的)连接到 GND(如果传感器有 3 个引脚)
// 将传感器的引脚 4(右边的)连接到 GND,并且引脚 3 空置(如果传感器有 4 个引脚)
// 将一个 10K 电阻从传感器的引脚 2(数据)连接到引脚 1(电源)

// 初始化 DHT 传感器。
// 注意:旧版本的库有一个可选的第三参数来调整较快处理器的时序。
// 现在不再需要这个参数,因为当前的 DHT 读取算法会自适应较快的处理器。
DHT dht(DHTPIN, DHTTYPE);

void setup() {
Serial.begin(9600);
Serial.println(F("DHTxx test!"));

dht.begin();
}

void loop() {
// Wait a few seconds between measurements.
delay(2000);

// Reading temperature or humidity takes about 250 milliseconds!
// Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
float h = dht.readHumidity();
// Read temperature as Celsius (the default)
float t = dht.readTemperature();
// Read temperature as Fahrenheit (isFahrenheit = true)
float f = dht.readTemperature(true);

// 检查是否有读取失败并提前退出(以便重试)
if (isnan(h) || isnan(t) || isnan(f)) {
Serial.println(F("Failed to read from DHT sensor!"));
return;
}

// Compute heat index in Fahrenheit (the default)
float hif = dht.computeHeatIndex(f, h);
// Compute heat index in Celsius (isFahreheit = false)
float hic = dht.computeHeatIndex(t, h, false);

Serial.print(F("Humidity: "));
Serial.print(h);
Serial.print(F("% Temperature: "));
Serial.print(t);
Serial.print(F("°C "));
Serial.print(f);
Serial.print(F("°F Heat index: "));
Serial.print(hic);
Serial.print(F("°C "));
Serial.print(hif);
Serial.println(F("°F"));
}

2.6HW504摇杆的使用

引脚说明
GND 地
5V 电源5V
SW 按键(数字量)
VRX X轴 (模拟量)
VRY Y轴 (模拟量)
1.输入电压范围:直流3.3V 至 5V。
2.输出信号:模块特设二路模拟输出和一路数字输出接口,输出值分别对应(X,Y)双轴偏移量,其类型为模拟量;按键表示用户是否在Z轴上按下,其类型为数字开关量
VRx,VRy (X、Y轴)为模拟输入信号,连接到模拟IO口A0A7。
VRx,VRy 的值:从 0 ~ 1023 分别代表 左
右,上~下。中间值为512。
SW (Z轴)是数字输入信号,连接到数字端口,并启用上拉电阻。
SW 的值:1代表未按下,0代表按下。
实验这里VRx接A0,VRy接A1,SW接D6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int value = 0;
void setup()
{
pinMode(6, INPUT_PULLUP);
Serial.begin(115200);
}

void loop(){
value = analogRead(A0);
Serial.print("X:");
Serial.print(value, DEC);
value = analogRead(A1);
Serial.print(" | Y:");
Serial.print(value, DEC);
value = digitalRead(6);
Serial.print(" | Z: ");
Serial.println(value, DEC);
delay(100);
}

2.7WIFI模块的使用

这里我们使用的是4Pin的ESP8266模块,这里只做简单的介绍,具体可后面学习ESP32.ESP8266模块内置TCP/IP协议,能够实现串口与WIFI之间的转换。该模块有三种工作WIFI模式,STA,AP,STA+AP.
STA,客户端模式,可连接其他WIFI,手机等设施实现对设备的远程连接。
AP,接入点模式,模块作为WIFi热点。
STA+AP,混合模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<SoftwareSerial.h>
#define rxPin 2
#define txPin 3

SoftwareSerial ESP8266(rxPin,txPin);
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
ESP8266.begin(115200);
}

void loop() {
// put your main code here, to run repeatedly:
if(ESP8266.available())
Serial.write(ESP8266.read());
else if(Serial.available())
ESP8266.write(Serial.read());
}

测试,在串口里输入AT,会返回OK;

2.8 其他

Arduino uno的模块还有蓝牙模块等,不过感觉有些鸡肋,使用uno的板子搭配这些模块,不如直接买esp32.所以一些无线通讯的模块就不介绍了。
Arduino的外设很丰富,这里就不多介绍了。

第三章 电机的驱动

3.1PWM波的认识

PWM(Pulse Width Modulation,脉宽调制)是一种用于模拟信号和功率控制的技术,通过改变脉冲的宽度来调节信号的平均电压或功率.
PWM 信号在一定周期内以高电平和低电平交替出现,通过改变高电平的持续时间(占空比)来实现对输出电压或功率的调节。
应用场景有电机控制,LED调光,电源转换和通信等。


PWM的重要参数有,
- 频率(Frequency):频率的选择取决于具体应用。例如,电机控制通常使用几千赫兹的频率,而 LED 调光可能使用几百赫兹到几千赫兹。
- 占空比(Duty Cycle):占空比是指高电平时间相对于一个周期的比例,通常用百分比表示
- 分辨率(Resolution):分辨率是指占空比可调节的最小步进,通常与控制器的位数有关。例如,8 位分辨率的 PWM 控制器可以将占空比调整到 256 个不同的级别。
- 电压幅度(Voltage Amplitude):PWM 信号的高电平和低电平的电压值。高电平通常是电源电压(如 5V 或 3.3V),低电平通常是 0V。

一般而言,要特别注意频率,频率过低,在驱动电机时可能出现异常,电机哮鸣。一般电机是需要较高频率的PWM信号控制.
单片机的PWM一般作为信号使用,驱动一些小功率LED时,可作为电源使用。

3.2 PWM波驱动小功率直流电机

我们在前面学习到,UNO的数字引脚上有几个特定引脚可以输出PWM波,分别为3,5,6,9,10,11.但是这种方式 PWM 信号的频率是固定的默认值,引脚上的 PWM 信号频率约为490 Hz,在 Uno板上,引脚5和6的频率约为980Hz。

1
2
3
4
5
6
7
8
9
# define analogPin 3 // 引脚命名
void setup()
{
pinMode(analogPin,OUTPUT);
}
void loop()
{
analogWrite(analogPin,100); // 输出PWM,占空比为 100/255
}

通过这种方法产生的PWM波频率过低,在驱动电机时可能出现异常。因此需要输出更高频率的PWM信号。
此时,小功率的直流电机一端接GND,一端接3号引脚也是可以驱动的,通过调节占空比即可调节速率。
为了使PWM的发生不占用主进程,这里使用定时器来操作。

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
/**
* 通过一个除数来分频给定的 PWM 引脚频率。
* 生成的频率等于基频除以给定的除数:
* – 基频:
* o 引脚 3、9、10 和 11 的基频是 31250 Hz。
* o 引脚 5 和 6 的基频是 62500 Hz。
* – 除数:
* o 引脚 5、6、9 和 10 可用的除数有:1、8、64、256 和 1024。
* o 引脚 3 和 11 可用的除数有:1、8、32、64、128、256 和 1024。
* PWM 频率成对关联。如果一对中的一个改变,另一个也会改变以匹配:
* – 引脚 5 和 6 在定时器0上成对
* – 引脚 9 和 10 在定时器1上成对
* – 引脚 3 和 11 在定时器2上成对
*
* 请注意,此函数会对其他使用定时器的内容产生副作用:
* – 改变引脚 3、5、6 或 11 可能会导致 delay() 和 millis() 函数停止工作。其他与定时相关的函数也可能受到影响。
* – 改变引脚 9 或 10 会导致 Servo 库无法正常工作。
*
* 感谢 Arduino 论坛的 macegr 提供的 PWM 频率除数文档。他的帖子可以在以下链接查看:
* http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1235060559/0#4
*/

void setup() {
// Set pin 6’s PWM frequency to 62500 Hz (62500/1 = 62500)
// Note that the base frequency for pins 5 and 6 is 62500 Hz
setPwmFrequency(6, 1);
analogWrite(6,124);
}
void loop() {
}
void setPwmFrequency(int pin, int divisor) {
byte mode;
if(pin == 5 || pin == 6 || pin == 9 || pin == 10) {
switch(divisor) {
case 1: mode = 0x01; break;
case 8: mode = 0x02; break;
case 64: mode = 0x03; break;
case 256: mode = 0x04; break;
case 1024: mode = 0x05; break;
default: return;
}
if(pin == 5 || pin == 6) {
TCCR0B = TCCR0B & 0b11111000 | mode;
} else {
TCCR1B = TCCR1B & 0b11111000 | mode;
}
} else if(pin == 3 || pin == 11) {
switch(divisor) {
case 1: mode = 0x01; break;
case 8: mode = 0x02; break;
case 32: mode = 0x03; break;
case 64: mode = 0x04; break;
case 128: mode = 0x05; break;
case 256: mode = 0x06; break;
case 1024: mode = 0x07; break;
default: return;
}
TCCR2B = TCCR2B & 0b11111000 | mode;
}
}

示波器测量频率为62.5KHZ,改变下面的124,改变占空比,占空比0-255。

analogWrite(6,124);

3.3 L298N的使用与双路电机驱动

供电方式:
- 7-12V电源走VCC供电,电源上的跳线帽连接,5v的位置不用接电源,该位置可输出一个5v,可用于给单片机供电。
- 当输入电压大于12v时,需要拔掉电源旁的跳线帽,5V端需要接入5v的电压,GND还是接GND
- 接入5V电源, L298N的12V和5V都接5V供电。

注意:GND不但要接驱动电源的GND,一定要从这里再引出一根GND和单片机或者系统的GND相连,使电压有参考电平

控制引脚

  1. IN1 & IN2 电机驱动器A的输入引脚,控制电机A转动及旋转角度
    IN1输入高电平HIGH,IN2输入低电平LOW,对应电机A正转
    IN1输入低电平LOW,IN2输入高电平HIGH,对应电机A反转
    IN1、IN2同时输入高电平HIGH或低电平LOW,对应电机A停止转动
    调速就是改变IN1、IN2高电平的占空比(需插上ENA处跳帽)

  2. IN3 & IN4 电机驱动器B的输入引脚,控制电机B转动及旋转角度
    IN3输入高电平HIGH,IN4输入低电平LOW,对应电机B正转
    IN3输入低电平LOW,IN4输入高电平HIGH,对应电机B反转
    IN3、IN4同时输入高电平HIGH或低电平LOW,对应电机B停止转动
    调速就是改变IN3、IN4高电平的占空比(需插上ENB处跳帽)

控制有两种方式,以一边电机为例:
1. ENA,引脚通过跳帽线与+5V连接,IN1悬空或接GND,IN2输入PWM信号。
2. ENA拔掉跳帽线,输入PWM信号,IN1接1,IN2接0。改变IN1与IN2的值就可以停止或正反转。

3.4舵机的驱动

普通舵机有3根线:GND(黑)、VCC(红)、Signal(黄),一般情况下,建议为舵机单独供电,此处实验为了图方便,用arduino为舵机供电。

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
/*
servo类下有以下成员函数
attach()//连接舵机
write()//角度控制
writeMicroseconds()//
read()//读上一次舵机转动角度
attached()//
detach()//断开舵机连接
*/
#include <Servo.h>
Servo myservo; //创建一个舵机控制对象
// 使用Servo类最多可以控制8个舵机
int pos = 0; // 该变量用与存储舵机角度位置
/*~~~~~~~~~~~~~~~~~~~~~~~~~~华丽的分割线~~~~~~~~~~~~~~~~~~~~~~~~~~ */
void setup()
{
Serial.begin(9600);
myservo.attach(9); // 该舵机由arduino第九脚控制
}
/*~~~~~~~~~~~~~~~~~~~~~~~~~~华丽的分割线 ~~~~~~~~~~~~~~~~~~~~~~~~~~ */
void loop()
{
myservo.write(0); // 复位
for(pos = 0; pos < 180; pos += 1) // 从0度到180度运动
{ // 每次步进一度
myservo.write(pos); // 指定舵机转向的角度
delay(15); // 等待15ms让舵机到达指定位置

}
for(pos = 180; pos>=1; pos-=1) //从180度到0度运动
{
myservo.write(pos); // 指定舵机转向的角度
delay(15); // 等待15ms让舵机到达指定位置
Serial.println(myservo.read());
}
}

3.5 其他

还有无刷电机,步进电机,arduino驱动这些电机设备时,一般不直接驱动,中间都会连接一个驱动模块,上文的L298N就是一种驱动模块。

第四章 成品制作

4.1可以制作哪些?如何制作?

个人DIY时,除了学习单片机外,还需要学习PCB焊接和3D打印的一些知识,并且熟悉一些常见的DIY材料,丙烯颜料,环氧树脂等,以及钳子,等常见工具的使用。当然最最最重要的是热爱。
基于UNO,我们可以做出很多有意思的作 品,小时钟,个性灯等,在开源社区有很多有意思的作品,可以自行搜索。