25届嵌入式秋招准备
1.手撕
跟随大神的脚步学习二维数组 | 鲤鱼笔记 (lichangao.com)
前言
刷到二叉树,发现遗忘较快,开始写一些记录吧...大佬们别看,轻喷
二分查找
条件:数据是有序的;只能用来查找单个目标
边界问题解决思路:根据区间的定义是否有效
例如选择[ , ] 左闭右闭来解决,
那么循环成立的核心条件就是while (left<=right),当left与right相等时。
(2)改变边界时,if(target>nums[mid]) -----> 更新left=mid+1,因为mid已经被排除了
力扣35,704
该算法的时间复杂度通常为log n。若要查找序列中的第一个或最后一个出现的目标值,则可基于二分查找的原理分别执行两次查找过程:第一次定位到目标值的位置后即可立即确定其首次出现的位置;随后再从当前位置开始向右(左)方向依次将该目标值及其前后元素依次向右(左)移动一位以确定最后一次出现的位置。
力扣34
链表
相交链表
力扣160
法一:哈希表检测交点:第一个链表从头到尾insert,第二个链表去count
法二:对齐两个链表,同时查找
反转链表
力扣206
想象在不移动原链表的情况下完成翻转,并涉及对原始链表的操作。需要注意的是,在断开前一节点时必须先保存后节点的第一个元素,否则会导致整个链的数据丢失。
ListNode* pre=NULL;
ListNode* cur=head;
while(cur!=NULL)
{
ListNode* temp=cur->next;
cur->next=pre;
pre=cur;
cur=temp;
}
return pre;

回文链表

方法1:存入数组后反转比较
//投机取巧法,把链表的值遍历存入数组,再将数组反转看和原数组是否相等
class Solution {
public:
bool isPalindrome(ListNode* head) {
vector<int > st;
while (head)
{
st.push_back(head->val);
head=head->next;
}
//遍历存入
auto t=st;
reverse(t.begin(),t.end());
return t==st;
//反转比较
}
};
方法2:利用栈的特性,存一半,边弹出边比较
class Solution {
public:
bool isPalindrome(ListNode* head) {
stack<ListNode*> stk;
int n=0;
auto cur =head;
while (head)
{
head=head->next;
n++;
}
//计算大小
int k=(n+1)/2;
while (k--)
{
stk.push(cur);
cur=cur->next;
}
if (n%2) stk.pop();
//需对齐,总数为奇数的话弹出中间的
while (cur)
{
if(cur->val!=stk.top()->val) return false;
cur=cur->next;
stk.pop();
}
return true;
}
};
具体步骤如下:首先通过使用快慢指针技术确定整个链表的一半长度;然后反转后半部分,并将其与前半部分对比。
class Solution {
public:
ListNode* endoffirsthalf(ListNode* head)
{
ListNode *fast=head;
ListNode *slow=head;
while (fast->next!=nullptr&&fast->next->next!=nullptr)
{
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
//找到前半部分的末尾
ListNode* reverselist(ListNode * head)
{
ListNode* pre=nullptr;
ListNode* cur=head;
while (cur!=nullptr)
{
ListNode *temp=cur->next;
cur->next=pre;
pre=cur;
cur=temp;
}
return pre;
}
//翻转链表
bool isPalindrome(ListNode* head) {
if (head==nullptr)
return true;
ListNode* halfoffirst=endoffirsthalf(head);
ListNode* halfofsecond=reverselist(halfoffirst->next);
ListNode*p1=head;
ListNode*p2=halfofsecond;
bool result =true;
while (result&&p2!=nullptr)
{
if (p1!=p2)
result=false;
p1=p1->next;
p2=p2->next;
}
//比较
halfoffirst->next=reverselist(halfofsecond);
return result;
}
};
两数相加(力扣2):难点在于对sum值的处理,以及何时停止的判断
二叉树
对称二叉树

首先需要了解什么是轴对称图形,并且能够被左右翻折的现象;然后对其进行分类分析;当两边都存在数字时,则比较左边左边与右边右边。
数组
矩阵置0:使用矩阵的第一行和第一列标记对应行列是否置零,再单独处理
动态规划
动规基础,背包问题,打家劫舍,股票问题,子序列问题
(1)dp[i][j]数组及下标的含义
(2)递推公式
(3)dp数组怎么初始化
(4)dp数组遍历顺序
(5)打印dp数组
斐波那契数,求第n个数
1 1 2 3 5 8
(1)dp[i]的含义:第i个斐波那契数的数值是dp[i]
(2)递推公式:dp[i]=dp[i-1]+dp[i-2]
(3)dp数组的初始化:dp[0]=1,dp[1]=1
(4)确定遍历顺序: 从前向后
爬楼梯
乐,怎么还是类似斐波那契,但是递推公式的获取不算太清晰
递推公式:当前状态可以由前两个状态推导出来
为啥是初始化 需要dp(n+1)?
阶数 方法
0 0
1 1
2 2
3 3
4 5
爬楼梯的最小花费
(1)dp[i]的含义:到第i个台阶的最小花费
(2)递推公式:
如何得到dp[i] ,由dp[i-1]跳一步或者dp[i-2]跳两步
dp[i-1]+cost[i-1]
dp[i-2]+cost[i-2]
取两者间的最小值
dp[i]=min( dp[i-1]+cost[i-1] , dp[i-2]+cost[i-2] )
(3)dp数组的初始化:dp[1],dp[0]均为0
(4)确定遍历顺序: 从前向后 (后面由前面得到)
不同路径

(1)dp[i][j]的含义:从(0,0)到(i,j)有dp[i][j]的方法
(2)递推公式:dp[i][j]=dp[i-1][j]+dp[i][j-1]
(3)dp数组的初始化:最左边和最上边一定要初始化
dp[0][j]=1;
dp[i][0]=1;
(4)确定遍历顺序: 从前向后
> 1. vector<vector<int>> dp(m, vector<int>(n, 0));
>
> 2.
>
>
>
/向量< >中的方括号内部放置的是数组元素的数据类型,在二维数组的情况下,则是第一层数组中存储的数据类型;既然如此,则二维数组的第一层数据类型声明已经完成。/
/右边dp表示数组的名字;m代表第一层数组中的元素数量;这对应于我们所说的二维数组中的行数;而vector
//整句话合起来就是建立了一个包含m行n列(共计m×n个)初始值均为零的一个二维数组。
int m = dp.size(); // dp.size()返回值即为第一层数组中元素的数量(行数)
int n = dp[0].size(); // dp[0].size()返回值即为第一层第一个子容器中的元素数量(列数)
不同路径2

(1)dp[i][j]的含义:从(0,0)到(i,j)有dp[i][j]的方法
(2)递推公式:
多了一个前提条件if(obs[i][j]==0)
dp[i][j]=dp[i-1][j]+dp[i][j-1]

(3)dp数组的初始化:最左边和最上边一定要初始化
dp[0][j]=1;
dp[i][0]=1;
当障碍在第一排和第一列的第i个位置,i往后所有位置都需要置1
所以起始或者终止出现障碍,都是return 0
(4)确定遍历顺序: 从前向后
杨辉三角

(1)dp[i][j]的含义:第i行j列元素的数值
(2)递推公式:dp[i][j]=dp[i-1][j]+dp[i-1][j-1]
(3)dp数组的初始化:每行第一个数
dp[i][0]=1;
(4)确定遍历顺序: 从前向后
注意点就是输出
整数拆分
dp[j]:容量为j 的背包的装的物品价值
dp[j]=max (dp[j],dp[j-weight[i]]+value[i])
非0数初始为非0的最小值,0
遍历顺序第一层物品顺序,第二层背包逆序
2.八股文
2.1C/C++
2.1.1关键字
(a)sizeof 和strlen
(1)sizeof是运算符,计算变量或数据类型占用的字节数,在编译时计算(内存大小)
(2)strlen是函数,计算字符串的长度,在运行期计算(长度)
(3)sizeof 可以是类型函数做参数,而strlen只能是char *,且以/0结尾
(4)字符串里面,sizeof 计算的是长度+ /0,stelen是真实长度
(b)volatile
(c)static
(d)extern
扩展单个C文件中的变量或函数对整个项目的影响力范围,并实现跨文件协作
(1)修饰变量,extern声明这个变量是在其他文件中定义的


(2)extern定义函数,用法一样。
extern void hello();
(e)new/delete,malloc/free
(f)typedef和define
(g)#define 和const,谁定义变量好
(h)struct和union区别
2.1.2
2.2arm体系架构
2.3操作系统
2.4网络编程
TCP/UDP
(a)TCP怎么保证可靠性
TCP被视为一种可靠的数据传输协议,在确保可靠传输的同时,需要解决包括以下几种情况的问题:数据包丢失、数据损坏、重复发送以及数据顺序混乱。
(1)建立连接: 通过三次握手建立连接,保证连接实体真实存在。
(2)数据校验: TCP每一段报文头都有校验和,用于检验报文是否损坏。
(3)序号与确认响应: 每个TCP报文中都包含一个序列编号字段,在接收端能够明确标识已经成功收到的所有数据段,并且能够检测出可能存在遗漏或重复的数据段;对于重复发送的数据段应当予以丢弃;此外,在处理数据段时需要按照其顺序进行排序;而每个发送的数据段都会对应生成一个确认响应字段以表明其已正确接收。
(4)重传机制:在未接收到确认应答消息的情况下,将采用超时重传、快速重传、SACK及D-SAC等技术手段进行数据传输。
超时重传机制:每一次遇到超时重传的情况都会设定下次的超时间隔为前一倍数的两倍。两次超时的情形表明网络状况较差,在这种情况下不宜频繁重复发送数据。
该协议采用快速重传机制。基于数据驱动的方法,在网络传输中出现数据包乱序的情况下,系统会向接收端发送ACK响应。例如,在接收到包含顺序号6、7、8、9的数据包时(注:原文中的"发"被替换为"发送"),服务器会发送所有已接收数据包的顺序号ACK=5(注:原文中的"全都发"被替换为"发送")。由此可知顺序号5丢失了。当客户端连续接收到三个相同的ACK响应,并在超时前未处理完所有丢失的数据段前(注:原文中的"当...时...之前"被拆分为两句话),系统会自动发起重传操作以补充缺失的数据段。
(5)流量控制
若接收端无法及时处理来自发送端的数据包,则可借助滑动窗口机制向发送端发出指令以减缓传输速率,并避免数据包丢失。
接收方返回的ACK中包含自己的接收窗口大小,并根据该大小进行数据发送管理

(6)拥塞控制
拥塞管理机制是专门针对整个网络系统的优化措施,在实际运行过程中能够有效抑制大量数据进入的情况,并最大限度地降低因数据流量过大导致的系统压力。该机制通过动态监控各节点的负载水平,在发现潜在拥塞迹象时及时采取措施限制流量增长幅度;当遇到网络拥堵时适当减少数据包的发送量以缓解压力,并在恢复至理想状态后逐步提升带宽利用率。触发条件:检测到超时重传行为。
有慢启动,拥塞避免,快速重传三种机制。
(b)TCP建立连接和断开连接的过程
2.5linux相关
3.技能学习
主要是操作系统相关,学习linux
感觉还是韦东山更高效一点?看两天再说
ctrl+alt+T打开终端
安装git ubuntu时必须输入密码;但遇到问题(如无法显示密码),只需键入正确的用户名和密码即可。


[可选]
<必须>
目录与文件操作命令
shell命令解析
(1)接收键盘命令并回显
(2)解析输入的字符串,寻找程序,执行程序

pwd print working directory
cd change directory
mkdir make directory
rmdir remove directory
ls list
4.项目准备
毕设
毕设的技术点主要是单片机+卷积网络,项目的深度是有的,就是有点杂乱。
所做的是:通过MPU6050读取加速度数据,并结合摩擦纳米发电机的输出电压来判断人的步态以及是否摔倒。
MPU6050陀螺仪读取数据(iic)
IIC
iic通信协议:基于对话,实现单片机读写外部模块寄存器
两根通信线:SCL(时钟线),SDA(数据线)
半双工 (一根线兼发送和接收),同步(时钟线)
plus:带数据应答,支持总线挂在多设备(一主多从,多主多从)

--------------------------------------------------------------------------------------------------------
所有iic设备的SCL端子均连接在一起,并与SDA端子各自连接在一起,并对SCL与SDA均需配置一个上拉电阻
SCL和SDA都配置为开漏输出模式
---------------------------------------------------------------------------------------------------------
空闲都是高电平,每个数据传输单元都是低电平开始,低电平结束
起始条件:SCL高电平期间,SDA从高电平切换到低电平

终止条件:SCL高电平期间,SDA从低电平切换到高电平

发送一个字节:当处于低电平时,在时钟信号有效期间(TClk),系统会完成以下操作:首先在总数据线(SDA)上按顺序传输数据位(高位先传入),随后释放在低电平状态下的总时钟信号(SCL)。
主控单元在SCL高电平时段读取数据位(当SCL处于高电平时段时),每八个数据位构成一个字节

接收一个字节的操作:当SCL处于低电平时段时,在 master 端按照顺序将八个数据位传输到 SDA 线路上(高位先传输),随后释放 SCL 信号;主控制器在等待 SCL 转换至高电平时段后,在该时段内逐个读取 SDA 线上的八个数据位,并且保证该时段的数据不可更改
主机在接收之前,需要释放SDA

发送应答:主机接收了一个完整的字节,在下一个时钟周期内发送了一位数据位;其中编码为0表示应答;编码为1则表示非应答。

接收应答:
---------------------------------------------------------------------------------------------------------------------------------
主机可以访问任一从机设备,所以每个从机有一个唯一地址名,MPU6050 (1101000)
当存在多个相同设备时,则由后续可配置的部分决定(例如MPU6050, AD⁰引脚分别连接低电位为11₀₁₀₀₀, 高电位为₁₁₀₁₀₀₁)
MPU6050


110 1000 =0x68
读1 写0
(0x68<<1)| (0/1)
寄存器:
(1)采样时钟分频器:分频越小,AD转换越快,数据寄存器刷新越快
sample rate=Gyro output rate /(1+smplrt_div)
(2)电源管理寄存器:复位、睡眠模式、循环模式等
(3)ID号:IIC地址? 0x68
软件IIC:手动翻转GPIO电平
PB10-----SCL
PB11-----SDA
框架:

IIC初始化:SCL SDA都设置开漏输出,都置高电平

封装对SCL操作的函数,方便加延时

同理,SDA的读与写

起始条件:SCL高电平期间,SDA从高电平切换到低电平


除了终止条件,其他每个单元SCL都是低电平结束
发送一个字节:高电平期间读,读完拉低SCL
xxxx xxxx
& 1000 0000
= x000 0000

变成循环

读一个字节

发送/接收 应答


点名功能

这样应答位就是0 点名的是MPU6050设备
从机地址的扫描,可以遍历所有地址,把应答位为0的记录下来
向特定设备发送指令:用于将数据输入至特定位置。具体来说,在slave端设置slave address(Slave Address),然后在reg address的位置输入desired data(Data)。
MPU6050_ADDRESS 0xD0

指定地址读:0xD0|0x01

读寄存器
查芯片手册:ID号为0x75
那么可以uint8_t ID=MPU6050_ReadReg(0x75)
打印出来的ID号为68
写寄存器
解除睡眠模式

写入(0x6B,0x00)
用到的寄存器

获取MPU6050数据
(1)MPU6050初始化
电源管理寄存器1 0x01
电源管理寄存器2 0x00
采样率分频(10分频) 0x09
陀螺仪配置寄存器 000X X000 XX为满量程(0001 1000) 最大量程 0x18
加速度计配置寄存器 同理0x18
(2)获取数据:分别读取6个轴数据寄存器的高位和低位,拼接成16位的数据
//可以使用连续读取一片连续地址的寄存器
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{ //x,y,z加速度值和陀螺仪值
uint8_t DataH, DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//读取数据
*AccX = (DataH << 8) | DataL; //加速度x轴,16位数据,用指针返回数据
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH << 8) | DataL;
}

竖直放置得到 Z轴的加速度为1943
1943/32768=x/16g 测得x 约为1g
IIC的硬件读取
TENG输出电压采集获得卷积模型
def create_cnn(sample_length, channel_number):
# CNN model creation function, parameterized for flexibility.
input_shape = (sample_length, channel_number)
inputs = Input(shape=input_shape)
x = Convolution1D(filters=32, kernel_size=5, padding='same')(inputs)
x = Activation("relu")(x)
x = MaxPooling1D(pool_size=2, strides=2, padding='same')(x)
x = Convolution1D(filters=64, kernel_size=5, padding='same')(x)
x = Activation("relu")(x)
x = MaxPooling1D(pool_size=2, strides=2, padding='same')(x)
x = Convolution1D(filters=128, kernel_size=5, padding='same')(x)
x = Activation("relu")(x)
x = MaxPooling1D(pool_size=2, strides=2, padding='same')(x)
x = Dropout(0.5)(x)
x = Flatten()(x)
x = Dense(200)(x)
x = Activation("relu")(x)
model = Model(inputs, x)
创建CNN模型,接受数据的长度和通道的数量两个参数
卷积层:提取输入数据的特征,设置了卷积核大小=5,以及不同的滤波器
激活函数:引入非线性特征,加快训练速度
池化层:降低计算复杂度的同时保留重要特征
dropout层:丢弃掉一部分神经元,防止过拟合
flatten:将多维变成一维
Dense:整合特征,准备输出最终的模型
训练集:验证集:测试集=70:25:25
两者如何共同决策
LM324实现偏置

因为TENG模块输出了具有正负值的数据,而控制板上的Arduino单片机的ADC模块仅能接收正值信号,在这种情况下就需要对处理电路进行调整和优化以满足工作需求
linux简易相机+HITlinux0.11内核实验
该框架可适配各类主流摄像头的开发需求,并提供三种核心功能:通过内存映射(mmap)模式能显著提升采集速度、直接读取文件数据以及提供用户自定义指针管理。其中内存映射(mmap)模式能显著提升采集速度且适用于连续视频流采集;而通过direct read模式则主要应用于静态图像捕获。
驱动流程
(1)安装v4l2 找到接入的摄像头节点

(2)利用ioctl获取设备格式
在设备驱动程序中,在设备驱动程序中, ioct 是一个负责对设备的I/O通道进行管理的关键函数,它主要负责控制设备的各种特性(如串口速率等)。
用例:int ioctl(int fd, ind cmd, …);
fd表示程序在打开设备时通过open函数得到的文件标识符.cmd表示操作者执行对设备的控制命令.省略号用于补充具体的操作参数信息.
数据采集流程
摄像头驱动会有多个buffer

当采集到数据(产生中断),把INI从空闲链表移出,放入完成链表的尾部
该程序在查询链表中是否存在缓冲区,并将处理后的数据追加到空闲链表的末尾
完整流程:
lwip相关
经典的一句话:Lwip+MAC地址+PHY芯片实现了TCP/IP协议
参考:原文链接:
前言
lwip是在凭借mentors的引导下学习并实践的一项技能;正点原子拥有网课资源但未能有效掌握。
先从网络编程的八股文开始学习,跳转>>2.4网络编程
前提问题
问题1:为什么要用以太网通信?
问题2:既然要用以太网,那什么是以太网?
问题3:IIC、SPI都有自己的协议,那以太网用什么协议通信呢?
问题4:TCP/IP又是什么?
问题5:为什么要对网络进行分层?分层有什么好处?
遵循统一的协议规范使不同操作系统与设备厂商的产品能够互操作兼容。通过层次结构设计使底层网卡能够自主完成特定功能:物理层面采用统一的标准进行编码处理;在网络层面提供统一的标准接口供各IP地址空间使用。同样地,在IP层各IP地址空间独立完成自身功能:向其提供统一的标准接口,并向下(物理层)传递相应的数据包信息。
问题6:网络分了几层?这几层该怎么理解?



问题7:TCP/IP协议这么复杂,STM32就这么点空间,怎么放得下呢?(LWIP)
问题8:LwIP是什么?和TCP/IP有什么关系?
LwIP 全名:Light weight IP(意为轻量化的 TCP/IP 协议),即为瑞典计算机科学院 (SICS) 的 Adam Dunkels 开发的一个小型开源 TCP/IP 协议栈。
LwIP 的设计目的:以极低资源消耗实现功能完整且功能齐全的协议栈。
与 TCP/IP 的区别:
① LwIP 未实现全部 TCP/IP 功能;
② 大幅降低了对内存空间的需求;
③ 可移植于有操作系统环境运行,
也可独立于操作系统运行;
④ 不采用明确分层结构,
各层间的数据结构可见,
如传输层可了解 IP 层封装数据方式,
而 IP 层又能了解链路层封装数据方式,
从而实现了内存区域共享,
尽量避免数据复制,
最大限度减少内存占用。
特点:
① 资源使用率极低,
可灵活移除非必要组件,
运行所需的代码 ROM 仅需 40KB
RAM 约几十 KB;
② 支持较为完善的一系列协议;
③ 实现了多个常见应用:
DHCP 客户端、DNS 客户端、HTTP 服务器等;
④ 提供了三种编程接口:
RAW/Callback API(适用于轻量级任务)
NETCONN API(适合多任务及大数据处理)
Socket API(为基础版本封装,
更易使用但效率较低);
⑤ 极高兼容性:
全部采用 C 语言编写源码;
⑥ 开源免费:
使用者无需承担商业风险;
⑦ 历史发展时间久远
问题9:让你用LwIP协议栈编程,这个LwIP协议栈又是什么意思?
用LwIP提供给用户的接口函数在自己的程序里实现TCP通信的功能
❤问题10:TCP的“三次握手”和“四次挥手”是什么意思?
该协议中的三次握手主要用于建立可靠通信。其核心机制通过三步过程确保双方的数据传输能够正常进行:首先发送方发送一个SYN flag以建立连接;随后双方交换一个SYN-ACK message以完成首次确认;最后发送方发送一个ACK message以完成最终确认。这一过程既保证了通信的可靠性又降低了数据包丢失的风险。
①首先 Client 端发送连接请求报文
②Server 端接受连接后回复 ACK 报文,并为这次连接分配资源。
当Client接收到了Ack信息后会向Server发送Ack信号,并负责资源分配工作;这样就能完成TCP连接的建立过程。
第一次 A:我希望与B进行通信
第二次 B:好的,请您放心地与我沟通(当A接收到这条信息时已经确认了连接的有效性;但目前B方尚无法确定是否能够接收到来自A方的信息)
第三次 A:我能听到您的回复(此时双方均已经确认了连接的有效性)
如果经过五次握手 attempt后仍未得到响应,则判定该会面失败。
四次挥手:为了释放连接
第一次握手:客户端提交一个FIN指令以终止客户端至服务器的数据传输,并切换至FIN_WAIT_1状态。
当Server收到FIN信号时,在完成ACK操作后,Server将过渡到CLOSE_WAIT阶段。
在第三次挥手操作中, Server向Client发送了一个FIN信号用于断开数据传输, 并达到了LAST_ACK状态.
Client接收到FIN信号后,在等待期内响应ACK信息;服务器在收到ACK后切换至CLOSED状态;整个过程完成了四个阶段的通信流程。
- ① 客户端发起断开请求以发送FIN指令
- 服务端接收到数据并回复
- 当服务端发送相关数据时
- 客户端确认接收到数据后立即反馈给服务器
- 在发送完毕后短暂等待两毫秒期间内
*(如果在此期间内接收到来自服务器的ACK报文,则表明服务器已准备好响应)
问题11:有基本的理论基础了,但实际要用STM32+LWIP该怎么操作?
前提准备:集成了MAC和PHY,PHY层芯片:LAN8720,只支持RMII
初步配置学习
利用CubeMX,初步配置-
(1)选择芯片
(2)系统时钟SYS
如果裸机:systick
如果带操作系统:TIM1

(3)开个串口
(4)开以太网外设RMII

利用GPIO配置RMII引脚

(5)PHY Address=0/1
基于实际情况进行配置时,请注意PHY地址和LAN8720的PHYAD引脚的状态取决于其悬空状态是否存在以及是否带有弱下拉。建议在配置前查看原理图以确保正确性。
(6)高级参数
对于PHY芯片,Cubemx预置了LAN8742和DP83848

根据说明书的寄存器配置

再比如设置全双工的100Mb/s,0x2100,查表是在第8位写1,100Mbps是在第13位写1



(7)配置LWip

开发板直接连接电脑:静态分配,所以DHCP关闭
手动配置和电脑一样的IP地址
8720晶振倍频出50M
因为这块板子上使用了25MHz晶振并通过内部锁相环实现自倍波至50MHz的时钟输出,在这种情况下可以直接进行默认设置无需担心其稳定性问题。需要特别注意的是RMII接口必须连接到50MHz的主时钟源如果当前系统并未采用本体晶振则必须借助MCO模块来提供相应的主时钟信号。
测试MX_LWIP_Process()是否能在主函数中启动,并确保在CMD控制台能够正常通信。

(9)为了使用串口调试程序,打印一些信息,我们把串口重定向
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
return ch;
}
(10)实现客户端和服务器端
立足控制块,认清状态转换图,编写回调函数。
客户端回调函数
处理客户端连接过程中出现的错误
static void client_err(void *arg, err_t err) //出现错误时调用这个函数,打印错误信息,并尝试重新连接
{
printf("连接错误!!\n");
printf("尝试重连!!\n");
//连接失败的时候释放TCP控制块的内存
printf("关闭连接,释放TCP控制块内存\n");
//tcp_close(client_pcb);
//重新连接
printf("重新初始化客户端\n");
TCP_Client_Init();
}
客户端发送给服务器端函数
static err_t client_send(void *arg, struct tcp_pcb *tpcb) //发送函数,调用了tcp_write函数
{
uint8_t send_buf[]= "我是客户端,是你的好哥哥\n";
//发送数据到服务器
tcp_write(tpcb, send_buf, sizeof(send_buf), 1);
return ERR_OK;
}
客户端接收服务器端的数据
static err_t client_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
if (p != NULL)
{
/* 接收数据*/
tcp_recved(tpcb, p->tot_len);
/* 返回接收到的数据*/
tcp_write(tpcb, p->payload, p->tot_len, 1);
memset(p->payload, 0 , p->tot_len);
pbuf_free(p);
}
else if (err == ERR_OK)
{
//服务器断开连接
printf("服务器断开连接!\n");
tcp_close(tpcb);
//重新连接
TCP_Client_Init();
}
return ERR_OK;
}
连接函数
static err_t client_connected(void *arg, struct tcp_pcb *pcb, err_t err)
{
printf("connected ok!\n");
//注册一个周期性回调函数
tcp_poll(pcb,client_send,2);
//注册一个接收函数
tcp_recv(pcb,client_recv);
return ERR_OK;
}
初始化
void TCP_Client_Init(void)
{
struct tcp_pcb *client_pcb = NULL; //这一句一定要放在里面,否则会没用
ip4_addr_t server_ip; //因为客户端要主动去连接服务器,所以要知道服务器的IP地址
/* 创建一个TCP控制块 */
client_pcb = tcp_new();
IP4_ADDR(&server_ip, DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3);//合并IP地址
printf("客户端开始连接!\n");
//开始连接
tcp_connect(client_pcb, &server_ip, TCP_CLIENT_PORT, client_connected);
ip_set_option(client_pcb, SOF_KEEPALIVE);
printf("已经调用了tcp_connect函数\n");
//注册异常处理
tcp_err(client_pcb, client_err);
printf("已经注册异常处理函数\n");
}
- 初始化操作:在TCP_Client_Init中执行初始化步骤,并建立TCP控制块以配置服务器的IP地址位置。
- 建立连接后的行为:当成功建立 TCP 连接时,在 client_connected 函数中触发事件绑定机制以完成数据发送与接收功能的注册。
- 数据传输与接收处理:在 client_send 操作中向目标发送数据包,在 client_recv 操作中接收到来的数据包或检测到断开时进行相应的操作。
- 异常处理机制:在 client_err 函数中设计逻辑框架来捕获并记录任何可能出现的异常情况,并采取措施释放相关资源以避免潜在的安全漏洞的同时,在断开后立即发起重连请求
回显服务器
tcpecho_recv 函数用于接收来自客户端的数据报文。接收到数据后会触发 tcp Echo Send 函数以通知客户端数据已接收。随后系统会发送包含固定前缀、原始数据内容以及固定后缀的信息包给客户端,并在后续操作完成后清空缓冲区并释放相关资源变量 pbuf 的占用空间。
static err_t tcpecho_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
if (p != NULL) {
// 更新窗口,通知发送方已经接收到数据
tcp_recved(tpcb, p->tot_len);
// 发送回显数据给客户端
uint8_t send_buf1[] = "我收到了你的信息!是";
uint8_t send_buf2[] = "吗?\n";
tcp_write(tpcb, send_buf1, sizeof(send_buf1), 1); // 发送固定的回复前缀
tcp_write(tpcb, p->payload, p->tot_len, 1); // 发送接收到的数据
tcp_write(tpcb, send_buf2, sizeof(send_buf2), 1); // 发送固定的回复后缀
// 清空接收到的数据缓冲区并释放pbuf
memset(p->payload, 0, p->tot_len);
pbuf_free(p);
} else if (err == ERR_OK) {
// 当p为空且err为ERR_OK时,表示对方主动关闭连接,执行关闭操作
return tcp_close(tpcb);
}
return ERR_OK;
}
tcpecho_accept 函数响应当存在新的连接连接时的事件。该函数配置了tcpecho_recv函数作为接收来自新连接的数据的回调机制。
static err_t tcpecho_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{
// 注册接收数据的回调函数
tcp_recv(newpcb, tcpecho_recv);
return ERR_OK;
}
该函数用于初始化TCP回显服务器。该函数创建一个TCP控制块并将其绑定至任意IP地址以及指定的端口(TCP_ECHO_PORT)。接着将该控制块设置为监听状态并完成注册操作以处理新连接到达时的操作。
void TCP_Echo_Init(void)
{
struct tcp_pcb *server_pcb = NULL;
// 创建一个TCP控制块
server_pcb = tcp_new();
printf("创建了一个控制块\n");
// 绑定TCP控制块到任意IP地址和指定的端口
tcp_bind(server_pcb, IP_ADDR_ANY, TCP_ECHO_PORT);
printf("已经绑定一个控制块\n");
// 进入监听状态
server_pcb = tcp_listen(server_pcb);
printf("进入监听状态\n");
// 处理连接,注册当有连接到达时的处理函数
tcp_accept(server_pcb, tcpecho_accept);
}
5.lihq嵌入式笔记
C源文件----预处理----编译----汇编----链接----可执行文件
预处理解决 #
./ xx 执行当前路径下的xx文件
精度是丢失
