当前位置: 首页 > news >正文

基于STM32F412+RT-Thread的智能汽车CAN通信仪表盘

文章目录

  • 一、项目概述与工程搭建
    • 1.1 项目概述与演示
      • 1.1.1 项目功能结构图
      • 1.1.2 硬件与软件资源
        • 1.硬件资源
        • 2.软件资源
    • 1.2 硬件原理图与软件开发平台(RT-Thread)
      • 1.2.1 硬件原理图
      • 1.2.2 硬件资源汇总
      • 1.2.3 RT-Thread简介
      • 1.2.4 Env开发工具
    • 1.3 RT-Thread工程抽取
      • 1.3.1 拉取RT-Thread的SDK
      • 1.3.2 抽取工程并调整
    • 1.4 RT-Thread工程配置
      • 1.4.1 配置工程模板
      • 1.4.2 更改MCU型号
      • 1.4.3 CubeMX项目重构
        • 1.重建CubeMX配置
        • 2.开启配置UART
        • 3.点亮LED
  • 二、为“学习迪文屏”准备的工具软件——“串口读写工具”项目
    • 2.1 “串口读写工具”需求分析
    • 2.2 设备接口层与业务逻辑层
    • 2.3 提取并调整RT-Thread工程
    • 2.4 Env管理组和RTT代码规范
    • 2.5 设备接口层实现
    • 2.6 业务逻辑层实现
  • 三、迪文屏开发
  • 四、CAN 调试屏项目软件实现
    • 4.1 CAN 数据分析
    • 4.2 APP 数据分析
    • 4.3 提取并调整RT-Thread工程
    • 4.4 Env管理组和RTT代码规范
    • 4.5 设备接口层实现
    • 4.6 分发器实现
    • 4.7 业务逻辑层实现
    • 4.8 最终代码

一、项目概述与工程搭建

1.1 项目概述与演示

1.1.1 项目功能结构图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1、上位机运行“CAN Pro协议分析平台”软件,通过CAN分析仪向迪文屏(含协议转换板)发送CAN协议数据;
2、协议转换板将CAN协议数据转换成迪文屏协议(串口)数据;
3、协议转换板通过向迪文屏传输迪文协议串口数据,控制迪文屏显示汽车仪表盘相关数据;
4、迪文屏的触摸控件会输出迪文协议串口数据,称为“迪文屏上传数据”;
5、协议转换板将上传数据转换为CAN协议数据,传输给上位机;
6、协议转换板可以通过J-Link和运行在主机上的串口助手,输出调试信息。

1.1.2 硬件与软件资源

1.硬件资源
  • 上位机(Windows)

  • 迪文屏(含协议转换板)
    在这里插入图片描述

  • CAN分析仪
    在这里插入图片描述

  • 多功能J-Link带串口调试器
    在这里插入图片描述

  • SD卡及读卡器
    在这里插入图片描述

2.软件资源

APP编辑、编译及管理软件:

  • MDK V5
  • Sourceinsight
  • CubeMX
  • ENV

迪文工程编辑软件:

  • T5L_DGUS Tool
  • BMP编辑软件

模拟数据收发软件:

  • 串口助手
  • CAN Pro

1.2 硬件原理图与软件开发平台(RT-Thread)

1.2.1 硬件原理图

在这里插入图片描述

1、mcu:STM32F412RET6;
2、Sp3232E:232转TTL;
3、TJA1042T/3/1J:CAN收发器;
4、8M晶振。

说明:

  1. 选择STM32F4系列的主要原因是其SRAM为256K;
  2. 8M晶振是为CAN收发器提供精准时序,提高CAN采样成功率;
  3. 迪文屏输出的是232电平,需要转换为TTL电平。

1.2.2 硬件资源汇总

协议转换板主要硬件资源汇总:
1、外部8M晶振
2、USART1(Rx:PA10、Tx:PA9)
3、USART3(Rx:PC5、Tx:PB10)
4、CAN1(PB9、PB8)

其中,USART3接驳232,即USART3接驳迪文屏串口
USART1可以作为调试串口,也可以作为串口输入

1.2.3 RT-Thread简介

RT-Thread,全称为Real Time-Thread,是一款由中国开源社区主导开发的开源实时操作系统(许可证GPLv2)。它包含了实时、嵌入式系统相关的各个组件,如TCP/IP协议栈、图形用户界面等。

  • 嵌入式实时多线程操作系统
  • 分层开发结构
  • 面向对象编程思想

对RT-Thread有需要学习了解的可以看我前面的文章https://blog.csdn.net/2301_78772787/article/details/147985520?spm=1001.2014.3001.5501

1.2.4 Env开发工具

Env是RT-Thread的开发辅助工具,提供编译构建环境、图形化系统配置、软件包管理等功能。主要特性包括:

  • 使用scons作为构建工具,提供编译环境,生成工程。
  • 内置menuconfig配置剪裁工具,可对内核、组件和软件包进行自由裁剪。
  • 提供多种软件包,可在线下载,各包耦合关联少,具有良好的可维护性

对Env开发工具需要学习了解的可以看我前面的文章https://blog.csdn.net/2301_78772787/article/details/148700726

1.3 RT-Thread工程抽取

1.3.1 拉取RT-Thread的SDK

git clone https://gitcode.com/gh_mirrors/rt/rt-thread.git
cd rt-thread
git checkout v5.0.2

1.3.2 抽取工程并调整

找到与项目对应的mcu的开发板资源包:

在这里插入图片描述

生成 MDK5 项目文件:

scons  --dist  --target=mdk5

在这里插入图片描述

可见多了一个 dist 文件夹:

在这里插入图片描述

进去该文件夹能发现多了一个 project 的文件夹和一个 project 压缩包,我们将压缩包复制出来放到我们的项目目录下,将 dist 文件夹删除:

在这里插入图片描述

1.4 RT-Thread工程配置

如上抽取的RT-THread工程不是完完全全符合项目要求的,比如,mcu不符合、主频、外设等都未必符合。这需要调整。调整过程包括:

1.4.1 配置工程模板

1. 更改Device为STM32F412Rx

打开 project 中的 template 模板工程:

在这里插入图片描述

点击魔术棒,点击Device,找到我们项目对应的开发板型号并修改:

在这里插入图片描述

2. 选中C/C++ -> GNU
为了兼容 GCC 工具链的编译环境,需要去打开GNU选项:

在这里插入图片描述

3. 选择J-Link调试器
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.4.2 更改MCU型号

1. 更改board\SConscript中有关mcu型号的参数

在这里插入图片描述
在这里插入图片描述

用notepad++打开后将里面的 Zx/zx 修改成 Rx/rx

在这里插入图片描述

2. 更改linker_scripts\link.sct中有关”分散加载“的参数

在这里插入图片描述
在这里插入图片描述

有关STM32F412RET数据手册网址:
https://www.st.com/en/microcontrollers-microprocessors/stm32f412re.html
在这里插入图片描述

通过计算器可得,512Byte和256Byte的16进制大小为:

在这里插入图片描述

那么256对应的就是0004 0000,在 link.sct 文件中修改参数:

在这里插入图片描述

想要了解分散加载的可以去学习以下文章:
https://blog.csdn.net/wuhenyouyuyouyu/article/details/71171546

1.4.3 CubeMX项目重构

1.重建CubeMX配置

打开CubeMX,找到我们项目所使用的开发板型号:

在这里插入图片描述

双击创建一个new project,并进行如下工程配置(注意下面的第三步文件名字,我们需要将board路径下的CubeMX_Config改名成CubeMX_Config1,再存放下面的文件):

在这里插入图片描述
在这里插入图片描述

设置下载模式以及外部晶振:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

打开USART1、USART3:
在这里插入图片描述
在这里插入图片描述

最后点击创建,创建后将原来的 CubeMX_Config1 删掉:
在这里插入图片描述

由于RTT原先的时钟配置不符合我们的开发板时钟配置要求,所以创建好CubeMX后打开 CubeMX_Config 的 main.c,将其中的 void SystemClock_Config(void) 与 board.c 中的 void SystemClock_Config(void)
进行替换:
在这里插入图片描述

2.开启配置UART

找到 board 下的 Kconfig 文件,将我们事先对 UART 的配置代码复制粘贴过去:

menu "Hardware Drivers Config"config SOC_STM32F412RETboolselect SOC_SERIES_STM32F4select RT_USING_COMPONENTS_INITselect RT_USING_USER_MAINdefault ymenu "On-chip Peripheral Drivers"config BSP_USING_GPIObool "Enable GPIO"select RT_USING_PINdefault ymenuconfig BSP_USING_UARTbool "Enable UART"default yselect RT_USING_SERIALif BSP_USING_UARTconfig BSP_USING_UART1bool "Enable UART1"default yif BSP_USING_UART1 && RT_USING_SERIAL_V2config BSP_UART1_RX_BUFSIZEint "UART1 RX BUFFER SIZE"default 256config BSP_UART1_TX_BUFSIZEint "UART1 TX BUFFER SIZE"default 0endifconfig BSP_USING_UART3bool "Enable UART3"default yif BSP_USING_UART3 && RT_USING_SERIAL_V2config BSP_UART3_RX_BUFSIZEint "UART3 RX BUFFER SIZE"default 256config BSP_UART3_TX_BUFSIZEint "UART3 TX BUFFER SIZE"default 0endifendif
endmenuendmenu

在这里插入图片描述

在 project 下使用 Env 工具输入 menuconfig 对 UART 进行配置:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

最后我们去硬件驱动配置查看,不难发现与我们前面的 Kconfig 配置文件修改项一样:
在这里插入图片描述

上面我们修改的只是配置环境的参数,我们需要使用该参数来重新创建工程,输入命令 scons --target=mdk5
在这里插入图片描述

3.点亮LED

查看开发板 LED 的引脚位置:
在这里插入图片描述
在这里插入图片描述

点击编译烧录发现开发板灯开始闪烁。

同时为了加上我们刚配置的 UART 功能,我们进行下面的代码修改:

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>#define DBG_LVL		DBG_LOG
/*
I	Informatiom
D	Debug
W	Warning
E	Error
*/
#define DBG_TAG	 	"main"
#include <rtdbg.h>/* defined the USER LED2 pin: PB7 */
#define LED2_PIN               GET_PIN(A, 1)int main(void)
{static rt_uint32_t cnt = 0;/* set LED0 pin mode to output */rt_pin_mode(LED2_PIN, PIN_MODE_OUTPUT);while (1){rt_pin_write(LED2_PIN, PIN_HIGH);rt_thread_mdelay(500);rt_pin_write(LED2_PIN, PIN_LOW);rt_thread_mdelay(500);LOG_I("%d RT-Thread ok!",cnt++);//我们这里打印调试信息所以用LOG_I,若是其他信息例如错误则用LOG_E。}
}

编译烧录后打开串口助手可以看到有相关打印信息。

二、为“学习迪文屏”准备的工具软件——“串口读写工具”项目

2.1 “串口读写工具”需求分析

迪文屏的学习需要“串口设备数据输入输出”,即串口读写工具的支持。

UART3负责与迪文屏通讯,UART1负责与上位机通讯。

所以,串口读写工具的主要功能是:信息“透传”

功能描述如下图:

在这里插入图片描述

2.2 设备接口层与业务逻辑层

分层开发思想其实是结构化程序设计思想的一种具体实现。目的是:
1、降低开发难度;
2、提高代码质量;
3、降低代码维护成本(利于测试);
4、支持代码复用;
5、利于部件替换。

分层原则:
1、职责单一(提高代码内聚性,同时降低代码耦合度);只做该做的事
2、谨慎设计接口;对于上下层之间的调用、被调用关系,需要通过“函数调用”

实现,因此,慎重设计函数及其接口,是降低代码耦合度的关键。

可以将项目(串口读写工具)分层为:
1、设备接口层;
2、业务逻辑层。

设备接口层:主要实现串口读写功能,不关心所读写的信息的意义;
业务逻辑层:通过调用设备接口层所提供的读写功能,实现通信,并通过通信实现迪文格式数据的业务逻辑。

2.3 提取并调整RT-Thread工程

注意事项:
1、不需要,也不能进行“控制台”输出调试信息;
2、暂时不考虑CAN设备。

ENV功能的进一步学习。

这里我们解压原来工程project到新建文件夹serila-dwin,按照上面的1.4章节进行修改配置即可:

在这里插入图片描述

大致是需要去修改工程模板template、board下的SConscript、Kconfig以及board下linker_scripts的link.sct,最后是cubeMX配置。配置完毕后图形配置与上面的有所不同,操作如下:

D:\RT-project\serial-dwin 目录下打开 Env 工具,输入 menuconfig 打开图像配置,先取消调试信息打印:

在这里插入图片描述
在这里插入图片描述

修改串行驱动设备。改成V2版本,与Kconfig一致,并开启DMA模式:
在这里插入图片描述
在这里插入图片描述

上面我们修改的只是配置环境的参数,最后我们需要使用该参数来重新创建工程,输入命令 scons --target=mdk5

在这里插入图片描述

2.4 Env管理组和RTT代码规范

我们抽取过来的project可以使用 Env 工具进行代码管理,操作如下:

打开 D:\RT-project\serial-dwin\applications 路径下的 SConscript 文件进行修改:
在这里插入图片描述
在这里插入图片描述

修改后在 D:\RT-project\serial-dwin 目录下运行 Env 工具,输入命令 scons --target=mdk5 ,查看 Keil5 工程目录:
在这里插入图片描述

不难发现,从一开始的 Application 变成我们自己定义的 App,有了这个变化,我们可以创建一个 interface 文件夹,用来存放我们的接口层函数,同时在该文件夹中存放一个 SConscript 文件,以后需要增加什么代码时,打开 Env 工具更新即可:
在这里插入图片描述

SConscript 文件做如下修改:

在这里插入图片描述

再次在 D:\RT-project\serial-dwin 目录下运行 Env 工具,输入命令 scons --target=mdk5 ,查看 Keil5 工程目录:
在这里插入图片描述

更新完目录出现了一个新的文件夹,于是我们的工程得到了更好的管理。

2.5 设备接口层实现

在2.4中,我们利用 Env 工具完成了对工程的分组,我们使用了一个接口组:interface,接下来我们要使用面向对象的编程思想,来完成串口的一系列函数操作。

面向对象:

  • 对象(数据)
  • 操作(函数)

接口层应该完成的任务:
1、初始化serial;
2、实现serial的Rx功能;
3、实现serial的Tx功能;
4、初始化dwin;
5、实现dwin的Rx功能;
6、实现dwin的Tx功能。

大致框图如下:
在这里插入图片描述

D:\RT-project\serial-dwin\applications\interface 目录下创建以下.c和.h文件:
在这里插入图片描述

D:\RT-project\serial-dwin 目录下打开 Env 工具输入输入命令 scons --target=mdk5 更新 keil5 工程:
在这里插入图片描述

下面函数用到一个 RT-Thread 提供的默认串口配置,该配置如下:

/* Default config for serial_configure structure */
#define RT_SERIAL_CONFIG_DEFAULT                    \
{                                                   \BAUD_RATE_115200,       /* 115200 bits/s */     \DATA_BITS_8,            /* 8 databits */        \STOP_BITS_1,            /* 1 stopbit */         \PARITY_NONE,            /* No parity  */        \BIT_ORDER_LSB,          /* LSB first sent */    \NRZ_NORMAL,             /* Normal mode */       \RT_SERIAL_RX_MINBUFSZ,  /* rxBuf size */        \RT_SERIAL_TX_MINBUFSZ,  /* txBuf size */        \0                                               \
}

应用程序通过 RT-Thread 提供的 I/O 设备管理接口来访问串口硬件,相关接口如下所示(我们主要用下图四个来完成对 UART 的配置即可):
在这里插入图片描述

interface_serial.c

#include <board.h>#include "interface_serial.h"
#include "interface_dwin.h"/* 以下四个宏是为了创建进程而定义 */
#define INTERFACE_SERIAL_NAME	"uart1"	/* 串口设备名称 */
#define INTERFACE_SERIAL_THREAD_STACK_SIZE	1024	/* 堆、栈大小 */
#define INTERFACE_SERIAL_THREAD_PRO		20	/* 优先级 */
#define INTERFACE_SERIAL_THREAD_SEC		20	/* 时间段大小 *//*串口完成量(信号量)
*/
static struct 
{rt_device_t device;					//串口设备句柄(最终会落实到串口)struct rt_completion cpt;			//完成量(二值信号量)
}interface_serial;/* 数据接收处理函数 */
static void serail_rx_dealer(void *arg)
{static rt_uint8_t rx_buffer[BSP_UART1_RX_BUFSIZE + 1];	//+1是考虑到若为字符串则结尾有个`\0`rt_uint16_t len;while (1){// 以阻塞方式等待完成量rt_completion_wait(&interface_serial.cpt, RT_WAITING_FOREVER);// 接收串口数据,而实际接收到的数据长度是由下面的函数返回值告知的。len = rt_device_read(interface_serial.device, 0, rx_buffer, BSP_UART1_RX_BUFSIZE);// 串口数据交由UART3发送给迪文屏。dwin_serial_send(rx_buffer, len);}
}/* 串口接收回调函数 */
static rt_err_t serial_rx_callback(rt_device_t deveice, rt_size_t size)
{rt_completion_done(&interface_serial.cpt);	// 释放完成量,唤醒某个处理(等待)完成量的函数return RT_EOK;
}void init_serial(void)
{struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;	// 初始化配置参数rt_err_t result;rt_thread_t thread;	//线程句柄/* 1.查找串口设备 */interface_serial.device = rt_device_find(INTERFACE_SERIAL_NAME);RT_ASSERT(interface_serial.device != RT_NULL);	// 确保串口设备对象指针有效 若为空指针则不会执行后续代码/* 2.控制串口设备。通过控制接口传入命令控制字,与控制参数 */result = rt_device_control(interface_serial.device, RT_DEVICE_CTRL_CONFIG, &config);RT_ASSERT(result == RT_EOK);	//验证操作结果是否成功 主要用于调试/* 3.打开串口设备。以非阻塞接收(DMA模式)和阻塞发送模式打开串口设备 */result = rt_device_open(interface_serial.device, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);RT_ASSERT(result == RT_EOK);/* 初始化完成量 */rt_completion_init(&interface_serial.cpt);/* 4.设置设备接收数据的回调函数 */result = rt_device_set_rx_indicate(interface_serial.device, serial_rx_callback);RT_ASSERT(result == RT_EOK);/* 创建线程 */thread = rt_thread_create("SERAIL_RX", serail_rx_dealer, RT_NULL, INTERFACE_SERIAL_THREAD_STACK_SIZE,INTERFACE_SERIAL_THREAD_PRO,INTERFACE_SERIAL_THREAD_SEC);RT_ASSERT(thread != RT_NULL);/* 启动线程 */rt_thread_startup(thread);
}void serial_send(rt_uint8_t *buff, rt_uint32_t size)
{rt_device_write(interface_serial.device, 0, buff, size);
}

interface_serial.h

#ifndef __INTERFACE_SERIAL_H__
#define __INTERFACE_SERIAL_H__#include <rtthread.h>
#include <rtdevice.h>#ifdef __cplusplus
extern "C" {
#endifvoid init_serial(void);
void serial_send(rt_uint8_t *buff, rt_uint32_t size);#ifdef __cplusplus
}
#endif#endif

interface_diwn.c

#include <board.h>#include "interface_dwin.h"
#include "interface_serial.h"/* 以下四个宏是为了创建进程而定义 */
#define INTERFACE_DWIN_SERIAL_NAME	"uart3"	/* 串口设备名称 */
#define INTERFACE_DWIN_SERIAL_THREAD_STACK_SIZE	1024
#define INTERFACE_DWIN_SERIAL_THREAD_PRO	20	/* 优先级 */
#define INTERFACE_DWIN_SERIAL_THREAD_SEC	20	/* 时间段大小 *//*串口完成量(信号量)
*/
static struct 
{rt_device_t device;					//串口设备句柄(最终会落实到串口)struct rt_completion cpt;		//完成量(二值信号量)
}interface__dwin_serial;/* 数据接收处理函数 */
static void dwin_serail_rx_dealer(void *arg)
{static rt_uint8_t rx_buffer[BSP_UART3_RX_BUFSIZE + 1];rt_uint16_t len;while (1){// 以阻塞方式等待完成量rt_completion_wait(&interface__dwin_serial.cpt, RT_WAITING_FOREVER);// 接收迪文屏串口数据,应该交由UART1上传到上位机。len = rt_device_read(interface__dwin_serial.device, 0, rx_buffer, BSP_UART3_RX_BUFSIZE);serial_send(rx_buffer, len);}
}/* 串口接收回调函数 */
static rt_err_t dwin_serial_rx_callback(rt_device_t deveice, rt_size_t size)
{rt_completion_done(&interface__dwin_serial.cpt);	// 释放完成量,唤醒某个处理(等待)完成量的函数return RT_EOK;
}void init_dwin_serial(void)
{struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;	// 初始化配置参数rt_err_t result;rt_thread_t thread;	//线程句柄/* 1.查找串口设备 */interface__dwin_serial.device = rt_device_find(INTERFACE_DWIN_SERIAL_NAME);RT_ASSERT(interface__dwin_serial.device != RT_NULL);	// 确保串口设备对象指针有效 若为空指针则不会执行后续代码/* 2.控制串口设备。通过控制接口传入命令控制字,与控制参数 */result = rt_device_control(interface__dwin_serial.device, RT_DEVICE_CTRL_CONFIG, &config);RT_ASSERT(result == RT_EOK);	//验证操作结果是否成功 主要用于调试/* 3.打开串口设备。以非阻塞接收(DMA模式)和阻塞发送模式打开串口设备 */result = rt_device_open(interface__dwin_serial.device, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);RT_ASSERT(result == RT_EOK);/* 初始化完成量 */rt_completion_init(&interface__dwin_serial.cpt);/* 4.设置设备接收数据的回调函数 */result = rt_device_set_rx_indicate(interface__dwin_serial.device, dwin_serial_rx_callback);RT_ASSERT(result == RT_EOK);/* 创建线程 */thread = rt_thread_create("DWIN_RX", dwin_serail_rx_dealer, RT_NULL, INTERFACE_DWIN_SERIAL_THREAD_STACK_SIZE,INTERFACE_DWIN_SERIAL_THREAD_PRO,INTERFACE_DWIN_SERIAL_THREAD_SEC);RT_ASSERT(thread != RT_NULL);/* 启动线程 */rt_thread_startup(thread);
}void dwin_serial_send(rt_uint8_t *buff, rt_uint32_t size)
{// UART3发送的串口数据会被迪文屏接收rt_device_write(interface__dwin_serial.device, 0, buff, size);
}

interface_dwin.h

#ifndef __INTERFACE_DWIN_H__
#define __INTERFACE_DWIN_H__#include <rtthread.h>
#include <rtdevice.h>#ifdef __cplusplus
extern "C" {
#endifvoid init_dwin_serial(void);
void dwin_serial_send(rt_uint8_t *buff, rt_uint32_t size);#ifdef __cplusplus
}
#endif#endif 

interface_serial.c 与 interface_diwn.c 类似,只是串口不同,在 interface_serial.c 中主要是完成了对 UART1 的配置,包含了接收到来自上位机的数据后能够发给 UART3 ;在 interface_dwin.c 中主要是完成了对 UART3 的配置,包含了接收到来自 UART1 的数据后能够发给迪文屏。

2.6 业务逻辑层实现

这个工具性质的项目的业务逻辑非常简单,只需要实现对两个串口的初始化

但是,从层次编程思想角度出发,还是应该增加一个“业务逻辑层”。

Business logic layer 简称 bll。

D:\RT-project\serial-dwin\applications 目录下新建一个 bll 文件夹,其中放入如下文件:
在这里插入图片描述

在这里我们需要对 SConscript 文件进行修改,主要修改是名字,方便在 keil5 工程目录区别:
在这里插入图片描述

D:\RT-project\serial-dwin 目录下打开 Env 工具输入输入命令 scons --target=mdk5 更新 keil5 工程:
在这里插入图片描述

diwn.bll.c

#include <board.h>#include "interface_dwin.h"
#include "interface_serial.h"
#include "dwin_bll.h"void init_dwin_bll(void)
{init_serial();init_dwin_serial();
}

diwn.bll.h

#ifndef __DWIN_BLL_H__
#define __DWIN_BLL_H__#include <rtthread.h>
#include <rtdevice.h>#ifdef __cplusplus
extern "C" {
#endifvoid init_dwin_bll(void);#ifdef __cplusplus
}
#endif#endif

接着在 main.c 调用 init_dwin_bll 函数即可完成对两个接口层的初始化。

三、迪文屏开发

有关迪文屏开发相关内容可以看我前面的文章:

  • 迪文屏开发指南(上)
  • 迪文屏开发指南(中)
  • 迪文屏开发指南(下)

四、CAN 调试屏项目软件实现

4.1 CAN 数据分析

这里讲述的CAN数据并非“CAN数据帧”。 CAN数据帧包括帧起始、仲裁段、控制段、数据段、循环校验段、确认段和帧结束。其中数据段最长8字节。这里只关心数据段,以及CAN数据段数据与APP数据之间的关系。也就是需要知道APP数据是如何存储到CAN数据段中,以及如何解析CAN数据,得到APP数据。

为方便描述,下面用“信号”代替APP数据。

将信号存储到CAN数据段,以及从CAN数据段解析出信号,需要知道以下信息:
1、CAN数据帧ID;
2、信号字节号(字节下标);
3、信号位号(位下标);
4、信号位长度(位数)。

通过上述信息,就可以从CAN数据段中解析出信号,即APP数据。但是,APP数据本身还有其它信息,需要进一步说明。

4.2 APP 数据分析

本项目所涉及到的数据包括:
在这里插入图片描述
上表所描述的APP数据,重点在“取值范围”、“分辨率”和“数据方向”这三个属性。

4.3 提取并调整RT-Thread工程

所涉及的资源:

  1. UART1:用于控制台(console)调试信息输出
  2. UART3:用于与迪文屏通讯
  3. CAN1:用于收发CAN数据帧

抽取和调整步骤与前面1.3、1.4章节内容类似,这里我就列举大致操作,有不同的我会详细说明:

  • D:\RT-project\RT-Thread_SDK\rt-thread\bsp\stm32\stm32f412-st-nucleo ->(env)-> scons --dist --target=mdk5
  • 剪切dist目录并更名为can_dwin_debug:D:\zho\can_dwin_debug\project
  • 调整模板工程:
    • 魔术棒 -> device -> stm32f412ret
    • C/C++ -> GNU
    • debug -> J-Link
    • debug -> J-Link(setting) -> port(SW)
    • debug -> J-Link(setting) -> Flash download(Reset and run)
  • 调整mcu型号
    • 修改SConscript的mcu型号为:STM32F412Rx(同时需要修改启动文件名称)
    • 修改linker_scripts\link.sct中分散加载参数;
    • 重新构建STM32F412Rx的Cube MX工程:
      • 选择STM32F412Rx;
      • 项目参数:
        • Projct Location:D:\RT-project\can_dwin_debug\board
        • Project Name:CubeMX_Config
        • App struct:Basic
        • Tool chain:MDK-ARM
        • Code Generater:xxy xxyy(x表示没选中 y表示选中)
        • PinOut:SYS -> SW
        • PinOut:RCC -> HSE
        • Clock:8M外部晶振、100M
        • Pinout:connectivity:使能USART1、USART3和CAN1(CAN1的参数:Pres:5、B1:15、B2:4)(CAN1设置如下,CAN波特率为500k)
          在这里插入图片描述
      • 生成代码
      • 复制CubeMX的src的main.c的SystemClock_Config()函数到board.c中,覆盖相应函数
    • 通过Env调整项目中的串口和CAN:
      • D:\RT-project\can_dwin_debug\board 目录下的 Kconfig 进行调整,修改如下:
        在这里插入图片描述
        在这里插入图片描述
      • D:\RT-project\can_dwin_debug 目录下执行Env
        在这里插入图片描述在这里插入图片描述
        在这里插入图片描述
        在这里插入图片描述
        在这里插入图片描述
        保存设置
        在这里插入图片描述
        退出menuconfig命令,并执行:scons --target=mdk5

最后我们打开工程,找到如下图位置,发现其中配置已经完善:
在这里插入图片描述

4.4 Env管理组和RTT代码规范

我们抽取过来的project可以使用 Env 工具进行代码管理,操作如下:

打开 D:\RT-project\can_dwin_debug\applications 路径下的 SConscript 文件进行修改:
在这里插入图片描述
在这里插入图片描述

在该目录下创建一个 interface 文件夹,用来存放我们的接口函数,同时需要把 SConcript 文件复制粘贴过去并进行修改,操作如下:
在这里插入图片描述
在这里插入图片描述

修改后在 D:\RT-project\can_dwin_debug 目录下运行 Env 工具,输入命令 scons --target=mdk5 ,查看 Keil5 工程目录:
在这里插入图片描述

4.5 设备接口层实现

功能:(功能在实现时,需注意“工具化”)

  • CAN:
    • CAN初始化;
    • CAN数据侦听线程;
    • CAN数据帧发送。
  • 串口3:
    • 串口3初始化;
    • 串口3数据侦听线程;
    • 串口3数据发送。

应用程序通过 RT-Thread 提供的 CAN 设备管理接口来访问串口硬件,相关接口如下所示(我们主要用下图四个来完成代码编写即可):
在这里插入图片描述

在前面我们完成了CAN在CubeMX的配置,但是我们忽略了一个严重的事情,就是我们在配置时没有去仔细查看CAN在硬件中的引脚位置,于是我们需要去查看原理图并重新去CubeMX中进行修改:
在这里插入图片描述

在这里插入图片描述

interface_can.c

#include <rtthread.h>
#include <rtdevice.h>#include "interface_can.h"#define DBG_LEVEL	DBG_LOG
#define DBG_TAG		"interface_can"
#include <rtdbg.h>static struct
{rt_device_t device;				// 设备句柄struct rt_completion cpt;	// 完成量
}interface_can;/* 接收处理回调函数 */
static rt_err_t can_rx_callback(rt_device_t dev, rt_size_t size)
{/* 释放完成量 唤醒某个等待完成量的函数 */rt_completion_done(&interface_can.cpt);return RT_EOK;
}/* 接收线程 */
static void can_rx_thread(void *parameter)
{static struct rt_can_msg can_receive_msg;while (1){can_receive_msg.hdr_index = -1;rt_completion_wait(&interface_can.cpt, RT_WAITING_FOREVER);rt_device_read(interface_can.device, 0, &can_receive_msg, sizeof(can_receive_msg));
#if 0{int i;// 回环测试:在CAN软件输入id号并发送,开发板接收到数据之后重新发送给CAN软件,同时将数据通过UART1发送给串口软件can_send(can_receive_msg.id, can_receive_msg.data, can_receive_msg.len);rt_kprintf("Received CAN data frame:(%04X) ->", can_receive_msg.id);	//id号有8位 这里显示后4位 假如0000 0101 则显示0101for (i = 0; i < can_receive_msg.len; i++){rt_kprintf(" %02X", can_receive_msg.data[i]);	//将数据按两位发送}}
#endif}
}void init_can(void)
{rt_err_t res;rt_thread_t thread;/* 1.查找CAN设备 名字的宏定义在修改KConfig时生效 在rtconfig.h文件最底 */interface_can.device = rt_device_find(INTERFACE_CFG_CAN_NAME);if (interface_can.device == RT_NULL){LOG_E("%s not found", INTERFACE_CFG_CAN_NAME);RT_ASSERT(0);	// 这里为调试断言 如果进了if分支 执行到此处会直接停止程序继续运行}/* 2.控制CAN设备 我们的控制命令是修改波特率 在CubeMX配置波特率为500K */res = rt_device_control(interface_can.device, RT_CAN_CMD_SET_BAUD, (void *)CAN500kBaud);if (res != RT_EOK){LOG_E("CAN conctrl failed");RT_ASSERT(0);}/* 3.打开CAN设备 *//* 以中断接收及发送模式打开 CAN 设备 */res = rt_device_open(interface_can.device, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);if (res != RT_EOK){LOG_E("CAN open failed");RT_ASSERT(0);}/* 初始化完成量 */rt_completion_init(&interface_can.cpt);/* 4.设置设备接收数据的回调函数 */rt_device_set_rx_indicate(interface_can.device, can_rx_callback);/* 创建线程 其中参数宏都是在修改KConfig文件时生成的 在KConfig文件最底可找到 */thread = rt_thread_create("CAN_RN", can_rx_thread, RT_NULL,INTERFACE_CFG_CAN_THREAD_SIZE,INTERFACE_CFG_CAN_THREAD_PRO,INTERFACE_CFG_CAN_THREAD_CPU_SECTION);if (thread == RT_NULL){LOG_E("thread create failed");RT_ASSERT(0);}/* 启动线程 */rt_thread_startup(thread);
}/* 发送数据 */
//struct rt_can_msg
//{
//    rt_uint32_t id  : 29;   /* CAN ID, 标志格式 11 位,扩展格式 29 位 */
//    rt_uint32_t ide : 1;    /* 扩展帧标识位 */
//    rt_uint32_t rtr : 1;    /* 远程帧标识位 */
//    rt_uint32_t rsv : 1;    /* 保留位 */
//    rt_uint32_t len : 8;    /* 数据段长度 */
//    rt_uint32_t priv : 8;   /* 报文发送优先级 */
//    rt_uint32_t hdr : 8;    /* 硬件过滤表号 */
//    rt_uint32_t reserved : 8;
//    rt_uint8_t data[8];     /* 数据段 */
//};
rt_err_t can_send(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{static struct rt_can_msg can_msg;	// CAN消息 结构体原型如上int i;rt_uint16_t len;	// 发送数据的真正长度rt_uint16_t length = sizeof(can_msg);	// CAN数据的总长度can_msg.id = id;can_msg.ide = RT_CAN_STDID;     /* 标准格式 */can_msg.rtr = RT_CAN_DTR;       /* 数据帧 */	for (i = 0; i < size; i++){can_msg.data[i] = buff[i]; }len = rt_device_write(interface_can.device, 0, &can_msg, length);// 发送的真正长度不等于CAN数据总长度 说明发送失败if (len != length){LOG_I("can date %d send failed (%d / %d)", id, len, length);return RT_ERROR;}return RT_EOK;
}

在这里我们通过CAN Pro软件设置好id,写入要发送的数据,点击发送后开发板能接收到数据,接收到数据之后将数据发送给CAN Pro软件,同时以调试信息方式输出在串口软件。

interface_dwin.c

#include <board.h>
#include <string.h>#include "interface_dwin.h"#define INTERFACE_DWIN_SERIAL_NAME							"uart3"	/* 串口设备名称 */
#define INTERFACE_DWIN_SERIAL_THREAD_STACK_SIZE	1024
#define INTERFACE_DWIN_SERIAL_THREAD_PRO					20	/* 优先级 */
#define INTERFACE_DWIN_SERIAL_THREAD_SEC					20	/* 时间段大小 */#define DWIN_DATA_FRAME_MAX_LENGTH 							255/*串口完成量(信号量)
*/
static struct 
{rt_device_t device;					//串口设备句柄(最终会落实到串口)struct rt_completion cpt;		//完成量(二值信号量)
}interface_dwin_serial;static void collect_dwin_data_frame(rt_uint8_t *data_frame, rt_uint32_t size)
{// 静态缓冲区用于累积存储接收到的数据片段static rt_uint8_t buffer[DWIN_DATA_FRAME_MAX_LENGTH + 1];// 当前缓冲区中累积的数据长度(即当前接收到多少数据)static rt_uint32_t len = 0;// 把接收到的数据全部传入buffer中memcpy(buffer + len, data_frame, size);// 更新len的值 使其等于传入数据大小len += size;if (len < 3){return;}if (((rt_uint32_t)buffer[2]) + 3 ==len)	//buffer[2]为后面的数据个数 3是前面的格式数据以及buffer[2]本身{// 得到完整数据帧,可以进行下一步处理		
#if 1{// 其实,现在的处理方案是存在问题的。需要先直接输出所接收到的数据,进行观察:int i;rt_kprintf("\nReceive auto upload data from DWIN: %d\n", len);for (i = 0; i < len; i++){rt_kprintf("%02X ", buffer[i]);}rt_kprintf("\n");// 通过上面的验证发现,上传数据有可能被分割成不同的片段,使得程序无法完整处理所有上传数据。}
#endif				}
}/* 数据接收处理函数 */
static void dwin_serail_rx_dealer(void *arg)
{static rt_uint8_t rx_buffer[BSP_UART3_RX_BUFSIZE + 1];rt_uint16_t len;while (1){// 以阻塞方式等待完成量rt_completion_wait(&interface_dwin_serial.cpt, RT_WAITING_FOREVER);// 接收迪文屏串口数据,应该交由UART1上传到上位机。len = rt_device_read(interface_dwin_serial.device, 0, rx_buffer, BSP_UART3_RX_BUFSIZE);// 接收迪文屏串口数据,这是“自动上传”类型的数据,需要进一步处理。collect_dwin_data_frame(rx_buffer, len);}
}/* 串口接收回调函数 */
static rt_err_t dwin_serial_rx_callback(rt_device_t deveice, rt_size_t size)
{rt_completion_done(&interface_dwin_serial.cpt);	// 释放完成量,唤醒某个处理(等待)完成量的函数return RT_EOK;
}void init_dwin_serial(void)
{struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;	// 初始化配置参数rt_err_t result;rt_thread_t thread;	//线程句柄/* step1:查找串口设备 */interface_dwin_serial.device = rt_device_find(INTERFACE_DWIN_SERIAL_NAME);RT_ASSERT(interface_dwin_serial.device != RT_NULL);	// 确保串口设备对象指针有效 若为空指针则不会执行后续代码/* 控制串口设备。通过控制接口传入命令控制字,与控制参数 */result = rt_device_control(interface_dwin_serial.device, RT_DEVICE_CTRL_CONFIG, &config);RT_ASSERT(result == RT_EOK);	//验证操作结果是否成功 主要用于调试/* 打开串口设备。以非阻塞接收(DMA模式)和阻塞发送模式打开串口设备 */result = rt_device_open(interface_dwin_serial.device, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);RT_ASSERT(result == RT_EOK);/* 初始化完成量 */rt_completion_init(&interface_dwin_serial.cpt);result = rt_device_set_rx_indicate(interface_dwin_serial.device, dwin_serial_rx_callback);RT_ASSERT(result == RT_EOK);/* 创建线程 */thread = rt_thread_create("DWIN_RX", dwin_serail_rx_dealer, RT_NULL, INTERFACE_DWIN_SERIAL_THREAD_STACK_SIZE,INTERFACE_DWIN_SERIAL_THREAD_PRO,INTERFACE_DWIN_SERIAL_THREAD_SEC);RT_ASSERT(thread != RT_NULL);/* 启动线程 */rt_thread_startup(thread);
}void dwin_serial_send(rt_uint8_t *buff, rt_uint32_t size)
{// UART3发送的串口数据会被迪文屏接收rt_device_write(interface_dwin_serial.device, 0, buff, size);
}

串口3获取的来自迪文屏的自动上传数据,不能被全部完整接收,需要进一步处理。

对于多批次发送的数据,有可能组合成一帧完整的自动上传数据,每一次得到的未必是完整数据,可以通过观察数据格式,找到拼凑数据的逻辑和方法:

5A A5 数据个数 ...

发现第三个数据(下标为2)说明了后面所跟的数据个数(字节个数),可以这样设计处理逻辑:

  • 如果所接收信息长度 < 3
    • 结束
  • 根据“数据长度”检测当前数据帧是否完整
    • 若完整,则回到初始状态,并调用处理数据的函数,结束。
    • 否则,继续接收下一帧数据(结束)

4.6 分发器实现

分发器分发什么?分类对象(标准)是“帧编号”或者“变量地址”;
分给谁?分给函数!这些函数是尚未定义的业务逻辑函数!

在分发器中,只需要定义函数接口即可。

这完全可以按照“对象”思想处理。
对象是:包括编号和函数指针在内的数据包;
操作是:1、初始化;2、分发过程。

D:\RT-project\can_dwin_debug\applications 下新建一个dispatcher 文件,其中再次放入 SConcript 文件,并作出如下修改:
在这里插入图片描述
在这里插入图片描述

创建dispatcher_can_dwin.c以及dispatcher_can_dwin.h,用Env工具输入scons --target=mdk5来更新keil工程:
在这里插入图片描述
dispatcher_can_dwin.c

#include <string.h>
#include <board.h>#include "interface_dwin.h"
#include "dispatcher_can_dwin.h"#define DBG_LEVEL	DBG_LOG
#define DBG_TAG		"dispatcher_can_dwin"
#include <rtdbg.h>/* 字节交换宏将16位数值高低字节交换(原因是迪文变量地址上传时500B显示成0B50 需要更换字节序) */
#define SWIP_16(val)		((((val) & 0xFF) << 8) | (((val) >> 8) & 0xFF))static struct
{can_dispatcher_t *list; 	// CAN调度器列表指针rt_size_t count;					// 调度器数量
}can_dispatcher_tab;static struct
{dwin_dispatcher_t *list;	// DWIN调度器列表指针rt_size_t count;					// 调度器数量
}dwin_auto_load_dispatcher_tab;/* 初始化CAN消息调度器 */ 
void init_can_dispatcher(can_dispatcher_t *list, rt_size_t count)
{can_dispatcher_tab.list = list;can_dispatcher_tab.count = count;
}/* 初始化DWIN自动上传数据调度器 */
void init_dwin_dispatcher(dwin_dispatcher_t *list, rt_size_t count)
{dwin_auto_load_dispatcher_tab.list = list;dwin_auto_load_dispatcher_tab.count = count;
}/* 根据CAN ID查找并调用对应的处理函数 */
void can_data_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{int index;// 遍历调度表查找匹配的CAN IDfor (index = 0; index < can_dispatcher_tab.count; index++){if (can_dispatcher_tab.list[index].id == id){// 找到匹配ID,调用对应的钩子函数can_dispatcher_tab.list[index].hook(id, buff, size);return;}}// 未找到匹配的处理函数LOG_I("CAN data (%04X) parser not found", id);
}// 根据变量地址查找并调用对应的处理函数
void dwin_auto_load_data_parser(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size)
{int index;// 转换地址字节序(与DWIN设备字节序保持一致)address = SWIP_16(address);for (index = 0; index < dwin_auto_load_dispatcher_tab.count; index++){if (dwin_auto_load_dispatcher_tab.list[index].address == address){// 找到匹配地址,调用对应的钩子函数dwin_auto_load_dispatcher_tab.list[index].hook(address, buff, size);return;}}// 未找到匹配的处理函数LOG_I("DWIN auto load data (%04X) parser not found",address);
}/* 测试函数 用于接收到数据之后对数据的拼接上传打印 */
void default_can_data_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{char str[512] = {0};char tmp[8];int i;rt_sprintf(str, "CAN data (%04X) :", id);for (i = 0; i < size; i++){rt_sprintf(tmp, " %02X", buff[i]);strcat(str, tmp);	//数据拼接}// 打印调试信息 若没有此行 上面的打印不会输出LOG_I(str);
}/* 测试函数 用于接收到数据之后对数据的拼接上传打印 */
void default_dwin_auto_load_data_parser(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size)
{char str[512] = {0};char tmp[8];int i;rt_sprintf(str, "Dwin auto load data (%04X) :", address);for (i = 0; i < size; i++){rt_sprintf(tmp, " %02X", buff[i]);strcat(str, tmp);	//数据拼接}// 打印调试信息 若没有此行 上面的打印不会输出LOG_I(str);
}

dispatcher_can_dwin.h

#ifndef __DISPATCHER_CAN_DWIN_H__
#define __DISPATCHER_CAN_DWIN_H__#include <rtthread.h>
#include <rtdevice.h>#ifdef __cplusplus
extern "C" {
#endiftypedef void (*can_data_parser_hook)(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size);
typedef void (*dwin_data_parser_hook)(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size);typedef struct can_dispatcher
{rt_uint32_t id;can_data_parser_hook hook;
}can_dispatcher_t;typedef struct dwin_dispatcher
{rt_uint32_t address;dwin_data_parser_hook hook;
}dwin_dispatcher_t;void init_can_dispatcher(can_dispatcher_t *list, rt_size_t count);
void init_dwin_dispatcher(dwin_dispatcher_t *list, rt_size_t count);void can_data_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size);
void dwin_auto_load_data_parser(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size);void default_can_data_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size);
void default_dwin_auto_load_data_parser(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size);#ifdef __cplusplus
}
#endif#endif /* __DISPATCHER_CAN_DWIN_H__ */

main.c
在这里插入图片描述

此时就可以实现分发器的效果,能够准确识别出id及后面的数据或者触控控件的地址及后面的数据,但这里的迪文屏上传数据有点问题,即上传时存在找不到地址的情况和只上传一个字节的数据(数据为两字节),我们后续改进代码解决此问题。

问题在于实现interface_dwin.c时,collect_dwin_data_frame函数中上传数据时应该是buffer,而不是data_frame(因为我们将data_frame中的数据全部复制给buffer),所以应该修改参数;第二个时迪文数据上传时数据缺失,而数据地址后面就是数据个数,size_t表示一个字节一个字节的数据,但数据为两个字节,所以应该为size*2
在这里插入图片描述
在这里插入图片描述

4.7 业务逻辑层实现

D:\RT-project\can_dwin_debug\applications 目录下新建一个 bll 文件夹,将 SConcript 文件复制粘贴进去并修改参数,同时新建 bll_can.c、bll_can.h、bll_dwin.c、bll_dwin.h,打开 Env 工具输入 scons --target=mdk5 刷新keil工程:
在这里插入图片描述
在这里插入图片描述
bll_can.c

#include <board.h>#include "interface_can.h"
#include "dispatcher_can_dwin.h"
#include "bll_can.h"#define DBG_LEVEL	DBG_LOG
#define DBG_TAG		"bll_can"
#include <rtdbg.h>/* 为每个不同id编写一个数据上传函数 */
static void can_data_101_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{default_can_data_parser(id, buff, size);
}static void can_data_201_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{default_can_data_parser(id, buff, size);
}static void can_data_301_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{default_can_data_parser(id, buff, size);
}void init_bll_can(void)
{static can_dispatcher_t can_dispatcher_pool[] = {{ 0x101, can_data_101_parser },{ 0x201, can_data_201_parser },{ 0x301, can_data_301_parser },};	init_can();init_can_dispatcher(can_dispatcher_pool, sizeof(can_dispatcher_pool) / sizeof(can_dispatcher_t));
}

bll_dwin.c

#include <board.h>#include "interface_dwin.h"
#include "dispatcher_can_dwin.h"
#include "bll_dwin.h"#define DBG_LEVEL	DBG_LOG
#define DBG_TAG		"bll_can"
#include <rtdbg.h>void init_bll_dwin(void)
{static dwin_dispatcher_t dwin_dispatcher_pool[] = {{ 0x500A, default_dwin_auto_load_data_parser },{ 0x500B, default_dwin_auto_load_data_parser },{ 0x500C, default_dwin_auto_load_data_parser },{ 0x500D, default_dwin_auto_load_data_parser },};init_dwin_serial();init_dwin_dispatcher(dwin_dispatcher_pool, sizeof(dwin_dispatcher_pool) / sizeof(dwin_dispatcher_t));
}

4.8 最终代码

interface_can.c

#include <rtthread.h>
#include <rtdevice.h>#include "interface_can.h"       // CAN接口头文件
#include "dispatcher_can_dwin.h" // CAN数据分发器头文件// 调试配置:日志级别为LOG,标签为"interface_can"
#define DBG_LEVEL    DBG_LOG
#define DBG_TAG      "interface_can"
#include <rtdbg.h>// CAN接口设备信息结构体
static struct
{rt_device_t device;               // CAN设备句柄(用于操作CAN设备)struct rt_completion cpt;         // 完成量(用于线程同步,实现异步接收)
}interface_can;/* CAN接收回调函数(设备收到数据时触发) */
static rt_err_t can_rx_callback(rt_device_t dev, rt_size_t size)
{/* 释放完成量,唤醒等待该完成量的线程(can_rx_thread) */rt_completion_done(&interface_can.cpt);return RT_EOK;
}/* CAN接收线程(负责处理接收到的CAN数据) */
static void can_rx_thread(void *parameter)
{static struct rt_can_msg can_receive_msg;  // 存储接收到的CAN消息while (1)  // 循环处理接收{can_receive_msg.hdr_index = -1;  // 初始化消息头部索引// 等待完成量(阻塞,直到接收到数据被唤醒)rt_completion_wait(&interface_can.cpt, RT_WAITING_FOREVER);// 从CAN设备读取数据到消息结构体rt_device_read(interface_can.device, 0, &can_receive_msg, sizeof(can_receive_msg));// 将解析后的CAN数据交给分发器处理(分发到对应的数据解析函数)can_data_parser(can_receive_msg.id, can_receive_msg.data, can_receive_msg.len);#if 0  // 调试代码(默认关闭){int i;// 回环测试:将接收到的数据重新发送,并通过UART1打印can_send(can_receive_msg.id, can_receive_msg.data, can_receive_msg.len);rt_kprintf("Received CAN data frame:(%04X) ->", can_receive_msg.id);  // 打印CAN ID(4位十六进制)for (i = 0; i < can_receive_msg.len; i++){rt_kprintf(" %02X", can_receive_msg.data[i]);  // 打印数据(2位十六进制)}}
#endif}
}/* 初始化CAN接口 */
void init_can(void)
{rt_err_t res;               // 函数返回结果rt_thread_t thread;         // 线程句柄/* 1.查找CAN设备(设备名通过KConfig配置,定义在rtconfig.h中) */interface_can.device = rt_device_find(INTERFACE_CFG_CAN_NAME);if (interface_can.device == RT_NULL){LOG_E("%s not found", INTERFACE_CFG_CAN_NAME);  // 打印设备未找到错误RT_ASSERT(0);  // 断言失败,程序停止(调试用,确保设备正确初始化)}/* 2.配置CAN设备波特率(通过控制命令设置) */// 波特率配置为500Kbps(CAN500kBaud为宏定义,对应波特率参数)res = rt_device_control(interface_can.device, RT_CAN_CMD_SET_BAUD, (void *)CAN500kBaud);if (res != RT_EOK){LOG_E("CAN control failed");  // 打印波特率设置失败错误RT_ASSERT(0);}/* 3.打开CAN设备(配置为中断模式收发数据) */// RT_DEVICE_FLAG_INT_TX:发送中断模式;RT_DEVICE_FLAG_INT_RX:接收中断模式res = rt_device_open(interface_can.device, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);if (res != RT_EOK){LOG_E("CAN open failed");  // 打印设备打开失败错误RT_ASSERT(0);}/* 初始化完成量(用于接收线程同步) */rt_completion_init(&interface_can.cpt);/* 4.设置CAN接收回调函数(设备收到数据后会调用该函数) */rt_device_set_rx_indicate(interface_can.device, can_rx_callback);/* 创建CAN接收线程(参数通过KConfig配置) */thread = rt_thread_create("CAN_RN",                // 线程名can_rx_thread,           // 线程入口函数RT_NULL,                 // 线程参数INTERFACE_CFG_CAN_THREAD_SIZE,  // 线程栈大小(宏定义)INTERFACE_CFG_CAN_THREAD_PRO,   // 线程优先级(宏定义)INTERFACE_CFG_CAN_THREAD_CPU_SECTION  // 线程CPU核绑定(宏定义,单核可忽略));if (thread == RT_NULL){LOG_E("thread create failed");  // 打印线程创建失败错误RT_ASSERT(0);}/* 启动接收线程 */rt_thread_startup(thread);
}/* CAN数据发送函数 */
// CAN消息结构体定义(参考):
// struct rt_can_msg
// {
//     rt_uint32_t id  : 29;   /* CAN ID,标准帧11位,扩展帧29位 */
//     rt_uint32_t ide : 1;    /* 扩展帧标识位(0=标准帧,1=扩展帧) */
//     rt_uint32_t rtr : 1;    /* 远程帧标识位(0=数据帧,1=远程帧) */
//     rt_uint32_t rsv : 1;    /* 保留位 */
//     rt_uint32_t len : 8;    /* 数据长度(0-8字节) */
//     rt_uint32_t priv : 8;   /* 发送优先级 */
//     rt_uint32_t hdr : 8;    /* 硬件过滤表号 */
//     rt_uint32_t reserved : 8;
//     rt_uint8_t data[8];     /* 数据段(最多8字节) */
// };
rt_err_t can_send(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{static struct rt_can_msg can_msg;  // 待发送的CAN消息int i;rt_uint16_t len;                   // 实际发送长度rt_uint16_t length = sizeof(can_msg);  // CAN消息结构体总长度// 填充CAN消息内容can_msg.id = id;                       // 设置CAN IDcan_msg.ide = RT_CAN_STDID;            // 使用标准帧(RT_CAN_STDID=0)can_msg.rtr = RT_CAN_DTR;              // 发送数据帧(RT_CAN_DTR=0,远程帧为RT_CAN_RTR=1)can_msg.len = size;                    // 数据长度(不超过8字节)// 复制数据到消息数据段for (i = 0; i < size; i++){can_msg.data[i] = buff[i];}// 向CAN设备写入数据(发送消息)len = rt_device_write(interface_can.device, 0, &can_msg, length);// 检查发送结果(实际发送长度不等于消息总长度则发送失败)if (len != length){LOG_I("can data %d send failed (%d / %d)", id, len, length);  // 打印发送失败日志return RT_ERROR;}return RT_EOK;  // 发送成功
}

这段代码是基于 RT-Thread 实时操作系统的 CAN 总线接口驱动实现,主要功能如下:

  • CAN 设备管理:
    • 通过interface_can结构体封装 CAN 设备句柄和同步用的完成量,实现对 CAN 硬件的抽象管理。
    • 提供init_can初始化函数,完成设备查找、波特率配置(500Kbps)、设备打开等基础操作。
  • 数据接收机制:
    • 采用 “中断回调 + 完成量 + 线程” 的异步接收模式:CAN 硬件收到数据后触发can_rx_callback回调,通过完成量唤醒can_rx_thread接收线程。
    • 接收线程负责从设备读取完整的 CAN 消息,并调用can_data_parser将数据分发给对应的处理逻辑。
    • 内置调试代码(默认关闭),支持数据回环测试和串口打印,方便开发阶段验证硬件功能。
  • 数据发送功能:
    • 实现can_send函数,封装 CAN 消息结构体(标准数据帧、11 位 ID),支持向指定 CAN ID 发送数据(最多 8 字节,符合 CAN 协议规范)。
    • 发送过程包含结果检查,确保数据可靠发送,失败时返回错误状态并打印日志。
  • 线程同步:
    • 使用rt_completion完成量实现中断回调与接收线程的同步,避免线程空轮询浪费 CPU 资源,提高系统效率。

interface_dwin.c

#include <board.h>
#include <string.h>#include "interface_dwin.h"
#include "dispatcher_can_dwin.h"#define DBG_LEVEL	DBG_LOG
#define DBG_TAG		"interface_dwin"
#include <rtdbg.h>// 串口配置参数
#define INTERFACE_DWIN_SERIAL_NAME                     "uart3"       /* 串口设备名称 */
#define INTERFACE_DWIN_SERIAL_THREAD_STACK_SIZE        1024          /* 串口处理线程栈大小 */
#define INTERFACE_DWIN_SERIAL_THREAD_PRO               20            /* 线程优先级 */
#define INTERFACE_DWIN_SERIAL_THREAD_SEC               20            /* 线程时间片大小 *//*** @brief Dwin串口通信结构体* @details 存储串口设备句柄和完成量,用于串口数据收发同步*/
static struct 
{rt_device_t device;                // 串口设备句柄(对应实际的串口硬件)struct rt_completion cpt;          // 完成量(用于线程同步的二值信号量)
}interface_dwin_serial;/*** @brief 收集并解析Dwin屏数据帧* @param data_frame 接收到的串口数据缓冲区* @param size 数据长度* @details 累加接收到的串口数据,当数据长度符合完整帧格式时,*          提取地址和数据并调用分发器处理*/
static void collect_dwin_data_frame(rt_uint8_t *data_frame, rt_uint32_t size)
{static rt_uint8_t buffer[DWIN_DATA_FRAME_MAX_LENGTH + 1];  // 数据帧缓冲区(静态变量,跨调用保存数据)static rt_uint32_t len = 0;                                // 当前缓冲区中的数据长度rt_uint16_t address;                                       // 数据帧中的地址信息rt_uint8_t data_count;                                     // 数据帧中的数据个数// 将新接收的数据追加到缓冲区memcpy(buffer + len, data_frame, size);len += size;// 数据长度不足3字节时,不处理(无法判断帧完整性)if (len < 3){return;}// 检查数据帧是否完整(根据帧头中的长度字段判断)// 帧格式:[帧头][长度][数据...],总长度 = 长度字段值 + 3(帧头和长度字段的字节数)if (((rt_uint32_t) buffer[2]) + 3 == len){// 提取数据地址(从地址索引位置取2字节)address = *((rt_uint16_t *) (buffer + DWIN_DATA_FRAME_ADDRESS_INDEX));// 提取数据个数(从数据计数索引位置取1字节)data_count = *((rt_uint8_t *) (buffer + DWIN_DATA_COUNT_INDEX));// 调用Dwin自动上传数据解析器处理完整帧dwin_auto_load_data_parser(address, buffer + DWIN_DATA_OFFSET_INDEX, data_count);// 处理完成后清空缓冲区len = 0;}
}/*** @brief Dwin串口接收处理线程* @param arg 线程参数(未使用)* @details 阻塞等待串口接收完成信号,接收到数据后调用帧收集函数处理*/
static void dwin_serail_rx_dealer(void *arg)
{static rt_uint8_t rx_buffer[BSP_UART3_RX_BUFSIZE + 1];  // 接收缓冲区rt_uint16_t len;                                        // 接收数据长度while (1){// 阻塞等待完成量(由串口接收回调函数触发)rt_completion_wait(&interface_dwin_serial.cpt, RT_WAITING_FOREVER);// 从串口设备读取数据(非阻塞模式,实际已由回调确保有数据)len = rt_device_read(interface_dwin_serial.device, 0, rx_buffer, BSP_UART3_RX_BUFSIZE);// 处理接收到的数据,收集完整帧collect_dwin_data_frame(rx_buffer, len);}
}/*** @brief 串口接收回调函数* @param deveice 串口设备指针* @param size 接收数据长度(未使用)* @return 总是返回RT_EOK* @details 当串口接收到数据时被调用,释放完成量以唤醒处理线程*/
static rt_err_t dwin_serial_rx_callback(rt_device_t deveice, rt_size_t size)
{// 释放完成量,唤醒等待的处理线程rt_completion_done(&interface_dwin_serial.cpt);return RT_EOK;
}/*** @brief 初始化Dwin串口通信* @details 配置串口参数、注册回调函数、创建并启动接收处理线程*/
void init_dwin_serial(void)
{struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;  // 串口默认配置(波特率等)rt_err_t result;                                           // 操作结果rt_thread_t thread;                                         // 线程句柄/* 步骤1:查找串口设备 */interface_dwin_serial.device = rt_device_find(INTERFACE_DWIN_SERIAL_NAME);// 断言:确保串口设备存在,否则程序终止(调试阶段用于排查设备问题)RT_ASSERT(interface_dwin_serial.device != RT_NULL);/* 步骤2:配置串口参数(如波特率、数据位、停止位等) */result = rt_device_control(interface_dwin_serial.device, RT_DEVICE_CTRL_CONFIG, &config);// 断言:确保配置成功RT_ASSERT(result == RT_EOK);/* 步骤3:打开串口设备 */// 模式:非阻塞接收(有数据时通过回调通知)、阻塞发送(发送时等待完成)result = rt_device_open(interface_dwin_serial.device, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);RT_ASSERT(result == RT_EOK);/* 步骤4:初始化完成量(用于线程同步) */rt_completion_init(&interface_dwin_serial.cpt);/* 步骤5:注册串口接收回调函数 */result = rt_device_set_rx_indicate(interface_dwin_serial.device, dwin_serial_rx_callback);RT_ASSERT(result == RT_EOK);/* 步骤6:创建并启动接收处理线程 */thread = rt_thread_create("DWIN_RX",               // 线程名称dwin_serail_rx_dealer,  // 线程入口函数RT_NULL,                // 入口函数参数INTERFACE_DWIN_SERIAL_THREAD_STACK_SIZE,  // 栈大小INTERFACE_DWIN_SERIAL_THREAD_PRO,         // 优先级INTERFACE_DWIN_SERIAL_THREAD_SEC);        // 时间片// 断言:确保线程创建成功RT_ASSERT(thread != RT_NULL);/* 启动线程 */rt_thread_startup(thread);
}/*** @brief 向Dwin屏发送数据* @param buff 待发送数据缓冲区* @param size 数据长度* @details 通过串口设备发送数据到Dwin屏(阻塞模式,确保数据发送完成)*/
void dwin_serial_send(rt_uint8_t *buff, rt_uint32_t size)
{// 调用设备接口发送数据(UART3对应Dwin屏的串口)rt_device_write(interface_dwin_serial.device, 0, buff, size);
}

这段代码是基于 RT-Thread 操作系统的迪文屏串口通信驱动,主要实现以下功能:

  • 串口设备管理:通过interface_dwin_serial结构体管理连接迪文屏的 UART3 设备,包含设备句柄和同步用的完成量。
  • 数据接收机制:
    • 采用 “中断回调 + 完成量 + 线程” 的异步接收模式:串口硬件收到数据后触发回调函数,通过完成量唤醒接收线程。
  • 接收线程(dwin_serail_rx_dealer)负责读取数据并调用collect_dwin_data_frame函数拼接完整帧。
    • 帧拼接函数解决了串口数据分片接收的问题,确保只有完整的数据帧才会被处理。
  • 数据发送功能:提供dwin_serial_send函数,通过阻塞模式的串口写操作向迪文屏发送数据,保证数据可靠传输。
  • 初始化流程:init_dwin_serial函数完成串口设备的查找、配置、打开,注册回调函数并- 创建接收线程,为通信提供基础环境。

interface_curve.c

#include <board.h>
#include <string.h>#include "interface_dwin.h"      // Dwin屏通信接口
#include "interface_curve.h"     // 曲线显示接口定义#define DBG_LEVEL    DBG_LOG
#define DBG_TAG      "interface_curve"
#include <rtdbg.h>               // RT-Thread日志库/*============================ 宏定义 ================================*/
#define CLEAN_CURVE_CAHNNEL_INDEX    4  // 清理曲线命令中通道编号的索引位置/*============================ 静态数据结构与变量 ================================*/
/*** @brief 曲线通道清理信息结构体* @details 记录各曲线通道的使用状态,用于清理曲线时识别需要操作的通道*/
static struct curve_clean_info
{rt_int16_t curve_channel;  // 曲线通道编号(与Dwin屏定义一致)rt_bool_t used;            // 通道是否被使用(RT_TRUE:已使用,RT_FALSE:未使用)
}curve_clean_list[DWIN_CURVE_CHANNEL_MAX_COUNT] =  // 曲线通道清理列表
{{DWIN_CURVE_CHANNEL1, RT_FALSE},{DWIN_CURVE_CHANNEL2, RT_FALSE},{DWIN_CURVE_CHANNEL3, RT_FALSE},{DWIN_CURVE_CHANNEL4, RT_FALSE},{DWIN_CURVE_CHANNEL5, RT_FALSE},{DWIN_CURVE_CHANNEL6, RT_FALSE},{DWIN_CURVE_CHANNEL7, RT_FALSE},{DWIN_CURVE_CHANNEL8, RT_FALSE},
};/*** @brief 曲线数据帧缓冲区* @details 用于构建发送到Dwin屏的曲线数据帧,遵循Dwin屏通信协议格式*/
static rt_uint8_t curve_data_frame[DWIN_DATA_FRAME_MAX_LENGTH] =
{0x5A, 0xA5,                  // 帧头(固定标识)0x00,                         // 后续字节数(动态填充,DWIN_DATA_BYTE_COUNT_INDEX)0x82,                         // 曲线操作命令(Dwin协议定义)0x03, 0x10,                   // 固定参数(曲线控制指令)0x5A, 0xA5,                   // 子帧头(嵌套帧标识)0x00,                         // 同时写入的曲线通道个数(CURVE_CHANNEL_COUNT_INDEX)0x00,                         // 保留位
};
// 曲线数据帧中关键字段的索引定义
#define CURVE_CHANNEL_COUNT_INDEX    8   // 曲线通道个数的索引位置
#define CURVE_DATA_START_INDEX       10  // 曲线实际数据的起始索引位置/*** @brief 单条曲线数据缓冲区* @details 存储单条曲线的通道编号、数据个数和具体数据,用于拼接至总数据帧*/
static rt_uint8_t one_curve_data_buff[DWIN_CURVE_DATA_MAX_COUNT * 2 + 2] =
{0x00,  // 曲线通道编号(00~07,CURVE_CHANNEL_ID_INDEX)0x00,  // 本次写入的数据个数(CURVE_DATA_COUNT_INDEX)
};
// 单条曲线数据缓冲区中关键字段的索引定义
#define CURVE_CHANNEL_ID_INDEX      0   // 通道编号索引
#define CURVE_DATA_COUNT_INDEX      1   // 数据个数索引
#define CURVE_DATA_OFFSET_INDEX     2   // 实际数据的起始偏移量/*** @brief 清理曲线命令帧* @details 用于向Dwin屏发送清理指定曲线通道的命令,固定格式*/
static rt_uint8_t clean_curve_command[] = 
{0x5A, 0xA5,  // 帧头0x05,        // 后续字节数(固定)0x82,        // 曲线操作命令0x03,        // 曲线通道编号(可动态修改)0x00, 0x00,  // 保留位0x00         // 结束标识
};/*** @brief 曲线数据结构体数组* @details 存储所有曲线的核心信息,包括数据队列、所属通道、互斥锁和数据调整函数*/
static curve_data_t curve_list[DWIN_CURVE_MAX_COUNT];/*** @brief 曲线窗口结构体* @details 每个窗口可包含多条曲线,记录曲线索引、数量及是否首次显示*/
static struct curve_window
{rt_int16_t curve_index_list[DWIN_CURVE_IN_WINDOW_MAX_COUNT];  // 窗口内的曲线索引列表rt_int16_t curve_count;                                       // 窗口内的曲线数量rt_bool_t first_show;                                         // 是否首次显示(RT_TRUE:首次)
}curve_window_list[DWIN_CURVE_WINDOW_MAX_COUNT];  // 曲线窗口列表static volatile rt_int16_t current_curve_window_index;  // 当前激活的曲线窗口索引
static volatile rt_int16_t last_curve_window_index;     // 上一个激活的曲线窗口索引/*============================ 内部函数实现 ================================*/
/*** @brief 获取并调整曲线数据* @param curve_id 曲线ID(对应curve_list中的索引)* @param all 是否获取所有数据(RT_TRUE:全量数据,RT_FALSE:仅新增数据)* @return 有效数据的个数(0表示无数据)* @details 从曲线数据队列中读取数据,通过调整函数处理后存入单曲线缓冲区,*          同时设置缓冲区中的通道编号和数据个数*/
static rt_uint16_t get_and_adjust_curve_data(rt_uint16_t curve_id, rt_bool_t all)
{curve_data_t *curve = &curve_list[curve_id];  // 当前曲线的信息结构体rt_uint16_t curve_data_count = 0;             // 有效数据的个数rt_uint16_t *curve_data_list;                 // 指向数据存储缓冲区rt_uint16_t index;                            // 循环索引// 指向单曲线缓冲区中的数据区域(跳过通道编号和数据个数)curve_data_list = (rt_uint16_t *) (one_curve_data_buff + CURVE_DATA_OFFSET_INDEX);// 加锁保护数据队列操作(多线程安全)rt_mutex_take(curve->mutex, RT_WAITING_FOREVER);if (all == RT_TRUE){// 获取队列中所有数据(首次显示时使用)curve_data_count = curve_data_queue_get_all_data(&curve->queue, curve_data_list);}else {// 获取队列中自上次读取后的新增数据(非首次显示时使用)curve_data_count = curve_data_queue_get_last_data(&curve->queue, curve_data_list);}rt_mutex_release(curve->mutex);  // 解锁if (curve_data_count == 0){return curve_data_count;  // 无数据则返回}// 设置单曲线缓冲区的通道编号和数据个数one_curve_data_buff[CURVE_CHANNEL_ID_INDEX] = curve->curve_channel & 0xFF;one_curve_data_buff[CURVE_DATA_COUNT_INDEX] = curve_data_count & 0xFF;// 对每个数据执行调整函数(适配Dwin屏的显示范围或格式)for (index = 0; index < curve_data_count; index++){curve_data_list[index] = curve->adjust_fun(curve_data_list[index]);}return curve_data_count;
}/*============================ 外部函数实现 ================================*/
/*** @brief 显示指定窗口的曲线数据* @param curve_window_id 曲线窗口ID(对应curve_window_list中的索引)* @param all 是否显示所有数据(RT_TRUE:全量,RT_FALSE:仅新增)* @details 遍历窗口内的所有曲线,获取并拼接数据,构建完整的Dwin协议帧后发送,*          实现曲线在Dwin屏上的实时显示*/
void curve_show(rt_int16_t curve_window_id, rt_bool_t all)
{rt_uint16_t show_curve_data_count = 0;  // 总显示数据个数rt_uint16_t show_curve_count = 0;       // 总显示曲线条数struct curve_window *curve_window;      // 当前窗口的信息结构体rt_uint16_t index;                      // 循环索引rt_uint16_t one_curve_data_count = 0;   // 单条曲线的数据个数rt_uint16_t curve_data_offset = CURVE_DATA_START_INDEX;  // 数据在帧中的偏移量curve_window = &curve_window_list[curve_window_id];  // 获取当前窗口信息show_curve_count = curve_window->curve_count;         // 窗口内的曲线数量// 设置数据帧中曲线通道的总个数curve_data_frame[CURVE_CHANNEL_COUNT_INDEX] = show_curve_count & 0xFF;// 遍历窗口内的每条曲线for (index = 0; index < show_curve_count; index++){// 获取并调整当前曲线的数据one_curve_data_count = get_and_adjust_curve_data(curve_window->curve_index_list[index], all);if (one_curve_data_count <= 0){continue;  // 无数据则跳过该曲线}// 将单曲线数据复制到总数据帧中rt_memcpy(curve_data_frame + curve_data_offset, one_curve_data_buff, one_curve_data_count * 2 + 2);  // 数据长度 = 个数*2字节 + 2字节头部// 更新数据偏移量(为下一条曲线预留空间)curve_data_offset += one_curve_data_count * 2 + 2;// 累加总数据个数show_curve_data_count += one_curve_data_count;}// 无有效数据则不发送if (show_curve_data_count <= 0){return;}// 设置数据帧的总字节数(从字节数字段到帧尾的长度)curve_data_frame[DWIN_DATA_BYTE_COUNT_INDEX] = (curve_data_offset - 3) & 0xFF;// 发送数据帧到Dwin屏dwin_serial_send(curve_data_frame, curve_data_offset);
}/*** @brief 判断曲线窗口是否首次显示* @param curve_window_id 曲线窗口ID* @return 首次显示返回RT_TRUE,否则返回RT_FALSE*/
rt_bool_t is_curve_window_first_show(rt_int16_t curve_window_id)
{return curve_window_list[curve_window_id].first_show;
}/*** @brief 标记曲线窗口为非首次显示* @param curve_window_id 曲线窗口ID* @details 将窗口的first_show设为RT_FALSE,后续显示时仅更新新增数据*/
void set_curve_window_not_first_show(rt_int16_t curve_window_id)
{curve_window_list[curve_window_id].first_show = RT_FALSE;
}/*** @brief 获取当前激活的曲线窗口索引* @return 当前窗口索引(-1表示无激活窗口)*/
rt_int16_t get_current_curve_window(void)
{return current_curve_window_index;
}/*** @brief 设置当前激活的曲线窗口* @param curve_window_id 目标窗口ID(-1表示无激活窗口)* @details 切换窗口时,标记上一个窗口为"需首次显示"(下次显示时全量刷新),*          更新当前和上一个窗口的索引*/
void set_current_curve_window(rt_int16_t curve_window_id)
{// 窗口未变化则直接返回if (current_curve_window_index == curve_window_id){return;}// 标记上一个窗口为"需首次显示"if (last_curve_window_index != -1){curve_window_list[last_curve_window_index].first_show = RT_TRUE;}// 更新上一个窗口索引(当前窗口非-1时)if (current_curve_window_index != -1){last_curve_window_index = current_curve_window_index;}// 设置当前窗口索引current_curve_window_index = curve_window_id;
}/*** @brief 向曲线数据队列添加数据* @param curve_id 曲线ID* @param data 待添加的数据* @details 加锁保护数据队列,确保多线程环境下的数据安全性,*          数据将被存入对应曲线的循环队列中*/
void add_curve_data(rt_uint16_t curve_id, rt_uint16_t data)
{// 校验曲线ID合法性(超出最大数量则断言报错)if (curve_id >= DWIN_CURVE_MAX_COUNT){LOG_W("curve index (%d) too large!", curve_id);RT_ASSERT(0);  // 调试阶段确保参数合法}// 加锁保护队列操作rt_mutex_take(curve_list[curve_id].mutex, RT_WAITING_FOREVER);// 添加数据到曲线的循环队列curve_data_queue_add_data(&curve_list[curve_id].queue, data);rt_mutex_release(curve_list[curve_id].mutex);  // 解锁
}/*** @brief 将曲线添加到指定窗口* @param curve_id 曲线ID* @param curve_window_id 目标窗口ID* @details 将曲线索引存入窗口的曲线列表,同时更新窗口内的曲线数量*/
void add_curve_to_window(rt_uint16_t curve_id, rt_uint16_t curve_window_id)
{rt_uint16_t curve_count;  // 窗口内当前的曲线数量// 校验曲线ID合法性if (curve_id >= DWIN_CURVE_MAX_COUNT){LOG_W("curve index (%d) too large!", curve_id);RT_ASSERT(0);}// 校验窗口ID合法性if (curve_window_id >= DWIN_CURVE_WINDOW_MAX_COUNT){LOG_W("curve window index (%d) too large!", curve_window_id);RT_ASSERT(0);}// 获取窗口当前的曲线数量,添加新曲线索引并更新数量curve_count = curve_window_list[curve_window_id].curve_count;curve_window_list[curve_window_id].curve_index_list[curve_count] = curve_id;++curve_window_list[curve_window_id].curve_count;
}/*** @brief 清理所有已使用的曲线通道* @details 遍历曲线通道清理列表,对已使用的通道发送清理命令,*          发送后延时1ms确保Dwin屏有足够时间处理*/
void clean_curve(void)
{int index;  // 循环索引// 遍历所有曲线通道for (index = 0; index < DWIN_CURVE_CHANNEL_MAX_COUNT; index++){// 仅清理已使用的通道if (curve_clean_list[index].used == RT_TRUE){// 设置清理命令中的通道编号clean_curve_command[CLEAN_CURVE_CAHNNEL_INDEX] = curve_clean_list[index].curve_channel & 0xFF;clean_curve_command[CLEAN_CURVE_CAHNNEL_INDEX + 1] = (curve_clean_list[index].curve_channel >> 8) & 0xFF;// 发送清理命令到Dwin屏dwin_serial_send(clean_curve_command, sizeof(clean_curve_command));// 延时1ms,避免串口发送拥塞(串口发送耗时,需等待完成)rt_thread_mdelay(1);}}
}/*** @brief 初始化曲线* @param curve_id 曲线ID* @param curve_channel 曲线通道(Dwin屏定义的物理通道)* @param adjust_fun 数据调整函数(NULL则使用默认函数)* @details 初始化曲线的数据队列、所属通道、互斥锁和调整函数,*          并标记通道为已使用(用于后续清理)*/
void init_curve(rt_uint16_t curve_id, rt_uint16_t curve_channel, curve_data_adjust_t adjust_fun)
{rt_uint16_t index;  // 循环索引// 校验曲线ID合法性if (curve_id >= DWIN_CURVE_MAX_COUNT){LOG_W("curve index (%d) too large!", curve_id);RT_ASSERT(0);}// 校验曲线通道合法性(需符合Dwin屏定义的通道范围)if (!IS_DWIN_CURVE_CHANNEL(curve_channel)){LOG_W("error curve channel : %0x4X", curve_channel);RT_ASSERT(0);}// 标记通道为已使用(用于清理曲线时识别)for (index = 0; index < DWIN_CURVE_CHANNEL_MAX_COUNT; index++){if (curve_clean_list[index].curve_channel == curve_channel){curve_clean_list[index].used = RT_TRUE;break;}}// 初始化曲线的循环数据队列curve_data_queue_init(&curve_list[curve_id].queue);// 计算曲线通道编号(转换为Dwin屏内部的逻辑通道索引)curve_list[curve_id].curve_channel = ((curve_channel & 0x0F) - 1) >> 1;// 创建互斥锁(保护多线程对数据队列的操作)curve_list[curve_id].mutex = rt_mutex_create("CURVE", RT_IPC_FLAG_PRIO);// 设置数据调整函数(默认函数直接返回原始数据)if (adjust_fun == RT_NULL){adjust_fun = default_curve_data_adjust;}curve_list[curve_id].adjust_fun = adjust_fun;
}/*** @brief 默认曲线数据调整函数* @param data 原始数据* @return 调整后的数据(直接返回原始数据)* @details 当未指定调整函数时使用,适用于无需转换的数据*/
rt_uint16_t default_curve_data_adjust(rt_uint16_t data)
{return data;
}

这段代码实现了一个循环队列数据结构,专门用于存储曲线数据。主要功能包括:

  • 队列初始化:
    • curve_data_queue_init():将队列节点连接成循环链表,初始化头尾指针和读取指针。
  • 数据添加:
    • curve_data_queue_add_data():将数据添加到队列尾部,当队列满时自动覆盖最早的数据,实现 FIFO(先进先出)特性
  • 数据获取:
    • curve_data_queue_get_all_data():获取队列中所有数据,适用于首次显示或刷新显示。
    • curve_data_queue_get_last_data():获取自上次读取后新增的数据,适用于增量更新显示。

dispatcher_can_dwin.c

#include <string.h>
#include <board.h>#include "interface_dwin.h"      // Dwin屏通信接口
#include "dispatcher_can_dwin.h" // 本模块头文件(CAN与Dwin数据分发器)#define DBG_LEVEL    DBG_LOG
#define DBG_TAG      "dispatcher_can_dwin"
#include <rtdbg.h>               // RT-Thread日志库/*============================ 静态数据结构 ================================*/
/*** @brief CAN消息调度器结构体* @details 存储CAN消息ID与对应处理函数的映射关系,用于根据ID分发消息*/
static struct
{can_dispatcher_t *list;  // CAN调度器列表(数组),每个元素包含ID和处理函数rt_size_t count;         // 调度器数量(列表中元素的个数)
}can_dispatcher_tab;/*** @brief DWIN自动上传数据调度器结构体* @details 存储Dwin屏自动上传数据的地址与对应处理函数的映射关系,用于根据地址分发数据*/
static struct
{dwin_dispatcher_t *list; // DWIN调度器列表(数组),每个元素包含地址和处理函数rt_size_t count;         // 调度器数量(列表中元素的个数)
}dwin_auto_load_dispatcher_tab;/*============================ 外部函数实现 ================================*/
/* 初始化CAN消息调度器 */ 
/*** @brief 初始化CAN消息调度器* @param list CAN调度器列表(包含CAN ID和对应处理函数的数组)* @param count 调度器列表中元素的数量* @details 将外部定义的CAN消息处理映射表注册到调度器中,后续可通过CAN ID查找处理函数*/
void init_can_dispatcher(can_dispatcher_t *list, rt_size_t count)
{can_dispatcher_tab.list = list;   // 保存列表指针can_dispatcher_tab.count = count; // 保存列表元素数量
}/* 初始化DWIN自动上传数据调度器 */
/*** @brief 初始化Dwin自动上传数据调度器* @param list Dwin调度器列表(包含数据地址和对应处理函数的数组)* @param count 调度器列表中元素的数量* @details 将外部定义的Dwin数据处理映射表注册到调度器中,后续可通过地址查找处理函数*/
void init_dwin_dispatcher(dwin_dispatcher_t *list, rt_size_t count)
{dwin_auto_load_dispatcher_tab.list = list;   // 保存列表指针dwin_auto_load_dispatcher_tab.count = count; // 保存列表元素数量
}/* 根据CAN ID查找并调用对应的处理函数 */
/*** @brief CAN数据解析分发函数* @param id 接收到的CAN消息ID* @param buff 数据缓冲区(存储CAN消息的数据部分)* @param size 数据长度(buff中有效数据的字节数)* @details 遍历CAN调度器列表,根据CAN ID匹配对应的处理函数并执行,*          若未找到匹配的处理函数则打印日志提示*/
void can_data_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{int index;  // 循环索引// 遍历调度表中的所有元素for (index = 0; index < can_dispatcher_tab.count; index++){// 匹配CAN IDif (can_dispatcher_tab.list[index].id == id){// 找到匹配ID,调用对应的处理函数(钩子函数)can_dispatcher_tab.list[index].hook(id, buff, size);return;  // 处理完成后返回}}// 未找到匹配的处理函数,打印提示日志(ID以十六进制显示)LOG_I("CAN data (%04X) parser not found", id);
}// 根据变量地址查找并调用对应的处理函数
/*** @brief Dwin自动上传数据解析分发函数* @param address 接收到的数据对应的变量地址* @param buff 数据缓冲区(存储Dwin上传的数据部分)* @param size 数据长度(buff中有效数据的字节数)* @details 遍历Dwin调度器列表,根据数据地址匹配对应的处理函数并执行,*          处理前会进行字节序转换(与Dwin屏的字节序保持一致),*          若未找到匹配的处理函数则打印日志提示*/
void dwin_auto_load_data_parser(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size)
{int index;  // 循环索引// 转换地址字节序(Dwin屏采用特定字节序,需转换后才能与本地地址匹配)address = SWAP_16(address);// 遍历调度表中的所有元素for (index = 0; index < dwin_auto_load_dispatcher_tab.count; index++){// 匹配数据地址if (dwin_auto_load_dispatcher_tab.list[index].address == address){// 找到匹配地址,调用对应的处理函数(钩子函数)dwin_auto_load_dispatcher_tab.list[index].hook(address, buff, size);return;  // 处理完成后返回}}// 未找到匹配的处理函数,打印提示日志(地址以十六进制显示)LOG_I("DWIN auto load data (%04X) parser not found", address);
}/* 测试函数 用于接收到数据之后对数据的拼接上传打印 */
/*** @brief CAN数据默认解析函数(测试用)* @param id CAN消息ID* @param buff 数据缓冲区* @param size 数据长度* @details 将接收到的CAN数据拼接成十六进制字符串并通过日志打印,用于调试时查看原始数据*/
void default_can_data_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{char str[512] = {0};  // 存储拼接后的字符串char tmp[8];          // 临时存储单个字节的十六进制表示int i;                // 循环索引// 拼接CAN ID前缀(格式:"CAN data (ID) :")rt_sprintf(str, "CAN data (%04X) :", id);// 遍历数据缓冲区,拼接每个字节的十六进制值(格式:" XX")for (i = 0; i < size; i++){rt_sprintf(tmp, " %02X", buff[i]);  // 单个字节转为两位十六进制strcat(str, tmp);                  // 拼接至结果字符串}// 打印拼接后的调试信息LOG_I(str);
}/* 测试函数 用于接收到数据之后对数据的拼接上传打印 */
/*** @brief Dwin自动上传数据默认解析函数(测试用)* @param address 数据对应的变量地址* @param buff 数据缓冲区* @param size 数据长度* @details 将接收到的Dwin数据拼接成十六进制字符串并通过日志打印,用于调试时查看原始数据*/
void default_dwin_auto_load_data_parser(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size)
{char str[512] = {0};  // 存储拼接后的字符串char tmp[8];          // 临时存储单个字节的十六进制表示int i;                // 循环索引// 拼接地址前缀(格式:"Dwin auto load data (地址) :")rt_sprintf(str, "Dwin auto load data (%04X) :", address);// 遍历数据缓冲区,拼接每个字节的十六进制值(格式:" XX")// 注:size*2是因为Dwin数据通常以16位为单位,实际字节数为2*sizefor (i = 0; i < size * 2; i++){rt_sprintf(tmp, " %02X", buff[i]);  // 单个字节转为两位十六进制strcat(str, tmp);                  // 拼接至结果字符串}// 打印拼接后的调试信息LOG_I(str);
}

本模块是 CAN 总线与迪文屏之间的数据分发中心,核心功能是实现 “数据识别 - 处理函数匹配” 的自动化分发,主要包含:

  • 调度器注册:
    • 通过init_can_dispatcher注册 CAN 消息的 {ID, 处理函数} 映射表
    • 通过init_dwin_dispatcher注册迪文屏数据的 {地址,处理函数} 映射表
    • 注册后的数据会存储在静态结构体can_dispatcher_tabdwin_auto_load_dispatcher_tab
  • 数据分发:
    • can_data_parser:接收 CAN 数据后,根据 ID 在映射表中查找并调用对应的处理函数
    • dwin_auto_load_data_parser:接收迪文屏数据后,转换地址字节序,再根据地址查找并调用处理函数
  • 调试辅助:
    • 提供default_can_data_parser和default_dwin_auto_load_data_parser两个默认处理函数
    • 功能是将原始数据拼接成十六进制字符串并打印,方便开发阶段查看数据格式是否正确

dwin_page_var.c

#include <board.h>#include "interface_dwin.h"
#include "interface_curve.h"
#include "dwin_page_var.h"// 调试日志配置
#define DBG_LEVEL	DBG_LOG
#define DBG_TAG		"dwin_page_var"
#include <rtdbg.h>// 迪文屏变量显示线程配置
#define DWIN_VAR_SHOW_THREAD_STACK_SIZE		1024
#define DWIN_VAR_SHOW_THREAD_PRO					20
#define DWIN_VAR_SHOW_THREAD_SECTION			20/* 迪文屏变量信息 */
typedef struct dwin_var_info
{rt_uint16_t *var_list;			/* 迪文屏所有显示变量数据 */rt_uint16_t var_count;			/* 变量个数 */one_page_info_t *page_list;	/* 迪文屏界面列表(包含了该界面的数据) */rt_uint16_t page_count;			/* 界面个数 */
}dwin_var_info_t;static dwin_var_info_t dwin_var;	// 迪文屏变量信息实例
static volatile rt_uint16_t page_id;	/* 当前界面id */// 迪文屏通信命令缓冲区,固定头部和基本格式
static rt_uint8_t dwin_command_buff[DWIN_DATA_FRAME_MAX_LENGTH] = 
{0X5A, 0XA5, 					/* 迪文数据头部 */0x00,									/* 数据个数(调用时修改) */DWIN_COMMAND_WRITE,		/* 写指令 */0x00, 0x00,						/* 变量地址 */
};/* 准备迪文屏数据帧 */ 
static rt_int16_t prepared_dwin_data_frame(const one_page_info_t *one_page)
{/* 定义一个结构体指针存放数据 */dwin_data_frame_format_t *dwin_data_frame_format;// 设置数据帧格式dwin_data_frame_format = (dwin_data_frame_format_t *)dwin_command_buff;dwin_data_frame_format->command = DWIN_COMMAND_WRITE;												// 0x82 写命令dwin_data_frame_format->var_address = SWAP_16(one_page->var_address);				// 注意迪文变量地址需要更换字节序dwin_data_frame_format->byte_count = (rt_uint8_t)(one_page->count * 2 + 3);	// 迪文数据个数计算:变量个数*2+3(指令、变量地址)/* 5A  A5  0F  82  5000  0011  033A  FF99  FF77  0543  0022 *//* 将迪文屏所有显示变量数据复制到dwin_command_buff的写指令数据位(从第六位开始为数据位)长度为该界面下所有变量个数(注意变量是按16位作为一个单元,但在上传时分为2字节一个单元,所以需要*2) */rt_memcpy((rt_uint8_t *)(dwin_command_buff + DWIN_WRITE_DATA_OFFSET), (rt_uint8_t *)(dwin_var.var_list + one_page->start_index), one_page->count * 2);/* 根据上面后6个为数据 乘以2加上前面的数据头、数据个数、写指令以及变量地址可以得到整个指令长度 */return one_page->count * 2 + 6;
}/* 显示当前曲线窗口 */
static void show_current_curve_window(void)
{rt_int16_t current_curve_window_id;// 获取当前曲线窗口IDcurrent_curve_window_id = get_current_curve_window();if (current_curve_window_id == -1){return;}// 判断是否首次显示if (is_curve_window_first_show(current_curve_window_id)){clean_curve();					// 清除曲线curve_show(current_curve_window_id, RT_TRUE);	// 显示曲线所有数据set_curve_window_not_first_show(current_curve_window_id);	// 标记为非首次显示}else{curve_show(current_curve_window_id, RT_FALSE);	// 只显示曲线新数据}
}/* 迪文屏界面切换线程函数 */
static void dwin_var_show_thread(void *arg)
{int i;int len;// 遍历所有界面for (i = 0; i < dwin_var.page_count; i++){/* 当前界面刚好等于遍历到的界面id */if(page_id == dwin_var.page_list[i].page_id){/* 获得当前界面的信息长度 */len = prepared_dwin_data_frame(&dwin_var.page_list[i]);/* 将信息(指令)通过串口发送给迪文屏(迪文屏接收到后显示相应数据) */dwin_serial_send(dwin_command_buff, len);/* 执行该界面的特殊显示处理函数 */dwin_var.page_list[i].show_fun();// 显示曲线show_current_curve_window();}}
}/* 迪文屏变量初始化 */
void init_dwin_var(rt_uint16_t *var_list, rt_uint16_t var_count, one_page_info_t *page_list, rt_uint16_t page_count)
{rt_thread_t thread;// 保存变量和界面信息dwin_var.var_list = var_list;dwin_var.var_count = var_count;dwin_var.page_list = page_list;dwin_var.page_count = page_count;// 创建迪文屏显示线程thread = rt_thread_create("DWIN_SHOW", dwin_var_show_thread, RT_NULL,DWIN_VAR_SHOW_THREAD_STACK_SIZE,DWIN_VAR_SHOW_THREAD_PRO + 1,DWIN_VAR_SHOW_THREAD_SECTION);if (thread == RT_NULL){LOG_E("DWIN SHOW create failed");RT_ASSERT(0);}rt_thread_startup(thread);	// 启动线程
}/* 设置迪文屏当前界面id */
void set_current_page_id(rt_uint16_t current_page_id)
{page_id = current_page_id;
}/* 获取迪文屏当前界面id*/
rt_uint16_t get_current_page_id(void)
{return page_id;
}

这段代码实现了基于 RT-Thread 操作系统的迪文屏变量显示控制功能,主要包含以下部分:

  • 数据结构:定义了迪文屏变量信息结构体dwin_var_info_t用于存储所有变量数据和界面信息
  • 通信协议:实现了迪文屏通信协议,包括固定的数据帧格式和字节序处理
  • 界面管理:通过线程循环检测当前界面 ID,根据 ID 准备对应界面的数据并发送给迪文屏。
  • 曲线显示:支持曲线窗口的显示,区分首次显示和更新显示的不同处理。
  • 线程控制:创建了专门的显示线程,负责定期更新迪文屏显示内容(注意线程优先级 否则切换界面是会出现界面卡顿)。
  • 接口函数:提供了初始化、设置和获取当前界面 ID 等公共接口。

bll_can.c

#include <board.h>#include "interface_can.h"       // CAN通信接口
#include "interface_dwin.h"      // 迪文屏接口
#include "interface_curve.h"     // 曲线显示接口
#include "dwin_page_var.h"       // 迪文屏页面变量管理
#include "dispatcher_can_dwin.h" // CAN数据分发器
#include "bll_can.h"             // 本模块头文件
#include "bll_dwin.h"            // 迪文屏业务逻辑// 调试配置:日志级别为LOG,标签为"bll_can"
#define DBG_LEVEL    DBG_LOG
#define DBG_TAG      "bll_can"
#include <rtdbg.h>/* 迪文屏显示变量列表(所有需要在屏上显示的变量)注:不为0的数据需要进行字节序转换(SWAP_16) */
static rt_uint16_t dwin_var_list[] = 
{0,                              // 运行进程(百分比)	数据显示	输入	0x50000,                              // 本车加速度			数据显示	输入	0x50010,                              // 方向盘转角			数据显示	输入	0x50020,                              // 横摆角速度			数据显示	输入	0x50030,                              // 发动机扭矩			数据显示	输入	0x50040,                              // 本车车速(文本)		数据显示	输入	0x5005SWAP_16(25000),                // 本车质量(文本)1	数据显示	输入	0x5006(初始值25000,需字节序转换)0,                              // 道路坡度(文本)1	数据显示	输入	0x5007SWAP_16(2),                     // 指示灯(初始化ACC)	位变量图标	输入	0x5008(初始值2,需字节序转换)SWAP_16(1),                     // 挡位P(初始化P)		位变量图标	输入	0x5009(初始值1,需字节序转换)
};/*** CAN数据ID: 0x101 的数据格式说明* 运行进程	: 第0字节,范围0-100(百分比)* 本车车速	: 第1字节,范围0-100(单位未明确)* 指示灯	: 第2字节(包含ABS、ACC、Slip、Lost等指示灯状态,按位存储)* 挡位		: 第3字节(包含P、R、D、N等挡位状态,按位存储)*//* CAN ID为0x101的数据解析函数 */
static void can_data_101_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{rt_uint16_t value;  // 临时存储解析后的值(需转换字节序)// 解析运行进程(第0字节)value = buff[0];dwin_var_list[DWIN_DATA_FRAME_RUN_PROGRESS_INDEX] = SWAP_16(value);  // 转换字节序后存入变量列表// 解析本车车速(第1字节)value = buff[1];dwin_var_list[DWIN_DATA_FRAME_SELF_SPEED_INDEX] = SWAP_16(value);   // 转换字节序后存入变量列表add_curve_data(CURVE_SELF_SPEED_INDEX, value);  // 将车速数据添加到曲线显示队列// 解析指示灯状态(第2字节)value = buff[2];dwin_var_list[DWIN_DATA_FRAME_LIGHT_INDEX] = SWAP_16(value);        // 转换字节序后存入变量列表// 解析挡位状态(第3字节)value = buff[3];dwin_var_list[DWIN_DATA_FRAME_GEAR_INDEX] = SWAP_16(value);         // 转换字节序后存入变量列表
}/*** CAN数据ID: 0x201 的数据格式说明* 本车加速度	: 第0-1字节(16位),范围-20~10,精度0.01* 估计加速度	: 第2-3字节(16位),范围-20~10,精度0.01* 方向盘转角	: 第4-5字节(16位),范围-720~720(度)*/
static void can_data_201_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{rt_uint16_t value;  // 临时存储解析后的值(16位)// 解析本车加速度(第0-1字节,小端模式拼接)value = buff[1] | (buff[0] << 8);  // 低字节在前,高字节在后,拼接为16位值dwin_var_list[DWIN_DATA_FRAME_SELF_ACC_INDEX] = SWAP_16(value);    // 转换字节序后存入变量列表add_curve_data(CURVE_REAL_ACC_INDEX, value);  // 添加到加速度曲线队列// 解析估计加速度(第2-3字节)value = buff[3] | (buff[2] << 8);add_curve_data(CURVE_ESTI_ACC_INDEX, value);  // 添加到估计加速度曲线队列// 解析方向盘转角(第4-5字节)value = buff[5] | (buff[4] << 8);dwin_var_list[DWIN_DATA_FRAME_STEERING_INDEX] = SWAP_16(value);    // 转换字节序后存入变量列表
}/*** CAN数据ID: 0x301 的数据格式说明* 横摆角速度	: 第0-1字节(16位),范围-20~20,精度0.1* 发动机扭矩	: 第2-3字节(16位),范围-3000~3000* 本车质量		: 第4-5字节(16位),范围0~50000* 道路坡度		: 第6字节(8位),范围-90~90*/
static void can_data_301_parser(rt_uint32_t id, rt_uint8_t *buff, rt_size_t size)
{rt_uint16_t value;  // 临时存储解析后的值(16位)// 解析横摆角速度(第0-1字节)value = buff[1] | (buff[0] << 8);dwin_var_list[DWIN_DATA_FRAME_YAW_INDEX] = SWAP_16(value);         // 转换字节序后存入变量列表// 解析发动机扭矩(第2-3字节)value = buff[3] | (buff[2] << 8);dwin_var_list[DWIN_DATA_FRAME_TORQUE_INDEX] = SWAP_16(value);      // 转换字节序后存入变量列表
}/* 页面0(运行进度条页面)的显示处理函数 */
void page_0_show(void)
{// 迪文屏图形复制命令帧:用于绘制运行进度条// 格式:5A A5 [长度] 82 [变量地址] [图形复制命令] [参数]rt_uint8_t data_frame[] ={0x5A, 0xA5,                   // 帧头0x17,                         // 数据长度(字节数)0x82,                         // 写命令0x51, 0x00,                   // 显示变量地址(0x5100)0x00, 0x06,                   // 图形复制命令(0x0600)0x00, 0x01,                   // 复制数量(1个)0x00, 0x02,                   // 复制来源页面(页面2)0x01, 0x28,                   // xss(源起点X坐标)0x00, 0x00,                   // yss(源起点Y坐标)0x01, 0xD6,                   // xse(源终点X坐标)0x00, 0x23,                   // yse(源终点Y坐标)0x00, 0x0E,                   // xt(目标X坐标)0x00, 0x0A,                   // yt(目标Y坐标)0xFF, 0x00                    // 颜色(红色)};rt_uint16_t run_progress;  // 运行进度值(0-100)rt_uint16_t xss;           // 进度条起点X坐标(动态计算)// 从变量列表中获取运行进度(需转换字节序)run_progress = SWAP_16(dwin_var_list[DWIN_DATA_FRAME_RUN_PROGRESS_INDEX]);// 根据进度计算进度条起点X坐标(总长度470,按比例缩放)xss = (1 - run_progress / 100.0) * 470;// 将计算出的X坐标拆分到数据帧(高字节和低字节)data_frame[12] = xss >> 8;    // 高8位data_frame[13] = xss & 0xFF;  // 低8位// 发送命令帧到迪文屏dwin_serial_send(data_frame, sizeof(data_frame));    
}/* 初始化CAN业务逻辑模块 */
void init_bll_can(void)
{// CAN数据分发器列表:绑定CAN ID与对应的解析函数static can_dispatcher_t can_dispatcher_pool[] = {{ 0x101, can_data_101_parser },  // ID=0x101的数据由can_data_101_parser解析{ 0x201, can_data_201_parser },  // ID=0x201的数据由can_data_201_parser解析{ 0x301, can_data_301_parser },  // ID=0x301的数据由can_data_301_parser解析};  // 迪文屏页面信息列表:定义页面的属性和处理函数static one_page_info_t dwin_pages[] = {{0,                                  // 页面ID为0DWIN_DATA_FRAME_RUN_PROGRESS_INDEX, // 页面数据在变量列表中的起始索引DWIN_DATA_RUN_PROGRESS_ADDRESS,     // 页面数据在迪文屏的起始地址10,                                 // 页面包含的变量数量(10个)page_0_show                         // 页面显示处理函数},};  // 初始化CAN通信init_can();// 初始化CAN数据分发器(注册ID与解析函数的映射)init_can_dispatcher(can_dispatcher_pool, sizeof(can_dispatcher_pool) / sizeof(can_dispatcher_t));// 初始化迪文屏变量管理(绑定变量列表和页面信息)init_dwin_var(dwin_var_list, sizeof(dwin_var_list)/sizeof(rt_uint16_t),  // 变量数量dwin_pages, sizeof(dwin_pages)/sizeof(one_page_info_t)  // 页面数量);
}/* 设置迪文屏变量的值(通过索引) */
void set_dwin_var_value(rt_uint16_t var_index, rt_uint16_t value)
{dwin_var_list[var_index] = value;
}/* 获取迪文屏变量的值(通过索引) */
rt_uint16_t get_dwin_var_value(rt_uint16_t var_index)
{return dwin_var_list[var_index];
}

这段代码是基于 RT-Thread 系统的 CAN 数据处理与迪文屏显示的业务逻辑模块,主要功能如下:

  • 变量管理:通过dwin_var_list数组存储所有需要在迪文屏上显示的变量(如运行进度、车速、加速度等),并提供set_dwin_var_valueget_dwin_var_value接口用于变量读写。
  • CAN 数据解析:
    • 定义了 CAN ID 为 0x101、0x201、0x301 的数据格式
    • 为每个 ID 实现了解析函数,将 CAN 总线上的原始数据转换为变量并更新到dwin_var_list
    • 支持字节序转换(SWAP_16),适配迪文屏的通信要求(大端存储(迪文屏通信要求),是将数据的低位字节放到高地址处,高位字节放到低地址处。小端存储,是将数据的低位字节放到低地址处,高位字节放到高地址处。)
  • 页面显示:
    • 实现了页面 0(page_0_show)的显示逻辑,主要用于绘制运行进度条
    • 通过迪文屏的图形复制命令动态更新进度条的长度(根据运行进度计算)
  • 初始化流程:
    • 初始化 CAN 通信
    • 注册 CAN 数据分发器(绑定 ID 与解析函数)
    • 初始化迪文屏变量管理,关联变量列表与页面信息

bll_dwin.c

#include <board.h>#include "bll_dwin.h"              // 迪文屏业务逻辑层头文件
#include "bll_can.h"               // CAN业务逻辑层头文件
#include "interface_can.h"         // CAN硬件接口
#include "interface_dwin.h"        // 迪文屏硬件接口
#include "interface_curve.h"       // 曲线显示接口
#include "dispatcher_can_dwin.h"   // 数据分发器
#include "dwin_page_var.h"         // 迪文屏页面变量管理// 调试配置:日志级别为LOG,标签为"bll_can"
#define DBG_LEVEL    DBG_LOG
#define DBG_TAG      "bll_can"
#include <rtdbg.h>// 迪文屏自动上传数据的地址定义(对应迪文屏上的交互控件地址)
#define DWIN_AUTO_LOAD_DATA_SELF_QUALITY   0x500A  // 本车质量数据地址
#define DWIN_AUTO_LOAD_DATA_SLOPE          0x500B  // 道路坡度数据地址
#define DWIN_AUTO_LOAD_DATA_SELECT_PAGE    0x500C  // 页面切换控制地址
#define DWIN_AUTO_LOAD_DATA_CURVE_BUTTON   0x500D  // 曲线窗口切换按钮地址/* CAN ID为0x301的数据缓冲区 用于存储需要通过CAN发送的本车质量和道路坡度等数据 */
static rt_uint8_t auto_upload_data[] =
{0x00,  /* 横摆角速度(高8位) */0x00,  /* 横摆角速度(低8位) */0x00,  /* 发动机扭矩(高8位) */0x00,  /* 发动机扭矩(低8位) */0x00,  /* 本车质量(高8位) */0x00,  /* 本车质量(低8位) */0x00,  /* 道路坡度(8位数据) */
};static rt_uint16_t lasted_curve_window_id;  // 记录上一次选中的曲线窗口ID/* 本车质量数据解析函数(对应地址0x500A) */
static void self_quality_parser(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size)
{rt_uint16_t value;  // 存储解析后的本车质量值// 从缓冲区解析16位数据(高8位+低8位)value = (buff[0] << 8) | buff[1];// 转换刻度:迪文屏滑动控件范围是0~1000,对应本车质量0~50000(1000*50=50000)value *= 50;// 更新迪文屏显示变量(需转换字节序)set_dwin_var_value(DWIN_DATA_FRAME_QUALITY_INDEX, SWAP_16(value));// 更新CAN发送缓冲区(本车质量高8位和低8位)auto_upload_data[4] = (value >> 8) & 0xFF;  // 高8位auto_upload_data[5] = value & 0xFF;         // 低8位// 通过CAN发送更新后的数据(ID=0x301)can_send(0x301, auto_upload_data, sizeof(auto_upload_data));
}/* 道路坡度数据解析函数(对应地址0x500B) */
static void road_slope_parser(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size)
{rt_int16_t value;  // 存储解析后的道路坡度值(支持负数)// 从缓冲区解析16位数据value = (buff[0] << 8) | buff[1];// 转换刻度:迪文屏滑动控件范围0~1000,对应道路坡度-90~90// 计算逻辑:(0~1000 - 500) → -500~500 → 除以500.0 → -1~1 → 乘以90 → -90~90value = (value - 500) / 500.0 * 90;// 更新迪文屏显示变量(转换字节序)set_dwin_var_value(DWIN_DATA_FRAME_SLOPE_INDEX, SWAP_16(value));// 更新CAN发送缓冲区(道路坡度为8位数据)auto_upload_data[6] = value & 0xFF;// 通过CAN发送更新后的数据(ID=0x301)can_send(0x301, auto_upload_data, sizeof(auto_upload_data));
}/* 页面切换解析函数(对应地址0x500C) */
static void select_page_parser(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size)
{rt_uint16_t value;  // 页面ID值// 从缓冲区解析16位页面IDvalue = (buff[0] << 8) | buff[1];// 设置当前页面ID(切换显示页面)set_current_page_id(value);// 根据页面ID控制曲线窗口显示状态if (value == 1){// 页面1:关闭曲线窗口(-1表示无曲线窗口)set_current_curve_window(-1);}else if (value == 0){// 页面0:恢复上一次显示的曲线窗口set_current_curve_window(lasted_curve_window_id);}
}/* 曲线窗口切换解析函数(对应地址0x500D) */
static void dwin_cruve_selected(rt_uint16_t address, rt_uint8_t *buff, rt_size_t size)
{rt_uint16_t value;  // 曲线窗口选择值// 从缓冲区解析16位选择值value = (buff[0] << 8) | buff[1];// 根据选择值切换曲线窗口if (value == 1){// 选择1:切换到车速曲线窗口lasted_curve_window_id = CURVE_WINDOW_SELF_SPEED;set_current_curve_window(CURVE_WINDOW_SELF_SPEED);}else if (value == 2){// 选择2:切换到加速度曲线窗口lasted_curve_window_id = CURVE_WINDOW_ACC;set_current_curve_window(CURVE_WINDOW_ACC);}
}/* 车速数据转换函数(适配曲线显示范围) */
static rt_uint16_t self_speed_adjust(rt_uint16_t value)
{// 转换逻辑:原始值 ×10 放大,再转换字节序value *= 10;value = SWAP_16(value);return value;
}/* 实际加速度数据转换函数(适配曲线显示范围) */
static rt_uint16_t real_acc_adjust(rt_uint16_t value)
{rt_int16_t data = (rt_uint16_t) value;  // 转为有符号整数处理// 转换逻辑:// 原始范围-2000~1000 → 偏移+2000 → 0~3000 → 除以3 → 0~1000(曲线显示范围)data = (data + 2000) / 3;data = data > 1000 ? 1000 : data;  // 限制最大值为1000data = SWAP_16(data);  // 转换字节序return (rt_uint16_t) data;
}/* 估计加速度数据转换函数(适配曲线显示范围) */
static rt_uint16_t esti_acc_adjust(rt_uint16_t value)
{rt_int16_t data = (rt_uint16_t) value;  // 转为有符号整数处理// 同实际加速度转换逻辑(统一显示范围)data = (data + 2000) / 3;data = data > 1000 ? 1000 : data;data = SWAP_16(data);return (rt_uint16_t) data;
}/* 迪文屏业务逻辑初始化函数 */
void init_bll_dwin(void)
{// 迪文屏数据分发器列表(地址与解析函数的映射)static dwin_dispatcher_t dwin_dispatcher_pool[] ={{ DWIN_AUTO_LOAD_DATA_SELF_QUALITY, self_quality_parser },  // 本车质量{ DWIN_AUTO_LOAD_DATA_SLOPE,          road_slope_parser },   // 道路坡度{ DWIN_AUTO_LOAD_DATA_SELECT_PAGE,    select_page_parser },  // 页面切换{ DWIN_AUTO_LOAD_DATA_CURVE_BUTTON,   dwin_cruve_selected }, // 曲线窗口切换};// 初始化曲线显示(绑定曲线ID、通道和转换函数)init_curve(CURVE_SELF_SPEED_INDEX,  DWIN_CURVE_CHANNEL1, self_speed_adjust);  // 车速曲线init_curve(CURVE_REAL_ACC_INDEX,    DWIN_CURVE_CHANNEL1, real_acc_adjust);    // 实际加速度曲线init_curve(CURVE_ESTI_ACC_INDEX,    DWIN_CURVE_CHANNEL2, esti_acc_adjust);    // 估计加速度曲线// 将曲线添加到对应的窗口(一个窗口可显示多条曲线)add_curve_to_window(CURVE_SELF_SPEED_INDEX,  CURVE_WINDOW_SELF_SPEED);  // 车速窗口只显示车速曲线add_curve_to_window(CURVE_REAL_ACC_INDEX,    CURVE_WINDOW_ACC);         // 加速度窗口显示实际+估计加速度add_curve_to_window(CURVE_ESTI_ACC_INDEX,    CURVE_WINDOW_ACC);// 设置初始显示的曲线窗口(默认显示车速曲线)set_current_curve_window(CURVE_WINDOW_SELF_SPEED);// 初始化迪文屏串口通信init_dwin_serial();// 初始化迪文屏数据分发器(注册地址与解析函数的映射)init_dwin_dispatcher(dwin_dispatcher_pool, sizeof(dwin_dispatcher_pool) / sizeof(dwin_dispatcher_t));
}

这段代码是迪文屏与 CAN 总线交互的业务逻辑层实现,主要功能包括:

  • 数据交互映射:
    • 定义迪文屏上交互控件(如滑动条、按钮)的地址(0x500A~0x500D)
    • 通过·dwin_dispatcher_pool·建立地址与解析函数的映射,实现 “控件操作→数据处理” 的自动分发
  • 用户输入处理:
    • 本车质量(0x500A):将迪文屏滑动条值(0-1000)转换为实际质量(0~50000),更新显示并通过 CAN 发送
    • 道路坡度(0x500B):将滑动条值(0-1000)转换为坡度(-90-90),更新显示并通过 CAN 发送
    • 页面切换(0x500C):根据页面 ID 切换显示页面,并控制曲线窗口的显示 / 隐藏
    • 曲线切换(0x500D):根据按钮值切换显示的曲线窗口(车速 / 加速度)
  • 曲线显示管理:
    • 初始化多条曲线(车速、实际加速度、估计加速度),绑定各自的转换函数(适配显示范围)
    • 支持多窗口显示:车速窗口只显示车速曲线,加速度窗口同时显示两条加速度曲线
    • 记录历史窗口状态,切换页面后可恢复之前显示的曲线
  • 初始化流程:
    • 初始化迪文屏串口通信
    • 注册数据分发器(绑定地址与解析函数)
    • 设置初始显示状态(默认显示车速曲线窗口)

util.c

/*** 初始化曲线数据队列* @param queue 队列指针* @details 将所有节点的next指针连接成环,初始状态下head、tail和read_point均指向0*/
void curve_data_queue_init(curve_data_queue_t *queue)
{rt_uint16_t index;// 初始化每个节点的next指针,形成环for (index = 0; index < MAX_COUNT; index++){queue->queue[index].next = (index + 1) % MAX_COUNT;}// 初始状态:空队列queue->head = queue->tail = queue->read_point = 0;
}/*** 向队列添加新数据点* @param queue 队列指针* @param data 新数据值* @details 数据写入tail位置,然后tail指针前移。若队列已满(head==tail),则head指针也前移(覆盖最早数据)*/
void curve_data_queue_add_data(curve_data_queue_t *queue, rt_uint16_t data)
{// 将数据写入当前tail位置queue->queue[queue->tail].data = data;// tail指针前移(循环)queue->tail = queue->queue[queue->tail].next;// 队列已满,发生覆盖(head指针前移)if (queue->tail == queue->head){queue->head = queue->queue[queue->head].next;}
}/*** 获取队列中所有数据点(从head到tail)* @param queue 队列指针* @param buff 输出缓冲区* @return 返回实际读取的数据点数* @details 读取后将read_point标记移动到tail位置*/
rt_uint16_t curve_data_queue_get_all_data(curve_data_queue_t *queue, rt_uint16_t *buff)
{rt_uint16_t index = 0;  // 输出缓冲区索引rt_uint16_t head;       // 当前读取位置head = queue->head;// 从head到tail遍历所有有效数据while (head != queue->tail){buff[index++] = queue->queue[head].data;head = queue->queue[head].next;}// 更新读取标记到最新数据位置queue->read_point = queue->tail;return index;  // 返回读取的数据点数
}/*** 获取上次读取后新增的数据点(从read_point到tail)* @param queue 队列指针* @param buff 输出缓冲区* @return 返回实际读取的数据点数* @details 仅读取上次读取后新增的数据,适合增量更新显示*/
rt_uint16_t curve_data_queue_get_last_data(curve_data_queue_t *queue, rt_uint16_t *buff)
{rt_uint16_t index = 0;  // 输出缓冲区索引rt_uint16_t head;       // 当前读取位置head = queue->read_point;// 从read_point到tail遍历新增数据while (head != queue->tail){buff[index++] = queue->queue[head].data;head = queue->queue[head].next;}// 更新读取标记到最新数据位置queue->read_point = queue->tail;return index;  // 返回读取的数据点数
}
  • 环形缓冲区实现:通过next指针形成链表环,避免了传统数组环形缓冲区的取模运算,提高效率。
  • 双指针管理:
    • head:指向最早数据的位置,当队列满时会自动覆盖旧数据
    • tail:指向即将写入的位置(新数据插入点)
    • read_point:记录上次读取位置,支持增量读取
  • 数据覆盖机制:当队列满时(head==tail),新数据会覆盖最早的数据,实现 FIFO 特性。
http://www.xdnf.cn/news/1112005.html

相关文章:

  • ADSP-1802这颗ADI的最新DSP应该怎么做开发(一)
  • JavaScript 常见10种设计模式
  • TCP详解——各标志位
  • linux 系统找出磁盘IO占用元凶 —— 筑梦之路
  • Java从入门到精通!第四天(面向对象(一))
  • HTTP和HTTPS部分知识点
  • python库之jieba 库
  • 模拟注意力:少量参数放大 Attention 表征能力
  • C#与FX5U进行Socket通信
  • 【设计模式】桥接模式(柄体模式,接口模式)
  • OneCode 3.0架构深度剖析:工程化模块管理与自治UI系统的设计与实现
  • 企业商业秘密保卫战:经营信息类案件维权全攻略
  • 分布式系统高可用性设计 - 缓存策略与数据同步机制
  • wedo稻草人-----第32节(免费分享图纸)
  • 实验一 接苹果
  • LeetCode经典题解:3、无重复字符的最长子串
  • ADI的EV-21569-SOM核心板和主板转接卡的链接说明
  • Kubernetes持久卷实战
  • 13. G1垃圾回收器
  • os.loadavg()详解
  • Python 训练营打卡 Day 59-经典时序预测模型3
  • Java 大视界 -- Java 大数据机器学习模型在电商用户复购行为预测与客户关系维护中的应用(343)
  • IDEA中一个服务创建多个实例
  • 【C/C++】迈出编译第一步——预处理
  • [案例八] NX二次开发长圆孔的实现(支持实体)
  • TensorFlow2 study notes[2]
  • 【Linux网络】IP 协议详解:结构、地址与交付机制全面解析
  • 算法第三十一天:贪心算法part05(第八章)
  • Qt 多线程编程:单例任务队列的设计与实现
  • 【数据结构初阶】--顺序表(二)