STM32F103C8T6 学习笔记摘要(四)
第一节 中断
1. 基本概念
中断其实就是当CPU 执行程序时,由于发生了某种随机的事件(外部或内部),引起 CPU 暂时中断正在运行的程序,转去执行一段特殊的服务程序(中断服务子程序或中断处理程序),以处理该事件,该事件处理完后又返回被中断的程序继续执行,这一过程就称为中断,引发中断的称为中断源
2. NVIC 介绍
NVIC 英文全称是 Nested Vectored Interrupt Controller,中文意思就是嵌套向量中断控制器,它属于 M3 内核的一个外设,控制着芯片的中断相关功能
说明一下:
- 就一个管理所有中断的东西,它会将中断发给M3内核
3. 中断优先级
4. 中断配置的大概步骤
①使能外设中断
②设置中断优先级分组,初始化 NVIC_InitTypeDef 结构体
typedef struct
{uint8_t NVIC_IRQChannel; //中断通道 uint8_t NVIC_IRQChannelPreemptionPriority; //抢占式优先级uint8_t NVIC_IRQChannelSubPriority; //响应式优先级 FunctionalState NVIC_IRQChannelCmd; //中断使能
} NVIC_InitTypeDef;
③编写中断服务函数(有固定名称-启动文件可以查看)
5. 外部中断
我使用独立按键来作为外部中断源,控制LED1,2,3,4的中断
5.1 理论介绍
这些中断通道已按照不同优先级顺序固定分配给相应的外部设备。从STM32F10x 中文参考手册的中断向量表可以知道具体分配到那些外设,这里只截取一部分
3-4-5 用于产生中断, 另一条是 3-6-7-8 用于产生事件 。中断线路最终会输入到 NVIC 控制器中, 从而会运行中断服务函数, 实现中断内功能, 这个是软件级的。 而事件线路最后产生的脉冲信号会流向其他的外设电路, 是硬件级的。 在 EXTI 框图最顶端可以看到,其外设接口时钟是由 PCLK2, 即 APB2 提供, 所以在后面使能 EXTI 时钟的时候一定要注意。
STM32F103 芯片每个 GPIO 端口均有 16 个管脚, 因此把每个端口的 16 个 IO 对应那 16 根中断线 EXTI0-EXTI15。
因为有多组,需要通过 AFIO 的外部中断配置寄存器 1 的 EXTIx[3:0]位来决定对应的中断线映射到哪个 GPIO 端口上, 对于中断线映射到 GPIO 端口上的配置函数在stm32f10x_gpio.c 和 stm32f10x_gpio.h 中。
EXTI相关库函数在stm32f10x_exti.c和stm32f10x_exti.h文件中。
(1)使能IO口时钟,配置IO口模式为输入
(2)开启 AFIO 时钟,设置 IO 口与中断线的映射关系
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
(3)配置中断分组(NVIC),使能中断
//EXTI12-15 NVIC 配置NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;//EXTI中断通道NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; //子优先级NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
(4)初始化EXTI,选择触发方式
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
typedef struct
{uint32_t EXTI_Line; //中断/事件EXTIMode_TypeDef EXTI_Mode; //EXTI模式EXTITrigger_TypeDef EXTI_Trigger; //EXTI触发方式FunctionalState EXTI_LineCmd; //中断线使能或失能
}EXTI_InitTypeDef;
(5)编写EXTI中断服务函数---和中断线有关系---》和gpio引脚有关系
EXTI0_IRQHandler
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
EXTI9_5_IRQHandler
EXTI15_10_IRQHandler 12 13 14 15
{如果12
}
5.2 exit.h
#ifndef _H_EXIT
#define _H_EXIT
#include "stm32f10x.h"
#include "system.h"
#include "systick.h"
#include "Led.h"
#include "key.h"// 中断初始化
void exit_init(void);#endif
5.3 exit_init函数
#include "exit.h"// 中断初始化
void exit_init(void)
{//1.时钟使能RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//选择GPIO管脚用作外部中断线// 这是独立按键的几个引脚GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_Pin_12);GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_Pin_13);GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_Pin_14);GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_Pin_15);//2.NVIC初始化NVIC_InitTypeDef nvicDef;nvicDef.NVIC_IRQChannelCmd = ENABLE;nvicDef.NVIC_IRQChannel = EXTI15_10_IRQn;// 中断通道nvicDef.NVIC_IRQChannelPreemptionPriority = 2;// 优先级nvicDef.NVIC_IRQChannelSubPriority = 3;NVIC_Init(&nvicDef);//3.中断初始化EXTI_InitTypeDef exitDef;exitDef.EXTI_LineCmd = ENABLE;exitDef.EXTI_Trigger = EXTI_Trigger_Falling;// 下降沿触发exitDef.EXTI_Mode = EXTI_Mode_Interrupt;exitDef.EXTI_Line = EXTI_Line12|EXTI_Line13|EXTI_Line14|EXTI_Line15;EXTI_Init(&exitDef);
}
- 我使用的那4个独立按键作为外部中断源,自然就需要设置对应的引脚
5.4 中断处理函数
// 中断处理函数
void EXIT15_10_IRQHandler()
{if(EXTI_GetITStatus(EXTI_Line12) == 1) LED1 = !LED1;if(EXTI_GetITStatus(EXTI_Line13) == 1) LED2 = !LED2;if(EXTI_GetITStatus(EXTI_Line14) == 1) LED3 = !LED3;if(EXTI_GetITStatus(EXTI_Line15) == 1) LED4 = !LED4;// 清除上一次中断触发后寄存器里面的数据值EXIT_ClearITPendingBIt(EXTI_Line12|EXTI_Line13|EXTI_Line14|EXTI_Line15);
}
说明一下:
- 这个中断函数的函数名需要查手册
5.5 main.c
#include "stm32f10x.h"
#include "led.h"
#include "key.h"
#include "exit.h"
#include "systick.h"int main()
{// 中断优先级分组 分2组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);SysTick_Init(72);// 1.初始化led_init();key_init();exit_init();while(1){delay_ms(150);}
}
6. 内部中断
我使用定时器中断来作为内部中断源,来控制LED1,2,3,4的亮灭
6.1 基础概念
STM32F1 的定时器非常多,由 2 个基本定时器(TIM6、TIM7)、4 个通用定时器(TIM2-TIM5)和 2 个高级定时器(TIM1、TIM8)组成。基本定时器的功能最为简单,类似于 51 单片机内定时器。通用定时器是在基本定时器的基础上扩展而来,增加了输入捕获与输出比较等功能。高级定时器又是在通用定时器基础上扩展而来,增加了可编程死区互补输出、重复计数器、带刹车(断路)功能,这些功能主要针对工业电机控制方面。 f103有TIM1,TIM2-TIM4
6.2 exit.h
#ifndef _H_EXIT
#define _H_EXIT
#include "stm32f10x.h"
#include "system.h"
#include "systick.h"
#include "Led.h"
#include "key.h"// 中断初始化
void exit_init(void);#endif
6.3 exit.c
#include "exit.h"// 中断初始化
void exit_init(void)
{//1.时钟使能RCC_APB2PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);//2.NVIV初始化NVIC_InitTypeDef nvicDef;nvicDef.NVIC_IRQChannelCmd = ENABLE;nvicDef.NVIC_IRQChannel = TIM4_IRQn;// 中断通道nvicDef.NVIC_IRQChannelPreemptionPriority = 2;// 优先级nvicDef.NVIC_IRQChannelSubPriority = 3;NVIC_Init(&nvicDef);//3.定时器中断初始化TIM_TimeBaseInitTypeDef tim4Def;tim4Def.TIM_ClockDivision = TIM_CKD_DIV1;tim4Def.TIM_Period=2000;tim4Def.TIM_Prescaler=35999;//会自动加1tim4Def.TIM_CounterMode=TIM_CounterMode_Up;TIM_TimeBaseInit(TIM4,&tim4Def);//4.开启定时器中断TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE);TIM_ClearITPendingBit(TIM4,TIM_IT_Update);//5.启动定时器TIM_Cmd(TIM4,ENABLE);
}// 中断处理函数
void TIM4_IRQHandler()
{if(TIM_GetITStatus(TIM4,TIM_IT_Update)){LED1!=LED1;}// 清除上一次中断触发后寄存器里面的数据值TIM_ClearITPendingBit(TIM4,TIM_IT_Update);
}
6.4 main.c
#include "stm32f10x.h"
#include "led.h"
#include "key.h"
#include "exit.h"
#include "systick.h"int main()
{// 中断优先级分组 分2组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);SysTick_Init(72);// 1.初始化led_init();key_init();exit_init();while(1){delay_ms(150);}
}
7. 呼吸灯
7.1 简单原理
需要使用输出比较寄存器,来改变方波波形,影响有效电平的占空比,从而改变电压高低,实现呼吸灯
7.2 pwm.h
#ifndef _H_PWM
#define _H_PWM
#include "stm32f10x.h"
#include "system.h"// per表示定时器自动装载值 psc表示定时器预分配系数
void pwm_init(u16 per,u16 psc);#endif
7.3 pwm.c
#include "pwm.h"void pwm_init(u16 per, u16 psc)
{// 1 定时器时钟使能RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 使能 TIM3 时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能 GPIOA 时钟/* 配置GPIO的模式和IO口 */GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出GPIO_Init(GPIOA, &GPIO_InitStructure);// 2 初始化定时器TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;TIM_TimeBaseInitStruct.TIM_Period = per; // 自动装载值TIM_TimeBaseInitStruct.TIM_Prescaler = psc; // 分频系数TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 设置向上计数模式TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);// 3 pwm配置/输出比较寄存器初始化TIM_OCInitTypeDef TIM_OCInitStructure;TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 设定PWM1模式TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; // 配置输出极性=低电平有效TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 输出使能TIM_OC2Init(TIM3, &TIM_OCInitStructure); // 输出比较通道2 初始化TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能TIMx在 CCR2 上的预装载寄存器TIM_ARRPreloadConfig(TIM3, ENABLE); // 使能预装载寄存器// 5 启动定时器TIM_Cmd(TIM3, ENABLE); // 使能定时器
}
说明一下:
- 这里的引脚的工作模式要设置为复用推挽输出模式
7.4 main.c
#include "stm32f10x.h"
#include "pwm.h"
#include "systick.h"int main()
{// 中断优先级分组 分2组// NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);SysTick_Init(72);// 1.初始化pwm_init(500,71);u16 j = 0;u8 mode = 1;while(1){// 使j在0到300之间变化if(mode){j++;if(j>=300) mode = 0;}else{j--;if(j<=0) mode = 1;}TIM_SetCompare2(TIM3,j);delay_ms(150);}
}
第二节 USART串口通信
1. 原理图
说明一下:
- 一端的输入对应另一端的输出
2. 简单介绍
STM32F103C8T6 只有 3 个 USART 外设。USART 支持同步单向通信和半双工单线通信;还支持 LIN(域互连网络)、智能卡协议与IrDA(红外线数据协会) SIR ENDEC 规范,以及调制解调器操作 (CTS/RTS)。而且,它还支持多处理器通信和 DMA 功能,使用 DMA 可实现高速数据通信。
USART 通过小数波特率发生器提供了多种波特率。 USART 在 STM32 中应用最多的是 printf 输出调试信息,当我们需要了解程序内的一些变量数据信息时,可以通过 printf 输出函数将这些信息打印到串口助手上显示,这样一来就给我们调试程序带来了极大的方便。
3. 结构框图
STM32F103C8T6有 3 个串口,其对应的管脚可在芯片数据手册上查找到,也可以直接查看开发板原理图,芯片所有的 IO 口功能都标注在管脚上了。USART1 挂接在 APB2 总线上,其他的挂接在 APB1 总线,由于 UART4 和UART5 只有异步传输功能,所以没有 SCLK、nCTS 和 nRTS 脚,如下:
4. myusart.h
#ifndef __usart_H
#define __usart_H#include "system.h"#define USART1_REC_LEN 200 //定义最大接收字节数 200//接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
extern u8 USART1_RX_BUF[USART1_REC_LEN];//接收状态标记
extern u16 USART1_RX_STA;// 这个参数表示波特率
void USART1_Init(u32 bound);#endif
5. myusart.c
#include "myusart.h"// 串口1中断服务程序
// 注意,读取USARTx->SR能避免莫名其妙的错误
u8 USART1_RX_BUF[USART1_REC_LEN]; // 接收缓冲,最大USART_REC_LEN个字节.
// 接收状态
// bit15, 接收完成标志
// bit14, 接收到0x0d
// bit13~0, 接收到的有效字节数目
u16 USART1_RX_STA = 0; // 接收状态标记/******************************************************************************** 函 数 名 : USART1_Init* 函数功能 : USART1初始化函数* 输 入 : bound:波特率* 输 出 : 无*******************************************************************************/
void USART1_Init(u32 bound)
{// GPIO端口设置GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);/* 配置GPIO的模式和IO口 */GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX //串口输出PA9GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出GPIO_Init(GPIOA, &GPIO_InitStructure); /* 初始化串口输入IO */GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // RX //串口输入PA10GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 模拟输入GPIO_Init(GPIOA, &GPIO_InitStructure); /* 初始化GPIO */// USART1 初始化设置USART_InitStructure.USART_BaudRate = bound; // 波特率设置USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 字长为8位数据格式USART_InitStructure.USART_StopBits = USART_StopBits_1; // 一个停止位USART_InitStructure.USART_Parity = USART_Parity_No; // 无奇偶校验位USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件数据流控制USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式USART_Init(USART1, &USART_InitStructure); // 初始化串口1USART_Cmd(USART1, ENABLE); // 使能串口1USART_ClearFlag(USART1, USART_FLAG_TC);USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 开启相关中断// Usart1 NVIC 配置NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; // 串口1中断通道NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; // 抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 子优先级3NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // IRQ通道使能NVIC_Init(&NVIC_InitStructure); // 根据指定的参数初始化VIC寄存器、
}/******************************************************************************** 函 数 名 : USART1_IRQHandler* 函数功能 : USART1中断函数* 输 入 : 无* 输 出 : 无*******************************************************************************/
void USART1_IRQHandler(void) // 串口1中断服务程序
{u8 r;if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) // 接收中断{r = USART_ReceiveData(USART1); //(USART1->DR); //读取接收到的数据if ((USART1_RX_STA & 0x8000) == 0) // 接收未完成{if (USART1_RX_STA & 0x4000) // 接收到了0x0d{if (r != 0x0a)USART1_RX_STA = 0; // 接收错误,重新开始elseUSART1_RX_STA |= 0x8000; // 接收完成了}else // 还没收到0X0D{if (r == 0x0d)USART1_RX_STA |= 0x4000;else{USART1_RX_BUF[USART1_RX_STA & 0X3FFF] = r;USART1_RX_STA++;if (USART1_RX_STA > (USART1_REC_LEN - 1))USART1_RX_STA = 0; // 接收数据错误,重新开始接收}}}}
}
6. main.c
#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "myusart.h"int main()
{u8 i = 0;u16 t = 0;u16 len = 0;SysTick_Init(72);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 中断优先级分组分2组USART1_Init(115200);led_init();while (1){if (USART1_RX_STA & 0x8000){len = USART1_RX_STA & 0x3fff; // 得到此次接收到的数据长度for (t = 0; t < len; t++){USART_SendData(USART1, USART1_RX_BUF[t]); // 向串口1发送数据while (USART_GetFlagStatus(USART1, USART_FLAG_TC) != SET); // 等待发送结束}USART1_RX_STA = 0;}i++;if (i % 10 == 0)LED1 = !LED1;delay_ms(10);}
}
7. 实现printf的重定向输出
int fputc(int ch,FILE *p) //函数默认的,在使用 printf 函数时自动调用
{USART_SendData(USART1,(u8)ch);while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);return ch;
}
第三节 ADC模数转换
1. STM32 ADC简介
ADC(analog to digital converter)即模数转换器,它可以将模拟信号转 换为数字信号。按照其转换原理主要分为逐次逼近型、双积分型、电压频率转换型三种
2. STM32 ADC结构框图
3. 开发板
4. adc.h
#ifndef _adc_H
#define _adc_H#include "system.h"void ADCx_Init(void);
u16 Get_ADC_Value(u8 ch,u8 times);#endif
5. adc.c
#include "adc.h"
#include "SysTick.h"/*******************************************************************************
* 函 数 名 : ADCx_Init
* 函数功能 : ADC初始化
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void ADCx_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量 ADC_InitTypeDef ADC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_ADC1,ENABLE);RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14MGPIO_InitStructure.GPIO_Pin=GPIO_Pin_1;//ADCGPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN; //模拟输入GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//独立模式ADC_InitStructure.ADC_ScanConvMode = DISABLE;//非扫描模式 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//关闭连续转换ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//禁止触发检测,使用软件触发ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//右对齐 ADC_InitStructure.ADC_NbrOfChannel = 1;//1个转换在规则序列中 也就是只转换规则序列1 ADC_Init(ADC1, &ADC_InitStructure);//ADC初始化ADC_Cmd(ADC1, ENABLE);//开启AD转换器ADC_ResetCalibration(ADC1);//重置指定的ADC的校准寄存器while(ADC_GetResetCalibrationStatus(ADC1));//获取ADC重置校准寄存器的状态ADC_StartCalibration(ADC1);//开始指定ADC的校准状态while(ADC_GetCalibrationStatus(ADC1));//获取指定ADC的校准程序ADC_SoftwareStartConvCmd(ADC1, ENABLE);//使能或者失能指定的ADC的软件转换启动功能
}/*******************************************************************************
* 函 数 名 : Get_ADC_Value
* 函数功能 : 获取通道ch的转换值,取times次,然后平均
* 输 入 : ch:通道编号times:获取次数
* 输 出 : 通道ch的times次转换结果平均值
*******************************************************************************/
u16 Get_ADC_Value(u8 ch,u8 times)
{u32 temp_val=0;u8 t;//设置指定ADC的规则组通道,一个序列,采样时间ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5); //ADC1,ADC通道,239.5个周期,提高采样时间可以提高精确度 for(t=0;t<times;t++){ADC_SoftwareStartConvCmd(ADC1, ENABLE);//使能指定的ADC1的软件转换启动功能 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束temp_val+=ADC_GetConversionValue(ADC1);delay_ms(5);}return temp_val/times;
}
6. main.c
#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "usart.h"
#include "adc.h"int main()
{u8 i=0;u16 value=0;float vol;SysTick_Init(72);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组分2组USART1_Init(115200);LED_Init();ADCx_Init();while(1){ i++;if(i%10==0)LED0=!LED0;if(i%100==0){value=Get_ADC_Value(ADC_Channel_9,20);printf("检测AD值为:%d\r\n",value);vol=(float)value*(3.3/4095);printf("检测电压值为:%.2fV\r\n",vol);}delay_ms(10);}
}
7. 内部温度采集 - adc
第四节 I2C串行同步通信
1. 基础概念
I2C是单片机与外设连接的一种常用方式,是属于一种串行同步通信方式。
2. 物理层
I2C 通信设备常用的连接方式如下图所示:
说明一下:
- 它是一个支持多设备的总线(指多个设备共用的信号线)。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机
- 一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
- 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问
- 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平
- 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线
- 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为400kbit/s,高速模式下可达 3.4Mbit/s,但目前大多I2C 设备尚不支持高速模式
- 连接到相同总线的 IC 数量受到总线的最大电容400pF 限制。
3. 常用术语
- 主机:启动数据传送并产生时钟信号的设备;
- 从机:被主机寻址的器件;
- 多主机:同时有多于一个主机尝试控制总线但不破坏传输;
- 主模式:MCU作为I2C通信的主动发起方,负责控制整个通信过程。
- 从模式:MCU作为I2C通信的被动响应方,等待主设备的指令。
- 仲裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并使传输不被破坏的过程;
- 同步:两个或多个器件同步时钟信号的过程;
- 发送器:发送数据到总线的器件;
- 接收器:从总线接收数据的器件。
4. 协议层
I2C 的协议定义了通信的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节
4.1 数据有效性规定
说明一下:
- 当SCL为高电平时,SDA的数据必须稳定
- 当SDA为低电平时,SDA的数据允许变化
- 每次数据传输都以字节为单位,每次传输的字节数不受限制。
4.2 起始和停止信号
起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态。
说明一下:
- SCL线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号
- SCL线为低电平期间,SDA 线由低电平向高电平的变化表示终止信号
4.3 应答响应
每当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位
在 I2C 通信中,每传输完一个字节后,接收端需通过控制 SDA 线发送一个 应答位(ACK/NACK),告知发送端是否继续传输:
-
ACK(应答):接收端拉低 SDA,表示已成功接收,发送端可继续发送下一个字节。
-
NACK(非应答):接收端释放 SDA(保持高电平),表示不再接收数据,发送端将发出停止信号,结束通信。
每一个字节必须保证是 8 位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9 位)
4.4 总线的寻址方式
I2C 总线寻址按照从机地址位数可分为两种,一种是7 位,另一种是10位。采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节)的位定义如下:
D7~D1 位组成从机的地址。D0 位是数据传送方向位,为“0”时表示主机向从机写数据,为“1”时表示主机由从机读数据
4.5 数据传输
在 I2C 总线上,传输的数据包括地址和实际数据。起始信号后,主机先发送7 位从机地址,第 8 位为读写位(R/W):
-
“0” 表示写数据(主机发送)
-
“1” 表示读数据(主机接收)
每次传输通常以主机发出的停止信号结束。若主机需继续通信,也可省略停止信号,直接发送新的起始信号开始下一次传输
在总线的一次数据传送过程中,可以有以下几种组合方式:
注意:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S 表示起始信号,P表示终止信号
a、主机向从机发送数据,数据传送方向在整个传送过程中不变
b、主机在第一个字节后,立即从从机读数据
c、在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好反相
5. 分类
硬件i2c:
大部分的MCU 都自带I2C 总线接口,STM32F1 芯片也不例外,STM32F1 芯片自带 2 个 I2C 接口,I2C1 和I2C2。
我们用到的实验实验我们不使用 STM32F1 自带的硬件 I2C,
软件i2c:
而采用软件模拟I2C。主要原因是STM32F1 的硬件 IIC 设计的比较复杂,而且稳定性不怎么好,程序移植比较麻烦,而用软件模拟 IIC,最大的好处就是移植方便,同一个代码兼容所有单片机,任何一个单片机只要有 IO 口(不需要特定 IO),都可以很快的移植过去。
6. 实验代码
I2C的代码我们就不去专门写了,涉及信号控制面的逻辑较多。我们直接使用现成的代码,有兴趣的同学可以对照信号控制看看代码。。
我们把焦点放在怎么用, 通过i2c协议操作一个支持i2c协议的外设
//IIC所有操作函数
void IIC_Init(void); //初始化IIC的IO口
void IIC_Start(void); //发送IIC开始信号
void IIC_Stop(void); //发送IIC停止信号
void IIC_Send_Byte(u8 txd); //IIC发送一个字节
u8 IIC_Read_Byte(u8 ack); //IIC读取一个字节
u8 IIC_Wait_Ack(void); //IIC等待ACK信号
void IIC_Ack(void); //IIC发送ACK信号
void IIC_NAck(void); //IIC发送非ACK信号
感兴趣的话,可以看看
#include "iic.h"
#include "SysTick.h"/*******************************************************************************
* 函 数 名 : IIC_Init
* 函数功能 : IIC初始化
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void IIC_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(IIC_SCL_PORT_RCC|IIC_SDA_PORT_RCC,ENABLE);GPIO_InitStructure.GPIO_Pin=IIC_SCL_PIN;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_Init(IIC_SCL_PORT,&GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin=IIC_SDA_PIN;GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);IIC_SCL=1;IIC_SDA=1;
}/*******************************************************************************
* 函 数 名 : SDA_OUT
* 函数功能 : SDA输出配置
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void SDA_OUT(void)
{GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin=IIC_SDA_PIN;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
}/*******************************************************************************
* 函 数 名 : SDA_IN
* 函数功能 : SDA输入配置
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void SDA_IN(void)
{GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin=IIC_SDA_PIN;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
}/*******************************************************************************
* 函 数 名 : IIC_Start
* 函数功能 : 产生IIC起始信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void IIC_Start(void)
{SDA_OUT(); //sda线输出IIC_SDA=1; IIC_SCL=1;delay_us(5);IIC_SDA=0;//START:when CLK is high,DATA change form high to low delay_us(6);//IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
} /*******************************************************************************
* 函 数 名 : IIC_Stop
* 函数功能 : 产生IIC停止信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void IIC_Stop(void)
{SDA_OUT();//sda线输出IIC_SCL=0;IIC_SDA=0;//STOP:when CLK is high DATA change form low to highIIC_SCL=1; delay_us(6); IIC_SDA=1;//发送I2C总线结束信号delay_us(6);
}/*******************************************************************************
* 函 数 名 : IIC_Wait_Ack
* 函数功能 : 等待应答信号到来
* 输 入 : 无
* 输 出 : 1,接收应答失败0,接收应答成功
*******************************************************************************/
u8 IIC_Wait_Ack(void)
{u8 tempTime=0;IIC_SDA=1;delay_us(1);SDA_IN(); //SDA设置为输入 IIC_SCL=1;delay_us(1); while(READ_SDA){tempTime++;if(tempTime>250){IIC_Stop();return 1;}}IIC_SCL=0;//时钟输出0 return 0;
} /*******************************************************************************
* 函 数 名 : IIC_Ack
* 函数功能 : 产生ACK应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void IIC_Ack(void)
{IIC_SCL=0;SDA_OUT();IIC_SDA=0;delay_us(2);IIC_SCL=1;delay_us(5);IIC_SCL=0;
}/*******************************************************************************
* 函 数 名 : IIC_NAck
* 函数功能 : 产生NACK非应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void IIC_NAck(void)
{IIC_SCL=0;SDA_OUT();IIC_SDA=1;delay_us(2);IIC_SCL=1;delay_us(5);IIC_SCL=0;
} /*******************************************************************************
* 函 数 名 : IIC_Send_Byte
* 函数功能 : IIC发送一个字节
* 输 入 : txd:发送一个字节
* 输 出 : 无
*******************************************************************************/
void IIC_Send_Byte(u8 txd)
{ u8 t; SDA_OUT(); IIC_SCL=0;//拉低时钟开始数据传输for(t=0;t<8;t++){ if((txd&0x80)>0) //0x80 1000 0000IIC_SDA=1;elseIIC_SDA=0;txd<<=1; delay_us(2); //对TEA5767这三个延时都是必须的IIC_SCL=1;delay_us(2); IIC_SCL=0; delay_us(2);}
} /*******************************************************************************
* 函 数 名 : IIC_Read_Byte
* 函数功能 : IIC读一个字节
* 输 入 : ack=1时,发送ACK,ack=0,发送nACK
* 输 出 : 应答或非应答
*******************************************************************************/
u8 IIC_Read_Byte(u8 ack)
{u8 i,receive=0;SDA_IN();//SDA设置为输入for(i=0;i<8;i++ ){IIC_SCL=0; delay_us(2);IIC_SCL=1;receive<<=1;if(READ_SDA)receive++; delay_us(1); } if (!ack)IIC_NAck();//发送nACKelseIIC_Ack(); //发送ACK return receive;
}
7. 应用一:EEPROM实验
EEPROM(Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。
7.1 AT24C02介绍
AT24C02 是一款由 Microchip(原 Atmel)生产的 串行 EEPROM(可擦写只读存储器),容量为 2Kbit(256 字节),采用 I2C 总线协议 与主机通信,广泛用于存储小量数据,如参数配置、序列号等
主要特性
-
容量:2Kbit = 256 字节
-
接口协议:I2C(支持标准/快速模式,最高 400kHz)
-
地址范围:0x00 ~ 0xFF
-
数据写入周期:典型 5ms(页写:最多 8 字节)
-
电压范围:1.8V ~ 5.5V,适用于多种嵌入式系统
-
寿命:写入寿命 100 万次,数据保存 ≥100 年
-
封装类型:常见有 DIP、SOP、TSSOP 等
-
器件地址:前 4 位固定为 1010,后三位由 A2/A1/A0 引脚配置(可接多片)
原理图
开发板已经将芯片的 A0/A1/A2 连接到 GND,所以器件地址为1010000,即 0x50(未计算最低位)。如果要对芯片进行写操作时,R/W 即为0,写器件地址即为 0XA0;如果要对芯片进行读操作时,R/W 即为 1,此时读器件地址为0XA1。开发板上我们也将 WP 引脚直接接在 GND 上(0),此时芯片允许数据正常读写。
感兴趣的话,可以看看相关代码
#include "24cxx.h"
#include "SysTick.h"/*******************************************************************************
* 函 数 名 : AT24CXX_Init
* 函数功能 : AT24CXX初始化
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void AT24CXX_Init(void)
{IIC_Init();//IIC初始化
}/*******************************************************************************
* 函 数 名 : AT24CXX_ReadOneByte
* 函数功能 : 在AT24CXX指定地址读出一个数据
* 输 入 : ReadAddr:开始读数的地址
* 输 出 : 读到的数据
*******************************************************************************/
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{ u8 temp=0; IIC_Start(); if(EE_TYPE>AT24C16){IIC_Send_Byte(0XA0); //发送写命令IIC_Wait_Ack();IIC_Send_Byte(ReadAddr>>8);//发送高地址 }else {IIC_Send_Byte(0XA0+((ReadAddr/256)<<1)); //发送器件地址0XA0,写数据} IIC_Wait_Ack(); IIC_Send_Byte(ReadAddr%256); //发送低地址IIC_Wait_Ack(); IIC_Start(); IIC_Send_Byte(0XA1); //进入接收模式 IIC_Wait_Ack(); temp=IIC_Read_Byte(0); IIC_Stop();//产生一个停止条件 return temp;
}/*******************************************************************************
* 函 数 名 : AT24CXX_WriteOneByte
* 函数功能 : 在AT24CXX指定地址写入一个数据
* 输 入 : WriteAddr :写入数据的目的地址 DataToWrite:要写入的数据
* 输 出 : 无
*******************************************************************************/
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{ IIC_Start(); if(EE_TYPE>AT24C16){IIC_Send_Byte(0XA0); //发送写命令IIC_Wait_Ack();IIC_Send_Byte(WriteAddr>>8);//发送高地址 }else {IIC_Send_Byte(0XA0+((WriteAddr/256)<<1)); //发送器件地址0XA0,写数据} IIC_Wait_Ack(); IIC_Send_Byte(WriteAddr%256); //发送低地址IIC_Wait_Ack(); IIC_Send_Byte(DataToWrite); //发送字节 IIC_Wait_Ack(); IIC_Stop();//产生一个停止条件 delay_ms(10);
}/*******************************************************************************
* 函 数 名 : AT24CXX_WriteLenByte
* 函数功能 : 在AT24CXX里面的指定地址开始写入长度为Len的数据用于写入16bit或者32bit的数据
* 输 入 : WriteAddr :写入数据的目的地址 DataToWrite:要写入的数据Len :要写入数据的长度2,4
* 输 出 : 无
*******************************************************************************/
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len)
{ u8 t;for(t=0;t<Len;t++){AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);}
}/*******************************************************************************
* 函 数 名 : AT24CXX_ReadLenByte
* 函数功能 : 在AT24CXX里面的指定地址开始读出长度为Len的数据用于读出16bit或者32bit的数据
* 输 入 : ReadAddr :开始读出的地址 Len :要读出数据的长度2,4
* 输 出 : 读取的数据
*******************************************************************************/
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len)
{ u8 t;u32 temp=0;for(t=0;t<Len;t++){temp<<=8;temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1); }return temp;
}/*******************************************************************************
* 函 数 名 : AT24CXX_Check
* 函数功能 : 检查AT24CXX是否正常
* 输 入 : 无
* 输 出 : 1:检测失败,0:检测成功
*******************************************************************************/
u8 AT24CXX_Check(void)
{u8 temp;temp=AT24CXX_ReadOneByte(255);//避免每次开机都写AT24CXX if(temp==0x36)return 0; else//排除第一次初始化的情况{AT24CXX_WriteOneByte(255,0X36);temp=AT24CXX_ReadOneByte(255); if(temp==0X36)return 0;}return 1;
}/*******************************************************************************
* 函 数 名 : AT24CXX_Read
* 函数功能 : 在AT24CXX里面的指定地址开始读出指定个数的数据
* 输 入 : ReadAddr :开始读出的地址 对24c02为0~255pBuffer :数据数组首地址NumToRead:要读出数据的个数
* 输 出 : 无
*******************************************************************************/
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{while(NumToRead){*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++); NumToRead--;}
} /*******************************************************************************
* 函 数 名 : AT24CXX_Write
* 函数功能 : 在AT24CXX里面的指定地址开始写入指定个数的数据
* 输 入 : WriteAddr :开始写入的地址 对24c02为0~255pBuffer :数据数组首地址NumToRead:要读出数据的个数
* 输 出 : 无
*******************************************************************************/
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{while(NumToWrite--){AT24CXX_WriteOneByte(WriteAddr,*pBuffer);WriteAddr++;pBuffer++;}
}
8. 应用二:温度传感器
8.1 DS18B20介绍
DS18B20 是一款由 Maxim Integrated(现为 Analog Devices)推出的 数字温度传感器,采用 单总线(1-Wire)通信协议,具有高精度、可多点挂载等特点,广泛应用于环境监测、智能家居、工业控制等领域
主要特性
-
测温范围:-55°C ~ +125°C
-
测量精度:±0.5°C(-10°C ~ +85°C 范围内)
-
分辨率可调:9~12 位(默认 12 位,分辨率为 0.0625°C)
-
通信方式:1-Wire 总线(单根数据线即可通信,需上拉电阻)
-
供电方式:
-
正常供电(VCC:3.0V ~ 5.5V)
-
寄生电源模式(仅通过数据线供电)
-
-
唯一 64 位 ROM 地址:支持多点挂载和识别
-
封装形式:TO-92(三极管样式)、SOP、小封装防水管等多种形式
相关代码
#include "ds18b20.h"
#include "SysTick.h"/*******************************************************************************
* 函 数 名 : DS18B20_IO_IN
* 函数功能 : DS18B20_IO输入配置
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void DS18B20_IO_IN(void)
{GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin=DS18B20_PIN;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;GPIO_Init(DS18B20_PORT,&GPIO_InitStructure);
}/*******************************************************************************
* 函 数 名 : DS18B20_IO_OUT
* 函数功能 : DS18B20_IO输出配置
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void DS18B20_IO_OUT(void)
{GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin=DS18B20_PIN;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_Init(DS18B20_PORT,&GPIO_InitStructure);
}/*******************************************************************************
* 函 数 名 : DS18B20_Reset
* 函数功能 : 复位DS18B20
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void DS18B20_Reset(void)
{ DS18B20_IO_OUT(); //SET PG11 OUTPUTDS18B20_DQ_OUT=0; //拉低DQdelay_us(750); //拉低750usDS18B20_DQ_OUT=1; //DQ=1 delay_us(15); //15US
}/*******************************************************************************
* 函 数 名 : DS18B20_Check
* 函数功能 : 检测DS18B20是否存在
* 输 入 : 无
* 输 出 : 1:未检测到DS18B20的存在,0:存在
*******************************************************************************/
u8 DS18B20_Check(void)
{ u8 retry=0;DS18B20_IO_IN();//SET PG11 INPUT while (DS18B20_DQ_IN&&retry<200){retry++;delay_us(1);};if(retry>=200)return 1;else retry=0;while (!DS18B20_DQ_IN&&retry<240){retry++;delay_us(1);};if(retry>=240)return 1; return 0;
}/*******************************************************************************
* 函 数 名 : DS18B20_Read_Bit
* 函数功能 : 从DS18B20读取一个位
* 输 入 : 无
* 输 出 : 1/0
*******************************************************************************/
u8 DS18B20_Read_Bit(void) // read one bit
{u8 data;DS18B20_IO_OUT();//SET PG11 OUTPUTDS18B20_DQ_OUT=0; delay_us(2);DS18B20_DQ_OUT=1; DS18B20_IO_IN();//SET PG11 INPUTdelay_us(12);if(DS18B20_DQ_IN)data=1;else data=0; delay_us(50); return data;
}/*******************************************************************************
* 函 数 名 : DS18B20_Read_Byte
* 函数功能 : 从DS18B20读取一个字节
* 输 入 : 无
* 输 出 : 一个字节数据
*******************************************************************************/
u8 DS18B20_Read_Byte(void) // read one byte
{ u8 i,j,dat;dat=0;for (i=0;i<8;i++) {j=DS18B20_Read_Bit();dat=(j<<7)|(dat>>1);} return dat;
}/*******************************************************************************
* 函 数 名 : DS18B20_Write_Byte
* 函数功能 : 写一个字节到DS18B20
* 输 入 : dat:要写入的字节
* 输 出 : 无
*******************************************************************************/
void DS18B20_Write_Byte(u8 dat)
{ u8 j;u8 testb;DS18B20_IO_OUT();//SET PG11 OUTPUT;for (j=1;j<=8;j++) {testb=dat&0x01;dat=dat>>1;if (testb) {DS18B20_DQ_OUT=0;// Write 1delay_us(2); DS18B20_DQ_OUT=1;delay_us(60); }else {DS18B20_DQ_OUT=0;// Write 0delay_us(60); DS18B20_DQ_OUT=1;delay_us(2); }}
}/*******************************************************************************
* 函 数 名 : DS18B20_Start
* 函数功能 : 开始温度转换
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void DS18B20_Start(void)// ds1820 start convert
{ DS18B20_Reset(); DS18B20_Check(); DS18B20_Write_Byte(0xcc);// skip romDS18B20_Write_Byte(0x44);// convert
} /*******************************************************************************
* 函 数 名 : DS18B20_Init
* 函数功能 : 初始化DS18B20的IO口 DQ 同时检测DS的存在
* 输 入 : 无
* 输 出 : 1:不存在,0:存在
*******************************************************************************/
u8 DS18B20_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(DS18B20_PORT_RCC,ENABLE);GPIO_InitStructure.GPIO_Pin=DS18B20_PIN;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_Init(DS18B20_PORT,&GPIO_InitStructure);DS18B20_Reset();return DS18B20_Check();
} /*******************************************************************************
* 函 数 名 : DS18B20_GetTemperture
* 函数功能 : 从ds18b20得到温度值
* 输 入 : 无
* 输 出 : 温度数据
*******************************************************************************/
float DS18B20_GetTemperture(void)
{u16 temp;u8 a,b;float value;DS18B20_Start(); // ds1820 start convertDS18B20_Reset();DS18B20_Check(); DS18B20_Write_Byte(0xcc);// skip romDS18B20_Write_Byte(0xbe);// convert a=DS18B20_Read_Byte(); // LSB b=DS18B20_Read_Byte(); // MSB temp=b;temp=(temp<<8)+a;if((temp&0xf800)==0xf800){temp=(~temp)+1;value=temp*(-0.0625);}else{value=temp*0.0625; }return value;
}
9. 应用三:OLED液晶显示器
9.1 OLED 介绍
OLED(Organic Light Emitting Diode,有机发光二极管) 是一种新型自发光显示技术,与传统 LCD 相比,无需背光源,具备更高对比度、更广视角和更快响应速度,广泛应用于手机、电视、智能手环、嵌入式显示等场景。
主要特点
-
自发光显示:每个像素点单独发光,不需背光,黑色更纯净,对比度高
-
视角广:接近 180°,颜色不易偏移
-
响应快:响应时间可达微秒级,适合动态内容显示
-
功耗低:黑色像素不发光,整体功耗低于 LCD(部分情况下)
-
厚度薄、可柔性:适合可穿戴设备、柔性屏等应用
-
常见颜色:单色(白、黄、蓝)、双色、全彩(RGB)
相关代码
#include "oled.h"
#include "iic.h"
#include "oledfont.h" //OLED的显存
//存放格式如下.
//[0]0 1 2 3 ... 127
//[1]0 1 2 3 ... 127
//[2]0 1 2 3 ... 127
//[3]0 1 2 3 ... 127
//[4]0 1 2 3 ... 127
//[5]0 1 2 3 ... 127
//[6]0 1 2 3 ... 127
//[7]0 1 2 3 ... 127
u8 OLED_GRAM[128][8];//IIC写命令
void write_iic_cmd(u8 cmd)
{IIC_Start();IIC_Send_Byte(0x78);//Slave address,SA0=0IIC_Wait_Ack(); IIC_Send_Byte(0x00);//write commandIIC_Wait_Ack(); IIC_Send_Byte(cmd); IIC_Wait_Ack(); IIC_Stop();
}//IIC写数据
void write_iic_data(u8 dat)
{IIC_Start();IIC_Send_Byte(0x78);//Slave address,SA0=0IIC_Wait_Ack(); IIC_Send_Byte(0x40);//write dataIIC_Wait_Ack(); IIC_Send_Byte(dat); IIC_Wait_Ack(); IIC_Stop();
}//向SSD1306写入一个字节。
//dat:要写入的数据/命令
//cmd:数据/命令标志 0,表示命令;1,表示数据;
void OLED_WR_Byte(u8 dat,u8 cmd)
{if(cmd)write_iic_data(dat);elsewrite_iic_cmd(dat);
}//更新显存到LCD
void OLED_Refresh_Gram(void)
{int i,n; for(i=7;i>=0;i--) //修改刷新方向 { OLED_WR_Byte (0xb0+i,OLED_CMD); //设置页地址(0~7)OLED_WR_Byte (0x00,OLED_CMD); //设置显示位置—列低地址OLED_WR_Byte (0x10,OLED_CMD); //设置显示位置—列高地址 for(n=0;n<128;n++)OLED_WR_Byte(OLED_GRAM[n][i],OLED_DATA); }
}void OLED_Set_Pos(unsigned char x, unsigned char y)
{ OLED_WR_Byte(0xb0+y,OLED_CMD);OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD);OLED_WR_Byte((x&0x0f)|0x01,OLED_CMD);
}
//开启OLED显示
void OLED_Display_On(void)
{OLED_WR_Byte(0X8D,OLED_CMD); //SET DCDC命令OLED_WR_Byte(0X14,OLED_CMD); //DCDC ONOLED_WR_Byte(0XAF,OLED_CMD); //DISPLAY ON
}
//关闭OLED显示
void OLED_Display_Off(void)
{OLED_WR_Byte(0X8D,OLED_CMD); //SET DCDC命令OLED_WR_Byte(0X10,OLED_CMD); //DCDC OFFOLED_WR_Byte(0XAE,OLED_CMD); //DISPLAY OFF
}//清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
void OLED_Clear(void)
{ u8 i,n; for(i=0;i<8;i++) { for(n=0;n<128;n++){OLED_GRAM[n][i]=0;}}OLED_Refresh_Gram();//更新显示
}//m^n函数
u32 oled_pow(u8 m,u8 n)
{u32 result=1; while(n--)result*=m; return result;
}//画点
//x:0~127
//y:0~63
//t:1 填充 0,清空
void OLED_DrawPoint(u8 x,u8 y,u8 t)
{u8 pos,bx,temp=0;if(x>127||y>63)return;//超出范围了.pos=7-y/8;bx=y%8;temp=1<<(7-bx);if(t)OLED_GRAM[x][pos]|=temp;else OLED_GRAM[x][pos]&=~temp;
}//x1,y1,x2,y2 填充区域的对角坐标
//确保x1<=x2;y1<=y2 0<=x1<=127 0<=y1<=63
//dot:0,清空;1,填充
void OLED_Fill(u8 x1,u8 y1,u8 x2,u8 y2,u8 dot)
{ u8 x,y; for(x=x1;x<=x2;x++){for(y=y1;y<=y2;y++){ OLED_DrawPoint(x,y,dot); }}
}//填充矩形
//x0,y0:矩形的左上角坐标
//width,height:矩形的尺寸
//color:颜色
void OLED_Fill_rectangle(u8 x0,u8 y0,u8 width,u8 height,u8 color)
{ if(width==0||height==0)return;//非法. OLED_Fill(x0,y0,x0+width-1,y0+height-1,color);
}//画线
//x1,y1:起点坐标
//x2,y2:终点坐标
void OLED_DrawLine(u8 x1, u8 y1, u8 x2, u8 y2,u8 color)
{u8 t; char xerr=0,yerr=0,delta_x,delta_y,distance; char incx,incy,uRow,uCol; delta_x=x2-x1; //计算坐标增量 delta_y=y2-y1; uRow=x1; uCol=y1; if(delta_x>0)incx=1; //设置单步方向 else if(delta_x==0)incx=0;//垂直线 else {incx=0xff;delta_x=-delta_x;} if(delta_y>0)incy=1; else if(delta_y==0)incy=0;//水平线 else{incy=0xff;delta_y=-delta_y;} if( delta_x>delta_y)distance=delta_x; //选取基本增量坐标轴 else distance=delta_y; for(t=0;t<=distance+1;t++ )//画线输出 { OLED_DrawPoint(uRow,uCol,color);//画点 xerr+=delta_x ; yerr+=delta_y ; if(xerr>distance) { xerr-=distance; uRow+=incx; } if(yerr>distance) { yerr-=distance; uCol+=incy; } }
}//画大点函数
//x0,y0:坐标
//color:颜色
//以(x0,y0)为中心,画一个9个点的大点
void OLED_Draw_Bigpoint(u8 x0,u8 y0,u8 color)
{u8 i,j;u8 x,y; if(x0>=1)x=x0-1;else x=x0;if(y0>=1)y=y0-1;else y=y0;for(i=y;i<y0+2;i++){for(j=x;j<x0+2;j++)OLED_DrawPoint(j,i,color);}
}//画垂直线
//x0,y0:坐标
//len:线长度
//color:颜色
void OLED_Draw_vline(u8 x0,u8 y0,u8 len,u8 color)
{if((len==0)||(x0>Max_Column-1)||(y0>Max_Row-1))return;OLED_Fill(x0,y0,x0,y0+len-1,color);
}//画水平线
//x0,y0:坐标
//len:线长度
//color:颜色
void OLED_Draw_hline(u8 x0,u8 y0,u8 len,u8 color)
{if((len==0)||(x0>Max_Column-1)||(y0>Max_Row-1))return;OLED_Fill(x0,y0,x0+len-1,y0,color);
}//画实心圆
//x0,y0:坐标
//r半径
//color:颜色
void OLED_Fill_circle(u8 x0,u8 y0,u8 r,u8 color)
{ u16 i;u16 imax = ((u32)r*707)/1000+1;u16 sqmax = (u32)r*(u32)r+(u32)r/2;u16 x=r;OLED_Draw_hline(x0-r,y0,2*r,color);for (i=1;i<=imax;i++) {if ((i*i+x*x)>sqmax) {// draw lines from outside if (x>imax) {OLED_Draw_hline (x0-i+1,y0+x,2*(i-1),color);OLED_Draw_hline (x0-i+1,y0-x,2*(i-1),color);}x--;}// draw lines from inside (center) OLED_Draw_hline(x0-x,y0+i,2*x,color);OLED_Draw_hline(x0-x,y0-i,2*x,color);}
}//画椭圆
//x0,y0:坐标
//rx:x方向半径
//ry:y方向半径
void OLED_Draw_ellipse(u8 x0, u8 y0, u8 rx, u8 ry,u8 color)
{u16 OutConst, Sum, SumY;u8 x,y;u8 xOld;u8 _rx = rx;u8 _ry = ry;if(rx>x0||ry>y0)return;//非法.OutConst = _rx*_rx*_ry*_ry+(_rx*_rx*_ry>>1); // Constant as explaint above // To compensate for rounding xOld = x = rx;for (y=0; y<=ry; y++) {if (y==ry)x=0; else {SumY =((u16)(rx*rx))*((u32)(y*y)); // Does not change in loop while(Sum = SumY + ((u16)(ry*ry))*((u32)(x*x)),(x>0) && (Sum>OutConst)) x--;}// Since we draw lines, we can not draw on the first iteration if (y) {OLED_DrawLine(x0-xOld,y0-y+1,x0-x,y0-y,color);OLED_DrawLine(x0-xOld,y0+y-1,x0-x,y0+y,color);OLED_DrawLine(x0+xOld,y0-y+1,x0+x,y0-y,color);OLED_DrawLine(x0+xOld,y0+y-1,x0+x,y0+y,color);}xOld = x;}
}//画矩形
//(x1,y1),(x2,y2):矩形的对角坐标
void OLED_DrawRectangle(u8 x1, u8 y1, u8 x2, u8 y2,u8 color)
{OLED_DrawLine(x1,y1,x2,y1,color);OLED_DrawLine(x1,y1,x1,y2,color);OLED_DrawLine(x1,y2,x2,y2,color);OLED_DrawLine(x2,y1,x2,y2,color);
}//在指定位置画一个指定大小的圆
//(x,y):中心点
//r :半径
void OLED_Draw_Circle(u8 x0,u8 y0,u8 r,u8 color)
{int a,b; // !9int di;a=0;b=r; di=3-(r<<1); //判断下个点位置的标志while(a<=b){OLED_DrawPoint(x0+a,y0-b,color); //5OLED_DrawPoint(x0+b,y0-a,color); //0 OLED_DrawPoint(x0+b,y0+a,color); //4 OLED_DrawPoint(x0+a,y0+b,color); //6 OLED_DrawPoint(x0-a,y0+b,color); //1 OLED_DrawPoint(x0-b,y0+a,color); OLED_DrawPoint(x0-a,y0-b,color); //2 OLED_DrawPoint(x0-b,y0-a,color); //7 a++;//使用Bresenham算法画圆 if(di<0)di +=4*a+6; else{di+=10+4*(a-b); b--;} }
} //在指定位置显示一个字符,包括部分字符
//x:0~127
//y:0~63
//mode:0,反白显示;1,正常显示
//size:选择字体 12/16/24
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size,u8 mode)
{ u8 temp,t,t1;u8 y0=y;u8 csize=(size/8+((size%8)?1:0))*(size/2); //得到字体一个字符对应点阵集所占的字节数chr=chr-' ';//得到偏移后的值 for(t=0;t<csize;t++){ if(size==12)temp=ascii_1206[chr][t]; //调用1206字体else if(size==16)temp=ascii_1608[chr][t]; //调用1608字体else if(size==24)temp=ascii_2412[chr][t]; //调用2412字体else return; //没有的字库for(t1=0;t1<8;t1++){if(temp&0x80)OLED_DrawPoint(x,y,mode);else OLED_DrawPoint(x,y,!mode);temp<<=1;y++;if((y-y0)==size){y=y0;x++;break;}} }
}//显示2个数字
//x,y :起点坐标
//len :数字的位数
//size:字体大小
//num:数值(0~4294967295);
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size)
{ u8 t,temp;u8 enshow=0; for(t=0;t<len;t++){temp=(num/oled_pow(10,len-t-1))%10;if(enshow==0&&t<(len-1)){if(temp==0){OLED_ShowChar(x+(size/2)*t,y,' ',size,1);continue;}else enshow=1; }OLED_ShowChar(x+(size/2)*t,y,temp+'0',size,1); }
} //显示字符串
//x,y:起点坐标
//size:字体大小
//*p:字符串起始地址
void OLED_ShowString(u8 x,u8 y,const u8 *p,u8 size)
{ while((*p<='~')&&(*p>=' '))//判断是不是非法字符!{ if(x>(X_WIDTH-(size/2))){x=0;y+=size;}if(y>(Y_WIDTH-size)){y=x=0;OLED_Clear();}OLED_ShowChar(x,y,*p,size,1); x+=size/2;p++;}
}//显示汉字
//x,y:起点坐标
//pos:数组位置汉字显示
//size:字体大小
//mode:0,反白显示;1,正常显示
void OLED_ShowFontHZ(u8 x,u8 y,u8 pos,u8 size,u8 mode)
{u8 temp,t,t1;u8 y0=y; u8 csize=(size/8+((size%8)?1:0))*(size);//得到字体一个字符对应点阵集所占的字节数 if(size!=12&&size!=16&&size!=24)return; //不支持的sizefor(t=0;t<csize;t++){ if(size==12)temp=FontHzk[pos][t]; //调用1206字体else if(size==16)temp=FontHzk[pos][t]; //调用1608字体else if(size==24)temp=FontHzk[pos][t]; //调用2412字体else return; //没有的字库for(t1=0;t1<8;t1++){if(temp&0x80)OLED_DrawPoint(x,y,mode);else OLED_DrawPoint(x,y,!mode);temp<<=1;y++;if((y-y0)==size){y=y0;x++;break;}} }
} //显示BMP图片128×64
//起始点坐标(x,y),x的范围0~127,y为页的范围0~7
void OLED_DrawBMP(u8 x0, u8 y0,u8 x1, u8 y1,u8 BMP[])
{ u16 j=0;u8 x,y;if(y1%8==0)y=y1/8; else y=y1/8+1;for(y=y0;y<y1;y++){OLED_Set_Pos(x0,y);for(x=x0;x<x1;x++){ OLED_WR_Byte(BMP[j++],OLED_DATA); }}
}//OLED GPIO初始化
//OLED初始化
void OLED_Init(void)
{ IIC_Init();OLED_WR_Byte(0xAE,OLED_CMD); //关闭显示OLED_WR_Byte(0xD5,OLED_CMD); //设置时钟分频因子,震荡频率OLED_WR_Byte(80,OLED_CMD); //[3:0],分频因子;[7:4],震荡频率OLED_WR_Byte(0xA8,OLED_CMD); //设置驱动路数OLED_WR_Byte(0X3F,OLED_CMD); //默认0X3F(1/64) OLED_WR_Byte(0xD3,OLED_CMD); //设置显示偏移OLED_WR_Byte(0X00,OLED_CMD); //默认为0OLED_WR_Byte(0x40,OLED_CMD); //设置显示开始行 [5:0],行数.OLED_WR_Byte(0x8D,OLED_CMD); //电荷泵设置OLED_WR_Byte(0x14,OLED_CMD); //bit2,开启/关闭OLED_WR_Byte(0x20,OLED_CMD); //设置内存地址模式OLED_WR_Byte(0x02,OLED_CMD); //[1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10;OLED_WR_Byte(0xA1,OLED_CMD); //段重定义设置,bit0:0,0->0;1,0->127;OLED_WR_Byte(0xC0,OLED_CMD); //设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数OLED_WR_Byte(0xDA,OLED_CMD); //设置COM硬件引脚配置OLED_WR_Byte(0x12,OLED_CMD); //[5:4]配置OLED_WR_Byte(0x81,OLED_CMD); //对比度设置OLED_WR_Byte(0xEF,OLED_CMD); //1~255;默认0X7F (亮度设置,越大越亮)OLED_WR_Byte(0xD9,OLED_CMD); //设置预充电周期OLED_WR_Byte(0xf1,OLED_CMD); //[3:0],PHASE 1;[7:4],PHASE 2;OLED_WR_Byte(0xDB,OLED_CMD); //设置VCOMH 电压倍率OLED_WR_Byte(0x30,OLED_CMD); //[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc;OLED_WR_Byte(0xA4,OLED_CMD); //全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏)OLED_WR_Byte(0xA6,OLED_CMD); //设置显示方式;bit0:1,反相显示;0,正常显示 OLED_WR_Byte(0xAF,OLED_CMD); //开启显示 OLED_Clear();
}