diff options
author | jaseg <git@jaseg.net> | 2018-04-17 11:56:09 +0200 |
---|---|---|
committer | jaseg <git@jaseg.net> | 2018-04-17 11:56:09 +0200 |
commit | ddbb807e54245bd5b6686096827f4124ef196957 (patch) | |
tree | 1643ca05e0b3cf7c06fbf743627f620290ebb4d3 | |
parent | 545abc6b6fccd1de4c8e4b95b83a453ce31c90ac (diff) | |
download | olsndot-ddbb807e54245bd5b6686096827f4124ef196957.tar.gz olsndot-ddbb807e54245bd5b6686096827f4124ef196957.tar.bz2 olsndot-ddbb807e54245bd5b6686096827f4124ef196957.zip |
Move to new serial protocol.
This code is tested.
-rw-r--r-- | firmware/Makefile | 10 | ||||
-rw-r--r-- | firmware/adc.c | 101 | ||||
-rw-r--r-- | firmware/adc.h | 30 | ||||
-rw-r--r-- | firmware/global.h | 13 | ||||
-rw-r--r-- | firmware/mac.c | 3 | ||||
-rw-r--r-- | firmware/mac.h | 22 | ||||
-rw-r--r-- | firmware/main.c | 135 | ||||
-rw-r--r-- | firmware/serial.c | 214 | ||||
-rw-r--r-- | firmware/serial.h | 67 |
9 files changed, 478 insertions, 117 deletions
diff --git a/firmware/Makefile b/firmware/Makefile index 909cf8d..1e668e1 100644 --- a/firmware/Makefile +++ b/firmware/Makefile @@ -3,12 +3,14 @@ CMSIS_PATH ?= STM32Cube/Drivers/CMSIS CMSIS_DEV_PATH ?= $(CMSIS_PATH)/Device/ST/STM32F0xx HAL_PATH ?= STM32Cube/Drivers/STM32F0xx_HAL_Driver +MAC_ADDR ?= 0xDEBE10BB + CC := arm-none-eabi-gcc OBJCOPY := arm-none-eabi-objcopy OBJDUMP := arm-none-eabi-objdump SIZE := arm-none-eabi-size -CFLAGS = -Wall -g -std=gnu11 -Os +CFLAGS = -Wall -Wpedantic -Wstrict-aliasing -g -std=gnu11 -Os CFLAGS += -mlittle-endian -mcpu=cortex-m0 -march=armv6-m -mthumb #CFLAGS += -ffunction-sections -fdata-sections -Wl,--gc-sections CFLAGS += -Wl,-Map=main.map @@ -16,7 +18,7 @@ CFLAGS += -Wl,-Map=main.map # Technically we're using an STM32F030F4, but apart from the TSSOP20 package that one is largely identical to the # STM32F030*6 and there is no separate device header provided for it, so we're faking a *6 device here. This is # even documented in stm32f0xx.h. Thanks ST! -CFLAGS += -DSTM32F030x6 -DHSE_VALUE=25000000 +CFLAGS += -DSTM32F030x6 -DHSE_VALUE=25000000 -DMAC_ADDR=$(MAC_ADDR) CFLAGS += -Tstm32_flash.ld CFLAGS += -I$(CMSIS_DEV_PATH)/Include -I$(CMSIS_PATH)/Include -I$(HAL_PATH)/Inc -Iconfig @@ -31,7 +33,7 @@ all: main.elf cmsis_exports.c: $(CMSIS_DEV_PATH)/Include/stm32f030x6.h $(CMSIS_PATH)/Include/core_cm0.h python3 gen_cmsis_exports.py $^ > $@ -sources.tar.xz: main.c Makefile +sources.tar.xz: main.c serial.h global.h serial.c adc.h adc.c Makefile tar -cf $@ $^ # don't ask... @@ -41,7 +43,7 @@ sources.tar.xz.zip: sources.tar.xz sources.c: sources.tar.xz.zip xxd -i $< | head -n -1 | sed 's/=/__attribute__((section(".source_tarball"))) =/' > $@ -main.elf: main.c startup_stm32f030x6.s system_stm32f0xx.c $(HAL_PATH)/Src/stm32f0xx_ll_utils.c cmsis_exports.c sources.o +main.elf: main.c mac.c adc.c serial.c startup_stm32f030x6.s system_stm32f0xx.c $(HAL_PATH)/Src/stm32f0xx_ll_utils.c cmsis_exports.c $(CC) $(CFLAGS) -o $@ $^ $(OBJCOPY) -O ihex $@ $(@:.elf=.hex) $(OBJCOPY) -O binary $@ $(@:.elf=.bin) diff --git a/firmware/adc.c b/firmware/adc.c new file mode 100644 index 0000000..02517a7 --- /dev/null +++ b/firmware/adc.c @@ -0,0 +1,101 @@ +/* Megumin LED display firmware + * Copyright (C) 2018 Sebastian Götte <code@jaseg.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stm32f0xx.h> +#include <stdint.h> +#include <system_stm32f0xx.h> +#include <stm32f0xx_ll_utils.h> +#include <math.h> + +#include "adc.h" + +volatile int16_t adc_vcc_mv = 0; +volatile int16_t adc_temp_celsius = 0; + +static volatile uint16_t adc_buf[2]; + +void adc_init(void) { + /* The ADC is used for temperature measurement. To compute the temperature from an ADC reading of the internal + * temperature sensor, the supply voltage must also be measured. Thus we are using two channels. + * + * The ADC is triggered by compare channel 4 of timer 1. The trigger is set to falling edge to trigger on compare + * match, not overflow. + */ + ADC1->CFGR1 = ADC_CFGR1_DMAEN | ADC_CFGR1_DMACFG | (2<<ADC_CFGR1_EXTEN_Pos) | (1<<ADC_CFGR1_EXTSEL_Pos); + /* Clock from PCLK/4 instead of the internal exclusive high-speed RC oscillator. */ + ADC1->CFGR2 = (2<<ADC_CFGR2_CKMODE_Pos); + /* Use the slowest available sample rate */ + ADC1->SMPR = (7<<ADC_SMPR_SMP_Pos); + /* Internal VCC and temperature sensor channels */ + ADC1->CHSELR = ADC_CHSELR_CHSEL16 | ADC_CHSELR_CHSEL17; + /* Enable internal voltage reference and temperature sensor */ + ADC->CCR = ADC_CCR_TSEN | ADC_CCR_VREFEN; + /* Perform ADC calibration */ + ADC1->CR |= ADC_CR_ADCAL; + while (ADC1->CR & ADC_CR_ADCAL) + ; + /* Enable ADC */ + ADC1->CR |= ADC_CR_ADEN; + ADC1->CR |= ADC_CR_ADSTART; + + /* Configure DMA 1 Channel 1 to get rid of all the data */ + DMA1_Channel1->CPAR = (unsigned int)&ADC1->DR; + DMA1_Channel1->CMAR = (unsigned int)&adc_buf; + DMA1_Channel1->CNDTR = sizeof(adc_buf)/sizeof(adc_buf[0]); + DMA1_Channel1->CCR = (0<<DMA_CCR_PL_Pos); + DMA1_Channel1->CCR |= + DMA_CCR_CIRC /* circular mode so we can leave it running indefinitely */ + | (1<<DMA_CCR_MSIZE_Pos) /* 16 bit */ + | (1<<DMA_CCR_PSIZE_Pos) /* 16 bit */ + | DMA_CCR_MINC + | DMA_CCR_TCIE; /* Enable transfer complete interrupt. */ + DMA1_Channel1->CCR |= DMA_CCR_EN; /* Enable channel */ + + /* triggered on transfer completion. We use this to process the ADC data */ + NVIC_EnableIRQ(DMA1_Channel1_IRQn); + NVIC_SetPriority(DMA1_Channel1_IRQn, 3); +} + +void DMA1_Channel1_IRQHandler(void) { + /* This interrupt takes either 1.2us or 13us. It can be pre-empted by the more timing-critical UART and LED timer + * interrupts. */ + static int count = 0; /* oversampling accumulator sample count */ + static uint32_t adc_aggregate[2] = {0, 0}; /* oversampling accumulator */ + + /* Clear the interrupt flag */ + DMA1->IFCR |= DMA_IFCR_CGIF1; + + adc_aggregate[0] += adc_buf[0]; + adc_aggregate[1] += adc_buf[1]; + + if (++count == (1<<ADC_OVERSAMPLING)) { + /* This has been copied from the code examples to section 12.9 ADC>"Temperature sensor and internal reference + * voltage" in the reference manual with the extension that we actually measure the supply voltage instead of + * hardcoding it. This is not strictly necessary since we're running off a bored little LDO but it's free and + * the current supply voltage is a nice health value. + */ + adc_vcc_mv = (3300 * VREFINT_CAL)/(adc_aggregate[0]>>ADC_OVERSAMPLING); + int32_t temperature = (((uint32_t)TS_CAL1) - ((adc_aggregate[1]>>ADC_OVERSAMPLING) * adc_vcc_mv / 3300)) * 1000; + temperature = (temperature/5336) + 30; + adc_temp_celsius = temperature; + + count = 0; + adc_aggregate[0] = 0; + adc_aggregate[1] = 0; + } +} + diff --git a/firmware/adc.h b/firmware/adc.h new file mode 100644 index 0000000..57a2ce9 --- /dev/null +++ b/firmware/adc.h @@ -0,0 +1,30 @@ +/* Megumin LED display firmware + * Copyright (C) 2018 Sebastian Götte <code@jaseg.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ADC_H__ +#define __ADC_H__ + +#include "global.h" + +#define ADC_OVERSAMPLING 8 + +extern volatile int16_t adc_vcc_mv; +extern volatile int16_t adc_temp_celsius; + +void adc_init(void); + +#endif/*__ADC_H__*/ diff --git a/firmware/global.h b/firmware/global.h new file mode 100644 index 0000000..6116adc --- /dev/null +++ b/firmware/global.h @@ -0,0 +1,13 @@ +#ifndef __GLOBAL_H__ +#define __GLOBAL_H__ + +#define FIRMWARE_VERSION 2 +#define HARDWARE_VERSION 2 + +#define TS_CAL1 (*(uint16_t *)0x1FFFF7B8) +#define VREFINT_CAL (*(uint16_t *)0x1FFFF7BA) + +extern uint32_t sys_time; +extern uint32_t sys_time_seconds; + +#endif/*__GLOBAL_H__*/ diff --git a/firmware/mac.c b/firmware/mac.c new file mode 100644 index 0000000..b2fb48a --- /dev/null +++ b/firmware/mac.c @@ -0,0 +1,3 @@ +#include "mac.h" + +uint32_t device_mac = MAC_ADDR; diff --git a/firmware/mac.h b/firmware/mac.h new file mode 100644 index 0000000..26aaff6 --- /dev/null +++ b/firmware/mac.h @@ -0,0 +1,22 @@ +#ifndef __MAC_H__ +#define __MAC_H__ + +#include <unistd.h> + +/* Device MAC address. + * + * 32 bits might seem a little short for a device MAC, but at 20 bus nodes the probablility of a collision is about 1 in + * 10 million. Check for yourself using the python code below. + * + * #!/usr/bin/env python3 + * from operator import mul + * from functools import reduce + * m = 32 + * n = 20 + * print(reduce(mul, [2**m-i for i in range(n)]) / ((2**m)**n)) + * # -> 0.9999999557621786 + */ + +extern uint32_t device_mac; + +#endif /* __MAC_H__ */ diff --git a/firmware/main.c b/firmware/main.c index 1c45dc3..7fc78c3 100644 --- a/firmware/main.c +++ b/firmware/main.c @@ -34,6 +34,10 @@ #include <stm32f0xx_ll_utils.h> #include <math.h> +#include "global.h" +#include "serial.h" +#include "adc.h" + /* Bit count of this device. Note that to change this you will also have to adapt the per-bit timer period lookup table * below. */ @@ -45,18 +49,6 @@ void do_transpose(void); -/* Right-aligned integer raw channel brightness values like so: - * - * bit index 31 ... 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 - * | (MSB) serial data put *here* (LSB) | - * |<-utterly ignored->| |<-----------------MAX_BITS------------------>| - * |<----------------NBITS---------------->| |<>|--ignored - * | (MSB) brightness data (LSB) | |<>|--ignored - */ -uint32_t brightness[32] = { - 0x23 -}; - /* Bit-golfed modulation data generated from the above values by the main loop, ready to be sent out to the shift * registers. */ @@ -67,15 +59,12 @@ uint32_t sys_time = 0; uint32_t sys_time_seconds = 0; int main(void) { - /* Get all the good clocks and PLLs on this thing up and running. We're - * running from an external 25MHz crystal, which we're first dividing - * down by 5 to get 5 MHz, then PLL'ing up by 6 to get 30 MHz as our - * main system clock. + /* Get all the good clocks and PLLs on this thing up and running. We're running from an external 25MHz crystal, + * which we're first dividing down by 5 to get 5 MHz, then PLL'ing up by 6 to get 30 MHz as our main system clock. * * The busses are all run directly from these 30 MHz because why not. * - * Be careful in mucking around with this code since you can kind of - * semi-brick the chip if you do it wrong. + * Be careful in mucking around with this code since you can kind of semi-brick the chip if you do it wrong. */ RCC->CR |= RCC_CR_HSEON; while (!(RCC->CR&RCC_CR_HSERDY)); @@ -160,8 +149,7 @@ int main(void) { TIM1->ARR = 1; TIM1->CR1 |= TIM_CR1_CEN; - /* Configure Timer 1 update (overrun) interrupt on NVIC. - * Used only for update (overrun) for strobe timing. */ + /* Configure Timer 1 update (overrun) interrupt on NVIC. Used only for update (overrun) for strobe timing. */ NVIC_EnableIRQ(TIM1_BRK_UP_TRG_COM_IRQn); NVIC_SetPriority(TIM1_BRK_UP_TRG_COM_IRQn, 1); @@ -174,8 +162,7 @@ int main(void) { TIM3->PSC = 30; TIM3->ARR = 1000; - /* Configure Timer 3 update (overrun) interrupt on NVIC. - * Used only for update (overrun) for USART timeout handling. */ + /* Configure Timer 3 update (overrun) interrupt on NVIC. Used only for update (overrun) for USART timeout handling. */ NVIC_EnableIRQ(TIM3_IRQn); NVIC_SetPriority(TIM3_IRQn, 2); @@ -204,16 +191,20 @@ int main(void) { NVIC_EnableIRQ(USART1_IRQn); NVIC_SetPriority(USART1_IRQn, 2); + adc_init(); + /* Idly loop around, occassionally disfiguring some integers. */ while (42) { /* Debug output on LED. */ GPIOA->ODR ^= GPIO_ODR_6; - /* Bit-mangle the integer brightness data to produce raw modulation data */ - do_transpose(); - /* Wait a moment */ - for (int k=0; k<10000; k++) - asm volatile("nop"); + if (framebuf_out_of_sync != 0) { + /* This logic is very slightly racy, but that should not matter since we're updating the frame buffer often + * enough so you don't notice one miss every billion frames. */ + framebuf_out_of_sync = 0; + /* Bit-mangle the integer framebuf data to produce raw modulation data */ + do_transpose(); + } } } @@ -224,7 +215,7 @@ void do_transpose(void) { uint32_t mask = 1<<i<<(MAX_BITS-NBITS); /* Bit mask for this bit value. */ uint32_t bv = 0; /* accumulator thing */ for (uint32_t j=0; j<32; j++) { - if (brightness[j] & mask) + if (rx_buf.set_fb_rq.framebuf[j] & mask) bv |= 1<<j; } brightness_by_bit[i] = bv; @@ -299,9 +290,9 @@ void TIM1_BRK_UP_TRG_COM_IRQHandler(void) { uint32_t val = brightness_by_bit[idx]; /* Shift out the current period's data. The shift register clear and strobe lines are handled by the timers - * capture/compare channel 3 complementary outputs. The dead-time generator is used to sequence the clear and strobe - * edges one after another. Since there may be small variations in IRQ service latency it is critical to allow for - * some leeway between the end of this data transmission and strobe and clear. */ + * capture/compare channel 3 complementary outputs. The dead-time generator is used to sequence the clear and + * strobe edges one after another. Since there may be small variations in IRQ service latency it is critical to + * allow for some leeway between the end of this data transmission and strobe and clear. */ SPI1->DR = (val&0xffff); while (SPI1->SR & SPI_SR_BSY); SPI1->DR = (val>>16); @@ -362,88 +353,6 @@ void TIM3_IRQHandler(void) { rxpos = 0; } -/* This macro defines the lowest channel number of this board on the serial command bus. On a shared bus with several - * boards, you would generally assign increasing USART_CHANNEL_OFFX values to each one (0, 8, 16, 24, ...). - * - * Example: Let USART_CHANNEL_OFFX be 8. - * - * /--Command channel number received in command packet (packet.set_step.step) - * | /--USART_OFFX - * | | /--4 raw channels per logical channel (step): R, G, B, W - * | | | /--Raw channel offset for R, G, B, W - * | | | | /--Resulting raw channels for R, G, B, W data received in command packet - * | | | | | - * v v v v v - * - * (8 - 8) * 4 + {0, 1, 2, 3} = {0, 1, 2, 3} - * (9 - 8) * 4 + {0, 1, 2, 3} - * (10 - 8) * 4 + {0, 1, 2, 3} - * (11 - 8) * 4 + {0, 1, 2, 3} - * (12 - 8) * 4 + {0, 1, 2, 3} - * (13 - 8) * 4 + {0, 1, 2, 3} - * (14 - 8) * 4 + {0, 1, 2, 3} - * (15 - 8) * 4 + {0, 1, 2, 3} - */ -#ifndef USART_CHANNEL_OFFX -#define USART_CHANNEL_OFFX 0 -#endif//USART_CHANNEL_OFFX - -#define NCHANNELS (sizeof(brightness)/sizeof(brightness[0])) -void USART1_IRQHandler() { - static union packet rxbuf; - - int isr = USART1->ISR; - USART1->RQR |= USART_RQR_RXFRQ; - /* Overrun detected? */ - if (isr & USART_ISR_ORE) { - USART1->ICR = USART_ICR_ORECF; /* Acknowledge overrun */ - //asm("bkpt"); /* uncomment for debug */ - return; - } - - if (!(isr & USART_ISR_RXNE)) { - //asm("bkpt"); /* uncomment for debug */ - return; - } - - /* Store received data */ - uint8_t data = USART1->RDR; - rxbuf.data[rxpos] = data; - rxpos++; - - /* If we finished receiving a packet, deal with it. */ - if (rxpos == sizeof(union packet)) { - /* Check packet header */ - if (rxbuf.set_step.cmd == 0x23 && - /* bounds-check received channel number. This allows several driver boards to share one common serial bus */ - rxbuf.set_step.step >= USART_CHANNEL_OFFX && - rxbuf.set_step.step < USART_CHANNEL_OFFX+NCHANNELS) { - - /* Calculate raw channel brightness value base address for logical channel */ - uint32_t *out = &brightness[(rxbuf.set_step.step - USART_CHANNEL_OFFX)*4]; - - /* Correct RGBW raw channel ordering per logical channel according to SUB-D pinout used. - * - * (matti) (treppe) - * weiß blau - * rot weiß - * grün rot - * blau grün - */ - out[1] = rxbuf.set_step.rgbw[0]; - out[2] = rxbuf.set_step.rgbw[1]; - out[3] = rxbuf.set_step.rgbw[2]; - out[0] = rxbuf.set_step.rgbw[3]; - } - /* Reset receive data counter */ - rxpos = 0; - } - - /* Reset usart timeout handler */ - TIM3->CNT = 0; - TIM3->CR1 |= TIM_CR1_CEN; -} - /* Misc IRQ handlers */ void NMI_Handler(void) { } diff --git a/firmware/serial.c b/firmware/serial.c new file mode 100644 index 0000000..fcdaed7 --- /dev/null +++ b/firmware/serial.c @@ -0,0 +1,214 @@ +/* Megumin LED display firmware + * Copyright (C) 2018 Sebastian Götte <code@jaseg.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stm32f0xx.h> +#include <stdint.h> +#include <system_stm32f0xx.h> +#include <stm32f0xx_ll_utils.h> +#include <math.h> + +#include "serial.h" +#include "adc.h" +#include "mac.h" + +unsigned int uart_overruns = 0; +unsigned int frame_overruns = 0; +unsigned int invalid_frames = 0; + +static union tx_buf_union tx_buf; +volatile union rx_buf_union rx_buf; +volatile uint8_t framebuf_out_of_sync; + +void tx_char(uint8_t c) { + while (!(USART1->ISR & USART_ISR_TC)); + USART1->TDR = c; +} + +void send_frame_formatted(uint8_t *buf, int len) { + uint8_t *p=buf, *q=buf, *end=buf+len; + do { + while (*q && q!=end) + q++; + tx_char(q-p+1); + while (*p && p!=end) + tx_char(*p++); + p++, q++; + } while (p < end); + tx_char('\0'); +} + +void send_status_reply(void) { + tx_buf.desc_reply.firmware_version = FIRMWARE_VERSION; + tx_buf.desc_reply.hardware_version = HARDWARE_VERSION; + tx_buf.desc_reply.uptime_s = sys_time_seconds; + tx_buf.desc_reply.vcc_mv = adc_vcc_mv; + tx_buf.desc_reply.temp_celsius = adc_temp_celsius; + tx_buf.desc_reply.uart_overruns = uart_overruns; + tx_buf.desc_reply.frame_overruns = frame_overruns; + tx_buf.desc_reply.invalid_frames = invalid_frames; + send_frame_formatted(tx_buf.byte_data, sizeof(tx_buf.desc_reply)); +} + +/* This is the higher-level protocol handler for the serial protocol. It gets passed the number of data bytes in this + * frame (which may be zero) and returns a pointer to the buffer where the next frame should be stored. + */ +volatile uint8_t *packet_received(int len) { + static enum { + PROT_ADDRESSED = 0, + PROT_EXPECT_FRAME_SECOND_HALF = 1, + PROT_IGNORE = 2, + } protocol_state = PROT_IGNORE; + /* Use mac frames as delimiters to synchronize this protocol layer */ + if (len == 0) { /* Discovery packet */ + if (sys_time < 100 && sys_time_seconds == 0) { /* Only respond during the first 100ms after boot */ + send_frame_formatted((uint8_t*)&device_mac, sizeof(device_mac)); + } + + } else if (len == 1) { /* Command packet */ + if (protocol_state == PROT_ADDRESSED) { + switch (rx_buf.byte_data[0]) { + case 0x01: + send_status_reply(); + break; + } + } else { + invalid_frames++; + } + protocol_state = PROT_IGNORE; + + } else if (len == 4) { /* Address packet */ + if (rx_buf.mac_data == device_mac) { /* we are addressed */ + protocol_state = PROT_ADDRESSED; /* start listening for frame buffer data */ + } else { /* we are not addressed */ + protocol_state = PROT_IGNORE; /* ignore packet */ + } + + } else if (len == sizeof(rx_buf.set_fb_rq.framebuf)) { + if (protocol_state == PROT_ADDRESSED) { /* First of two half-framebuffer data frames */ + /* Kick off buffer transfer. This triggers the main loop to copy data out of the receive buffer and paste it + * properly formatted into the frame buffer. */ + if (framebuf_out_of_sync == 0) { + framebuf_out_of_sync = 1; + } else { + /* FIXME An overrun happend. What should we do? */ + frame_overruns++; + } + + /* Go to "hang mode" until next zero-length packet. */ + protocol_state = PROT_IGNORE; + } + + } else { + /* FIXME An invalid packet has been received. What should we do? */ + invalid_frames++; + protocol_state = PROT_IGNORE; /* go into "hang mode" until next zero-length packet */ + } + + /* By default, return rx_buf.byte_data . This means if an invalid protocol state is reached ("hang mode"), the next + * frame is still written to rx_buf. This is not a problem since whatever garbage is written at that point will be + * overwritten before the next buffer transfer. */ + return rx_buf.byte_data; +} + +void USART1_IRQHandler(void) { + /* Since a large amount of data will be shoved down this UART interface we need a more reliable and more efficient + * way of framing than just waiting between transmissions. + * + * This code uses "Consistent Overhead Byte Stuffing" (COBS). For details, see its Wikipedia page[0] or the proper + * scientific paper[1] published on it. Roughly, it works like this: + * + * * A frame is at most 254 bytes in length. + * * The null byte 0x00 acts as a frame delimiter. There is no null bytes inside frames. + * * Every frame starts with an "overhead" byte indicating the number of non-null payload bytes until the next null + * byte in the payload, **plus one**. This means this byte can never be zero. + * * Every null byte in the payload is replaced by *its* distance to *its* next null byte as above. + * + * This means, at any point the receiver can efficiently be synchronized on the next frame boundary by simply + * waiting for a null byte. After that, only a simple state machine is necessary to strip the overhead byte and a + * counter to then count skip intervals. + * + * Here is Wikipedia's table of example values: + * + * Unencoded data Encoded with COBS + * 00 01 01 00 + * 00 00 01 01 01 00 + * 11 22 00 33 03 11 22 02 33 00 + * 11 22 33 44 05 11 22 33 44 00 + * 11 00 00 00 02 11 01 01 01 00 + * 01 02 ...FE FF 01 02 ...FE 00 + * + * [0] https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing + * [1] Cheshire, Stuart; Baker, Mary (1999). "Consistent Overhead Byte Stuffing" + * IEEE/ACM Transactions on Networking. doi:10.1109/90.769765 + * http://www.stuartcheshire.org/papers/COBSforToN.pdf + */ + + /* This pointer stores where we write data. The higher-level protocol logic decides on a frame-by-frame-basis where + * the next frame's data will be stored. */ + static volatile uint8_t *writep = rx_buf.byte_data; + /* Index inside the current frame payload */ + static int rxpos = 0; + /* COBS state machine. This implementation might be a little too complicated, but it works well enough and I find it + * reasonably easy to understand. */ + static enum { + COBS_WAIT_SYNC = 0, /* Synchronize with frame */ + COBS_WAIT_START = 1, /* Await overhead byte */ + COBS_RUNNING = 2 /* Process payload */ + } cobs_state = 0; + /* COBS skip counter. During payload processing this contains the remaining non-null payload bytes */ + static int cobs_count = 0; + + if (USART1->ISR & USART_ISR_ORE) { /* Overrun handling */ + uart_overruns++; + /* Reset and re-synchronize. Retry next frame. */ + rxpos = 0; + cobs_state = COBS_WAIT_SYNC; + /* Clear interrupt flag */ + USART1->ICR = USART_ICR_ORECF; + + } else { /* Data received */ + uint8_t data = USART1->RDR; /* This automatically acknowledges the IRQ */ + + if (data == 0x00) { /* End-of-packet */ + /* Process higher protocol layers on this packet. */ + writep = packet_received(rxpos); + + /* Reset for next packet. */ + cobs_state = COBS_WAIT_START; + rxpos = 0; + + } else { /* non-null byte */ + if (cobs_state == COBS_WAIT_SYNC) { /* Wait for null byte */ + /* ignore data */ + + } else if (cobs_state == COBS_WAIT_START) { /* Overhead byte */ + cobs_count = data; + cobs_state = COBS_RUNNING; + + } else { /* Payload byte */ + if (--cobs_count == 0) { /* Skip byte */ + cobs_count = data; + data = 0; + } + + /* Write processed payload byte to current receive buffer */ + writep[rxpos++] = data; + } + } + } +} + diff --git a/firmware/serial.h b/firmware/serial.h new file mode 100644 index 0000000..5066c43 --- /dev/null +++ b/firmware/serial.h @@ -0,0 +1,67 @@ +/* Megumin LED display firmware + * Copyright (C) 2018 Sebastian Götte <code@jaseg.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __SERIAL_H__ +#define __SERIAL_H__ + +#include "global.h" + +/* High-level stuff */ +void serial_init(void); +void send_status_reply(void); + +/* Internal low-level stuff */ +void tx_char(uint8_t c); +void send_frame_formatted(uint8_t *buf, int len); +volatile uint8_t *packet_received(int len); + +/* Error counters for debugging */ +extern unsigned int uart_overruns; +extern unsigned int frame_overruns; +extern unsigned int invalid_frames; + +union tx_buf_union { + struct __attribute__((packed)) { + uint8_t firmware_version, + hardware_version; + uint32_t uptime_s, + uart_overruns, + frame_overruns, + invalid_frames; + int16_t vcc_mv, + temp_celsius; + } desc_reply; + uint8_t byte_data[0]; +}; + +union rx_buf_union { + /* Right-aligned integer raw channel brightness values like so: + * + * bit index 31 ... 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 + * | (MSB) serial data put *here* (LSB) | + * |<-utterly ignored->| |<-----------------MAX_BITS------------------>| + * |<----------------NBITS---------------->| |<>|--ignored + * | (MSB) brightness data (LSB) | |<>|--ignored + */ + struct __attribute__((packed)) { uint32_t framebuf[32]; uint8_t end[0]; } set_fb_rq; + uint8_t byte_data[0]; + uint32_t mac_data; +}; +extern volatile union rx_buf_union rx_buf; +extern volatile uint8_t framebuf_out_of_sync; + +#endif/*__SERIAL_H__*/ |