2024年1月

1、准备材料

开发板(
正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

野火DAP仿真器

keil µVision5 IDE(
MDK-Arm

ST-LINK/V2驱动

CH340G Windows系统驱动程序(
CH341SER.EXE

XCOM V2.6串口助手

2、实验目标

使用STM32CubeMX软件配置
两台STM32F407开发板的CAN1模块实现双机通信

3、实验流程

3.0、前提知识

3.0.1、CAN总体概述

STM32F407内部有两个CAN控制器,其中CAN1做为主CAN拥有所有的权限,而CAN2做为从CAN不能单独设置验证筛选器,每个CAN都有3个发送邮箱和两个接收FIFO,每个接收FIFO可以存储三条完整消息,具体的CAN框图如下图所示
(注释1)

CAN的总线网络结构有开环和闭环两种形式

闭环网络结构下,两根信号线H/L组成一个环路,在网络环路的两端连接120欧姆的电阻,这种网络是一种高速、短距离的CAN网络,通信速率最高1Mbit/s;

开环网络结构下,两根信号线H/L各自独立,在两根信号线上各自串联一个2.2千欧的电阻,这种网络是一种低速、远距离的CAN网络,通信速率最高125kbit/s;

如下图所示为开环/闭环的CAN总线网络结构
(注释2)

不管是开环还是闭环CAN网络结构都可以挂载多个节点,
CAN网络上的每个节点由CAN控制器和CAN收发器组成
,STM32F407内部集成的是CAN控制器,因此硬件设计时需要外部搭载CAN收发器才可以组成一个完整的CAN节点,如下图所示为笔者使用的开发板上搭载的CAN收发器芯片硬件原理图

这里CAN收发器芯片的TX/RX引脚并没有直接与单片机CAN1控制器的TX/RX引脚相连接,而是通过了一个跳线帽来进行调节,
读者在做该实验的时候应该注意USB/CAN排针座应该利用跳线帽将CAN_TX/RX引脚与USB_D+/D-进行短接
,具体硬件原理图如下图所示

3.0.2、CAN位时序和波特率

CAN通信是一种异步通信
,异步通信的收发双方无时钟同步,因此需要确保收发双方发送/接收一帧数据的帧格式和波特率保持一致,这样才能保证收发双方正确的进行通信

CAN1/2挂载在APB1最高42MHz的时钟总线上,其一个
时间片
的长度由PCLK1频率和CAN分频参数决定,假设PCLK1频率为25MHz,CAN分频参数位5,则一个时间片的长度为5/25Mhz=0.0000002s=0.0002ms=0.2us=200ns

CAN网络上一个节点采集一个位数据的时序叫做
位时序
,位时序由同步段(SYNC_SEG)、位段 1(BS1)和位段2(BS2)三段组成,其中同步段固定为一个时间片,在该段总线上应该发生一次位信号的跳变;位段1定义了采样点的位置,其可以是1-16个时间片;位段2定义了发送点的位置,其可以是1-8个时间片;

除了上面几个可调节的参数外,CAN还有一个再同步跳转宽度(SJW)参数可以调节,其取值可以是1-4个时间片,通过调节该参数长短决定了CAN再同步时自动调节位段1和位段2长度缩短/加长的上限,此处笔者未深究该参数

通过调节
时钟分频、位段1、位段2和再同步跳转宽度SJW
四个参数,就可以确定CAN通信的波特率,如下图所示位位序数的结构图
(注释1)

3.0.3、CAN帧格式

CAN通信过程中共有数据帧、遥控帧、错误帧、过载帧和帧间空间五种不同用途的帧
,其中数据帧和遥控帧又有标准格式的帧和扩展格式的帧两种,数据帧可以理解为CAN网络上的节点发送消息ID+要发送的数据,遥控帧可以理解为CAN网络上某个节点需要另外一个节点的数据,收到遥控帧的节点就发送对应的数据给请求数据的节点,这里不详细介绍每个帧的格式,想要知道具体帧格式的读者请阅读其他文章,如下图所示为CAN数据/遥控帧一帧的结构图
(注释1)

在HAL库中有一个CAN发送消息头结构体CAN_TxHeaderTypeDef,以下为结构体内主要定义参数

  1. IDE(帧格式):可选参数CAN_ID_STD(标准格式帧)和CAN_ID_EXT(扩展格式帧)
  2. StdId(标准格式帧ID):可选值范围0-0x7FF(11位)
  3. ExtId(扩展格式帧ID):可选值范围0-0x1FFF FFFF(29位,其中标准11位+扩展18位)
  4. RTR(帧类型):可选参数CAN_RTR_DATA(数据帧)和CAN_RTR_REMOTE(遥控帧)
  5. DLC(发送数据的长度):可选值范围0-8
  6. TransmitGlobalTime(传输时间戳使能):ENABLE/DISABLE

3.0.4、CAN验收筛选器

CAN网络上的所有节点没有地址的概念,因此当某个节点发送了特定ID的一条数据帧的时候,所有节点都会收到该帧消息,但是该帧应该只被需要接收该帧的节点接收,而其他不需要接收该数据帧的节点应该自动筛除掉该消息,减少资源浪费,
那一个节点如何判断是否应该接收该帧呢?

配置CAN控制器的验收筛选器
,STM32F407的CAN提供了28个可调整/可配置的标识符筛选器组,注意CAN1/2共用这个标识符筛选器组,而且CAN2不能够单独直接配置,需要使用CAN1来配置,这里的筛选功能为硬件筛选功能,无需软件筛选,可以节省软件筛选所需的CPU资源

筛选器可配置为掩码模式或标识符列表模式
,在掩码模式下,标识符寄存器与掩码寄存器关联,用以指示标识符的哪些位“必须匹配”,哪些位“无关”

在HAL库中有一个CAN过滤器配置结构体CAN_FilterTypeDef用于配置筛选器,以下为结构体内主要定义参数

  1. FilterMode(筛选器模式):可选参数CAN_FILTERMODE_IDMASK(掩码模式)和CAN_FILTERMODE_IDLIST(标识符列表模式)
  2. FilterBank(筛选器组):指定将初始化的筛选器组,单CAN时参数范围0-13,双CAN时参数范围0-27
  3. FilterFIFOAssignment(分配给筛选器的FIFO):指定分配给过筛选器的FIFO0/1,可选参数CAN_FILTER_FIFO0(FIFO0)和CAN_FILTER_FIFO1(FIFO1)
  4. FilterScale(筛选器宽度):CAN_FILTERSCALE_16BIT(两个16位)和CAN_FILTERSCALE_32BIT(一个32位)
  5. FilterActivation(筛选器使能):CAN_FILTER_DISABLE(不使能)和CAN_FILTER_ENABLE(使能)
  6. FilterIdHigh(CAN_FxR1 的高16位):在32位的屏蔽位模式下,用于指定这些位的标准值
  7. FilterIdLow(CAN_FxR1 的低16位):在32位的屏蔽位模式下,用于指定这些位的标准值
  8. FilterMaskIdHigh(CAN_FxR2的高16位):在32位的屏蔽位模式下,用于指定需要关心哪些位
  9. FilterMaskIdLow(CAN_FxR2的低16位):在32位的屏蔽位模式下,用于指定需要关心哪些位

这一部分很重要,不配置筛选器则CAN不能正常接收数据,具体配置请阅读本实验3.2.3小节程序,这里笔者自认为讲的不是很到位,读者可以阅读“
STM32 CAN 过滤器、滤波屏蔽器配置总结
”文章

3.0.5、CAN工作模式

CAN1/2有工作模式和测试模式两种模式,工作模式中又包括初始化模式、正常模式和睡眠模式三种,测试模式中包括静默模式、环回模式和环回与静默组合模式三种,测试模式主要用于测试单个CAN是否工作正常,本实验主要实现双机通信,因此CAN工作在正常模式即可,由于内容太多这里不再详细介绍,具体内容可参考STM32F4xx 参考手册 RM009,如下图所示为测试模式下结构示意图
(注释1)

3.0.6、CAN发送和接收流程

CAN1/2均有3个邮箱可以发送数据,当用户发送数据时只需要
利用CAN_TxHeaderTypeDef结构体生成要发送的帧(具体可阅读本实验3.0.3小节),然后使用HAL_CAN_AddTxMessage()函数将生成的帧添加到邮箱即可
,如果此时有空闲邮箱那么邮箱就会被挂起,当该邮箱具有最高优先级的时候就会安排发送出去,发送出去的过程完全由硬件实现,用户只需按要求生成帧,然后放入空闲邮箱即可,如下图所示为发送邮箱状态流程图
(注释1)

CAN1/2均有两个接收FIFO,每个接收FIFO都有三级深度,通俗理解就是有三个邮箱,可以接收三条信息,当发送的信息通过某个CAN网络上的节点验收筛选器成为一条有效信息时,那么该消息就会被该节点的CAN接收FIFO接收,同时该FIFO的接收0会被挂起,如果持续收到消息,该FIFO的接收1/2也会被挂起,直到三个邮箱全部用完,如果开启了CAN RX接收中断,那么当FIFO接收0/1/2/被挂起时会进入对应的中断服务回调函数中,
当使用HAL_CAN_GetRxMessage()函数读取掉接收FIFO0/1的某级深度的消息时,该级别邮箱将会被释放
,方便接收下一条消息,如下图所示为接收FIFO状态流程图
(注释1)

值得提醒的是,
在本实验中由于笔者使用了两个一摸一样的开发板,因此下面配置的一套程序可以直接烧录到两个开发板上就可以通信
,但是如果读者使用了不一样的开发板,每个板子的配置流程和下述流程一模一样,但请注意调节时钟树和CAN的参数配置时将两个开发板的CAN通信波特率调节为一致,两个开发板的连接应该如下图所示连接
(注释4)

另外就是尽量使用CAN1作为通信CAN,因为在STM32F407的两个CAN中CAN1为主CAN,本实验使用CAN2可能在接收信息时存在问题(大概率笔者没有彻底了解到应该对CAN2如何正确配置),读者可以自行尝试

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“
STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

本实验时钟树建议按照下图所示将MCU时钟频率配置为100MHz,APB1时钟频率配置为25MHz,这样设置是为了能够得到一个整数的时间片,当然也可以和之前的实验类似,将所有总线频率均设置为最高频率

3.1.2、外设参数配置

本实验需要初始化开发板上KEY2用户按键做普通输入,具体配置步骤请阅读“
STM32CubeMX教程3 GPIO输入 - 按键响应

本实验需要需要初始化USART2作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信

单击Pinout & Configuration页面左边Connectivity/CAN1,在Mode中勾选Activated激活CAN1,在其下方的参数配置栏目中按照图示参数配置即可,位时序参数详解可以阅读本实验“3.0.2、CAN位时序和波特率”小节

3.1.3、外设中断配置

在Pinout & Configuration页面左边System Core/NVIC中
勾选CAN1 TX interrupts和CAN1 RX0 interrupts发送接收两个中断
,然后选择合适的中断优先级即可,具体如配置下图所示

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节

3.2.1、外设初始化调用流程

在生成的工程代码中新增加了MX_CAN1_Init()函数,该函数对CAN1的参数进行了配置,并调用了CAN初始化函数HAL_CAN_Init()

在该CAN初始化函数HAL_CAN_Init()中调用了HAL_CAN_MspInit()函数对外设CAN1所需要的时钟使能,引脚复用和中断进行了配置

CAN1具体初始化调用流程如下图所示

3.2.2、外设中断调用流程

在STM32CubeMX中勾选CAN1的TX中断和RX0中断后,会在生成的工程代码stm32f4xx_it.c中新增CAN1_TX_IRQHandler()和CAN1_RX0_IRQHandler()中断服务函数

这两个中断服务函数均调用了HAL库的CAN中断统一处理函数HAL_CAN_IRQHandler(),在该函数中
当CAN1邮箱0发送完成消息后会调用HAL_CAN_TxMailbox0CompleteCallback()函数,当CAN1FIFO0消息挂起时会调用HAL_CAN_RxFifo0MsgPendingCallback()函数
,这两个函数均为虚函数,需要用户重新实现

CAN1接收/发送中断具体调用流程如下图所示

3.2.3、添加其他必要代码

在can.c中添加FIFO0的消息筛选器函数CAN_SetFilters(),然后添加CAN发送数据测试函数CAN1_Send_Test(),具体源代码如下所示
(注释3)

//设置筛选器,要在完成CAN初始化之后调用此函数
HAL_StatusTypeDef CAN_SetFilters(void)
{
    CAN_FilterTypeDef	canFilter;                      //筛选器结构体变量
     // Configure the CAN Filter
    canFilter.FilterBank = 0;		                    //筛选器组编号
    canFilter.FilterMode = CAN_FILTERMODE_IDMASK;	    //ID掩码模式
    canFilter.FilterScale = CAN_FILTERSCALE_32BIT;	    //32位长度
    //设置1:接收所有帧
    //  canFilter.FilterIdHigh = 0x0000;		            //CAN_FxR1 的高16位
    //	canFilter.FilterIdLow = 0x0000;			            //CAN_FxR1 的低16位
    //	canFilter.FilterMaskIdHigh = 0x0000;	            //CAN_FxR2的高16位。所有位任意
    //	canFilter.FilterMaskIdLow = 0x0000;		            //CAN_FxR2的低16位,所有位任意
    //设置2:只接收stdID为奇数的帧
    canFilter.FilterIdHigh = 0x0020;		            //CAN_FxR1 的高16位
    canFilter.FilterIdLow = 0x0000;			            //CAN_FxR1 的低16位
    canFilter.FilterMaskIdHigh = 0x0020;	            //CAN_FxR2的高16位
    canFilter.FilterMaskIdLow = 0x0000;		            //CAN_FxR2的低16位
    
    canFilter.FilterFIFOAssignment = CAN_RX_FIFO0;		//应用于FIFO0
    canFilter.FilterActivation = ENABLE;		        //使用筛选器
    canFilter.SlaveStartFilterBank = 14;		        //从CAN控制器筛选器起始的Bank
    HAL_StatusTypeDef result=HAL_CAN_ConfigFilter(&hcan1, &canFilter);
    return result;
}
 
/*CAN发送数据测试函数*/
void CAN1_Send_Test(uint32_t msgid, uint8_t *data)
{
    TxMessage.IDE = CAN_ID_STD;                         //设置ID类型
    TxMessage.StdId = msgid;                            //设置ID号
    TxMessage.RTR = CAN_RTR_DATA;                       //设置传送数据帧
    TxMessage.DLC = 4;                                  //设置数据长度
    if(HAL_CAN_AddTxMessage(&hcan1, &TxMessage, data, &TxMailbox) != HAL_OK) 
    {
        printf("CAN send test data fail!\r\n");
        Error_Handler();
    }
}

在can.c中重新实现CAN接收/发送中断处理函数,具体源代码如下所示

/*CAN接收FIFO0挂起中断处理函数*/
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    uint8_t  data[8];
    HAL_StatusTypeDef status;
    if(hcan == &hcan1) 
    {
        status = HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxMessage, data);
        if (HAL_OK == status)
        {                             
            printf("--->Data Receieve!\r\n");
            printf("RxMessage.StdId is %#x\r\n", RxMessage.StdId);
            printf("data[0] is 0x%02x\r\n", data[0]);
            printf("data[1] is 0x%02x\r\n", data[1]);
            printf("data[2] is 0x%02x\r\n", data[2]);
            printf("data[3] is 0x%02x\r\n", data[3]);
            printf("<---\r\n");
        }
    }
}
 
/*CAN发送完成中断处理函数*/
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan)
{
    printf("--->Into TxMailbox0CompleteCallback Function!\r\n");
    printf("--->CAN send test data success!\r\n\r\n");
}

在main.c主函数中设置CAN接收筛选器,启动CAN,使能中断,然后再主循环中实现按键控制,每当按键KEY2按下时就调用CAN1_Send_Test()函数发送数据

具体源代码如下所示

/*主循环外程序*/
printf("----- CAN Test Board #1 -----\r\n");
//设置筛选器
if (CAN_SetFilters() == HAL_OK)   
    printf("ID Filter: Only Odd IDs\r\n");
//启动CAN1模块
if (HAL_CAN_Start(&hcan1) == HAL_OK)  
    printf("CAN is started\r\n");
//启用CAN发送/接收中断
if(HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_TX_MAILBOX_EMPTY) != HAL_OK) 
{
    printf("CAN_IT_RX_FIFO0_MSG_PENDING Enable Fail\r\n");
        Error_Handler();
}
uint32_t msg_id=0;
uint8_t data[4] = {0x01, 0x02, 0x03, 0x04};
 
 
/*主循环内程序*/
/*按键KEY2按下*/
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
    {
        for(uint16_t i =0;i<4;i++)
            data[i]++;
 
        CAN1_Send_Test(msg_id++,data);
        while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin));
    }
}

最后将在can.c中定义的消息筛选器函数、发送数据测试函数在can.h中声明即可,具体源代码如下所示

/*can.h中函数声明*/
void CAN1_Send_Test(uint32_t msgid, uint8_t *data);
HAL_StatusTypeDef CAN_SetFilters(void);

4、常用函数

/*CAN开始通信*/
HAL_StatusTypeDef HAL_CAN_Start(CAN_HandleTypeDef *hcan)
 
/*CAN停止通信*/
HAL_StatusTypeDef HAL_CAN_Stop(CAN_HandleTypeDef *hcan)
 
/*获取当前空闲邮箱数量*/
uint32_t HAL_CAN_GetTxMailboxesFreeLevel(const CAN_HandleTypeDef *hcan)
 
/*请求发送相应的邮箱内容*/
HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, const CAN_TxHeaderTypeDef *pHeader,const uint8_t aData[], uint32_t *pTxMailbox)
 
/*获取FIFO中挂起的消息数*/
uint32_t HAL_CAN_GetRxFifoFillLevel(const CAN_HandleTypeDef *hcan, uint32_t RxFifo)
 
/*读取FIFI中挂起的消息信息并释放邮箱*/
HAL_StatusTypeDef HAL_CAN_GetRxMessage(CAN_HandleTypeDef *hcan, uint32_t RxFifo,CAN_RxHeaderTypeDef *pHeader, uint8_t aData[])
 
/*CAN发送完成中断处理函数*/
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan)
 
/*CAN接收中断处理函数*/
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)

5、烧录验证

烧录程序,开发板1/2上电后均显示CAN初始化成功可以开始通信

当第一次按下开发板1的KEY2按键,此时串口输出进入CAN发送完成中断处理函数中并成功发送信息的提示,但是开发板2并没有接收消息(因为开发板1/2均设置了只接收stdID为奇数的帧)

当第二次按下开发板1的KEY2按键时,可以发现开发板2收到了消息,并将接收到的消息打印了出来,具体实验现象如下图所示(左边为开发板1,右边为开发板2)

6、注释详解

注释1
:图片来源 STM32F4xx 中文参考手册 RM009

注释2
:图片来源
通信——CAN总线基础介绍

注释3
:在CAN发送测试函数末尾不要使用printf输出,如果非要使用请在使用前进行1ms延时,否则可能进不去发送完成函数HAL_CAN_TxMailbox0CompleteCallback中

注释4
:图片来源
STM32CubeMX | 36 - 使用CAN总线进行双板通信(TJA1050)

参考资料

STM32Cube高效开发教程(基础篇)

更多内容请浏览
STM32CubeMX+STM32F4系列教程文章汇总贴

最近,有群里在群里发了这么一个非常有意思的卡片 Hover 动效,来源于此网站 --
key-drop
,效果如下:

非常有意思酷炫的效果。而本文,我们不会完全还原此效果,而是基于此效果,尝试去制作这么一个类似的卡片交互效果:

该效果的几个核心点:

  1. 卡片的 3D 旋转跟随鼠标移动效果
  2. 如何让卡片在 Hover 状态,有不同的光泽变化
  3. 如何让卡片在 Hover 状态,有 Blink,Blink 的星星闪烁效果

当然,要做到
卡片的 3D 旋转跟随鼠标移动效果
需要一定程度的借助 JavaScript,因此,最终的效果是 CSS 配合 JavaScript 以及一些动态效果的 Gif 共同实现。

好,下面就让我们一步一步一起来实现这个效果。

卡片的 3D 旋转跟随效果

OK,接下来,如何实现 3D 卡片效果呢?

这个效果之前在
让交互更加生动!有意思的鼠标跟随 3D 旋转动效
实现过一次,我们复习一下。

这个交互效果主要有两个核心:

  1. 借助了 CSS 3D 的能力
  2. 元素的旋转需要和鼠标的移动相结合

我们的目标是实现这样一个动画效果:

这里,我们其实有两个核心元素:

  1. 鼠标活动区域
  2. 旋转物体本身

鼠标在
鼠标活动区域
内的移动,会影响
旋转物体本身
的 3D 旋转,而旋转的方向其实可以被分解为 X 轴方向与 Y 轴方向。

我们来看一下,假设我们的 HTML 结构如下:

<body>
    <div id="element"></div>
</body>

得到这样一个图形:

这里,
body
的范围就是整个鼠标可活动区域,也是我们添加鼠标的
mousemove
事件的宿主 target,而
#element
就是需要跟随鼠标一起转动的旋转物体本身。

因为整个效果是需要基于 CSS 3D 的,我们首先加上简单的 CSS 3D 效果:

body {
    width: 100vw;
    height: 100vh;
    transform-style: preserve-3d;
    perspective: 500px;
}

div {
    width: 200px;
    height: 200px;
    background: #000;
    transform-style: preserve-3d;
}

效果如下:

没有什么不一样。这是因为还没有添加任何的 3D 变换,我们给元素添加 X、Y 两个方向的
rotate()
试一下(注意,这里默认的旋转圆心即是元素中心):

div {
     transform: rotateX(15deg) rotateY(30deg);
}

效果如下,是有那么点意思了:

好,接下来,我们的目标就是通过结合 mouseover 事件,让元素动起来。

控制 X 方向的移动

当然,为了更加容易理解,我们把动画拆分为 X、Y 两个方向上的移动。首先看 X 方向上的移动:

这里,我们需要以元素的中心为界:

  1. 当鼠标在中心右侧连续移动,元素绕 Y 轴移动,并且值从 0 开始,越来越大,范围为(0, +∞)deg
  2. 反之,当鼠标在中心左侧连续移动,元素绕 Y 轴移动,并且值从 0 开始,越来越小,范围为(-∞, 0)deg

这样,我们可以得到这样一个公式:

rotateY = (鼠标 x 坐标 - 元素左上角 x 坐标 - 元素宽度的一半)deg

通过绑定 onmousemove 事件,我们尝试一下:

const mouseOverContainer = document.getElementsByTagName("body")[0];
const element = document.getElementById("element");

mouseOverContainer.onmousemove = function(e) {
  let box = element.getBoundingClientRect();
  let calcY = e.clientX - box.x - (box.width / 2);
    
  element.style.transform  = "rotateY(" + calcY + "deg) ";
}

效果如下:

好吧,旋转的太夸张了,因此,我们需要加一个倍数进行控制:

const multiple = 20;
const mouseOverContainer = document.getElementsByTagName("body")[0];
const element = document.getElementById("element");

mouseOverContainer.onmousemove = function(e) {
  let box = element.getBoundingClientRect();
  let calcY = (e.clientX - box.x - (box.width / 2)) / multiple;
    
  element.style.transform  = "rotateY(" + calcY + "deg) ";
}

通过一个倍数约束后,效果好了不少:

控制 Y 方向的移动

同理,我们利用上述的方式,同样可以控制 Y 方向上的移动:

const multiple = 20;
const mouseOverContainer = document.getElementsByTagName("body")[0];
const element = document.getElementById("element");

mouseOverContainer.onmousemove = function(e) {
  let box = element.getBoundingClientRect();
  let calcX = (e.clientY - box.y - (box.height / 2)) / multiple;
    
  element.style.transform  = "rotateX(" + calcX + "deg) ";
};

效果如下:

当然,在这里,我们会发现方向是元素运动的方向是反的,所以需要做一下取反处理,修改下
calcX
的值,乘以一个
-1
即可:

let calcX = (e.clientY - box.y - (box.height / 2)) / multiple * -1;

结合 X、Y 方向的移动

OK,到这里,我们只需要把上述的结果合并一下即可,同时,上面我们使用的是
onmousemove
触发每一次动画移动。现代 Web 动画中,我们更倾向于使用
requestAnimationFrame
去优化我们的动画,确保每一帧渲染一次动画即可。

完整的改造后的代码如下:

const multiple = 20;
const mouseOverContainer = document.getElementsByTagName("body")[0];
const element = document.getElementById("element");

function transformElement(x, y) {
  let box = element.getBoundingClientRect();
  let calcX = -(y - box.y - (box.height / 2)) / multiple;
  let calcY = (x - box.x - (box.width / 2)) / multiple;
  
  element.style.transform  = "rotateX("+ calcX +"deg) "
                        + "rotateY("+ calcY +"deg)";
}

mouseOverContainer.addEventListener('mousemove', (e) => {
  window.requestAnimationFrame(function(){
    transformElement(e.clientX, e.clientY);
  });
});

至此,我们就能简单的实现鼠标跟随 3D 旋转动效:

设置平滑出入

现在,还有最后一个问题,就是当我们的鼠标离开活动区域时,元素的 transform 将停留在最后一帧,正确的表现应该是复原到原状。因此,我们还需要添加一些事件监听做到元素的平滑复位。

通过一个
mouseleave
事件配合元素的
transition
即可。

div {
    // 与上述保持一致...
    transition: all .2s;
}
mouseOverContainer.addEventListener('mouseleave', (e) => {
  window.requestAnimationFrame(function(){
    element.style.transform = "rotateX(0) rotateY(0)";
  });
});

至此,我们就可以完美的实现平滑出入,整体效果最终如下:

完整的代码,你可以戳这里:
CodePen Demo -- CSS 3D Rotate With Mouse Move

Hover 状态下的光泽变化

好,有了上述铺垫之后,我们就可以将黑色背景图,替换成实际的图片,得到这么一个初步效果:

接下来,我们需要让卡片能够变得有光泽,并且也能基于鼠标 Hover 的坐标不同,展现出不一样的效果,像是这样:

怎么实现呢?看似复杂,其实只需要简单的利用混合模式即可。其中本质就是图片叠加上黑白相间的渐变,再调整混合模式,就能实现上述的高光效果。

代码如下:

<div></div>
div {
    position: relative;
    background: url('image.png');
    
    &::before {
        content: "";
        position: absolute;
        inset: 0;
        background: 
            linear-gradient(
                115deg, 
                transparent 0%, 
                rgba(255, 255, 255, 0.5 30%), 
                rgba(0, 0, 0, .5) 55%), 
                rgba(255, 255, 255, .5) 80%), 
                transparent 100%
            );
        mix-blend-mode: color-dodge;
    }
}

这里,我们利用 div 元素的背景展示了图片,利用元素的伪元素展示了黑白渐变效果,最终再叠加上混合模式
mix-blend-mode: color-dodge
,示意图如下:

但是,此时,只有卡片是有 3D 效果的,叠加的黑白渐变层是不会随着 Hover 效果进行变化的:

为了解决这个问题,我们需要让渐变图层也能受到 Hover 的动态影响,这个好做,我们额外引入一个 CSS 变量,基于鼠标当前 Hover 卡片时,距离卡片最左侧的横向距离,设置动态的 CSS 变量。

改造一下代码:

<div id="g-img"></div>
div {
    --per: 30%;
    position: relative;
     // ...
    
    &::before {
        content: "";
        position: absolute;
        inset: 0;
        background: 
            linear-gradient(
                115deg, 
                transparent 0%, 
                rgba(255, 255, 255, 0.5) var(--per), 
                rgba(0, 0, 0, .5) calc(var(--per) + 25%), 
                rgba(255, 255, 255, .5) calc(var(--per) + 50%), 
                transparent 100%
            );
        mix-blend-mode: color-dodge;
    }
}
const multiple = 15;
const mouseOverContainer = document.getElementsByTagName("body")[0];
const element = document.getElementById("element");
const img = document.getElementById("g-img");

function transformElement(x, y) {
    let box = element.getBoundingClientRect();
    const calcX = -(y - box.y - box.height / 2) / multiple;
    const calcY = (x - box.x - box.width / 2) / multiple;
    const percentage = parseInt((x - box.x) / box.width * 1000) / 10;
    
    element.style.transform = "rotateX(" + calcX + "deg) " + "rotateY(" + calcY + "deg)";

    // 额外增加一个控制 --per 的变量写入
    img.style = `--per: ${percentage}%`;
}

mouseOverContainer.addEventListener("mousemove", (e) => {
    window.requestAnimationFrame(function () {
        transformElement(e.clientX, e.clientY);
    });
});

简单解释一下,上述代码最核心的部分就是引入了
--per
CSS 变量,其应用在渐变代码中。

我们通过计算当前鼠标距离卡片左侧的横向距离,除以卡片整体的宽度,得到
--per
实际表示的百分比,再赋值给
--per
,以此实现 Hover 时候的光效变化:

叠加星星闪烁效果

好,效果已经非常接近了。当然,总感觉缺少什么,我们可以在这一步,继续叠加上另外一层星星闪烁的效果。

这里,我们可以用现成的 GIF 图,像是这样(图片来源于
Pokemon Card Holo Effect
):

这样,我们的整个效果,其实就变成了这种叠加状态:

我们再简单改造一下代码:

#g-img {
    --per: 30%;
    position: relative;
    background: url('image.png');
    
    &::after {
        content: "";
        display: none;
        position: absolute;
        inset: 0;
        background: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/13471/sparkles.gif");
        mix-blend-mode: color-dodge;
    }
    
    &::before {
        content: "";
        display: none;
        position: absolute;
        background: 
            linear-gradient(
                115deg, 
                transparent 0%, 
                rgba(255, 255, 255, 0.7) var(--per), 
                rgba(0, 0, 0, .6) calc(var(--per) + 25%), 
                rgba(255, 255, 255, .5) calc(var(--per) + 50%), 
                transparent 100%
            );
        mix-blend-mode: color-dodge;
    }
    
    &:hover::after,
    &:hover::before {
        display: block;
    }
}

当 Hover 状态下,才展示渐变背景与星星 Gif 图的叠加效果,最终,我们就实现了最开头的效果:

完整的代码,你可以戳这里
CodePen Demo -- CSS 3D Rotate With Mouse Move

尝试不同渐变背景与不同混合模式

了解上述制作方式的全过程后,我们就可以改变叠加的混合模式与渐变背景,以创造更多不一样的效果。

像是这样:

完整的代码,你可以戳这里
CodePen Demo -- CSS 3D Rotate With Mouse Move2

或者是这样:

完整的代码,你可以戳这里
CodePen Demo -- CSS 3D Rotate With Mouse Move3

最后

怎样,学会了吗。通过不同的混合模式与不同的渐变背景,可以排列组合出非常多种有趣有意思的效果。感兴趣的,一定动手试试!


好了,本文到此结束,希望本文对你有所帮助

前言

为什么需要这样一个框架,以及这个框架带来的好处是什么?

从字面意思上理解:该框架可以用来同时管理多个小程序,并且可以抽离公用组件或业务逻辑供各个小程序使用。当你工作中面临这种同时维护多个小程序的业务场景时,可以考虑使用这种模式。灵感来自最近
webpack
的多项目打包构建

起步

首先你得先安装好
taro
脚手架,然后基于该脚手架生成一个
taro
项目

初始化taro项目

taro init miniApp

这是我选择的初始化配置,你当然也可以选择其它模版,只要编译工具选择
webpack
就可以,下面的步骤基本相同

打开项目安装依赖

pnpm install

这样一个基本的taro项目就生成好了,但这样只是生成了一个小程序,那我们如果有许多个小程序是不是还要按上面这些步骤继续生成,当然不需要,这样不仅费时间,而且难以维护。

下面我们就来把这个框架改造成支持同时管理多个小程序。

改造(支持多小程序)

此时的项目结构是这样的:

  • config
    下面是一些小程序以及
    webpack
    的配置
  • src
    下面是我们小程序的项目代码
  • project.config.json
    是当前小程序配置文件
  • ...

改造目录


src
目录下新增目录:
apps

common

  • apps
    :小程序目录,存放各个小程序的代码
  • common
    :公用目录,存放公用组件及业务逻辑代码

apps

这里每个小程序对应一个文件夹,里面存放对应小程序的代码

这里需要把根目录下的
project.config.json
放到小程序目录下,因为每个小程序都需要自己的配置文件

比如:nanjiu、nanjiu_notebook两个小程序

common

这里主要是存放公用代码:组件、业务、请求

修改配置

config/index.js


import path from 'path'
const app = process.env.APP

const config = {
  projectName: 'mini_app',
  date: '2024-1-21',
  designWidth: 750,
  deviceRatio: {
    640: 2.34 / 2,
    750: 1,
    828: 1.81 / 2
  },
  sourceRoot: `src/apps/${app}`, // 项目源码目录
  outputRoot: `${app.toUpperCase()}APP`,  // 打包产物目录
  alias: {
    '@/common': path.resolve(__dirname, '..', 'src/common'), // 别名配置
  },
// ....

module.exports = function (merge) {
  if (process.env.NODE_ENV === 'development') {
    return merge({}, config, require('./dev'))
  }
  return merge({}, config, require('./prod'))
}

这里需要注意的是
sourceRoot
,因为要支持多小程序,那么这里就不能固定写死了,我们可以在启动时通过传参来区分当前启动或打包哪个小程序。

自定义构建脚本

在项目根目录新建文件夹
build
存放构建脚本

// cli.js
const shell = require('shelljs')
const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')

const action = process.argv[2]
let app = process.argv[3]
const runType = action == 'dev' ? '启动': '打包'

function start() {
// 处理配置文件
process.env.APP = app
console.log(`

今天给大家分享一个话题,如何有效的学习编程,大家都知道,我是计算机专业毕业的,2008年开始学习编程,2014年研究生毕业后一直从事软件开发工作,先后在京东、爱奇艺、完美世界从事过软件开发工程师工作,具有十多年编程经验积累,所以我来讲这个话题,我是有发言权的,也具有一定的权威性。

好的,先说一下为什么学习编程。因为编程很重要啊,我认为编程是人生中最重要的技能之一,与驾车、英语同样重要。任何人都能学习编程,只要你能付出时间和热情,都能够学会。而且编程能让你获取更多收入的可能性,根据最近十年全球收入排名统计,软件开发一直排名前列/需求量大。所以,编程就是就是能打开更多人生机会的钥匙。

好的,那如何学习编程呢,我认为学习编程,做到四点就足够了。
第一点:实践
第二点:阅读
第三点:做项目
第四点:问题解决能力

对于初学者来说,第一点是最重要的,通常大家是怎么开始学编程的,无非是找一门编程语言(无论是c语言、java、php、python等等),然后买本教程,或者,买个视频教程,跟着看,跟着学,然后实践,这个阶段是正确的,非常正确的,然而初学者最容易忽略的一点是实践,就像学习英语学习韩语一样,其实学编程并不像学习外语那样,学习外语其实比学习编程难多了,外语的语法复杂,单词巨多,而编程语言就不同了,大多数的编程语言,一共才几十种关键词,public static void int string ,很容易记住。所以还是要多实践。还有就是真正的编程高手是不区分编程语言的,任何语言都能上手,关键是实践。所以从现在开始拿起键盘,开始敲代码,别只学习不动手!学习编程最重要的就是实践。写些简单的代码,试试运行,看看结果。

第二点就是阅读。阅读对于编程者来说同样重要,程序员也要有阅读的习惯,阅读分为3个部分,分别是:
阅读基础理论+阅读框架技巧+阅读经典图书
(1)阅读基础理论,包括计算机基础知识、操作系统、计算机网络、数据库、数据结构。阅读基础能提高对计算机的认识,因为计算机的架构都依赖于基础数据结构和算法和网络,因此懂基础会让我们更好地理解计算机。 而且 无论你用哪门语言编程,其实操作系统知识、数据库知识、计算机网络知识、数据结构、算法这些基础是一样的。 所以基础知识需要长期学习
(2)再就是阅读框架技巧,如果你的基础知识已经学好了,那么可以学习了一部分编程框架了,比如java的框架springboot,php的thingkphp,python的django,javascript的vue。学习了框架之后,可以阅读一些框架的书籍,让你对于这个框架的整体结构有更深入的了解,遇到一些牛逼的设计,往往会感到惊叹。哇塞,这个框架设计的太强悍了,太吊了。会有一种惊叹的感觉。
(3)再就是阅读经典图书,阅读经典图书,网上有很多经典图书,比如图灵、豆瓣,按照自己的兴趣去读,有的图书或许不是讲编程的,或许是讲设计模式,或许是讲网络,作为一种技术沉淀。通过阅读经典图书可以扩宽知识面,提升内功(就像武侠里面的内功)。经典的编程图书不仅仅是技术性的,它们通常也具有很高的可读性(有些图书作者是国外的,写的书也很有意思,很有趣,比如设计模式/tcpip)。通过阅读这些图书,既可以享受阅读带来的乐趣,同时也会激发你的创造力。了解到一些创新性的项目和想法,激发自己的创造力和想象力。而且,这些经典图书还会提到一些其它的优秀书籍,让我们可以顺藤摸瓜,进一步探索相关领域。

第三点就是做项目,学习一段时间后,就可以做一些项目,先从小型项目开始,比如编写一个博客,编写一个计算器,编写一个留言板,编写一个学生信息系统。一个一个不断的做项目,其实做项目就像是练习射击,当你拥有了一把手枪后,只有不断的喂子弹,才会成为一个真正的射击高手。对于编程来说同样如此。

最后一点就是问题解决能力的提升,在编程过程中会遇到很多很多的问题,比如编译问题、运行问题、部署问题、安装问题。在学习编程过程中会遇到很多很多的问题。对于这种情况,我的应对方案是:先自己解决,通过看日志、断点调试来解决,如果是实在解决不了,那么就求助搜索引擎,第一选择是google,第二选择是bing,然后是baidu,我感觉google给出的解决办法会更准确,如果你的网络登不上google,那么可以尝试bing,最后是百度。如果你英文水平好的话,还可以去stackoverflow或github上去寻找答案。(等到1年或2年变成中级工程师之后 你会发现github的作用,简直就是一个宝藏)或者询问chat ai。

最后还有一点需要补充就是作为一名程序员,需要坐的住,需要一天能坐在电脑屏幕前10小时的能力,不是说坐一会,玩一会,刷一会视频。而是一直坐着的能力。我个人认为这种能力是合格程序员的必要技能。

无论IT的就业环境变的多差,程序员永远不会下岗,我认为IT行业依然是非常好的就业行业。

参考资料:

项目简介

Mocha 是一个基于 .NET 开发的 APM 系统,同时提供可伸缩的可观测性数据分析和存储平台。

项目地址:
https://github.com/dotnetcore/mocha

注意:使用
git clone --recursive
克隆本仓库以及子模块。

项目进度

目前项目处于早期阶段,已经完成了对 Trace 的基础支持,可以通过 OTel SDK 上报数据到 Mocha Distributor,然后通过 Grafana 展示。

v0.1.0 发布内容

  • 支持 OTel Trace 数据上报
  • 支持通过 Grafana 展示 Trace 数据
  • 提供 Docker Compose 本地体验方案

项目背景

近年来,可观测性概念被提出和逐渐流行,OpenTelemetry 也逐渐成为最流行的可观测性框架,OTel 很好的解决了多语言系统中Metrics\Trace\Log 的收集和标准化问题,但对于如何存储和分析使用收集到的 M.T.L 数据,业界并没有统一的方案,一般来说大家需要

  1. 使用 Jaeger、Prometheus、Loki 等不同的后端系统搭配,但引入了相当的复杂度
  2. 或者引入 SkyWalking 、ElasticAPM 等 APM 系统
  3. 或者使用 Datadog、SLS 等 SaaS 可观测性平台,需要支付昂贵的流量和数据存储费用

同时上述提到的开源 APM 后端,除 SkyWalking [Java实现]外,其余无一例外使用 Golang 实现。
而从 .NET 5 以来,到目前的 .NET 8,每一个版本都对 CLR 和 BCL做了大量性能优化和提供了面向高性能场景的新语言特性,.NET 的演进很适合开发高性能的云原生中间件。所以我们发起 mocha 项目,使用 .NET 实现一个面向大规模可观测性数据分析和存储的平台。

平台功能


Mocha 将要提供的功能集合:

  • APM 和 分布式追踪
    • 服务概览、R.E.D 指标和可用性监控
    • 服务拓扑
    • 调用监控,包括 HTTP、RPC、Cache、DB、MQ 等
    • 调用链路查询和检索
  • 基础设施监控
    • 主机监控
    • 容器和 Kubernetes 监控
    • 主流中间件监控
  • 日志
    • 日志查询
    • 日志聚合分析
  • 报警
    • 报警规则管理
    • 报警通知
  • M.T.L 数据探索 [Data Explore / Inspect]

技术架构

Mocha 整体架构由下面的部分组成

  • Mocha Distributor Cluster:作为 mocha 系统的数据入口,负责接收 OTel SDK 和 Collector 上报的数据,并通过一致性Hash 将数据路由到对应的 aggregator 节点上。为了保证数据不丢失,最终 Distributor 应该具备本地 FIFO 队列的能力。
  • Mocha Streaming Cluster:mocha 的核心组件,通过读取预配置或者用户配置的 aggr rule dsl 生成对应的 streaming data flow 并执行。Streaming 是具备分布式 shuffle 的能力的有状态组件,需要将自身信息注册到ETCD中。
  • Storage:mocha M.T.L 存储,可以选用开源存储组件,如 ClickHouse、ElasticSearch、victoriametrics 等。
  • Mocha Querier + Grafana: 从存储查询数据并提供给 grafana 做展示。因此要兼容 promql / jeager / loki 等数据源的查询。
  • Mocha Manager : 包括 manager server、dashboard和ETCE组件,集群元数据和 M.T.L 数据分析规则存储。
  • OTel SDK / Collector : 开源 OpenTelemetry 采集套件。

v0.10 快速体验

启动项目

在项目根目录下的docker目录中,执行以下命令启动项目:

docker-compose up -d

启动成功后,可以看到以下容器:

  • distributor: 提供用于接收 OTLP 数据的 gRPC API
  • jaeger-query: 提供用于接收 Jaeger 查询协议的 HTTP API
  • mysql: 用于存储数据
  • grafana: 用于展示数据

Trace 数据的发送

将 SDK 的 OTLP exporter 配置为
http://localhost:4317
即可将数据发送到 distributor。

配置 Jaeger 数据源

我们实现了支持 Jaeger 查询协议的 API,因此可以直接在 Grafana 中配置 Jaeger 数据源。

访问
http://localhost:3000/
即可看到grafana的登录页面。用户名和密码都是admin。

登录后,点击左侧的菜单,选择 Data Sources,然后点击 Add data source。

选择 Jaeger。

配置 Jaeger 数据源的 URL 为
http://jaeger-query:5775

点击 Save & Test,如果显示如下信息,则说明配置成功。

如果还没往 distributor 发送过数据,会显示如下警告信息。

Trace 数据的查询

点击左侧的菜单,选择 Explore,然后选择 Jaeger 数据源,即可看到 Trace 数据。