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; // 返回包含三轴数据的结构体
}
数据处理,最终得到重力加速度:
