技术宅的结界

 找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 609|回复: 0
收起左侧

【单片机】给STM32F103实现一个靠谱的并行DMA库

[复制链接]

1008

主题

2235

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
200
威望
265 点
宅币
16762 个
贡献
33386 次
宅之契约
0 份
在线时间
1598 小时
注册时间
2014-1-26
发表于 2018-3-28 12:25:09 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有帐号?立即注册→加入我们

x
原文网址:https://www.0xaa55.com/thread-16797-1-1.html
作者:0xAA55
可以转载。转载请注明出处。

所谓靠谱的并行DMA库,就是让单片机在设置完要发送的数据后,它就可以去干别的去了——想象一下你的电饭煲是DMA的话,那么你只需要把米淘好,放进电饭锅,然后插上电,按一下“煮饭”。然后你就可以去吃鸡了,不需要站在电饭煲面前干等。

STM32F10x系列单片机它有很多的外设(Peripheral),能给你提供各种各样的功能——模数转换、各种串口协议传输(UART、I2C、SPI、CAN等)、各种定时器等。这些外设你可以理解为你的“家具”,比如刚才所说的电饭煲等。

之前看了个毛子写的开源的ILI9341显示屏驱动,给STM32F10x写的,非常不清真——它使用DMA的方法就是按下“煮饭”以后就在电饭煲面前转圈圈了。当然这样的设计非常直观易懂,能让你一眼就能看明白这个显示器是怎么驱动的。至少,毛子的这个代码,比Adafruit的官方开源库好看多了。他还在这个的基础上写了个字体库和示波器实例。

毛子驱动:https://github.com/
fagcinsk
/stm-ILI9341-spi


毛子视频:https://www.youtube.com/watch?v=
5nLQ-VqMv-g


虽然很不错,但是实际的应用中我们不是更应该把STM32的性能发挥出来嘛?使用DMA还干等,那还不如直接写GPIO了。

我买到的ILI9341显示屏大概是下面网址上的这样的(类似的多得很,淘宝上肯定也有)

https://www.ebay.com/itm/2-8-TFT-LCD-Display-Touch-Panel-SPI-Serial-240-320-ILI9341-5V-3-3V-STM32-/
201950756171



s-l1600.png s-l1600-(1).png

我买到的这个它是使用SPI接口来控制的。SPI接口我就不详细介绍了。一言概括就是:带时钟的串行接口,数据的输入和输出是分开的,用一个单独的针来选片(低电平选片)。

一开始我买到这个屏幕后,我就到处找它的PDF文档,然后,我找到了——这文档与其说是文档不如说是“对那个官方的C艹库提供的补充说明”
PDF回帖后可见。
游客,如果您要查看本帖隐藏内容请回复
最后我决定以毛子代码为基准来重新造轮子了——我要让它把LCD绘图的工作都交给DMA控制器,然后处理器可以腾出手去干别的。

LCD绘图的工作,除了SPI接口交互的MOSI、MISO、SCK、CS四个针以外,ILI9341还有个“是数据还是命令”(DC,Data/Command)的针,有个背光灯的针(高电平亮灯),以及重置信号(RESET,低电平重置)的针。在发送命令和数据的时候,SPI控制器和DMA控制器只能负责把MOSI、MISO、SCK这几个针的信号给你并行处理好,而选片CS、区分数据命令的DC等,则需要手动干预

这意味着如果你只是简单地把你要发送的数据排个FIFO的队是行不通的。它没办法区分你发送的字节是数据还是命令。我的解决办法是:把每个DMA传输的请求封装为一个类似于“消息队列”一样的东西,它也是个FIFO,但我可以在开始传输请求的时候调用一个回调来完成CS、DC等pin的操作,然后在传输完毕中断里面,调用另一个回调函数来改回CS和DC等针的电平,并且从传输队列里面取出下一个传输请求。

也就是说,开始传输的时候,你只需要设置一下DMA控制器它就开始传输了——你只需要按一下“煮饭”,你的电饭煲就开始煮饭了。等到它完成了传输,它就会产生一个特定的中断信号,中断处理程序就会启动下一个传输请求——电饭煲煮好饭的时候会发出声音通知你,然后你就可以把米饭拿出来,再把煮粥的材料倒进电饭煲里,按下“煮粥”按钮了……

虽然传输的结束和开始依然需要处理器的干预,但传输的过程是不需要干预的了。这样就达到了我的目的——使用一个电饭煲煮各种食物的同时,不影响我干别的事。
[C] 纯文本查看 复制代码
#ifndef	_DMA_BUFFERED_
#define _DMA_BUFFERED_

#include<inttypes.h>
#include<stm32f10x_dma.h>

#include"dma_queue_cfg.h"
#include"delay.h"

typedef struct dma_ctrl_struct
{
	volatile data_t data; // 数据,或者数据的地址
	volatile uint16_t count; // 传输的数量
	volatile uint8_t repeat_count; // 重复的次数
	volatile uint8_t flags; // 控制属性
}dma_ctrl_t, *dma_ctrl_p;

// DCF开头的宏就是控制属性了

// 传输单元位数
#define DCF_BITS_8          0x0000 /* default */
#define DCF_BITS_16         0x0001
#define DCF_BITS_32         0x0002

#define DCF_BITS_FLAGS      (DCF_BITS_8 | DCF_BITS_16 | DCF_BITS_32)

// 指针行为(增大,或者不变)
#define DCF_PTR_INC         0x0000 /* default */
#define DCF_PTR_STAY        0x0004

#define DCF_PTR_FLAGS       (DCF_PTR_INC | DCF_PTR_STAY)

// 数据的存储方式(指针,或者直接装填)
#define DCF_DATA_PTR        0x0000 /* default */
#define DCF_DATA_INLINED    0x0008

#define DCF_DATA_FLAGS      (DCF_DATA_PTR | DCF_DATA_INLINED)

// 收,还是发
#define DCF_DIR_TX          0x0000 /* default */
#define DCF_DIR_RX          0x0010

#define DCF_DIR_FLAGS       (DCF_DIR_TX | DCF_DIR_RX)

// 给调用者预留的Flag,供调用者在on_start和on_finish里面判断。
#define DCF_CUSTOM_FLAG     0x0080

typedef struct dma_queue_struct dma_queue_t, *dma_queue_p;

// 回调函数
typedef void(*on_start_f)(dma_queue_p pq, uint32_t index);
typedef void(*on_finish_f)(dma_queue_p pq, uint32_t index);

// DMA队列对象
struct dma_queue_struct
{
	DMA_Channel_TypeDef *channel; // DMA控制器寄存器地址
	volatile void *periph_addr; // 要操作的外设的地址
	
	int dma_controller; // DMA控制器号,1或2
	int channel_num; // DMA通道号,1,2,3,4,5,6,7
	uint32_t flags; // 状态控制
	
	on_start_f on_start; // 开始传输时被调用的回调函数
	on_finish_f on_finish; // 传输结束后被调用的回调函数
	
	volatile uint32_t dma_ctrl_current; // 正在传输的DMA请求
	volatile uint32_t dma_ctrl_count; // 队列里的请求个数
	volatile uint32_t dma_ctrl_inprogress; // 是否正在传输
	dma_ctrl_t dma_ctrl[DMA_QUEUE_LENGTH]; // 队列
};

// 按要求定义全局变量
#if DMA1_1_QUEUE
extern dma_queue_t g_dma1_1_queue;
#endif
#if DMA1_2_QUEUE
extern dma_queue_t g_dma1_2_queue;
#endif
#if DMA1_3_QUEUE
extern dma_queue_t g_dma1_3_queue;
#endif
#if DMA1_4_QUEUE
extern dma_queue_t g_dma1_4_queue;
#endif
#if DMA1_5_QUEUE
extern dma_queue_t g_dma1_5_queue;
#endif
#if DMA1_6_QUEUE
extern dma_queue_t g_dma1_6_queue;
#endif
#if DMA1_7_QUEUE
extern dma_queue_t g_dma1_7_queue;
#endif

// 初始化一个DMA队列对象,返回上述的全局变量中的一个(根据前两个参数匹配)
dma_queue_p dma_queue_init
(
	int dma_controller, // DMA控制器号,1或2
	int channel_num, // DMA通道号,1,2,3,4,5,6,7
	volatile void *periph_addr, // 要操作的外设的地址
	on_start_f on_start, // 开始传输时被调用的回调函数
	on_finish_f on_finish, // 传输结束后被调用的回调函数
	uint32_t DMA_Priority // DMA传输优先级
);

// 压入一个DMA传输请求到队列里,返回它在队列中的位置
uint32_t dma_queue_push
(
	dma_queue_p dma_queue, // DMA队列对象
	data_t data, // 数据,或者数据的地址
	uint16_t count, // 传输的数量
	uint8_t repeat_count, // 重复的次数
	uint8_t flags // 控制属性
);

// 等待特定的传输请求完成
void dma_queue_wait_transfer(dma_queue_p dma_queue, uint32_t index);

// 等待全部传输请求完成
void dma_queue_wait(dma_queue_p dma_queue);

#endif
设计接口挺麻烦的。尤其是在单片机上,RAM非常有限,稍有不慎就爆了。没有内存管理,虽然你可以自己造一个,但动态管理内存的意义并不大。

当你的代码里出现数据段的时候,数据段里面的数据是跟着你的.text走的,而.text是存储在ROM里面的,只读。如果你对这部分区域的内存执行了写入操作,你的单片机就会Halt停机。你可以在ST-LINK Utility里面看到它停机时的PC计数器位置。

任何全局变量,或者静态的局部变量,写的时候你要是顺手加了个初始化,那就
Boom
了。你可以初始化非静态的局部变量,因为它在栈上。然后,任何没有被初始化过的全局变量和静态局部变量,最终都会进到.bss段内。

但是你的单片机程序在启动的时候,你得手动把.bss段的内存清零。

而且还有很多坑爹的玩意儿需要你去体验——它给你带的那些头文件和驱动库,里面有各种好玩的。五花八门的命名规则,钦定结构体,钦定地址,钦定外设寄存器,钦定只读中断表——中断表的每个表项的名字都是钦定的。

还有各种意义不明的宏——有的你只能靠翻PDF找出它的用途,有的你只能靠看头文件来理解它的用途,还有的你只能谷歌。

很多我们在PC开发上讨论出的各种“优良习惯”在单片机上都不适用。

dma_queue_struct这个结构体,就是用来维护一个DMA传输队列用的结构体了。它的dma_ctrl数组,就是DMA请求队列的FIFO了。我在dma_queue_cfg.h里面定义了宏用于配置它。extern dma_queue_t g_dma
x
_
y
_queue;就是这个结构体的实例化变量了。因为中断处理函数的名字是钦定的,所以我要给每一个需要用到的DMA通道都定义一个专属的全局变量。然后每个变量的体积都不小,所以用宏来配置它,只定义用得到的。免得占用RAM过多。

dma_queue_push()用来提交你的DMA传输请求。它的第一个参数,就是传递的上述结构体的地址。说到地址,这里就要提到数据的有效性。这个和我们在PC上进行多线程开发的情况很接近。在没有电饭煲的时候,我们煮饭用的是柴,我们把柴砍下来后,像打桩一样插在地上,钻木取火,引燃枯树叶,然后点燃木桩。等到木桩烧起来了,把锅子架上去。大功告成!玩泥巴去了。然后回来的时候,柴早就被别人拿走了,釜底抽薪。这是因为我们的DMA队列它的设计结构是队列方式,你提交的传输请求并不会被立即处理——它要等到前面的所有请求都处理了之后才会轮到你现在提交的请求,而这个时候你早就玩泥巴去了,栈上的数据都“物是人非”了。所以我们不能用栈来存储不会被立即引用的数据。

我的设计是:对于少量的数据(比如一条用于控制显示器的命令等),直接把它存储在每个DMA请求的data字段里,然后用DCF_DATA_INLINED来标识。而对于较大的数据的话,则另想办法。至少,我们不至于为了那几个字节的控制命令而浪费有限的RAM。用柴火煮饭的话好歹刨个坑,然后把柴火放坑里。
[C] 纯文本查看 复制代码
#include"dma_queue.h"

#define CCR_CLEAR_Mask ((uint32_t)0xFFFF800F)

// 按要求定义全局变量
#if DMA1_1_QUEUE
dma_queue_t g_dma1_1_queue;
#endif
#if DMA1_2_QUEUE
dma_queue_t g_dma1_2_queue;
#endif
#if DMA1_3_QUEUE
dma_queue_t g_dma1_3_queue;
#endif
#if DMA1_4_QUEUE
dma_queue_t g_dma1_4_queue;
#endif
#if DMA1_5_QUEUE
dma_queue_t g_dma1_5_queue;
#endif
#if DMA1_6_QUEUE
dma_queue_t g_dma1_6_queue;
#endif
#if DMA1_7_QUEUE
dma_queue_t g_dma1_7_queue;
#endif

// 开始传输。
static void _start_transfer(dma_queue_p dma_queue)
{
	uint32_t tmpreg;
	uint32_t addr = 0;
	dma_ctrl_p pdc;
	DMA_Channel_TypeDef *channel = dma_queue->channel;
	
	pdc = &dma_queue->dma_ctrl[dma_queue->dma_ctrl_current];
	tmpreg = channel->CCR & CCR_CLEAR_Mask;
	
	// 设置状态
	dma_queue->dma_ctrl_inprogress = 1;
	
	// 调用回调
	dma_queue->on_start(dma_queue, dma_queue->dma_ctrl_current);
	
	// 分析位数
	if((pdc->flags & DCF_BITS_FLAGS) == DCF_BITS_8)
	{
		tmpreg |= DMA_MemoryDataSize_Byte | DMA_PeripheralDataSize_Byte;
	}
	
	if((pdc->flags & DCF_BITS_FLAGS) == DCF_BITS_16)
	{
		tmpreg |= DMA_MemoryDataSize_HalfWord | DMA_PeripheralDataSize_HalfWord;
	}
	
	if((pdc->flags & DCF_BITS_FLAGS) == DCF_BITS_32)
	{
		tmpreg |= DMA_MemoryDataSize_Word | DMA_PeripheralDataSize_Word;
	}
	
	// 分析指针行为
	if((pdc->flags & DCF_PTR_FLAGS) == DCF_PTR_INC)
	{
		tmpreg |= DMA_MemoryInc_Enable;
	}
	
	if((pdc->flags & DCF_PTR_FLAGS) == DCF_PTR_STAY)
	{
		tmpreg |= DMA_MemoryInc_Disable;
	}
	
	// 数据是指针
	if((pdc->flags & DCF_DATA_FLAGS) == DCF_DATA_PTR)
	{
		addr = pdc->data;
	}
	
	// 数据是数据
	if((pdc->flags & DCF_DATA_FLAGS) == DCF_DATA_INLINED)
	{
		addr = (uint32_t)&(pdc->data); // 直到传输结束前,这个地址上的数据都不会变
	}
	
	// 收发行为
	if((pdc->flags & DCF_DIR_FLAGS) == DCF_DIR_TX)
	{
		tmpreg |= DMA_DIR_PeripheralDST; // 内存到外设
	}
	
	if((pdc->flags & DCF_DIR_FLAGS) == DCF_DIR_RX)
	{
		tmpreg |= DMA_DIR_PeripheralSRC; // 外设到内存
	}
	
	// 其它控制位
	tmpreg |= dma_queue->flags;
	
	channel->CCR = tmpreg; // 设置DMA通道寄存器(并且暂停传输,如果旧的传输在进行的话)
	channel->CNDTR = pdc->count; // 个数
	channel->CPAR = (uint32_t)dma_queue->periph_addr; // 外设地址
	channel->CMAR = addr; // 数据地址
	
	channel->CCR |= DMA_CCR1_EN; // 开启传输
}

// 中断回调
static void _on_irq(dma_queue_p dma_queue, const int dma_controller, const int channel_num)
{
	dma_ctrl_p pdc;
	DMA_TypeDef *dma;
	uint32_t TC_Flag;
	
	//if(dma_controller != dma_queue->dma_controller || channel_num != dma_queue->channel_num)
	//	return;
	
	if(dma_controller == 1)
	{
		dma = DMA1;
		TC_Flag = 0x00000000;
	}
	else if(dma_controller == 2)
	{
		dma = DMA2;
		TC_Flag = 0x10000000;
	}
	//else
	//{
	//	return;
	//}
	
	TC_Flag |= DMA_IT_TC << ((channel_num - 1) * 4);
	
	// 判断是否自己对应的中断
	if((dma->ISR & TC_Flag) == TC_Flag)
	{
		// 停止传输
		dma_queue->channel->CCR &= (uint16_t)~DMA_CCR1_EN;
		
		// 清除中断位
		dma->IFCR = TC_Flag;
	}
	else
	{
		return;
	}
	
	// 这是DMA传输完成时的中断,因此我们要开始下一轮传输
	__disable_irq();
	pdc = &dma_queue->dma_ctrl[dma_queue->dma_ctrl_current];
	
	if(pdc->repeat_count) // 如果它有重复次数,那就尽量以最快速度重复这次传输
	{
		pdc->repeat_count --;
		dma_queue->channel->CNDTR = pdc->count; // 必须设置计数器
		dma_queue->channel->CCR |= DMA_CCR1_EN; // 别的寄存器不用动。开始传输
	}
	else
	{
		dma_queue->dma_ctrl_inprogress = 0; // 完成了传输
		dma_queue->on_finish(dma_queue, dma_queue->dma_ctrl_current); // 完成传输的回调
		dma_queue->dma_ctrl_count --; // 队列请求计数-1
		dma_queue->dma_ctrl_current = (dma_queue->dma_ctrl_current + 1) % DMA_QUEUE_LENGTH; // 下一个请求
		
		if(dma_queue->dma_ctrl_count) // 依然有剩余请求,则开始传输
			_start_transfer(dma_queue);
	}
	__enable_irq();
}

// 初始化一个DMA队列对象,返回上述的全局变量中的一个(根据前两个参数匹配)
dma_queue_p dma_queue_init
(
	int dma_controller, // DMA控制器号,1或2
	int channel_num, // DMA通道号,1,2,3,4,5,6,7
	volatile void *periph_addr, // 要操作的外设的地址
	on_start_f on_start, // 开始传输时被调用的回调函数
	on_finish_f on_finish, // 传输结束后被调用的回调函数
	uint32_t DMA_Priority // DMA传输优先级
)
{
	dma_queue_p dma_queue;
	if(dma_controller == 1)
	{
		RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
		switch(channel_num)
		{
#if DMA1_1_QUEUE
		case 1:
			dma_queue = &g_dma1_1_queue;
			dma_queue->channel = DMA1_Channel1;
			NVIC_EnableIRQ(DMA1_Channel1_IRQn);
			break;
#endif
#if DMA1_2_QUEUE
		case 2:
			dma_queue = &g_dma1_2_queue;
			dma_queue->channel = DMA1_Channel2;
			NVIC_EnableIRQ(DMA1_Channel2_IRQn);
			break;
#endif
#if DMA1_3_QUEUE
		case 3:
			dma_queue = &g_dma1_3_queue;
			dma_queue->channel = DMA1_Channel3;
			NVIC_EnableIRQ(DMA1_Channel3_IRQn);
			break;
#endif
#if DMA1_4_QUEUE
		case 4:
			dma_queue = &g_dma1_4_queue;
			dma_queue->channel = DMA1_Channel4;
			NVIC_EnableIRQ(DMA1_Channel4_IRQn);
			break;
#endif
#if DMA1_5_QUEUE
		case 5:
			dma_queue = &g_dma1_5_queue;
			dma_queue->channel = DMA1_Channel5;
			NVIC_EnableIRQ(DMA1_Channel5_IRQn);
			break;
#endif
#if DMA1_6_QUEUE
		case 6:
			dma_queue = &g_dma1_6_queue;
			dma_queue->channel = DMA1_Channel6;
			NVIC_EnableIRQ(DMA1_Channel6_IRQn);
			break;
#endif
#if DMA1_7_QUEUE
		case 7:
			dma_queue = &g_dma1_7_queue;
			dma_queue->channel = DMA1_Channel7;
			NVIC_EnableIRQ(DMA1_Channel7_IRQn);
			break;
#endif
		default:
			DMA_QUEUE_BKPT();
			return 0;
		}
	}
	else
	{
		// 暂时懒得处理DMA2
		DMA_QUEUE_BKPT();
		return 0;
	}
	
	dma_queue->dma_controller = dma_controller;
	dma_queue->channel_num = channel_num;
	dma_queue->periph_addr = periph_addr;
	dma_queue->flags = DMA_Priority | // 优先级是参数
		// 下面的才是宏
		DMA_Mode_Normal | DMA_PeripheralInc_Disable | DMA_M2M_Disable | DMA_IT_TC;
	dma_queue->on_start = on_start;
	dma_queue->on_finish = on_finish;
	dma_queue->dma_ctrl_current = 0;
	dma_queue->dma_ctrl_count = 0;
	dma_queue->dma_ctrl_inprogress = 0;
	return dma_queue;
}

// 压入一个DMA传输请求到队列里,返回它在队列中的位置
uint32_t dma_queue_push
(
	dma_queue_p dma_queue, // DMA队列对象
	data_t data, // 数据,或者数据的地址
	uint16_t count, // 传输的数量
	uint8_t repeat_count, // 重复的次数
	uint8_t flags // 控制属性
)
{
	uint32_t cur;
	
	while(1)
	{
		__disable_irq();
		// 防止队列溢出
		if(dma_queue->dma_ctrl_count >= DMA_QUEUE_LENGTH)
		{
			// 如果队列是满的,但并没有传输的话,开启传输
			if(!dma_queue->dma_ctrl_inprogress)
				_start_transfer(dma_queue);
		}
		else
		{
			// 插入传输请求到队列里
			cur = (dma_queue->dma_ctrl_current + dma_queue->dma_ctrl_count) % DMA_QUEUE_LENGTH;
			dma_queue->dma_ctrl[cur].data = data;
			dma_queue->dma_ctrl[cur].count = count;
			dma_queue->dma_ctrl[cur].repeat_count = repeat_count;
			dma_queue->dma_ctrl[cur].flags = flags;
			dma_queue->dma_ctrl_count ++;
			
			// 如果之前队列是空的,现在插入了传输请求,那就开启传输
			if(dma_queue->dma_ctrl_count == 1)
				_start_transfer(dma_queue);
			__enable_irq();
			break; // 并且退出
		}
		__enable_irq();
		
		// 这个循环是为了等待队列空出空位
	}
	
	return cur;
}

// 等待特定的传输请求完成
void dma_queue_wait_transfer(dma_queue_p dma_queue, uint32_t index)
{
	index %= DMA_QUEUE_LENGTH;
	while(dma_queue->dma_ctrl_current != index)
	{
		if(!dma_queue->dma_ctrl_inprogress) break;
	}
}

// 等待全部传输请求完成
void dma_queue_wait(dma_queue_p dma_queue)
{
	while(dma_queue->dma_ctrl_count);
}

// 用宏来生成函数名
#define DMAx_Channely_IRQHandler(x,y) \
	void DMA ## x ## _Channel ## y ## _IRQHandler(void) \
	{ \
		_on_irq(&g_dma ## x ## _ ## y ## _queue, x, y); \
	}

#if DMA1_1_QUEUE
DMAx_Channely_IRQHandler(1,1)
#endif

#if DMA1_2_QUEUE
DMAx_Channely_IRQHandler(1,2)
#endif

#if DMA1_3_QUEUE
DMAx_Channely_IRQHandler(1,3)
#endif

#if DMA1_4_QUEUE
DMAx_Channely_IRQHandler(1,4)
#endif

#if DMA1_5_QUEUE
DMAx_Channely_IRQHandler(1,5)
#endif

#if DMA1_6_QUEUE
DMAx_Channely_IRQHandler(1,6)
#endif

#if DMA1_7_QUEUE
DMAx_Channely_IRQHandler(1,7)
#endif
其实刚开始写的时候并没有什么合适的方法调试。除了走ST-Util跑单步以外(遇到delay_ms的时候蛋疼死),我并没有示波器(我正打算自己做一个呢),SWO调试貌似需要拆开我的ST-LINK然后飞个线。

在PC上模拟运行的话,并不靠谱。单片机情况本来就比PC复杂。其实最简单(简陋)的调试方法就是配合断点+LED灯,它板子上有个LED,控制它的亮灭来判断代码的执行状况。

我其实写了两个DMA的库,接口一样。用来调试屏幕交互的部分。只不过另一个是“干等”系列。使用干等可以保证屏幕交互这块儿的协议没有问题,但队列方式则需要考虑数据的位置和保留方式等,依然需要单独测试。

单片机控制LCD绘图的方式很像远古时代8086处理器控制显示卡进行光栅操作的绘图。你向ILI9341传输的东西,可以是命令,也可以是数据。传输命令的过程相当于写它的寄存器,然后传输数据则相当于给上一个命令提供参数。绘图是怎么完成的呢?核心就两个命令:设置寻址区域,写显存。

ILI9341的像素格式是RGB各6bit的18bit颜色,但你可以让它进入“16bit颜色传输模式”,此时你就可以传输RGB565这种16bit格式的颜色过去了。它会把你传入的16bit颜色数据扩成18bit颜色数据,然后再显示。

它的分辨率是240x320(也说320x240,看方向。默认是竖着的方向),总共有76800个像素——光是像素的个数,就超过了STM32F103
C8T6
的内存字节数(20480字节)。所以在单片机的内存里存储屏幕图像的做法并不现实,因为你存不下。

所以绘图都是靠的直接写屏。也就是设置屏幕的寻址区域,然后输出数据到显存里。就像当年x86电脑的内存也很小的时候,在Win
do
ws上通过调用GDI的绘图函数来绘制窗体。而不是像现在这样可以随便加载jpg、png、bmp、
gif
等各种五花八门的图像存储格式到内存里(jpg、png、
gif
等这些格式最终都要解压成bmp才能使用),然后用各种操作像素的方式来绘图。

理解了这个,你就更容易能理解GDI的设计初衷了。
[C] 纯文本查看 复制代码
#ifndef	_ILI9341_LCD_
#define _ILI9341_LCD_

#include<inttypes.h>
#include"config.h"

typedef enum lcd_rgb_format_enum
{
	lcdf_rgb565 = 16,
	lcdf_rgb666 = 18
}lcd_rgb_format_t, *lcd_rgb_format_p;

// 屏幕分辨率
extern int g_lcd_width;
extern int g_lcd_height;
extern const uint32_t g_lcd_pixels;
extern lcd_rgb_format_t g_lcd_rgb_format; // 传输的颜色格式

#define RGB565(r,g,b) (((((uint32_t)(b) >> 3) & 0x001F) | \
					    (((uint32_t)(g) << 3) & 0x07E0) | \
					    (((uint32_t)(r) << 8) & 0xf800)) & 0x0FFFF)

#define RGB666(r,g,b) ((((uint32_t)(r) <<  0) & 0x0000FC) | \
					   (((uint32_t)(g) <<  8) & 0x00FC00) | \
					   (((uint32_t)(b) << 16) & 0xFC0000))
					   
#define RGB(r,g,b) (g_lcd_rgb_format == lcdf_rgb666 ? RGB666(r, g, b) : RGB565(r, g, b))

// 初始化、重置
void lcd_init();
void lcd_reset();

// 设置方向
void lcd_set_landscape(const int mirror);
void lcd_set_portrait(const int mirror);

// 设置传输的颜色格式
void lcd_set_rgb16();
void lcd_set_rgb18();

// 设置绘图区域
void lcd_set_draw_area(const int x1, const int y1, const int x2, const int y2);

// 从绘图区域里面取出像素数据
void lcd_get_pixels(void *pixels, const uint32_t count);

// 写入像素数据到绘图区域里
void lcd_put_pixels(const void *pixels, const uint32_t count);

// 写入纯色到绘图区域里
void lcd_put_pure_color(const uint32_t color, const uint32_t count);

// 用一个颜色填充全屏
void lcd_fill_screen(const uint32_t color);

// 填充区域(根据左、上、右、下确定范围)
void lcd_fill_area(int x1, int y1, int x2, int y2, const uint32_t color);

// 填充矩形(根据左、上、宽、高确定范围)
void lcd_fill_rect(int x, int y, int w, int h, const uint32_t color);

// 等待绘图完成
void lcd_flush();

#endif
这是最基本的接口。在不考虑其它功能比如休眠模式(实际应用是必须考虑的,因为显示屏很耗电)、伽玛度调节、亮度对比度、关屏序列以外,与像素操作关系最密切的就是上面的函数了。

其它的任何绘图方式,比如画线、画圆、显示文字、画各种几何图形等,都是用上面这些函数实现的。

底层上,这些函数就是靠调用DMA把命令和命令参数都传输过去。当你实现了自己的画线的函数以后,假设你要画一条斜线,它其实是靠画多个横线或者竖线来完成的。而画横线和竖线则是靠填充区域完成的。

tri.png
bigtri.png

想象一下,如果你要画一条斜线,你需要一边计算每个小的直线的线段位置和长度,一边把绘图的请求发给屏幕。这个过程如果能并行处理的话,你可以在很短的时间里就画好一条直线。
[C] 纯文本查看 复制代码
#include"lcd.h"
#include"delay.h"
#include"debug.h"
#include"dma_queue.h"

#include<stm32f10x_dma.h>
#include<stm32f10x_spi.h>
#include<system_stm32f10x.h>

/* Level 1 Commands */
#define LCD_SWRESET             0x01   /* Software Reset */
#define LCD_READ_DISPLAY_ID     0x04   /* Read display identification information */
#define LCD_RDDST               0x09   /* Read Display Status */
#define LCD_RDDPM               0x0A   /* Read Display Power Mode */
#define LCD_RDDMADCTL           0x0B   /* Read Display MADCTL */
#define LCD_RDDCOLMOD           0x0C   /* Read Display Pixel Format */
#define LCD_RDDIM               0x0D   /* Read Display Image Format */
#define LCD_RDDSM               0x0E   /* Read Display Signal Mode */
#define LCD_RDDSDR              0x0F   /* Read Display Self-Diagnostic Result */
#define LCD_SPLIN               0x10   /* Enter Sleep Mode */
#define LCD_SLEEP_OUT           0x11   /* Sleep out register */
#define LCD_PTLON               0x12   /* Partial Mode ON */
#define LCD_NORMAL_MODE_ON      0x13   /* Normal Display Mode ON */
#define LCD_DINVOFF             0x20   /* Display Inversion OFF */
#define LCD_DINVON              0x21   /* Display Inversion ON */
#define LCD_GAMMA               0x26   /* Gamma register */
#define LCD_DISPLAY_OFF         0x28   /* Display off register */
#define LCD_DISPLAY_ON          0x29   /* Display on register */
#define LCD_COLUMN_ADDR         0x2A   /* Colomn address register */
#define LCD_PAGE_ADDR           0x2B   /* Page address register */
#define LCD_GRAM                0x2C   /* GRAM register */
#define LCD_RGBSET              0x2D   /* Color SET */
#define LCD_RAMRD               0x2E   /* Memory Read */
#define LCD_PLTAR               0x30   /* Partial Area */
#define LCD_VSCRDEF             0x33   /* Vertical Scrolling Definition */
#define LCD_TEOFF               0x34   /* Tearing Effect Line OFF */
#define LCD_TEON                0x35   /* Tearing Effect Line ON */
#define LCD_MAC                 0x36   /* Memory Access Control register*/
#define LCD_VSCRSADD            0x37   /* Vertical Scrolling Start Address */
#define LCD_IDMOFF              0x38   /* Idle Mode OFF */
#define LCD_IDMON               0x39   /* Idle Mode ON */
#define LCD_PIXEL_FORMAT        0x3A   /* Pixel Format register */
#define LCD_WRITE_MEM_CONTINUE  0x3C   /* Write Memory Continue */
#define LCD_READ_MEM_CONTINUE   0x3E   /* Read Memory Continue */
#define LCD_SET_TEAR_SCANLINE   0x44   /* Set Tear Scanline */
#define LCD_GET_SCANLINE        0x45   /* Get Scanline */
#define LCD_WDB                 0x51   /* Write Brightness Display register */
#define LCD_RDDISBV             0x52   /* Read Display Brightness */
#define LCD_WCD                 0x53   /* Write Control Display register*/
#define LCD_RDCTRLD             0x54   /* Read CTRL Display */
#define LCD_WRCABC              0x55   /* Write Content Adaptive Brightness Control */
#define LCD_RDCABC              0x56   /* Read Content Adaptive Brightness Control */
#define LCD_WRITE_CABC          0x5E   /* Write CABC Minimum Brightness */
#define LCD_READ_CABC           0x5F   /* Read CABC Minimum Brightness */
#define LCD_READ_ID1            0xDA   /* Read ID1 */
#define LCD_READ_ID2            0xDB   /* Read ID2 */
#define LCD_READ_ID3            0xDC   /* Read ID3 */

/* Level 2 Commands */
#define LCD_RGB_INTERFACE       0xB0   /* RGB Interface Signal Control */
#define LCD_FRMCTR1             0xB1   /* Frame Rate Control (In Normal Mode) */
#define LCD_FRMCTR2             0xB2   /* Frame Rate Control (In Idle Mode) */
#define LCD_FRMCTR3             0xB3   /* Frame Rate Control (In Partial Mode) */
#define LCD_INVTR               0xB4   /* Display Inversion Control */
#define LCD_BPC                 0xB5   /* Blanking Porch Control register */
#define LCD_DFC                 0xB6   /* Display Function Control register */
#define LCD_ETMOD               0xB7   /* Entry Mode Set */
#define LCD_BACKLIGHT1          0xB8   /* Backlight Control 1 */
#define LCD_BACKLIGHT2          0xB9   /* Backlight Control 2 */
#define LCD_BACKLIGHT3          0xBA   /* Backlight Control 3 */
#define LCD_BACKLIGHT4          0xBB   /* Backlight Control 4 */
#define LCD_BACKLIGHT5          0xBC   /* Backlight Control 5 */
#define LCD_BACKLIGHT7          0xBE   /* Backlight Control 7 */
#define LCD_BACKLIGHT8          0xBF   /* Backlight Control 8 */
#define LCD_POWER1              0xC0   /* Power Control 1 register */
#define LCD_POWER2              0xC1   /* Power Control 2 register */
#define LCD_VCOM1               0xC5   /* VCOM Control 1 register */
#define LCD_VCOM2               0xC7   /* VCOM Control 2 register */
#define LCD_NVMWR               0xD0   /* NV Memory Write */
#define LCD_NVMPKEY             0xD1   /* NV Memory Protection Key */
#define LCD_RDNVM               0xD2   /* NV Memory Status Read */
#define LCD_READ_ID4            0xD3   /* Read ID4 */
#define LCD_PGAMMA              0xE0   /* Positive Gamma Correction register */
#define LCD_NGAMMA              0xE1   /* Negative Gamma Correction register */
#define LCD_DGAMCTRL1           0xE2   /* Digital Gamma Control 1 */
#define LCD_DGAMCTRL2           0xE3   /* Digital Gamma Control 2 */
#define LCD_INTERFACE           0xF6   /* Interface control register */

/* Extend register commands */
#define LCD_POWERA              0xCB   /* Power control A register */
#define LCD_POWERB              0xCF   /* Power control B register */
#define LCD_DTCA                0xE8   /* Driver timing control A */
#define LCD_DTCB                0xEA   /* Driver timing control B */
#define LCD_POWER_SEQ           0xED   /* Power on sequence register */
#define LCD_3GAMMA_EN           0xF2   /* 3 Gamma enable register */
#define LCD_PRC                 0xF7   /* Pump ratio control register */

#define ORIENTATION_PORTRAIT 0x48
#define ORIENTATION_LANDSCAPE 0x28
#define ORIENTATION_PORTRAIT_MIRROR 0x88
#define ORIENTATION_LANDSCAPE_MIRROR 0xE8

#define LCD_PIXEL_WIDTH		240
#define LCD_PIXEL_HEIGHT	320
#define LCD_PIXEL_COUNT		LCD_PIXEL_WIDTH * LCD_PIXEL_HEIGHT

#define MAKE_16BIT(l,h) ((uint16_t)(l) | ((uint16_t)(h) << 8))
#define MAKE_PARAM(l,h) (((uint32_t)(l) & 0x0000FFFF) | ((uint32_t)(h) << 16))

// 屏幕分辨率
int g_lcd_width;
int g_lcd_height;
const uint32_t g_lcd_pixels = LCD_PIXEL_COUNT;
lcd_rgb_format_t g_lcd_rgb_format;

// 初始化命令
static const uint8_t _init_commands[] =
{
	// Power control A
	6, LCD_POWERA, 0x39, 0x2C, 0x00, 0x34, 0x02,
	// Power control B
	4, LCD_POWERB, 0x00, 0xC1, 0x30,
	// Driver timing control A
	4, LCD_DTCA, 0x85, 0x00, 0x78,
	// Driver timing control B
	3, LCD_DTCB, 0x00, 0x00,
	// Power on sequence control
	5, LCD_POWER_SEQ, 0x64, 0x03, 0x12, 0x81,
	// Pump ratio control
	2, LCD_PRC, 0x20,
	// Power control 1
	2, LCD_POWER1, 0x23,
	// Power control 2
	2, LCD_POWER2, 0x10,
	// VCOM control 1
	3, LCD_VCOM1, 0x3E, 0x28,
	// VCOM cotnrol 2
	2, LCD_VCOM2, 0x86,
	// Memory access control
	2, LCD_MAC, 0x48,
	// Pixel format set
	2, LCD_PIXEL_FORMAT, 0x55,
	// Frame rate control
	3, LCD_FRMCTR1, 0x00, 0x18,
	// Display function control
	4, LCD_DFC, 0x08, 0x82, 0x27,
	// 3Gamma function disable
	2, LCD_3GAMMA_EN, 0x00,
	// Gamma curve selected
	2, LCD_GAMMA, 0x01,
	// Set positive gamma
	16, LCD_PGAMMA, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00,
	16, LCD_NGAMMA, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F,
	0
};

// 开始传输的回调
static void _on_start(dma_queue_p pq, uint32_t index)
{
	TFT_GPIO->BRR = TFT_CS_PIN;
	if((pq->dma_ctrl[index].flags & DCF_CUSTOM_FLAG) == DCF_CUSTOM_FLAG)
	{
		TFT_GPIO->BSRR = TFT_DC_PIN;
	}
	if((pq->dma_ctrl[index].flags & DCF_BITS_FLAGS) == DCF_BITS_16)
	{
		SPI_MASTER->CR1 &= ~SPI_CR1_SPE; // DISABLE SPI
		SPI_MASTER->CR1 |= SPI_CR1_DFF;  // SPI 16
		SPI_MASTER->CR1 |= SPI_CR1_SPE;  // ENABLE SPI
	}
	else
	{
		SPI_MASTER->CR1 &= ~SPI_CR1_SPE; // DISABLE SPI
		SPI_MASTER->CR1 &= ~SPI_CR1_DFF; // SPI 8
		SPI_MASTER->CR1 |= SPI_CR1_SPE;  // ENABLE SPI
	}
#if LCD_CMDBUF_LED // 调试:传输时亮灯
	LCD_CMDBUF_LED_GPIO->BRR = LCD_CMDBUF_LED_PIN;
#endif
}

// 传输结束的回调
static void _on_finish(dma_queue_p pq, uint32_t index)
{
	if((pq->dma_ctrl[index].flags & DCF_CUSTOM_FLAG) == DCF_CUSTOM_FLAG)
	{
		TFT_GPIO->BRR = TFT_DC_PIN;
	}
	TFT_GPIO->BSRR = TFT_CS_PIN;
#if LCD_CMDBUF_LED // 调试:传输结束时熄灯
	LCD_CMDBUF_LED_GPIO->BSRR = LCD_CMDBUF_LED_PIN;
#endif
}

// 配置SPI与DMA协作
static void _spi_dma_config()
{
    SPI_I2S_DMACmd(SPI_MASTER, SPI_I2S_DMAReq_Tx, ENABLE);
    SPI_I2S_DMACmd(SPI_MASTER, SPI_I2S_DMAReq_Rx, ENABLE);
	
	// 初始化DMA传输队列
	dma_queue_init(1, 2, &(SPI_MASTER->DR), _on_start, _on_finish, DMA_Priority_Medium);
	dma_queue_init(1, 3, &(SPI_MASTER->DR), _on_start, _on_finish, DMA_Priority_Medium);
}

// 配置SPI
static void _spi_config()
{
	SPI_InitTypeDef spi_conf;
	
    RCC_APB2PeriphClockCmd(SPI_MASTER_CLK, ENABLE);
	
    SPI_StructInit(&spi_conf);
    spi_conf.SPI_Mode              = SPI_Mode_Master;
    spi_conf.SPI_NSS               = SPI_NSS_Soft;
    spi_conf.SPI_CPOL              = SPI_CPOL_High;
    spi_conf.SPI_CPHA              = SPI_CPHA_2Edge;
    spi_conf.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
    SPI_Init(SPI_MASTER, &spi_conf);

    SPI_Cmd(SPI_MASTER, ENABLE);
}

// 配置GPIO
static void _spi_gpio_config()
{
	GPIO_InitTypeDef gpio_conf;

    RCC_PCLK2Config(RCC_HCLK_Div2);
    RCC_APB2PeriphClockCmd(SPI_MASTER_GPIO_CLK, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2ENR_AFIOEN, ENABLE);

    // GPIO speed by default
    gpio_conf.GPIO_Speed = GPIO_Speed_50MHz;

    // GPIO for CS/DC/LED/RESET
    gpio_conf.GPIO_Pin  = TFT_CS_PIN | TFT_DC_PIN | TFT_RESET_PIN | TFT_LED_PIN;
    gpio_conf.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(GPIOA, &gpio_conf);

    // GPIO for SPI
    gpio_conf.GPIO_Pin  = SPI_MASTER_PIN_SCK | SPI_MASTER_PIN_MOSI;
    gpio_conf.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(SPI_MASTER_GPIO, &gpio_conf);

    // GPIO for SPI
    gpio_conf.GPIO_Pin  = SPI_MASTER_PIN_MISO;
    gpio_conf.GPIO_Mode = GPIO_Mode_IPD;
    GPIO_Init(SPI_MASTER_GPIO, &gpio_conf);
}

// 等待SPI传输
// static void _spi_transfer_wait()
// {
// 	while(SPI_I2S_GetFlagStatus(SPI_MASTER, SPI_I2S_FLAG_BSY) == SET);
// }

// 等待所有DMA队列完成传输
static void _spi_lcd_flush(void)
{
	dma_queue_wait(&g_dma1_2_queue);
	dma_queue_wait(&g_dma1_3_queue);
}

// 发送命令
static uint32_t _spi_lcd_send_cmd(const uint8_t cmd)
{
	return dma_queue_push(&g_dma1_3_queue, cmd, 1, 0, DCF_BITS_8 | DCF_PTR_INC | DCF_DIR_TX | DCF_DATA_INLINED);
}

///////////////////////////////////////////////////////////////////////////////
//
// 以下的函数都是发送数据的
//
// 所谓数据,就是传输的时候DC针要设为高电平
// param的意思是“发送的是参数”,数据是4字节以内的
// num是DMA传输的单位数量
// repeat_count是DMA传输的自动重启的次数
//
///////////////////////////////////////////////////////////////////////////////

// 以8bit为单位发送指针指向的数据
static uint32_t _spi_lcd_send_data8(const void *data, const uint32_t num, const uint8_t repeat_count)
{
	return dma_queue_push(&g_dma1_3_queue, (data_t)data, num, repeat_count,
		DCF_BITS_8 | DCF_PTR_INC | DCF_DIR_TX | DCF_CUSTOM_FLAG);
}

// 以8bit为单位发送4个字节以内的数据
static uint32_t _spi_lcd_send_param8(const data_t param, const uint32_t num, const uint8_t repeat_count)
{
	return dma_queue_push(&g_dma1_3_queue, param, num, repeat_count,
		DCF_BITS_8 | DCF_PTR_INC | DCF_DIR_TX | DCF_DATA_INLINED | DCF_CUSTOM_FLAG);
}

// 以16bit为单位发送指针指向的数据
static uint32_t _spi_lcd_send_data16(const void *data, const uint32_t num, const uint8_t repeat_count)
{
	return dma_queue_push(&g_dma1_3_queue, (data_t)data, num, repeat_count,
		DCF_BITS_16 | DCF_PTR_INC | DCF_DIR_TX | DCF_CUSTOM_FLAG);
}

// 以16bit为单位发送4个字节以内的数据
static uint32_t _spi_lcd_send_param16(const data_t data, const uint32_t num, const uint8_t repeat_count)
{
	return dma_queue_push(&g_dma1_3_queue, data, num, repeat_count,
		DCF_BITS_16 | DCF_PTR_INC | DCF_DIR_TX | DCF_DATA_INLINED | DCF_CUSTOM_FLAG);
}

// 以8bit为单位发送同一个uint8_t的数据,num次
// static uint32_t _spi_lcd_send_param8_repeat(const data_t param, const uint32_t num, const uint8_t repeat_count)
// {
// 	return dma_queue_push(&g_dma1_3_queue, param, num, repeat_count,
// 		DCF_BITS_8 | DCF_PTR_STAY | DCF_DIR_TX | DCF_DATA_INLINED | DCF_CUSTOM_FLAG);
// }

// 以16bit为单位发送同一个uint16_t的数据,num次
static uint32_t _spi_lcd_send_param16_repeat(const data_t data, const uint32_t num, const uint8_t repeat_count)
{
	return dma_queue_push(&g_dma1_3_queue, data, num, repeat_count,
		DCF_BITS_16 | DCF_PTR_STAY | DCF_DIR_TX | DCF_DATA_INLINED | DCF_CUSTOM_FLAG);
}

// 以8bit为单位,接收数据到一个缓冲区
static uint32_t _spi_lcd_recv_data8(const void *data, const uint32_t num)
{
	return dma_queue_push(&g_dma1_2_queue, (data_t)data, num, 1,
		DCF_BITS_8 | DCF_PTR_INC | DCF_DIR_RX | DCF_CUSTOM_FLAG);
}

// 以16bit为单位,接收数据到一个缓冲区
static uint32_t _spi_lcd_recv_data16(const void *data, const uint32_t num)
{
	return dma_queue_push(&g_dma1_2_queue, (data_t)data, num, 1,
		DCF_BITS_16 | DCF_PTR_INC | DCF_DIR_RX | DCF_CUSTOM_FLAG);
}

///////////////////////////////////////////////////////////////////////////////
//
// 以下的函数都是传输特定格式的颜色数据用的
//
// RGB565的格式是:
//   红色在高位,绿色在中间,蓝色在低位,组成一个uint16_t,红色和蓝色是5bit,
//   绿色是6bit。
//   rrrrrGGGGGGbbbbb
//
// RGB666的格式是:
//   红色在低位,绿色在中间,蓝色在高位,红绿蓝各占一个字节,共3个字节,24位。
//   其中红绿蓝每个通道的低2位被丢弃,传输的时候不起作用。只有高6bit起作用。
//   bbbbbb00gggggg00rrrrrr00
//
///////////////////////////////////////////////////////////////////////////////

static void _get_pixels_rgb565(void *rgb565, const uint32_t count)
{
	uint32_t last_index;
	_spi_lcd_send_cmd(LCD_RAMRD);
	// 接收数据前,需要先发个0xFF
	last_index = _spi_lcd_send_param8(0xFF, 1, 0);
	dma_queue_wait_transfer(&g_dma1_3_queue, last_index); // 要等这个0xFF发出去,再接收
	_spi_lcd_recv_data16(rgb565, count);
}

static void _get_pixels_rgb666(void *rgb666, const uint32_t count)
{
	uint32_t last_index;
	_spi_lcd_send_cmd(LCD_RAMRD);
	// 接收数据前,需要先发个0xFF
	last_index = _spi_lcd_send_param8(0xFF, 1, 0);
	dma_queue_wait_transfer(&g_dma1_3_queue, last_index); // 要等这个0xFF发出去,再接收
	_spi_lcd_recv_data8(rgb666, count * 3);
}

static void _put_pixels_rgb565(const void *rgb565, const uint32_t count)
{
	_spi_lcd_send_cmd(LCD_GRAM);
	_spi_lcd_send_data16(rgb565, count, 0);
}

static void _put_pixels_rgb666(const void *rgb666, const uint32_t count)
{
	_spi_lcd_send_cmd(LCD_GRAM);
	_spi_lcd_send_data8(rgb666, count * 3, 0);
}

static void _put_pure_color_rgb565(const uint32_t rgb565, const uint32_t count)
{
	uint32_t rest = count;

	_spi_lcd_send_cmd(LCD_GRAM);
	// 一次不能发送超过65535个数据
	if(rest > 0x0FFFF)
	{
		_spi_lcd_send_param16_repeat(rgb565, 0x0FFFF, rest / 0x0FFFF - 1); // 重复发送
		rest %= 0x0FFFF;
	}
	_spi_lcd_send_param16_repeat(rgb565, rest, 0); // 发送剩下的
}

static void _put_pure_color_rgb666(const uint32_t rgb666, const uint32_t count)
{
	// 发送3个字节编码的纯色,这里其实非常尴尬。
	// DMA无法以3字节为单位发送数据
	// 在栈上建立缓冲区来实现功能
	
	const uint32_t buffer_pixels = 64; // 缓冲区大小,像素数
	uint8_t color_buf[buffer_pixels * 3];
	int i;
	uint32_t last_index;
	uint32_t rest = count;
	
	// 填充纯色到缓冲区里
	for(i = 0; i < buffer_pixels * 3 && i < count * 3; i++)
	{
		color_buf[i] = rgb666 >> ((i % 3) * 8);
	}
	
	last_index = _spi_lcd_send_cmd(LCD_GRAM);
	
	// 数据量大,循环重复
	while(rest > buffer_pixels * 256)
	{
		last_index = _spi_lcd_send_data8(color_buf, buffer_pixels * 3, 255);
		rest -= buffer_pixels * 256;
	}
	// 发送剩下的循环重复的部分
	if(rest > buffer_pixels)
	{
		last_index = _spi_lcd_send_data8(color_buf, buffer_pixels * 3, rest / buffer_pixels - 1);
		rest %= buffer_pixels;
	}
	// 发送最后剩下的
	if(rest)
		last_index = _spi_lcd_send_data8(color_buf, rest * 3, 0);
	
	// 数据在栈上,所以只能等传输完了才能返回。
	dma_queue_wait_transfer(&g_dma1_3_queue, last_index);
	
	/*
	// 另一个实现,直接重复发送指定个数的3字节,非常慢。
	// 每发3个字节会产生一次中断。
	uint32_t rest = count;

	_spi_lcd_send_cmd(LCD_GRAM);
	while(rest > 256)
	{
		_spi_lcd_send_param8(rgb666, 3, 255);
		rest -= 256;
	}
	_spi_lcd_send_param8(rgb666, 3, rest - 1);*/
	
}

// 函数指针,用于对外提供不针对格式的接口。
static void(*_get_pixels)(void *pixels, const uint32_t count);
static void(*_put_pixels)(const void *pixels, const uint32_t count);
static void(*_put_pure_color)(const uint32_t color, const uint32_t count);

void lcd_init()
{
	_spi_gpio_config();
	_spi_config();
	_spi_dma_config();
	
#if LCD_CMDBUF_LED // 调试:传输时的灯
	RCC_APB2PeriphClockCmd(LCD_CMDBUF_LED_GPIO_CLK, ENABLE);
#endif
	
	lcd_reset();
}

void lcd_reset()
{
    const uint8_t *ptr = _init_commands;

	TFT_GPIO->BRR = TFT_RESET_PIN;
    delay_ms(10);
	TFT_GPIO->BSRR = TFT_RESET_PIN;
    delay_ms(50);
    _spi_lcd_send_cmd(LCD_SLEEP_OUT);
    delay_ms(150);
    _spi_lcd_send_cmd(LCD_DISPLAY_ON);
	
	// 发送初始化序列
    while(1)
	{
        uint8_t count = *ptr;
        if(!count) break; // 参数字节数
		ptr ++;
        _spi_lcd_send_cmd(*ptr); // 第一个是命令
		ptr ++;
        _spi_lcd_send_data8(ptr, count - 1, 0); // 剩下的是参数
        ptr += count - 1;
    }
	
	// 初始化后的传输格式是RGB565
	g_lcd_rgb_format = lcdf_rgb565;
	
	// 设置函数指针
	_get_pixels = _get_pixels_rgb565;
	_put_pixels = _put_pixels_rgb565;
	_put_pure_color = _put_pure_color_rgb565;
	
	// 此处设置 g_lcd_width 和 g_lcd_height的数值
	lcd_set_portrait(0);
	
	// 开启背光
	TFT_GPIO->BSRR = TFT_LED_PIN;
}

// 切换格式为16bit颜色
void lcd_set_rgb16()
{
	_spi_lcd_send_cmd(LCD_PIXEL_FORMAT);
	_spi_lcd_send_param8(0x55, 1, 0);
	g_lcd_rgb_format = lcdf_rgb565;
	
	_get_pixels = _get_pixels_rgb565;
	_put_pixels = _put_pixels_rgb565;
	_put_pure_color = _put_pure_color_rgb565;
}

// 切换格式为18bit颜色
void lcd_set_rgb18()
{
	_spi_lcd_send_cmd(LCD_PIXEL_FORMAT);
	_spi_lcd_send_param8(0x66, 1, 0);
	g_lcd_rgb_format = lcdf_rgb666;
	
	_get_pixels = _get_pixels_rgb666;
	_put_pixels = _put_pixels_rgb666;
	_put_pure_color = _put_pure_color_rgb666;
}

// 设置横向显示
void lcd_set_landscape(const int mirror)
{
	uint8_t cmd = mirror ? ORIENTATION_LANDSCAPE_MIRROR : ORIENTATION_LANDSCAPE;
	
	g_lcd_height = LCD_PIXEL_WIDTH;
	g_lcd_width  = LCD_PIXEL_HEIGHT;
	
    _spi_lcd_send_cmd(LCD_MAC);
    _spi_lcd_send_param8(cmd, 1, 0);
}

// 设置竖向显示
void lcd_set_portrait(const int mirror)
{
	uint8_t cmd = mirror ? ORIENTATION_PORTRAIT_MIRROR : ORIENTATION_PORTRAIT;
	
	g_lcd_height = LCD_PIXEL_HEIGHT;
	g_lcd_width  = LCD_PIXEL_WIDTH;
	
    _spi_lcd_send_cmd(LCD_MAC);
    _spi_lcd_send_param8(cmd, 1, 0);
}

// 设置绘图区域
void lcd_set_draw_area(const int x1, const int y1, const int x2, const int y2)
{
	_spi_lcd_send_cmd(LCD_COLUMN_ADDR);
	_spi_lcd_send_param16(MAKE_PARAM(x1, x2), 2, 0);
	
	_spi_lcd_send_cmd(LCD_PAGE_ADDR);
	_spi_lcd_send_param16(MAKE_PARAM(y1, y2), 2, 0);
}

// 从绘图区域里面取出像素数据
void lcd_get_pixels(void *color, const uint32_t count)
{
	_get_pixels(color, count);
}

// 写入像素数据到绘图区域里
void lcd_put_pixels(const void *color, const uint32_t count)
{
	_put_pixels(color, count);
}

// 写入纯色到绘图区域里
void lcd_put_pure_color(const uint32_t color, const uint32_t count)
{
	_put_pure_color(color, count);
}

// 用一个颜色填充全屏
void lcd_fill_screen(const uint32_t color)
{
	lcd_set_draw_area(0, 0, g_lcd_width - 1, g_lcd_height - 1);
	lcd_put_pure_color(color, g_lcd_pixels);
}

// 填充区域(根据左、上、右、下确定范围)
void lcd_fill_area(int x1, int y1, int x2, int y2, const uint32_t color)
{
	if(x1 > x2)
	{
		int t = x1;
		x1 = x2;
		x2 = t;
	}
	if(y1 > y2)
	{
		int t = y1;
		y1 = y2;
		y2 = t;
	}
	if(x1 >= g_lcd_width || y1 >= g_lcd_height || x2 < 0 || y2 < 0) return;
	
	if(x1 < 0) x1 = 0;
	if(y1 < 0) y1 = 0;
	if(x2 > g_lcd_width - 1) x2 = g_lcd_width - 1;
	if(y2 > g_lcd_height - 1) y2 = g_lcd_height - 1;
	
	lcd_set_draw_area(x1, y1, x2, y2);
	lcd_put_pure_color(color, (x2 - x1 + 1) * (y2 - y1 + 1));
}

// 填充矩形(根据左、上、宽、高确定范围)
void lcd_fill_rect(int x, int y, int w, int h, const uint32_t color)
{
	if(x >= g_lcd_width || y >= g_lcd_height) return;
	
	if(x < 0)
	{
		w += x;
		x = 0;
	}
	
	if(y < 0)
	{
		h += y;
		y = 0;
	}
	
	if(x + w > g_lcd_width)
	{
		w = g_lcd_width - x;
	}
	
	if(y + h > g_lcd_height)
	{
		h = g_lcd_height - y;
	}
	
	if(w <= 0 || h <= 0) return;
	
	lcd_set_draw_area(x, y, x + w - 1, y + h - 1);
	lcd_put_pure_color(color, w * h);
}

// 等待绘图完成
void lcd_flush()
{
	_spi_lcd_flush();
}
这是LCD控制部分的源码。经过测试,它可以在屏幕上绘制出图形。

然后这是测试代码。
[C] 纯文本查看 复制代码
#include<inttypes.h>
#include<stdlib.h>

#include<stm32f10x.h>
#include<system_stm32f10x.h>

#include"lcd.h"
#include"delay.h"
#include"debug.h"

void main()
{
	SystemInit();
	
	lcd_init();
	// lcd_set_landscape(0);
	
	lcd_fill_screen(RGB565(0, 127, 255));
	
	int i = 0, f = 0;
	
	while(1)
	{
		int x, y;
		
		i = f;
		
		// 画全屏棋盘格
		for(y = 0; y < g_lcd_height; y += 20)
		{
			for(x = 0; x < g_lcd_width; x += 20)
			{
				int r = 0, g = 0, b = 0;
				int v = i % 1536;
				// 根据i的值生成平滑过渡的颜色(有六个过渡阶段)
				if(v < 256)
				{
					r = 255;
					g = 0;
					b = 255 - v;
				}
				else if(v < 512)
				{
					r = 255;
					g = v - 256;
					b = 0;
				}
				else if(v < 768)
				{
					r = 255 - (v - 512);
					g = 255;
					b = 0;
				}
				else if(v < 1024)
				{
					r = 0;
					g = 255;
					b = v - 768;
				}
				else if(v < 1280)
				{
					r = 0;
					g = 255 - (v - 1024);
					b = 255;
				}
				else
				{
					r = v - 1280;
					g = 0;
					b = 255;
				}
				
				// 奇偶块反色
				lcd_fill_rect(x, y, 20, 20, ((x / 20) ^ (y / 20)) & 1 ? ~RGB(r, g, b) : RGB(r, g, b));
				
				i ++;
			}
		}
		
		f += 12;
	}
}
效果如下(拍的角度不太好,正面看的效果是很好的)
ili9341_.gif

放大看它的像素点
lcd.jpg

可以用ST-Util调试看到DMA请求队列的变化过程。
st-util.gif

接下来,我应该可以用这个去做个ADC示波器了。

本版积分规则

QQ|申请友链|Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号|网站地图

GMT+8, 2018-11-21 16:39 , Processed in 0.107932 second(s), 16 queries , Gzip On, Memcache On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表