这里仅总结一下IO控制相关及这种总线等 ~持续更新 第17部的啊
一、STM32指针抽象出I2C的数据实例
1.
I2C总线是由PHILIPS公司开发的一种简单、「双向二线制同步串行总线」。
关于i2c的使用,并不陌生,STM32、C51、ARM、MSP430等,都基本集成硬件i2c,或者不集成i2c的,可以根据总线时序图使用普通IO口翻转模拟一根i2c总线。
对于流行的STM32饱受诟病的硬件I2C,相信很多人都是使用模拟I2C。
模拟i2c的源码比较多,大多都是大同小异,对于各类例程,提供的模拟i2c似乎都不是太规范(个人见解),特别是一根i2c总线挂多个外设、模拟多根i2c总线、以及更换一个i2c外设时,都需要大幅度修改源码、复制源码、重新调试时序等重复的工作。
在阅读过Linux设备驱动框架和RT-Thread的驱动框架,发现在总线分层上处理就特别好,完美解决了上述提及的问题。参考RT-Thread和Linux下的模拟i2c,整理修改在裸机上使用。
2.Linux、RT-Thread设备驱动模型
1)模型分为总线驱动和设备驱动;
2) 总线驱动与外设驱动分离,方便一根总线挂多个外设,方便移植;
3) 底层(与硬件相关)与上层分离,方便添加总线及移植到不同处理器,移植到其他处理器,只需重新实现硬件相关的“寄存器”层即可;
3.MCU下裸机形式i2c总线抽象
此部分实现源码为:i2c_core.c i2c_core.h
1)i2c总线抽象对外接口(API)
“i2c_bus_xfer”为i2c封装对外的API,函数原型如下,提供一个函数模型,具体需要实例化函数指针。
int i2c_bus_xfer(struct i2c_dev_device *dev,struct i2c_dev_message msgs[],unsigned int num)
{
int size;
size = dev->xfer(dev,msgs,num);
return size;
}
a)此函数即作为驱动外设的对外接口,所有操作通过此函数接口,与底层总线实现分离,如EEPROM、RTC、温度传感器等;
b)一个对外函数已经实现90%的情况使用,对应一些特殊情况,后期再完善或增加API。
c)struct i2c_dev_device *i2c_dev
2)i2c总线抽象API参数
a)i2c_dev:i2c设备指针,类型为“struct i2c_dev_device”,驱动一个i2c外设时,首先要对此指针设备初始化;
b)msgs:i2c一帧数据,发送数据及存放返回数据的缓存;
c)num:数据帧数量。
3)struct i2c_dev_device
该结构体为关键,调用API驱动外设时,首先对此初始化(类似于Linux/RT-Thread注册设备)。完整的设备包括两部分,数据操作函数和i2c相关信息(如硬件i2c或者模拟i2c)。因此“struct i2c_dev_device”的原型为:
struct i2c_dev_device
{
int (*xfer)(struct i2c_dev_device *dev,struct i2c_dev_message msgs[],unsigned int num);
void *i2c_phy;
};
a)第一个参数是函数指针,数据收发通过此函数指针调用实体函数实现;
b)第二个参数是一个void指针,初始化时指向我们使用的物理i2c(硬件/模拟),使用时可强制转换为对应的类型。
4)xfer
该函数与i2c总线设备对外接口函数“i2c_bus_xfer”具有相同的参数,形参参数参考此项的第2点,初始化时实例化指向实体函数。
5)struct i2c_dev_message
“struct i2c_dev_message”为i2c总线访问外设的一帧数据信息,包括发送数据、外设从地址、访问标识等。原型如下:
struct i2c_dev_message
{
unsigned short addr;
unsigned short flags;
unsigned short size;
unsigned char *buff;
unsigned char retries;
};
a)addr:i2c外设从机地址,常用为7位,10位较少用;
b)flags:标识,发送、接收、应答、地址位选择等标识;几种标识如下:
#define I2C_BUS_WR 0x0000
#define I2C_BUS_RD (1u << 0)
#define I2C_BUS_ADDR_10BIT (1u << 2)
#define I2C_BUS_NO_START (1u << 4)
#define I2C_BUS_IGNORE_NACK (1u << 5)
#define I2C_BUS_NO_READ_ACK (1u << 6)
c)size:发送的数据大小,或者接收的缓存大小;
d)buff:缓存区;
e)retries:i2c启动失败时,重启的次数。
4.模拟i2c抽象
对于模拟i2c,在以往的实现方式中,基本是时序图和外设代码混合在一起,增加外设或者使用新的i2c外设时,需要对模拟i2c代码进行较大工作量的修改,或者以“复制”的方式实现一套新的i2c总线。
但同理,可以把模拟i2c时序部分代码抽象出来,以“复用”代码的形式实现。此部分实现源码为:i2c_bitops.c i2c_bitops.h
1)模拟i2c抽象对外接口
根据上述封装的对外API,使用时,首先需要实现入口参数“i2c_dev”实例化,用模拟i2c即是调用模拟i2c相关接口。
int i2c_bitops_bus_xfer(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message msgs[],unsigned long num)
{
struct i2c_dev_message *msg;
unsigned long i;
unsigned short ignore_nack;
int ret;
ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK;
i2c_bitops_start(i2c_bus);
for (i = 0; i < num; i++)
{
msg = &msgs[i];
if (!(msg->flags & I2C_BUS_NO_START))
{
if (i)
{
i2c_bitops_restart(i2c_bus);
}
ret = i2c_bitops_send_address(i2c_bus,msg);
if ((ret != 0) && !ignore_nack)
goto out;
}
if (msg->flags & I2C_BUS_RD)
{//read
ret = i2c_bitops_bus_read(i2c_bus,msg);
if(ret < msg->size)
{
ret = -1;
goto out;
}
}
else
{//write
ret = i2c_bitops_bus_write(i2c_bus,msg);
if(ret < msg->size)
{
ret = -1;
goto out;
}
}
}
ret = i;
out:
i2c_bitops_stop(i2c_bus);
return ret;
}
int ops_i2c_bus_xfer(struct i2c_dev_device *i2c_dev,struct i2c_dev_message msgs[],unsigned int num)
{
return (i2c_bitops_bus_xfer((struct ops_i2c_dev*)(i2c_dev->i2c_phy),msgs,num));
}
a)模拟一根i2c总线时,对外的操作函数都通过上诉函数;i2c信息帧相关参数由上层调用传递进入,此处主要增加“struct ops_i2c_dev”的封装;
b)该函数使用到的函,其中入口参数为“struct ops_i2c_dev”类型的都是模拟i2c相关;
d)模拟i2c封装实现主要针对“struct ops_i2c_dev”原型的实例化。
2)struct ops_i2c_dev
“struct ops_i2c_dev”原型如下:
struct ops_i2c_dev
{
void (*set_sda)(int8_t state);
void (*set_scl)(int8_t state);
int8_t (*get_sda)(void);
int8_t (*get_scl)(void);
void (*delayus)(uint32_t us);
};
a)set_sda:数据线输出;
b)set_scl:时钟线输出;
c)get_sda:数据线输入(捕获);
d)get_scl:时钟线输入(捕获);
e)delayus:延时函数;
要实现一个模拟i2c,只需将上诉函数指针的实体实现即可,具体看后面描述。
3)模拟i2c时序
以产生i2c起始信号函数为例子,简要分析:
static void i2c_bitops_start(struct ops_i2c_dev *i2c_bus)
{
i2c_bus->set_sda(0);
i2c_bus->delayus(3);
i2c_bus->set_scl(0);
}
入口参数为struct ops_i2c_dev * i2c_bus,其实就是i2c_bitops_bus_xfer应用层函数传入的参数,最终是在此调用,底层需要实现的就是io模拟的输入/输出状态函数。
其他函数,如
static void i2c_bitops_restart(struct ops_i2c_dev *i2c_bus)
static char i2c_bitops_wait_ack(struct ops_i2c_dev *i2c_bus)
static int i2c_bitops_send_byte(struct ops_i2c_dev*i2c_bus,unsigned char data)
等等,入口参数都是i2c_bus,时序实现与常规裸机程序设计是一致的,不同的是函数指针的分离调用,具体看附件源码。
4)标识位
在以往的模拟i2c或者硬件i2c中,操作外设时都有各类情况,如读和写方向的切换、连续操作(不需启动i2c总线,如写EEPROM,先写地址再写数据)等。对于这类情况,我们处理办法是选择相关的宏标识即可,具体实现由“中间层”实现,让i2c外设驱动起来更简单!以上述对外函数为例:
a)通过标识位判断是读还是写状态
if (msg->flags & I2C_BUS_RD)
{//read
ret = i2c_bitops_bus_read(i2c_bus,msg);
if(ret < msg->size)
{
ret = -1;
goto out;
}
}
b)应答状态标识
ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK;
5)读写函数
读写函数最终是通过io口1bit的翻转模拟出时序,从而获得数据,这部分与常规模拟i2c一致,通过函数指针方式操作。主要实现接口函数:
static unsigned long i2c_bitops_bus_write(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message *msg);
static unsigned long i2c_bitops_bus_read(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message *msg);
5.模拟i2c总线实现
此部分实现源码为:i2c_hw.c i2c_hw.h
以stm32f1为硬件平台,采用上述模拟i2c封装,实现一根模拟i2c总线。
1)实现struct ops_i2c_dev函数实体
除了“delayus”函数外,其余为io翻转,以“set_sda”和“delayus”为例,实现如下:
static void gpio_set_sda(int8_t state)
{
if (state)
I2C1_SDA_PORT->BSRR = I2C1_SDA_PIN;
else
I2C1_SDA_PORT->BRR = I2C1_SDA_PIN;
}
static void gpio_delayus(uint32_t us)
{
#if 0
volatile int32_t i;
for (; us > 0; us--)
{
i = 30; //mini 17
while(i--);
}
#else
Delayus(us);
#endif
}
a)为例提高速率,上诉代码采用寄存器方式操作,可以用库函数操作io口;
b)延时可以用硬件定时器延时,或者软件延时,具体根据cpu时钟计算;
c)其他源码看附件中“i2c_hw.c”
2)初始化一根模拟i2c总线
void stm32f1xx_i2c_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitStructure.GPIO_Pin = I2C1_SDA_PIN | I2C1_SCL_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C1_SDA_PORT, &GPIO_InitStructure);
I2C1_SDA_PORT->BSRR = I2C1_SDA_PIN;
I2C1_SCL_PORT->BSRR = I2C1_SCL_PIN;
//device init
ops_i2c1_dev.set_sda = gpio_set_sda;
ops_i2c1_dev.get_sda = gpio_get_sda;
ops_i2c1_dev.set_scl = gpio_set_scl;
ops_i2c1_dev.get_scl = gpio_get_scl;
ops_i2c1_dev.delayus = gpio_delayus;
i2c1_dev.i2c_phy = &ops_i2c1_dev;
i2c1_dev.xfer = ops_i2c_bus_xfer;
}
a)i2c io初始化;
b)i2c设备实例化,其中“ops_i2c1_dev”和“i2c1_dev”即是我们定义的总线设备,后面使用该总线时主要通过“i2c1_dev”实现对底层的调用。
6.驱动EEPROM(AT24C16)
此部分实现源码为:24clxx.c 24clxx.h
上面总线完成后,驱动一个i2c外设可以说就是信手拈来的事情了,而且模拟i2c总线抽象出来后,不需在做重复调试时序的工作。
假设初始化的i2c设备为i2c1_dev。
1)写EEPROM
写一个字节,页写算法详细见源码附件(24clxx.c):
char ee_24clxx_writebyte(u16 addr,u8 data)
{
struct i2c_dev_message ee24_msg[1];
u8 buf[3];
u8 slave_addr;
if(EEPROM_MODEL > 16)
{
slave_addr =EE24CLXX_SLAVE_ADDR;
buf[0] = (addr >>8)& 0xff;
buf[1] = addr & 0xff;
buf[2] = data;
ee24_msg[0].size = 3;
}
else
{
slave_addr = EE24CLXX_SLAVE_ADDR | (addr>>8);
buf[0] = addr & 0xff;
buf[1] = data;
ee24_msg[0].size = 2;
}
ee24_msg[0].addr = slave_addr;
ee24_msg[0].flags = I2C_BUS_WR;
ee24_msg[0].buff = buf;
i2c_bus_xfer(&i2c1_dev,ee24_msg,1);
return 0;
}
2)读EEPROM
voidee_24clxx_readbytes(u16 read_ddr, char* pbuffer, u16 read_size)
{
struct i2c_dev_message ee24_msg[2];
u8 buf[2];
u8 slave_addr;
if(EEPROM_MODEL > 16)
{
slave_addr =EE24CLXX_SLAVE_ADDR;
buf[0] = (read_ddr>>8)& 0xff;
buf[1] = read_ddr& 0xff;
ee24_msg[0].size = 2;
}
else
{
slave_addr =EE24CLXX_SLAVE_ADDR | (read_ddr>>8);
buf[0] = read_ddr & 0xff;
ee24_msg[0].size = 1;
}
ee24_msg[0].buff = buf;
ee24_msg[0].addr = slave_addr;
ee24_msg[0].flags = I2C_BUS_WR;
ee24_msg[1].addr = slave_addr;
ee24_msg[1].flags = I2C_BUS_RD;
ee24_msg[1].buff = (u8*)pbuffer;
ee24_msg[1].size = read_size;
i2c_bus_xfer(&i2c1_dev,ee24_msg,2);
}
3)注意事项
驱动一个外设相对容易了,注意的事项就是标识位部分。
a)此处外设地址(addr),是实际地址,不含读写位(7bit),比如AT24C16外设地址为0x50,可能大家平常用的是0xA0,因为包括读写位;
b)写数据时,如果以2帧i2c_dev_message消息发送,需要注意“I2C_BUS_NO_START”宏,此宏标识意思是不需要再次启动i2c了,一般看i2c外设手册时序图可知道。如写EEPROM是先写地址,然后写数据这个过程是连续的,此时就需用到“I2C_BUS_NO_START”标识。程序可改成这样:
char ee_24clxx_writebyte(u16 addr,u8 data)
{
struct i2c_dev_message ee24_msg[2];
u8 buf[2];
u8 slave_addr;
if(EEPROM_MODEL > 16)
{
slave_addr =EE24CLXX_SLAVE_ADDR;
buf[0] = (addr>>8)& 0xff;
buf[1] = addr &0xff;
ee24_msg[0].size = 2;
}
else
{
slave_addr =EE24CLXX_SLAVE_ADDR | (addr>>8);
buf[0] = addr &0xff;
ee24_msg[0].size = 1;
}
ee24_msg[0].addr = slave_addr;
ee24_msg[0].flags = I2C_BUS_WR;
ee24_msg[0].buff = buf;
ee24_msg[1].addr = slave_addr;
ee24_msg[1].flags = I2C_BUS_WR |I2C_BUS_NO_START;
ee24_msg[1].buff = &data;
ee24_msg[1].size = 1;
i2c_bus_xfer(&i2c1_dev,ee24_msg,2);
return 0;
}
4)其他
理解之后,或者使用过Linux、RT-Thread的驱动框架的,再驱动其他i2c外设,就是很容易的事情了,剩下的就是配置寄存器、应用算法的问题了。
7.总结
1)整体思路比较易理解,本质就是函数指针,将与硬件底层无关的部分抽象出来,相关联的地方分层明确,通过函数指针的方式进行调用。
2)事务分离,通用、重复的事情交给总线处理,特殊任务留给外设驱动。
源码
【1】 https://github.com/Prry/drivers-for-mcu
参考
【1】 https://github.com/RT-Thread/rt-thread
二、STM32中通过IO口模拟串口通信
在项目中需要用到多路的串口使用,而自己的单片机目前来讲没法满足我们项目所需要的串口需求,因此要对普通的GPIO进行转换为UART进行使用。从而使得我们单片机能够得到多一路的串口。
UART的通信方式是由1个起始位,8个数据位,包含一个奇偶校验位,和结束位构成。因此我们将使用单片机中的两个普通的IO口电平的高低进行对相应时序的模拟。
接下来,让我们一起学习如何实现IO模拟串口通信。
添加头文件
首先我们先添加相应的头文件。
#include "stm32f10x.h"
#include "vuart2.h"
宏定义
使用到的io口为:
#define OI2_TXD PDout(6)
#define OI2_RXD PDin(7)
#define BuadRate2_9600 104
#define Recive2_Byte 19 //接收缓冲器的个数
u8 len2 = 0; //接收计数
u8 USART2_buf[Recive2_Byte]; //接收缓冲区
将IO口相应的位带操作函数进行宏定义从而使得在对不同的电平的进行转换的时候更为方便,并且减少了调用其他函数的过程所消耗的时间,程序执行效率更高。
在本次的传输过程中我选用的是使用波特率速率为9600bps,也就是1s中发送9600个数据位(bit),因此对每个位数据进行计算1000000us/9600可以得出,发一个bit的数据需要进行大概需要 104.16us,并且对于相应的电平持续时间要求误差不能超过±5%,因此对我们进行时间的控制要求就显得比较重要了。
枚举出各个位
enum{
COM_START_BIT,
COM_D0_BIT,
COM_D1_BIT,
COM_D2_BIT,
COM_D3_BIT,
COM_D4_BIT,
COM_D5_BIT,
COM_D6_BIT,
COM_D7_BIT,
COM_STOP_BIT,
};
u8 recvStat2 = COM_STOP_BIT;
u8 recvData2 = 0;
IO——TXD进行模拟
void IO2_TXD(u8 Data)
{
u8 i = 0;
OI2_TXD = 0;
delay_us(BuadRate2_9600);
for(i = 0; i < 8; i++)
{
if(Data&0x01)
OI2_TXD = 1;
else
OI2_TXD = 0;
delay_us(BuadRate2_9600);
Data = Data>>1;
}
OI2_TXD = 1;
delay_us(BuadRate2_9600);
}
由于发送的信号是将TXD信号进行拉低处理,因此在拉低TXD相应的IO口之后进行延时处理,再进行循环对我们需要发送的各个位的数据继续进行发送循环发送完成之后将电平拉高代表停止位。
构建发送函数
void USART2_Send(u8 *buf, u8 len2)
{
u8 t;
for(t = 0; t < len2; t++)
{
IO2_TXD(buf[t]);
}
}
其中的*buf为需要发送的数据,len2为数据长度,进行循环调用IO_TXD进行一个字节一个字节的数据发送。
IO口初始化
void IO2Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;//初始化gpio
NVIC_InitTypeDef NVIC_InitStructure;//中断初始化函数
EXTI_InitTypeDef EXTI_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOC, ENABLE); //使能PD,PC端口时钟
//SoftWare Serial TXD
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //选择io口6
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_SetBits(GPIOD,GPIO_Pin_6); //TXD默认电平拉高
//SoftWare Serial RXD
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOD, GPIO_PinSource7); //对D7的下降沿进行中断采样,当接收到下降沿时代表接收到数据触发中断处理函数
EXTI_InitStruct.EXTI_Line = EXTI_Line7;//用到了中断7
EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Falling; //下降沿触发中断
EXTI_InitStruct.EXTI_LineCmd=ENABLE;
EXTI_Init(&EXTI_InitStruct);//初始化中断
NVIC_InitStructure.NVIC_IRQChannel= EXTI9_5_IRQn ; //中断发生于9-5的中断之中
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority =2;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
定时器初始化
void TIM5_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //时钟使能
//定时器TIM5初始化
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
TIM_ClearITPendingBit(TIM5, TIM_FLAG_Update);
TIM_ITConfig(TIM5,TIM_IT_Update,ENABLE ); //使能指定的TIM5中断,允许更新中断
//中断优先级NVIC设置
NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn; //TIM5中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //先占优先级1级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //从优先级1级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC寄存器
}
对TIM5进行初始化操作使得定时器可以检测到各个位的电平持续性时间从而对接收到的数据进行分析。计时结束后进入中断TIM5处理。
外部中断处理函数
void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetFlagStatus(EXTI_Line7) != RESET)//对中断标志位进行采集
{
if(OI2_RXD == 0)
{
if(recvStat2 == COM_STOP_BIT)
{
recvStat2 = COM_START_BIT;//将当前的状态设置为开始位
TIM_Cmd(TIM5, ENABLE);//开启定时器计数
}
}
EXTI_ClearITPendingBit(EXTI_Line7); //清除中断标志
}
}
定时器中断处理函数
void TIM5_IRQHandler(void)
{
if(TIM_GetFlagStatus(TIM5, TIM_FLAG_Update) != RESET)
{
TIM_ClearITPendingBit(TIM5, TIM_FLAG_Update); //清除中断标志位
recvStat2++; //将位置移动到第一位的数据
if(recvStat2 == COM_STOP_BIT)//当运行到停止位时进入
{
TIM_Cmd(TIM5, DISABLE);//停止tim5
USART2_buf[len2++] = recvData2;//将采集到的各个数据传递给USART2_buf
if(len2 > Recive2_Byte-1)//将数据通过回显到串口调试助手中
{
len2 = 0;
USART2_Send(USART2_buf,Recive2_Byte);
}
return;
}
if(OI2_RXD)//采集RXD各个电平
{
recvData2 |= (1 << (recvStat2 - 1));
}else{
recvData2 &= ~(1 << (recvStat2 - 1));
}
}
}
整体代码
vuart2.c:
#include "stm32f10x.h"
#include "vuart2.h"
/**
*软件串口的实现(IO模拟串口)
* 波特率:9600 1-8-N
* TXD : PD6
* RXD : PD7
* 使用外部中断对RXD的下降沿进行触发,使用定时器5按照9600波特率进行定时数据接收。
* Demo功能: 接收11个数据,然后把接收到的数据发送出去
*/
#define OI2_TXD PDout(6)
#define OI2_RXD PDin(7)
#define BuadRate2_9600 104
#define Recive2_Byte 19 //接收缓冲器的个数
u8 len2 = 0; //接收计数
u8 USART2_buf[Recive2_Byte]; //接收缓冲区
enum{
COM_START_BIT,
COM_D0_BIT,
COM_D1_BIT,
COM_D2_BIT,
COM_D3_BIT,
COM_D4_BIT,
COM_D5_BIT,
COM_D6_BIT,
COM_D7_BIT,
COM_STOP_BIT,
};
u8 recvStat2 = COM_STOP_BIT;
u8 recvData2 = 0;
void IO2_TXD(u8 Data)
{
u8 i = 0;
OI2_TXD = 0;
delay_us(BuadRate2_9600);
for(i = 0; i < 8; i++)
{
if(Data&0x01)
OI2_TXD = 1;
else
OI2_TXD = 0;
delay_us(BuadRate2_9600);
Data = Data>>1;
}
OI2_TXD = 1;
delay_us(BuadRate2_9600);
}
void USART2_Send(u8 *buf, u8 len2)
{
u8 t;
for(t = 0; t < len2; t++)
{
IO2_TXD(buf[t]);
}
}
void IO2Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOC, ENABLE); //使能PB,PC端口时钟
//SoftWare Serial TXD
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_SetBits(GPIOD,GPIO_Pin_6);
//SoftWare Serial RXD
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOD, GPIO_PinSource7);
EXTI_InitStruct.EXTI_Line = EXTI_Line7;
EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Falling; //下降沿触发中断
EXTI_InitStruct.EXTI_LineCmd=ENABLE;
EXTI_Init(&EXTI_InitStruct);
NVIC_InitStructure.NVIC_IRQChannel= EXTI9_5_IRQn ;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority =2;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void TIM5_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //时钟使能
//定时器TIM5初始化
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
TIM_ClearITPendingBit(TIM5, TIM_FLAG_Update);
TIM_ITConfig(TIM5,TIM_IT_Update,ENABLE ); //使能指定的TIM5中断,允许更新中断
//中断优先级NVIC设置
NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn; //TIM4中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //先占优先级1级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //从优先级1级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC寄存器
}
void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetFlagStatus(EXTI_Line7) != RESET)
{
if(OI2_RXD == 0)
{
if(recvStat2 == COM_STOP_BIT)
{
recvStat2 = COM_START_BIT;
TIM_Cmd(TIM5, ENABLE);
}
}
EXTI_ClearITPendingBit(EXTI_Line7);
}
}
void TIM5_IRQHandler(void)
{
if(TIM_GetFlagStatus(TIM5, TIM_FLAG_Update) != RESET)
{
TIM_ClearITPendingBit(TIM5, TIM_FLAG_Update);
recvStat2++;
if(recvStat2 == COM_STOP_BIT)
{
TIM_Cmd(TIM5, DISABLE);
USART2_buf[len2++] = recvData2;
if(len2 > Recive2_Byte-1)
{
len2 = 0;
USART2_Send(USART2_buf,Recive2_Byte);
}
return;
}
if(OI2_RXD)
{
recvData2 |= (1 << (recvStat2 - 1));
}else{
recvData2 &= ~(1 << (recvStat2 - 1));
}
}
}
vuart2.h:
#ifndef __VUART2__H
#define __VUART2__H
#include "stm32f10x.h"
void IO2_TXD(u8 Data);
void USART2_Send(u8 *buf, u8 len);
void IO2Config(void);
void TIM5_Int_Init(u16 arr,u16 psc);
#endif
三、UART、TTL、RS232、USB通信协议
串行通信
串行通信技术(Serial Communication),是指通信双方按位bit进行,遵守时序的一种通信方式。串行通信中,将数据按位依次传输, 每位数据占据固定的时间长度,仅用一根接收线和一根发送线就可以完成系统间交换信息。
尽管串行通讯比按字节传输的并行通信慢,但是串行具有通信线路少,布线简便易行,施工方便,结构灵活,系统间协商协议,自由度及灵活度较高的特点。
同步通信:
同步通信是一种连续串行传送数据的通信方式,一次通信传送一帧信息。这里的信息帧与异步通信中的字符帧不同,通常含有若干个数据字符。它们均由同步字符、数据字符和校验字符(CRC)组成。
同步字符位于帧开头,用于确认数据字符的开始。
数据字符在同步字符之后,个数没有限制,由所需传输的数据块长度来决定;
校验字符有1到2个,用于接收端对接收到的字符序列进行正确性的校验。
同步通信的缺点是要求发送时钟和接收时钟保持严格的同步。
异步通信:
异步通信中,有两个比较重要的指标:字符帧格式和波特率,在发送端和接收端要保持一致。
数据通常以字符或者字节为单位组成字符帧传送。字符帧由发送端逐帧发送,通过传输线被接收设备逐帧接收。
发送端和接收端可以由各自的时钟来控制数据的发送和接收,这两个时钟源彼此独立,互不同步。
接收端检测到传输线上发送过来的低电平逻辑"0"(即字符帧起始位)时,确定发送端已开始发送数据,每当接收端收到字符帧中的停止位时,就知道一帧字符已经发送完毕。
虽然异步技术使用简单,但起始和停止位是额外开销,浪费了带宽。
不同接口之间的区别
- USB (Universal Serial Bus,通用串行总线)
是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。
- COM口(串口)
串行接口简称串口,也称串行通信接口(通常指COM接口),是采用串行通信方式的扩展接口。
(1) D型9针串口(DB9):RS-232电平标准
(2)4针串口:TTL电平标准
串口通信
典型的串口通信使用3根线完成,分别是地线GND、发送TXD(transport)、接收RXD(receive)。
由于串口通信是异步的,所以端口能够在一根线上发送数据同时在另一根线上接收数据
串口通信(异步)最重要的参数是波特率、数据位、停止位和奇偶的校验。对于两个需要进行串口通信的端口,这些参数必须匹配,这也是能够实现串口通讯的前提。
串口通信(如RS232接口)的数据传输都是0和1,在单总线、I2C、UART 中都是通过一根线的高低电平来判断逻辑1或者逻辑0,但这种信号线的 GND 再与其他设备形成共地模式的通信,这种共地模式传输容易产生干扰,并且抗干扰性能也比较弱。
而差分通信、支持多机通信、抗干扰强的RS485则能够实现更适合长距离、高速传输。
对于通讯协议,最基本的是把它分为物理层和协议层。
物理层
物理层规定通讯系统中具有机械、电子功能部分的特性, 确保原始数据在物理媒体的传输。
串口通讯的物理层有很多标准及变种,例如RS-232标准主要规定了信号的用途、通讯接口以及信号的电平标准。
在上面的通讯方式中,两个通讯设备的"DB9接口"之间通过串口信号线建立起连接,串口信号线中使用"RS-232标准"传输数据信号。
由于RS-232电平标准的信号不能被控制器直接识别,所以这些信号会经过一个"电平转换芯片"转换成控制器能识别的"TTL标准"的电平信号,才能实现通讯。
RS232与TTL的电平标准:
不平衡传输、相对于信号地的单端通讯、全双工。
RS485 差分信号:
为了提高抗干扰特性和增大传输距离,RS485采用差分信号进行数据传输,是一种半双工通信方式;
在RS485通信网络中一般采用的是主从通信方式,即一个主机带多个从机。
相对于单信号线传输的方式,使用差分信号传输具有如下优点:
1)抗干扰能力强,当外界存在噪声干扰时,几乎会同时耦合到两条信号线上,而接收端只关心两个信号的差值,所以外界的共模噪声可以被完全抵消。
2)能有效抑制它对外部的电磁干扰,同样的道理,由于两根信号的极性相反,他们对外辐射的电磁场可以相互抵消,耦合的越紧密,泄放到外界的电磁能量越少。
3)时序定位精确,由于差分信号的开关变化是位于两个信号的交点,而不像普通单端信号依靠高低两个阈值电压判断,因而受工艺,温度的影响小,能降低时序上的误差,同时也更适合于低幅度信号的电路。
由于差分信号线具有这些优点,所以在USB协议、485协议、以太网协议及CAN协议的物理层中,都使用了差分信号传输。
协议层
串口通讯的数据包由发送设备通过自身的TXD接口传输到接收设备的RXD接口。
在串口通讯的协议层中, 规定了通讯逻辑(数据包的内容),它由起始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式(帧格式)要约定一致才能正常收发数据;通常两个设备之间通信要约定好波特率、数据长度、检验位和停止位。
- 波特率:数据信号对载波的调制速率,串口异步通讯中由于没有时钟信号(如DB9接口中是没有时钟信号的), 所以两个通讯设备之间需要约定好波特率,即每个码元的长度,以便对信号进行解码。比如波特率为9600bps;代表的就是每秒中传输9600bit,也就是相当于每一秒中划分成了9600等份。【比特率 = 波特率 X 单个调制状态对应的二进制位数】
- 起始位:起始位必须是持续一个比特时间的逻辑0电平,标志传输一个字符的开始,接收方可用起始位使自己的接收时钟与发送方的数据同步。
- 数据位:数据位紧跟在起始位之后,是通信中的真正有效信息。数据位的位数可以由通信双方共同约定(通常为5、6、7或8位。传输数据时先传送字符的低位,后传送字符的高位。
- 校验位:在有效数据之后,有一个可选的数据校验位。由于数据通信相对更容易受到外部干扰导致传输数据出现偏差, 可以在传输过程加上校验位来解决这个问题。
(1)奇校验(odd):数据位加上校验位中的“逻辑高位1”的个数保持为奇数;
(2)偶校验(even):数据位加上校验位中的“逻辑高位1”的个数保持为偶数(3)0校验(space):校验位永远是0;
(4)1校验(mark):校验位永远是1;
(5)无校验(noparity):没有校验位。 - 停止位:由0.5、1、1.5或2个逻辑1的数据位表示,只要双方约定一致即可。
- 空闲位:空闲位是指从一个字符的停止位结束到下一个字符的起始位开始,表示线路处于空闲状态,必须由高电平来填充。
例如,对于16进制数据 55aaH,当采用8位数据位、1位停止位传输时,它在信号线上的波形如下图所示。
(先传第一个字节55,再传第二个字节aa,每个字节都是 从低位向高位 逐位传输)
数据传输方向
单工:数据传输只支持数据在一个方向上传输;
半双工:允许数据在两个方向上传输,但某一时刻只允许数据在一个方向上传输,实际上是一种切换方向的单工通信,不需要独立的接收端和发送端,两者可合并为一个端口;
全双工:允许数据同时在两个方向上传输,因此全双工通信是两个单工方式的结合,需要独立的接收端和发送端。
设备间通信
原生的串口通信主要是控制器跟串口的设备或者传感器通信,不需要经过电平转换芯片来转换电平,直接就用TTL电平通信。例如GPS模块、GSM模块、串口转WIFI模块、HC04蓝牙模块等与控制器之间的通讯。
RS232转TTL,通过电平转换芯片将 标准 RS232 串口信号转换为 TTL电平的 RS232 串口信号,不需要安装驱动。
USB转串口(TTL)实现计算机USB接口到物理串口之间的转换。主要用于设备跟电脑通信,电平转换芯片一般有CH340、PL2303、CP2102、FT232 使用的时候电脑端需要安装电平转换芯片的驱动。
串口发送:
串口应用发送数据->USB串口驱动获取数据->驱动将数据经过USB通道发送给USB串口设备->USB串口设备接收到数据通过串口发送
串口接收:
USB串口设备接收串口数据->将串口数据经过USB打包后上传给USB主机->USB串口驱动获取到通过USB上传的串口数据->驱动将数据保存在串口缓冲区提供给串口应用读取
USART、UART简介
通用同步异步收发器(USART,Universal Synchronous Asynchronous Receiver and Transmitter)是一个串行通信设备, 可以灵活地与外部设备进行全双工数据交换。
通用异步收发器(UART,Universal Asynchronous Receiver and Transmitter), 它是在USART基础上裁剪掉了同步通信功能,只有异步通信。
数据通信方式包括同步和异步通信,发送方和接收方按照同一个时钟周期工作就叫同步,发送方和接收方没有提供时钟输出、不按照统一的时钟周期、而各自按照自己的时钟周期工作就叫异步。
异步通信时接收方不必一直在意发送方,发送方需要发送信息时会首先给接收方一个信息开始的起始信号,接收方接收到起始信号后就认为后面紧跟着的就是有效信息,才会开始注意接收信息,直到收到发送方发过来的结束标志。串口通信是属于异步的,这个时候的波特率及数据包规则(帧格式)就显得很重要了。
串口流控
在两个串口设备间传输数据时经常有必要进行数据流控。这可能是受到中间串口通信线路、其中一个设备或者其他存储介质的限制。异步数据流控通常使用的有两种方法。
第一种方法通常称为软件流控,使用特殊字符开始(XON or DC1)或者停止(XOFF or DC3)数据流。这些字符定义参见 ASCII 码表。这些码值在传输文本信息时很有用,但不能在未经特殊编程时用于传输其他类型的信息。
第二种方法称作硬件流控,使用RTS和CTS信号线取代特殊字符。当接收方准备好接收数据时会将RTS置为逻辑0以请求对方发送数据,当未准备好时置为逻辑1,因此发送方会通过检测 CTS 电平状态判断是否可以发送数据。
使用硬件流控至少需要连接的信号线有:GND、RXD、TXD、RTS、CTS。
使用软件流控只需要:GND、RXD、TXD。
四、常用通信协议原理
1 SPI传输
▲ 图1 SPI 数据传输
▲ 图1.2 SPI数据传输(2)
▲ 图1.3 SPI时序信号
2 I²C传输
▲ 图1.2.1 I2C总线以及寻址方式
3 UART传输
▲ 图1.3.1 PC 上通过UART来调试MCU
▲ 图1.3.2 RS-232通过电平转换芯片与MCU通讯
4 红外控制
▲ 图1.4.1 红外控制信号也是一个串行通讯信号
▲ 图1.4.2 红外信号接收与放大整形电路
▲ 图1.4.3 一个使用红外接收光电管控制继电器进行鱼食投喂电路
5 串并转换电路
▲ 图1.5.1 串入、并出移位寄存器
▲ 图1.5.2 由八个D寄存器组成的移位寄存器
▲ 图1.5.4 串行传输示意图
6 其他波形动画
▲ 图1.6.1 PWM控制LED亮度
▲ 图1.6.2 PWM控制LED亮度
▲ 图1.6.3 调幅与调频信号
▲ 图1.6.4 相位调制信号
▲ 图1.6.5 方波边沿抖动波形
五、为什么单片机的I/O口需要驱动
为什么单片机的I/O口需要驱动呢?这个问题需要从I/O口的电气特性上进行解释。
首先,给出单片机典型的I/O口,即P1口电气结构图,如图所示。P1口通常是作为通用I/O口使用,不需要多路转换电路MUX。其输出级电路内部有上拉电阻,与场效应管共同组成输出驱动电路。因此,P1口作为输出时,不需要再外接上拉电阻,而当P1口作为输入口使用时,仍然需要先向锁存器写“1”,截止场效应。
内部上拉电阻阻值很大,经过测量大致在330KΩ左右,而内部电源Vcc仅仅+5V,这样以P1.X高电平驱动发光二极管为例,场效应管截止,相当于Vcc通过330KΩ的电阻向二极管提供电流,5/330*10-3=0.015mA,而二极管的点亮电流为5mA至10mA,这就说明单片机的端口只是驱动TTL电平,不提供或提供很小的驱动电流,所以在带负载时,单片机应当在I/O口加上驱动芯片。