0xAA55 发表于 2018-3-28 12:25:09

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

原文网址: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://camo.githubusercontent.com/598be785690c99f7bd22c349e9abe7d2baa1240e/687474703a2f2f696d672e796f75747562652e636f6d2f76692f356e4c512d56714d762d672f302e6a7067
毛子视频: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




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

一开始我买到这个屏幕后,我就到处找它的PDF文档,然后,我找到了——这文档与其说是文档不如说是“对那个官方的C艹库提供的补充说明”
PDF回帖后可见。**** Hidden Message *****最后我决定以毛子代码为基准来重新造轮子了——我要让它把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控制器它就开始传输了——你只需要按一下“煮饭”,你的电饭煲就开始煮饭了。等到它完成了传输,它就会产生一个特定的中断信号,中断处理程序就会启动下一个传输请求——电饭煲煮好饭的时候会发出声音通知你,然后你就可以把米饭拿出来,再把煮粥的材料倒进电饭煲里,按下“煮粥”按钮了……

虽然传输的结束和开始依然需要处理器的干预,但传输的过程是不需要干预的了。这样就达到了我的目的——使用一个电饭煲煮各种食物的同时,不影响我干别的事。#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; // 队列
};

// 按要求定义全局变量
#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_dmax_y_queue;就是这个结构体的实例化变量了。因为中断处理函数的名字是钦定的,所以我要给每一个需要用到的DMA通道都定义一个专属的全局变量。然后每个变量的体积都不小,所以用宏来配置它,只定义用得到的。免得占用RAM过多。

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

我的设计是:对于少量的数据(比如一条用于控制显示器的命令等),直接把它存储在每个DMA请求的data字段里,然后用DCF_DATA_INLINED来标识。而对于较大的数据的话,则另想办法。至少,我们不至于为了那几个字节的控制命令而浪费有限的RAM。用柴火煮饭的话好歹刨个坑,然后把柴火放坑里。#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;
        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;
       
        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.data = data;
                        dma_queue->dma_ctrl.count = count;
                        dma_queue->dma_ctrl.repeat_count = repeat_count;
                        dma_queue->dma_ctrl.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个像素——光是像素的个数,就超过了STM32F103C8T6的内存字节数(20480字节)。所以在单片机的内存里存储屏幕图像的做法并不现实,因为你存不下。

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

理解了这个,你就更容易能理解GDI的设计初衷了。#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把命令和命令参数都传输过去。当你实现了自己的画线的函数以后,假设你要画一条斜线,它其实是靠画多个横线或者竖线来完成的。而画横线和竖线则是靠填充区域完成的。




想象一下,如果你要画一条斜线,你需要一边计算每个小的直线的线段位置和长度,一边把绘图的请求发给屏幕。这个过程如果能并行处理的话,你可以在很短的时间里就画好一条直线。#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_CONTINUE0x3C   /* 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.flags & DCF_CUSTOM_FLAG) == DCF_CUSTOM_FLAG)
        {
                TFT_GPIO->BSRR = TFT_DC_PIN;
        }
        if((pq->dma_ctrl.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.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;
        int i;
        uint32_t last_index;
        uint32_t rest = count;
       
        // 填充纯色到缓冲区里
        for(i = 0; i < buffer_pixels * 3 && i < count * 3; i++)
        {
                color_buf = 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控制部分的源码。经过测试,它可以在屏幕上绘制出图形。

然后这是测试代码。#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;
        }
}效果如下(拍的角度不太好,正面看的效果是很好的)


放大看它的像素点


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


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

beck74 发表于 2020-1-4 14:54:32

学习一下。
页: [1]
查看完整版本: 【单片机】给STM32F103实现一个靠谱的并行DMA库