很久以前听说过硬件IIC在其他单片机上,难用也试图调试硬件IIC,调整通过,但很容易卡住,所以默默地给硬件IIC贴上不稳定标签,然后用软件模拟IIC。
CH582这款单片机主攻蓝牙相关功能,还有硬件IIC这个模块。考虑到低功耗蓝牙严格控制时间,与软件相比IIC,硬件IIC能够帮助节省模拟时序代码中“无意义”的等待,且时序更精准,能达到更高的通信速率,更契合在蓝牙方面的应用,再加上有中断功能,还是有不错的应用价值的。
以下代码以MPU以6050外设为例,简单验证了三个方向的加速度读取值-旋转MPU各方向读取的数据发生了相应的变化。
以后会更新一篇使用硬件的文章IIC与EEPROM外设通信测试最大的区别在于EEPROM2字节数据表示内存地址。
先贴一波MPU与6050寄存器相关的宏定义:
#define SMPLRT_DIV 0x19 //陀螺仪采样率典型值0x07 1000/(1 7)=125HZ #define CONFIG 0x1A //低通滤波器 典型值0x06 5hz #define GYRO_CONFIG 0x1B //陀螺仪测量范围 0x18正负2000度 #define ACCEL_CONFIG 0x1C ////加速度计测量范围 0x18正负16g #define ACCEL_CONFIG2 0x1D ///加速度计低通滤波器 0x06 5hz #define USER_CTRL 0x6A ///用户配置为0x10时使用SPI模式 #define PWR_MGMT_1 0x6B ///电源管理1典型值0x00 #define PWR_MGMT_2 0x6C ///电源管理2典型值0x00 #define WHO_AM_I 0x75 //器件ID MPU6050默认返回值0x68 #define I2C_ADDR_MPU6050 0xD0 ///左移一位后的地址 //0x68 ///没有左移一位的7位地址 #define ACCEL_XOUT_H 0x3B ////加速度计输出数据 #define ACCEL_XOUT_L 0x3C #define ACCEL_YOUT_H 0x3D #define ACCEL_YOUT_L 0x3E #define ACCEL_ZOUT_H 0x3F #define ACCEL_ZOUT_L 0x40 #define TEMP_OUT_H 0x41 ///温度计输出数据 #define TEMP_OUT_L 0x42 #define GYRO_XOUT_H 0x43 //陀螺仪输出数据 #define GYRO_XOUT_L 0x44 #define GYRO_YOUT_H 0x45 #define GYRO_YOUT_L 0x46 #define GYRO_ZOUT_H 0x47 #define GYRO_ZOUT_L 0x48
硬件IIC难用部分归功于标志位——保证其可靠性,但也限制了其稳定性,错过了一个ACK可能会导致卡死,导致下次判决忙碌Busy等他问题。以下是相关标志位的粘贴。详见详细描述CH58x系列手册和EVT包中的IIC接口使用指南。
/*各标志位 * AF: 应答失败 * BUSY: 当检测到停止位时,总线忙于标志位置 * MSL: 当接口处于主模式时,主从模式指示位(SB=1)硬件位置 * SB: 起始位发送标志位,读取状态寄存器 1 后写数据寄存器的操作将清除该位置 * ADDR: 地址发送/地址匹配标志位,用户读取状态寄存器 一、状态寄存器 2的读操作将被清除 * TxE: 数据寄存器为空标志位,可以通过将数据写入数据寄存器进行清除,也可以通过硬件自动清除,从而产生起始或停止位置。TxE=0表示非空 * TRA: 发送 /接收标志位 ,在检测到停止事件时(STOPF=1)硬件清除重复的起始条件。根据地址字节 R/W位来决定 * BTF: 字节发送结束时,用户读取状态寄存器 1后,数据寄存器的读写将被清除;在传输过程中,硬件将在启动或停止事件后清除次位 */
IIC这里不再介绍阅读和写作的顺序和协议特征,很多信息可以在网上找到。
就像单片机上的其他硬件资源一样,记得在使用前初始化。MPU6050模块上有时钟线和数据线的上拉电阻,最好加3个自制板或焊接.3v的上拉。
GPIOB_ModeCfg(GPIO_Pin_12 | GPIO_Pin_13, GPIO_ModeIN_PU); //PB12:SDA,PB13:SCL 内部上拉较弱,可能需要外部上拉 I2C_Init(I2C_Mode_I2C, 400000, I2C_DutyCycle_16_9, I2C_Ack_Enable, I2C_AckAddr_7bit, Host_No_Addr); //选择IIC模式,400k速率,选择空比,默认打开ACK(必须打开接收模式),从机时使用的地址位数为7位,从机时使用的地址(本测试中单片机为主机模式,后两个参数无效)
习惯性地,我在用MPU在6050之前,问问它是谁(只有一个MPU如果是6050,识别设备ID也可以,直接初始化)。MPU一般6050模块上都有一个模块AD0引脚或焊盘,悬挂或接地,阅读WHO_AM_I寄存器,将返回0x68;连接高电平,将返回0x69,此时I2C_ADDR_MPU6050这个宏这个宏设为0x68左移1位,也要改为0x69左移一位。
temp_data = IIC_read_reg(I2C_ADDR_MPU6050, (WHO_AM_I | 0x80)); //获取器件ID PRINT("0x%2x\n", temp_data);
这就需要读取寄存器的代码以下是返回字节的读取代码↓
///从从机器的寄存器读取字节数据 uint8_t IIC_read_reg(uint8_t addr, uint8_t reg) { uint8_t data = 0; ///主机通知从机读取它的寄存器 while(I2C_GetFlagStatus(I2C_FLAG_BUSY)); //IIC主机判忙 I2C_GenerateSTART(ENABLE); ///开始信号 while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT)); //判断BUSY, MSL and SB flags I2C_Send7bitAddress(addr, I2C_Direction_Transmitter); //发送地址 最低位0表示写 while(!I2C_CheckEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); //判断BUSY, MSL, ADDR, TXE and TRA flags I2C_SendData(reg); ////送寄存器的地址 while(!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED)); //判断TRA, BUSY, MSL, TXE and BTF flags ////直接生成一个重起始信号开始阅读的过程 I2C_GenerateSTART(ENABLE); ///重启信号 while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT)); //判断BUSY, MSL and SB flags I2C_Send7bitAddress(addr, I2C_Direction_Receiver); //发送地址 最低位1表示读 while(!I2C_CheckEvent(I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); //判断BUSY, MSL and ADDR flags I2C_GenerateSTOP(DISABLE); ///关闭停止信号 I2C_AcknowledgeConfig(DISABLE); //关闭ACK使能,主机字节数据后,主机会返回NACK表示不再接收数据 while(!I2C_GetFlagStatus(I2C_FLAG_RXNE)); //获取RxEN的状态,等待收到数据 data = I2C_ReceiveData(); ///从机寄存器获取数据 I2C_GenerateSTOP(ENABLE); //停止信号 I2C_AcknowledgeConfig(ENABLE); ///传输,再次打开ACK使能 return data; }
在识别了设备ID之后,就可以对其进行初始化。
void MPU6050Init()
{
IIC_write_reg(I2C_ADDR_MPU6050, USER_CTRL, 0x00); //配置为0x10时为SPI通信模式
IIC_write_reg(I2C_ADDR_MPU6050, PWR_MGMT_1, 0x00); //解除休眠状态
IIC_write_reg(I2C_ADDR_MPU6050, SMPLRT_DIV, 0x02); //333HZ采样率
IIC_write_reg(I2C_ADDR_MPU6050, CONFIG, 0x03); //设置低通滤波,频率见手册 1K/ 1+2 = 333HZ
IIC_write_reg(I2C_ADDR_MPU6050, GYRO_CONFIG, 0x18); //角速度计2000°/s量程
IIC_write_reg(I2C_ADDR_MPU6050, ACCEL_CONFIG, 0x10); //加速度计8g量程
}
这又需要写寄存器的代码↓
//向从机的某寄存器,写入一个字节的数据
void IIC_write_reg(uint8_t addr, uint8_t reg, uint8_t data)
{
//主机通知从机要写它的哪个寄存器
while(I2C_GetFlagStatus(I2C_FLAG_BUSY)); //IIC主机判忙
I2C_GenerateSTART(ENABLE); //起始信号
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT)); //判断BUSY, MSL and SB flags
I2C_Send7bitAddress(addr, I2C_Direction_Transmitter); //发送地址+最低位0表示为“写”
while(!I2C_CheckEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); //判断BUSY, MSL, ADDR, TXE and TRA flags
I2C_SendData(reg); //发送寄存器的地址
//ACK之后直接写入数据
while(!I2C_GetFlagStatus(I2C_FLAG_TXE)); //获取TxE的状态 数据寄存器为空标志位,可以向其中写数据
I2C_SendData(data); //发送寄存器的地址
while(!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED)); //判断TRA, BUSY, MSL, TXE and BTF flags
I2C_GenerateSTOP(ENABLE); //停止信号
}
至此,就可以读取传感器相应的寄存器的数值了。MPU6050返回的主要数值大多是16位的数据,使用以下函数一次读两个字节数据↓(一个字节一个字节读也行,慢点罢了)
//从从机的某寄存器起始,读取并返回16位数据
uint16_t IIC_read_reg_2Bytes(uint8_t addr, uint8_t reg)
{
uint8_t dataH = 0, dataL = 0;
uint16_t data = 0;
//主机通知从机要读取它的哪个寄存器
while(I2C_GetFlagStatus(I2C_FLAG_BUSY)); //IIC主机判忙
I2C_GenerateSTART(ENABLE); //起始信号
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT)); //判断BUSY, MSL and SB flags
I2C_Send7bitAddress(addr, I2C_Direction_Transmitter); //发送地址+最低位0表示为“写”
while(!I2C_CheckEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); //判断BUSY, MSL, ADDR, TXE and TRA flags
I2C_SendData(reg); //发送寄存器的地址
while(!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED)); //判断TRA, BUSY, MSL, TXE and BTF flags
//直接产生一个重起始信号即可开始读的过程
I2C_GenerateSTART(ENABLE); //重起始信号
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT)); //判断BUSY, MSL and SB flags
I2C_Send7bitAddress(addr, I2C_Direction_Receiver); //发送地址+最低位1表示为“读”
while(!I2C_CheckEvent(I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); //判断BUSY, MSL and ADDR flags
I2C_GenerateSTOP(DISABLE); //关闭停止信号使能
while(!I2C_GetFlagStatus(I2C_FLAG_RXNE)); //获取RxEN的状态,等待收到数据
dataH = I2C_ReceiveData(); //获得从机的寄存器中的数据
I2C_AcknowledgeConfig(DISABLE); //清除ACK位 主设备为了能在收到最后一个字节后产生一个NACK脉冲,
//必须在读取倒数第二个字节之后(倒数第二个RxNE 事件之后)清除ACK位(ACK=0)
while(!I2C_GetFlagStatus(I2C_FLAG_RXNE)); //获取RxEN的状态
dataL = I2C_ReceiveData(); //获得从机地址的寄存器地址中的数据
I2C_GenerateSTOP(ENABLE); //使能停止信号
I2C_AcknowledgeConfig(ENABLE); //传输完毕,再次打开ACK使能
data = (uint16_t)(dataH<<8) + dataL;
return data;
}
为了验证代码运行的稳定性,加上随机延时,将下列代码跑了一个下午,也没有卡死(在上面的函数中我一直用while(判断标志)的死循环来等待,一旦标志位不对就会卡死),动一动传感器有相应的数据变化,就算成功了。
while(1)
{
DelayMs(100+rand()%300);
temp_data = IIC_read_reg(I2C_ADDR_MPU6050, (WHO_AM_I | 0x80)); //获取器件ID
PRINT("0x%2x\n", temp_data);
DelayMs(100+rand()%300);
MPU6050Init();
DelayMs(50+rand()%300);
mpu_acc_x = IIC_read_reg_2Bytes(I2C_ADDR_MPU6050, ACCEL_XOUT_H); //获取X轴的加速度
PRINT("X:0x%4x\n",mpu_acc_x);
DelayMs(50+rand()%300);
mpu_acc_y = IIC_read_reg_2Bytes(I2C_ADDR_MPU6050, ACCEL_YOUT_H); //获取Y轴的加速度
PRINT("Y:0x%4x\n",mpu_acc_y);
DelayMs(50+rand()%300);
mpu_acc_z = IIC_read_reg_2Bytes(I2C_ADDR_MPU6050, ACCEL_ZOUT_H); //获取Z轴的加速度
PRINT("Z:0x%4x\n",mpu_acc_z);
}
IIC通信协议中是可以连续读/写n个字节的数据的,手头的这块MPU6050也支持这样的操作,代码如下↓
//从从机的某寄存器起始,连续读取n个字节的数据
void IIC_read_reg_nBytes(uint8_t addr, uint8_t reg, uint8_t *des, uint8_t len)
{
//主机通知从机要读取它的哪个寄存器
while(I2C_GetFlagStatus(I2C_FLAG_BUSY)); //IIC主机判忙
I2C_GenerateSTART(ENABLE); //起始信号
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT)); //判断BUSY, MSL and SB flags
I2C_Send7bitAddress(addr, I2C_Direction_Transmitter); //发送地址+最低位0表示为“写”
while(!I2C_CheckEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); //判断BUSY, MSL, ADDR, TXE and TRA flags
I2C_SendData(reg); //发送寄存器的地址
while(!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED)); //判断TRA, BUSY, MSL, TXE and BTF flags
//直接产生一个重起始信号即可开始读的过程
I2C_GenerateSTART(ENABLE); //重起始信号
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT)); //判断BUSY, MSL and SB flags
I2C_Send7bitAddress(addr, I2C_Direction_Receiver); //发送地址+最低位1表示为“读”
while(!I2C_CheckEvent(I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); //判断BUSY, MSL and ADDR flags
I2C_GenerateSTOP(DISABLE); //关闭停止信号使能
for(uint8_t i=0; i<len; i++)
{
if(i == len-1)
I2C_AcknowledgeConfig(DISABLE); //清除ACK位 主设备为了能在收到最后一个字节后产生一个NACK脉冲,
//必须在读取倒数第二个字节之后(倒数第二个RxNE 事件之后)清除ACK位(ACK=0)
while(!I2C_GetFlagStatus(I2C_FLAG_RXNE)); //获取RxEN的状态,等待收到数据
*(des+i) = I2C_ReceiveData(); //获得从机的寄存器中的数据
}
I2C_GenerateSTOP(ENABLE); //使能停止信号
I2C_AcknowledgeConfig(ENABLE); //传输完毕,再次打开ACK使能
}
//向从机某寄存器起始,连续写入n个字节的数据
void IIC_write_reg_nBytes(uint8_t addr, uint8_t reg, uint8_t *src, uint8_t len)
{
//主机通知从机要写它的哪个寄存器
while(I2C_GetFlagStatus(I2C_FLAG_BUSY)); //IIC主机判忙
I2C_GenerateSTART(ENABLE); //起始信号
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT)); //判断BUSY, MSL and SB flags
I2C_Send7bitAddress(addr, I2C_Direction_Transmitter); //发送地址+最低位0表示为“写”
while(!I2C_CheckEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); //判断BUSY, MSL, ADDR, TXE and TRA flags
I2C_SendData(reg); //发送寄存器的地址
//ACK之后直接写入数据
for(uint8_t i=0; i<len; i++)
{
while(!I2C_GetFlagStatus(I2C_FLAG_TXE)); //获取TxE的状态 数据寄存器为空标志位,可以向其中写数据
I2C_SendData(*(src+i)); //发送寄存器的地址
}
while(!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED)); //判断TRA, BUSY, MSL, TXE and BTF flags
I2C_GenerateSTOP(ENABLE); //停止信号
}
使用连续读/写的代码后,初始化和读传感器数值,就变成下面这个样子↓
void MPU6050Init()
{
uint8_t temp_str1[2] = {0};
IIC_write_reg_nBytes(I2C_ADDR_MPU6050, USER_CTRL, temp_str1, 2);
uint8_t temp_str2[4] = {0x02, 0x03, 0x18, 0x10};
IIC_write_reg_nBytes(I2C_ADDR_MPU6050, SMPLRT_DIV, temp_str2, 4);
}
while(1)
{
DelayMs(300+rand()%300);
MPU6050Init();
PRINT("Init_OK\n");
DelayMs(300+rand()%300);
IIC_read_reg_nBytes(I2C_ADDR_MPU6050, ACCEL_XOUT_H,acc_str,6);
for(uint8_t i=0; i<6; i++)
PRINT("0x%2x ",acc_str[i]);
PRINT("\n");
}
又挂着跑了三个多小时,也没有出现卡死的情况,传感器数据变化正常。
回过头来看,使用硬件IIC去写数据,没有磕绊,但在读数据时,要注意两点:
①在读最后一个字节的数据前关闭ACK使能,即使是只读一个数据,也要在读取前关闭。我的理解是ACK使能后,主机会自动拉低数据线以回复ACK,若没有及时关闭,最好的情况是多收一个字节数据,通信照常进行,最坏的情况就是卡死了。
②在读数据之前关闭STOP使能。很奇怪,在读之前关闭停止信号使能,在读完数据之后再使能,产生一个停止信号,可以“增强”程序的稳定性;若缺少关闭使能的代码,程序会在读取或多或少的传感器数值后卡死。解释或猜测还需要笔者继续深入了解硬件IIC。
另外,如果想加强代码的稳健性,可以在读/写函数的判断标志等待中加上计数来判断等待超时,超时则直接返回错误代码,根据错误代码选择重新读写或是重新初始化硬件IIC模块或外设。比如说↓
//向从机的某寄存器写入一个字节的数据
uint8_t IIC_write_reg(uint8_t addr, uint8_t reg, uint8_t data)
{
uint16_t i=1000;
//主机通知从机要写它的哪个寄存器
while(I2C_GetFlagStatus(I2C_FLAG_BUSY) && i--) //IIC主机判忙
{
if(!i)
return 1;
}
i = 1000;
I2C_GenerateSTART(ENABLE); //起始信号
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT) && i--) //判断BUSY, MSL and SB flags
{
if(!i)
return 2;
}
i = 1000;
I2C_Send7bitAddress(addr, I2C_Direction_Transmitter); //发送地址+最低位0表示为“写”
//略
}