Optocom – part 3 – rework & project structure

You know how it goes. If you design a PCB, you’ll always have to rework the first revision. That is also the case for my Optocom PCB.

I didn’t read the reference manual for the STM32F103 properly, so I just haphazardly connected my encoder A/B lines to timer channels. But, in order to use quadrature mode on this part, you are restricted to timer channels 1 and 2. Luckily the column encoder was connected to TIM3 channels 1 and 2, but for the head encoder I connected A and B to channels 3 and 2. The Z index was connected to the pin used for channel 1, so that makes 2 bodges to swap Z and A.

For the motor drivers I connected the pins to non-timer outputs, but since the DRV8871 needs PWM inputs it has to be connected to a timer with PWM capability. My mistake was the assumption that I just needed to apply a voltage to the drivers, which works, but then you can’t implement speed control. That makes 4 additional bodges.

I chose to bodge the driver’s IN1 and IN2’s to attach to TIM1 channels 1 to 4, but the pins for these were occupied by some switches from the turret and limit positions. So that makes 4 more bodges to remap those as well.

Luckily I recently bought a new soldering iron, a Geeboon JBC clone station with 2 handles for €100. What a breath of fresh air compared to the crappy irons I had before. The end result is this:

With the original PCB for scale. 50% reduction! Note the stash of angel hair, also coming from the bottom, those are the required bodges.

When starting the project, the HSE clock didn’t want to come up. I put an unpopulated pad on the OSCIN pin of the oscillator, for reasons unknown. Dropping a solder bridge onto the pad fixed that problem as well.

To summarize the PCB:

  • XL1509, to make 12v out of the 24v input for the motors and encoders
  • AMS1117-3.3 to make 3v3 out of 12v for the MCU and ICs
  • DRV8871, 2x, one for the column motor and one for the head motor
  • SP3485 RS485 transceivers, 2x, for full duplex operation
  • UCC27424 MOSFET driver to drive a IRLZ44N
  • B57127 NTC

On to the firmware. I decided to ask Copilot Pro, which I received a license for to to try out, to write the skeleton for the project. Remind me to never do that again. AI might be promising, but having it write your projects requires you to do so much rework it’s almost not worth your time.

Structure

󰪢 0s 󰉋 ~/source/optograin-enlarger 󰘬 main
    tree -d -L 3
.
├── build
├── cmake
| └── toolchain.cmake
├── lib
│   └── SeggerRTT
│   └── OptoComm
│   └── protobuf
├── optograin
│   ├── inc
│   └── src
| └── main.c
| └── motion.c
| └── exposure.c
| └── communication.c
├── scripts
| └── size_mcu.sh
└── sys
├── bsp
│   ├── inc
│   └── src
| └── board.c
| └── encoder.c
| └── lamp.c
| └── switch.c
| └── motor.c
| └── syscalls.c
| └── uart.c
└── stm32-hal
└── STM32CubeF1

This is the structure I use for pretty much every project. lib contains reusable blocks. sys contains the vendor drivers, which are glued to the board via bsp. The head source folder is named after the project, optograin in this case. It makes use of the glue routines in bsp and libraries from lib to implement the firmware.

cmake

toolchain.cmake contains basic toolchain config I use for most embedded projects, tuned to the specific MCU. Most of it is pretty standard, but I like to use the following CMake default flags:

CMake
set(CMAKE_C_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON)

set(COMMON_FLAGS      "-ffreestanding -mcpu=${TARGET_CPU} -mthumb -fmax-errors=5 -msoft-float -mfloat-abi=soft")
set(WARN_FLAGS        "-Wall -Wextra -Wpointer-arith -Wformat -Wno-unused-local-typedefs -Wno-unused-parameter -Wfloat-equal \
                       -Wshadow -Wwrite-strings -Wcast-qual -Wstrict-prototypes -Wmissing-prototypes -Wconversion")
set(C_FLAGS           "-ffunction-sections -fdata-sections -fno-common")

set(CMAKE_C_FLAGS_INIT          "${COMMON_FLAGS} ${C_FLAGS} ${WARN_FLAGS} -MMD -MP" CACHE STRING "" FORCE)
set(CMAKE_ASM_FLAGS_INIT        "${COMMON_FLAGS} -x assembler-with-cpp"        CACHE STRING "" FORCE)
set(CMAKE_EXE_LINKER_FLAGS_INIT "-nostartfiles -Wl,--gc-sections -Wl,--print-memory-usage -Wl,--defsym=_init=0 -Wl,--defsym=_fini=0 --specs=nano.specs --specs=nosys.specs"    CACHE STRING "" FORCE)

The F103 doesn’t have a FPU, but if there’s one I usually choose to use softfp to keep compatibility with the softfloat ABI while still leveraging the FPU. Most of the MCUs I work have limited storage and don’t have a FPU. If float work is necessary I usually prefer to use fixed point arithmetic. For this project, the heavy lifting (e.g. autofocus calculations) will be performed by the control box so we don’t need any float stuff here.

lib

I prefer working with modular building blocks. If I use an external library, it goes into lib. If I write something that can be reused, it goes into lib. Everything here is built as a static library and included as such from the main project.

One set of external code I almost always include is Segger RTT. This allows me to send and receive debug output from and to the MCU without having to use a UART peripheral, and comes with the added bonus of high-speed transfer without having to wait on the peripheral.

OptoComm

OptoComm is what I’m going to call my communication lib. It will use protobuf for encapsulation. The data format is TBD, but it’ll probably consist of <start char> <header> <data> <CRC32>. It’ll run on both sides, in the enlarger and in the control box.

optograin

I like to put my project-specific source files in a folder named after the project. Headers go to inc, sources to src. Each functional block gets its own file: one for motion (motion.c), one for lamp control and lens selection (exposure.c) and one for structuring comms to and from the MCU (communication.c).

sys

sys is where the BSP and the vendor drivers live. For ST, I always use the LL implementations, as I find that the HAL sources suffer from a lack of transparency of what’s going on, which makes it hard to debug issues.

stm32-hal

I prefer to add ST’s entire Cube repo for the MCU family I’m working with as a submodule, but paired with my own CMakeLists.txt that selects only the LL drivers and required CMSIS stuff. It almost always looks something like this:

CMake
add_library(stm32f1xx_ll
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_adc.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_crc.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_dac.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_dma.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_exti.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_gpio.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_i2c.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_pwr.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_rcc.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_rtc.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_spi.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_tim.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_usart.c
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_utils.c
    STM32CubeF1/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c

    startup_f103c8t6.s
)

add_library(stm32f1xx_cmsis INTERFACE)

target_include_directories(stm32f1xx_ll PUBLIC
    STM32CubeF1/Drivers/STM32F1xx_HAL_Driver/Inc
)

target_include_directories(stm32f1xx_cmsis INTERFACE
    STM32CubeF1/Drivers/CMSIS/Device/ST/STM32F1xx/Include
    STM32CubeF1/Drivers/CMSIS/Include
)

# don't enforce warnings on external libraries
target_compile_options(stm32f1xx_ll PRIVATE
    -w  # -w disables all gcc warnings
)

target_link_libraries(stm32f1xx_ll PUBLIC
    stm32f1xx_cmsis
)

# Specify the linker script
target_link_options(stm32f1xx_ll INTERFACE
    -T${CMAKE_CURRENT_SOURCE_DIR}/STM32CubeF1/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/linker/STM32F103XB_FLASH.ld
)

I also like using a simplified startup script that skips all the __libc_init stuff required for constructors and destructors, as I always use plain C:

ASM
.syntax unified
.cpu cortex-m3
.fpu softvfp
.thumb

.global g_pfnVectors
.global Default_Handler

.word _sidata
.word _sdata
.word _edata
.word _sbss
.word _ebss

.equ BootRAM, 0xF108F85F

.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
    bl SystemInit

    ldr r0, =_sdata
    ldr r1, =_edata
    ldr r2, =_sidata
    movs r3, #0
    b LoopCopyDataInit

CopyDataInit:
    ldr r4, [r2, r3]
    str r4, [r0, r3]
    adds r3, r3, #4

LoopCopyDataInit:
    adds r4, r0, r3
    cmp r4, r1
    bcc CopyDataInit

    ldr r2, =_sbss
    ldr r4, =_ebss
    movs r3, #0
    b LoopFillZerobss

FillZerobss:
    str r3, [r2]
    adds r2, r2, #4

LoopFillZerobss:
    cmp r2, r4
    bcc FillZerobss

    bl main
    bx lr
.size Reset_Handler, .-Reset_Handler

.section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
    b Infinite_Loop
.size Default_Handler, .-Default_Handler

/* vectors go here */
bsp

This folder contains everything related to board support, mainly glue to tape ST’s peripherals to my specific board implementation. board.c contains the board configuration, with pin definitions and such. Every logical device gets its own BSP file, so we have an encoder file, a motor file, a lamp file, etc/

Leave a Comment