目录
前言:
IIC协议简介:
1、起始信号和停止信号:
2、应答信号:
3、读写字节:
AT24C02:
字节写操作:
页写操作:
读操作:
MCP4017:
写操作:
读操作:
本篇文章主要介绍IIC通信协议,同时给大家介绍一下蓝桥杯嵌入式的模块的AT24C02和MCP4017,此外本篇博客会采用按键控制PB14来读取可编程电阻MCP分的电压值,并将电压值存储在AT24C02中。
I2C(IIC,Inter-Integrated Circuit), 一种半双工通信协议,采用两线式串行总线, 它是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据,这两条线必须通过上拉电阻连接正电源。数据传输只能在总线不忙时启动。在CPU与被控IC之间、IC与IC之间进行双向传送,高速IIC总线一般可达400kbps以上。
对于STM32来说,G4系列的芯片自带3个硬件IIC,而对于我们的比赛来说,官方为我们提供的代码采用的不是硬件上的IIC,而是使用PB6 和 PB7这两个IO口来模拟IIC的进程。接下来我会结合蓝桥杯嵌入式为大家提供的参考代码来为大家讲解一下IIC通信的一些操作。
下图是IIC起始和停止时序图,起始和停止均由主机来发出,IIC总线在主机发出起始信号后处于忙碌状态,而在主机发出停止信号后,总线处于空闲状态(I2C总线总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高)。
起始条件:在SCL保持为高电平的情况下,SDA出现一个下降沿,对应代码部分为:
void I2CStart(void)
{
SDA_Output(1);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SDA_Output(0);
delay1(DELAY_TIME);
SCL_Output(0);
delay1(DELAY_TIME);
}
停止条件:在SCL保持为高电平的情况下,SDA出现一个上升沿,对应代码部分为:
void I2CStop(void)
{
SCL_Output(0);
delay1(DELAY_TIME);
SDA_Output(0);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SDA_Output(1);
delay1(DELAY_TIME);
}
每当主机向从机发送完一个字节的数据,主机总是需要等待从机发出一个应答信号,以检验从机是否成功接收到了数据。
下面就是应答时序图,只有在DATA OUT 被置为低的时候,才会被认为是从机产生应答,否则被认为是不应答。
等待从机发送应答对应代码部分:
unsigned char I2CWaitAck(void)
{
unsigned short cErrTime = 5;
SDA_Input_Mode();
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
while(SDA_Input())
{
cErrTime--;
delay1(DELAY_TIME);
if (0 == cErrTime)
{
SDA_Output_Mode();
I2CStop();
return ERROR;
}
}
SDA_Output_Mode();
SCL_Output(0);
delay1(DELAY_TIME);
return SUCCESS;
}
主从机发送应答或者非应答对应代码:
/**
* @brief I2C发送确认信号
* @param None
* @retval None
*/
void I2CSendAck(void)
{
SDA_Output(0);
delay1(DELAY_TIME);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SCL_Output(0);
delay1(DELAY_TIME);
}
/**
* @brief I2C发送非确认信号
* @param None
* @retval None
*/
void I2CSendNotAck(void)
{
SDA_Output(1);
delay1(DELAY_TIME);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SCL_Output(0);
delay1(DELAY_TIME);
}
在介绍读写字节前,我要给大家强调一点,数据传输过程中,数据传输保持稳定(在SCL高电平期间,SDA一直保持稳定,没有跳变),只有当SCL被拉低后,SDA才能被改变在SCL为高电平期间(有效数据时间段),发送数据,发送8次数据,如果数据为1,显然SDA是被拉高;如果数据为0,那么SDA被拉低
对于IIC协议来说,传输到SDA上的数据必须是八位,在数据传输的时候,先传输数据的最高位,再传输数据的最低位,也就是嵌入式工程师所说的高位先行(MSB),每当有一个字节的数据发送后,必须发送一位应答位,总的数据就是8位数据加1位应答,也就是说一帧有9位数据。
读写操作对应的代码部分:
/**
* @brief I2C发送一个字节
* @param cSendByte 需要发送的字节
* @retval None
*/
void I2CSendByte(unsigned char cSendByte)
{
unsigned char i = 8;
while (i--)
{
SCL_Output(0);
delay1(DELAY_TIME);
SDA_Output(cSendByte & 0x80);
delay1(DELAY_TIME);
cSendByte += cSendByte;
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
}
SCL_Output(0);
delay1(DELAY_TIME);
}
/**
* @brief I2C接收一个字节
* @param None
* @retval 接收到的字节
*/
unsigned char I2CReceiveByte(void)
{
unsigned char i = 8;
unsigned char cR_Byte = 0;
SDA_Input_Mode();
while (i--)
{
cR_Byte += cR_Byte;
SCL_Output(0);
delay1(DELAY_TIME);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
cR_Byte |= SDA_Input();
}
SCL_Output(0);
delay1(DELAY_TIME);
SDA_Output_Mode();
return cR_Byte;
}
这里我要给大家解释一点代码,SDA_Output(cSendByte & 0x80);
函数每次接收过来的都是一个字节,而通过&0x80就会保留最高位的数据,舍弃后七位的数据,也就是说,通过这个操作之后,就只有1位数据被保存了下来,然后通过cSendByte += cSendByte;将这一位数据进行加法计算,这可以看作是这一位数据的左移运算。同样的道理也适用于cR_Byte += cR_Byte;cR_Byte |= SDA_Input();这两句代码。
AT24C02是一款2K容量的串行电可擦除只读存储器,内部包含256个字节,每个字节8位。
在官方给出的原理图上,AT24C02和MCP4017被挂载在了PB6 PB7两个接口上,也就以为着,AT24C02是做为从机来跟单片机进行通信的。这就要求我们去看一下数据手册,找到其从机地址。
手册中的地址前4位是固定不变的,后3位是可以根据电平的高低来决定,最后一位由开发者决定是对其进行读操作还是写操作来决定最后一位的数据是0还是1。而开发板将A2 A1 A0全部接地,这就决定了我们如果用读操作就是0xA1,写操作就是0xA0;
字节写操作需要由单片机发出起始状态和器件地址,紧跟着给出一个8位数据地址。一经收到该地址,AT24C02就立刻发送应答,并随时钟输入8位数据。在收到8位数据之后,AT24C02再次发送应答,单片机发送停止信号来终止写操作。同时,这里要注意的是AT24C02每次写完内容后会自动指向下一个内存空间。但是这里一定要延时一下,这样才能保证写入的正确
AT24C02写操作对应代码部分:
void EEPROM_write(unsigned char address,unsigned char data)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(address);
I2CWaitAck();
I2CSendByte(data);
I2CWaitAck();
I2CStop();//停止IIC
HAL_Delay(5);
}
AT24C02能进行8字节页面写入,04、08和16系列设备能进行16字节页面写入。激发写页面与激发写字节相同,只是数据传送设备无须在第一个字节随时钟输入之后,发出一个停止状态。在EEPROM确认收到第一个数据之后,数据传送设备能再传送7个(1KB、2KB)或15个(4KB、8KB、16KB)数据,每一个数据收到之后,EEPROM都将通过SDA回送一个确认信号,最后数据传送设备必须通过停止状态终止页面写序列。
页写操作对应代码部分:
void EEPROM_pagewrite(unsigned char *pageBuf,unsigned char address,unsigned char num)
{
I2CStart();
I2CSendByte(0xa0); // 器件地址
I2CWaitAck();
I2CSendByte(address); // 写数据地址
I2CWaitAck();
while(num--)
{
I2CSendByte(*pageBuf++);
I2CWaitAck();
}
I2CStop();
delay1(500);
}
读取操作分为三种,分别是当前地址读取,一个是随机读取,另一个是顺序读取。这里给大家介绍一下随机读取,随机读取的时序图如下所示。
先由主机发送一个起始信号,紧接着再发送器件地址(0xa0),等待从机响应,然后发送读取的地址,等待从机响应,之后再次发送起始信号,这时候发送的器件地址就是0xa1了,等待从机回应之后,然后才开始数据读取,每次进行数据读取后,需要进行等待响应操作,如果收到的响应是不响应,那么这时候发送停止信号,代表着读取结束。
unsigned char EEPROM_read(unsigned char address)
{
unsigned char dat;
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(address);
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
dat = I2CReceiveByte();
I2CWaitAck();
I2CStop();
return dat;
}
MCP4017是一款可编程的电阻,其内置了7位寄存器,共计127个档位的分辨率。在蓝桥杯嵌入式比赛中,MCP4017同样放在了PB6 PB7 两个IO上,我们可以用IIC总线来跟他通信。
对于蓝桥杯嵌入式比赛来说,采用的是MCP4017T-104ELT,也就是说他的最大电阻达到了100K。
通过这张图,我们可以清晰的看出,随着我们向MCP中输入的数越大,他对应的电阻也就越大,当我们传入0x7f时,对应的电阻就是100K。这里要注意的一点是,我们写进去的一个数字(0-127),读出来也是一个数字,转化为电阻阻值:R = 787.4 * read_resistor 欧,电压:3.3*(R/(R+10)) (假设外接的电压为3.3)
通过阅读数据手册,我们可以得到MCP4017的从机地址,如果是读操作的话就是0x5f,写操作的话就是0X5E。接下来,我们根据时序图来编写MCP的读写操作。
这个就比AT24C02好看多了,先发送起始信号,等待响应,发送数据,等待响应,结束信号,一个写操作就结束了。对应代码部分如下:
void MCP4017_write(unsigned char value)
{
I2CStart();
I2CSendByte(0x5e);
I2CWaitAck();
I2CSendByte(value);
I2CWaitAck();
I2CStop();
}
这里需要注意的一点是主设备负责发起响应和响应信号。如果出现响应信号,MCP4017将中止此传输并释放总线。
读取操作对应的代码部分:
unsigned char MCP4017_read(void)
{
unsigned char value;
I2CStart();
I2CSendByte(0x5f);
I2CWaitAck();
value = I2CReceiveByte();
I2CSendNotAck();
I2CStop();
return value;
}
写好的按键工程 提取码:2471
大家可以在我这个按键工程的基础上进行修改。
通过开发板上的B2 B3 B4个按键,来分别控制MCP4017不同的电阻值,同时利用B1来显示上一次存储到AT24C02中的电压。
1、将PB6 PB7设定为推挽输出
2、设定PB14为ADC通道,并完成相关配置
Resolution:ADC采样的分辨率这里直接默认选择12位的精度就可以了,如输入电压为0-3.3V,12位,即0V对应0,3.3V对应2^12-1=4095,通过这个转换我们就可以算出对应的电压值。
Rank:采样间隔设置我们这里选择默认2.5就行了,间隔越小采样频率越高。 之后,咱们的Cubemx的配置就算基本完成了,点击generate生成代码即可。不过要记得把官方提供的i2c_hal.c和i2c_hal.h加入我们的工程。
打开工程后,在i2c_hal.c里面添加AT24C02和MCP4017的代码,并且在.h文件中声明一下
unsigned int adc_val;
float vol;
void Get_vol()
{
HAL_ADC_Start(&hadc1);
adc_val = HAL_ADC_GetValue(&hadc1);
vol = adc_val/4096.0f * 3.3f;
}
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* © Copyright (c) 2022 STMicroelectronics.
* All rights reserved.
*
* This software component is licensed by ST under BSD 3-Clause license,
* the "License"; You may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
* opensource.org/licenses/BSD-3-Clause
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "adc.h"
#include "tim.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
#include "lcd.h"
//#include "fonts.h"
#include "interrupt.h"
#include "stdio.h"
#include "i2c_hal.h"
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
char text[20];
extern unsigned int TIM3_count;
extern struct keys key[4];
unsigned char TIM3_count_H;
unsigned char TIM3_count_L;
unsigned int EEPROM_temp;
unsigned char res_4017;
unsigned int PB14_ADC;
float volt1;
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM3_Init();
MX_ADC1_Init();
/* USER CODE BEGIN 2 */
LCD_Init();
LCD_Clear(Blue);
LCD_SetBackColor(Blue);
LCD_SetTextColor(White);
HAL_TIM_Base_Start_IT(&htim3);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_ADC_Start(&hadc1);
PB14_ADC = HAL_ADC_GetValue(&hadc1);
volt1 = PB14_ADC * 3.5/4096;
sprintf(text,"Now time_value:%d",TIM3_count);
LCD_DisplayStringLine(Line4 ,(unsigned char *)text);
EEPROM_temp = (EEPROM_read(0x00)<<8)+EEPROM_read(0x01);
sprintf(text,"EEPROM_temp:%d",EEPROM_temp);
LCD_DisplayStringLine(Line1 ,(unsigned char *)text);
sprintf(text,"RES %5.4fK",res_4017*0.7874);
LCD_DisplayStringLine(Line6,(uint8_t *)text);
sprintf(text,"Vol %5.4fV",3.3*res_4017*0.7874/(res_4017*0.7874 + 10));
LCD_DisplayStringLine(Line3, (unsigned char *)text);
sprintf(text,"ture %5.4fV",volt1);
// sprintf(text,"ture %d",PB14_ADC);
LCD_DisplayStringLine(Line7, (unsigned char *)text);
if(key[0].single_flag == 1)
{
sprintf(text,"key0down");
LCD_DisplayStringLine(Line8,(uint8_t *)text);
MCP4017_write(0x00);
res_4017 = MCP4017_read();
key[0].single_flag = 0;
}
else if(key[1].single_flag == 1)
{
sprintf(text,"key1down");
LCD_DisplayStringLine(Line8,(uint8_t *)text);
MCP4017_write(0x0d);
res_4017 = MCP4017_read();
key[1].single_flag = 0;
}
else if(key[2].single_flag == 1)
{
sprintf(text,"key2down");
LCD_DisplayStringLine(Line8,(uint8_t *)text);
key[2].single_flag = 0;
MCP4017_write(0x40);
res_4017 = MCP4017_read();
}
else if(key[3].single_flag == 1)
{
TIM3_count_H = TIM3_count >> 8;
TIM3_count_L = TIM3_count & 0xff;
EEPROM_write(0x00,TIM3_count_H);
HAL_Delay(10); //写入需要时间,延时是为了给写操作留出时间
EEPROM_write(0x01,TIM3_count_L);
HAL_Delay(10);
sprintf(text,"key3down");
MCP4017_write(0x7f);
res_4017 = MCP4017_read();
LCD_DisplayStringLine(Line8,(uint8_t *)text);
key[3].single_flag = 0;
}
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
/** Configure the main internal regulator output voltage
*/
HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = RCC_PLLM_DIV3;
RCC_OscInitStruct.PLL.PLLN = 20;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
/** Initializes the peripherals clocks
*/
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC12;
PeriphClkInit.Adc12ClockSelection = RCC_ADC12CLKSOURCE_SYSCLK;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
最终代码 提取码:2471