韦东山嵌入式Liunx入门应用开发一(含课后作业、代码详解)


参考视频 Linux快速入门到精通视频
基于 IMX6ULL-PRO,含部分自己原创编写出来的 课后作业
本人学习完韦老师的视频,因此来复习巩固,写以笔记记之。
参考资料:01_嵌入式Linux应用开发完全手册V5.1_IMX6ULL_Pro开发板.pdf

一、HelloWorld和GCC编译

编译hello.c程序参考前文开发板第一个APP实验。需要使用交叉编译工具链,程序才可以在ARM板子上运行。

arm-buildroot-linux-gnueabihf-gcc -o hello hello.c

参考文章GCC编译过程更为详尽
一个C/C++ 文件要经过预处理、编译、汇编和链接等4步才能变成可执行文件。
在这里插入图片描述

1-1 预处理 -E

C/C++源文件中,以“#”开头的命令被称为预处理命令,如包含命令
#include ”、宏定义命令 “#define”、条件编译命令 “#if ”、 “#ifdef ”等。
预处理就是将要包含include的文件插入原文件中、将宏定义展开、根据条件
编译命令选择要使用的代码,最后将这些东西输出到一个.i 文件中等待进一
步处理。

1-2 编译 -S

编译就是把C/C++代码(比如上述的“.i 文件)翻译成汇编代码(.S) ,所用到的工具为cc1命令。

1-3 汇编 -c

将输出的汇编代码翻译成符合一定格式的机器代码(.o),在Linux 系统 上一般表现为ELF目标文件 (OBJ 文件 )),用到的工具为 as命令。

1-4 链接

链接就是将上步生成的OBJ文件和系统库的OBJ文件 、 库文件链接起来,最终生成了可以在特定平台运行的可执行文件 ,用到的工具为ld 或 collect2命令。

二、Makefile的使用

make命令根据文件更新的时间戳来决定哪些文件需要重新编译,这使得可
以避免编译已经编译过的、没有变化的程序,可以大大提高编译效率。
略略略…以后更新

三、文件IO

在Linux系统中,一切皆文件。不仅是普通的文件,包括目录、字符设备、块设备、套接字等,所有的操作,都是通过文件 IO来操作的。实现这一行为的基础,正是Linux的虚拟文件系统机制。
虚拟文件系统(Virtual File System,简称VFS)是Linux内核的子系统之一,它为用户程序提供文件和文件系统操作的统一接口,屏蔽不同文件系统的差异和操作细节。借助VFS可以直接使用open()、read()、write()这样的系统调用操作文件,而无须考虑具体的文件系统和实际的存储介质。

3-1 open()函数
int open(const char *pathname, int flags, mode_t mode);		 /*函数原型*/
int close(int fd);

pathname:文件路径名
flags:O_RDONLY、O_WRONLY、O_RDWR、O_NONBLOCK(设置非阻塞)
mode :参数3使用的前提是参数2指定了 O_CREAT;取值8进制数,用来描述文件的访问权限,0664(rwx)
返回值:①成功则打开文件所得到对应的文件描述符(整数);②失败:-1

例:fd= open(argv[1], O_RDONLY);
3-2 read()函数
ssize_t read(int fd, void *buf, size_t count);		/*函数原型*/

fd:文件描述符
buf:存数据的缓冲区
count:缓冲区大小
返回值:①成功(>0):读到的字节数;②失败 -1;③0:读到文件末尾
-1:并且设置errno = EAGIN 或 EWOULDBLOCK:说明不是read失败,而是read在以非阻塞方式读一个设备文件(网络文件), 并且文件无数据。

例:len = read(fd, buf, 1024);
3-3 write()函数
ssize_t wtite(int fd, const void *buf, size_t count);		/*函数原型*/

fd:文件描述符
buf:待写出数据的缓冲区
count:read实际返回的字节数
返回值:①成功:实际写入的大小;②失败: -1

例:write(fd, buf, len)
3-4 mmap()函数
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
int munmap(void *addr, size_t length);  /*释放映射区*/

addr:指定映射区的首地址。通常传NULL,表示让系统自动分配
length:共享内存映射区的大小(<=文件的实际大小)
prot:共享内存映射区的读写属性,PROT_READ、 PROT_WRITE、 PROT_READ | PROT_WRITE
flags:MAP_SHARED(进程间通信)、MAP_PRIVATE
fd:用于创建共享内存映射区的那个文件的文件描述符
offset:默认0,表示映射文件全部。偏移位置
返回值:①成功:映射区的首地址②失败:MAP_FAILED

buf = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);

传统的读写文件方式中,对于修改文件内容的步骤:①把文件内容读入到内存中 ②修改内存中的内容 ③把内存的数据写入到文件中
使用mmap系统调用可以将用户空间的虚拟内存地址与文件进行映射绑定,操作系统可以将一个文件的内容映射到进程的地址空间中,对映射后的虚拟内存地址采用指针的方式读写操作这一段内存就如同对文件进行读写操作一样。应用程序就可以直接访问内存中的数据,而不需要通过传统的文件读写接口。
多个进程可以通过mmap共享同一文件的映射,从而实现进程间的共享内存通信。当一个进程对映射的内容进行修改时,其他映射同一文件的进程也能看到这些变化。

3-5 ioctl()函数
int ioctl(int fd, unsigned long request, ...);

fd:文件描述符
request:驱动程序交互的命令,设备驱动将根据 cmd 执行对应操作
:可变参数arg,根据request命令,设备驱动程序返回输出的数据
ioctl 是设备驱动程序中设备控制接口函数,一个字符设备驱动通常会实现设备打开、关闭、读、写等功能。

3-6 系统调用函数进入内核

open/read等函数从用户态调用API触发异常进入内核,sys _read/open 会首先根据参数判断文
件的类型,然后根据不同的文件类型去找不同的设备驱动,继而进行读写或者输入输出控制。最后调用的 sys_call_table 的函数指针数组。

四、Framebuffer应用编程详解

在Linux系统中通过Framebuffer 驱动程序来控制LCD;Framebuffer就是一块内存,里面保存着一帧图像,Framebuffer中保存着一帧图像的每一个像素颜色值。

4-1 LCD的操作原理

① 驱动程序设置好 LCD控制器:根据LCD的参数设置LCD控制器的时序、信号极性;根据LCD分辨率 、BPP分配Framebuffer。
② APP使用ioctl函数获得LCD分辨率、 BPP。
③ APP通过mmap函数映射Framebuffer,在Framebuffer中写入数据。

4-2 Framebuffer的作用

Framebuffer显示原理图如下
在这里插入图片描述
软件层面分析:framebuffer 起着承上启下的作用。向上,为应用层提供通用系统调用open(),ioctl(),mmap();向下,连接ARM的LCD控制器,直接对硬件进行操作。
硬件层面分析:用户只需要将数据写到framebuffer,硬件会自动刷新到屏幕上。

部分代码注释解析

void lcd_put_pixel(int x, int y, unsigned int color)    /*描点函数*/
{
	unsigned char *pen_8 = fb_base+y*line_width+x*pixel_width; 	/*计算要写入的像素的内存位置 8位像素指针*/
	unsigned short *pen_16;	
	unsigned int *pen_32;	

	unsigned int red, green, blue;	

	pen_16 = (unsigned short *)pen_8;
	pen_32 = (unsigned int *)pen_8;  /*类型转换为32位的指针*/

	switch (var.bits_per_pixel){
		case 8:{
			*pen_8 = color;
			break;
		}
		case 16:{
			/* 565 */
			red   = (color >> 16) & 0xff;
			green = (color >> 8) & 0xff;
			blue  = (color >> 0) & 0xff;
			color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
			*pen_16 = color;
			break;
		}
		case 32:{
			*pen_32 = color;  /*传入颜色值 经测试屏幕为32位像素 直接跳转到这执行*/
			//printf("%d\n", var.bits_per_pixel);
			break;
		}
		default:{
			printf("can't surport %dbpp\n", var.bits_per_pixel);
			break;
		}
	}
}

int main(int argc, char **argv)
{
	int i;
	
	fd_fb = open("/dev/fb0", O_RDWR); 	/*打开设备节点*/
	if (fd_fb < 0){
		printf("can't open /dev/fb0\n");
		return -1;
	}
	if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var)){	/*FBIOGET_VSCREENINFO 获取屏幕的可变信息:屏幕分辨率,像素格式*/
		printf("can't get var\n");
		return -1;
	}

	line_width  = var.xres * var.bits_per_pixel / 8; 	/*计算每行像素数据所占的字节数*/
	pixel_width = var.bits_per_pixel / 8; 			/*每个像素所占的字节数*/
	screen_size = var.xres * var.yres * var.bits_per_pixel / 8; 	/*整个屏幕的像素数据占用的总字节数*/
	fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
	if (fb_base == (unsigned char *)-1){
		printf("can't mmap\n");
		return -1;
	}

	/* 清屏: 全部设为白色 */
	memset(fb_base, 0xff, screen_size);

	/* 随便设置出100个为红色 */
	for (i = 0; i < 100; i++)
		//lcd_put_pixel(var.xres/2+i, var.yres/2, 0xFF0000); 	/*0x00RRGGBB  水平生成*/
		lcd_put_pixel(var.xres/2, var.yres/2+i, 0x00FF00);		/*实验结果:垂直生成 绿色竖线*/
	
	munmap(fb_base , screen_size); /*释放映射区*/
	close(fd_fb); /*关闭文件*/
	
	return 0;	
}

五、文字显示

5-1 字符编码

字符以编码的形式存储。编码标准:
① ASCII编码,使用一个字节来表示一个字符,只用到其中的 7位,最高位恒为0。
② ANSI编码,对于ASCII字符仍使用一个字节来表示 (BIT7是 0),对于非ASCII字符一般使用2个字节来表示,非与地区相关,中国大陆默认GB2312,因此不适于跨地区交流。
③ UNICODE编码,采用UTF-8的格式,"中"字对应的UTF-8为0xe4 b8 ad解析编码值为0x4e2d。

5-2 ASCII字符的点阵显示
(1) 课后作业1 指定字符颜色为红色
//lcd_put_ascii(var.xres/2, var.yres/2, 'A');  /*在LCD指定位置上显示一个8*16的字符*/
void lcd_put_ascii(int x, int y, unsigned char c)
{
	unsigned char *dots = (unsigned char *)&fontdata_8x16[c*16];  /*指针指向目标字符的首地址*/
	int i, b;
	unsigned char byte;

	for (i = 0; i < 16; i++){
		byte = dots[i];				/*依次取出一个字节*/
		for (b = 7; b >= 0; b--){
			if (byte & (1<<b)){		/*根据每位来显示黑、白色*/
				/* show */
				//lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 调用描点函数 */
				lcd_put_pixel(x+7-b, y+i, 0xff0000); /* 红 课后作业1*/
			}
			else{
				/* hide */
				lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */
			}
		}
	}
}
(2) 课后作业2 输出字符串(简单实现)
	static char str[] = "hello world!";
	static int i = 0;
	...
	...
	for(i = 0; i <= strlen(str); i++){
		lcd_put_ascii(var.xres/2 + i*8, var.yres/2, str[i]); /*在屏幕中间显示8*16的字母A*/
		printf("%d",strlen(str));
	}
	//lcd_put_ascii(var.xres/2, var.yres/2, 'A'); /*在屏幕中间显示8*16的字母A*/

中文字符的点阵显示章节启发,回头来看lcd_put_str()函数实现,只需要修改lcd_put_ascii最后一个形参即可,str指针用数组for循环遍历即可,下一章有lcd_put_str()函数的实现。

void lcd_put_ascii(int x, int y, unsigned char c)
void lcd_put_str(int x, int y, unsigned char *str)
5-3 中文字符的点阵显示

在编译程序时,可以指定编码格式,一般为2种,GB2312和UTF-8。GCC默认C程序的编码方式为UTF8。

-finput-charset=GB2312 
-finput-charset=UTF-8

汉字采用HZK16(16*16)点阵字库。每个汉字用32字节来描述,以GB2312编码值来查找点阵。它是常用汉字的ASCII字库一样,每个字节中每一位用来表示一个像素,位值等于1时表示对应像素被点亮,位值等于0时表示对应像素被熄灭。
以“”字为例,编码值是“ 0xd6 0xd0 ”,其中的0xd6 表示“区码”,表示在哪一个区:第“ 0xd6-0xa1”区;其中的 0xd0 表示“位码”,表示它是这个区里的哪一个字符:第“ 0xd0-0xa1 ”个。每一个区有94个汉字。区位码从0xa1而不是从0开始,是为了兼容ASCII码。

部分代码解析

int main(int argc, char **argv)
{
	if(fstat(fd_hzk16, &hzk_stat)){  /*由文件描述符取得文件状态 即stat结构体 */
		printf("can't get fstat\n");	/*为mmap函数获取文件大小做铺垫 即stat结构体中的st_size成员*/
		return -1;
	}
	hzkmem = (unsigned char *)mmap(NULL , hzk_stat.st_size, PROT_READ, MAP_SHARED, fd_hzk16, 0);
}

void lcd_put_chinese(int x, int y, unsigned char *str)
{
	unsigned int area  = str[0] - 0xA1;   /*区码*/
	unsigned int where = str[1] - 0xA1;		/*位码*/
	unsigned char *dots = hzkmem + (area * 94 + where)*32;  //32位 得到偏移地址 指针指向首地址
	unsigned char byte;

	int i, j, b;
	for (i = 0; i < 16; i++)	/*i代表行数  i、j遍历完共32位*/
		for (j = 0; j < 2; j++)  /*j代表某行中的第几个,j=0代表前8位,j=1代表后8位*/  
		{
			byte = dots[i*2 + j];
			for (b = 7; b >=0; b--) /*某行的第某个的某位*/
			{
				if (byte & (1<<b)){
					/* show */
					lcd_put_pixel(x+j*8+7-b, y+i, 0xffffff); /* 白 */
				}
				else{
					/* hide */
					lcd_put_pixel(x+j*8+7-b, y+i, 0); /* 黑 */
				}	
			}
		}
}
课后作业-中英文字符混合输出

实现lcd_put_str函数,可以输出混合的中英文字符,比如“中国china’',支持自动换行。
本人单纯实现lcd_put_str函数,至于多个汉字,参考lcd_put_str()即可改进。
只提供需要改进代码的地方,lcd_put_ascii函数去掉改为lcd_put_str函数

int main(int argc, char **argv)
{
	unsigned char str[] = "中";
	unsigned char str1[] = "China";
	
	lcd_put_str(var.xres/2, var.yres/2, strlen(str1), str1); /*在屏幕中间显示8*16的字母A*/
	
	printf("chinese code: %02x %02x\n", str[0], str[1]);
	lcd_put_chinese(var.xres/2 + strlen(str1)*8,  var.yres/2, str);
}
void lcd_put_str(int x, int y, int len, unsigned char *str)
{
	int k;
	int i, b;
	unsigned char byte;
	for(k = 0; k <= len; k++){
		unsigned char *dots = (unsigned char *)&fontdata_8x16[str[k]*16];
		for (i = 0; i < 16; i++){
			byte = dots[i];
			for (b = 7; b >= 0; b--){
				if (byte & (1<<b)){
					/* show */
					lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */
				}
				else{
					/* hide */
					lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */
				}
			}
		}
		x += 8;
	}
}

实验结果显示"China中"
在这里插入图片描述

六、Freetype

6-1 程序运行时的一些问题

程序运行时不需要头文件,所以头文件不用放到板子上。
编译程序的头文件来自哪里:①系统目录:交叉编译工具链里的某个include 目录 ;② 使用下面选项指定头文件

-I dir   /*dir为文件路径*/

链接时的库文件来自哪里:①系统目录:交叉编译工具链里的某个lib目录;②使用下面选项指定库文件。想链接libabc.so ,那链接时加上 -labc

-L dir

列出当前交叉编译工具链arm-buildroot-linux-gnueabihf-gcc的头文件目录、库目录。如果你编译的是一个库(如freetype),则可以把得到的头文件、库文件放入工具链的include 、lib目录里。

echo 'main(){}'| arm-buildroot-linux-gnueabihf-gcc -E -v -
6-2 Freetype

Freetype 是开源的字体引擎库,它提供统一的接口来访问多种字体格式文件,从而实现矢量字体显示。我们只需要移植这个字体引擎,调用对应的API 接口,提供字体文件,就可以让freetype 库帮我们取出关键点、实现闭合曲线,填充颜色,达到显示矢量字体的目的。
略…

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 很抱歉,我并不能完全理解您的问题。如果您是想了解关于嵌入式Linux机器人的开发,这需要考虑到硬件和软件两个方面。一般情况下,我们需要选择合适的硬件平台,并在其上安装嵌入式Linux系统,然后使用编程语言(如C/C++、Python等)编写机器人的控制程序。 以下是一个简单的示例代码,使用Python语言实现了一个基于嵌入式Linux机器人的前进和后退功能: ``` python import RPi.GPIO as GPIO import time # 设置GPIO口 GPIO.setmode(GPIO.BOARD) GPIO.setup(11, GPIO.OUT) GPIO.setup(13, GPIO.OUT) GPIO.setup(15, GPIO.OUT) GPIO.setup(19, GPIO.OUT) # 向前 def forward(): GPIO.output(11, GPIO.HIGH) GPIO.output(13, GPIO.LOW) GPIO.output(15, GPIO.HIGH) GPIO.output(19, GPIO.LOW) # 向后 def backward(): GPIO.output(11, GPIO.LOW) GPIO.output(13, GPIO.HIGH) GPIO.output(15, GPIO.LOW) GPIO.output(19, GPIO.HIGH) # 停止 def stop(): GPIO.output(11, GPIO.LOW) GPIO.output(13, GPIO.LOW) GPIO.output(15, GPIO.LOW) GPIO.output(19, GPIO.LOW) # 主函数 if __name__ == '__main__': try: while True: forward() time.sleep(2) stop() time.sleep(1) backward() time.sleep(2) stop() time.sleep(1) except KeyboardInterrupt: GPIO.cleanup() ``` 上述代码使用了树莓派(Raspberry Pi)的GPIO库,通过设置GPIO口的电平来控制机器人的前进和后退。当然,这只是一个简单的示例代码,实际的机器人开发还需要考虑到很多其他的因素,比如传感器、图像识别等等。 ### 回答2: 嵌入式Linux机器人的实现源代码可以分为几个关键方面: 1.硬件驱动程序:为了使机器人能够与外部设备进行通信和控制,需要编写硬件驱动程序。这些驱动程序可以包括控制底盘的驱动程序、传感器驱动程序、摄像头驱动程序等。 2.操作系统和内核定制:嵌入式Linux机器人需要一个适合嵌入式设备的操作系统和内核。在源代码中,我们需要对操作系统和内核进行定制,以满足机器人的需求。例如,可以选择定制轻量级的Linux发行版,去除不必要的组件和功能,加入对机器人硬件的支持等。 3.通信和控制程序:机器人要能够与其他设备、服务器或人机界面进行通信和控制。因此,我们需要编写通信和控制程序,以实现远程控制、信息传输等功能。这些程序可以使用网络协议、串口通信或其他通信方式。 4.感知和决策算法:机器人需要能够感知周围环境并做出相应决策。在源代码中,我们需要实现各种感知算法,如图像识别、目标跟踪、距离测量等,并结合决策算法,使机器人能够做出适当的行动。 5.用户界面和应用程序:为了更好地与机器人进行交互,我们可以编写用户界面和应用程序。这些程序可以包括机器人状态显示、地图生成、路径规划等。用户界面可以是图形化界面或命令行界面,根据实际需求进行选择。 总之,嵌入式Linux机器人的实现源代码是一个综合性的工程,需要涉及到硬件驱动、操作系统定制、通信和控制程序、感知和决策算法以及用户界面和应用程序等方面的编写和集成。这些源代码的编写需要深入理解嵌入式Linux系统的原理和机器人的工作原理,并结合实际的硬件和应用需求进行开发。 ### 回答3: 嵌入式Linux机器人的源代码实现可以包括以下几个方面: 1. 系统初始化:源代码需要实现嵌入式Linux系统的初始化,包括启动引导程序、加载内核镜像、初始化硬件设备、加载文件系统等。 2. 传感器驱动:机器人通常需要使用各种传感器来感知环境。源代码需要实现传感器驱动程序,以便与传感器进行通信和数据交换,如摄像头驱动、红外传感器驱动、超声波传感器驱动等。 3. 运动控制:机器人的运动控制涉及到电机、舵机等设备的驱动。源代码需要实现运动控制的相关算法,如PID控制算法、路径规划算法等,并与电机和舵机进行通信,控制机器人的运动。 4. 环境感知:源代码需要实现对机器人周围环境的感知和分析,以便进行智能决策。例如,图像处理算法可以用于识别目标物体,声音处理算法可以用于语音识别等。 5. 通信模块:机器人通常需要与其他系统进行通信,如与远程控制台进行通信、与其他机器人进行协作等。源代码需要实现通信模块,以便机器人能够与其他系统进行数据传输和指令交互。 6. 高级功能:根据机器人的具体应用场景,源代码还可以实现一些高级功能,如人脸识别、目标跟踪、自主导航等。这些功能需要依赖于相应的算法和模型,源代码需要与这些算法和模型进行集成。 总之,基于嵌入式Linux实现机器人的源代码了系统初始化、传感器驱动、运动控制、环境感知、通信模块以及各种高级功能的实现。这些源代码的编写需要结合具体的硬件平台和应用需求,并进行充分的调试和测试,确保机器人的正常运行和实现预期的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值