基于正点原子代码的个人改编,本篇(IIC实验)共3个章节。
注:本博客无盈利行为,真诚希望能帮助到大家!如有错误,还请指正!
1.在写程序之前需要弄清楚三个问题,是什么?为什么?怎么做?
IIC是什么?为什么要使用IIC通信?
IIC_STM32F1_AT24C02实验:IIC介绍(1)
这里的 AT24C02 存储器模块(EEPROM:带电可擦可编程只读存储器)就相当于是从机设备,STM32 单片机就相当于主机设备。
注:因为是利用 IO 口模拟 IIC 通信,所以选取任意正常可用的 IO口 都可行,例如 GPIOA~GPIOG,这里选用 PB6 和 PB7 仅是因为 AT24C02 外接模块挂载在了这两个IO端口上。
1.创建 iic.h 用户头文件
/*
* 基于 ST 官方固件库编程
* 通用 GPIO 模拟 IIC 模块
* 程序员:贬道
*/
#ifndef IIC_H
#define IIC_H
#include "stm32f10x.h"
void IIC_Idle(void); // 空闲状态
void IIC_Start(void); // 起始状态
void IIC_Stop(void); // 停止状态
#endif
2.创建 iic.c 头文件
#include "iic.h"
void IIC_Idle(void){
}
void IIC_Start(void){
}
void IIC_Stop(void){
}
3.根据 IIC 时序图编写通信的基础实现功能(本文的代码都已通过测试)
(1)首先编写空闲状态,即初始化并配置 IIC。
iic.c 文件
#include "iic.h"
#include "delay.h" // 滴答定时器延时模块
/**************************************/
void SCL_HIGH(void){
GPIO_SetBits(GPIOB, GPIO_Pin_6);
}
void SCL_LOW(void){
GPIO_ResetBits(GPIOB, GPIO_Pin_6);
}
/**************************************/
/**********************************************************************************/
GPIO_InitTypeDef GPIO_InitStruct_SDA; // SDA 配置结构体变量
void SDA_IN(void){
GPIO_InitStruct_SDA.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStruct_SDA.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOB, &GPIO_InitStruct_SDA);
}
void SDA_OUT(void){
GPIO_InitStruct_SDA.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStruct_SDA.GPIO_Mode = GPIO_Mode_Out_PP; // 通用推挽输出
GPIO_InitStruct_SDA.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct_SDA);
}
void SDA_HIGH(void){
GPIO_SetBits(GPIOB, GPIO_Pin_7);
}
void SDA_LOW(void){
GPIO_ResetBits(GPIOB, GPIO_Pin_7);
}
u8 READ_SDA(void){
return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7);
}
/**********************************************************************************/
/************************************************************************************/
// IIC 空闲状态:两条线都为高电平
void IIC_Idle(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// SCL 时钟线配置
GPIO_InitTypeDef GPIO_InitStruct_SCL;
GPIO_InitStruct_SCL.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct_SCL.GPIO_Mode = GPIO_Mode_Out_PP; // 通用推挽输出
GPIO_InitStruct_SCL.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct_SCL);
SCL_HIGH(); // 空闲时拉高 SCL
SDA_OUT();
SDA_HIGH(); // 空闲时拉高 SDA
}
void IIC_Start(void){
}
void IIC_Stop(void){
}
/************************************************************************************/
iic.h 文件
/*
* 基于 ST 官方固件库编程
* 通用 GPIO 模拟 IIC 模块
* 程序员:贬道
*/
#ifndef IIC_H
#define IIC_H
#include "stm32f10x.h"
// SCL 时钟线
/*************************************************************/
void SCL_HIGH(void); // SCL 输出高电平
void SCL_LOW(void); // SCL 输出低电平
/*************************************************************/
// SDA 数据线
/*************************************************************/
void SDA_IN(void); // SDA 输入模式
void SDA_OUT(void); // SDA 输出模式
void SDA_HIGH(void); // SDA 输出高电平
void SDA_LOW(void); // SDA 输出低电平
u8 READ_SDA(void); // 读 SDA 的输入电平
/*************************************************************/
// IIC 通信状态
/************************************/
void IIC_Idle(void); // 空闲状态
void IIC_Start(void); // 起始状态
void IIC_Stop(void); // 停止状态
/************************************/
#endif
(2)接着编写起始和停止状态(这里单独列出函数,否则篇幅过长,最后会附上本人所写的完整代码)。
iic.c 文件
// IIC 起始状态:SCL 高电平期间,SDA 由高变低
void IIC_Start(void){
SDA_OUT();
SDA_HIGH(); // 先对 SDA 拉高,以免产生停止信号
SCL_HIGH();
Delay_us(1); // 延时时间需参考要通信模块的手册,这里延时时间最好大于0.6微妙
SDA_LOW(); // SDA 拉低,起始信号产生
Delay_us(1); // 起始信号维持时间,这里延时时间最好大于0.6微妙
SCL_LOW(); // 钳住 IIC 总线,准备收发数据
}
// IIC 停止状态:SCL 高电平期间,SDA 由低变高
void IIC_Stop(void){
SDA_OUT();
SCL_LOW(); // 这里正常的逻辑就是先对 SCL 拉低
SDA_LOW();
Delay_us(1); // 停止信号建立时间,这里延时时间最好大于0.6微妙
SCL_HIGH(); // 这里正常的逻辑就是先对 SCL 拉高
SDA_HIGH(); // 停止信号触发
}
(3)再接着编写应答等待、应答、非应答函数功能。
iic.h 文件
void IIC_Ack(void); // 应答信号
void IIC_NAck(void); // 非应答信号
u8 IIC_WaitAck(void); // 等待应答
iic.c 文件
// IIC 应答:SCL 第9个脉冲期间,SDA 保持低
void IIC_Ack(void){
SCL_LOW(); // 这里正常的逻辑就是先对 SCL 拉低
SDA_OUT();
SDA_LOW(); // SDA 保持低电平
Delay_us(2); // SCL 低电平延时,延时时间自定义,保持相对正常的数值
SCL_HIGH();
Delay_us(2); // SCL 高电平延时
SCL_LOW(); // 应答信号产生,结束第9位的脉冲
}
// IIC 非应答:SCL 第9个脉冲期间,SDA 保持高
void IIC_NAck(void){
SCL_LOW(); // 这里正常的逻辑就是先对 SCL 拉低
SDA_OUT();
SDA_HIGH(); // SDA 保持高电平
Delay_us(2); // SCL 低电平延时,延时时间自定义,保持相对正常的数值
SCL_HIGH();
Delay_us(2); // SCL 高电平延时
SCL_LOW(); // 非应答信号产生,结束第9位的脉冲
}
// 等待应答:
// 返回值:1,接收应答失败;0,接收应答成功
u8 IIC_WaitAck(void){
u8 ucErrTime = 0; // 应答超时时间
SDA_IN(); // SDA 设置为输入
SDA_HIGH(); // SDA 先拉高,若被从机拉低则说明收到应答信号
SCL_HIGH();
Delay_us(2); // SCL 拉高,产生第9位的脉冲
while(READ_SDA()){
ucErrTime++;
if(ucErrTime > 250){ // 250 是自定义的数字上限,可以自行修改
IIC_Stop(); // 应答超时,结束 IIC 通信
return 1;
}
}
SCL_LOW(); // SCL 拉低,结束第9位的脉冲
return 0;
}
(4)最后编写 IIC 的数据传输逻辑函数
iic.h 文件
void IIC_Send_Byte(u8 txd); // IIC 发送一个字节
u8 IIC_Read_Byte(unsigned char ack); // IIC 读取一个字节
iic.c 文件
// IIC 发送一个字节:IIC 传输数据特点——高位先行。
/*
每个时钟脉冲传送一位数据。
SCL 为高时 SDA 必须保持稳定,因为此时 SDA 的改变被认为是控制信号。
每发送一个字节,产生8次时序的循环,8个时钟信号,
并将 SDA 的高低电平输出分别赋值为 1 或 0。
*/
void IIC_Send_Byte(u8 txd){
u8 t; // 数据位数
SDA_OUT();
SCL_LOW(); // 拉低时钟开始数据传输
for(t = 0; t < 8; t++){
// 数据高位先行,SDA 高低电平表示数据 1 和 0
if( ((txd & 0x80)>>7) == 0 ){
SDA_LOW();
}else{
SDA_HIGH();
}
txd <<= 1; // 数据高位先行,因此需将其移位存储
Delay_us(2); // 延时时间自定义(下面两个同理),保持相对正常的数值
SCL_HIGH(); // SCL先上升
Delay_us(2);
SCL_LOW(); // SCL再下降,形成一个脉冲,发送一位数据生效
Delay_us(2);
}
}
// IIC 读取一个字节:ack为1时,发送ACK;ack为0时,发送NACK。
/*
每读取一个字节,产生8次时序的循环,8个时钟信号,
并读取 SDA 的高低电平信号,最后还需要考虑要不要
继续读下一个字节,发送第9位的 Ack 或 NACK。
*/
u8 IIC_Read_Byte(unsigned char ack){
unsigned char i,receive = 0; // i 数据位数,receive 返回读取值
SDA_IN(); // SDA 输入模式
for(i = 0; i < 8; i++){
SCL_LOW(); // SCL 先下降,通过循环,形成时钟脉冲
Delay_us(2); // 延时时间自定义,但最好与写字节保持一致
SCL_HIGH();
receive <<= 1; // 数据高位先行,因此需将其移位存储
if(READ_SDA())
receive++; // 读取并组合记录数据,++表示读到1了
Delay_us(2);
}
// 读取8位后,主机需要变为发送模式,在第9位进行应答或不应答
// 此时 SCL 还是高电平状态,不过下面的应答会先将 SCL 拉低的
if (!ack){
IIC_NAck(); // 不应答
}else{
IIC_Ack(); // 应答
}
return receive;
}
IIC 位传输总结:
1.写字节:SDA为输出模式,输出高电平表示1,输出低电平表示0。
2.读字节:SDA为输入模式,读取高电平表示1,读取低电平表示0,包含第9位应答位。
**读写字节共同点:**每个时钟脉冲传送一位数据。数据高位先行。
iic.h
/*
* 基于 ST 官方固件库编程
* 通用 GPIO 模拟 IIC 模块
* 程序员:贬道
*/
#ifndef IIC_H
#define IIC_H
#include "stm32f10x.h"
// SCL 时钟线
/*************************************************************/
void SCL_HIGH(void); // SCL 输出高电平
void SCL_LOW(void); // SCL 输出低电平
/*************************************************************/
// SDA 数据线
/*************************************************************/
void SDA_IN(void); // SDA 输入模式
void SDA_OUT(void); // SDA 输出模式
void SDA_HIGH(void); // SDA 输出高电平
void SDA_LOW(void); // SDA 输出低电平
u8 READ_SDA(void); // 读 SDA 的输入电平
/*************************************************************/
// IIC 通信状态
/************************************/
void IIC_Idle(void); // 空闲状态
void IIC_Start(void); // 起始状态
void IIC_Stop(void); // 停止状态
/************************************/
// IIC 通信操作
/************************************************************/
/*
主机每向从机发送完一个字节的数据,总是需要等待从机给出一个
应答信号,来确认从机是否成功接收到了数据,从机应答主机
所需要的时钟也是由主机提供的。应答出现在每一次主机完成 8 个
数据位传输后紧跟着的时钟周期:
低电平 0 表示应答,1 表示非应答。
需要应答时,数据发出方将 SDA 总线设置为 3 态输入,
由于 IIC 总线上有上拉电阻,因此此时总线默认高电平,若数据接
收方正确接收到数据,则数据接收方将SDA总线拉低,以示正确应答。
*/
void IIC_Ack(void); // 应答信号
void IIC_NAck(void); // 非应答信号
u8 IIC_WaitAck(void); // 等待应答
void IIC_Send_Byte(u8 txd); // IIC 发送一个字节
u8 IIC_Read_Byte(unsigned char ack); // IIC 读取一个字节
/************************************************************/
#endif
iic.c
#include "iic.h"
#include "delay.h"
/**************************************/
void SCL_HIGH(void){
GPIO_SetBits(GPIOB, GPIO_Pin_6);
}
void SCL_LOW(void){
GPIO_ResetBits(GPIOB, GPIO_Pin_6);
}
/**************************************/
/**********************************************************************************/
GPIO_InitTypeDef GPIO_InitStruct_SDA; // SDA 配置结构体变量
void SDA_IN(void){
GPIO_InitStruct_SDA.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStruct_SDA.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOB, &GPIO_InitStruct_SDA);
}
void SDA_OUT(void){
GPIO_InitStruct_SDA.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStruct_SDA.GPIO_Mode = GPIO_Mode_Out_PP; // 通用推挽输出
GPIO_InitStruct_SDA.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct_SDA);
}
void SDA_HIGH(void){
GPIO_SetBits(GPIOB, GPIO_Pin_7);
}
void SDA_LOW(void){
GPIO_ResetBits(GPIOB, GPIO_Pin_7);
}
u8 READ_SDA(void){
return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7);
}
/**********************************************************************************/
/************************************************************************************/
// IIC 空闲状态:两条线都为高电平
void IIC_Idle(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// SCL 时钟线配置
GPIO_InitTypeDef GPIO_InitStruct_SCL;
GPIO_InitStruct_SCL.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct_SCL.GPIO_Mode = GPIO_Mode_Out_PP; // 通用推挽输出
GPIO_InitStruct_SCL.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct_SCL);
SCL_HIGH(); // 空闲时拉高 SCL
SDA_OUT();
SDA_HIGH(); // 空闲时拉高 SDA
}
// IIC 起始状态:SCL 高电平期间,SDA 由高变低
void IIC_Start(void){
SDA_OUT();
SDA_HIGH(); // 先对 SDA 拉高,以免产生停止信号
SCL_HIGH();
Delay_us(1); // 延时时间需参考要通信模块的手册,这里延时时间最好大于0.6微妙
SDA_LOW(); // SDA 拉低,起始信号产生
Delay_us(1); // 起始信号维持时间,这里延时时间最好大于0.6微妙
SCL_LOW(); // 钳住 IIC 总线,准备收发数据
}
// IIC 停止状态:SCL 高电平期间,SDA 由低变高
void IIC_Stop(void){
SDA_OUT();
SCL_LOW(); // 这里正常的逻辑就是先对 SCL 拉低
SDA_LOW();
Delay_us(1); // 停止信号建立时间,这里延时时间最好大于0.6微妙
SCL_HIGH(); // 这里正常的逻辑就是先对 SCL 拉高
SDA_HIGH(); // 停止信号触发
}
/************************************************************************************/
/************************************************************************************/
// IIC 应答:SCL 第9个脉冲期间,SDA 保持低
void IIC_Ack(void){
SCL_LOW(); // 这里正常的逻辑就是先对 SCL 拉低
SDA_OUT();
SDA_LOW(); // SDA 保持低电平
Delay_us(2); // SCL 低电平延时,延时时间自定义,保持相对正常的数值
SCL_HIGH();
Delay_us(2); // SCL 高电平延时
SCL_LOW(); // 应答信号产生,结束第9位的脉冲
}
// IIC 非应答:SCL 第9个脉冲期间,SDA 保持高
void IIC_NAck(void){
SCL_LOW(); // 这里正常的逻辑就是先对 SCL 拉低
SDA_OUT();
SDA_HIGH(); // SDA 保持高电平
Delay_us(2); // SCL 低电平延时,延时时间自定义,保持相对正常的数值
SCL_HIGH();
Delay_us(2); // SCL 高电平延时
SCL_LOW(); // 非应答信号产生,结束第9位的脉冲
}
// 等待应答:
// 返回值:1,接收应答失败;0,接收应答成功
u8 IIC_WaitAck(void){
u8 ucErrTime = 0; // 应答超时时间
SDA_IN(); // SDA 设置为输入
SDA_HIGH(); // SDA 先拉高,若被从机拉低则说明收到应答信号
SCL_HIGH();
Delay_us(2); // SCL 拉高,产生第9位的脉冲
while(READ_SDA()){
ucErrTime++;
if(ucErrTime > 250){ // 250 是自定义的数字上限,可以自行修改
IIC_Stop(); // 应答超时,结束 IIC 通信
return 1;
}
}
SCL_LOW(); // SCL 拉低,结束第9位的脉冲
return 0;
}
// IIC 发送一个字节:IIC 传输数据特点——高位先行。
/*
每个时钟脉冲传送一位数据。
SCL 为高时 SDA 必须保持稳定,因为此时 SDA 的改变被认为是控制信号。
每发送一个字节,产生8次时序的循环,8个时钟信号,
并将 SDA 的高低电平输出分别赋值为 1 或 0。
*/
void IIC_Send_Byte(u8 txd){
u8 t; // 数据位数
SDA_OUT();
SCL_LOW(); // 拉低时钟开始数据传输
for(t = 0; t < 8; t++){
// 数据高位先行,SDA 高低电平表示数据 1 和 0
if( ((txd & 0x80)>>7) == 0 ){
SDA_LOW();
}else{
SDA_HIGH();
}
txd <<= 1; // 数据高位先行,因此需将其移位存储
Delay_us(2); // 延时时间自定义(下面两个同理),保持相对正常的数值
SCL_HIGH(); // SCL先上升
Delay_us(2);
SCL_LOW(); // SCL再下降,形成一个脉冲,发送一位数据生效
Delay_us(2);
}
}
// IIC 读取一个字节:ack为1时,发送ACK;ack为0时,发送NACK。
/*
每读取一个字节,产生8次时序的循环,8个时钟信号,
并将 SDA 的高低电平输出分别赋值为 1 或 0。
并读取 SDA 的高低电平信号,最后还需要考虑要不要
继续读下一个字节,发送第9位的 Ack 或 NACK。
*/
u8 IIC_Read_Byte(unsigned char ack){
unsigned char i,receive = 0; // i 数据位数,receive 返回读取值
SDA_IN(); // SDA 输入模式
for(i = 0; i < 8; i++){
SCL_LOW(); // SCL 先下降,通过循环,形成时钟脉冲
Delay_us(2); // 延时时间自定义,但最好与写字节保持一致
SCL_HIGH();
receive <<= 1; // 数据高位先行,因此需将其移位存储
if(READ_SDA())
receive++; // 读取并组合记录数据,++表示读到1了
Delay_us(2);
}
// 读取8位后,主机需要变为发送模式,在第9位进行应答或不应答
// 此时 SCL 还是高电平状态,不过下面的应答会先将 SCL 拉低的
if (!ack){
IIC_NAck(); // 不应答
}else{
IIC_Ack(); // 应答
}
return receive;
}
/************************************************************************************/
至此,IIC模块代码编写完毕!