## Code to produce video signals on the STM32L Discovery

I’ve finally coded the program that produces a video signal.  There weren’t too many surprises.

One issue I had was that there was that occasionally a line would be drawn slightly to the right.  I changed the interrupt priority from 7 to 0, and disabled ChibiOS’ thread preemption. Probably only changing the interrupt will do, but I had no trouble after that.

A bigger problem is that any vertical line wriggles around on the screen a lot.  I don’t think there’s anything in my code that might cause this.  I came across a question on the STM site, which suggests that an instruction must complete before an interrupt will be triggered.  If the CPU happens to be running a long instruction, the line produced by the following interrupt will be shifted over slightly.  The Arduino TV-Out library doesn’t have this problem, even though the AVRs instructions can take different amounts of time.  I’m not sure what I can do about this – the timer should still be correct, so maybe I’d need a busy-wait loop while waiting for the timer to hit a particular value.  It looks like the TV-Out library does this.  It might need some assembler, which I don’t plan to learn right now (but maybe check this ST forum post).  But the author of the RBox suggests it’s because of the wait states.  I don’t know why there would be anything non-deterministic with wait states, unless there was caching involved, which I don’t think the STM32 has.  I suspect it’s ChibiOS running stuff during the timing interrupt, which would cause jitter.

I don’t plan to do any more work on this program, since it’s served its task of teaching me about ARM processors.  It has potential to show data its capturing or interacting with an operator.  There’s plenty of memory for a framebuffer for its its 400×288 display, It should be fairly easy to port the TVOut library to it, to add graphics and text rendering capabilities.  The advantage of the ARM chip is that because it uses DMA to write to the screen, the CPU is doing almost nothing while it’s displaying an image.  An AVR needs to work hard while a line is being drawn.

I’ve seen one project where an ARM chip produced colour signals.  The CPU didn’t have DMA though, but was faster than the STM32L.  The Freescale Freedom board looks like a good target (although ChibiOS doesn’t support it yet).  I was thinking about the way that 2D polygons are drawn, and I think it might be possible to render a number of 3D polygons with occlusion as each line is being drawn.  The unusual part of this rendering is that instead of the frame rate slowing when the CPU was busy, the vertical resolution would decrease instead as the previous line keeps getting rendered as a new line is being drawn.

Generating video signals like is is nifty, but maybe a bit pointless since there are chips around with composite output anyway.  The OLinuXino iMX233 would be ideal for this, as its CPU has a complete reference manual available.  It’s designed for running Linux, but some low level programming like I did here would provide an “instant-on” function.  The same could be done with the Raspberry Pi, but since there’s no user manual available, you’d need to rely on its limited documentation and Linux drivers.  I like the idea of porting the RTEMS operating system to the OLinuXino, since that OS provides a POSIX API and BSD networking, so porting other applications would be easier.

Here’s a video of my results.  My code is here: https://github.com/33d/stm32-video

## Video signal output circuitry

I’ll need a simple circuit to mix my video signals together. The Arduino TV Out library shows how to do this, but that works with 5V IOs, but the STM32L Discovery (and all ARM chips AFAIK) uses 3.3V.

So which resistors will I need?  To produce a white signal, the sync and video lines will be high.  The equivalent circuit looks like this:

(The 75Ω resistor is the resistance inside the TV).

To show a black signal, the sync line will be high, and the video line will be low, which looks like this:

Wikipedia gives the formula for a voltage divider, so the resistors in the first diagram can be calculated with this formula:

$1= {75 \over {75+{1 \over { {1 \over V} + {1 \over S} }}}}\times 3.3$

and in the second:

$0.3= {{1 \over {{1 \over V} + {1 \over 75}}} \over {1 \over {{1 \over V} + {1 \over 75}}} + S} \times 3.3$

I tried solving these, but that’s well beyond my mathematical ability.  Instead I found some online site that could plot the two formulas (edit: I could have used Wolfram Alpha).  The lines crossed at about RV=250Ω and RS=580Ω. These resistor values don’t exist, so RV=270Ω and RS=560Ω is close enough.  They seem to work fine in the circuit.

## DMA on the STM32L Discovery

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).

## Picture data using SPI

I plan to use SPI to send the picture data for my video generator.

First I need to work out what speed to run the port at.  Each line goes for 52 μs, or 1664 cycles.  I could divide this by 4 for 416 pixels per line or 8 for 208 per line.  This sets the baud rate, so I shouldn’t need to divide this by 8 again to get a bytes per second speed.  It looks (from the clock registers) like SPI1 is connected to the APB2 clock, and SPI2 is connected to APB1.  I’m already running APB1 at the system clock (32MHz), so I’d like to use that if I can.  The speed is set in the CR1 register, by the BR bits, which supports dividing by 4 or 8.  I might as well use SPI2.  The datasheet says that SPI2_MOSI can only be on pin B15.  I won’t need the clock output, so I won’t configure a pin for that.

The CR1 register contains a setting for 8 or 16-bit operation.  This affects the size of the data being written.  Since I plan to use DMA I’ll leave it at 8 bits.

It turns out there are very few settings to get SPI working.  I had to stuff around a lot before I got it working though – eventually I copied the ChibiOS code, and set the SPI_CR1_CPOL, SPI_CR1_SSM, SPI_CR1_SSI and SPI_CR2_SSOE flags even though I wouldn’t have thought I need them, and it suddenly worked!

This was enough to get SPI working:

  rccEnableAPB1(RCC_APB1ENR_SPI2EN, 0); // Enable SPI2 clock, run at SYSCLK
PAL_STM32_OSPEED_HIGHEST);           /* MOSI.    */
SPI2->CR1 = //SPI_CR1_BR_0 // divide clock by 4
SPI_CR1_CPOL | SPI_CR1_SSM | SPI_CR1_SSI |
SPI_CR1_BR // divide clock by 256
| SPI_CR1_MSTR;  // master mode
SPI2->CR2 = SPI_CR2_SSOE;
SPI2->CR1 |= SPI_CR1_SPE; // Enable SPI

To send data, write bytes to SPI2->DR.  The output appears on PB15.  I think in the future I’ll try using palSetPadMode for configuring the pins, since it’s better than the 8 lines of code I’ve been using previously to do this.  The above code divides the clock by 256 so I could see the output on my DSO Nano, but I’ll change this to 4 later.

The next step will be using DMA to write the data to SPI instead.

## More on the vsync interrupts

I had a thought after my previous disheartening attempt to get the vsync working: since they might have stuffed up timer 11 in ChibiOS, I might change to timer 4.  One massive advantage is that timers 2, 3 and 4 have 4 compare registers, instead of one!  This means I can use one to turn off the vsync pulse, and one to trigger the DMA interrupt and adjust the sync timings on the next line.  Changing everything to use TIM4 was fairly straightforward, the only tricky part being changing the single compare register to use compare register 4, and enabling APB1 instead of APB2 since that’s where timer 4 is.  (I originally used register 1, but that is connected to PORTB6 which is attached to the blue LED.)

There was one catch though when compiling:

$make Compiling main.c Linking build/ch.elf build/obj/main.o: In function VectorB8': /home/damien/projects/stm32/video/main.c:8: multiple definition of VectorB8' build/obj/pwm_lld.o:/opt/ChibiOS_2.4.2/os/hal/platforms/STM32/pwm_lld.c:221: first defined here Looking in pwm_lld.c suggests that I need to unset STM32_PWM_USE_TIM4. I notice that one example has another configuration file: /demos/ARMCM3-STM32L152-DISCOVERY/mcuconf.h, which declares this symbol. I copied this file to my project directory, hoping the makefile would use it in preference. Now there were no timers available for ChibiOS, its PWM module made the compiler complain because it had no timers to use, so I had to set HAL_USE_PWM to FALSE in halconf.h. The good news: my interrupt is being called! I connected the debugger and used the “p” command to show the contents of “line”. The bad news: my PB7 light isn’t blinking, which means the ChibiOS main thread isn’t working. Maybe the interrupt is using all of the CPU? It shouldn’t, since I thought each line took about 1000 cycles to run, and the interrupt shouldn’t be using more than about 50. I remember seeing somewhere that ARMs don’t reset their interrupt flags automatically, like AVRs do. If this is the case, my interrupt will return, and the NVIC (the “nested vectored interrupt controller”, apparently) will see the flag is still set, and call the interrupt again. The interrupt handler in ChibiOS’ pal_lld.c contains this line, which would clear this flag: STM32_TIM1->SR = ~TIM_SR_UIF; In my case, this would be: TIM4->SR &= ~TIM_SR_UIF; and my light blinks again, so that seems to have worked! I checked with gdb that the “line” variable is still incrementing. I’ll try setting the PWM duration registers during the interrupt, so my interrupt handler looks like this:  if (line & 1) { TIM4->ARR = STM32_SYSCLK * 0.0001; // horizontal line duration TIM4->CCR4 = STM32_SYSCLK * 0.00009; // hsync pulse duration } else { TIM4->ARR = STM32_SYSCLK * 0.000064; // horizontal line duration TIM4->CCR4 = STM32_SYSCLK * 0.0000047; // hsync pulse duration } That seems to have worked fine. That’s about everything I need to generate the vsync signals. I’ve done something slightly wrong though – I’d be better off using one of the compare registers to trigger the interrupt instead. In the interrupt handler, I’d initiate a DMA transfer for the current line, then set the timing registers for the next line. Now my code looks like this (maybe I should start putting it on Github): #include "ch.h" #include "hal.h" #include "stm32l1xx.h" volatile int line; CH_IRQ_HANDLER(TIM4_IRQHandler) { TIM4->SR &= ~TIM_SR_UIF; ++line; if (line & 1) { TIM4->ARR = STM32_SYSCLK * 0.0001; // horizontal line duration TIM4->CCR4 = STM32_SYSCLK * 0.00009; // hsync pulse duration } else { TIM4->ARR = STM32_SYSCLK * 0.000064; // horizontal line duration TIM4->CCR4 = STM32_SYSCLK * 0.0000047; // hsync pulse duration } } int main(void) { halInit(); chSysInit(); rccEnableAPB1(RCC_APB1ENR_TIM4EN, 0); // Enable TIM4 clock, run at SYSCLK nvicEnableVector(TIM4_IRQn, CORTEX_PRIORITY_MASK(7)); // TIM11 outputs on PB6 GPIOB->OTYPER &= ~GPIO_OTYPER_OT_9; // Push-pull output GPIOB->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR9; // 40MHz GPIOB->PUPDR &= ~GPIO_PUPDR_PUPDR9; GPIOB->PUPDR |= GPIO_PUPDR_PUPDR9_0; // Pull-up GPIOB->MODER &= ~GPIO_MODER_MODER9; GPIOB->MODER |= GPIO_MODER_MODER9_1; // alternate function on pin B9 // Reassign port B9 GPIOB->AFRH &= ~GPIO_AFRH_AFRH9; GPIOB->AFRH |= 0x2 << 4; // ChibiOS doesn't seem to have constants for these TIM4->CR1 |= TIM_CR1_ARPE; // buffer ARR, needed for PWM (?) TIM4->CCMR2 &= ~(TIM_CCMR2_CC4S); // configure output pin TIM4->CCMR2 = TIM_CCMR2_OC4M_2 | TIM_CCMR2_OC4M_1 /*| TIM_CCMR1_OC1M_0*/ // output high on compare match | TIM_CCMR2_OC4PE; // preload enable TIM4->CCER = TIM_CCER_CC4P // active low output | TIM_CCER_CC4E; // enable output TIM4->DIER = TIM_DIER_UIE; // enable interrupt on "update" (ie. overflow) TIM4->ARR = STM32_SYSCLK * 0.000064; // horizontal line duration TIM4->CCR4 = STM32_SYSCLK * 0.0000047; // hsync pulse duration TIM4->CR1 |= TIM_CR1_CEN; // enable the counter palSetPadMode(GPIOB, 7, PAL_MODE_OUTPUT_PUSHPULL); while (1) { palSetPad(GPIOB, 7); chThdSleepMilliseconds(500); palClearPad(GPIOB, 7); chThdSleepMilliseconds(500); } } Next I’ll try using DMA, which I’ve never used before. With any luck I’ll be able to use ChibiOS to do this. ## Starting on the vsync interrupts Now it’s time to adjust the sync signals to get vsync working, so I need an interrupt when the signal goes low so I can adjust the sync pulse on the next cycle. I have no idea how to start the interrupts. I did a search in the ChibiOS code for the term “vect”, and found a bunch of them in hal/platforms/STM32L1xx/hal_lld.h, of which TIM11_IRQHandler looks the most appropriate. But there seems to be only one vector all possible events, like an overflow or a compare match. It looks like “fast” interrupt handlers look like this: volatile int line; CH_IRQ_HANDLER(TIM11_IRQHandler) { ++line; } It compiles at least… but did it register as an interrupt handler? I tried the trick with AVRs that shows disassembled code: arm-none-eabi-objdump -S -h build/ch.elf It shows that the first section starts at address 0x08000100… just after the vector table, which should appear at the start! Right at the top though, there’s a “startup” section at the correct address. After stuffing around with various objdump options, this showed me the vector table: arm-none-eabi-objdump -h -s --special-syms build/ch.elf Table 34 of the reference manual lists all of the interrupts, and TIM11 is at offset 0xAC. Objdump doesn’t show anything promising in this location! But there’s something strange in hal_lld.h: #define TIM9_IRQHandler VectorA0 /**< TIM9. */ #define TIM10_IRQHandler VectorA4 /**< TIM10. */ #define TIM11_IRQHandler VectorA8 /**< TIM11. */ #define LCD_IRQHandler VectorAC /**< LCD. */ The datasheet though says that the LCD is vector A0, TIM9 is A4, 10 is A8 and 11 is AC. I guess the way to find out is to try both and see what happens. First the timer needs to be configured to use those interrupts.  TIM11->DIER = TIM_DIER_UIE; // enable interrupt on "update" (ie. overflow) So does this do anything? I used the debugger to find out: $ arm-none-eabi-gdb build/ch.elf
....
(gdb) p lineA8
$1 = 0 (gdb) p lineAC$2 = 0
(gdb) cont
^C
at /opt/ChibiOS_2.4.2/os/kernel/src/chsys.c:62
(gdb) p lineAC
$3 = 0 (gdb) p lineA8$4 = 0
(gdb)

So it’s doing nothing!  Why not? I looked through the ChibiOS sources where the timers are configured, and found a call to nvicEnableVector, which looks promising.  It needs a “priority” parameter though, so what should that be?  gpt_lld.h lists some priorities.  ChibiOS always sends the priorities through a macro called CORTEX_PRIORITY_MASK, but that seems to move the number to the correct place in a register.  The CPU manual (section 5.3) says that lower numbers have higher priority, and this needs to be very high, so I’ll choose 2.

  nvicEnableVector(TIM11_IRQn, CORTEX_PRIORITY_MASK(2));

It doesn’t like that much – the light doesn’t blink, so it looks like the chip crashed!  I’m starting to get a bit annoyed with my slow progress; maybe I’ll try to rewrite my code using ChibiOS as much as I can.  While this chip is powerful, its also very complicated, which makes me think it’s not that practical to program it directly.  I soldiered on anyway

## Coding PWM on the STM32L

So how about coding up the PWM to produce the hsync signal from my previous post?  Here’s my first attempt:

#include "ch.h"
#include "hal.h"
#include "stm32l1xx.h"

int main(void) {
halInit();
chSysInit();

rccEnableAPB2(RCC_APB2ENR_TIM11EN, 0); // Enable TIM11 clock, run at SYSCLK

// TIM11 outputs on PA7, PB9 or PB15
GPIOB->MODER &= ~GPIO_MODER_MODER9;
GPIOB->MODER |= GPIO_MODER_MODER9_1; // alternate function on pin B9
TIM11->CCR1 = TIM_CR1_ARPE // buffer ARR, needed for PWM (?)
| TIM_CR1_CEN; // counter enable... proably important!
TIM11->CCMR1 &= ~(TIM_CCMR1_CC1S); // configure output pin
TIM11->CCMR1 =
TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_0  // output high on compare match
TIM11->CCER = TIM_CCER_CC1P // active low output
| TIM_CCER_CC1E; // enable output
TIM11->ARR = STM32_SYSCLK * 0.000064;   // horizontal line duration
TIM11->CCR1 = STM32_SYSCLK * 0.0000047; // hsync pulse duration

TIM11->CR1 |= TIM_CR1_CEN; // enable the counter

while (1) {
chThdSleepMilliseconds(500);
chThdSleepMilliseconds(500);
}
}

I’ll attach my DSO Nano to PB9, and… nothing!  I don’t think it’s the DSO; that should be good to a few μs.

So why isn’t it running? Maybe the timer is running, but the pin output isn’t.  I started OpenOCD, and attached GDB to it.  The datasheet says that TIM11 is at 0x40011000, and the reference manual says the counter is at offset 0x24.  With this information, I can look at the contents of the timer – the GDB manual says I can look at memory using the “x” command:

\$ arm-none-eabi-gdb build/ch.elf
GNU gdb (GNU Tools for ARM Embedded Processors) 7.3.1.20120613-cvs
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=i686-linux-gnu --target=arm-none-eabi".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
(gdb) target remote :3333
Remote debugging using :3333
ResetHandler () at /opt/ChibiOS_2.4.2/os/ports/GCC/ARMCMx/crt0.c:262
262      asm volatile ("cpsid   i");
(gdb) cont
Continuing.
^C
at /opt/ChibiOS_2.4.2/os/kernel/src/chsys.c:62
(gdb) x 0x40011024
0x40011024:    0x000002ed
(gdb) cont
Continuing.
^C
at /opt/ChibiOS_2.4.2/os/kernel/src/chsys.c:62
(gdb) x 0x40011024
0x40011024:    0x0000067c
(gdb) x 0x40011024
0x40011024:    0x000005b8
(gdb) x 0x40011024
0x40011024:    0x0000050c
(gdb) x 0x40011024
0x40011024:    0x00000774
(gdb)

The value is changing – that’s a good start! Note that I didn’t have to continue the program to see the timer incrementing – it runs even though the debugger has stopped execution! Also, the timer never seems to set the top 4 bits, which suggests it’s resetting at some value before it overflows its 16 bits. This maximum value should be 32MHz*64μs=0x800, so it’s looking good.

There’s an application note discussing the timers in the STM32 chips, and reading that closely it looks like I’ve been confusing the “output compare” mode and the PWM mode.  I think in the AVR these are essentially the same, but in the STM32 there doesn’t look like there’s a way to reset the pin upon overflow when in output compare mode (so this mode doesn’t seem to be that useful to me, apart from one-shot events).  The ChibiOS PWM code is worth looking at too.

A few days have passed since I did the above bit…

I read the reference on GPIOs a bit more. I would have guessed that since I told the chip to connect to the timer, the timer would look after that pin. But after reading section 6.3.2 of the datasheet about configuring pins for the alternate function, it looks like there’s a AFR register to set. Figure 18 suggests I need to set the AFRH register to “3” to enable the timer on this pin. I added these lines:

  GPIOB->AFRH &= ~GPIO_AFRH_AFRH9;
GPIOB->AFRH |= 0x3 << 4; // ChibiOS doesn't seem to have constants for these

and finally I get some output! The timing looks right, but there’s some strange stuff happening with the amplitude of this waveform. I’m hoping it’s aliasing with my DSO, so hopefully I’ll get to use a CRO in a few days to confirm this. I made the timings a bit longer, and it didn’t look as bad, but maybe there’s some other aliasing going on with the clocks in the chip.

Another problem is that the waveform is the wrong way around! There seem to be two settings that affect this: the OC1M bits in CCMR1, which say whether the waveform is active when the counter is less than the compare register; the other specifies the polarity of the output. Maybe I only have to change one of these? It seems odd to have two registers which do mostly the same thing. I’ll change the polarity in CC1P.

That’s looking better! I swapped CCER and OC1M around, and the output looked the same.

I noticed that the signal doesn’t rise very quickly. Looking through the GPIO registers again, I got the OTYPER setting wrong which made it open drain. Changing this around fixed this problem.

So here’s my complete code:

  rccEnableAPB2(RCC_APB2ENR_TIM11EN, 0); // Enable TIM11 clock, run at SYSCLK

// TIM11 outputs on PA7, PB9 or PB15
GPIOB->OTYPER &= ~GPIO_OTYPER_OT_9;        // Push-pull output
GPIOB->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR9; // 40MHz
GPIOB->PUPDR &= ~GPIO_PUPDR_PUPDR9;
GPIOB->PUPDR |= GPIO_PUPDR_PUPDR9_0;      // Pull-up
GPIOB->MODER &= ~GPIO_MODER_MODER9;
GPIOB->MODER |= GPIO_MODER_MODER9_1; // alternate function on pin B9

// Reassign port B9
GPIOB->AFRH &= ~GPIO_AFRH_AFRH9;
GPIOB->AFRH |= 0x3 << 4; // ChibiOS doesn't seem to have constants for these   TIM11->CR1 |= TIM_CR1_ARPE; // buffer ARR, needed for PWM (?)
TIM11->CCMR1 &= ~(TIM_CCMR1_CC1S); // configure output pin
TIM11->CCMR1 =
TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 // output high on compare match
TIM11->CR1 |= TIM_CR1_CEN; // enable the counter