Alessandro Mauri 2ccf069e15 Add SC7A20 accelerometer support
!!! AI GENERATED MESSAGE !!!
This commit introduces support for the SC7A20 accelerometer.
The accelerometer readings can now be displayed on the screen,
and the display mode can be toggled using the encoder.
Additionally, the EMA filter constants for voltage and current
readings have been updated from K2 to K4 for potentially smoother
readings.
2026-04-24 22:42:18 +02:00

458 lines
14 KiB
C

#include <ch32fun.h>
#include <stdint.h>
#include <stdio.h>
#include <fsusb.h>
#define USBPD_IMPLEMENTATION
#include "usbpd.h"
#include "lib_i2c.h"
#include "display.h"
#include "filter.h"
#include "sc7a20.h"
// Pin definitions
#define PIN_VBUS PA0 // vbus voltage feedback
#define PIN_CURRENT PA1 // current feedback
#define PIN_NTC PA2 // ntc temperature sensor
#define PIN_TEMP PA3 // thermocouple amplifier
#define PIN_12V PA5 // 12V regulator enable
#define PIN_HEATER PA6 // power mosfet gate control
#define PIN_ENC_A PB3 // rotary encoder A
#define PIN_ENC_B PB11 // rotary encoder B
#define PIN_BTN PB1 // rotary encoder button
// Analog channel definitions
#define VBUS_ADC_CHANNEL ANALOG_0 // PA0
#define CURRENT_ADC_CHANNEL ANALOG_1 // PA1
#define NTC_ADC_CHANNEL ANALOG_2 // PA2
#define TEMP_ADC_CHANNEL ANALOG_3 // PA3
#define FRAME_TIME_MS 41 // roughly 24 fps
// constants
// LUT for converting NTC readings to degrees kelvin
// Nominal: 1kOhm, Beta: 3380, Step: 64
const uint8_t ntc_step_size = 64;
const int16_t ntc_lut[] = {
1316, 197, 155, 133, 119, 108, 100, 93, 87, 82, 77, 73, 69, 66, 63, 60,
57, 54, 52, 50, 47, 45, 43, 41, 39, 37, 35, 34, 32, 30, 28, 27,
25, 23, 22, 20, 19, 17, 15, 14, 12, 11, 9, 7, 6, 4, 2, 0,
-1, -3, -5, -7, -9, -11, -14, -16, -19, -22, -25, -28, -32, -38, -44, -55,
-55 // extra value to not have an extra if statement
};
u8g2_t *u8g2;
int16_t encoder = 0; // rotary encoder counter
uint32_t last_interrupt = 0; // last time the encoder interrupt was triggered
#define ENCODER_DEBOUNCE 6000
// Convert the raw adc reading to a temperature in kelvin with the ntc lut,
// linearly interpolating between positions
static inline int16_t get_temp_k(uint16_t adc_reading)
{
if (adc_reading > 4095) return 0;
uint8_t index = adc_reading / ntc_step_size;
uint8_t remainder = adc_reading % ntc_step_size;
int16_t temp_base = index < 64 ? ntc_lut[index] : 0;
int16_t temp_next = ntc_lut[index + 1];
return temp_base + ((temp_next - temp_base) * remainder)/ntc_step_size;
}
// convert the raw TPA191 adc reading to a current in milliamps
static inline int16_t get_current_ma(uint16_t adc_reading)
{
// Rshunt = 4 milliOhm
// Gain = 100
u32 mv = ((u32)adc_reading * VCC_MV) / 4096;
return (mv * 10) / 4;
}
void print_i2c_device(uint8_t addr)
{
printf("Device found at 0x%02X\n", addr);
}
// this callback is mandatory when FUNCONF_USE_USBPRINTF is defined,
// can be empty though
void handle_usbfs_input(int numbytes, uint8_t *data)
{
// handle single character commands
// TODO:
// - 'c' to calibrate the tip temperature
// - 't' to test tip presence
if(numbytes == 1) {
switch(data[0]) {
case 'r': // toggle the 12V regulator
if (funDigitalRead(PIN_12V)) {
funDigitalWrite(PIN_12V, 0);
printf("Disabled 12V Regulator\n");
} else {
funDigitalWrite(PIN_12V, 1);
printf("Enabled 12V Regulator\n");
}
break;
case 'h':
printf(
"Available commands:\n"
"\tr : toggle the 12V regulator\n"
"\ts : scan I2C bus\n"
);
break;
case 's':
printf("Scanning I2C bus...\n");
i2c_scan(I2C_TARGET, print_i2c_device);
break;
default:
printf("Unknown command '%c'\n", data[0]);
break;
}
}
else {
// echo
// _write(0, (const char*)data, numbytes);
}
}
// triggered on the falling edge of the rotary encoder PIN_A
void EXTI15_8_IRQHandler(void) __attribute__((interrupt));
void EXTI15_8_IRQHandler(void)
{
uint32_t now = funSysTick32();
if (now - last_interrupt > ENCODER_DEBOUNCE) {
last_interrupt = now;
if (funDigitalRead(PIN_ENC_A)) {
encoder++;
} else {
encoder--;
}
}
EXTI->INTFR = EXTI_Line11;
}
// Procedure to get hardware I2C working on the CH32X035F8U6
static inline void setup_i2c(void)
{
// Order here matters, first initialize the AFIO and I2C subsystems then
// change register values, do that the other way around and the configuration
// wont take effect
// Enable AFIO (Alternate Function IO)
RCC->APB2PCENR |= RCC_AFIOEN;
// Init I2C
i2c_init(I2C_TARGET, FUNCONF_SYSTEM_CORE_CLOCK, 400000);
// To utilize the I2C bus we need to disable SWD first, since the pins overlap
AFIO->PCFR1 &= ~(0b0111 << 24);
AFIO->PCFR1 |= 0b0100 << 24;
// Map SCL to PC18 and SDA to PC19
AFIO->PCFR1 |= 0b0101 << 2;
// Manually set the I2C pins to Alternate Function IO, CNF=10b, MODE=10b
GPIOC->CFGXR &= ~((0xF << 8) | (0xF << 12)); // first clear the bits
// then set them
GPIOC->CFGXR |= 0b1010 << 8; // PC18
GPIOC->CFGXR |= 0b1010 << 12; // PC19
}
// FIXME: this holds the max number of ADC channels, ideally this would be lower
volatile uint16_t adc_buffer[16];
// Set-up the adc to perform continous conversion of enabled channels, enable
// channel injection and setup DMA to automatically transfer conversion results
// to a buffer
static inline void setup_adc_and_dma(uint8_t *channels, uint8_t ch_size, uint8_t *injected, uint8_t in_size)
{
if (channels == NULL || ch_size == 0) return;
// FIXME: for now only support a single configuration register, so at max 6 channels
// I don't want to implement logic to switch registers right now
// TODO: report an error
if (ch_size > 6) return;
// General ADC initialization, I use this only for enabling clocks and such
funAnalogInit(); // general initialization
// Set the SCAN and CONT bits to enter continous conversion mode
// Do not set the IAUTO bit to ensure injected channels are converted only
// when specified by software
// Set the regular channel trigger mode to software (EXTSEL = 0b111) to ensure
// conversion starts in sync with DMA (this should already be done by funAnalogInit())
ADC1->CTLR1 |= ADC_SCAN;
ADC1->CTLR2 |= ADC_CONT | ADC_EXTSEL;
// Setup regular channels (scanned continously)
// TODO: setup sampling time
for (uint8_t i = 0; i < ch_size; i++) {
// Increase sampling time to have less cross talk between channels
// 0b111 means 11 ADC clock cycles (max)
if (channels[i] < 10) {
ADC1->SAMPTR2 |= 0b111 << (channels[i]*3);
} else {
ADC1->SAMPTR1 |= 0b111 << ((channels[i]-10)*3);
}
ADC1->RSQR3 |= channels[i] << (i*5);
}
// Set the number of normal conversion channels
ADC1->RSQR1 |= (ch_size-1) << 20;
// Set-up injection channels
// The injection channel register allows for up to 4 channels, if the buffer is larger
// skip this step
// TODO: report an error
if (injected != NULL && in_size != 0 && in_size <= 4) {
// enable injection group
// JEXTSEL = 0b111 (software trigger)
ADC1->CTLR2 |= ADC_JEXTSEL;
for (uint8_t i = 0; i < in_size; i++) {
ADC1->ISQR |= injected[i] << ((4-in_size+i)*5);
}
// Set the number of injection channels
ADC1->ISQR |= (in_size-1) << 20;
}
// Set-up DMA, ADC is connected only to channel 1
// Turn on DMA
RCC->AHBPCENR |= RCC_AHBPeriph_DMA1;
// 1. Set the address of the source peripheral, in this case the output
// register of the ADC
DMA1_Channel1->PADDR = (uint32_t)&(ADC1->RDATAR);
// 2. Set the destination (base) address of the data
DMA1_Channel1->MADDR = (uint32_t)adc_buffer;
// 3. Set the number of transfers to be done each cycle, in this case it's
// the same as the number of regular conversion cycles
DMA1_Channel1->CNTR = ch_size;
// 4. Set mode: Peripheral to Memory, MEM2MEM=0 DIR=0
// Circular mode CIRC=1, restart after CNTR transfers
// Max priority to not lose conversions and offset values in the buffer
DMA1_Channel1->CFGR =
DMA_M2M_Disable |
DMA_Priority_VeryHigh |
DMA_MemoryDataSize_HalfWord |
DMA_PeripheralDataSize_HalfWord |
DMA_MemoryInc_Enable |
DMA_Mode_Circular |
DMA_DIR_PeripheralSRC;
// Turn on DMA channel 1
DMA1_Channel1->CFGR |= DMA_CFGR1_EN;
// Enable DMA requests from the ADC
ADC1->CTLR2 |= ADC_DMA;
// Power up ADC again
ADC1->CTLR2 |= ADC_ADON;
// start conversion
ADC1->CTLR2 |= ADC_SWSTART;
}
volatile uint16_t injection_results[4];
bool adc_injection_conversion() {
// Clear any pending flags
ADC1->STATR &= ~(ADC_JEOC);
// Start injection conversion using external trigger method
ADC1->CTLR2 |= ADC_JSWSTART;
// Wait for all conversions to complete
s32 timeout = 1000000;
while(!(ADC1->STATR & ADC_JEOC) && timeout--) {}
if (timeout <= 0) return false;
// Read all results
injection_results[0] = ADC1->IDATAR1 & 0x0FFF;
injection_results[1] = ADC1->IDATAR2 & 0x0FFF;
injection_results[2] = ADC1->IDATAR3 & 0x0FFF;
injection_results[3] = ADC1->IDATAR4 & 0x0FFF;
// Clear JEOC flag
ADC1->STATR &= ~ADC_JEOC;
return true;
}
__attribute__((noreturn)) int main(void)
{
SystemInit();
funGpioInitAll();
USBFSSetup();
funPinMode(PIN_VBUS, GPIO_CFGLR_IN_ANALOG);
funPinMode(PIN_CURRENT, GPIO_CFGLR_IN_ANALOG);
funPinMode(PIN_NTC, GPIO_CFGLR_IN_ANALOG);
funPinMode(PIN_TEMP, GPIO_CFGLR_IN_ANALOG);
uint8_t adc_channels[3] = {
VBUS_ADC_CHANNEL,
CURRENT_ADC_CHANNEL,
NTC_ADC_CHANNEL
};
uint8_t adc_injected[1] = {
TEMP_ADC_CHANNEL
};
setup_adc_and_dma(adc_channels, 3, adc_injected, 1);
funPinMode(PIN_12V, GPIO_CFGLR_OUT_10Mhz_PP);
funDigitalWrite(PIN_12V, 0);
funPinMode(PIN_HEATER, GPIO_CFGLR_OUT_10Mhz_PP);
funDigitalWrite(PIN_HEATER, 0);
funPinMode(PIN_DISP_RST, GPIO_CFGLR_OUT_10Mhz_PP);
funDigitalWrite(PIN_DISP_RST, 1); // start with display disabled
funPinMode(PIN_ENC_A, GPIO_CFGLR_IN_PUPD); // enable pull-up/down
funDigitalWrite(PIN_ENC_A, 1); // specify pull-up
funPinMode(PIN_ENC_B, GPIO_CFGLR_IN_PUPD); // enable pull-up/down
funDigitalWrite(PIN_ENC_B, 1); // specify pull-up
funPinMode(PIN_BTN, GPIO_CFGLR_IN_FLOAT);
setup_i2c();
// Configure the IO as an interrupt.
// PIN_ENC_B is on port B, channel 11
AFIO->EXTICR1 = AFIO_EXTICR1_EXTI11_PB; // Port B channel (pin) 11
EXTI->INTENR = EXTI_INTENR_MR11; // Enable EXT11
EXTI->FTENR = EXTI_FTENR_TR11; // Falling edge trigger
// enable interrupt
NVIC_EnableIRQ(EXTI15_8_IRQn);
Delay_Ms(500);
u8g2 = display_init();
sc7a20_init();
// Init USBPD
USBPD_VCC_e vcc = eUSBPD_VCC_3V3;
USBPD_Result_e result = USBPD_Init(vcc);
if (result != eUSBPD_OK) {
printf("USBPD_Init failed: %d\n", result);
}
// USBPD_Reset();
bool has_pd = false;
USBPD_SPR_CapabilitiesMessage_t *capabilities = NULL;
uint32_t cap_count = 0;
int max_v = 5;
int idx_9v = -1;
u32 start = funSysTick32();
while (eUSBPD_BUSY == (result = USBPD_SinkNegotiate())) {
u32 now = funSysTick32();
if (now - start > Ticks_from_Ms(5000)) {
printf("USBPD_SinkNegotiate timed out\n");
break;
}
u8g2_ClearBuffer(u8g2);
u8g2_SetBitmapMode(u8g2, 1);
u8g2_SetFontMode(u8g2, 1);
u8g2_SetFont(u8g2, u8g2_font_5x8_tr);
u8g2_DrawStr(u8g2, 0, 18, "waiting...");
u8g2_SendBuffer(u8g2);
}
if (result != eUSBPD_OK) {
printf("USBPD_SinkNegotiate failed: %s, state: %s\n",
USBPD_ResultToStr(result),
USBPD_StateToStr(USBPD_GetState())
);
} else {
has_pd = true;
cap_count = USBPD_GetCapabilities(&capabilities);
for (u32 i = 0; i < cap_count; i++) {
USBPD_SinkPDO_t *pdo = &capabilities->Sink[i];
switch (pdo->Header.PDOType) {
case eUSBPD_PDO_FIXED:
if (pdo->FixedSupply.VoltageIn50mV/20 > max_v) {
max_v = pdo->FixedSupply.VoltageIn50mV/20;
}
if (pdo->FixedSupply.VoltageIn50mV/20 == 9) {
idx_9v = i;
}
break;
case eUSBPD_PDO_BATTERY:
if (pdo->BatterySupply.MaxVoltageIn50mV/20 > max_v) {
max_v = pdo->BatterySupply.MaxVoltageIn50mV/20;
}
break;
case eUSBPD_PDO_VARIABLE:
if (pdo->VariableSupply.MaxVoltageIn50mV/20 > max_v) {
max_v = pdo->VariableSupply.MaxVoltageIn50mV/20;
}
break;
case eUSBPD_PDO_AUGMENTED:
// TODO
break;
}
}
}
for (;;) {
static uint16_t tip_mv, vbus_mv, current_ma;
static int16_t temp_k;
u32 start = funSysTick32();
poll_input(); // usb
vbus_mv = U16_FP_EMA_K4(vbus_mv, ((u32)adc_buffer[0]*VCC_MV*11)/4096);
current_ma = U16_FP_EMA_K4(current_ma, get_current_ma(adc_buffer[1]));
temp_k = I16_FP_EMA_K4(temp_k, get_temp_k(adc_buffer[2]));
if (!adc_injection_conversion()) {
printf("injection conversion failed");
} else {
tip_mv = U16_FP_EMA_K4(tip_mv, (u32)(injection_results[0]*VCC_MV)/4096);
}
u8g2_ClearBuffer(u8g2);
u8g2_SetBitmapMode(u8g2, 1);
u8g2_SetFontMode(u8g2, 1);
u8g2_SetFont(u8g2, u8g2_font_5x8_tr);
#define x_off 0
#define y_off 8
static bool mode = true;
if (mode) {
u8g2_DrawStr(u8g2, x_off+0, y_off+7, "TIP:");
u8g2_DrawStr(u8g2, x_off+20, y_off+7, u8x8_u16toa(tip_mv, 4));
u8g2_DrawStr(u8g2, x_off+0, y_off+15, "VBUS:");
u8g2_DrawStr(u8g2, x_off+25, y_off+15, u8x8_u16toa(vbus_mv, 4));
u8g2_DrawStr(u8g2, x_off+51, y_off+7, "TEMP:");
u8g2_DrawStr(u8g2, x_off+75, y_off+7, u8x8_u16toa(temp_k, 2));
u8g2_DrawStr(u8g2, x_off+51, y_off+15, "V:");
u8g2_DrawStr(u8g2, x_off+60, y_off+15, u8x8_u16toa(max_v, 2));
} else {
static int16_t ax, ay, az;
sc7a20_get_readings(&ax, &ay, &az);
u8g2_DrawStr(u8g2, x_off+0, y_off+7, ax > 0 ? "AX:+" : "AX:-");
u8g2_DrawStr(u8g2, x_off+20, y_off+7, u8x8_u16toa(ax > 0 ? ax : -ax, 5));
u8g2_DrawStr(u8g2, x_off+0, y_off+15, ay > 0 ? "AY:+" : "AY:-");
u8g2_DrawStr(u8g2, x_off+20, y_off+15, u8x8_u16toa(ay > 0 ? ay : -ay, 5));
u8g2_DrawStr(u8g2, x_off+50, y_off+7, az > 0 ? "AZ:+" : "AZ:-");
u8g2_DrawStr(u8g2, x_off+70, y_off+7, u8x8_u16toa(az > 0 ? az : -az, 5));
}
u8g2_SendBuffer(u8g2);
if (idx_9v != -1 && funDigitalRead(PIN_BTN) == 0) {
USBPD_SelectPDO(idx_9v, 0);
Delay_Ms(200);
}
if (encoder != 0) {
mode = !mode;
encoder = 0;
}
// printf("VBUS=%d, CURRENT=%d, TEMP=%d, TIP=%d, COUNTER=%d\n", vbus_mv, current_ma, temp_k, tip_mv, encoder);
u32 elapsed = funSysTick32() - start;
if (elapsed < Ticks_from_Ms(FRAME_TIME_MS)) {
DelaySysTick(Ticks_from_Ms(FRAME_TIME_MS) - elapsed);
} else {
printf("Frame took too long: %ld ms\n", elapsed/DELAY_MS_TIME);
}
}
}