基于STM32F4的心电监护仪
从题目中可以看出该课题来源于2020年省电赛A题的无线运动传感器节点的设计,该作品得过湖北省电赛二等奖,同时也是我本科毕业设计,这里我把自己做的关于心电部分的工作进行一次总结,也对我的大学四年进行一次总结。
本研究的处理器模块选择正点原子公司的STM32F4最小系统板子,如图1所示,该最小系统板子搭载STM32F407ZGT6芯片,并具有 192KB的SRAM、1024KB的FLASH、丰富的定时器资源(12个16位定时器,2个32位定时器)、112个通用I/O口、2个DMA控制器以及1个FSMC接口,其中通过FSMC接口可以使得刷屏的速度可达3300W像素/秒,另外该板子还外扩了1M字节的SRAM芯片,更加有利于该处理器驱动4.3寸的LCD,这样极大加快心电监测仪的刷屏速度,而且STM32F407ZGT6这款芯片还集成FPU和DSP指令,可以加快数字滤波器的处理速度,而且该最小系统板子还将FSMC接口和其他IO口一并引出。

本研究最重要的地方便是心电采集板,关于心电信号的采集板的芯片选择TI公司的ADS1292R,外围电路参考TI公司所给的原理图和建议绘制,如图所示。

关于ADS1292R的外围电路的介绍和使用,这里推荐这篇博文,ADS1292R的使用
温度检测模块采用LMT70温度传感器。其优点是:超小型、高精度、低功耗的模拟温度传感器。而缺点是:接触式的温度传感器,测体表温度存在一定误差。但是考虑到温度测量的精度以及测量方便,最终选择LMT70作为测温传感器,同时选择采用ADS1118具有PGA、电压基准、16位的高精度ADC对LMT70数据的温度模拟量进行采集。


本系统为了更好的人机交互,采用4.3寸触摸屏搭载开源图形库LVGL,一方面将显示波形和数据与心电信号的采集和处理隔离开,另一方面是为了交互的方便和美观,系统运行界面如下图所示。整体界面主要有菜单、返回、图表、数据栏、导联状态灯以及开启心电采集按钮。

系统的菜单是通过LVGL的roller控件绘制的,roller里面选项的事件则是通过回调函数的形式调用。由于选中roller的选项,LVGL就会返回选项的值,因此我自己设计了函数指针数组来注册回调函数,并且将这个选中的序号通过数组方式调用函数,代码如下所示:
void (*oper_fuc[4])();//函数指针数组
void Menuitem_Init(void)
{
oper_fuc[0]=send_type_server;
oper_fuc[1]=Set_chart_div_line;
oper_fuc[2]=clear_step;
oper_fuc[3]=smooth_filter;
}
static void roller_event_handler(lv_obj_t * obj, lv_event_t event)
{
static unsigned char count=0;
if(event==LV_EVENT_VALUE_CHANGED)
{
count=lv_roller_get_selected(obj);
}
if(event==LV_EVENT_CLICKED)
{
oper_fuc[count]();
}
}
roller_event_handler是选中roller中的事件函数,在事件函数里面来回调选项的处理函数。roller中总共写了4个选项,分别为
send_type选择发送类型(支持发送到本地显示或者串口发送给上位机)、set_div_line是否设置图表的等分线、
clear_step清除界面上的数据、
smooth_filter是否进行平滑滤波

本系统还设计导联状态指示灯,前面讨论过ADS1292R可以检测电联的脱落状态,因而这里用LVGL的led控件作为导联的状态指示,当检测到导联接入人体,led控件就会点亮。设计了红心周期性跳动,当检测到导联接入人体后,红心就会周期性跳动,当心电数据采样开始后,红心随着心率值的改变而跳动着。同时还设计了采样开始/停止按钮,可以随时暂停和开始采样心电信号。
除了以上看得见的设计之外,还创建了四个周期性的任务,任务优先级从高到低分别为:更新数据栏里的数据、更新导联状态、检查心电信号的纵轴范围、系统状态的检查。
心电信号本质上是测量人体体表的电信号,将电极通过一定的导联体系就可以记录到心电图,因而选择合适的电极是观察心电图至关重要的选择。在医学上常见的导联体系分别为标准12导联体系、Lewis导联、Fontaine导联、Cabrera导联、Nehb导联、frank导联、Mason-Likar导联等。标准12导联体系是医院所使用的,它由3个双极肢体导联、6个单极胸前导联、3个单极加压肢体导联所组成。
该系统的主要目的是实时检测心率和QRS宽度,因此选择的导联应该基于能观察心电中R波较大的原则。因而选择标准12导联中标准肢体导联I(见图左),或者Mason-Likar导联(见图右)。

人体的内阻很高,因而心电信号是一个高内阻且幅度很低的信号,如果处理不好就会造成心电信号的衰减,因此就需要从两方面解决:
(1)降低与电极的接触阻抗(2)提高采集电路的输入阻抗。

目前,市面上有三种电极,分别为湿电极、干电极和非接触式电极,这三种电极中湿电极的接触电阻最小,因而对于模拟前端的输入电阻不需要太大。湿电极主要由电极片、Ag/AgCl 涂层、导电胶等物质组成。 医学电极贴片与身体接触的是水凝胶(亲水化合物),“黑色”部分为Ag/AgCl,使用导电金属和导线与仪器连接,实物如图所示。

人体的心电信号是一种非平稳、非线性、随机性比较强的微弱生理信号,幅值约为毫伏(mV)级,频率在0.05-100Hz之间。心电信号的每一个心跳循环由一系列有规律的波形组成,它们分别是P波、QRS复合波和T波,而这些波形的起点、终点、波峰、波谷、以及间期分别记录着心脏活动状态的详细信息

心电信号各个波段的详细说明如下:

心电各个波段的功率谱如下:

心电信号的噪声分析如下:

读者想对心电信号进一步了解可以参考如下链接:http://www.mythbird.com/ecgxin-hao-te-zheng/。
系统先从硬件初始化开始,其中包括串口初始化、触摸屏初始化、外部SRAM初始化、ADS1292R初始化、LMT70初始化、LVGL心跳定时器初始化。
其次就是LVGL初始化,主要是一些主题和变量的初始化。然后创建系统的UI界面和一些定时的任务。
最后初始化心电数据缓存、 数字低通滤波器初始化、心率数据缓存初始化。
完成以上的初始化,系统便进入主循环,等待心电数据输入缓存中出现数据,随后开始滤波,将滤波之后的数据写入心电输出缓存中,然后轮询LVGL的任务和触摸屏扫描。就这样不停地循环。其中心电输入缓存中的数据是通过中断从ADS1292R的输出引脚中读取,而心电输出缓存则是原始数据经过低通处理后的数据,等待LVGL显示任务的到来并显示在触摸屏上。系统总体框图和软件框图如下所示


在前面讨论过心电信号频谱和噪声,因而要对心电信号进行滤波,为了同时实现心电信号的实时滤波和心电波形实时显示,所以有必要设计一个缓存区来解决这个难题。这里我打算用我自己设计的两个循环队列解决这个难题。

为了使得在滤波的时候,心电数据依然能够采集,设计两个循环队列,如上图所示,其中IN_Buffer和OUT_Buffer的每个矩形框表示25x4个字节的空间,这取决一次需要多少字节的数据滤波。这里一次滤波需要25个int型的数据,因而每个缓存需要25x4字节。图中的蓝色填充表示缓存区中填满了数据,每次读完数据之后都需要切换缓存区,且IN_Buffer和OUT_Buffer的读写操作相反,即IN_Buffer的读操作是OUT_Buffer的写操作,程序框图如下图所示。

图上所示的三个程序均是并行处理的,
程序1是通过外部中断的服务函数调用的,
程序2则是在UI画图程序里面通过定时器周期性的调用,
程序3则是在主程序中的滤波函数里面调用
程序1代码如下(ADS1292R采用中断方式读取数据):
void EXTI9_5_IRQHandler(void)
{
if(EXTI->IMR&EXTI_Line5 && ADS_DRDY==0)//数据接收中断
{
ADS1292_Read_Data(ads1292_Cache);//数据存到9字节缓冲区
Update_ECG_Data(ads1292_Cache);
Cheack_lead_stata(ads1292_Cache);
if(state_pcb.SampleStartFlag==true)
WriteAdsInBuffer(ecg_info.ecg_data);//数据写入缓存区
}
EXTI_ClearITPendingBit(EXTI_Line5);
}
程序2代码如下(LVGL的心跳在定时器中周期调用,同时程序2也在其中运行,主要从滤波后的数据缓存中取出数据进行波形显示):
void Wave_show(void)
{
int value=0;
if(ReadEcgOutBuffer(&value)!=0) {
if(ecg_graph.send_type==GRAPH) {
ecg_graph.y_pose=Transf_EcgData_To_Vert(value,ecg_graph.sacle);
chart_add_data(ecg_graph.y_pose);
set_data_into_heart_buff(ecg_graph.y_pose);
} else if(ecg_graph.send_type==USART) {
//EcgSendByUart(value);
printf("%d\r\n",(int)alg(value/200));
}
}
}
//定时器3中断服务程序
void TIM3_IRQHandler(void)
{ static u8 show_cnt=0;
if(TIM3->SR&TIM_IT_Update)//溢出中断
{ show_cnt++;
lv_tick_inc(1);//lvgl的1ms心跳
if(show_cnt==3){
show_cnt=0;
Wave_show();
}
}
TIM3->SR = (uint16_t)~TIM_IT_Update;
}
程序3代码如下(在滤波函数中调用,用于承上启下,即从IN缓存中取出数据,滤波之后写入OUT缓存中):
void arm_fir_f32_lp(void)
{
float32_t *inputf32, *outputf32;
if(ReadAdsInBuffer() && WriterEcgOutBuffer()){//指针定位成功
/* 初始化输入输出缓存指针 */
inputf32 = (float32_t *)InFifoDev.rp;
outputf32 =(float32_t *)OutFifoDev.wp;
/* 实现FIR滤波 */
arm_fir_f32(&S, inputf32, outputf32, BLOCK_SIZE);
//my_memcpy(OutFifoDev.wp,InFifoDev.rp,BLOCK_SIZE*4);
InFifoDev.state[InFifoDev.read_front]=Empty;
InFifoDev.read_front=(InFifoDev.read_front+1)%PACK_NUM;//切换读缓存块
OutFifoDev.state[OutFifoDev.writer_rear]=Full;
OutFifoDev.writer_rear=(OutFifoDev.writer_rear+1)%PACK_NUM;//切换写缓存块
}
}
关于缓存切换代码如下:
static void WriteAdsInBuffer(int date)
{
static u8 cnt=0;
if(InFifoDev.state[InFifoDev.writer_rear]==Empty){//缓存块可写
InFifoDev.wp=&AdsInBuffer[InFifoDev.writer_rear*(BLOCK_SIZE)];//将写指针定位写缓存块
InFifoDev.wp[cnt++]=date;
if(cnt==BLOCK_SIZE){
cnt=0;
InFifoDev.state[InFifoDev.writer_rear]=Full;
InFifoDev.writer_rear=(InFifoDev.writer_rear+1)%PACK_NUM;//切换写缓存块
}
}
}
//定位读指针
//成功则返回1,不成功则返回0
u8 ReadAdsInBuffer(void)
{
if(InFifoDev.state[InFifoDev.read_front]==Full){//缓存块可读
InFifoDev.rp=&AdsInBuffer[InFifoDev.read_front*(BLOCK_SIZE)];//将读指针定位读缓存块
return 1;
}
return 0;
}
//定位读指针
u8 WriterEcgOutBuffer(void)
{
if(OutFifoDev.state[OutFifoDev.writer_rear]==Empty){//缓存块可写
OutFifoDev.wp=&EcgOutBuffer[OutFifoDev.writer_rear*(BLOCK_SIZE)];//将读指针定位读缓存块
return 1;
}
return 0;
}
//成功则返回1,不成功则返回0
u8 ReadEcgOutBuffer(int32_t *p)
{
static u8 cnt=0;
if(OutFifoDev.state[OutFifoDev.read_front]==Full){//缓存块可读
OutFifoDev.rp=&EcgOutBuffer[OutFifoDev.read_front*(BLOCK_SIZE)];//将写指针定位读缓存块
*p=OutFifoDev.rp[cnt++];
if(cnt==BLOCK_SIZE){
cnt=0;
OutFifoDev.state[OutFifoDev.read_front]=Empty;
OutFifoDev.read_front=(OutFifoDev.read_front+1)%PACK_NUM;//切换写读缓存块
}
return 1;
}
return 0;
}
滤除工频噪声的数字滤波算法主要有经典滤波器、小波变换、自适应滤波。小波变换能将心电信号进行多层分解,可以使得心电信号与工频噪声分离,但是计算量大,所占用的中间变量也比较多,对于单片机来说,处理的速度也不够快,因而对于系统的实时性这一指标很难实现。自适应滤波能够自动跟踪工频噪声的改变,但是需要增加一个输入信号作为参考,因而增加了系统的复杂性。在前面也讨论过心电信号95%的能量都是集中在0~40Hz,而工频噪声则在50Hz左右,过渡带比较宽,因而可以选择截止频率为40Hz的低通滤波器。
该低通滤波器利用MATLAB的FDATOOL生成,只需要选择低通滤波器是FIR结构,选择Blackman-Harris窗函数,滤波器的阶数定为50,选择采样频率为250Hz,截止频率为40Hz,参数如下图所示:

然后利用FDATOOL生成的冲激响应的数组,选择ARM官方的DSP库,调用arm_fir_f32函数,既可以完成一次滤波。但是在这之前,需要调用arm_fir_init_f32进行初始化。
滤波器系数如下:
const float32_t fir32LP[NUM_TAPS] = {
-7.484454468902e-22,-3.269336712398e-06,-1.365915864079e-05,-5.014073980636e-06,
6.804735231975e-05,0.0001662336497003,7.965197426322e-05,-0.0003784662837741,
-0.0008928563387901,-0.0005280588787408, 0.001284875839485, 0.003225662215767,
0.0022425431358,-0.003157084585057,-0.009028737319977,-0.007219934929014,
0.006057868257093, 0.02144319498633, 0.01971312591228,-0.009448071870685,
-0.04806332586811, -0.05291973061693, 0.01224382260678, 0.1388254178822,
0.2663085232723, 0.3199984843521, 0.2663085232723, 0.1388254178822,
0.01224382260678, -0.05291973061693, -0.04806332586811,-0.009448071870685,
0.01971312591228, 0.02144319498633, 0.006057868257093,-0.007219934929014,
-0.009028737319977,-0.003157084585057, 0.0022425431358, 0.003225662215767,
0.001284875839485,-0.0005280588787408,-0.0008928563387901,-0.0003784662837741,
7.965197426322e-05,0.0001662336497003,6.804735231975e-05,-5.014073980636e-06,
-1.365915864079e-05,-3.269336712399e-06,-7.484454468902e-22
};
static float32_t firStateF32[BLOCK_SIZE + NUM_TAPS - 1];
arm_fir_instance_f32 S;
void arm_fir_Init(void)
{
arm_fir_init_f32(&S, NUM_TAPS, (float32_t *)&fir32LP[0], &firStateF32[0], BLOCK_SIZE);
}
滤波函数见程序3(往上找)
基线漂移与工频噪声不同,它是由于呼吸和电极滑动变化所异致的,其频率一般低于1Hz左右。常见对于基线漂移滤除的数字算法有高通滤波器、中值滤波、小波变换、形态学滤波、曲线拟合等,其中高通滤波器可能会对心电信号的ST波段产生影响,毕竟基线漂移的频率也在ST波段里面。曲线拟合对较大的基线漂移处理能力较弱,处理的效果与处理数据的长度成正相关,因而不适用实时处理的系统。小波变换计算量大,也不适用实时处理的系统。相比之下,形态学滤波对心电信号的基线漂移滤除效果更好,计算量也比中值滤波小。但是形态学滤波要求数据长度足够长,因而会改变前面的缓存结构,并且在本系统中并未太严重的基线漂移,系统的任务也比较多,多方面权衡之下,选择不处理基线漂移。
肌电噪声主要是由于人体肌肉颤抖导致体表的电位发生变化,这种噪声通过电极贴传导至心电模拟前端,并且这种噪声持续时间较短,使得ECG信号波形产生细小的波纹,这种噪声频率分布比较广,前面已经将心电信号通过截止频率为40Hz的低通滤波器,因而需要5点平滑滤波将细小的波纹滤除,为了不影响心电信号的实时处理,因而改进版的平滑滤波器代码如下:
/*
* 滑动平均值滤波。
* 每调用一次,就加入一个新数据,并得到当前的滤波值。
*/
float alg(float new_val)
{
/* 用一个减法,就做了"丢弃最旧的数据,加入最新的数据"这一操作 */
sum += (new_val - buf[pos]);
buf[pos] = new_val;
pos = (pos + 1) % MAX_COUNT;
/* 个数不足时,cnt是实际个数,个数足够时,cnt最多也只是MAX_COUNT */
pcnt += (pcnt < MAX_COUNT);
return sum / MAX_COUNT;
}
心率和QRS宽度检测作为本系统的算法核心,有了心率值和QRS宽度值才能进一步判断常见的心律失常。心率基本上都是检测两个R波之间的时隙来计算的,常见检测R的算法主要有阈值法、模板法和语句描述法。
而本系统的心率和QRS宽度检测算法是在一起检测的,所采用的算法是幅度阈值检测和差分检测相结合,因为观察心电信号的R波,发现R波是具有窄的脉冲,且脉冲的幅度是心电信号最高的,因而采用幅度和一阶差分共同约束找到R波,同时在找R波的同时还可以估计出QRS的宽度,算法的框图如图

心率检测和QRS宽度检测算法是采用状态机的编程思想,通过R波幅度大且从Q到R一直递增,并且R波到S波的一阶差分值很大,从而将R波定位出来,检测两个R波之前的时间,然后通过如下公式就可以计算出心率:
H
R
=
(
60
∗
S
a
m
p
l
e
R
a
t
e
)
/
c
o
u
n
t
HR=(60*SampleRate) /count
HR=(60∗SampleRate)/count
而QRS宽度则是由
Q
R
S
=
Q
R
S
c
n
t
∗
2.2
∗
1000
/
(
S
a
m
p
l
e
R
a
t
e
)
QRS=QRScnt*2.2* 1000/(SampleRate)
QRS=QRScnt∗2.2∗1000/(SampleRate)
上式中的2.2是估计值,因为QRS_cnt是在检测到R波之后才开始计数,并且未到S波谷停止计数,观察QRS波,发现Q到R与R到S近似对称,因而采用2.2这个估计值,这也是实时检测的缺陷,检测的样本不多。
心率算法和QRS宽度检测代码如下:
/**
* @Brief 测量心率
* @Call
* @Param
* @Note
* @Retval
*/
void ecg_heart_rate(int data)
{
int Signal=data;
if(Signal>hr.vmax)
hr.vmax=Signal;
if(Signal<hr.vmin)
hr.vmin=Signal;
thresh=hr.vmax-(hr.vmax-hr.vmin)/5;
for( uint16_t i = 0; i <= DATA_NUM_CAL_HR - 2; i++ )
{
DataArrayCalHR[i] = DataArrayCalHR[i + 1];
}
DataArrayCalHR[DATA_NUM_CAL_HR - 1] = Signal;
Diff_Arrray( DiffDataArrayCalHR, DataArrayCalHR, DATA_NUM_CAL_HR ); //差分
if(hr.flag==StartDetected){
uint8_t FlagAllDiffRise = true;
for( uint16_t i = 0; i <= DATA_NUM_CAL_HR - 2; i++ ) //判断波形是否一直上升
{
if( DiffDataArrayCalHR[i] <= 0 )
{
FlagAllDiffRise = false;
break;
}
}
if(FlagAllDiffRise==true){
hr.flag=QWave;
}
}
else if(hr.flag==QWave)//已经找Q波
{
if(DataArrayCalHR[DATA_NUM_CAL_HR-1]>thresh){
if(hr.count>125){
if( hr.firstBeat==true )//如果已经找到 过R波
{
hr.rate=(float)60*SAMPLE_RATE/(hr.count);
hr.count=0;//清除计数
hr.flag=RWave;
QRScntflag=true;
} else if(hr.firstBeat==false) {
hr.firstBeat=true;
hr.count=0;//清除计数
hr.flag=RWave;
QRScntflag=true;
}
}
}
}
else if(hr.flag==RWave ){
if(DiffDataArrayCalHR[0]<-(hr.vmax-hr.vmin)/5){
hr.flag=SWave;
}
}
else if(hr.flag==SWave){
if(hr.QRS_cnt<15){
hr.flag=StartDetected;
hr.QRS=hr.QRS_cnt*22*100/SAMPLE_RATE;
hr.QRS_cnt=0;
QRScntflag=false;
}else {
hr.flag=StartDetected;
hr.QRS=0;
hr.QRS_cnt=0;
QRScntflag=false;
}
}
if(hr.count>420){
hr.firstBeat=false;
hr.flag=StartDetected;
hr.vmax=Heart_MIN;
hr.vmin=Heart_MAX;
hr.rate=0;
thresh=0;
hr.count=0;//清除计数
}
}
实机演示
本系统因为没有加入操作系统的管理,造成实现的功能较为少,并且数据分析功能因为缺乏强大处理器造成数据分析功能所需要的指标太少,要想对心电信号实现自动化分析,必须对心电信号更多的信息进行提前,而且由于处理器的限制使得一些强大的数字算法是用不了,而且将采集—滤波—显示集成一体化本身就显得笨重,会让每个处理单元相互牵制,严重的会影响系统的采样率,造成一些不必要的误差。所以日后会有针对选择更为强大的处理器,会将采集、滤波、显示分开来。同时为了减少外界噪声,应该选择更为干净的电源和屏蔽外壳,系统也不能以这种模块化的方式拼接在一起,日后会选择画PCB,将所有模块集成在PCB上,在套上屏蔽壳,这样能够最大程度减少外界噪声干扰。并且无线通信模块保留着,但是上位机并未实现,因而日后需要增加这一项功能。总结以上的不足如下:
代码我会放在github上,链接如下:
https://github.com/lvzhe-speed/STM32_ECG
后面我去考研了,希望能够考上,以后会每一年至少写一篇技术博客,谢谢各位大佬前来斧正,欢迎探讨,一起技术进步。其实我也想借此说一下,不能因为一次失败就否定自己之前的努力,100-1=0这也许是别人对你的评价,但自己不能认同这个错误的式子,人生不仅有加法,也有减法。也希望对别人多一点谅解,每个人都不容易,也许今日之菜鸟,明日之大鹏,总会翱翔九天。
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc
我正在寻找用于Rails的优质管理插件。似乎大多数现有的插件/gem(例如“restful_authentication”、“acts_as_authenticated”)都围绕着self注册等展开。但是,我正在寻找一种功能齐全的基于管理/管理角色的解决方案——但不是简单地附加到另一个非基于角色的解决方案。如果我找不到,我想我会自己动手......只是不想重新发明轮子。 最佳答案 RyanBates最近做了两个关于授权的railscast(注意身份验证和授权之间的区别;身份验证检查用户是否如她所说的那样,授权检查用户是否有权访问资源
我正在根据Rakefile中的现有测试文件动态生成测试任务。假设您有各种以模式命名的单元测试文件test_.rb.所以我正在做的是创建一个以“测试”命名空间内的文件名命名的任务。使用下面的代码,我可以用raketest:调用所有测试require'rake/testtask'task:default=>'test:all'namespace:testdodesc"Runalltests"Rake::TestTask.new(:all)do|t|t.test_files=FileList['test_*.rb']endFileList['test_*.rb'].eachdo|task|n
我想要像“嘿那里”这样的东西变成,例如,#316583。我希望将任意长度的字符串“归结”为十六进制颜色。我不知道从哪里开始。我在想,每个字符串的MD5散列都是不同的-但如何将该散列转换为十六进制颜色数字? 最佳答案 你可以只取几位前几位:require'digest/md5'color=Digest::MD5.hexdigest('Mytext')[0..5] 关于ruby-如何使用Ruby基于字母数字字符串生成颜色?,我们在StackOverflow上找到一个类似的问题:
文章目录1.自动驾驶实战:基于Paddle3D的点云障碍物检测1.1环境信息1.2准备点云数据1.3安装Paddle3D1.4模型训练1.5模型评估1.6模型导出1.7模型部署效果附录show_lidar_pred_on_image.py1.自动驾驶实战:基于Paddle3D的点云障碍物检测项目地址——自动驾驶实战:基于Paddle3D的点云障碍物检测课程地址——自动驾驶感知系统揭秘1.1环境信息硬件信息CPU:2核AI加速卡:v100总显存:16GB总内存:16GB总硬盘:100GB环境配置Python:3.7.4框架信息框架版本:PaddlePaddle2.4.0(项目默认框架版本为2.3
LL库和HAL库简介LL:Low-Layer,底层库HAL:HardwareAbstractionLayer,硬件抽象层库LL库和hal库对比,很精简,这实际上是一个精简的库。LL库的配置选择如下:在STM32CUBEMX中,点击菜单的“ProjectManager”–>“AdvancedSettings”,在下面的界面中选择“AdvancedSettings”,然后在每个模块后面选择使用的库总结:1、如果使用的MCU是小容量的,那么STM32CubeLL将是最佳选择;2、如果结合可移植性和优化,使用STM32CubeHAL并使用特定的优化实现替换一些调用,可保持最大的可移植性。另外HAL和L
我正在尝试整个BDD方法并想测试AMQP基于Vanilla的方面Ruby我正在写的应用程序。选择Minitest后作为与其他名副其实的蔬菜框架不同的平衡功能和表现力的测试框架,我着手编写此规范:#File./test/specs/services/my_service_spec.rb#Requirementsfortestrunningandconfigurationrequire"minitest/autorun"require"./test/specs/spec_helper"#Externalrequires#MinitestSpecsforEventMachinerequire