Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103 · уровень средний · 32 мин чтения
Открыть в интерактивном справочнике

15в. Внешний ЦАП MCP4725 по I2C: 12 бит, быстрая запись и EEPROM, опорное от питания, генерация сигнала

В разделе 15 — встроенный ЦАП, и первым делом там предупреждение: у STM32F103C8 на BluePill его нет (15.1). Вместо него — ШИМ с фильтром или внешняя микросхема. В 15б к F103 по I2C подключали внешний АЦП ADS1115; здесь обратная задача — внешний ЦАП MCP4725 на той же шине. Это самый ходовой модуль: 12 бит, один выход, опора — собственное питание и EEPROM, из которой код берётся при включении. Здесь — команды и форматы кадров, перевод кода в напряжение и что значит опора от питания, ошибки выхода и поправка по двум точкам, сколько точек в секунду пропускает шина, синус, пила и треугольник по таймеру и через DMA, спектр ступенек и RC-фильтр, запись EEPROM и режимы отключения выхода.

Опоры: RM0008 (DocID13902 Rev 17): DMA1, канал 6 — I2C1_TX, таблица 78, с. 281; I2C до 400 кГц, с. 752; приём больше двух байт ведущим, с. 763; запросы DMA от I2C, с. 769; бит DMAEN в I2C_CR2, с. 775. DocID13587 Rev 17 (STM32F103x8/xB): PB6 — I2C1_SCL, PB7 — I2C1_SDA, оба FT, с. 32. Документа на MCP4725 в проекте нет. Формат быстрой записи (два байта: 0 0 PD1 PD0 D11…D8, затем D7…D0), чтение пяти байт с битом RDY в старшем разряде первого и запись EEPROM от 25 до 50 мс сверены с драйвером drivers/dac/dac_mcp4725.c из Zephyr 4.4.2. Остальное — адреса, коды команд, формат трёхбайтной записи и чтения, бит POR, режимы power-down, ресурс EEPROM — по документации производителя, не проверено; это заложено в модель, и всё ниже проверяет согласие драйвера с моделью, а не с микросхемой. Все числа — прогон драйвера _verify/dac15d/mcp15d.c на компьютере (gcc 12.2.0) со своей моделью MCP4725 как ведомого I2C на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3.

Как это называется в документации

Оригинал Перевод
rail-to-rail output выход от нуля до питания
fast mode write command быстрая запись: два байта на код
write DAC register / write DAC register and EEPROM запись регистра ЦАП / ЦАП и EEPROM
power-down mode выход отключён и стянут на землю через резистор
ready/busy (RDY/BSY) запись EEPROM закончена / идёт
power-on reset (POR) сброс при включении питания
offset / gain error ошибка смещения / усиления
INL / DNL интегральная / дифференциальная нелинейность
settling time время установления выхода
high-speed mode (Hs) I2C на 3,4 МГц
endurance ресурс: сколько раз можно записать EEPROM

15в.1 Микросхема: чем она отличается от встроенного ЦАП

Встроенный ЦАП F407 или F103xC сидит на шине микроконтроллера: запись в регистр — и через время установления на выходе напряжение, а DMA от таймера кладёт точки без участия процессора (15.4). MCP4725 по документации производителя устроен так:

Встроенный ЦАП (15) MCP4725
разрядность 12 бит 12 бит
опора VREF+ собственное питание VDD
выход с буфером или без усилитель, от нуля до VDD
связь регистр, DMA от таймера I2C, 2 байта на код
код при включении 0 из своей EEPROM
отключение выхода бит EN power-down: 1, 100 или 500 кОм на землю
у F103C8 нет ставится на I2C1

Напряжение на выходе по документации производителя — VDD × код / 4096 (в разделе 15 у встроенного — код × Vref / 4095). Опоры внутри нет: всё, что гуляет на питании, гуляет и на выходе (15в.4). Адрес — 1100 A2 A1 A0: A0 — вывод, A2 и A1 зашиты при изготовлении, поэтому одна и та же микросхема бывает с адресом 0x60 и 0x62 (15в.7). Выход между записями не трогает шину — код держится в регистре.

Вверху — подключение MCP4725 к STM32F103: PB6 — SCL, PB7 — SDA на I2C1 на 400 кГц, вывод A0 на GND даёт адрес 0x60, у других исполнений 0x62; питание 3,3 В служит опорой, шаг 805,66 мкВ; выход VOUT идёт через RC-фильтр 1 кОм и 100 нФ с частотой среза 1591,5 Гц на нагрузку. Ниже — три формата кадров после байта адреса: быстрая запись из 2 байт с битами PD1 PD0 и кодом D11–D0, запись регистра ЦАП и ЦАП с EEPROM из 3 байт с командой C2 C1 C0, чтение 5 байт — состояние с RDY и POR, регистр ЦАП и EEPROM; на 400 кГц быстрая запись занимает 72,5 мкс, потоком 45 мкс на точку, чтение 140 мкс


15в.2 Подключение к F103

Вывод модуля STM32F103 Назначение
SCL PB6 такт I2C1
SDA PB7 данные I2C1
A0 GND младший бит адреса: 0x60
VDD, GND 3,3 В и земля питание и опора выхода
VOUT вход нагрузки через RC-фильтр, если нужен гладкий сигнал

I2C1 — как в 15б.2: PB6 и PB7, 400 кГц, CCR = F/S | 30, TRISE = 11. Оба вывода FT (DocID13587, с. 32), так что модуль на 5 В с подтяжками к 5 В их не сожжёт. Быстрее 400 кГц I2C у F1 не умеет: RM0008 (с. 752) называет только стандартный режим до 100 кГц и быстрый до 400 кГц. Режим 3,4 МГц, который по документации производителя есть у MCP4725, с F103 недоступен.

Питание модуля — это опора. Поставить его на ту же 3,3 В, что и микроконтроллер, проще всего, но тогда на выходе вся помеха от цифры. Если нужна точность, MCP4725 питают от отдельного малошумящего стабилизатора и ставят развязку у самого вывода VDD (15в.4).

Модуль в прошивке ищется перебором всех восьми адресов: в модели при A0 на земле и на питании, у исполнений A2 A1 = 00 и 01, mcp_find нашла 0x60, 0x61, 0x62 и 0x63, а прямой опрос 0x60 у трёх последних вернул MCP_E_NACK.


15в.3 Команды: быстрая запись, запись регистра, чтение пяти байт

Регистров с указателем, как у ADS1115, нет: что делать, говорят старшие биты первого байта после адреса. Быстрая запись — два байта, это и есть рабочая команда генератора:

/* быстрая запись: 0 0 PD1 PD0 D11…D8, D7…D0 */
void mcp_fast_bytes(uint16_t code, uint32_t pd, uint8_t b[2])
{
    b[0] = (uint8_t)(MCP_CMD_FAST | (pd & 3u) << 4 | (code >> 8 & 0x0Fu));
    b[1] = (uint8_t)code;
}

Код 2048 — байты 08 00, 4095 — 0F FF, 2048 с отключением через 1 кОм — 18 00, через 500 кОм — 38 00. Тот же порядок бит у драйвера Zephyr. Запись регистра ЦАП (команда 010) и ЦАП с EEPROM (011) — три байта, код сдвинут влево на 4 бита:

/* запись регистра ЦАП (cmd = MCP_CMD_DAC) или ЦАП и EEPROM (MCP_CMD_DAC_EE): три байта */
static int write3(mcp_t *d, uint8_t cmd, uint16_t code, uint32_t pd)
{
    uint8_t b[3] = {(uint8_t)(cmd | (pd & 3u) << 1), (uint8_t)(code >> 4), (uint8_t)(code << 4)};
    if (code > MCP_MAX)
        return MCP_E_RANGE;
    int rc = xfer(d, b, 3, 0, 0);
    if (rc == MCP_OK) {
        d->code = code;
        d->pd = (uint8_t)(pd & 3u);
    }
    return rc;
}

В модели код 2048 ушёл байтами 40 80 00, с EEPROM — 60 80 00. Чтение — пять байт без указателя: состояние, регистр ЦАП, EEPROM.

/* пять байт: состояние (RDY, POR, PD), регистр ЦАП (D11…D4, D3…D0 xxxx), EEPROM (x PD1 PD0 x D11…D8, D7…D0) */
int mcp_read(mcp_t *d, mcp_status_t *s)
{
    uint8_t b[5];
    int rc = xfer(d, 0, 0, b, 5);
    if (rc)
        return rc;
    s->rdy = (uint8_t)(b[0] >> 7 & 1u);
    s->por = (uint8_t)(b[0] >> 6 & 1u);
    s->pd = (uint8_t)(b[0] >> 1 & 3u);
    s->code = (uint16_t)(b[1] << 4 | b[2] >> 4);
    s->ee_pd = (uint8_t)(b[3] >> 5 & 3u);
    s->ee_code = (uint16_t)((b[3] & 0x0Fu) << 8 | b[4]);
    return MCP_OK;
}

Первое чтение в модели после включения — C0 00 00 00 00: RDY = 1, POR = 1, выход включён, код 0, в EEPROM тоже 0 (что записано с завода у настоящей микросхемы, не проверено). Пять байт на F103 принимаются по схеме для N > 2 (RM0008, с. 763): последние три — через BTF, ACK снимается перед предпоследним.

Сколько это стоит на шине (байт — 9 тактов, старт и стоп — по такту):

Команда Тактов SCL 100 кГц 400 кГц 3,4 МГц
быстрая запись 29 290 мкс 72,5 мкс 33,53 мкс
запись регистра ЦАП 38 380 мкс 95 мкс 36,18 мкс
чтение пяти байт 56 560 мкс 140 мкс 41,47 мкс

На 3,4 МГц каждая передача по правилам I2C начинается с кода ведущего на 400 кГц — в модели 10 тактов, 25 мкс, — поэтому одиночная запись ускоряется всего вдвое.


15в.4 Код в напряжение: шаг, ошибки и опора от питания

Шаг — VDD / 4096. Перевод целыми, с округлением:

/* код → микровольты: VDD × код / 4096, опора — питание */
uint32_t mcp_code_uv(uint16_t code, uint32_t vdd_mv)
{
    return (uint32_t)(((uint64_t)code * vdd_mv * 1000u + 2048u) >> 12);
}
VDD Шаг Код 1 Код 2048 Код 4095
3,3 В 805,66 мкВ 806 мкВ 1 650 000 мкВ 3 299 194 мкВ
5 В 1220,7 мкВ 1221 мкВ 2 500 000 мкВ 4 998 779 мкВ

Обратно mcp_uv_code: 1 В при 3,3 В — код 1241, при 5 В — 819; 4 В при 3,3 В не бывает — код упирается в 4095. До VDD выход не доходит ровно на один шаг.

Ошибки. У настоящего ЦАП выход отличается от VDD × код / 4096: смещение сдвигает всю прямую, ошибка усиления меняет наклон, INL — изгиб прямой, DNL — неравные ступеньки. Чисел из документа здесь нет; в модели заданы смещение −2 шага, усиление −0,2 %, дуга INL 1,5 шага и своя ошибка каждого кода 0,15 шага (с. к. о.) — это допущения. Что получилось по всем 4096 кодам при 3,3 В:

Как и у АЦП (14а.5), смещение и усиление снимает поправка по двум точкам: измерить выход на кодах 256 и 3840 (в модели ошибки −2,112 и −9,239 шага) и пересчитывать каждый код:

/* какой код записать, чтобы на выходе было напряжение кода want: c1 + ((want − c1) × 256 − e1) × span / den */
uint16_t mcp_cal_apply(const mcp_cal_t *c, uint16_t want)
{
    int64_t num = ((int64_t)((int32_t)want - c->c1) * 256 - c->e1_q8) * c->span;
    int64_t q = (num >= 0 ? num + c->den / 2 : num - c->den / 2) / c->den;
    int64_t r = c->c1 + q;
    return (uint16_t)(r < 0 ? 0 : r > (int64_t)MCP_MAX ? MCP_MAX : r);
}

Для 1,65 В поправка пишет 2054 вместо 2048. После неё на кодах 64…4031 ошибка не больше 2,11 шага (у кода 1967): остались изгиб INL и ступеньки, прямой линией они не снимаются.

Опора — питание. Это главное отличие от ЦАП с внутренней опорой. Код 2048 при разном VDD:

VDD Выход Ошибка против 3,3 В АЦП F103 на том же питании
3,3 В 1650 мВ 0 2048
3,25 В 1625 мВ −31 шаг 2048
3,2 В 1600 мВ −62,1 шага 2048
3,4 В 1700 мВ 62,1 шага 2048

Стабилизатор, давший 3,25 вместо 3,3 В, — это уже 31 шаг ошибки, больше, чем все ошибки модели вместе. И пульсации питания идут на выход пропорционально коду: 20 мВ от пика до пика на VDD дали на выходе 0 мВ на коде 0, 5 мВ на 1024, 10 мВ на 2048 и 19,995 мВ на 4095.

Из этого два приёма. Если выход меряет АЦП F103 с опорой от того же питания, ошибка сокращается — последний столбец таблицы: отношение напряжения к питанию не зависит от питания. Если нужны абсолютные вольты — питание измеряют (через VREFINT, 14.7) и пересчитывают код, или питают MCP4725 от отдельного точного стабилизатора.


15в.5 Скорость и генерация сигнала

Генератор — та же таблица одного периода, что в 15.4, только каждую точку надо донести по шине. Таблицу синуса драйвер строит целыми из четверти периода (65 значений), без sinf:

/* синус по 256-точечному кругу из четверти; n — степень двойки от 4 до 256 */
int mcp_table_sine(uint16_t *tab, uint32_t n, uint16_t mid, uint16_t amp)
{
    if (n < 4u || n > 256u || (n & (n - 1u)) || mid < amp || mid + amp > MCP_MAX)
        return MCP_E_RANGE;
    for (uint32_t i = 0; i < n; i++) {
        uint32_t j = i * (256u / n), r = j & 63u;
        int32_t s = (j & 64u) ? sin_q[64u - r] : sin_q[r];
        if (j & 128u)
            s = -s;
        int32_t v = (int32_t)amp * s;
        tab[i] = (uint16_t)(mid + (v >= 0 ? (v + 16384) >> 15 : -((-v + 16384) >> 15)));
    }
    return MCP_OK;
}

Синус из 32 точек с серединой 2048 и размахом ±2000 начинается с 2048, 2438, 2813, 3159, 3462, 3711, 3896, 4009 и идёт от 48 до 4048; треугольник — 48, 298, 548, …, пила — 48, 177, 306, … в тех же пределах.

Два способа гнать точки. По таймеру: каждая точка — своя передача из 29 тактов. Потоком: одна передача без конца, и по документации производителя пары байт быстрой записи можно слать одну за другой без нового адреса — 18 тактов на точку. Модель прогнала оба:

Шина Своей передачей точек/с Потоком точек/с Синус 16 / 32 / 64 точки, своей передачей то же потоком
100 кГц 290 мкс 3448 180 мкс 5556 215,5 / 107,8 / 53,9 Гц 347,2 / 173,6 / 86,8 Гц
400 кГц 72,5 мкс 13 793 45 мкс 22 222 862,1 / 431 / 215,5 Гц 1388,9 / 694,4 / 347,2 Гц
3,4 МГц 33,53 мкс 29 825 5,294 мкс 188 889 1864 / 932 / 466 Гц 11 805,6 / 5902,8 / 2951,4 Гц

Это предел шины; время установления выхода в модели не учтено, а на 3,4 МГц точка короче нескольких микросекунд, и что выход успеет за ней, не проверено. На F103 реальны две верхние строки.

По таймеру. TIM2 тикает с частотой точек, прерывание начинает передачу, остальное ведут прерывания I2C1:

void TIM2_IRQHandler(void)
{
    TIM2->SR = (uint16_t)~TIM_SR_UIF;
    if (busy) {                                         /* прошлая точка ещё на шине — эту пропускаем */
        overruns++;
        return;
    }
    mcp_fast_bytes(mcp_gen_next(&gen), MCP_PD_ON, frame);
    busy = 1;
    sent = 0;
    I2C1->CR2 |= I2C_CR2_ITEVTEN | I2C_CR2_ITBUFEN | I2C_CR2_ITERREN;
    I2C1->CR1 |= I2C_CR1_START;
}

void I2C1_EV_IRQHandler(void)
{
    uint16_t sr1 = I2C1->SR1;
    if (sr1 & I2C_SR1_SB) {
        I2C1->DR = (uint8_t)(dac.addr << 1);
    } else if (sr1 & I2C_SR1_ADDR) {
        (void)I2C1->SR2;
        I2C1->DR = frame[0];
        sent = 1;
    } else if ((sr1 & I2C_SR1_TXE) && sent == 1u) {
        I2C1->DR = frame[1];
        sent = 2;
        I2C1->CR2 &= (uint16_t)~I2C_CR2_ITBUFEN;        /* дальше нужен только BTF */
    } else if (sr1 & I2C_SR1_BTF) {
        I2C1->CR1 |= I2C_CR1_STOP;
        I2C1->CR2 &= (uint16_t)~(I2C_CR2_ITEVTEN | I2C_CR2_ITERREN);
        busy = 0;
    }
}

В модели, синус 32 точки, 320 тиков:

Шина Тик Ждали Пропусков Вышло
400 кГц 100 мкс 312,5 Гц 0 312,5 Гц
400 кГц 80 мкс 390,62 Гц 0 390,62 Гц
400 кГц 70 мкс 446,43 Гц 319 223,21 Гц
100 кГц 100 мкс 312,5 Гц 319 104,17 Гц
100 кГц 300 мкс 104,17 Гц 0 104,17 Гц

Выход меняется через 70 мкс после тика на 400 кГц (280 мкс на 100 кГц) — в конце второго байта, — но задержка постоянная, и частота от неё не зависит. Тик короче передачи — и каждая вторая точка пропущена: шаг 140 мкс вместо 70, частота вдвое ниже. Пропуски ничем не видны, кроме счётчика overruns.

Через DMA. Кадры всей таблицы — 64 байта на 32 точки — лежат в памяти, DMA1 канал 6 (I2C1_TX, RM0008, с. 281) по кругу кладёт их в I2C1->DR, а передача не кончается никогда. Процессор свободен, частота — 400 кГц / 18 / 32 = 694,4 Гц, и задаёт её шина, а не таймер:

/* ── потоком: одна передача без конца, DMA1 канал 6 (I2C1_TX) по кругу ── */
static int gen_dma_start(void)
{
    int rc;
    uint32_t len = mcp_stream_frames(tab, GEN_N, stream_buf);
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    DMA1_Channel6->CCR = 0;
    DMA1_Channel6->CPAR = (uint32_t)&I2C1->DR;
    DMA1_Channel6->CMAR = (uint32_t)stream_buf;
    DMA1_Channel6->CNDTR = len;
    DMA1_Channel6->CCR = DMA_CCR6_MINC | DMA_CCR6_CIRC | DMA_CCR6_DIR | DMA_CCR6_EN;   /* память → DR, по кругу */
    I2C1->CR2 |= I2C_CR2_DMAEN;                         /* до события ADDR: RM0008, с. 769 */
    if ((rc = start((uint8_t)(dac.addr << 1))) != MCP_OK)
        return fail(rc);
    (void)I2C1->SR2;                                    /* ADDR снят — дальше байты кладёт DMA */
    return MCP_OK;
}

Цена — шина занята навсегда: другим устройствам на I2C1 места нет, а частоту меняют только числом точек. Держит ли F103 такую передачу часами, не проверено.

Спектр и фильтр. Выход — ступеньки. Модель собрала выход синуса 32 точки на 10 кГц (312,5 Гц) по 64 отсчёта на ступеньку, пропустила через RC-фильтр и разложила последний период быстрым преобразованием Фурье. Помехи — «отражения» частоты точек: гармоники 31 и 33 (9687,5 и 10 312,5 Гц), 63 и 65:

Без фильтра 1 кОм, 100 нФ (1591,5 Гц) 3,3 кОм, 100 нФ (482,3 Гц)
основная, амплитуда 1608,6 мВ 1578,46 мВ 1349,98 мВ
гармоника 31 −29,8 дБ −45,5 дБ −54,4 дБ
гармоника 33 −30,4 дБ −46,5 дБ −55,5 дБ
гармоника 63 −36 дБ −57,7 дБ −66,7 дБ
всё, кроме основной −24,9 дБ −42,6 дБ −51,5 дБ

Без фильтра амплитуда 1608,6 мВ — почти ровно 2000 шагов, умноженные на 0,99839 (1608,74 мВ): ступенька чуть ослабляет и основную. Фильтр первого порядка гасит отражения почти на 16 дБ, но второй вариант уже съел 16 % основной амплитуды: срез у одного RC пологий (178.1), и точек на период выгоднее брать больше. С ошибками модели появилась вторая гармоника от изгиба INL: −70,1 дБ без фильтра, −70,6 и −72,9 дБ с фильтрами, третья — −88,2 дБ. До отражений ей далеко.


15в.6 EEPROM и power-down

EEPROM хранит код и режим выхода на время без питания: при включении микросхема по документации производителя берёт их в регистр ЦАП. Запись идёт командой 011 и длится долго; пока она идёт, RDY в первом байте чтения — 0. Драйвер опрашивает его раз в 1000 мкс не дольше 60 000 мкс — столько же ждёт драйвер Zephyr:

/* после записи EEPROM: опрашивать RDY, пока не станет 1 */
int mcp_wait_ready(mcp_t *d, uint32_t timeout_us)
{
    mcp_status_t s;
    for (uint32_t waited = 0;; waited += MCP_EE_POLL_US) {
        int rc = mcp_read(d, &s);
        if (rc)
            return rc;
        d->polls++;
        if (s.rdy)
            return MCP_OK;
        if (waited >= timeout_us)
            return MCP_E_TIMEOUT;
        mcp_port_delay_us(MCP_EE_POLL_US);
    }
}
Запись EEPROM в модели Опросов Время Обменов Итог
25 мс 23 25 315 мкс 24 готово
50 мс 45 50 395 мкс 46 готово
70 мс 61 68 635 мкс 62 MCP_E_TIMEOUT

25 и 50 мс — пределы из комментария Zephyr; 70 мс — неисправная микросхема, её драйвер отличает от медленной. После записи 2048 в EEPROM и быстрой записи 100 в модели перезапустили питание: регистр ЦАП снова 2048, на выходе 1,6462 В — с ошибками модели.

Power-down. Биты PD1 PD0 отключают выходной усилитель и стягивают выход на землю через 1 кОм (01), 100 кОм (10) или 500 кОм (11). Свободный выход в любом режиме — 0 В. Разница видна, когда снаружи есть подтяжка: через 10 кОм к 3,3 В в модели вышло 0,3, 3 и 3,235 В. Чтение пяти байт показывает режим (pd = 1, 2, 3), код при этом остаётся — 3000. Быстрая запись с PD = 00 вернула выход к 3000 кодов, 2,4114 В. Режим с 1 кОм держит линию в нуле надёжнее всего; 500 кОм почти не нагружает чужой источник, если выход ЦАП стоит параллельно ему.


15в.7 Грабли

Адрес не тот. Модуль, купленный взамен, молчит по 0x60 — у него исполнение с A2 A1 = 01 и адрес 0x62 или 0x63. В модели прямой опрос 0x60 у такого модуля — MCP_E_NACK, а перебор mcp_find по 0x60…0x67 его нашёл. Проще всего искать перебором при каждом включении — как сканирование шины из 17.7.

EEPROM в цикле. Значение пишут командой 011 на каждое изменение — «чтобы помнил». Каждая запись — 25 мс с ожиданием RDY: в модели 20 записей заняли 506 300 мкс, по 25 315 мкс, 39,5 в секунду, а быстрая запись — 72,5 мкс. Ресурс EEPROM по документации производителя — порядка миллиона записей (не проверено); 1 000 000 записей с такой скоростью — 7,03 ч. В прошивке EEPROM пишется один раз, и только если там другое значение.

Опора гуляет вместе с питанием. 3,25 В вместо 3,3 — 31 шаг ошибки на середине шкалы; 20 мВ пульсаций — 10 мВ на выходе (15в.4).

Нагрузка. Выход не источник питания. RC-фильтр перед нагрузкой 10 кОм — делитель: 1 кОм фильтра и 10 кОм нагрузки оставили 0,909 сигнала. Нагрузку — через операционный усилитель (15.3).

Питание 5 В, подтяжки к 3,3 В. По документации производителя микросхема видит единицу на SDA от 0,7 × VDD — при 5 В это 3,5 В, а подтяжка к 3,3 В до них не дотягивает. В модели при таком включении mcp_find не нашла ничего, с подтяжками к 5 В нашла 0x60. PB6 и PB7 — FT, подтяжка к 5 В им не страшна. Ещё одно: выход при 5 В выше 3,3 В начиная с кода 2704 (3,3 / 5 × 4096 = 2703,36) — на вход АЦП F103 его так не подать.

Тик таймера короче передачи. 70 мкс при 72,5 мкс на точку — каждая вторая пропущена, вместо 446,43 Гц вышло 223,21 Гц, и снаружи этого не видно (15в.5).


15в.8 Итоги

Полные исходники

mcp15d.h — адреса, команды, режимы power-down, биты состояния, порт и функции драйвера и генератора.

/* mcp15d.h — раздел 15в: драйвер внешнего ЦАП MCP4725 по I2C и генератор сигнала без регистров микроконтроллера.
 * Драйвер говорит с микросхемой только через функции порта (mcp_port_*): в прошивке fw15d.c их делает I2C1
 * STM32F103, в стенде host15d.c — модель model15d.py.
 * Команды, форматы кадров, адреса, биты RDY и POR, режимы power-down — по документации производителя;
 * документа в проекте нет, с микросхемой не сверено (формат быстрой записи и чтение пяти байт с RDY
 * совпадают с драйвером dac_mcp4725 из Zephyr). */
#ifndef MCP15D_H
#define MCP15D_H
#include <stdint.h>

/* ── адрес: 1100 A2 A1 A0; A2 и A1 заданы при изготовлении, A0 — вывод ──────── */
#define MCP_ADDR_BASE     0x60u         /* A2 A1 = 00; у других исполнений 0x62, 0x64, 0x66 */
#define MCP_ADDR_LAST     0x67u

/* ── команды: старшие биты первого байта ───────────────────────────────── */
#define MCP_CMD_FAST      0x00u         /* 0 0 PD1 PD0 D11…D8, затем D7…D0 — два байта */
#define MCP_CMD_DAC       0x40u         /* 0 1 0 x x PD1 PD0 x, D11…D4, D3…D0 x x x x — три байта */
#define MCP_CMD_DAC_EE    0x60u         /* то же и запись в EEPROM: значение при включении */

/* ── power-down: что делает выход ──────────────────────────────────────── */
enum { MCP_PD_ON = 0, MCP_PD_1K = 1, MCP_PD_100K = 2, MCP_PD_500K = 3 };   /* 1, 100, 500 кОм на землю */

/* ── чтение пяти байт: состояние, регистр ЦАП, EEPROM ─────────────────── */
#define MCP_ST_RDY        0x80u         /* 1 — запись EEPROM закончена */
#define MCP_ST_POR        0x40u         /* 1 — питание в норме */

#define MCP_MAX           4095u
#define MCP_EE_POLL_US    1000u         /* шаг опроса RDY после записи EEPROM */
#define MCP_EE_TIMEOUT_US 60000u        /* столько же ждёт драйвер Zephyr */

enum { MCP_OK = 0, MCP_E_NACK = -1, MCP_E_BUS = -2, MCP_E_TIMEOUT = -3, MCP_E_RANGE = -4 };

typedef struct {
    uint8_t addr;                       /* семибитный адрес: 0x60…0x67 */
    uint8_t pd;                         /* последний записанный режим power-down */
    uint16_t code;                      /* последний записанный код */
    uint32_t xfers, errors, ee_writes, polls;
} mcp_t;

typedef struct {
    uint8_t rdy, por, pd, ee_pd;
    uint16_t code, ee_code;
} mcp_status_t;

/* поправка по двум точкам: ошибка e1, e2 (в единицах кода × 256) измерена на кодах c1 и c2 */
typedef struct {
    int32_t c1, e1_q8, span, den;
} mcp_cal_t;

/* генератор: таблица одного периода и номер следующей точки */
typedef struct {
    const uint16_t *tab;
    uint32_t n, i;
} mcp_gen_t;

/* ── порт: в прошивке — I2C1, в стенде — строки к модели ──────────────── */
/* записать ntx байт или прочитать nrx байт (MCP4725 читается без указателя); 0, MCP_E_NACK или MCP_E_BUS */
int mcp_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx);
void mcp_port_delay_us(uint32_t us);

/* ── драйвер ──────────────────────────────────────────────────────────── */
void mcp_fast_bytes(uint16_t code, uint32_t pd, uint8_t b[2]);
int mcp_probe(mcp_t *d, uint8_t addr7);
int mcp_find(mcp_t *d);
int mcp_fast(mcp_t *d, uint16_t code, uint32_t pd);
int mcp_write_dac(mcp_t *d, uint16_t code, uint32_t pd);
int mcp_read(mcp_t *d, mcp_status_t *s);
int mcp_wait_ready(mcp_t *d, uint32_t timeout_us);
int mcp_write_eeprom(mcp_t *d, uint16_t code, uint32_t pd);
uint32_t mcp_code_uv(uint16_t code, uint32_t vdd_mv);
uint16_t mcp_uv_code(uint32_t uv, uint32_t vdd_mv);
void mcp_cal_two_point(mcp_cal_t *c, uint16_t c1, int32_t e1_q8, uint16_t c2, int32_t e2_q8);
uint16_t mcp_cal_apply(const mcp_cal_t *c, uint16_t code);

/* ── генератор ────────────────────────────────────────────────────────── */
int mcp_table_sine(uint16_t *tab, uint32_t n, uint16_t mid, uint16_t amp);
int mcp_table_triangle(uint16_t *tab, uint32_t n, uint16_t lo, uint16_t hi);
int mcp_table_saw(uint16_t *tab, uint32_t n, uint16_t lo, uint16_t hi);
uint16_t mcp_gen_next(mcp_gen_t *g);
uint32_t mcp_stream_frames(const uint16_t *tab, uint32_t n, uint8_t *buf);

#endif

mcp15d.c — драйвер: быстрая запись, запись регистра и EEPROM, чтение пяти байт, поправка, таблицы.

/* mcp15d.c — раздел 15в: драйвер MCP4725 по I2C и генератор по таблице. Один и тот же файл собирается в прошивку
 * (armclang, Cortex-M3) и в стенд на компьютере (gcc): микроконтроллера он не знает, только порт mcp_port_*. */
#include "mcp15d.h"

/* четверть периода синуса: round(32767 × sin(π/2 × i / 64)), i = 0…64 */
static const uint16_t sin_q[65] = {
    0, 804, 1608, 2410, 3212, 4011, 4808, 5602, 6393, 7179, 7962, 8739, 9512, 10278, 11039, 11793,
    12539, 13279, 14010, 14732, 15446, 16151, 16846, 17530, 18204, 18868, 19519, 20159, 20787, 21403, 22005, 22594,
    23170, 23731, 24279, 24811, 25329, 25832, 26319, 26790, 27245, 27683, 28105, 28510, 28898, 29268, 29621, 29956,
    30273, 30571, 30852, 31113, 31356, 31580, 31785, 31971, 32137, 32285, 32412, 32521, 32609, 32678, 32728, 32757,
    32767};

static int xfer(mcp_t *d, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx)
{
    int rc = mcp_port_xfer(d->addr, tx, ntx, rx, nrx);
    d->xfers++;
    if (rc)
        d->errors++;
    return rc;
}

/* быстрая запись: 0 0 PD1 PD0 D11…D8, D7…D0 */
void mcp_fast_bytes(uint16_t code, uint32_t pd, uint8_t b[2])
{
    b[0] = (uint8_t)(MCP_CMD_FAST | (pd & 3u) << 4 | (code >> 8 & 0x0Fu));
    b[1] = (uint8_t)code;
}

int mcp_fast(mcp_t *d, uint16_t code, uint32_t pd)
{
    uint8_t b[2];
    if (code > MCP_MAX)
        return MCP_E_RANGE;
    mcp_fast_bytes(code, pd, b);
    int rc = xfer(d, b, 2, 0, 0);
    if (rc == MCP_OK) {
        d->code = code;
        d->pd = (uint8_t)(pd & 3u);
    }
    return rc;
}

/* запись регистра ЦАП (cmd = MCP_CMD_DAC) или ЦАП и EEPROM (MCP_CMD_DAC_EE): три байта */
static int write3(mcp_t *d, uint8_t cmd, uint16_t code, uint32_t pd)
{
    uint8_t b[3] = {(uint8_t)(cmd | (pd & 3u) << 1), (uint8_t)(code >> 4), (uint8_t)(code << 4)};
    if (code > MCP_MAX)
        return MCP_E_RANGE;
    int rc = xfer(d, b, 3, 0, 0);
    if (rc == MCP_OK) {
        d->code = code;
        d->pd = (uint8_t)(pd & 3u);
    }
    return rc;
}

int mcp_write_dac(mcp_t *d, uint16_t code, uint32_t pd)
{
    return write3(d, MCP_CMD_DAC, code, pd);
}

/* пять байт: состояние (RDY, POR, PD), регистр ЦАП (D11…D4, D3…D0 xxxx), EEPROM (x PD1 PD0 x D11…D8, D7…D0) */
int mcp_read(mcp_t *d, mcp_status_t *s)
{
    uint8_t b[5];
    int rc = xfer(d, 0, 0, b, 5);
    if (rc)
        return rc;
    s->rdy = (uint8_t)(b[0] >> 7 & 1u);
    s->por = (uint8_t)(b[0] >> 6 & 1u);
    s->pd = (uint8_t)(b[0] >> 1 & 3u);
    s->code = (uint16_t)(b[1] << 4 | b[2] >> 4);
    s->ee_pd = (uint8_t)(b[3] >> 5 & 3u);
    s->ee_code = (uint16_t)((b[3] & 0x0Fu) << 8 | b[4]);
    return MCP_OK;
}

int mcp_probe(mcp_t *d, uint8_t addr7)
{
    mcp_status_t s;
    d->addr = addr7;
    d->xfers = d->errors = d->ee_writes = d->polls = 0;
    int rc = mcp_read(d, &s);                           /* ответил ли кто-то по этому адресу */
    if (rc == MCP_OK) {
        d->code = s.code;
        d->pd = s.pd;
    }
    return rc;
}

/* перебрать 0x60…0x67: A0 и заводские A2, A1 бывают разными */
int mcp_find(mcp_t *d)
{
    for (uint8_t a = MCP_ADDR_BASE; a <= MCP_ADDR_LAST; a++) {
        if (mcp_probe(d, a) == MCP_OK)
            return a;
    }
    return MCP_E_NACK;
}

/* после записи EEPROM: опрашивать RDY, пока не станет 1 */
int mcp_wait_ready(mcp_t *d, uint32_t timeout_us)
{
    mcp_status_t s;
    for (uint32_t waited = 0;; waited += MCP_EE_POLL_US) {
        int rc = mcp_read(d, &s);
        if (rc)
            return rc;
        d->polls++;
        if (s.rdy)
            return MCP_OK;
        if (waited >= timeout_us)
            return MCP_E_TIMEOUT;
        mcp_port_delay_us(MCP_EE_POLL_US);
    }
}

int mcp_write_eeprom(mcp_t *d, uint16_t code, uint32_t pd)
{
    int rc = write3(d, MCP_CMD_DAC_EE, code, pd);
    if (rc)
        return rc;
    d->ee_writes++;
    return mcp_wait_ready(d, MCP_EE_TIMEOUT_US);
}

/* код → микровольты: VDD × код / 4096, опора — питание */
uint32_t mcp_code_uv(uint16_t code, uint32_t vdd_mv)
{
    return (uint32_t)(((uint64_t)code * vdd_mv * 1000u + 2048u) >> 12);
}

uint16_t mcp_uv_code(uint32_t uv, uint32_t vdd_mv)
{
    uint64_t c = (((uint64_t)uv << 12) + vdd_mv * 500u) / (vdd_mv * 1000u);
    return (uint16_t)(c > MCP_MAX ? MCP_MAX : c);
}

/* ошибка выхода (в кодах × 256) измерена на кодах c1 и c2; считаем её прямой линией */
void mcp_cal_two_point(mcp_cal_t *c, uint16_t c1, int32_t e1_q8, uint16_t c2, int32_t e2_q8)
{
    c->c1 = c1;
    c->e1_q8 = e1_q8;
    c->span = (int32_t)c2 - (int32_t)c1;
    c->den = c->span * 256 + (e2_q8 - e1_q8);
}

/* какой код записать, чтобы на выходе было напряжение кода want: c1 + ((want − c1) × 256 − e1) × span / den */
uint16_t mcp_cal_apply(const mcp_cal_t *c, uint16_t want)
{
    int64_t num = ((int64_t)((int32_t)want - c->c1) * 256 - c->e1_q8) * c->span;
    int64_t q = (num >= 0 ? num + c->den / 2 : num - c->den / 2) / c->den;
    int64_t r = c->c1 + q;
    return (uint16_t)(r < 0 ? 0 : r > (int64_t)MCP_MAX ? MCP_MAX : r);
}

/* синус по 256-точечному кругу из четверти; n — степень двойки от 4 до 256 */
int mcp_table_sine(uint16_t *tab, uint32_t n, uint16_t mid, uint16_t amp)
{
    if (n < 4u || n > 256u || (n & (n - 1u)) || mid < amp || mid + amp > MCP_MAX)
        return MCP_E_RANGE;
    for (uint32_t i = 0; i < n; i++) {
        uint32_t j = i * (256u / n), r = j & 63u;
        int32_t s = (j & 64u) ? sin_q[64u - r] : sin_q[r];
        if (j & 128u)
            s = -s;
        int32_t v = (int32_t)amp * s;
        tab[i] = (uint16_t)(mid + (v >= 0 ? (v + 16384) >> 15 : -((-v + 16384) >> 15)));
    }
    return MCP_OK;
}

int mcp_table_triangle(uint16_t *tab, uint32_t n, uint16_t lo, uint16_t hi)
{
    if (n < 2u || (n & 1u) || lo > hi || hi > MCP_MAX)
        return MCP_E_RANGE;
    for (uint32_t i = 0; i < n; i++) {
        uint32_t k = i < n / 2u ? 2u * i : 2u * (n - i);         /* 0 … n … 0 */
        tab[i] = (uint16_t)(lo + ((hi - lo) * k + n / 2u) / n);
    }
    return MCP_OK;
}

int mcp_table_saw(uint16_t *tab, uint32_t n, uint16_t lo, uint16_t hi)
{
    if (n < 2u || lo > hi || hi > MCP_MAX)
        return MCP_E_RANGE;
    for (uint32_t i = 0; i < n; i++)
        tab[i] = (uint16_t)(lo + ((hi - lo) * i + (n - 1u) / 2u) / (n - 1u));
    return MCP_OK;
}

uint16_t mcp_gen_next(mcp_gen_t *g)
{
    uint16_t v = g->tab[g->i];
    if (++g->i >= g->n)
        g->i = 0;
    return v;
}

/* таблица → кадры быстрой записи подряд: 2 × n байт, для DMA по кругу в одной передаче */
uint32_t mcp_stream_frames(const uint16_t *tab, uint32_t n, uint8_t *buf)
{
    for (uint32_t i = 0; i < n; i++)
        mcp_fast_bytes(tab[i], MCP_PD_ON, &buf[2u * i]);
    return 2u * n;
}

fw15d.c — прошивка STM32F103: I2C1 на 400 кГц, генератор по TIM2 из прерываний или потоком через DMA.

/* fw15d.c — раздел 15в: MCP4725 на STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц, таймеры APB1 — 72 МГц.
 * I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 400 кГц). Вывод A0 модуля — на GND.
 * При включении: найти микросхему, записать в EEPROM код 0 — только если там другое. Затем синус 32 точки:
 * GEN_DMA = 0 — по таймеру TIM2 10 кГц, каждая точка своей передачей из прерываний I2C (312,5 Гц);
 * GEN_DMA = 1 — одна бесконечная передача, DMA1 канал 6 по кругу кладёт кадры в I2C1->DR (694,4 Гц).
 * Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "mcp15d.h"

#define CPU_HZ          72000000u
#define APB1_MHZ        36u
#define I2C_TIMEOUT_US  1000u
#ifndef GEN_DMA
#define GEN_DMA         0
#endif
#define GEN_N           32u
#define GEN_FS_HZ       10000u

static mcp_t dac;
static uint16_t tab[GEN_N];
static mcp_gen_t gen;
static uint8_t frame[2], stream_buf[2u * GEN_N];
static volatile uint8_t busy, sent;
volatile uint32_t overruns, bus_errors;

static int expired(uint32_t t0, uint32_t us)
{
    return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}

void mcp_port_delay_us(uint32_t us)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!expired(t0, us)) {
    }
}

static void i2c1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u;    /* PB6, PB7 — альтернативная, открытый сток (1111) */
    I2C1->CR1 = 0;
    I2C1->CR2 = APB1_MHZ;
    I2C1->CCR = I2C_CCR_FS | 30u;                       /* DUTY = 0: 36 МГц / (3 × 30) = 400 кГц */
    I2C1->TRISE = APB1_MHZ * 300u / 1000u + 1u;         /* 11 */
    I2C1->CR1 = I2C_CR1_PE;
}

static int sr1_wait(uint16_t mask)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!(I2C1->SR1 & mask)) {
        if (I2C1->SR1 & I2C_SR1_AF)
            return MCP_E_NACK;
        if (expired(t0, I2C_TIMEOUT_US))
            return MCP_E_BUS;
    }
    return MCP_OK;
}

static int fail(int rc)
{
    I2C1->CR1 |= I2C_CR1_STOP;
    I2C1->SR1 = (uint16_t)~I2C_SR1_AF;
    I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
    return rc;
}

static int start(uint8_t addr_rw)
{
    int rc;
    I2C1->CR1 |= I2C_CR1_START;
    if ((rc = sr1_wait(I2C_SR1_SB)) != MCP_OK)
        return rc;
    I2C1->DR = addr_rw;
    return sr1_wait(I2C_SR1_ADDR);
}

/* запись ntx байт или чтение nrx ≥ 3 байт (RM0008, с. 763: три последних байта — через BTF) */
int mcp_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx)
{
    int rc;
    uint32_t t0 = DWT->CYCCNT;
    if ((ntx && nrx) || (nrx && nrx < 3u))
        return MCP_E_BUS;
    while (I2C1->SR2 & I2C_SR2_BUSY) {
        if (expired(t0, I2C_TIMEOUT_US))
            return MCP_E_BUS;
    }
    if (ntx) {
        if ((rc = start((uint8_t)(addr7 << 1))) != MCP_OK)
            return fail(rc);
        (void)I2C1->SR2;
        for (uint32_t i = 0; i < ntx; i++) {
            if ((rc = sr1_wait(I2C_SR1_TXE)) != MCP_OK)
                return fail(rc);
            I2C1->DR = tx[i];
        }
        if ((rc = sr1_wait(I2C_SR1_BTF)) != MCP_OK)
            return fail(rc);
        I2C1->CR1 |= I2C_CR1_STOP;
        return MCP_OK;
    }
    I2C1->CR1 |= I2C_CR1_ACK;
    if ((rc = start((uint8_t)(addr7 << 1 | 1u))) != MCP_OK)
        return fail(rc);
    (void)I2C1->SR2;
    for (uint32_t i = 0; i + 3u < nrx; i++) {
        if ((rc = sr1_wait(I2C_SR1_RXNE)) != MCP_OK)
            return fail(rc);
        rx[i] = (uint8_t)I2C1->DR;
    }
    if ((rc = sr1_wait(I2C_SR1_BTF)) != MCP_OK)         /* N−2 в DR, N−1 в сдвиговом регистре */
        return fail(rc);
    I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
    rx[nrx - 3u] = (uint8_t)I2C1->DR;
    if ((rc = sr1_wait(I2C_SR1_BTF)) != MCP_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_STOP;
    rx[nrx - 2u] = (uint8_t)I2C1->DR;
    if ((rc = sr1_wait(I2C_SR1_RXNE)) != MCP_OK)
        return fail(rc);
    rx[nrx - 1u] = (uint8_t)I2C1->DR;
    return MCP_OK;
}

/* ── генератор по таймеру: TIM2 начинает передачу, прерывания I2C1 её ведут ── */
void TIM2_IRQHandler(void)
{
    TIM2->SR = (uint16_t)~TIM_SR_UIF;
    if (busy) {                                         /* прошлая точка ещё на шине — эту пропускаем */
        overruns++;
        return;
    }
    mcp_fast_bytes(mcp_gen_next(&gen), MCP_PD_ON, frame);
    busy = 1;
    sent = 0;
    I2C1->CR2 |= I2C_CR2_ITEVTEN | I2C_CR2_ITBUFEN | I2C_CR2_ITERREN;
    I2C1->CR1 |= I2C_CR1_START;
}

void I2C1_EV_IRQHandler(void)
{
    uint16_t sr1 = I2C1->SR1;
    if (sr1 & I2C_SR1_SB) {
        I2C1->DR = (uint8_t)(dac.addr << 1);
    } else if (sr1 & I2C_SR1_ADDR) {
        (void)I2C1->SR2;
        I2C1->DR = frame[0];
        sent = 1;
    } else if ((sr1 & I2C_SR1_TXE) && sent == 1u) {
        I2C1->DR = frame[1];
        sent = 2;
        I2C1->CR2 &= (uint16_t)~I2C_CR2_ITBUFEN;        /* дальше нужен только BTF */
    } else if (sr1 & I2C_SR1_BTF) {
        I2C1->CR1 |= I2C_CR1_STOP;
        I2C1->CR2 &= (uint16_t)~(I2C_CR2_ITEVTEN | I2C_CR2_ITERREN);
        busy = 0;
    }
}

void I2C1_ER_IRQHandler(void)
{
    I2C1->SR1 = 0;                                      /* AF, ARLO, BERR снимаются записью нуля */
    I2C1->CR1 |= I2C_CR1_STOP;
    I2C1->CR2 &= (uint16_t)~(I2C_CR2_ITEVTEN | I2C_CR2_ITBUFEN | I2C_CR2_ITERREN);
    bus_errors++;
    busy = 0;
}

static void gen_timer_start(uint32_t fs_hz)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    TIM2->PSC = CPU_HZ / 1000000u - 1u;                 /* 72 МГц → тик 1 мкс */
    TIM2->ARR = 1000000u / fs_hz - 1u;                  /* 10 кГц — каждые 100 мкс */
    TIM2->DIER = TIM_DIER_UIE;
    NVIC_SetPriority(I2C1_EV_IRQn, 0);
    NVIC_SetPriority(I2C1_ER_IRQn, 0);
    NVIC_SetPriority(TIM2_IRQn, 1);
    NVIC_EnableIRQ(I2C1_EV_IRQn);
    NVIC_EnableIRQ(I2C1_ER_IRQn);
    NVIC_EnableIRQ(TIM2_IRQn);
    TIM2->CR1 = TIM_CR1_CEN;
}

/* ── потоком: одна передача без конца, DMA1 канал 6 (I2C1_TX) по кругу ── */
static int gen_dma_start(void)
{
    int rc;
    uint32_t len = mcp_stream_frames(tab, GEN_N, stream_buf);
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    DMA1_Channel6->CCR = 0;
    DMA1_Channel6->CPAR = (uint32_t)&I2C1->DR;
    DMA1_Channel6->CMAR = (uint32_t)stream_buf;
    DMA1_Channel6->CNDTR = len;
    DMA1_Channel6->CCR = DMA_CCR6_MINC | DMA_CCR6_CIRC | DMA_CCR6_DIR | DMA_CCR6_EN;   /* память → DR, по кругу */
    I2C1->CR2 |= I2C_CR2_DMAEN;                         /* до события ADDR: RM0008, с. 769 */
    if ((rc = start((uint8_t)(dac.addr << 1))) != MCP_OK)
        return fail(rc);
    (void)I2C1->SR2;                                    /* ADDR снят — дальше байты кладёт DMA */
    return MCP_OK;
}

int main(void)
{
    mcp_status_t st;
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    i2c1_init();
    while (mcp_find(&dac) < 0)
        mcp_port_delay_us(100000u);
    /* значение при включении — 0 В; EEPROM пишем, только если там другое: ресурс записи не бесконечен */
    if (mcp_read(&dac, &st) == MCP_OK && (st.ee_code != 0u || st.ee_pd != MCP_PD_ON))
        (void)mcp_write_eeprom(&dac, 0, MCP_PD_ON);
    (void)mcp_table_sine(tab, GEN_N, 2048, 2000);
    gen.tab = tab;
    gen.n = GEN_N;
    gen.i = 0;
    if (GEN_DMA)
        (void)gen_dma_start();
    else
        gen_timer_start(GEN_FS_HZ);
    for (;;)
        __WFI();
}

Дальше: раздел 16 «UART» — самый первый интерфейс, который подключают.

Открыть в интерактивном справочнике