目录
一:UART
一:串口定义与分类
串口的定义:
串口(UART,全称Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)是一种串行通信接口,用于在计算机和外围设备之间或两个设备之间进行数据传输。串口通过逐位传输数据,通常采用两根信号线:一根用于发送数据(TXD),另一根用于接收数据(RXD)。与并行通信不同,串行通信按顺序传输每一位数据,这减少了所需的信号线数量,适用于长距离传输和简单的连接要求。
串口通信是一种设备间常用的串行通信方式,串口按位(bit)发送和接收字节。尽管比特 字节(byte)的串行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。 串口通信协议是指规定了数据包的内容,内容包含了起始位、主体数据、校验位及停止位,双 方需要约定一致的数据包格式才能正常收发数据的有关规范。在串口通信中,常用的协议包括 RS-232、RS-422 和 RS-485 等。 随着科技的发展,RS-232 在工业上还有广泛的使用,但是在商业技术上,已经慢慢的使用 USB 转串口取代了 RS-232 串口。我们只需要在电路中添加一个 USB 转串口芯片,就可以实现 USB 通信协议和标准 UART 串行通信协议的转换
串口的分类:
串口(UART,通用异步收发传输器)可以按多种方式分类。按数据传输方向分为单工(Simplex)、半双工(Half-Duplex)和全双工(Full-Duplex);按物理层协议分为RS-232、RS-422和RS-485等;按应用场景分为标准串口和虚拟串口。单工通信只能单方向传输数据,半双工通信可双向但不能同时传输,全双工通信可同时双向传输。不同物理层协议规定了不同的电气特性和最大传输距离,应用场景则决定了串口的实现方式,如PC机上的标准串口或通过USB转换的虚拟串口。
按数据传输方向分类
单工(Simplex):
- 只能单方向传输数据。
- 例如,数据只能从设备A发送到设备B,反之则不行。
半双工(Half-Duplex):
- 数据可以在两个方向上传输,但不能同时进行。
- 例如,对讲机通信,一个时刻只能一个方向传输数据。
全双工(Full-Duplex):
- 数据可以同时在两个方向上传输。
- 例如,电话通信,双方可以同时讲话和听到对方的声音。
按物理层协议分类
RS-232:
- 常用于短距离和低速率的通信。
- 典型应用包括电脑的串口通信。
- 最大传输距离约为15米。
RS-422:
- 支持更长距离和更高速度的通信。
- 常用于工业控制和仪表通信。
- 最大传输距离约为1200米。
RS-485:
- 适用于多点通信,支持32个设备同时连接。
- 常用于楼宇自动化和工业网络。
- 最大传输距离约为1200米。
按应用场景分类
标准串口:
- 直接集成在计算机或设备中的物理串口。
- 例如,老式电脑上的DB9串口接口。
虚拟串口:
- 通过软件模拟的串口,通常通过USB转串口实现。
- 例如,现代计算机上没有物理串口,但可以通过USB转串口线缆连接外部串口设备。
数据同步方式:
同步通信要求通信双方共用同一时钟信号,在总线上保持统一的时序和周期完成信息传输。
优点:可以实现高速率、大容量的数据传输,以及点对多点传输。
缺点:要求发送时钟和接收 时钟保持严格同步,收发双方时钟允许的误差较小,同时硬件复杂。
异步通信不需要时钟信号,而是在数据信号中加入开始位和停止位等一些同步信号,以便使接收端能够正确地将每一个字符接收下来,某些通信中还需要双方约定传输速率。
优点:没有时钟信号硬件简单,双方时钟可允许一定误差。
缺点:通信速率较低,只适用点对点传输。
通信速率:(了解即可)
在数字通信系统中,通信速率(传输速率)指数据在信道中传输的速度,它分为两种:传信率和传码率。
传信率:每秒钟传输的信息量,即每秒钟传输的二进制位数,单位为 bit/s(即比特每秒), 因而又称为比特率。
传码率:每秒钟传输的码元个数,单位为 Baud(即波特每秒),因而又称为波特率。
比特率和波特率这两个概念又常常被人们混淆。比特率很好理解,我们来看看波特率,波特率被传输的是码元,码元是信号被调制后的概念,每个码元都可以表示一定 bit 的数据信息量。举个例子,在 TTL 电平标准的通信中,用 0V 表示逻辑 0,5V 表示逻辑 1,这时候这个码元就可以表示两种状态。如果电平信号 0V、2V、4V 和 6V 分别表示二进制数 00、01、10、11, 这时候每一个码元就可以表示四种状态。 由上述可以看出,码元携带一定的比特信息,所以比特率和波特率也是有一定的关系的。
比特率和波特率的关系可以用以下式子表示: 比特率 = 波特率 * log2M 其中 M 表示码元承载的信息量。我们也可以理解 M 为码元的进制数。
举个例子:波特率为 100 Baud,即每秒传输 100 个码元,如果码元采用十六进制编码(即 M=2,代入上述式子),那么这时候的比特率就是 400 bit/s。如果码元采用二进制编码(即 M=2, 代入上述式子),那么这时候的比特率就是 100 bit/s。 可以看出采用二进制的时候,波特率和比特率数值上相等。但是这里要注意,它们的相等 只是数值相等,其意义上不同,看波特率和波特率单位就知道。由于我们的所用的数字系统都 是二进制的,所以有部分人久而久之就直接把波特率和比特率混淆了
波特率和数据帧格式(了解即可)
串口通信协议数据包组成可以分为波特率和数据帧格式两部分。
1. 波特率
本章主要讲解的是串口异步通信,异步通信是不需要时钟信号的,但是这里需要我们约定 好两个设备的波特率。波特率表示每秒钟传送的码元符号的个数,所以它决定了数据帧里面每 一个位的时间长度。两个要通信的设备的波特率一定要设置相同,我们常见的波特率是 4800、 9600、115200 等。
2. 数据帧格式
数据帧格式需要我们提前约定好,串口通信的数据帧包括起始位、停止位、有效数据位以 及校验位。
⚫ 起始位和停止位 串口通信的一个数据帧是从起始位开始,直到停止位。数据帧中的起始位是由一个逻辑 0 的数据位表示,而数据帧的停止位可以是 0.5、1、1.5 或 2 个逻辑 1 的数据位表示,只要双方约 定一致即可。
⚫ 有效数据位 数据帧的起始位之后,就接着是数据位,也称有效数据位,这就是我们真正需要的数据, 有效数据位通常会被约定为 5、6、7 或者 8 个位长。有效数据位是低位(LSB)在前,高位(MSB) 在后。
⚫ 校验位 校验位可以认为是一个特殊的数据位。校验位一般用来判断接收的数据位有无错误,检验 方法有:奇检验、偶检验、0 检验、1 检验以及无检验。
下面分别介绍一下: 奇校验是指有效数据为和校验位中“1”的个数为奇数,比如一个 8 位长的有效数据为: 10101001,总共有 4 个“1”,为达到奇校验效果,校验位设置为“1”,最后传输的数据是 8 位 的有效数据加上 1 位的校验位总共 9 位。 偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如数据帧:11001010,此时数据帧“1”的个数为 4 个,所以偶校验位为“0”。 0 校验是指不管有效数据中的内容是什么,校验位总为“0”,1 校验是校验位总为“1”。 无校验是指数据帧中不包含校验位。 我们一般是使用无校验的情况
二:STM32CubeMX中关于USART的配置
补充文章:
STM32CubeMX— 配置串口_cubemx配置串口-CSDN博客
stm32cubemx 串口配置_stm32cubemx配置nucleo-l476rg虚拟串口-CSDN博客
异步通讯
做调制解调器时可以使能
三:串口常用的函数:
1. 配置UART外设
在使用UART之前,需要对其进行配置。通常在main.c
文件中或使用STM32CubeMX工具生成初始化代码。
huart1.Instance = USART1;
:
- 这行代码指定
huart1
句柄指向的硬件外设实例是USART1
。
huart1.Init.BaudRate = 115200;
:
- 设置串口的波特率为 115200 bps。波特率是指每秒传输的比特数。
huart1.Init.WordLength = UART_WORDLENGTH_8B;
:
- 设置每个数据帧的长度为 8 位。常见的数据位长度有 7 位、8 位和 9 位。
huart1.Init.StopBits = UART_STOPBITS_1;
:
- 设置停止位的数量为 1 位。停止位用于标志一帧数据的结束。
huart1.Init.Parity = UART_PARITY_NONE;
:
- 设置无奇偶校验。奇偶校验是一种错误检测机制。
huart1.Init.Mode = UART_MODE_TX_RX;
:
- 设置 USART1 既用于发送(TX)也用于接收(RX)。其他模式包括仅发送或仅接收。
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
:
- 设置无硬件流控制。硬件流控制用于管理数据流,以避免缓冲区溢出。
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
:
- 设置过采样率为 16 倍。过采样率影响数据传输的准确性和抗干扰能力。
UART_HandleTypeDef huart1;
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
// Initialization Error
Error_Handler();
}
}
HAL库中串口发送的重要函数:
在发送的数据中,单片机不能做其他的事情
huart:指针 (代表一个外设)
pData:指针(代表发的内容在哪里【可能是个字节,数组等等】指向的是一个数据的缓冲区)
Size:几个字节
Timeout:差时,时间过了差时会自动结束函数
只要函数后面带IT就一定和中断相关
当发送完成时,会进入到一个发生中断
会调用一个
发送一半
例子:
非阻塞:
HAL库中串口接收的重要函数:
推荐使用非阻塞的
Size的单位是字节
例子:
变量指针,数组写数组名就好了
实训:
Cubemx操作
1.选择mode
2.
keil会生成一个名叫usart.c的文件,在这里面对串口进行了初始化:
UART_HandleTypeDef huart1;
/* USART1 init function */
void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 9600;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART1_Init 2 */
/* USER CODE END USART1_Init 2 */
}
串口函数:
1.回调函数:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance==USART1)
{
if(Rx_dat==0xa1)
{
LED1_ON();
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_SET);
HAL_UART_Transmit(&huart1,Tx_str2,sizeof(Tx_str1),10000); //阻塞式的串口发送数据
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_RESET); // led2是指示灯,在发送前亮,在发送后熄灭
HAL_UART_Receive_IT(&huart1,&Rx_dat,1); //接收到的字节,存放在Rx_dat中,字节数字为1个
}
else if(Rx_dat==0xa2)
{
LED1_OFF();
LED2_ON();
HAL_UART_Transmit(&huart1,Tx_str2,sizeof(Tx_str1),10000); //阻塞式的串口发送数据
LED2_OFF();
HAL_UART_Receive_IT(&huart1,&Rx_dat,1); //接收到的字节,存放在Rx_dat中,字节数字为1个
}
}
}
全部的回调函数:
// UART中断处理函数
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart) {
// 实现代码
}
// UART发送完成回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
// 实现代码
}
// UART发送完成一半回调函数
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart) {
// 实现代码
}
// UART接收完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 实现代码
}
// UART接收完成一半回调函数
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
// 实现代码
}
// UART错误回调函数
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) {
// 实现代码
}
// UART中断传输中止完成回调函数
void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart) {
// 实现代码
}
// UART中断发送中止完成回调函数
void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart) {
// 实现代码
}
// UART中断接收中止完成回调函数
void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart) {
// 实现代码
}
在典型的情况下,这些回调函数确实是与中断相关的。当UART模块接收到数据、发送数据完成、出现错误或者传输被中止时,这些回调函数会被调用。在许多情况下,这些回调函数是由中断触发的,以响应UART模块的状态变化。
2.接收和发送函数
/**
* @brief 通过UART发送数据
* @param huart: 指向 UART 句柄结构体的指针。UART 句柄结构体包含了与特定 UART 控制器相关的各种配置和状态信息。
* @param pData: 指向要发送数据的缓冲区的指针。
* @param Size: 要发送的数据的字节数。
* @param Timeout: 发送操作的超时时间,以毫秒为单位。如果在指定的时间内未完成发送,则函数可能会返回错误或超时。
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout) {
// 实现代码
}
/**
* @brief 通过UART接收数据
* @param huart: 指向 UART 句柄结构体的指针。UART 句柄结构体包含了与特定 UART 控制器相关的各种配置和状态信息。
* @param pData: 指向接收数据存储缓冲区的指针。
* @param Size: 要接收的数据的字节数。
* @param Timeout: 接收操作的超时时间,以毫秒为单位。如果在指定的时间内未完成接收,则函数可能会返回错误或超时。
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout) {
// 实现代码
}
/**
* @brief 通过UART发送数据(中断方式)
* @param huart: 指向 UART 句柄结构体的指针。UART 句柄结构体包含了与特定 UART 控制器相关的各种配置和状态信息。
* @param pData: 指向要发送数据的缓冲区的指针。
* @param Size: 要发送的数据的字节数。
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size) {
// 实现代码
}
/**
* @brief 通过UART接收数据(中断方式)
* @param huart: 指向 UART 句柄结构体的指针。UART 句柄结构体包含了与特定 UART 控制器相关的各种配置和状态信息。
* @param pData: 指向接收数据存储缓冲区的指针。
* @param Size: 要接收的数据的字节数。
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) {
// 实现代码
}
/**
* @brief 通过UART发送数据(DMA方式)
* @param huart: 指向 UART 句柄结构体的指针。UART 句柄结构体包含了与特定 UART 控制器相关的各种配置和状态信息。
* @param pData: 指向要发送数据的缓冲区的指针。
* @param Size: 要发送的数据的字节数。
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size) {
// 实现代码
}
/**
* @brief 通过UART接收数据(DMA方式)
* @param huart: 指向 UART 句柄结构体的指针。UART 句柄结构体包含了与特定 UART 控制器相关的各种配置和状态信息。
* @param pData: 指向接收数据存储缓冲区的指针。
* @param Size: 要接收的数据的字节数。
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) {
// 实现代码
}
/**
* @brief 暂停UART的DMA传输
* @param huart: 指向 UART 句柄结构体的指针。UART 句柄结构体包含了与特定 UART 控制器相关的各种配置和状态信息。
* @retval None
*/
void HAL_UART_DMAPause(UART_HandleTypeDef *huart) {
// 实现代码
}
/**
* @brief 恢复UART的DMA传输
* @param huart: 指向 UART 句柄结构体的指针。UART 句柄结构体包含了与特定 UART 控制器相关的各种配置和状态信息。
* @retval None
*/
void HAL_UART_DMAResume(UART_HandleTypeDef *huart) {
// 实现代码
}
/**
* @brief 停止UART的DMA传输
* @param huart: 指向 UART 句柄结构体的指针。UART 句柄结构体包含了与特定 UART 控制器相关的各种配置和状态信息。
* @retval None
*/
void HAL_UART_DMAStop(UART_HandleTypeDef *huart) {
// 实现代码
}
3.实训代码参考
二:IIC
(1)特点
IIC(Inter-Integrated Circuit)通讯协议,也被称为I2C协议,是一种广泛应用的同步串行通信接口。它由荷兰的PHILIPS公司(现为NXP半导体公司)在1982年开发,主要用于连接电路之间进行短距离的数字通信,如微控制器与其外围设备之间的通信。以下是IIC通讯协议的详细介绍:
一、协议特点
- 半双工通信:IIC协议采用半双工工作模式,即在同一时间只能进行单向的数据传输。
- 两线制结构:总线由两条信号线组成,一条是数据线SDA(Serial Data Line),用于传输数据;另一条是时钟线SCL(Serial Clock Line),由主设备提供时钟信号,以确保所有连接到总线的设备同步进行数据交换。
- 多设备连接:IIC总线支持多设备连接,每个从设备都有一个唯一的地址,主设备通过发送这个地址来选择与其通信的目标设备。同时,IIC支持多主控功能,多个具备主控能力的设备可以在同一总线上竞争控制权,并通过硬件仲裁机制避免冲突。
- 简单高效:IIC协议的物理接口简单且占用线路少,被广泛应用于嵌入式系统和电子设备中,方便连接各种低速外设,如传感器、存储器、时钟芯片等。
总线上每一个器件都有一个唯一的地址识别,所以我们只需要知道器件的地址,根据时 序就可以实现微控制器与器件之间的通信。
数据线 SDA 和时钟线 SCL 都是双向线路,都通过一个电流源或上拉电阻连接到正的电 压,所以当总线空闲的时候,这两条线路都是高电平。
总线支持设备连接。在使用 IIC 通信总线时,可以有多个具备 IIC 通信能力的设备挂载 在上面,同时支持多个主机和多个从机,连接到总线的接口数量只由总线电容 400pF 的限制决 定。
二、信号类型
IIC总线在传输数据的过程中主要有三种类型的信号:
- 起始信号(Start):当SCL为高电平时,SDA由高电平向低电平跳变,标志着开始传输数据。
- 结束信号(Stop):当SCL为高电平时,SDA由低电平向高电平跳变,标志着结束传输数据。
- 应答信号(ACK/NACK):所有地址和数据都以8bit为单位传输,如果接受端正确接收了8bit数据,则回复一个bit的“0”信号——ACK信号;如果未正确接收8bit数据,或者接收端不再接受数据,则回复一个bit的“1”信号——NACK信号。
三、通信速率
IIC协议支持多种速度等级,以满足不同应用环境对数据传输速率的需求。常见的速度等级包括:
- 标准模式:100k bit/s
- 快速模式:400k bit/s
- 高速模式:3.4Mbit/s
四、通信过程
以主设备向从设备写数据为例,通信过程大致如下:
- 主机产生起始信号:标志着数据传输的开始。
- 主机发送从机地址:包括7位从机地址和1位读写位(0表示写,1表示读)。
- 从机应答:从机接收到地址后,如果地址匹配且准备好接收数据,则回复ACK信号。
- 主机发送数据:主机发送要写入从机的数据,每发送完8bit数据后,从机回复ACK信号以确认接收。
- 主机产生结束信号:标志着数据传输的结束。
五、应用实例
IIC只适合在同一款芯片版上距离不超过30cm的芯片进行通讯。
IIC协议在嵌入式系统和电子设备中有广泛应用,如连接传感器、存储器、时钟芯片等低速外设。以AT24C02存储芯片为例,它是一款基于EEPROM技术的存储芯片,采用I2C总线协议进行通信,主机可以通过I2C接口对AT24C02进行读写操作,以保存和加载系统配置等信息。
综上所述,IIC通讯协议以其简单、高效、灵活的特点,在嵌入式系统和电子设备领域发挥着重要作用。
六、通讯原理
看懂波形图中,需要示波器,IIC协议说明书,传感器说明书
单片机I2C通信入门(下):三份文件搞清楚I2C通信协议_哔哩哔哩_bilibili
相对讲的最清楚的视频
时钟信号完全由主机控制,写入(主机操作完成后),时钟线依旧有(在高电平处读取数据),从机的应答位为低电平,此时主机知道从机回复,给出主机给从机数据(让从机进行一个对象处理)
正点原子解析:
① 起始信号 当 SCL 为高电平期间,SDA 由高到低的跳变。起始信号是一种电平跳变时序信号(一段时间),而不是 一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传 输。
② 停止信号 当 SCL 为高电平期间,SDA 由低到高的跳变。停止信号也是一种电平跳变时序信号,而不 是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
③ 应答信号 发送器每发送一个字节,就在时钟脉冲 9 期间释放数据线,由接收器反馈一个应答信号。 应答信号为低电平时,规定为有效应答位(ACK 简称应答位),表示接收器已经成功地接收了 该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成 功。(应答位低电平就是接收到信息了,高电平就是没有接收到信息) 观察上图标号③就可以发现,有效应答的要求是从机在第 9 个时钟脉冲之前的低电平期间 将 SDA 线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主机,则在它 收到最后一个字节后,发送一个 NACK 信号,以通知被控发送器结束数据发送,并释放 SDA 线,以便主机接收器发送一个停止信号。
④ 数据有效性 IIC 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在 时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。数据在 SCL 的上 升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
⑤ 数据传输 在 I2C 总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在 SCL 串行 时钟的配合下,在 SDA 上逐位地串行传送每一位数据。数据位的传输是边沿触发。
⑥ 空闲状态 IIC 总线的 SDA 和 SCL 两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个 器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
总结:起始信号是时钟线在高电平,数据线拉低电平时触发,随后有8位代表从机地址(在从机数据手册中会写)一位读写位(主机对从机),一位应答位是否接收(低电平为接收到了,高电平为非接受)随后几位为数据传输(从机需要实行的行为在数据手册中也可以识别到),读写位,应答位
(2)上拉电阻的选值:
不能过小,不然电流过大烧毁MOS管,同时过小无法把导通时的电压拉到低电压(MOS本身就相当于100欧姆)
不能过大,每个IO口都会有一个寄生电容,当阻值过大时,由于t=RC(时间常数过大)则充电时间会有一个缓坡
单片机I2C通信入门(下):三份文件搞清楚I2C通信协议_哔哩哔哩_bilibili
(3)Cubemx配置IIC:
STM32cubemx的开发流程-CSDN博客
STM32 cubemx配置IIC_哔哩哔哩_bilibili
(4)关于IIC的程序设计:
如果用硬件IIC在cubemx中直接设置就行
软件IIC的配置:
1) 使能 IIC 的 SCL 和 SDA 对应的 GPIO 时钟。 本实验中 IIC 使用的 SCL 和 SDA 分别是 PB8 和 PB9,因此需要先使能 GPIOB 的时钟,代 码如下:
__HAL_RCC_GPIOB_CLK_ENABLE(); /* 使能 GPIOB 时钟 */
2) 设置对应 GPIO 工作模式(开漏输出) 本实验 GPIO 使用开漏输出模式(硬件已接外部上拉电阻,对于 F4 以上板子也可以用内部 的上拉电阻),通过函数 HAL_GPIO_Init 设置实现。
3) 参考 IIC 总线协议,编写信号函数(起始信号,停止信号,应答信号)
起始信号:SCL 为高电平时,SDA 由高电平向低电平跳变。停止信号:SCL 为高电平时,SDA 由低电平向高电平跳变。
应答信号:接收到 IC 数据后,向 IC 发出特定的低电平脉冲表示已接收到数据
(1)IIC.h
/**
****************************************************************************************************
* @file myiic.h
* @author 正点原子团队(ALIENTEK)
* @version V1.0
* @date 2021-10-23
* @brief IIC 驱动代码
* @license Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
****************************************************************************************************
* @attention
*
* 实验平台:正点原子 探索者 F407开发板
* 在线视频:www.yuanzige.com
* 技术论坛:www.openedv.com
* 公司网址:www.alientek.com
* 购买地址:openedv.taobao.com
*
* 修改说明
* V1.0 20211023
* 第一次发布
*
****************************************************************************************************
*/
#ifndef __MYIIC_H
#define __MYIIC_H
#include "./SYSTEM/sys/sys.h"
/******************************************************************************************/
/* 引脚 定义 */
#define IIC_SCL_GPIO_PORT GPIOB
#define IIC_SCL_GPIO_PIN GPIO_PIN_8
#define IIC_SCL_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
#define IIC_SDA_GPIO_PORT GPIOB
#define IIC_SDA_GPIO_PIN GPIO_PIN_9
#define IIC_SDA_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
/******************************************************************************************/
/* IO操作 */
#define IIC_SCL(x) do{ x ? \
HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* SCL */
#define IIC_SDA(x) do{ x ? \
HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* SDA */
#define IIC_READ_SDA HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN) /* 读取SDA */
/* IIC所有操作函数 */
void iic_init(void); /* 初始化IIC的IO口 */
void iic_start(void); /* 发送IIC开始信号 */
void iic_stop(void); /* 发送IIC停止信号 */
void iic_ack(void); /* IIC发送ACK信号 */
void iic_nack(void); /* IIC不发送ACK信号 */
uint8_t iic_wait_ack(void); /* IIC等待ACK信号 */
void iic_send_byte(uint8_t txd);/* IIC发送一个字节 */
uint8_t iic_read_byte(unsigned char ack);/* IIC读取一个字节 */
#endif
(2)IIC.c
初始化
/**
****************************************************************************************************
* @file myiic.c
* @author 正点原子团队(ALIENTEK)
* @version V1.0
* @date 2021-10-23
* @brief IIC 驱动代码
* @license Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
****************************************************************************************************
* @attention
*
* 实验平台:正点原子 探索者 F407开发板
* 在线视频:www.yuanzige.com
* 技术论坛:www.openedv.com
* 公司网址:www.alientek.com
* 购买地址:openedv.taobao.com
*
* 修改说明
* V1.0 20211023
* 第一次发布
*
****************************************************************************************************
*/
#include "./BSP/IIC/myiic.h"
#include "./SYSTEM/delay/delay.h"
/**
* @brief 初始化IIC
* @param 无
* @retval 无
*/
void iic_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
IIC_SCL_GPIO_CLK_ENABLE(); /* SCL引脚时钟使能 */
IIC_SDA_GPIO_CLK_ENABLE(); /* SDA引脚时钟使能 */
gpio_init_struct.Pin = IIC_SCL_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 快速 */
HAL_GPIO_Init(IIC_SCL_GPIO_PORT, &gpio_init_struct);/* SCL */
gpio_init_struct.Pin = IIC_SDA_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD; /* 开漏输出 */
HAL_GPIO_Init(IIC_SDA_GPIO_PORT, &gpio_init_struct);/* SDA */
/* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了, 开漏输出的时候(=1), 也可以读取外部信号的高低电平 */
iic_stop(); /* 停止总线上所有设备 */
}
/**
* @brief IIC延时函数,用于控制IIC读写速度
* @param 无
* @retval 无
*/
static void iic_delay(void)
{
delay_us(2); /* 2us的延时, 读写速度在250Khz以内 */
}
/**
* @brief 产生IIC起始信号
* @param 无
* @retval 无
*/
void iic_start(void)
{
IIC_SDA(1);
IIC_SCL(1);
iic_delay();
IIC_SDA(0); /* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 */
iic_delay();
IIC_SCL(0); /* 钳住I2C总线,准备发送或接收数据 */
iic_delay();
}
/**
* @brief 产生IIC停止信号
* @param 无
* @retval 无
*/
void iic_stop(void)
{
IIC_SDA(0); /* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */
iic_delay();
IIC_SCL(1);
iic_delay();
IIC_SDA(1); /* 发送I2C总线结束信号 */
iic_delay();
}
/**
* @brief 等待应答信号到来
* @param 无
* @retval 1,接收应答失败
* 0,接收应答成功
*/
uint8_t iic_wait_ack(void)
{
uint8_t waittime = 0;
uint8_t rack = 0;
IIC_SDA(1); /* 主机释放SDA线(此时外部器件可以拉低SDA线) */
iic_delay();
IIC_SCL(1); /* SCL=1, 此时从机可以返回ACK */
iic_delay();
while (IIC_READ_SDA) /* 等待应答 */
{
waittime++;
if (waittime > 250)
{
iic_stop();
rack = 1;
break;
}
}
IIC_SCL(0); /* SCL=0, 结束ACK检查 */
iic_delay();
return rack;
}
/**
* @brief 产生ACK应答
* @param 无
* @retval 无
*/
void iic_ack(void)
{
IIC_SDA(0); /* SCL 0 -> 1 时 SDA = 0,表示应答 */
iic_delay();
IIC_SCL(1); /* 产生一个时钟 */
iic_delay();
IIC_SCL(0);
iic_delay();
IIC_SDA(1); /* 主机释放SDA线 */
iic_delay();
}
/**
* @brief 不产生ACK应答
* @param 无
* @retval 无
*/
void iic_nack(void)
{
IIC_SDA(1); /* SCL 0 -> 1 时 SDA = 1,表示不应答 */
iic_delay();
IIC_SCL(1); /* 产生一个时钟 */
iic_delay();
IIC_SCL(0);
iic_delay();
}
/**
* @brief IIC发送一个字节
* @param data: 要发送的数据
* @retval 无
*/
void iic_send_byte(uint8_t data)
{
uint8_t t;
for (t = 0; t < 8; t++)
{
IIC_SDA((data & 0x80) >> 7); /* 高位先发送 */
iic_delay();
IIC_SCL(1);
iic_delay();
IIC_SCL(0);
data <<= 1; /* 左移1位,用于下一次发送 */
}
IIC_SDA(1); /* 发送完成, 主机释放SDA线 */
}
/**
* @brief IIC读取一个字节
* @param ack: ack=1时,发送ack; ack=0时,发送nack
* @retval 接收到的数据
*/
uint8_t iic_read_byte(uint8_t ack)
{
uint8_t i, receive = 0;
for (i = 0; i < 8; i++ ) /* 接收1个字节数据 */
{
receive <<= 1; /* 高位先输出,所以先收到的数据位要左移 */
IIC_SCL(1);
iic_delay();
if (IIC_READ_SDA)
{
receive++;
}
IIC_SCL(0);
iic_delay();
}
if (!ack)
{
iic_nack(); /* 发送nACK */
}
else
{
iic_ack(); /* 发送ACK */
}
return receive;
}
/**
* @brief IIC 延时函数,用于控制 IIC 读写速度
* @param 无
* @retval 无
*/
static void iic_delay(void)
{
delay_us(2); /* 2us 的延时, 读写速度在 250Khz 以内 */
}
/**
* @brief 产生 IIC 起始信号
* @param 无
* @retval 无
*/
void iic_start(void)
{
IIC_SDA(1);
IIC_SCL(1);
iic_delay();
IIC_SDA(0); /* START 信号: 当 SCL 为高时, SDA 从高变成低, 表示起始信号 */
iic_delay();
IIC_SCL(0); /* 钳住 I2C 总线,准备发送或接收数据 */
iic_delay();
}
/**
* @brief 产生 IIC 停止信号
* @param 无
* @retval 无
*/
void iic_stop(void)
{
IIC_SDA(0); /* STOP 信号: 当 SCL 为高时, SDA 从低变成高, 表示停止信号 */
iic_delay();
IIC_SCL(1);
iic_delay();
IIC_SDA(1); /* 发送 I2C 总线结束信号 */
iic_delay();
}
在这里首先定义一个 iic_delay 函数,目的就是控制 IIC 的读写速度,通过示波器检测读写 速度在 250KHz 内,所以一秒钟传输 500Kb 数据,换算一下即一个 bit 位需要 2us,在这个延时 时间内可以让器件获得一个稳定性的数据采集。
通过上图就可以很清楚了解数据传输时的细节,经过第一步的 SDA 高低电平的确定后,接 着需要延时,确保 SDA 输出的电平稳定,在 SCL 保持高电平期间,SDA 线上的数据是有效的, 此过程也是需要延时,使得从设备能够采集到有效的电平。然后准备下一位的数据,所以这里 需要的是把 data 左移一位,等待下一个时钟的到来,从设备进行读取。把上述的操作重复 8 次 就可以把 data 的 8 个位数据发送完毕,循环结束后,把 SDA 线拉高,等待接收从设备发送过 来的应答信号。
iic 的发送函数 iic_send_byte
我们把需要发送的数据作为形参,形参大小为 1 个字 节。在 iic 总线传输中,一个时钟信号就发送一个 bit,所以该函数需要循环八次,模拟八个时 钟信号,才能把形参的 8 个位数据都发送出去。这里使用的是形参 data 和 0x80 与运算的方式, 判断其最高位的逻辑值,假如为 1 即需要控制 SDA 输出高电平,否则为 0 控制 SDA 输出低电 平。为了更好说明,数据发送的过程,单独拿出数据传输时序图
通过上图就可以很清楚了解数据传输时的细节,经过第一步的 SDA 高低电平的确定后,接 着需要延时,确保 SDA 输出的电平稳定,在 SCL 保持高电平期间,SDA 线上的数据是有效的, 此过程也是需要延时,使得从设备能够采集到有效的电平。然后准备下一位的数据,所以这里 需要的是把 data 左移一位,等待下一个时钟的到来,从设备进行读取。把上述的操作重复 8 次 就可以把 data 的 8 个位数据发送完毕,循环结束后,把 SDA 线拉高,等待接收从设备发送过 来的应答信号。
/**
* @brief IIC 发送一个字节
* @param data: 要发送的数据
* @retval 无
*/
void iic_send_byte(uint8_t data)
{
uint8_t t;
for (t = 0; t < 8; t++)
{
IIC_SDA((data & 0x80) >> 7); /* 高位先发送 */
iic_delay();
IIC_SCL(1);
iic_delay();
IIC_SCL(0);
data <<= 1; /* 左移 1 位,用于下一次发送 */
}
IIC_SDA(1); /* 发送完成, 主机释放 SDA 线 */
}
iic 的读取函数 iic_read_byte
iic_read_byte 函数具体实现的方式跟 iic_send_byte 函数有所不同。首先可以明确的是时钟 信号是通过主机发出的,而且接收到的数据大小为 1 字节,但是 IIC 传输的单位是 bit,所以就 需要执行 8 次循环,才能把一字节数据接收完整。 具体实现过程:首先需要一个变量 receive 存放接收到的数据,在每一次循环开始前都需要 对 receive 进行左移 1 位操作,那么 receive 的 bit0 位每一次赋值前都是空的,用来存放最新接 收到的数据位,然后在 SCL 线进行高低电平切换时输出 IIC 时钟,在 SCL 高电平期间加入延 时,确保有足够的时间能让数据发送并进行处理,使用宏定义 IIC_READ_SDA 就可以判断读取 到的高低电平,假如 SDA 为高电平,那么 receive++即在 bit0 置 1,否则不做处理即保持原来的 0 状态。当 SCL 线拉低后,需要加入延时,便于从机切换 SDA 线输出数据。在 8 次循环结束 后,我们就获得了 8bit 数据,把它作为返回值返回,然而按照时序图,作为主机就需要发送应 答或者非应答信号,去回复从机
/**
* @brief IIC 读取一个字节
* @param ack: ack=1 时,发送 ack; ack=0 时,发送 nack
* @retval 接收到的数据
*/
uint8_t iic_read_byte(uint8_t ack)
{
uint8_t i, receive = 0;
for (i = 0; i < 8; i++ ) /* 接收 1 个字节数据 */
{
receive <<= 1; /* 高位先输出,所以先收到的数据位要左移 */
IIC_SCL(1);
iic_delay();
if (IIC_READ_SDA)
{
receive++;
}
IIC_SCL(0);
iic_delay();
}
if (!ack)
{
iic_nack(); /* 发送 nACK */
}
else
{
iic_ack(); /* 发送 ACK */
}
return receive;
}
iic_wait_ack 函数
上面提及到应答信号和非应答信号是在读时序中发生的,此外在写时序中也存在有一个信 号响应,当发送完 8bit 数据后,这里是一个等待从机应答信号的操作,这里我们也定义了,下 面看一下它们的定义
/**
* @brief 等待应答信号到来
* @param 无
* @retval 1,接收应答失败
* 0,接收应答成功
*/
uint8_t iic_wait_ack(void)
{
uint8_t waittime = 0;
uint8_t rack = 0;
IIC_SDA(1); /* 主机释放 SDA 线(此时外部器件可以拉低 SDA 线) */
iic_delay();
IIC_SCL(1); /* SCL=1, 此时从机可以返回 ACK */
iic_delay();
while (IIC_READ_SDA) /* 等待应答 */
{
waittime++;
if (waittime > 250)
{
iic_stop();
rack = 1;
break;
}
}
IIC_SCL(0); /* SCL=0, 结束 ACK 检查 */
iic_delay();
return rack;
}
/**
* @brief 产生 ACK 应答
* @param 无
* @retval 无
*/
void iic_ack(void)
{
IIC_SDA(0); /* SCL 0 -> 1 时 SDA = 0,表示应答 */
iic_delay();
IIC_SCL(1); /* 产生一个时钟 */
iic_delay();
IIC_SCL(0);
iic_delay();
IIC_SDA(1); /* 主机释放 SDA 线 */
iic_delay();
}
/**
* @brief 不产生 ACK 应答
* @param 无
* @retval 无
*/
void iic_nack(void)
{
IIC_SDA(1); /* SCL 0 -> 1 时 SDA = 1,表示不应答 */
iic_delay();
IIC_SCL(1); /* 产生一个时钟 */
iic_delay();
IIC_SCL(0);
iic_delay();
}
该函数主要用在写时序中,当启动起始信号,发送完 8bit 数据到从机时,我们就需要等待以及处理接收从机发送过来的响应信号或者非响应信号, 一般就是在 iic_send_byte 函数后面调用。 具体实现:首先先释放 SDA,把电平拉高,延时等待从机操作 SDA 线,然后主机拉高时 钟线并延时,确保有充足的时间让主机接收到从机发出的 SDA 信号,这里使用的是 IIC_READ_SDA 宏定义去读取,根据 IIC 协议,主机读取 SDA 的值为低电平,就表示“应答信 号”;读到 SDA 的值为高电平,就表示“非应答信号”。在这个等待读取的过程中加入了超时判 断,假如超过这个时间没有接收到数据,那么主机直接发出停止信号,跳出循环,返回等于 1 的变量。在正常等待到应答信号后,主机会把 SCL 时钟线拉低并延时,返回是否接收到应答信 号。 当主机作为接收端时,调用 iic_read_byte 函数之后,按照 iic 通信协议,需要给从机返回应 答或者是非应答信号,这里就是用到了 iic_ack 和 iic_nack 函数。 具体实现:从上面的说明已经知道了 SDA 为低电平即应答信号,高电平即非应答信号,那 么还是老规矩,首先先根据返回“应答”或者“非应答”两种情况拉低或者拉高 SDA,并延时 等待 SDA 电平稳定,然后主机拉高 SCL 线并延时,确保从机能有足够时间去接收 SDA 线上的电平信号。然后主机拉低时钟线并延时,完成这一位数据的传送。最后把 SDA 拉高,呈高阻态, 方便后续通信用到。
实用:
(a)0.96寸 OLED驱动代码:
STM32——IIC 0.96英寸OLED驱动代码-CSDN博客
(b)MPU6050标准库和HAL库代码:
STM32标准库HAL库——MPU6050原理和代码-CSDN博客