【经验分享】CANOPEN协议驱动移植(基于CANfestival源码架构)

【经验分享】CANOPEN协议驱动移植(基于CANfestival源码架构)

  • 前言
  • 一、CANOPEN整体实现原理
  • 二、CANOPEN驱动收发
  • 三、Timer定时器
  • 四、Object Dictionary对象字典
  • 五、CANOPEN应用层接口
  • 六、CANOPEN 驱动移植经验
  • 总结


前言

本次CANOPEN移植基于CANfestival开源代码,整体参考了如下文章:
基于STM32F4的CANOpen移植教程(超级详细)
谈谈自己对CANOPEN协议的驱动移植理解。每个移植CANOPEN协议的请务必认真阅读《周立功CANopen 轻松入门》,其中的内容生动形象,对你移植CANOPEN代码会有很大帮助。

CANopen的难点在于需要掌握的知识点比较多,如果没有移植过类似于Ethercat等协议,对新手来说并不算容易。如果移植过协议类驱动,那入手相对容易一些。


一、CANOPEN整体实现原理

带OS(操作系统)的整体实现原理
在这里插入图片描述
不带OS的整体实现原理
在这里插入图片描述
不管是带OS还是不带OS,都需要注意三个要点
1、CAN驱动收发实现
2、Timer定时器实现
3、Object Dictionary对象字典实现
这三点贯穿CANOPEN驱动调试整个过程,实现成功基本就不会有太大问题了,后续详细讲解。
CANOPEN协议的报文格式和CAN的消息格式区别不大,唯一的区别在于COB-ID的区别,COB-ID由Funciton code(功能码)和NODE ID构成。
在这里插入图片描述
这部分了解即可,例如0x580+NODE ID为SDO接收,0x600+NODE ID为SDO发送。
在这里插入图片描述
由此引出三种模型
在这里插入图片描述
第一种是主从站模型,一主多从模型,网络管理基于此模型。
第二种是客户端/服务器模型,一般是主站作为客户端,从站作为服务器端,SDO传输基于此模型。
第三种是消费者/生产者模型,这在《周立功CANopen 轻松入门》中有很生动的解释,生产者数据发送之后,消费者只接收不回复,就像买菜一样,PDO运行基于此模型。


二、CANOPEN驱动收发

CANOPEN调用的接口
1、canSend(CAN_PORT notused, Message *m)
canSend是canfestival协议实现的关键底层接口函数,最终调用的是CAN的底层驱动发送接口。

查看Message结构体定义,如果原来就有CAN驱动发送接口,对接上即可,转换下并不难。

typedef struct {UNS16 cob_id;	/**< message's ID */UNS8 rtr;		/**< remote transmission request. (0 if not rtr message, 1 if rtr message) */UNS8 len;		/**< message's length (0 to 8) */UNS8 data[8]; /**< message's datas */
} Message;

如果原先没有CAN底层发送接口,那么建议先实现CAN收发,再来实现CANOPEN收发。AT91的实现如下

unsigned char canSend(CAN_PORT notused, Message *m)
/******************************************************************************
The driver send a CAN message passed from the CANopen stack
INPUT	CAN_PORT is not used (only 1 avaiable)Message *m pointer to message to send
OUTPUT	1 if  hardware -> CAN frame
******************************************************************************/
{unsigned int mask;AT91S_CAN_MB *mb_ptr = AT91C_BASE_CAN_MB0 + START_TX_MB;if ((AT91F_CAN_GetStatus(AT91C_BASE_CAN) & TX_INT_MSK) == 0)return 0;			// No free MB for sendingfor (mask = 1 << START_TX_MB;(mask & TX_INT_MSK) && !(AT91F_CAN_GetStatus(AT91C_BASE_CAN) & mask);mask <<= 1, mb_ptr++)	// Search the first free MB{}AT91F_CAN_CfgMessageIDReg(mb_ptr, m->cob_id, 0);	// Set cob id// Mailbox Control Register, set remote transmission request and data lenght codeAT91F_CAN_CfgMessageCtrlReg(mb_ptr, m->rtr ? AT91C_CAN_MRTR : 0 | (m->len << 16));	AT91F_CAN_CfgMessageDataLow(mb_ptr, *(UNS32*)(&m->data[0]));// Mailbox Data Low RegAT91F_CAN_CfgMessageDataHigh(mb_ptr, *(UNS32*)(&m->data[4]));// Mailbox Data High Reg// Start sending by writing the MB configuration register to transmitAT91F_CAN_InitTransferRequest(AT91C_BASE_CAN, mask);return 1;	// successful
}

2、canReceive(Message *m)
CANOPEN发送接口一般在CAN中断中实现,主要用来实现当收到CANOPEN消息后,进行CANOPEN的协议解析,协议解析的接口为canDispatch函数。

unsigned char canReceive(Message *m)
/******************************************************************************
The driver passes a received CAN message to the stack
INPUT	Message *m pointer to received CAN message
OUTPUT	1 if a message received
******************************************************************************/
{unsigned int mask;AT91S_CAN_MB *mb_ptr = AT91C_BASE_CAN_MB0;if ((AT91F_CAN_GetStatus(AT91C_BASE_CAN) & RX_INT_MSK) == 0)return 0;		// Nothing receivedfor (mask = 1;(mask & RX_INT_MSK) && !(AT91F_CAN_GetStatus(AT91C_BASE_CAN) & mask);mask <<= 1, mb_ptr++)	// Search the first MB received{}m->cob_id = AT91F_CAN_GetFamilyID(mb_ptr);m->len = (AT91F_CAN_GetMessageStatus(mb_ptr) & AT91C_CAN_MDLC) >> 16;m->rtr = (AT91F_CAN_GetMessageStatus(mb_ptr) & AT91C_CAN_MRTR) ? 1 : 0;*(UNS32*)(&m->data[0]) = AT91F_CAN_GetMessageDataLow(mb_ptr);*(UNS32*)(&m->data[4]) = AT91F_CAN_GetMessageDataHigh(mb_ptr);// Enable Reception on MailboxAT91F_CAN_CfgMessageModeReg(mb_ptr, AT91C_CAN_MOT_RX | AT91C_CAN_PRIOR);AT91F_CAN_InitTransferRequest(AT91C_BASE_CAN, mask);return 1;		// message received
}

3、can_irq_handler
can中断函数实现,当中断来临时判断,如果接收到消息就进行canopen协议解析。

void can_irq_handler(void)
/******************************************************************************
CAN Interrupt
******************************************************************************/
{volatile unsigned int status;static Message m = Message_Initializer;		// contain a CAN messagestatus = AT91F_CAN_GetStatus(AT91C_BASE_CAN) & AT91F_CAN_GetInterruptMaskStatus(AT91C_BASE_CAN);if(status & RX_INT_MSK){	// Rx Interruptif (canReceive(&m))			// a message receivedcanDispatch(&ObjDict_Data, &m);         // process it}
}

canDispatch原型如下:

void canDispatch(CO_Data* d, Message *m)
{UNS16 cob_id = UNS16_LE(m->cob_id);switch(cob_id >> 7){case SYNC:		/* can be a SYNC or a EMCY message */if(cob_id == 0x080)	/* SYNC */{if(d->CurrentCommunicationState.csSYNC)proceedSYNC(d);} else 		/* EMCY */if(d->CurrentCommunicationState.csEmergency)proceedEMCY(d,m);break;case TIME_STAMP:case PDO1tx:case PDO1rx:case PDO2tx:case PDO2rx:case PDO3tx:case PDO3rx:case PDO4tx:case PDO4rx:if (d->CurrentCommunicationState.csPDO)proceedPDO(d,m);break;case SDOtx:case SDOrx:if (d->CurrentCommunicationState.csSDO)proceedSDO(d,m);break;case NODE_GUARD:if (d->CurrentCommunicationState.csLifeGuard)proceedNODE_GUARD(d,m);break;case NMT:if (*(d->iam_a_slave)){proceedNMTstateChange(d,m);}break;
#ifdef CO_ENABLE_LSScase LSS:if (!d->CurrentCommunicationState.csLSS)break;if ((*(d->iam_a_slave)) && cob_id==MLSS_ADRESS){proceedLSS_Slave(d,m);}else if(!(*(d->iam_a_slave)) && cob_id==SLSS_ADRESS){proceedLSS_Master(d,m);}break;
#endif}
}

该函数对接收到的信息首先进行COB-ID判断是什么类型,然后进行相应的报文处理。

各函数实现可以参考CANfestival中examples中的实现,带OS和不带OS的都有。
在这里插入图片描述


三、Timer定时器

这是第二个重点,各驱动的实现各有不同,需要实现:
1、void initTimer(void) 初始化定时器
2、void setTimer(TIMEVAL value) 用于设置下一个定时器报警时间
3、TIMEVAL getElapsedTime(void) 该函数用于获取自上次调用以来所经过的时间。它通过复制运行中的计时器的值,然后计算当前计时器值与上次调用时计时器值之间的差异来实现。
4、void timer_can_irq_handler(void) 此函数处理定时器中断,定时时间到即处理,调用TimeDispatch函数更新栈中的时间信息。TimeDispatch函数原型如下:

void TimeDispatch(void)
{TIMER_HANDLE i;TIMEVAL next_wakeup = TIMEVAL_MAX; /* used to compute when should normaly occur next wakeup *//* First run : change timer state depending on time *//* Get time since timer signal */UNS32 overrun = (UNS32)getElapsedTime();TIMEVAL real_total_sleep_time = total_sleep_time + overrun;s_timer_entry *row;for(i=0, row = timers; i <= last_timer_raw; i++, row++){if (row->state & TIMER_ARMED) /* if row is active */{if (row->val <= real_total_sleep_time) /* to be trigged */{if (!row->interval) /* if simply outdated */{row->state = TIMER_TRIG; /* ask for trig */}else /* or period have expired */{/* set val as interval, with 32 bit overrun correction, *//* modulo for 64 bit not available on all platforms     */row->val = row->interval - (overrun % (UNS32)row->interval);row->state = TIMER_TRIG_PERIOD; /* ask for trig, periodic *//* Check if this new timer value is the soonest */if(row->val < next_wakeup)next_wakeup = row->val;}}else{/* Each armed timer value in decremented. */row->val -= real_total_sleep_time;/* Check if this new timer value is the soonest */if(row->val < next_wakeup)next_wakeup = row->val;}}}/* Remember how much time we should sleep. */total_sleep_time = next_wakeup;/* Set timer to soonest occurence */setTimer(next_wakeup);/* Then trig them or not. */for(i=0, row = timers; i<=last_timer_raw; i++, row++){if (row->state & TIMER_TRIG){row->state &= ~TIMER_TRIG; /* reset trig state (will be free if not periodic) */if(row->callback)(*row->callback)(row->d, row->id); /* trig ! */}}
}

该函数实现定时器调度功能:

计算自上次信号以来的时间偏移。
遍历所有定时器,根据是否已触发或周期到期更新状态和下次触发时间。
根据最近需触发的定时器设置系统睡眠时间和实际定时器值。
再次遍历并调用已触发定时器的回调函数。可以参考
CANopen补充–时间计算出错


四、Object Dictionary对象字典

对象字典是连接底层和应用层通信的重要桥梁,没有对象字典就无法解析SDO和PDO等报文。对象字典的生成依赖于如下python工具objdictedit,在canfestival源码里。
在这里插入图片描述
根据sdo和pdo的要求进行配置,注意主站需要配置成client,从站配置成server。pdo配置,主站的tpdo和从站的rpdo cob-id要一致,主站的rpdo和从站的tpdo要一致,否则无法获取到数据。
在这里插入图片描述
配置完成后点击建立词典即可生成Master.c和Master.h文件。
在这里插入图片描述
需要注意,生成后的文件可能还需要进行二次修改,不一定可以直接使用。Master.c最后一句是连接主函数和对象字典的关键。所以一定要匹配上。
main.c函数,引用对象字典Master_Data
在这里插入图片描述
Master.c函数Master_Data定义
在这里插入图片描述
Co_Data这个结构体包含了对象字典解析后的关键信息,在调试过程中也可以查看,比如是否存在越界,空指针等问题。详细的不再赘述,可以参考网上相关文章。

struct struct_CO_Data {/* Object dictionary */UNS8 *bDeviceNodeId;const indextable *objdict;s_PDO_status *PDO_status;TIMER_HANDLE *RxPDO_EventTimers;void (*RxPDO_EventTimers_Handler)(CO_Data*, UNS32);const quick_index *firstIndex;const quick_index *lastIndex;const UNS16 *ObjdictSize;const UNS8 *iam_a_slave;valueRangeTest_t valueRangeTest;/* SDO */s_transfer transfers[SDO_MAX_SIMULTANEOUS_TRANSFERS];/* s_sdo_parameter *sdo_parameters; *//* State machine */e_nodeState nodeState;s_state_communication CurrentCommunicationState;initialisation_t initialisation;preOperational_t preOperational;operational_t operational;stopped_t stopped;void (*NMT_Slave_Node_Reset_Callback)(CO_Data*);void (*NMT_Slave_Communications_Reset_Callback)(CO_Data*);/* NMT-heartbeat */UNS8 *ConsumerHeartbeatCount;UNS32 *ConsumerHeartbeatEntries;TIMER_HANDLE *ConsumerHeartBeatTimers;UNS16 *ProducerHeartBeatTime;TIMER_HANDLE ProducerHeartBeatTimer;heartbeatError_t heartbeatError;e_nodeState NMTable[NMT_MAX_NODE_ID]; /* NMT-nodeguarding */TIMER_HANDLE GuardTimeTimer;TIMER_HANDLE LifeTimeTimer;nodeguardError_t nodeguardError;UNS16 *GuardTime;UNS8 *LifeTimeFactor;UNS8 nodeGuardStatus[NMT_MAX_NODE_ID];/* SYNC */TIMER_HANDLE syncTimer;UNS32 *COB_ID_Sync;UNS32 *Sync_Cycle_Period;/*UNS32 *Sync_window_length;;*/post_sync_t post_sync;post_TPDO_t post_TPDO;post_SlaveBootup_t post_SlaveBootup;post_SlaveStateChange_t post_SlaveStateChange;/* General */UNS8 toggle;CAN_PORT canHandle;	scanIndexOD_t scanIndexOD;storeODSubIndex_t storeODSubIndex; /* DCF concise */const indextable* dcf_odentry;UNS8* dcf_cursor;UNS32 dcf_entries_count;UNS8 dcf_status;UNS32 dcf_size;UNS8* dcf_data;/* EMCY */e_errorState error_state;UNS8 error_history_size;UNS8* error_number;UNS32* error_first_element;UNS8* error_register;UNS32* error_cobid;s_errors error_data[EMCY_MAX_ERRORS];post_emcy_t post_emcy;#ifdef CO_ENABLE_LSS/* LSS */lss_transfer_t lss_transfer;lss_StoreConfiguration_t lss_StoreConfiguration;
#endif	
};

五、CANOPEN应用层接口

上述移植完成后驱动大部分内容已经完成,接下来就是应用层调用什么接口进行主从站的通信。在例子中可以看到,主站进入工作状态y以及设置NodeID,需要调用如下接口
1、setState设置状态

  setState(&ObjDict_Data, Initialisation);	// Init the statesetNodeId (&ObjDict_Data, 0x7F);setState(&ObjDict_Data, Operational);		// Put the master in operational mode

2、masterSendNMTstateChange设置从站状态

UNS8 masterSendNMTstateChange(CO_Data* d, UNS8 nodeId, UNS8 cs)
{Message m;MSG_WAR(0x3501, "Send_NMT cs : ", cs);MSG_WAR(0x3502, "    to node : ", nodeId);/* message configuration */m.cob_id = 0x0000; /*(NMT) << 7*/m.rtr = NOT_A_REQUEST;m.len = 2;m.data[0] = cs;m.data[1] = nodeId;return canSend(d->canHandle,&m);
}

可以发现最终都是调用canSend进行底层报文发送出去。
3、masterSendNMTnodeguard用来设置从站节点守护进程,设置成功后可以收到从站的心跳报文。
4、sendsdo用来发送服务数据对象sdo命令,直接调用即可。
5、过程数据对象发送pdo比较特殊,有好几种通信方式。选择FEh或者FFh后,再设置Event timer,tpdo就会自动发送。(注意:TPDO和RPDO是相对于自身来定义的,T发送,R接收)。
在这里插入图片描述
6、写字典和读字典setODentry和getODentry,可以用来改变对象字典中的参数,Pdo过程中的数据传递等,注意各输入参数的定义。


六、CANOPEN 驱动移植经验

1、timer定时器调试过程中需要注意时间溢出问题,避免出现定时不准,如果不重新开定时器也可以用系统时钟。把timer加入监控内容,调试过程中需要注意是否停止。
2、canfestival默认是开启串口Log的,可以借助串口工具进行开发。
在这里插入图片描述

研发阶段结束后需要关闭,避免打印的延时。

#define DEBUG_WAR_CONSOLE_ON
#define DEBUG_ERR_CONSOLE_ON

3、借助支持canopen协议的工具可以直接看到传输的协议内容,对于调试有很大帮助,建议备一个,如果实在没有就接串口。
在这里插入图片描述
4、有些canfestival源码可能存在bug,根据实际情况依然需要查看源码进行修改,不要觉得源码必然靠谱。
5、canSend转换到can底层传输一定要注意RTR,数据帧用的比较多,但是一定不能省。
6、想到再补充。


总结

CANOPEN协议移植主要调试时间花在timer定时器、can发送和中断实现和对象字典的实现上,其他接口都是统一通用的,只要知道调用哪个接口就可以实现。时间仓促讲的不是很详细,有什么问题可以留言。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/52648.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot中MyBatis使用自定义TypeHandler

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

Spring Boot Web开发实践:响应参数的使用方法、IOC、DI和Bean基本介绍

主要介绍了SpringBootWeb响应参数的基本使用和spring框架的控制反转&#xff08;IOC&#xff09;和依赖注入&#xff08;DI&#xff09;以及Bean对象的声明、扫描、注入&#xff01;&#xff01;&#xff01; 目录 前言 响应参数 分层解耦 三层架构 分层解耦 IOC & …

第15届蓝桥杯青少组Scratch初级组省赛真题试卷

第十五届蓝桥杯青少组省赛Scratch初级组真题试卷 题目总数&#xff1a;10 总分数&#xff1a;360 选择题 第 1 题 单选题 Scratch运行以下程序&#xff0c;角色会说( )? A.29 B.31 C.33 D.35 第 2 题 单选题 scratch运行下列哪个程序后&#xff0c;宇航…

RabbitMQ 集群与高可用性

目录 单节点与集群部署 1.1. 单节点部署 1.2. 集群部署 镜像队列 1.定义与工作原理 2. 配置镜像队列 3.应用场景 4. 优缺点 5. Java 示例 分布式部署 1. 分布式部署的主要目标 2. 典型架构设计 3. RabbitMQ 分布式部署的关键技术 4. 部署策略和实践 5. 分布式部署…

解决银河麒麟桌面操作系统V10(特别是2101版本)中无法通过interfaces设置网络

解决银河麒麟桌面操作系统V10&#xff08;特别是2101版本&#xff09;中无法通过interfaces设置网络 1、问题简述2、解决方案1. 尝试删除ppp文件、重启2. 使用NetworkManager &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 1、问题简述 在…

day44——C++对C的扩充

八、C对函数的扩充 8.1 函数重载&#xff08;overload&#xff09; 1> 概念 函数重载就是能够实现"一名多用"&#xff0c;是实现泛型编程的一种 泛型编程&#xff1a;试图以不变的代码&#xff0c;来实现可变的功能 2> 引入背景 程序员在写函数时&#x…

k8s的组件以及安装

目录 概念 k8s的使用场景 k8s的特点 核心组件 master主组件 1.kube-apiserver 2.etcd 3.kube-controller-manager 控制器 4.kube-scheduler node从节点组件 1.kubelet 2.kube-proxy 3.docker 总结 k8s的核心概念 安装k8s 架构 安装步骤 实验&#xff1a;创…

Linux学习笔记(4)----Debian压力测试方法

使用命令行终端压力测试需要两个实用工具&#xff1a;s-tui和stress sudo apt install s-tui stress 安装完成后&#xff0c;在终端中启动 s-tui实用工具&#xff1a; s-tui 执行后如下图&#xff1a; 你可以使用鼠标或键盘箭头键浏览菜单&#xff0c;然后点击“压力选项(Str…

Leetcode Day14排序算法

动态git可以看 :https://leetcode.cn/problems/sort-an-array/solutions/179370/python-shi-xian-de-shi-da-jing-dian-pai-xu-suan-fa/ 选择排序 def selection_sort(nums):n len(nums)for i in range(n):for j in range(i, n):if nums[i] > nums[j]:nums[i], nums[j] …

甲基化组学全流程生信分析教程

甲基化组学全流程分析和可视化教程 读取数据目录下的idat文件的甲基化全流程一键分析 功能简介 甲基化分析模块可以实现甲基化芯片450K, 870kEPIC数据的自动读取&#xff0c;可以读取idat文件&#xff0c;也可以读取beta甲基化矩阵文件甲基化数据的缺失值插值甲基化数据的质…

python测试框架之Pytest

初识Pytest Pytest1.Pytest的特点&#xff1a;2.Pytest的基本使用规则3.pytest安装1&#xff09;使用编译器安装2&#xff09;使用命令安装 4.pytest规则 Pytest Pytest是python的一个第三方单元测试库&#xff0c;它的目的是让单元测试变得容易&#xff0c;并且也能扩展到支持…

解析云上实时数仓的挑战与实践 | Databend @DTCC 2024 演讲回顾

8 月 22 日 ~ 24 日&#xff0c;由 IT168 联合旗下 ITPUB、ChinaUnix 两大技术社区主办的第 15 届中国数据库技术大会&#xff08;DTCC2024&#xff09;在北京朗丽兹西山花园酒店成功召开。本次大会以“自研创新 数智未来”为主题&#xff0c;通过深度交流与探讨&#xff0c;推…

如何在手机上设置国内代理IP地址:详细指南

在某些情况下&#xff0c;我们可能需要在手机上设置国内代理IP地址&#xff0c;以便访问特定的网络服务或提高网络连接的稳定性。本文将详细介绍如何在Android和iOS设备上设置代理IP地址。 在Android设备上设置代理IP地址 在Android设备上设置代理IP地址非常简单&#xff0c;只…

MYSQL:简述对B树和B+树的认识

MySQL的索引使用B树结构。 1、B树 在说B树之前&#xff0c;先说说B树&#xff0c;B树是一个多路平衡查找树&#xff0c;相较于普通的二叉树&#xff0c;不会发生极度不平衡的状况&#xff0c;同时也是多路的。 B树的特点是&#xff1a;他会将数据也保存在非叶子节点。而这个…

C语言典型例题55

《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 题目&#xff1a; 例题4.7 兔子的繁殖。这是一个有趣的古典问题&#xff1a;有一对兔子&#xff0c;从出生后的第3个月开始起每个月都生一对兔子。小兔子长到第3个月又生一对兔子。假设所有兔子都不死&#xff0c;…

二叉搜索树进阶之红黑树

前言&#xff1a; 在上文我们已经学习了AVL树的相关知识以及涉及的四种旋转的内容&#xff0c;但是AVL树追求平衡导致旋转操作过多&#xff0c;一些情况下影响性能&#xff0c;由此我们就来了解一下二叉搜索树的另外一个分支&#xff0c;红黑树。 &#xff08;倘若对旋转知识…

2024版Assimp配置教程

最近想看看图形学&#xff0c;选择速通LearnOpenGL&#xff0c;不出意外最耗时间的依然是配置环境。按照教程上的把GLFW等等配置的没有问题&#xff0c;但是在Assimp这里卡住了。原因是教程上说的不详细&#xff0c;而网上查的又和现在的版本相去甚远&#xff0c;导致捣鼓了好一…

从web.xml动态读取sunspringmvc.xml文件

文章目录 1.问题分析1.SunWebApplicationContext.java 中sunspringmvc.xml是写死的2.但是web.xml已经配置了init-param&#xff0c;所以应该是可以读取的 2.具体实现1.SunDispatcherServlet.java 得到ServletConfig传递给Spring容器完成初始化2.SunWebApplicationContext.java …

【C++从小白到大牛】C++的隐式和显示类型转换基础知识讲解

目录 1、C语言中的类型转换 2、C语言和C中可以相互转换的类型总结 C语言&#xff1a; CPP&#xff1a; 3. 为什么C需要四种类型转换 4、C四大强制类型转换 4.1static_cast 4.2 reinterpret_cast 4.3 const_cast 4.4dynamic_cast 注…

基于x86 平台opencv的图像采集和seetaface6的性别识别功能

目录 一、概述二、环境要求2.1 硬件环境2.2 软件环境三、开发流程3.1 编写测试3.2 配置资源文件3.3 验证功能一、概述 本文档是针对x86 平台opencv的图像采集和seetaface6的性别识别功能,opencv通过摄像头采集视频图像,将采集的视频图像送给seetaface6的性别识别模块从而实现…