文章目录
IO拓展件:PCF8574T
开发板:STM32F429
参考:
1、[正点原子]《STM32F429 开发指南(HAL 库版)》
2、STM32CubeMX使用之I2C通讯
1、理论基础
1.1、物理层特点
- “总线”指多个设备共用的信号线。在I2C 通讯总线中,支持多个主机及多个从机
- 一个 I2C 有两条总线, 数据线 (SDA) 用来表示数据,时钟线 (SCL)用于数据收发同步。
- 每个连接到总线的设备都有一个独立的地址
从机地址可以是 7位或 10 位 - 总线通过上拉电阻接到电源。
当 I2C 设备空闲时,会输出高阻态,当所有设备都空闲时,由上拉电阻把总线拉成高电平 - 具有仲裁机制 (开漏输出:线与,低电平有效)
多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线 - 三种传输模式
标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s - 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制
- 主机Master 从机Slave
1.2、通信过程
1.2.1、通讯起始和停止信号
- 空闲时 sda和scl都是高电平
- 起始位s 停止位p
1.2.2、有效数据
- scl为高电平时数据有效
- 每次数据传输都以字节为单位,每次传输的字节数不受限制
- scl 1 sda 1 表示1
- scl 1 sda 0 表示0
1.2.3、读写过程
- 主机写数据到从机 (应答ACK)
DATA数据包的大小为 8 位
- 从机写数据到主机
主机等待从机的应答
1.3、IIC架构
- 数据寄存器 DR
- 地址寄存器OAR1
- 第二地址寄存器OAR2
- 校验寄存器PEC
- 控制寄存器CR1/2
- 状态寄存器SR1/2
- 控制寄存器CCR
2、工程建立(硬件iic)
ST公司实现了硬件的iic,但据说用着不方便,可以参考第3节软件实现
2.1、配置步骤
- RCC
设置外部晶振、PLL、主频 - 调试口
选择调试模式,选择调试IO口 - IO分配
打开相应外设开关 - IIC参数
设置速率、地址、主从、时序
2.2、cubemx具体配置
- 选择开漏模式(一般只有这个选项)
- 参数设置
3、应用(软件iic)
- 利用EEPROM 24C02 测试
- 这里先用标准库
3.1、原理图
3.2、端口模式+位带操作
端口模式设置
GPIO 端口模式寄存器 (GPIOx_MODER) (x = A…I)
- 3(二进制11),将11左移10位,取反再与,使MODER5的10、11位寄存器清零
0左移10位,使配置位10为0;即配置端口5为输入模式
1左移10位,即配置位10为1;即配置端口5为输出模式
#define SDA_IN() {GPIOH->MODER&=~(3<<(5*2));GPIOH->MODER|=0<<5*2;} //PH5输入模式
#define SDA_OUT() {GPIOH->MODER&=~(3<<(5*2));GPIOH->MODER|=1<<5*2;} //PH5输出模式
位带操作
stm32之bit-band(位带)操作
3.3、iic头文件
- 代码均来自正点原子,只是改了一点点
#ifndef _MYIIC_H
#define _MYIIC_H
#include "main.h"
#include "stm32f4xx_hal_gpio.h"
//IO口位带操作
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO方向设置
#define SDA_IN() {GPIOH->MODER&=~(3<<(5*2));GPIOH->MODER|=0<<5*2;} //PH5输入模式
#define SDA_OUT() {GPIOH->MODER&=~(3<<(5*2));GPIOH->MODER|=1<<5*2;} //PH5输出模式
//IO口地址映射
#define GPIOH_ODR_Addr (GPIOH_BASE+20) //0x40021C14
#define GPIOH_IDR_Addr (GPIOH_BASE+16) //0x40021C10
//IO操作
#define PHout(n) BIT_ADDR(GPIOH_ODR_Addr,n) //输出 gpio_writepin
#define PHin(n) BIT_ADDR(GPIOH_IDR_Addr,n) //输入 gpio_readpin
#define IIC_SCL PHout(4) //SCL
#define IIC_SDA PHout(5) //SDA
#define READ_SDA PHin(5) //输入SDA
#define CPU_FREQUENCY_MHZ 180 // STM32时钟主频 用于微秒级延时
typedef unsigned char u8;
typedef unsigned short int u16;
typedef unsigned int u32;
//IIC所有操作函数
void IIC_Init(void); //初始化IIC的IO口
void IIC_Start(void); //发送IIC开始信号
void IIC_Stop(void); //发送IIC停止信号
void IIC_Send_Byte(u8 txd); //IIC发送一个字节
u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节
u8 IIC_Wait_Ack(void); //IIC等待ACK信号
void IIC_Ack(void); //IIC发送ACK信号
void IIC_NAck(void); //IIC不发送ACK信号
void delay_us(uint32_t delay); //hal库 没有微秒延时,需要自己写
void delay_ms(uint32_t ms);
#endif
3.4、iic具体实现
- 为了配合AT24C02的使用,具体的设置可能和其他教程有所差别
#include "myiic.h"
//IIC初始化
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOH_CLK_ENABLE(); //使能GPIOH时钟
//PH4,5初始化设置
GPIO_Initure.Pin=GPIO_PIN_4|GPIO_PIN_5;
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH; //快速
HAL_GPIO_Init(GPIOH,&GPIO_Initure);
//都拉高表示iic空闲
IIC_SDA=1;
IIC_SCL=1;
}
//产生IIC起始信号:scl 拉高 sda产生下降沿
void IIC_Start(void)
{
SDA_OUT(); //配置sda线为输出模式
IIC_SDA=1;
IIC_SCL=1;
delay_us(4);
IIC_SDA=0;//产生下降沿
delay_us(4);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
}
//产生IIC停止信号:scl 拉高 sda产生上升沿
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SCL=0;//拉低用于sda高低电平变换
IIC_SDA=0;
delay_us(4);
IIC_SCL=1;
delay_us(4);
IIC_SDA=1;//产生上升沿
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA设置为输入
IIC_SDA=1;
delay_us(1);
IIC_SCL=1;
delay_us(1);
while(READ_SDA)//sda为低表示应答信号
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL=0;
return 0;
}
//产生ACK应答(这里我还没弄清楚)
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)//一位一位的发
{
IIC_SDA=(txd&0x80)>>7;//发送txd的最高位
txd<<=1;
delay_us(2); //对TEA5767这三个延时都是必须的
IIC_SCL=1;
delay_us(2);
IIC_SCL=0; //拉低时钟,变化sda
delay_us(2);
}
}
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;//uint8_t
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)
{
receive++;
}
delay_us(1);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
void delay_us(uint32_t delay)
{
int last, curr, val;
int temp;
while (delay != 0)
{
temp = delay > 900 ? 900 : delay;
last = SysTick->VAL;
curr = last - CPU_FREQUENCY_MHZ * temp;
if (curr >= 0)
{
do
{
val = SysTick->VAL;
}
while ((val < last) && (val >= curr));
}
else
{
curr += CPU_FREQUENCY_MHZ * 1000;
do
{
val = SysTick->VAL;
}
while ((val <= last) || (val > curr));
}
delay -= temp;
}
}
void delay_ms(uint32_t ms){
for(int i=0;i<ms;i++){
delay_us(1000);
}
}
3.5、AT24C02读写驱动
- 头文件
#ifndef _24CXX_H
#define _24CXX_H
#include "myiic.h"
#define AT24C01 127
#define AT24C02 255
#define AT24C04 511
#define AT24C08 1023
#define AT24C16 2047
#define AT24C32 4095
#define AT24C64 8191
#define AT24C128 16383
#define AT24C256 32767
//STM32F429开发板使用的是24c02,所以定义EE_TYPE为AT24C02
#define EE_TYPE AT24C02
u8 AT24CXX_ReadOneByte(u16 ReadAddr); //指定地址读取一个字节
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite); //指定地址写入一个字节
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len);//指定地址开始写入指定长度的数据
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len); //指定地址开始读取指定长度数据
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite); //从指定地址开始写入指定长度的数据
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead); //从指定地址开始读出指定长度的数据
u8 AT24CXX_Check(void); //检查器件
void AT24CXX_Init(void); //初始化IIC
#endif
- 具体实现
#include "24cxx.h"
//初始化IIC接口
void AT24CXX_Init(void)
{
IIC_Init();//IIC初始化
}
//在AT24CXX指定地址读出一个数据
//ReadAddr:开始读数的地址
//返回值 :读到的数据
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
u8 temp=0;
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr>>8);//发送高地址
}
else
IIC_Send_Byte(0XA0+((ReadAddr/256)<<1)); //发送器件地址0XA0,写数据
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0XA1); //进入接收模式
IIC_Wait_Ack();
temp=IIC_Read_Byte(0);
IIC_Stop();//产生一个停止条件
return temp;
}
//在AT24CXX指定地址写入一个数据
//WriteAddr :写入数据的目的地址
//DataToWrite:要写入的数据
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr>>8);//发送高地址
}
else
IIC_Send_Byte(0xA0+((WriteAddr/256)<<1)); //发送器件地址0XA0,写数据
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); //发送字节
IIC_Wait_Ack();
IIC_Stop();//产生一个停止条件
delay_ms(10);
}
//在AT24CXX里面的指定地址开始写入长度为Len的数据
//该函数用于写入16bit或者32bit的数据.
//WriteAddr :开始写入的地址
//DataToWrite:数据数组首地址
//Len :要写入数据的长度2,4
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len)
{
u8 t;
for(t=0;t<Len;t++)
{
AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);
}
}
//在AT24CXX里面的指定地址开始读出长度为Len的数据
//该函数用于读出16bit或者32bit的数据.
//ReadAddr :开始读出的地址
//返回值 :数据
//Len :要读出数据的长度2,4
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len)
{
u8 t;
u32 temp=0;
for(t=0;t<Len;t++)
{
temp<<=8;
temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1);
}
return temp;
}
//检查AT24CXX是否正常
//这里用了24XX的最后一个地址(255)来存储标志字.
//如果用其他24C系列,这个地址要修改
//返回1:检测失败
//返回0:检测成功
u8 AT24CXX_Check(void)
{
u8 temp;
temp = AT24CXX_ReadOneByte(255);//避免每次开机都写AT24CXX
if(temp==0X55)
return 0;
else//排除第一次初始化的情况
{
AT24CXX_WriteOneByte(255,0X55);
temp=AT24CXX_ReadOneByte(255);
if(temp==0X55)
return 0;
}
return 1;
}
//在AT24CXX里面的指定地址开始读出指定个数的数据
//ReadAddr :开始读出的地址 对24c02为0~255
//pBuffer :数据数组首地址
//NumToRead:要读出数据的个数
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
while(NumToRead)
{
*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);
NumToRead--;
}
}
//在AT24CXX里面的指定地址开始写入指定个数的数据
//WriteAddr :开始写入的地址 对24c02为0~255
//pBuffer :数据数组首地址
//NumToWrite:要写入数据的个数
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
WriteAddr++;
pBuffer++;
}
}
- 部分测试代码
const u8 TEXT_Buffer[]={"IIC AT24c02 测试"};
#define SIZE sizeof(TEXT_Buffer)
//需要自行配置串口
#define u1_printf(...) HAL_UART_Transmit((UART_HandleTypeDef *)&huart1,\
(const uint8_t *)u1_data,\
(uint16_t)sprintf((char *)u1_data,__VA_ARGS__),\
(uint32_t)0xffff)
uint8_t u1_data[2048];//设为全局
u8 datatemp[SIZE] = {0};
AT24CXX_Init(); //初始化IIC
while(AT24CXX_Check());//检测不到24c02
u1_printf("AT24C02 OK\r\n");
AT24CXX_Write(0,(u8*)TEXT_Buffer,SIZE);
u1_printf("write ok\r\n");
AT24CXX_Read(0,datatemp,SIZE);
u1_printf("%s\r\n",datatemp)
4、利用iic拓展io
4.1、原理图
INT为拓展件的中断线 低电平有效
就是mcu用scl 和 sda 两根线 来 读写 p0 ~ p7 的引脚电平值
- mcu往拓展件写数据
- mcu从拓展件读数据
注意:一旦中断有效后,必须对 PCF8574T 进行一次读取/写入操作,复位中断,
才可以输出下一次中断,否则中断将一直保持(无法输出下一次输入信号变化所产生的中断)。
4.2、代码
代码就直接照搬正点原子的源码了
- 头文件
#ifndef __PCF8574_H
#define __PCF8574_H
#include "sys.h"
#include "myiic.h"
#define PCF8574_INT PBin(12) //PCF8574 INT脚
#define PCF8574_ADDR 0X40 //PCF8574地址(左移了一位)
//PCF8574各个IO的功能
#define BEEP_IO 0 //蜂鸣器控制引脚 P0
#define AP_INT_IO 1 //AP3216C中断引脚 P1
#define DCMI_PWDN_IO 2 //DCMI的电源控制引脚 P2
#define USB_PWR_IO 3 //USB电源控制引脚 P3
#define EX_IO 4 //扩展IO,自定义使用 P4
#define MPU_INT_IO 5 //MPU9250中断引脚 P5
#define RS485_RE_IO 6 //RS485_RE引脚 P6
#define ETH_RESET_IO 7 //以太网复位引脚 P7
u8 PCF8574_Init(void); //初始化
u8 PCF8574_ReadOneByte(void);
void PCF8574_WriteOneByte(u8 DataToWrite);
void PCF8574_WriteBit(u8 bit,u8 sta);//写入数据到指定引脚
u8 PCF8574_ReadBit(u8 bit);
#endif
- 具体实现
#include "pcf8574.h"
#include "delay.h"
//初始化PCF8574
u8 PCF8574_Init(void)
{
u8 temp=0;
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOB_CLK_ENABLE(); //使能GPIOB时钟
GPIO_Initure.Pin=GPIO_PIN_12; //PB12
GPIO_Initure.Mode=GPIO_MODE_INPUT; //输入
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
HAL_GPIO_Init(GPIOB,&GPIO_Initure); //初始化
IIC_Init(); //IIC初始化
//检查PCF8574是否在位
IIC_Start();
IIC_Send_Byte(PCF8574_ADDR); //写地址
temp=IIC_Wait_Ack(); //等待应答,通过判断是否有ACK应答,来判断PCF8574的状态
IIC_Stop(); //产生一个停止条件
PCF8574_WriteOneByte(0XFF); //默认情况下所有IO输出高电平
return temp;
}
//读取PCF8574的8位IO值
//返回值:读到的数据
u8 PCF8574_ReadOneByte(void)
{
u8 temp=0;
IIC_Start();
IIC_Send_Byte(PCF8574_ADDR|0X01); //进入接收模式
IIC_Wait_Ack();
temp=IIC_Read_Byte(0);
IIC_Stop(); //产生一个停止条件
return temp;
}
//向PCF8574写入8位IO值
//DataToWrite:要写入的数据
void PCF8574_WriteOneByte(u8 DataToWrite)
{
IIC_Start();
IIC_Send_Byte(PCF8574_ADDR|0X00); //发送器件地址0X40,写数据
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); //发送字节
IIC_Wait_Ack();
IIC_Stop(); //产生一个停止条件
delay_ms(10);
}
//设置PCF8574某个IO的高低电平
//bit:要设置的IO编号,0~7
//sta:IO的状态;0或1
void PCF8574_WriteBit(u8 bit,u8 sta)
{
u8 data;
data=PCF8574_ReadOneByte(); //先读出原来的设置
if(sta==0)
{
data&=~(1<<bit); //把bit位的置零
}
else
{
data|=1<<bit;
}
PCF8574_WriteOneByte(data); //写入新的数据
}
//读取PCF8574的某个IO的值
//bit:要读取的IO编号,0~7
//返回值:此IO的值,0或1
u8 PCF8574_ReadBit(u8 bit)
{
u8 data;
data=PCF8574_ReadOneByte(); //先读取这个8位IO的值
if(data&(1<<bit))
{
return 1;
}
else
return 0;
}