首先說一下,這也就是個人學習筆記,如果有不對的地方歡迎各位大神指出,軟體部分的代碼網上到處都是,我這份不過是自己手敲了一遍,確保可用而已,
最近公司要在ST8的板子上使用硬體IIC,網上這方面的資料太少了,就想著把自己的學習經歷記錄一下,也給后來的人省一些時間,這份筆記更多的不是代碼,而是為什么要這么寫代碼,想直接抄作業的朋友們可以直接看每個大章節的最后一節,
IIC分為硬體實作和軟體模擬,這兩種在作業模式下都有四種作業模式
● 從發送器
● 從接收器
● 主發送器
● 主接收器
1.軟體IIC主機實作
1.1 初始化
萬物始于初始化嘛,首先肯定是要寫個初始化
因為使用的是軟體模擬IIC,任意使用兩個IO口作為SDA和SCL,只要和你硬體接的是一樣的就可以了,ST8 IIC模塊對于IO口時鐘也沒有單獨的管理,所以初始化非常簡單,
void box_I2C_Init(void)
{
/* 內部時鐘 16 M 不分頻 */
CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV8);
GPIO_Init(IIC_GPIO, IIC_SCLPIN, GPIO_MODE_OUT_PP_HIGH_FAST);
GPIO_Init(IIC_GPIO, IIC_SDAPIN, GPIO_MODE_OUT_PP_HIGH_FAST);
}
1.2 延時
//軟體延時
void delay_1us(uint32_t num)
{
for(int i=0;i<num;i++)
{
nop();
}
}
void delay_1ms(uint32_t num)
{
for(int i=0;i<num;i++)
{
delay_us(1000);
}
}
1.3 開始
由圖可知,開始時SCL置高,SDA線輸出一個高到底的信號,圖是stm32f4 的資料手冊里的圖,但是IIC的邏輯是一樣的,st8的資料手冊里沒有這個圖,

//發送開始信號
void box_I2C_Start(void)
{
SDA_OUT(); //SDA設定為輸出
SDA_HIGH(); //拉高SDA
SCL_HIGH(); //拉高SCL
delay_us(2); //等待2us
SDA_LOW(); //拉低SDA,給一個由高到低的跳變
delay_us(2); //等待2us
SCL_LOW(); //鉗住I2C時鐘總線,準備發送或接收資料
}
1.3 終止信號
還是如上圖所示,截止時SCL需要保持在高電平,SDA有一個由低到高的跳變
//發送停止信號
void box_I2C_Stop(void)
{
SDA_OUT(); //SDA設定為輸出
SDA_LOW(); //拉低SDA
SCL_HIGH(); //拉高SCL
delay_us(2); //等待2us
SDA_HIGH(); //拉低SDA,給一個由低到高的跳變
delay_us(2); //等待2us
}
1.4 ACK應答與NACK應答
應答信號是第九個時鐘周期內發生的情況,ACK(低電平)---規定為有效應答位,NACK(高電平),規定為非應答位,應答和非應答都是一種信號,應答表示通信還要繼續,非應答表示通信要結束了,這里要注意一下,不發送應答信號和非應答信號不是一回事,
SCL一直由主機控制,開始和結束信號都由主機控制
SDA根據讀寫確定傳輸資料方向:主機->從機、從機->主機
SDA資料按位發送(或讀取),每發送8位,第9位被判定為應答和非應答
應答和非應答可由主機發送給從機,可以由從機發送給主機,

//產生一個ACK應答信號
void box_I2C_Ack(void)
{
SCL_LOW(); //拉低SCL
SDA_OUT(); //SDA設定為輸出
SDA_LOW(); //拉低SDA
delay_1us(4); //保持4us
SCL_HIGH(); //拉高SCL
delay_1us(4); //保持4us
SCL_LOW(); //拉低SCL
}
//產生一個NACK非應答信號
void box_I2C_NAck(void)
{
SCL_LOW(); //拉低SCL
SDA_OUT(); //SDA設定為輸出
SDA_HIGH(); //拉低SDA
delay_1us(4); //保持4us
SCL_HIGH(); //拉高SCL
delay_1us(4); //保持4us
SCL_LOW(); //拉低SCL
}
1.5 等待一個應答

由圖可知,在已經經過八個時鐘周期后,我們此時應當等待一個應答,邏輯是先拉高SDA和SCL,然后去讀SDA線的狀態,如果在250次判斷(可以適當拉長,不要超過256)內讀到SDA變為0,則說明成功,如果沒有讀到,就說明沒有接收到ACK信號,執行結束并回傳讀取失敗,
//等待應答信號到來
//回傳值:1,接收應答失敗
// 0,接收應答成功
u8 box_I2C_Wait_Ack(void)
{
u8 uTime=0;
SDA_IN(); //SDA設定為輸入
// SDA_HIGH();
SCL_HIGH();
delay_1us(4);
while(GPIO_ReadInputPin(IIC_GPIO,SDAPIN))
{
uTime++;
if(uTime>250)
{
box_I2C_Stop();
return 1;
}
}
SCL_LOW();//時鐘輸出0
return 0;
}
到此為止,準備作業基本完成,接下來可以寫點有邏輯的東西了,
1.6 寫一個位元組
這段代碼的思想就是先發送一個位元組中的最高位,然后左移,依次發送8次,走完也就發完了一個位元組,代碼如下,
//IIC發送一個位元組
//回傳從機有無應答
//1,有應答
//0,無應答
void box_I2C_Send_Byte(u8 txd)
{
u8 t;//用來計數的臨時變數
SDA_OUT(); //配置資料為輸出
SCL_LOW(); //拉低時鐘開始資料傳輸
for(t=0;t<8;t++)//發送八位
{
if((txd&0x80)>>7)//判斷最高位資料 0x1000 0000
{
SDA_HIGH();//如果最高位為1,SDA線置高發送1
}
else
{
SDA_LOW();//如果最高位為0,SDA線置低發送0
}
txd<<=1; //將txd左移一位 ,等待下次發送
//寫EEprom需要加這個延時
// delay_us(1); //對TEA5767這三個延時都是必須的
// SCL_HIGH();
// delay_us(1);
// SCL_LOW();
// delay_us(1);
}
box_I2C_Wait_Ack();
}
1.7 讀一個位元組
讀一個位元組算是主接收器,主機接收資料,
這段代碼邏輯思想是:因為時候串行同行,只能按位接收,所以定義1個u8型別的變數recive,用來存盤讀入的資料,每個時鐘周期內讀到一位資料,如果是高電平,就令recive+1然后左移一位,如果是低電平,就不進行操作直接左移一位,循壞八次后,正好讀出一個位元組的資料,
//讀1個位元組,ack=1時,發送ACK,ack=0,發送nACK
u8 box_I2C_Read_Byte()
{
unsigned char i,receive=0;
SDA_OUT();//SDA設定為輸出
SDA_HIGH();//SDA輸出一個高電平
SDA_IN();//SDA設定為輸入
for(i=0;i<8;i++ )
{
SCL_LOW();
delay_us(2);
SCL_HIGH();
delay_us(2);
//一個時鐘周期,把receive左移一位,最低位
receive<<=1;
if(GPIO_ReadInputPin(IIC_GPIO,SDAPIN))receive++;
SCL_LOW();
delay_us(2);
}
// if (!ack)
// IIC_NAck();//發送nACK
// else
// IIC_Ack(); //發送ACK
return receive;
}
1.8 與eeprom互動
IIC 軟體模擬底層模塊到此就結束了,接下來的步驟是由另一個IIC決定的,也就是IIC從機,一般使用的比較多的從機是eeprom設備,簡單的說一下寫一下,
這個寫資料和上邊那個寫資料有什么不同呢,兩方通信就要滿足兩方的規則,可以簡單的理解為上邊那個發送寫完,我們就能發出去一個資料了,但是這個資料是什么,對方怎么接受,還需要滿足對方的規則,話不多說,老規矩,上圖,

這張圖就是向從機中寫入資料的時序圖,可以看到,我們需要發送一個開始信號,然后發送設備地址(注意是從設備地址),然后發送內部地址(也就是資料在從設備中存放的地址),然后發送資料,最后發送停止信號,只有按這樣的格式發送出去的資料,從設備才能識別,才能認識,
//一般的寫邏輯
void box_I2C_Write(u8 reg_addr,u8 data)
{
box_I2C_Start();//開始信號
box_I2C_Send_Byte(Slave_Address);//第一個八位資料發送的是從設備地址
box_I2C_Send_Byte(reg_addr);//第二個八位資料發送的是從設備內部地址,一般也就是這個資料要存放在從設備的那個地址
box_I2C_Send_Byte(data);//第三個八位資料發送的是從設備地址,開始寫入資料
box_I2C_Stop();//停止信號
}

讀邏輯要相對復雜一些,宗旨還是看圖寫程式,他需要什么東西就給他發送什么東西,和上一段沒有什么本質不同,這里就不細說了,直接上原始碼,
//一般的讀邏輯
u8 box_I2c_Read(u8 addr)
{
u8 data;
box_I2C_Start();
box_I2C_Send_Byte(Slave_Address);
box_I2C_Send_Byte(addr);
// IIC_Stop();
box_I2C_Start();
box_I2C_Send_Byte(Slave_Address+1);
data=box_I2C_Read_Byte();
box_I2C_NAck();
box_I2C_Stop();
return data;
}
到這里為止的代碼網上到處都是,我這份不過是自己手敲了一遍,確保可用,軟體模擬IIC的方法寫在哪個板子里都差不多,寫在STM32里初始化時候記得開時鐘就可以了,別的板子的要用做煮粉代碼的時候改一下埠改一下頭檔案就能用了,這就是軟體模擬的好處,可移植性強,缺點也很明顯,就是主機在進行IIC通信時,只能進行IIC通信,CPU利用率低,速度慢,
以下是軟體IIC原始碼
/**
******************************************************************************
* @file box_i2c.h
* @author box
* @version V1.0.0
* @date 2021-6-10
******************************************************************************
*/
#include "stm8s.h"
#include "stm8s_gpio.h"
extern GPIO_Pin_TypeDef SDAPIN ;
extern GPIO_Pin_TypeDef SCLPIN ;
#define IIC_GPIO GPIOE
#define IIC_SDAPIN GPIO_PIN_2
#define IIC_SCLPIN GPIO_PIN_1
#define u8 uint8_t
#define u16 uint16_t
#define Host_Address 0x75
#define Slave_Address 0xD0 //IIC寫入時的地址位元組資料,+1為讀取
//IO方向設定
#define SDA_IN() {GPIO_Init(IIC_GPIO, IIC_SDAPIN, GPIO_MODE_IN_PU_NO_IT);}
#define SDA_OUT() {GPIO_Init(IIC_GPIO, IIC_SDAPIN, GPIO_MODE_OUT_PP_HIGH_FAST);}
#define SDA_HIGH() {GPIO_WriteHigh(IIC_GPIO,IIC_SDAPIN);}
#define SDA_LOW() {GPIO_WriteLow(IIC_GPIO,IIC_SDAPIN);}
#define SCL_HIGH() {GPIO_WriteHigh(IIC_GPIO,IIC_SCLPIN);}
#define SCL_LOW() {GPIO_WriteLow(IIC_GPIO,IIC_SCLPIN);}
//IO操作函式
#define READ_SDA() {(u8)GPIO_ReadInputPin(IIC_GPIO,IIC_SDAPIN) }//輸入SDA
//IIC所有操作函式
void box_I2C_Init(void); //初始化IIC的IO口
void box_I2C_Start(void); //發送IIC開始信號
void box_I2C_Stop(void); //發送IIC停止信號
void box_I2C_Send_Byte(u8 txd); //IIC發送一個位元組
u8 box_I2C_Read_Byte(void);
u8 box_I2C_Wait_Ack(void); //IIC等待ACK信號
void box_I2C_Ack(void); //IIC發送ACK信號
void box_I2C_NAck(void); //IIC不發送ACK信號
void delay_1us(uint32_t num);
void delay_1ms(uint32_t num);
void box_I2C_Write(u8 reg_addr,u8 data);
u8 box_I2c_Read(u8 addr);
/**
******************************************************************************
* @file box_i2c.c
* @author box
* @version V1.0.0
* @date 2021-6-10
******************************************************************************
*/
#include "box_i2c.h"
void delay_1us(uint32_t num)
{
for(int i=0;i<num;i++)
{
nop();
}
}
void delay_1ms(uint32_t num)
{
for(int i=0;i<num;i++)
{
delay_1us(1000);
}
}
//初始化
void box_I2C_Init(void)
{
GPIO_Init(IIC_GPIO, IIC_SCLPIN, GPIO_MODE_OUT_PP_HIGH_FAST);
GPIO_Init(IIC_GPIO, IIC_SDAPIN, GPIO_MODE_OUT_PP_HIGH_FAST);
}
//發送開始信號
void box_I2C_Start(void)
{
SDA_OUT(); //SDA設定為輸出
SDA_HIGH(); //拉高SDA
SCL_HIGH(); //拉高SCL
delay_1us(2); //等待2us
SDA_LOW(); //拉低SDA,給一個由高到低的跳變
delay_1us(2); //等待2us
SCL_LOW(); //鉗住I2C時鐘總線,準備發送或接收資料
}
//發送停止信號
void box_I2C_Stop(void)
{
SDA_OUT(); //SDA設定為輸出
SDA_LOW(); //拉低SDA
SCL_HIGH(); //拉高SCL
delay_1us(2); //等待2us
SDA_HIGH(); //拉低SDA,給一個由低到高的跳變
delay_1us(2); //等待2us
}
//產生一個ACK應答信號
void box_I2C_Ack(void)
{
SCL_LOW(); //拉低SCL
SDA_OUT(); //SDA設定為輸出
SDA_LOW(); //拉低SDA
delay_1us(4); //保持4us
SCL_HIGH(); //拉高SCL
delay_1us(4); //保持4us
SCL_LOW(); //拉低SCL
}
//產生一個NACK非應答信號
void box_I2C_NAck(void)
{
SCL_LOW(); //拉低SCL
SDA_OUT(); //SDA設定為輸出
SDA_HIGH(); //拉低SDA
delay_1us(4); //保持4us
SCL_HIGH(); //拉高SCL
delay_1us(4); //保持4us
SCL_LOW(); //拉低SCL
}
//等待應答信號到來
//回傳值:1,接收應答失敗
// 0,接收應答成功
u8 box_I2C_Wait_Ack(void)
{
u8 uTime=0;
SDA_IN(); //SDA設定為輸入
// SDA_HIGH();
SCL_HIGH();
delay_1us(4);
while(GPIO_ReadInputPin(IIC_GPIO,SDAPIN))
{
uTime++;
if(uTime>250)
{
box_I2C_Stop();
return 1;
}
}
SCL_LOW();//時鐘輸出0
return 0;
}
//IIC發送一個位元組
//回傳從機有無應答
//1,有應答
//0,無應答
void box_I2C_Send_Byte(u8 txd)
{
u8 t;//用來計數的臨時變數
SDA_OUT(); //配置資料為輸出
SCL_LOW(); //拉低時鐘開始資料傳輸
for(t=0;t<8;t++)//發送八位
{
if((txd&0x80)>>7)//判斷最高位資料 0x1000 0000
{
SDA_HIGH();//如果最高位為1,SDA線置高發送1
}
else
{
SDA_LOW();//如果最高位為0,SDA線置低發送0
}
txd<<=1; //將txd左移一位 ,等待下次發送
// delay_us(1); //對TEA5767這三個延時都是必須的
// SCL_HIGH();
// delay_us(1);
// SCL_LOW();
// delay_us(1);
}
box_I2C_Wait_Ack();
}
//讀1個位元組,ack=1時,發送ACK,ack=0,發送nACK
u8 box_I2C_Read_Byte()
{
unsigned char i,receive=0;
SDA_OUT();//SDA設定為輸出
SDA_HIGH();//SDA輸出一個高電平
SDA_IN();//SDA設定為輸入
for(i=0;i<8;i++ )
{
SCL_LOW();
delay_us(2);
SCL_HIGH();
delay_us(2);
//一個時鐘周期,把receive左移一位,最低位
receive<<=1;
if(GPIO_ReadInputPin(IIC_GPIO,IIC_SDAPIN))receive++;
SCL_LOW();
delay_us(2);
}
// if (!ack)
// IIC_NAck();//發送nACK
// else
// IIC_Ack(); //發送ACK
return receive;
}
//一般的寫邏輯
void box_I2C_Write(u8 reg_addr,u8 data)
{
box_I2C_Start();//開始信號
box_I2C_Send_Byte(Slave_Address);//第一個八位資料發送的是從設備地址
box_I2C_Send_Byte(reg_addr);//第二個八位資料發送的是從設備內部地址,一般也就是這個資料要存放在從設備的那個地址
box_I2C_Send_Byte(data);//第三個八位資料發送的是從設備地址,開始寫入資料
box_I2C_Stop();//停止信號
}
//一般的讀邏輯
u8 box_I2c_Read(u8 addr)
{
u8 data;
box_I2C_Start();
box_I2C_Send_Byte(Slave_Address);
box_I2C_Send_Byte(addr);
// IIC_Stop();
box_I2C_Start();
box_I2C_Send_Byte(Slave_Address+1);
data=box_I2C_Read_Byte();
box_I2C_NAck();
box_I2C_Stop();
return data;
}
2. 硬體IIC主機實作
首先STM32標準庫的硬體IIC部分肯定是有問題的,這一點毋庸置疑,那么這個問題是什么,又是怎么解決的,請看下文,
2.1 初始化
首先還是萬物始于初始化,
ST8這個板子里沒有對于IO口的單獨時鐘管理,所以只要記得開IIC時鐘就可以了,
既然要用硬體IIC,直接使用標準庫里的IIC_Init(),記得在專案中添加檔案stm8s_i2c.c,記得包含頭檔案
接下來就是看引數應該怎么填了,來看下庫里的說明
/**
* @brief Initializes the I2C according to the specified parameters in standard
* or fast mode.
* @param OutputClockFrequencyHz : Specifies the output clock frequency in Hz.
* @param OwnAddress : Specifies the own address.
* @param I2C_DutyCycle : Specifies the duty cycle to apply in fast mode.
* This parameter can be any of the @ref I2C_DutyCycle_TypeDef enumeration.
* @note This parameter don't have impact when the OutputClockFrequency lower
* than 100KHz.
* @param Ack : Specifies the acknowledge mode to apply.
* This parameter can be any of the @ref I2C_Ack_TypeDef enumeration.
* @param AddMode : Specifies the acknowledge address to apply.
* This parameter can be any of the @ref I2C_AddMode_TypeDef enumeration.
* @param InputClockFrequencyMHz : Specifies the input clock frequency in MHz.
* @retval None
*/
那么,翻譯成人話是什么意思呢,
void I2C_Init(uint32_t OutputClockFrequencyHz, //IIC的輸出時鐘頻率Hz 100k或400k
uint16_t OwnAddress, //本I2C設備地址
I2C_DutyCycle_TypeDef I2C_DutyCycle, //占空比
I2C_Ack_TypeDef Ack, //應答模式
I2C_AddMode_TypeDef AddMode, //地址模式設定, 7 or 10
uint8_t InputClockFrequencyMHz //輸入時鐘頻率MHz,提供給I2C硬體時鐘
);
void IIC_Init(void)
{
CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1); /* 內部時鐘 16 M 不分頻 */
CLK_PeripheralClockConfig (CLK_PERIPHERAL_I2C,ENABLE);//開啟IIC時鐘
I2C_DeInit();//復位
GPIO_Init(GPIOE, SCLPIN, GPIO_MODE_OUT_OD_HIZ_FAST);//PE1 I2C_SCL 功能引腳,總線的時鐘腳,設為高速開漏高阻輸出,
GPIO_Init(GPIOE, SDAPIN, GPIO_MODE_OUT_OD_HIZ_FAST);//PE2 I2C_SDA 功能引腳,總線的資料腳,設為高速開漏高阻輸出
I2C_Init(100000, //輸出時鐘頻率Hz 100k
Host_address, //本I2C設備地址
I2C_DUTYCYCLE_2, //2 or 17,占空比
I2C_ACK_CURR, //應答模式
I2C_ADDMODE_7BIT, //地址設定, 7 or 10
8//輸入時鐘頻率MHz,提供給I2C硬體時鐘
);//初始化I2C
I2C_Cmd(ENABLE);//使能I2C
r = 1;//除錯用的變數
}
2.2 寫一個位元組
使用庫里的函式去進行一個寫位元組的操作,但是ST8的手冊中沒有傳輸序列圖,這里就參考了STM32的,和軟體模擬一樣,想要雙方進行通信,不光需要滿足主發送器的邏輯,還要滿足從接收器的邏輯,




這里為了方便對比,根據四個圖的邏輯做了一個表,

這個部分就比較有意思了,正常的初始化讀寫等操作寫萬以后下載除錯,發現總是卡死,第一次會卡死在Event的判斷,卡死以后在重新燒寫直接卡死在總線判忙,后來上了邏輯分析儀,看到的現象和除錯時一樣,
邏輯分析儀截圖
從代碼上來看,每次都會卡死在Event,網上找了很多資料,比較公認的說法是標準庫的問題,在有一個標志位沒有及時清除
參考博客:
https://www.cnblogs.com/1024E/p/13322890.html
https://blog.csdn.net/zhejfl/article/details/82866684
如果這篇文章幫助到了您,希望可以給我點一個贊,求三連求硬幣求轉發哈哈哈,這對我真的很重要!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/286837.html
標籤:其他
