通过串口通信 对TCP传输层以下的理解
这可能是近期暂时最后一篇c嵌入式的文章了
基础的串口使用
参照网上的stm32教程套路引入标准库,初始化芯片手册上对应串口引脚 ,初始化stm32串口功能,然后有数据了就自然在寄存器上,就这样,你的波特率跟对方一样寄存器上就自然不断刷新数据。不及时取数据就自然被冲掉 就这样简单粗暴。要想接住数据打开对应的IRQHandler中断函数即可。由于很快被冲掉代码执行时间不宜过长,正常情况下一般都还是要搞个简单的buffer来存的。也没有啥连接 粘包开始标识结束标识这一说,一般都简单的利用一个定时器来断包。
1 #include "sys.h" 2 #include "usart.h" 3 4 5 #define USAER1_REC_MAX 64 //串口1最大接收数据量 6 7 /* 串口1接收数据缓存结构体 */ 8 struct Usart1_RecData_t{ 9 uint16_t recFlag:1; //数据接收标记 0:接收未完成 1:接收完成等待处理 10 uint16_t recLen:15; //接收数据长度 11 char buff[USAER1_REC_MAX]; //接收数据缓存指针 12 }static usart1Rec = { 13 .recFlag = 0, 14 .recLen = 0, 15 };//定义结构体Usart1_RecData_t 并立即初始化一个变量usart1Rec 16 17 //初始化定时器 18 void TIM2_Int_Init(const uint16_t arr, const uint16_t psc) 19 { 20 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 21 NVIC_InitTypeDef NVIC_InitStructure; 22 23 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //时钟使能 24 25 //定时器TIM2初始化 26 TIM_TimeBaseStructure.TIM_Period = arr; //设置自动重装载值 27 TIM_TimeBaseStructure.TIM_Prescaler = psc; //设置预分频值 28 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim 29 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式 30 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); 31 32 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //使能指定的TIM2中断,允许更新中断 33 34 NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //TIM2中断 35 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 4; //先占优先级4级 36 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级0级 37 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能 38 NVIC_Init(&NVIC_InitStructure); 39 40 TIM_Cmd(TIM2, ENABLE); 41 } 42 43 void USART_SendByte(USART_TypeDef* USARTx, uint16_t Data) { 44 USART_SendData(USARTx, Data); 45 while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);//每发送一个字符都等待数据寄存器空确保发送完成 46 } 47 48 void USART_SendString(USART_TypeDef* USARTx, char* str) { 49 //关于前导判断是没写的 问题不大 ,不可能手动触发 或者手按能达到毫秒级导致数据没有判断而覆盖的吧 50 while (*str != '\0') {//遍历每一个字符直到\0 51 USART_SendByte(USARTx, *str++); 52 } 53 54 while (USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET); 55 } 56 57 void USART_SendString2(USART_TypeDef* USARTx, char* str, uint16_t len) { 58 59 for (uint16_t sendlen = 0; sendlen < len; sendlen++) 60 USART_SendByte(USARTx, *str++); 61 while (USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET); 62 } 63 64 //定时器中断函数 利用定时器来断包 65 void TIM2_IRQHandler(void) 66 { 67 if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源 68 { 69 usart1Rec.recFlag = 1; //标记接收完成 70 71 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIMx的中断待处理位:TIM 中断源 72 TIM_Cmd(TIM2, DISABLE); //关闭定时器2 73 } 74 } 75 76 //串口中断处理函数 77 void USART3_IRQHandler() { 78 79 if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) {//如果可以读了 80 if (usart1Rec.recFlag == 0) { 81 //关键就在这个地方了,如果还没读满,关键就在这一块了 82 //后面的使用只需要usart1Rec.recFlag usart1Rec.buff 即可 83 //满了后 usart1Rec.recFlag 置1 就不再往里填了 免的冲掉数据 84 //注意了关键就在于心跳喂狗 超过10ms 也会认为读完了 定时器进行usart1Rec.recFlag=1 85 //并且关闭定时器,定时器平常是不工作的,开启也仅仅是从此处开启的 纯粹把作为一个喂狗秒表 86 //所以效率跟易用性的话都还是可以的 87 if (usart1Rec.recLen < USAER1_REC_MAX) 88 { 89 TIM_SetCounter(TIM2, 0);//计数器清空,通过这种边读边喂心跳的方式把buf填充满 90 if (usart1Rec.recLen == 0) 91 TIM_Cmd(TIM2, ENABLE);//使能定时器 92 usart1Rec.buff[usart1Rec.recLen++] = (char)USART_ReceiveData(USART3);//接收数据到缓存 93 94 } 95 96 } 97 else 98 { 99 usart1Rec.recFlag = 1; 100 USART_ReceiveData(USART3); 101 } 102 }//下面这句没意义啊 标志位都不能读 还读啥,纯粹只是做做样子 然后放弃这些数据 103 else 104 USART_ReceiveData(USART3); 105 106 } 107 108 109 110 //pData:读取数据的缓存 111 //返 回 值:实际接收的数据大小,0表示未接收到数据 112 uint16_t Get_Usart3RecData(char* pData) 113 { 114 uint16_t len = 0; 115 116 if (usart1Rec.recFlag != 0) //接收一帧数据结束 117 { 118 /* 获取数据 */ 119 len = usart1Rec.recLen; 120 //memset(pData, 0, len); 121 //pData=(char *)malloc(len); 122 memcpy(pData, usart1Rec.buff, len); 123 124 /* 清缓存 */ 125 memset(&usart1Rec, 0, sizeof(usart1Rec)); 126 return len; 127 } 128 else //接收一帧数据未结束 129 return 0; 130 } 131 132 void USART3_Init(void) { 133 //使能两个时钟 134 GPIO_InitTypeDef GPIO_InitStruct; 135 //下面的写法一个意思 136 //1串口和串口引脚的时钟使能 137 // RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_USART1,ENABLE); 138 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); 139 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); 140 141 //为啥 uart1 就必须设置pa9 pa10 ,因为是对应了的 ,芯片手册 厂家就是这么规定的 142 //pa2 usart2_tx //pa3 usart2_rx 143 //pb10 usart3_tx //pb11 usart3_rx 144 //pa9 usart1_tx //pa10 usart1_rx 145 146 //进行串口对应端口引脚的模式设置 147 148 //2gpio端口模式设置 149 GPIO_InitStruct.GPIO_Pin = USART1_GPIO_PIN_TX;//pa9 发送,配置成推挽输出 150 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //GPIO_Mode_Out_PP; 151 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; 152 GPIO_Init(USART_GPIO_PORT, &GPIO_InitStruct); 153 154 //输入 则不需要配置速度 155 GPIO_InitStruct.GPIO_Pin = USART1_GPIO_PIN_RX;//pa10 接收 156 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;//GPIO_Mode_IN_FLOATING; 157 GPIO_Init(USART_GPIO_PORT, &GPIO_InitStruct); 158 159 //3串口参数初始化 160 USART_InitTypeDef USART1_InitStruct; 161 USART1_InitStruct.USART_BaudRate = 9600; 162 USART1_InitStruct.USART_WordLength = USART_WordLength_8b; 163 USART1_InitStruct.USART_StopBits = USART_StopBits_1; 164 USART1_InitStruct.USART_Parity = USART_Parity_No; 165 USART1_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; 166 USART1_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; 167 168 USART_Init(USART3, &USART1_InitStruct); 169 170 //4配置串口接收使用的nvic 171 //开启中断 172 USART_ITConfig(USART3, USART_IT_RXNE, ENABLE); 173 NVIC_InitTypeDef NVIC_InitStruct; 174 NVIC_InitStruct.NVIC_IRQChannel = USART3_IRQn; 175 NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 3;//1; 176 NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;//1; 177 NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; 178 NVIC_Init(&NVIC_InitStruct); 179 180 //通过定时器来进行断续 分包 ,如果超过10ms没有数据来则 结束,作为一个包 181 TIM2_Int_Init(99, 7199); //10ms中断一次 182 memset(usart1Rec.buff, 0, USAER1_REC_MAX); //清除缓存 183 184 //5使能串口 185 USART_Cmd(USART3, ENABLE); 186 187 } 188 189 #pragma import(__use_no_semihosting) 190 struct __FILE 191 { 192 int handle; 193 }; 194 FILE __stdout; 195 void _sys_exit(int x) 196 { 197 x = x; 198 } 199 200 int fputc(int ch, FILE* f) 201 { 202 USART_SendData(USART3, (uint8_t)ch); 203 while (USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET); 204 return (ch); 205 } 206 int fgetc(FILE* f) 207 { 208 while (USART_GetFlagStatus(USART3, USART_FLAG_RXNE) == RESET); 209 return (int)USART_ReceiveData(USART3); 210 }
手持表项目用一种先进的双缓冲手段来收发
1 /* 串口1接收数据缓存结构体 */ 2 struct Usart1_RecData_t{ 3 uint16_t recFlag:1; //数据接收标记 0:接收未完成 1:接收完成等待处理 4 uint16_t recLen:15; //接收数据长度 5 uint8_t buff[USART1_REC_MAX]; //接收数据缓存指针 6 }; 7 8 static struct Usart1_RecData_t usart1Rec[USART1_REC_NUM]; 9 static uint8_t usart1RecCacheNum = 9; //当前使用的缓存空间 10 static uint8_t readCacheNum = 0; //当前读取的缓存 11 static uint8_t sendBuff[2 * USART1_REC_MAX];
处理数据时还可以用信号量来控制硬件资源访问冲突
1 xSemaphoreTake(MutexSemaphore, portMAX_DELAY); //获取互斥信号量 2 relativeHumi = sysData.relativeHumi; 3 pageID = sysData.pageID; 4 xSemaphoreGive(MutexSemaphore); //释放信号量
其它一些深入的理解
虽然学计算机 都会有看的书上写 网络部分的 什么ISO7个层啊 什么传输层 链路层啊 啥啥啥的,但是从来没有专门去背过那玩意儿。也了解平常我们自己使用的socket 基本上处于传输层。
首先对于TCP socket 来说的的话 处在receipt阻塞状态 本身就能够对 对方掉线进行一个感知,利用这点机制 我们也可以send 0字节来对连接状态判断,为什么能够感知 ,TCP协议栈底层已经实现了,你可以理解为像单片机处理TTL电平信号那样 操作系统底层已经编了些程序代码实现了。如果你既不进行发送 也不进行接收动作 则是感知不到离线状态的 ,但是一般都会处于一个接收阻塞循环里,所以严格来说是可以不用心跳处理的。然后另一个关于缓冲,TCP stream 本身 就有缓冲机制 这个也是协议栈帮我们实现了 ,并且网络设备还会自动的根据你receipt的处理速度 进行流控。可以看出来这玩意儿高深的很 原来传输层以下还有这么多机制要处理 ,什么三次握手, 并不只是串口那种无脑的根据频率处理电信号 原来协议栈底层帮我们做了这么多事情。而winsocket 只是操作系统暴露给我们的一个协议栈 数据流接口而已 ,这是传输层的 然后我们的一切工作都基于其之上,就像学校老师教的 他这种接口的方式 也并不是面向对象的 更形象点来说的话 类似打电话或者 管道水流 处理。一般情况下我们只需要简易处理粘包,无需管缓存 和数据断包 甚至无需校验 无需管数据错乱,因为TCP已经实现了数据完整性保证机制。 然后我们只需在之上构建业务即可。
这么多年了想不到自己的水平到现在才将就会写一丁点网络通讯的东西 ,真的是越搞这个越觉得自己资质平平。这么多年的浸淫 socketTCP也算是写了一些了 串口上位机也做过 ,进而做了单片机底层相关,算是有那么点自己独到理解的想法 意味 看法在那吧 。
前面谈论了TCP 。最明显的区别也是常识 以前电脑上玩棋牌游戏 或者qq传文件的时候 我们可以把网线拔掉 等几秒钟甚至十秒钟再插上 ,程序整个交互过程不会受任何影响,对比一下串口简直low逼的就不是一个水平的东西,由于串口是及时性的波特率在那管着的 你保持我这个频率动作自然就能够收到,数据在你的单片机寄存器里就待那么0.001秒的时间 ,如果你单片机运行频率如果低于 那毫无疑问是取不到的,或者你单片机取到了非要用冗长的处理代码干别的那后面的数据丢了我也管不着,对比一下TCP 这真是low逼的不能再low了 这不就是ttl电讯号吗 就是原始人啊 啥串口不串口。但是对比一下TCP 最主要的两点 也只是 加了 数据缓冲 和数据完整性保障。想象一下你自己是一台电脑 网卡的底层那几根儿金属线跟串口有啥区别 说实话没任何区别 网卡底层照串口处理TTL电信号一样处理自己的,只不过low逼的串口到这就结了,TCP则以socket接口为界限 对收到的电信号进行了包装(这个属于网卡固件 或者操作系统系统级提供我们就不去掰扯它了哈,你可以单片机4个IO口 加上 移植LWIP 固件 按照国际通行惯例 处理握手 缓冲 且不说包装成跟socketAPI一样 ,能跟其他TCP互通就成功了一半了,这不就是基于串口电讯号做了一张网卡么)这样基于socketAPI提供给我们应用程序的就是一个非常现代化的东西了 众所周知的 最基本的就有了 数据完整性保障 和数据缓冲 特性。
由此可理解 socket是操作系统暴露的 API ,以及TCP在操作系统层面的底层固件 自动处理握手确保数据收发成功 缓冲, 以及与操作系统嵌合 通过socket暴露给传输层使用 这句意味深长的话了吧。
看了很多的单片机串口代码写的都是非常的普通的不能再普通了,直到看到了手持表的FIFO双指针缓冲读取技术(相对于纯软件层看到的 有了缓冲) ,然后再加上freertos本身提供的互斥量管控(软件层见到的仅仅是一个清晰的统一API的IO调用 也可以多线程。底层确对硬件IO资源进行了管控调度排队处理 随便你上层咋个乱调用 我底层只有两根电线 频率只有那么高),不是low逼串口么 ,怎么回事,怎么样感觉画风是不是有点变了 有那么一点点操作系统底层LWIP固件的影子了,世界的运转机制是如此的相像。