1
上期回顾
上期我们从数据链路层介绍了串口通信面临的四个问题,以及串口通讯是如何优化克服这个问题的。为了更深刻的理解串口通讯协议,我们本期仿照串口通信协议利用GPIO翻转实现模拟串口。
虽然 软件模拟串口非常没有必要 ,连最基本的单片机基本都会配备硬件串口,但是这样子走一遍这个 流程可以加强我们了解串口通信的组成。不喜勿喷!
2
准备工作
为了实现标准串口通信,我们需要实现特定波特率下的通信,也就是固定的时间下发送一位数据以及实现开始位停止位和校验位。
为了实现精准的波特率,我们可以利用单片机的硬件定时器,当波特率设置为9600的时候。我们计算此时的时间应该是:表示每秒传输9600个比特(位)
因此,每一位的时间为:
1秒 ÷ 9600 = 0.0001042秒 = 104.2微秒
因此我们的定时器参数可以这样子设置,这样子可以实现103微秒的定时。至于和标准的104微秒,这多出来的1微秒用以给中断服务函数的语句执行。
并且上期我们说过,实际上略微的偏差并不影响接收端的读写,通过开始位和停止位可以调节这个偏差。
3
程序实现
struct MyUART{ uint8_t Data;//每帧数据 uint8_t Check;//校验位 uint8_t DR;//发送信号 uint8_t Num;//移位 uint8_t CR;//发送完毕} myuart;
我们定义一个结构体定义我们的串口:包括帧数据,校验位,发送标志位,移位以及发送完毕标志位。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){ if(myuart.DR) { switch(myuart.Num) { case 0: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, 0); myuart.Num++; break; case 1: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, (myuart.Data & (1 << (myuart.Num - 1))) ? 1 : 0); myuart.Check+= (myuart.Data & (1 << (myuart.Num - 1))); myuart.Num++; break; case 2: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, (myuart.Data & (1 << (myuart.Num - 1))) ? 1 : 0); myuart.Check+= (myuart.Data & (1 << (myuart.Num - 1))); myuart.Num++; break; case 3: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, (myuart.Data & (1 << (myuart.Num - 1))) ? 1 : 0); myuart.Check+= (myuart.Data & (1 << (myuart.Num - 1))); myuart.Num++; break; case 4: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, (myuart.Data & (1 << (myuart.Num - 1))) ? 1 : 0); myuart.Check+= (myuart.Data & (1 << (myuart.Num - 1))); myuart.Num++; break; case 5: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, (myuart.Data & (1 << (myuart.Num - 1))) ? 1 : 0); myuart.Check+= (myuart.Data & (1 << (myuart.Num - 1))); myuart.Num++; break; case 6: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, (myuart.Data & (1 << (myuart.Num - 1))) ? 1 : 0); myuart.Check+= (myuart.Data & (1 << (myuart.Num - 1))); myuart.Num++; break; case 7: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, (myuart.Data & (1 << (myuart.Num - 1))) ? 1 : 0); myuart.Check+= (myuart.Data & (1 << (myuart.Num - 1))); myuart.Num++; break; case 8: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, (myuart.Data & (1 << (myuart.Num - 1))) ? 1 : 0); myuart.Check+= (myuart.Data & (1 << (myuart.Num - 1))); myuart.Num++; break; case 9: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, myuart.Check%2); myuart.Num++; break; case 10: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, 1); myuart.DR = 0; myuart.Check = 0; myuart.Num=0; myuart.CR=1; break; } }}
在定时器的中断函数中,我们先较为浅薄的使用switch来分别来实现:起始位,8位数据位,校验位以及停止位的IO引脚电平设置。
void print(unsigned char * s,int size){ int num = 0; myuart.DR = 1;//开始发送 while(num<size) { if(myuart.CR)//如果发送结束 { myuart.Data = *(s+num); myuart.DR = 1;//发送开始标志位 myuart.CR = 0;//清空结束标志位 num++; } } }
接着我们定义一个print函数用来测试我们的串口通讯,我们采用轮询的方式检查标志位,并赋值数据来实现。
HAL_TIM_Base_Start_IT(&htim1); myuart.DR = 0; myuart.Check = 0; myuart.Num=0; /* USER CODE END 2 */
/* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */
/* USER CODE BEGIN 3 */ print("Hellorn",8); HAL_Delay(1000); }
打开定时器以及添加我们的串口输出函数。
事实上只要大家充分了解通讯协议的组成,任何的通讯协议都可以用软件模拟的方式来实现。因此大家学习之余也可以了解了解底层,加深其理解٩(๑ᵒ̴̶̷͈᷄ᗨᵒ̴̶̷͈᷅)و