树莓派镜像打包

众所周知,树莓派是用来折腾的,但是因为系统跑在SD上以及折腾时手残总是无法避免,这就导致系统很容易崩了,系统蹦了如果从头烧镜像,装环境那简直是生不如死,所以这就体现了备份镜像的必要性。

备份镜像最傻瓜的方式就是在Windows平台下使用[Win32 Disk Image]的Read功能直接把SD卡中的镜像读取到文件中,但是直接读取的文件是与SD卡大小一致的,这就导致不管是备份存储还是重新写入都会有很多麻烦,为了解决这个麻烦就需要调整SD卡的分区大小然后再读出备份。而调整分区的操作在Windows下又会遇到一些问题。而且Linux下有十分便利的脚本可以实现镜像压缩,所以推荐的方式是在Linux平台下操作,比如ubuntu。

具体操作:

先将SD连接到Linux主机,并查看设备名以及分区信息。

xingyong@xingyong-dell-7050:~$ sudo fdisk -l

Disk /dev/sda: 931.5 GiB, 1000204886016 bytes, 1953525168 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/nvme0n1: 238.5 GiB, 256060514304 bytes, 500118192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x6a4965ff

Device         Boot Start       End   Sectors   Size Id Type
/dev/nvme0n1p1 *     2048 500117503 500115456 238.5G 83 Linux


Disk /dev/sdb: 59.5 GiB, 63864569856 bytes, 124735488 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xe4b7df54

Device     Boot  Start       End   Sectors  Size Id Type
/dev/sdb1         8192    532479    524288  256M  c W95 FAT32 (LBA)
/dev/sdb2       532480 124735487 124203008 59.2G 83 Linux

此时可以看到SD卡的设备名称,我的是sdb,有两个分区,然后使用dd指令将SD中的镜像拷贝到Linux主机内,注意,此时拷贝的依旧是完整的镜像,大小与SD卡大小一致,这就需要你的主机得有比SD卡容量大的空闲空间,如果不够,就需要先调整SD卡分区大小再操作!

xingyong@xingyong-dell-7050:~$ sudo dd if=/dev/sdb of=./raspberrypi.img bs=512
124735488+0 records in
124735488+0 records out
63864569856 bytes (64 GB, 59 GiB) copied, 779.038 s, 82.0 MB/s

经过上面的操作,我们就将镜像从SD卡打包到img文件中了,但是此时的文件大小还是很大,需要压缩一下,这里推荐一个超级好用的脚本[GitHub链接],关于脚本的详细信息大家可以去仓库查看,这里就不展开了。

xingyong@xingyong-dell-7050:~$ sudo pishrink.sh raspberrypi.img 
pishrink.sh v0.1.2
pishrink.sh: Gathering data ...
Creating new /etc/rc.local
pishrink.sh: Checking filesystem ...
rootfs: 120206/3784416 files (0.4% non-contiguous), 1326501/15525376 blocks
resize2fs 1.44.1 (24-Mar-2018)
pishrink.sh: Shrinking filesystem ...
resize2fs 1.44.1 (24-Mar-2018)
Resizing the filesystem on /dev/loop0 to 1660441 (4k) blocks.
Begin pass 2 (max = 14804)
Relocating blocks             XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 3 (max = 474)
Scanning inode table          XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
The filesystem on /dev/loop0 is now 1660441 (4k) blocks long.

pishrink.sh: Shrinking image ...
pishrink.sh: Shrunk imgname.img from 60G to 6.6G ...

可以看到执行完脚本之后镜像从60G变为了6.6G,这样你就可以愉快的使用这个打包并压缩的镜像了。

如果要在Linux下烧录镜像依旧可以使用dd指令,只需要调换上面if/of的内容就好。

xingyong@xingyong-dell-7050:~$ sudo dd if=./raspberrypi.img of=/dev/sdb bs=512
13816009+0 records in
13816009+0 records out
7073796608 bytes (7.1 GB, 6.6 GiB) copied, 873.813 s, 8.1 MB/s


把做好镜像的TF卡插回树莓派上启动,进入系统后使用raspi-config对文件系统扩展使用全部TF卡空间就可以愉快的使用了。

如果不知道如何扩展可以查看树莓派手册Expand Filesystem章节

Arduino与树莓派字节对齐问题记录

我在做一个小东西的时候用到了Arduino和树莓派,因为需要将数据采集端放的比较远所以没有直接将传感器接在树莓派上,而是选择了使用Arduino采集传感器数据,然后使用串口与树莓派通信。就在我很快写完两端的代码之后遇到了一个奇怪的问题,那就是明明数据接收解析过程没有问题,但是解析的数据怎么也不对。经过分析发现是两个平台数据长度和默认的对齐方式不同导致的,特此记录一下。

struct sensor_data{
    uint16_t hand;
    double ambient_temp;
    double object_temp;
    uint16_t tail;
}sensor_data;

我在两个平台定义了相同的结构体用来传输数据,在Arduino上将传感器数据获取到之后写入结构体,然后使用按字节从串口发送,因为两个平台都是小端模式,所以理论上在树莓派上我只需要按字节把接收的数据拷贝到相同结构体就可以获取到数据,但是,实际发现Arduino只发送了14字节,也就是说在Arduino平台下double只占用了4字节,与float是一致的。而且是按2字节对齐。

A5 5A D0 A3 E2 41 30 5C E1 41 0F F0

实际打印的数据也印证了我的猜想,后来发现Arduino的文档中关于double的描述明确指出了这个问题。

文档中关于double的说明

树莓派自然是不存在double与float长度一致的问题,而且树莓派是4字节对齐,所以就会出现我遇到的数据错误的问题。知道问题所在,修改代码就愉快的解决问题了。

关于C语言中的字节对齐一般有两种方式:

一、强制按字节对齐:

#pragma pack (n)    //C编译器将按照n个字节对齐。
#pragma pack ()     //取消自定义字节对齐方式。

二、对齐到n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐:

__attribute((aligned (n)))  //C编译器将按照n个字节对齐。
__attribute ((packed))      //取消自定义字节对齐方式。

最后附上此次测试的代码:

树莓派:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <wiringPi.h>
#include <wiringSerial.h>
 
#pragma pack (2)
struct sensor_data{
    uint16_t hand;
    uint16_t serial_number;
    float ambient_temp;
    float object_temp;
    uint16_t tail;
}sensor_data;
#pragma pack ()
 
struct sensor_data rx_data;

int main(void)
{
    int hs1;
    char uart_rx_buffer[64];
    uint16_t uart_flag = 0;
    int16_t index = 0;
    int16_t data_flag = 0;
    wiringPiSetup();                            // 使用wiring编码去初始化GPIO序号
    // hs1 = serialOpen("/dev/ttyS0", 115200);     // 打开 /dev/ttyS0 串口设备,波特率115200
    hs1 = serialOpen("/dev/ttyUSB0", 115200); // 打开 /dev/ttyUSB0 串口设备,波特率115200
    if(hs1 < 0){
        printf("UART open error %d!\r\n",hs1);
        return -1;
    }
    else{
        printf("UART open success %d!\r\n",hs1);
    }

    char keyboard_input[10] = {};
    while(TRUE){

        int buffer_size = serialDataAvail(hs1);
        while(buffer_size){
            char c = serialGetchar(hs1);    // 从接收缓存区读取一个字节
            if((uart_flag == 0)&&(c == 0xA5)){
                uart_flag = 1;
            }
            else if((uart_flag == 1)&&(c == 0x5A)){
                uart_flag = 2;
            }
            else if((uart_flag == 1)&&(c != 0x5A)){
                uart_flag = 0;
                index = 0;
                memset(uart_rx_buffer,0,sizeof(uart_rx_buffer));
            }

            if(uart_flag > 0){
                uart_rx_buffer[index++] = c;
                if(index > sizeof(rx_data)+1){
                    uart_flag = 0;
                    index = 0;
                    memset(uart_rx_buffer,0,sizeof(uart_rx_buffer));
                }
            }
            
            if((uart_flag == 2)&&(c == 0x0F)){
                uart_flag = 3;
            }
            else if((uart_flag == 3)&&(c == 0xF0)){
                data_flag = 1;
                memcpy((&rx_data),uart_rx_buffer,sizeof(rx_data));
                uart_flag = 0;
                index = 0;
                memset(uart_rx_buffer,0,sizeof(uart_rx_buffer));
            }
            else if((uart_flag == 3)&&(c != 0xF0)){
                uart_flag = 0;
                index = 0;
                memset(uart_rx_buffer,0,sizeof(uart_rx_buffer));
            }
            buffer_size --;
        }

        if(data_flag){
            printf("%5d Ambient = %5.2f   Object = %5.2f\r\n",\
                  rx_data.serial_number,\
                  rx_data.ambient_temp,\
                  rx_data.object_temp);
            data_flag = 0;
        }
    }
 
    serialClose(hs1);                           // 关闭串口
    return 0;
}

因为使用到了wiringPi的库,所以编译的时候记得加-lwiringPi链接,执行的时候也需要使用管理员权限才可执行。

Arduino:

#include <Wire.h>
#include <Adafruit_MLX90614.h>

Adafruit_MLX90614 mlx = Adafruit_MLX90614();

struct sensor_data{
  uint16_t hand;
  uint16_t serial_number;
  float ambient_temp;
  float object_temp;
  uint16_t tail;
}sensor_data;

struct sensor_data tx_data;
uint16_t num =0;

void setup() {
  tx_data.hand = 0x5aa5;
  tx_data.tail = 0xf00f;
  Serial.begin(115200);
  mlx.begin();  
}

void loop() {
  tx_data.serial_number = num;
  tx_data.ambient_temp = mlx.readAmbientTempC();
  tx_data.object_temp = mlx.readObjectTempC();
  Serial.write((char*)(&tx_data),sizeof(tx_data));
  num++;
  delay(500);
}

Tmux 使用教程

Tmux 是一款终端复用命令行工具,一般用于 Terminal 的窗口管理。Tmux 拥有如下特性:

  • 可以同时开启多个会话和窗口,并持久地保存工作状态。

例如,若您需要在 Terminal 中编辑一个文件,同时还需要在 Python 交互环境中执行命令,那么正常情况下您需要开启两个 Terminal。
若您使用 Tmux,则无须开启多个 Terminal,您可使用 session 管理会话和窗口,在多个会话和窗口之间进行切换。

  • 断线后任务能够在后台继续执行。

Tmux 还能解决由于断线导致的任务丢失问题。一般的 shell 工具遇到断线,远程任务可能会中止并无法继续,重连后任务需从头再来。而在 Tmux 中运行的命令,会一直保存在服务器上,断线后只需从 Tmux 中恢复该会话,任务仍然在运行。

Tmux安装

在 Terminal 中使用如下命令:

sudo apt update
sudo apt install tmux

Tmux 由如下三个基础组成

  • 1. Session。即会话,任务通常在 session 中运行,在断开连接后 session 仍会保持。
  • 2. Window。即窗口,一个会话可以包含多个窗口。可以存在多个窗口。
  • 3. Pane。即窗格,一个窗口可以包含多个窗格。类似于 Vim 中 C-w +v 后的效果。

使用 Tmux 管理会话

在Terminal中输入:

tmux

这样就会开启了一个 session-name 为 0 的 Tmux 会话。

tmux会话

左下角即当前窗口的 session-name,此时就可以在这个会话中正常输入命令。

在tmux的会话中启动一个服务

此时如果您和服务器断开连接,tmux 中的任务还会继续保持。您可重新打开 Terminal 后,输入命令:

tmux a -t 0 

其中 0 为之前会话的 session-name。

如果您想从该会话中退出,可以输入如下命令回到普通的 Termina:

tmux detach

此时可以再次输入 tmux 命令开启一个新的会话。Tmux 默认的 session-name 会逐次加一,再次新建的会话默认 session-name 就是 1 了。

在启用会话时可以指定session的名称,方便自己记忆:

tmux new -s [session-name]

eg:
tmux new -s tcpserver
自定义名称的 session

普通 Terminal 页面中,可以查看所有的 Tmux 会话:

tmux ls

如果要删除指定会话,在普通 Terminal 页面中,输入命令:

tmux kill-session -t [session-name]

eg:
tmux kill-session -t 0     #删除名称为0的会话
tmux kill-server           #删除所有会话

切换会话:

tmux switch -t <session-name>

重命名会话:

tmux rename-session -t 0 <new-name>

窗口管理

在每个 session 会话中,您可以开启多个窗口和面板。
Tmux 为了防止与全局快捷键冲突,大部分快捷键需要先需要输入前缀,默认为 Ctrl + b。该操作被定义为 Prefix
 
创建一个窗口需要两步:
  • 第一步:按 Ctrl+B 组合键,然后松开。
  • 第二步:再单独按一下 c 键。
创建了三个窗口

选择窗口

星号(*)在这里表示的是“当前处于活跃状态的窗口”,也就是哪个窗口现在处于可操作状态,星号(*)就在哪个窗口的后面

Tmux 常用快捷键
快捷键	说明	
Prefix ?	显示快捷键帮助	
Prefix :	进入命令模式	
Prefix C-z	挂起会话,不影响其他命令的运行,C 表示 Ctrl 键	
Prefix C-o	调换窗格位置	
Prefix 空格键	采用下一个内置布局	
Prefix !	把当前窗格(pane)变为新窗口(window)	
Prefix "	横向分隔窗格	
Prefix %	纵向分隔窗格	
Prefix q	显示分隔窗格的编号	
Prefix o	跳到下一个分隔窗格	
Prefix 上下键	上一个及下一个分隔窗格	
Prefix C-方向键	调整分隔窗格大小,C 表示 Ctrl 键	
Prefix z	最大化当前窗格,再一次则恢复	
Prefix c	创建新窗口	
Prefix 0~9	选择几号窗口	
Prefix n	选择下一个窗口	
Prefix p	选择前一个窗口	
Prefix l	切换到前一个窗口,该快捷键通常会被重定义为符合 vim 下的空格切换	
Prefix w	以菜单方式显示及选择窗口	
Prefix s	以菜单方式显示和选择会话	
Prefix t	显示时钟	
Prefix ;	切换到最后一个使用的面板	
Prefix x	关闭面板	
Prefix &	关闭窗口	
Prefix d	退出 Tmux,并保存当前会话,此时 Tmux 仍在后台运行,可以通过 Tmux attach 进入指定的会话