首页
📁归档
⏳时光机
🚩友链
📫留言
📧订阅本站
推荐
📕考研课程
🏜️ 免费壁纸
❤ 捐助本站
💰资助名单
🎵音乐实验
Search
1
【NPN/PNP三极管】放大电路饱和失真和截止失真的区别
20,043 阅读
2
论文写作中如何把word里面所有数字和字母替换为新罗马字体
10,606 阅读
3
【高数】形心计算公式讲解大全
9,037 阅读
4
Vivado-FPGA Verilog烧写固化教程
7,868 阅读
5
【概论】一阶矩、二阶矩原点矩,中心矩区别与概念
7,805 阅读
🪶微语&随笔
励志美文
我的随笔
写作办公
📡电子&通信
嵌入式&系统
通信&信息处理
编程&脚本笔记
⌨️IC&系统
FPGA&ASIC
VLSI&IC验证
EDA&虚拟机
💻电子&计算机
IP&SOC设计
机器学习
软硬件算法
登录
/
注册
📡电子&通信(共117篇)
找到
117
篇与
📡电子&通信
相关的结果
在FPGA上部署轻量BP神经网络-Python与Verilog:信号检测实战
目录 储备知识: 一、背景:为什么要在FPGA上跑神经网络? 二、系统整体架构 三、神经网络模型设计:极致轻量 四、FPGA硬件实现详解4.1 串并转换模块(Serial-to-Parallel 4.2 数据归一化模块 4.3 隐藏层神经元模块(核心计算单元) 4.4 ReLU激活函数模块 4.5 输出层神经元模块 4.6 阈值判断模块 五、关键优化技巧5.1 归一化的"逆向"处理 5.2 权值二值化(BWN)降低功耗 5.3 亚稳态消除 六、实验结果:功耗与性能 七、总结与思考 python和verilog源程序下载 储备知识: 我们在前文讲了BP神经网络算法和激活函数 BP神经网络算法 https://ee.ac.cn/index.php/archives/612.html 激活函数讲解 https://ee.ac.cn/index.php/archives/617.html 一、背景:为什么要在FPGA上跑神经网络? 在无源物联网(Battery-free IoT)和反向散射通信(Backscatter)场景中,传统的信号检测方法(如能量检测)虽然电路简单,但无法区分同频段的多种信号,且受噪声影响大;而基于PC或GPU的深度学习方案虽然精度高,但功耗动辄数十瓦,显然无法部署在依赖能量采集的微瓦级标签上。 核心矛盾在于:缺乏一种检测精度高、功耗极低(毫瓦级)、且能实时处理的解决方案。 答案是将轻量级的BP神经网络部署到低功耗FPGA上,在边缘端完成信号推理。 二、系统整体架构 整个检测系统的链路如下: 目标信号 → 天线 → 阻抗匹配 → LNA放大 → 包络检波(ENV) → ADC采样(1MSPS, 12bit) → FPGA(BP神经网络推理) → 调制控制信号在FPGA内部,BP神经网络负责识别当前输入的512点采样数据是否为LoRa信号的前导码(Preamble)。一旦识别成功,输出控制信号,开启后端的Backscatter调制模块。 FPGA选型:Actel IGLOO/e系列 AGLE600V5-FG484。选择它的原因是: Flash架构:上电即运行,无需外部配置芯片,适合无源系统; 超低功耗:静态功耗仅0.046mW,适合能量采集场景; 资源适中:13824个逻辑单元,足以容纳轻量级网络。 三、神经网络模型设计:极致轻量 为了在FPGA上高效部署,网络结构必须足够"轻"。本文设计的网络结构如下: pasted_1785558315326_lc6zyp.png图片 层级神经元数说明输入层512对应ADC采集的一条信号的512个采样点隐藏层2仅2个神经元,大幅降低计算量输出层1二分类:是目标信号(1) / 非目标信号(0)激活函数:隐藏层和输出层均采用 ReLU。 $$f(x) = \max(0, x)$$ 选择ReLU的原因不仅是训练效果好,更重要的是硬件实现极其简单——只需一个比较器判断正负,无需计算指数或乘法,这对FPGA非常友好。 pasted_1785561816152_bsny25.png图片 训练环境:TensorFlow/Keras,使用Adam优化器和MSE损失函数。在真实环境数据集上训练后,测试集识别准确率可达 95.76%。 pasted_1785561967835_yy5zgm.png图片 四、FPGA硬件实现详解 FPGA内部的神经网络是一个纯推理引擎。本节按数据流方向,逐个拆解每个模块的设计思路和Verilog实现要点。 pasted_1785562098406_gf64mu.png图片 整体RTL结构如下: pasted_1785563631508_f74j4w.png图片 4.1 串并转换模块(Serial-to-Parallel pasted_1785563649652_chqrj8.png图片 ADC(ADS7042)在片选信号CS拉低后,先输出2个前导0,再输出12位有效数据(D11→D0),每个数据位占用一个CLK周期。 设计要点: CS和CLK由FPGA内部产生,CS每16个CLK周期拉低一次; 在CS下降沿开始,用计数器对串行数据移位寄存; 凑齐12位有效数据后,输出一组并行数据 adc_data[11:0]。 波形图 pmhbofe.png图片 module serial_p(input data0_in,input cs,input clk,output[12:0] data0_out,input rst); reg[11:0] temp; reg[12:0] save; integer count; integer cnt; always @(negedge clk or negedge rst) begin if(!rst) begin//reset temp<=12'b000000000000; count<=-1; save<=12'b111111111111; cnt<=-1; end else if(cs&&count%16==0) begin save<={1'b0,temp}; count<=1; cnt<=cnt+1; if(cnt==512) begin cnt<=1; end end else if(cs) begin temp<=12'b0; count<=1; end else if(count<3) count<=count+1; else begin temp<={temp,data0_in}; count<=count+1; end end assign data0_out=save; endmodule 4.2 数据归一化模块 问题:训练时,输入数据被归一化到 [0, 1.8](对应ADC参考电压1.8V)。如果在FPGA中直接用除法做 Vin = adc_data / 4095 * 1.8,会消耗大量逻辑资源(FPGA做除法器代价极高)。 解决方案:在软件端完成归一化,FPGA端只做整数运算。 具体做法: 在Python/Keras训练时,将输入数据 x 归一化为 x_norm = x / 4095 * 1.8; 在提取权值时,将第一层权值 w 除以 1.8 并放大 1024 倍取整; 这样FPGA输入的原始ADC值 adc_data(范围0~4095)直接与预处理后的权值相乘,等效于完成了归一化+加权。 # 创建一个自定义的回调对象 cb = MyCallback() # 以只读方式打开训练数据文件 with open('/content/work/data/data.txt') as traindata: # 读取文件内容 data = traindata.read() # 将文件内容转换为numpy数组,并将数据类型转换为float,并将数组形状调整为(47100, 2) data = np.array(list(data.split()), dtype=float).reshape((47100, 2)) # 创建一个最大最小归一化对象 min_max_scaler = preprocessing.MinMaxScaler() # 归一化 data = data * (1/65535) # 以只读方式打开训练标签文件 with open('/content/work/data/label.txt') as labda: # 读取文件内容 labels = labda.read() # 将文件内容转换为numpy数组,并将数据类型转换为float,并将数组形状调整为(47100, 2) labels = np.array(list(labels.split()), dtype=float).reshape((47100, 2)) # 对data和labels进行乱序,保持它们的对应关系 # 为了保持对应关系,需要先将data和labels沿着第二个维度拼接起来,形成一个(100, 4)的数组 # 然后对这个数组进行乱序,再将它们沿着第二个维度分开,还原成data和labels data_labels = np.concatenate((data, labels), axis=1) np.random.shuffle(data_labels) data = data_labels[:, :2] labels = data_labels[:, 2:]FPGA端实现: 实际上,归一化模块在FPGA中被合并到了权值预处理阶段,硬件上不需要额外的归一化电路。但为了模块化清晰,也可以显式设计一个"电压映射"模块,将12bit ADC码值转换为定点数格式: 工程建议:永远不要在FPGA里做除法。所有线性变换(归一化、缩放)都应在软件端合并到权值中,FPGA只做整数乘加。 4.3 隐藏层神经元模块(核心计算单元) 隐藏层的数学表达式: $$y = \text{ReLU}\left( \sum_{i=1}^{512} x_i \cdot w_i + b \right)$$ , i = 1..512 工程难点: 512次乘累加如果在一个时钟周期完成,需要512个乘法器,资源爆炸; 如果纯串行完成,需要512个时钟周期,一条chirp要512×512=262144个周期,太慢。 解决方案:采用 串行MAC流水线 + 双缓冲FIFO 架构。 设计思路: 权值存储:512个权值和1个偏置在编译时以 reg [31:0] w[0:511] 形式固化在FPGA中; FIFO缓存:ADC每输出一个采样点,立即写入FIFO。FIFO深度设为16即可(只要保证不溢出),因为读出速度(MAC运算)和写入速度(ADC采样)基本匹配; 串行MAC:每个时钟周期从FIFO读出一个数据 xi,与对应的 wi 相乘,累加到累加器 acc; 512个周期后:累加器结果加上偏置 b,送入ReLU模块。 pasted_1785563798085_32317i.png图片 FIFO波形图 pmhqDBt.png图片 关键设计细节: 位宽设计:输入22bit × 权值32bit = 乘法结果54bit,累加512次需要额外9bit,因此累加器至少需要 63bit。如果位宽不够,累加会溢出,导致结果完全错误。 时序约束:乘法器和加法器要满足16MHz时钟的建立保持时间。如果组合逻辑延迟太大,需要插入流水线寄存器(将乘法器和加法器分两级完成)。 双神经元并行:隐藏层有2个神经元,需要例化两个上述模块,各自独立的权值存储和累加器,但共享同一个FIFO输入。 4.4 ReLU激活函数模块 ReLU的硬件实现是整个网络中最简单的部分: pmhqWcj.png图片 module relu ( input wire clk, input wire rst_n, input wire [31:0] x_in, // MAC+bias结果 input wire x_valid, output reg [31:0] y_out, output reg y_valid ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin y_out <= 32'd0; y_valid <= 1'b0; end else begin y_valid <= x_valid; if ($signed(x_in) >= 0) y_out <= x_in; else y_out <= 32'd0; end end endmodule4.5 输出层神经元模块 输出层只有1个神经元,输入是隐藏层2个神经元的输出: $$\text{output} = \text{ReLU}\left( h_1 \cdot w'_1 + h_2 \cdot w'_2 + b' \right)$$ 这个模块与隐藏层类似,但逻辑资源消耗极小。 pmhq7NT.png图片 module nerual_out( clk, rst,x1,x2,data_out ); input clk, rst; input signed[0:49] x1,x2; output signed[79:0] data_out; //<statements> //reg signed[0:28] weight[0:1]; reg signed[0:7] save,save1,save2; localparam bais=-50'd214979034352654; //localparam bais = -26'd6406874; localparam weight0 = 29'd22506522; localparam weight1 =-29'd341584992; //integer bais; integer count; always @(negedge clk or negedge rst) begin if(!rst) //reset,set values and bias begin // bais<=-50'd214979034352654; // weight[0]<=29'd22506522; // weight[1]<=-29'd341584992; save<=0; save1<=0; save2<=0; count<=0; end else begin save1<=x1*weight0; save2<=x2*weight1; count<=count+1; if(count>518) save<=save1+save2+bais;//Multiply the input by the weights and add up the results end end assign data_out=save; endmodule4.6 阈值判断模块 为什么需要阈值判断? 前面提到,我们在保存权值时将所有参数扩大了 1024 倍(即 $2^{10}$)。这意味着: 隐藏层的输出值也被放大了约1024倍; 输出层的计算结果同样被放大; 原本训练时,输出层ReLU后大于0即判定为目标信号; 现在由于数值被放大,不能简单以0为界,需要设定一个等效的阈值。 阈值推导: 原始判定边界:输出 > 0.5(二分类常用阈值); 放大后等效边界:threshold = 0.5 × 1024 × 1024 = 524288; 实际上,由于隐藏层和输出层都放大了1024倍,总放大倍数为 $1024^2 = 1048576$,因此: threshold = 0.5 × 1048576 = 524288。 波形图 pmhqXv9.png图片 五、关键优化技巧 5.1 归一化的"逆向"处理 再次强调这个最重要的优化: 处理方式FPGA资源消耗推荐度FPGA内做除法(adc/4095)极高,需除法器IP❌FPGA内用移位近似除法中等,精度损失大△权值预处理(本文方法)零额外消耗✅具体做法: 训练时归一化:$V_{\text{in}} = \frac{\text{adc_data}}{4095} \times 1.8$; 提取权值时:$w' = \frac{w}{1.8} \times 1024$,取整; FPGA输入:直接使用原始ADC值 $x$(0~4095); 等效运算。 5.2 权值二值化(BWN)降低功耗 为了进一步压缩模型、降低功耗,本文采用了 Binary-Weight-Networks (BWN) 对权值进行二值化: 权值只取 +1 或 -1; 引入缩放因子 α 保证量化后的权值尽可能接近原始浮点权值。 二值化后,乘法运算退化为加减法或符号位判断,FPGA资源占用和动态功耗大幅下降。虽然精度略有损失,但在本场景中仍能保持97%以上的识别率。 二值化乘法器的Verilog实现: // 二值化权值乘法:只需判断符号 wire signed [31:0] product; assign product = bin_weight ? x_in : -x_in; // bin_weight=1为正,0为负相比32bit×32bit的硬件乘法器,这个逻辑几乎不消耗DSP资源。 5.3 亚稳态消除 ADC数据输入和CS信号是异步信号(相对于16MHz系统时钟),必须做双级触发器同步,防止亚稳态传播: reg adc_dat_d1, adc_dat_d2; always @(posedge clk) begin adc_dat_d1 <= adc_dat; // 第一级 adc_dat_d2 <= adc_dat_d1; // 第二级(稳定后使用) end wire adc_dat_sync = adc_dat_d2;六、实验结果:功耗与性能 通过 Actel Libero SOC 对FPGA各模块进行功耗仿真,结果如下: 工作模式静态功耗动态功耗总功耗空闲态0.046 mW00.046 mW正常工作0.046 mW2.79 mW2.835 mWFlash*Freeze模式0.115 mW00.115 mW2.835 mW 的总功耗,对于低功耗设备来说是完全可以接受的。 各子模块动态功耗分解: 串并转换:0.01 mW 隐藏层神经元(含FIFO):0.083 mW ReLU激活:0.067 mW 输出层神经元:0.067 mW 七、总结与思考 本文介绍了一套完整的从算法到硬件的轻量化神经网络部署方案: 算法层:针对低功耗约束,设计了512-2-1极简BP网络,避免使用CNN/RNN等重算力模型; 数据层:在真实环境中采集目标信号,建立含噪样本库,并通过数据增强提升泛化能力; 硬件层:基于Actel低功耗FPGA,用FIFO流水、整数运算、ReLU硬判决、权值二值化等手段,实现了毫瓦级推理; 系统层:与射频前端(LNA、包络检波、ADC)紧密配合,形成完整的标签信号检测链路。 可扩展性:该FPGA神经网络框架不仅可用于目标信号检测,只需更换训练数据集和权值参数,即可用于FMCW、RFID等其他射频信号的识别,具备良好的通用性。 python和verilog源程序下载
FPGA&ASIC
通信&信息处理
机器学习
软硬件算法
# ASIC/FPGA
# 信号处理
# 机器学习
# 软件算法
# 物联网
# Python
刘航宇
2天前
0
14
3
基于51/52单片机电容、电感、电阻参数测量系统设计与Proteus仿真
目录 一、设计任务和要求 二、方案设计与论证2.1 系统总设计框图 2.2 方案一:电桥平衡原理① 电阻的测量:直流电桥平衡原理 ② 电容的测量:交流电桥平衡原理 ③ 电感的测量:交流电桥平衡原理 2.3 方案二:频率转换法(本设计采用)① 电阻的测量:555 构成单稳态 ② 电容的测量:555 构成单稳态 ③ 电感的测量:电容三点式正弦波振荡 2.4 方案对比与选择 三、单元电路设计和参数计算3.1 电阻的检测单元电路设计 3.2 电容的检测单元电路设计 3.3 电感的检测单元电路设计 四、总原理图及元器件清单4.1 总原理图 4.2 元器件清单 五、程序设计5.1 程序流程图 5.2 核心代码中断函数(频率测量) 主函数 显示函数 键盘检测函数 5.3 完整程序清单及protues仿真文件(附录) 六、安装与调试6.1 仿真调试(1)电阻测量电路仿真 (2)电容测量电路仿真 (3)电感测量电路仿真 调试要点 液晶显示电路调试 仿真整体显示 6.2 实物调试电阻检测 电容检测 电感检测 八、参考文献 九、附录-完整软件程序与protues仿真文件 完整程序与仿真文件见文章末尾 一、设计任务和要求 设计并制作一个元器件参数测量仪 电阻阻值测量,范围:100Ω ~ 1MΩ 电容容值测量,范围:100pF ~ 10000pF 测量精度:±5% 电感参数的测量 扩大量程 提高测量精度 二、方案设计与论证 2.1 系统总设计框图 pasted_1784990858481_sxzptf.png图片 图1 系统总设计框图在测量各元件参数时,采用首先把电感、电容、电阻的参数转化为频率[1],再送至单片机检测,检测结束后,送至 LCD1602 进行显示。 2.2 方案一:电桥平衡原理 ① 电阻的测量:直流电桥平衡原理 pasted_1784990980814_5yavx3.png图片 图2 直流电桥平衡电路图根据电路平衡原理,不断调节电位器 R3,使得电表指针指向正中间,再测量电位器电阻值。 ② 电容的测量:交流电桥平衡原理 pasted_1784991076614_jito75.png图片 图3 交流电桥平衡电路图通过调节 Z1、Z2 使电桥平衡。这时电表的读数为零。通过读取 Z1、Z2、Zn 的值,即可得到被测电容的值。 ③ 电感的测量:交流电桥平衡原理 pasted_1784991402619_84r7tl.png图片 图4 交流电桥平衡电路图2.3 方案二:频率转换法(本设计采用) ① 电阻的测量:555 构成单稳态 pasted_1784991953296_lg44ir.png图片 图5 555 定时器构成单稳态电路图根据 555 定时器构成单稳态,产生脉冲波形,通过单片机读取高低电平得出频率,通过公式换算得到电阻阻值。 ② 电容的测量:555 构成单稳态 pasted_1785027612555_za97df.png图片 图6 555 定时器构成单稳态电路图** 根据 555 定时器构成单稳态,产生脉冲波形,通过单片机读取高低电平得出频率,通过公式换算得到电容值。 ③ 电感的测量:电容三点式正弦波振荡 pasted_1785027682239_4u3sw9.png图片 图7 电容三点式正弦波振荡电路图由电容三点式正弦波振荡电路得出频率,通过单片机读取高低电平得出频率,通过公式换算得到电感值。 2.4 方案对比与选择 对比项方案一(电桥法)方案二(频率法)测量精度受人为调节影响大单片机数字化,精度高操作难度调节麻烦,不易操作自动测量,一键显示电感测量不易测得准确值振荡电路,相对稳定数字化不易数字化便于单片机处理结论: 方案二通过单片机读取转化,精确度会明显提高,且便于数字化显示。故本设计选择方案二。 三、单元电路设计和参数计算 3.1 电阻的检测单元电路设计 pasted_1785027698225_mjicut.png图片 图8 555 定时器构成单稳态电路图由 555 单稳态触发公式: $$f=\frac{1}{\ln2(R_{1}+2R_x)C}$$ 得: $$R_x=\frac{1}{2}(\frac{1}{\ln2fC}-R_{1})$$ 从而,可以计算得出电阻值的大小。 3.2 电容的检测单元电路设计 pasted_1785028035648_x380cp.png图片 图9 555 定时器构成单稳态电路图根据 555 定时器构成单稳态,产生脉冲波形,通过单片机读取高低电平得出频率,通过公式换算得到电容值[2]。 由: $$f=\frac{1}{\ln2^*(R_1+2R_2)^*C_x}$$ 若 R1 = R2,得: $$C_x=\frac{1}{3\ln2^*f^*R_1}$$ 从而,可以计算得出电容值的大小。 3.3 电感的检测单元电路设计 pasted_1785028091408_lymeeb.png图片 图10 电容三点式正弦波振荡电路图由 LC 振荡频率公式: $$f=\frac{1}{2\pi\sqrt{\frac{C_1*C_2}{C_1+C_2}*L_x}}$$ 得: $$L_x=\frac{1}{\left(2\pi f\right)^2\frac{C_1*C_2}{C_1+C_2}}$$ 从而,可以计算得出电感值的大小。 四、总原理图及元器件清单 4.1 总原理图 pasted_1785028273962_o8rzuv.png图片 图11 电阻、电容、电感参数测试总原理图 pasted_1785028294514_u0wku4.png图片 图12 测量电阻、电容部分的原理图 pasted_1785028310991_mmmde2.png图片 图13 测量电感以及 ±5V 发生原理图4.2 元器件清单 元件序号型号数量备注U1STC89C52RC1单片机R3A103J1排阻R2, R3, R17, R1810KΩ4电位器X111.0592M1晶振C325V, 10uF1电解电容C1, C2, C11103 (0.01uF)3瓷片电容R1, R13, R1810KΩ3电阻J1, J2, J3, J4—4按键U216021液晶A1, A2NE5552555 定时器R4300Ω1电阻R5, R6510KΩ2电阻C4~C9104 (0.1uF)6瓷片电容R7, R9100KΩ2电阻C1050V, 22uF1电解电容Q1~Q3S9108 (NPN)3三极管R102KΩ1电阻R8, R11, R141KΩ3电阻C12, C1350V, 47uF2电解电容R12, R1939KΩ2电阻R15, R1651Ω2电阻C1425V, 470uF1电解电容U3NE555321运放五、程序设计 5.1 程序流程图 图14 中断程序流程图 pasted_1785028347463_kh1ui9.png图片 图15 主程序流程图 pasted_1785028417008_m02be6.png图片 初始化 (led_init, t_init, keyscan) ↓ 清屏 ↓ 循环: ├── 显示频率 display_f(f) └── 根据 a 值分支: ├── case 1: 计算电阻 R,显示 display_R(R) ├── case 2: 计算电容 C,显示 display_C(C) └── case 3: 计算电感 L,显示 display_L(L)5.2 核心代码 中断函数(频率测量) void T0_count() interrupt 1 { switch(a) { case 1: // 电阻测量 while(R_out); while(!R_out); TH0 = 0; TL0 = 0; while(R_out); while(!R_out); th0 = TH0; tl0 = TL0; TR0 = 0; break; case 2: // 电容测量 while(C_out); while(!C_out); TH0 = 0; TL0 = 0; while(C_out); while(!C_out); th0 = TH0; tl0 = TL0; TR0 = 0; break; case 3: // 电感测量 while(L_out); while(!L_out); TH0 = 0; TL0 = 0; while(L_out); while(!L_out); th0 = TH0; tl0 = TL0; TR0 = 0; break; } f = 1000000.0 / 1.085069 / (th0 * 256 + tl0) + 0.5; }主函数 void main() { led_init(); // 液晶初始化 t_init(); // 定时器初始化 keyscan(); // 按键检测 write_com(0x01); // 清屏 while(1) { display_f(f); switch(a) { case 1: // 电阻: R = (T/0.693 - 150) 换算 R = (ulong)(5000000.0 / 0.6931472 / f - 150 + 0.5); display_R(R); break; case 2: // 电容: C = 换算公式 C = (int)(100000000.0 / 153 / 0.6931472 / f + 0.5); display_C(C); break; case 3: // 电感: L = 1/(4π²f²C) 换算 L = (int)(1000000000000.0 / 0.1 / PI / PI / f / f + 0.5); display_L(L); break; } } }显示函数 // 频率显示 void display_f(ulong f) { uchar count = 0; ulong f0 = f; while(f) { f = f / 10; count++; } for(num = 5 + count; num > 5; num--) { table2[num] = f0 % 10 + 48; f0 = f0 / 10; } write_com(0x80); for(num = 0; num < 6 + count; num++) { write_data(table2[num]); delayms(5); } } // 电阻显示 void display_R(ulong R) { uchar count = 0; ulong R0 = R; while(R) { R = R / 10; count++; } for(num = 6 + count; num > 6; num--) { table3[num] = R0 % 10 + 48; R0 = R0 / 10; } write_com(0x80 + 0x40); for(num = 0; num < 7 + count; num++) { write_data(table3[num]); delayms(5); } } // 电容显示 void display_C(uint C) { uchar count = 0; uint C0 = C; while(C) { C = C / 10; count++; } for(num = 5 + count; num > 5; num--) { table4[num] = C0 % 10 + 48; C0 = C0 / 10; } write_com(0x80 + 0x40); for(num = 0; num < 6 + count; num++) { write_data(table4[num]); delayms(5); } } // 电感显示 void display_L(uint L) { uchar count = 0; uint L0 = L; while(L) { L = L / 10; count++; } for(num = 5 + count; num > 5; num--) { table5[num] = L0 % 10 + 48; L0 = L0 / 10; } write_com(0x80 + 0x40); for(num = 0; num < 6 + count; num++) { write_data(table5[num]); delayms(5); } }键盘检测函数 void keyscan() { if(key_R == 0) // 检测电阻按键 { delayms(10); // 消抖 if(key_R == 0) a = 1; } else if(key_C == 0) // 检测电容按键 { delayms(10); if(key_C == 0) a = 2; } else if(key_L == 0) // 检测电感按键 { delayms(10); if(key_L == 0) a = 3; } else while(key_R && key_C && key_L); // 等待按键按下 }5.3 完整程序清单及protues仿真文件(附录) 见文末附录部分。 六、安装与调试 6.1 仿真调试 (1)电阻测量电路仿真 图16 电阻测量的仿真电路图 pasted_1785028525830_e8xagf.png图片 图17 电阻测量的仿真输出波形 pasted_1785028553380_77yjdf.png图片(2)电容测量电路仿真 图18 电容测量的仿真电路图 pasted_1785028566386_213kgj.png图片 图19 电容测量的仿真输出波形 pasted_1785028577785_fn2fqc.png图片(3)电感测量电路仿真 图20 电感测量的仿真电路图 pasted_1785028588732_9bvoyx.png图片 图21 电感测量的仿真输出波形 pasted_1785028599631_39ilf2.png图片调试要点 接通电源,用示波器观察输出波形,若为方波,则电路焊接无误,否则检查电路[3] 在调试过程中发现,若改变电源电压,输出方波的频率会发生变化,计算出的数值存在一定误差 当 VCC 为 3.25V 左右时误差较小 液晶显示电路调试 将测量电路的输出分别与单片机的 P1^5、P1^6、P1^7 相接,观察液晶是否显示测量结果 在调试过程中发现,电阻、电容的测量误差较小 由于电容三点式震荡电路的频率不稳定,电感测量的误差较大 仿真整体显示 图22 电阻测量显示 pasted_1785028620741_10mwwy.png图片 图23 电容测量显示 pasted_1785028631102_motyub.png图片 图24 电感测量显示 pasted_1785028644198_twwyn4.png图片 图25 电源生成模块显示 pm2vEdK.png图片6.2 实物调试 实物按照仿真电路搭建,通电显示 "Welcome! RCL detector"。 图26 实物开机界面 pm2vmJe.png图片电阻检测 插入 2.7KΩ 电阻 若显示不准确,调节仿真中 R2 对应的电位器即可改变输出频率,使得检测精准 检测显示 2735Ω,可见调试准确 图27 电阻检测显示 pm2vnRH.png图片电容检测 插入 0.1uF 电容 若显示不准确,调节仿真中 R3 对应的电位器即可改变输出频率,使得检测精准 检测显示 101pF,可见调试准确[4] 图28 电容检测显示 pm2vuzd.png图片电感检测 插入 65mH 电感 若显示不准确,调节程序计算公式5,即可改显示结果,使得检测精准 检测显示 65535uH,可见调试准确 图29 电感检测显示 pm2vMQA.png图片八、参考文献 [1]杨沁佳.浅谈电感传感器微位移检测仪的原理与设计[J].数字通信世界,2019(09):143+159. [2]张璐雅,阮景.一种基于555时基电路的振荡器设计[J].集成电路应用,2019,36(12):12-13. [3]谢琪林.浅谈STC89S51单片机时序[J].电子测试,2020(24):139-140. [4]黄江.基于STM32的高精度电容测量仪设计[J].信息技术与信息化,2020(11):140-141. [5]王晓辉,王智永,韩智伟,胡帅可,谢印庆.基于三极管放大原理的电容三点式LC振荡器的设计[J].电子世界,2020(03):206. [6] E. L. Pankratov. An approach to model manufacturing of an enhanced swing differential Colpitts oscillator based on heterostructures to increase density of their elements with account mismatch-induced stress: on optimization of annealing. 2020, :1-19. 九、附录-完整软件程序与protues仿真文件 // ============================================ // RCL 测量仪程序 // 基于 STC89C52RC + NE555 + LCD1602 // ============================================ #include <reg52.h> #define uint unsigned int #define uchar unsigned char #define ulong unsigned long #define PI 3.1415926 // ========== 字符串常量 ========== uchar code table1[8] = "Welcome! RCL detector"; uchar table2[16] = "f(Hz)="; uchar table3[16] = "R(Ohm)="; uchar table4[16] = "C(pF)="; uchar table5[16] = "L(uH)=";
嵌入式&系统
通信&信息处理
软硬件算法
# 嵌入式
# 信号处理
# 软件算法
# 物联网
刘航宇
7月26日
0
26
5
2026-07-24
利用图像处理技术mfc设计细胞识别程序-C++
目录 一、图像处理设计目标1.1 课题与技术指标 1.2 任务点 二、设计思路 三、创建工程及算法分析3.1 预备工作及工程创建 3.2 打开 BMP 图片与显示 HSI 值代码分析 主要代码 3.3 Mark - 细胞标记代码分析 主要代码 3.4 OnTwoValue() - 二值化代码分析 状态标记说明 主要代码 3.5 OnFillHoles() - 填洞代码分析 主要代码 3.6 shrink() - 收缩代码分析 主要代码 3.7 findcenter() - 中心点获取代码分析 合并中心点 去除多余圆或者错误圆 主要代码 3.8 count() - 统计代码分析 3.9 OnCellprgAll() - 按序执行全部 四、错误情况及分析4.1 未声明全局变量信息头 4.2 错把指针当作 int 型 4.3 软件菜单栏突然丢失 4.4 矩形框在鼠标未按下就出现了 五、总结 参考文献 一、图像处理设计目标 1.1 课题与技术指标 课题: 利用图像处理技术设计细胞识别程序。 设计指标: 实验 VS2019 MFC 开发平台 待识别图像为 24bit 的真彩色细胞图像进行处理 要求识别出细胞,并且保证准确度情况下,统计出细胞的个数和大小 1.2 任务点 完成图像标注:添加 cdib.h、cdib.cpp、添加函数、添加消息响应函数、添加菜单、打开图像 完成细胞的判别:持续判断 Maybe 点邻域有无 Mark 点、Sobel 计算边缘、删除孤立边缘、生成黑白图像 细胞收缩:利用 stack、vector 获取孔洞坐标、填充孔洞、Edge 处置 0、4 方向、8 方向交替生成边缘 细胞中心点信息获取:递归算法、判别局部是否全是边缘、储存全部中心位置 错误信息的删除:计算中心点均值、半径、去除半径过小的点、包含圆的剔除、相交过大圆的剔除、信息统计与显示 二、设计思路 图像处理之细胞识别的主要工作就是识别细胞数目以及选择出指定大小细胞。设计思路如下图所示。同时我们要去除不符合要求的中心点,来确定最终的细胞数目,去除太小和太大的细胞1,并结合实际来修改各种参数,设计合理。 pasted_1784860654942_5veaow.png图片 图1 系统设计思路 (流程图:打开图像 → HSI 转换 → 细胞标记 → 二值化 → 填洞 → 收缩 → 中心点获取 → 错误删除 → 统计输出)三、创建工程及算法分析 3.1 预备工作及工程创建 (1)VS2019 安装 MFC 与 C++ 开发环境 (2)创建 MFC 工程,配置 MFC 应用程序向导 pasted_1784860679771_dmm4ha.png图片 pasted_1784860708914_t33nmc.png图片 图2 创建工程 / 图3 MFC 配置3.2 打开 BMP 图片与显示 HSI 值 代码分析 (1)在 OnDraw 函数实现显示图片;在 messages 中添加 display() 实现打开一幅图像的功能。在 LoadBmp() 函数中利用 fopen 实现自动打开图片,利用 fread 读取图片信息。 (2)通过:查看 → 建立类向导 → 添加 OnMouseMove() 函数,添加代码实现获取所要信息;实现鼠标移动显示 HSI。 (3)通过函数 RgbtoHsi 实现 RGB 向 HSI 的转化。 RGB → HSI 转换原理: RGB 向 HSI 模型的转换是由一个基于笛卡尔直角坐标系的单位立方体向基于圆柱极坐标的双锥体的转换。基本要求是将 RGB 中的亮度因素分离,将色度分解为色调和饱和度,并用角向量表示色调2。如果直接对 R、G、B 处理,其处理过程中很可能会引起三个量不同程度的变化,这样就会产生色差问题,甚至带来颜色上的失真。HSI 模型的出现,使得在保持色彩无失真的情况下实现图像处理成为可能。 pasted_1784860731276_p9qcks.png图片 图4 HSI 显示效果主要代码 // HSI 主要代码示意 void RgbtoHsi(RGB* rgb, HSI* hsi) { // 将 RGB 转换为 HSI 色彩空间 // H: 色调, S: 饱和度, I: 亮度 // 分离亮度因素,保持色彩无失真 }pasted_1784860760614_roywnx.png图片 图5 HSI 主要代码3.3 Mark - 细胞标记 代码分析 确定 Mark 与 mbMark 是通过半径来的,通过计算图像上的某个像素点与细胞内部像素点的欧几里得距离与归一化门限值分别为 0.09 和 0.15 比较: r < 0.09 → Mark(红色,细胞内部) 0.09 < r < 0.15 → mbMark(蓝色,可能边界) OnMouseMove 函数里调用 RgbtoHsi(&rgb, &Hsi) 函数,可以在屏幕上显示鼠标所指点的坐标以及 RGB、HSI 和灰度值,通过 HSI 的可以选取合适的阈值来找到细胞以及边界3。 边缘提取的方法: 先滤波,去除噪声的影响,防止细胞内的噪声的影响 之后用 Sobel 算子 求出边界 之后画一个 5×5 的矩形,如果没有遇到边界,是噪声,去除 边界点是绿色。细胞边界分别用红色、暗红、蓝色和绿色标记出来 主要代码 // Mark 主要代码示意 // 根据 HSI 距离判断像素属性 // Sobel 边缘检测 + 噪声去除 // 多色标记不同区域pasted_1784860785613_yfx4ad.png图片 pasted_1784860803522_x9441d.png图片 pasted_1784860810461_y7dtuy.png图片 图6 Mark 主要代码 / 图7 标记后情况3.4 OnTwoValue() - 二值化 代码分析 背景灰度值为 128,细胞灰度值为 240,边缘为 255。二值化便于轮廓提取。 均值滤波的基本原理是用均值代替原图像中的各个像素值,即对待处理的当前像素点 (x, y),选择一个模板,该模板由其近邻的若干像素组成,求模板中所有像素的均值,再把该均值赋予当前像素点 (x, y),作为处理后图像在该点上的灰度 g(x, y),即 g(x, y) = 1/m ∑f(x, y);m 为该模板中包含当前像素在内的像素总个数。 状态标记说明 标记值含义0x7Xedge 边界(表示边界没 Mark,存在为四边)0x8XMark -- not edge(是细胞)0xfXMark -- edge0xX1visited(已访问)0x2CENTERED(中心点) m_vCenterPoints.at(j) —— 存储最后中心点的地方 主要代码 // 二值化主要代码示意 // 灰度阈值分割 + 均值滤波 // 状态标记管理pasted_1784860833275_8scd1m.png图片 pasted_1784860839706_6y6bwf.png图片 图8 二值化主要代码 / 图9 二值化现象3.5 OnFillHoles() - 填洞 代码分析 在阈值处理时,如果像素在阈值范围内,则像素将被标志。孔洞填充将先统计所有连通的非标志区域面积,总会有一个或者几个面积特别大的区域,其它的都是面积相对较小的区域。较小或者很小的往往就是系统所要填充的孔洞了。 填洞方法: 用栈(stack)、向量(vector)处理 从四个方向遍历,没访问过的黑点进栈,直到边和走完 之后读值到 xt, yt 并去值,v 放的是点的位置 没有碰到边界和小于 100,则填洞,即令洞的各点值为 Mark 值 将细胞中灰度值为 128 的部分的灰度值设置为 2404 填完细胞中的洞(即为 0 的值)后,处理非 Mark 的边界值(目的是打开边缘),设为 0。 主要代码 // 填洞主要代码示意 // stack + vector 获取孔洞坐标 // 连通区域面积统计 // 小区域填充为 Mark 值pasted_1784860863112_8iao9n.png图片 pasted_1784860868652_0m8z7m.png图片 图10 填洞主要代码 / 图11 填洞现象3.6 shrink() - 收缩 代码分析 收缩的目的是为了方便计数。扫描图像,对图像进行预先的 3 次腐蚀,判断所生成边界点,然后根据原理判定是否标注该点,存放所标志的中心点,便于统计细胞个数及计算细胞半径。 算法: 由 Mark 生成边界,我们有四邻域生成边界和八邻域生成边界 判断该点是否为 Mark 点,如果是 Mark 点的话,判断 i, j 是否是我们选取图片的边界,如果是的话,将该点变成边缘点 否则判断它的上下左右(周围八个点)是否有非 Mark 点,如果有,则将这边变成边缘点,反之不变 最后去掉边界则完成收缩 主要代码 // 收缩主要代码示意 // 3次腐蚀预处理 // 4/8邻域边界生成 // 边界点判定与标记pasted_1784860892612_9svgpz.png图片 图12 收缩主要代码及现象3.7 findcenter() - 中心点获取 代码分析 通过判断 MARK 点上下左右四个方向或者八个方向是否有非 Mark 点,如果有的话即认为是边界,将该点加上边界标志。 在收缩的过程(直到没有边界可去)中: 清除所有标志点 在不超过边下,没有被访问过的边缘,四个方向全是非 Mark 的话(收缩 k < 2 时)则是孤立点 k >= 2,是中心点,保存 x, y, 半径 还有除了边界就是背景,这也要保存(就是消失前保存) Mark(i, j): 八个方向,没有访问过的 Mark,且是边界点,则保存 SaveIT(i, j): 这点不是中心点(相当于没有访问过)则设为中心点(并保存);已经是中心点则去除标志,八方向寻找中心点 值得注意的是我们也要去除半径不大于 2 的孤立点,因为我们认为它的半径太小,是噪声。如果是半径大于 2 的孤立点,我们对他进行标记成中心点,对半径做一点补偿(pt.radius = k + pre_shrink_count + 4,4 为补偿),然后入队。合并中心点 本点是中心点,则八个方向看都不是中心点,则保存。有相连的话,多点求中心与半径(用到了 CalcCenterArea(i, j))。 CalcCenterArea(i, j): 八方向求中心点后去除标志,循环统计。半径取最大,而 x, y 则是取平均。 去除多余圆或者错误圆 相近圆: 圆心距离小于 10,则取半径小的中心点为中心点,取半径最大值 + 2 去除潜在的错误: 即半径小于 9 的去掉。但四个方向,边部补偿,防止边部的细胞被去除 多圆相交时,不相交部分小于则去除: 首先,相交部分先取出来保存 之后,单个圆内的交上了被标志(画一个方框然后统计之内的点数) 之后单个圆不相交的进行统计 不相交的占 50% 以下则清除本圆 主要代码 // 中心点获取主要代码示意 // 递归算法判别局部边缘 // 孤立点过滤与半径补偿 // 中心点合并与错误圆剔除pasted_1784860913700_o2gfhm.png图片 pasted_1784860921869_zuiuiy.png图片 图13 中心点获取主要代码及现象3.8 count() - 统计 代码分析 首先图像重新打开,然后获取细胞内部的 HSI 的最大值和最小值,并且计算细胞、平均半径、平均面积等值。 Count() 函数中首先调用了重载函数 reload(),将图片还原,随后将之前细胞识别所保留下来的信息(细胞个数),从 view.h 中提取信息,将细胞数、半径、面积显示在对话框中。 利用前面修正的结果,在 Count 模块进行总的计数,计算出平均半径和平均面积并输出 文件重新加载模块: 如果 noclick 置为 true,则进行重新加载图片 pasted_1784860943157_r86ld7.png图片 pasted_1784860947860_2i9gn5.png图片 pasted_1784860960332_53obi4.png图片 图14 重新加载图片 / 图15 最终标记现象 3.9 OnCellprgAll() - 按序执行全部 void OnCellprgAll() { // 依次调用前面各个函数 // 实现细胞识别的所有操作步骤 }pasted_1784860975641_sjtz6y.png图片 pasted_1784860983894_4m8nzp.png图片 图16 OnCellprgAll()代码分析: 不难看出调用了前面各个函数可以依次实现细胞识别的所有操作步骤。 四、错误情况及分析 4.1 未声明全局变量信息头 pasted_1784861010264_hf1vnq.png图片 图17 错误案例1原因: 缺少必要的头文件包含或全局变量声明 修正: 在适当位置添加全局变量声明 4.2 错把指针当作 int 型 pasted_1784861026701_fl51zf.png图片 图18 错误案例2修正: 应将 int m_pImage; 改为 BYTE* m_pImage; 4.3 软件菜单栏突然丢失 pasted_1784861045886_ob0u8q.png图片 图19 错误案例3原因: 误碰软件,把菜单移出去了 修正: 可以移动菜单栏使其恢复正常 pasted_1784861066297_b3vczn.png图片 图20 修正错误4.4 矩形框在鼠标未按下就出现了 原因: OnMouseMove 中判断条件不对 修正: 检查鼠标按下状态标志位的逻辑判断 五、总结 本项目基于 VS2019 MFC 平台,利用图像处理技术实现了细胞识别程序。主要技术路线包括: HSI 色彩空间转换 —— 分离亮度与色度,保持色彩无失真处理 Sobel 边缘检测 + 噪声过滤 —— 提取细胞边界 二值化与形态学处理 —— 填洞、腐蚀收缩,简化细胞轮廓 递归中心点检测 —— 通过迭代收缩获取细胞中心 后处理优化 —— 去除噪声点、合并相近圆、剔除错误检测 统计输出 —— 细胞个数、平均半径、平均面积 参考文献 1-2 ↩ 朱会平, 陈志远. 基于细胞显微图像的数量统计应用[J]. 实验室科学, 2011, 14(05): 73-75. ↩ 蔡朋杞. 红细胞识别系统的设计与实现[D]. 电子科技大学, 2011. ↩ 赵秋影. 人体细胞识别技术研究[D]. 长春理工大学, 2007. ↩
通信&信息处理
软硬件算法
# 信号处理
# 图像处理
# 通信&射频
刘航宇
7月24日
0
23
2
EBI开发手册|FPGA与STM32 EBI高速并行通信技术详解
适用场景:复杂电子系统 / 高可靠FPGA-MCU协同设计 关键词:FPGA · STM32 · EBI · LocalBus · 异步SRAM · EE学术目录 1. 为什么选EBI而非SPI? 2. EBI系统框图 3. EBI开发手册下载 4. EBI读写时序EBI读时序 EBI写时序 5. EBI硬件连接与信号定义5.1 信号连接表 5.2 硬件设计注意事项 6. FPGA端Verilog接口设计6.1 模块接口定义 6.2 核心设计要点6.2.1 信号直通与数据三态控制 3.2.2 写信号同步 6.2.3 读信号同步 7. 内部模块连接示例 8. STM32端HAL配置与驱动8.1 GPIO初始化 8.2 FMC异步SRAM模式初始化 8.3 读写驱动函数 9. 读写时序分析9.1 读时序 (FPGA → STM32) 9.2 写时序 (STM32 → FPGA) 10. 关键设计决策说明10.1 为什么写信号只用1级同步,读信号用3级? 10.2 三态门控制逻辑 10.3 地址直通 vs 同步 EBI开发手册下载 1. 为什么选EBI而非SPI? 在高可靠嵌入式系统中,STM32与FPGA通信的常见方式有SPI、I2C、UART等串行接口,以及EBI(External Bus Interface)等并行总线。虽然SPI实现简单,但在需要高带宽、低延迟、内存映射访问的场景下,EBI并行总线具有显著优势: 对比维度SPIEBI数据宽度1~4 bit (QSPI)8/16 bit理论带宽~50 MB/s (QSPI)~100+ MB/s访问方式寄存器操作内存映射 (直接读写)DMA支持有限原生支持适用场景控制命令、低速数据图像缓冲、FFT数据流、航电总线对于需要FPGA处理高速数据流(如传感器采集、信号处理)并将结果回传MCU的场景,EBI允许STM32像访问内部SRAM一样直接读写FPGA内部寄存器或FIFO,无需复杂的协议栈开销。这种"内存映射"方式使得CPU可以通过简单的指针操作完成数据交换,几乎零额外软件开销。 2. EBI系统框图 pasted_1784792114679_uqlyxj.png图片 pasted_1784792133238_8k92xm.png图片 3. EBI开发手册下载 见文章末尾 4. EBI读写时序 EBI读时序 pasted_1784792256693_rqlsy7.png图片 EBI写时序 pasted_1784792336779_i0i8ah.png图片 5. EBI硬件连接与信号定义 以STM32F407 + FPGA(Xilinx/Altera)为例,采用16位数据宽度、非复用地址模式,典型连接如下: 5.1 信号连接表 STM32 引脚FPGA 引脚信号说明PD14~PD15, PE7~PE15IO_EBI_data[15:0]EBI数据总线 (16位双向)PF0~PF5, PG0~PG13I_EBI_addr[23:0]EBI地址总线 (24位,可寻址16MB)PG9I_EBI_CS_nEBI片选 (低有效)PD4I_EBI_OE_nEBI读使能 (低有效)PD5I_EBI_WE_nEBI写使能 (低有效)—I_sys_clkFPGA系统时钟 (≥100MHz)—I_rst_n系统复位 (低有效)地址映射:Bank1_NE2 → 0x6400_0000 ~ 0x64FF_FFFF (16MB空间) 5.2 硬件设计注意事项 ⚠️ 关键提示 FPGA的IO电平必须与STM32匹配(通常 3.3V LVTTL/LVCMOS33) 数据线建议串联 22~47Ω 电阻抑制信号反射,尤其是高速运行时 地址线若不需要全部使用,可接地或接固定电平,减少FPGA引脚占用 若使用等待信号(NWAIT),需在EBI配置中使能,并在FPGA中正确驱动 航空电子场景需特别关注 EMC/EMI 设计,建议增加滤波和屏蔽 6. FPGA端Verilog接口设计 FPGA侧的核心任务是模拟一个"SRAM设备",响应STM32的读写时序。以下是基于异步SRAM模式的完整Verilog接口模块,直接对应你的实际工程代码: 6.1 模块接口定义 module EBI_LOCALBUS ( /* --------------------- Input Signals --------------------- */ input I_sys_clk, // 系统时钟 input I_rst_n, // 系统复位 input I_EBI_CS_n, // EBI片选 input I_EBI_WE_n, // EBI写信号 input I_EBI_OE_n, // EBI读信号 input [23 : 00] I_EBI_addr, // EBI地址 (24位) inout [15 : 00] IO_EBI_data, // EBI数据总线 (16位双向) input [15 : 00] I_EBI_RD_data, // CPU读数据 (来自内部模块) /* --------------------- Output Signals -------------------- */ output [15 : 00] O_EBI_WR_data, // CPU写数据 (输出到内部模块) output O_EBI_CS_n, // 片选转接 output reg O_EBI_WE_n, // 写信号同步输出 output O_EBI_RD_n, // 读信号同步输出 output [23 : 00] O_EBI_addr // 地址转接 );6.2 核心设计要点 6.2.1 信号直通与数据三态控制 // EBI片选直通 assign O_EBI_CS_n = I_EBI_CS_n; // EBI地址直通 assign O_EBI_addr = I_EBI_addr; // 写数据:EBI总线 → FPGA内部 (写操作时数据由STM32驱动) assign O_EBI_WR_data = IO_EBI_data; // 读数据:FPGA内部 → EBI总线 (仅在片选有效且读使能有效时驱动) assign IO_EBI_data = ( (I_EBI_CS_n == 1'b0) && (S_EBI_RD_N_d2 == 1'b0) ) ? I_EBI_RD_data : 16'bz;关键设计: O_EBI_CS_n 和 O_EBI_addr 为直通信号,不做时钟域转换,确保地址和片选的最小延迟 IO_EBI_data 采用三态控制:仅在 CS_n == 0 且 RD_N_d2 == 0 时驱动读数据,否则置高阻态,避免总线冲突 O_EBI_WR_data 直接采样 IO_EBI_data,因为写操作时数据总线由STM32驱动,无需三态判断 3.2.2 写信号同步 // EBI写信号打拍同步 always @(posedge I_sys_clk or negedge I_rst_n) begin if (I_rst_n == 1'b0) begin O_EBI_WE_n <= 1'b1; // 复位后写无效 end else begin O_EBI_WE_n <= I_EBI_WE_n; end end设计意图: 写信号经过1级同步,从异步EBI域转入FPGA系统时钟域 同步后的 O_EBI_WE_n 可直接用于内部模块的写使能,配合 O_EBI_addr 和 O_EBI_WR_data 完成一次完整的写操作 6.2.3 读信号同步 reg S_EBI_RD_N_d0; reg S_EBI_RD_N_d1; reg S_EBI_RD_N_d2; // EBI读信号三级打拍 always @(posedge I_sys_clk or negedge I_rst_n) begin if (I_rst_n == 1'b0) begin S_EBI_RD_N_d0 <= 1'b1; S_EBI_RD_N_d1 <= 1'b1; S_EBI_RD_N_d2 <= 1'b1; end else begin S_EBI_RD_N_d0 <= I_EBI_OE_n; S_EBI_RD_N_d1 <= S_EBI_RD_N_d0; S_EBI_RD_N_d2 <= S_EBI_RD_N_d1; end end assign O_EBI_RD_n = S_EBI_RD_N_d2;为什么读信号需要3级同步? 读信号采用三级延迟(而非写信号的1级),原因如下: 同步级数作用S_EBI_RD_N_d0第一级同步,消除亚稳态S_EBI_RD_N_d1第二级同步,稳定信号S_EBI_RD_N_d2第三级延迟,为FPGA内部模块预留数据准备时间读操作需要FPGA内部模块在检测到读使能后,将数据放到 I_EBI_RD_data 上,再通过三态门驱动到 IO_EBI_data。三级延迟确保了: 内部模块有足够的时间响应读请求 数据在 S_EBI_RD_N_d2 == 0 时已经稳定 满足STM32 EBI的数据建立时间要求 7. 内部模块连接示例 EBI_LOCALBUS 模块本身是一个总线桥接器,需要配合内部功能模块使用。以下是一个典型的寄存器文件连接示例: module internal_regfile ( input I_sys_clk, input I_rst_n, input I_cs_n, // 来自 EBI_TO_LOCALBUS.O_EBI_CS_n input I_we_n, // 来自 EBI_TO_LOCALBUS.O_EBI_WE_n input I_rd_n, // 来自 EBI_TO_LOCALBUS.O_EBI_RD_n input [23:0] I_addr, // 来自 EBI_TO_LOCALBUS.O_EBI_addr input [15:0] I_wr_data, // 来自 EBI_TO_LOCALBUS.O_EBI_WR_data output reg [15:0] O_rd_data // 送到 EBI_TO_LOCALBUS.I_EBI_RD_data ); reg [15:0] reg_file [0:255]; // 256个16位寄存器 integer i; initial begin for (i = 0; i < 256; i = i + 1) reg_file[i] = 16'd0; end // 写操作:WE_n下降沿 (同步后的信号) always @(posedge I_sys_clk or negedge I_rst_n) begin if (!I_rst_n) begin // 复位逻辑 end else if (!I_cs_n && !I_we_n) begin reg_file[I_addr[7:0]] <= I_wr_data; end end // 读操作:组合逻辑输出 (确保在RD_n有效时数据已稳定) always @(*) begin if (!I_cs_n && !I_rd_n) O_rd_data = reg_file[I_addr[7:0]]; else O_rd_data = 16'h0000; end endmodule顶层连接: module top_fpga_stm32 ( input sys_clk, input sys_rst_n, // EBI物理接口 inout [15:0] fmc_data, input [23:0] fmc_addr, input fmc_cs_n, input fmc_oe_n, input fmc_we_n ); wire [15:0] ebi_rd_data; wire [15:0] ebi_wr_data; wire ebi_cs_n; wire ebi_we_n; wire ebi_rd_n; wire [23:0] ebi_addr; EBI_LOCALBUS u_ebi_bridge ( .I_sys_clk (sys_clk), .I_rst_n (sys_rst_n), .I_EBI_CS_n (fmc_cs_n), .I_EBI_WE_n (fmc_we_n), .I_EBI_OE_n (fmc_oe_n), .I_EBI_addr (fmc_addr), .IO_EBI_data (fmc_data), .I_EBI_RD_data (ebi_rd_data), .O_EBI_WR_data (ebi_wr_data), .O_EBI_CS_n (ebi_cs_n), .O_EBI_WE_n (ebi_we_n), .O_EBI_RD_n (ebi_rd_n), .O_EBI_addr (ebi_addr) ); internal_regfile u_regfile ( .I_sys_clk (sys_clk), .I_rst_n (sys_rst_n), .I_cs_n (ebi_cs_n), .I_we_n (ebi_we_n), .I_rd_n (ebi_rd_n), .I_addr (ebi_addr), .I_wr_data (ebi_wr_data), .O_rd_data (ebi_rd_data) ); endmodule8. STM32端HAL配置与驱动 STM32侧使用HAL库配置FMC/EBI为异步SRAM模式。以下代码基于STM32F407,使用Bank1_NE2(对应地址 0x64000000): 8.1 GPIO初始化 #include "stm32f4xx_hal.h" #define FPGA_BASE_ADDR 0x64000000 // 内存映射指针 (16位访问) volatile uint16_t *fpga_regs = (volatile uint16_t *)FPGA_BASE_ADDR; static void FMC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOG_CLK_ENABLE(); // 数据总线 D0~D15: PD14/15, PE7~15 GPIO_InitStruct.Pin = GPIO_PIN_14 | GPIO_PIN_15; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF12_FMC; HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); // 地址总线 A0~A5: PF0~PF5 (可扩展至A23) GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5; HAL_GPIO_Init(GPIOF, &GPIO_InitStruct); // 控制信号: NOE(PD4), NWE(PD5), NE2(PG9) GPIO_InitStruct.Pin = GPIO_PIN_4 | GPIO_PIN_5; HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_9; HAL_GPIO_Init(GPIOG, &GPIO_InitStruct); }8.2 FMC异步SRAM模式初始化 void FMC_FPGA_Init(void) { SRAM_HandleTypeDef hsram = {0}; FMC_NORSRAM_TimingTypeDef Timing = {0}; FMC_GPIO_Init(); __HAL_RCC_FMC_CLK_ENABLE(); hsram.Instance = FMC_NORSRAM_DEVICE; hsram.Extended = FMC_NORSRAM_EXTENDED_DEVICE; // Bank1_NE2, 16位数据宽度, SRAM类型 hsram.Init.NSBank = FMC_NORSRAM_BANK2; hsram.Init.DataAddressMux = FMC_DATA_ADDRESS_MUX_DISABLE; // 非复用 hsram.Init.MemoryType = FMC_MEMORY_TYPE_SRAM; hsram.Init.MemoryDataWidth = FMC_NORSRAM_MEM_BUS_WIDTH_16; hsram.Init.BurstAccessMode = FMC_BURST_ACCESS_MODE_DISABLE; hsram.Init.WaitSignalPolarity = FMC_WAIT_SIGNAL_POLARITY_LOW; hsram.Init.WrapMode = FMC_WRAP_MODE_DISABLE; hsram.Init.WaitSignalActive = FMC_WAIT_TIMING_BEFORE_WS; hsram.Init.WriteOperation = FMC_WRITE_OPERATION_ENABLE; hsram.Init.WaitSignal = FMC_WAIT_SIGNAL_DISABLE; hsram.Init.ExtendedMode = FMC_EXTENDED_MODE_DISABLE; hsram.Init.AsynchronousWait = FMC_ASYNCHRONOUS_WAIT_DISABLE; hsram.Init.WriteBurst = FMC_WRITE_BURST_DISABLE; hsram.Init.ContinuousClock = FMC_CONTINUOUS_CLOCK_SYNC_ONLY; // 时序配置 (HCLK = 168MHz, 1周期 ≈ 5.95ns) // 需根据FPGA时序报告调整,满足建立/保持时间 Timing.AddressSetupTime = 2; // 地址建立: ~12ns Timing.AddressHoldTime = 1; // 地址保持 Timing.DataSetupTime = 5; // 数据建立: ~30ns Timing.BusTurnAroundDuration = 1; // 总线周转 Timing.CLKDivision = 2; Timing.DataLatency = 2; Timing.AccessMode = FMC_ACCESS_MODE_A; HAL_SRAM_Init(&hsram, &Timing, &Timing); }8.3 读写驱动函数 /** * @brief 向FPGA寄存器写入16位数据 */ static inline void FPGA_WriteReg(uint16_t reg, uint16_t value) { fpga_regs[reg] = value; __DSB(); // 数据同步屏障,确保写入完成 } /** * @brief 从FPGA寄存器读取16位数据 */ static inline uint16_t FPGA_ReadReg(uint16_t reg) { uint16_t val = fpga_regs[reg]; __DSB(); return val; } /** * @brief 批量数据写入 */ void FPGA_WriteBuffer(uint16_t reg_base, uint16_t *data, uint32_t len) { for (uint32_t i = 0; i < len; i++) { fpga_regs[reg_base + i] = data[i]; } __DSB(); } /** * @brief 批量数据读取 */ void FPGA_ReadBuffer(uint16_t reg_base, uint16_t *data, uint32_t len) { for (uint32_t i = 0; i < len; i++) { data[i] = fpga_regs[reg_base + i]; } __DSB(); }9. 读写时序分析 9.1 读时序 (FPGA → STM32) 读时序要点: I_EBI_OE_n 下降沿后,FPGA内部模块开始准备数据 经过3个时钟周期(d0 → d1 → d2),S_EBI_RD_N_d2 变为低电平 三态门在 S_EBI_RD_N_d2 == 0 时使能,将 I_EBI_RD_data 驱动到 IO_EBI_data STM32在 NOE 上升沿采样数据,需确保数据在采样前已稳定(满足DataSetupTime) 9.2 写时序 (STM32 → FPGA) 写时序要点: I_EBI_WE_n 下降沿表示写周期开始,STM32地址和数据已稳定 O_EBI_WE_n 在1个时钟周期后同步到低电平,内部模块在时钟上升沿检测 !O_EBI_WE_n && !O_EBI_CS_n 执行写操作 O_EBI_WR_data 直接采样 IO_EBI_data,无需额外延迟,因为写操作时数据总线由STM32持续驱动 10. 关键设计决策说明 10.1 为什么写信号只用1级同步,读信号用3级? 信号同步级数原因I_EBI_WE_n → O_EBI_WE_n1级写操作对FPGA是"被动接收",数据总线由STM32驱动,FPGA只需在时钟沿采样稳定的数据即可I_EBI_OE_n → S_EBI_RD_N_d23级读操作对FPGA是"主动输出",需要给内部模块预留时间准备数据,再通过三态门驱动到总线10.2 三态门控制逻辑 assign IO_EBI_data = ( (I_EBI_CS_n == 1'b0) && (S_EBI_RD_N_d2 == 1'b0) ) ? I_EBI_RD_data : 16'bz;使用 S_EBI_RD_N_d2 而非 I_EBI_OE_n 的原因: 直接使用 I_EBI_OE_n 会导致FPGA在STM32还未准备好接收数据时就驱动总线,可能产生总线冲突 使用延迟后的 S_EBI_RD_N_d2 确保:① 内部数据已准备好;② STM32的读使能确实有效;③ 满足数据建立时间 10.3 地址直通 vs 同步 assign O_EBI_addr = I_EBI_addr; // 直通,无时钟延迟地址采用组合逻辑直通而非同步,原因: 地址在EBI访问期间保持稳定,无需跨时钟域处理 减少地址到内部模块的延迟,提高访问速度 地址稳定性由STM32硬件保证 EBI开发手册下载
嵌入式&系统
FPGA&ASIC
# ASIC/FPGA
# 嵌入式
# Verilog
# C/C++
刘航宇
7月23日
0
34
2
2026-06-29
格雷码(Gray Code)原理与应用详解-结合工程领域说明
一、什么是格雷码 格雷码(Gray Code)是一种特殊的二进制编码方式,它的核心特点是相邻两个数之间只有一位二进制位发生变化。这与普通二进制码不同——比如二进制中 3011011011 到 4100100100 有三位同时翻转,而格雷码中相邻数永远只变一位。 二、为什么需要格雷码 普通二进制在数值递增时,多位可能同时翻转,这在硬件世界中会引入一个严重问题:亚稳态和毛刺(Glitch)。 举个例子: • 二进制从 0111 777 变到 1000 888 • 4位同时翻转,由于电路延迟差异,实际可能短暂出现 0101、0010 等错误中间态 • 如果此时被采样,就会读到完全错误的数据 格雷码从根本上避免了这个问题——任何时候只有1位变化,不存在中间错误态。 三、格雷码的主要应用场景 应用场景 为什么用格雷码 图片 四、格雷码 vs 二进制 对照表(3位) 图片 五、FPGA中最经典的用法:异步FIFO // 二进制计数器 → 格雷码转换 → 跨时钟域传输 module gray_code_converter #( parameter WIDTH = 8 )( input WIDTH−1:0WIDTH-1:0WIDTH−1:0 bin, // 二进制计数器值 output WIDTH−1:0WIDTH-1:0WIDTH−1:0 gray // 格雷码输出 ); // 转换公式:gray = bin ^ bin>>1bin >> 1bin>>1 assign gray = bin ^ bin>>1bin >> 1bin>>1; endmodule为什么FIFO指针必须用格雷码? • 写时钟域的写指针要传到读时钟域进行比较 • 如果用二进制,多位同时变化 → 跨时钟域采样可能抓到错误值 → 空满判断出错 → 数据丢失或覆盖 • 格雷码每次只变1位 → 即使采样时机不对,最多差1个地址 → 不会误判空满状态 六、格雷码 ↔ 二进制 转换公式 【二进制 → 格雷码】 G(n) = B(n) ^ B(n+1) // 最高位不变,其余位与高位异或 // 或简写: gray = bin ^ (bin >> 1) 【格雷码 → 二进制】 B(n) = G(n) ^ B(n+1) // 从最高位开始,逐位异或恢复 // 或递推: bin[MSB] = gray[MSB] bin[i] = gray[i] ^ bin[i+1] 七、一句话总结 格雷码不是为了"计算"方便,而是为了"变化安全"——在硬件世界中,让每一次状态切换都干净、无歧义、无毛刺。这是数字电路从"理论正确"走向"工程可靠"的关键一步,也是FPGA/嵌入式设计中跨时钟域、机械编码器等场景的标准做法。
嵌入式&系统
通信&信息处理
# 嵌入式
# 通信&射频
刘航宇
6月29日
0
85
5
2024-07-05
Linux之不使用命令删除文件中的第N行
1.问题描述 2. 解题思路: 3. 代码实现: 1.问题描述 设计一个程序,通过命令行参数接收一个文件名 filename.txt (纯文本文件)和一个整型数字 n,实现从 filename.txt 中删除第 n 行数据。 2. 解题思路: (1) 借助临时文件: 将文件逐行读取,跳过要删除的行,并将其写入临时文件,然后删除源文件,重命名临时文件为源文件,完成删除指定行数据。 (2) 不借助临时文件: 将文件以读写方式打开,读取到要删除行后,通过移动文件指针将文件后面所有行前移一行,但是最后一行会重复,可以通过截断文件操作完成在源文件上删除指定行数据。 (3) 通过sed 或 awk 删除文件指定行 3. 代码实现: (1) 通过 fopen 打开文件借助临时文件删除指定行数据 #filename:ques_15a.c #include <stdio.h> // 包含标准输入输出库 #include <stdlib.h> // 包含标准库函数,比如exit int main(int argc, char *argv[]) { // 检查命令行参数的数量是否正确 if (argc != 3) { printf("Usage: ./a.out filename num\n"); exit(EXIT_FAILURE); // 如果不正确,打印用法信息并退出 } char buf[4096]; // 定义一个足够大的字符数组用于读取文件行 int linenum = atoi(argv[2]); // 将命令行参数中的行号转换为整数 FILE *fp = fopen(argv[1], "r"); // 尝试以只读模式打开源文件 FILE *fpt = fopen("temp.txt", "w"); // 创建一个临时文件用于写入 // 如果源文件无法打开,打印错误信息并退出 if (!fp) { printf("File %s not exist!\n", argv[1]); exit(EXIT_FAILURE); } // 检查是否有权限修改源文件 char str[100]; sprintf(str, "%s%s", "test -w ", argv[1]); if (system(str)) { // 如果没有写权限 printf("Can't modify file %s, permission denied!\n", argv[1]); exit(EXIT_FAILURE); // 打印错误信息并退出 } int total_line = 0; // 初始化文件总行数计数器 while (fgets(buf, sizeof(buf), fp)) { // 读取文件的每一行 total_line++; } fseek(fp, 0, SEEK_SET); // 将文件指针重置到文件的开头 // 如果要删除的行数大于文件的总行数,打印错误信息并退出 if (linenum > total_line) { printf("%d is greater than total_line!\n", linenum); exit(EXIT_FAILURE); } int i = 0; // 初始化当前行计数器 while (fgets(buf, sizeof(buf), fp)) { // 再次读取文件的每一行 i++; // 当前行数加1 if (i != linenum) { // 如果当前行不是要删除的行 fputs(buf, fpt); // 将当前行写入临时文件 } } remove(argv[1]); // 删除原始文件 rename("temp.txt", argv[1]); // 将临时文件重命名为原始文件名 // 关闭文件指针 fclose(fp); fclose(fpt); return 0; // 程序正常退出 }(2) 通过 Linux 系统调用 open 打开文件,需要自定义读取一行的函数,不借助临时文件删除指定行数据 # filename:ques_15b.c #include <stdio.h> // 包含标准输入输出库 #include <stdlib.h> // 包含标准库函数,比如atoi和exit #include <string.h> // 包含字符串操作函数,比如strlen #include <fcntl.h> // 包含文件控制的定义 #include <sys/stat.h> // 包含文件状态的定义 #include <sys/types.h>// 包含各种数据类型 #include <unistd.h> // 包含UNIX标准函数定义 // 函数声明:读取一行文件内容到缓冲区 int readline(int fd, char *buf) { int t = 0; // 用于记录读取的字符数 // 循环读取直到遇到换行符 for (; ;) { read(fd, &buf[t], 1); // 从文件描述符fd读取一个字符到buf t++; // 增加读取的字符数 if (buf[t-1] == '\n') { // 如果读取到换行符 break; // 退出循环 } } return t; // 返回读取的字符数 } // 函数声明:获取文件的大小和行数 int get_file_info(int fd, int *size) { int num = 0; // 记录行数 char ch; // 临时变量用于存储读取的字符 // 循环读取直到文件结束 while (read(fd, &ch, 1) > 0) { (*size)++; // 文件大小加1 if (ch == '\n') { // 如果读取到换行符 num++; // 行数加1 } } return num; // 返回行数 } int main(int argc, char *argv[]) { // 检查命令行参数数量 if (argc != 3) { printf("Usage: ./a.out filename num\n"); exit(EXIT_FAILURE); // 参数不正确时退出 } int fd; // 文件描述符 char buf[4096]; // 缓冲区 int linenum = atoi(argv[2]); // 将命令行参数转换为整数 // 尝试以读写模式打开文件 fd = open(argv[1], O_RDWR); if (fd < 0) { printf("Can't open file %s, file not exist or permission denied!\n", argv[1]); exit(EXIT_FAILURE); // 打开失败时退出 } int size = 0; // 文件大小 // 获取文件的行数和大小 int total_line = get_file_info(fd, &size); // 如果要删除的行数大于文件总行数,退出 if (linenum > total_line) { printf("%d is greater than total_line!\n", linenum); exit(EXIT_FAILURE); } int s = 0; // 要删除行的大小 int t = 0; // 当前行的大小 int i = 0; // 当前行数 lseek(fd, 0, SEEK_SET); // 将文件指针移到文件头 // 循环读取文件,直到文件结束 while (read(fd, &buf[0], 1) > 0) { lseek(fd, -1, SEEK_CUR); // 回退一个字符 memset(buf, 0, sizeof(buf)); // 清空缓冲区 readline(fd, buf); // 读取一行到缓冲区 i++; // 行数加1 t = strlen(buf); // 当前行的大小 // 如果当前行是目标行,记录其大小 if (i == linenum) { s = t; } // 如果当前行在目标行之后,将该行前移 if (i > linenum) { lseek(fd, -(s+t), SEEK_CUR); // 移动文件指针 write(fd, buf, strlen(buf)); // 写入当前行 lseek(fd, s, SEEK_CUR); // 移动文件指针 } } ftruncate(fd, size-s); // 截断文件,删除指定行 close(fd); // 关闭文件描述符 return 0; // 正常退出 }(3) 通过 fopen 打开文件,不借助临时文件删除指定行数据 # filename:ques_15b.c #include <stdio.h> // 包含标准输入输出库 #include <stdlib.h> // 包含标准库函数,如atoi和exit #include <string.h> // 包含字符串处理函数,如strlen #include <math.h> // 包含数学函数,虽然在这个程序中没有使用 #include <unistd.h> // 包含UNIX标准函数,如truncate int main(int argc, char *argv[]) { // 检查命令行参数个数是否正确 if (argc != 3) { printf("Usage: ./a.out filename num\n"); exit(EXIT_FAILURE); // 参数不正确时退出程序 } int linenum = atoi(argv[2]); // 将命令行中指定的行号转换为整数 char buf[4096]; // 定义缓冲区,用于读取文件内容 // 尝试以读写模式打开文件 FILE *fp = fopen(argv[1], "r+"); // 如果文件无法打开,打印错误信息并退出程序 if (!fp) { printf("Can't open file %s, file not exist or permission denied!\n", argv[1]); exit(EXIT_FAILURE); } int total_line = 0; // 记录文件的总行数 int size = 0; // 记录文件的总大小 // 循环读取文件直到文件末尾,计算总行数和总大小 while (fgets(buf, sizeof(buf), fp)) { size += strlen(buf); // 累加每行的长度 total_line++; // 行数加1 } // 如果要删除的行数大于文件的总行数,打印错误信息并退出程序 if (linenum > total_line) { printf("%d is greater than total_line!\n", linenum); exit(EXIT_FAILURE); } int s = 0; // 记录要删除的行的大小 int t = 0; // 记录当前读取行的大小 int i = 0; // 记录当前行数 fseek(fp, 0L, SEEK_SET); // 将文件指针重置到文件开头 // 再次循环读取文件,准备删除指定行 while (fgets(buf, sizeof(buf), fp)) { i++; // 当前行数加1 t = strlen(buf); // 当前行的长度 // 如果当前行是要删除的行,记录其大小 if (i == linenum) { s = t; } // 如果当前行在要删除的行之后,将其前移 if (i > linenum) { fseek(fp, -(s+t), SEEK_CUR); // 将文件指针移动到正确的位置 fputs(buf, fp); // 写入当前行 fseek(fp, s, SEEK_CUR); // 将文件指针向前移动s个字节 } } // 截断文件,删除指定行 truncate(argv[1], size-s); // 关闭文件指针 fclose(fp); return 0; // 正常退出程序 }(4) 这三个删除文件指定行的函数都需借助临时文件完成
嵌入式&系统
# 嵌入式
刘航宇
2年前
0
453
1
2024-06-27
基础概念:中断、任务、进程、线程、RTOS、Linux
中断 任务 进程 线程 它们之间的区别 RTOS和Linux的区别 中断、任务、进程和线程是计算机科学和操作系统中的基本概念,它们在多任务操作和资源管理中扮演着重要的角色。下面是这些概念的简要解释以及它们之间的区别: 中断 中断是硬件或软件发出的信号,用来通知CPU暂停当前的工作,转而去执行一个特殊的程序(中断处理程序)。中断可以是外部的,比如来自硬件设备的信号,或者是内部的,比如软件生成的信号。中断机制允许操作系统响应外部事件,如用户输入或硬件状态变化。 任务 在某些操作系统中,任务是一个抽象概念,用来表示一个执行单元,它可以是一个进程或者线程。任务通常指的是需要操作系统调度和资源管理的执行流。 进程 进程是操作系统分配资源和调度的基本单位。每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程状态和执行的资源。进程可以包含一个或多个线程。 线程 线程是进程中的一个实体,是CPU调度和执行的单位。线程共享所属进程的资源,但拥有自己的堆栈和程序计数器。线程比进程更轻量级,创建和切换的开销更小。 它们之间的区别 资源分配:进程是资源分配的最小单位,线程则不是。 执行:进程是执行程序的实例,线程是进程中的实际执行流。 地址空间:进程有独立的地址空间,线程共享进程的地址空间。 创建开销:进程的创建开销通常大于线程。 通信:线程间可以通过共享内存进行通信,进程间通信需要使用IPC(进程间通信)机制。 RTOS和Linux的区别 RTOS(实时操作系统)和Linux是两种不同类型的操作系统,它们在设计目标和特性上有所区别: 设计目标: RTOS:设计用于需要快速、可预测响应的系统,如嵌入式系统、工业控制等。 Linux:是一个通用操作系统,主要用于桌面、服务器、移动设备等。 实时性: RTOS:提供确定的响应时间,可以保证任务在指定的时间内得到处理。 Linux:虽然可以配置为实时系统,但通常不具备RTOS的严格实时性。 调度策略: RTOS:通常使用基于优先级的抢占式调度。 Linux:使用完全公平调度器(CFS)进行调度,可以配置为实时调度。 内存管理: RTOS:通常有简单的内存管理机制,适合资源受限的环境。 Linux:具有复杂的内存管理机制,支持虚拟内存和内存共享。 应用场景: RTOS:适用于对实时性要求高、资源受限的场合。 Linux:适用于需要高度灵活性和扩展性的场合。 开源和社区支持: RTOS:有些RTOS是开源的,但社区规模通常小于Linux。 Linux:是一个开源项目,拥有庞大的社区和开发者支持。 总的来说,RTOS和Linux各有优势,选择哪个系统取决于应用的具体需求。RTOS适合对实时性要求极高的场景,而Linux适合需要高度灵活性和功能丰富的环境。
嵌入式&系统
# 嵌入式
刘航宇
2年前
0
901
2
2024-03-12
C语言编译的四个步骤
编译一个C语言程序是一个多阶段的过程。从总体上看,这个过程可以分成四个独立的阶段。预处理、编译、汇编和连接。 在这篇文章中,我将逐一介绍编译下列C程序的四个阶段。 /* * "Hello, World!": A classic. */ #include <stdio.h> int main(void) { puts("Hello, World!"); return 0; }预处理 编译的第一个阶段称为预处理。在这个阶段,以#字符开头的行被预处理器解释为预处理器命令。这些命令形成一种简单的宏语言,有自己的语法和语义。这种语言通过提供内联文件、定义宏和有条件地省略代码的功能,来减少源代码的重复性。 在解释命令之前,预处理器会做一些初始处理。这包括连接续行(以 \ 结尾的行)和剥离注释。 要打印预处理阶段的结果,请向gcc传递-E选项。 gcc -E hello_world.c 考虑到上面的 "Hello, World!"的例子,预处理器将产生stdio.h头文件的内容和hello_world.c文件的内容,并将其前面的注释剥离出来。 编译 编译的第二个阶段被称为编译,令人困惑。在这个阶段,预处理过的代码被翻译成目标处理器架构特有的汇编指令。这些形成了一种中间的人类可读语言。 这一步骤的存在允许C代码包含内联汇编指令,并允许使用不同的汇编器。 一些编译器也支持使用集成汇编器,在这种情况下,编译阶段直接生成机器代码,避免了生成中间汇编指令和调用汇编器的开销。 要保存编译阶段的结果,可以向gcc传递-S选项。 gcc -S hello_world.c 这将创建一个名为hello_world.s的文件,包含生成的汇编指令。 汇编 在这个阶段,汇编器被用来将汇编指令翻译成目标代码。输出包括目标处理器要运行的实际指令。 要保存汇编阶段的结果,请向gcc传递-c选项。 gcc -c hello_world.c 运行上述命令将创建一个名为hello_world.o的文件,包含程序的目标代码。这个文件的内容是二进制格式,可以用运行命令hexdump或od来检查。 hexdump hello_world.o od -c hello_world.oLinux中的od(octal dump)命令用于转换输入内容为八进制。 Hexdump是一个命令行工具,用于以各种方式显示文件的原始内容,包括十六进制,可用于Linux、FreeBDS、OS X和其他平台。Hexdump不是传统Unix系统或GNU命令的一部分。 链接 汇编阶段产生的目标代码是由处理器能够理解的机器指令组成的,但程序的某些部分是不符合顺序的或缺失的。为了产生一个可执行的程序,现有的部分必须被重新排列,并把缺失的部分补上。这个过程被称为链接。 链接器将安排目标代码的各个部分,使某些部分的功能能够成功地调用其他部分的功能。它还将添加包含程序所使用的库函数指令的片段。在 "Hello,world!"程序的例子中,链接器将添加puts函数的对象代码。 这一阶段的结果是最终的可执行程序。当不使用选项运行时,gcc 将把这个文件命名为 a.out。如果要给文件命名,请向 gcc 传递 -o 选项。 gcc -o hello_world hello_world.c
编程&脚本笔记
# C/C++
刘航宇
2年前
0
622
1
Socket通信-Linux系统中C语言实现TCP/UDP图片和文件传输
TCP实现1.服务端 2.客户端 二、UDP实现1.服务端 2.客户端 TCP实现 传输控制协议(TCP,Transmission Control Protocol) 是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK,并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP使用的流量控制协议是可变大小的滑动窗口协议。 1.服务端 基于TCP协议的socket的server端程序编程步骤: 1、建立socket ,使用socket() 2、绑定socket ,使用bind() 3、打开listening socket,使用listen() 4、等待client连接请求,使用accept() 5、收到连接请求,确定连接成功后,使用输入,输出函数recv(),send()与client端互传信息 6、关闭socket,使用close() 服务端代码server.c /*server.c*/ #include<netinet/in.h> #include<sys/types.h> #include<sys/socket.h> #include<stdio.h> #include<stdlib.h> #include<string.h> #define SERVER_PORT 5678 //端口号 #define LENGTH_OF_LISTEN_QUEUE 20 #define BUFFER_SIZE 1024 #define FILE_NAME_MAX_SIZE 512 int main(int argc, char **argv) { // 设置一个socket地址结构server_addr,代表服务器ip地址和端口 struct sockaddr_in server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htons(INADDR_ANY); server_addr.sin_port = htons(SERVER_PORT); // 创建用于流协议(TCP)socket,用server_socket代表服务器向客户端提供服务的接口 int server_socket = socket(PF_INET, SOCK_STREAM, 0); if (server_socket < 0) { printf("Create Socket Failed!\n"); exit(1); } else printf("Create Socket Success.\n"); // 把socket和socket地址结构绑定 if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr))) { printf("Server Bind Port: %d Failed!\n", SERVER_PORT); exit(1); } else printf("Client Bind Port Success.\n"); // server_socket用于监听 if (listen(server_socket, LENGTH_OF_LISTEN_QUEUE)) { printf("Server Listen Failed!\n"); exit(1); } else printf("Listening....\n"); // 服务器始终监听 while(1) { // 定义客户端的socket地址结构client_addr,当收到来自客户端的请求后,调用accept // 接受此请求,同时将client端的地址和端口等信息写入client_addr中 struct sockaddr_in client_addr; socklen_t length = sizeof(client_addr); // 接受一个从client端到达server端的连接请求,将客户端的信息保存在client_addr中 // 如果没有连接请求,则一直等待直到有连接请求为止,这是accept函数的特性 // accpet返回一个新的socket,这个socket用来与此次连接到server的client进行通信 // 这里的new_server_socket代表了这个通信通道 int new_server_socket = accept(server_socket, (struct sockaddr*)&client_addr, &length); if (new_server_socket < 0) { printf("Server Accept Failed!\n"); break; } else printf("Server Accept Success.\n"); char buffer[BUFFER_SIZE]; bzero(buffer, sizeof(buffer)); length = recv(new_server_socket, buffer, BUFFER_SIZE, 0); if (length < 0) { printf("Server Recieve Data Failed!\n"); break; } else printf("Server Recieve Data Success.\n"); char file_name[FILE_NAME_MAX_SIZE + 1]; bzero(file_name, sizeof(file_name)); strncpy(file_name, buffer, strlen(buffer) > FILE_NAME_MAX_SIZE ? FILE_NAME_MAX_SIZE : strlen(buffer)); FILE *fp = fopen(file_name, "r"); //获取文件操作符 if (fp == NULL) { printf("File:\t%s Not Found!\n", file_name); } else { bzero(buffer, BUFFER_SIZE); int file_block_length = 0; while( (file_block_length = fread(buffer, sizeof(char), BUFFER_SIZE, fp)) > 0) { // 发送buffer中的字符串到new_server_socket,实际上就是发送给客户端 if (send(new_server_socket, buffer, file_block_length, 0) < 0) { printf("Send File:\t%s Failed!\n", file_name); break; } bzero(buffer, sizeof(buffer)); } fclose(fp); printf("File:\t%s Transfer Finished!\n", file_name); } close(new_server_socket); } close(server_socket); return 0; } 2.客户端 基于TCP协议的socket的Client程序编程步骤: 1、建立socket,使用socket() 2、通知server请求连接,使用connect() 3、若连接成功,就使用输入输出函数recv(),send()与server互传信息 4、关闭socket,使用close() 客户端代码client.c /*client.c*/ #include<netinet/in.h> // for sockaddr_in #include<sys/types.h> // for socket #include<sys/socket.h> // for socket #include<stdio.h> // for printf #include<stdlib.h> // for exit #include<string.h> // for bzero #define SERVER_PORT 5678 #define BUFFER_SIZE 1024 #define FILE_NAME_MAX_SIZE 512 int main(int argc, char **argv) { if (argc != 2) //判断有没有输入服务器ip { printf("Usage: ./%s ServerIPAddress\n", argv[0]); exit(1); } // 设置一个socket地址结构client_addr, 代表客户机的ip地址和端口 struct sockaddr_in client_addr; bzero(&client_addr, sizeof(client_addr)); client_addr.sin_family = AF_INET; // internet协议族IPv4 client_addr.sin_addr.s_addr = htons(INADDR_ANY); // INADDR_ANY表示自动获取本机地址 client_addr.sin_port = htons(0); // auto allocated, 让系统自动分配一个空闲端口 // 创建用于internet的流协议(TCP)类型socket,用client_socket代表客户端socket int client_socket = socket(AF_INET, SOCK_STREAM, 0); if (client_socket < 0) { printf("Create Socket Failed!\n"); exit(1); } else printf("Create Socket Success.\n"); // 把客户端的socket和客户端的socket地址结构绑定 if (bind(client_socket, (struct sockaddr*)&client_addr, sizeof(client_addr))) { printf("Client Bind Port Failed!\n"); exit(1); } else printf("Client Bind Port Success.\n"); // 设置一个socket地址结构server_addr,代表服务器的internet地址和端口 struct sockaddr_in server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; // 服务器的IP地址来自程序的参数 if (inet_aton(argv[1], &server_addr.sin_addr) == 0) { printf("Server IP Address Error!\n"); exit(1); } server_addr.sin_port = htons(SERVER_PORT); int server_addr_length = sizeof(server_addr); // 向服务器发起连接请求,连接成功后client_socket代表客户端和服务器端的一个socket连接 if (connect(client_socket, (struct sockaddr*)&server_addr, server_addr_length) < 0) { printf("Can Not Connect To %s!\n", argv[1]); exit(1); } else printf("Alreadly Connect To %s.\n", argv[1]); char file_name[FILE_NAME_MAX_SIZE + 1]; bzero(file_name, sizeof(file_name)); printf("Please Input File Name On Server: "); scanf("%s", file_name); char buffer[BUFFER_SIZE];//缓存区 bzero(buffer, sizeof(buffer)); strncpy(buffer, file_name, strlen(file_name) > BUFFER_SIZE ? BUFFER_SIZE : strlen(file_name)); // 向服务器发送buffer中的数据,此时buffer中存放的是客户端需要接收的文件的名字 send(client_socket, buffer, BUFFER_SIZE, 0); // send , sendto(), recv(),recvfrom() FILE *fp = fopen(file_name, "w"); if (fp == NULL) { printf("File: %s Can Not Open To Write!\n", file_name); exit(1); } // 从服务器端接收数据到buffer中 bzero(buffer, sizeof(buffer)); int length = 0; while(length = recv(client_socket, buffer, BUFFER_SIZE, 0)) { if (length < 0) { printf("Recieve Data From Server %s Failed!\n", argv[1]); break; } int write_length = fwrite(buffer, sizeof(char), length, fp); if (write_length < length) { printf("File:\t%s Write Failed!\n", file_name); break; } bzero(buffer, BUFFER_SIZE); } printf("Recieve File: %s From Server[%s] Finished!\n", file_name, argv[1]); // 传输完毕,关闭socket fclose(fp); close(client_socket); return 0; } 如图,客户端(左)从服务端(右)下载文件/图片: 图片 二、UDP实现 UDP(User Datagram Protocol) 全称是用户数据报协议,是一种非面向连接的协议,这种协议并不能保证我们的网络程序的连接是可靠的。 1.服务端 基于UDP协议的socket的server编程步骤: 1、建立socket,使用socket() 2、绑定socket,使用bind() 3、以recvfrom()函数接收发送端传来的数据(使用recvfrom函数 时需设置非阻塞,以免程序卡在此处) 4、关闭socket,使用close() /*server.c*/ #include<netinet/in.h> #include<sys/types.h> #include<sys/socket.h> #include<stdio.h> #include<stdlib.h> #include<string.h> #define SERVER_PORT 5678 //端口号 #define LENGTH_OF_LISTEN_QUEUE 20 #define BUFFER_SIZE 1024 #define FILE_NAME_MAX_SIZE 512 int main(int argc, char **argv) { // 设置一个socket地址结构server_addr,代表服务器internet的地址和端口 struct sockaddr_in server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htons(INADDR_ANY); server_addr.sin_port = htons(SERVER_PORT); // create a stream socket // 创建用于internet的流协议(UDP)socket,用server_socket代表服务器向客户端提供服务的接口 int server_socket = socket(PF_INET, SOCK_DGRAM, 0); if (server_socket < 0) { printf("Create Socket Failed!\n"); exit(1); } else printf("Create Socket Success.\n"); // 把socket和socket地址结构绑定 if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr))) { printf("Server Bind Port: %d Failed!\n", SERVER_PORT); exit(1); } else printf("Server Bind Port Success.\n"); printf("Waiting......\n"); // 服务器端一直运行用以持续为客户端提供服务 while(1) { // 接受此请求,同时将client端的地址和端口等信息写入client_addr中 struct sockaddr_in client_addr; int length = 0; int addrlen = sizeof(client_addr); char buffer[BUFFER_SIZE]; bzero(buffer, sizeof(buffer)); length = recvfrom(server_socket, buffer, BUFFER_SIZE, 0,(struct sockaddr *)&client_addr,&addrlen); if (length < 0) { printf("Server Recieve Data Failed!\n"); break; } char file_name[FILE_NAME_MAX_SIZE + 1]; bzero(file_name, sizeof(file_name)); strncpy(file_name, buffer, strlen(buffer) > FILE_NAME_MAX_SIZE ? FILE_NAME_MAX_SIZE : strlen(buffer)); FILE *fp = fopen(file_name, "r"); if (fp == NULL) { printf("File:\t%s Not Found!\n", file_name); } else { bzero(buffer, BUFFER_SIZE); int file_block_length = 0; while( (file_block_length = fread(buffer, sizeof(char), BUFFER_SIZE, fp)) > 0) { // 发送buffer中的字符串到server_socket,实际上就是发送给客户端 if (sendto(server_socket, buffer, BUFFER_SIZE, 0,(struct sockaddr *)&client_addr,addrlen) < 0) { printf("Send File:\t%s Failed!\n", file_name); break; } bzero(buffer, sizeof(buffer)); } fclose(fp); printf("File:\t%s Transfer Finished!\n", file_name); } } close(server_socket); return 0; }2.客户端 基于UDP协议的socket的client端编程步骤: 1、建立Socket,使socket() 2、用sendto()函数向接收端发送数据。 3、关闭socket,使用close()函数 /*client.c*/ #include<netinet/in.h> // for sockaddr_in #include<sys/types.h> // for socket #include<sys/socket.h> // for socket #include<stdio.h> // for printf #include<stdlib.h> // for exit #include<string.h> // for bzero #include <fcntl.h> #include <unistd.h> #include <sys/time.h> #define SERVER_PORT 5678 #define BUFFER_SIZE 1024 #define FILE_NAME_MAX_SIZE 512 int main(int argc, char **argv) { if (argc != 2) { printf("Usage: ./%s ServerIPAddress\n", argv[0]); exit(1); } // 设置一个socket地址结构client_addr, 代表客户机的internet地址和端口 struct sockaddr_in client_addr; bzero(&client_addr, sizeof(client_addr)); client_addr.sin_family = AF_INET; // internet协议族 client_addr.sin_addr.s_addr = htons(INADDR_ANY); // INADDR_ANY表示自动获取本机地址 client_addr.sin_port = htons(0); // auto allocated, 让系统自动分配一个空闲端口 // 创建用于internet的流协议(TCP)类型socket,用client_socket代表客户端socket int client_socket = socket(AF_INET, SOCK_DGRAM, 0); if (client_socket < 0) { printf("Create Socket Failed!\n"); exit(1); } // 设置超时,防止recvfrom()函数阻塞 struct timeval timeout; timeout.tv_sec = 1;//秒 timeout.tv_usec = 0;//微秒 if (setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) == -1) { perror("setsockopt failed:"); } // 把客户端的socket和客户端的socket地址结构绑定 if (bind(client_socket, (struct sockaddr*)&client_addr, sizeof(client_addr))) { printf("Client Bind Port Failed!\n"); exit(1); } // 设置一个socket地址结构server_addr,代表服务器的internet地址和端口 struct sockaddr_in server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; // 服务器的IP地址来自程序的参数 if (inet_aton(argv[1], &server_addr.sin_addr) == 0) { printf("Server IP Address Error!\n"); exit(1); } server_addr.sin_port = htons(SERVER_PORT); int server_addr_length = sizeof(server_addr); char file_name[FILE_NAME_MAX_SIZE + 1]; bzero(file_name, sizeof(file_name)); printf("Please Input File Name On Server: "); scanf("%s", file_name); char buffer[BUFFER_SIZE]; bzero(buffer, sizeof(buffer)); strncpy(buffer, file_name, strlen(file_name) > BUFFER_SIZE ? BUFFER_SIZE : strlen(file_name)); // 向服务器发送buffer中的数据,此时buffer中存放的是客户端需要接收的文件的名字 if( sendto(client_socket, buffer, BUFFER_SIZE, 0,(struct sockaddr *)&server_addr,server_addr_length) ) printf("Waiting receive %s from server....\n",file_name); FILE *fp = fopen(file_name, "w"); if (fp == NULL) { printf("File:\t%s Can Not Open To Write!\n", file_name); exit(1); } // 从服务器端接收数据到buffer中 bzero(buffer, sizeof(buffer)); int length = 0; //length = recvfrom(client_socket, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&server_addr, &server_addr_length)//非阻塞 while( length = recvfrom(client_socket, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&server_addr, &server_addr_length)) { if (length < 0) { //printf("Recieve Data From Server %s Failed!\n", argv[1]); break; } int write_length = fwrite(buffer, sizeof(char), length, fp); if (write_length < length) { printf("File: %s Write Failed!\n", file_name); break; } bzero(buffer, BUFFER_SIZE); } printf("Receive File: %s From Server[%s] Finished!\n", file_name, argv[1]); // 传输完毕,关闭socket fclose(fp); close(client_socket); return 0; } 如图,客户端(左)从服务端(右)下载文件/图片: 图片 注:使用recvfrom函数 时需设置非阻塞,以免程序卡住。
嵌入式&系统
# 嵌入式
刘航宇
2年前
0
730
0
2024-01-11
嵌入式常见知识点
图片 1、线程、进程的区别?最小执行单元是进程还是线程? 2、如何计算一个整数是不是2的n次方? 3、printf的具体实现? 4、什么是大小端?如何区分?有几种方法? 5、new与malloc的区别? 6、程序链接完毕之后分几部分? 7、Linux、Windows与FreeRtos的区别? 8、Linux系统中的中断为什么分为上下两个部分? 9、会快速排序吗?简要说一下? 10、static关键字的作用? 11、extern 关键字的作用? 12、volatile关键字的作用? 13、编译原理分哪几步? 14、内存分区? 15、freertos启动流程? 16、互斥锁与信号量的区别? 17、什么是死锁?死锁产生的原因?如何避免? 18、什么是内存泄漏? 19、系统死机了怎么排查原因?逐一看代码?工程量太大了吧? 20、同一类型的结构体定义两个变量能用内存大小来比较判断两者一样吗?(没懂) 21、freertos中EventBits_t是干啥的? 22、freertos使任务切换的方式有哪些? 23、项目中用到网络了吗? 24、了解Socket吗? 25、c++中set是什么? 26、有没有用到C++模板? 27、有没有对代码裁剪的经验 28、freertos系统是买模块时人家配置好的?还是移植的? 29、任务里有两把锁的时候该怎么处理 30、熟悉Shell脚本吗?$和#啥意思? 表示注释的开始,可以用来对代码进行说明,不会被执行,例如#echo hello,#这是一个注释,表示打印hello,后面的内容是一个注释。31、知道#error吗? 32、freertos消息队列的的具体实现? 33、堆栈区别? 34、程序存放状态和区别 1、线程、进程的区别?最小执行单元是进程还是线程? 线程和进程都是程序的执行实体,但是它们有以下区别: 进程是操作系统分配资源的基本单位,每个进程都有自己的独立的地址空间,代码段,数据段,堆栈段等。进程之间的切换需要保存和恢复上下文,开销较大。 线程是操作系统调度的基本单位,每个线程都属于某个进程,一个进程可以有多个线程,它们共享进程的地址空间,但是有自己的栈和寄存器。线程之间的切换只需要保存和恢复少量的寄存器,开销较小。 最小的执行单元是线程,因为一个进程至少要有一个线程,而一个线程可以独立运行。 2、如何计算一个整数是不是2的n次方? 一个整数是2的n次方,当且仅当它的二进制表示中只有一个1,其余都是0。例如,23=8=(1000)2 ,只有一个1。所以,我们可以用以下的方法来判断一个整数x是否是2的n次方: 如果x小于等于0,那么它不是2的n次方。 如果x大于0,那么我们可以用位运算的技巧,将x与x-1做按位与,如果结果为0,那么x是2的n次方,否则不是。例如,8&(8−1)=8&7=(1000)2&(0111)2=(0000)2=0 ,所以8是2的n次方。而9&(9−1)=9&8=(1001)2&(1000)2=(1000)2=0 ,所以9不是2的n次方。 3、printf的具体实现? printf是C语言中的一个标准库函数,用于向标准输出流(通常是屏幕)打印格式化的字符串。它的具体实现可能因编译器和操作系统的不同而有所差异,但是一般来说,它的主要步骤如下: 首先,printf会解析第一个参数,也就是格式化字符串,根据其中的转换说明符(如%d, %f, %s等),确定需要打印的数据类型和格式。 然后,printf会从第二个参数开始,依次获取对应的数据,并将其转换为字符串,拼接到格式化字符串的相应位置。如果参数的个数和类型与格式化字符串不匹配,可能会导致错误或未定义的行为。 最后,printf会调用底层的系统函数,将拼接好的字符串写入到标准输出流中,并返回成功写入的字符数。如果发生错误,返回负值。 4、什么是大小端?如何区分?有几种方法? 大小端是指数据在内存中的存储顺序,大端表示高位字节存放在低地址,低位字节存放在高地址,小端表示高位字节存放在高地址,低位字节存放在低地址。例如,一个32位的整数0x12345678,在大端模式下,存储为0x12 0x34 0x56 0x78,而在小端模式下,存储为0x78 0x56 0x34 0x12。 区分大小端的方法有以下几种: 通过联合体(union)的方式,将一个整数和一个字符数组放在同一个联合体中,然后判断字符数组的第一个元素是不是整数的最低字节,如果是,说明是小端,否则是大端。 通过指针的方式,将一个整数的地址赋给一个字符指针,然后判断指针指向的内容是不是整数的最低字节,如果是,说明是小端,否则是大端。 通过位运算的方式,将一个整数右移24位,然后与0xFF做按位与,得到的结果是不是整数的最高字节,如果是,说明是大端,否则是小端。 5、new与malloc的区别? new和malloc都是用于动态分配内存的,但是它们有以下区别: new是C++中的运算符,malloc是C语言中的函数,它们的用法不同。new可以直接分配对象,而malloc只能分配字节,需要强制类型转换。 new会调用对象的构造函数,初始化对象,而malloc只是分配原始的内存空间,不做任何初始化。 new会根据对象的类型,自动计算所需的内存大小,而malloc需要手动指定分配的字节数。 new分配失败时,会抛出异常,而malloc分配失败时,会返回NULL指针。 new对应的释放内存的运算符是delete,而malloc对应的释放内存的函数是free。 6、程序链接完毕之后分几部分? 程序链接完毕之后,一般分为以下几个部分: 代码段(text segment),存放程序的指令和常量。 数据段(data segment),存放程序的全局变量和静态变量。 堆(heap),存放程序动态分配的内存空间。 栈(stack),存放程序的局部变量,函数参数,返回地址等。 BSS段(bss segment),存放程序未初始化的全局变量和静态变量。 7、Linux、Windows与FreeRtos的区别? Linux、Windows和FreeRtos都是操作系统,但是它们有以下区别: Linux是一个开源的,基于Unix的,多用户,多任务,支持多种硬件平台的操作系统,它有很多不同的发行版,如Ubuntu,RedHat,Debian等。Linux适合用于服务器,嵌入式系统,桌面系统等。 Windows是一个闭源的,基于NT内核的,多用户,多任务,主要支持x86和x64架构的操作系统,它有很多不同的版本,如Windows 10,Windows Server,Windows CE等。Windows适合用于桌面系统,移动设备,游戏机等。 FreeRtos是一个开源的,基于微内核的,实时,多任务,支持多种嵌入式平台的操作系统,它有很多不同的移植,如ARM,MIPS,AVR等。FreeRtos适合用于实时控制,物联网,低功耗设备等。 8、Linux系统中的中断为什么分为上下两个部分? Linux系统中的中断为了提高效率和响应时间,分为上半部(top half)和下半部(bottom half)。上半部是指中断处理程序(interrupt handler),它负责处理中断的紧急事务,如保存寄存器,清除中断标志,识别中断源等。下半部是指中断延迟服务程序(interrupt deferred service routine),它负责处理中断的非紧急事务,如数据传输,设备驱动,信号发送等。上半部和下半部的区别如下: 上半部在中断上下文中执行,下半部在进程上下文中执行。 上半部不能被其他中断打断,下半部可以被其他中断打断。 上半部不能睡眠,下半部可以睡眠。 上半部不能调用可能导致阻塞的函数,如malloc,copy_to_user等,下半部可以调用这些函数。 上半部的执行时间应该尽可能短,下半部的执行时间可以较长。 9、会快速排序吗?简要说一下? 快速排序是一种基于分治思想的排序算法,它的基本步骤如下: 从待排序的数组中选择一个元素作为基准(pivot),通常选择第一个或者最后一个元素。 将数组分成两个子数组,一个子数组中的元素都小于或等于基准,另一个子数组中的元素都大于基准,这个过程称为划分(partition)。 对两个子数组递归地进行快速排序,直到子数组的长度为1或0。 将排好序的子数组合并,得到最终的排序结果。 快速排序的平均时间复杂度是O(nlogn) ,最坏情况是O(n^2) ,空间复杂度是O(logn) ,它是一种不稳定的排序算法。 10、static关键字的作用? static是一个修饰符,它可以用于变量和函数,它有以下作用: 用于全局变量,表示该变量只能在本文件中访问,不能被其他文件引用,这样可以避免命名冲突。 用于局部变量,表示该变量的生命周期是整个程序,而不是函数调用结束,这样可以保持变量的值不被销毁。 用于函数,表示该函数只能在本文件中调用,不能被其他文件引用,这样可以提高函数的封装性和安全性。 11、extern 关键字的作用? extern是一个修饰符,它可以用于变量和函数,它有以下作用: 用于变量,表示该变量是在其他文件中定义的,需要在本文件中引用,这样可以避免重复定义。 用于函数,表示该函数是在其他文件中定义的,需要在本文件中声明,这样可以避免隐式声明。 12、volatile关键字的作用? volatile是一个修饰符,它可以用于变量,它有以下作用: 用于变量,表示该变量可能会被外部因素(如中断,多线程,硬件等)改变,需要每次都从内存中读取,而不是从寄存器或缓存中读取,这样可以保证变量的实时性和一致性。 用于变量,表示该变量不会被编译器优化,需要按照程序的顺序执行,而不是进行重排或删除,这样可以避免编译器的干扰。 13、编译原理分哪几步? 编译原理是指将一种高级语言(源语言)的程序转换为另一种低级语言(目标语言)的程序的原理和方法,它一般分为以下几个步骤: 词法分析(lexical analysis),将源程序的字符序列分割成有意义的单词(token)。 语法分析(syntax analysis),将单词序列组织成语法树(parse tree)或抽象语法树(abstract syntax tree),表示程序的结构和语义。 语义分析(semantic analysis),检查程序是否符合语言的语法规则和语义规则,如类型检查,作用域分析等。 中间代码生成(intermediate code generation),将抽象语法树转换为一种中间表示(intermediate representation),如三地址码,四元式,后缀表达式等,便于优化和目标代码生成。 代码优化(code optimization),对中间表示进行一些变换,以提高程序的执行效率,如常量折叠,公共子表达式消除,循环优化,死代码删除等。 目标代码生成(target code generation),将中间表示转换为目标语言的代码,如汇编语言,机器语言等,同时进行一些分配,如寄存器分配,指令选择等。 14、内存分区? 内存分区是指将物理内存划分为若干个逻辑区域,以便于管理和使用。内存分区的方式有以下几种: 固定分区,将内存分为大小相等或不等的若干个区域,每个区域只能装入一个进程,如果进程的大小超过区域的大小,就会产生内部碎片。 动态分区,根据进程的大小和数量,动态地分配和回收内存空间,每个区域可以装入一个或多个进程,如果进程的大小不是区域的整数倍,就会产生外部碎片。 页式分区,将进程的地址空间划分为大小相等的若干个页,将物理内存划分为大小相等的若干个页框,然后将页映射到页框,实现非连续的内存分配,避免了外部碎片,但是可能产生内部碎片。 段式分区,将进程的地址空间划分为大小不等的若干个段,每个段有自己的逻辑地址和属性,然后将段映射到物理内存,实现非连续的内存分配,避免了内部碎片,但是可能产生外部碎片。 段页式分区,将进程的地址空间划分为若干个段,每个段再划分为若干个页,然后将页映射到物理内存的页框,实现非连续的内存分配,避免了内部碎片和外部碎片,但是增加了地址转换的复杂度。 15、freertos启动流程? freertos是一个实时操作系统,它的启动流程一般如下: 首先,执行硬件初始化,如设置时钟,中断,堆栈等。 然后,执行软件初始化,如创建任务,队列,信号量,定时器等。 接着,调用vTaskStartScheduler ()函数,启动调度器,选择优先级最高的就绪任务运行。 最后,当发生中断,延时,阻塞等事件时,调度器会根据算法,如优先级抢占式,时间片轮转式等,切换任务,实现多任务的并发执行。 16、互斥锁与信号量的区别? 互斥锁(mutex)和信号量(semaphore)都是用于实现多任务的同步和互斥的机制,但是它们有以下区别: 互斥锁是一个二元的同步对象,它只有两种状态:锁定和解锁。一个互斥锁只能被一个任务拥有,当一个任务获取互斥锁后,其他任务就不能再获取该互斥锁,直到拥有者释放它。互斥锁通常用于保护临界区的访问,避免数据的不一致。 信号量是一个计数的同步对象,它有一个初始值,表示可用的资源数量。一个信号量可以被多个任务共享,当一个任务获取信号量后,信号量的值减一,表示资源被占用。当信号量的值为零时,表示没有可用的资源,其他任务就要等待,直到有任务释放信号量,信号量的值加一,表示资源被释放。信号量通常用于实现生产者-消费者模型,控制资源的分配和回收。 17、什么是死锁?死锁产生的原因?如何避免? 死锁是指多个任务因为争夺有限的资源而相互等待,导致无法继续执行的现象。死锁产生的原因一般有以下四个必要条件: 互斥条件,指每个资源只能被一个任务拥有,其他任务不能访问。 占有且等待条件,指一个任务已经占有了至少一个资源,但是又申请了其他已经被占有的资源,同时不释放自己已经占有的资源。 不可抢占条件,指一个任务占有的资源不能被其他任务强行剥夺,只能由占有者主动释放。 循环等待条件,指多个任务形成一个环路,每个任务都在等待下一个任务占有的资源。 避免死锁的方法有以下几种: 破坏互斥条件,使用非互斥的资源,如可复制的资源,或者使用软件技术,如事务,来实现对资源的虚拟访问。 破坏占有且等待条件,要求一个任务在申请资源时,必须一次性申请所有需要的资源,或者在申请新的资源时,必须先释放已经占有的资源。 破坏不可抢占条件,允许一个任务在占有资源时,被其他任务抢占,或者主动释放资源,以满足其他任务的需求。 破坏循环等待条件,给每个资源分配一个优先级,要求一个任务只能申请优先级高于或等于自己已经占有的资源的资源,或者按照一定的顺序申请资源,避免形成环路。 18、什么是内存泄漏? 内存泄漏是指程序在运行过程中,动态分配了一些内存空间,但是没有及时释放,导致这些内存空间无法被其他程序使用,造成内存的浪费和紧张。内存泄漏可能会导致程序的性能下降,甚至崩溃。 19、系统死机了怎么排查原因?逐一看代码?工程量太大了吧? 系统死机是指系统无法响应用户的输入,或者出现异常的错误,导致系统无法正常运行。排查系统死机的原因有以下几种方法: 通过日志文件,查看系统在死机前的运行状态,是否有异常的信息,如错误码,警告,断言等,以及死机的时间,位置,频率等,从而定位可能的问题源。 通过调试工具,如gdb,lldb等,对系统进行调试,查看系统的内存,寄存器,堆栈,断点等,分析系统的运行流程,发现潜在的错误,如内存泄漏,空指针,死锁等。 通过测试工具,如valgrind,asan等,对系统进行测试,检测系统的内存管理,性能,覆盖率等,发现系统的缺陷,如内存错误,资源泄漏,性能瓶颈等。 通过代码审查,对系统的代码进行分析,检查代码的风格,规范,逻辑,注释等,发现代码的不合理,不一致,不完善等,提高代码的质量,可读性,可维护性等。 通过重现问题,对系统的死机现象进行复现,观察系统的表现,输入,输出等,找出问题的触发条件,规律,范围等,缩小问题的范围,提高问题的可解决性。 20、同一类型的结构体定义两个变量能用内存大小来比较判断两者一样吗?(没懂) 同一类型的结构体定义两个变量,不能用内存大小来比较判断两者一样,因为内存大小只能反映结构体的占用空间,而不能反映结构体的内容。例如,以下两个结构体变量的内存大小都是8字节,但是它们的内容是不一样的: struct Point { int x; int y; }; struct Point p1 = {1, 2}; struct Point p2 = {3, 4}; 如果要比较两个结构体变量是否一样,需要逐个比较它们的成员,或者使用memcmp函数比较它们的内存内容。例如: // 逐个比较 if (p1.x == p2.x && p1.y == p2.y) { printf("p1 and p2 are equal\n"); } else { printf("p1 and p2 are not equal\n"); } // 使用memcmp比较 if (memcmp(&p1, &p2, sizeof(struct Point)) == 0) { printf("p1 and p2 are equal\n"); } else { printf("p1 and p2 are not equal\n"); }21、freertos中EventBits_t是干啥的? EventBits_t是一个数据类型,它用于表示事件标志组(event group)中的每个位的状态,每个位可以表示一个事件的发生或者一个条件的满足。EventBits_t通常是一个无符号整数,它可以使用位运算来设置,清除,读取或等待事件标志。 22、freertos使任务切换的方式有哪些? freertos使任务切换的方式有以下几种: 时间片轮转法,按照任务的优先级和时间片的长度,依次轮流执行每个就绪任务,当一个任务的时间片用完或者主动放弃时,切换到下一个任务。 优先级抢占法,按照任务的优先级,总是执行优先级最高的就绪任务,当有更高优先级的任务就绪时,立即抢占当前任务,切换到更高优先级的任务。 混合法,结合时间片轮转法和优先级抢占法,对于同一优先级的任务,使用时间片轮转法,对于不同优先级的任务,使用优先级抢占法,实现任务的公平性和效率。 23、项目中用到网络了吗? 这个问题的答案取决于你的项目的具体情况,如果你的项目需要与其他设备或服务器进行通信,或者需要访问互联网上的资源,那么你的项目就用到了网络。如果你的项目只是在本地运行,不需要与外界交互,那么你的项目就没有用到网络。 24、了解Socket吗? Socket是一种通信机制,它可以实现不同进程或不同设备之间的数据交换。Socket通常基于TCP/IP协议,提供了可靠的,双向的,面向连接的通信服务。Socket的基本操作包括创建,绑定,监听,连接,发送,接收,关闭等。Socket的编程接口通常是一组函数或类,不同的编程语言或平台可能有不同的实现,如C语言的socket.h,Java语言的java.net.Socket等。 25、c++中set是什么? set是C++标准模板库(STL)中的一个容器,它可以存储一组不重复的元素,并且按照一定的顺序排列。set的元素可以是任意类型,但是必须支持比较操作,如<,==等。set的底层实现通常是红黑树,所以它的插入,删除,查找等操作的时间复杂度都是O(logn) ,其中n是元素的个数。set的优点是可以快速地检查一个元素是否存在,以及保持元素的有序性。set的缺点是不能存储重复的元素,以及不能直接访问元素,只能通过迭代器遍历。 26、有没有用到C++模板? C++模板是一种泛型编程的技术,它可以让程序员定义一种通用的模式,然后根据不同的类型或参数,生成不同的代码,从而实现代码的复用和抽象。C++模板有两种,一种是函数模板,用于定义通用的函数,另一种是类模板,用于定义通用的类。例如,以下是一个函数模板,用于比较两个值的大小: template <typename T> T max(T x, T y) { return (x > y) ? x : y; } 我有用过C++模板,它们可以让我的代码更简洁,更灵活,更高效。我用过STL中的一些类模板,如vector,map,set等,也用过自己定义的一些函数模板和类模板,来实现一些通用的算法和数据结构。 27、有没有对代码裁剪的经验 代码裁剪是指对代码进行优化,删除不必要的或冗余的代码,减少代码的体积和复杂度,提高代码的可读性和可维护性。代码裁剪的目的是为了提高程序的性能,节省内存空间,降低编译时间,避免错误和漏洞等。 我有对代码裁剪的经验,我用过一些工具和方法来进行代码裁剪,如: 使用编译器的优化选项,如-Os,-O3等,让编译器自动进行一些代码裁剪,如常量折叠,死代码删除,循环展开等。 使用代码分析工具,如lint,coverity等,检查代码的质量,发现代码的缺陷,如未使用的变量,函数,参数等,以及代码的风格,规范,注释等,然后根据工具的建议,修改或删除代码。 使用代码重构工具,如refactor,eclipse等,对代码进行重构,改善代码的结构,设计,逻辑等,消除代码的冗余,重复,复杂等,提高代码的可读性,可维护性,可扩展性等。 使用代码压缩工具,如upx,gzip等,对代码进行压缩,减少代码的体积,提高代码的传输速度,节省存储空间等。 28、freertos系统是买模块时人家配置好的?还是移植的? freertos系统是一个开源的,可移植的,实时的操作系统,它可以运行在多种嵌入式平台上,如ARM,MIPS,AVR等。freertos系统不是买模块时人家配置好的,而是需要根据不同的硬件和需求进行移植和定制的。移植freertos系统的步骤一般如下: 下载freertos的源码,选择合适的移植层,如portable/GCC/ARM_CM3等,根据目标平台的特性,修改一些配置参数,如configCPU_CLOCK_HZ,configTICK_RATE_HZ等。 编写启动代码,如设置时钟,中断,堆栈等,调用vPortStartFirstTask ()函数,启动第一个任务。 编写应用代码,如创建任务,队列,信号量,定时器等,调用vTaskStartScheduler ()函数,启动调度器。 编译,链接,下载,调试代码,检查系统的运行情况,如任务切换,中断响应,内存管理等。 29、任务里有两把锁的时候该怎么处理 任务里有两把锁的时候,可能会出现死锁的问题,即两个或多个任务因为互相等待对方占有的锁而无法继续执行。处理任务里有两把锁的时候,有以下几种方法: 避免使用两把锁,如果可能的话,尽量使用一把锁来保护临界区,或者使用其他同步机制,如信号量,事件标志等,来实现任务之间的协作。 按照一定的顺序获取和释放锁,如果必须使用两把锁,那么要求所有的任务都按照相同的顺序获取和释放锁,避免形成循环等待的条件。 使用超时机制,如果一个任务在获取锁时,发现锁已经被占用,那么不要无限期地等待,而是设置一个超时时间,如果超时时间到了,还没有获取到锁,那么就放弃获取,释放已经占有的锁,然后重新尝试或者执行其他操作。 使用优先级继承机制,如果一个任务在获取锁时,发现锁已经被占用,那么就把自己的优先级赋给占有锁的任务,让占有锁的任务尽快执行完毕,释放锁,然后恢复原来的优先级,这样可以避免优先级反转的问题。 30、熟悉Shell脚本吗?$和#啥意思? Shell脚本是一种用于控制Unix或Linux系统的命令行解释器,它可以实现一些自动化的任务,如文件操作,文本处理,程序运行等。Shell脚本的语法和结构类似于C语言,但是更加简洁和灵活。Shell脚本的文件名通常以.sh为后缀,例如test.sh。 $和#是Shell脚本中的两个特殊符号,它们有以下含义: $表示变量的引用,可以用来获取或设置变量的值,例如x=10,echo $x,表示将10赋值给变量x,然后打印x的值。 表示注释的开始,可以用来对代码进行说明,不会被执行,例如#echo hello,#这是一个注释,表示打印hello,后面的内容是一个注释。 31、知道#error吗? 、#error是一个预处理指令,它可以用来在编译时产生一个错误信息,中断编译过程。#error通常用来检查一些条件,如宏定义,平台,版本等,如果不满足条件,就提示错误,防止编译出错的代码。例如: #ifdef __linux__ #error This code is not compatible with Linux #endif 这段代码表示如果定义了__linux__宏,就产生一个错误信息,表示这段代码不兼容Linux系统。 32、freertos消息队列的的具体实现? freertos消息队列是一种用于实现任务之间或任务与中断之间的异步通信的机制,它可以存储一组有序的消息,每个消息可以是任意类型的数据。freertos消息队列的具体实现如下: 消息队列是一个结构体,它包含了一些成员,如队列的头指针,尾指针,长度,容量,锁,信号量等,用来管理队列的状态和操作。 消息队列的存储空间是一个数组,它可以是静态分配的或动态分配的,它的大小要能够容纳队列的最大容量乘以每个消息的大小。 消息队列的操作有以下几种,如创建,删除,发送,接收等,它们都是通过调用一些API函数来实现的,如xQueueCreate,vQueueDelete,xQueueSend,xQueueReceive等。 消息队列的操作都是原子的,即在操作过程中,不会被其他任务或中断打断,这是通过使用临界区或中断屏蔽来实现的,以保证队列的一致性和完整性。 消息队列的操作都是阻塞的,即如果队列满了,就不能发送消息,如果队列空了,就不能接收消息,这是通过使用信号量来实现的,以实现任务的同步和等待。 33、堆栈区别? 堆(heap)和栈(stack)都是程序运行时使用的内存空间,但是它们有以下区别: 堆是动态分配的,程序员可以自由地申请和释放堆上的内存空间,堆的大小受到物理内存的限制,堆上的内存空间的地址是不连续的。 栈是静态分配的,编译器会自动地分配和回收栈上的内存空间,栈的大小受到操作系统的限制,栈上的内存空间的地址是连续的。 堆是全局共享的,堆上的内存空间可以被任何函数或模块访问,堆上的内存空间的生命周期是由程序员控制的,如果不及时释放,可能会导致内存泄漏。 栈是局部私有的,栈上的内存空间只能被当前函数或模块访问,栈上的内存空间的生命周期是由编译器控制的,当函数调用结束时,栈上的内存空间就会被自动释放。 34、程序存放状态和区别 程序存放状态是指程序在运行过程中的不同阶段,它有以下几种: 新建状态,指程序刚刚被创建,还没有被加载到内存中,等待调度器的调度。 就绪状态,指程序已经被加载到内存中,已经分配了必要的资源,等待处理器的分配。 运行状态,指程序已经被分配了处理器,正在执行程序的指令。 阻塞状态,指程序在执行过程中,因为等待某些事件的发生,如输入输出,信号量,中断等,而暂时停止执行,释放处理器,等待事件的完成。 终止状态,指程序已经执行完毕,或者因为某些原因,如错误,异常,中断等,而被终止,释放内存和资源,从系统中消失。
嵌入式&系统
# 嵌入式
刘航宇
2年前
0
718
4
面向对象在编程中的概念
前言 那么究竟什么是面向对象呢?面向过程 面向对象 面向对象的三大特性封装 继承 多态 面向对象的五大基本原则单一职责原则(Single-Responsibility Principle) 开放封闭原则(Open-Closed principle) 里氏替换原则(Liskov-Substitution Principle) 依赖倒置原则(Dependecy-Inversion Principle) 接口隔离原则(Interface-Segregation Principle) 面向对象编程的用途 面向对象编程的应用领域 前言 在刚接触java、C++、Python语言的时候,就知道这是一门面向对象的语言。学不好java的原因找到了,面向对象的语言,没有对象怎么学 ::(狗头) 图片 那么究竟什么是面向对象呢? 面向对象,Object Oriented Programming,简称为OOP。说到面向对象,不得不提一嘴面向过程。 面向过程 面向过程是一种自顶而下的编程模式。 把问题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。 举个生活中的例子,假如你想吃红烧肉,你需要买肉,买调料,洗肉,切肉,烧肉,装盘。 需要我们具体每一步去实现,每个步骤相互协调,最终盛出来的才是正宗好吃的红烧肉。 面向对象 面向对象就是将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。 还是你想吃红烧肉这个例子,不过这次不同的是你发现了一家餐馆里有红烧肉这道菜,你要做的只是去点菜,就可以吃到红烧肉。 针不戳,针不戳,面向对象针不戳,你不用再去关心红烧肉繁琐的制作流程,就能吃到美味的红烧肉。 我们接着往下看 图片 面向对象的三大特性 封装 封装就是把客观的事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的类进行信息的隐藏。简单的说就是:封装使对象的设计者与对象的使用者分开,使用者只要知道对象可以做什么就可以了,不需要知道具体是怎么实现的。封装可以有助于提高类和系统的安全性 这里有点像FPGA的IP了,电子人懂! 继承 当多个类中存在相同属性和行为时,将这些内容就可以抽取到一个单独的类中,使得多个类无需再定义这些属性和行为,只需继承那个类即可。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类” 多态 多态同一个行为具有多个不同表现形式或形态的能力。是指一个类实例(对象)的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。 菜鸟教程中的例子就很形象 现实中,比如我们按下 F1 键这个动作: 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档; 如果当前在 Word 下弹出的就是 Word 帮助; 在 Windows 下弹出的就是 Windows 帮助和支持。 同一个事件发生在不同的对象上会产生不同的结果。 面向对象的五大基本原则 单一职责原则(Single-Responsibility Principle) 简而言之,就是一个类最好只有一个能引起变化的原因,只做一件事,单一职责原则可以看做是低耦合高内聚思想的延伸,提高高内聚来减少引起变化的原因。 开放封闭原则(Open-Closed principle) 简而言之,就是软件实体应该是可扩展的,但是不可修改。因为修改程序有可能会对原来的程序造成错误。即对扩展开放,对修改封闭 里氏替换原则(Liskov-Substitution Principle) 简而言之,就是子类一定可以替换父类,子类包含其基类(父类)的功能 依赖倒置原则(Dependecy-Inversion Principle) 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。依赖于抽象即对接口编程,不要对实现编程 接口隔离原则(Interface-Segregation Principle) 图片 简而言之,就是使用多个小的专门的接口,而不要使用一个大的总接口。例如将一个房子再分割成卧室,厨房,厕所等等,而不是将所有功能放在一起 面向对象编程的用途 使用封装(信息隐藏)可以对外部隐藏数据 使用继承可以重用代码 使用多态可以重载操作符/方法/函数,即相同的函数名或操作符名称可用于多种任务 数据抽象可以用抽象实现 项目易于迁移(可以从小项目转换成大项目) 同一项目分工 软件复杂性可控 面向对象编程的应用领域 人工智能与专家系统 企业级应用 神经网络与并行编程 办公自动化系统
编程&脚本笔记
# 软件算法
# C/C++
刘航宇
3年前
0
489
4
2023-12-20
【嵌软】STM32的4种开发方式介绍
与FPGA一样,STM32也属于板级开发,可适用于大多数计算场合,但神经网络这种需要大量并行计算的需求难以满足。 STM32的开发主要指的是通过程序实现功能,ST官方提供的开发方式来说从远及近分别是: 1、直接读写寄存器 2、标准外设驱动库 SPL 3、硬件抽象层库 HAL库 4、底层库 LL库 四种开发方式各有优缺点,可以参考ST官方的测试与说明: 图片 标准外设库:这是ST官方提供的一套固件库,包含了STM32所有外设的驱动函数,可以方便地调用。它的优点是稳定、兼容、易用,缺点是占用资源较多,效率较低,更新较慢。 寄存器操作:这是直接对STM32的寄存器进行读写的方式,可以实现最底层的控制。它的优点是占用资源最少,效率最高,缺点是难度较大,需要熟悉寄存器的功能和位定义,不利于移植。 HAL库:这是ST官方推出的一套新的固件库,基于硬件抽象层(Hardware Abstraction Layer)的思想,提供了更加简洁和统一的接口。它的优点是支持多种IDE,更新较快,易于移植,缺点是文档较少,兼容性较差,有些BUG。CubeMX:这是ST官方提供的一套图形化的配置工具,可以自动生成HAL库的代码,还可以集成一些中间件和应用层的功能。它的优点是操作简单,功能强大,缺点是生成的代码较冗余,不易修改,有些功能不完善。 直接读写寄存器 开发是最慢的,可移植性最差,基本不推荐使用,只有个别对时间或是内存要求特别高、或者在写操作系统调度器时才需要直接读写寄存器; 标准外设驱动库 是ST最开始提供的库(国内的教程也很多是依据题库出的),现在已经被ST放弃了; HAL库 和 LL库 是近几年推出的库,结合 STM32CubeIDE 使用非常方便, HAL库 性能较差、在STM32系列芯片中可移植性好, LL库 性能好、可移植性差。
嵌入式&系统
# 嵌入式
# C/C++
刘航宇
3年前
0
1,278
3
1
2
...
10
下一页