[Zephyr] 01-开发环境搭建

前言:因为需要做一个蓝牙项目,所以接触到了Zephyr操作系统。本文的内容主要来自官方的文档,有部分是自己的理解和笔记。我的开发过程都是在Ubuntu环境下进行的,Zephyr也支持在Windows和macOS环境下开发,但是该系列文章不会涉及Windows和macOS环境下的开发环境搭建以及开发。

免责声明:本文引用的文章、资料版权及所有权均归原作者所有。受限于自己的能力,本文不保证完全没有错误。如果有与官方文档不一致的地方请以官方文档为准!

1、Zephyr简介

Zephyr是专门面向微控制器(MCU)的实时操作系统。经过很多年的发展,现在的Zephyr已经能够很好的支持多种架构的CPU以及很多厂家的芯片,包括大家熟悉的STM32、GD32、NRF、ESP32等系列的芯片,大有成为MCU界的Linux之势。而事实上现在的Zephyr也是由Linux基金会和Wind River Systems Inc(Intel旗下的子公司)共同管理。在Zephyr身上确实也能看到Linux的影子,比如设备树,没错,Zephyr上也有设备树,不过Zephyr的设备树和Linux的设备树不能说有所不同,只能说完全不同。受限于MCU平台的资源,Zephyr的设备树不像Linux中会有单独的文件和设备树系统。而是在编译阶段发挥作用,在预编译阶段脚本会把.dts中的内容以宏定义的形式生成.h文件。

不过我觉得与其说Zephyr是一个嵌入式操作系统,不如说它是一套开发工具,因为除了操作系统内核Zephyr还包含了文件系统、驱动模型、电源管理、固件更新、密码学等等,Zephyr甚至还包含了蓝牙协议栈、USB协议栈、LVGL等等。

除了上面说的这些优势,最重要的是Zephyr是完全开源的,并且Zephyr 使用Apache 2.0 许可证,也就是说Zephyr是支持商用的。

更详细的介绍大家可以去看Zephyr官方文档中的介绍

2、安装

Zephyr的开发环境搭建相比Keil、MDK、IAR等大家熟悉的IDE那种无脑下一步的安装方式还是要麻烦不少,不管是什么平台都没法逃过命令行,不过大家也不用一看到命令行就发懵,其实常用的命令就那么几个,很快就可以熟悉的。

下面就介绍在Linux下的安装过程,这里我用Ubuntu系统,Zephyr还支持其它的Linux发行版,其它发行版请参照Zephyr官方文档。注意:Zephyr需要Ubuntu 18.04 LTS 及更高的版本!整个安装过程会涉及到很从外网服务器下载源码以及安装包的过程,需要多次尝试或者想别的办法解决。推荐使用Ubuntu20.04或以上版本,会容易一些。

更新软件

Linux在搭建一个环境之前我们一般都会先同步系统上的软件包列表为最新的状态并且更新本地的软件为最新的状态。

sudo apt update
sudo apt upgrade

安装依赖

工具版本
CMake3.20.0
Python3.8
Devicetree compiler1.4.6
Zephyr需要的主要软件依赖以及版本

当Ubuntu版本小于22.04时(实测20.04也不需要,官方文档说大于22.04不需要),需要添加额外的存储库来满足上面的主要依赖的最低版本需求,这里需要使用 Kitware 存档脚本将 Kitware APT 存储库添加到源列表中。

wget https://apt.kitware.com/kitware-archive.sh 
sudo bash kitware-archive.sh

如果是22.04版本就不需要上面这步。其余版本执行完脚本后就可以使用apt安装需要的依赖

sudo apt install --no-install-recommends git cmake ninja-build gperf \
  ccache dfu-util device-tree-compiler wget \
  python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \
  make gcc gcc-multilib g++-multilib libsdl2-dev libmagic1

安装完成后检查一下主要依赖的版本和上面表格中的版本,如果一致就进行下一步,如果不一致就需要手动调整版本。具体的解决方式参考官方安装Linux主机依赖项

cmake --version
python3 --version
dtc --version

获取Zephyr并且安装Python依赖项

Zephyr的安装、使用都依赖于west工具,west工具依赖Python环境,用过Python的朋友一定知道我们平时做Python环境的时候一般都不会全局配置,因为如果全局配置各个软件的兼容性会让你怀疑人生。所以安装Zephyr的时候我们同样使用Python的虚拟环境安装,之后使用的时候再激活环境就可以了,虽然听着好像麻烦了一点,但是如果全局安装遇到兼容性的问题的时候就知道虚拟环境有多美好了。

首先安装Python的venv包,然后创建虚拟环境并且激活环境:

sudo apt install python3-venv
# 在Home下创建了一个名叫zephyrproject的文件夹,后续Zephyr相关的内容都会放在这个文件夹下。这个文件夹的名字和路径可以自定义,但是后面涉及路径的就需要改为你自己的实际路径。
cd ~
mkdir zephyrproject
python3 -m venv ~/zephyrproject/.venv
source ~/zephyrproject/.venv/bin/activate
# 想要停用环境可以用
# source ~/zephyrproject/.venv/bin/deactivate

通过上面的步骤就安装并且激活了Python的虚拟环境,下面我们就在刚才激活的环境中安装west,并且获取Zephyr源代码:

pip install west
west init ~/zephyrproject
cd ~/zephyrproject
west update

成功获取Zephyr的源代码后就可以导出Zephyr Cmake包(让Cmake能够自动加载编译Zephyr应用程序所需的样板代码)并且安装Zephyr额外的依赖:

west zephyr-export
pip install -r ~/zephyrproject/zephyr/scripts/requirements.txt

安装Zephyr SDK

经过上面的步骤,我们已经安装了Zephyr的开发工具west,并且获取Zephyr的源码到我们的电脑中,想要将代码编译为各个实际的可执行文件还需要对应架构的工具链。Zephyr将各个平台的工具链都包含在了Zephyr SDK中,我们只需要安装Zephyr SDK就可以支持各个平台的编译了。

#下载并校验SDK包
cd ~
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.15.0/zephyr-sdk-0.15.0_linux-x86_64.tar.gz
wget -O - https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.15.0/sha256.sum | shasum --check --ignore-missing

#解压SDK包,Zephyr推荐了几个解压路径,这里我直接解压到home目录下
tar xvf zephyr-sdk-0.15.0_linux-x86_64.tar.gz

#进入解压后的文件夹,安装Zephyr SDK
cd zephyr-sdk-0.15.0
./setup.sh

#安装udev规则这样就可以以普通用户的身份刷写大多数 Zephyr 板
sudo cp ~/zephyr-sdk-0.15.0/sysroots/x86_64-pokysdk-linux/usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d
sudo udevadm control --reload

编译测试

至此Zephyr的开发环境就搭建完成了,现在可以尝试编译一下例程验证一下我们的环境搭建是否成功。

这里我使用NRF52840DK开发板,尝试运行LED闪烁的代码。

cd ~/zephyrproject/zephyr
west build -p always -b nrf52840dk_nrf52840 samples/basic/blinky
#下载
west flash

如果环境搭建成功,那么就会看到与我类似的编译输出。

编译完成后输出的内存占用情况

简单解释一下上面的west指令的含义,build代表这是编译命令,-p always表示编译前始终清除之前的编译内容,强制开始完整的编译过程-p 还可以跟auto ,表示自动判断是否需要清除之前的编译结果,如果不加-p指令,如果需要将当前的代码编译为别的板子上运行的代码就会报错。后面的-b是指当前要编译的板子类型,官方支持的板子都在zephyr->board目录下,我们常用的比如STM32,GD32,NRF52840这些都是arm32位的,都在arm目录下。ESP32在xtensa目录下,安装完成之后最好先自己翻着看看各个目录下都有些什么东西。

west flash 可以直接将编译后的代码烧写到板子中,但是在实际情况中经出会遇到比如编译是在统一的云服务器中,或者主机没办法连接调试器,这就需要我们将文件从服务器copy到本地用对应的烧写工具烧写。这里推荐一个叫做NetDrive2的软件,可以很方便的将服务器的文件夹映射成为本地磁盘,当然还有很多别的方式,这里用自己最熟悉的就可以。

至此,环境的安装,验证都完成了,看似整个安装过程很复杂其实归根结底只有三个步骤:

  • 准备环境,依赖、python
  • 安装west,获取源码
  • 安装SDK

安装过程就写到这里,下一篇文章将会讲解如何创建自定义的板级描述文件,并且创建自己的工程。

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);
}