嵌入式开发-STM32硬件I2C驱动OLED屏
I2C简介
I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。
主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件.在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件.然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下.主机负责产生定时时钟和终止数据传送。
STM32的I2C
坊间流传STM32的硬件I2C很容易死机,所以不能使用硬件I2C,正点原子也在教程中强调了这一点。个人猜想由于Philips拥有专利,而ST为了绕开专利,而将硬件I2C弄得异常复杂(从相关的寄存器数量及设置可见一斑),造成硬件I2C很是难用,也容易出现异常死机。
今天我就来挑战一下。尝试使用STM32F103C8T6用硬件I2C的方式来驱动OLED屏。
MCU与OLED的硬件连接
都是常规设置不啰嗦:开启外部时钟,开启SWD调试接口,开启I2C2,配置默认即可,LED可用可不用。
OLED驱动函数
这个是参照正点原子的软件驱动I2C例程修改的,将其中的软件驱动IO口电平的相关代码改为HAL_I2C_Mem_Write()函数来驱动。
GRAM的定义
有一个问题,正点原子的代码中,GRAM的定义有点问题,如下:
OLED_GRAM[144][8]; //行定义是Y值,列定义是X值
144是X的值,8是Y的值,符合常规,没有问题。
OLED的驱动时,是要求连续发送X的值,一行发完再发一行,正点原子的代码中也是这样做的,他不是按数组的行来取的,而是按数组的列来取值,对于软件I2C来说没有问题。
但是通过HAL_I2C_Mem_Write()函数连续发送数据时,其发送过程是提供数组首地址,然后地址自增,也就是先发送Y的那8个数据,再连续发送整列,因为数组就是这样配置的,这样就不能用。
于是将X和Y的定义换一下,成下面这样
OLED_GRAM[8] [144]; //行定义改为X值,列定义改为Y值
这样再使用HAL_I2C_Mem_Write()函数连续发送数据时,就正常了。
当然,画点画线写字符等所有相关函数都要做配合性的修改,这里不再一一列出,需要的可以下载完整的工程文档,文末有链接。
显示内容
显示的内容很简单,就是交替显示2行字符,这样如果程序死机的话画面就肯定不动了
人为模拟干扰
将SCL和SDA两根线经由120欧电阻引出,经过120欧电阻可以对该引脚强制拉高或拉低,方便测试。然后分别对地进行触碰,和互相碰触,绝对不可以直接焊线引出然后直接碰触,否则会由于电源对地短路烧电源。
可以发现一旦强制拉低,画面立刻不再切换显示,或者直接黑屏,这取决于卡死点的程序运行位置。
但是此时程序仍然在正常运行,可以打断点和跟踪调试,于是判断故障为刚才的强制拉低导致OLED屏幕运行错乱。
修改代码
检查代码发现,是下面这个语句出了问题
void oled_write_onebyte(u8 data, u8 cmd)
{
HAL_I2C_Mem_Write(&hi2c2, OLED_ADDR, 0, I2C_MEMADD_SIZE_8BIT, &data, 1, 1000);
}
void oled_write_bytes(u8* data, u8 len, u8 cmd)
{
HAL_I2C_Mem_Write(&hi2c2, OLED_ADDR, cmd, I2C_MEMADD_SIZE_8BIT, data, len, 1000);
}
也就是说,在STM32往OLED寄存器中写入数据时,由于人为操作导致数据异常,OLED内部寄存器参数乱了,导致工作不正常。
针对性的解决方法也是简单粗暴,将OLED重新初始化,并重新上电,改代码如下:
void oled_write_onebyte(u8 data, u8 cmd)
{
u32 ret;
ret = HAL_I2C_Mem_Write(&hi2c2, OLED_ADDR, 0, I2C_MEMADD_SIZE_8BIT, &data, 1, 1000);
if(ret!=0)
{
oled_init();
OLED_DisPlay_Off();
HAL_Delay(10);
OLED_DisPlay_On();
}
}
void oled_write_bytes(u8* data, u8 len, u8 cmd)
{
u32 ret;
ret = HAL_I2C_Mem_Write(&hi2c2, OLED_ADDR, cmd, I2C_MEMADD_SIZE_8BIT, data, len, 1000);
if(ret!=0)
{
oled_init();
OLED_DisPlay_Off();
HAL_Delay(10);
OLED_DisPlay_On();
}
}
重复人为模拟干扰
发现显示仍然没有正常,检查发现程序在I2C_RequestMemoryWrite()这个函数出了问题,简单粗暴的直接把I2C再来一次初始化搞定。
void oled_write_onebyte(u8 data, u8 cmd)
{
u32 ret;
ret = HAL_I2C_Mem_Write(&hi2c2, OLED_ADDR, 0, I2C_MEMADD_SIZE_8BIT, &data, 1, 1000);
if(ret!=0)
{
MX_I2C2_Init();
oled_init();
OLED_DisPlay_Off();
HAL_Delay(10);
OLED_DisPlay_On();
}
}
void oled_write_bytes(u8* data, u8 len, u8 cmd)
{
u32 ret;
ret = HAL_I2C_Mem_Write(&hi2c2, OLED_ADDR, cmd, I2C_MEMADD_SIZE_8BIT, data, len, 1000);
if(ret!=0)
{
MX_I2C2_Init();
oled_init();
OLED_DisPlay_Off();
HAL_Delay(10);
OLED_DisPlay_On();
}
}
著名的HardFault_Handler错误
此时程序工作基本正常,可以从故障中恢复,但长时间维持人为异常状态(十多秒)后,仍有小几率卡死,程序会跳转到HardFault_Handler,这个排查起来花了点时间,最后发现是
堆栈溢出,见下图:
注意看箭头所指的这个拖动条,oled_init和oled_write_onebyte这两个函数已经重复了N次,继续下去肯定是堆栈溢出跑不了的。
这个问题是由于在oled_init函数中调用了oled_write_onebyte这个函数,而在这个函数运行时,如果仍然处于故障状态的话,就又会调用oled_init,如此便会形成嵌套,嵌套多了就会导致堆栈溢出。
解决起来还是简单粗暴,加延时。
你不是时间长了,重复次数多,导致堆栈溢出么,我给你加延时,让你在几十分钟内跑不了那么多次不就行了。
当然这个搞法治标不治本,但是谁没事把个OLED屏幕短路那么久。如果哪位有更好的解决思路,请在下方留言讨论。
void oled_write_onebyte(u8 data, u8 cmd)
{
u32 ret;
ret = HAL_I2C_Mem_Write(&hi2c2, OLED_ADDR, 0, I2C_MEMADD_SIZE_8BIT, &data, 1, 1000);
if(ret!=0)
{
MX_I2C2_Init();
HAL_Delay(100);
oled_init();
OLED_DisPlay_Off();
HAL_Delay(100);
OLED_DisPlay_On();
}
}
void oled_write_bytes(u8* data, u8 len, u8 cmd)
{
u32 ret;
ret = HAL_I2C_Mem_Write(&hi2c2, OLED_ADDR, cmd, I2C_MEMADD_SIZE_8BIT, data, len, 1000);
if(ret!=0)
{
MX_I2C2_Init();
HAL_Delay(100);
oled_init();
OLED_DisPlay_Off();
HAL_Delay(100);
OLED_DisPlay_On();
}
}
至此故障解决。
咦,传说中的I2C不好用的问题在哪里呀?出了问题复位重来不就行了么?ST这么大的公司,如果连个I2C都做得不能用的话,他的片子还能卖得这么火么?可能做到每年几百亿的销售额么?
所以不要人云亦云,有问题还是自己过一遍,尝试一下也许就自己解决了。
完整工程链接
完整工程,包括CubeMX工程,Keil工程,链接如下:嵌入式开发-STM32硬件I2C驱动OLED屏