mirror of
https://codeberg.org/SiB64/turbo_weather.git
synced 2026-05-01 15:14:22 +02:00
Compare commits
4 commits
ed12ab2678
...
35fe5990ef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35fe5990ef | ||
|
|
aa1b244e0a | ||
|
|
d57f3e2cab | ||
|
|
d087c48ef5 |
12 changed files with 457 additions and 79 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -15,4 +15,4 @@ gerber/*.pdf
|
||||||
*.map
|
*.map
|
||||||
*.o
|
*.o
|
||||||
*.s
|
*.s
|
||||||
|
*.EI
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,14 @@ all: $(PROJ).hex
|
||||||
|
|
||||||
SN_bate = 1
|
SN_bate = 1
|
||||||
MCU_bate = attiny424
|
MCU_bate = attiny424
|
||||||
C_FILES_bate = uart.c rtc.c calib.c
|
C_FILES_bate = uart.c rtc.c calib.c mul.c
|
||||||
|
|
||||||
BATE_PERIOD=76
|
BATE_PERIOD=76
|
||||||
bate_CFLAGS = -DPERIOD=$(BATE_PERIOD)
|
bate_CFLAGS = -DPERIOD=$(BATE_PERIOD)
|
||||||
|
|
||||||
MCU = $(MCU_$(PROJ))
|
MCU = $(MCU_$(PROJ))
|
||||||
|
|
||||||
CC=avr-gcc -v -Wall -Wno-parentheses -MMD -std=c99 -O3 \
|
CC=avr-gcc -v -Wall -Wno-parentheses -MMD -std=c99 -O2 \
|
||||||
-mmcu=$(MCU) \
|
-mmcu=$(MCU) \
|
||||||
-funsigned-char \
|
-funsigned-char \
|
||||||
-funsigned-bitfields \
|
-funsigned-bitfields \
|
||||||
|
|
@ -113,6 +113,11 @@ fuse: $(PROJ).fuse$F
|
||||||
echo "$*: fuse$F = $(fuse$F_$*)"
|
echo "$*: fuse$F = $(fuse$F_$*)"
|
||||||
[ -n "$(fuse$F_$*)" ] && $(AD) -B 5 -U fuse$F:w:$(fuse$F_$*):m
|
[ -n "$(fuse$F_$*)" ] && $(AD) -B 5 -U fuse$F:w:$(fuse$F_$*):m
|
||||||
|
|
||||||
|
BATE_CONFIG=0xba 0x01 0x00 0x8a 0x08 0x00 0xff 0xff 0x00 0x00
|
||||||
|
bate.config:
|
||||||
|
$(AD) -U userrow:v:"$(BATE_CONFIG)":m \
|
||||||
|
|| $(AD) -U userrow:w:"$(BATE_CONFIG)":m
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f *.hex *.o *.s *.map *.elf *.d
|
rm -f *.hex *.o *.s *.map *.elf *.d
|
||||||
|
|
||||||
|
|
|
||||||
88
src/bate.c
88
src/bate.c
|
|
@ -5,6 +5,8 @@
|
||||||
// !!! int = int8_t
|
// !!! int = int8_t
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#include <avr/io.h>
|
#include <avr/io.h>
|
||||||
#include <avr/interrupt.h>
|
#include <avr/interrupt.h>
|
||||||
#include <avr/sleep.h>
|
#include <avr/sleep.h>
|
||||||
|
|
@ -87,7 +89,7 @@ void rfen(uint8_t on)
|
||||||
static inline
|
static inline
|
||||||
void init_mclk(uint8_t p)
|
void init_mclk(uint8_t p)
|
||||||
{
|
{
|
||||||
if (p < 120 || p > 200)
|
if (p < 50 || p > 100)
|
||||||
p = PERIOD;
|
p = PERIOD;
|
||||||
PORTB.DIRSET = Bit(0);
|
PORTB.DIRSET = Bit(0);
|
||||||
MCLK.INTCTRL = TCA_SINGLE_CMP0_bm;
|
MCLK.INTCTRL = TCA_SINGLE_CMP0_bm;
|
||||||
|
|
@ -293,18 +295,25 @@ void read_bate()
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Configutarion in USERROW
|
// Configuration in USERROW
|
||||||
|
|
||||||
struct config {
|
struct config {
|
||||||
|
uint8_t magic;
|
||||||
|
uint8_t version;
|
||||||
uint8_t power;
|
uint8_t power;
|
||||||
uint8_t send;
|
uint8_t send;
|
||||||
uint8_t triggers;
|
uint8_t triggers;
|
||||||
uint8_t mclk_period;
|
uint8_t mclk_period;
|
||||||
|
uint8_t mclk_delay;
|
||||||
|
uint8_t pit_period;
|
||||||
uint16_t baud_div;
|
uint16_t baud_div;
|
||||||
uint16_t mclk_delay;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static const struct config *config = (struct config *) & USERROW;
|
enum magic_flags {
|
||||||
|
USE_USERROW = 0xBA,
|
||||||
|
USE_VERSION = 0x01,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
enum power_flags {
|
enum power_flags {
|
||||||
POWER_DOWN = 0x01,
|
POWER_DOWN = 0x01,
|
||||||
|
|
@ -316,6 +325,7 @@ enum send_flags {
|
||||||
SEND_CLOCK = 0x02,
|
SEND_CLOCK = 0x02,
|
||||||
SEND_HEX = 0x04,
|
SEND_HEX = 0x04,
|
||||||
SEND_CALIB = 0x08,
|
SEND_CALIB = 0x08,
|
||||||
|
SEND_DEBUG = 0x80,
|
||||||
};
|
};
|
||||||
enum trigger_flags {
|
enum trigger_flags {
|
||||||
TRIGGER_ONCE = 0x01,
|
TRIGGER_ONCE = 0x01,
|
||||||
|
|
@ -325,6 +335,15 @@ enum trigger_flags {
|
||||||
TRIGGER_BREAK = 0x10,
|
TRIGGER_BREAK = 0x10,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static struct config config_ram = {
|
||||||
|
.send = SEND_BOOT_MESSAGE | SEND_CLOCK | SEND_DEBUG,
|
||||||
|
.triggers = TRIGGER_CONT,
|
||||||
|
.mclk_delay = 0xff,
|
||||||
|
.pit_period = RTC_CLKSEL_INT1K_gc,
|
||||||
|
};
|
||||||
|
static struct config *config = & config_ram;
|
||||||
|
static const struct config *userrow = (struct config *) & USERROW;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// main()
|
// main()
|
||||||
|
|
@ -334,11 +353,16 @@ int main()
|
||||||
CCP = CCP_IOREG_gc;
|
CCP = CCP_IOREG_gc;
|
||||||
CLKCTRL.MCLKCTRLB = CLKCTRL_PDIV_2X_gc | 1;
|
CLKCTRL.MCLKCTRLB = CLKCTRL_PDIV_2X_gc | 1;
|
||||||
BATE_PORT.DIRSET = Bit(SCK_PORT) | Bit(DIN_PORT);
|
BATE_PORT.DIRSET = Bit(SCK_PORT) | Bit(DIN_PORT);
|
||||||
|
|
||||||
|
if (userrow->magic == USE_USERROW
|
||||||
|
&& userrow->version == USE_VERSION)
|
||||||
|
memcpy(config, &USERROW, sizeof(USERROW));
|
||||||
|
|
||||||
init_mclk(config->mclk_period);
|
init_mclk(config->mclk_period);
|
||||||
init_led();
|
init_led();
|
||||||
init_rfen();
|
init_rfen();
|
||||||
init_uart(config->baud_div);
|
init_uart(config->baud_div);
|
||||||
init_rtc();
|
init_rtc(config->pit_period);
|
||||||
|
|
||||||
rfen(1);
|
rfen(1);
|
||||||
|
|
||||||
|
|
@ -346,26 +370,61 @@ int main()
|
||||||
sleep_enable();
|
sleep_enable();
|
||||||
sei();
|
sei();
|
||||||
|
|
||||||
|
send_str("Hallo\n");
|
||||||
|
|
||||||
if (config->send & SEND_BOOT_MESSAGE) {
|
if (config->send & SEND_BOOT_MESSAGE) {
|
||||||
send_str("V Turbo Weather V0.01\n");
|
send_str("V Turbo Weather V0.01\n");
|
||||||
send_hex('S', (uint8_t *)&SIGROW, sizeof(SIGROW_t));
|
send_hex('S', (uint8_t *)&SIGROW, sizeof(SIGROW_t));
|
||||||
send_hex('F', (uint8_t *)&FUSE, sizeof(FUSE_t));
|
send_hex('F', (uint8_t *)&FUSE, sizeof(FUSE_t));
|
||||||
send_hex('U', (uint8_t *)&USERROW, sizeof(USERROW_t));
|
send_hex('U', (uint8_t *)&USERROW, sizeof(USERROW_t));
|
||||||
|
send_hex('C', (uint8_t *)config, sizeof(struct config));
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t trigger = TRIGGER_CONT | TRIGGER_ONCE;
|
uint8_t trigger = TRIGGER_CONT | TRIGGER_ONCE;
|
||||||
uint8_t mclk_delay = config->mclk_delay;
|
uint8_t mclk_delay = config->mclk_delay;
|
||||||
|
|
||||||
|
//#define BATE_DEBUG
|
||||||
|
#ifdef BATE_DEBUG
|
||||||
|
uint16_t nn = 0;
|
||||||
|
uint8_t depth = 0;
|
||||||
|
# define DEPTH(w, d) do { if (n>=w && d!=depth) {nn=n; n=0;} depth=d; } while(0)
|
||||||
|
#else
|
||||||
|
# define DEPTH(w, d)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint16_t n = 0;
|
||||||
|
uint16_t itest = 0;
|
||||||
|
static const union bate testdata[4] = {
|
||||||
|
{ .W = { 0xabaf, 0x3c99, 0xa31a, 0xb589}, .D = { 0x470c, 0x773f }, }, // 21.2 °C, 1021.7 mbar
|
||||||
|
{ .W = { 0xabaf, 0x3c99, 0xa31a, 0xb589}, .D = { 0x1a51, 0x6fed }, }, // 7.5 °C, 5.4 mbar
|
||||||
|
{ .W = { 0xaa3d, 0x35d9, 0xcbe5, 0xb736}, .D = { 0x4bb7, 0x7487 }, }, // 17.7 °C, 1023.0 mbar
|
||||||
|
{ .W = { 0xaa3d, 0x35d9, 0xcbe5, 0xb736}, .D = { 0x1e25, 0x650a }, }, // -11.3 °C, 2.4 mbar
|
||||||
|
};
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
|
|
||||||
|
#ifdef BATE_DEBUG
|
||||||
|
if (!n && config->send & SEND_DEBUG) {
|
||||||
|
send_str("D ");
|
||||||
|
send_hex_byte(depth);
|
||||||
|
send_hex_word(nn);
|
||||||
|
send_hex_word(clock);
|
||||||
|
send_char_sleep('\n');
|
||||||
|
}
|
||||||
|
led(n & 1);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
n++;
|
||||||
sleep_cpu();
|
sleep_cpu();
|
||||||
|
|
||||||
if (uart_busy()) {
|
if (uart_busy()) {
|
||||||
uart_tick();
|
uart_tick();
|
||||||
|
DEPTH(4096, 1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
wdt_reset();
|
wdt_reset();
|
||||||
|
|
||||||
rfen(0);
|
rfen(0);
|
||||||
led(0);
|
|
||||||
|
|
||||||
if (uart_tick())
|
if (uart_tick())
|
||||||
trigger |= TRIGGER_UART;
|
trigger |= TRIGGER_UART;
|
||||||
|
|
@ -375,6 +434,7 @@ int main()
|
||||||
trigger |= TRIGGER_CLOCK;
|
trigger |= TRIGGER_CLOCK;
|
||||||
|
|
||||||
if (!(trigger & config->triggers)) {
|
if (!(trigger & config->triggers)) {
|
||||||
|
DEPTH(0, 2);
|
||||||
if (config->power & POWER_DOWN) {
|
if (config->power & POWER_DOWN) {
|
||||||
mclk(0);
|
mclk(0);
|
||||||
if (config->power & POWER_DOWN_CLI)
|
if (config->power & POWER_DOWN_CLI)
|
||||||
|
|
@ -382,12 +442,16 @@ int main()
|
||||||
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
|
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
|
||||||
sleep_enable();
|
sleep_enable();
|
||||||
sleep_cpu();
|
sleep_cpu();
|
||||||
|
set_sleep_mode(SLEEP_MODE_IDLE);
|
||||||
|
DEPTH(0, 3);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tick)
|
if (!tick) {
|
||||||
|
DEPTH(256, 4);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
tick = 0;
|
tick = 0;
|
||||||
|
|
||||||
if (!mclk_status()) {
|
if (!mclk_status()) {
|
||||||
|
|
@ -396,11 +460,11 @@ int main()
|
||||||
}
|
}
|
||||||
if (mclk_delay) {
|
if (mclk_delay) {
|
||||||
mclk_delay--;
|
mclk_delay--;
|
||||||
|
DEPTH(0, 5);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
DEPTH(0, 6);
|
||||||
|
|
||||||
trigger = TRIGGER_CONT;
|
|
||||||
led(1);
|
|
||||||
rfen(1);
|
rfen(1);
|
||||||
cli();
|
cli();
|
||||||
uint32_t time = clock;
|
uint32_t time = clock;
|
||||||
|
|
@ -413,12 +477,15 @@ int main()
|
||||||
if (config->send & SEND_CLOCK) {
|
if (config->send & SEND_CLOCK) {
|
||||||
send_str("T 0x");
|
send_str("T 0x");
|
||||||
send_hex_long(time);
|
send_hex_long(time);
|
||||||
|
send_str(" 0x");
|
||||||
|
send_hex_byte(trigger);
|
||||||
send_char_sleep('\n');
|
send_char_sleep('\n');
|
||||||
}
|
}
|
||||||
if (config->send & SEND_HEX)
|
if (config->send & SEND_HEX)
|
||||||
send_hex('B', bate.b, sizeof(bate));
|
send_hex('B', bate.b, sizeof(bate));
|
||||||
if (config->send & SEND_CALIB) {
|
if (config->send & SEND_CALIB) {
|
||||||
bate_calib(&bate, &pressure);
|
// bate_calib(&bate, &pressure);
|
||||||
|
bate_calib(testdata+(itest++&3), &pressure);
|
||||||
send_str("P ");
|
send_str("P ");
|
||||||
send_decimal(pressure.p, 1);
|
send_decimal(pressure.p, 1);
|
||||||
send_str(" mbar, ");
|
send_str(" mbar, ");
|
||||||
|
|
@ -427,5 +494,6 @@ int main()
|
||||||
}
|
}
|
||||||
uart_tick();
|
uart_tick();
|
||||||
clock_tick = 0;
|
clock_tick = 0;
|
||||||
|
trigger = TRIGGER_CONT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
src/calib.c
30
src/calib.c
|
|
@ -6,26 +6,38 @@
|
||||||
// https://media.digikey.com/pdf/Data%20Sheets/Measurement%20Specialties%20PDFs/MS5534-CM.pdf
|
// https://media.digikey.com/pdf/Data%20Sheets/Measurement%20Specialties%20PDFs/MS5534-CM.pdf
|
||||||
|
|
||||||
#include "calib.h"
|
#include "calib.h"
|
||||||
|
#include "mul.h"
|
||||||
|
|
||||||
void bate_calib(union bate *bate, struct pressure *pt)
|
void bate_calib(union bate *bate, struct pressure *pt)
|
||||||
{
|
{
|
||||||
uint16_t C1 = bate->W1 >> 1;
|
uint16_t C1 = bate->W1 >> 1;
|
||||||
uint16_t C5 = (bate->W1 & 1) << 13 | (bate->W2 & 0xffc0) >> 4;
|
uint16_t C5 = (bate->W1 & 1) << 13 | (bate->W2 & 0xffc0) >> 4;
|
||||||
uint8_t C6 = bate->W2 & 63;
|
uint8_t C6 = bate->W2 & 63;
|
||||||
uint16_t C4 = bate->W3 >> 6;
|
uint16_t C4 = bate->W3 >> 2 & 0x3ff0;
|
||||||
uint16_t C3 = bate->W4 >> 6;
|
uint16_t C3 = bate->W4 >> 6;
|
||||||
uint16_t C2 = (bate->W3&63) << 8 | (bate->W4&63) << 2;
|
uint16_t C2 = (bate->W3&63) << 8 | (bate->W4&63) << 2;
|
||||||
|
|
||||||
uint16_t D1 = bate->D1;
|
uint16_t D1 = bate->D1;
|
||||||
uint16_t D2 = bate->D2;
|
uint16_t D2 = bate->D2;
|
||||||
|
|
||||||
uint16_t UT1 = C5 + 20224;
|
uint16_t UT1 = C5 + 20224;
|
||||||
int32_t dT = D2-UT1;
|
int32_t dT = D2 - UT1;
|
||||||
uint16_t TEMP = ((uint32_t)(dT*(C6 + 50) >> 2) >> 8) + 200;
|
|
||||||
uint16_t OFF = C2 + ((uint32_t)((C4 - 512LL)*dT >> 4) >> 8);
|
uint16_t TEMPSENS = C6 + 50;
|
||||||
uint16_t SENS = C1 + ((uint32_t)(C3*dT >> 2) >> 8) + 24576;
|
uint16_t TEMP = mul16su(dT, TEMPSENS) + 200;
|
||||||
uint16_t X = ((uint32_t)(SENS*(D1-7168LL) << 2) >> 16) - OFF;
|
|
||||||
uint16_t P = ((uint32_t)(X*80LL) >> 8) + 2500;
|
int16_t TCO = C4 - (512<<4);
|
||||||
pt->T = TEMP - 2732;
|
uint16_t OFFT1 = C2;
|
||||||
|
uint16_t OFF = OFFT1 + mul16ss(dT, TCO);
|
||||||
|
|
||||||
|
uint16_t SENST1 = C1 + 24576;
|
||||||
|
uint16_t TCS = C3 << 4;
|
||||||
|
uint16_t SENS = SENST1 + mul16su(dT, TCS);
|
||||||
|
|
||||||
|
uint16_t X = mul16uu((D1-7168)<<2, SENS) - OFF;
|
||||||
|
|
||||||
|
uint16_t P = mul16su(X, 20480) + 2500;
|
||||||
|
|
||||||
|
pt->T = TEMP + 2732;
|
||||||
pt->p = P;
|
pt->p = P;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
76
src/calib.py
Executable file
76
src/calib.py
Executable file
|
|
@ -0,0 +1,76 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
# encoding: UTF-8
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def calibrate(Word, D):
|
||||||
|
|
||||||
|
print(f"""INPUT Words
|
||||||
|
W1 = 0x{Word[1]:04x}
|
||||||
|
W2 = 0x{Word[2]:04x}
|
||||||
|
W3 = 0x{Word[3]:04x}
|
||||||
|
W4 = 0x{Word[4]:04x}
|
||||||
|
D1 = 0x{D[1]:04x}
|
||||||
|
D2 = 0x{D[2]:04x}
|
||||||
|
""", file=sys.stderr)
|
||||||
|
|
||||||
|
C=[0]*7
|
||||||
|
C[1] = Word[1] >> 1
|
||||||
|
C[2] = ((Word[3] & 0x3f)<<6) | (Word[4] & 0x3f)
|
||||||
|
C[3] = Word[4]>>6
|
||||||
|
C[4] = Word[3]>>6
|
||||||
|
C[5] = ((Word[1] & 1)<<10) | (Word[2]>>6)
|
||||||
|
C[6] = Word[2] & 0x3f
|
||||||
|
|
||||||
|
print(f"""CAL Words
|
||||||
|
C1 = {C[1]}
|
||||||
|
C2 = {C[2]}
|
||||||
|
C3 = {C[3]}
|
||||||
|
C4 = {C[4]}
|
||||||
|
C5 = {C[5]}
|
||||||
|
C6 = {C[6]}
|
||||||
|
""", file=sys.stderr)
|
||||||
|
|
||||||
|
UT1 = 8*C[5]+20224
|
||||||
|
dT = D[2] - UT1
|
||||||
|
TEMPSENSE = C[6]+50
|
||||||
|
TEMP = 200 + dT*TEMPSENSE//1024
|
||||||
|
|
||||||
|
print(f"""Temperature
|
||||||
|
D2 = {D[2]}
|
||||||
|
UT1 = {UT1}
|
||||||
|
dT = {dT}
|
||||||
|
TSENSENSE = {TEMPSENSE}
|
||||||
|
TEMP = {TEMP*0.1:.1f} °C
|
||||||
|
""", file=sys.stderr)
|
||||||
|
|
||||||
|
TCO = C[4]-512
|
||||||
|
OFFT1 = C[2]*4
|
||||||
|
OFF = OFFT1 + (TCO*dT) // 4096
|
||||||
|
|
||||||
|
SENST1 = C[1] + 24576;
|
||||||
|
TCS = C[3]
|
||||||
|
SENS = SENST1 + (TCS*dT) // 1024
|
||||||
|
X = (SENS * (D[1]-7168)) // 16384 - OFF
|
||||||
|
P = X*10//32 + 2500
|
||||||
|
|
||||||
|
print(f"""Pressure
|
||||||
|
D1 = {D[1]} {D[1]-7168}10
|
||||||
|
TCO = {TCO}
|
||||||
|
OFFT1 = {OFFT1}
|
||||||
|
OFF = {OFF}
|
||||||
|
TCS = {TCS}
|
||||||
|
SENST1= {SENST1}
|
||||||
|
SENS = {SENS} {SENS/2**14}
|
||||||
|
X = {X}
|
||||||
|
P = {P*0.1:.1f} mbar
|
||||||
|
""", file=sys.stderr)
|
||||||
|
|
||||||
|
return (TEMP,P)
|
||||||
|
|
||||||
|
for l in sys.stdin:
|
||||||
|
if l[0]!='P':
|
||||||
|
continue
|
||||||
|
Word=[None]
|
||||||
|
Word.extend([int(ll,0) for ll in l.split()[1:]])
|
||||||
|
calibrate(Word, Word[4:])
|
||||||
119
src/mul.c
Normal file
119
src/mul.c
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
//
|
||||||
|
// mul.c
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
int16_t mul16su(int16_t s, int16_t u)
|
||||||
|
{
|
||||||
|
__asm__(
|
||||||
|
"movw r18, %[s]" "\n\t"
|
||||||
|
"mul r18, %A[u]" "\n\t"
|
||||||
|
"mov r20, r1" "\n\t"
|
||||||
|
"mulsu r19, %B[u]" "\n\t"
|
||||||
|
"movw %[s], r0" "\n\t"
|
||||||
|
"mul r18, %B[u]" "\n\t"
|
||||||
|
"clr r18" "\n\t"
|
||||||
|
"add r20, r0" "\n\t"
|
||||||
|
"adc %A[s], r1" "\n\t"
|
||||||
|
"adc %B[s], r18" "\n\t"
|
||||||
|
"mulsu r19, %A[u]" "\n\t"
|
||||||
|
"sbc %B[s], r18" "\n\t"
|
||||||
|
"add r20, r0" "\n\t"
|
||||||
|
"adc %A[s], r1" "\n\t"
|
||||||
|
"adc %B[s], r18" "\n\t"
|
||||||
|
"clr r1" "\n\t"
|
||||||
|
: [s] "+r" (s)
|
||||||
|
: [u] "a" (u)
|
||||||
|
: "r0", "r1", "r18", "r19", "r20"
|
||||||
|
);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t mul16ss(int16_t a, int16_t b)
|
||||||
|
{
|
||||||
|
// ((int32_t)a*b) >> 16
|
||||||
|
__asm__(
|
||||||
|
"movw r18, %[a]" "\n\t"
|
||||||
|
"mul r18, %A[b]" "\n\t"
|
||||||
|
"mov r20, r1" "\n\t"
|
||||||
|
"muls r19, %B[b]" "\n\t"
|
||||||
|
"movw %[a], r0" "\n\t"
|
||||||
|
"mulsu %B[b], r18" "\n\t"
|
||||||
|
"clr r18" "\n\t"
|
||||||
|
"sbc %B[a], r18" "\n\t"
|
||||||
|
"add r20, r0" "\n\t"
|
||||||
|
"adc %A[a], r1" "\n\t"
|
||||||
|
"adc %B[a], r18" "\n\t"
|
||||||
|
"mulsu r19, %A[b]" "\n\t"
|
||||||
|
"sbc %B[a], r18" "\n\t"
|
||||||
|
"add r20, r0" "\n\t"
|
||||||
|
"adc %A[a], r1" "\n\t"
|
||||||
|
"adc %B[a], r18" "\n\t"
|
||||||
|
"clr r1" "\n\t"
|
||||||
|
: [a] "+r" (a)
|
||||||
|
: [b] "a" (b)
|
||||||
|
: "r0", "r1", "r18", "r19", "r20"
|
||||||
|
);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t mul16uu(int16_t a, int16_t b)
|
||||||
|
{
|
||||||
|
// ((uint32_t)a*(uint32_t)b) >> 16
|
||||||
|
__asm__(
|
||||||
|
"movw r18, %[a]" "\n\t"
|
||||||
|
"mul r18, %A[b]" "\n\t"
|
||||||
|
"mov r20, r1" "\n\t"
|
||||||
|
"mul r19, %B[b]" "\n\t"
|
||||||
|
"movw %[a], r0" "\n\t"
|
||||||
|
"mul %B[b], r18" "\n\t"
|
||||||
|
"clr r18" "\n\t"
|
||||||
|
"add r20, r0" "\n\t"
|
||||||
|
"adc %A[a], r1" "\n\t"
|
||||||
|
"adc %B[a], r18" "\n\t"
|
||||||
|
"mul r19, %A[b]" "\n\t"
|
||||||
|
"add r20, r0" "\n\t"
|
||||||
|
"adc %A[a], r1" "\n\t"
|
||||||
|
"adc %B[a], r18" "\n\t"
|
||||||
|
"clr r1" "\n\t"
|
||||||
|
: [a] "+r" (a)
|
||||||
|
: [b] "a" (b)
|
||||||
|
: "r0", "r1", "r18", "r19", "r20"
|
||||||
|
);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t divmod10(uint16_t u, uint8_t *mod)
|
||||||
|
{
|
||||||
|
uint16_t r;
|
||||||
|
uint8_t d;
|
||||||
|
__asm__(
|
||||||
|
"ldi r18, lo8(6554)" "\n\t"
|
||||||
|
"ldi r19, hi8(6554)" "\n\t"
|
||||||
|
"mul r18, %A[u]" "\n\t"
|
||||||
|
"mov r20, r1" "\n\t"
|
||||||
|
"mul r19, %B[u]" "\n\t"
|
||||||
|
"movw %[r], r0" "\n\t"
|
||||||
|
"mul r19, %A[u]" "\n\t"
|
||||||
|
"clr r19" "\n\t"
|
||||||
|
"add r20, r0" "\n\t"
|
||||||
|
"adc %A[r], r1" "\n\t"
|
||||||
|
"adc %B[r], r19" "\n\t"
|
||||||
|
"mul r18, %B[u]" "\n\t"
|
||||||
|
"add r20, r0" "\n\t"
|
||||||
|
"adc %A[r], r1" "\n\t"
|
||||||
|
"adc %B[r], r19" "\n\t"
|
||||||
|
"ldi r19, lo8(10)" "\n\t"
|
||||||
|
"mul r19, r20" "\n\t"
|
||||||
|
"mov %[d], r1" "\n\t"
|
||||||
|
"clr r1" "\n\t"
|
||||||
|
: [r] "=&r" (r),
|
||||||
|
[d] "=r" (d)
|
||||||
|
: [u] "d" (u)
|
||||||
|
: "r0", "r1", "r18", "r19", "r20"
|
||||||
|
);
|
||||||
|
*mod = d;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
8
src/mul.h
Normal file
8
src/mul.h
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
//
|
||||||
|
// mul.h
|
||||||
|
//
|
||||||
|
|
||||||
|
int16_t mul16su(int16_t s, int16_t u);
|
||||||
|
int16_t mul16ss(int16_t a, int16_t b);
|
||||||
|
int16_t mul16uu(int16_t a, int16_t b);
|
||||||
|
uint16_t divmod10(uint16_t u, uint8_t *mod);
|
||||||
73
src/mul.py
Normal file
73
src/mul.py
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
|
||||||
|
def mul(a,b):
|
||||||
|
return (a & 0xff) * (b & 0xff)
|
||||||
|
|
||||||
|
def mulsu(a,b):
|
||||||
|
a &= 0xff
|
||||||
|
if a & 0x80:
|
||||||
|
a |= 0xff00
|
||||||
|
return (a * (b&0xff)) & 0xffff
|
||||||
|
|
||||||
|
def mul16su(a, b):
|
||||||
|
a &= 0xffff
|
||||||
|
b &= 0xffff
|
||||||
|
ah = a>>8
|
||||||
|
al = a & 0xff
|
||||||
|
bh = b>>8
|
||||||
|
bl = b & 0xff
|
||||||
|
r = mulsu(ah, bh) << 16
|
||||||
|
r |= mul(al, bl)
|
||||||
|
rr = mulsu(ah, bl)
|
||||||
|
if rr & 0x8000:
|
||||||
|
r -= 0x1000000
|
||||||
|
#c = r & 0x1000000
|
||||||
|
r += rr << 8
|
||||||
|
#if c != (r & 0x1000000):
|
||||||
|
# r += 0x1000000
|
||||||
|
r += mul(al, bh) << 8
|
||||||
|
return r & 0xffffffff
|
||||||
|
|
||||||
|
def test():
|
||||||
|
errors = 0
|
||||||
|
for s in range(2):
|
||||||
|
for l in range(4):
|
||||||
|
for a in range(64, 128):
|
||||||
|
for b in range(64, 128):
|
||||||
|
for c in range(9):
|
||||||
|
for d in range(10):
|
||||||
|
e = a << (8-c)
|
||||||
|
if l & 1:
|
||||||
|
e ^= 0xff >> c
|
||||||
|
if (s):
|
||||||
|
e = -e
|
||||||
|
f = b << (9-d)
|
||||||
|
if l & 2:
|
||||||
|
f ^= 0x1ff >> c
|
||||||
|
r = mul16su(e, f)
|
||||||
|
rr = (e*f) & 0xffffffff
|
||||||
|
if r != rr:
|
||||||
|
print("ERROR", e,f,hex(r),hex(rr))
|
||||||
|
errors += 1
|
||||||
|
else:
|
||||||
|
print(e,f,r)
|
||||||
|
print(errors, "Errors")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
def full():
|
||||||
|
errors = 0
|
||||||
|
for u in range(0x10000):
|
||||||
|
print(u)
|
||||||
|
for s in range(0x8000):
|
||||||
|
r = mul16su(s, u)
|
||||||
|
rr = (s*u) & 0xffffffff
|
||||||
|
if r != rr:
|
||||||
|
print("ERROR", s, u, hex(r), hex(rr))
|
||||||
|
errors += 1
|
||||||
|
for s in range(0x8000):
|
||||||
|
r = mul16su(-s, u)
|
||||||
|
rr = (-s*u) & 0xffffffff
|
||||||
|
if r != rr:
|
||||||
|
print("ERROR", -s, u, hex(r), hex(rr))
|
||||||
|
errors += 1
|
||||||
|
print(errors, "Errors")
|
||||||
|
return errors
|
||||||
11
src/rtc.c
11
src/rtc.c
|
|
@ -15,13 +15,15 @@
|
||||||
uint32_t clock;
|
uint32_t clock;
|
||||||
uint8_t clock_tick;
|
uint8_t clock_tick;
|
||||||
|
|
||||||
void init_rtc()
|
void init_rtc(uint8_t p)
|
||||||
{
|
{
|
||||||
|
if (p>=16)
|
||||||
|
p = RTC_PERIOD_CYC1024_gc;
|
||||||
RTC.CLKSEL = RTC_CLKSEL_INT1K_gc;
|
RTC.CLKSEL = RTC_CLKSEL_INT1K_gc;
|
||||||
RTC.PITINTCTRL = 1;
|
RTC.PITINTCTRL = 1;
|
||||||
RTC.PITCTRLA = RTC_PERIOD_CYC1024_gc;
|
RTC.PITCTRLA = p;
|
||||||
while (RTC.PITSTATUS & 1) ;
|
while (RTC.PITSTATUS & 1) ;
|
||||||
RTC.PITCTRLA = RTC_PERIOD_CYC1024_gc | 1;
|
RTC.PITCTRLA = p | 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 1
|
#if 1
|
||||||
|
|
@ -31,6 +33,7 @@ ISR(RTC_PIT_vect, ISR_NAKED)
|
||||||
"in r24, __SREG__" "\n\t"
|
"in r24, __SREG__" "\n\t"
|
||||||
"push r24" "\n\t"
|
"push r24" "\n\t"
|
||||||
"ldi r24,1" "\n\t"
|
"ldi r24,1" "\n\t"
|
||||||
|
"sts %[flag], r24" "\n\t"
|
||||||
"sts %[tick], r24" "\n\t"
|
"sts %[tick], r24" "\n\t"
|
||||||
"lds r24, %[clock]" "\n\t"
|
"lds r24, %[clock]" "\n\t"
|
||||||
"subi r24, -1" "\n\t"
|
"subi r24, -1" "\n\t"
|
||||||
|
|
@ -50,12 +53,14 @@ ISR(RTC_PIT_vect, ISR_NAKED)
|
||||||
"reti" "\n"
|
"reti" "\n"
|
||||||
:[tick] "+m" (clock_tick),
|
:[tick] "+m" (clock_tick),
|
||||||
[clock] "+m" (clock)
|
[clock] "+m" (clock)
|
||||||
|
:[flag] "n" (&RTC.PITINTFLAGS)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
ISR(RTC_PIT_vect)
|
ISR(RTC_PIT_vect)
|
||||||
{
|
{
|
||||||
clock_tick = 1;
|
clock_tick = 1;
|
||||||
|
RTC.PITINTFLAGS = 1;
|
||||||
clock++;
|
clock++;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,4 @@
|
||||||
|
|
||||||
extern uint32_t clock;
|
extern uint32_t clock;
|
||||||
extern uint8_t clock_tick;
|
extern uint8_t clock_tick;
|
||||||
void init_rtc();
|
void init_rtc(uint8_t p);
|
||||||
|
|
|
||||||
90
src/uart.c
90
src/uart.c
|
|
@ -5,6 +5,7 @@
|
||||||
// !!! int = int8_t
|
// !!! int = int8_t
|
||||||
|
|
||||||
#include "uart.h"
|
#include "uart.h"
|
||||||
|
#include "mul.h"
|
||||||
|
|
||||||
#include <avr/interrupt.h>
|
#include <avr/interrupt.h>
|
||||||
#include <avr/sleep.h>
|
#include <avr/sleep.h>
|
||||||
|
|
@ -54,6 +55,7 @@ uint8_t uart_tick()
|
||||||
static uint8_t uart_tx[64];
|
static uint8_t uart_tx[64];
|
||||||
static uint8_t uart_tx_w;
|
static uint8_t uart_tx_w;
|
||||||
static uint8_t uart_tx_r;
|
static uint8_t uart_tx_r;
|
||||||
|
static const uint8_t uart_tx_m = sizeof(uart_tx) - 1;
|
||||||
|
|
||||||
static inline void tx()
|
static inline void tx()
|
||||||
{
|
{
|
||||||
|
|
@ -65,8 +67,8 @@ static inline void tx()
|
||||||
uart_tx_r = r;
|
uart_tx_r = r;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
USART0.TXDATAL = uart_tx[r];
|
USART0.TXDATAL = uart_tx[r & uart_tx_m];
|
||||||
r = (r+1) & (sizeof(uart_tx)-1);
|
r++;
|
||||||
}
|
}
|
||||||
uart_tx_r = r;
|
uart_tx_r = r;
|
||||||
USART0.CTRLA &=~ USART_DREIE_bm;
|
USART0.CTRLA &=~ USART_DREIE_bm;
|
||||||
|
|
@ -77,37 +79,38 @@ ISR(USART0_DRE_vect)
|
||||||
tx();
|
tx();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__attribute__ ((noinline, noclone))
|
||||||
uint8_t uart_busy()
|
uint8_t uart_busy()
|
||||||
{
|
{
|
||||||
return (uart_tx_w - uart_tx_r) & (sizeof(uart_tx) - 1);
|
cli();
|
||||||
|
tx();
|
||||||
|
uint8_t r = uart_tx_w - uart_tx_r;
|
||||||
|
sei();
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
__attribute__ ((noinline, noclone))
|
__attribute__ ((noinline, noclone))
|
||||||
uint8_t send_char(uint8_t c)
|
uint8_t send_char(uint8_t c)
|
||||||
{
|
{
|
||||||
uint8_t ww = (uart_tx_w+1) & (sizeof(uart_tx)-1);
|
uint8_t r = 0;
|
||||||
if (ww==uart_tx_r)
|
uint8_t ww = uart_tx_w + 1;
|
||||||
return 0;
|
if ((ww & uart_tx_m) == (uart_tx_r & uart_tx_m))
|
||||||
uart_tx[uart_tx_w] = c;
|
goto full;
|
||||||
|
r = 1;
|
||||||
|
uart_tx[uart_tx_w & uart_tx_m] = c;
|
||||||
__asm__("" ::: "memory");
|
__asm__("" ::: "memory");
|
||||||
uart_tx_w = ww;
|
uart_tx_w = ww;
|
||||||
__asm__("" ::: "memory");
|
__asm__("" ::: "memory");
|
||||||
cli();
|
full:
|
||||||
tx();
|
uart_busy();
|
||||||
sei();
|
return r;
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_char_sleep(uint8_t c)
|
|
||||||
{
|
|
||||||
while (!send_char(c))
|
|
||||||
sleep_cpu();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t uart_rx[16];
|
uint8_t uart_rx[16];
|
||||||
static uint8_t uart_rx_w;
|
static uint8_t uart_rx_w;
|
||||||
uint8_t uart_rx_n;
|
uint8_t uart_rx_n;
|
||||||
uint8_t uart_rx_m;
|
static const uint8_t uart_rx_m = sizeof(uart_rx) - 1;
|
||||||
|
uint8_t uart_rx_mes;
|
||||||
|
|
||||||
ISR(USART0_RXC_vect)
|
ISR(USART0_RXC_vect)
|
||||||
{
|
{
|
||||||
|
|
@ -115,12 +118,12 @@ ISR(USART0_RXC_vect)
|
||||||
uint8_t n = uart_rx_n;
|
uint8_t n = uart_rx_n;
|
||||||
while (USART0.STATUS & USART_RXCIF_bm) {
|
while (USART0.STATUS & USART_RXCIF_bm) {
|
||||||
uint8_t c = USART0.RXDATAL;
|
uint8_t c = USART0.RXDATAL;
|
||||||
uart_rx[w] = c;
|
uart_rx[w & uart_rx_m] = c;
|
||||||
n++;
|
n++;
|
||||||
if (w < sizeof(uart_rx)-1)
|
if (w < uart_rx_m)
|
||||||
w++;
|
w++;
|
||||||
if (c=='\n')
|
if (c=='\n')
|
||||||
uart_rx_m = w;
|
uart_rx_mes = w;
|
||||||
}
|
}
|
||||||
uart_rx_w = w;
|
uart_rx_w = w;
|
||||||
uart_rx_n = n;
|
uart_rx_n = n;
|
||||||
|
|
@ -131,19 +134,35 @@ void rx_dismiss(uint8_t n)
|
||||||
{
|
{
|
||||||
cli();
|
cli();
|
||||||
uint8_t i = 0;
|
uint8_t i = 0;
|
||||||
uart_rx_m = 0;
|
uart_rx_mes = 0;
|
||||||
if (uart_rx_w != sizeof(uart_rx)-1)
|
if (uart_rx_w != uart_rx_m)
|
||||||
while (n < uart_rx_w) {
|
while (n < uart_rx_w) {
|
||||||
uint8_t c = uart_rx[n++];
|
uint8_t c = uart_rx[n++];
|
||||||
uart_rx[i++] = c;
|
uart_rx[i++] = c;
|
||||||
if (c=='\n')
|
if (c=='\n')
|
||||||
uart_rx_m = i;
|
uart_rx_mes = i;
|
||||||
}
|
}
|
||||||
uart_rx_w = i;
|
uart_rx_w = i;
|
||||||
uart_rx_n = i;
|
uart_rx_n = i;
|
||||||
sei();
|
sei();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static __attribute__ ((noinline, noclone))
|
||||||
|
void send_hex_nibble(uint8_t b)
|
||||||
|
{
|
||||||
|
b += '0';
|
||||||
|
if (b>'9')
|
||||||
|
b += '@' - '9';
|
||||||
|
send_char_sleep(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__ ((noinline, noclone))
|
||||||
|
void send_hex_byte(uint8_t b)
|
||||||
|
{
|
||||||
|
send_hex_nibble(b >> 4);
|
||||||
|
send_hex_nibble(b & 0xf);
|
||||||
|
}
|
||||||
|
|
||||||
__attribute__ ((noinline, noclone))
|
__attribute__ ((noinline, noclone))
|
||||||
void send_hex_word(uint16_t b)
|
void send_hex_word(uint16_t b)
|
||||||
{
|
{
|
||||||
|
|
@ -171,20 +190,15 @@ void send_hex(uint8_t header, uint8_t *s, uint8_t n)
|
||||||
__attribute__ ((noinline, noclone))
|
__attribute__ ((noinline, noclone))
|
||||||
void send_decimal(uint16_t b, uint8_t dec)
|
void send_decimal(uint16_t b, uint8_t dec)
|
||||||
{
|
{
|
||||||
uint8_t c[5];
|
char c[7];
|
||||||
|
c[6] = 0;
|
||||||
uint8_t n = 0;
|
uint8_t n = 0;
|
||||||
while (b) {
|
uint8_t d;
|
||||||
uint16_t bb = b/10;
|
while ((b || n < dec) && n<6) {
|
||||||
uint16_t bbb = bb * 10;
|
b = divmod10(b, &d);
|
||||||
c[n++] = b - bbb + '0';
|
c[5-n++] = '0'+d;
|
||||||
b = bb;
|
if (n==dec)
|
||||||
|
c[5-n++] = '.';
|
||||||
}
|
}
|
||||||
if (n <= dec)
|
send_str(c+6-n);
|
||||||
send_char('0');
|
|
||||||
while (n-->dec)
|
|
||||||
send_char(c[n]);
|
|
||||||
if (dec)
|
|
||||||
send_char('.');
|
|
||||||
while (n--)
|
|
||||||
send_char(c[n]);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
src/uart.h
28
src/uart.h
|
|
@ -4,43 +4,41 @@
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <avr/io.h>
|
#include <avr/io.h>
|
||||||
|
#include <avr/sleep.h>
|
||||||
|
|
||||||
void init_uart(uint16_t div);
|
void init_uart(uint16_t div);
|
||||||
uint8_t uart_tick();
|
uint8_t uart_tick();
|
||||||
uint8_t uart_busy();
|
uint8_t uart_busy();
|
||||||
uint8_t send_char(uint8_t c);
|
uint8_t send_char(uint8_t c);
|
||||||
void send_char_sleep(uint8_t c);
|
|
||||||
extern uint8_t uart_rx[16];
|
extern uint8_t uart_rx[16];
|
||||||
extern uint8_t uart_rx_n;
|
extern uint8_t uart_rx_n;
|
||||||
extern uint8_t uart_rx_m;
|
extern uint8_t uart_rx_mes;
|
||||||
void rx_dismiss(uint8_t n);
|
void rx_dismiss(uint8_t n);
|
||||||
|
|
||||||
static inline
|
static inline
|
||||||
uint8_t uart_break_p()
|
uint8_t uart_break_p()
|
||||||
{
|
{
|
||||||
return VPORTB.IN & 0x08;
|
return !(VPORTB.IN & 0x08);
|
||||||
}
|
|
||||||
|
|
||||||
static inline
|
|
||||||
void send_hex_nibble(uint8_t b)
|
|
||||||
{
|
|
||||||
send_char_sleep(b<10 ? b+'0' : b+'A'-'9'+1);
|
|
||||||
}
|
|
||||||
static inline
|
|
||||||
void send_hex_byte(uint8_t b)
|
|
||||||
{
|
|
||||||
send_hex_nibble(b >> 4);
|
|
||||||
send_hex_nibble(b & 0xf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void send_hex_byte(uint8_t b);
|
||||||
void send_hex_word(uint16_t b);
|
void send_hex_word(uint16_t b);
|
||||||
void send_hex_long(uint32_t b);
|
void send_hex_long(uint32_t b);
|
||||||
void send_hex(uint8_t header, uint8_t *s, uint8_t n);
|
void send_hex(uint8_t header, uint8_t *s, uint8_t n);
|
||||||
void send_decimal(uint16_t b, uint8_t dec);
|
void send_decimal(uint16_t b, uint8_t dec);
|
||||||
|
|
||||||
|
|
||||||
|
static inline
|
||||||
|
void send_char_sleep(uint8_t c)
|
||||||
|
{
|
||||||
|
while (!send_char(c))
|
||||||
|
sleep_cpu();
|
||||||
|
}
|
||||||
|
|
||||||
static inline
|
static inline
|
||||||
void send_str(char *s)
|
void send_str(char *s)
|
||||||
{
|
{
|
||||||
while (*s)
|
while (*s)
|
||||||
send_char_sleep(*s++);
|
send_char_sleep(*s++);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue