目录
一、知识点
1.1 关于I2C协议
- 物理层

- 协议层
主要是定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等。
可以看出I2C在通讯的时候,只有在SCL处于高电平时,SDA的数据传输才是有效的。SDA 信号线是用于传输数据,SCL 信号线是保证数据同步。
当SDA传输数据后,接收方对接受到的数据进行一个应答。如果希望继续进行传输数据,则回应应答信号(低电平),否则回应非应答信号(高电平)。
- 软件I2C和硬件I2C
①硬件I2C
直接利用 STM32 芯片中的硬件 I2C 外设。
只要配置好对应的寄存器,外设就会产生标准串口协议的时序。在初始化好 I2C 外设后,只需要把某寄存器位置 1,此时外设就会控制对应的 SCL 及 SDA 线自动产生 I2C 起始信号,不需要内核直接控制引脚的电平。
②软件I2C
直接使用 CPU 内核按照 I2C 协议的要求控制 GPIO 输出高低电平,从而模拟I2C。
需要在控制产生 I2C 的起始信号时,控制作为 SCL 线的 GPIO 引脚输出高电平,然后控制作为 SDA 线的 GPIO 引脚在此期间完成由高电平至低电平的切换,最后再控制SCL 线切换为低电平,这样就输出了一个标准的 I2C 起始信号。
③两者的差别
硬件 I2C 直接使用外设来控制引脚,可以减轻 CPU 的负担。不过使用硬件I2C 时必须使用某些固定的引脚作为 SCL 和 SDA,软件模拟 I2C 则可以使用任意 GPIO 引脚,相对比较灵活。对于硬件I2C用法比较复杂,软件I2C的流程更清楚一些。如果要详细了解I2C的协议,使用软件I2C可能更好的理解这个过程。在使用I2C过程,硬件I2C可能通信更加快,更加稳定。
1.2 关于SPI协议
- SPI物理层

SS( Slave Select):从设备选择信号线,常称为片选信号线。
SCK (Serial Clock):时钟信号线,用于通讯数据同步。
MOSI (Master Output, Slave Input):主设备输出/从设备输入引脚。
MISO(Master Input,,Slave Output):主设备输入/从设备输出引脚。
- SPI协议层
SPI 基本通讯过程
MOSI 与 MISO 的信号只在 NSS 为低电平的时候才有效,在 SCK 的每个时钟周期 MOSI 和 MISO 传输一位数据。
数据有效性
CPOL/CPHA 及通讯模式
1.3 了解AHT20芯片的相关信息
具体信息请到官方下载对应产品介绍文档,资料链接如下
软件下载-温湿度传感器 温湿度芯片 温湿度变送器模块 气体传感器 流量传感器 广州奥松电子股份有限公司
1.4 OLED屏的滚屏命令
- 水平左右移
OLED_WR_Byte(0x2E,OLED_CMD); //关闭滚动
OLED_WR_Byte(0x26,OLED_CMD); //水平向左或者右滚动 26/27
OLED_WR_Byte(0x00,OLED_CMD); //虚拟字节
OLED_WR_Byte(0x00,OLED_CMD); //起始页 0
OLED_WR_Byte(0x07,OLED_CMD); //滚动时间间隔
OLED_WR_Byte(0x07,OLED_CMD); //终止页 7
OLED_WR_Byte(0x00,OLED_CMD); //虚拟字节
OLED_WR_Byte(0xFF,OLED_CMD); //虚拟字节
OLED_WR_Byte(0x2F,OLED_CMD); //开启滚动
- 垂直和水平滚动
OLED_WR_Byte(0x2e,OLED_CMD); //关闭滚动
OLED_WR_Byte(0x29,OLED_CMD); //水平垂直和水平滚动左右 29/2a
OLED_WR_Byte(0x00,OLED_CMD); //虚拟字节
OLED_WR_Byte(0x00,OLED_CMD); //起始页 0
OLED_WR_Byte(0x07,OLED_CMD); //滚动时间间隔
OLED_WR_Byte(0x07,OLED_CMD); //终止页 1
OLED_WR_Byte(0x01,OLED_CMD); //垂直滚动偏移量
OLED_WR_Byte(0x2F,OLED_CMD); //开启滚动
本OLED屏的芯片类型:SSD1306
详细命令的介绍,请查找SSD1306-OLED驱动中文手册或者参考下面链接进行了解
SSD1306(OLED驱动芯片)指令详解_朝气蓬勃-优快云博客
1.5 AHT20引脚接法
SCL-->PB6
SDA-->PB7
本文代码主要使用PB6,PB7引脚,如果需要使用其它引脚可自行修改。
1.6 OLED引脚接法
0.96寸OLED显示屏相关介绍
参考下面链接:
0.96inch SPI OLED Module - LCD wiki
二、采集温度传至上位机
2.1 完整代码
链接:https://pan.baidu.com/s/1TZNBqkWH_aRGktK8Wk8cTA
提取码:qwer
下面是主要的代码
main.c
#include "stm32f10x.h"
#include "usart.h"
#include "delay.h"
#include "bsp_i2c.h"
int main(void)
{
//延时初始化
delay_init();
//串口初始化
uart_init(115200);
//
IIC_Init();
while(1)
{
read_AHT20_once();
delay_ms(1500);
}
}
/*********************************************END OF FILE**********************/
bsp_i2c.c
#include "bsp_i2c.h"
#include "delay.h"
#include "string.h"
uint8_t ack_status=0;
uint8_t readByte[6];
uint32_t H1=0; //Humility
uint32_t T1=0; //Temperature
uint8_t AHT20_OutData[4];
/****************
*初始化 I2C 函数
****************/
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//启用高速 APB (APB2) 外围时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE );
//GPIO 定义
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//初始化 SCL(Pin6)高电平
IIC_SCL=1;
//初始化 SDA(Pin7)高电平
IIC_SDA=1;
}
/*********************
*AHT20 数据操作总函数
*********************/
void read_AHT20_once(void)
{
printf("读取数据中");
//延时 10 微妙
delay_ms(10);
//传输数据前进行启动传感器和软复位
reset_AHT20();
delay_ms(10);
//查看使能位
init_AHT20();
delay_ms(10);
//触发测量
startMeasure_AHT20();
delay_ms(80);
//读数据
read_AHT20();
delay_ms(5);
}
void reset_AHT20(void)
{
//数据传输开始信号
I2C_Start();
//发送数据
I2C_WriteByte(0x70);
//接收 ACK 信号
ack_status = Receive_ACK();
//判断 ACK 信号
if(ack_status)
{
printf(">");
}
else
printf("×");
//发送软复位命令(重启传感器系统)
I2C_WriteByte(0xBA);
//接收 ACK 信号
ack_status = Receive_ACK();
//判断 ACK 信号
if(ack_status)
printf(">");
else
printf("×");
//停止 I2C 协议
I2C_Stop();
}
//0x70 —> 0111 0000 前七位表示 I2C 地址,第八位为0,表示 write
//0xE1 —> 看状态字的校准使能位Bit[3]是否为 1
//0x08 0x00 —> 0xBE 命令的两个参数,详见 AHT20 参考手册
void init_AHT20(void)
{
//传输开始
I2C_Start();
//写入 0x70 数据
I2C_WriteByte(0x70);
//接收 ACK 信号
ack_status = Receive_ACK();
//判断 ACK 信号
if(ack_status)
printf(">");
else
printf("×");
//写入 0xE1 数据
I2C_WriteByte(0xE1);
ack_status = Receive_ACK();
if(ack_status)
printf(">");
else
printf("×");
//写入 0x08 数据
I2C_WriteByte(0x08);
ack_status = Receive_ACK();
if(ack_status)
printf(">");
else
printf("×");
//写入 0x00 数据
I2C_WriteByte(0x00);
ack_status = Receive_ACK();
if(ack_status)
printf(">");
else
printf("×");
//停止 I2C 协议
I2C_Stop();
}
//0x70 —> 0111 0000 前七位表示 I2C 地址,第八位为0,表示 write
//0xAC —> 触发测量
//0x33 0x00 —> 0xAC 命令的两个参数,详见 AHT20 参考手册
void startMeasure_AHT20(void)
{
//启动 I2C 协议
I2C_Start();
I2C_WriteByte(0x70);
ack_status = Receive_ACK();
if(ack_status)
printf(">");
else
printf("×");
I2C_WriteByte(0xAC);
ack_status = Receive_ACK();
if(ack_status)
printf(">");
else
printf("×");
I2C_WriteByte(0x33);
ack_status = Receive_ACK();
if(ack_status)
printf(">");
else
printf("×");
I2C_WriteByte(0x00);
ack_status = Receive_ACK();
if(ack_status)
printf(">");
else
printf("×");
I2C_Stop();
}
void read_AHT20(void)
{
uint8_t i;
//初始化 readByte 数组
for(i=0; i<6; i++)
{
readByte[i]=0;
}
I2C_Start();
//通过发送 0x71 可以获取一个字节的状态字
I2C_WriteByte(0x71);
ack_status = Receive_ACK();
//接收 6 个 8 bit的数据
readByte[0]= I2C_ReadByte();
//发送 ACK 信号
Send_ACK();
readByte[1]= I2C_ReadByte();
Send_ACK();
readByte[2]= I2C_ReadByte();
Send_ACK();
readByte[3]= I2C_ReadByte();
Send_ACK();
readByte[4]= I2C_ReadByte();
Send_ACK();
readByte[5]= I2C_ReadByte();
//发送 NACK 信号
SendNot_Ack();
I2C_Stop();
//温湿度的二进制数据处理
//0x68 = 0110 1000
//0x08 = 0000 1000
if( (readByte[0] & 0x68) == 0x08 )
{
H1 = readByte[1];
//H1 左移 8 位并与 readByte[2] 相或
H1 = (H1<<8) | readByte[2];
H1 = (H1<<8) | readByte[3];
//H1 右移 4 位
H1 = H1>>4;
H1 = (H1*1000)/1024/1024;
T1 = readByte[3];
//与运算
T1 = T1 & 0x0000000F;
T1 = (T1<<8) | readByte[4];
T1 = (T1<<8) | readByte[5];
T1 = (T1*2000)/1024/1024 - 500;
AHT20_OutData[0] = (H1>>8) & 0x000000FF;
AHT20_OutData[1] = H1 & 0x000000FF;
AHT20_OutData[2] = (T1>>8) & 0x000000FF;
AHT20_OutData[3] = T1 & 0x000000FF;
}
else
{
AHT20_OutData[0] = 0xFF;
AHT20_OutData[1] = 0xFF;
AHT20_OutData[2] = 0xFF;
AHT20_OutData[3] = 0xFF;
printf("ê§°üá?");
}
printf("完成!\n");
printf("----温度:%d%d.%d °C\n",T1/100,(T1/10)%10,T1%10);
printf("----湿度:%d%d.%d %%",H1/100,(H1/10)%10,H1%10);
printf("\n\n");
}
//接收 ACK 信号
uint8_t Receive_ACK(void)
{
uint8_t result=0;
uint8_t cnt=0;
//置 SCL 低电平
IIC_SCL = 0;
//设置 SDA 为读取数据模式
SDA_IN();
delay_us(4);
//置 SCL 高电平
IIC_SCL = 1;
delay_us(4);
//等待从机发送 ACK 信号,等待时间为 100 个循环
while(READ_SDA && (cnt<100))
{
cnt++;
}
IIC_SCL = 0;
delay_us(4);
//如果在等待时间内,则结果为 1
if(cnt<100)
{
result=1;
}
return result;
}
//发送 ACK 信号
void Send_ACK(void)
{
//设置 SDA 为写数据模式
SDA_OUT();
IIC_SCL = 0;
delay_us(4);
//置 SDA 为低电平
IIC_SDA = 0;
delay_us(4);
IIC_SCL = 1;
delay_us(4);
IIC_SCL = 0;
delay_us(4);
SDA_IN();
}
//发送 NACK 信号
void SendNot_Ack(void)
{
//设置 SDA 为写数据模式
SDA_OUT();
IIC_SCL = 0;
delay_us(4);
IIC_SDA = 1;
delay_us(4);
IIC_SCL = 1;
delay_us(4);
IIC_SCL = 0;
delay_us(4);
IIC_SDA = 0;
delay_us(4);
}
//发送一个字节数据
void I2C_WriteByte(uint8_t input)
{
uint8_t i;
//设置 SDA 为写数据模式
SDA_OUT();
//循环左移发送 8 bit数据
for(i=0; i<8; i++)
{
IIC_SCL = 0;
delay_ms(5);
if(input & 0x80)
{
IIC_SDA = 1;
}
else
{
IIC_SDA = 0;
}
IIC_SCL = 1;
delay_ms(5);
input = (input<<1);
}
IIC_SCL = 0;
delay_us(4);
SDA_IN();
delay_us(4);
}
//循环检测 SDA 的电平状态并存储起来
uint8_t I2C_ReadByte(void)
{
uint8_t resultByte=0;
uint8_t i=0, a=0;
IIC_SCL = 0;
SDA_IN();
delay_ms(4);
//循环检测
for(i=0; i<8; i++)
{
IIC_SCL = 1;
delay_ms(3);
a=0;
if(READ_SDA)
{
a=1;
}
else
{
a=0;
}
resultByte = (resultByte << 1) | a;
IIC_SCL = 0;
delay_ms(3);
}
SDA_IN();
delay_ms(10);
return resultByte;
}
//设置 I2C 协议开始
void I2C_Start(void)
{
SDA_OUT();
IIC_SCL = 1;
delay_ms(4);
//SDA 从 1 跳变为 0 的这个过程
//表示起始信号
IIC_SDA = 1;
delay_ms(4);
IIC_SDA = 0;
delay_ms(4);
//SCL 变为 0
//表示 SDA 数据无效,此时 SDA 可以进行电平切换
IIC_SCL = 0;
delay_ms(4);
}
//设置 I2C 协议停止
void I2C_Stop(void)
{
SDA_OUT();
//SCL 高电平,SDA 高电平
//停止时序
IIC_SDA = 0;
delay_ms(4);
IIC_SCL = 1;
delay_ms(4);
//SDA 切换到高电平
IIC_SDA = 1;
delay_ms(4);
}
2.2 打开工程文件
2.3 编译加烧录
需要找到在output中生成的usart.hex文件
进行文件烧录
2.4 运行结果
可以看出在每隔两秒用AHT20采集一次温度和湿度 。
三、基于SPI的OLED显示
3.1 显示自己的学号和姓名
3.1.1 完整代码
链接:https://pan.baidu.com/s/1vawtPaudZmzuWdMjImrF3Q
提取码:qwer
3.1.2 修改代码
修改test.c中的TEST_MainPage函数中GUI_ShowString,GUI_ShowCHinese的参数
需要将对应的字模点阵加入到oledfont.h里
3.1.3 汉字取模点阵软件
链接:https://pan.baidu.com/s/1hoh1iYDoNSsJI9uaOsi68g
提取码:qwer
首先设置字模输出选项
不同的取模方式对应的十六进制码是不一样的
添加到oledfont.h
主程序:
main.c
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "gui.h"
#include "test.h"
int main(void)
{
delay_init(); //延时函数初始化
NVIC_Configuration(); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
OLED_Init(); //初始化OLED
OLED_Clear(0); //清屏(全黑)
while(1)
{
TEST_MainPage(); //主界面显示测试
}
}
3.1.4 编译烧录
3.1.5 运行结果
3.2 显示AHT20的温度和湿度
3.2.1 完整代码
链接:https://pan.baidu.com/s/1fwpkkrHpujE57SGZBOXlmQ
提取码:qwer
3.2.2 主要代码
main.c
#include "delay.h"
#include "usart.h"
#include "bsp_i2c.h"
#include "sys.h"
#include "oled.h"
#include "gui.h"
#include "test.h"
int main(void)
{
delay_init(); //延时函数初始化
uart_init(115200);
IIC_Init();
NVIC_Configuration(); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
OLED_Init(); //初始化OLED
OLED_Clear(0);
while(1)
{
//printf("温度湿度显示");
read_AHT20_once();
OLED_Clear(0);
delay_ms(1500);
}
}
主要是将温度采集通过串口发到OLED屏。
3.2.3编译烧录
3.2.4 运行结果
3.3 左右的滑动显示长字符
3.3.1 完整代码
链接:https://pan.baidu.com/s/1KhdrhXcBWLVsky-pVAXH_A
提取码:qwer
3.3.2 修改代码
main.c
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "gui.h"
#include "test.h"
int main(void)
{
delay_init(); //延时函数初始化
NVIC_Configuration(); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
OLED_Init(); //初始化OLED
OLED_Clear(0); //清屏(全黑)
OLED_WR_Byte(0x2E,OLED_CMD); //关闭滚动
OLED_WR_Byte(0x27,OLED_CMD); //水平向左或者右滚动 26/27
OLED_WR_Byte(0x00,OLED_CMD); //虚拟字节
OLED_WR_Byte(0x00,OLED_CMD); //起始页 0
OLED_WR_Byte(0x07,OLED_CMD); //滚动时间间隔
OLED_WR_Byte(0x07,OLED_CMD); //终止页 7
OLED_WR_Byte(0x00,OLED_CMD); //虚拟字节
OLED_WR_Byte(0xFF,OLED_CMD); //虚拟字节
TEST_MainPage();
OLED_WR_Byte(0x2F,OLED_CMD); //开启滚动
while(1)
{
}
}
继续添加字模数据
"我",0x04,0x40,0x0E,0x50,0x78,0x48,0x08,0x48,0x08,0x40,0xFF,0xFE,0x08,0x40,0x08,0x44,
0x0A,0x44,0x0C,0x48,0x18,0x30,0x68,0x22,0x08,0x52,0x08,0x8A,0x2B,0x06,0x10,0x02,/*"我",0*/
"们",0x09,0x00,0x08,0x80,0x08,0xBC,0x12,0x04,0x12,0x04,0x32,0x04,0x32,0x04,0x52,0x04,
0x92,0x04,0x12,0x04,0x12,0x04,0x12,0x04,0x12,0x04,0x12,0x04,0x12,0x14,0x12,0x08,/*"们",1*/
"的",0x10,0x40,0x10,0x40,0x20,0x40,0x7E,0x7C,0x42,0x84,0x42,0x84,0x43,0x04,0x42,0x44,
0x7E,0x24,0x42,0x24,0x42,0x04,0x42,0x04,0x42,0x04,0x7E,0x04,0x42,0x28,0x00,0x10,/*"的",2*/
"爱",0x00,0x08,0x01,0xFC,0x7E,0x10,0x22,0x10,0x11,0x20,0x7F,0xFE,0x42,0x02,0x82,0x04,
0x7F,0xF8,0x04,0x00,0x07,0xF0,0x0A,0x10,0x11,0x20,0x20,0xC0,0x43,0x30,0x1C,0x0E,/*"爱",3*/
"情",0x10,0x40,0x10,0x40,0x17,0xFC,0x10,0x40,0x1B,0xF8,0x54,0x40,0x57,0xFE,0x50,0x00,
0x93,0xF8,0x12,0x08,0x13,0xF8,0x12,0x08,0x13,0xF8,0x12,0x08,0x12,0x28,0x12,0x10,/*"情",4*/
"到",0x00,0x04,0xFF,0x84,0x08,0x04,0x10,0x24,0x22,0x24,0x41,0x24,0xFF,0xA4,0x08,0xA4,
0x08,0x24,0x08,0x24,0x7F,0x24,0x08,0x24,0x08,0x04,0x0F,0x84,0xF8,0x14,0x40,0x08,/*"到",5*/
"这",0x00,0x80,0x20,0x40,0x10,0x40,0x17,0xFC,0x00,0x10,0x02,0x10,0xF1,0x20,0x10,0xA0,
0x10,0x40,0x10,0xA0,0x11,0x10,0x12,0x08,0x14,0x08,0x28,0x00,0x47,0xFE,0x00,0x00,/*"这",6*/
"刚",0x00,0x04,0x7F,0x04,0x41,0x04,0x41,0x24,0x65,0x24,0x55,0x24,0x55,0x24,0x49,0x24,
0x49,0x24,0x55,0x24,0x55,0x24,0x65,0x24,0x41,0x04,0x41,0x04,0x45,0x14,0x42,0x08,/*"刚",7*/
"好",0x10,0x00,0x10,0xFC,0x10,0x04,0x10,0x08,0xFC,0x10,0x24,0x20,0x24,0x20,0x25,0xFE,
0x24,0x20,0x48,0x20,0x28,0x20,0x10,0x20,0x28,0x20,0x44,0x20,0x84,0xA0,0x00,0x40,/*"好",9*/
3.3.3 编译烧录
3.3.4 运行结果
这里的闪烁不是屏的原因,这是由于手机摄像头的分辨率问题。
四、逻辑分析仪分析协议
基本使用:
在Analyzers下可选择需要查看的协议。
点击开始仿真,逻辑分析仪可支持无硬件仿真,如果插入了硬件,这个按键会变成START
左边的通道显示了此协议的通道。
更多操作可参考PDF
链接:https://pan.baidu.com/s/1_CASAWFFfdoRgZdAEOWYgQ
提取码:qwer
4.1 I2C协议
由图中绿色小点代表信号开始采集,红色代表结束
当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA线由低电平向高电平切换,表示通讯的停止。
对于I2C协议前面有所提到,当SCL为高电平时,SDA才有作用, 每一个上升沿读取一个bit,则第一个波形对应的二进制信号为01000001,而最后一个是I2C协议的一个应答信号,当SDA为低电平时表示ACK希望继续接收信号,当SDA为高电平表示NAK表示不接收信号了。
代码分析:
首先初始化SCL和SDA都为高电平,那么在SDA从高电平跳变到低电平时表示开始接收。
开始接收,SDA从1到0
该函数功能即将SCL为高电平时,存储SDA的电平值。
这两个函数定义了ACK和NAK应答信号的发送。
4.2 SPI协议
当CH3即SS为低电平时SPI协议开始工作,而当CH2即时钟信号在上升沿则MOSI和MISO同步开始发送和采集信号,由图中可读出第一个信号的二进制码为MOSI:10000100以及MISO:10000101。
代码解析:
该函数是对SPI协议的一个初始化
该函数是SPI工作时的操作函数。
4.3 Async Serial 串口协议
串口是通过十六进制一步一步传输信号比特 。
五、心得
基于对I2C和SPI协议的学习,我明白了这些芯片底层的逻辑,并且通过逻辑分析仪更加深层次的理解了协议的工作原理,此次实验使得自己的动手能力有所提高,也明白了点阵格式转换成十六进制格式。此次项目的代码是比较多的,需要仔细的去理解每一个模块的函数的使用和意义。
参考文献:
基于 I2C 协议使用 AHT20 温湿度传感器采集数据_ssj925319的博客-优快云博客
基于STM32的0.96寸OLED显示屏显示数据_Harriet的博客-优快云博客_基于stm32的oled显示时间