本帖最后由 gaoyang9992006 于 2022-9-6 21:13 编辑
#申请原创# @21小跑堂
在网上我想找一个新唐库函数操作OLED获取其他什么的例子都很难找到,至今没找到。都是采用的IO模拟,或者采用了基础库函数(类似寄存器的操作)实现。
那么问题在哪儿呢?
我要点亮一个SSD1306的0.91寸OLED,接口I2C。我用IO很容易点亮。非常方便的移植,但是我是有强迫症的,有硬件的I2C为何大家不用?
用过这个的都应该清楚,通常是要实现以下操作函数
//发送一个字节
//向SSD1306写入一个字节。
//mode:数据/命令标志 0,表示命令;1,表示数据;
void OLED_WR_Byte(u8 dat,u8 mode)
{
I2C_Start();
Send_Byte(0x78);
I2C_WaitAck();
if(mode){Send_Byte(0x40);}
else{Send_Byte(0x00);}
I2C_WaitAck();
Send_Byte(dat);
I2C_WaitAck();
I2C_Stop();
}
厂家通常也是提供这个函数,或者分开,一个是写指令,一个写数据。
void OLED_Write_cmd(uint8_t cmd);
void OLED_Write_data(uint8_t data);
而惯常的用法就是采用IO模拟,你只需要在头文件选择使用哪两个IO就行了,多么的简单!
而后我用过STM32驱动它,采用了STM32 HAL的一个库函数,如下所示,请注意这里的从机地址我填写的是0x78,这也是很多卖OLED模块提供例子中给的地址
void OLED_Write_cmd(uint8_t cmd)
{
HAL_I2C_Mem_Write(&hi2c2, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 0x100);
}
void OLED_Write_data(uint8_t data)
{
HAL_I2C_Mem_Write(&hi2c2, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, &data, 1, 0x100);
}
然后我照猫画虎在新唐的BSP库函数中找到了类似的函数移植
void OLED_Write_cmd(uint8_t cmd)
{
I2C_WriteByteOneReg(I2C0,0x78,0x00,cmd);
}
void OLED_Write_data(uint8_t data)
{
I2C_WriteByteOneReg(I2C0,0x78,0x40,data);
}
本想着不出意外的情况下就点亮了,结果出意外了,没亮。。。
然后我摸索了一天没找到原因啊,头大。
然后我问了新唐的技术,技术给我的建议是让我看I2C的STATUS状态寄存器,这个寄存器在每次操作后会更新状态刚好库函数是根据这个状态选择下一步操作的。
发现,发起START信号是正常的,正确的返回了0x08状态,然后下一步竟然跳过了本应出现的0x18,直接给了个0x20状态。
然后我开始自己着手不用这个组合库函数,而采用基于寄存器的底层操作函数直接操作
void OLED_Write_cmd(uint8_t cmd)
{
/* Send START */
I2C_START(I2C0);
I2C_WAIT_READY(I2C0);
/* Send device address */
I2C_SET_DATA(I2C0, 0x78);
I2C_SET_CONTROL_REG(I2C0, I2C_CTL_SI);
I2C_WAIT_READY(I2C0);
/* Send register number and MSB of data */
I2C_SET_DATA(I2C0, 0x00);
I2C_SET_CONTROL_REG(I2C0, I2C_CTL_SI);
I2C_WAIT_READY(I2C0);
/* Send data */
I2C_SET_DATA(I2C0, cmd);
I2C_SET_CONTROL_REG(I2C0, I2C_CTL_SI);
I2C_WAIT_READY(I2C0);
/* Send STOP */
I2C_STOP(I2C0);
}
void OLED_Write_data(uint8_t data)
{
/* Send START */
I2C_START(I2C0);
I2C_WAIT_READY(I2C0);
/* Send device address */
I2C_SET_DATA(I2C0, 0x78);
I2C_SET_CONTROL_REG(I2C0, I2C_CTL_SI);
I2C_WAIT_READY(I2C0);
/* Send register number and MSB of data */
I2C_SET_DATA(I2C0, 0x40);
I2C_SET_CONTROL_REG(I2C0, I2C_CTL_SI);
I2C_WAIT_READY(I2C0);
/* Send data */
I2C_SET_DATA(I2C0, data);
I2C_SET_CONTROL_REG(I2C0, I2C_CTL_SI);
I2C_WAIT_READY(I2C0);
/* Send STOP */
I2C_STOP(I2C0);
}
这次没出意外,点亮了。好奇怪,我开始比对那个组合型库函数与我上面这个操作的区别
I2C_WriteByteOneReg();uint8_t I2C_WriteByteOneReg(I2C_T *i2c, uint8_t u8SlaveAddr, uint8_t u8DataAddr, uint8_t data)
{
uint8_t u8Xfering = 1u, u8Err = 0u, u8Ctrl = 0u;
uint32_t u32txLen = 0u;
g_I2C_i32ErrCode = 0;
I2C_START(i2c); /* Send START */
while(u8Xfering && (u8Err == 0u))
{
uint32_t u32TimeOutCount = SystemCoreClock; // 1 second timeout
I2C_WAIT_READY(i2c)
{
u32TimeOutCount--;
if(u32TimeOutCount == 0)
{
g_I2C_i32ErrCode = I2C_TIMEOUT_ERR;
break;
}
}
switch(I2C_GET_STATUS(i2c))
{
case 0x08u:
I2C_SET_DATA(i2c, (uint8_t)(u8SlaveAddr << 1u | 0x00u)); /* Send Slave address with write bit */
u8Ctrl = I2C_CTL_SI; /* Clear SI */
break;
case 0x18u: /* Slave Address ACK */
I2C_SET_DATA(i2c, u8DataAddr); /* Write Lo byte address of register */
break;
case 0x20u: /* Slave Address NACK */
case 0x30u: /* Master transmit data NACK */
u8Ctrl = I2C_CTL_STO_SI; /* Clear SI and send STOP */
u8Err = 1u;
break;
case 0x28u:
if(u32txLen < 1u)
{
I2C_SET_DATA(i2c, data);
u32txLen++;
}
else
{
u8Ctrl = I2C_CTL_STO_SI; /* Clear SI and send STOP */
u8Xfering = 0u;
}
break;
case 0x38u: /* Arbitration Lost */
default: /* Unknow status */
I2C_SET_CONTROL_REG(i2c, I2C_CTL_STO_SI); /* Clear SI and send STOP */
u8Ctrl = I2C_CTL_SI;
u8Err = 1u;
break;
}
I2C_SET_CONTROL_REG(i2c, u8Ctrl); /* Write controlbit to I2C_CTL register */
}
return (u8Err | u8Xfering); /* return (Success)/(Fail) status */
}
注意以上新唐库函数中实现中的那个左移操作。
终于让我给找到了,我们知道I2C从机元件通常是有7位地址和10位地址之分的,我想要使用的这个库函数是7位地址的,而它就认为你要输入的地址是原始的7位地址,而我认为是地址的地址0x78,我以为就是那个地址,因为我没去看OLED的驱动芯片原始手册,认为大家都是用的这个0x78就真的是它的7位地址了,而I2C_WriteByteOneReg函数中是把它当作7位的原始地址处理的,所以给左移了一位,在末尾补0表示写操作。
问题就出在这,实际上OLED的原本7位地址是0x3C 而不是0x78,通常使用IO模拟的那些网上的操作函数是手动给移位后作为它的写入地址用了。
因此问题就在这里了。
现在我已经可以通过
void OLED_Write_cmd(uint8_t cmd)
{
// HAL_I2C_Mem_Write(&hi2c2, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 0x100);
I2C_WriteByteOneReg(I2C0,0x3C,0x00,cmd);
}
void OLED_Write_data(uint8_t data)
{
// HAL_I2C_Mem_Write(&hi2c2, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, &data, 1, 0x100);
I2C_WriteByteOneReg(I2C0,0x3C,0x40,data);
}
成功点亮我的OLED屏幕了,在此分享给大家,以后可以大胆的使用硬件I2C的库函数驱动这些外设了,使用的时候注意一下这个地址到底是不是那个原本的地址,不要用它的“小名”。。。o(* ̄︶ ̄*)o
补充:
在系统初始化函数中增加以下相关内容,用于使能I2C的时钟模块,以及配置使用哪些IO连接到内部的I2C收发器的时钟与数据端口
void SYS_Init(void)
{
/* Enable I2C0 peripheral clock */
CLK_EnableModuleClock(I2C0_MODULE);
/* Set I2C0 multi-function pins */
SYS->GPG_MFPL = (SYS->GPG_MFPL & ~(SYS_GPG_MFPL_PG0MFP_Msk | SYS_GPG_MFPL_PG1MFP_Msk)) |
(SYS_GPG_MFPL_PG1MFP_I2C0_SDA | SYS_GPG_MFPL_PG0MFP_I2C0_SCL);
}
另外就是设置I2C的速率,一般有三种选择100K,400K,1M
void I2C0_Init(void)
{
/* Open I2C module and set bus clock */
I2C_Open(I2C0, 100000);
/* Get I2C0 Bus Clock */
printf("I2C clock %d Hz\n", I2C_GetBusClockFreq(I2C0));
}
好了,看了我这个贴,以后别再抱怨新唐的I2C库函数难用了。最后分享给大家我的测试工程,接线如下图文字描述
|
亲测新塘的库函数无问题,但是硬件IIC一直是块难啃的骨头,使用时经常会卡死,关键还是要理解IIC底层和硬件接口的知识。这就是为什么我们不能拘泥于库函数,而是要适当关注底层寄存器的原因。