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). Выход между записями
не трогает шину — код держится в регистре.
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 В:
- три нижних кода дают 0 В: отрицательное смещение срезано землёй;
- на коде 2048 ошибка −4,68 шага, наибольшая — 10,39 шага у кода 4082;
- наибольшая ошибка ступеньки (DNL) — 0,84 шага.
Как и у АЦП (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 Итоги
- MCP4725 — 12 бит, один выход, опора — питание: шаг 805,66 мкВ при 3,3 В и 1220,7 мкВ при 5 В; адрес 0x60 или 0x62 плюс A0 — искать перебором.
- Быстрая запись — 2 байта, 72,5 мкс на 400 кГц; запись регистра ЦАП — 95 мкс; чтение пяти байт — 140 мкс.
- Отклонение питания на 50 мВ — 31 шаг на середине шкалы; АЦП на том же питании этого не видит, абсолютные вольты требуют меры питания.
- Поправка по двум точкам сняла ошибку модели с 10,39 до 2,11 шага.
- По таймеру на 400 кГц — до 13 793 точек/с, синус 32 точки — 431 Гц; потоком через DMA — 22 222 точки/с, 694,4 Гц, но шина занята.
- Отражения ступенек на 32 точках — −29,8 дБ; RC 1 кОм, 100 нФ даёт −45,5 дБ.
- EEPROM — 25 мс на запись с ожиданием
RDY, писать редко; power-down стягивает выход на землю через 1, 100 или 500 кОм.
Полные исходники
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» — самый первый интерфейс, который подключают.