实验效果
本次实验主要讲APM32的I2C外设的初始化和APM32作为主机如何发送数据,OLED的驱动写起来较难本次实验不涉及。由于条件有限本次只能讲主机发送,接收也没有涉及。
硬件原理图
源代码
I2C初始化部分
#ifndef __BSP__IIC_H__
#define __BSP__IIC_H__
#include "apm32f10x.h"
#include "apm32f10x_gpio.h"
#include "apm32f10x_i2c.h"
#include "apm32f10x_rcm.h"#define IIC_OWN_ADDRESS 0X0A // 与从机地址不同即可#define OLED_ADDRESS 0x78 // 从机地址
#define OLED_IIC I2C1 // 使用I2C1
#define OLED_IIC_CLOCK RCM_APB1_PERIPH_I2C1
#define OLDE_IIC_SCL_PIN GPIO_PIN_6
#define OLDE_IIC_SDA_PIN GPIO_PIN_7#define OLED_GPIO_SCL_PORT GPIOB
#define OLED_GPIO_SDA_PORT GPIOB#define OLED_GPIO_SCL_CLOCK RCM_APB2_PERIPH_GPIOB
#define OLED_GPIO_SDA_CLOCK RCM_APB2_PERIPH_GPIOBvoid OLED_IIC_Config(void);
#endif
#include "bsp_iic.h"/*** @brief I2C GPIO 配置* @param*/
static void OLED_GPIO_Config(void)
{GPIO_Config_T GPIO_ConfigStruct;RCM_EnableAPB2PeriphClock(OLED_GPIO_SCL_CLOCK | OLED_GPIO_SDA_CLOCK);// 开漏输出GPIO_ConfigStruct.mode = GPIO_MODE_AF_OD;GPIO_ConfigStruct.pin = OLDE_IIC_SCL_PIN;GPIO_ConfigStruct.speed = GPIO_SPEED_10MHz;GPIO_Config(OLED_GPIO_SCL_PORT, &GPIO_ConfigStruct);GPIO_ConfigStruct.pin = OLDE_IIC_SDA_PIN;GPIO_Config(OLED_GPIO_SDA_PORT, &GPIO_ConfigStruct);
}/*** @brief 配置I2C* @param*/
void OLED_IIC_Config(void)
{I2C_Config_T I2C_ConfigStruct;RCM_EnableAPB1PeriphClock(OLED_IIC_CLOCK);OLED_GPIO_Config();// 在接收到一个字节后返回一个应答(匹配的地址或数据)I2C_ConfigStruct.ack = I2C_ACK_ENABLE;// I2C的寻址模式I2C_ConfigStruct.ackAddress = I2C_ACK_ADDRESS_7BIT;// 通信速率I2C_ConfigStruct.clockSpeed = 100000;// 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比I2C_ConfigStruct.dutyCycle = I2C_DUTYCYCLE_2;// I2C模式I2C_ConfigStruct.mode = I2C_MODE_I2C;// 主主机地址I2C_ConfigStruct.ownAddress1 = IIC_OWN_ADDRESS;I2C_Config(OLED_IIC, &I2C_ConfigStruct);I2C_Enable(OLED_IIC);
}
主机发送部分
#ifndef __APP_IIC_H__
#define __APP_IIC_H__
#include "bsp_iic.h"uint8_t App_IIC_MasterTransmit(I2C_T *IIC, uint8_t SlaveAddress, const uint8_t *pData, uint8_t Size);#endif
#include "app_iic.h"/*** @brief IIC主模式发送数据,支持多字节发送,起始和停止均在这个函数中,APM32手册没有具体流程,根据STM32手册编写* @param IIC F1可选I2C1、I2C2* @param SlaveAddress 从机地址* @param pData 要发送的数据* @param Size 数据长度* @return SUCCESS成功,ERROR失败*/
uint8_t App_IIC_MasterTransmit(I2C_T *IIC, uint8_t SlaveAddress, const uint8_t *pData, uint8_t Size)
{uint8_t ret = SUCCESS;uint32_t i = 0;// 1. 等待总线空闲while (I2C_ReadStatusFlag(IIC, I2C_FLAG_BUSBSY) == SET);// 2. 发送起始位I2C_EnableGenerateStart(IIC);// 等待起始位发送完成while (I2C_ReadStatusFlag(IIC, I2C_FLAG_START) == RESET);// 3.发送地址// 清除AE标志位I2C_ClearStatusFlag(IIC, I2C_FLAG_AE);I2C_Tx7BitAddress(IIC, SlaveAddress, I2C_DIRECTION_TX);while (I2C_ReadStatusFlag(IIC, I2C_FLAG_ADDR) == RESET){if (I2C_ReadStatusFlag(IIC, I2C_FLAG_AE) == SET){ret = ERROR;goto STOP;}}// 4. 发送数据// 清除ADDR标志位I2C_ReadRegister(IIC, I2C_REGISTER_STS1);I2C_ReadRegister(IIC, I2C_REGISTER_STS2);for (i = 0; i < Size; i++){while (I2C_ReadStatusFlag(IIC, I2C_FLAG_TXBE) == RESET){if (I2C_ReadStatusFlag(IIC, I2C_FLAG_AE) == SET){ret = ERROR;goto STOP;}}I2C_TxData(IIC, pData[i]);}while (I2C_ReadStatusFlag(IIC, I2C_FLAG_BTC) == RESET){}// 5.发送停止位
STOP:I2C_EnableGenerateStop(IIC);// 等待总线空闲while (I2C_ReadStatusFlag(IIC, I2C_FLAG_BUSBSY) == SET);return ret;
}
主程序部分
/*** @brief SCL-->PB6* SDA-->PB7*/#include "bsp_oled.h"int main(void)
{/*模块初始化*/OLED_Init(); // OLED初始化/*OLED显示*/OLED_ShowChar(1, 1, 'Y'); // 1行1列显示字符YOLED_ShowString(1, 3, "Hardware IIC!"); // 1行3列显示字符串Hardware IIC!OLED_ShowNum(2, 1, 12345, 5); // 2行1列显示十进制数字12345,长度为5OLED_ShowSignedNum(2, 7, -55, 2); // 2行7列显示有符号十进制数字-55,长度为2OLED_ShowHexNum(3, 1, 0xA5A5, 4); // 3行1列显示十六进制数字0xA5A5,长度为4OLED_ShowBinNum(4, 1, 0xA5A5, 16); // 4行1列显示二进制数字0xA5A5,长度为16// C语言无法直接写出二进制数字,故需要用十六进制表示while (1){}
}
代码分析
bsp_iic.c
/*** @brief I2C GPIO 配置* @param*/
static void OLED_GPIO_Config(void)
{GPIO_Config_T GPIO_ConfigStruct;RCM_EnableAPB2PeriphClock(OLED_GPIO_SCL_CLOCK | OLED_GPIO_SDA_CLOCK);// 开漏输出GPIO_ConfigStruct.mode = GPIO_MODE_AF_OD;GPIO_ConfigStruct.pin = OLDE_IIC_SCL_PIN;GPIO_ConfigStruct.speed = GPIO_SPEED_10MHz;GPIO_Config(OLED_GPIO_SCL_PORT, &GPIO_ConfigStruct);GPIO_ConfigStruct.pin = OLDE_IIC_SDA_PIN;GPIO_Config(OLED_GPIO_SDA_PORT, &GPIO_ConfigStruct);
}
I2C需要两个GPIO,一个作为SCL,另一个作为SDA,IIC的GPIO和其他外设配置就一个点不太相同,GPIO_ConfigStruct.mode = GPIO_MODE_AF_OD;
模式要设置成复用开漏输出模式。
我们主要讲下I2C相关参数如何配置
/*** @brief 配置I2C* @param*/
void OLED_IIC_Config(void)
{I2C_Config_T I2C_ConfigStruct;RCM_EnableAPB1PeriphClock(OLED_IIC_CLOCK);OLED_GPIO_Config();// 在接收到一个字节后返回一个应答(匹配的地址或数据)I2C_ConfigStruct.ack = I2C_ACK_ENABLE;// I2C的寻址模式I2C_ConfigStruct.ackAddress = I2C_ACK_ADDRESS_7BIT;// 通信速率I2C_ConfigStruct.clockSpeed = 100000;// 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比I2C_ConfigStruct.dutyCycle = I2C_DUTYCYCLE_2;// I2C模式I2C_ConfigStruct.mode = I2C_MODE_I2C;// 主主机地址I2C_ConfigStruct.ownAddress1 = IIC_OWN_ADDRESS;I2C_Config(OLED_IIC, &I2C_ConfigStruct);I2C_Enable(OLED_IIC);
}
先来看下结构体中的参数
/*** @brief I2C Config structure definition*/
typedef struct
{uint32_t clockSpeed;I2C_MODE_T mode;I2C_DUTYCYCLE_T dutyCycle;uint16_t ownAddress1;I2C_ACK_T ack;I2C_ACK_ADDRESS_T ackAddress;
} I2C_Config_T;
clockSpeed
通信速率,这个值主要根据从机来选,我们要填的参数要小于从机所支持的最高通信速率。我们这里填100000,属于标准模式的速度。
I2C_MODE_T
I2C模式,可选参数I2C_MODE_I2C
I2C模式、I2C_MODE_SMBUUSDEVICE
SMBUS从机模式、I2C_MODE_SMBUSHOST
SMBUS主机模式。我们这里选I2C模式,另外两个位系统管理总线,主要用在PC上。
I2C_DUTYCYCLE_T
I2C的时钟线SCL的占空比,可选I2C_DUTYCYCLE_16_9
低电平比高电平为16:9、I2C_DUTYCYCLE_2
低电平比高电平为2:1,两个选项其实区别不大,若从机无特殊要求,随便选即可。
ownAddress1
主机地址,这个地址可以随便填,但是不能和其他设备地址相同,要保证唯一性即可。
ack
应答使能,可选I2C_ACK_ENABLE
使能应答、I2C_ACK_DISABLE
禁止应答。这个主要是接收数据时用到,接收到一个字节后自动返回一个应答。一般都是使能的,不过我们这次实验没用到,因为我们主机只发送数据没有接收。
ackAddress
I2C寻址模式,可选参数I2C_ACK_ADDRESS_7BIT
7位地址、I2C_ACK_ADDRESS_10BIT
10位地址。这个要根据从机来选,一般还是7位的多,我们这里选I2C_ACK_ADDRESS_7BIT
。
填好这些参数后把结构体填入初始化函数中即可,然后使能I2C外设。
app_i2c.c
我们主要来讲下APM32作为I2C主机如何发送数据。APM32的参考手册没写如何发送,我不得不去看STM32手册,RM0008 Rev21 760页。
我们主要看7bit的发送,上面这种图可能看得不够清楚,我们结合下方流程图看
- 首先时等待总线空闲
while (I2C_ReadStatusFlag(IIC, I2C_FLAG_BUSBSY) == SET);
,STM32手册上写了要等待总线空闲之后才可以发送起始位,不看手册我们自己想应该也是要等待总线空闲。 - 发送起始位
I2C_EnableGenerateStart(IIC);
- 等待EV5并处理,可以while(!I2C_ReadEventStatus(I2C1, I2C_EVENT_MASTER_MODE_SELECT));,我们这里没这样写,我们是
while (I2C_ReadStatusFlag(IIC, I2C_FLAG_START) == RESET)
我们是等待START位置位,怎么理解EV5这个事件呢,其实EV5就是个别名,我们的写法只是没用别名,具体出来了。 - 在发送从机地址之前我们清除了AE标志位(应答错误标志)
I2C_ClearStatusFlag(IIC, I2C_FLAG_AE);
,怎么理解呢?可以说是为了保险起见,给这个标志位赋了初值。I2C_Tx7BitAddress(IIC, SlaveAddress, I2C_DIRECTION_TX);
发送七位地址,SlaveAddress
为从机地址,I2C_DIRECTION_TX
为发送模式,I2C_DIRECTION_RX
为接收模式。 - 发送完地址后要等待EV6事件,也就是等待ADDR置位(主机地址模式地址发送完成),
while (I2C_ReadStatusFlag(IIC, I2C_FLAG_ADDR) == RESET)
,在等待ADDR置位的同时我们一直在检测AE标志位,若出现错误直接跳转到错误部分,结束发送。ADDR置位表示从机地址发送完成,需要清除该标志位,I2C_ReadRegister(IIC, I2C_REGISTER_STS1);
I2C_ReadRegister(IIC, I2C_REGISTER_STS2);
,读这两个寄存器就可以清除ADDR标志位。 - 然后就是发送数据,通过一个for循环发,有多少发多少,在发之前要等到发送缓冲区为空,这一点和串口那边发送有些相似。
for (i = 0; i < Size; i++)
{while (I2C_ReadStatusFlag(IIC, I2C_FLAG_TXBE) == RESET){if (I2C_ReadStatusFlag(IIC, I2C_FLAG_AE) == SET){ret = ERROR;goto STOP;}}I2C_TxData(IIC, pData[i]);
}
- 发送缓冲区数据发送完成后等待完成数据字节传输标志置位,这里也和串口那边的标志位相似。
- 完成一系列操作后就可以发送停止位了,一套发送完成。
完成这个函数后,就可以去移植OLED驱动了,把接口函数改好即可。具体代码可以参考Git仓库