There's one more part to my video generator - the picture data, which I want to transfer to the SPI port using DMA. This actually looks fairly straightforward, these are the available registers:

MEM2MEM I'm transferring from memory to a peripheral, so this should be off.
PL I'll make this "very high" priority, because I want to keep the picture stable at all costs. If a program writes to the framebuffer during this DMA transfer, it will be blocked.
MSIZE I've set the SPI port to 8 bits so I'll stick with that. I don't think it will make any difference whether it's 8 or 16.
MINC I want the memory pointer to increment during the transfer
PINC I guess this should be off, because to write SPI you keep sending data to the same memory location.
CIRC I don't want the memory pointer to circle around.
DIR Read from memory
Interrupts I won't need any yet, but eventually I'll have to turn off the SPI port at the end of the transfer, otherwise I'll get white bars down the sides of the screen.
`DMA_CNDTRx` contains how much data to transfer. There are 7 channels, and table 40 of the reference manual says `SPI2_TX` is on channel 5. This needs to be set to the number of pixels / 8, since I'll have 8 pixels in one byte.  There's a "auto-reload" setting somewhere which resets this counter value after a transfer; I think this happens in circular mode.

Table 40 also suggests I must use DMA1 for these transfers.

The peripheral address register should point to the SPI data register (&(SPI2->DR)), and the memory register is the start of the current line of pixels.

That's all of the available settings!  There's one more thing to do though: section 10.3.7 says this:

The peripheral DMA requests can be independently activated/de-activated by programming the DMA control bit in the registers of the corresponding peripheral.

I guess this is the TXDMAEN bit in the SPI_CR2 register.

Now for some code... first I'll make some data to send:

const uint8_t image[] = { 0xAA, 0x55, 0xAA, 0x55 };

Of course later on I'll have a lot more data...

Now to set the above settings:

  DMA1_Channel5->CCR = DMA_CCR5_PL // very high priority
          | DMA_CCR5_MINC  // memory increment mode
          | DMA_CCR5_DIR;  // read from memory, not peripheral

Section 10.3.3 has this useful bit of information:

The first transfer address is the one programmed in the DMA_CPARx/DMA_CMARx registers. During transfer operations, these registers keep the initially programmed value. The current transfer addresses (in the current internal peripheral/memory address register) are not accessible by software.

This suggests that I only need to set these at the start and shouldn't need to touch them again.

To set these:

  DMA1_Channel5->CMAR = (uint32_t) image;       // where to read from
  DMA1_Channel5->CPAR = (uint32_t) &(SPI2->DR); // where to write to

Time to try it out... and... nothing!  Maybe there's another clock setting for DMA, and sure enough there is:

  rccEnableAHB(RCC_AHBENR_DMA1EN, 0); // Enable DMA clock, run at SYSCLK

I still haven't got anything, so I tried setting the source and destination registers each time before I start a DMA transfer. It looks like now I get a single transfer, but I'm trying to get a transfer on every hsync.

I poked around with the debugger, especially at 0x40026058 which is DMA5->CCR1 (I calculated the address from values in stm32l1xx.h), and noticed that the Enable flag is still set.  Maybe it has to be toggled each time?  Now I get a square wave instead of my data... I then tried decreasing my hsync timer, and decreasing the SPI speed, and I got a reasonable output.  I'm getting some nasty aliasing on my DSO Nano though, maybe I should have borrowed a faster scope!  I think I was triggering the DMA transfers too quickly, which produced that square wave.  Conveniently, I notice the SPI line is now low when it's idle, which is the output I want.  I'm not sure why it's gone low, but I'm not complaining.

So to sum up:

  rccEnableAHB(RCC_AHBENR_DMA1EN, 0); // Enable DMA clock, run at SYSCLK
  // Configure DMA
  DMA1_Channel5->CCR = DMA_CCR5_PL // very high priority
          | DMA_CCR5_MINC  // memory increment mode
          | DMA_CCR5_DIR;  // read from memory, not peripheral
  DMA1_Channel5->CMAR = (uint32_t) image;       // where to read from
  DMA1_Channel5->CPAR = (uint32_t) &(SPI2->DR); // where to write to
...
  SPI2->CR2 = SPI_CR2_SSOE | SPI_CR2_TXDMAEN;

then in my hsync handler:

    // Activate the DMA transfer
    DMA1_Channel5->CCR &= ~DMA_CCR5_EN;
    DMA1_Channel5->CNDTR = sizeof(image);
    DMA1_Channel5->CCR |= DMA_CCR5_EN;

I didn't need to reset CMAR and CPAR after all.

I think that's now demonstrated everything I need for the video signal generator! My code needs a big cleanup, and I'd like to use ChibiOS functions where I can (palSetPadMode instead of messing around with memory locations and data structures, etc).