为了克服硬件IIC的缺点以及更好了了解IIC协议,下面让我来介绍一下HAL库环境下的软件IIC如何使用。
硬件:stm32f103RCT6、AT24C02
软件:cubemx、keil5、野火上位机
我们把程序分为三部分,第一部分是延时函数和软件IIC函数;第二部分是AT24C02读写函数;第三部分是main()测试函数。
delay函数:
delay.c:
#include "stm32f1xx_hal.h"
#include "delay.h"
void Delay_Us(uint16_t us){
SysTick->LOAD = us*72; //运行频率为72MHZ就乘72,因为一个systick为 (1/运行频率)秒
SysTick->VAL = 0x00;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(!(SysTick->CTRL&SysTick_CTRL_COUNTFLAG_Msk));
SysTick->CTRL &=~ SysTick_CTRL_ENABLE_Msk;
}
void Delay_Ms(uint16_t ms){
while(ms--){
Delay_Us(1000);
}
}
delay.h: 头文件
#ifndef DELAY_H
#define DELAY_H
#include "stdint.h"
void Delay_Us(uint16_t us);
void Delay_Ms(uint16_t ms);
#endif
软件IIC函数:
SCL我们选择推挽输出+上拉,默认输出高电平;
SDA我们选择开漏输出+上拉,默认输出高电平;因为SDA线既要用作输出,也要用作输入(从机应答信号),使用开漏模式则可以解决这个问题。当然我们也可以使SDA为推挽输出模式,但是这样每次SDA输出和输入模式转变时都需要重新初始化SDA的GPIO口,比较麻烦。
Sotf_IIC.c:
#include "Soft_IIC.h"
#include "delay.h"
/* iic起始信号,当SCL为高电平时,SDA从高电平变为低电平*/
void iic_start(void)
{
/* 保持时钟线高电平,数据线产生下降沿 */
IIC_SDA_H();
IIC_SCL_H();
iic_delay();
IIC_SDA_L();
iic_delay();
/* 拉低时钟线,准备发送/接收数据 ,此时SCL和SDA都为低电平*/
IIC_SCL_L();
iic_delay();
}
/* iic停止信号,当SCL为高电平时,SDA从低电平变为高电平 */
void iic_stop(void)
{
/* 保持时钟线高电平,数据线产生上升沿 */
IIC_SDA_L();
IIC_SCL_H();
iic_delay();
IIC_SDA_H();
iic_delay();
//停止信号发送后传输结束,SDA和SCL都为高电平
}
/* 等待应答信号 */
/* return 0:fail 1:succeed*/
uint8_t iic_wait_ack (void)
{
IIC_SDA_H(); /* 主机释放SDA线 */
iic_delay();
IIC_SCL_H(); /* 拉高SCL等待读取从机应答信号 */
iic_delay();
if (IIC_READ_SDA) /* SCL高电平读取SDA状态 */
{
iic_stop(); /* SDA高电平表示从机非应答 */
return 0;
}
/* SDA低电平表示从机应答 */
IIC_SCL_L(); /* SCL低电平表示结束应答检查 */
iic_delay();
return 1;
}
/* 应答信号 */
void iic_ack(void)
{
IIC_SDA_L(); //拉低SDA
IIC_SCL_H(); //拉高SCL
iic_delay();
IIC_SCL_L(); //拉低SCL
IIC_SDA_H(); //拉高SDA,释放SDA线
iic_delay();
}
/* 非应答信号 */
void iic_nack(void)
{
IIC_SCL_L();
iic_delay();
IIC_SDA_H(); /* 数据线为高电平,表示非应答 */
iic_delay();
IIC_SCL_H();
iic_delay();
IIC_SCL_L();
}
/* 发送一个字节数据 */
void iic_send_byte(uint8_t data)
{
for (uint8_t t=0; t<8; t++)
{
/* 高位先发 */
IIC_SDA((data & 0x80) >> 7);
iic_delay();
/* 拉高时钟线,稳定数据接收 */
IIC_SCL_H();
iic_delay();
IIC_SCL_L();
data <<= 1; /* 左移1位, 用于下一次发送 */
}
IIC_SDA_H(); /* 发送完成,主机释放SDA线 */
}
/* 读取1字节数据 */
/* ack:通知从机是否继续发送。
0,停止发送;1,继续发送
*/
uint8_t iic_read_byte(uint8_t ack)
{
uint8_t receive = 0 ;
for (uint8_t t=0; t<8; t++)
{
/* 高位先输出,先收到的数据位要左移 */
receive <<= 1;
IIC_SCL_H();
iic_delay();
if (IIC_READ_SDA)
{
receive++;
}
IIC_SCL_L();
iic_delay();
}
/* 判断是否继续读取从机数据 */
if ( !ack )
{
iic_nack();
}
else
{
iic_ack();
}
return receive;
}
Sotf_IIC.h: 头文件
#ifndef _SOFT_IIC_H_
#define _SOFT_IIC_H_
#include "main.h"
#define IIC_DELAY_TIME 5 //延时时间根据芯片手册的时序设置,这里选择5us
#define iic_delay() Delay_Us(IIC_DELAY_TIME)
#define IIC_SCL_H() HAL_GPIO_WritePin(IIC_SCL_GPIO_Port, IIC_SCL_Pin, GPIO_PIN_SET)
#define IIC_SCL_L() HAL_GPIO_WritePin(IIC_SCL_GPIO_Port, IIC_SCL_Pin, GPIO_PIN_RESET)
#define IIC_SDA_H() HAL_GPIO_WritePin(IIC_SDA_GPIO_Port, IIC_SDA_Pin, GPIO_PIN_SET)
#define IIC_SDA_L() HAL_GPIO_WritePin(IIC_SDA_GPIO_Port, IIC_SDA_Pin, GPIO_PIN_RESET)
#define IIC_READ_SDA HAL_GPIO_ReadPin(IIC_SDA_GPIO_Port, IIC_SDA_Pin)
/* SDA引脚设置宏定义 */
#define IIC_SDA(x) do{x ? \
HAL_GPIO_WritePin(IIC_SDA_GPIO_Port, IIC_SDA_Pin, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(IIC_SDA_GPIO_Port, IIC_SDA_Pin, GPIO_PIN_RESET); \
}while(0)
void iic_start(void);
void iic_stop(void);
uint8_t iic_wait_ack (void);
void iic_ack(void);
void iic_nack(void);
void iic_send_byte(uint8_t data);
uint8_t iic_read_byte(uint8_t ack);
#endif
AT24C02读写函数,分为随机地址单字节读写和连续字节读取以及按页写入
AT24C02.c:
#include "stm32f1xx_hal.h"
#include "24c02.h"
#include "main.h"
#include "string.h"
#include "i2c.h"
#include "Soft_IIC.h"
#include "delay.h"
/* 写入一个字节到从机AT24C02中 */
void at24c02_write_one_byte(uint8_t addr, uint8_t data)
{
/* 1、发送起始信号 */
iic_start();
/* 2、发送通讯地址(写操作地址) */
iic_send_byte(0xA0);
/* 3、等待应答信号 */
iic_wait_ack();
/* 4、发送内存地址:0~255 */
iic_send_byte(addr);
/* 5、等待应答信号 */
iic_wait_ack();
/* 6、发送写入数据 */
iic_send_byte(data);
/* 7、等待应答信号 */
iic_wait_ack();
/* 8、发送停止信号 */
iic_stop();
/* 等待EEPROM写入完成 */
Delay_Ms(10);
}
/* 往从机AT24C02中读取一个字节 */
uint8_t at24c02_read_one_byte(uint8_t addr)
{
uint8_t read = 0;
/* 1、发送起始信号 */
iic_start();
/* 2、发送通讯地址(写操作地址) */
iic_send_byte(0xA0);
/* 3、等待应答信号 */
if(iic_wait_ack() != 1) return 1;
/* 4、发送内存地址:0~255 */
iic_send_byte(addr);
/* 5、等待应答信号 */
if(iic_wait_ack() != 1) return 2;
/* 6、发送起始信号 */
iic_start();
/* 7、发送通讯地址(读操作地址) */
iic_send_byte(0xA1);
/* 8、等待应答信号 */
if(iic_wait_ack() != 1) return 3;
/* 9、接收数据并发送非应答(获取该地址即可) */
read = iic_read_byte(0);
/* 10、发送停止信号 */
iic_stop();
return read;
}
/*---------------------------------------------------------*/
/*函数名:AT24C02读取数据 */
/*参 数:addr:地址 rdata:接收缓冲区 datalen:读取长度 */
/*返回值:0:正确 其他:错误 */
/*---------------------------------------------------------*/
uint8_t AT24C02_ReadData(uint8_t addr, uint8_t *rdata, uint16_t datalen){
uint16_t i; //用于for循环
iic_start();
/* 2、发送通讯地址(写操作地址) */
iic_send_byte(0xA0);
/* 3、等待应答信号 */
if(iic_wait_ack() != 1) return 1;
/* 4、发送内存地址:0~255 */
iic_send_byte(addr);
/* 5、等待应答信号 */
if(iic_wait_ack() != 1) return 2;
/* 6、发送起始信号 */
iic_start();
/* 7、发送通讯地址(读操作地址) */
iic_send_byte(0xA1);
/* 8、等待应答信号 */
if(iic_wait_ack() != 1) return 3;
for(i = 0;i<datalen - 1;i++)
{
rdata[i] = iic_read_byte(1);
}
rdata[datalen - 1] = iic_read_byte(0);
iic_stop();
return 0; //正确,返回0
}
/*-------------------------------------------------*/
/*函数名: AT24C02写入一页(8字节)数据 */
/*参 数:addr:地址 wdata:需要写入的数据指针 */
/*返回值:0:正确 其他:错误 */
/*-------------------------------------------------*/
uint8_t AT24C02_WritePage(uint8_t addr, uint8_t *wdata){
uint8_t i; //用于for循环
iic_start(); //发送起始信号
iic_send_byte(0xA0); //发送24C02写操作器件地址
if(iic_wait_ack() != 1) return 1; //等待应答,错误的话,返回1
iic_send_byte(addr); //发送内部存储空间地址
if(iic_wait_ack() != 1) return 2; //等待应答,错误的话,返回2
for(i=0;i<8;i++){ //循环8次写入一页
iic_send_byte(wdata[i]); //发送数据
if(iic_wait_ack() != 1) return 3+i; //等待应答,错误的话,返回3+i
}
iic_stop(); //发送停止信号
/* 等待EEPROM写入完成 */
Delay_Ms(10);
return 0; //正确,返回0
}
AT24c02.h: 头文件
#ifndef M24C02_H
#define M24C02_H
#include "stdint.h"
#define M24C02_WADDR 0xA0 //24C02写操作器件地址
#define M24C02_RADDR 0xA1 //24C02读操作器件地址
void at24c02_write_one_byte(uint8_t addr, uint8_t data);
uint8_t at24c02_read_one_byte(uint8_t addr);
uint8_t AT24C02_ReadData(uint8_t addr, uint8_t *rdata, uint16_t datalen);
uint8_t AT24C02_WritePage(uint8_t addr, uint8_t *wdata);
#endif
main测试函数以及测试结果:(友情提醒,测试软件IIC时可以先从单字节写入读取开始,这样出问题了可以缩小查找范围)
main.c测试部分代码
uint8_t date[16];
uint8_t write[10] ={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A};
AT24C02_WritePage(0, write);
AT24C02_WritePage(8, write);
AT24C02_ReadData(0, date, 16);
for(int i = 0;i<16;i++){
myprintf("date:%x\r\n",date[i]);
}
AT24C32读写函数,由于其存储单元大于256个,因此寻址的时候需要输入16位地址,这也是它和AT24C02在驱动代码上唯二的区别,AT24C32地址要分两次输入,先输入高八位后输入低八位;其次就是AT24C32一页有32个字节,所以按页写入时应该一次写32个而不是8个字节。
AT24C32.c:
/* 4、发送内存地址:0~65536 */
if(EE_SIZE > 256) // >AT24C02,此时无法用8位寻址,得用16位寻址,先发高八位后发低八位
{
iic_send_byte(addr>>8);//发送高地址
iic_wait_ack();
}
iic_send_byte(addr%256);//发送低地址
AT24C32.h:
#define EE_SIZE 4095 //24C32有4096个字节,地址最大为4095
#define PAGE_SIZE 32 //页面大小(字节)
参考链接:STM32软件模拟实现IIC写入和读取AT24C02(STM32CubeMx配置)_怎么用stm32cubemx驱动软件 iic-CSDN博客AT24C01/AT24C02系列EEPROM芯片单片机读写驱动程序-CSDN博客AT24C01C/AT24C02C Data Sheet (microchip.com)