文章目录
实验目的
学习I2C总线通信协议,使用STM32F103完成基于I2C协议的AHT20温湿度传感器的数据采集,并将采集的温度-湿度值通过串口输出。
具体任务:
1)解释什么是“软件I2C”和“硬件I2C”? (阅读野火配套教材的第23章“I2C–读写EEPROM”原理章节)
2)阅读AHT20数据手册,编程实现:每隔2秒钟采集一次温湿度数据,并通过串口发送到上位机(win10)。
实验材料
软件
- KEIL5
- FlyMcu
- FireTools串口助手
硬件
- STM32F103C8T6最小开发板
- AHT20温湿度传感器
- CH340模块
- 面包板一块
- 杜邦线若干
AHT20
AHT20是奥松电子生产的,一种基于IIC协议的温湿度传感器。单片机通过给AHT20通过IIC协议发送指令,可以从AHT20读取温湿度数据到单片机。
我在本实验中使用的芯片的引脚图如下:
根据引脚定义进行连接,按规定编写代码给传感器发送指令,读取温湿度数据。
实验原理
什么是I2C
I2C 通讯协议(Inter-Integrated Circuit)是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。
I2C可以分成物理层与协议层。
物理层
I2C是一个支持设备的总线。可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。对于I2C 总线,只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。
连接到总线上的设备在输出时使用开漏输出的模式。当总线上没有设备发数据时,大家输出高阻态,总线被上拉电阻设置为高电平。当有设备发送数据时,如果发送1,设备输出高阻态,接收设备与上拉电阻相连得到1;如果输出0,由于上拉电阻把总线与电源隔开,整条总线被设置成低电平。
逻辑层
首先主机向总线发送开始信号,发送之后再发送从机地址(7位或10位)+读或写(0或1),对应的从机再给主机发送确认信号。
如果是主机发数据,主机会发送8位数据到从机,从机发送确认信号,主机再接着发下8位数据。发送完毕后,主机发送停止信号,传输结束。
如果是主机读数据,从机会发送8位数据给主机,主机收到后发送确认信号,接着收下8位数据。主机可以发送非确认信号停止接收。发送非停止信号后,主机再发送结束信号。
I2C常使用复合模式,有两次起始信号。主机找到从机后,再发送传感器里面的寄存器地址,再进行读写。
野火的资料里面有图片读写过程的示意图:
硬件I2C与软件I2C
硬件I2C是直接利用 STM32 芯片中的硬件 I2C 外设,在初始化好 I2C 外设后,只需要把某寄存器位置 1,此时外设就会控制对应的 SCL 及 SDA 线自动产生 I2C 起始信号,不需要内核直接控制引脚的电平。
软件I2C直接使用 CPU 内核按照 I2C 协议的要求控制 GPIO 输出高低电平,从而模拟I2C。
本实验中,使用I2C读写寄存器的具体实现已经被官方写成函数给我们使用,我们仅需要根据示例代码,按自己需要进行调用即可。但是想要分析它的源码,就必须先了解I2C协议。
项目制作
制作一个标准库项目
制作标准库项目过程较为复杂,野火的资料库提供了项目模板。我也写好了一个模板在这个链接里面,可以下载好后根据文本文件里面的提示进行构建。
导入AHT20模块与USART配置
构建好标准库项目后,使用空main函数编译无误后,进行这一步。
由于本实验实现的需求是使用AHT20模块采集温湿度数据,并将采集到的数据发送给上位机显示。因此要使用AHT20相关的代码以及配置串口。
对于AHT20的代码,可以下载官方的文档。点击这里下载。
注意,下载到的代码有可能会有bug,下载好代码后,进入.c文件查看Init_I2C_Sensor_Port(void)函数,看SDA对应的GPIO口以及是否为开漏输出。
检查IIC的GPIO口配置正确后,剪切AHT20-21_DEMO_V1_3.c文件中的示例主函数到main.c中。
接着进行USART配置。具体的配置过程不再赘述,我贴出自己写好的模块化代码,可以分别复制到头文件与源文件中导入项目。
我的代码仅做了对USART1的配置:
//usart.h
#ifndef __PSD_USART_H__
#define __PSD_USART_H__
#include "stm32f10x.h"
void Usart_1_Config();//usart1初始配置
void Usart_SendByte(USART_TypeDef *pUSARTx, uint8_t ch);//发送字符
void Usart_SendString(USART_TypeDef *pUSARTx, char *ch);//发送字符串
void Usart_SendHalfWord(USART_TypeDef *pUSARTx, uint16_t ch);//发送无符号16位数据
void Usart_SendWord(USART_TypeDef *pUSARTx, uint32_t ch);//发送无符号32位数据
void Usart_SendNum_Uint8(USART_TypeDef *pUSARTx, uint8_t num);//发送Uint8类型数字
void Usart_SendNum_Uint16(USART_TypeDef *pUSARTx, uint16_t num);//发送Uint16类型数字
void Usart_SendNum_Uint32(USART_TypeDef *pUSARTx, uint32_t num);//发送Uint32类型数字
void Usart_SendNum_Int8(USART_TypeDef *pUSARTx, int8_t num);//发送int8类型数字
void Usart_SendNum_Int16(USART_TypeDef *pUSARTx, int16_t num);//发送int16类型数字
void Usart_SendNum_Int32(USART_TypeDef *pUSARTx, int32_t num);//发送int32类型数字
void Usart_SendNum_Double(USART_TypeDef *pUSARTx, double num);//发送double类型数字
#endif
//usart.c
#include "psd_usart.h"
#include "math.h"
static void NVIC_Configuration(){
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_Init(&NVIC_InitStruct);
}
void Usart_1_Config(){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//打开串口GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//打开串口时钟
//配置RX,TX的GPIO口
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; //将TX配置为浮空输入模式
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; //将RX配置为复用推挽输出模式
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//配置USART1
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Rx|USART_Mode_Tx;
USART_Init(USART1,&USART_InitStruct);
USART_Cmd(USART1,ENABLE);
NVIC_Configuration();
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}
void Usart_SendByte(USART_TypeDef *pUSARTx, uint8_t ch){
USART_SendData(pUSARTx,ch);
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE)==RESET);
}
void Usart_SendString(USART_TypeDef *pUSARTx, char *ch){
unsigned int k = 0;
do{
Usart_SendByte(pUSARTx,*(ch+k));
k++;
}while(*(ch+k)!='\0');
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE)==RESET);
}
void Usart_SendHalfWord(USART_TypeDef *pUSARTx, uint16_t ch){
uint8_t temp_h, temp_l;
temp_h = (ch&0xFF00)>>8;
temp_l = ch&0xff;
Usart_SendByte(pUSARTx, temp_h);
Usart_SendByte(pUSARTx, temp_l);
}
void Usart_SendWord(USART_TypeDef *pUSARTx, uint32_t ch){
uint8_t temp_h, temp_l;
temp_h = (ch&0xFFFF0000)>>16;
temp_l = ch&0xffff;
Usart_SendHalfWord(pUSARTx, temp_h);
Usart_SendHalfWord(pUSARTx, temp_l);
}
void Usart_SendNum_Uint8(USART_TypeDef *pUSARTx, uint8_t num){
if (num == 0){
Usart_SendByte(pUSARTx,'0');
return;
}
char dat[4];
dat[3] = '\0';
uint8_t i = 0;
while(num>0){
uint8_t n = num%10;
if(n==0) dat[2-i] = '0';
else if(n==1) dat[2-i] = '1';
else if(n==2) dat[2-i] = '2';
else if(n==3) dat[2-i] = '3';
else if(n==4) dat[2-i] = '4';
else if(n==5) dat[2-i] = '5';
else if(n==6) dat[2-i] = '6';
else if(n==7) dat[2-i] = '7';
else if(n==8) dat[2-i] = '8';
else if(n==9) dat[2-i] = '9';
i++;
num/=10;
}
uint8_t a = 3-i;
Usart_SendString(pUSARTx,&dat[a]);
}
void Usart_SendNum_Uint16(USART_TypeDef *pUSARTx, uint16_t num){
if (num == 0){
Usart_SendByte(pUSARTx,'0');
return;
}
char dat[6];
dat[5] = '\0';
uint8_t i = 0;
while(num>0){
uint8_t n = num%10;
if(n==0) dat[4-i] = '0';
else if(n==1) dat[4-i] = '1';
else if(n==2) dat[4-i] = '2';
else if(n==3) dat[4-i] = '3';
else if(n==4) dat[4-i] = '4';
else if(n==5) dat[4-i] = '5';
else if(n==6) dat[4-i] = '6';
else if(n==7) dat[4-i] = '7';
else if(n==8) dat[4-i] = '8';
else if(n==9) dat[4-i] = '9';
i++;
num/=10;
}
uint8_t a = 5-i;
Usart_SendString(pUSARTx,&dat[a]);
}
void Usart_SendNum_Uint32(USART_TypeDef *pUSARTx, uint32_t num){
if (num == 0){
Usart_SendByte(pUSARTx,'0');
return;
}
char dat[11];
dat[10] = '\0';
uint8_t i = 0;
while(num>0){
uint8_t n = num%10;
if(n==0) dat[9-i] = '0';
else if(n==1) dat[9-i] = '1';
else if(n==2) dat[9-i] = '2';
else if(n==3) dat[9-i] = '3';
else if(n==4) dat[9-i] = '4';
else if(n==5) dat[9-i] = '5';
else if(n==6) dat[9-i] = '6';
else if(n==7) dat[9-i] = '7';
else if(n==8) dat[9-i] = '8';
else if(n==9) dat[9-i] = '9';
i++;
num/=10;
}
uint8_t a = 10-i;
Usart_SendString(pUSARTx,&dat[a]);
}
void Usart_SendNum_Double(USART_TypeDef *pUSARTx, double num){
int32_t num1 = floor(num);
int32_t num2 = floor((num-num1)*1000);
Usart_SendNum_Int32(pUSARTx,num1);
Usart_SendByte(pUSARTx,'.');
Usart_SendNum_Int32(pUSARTx,num2);
}
void Usart_SendNum_Int8(USART_TypeDef *pUSARTx, int8_t num){
if(num>=0) Usart_SendNum_Uint8(pUSARTx,num);
else{
if(num == -128){
Usart_SendString(pUSARTx,"-128");
}
else{
num = 0 - num;
Usart_SendByte(pUSARTx,'-');
Usart_SendNum_Uint8(pUSARTx,num);
}
}
}
void Usart_SendNum_Int16(USART_TypeDef *pUSARTx, int16_t num){
if(num>=0) Usart_SendNum_Uint16(pUSARTx,num);
else{
if(num == -32768){
Usart_SendString(pUSARTx,"-32768");
}
else{
num = 0 - num;
Usart_SendByte(pUSARTx,'-');
Usart_SendNum_Uint16(pUSARTx,num);
}
}
}
void Usart_SendNum_Int32(USART_TypeDef *pUSARTx, int32_t num){
if(num>=0) Usart_SendNum_Uint32(pUSARTx,num);
else{
if(num == -2147483648){
Usart_SendString(pUSARTx,"-2147483648");
}
else{
num = 0 - num;
Usart_SendByte(pUSARTx,'-');
Usart_SendNum_Uint32(pUSARTx,num);
}
}
}
修改main函数
官方的实例代码已经读出了放大10倍的温湿度数据,在main.c函数中找到对应的位置,在读到的数据后面使用串口通信代码,将温湿度除以10之后将数据发送给串口助手。
代码如下:
int32_t main(void)
{
Usart_1_Config();//配置Usart1
uint32_t CT_data[2];
volatile int c1,t1;
/***********************************************************************************/
/**///上电初始化SDA,SCL的IO口
/***********************************************************************************/
Init_I2C_Sensor_Port();
/***********************************************************************************/
/**///①刚上电,产品芯片内部就绪需要时间,延时100~500ms,建议500ms
/***********************************************************************************/
Delay_1ms(500);
/***********************************************************************************/
/**///②上电第一次发0x71读取状态字,判断状态字是否为0x18,如果不是0x18,进行寄存器初始化
/***********************************************************************************/
if((AHT20_Read_Status()&0x18)!=0x18)
{
AHT20_Start_Init(); //重新初始化寄存器
Delay_1ms(10);
}
/***********************************************************************************/
/**///③根据客户自己需求发测量命令读取温湿度数据,当前while(1)循环发测量命令读取温湿度数据,仅供参考
/***********************************************************************************/
while(1)
{
AHT20_Read_CTdata_crc(CT_data);
if(CT_data[0]==0xff&&CT_data[1]==0xff){
Usart_SendString(USART1,"CRC检验未通过!");
}
else{
c1 = CT_data[0]*100*10/1024/1024; //计算得到湿度值c1(放大了10倍)
t1 = CT_data[1]*200*10/1024/1024-500;//计算得到温度值t1(放大了10倍)
//下一步客户处理显示数据,
Usart_SendString(USART1,"温度:");
Usart_SendNum_Double(USART1,t1/10.0,1);
Usart_SendByte(USART1,'\n');
Usart_SendString(USART1,"湿度:");
Usart_SendNum_Double(USART1,c1/10.0,1);
Usart_SendByte(USART1,'\n');
Usart_SendString(USART1,"数据通过检验!\n");
Usart_SendByte(USART1,'\n');
}
Delay_1ms(2000);
}
}
实验现象
查看SDA,SCL对应的GPIO口,将AHT20芯片的VCC,SDA,GND,SCL四个引脚与芯片对应的引脚在面包板上连接好,连接好硬件后烧录程序到芯片中,打开串口助手,发现接收到温度与湿度数据。
根据上图的函数查看自己下的代码使用的哪个GPIO口。
总结
本实验实现了使用AHT20传感器收集温湿度数据,并发送到串口助手。涉及到I2C与Usart配置。为了完成这个实验,我将Usart模块化,制作了输出中文,浮点数,数字的函数,巩固了之前所学习的知识。此外还通过AHT20的官方例子学习了I2C的应用。
参考资料
-
《零死角玩转STM32——F103指南者》
-
《AHT20产品手册》