项目学习——环境监测系统a

一、项目介绍

(1).主要任务

1. 采集环境数据(温湿度、有害气体浓度)
2. 存储采集的数据(数据库)
3. 将采集到的数据显示在显示器上
4. 将采集到的数据上传到mqtt服务器上
--网络状态不好时上传失败的数据应该在网络状态恢复时重新上传

  • 主进程:负责启动和管理其他线程/进程。
  • 数据采集线程:负责从传感器读取数据。
  • 数据存储线程:负责将采集到的数据存储到数据库中。
  • 显示线程(可选):负责更新显示界面(如果应用有GUI)。
  • 网络通信进程/线程:负责将数据上传到MQTT服务器,并处理网络状态变化(如重连机制)

二、framebuffer

(一)了解framebuffere

       在Linux系统下,如果我们想对lcd屏或者其他显示器件进行画点、画线、画图案等操作,那么我们就必须要了解framebuffer(帧缓存)。
        对于用户来说,帧缓冲是Linux为显示设备提供的一个接口,它把一些显示设备描述成一个缓冲区,允许应用程序通过 FrameBuffer定义好的接口访问这些图形设备,从而不用去关心具体的硬件细节
         对于驱动工程师来说,帧缓存是内核驱动申请的一片内存空间,然后lcd内有一片sram,cpu内部有个lcd控制器,它有个单独的dma用来将frambuffer中的数据拷贝到lcd的sram中去 拷贝到lcd的sram中的数据就会显示在lcd上。

   简单来说:FrameBuffer就是一块显存,你往上面画什么,它就会显示什么。可以将它想象成一个二维平面,由一个个像素的组成,其分辨率宽度和高度的乘积就是整个FrameBuffer的像素点。例如:分辨率为800x600的FrameBuffer,就表示这块显存有600行,每行800个像素点

(二)如何使用framebuffer

(1) 在应用程序中,操作/dev/fbn的一般步骤如下:
打开/dev/fbn设备文件。

 (2) 用ioctl()操作取得当前显示屏幕的参数,如屏幕分辨率、每个像素点的比特数。根据屏幕参数可计算屏幕缓冲区的大小。

 (3) 用mmap()函数,将屏幕缓冲区映射到用户空间。

 (4) 映射后就可以直接读/写屏幕缓冲区,进行绘图和图片显示了。

 (5) 使用完帧缓冲设备后需要将其释放。

 (6) 关闭文件。

 (三)相关函数和结构体

(1)ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo)

获取fb_var_screeninfo结构的信息,在linux/include/linux/fb.h定义。

ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo)

获取fb_fix_screeninfon结构的信息。

在linux/include/linux/fb.h定义。

fbfd为设备文件号。

  • fbfd:是之前通过 open 系统调用打开的帧缓冲设备文件的文件描述符。
  • FBIOGET_FSCREENINFO:是一个宏定义,用于指示 ioctl 调用要执行的具体操作。对于帧缓冲设备,FBIOGET_FSCREENINFO 用于获取帧缓冲设备的可变屏幕信息。
  • &finfo:是一个指向 fb_var_screeninfo 结构体的指针,该结构体用于存储从帧缓冲设备获取的可变屏幕信息。

//fb_var_screeninfo结构体主要记录用户可以修改的控制器的参数,比如屏幕的分辨率和每个像素的比特数等,该结构体定义如下 


struct fb_var_screeninfo {
    __u32 xres; /*可见屏幕一行有多少个像素点*/
    __u32 yres; /*可见屏幕一列有多少个像素点*/
    __u32 xres_virtua l; /*虚拟屏幕一行有多少个像素点*/
    __u32 yres_virtual; /*虚拟屏幕一列有多少个像素点*/
    __u32 xoffset; /*虚拟到可见屏幕之间的行偏移*/
    __u32 yoffset; /*虚拟到可见屏幕之间的列偏移*/
    __u32 bits_per_pixel; /*每个像素的位数即BPP*/
    __u32 grayscale; /*非0时,指的是灰度*/
    struct fb_bitfield red; /*fb缓存的R位域*/
    struct fb_bitfield green; /*fb缓存的G位域*/
    struct fb_bitfield blue; /*fb缓存的B位域*/
    struct fb_bitfield transp; /*透明度*/
    __u32 nonstd; /* != 0 非标准像素格式*/
    __u32 activate;
    __u32 height; /*高度*/
    __u32 width; /*宽度*/
    __u32 accel_flags;
    /*定时:除了pixclock本身外,其他的都以像素时钟为单位*/
    __u32 pixclock; /*像素时钟(皮秒)*/
    __u32 left_margin; /*行切换,从同步到绘图之间的延迟*/
    __u32 right_margin; /*行切换,从绘图到同步之间的延迟*/
    __u32 upper_margin; /*帧切换,从同步到绘图之间的延迟*/
    __u32 lower_margin; /*帧切换,从绘图到同步之间的延迟*/
    __u32 hsync_len; /*水平同步的长度*/
    __u32 vsync_len; /*垂直同步的长度*/
    __u32 sync;
    __u32 vmode;
    __u32 rotate;
    __u32 reserved[5]; /*保留*/
};

(2)mmap函数

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

  • void *addr:映射的起始地址。一般设置为NULL,由内核自动选择映射的起始地址。
  • size_t length:映射的长度,可以是文件的长度或者是页的整数倍。
  • int prot:内存保护标志,用于指定内存的保护方式,如读(PROT_READ)、写(PROT_WRITE)、执行(PROT_EXEC)等。这些标志可以按位或运算进行组合。
  • int flags:控制映射的标志,用于指定映射是共享的还是私有的,以及一些其他的映射特性。常见的标志有MAP_SHARED(共享映射)和MAP_PRIVATE(私有映射)。
  • int fd:打开的文件描述符,指定要映射的文件。如果fd为-1,则表示创建一个匿名映射(不与任何文件关联)。
  • off_t offset:文件映射的偏移量,指定映射文件开始的位置。偏移量必须是页面大小的整数倍。

mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。

munmap执行相反的操作,删除特定地址区域的对象映射。

基于文件的映射,在mmap和munmap执行过程的任何时刻,被映射文件的st_atime可能被更新。如果st_atime字段在前述的情况下没有得到更新,首次对映射区的第一个页索引时会更新该字段的值。用PROT_WRITE和 MAP_SHARED标志建立起来的文件映射,其st_ctime和 st_mtime

在对映射区写入之后,但在msync()通过MS_SYNC和 MS_ASYNC两个标志调用之前会被更新。

(四)实际应用

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <math.h>

struct fb_var_screeninfo info;
unsigned char * p_fd = NULL;

typedef struct __color
{
	unsigned char b;
	unsigned char g;
	unsigned char r;
	unsigned char null;
}color;

typedef union 
{
	color rgb;
	unsigned int l;
}col;

1. fb_var_screeninfo 结构体

这个结构体通常定义在<linux/fb.h>头文件中,它包含了可变屏幕信息,即可以在运行时更改的显示参数。然而,需要注意的是,具体的字段可能会根据不同的内核版本和硬件而有所不同。但通常包括以下一些字段:

  • xres 和 yres:分别表示屏幕的水平和垂直分辨率(以像素为单位)。
  • xres_virtual 和 yres_virtual:分别表示虚拟屏幕的水平和垂直分辨率。虚拟分辨率可能比实际分辨率大,允许在屏幕上滚动或缩放图像。
  • bits_per_pixel:每个像素的位数,决定了颜色深度。
  • 其他字段可能包括像素格式、偏移量、时序信息等。

2. __color 结构体(别名color

这个结构体是您自定义的,用于表示一个颜色。它包含四个unsigned char类型的字段:

  • bgr:分别表示蓝色、绿色和红色的强度(或值)。这些值通常是0到255之间的整数。
  • null:这个字段的名称可能有些误导,因为它在颜色表示中通常不是必需的。它可能是为了保持结构体的字节对齐或预留的额外空间。然而,在这个上下文中,它没有被使用,因此可以安全地移除或重命名以避免混淆。

3. col 联合体

这个联合体允许您以两种不同的方式访问相同的内存位置:作为color结构体或作为unsigned int。这提供了一种方便的方法来将RGB颜色值打包成一个整数,以便在需要时将其写入帧缓冲设备。

  • rgbcolor类型的成员,允许您分别设置红色、绿色和蓝色的值。
  • lunsigned int类型的成员,当您想要将整个颜色值作为一个整数来处理时,可以使用它。通过适当的方式(如位操作或移位),您可以将RGB值组合成一个整数,并将其存储在l中。

main 函数

int main(int argc, const char *argv[])  
{  
    // 打开帧缓冲设备文件  
    int fd = open("/dev/fb0", O_RDWR);  
    if(fd < 0)  
    {  
        perror("open fd failed:");  
        return fd; // 如果打开失败,返回错误码  
    }  
  
    // 获取屏幕信息  
    int ret = ioctl(fd, FBIOGET_VSCREENINFO, &info);  
    if(ret < 0)  
    {  
        perror("ioctl failed:");  
        return ret; // 如果ioctl调用失败,返回错误码  
    }  
    printf("xres = %d yres = %d\n", // 打印屏幕的分辨率  
            info.xres, info.yres);  
    printf("xres_virtual = %d yres_virtual = %d\n", // 打印虚拟分辨率  
            info.xres_virtual, info.yres_virtual);  
    printf("bits_per_pixel = %d\n", info.bits_per_pixel); // 打印每像素位数  
  
    // 使用mmap映射帧缓冲到用户空间  
    unsigned long size = info.xres_virtual * info.yres_virtual * info.bits_per_pixel / 8;  
    p_fd = mmap(NULL, size, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);  
    if(NULL == p_fd)  
    {  
        perror("mmap failed:");  
        return -3; // 如果mmap调用失败,返回错误码  
    }  
      
    // 定义一个颜色,这里是黄色(RGB值为0xFF, 0xFF, 0x00)  
    col c;  
    c.rgb.r = 0xff;  
    c.rgb.g = 0xff;  
    c.rgb.b = 0x0;  
  
    // 在屏幕上绘制一条水平线  
    // 从(100, 100)开始,长度为200像素,颜色为黄色  
    draw_h_line(100, 100, 200, c);  
  
  
    // munmap(p_fd, size);  
    // close(fd);  
  
    return 0; // 程序正常结束  

(1)点,线,圆

int draw_point(int x0, int y0, col c)
{
	if((x0 < 0) || (x0 > info.xres_virtual))
		return -1;
	if((y0 < 0) || (y0 > info.yres_virtual))
		return -1;

	unsigned int * p = (unsigned int *)(p_fd + (y0 * info.xres_virtual + x0) * 4);
	*p = c.l;

	return 0;
}

int draw_h_line(int x0, int y0, int len, col c)
{
	int i = 0;
	for(i = 0; i < len; i++)
	{
		draw_point(x0 + i, y0, c);	
	}

	return 0;
}

int draw_v_line(int x0, int y0, int len, col c)
{
	int i = 0;
	for(i = 0; i < len; i++)
	{
		draw_point(x0, y0 + i, c);	
	}

	return 0;
}

#define PI 3.1415926

int draw_circle(int x0, int y0, int r, col c)
{
	double si = 0;
	unsigned int x = 0;
	unsigned int y = 0;
	for(si = 0; si < 360; si+=0.1)
	{
		x = x0 + r * cos(2 * PI * si / 360);
		y = y0 + r * sin(2 * PI * si / 360);
		draw_point(x, y, c);
	}
	return 0;
}

draw_point(int x0, int y0, col c)

这个函数用于在屏幕上的(x0, y0)位置绘制一个点,使用颜色c。它首先检查给定的坐标是否超出了屏幕的虚拟分辨率(info.xres_virtualinfo.yres_virtual),如果超出则返回-1表示失败。然后,它计算该点在帧缓冲区(p_fd指向的内存区域)中的位置,并将该位置处的像素值设置为颜色cl成员(这里假设col类型有一个l成员,它可能是一个整数,表示颜色值)。

draw_h_line(int x0, int y0, int len, col c)

这个函数用于在(x0, y0)位置开始,沿水平方向绘制一条长度为len的直线,使用颜色c。它通过循环调用draw_point函数来实现,每次循环将x0增加1,直到绘制完整个直线。

draw_v_line(int x0, int y0, int len, col c)

这个函数与draw_h_line类似,但它是沿垂直方向绘制直线。它通过循环调用draw_point函数,每次循环将y0增加1,直到绘制完整个直线。

draw_circle(int x0, int y0, int r, col c)

这个函数尝试在(x0, y0)位置绘制一个半径为r的圆,使用颜色c

(2)图片显示

①bmp简介

BMP取自位图Bitmap的缩写,也称为DIB(与设备无关的位图),是一种独立于显示器的位图数字图像文件格式。常见于微软视窗和OS/2操作系统。 -

BMP格式就是表示位图的格式。

BMP格式图像中的像素点,其位深度可以是1,4,8,24,32,但常见的BMP位深度还是8和24。

选择一张BMP图像,右键打开属性 --> 详细信息,可以查看其位深度。

②bmp文件格式

BMP文件由以下四部分组成:

  • 位图文件头(BITMAPFILEHEADER)
  • 位图信息头(BITMAPINFOHEADER)
  • 颜色表*(RGBQUAD[])
  • 像素阵列(Pixels[][])

由于颜色表并不一定存在,故加*说明

③bmp位图文件头

用于描述整个bmp文件的情况,具体包括BMP文件的类型、文件大小和位图起始位置等信

typedef struct tagBITMAPFILEHEADER{
    WORD	bfType;                  // 位图文件的类型,必须为BMP (2个字节)
    DWORD	bfSize;                  // 位图文件的大小,以字节为单位 (4个字节)
    WORD	bfReserved1;             // 位图文件保留字,必须为0 (2个字节)
    WORD	bfReserved2;             // 位图文件保留字,必须为0 (2个字节)
    DWORD	bfOffBits;               // 位图数据的起始位置,以相对于位图 (4个字节)
} BITMAPFILEHEADER;

   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

位图文件头总共有14个字节

⑤位图信息头

用于描述位图的尺寸等信息。

typedef struct tagBITMAPINFOHEADER{
	DWORD biSize;     		// 本结构所占用字节数  (4个字节)
	LONG biWidth;      		// 位图的宽度,以像素为单位(4个字节)
	LONG biHeight;     		// 位图的高度,以像素为单位(4个字节)
	WORD biPlanes;    		// 目标设备的级别,必须为1(2个字节)
	WORD biBitCount; 		// 每个像素所需的位数,必须是1(双色)、// 4(16色)、8(256色)、
	    					//24(真彩色)或32(增强真彩色)之一 (2个字节)
	DWORD biCompression; 	// 位图压缩类型,必须是 0(不压缩)、 1(BI_RLE8 
	                     	// 压缩类型)或2(BI_RLE4压缩类型)之一 ) (4个字节)
	DWORD biSizeImage;     	// 位图的大小,以字节为单位(4个字节)
	LONG biXPelsPerMeter;  	// 位图水平分辨率,每米像素数(4个字节)
	LONG biYPelsPerMeter;   // 位图垂直分辨率,每米像素数(4个字节)
	DWORD biClrUsed;        // 位图实际使用的颜色表中的颜色数(4个字节)
	DWORD biClrImportant;   // 位图显示过程中重要的颜色数(4个字节)
} BITMAPINFOHEADER;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

位图信息头一共有40个字节

由于

  1. 硬件访问效率:许多现代计算机体系结构(如x86、ARM等)访问对齐的内存地址时更加高效。如果结构体成员没有正确对齐,处理器可能需要多次访问内存来读取一个成员,这降低了效率。

  2. 硬件限制:某些硬件平台可能要求特定类型的数据必须对齐到特定的内存地址上。如果违反了这些规则,程序可能会崩溃或表现出不可预测的行为。

我们在使用这样的字节数没对齐的结构体需要使用结构体对齐:

结构体对齐的原则

  • 成员对齐:结构体中的每个成员都会根据其类型进行对齐。例如,在大多数平台上,int 类型(通常4字节)会被对齐到4字节的边界上。

  • 结构体整体对齐:结构体本身也会根据其成员中最大对齐要求或编译器/平台指定的对齐规则进行对齐。

  • 填充(Padding):为了满足对齐要求,编译器会在结构体成员之间以及结构体末尾插入额外的字节(填充)。

编译器指令:不同的编译器提供了不同的指令来控制结构体的对齐。例如,GCC和Clang提供了__attribute__((aligned(n)))#pragma pack(n)等指令。

#include <fcntl.h> // 引入对open函数的支持  
#include <unistd.h> // 引入对read, close等函数的支持  
#include <stdlib.h> // 引入对malloc, free等函数的支持  
#include <stdio.h> // 引入对perror等函数的支持  
  
// 假设的col结构体和draw_point函数声明  
typedef struct {  
    unsigned char r, g, b;  
} col;  
  
void draw_point(int x, int y, col c); // 假设的函数声明,用于在图形界面上绘制点  
  
// ...(之前的结构体定义保持不变)  
  
#pragma pack(2)
typedef struct _tag_bmp_file_head{
    unsigned short	file_type;			// 位图文件的类型,必须为BMP (2个字节)
    unsigned int	file_size;			// 位图文件的大小,以字节为单位 (4个字节)
    unsigned short	file_reserved1;		// 位图文件保留字,必须为0 (2个字节)
    unsigned short	file_reserved2;		// 位图文件保留字,必须为0 (2个字节)
    unsigned int	file_offset_bits;	// 位图数据的起始位置,以相对于位图 (4个字节)
}bmp_file_head;

typedef struct _tag_bmp_info_head{
	unsigned int info_size;     		// 本结构所占用字节数  (4个字节)
	unsigned int bit_width;      		// 位图的宽度,以像素为单位(4个字节)
	unsigned int bit_height;     		// 位图的高度,以像素为单位(4个字节)
	unsigned short bit_planes;    		// 目标设备的级别,必须为1(2个字节)
	unsigned short bits_per_pixel; 		// 每个像素所需的位数,必须是1(双色)、// 4(16色)、8(256色)、
										// 24(真彩色)或32(增强真彩色)之一 (2个字节)
	unsigned int bit_compression;		// 位图压缩类型,必须是 0(不压缩)、 1(BI_RLE8 
										// 压缩类型)或2(BI_RLE4压缩类型)之一 ) (4个字节)
	unsigned int bit_sizeImage;     	// 位图的大小,以字节为单位(4个字节)
	unsigned int bit_xpels_per_meter;  	// 位图水平分辨率,每米像素数(4个字节)
	unsigned int bit_ypels_per_meter;   // 位图垂直分辨率,每米像素数(4个字节)
	unsigned int bit_clr_used;			// 位图实际使用的颜色表中的颜色数(4个字节)
	unsigned int bit_clr_important;		// 位图显示过程中重要的颜色数(4个字节)
}bmp_info_head;

#pragma pack(4)


int show_bmp(const char * pathname, int x0, int y0)  
{  
    // 尝试以读写模式打开BMP文件  
    int fd = open(pathname, O_RDWR);  
    if(fd < 0)  
    {  
        perror("open bmp failed\n"); // 如果打开失败,打印错误信息  
        return -1; // 返回错误码  
    }  
  
    // 定义并读取BMP文件的文件头和信息头  
    bmp_file_head fhead;  
    bmp_info_head ihead;  
    read(fd, &fhead, sizeof(fhead)); // 读取文件头  
    read(fd, &ihead, sizeof(ihead)); // 读取信息头  
  
    // 计算位图数据的大小(这里假设是24位真彩色BMP,每个像素3字节)  
    // 注意:这里没有考虑位图可能存在的行填充(padding)  
    int size = ihead.bit_height * ihead.bit_width * 3;  
    unsigned char * data = malloc(size); // 分配内存用于存储位图数据  
    if (data == NULL) {  
        // 如果内存分配失败,应该处理这种情况  
        close(fd);  
        return -1;  
    }  
    read(fd, data, size); // 读取位图数据  
  
    // 遍历位图数据,并将其绘制到图形界面上  
    // 注意:BMP文件通常是从下往上存储图像的,因此需要调整y坐标  
    unsigned char * p = data;  
    int i, j;  
    for(j = 0; j < ihead.bit_height; j++) // 遍历每一行  
    {  
        for(i = 0; i < ihead.bit_width; i++) // 遍历每一行的每一个像素  
        {  
            col c; // 定义一个颜色结构体  
            c.rgb.b = *p++; // 读取蓝色分量  
            c.rgb.g = *p++; // 读取绿色分量  
            c.rgb.r = *p++; // 读取红色分量  
            // 调用draw_point函数绘制点,注意y坐标需要调整以匹配BMP的存储方式  
            draw_point(x0 + i, y0 + ihead.bit_height - j - 1, c);  
        }  
    }  
  
    // 释放位图数据占用的内存,并关闭文件描述符  
    free(data);  
    close(fd);  
  
    return 0; 
}

⑥使用bmp文件实现截屏工具

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#pragma pack(2)
typedef struct _tag_bmp_file_head{
    unsigned short	file_type;			// 位图文件的类型,必须为BMP (2个字节)
    unsigned int	file_size;			// 位图文件的大小,以字节为单位 (4个字节)
    unsigned short	file_reserved1;		// 位图文件保留字,必须为0 (2个字节)
    unsigned short	file_reserved2;		// 位图文件保留字,必须为0 (2个字节)
    unsigned int	file_offset_bits;	// 位图数据的起始位置,以相对于位图 (4个字节)
}bmp_file_head;

typedef struct _tag_bmp_info_head{
	unsigned int info_size;     		// 本结构所占用字节数  (4个字节)
	unsigned int bit_width;      		// 位图的宽度,以像素为单位(4个字节)
	unsigned int bit_height;     		// 位图的高度,以像素为单位(4个字节)
	unsigned short bit_planes;    		// 目标设备的级别,必须为1(2个字节)
	unsigned short bits_per_pixel; 		// 每个像素所需的位数,必须是1(双色)、// 4(16色)、8(256色)、
										// 24(真彩色)或32(增强真彩色)之一 (2个字节)
	unsigned int bit_compression;		// 位图压缩类型,必须是 0(不压缩)、 1(BI_RLE8 
										// 压缩类型)或2(BI_RLE4压缩类型)之一 ) (4个字节)
	unsigned int bit_sizeImage;     	// 位图的大小,以字节为单位(4个字节)
	unsigned int bit_xpels_per_meter;  	// 位图水平分辨率,每米像素数(4个字节)
	unsigned int bit_ypels_per_meter;   // 位图垂直分辨率,每米像素数(4个字节)
	unsigned int bit_clr_used;			// 位图实际使用的颜色表中的颜色数(4个字节)
	unsigned int bit_clr_important;		// 位图显示过程中重要的颜色数(4个字节)
}bmp_info_head;

#pragma pack(4)

int main(int argc, const char *argv[])  
{  
    srand(time(NULL)); // 初始化随机数生成器,确保每次运行程序时生成的随机数不同  
  
    int fd = open("2.bmp", O_WRONLY | O_CREAT, 0666); // 尝试以只写和创建模式打开(或创建)文件  
    if(fd < 0)  
    {  
        perror("open bmp2 failed:"); // 如果打开(或创建)文件失败,打印错误信息  
        return 1; // 返回错误码  
    }  
  
    // 初始化文件头和信息头  
    bmp_file_head fhead = {  
        .file_type = 0x4d42,           // BMP文件的标识符,'BM'的ASCII码值(小端序)  
        .file_size = 800 * 600 * 3 + 54, // 文件总大小,包括文件头、信息头和像素数据  
        .file_reserved1 = 0,  
        .file_reserved2 = 0,  
        .file_offset_bits = 54         // 像素数据的偏移量,即文件头和信息头的总大小  
    };  
  
    bmp_info_head ihead = {  
        .info_size = 40,               // 信息头的大小,固定为40字节  
        .bit_width = 800,              // 位图的宽度  
        .bit_height = 600,             // 位图的高度  
        .bit_planes = 1,               // 目标设备的级别,对于BMP图像,总是1  
        .bits_per_pixel = 24,          // 每个像素的位数,这里是24位真彩色  
        .bit_compression = 0,          // 压缩类型,0表示不压缩  
        .bit_sizeImage = 800 * 600 * 3,// 位图数据的大小(以字节为单位),对于24位图像,每个像素3字节  
        .bit_xpels_per_meter = 0,      // 水平分辨率(可选,这里设为0)  
        .bit_ypels_per_meter = 0,      // 垂直分辨率(可选,这里设为0)  
        .bit_clr_used =  0,           // 实际使用的颜色数(对于24位图像,可以设为0)  
        .bit_clr_important = 0         // 重要的颜色数(对于24位图像,可以设为0)  
    };  
  
    // 写入文件头和信息头到文件  
    write(fd, &fhead, sizeof(fhead));  
    write(fd, &ihead, sizeof(ihead));  
  
    // 生成并写入随机的像素数据  
    for(int i = 0; i < 800 * 600 * 3; i++)  
    {  
        unsigned char data = rand() % 256; // 生成0到255之间的随机数  
        write(fd, &data, sizeof(data));    // 写入单个字节的像素数据  
    }  
  
    close(fd); // 关闭文件描述符  
	return 0;
}

(3)文字显示

①ASCII艺术绘制(例如b)

ASCII艺术是一种使用文本字符(如字母、数字和符号)来创建图形和图像的艺术形式。在这个代码中,draw_a_ascii 函数通过定义一个字符(这里是'B')的位图数据(data_B 数组),然后使用这些位图数据在终端或图形界面上绘制出该字符的图形表示。

位图数据(data_B)是一个数组,其中每个元素代表字符图形的一行。数组中的每个字节(在这个例子中是unsigned char类型)代表该行中每个点(或像素)的开关状态(0表示关闭/空白,1表示打开/绘制)。由于一个unsigned char有8位,因此每行可以表示8个点。

为了绘制这个字符,draw_a_ascii 函数遍历位图数据的每一行(j循环),然后对于每一行,它遍历该行的每一位(i循环),检查该位是否为1。如果是1,则调用draw_point函数在相应的位置绘制一个点(在这个例子中,是通过打印坐标和颜色来模拟的)。

②字符串的逐字符处理

draw_str 函数接受一个字符串、起始坐标(x0y0)和一个颜色(c),然后逐字符地处理这个字符串。对于字符串中的每个字符,它执行以下操作:

  • 如果字符是换行符(\n),它将y坐标增加一行的高度(在这个例子中假设为9),并将x坐标重置为起始x坐标(x0),以便在下一行开始绘制。

  • 如果字符是空格(' '),它将x坐标增加空格的宽度(在这个例子中也是假设为9),但不调用draw_a_ascii来绘制任何内容。

  • 对于其他字符,它调用draw_a_ascii函数来绘制该字符的ASCII艺术表示。然后,它将x坐标增加该字符的宽度(同样假设为9),以便为下一个字符腾出空间。

// 绘制单个 ASCII 字符的函数  
int draw_a_ascii(char ch, int x0, int y0, col c) {  
    // 这里只处理了 'B' 的情况,如果需要其他字符,需要添加更多的数据数组  
    unsigned char data_B[8] = {0x00, 0x38, 0x24, 0x24, 0x38, 0x24, 0x24, 0x38}; // 'B'  

    int i, j;  
    for (j = 0; j < 8; j++) {  
        unsigned char tmp = data_B[j];  
        for (i = 0; i < 8; i++) {  
            if (tmp & (1 << (7 - i))) { // 注意位操作的方向  
                draw_point(x0 + i, y0 + j, c);  
            }  
        }  
    }  
    return 0;  
}  
  
// 绘制字符串的函数  
int draw_str(const char *str, int x0, int y0, col c) {  
    int len = strlen(str);  
    int x = x0;  
    int y = y0;  
    for (int i = 0; i < len; i++) {  
        switch (str[i]) {  
            case '\n':  
                y += 9; // 假设每行高度为 9  
                x = x0;  
                break;  
            case ' ':  
                x += 9; // 空格也占据空间  
                break;  
            default:  
                draw_a_ascii(str[i], x, y, c);  
                x += 9; // 假设每个字符宽度为 9  
                break;  
        }  
    }  
    return 0;  
}  
  
int main() {  
    draw_str("B Hello\nWorld!", 0, 0, BLUE); // 示例调用  
    return 0;  
}

这里会按照main里面的字符串的格式打印出一堆B。

③利用已经编辑好的字符数组文件实现显示数字(font)

void draw_a_ascii_by_zimo (int x0, int y0, unsigned char *zimo, col f)  
{  
	int x, y;  
  
	// 遍历字体的每一行(总共16行)  
	for (y = 0; y < 16; y++) {  
		unsigned char tmp = *zimo++; // 取出当前行的字节  
		// 遍历当前行的每一个像素(总共8个)  
		for (x = 0; x < 8; x++) {  
			// 检查当前位(从最高位开始)是否为1  
			if (tmp & 0x80) {  
				draw_point (x0 + x, y0 + y, f); // 如果是,则在该位置绘制颜色f  
			}  
			tmp = tmp << 1; // 左移一位,检查下一个位  
		}  
	}  
}
extern unsigned char font_8x16[4096];
void draw_a_ascii (int x0, int y0, char ch, col f)  
{  
	// 根据字符的ASCII值在字体库中定位到该字符的起始位置  
	unsigned char *pzimo = (void *)(font_8x16 + (int)ch * 16UL);  
	// 调用draw_a_ascii_by_zimo函数绘制字符  
	draw_a_ascii_by_zimo(x0, y0, pzimo, f);  
}

void draw_string (char *str, int x0, int y0, col f)  
{  
	int len = strlen(str); // 获取字符串长度  
	int i;  
  
	// 遍历字符串中的每个字符  
	for (i = 0; i < len; i++)   
	{  
		switch (*str) // 根据字符类型执行不同操作  
		{  
		case '\r': // 回车符,重置x0到0(但这里未处理y0,可能需要结合'\n'使用)  
			x0 = 0;  
			break;  
		case '\n': // 换行符,重置x0到0并增加y0  
			x0 = 0;  
			y0 += 16;  
			break;  
		case '\t': // 制表符,这里未实现缩进,可能需要自定义  
			break;  
		case '\b': // 退格符,这里未实现删除前一个字符,可能需要自定义  
			break;  
		default: // 其他字符,绘制字符  
			draw_a_ascii(x0, y0, *str, f);  
			x0 += 8; // 更新x0到下一个字符的起始位置  
			str++; // 移动到字符串的下一个字符  
			break;  
		}  
	}  
}

三、库

(1)gcc的编译过程

gcc的编译分为以下四个阶段:

  • gcc预处理器:把.c文件编译成预处理.i文件
  • gcc编译器:把预处理.i文件编译成.s的汇编文件
  • gcc汇编器:把.s汇编文件编译成.o二进制文件
  • gcc链接器:把.o二进制文件链接成一个可执行文件
  • 预处理:gcc -E hello.c -o hello.i
  • 编译:gcc -S hello.i -o hello.s
  • 汇编:gcc -c hello.s -o hello.o
  • 链接:gcc hello.o -o hello

在实际开发过程中,当代码的文件较多,可以将一部分代码编译成动态库或者静态库然后再加载到程序中使用

gcc总体选项列表
1) -c :指编译,不链接,生成目标文件“.o”。
2) -S :只编译,不汇编,生成汇编代码“.S”。
3) -E :只进行预编译/预处理,不做其他处理。
4) -o file:把输出文件输出到file里。
5) -g :在可执行程序中包含标准调试信息。
6) -v :打印出编译器内部编译各过程的命令行信息和编译器的版本。
7) -I dir :在头文件的搜索路径列表中添加dir目录
8) -L dir :在库文件的搜索路径列表中添加dir目录
9) -static :连接静态库(静态库也可以用动态库链接方式链接)
10) -llibrary :连接名为library的库文件(显示指定需要链接的动态库文件)、

Windows的库
Windows系统本身提供并使用了大量的库;
包括:静态库(.lib)和动态链接库(.dll)

Linux的库
· Linux系统通常把库文件存放在/usr/lib或/lib目录下,
· Linux库文件名组成:前缀lib + 库名 + 后缀(3部分组成)
动态库:以.so作为后缀
静态库:通常以.a或.la作为后缀。

 (二)linux中的库

(1)创建静态库的过程

①头文件show.h——声明静态库所导出的函数

②对应于头文件中的源文件show.c——实现静态所导出的函数

③编译show.c生成的目标文件

# gcc –o show.o –c show.c
注: -c是只编译而不生成可执行文件,-o指定文件名

④ 将目标文件加入到静态库中,静态库为libmylib.a
#ar rcs libshow.a show.o

⑤将静态库拷贝到Linux的库目录下

#cp libshow. /usr/lib/libshow.a

调用静态库
(就是用一个.c源文件在编译的时候做静态连接)

#gcc main.c –lshow

参数说明:
(1)选项l为链接选项说明在链接时就将静态库链接至源程序中
(2)在Linux中约定所有库都以前缀lib开始,静态库以.a结尾,动态库以.so结尾,在编译时,无需带上前缀(lib)和后缀(.a)。

(2)动态库

动态库没有归档,在编译过程中就要生成符号表

在Linux环境下,只要在编译函数库源程序时加上
-fPIC -shared选项即可生成动态链接库。
 ①动态库的创建
# gcc –fPIC -o mylib.o -c mylib.c (生成符号表)
# gcc –shared -o libmylibs.so mylib.o(防止重复加载)

· 或者可以直接使用一条命令
# gcc –fPIC –shared –o libmylibs.so mylib.c (可以省略 -c 和 .o后缀文件名)

· 将生成的静态库拷贝至/usr/lib或/lib目录中
# cp libmylibs.so /usr/lib/libmylibs.so

②动态库的使用

gcc main.c -I ./show -L ./show -lshow -lm

-I 指定头文件路径
-L指定库的路径

四、makefile

       Makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。

    make命令执行时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接序。   

具体查看:

Makefile教程(绝对经典,所有问题看这一篇足够了)-优快云博客

五、linux和arm的互通

(一)、交叉开发:

交叉开发是一种软件开发方法,其中开发者在一种平台上(如PC或高端工作站,称为宿主机)使用交叉编译器等工具链来编译和生成目标平台(如嵌入式设备、ARM架构设备等)上运行的程序。这种方法的目的是避免在目标平台上直接进行开发,因为目标平台可能资源有限(如内存、处理器速度等),或者开发环境不易搭建。

(二)交叉编译:

在Ubuntu(一个基于x86架构的PC操作系统)上使用arm-linux-gcc(这是一个为ARM架构优化的GCC编译器)来编译程序,生成的可执行文件可以在ARM架构的Linux系统上运行。

gcc 编译Ubuntu运行的程序

arm-linux-gcc 编译ARM的程序

(三)nfs:网络文件系统

服务器:配置可以共享的目录

客户端:挂载本地目录的

在arm端执行这个命令后,在pc端执行arm—Linux—gcc后会可以将编译完成的a.out文件传入到arm端的mnt目录中,可以直接在arm端运行

六、MQTT协议

(一)简介

      MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
      MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。

(二)应用

七、库移植

(一)MQTT库移植

①设置双网卡

Ubuntu路由器配置方法

将网卡链接模式设置为桥接模式

  1. 点击 ”虚拟机”
  2. 点击 ”设置”
  3. 点击 ”网络适配器”
  4. 点击 ”桥接模式”
  5. 点击 “确定”

添加第二块网卡并设置为NAT模式

1) 点击 ”虚拟机”

2) 点击 “设置”

3) 点击 “网络适配器”

4) 点击 “添加”

5) 点击 “网络适配器”

6) 点击 “下一步”

7) 点击 “NAT 模式(N):用于共享主机的IP地址”

8)点击 “完成”

将有线网卡绑定到第一个网卡上

  1. 点击 “编辑”
  2. 点击 “虚拟网络编辑器”
  3. 点击 “更改设置”
  4. 点击 “是”
  5. 点击 “桥接到(T): Realtek PCIe GBE Family Controller”
  6. 点击 “应用”
  7. 点击 “确定”

进入虚拟机

1)点击 ”开启此虚拟机”

  1. 设置两个网卡的配置信息点击

1)键盘输入: ctrl + alt + t 打开终端

2) 键盘输入: sudo vim /etc/network/interfaces

3) 将文件修改为如下格式: (注意: 一个字符都不能错)

auto lo

iface lo inet loopback

auto eth0

iface eth0 inet static

address 192.168.1.3

netmask 255.255.255.0

gateway 192.168.1.1

auto eth1

iface eth1 inet dhcp

4) 保存并退出

5) 将windows wifi连入internet中

5) 键盘输入: sudo shutdown -r now 重启虚拟机

  1. 使用iptables设置转发规则
  2. 键盘输入: sudo modprobe ip_tables
  3. 键盘输入: sudo modprobe ip_nat_ftp
  4. 键盘输入: sudo iptables -P INPUT ACCEPT
  5. 键盘输入: sudo iptables -P OUTPUT ACCEPT
  6. 键盘输入: sudo iptables -P FORWARD ACCEPT

7. 设置网卡地址转发的详细信息

1)sudo vim /etc/sysctl.conf

2) 修改内容

#net.ipv4.ip_forward = 1

net.ipv4.ip_forward = 1

3)键盘输入: ctrl + s 保存退出

4) 键盘输入: sudo sysctl -p

5)键盘输入: sudo iptables -t nat -A POSTROUTING -s 目的网段/子网掩码 -j SNAT  --to 虚拟NAT网卡地址

sudo iptables -t nat -A POSTROUTING -s 192.168.1.0/255.255.255.0 -j SNAT  --to 192.168.XX.XX

(eht1 ip)

  1. 查看网关路由表
  1. route
  2. sudo route delete default
  3. 设置转发网关eth1:sudo route add default gw 192.168.xxx.2 eth1
  4. ping www.baidu.com

  1. 开发板路由配置

删除旧的路由:

route delete default

添加新的网关路由:

route add default gw 192.168.1.3

  1. 开发板ping外网

关闭虚拟机防火墙:sudo ufw disable

关闭windows防火墙:

ping 8.8.8.8

或者

vi /etc/resolv.conf

增加:nameserver 8.8.8.8

ping www.baidu.com

②相关文件解压

 tar文件

zip文件

给权限

多线程编译(提升速度)

③PC库移植

 1. openssl
        进入openssl源码目录
        ./config enable-shared -fPIC #加入-fPIC 选项,不然,编译paho会出问题。
        make #这个过程需要等待下
        sudo make install 
    2.paho 的编译
        修改makefile
        1.CC ?=gcc             122line
        2.133 line 加入这两个选项
        CFLAGS += -I /usr/local/ssl/include
        LDFLAGS += -L /usr/local/ssl/lib  
3. 192line  注意-I,-L 后面填写ssl库的头文件目录和库文件目录 我这里是/usr/local/ssl/include 和 /usr/local/ssl/lib,如果你的不是,需要切换下
        CCFLAGS_SO += -Wno-deprecated-declarations -DOSX -I /usr/local/ssl/include
        LDFLAGS_C += -Wl,-install_name,lib$(MQTTLIB_C).so.${MAJOR_VERSION}
        LDFLAGS_CS += -Wl,-install_name,lib$(MQTTLIB_CS).so.${MAJOR_VERSION}
        LDFLAGS_A += -Wl,-install_name,lib${MQTTLIB_A}.so.${MAJOR_VERSION}
        LDFLAGS_AS += -Wl,-install_name,lib${MQTTLIB_AS}.so.${MAJOR_VERSION} 
        FLAGS_EXE += -DOSX
        FLAGS_EXES += 

        4.make 

        quesion:
        /usr/bin/ld: /usr/local/ssl/lib/libcrypto.a(x86_64cpuid.o): relocation R_X86_64_PC32 against symbol `OPENSSL_cpuid_setup' can not be used when making a shared object; recompile with -fPIC
        初步分析,ssl编译的有问题,需要在编译的时候添加 ./config enable-shared

         /tmp/ccM0lsiB.o: relocation R_X86_64_PC32 against symbol `stdout@@GLIBC_2.2.5' can not be used when making a shared object; recompile with -fPIC
        检查paho库makefile 中是否加入-fPIC编译选项。
         本makefile line:157 CCFLAGS_SO = -g  -fPIC

         5. sudo make install   /usr/local/lib /usr/local/include 
         最后:
         install: cannot stat 'build/output/doc/MQTTClient/man/man3/MQTTClient.h.3': No such file or directory
         Makefile:275: recipe for target 'install' failed
         make: [install] Error 1 (ignored)
         install -m 644 build/output/doc/MQTTAsync/man/man3/MQTTAsync.h.3 /usr/local/share/man/man3
         install: cannot stat 'build/output/doc/MQTTAsync/man/man3/MQTTAsync.h.3': No such file or directory
         Makefile:275: recipe for target 'install' faile
        
         报这个错不用理会。到此就完成了编译。
         pc 版mqtt库的安装。

如果出现

我们这已经生成了相对应的库

将库直接复制到相对应的位置即可

④ARM库移植

  arm-linux-gcc 配置
 先按照之前方法配置
        apt-get install lib32ncurses5 lib32z1

(由于是64位系统  arm-linux-gcc 是32位,需要安装这个库)
    openssl arm 版本编译
    1.     ./config no-asm shared --prefix=$(pwd)/__install
     no-asm: 是在交叉编译过程中不使用汇编代码代码加速编译过程,原因是它的汇编代码是对arm格式不支持的。
    shared :生成动态连接库。
    –prefix :指定make install后生成目录的路径,不修改此项则默认为OPENSSLDIR目录(/usr/local/ssl)
    2.修改makefile
    1) CC= gcc 改成 CC = arm-linux-gcc;(根据你自己的交叉编译环境设置,我的交叉编译环境是:arm-none-linux-gnueabi-)
    2) 删除 CFLAG= 中的 “-march=pentium”;(如果有的话)

    3) AR=ar $(ARFLAGS) r 改为 AR=arm-none-linux-gnueabi-ar $(ARFLAGS) r;

    4) ARD=ar $(ARFLAGS) d 改为 ARD=arm-none-linux-gnueabi-ar $(ARFLAGS) d;(如果有的话)

    5)RANLIB= /usr/bin/ranlib 改为 RANLIB= arm-none-linux-gnueabi-ranlib;
    3.makefile 中去掉
    153 63 -m64 去掉

    4. make
    5. make install
    6.生成的arm库子当前目录下__install

.arm paho

        1.进入paho目录
        2.  修改makefile 24行

        .PHONY: clean, mkdir, install, uninstall, html,在这行的下面加入2行

        INCLUDES = -I/home/linux/code_test/mqtt_src/openssl-1.0.0s/__install/include
        LIBSDIR  = -L/home/linux/code_test/mqtt_src/openssl-1.0.0s/__install/lib
        3. 然后到文件的第181行,在${CC} 后面加上$(INCLUDES),在最后加上$(LIBSDIR),生成的时候需要库,要把库的路径添加进去。

        ${CC} $(INCLUDES) -g -o $@ $< -l${MQTTLIB_CS} ${FLAGS_EXES} $(LIBSDIR)
        在第187行,203行,215行做同样处理。最好和我用一样的版本,一个是这个位置不对,另一个是我之前用的1.10版本的,这样子做没有效果。
        需要多行加入。行号稍微有点差异。
 

make CC=arm-linux-gcc

    问题
    报错:build/output/libpaho-mqtt3a.so: undefined reference to `clock_gettime'
    修改makefile 最后 加入-lrt
    FLAGS_EXES = $(LDFLAGS) -I ${srcdir} ${START_GROUP} -lpthread -lssl -lcrypto -lrt ${END_GROUP} -L ${blddir}  


    
    最后,bulid/output 下面就是 arm 版本的库了.

    在paho 目录下面有两个makefile ,后缀为arm,是arm版本的makefile 生成arm版本的库。
    后缀名为PC的是生成PC版本的库,可以根据情况进行改名。
    cp Makefile_arm makefile 
    cp Makefile_PC makefile

七、数据库移植到ARM端

具体步骤详见:

成功移植SQLite3到ARM Linux开发板_linux arm sqlite3 so-优快云博客

最后可以实现在arm端运行sqlite3

八、ARM的具体编程详见之后的学习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值