全志H616开发学习笔记(四):基本IO练习与应用

基本IO练习与应用

蜂鸣器控制

首先看一下wiringOP库中关于IO控制的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//blink.c
#include <stdio.h>
#include <wiringPi.h>
#define NUM 17 //26pin

int main (void)
{
int i = 0;
wiringPiSetup () ;
for (i = 0; i < NUM; i++)
pinMode (i, OUTPUT) ;
for ( ;; )
{
for (i = 0; i < NUM; i++)
digitalWrite (i, HIGH) ; // On
delay (2000) ; // mS
for (i = 0; i < NUM; i++)
digitalWrite (i, LOW) ; // Off
delay (2000) ;
}
return 0;
}

可以看到:关于IO口怎么控制,其实和ardunio的操作有点类似,先要将物理针脚控制定义为逻辑口,然后根据外设的电气特性,加上一定的逻辑,使用digitalWrite函数给针脚赋值(0、1)就行了

再简单回顾一下蜂鸣器的三个引脚:VCC、GND、IO口。这里我们VCC接上开发板的3.3V(物理一号针脚),GND接GND,IO口接0号口(物理3号针脚)

那我们基于这个示例编写一个简单的蜂鸣器控制程序,使其发出“滴滴”声

点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//beep.c
#include <stdio.h>
#include <wiringPi.h>
#include <unistd.h>

#define BEEP 0

int main (void)
{
wiringPiSetup () ;
pinMode (BEEP, OUTPUT) ;
while(1){
usleep(100000);//微妙为单位,此处设置100ms
digitalWrite(BEEP,Low);
usleep(100000);
digitalWrite(BEEP,HIGH);
}
return 0;
}

文件编写后,如果尝试直接使用gcc编译,会因为无法识别我们所使用的HIGHLOWOUTPUT等表达而编译失败。我们不妨看一下官方示例中是如何编译的:

1
2
3
4
5
6
7
8
9
10
……
#DEBUG = -g -O0
DEBUG = -O3
CC = gcc
INCLUDE = -I/usr/local/include
CFLAGS = $(DEBUG) -Wall $(INCLUDE) -Winline -pipe

LDFLAGS = -L/usr/local/lib
LDLIBS = -lwiringPi -lwiringPiDev -lpthread -lm -lcrypt -lrt
……

上面是官方示例文件Makefile,根据LDIBS可以得知我们编译时加上-lwiringPi -lwiringPiDev -lpthread -lm -lcrypt -lrt后缀即可。但是每次都这样编译是不是太麻烦了呢?

我们写一个简易的自动编译脚本:

1
gcc $1 -lwiringPi -lwiringPiDev -lpthread -lm -lcrypt -lrt -o beep

然后执行此脚本./build.sh beep.c,程序就编译好了。

shell脚本中的$+数字表示执行命令的第x个参数,$1表示的是第一个参数beep.c

这只是一种最简单的方式,前面还介绍过此种方式的初级用法:自动化反复测试,取多次样本。

运行编译得到的程序,蜂鸣器就会开始快速地“滴滴”,终止程序后我们再检查一下gpio的情况:

可以看到,0号口的Name从OFF变为了OUT,并且值为1(有输出0的情况)

Vim缩进小技巧

复制到剪贴板上的代码右键粘贴到vim编辑器,有时会发现原有的缩进格式被破坏掉。

如果希望保留原有的缩进,可以在命令模式下输入:set paste,这样编辑器就不会更改缩进的长度

如果自己编写的代码需要自动批量缩进,可使用gg = Gshift G自动格式化

默认情况下,vim中Tab长度为8,可通过/etc/vim/vimrc设置tabstopshiftwidth为4修改。

超声波测距

超声波模块回顾

实物图:

  • 测距信号触发方法:
    Trig,给Trig端口至少10us的高电平

  • 怎么知道开始发超声波了:
    Echo信号,由低电平跳转到高电平,表示开始发送波

  • 怎么知道接收了返回波:
    Echo,由高电平跳转回低电平,表示波回来了

  • 怎么算时间:
    Echo引脚维持高电平的时间,即:
    波发出去的那一下,开始启动定时器;
    波回来的拿一下,我们开始停止定时器,计算出中间经过多少时间

  • 怎么算距离:
    距离=速度(340m/s)*时间/2

信号触发和接收的时序逻辑:

时间计算

以前在51单片机上,由于环境受限,没有操作系统,因此获取不到时间,因此也就无法通过前后时间差计算距离。

现在用香橙派就可以通过系统APIgettimeofday()获取时间(以1970年1月1日为基准的格林威治时间)。

原型:

1
2
3
4
5
6
7
#include<sys/time.h>
int gettimeofday(struct timeval *tv,struct timezone *tz );
struct timeval
{
long tv_sec;/*秒*/
long tv_usec;/*微秒*/
};

gettimeofday()会把目前的时间用tv 结构体返回,当地时区的信息则放到tz所指的结构中

一个测试程序性能的简单demo,直观但不精确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <sys/time.h>
#include <stdio.h>
//int gettimeofday(struct timeval *tv,struct timezone *tz )
void mydelay()
{
int i,j;
for(i=0;i<100;i++){
for(j=0;j<1000;j++);
}
}
int main()
{
struct timeval startTime;
struct timeval stopTime;
gettimeofday(&startTime,NULL);
mydelay();
gettimeofday(&stopTime,NULL);
long diffTime = 1000000*(stopTime.tv_sec - startTime.tv_sec) +(stopTime.tv_usec - startTime.tv_usec);
printf("数数100000,耗时%ldus\n",diffTime);
return 0;
}

可以看到,该程序运行十次,耗时均在1000us,也就是1ms以上。但由于进程的竞争原因,其他程序运行时并不会等这十万数字数完了才开始执行,而是执行过程中会有不可预见的,随机的中断,因此每次耗时都不一致

测距实现

点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include<unistd.h>
#include<sys/time.h>
#include<wiringPi.h>
#include<stdlib.h>
#include<stdio.h>

#define Trig 0
#define Echo 1

double getDistance(){

double diffTime;
double distance;
struct timeval start;
struct timeval stop;

pinMode(Trig, OUTPUT);
pinMode(Echo, INPUT);

digitalWrite(Trig,LOW);//初始化置低电平
usleep(5);

digitalWrite(Trig,HIGH);//开始置高电平
usleep(10);

digitalWrite(Trig,LOW);//Trig恢复

while(digitalRead(Echo)==0);//没有高电平,还没发波
gettimeofday(&start,NULL);//收到高电平,开始计时
while(digitalRead(Echo)==1);//变为低电平,收到反射的波了
gettimeofday(&stop,NULL);

diffTime = 1000000*(stop.tv_sec - start.tv_sec)+(stop.tv_usec - start.tv_usec);
distance = (double)diffTime/1000000 * 34000/2;
return distance;
}
int main(){
double distance;
if(wiringPiSetup() == -1){//wiringPi初始化
fprintf(stderr,"%s","initWiringPi error");
exit(-1);
}

while(1){
distance=getDistance();
printf("distace is:%lf cm\n", distance);
usleep(1000000);//1000ms
}
return 0;
}

SG90舵机

简单回顾一下舵机的基本原理:通过向PWM信号线灌入一定占空比PWM(Pulse Width Modulation,脉冲宽度调制)信号,控制舵机上方的转向

以20ms为单位时间,则高电平持续时间、波形图与转动角度的关系如下表所示

高电平持续时间 低电平持续时间 波形图 角 度
0.5ms 19.5ms


0
1.0ms 19ms


45
1.5ms 18.5ms


90
2.0ms 18ms 如上图推理 135
2.5ms 17.5ms 如上图推理 180

在51单片机上,我们通过中断和计数器实现计时,在ARM Linux上,通过系统时间来定时。

Linux定时器实现

实现定时器,通过itimerval结构体以及函数setitimer产生的信号,系统随之使用signal信号处理
函数来处理产生的定时信号。从而实现定时器。
先看itimerval的结构体

1
2
3
4
5
6
7
struct itimerval
{
/* Value to put into `it_value' when the timer expires. */
struct timeval it_interval;
/* Time to the next timer expiration. */
struct timeval it_value;
};
  • it_interval:计时器的初始值,一般基于这个初始值来加或者来减,看控制函数的参数配置
  • it_value:程序跑到这之后,多久启动定时器
1
2
3
4
5
6
7
8
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
int setitimer (__itimer_which_t __which,
const struct itimerval *__restrict __new,
struct itimerval *__restrict __old)

setitimer()将value指向的结构体设为计时器的当前值,如果ovalue不是NULL,将返回计时器原有值

which的三种类型:

  • ITIMER_REAL //数值为0,计时器的值实时递减,发送的信号是SIGALRM。
  • ITIMER_VIRTUAL //数值为1,进程执行时递减计时器的值,发送的信号是SIGVTALRM。
  • ITIMER_PROF //数值为2,进程和系统执行时都递减计时器的值,发送的信号是SIGPROF。
    很明显,这边需要捕获对应的信号进行逻辑相关处理 signal(SIGALRM,signal_handler);

返回说明:
成功执行时,返回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
/*该代码实现的功能是: 1s后开启定时器,然后每隔1s向终端打印hello。*/
#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.h>
static int i;
void signal_handler(int signum)
{
i++;
if(i == 2000){
printf("hello\n");
i = 0;
}
}
int main()
{
struct itimerval itv;
//!!!!!!!!!!!!!!!!!!!!!!!!这里对应课程部分的代码,两个时间戳是做反了,参考下节课!!!!!!!!!!!!!!!!!!!!!
//设定定时时间,500ms
itv.it_interval.tv_sec = 0;
itv.it_interval.tv_usec = 500;
//设定开始生效,启动定时器的时间
itv.it_value.tv_sec = 1;
itv.it_value.tv_usec = 0;
//!!!!!!!!!!!!!!!!!!!!!!!!这里对应课程部分的代码,两个时间戳是做反了,参考下节课!!!!!!!!!!!!!!!!!!!!!
//设定定时方式
if( setitimer(ITIMER_REAL, &itv, NULL) == -1){
perror("error");
exit(-1);
}
//信号处理
signal(SIGALRM,signal_handler);
while(1);
return 0;
}

三个重点:

  • 定时器结构体的设置,包括定时时间和开始生效时间。在试验的时候可以根据是调节时间长短并运行,观察实际时间判断避免弄反
  • settimer()启动定时器
  • 信号捕获

舵机控制实现

基于上面的定时器,获取键盘输入,并控制舵机角度。

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
#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.h>
#include <wiringPi.h>

#define SG90 5

int angle;
static int i=0;

void signal_handler(int signum)
{
if(i <= angle){//必须重复操作digitalWrite,否则由于20ms太短,舵机会反复抽搐
digitalWrite(SG90, HIGH);
printf("HIGH\n");
} else {
digitalWrite(SG90, LOW);
}
if(i == 40 ){//40个0.5mm,就是20ms
i = 0;
}
i++;
}
int main()
{
wiringPiSetup();
pinMode(SG90,OUTPUT);

struct itimerval itv;
itv.it_interval.tv_sec = 0;
itv.it_interval.tv_usec = 500;

itv.it_value.tv_sec = 1;
itv.it_value.tv_usec = 0;

if( setitimer(ITIMER_REAL, &itv, NULL) == -1){
perror("error");
exit(-1);
}

signal(SIGALRM,signal_handler);
while(1){
printf("请输入角度:0-0°;1-45°;2-90°;3-135°;4-180°\n");
scanf("%d",&angle);
}
return 0;
}

Oled应用—IIC协议

OledIIC基础回顾

Oled模块有四个针脚,其中两个数据相关的为SDASCL,使用IIC协议进行数据交互。IIC全称总集成电路总线,是Philips公司开发的一种两线式串行总线,用于连接微控制器及外围设备。IIC是全双工同步通信方式(两方可同时收发)

在51单片机部分,对IIC协议通信的信号特性、具体过程、时序逻辑等做了较为详细的介绍和练习,而在Linux内核中,这部分已经集成了,无需开发者去实现。

如上如所示,在/dev目录下存在两个i2c设备文件(Linux下,设备也是以文件形式管理)

OrangePiIIC开发准备

根据香橙派官方提供的原理图可以得知,OrangePi Zero 2可用的I2Ci2c-3,对应接口为物理针脚对应3和5.

首先将Oled模块按照SDA-SDASCL-SCL的方式接到开发板上,然后安装i2c-tools

1
2
sudo apt update
sudo apt install i2c-tools

使用i2cdetect命令检查模块是否正常接入:

然后就可以进行开发了。

Oled官方示例代码阅读

如下是WiringOP示例代码中的oled_demo.c,我们先根据这个文件来学习一下Oled如何控制

源码(推荐使用SourceInsight进行阅读,可以快速跳转相关方法的声明和实现):

点击展开
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
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>

#include "oled.h"
#include "font.h"

int oled_demo(struct display_info *disp) {
int i;
char buf[100];

//putstrto(disp, 0, 0, "Spnd spd 2468 rpm");
// oled_putstrto(disp, 0, 9+1, "Spnd cur 0.46 A");
//oled_putstrto(struct display_info *disp, uint8_t x, uint8_t y, char *str)
oled_putstrto(disp, 0, 9+1, "Welcome to");
disp->font = font1;
// oled_putstrto(disp, 0, 18+2, "Spnd tmp 53 C");
oled_putstrto(disp, 0, 18+2, "----OrangePi----");
disp->font = font2;
// oled_putstrto(disp, 0, 27+3, "DrvX tmp 64 C");
oled_putstrto(disp, 0, 27+3, "This is 0.96OLED");
oled_putstrto(disp, 0, 36+4, "");
oled_putstrto(disp, 0, 45+5, "");
disp->font = font1;
// oled_putstrto(disp, 0, 54, "Total cur 2.36 A");
oled_putstrto(disp, 0, 54, "*****************");
oled_send_buffer(disp);

disp->font = font3;
for (i=0; i<100; i++) {
sprintf(buf, "Spnd spd %d rpm", i);
oled_putstrto(disp, 0, 0, buf);
oled_putstrto(disp, 135-i, 36+4, "===");
oled_putstrto(disp, 100, 0+i/2, ".");
oled_send_buffer(disp);
}
//oled_putpixel(disp, 60, 45);
//oled_putstr(disp, 1, "hello");

return 0;
}

void show_error(int err, int add) {
//const gchar* errmsg;
//errmsg = g_strerror(errno);
printf("\nERROR: %i, %i\n\n", err, add);
//printf("\nERROR\n");
}

void show_usage(char *progname) {
printf("\nUsage:\n%s <I2C bus device node >\n", progname);
}

int main(int argc, char **argv) {
int e;
char filename[32];
struct display_info disp;

if (argc < 2) {
show_usage(argv[0]);

return -1;
}

memset(&disp, 0, sizeof(disp));
sprintf(filename, "%s", argv[1]);
disp.address = OLED_I2C_ADDR;
disp.font = font2;

e = oled_open(&disp, filename);

if (e < 0) {
show_error(1, e);
} else {
e = oled_init(&disp);
if (e < 0) {
show_error(2, e);
} else {
printf("---------start--------\n");
if (oled_demo(&disp) < 0)
show_error(3, 777);
printf("----------end---------\n");
}
}

return 0;
}
  • 整个程序从main函数开始看,首先声明了一个的结构体,该结构体的内容:

    1
    2
    3
    4
    5
    6
    struct display_info {
    int address;
    int file;
    struct font_info font;
    uint8_t buffer[8][128];
    };

    用于存放设备地址、文件描述符、显示字体相关信息、以及最重要的8*128大小的unit8_t格式的数据缓冲区。

  • 然后对程序调用的参数进行判断,如果没有指定i2c设备直接退出;

  • 将结构体的空间清零初始化,根据运行参数设置各项内容;

  • 调用oled_open打开设备;

  • 调用oled_init初始化数据缓存等内容;

  • oled_demo,显示demo内容

    • 上方第一个方法,主要通过disp指针设置显示字体和通过putstrto方法向displaybuffer写入内容,
    • 写完后通过oled_send_buffer,将displaybuffer拷贝到一个一维数组packet(每一个元素对应8个字节,原buffer中的一行)
    • oled_send_buffer最终调用oled_sed方法,通过write命令,将packet的内容真正写入设备文件,(通过驱动层的实现,具体可以参考wiringPi.c,结合用户手册和原理图了解)从而显示到屏幕上

总结来看,其实和原来学过的文件操作没有多大区别,都是首先打开文件、通信初始化,然后写入/读取数据、最后关闭文件。

下面是外部引用的oled.cdemo中的很多方法在此实现:

点击展开
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
129
130
131
132
133
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>

// real-time features
#include <sys/mman.h>
#include <sched.h>

#include "oled.h"
#include "font.h"
#include <wiringPiI2C.h>

int oled_close(struct display_info *disp) {
if (close(disp->file) < 0)
return -1;
return 0;
}

void cleanup(int status, void *disp) {
oled_close((struct display_info *)disp);
}

int oled_open(struct display_info *disp, char *filename) {
disp->file = wiringPiI2CSetupInterface (filename, disp->address);
if (disp->file < 0)
return -1;
return 0;
}

// write commands and data to /dev/i2c*
int oled_send(struct display_info *disp, struct sized_array *payload) {
if (write(disp->file, payload->array, payload->size) != payload->size)
return -1;
return 0;
}

int oled_init(struct display_info *disp) {
struct sched_param sch;
int status = 0;
struct sized_array payload;

payload.size = sizeof(display_config);
payload.array = display_config;

status = oled_send(disp, &payload);
if (status < 0)
return 666;

memset(disp->buffer, 0, sizeof(disp->buffer));
return 0;
}

// send buffer to oled (show)
int oled_send_buffer(struct display_info *disp) {
struct sized_array payload;
uint8_t packet[129];
int index;

for (index = 0; index < 8; index++) {
packet[0] = OLED_CTRL_BYTE_DATA_STREAM;
memcpy(packet + 1, disp->buffer[index], 128);
payload.size = 129;
payload.array = packet;
oled_send(disp, &payload);
}
return 0;
}

// clear screen
void oled_clear(struct display_info *disp) {
memset(disp->buffer, 0, sizeof(disp->buffer));
oled_send_buffer(disp);
}

// put string to one of the 8 pages (128x8 px)
void oled_putstr(struct display_info *disp, uint8_t line, uint8_t *str) {
uint8_t a;
int slen = strlen(str);
uint8_t fwidth = disp->font.width;
uint8_t foffset = disp->font.offset;
uint8_t fspacing = disp->font.spacing;
int i=0;

for (i=0; i<slen; i++) {
a=(uint8_t)str[i];
if (i >= 128/fwidth)
break; // no text wrap
memcpy(disp->buffer[line] + i*fwidth + fspacing, &disp->font.data[(a-foffset) * fwidth], fwidth);
}
}

// put one pixel at xy, on=1|0 (turn on|off pixel)
void oled_putpixel(struct display_info *disp, uint8_t x, uint8_t y, uint8_t on) {
uint8_t pageId = y / 8;
uint8_t bitOffset = y % 8;
if (x < 128-2) {
if (on == 1)
disp->buffer[pageId][x] |= (1<<bitOffset);
else
disp->buffer[pageId][x] &= ~(1<<bitOffset);
}
}

// put string to the buffer at xy
void oled_putstrto(struct display_info *disp, uint8_t x, uint8_t y, char *str) {
uint8_t a;
int slen = strlen(str);
uint8_t fwidth = disp->font.width;
uint8_t fheight = disp->font.height;
uint8_t foffset = disp->font.offset;
uint8_t fspacing = disp->font.spacing;
int i=0;
int j=0;
int k=0;

for (k=0; k<slen; k++) {
a=(uint8_t)str[k];
for (i=0; i<fwidth; i++) {
for (j=0; j<fheight; j++) {
if (((disp->font.data[(a-foffset)*fwidth + i] >> j) & 0x01))
oled_putpixel(disp, x+i, y+j, 1);
else
oled_putpixel(disp, x+i, y+j, 0);
}
}
x+=fwidth+fspacing;
}
}

示例程序现实的效果:

基于示例进行精简,显示自定义内容

下方是精简后的代码,只有oled_showshow_errorshow_usage三个子函数,只需在oled_show方法中修改显示的内容和起始位置即可。

其实跟完了上面的部分,单纯的内容显示看起来就没啥技术含量了。上面的示例代码中“===”还有字符移动的效果,实质是利用了像素点不写入新内容就不会更新的特点,先将边框固定部分显示出来,然后随时间变化不断写入变化部分的内容即可。

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
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>
#include "oled.h"
#include "font.h"
int oled_show(struct display_info *disp) {
int i;
char buf[100];
oled_putstrto(disp, 0, 9+1, "Say Hello To The World");
disp->font = font2;
oled_putstrto(disp, 0, 20, " ---Awesome Yuan--- ");
disp->font = font2;
oled_send_buffer(disp);
return 0;
}
void show_error(int err, int add) {
printf("\nERROR: %i, %i\n\n", err, add);
}
void show_usage(char *progname) {
printf("\nUsage:\n%s <I2C bus device node >\n", progname);
}
int main(int argc, char **argv) {
int e;
char filename[32];
struct display_info disp;
if (argc < 2) {
show_usage(argv[0]);
return -1;
}
memset(&disp, 0, sizeof(disp));
sprintf(filename, "%s", argv[1]);
disp.address = OLED_I2C_ADDR;
disp.font = font2;
e = oled_open(&disp, filename);
e = oled_init(&disp);
oled_show(&disp);
return 0;
}

全志H616开发学习笔记(四):基本IO练习与应用
https://dockingyuan.top/2022/11/28/OrangePiZero2/4-基本IO练习与应用/
作者
Yuan Yuan
发布于
2022年11月28日
许可协议