Day59 SPI驱动与ADXL345传感器应用及Linux系统移植基础
day59 SPI驱动与ADXL345传感器应用及Linux系统移植基础
本日内容涵盖SPI总线协议的底层配置、ADXL345加速度计的驱动实现,以及Linux内核驱动开发前的系统移植基础知识。我们将从裸机驱动的SPI初始化开始,逐步封装读写函数,最终在板卡上读取传感器数据并显示。随后,我们将过渡到Linux内核驱动领域,详细解析系统启动的三个核心阶段:Bootloader、Kernel和Rootfs,并介绍相关的硬件存储概念。
一、SPI总线底层驱动实现
1.1 硬件引脚配置与片选控制
首先,我们需要将i.MX6ULL芯片的UART2引脚复用为ECSPI3的信号线,并初始化片选引脚(GPIO1_IO20)为高电平,以确保设备处于非选中状态。
// spi.c
#include "../imx6ull/MCIMX6Y2.h"
#include "../imx6ull/core_ca7.h"
#include "../imx6ull/fsl_iomuxc.h"
#include "spi.h"void spi_init()
{// 将UART2的TX/RX/CTS/RTS引脚复用为ECSPI3的SS/SCLK/MOSI/MISOIOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 1); // SS (片选)IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 1); // SCLK (时钟)IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 1); // MOSI (主出从入)IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 1); // MISO (主入从出)// 配置引脚电气属性 (100K下拉, 100MHz, 40Ohm驱动能力)IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0x10B0);IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B0);IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B0);IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B0);// 将GPIO1_IO20配置为输出模式,并设置为高电平(片选无效)GPIO1->GDIR |= (1 << 20); // 设置为输出方向GPIO1->DR |= (1 << 20); // 输出高电平,取消片选
}
功能讲解: 此函数完成了SPI通信所需的硬件初始化。它通过IOMUXC_SetPinMux
将物理引脚的功能从UART切换到SPI,再通过IOMUXC_SetPinConfig
设置引脚的电气特性。最后,将片选引脚(GPIO1_IO20)配置为输出并置高,确保在初始化完成前SPI设备不会被意外选中。
1.2 SPI寄存器配置详解
接下来,我们配置ECSPI3控制器的核心寄存器,包括控制寄存器(CONREG
)和配置寄存器(CONFIGREG
),以设定通信参数。
// spi.c (续)
void spi_init()
{// ... (前面的引脚配置代码)// 配置ECSPI3控制寄存器 (CONREG)ECSPI3->CONREG = (0x7 << 20) | // 突发长度: 8位 (7+1=8)(14 << 12) | // 分频器: 2^14 (用于计算时钟频率)(2 << 8) | // 时钟分频: 2^2 = 4(1 << 4) | // 通道模式: 主机模式(1 << 3) | // 自动发送使能: 写入TXDATA后自动发送(1 << 0); // SPI使能: 开启SPI模块// 配置ECSPI3配置寄存器 (CONFIGREG)ECSPI3->CONFIGREG = (1 << 20) | // 时钟极性 (CPOL): 1 (空闲时钟为高)(1 << 4) | // 片选控制: 低电平有效(1 << 0); // 相位 (CPHA): 1 (数据在时钟第二个边沿采样)
}
功能讲解:
CONREG
: 设定了SPI的核心工作模式。(0x7 << 20)
: 设置突发长度为8位,即每次传输一个字节。(14 << 12) | (2 << 8)
: 计算SPI时钟频率。系统时钟为60MHz,经过(2^2)
分频后再经(2^14)
分频,最终得到约1.46MHz的SPI时钟,满足ADXL345最大5MHz的要求。(1 << 4)
: 设置为Master模式。(1 << 3)
: 启用自动发送功能,向TXDATA
写入数据后会自动触发传输。(1 << 0)
: 使能SPI模块。
CONFIGREG
: 设定了SPI的电气特性。(1 << 20)
: 设置CPOL=1,即时钟空闲时为高电平。(1 << 4)
: 设置片选信号为低电平有效。(1 << 0)
: 设置CPHA=1,即数据在时钟的第二个边沿采样。这与ADXL345的规格书要求一致。
1.3 SPI读写函数封装
根据SPI“全双工”特性(发送的同时接收),我们分别封装了写入和读取函数,并最终提供一个通用的传输函数。
// spi.c (续)
void spi_write(unsigned char data)
{unsigned char invalid_data = 0;// 等待发送缓冲区为空while(!(ECSPI3->STATREG & (1 << 0)));// 发送数据ECSPI3->TXDATA = data;// 等待接收缓冲区有数据 (此时接收到的是无效数据)while(!(ECSPI3->STATREG & (1 << 3)));// 读取并丢弃接收到的无效数据invalid_data = ECSPI3->RXDATA;
}unsigned char spi_read(void)
{unsigned char data = 0;// 等待发送缓冲区为空while(!(ECSPI3->STATREG & (1 << 0)));// 发送一个虚拟字节 (0xFF),以产生时钟来读取数据ECSPI3->TXDATA = 0xff;// 等待接收缓冲区有数据while(!(ECSPI3->STATREG & (1 << 3)));// 读取并返回接收到的有效数据data = ECSPI3->RXDATA;return data;
}unsigned char spi_transfer(unsigned char data)
{unsigned char r_data = 0;// 等待发送缓冲区为空while(!(ECSPI3->STATREG & (1 << 0)));// 发送数据ECSPI3->TXDATA = data;// 等待接收缓冲区有数据while(!(ECSPI3->STATREG & (1 << 3)));// 读取接收到的数据r_data = ECSPI3->RXDATA;return r_data;
}
功能讲解:
spi_write
: 用于向SPI设备写入一个字节。它先等待发送缓冲区空闲,然后写入数据。由于SPI是全双工的,写入的同时也会收到一个字节,这个字节对写操作而言是无效的,因此需要将其读出并丢弃。spi_read
: 用于从SPI设备读取一个字节。为了产生时钟信号以获取数据,它必须先向总线发送一个虚拟字节(通常为0xFF)。设备会在时钟作用下将数据放在MISO线上,程序再读取该数据。spi_transfer
: 这是一个通用的读写函数。它执行一次完整的SPI事务:发送一个字节,并同时接收一个字节。这个函数可以用于任何需要进行单字节交换的场景。
二、ADXL345加速度计驱动实现
2.1 ADXL345寄存器访问协议
ADXL345使用SPI协议进行通信。其读写命令格式如下:
- 写入:
0x00 + 寄存器地址
(最高位为0表示写操作) - 读取:
0x80 + 寄存器地址
(最高位为1表示读操作)
// adxl345.h
#ifndef __ADXL345_H
#define __ADXL345_Htypedef struct __adxl345_g
{unsigned short x;unsigned short y;unsigned short z;
}adxl345_g_t;void adxl345_write(unsigned char reg, unsigned char data);
unsigned char adxl345_read(unsigned char reg);
void adxl345_init(void);
void adxl345_get_data(adxl345_g_t * data);#endif
功能讲解: 头文件定义了一个结构体adxl345_g_t
用于存储XYZ三轴的加速度值,并声明了四个对外接口函数,供应用层调用。
2.2 驱动函数实现
// adxl345.c
#include "../imx6ull/MCIMX6Y2.h"
#include "../imx6ull/core_ca7.h"
#include "../imx6ull/fsl_iomuxc.h"
#include "adxl345.h"
#include "spi.h"void adxl345_write(unsigned char reg, unsigned char data)
{// 拉低片选,选中ADXL345GPIO1->DR &= ~(1 << 20);// 发送寄存器地址spi_transfer(reg);// 发送要写入的数据spi_transfer(data);// 拉高片选,取消选中GPIO1->DR |= (1 << 20);
}unsigned char adxl345_read(unsigned char reg)
{unsigned char data = 0;// 拉低片选,选中ADXL345GPIO1->DR &= ~(1 << 20);// 发送带读标志的寄存器地址spi_transfer(reg | 0x80);// 发送虚拟字节,读取数据data = spi_transfer(0xff);// 拉高片选,取消选中GPIO1->DR |= (1 << 20);return data;
}void adxl345_init(void)
{// 设置电源控制寄存器 (0x2D): 使能测量模式adxl345_write(0x2D, 0x08);// 设置数据格式寄存器 (0x31): 全分辨率模式,±16g量程adxl345_write(0x31, 0x08);// 设置数据速率寄存器 (0x2C): 设置为200Hzadxl345_write(0x2C, 0x0B);
}void adxl345_get_data(adxl345_g_t * data)
{// 读取X轴数据 (低8位)data->x = adxl345_read(0x32);// 读取X轴数据 (高8位),并左移8位与低8位合并data->x |= (adxl345_read(0x33) << 8);// 读取Y轴数据 (低8位)data->y = adxl345_read(0x34);// 读取Y轴数据 (高8位),并左移8位与低8位合并data->y |= (adxl345_read(0x35) << 8);// 读取Z轴数据 (低8位)data->z = adxl345_read(0x36);// 读取Z轴数据 (高8位),并左移8位与低8位合并data->z |= (adxl345_read(0x37) << 8);
}
功能讲解:
adxl345_write
: 实现了对ADXL345寄存器的写入操作。它首先拉低片选信号,然后通过spi_transfer
发送寄存器地址和数据,最后拉高片选信号结束通信。adxl345_read
: 实现了对ADXL345寄存器的读取操作。它拉低片选,发送带读标志的寄存器地址,再发送虚拟字节0xFF
以获取数据,最后拉高片选。adxl345_init
: 初始化ADXL345。它配置了三个关键寄存器:0x2D
: 设置为0x08
,启动测量模式。0x31
: 设置为0x08
,选择全分辨率模式和±16g量程。0x2C
: 设置为0x0B
,将数据输出速率设为200Hz。
adxl345_get_data
: 读取XYZ三轴的加速度原始数据。由于每个轴的数据是16位,存储在两个连续的寄存器中,因此需要分别读取高低8位并合并。
2.3 应用层测试代码
// main.c (节选)
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "core_ca7.h"
#include "led.h"
#include "beep.h"
#include "key.h"
#include "interrupt.h"
#include "clock.h"
#include "epit.h"
#include "gpt.h"
#include "uart.h"
#include "stdio.h"
#include "i2c.h"
#include "string.h"
#include "adc.h"
#include "lcd.h"
#include "framebuffer.h"
#include "pwm.h"
#include "touchscreen.h"
#include "adxl345.h"
#include "spi.h"int main(void)
{system_interrupt_init();clock_init();led_init();beep_init();gpt1_init();uart1_init();i2c_init(I2C1);i2c_init(I2C2);pwm1_init();lcd_init();clear_screen();spi_init(); // 初始化SPIunsigned char dev_id = 0; char buf[10] = {0};// 读取设备ID,验证通信是否正常dev_id = adxl345_read(0);sprintf(buf, "devid %02x", dev_id);lcd_show_string(100, 100, 200, 200, 32, buf, 0xff0000);adxl345_init(); // 初始化ADXL345while(1){unsigned char str[40] = {0};adxl345_g_t data;// 获取当前XYZ三轴数据adxl345_get_data(&data);// 格式化为字符串sprintf(str, "%d %d %d ", data.x, data.y, data.z);// 在LCD上显示lcd_show_string(100, 200, 200, 200, 32, str, 0xff0000);}return 0;
}
理想运行结果: 程序启动后,LCD屏幕上会先显示ADXL345的设备ID(应为0xE5
)。随后,屏幕下方会实时刷新并显示XYZ三轴的原始加速度数据。当移动或倾斜开发板时,这些数值会发生相应变化。
三、Linux内核驱动开发前置知识:系统移植基础
3.1 Linux系统启动的三个核心阶段
一个完整的Linux系统启动过程分为三个主要阶段:
- Bootloader (引导程序): 一个小型的裸机程序,负责为内核启动准备环境并引导内核启动。
- Kernel (内核): 操作系统的核心,负责管理系统的硬件资源和软件进程。
- Rootfs (根文件系统): 一个按特定格式组织的文件集合,包含了系统运行所需的所有程序、库和配置文件。
这三个阶段按顺序执行,缺一不可。
3.2 Bootloader详解
Bootloader是一个一次性执行的裸机程序,它的主要任务是在内核启动前完成一系列初始化工作。
主要功能:
- 初始化CPU: 设置CPU的工作模式。
- 初始化异常向量表: 为后续的中断和异常处理做准备。
- 关闭看门狗 (Watchdog): 防止在初始化过程中因超时而被复位。
- 初始化时钟: 配置系统时钟源和分频器。
- 初始化栈: 为C语言程序运行提供堆栈空间。
- 初始化内存: 配置SDRAM控制器,使其能够正常工作。
- 关闭缓存 (Cache): 关闭D-Cache(数据缓存),有时也关闭I-Cache(指令缓存),以确保直接访问硬件寄存器。
- 关闭MMU: 在内核初始化之前,通常关闭内存管理单元。
- 初始化外设: 如串口、网口等,用于调试或加载内核。
- 集成协议: 如TFTP、NFS等,用于从网络加载内核。
- 搬移内核: 将内核镜像从存储介质(如SD卡、eMMC)复制到内存中。
- 向内核传参: 告诉内核根文件系统的位置、类型、控制台等信息。
- 启动内核: 设置PC指针指向内核入口地址,将CPU控制权完全移交给内核。
关键点: Bootloader的任务完成后,其自身就不再运行,CPU的控制权彻底移交给了内核。
3.3 Kernel详解
内核是一个庞大而复杂的程序,它是操作系统的核心,负责管理系统的所有资源。
主要功能:
- 文件管理: 管理磁盘上的所有文件和目录。
- 进程管理: 创建、调度和销毁进程,并提供进程间通信机制。
- 内存管理: 管理物理内存和虚拟内存,为进程分配和回收内存空间。
- 网络管理: 提供网络协议栈,实现网络通信功能。
- 设备管理: 管理各种硬件设备,为应用程序提供统一的访问接口。
内核启动的最后阶段是加载并挂载根文件系统,然后启动init
进程,这是用户空间的第一个进程。
3.4 Rootfs详解
根文件系统是系统启动后加载的第一个文件系统,它是一个包含所有必要文件的集合。
包含内容:
- 配置文件: 如
/etc/fstab
,/etc/inittab
等。 - 系统命令: 如
ls
,cp
,mkdir
等。 - 服务程序: 如SSH服务器、Web服务器等后台服务。
- 库文件: 如
libc.so
等动态链接库。 - 用户程序: 用户安装的应用程序。
- 普通文件: 文本文件、图片、音频文件等。
挂载 (Mount): “挂载”是指将一个文件系统连接到现有文件系统树中的某个目录(挂载点)的过程。例如,将位于/dev/mmcblk0p2
的分区挂载到/
目录下。
3.5 存储器类型概览
- RAM (Random Access Memory): 随机存取存储器,访问速度快,但掉电后数据丢失。用于存放正在运行的程序和数据。
- ROM (Read Only Memory): 只读存储器,掉电后数据不丢失,但访问速度慢。早期用于存放固件。
- Flash: 闪存,结合了RAM和ROM的优点,速度快且掉电不丢失。现代嵌入式系统中广泛使用的存储介质,如SD卡、eMMC。
- DDR3: 第三代双倍数据速率同步动态随机存取存储器,是本开发板上使用的内存类型,容量为512MB。
- eMMC: 嵌入式多媒体卡,是一种集成了控制器的Flash存储设备,本开发板上容量为8GB。
3.6 系统烧写实践
我们使用Mfgtool2-eMMC-ddr512-SDCard.vbs
工具将预先编译好的Linux系统镜像烧录到SD卡中。
步骤:
- 断开开发板电源。
- 将SD卡从开发板拔出。
- 使用USB Type-C线将开发板连接到电脑。
- 打开烧写工具
Mfgtool2-eMMC-ddr512-SDCard.vbs
。 - 给开发板上电,等待工具识别到设备(显示为"Connected")。
- 将SD卡插入开发板的卡槽。
- 点击工具上的"Start"按钮,开始烧写。
- 等待两个进度条都走完,烧写完成。
- 拔掉USB线,将拨码开关拨至SD卡启动模式(通常是1和7位向上)。
- 上电启动,即可看到Linux系统成功运行。
此过程将完整的Linux系统(包括Bootloader, Kernel, Rootfs)部署到SD卡上,为后续的内核驱动开发做好了准备。