ARM.8(ADC,SPI)

1.ADC

ADC(Analog-to-Digital Converter,模数转换器) ​ 是一种将连续的模拟电压信号 (如温度、电位器、电池电压)转换为 CPU 可以处理的离散数字量 ​ 的外设。在 i.MX6ULL 上,ADC 通常是 12 位逐次逼近型(SAR) ,输入电压范围一般为 0~3.3V ,转换公式为 数字值 = 4095 × (输入电压 / 参考电压)

逐次逼近,产生二进制量化信号,比较器越多,分辨率越高。

例:计算真实电压:

电压 = ADC值 × (3.3V / 255)

通过查手册我们完成对ADC1模块的初始化,校对,以及采样值提取:

cs 复制代码
#include "adc.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h" 
#include"stdio.h"

void init_adc1_channele1(void) // 初始化ADC1通道1的函数
{
    // 配置GPIO1_IO01引脚复用为模拟输入功能
    IOMUXC_SetPinMux(IOMUXC_GPIO1_IO01_GPIO1_IO01, 0); // 设置引脚复用为GPIO1_IO01(ADC输入)
    // 配置引脚电气属性:使能内部上拉/下拉保持器
    IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO01_GPIO1_IO01, IOMUXC_SW_PAD_CTL_PAD_PKE(1));
    // 配置引脚电气属性:关闭内部上拉电阻,选择下拉或保持
    IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO01_GPIO1_IO01, IOMUXC_SW_PAD_CTL_PAD_PUE(0));

    // 清除ADC1硬件触发通道0的最高位,禁用硬件触发
    ADC1->HC[0] &= ~(1 << 7);
    // 清零ADC1配置寄存器
    ADC1->CFG = 0;
    // 配置ADC1:设置平均次数(2<<2表示4次平均),时钟源选择(3<<0表示时钟源为IPG时钟/2)
    ADC1->CFG |= (2 << 2) | (3 << 0);
    // 清零ADC1全局控制寄存器
    ADC1->GC = 0;
    // 使能ADC1模块(使能位设为1)
    ADC1->GC |= (1 << 0);
    // 调用校准函数并打印校准结果
    printf(adc1_clibration() ? "Calibration completed normally." : "Calibration failed. ");
}

int adc1_clibration(void) // ADC1自动校准函数
{
    // 设置ADC1全局控制寄存器的校准位(第7位),启动校准
    ADC1->GC |= (1 << 7);
    // 等待校准完成(等待校准位自动清零)
    while((ADC1->GC & (1 << 7)) != 0);
    // 检查ADC1全局状态寄存器的校准失败标志位(第1位)
    if((ADC1->GS & (1 << 1)) == 0) // 如果校准失败标志位为0
    {
        return 1; // 校准成功,返回1
    }
    else // 如果校准失败标志位为1
    {
        return 0; // 校准失败,返回0
    }
}

unsigned short adc1_get_origin_value(void) // 获取ADC1通道0的原始转换值函数
{
    ADC1->HC[0] = 0; // 先清零ADC1通道0的硬件触发控制寄存器,准备新的转换
    ADC1->HC[0] = 1; // 设置ADC1通道0的硬件触发控制寄存器为1,启动AD转换(位0=1表示启动转换)
    while((ADC1->HS & (1 << 0)) == 0); // 等待ADC1通道0的状态寄存器转换完成标志位(位0)置1,表示转换完成
    return ADC1->R[0] & 0xFFF; // 读取ADC1通道0的数据寄存器,只保留低12位有效数据(0xFFF=4095,即12位ADC转换结果)
}

再进行手动中值滤波:

cs 复制代码
void swap(unsigned short *a, unsigned short *b)//交换两个无符号短整型变量的值
{
    unsigned short t = *a;  
    *a = *b;                
    *b = t;                
}


void sort(unsigned short *a, int len)
{
    int i, j;
    for(i = 0; i < len - 1; ++i)          
    {
        for(j = i + 1; j < len; ++j)     
        {
            if(a[i] > a[j])                
            {
                swap(a + i, a + j);        
            }
        }
    }
}

unsigned int sum_of_array(unsigned short *a, int len)
{
    unsigned int sum = 0;  
    int i;
    for(i = 0; i < len; ++i)   
    {
        sum += a[i];          
    }
    return sum;               
}

/**
 * @brief 获取ADC1通道的精确电压值(带软件滤波)
 * @return 转换后的电压值,单位:伏特(V)
 * 
 * 实现原理:
 * 1. 采集100次原始ADC值(每采集一次,硬件转换完成后读取结果寄存器)
 * 2. 对100个数据进行升序排序
 * 3. 剔除前10个最小值和后10个最大值(去除极值干扰)
 * 4. 对中间80个有效数据进行算术平均
 * 5. 将平均ADC值转换为电压值
 *    - 12位ADC的分辨率为4096(0~4095)
 *    - 参考电压为3.3V
 *    - 转换公式:电压 = ADC值 / 4095 * 3.3
 */
float adc1_get_value(void)
{
    unsigned short a[100]; 
    int i;
    
    // 步骤1:采集100次原始ADC值
    for(i = 0; i < 100; ++i)
    {
        a[i] = adc1_get_origin_value();  // 调用底层函数获取单次转换值
    }
    
    // 步骤2:对100个数据进行升序排序
    sort(a, sizeof(a) / sizeof(a[0]));   // sizeof(a)/sizeof(a[0]) = 100
    
    // 步骤3-5:剔除前后各10个极值,对中间80个数据求平均并转换电压
    // a + 10:跳过前10个最小值,从第11个开始
    // 80:取中间80个数据(从第11个到第90个)
    // / 80:求算术平均值
    // / 4095.0 * 3.3:将12位ADC原始值转换为电压值(0~4095对应0~3.3V)
    return sum_of_array(a + 10, 80) / 80 / 4095.0 * 3.3;
}

2.SPI

SPI(Serial Peripheral Interface,串行外设接口) ​ 是一种高速串行同步全双工通信总线,主要用于 MCU 与外围芯片 之间的短距离通信。如 EEPROM,FLASH,实时时钟,AD 转换器,还有数字信号处理器和数字信号解码器之间。

I2C)和SPI的主要区别在于:I2C使用两根线(SDA数据线和SCL时钟线)通过设备地址寻址支持多主多从通信,传输速度较慢(标准模式100kbps-400kbps),但引脚少、适合连接多个设备;而SPI使用四根线(MOSI、MISO、SCK、CS)通过片选信号选择设备,仅支持单主多从,传输速度快(可达几十Mbps),但每个从设备需要独立的片选线,引脚较多。简单来说,I2C节省引脚、适合低速多设备场景,SPI速度快、适合高速实时通信但占用引脚多。

SPI的数据传输的时序:要理解SPI的通信时序,需要搞清楚SPI的两个重要概念:时钟极性(Clock Polarity,简称CPOL)和时钟相位(Clock Phase,简称CPHA)。注意这两个概念都是跟时钟信号线相关的。

➢ CPOL:规定了时钟信号线在空闲时的状态,CPOL=0表示时钟信号线在空闲时为低电平;CPOL=1表示时钟信号线在空闲时为高电平;

➢ CPHA:与I2C不同,SPI的数据接收方并不是在时钟高或者低电平时采样的,而是在时钟信号处于沿时,至于是在上升沿还是下降沿取决于CPHA。CPHA=0表示是在时钟信号变化的第一个沿时采样,CPHA=1表示是在时钟信号变化的第二个沿时采样。

因此不难理解,这样的排列组合总共能够组合出四种方式,具体使用哪种方式是由外设厂家规定的,因此在使用SPI之前就不得不读懂外设的说明文档。

2.1ADXL345 三轴数字加速度计(本质ADC)

通过 SPI​ 接口与i.MX6ULL 主控进行通信

ADXL345时序图:

设置引脚复用以及SPI初始化:

2.2对SPI初始化

cs 复制代码
void init_spi3(void)
{
    // 配置SPI3的4个引脚复用功能
    IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0);    // 配置UART2_RX_DATA引脚复用为ECSPI3的时钟线SCLK
    IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0);      // 配置UART2_CTS_B引脚复用为ECSPI3的主机输出从机输入线MOSI
    IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0);      // 配置UART2_RTS_B引脚复用为ECSPI3的主机输入从机输出线MISO
    IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0);     // 配置UART2_TX_DATA引脚复用为GPIO1_IO20(用作片选CS信号)

    // 配置SPI3引脚的电气属性(驱动能力、开漏、压摆率等),0x10B1为标准配置值
    IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B1);   
    IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B1);   
    IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B1);    
    IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0x10B1); 
    // 初始化片选引脚GPIO1_IO20为输出模式,默认输出高电平(失能状态)
    struct GPIO_Type_t t = 
    {
        .direction = gpio_output,   // 设置为输出方向
        .defalut_value = 1          // 默认输出高电平(片选无效)
    };
    init_gpio(GPIO1, 20, &t);       // 调用GPIO初始化函数

    // 配置ECSPI3控制寄存器
    ECSPI3->CONREG = 0;             // 清零CONREG寄存器
    ECSPI3->CONREG |= (7 << 20) |    // 设置突发长度:7+1=8位(即每个字节传输8位)
                       (0x0E << 12) | // 设置时钟分频因子,产生SCLK时钟频率
                       (2 << 8) |     // 设置通道模式:通道2
                       (1 << 4) |     // 设置SPI模式:使用SPI(非FIFO模式)
                       (1 << 3) |     // 设置MSB先行(先发送最高有效位)
                        (1 << 0);// 使能ECSPI3模块
    ECSPI3->CONFIGREG = 0;          // 清零配置寄存器
    ECSPI3->CONFIGREG |= (1 << 20) | // 禁用SCLK时钟线在空闲时的无效状态
                         (1 << 4) |  // 设置片选极性:低电平有效
                         (1 << 0);   // 设置通道0的片选极性
}

2.3SPI相关函数

SPI读既是写,写既是读

spi读写:

cs 复制代码
unsigned char spi3_read_write(unsigned char data)
{
    // 等待发送FIFO非满(TXFIFO有空闲位置)
    while(((ECSPI3)->STATREG & (1 << 0)) == 0);
    ECSPI3->TXDATA = data;          // 将数据写入发送寄存器,启动传输

    // 等待接收FIFO非空(RXFIFO有数据可读)
    while(((ECSPI3)->STATREG & (1 << 3)) == 0);
    return ECSPI3->RXDATA;          // 读取接收寄存器并返回数据
}

2.4adxl345相关函数

adxl345读:

cs 复制代码
/**
 * @brief 从ADXL345加速度计读取一个字节数据
 * @param reg_address 寄存器地址
 * @return 读取到的数据
 * 
 * ADXL345 SPI通信协议:
 * - 片选CS拉低(低电平有效)启动通信
 * - 第一个字节:寄存器地址 | 0x80(最高位为1表示读操作)
 * - 第二个字节:发送任意数据(如0xFF),同时接收从设备返回的数据
 * - 片选CS拉高结束通信
 */
unsigned char adxl345_read(unsigned char reg_address)
{
    unsigned char ret;
    write_gpio(GPIO1, 20, 0);                   // 拉低片选引脚,选中ADXL345设备
    spi3_read_write(reg_address | 0x80);        // 发送寄存器地址,最高位或1表示读操作(0x80 = 1000 0000)
    ret = spi3_read_write(0xFF);                // 发送任意时钟数据(0xFF),同时接收读取到的数据
    write_gpio(GPIO1, 20, 1);                   // 拉高片选引脚,结束通信
    return ret;                                 // 返回读取到的数据
}

adxl345写:

cs 复制代码
/**
 * @brief 向ADXL345加速度计写入一个字节数据
 * @param reg_address 寄存器地址
 * @param data 要写入的数据
 * 
 * ADXL345 SPI通信协议:
 * - 片选CS拉低(低电平有效)启动通信
 * - 第一个字节:寄存器地址(最高位为0表示写操作)
 * - 第二个字节:要写入的数据
 * - 片选CS拉高结束通信
 */
void adxl345_write(unsigned char reg_address, unsigned char data)
{
    write_gpio(GPIO1, 20, 0);           // 拉低片选引脚GPIO1_IO20,选中ADXL345设备
    spi3_read_write(reg_address);       // 发送寄存器地址(写操作,最高位为0)
    spi3_read_write(data);              // 发送要写入的数据
    write_gpio(GPIO1, 20, 1);           // 拉高片选引脚,结束通信
}

2.5adxl345测重力加速度

初始化adxl345:

cs 复制代码
/**
 * @brief 初始化ADXL345加速度计
 * 
 * 配置寄存器说明:
 * - 0x2E(中断使能寄存器):写入0x08,使能DATA_READY中断(数据就绪中断)
 * - 0x31(数据格式寄存器):写入0x0B,设置为±16g量程,4线SPI模式,高电平有效
 * - 0x2C(带宽速率寄存器):写入0x0A,设置输出数据速率为100Hz
 * - 0x2D(电源控制寄存器):写入0x08,设置为测量模式(开始采集数据)
 */
void init_adxl345(void)
{
    adxl345_write(0x2E, 0x08);   // 配置中断使能寄存器:使能DATA_READY中断
    adxl345_write(0x31, 0x0B);   // 配置数据格式寄存器:±16g量程,4线SPI
    adxl345_write(0x2C, 0x0A);   // 配置带宽速率寄存器:输出数据速率100Hz
    adxl345_write(0x2D, 0x08);   // 配置电源控制寄存器:设置为测量模式
}

读取xyz轴的加速度数据:

cs 复制代码
/**
 * @brief 读取ADXL345的三轴加速度数据
 * @return 包含X、Y、Z轴数据的结构体
 * 
 * 数据格式:
 * - 每个轴的数据存储在相邻的两个寄存器中
 * - 低字节在前(Little Endian),高字节在后
 * - X轴:0x32(低字节)和0x33(高字节)
 * - Y轴:0x34(低字节)和0x35(高字节)
 * - Z轴:0x36(低字节)和0x37(高字节)
 * - 最终值为16位有符号整数
 */
struct ADXL345_Data adxl345_work(void)
{
    struct ADXL345_Data ret;
    
    // 读取X轴数据(低字节 + 高字节左移8位合成16位数据)
    ret.x = adxl345_read(0x32);      // 读取X轴低字节(bits 7:0)
    ret.x |= adxl345_read(0x33) << 8; // 读取X轴高字节(bits 15:8)左移8位后或运算合并
    
    // 读取Y轴数据
    ret.y = adxl345_read(0x34);      // 读取Y轴低字节
    ret.y |= adxl345_read(0x35) << 8; // 读取Y轴高字节左移8位后合并
    
    // 读取Z轴数据
    ret.z = adxl345_read(0x36);      // 读取Z轴低字节
    ret.z |= adxl345_read(0x37) << 8; // 读取Z轴高字节左移8位后合并
    
    ret.xg = ret.x * 0.0039;
    ret.xg = ret.y * 0.0039;
    ret.xg = ret.z * 0.0039;
    return ret;                      // 返回包含三轴数据的结构体
}

数据处理,最终得到重力加速度:

相关推荐
hoiii1872 小时前
基于 STM32 的标准遥控器架构与源码
stm32·嵌入式硬件·架构
少年、潜行2 小时前
STM32 ISP 升级体验
stm32·嵌入式硬件·isp升级·系统编程区域
杨连江2 小时前
一种三模式可调气隙式双侧定子滑移可变磁通轴向永磁电机
单片机·嵌入式硬件
Aaron158811 小时前
无人机反制中AOA+TDOA联合定位技术与雷达探测定位技术的应用对比分析
arm开发·嵌入式硬件·fpga开发·硬件工程·无人机·信息与通信·信号处理
foundbug99912 小时前
STM32 睡眠模式测试程序
stm32·单片机·嵌入式硬件
wxmtwfx14 小时前
littlefs 源码分析
单片机·littlefs·嵌入式文件系统
嵌入式小站15 小时前
STM32 零基础可移植教程 18:I2C 入门,先用扫描器找一找总线上有没有设备
chrome·stm32·嵌入式硬件
天涯铭16 小时前
深入浅出:单片机I/O口串联电阻选型
单片机·嵌入式硬件·io口串联电阻
国科安芯16 小时前
ASP7A84AS——航天级低噪声高PSRR线性稳压器
网络·单片机·嵌入式硬件·架构·安全性测试