在前两篇文章中我们介绍了IO口模拟串口发送数据和接收数据,前两种方法都是使用定时器来进行发送和接收,没有用到中断,优点是逻辑简单,但是缺点很明显,只能进行单个字节的发送和接收,而且不能同时工作。因此在实际工程中没有什么作用,仅供学习使用。使用中断方式我们可以发送和接收多个字节的数据。
这篇文章我将使用中断的方式进行发送和接收,同样的,由于原理缺陷,这篇文章介绍的方法无法同时接收和发送,而且由于发送会延时,是一个不太好的方法,仅供学习使用。
注意:这篇文章实现的IO口模拟串口无法同时接收和发送数据!如有需要在实际项目中使用IO口模拟串口工作,请移步:
我们使用单片机的外部中断(IO中断)来开启比较中断,在比较中断中逐位接收,直到接收了10位后关闭比较中断,并保存接收的有效字节个数。
同样的,在实现过程中,我们在工程文件夹SimUART中共分了4个文件夹(分别为System:存放系统文件;Project:存放项目文件;User:存放main.c和UserApp.c;My_Lib:存放其它常用的文件)。根据我们将用到的单片机的资源,我们在My_Lib中分了二个文件夹,分别是——IO:存放与IO口相关函数的文件;Time:与定时器和中断相关函数的文件。下面我贴出相关函数的.c文件,而.h文件省略不写,有需要的同学可以根据文章后面的网址下载使用。我的编程环境是IAR,需要自己建立IAR工程。下面详细介绍(Project和System省略不写,其中System只用了stm8s.h)。
同样的,我们建立完工程后需要从main()函数开始,为便于理解,我将使用逻辑伪代码,逻辑伪代码如下:
int main( void ) { 单片机时钟初始化; IO口初始化; 定时器初始化; 中断初始化; while(1) { if( 需要发送的数据数 > 0 ) { 发送; 需要发送的数据数 = 0; } } }
我们首先需要进行初始化的配置All_Config()【在UserApp.c中】,代码如下:
//head file #include "UserApp.h" #include "IO.h" #include "User.h" #include "Time.h" #include "Delay.h" u16 SimUART_SendData = 0xFF; u8 SimUART_SendData_BitNum = 10; u8 RxData_ValidNum = 0; u16 RxDataValue_Temp = 0x0000; //初始化函数 void All_Config( void ) { Clock_Config(); IO_Init(); TIM2_Init(); EXTI_Init(); }
其中User.h是我将自己常用的宏写在了一个文件里面,对应于main.c。在没有接外部时钟的时候,STM8S003F在启动时主时钟默认为HSI RC时钟的8分频,我们这里的初始化仅指定为16MHZ高速内部RC振荡器(HSI),也可以省略不写,Clock_Config()【在UserApp.c中】函数代码如下:
//初始化时钟 选择内部16M晶振 void Clock_Config() { CLK->CKDIVR &= ~( BIT(4) | BIT(3) ); }我选择单片机的PD2作为我的模拟串口的数据发送口,选择PD3作为我的模拟串口的数据接收口, IO_Init()【在IO.c中】函数代码如下:
//head file #include "IO.h" #include "User.h" void IO_Init() { //TXD:TXD位推挽输出 PD2 SimUART_PORT->ODR |= SimUART_PIN_TX; //0000 0100 SimUART_PORT->DDR |= SimUART_PIN_TX; //0000 0100 SimUART_PORT->CR1 |= SimUART_PIN_TX; //0000 0100 SimUART_PORT->CR2 &= ~SimUART_PIN_TX; //0000 0100 //RXD:悬浮输入 高电平 PD3 SimUART_PORT->IDR |= SimUART_PIN_RX; //0000 1000 SimUART_PORT->DDR &= ~SimUART_PIN_RX; //0000 1000 SimUART_PORT->CR1 &= ~SimUART_PIN_RX; //0000 1000 SimUART_PORT->CR2 &= ~SimUART_PIN_RX; //0000 1000 }其中在IO.h中的宏定义为:
//宏定义 #define SimUART_PORT GPIOD #define SimUART_PIN_TX 0X04 //PD2 #define SimUART_PIN_RX 0X08 //PD3 #define SimUART_PIN_RX_0 0X00 //PD3 #define SimUART_PIN_RX_1 0X08 //PD3定时器的初始化和前面一样,具体操作可以见 这里。代码如下:
void TIM2_Init() { CLK->PCKENR1 |= CLK_PCKENR1_TIM2;<span style="white-space:pre"> </span>//使能 TIM2 TIM2->PSCR = 0x04; <span style="white-space:pre"> </span>//16分频 1MHZ 1us TIM2->ARRH = ARRValue_9600 >> 8; <span style="white-space:pre"> </span>//自动装载 每52us复位一次TIM2 TIM2->ARRL = ARRValue_9600; <span style="white-space:pre"> </span>//每1us递减1 TIM2->CNTRH = 0; <span style="white-space:pre"> </span>//定时器清零 TIM2->CNTRL = 0; TIM2->CR1 |= TIM2_CR1_CEN; <span style="white-space:pre"> </span>//开启定时器 }其中Time.h中的宏定义为:
#define ARRValue_9600 104中断初始化 EXTI_Init()【在UserApp.c中】代码如下:
//初始化中断 void Interrupt_Init() { //允许更新中断 //TIM2->CR1 &= ~TIM2_CR1_UDIS; //允许更新 可以不管默认为0 TIM2->IER |= TIM2_IER_UIE; //更新中断使能 //IO口下降中断 初始化 SimUART_PORT->CR2 |= 0x08; //使能外部中断 EXTI->CR1 = 0x80; //仅下降沿触发 //禁止比较中断 TIM2->IER &= ~TIM2_IER_CC1IE; //禁止捕获/比较1 }
根据上面的原理,我们知道:更新中断是在发送函数中打开的,因此更新初始化中使能;IO中断是通过下降沿(串口数据的起始位为低电平)打开的,因此设置成使能和下降沿触发;比较中断实在IO中断中打开的,因此设置成禁止。
完成时钟、IO口、定时器、中断的初始化以后我们就可以开始主体程序的设计了,逻辑伪代码如下:
//发送 函数 void SimUART_SendByte(u8 SendData) { 等待一个字节发送完毕; 第一步:清除 更新更新中断标志位(保证不进入更新中断); 第二步:数据调整(起始位为0,数据位不变,停止位和其它位为1); 第三步: 开启更新中断; } //定时器更新中断 发送接收到的数据 #pragma vector = 对应向量标志位 __interrupt void SimUART_Update_IRQHandler(void) { 第一步:清除 更新中断标志位(保证不进入更新中断); 发送一个位计数; if( 发送位为1 ) { 发送高电平; } esle { 发送低电平; } 移位,发送下一个位; //完成了一个位的发送 if( 发送了10个位 ) { 关闭中断; } }
进入发送函数,首先应该清除更新中断标志位,然后写功能代码,结束前需要打开更新中断,从而去执行更新中断的代码。我们需要考虑为何需要一个延时来等待一个字节完成发送。在中断函数中我们是让一个字节发送完成以后才关闭中断的,如果不延时,可能发生一个字节还没有发送完成,却进入下一个更新中断的情况,因此需要等待,我们直接用一个标志位就能解决。对应向量标志位通过查芯片手册和头文件可以得到。发送函数在UserApp.c中,更新中断在Time.c中,发送部分代码如下:
void SimUART_SendByte(u8 SendData) { while( SimUART_SendData_BitNum < 10 ); //清 更新中断标志位 TIM2->SR1 &= ~TIM2_SR1_UIF; //0000 0000 0000 0000 保证最低位(起始)为0,除数据位后全部为1 SimUART_SendData = ( ( SendData << 1 )| (0xFE00) ); SimUART_SendData_BitNum = 0; //开启更新中断 TIM2-> IER |= TIM2_IER_UIE; }
//定时器更新中断 发送接收到的数据 #pragma vector = TIM2_Updata_vector __interrupt void SimUART_Update_IRQHandler(void) { //第一步,清中断标志位 TIM2->SR1 &= ~TIM2_SR1_UIF; //发送一个位 计数 SimUART_SendData_BitNum++; if( ((SimUART_SendData) & 0X0001) ) //如果是高电平,发高电平 { SimUART_PORT->ODR |= SimUART_PIN_TX; } else //如果是低电平,发低电平 { SimUART_PORT->ODR &= ~SimUART_PIN_TX; } //发送一个位 SimUART_SendData >>= 1; if( 10 <= SimUART_SendData_BitNum ) { TIM2-> IER &= ~TIM2_IER_UIE; } }
接收字节也是一个字节一个字节的接收的,由于单片机的始终可能存在误差(这是不可避免的),因此我们需要像个方法来除去这个误差,我们可以增大采样速度(减少采样时间)来排除,比如:我采集5次(发送数据波特率为9600,采集就要是这个的5倍速度),如果有三个是高电平我们就认为该位是高电平。另一种方法是,我们让采样点始终在一个电平的中间,这就用到了IO中断来开启比较中断。具体实现的逻辑伪代码如下:
//接收数据 //判断一个数据的开始 IO外部中断 #pragma vector = 对应向量标志位 __interrupt void SimUART_IO_IRQHandler(void) { 关闭 IO中断 ; 设置 比较中断 ; 清除 比较中断 中断标志位; 打开 比较中断 ; } //接收数据 比较中断 #pragma vector = 对应向量标志位 __interrupt void SimUART_Capture_IRQHandler(void) { 清除 比较中断 标志位; 接收位个数计数; if( 接收位为1 ) { 将相应的位置1; } if( 接收了10个位 ) { 接收位个数清零; 关 比较中断; 清除 IO中断标志位; 开 IO中断; 接收到的有效字节个数计数; } }
两个中断函数均在Time.c中,具体实现代码如下:
//判断一个数据的开始 IO外部中断 #pragma vector = EXTI3_PD_vector __interrupt void SimUART_IO_IRQHandler(void) { //关闭IO中断 SimUART_PORT->CR2 &= ~0x08; //设置比较中断 TIM2-> CCMR1 &= 0x00; //还是有问题 //TIM2-> CCR1H = 0; TIM2-> CCR1L = TIM2->CNTRL + ( ARRValue_9600/2 ); RxDataValue = 0x0000; //TIM2->CCER1 |= 0x00; TIM2->SR1 &= ~TIM2_SR1_CC1IF; //清中断标志位 TIM2->IER |= TIM2_IER_CC1IE; //使能 捕获/比较中断1 }
//接收数据 比较中断 #pragma vector = TIM2_Capture_vector __interrupt void SimUART_Capture_IRQHandler(void) { //第一步,清中断标志位(防止始终进入中断) TIM2->SR1 &= ~TIM2_SR1_CC1IF; //清中断标志位 RxDataNum++; if( SimUART_PIN_RX_1 == (SimUART_PORT->IDR & SimUART_PIN_RX_1 ) )//其次,接收10个位 { RxDataValue |= ( 0x01 << (RxDataNum) );//该位 置1 } //第二步,读IO输入 //首先,判断是否接收了10个位 if(10 == RxDataNum) //如果是则 { RxDataNum = 0; TIM2->IER &= ~TIM2_IER_CC1IE; //1、关 比较中断 //STM8S没有外中断标志位,STM8L有标志位,因此暂时不需要清中断标志位 SimUART_PORT->CR2 |= 0x08; //2、开 IO中断 //处理有效数据 if( (RxDataValue & 0x0402) == 0x0400 ) { RxDataValue_Temp = ( RxDataValue >> 2 ); RxData_ValidNum++;//接收字有效 节数 } } }
int main( void ) { All_Config(); //初始化 _asm("rim"); //开总中断 while(1) //发送循环 { if( RxData_ValidNum > 0x00 ) { SimUART_SendByte( RxDataValue_Temp ); RxData_ValidNum = 0x00; } } //return 0; }
至此我们使用中断的方法来进行IO口模拟串口(未使用库函数)收发数据的功能已经实现,在本文章中,为了方便,我使用我的发送数据来验证我接收数据的正确性,因此先写的发送数据,再写的接收数据。正如前面所说,我是一个位一个位的发送和接受,在发送过程中有发送延时,这样的后果是如果是一个字符串的收发是没问题的,但是由于没有使用缓存区(即一个数组),导致我们收发数据不能分布于各个任务中,代码在实际项目中可能会出现一些问题,例如已接受就得发送,否则会出现错误,这回影响单片机在执行任务时产生问题。我将在后面进行介绍我们在实际工程中能够使用的全双工串口程序。值得注意的是,收发应该是单独存在的,我这里是为了方便反而让我的发送程序发送接收到的数据。
相关代码可以移步下面的地址下载使用,欢迎大家和我一起学习和交流。
/***************************************************************************************************************************************************************************************/
1、修改时间:2015.06.19
作者:Alan
说明:完成文章。