TFT屏幕:STM32硬件SPI+DMA+队列自动传输

看了网上的很多的SPI+DMA的代码,感觉都有一些缺陷,就是基本都是需要有手动等待DMA完成的这个操作,我感觉这种等待操作在很大程度上浪费了时间,那么我加入的“队列”就是一种将等待时间利用起来的方法。

原本的SPI+DMA的操作逻辑如下图,是比较简单的

下面是我加入队列逻辑的过程图,会变得比较庞大,占用的内容资源也多会比较多,需要斟酌使用

加入队列前的基本流程是控制"DC电平->写入数据->等待DMA传输完成->DC电平->写入数据->等待DMA传输完成"这种操作是很浪费时间资源的,那么加入队列之后的操作是"写入数据->写入数据->写入数据"大部分时间都在写入队列与中断中,其它部分都是DMA自己在传输数据,不需要一直等待

我会在最后放一个完整的代码(我使用的是ST7789,其中的执行代码是适配LVGL的)包括我自己使用芯片的驱动,有需要的可以直接拿走,基本上只需要修改一点点就可以用了。

首先是队列的创建,我这里只是简单写了一下,这个队列创建很占内存,内存分配也不是很灵活,其中忙标志也没有锁,小概率会出事,的如果有大佬的话可以修改一下,使其更完善一点

#define SPI_BUFFER_SIZE 4096
#define SPI_QUEUE_SIZE 8//数据结构体
typedef struct {uint8_t data[SPI_BUFFER_SIZE];uint16_t data_size;bool is_data; // 0:命令, 1:数据 用于控制DC
} spi_transaction_t;// 全局传输队列
spi_transaction_t tx_queue[SPI_QUEUE_SIZE];//队列索引
volatile uint16_t tx_read_index = 0;
volatile uint16_t tx_write_index = 0;
volatile uint16_t tx_count = 0;
//DMA忙标志
volatile uint8_t dma_busy = 0;

之后是队列数据的输入,需要输入数据地址,数据大小还有DC电平翻转方向(0命令/1数据),如果是ESP32的话好像可以实现DC的自动翻转,会方便很多,这里实现不了,就只能在DMA之前先把DC翻转给提前完成    if (tx->is_data)。还要注意队列的索引变化

/******************************************************************************函数说明:添加SPI传输任务到队列
******************************************************************************/
static void LCD_Add_To_Queue(uint8_t *data, uint16_t size, uint8_t is_data)
{// 等待队列有空位(非阻塞式可添加超时机制)while (tx_count >= SPI_QUEUE_SIZE) {// 可在此处添加少量延迟或任务调度}// 复制数据到队列uint8_t index = tx_write_index;memcpy(tx_queue[index].data, data, size);tx_queue[index].data_size = size;tx_queue[index].is_data = is_data;// 更新队列索引tx_write_index = (tx_write_index + 1) % SPI_QUEUE_SIZE;tx_count++;// 如果DMA空闲,立即启动传输if (!dma_busy) {LCD_Start_Next_Transaction();}
}/******************************************************************************函数说明:启动下一个DMA传输任务
******************************************************************************/
static void LCD_Start_Next_Transaction(void)
{if (tx_count == 0){       //lv_disp_t *disp = lv_disp_get_default();//if (disp != NULL) {//    lv_disp_flush_ready(disp->driver); //}这个是为了适配LVGL加的return;}else if(dma_busy){return;}uint8_t index = tx_read_index;spi_transaction_t *tx = &tx_queue[index];// 设置DC引脚if (tx->is_data) {OLED_DC_Set(); // 数据模式} else {OLED_DC_Clr(); // 命令模式}OLED_CS_Clr();dma_busy = 1;HAL_SPI_Transmit_DMA(&hspi1, tx->data, tx->data_size);
}

然后最后一个是DMA传输完成的回调,进入回调后需要寻找队列中是否还有数据,如果还有数据,就把数据传给DMA

/******************************************************************************函数说明:DMA传输完成回调
******************************************************************************/
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{if (hspi == &hspi1) {OLED_CS_Set(); // 传输完成,拉高CSdma_busy = 0;// 更新队列tx_read_index = (tx_read_index + 1) % SPI_QUEUE_SIZE;tx_count--;// 立即启动下一个传输if (tx_count == 0){       //lv_disp_t *disp = lv_disp_get_default();//if (disp != NULL) {//    lv_disp_flush_ready(disp->driver);//}这个是为了适配LVGL}else if (tx_count > 0) {LCD_Start_Next_Transaction();}}
}

最后是完整版本的代码

#include "oled.h"
#include <stdbool.h>
#include <stdint.h>
//#include "lvgl.h"
#include "stm32f4xx_hal.h"
// 在oled.h中定义#define SPI_BUFFER_SIZE 4096#define SPI_QUEUE_SIZE 8
typedef struct {uint8_t data[SPI_BUFFER_SIZE];uint16_t data_size;bool is_data; // 0:命令, 1:数据
} spi_transaction_t;// 全局传输队列
spi_transaction_t tx_queue[SPI_QUEUE_SIZE];//队列索引
volatile uint16_t tx_read_index = 0;
volatile uint16_t tx_write_index = 0;
volatile uint16_t tx_count = 0;
//DMA忙标志
volatile uint8_t dma_busy = 0;// 外部 SPI 句柄
extern SPI_HandleTypeDef hspi1;/******************************************************************************函数说明:启动下一个DMA传输任务
******************************************************************************/
static void LCD_Start_Next_Transaction(void)
{if (tx_count == 0){       //lv_disp_t *disp = lv_disp_get_default();//if (disp != NULL) {//    lv_disp_flush_ready(disp->driver);  // 正确!//}return;}else if(dma_busy){return;}uint8_t index = tx_read_index;spi_transaction_t *tx = &tx_queue[index];// 设置DC引脚if (tx->is_data) {OLED_DC_Set(); // 数据模式} else {OLED_DC_Clr(); // 命令模式}OLED_CS_Clr();dma_busy = 1;HAL_SPI_Transmit_DMA(&hspi1, tx->data, tx->data_size);
}/******************************************************************************函数说明:添加SPI传输任务到队列
******************************************************************************/
static void LCD_Add_To_Queue(uint8_t *data, uint16_t size, uint8_t is_data)
{// 等待队列有空位(非阻塞式可添加超时机制)while (tx_count >= SPI_QUEUE_SIZE) {// 可在此处添加少量延迟或任务调度}// 复制数据到队列uint8_t index = tx_write_index;memcpy(tx_queue[index].data, data, size);tx_queue[index].data_size = size;tx_queue[index].is_data = is_data;// 更新队列索引tx_write_index = (tx_write_index + 1) % SPI_QUEUE_SIZE;tx_count++;// 如果DMA空闲,立即启动传输if (!dma_busy) {LCD_Start_Next_Transaction();}
}/******************************************************************************函数说明:DMA传输完成回调
******************************************************************************/
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{if (hspi == &hspi1) {OLED_CS_Set(); // 传输完成,拉高CSdma_busy = 0;// 更新队列tx_read_index = (tx_read_index + 1) % SPI_QUEUE_SIZE;tx_count--;// 立即启动下一个传输if (tx_count == 0){       //lv_disp_t *disp = lv_disp_get_default();//if (disp != NULL) {//    lv_disp_flush_ready(disp->driver);//}}else if (tx_count > 0) {LCD_Start_Next_Transaction();}}
}/******************************************************************************函数说明:DMA传输错误回调
******************************************************************************/
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)
{if (hspi == &hspi1) {//OLED_CS_Set(); // 错误时也要拉高CSdma_busy = 0;// 可添加错误处理逻辑}
}/******************************************************************************函数说明:LCD写命令
******************************************************************************/
void LCD_WR_REG(u8 dat)
{LCD_Add_To_Queue(&dat, 1, 0); // 0表示命令
}/******************************************************************************函数说明:LCD写8位数据
******************************************************************************/
void LCD_WR_DATA8(u8 dat)
{LCD_Add_To_Queue(&dat, 1, 1); // 1表示数据
}/******************************************************************************函数说明:LCD写16位数据
******************************************************************************/
void LCD_WR_DATA(u16 dat)
{uint8_t temp[2] = {dat >> 8, dat & 0xFF};LCD_Add_To_Queue(temp, 2, 1); // 1表示数据
}/******************************************************************************函数说明:批量写入数据(优化性能)
******************************************************************************/
void LCD_Write_Bulk(uint8_t *data, uint16_t size)
{LCD_Add_To_Queue(data, size, 1);
}/******************************************************************************函数说明:设置显示区域起始坐标和结束坐标参数说明:x1,x2 起始和结束的列地址y1,y2 起始和结束的行地址返回值:  无
******************************************************************************/
void LCD_Address_Set(u16 x1, u16 y1, u16 x2, u16 y2)
{if (USE_HORIZONTAL == 0){LCD_WR_REG(0x2A); // Column Address SetLCD_WR_DATA(x1);LCD_WR_DATA(x2);LCD_WR_REG(0x2B); // Page Address SetLCD_WR_DATA(y1);LCD_WR_DATA(y2);LCD_WR_REG(0x2C); // Memory Write}else if (USE_HORIZONTAL == 1){LCD_WR_REG(0x2A);LCD_WR_DATA(y1);          // 注意:x 和 y 互换LCD_WR_DATA(y2);LCD_WR_REG(0x2B);LCD_WR_DATA(239 - x2);    // 坐标翻转LCD_WR_DATA(239 - x1);LCD_WR_REG(0x2C);}else if (USE_HORIZONTAL == 2){LCD_WR_REG(0x2A);LCD_WR_DATA(239 - x2);LCD_WR_DATA(239 - x1);LCD_WR_REG(0x2B);LCD_WR_DATA(319 - y2);LCD_WR_DATA(319 - y1);LCD_WR_REG(0x2C);}else if (USE_HORIZONTAL == 3){LCD_WR_REG(0x2A);LCD_WR_DATA(319 - y2);LCD_WR_DATA(319 - y1);LCD_WR_REG(0x2B);LCD_WR_DATA(x1);LCD_WR_DATA(x2);LCD_WR_REG(0x2C);}
}/******************************************************************************函数说明:LCD初始化参数说明:无返回值:  无
******************************************************************************/
void Lcd_Init(void)
{OLED_RES_Clr();HAL_Delay(200);OLED_RES_Set();HAL_Delay(100);//************* Start Initial Sequence **********//LCD_WR_REG(0x36);if (USE_HORIZONTAL == 0) LCD_WR_DATA8(0x00);else if (USE_HORIZONTAL == 1) LCD_WR_DATA8(0xC0);else if (USE_HORIZONTAL == 2) LCD_WR_DATA8(0x70);else LCD_WR_DATA8(0xA0);LCD_WR_REG(0x3A);LCD_WR_DATA8(0x05);LCD_WR_REG(0xB2);LCD_WR_DATA8(0x0C);LCD_WR_DATA8(0x0C);LCD_WR_DATA8(0x00);LCD_WR_DATA8(0x33);LCD_WR_DATA8(0x33);LCD_WR_REG(0xB7);LCD_WR_DATA8(0x35);LCD_WR_REG(0xBB);LCD_WR_DATA8(0x19);LCD_WR_REG(0xC0);LCD_WR_DATA8(0x2C);LCD_WR_REG(0xC2);LCD_WR_DATA8(0x01);LCD_WR_REG(0xC3);LCD_WR_DATA8(0x12);LCD_WR_REG(0xC4);LCD_WR_DATA8(0x20);LCD_WR_REG(0xC6);LCD_WR_DATA8(0x0F);LCD_WR_REG(0xD0);LCD_WR_DATA8(0xA4);LCD_WR_DATA8(0xA1);LCD_WR_REG(0xE0);LCD_WR_DATA8(0xD0);LCD_WR_DATA8(0x04);LCD_WR_DATA8(0x0D);LCD_WR_DATA8(0x11);LCD_WR_DATA8(0x13);LCD_WR_DATA8(0x2B);LCD_WR_DATA8(0x3F);LCD_WR_DATA8(0x54);LCD_WR_DATA8(0x4C);LCD_WR_DATA8(0x18);LCD_WR_DATA8(0x0D);LCD_WR_DATA8(0x0B);LCD_WR_DATA8(0x1F);LCD_WR_DATA8(0x23);LCD_WR_REG(0xE1);LCD_WR_DATA8(0xD0);LCD_WR_DATA8(0x04);LCD_WR_DATA8(0x0C);LCD_WR_DATA8(0x11);LCD_WR_DATA8(0x13);LCD_WR_DATA8(0x2C);LCD_WR_DATA8(0x3F);LCD_WR_DATA8(0x44);LCD_WR_DATA8(0x51);LCD_WR_DATA8(0x2F);LCD_WR_DATA8(0x1F);LCD_WR_DATA8(0x1F);LCD_WR_DATA8(0x20);LCD_WR_DATA8(0x23);LCD_WR_REG(0x21); // 逆显示LCD_WR_REG(0x11); // Sleep OutHAL_Delay(120);LCD_WR_REG(0x29); // Display On
}/******************************************************************************函数说明:填充指定区域参数说明:xsta,ysta   起始坐标xend,yend   结束坐标返回值:  无
******************************************************************************/
void LCD_Fill(u16 xsta, u16 ysta, u16 xend, u16 yend, u16 *color) {LCD_Address_Set(xsta, ysta, xend, yend);uint32_t total_pixels = (xend - xsta + 1) * (yend - ysta + 1);uint8_t *color_byte = (uint8_t*)color; // 转换为字节指针uint32_t transferred_pixels = 0;while (transferred_pixels < total_pixels) {// 每次传输最大块(SPI_BUFFER_SIZE字节 = 缓冲区容量)uint32_t chunk_bytes = SPI_BUFFER_SIZE;// 剩余像素对应的字节数 = (总像素 - 已传像素) * 2uint32_t remaining_bytes = (total_pixels - transferred_pixels) * 2;if (chunk_bytes > remaining_bytes) {chunk_bytes = remaining_bytes;}// 入队当前块LCD_Write_Bulk(color_byte + (transferred_pixels * 2), chunk_bytes);transferred_pixels += chunk_bytes / 2; // 字节数转像素数}
}// 临时缓冲区:10 行
static u16 fill_buf[LCD_H * 10];/*** @brief 测试:逐块填充整个屏幕为蓝色*/
void test_fill_entire_screen_blue(void)
{uint16_t y;HAL_Delay(500);// 填充当前块为蓝色for (int i = 0; i < LCD_H * 10; i++) {fill_buf[i] = YELLOW;}// 从 Y=0 开始,每次填充 10 行,直到填满 240 行for (y = 0; y < LCD_H; y += 10) {uint16_t y_end = (y + 9) < LCD_H ? (y + 9) : (LCD_H - 1);LCD_Fill(0, y, LCD_W, y_end, fill_buf);}
}

还有对应的.h文件

#ifndef __OLED_H
#define __OLED_H	   						  #include "main.h"
#include "stdlib.h"	  
#include "string.h"#define USE_HORIZONTAL 0#if USE_HORIZONTAL == 0 || USE_HORIZONTAL == 2#define LCD_W  240#define LCD_H  320
#else#define LCD_W  320#define LCD_H  240
#endif#define	u8 unsigned char
#define	u16 unsigned int
#define	u32 unsigned long#define OLED_CS_Clr() HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET)
#define OLED_CS_Set() HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET)#define OLED_DC_Clr() HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_RESET)
#define OLED_DC_Set() HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET)#define OLED_RES_Clr() HAL_GPIO_WritePin(LCD_RST_GPIO_Port, LCD_RST_Pin, GPIO_PIN_RESET)
#define OLED_RES_Set() HAL_GPIO_WritePin(LCD_RST_GPIO_Port, LCD_RST_Pin, GPIO_PIN_SET)#define OLED_CMD  0	
#define OLED_DATA 1	void Lcd_Init(void); 
void test_fill_entire_screen_blue(void);#define WHITE         	 0xFFFF
#define BLACK         	 0x0000	  
#define BLUE           	 0x001F  
#define BRED             0XF81F
#define GRED 			 0x07E0
#define GBLUE			 0X07FF
#define RED           	 0xF800
#define MAGENTA       	 0xF81F
#define GREEN         	 0x07E0
#define CYAN          	 0x7FFF
#define YELLOW        	 0xFFE0
#define BROWN 			 0XBC40
#define BRRED 			 0XFC07
#define GRAY  			 0X8430 #define DARKBLUE      	 0X01CF	
#define LIGHTBLUE      	 0X7D7C	
#define GRAYBLUE       	 0X5458 #define LIGHTGREEN     	 0X841F 
#define LGRAY 			 0XC618 #define LGRAYBLUE        0XA651 
#define LBBLUE           0X2B12 #endif   		     

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

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

相关文章

AI操作系统语言模型设计 之1 基于意识的Face-Gate-Window的共轭路径的思维-认知-情感嵌套模型

摘要&#xff08;AI生成&#xff09;本文提出了一种创新的AI操作系统语言模型设计框架&#xff0c;将人类意识活动的分层结构映射到人工智能系统中。该模型包含三个嵌套层次&#xff1a;理性思维层&#xff08;Face层&#xff09;&#xff1a;采用双面胶隐喻&#xff08;A/B面&…

疯狂星期四文案网第57天运营日记

网站运营第57天&#xff0c;点击观站&#xff1a; 疯狂星期四 crazy-thursday.com 全网最全的疯狂星期四文案网站 运营报告 今日访问量 今日搜索引擎收录情况

SQLark:一款面向信创应用开发者的数据库开发和管理工具

SQLark 是一款面向信创应用开发者的数据库开发和管理工具&#xff0c;用于快速查询、创建和管理不同类型的数据库系统&#xff0c;现已支持达梦、Oracle、MySQL、PostgreSQL 数据库。 SQLark 提供了对多种数据库的连接支持&#xff0c;实现跨平台数据库管理的无缝切换&#xff…

BigDecimal——解决Java浮点数值精度问题:快速入门与使用

在Java开发中&#xff0c;涉及金额计算、科学计数或需要高精度数值处理时&#xff0c;你是否遇到过这样的困惑&#xff1f;用double计算0.1加0.2&#xff0c;结果竟不是0.3&#xff1b;用float存储商品价格&#xff0c;小数点后两位莫名多出几位乱码&#xff1b;甚至在金融系统…

wpf之WrapPanel

前言 WrapPanel类似winform中的FlowLayoutPanel&#xff0c;采用流式布局。 1、Orientation 该属性指定WrapPanel中子空间布局的方向&#xff0c;有水平和垂直方向两种 1&#xff09;Horizontal 水平方向 子元素Button按照水平方向排列&#xff0c;如果一行排满了自动换下一…

Woody:开源Java应用性能诊断分析工具

核心价值 Woody是一款专注于Java应用性能问题诊断的工具&#xff0c;旨在帮助开发者 定位高GC频率问题&#xff0c;识别内存分配热点分析CPU使用率过高的代码路径追踪接口耗时瓶颈&#xff0c;定位内部操作耗时占比诊断锁竞争问题&#xff0c;支持精准优化针对特定业务接口/请…

《山东棒球》板球比赛规则·棒球1号位

⚾ Baseball vs Cricket 终极科普&#xff5c;规则异同发展史全解&#xff01;Hey sports babes&#xff01;别再傻傻分不清棒球⚾和板球&#xff01;全网最清晰双运动对照指南来啦&#xff5e;⚾ 棒球 Baseball&#xff5c;美式激情风暴Core Goal核心目标击球员&#xff08;Ba…

【游戏开发】Houdini相较于Blender在游戏开发上有什么优劣势?我该怎么选择开发工具?

在游戏开发中&#xff0c;Houdini与Blender的选择需结合项目规模、技术需求和团队资源综合考量。以下是两者的核心优劣势对比及决策建议&#xff1a; 一、核心优劣势对比 Houdini的优势与局限 优势&#xff1a;程序化内容生成的统治力 Houdini的节点系统&#xff08;如VEX语言、…

基于开源AI智能名片链动2+1模式S2B2C商城小程序的用户活跃度提升与价值挖掘策略研究

摘要&#xff1a;本文聚焦于在开源AI智能名片链动21模式S2B2C商城小程序环境下&#xff0c;探讨如何提高用户活跃度并挖掘用户价值。在用户留存的基础上&#xff0c;通过分析该特定模式与小程序的特点&#xff0c;提出一系列针对性的策略&#xff0c;旨在借助开源AI智能名片以及…

《投资-41》- 自然=》生物=》人类社会=》商业=》金融=》股市=》投资,其层层叠加构建中内在的相似的规律和规则

从自然到投资的层层递进中&#xff0c;尽管各领域看似差异巨大&#xff0c;但内在遵循着相似的规律和规则。这些规律体现了“底层逻辑的普适性”&#xff0c;即不同系统在动态平衡、资源分配、信息传递和反馈调节等方面具有共性。以下是关键规律的解析&#xff1a;1. 能量流动与…

VSCode中调试python脚本

VSCode中安装以下插件 ms-python.python&#xff1a;python调试ms-python.vscode-pylance&#xff1a;代码跳转&#xff08;非必要&#xff09; 配置launch.json 在当前工作区&#xff0c;按此路径.vscode\launch.json新建launch.json文件&#xff0c;并配置以下参数&#x…

动作指令活体检测通过动态交互验证真实活人,保障安全

在当今社会&#xff0c;人脸识别技术已深入日常生活的方方面面&#xff0c;从手机解锁、移动支付到远程开户、门禁考勤&#xff0c;人脸识别技术已无处不在。然而&#xff0c;这项技术也面临着严峻的安全挑战&#xff1a;打印照片、播放视频、制作3D面具等简单的“欺骗手段”都…

KingbaseES数据库:开发基础教程,从部署到安全的全方位实践

KingbaseES数据库&#xff1a;开发基础教程&#xff0c;从部署到安全的全方位实践 KingbaseES数据库&#xff1a;开发基础教程&#xff0c;从部署到安全的全方位实践&#xff0c;本文围绕 KingbaseES 数据库开发核心基础展开。先介绍三种部署模式&#xff0c;即单机、双机热备、…

安装nodejs安装node.js安装教程(Windows Linux)

文章目录Linux**一、下载 Node.js**1. **访问官网**&#xff1a;2. **选择版本**&#xff1a;**二、安装 Node.js****方法 1&#xff1a;使用包管理器&#xff08;推荐&#xff09;****Ubuntu/Debian 系统**1. **更新包列表**&#xff1a;2. **安装 Node.js**&#xff1a;3. **…

shell脚本函数介绍

1. 函数 (Functions)定义与优势函数是可重复使用的功能模块优势&#xff1a;代码复用&#xff0c;直接调用解决问题分类内置函数&#xff1a;编程语言自带的函数&#xff08;如 print&#xff09;自定义函数&#xff1a;程序员自己编写的函数定义语法# 方式一 function 函数名(…

DAY 20 奇异值SVD分解-2025.9.1

奇异值SVD分解 知识点回顾&#xff1a; 线性代数概念回顾奇异值推导奇异值的应用 a. 特征降维&#xff1a;对高维数据减小计算量、可视化 b. 数据重构&#xff1a;比如重构信号、重构图像&#xff08;可以实现有损压缩&#xff0c;k 越小压缩率越高&#xff0c;但图像质量损失…

《C++——定长内存池》

一、为什么需要内存池&#xff1f; 常规的new/delete操作存在两个主要问题&#xff1a; 性能开销大&#xff1a;每次new都需要向操作系统申请内存&#xff0c;delete需要归还给系统&#xff0c;这涉及内核态与用户态的切换&#xff0c;在高频次调用时性能损耗明显。 内存碎片&a…

【跨境电商】上中下游解释,以宠物行业为例

上中下游概念及其在宠物行业的应用 在产业链分析中&#xff0c;“上中下游”指的是一个产品或服务的不同环节&#xff1a;上游涉及原材料供应和基础资源&#xff0c;中游负责生产加工和制造&#xff0c;下游则包括销售、分销和服务。这种划分有助于理解整个价值链的运作。下面&…

飞牛NAS上部署Markdown文稿编辑器,阅读.md文件同时还可以跨平台访问!

前言前段时间小白在使用.md文件的阅读器&#xff0c;好像是什么*ypor*&#xff0c;但是这个软件它收费。&#xff08;也不是找不到PJ版本&#xff0c;只是感觉这是人家的知识产权&#xff0c;就不整了。&#xff09;于是小白在寻找能够代替这个软件的其他软件&#xff0c;而且如…

浅谈 SQL 窗口函数:ROW_NUMBER() 与聚合函数的妙用

在日常开发中&#xff0c;我们经常会遇到这样的需求&#xff1a;既要保留明细数据&#xff0c;又要对数据进行排名、累计、分区统计。如果仅依赖传统的 GROUP BY&#xff0c;往往需要做多次子查询或者复杂的 JOIN&#xff0c;既繁琐又低效。 而 窗口函数&#xff08;Window Fun…