I've found an example for using SPI to drive VGA signals: viewtopic.php?f=2&t=4011#p18107 . Github: https://github.com/t-mat/esp32-vga-experiment . I've modified this example. I've added a CLK and a CS signal and lowered the frequency.
I'm currently facing several issues with this:
- The chip select signal is not being driven. It is set low and never gets high.
- The clock signal is being driven before the first data is send on MOSI. This shouldn't be an issue since only the last 16 bits will be latched.
- There is no pause between the two-byte transmissions. They are back to back. Even if a link in the linked list is marked as start of frame and/or end of frame.
my_vga.cpp:
Code: Select all
#define SEGMENT_COUNT 3
static uint8_t spi_buffer_[2*SEGMENT_COUNT] __attribute__ ((aligned (4)));
static lldesc_t lldesc_[SEGMENT_COUNT]={};
esp_err_t init(const myvga_init_params_t* initParams) {
auto* ap = reinterpret_cast<uint8_t*>(this);
ap += sizeof(ThisClass);
{
const auto& ip = *initParams;
userVideo.width = ip.video.width;
userVideo.height = ip.video.height;
userVideo.stride = ip.video.strideInBytes;
userVideo.buffer = static_cast<uint8_t*>(ip.video.buffer);
spi.host = ip.spi.host;
spi.dmaChan = ip.spi.dmaChan;
spi.mosiGpioNum = ip.spi.mosiGpioNum;
spi.hw = myspi_get_hw_for_host(ip.spi.host);
rmt.hsyncChannel = ip.rmt.hsyncChannel;
rmt.vsyncChannel = ip.rmt.vsyncChannel;
rmt.hsyncGpioNum = ip.rmt.hsyncGpioNum;
rmt.vsyncGpioNum = ip.rmt.vsyncGpioNum;
}
vsyncCallback.callback = nullptr;
blankLine = reinterpret_cast<decltype(blankLine)>(ap);
ap += blankLineBytes;
{
memset(blankLine, 0, blankLineBytes);
}
descs = reinterpret_cast<decltype(descs)>(ap);
ap += sizeof(lldesc_t) * 2 * VgaSignalHeightInLines;
{
for(int y = 0; y < VgaSignalHeightInLines; ++y) {
const int videoY = y - (VgaVSyncSignalInLines + VgaVSyncBackPorchInLines);
const bool isVideoEnable = (videoY >= 0 && videoY < userVideo.height);
const bool isLast = (y == VgaSignalHeightInLines - 1);
{
auto* dd = &descs[y * 2 + 0];
auto* next = dd + 1;
const int dmaChunkLen = userVideo.width / 8;
dd->size = dmaChunkLen;
dd->length = dmaChunkLen;
uint8_t* buf = nullptr;
if(isVideoEnable) {
buf = &userVideo.buffer[userVideo.stride * videoY];
}
if(nullptr == buf) {
buf = blankLine;
}
dd->buf = buf;
dd->eof = 0;
dd->sosf = 0;
dd->owner = 1;
dd->qe.stqe_next = next;
}
{
auto* dd = &descs[y * 2 + 1];
auto* next = dd + 1;
if(isLast) {
next = &descs[0];
}
const int dmaChunkLen = (VgaSignalWidthInPixels - userVideo.width) / 8;
dd->size = dmaChunkLen;
dd->length = dmaChunkLen;
dd->buf = blankLine;
dd->eof = 0;
dd->sosf = 0;
dd->owner = 1;
dd->qe.stqe_next = next;
}
}
}
const double SpiDmaClockSpeedInHz = 1E6;//VgaPixelFrequencyInHz;
//put test data in buffer:
// for(int i = 0;i<SEGMENT_COUNT;++i)
// {
// spi_buffer_[i*2+0] = uint8_t(7 & (~(1<<i)));//anode
// spi_buffer_[i*2+1] = 0;//data
// }
// spi_buffer_[0*2+1] = 0xFF;
// spi_buffer_[1*2+1] = 0xFB;
// spi_buffer_[2*2+1] = 0xF7;
spi_buffer_[0]=129;
spi_buffer_[1]=3;
spi_buffer_[2]=7;
spi_buffer_[3]=15;
spi_buffer_[4]=31;
spi_buffer_[5]=63;
spi_buffer_[6]=127;
for(int i = 0;i<SEGMENT_COUNT;++i)
{
lldesc_[i].size = 2;
lldesc_[i].length = 2;
lldesc_[i].offset = 0;
lldesc_[i].sosf = 1;
lldesc_[i].eof = 1;
lldesc_[i].owner = 1;//????
//point to data buffer:
lldesc_[i].buf = &spi_buffer_[i*2];
//create circular linked list:
lldesc_[i].qe.stqe_next = &lldesc_[(i+1)%SEGMENT_COUNT];
}
gpio_num_t latch_pin=GPIO_NUM_4;
gpio_num_t data_pin=GPIO_NUM_5;
gpio_num_t clk_pin=GPIO_NUM_2;
myspi_prepare_circular_buffer(
spi.host
, spi.dmaChan
, lldesc_//descs
, SpiDmaClockSpeedInHz
, data_pin
, clk_pin
, latch_pin
, SpiHSyncBackporchWaitCycle
);
intr_handle_t my_rmt_isr_handle;
ESP_ERROR_CHECK(esp_intr_alloc(ETS_RMT_INTR_SOURCE, ESP_INTR_FLAG_SHARED, rmtIsr, this, &my_rmt_isr_handle));
portDISABLE_INTERRUPTS();
// Reset timers and begin SPI DMA transfer
spi_dev_t* const spiHw = getSpiHw();
// Here, we're waiting for completion of RMT TX. When TX is completed,
// RMT channel's internal counter becomes some constant value (maybe 0?).
// Therefore, we can see stable behaviour of RMT channel.
{
auto& hsyncRmtConf1 = RMT.conf_ch[rmt.hsyncChannel].conf1;
auto& vsyncRmtConf1 = RMT.conf_ch[rmt.vsyncChannel].conf1;
hsyncRmtConf1.tx_conti_mode = 0;
vsyncRmtConf1.tx_conti_mode = 0;
const uint32_t mask = BIT(rmt.hsyncChannel * 3 + 0) | BIT(rmt.vsyncChannel * 3 + 0);
for(;;) {
const uint32_t int_raw = RMT.int_raw.val;
if((int_raw & mask) == mask) {
break;
}
}
hsyncRmtConf1.ref_cnt_rst = 1; // RMT_REF_CNT_RST_CH Setting this bit resets the clock divider of channel n. (R/W)
vsyncRmtConf1.ref_cnt_rst = 1; // RMT_REF_CNT_RST_CH
hsyncRmtConf1.mem_rd_rst = 1; // RMT_MEM_RD_RST_CHn Set this bit to reset the read-RAM address for channel n by accessing the transmitter. (R/W)
vsyncRmtConf1.mem_rd_rst = 1; // RMT_MEM_RD_RST_CHn
}
spiHw->dma_conf.dma_tx_stop = 1; // Stop SPI DMA
//spiHw->ctrl2.val = 0; // Reset timing
spiHw->dma_conf.dma_tx_stop = 0; // Disable stop
spiHw->dma_conf.dma_continue = 1; // Set contiguous mode
spiHw->dma_out_link.start = 1; // Start SPI DMA transfer (1)
ESP_ERROR_CHECK(rmt_set_tx_thr_intr_en(rmt.hsyncChannel, true, 1));
ESP_ERROR_CHECK(rmt_set_tx_thr_intr_en(rmt.vsyncChannel, true, 7));
clearIsrCounters();
kickPeripherals(spiHw, rmt.hsyncChannel, rmt.vsyncChannel);
portENABLE_INTERRUPTS();
return ESP_OK;
}
Code: Select all
static uint8_t getSpid_cs0_out_ByHost(
spi_host_device_t host
) {
switch(host) {
case SPI_HOST: return SPICS0_OUT_IDX; break;
case HSPI_HOST: return HSPICS0_OUT_IDX; break;
case VSPI_HOST: return VSPICS0_OUT_IDX; break;
default: return InvalidIndex; break;
}
}
static uint8_t getSpid_cs0_in_ByHost(
spi_host_device_t host
) {
switch(host) {
case SPI_HOST: return SPICS0_IN_IDX; break;
case HSPI_HOST: return HSPICS0_IN_IDX; break;
case VSPI_HOST: return VSPICS0_IN_IDX; break;
default: return InvalidIndex; break;
}
}
static uint8_t getSpi_clk_in_ByHost(
spi_host_device_t host
) {
switch(host) {
case SPI_HOST: return SPICLK_IN_IDX; break;
case HSPI_HOST: return HSPICLK_IN_IDX; break;
case VSPI_HOST: return VSPICLK_IN_IDX; break;
default: return InvalidIndex; break;
}
}
static uint8_t getSpi_clk_out_ByHost(
spi_host_device_t host
) {
switch(host) {
case SPI_HOST: return SPICLK_OUT_IDX; break;
case HSPI_HOST: return HSPICLK_OUT_IDX; break;
case VSPI_HOST: return VSPICLK_OUT_IDX; break;
default: return InvalidIndex; break;
}
}
esp_err_t myspi_prepare_circular_buffer(
const spi_host_device_t spiHostDevice
, const int dma_chan
, const lldesc_t* lldescs
, const double dmaClockSpeedInHz
, const gpio_num_t mosi_gpio_num
, const gpio_num_t clk_gpio_num
, const gpio_num_t cs_gpio_num
, const int waitCycle
) {
const bool spi_periph_claimed = spicommon_periph_claim(spiHostDevice);
if(! spi_periph_claimed) {
return MY_ESP_ERR_SPI_HOST_ALREADY_IN_USE;
}
const bool dma_chan_claimed = spicommon_dma_chan_claim(dma_chan);
if(! dma_chan_claimed) {
spicommon_periph_free(spiHostDevice);
return MY_ESP_ERR_SPI_DMA_ALREADY_IN_USE;
}
spi_dev_t* const spiHw = myspi_get_hw_for_host(spiHostDevice);
const int Cs = 0;
const int CsMask = 1 << Cs;
//Use GPIO
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[mosi_gpio_num], PIN_FUNC_GPIO);
gpio_set_direction(mosi_gpio_num, GPIO_MODE_INPUT_OUTPUT);
gpio_matrix_out(mosi_gpio_num, getSpidOutByHost(spiHostDevice), false, false);
gpio_matrix_in(mosi_gpio_num, getSpidInByHost(spiHostDevice), false);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[clk_gpio_num], PIN_FUNC_GPIO);
gpio_set_direction(clk_gpio_num, GPIO_MODE_INPUT_OUTPUT);
gpio_matrix_out(clk_gpio_num, getSpi_clk_out_ByHost(spiHostDevice), false, false);
gpio_matrix_in(clk_gpio_num, getSpi_clk_in_ByHost(spiHostDevice), false);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[cs_gpio_num], PIN_FUNC_GPIO);
gpio_set_direction(cs_gpio_num, GPIO_MODE_INPUT_OUTPUT);
gpio_matrix_out(cs_gpio_num, getSpid_cs0_out_ByHost(spiHostDevice), false, false);
gpio_matrix_in(cs_gpio_num, getSpid_cs0_in_ByHost(spiHostDevice), false);
//Select DMA channel.
DPORT_SET_PERI_REG_BITS(
DPORT_SPI_DMA_CHAN_SEL_REG
, 3
, dma_chan
, (spiHostDevice * 2)
);
//Reset DMA
spiHw->dma_conf.val |= SPI_OUT_RST|SPI_IN_RST|SPI_AHBM_RST|SPI_AHBM_FIFO_RST;
spiHw->dma_out_link.start = 0;
spiHw->dma_in_link.start = 0;
spiHw->dma_conf.val &= ~(SPI_OUT_RST|SPI_IN_RST|SPI_AHBM_RST|SPI_AHBM_FIFO_RST);
//Reset timing
spiHw->ctrl2.val = 0;
//Disable unneeded ints
spiHw->slave.rd_buf_done = 0;
spiHw->slave.wr_buf_done = 0;
spiHw->slave.rd_sta_done = 0;
spiHw->slave.wr_sta_done = 0;
spiHw->slave.rd_buf_inten = 0;
spiHw->slave.wr_buf_inten = 0;
spiHw->slave.rd_sta_inten = 0;
spiHw->slave.wr_sta_inten = 0;
spiHw->slave.trans_inten = 0;
spiHw->slave.trans_done = 0;
//Set CS pin, CS options
//spiHw->pin.master_ck_sel &= ~CsMask;
//spiHw->pin.master_cs_pol &= ~CsMask;
spiHw->pin.master_ck_sel &= CsMask;
spiHw->pin.master_cs_pol &= CsMask;
// Set SPI Clock:
{
const double preDivider = 4.0;
const double apbClockSpeedInHz = APB_CLK_FREQ;
const double apbClockPerDmaCycle = (apbClockSpeedInHz / preDivider / dmaClockSpeedInHz);
const int32_t clkdiv_pre = ((int32_t) preDivider) - 1;
const int32_t clkcnt_n = ((int32_t) apbClockPerDmaCycle) - 1;
const int32_t clkcnt_h = (clkcnt_n + 1) / 2 - 1;
const int32_t clkcnt_l = clkcnt_n;
spiHw->clock.clk_equ_sysclk = 0;
spiHw->clock.clkcnt_n = clkcnt_n;
spiHw->clock.clkdiv_pre = clkdiv_pre;
spiHw->clock.clkcnt_h = clkcnt_h;
spiHw->clock.clkcnt_l = clkcnt_l;
}
//Configure bit order
spiHw->ctrl.rd_bit_order = 0; // MSb first
spiHw->ctrl.wr_bit_order = 0; // MSb first
//Configure polarity
spiHw->pin.ck_idle_edge = 0;
spiHw->user.ck_out_edge = 0;
spiHw->ctrl2.miso_delay_mode = 0;
//configure dummy bits
spiHw->user.usr_dummy = 0;
spiHw->user1.usr_dummy_cyclelen = 0;
//Configure misc stuff
spiHw->user.doutdin = 0;
spiHw->user.sio = 0;
//Configure CS timing
spiHw->ctrl2.setup_time = 0;
spiHw->user.cs_setup = 0;
spiHw->ctrl2.hold_time = 0;
spiHw->user.cs_hold = 0;
//Configure CS pin
spiHw->pin.cs0_dis = (Cs == 0) ? 0 : 1;
spiHw->pin.cs1_dis = (Cs == 1) ? 0 : 1;
spiHw->pin.cs2_dis = (Cs == 2) ? 0 : 1;
//spiHw->pin.cs_keep_active = 1;
//Reset SPI peripheral
spiHw->dma_conf.val |= SPI_OUT_RST|SPI_IN_RST|SPI_AHBM_RST|SPI_AHBM_FIFO_RST;
spiHw->dma_out_link.start = 0;
spiHw->dma_in_link.start = 0;
spiHw->dma_conf.val &= ~(SPI_OUT_RST|SPI_IN_RST|SPI_AHBM_RST|SPI_AHBM_FIFO_RST);
spiHw->dma_conf.out_data_burst_en = 1;
//Set up QIO/DIO if needed
spiHw->ctrl.val &= ~(SPI_FREAD_DUAL|SPI_FREAD_QUAD|SPI_FREAD_DIO|SPI_FREAD_QIO);
spiHw->user.val &= ~(SPI_FWRITE_DUAL|SPI_FWRITE_QUAD|SPI_FWRITE_DIO|SPI_FWRITE_QIO);
//DMA temporary workaround: let RX DMA work somehow to avoid the issue in ESP32 v0/v1 silicon
spiHw->dma_in_link.addr = 0;
spiHw->dma_in_link.start = 1;
spiHw->user1.usr_addr_bitlen = 0;
spiHw->user2.usr_command_bitlen = 0;
spiHw->user.usr_addr = 0;
spiHw->user.usr_command = 0;
if(waitCycle <= 0) {
spiHw->user.usr_dummy = 0;
spiHw->user1.usr_dummy_cyclelen = 0;
} else {
spiHw->user.usr_dummy = 1;
spiHw->user1.usr_dummy_cyclelen = (uint8_t) (waitCycle-1);
}
spiHw->user.usr_mosi_highpart = 0;
spiHw->user2.usr_command_value = 0;
spiHw->addr = 0;
spiHw->user.usr_mosi = 1; // Enable MOSI
spiHw->user.usr_miso = 0;
spiHw->dma_out_link.addr = (int)(lldescs) & 0xFFFFF;
spiHw->mosi_dlen.usr_mosi_dbitlen = 0; // works great! (there's no glitch in 5 hours)
spiHw->miso_dlen.usr_miso_dbitlen = 0;
// Set circular mode
// https://www.esp32.com/viewtopic.php?f=2&t=4011#p18107
// > yes, in SPI DMA mode, SPI will alway transmit and receive
// > data when you set the SPI_DMA_CONTINUE(BIT16) of SPI_DMA_CONF_REG.
spiHw->dma_conf.dma_continue = 1;
return ESP_OK;
}